CREATION DATE: 24-06-2005.
ВСЕ "<A NAME=...>
" ОБЯЗАТЕЛЬНО снабжать
"</A>
"!!!
13.05.2024: даты при действиях/записях писать через точку, "DD.MM.YYYY", а в "ссылках" на другие записи через тире, "DD-MM-YYYY". Так grep по "DD.MM.YYYY" найдёт оба варианта, но при надобности можно запросить конкретно либо действия, либо ссылки.
Сей файл начат для писания трудов по созданию CXv4.
10.06.2015: sw4cx/
26.06.2005: (начато)
privptr
, а не "usrptr", "userptr", etc.
(@21.02.2007)
Upd@20.05.2009: конкретно в сервере указатель на приватную
структуру драйвера -- devptr
, а privptr -- в callback'ах
отдельная фича.
Upd@31.08.2009: а в самих драйверах/ручках/etc. указатель на их
экземпляр (т.е., devptr/privptr, cast'ированный к типу
*privrec_t
) ВСЕГДА называется me
--
кратко и точно.
connect()
-- O_NONBLOCK.Upd@20.05.2007: хитрее -- раздельные библиотеки собственно cxlib (махинации с содержимым пакетов) и "cx-transport", могущая быть реализована как на SIGIO, так и на fdiolib'е или как-нибудь еще.
05.07.2008: на эту тему см. раздельчик "In-server channel API" в "Идеях".
(Видимо, на каждый канал иметь поле "locker", в котором писать ID соединения, и разрешать запись только при locker==0 || locker==this_client. Ну а на readonly -- простейшая проверка прямо при обработке fork'а.)
Кроме избавления от чисел, это даст возможность "относительной адресации" в клиентах ("refbase" -- к базовому адресу вмещающего элемента через "." добавляется имя канала, указанное в ручке).
Также это позволит делать "программы-приборы" -- всякие adc200 и d16, которым указывается имя блока, а они уже к нему конкатенируют имена каналов.
stroftime()
,
strcurtime()
, на крайняк -- собственная
"locale-independent" замена для ctime()
; сама же
ctime()
не используется НИКОГДА!
time()
. Безо всяких попыток
парсить строку, когда-то созданную ctime()
'ом.
28.06.2007: добавлен отдельный тип
cx_ival_t
, чтобы использовать его вместо int32 везде, где
надо указывать "значение".
Поскольку в будущем предполагается поддержка других типов, то сразу
про запас добавлены int64 cx_lval_t
и double
cx_dval_t
.
20.04.2020: из cx.h убраны никогда не
использовавшиеся cx_ival_t
, cx_lval_t
,
cx_dval_t
.
Кроме того, поскольку возрасты у нас теперь используют другой подход
-- миллисекунды, передаваемые хитрым способом -- то тип
tag_t
удален.
01.04.2011@Снежинск-каземат-11: в cx.h добавлена
reprof_cxdtype()
.
21.11.2013: а почему у нас cxdtype_t
сделан ОДНОбайтовым?
И вообще, а когда этот тип появился? Похоже, по результатам
обсуждений касательно карботрона HITS, как этакая элегантная штучка;
оно впервые есть в w20090715.tar.gz (вместе с
rvage_t
, который, кстати, возможно будет выкинут в связи с
timestamp'ами (14.06.2014: да,
rvage_t
удалён за пришедшей
ненадобностью.)), на cx.h дата 2009-07-08.
30.08.2014: а ведь идеи в его основе изложены в разделе
"Насчет типов данных" от 26-07-2008.
23.09.2019: всё, переделан с ОДНОбайтового на ЧЕТЫРЁХбайтовый (ещё месяц назад, а сегодня проверено и доделано).
22.07.2007: программа такова:
Это позволит прямо сейчас, не трогая сервер и протокол, дать возможность "писать программы самим физикам" -- т.е., просто составлять группировки в текстовом виде.
Так можно будет прозрачно начать заменять сервера по очереди.
(А по ходу дела можно будет добавить и cda_d_epics.c, тогда программы смогут доступаться и к гусиным данным.)
Это позволит получать выгоду с нового кода прямо по мере его появления, а не дожидаться завершения ВСЕГО пакета CXv4.
Поскольку в OpenCV своя логика работы и какой-то свой "основной цикл" (там одних библиотек ldd показывает 79 штук! основная, видимо, gtk+ -- т.е., GNOME), так что к нему интерфейсить нетривиально, а проще вызывать cdaclient.
P.S. Аналогично можно сделать совместимыми CXDTYPE_UCTEXT с CXDTYPE_INT32/CXDTYPE_UINT32 -- уже чисто "для удобства", тут вряд ли какая экономия на длине будет.
P.P.S. И отдельный вопрос: а что с {R,D}-conversion? С одной стороны, при участии строковых типлв хоть с одной стороны -- само понятие "преобразования" смысл теряет. С другой -- а если это именно просто "по дурости" тип перепутан, как обычно в Си'шных программах бывает?
05.09.2019: понятно, что такую конверсию если делать, то нужно делать во ВСЕХ местах: cda_core.c (2 места) и cxsd_hw.c (тоже 2 места). И организация кода во всех 4 местах похожа.
Но просмотр оного кода показывает, что добавить туда "просто лишнее условие" ("хоть один из типов -- REPR_INT, и хоть один другой -- REPR_TEXT, и размеры совпадают") не так-то просто -- там длинная цепочка if()else-if()...
Может, просто рассматривать TEXT наравне с INT? Тогда и вопрос "что с {R,D}-conversion" решится автоматом.
05.03.2020: да, делаем.
Побудительный мотив -- сложности при взаимодействии с EPICS, где бардак с текстовыми переменными (строки большой длины реализуются как массивы char (т.е., БАЙТОВ)).
Проект:
memcpy()
-- это
автоматом снимет проблему "а что с {R,D}-conversion?".
Реализация:
cda_dat_p_update_dataset()
.
is_text_int_compat
, которой присваиваем значение "да, это
именно такое преобразование" --
-- и затем используем её в обоих условиях.is_text_int_compat = (ssiz == size) && ((srpr == CXDTYPE_REPR_INT && repr == CXDTYPE_REPR_TEXT) || (srpr == CXDTYPE_REPR_TEXT && repr == CXDTYPE_REPR_INT));
Проверяем -- да, работает!!! Можно cdaclient'ом мониторировать байтовые каналы как текстовые и наоборот, и оно печатает данные именно в запрошенном виде. Ура!!!
DoStoreWithConv()
; но тут
неясно, что бы можно сделать:
DO_RD_CONV
и так сбрасывается.
NTVZ_DTYPE
не трогается.
Но, судя по отсутствию проблем ранее, он вроде как и не при делах.
А, во -- нашёл!
cda_snd_ref_data()
, а
тот для REF_TYPE_CHN вызывает
SendOrStore(,,,,DO_RD_CONV
)
-- т.е., никакого
NTVZ_DTYPE.
NTVZ_DTYPE
же используется ТОЛЬКО
cda_process_ref()
-- т.е., интерфейсом, принципиально
принимающим на вход только double
, и никак ни TEXT, ни даже INT
сгенерить не могущим.
Вывод: тут ничего делать НЕ надо.
StoreForSending()
.
Тут проверка на совместимость отделена от собственно отправки, и
выполняется в IsCompatible()
, вызываемой из
ConsiderRequest()
.
Соответственно, и реализация состоит из 2 частей:
IsCompatible()
дополнительную
OR-альтернативу, идентичную значению is_text_int_compat
выше.
StoreForSending()
к блоку "Identical" также
добавлена такая же OR-альтернатива, но для красоты всё условие
отрефакторено, т.к. в обоих случаях там обязательным является совпадение
размеров, так что сейчас оно выглядит так:
ssiz == size && (srpr == repr || (srpr == CXDTYPE_REPR_INT && repr == CXDTYPE_REPR_TEXT) || (srpr == CXDTYPE_REPR_TEXT && repr == CXDTYPE_REPR_INT))
ReturnDataSet()
... Анализ:
cda_dat_p_update_dataset()
, ...
StoreForSending()
.
Вывод из анализа: да просто заменяем условие ровно таким же образом.
Итого -- вроде сделано. Теперь надо тестировать все 3 ветви (ну ладно, оставшиеся 2).
05.03.2020: P.S. Замечание: эта реализация конверсии между char[] и byte[] ставит крест на идее "а сделать бы конверсию между числами и строками" (см. за 23-10-2012).
06.03.2020: посмотрел в коде
console_cda_util.c::ParseDatarefSpec()
-- флаг
'+' НЕ применяется к не-INT-типам (к ним не плюсуется
is_unsigned_mask
). А в
cxsd_db_via_ppf4td.c::ParseChangroup()
попытка указать
'+' и вовсе приводит к ошибке (впрочем, реальный ТИП такой
указывать незачем, так что эта часть иррелевантна). Но кто мешает это
поведение исправить?
Соответственно, можно использовать простенькое "угадывание". Тем более, что это "угадывание" будет производиться уже перед выводом о несовместимости типов, так что дополнительная как бы потеря производительности тут будет малозначимой.
Впрочем, ГЛАВНАЯ проблема на данном пути -- то, что у нас на уровне cxsd и cda нету никаких enum/lookup. Т.е., разговор беспредметен.
23.07.2007: постановка проблемы:
subsysdescr_t
добывалось еще ДО вызова
XhInitApplication()
, и оному передавались уже известные на
тот момент app_name
и app_class
.
CdrLoadSubsystem()
,
должно мочь указываться в командной строке. А командную строку
следует "руками" смотреть только уже ПОСЛЕ вызова Xh/Xt, который
перетряхнет argv[].
Решение:
XhInitApplication()
'у всегда передается
настоящее имя программы -- т.е., то, кто она есть -- грубо говоря, ее
__FILE__ с отрезанным расширением.
Это реально влияет только на то, из файла с каким именем будут браться app-defaults, и нам вообще слабо нужно.
XhCreateWindow()
будет вызываться уже ПОСЛЕ
добычи описания, и уже с правильными app_name
и
app_class
-- которые именно там уже по-настоящему нужны.
09.08.2007: ага, щас -- "влияет только на то, из файла с каким именем будут браться app-defaults, и нам вообще слабо нужно" -- если бы!!! Похоже, ключ -geometry работает именно через app_name или app_class -- видимо, кладет в xrm-db строчку вида "name|class.geometry:...". Мда...
Казалось бы, надо поточнее разобраться с конкретными ролями app_name и app_class, и кем-нибудь из них пожертвовать. Но вся проблема в том, что для geometry важен именно app_name, а если верить замечаниям в разделе по cx-starter'у в bigfile-0001.html, то нам для корректного различения окон нужен специфичный-для-окна также именно app_name (а нафига вообще app_class нужен?). Так что, проблема курицы и яйца остается. Надо думать дальше.
07.05.2008: крамольная мысль -- а не прирезать ли нафиг эту курицу, а? Нафига она реально нужна?
Т.е. -- либо программа запускается через symlink и с самого начала знает свои app_{name,class}, либо -- она запускается напрямую и тогда считается уже за мультиклиента. Паллиативное решение -- требовать, чтобы имя подсистемы указывалось ОБЯЗАТЕЛЬНО ПЕРВЫМ параметром. Но это далеко не всегда возможно (например, при "!#pultclient" -- точно нет), да и толку от этого будет мало -- тогда уж лучше симлинки делать.
...а вот как раз "!#pultclient"-то и объясняет "нафига" -- для именно этого. Мда, послать проблему было бы просто, а придется ее решать...
20.08.2015@пляж: пара идей:
Например, понимать ключи "-class NNN" или в варианте "/class NNN" -- требуя, чтобы они были в начале.
20.08.2015@вечер: а еще
XhInitApplication()
, и если оно !="pult", то
выполнять загрузку группировки прямо там -- тогда можно будет в Xh/Xt
подсунуть уже конкретные известные app_name/app_class.
Поскольку "клиенты" cx-starter'а будут запускаться через symlink'и в pult/bin/, то для них это вполне сработает. А то, что запускается руками через "pult ИМЯ.subsys" -- тем и неважно, их starter'у искать не надо.
21.08.2015: смотрим по пунктам.
-class
CLASS_NAME
. Результаты:
xterm -class 'abc-def' -xrm 'abc-def*background:red'
-- ресурс работает (фон красный).
xterm -class 'abc.def' -xrm 'abc.def*background:red'
-- не работает.
xterm -class 'abc*def' -xrm 'abc*def*background:red'
-- тоже не работает.
xterm -class 'abc:def' -xrm 'abc:def*background:red'
-- как ни странно, тоже нет.
xterm -class 'abc!def' -xrm 'abc!def*background:red'
-- опять работает.
Похоже, то ли Xt/Xrm как-то отдельно отфильтровывает '.' и '*' (и еще почему-то ':'), то ли поиск в БД ресурсов устроен так, что эти символы там интерпретируются особо (как разделители) и финт просто не проходит.
...все же прочие не-alnum-символы -- '!', ',', ';' -- на здоровье, разрешены. Но для наших целей от этого ни горячо, ни холодно.
-name NNN
и так Xt слопает (хотя CX'ные утилиты его почему-то игнорируют). А
ключ -class NAME
Xt не понимает (видимо, это чисто
xterm'овская фишка. А указывать эти ключи -- лишний геморрой,
которого как раз бы и не хотелось.
21.08.2015: а вот п.3 выглядит разумно. И даже более того: если argv[1] выглядит именем файла (как минимун -- НЕ '-'-ключом), то чё б не попробовать заиспользовать его сразу?
16.11.2015: сделано!!! По проектам п.3 и "если argv[1] выглядит именем файла...".
Сделано просто "чтоб работало", а с красотой кода изрядные проблемы -- ОЧЕНЬ криво и непоследовательно. Так что надо будет еще:
13.08.2007: с одной стороны, несравненно
красивее привязка к подсистеме -- при этом не приходится делать
дурацкую трансляцию knob->окно->структура. И средства для этого
теперь есть -- get_knob_subsys()
.
С другой же стороны, привязка к XhWindow позволит делать многооконные программы -- с НЕСКОЛЬКИМИ grouping'ами, каждое из которых имеет собственное окно и собственный набор служебных окошек.
Так что -- пока оставим привязку chl_privrec_t
к
XhWindow.
21.08.2015: по факту, сейчас (18-08-2015) сделана привязка к subsystem. Кривовато, конечно -- это там как бы "ad hoc", а не идеологически -- но всё же.
ChlRunSubsystem()
использование DSTN_WINOPTIONS -- парсинг
плюс использование флагов notoolbar, nostatusline, compact и
свежесделанного в CXv2 resizable.
18.01.2016: пока в простом варианте -- без указывабельности содержимого клиентом, чисто в интересах pult (и для отладки clientside-сохранения/чтения режимов).
stdtoolslist[]
.
CmdProc()
broadcast'инг команды по группировке был и
раньше, а теперь мы его дополняем реакцией на стандартные действия,
вызываемой, если команда НЕ была обработана (т.е., ручки в дереве могут
команду перехватить).
#define
'атся в
Chl.h под именами CHL_STDCMD_nnn
, сами строки имеют
префикс "chl".
Раньше в нём были только ChlRunSubsystem()
да
ChlLastErr()
.
19.01.2016: продолжаем:
ChlHandleStdCommand()
, чтоб программы могли вызывать её из
своих обработчиков.
ChlRunSubsystem()
добавлены параметры:
toolslist
-- даёт возможность программе указать своё
содержимое toolbar'а. При ==NULL используется stdtoolslist[].
commandproc
-- сохраняется в privrec'е (09.02.2016: как
upper_commandproc
) и дёргается в
Chl_app.c::CmdProc()
между broadcast'ингом и вызовом
ChlHandleStdCommand()
.
Это и есть возможность программам ставить свои обработчики -- ранее её не было.
XhCommandProc
сделан
возвращающим int
вместо былого void (но сам Xh результат,
естественно, игнорирует).
09.02.2016: сделана Chl'ная часть сохранения/чтения режимов, творческим копированием из v2.
ChlHandleStdCommand()
.
stddlgs_rec_t
, содержащая
пару CxWidget'ов для диалогов.
ChlSaveWindowMode()
и
ChlLoadWindowMode()
, являющиеся переходниками к Cdr.
ChlRecordEvent()
.
28.07.2016: некоторые комментарии:
02.08.2016: в ту же степь: а ведь лучше было бы, чтоб у каждой строчки могла указываться как крупная пиктограмма, так и мелкая, и отдельно указывалась бы крупность/мелкость тулбара, а использовалась бы по возможности нужная пиктограмма (при её отсутствии -- другая).
В xh_actdescr_t
такого поля нет, но, поскольку
формат всё равно отличается от v2 (из-за строковости команд), то можно
спокойно это поле добавить.
*_src
, а это не вполне корректно: ведь при realize туда
еще префикс/база конкатенируются.
Правильнее будет показывать то, что у ссылки (ref) лежит в
ri->reference
. Для чего в cda надо завести
соответствующий вызов.
05.05.2014: чуть хитрее: может быть ситуация, когда в ref НИЧЕГО не лежит -- если ссылка "ошибочная" (CDA_DATAREF_ERROR). Например, при указании несуществующего local::-канала.
В таком случае НАДО отобразить именно "исходник", с какой-то пометкой "невалидна". Т.е., 3 варианта, в зависимости от ref:
reference
.
05.05.2014: ...а может, всё-таки НЕ возвращать на CDA_DAT_P_ERROR-каналы CDA_DATAREF_ERROR, а делать их вечно-мёртвыми (с CXCF_FLAG_NOTFOUND)?
Да, так и сделано, а классификация выше -- "withdrawn".
Теперь вопрос, как сделать, чтобы такие РУЧКИ сразу были чёрными? Ведь Process для них и не вызовется...
05.02.2015: сделано очень просто (это прерогатива
Cdr): в CdrRealizeKnobs()
после получения
rd_ref
проверяется, что если
CDA_DATAREF_ERROR
CXCF_FLAG_NOTFOUND
,
KNOBSTATE_NOTFOUND
(заменяя там
умолчательное JUSTCREATED), и этого достаточно -- ведь именно по этому
состоянию и будет сделано первоначальное расцвечивание при создании
ручки.
Теперь прекрасно чернеет с самого начала.
18.02.2015: вопрос на 9 месяцев назад: а ПОЧЕМУ, собственно, "НЕ возвращать на CDA_DAT_P_ERROR-каналы CDA_DATAREF_ERROR"?
cda_add_chan()
и в методе
new_chan()
: первая может вернуть клиенту "облом", а вторая
лишена такой возможности.
Хотя граница между ними проведена довольно волюнтаристски.
Короче -- сделано (вёрнуто): при обломе возвращается
CDA_DATAREF_ERROR
.
Как это повлияет на прочие компоненты (Cdr_treeproc, Chl_knobprops), еще надо будет смотреть.
21.12.2015: аналогично -- в
cda_add_formula()
при возврате из pfl->create()
результата "ошибка" -- <0 -- эта ошибка не доводится до клиента. Что
весьма неприятно. (Видимо, код в обоих вызовах изначально был одинаковым, но
в add_chan()'е было улучшено, а в add_formula() -- нет.)
22.12.2015: да, тоже исправлено, теперь по облому регистрации ошибка возвращается наверх.
Кстати, проверил за компанию и
cda_add_varchan()
-- там эта проблема отсутствует, поскольку
никаких плагинов не предусмотрено.
При запрещенности смены, естественно, просто показывать диапазон как и сейчас.
28.06.2014: да, переделываем, по образу и подобию реализованного вчера в v2 (только уже безо всяких #if RANGES_INLINE, а сразу).
Заодно, кстати, выкинуто fqd_inp
("Graph frqdiv") --
давно ясно, что сама концепция изжилась.
15.01.2015: сделана отработка кнопки [Ok], с
надлежащим сохранением в u.k.params[].value
и взведением
тамошних флагов .modified
.
Пока только для диапазонов, шага и grpcoeff. А надо б еще для прочих параметров.
Плюс, пока не сделано:
PropsChg()
.
16.01.2015: в продолжение вчерашнего:
PropsChg()
добавлен -- скопировано из Cdr'ного
RefStrsChgEvproc()
(но БЕЗ вызова parent'ова
ChildPropsChg() -- тут оно ни к чему).
12.11.2015: вылез косячок "новой" (28-06-2014) схемы
отображения диапазонов: при /fixedranges
(DATAKNOB_B_RANGES_FXD
) диапазоны не показываются вовсе, что
неприятно. А желательно бы показывать, но, например, просто нередактируемыми
полями (просто editable:=False, БЕЗ sensitive:=False).
01.12.2014: имевшаяся строчка "Age (ms)" пока оставлена, хотя там всегда 0. Смысл -- есть мыслишка, не выводить ли туда именно возраст в миллисекундах (или уж в секундах -- SSS.msc?), чтоб удобнее было ориентироваться.
02.12.2014: да, сделано отображение возраста в
человекочитаемом виде, в разных форматах в зависимости от значения --
взято из v2'шного pzframe_gui.c::ShowStats()
.
А поле dataknob_knob_data_t.age_ms
, никогда не
использовавшееся, удалено.
16.09.2015: добавлено отображение fresh_age, для возможности разбирательства с работой этого механизма (сегодня потребовалось).
Отображается через дробь -- age/fresh_age. Сделано с #if-условием,
по значению SHOW_FRESH_AGE
.
21.03.2016: сделано, что бредовые отрицательные возрасты (возникающие при несовпадении настроек таймзон на клиенте и сервере), тоже отображаются -- вместо былой пустоты, чтоб понимать, что происходит.
comment
и units
.
Понадобилось для проверки работы модуля cda_d_vcas.c
Причина -- vsdc2 в GID25 на ВЭПП-4/К500: там и исходные числа (single) от девайса мелкие (e-8), и r, на который они делятся тоже (e-7), поэтому при %f они отображались нулями. А %g специально сделан для таких случаев, он сам выбирает наилучший формат (%f или %e, в зависимости от экспоненты).
11.05.2016: в основном скопировано с v2. Тонкости/отличия:
Замечание: поиск histplot_noop'ом делается через
datatree_find_node()
, которому указывается адрес ИЩУЩЕГО, а не
корень (хотя его тоже можно).
В любом случае -- имя корневого узла не используется никак.
А неактуальные -- показывать так, словно их нету (оно так и у KNOB'ов бывает).
k->w==NULL
-- клик при
этом делается по "отражению" такой ручки в другом месте, типа histplot'а --
программа падает по SIGSEGV (подробнее см. в разделе по histplot'у за
сегодня).
12.04.2019: в _ChlShowProps_m()
был
наивно-простой код -- прямо в объявлениях переменных выполнялась
инициализация:
XhWindow win = XhWindowOf(k->w); dlgrec_t *rec = ThisDialog(win);
Решено было заменить оное на "поиск вверх по дереву первого узла, у которого w!=NULL":
XhWindow win; dlgrec_t *rec; DataKnob src_of_w; for (src_of_w = k; src_of_w != NULL && src_of_w->w == NULL; src_of_w = src_of_w->uplink) ; if (src_of_w == NULL || src_of_w->w == NULL) return; win = XhWindowOf(src_of_w->w); rec = ThisDialog(win);
Выглядит уродливовато, но проблему решило.
Аналогично модифицированы и _ChlShowBigVal_m()
с
_ChlToHistPlot_m()
.
Обновление сделано так же, раз в 1 секунду.
_ChlShowBigVal_m()
(аналогично KnobProps'ову).
Ничего продвинутого -- что задумывалось 04-03-2006, с текстовым описанием и возможностью указывать своё в subsys-файлах -- не сделано (имевшаяся в v2'шном часть при копировании удалена). И, вероятно, никогда не будет сделано -- надобности такой нет из-за общего смещения в сторону питонопрограмм.
Старый вариант оставлен, но закрыт #if
'ом по
MAY_USE_RUSSIAN_KOI8
.
Сделано одновременно и унифицированно с cx-starter'ом.
15.03.2006: поле currflags
сделано
"всеобщим" -- т.е., сразу полем структуры knobinfo_t
.
Смысл -- это позволит иметь абсолютно унифицированную работу как с
knob'ами, так и с контейнерами, и с LOGT_BIGC, и даже с LOGT_USER. И
-- необязательно будет где-то хранить значение, вернутое
*Process*()'ом, поскольку оно всегда будет "под рукой".
12.05.2006: добавлено поле
knobinfo_t.placement
-- для реализации "layoutinfo".
26.10.2006@PCaPAC-2006: У всех сущностей в hw-db (точнее -- у контроллеров (крейтов) и устройств) надо завести еще поле "location" -- текст, где писать комнату/угол/etc.
А может, и у knob'ов тоже?
25.11.2006: ясно, что нужны И поле "placement/layoutinfo", И поле для обозначения привязки к физическому месту; последнее -- возможно, как-то ссылающееся на GIS.
Чтобы не было непоняток и конфликтов имен, называем их:
.layinfo
-- "layoutinfo", бывшее "placement".
.geoinfo
-- "location", географическая ссылка.
07.06.2007: еще в .geoinfo
надо
уметь указывать НЕСКОЛЬКО позиций, минимум 2:
Видимо, надо там иметь какой-то разделитель, через который и указывать. ',', ';' и '/', скорее всего, будут заняты, но вот '|' вполне может оказаться свободным.
18.06.2007: содержимое Knobs_typesP.h переехало в datatreeP.h, так что тут раздел закрываем.
29.07.2007: и вообще старые файлы Knobs_typesP.h* удалены. Если понадобятся -- последняя версия есть в star:viper-archive/20070727.tar.bz2.
CreateKnob()
и
RegisterKnobset()
.
18.06.2007: сделано, highlights таковы:
Следствие: "умолчательный" вид (text для KNOB'ов, grid для CONT'ов) должен быть первым в списке.
look
"" или NULL приводит именно к виду по
умолчанию.
Следствие: декорации должны идти в списке ПОСЛЕ настоящих ручек.
options
.
О VMT:
Т.е., реально VMT -- это скорее "описатель".
Create()
и Destroy()
объявлены
прямо в "универсальной" dataknob_unif_vmt_t
, которая во
все остальные типы VMT входит отдельным полем.
ref_count
.
extern knobs_knobset_t *first_knobset_ptr;
а уж реализация должна реально этот указатель заимевать (т.е., уже БЕЗ
extern'а), уставляя его в ссылку на свой набор.
Таким образом, для использования некоей реализации достаточно будет ее прилинковывать.
name==NULL
.
28.06.2007: с принятым подходом -- что "при ненахождении указанного вида оно берет просто ПЕРВУЮ найденную ручку указанного типа" -- есть неудобство: ведь регистрируемые-программой knobset'ы будут иметь приоритет, так? И, следствие -- умолчательным будет становиться какой-нибудь довольно левый тип ручки.
Поэтому теперь принят другой подход: в качестве умолчательного вида
ищется вид с именем "-". Т.е., например, для TEXT'а будет две
записи -- "text" и "-" (это "имя" определяется символом
DEFAULT_KNOB_LOOK_NAME
. Так "умолчательность" отделена
от порядка поиска.
Такой подход дает явный дополнительный плюс -- виды, работающие в качестве умолчательных, могут в методах "-_Create_m()" вызывать собственно "тела создателей", игнорируя потенциально-чужие поля в определениях ручек -- options, paramN, strN, и т.п. (а также -- поля layinfo в содержащихся ручках).
12.07.2007: поправка -- сделанная тогда
архитектура была изрядно кривой, поскольку предусматривала, что
knobs_knobset_t
содержит указатель на массив из
dataknob_unif_vmt_t
. Естественно, это бы не работало,
поскольку нам нужны РАЗНЫЕ виды VMT в одном "массиве".
Исправлено -- теперь там ссылка на массив указателей на
dataknob_unif_vmt_t
(которыми могут работать указатели и
на другие dataknob_???_vmt_t).
Следствие -- концом набора может (и, по-хорошему, ДОЛЖОН быть
указатель NULL, а не не только VMT с name==NULL
.
04.12.2013: парсинг спецификации-списка "тип1,тип2,..." был сделан криво: нормально оно хватало только первый вариант, поскольку перед следующей итерацией забывало перепрыгнуть через ','.
DestroyKnob()
.
19.06.2007: сделано. Для контейнеров также рекуррентно вызывается и DestroyKnob() для потомков -- так что для гроханья целой иерархии достаточно грохнуть корневой узел.
Схема вызова -- сначала грохаются потомки, а уж затем вызывается
Destroy() самой ручки. Порядок взят от XtDestroyWidget()
.
Плюс, поскольку CreateKnob() теперь умеет сама аллокировать privrec и psp-парсить его, то при гроханьи оно освобождается, при надобности с предварительным psp_free().
CreateKnob()
. Но сейчас, при "отделенности" ядра от
реализации -- такое не пройдет.
Как поступить? Ввести, в дополнение к extern first_knobset_ptr'у еще аналогичный указатель на implementation-specific метод "DecorateKnob()", который вызывался бы после создания ручки?
21.06.2007: Неа. Поскольку эти фокусы действительно шибко implementation-dependent, пусть уж оно вызывается методами Create() каждой ручки явно -- как HookPropsWindow() -- из своей реализации.
21.06.2007: угу, сделана такая функция
MotifKnobs_DecorateKnob()
, которая и вычитывает
deffg+defbg, и уставляет тултип. Кстати, поля deffg
и
defbg
сделаны общими -- прямо в data_knob_t
,
так что функция может использоваться с любыми типами ручек, включая
элементы (прицел -- на SUBWIN).
Если 1-й пункт еще действительно целиком в компетенции lib*Knobs*, то 2-й -- фиг знает в чьей, а 3-й -- уж точно для Chl'я.
Но: vmtlink-то -- один! И технология "просто вызова из таблицы, возможно -- пройдя вверх по дереву" -- очень удобна.
Что делать будем?
02.07.2007: угу, по-правде-то, с учетом, что поддержка разных-протоколов-уставки-данных уже будет в cda, все эти методы стоило бы иметь только на верхнем уровне -- у группировки. Но:
Создавать еще один синтетический контейнер, что ли? Которого бы реально не было, но чтоб uplink корневой ручки показывал бы на него, и уж в ЕГО VMT были бы эти методы... Видимо, именно так...
GetKnobLookVMT()
. Чтоб всякие tree-knob'ы могли этим
пользоваться, как записано в bigfile-0001.html за 24-04-2006 и за
14-07-2007.
04.12.2013: эта функция переделана с "отвлечённой" -- принимающей пару параметров (type,look) -- на "приземлённую" -- теперь принимает прямо ссылку на ручку.
Основной смысл -- чтоб при ненахождении VMT она могла б сама
АДРЕСНО ругаться об этом на stderr (что делается только при непустом
look
).
31.07.2007: два аспекта:
Destroy()
может
отсутствовать -- ручка ничего такого сама не аллокирует. И уж
тогда-то, казалось бы, гроханьем виджета должен заниматься
KnobsCore_knobset.c::DestroyKnob()
. Но -- он этого делать
НЕ МОЖЕТ, поскольку он не имеен никакого понятия ни о какой Xt, да и
вообще о том, каким GUI toolkit'ом пользуется конкретная реализация!!!
return -1
", и сам контейнер
плюс часть уже созданных виджетов ручек останутся!
Выход таков:
CreateKnob()
при обломе пусть вызывает
DestroyKnob()
.
Destroy()
-- которые, следовательно,
обязательно должны быть у всех ручек -- будут делать
XtDestroyWidget()
и, "для фиксации этого факта",
проставлять k->w=NULL
.
Учитывая то, КАК сделан порядок обхода в DestroyKnob()
-- как в Xt, сначала-дети-потом-сам, все отработает замечательно.
Тем самым мы вводим правило-постулат:
Постараемся его соблюдать.Методы
Create()
ни в коем случае никогда не должны при обломе оставлять (в точкеreturn -1
) ручку — и внутренние структуры, и виджеты -- в "неопределенном" (inconsistent) состоянии.И, соответственно, метод
Destroy()
должен быть способен в любой момент корректно грохнуть ручку — даже если ее создание обломилось по -1.
Собственно решение: сейчас же делаем стандартный метод
MotifKnobs_CommonDestroy_m()
, сводящийся к
а все типы ручек либо тупо проставят его в своем описании, либо вызовут его и потом сделают свои специфичные дела.if (k->w != NULL) XtDestroyWidget(CNCRTZE(k->w)); k->w = NULL;
Так и сделал, "done".
data_knob_t.conns_u
была int8*, хотя использование везде
-- uint8*. Исправил (см. bigfile-0001.html от 16.12.2010).
А чтоб это работало (активировало ручку, хлопая по ней желтым
прямоугольником), надо уметь типоконтейнеро- Можно ввести метод SetVisibility(k,state,child), которому
указывается требуемое состояние плюс (опционально) кого активировать
(нужно tabber'у для выбора закладки). (Метод, естественно,
НЕ-up'абельный, а персональный.)
13.08.2012: подготавливаем: введен тип
17.11.2013: see also "О программной управляемости
cont-плагинов"/30-03-2011.
16.08.2015: смысл -- чтоб они линковались вместе. Иначе -- при реализации
MotifKnobs_histplot.c -- возникла проблема "курицы и яйца":
в каком порядке указывать библиотеки?
_k_setvis_f
и поле
dataknob_cont_vmt_t.SetVis
. И повсюду оно сделано =NULL.
CreateSimpleKnob()
.
first_knobset_ptr
, необходимый
KnobsCore_knobset'у.
30.06.2007: в основном сделано.
Теперь psp-парсинг реализован несколько по-другому, чем в CXv2: оно
парсит не прямо в структуру-ручку, а в специальную структурку
simplerec_t
, в которой имеются поля для всех аспектов
(включая is_local, приводящая к флагу _B_NO_WASJUSTSET). А потом из
нее все копируется уже в ручку, и указатели на строки тоже.
И есть специальный флажок "noparams", подавляющий
аллокирования места под десяток CxKnobParam_t
.
16.07.2007: да, работает.
Выяснилась (случайно) одна "интересная" деталь: при создании
simple-ручек НЕ проставлялось поле curstate
, оставаясь в
состоянии UNINITIALIZED, которое -- осмысленно! -- было 07-11-2004
выбрано равным 0. А по поведению UNINITIALIZED эквивалентно состоянию
NONE (NORMAL). Причем это было еще с CXv2.
Собственно, это не бог весть какой баг -- при первом же уставлении значения в ручке состояние выбирается. Но все же некрасиво, так что исправлено, и теперь уставляется NONE (NORMAL).
А в остальном -- "done".
03.08.2007: переименовал бывшую
SetKnobValue()
в SetSimpleKnobValue()
, а
SetKnobState()
в SetSimpleKnobState()
-- так
корректнее и определеннее.
06.08.2012: замечание: в случае успеха
CreateSimpleKnob()
НЕ ДОЛЖНА делать
psp_free()
-- поскольку всё, аллокированное в simplerec'е,
потом переходит в свежерождённую ручку, и освобождено будет уже при её
конце.
CreateSimpleKnob()
параметр
options
, единственный пока поддерживаемый бит --
SIMPLEKNOB_OPT_READONLY
; всё идентично v2.
17.07.2007: всё хорошенько описано в bigfile-0001.html за 25-01-2007. Именно так и сделано.
Возвращает последнюю ошибку публичная функция
KnobsLastErr()
, а поддержкой занимаются определенные в
KnobsP.h
вызовы KnobsClearErr()
,
KnobsSetErr()
и KnobsVSetErr()
.
Реализаторы, например, в MotifKnobs будут пользоваться именно этим вызовом.
Сам этот модуль может служить модельным для аналогичных модулей других библиотек.
Вопрос архитектуры -- общее устройство данной библиотеки, чтобы было удобно. (И другие реализации могут иметь похожую структуру -- просто она должна быть достаточно разумной и гибкой.)
21.06.2007: сделано так: есть "ядро"/"общая часть"/"support" реализации, поселенная в MotifKnobs_internals.c -- она выполняет ту же роль, что раньше Knobs_internals.c. Ее API описан в файле include/MotifKnobsP.h, доступном для всяких прочих/конкретных ручек и программ -- как в CXv2 они используют KnobsI.h.
И там будут именно ТОЛЬКО специфичные для Motif'овских ручек вещи, а всякие "GetKnobLabel()" и прочие "UserBeganInputEditing()" поселятся в libdatatree. Так же, как и "get_label_and_tip()".
16.07.2007: как сие делать -- в принципе, ясно:
чтоб после тултипа, также через
MULTISTRING_OPTION_SEPARATOR
, можно было указывать и эти
опции. Просто надо это постулировать как стандарт.
02.10.2014: ссылаться в makefile'ах --
$(LIBMOTIFKNOBS_CDA)
, для регистрации её плагинов надо
вызвать MotifKnobs_cdaRegisterKnobset()
, объявленную в
MotifKnobs_cda.h.
#include<stdio.h>
. Хбз, как без него работало
(колёса вылезли под Linux Mint); в v2 оно было в
Knobs_includes.h, тут отсутствующем.
MotifKnobs_AllocStateGCs()
, в v2 использовавшаяся, тут просто
пустая.
И, судя по вчерашнему опыту wirebpm_gui, она вообще и не понадобится.
И даже больше: чтоб они были каналами, и были бы общими для всех клиентов и могли бы сохраняться в режимы.
P.S. Записываем тут, поскольку толком неясно, в какой ещё из
потенциально затрагиваемых разделов (Chl_knobprops, Cdr_treeproc, datatree)
-- потому, что конечным пользователем параметра "шаг" является
MotifKnobs_internals.c::HandleTextUpDown()
.
03.12.2018: как бы такое можно было реализовать -- не очень ясно.
Ну да, "шаг" -- это параметр (DATAKNOB_PARAM_STEP
), и он
потенциально программно-адресуем (хоть пока это не сделано), но только лишь
из формул, привязанных к этой же самой ручке.
А уж "маппирование на канал" -- вообще хбз...
05.12.2018@вечер-дорога-домой-вдоль-Лаврентьева: разве что
добавить к dataknob_knob_data_t
поля "st_src" и "st_ref", и
если st_src указано, то:
30.01.2019@~15:45, по дороге с
лыж в ИЯФ, после поворота налево около "Карасика": а
можно вообще КАЖДОМУ параметру дать возможность иметь внешнюю ссылку; т.е.,
просто в CxKnobParam_t
добавить некий "CxDataRef_t
ext_ref". (Как хорошо, что тип CxDataRef_t как раз определён
в cx_common_types.h же!)
Краткое обсуждение:
CxKnobParam_t
добавлять "char *ext_src"?
privptr2
ЧТО передавать? Надо ж "дуплет" --
{DataKnob,param_n}...
PropsChg
вызвался бы, а
ещё _ChlAppUpdatePlots()
не забыть.
Не-а, неохота такое делать: шибко уж монструозно.
21.10.2019@Морской-46, в подъезде, ~16:00: в ту же степь: ЕманоФедя ехидно поинтересовался -- а почему это в магнитной системе основная ручка *set и аналогичная ручка в под-окне [...] как будто бы РАЗНЫЕ? У них могут быть разные диапазоны, шаг, ...
Напрашивается мысль: а не сделать ли, чтобы у ручки можно было указать "реальную ручку", чтобы
Тогда:
Несколько замечаний по этой "гениальной идее":
dataknob_knob_data_t
; например, param_k
.
И "редиректить" все обращения туда -- несложно. Что-нибудь типа
(k->model_k != NULL? k->model_k : k)->
хотя...
dataknob_knob_data_t *kd
.
Как, например, choose_knob_rflags()
УЖЕ пользуется
указателем kd
.
u.k
ЭТОЙ ручки (пусть это и будет kd
) и на
kd->model_k->u.k
(обозвать kp
?).
Смысл -- что, например, set_knob_controlvalue()
адресуется
как к параметрам (при вгонянии в рамки), так и к полям конкретно ЭТОЙ ручки
(userval, userval_valid).
param_k
, кто и когда
будет его резолвить?
Очевидно -- CdrRealizeKnobs()
, больше просто негде.
А "откуда" -- видимо, понадобится отдельное поле
param_k_name
.
noparams
.
param_k
от B?
А если та -- ПОЗЖЕ по иерархии, и у неё ещё неразрезолвлено?
param_k
?
Второй вариант явно выглядит разумнее для реализации, хотя и позволяет всякие странные кунштюки (сослаться на ручку, которая сама уже ссылается -- это как бы заиметь "спрятанное" место для параметров, которое не соответствует ни одной реальной ручке; но, стоп -- у той же как бы noparams, так что параметров нет, это что, получится такой хитрый выариант noparams?).
В качестве резюме:
Так что, хоть данный путь и менее монструозен, такое делать тоже неохота.
P.S. Философское: да, сделать-то МОЖНО, что то, что это. Но проблема в том, что элегантным решением ни одно из них не является; а архитектуру любое из них изрядно попортит. Вот и не хочется делать такое "кривое, но влияющее".
XhTextSetCursorCallback()
и
XhTextExtractDoubleValue()
-- теперь тоже будут жить тут
(поскольку в Xh'е им вообще-то не место -- должны использоваться только
в MotifKnobs), под именами
MotifKnobs_SetTextCursorCallback()
и
MotifKnobs_ExtractDoubleValue()
.
28.10.2009: но в Xh_utils.c старые функции по-прежнему будут -- исключительно для возможности унификации с work/cx/ (там их переместить в lib/Knobs/ не представляется возможным, поскольку используются еще и в Chl/).
31.10.2009: неа, и там тоже перетащил их в Knobs_internals, так что теперь всё "красиво".
За компанию добавил MotifKnobs_ExtractIntValue()
.
06.02.2021: в
MotifKnobs_ExtractIntValue()
исторически, ещё с момента её
появления в виде XhTextExtractIntValue()
в CXv2 13-04-2004, в
вызове strtol()
использовалось значение base=10 (причина
неясна), из-за чего не работал ввод шестнадцатиричных чисел -- они
рассматривались как синтаксическая ошибка.
Сейчас же понадобилось для MotifKnobs_inttext'а, так что переделано на base=0.
08.02.2021: кстати, а почему до сих пор
strtol()
? Ведь ещё 16-10-2017 было определено, что из-за
"переполнения" на 32-битных платформах 16-ричные значения 0x80000000 и выше
считаются ошибкой и возвращают 0x7fffffff плюс errno=ERANGE.
Сейчас-то протестировано на 64-битной и всё типа прекрасно, а 32-битной просто нету под рукой (Raspberry PI?) и фиг проверишь.
Но для корректности -- всё же заменяем на strtoul()
.
16.07.2007: технически для самих knob'ов это
будет выглядеть просто -- функции Create_m() будут дергать некую
стандартную функцию, передавая ей свой k
, массив
al[]
с указанием его размера и со счетчиком
al
, плюс битовую маску интересующих опций (шрифт, цвета,
толщина сепаратора, etc.). Та функция как-то эти вещи добудет, и
складирует в al[]
, а уж вызывальщик может либо дополнить
этот список своими параметрами и сбагрить его в
XtCreateManagedWidget()
, либо потом отдельно сделать с ним
XtSetValues()
.
12.12.2007: кое-что тогда, полгода назад, было обдумываемо, но так и не записано (поскольку отвлекся на совсем другое): менеджмент стилей должен быть ПОХИТРЕЕ, с точки зрения хождения по дереву и наследования.
Исходные желания:
(Соответственно, всякие хитрые knob'ы -- bigc и user -- будут свою "внешность" делать как контейнеры, а ручки управления -- как knob'ы.)
А как это можно бы реализовать: делать в строке "стиль" отдельные спецификации -- для вариантов 1, 2 и 3?
Видимо, да. Итого: делаем параметры fg=, bg=, fsize=, и подобные -- влияющие на ТЕКУЩИЙ объект (knob/cont/...); плюс -- параметры с префиксом "cont": contfg=, contbg=, ... -- они будут использоваться данным контейнером и его подконтейнерами; и, наконец, параметры с префиксом "knob" -- knobfg=, knobbg=, ... -- они будут применяться для ТЕРМИНАЛЬНЫХ объектов (NOOP, KNOB, внутренние ручки BIGC и USER).
Замечание -- видимо, надо будет кроме "обычных" цветов предусмотреть какие-то параметры (и способ их добычи в не-Arg-виде) для графиков.
13.12.2007: итого, напрашиваются следующие параметры (могущие также иметь варианты с префиксом "cont" и "knob"):
fg | Основной цвет фона |
bg | Цвет символов |
lit | Цвет подсветки -- XmNselectColor для выключателей, fg@lit для кнопок/стрелок. |
bg3d | Фон "выступающестей" (CTL3D) |
bgrc | Фон "вдавленностей" (текстовые поля ввода, rc=recessed) |
24.03.2009: сегодня опять пришло в голову, что должны поддерживаться не только поштучные указания цветов/шрифтов/размеров, но и указания именно СТИЛЕЙ, типа class=NAME. И чтобы такие class= срабатывали бы так же, как явно указанный набор их содержимого. И тут встают три вопроса:
al[]
?
Например, сделать поле dataknob_t.cclass
("colorization
class"), добывать его точно так же при парсинге поля
style
, и использовать -- как былое
knobinfo_t.color
.
13.06.2016@пляж: опять про колоризацию (ну о-о-очень надо):
14.06.2016: делаем.
data_knob_t.colz_style
.
DATAKNOB_COLZ_
-- NORMAL, HILITED,
IMPORTANT, VIC, DIM, HEADING -- почти в точности, как v2'шные
LOGC_nnn
. Только начинаются с 0, а не с 1.
Просто пока нет полноценных "стилей" (а будут ли), нужного эффекта иначе не добиться.
А так-то -- да, вполне хватило бы DATAKNOB_B_IMPORTANT, чтоб при нём при колоризации использовались насыщенные желтый и красный вместо "обычных" бледных.
MotifKnobs_ChooseColors()
:
zzzzzz
" переименован в
colz_style
.
Замечание: в случае реализации-таки настоящих стилей этот параметр превратится в булевский флаг "is_important", беримый из _B_IMPORTANT.
Смысл -- чтобы подсвечивать некоторые каналы (в linmag'е) поярче.
MotifKnobs_ChooseKnobCs()
.
UpdateBG()
всегда берётся _COLZ_NORMAL (и смысла в ином нет, и
"importantness" на уровне _gui (а не _knobplugin) взять негде).
15.06.2016@пляж-~10:30: еще некоторые мысли насчёт стилей.
А делать это -- с использованием списков al[ac]. ...каковую идею, пожалуй, лучше НЕ использовать.
16.06.2016: а-а-а, нашел -- написано!!! В этом же разделе, в самом начале -- за 16-07-2007 и
12-12-2007. И в MotifKnobsP.h есть следы -- константы
MKS_nnn
и объявление функции
MotifKnobs_ParseStyle()
.
CreateKnob()
никакого такого параметра не предусмотрено (а в мыслях где-то было -- в автобусе в Шереметьево-2
13-03-2005 (стоючи где-то на задней площадке), но, похоже, так и не было
записано ни в один файл)?
А так -- косвенно! Завести в
dataknob_cont_data_t
поле "указатель на информацию о стиле" и:
Делать это надо так, чтобы корректно отрабатывались "smaller"/"larger" (см. ниже).
psp_parse()
'иться прямо в эту
тройственную структуру.
И надо как-то так подобрать "умолчательные" значения, чтобы они и работали бы "флагами неуказанности".
Т.к. при указании размера шрифтов в иерархии будет желание также и межручечные расстояния там менять.
16.06.2016: "свежеоткопанные" (сегодня) правила наследования стилей от 12-12-2007 выглядят даже интереснее свежесформулированных от 15-06-2016 (стареем, тупеем? :)).
Надо б определиться, кого из них использовать (сама технология от этого никак не меняется -- только решения, кого кем override'ить).
MotifKnobs_CreateContainer()
:
А то в liu::kuznitsa удобно помещать заголовки слева, но пришлось их эмулировать VLABEL'ами.
29.08.2011: в v2 titleat* сделано еще 18-04-2011.
И API должен будет САМ располагать кнопки и ручки в соответствии с положением заголовка (т.е., слева-направо для горизонтальных заголовков, и сверху-вниз для вертикальных).
29.08.2011: в v2 nattl сделано еще 19-04-2011.
04.02.2011@Снежинск-каземат-11: и еще в ту же тему:
Можно и в сравнительно простом варианте (хоть вообще другим cont-типом, НЕ grid'ом), но, главное -- АТТАЧИТЬ child'а ко всем сторонам.
15.07.2014: сделано за вчера-сегодня:
Контейнер тулбарчика тоже создаётся, но наполнять его некому.
MotifKnobs_containeropts_t
, а описание в
text2_MotifKnobs_containeropts[]
, которое и INCLUDE'ится.
Но параметры по-прежнему передаются словом флагов.
MotifKnobs_SetControlValue()
-- по опыту v2, чтоб
бибиканье было в одном месте.
29.08.2011: ну и во всех *_knob.c на неё
переведено с прямого вызова set_knob_controlvalue()
.
В button и alarmonoffled это выглядит, конечно, странновато -- вряд ли ТАМ будут выходя за диапазон при нажатии, но, в принципе, вполне возможно, так что для унификации пусть будет.
MotifKnobs_NoColorize_m()
-- чтоб всем
желающим игнорировать колоризацию не городить свои. Но: средняя цена
Пока "настоящих" стилей нет, придётся делать хак, воспроизводящий функционал v2.
05.09.2016@Снежинск-каземат-11: базовые постулаты:
TuneButtonKnob()
,
с описателем Knobs_buttonopts_t
.
style
(ибо
нефиг портить содержимое options
).
06.09.2016@Снежинск-каземат-11: делаем.
MotifKnobs_TuneButtonKnob()
. Ни buttonopts_t, ни
psp-таблицы там нет -- потому, что...
style
возлагается на саму Tune().
buttonopts_t
.
MotifKnobs_TuneButtonKnob()
:
TuneButtonKnob()
.
psp_free()
(т.к. присутствует MSTRING).
MaybeAssignPixmap()
. Только:
static
.
CreateSubwinCont()
, сразу после вызова
MotifKnobs_DecorateKnob()
-- работает.
Со старым приколом, что флаг "bold" работает только при указанном размере (поэтому надо писать size=normal).
CreateButtonKnob()
тоже добавлено, проверено.
Замечание: но НЕ добавлено в
CreateArrowKnob()
. И надо обратить внимание
при создании полноценных стилей, что для кнопок-стрелок размер надо
обрабатывать иначе (как и для пусто-меточных alarmonoffled'ов).
24.03.2017: исправлен косяк: из-за того, что в юзерах
MotifKnobs_TuneButtonKnob()
вызывалась ПОСЛЕ
MotifKnobs_DecorateKnob()
(сохраняющей текущие цвета -- в
качестве изначальных -- в deffg/defbg), спецификатор bg=ЦВЕТ
работал только наполовину: цвет теней менялся, а собственно фон -- нет (он
перепрописывался при колоризации). Перестановка вызовов спасла ситуацию.
Также задействована поддержка изменения цвета символов -- color=ЦВЕТ. Оно в opts и psp-таблице было изначально, но не использовалось (и умолчание стояло RED вместо -1). Работает.
MotifKnobs_colors_lkp[]
добавлен еще один вариант --
"armed". ИСКЛЮЧИТЕЛЬНО для v4gid25s.
21.03.2017: решение, конечно, так себе. И, возможно, надо было цвет назвать "cyan" или "turquoise".
22.03.2017@утро-дорога-на-лыжи: а не ввести ли возможность указывать также и ПРОСТО цвета?
Так старший бит остаётся свободным, тип -- знаковым, и позволяет иметь значение -1 (default).
Вообще идейка так себе. Готовый проект размещения есть, но от реализации лучше постараться воздержаться.
Очевидно, что реализовывать этот общий функционал надо в _internals.
Назвать, видимо, TuneTextKnob()
.
24.03.2017: приступаем.
MotifKnobs_TuneTextKnob()
сделана копированием из
_TuneButtonKnob()
.
_TuneButtonKnob()
бэкпортировано.
XH_FONT_FIXED_FMLY
.
MaybeAssignPixmap()
очевидным образом убран.
(это наWarning: Unable to find type of resource for conversion
XtVaSetValues()
с XtVaTypedArg, XmNfontList,
XmRString
.)
Странно -- вроде бы у XmPushButton'а тот же самый XmNfontList, что и у XmText'а.
Попытка разобраться при помощи editres'а сходу результата не дала: штатный EPEL'овский xorg-x11-resutils-7.5-13.el7.x86_64 на CentOS-7.3 просто не работает (как минимум под Гномом) -- не даёт возможности добыть дерево клиента, щелканье на нужном окне ничего не делает.
ChlLayOutGrouping()
.
27.07.2007: имя -- это сокращение от "Left-to-Right, Top-to-Bottom".
Оно объявляет себя и как CONT с именем "lrtb", и как умолчательный GRPG.
.layinfo
; флаг "перенос строки"
указывается как newline.
21.04.2014: позорище -- НЕ делалось
bzero(&placeopts)
, хотя ВСЕ флаги там не-default. Вот
оно и пыталось учитывать мусор...
k->w
элемента.
А этой форме никто НЕ делал аттачменты к workspace'у.
Исправлено -- в ChlRunSubsystem()
теперь при
opts.resizable
"корневой" ручке делаются аттачменты со всех
сторон.
ChlLayOutGrouping()
где-то в районе 2008/2009 годов и почему-то
не попавшие в v4 (то ли забыл, то ли добавились позже создания
MotifKnobs_lrtb_grpg.c).
Конкретно -- принудительный attachleft()
(или
attachright()
при is_vertical) для линии и собственно ручки.
Без этого они вместо растягивания прилеплялись только справа (или снизу для вертикалки), в полном соответствии с поведением XmForm: "если аттачмент ни слева, ни справа не указан, то аттачится к форме слева; а если указан, то используется только тот аттачмент" (моё краткое изложение написанного в man'е по XmForm); вот раз делался только правый, то оно лишь справа и клеило.
offset>=0
использовать его, вместо умолчательных hspc/vspc. Это всего 1 точка.
24.08.2018: сделано, работает.
29.07.2007: вроде бы более-менее скопировано.
Новая архитектура -- с унифицированными узлами дерева, с элементами, уравненными в правах с ручками, и со стандартными функциями, вынесенными в libdatatree -- делает все проще и элегантнее.
Но возня с многоколоночностью+транспонируемостью -- полное охренение. Вроде как и сделано, но -- надо проверять все варианты.
30.07.2007: и есть некоторые непонятки с концепцией alarm'ов -- методами ShowAlarm() и AckAlarm().
Пока решил оное не делать, а вообще надо б будет это как-то постараться переложить на libdatatree -- как она стандартные действия с ручками реализует, так нехай и с контейнерами тоже все стандартности исполняет.
20.09.2015: alarm'ы в MotifKnobs по-прежнему не работают, вообще ничего никак.
А ведь уже надо -- для вакуума нужно. 20.02.2016: да сделано, еще в ноябре 2015г. Обсуждение в разделе по MotifKnobs_alarmonoffled_knob.
15.10.2018: некорректно было сделано, что в
motifknobs_def_cont_vmt
присутствовала ссылка на
text2gridopts
вместо надлежащего NULL
. В
результате при использовании grid как замещающего (вместо ненайденного)
имелась ругань на неизвестный параметр.
Исправлено (у text_knob и lrtb_grpg было правильно изначально).
param3
.
15.07.2014: да,
05.08.2014: что-то подглючивает разворачивание coltitle'ов по double-click'у -- не те метки оно выставляет. Не с attitle ли связана проблема?
12.08.2014: причина косяка найдена -- это было следствие ДВУХ ошибок.
LabelClickHandler()
были перепутаны str1 и str2:
первая считалась за colnames, а вторая за rownames, хотя при создании
они использовались ровно наоборот.
В результате оно думало, что colnames==NULL и брало метки от ручек -- БЕЗ смещения на nattl.
param3
(который теперь в момент создания приравнивается
санитизированному значению nattl
).
Причём значение прибавляется к u.c.content
-- просто
записывать в userData сразу значение col с прибавленным nattl нельзя,
поскольку col
используется как индекс в colnames.
...а в v2 у multicol privrec'а нет, поэтому там на проблему пока просто забито.
noshadow,notitle,nohline,nocoltitles,norowtitles
Поэтому добавлен специальный ключик nodecor, делающий =1 всем тем флагам.
30.07.2007: новость знаний заключается в следующем:
me->victim_k
.
20.05.2009: а вот и фигушки -- "panel" оказалась очень наглядной, и лучше бы её всё-таки сохранить и тут.
24.08.2009: да, портировал сюда и "panel" тоже.
25.05.2016: с реализацией "panel" -- точнее, с XmNfillOnSelect=True -- есть косяк.
15.10.2018: "по-человеческость" реализации victim
несколько преувеличена: сам поиск "жертвы" по имени был сделан крайне
халтурно -- просто проходом по списку "соседей", так что никакие префиксы
':' и входы в под-элементы работать не могли. И это при том, что
даже в v2 использовалась datatree_FindNodeFrom()
и
нетривиальные ссылки работали!
Почему так странно -- очевидно, потому, что v4'шная
datatree_find_node()
появилась лишь 19-08-2015 (в интересах
histplot'а).
Перевёл на datatree_find_node()
-- всё стало прекрасно
(правда, сначала несколько часов не мог разобраться, какого ж чёрта
не работает (не находится ничего), пока, напичкав искодник искалки
отладочной печатью, не осознал, что последний параметр надо передавать
-1
, а не 0
).
Распределено оно по куче файлов/библиотек, но опишем тут.
09.11.2015: начинаем:
show_knob_alarm()
, используемый
CdrProcessKnobs()
'ом.
dataknob_cont_data_t
добавлены поля alarm_on
,
alarm_acked
, alarm_flipflop
.
MotifKnobs_CommonShowAlarm_m()
MotifKnobs_CommonAckAlarm_m()
MotifKnobs_CommonAlarmNewData_m()
-- таким вот неприличным
способом (скопировано из v2) делается собственно мерцание и периодическое
бибиканье.
...а надо бы -- РАЗ В СЕКУНДУ!!!
Там же рядом и внутренние, взятые 1-в-1 из v2 DisplayAlarm()
и
CycleAlarm()
(оный и вызывается из NewData_m).
Смысл -- чтоб моргал максимально возможный "внешний" контейнер.
...и у конкретно subwin'а в этой позиции вообще NULL -- чтоб моргало в под-окне, а не в содержателе его кнопки.
Работает -- бибикает, моргает. Из неприятностей только, что
12.11.2015: продолжаем:
choose_knobstate()
отсутствовала проверка на этот флаг. Добавлена -- теперь показывается.
Там еще рядом, возможно, будет OTHEROP. Хотя точно и не ясно.
NewData()
(где у
cont_subwin'а и происходит подсветка кнопки!), и лишь затем значение флагов
сохраняется в k->currflags
-- ПОСЛЕ общего switch(k->type).
Сделано в ветке CONT "предварительное" сохранение прямо перед вызовом NewData().
Помогло -- в основном. Но не полностью. Переходы NORMAL->ALARM и RELAX->NORMAL показывает сразу, а вот ALARM->RELAX -- с отсрочкой на 1 такт, через промежуточное NORMAL.
13.11.2015:
choose_knobstate()
делается не как со всеми прочими -- по наличию
флага, а в обход, по значению k->attn_state
==KNOBSTATE_RELAX (и
по нему же в choose_knob_rflags()
выставляется
CXCF_FLAG_ALARM_RELAX
!)
А от cont_subwin'а, кстати, никакого attn_state не передаётся (ибо передаётся k=NULL).
choose_knob_rflags()
show_knob_alarm()
(который
и вызывает set_knob_relax()
, в конечном итоге прописывающий
attn_state
).
Таким образом, ручковы rflags обновятся лишь на цикл позже, и subwin
отреагирует на них на цикл позже; в то время как сама ручка alarmonoffled
"позеленеет" уже сразу, т.к. отреагирует не на флаги, а на
attn_state
.
Вот такая вот у нас кривоватая схема организации отображения
ALARM'ов/RELAX'ов, сварганенная/накаляканая разработанная еще в v2.
Ну и что тут делать? Видимо, проще забить.
20.02.2016: да, мерцание и бибиканье слишком частые.
А причина -- в том, что бибиканье делается по приходу данных (метод NewData, по не-synthetic). И если раньше (v2, qult/configs/srvparams.conf) размер цикла 100-200мс вместо 1с ставился лишь части серверов, то в sw4cx/configs/srvparams.conf стоит
.srvparams * params=-b200000
Так что надо бы что-то делать.
Но это и громоздко (архитектурно сейчас бибиканье сделано методами в MotifKnobs_internals, а тут еще придётся следить (кому?) за таймаутами), да и сами таймауты в таком количестве совершенно лишние.
23.02.2016: вот по последнему проекту и делаем.
dataknob_cont_data_t.alarm_prev_time
.
cx_time_t
-- чисто "для красоты", чтобы со
struct timeval не связываться.
По-хорошему -- как было очевидно изначально -- нужно сравнивать, что разница больше не секунды, а, например, 0.99с.
Вроде с потенциальным разрывом соединения не связано (возможное переполнение отправного буфера cxsd_fe_cx при отсылке подписки) -- и ругательства нет, и управление прочими каналами продолжает работать.
30.07.2007: основное отличие (за вычетом украсивливания вследствие новой
архитектуры) -- то, что раз уж кнопчатые могут быть alarm'ами, то он по
нажатию на себя в варианте !is_rw дергает ack_knob_alarm()
.
18.05.2010: напрашивается пара идей, как это можно было бы реализовать на уровне экранных управляющих ручек:
На вопрос "надо ли это? где и как?" Карнаев прислал такой ответ:
Конечно, надо.
Такой режим работы часто используется. У меня в голове он представляется в следующем варианте: когда нажимается объект типа "стрелка2, то программа, спустя определенную небольшую паузу, начинает высылать с известной частотой (на ВЭПП-4 это от 1 до 5 Гц)запросы на добавление(убавление) какого-либо значения, если нажимается кнопка без фиксации - на включение бита; после отпускания кнопки все немедленно прекращается, в случае кнопки - бит зануляется. Поэтому мне кажется, что актуален твой второй вариант.
Первый вариант, мне кажется, менее гибок в случае сложных комбинаций со стороны клиентов.
Похоже, сделать надо будет ОБА режима -- благо, сие несложно:
options
--
normal/10/pulsed. Возможно -- для режима pulsed отдельно указывать
скорость автоповтора (еще вариант -- ввести ОДИН параметр mode:
==0:normal, <0:10, >0:частота).
EquipButtonKnob()
вместо просто уставки
activateCallback при надобности ставить armCallback+disarmCallback:
Естественно, всё это -- только о работе мышью, т.к. с клавиатуры Motif'овские кнопки сами делают нажатие+отпускание, что эффективно даёт 2-й описанный режим.
Оно, конечно, лучше б было стилями, но они еще фиг знает когда (и если) появятся.
02.08.2007: получилось намного более стройно -- еще бы, ведь как раз архитектура частично (str3, например) и разрабатывалась с учетом subwin'а.
Из неудобств/недоделок: показ на кнопке состояний ALARM_RELAX и OTHEROP: пока не стал воспроизводить даже то, что было -- оно гореть-то будет всего секунду (точнее, цикл), а от этого толку немного. Проблему надо решать где-то на уровне Cdr.
26.02.2009: через полтора года: а оно, оказывается, абсолютно не было доделано -- просто скелет, а располагать содержимое (как grid) оно нифига не умеет... Или и не должно уметь, а должно содержать grid?
10.02.2010: вообще-то очевидно -- раз это ОКНО, то пусть и содержимое умеет располагать так же, как это делает большинство окон -- как lrtb (опыт v2'шного показывает, что grid в под-окнах малополезен -- там нужен именно lrtb)..
Так что надо будет его как-то подружить с lrtb -- функцию заполнения оттуда экспортировать, что ли...
22.07.2010: по опыту возни с v2'шным subwin'ом: а может, лучше всё-таки сделать как в тулкитах -- чтобы ВСЕГДА мог быть только ОДИН child, а уж его можно делать любым -- хоть multicol, хоть lrtb? Ведь смысл subwin'а -- это, аналогично Shell'у (Shell is a top-level widget (with only one managed child)), быть контейнером ВЕРХНЕГО уровня, "отделяя" своё содержимое от родительского окна в отдельное. А расположение множественного содержимого -- задача следующих уровней.
01.05.2014: угу, так и сделано -- оно теперь создаёт единственного child'а (если оный есть, т.е., count>0), как в v2'шном SUBWINV4.
01.05.2014: а вот что пока НЕ сделано как в v2 -- это инвертирование кнопки открытого окна и соответствующее правильное расцвечивание.
28.08.2014@дома: уже сделано, творческим копированием из v2. Проверено -- вроде работает.
Заодно удалено мутное неиспользовавшееся поле
is_horizontal
, присутствовавшее издавна -- видимо,
заготовка под lrtb.
10.02.2010: поскольку содержимое будет всегда-lrtb, то "withdrawn".
29.03.2011@Снежинск-каземат-11: точнее, не "всегда-lrtb", а "единственный child" (как решено 22-07-2010). Но на withdrawn'нутость это всё равно не влияет :-).
Ввести бы правило, что при отсутствии subwintitle
(str3
) пытаться использовать tip
?
Сходу -- сделано, работает.
29.08.2014: и разноцветность отдельных пунктов добавлена, также творческим копированием из v2.
...хотя скорбно это -- по-хорошему, рассчитано-то всё под централизованный менеджмент стилей, а это хак...
23.09.2014: был багец -- в
SelectorSetValue_m()
неправильно ограничивались значение
сверху (забыто было -1
), и оно падало. Исправлен.
21.03.2017: подробности:
MotifKnobs_TuneButtonKnob()
с обвеской, переименованный в
TuneSelButton()
.
Раньше вначале делался парсинг style, потом создание виджета, потом расцвечивание; но, поскольку Tune...() сразу расцвечивает, то ей нужен уже "живой" виджет.
Поэтому схема изменена на "создание виджета, потом парсинг/раскрашивание".
Ответ очень прост: расцвечивание селектора делается отдельными хитрыми "заклинаниями" -- оно отслеживает текущий выбранный элемент и копирует его цвет на панельку. Но копируется только цвет фона, а символов -- нет.
Следовательно, если когда-то захочется иметь ПОЛНОЦЕННУЮ поддержку стилей с foreground'ом, то надо будет и для цвета "переднего плана" делать копирование.
15.07.2014: да, вроде сделано. Пока без поддержки "стилей" кнопок.
11.08.2014: поддержка стилей скопирована из v2, хоть и со злобным комментарием "!!!STYLE!!!".
11.07.2016: кстати, уже давным-давно используется, так что "done".
ChoicebsColorize_m()
лезет за умолчательными цветами в
me->items[0]
(см. bigfile-0001.html
за
18-08-2012).
Понятно, что ситуация несколько нештатная, но лучше б всё же как-то защититься.
13.07.2016: да, сделано -- теперь items[0]
используется не безусловно, а только при me->numitems>0
,
иначе -- k->w
(форма). Падать перестало.
15.07.2014: портировано из v2. Пока не проверено.
16.07.2014: проверено, работает, считаем за "done".
text2canvasopts[]
отсутствовала строка PSP_P_END()
.
В результате у Роговского клиент валился по SIGSEGV.
Почему не вылезло раньше -- загадка:
12.08.2014: пара идей:
16.01.2015: после загруза проблемой (на этой неделе), обмышления "как уведомлять histplot'ы из KnobProps" (вчера) и анализа тех 0001'шных сценариев (сегодня утром) стало ясно, какую архитектуру выбрать.
Все из этого списка и вызываются по "событию раз в секунду".
find_knobs_nearest_upmethod()
) из той таблицы, что
прибиндена к корневому контейнеру.
28.01.2015: реализуем (записывается всё здесь, включая поля и реализацию в Cdr+datatree):
data_subsys_t
указатели списка
hist_first
и hist_last
, а в
dataknob_knob_data_t
ссылки назад/вперёд
hist_prev
/hist_next
и собственно сама история
histring*
(скопировано из v2, за вычетом frqdiv &
Co.).
Historize
, должный
включать историзирование ручки. Он не void, а int, чтобы возвращать
успех/облом.
18.08.2015: уже несколько дней глодала мысль -- а
нафига этот метод? Ведь историзирование включается уже имеющимся
методом ToHistPlot
. Так что -- Historize
выкидываем.
20.08.2015: а вот и нифига -- ТОГДА я соображал, а не
"запамятовал о наличии ToHistPlot". Этот метод нужен
MotifKnobs_histplot_noop'у, который должен включать историзирование
НЕЗАВИСИМО от Chl_histplot'а! Так что метод восстанавливаем. И
datatree-адаптер к нему historize_knob()
.
CdrAddHistory()
-- аллокирует буфер и добавляет в
список историзируемых. В начале проверяется на DATAKNOB_KNOB; плюс всё
делается в правильном порядке -- добавление в список ПОСЛЕ
аллокирования буфера, так что в неопределенном состоянии не останется.
CdrShiftHistory()
-- сдвиг истории всех ручек
подсистемы.
CdrDestroyKnobs()
буфер освобождается и
удаляется из списка.
Замечание: поскольку список хранится в DataSubsys'е, то ВСЕ ручки подсистемы будут историзироваться синхронно и с одинаковой частотой, для ВСЕХ окон/группировок.
В командной строке указывать список имён каналов, плюс, опционально, параметры вроде "частота обновления" и "длина запоминания истории".
04.12.2015: сделан, еще в 20-х числах августа. И даже немножко используется (хотя в нём не хватает удобств, вроде возможности указывать период обновления и диапазоны отображения).
12.10.2016: (обсУждено неделей ранее) и еще, кроме диапазонов, надо уметь указывать также метки. Сейчас в качестве оной берётся последний компонент имени канала, но для кучи однотипных каналов (вроде A.Iset, B.Iset, C.Iset) это неприемлемо.
Какой взять синтаксис? Видимо, префикс /КЛЮЧ:ЗНАЧЕНИЕ:КАНАЛ. Т.е., префикс -- '/', equals_c -- обычный ':', терминатор/сепаратор -- тот же ':' или же '/' (этот для последовательного указания кучи параметров, он при окончании парсинга текущего параметра чтоб не съедался, а отдавался бы следующему). 16.06.2018: сделано иначе: просто {КЛЮЧ=ЗНАЧЕНИЕ:}КАНАЛ.
И не забыть возможность указывать dpyfmt -- через стандартный префикс %DPYFMT:.
Кстати, указывабельность "глобальных" параметров, вроде hist_period, добавлена еще 28-01-2016 -- см. за ту дату в следующем раздельчике (где про общую реализацию histplot'а).
22.05.2018: поскольку минимально рабочий вариант сделан, и вчера для программы создан свой раздел, то этот пункт считаем за "done".
15.08.2015: потихоньку пилим, копируя функционал из v2'шного Chl_histplot.c ("потихоньку" -- потому, что толпа всякой отвлекающей от работы фигни...).
Некоторые замечания по архитектуре:
CdrAddHistory()
;
CdrShiftHistory()
-- и последующий вызов обновления
MotifKnobs_UpdateHistplot()
Паковаться, видимо, будет в свою отдельную библиотеку -- libMotifKnobs_histplot.a.
Но СОЗДАЁТ кнопки [Close] и [Save] -- именно
MotifKnobs_CreateHistplot()
.
cyclesize_us
-- указывается при создании, а
используется только при вычислении подписей к осям.
Наполнение функционалом вроде завершено, теперь дело за использователями.
17.08.2015@сад-Вера:
18.08.2015: сделан первый из клиентов -- Chl_histplot.c.
Ответ: заводим контейнеров метод HistInterest
.
express_hist_interest()
.
set1_fgt0
.
CycleCallback()
с
некоторым периодом.
sl_enq_tout_at()
; вся технология
взята от циклов из cxsd_hw.c, только без определения
неуспевающести (cycle_pass_count*).
CdrShiftHistory()
и
уведомляет всех клиентов об обновлении.
20.08.2015: также добавлена внутренняя функция
_ChlAppUpdatePlots()
(объявляется в опять созданном
Chl_app.h). Нужна для того, чтобы Chl_knobprops вызывала
перерисовку всех графиков по [Ok] -- на случай изменения диапазонов
отрисовки.
data_subsys_t
, куда добавлены поля
cyclesize_us
и def_histring_len
. Так что эти
значения доступны всем использователям, для дальнейшей передачи
MotifKnobs_histplot'у.
_ChlHistInterest_m()
-- очевидным образом
живёт в Chl_app.c. А ChlTopVMT
-- в
Chl_gui.c. Для знания которого о новом методе пришлось ввести
Chl_app.h.
После чего, глядя на содержимое Chl_gui.c, встаёт вопрос -- а зачем он теперь вообще нужен? Его содержимое явно просится на переезд в Chl_app.c.
XhWindow->higher_private
?
data_subsys_t.gui_pripvtr
, куда Chl
(в лице Chl_app) записывает указатель на свой privrec.
21.08.2015: а теперь внимательно смотрим в "Общих вопросах" от Chl комментарий за 13.08.2007 :).
find_knobs_nearest_upmethod()
--
он теперь вначале пытается не сразу делать "шаг вверх"
(holder=k->uplink
), а если сама k
имеет
тип CONT или GRPG, то сначала пробует её (holder=k
).
19.08.2015: делаем второго клиента -- MotifKnobs_histplot_noop.c.
Содержимое переехало в Chl_app.c, а созданный вчера Chl_app.h стал излишен.
AddPluginParser()
).
datatree_find_node()
(детальный алгоритм внутренностей придуман @пляж-после-обеда).
Несколько подряд '.' -- разделителей компонентов -- считаются за один; в точности как "////" (точнее, не "считаются", а сам проход устроен таким образом -- перед вычленением очередного компонента пропускаются предшествующие ему '.' и ':' (тут и делаются шаги вверх)).
12.05.2016: был косяк с определением "ненайденности":
стояло условие rest<0
, никогда не исполнявшееся (т.к. в
цикле ограничение rest>0
); поэтому при указании ненаходимого
компонента вылетало по SIGSEGV. Сменено на rest<=0
.
15.10.2018: а еще там есть неочевидная тонкость: чтобы
искать узлы ЛЮБОГО вида, надо указывать последний параметр
(rqd_type
) равным -1
, а не 0
. Т.к.
сравнение проводится при rqd_dtype>=0
, так что
0
по факту означает "никогда не найдётся" (типа с кодом 0 не
существует).
CreateKnob(группировки)
, и в результате
используемый CreateHistplotNoop()
'ом
historize_knob()
просто не работал -- Chl'евы методы еще
не были прибиндены.
И тогда вообще неясно, как оно там работало бы с подмененным
uplink'ом -- поиск "вверх" по дереву в
datatree_find_node()
приводил бы чёрт-те-куда.
CreateKnobAs()
, в основном аналогичный простому
CreateKnob()
, но получающий еще параметр
supplied_vmt
, которым -- при !=NULL -- она подменяет
"родную" vmt (используемую, впрочем, при СОЗДАНИИ ручки-контейнера.
DoCreateKnob()
,
вызываемую обеими.
ChlRunSubsystem()
теперь вместо своего
"_ChlBindMethods()
" просто вызывает
CreateKnobAs()
.
А вообще напрашивается идея отделить "элементовы"
методы (вроде ShowAlarm, SetVis и всех unif) от "группировковых"; ну
или -- сделать у группировки дополнительную VMT (типа
dataknob_cont_vmt_t
), которая бы использовалась при
ненайденности метода при проходе вверх -- т.е., как бы "дополнительная
над верхним узлом". Иначе, как сейчас -- совсем некрасиво, т.к.
нынешний вариант перетирает VMT верхнего узла, так что у него реально
нужные "персональные" методы (вроде HandleCmd и Destroy) отрезаются.
24.08.2015: да, так и сделано, а та порнография с
порченьем vmtlink
удалена.
Плагин работает -- ура!
20.08.2015: и теперь третий -- histplot.c.
21.08.2015: добиваем histplot.c.
-dDEFPFX
не удастся, т.к. Xt воспринимает "-d" как "-display").
05.02.2016: сделано. (Начато еще 28-01-2016, но дурканул с передачей Realize()'у.)
-bBASEREF
, но надо унифицироваться с defserver). 05.02.2016: сделано.
16.06.2018: только не суффиксы через запятую, а префиксы в формате "КЛЮЧ=ЗНАЧЕНИЕ:".
Парсинг надо делать, видимо, в стиле v2'шных Chl_app/pzframe_main: сначала некоей эвристикой определять тип параметра (PK_NNN), а потом обрабатывать его.
...но всё это -- МОЖЕТ БЫТЬ, если понадобится.
27.11.2015: кстати, о диапазонах: а чё это у нас утилита histplot не позволяет менять диапазоны прямо на ходу, в окошке "Knob Properties"? 04.12.2015: а потому, что в него линкуется только MotifKnobs, а Chl (содержащая Chl_knobprops.c) -- нет, ибо он НЕ Chl-based. 06.10.2016@утро-душ: а кто сказал, что Chl_knobprops -- аксиома? Можно сделать MotifKnobs_knobprops, а Chl_knobprops чтоб стал его юзером (как это есть в паре MotifKnobs_histplot/Chl_histplot).
21.08.2015: исправляем порнографию с
"BindMethods" и CreateKnobAs()
.
data_subsys_t.sys_vmtlink
.
find_knobs_nearest_upmethod()
приучен к его
использованию.
ChlRunSubsystem()
заполняет его, а корневую
ручку-группировку создаёт при помощи обычного
CreateKnob()
.
_ChlBindMethods()
и ChlTopCont
, а
ChlTopVMT
переименована в ChlSysVMT
.
07.01.2016: вчера обнаружился косяк: при смене свойств
ручки (в Chl_knobprops) оси НЕ обновлялись и axis не ресайзился. И вообще
MotifKnobs_UpdateHistplotProps()
никогда не вызывалось.
Сегодня разобрался: вся схема "histinterest", дёргаемая в
CallUpdate()
, была рассчитана на один-единственный тип события
-- "Update plot", а тут другой тип -- "Update plot props", но Aоно ж никак
не передаётся, а вызывается по факту то же самое "Update plot".
Всего-то надо добавить в эту архитектуру еще "код события".
09.01.2016: добавлено, всё сделано.
_m_histplot_update()
добавлен параметр
reason
.
HISTPLOT_REASON_UPDATE_PLOT_GRAPH
и
HISTPLOT_REASON_UPDATE_PROT_PROPS
.
PlotUpdateProc()
-- научены
вызывать разные методы в зависимости от reason.
_ChlAppUpdatePlots()
+CallUpdate()
вставлена
передача кода события -- благо, CallUpdate()
'ов
privptr
как раз был свободен.
10.01.2016: для симметрии
MotifKnobs_UpdateHistplot()
переименована в
MotifKnobs_UpdateHistplotGraph()
.
24.09.2015: показывать надо просто ВМЕСТО обычных значений справа (незачем делать доп.виджеты с маппированием их точно поверх), а логику работы с мышью позаимствовать в fastadc_gui/Xh_histplot.
22.05.2018@утро-дорога-на-работу-около-ИПА: как это удобнее реализовать:
MotifKnobs_histplot_t
поле
x_index
.
-1
, что указывает "отображать текущее
значение из ручки".
UpdatePlotRow()
для каждой строки -- вытаскивается в отдельную
функцию, "UpdateAllPlotRows()".
UpdatePlotRow()
при x_index>=0:
k->u.k.curv
, а x_index'ное из
кольцевого буфера.
При x_index>=histring_used -- ставит пустую строку (ради чего придётся
вместо MotifKnobs_SetTextString()
вызывать
XmTextSetString()
вручную).
15.06.2018: также пустую строку ставит при
x_index<0
в режиме view_only -- т.к. нету никакого
"текущего" значения для показа вне графика.
KNOBSTATE_NONE
).
DisplayPlot()
-- при
x_index>=0 числа не обновляет.
...по-хорошему, ещё б и графики чтоб замирали, но с этим никак -- любой expose-event их перерисует.
x_index
и вызывается UpdateAllPlotRows()
.
На движение с нажатой левой кнопкой реакция та же.
ButtonRelease
и LeaveNotify
делается
x_index=-1
и вызывается UpdateAllPlotRows()
, тем
самым эффективно восстанавливая периодическое обновление.
MotionNotify
.
23.05.2018@утро-дома: еще б полезно было показывать и время, к которому относятся отображаемые числа.
Технически это несложно:
x_index
: при переходе
"<0
-> >=0
" она сделает Manage,
а при ">=0
-> <0
" -- Unmanage.
Заодно она же пусть и UpdateAllPlotRows() вызывает.
...вариант -- верх аттачить к grid'у, тогда будет прямо МАКСИМАЛЬНО близко к числам.
(Еще вариант -- рядом с курсором мыши, но там выдрёпываться надо с позиционированием, чтоб не перекрыл графики (курсор выше середины -- внизу, ниже середины -- вверху).)
24.05.2018: отображение при нажатии мышью сделано!
KNOBSTATE_NONE
.
Это определяется полем view_only
, выставляемым при передаче
MotifKnobs_CreateHistplot()
'у свежевведённого флага
MOTIFKNOBS_HISTPLOT_FLAG_VIEW_ONLY
.
set_x_index()
.
time_dpy
) почему-то глючит форма: если менялся размер окна, то
в момент XtManageChild(hp->time_dpy)
происходит какая-то
перверсия: то окно расхлопывается до исходного размера (график меняет размер
как надо), то расхлопывается "виртуально" -- содержимое размер/позиции
меняет, как для расхлопывания, но сама ширина остаётся, и в результате вся
правая часть (с числами) оказывается скрыта за правым краем окна.
Очередной глюк в XmForm...
Так что покамест показывание time_dpy за-#if0'ено (впрочем, показ на нём времени также так и не был реализован -- не дошло до того...).
Но это сделать не получится: ведь содержимое графика продолжает ехать, а "на паузе" стоят только отображаемые числа.
О! Можно будет этот "репер" рисовать для стоячих -- при
view_only
.
28.05.2018: улучшаем:
reprGC
и аллокирование этого GC.
...вот только оказалось, что цвет GRAPH_REPERS виден фиговато, посему пока используется axisGC.
DrawGraph()
добавлена отрисовка, при
x_index>=0
.
set_x_index()
-- вызов оного.
Отрисовка и вызов делаются только при view_only
.
make_time_str()
.
Там делается выбор формата времени "расстояние"/"точное" в зависимости от режима.
Форматрирование из struct timeval'а в HH:MM:SS.nnn было подсмотрено в
stroftime_msc()
.
04.06.2018: добавлен параметр with_msc
, при
подписи времени =1, а при осях =0 -- чтоб избежать каши (на осях
миллисекунды всё равно малоосмысленны).
05.06.2018: какие-то странности с геометрией: в самом правом пикселе НИКОГДА не бывает репера...
07.06.2018: что-то очень не слава богу с координатами: как будто X11 даёт в событиях координату на 1пиксел вбок. Но в fastadc_gui-то всё правильно!!!
13.06.2018: разобрался -- нашлась позорная ошибка, отравлявшая жизнь последние полмесяца (и не позволявшая считать задачу «чтоб при нажатии мышью на некую точку графика показывались бы значения в "тот" момент времени.» решенной).
PointerHandler()
было забыто "-1" при
пересчёте из экранных координат в буферные -- стояло
x = ((grf_w - x) + hp->horz_offset) * hp->x_scale;
вместо
x = ((grf_w - 1 - x) + hp->horz_offset) * hp->x_scale;
Концы нашлись путём добавления отрисовки вертикальных чёрточек прямо в
PointerHandler()
:
set_x_index()
'у делался пересчёт по той же формуле, что
используется в DrawGraph()
при отрисовке репера.
И вот ЭТА чёрточка оказалась на пиксел левее первой -- так и стало ясно, что где-то в расчётах ошибка.
Дальше всё просто -- косяк с отсутствием "- 1
" сразу в
глаза бросился.
DrawGraph()
: репер
рисовался
hp->x_index > 0
вместо надлежащего
hp->x_index >= 0
-- из-за чего в самой правой точке не показывался.
Итак -- косяки исправлены, всё работает. Конкретно (то, что нельзя было достоверно проверить сразу по изготовлении, а только потом, когда и со стороны histplot.c всё (загрузка файла) подготовлено):
В "статике" (view_only
) отображается при просто шевелении
мышью, нажатость кнопок не требуется.
view_only
).
x_index
), и как бы можно было
её модифицировать для подходящести -- неясно.
Короче: цель достигнута, считаем за "done".
cda_ref_is_sensible()
(но
саму её использовать нельзя!), последовательно, normrange и
yelwrange,
Короче, в стиле v2'шного CdrCvtLogchannets2Knobs()
.
Учитывая, что DISP_{MIN,MAX} используются в 3 местах --
DrawGraph()
, DrawAxis()
и
ShouldUseLog()
, правильнее будет завести процедуру
"GetMinMaxOfKnob()", содержащую ту цепочку.
06.01.2016: да, сделана GetDispRange()
.
И на неё всё переведено. Вроде работает, так что "done" (хотя при смене
диапазонов есть какие-то свои косяки с перерисовкой осей).
06.01.2016: насчёт косяков с перерисовкой осей: ну да,
grep'пинг показывает, что MotifKnobs_UpdateHistplotProps()
не
вызывается вообще ниоткуда. А как функционирует вызываемый из
PropsOkCB()
стек
_ChlAppUpdatePlots()
+CallUpdate()
-- тёмный лес.
07.01.2016: с "тёмным лесом" примерно разобрался -- см. выше за сегодня.
18.06.2018: небольшое дополнение -- теперь она стала
int
и возвращает 1, если отдаёт нарытый диапазон, и 0 в случае
умолчательного.
Заодно убран за-#if'ленный старый до-наследовательный код.
14.01.2016: разобрался. Тест показал, что кастинг nan
к int выдаёт значение -2147483648
(=0x80000000). А в
обсуждении этого вопроса на StackOverflow
сказано
gcc producesплюсINT_MIN
for all out-of-range conversions toint
GCC itself will give you 0
when converting NAN to int at
compile-time (rather than run-time, where you get INT_MIN). So even on a
single platform you can get two different values, depending on whether the
compiler was able to determine your NAN as a constant value.
(всё с оговоркой, что оно так на x86, а как на других архитектурах -- хбз).
Тогда всё проясняется: у нас результирующее -- после
RESCALE_VALUE()
и конверсии к int
-- значение
вгоняется в диапазон [-32767,+32767], так что все NAN отрисовываются где-то
далеко снизу нижней границы графика.
Формально всё правильно, но вот такой взбрык человеческого восприятния.
Идея от Беркаева, по опыту всяких осциллографов: показывать крупной стрелочкой, что "интересное -- вон там!". На вопрос "а как определять наличие «интересного»?" -- "а просто всегда при наличии чего-то за границей показывать стрелку".
Идея от меня -- в меру моего понимания: можно на боковых секциях axis рисовать крупные треугольники, светло-голубого цвета, направленные в ту сторону. Т.е., справа -- треугольник направо, слева -- налево.
Вопрос только в выборе места и размера, чтоб наименьше перекрывалось с подписями.
Несколькими минутами позже:
SetHorzbarParams()
-- показывает,
что, скорее всего, перерисовываться будет, т.к. 1 (флаг перерисовать)
возвращается при ЛЮБОМ изменении параметров.
DrawAxis()
указывается
do_clear=1 практически ВСЕГДА (кроме ExposeEvent'а).
Так что, видимо, всё окей.
07.06.2016@пляж-15:30: а может, сделать проще -- при любом ненулевом положении scrollbar'а делать ему XmNbackground:=red?
07.06.2016: сделано. Собственно действие выполняется
"write-accessor'ом" SetHorzOffset()
, через который теперь
проходит запись в horz_offset
, и если не-нулёвость уставляемого
значения не совпадает с не-нулёвостью текущего, то он уставляет нужный цвет.
Единственный "минус" -- что краснеют лишь бегунок и стрелки, а они при большой длине записи не очень бросаются в глаза, т.к. бегунок становится маленьким, а бОльшую часть места занимает "корыто" (trough), которое остаётся серым, не краснея.
MOTIFKNOBS_HISTPLOT_REASON_SAVE
.
Юзеры (в лице Ваагна) уже интересуются, им нужно сохранение.
30.09.2016: делаем.
MotifKnobs_SaveHistplotData()
.
В отличие от v2, она лишь сохраняет, не выдавая никаких сообщений в statusline и event-log, но возвращает 0/-1.
#define
'ы CHL_*_PATTERN_FMT
переехали из
Chl.h в datatree.h под именами
DATATREE_*_PATTERN_FMT
.
k
, для добычи
subsys'а.
ChlRecordEvent()
и subsys добывается
через _ChlPrivOf()
.
Итого -- готово!
17.06.2018: есть еще потребность -- чтоб в строке заголовков печатались бы диапазоны (disprange) в histplot.c-совместимом виде.
Отдельный вопрос: не выполнять ли в histplot.c авто-скейлинг?
18.06.2018: а еще там не хватает выдачи префикса %DPYFMT!
18.06.2018: делаем.
GetDispRange()
-- вот пусть она и возвращает: нарыла
какой-то осмысленный диапазон, или взяла умолчание.
MotifKnobs_SaveHistplotData()
в выдаче заголовков
она вызывается, и если вернёт true, то добытый диапазон тоже печатается --
"правильно", с обрезкой с использованием snprintf_dbl_trim()
.
AddGroupingChan()
'ом, и загруженный из
файла график выглядит точно так же, как и в оригинале.
timestamps_ring[]
.
Т.е., при повторном сохранении загруженных из файла данных новый файл
содержит идентичные времена и информация не теряется.
Есть, конечно, некоторый вопрос -- что будет, если вдруг
timestamps_ring_used
меньше, чем длина данных (значение
max_used
). Тогда времена за границей будут бредовыми (т.к.
начнут считаться как ТЕКУЩЕЕ-t*cyclesize_usec), что создаст "разрыв"; но это
ситуация невероятная (сейчас) и на неё можно забить.
Предыстория: еще энное время назад юзеры (в лице Ярика Куленко, см. bigfile-0001.html за 19-10-2012) просили подписывать вдоль горизонтальных осей не относительное время (сколько-то назад), а абсолютное (HH:MM:SS).
11.06.2016утро-пляж:
XClearArea()
верхней и нижней полос axis'а (в идеале -- НЕ
трогая tick'и).
23.05.2018@утро-дома: "то" пока не делаем, зато отображение абсолютных времён и можно и нужно сделать для "статичного" варианта -- когда данные загружены из файла.
По сегодняшнему проекту (см. детали в разделе по утилите "histplot") из
файла будут читаться и времена, в буфер
MotifKnobs_histplot_t.timestamps_ring
.
Так вот -- в случае его наличия можно прямо тамошние времена и использовать для подписей, вместо вычислений.
Следствие 1: надо б будет вытащить генерацию текста подписи в отдельную функцию -- чтобы использовать её же для показа времени при елозеньи мышью по графику.
Следствие 2: и в MotifKnobs_SaveHistplotData()
надо б
тогда эти же числа использовать, вместо вычислений, чтобы цепочка
"чтение,сохранение" не приводила к потере данных.
31.08.2016@Снежинск-каземат-11: чуть размашистее, чем в v2, т.к.
тут публичный интерфейс, содержащийся в MotifKnobs_histplot.h --
туда и добавлен MOTIFKNOBS_HISTPLOT_FLAG_HORZ_CTLS
.
И в утилиту histplot добавлена возможность указать "horz_ctls=1" -- на ней и проверено.
(Сейчас найти место, где это обсуждалось, не удалось. То ли в 0001, то ли в 0002; а может, вообще в разделе не по histplot, а по XhPlot или и вовсе по fastadc.)
Так вот, Лебедев внёс предложение: да просто ставить -- скроллбар на начало, нулить сдвиг да и всё.
15.10.2018: Лебедев повторно внёс то же предложение: при любом изменении масштаба ставить скроллбар на начало.
13.02.2023: и опять Лебедев жаждет того же.
14.02.2023: фиг с ним -- делаем. Вся работа в
XScaleKCB()
, которая ради этого слегка перетряхнута:
Смысл -- чтобы "тождественное" нажатие селектора не перекидывало бы на начало.
horz_offset
ставить, и самому скроллбару
XmNvalue
)?
После внимательного анализа всей цепочки действий, главное -- парочки
SetHorzbarParams()
и SetHorzOffset()
-- было
осознано, что достаточно сделать horz_offset=0
и
XtVaSetValues(,XmNvalue,0,NULL)
непосредственно перед вызовом
далее вызывающего их CalcPlotParams()
.
SetHorzOffset()
уже horz_offset==0 и тот считает, что цвет
менять не надо...
horz_offset=0
" руками, когда именно для этого и существует
SetHorzOffset()
-- его и надо вызывать.
15.02.2023: засим можно считать за "done".
20.02.2023: на пульт закинуто.
28.06.2018: разобрался -- всё там учитывается.
Сейчас же при загрузке из файла никаких обновлений нет, вот и вылезло.
И, кстати, сейчас проверил: в реальности проблема еще с v2'шного Chl_histplot.c: там, если порвать соединение с сервером (по циклам от которого идёт обновление/сдвиг самописца), тоже перестаёт реагировать на ресайз; и точно так же обновляется, если сменить горизонтальный масштаб.
SetHorzbarParams()
в
GraphResizeCB()
в ветку перед DrawGraph()
-- по
аналогии с Xh_viewport.c::ViewResizeCB()
.
Проблема ушла.
Ну оно и понятно -- архитектура histplot'а в v4 иная, тут он не единственный, а может быть много независимых экземпляров.
04.01.2019@вечер-по-пути-к-родителям-около-ЗолотойРощи (Морской, 26): ну и что -- вводить возможность указывать, что такой-то экземпляр histplot_noop'а должен ловить событие ToHistPlot (Shift+Btn3) вместо Chl_histplot'а?
В старом скрине weldcc всё показывается как надо при тех же границах.
Границы -- 1e-10-1e0.
16.04.2019: разбираемся. После получаса глазения в
код -- отличающийся от v2'шного, ради "оптимизации" -- была обнаружена
странность: там ДВАЖДЫ применяется log()
(показано для
mindisp
, а для maxdisp
аналогично):
mindisp = log(mindisp)
-- это
нововведение в v4.
RESCALE_VALUE()
также выполняется с
log()
-- вот это было и в v2.
Чуток исследований/расследований/археологии (излагается не по логике/временнОму развитию, а по мере разбирательства):
Чтобы не делать постоянное вычисление логарифма.
Этим также обусловлено добавление одноразовое вычисление
is_logarithmic
.
DrawGraph()
и DrawAxis()
одинаковая.
DrawGraph()
-- уже сразу такая; да-да, там УЖЕ была
оптимизация для не-вычисления-log()-постоянно.
DrawAxis()
-- всё равно логарифм границ считается
всего 1 раз (точнее, много, но эти разы разделены работой с другими
ручками: цикл по всем ручкам для каждого тика), так что в
оптимизации смысла нет.
DrawAxis()
по
ошибке, видимо, из-за перехода на использование GetDispRange()
(06.01.2016), так что прямое обращение типа v2'шного
rmins[KNOBS_RANGE_DISP]
стало бессмысленно и невозможно.
После устранения двойного логарифмирования подписи к осям исправились.
log()
в
RESCALE_VALUE()
.
Это отличается от v2. Смысл -- унификация кода DrawGraph()
и DrawAxis()
; плюс, сам код в последнем выглядит красивее (т.к.
унифицируются ветки is_logarithmic/не-is_logarithmic).
Т.е., проблема #2, в DrawAxis()
, решена.
Но НЕ проблема #1, в DrawGraph()
-- с ней надо ещё
разбираться. И пока неясно, родственна ли она хоть как-то проблеме #2.
17.04.2019: в порядке разбирательства с проблемой #1: провёл настольный эксперимент, запустив симулируемый сервер с 1 double-каналом, на который натравлен histplot с указанием отображать этот канал логарифмически, и последовательно пишем в этот канал различающиеся на порядок значения.
./sbin/cxsd -dsc configs/cxsd.conf -f <(echo dev a noop w1d -) :59 -b200000
histplot %5.0e:disprange=1e-10-1e0:localhost:59.a.0 hist_period=0.2
for i in {10..0}; do; ~/4pult/bin/cdaclient localhost:59.a.0=1e-$i; sleep 5; done
Результат -- равномерная лесенка.
Похоже, с histplot всё в порядке. Возможно, на сварке проблема из-за формулы, по которой вычисляется вакуум в паскалях:
# vacuum(volts)=10^(1.667*volts-d), where d=11.33mbar,9.33Pa,11.46torr define(`VAC_MES_FLA', `" _all_code; push 10; getchan vacuum_mes; mul 1.667; sub 9.33; pwr; ret; "')
Чуть позже: пообщался с Сашей Цыгановым, который и выдавал вышеприведённую формулу. Как он утверждает, формула вроде корректная (у него в исходной документации и график есть), но может глючить электроника лампы, посредством которой меряется вакуум, и тогда начинаются всякие ступеньки и прочие неправильности; в таких случаях надо менять лампу.
14.05.2019: в продолжение...
Семёнов признал, что проблема 1 обусловлена, скорее всего, #действительно проблемами с датчиком (лампой).
Засим можно считать проблему решённой.
12.04.2019: стал разбираться.
Добавление отладочной печати показало, что виноват
datatree_find_node()
-- он почему-то вместо NULL возвращает
что-то другое (но не совсем бред, а указатели куда-то рядом с существующими
ручками, идущими в списке далее).
Почему оно не сваливалось сразу, при "добавлении" -- загадка. А
сваливалось в момент нажатия MB3, судя по backtrace, потому, что пыталось
сделать XhWindowOf(NULL)
, что приводило к
XtParent(NULL)
.
Так что datatree_find_node()
невиновен (хотя глядя на его
код, ум за разум заходит и слабо понятно, как он работает -- видать, на
пляже у меня пробуждается гениальность :D).
А виновато именно то место, которое пытается что-то получить при помощи
XhWindowOf()
, да ещё и не удосужившись проверить !=NULL.
_ChlShowProps_m()
.
Очевидно, раньше никогда не встречалась ситуация, чтоб методу скормили узел без ручки.
_ChlShowProps_m()
исправления
внесены: оно теперь пытается найти вверх по иерархии первый же узел с
w!=NULL.
Но там надо будет ещё помозговать над решением: либо вытащить в отдельную функцию (т.к. юзеров в сумме трое -- ещё BigVals и сам Histplot), либо ещё и иной метод использовать (например, брать w от корневого узла).
А тут проблема, пусть и неместная, решена, так что "done".
_ChlToHistPlot_m()
сделан фикс от
SIGSEGV'а.
Фикс чуть отличается от KnobProps/BigVals'ового, т.к. тут ещё и
_ChlPrivOf(win)
выполняется.
09.10.2019: это было сделано "за компанию": возился с fastadc+Xh_plot, которые нужно было научить рисовать "толстые" линии, полез сюда на "посмотреть, как их делают и как указывают включение из параметров", и обнаружил, что в histplot'е работает только переключение вручную, хотя параметр "mode=..." в MotifKnobs_histplot_noop.c вроде бы и предусмотрен (но просто ничего не делает).
Итак:
k_md
и k_xs
(этот -- за
компанию, на будущее) были вытащены из
MotifKnobs_CreateHistplot()
(были короткоживущими переменными)
в MotifKnobs_histplot_t
(теперь это поля, "долгоживущие").
MotifKnobs_SetHistplotMode()
(и меняет режим,
и отображает его в селекторе), для доступа снаружи, ...
ModeKCB()
также переведана на неё.
MotifKnobs_SetHistplotMode()
при
надобности.
histplot_privrec_t.mode
теперь по умолчанию =-1.
mode
, табличка
mode_lkp[]
и строка в text2globopts[]
.
MotifKnobs_SetHistplotMode()
при надобности.
Всё работает.
Сначала общая моя и ЕманоФеди ракация "да ну нафиг!", но потом я решил-таки подумать.
13.02.2023: вспомнил, что там ведь при нажатии мышью на графике кроме отображения значений в этой точке в строках подписей также где-то отображается и время этой точки. Т.е., МЕСТО под отображение есть.
Полез в файл разбираться:
time_dpy
, длину оно имеет под
"HH:MM:SS.nnn" -- т.е., время влезет, а вот дата никак.
set_x_index()
, ...
x_index
.
Итого: да, сделать отображение текущего времени МОЖНО.
view_only
.
...а также лишь если на осях не будет отображаться реальное время (как есть проект), но лишь относительное, как сейчас.
MotifKnobs_UpdateHistplotGraph()
, а точнее, в вызываемом им
DisplayPlot()
.
set_x_index()
должен быть в курсе и
вместо очистки поля возвращать туда текущее время.
15.02.2023@утро-мытьё-посуды: ортогональный вопрос -- отображать только с секундами или и с миллисекундами тоже?
Раз уж это поле будет по факту "индикатором деятельности", то при периодах меньше 1 секунды имеет смысл и миллисекунды тоже показывать.
А откуда в самом MotifKnobs_histplot.c брать знание о размере периода, если данная информация относится к компетенции юзера этого модуля? Замерять интервал между моментом текущего отображения и предыдущего?
(Чуть позже) да нифига -- ведь histplot должен знать период, для
корректного отображения времён на горизонтальных осях. И период передаётся
MotifKnobs_CreateHistplot()
'у параметром
cyclesize_us
и хранится в одноимённом поле в hp
.
15.02.2023: по основному делу появилась идея, как организовать оптимальнее/элегантнее/прямолинейнее:
ShowCurTime()
", отобращающую
текущее время,
DisplayPlot()
, и из
set_x_index()
вместо явной выдачи "" (при осознании,
что переходим из режима отображения под курсором к неотображению),
Делаем:
ShowCurTime()
, формирующая пустую строку при
ненадобности отображения, а при надобности -- строку со временем, включающим
миллисекунды в случае cyclesize_us < 1000000
, и затем эту
строку выводящая в поле.
set_x_index()
переведен на её вызов.
DisplayPlot()
вызов также добавлен, рядышком с
UpdateAllPlotRows()
, условно при не-отображении под курсором.
Проверил -- на вид вроде работает как надо, в т.ч. при режиме просмотра файла (когда при неотображении под курсором показывает пустоту).
20.02.2023: на пульт закинуто. Будем ждать возможных отзывов от дежурных и прочих Лебедевых.
Я-то Феде сказал, что это скорее проблема "ну не требуйте от софтины больше, чем успевает железо!", но обещал посмотреть, что можно сделать. Ведь совсем необязательно всё перерисовывать каждые 0.2 секунды
14.06.2024: кстати, нифига -- в linthermcan.subsys никакого "hist_period" не указывается, так что там умолчательное 1Hz. И даже оно слишком напрягает при отрисовке 2 десятков каналов за сутки...
13.06.2024: посмотрел Chl_app.c -- там надо в
HistoryCycleCallback()
добавить делитель частоты, чтоб
перерисовывать не каждый цикл, а каждый N-й. Параметр hist_update_frqdiv=N?
comment2@душ перед сном: неа, не "update", а
"redraw" -- hist_redraw_frqdiv. И в утилиту histplot тоже не помешает.
14.06.2024: занимаемся.
chl_privrec_t
"hist_redraw_frqdiv" и "hist_redraw_ctr", стал изучать работу блока
обновления графиков тут в v4 и в v2, где был histring_frqdiv
.
Таким образом мы пресечём накапливание несделанной работы -- обычно будет оставаться максимум 1 несделанное, т.к. следующие "в очередь" просто не добавятся.
И после обновления счётчик пропущенных сбрасывать. Это убъёт двух зайцев: и новый цикл подсчёта после принудительного обновления запустит, и в момент "мы всё-таки успели -- обновимся!" сбросит счётчик неуспеваний.
Реализовываем.
HistoryCycleCallback()
добавлено получение
timenow и сравнение его с cycle_end;
Да, под нагрузкой --
"linthermcan hist_period=0.01 hist_len=864000
",
т.е., 100Hz -- видно, что начинает не успевать, и это неуспевание всё
накапливается (добавлена выдача числовых значений обеих точек времени, и
timenow опережает на всё большее время).
return
(пока безо всяких
счётчиков).
И да, ситуация магически улучшилась: хоть тормоза остались (ибо цепочка v5p2,duct,x10sae,star,p320t), но отставание расти перестало.
Как-нибудь бы в зависимости от периода; но как?
15.06.2024@утро, после завтрака: как вариант -- можно помнить время последней отрисовки, и если с него прошло больше 1-2-5 секунд, то тогда принудительно. А никаких счётчиков и "пределов пропусков для принудительной отрисовки" не нужно.
15.06.2024@утро, после завтрака,
моючи посуду: ещё была мысль "а пусть timestamp последней отрисовки
сохраняется не в HistoryCycleCallback()
, а прямо в процедуре
рисования -- чтобы и перерисовки в результате скроллинга, перетасовки
каналов и прочих причин тоже учитывались". Но нет: отрисовки то
в Chl-клиентах могут быть МНОЖЕСТВЕННЫЕ, в разных экземплярах histplot'ов.
15.06.2024: доделываем.
chl_privrec_t
добавлено поле hist_last_redraw_time
-- тот самый timestamp.
При копировании сделанного в histplot.c:
осознано, что всякие
cycle_start
/cycle_end
/cycle_tid
--
всё равно локальные, а не в privrec'е. (Да, это не вполне правильно с точки зрения
потенциальных (но так и не существующих!) многооконных программ.) Поэтому и
timestamp переехал в локальную last_redraw_time
.
gettimeofday()
ПОСЛЕ перерисовки (чтобы
считать с её окончания).
hist_last_redraw_time
прошло менее 5 секунд, то только тогда
return
.
Провёл тестирование под той же нагрузкой 100Hz (да ещё с v5p2 на p320t) -- вполне приемлемо, даже умудряется реагировать на ввод юзера.
Надо теперь это воспроизвести в histplot.c.
...часом позже -- сделано, довольно тривиальным копированием из
Chl_app.c. Аналогичный тест с
"hist_period=0.01 hist_len=864000
"
дал те же результаты -- по сети подтормаживает, но на конечное время и
остаётся отзывчивой.
cda_stop_formula()
.
Побудительный мотив непосредственно сейчас: Лебедеву надо циклировать один канал в linmagx, писать скрипт для этого под v2 крайне лень, а тут всё предусмотрено -- грех не воспользоваться.
02.10.2014: сделан в рудиментарном варианте -- просто кнопки [Start] и [Stop]. Даже статус исполнения формулы не показывает -- ибо никак, нет никаких механизмов узнать его.
25.11.2015: внутренности делались копированием v2'шных, но есть идеологические отличия.
MotifKnobs_oneled_t
, MotifKnobs_oneled_*()
API)
MotifKnobs_leds_t
и API
MotifKnobs_leds_*()
.
Последний
MotifKnobs_leds_create()
передаются cid
и
parent
, которых он сохраняет в свой объект.
Ему также указывается parent_kind
, могущий сейчас был как
_UNKNOWN, так и MOTIFKNOBS_LEDS_PARENT_GRID
-- тогда делаются
все надлежащие манипуляции XhGridSet{,Child}*()
.
MotifKnobs_leds_t.leds
(см. grow() ниже).
MotifKnobs_leds_grow()
-- собственно, _create() после создания
объекта просто вызывает _grow(), а уж клиент потом должен её дёргать в
реакции на событие NEWSRV.
25.11.2015: проверено, работает, "done".
01.12.2015: с вводом события SRVSTAT надобность в
MotifKnobs_leds_update()
исчезает -- теперь штучные LED'ы
должны обновляться индивидуально по мере надобности.
17.12.2017: да, переведено на ту индивидуальность. Через ДВА года с лишним, блин!!!
04.12.2015: он всё еще пользуется KeepaliveProc, а
надо бы перевести на CDA_CTX_R_SRVSTAT
и
CDA_CTX_R_NEWSRV
.
17.12.2017: переведён (хотя и не он, а сам _cda_leds.c), так что "done".
MotifKnobs_leds_update()
.
Также, поскольку теперь объект становится таким "активным", должен быть метод его удаления (для cx-starter'а, который сможет пере-считывать конфигурацию).
17.12.2017: делаем:
leds_context_evproc()
, реагирующий
на SRVSTAT и NEWSRV.
MotifKnobs_leds_destroy()
. В нём и
safe_free(leds->leds)
делается, и context_evproc снимается.
Вызов его в DestroyLedsNoop()
добавлен.
CDA_CTX_R_SRVSTAT
. Которая отключена
(маска указывалась в cx-starter_Cdr.c).
CDA_CTX_R_NEWSRV
(которое никак не
использовалось) оставлено, и еще надо будет добавить туда привязывание мыши
к новопоявившимся индикаторам -- что-то вроде
AddServMenu(sr->leds.leds[ns].w, sys, info_int)
Только одна проблема: порядок вызова evproc'ев. Ведь leds'ов будет
вызываться ПОСЛЕ cx-starter'ова, так что собственно лампочки-то в этот
момент еще не будет... Всё-таки руками вызывать
MotifKnobs_leds_grow()
(благо, он еще НЕ удалён из API)?
MotifKnobs_leds_grow()
и MotifKnobs_leds_update()
,
но явно пока не стоит (по причине cx-starter'а -- см. предыдущий пункт).
Проблема теперь будет в cx-starter'е еще и в том, что он пытается стартовать сервера по ИМЕНАМ машин, а тут будет только IP-адрес...
29.02.2020@дома-суббота:
вот только в pzframe_knobplugin.c и pzframe_main.c было
забыто -- там по-прежнему ежесекундно дёргался
MotifKnobs_leds_update()
.
Убрано -- удалены LEDS_KeepaliveProc()
из обоих файлов.
12.05.2020: невтепрёж становится -- пора!
Да и как-то придётся передавать в ledCB
ДВА параметра:
указатель на leds и номер сервера.
w
совпадёт с
получателем callback'а.
Об эфективности тут можно не беспокоиться -- событие крайне редкое и всё равно с юзером связанное.
У нас уже есть поле cid
в
MotifKnobs_leds_t
-- ну так можно прямо из cda_leds и дёргать
соответствующий метод cda.
...да, как бы это не совсем красиво -- с точки зрения разделения полномочий; но зато дёшево и просто. А если станет не хватать (захочется мочь другое действие повесить на клик по лампочке; что вряд ли), то всегда можно будет переделать.
ledCB()
добавлены:
nth
ставится =-1
; так что если вдруг (как бы*)
найдено не будет -- то даже так не беда, поскольку предполагаем сделать
правило, что при неуказанности номера сервера операция вызывается для всех.
cda_reconnect_srv()
(пока закомментированный).
13.05.2020: раскомментирован, в связи с его опубликованием.
Ну пока и всё.
13.05.2020: проверено при помощи отладочной печати -- да, нажатие на лампочку сервера приводит к вызову запроса на реконнект.
Так что здесь -- "done".
04.07.2016: сделано, в основном копированием из v2'шного -- файл-то маленький. Вроде работает.
05.07.2016: работает точно так же, как и в v2. Т.е., посредственно :)
Но "для сейчас" -- достаточно.
Делаем.
Короче -- сделано: просто добавлен один PSP_P_FLAG(), вроде бы без побочных эффектов.
28.04.2018: детали и анализ:
is_rw
-ручек форсится no_units=1
.
Выглядит странно, да?
units
,
добавляемого лишь rw-ручкам.
У ro-ручек же он отсутствует, а содержимое k->u.k.units
добавляется прямо к числу.
MotifKnobs_SetTextString()
-- не обращая никакого внимания на
значение флага no_units
(которого на том уровне и нет), а глядя
лишь на то, что виджет не-editable (даже НЕ на is_rw
!).
Таким образом, единственный способ избавиться от показа единиц у ro-ручки -- не указывать их вовсе.
When you click the widget or type anything into the widget, it enters an 'active' state, indicated by a different background color. It will now stop displaying received PV updates so you can edit without interruptions.-- и перестаёт обновлять, и визуально показывает факт режима редактирования (у нас -- зелёной рамкой).
Press 'Enter' to confirm. Press 'Escape', 'Tab', or click elsewhere to abort.-- тут просто в точности всё так же.
А может, просто сделать ОТДЕЛЬНЫЙ knob-компонент, который бы выводил именно ЦЕЛЫЕ числа? А формат -- чтобы использовал обычный dpyfmt, превращая %f в %d, а %a в %x.
И да, для простоты реализации достаточно только отображающего компонента, отдельный ввод целых ни к чему.
22.02.2020@дома-суббота, вечер, за просмотром первых 2 серий "Триггер": делаем.
dpyfmt
, превращая %f в %d, a/%A в %x/%X, а %o делаем,
%например, из %e.
Модификация только в определении vmt - она тут одна-единственная.
NEW_
и заменой "Text
" на
"IntText
":
NEW_MotifKnobs_CreateIntTextValue()
-- она по факту
пока не отличается от своего прародителя, т.к. использует
GetTextColumns()
вместо надлежащего
GetIntColumns()
, поскольку ей негде взять целочисленный формат.
И да, это некоторая идеологическая проблема.
NEW_MotifKnobs_CreateIntTextInput()
-- эта по факту
лишь макет, т.к. в ней закомментирована бОльшая часть, требующая
соответствующего функционала в виде Int-версий TextChangeCB()
,
TextUserCB()
, TextKbdHandler()
,
TextWheelHandler()
.
NEW_MotifKnobs_SetIntTextString()
-- вот эта в конечном
итоге полнофункциональна:
int_dpyfmt
, ...
double
в int
сделано
максимально простым способом -- (int)v
, безо всяких
trunc()
(подсмотрено в SelectorSetValue_m()
, да и
в AlarmonoffledSetValue_m
делается так же).
В остальном же -- удаление лишних пробелов, добавление units -- она идентична своей double-пародительнице.
Микро-обсуждение:
NEW_
-функции должны
будут войти в состав MotifKnobs_internals.c.
int_dpyfmt
.
DATAKNOB_INTK
, но это идея именно дурная, поскольку
функциональные отличия между double- и int-ручками минимальны.
23.02.2020дома, воскресенье,
~15:30: тогда уж скорее какой-то флажок, что обычная
ручка должна работать в Int-режиме, и тогда прямо ТЕ ЖЕ САМЫЕ функции меняли
бы своё поведение. И dpyfmt
бы интерпретировался (и парсился
бы!) как целочисленный. Единственное, что не вполне идеально в этой
идее -- "протокол" передачи данных всё равно останется через
double
(речь о методе SetValue()
и прочих
set_knob_controlvalue()
). Впрочем, и сейчас-то всё работает
через double
, и норм.
int_dpyfmt
, и в CreateIntTextKnob()
первым делом
выполняется цепочка:
ParseDoubleFormat()
.
conv_c
в соответствии с "правилом"; любой
неизвестный символ переводится в 'd'.
int_dpyfmt
посредством
CreateIntFormat()
.
Сделано использование в adc250_gui.c -- работает! В т.ч. ALTFORM-вариант %#10.0a, добавляющий префикс "0x".
Итого -- первоначальная задача поддержки ro-int-вывода для шестнадцатирички решена, а уж если понадобится что-то ещё, то и будем думать о реализации либо Int-вариантов для rw-ручек, либо вообще полноценной поддержки Int (23.02.2020: благо, микро-проект уже есть).
04.02.2021: сделана поддержка rw-ручек, НЕ по микро-проекту.
NEW_
, и в
"них" используется int вместо double.
NEW_HandleTextUpDown()
сделана пустой:
double
на
int
нельзя никак -- там и игры с гранулярностью шага и grpcoeff
(для целочисленных лишённые смысла), и групповое изменение, в случае
потенциально-разнотипных (double и int) ручек становящееся странным.
...хотя, в принципе, возможно, что обычный алгоритм пересчёта из
HandleTextUpDown()
, применённый к целочисленному, также
отработает нормально -- вследствие округления. Но это без гарантии --
учитывая "сбои на 1", наблюдённые на сварке и с козачиными блоками, когда
из-за неидеального округления получались совсем не те числа, что должны бы
по обычной арифметике.
Так что пока пусть стрелки вверх/вниз просто не работают.
IntTextKnobColorize_m()
, ранее почему-то не
использовавшийся -- вместо него стоял Common.
Но пока не проверено. А надо бы -- делалось-то ради диагностического скрина для rfmeas_l_timer (адреса и значения регистров), вот его и нужно изготовить.
05.02.2021: проверил -- да, работает. Хотя сначала казалось, что нет -- целочисленный формат "%x" себя ведёт сильно отлично от вещественных "%f", и в "обычной жизни", при обычном использовании для вывода, с этим не сталкиваешься:
Потому, что это для целочисленных форматов -- "the minimum number of digits to appear".
В результате формат "%04.0x" число 0 выдаёт как пустоту -- 4 пробела. Что, естественно, для полей ввода категорически неприемлемо (вообще-то это неприемлемо для всего, и зачем такое поведение -- для меня загадка).
Но это хотя бы некритично, а лишь просто некрасиво (хотя смысл такого поведения также неясен).
06.02.2021: сделал всё-таки и инкремент/декремент по
стрелкам и колесу мыши -- наполнение NEW_HandleTextUpDown()
.
Ни Ctrl (шаг/10), ни Shift (не-immed) не используются.
Причиной оказалось то, что в MotifKnobs_ExtractIntValue()
в
вызове strtol()
указывалось base=10 -- вот фиг знает, зачем так
было сделано.
После исправления на 0 всё пришло в чувство.
09.02.2021: убрано это лишнее ".0" из dl250.subsys и adc250_gui.c.
12.05.2018: итак:
Здесь определение наличия и взведение XM_TABBER_AVAILABLE
выполняется в MotifKnobs_tabber_cont.h.
В weldcc работает, помечаем как "done".
DATAKNOB_VECT
.
20.07.2018: сварганено в минимальном варианте -- скелет, в котором сделано отображение и колоризация, а создание пока простейшее, просто ячейки (с опциональными номерами), но без callback'ов/event-handler'ов для реального редактирования и отправки.
21.07.2018: первые проверки и добито до минимально приличного вида.
23.07.2018: пора делать callback'и/event-handler'ы для реального редактирования и отправки.
Следствие -- и фокус мы корректно отслеживать не можем, в смысле как у обычных текстовых полей: "при потере фокуса делать cancel_knob_editing()".
В результате переключаешься в другое окно, а зелёный прямоугольник редактируемости продолжает гореть.
А так -- вроде работает.
23.07.2018: а ведь по-хорошему можно было сделать и более простую форму -- реально ТЕКСТОВОЕ поле, в формате [ЗНАЧЕНИЕ{,ЗНАЧЕНИЕ}].
...либо -- N:[ЗНАЧЕНИЕ{,ЗНАЧЕНИЕ}]
ParseDatarefVal()
.
Не -- "matrix" (MotifKnobs_matrix_vect.c).
24.07.2018: да, переименовано: теперь ЭТО -- MotifKnobs_matrix_vect.c, а имя "text" оставлено для будущего реально простейшего.
Умолчательным пока будет именно matrix, а text'у отдадим это звание, когда он подрастёт.
Засим считаем за "done". Если захочется усовершенствований (многоколоночность etc.) -- отдельными пунктами.
CreateMatrixVect()
в строке 329 на вызове
XhGridSetChildPosition()
(на некоей машине neon со сравнительно
новой Ubuntu 22.04).
Вчера я попросил его собрать с OPTIMIZATION=-O0
, но сегодня
решил всё же глянуть на исходник -- ну мало ли, вдруг что очевидное, но даже
если и нет, то хоть заранее "въеду в тему".
28.02.2023: взгляд на вывод gcc при компиляции даже на x10sae (CentOS-7.3) показал наличие в этой строке 329 warning'а на путаницу pointer/integer:
и ранееMotifKnobs_matrix_vect.c:329:13: warning: passing argument 1 of 'XhGridSetChildPosition' makes pointer from integer without a cast [enabled by default] XhGridSetChildPosition (n_w, 0, y0 + n); ^ In file included from MotifKnobs_matrix_vect.c:11:0: ../../include/Xh.h:227:11: note: expected 'CxWidget' but argument is of type 'int' void XhGridSetChildPosition (CxWidget w, int x, int y); ^
MotifKnobs_matrix_vect.c:323:17: warning: assignment makes integer from pointer without a cast [enabled by default] n_w = XtVaCreateManagedWidget("rowlabel", ^
Т.е., этот "n_w
" ("Number Widget" -- метка с номером строки)
какого-то лешего объявлен как int
вместо CxWidget
(ошибка копирования из n
строкой выше?):
int n; int n_w;
Главный вопрос -- почему косяк проявился только сейчас? Ведь на любой 64-битной платформе запись 64-битного указателя в 32-битный int должна была привести к обрезанию значащих данных и катастрофе.
Исправил тип на CxWidget
, плюс добавил отладочную печать
самого значения. И получил любопытные числа, вроде
n_w=0xfb6f00
. Т.е., эти указатели вообще в 24 бита влазят. И
/proc/PID_OF_ringrf/maps это подтверждает:
x10sae:~% grep heap /proc/`pidof ringrf`/maps 00e8b000-01112000 rw-p 00000000 00:00 0 [heap]
На всякий случвй поискал "pointer from integer" во всём выводе от w_mkall.sh -- больше ничего критичного нету:
CreateSimpleKnob()
отсутствует приведение n
к
указателю.
unsigned char *sep_type
вместо просто "unsigned char" (в
v4'шном уже исправлено).
01.03.2023@вечер: кстати,
на E2K числовые значения указателей ещё меньше в разы --
n_w=0x29f970
, плюс
$ grep heap /proc/`pidof ringrf`/maps 00166000-003fc000 rw-p 00000000 00:00 0 [heap]
04.03.2023: осознал часть причины меньших значений адресов по сравнению с x86_64: тут программа маппируется с 0x00010000, в то время как на x86_64 -- с 0x00400000.
01.03.2023@вечер: Федя прислал результаты с его новомодного ядра, которое
-- n_w=Linux neon 5.19.0-32-generic #33~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon Jan 30 17:03:34 UTC 2 x86_64 x86_64 x86_64
0x56224ab2e990
(аж 6 байт, 48 бит).
(И да -- падение вылечено.)
Т.е., да -- изначальное предположение оказалось верным, проблема в обрезании при конверсии в int32 64-битных указателей, содержащих значащие данные за пределами 32 бит.
06.03.2023: сегодня Федя выдал и полный /proc/`pidof ringrf`/maps с той системы, и он выглядит прелюбопытно:
55c47f769000-55c47f783000 r--p 00000000 08:02 3409057 /home/femanov/4pult/bin/pult 55c47f783000-55c47f7cd000 r-xp 0001a000 08:02 3409057 /home/femanov/4pult/bin/pult 55c47f7cd000-55c47f7e5000 r--p 00064000 08:02 3409057 /home/femanov/4pult/bin/pult 55c47f7e6000-55c47f7ee000 r--p 0007c000 08:02 3409057 /home/femanov/4pult/bin/pult 55c47f7ee000-55c47f7f8000 rw-p 00084000 08:02 3409057 /home/femanov/4pult/bin/pult 55c47f7f8000-55c47f7f9000 rw-p 00000000 00:00 0 55c480ec8000-55c4811a8000 rw-p 00000000 00:00 0 [heap] 7f847a80f000-7f847ae00000 rw-p 00000000 00:00 0 7f847ae00000-7f847b374000 r--p 00000000 08:02 299793 /usr/lib/locale/locale-archive 7f847b3e6000-7f847b45a000 rw-p 00000000 00:00 0 7f847b48a000-7f847b533000 rw-p 00000000 00:00 0 7f847b533000-7f847b536000 r--p 00000000 08:02 301187 /usr/lib/x86_64-linux-gnu/libnss_resolve.so.2 7f847b536000-7f847b54b000 r-xp 00003000 08:02 301187 /usr/lib/x86_64-linux-gnu/libnss_resolve.so.2 7f847b54b000-7f847b552000 r--p 00018000 08:02 301187 /usr/lib/x86_64-linux-gnu/libnss_resolve.so.2 7f847b552000-7f847b553000 r--p 0001e000 08:02 301187 /usr/lib/x86_64-linux-gnu/libnss_resolve.so.2 7f847b553000-7f847b554000 rw-p 0001f000 08:02 301187 /usr/lib/x86_64-linux-gnu/libnss_resolve.so.2 . . .
Т.е.,
03.03.2023: возникла ещё очевидная идея: ведь для отладки -- нахождения программ, путающих int и pointer -- было бы полезно иметь возможность всё адресное пространство размещать за пределами 32-битного, т.е., чтобы младшие 4GB были бы вообще не маппированы и обращения туда вызывали бы SIGSEGV. Казалось бы -- ну напрашивается же такая фича. Погуглил "linux kernel force low 4gb unmapped on 64-bit" -- фиг, из 21 результата ничего по теме (а всё насчёт device drivers, DMA, IOMMU и т.п.). Возможно, я ищу не то, а оно управляется какими-то переменными окружения, анализируемыми динамическим загрузчиком.
Как бы то ни было, отладочную печать значения убираем, а раздел считаем за "done".
04.03.2023: в продолжение раскопок по теме "очевидная идея":
ld.so
-- ничего на эту тему.
strings -a /lib64/ld-linux-x86-64.so.2
"
(там имена переменных все сгруппированы толпой, я толпу искал по
"LD_LIBRARY_PATH") -- тоже ничего.
$LD_USE_LOAD_BIAS
, но она на код программы
влияния не оказывает (проверил), а только на динамические библиотеки.)
-fPIC
" (не-позиционно-независимым), то он в принципе не может
быть перемещён куда попало, так что не маппировать младшие 4GB -- увы, никак
(как минимум статические данные будут именно там, сразу после сегмента
кода).
Уф, надоело -- бросаю это исследование.
05.03.2023: кстати, посмотрел на раскладку в 32-битной программе -- xmbdfed, собранном, судя по дате 06-07-1999, ещё в чём-то вроде RedHat-5 или 6. Там -- как и встречалось мне позавчера в заметке "Reorganizing the address space" на LWN.net от 30-06-2004, где говорилось про немаппированность блока 0x00000000-0x08000000 -- адресное пространство кода начинается выше 0x08000000 (т.е., 128MB):
08048000-080a4000 r-xp 00000000 07:00 605662 /mnt/viper-10/viper/bolkhov/compile/xmbdfed-3.0/xmbdfed 080a4000-080aa000 rwxp 0005b000 07:00 605662 /mnt/viper-10/viper/bolkhov/compile/xmbdfed-3.0/xmbdfed 080aa000-080b4000 rwxp 00000000 00:00 0 09cd7000-09d39000 rwxp 00000000 00:00 0 [heap] f71e0000-f71eb000 r-xp 00000000 08:01 1607666 /usr/lib/libnss_files-2.17.so f71eb000-f71ec000 r-xp 0000a000 08:01 1607666 /usr/lib/libnss_files-2.17.so f71ec000-f71ed000 rwxp 0000b000 08:01 1607666 /usr/lib/libnss_files-2.17.so . . .
DATAKNOB_VECT
.
Он будет выглядеть как просто текстовое поле, с форматом [ЧИСЛО{,ЧИСЛО}].
24.07.2018: делаем на основе MotifKnobs_text_text.c -- они очень похожи.
DEFAULT_COLUMNS
увеличено с =10 до =20.
edata[]
.
Для этого аллокируется буфер tbuf
, объёмом в max_nelems раз
по GetTextColumns()
.
А в TextSetVect_m()
проверяется, что если места не хватает
-- то оно до-XtRealloc'ируется.
25.07.2018@утро-дома: а можно было б выпендриться проще: аллокировать ДВОЙНОЙ объём буфера, и первую половинку использовать для складирования получаемых от сервера данных (и реализации "cancel"), а вторую -- для парсинга в неё и отправки.
25.07.2018: деделываем.
...и пока что это всё -- всякие "undo" и мгновенные "cancel" при надобности сделаем потом; "done".
text_text_privrec_t
имеются поля defcols_obtained
,
text_deffg
, text_defbg
, реально не используемые
(работает стандартная MotifKnobs'овская колоризация).
Видимо, остались с 12-2009, когда оно копировалось с text_knob'а.
Убраны.
Потребность -- в том, чтоб куда-нибудь мочь помещать vect-узел, чтобы векторный канал постоянно запрашивался (это у Роговского в скрине-конфигураторе BPM'ов накопителя).
14.09.2018: ...сделано. Пока, правда, не проверено.
Смысл -- для onoff'ов с присутствующими victim'ами: чтобы в v5rfsyn/v5ie уметь дисэйблить селекторы делителей частоты при работе в режиме "Ext" (а в "Lcl" -- разрешать).
Можно, конечно, завести другой тип контекнера, но зачем? Проще добавить функционала этому.
15.10.2018: делаем. Проект таков:
Composite
-- compositeWidgetClass
.
Сделано. Проверено -- работает.
Заюзаны они были для того, чтобы «дисэйблить селекторы делителей частоты при работе в режиме "Ext"».
Соответственно, имеют ссылку на канал-источник.
Так вот: при очередной перетряске системы имён каналов (точнее, cpoint'ов) в canhw:19 имена каналов "Источник запусков" поменялись, и скрины были проверены по принципу "не горит ли где что чёрным цветом?".
Но cx-starter напротив v5rfsyn упорно показывал чёрную метку ненайденности. Я чуть голову не сломал, пытаясь понять причину.
Спасла только отладочная печать, вставленная в
CdrProcessKnobs()
по условию
"k->curstate == KNOBSTATE_NOTFOUND
"
-- указала ручку-источник проблемы.
Оказалось, что при перетряхивании имён в disp'ах, содержащихся в invisible-контейнере, остались старые, неисправленные имена. А тестирование глядением на скрин никакой черноты не показало -- оно ж в НЕВИДИМОМ контейнере! И показывал только cx-starter, которому пофигу на эти тонкости, он показывает кумулятивный результат всего, что есть -- вот в нём и вылезло.
02.07.2007: да, базовое населивание сделано. Пока что -- БЕЗ fdlg и stddlg, и без toolbar.
Но -- теперь команды являются строками, а не числами. И неуказанной считается команда NULL -- как раньше 0.
02.08.2007: и Xh_stddlg также сделал, для нужд subwin_cont'а.
04.09.2015: делаем.
XhCommandProc
дополнен параметром
info_int
.
А вот проверить пока никак -- нету ж пока ничего, что бы генерило команды.
14.01.2015@утро-лыжи-5км: и еще: давно -- 02-07-2007 -- для Xh_fdlg было придумано, что "результат" (Ok или Cancel) они будут передавать префиксом "+" или "-", после которого уже сам текст "команды". Но то дико неудобно -- пришлось бы выпендриваться с укладыванием префикса+команды в некий буфер.
А с info_int
всё становится тривиально: Ok будет
генерить info_int=0, а Cancel info_int=-1.
Причина -- что в (долбанутом) GNOME3 под RHEL7 все заголовки окон
показываются "крокозябрами" (латиницей с акцентами), поскольку тамошний дурной
window manager (gnome-shell?) игнорирует уставки локали клиента --
WM_LOCALE_NAME
, которая честно стоит в "ru_RU.KOI8-R"
(и, судя по
гуглению
на тему «"gnome-shell" "WM_LOCALE_NAME"», никого это и не
парит...).
А еще хочется, чтобы он дозволял "tool button plugins" (см. bigfile-0001 от 10-05-2005).
19.01.2016: за прошедшую неделю база изготовлена. В основном копированием из v2, но с модификациями (основное -- что команды теперь строковые, отсюда всё и пляшет).
А вот сами описатели кнопок -- вовсе не обязаны быть персистентными, из них информация добывается при создании и больше они не нужны.
XmNuserData
) избавлено.
closure
(потому и
требуется персистентность строки).
XmNuserData
.
ActOnCB()
с дополнительным параметром
type
.
Требуемый минимум работает.
23.09.2016: а вот зря сделан п.2 ("избавлено").
Сейчас понадобилось-таки реализовать XhSetCommandOnOff()
и
XhSetCommandEnabled()
, а фиг -- неоткуда для конкретной кнопки
взять её команду!
15.02.2020: грустно, что оно не работает -- халтурновато выглядят скрины осциллографов.
17.02.2020@лыжи: очевидно, что делать нужно просто по иной архитектуре: аллокировать "массив информации о тулбаре", который заполнять (по ячейке на кнопку) типами+командами плюс туда же складывать ссылки на виджеты кнопок.
22.02.2020@дома-суббота: делаем:
XhToolBtnInfo
, содержащий поля
type
, cmd
, btn
(последнее --
Widget
).
struct _WindowInfo
добавлено
XhToolBtnInfo *tbarButtons
.
XhCreateToolbar()
выполняется основная работа:
(А XtFree()
его делается в XhDeleteWindow
.)
XhSetCommandOnOff()
осталась уже
несложная работа -- проходимся по tbarButtons[] и ищем XhACT_CHECKBOX с
совпадающей командой; при нахождении проверяем, в том ли они состоянии, что
уставляется, и если нет -- то инвертируем.
tbarButtons
, избавляясь от использования
XmNuserData
.
XhToolBtnInfo
добавляем поле pressed
.
XmNuserData
.
Это, кстати, сильно проще, чем вычитывать и прописывать ресурс -- простое обращение к полю.
COMMAND_CB()
и
CHECKBOX_CB()
плюс выполняющей саму работу
ActOnCB()
перейдено на единый toolCB()
(да-да, как
в v2, только место хранения данных иное -- общий массив вместо
индивидуальных-в-userData).
В качестве closure передаётся "номер кнопки в тулбаре" -- b
,
индекс в массиве tbarButtons[]
.
Ну и за компанию --
XhSetCommandEnabled()
.
Да, проверено, работает. Хотя на современном Motif'е выглядит некрасивовато.
19.09.2016: еще энное время назад пришла в голову идея
(вроде записывал, а где -- хбз): для маленького варианта
тулбара желательно иметь маленькие пиктограммы, прямо в тех же самых
xh_actdescr_t toolslist[]
. С одной стороны, вроде как нехорошо
расширять xh_actdescr_t
, но с другой -- в v4 же её
использование весьма лимитировано, так что -- легко!
Собственно, действия:
xh_actdescr_t
: поле mini_pixmap
добавлено.
Никогда так и не использовавшееся поле hotkey
удалено.
Макросы XhXXX_*()
адаптированы, в
XhXXX_TOOLCMD()
и XhXXX_TOOLCHK()
добавлен
параметр mini_pixmap
.
XhWindowToolbarMiniMask
, при наличии
которого XhCreateToolbar()
'у в свежевведённом параметре
use_mini
передаётся 1, и оно пытается вместо полей
pixmap
использовать mini_pixmap
.
20.09.2016: продолжаем:
XhCreateToolbar()
допилена так, чтобы при use_mini и поля
вокруг кнопок делать поменьше, и XhACT_LABEL
'ам назначается
другой класс -- miniToolLabel, для которого в
Xh_fallbacks.h назначен шрифт поменьше.
Но в первом умолчание именно она, а во втором -- "maxitoolbar".
20.09.2016@дома: кстати, тулбарчик теперь настолько маленький, что его даже в мелких программах (вроде camsel/ringcamsel/turnmag) можно не отключать, а noop leds из них убрать.
21.09.2016: да, так и сделано -- высота увеличилась всего на 4 пиксела.
Далее было проделано добавление нужных кнопок (параметры with_NNN_btn) в конкретные скрины. И тут обнаружилось, что кнопка [Freeze] в linipp не работает -- просто нет этого функционала вовсе. Ну приступили к деланью...
data_subsys_t.is_freezed
.
ChlHandleStdCommand()
.
ProcessContextEvent()
: не выполняется обновление
дерева ручек (процессинг подсистемы).
CycleCallback()
: история не сдвигается.
knobprops_rec_t
добавить backreference на subsys.
22.09.2016@утро-дома-и-мимо-ИХБФМ: в принципе, можно в pzframe_knobplugin.c
сделать метод
"PzframeKnobpluginHandleCmd()", указывать его во всех
hw4cx/pzframes/SRC_*_knobplugin.c, и чтоб он вызывал
PzframeDataSetRunMode(,!info_int,-1)
. Одна проблема: на
уровне MotifKnobs НЕОТКУДА получить значение Chl'ного
CHL_STDCMD_FREEZE
. И чё делать? Или
переводить эту команду с уровня Chl на уровень Knobs?
23.09.2016@утро-душ: а даже забавнее: ввести такую команду в datatree.h, а в Chl с-алиасить её:
#define CHL_STDCMD_FREEZE DATATREE_STDCMD_FREEZE
23.09.2016: да, так и сделано. Работает -- паузится.
Засим считаем пункт о мини-тулбаре выполненным.
Причина -- то, что при запуске chl-клиента с пульта на t320p (через цепочку star,x10sae,duct,v5p2) им пользоваться вообще невозможно -- оно дико тормозит на первом же обновлении, и после этого всё (даже запуск с ключом "with_freeze_btn" и максимально быстрое нажатие этой кнопки ничего не дают).
Позже, уже после реализации: да, замечено, что обновление по кнопке "once" длится просто невообразимо долго -- чуть ли не минуту. Если таких обновлений валит со скоростью 5Hz, то оно сразу же и заткнётся намертво.
24.05.2022: делаем (записано не по порядку работы, а
по окончательным результатам; сама же работа делалась по принципу "найдём,
где используется is_freezed
, и добавим рядышком" плюс
подсматриванием касательно oneshot
в pzframe_data.c):
CHL_STDCMD_ONCE
равное
DATATREE_STDCMD_ONCE
-- см. далее.
DATATREE_STDCMD_ONCE
в "datatreeOnce" -- это для
унификации, чтобы рассылаемые Chl'ем команды были понятны knobplugin'ам.
oneshot
-- сразу после is_freezed
.
stdtoolslist[]
добавлена кнопочка
CHL_STDCMD_ONCE
.
appopts_t
добавлено поле with_once_btn
;
is_freezed
и oneshot
для
возможности указывать "замри" и "однократно всё-таки отработай!" из
командной строки.
text2appopts[]
-- флажки для всей этой троицы.
ChlRunSubsystem()
реакция на них:
with_once_btn
аналогична прочим
with_*_btn
-- при невзведённости соответствующая кнопка из
toolslist[]
'а превращается в XhACT_NOP
.
ChlHandleStdCommand()
реакция на ONCE тривиальная --
просто взводится subsys->oneshot
=1.
ProcessContextEvent()
и
Chl_knobprops.c::Update1HZProc()
-- принятие во
внимание oneshot
в дополнение к is_freezed==0.
CycleCallback()
, но потом стало очевидно,
что место ему -- в обработке CTX_R_CYCLE, а на ИСТОРИЮ оный oneshot вообще
не должен оказывать влияния.
Поэтому сброс oneshot0 переехал в
Cdr_treeproc.c::ProcessContextEvent()
, а из
циклирования истории он убран, как и сам учёт oneshot
.
После этого, кстати, и указание "oneshot
" в командной строке
стало иметь эффект.
CycleCallback()
переименован в HistoryCycleCallback()
.
subsys->oneshot
: в knobprops же обновления делаются
асинхронно, по собственному 1Hz-таймеру, а выставление и сброс -- синхронно,
по действию юзера и по приходу CDA_R_CYCLE соответственно.
В результате там обновляется "через раз".
25.05.2022@утро-просыпаясь: надо
бы как-то ловить событие "ONCE" и выставлять СОБСТВЕННЫЙ флаг в
knobprops_rec_t
, по которому и ориентироваться.
Итак, после рихтовки заработало -- цель достигнута, можно и тормозить прямо из командной строки, и "разово" производить отображение.
Далее решил сделать интеграцию с pzframe-knobplugin'ами:
PzframeKnobpluginHandleCmd_m()
добавлена реакция на
DATATREE_STDCMD_ONCE
.
25.05.2022: разобрался: дело в том, что в
Chl_app.c minitoolbar
/maxitoolbar
и
notoolbar
-- это вообще совершенно раздельные вещи: они
регулируются РАЗНЫМИ полями appopts_t
, поэтому указание
mini/maxi вообще никак не отменяет отключенности.
Исправляем: переводим это всё на ЕДИНОЕ поле:
TOOLBAR_KIND_MAXI
=0,
TOOLBAR_KIND_MINI
=1, TOOLBAR_KIND_NONE
=2.
toolbar_kind
.
text2appopts[]
подтюнена.
toolbar_mask
.
А minitoolbar_mask
изведён за ненадобностью.
И всё стало как надо.
...надо было ещё ТОГДА, в 2016-м, в Chl делать именно так, а не пытаться миррорить битовые флаги Xh'а, устроенные не самым очевидным образом.
blm minitoolbar with_freeze_btn with_once_btn is_freezed oneshot
-- что knobplugin'ы радостно бегут.
Ну оно и очевидно -- ИМ-то никто никакой паузы не делал.
А чтоб останавливались -- нужно бродкастить команду
DATATREE_STDCMD_FREEZE
.
25.05.2022: допилки:
is_freezed
и/или
oneshot
выполняется бродкастенье соответствующей команды.
Поскольку ранее проверка+копирование этих флажков были ДО создания корневого knob'а, то смысла бы не было. Поэтому оно перенесено в место ПОСЛЕ оного создания.
Проверено поштучно -- да, оба бродкастенья возымевают действие.
26.05.2022: а вот с "правильным" обновлением knobprops проблема, идеологического свойства: просто непонятно, как же сделать -- вроде бы сделать-то можно, но оно получится "грубой силой", некрасиво, халтурно и потенциально ненадёжно для будущих модификаций.
ProcessContextEvent()
при
is_freezed && oneshot
;
это будет вообще самым "метким" способом. Само обновление -- просто в
чистом виде работа ShowCurVals()
.
Но для этого придётся расширять dataknob_cont_vmt_t
для
добавления метода "обновить knobprops".
...так-то можно воспользоваться имеющимся методом
ShowProps()
, но это криво.
Update1HZProc()
на значение is_freezed
и
продолжать обновлять. Ну подумаешь -- будет раз в секунду несколько
XmText'ов перерисовывать, невелик поток.
...но это тоже кривизна...
Так пока и оставим, за неимением лучшего.
В общем -- ну не складывается никакого красивого решения. Похоже, надо отложить эту проблему в надежде, что решение придёт само.
30.05.2022: ещё чуток:
Отображается на поле remdbg
, при взведённости которого все
соответствующие флажки взводятся в 1 (и только toolbar_kind
делается =TOOLBAR_KIND_MAXI лишь при !=TOOLBAR_KIND_MINI: если НИКАКОГО не
указано, то включается MAXI, а MINI не трогается).
XhACT_CHECKBOX
, вместо должного XhACT_COMMAND
--
следствие копирования; в результате того косяка кнопка ONCE появлялась у
всех скринов.
24.07.2007: выяснилось, что проблема была в
каком-то значении ресурса XmNcolumns
в XmTextовом варианте
statusline'а -- значение по умолчанию, узнать которое не удалось, ибо
editres его не видит.
Решилась проблема принудительной уставкой XmNcolumns:=1. Функциональность от этого никак не пострадала, поскольку виджет приаттачен к форме и растягивается по ширине окна.
02.07.2007: да очень просто -- в качестве Ok будет идти "+"КОМАНДА, а Cancel -- "-"КОМАНДА. И это даже лучше -- вариаций будет больше (разными символами-префиксами), а не только 2.
14.01.2015@утро-лыжи-5км:
а вот и нифига: вместо махинаций со строками просто воспользуемся введённым
с той поры info_int
.
DoOkCancel()
,
которой передают готовый info_int
.
17.07.2007: во-первых, читалка из .so-файлов вообще-то нафиг не нужна -- намного более удобной будет читалка из текстовых файлов.
Во-вторых, скорее всего, нам на все случаи жизни хватит ОДНОГО парсера -- а отличаться будет только то, ОТКУДА он будет брать строки.
Посему -- изготовим некий модуль, умеющий парсить, и чтоб его
функциям передавались указатель на функцию "читай следующую строку" и
некий специфичный для функции указатель -- обычно "контекст",
содержащий либо FILE *fp
, либо char *
для чтения из текстовой строки, либо что-то подобное для БД, и т.п.
P.S. А для описания таких вещей -- заводим CdrP.h
(Да, немного это перекрывается с областью компетенции протокольных плагинов в cda, но здесь ключевое -- это именно отвязанность от подсистемы.)
Смысл -- чтобы можно было считать несколько описаний, а потом хоть мышью накидать некоторые их части на окошко. Как это делала программа knobs на CTF3.
22.07.2007: хм, а дело-то ведь даже несколько в
другом: если в CXv2 были отдельные операции
CdrOpenDescription()
и
CdrCvtGroupunits2Grouplist()
, т.е. -- отдельно "добыча"
описания и отдельно "realize" его, то в нынешнем проекте предполагалось
делать это ОДНОЙ операцией, в чем и кроется корень сложностей.
Решение же -- крайне простое: отделить операцию "realize" от "добычи", повесив ее (естественно, в ЕДИНСТВЕННОМ варианте) на ядро Cdr.
Для этого, конечно же, потребуется иметь ДВА комплекта всех полей
*_ref -- исходные текстовые и собственно CxDataRef_t
'ы.
(И, по соглашению, CxDataRef_t==0 будет считаться за
"неопределено"/"отсутствует".)
23.07.2007: вот только может понадобиться вызов типа "CdrIsDatarefValid()", чтобы на совсем уж бредовые ссылки ругаться прямо при парсинге.
Хотя, из-за разнообразного формата ссылок парсер и так должен будет немало о них знать... Ладно, бум думать.
27.11.2013: собственно -- а ведь еще почти тогда
же, летом 2007-го, такое разделение и было сделано: отдельно
считывание, и потом отдельно realize (которое, правда, пока
просто return 0
, но это непринципиально). Так
что считаем за "done".
10.08.2018: мелкое дополнение:
CdrClearErr()
теперь делает еще и errno=0
(по
следам вчерашних разборок с CdrRealizeKnobs()
, когда в
диагностику давалось сообщение от давнего ENOENT).
14.08.2007: у узлов есть несколько полей, которые
в принципе могут заполняться treeproc'ом -- например, в CdrRealize*()
-- uplink
, u.c.subsyslink
,
u.k.curstate
. И такое -- централизованное -- заполнение
вроде бы проще и "корректнее", но... Пока что -- раз CdrRealize*()
пока нету -- приходится проставлять эти поля прямо в
Cdr_fromtext.c::ParseKnobDescr()
. И, может, оно и
правильнее -- тем самым на выходе из "добытчика" мы имеем корректное,
готовое к использованию дерево. Посмотрим.
04.08.2010: это включает в себя две схожих вещи:
Здесь-то есть "параметры ручек", но:
Представляются разумными такие решения этих проблем:
.variable ИМЯ_ПЕРЕМЕННОЙ ЗНАЧЕНИЕ
WHAT:VALUE
". Но само-то значение указывается всегда,
безо всяких префиксов... Выходов два:
В принципе, применимы хоть оба подхода сразу, но для неизбыточности лучше остановиться на 2-м. И еще не забыть, что аналогичный же формат должен подходить и для не-скалярно-числовых каналов -- т.е., для строковых и для массивов и/или "больших каналов" и DATAKNOB_USER.
.channel КАНАЛ ЗНАЧЕНИЕ
Это нужно для "инициализации" -- записи в железо неких заранее известных значений, например, при старте софтины. Требуется, чтобы такой файл легко редактировался вручную (или вообще генерился).
Такое-то вроде и сейчас должно быть делаемо, при условии "глобальных" имён каналов.
И еще в ту же степь:
26.02.2013: под "что из информации", видимо, подразумевалось "какие классы информации" -- значения, диапазоны, ...
26.10.2010: и еще один подвид информации, требующей сохранения: "экранные данные" всяких штучек типа fastadc, а именно
Сейчас в v2 это делается просто экранными ручками/полями, специфично для программы.
А превращение этих вещей в ручки/переменные позволит обращаться с ними стандартным образом,
Единственное что -- ведь после считывания эти вещи надо как-то верифицировать...
24.10.2012@Снежинск-каземат-11: и еще некоторые соображения о режимах.
Ответ: адресация должна быть всегда одинаковая, без разницы -- обычная это ручка или "настройка". Следовательно --
25.10.2012@Снежинск-каземат-11: лучше бы нет: юзеры хотят мочь, например, значение в репере тоже отправлять на график -- для чего и оно должно быть "ручкой".
Правильный ответ, видимо -- knobplugin-контейнер в себе
уставит у себя в cont_vmt поле SetPhys
, и таким образом будет
перехватывать все записи.
Надо не сразу отправлять значения при чтении режимов, а СОХРАНЯТЬ, и уже потом отрабатывать.
Но при этом надо позаботиться о защите от зацикливания
(A пишет в B, B пишет в A). Т.е., (в CdrSetKnobValue()
?)
перед вызовом метода "запись" ручки выставлять у неё в
data_knob_t
некий флажок (а после -- сбрасывать), и перед
записью сначала проверять, не горит ли он.
17.11.2013: добавлено поле
being_modified
--
dataknob_knob_data_t
. Хотя, возможно,
полезно будет и у data_knob_t
вообще, ради "программной
управляемости контейнеров").
CdrSetKnobValue()
пока не используется. Хотя там
вообще не вполне ясно, как это делать для
17.11.2013: добавлено использование
being_modified
, по простому вышеприведённому проекту.
Оснований для такого желания два:
Вот как такое синтаксически делать -- чтоб "глобальные" директивы парсером элементов игнорировались? А как важные вещи (типа defserver) оставлять в силе?
09.07.2014: самое простое -- действительно в
CONTENT_fparser()
в основном цикле :
subsys_commands[]
тупо
игнорировать (надеясь, что они однострочны).
Этот мелкий хак позволит добиться эффекта прямо сейчас -- include'ить .subsys-файлы в другие файлы, что полезно для включения стандартных экранов устройств в другие экраны.
(Кстати, с таким подходом defserver можно было бы и воспринимать (хоть и чуть поднапрягшись). Особо незачем, правда.)
Чуть позже:
CONTENT_fparser()
'а
было организовано в виде for-цикла, после которого в принудительном
порядке ожидалась '}', и глобальные директивы вызывали
фатальную ругань.
cda_dat_p_update_server_cycle()
(возникающий из-за
несуществующего имени сервера, влекущего
MarkAllAsDefunct()
), приводящий к генерации события
CDA_CTX_R_CYCLE
, вызывающего
CdrProcessSubsystem()
-- может произойти еще ДО окончания
CdrRealizeSubsystem()
, прямо в момент регистрации первого
же канала (сложночитаемая цепочка-формулировка вышла,
да). А на недо-realized-ветке процессинг в MINMAX-ручках
попробует химичить с еще не созданным буфером -- что приводит к
SIGSEGV.
Конкретно для убирания SIGSEGV сейчас достаточно поставить проверку на аллокированность.
Но есть некая неясность: почему оно ТАК ведёт себя только при трансляции имён, а с обычной адресацией (если в командной строке указать такое несуществующее) -- нет?
12.02.2015: проверка добавлена.
data_subsys_t.is_realized
, но причину проблемы
оно тоже не вылечит.
Тут вопрос еще -- а НАДО ли реально "обновлять" всё первый раз, при обломе первоначального соединения?
12.02.2015@вечер-беговая-дорожка: еще идея, как устроить
не-посинение в cda_d_v2cx в первый раз: иметь поле
"was_connected", работающее guard'ом, и выставлять его =1 по приходу
данных от сервера (и НИКОГДА не сбрасывать); вызывать
cda_dat_p_update_server_cycle(()
только при !=0.
Тогда всё сходится: с одной стороны, первоначальный облом на экране отображаться не будет; с другой, реальные обрывы соединений уже покажутся, но только после реального прихода данных.
13.02.2015: сделано; только поле названо
was_data_somewhen
.
24.04.2015: вся архитектура с defunct_dataset()'еньем была только в v2cx, а сейчас копируем её и в cda_d_cx.c и cda_d_vcas.c.
FailureProc()
'а.
Собственно усиневание сделано не одним вызовом, как в v2cx, а перебором всех имеющихся -- поскольку тут единого актуального списка нет.
DefunctHbTCB()
с was_data_reply
и
last_data_time
-- нет, поскольку та идея имела смысл
только при обязательном ежецикленном обновлении всей подписки. С
индивидуально-присылаемыми же каналами оно смысла не имеет: можно
посиневать по таймауту, но раз-синивать ведь никто не будет.
...но если припрёт делать DEFUNCT'енье sid'а, то эта схема пойдёт -- только обращать внимание надо на ping-pong'и, т.к. по каналам ничего может подолгу и не приходить.
02.09.2015: в продолжение темы:
(Да, там могут быть множественные записи, но это пофиг -- отрезать построчно уж как-нибудь.)
01.06.2018: да, в v2 уже СЕЙЧАС формат ровно такой же; и даже строка заголовков начинается ровно с той же сигнатуры "Time(s-01.01.1970)".
CdrLoadSubsystem()
-- она свелась к добыче у plugmgr'а
указателя на функцию-загрузчик и ее вызов.
18.07.2007: ага, щаз-з-з! Там же надо еще стандартные поля (указатели на стандартные секции) заполнить. Так что это добавлено.
19.01.2010: слегка изменена "концепция"
использования переданных пераметров. Из (scheme,reference) они
превратились в (def_scheme,reference), и reference пропускается
через split_url()
, так что теперь можно указывать схему
прямо в командной строке всяких pult'ов.
А чтобы оставить программам возможность ЖЕСТКОГО указания схемы, введено правило: если схема начинается с '!', то url-splitting не делается, а (def_scheme,reference) используются сразу как (scheme,location) -- с пропуском '!', естественно.
CdrProcessKnobs()
.
06.08.2007: точнее, даже так: она в основном уже
сделана -- есть switch()
на всевозможные type'ы, с
нырянием внутрь, и есть уже бОльшая часть/скелет обработки
DATAKNOB_KNOB (отсутствуют: 1) поддержка KIND_DEVN и KIND_MINMAX,
которую надо лишь скопировать; 2) запись в историю -- тоже несложно; 3)
работа с alarm'ами -- ибо тут-то пока и не определились с моделью).
Кстати, CdrDestroySubsystem()
и
CdrDestroyKnobs()
также изготовлены уже некоторое время
назад, и поддерживаются в состоянии, соответствующем
datatreeP.h.
07.08.2014: добавлена
CdrProcessSubsystem()
, чтоб не размножать код перебора
всех DSTN_GROUPING-секций.
Кстати, CdrProcessKnobs()
к этому моменту уже вполне
функциональна.
29.11.2015: добавлено поле
data_subsys_t.currflags
и оно указывается
ProcessContextEvent()
'ом в вызове
CdrProcessSubsystem()
-- чтоб клиенты (в лице cx-starter'а)
получали готовые кумулятивных флаги от подсистемы, а не повторяли бы
Process() сами.
CdrLoadSubsystem()
сможет создавать секцию "sysname" по
умолчанию, если она не указана в файле.
27.08.2007: да, сделал --
CdrCreateSection()
, а объявлена она в CdrP.h. И
Cdr_fromtext.c переведен на нее.
И секция "sysname" теперь по умолчанию создается -- просто из
reference
.
Заодно переместил поле data_section_t.type
в начало.
19.01.2010: доп.комментарий/описание: там
изначально принято правило, что если переданное datasize
!=NULL, то аллокируется память указанного объёма и туда копируется
data
-- если оно !=NULL. А при datasize=NULL просто
делается nsp->data=data
.
CdrRealizeSubsystem()
плюс
CdrRealizeKnobs()
. На новой схеме -- с контекстами.
15.04.2014: некоторые детали:
CdrRealizeSubsystem()
работает в 2 этапа: сначала
делает realize всем секциям DSTN_GROUPING, а потом дрыгает все at_init,
при помощи рекурсивного helper'а CallAtInitOfKnobs()
.
02.10.2014: ага, щас --
не дрыгало: и рекуррентности не было (забыл), и дергало нерабочим
getrefvnr() вместо process_ref(), и без CDA_OPT_IS_W
.
src2ref()
:
22.04.2014: детали работы с параметром
defserver
в CdrRealizeSubsystem()
: при его
указанности создаётся дополнительная секция с типом DSTN_DEFSERVER и
именем "from_cmdline", и в sys->defserver прописывается
ссылка на её data.
Смысл -- чтобы реальный (эффективный) defserver имелся в метрике подсистемы, и чтобы он должным образом free()'ился при Destroy.
05.08.2014: поскольку и с baseref'ами оно давно уже обращается как надо, и всё прочее делает в полном объёме (кроме BIGC и USER, конечно), то считаем за "done".
05.08.2014: неа, только в ПОЧТИ полном объёме: нет поддержки концепции "conns_u" -- не производится насчитывание (и аллокация) этих массивов. В т.ч. потому, что не было требуемых подстилающих API в cda.
05.09.2016@Снежинск-каземат-11: да, сейчас при тестировании liu.subsys на симуляторе rack0:24 было обнаружено, что и содержимое под-окон модуляторов обновлялось, хотя оно никак не завязано на rack0.
А использование, кстати, есть (оно очень простое).
Непорядок, надо фичу дореализовывать. 27.09.2016: дореализована.
05.08.2014: пара замечаний:
Хотя когда-то давно drc и magcorr были на LOGT_DEVN.
А по-хорошему надо б использовать кольцевой буфер.
Ну что, добавлять механизм evproc'ев к data_subsys_t
?
И указание их в CdrRealizeSubsystem()
(или только в
"CdrAddSubsysEvproc()" -- ведь уставлять evproc надо будет Chl_app'у?)?
P.S. Кстати, еще надо позаботиться о синтетических обновлениях -- в результате REFRESH. Тут явно вводить флаг в options.
06.08.2014@вечер-пляж: evproc'ы-то evproc'ами, но -- мож, обновлять Chl_knobprops просто раз в секунду, и для всех curvals помнить предыдущие, а обновлять при !=. Плюс параметр "force", для принудительного ПЕРВОГО вывода. (Да, тут МОЖНО делать сравнение на равенство даже вещественных значений -- поскольку физически значение ровно то же, если оно не менялось.)
А была еще другая идея: во всех узлах иметь поле "время обновления" (mtime?), складируемое в timeval'ах или в cx_time_t, и при раз в секунду сравнивать со временем обновления окна knobprops.
18.08.2014: еще вариант:
завести в dataknob_knob_data_t
флажок "processed",
выставляемый в CdrProcessKnobs()
, а сбрасывать его в
ShowCurVals()
.
Неа -- кривовато выглядит, уж лучше помнить предыдущие.
02.12.2014: сделано по-простому -- тупо обновляем всё (кроме флагов, которые интеллектуально) каждый цикл.
07.08.2014: делание:
CDR_OPT_SYNTHETIC
.
CdrProcessKnobs()
он должным образом
обрабатывается.
CdrSetKnobValue()
при получении
CDA_PROCESS_FLAG_REFRESH
вызывается
CdrProcessSubsystem(,,CDR_OPT_SYNTHETIC,)
.
_k_newdata_f
добавлен параметр
synthetic
.
CDR_OPT_READONLY
плюс
CDA_OPT_READONLY
.
data_subsys_t.readonly
? ...и заодно ctxinfo_t.readonly
?
08.08.2014: "пусть цветут сто цветов" --
CdrProcessSubsystem()
вызывается самою Cdr, и в этот
момент флаги взять неоткуда.
data_subsys_t.readonly
, уставляемый
через CdrSetSubsystemRO()
, и используемый...
||
) с options
,
передаваемым в CdrProcess*()
и
CdrSetKnobValue()
, где в начале формируется готовая
cda_opts
, передаваемая потом всюду куда надо.
cda_process_ref()
-- запрещается вызов
snd_data()
proc_PUTCHAN()
-- было бы даже
избыточно, если бы вызывало cda_process_ref()
напрямую, но
дёргает упрощённый cda_set_dcval()
, не имеющий options.
21.08.2015: вопрос в функции
CdrRealizeSubsystem()
-- она вызывает
cda_new_context()
, но в качестве argv0 передаёт NULL,
поскольку больше нечего.
Кстати, аналогичная кривизна и в загрузке группировок, но
CdrLoadSubsystem()
-- реально конкретно
CdrLoadSubsystemFileViaPpf4td()
-- спасается
использованием program_invocation_name
, имеющейся под
Linux.
25.08.2015: делаем.
CdrRealizeSubsystem()
-- параметр в конце добавлен,
тут всё было просто.
ctxinfo_t.argv0
сохранялось
ОБРЕЗАННОЕ имя программы -- "short_name", после последнего
'/'.
cda_dat_p_report()
cda_ref_p_report()
теперь
будут выдаваться полные имена/пути, вместо коротких, но переживём.
ctxinfo_t
поле "оффсет короткого имени в argv0". Именно ОФФСЕТ, а НЕ указатель, поскольку
ctxinfo_t
-- компонент SLOTARRAY, и может гулять по
памяти.
cda_dat_p_argv0_of()
ждёт sid'а.
cda_dat_p_argv0_of_ref()
. С
ним всё заработало как надо.
cda_dat_p_NNNNN_of()
переименованы в cda_dat_p_NNNNN_of_sid()
.
CdrLoadSubsystem()
тоже добавлен параметр
argv0
. И тоже в конец.
Cdr_subsys_ldr_t
.
И -- единственный существующий! --
Cdr_file_subsys_ldr()
загрузчик адаптирован.
CdrLoadSubsystemFileViaPpf4td()
.
(А к
CdrLoadSubsystemViaPpf4td()
-- нет; вроде как не надо, там
даётся уже готовое имя файла, поиск не делается.)
Проверить -- не проверялось, т.к. особо не на чем.
Цель -- простейший вариант, для стендовых программ, вроде сварки и VEPP-4'шных GID25. Т.е., чтоб только скалярные каналы.
09.02.2016: начато. Вместо былых "Grouplist" теперь в именах фигурирует "Subsystem".
CdrStatSubsystemMode()
скопирована из v2 без изменений.
CdrSaveSubsystemMode()
и
CdrLoadSubsystemMode()
пока просто заглушки, чтоб в Chl всё
доделать.
10.02.2016: продолжаем:
DATAKNOB_B_UNSAVABLE
.
SaveKnobsMode()
.
"Почти" -- потому, что пока не печатает путь к ручке (в скобках "(ПУТЬ)") и почему-то вообще файл пустой, только заголовок без ручек...
11.02.2016: далее:
CdrSaveSubsystemMode()
почти допилена:
FprintfKnobPathPart()
.
12.02.2016: далее:
FprintfKnobSrc()
.
Кстати, там с самого начала были предосторожности: печаталась только если состоит из годных-для-имени-канала символов, иначе "-". Так что ни формулы, ни регистры в файл не попадут и не вызовут смущения cdaclient'а.
13.02.2016: далее:
cda_src_of_ref()
.
02.08.2016: а вот тут вопрос -- возможно, как раз и НАДО выдавать исходный адрес, БЕЗ defserver'а, а только combine(baseref,rd_src): чтоб режимы были "относительными" и годились бы даже при перенацеливании на другой сервер (как это предусмотрено в самом cdaclient/dataset-save).
cda_max_nelems_of_ref()
.
PrintChar()
,
скопированную из console_cda_util.c.
15.02.2016: еще --
FprintfKnobStats()
.
29.09.2016: флаг "UNSAVABLE" у контейнеров не работал,
т.к. поле behaviour изначально делалось для DATAKNOB_KNOB
и
парсилось и прописывалось только им.
Сейчас ситуация исправлена -- работает.
04.10.2016: приступаем к сложной части --
CdrLoadSubsystemMode()
. Активно копируется содержимое
cdaclient.c+console_cda_util.c.
Проблемы:
DSTN_GROUPING
, а при загрузке как поступать -- искать по
всем?
Решение выбрано просто-халтурное: т.к. ChlRunSubsystem()
сейчас оживляет лишь секцию "main", то и поиск делается только по
ней.
15.11.2016: пытаемся доделать:
(...Кстати, Chl_knobprops отображает так же -- без самого верхнего узла.)
Тогда поиск будет работать ровно как нужно.
...или сохранять, но при загрузке первый компонент пропускать -- будет тот же эффект.
Муторновато как-то. И есть неясность-двоякость: имён-то у группировки
ДВА: имя секции (у основной -- "main") и ident
её
узла. ...прямо как в CXv2 было с именами вложенных
элементов -- они существовали в 2 экземплярах: в узле-"содержателе" и в
самом описателе под-элемента.
16.11.2016: и-и-и:
(Проверено на сервере-симуляторе.)
Казалось бы: графический клиент -- ну пусть только со своими каналами и общается.
Потому вставлено "if (0)
".
01.11.2016@лыжи: понял, зачем -- чтоб Cdr могла читать файлы, созданные cdaclient'ом.
17.11.2016: продолжаем причёсывать:
set_knob_controlvalue()
отсутствовала
проверка на тему попытки записи в не-is_rw
-ручки.
...а в v2'шной datatree_SetControlValue()
она была!
И сюда добавлена такая же.
FprintfKnobPathPart()
выдаются как "-" и потом при
загрузке не находятся.
Что -- СЕЙЧАС -- позволяет иметь под-иерархии, которые в режим писаться будут, а считываться -- нет. Так мы получаем просто информационные данные в режиме.
Но в будущем-то предполагается, что NULL/"" -- это "прозрачность", так что сей хак пишется-но-не-читается работать перестанет.
18.11.2016: Карнаев проверил на v4gid25s -- работает.
15.02.2017: на днях вылезла проблема: при попытке
прочитать Cdr'овский режим при помощи cdaclient -f FILENAME.dat
оно вроде как всё прописывалось, но cdaclient зависал и никогда не
завершался.
Вчерашние разборки (при помощи специально введённого -D/) показали причину: он пытается загрузить ВСЁ содержимое режима, в т.ч. значения каналов чтения. А на каналы чтения (те, которые autoupdated) никакого ответа не приходит, вот он и подвисает.
Решение очевидно: каналы чтения пишутся в режим "закомментированными" -- с префиксом '#'. Это сделано, надо будет проверить (хотя куда оно денется...).
17.02.2017: то сохранение каналов чтения в режим как-и-каналов-записи вызывало еще одну проблему: не работала переполюсовка ИСТР'а M5 в v4gid25s при загрузке режима cdaclient'ом.
IST_STATE_RV_ZERO
, сброс уставки в 0 перед переполюсовкой.
Однако попытка руками cdaclient'ом сделать две записи подряд никакой проблемы не вызвала -- всё отработалось.
А загрузка режима "cdaclient -f"'ом тут же воспроизвела застревание.
И увиделось (это и в GUI заметно), что значение в ЦАПе чуток дрыгается, и остаётся стоять.
А более внимательный просмотр выдачи cdaclient'а показал, что это значение даже прыгает до 0, а потом возвращается к 3.3-что-то.
(Вывести его из этого состояния можно было сделав Выкл и потом опять Вкл.)
Очевидно, что проблема заключалась в попытке восстанавливания значений ВСЕХ каналов, сохранённых в режиме, и после позавчерашнего исправления (или просто за-'#'-чивания строки icd_m5.out в файле) всё пришло в чувство.
set_knob_controlvalue()
.
16.06.2017: вылез косметический косячок:
CdrLoadSubsystemMode()
ВСЕГДА возвращала -1 (там в конце так
стоит -- рудимент от времён заглушки), и посему в statusline всегда писалось
"Unable to open...".
Исправлено на return 0
.
26.09.2016: в основном довольно прямолинейное портирование из v2.
cda_srvs_of_ref()
.
Требуемая ей информация "номер сервера внутри контекста" уже есть -- это si>nth, созданное 01-08-2014 как раз для "conns_u".
cda_fla_p_rec_t
-- метод srvs_of()
.
cda_f_fla_p_srvs_of()
-- очень прямолинейная, просто вызывающая
cda_srvs_of_ref()
для всех наличествующих в формуле
OP_GETCHAN
(но НЕ OP_PUTCHAN -- полностью аналогично
v2).
CdrRealizeSubsystem()
(тут, в
отличие от v2, где CdrRealizeGrouplist()
появилась для conns_u,
имеющаяся изначально).
FillConnsOfContainer()
,
являющейся аналогом былой FillConnsOfElemInfo()
, сразу
содержащим в себе суть былой FillConnsOfKnobs()
в виде цикла.
По сравнению с v2 добавлен учёт контейнерова ap_ref
.
CdrRealizeSubsystem()
и передаётся всем
вниз.
И даже более того: в случае без-серверных имён в момент насчёта массивов conns_u[] они будут принципиально пустыми -- т.к. в этот момент (выполнения Realize) никаких ответов еще не пришло.
И что делать?
А как эту надобность определять?
Такое событие ловить можно -- у cda_core эта информация есть, таким
"событием" является момент изменения ri->sid
.
Но подобные события будут приходить "пачками" -- сразу для всех каналов сервера; реагировать на них по количеству элементов в "пачке" -- глупо.
CDA_CTX_R_CYCLE
, если
вышеупомянутый флажок взведён.
Пока-то забиваем (UDP-резолвинг недореализован), но проблема существует.
P.S. Ирония в том, что сама идея была придумана именно для v4 (в 2007-м), впервые реализована в v2 (в 2010-м), а в v4 оказалась уже плохо подходящей -- т.к. парадигма с 2007-го сильно изменилась.
27.09.2016: проверено на симулируемом ЛИУ (где 11 серверов) -- вроде работает.
Осталось только:
for (x = 0; x > 0 ...
не срабатывает ни разу, и последующее "x>0", соответственно, тоже.
В v2 тоже портировано, но это проверим уже как-нибудь потом, на живом ЛИУ-2.
Засим считаем раздел за "done" (на ПОКА, с учётом вчерашней гложущей мысли).
Мысль:
#.KNOBPARAMS ИМЯ-РУЧКИ ПАРАМЕТР=ЗНАЧЕНИЕ ...
CdrLoadSubsystemMode()
флагами в параметре
options
, чтоб можно было отдельно значения, отдельно параметры.
А сохраняется пусть всегда всё (или тоже сделать параметр options, где указывать, ЧТО сохранять?).
21.02.2017: предварительные действия:
CdrSaveSubsystemMode()
добавлен параметр
options
.
Вставлен после filespec
, ПЕРЕД comment
.
ChlSaveWindowMode()
.
CDR_MODE_OPT_PART_DATA
и CDR_MODE_OPT_PART_PARAMS
,
а также объединяющий их CDR_MODE_OPT_PART_EVERYTHING
.
И напрашиваются 2 решения:
Так что можно просто забить и использовать CDR_-константы напрямую.
ChlHandleStdCommand()
, вставлена передача
options=CDR_MODE_OPT_PART_EVERYTHING
.
20.02.2017: (в основном всё обдумано неделю-две назад, сейчас только записываю) задача состоит из 2 частей:
"Где-то" -- видимо, метка в тулбаре. Т.е.,
CdrStatSubsystemMode()
позволяла добыть
comment из загруженного файла.
21.02.2017: простейшие действия (занимаюсь этим с горя, т.к. плавное изменение уставок CAN-ЦАПов не вытанцовывается):
CdrStatSubsystemMode()
параметры
commentbuf
,commentbuf_size
(как у Stat'а).
Взято из Stat'а.
ChlLoadWindowMode()
добавлены параметры labelbuf
,labelbuf_size
.
ChlHandleStdCommand()
--
адаптирован к изменившемуся набору аргументов; пока просто NULL,0.
02.03.2017: потихоньку делаем GUI-часть (это совсем НЕ Cdr, но для цельности опишем здесь же).
XhACT_BANNER
и макрос
XhXXX_TOOLBANNER()
.
cmd
, хоть он и pointer -- иных кандидатов просто нет.
XhXXX_*()
полю
cmd
присваивается значение 0
(явно от v2
осталось), хотя в v4 оно уже не int
, а char*
.
Заменено на надлежащий NULL
.
_WindowInfo
добавлено поле
tbarBanner
.
XhCreateToolbar()
: обработка кода
XhACT_BANNER
и сохранение ссылки на созданный виджет в
tbarBanner
.
Имя-виджета -- "toolBanner" или "miniToolBanner".
При числе колонок ==0 объект не создаётся.
XhSetToolbarBanner()
-- собственно вывешивание
сообщения, тривиально.
Тут, увы, не всё гладко -- точной унификации с LABEL'ом не получилось, что-то там не слава богу с вертикальными полями (возможно, очередной косячок в Motif'е).
appopts_t.num_bnr_cols
.
toolbar_bnr_cols
.
XhXXX_TOOLBANNER(0)
сразу после кнопок Load/Save.
num_bnr_cols
в banner'ово
cmd
.
ChlLoadWindowMode()
теперь
складируется в comment[]
...
XhSetToolbarBanner()
.
CdrLoadSubsystemMode()
некорректно пыталась отработать "#!COMMENT:" -- в else() от
проверки на '#', так что никогда дотуда не доходило.
Исправлено.
Итого -- всё работает, "done".
15.03.2017: вот тогда сделал, а на следующий же день подумал: ведь режим-то на ВЭПП-4 в основном грузится не из самой программы, а автоматикой -- скриптом, следящим за каналом "полярность" (в другом сервере).
Следовательно, реализованная фича "показывать на тулбаре комментарий из последнего загруженного режима" оказывается полностью бесполезной.
А надо делать показ чего-то другого -- то ли полярности (преобразовывая ей в строку (readonly-селектором?)), то ли именно некоего строкового канала.
И сегодня Карнаев в разговоре это подтвердил.
И давняя идея (присутствовавшая еще до реализации TOOLBANNER'а) -- такой хитрый тип container-плагина, который бы размещал своё содержимое на тулбаре (а содержателю отдавал бы виджет 1*1 пиксел).
16.03.2017@утро-лыжи: некоторые идеи по реализации:
И чтоб этот вызов мог быть сделан только ЕДИНОЖДЫ -- следующие вызовы возвращали бы NULL, дабы не пыталось бы несколько "информаторов" туда разместиться (с труднопредсказуемыми последствиями).
k->w
;
k->w
создаётся виджет с типом widgetClass
и размером 1*1 пиксел.
17.03.2017: делаем.
Первоначально идея была "ложа" (как в театре), но там не очень красивый перевод -- "lodge" или "box".
XhACT_LOGGIA
и XhXXX_TOOLLOGGIA()
.
_WindowInfo
добавлены поля tbarLoggia
и
tbarLoggiaAsked
.
XhGetToolbarLoggia()
используется для добычи виджета
"лоджия" и работает по вчерашнему сценарию -- отдаёт лишь единожды, а потом
NULL.
Итого: оно работает.
Теперь надо придумывать, как использовать -- чтоб крупная (и цветная?) надпись в тулбаре показывалась. А то ни у text_text, ни у selector'а нету возможности менять размер.
Заодно на v4gid25s пара проблем:
24.03.2017: был косяк в kind_of_reference()
,
исправлен.
28.03.2017: а сегодня выяснилось, что на vepp4-pult6:0 ссылаться и не надо, канал показывать прямо из своего сервера. Но косяк был обнаружен, исправлен, и это хорошо.
21.03.2017: был косяк в pult.c, исправлен.
21.03.2017: selector наделён способностью менять размер/жирность шрифта. Теперь loggia в v4gid25s стала вполне юзабельна.
27.03.2017: задеплоено на vepp4-pult6, вроде всё OK. Считаем за "done".
CdrProcessKnobs()
обнаружилась несколько странная работа ветвей
_TEXT и _VECT с rflags: они получают от cda флаги прямо в
k->currflags
, НЕ трогая rflags
; а общая часть
после switch()'а потом радостно затирает k->currflags =
rflags
(в начале, перед switch()'ом, делается rflags=0
).
25.07.2018: исследование показало, что
choose_knobstate()
скармливалось значение именно
currflags
.
Но в колоризации контейнера эти узлы по факту не участвовали, поскольку в
currflags
эффективно прописывались нули.
Вывод: надо обнормаливать.
rflags
.
Скорее всего, последствий (деградации) не будет, но понаблюдаем.
CdrRealizeKnobs()
: почему-то считалось ошибкой
"count<=0
", хотя 0 -- совершенно легитимное количество
(отсутствие содержимого).
Наткнулся вчера при попытке сделать "скелет" rfsyn_eng.subsys с пустыми панельками.
Странно, что этот косяк раньше никогда не вылезал. А он ведь
присутствует с самого появления CdrRealizeKnobs()
~14-06-2014
(w20140615.tar.gz).
09.08.2018: исправлено:
<0
".
list==NULL
", что
также вполне легитимно при count==0, то вставлено отделное условие, что
count==0 приводит просто к "return 0
".
А еще добавляло "понятности" то, что ошибка там возвращается молчаливым
return -1
, без указания причины. И
CdrLastErr()
, видя пустую Cdr_lasterr_str[]
,
честно пыталась сказать хоть что-то -- возвращая
strerror(errno)
, где была совершенно не относящаяся к делу
ENOENT
:"No such file or directory", что еще больше запутывало
ситуацию. Добавлены CdrSetErr()
'ы на каждый возврат ошибки.
Причина: сегодня звонит ЕманоФедя и говорит, что на v5p1 X-сервер жрёт 99% процессора. И виноваты, видимо, мои программы -- потому, что если их грохнуть, то X-сервер успокаивается.
Стал я разбираться -- ну да, запущено десяток скринов, причём крупномасштабных, вроде linmag, ringcor*, linvac. И они обновляются с частотой 5Гц, давая преизрядную нагрузку на X11-сервер.
Федя-то разверещался в стиле "ну а нафига они обновляются, если в основном там одни и те же данные?!". Но в принципе -- да, мысль разумная.
19.02.2020: пытаемся проанализировать, как бы и что можно сделать для оптимизации.
Да, понятно, что для вещественных чисел сравнение на равенство вроде как некорректно, но в данном случае оно вполне катит: числа получаются из одних источников, так что при неизменности исходных данных и результаты будут совпадать.
И тут есть 2 составляющих:
Так что подход выглядит здравым.
u.k.curv
выполняет CdrProcessKnobs()
.
И в нём же можно производить сравнение.
SetValue()
-- делает set_knob_controlvalue()
, вызываемый в конце
процессинга ручки.
...или нет? И SetValue()
отображает именно ЧИСЛО-значение,
а за отображение расцвеченности отвечают иные функции/блоки?
02.09.2020: ага, вот этот неучёт wasjustset'а аукнулся -- если ввести бредовое число в поле канала, и сервер его модифицирует/ограничит, то на экране так и останется это бредовое число вплоть до следующего изменения в сервере. Подробнее -- ниже.
19.02.2020@лыжи-~16:30: дальнейшие размышления по теме:
CdrProcessKnobs()
определяет
изменённость/неизменённость и передаёт сей факт в
set_knob_controlvalue()
для использования.
SetValue()
.
CdrProcessKnobs()
пришлось (воизбежание проблем из п.A)
проверять лишние параметры.
set_knob_controlvalue()
. Но это криво -- смена API.
fromlocal
.
По семантике он в принципе не пересекается с "value not changed": уж если изменение от юзера, то отображать надо всегда.
Фактически получаются 3 варианта:
Но, к сожалению, текущая ситуация со значениями иная:
fromlocal
-- булевский флаг, и "локальное изменение" имеет
значение !=0.
fromlocal
(подобную технологию "подселения" мы
уже много раз использовали -- например, с DoDriverLog()
'овым
параметром level
).
И что -- инициализировать при создании curv=NAN? Чтобы последующее сравнение "v==curv", которое сведётся к somevalue==NAN, всегда дало бы false?
Как-то не хочется.
valbuf.f64
при CXDTYPE_DOUBLE и max_nelems==1.
И, судя по отсутствию упоминаний тут в bigfile-0002.html, никогда и не было.
19.02.2020@дома-вечер:
приступаем. Пока сделан минимум, в CdrProcessKnobs()
:
v_ne_curv
("ne" -- "Not
Equal", а не русское "не" :D).
v
--
присваивается
v != k->u.k.curv
.
20.02.2020@дома-утро: доделываем.
DATATREE_FROMLOCAL_FLAG_NOVALUECHANGE
=1<<16
set_knob_controlvalue()
:
valchg = (fromlocal & DATATREE_FROMLOCAL_FLAG_NOVALUECHANGE) == 0;
(да, он булево-ОБРАТЕН битовому флагу).
&=~
-- из
fromlocal
.
valchg
используется как дополнительное обязательное
условие для вызова SetValue()
.
set_knob_textvalue()
и set_knob_vectvalue()
.
Но использования в них нет (поскольку и "юзеров", сравнивающих новое значение со старым, тоже нет -- в Cdr старые значения брать просто негде).
CdrProcessKnobs()
:
v_ne_curv
переименована в
v_ne_curv_flag
.
11.09.2020: а надо было -- в v_eq_curv_flag
!
v_ne_curv_flag = ((v != k->u.k.curv) || k->curstate == KNOBSTATE_JUSTCREATED)? 0 : DATATREE_FROMLOCAL_FLAG_NOVALUECHANGE;
set_knob_controlvalue()
в
качестве fromlocal
.
В общем-то всё просто -- делалось минут 10-20, записываю дольше...
20.02.2020: проверяем.
set_knob_otherop(,!v_ne_curv_flag)
-- таким
образом изменяемые в данном цикле ручки подсвечиваются оранжевым.
Во время переключения режимов, когда меняется сразу много каналов (сначала все управляющие, а потом все измеряемые) -- загрузка кратковременно подскакивает (точно засечь числа не удалось, но до в районе ~+7-15%).
Итого: всё замечательно, достигнута огромная оптимизация и очень задёшево!
Бинарники pult
и pzframeclient
скопированы на
ctlhomes, задачу считаем решённой, "done".
21.02.2020: это диагностическое об-OTHEROP'ливание
изменившихся выглядит столь забавно, что возникает даже желание сделать так,
чтобы такая диагностика осталась напостоянку, просто включалась бы при
запуске программы уставкой какой-нибудь переменной окружения. Например,
CDR_SHOW_CHANGED_KNOBS
=1.
25.02.2020: да, диагностика сделана напостоянку.
Управляется переменной окружения CDR_SHOW_CHANGED_KNOBS
, в
качестве "да" подходят "1", "y" и "Y"; внутри
используется флаговая переменная _Cdr_show_changed_knobs
, чья
инициализация (вычитывание из окружения) повешена на
CdrRealizeSubsystem()
(предполагается, что уж она-то вызывается
ВСЕМИ Cdr-клиентами).
02.09.2020: стыд-позор: из-за неучёта wasjustset'а получилась гадость гадская... История:
И тут обнаружилось -- что ДА, сказанное Федей правда: остаётся что введено.
А во втором клиенте при этом -- отображается истинное число.
ProcessKnobs()
быстро
догадался о причине: НЕотображение совпадающих значений. Т.е., происходит
следующая цепочка:
...в curv
прописывается пришедшее от сервера значение.
Но в поле-то значение всё ещё НЕПРАВИЛЬНОЕ! А об этом уже никто не помнит :-(.
А-а-а, нет -- наоборот, при SYNTHETIC==0. Т.е., практически всегда.
Итого: ПРИНЦИПИАЛЬНО-то понятно, как решать проблему: надо следующий после снятия wasjustset цикл отрабатывать обновление ОБЯЗАТЕЛЬНО. Но вот КАК? Где/как сохранять уведомление "обязательно"?
Но не хочется тратить ещё один int.
...если б там хоть битовая маска была, а так -- расточительство...
set_knob_controlvalue()
, видимо -- записывать его ещё и в
u.k.curv
-- тогда это автоматически приведёт к несовпадению
значений и обновлению! Но нет, т.к.:
А надо ЧЕРЕЗ цикл.
08.09.2020: сделал, по простейшему и наиболее очевидному пути -- флагом "force_next_update".
force_next_update
.
wasjustset
'а в CdrProcessKnobs()
сделан более хитро:
force_next_update=1
.
force_next_update
выполняется в точке
после определения v_ne_curv_flag
:
force_next_update
=0.
v_ne_curv_flag
=0.
DATAKNOB_KNOB
.
Проверено -- да, теперь глюк ушёл, значения в полях отскакивают к значениям, модифицированным драйвером.
11.09.2020: исправлена недоработка от 20-02-2020 в
имени флаговой переменной -- v_ne_curv_flag
переименована в
v_eq_curv_flag
: ведь флаг ВЗВОДИТСЯ при РАВЕНСТВЕ
значений.
17.07.2007: он содержит таблицу стандартных читеталей (в данный момент -- file и string), связанных в список, и позволяет добавлять к этому списку (регистрировать) новые читалки. Поскольку добавление идет в начало списка, то у "регистрируемых" приоритет перед стандартными.
Можно также раз-регистрировать читалки -- по имени схемы. Чтобы не
раз-регистрировали стандартные, в описателях есть флажок
builtin
, равный у них 1.
Сами внутренности Cdr добывают у plugin-manager'а указатель на
читалку при помощи CdrGetSubsysLdrByScheme()
.
Проверено на симуляторе -- работает!!!
Скопирован из "file", только схема "!m4" заменена на "!plaintext".
17.10.2016: зачем:
Для коего натравливания и требовалась возможность сказать "бери plaintext, безо всякого препроцессирования!".
PPF4TD_LINESYNC_NONE
, вот
он и считал те строки за обычные комментарии, и потому backspash-NL'и из
макросов обрабатывались криво.
А надо было запускать просто "m4", без "-s". Ладно, на будущее останется.
17.07.2007: протокол функции-метода "читай
следующую строку" таков: она возвращает 1 при успехе, 0 при конце
"файла", -1 при ошибке; это определяется константами
CTLR_OK
, CTLR_EOF
и CTLR_ERR
.
"Frontend'ы" для схем "file" и "string" располагаются в Cdr_file_ldr.c и Cdr_string_ldr.c соответственно.
19.07.2007: сделана минимальная инфраструктура --
команды .define и .undef, плюс функция
ParseMacroString()
, парсящая строку с содержащимися в ней
$-ссылками и складывающая ее в указанный буфер. Собственно в
.define в определении значения также работают макрорасширения.
19.07.2007: есть некоторые непонятки, надо или
нет передавать CdrLoadSubsysFromText()
'у имя того, откуда
читается? Сейчас сделано, но это кривизна -- одно и то же десять раз
повторяется. Лучше пусть тот, кто вызвал
CdrLoadSubsystem()
-- тот и печатает схему/имя.
23.07.2007: да, параметр "откуда" (reference)
CdrLoadSubsysFromText()
'у больше не передается.
24.07.2007: хотя разрыв строк с помощью бэкслэша у нас "на уровне читчика файла" и не поддерживается, но можно легко ввести в парсер определений ручек такую фичу -- если там, где должно начинаться определение очередного поля, имеется бэкслэш+NL, то он молча прочитывает в буфер очередную строку и продолжает парсинг с ее начала.
Это будет решением хоть и частного случая, но для наших целей его хватит в 99% случаев.
26.07.2007: да, базовая инфраструктура парсера сделана, проверена -- она работает!!! Highlights:
07.08.2007: продолжение:
PARAM_pparser()
и связанную с ним
инфраструктуру.
Сам парсер довольно уродлив -- просто куча выпарсиваний и проверок,
все "вручную", а в конце -- добавление ячейки к массиву
kparamsdescr_t.aux_params
и ее заполнение.
ParseKnobDescr()
же в конце "сливает вместе"
стандартные параметры и эти дополнительные.
REF_pparser()
пока работает просто как MSTR --
поначалу этого более чем достаточно.
08.08.2007: перевел Cdr_file_ldr.c на
свежесделанный findfilein()
-- теперь он честно ищет
subsys-файл в нескольких местах, причем суффикс ".subsys" прибавляет
сам.
#define
'enum
'Самым подходящим синтаксисом выглядит shell-подобный
$[ВЫРАЖЕНИЕ]. Он отлично ложится на нынешнюю модель макросов
вообще и ParseMacroString()
в частности. И, как и в
shell'е, ссылки на макросы изнутри $[...] можно делать прямо
просто именами, безо всяких символов доллара.
Вопрос только в том, что надо будет собственно корректный парсер арифметических выражений сделать :-).
read_line()
, что в любом
случае выглядит более резонно.
А .if/.elif пользовались бы наличием $[...].
tfp_
.
Тогда его же услугами смогут пользоваться и читчик текстовых blklist-файлов в сервере, и конфигуратор сервера, и cx-starter для своего subsys-list файла.
Конечно, область компетенции его несколько параллельна/ортогональна PSP, но уж с этим мы разберемся.
DSTN_WINOPTIONS
.
Вроде и несложно, но начинает надоедать -- надо б сделать helper-функцию "отпарсь в секцию с таким-то кодом"; оно понадобится и для прочих атрибутов окна. И аналогично -- на глобальные директивы (с name==NULL).
22.05.2008: да, перевел такой парсинг в функцию
parse_n_store_section_name_str()
, и сразу же добавил через
нее парсинг wintitle, winname, winclass.
Получасом позже: под-обумнил эту функцию -- теперь в значении поддерживаются кавычки (нафиг, впрочем, реально не нужные, но для общности), ругается при слишком длинных значениях, и требует наличия конца строки (актуально только при закавыченном значении).
Еще часом позже: научил ее также парсить и строки без-имени -- при
указании параметра with_name
=0 она уставляет имя в NULL.
И на этом сделал парсинг sysname, defserver,
treeplace.
Вообще, конечно, такой подход не совсем корректен -- поскольку
значения всех этих директив (кроме wintitle и, м.б., winopts) являются
не просто строками, а имеют некий формат, который стоило бы проверять
сразу. Но пока что оставим это на потом -- как и
REF_pparser()
:-).
И еще чуть позже -- чтоб не было двояких толкований термина "name",
переделал имя поля команды в ft_cmd_t
и имена параметров
функций-процессеров из "name" в dirv
.
Полный список удалённых -- Cdr_file_ldr* Cdr_fromtext* Cdr_newf_ldr* Cdr_string_ldr* Cdr_via_smp4td*.
Ну и "obsolete".
Модуль создаётся пока под именем Cdr_Zfile_ldr.c. Пока тут всё вместе, а потом разделим на сам плагин и Cdr_via_ppf4td.c.
03.12.2013: за прошедшую неделю наполнен, копированием из старого Cdr_fromtext.c с переделкой под ppf4td.
CONTENT_fparser()
:
Также оно может быть devn или minmax.
Ну и прочие "шорткаты" вроде selector остались.
ParseKnobDescr()
добавлен параметр
kind
, куда результат
Сюда входят все индивидуальные флажки DATAKNOB_B_
(кроме NO_WASJUSTSET), так что любой из "шорткатов" имитируется при
помощи knob/ФЛАГИ.
Перед '/' разрешается whitespace (а не обязательно слитно).
Множественные флаги могут указываться либо через тот же '/', либо через ','.
CONTENT_fparser()
имеется с
проходом по строкам в числе count штук: ведь пустые строки надо
пропускать, а ppf4td_is_at_eol()
по EOF тоже отдаёт +1.
Так что пришлось химичить, делая в IsAtEOL()
отдельную
проверку на EOF с возвратом -1.
NextCh()
и
PeekCh()
("интеллектуальные" адаптеры к
ppf4td_nextc()
и ppf4td_peekc()
) тоже
содержат тучу проверок.
ppf4td_ungetchars()
.
ParseKnobDescr()
при обращении с
параметрами: вместо странного подхода с "pret" теперь оно просто делает
корректный cleanup как при ошибке (чего раньше НЕ производилось в
случае ошибки где-то ПОСЛЕ указания параметров), так и при обычном
переселении auxparams'ов в общий список. Для чего введена
CleanKparamsDescr()
.
04.12.2013: первоначальную проверку -- nexample.subsys оно прошло, окно-результат парсинга выглядит точно так же.
Теперь можно удалять старое, разделять файлы, исправлять мелкие тонкость и проверять работоспособность всех проверок парсинга.
05.12.2013: финальные штрихи этой первой версии:
CdrLoadSubsystemFileViaPpf4td()
, выполняющая
предварительный поиск файла.
CdrLoadSubsystemViaPpf4td()
-- "мясо" парсинга.
Засим первоначальную версию ставим "done".
16.04.2014: сделан первый m4-based-экран -- frolov_d16.subsys. Проверка прошла успешно -- генерация контента макросами работает как надо.
21.04.2014: сделано второй m4-based-экран -- cac208.subsys, более навороченный:
Используется для тестирования эвристики сборки полного имени канала из defpfx,base,spec.
Замечено сильное неудобство нынешней реализации связки Cdr_via_ppf4td.c+ppf4td: строки, начинающиеся с '#', не вполне эквивалентны просто пустым, и если так пытаться закомментировывать куски, то парсер на них ругается.
27.04.2015: вроде бы как минимум с частью проблемы разобрались давно -- 04-08-2014.
А желательно бы еще мочь оное пропускать через m4 -- чтоб иметь возможность использовать в builtin-экранах как минимум арифметику. Вот как? Через pipe в v4 на stdin? В любом случае, это надо будет реализовывать схемой в ppf4td.
22.08.2023: забавно, что это тут записано -- через полтора месяца ПОСЛЕ успешной реализации схемы "mem::" 22-10-2013.
А надо бы мочь -- в т.ч. чтобы можно было оформлять группировки с
#!/PATH/TO/pult
и делать их executable.
18.07.2014: сделано -- первым шагом оно пытается
сделать прямо fopen(reference)
, и только если облом, то
начинает поиск.
12.12.2015: только от той фичи вылез неприятный побочный эффект (еще давно обнаружилось): при попытке запустить pult-программу, сделанную симлинком на pult, прямо в текущей директории, из-за вышеуказанного алгоритма она пытается открыть прямо саму программу -- т.е., бинарник pult.
Например: программа ringsqr --
Придумываются 3 варианта защиты от этой напасти:
CdrLoadSubsystemFileViaPpf4td()
.
Сработает, но это криво: получается, что в поиске приоритет у "других" директорий, вместо того, что указано прямо в командной строке прямой ссылкой.
Идея типа правильная, но не сработает: а) в качестве reference передаётся не полное argv0, а его basename(); б) собственно argv0 НЕ передаётся, так что и сравнивать не с чем (хотя вот ЭТОТ бардак -- исправить НАДО).18.12.2015: как это не передаётся -- передайтся! Еще 25-08-2015 сделано!
Неприятно, конечно, что для текущей директории придётся указывать "./", но терпимо. А уж файл-менеджеры префикс пути указывать будут, проверено (SL-6.3): MC -- ./ИМЯ, Nautilus -- полный путь.
Сделано по 3-му варианту -- работает.
24.12.2015: вариант с необходимостью указывать "./" всё-таки неудобен -- вроде и [Tab] в shell'е работает, а файл не подхватывается.
Поэтому добавлено альтернативное условие: что в имени есть '.' -- предполагается, что это начало расширения ".subsys". Имена подсистем вроде точек иметь не должны.
Да, под Windows будет "проблема" -- чего-то.EXE конвенцию нарушит; но вся затея актуальна только для симлинков, а чтоб нажимать [Enter] на симлинке.EXE -- это уж совсем вряд ли. (Хотя если что -- всё-таки можно будет вариант «содержит '.'» ужесточить до «заканчивается на ".subsys"».
06.12.2015: уже энное время ВСЁ в CXv4 настроено именно на ~/4pult/, включая cx-starter. Может, и оставить именно 4pult "родной" директорией для v4?
И, кстати, желательно б (в datatreeP.h и Cdr) поддержку user-узлов сделать -- чтоб гистограммам исходники данных подготавливала бы Cdr: {r0,r1,...,r9}_{src,ref}.
SL-6.3, m4-1.4.13-5.el6.x86_64; скрин linmag.subsys, последний элемент (макрос MAGX_COR208_LINE). p8b2:~/20151217-m4-bug/: если в параметре r: есть "`'", то баг проявляется.
18.12.2015@утро-душ: а не
в том ли дело, что ppf4td_get_string()
воспринимает
'\'' как открывающую кавычку (апострофный вариант), и сжирает всё
до следующей такой же? Это как раз и объяснило бы, почему парсер "видит" 17
ручек из 20 (3 -- ширина строки и "шаг" между одинаковыми).
Но всё равно остаётся вопрос -- почему m4 оставляет "`'" только ИНОГДА.
...кстати, m4-1.4.1-7 в RH-7.3 ведёт себя так же.
26.12.2015: и вроде сделать-то, наверное, было бы несложно, но неясно, как это должно выглядеть -- синтаксически -- и на каком уровне существовать: ppf4td или клиентов.
ppf4td_get_string()
, чтоб
функционал был доступен в любом клиенте.
...а еще более в идеале -- в самом ppf4td, т.е. в
ppf4td_nextc()
, чтоб и не только в строках.
28.12.2015@утро-душ: вариант -- ввести в ppf4td правило, что если сразу после кавычки идёт "\NL", то выкидывать и эту кавычку, и "\NL", и следующие пробелы (и следующую кавычку).
Вопрос только, как это всё реализовывать -- шибко переплетёнными будут куча кусков кода.
28.12.2015@утро-душ (несколькими минутами позже): есть вариант проще и элегантнее: после "\NL" сжирать также все последующие пробелы -- isspace(), но за вычетом '\n' и '\r'. (За вычетом -- чтоб в последовательности "\NL","NL" второй NL считался уже окончанием строки; актуально для "генерируемого" текста -- там вполне возможны такие вроде бы бессмысленные последовательности.)
Реализовать оное очень просто.
28.12.2015: сделано. Буквально один дополнительный while() с длинным условием.
Поскольку сейчас используется shell-style, то можно "разрывать" как внутри кавычек, так и закрыв кавычку на первой строке и заново открыв на следующей.
Единственный потенциальный drawback нововведения -- что такие "\NL"+пробелы перестали "создавать whitespace", могущий быть разделителем. Но если когда нужно разделитель на месте разрыва строки, то правильнее поставить пробел ПЕРЕД '\\'. 30.12.2015: прикол в том, что такие места были: одно в linmag.subsys и по штучке в dl200me_s и dl200me_f.
06.09.2016@Снежинск-каземат-11: не всё так идеально: если эта строка не прямо в самом исходном тексте, а приходит из макроса, то m4 напихивает после каждого NL'я (т.е. -- \NL'я) строки синхронизации "#line...", и удаление пробелов перестаёт работать.
PeekNextCh()
/ReadNextCh()
, и видит этот
'#' как символ, на чём и прекращает пропускание пробелов.
Но то было под RH-7.3, а в CentOS-5.2/m4-1.4.5-3, похоже, поведение отличается.
dl200me_f.m4
), выкрутились удалением отступов -- всё идёт прямо
с начала строк.
07.09.2016@Снежинск-каземат-11: и еще в схожую степь: конструкция
не работает, как ожидается: "second line" НЕ является продолжением "first line".first line \ #commented line second line
Впрочем, тут некий вопрос -- а КАК правильно? Пожалуй, именно нынешнее поведение...
Сделать бы "alias'ы" с человекочитаемыми именами...
28.07.2016: вводим:
Всё это, конечно, ПОСЛЕ KFD_BRK()
, и в обоих --
GRPG_fields[]
и CONT_fields[]
(кстати, а нафига
они у нас разделены? чисто исторически?).
ParseKnobDescr()
теперь
k->behaviour=behaviour
делается всем типам.
Да, есть некоторая странность в том, что, например, контейнеру может указываться "/fixedranges", но это плата за универсальность.
18.06.2007: и именно в ее компетенцию переведены все структуры дерева ручек.
Для include-файлов принята следующая организация: публичные определения (в т.ч. KNOBSTATE_* -- бывшие COLALARM_*) и API расположены в datatree.h, а структуры -- в datatreeP.h, безо всяких там *types.h и typesP.h.
ChooseColorState()
в
choose_knobstate()
вылезла проблема: ведь флаги-то там все
-- CDR_FLAG_*
!
Видимо, флаги эти также надо уносить в компетенцию datatree... Мерзко -- там ведь и cda'шные... И как же их тогда префиксировать?
19.06.2007: похоже, надо уносить их вообще в cx.h. А префиксировать -- либо тоже CXRF_, либо уж CXCF_ (Client Flag).
20.06.2007: да, так и сделано -- все эти флаги
унесены в cx.h, где имеют префикс CXCF_
.
Дополнительный плюс -- теперь ВСЕ флаги определены в едином месте-"реестре".
Заодно флаг DEFUNCT был переведен из компетенции Cdr в cda.
А ведь такое вполне осмысленно, и синий цвет у нас как раз пока незадействован, а главное -- сделать-то это очень просто! И флаг "синее" будет уставляться ТОЛЬКО формулами (потому как никакого общего правила для него существовать не может).
26.06.2009: итак:
CXCF_FLAG_COLOR_WEIRD=1<<29
-- сдвинув
RED и YELLOW; _cx_rflagslist[]
проапдейчен -- "Value
is weird".
KNOBSTATE_WEIRD
-- после RED;
_datatree_knobstates_list[]
проапдейчен.
choose_knobstate()
--
причем приоритет у WEIRD выше, чем у RED.
XH_COLOR_BG_WEIRD
(после BG_IMP_RED) и
XH_COLOR_FG_WEIRD
(после FG_DIM; нужен чисто для
читаемости на синем), определяемые как
#FFFFFF on #0000FF
(в cx/ too).
MotifKnobs_ChooseColors()
добавлена
соответствующая альтернатива.
Пока не проверял, но не работать там нечему, так что "done".
03.08.2009: полностью портировал и в CXv2. Смысл: у нас ведь уже давно есть потребность в такой фиче -- в случае многобитных входных регистров надо как-то показывать, если комбинация бит "бессмысленна". Такое есть в ringcamsel и в ringpem, а сейчас понадобилось в senkov_ebc. Итак:
CDA_FLAG_WEIRD=1<<20
. Таким образом, этот флаг
находится в компетенции cda, а не Cdr.
OP_WEIRD='\\'
, и добавлена его
условная обработка по образу и подобию OP_CALCERR.
24.07.2012: по здравому размышлению -- WEIRD должен ВСЕГДА быть в компетенции cda, поскольку именно она этот флаг и уставляет.
Так и сделано; с соответствующей корректировкой таблицы строк, и в v2 тоже скопировано.
(При этом сменилось определение
CXCF_FLAG_CDR_MASK
-- на просто явный список. "Смысловые
группы" же теперь в конце, под заголовком "General
classification".)
04.12.2009: резон -- во-первых, уже в протоколе и сервере предусмотрен тип "строка" (для отдачи всяких статусов); во-вторых, гаврикам на балконе 14-го здания понадобилось мочь менять метки у строк (потому как они убивают каналы DL200, и перетыкают концы) -- скорее всего, аналогичная потребность сохранится и в будущем.
Поскольку у строк представление только одно -- собственно текст, то и отображатор, видимо, будет только один -- MotifKnobs_text_text.c.
Главной же сложностью будет корректная интеграция этого в Cdr & Co.
22.12.2009: да, соответствующие определения
структур сделал. И даже ради DATAKNOB_TEXT=6
сдвинул DATAKNOB_USER
в =7.
В dataknob_text_vmt_t
помимо метода
SetText()
имеется также и идентичный knob'ову
Colorize()
.
А из неприятного -- ведь, по идее, такой тип ручек также должен бы реализовывать технологии "usertime" и "wasjustset", но они сейчас плотно привязаны к DATAKNOB_KNOB. А дублировать неохота...
07.08.2015: ну очень уже захотелось сделать поддержку текстовых строк в GUI. Обдумывалось еще неделю назад, а сегодня приступаем. Коснулось тучи мест, но, продолжая то, почти 6-летней давности, запишем здесь.
Файл набит содержимым практически сразу -- благо, в основном было копирование кусков из MotifKnobs_text_knob.c и из MotifKnobs_internals.c -- того, что относится к API "Text", и первоначально создавалось в v2'шном Knobs_text_widget.c.
MakeTextWidget()
сделан
публичным под именем MotifKnobs_MakeTextWidget()
.
SetPhys()
переименован в
SndPhys()
-- так явно корректнее. И введён аналогичный
ему метод SndText()
, в конечном итоге приводящий к...
CdrSetKnobText()
, чьи внутренности практически
отражают товарку CdrSetKnobValue()
.
set_knob_textvalue()
.
dataknob_text_data_t
поля-копии таких же от
dataknob_knob_data_t
(is_rw, timestamp, usertime,
wasjustset, being_modified).
dataknob_knob_data_t
полей curstate, attn_endtime и
attn_state.
data_knob_t
(включая behaviour
-- ради
DATAKNOB_B_NO_WASJUSTSET
). Объём её не увеличится
-- т.к. knob_data и так самая большая, а удобство возрастёт.
cda_acc_ref_data()
:
она даёт указатель непосредственно на внутренний cda'шный буфер со
значением ("acc" -- access), плюс текущий размер данных в нём (этакий
рудимент zero-copy).
08.08.2015@ночь:
решение очень простое: пусть в cda для
CXDTYPE_REPR_TEXT
аллокируется на 1 ячейку больше, а при
получении данных место после nelems заполняется нулём.
09.08.2015: делаем,
txt2ref()
-- регистрирует
ссылку, предварительно парся опциональный префикс "@t[COUNT]:". Флаги
понимаются все те же, а вот тип -- только 't', зато понимает
количество элементов.
Полностью отдельная от src2ref()/cvt2ref(). Хотя в будущем напрашивается как-то это всё унифицировать (передавать туда параметром def_dtype?), чтоб оно и для потенциальных DATAKNOB_BIGC и DATAKNOB_USER сгодилось.
11.08.2015: вытаскиваем некоторые ранее
DATAKNOB_KNOB-specific поля в data_knob_t
.
choose_knob_rflags()
(в нём чуть
переупорядочены проверки, так что сравнения с диапазонами делаются
только для KNOB), choose_knobstate()
и прочие
set_attn()
годятся для ВСЕХ типов ручек.
Colorize()
общим, перенося его в
dataknob_unif_vmt_t
.
set_knobstate()
работают со всеми типами.
MotifKnobs_DecorateKnob()
тоже.
Засим считаем задачу решенной, "done".
15.11.2016: авотфиг! Некоторое время назад, при создании панельки vepp34info.subsys, было замечено, что строковые отображаторы (mesg) почему-то НЕ обновляются и вообще остаются перманентно цвета JUSTCREATED.
Разбираемся:
Вот оно в cda_dat_p_update_dataset()
и пропускает обновления
из не-1-символа для каналов с max_nelems==1.
(А если в канал заслать 1 символ -- то он отображается.)
Итого, выбранное решение: в
Cdr_treeproc.c::txt2ref()
умолчание nelems=1 сменено
на nelems=10
.
Вставлен вызов set_knobstate()
, аналогичный
DATAKNOB_KNOB'ову, и всё стало тип-топ.
Пара мыслей о технике реализации:
big2ref()
, воспринимающей префикс "@T[NNN]:", где "T" -- dtype
(обязательный), а "NNN" -- количество элементов (опциональное).
Синтаксически совместимо с console_cda_util.
Заодно и обычные числовые ручки тоже получат такой функционал.
23.02.2016: сам флаг DATAKNOB_B_ON_UPDATE
создан, но не используется пока нигде.
13.04.2016: кстати, "обновлять"-то -- это прерогатива Cdr, да; но dat-плагину ж (конкретно только cda_d_cx.c) тоже надо будет указывать, что обновление не по циклу. Просто у таких каналов указывать обязательный суффикс "@u"?
25.05.2016: вчера был введён флаг
CDA_DATAREF_OPT_ON_UPDATE
-- вот его и надо использовать.
CONTENT_fparser()
добавлено распознавание флага
/on_update (и, за компанию, /unsavable).
cvt2ref()
/src2ref()
добавлен параметр behaviour
, у которого если выставлен
DATAKNOB_B_ON_UPDATE
, то заказывается
CDA_DATAREF_OPT_ON_UPDATE
.
k->behaviour
передаётся при регистрации
rd_ref.
Просто пока неясно, зачем бы (pzframe_gui пока своими силами обходится).
Однако оказалось, что символы, индифферентные для shell'а, уже почти все заняты:
...а для cdaclient'а по-прежнему что-то надо будет придумать...
20.05.2023: возвращаемся и доделываем поддержку
DATAKNOB_B_ON_UPDATE
для ОБЫЧНЫХ ручек (а никаких "_BIGC- и
_USER-ручек" де-факто сейчас просто нет).
CDA_REF_EVMASK_UPDATE
в evmask.
RefStrsChgEvproc()
переименована в
Knob_rd_Evproc()
.
CdrProcessKnobs()
с cause_conn_n=-1 и
options=CDR_OPT_SYNTHETIC -- т.е., оно выглядит как синтетическое
обновление.
Но решено было такого не городить (хотя и можно было, введя специальный
CDR_OPT_
-битик для options
в
CdrProcessKnobs()
-- обрабатывать ли событийные ручки, и для
обычного процессинга ставить "нет", а изнутри события "да"). Во-первых,
оптимизация сомнительная, а во-вторых (и в-главных) -- обновления по событию
сейчас работают как "синтетические", но надо же и обычные исполнять.
После этого желаемое заработало. Проверено 2 способами, в обоих случаях генерацией изменений чаще, чем цикл сервера:
Понадобилось для таблицы из 2 колонок (ringrf, куркинский модулятор), чтоб в некоей строчке были только в теле колонок метки "1" и "2", а row-label чтоб "". Но вот фиг -- нет способа.
@Вечер-дорога-домой-Лаврентьева принять, что "ну вот так оно, смиритесь"?
09.10.2017@утро-лифт-с-1-го-на-5-й: а если ввести ЕЩЕ один такой префикс, но с обратным значением -- "эта метка/тултип только для ручки, а в заголовок её не копировать!"? Например, '\r' -- он и близок к '\n', и практического смысла в начале строки в принципе не имеет.
10.10.2017@вечер-дорога-домой-около-ИПА: да, и -- чтобы всё
чётко различалось по именам -- этому нововведению дать дополнительный
префикс "TITLE
", а старому -- "KNOB
".
11.10.2017: делаем.
CX_KNOB_NOLABELTIP_PFX_C
и
CX_KNOB_NOLABELTIP_PFX_S
(нигде не
используется) ничего добавлять не нужно -- там словцо "KNOB" и так
фигурирует.
CX_TITLE_NOLABELTIP_PFX_C
и
CX_TITLE_NOLABELTIP_PFX_S
.
get_knob_label()
и get_knob_tip()
:
добавлена дополнительная проверка-альтернатива на
CX_TITLE_NOLABELTIP_PFX_C
, для сдвига указателя на 1 символ
(пропуск '\r').
get_knob_rowcol_label_and_tip()
: в строчки "не взять ли
label и tip от model_k" добавлены 2 дополнительных условия -- что
соответствующая строка !=NULL и что она не начинается с
CX_TITLE_NOLABELTIP_PFX_C
.
CX_KNOB_NOLABELTIP_PFX_C
-- создание списков в selector_knob и
choicebs_knob. Так вот: тех мест эта тема не касается, т.к. там
'\n' используется в других целях -- показатель disable'нности
пункта.
Проверено -- вроде работает, "done".
23.06.2007: итак:
data_subsys_t
содержит список секций, плюс -- готовые
указатели на некоторые стандартные разделы, типа
grouping
'а.
DataKnob
'ом.
param1
,
param2
, param3
. Первые два как раз и будут
за ncols и nfloors.
label
) введены строковые str1
,
str2
, str1
-- они и будут за rownames,
colnames, subwintitle.
dataknob_knob_data_t
ВСЕГДА иметь поле string-list, для
селектора -- чтобы эта штука была отвязана от look'а "селектор".
03.07.2007: да, ввел поле items
,
ПЕРЕД units и dpyfmt. И в KnobsCore_simple.c его поддержка тоже
добавлена.
Теперь надо вводить функции для обработки селекторных строк-списков -- подсчет числа элементов и выдача элемента по номеру.
02.07.2007: в принципе, ничего сложного. Проект:
dataknob_unif_vmt_t
-- поскольку такой метод нужен и
контейнерам, и ручкам, и user-type'ам.
int
, при 0 обход
продолжается, а не-0 означает, что метод команду обработал и можно
завершать обход.
03.07.2007: да, введен метод
HandleCmd()
, int, прямо в dataknob_unif_vmt_t
.
09.08.2007: и функция
CdrBroadcastCmd()
изготовлена -- тривиально. Так что этот
раздел можно считать за "done".
08.11.2010: побочный эффект, о котором тогда не задумывалось: можно делать "локальные" тулбары для подветок -- так что команды будут бродкаститься только на эту под-иерархию. Полезно, например, для fastadc-плагинов.
Всё получится автоматически -- достаточно указать верхушку под-иерархии.
05.07.2007: угу.
Теперь флаги "что это есть такое" (BUTTON, LIGHT, ALARM, ...) идут вверх с 0, а "то, как с ручкой обращаться" (GROUPABLE, NO_WASJUSTSET, INCDECSTEP_FXD) -- вниз с 31.
И DATAKNOB_B_IS_SELECTOR
введен, он -- 3.
P.S. Естественно, все _B_HALIGN_* и _B_VALIGN_* оставлены в прошлом -- они теперь и нафиг не нужны, и вообще мимо кассы.
23.07.2007: введен такой метод, и text_knob ему уже даже обучен -- для перещелкивания пометки "загруппировано".
Вызывать этот метод, конечно, пока что некому -- ибо Chl пока даже директории нету -- но это дело наживное.
16.07.2007: он у нас предусмотрен -- код выделен -- но пока никак не поддерживается. Предполагалось, что это будет некое "надмножество" над типом CONT.
Сначала вопрос: а что вообще за свойства/поля такие в типе SYST, которых не будет в CONT? Вроде что-то предусматривалось, но вот что...
Далее идея реализации: по аналогии с парочкой KNOB/NOOP --
пусть FindVMT()
умеет при поиске SYST подхватывать и CONT
с подходящим именем. А в (будущей) структурке
dataknob_syst_data_t
первым полем будет как раз
dataknob_cont_data_t
, так что обычные контейнеры смогут
преспокойно работать в роли группировки.
Так что, если понадобится этот тип -- то сделается все без проблем.
Несколькими минутами позже: да, сделал все по вышеприведенному
проекту: и dataknob_syst_data_t
введено, и
FindVMT()
правильно обучена.
27.07.2007: нашлось "специфичное для grouping" --
поле treeplace
, жившее в
dataknob_cont_data_t
. Но, по-хорошему, это вообще надо
выносить на уровень именно подсистемы, а не группировки. Так и делаем,
а из контейнера -- удаляем.
16.07.2007: да, поменял везде SYST/syst на GRPG/grpg.
data_subsys_t
, ВООБЩЕ никак не
привязанные ни к каким секциям, все-таки будут.
В частности, сейчас туда напрашивается список макросов. С одной стороны, он нужен скорее только для парсинга, но все же полезно его иметь. Да и парсеру тогда легче будет -- не надо заводить у себя :-)
19.07.2007: неа, а вот неправильно это. Лучше все-таки список макросов держать в контексте парсера.
23.07.2007: да, макросы перенесены прямо в контекст парсинга.
А "куски ... никак не привязанные ни к каким секциям" -- возможно, все же и появятся. Тогда и будем думать конкретно.
23.07.2007: появятся, появятся. Как минимум -- "datainfo", оно ведь должно быть привязано именно к подсистеме.
Плюс, либо в datainfo же, либо прямо в подсистеме -- "CxDataRef at_init",
опциональный код инициализации, который надлежит исполнять сразу после
realize подсистемы. 13.02.2011@Снежинск-каземат-11: неа, не в подсистеме -- а в
каждом контейнере, т.е., в dataknob_cont_data_t
.
10.08.2010: кстати, а не сделать ли в дополнение к "at_init" еще и "at_term" -- которое бы вызывалось при завершении работы программы?
Смысл --
Для реальной юзабельности этой фичи потребуются 2 основных условия:
CdrDestroySubsystem()
, а не просто exit()
.
XtAppMainLoop()
etc.), и сигналы. И чтобы по этим вещам
выполняли "корректное завершение".
И еще --
Не слишком ли много сложностей?
Пожалуй, правильнее оставить возможность делать что-то по выходу только ОБЫЧНЫМ программам, а chlclient'ам это незачем.
#define
'ов со стандартными именами -- чтобы не указывать
всякие "grouping" каждый раз вручную (что чревато ошибками).
(На эту тему даже в документации по Xt отдельно было сказано
-- что всякие XtNnnn сделаны ровно для предохранения от
опечаток.)
Возьмем префикс, например, DSTN_
(Data Section Type
Name).
24.07.2007: да, DSTN_GROUPING
уже
введен, по мере надобности будем вводить еще имена.
И, поскольку этот "тип" -- штука фиксированная, то в
data_section_t.type
всегда будет прописываться просто
указатель на строку, а не на malloc()'ed memory, так что его НИКОГДА
освобождать и не надо. О чем там сделан комментарий.
data_knob_t
.
27.07.2007: во-первых, так что type
,
look
и options
-- т.е., те, которые
определяют "суть" ручки -- теперь идут самыми первыми.
Во-вторых, текстовые поля теперь идут "по мере убывания важности" -- сначала ident и label, а уж за ними -- редконужные tip, comment, style, layinfo и geoinfo, которые в описании группировки-то являются до того optional, что могут указываться только-по-имени.
dataknob_knob_data_t.group_coeff
завести просто еще один
параметр.
03.08.2007: да, сделано -- введен индекс
DATAKNOB_PARAM_GRPCOEFF
=2, плюс резервный
DATAKNOB_PARAM_RSRVD1
, а нижеидущие сдвинуты на 2.
И CreateSimpleKnob()
с
Cdr_fromtext.c::KNOB_fields[]
проапдейчены соответственно.
И тут вопрос: делать это полями в dataknob_grpg_data_t
,
или же (учитывая, что понадобится еще и где-то держать описание
toolbar'а) заводить для оного отдельные секции в
data_subsys_t
?
10.04.2008: да, и нужен еще один параметр -- "метка", которая аналогична полю data_knob_t.label (а не data_knob_t.tip, как DSTN_WINTITLE). Смысл -- чтобы в окошке "стартера" отображалась бы эта метка, а не имя подсистемы (например, "ФЭУ кольца", а не "ringpem").
31.07.2007: выход напрашивается простой --
добавить в data_section_t
еще указатель на
функцию-деструктор, который если не-NULL -- то можно вызвать, и оно все
сделает.
Тогда всякие хитрые плагины (это будут уже "subsystem-sections plug-ins" :-) будут туда прописывать свои специфичности.
В общем -- сделал поле destroy
. А в
CdrDestroySubsystem() у него будет приоритет -- если оно не-NULL, то
дергается этот деструктор, а внутренние "знания" не используются.
31.07.2007: конкретно -- ведь у KNOB'ов есть еще
методы SetValue(), Colorize(), PropsChg(), [...], которые в
dataknob_noop_vmt_t
напрочь отсутствуют за ненадобностью.
А кто-нибудь такой метод возьмет да вызовет -- да хоть Colorize при
создании "ручки"...
С CONT/GRPG чуть проще, поскольку у GRPG пока никаких дополнительных методов не предвидится.
И что делать будем? "Правильный"-то способ -- добавить
отсутствующие поля. Но это муторно, да и некрасиво. Потому --
придется использовать второй метод: перед вызовом методов кроме прочего
обязательно проверять, что
k->vmtlink->type==DATAKNOB_KNOB
. Нервно, конечно,
но мест не так уж и много. (А на крайняк -- можно будет сготовить в
libdatatree специальные функции-accessor'ы, которые и будут выполнять
все проверки перед вызовом.)
По второму варианту и сделал. В datatree.c добавил
проверки в set_knobstate()
и
set_knob_controlvalue()
(в них уже были проверки на
k->type==DATAKNOB_KNOB
, но это ведь не то --
проверять надо именно VMT!). А в
MotifKnobs_DecorateKnob()
вставил проверку скорее для
порядку -- оную никому, кроме Create_m'ов настоящих KNOB'ов вызывать и
не положено.
Так что теперь осталось только помнить необходимость проверки на будущее -- в Cdr и в Chl (до которой в нормальных условиях и не должна дойти необходимость иметь дело с NOOP в роли KNOB'а). "done".
01.08.2007: во-первых, "то, что исполнять при
обработке контейнера" -- ap_src и ap_ref ("ap" -- "At Process")
добавлены к dataknob_cont_data_t
.
Во-вторых, в связи с изобретением сегодня алгоритма "выборочного
обновления ручек по приходу данных" -- добавлен
uint8 *conns_u
("u" - Used) в
data_knob_t
.
13.02.2011@Снежинск-каземат-11: также добавляем в
dataknob_cont_data_t
, рядышком с ap_*, и "at_init" --
at_init_src
и at_init_ref
.
Не забыт и его парсинг и освобождение в
Cdr_fromtext.c::CONT_fields[]
и
CdrDestroyKnobs()
, но вот ВЫЗОВ пока не сделан.
02.10.2014: и вызов сделан и сегодня доделан.
02.08.2007: понадобился он для subwin_cont'а, чтобы отображать цветом состояние под-окна.
Метод добавлен, и вызов его в CdrProcessKnobs()
тоже.
03.08.2007: первоначально-то делал с подчерком потому, что казалось, что как-то кривовато выглядит -- если всякие CdrP.h имеют и заглавные буквы, то тут, мол, надо бы отделить подчерком. Фигня -- не надо. Только путаницу вносит из-за нестандартности.
Так что -- переименовал, везде в коде исправил ссылки, и в этом файле тоже везде позаменял, кроме ЭТОГО раздельчика.
dataknob_knob_data_t
есть куча битовых флагов текущего состояния -- userval_valid,
curv_raw_useful, wasjustset, undo_val_saved, is_ingroup -- а не сделать
ли их (экономии ради) битиками в одном слове "текущего состояния"?
11.08.2007: способ напрашиватся сразу -- ввести в
dataknob_cont_data_t
ссылку на подсистему. Во-первых, это
достаточно -- каждая ручка будет содержаться внутри элемента, так что
subsys, к которой она принадлежит, добывается на раз. Во-вторых --
поскольку размер data_knob_t.u
все равно определяется
dataknob_knob_data_t
, а не cont, то на занимаемую память
это никак не повлияет.
Так и сделал -- поле subsyslink
, не забыв в
Cdr_fromtext.c::ParseKnobDescr()
проставлять туда нужную
ссылку.
13.08.2007: Upd: в libdatatree добавлен вызов
get_knob_subsys()
, добывающий DataSubsys указанной ручки
"интеллектуально".
11.08.2007: да, вводим DSTN_SYSNAME
и "шорткат" data_subsys_t.sysname
, не забывая и его
проставление.
DATAKNOB_B_STEP_FXD
вводить также
DATAKNOB_B_RANGES_FXD
-- запрет менять диапазоны.
11.02.2011@Снежинск-каземат-11: резон -- в некоторых случаях (кнопки включено/выключено в УБСах в liucc) диапазоны являются как бы не числовыми, а "селекторными", и тогда их изменение влияет не количественно, а качественно -- полностью меняет СХЕМУ сигнализации.
Так вот -- чтобы закрыть эту проблему, надо ввести behaviour-флажок "запретить смену диапазонов", но выставляться он должен не типом ручки, а парсером подсистем по некоему ключику в тексте -- например, "dpy/fixedranges".
13.02.2011@Снежинск-каземат-11: флаг добавлен, но в Chl_knobprops пока не используется.
14.02.2011@Снежинск-каземат-11: флаг
DATAKNOB_B_RANGES_FXD
заиспользован в
Chl_knobprops.c::ShowOneRange()
. Правда, проверить
пока никак.
18.02.2011@Снежинск-каземат-11: заодно сделал, чтоб для не-rw-ручек диапазон ALWD считался бы отсутствующим -- ибо он там совсем ни к чему.
03.12.2013: и парсинг behaviour-флагов тоже сделан.
Работа проверена, всё окей, так что "done".
set_knob_controlvalue()
факт записи протоколировался в
event_log. Нужно -- например, в kuznitsa при изменении значения накала,
чтоб сей факт записывался независимо от включенности периодического
логгинга.
Вопрос только -- КТО будет это делать и как. Наверное, придётся ввести hook.
IdentifierIsSensible()
.
А сейчас вот реализуем в manyadcs сохранение данных от "прочих" fast-ADC, и ясно, что такое должно делаться стандартными средствами "среды". Но вопрос об ограничении -- кого сохранять, а кого нет -- по-прежнему стоит, и именами оно не шибко правильно.
А чё б это не повесить на что-то типа "behaviour" -- чтоб можно было для любого узла указать, что "начиная с этой точки сохранять не надо".
Только behaviour
-- поле в
dataknob_knob_data_t
, а надо бы что-то пообщее. Ввести нечто
прям в data_knob_t
.
22.05.2014@Снежинск-каземат-11: несколько в другую сторону, хоть и связано -- насчёт сохранения вообще.
23.05.2014@Снежинск-каземат-11: и еще прикол: идентификаторы используются как
Т.е., (1) -- по смыслу, а (2) -- декоративное.
Но они вступают в противоречие: по смыслу может понадобиться иметь идентификатор ":", а для оформления при этом он будет нужен!
И добавление правила "считать прозрачными все имена, НАЧИНАЮЩИЕСЯ с ':'" не сильно поможет, из-за проблем с поиском -- см. bigfile-0001.html за 27-08-2013.
28.07.2014: да, НАДО считать прозрачными:
Пара замечаний:
Но тут всё складывается наилучшим образом:
11.08.2015: учитывая, что экраны избавлены (СЕЙЧАС) от функционала сохранения/чтения режимов, вопрос вообще несколько подвисает в воздухе -- не станет ли он вообще obsolete?
09.10.2015: а вот и нет, для маленьких стендов этот функционал очень нужен, даже НЕОБХОДИМ.
Дальнейшие рассуждения -- в раздельчике "Clientside-режимы".
data_knob_t
» из предыдущего пункта понадобится для
еще одной вещи: мочь указывать контейнеру (ОБЫЧНО контейнеру), что не надо
использовать фичу "conns_u", а принудительно уставить оное в NULL.
Нужно для того, чтобы ручки, ссылающиеся только на локальные регистры (а НЕ на каналы), и контейнеры их содержащие, обновлялись бы вместе со своими содержателями. А то на liu уже задолбало в формулы добавлять фейковые ссылки на каналы "just for updatability".
(Да, частично проблема обусловлена самой моделью группового обновления и неясностью с тем, как её изменить/улучшить для v4 (экран на одних только регистрах будет вообще необновляемым никогда... :(); но всё же...)
Ну что -- переносим behaviour из dataknob_knob_data_t
в
data_knob_t
?
11.08.2015: да, behaviour перенесён. Хотя флаг "не использовать conns_u" пока отсутствует.
27.09.2016: как выяснилось, та необновляемость была вызвана косячной реализацией. Следовательно, никакой флаг "DATAKNOB_B_NO_CONNS_U" не нужен, и раздел "withdrawn".
...а behaviour
останется в data_knob_t
, ему там
самое место.
CX_KNOB_NOLABELTIP_PFX_C
" занимается прямо сама
get_knob_rowcol_label_and_tip()
.
25.06.2007: сделана функция
find_knobs_nearest_upmethod()
, производящая такой поиск и
возвращающая указатель на метод, а при ненайденности -- NULL.
Поскольку в C нет такого понятия, как "указатель на поле", а тем более -- на произвольное поле, то функции передается просто оффсет этого поля в VMT, а уж она выполняет надлежащую арифметику.
Так что теперь именно эта функция используется и в
set_knob_controlvalue()
-- для метода SetPhys(), и в
MotifKnobs_internals.c, для методов ShowProps(), ShowBigVal() и
ToHistPlot(). Поэтому сии методы можно указывать только у контейнера
верхнего уровня (группировки), а у промежуточных элементов -- оставлять
NULL (хотя -- см. проблему «в "методах элемента" было смешано
несколько разных сущностей» 21-06-2007). С ShowAlarm(),
видимо, будет чуть хитрее -- хотя и ненамного.
05.07.2007: общие положения:
get_knob_rowcol_label_and_tip()
.
В общем -- так и сделано, функции
get_knob_items_count()
и get_knob_item_n()
.
Последней передается уже только указатель на приватную структуру о
состоянии -- строку передавать незачем, поскольку вся информация
сохранена в структуре.
06.07.2007: дополнение: добавлена возможность
указывать тултипы и отдельным пунктам -- по аналогии с метками
строк/колонок, можно указать тултип через CX_SS_C ('\b').
Для возврата оного, добавлен параметр char **tip_p
,
могущий быть NULL.
15.07.2007: проверил -- все работает. И тултипы на отдельные пункты селектора также навешиваются.
12.05.2009: forw-портировал из CXv2 свежевведенную возможность указывать через MULTISTRING_OPTION_SEPARATOR (=CX_SS_C='\b') еще и "стиль". Правда, в selector'е оно пока не используется (поскольку стили пока никак не поддерживаются).
03.08.2007: сделан
choose_knob_rflags()
, пока что БЕЗ махинаций с RELAX и
OTHEROP -- поскольку с ними надо разбираться отдельно, разделяя
обязанности этой функции и прочих.
И в SetSimpleKnobValue()
также добавлены вызовы
choose_knob_rflags()
и set_knobstate()
.
16.07.2007: "пользователей" этих имен и меток двое:
Для первого хватит функции типа "get_param_ident(str,param_n)", которая бы при str!=NULL вернула бы ее, иначе при param_n<DATAKNOB_NUM_STD_PARAMS возвернула бы имя из массива, а совсем иначе -- NULL. И аналогично с меткой.
Второму же нужна функция
CxKnobParam_t *find_param_by_name(char *name, CxKnobParam_t *params, int numparams)коея вначале сделает простой поиск по этому списку, а при ненайденности сделает lookup по стандартной таблице, и если найдет и в params по номеру найденного .ident==NULL, то вернет его.
(Поскольку за стандартные параметры отвечает datatree, то и вышеописанная алхимия в ее компетенции.)
03.08.2007: стандартных параметров уже не 10, а 12.
ack_knob_alarm()
.
30.07.2007: причина рождения этой функции -- то,
что вызывать parent'ов AckAlarm() нужно из нескольких мест, а процесс
его вызова -- из-за find_knobs_nearest_upmethod()
--
весьма нетривиален.
MotifKnobs_Mouse3Handler()
и
MotifKnobs_CommonKbdHandler()
не мучались -- добавил
функции show_knob_props()
, show_knob_bigval()
и show_knob_histplot()
.
01.08.2007: кстати, а вообще, не скрыть ли тогда
find_knobs_nearest_upmethod()
внутри datatree? Ведь, по
идее, для всех VMT-вызовов должны иметься подобные функции-переходники,
а их "клиентам" совершенно незачем знать специфику выполнения вызовов.
03.08.2007: да, сделал и заиспользовал.
(Возникло из идеи "а на что еще можно было бы обобщить работу weldproc_drv, и насколько сам он реально нужен -- чё б не обходиться просто записью в каналы таблицы напрямую из программ?")
11.07.2018@утро-пляж: соображения (просто поток сознания):
DATAKNOB_VECT
.
В отличие от узлов TEXT, где схалявлено и напрямую сбагривается по указателю от cda к XmText'у.
@вечер: еще что-нибудь с cur_nelems устроить?
11.07.2018@дома-душ-после-пляжа: типа продолжение:
Тут будут трудности: как поддерживать кратное количество столбцов/строк (чтоб общий размер делился нацело), ...?
@после-обеда-переход-в-13-е: да, иных вариантов и не видно, кроме кнопок "галочка" и "x". Точнее, этот выглядит самым правильным (а мутить с "появлением" кнопок при начале редактирования, как делают Excel и OpenOffice -- в Motif'е опухнешь, из-за менеджера геометрии).
Вот если б реальное использование где-то требовалось -- тогда да, а так -- особо незачем.
11.07.2018: делаем пока минимум -- в datatreeP.h:
DATAKNOB_VECT
=9
dataknob_vect_data_t
, с полями dpyfmt, src,
ref.
data_knob_t.u
под именем "v
".
dataknob_vect_vmt_t
-- пока без специфичностей.
Далее потребуется интеграция по данным (уведомление knobplugin'а о новых данных плюс передача им новых данных) и "угражданивание" (поддержка в Cdr_via_ppf4td.c и Cdr_treeproc.c).
12.07.2018@утро-пляж: а как быть с max_nelems? Вариантов светит 2:
Если чуть подумать, то очевидно, что 2-й вариант не катит: даже если забыть про меньшее удобство доступа, всё равно остаётся вопрос "выбора правильного места для функционала". Если без словоблудия, то: теоретически vect-плагины могут использоваться в simple-режиме, БЕЗ ссылок на каналы; и тогда остаётся только 1-й вариант, как единственный удовлетворяющий требованиям.
12.07.2018: да, в dataknob_vect_data_t
добавлены:
max_nelems
(указывабельное группировкой/создателем) и
cur_nelems
(будет заполняться Cdr'ом или потенциальным
simple-интерфейсом).
Кстати, а rflags и timestamp уже есть -- общие в
data_knob_t
.
databuf
, имеющий тип double*
.
Да, API будет ориентирован на double'ы -- ровно как и у скалярных ручек.
16.07.2018: замечание: хоть "профита немного", но всё же по минимуму делаем -- то, что ПРОСТО (и очевидно).
16.07.2018@вечер-пляж:
надо объединять поля max_nelems
и
src
в одну структуру. Смысл:
data_knob_t
-- как это делает
CONTENT_fparser()
-- для получения указателя на вмещающую ручку
и уже поштучного парсинга в неё.
dataknob_vect_src_t
".
16.07.2018@хбз-когда-дорога-на-работу-мимо-НИПС: был вопрос, как делать кнопку "отправить", которая должна выглядеть как зелёная галочка (в противовес "отмене", которая красный крестик).
Да просто: символ "=".
Т.е., будет пара кнопочек -- [=][X].
17.07.2018@утро-пляж: некоторые мысли:
double
указывать дуплет {int nelems
, double
*values
}.
_k_setvectv_f
под названием
SetVect()
.
set_knob_vectvalue()
-- третья,
после set_knob_controlvalue()
и
set_knob_textvalue()
.
SndVect()
и
соответствующая поддержка в Chl_app.c в виде
_ChlSndVect_m()
, адаптера к тоже требующемуся
CdrSetKnobVect()
.
Тем более, что сами поля уже давно прямо в общем data_knob_t
Кста-а-ати, см. в пункте "о реализации таблиц" за 11-06-2017 :D Там бОльшая часть мыслей про столбцы, число строк, кнопки "Отправить" и "Отмена", плюс еще идея насчёт нумерации строк (для больших количеств актуально).
17.07.2018@~12:40-дорога-на-работу-мимо-НИПС-и-ИПА:
17.07.2018: реально то, что сейчас готовим под именем "DATAKNOB_VECT" -- это то, что изначально задумывалось как DATAKNOB_BIGC.
И в ручках DATAKNOB_USER, если они когда-то реально появятся, де-факто
будут массивы именно из dataknob_vect_src_t
.
18.07.2018: ведём минимальные работы -- пока в отдельной от основного дерева work/z4cx/.
dataknob_vect_src_t
для дуплета сделан и переведено
на него (еще вчера).
_k_setvectv_f
, с ним сделан метод
dataknob_vect_vmt_t.SetVect()
.
_k_sndvect_f
, для...
dataknob_cont_vmt_t.SndVect
-- вот из-за его вставки
пришлось пораздвигать VMT во всех cont-knobplugin'ах.
set_knob_vectvalue()
-- скопирована с очевидными
модификациями с set_knob_textvalue()
.
VECTREF_fparser()
, сводящийся к вызову
INT_fparser()
с REF_fparser()
, с
проверкой/пропуском ':' между ними.
Да, кстати, еще изначально 16-07-2018 при осознании, что парсить max_nelems+src надо вместе, был придуман формат N:REF; т.е., max_nelems:channel_reference -- похоже на синтаксис указания cdaclient'овских каналов, только без префикса типа.
20.07.2018: авотфиг: не работает такой вариант с вызовом
INT_fparser()
. Проблема в том, что тот использует
ppf4td_get_string()
, а та НЕ заканчивает парсинг по
':' (да и вообще нету "PPF4TD_FLAG_COLTERM"); в результате токеном
оказывается нечто вида "31:out_tab_times". Вывод: придётся
считывать целое самостоятельно по циферке.
21.07.2018: ой, нет, зачем по циферке -- есть же
ppf4td_get_int()
(реализованный недавно для histplot'а)! На
него и переведено.
VECT_fields[]
-- копия
PZFR_fields[]
, с отличием только в строке "r".
CONTENT_fparser()
-- опознание ключевых слов
vector и column.
ParseKnobDescr()
:
DATAKNOB_VECT
таблички
VECT_fields[]
.
k->is_rw = kind == DATAKNOB_KIND_WRITE;
-- аналогично DATAKNOB_TEXT'ову.
CdrDestroyKnobs()
: подчистка.
FillConnsOfContainer()
: добыча списка слизана с
TEXT'ова.
vec2ref()
:
Так что унификации, о которой думалось при введении
txt2ref()
, покамест не получится.
dataknob_vect_src_t
.
CXDTYPE_DOUBLE
, с указанным
max_nelems
.
CdrRealizeKnobs()
: тут всё тривиально и слизано с
TEXT'ова.
Плюс аллокирование databuf.
CdrProcessKnobs()
: тоже всё просто и аналогично
TEXT'ову.
Отличие в том, что данные добываются в "свой буфер" -- databuf. ...а ЗАЧЕМ, кстати? А для возможности делать "cancel", по Esc.
CdrSetKnobVect()
-- опять же очевидно слизано с
CdrSetKnobText()
.
_ChlSndPhys_m()
-- тривиальный
адаптер к предыдущему.
Ну, собственно -- вроде как всё, теперь нужен MotifKnobs_text_vect.c.
Хотя идеологически/методологически остался вопрос: а нафига нам
databuf[]
?
dataknob_knob_data_t.curv
-- т.е., туда складывается полученное
от сервера значение.
А коли так, то его НЕЛЬЗЯ менять knobplugin'у, не его это владение:
придёт от сервера новое значение, даже при взведённом usertime
и типа заблокированном обновлении -- и всё, данные будут испорчены.
userval
, а для возможности undo -- еще и с аналогом
undo_val
...
Вот и получается, что:
cda_acc_ref_data()
.
dataknob_vect_data_t.cur_nelems
нафиг не нужно: оно имеет смысл только в связке с databuf[], а без него
достаточно локальной переменной при передаче данных
SetVect()
'у.
is_rw
-ручек, т.к.
ro'шным оные удобства без надобности.
19.07.2018@утро-дома: с
синтаксисом указания ссылок N:CHANREF может быть проблема --
ParseKnobDescr()
воспримет это "N:" как ключ (имя поля).
Анализ кода показывает, что достаточно заменить в предварительной
проверке isletnum(ch)
на isalpha(ch)
-- всё равно
имя поля не может начинаться с цифры.
20.07.2018: да, ровно эта проблема вылезла, и была решена именно тем способом.
19.07.2018@обед-дома-лестница-по-дороге-на-работу: а еще не помешала бы возможность ограничивать диапазоны ввода.
Только куда тут приткнуть этот alwdrange?
RNG_fparser()
?
23.07.2018: knobplugin допилен, проверено -- инфраструктура работает.
24.07.2018: да, dataknob_vect_data_t
'овы
поля cur_nelems
и databuf
убраны (точнее,
за-#if0'ены, с комментарием /*!!! vect:databuf*/
).
И всё окей.
25.07.2018: проверен уже и второй knobplugin (text в дополнение к matrix'у), и из временного места в work/z4cx/ всё перенесено в основное в work/4cx/.
Засим проект можно считать реализованным (2 недели от замысливания, 1 неделя плотной работы).
22.06.2023: и саму work/z4cx/ удаляем -- давно было пора, она только grep'у мешалась (т.к. подходила под "work/*4cx/").
21.06.2007: предполагаемые компоненты библиотеки:
08.09.2009: во-первых, "ядро" давно уже именуется cda_api.c, а не "cda_core". 31.08.2014: уже cda_core.c.
Во-вторых, менеджер плагинов всё-таки живёт в отдельном модуле cda_plugmgr.[ch].
Ну и как эту проблему решать будем? Выпендриваться с точками -- типа "..ПРОТОКОЛ.СЕРВЕР"? (одиночная "." начинает "абсолютную" ссылку; в VMS двойное двоеточие -- "::" -- отделяло (хотя и не префиксировало) имя узла.)
Или tcl подскажет идею?
10.07.2007: хе, все очень, ОЧЕНЬ просто!
Ведь нам пришлось бы извращаться с префиксами "..ПРОТОКОЛ" из-за того, что символ ':' уже занят, причем и удвоенный тоже -- под разделитель имени/номера канала от имени сервера, так? Ну дык надо забить на тот странный синтаксис с "!КАНАЛ"/"::КАНАЛ", и использовать в качестве такого разделителя просто '.' -- это будет совершенно однозначно и логично. Тогда "::" можно применять в качестве опционального префикса протокола (будет ТОЧНАЯ аналогия VMS'у).
Итого, допустимые варианты синтаксиса имен каналов:
cx::linac1:5.dac2.current1 | полное имя канала. |
current1 | имя канала "в текущем контексте". В cda оно будет резолвиться в
текущем сервере; в Cdr к нему будут добавлены сконкатенированные через
'.' поля refbase всех его содержателей.
(Кстати, при этом '.' оказывается наиболее удобной -- поскольку самый-верхний refbase обычно будет просто именем сервера, то можно будет и после него не задумываясь ставить разделитель '.'.) |
.dac3.current2 | "абсолютная" ссылка, к которой в Cdr добавляется лишь самый-верхний refbase, т.е. -- имя сервера. |
epics::gusev:smth.smth.else | не-CX'ные имена каналов могут содержать хоть точки, хоть двоеточия в произвольном количестве, и резолвиться это будет уже, например, EPICS'ом. |
Замечание 1: и Cdr (в ссылках в дереве), и cda (в каналах в формулах) при наличии у ссылки на канал префикса "ПРОТОКОЛ::" будут автоматически считать это "абсолютной" ссылкой и НЕ префиксировать такое имя никакими refbase вообще.
Замечание 2: в 90% случаев все refbase, кроме самого верхнего -- обычно являющегося ссылкой на сервер -- будут пустыми.
06.08.2007: дополнительный вопрос: хочется ведь, чтобы автоматом за абсолютные брались ссылки вида "HOST:N", т.е. -- ссылки на другие сервера.
Но: в случае EPICS'а-то строка /[a-z0-9]+:.*/ может быть и относительной ссылкой -- относительно предыдущих уровней. И -- как будем это противоречие разрешать?
07.08.2007: пока мы на это противоречие просто забъем.
Т.е., сделаем так:
На будущее же можно ввести какой-нибудь легкий хак -- например, если ref[0]==':', то это первое двоеточие отбрасывается, а далее идущее считается относительной ссылкой.
18.09.2007: все-таки есть, есть проблема с использованием '.' в качестве разделителя сервера и канала. Формат получается [ПРОТОКОЛ::]СЕРВЕР[:N].КАНАЛ, так что при отсутствии ":N" это будет выглядеть как ХОСТ.КАНАЛ, что распознАется как FQDN.
Другое дело, что ":N" будет указываться практически всегда, так что проблема скорее теоретическая.
11.12.2013: еще одна потенциальная проблема, уже не-только-epics'ная: имена хостов могут содержать дефисы, которые в формулах должны восприниматься как оператор "минус".
Очевидно, надо ввести правило, что имена каналов в формулах могут кавычиться -- хоть в обычные двойные, хоть одинарные (это чтоб не иметь проблем ни с C'шными строками, ни с парсером .subsys-исходников). Соответственно -- из кавычек всё берётся "как-есть", и далее используется уже как цельное имя канала, с поиском только точек и двоеточий (как описано выше в 2007-м).
(Теоретически есть еще одно решение: требовать указывать в таких именах хостов вместо дефисов (-) подчерки ('_'), и чтоб cxlib при hostname-lookup'е сам заменял подчерки на дефисы (пользуясь тем, что по RFC как раз именно только дефисы и могут быть). Но это, во-первых, использование сравнительно тонкого хака (про запрет подчерков в именах), а во-вторых потребует лишних телодвижений при подготовке ссылок (которые могут и генериться).)
12.04.2014@Баня-Б12-бассейн: некоторое количество мыслей на тему "ссылки на каналы и формулы, как их окучивать в cda/Cdr/программах":
src2ref()
-- ей передавать cid,base,src,rw.
cda_add_formula(cid, base, text,
flags/rw)
.
13.04.2014@дома: nCdr: надо еще отличать ссылки на канал от формул и от регистров (bigfile-0001.html от 09-06-2007/15-06-2007):
А ведь когда-то хотели сделать '=' префиксом принудительно-формулы -- чтоб "=ВЫРАЖЕНИЕ" (только где это записано -- не находится)...
А не сделать ли прямо в cda функцию "cda_src2ref()", определяющую тип ссылки и саму выполняющую вышеописанную эвристику? 15.04.2014: вряд ли получится -- ведь префикс "@T[count]" должен парсить сам клиент...
11.10.2018: возникла потребность (в v5rfsyn.subsys) уметь "выходить" из пространства имён сервера, чтоб переходить к безсерверным (глобальным) именам.
Причём даже мнемоничность наличествует: уровнем выше от сервера -- как раз и будет уровень серверов.
Главная задача -- понять, КУДА эти "мозги" (отбрасывать сервер; как?)
засунуть в cda_core.c: там ведь то ещё шаманство, с этими
combine_name_parts()
(делать-то должно, видимо, она) и
kind_of_reference()
(эта вообще фиг с ходу поймёшь, где/как
влияет...).
Парой часов позже: отлов "'.' вначале" делается в
combine_name_parts()
после комментария "From-root reference?".
Туда же надо проверку на ".:" ставить, да?
А имя сервера как откидывать (причём, оставляя протокол)?
cda_ref_is_sensible()
, которая проверяет на
не-CDA_ERR и не-CDA_NONE.
04.09.2007: во-первых, нарисовал базовый API cda, требуемый для Cdr'а. API вышел несложным, едва ли не проще, чем в CXv2 (из-за большей унификации).
Во-вторых, стояла проблема -- что при сбагривании cda ссылок на каналы (и формул) надо передавать "base reference", которая, в свою очередь, образуется хитрым образом от более верхних baseref'ов, и -- получалось, что Cdr должна знать правила "наследования". Так вот -- надо просто сделать в cda API, по триплету (top_refbase,cur_refbase,ref) возвращающий "effective reference", который становится либо уже абсолютной ссылкой на канал, либо effective-refbase'ом очередного уровня.
28.06.2008: первая-то мысль, пришедшая в голову -- разбить такие "вставки" на ДВЕ обязательные части: декларация, в которой надлежит "регистрировать" все затрагиваемые каналы, и собственно "код" формулы.
Но после знакомства с LabVIEW'шными "formula nodes", у которых есть inputs (входные параметры) и outputs (нитки-результаты), возникла модификация этой идеи: сделать что-то в этом же духе -- т.е., интересующие каналы не просто "объявлять" заранее, а авансом, ПЕРЕД вызовом "формулы, добывать их значения и передавать в качестве параметров.
При этом мы просто на корню убиваем проблему "формула затребует-таки каналы, забыв их декларировать", да и никаких вызовов-cda-из-Tcl'я (для предоставления значений, а еще ведь парсить имена надо!) не потребуется.
(Был еще один вариант -- заводить Tcl'ный "контекст", и при регистрации сопоставлять именам каналов какие-то целочисленные handle'ы, записывая их в контекст как переменные, а потом уж из формулы адресоваться по ним. Но это тоже криво -- и сохранение в контексте, и вообще эти "handle'ы"...)
30.06.2008: да, после некоторых размышлений очевидно, что надо передавать заинтересовавшие каналы Tcl'ю именно в "параметрах".
Отдельный вопрос: что делать с собственно "параметрами" и "регистрами", которые в cda/Cdr(?/?) -- поскольку запись в них должна отражаться в их значениях немедленно, прямо в той же "формуле". Но те-то вещи уже более локальны, и можно им устраивать "регистрацию-на-лету", да и хоть lookup'ы по именам -- не проблема. Хотя "регистры" -- локальные для группировки переменные, конечно, лучше также объявлять в "заголовке".
05.08.2008: "криво/не-криво", а г-н Теребило в своей работе "Channel Access Client Toolbox for MATLAB" на ICALEPCS-2001 описывает, что они как раз вначале для каждой PV получают handle, и работают через него. И ничего -- не пищат!
05.08.2008: P.S. Вот мы тут все о красивости/кривости баем, а надо ведь не забывать и о ЗАПИСИ в каналы.
От идеи маппировать каналы на переменные вроде уже отказались -- и то хлеб (иначе вылезла бы проблема, что вроде как только что записали, а хрен -- "переменная" пока не изменилась; у любого оптимизатора крыша поедет).
Видимо, сделаем специальную функцию типа "уставить канал". И, видимо, передавать ей также надо именно handle.
И -- !!! -- такие каналы, которые только-для-записи, также должны быть объявлены в заголовке деклараций. Даже, видимо, более того: стоит вообще никак, с точки зрения Tcl/... не различать каналы для-чтения и для-записи -- как и в обычном C, пусть это будет заботой cda (а реально -- вообще сервера).
24.03.2014: неа, не так -- нефиг использовать такие символы, потому что:
А -- использовать надо суффиксы, начинающиеся с '@', за которым идёт ключевое слово (ЧТО указываем) с опциональным значением через ':'. И таких может быть не один; т.е.,
ИМЯ.КАНАЛА{@КЛЮЧ[:ЗНАЧЕНИЕ]}
Например, "Magnet1.Imes@freq:2@delta:0.05" (странная спецификация, да). Выглядит, конечно, не супер-красиво, но зато всё отлично алгоритмизируется.
24.03.2014: несколькими часами позже: насчёт групповых суффиксов: в принципе, можно считать, что если "@..." указывается в каком-то из компонентов defserver'а (т.е., в основном окне/контексте или в под-элементе), то всё начиная с первой '@' оттуда выкусывать (оставляя обычный голый префикс) и сохранять на будущее, добавляя как суффикс к каналам.
Правда, делать это будет очень нетривиально:
Видимо, надо будет выдавать из cda какой-то сервис на эту тему, зависящий и от конкретного реализатора (т.е., из оного будут вызываться "справочные" функции).
16.01.2015: с этой "дельтой" есть некоторая засада: ведь клиент будет указывать её в ЛОГИЧЕСКИХ единицах, а на стороне сервера сравнивать надо в АППАРАТНЫХ единицах. Спрашивается -- КАК?
Мысли в порядке появления:
Но это дюже неудобно: громоздко и потенциально проблемно (всякие race conditions).
надо ввести соглашение, что fresh_age==0 означает "не сравнивать текущий возраст с fresh_age".
При этом:
CXCF_FLAG_DEFUNCT
уставляется по-канально, а в формулах он
OR'ится.
19.08.2014: да, сделано именно так, работает.
13.09.2009: дополнение: добавил функцию
cda_report()
, по образу и подобию
cxlib_report()
-- чтоб можно было выводить сообщения о
нестандартных ситуациях, которые либо некому вернуть по _lasterr(),
либо слишком асинхронные.
Из неудобств -- ради возможности печатать имя программы пришлось
"просверлить дырочку" в cda_api, в виде функции
_cda_progname_of()
.
14.09.2009: дырочку убрал, поскольку и так уже
есть функция -- для dat-плагинов -- cda_argv0_of()
.
А вот выдача функции стала максимально полной -- и имя программы, и номер соединения, и имя сервера.
10.08.2018: в cda_clear_err()
добавлено
errno=0
-- коль уж cda_last_err()
может
пользоваться strerr(errno)
, то и очистку надо настолько же
распространять. (По аналогии с CdrLastErr()
, дополненной
сегодня по вчерашним следам.)
Или уставлять "контекст" dataref'ам?
24.03.2014: всё становится на свои места с новой архитектурой: srvconn'ы ПОДЧИНЕНЫ контексту. И dataref'ы тоже.
cda_dataref_t
) так же, как и в v2
(cda_objhandle_t
) -- чтоб в старших битах был sid, а в
младших -- индекс канала внутри него?
Можно ж сделать как в v2'шном simpleaccess -- иметь
ЕДИНЫЙ, СКВОЗНОЙ массив каналов, а в описателе каждого держать его
sid
.
Потенциальный минус -- сложнее "удаление" сервера, поскольку придётся переться по всему массиву, ищя каналы этого сервера. Но а) действие не такое уж частое; б) можно каналы и в список помещать (как это сейчас делается с таймаутами в новой, tid-based версии cxscheduler'а).
Главный вопрос -- какие бонусы мы от такого подхода получим, и вообще, какие плюсы и минусы у каждого из вариантов.
24.03.2014: уже решено, что делаться будет именно так -- "контекст", ему принадлежат sid'ы и dataref'ы, а адресация dataref'оы СКВОЗНАЯ, безо всяких битов.
19.08.2014: сквозная адресация давно сделано и используется, так что "done".
25.07.2013: сделано. Но возникла некоторая неопределённость: поскольку тут не просто "владелец-создатель ресурса, который и указывает uniq,privptr1", а еще и ПОТОМ могут донавешиваться evproc'ы, то вопрос -- а какие у тех evproc'ев должны быть uniq,privptr1? Варианты:
Сейчас выбран вариант (1), поскольку а) изначально отдельная установка evproc'ев в cda сделана чисто для удобства и красоты, а реально б) пользоваться sid'ом должен бы иметь возможность ТОЛЬКО его владелец (тем более, нефиг драйверам мешать друг дружке).
ЗЫ: cda_do_cleanup()
пока нету.
25.11.2013: и cda_do_cleanup()
сделана. Правда, пока ведь cda_del_srvconn()
пуста.
cda_srvconn_t
-- unsigned? Это бессмысленно и неудобно.
И если менять -- то не забыть добавить проверку на <0 в
CheckSid()
.
Кстати, cda_dataref_t
тоже касается.
14.11.2013: производим глобальное "означивание"
-- убираем unsigned
у всех типов-идентификаторов.
CxDataRef_t
CDA_ERR
cda_ref_is_sensible()
сильно упростилась.
cda_srvconn_t
и CDA_SRVCONN_ERROR
.
cda_dataref_t
переделан на typedef-ссылку на
CxDataRef_t
(да и вообще неясно, откуда он появился --
т.к. используется вперемежку).
Явно надо еще разок прикинуть (по глобальной картинке) и, видимо, просто извести его полностью.
Проверяться ничего толком не проверялось, ибо и нету пока.
09.06.2014: ню-ню, проверка на <0 была в "старой" cda благополучно забыта. В "новой" ncda уже везде есть.
05.02.2015: кстати, убрано CDA_ERR
(вместо него надо использовать CDA_DATAREF_ERROR
), а
CDA_NONE
переименовано в CDA_DATAREF_NONE
.
Работа идёт во временной директории lib/ncda/.
24.03.2014: некоторые детали: там есть 4 типа объектов, реализованных на основе SLOTARRAY:
06.05.2014: удалено за некместностью.
30.07.2014: частично вёрнуто, как int.
17.09.2014: cda_hwcnref_t
.
30.03.2014@поезд: была
мыслишка в cda_add_chan()
передавать не char* baseref, а
- этакий списочек на всю цепочку вложения datatree-уровней. Но это очень НЕудобно, когда spec начинается с ':' - удобнее откусывать конечные компоненты СТРОКИ, поскольку каждый baseref может содержать более 1 компонента.struct _cda_base_t_struct { char *base; struct _cda_base_t_struct *uplink; } cda_base_t;
Кстати, а что, если относительная-ссылка-вверх содержится в СЕРЕДИНЕ spec'а? Т.е., "abc.def.::ghi"? Придётся мочь их обрабатывать - но, естественно, только если ':' идёт сразу после '.'.
31.03.2014 @Снежинск-каземат-11: сделана некоторая "логика" в
cda_add_chan()
-- оно опознаёт ссылки PROTO:: и "откусывает"
нужное количество от base
при наличии цепочки из ':' в
начале spec
.
01.04.2014 @Снежинск-каземат-11: начинаем делать сопутствующие вещи: вариант Cdr на новой cda -- nCdr (реально там затронут только Cdr_treeproc.c), тестовая программа с ними обоими -- npult.
08.04.2014: много работы с
cda_add_chan()
:
kind_of_reference()
.
24.03.2017: был косячок: при определении "можно ли
считать начало имени канала именем хоста" (для
SPEC_INCL_SRV
) было забыто, что в имени хоста могут быть дефисы
'-', а проверялось только на isalnum()
и точки. В
результате ссылка на vepp4-pult6:0 не воспринималась, как
ссылка на другой сервер (приходилось ставить cx::). Добавлено, что
в качестве не-первого символа может быть и '-' (именно НЕ-первого
-- в соответствии с RFC952 и RFC1123, первым может быть только буква или
цифра). Проблема ушла.
19.12.2017: еще косячок: не учитывается, что в имени сервера в качестве ":N" может быть также и ":-N" (прямое указание порта). Аж в ДВУХ местах -- в PROTO::SERVER:N и в просто SERVER:N. Но проблема в том, что прямое указание имён с ":-N" pult'у (даже в варианте с пустым именем сервера -- т.е., просто ":-8014") и так работает! Или сложности будут при указании не в defpfx/base, а в самих именах?
defpfx
:
Если refbase'ы просто конкатенируются и могут легко быть отменены (глобальной ссылкой .abc.def... либо относительной :::...), то defpfx (он же defserver из командной строки) -- более "авторитетен".
refbase
'ами вложенных
элементов.
09.04.2014: пара замечаний:
cda_add_chan()
и cda_getrefvnr()
вообще никак
не учитывает существование ПАРАМЕТРОВ ручек.
cda_add_chan()
был бы полезен параметр "флаги" -- чтоб
указывать CDA_F_IS_W
.
14.04.2014: продолжаем пилить.
cda_getrefvnr()
убран параметр void
*ctx
: он рос из идеи "надо бы иметь контекст", а в нынешних
условиях стал лишним -- ссылка однозначно определяет и контекст.
cda_add_chan()
добавлен параметр
flags
.
cda_add_formula()
. Тип -- r/w --
указывается прямо при регистрации в таком же flags
.
Пока пустой -- отдаёт CDA_NONE.
15.04.2014: продолжаем дальше:
cda_add_varchan()
. Для ссылок,
прямо маппирующихся на регистры.
Семантика должна быть такой:
Также пока пустой.
16.04.2014: сделана, как описано.
in_use
теперь
считается селектором, с возможными значениями REF_TYPE_nnn
(UNS=0).
cda_add_formula()
добавлены
params,num_params
-- для передачи параметров ручки.
cda_getrefvnr()
ДВЕ функции для разных аспектов:
cda_process_ref()
(да-да, "process" -- тут
этот термин уместен): имеет пока что только входные параметры.
И потенциально может оставлять "ссылку" (формульную) в
незавершенном, работающем состоянии -- это для сценариев. При этом
возвращается CDA_PROCESS_BUSY
=+1.
22.07.2019: битым текстом о том, почему
«тут термин "process" уместен»: потому, что ЗДЕСЬ формула
является именно просто "обработчиком входных параметров, выдающим
результат". И "направление" работы формулы задаётся изначально, при её
создании -- флагами CDA_OPT_RD_FLA
/CDA_OPT_WR_FLA
(складывающимися из битиков CDA_OPT_IS_W
etc.). (Это всё же
сильно отличается от EPICS'ных рЕкордов, технически как бы тоже "выполняющих
процессинг", но должных реализовывать чтение и запись в железо.)
cda_get_ref_data()
-- позволяет вычитать
различные вещи (текущие их значения): обычное, сырое, флаги, timestamp.
08.07.2014: переименована в
cda_get_ref_dval()
, поскольку относится только к
double-ref'ам.
11.01.2015: а теперь создана новая
cda_get_ref_data()
-- уже чтение данных произвольного
типа/размера.
Это, конечно, весьма неполный -- даже куцый -- набор: например, нет записи "сырого" значения. Но сие оставим на будущее и сделаем по мере понимания либо возникновению потребности.
05.05.2015: cda_getrefvnr()
удалена
полностью.
16.04.2014: дальше:
CxKnobParam_t
.
cda_add_varparm()
.
cda_add_varchan()
наполнен содержимым, по вчерашнему
проекту.
Для хранения номера регистра используется поле hwr
.
17.04.2014: еще:
Ох и дико/муторно/некрасиво же вся эта логика выглядит...
Замечания:
kind_of_reference()
, чтоб хоть теоретически корректно
поддерживались не-CX-протоколы.
22.04.2014: и еще:
combine_name_parts()
, ...
cda_combine_base_and_spec()
, и
CdrRealizeKnobs()
этим уже пользуется.
Проверено -- задуманная логика работает, теперь можно указывать имена каналов и базы веток, с должной обработкой префиксов.
31.05.2016: только проверки на base==NULL и spec==NULL
отсутствовали, из-за чего pzframeclient SIGSEGV'ился. Добавлены --
=""
.
23.04.2014: для дальнейшей работы с каналами скопирована из старой cda инфраструктура cda_plugmgr и скелеты плагинов cda_d_cx и cda_d_local.
24.04.2014: найдена причина тормозной работы cda_add_chan()'а:
Обнаружилось случайно -- при взгляде на top "а чё эта программа жрёт 73 метра ОЗУ?!".
ForeachRefSlot()
для поиска "не зарегистрирован ли уже такой канал?" перебирала по
миллиону элементов.
И так для каждого из 68 каналов, что в сумме давало время запуска более 4 секунд.
Проблема успешно решена, и теперь в сухом остатке следующие выводы:
06.05.2014: на данный момент сделана уже изрядная часть API cda<->dat_p. И регистрация каналов, и создание серверов (делается косвенно, из каналосоздания), и обновление значений (пока только в направлении dat_p->cda). Реализовывалось оно всё в партнёрстве с cda_d_local.
Из результатов на текущий момент:
cda_process_ref()
-- не поддерживается концепция
"raw_value, raw_value_useful". 06.08.2014: уже
поддерживается.
14.06.2014: переходим целиком на новое -- переименовываем ncda/, nCdr_treeproc.c, npult.c, ncdaclient.c в аналогичные без "n" (старые предварительно удалены).
08.07.2014: в порядке протоколирования прогресса:
cda_dat_p_update_dataset()
был сделан еще в июне,
одновременно с наполнением cda_d_local.c.
cda_fla_p_update_fla_result()
добавлена сейчас, для
возврата результатов отработки формул.
В ней очень упрощённые мозги, рассчитанные исключительно на
double
.
cda_get_ref_dval()
возвращает текущее значение
ref'а, и работает ТОЛЬКО с double
(сделана в интересах
формул).
cda_snd_ref_data()
предназначена для отправки
данных; пока пуста. 11.01.2015: наполнена.
cda_process_ref()
задумана для "общей" обработки
dataref'ов вида double
.
Её взаимоотношения с предыдущей пока не ясны.
11.01.2015: прояснились: она вызывается предыдущей
для REF_TYPE_FLA
и REF_TYPE_REG
.
Везде, где упомянуты double
-- по-хорошему,
надо б обобщить на просто скаляры.
08.07.2014: в продолжение прогресса:
cda_process_ref()
(на которую уже
переведен весь Cdr_treeproc.c).
ExecFormula()
.
30.07.2014: еще прогресс:
cda_dat_p_set_hwr()
(и сохраняемое в ri->hwr
) в качестве внутреннего
идентификатора канала внутри sid'а -- чтоб сервер указывал его плагину
при записи в канал, а не заставлял бы искать по ref'у.
cda_dat_p_set_phys_rds()
для указания
набора пар {r,d}.
snd_data
.
cda_process_ref()
.
В которую также запихнуто прозрачное преобразование из входного double в любое из float,double, int{8,16,32}.
Теперь встаёт старый вопрос -- DATACONV, т.е., что надо ВЕЗДЕ устраивать прозрачную конверсию между int32 и double/single.
31.07.2014: далее (в основном -- DATACONV):
cda_get_ref_dval()
-- добавлена поддержка регистров
(вычитывание идёт не из refinfo, а из указуемого им CxKnobParam'а).
cda_get_ref_dval()
же добавлена конверсия -- из
исходного типа в double. Причём с отдельным учётом USGN-типов.
cda_dat_p_update_dataset()
тоже добавлена
конверсия, причём:
dtypes[]
) в
double
.
double
в единицы dataref'а
(ri->dtype
).
04.09.2014: старый (за-#if-0'енный) код без поддержки конверсии выкинут.
cda_process_ref()
отдельные проверки на USGN
не добавлены. Предполагается, что это преобразование
"однонаправленное" -- ВЕЩЕСТВЕННОЕ->ЦЕЛОЧИСЛЕННОЕ, и тут о тонкостях
знаковости/беззнаковости позаботится формат "в дополнительном коде",
так что потерь либо не произойдёт, либо они неизбежны традиционно для
такого кодирования.
Итого -- пашет!!! Приходящие от v2 значения корректно пересчитываются (проверялось на linthermcan), а при указании dataref'ам INT-dtype'ов еще и округляются.
01.08.2014: далее -- сопутствующее:
cda_dat_p_update_server_cycle()
наполнена
содержимым. Для поддержки концепции "conns_u"...
srvinfo_t.nth
.
06.08.2014: добавлено складирование raw_value -- при nels==1 и srpr==CXDTYPE_REPR_INT. Вариант *INT64 считается rv_useful=0 (как и во всех прочих не-INT случаях).
06.08.2014@вечер-пляж: а может, rv_useful вместо true/false сделать варианты -- 0:no, 1:int, 2:float, чтобы raw могло отображаться и вещественное?
Или вообще dtype отдавать (UNKNOWN в качестве "неа", при задействованном кол-ве каналов !=1 или при не INT или FLOAT) -- тогда совсем любые варианты отобразятся один-в-один...
18.08.2014: да, сделано:
CxAnyVal_t
,
являющийся union'ом из всех возможных типов, включая раздельно
целочисленные знаковые и беззнаковые. 25.08.2016:
переехал в cx.h.
И "сырые значения" теперь этого типа, вместо былого
cx_ival_t
.
raw_dtype
, при неопределённости равная CXDTYPE_UNKNOWN.
cda_dat_p_update_dataset()
сократилось до тривиального
копирования, с единственным условием на nels==1.
ShowCurVals()
содержит тучу
вариантов в зависимости от curv_raw_dtype, в т.ч. оно и TEXT выводит
как \xNN, а UCTEXT -- как U+NNNN (для BMP) или
U+NNNNNNNN (за пределами BMP).
18.08.2014: теперь мыслишка -- а не сделать ли
refinfo_t.valbuf
типа CxAnyVal_t
?
Десятью минутами позже: сделано. Код
cda_get_ref_dval()
изрядно упростился.
22.08.2014:
cda_dat_p_update_dataset()
переведена на двойной цикл:
(И теперь при обнаружении ошибок вместо return делается переход к следующему каналу.)
Смысл -- чтобы при получении пачки данных из evproc'а любого из хатронутых каналов иметь свежие значения их ВСЕХ, а не греть голову, ставя обработку в "вероятно-последний".
17.09.2014: окончательно вёрнуто/унифицировано
понятие "hwr" в виде явного типа cda_hwcnref_t
-- теперь
он используется везде вместо былого int и локальных определений в
каждом cda_d_*.c.
Потенциальные бонусы:
09.06.2014: идея эта приходила в голову очень давно, но контраргумент -- "а как адресоваться, если в v2 всё по номерам, а в v4 по именам?". Ну так и указывать вместо имён прямо номера -- уж m4 обеспечит арифметику (только исходные константы придётся как-то подготовить).
Например, добавлением префикса cxv2_
-- что можно
реализовать даже при помощи #define
, для минимизации
изменений (коие поместить в отдельный файл v2_adapter.h).
Естественно, большие каналы будут никак не задействованы. 06.07.2014: хотя, см. в его разделе за сегодня.
11.06.2014@утро-душ:
насчёт "уж m4 обеспечит арифметику": а зачем? Пусть cda_d_cxv2_new_chan()
понимает имена
каналов вида "ЧИСЛО{.ЧИСЛО}", и суммирует все числа. Тогда на m4 можно
повесить только define'ы. 18.07.2014@клистронка-утро: а вообще кабы не правильнее
будет на этом этапе уже добавить name resolution в v2'шные cxsd и cxlib
-- ну хоть в каком-то виде (резолвить имена устройств в оффсеты -- уже
сейчас можно, но и индивидуальные имена каналов тоже при надобности
слабать несложно -- главное, что в v2'шные cda/Cdr/... это добавлять не
надо).
Замечание: вот cda_d_cxv2.c на пару с
_code
способны обеспечить плавный переход с v2 на v4 --
т.е., быть за "CXv3".
25.06.2014: только не cda_d_cxv2.c, а cda_d_v2cx.c (для уменьшения пересечения имён). Файл начат, но еще не задействован.
Зато реализована сборка v2'шного cxlib'а:
#define
'ов для переименования.
30.06.2014: да, скелет cda_d_v2cx.c сделан.
Дальнейшее обсуждение -- в его разделе.
05.08.2014: он доведён до весьма функционального состояния, и бОльшая часть остального проверена на нём, так что тут уж точно "done".
Благо, там всё текстовое, и документация есть -- http://vepp2k.inp.nsk.su/trac/wiki/CAS.
09.06.2014: только одна тонкость -- там в иерархии имён разделители '/'. Но модуль может сам заменять '.' на '/'.
24.06.2014: скелет сделан. Пока совсем без содержимого ("мозгов").
Дальнейшее обсуждение -- в его разделе.
И отдельный knobplugin с 2 кнопками -- [Пуск] и [Стоп], как в v2'шном Chl_scenario; и чтоб он напрямую обращался к cda.
08.07.2014: да, сделана
cda_stop_formula()
, которая проверяет, что REF_TYPE_FLA, и
если "да", то вызывает свежевведённый метод stop()
(делающий sl_deq_tout()
в случае cda_f_fla).
02.10.2014: сделан MotifKnobs_cda_scenario_knob.c, пользующийся этим интерфейсом -- всё работает.
snd_data
, но нет никакого способа
заказать ЧТЕНИЕ.
Для v2cx это пока не особо нужно, а вот для inserver будет необходимо -- там иначе никак.
Как поступим? Есть 2 варианта:
req_data
-- для inserver'а хватит.
do_io
-- аналогично драйверовым,
и тогда сюда же можно будет в будущем вешать и PZREAD/PZWRITE.
29.09.2014: а может, аналогично v2'шному inserver: обычно -- чтение текущего (get), но отдельно также иметь "запрос" (req)?
А сам cda_d_cx.c чтоб обеспечивал первоначальное
спрашивание -- аналог v2'шного CX_CACHECTL_FROMCACHE
?
02.08.2015: вопрос давно решен: поддержание актуальности значений (т.е., вызов чтения) возложено на реализаторы протоколов. В cda_d_insrv и в cxsd_fe_cx (on behalf of cda_d_cx) запрос чтения делается каждый цикл.
Т.е., проблемы как таковой нету, так что "obsolete".
cda_run_context()
и
cda_hlt_context()
-- это были рудименты от без-контекстной
архитектуры v2, где сии операции теоретически могли иметь смысл
применительно к "серверам" (соединениям).
В v4 покамест в них надобности не видно вовсе (только когда-нибудь для какой-нибудь экзотики вроде "временно приостанавливать часть В/В при узких каналах связи", описанной в bigfile-0001.html за 31-10-2006).
combine_name_parts()
с обработкой последовательностей
"..." в начале имён. Пример можно посмотреть конкретно в
cac208.subsys, ключевые слова WWW.ZZZ и YYY (они там
закомментированы), а строка запуска --
./pult cac208.subsys :0.c208_1
(т.е. прямо в defserver уже указано имя устройства -- т.е., кусок
"пути").
24.03.2017: а это, случаем, никак не похоже на
найденный сегодня косячокк с '-' в
kind_of_reference()
?
Сейчас-то, кстати, той штуки уже нету, оно сохранилось в архиве за тогда (например, w20150429-morning.tar.gz) в 4cx/src/programs/tests/cac208.subsys.
Попробовал -- неа, вроде непохоже: там проблема в НЕубирании '.' из начала имени.
19.12.2017: надо бы подразобраться и найти проблему -- а то несколько мешает, в т.ч. при указании базовых имён стандартным скринам устройств.
Некоторые замечания:
Так вот: а что если ОТДЕЛЬНО считать такие ссылки, при указании их в качестве defpfx, за аналог "localhost:N..."? Т.е., если после двоеточия идут только цифры (с опциональным '-' впереди).
Вопрос только -- ГДЕ это надо считать (учитывать): в cda_core, cda_d_cx, pult?
Часом позже: а вот и нет ни проблемы, ни невозможности -- pult прекрасно сжёвывает как ":2.icd", так и даже ":-8014.icd".
container "" "" grid "noshadow,notitle,nocoltitles,norowtitles" \ 2 content:2 { # 2 base:WWW.ZZZ content:2 { disp "dac_mode" "Mode" selector r:out_mode \ items:"Norm\tTable" container "" "Table..." subwin str3:"DAC table mode control" \ content:1 { # base:...YYY content:1 { container "" "" grid "noshadow,notitle,nocoltitles,norowtitles"\ 3 content:3 { noop noop - - vseparator layinfo:vert=fill container "" "" grid "noshadow,notitle,nocoltitles,norowtitles" \ content:6 {
kind_of_reference()
и его применений
показывает некоторые непонятности с использованием "dcln". Там точно
никаких косяков не кроется?
Естественно, при этом постулируется, что указатель валиден только до следующего входа в cxscheduler'ов main-loop.
Кроме того, понадобится cda-API для добычи присланных сервером {r,d}.
22.10.2015@ICALEPCS-2015, Melbourne, THK3K01: cda/insrv/zero-copy: ведь insrv-драйверы могут просить только УКАЗАТЕЛЬ на данные, который для insrv можно вертать прямо указателем на серверовы буфера, а для не-insrv -- указателем на cda'шный буфер. Вопрос лишь, а саму-то cda как этому научить -- чтоб указатель на серверов буфер "сохранялся" и доходил до "клиента" (плюс, НЕ копировать сами данные).
P.S. Это всё актуально для вещей вроде liu'шного анализа осциллограмм в контроллерах.
03.11.2015: кстати, уже ведь есть
cda_acc_ref_data()
.
16.04.2015@Снежинск-утро-пешком-из-Снежинки-на-9-ю: в продолжение:
Так что надо тогда вводить флаг (cda_add_chan().flags) "CDA_DATAREF_FLAG_PRIVATE", означающий, что НЕ следует искать такой канал в списке уже зарегистрированных, а надо принудительно аллокировать слот (и, соответственно, при поиске игнорировать слоты с уставленным этим флагом).
20.04.2015: да, вот сегодня в драйверы ADC200ME и
ADC812ME вставлено SetChanRDs()
на линии данных с параметрами
{1000000.0,0.0}. В результате при указании не-вещественного dtype у клиента
будет дикое округление, и данные бессмысленны. А с отключением
{r,d}-пересчёта -- было б всё вполне окей.
21.04.2015: делаем:
cda_new_context()
, cda_add_chan()
,
cda_add_formula()
с flags
на options
-- в основном в cda.h, а в cda_core.c оно частично уже
было сделано когда-то раньше.
CDA_DATAREF_OPT_*
, в котором сейчас
есть:
CDA_DATAREF_OPT_PRIVATE
-- 31-й бит,
CDA_DATAREF_OPT_NO_RD_CONV
-- 30-й бит.
Т.е., эти флаги идут "сверху" чтоб никогда не перекрылось с
-- CDA_OPT_*
(формально они о разном, но мало ли что в будущем
-- захочется, да и лишний запас прочности не помешает).
refinfo_t.options
, куда сохраняется
переданный cda_add_chan()
параметр...
CDA_DATAREF_OPT_PRIVATE
поиск
одноимённого канала не производится.
CDA_DATAREF_OPT_PRIVATE
.
CDA_DATAREF_OPT_NO_RD_CONV
и не-конверсия в
cda_dat_p_update_dataset()
-- в паре точек, в прямом
копировании memcpy()
и в конверсии. Второй вариант при этом
получается не самым оптимальным -- т.к. данные всегда прогоняются через
double
, даже есть оба REPR_INT; но на это проще забить
(можно сделать еще одну ветку, но пока глубокого смысла не видно).
cda_process_ref()
-- всё тривиально.
cda_snd_ref_data()
-- НЕТ, поскольку там пока
просто недореализована отправка "произвольных" данных, вместе с конверсией.
refinfo_t.nelems
было переименовано в
max_nelems
, а cda_nelems_of_ref()
в
cda_max_nelems_of_ref()
-- всё для устранения неоднозначности и
полной непутающести с current_nelems
.
Теперь использование этого в клиентах.
Следствие: теперь можно писать даже просто "@:", с пустотой внутри -- отработается корректно.
cvt2ref()
. Сюда, конечно,
немного изврат, но пусть будет, для общности и гибкости: оно позволяет
отдельные ручки делать "приватными" (вопрос -- зачем ;)).
Имевшийся там фрагмент
rw? CDA_OPT_IS_W : CDA_OPT_NONE
убран -- смысл его не вполне ясен, а работать оно и не могло, т.к.
cda_add_chan(.options)
раньше и не использовалось.
26.04.2015: проверяем:
И да, без PRIVATE оно портит жизнь другим экземплярам этого канала.
01.06.2015: кстати, "done".
14.12.2015: а "cda-API для добычи присланных сервером {r,d}" тогда был забыт. Сейчас же занадобился -- для отображения цепочки {r,d} в Chl_knobprops.
Сделана cda_phys_rds_of_ref()
.
Заодно добавлена рассылка уведомления CDA_REF_R_RDSCHG
в
cda_dat_p_set_phys_rds()
(оно просто отсутствовало).
cda_add_chan()
'е поиск "зарегистрирован ли уже такой"
проверял только совпадение по имени. А это весьма некорректно -- ведь
клиент может заказать то же имя с другим dtype (а то и max_nelems), и
неправильно ему давать ссылку на тот же канал с отличающимися
характеристиками -- там даже комментарий был,
/*!!! Check {dtype,max_nelems} compatibility? */
Поэтому дополняем также сравнением полей detype
и
max_nelems
.
26.04.2015: да, проверено, работает -- при различии ссылки отдаются разные.
Тогда в консольных утилитах вместо догадок ("если пришло очередное уведомление -- значит, запись отработалась") можно было бы реагировать на настоящее ПОДТВЕРЖДЕНИЕ записи.
02.05.2015: а вот нифига -- код
CXC_RQWRC
, не вызывает ответа, просто приходит ответный
пакет CXT4_DATA_IO
, БЕЗ param1/param2.
...с другой стороны, в текущей архитектуре есть свой плюс: коль на канал записи придёт событие R_UPDATE, значит какая-то запись реально произошла (именно отработалась, а не просто была отправлена далее).
Посему -- "withdrawn".
27.05.2015: но у технологии "дождаться update, отправить запись, еще раз дождаться update" (используемой в cdaclient) есть своя кривизна: два подряд падения сервера (идеально -- в момент коннекта/резолвинга), и запись считается выполненной!
07.05.2015: сделана,
cda_status_of_ref_sid()
. Не проверена, но там всё на вид
просто.
cda_snd_ref_data()
поддерживать отправку произвольных
данныхх"...
Вчера-позавчера гуляли в голове мысли "придётся портить переданные сверху данные, а то где ж еще мы возьмём буфер достаточно большого размера, чтоб в него влез произвольный объём данных". А сейчас стало очевидно --
refinfo_t
иметь "send buffer", который
растить при надобности, и уж в него складывать данные.
Таким образом, для подавляющего большинства каналов -- которые скалярны -- никакие буфера аллокировать не потребуется, а нужны они будут только для векторных.
02.05.2015: некоторое обсуждение:
cda_snd_ref_data()
:
CDA_PROCESS_FLAG_BUSY
; для реально же
отправленного -- CDA_PROCESS_DONE
.
Для чего cda_dat_p_snd_data_f
должна возвращать не
void, а int (или хотя бы boolean -- ==0:success, !=0:error).
Юзеру-то можно возвращать просто
snd_rqd?CDA_PROCESS_FLAG_BUSY:CDA_PROCESS_DONE
03.05.2015: нифига -- могут быть еще перманентные
ошибки, при которых надо возвращать CDA_PROCESS_ERR/-1.
cda_process_ref()
нужно всё делать в
точности по тем же правилам.
CDA_DAT_P_OPERATING
-- надо вызывать snd() для всех
готовых каналов.
cda_dat_p_set_hwr()
.
И как -- введём еще один флаг -- CDA_DATAREF_OPT_SHY
?
И чтоб он ставился таким потенциально-опасным каналам.
И вот ЭТОТ флаг, видимо, должен быть кумулятивным/"tainting": при
регистрации очередного канала, даже если такой канал уже есть, этот
флаг надо принудительно взводить в ri->options
.
03.05.2015: да, делаем:
cda_dat_p_snd_data_f
теперь возвращает int.
Некоторые замечания:
CDA_PROCESS_*
-- по аналогии с
cda_fla_p_execute_t
; конкретно CDA_PROCESS_DONE
(=0) при успехе и CDA_PROCESS_ERR
(=-1) при проблеме.
Т.е., как бы нужен еще один код ошибки, означающий "не выставлять флаг наличия данных (точнее -- сбросить уже выставленный), а юзеру вообще вернуть ошибку". CDA_PROCESS_PERMANENT_ERR=-2?
WriteToVar()
могут возвращать ошибку (как раз касательно
совместимости типов). Поэтому:
WriteToVar()
!=0 возвращается
CDA_PROCESS_ERR
.
WriteToVar()
теперь возвращают то, что им вернут
реализаторы do_write()
-- там протокол "0:OK, <0:Error"
(do_write==NULL -- возвращается 0 ("ушло в никуда", но ушло "успешно")).
refinfo_t
добавлена толпа полей snd*
,
плюс imm_val2snd
.
StoreData4Snd()
, вызов
отправки -- CallSndData()
.
04.05.2015@из-дома:
CDA_PROCESS_SEVERE_ERR
=-2, должная использоваться только между
cda_core и cda_d_*, а наружу не светиться.
И во всех cda_d_*.c при "серьёзных" проблемах (т.е., ЛОГИЧЕСКИХ (вроде неподходящего dtype), а не обрыве соединения) возвращается этот код.
CDA_DATAREF_OPT_SHY
для маркировки
не-персистентных каналов.
Он "кумулятивный" -- принудительно проставляется (|=
) уже
зарегистрированным каналам, если указан в регистрируемом позже. 23.05.2017: а вот это неправильно! Надо не "принудительно
проставлять", а считать каналы с/без РАЗНЫМИ. Сделано, см. ниже за
сегодня.
Для указания в @-флагах ссылок выбран символ '/'.
05.05.2015: продолжаем:
SendOrStore()
, комбинирующая Store и CallSnd и реализующая
алгоритм "если можно -- то отправим прямо сейчас (по возможности напрямую),
иначе -- складируем в буфер и взведём флаг".
do_conv
-- надо ли выполнять {r,d}-конверсию.
cda_snd_ref_data()
указывает 1.
cda_process_ref()
, делающая конверсию сама при
преобразовании типа, указывает 0.
И если успешно, то возвращается DONE, а если SEVERE_ERR -- то
CDA_PROCESS_ERR
. Иначе -- идёт дальше, на сохранение.
StoreData4Snd()
.
CDA_PROCESS_FLAG_BUSY
.
CallSndData()
А дальше, как по алгоритму: успешно -- DONE; SEVERE_ERR -- возвращает просто ERR (флаг snd_rqd при этом будет сброшен); иначе (в т.ч. при ошибке) -- возвращает BUSY.
Теперь и cda_snd_ref_data()
, и
cda_process_ref()
переведены на неё.
И собственно
StoreData4Snd()
:
imm_val2snd
, то ограничивается им.
snd_rqd
.
CallSndData()
: вызывает отправку, и если результат DONE
либо SEVERE_ERR (т.е., логическая ошибка, и данные в буфере всё равно
никогда отправиться не смогут), то сбрасывает snd_rqd
.
Замечание: отсутствующий метод snd_data()
считается за успех
(CDA_PROCESS_DONE
).
cda_dat_p_set_server_state()
при переходе в
CDA_DAT_P_OPERATING
проходится по всем каналам и вызывает
отправку ждущих.
Тут есть неоптимальность, выражающаяся в том, что проход идёт по ВСЕМ каналам ВСЕХ контекстов/sid'ов. Пока на это забиваем, но если станет проблемой -- то можно держать все ref'ы каждого sid'а в своём списке (перемещая при надобности между серверами и "ничем"), как это сделано в cda_d_cx.c.
Замечание: итератор прерывается (возвратом -1), если после какой-то отправки состояние сервера перестанет быть OPERATING.
cda_dat_p_set_hwr()
при переходе канала в состояние
готовности (и сервер чтоб был OPERATING) при взведённом snd_rqd
тоже дергает CallSndData()
.
06.05.2015: начинаем проверять:
Роем...
cda_dat_p_set_notfound()
.
cda_dat_p_set_resolved()
. Из которого, кстати, дрыгать
CallSndData()
.
is_found
;
Результат -- тоже мрак. Например, при обрыве соединения все каналы скопом становятся чёрными, ибо NOTFOUND.
Вывод: нефиг махинировать, смешивая "найденность" с "готовностью".
Надо иметь ОТДЕЛЬНЫЙ флаг именно ГОТОВНОСТИ, который dat-плагины могли бы выставлять и сбрасывать.
is_ready
.
Вот теперь всё работает как надо.
ConnectProc()
'е), тут
результат отправки (удалось/обломилось) известен сразу, поэтому надо просто
сразу сбрасывать snd_rqd
при обломе отправки.
Так и сделано. Работает.
Не очень красиво, правда -- аж 2 одинаковые
проверки, в разных ветвях; но SendOrStore()
вообще выглядит
монструозновато и напрашивается на украсивливание.
07.05.2015: засим считаем раздел за "done".
Конкретно cdaclient такой проблемы не вызовет -- в нём отправка производится после первого обновления (что автоматом защищает и от записи в каналы, принадлежащие установленному соединению, где сервер еще не приконнектился к удалённому драйверу).
Но вот "иные" программы такое могут сделать, да и из GUI тоже можно устроить.
То ли надо как-то таким "fresh"-каналам (а как их определять?) делать принудительный временный "SHY", то ли -- лучше! -- значения складировать и производить конверсию уже в момент отправки.
...или вообще ВСЕГДА конверсию выполнять в момент отправки?
27.10.2015: да, прямо сегодня этот косяк вылез: программа АстреКсюши, работающая через питонский binding by ЕманоФедя, попыталась выполнить запись сразу после создания канала -- когда он еще не был приконнекчен.
В результате выглядело как запись в raw-value.
13.08.2017: давно пора этот косяк исправить -- и Федя ноет, и вообще это кривизна, да и не так уж это сложно.
Еще с несколько месяцев назад было очевидно, как именно, но почему-то не записано (хотя казалось, что писал...).
Собственно "идея":
refinfo_t.rds_rcvd
. Пара замечаний:
Но, видимо, за "получение данных" надо считать именно ПОЛУЧЕНИЕ, т.е., чтобы НЕ curval. А то если "драйвер еще не готов", то пришлётся не-пойми-какое curval, и нельзя, чтобы считалось, что уже всё окей.
...а тут-то как раз может быть засада: ведь на rw-каналы "текущее" считается за is_update всегда, n'est pas?
Да! Как раз НЕ надо считать за обновление просто приход данных, а надо
именно ТРЕБОВАТЬ от cda_d_*.c обязательного указания
cda_dat_p_set_phys_rds()
по готовности. Конкретно cda_d_insrv
это делает, cda_d_cx тоже (обеспечивается в cxsd_fe_cx по
CXC_RESOLVE()
да и по CXC_SETMON
), а в
cda_d_local/cda_d_dircn и прочие надо будет добавить, с phys_count=0.
Однако, проблемы "драйвер прочухался ПОСЛЕ клиента" это не решит: клиент получит старые {r,d}, еще ДО получения сервером калибровки от драйвера...
SendOrStore()
проверять, что если калибровок еще
нет, то ВСЕГДА делать Store, помечая "конверсия еще не делалась и её НАДО
сделать".
Некоторые детали:
StoreData4Snd()
делается safe_realloc() под
полученные данные. Так вот: надо делать объём по МАКСИМАЛЬНОМУ из
sizeof_cxdtype()'ов переданного и ref'ового, чтобы влезло и исходное
представление, и "сетевое".
А копировать в буфер надо при этом так, чтобы исходные данные были "прижаты" к его концу. Дабы потом, при конверсии, в случае, если usize исходного меньше, чем у "сетевого", то не произошло бы наложения (порчи исходных данных "сетевыми").
...хотя, кажется, даже конверсия всегда делается просто внутри одного типа -- какой передан, такой и будет складирован и потом отправлен по сети.
Так что всё описанное в этом пункте -- мимо кассы, irrelevant (as for now).
snd_rqd_checker()
, CallSndData()
-- тоже надо
понятие "готовность" понимать иначе: "когда появятся калибровки", а не
просто "соединение установлено".
cda_process_ref()
есть "своя"
конверсия, то и её тоже отключать при неполученности калибровок.
Вывод: оный механизм будет полезен, хотя и частично: только для клиентов, подключающихся к уже полностью функционирующему серверу. Ну хоть так..
И чё делать? Как определять РЕАЛЬНОЕ получение уже полных калибровок? Как-то химичить с флагами? Ох-хо-хо...
14.08.2017@утро-дома-кухня-мытьё-посуды: а "полностью правильно" определять момент можно на стороне сервера: это момент перехода в DEVSTATE_OPERATING. Т.е., ровно тот момент, когда cxsd_hw отправит накопленные запросы.
Вывод: безупречно такая система сможет работать лишь в случае переноса {r,d}-конверсии на сторону сервера.
15.08.2017@утро-дома-мытьё-посуды: кстати, ведь наверх это "состояние -- OPERATING ли" отдаётся в битике 15 --
CXRF_OFFLINE
. Вполне можно его заюзать для определения полной
готовности канала.
12.01.2018: кстати, с учётом того, что cdaclient эту
логику реализует сам: очевидно, стоит иметь per-dataref-флаг, её
отключающий. CDA_DATAREF_OPT_NO_WR_WAIT
?
13.01.2018: работа состоит из 2 частей:
Первая часть следит за готовностью -- поддерживает состояние флажка (в т.ч. сбрасывает его по обрыву соединения) и уведомляет вторую часть о приходе в готовность.
Вторая же складирует в буфер (следя за применением/неприменением калибровок) и выполняет отправку по уведомлению о готовности от первой.
14.01.2018@дома: все рассуждения выше вроде игнорируют сценарий, когда клиент подключается МЕЖДУ стартом сервера и запуском драйверов (задающих свои калибровки -- в частности, все ЦАПы). Например:
15.01.2018: анализируем и проверяем.
ProcessDatarefEvent()
"разрешением на запись"
считается именно CDA_REF_R_UPDATE
(а не CURVAL -- тот только
при -dC
).
ServeIORequest()
в ветке по
CXC_PEEK
. И, похоже, там ЕСТЬ проверка на тему "НЕ считать
каналы с timestamp.sec==CX_TIME_SEC_NEVER_READ обновившимися даже если они
rw".
...после обеда: сделан. Хотя сборка сделана в Makefile через одно место.
path[]
, которая в этот момент еще
не заполнена; детали см. в разделе VME за сегодня.), а главное -- оно почему-то
тут же само рвёт соединение. SIGSEGV'ится?
16.01.2018: продолжаем:
_rw_p()
было просто
privrec_t *me
, вместо privrec_t
*me=devptr
.
Единственная тонкость:
Но тут возможна иная (хотя и более труднодостижимая) ситуация: драйвер рестартует ПОСЛЕ того, как клиент отправил запрос на запись
rds_rcvd
взводится просто при получении значения с
is_update
!=0, и никогда не сбрасывается.
Ну и в случае наличествующей опции
CDA_DATAREF_OPT_NO_WR_WAIT
-- тоже просто сразу взводится.
Делаем!
refinfo_t.rds_rcvd
.
CDA_DATAREF_OPT_NO_WR_WAIT
=1<<25.
cda_dat_p_update_dataset()
добавлено определение, что
канал перешел в "годное для отправки состояние" -- при is_update
и rds_rcvd==0
оная выставляется в =1, а если при этом
snd_rqd!=0
, то может быть вызвана отправка с конверсией (но
пока не вызывается, т.к. еще нет чего).
rds_rcvd
добавлены в
точки перед вызовами CallSndData()
: в
snd_rqd_checker()
, cda_dat_p_set_hwr()
и
cda_dat_p_set_ready()
.
А вот SendOrStore()
пока не тронут (это задача второй
части).
17.01.2018: далее:
StoreData4Snd()
складирует именно в формате
ri->dtype
, а не в переданном ему dtype
. И
cda_process_ref()
тоже конвертирует в туда же.
SendOrStore()
, и StoreData4Snd()
при отсутствии
необходимости в RD-конверсии вместо ri->dtype
используют
переданный dtype
; первая прямо вызывает
snd_data()
, а вторая memcpy()
в
sndbuf
.
StoreData4Snd()
есть отдельный
шизоидный баг: размер dsize
она считает по переданному
dtype
и в ri->snd_dtype
пишет его же, а при
RD-конверсии использует ri->dtype
-- т.е.,
sizeof_cxdtype(ri->dtype) >
sizeof_cxdtype(dtype)
будет buffer overflow.
Так что всё работало без сбоев исключительно потому, что все программы (де-факто в основном pult (т.е., Cdr), плюс фединое) какой тип регистрировали, такой и отправляли.
Но раз уже есть snd_rqd
, то не перевести ли его с булевского
0/1 на enum "нет=0,да=1,да_и_конвертировать=2"?
Например, cda_process_ref()
зачем-то САМА делает
RD-конверсию, ради чего и был введён SendOrStore()
'ов параметр
do_conv
(который только process_ref и указывает =0).
Частным случаем этого будет и исправление бага в
StoreData4Snd()
-- там просто надо вместо
ri->snd_dtype
ВСЕГДА использовать переданный
dtype
.
Вопрос только: где это может аукнуться? Где-нибудь произойдёт потеря точности из-за использования int-типов вместо вещественных?
09.02.2018: возвращаемся:
StoreData4Snd()
:
складируется теперь тоже на основании dtype
, а не
ri->dtype
.
Кстати, баг явно был следствием копирования откуда-то из
более раннего места (которое сейчас, похоже, уже не сохранилось). Либо в
момент введения "буферизации" -- полей snd*
и собственно
StoreData4Snd()
03-05-2015.
cda_process_ref()
: там нынешний код состоит из 3
частей:
double
к ri>dtype
.
Анализ:
cda_snd_ref_data()
, с "мясом" в
SendOrStore()
-- это не делается, то можно и его убрать.
После чего вся ветка по REF_TYPE_CHN
сводится к
if ((options & CDA_OPT_READONLY) == 0) ret = SendOrStore(ri, CXDTYPE_DOUBLE, 1, &userval, 1);
cda_set_dcval()
-- пользуется много кто, включая pzframe_gui,
где данные изначально INT32 (из ручек такими приходят) и в сервере тоже
INT32, а мы будем фиг знает зачем слать DOUBLE по сети, тем самым перенося
конверсию с клиента в сервер. Не вылезет ли где чего?
cda_process_ref()
выделенной -- чтоб она всё-таки конвертировала в ri->dtype
.
Причина -- что ей на вход ВСЕГДА поступает значение double
, и
просто нет возможности указать другой тип.
cda_snd_ref_data()
указывать флажком/опцией, конвертировать ли
в "штатный" тип, или оставить как передано.
Одна проблема: НЕТУ у ней параметра "flags".
cda_process_ref()
делать
преобразование типов без предварительной RD-конверсии -- получится дикая
потеря точности!
Например, просят записать число 3.5 (вольт). Так бы оно превратилось в 3500000.0 (микровольт), и потом при преобразовании в целые стало бы просто 3500000. А при предварительном приведении типов -- стало бы 3 и затем 3000000. ...еще лучше с 0.9, которое вместо 900000uV превратится в 0.
cda_snd_ref_data()
надо
оставлять просто вызов SendOrStore()
, но вот ДАЛЬШЕ надо
передавать флаг "выполнять ли преобразование типа при
RD-конверсии".
CXDTYPE_REPR_TEXT
--
этот механизм не использовать; в смысле -- игнорировать
rds_rcvd
и отправлять данные сразу по готовности.
rds_rcvd
при добавлении? --
однозначно нет (а если записывают сначала TEXT, а потом туда же INT?).
Или прямо в момент возможной отправки при коннекте -- в
вызываемом из cda_dat_p_set_server_state()
'а
snd_rqd_checker()
'е проверять, что
НЕ-"требуется_конверсия"? Да, видимо, так -- оно автоматом получится
"как надо", покрывая ВСЕ случаи, не требующие конверсии (включая
CDA_DATAREF_OPT_NO_RD_CONV
).
SendOrStore()
нет проверки на rds_rcvd
?
Ответ: потому (16-01-2018), что "это задача второй части".
16.02.2018: добавлена проверка.
ri->sndflags
, где флаг
DO_RD_CONV
при мимокассности сбрасывается, тем самым отключая
отсроченное преобразование.
13.02.2018: и-и-и...
REF_TYPE_CHN
в
cda_process_ref()
оставляем тем, одним простым вызовом.
SendOrStore()
и её подчинённых.
do_conv
в
snd_flags
(при надобности складируемый в
refinfo_t
), могущий сейчас содержать 2 флага: "DO_RD_CONV" и
"DTYPE_CONV" (этот означает, что после RD-конверсии нужно преобразовать к
ri->dtype
).
DO_RD_CONV
и NTVZ_DTYPE
(NTVZ --
NaTiViZe, "привести к родному"), плюс SNDFL_NONE
=0.
do_conv
заменён на snd_flags
, в
вызовы вместо "1" и "0" вставлены соответствующие константы.
cda_process_ref()
добавлено
|NTVZ_DTYPE
.
snd_flags
для
сохранения на будущее, выполняемого в StoreData4Snd()
.
ri->snd_flags
.
sizeof_cxdtype()
'ах используемых
типов: 1) требуемый размер буфера считать по максимальному; 2) складировать
"в конец".
...неприятность в том, что -- сейчас -- и складирование, и отправка делаются в ДВУХ местах (а точно?).
CallSndData()
. И, похоже, её же и вызывать по "приходу канала
в готовность" -- в cda_dat_p_update_dataset()
при
rds_rcvd=1
.
StoreData4Snd()
одновременно с
memcpy()
.
ri->phys_count == 0
"
--
"&& ri->rds_rcvd
", иначе при просто еще не
полученных калибровках считалось бы, что можно слать сразу.
SendOrStore()
тоже.
CallSndData()
.
В StoreData4Snd()
же оно нафиг не нужно -- там должно быть
просто складирование.
CallSndData()
.
Тогда все мозги-арифметика на тему "по какому оффсету класть в буфер..." будут инкапсулированы в одной функции.
Для чего придётся сделать snd_flags in-out-параметром.
...или просто в ri
всё делать?
ri
'шные sndbuf и imm_val2snd.
15.02.2018: м-м-м...
DoStoreWithConv()
. Состоит из 2 частей:
memcpy()
, а иначе цикл, взятый из
StoreData4Snd()
-- загрузка, RD-конверсия, сохранение.
max_total
до размера, кратного 16 ("(...+ 15)
&~15U
"). Ведь любой современный тип (int16/32/64/128) не просто
укладывается на границу 16, но и в минус от неё (как и в плюс) тоже будет
иметь правильное выравнивание.
Ну или, для наглядности, при количестве 5: конец буфера -- 3*5=15, смещение для исходного -- 15-2*5=5; нечётное -- низзя.
@вечер-дорога-домой-по-Лаврентьева: а вот и нифига!!! Ведь это "прижимать к концу" имеет смысл только в случаях, когда размер "исходных" единиц меньше, чем "конечных" (например, INT32->DOUBLE). А если наоборот (DOUBLE->INT32) -- то напротив, надо, чтобы оба были у начала.
@вечер-дорога-домой-по-Лаврентьева: тоже нифига-нифига! Задом наперёд копировать можно тоже только если размер "исходных" единиц меньше, чем "конечных"; а если наоборот -- то нужно в обычном прямом направлении.
Сходивши по воду в пультовую -- похоже, понял причину:
Штука в том, что вариант 2 отличается от 1 тем, как именно используются dtype'ы. Т.к. нужно при определении места помещения в буфер учитывать не "текущую" пару src_dtype/dst_dtype, а БУДУЩИЙ dst_dtype, от варианта 3.
Рассмотрим подробнее (тем самым выпишем сценарий/алгоритм):
DO_RD_CONV
.
DO_RD_CONV
.
Но это делается прямо в StoreData4Snd()
, в отдельной ветке.
DoStoreWithConv()
в
зависимости от этого выставлять все параметры.
16.02.2018: и-и-и!!!
dtype==ri->dtype
", то флаг сбрасывается.
Это актуально для
cda_process_ref()
: она нативизации требует (постулировано
09-02-2018), но если канал зарегистрирован и так как DOUBLE, то лишних
телодвижений не нужно.
17.02.2018@дома: и еще одно условие, отключающее нативизацию: если "нативный" тип канала -- UNKNOWN.
19.02.2018: впрочем, в CXDTYPE_UNKNOWN-каналы запись
сейчас и невозможна: она явно запрещается специальной проверкой в
cda_snd_ref_data()
. ...но в
cda_process_ref()
-то нет!!!
DO_STORE_JUST_STORE
DO_STORE_STORE_WITH_CONV
DO_STORE_CONV_IN_SNDBUF
snd_flags
убран, т.к. всегда используется
просто ri->sndflags
.
ri->snd_flags
уже потом
складируются те, которые стали актуальны ПОСЛЕ. А при "косвенной" передаче
-- приходится сначала складировать.
snd_flags
нет, т.к. используется
ri->sndflags
.
ri->sndflags
модифицируется (флаги нулятся
после выполнения действий) прямо в DoStoreWithConv()
.
DoStoreWithConv()
дополнен
"мозгами".
GrowBuf()
(по сути,
делающий то же), но лень.
DO_STORE_JUST_STORE
:
src*
и dst*
там
используются не в прямом смысле "откуда и куда копируем", а в смысле
"откуда и куда копироваться будет в завершении, в ветке
DO_STORE_CONV_IN_SNDBUF
". И копируется В src
,
а в качестве источника выступает data
.
Зато это позволило унифицировать кусочки кода с веткой завершения -- и
вычисления {src,dst}_{dtype,usize,total}, и адресуню арифметику (сдвиг
src
в сторону хвоста буфера).
memcpy()
с последующим
return
'ом.
StoreData4Snd()
, а не в DoStoreWithConv()
?
Ведь разница в готовности к конверсии и её надобности, и решение -- ровно тем же условием -- могла бы принять и функция-исполнитель.
Но пока оставим как есть -- ради наглядности.
StoreData4Snd()
проверка на
тему "а всё равно ничего конвертировать не надо/нельзя", по
результатам которой делается просто memcpy()
: не перетащить ли
это условие прямо в исполнительницу, ведь там тоже есть вариант
"копирование реально без какой-либо конверсии, поэтому просто
memcpy()
".
Но тоже в целях простоты и наглядности оставим как есть.
ri->snd_*
.
После обеда:
А достаточно поставить guard'ы перед memcpy()'ями.
if (dsize != 0)
" из
StoreData4Snd()
убрана.
Вроде всё. Теперь проверять.
Проверяем. Методика:
Т.е., клиент БЕЗ буферизации отправит значение сразу и оно НЕ будет масштабировано на коэффициент. С буферизацией же клиент дождётся калибровок и отмасштабирует число как положено.
...естественно, это касается исключительно ПЕРВОГО коннекта с сервером -- при последующих реконнектах калибровки считаются полученными и значения отправляются сразу хоть в новой версии, хоть в старой.
Результаты:
...несколькими минутами позже: зато при следующем реконнекте он запись произвёл! Очевидная догадка: это ж потому, что в момент перехода rds_rcvd=1 сейчас НЕ делается вызов отправки -- это просто было забыто.
14.10.2023@ИЯФ-1П-613,
ожидая диагностики Kei: и так никогда в
cda_dat_p_set_phys_rds()
и не было добавлено!
Более того -- там даже и rds_rcvd=1
не делалось, а делалось
оно ТОЛЬКО в cda_dat_p_update_dataset()
!
Обнаружилось при попытке сделать на основе formula_drv серверную формулу-"трансформатор" (см. devlist-test-formula-transformer.lst):
-w
" ("send writes to cda immediately"), при
котором буферизация записи возлагается на cda.
rds_rcvd
оставалась ==0 -- из-за отсутствия =1
в set_phys_rds() и невызова update_dataset().
Исправлено:
rds_rcvd=1
, т.к. по диаграмме работы cda_d_cx.c (и
CX-протокола?) сначала делается set_phys_rds(), а уже затем set_ready(); но
для иных dat-плагинов может быть иначе.
cda_d_insrv_new_chan()
делается наоборот: СНАЧАЛА set_ready(), и лишь затем set_phys_rds().
После этого проблема решилась.
Решение: в эту точку в cda_dat_p_update_dataset()
добавлен
просто вызов CallSndData()
; но со всеми обычными проверками
готовности (а то мало ли -- dat-плагин зачем-то (сдурел?) вернул значение, а
соединение в реальности еще не готово).
Помогло -- значение теперь записывается должным образом откалиброванное!
21.02.2018: уже сделано -- теперь конвертируется корректно во всех 3 вариантах (только NTVZ, только DO_RD_CONV, NTVZ+DO_RD_CONV). На что ушел весь день, зато стало "и это хорошо!".
21.02.2018: проверяем дальше.
ParamKCB()
, делается
cda_set_dcval()
, сводящийся к process_ref(), а регистрируется
всё по умолчанию как INT32, так что NTVZ-конверсия поиисходить должна.
PerformWrite()
.
-w
-- когда
проходит через DO_STORE_JUST_STORE
.
И на входе в него показывается верное значение -- 3.
DO_STORE_STORE_WITH_CONV
-- всё окей.
DO_STORE_JUST_STORE
делалось ri->snd_dtype=dst_dtype
вместо надлежащего
src_dtype
-- ведь мы ж в "завершаторе" _CONV_IN_SNDBUF
используем именно это поле для определения типа, а данные еще
НЕконвертированные.
Исправлено, и значение стало верным.
SendOrStore()
в "большом условии"
(можно ли слать сразу) взведённость CDA_DATAREF_OPT_NO_RD_CONV
является ДОСТАТОЧНЫМ условием, позволяющим игнорировать snd_flags, включая
_NTVZ_DTYPE.
StoreData4Snd()
тоже.
SendOrStore()
-- через который
проходят ВСЕ запросы, уходящие потом на буферизацию -- вставить отдельную
проверку типа
if ((ri->options & CDA_DATAREF_OPT_NO_RD_CONV) != 0) snd_flags &=~ DO_RD_CONV
DoStoreWithConv()
RD-конверсию при взведённом _NO_RD_CONV"
-- решать, проверяя именно snd_flags
на флажок
DO_RD_CONV
.
...да и остальные "не могущие измениться в будущем" условия -- на пустой список {R,D}, nelems==0 -- тоже.
В результате "в большом условии" для проверки на необходимость конверсии
остался коротенький тест
((snd_flags & (DO_RD_CONV | NTVZ_DTYPE)) == 0)
,
который теперь выглядит наиболее адекватно -- ведь он отражает суть безо
всяких лишних/путающих деталей.
StoreData4Snd()
при этом усохла до совсем мелких
размеров, и стал виден косячок: там не было из начала убрано
реаллокирование-при-надобности sndbuf'а.
Поскольку это реаллокирование (разрастание) нужно для имеющейся ветки "рудиментарного" складывания (когда конверсия не нужна никакая), то внутрь этой ветки и перенесено.
-w
(т.е., запись идёт ПОСЛЕ получения {R,D})
выполняет непрошеную конвертацию по калибровке.
DoStoreWithConv()
RD-конверсия в цикле сделана
условной, ...
SendOrStore()
условии на тему "сбросить ли DO_RD_CONV": в нём
использовалось значение переменной repr
, присваивавшейся позже.
-w
.
22.02.2018: продолжение тестирования.
-w
)).
DO_STORE_JUST_STORE
в случае, если размер "исходных" единиц
меньше, чем "конечных".
@вечер-дома: Проверять надо, очевидно, тоже куроченьем cdaclient'а -- вставить туда фиксированную отправку вектора нужного типа и размера.
Сделано -- ВСЁ: и cdaclient временно покурочен (шлёт вектор INT32), и лишний NTVZ сделан, да еще и печать src и dst в DoStore вставлена.
Да, работает как надо, и, судя по печатаемым значениям указателей, сдвиг делается корректно.
Заодно, кстати, проверена корректность cda'шной/cxsd_hw'шной конверсии типов разного размера: при R=100 при указании в качестве типа канала @b (байт) отсылаемое значение 9 обрезалось до -124 (9*100=900, 900&0xFF=132, знаково это именно -124), а при @+b (UINT8) в канал в сервере попадало именно 132.
25.02.2018@дома: кстати, а ведь по факту этот "сдвиг исходных данных к концу буфера" нафиг не нужен -- по крайней мере, СЕЙЧАС: поскольку NTVZ делается только из process_ref()'а, со скаляром, то вся "конверсия" получается за один шаг, так что в буфере уже ничего не остаётся и портить нечего. Так что можно было и не делать. Другое дело, что в БУДУЩЕМ ситуация может измениться, и нынешняя реализация получилась future-proof.
24.02.2018: вот теперь считаем, что протестировано всё, и можно деплоить на комплексе (вот там если что ещё вылезет ;)).
Причина -- похоже, они не нулятся.
...даже не "похоже", а точно -- в
cda_dat_p_set_notfound()
делается
ri->rflags|=CXCF_FLAG_NOTFOUND, а надо бы вместо "|=" просто "=" (и
в cda_dat_p_set_hwr()
тоже).
18.09.2015: сделано просто "=" -- всё исправилось.
CDA_CTX_R_NEWSRV
. Уведомляет клиента
о появлении в контексте нового nth-sid'а. Сделано в интересах leds -- чтоб
расширить панельку LED'ом нового сервера.
01.12.2015: и еще одно --
CDA_CTX_R_SRVSTAT
: смена "статуса" sid'а. Генерится из
cda_dat_p_set_server_state()
, чтоб cx-starter'у и прочим
leds-клиентам не приходилось периодически поллить обновления, а просто
реагировать на события.
В результате использовать cda в драйверах очень чревато -- сейчас это позволительно лишь из-за отсутствия консольного интерфейса, позволявшего бы рестартовать устройства.
И сделать бы не особо сложно -- всё под это рассчитано, надо просто взять да аккуратно реализовать во всех компонентах и потом проверить.
09.01.2016: потихоньку приступаем к реализации. 26.10.2017: а вот нифига не видно, где именно тогда было как "приступлено" к реализации. Следов в файлах никаких нет. Видимо, чисто намерения.
Основной постулат -- на уровне cda_core.c
освобождение полей должно делаться в соответствующих
Rls*Slot()
.
...конечно, неоптимальность только при вызове Rls() сразу в аллокаторах (при ошибках, чего почти никогда не будет), но всё же.
21.04.2017: вот и аукнулось наконец отсутствие корректного освобождения ("гроханья") ресурсов в cda.
Всё последовательно перепроверил (час ушел!) -- нифига, всё в порядке: пакеты бегают, и даже экранчик cdac20.subsys, натравленный напрямую на linac1:11.icd_ql15, прекрасно функционировал. А вот ist_cdac20-девайс linac1:11.ql15 -- нет.
Вот так: у всех прочих vdev'ных протокол insrv данные успешно доставляет, а у ql15 -- нет.
Сделал -- получилось, источник стал управляться.
-dc
)
встречалось аналогичное ругательство.
Вот эта загадка так и осталась неразрешенной.
StreamReadyForRead()
первых 8x int32 из пакета показала, что
первым словом пакета (которое представляет собой CxV4Header.DataSize
)
иногда прилетает 0xFFFFFFFF.
А это, кстати, как раз значение insrv'шного HWR_VAL_CYCLE
=-1.
Вот карта по номерам:
stat_evproc()
-- показала, что уведомления о смене target'овых
состояний на +1 (OPERATING) получают [82] (мёртвый!!!) и [84], а [83] --
нет. Вот ql15[83] и выглядел болотным.
cda_dat_p_get_server()
возвращал его, т.к.
cid'ы совпадают, ...
InsrvCycleEvproc()
(об этом ниже), но с
самим промахом по дескрипторам всё-таки загадка.
CxsdHwDelCycleEvproc(...,InsrvCycleEvproc,...)
при удалении
соединения. А надо делать в RlsLcnSlot()
.
...оно, конечно, СЕЙЧАС всё равно не используется, ибо
cda_d_insrv_del_srv()
пуста, да и из cda_core метод del_srv()
пока не вызывается, но всё же.
25.04.2017: добавлено.
21.05.2017: в частности по "гроханью": при "отбирании" COM-порта у kshd485@usbpiv485 (см. bigfile-0001.html за 15-05-2017) почему-то еще сыплются ругательства
lyr:usbpiv485[-1]/DEFAULT: piv485_q_foreach: invalid handle 101 (=>0:101)
Что-то недоподчищается?
23.05.2017: поразбирался -- нет, тут ситуация всё же иная; назвать это "недоподчисткой" будет не совсем корректно.
piv485_fd_p()
проблемы "мы были
вызваны по готовности на чтение, но uintr_read()
ничего прочитать не смог":
lp->fd=-1
.
...там в комментариях написано, что надо бы "(schedule?) re-open", но этого НЕ делается.
DECODE_AND_CHECK()
один из
признаков проблемы -- lines[line].fd<0
.
Поэтому при первом же после "молчаливого закрытия" запросе на чтение канала (или регулярной пере-отправке из очереди по истечению таймаута) обнаруживается, что линия "неживая" и генерится сообщение об ошибке.
Т.е., всё сделано в надежде, что при проблеме чтения будет производиться пере-открытие (в v2'шном старом kshd485_drv_common.c ближе к концу его жизни добавленное, но кривовато). А этого не происходит, вот оно и сдуревает.
26.10.2017: вот теперь действительно приступаем к реализации.
GENERIC_SLOTARRAY_DEFINE_GROWING(,Lcn,...
: параметры
FIRST,INC,MAX были заменены со "стандартных" и безликих 0,2,0 на
LCN_MIN_VAL, LCN_ALLOC_INC, LCN_MAX_COUNT
.
DestroyInsrvPrivrec()
, т.к...
cda_d_insrv_del_srv()
проверяется, что если
being_processed
, то лишь выставляется
being_destroyed=1
, а если нет, то реально вызов убиения.
insrv_fd_p()
в "закрывающей скобке" при
уменьшении до being_processed==0
и выставленном
being_destroyed
вызывается деструктор и цикл обрывается
return
'ом.
UnRegisterInsrvSid()
-- парной к RegisterInsrvSid()
.
RlsLcnSlot()
, стоящей РАНЬШЕ,
так что пришлось делать forward declaration.
RegisterDDDSid()
. Но там-то никакого cycle-evproc'а нет.
UnRegisterInsrvHwr()
идёт физически
РАНЬШЕ, чем RlsHwrSlot()
.
Надо будет потом к этой теме еще вернуться.
cda_dat_p_del_chan_f()
в
cdaP.h имел пустой список параметров.
Изменено на (pdt_privptr, hwr).
cda_d_insrv_del_chan()
создан (и в метрику
добавлен), но он очень простой -- проверка валидности hwr, а потом вызов
RlsHwrSlot()
.
RlsHwrSlot()
, как и положено, делает "всю
работу". Правда, реальная работа в данном случае заключается в снятии
evproc'а, что выполняется свежесозданным...
UnRegisterInsrvHwr()
-- парная к
RegisterInsrvHwr()
функция, снимающая evproc.
Маска событий была вынесена в enum GCN_EVMASK
.
И хорошо еще, если просто нет (он проверкой отсеется), а если этот hwr уже успел получить другой канал, зарегистрированный сразу ПОСЛЕ удаления того?
Но проблема в том, что тут нет никакого пакета, в котором присылалось бы число для сравнения.
...единственное -- можно б было изменить протокол:
int
'а.
hi->
.
Но этого утолщения протокола делать тоже совсем не охота.
27.10.2017: Пока что, пожалуй, можно просто забить: по факту, ВСЕ ДАННЫЕ браться будут всё равно от правильного канала, а единственная реальная путаница (точнее, "потеря" информации) -- о факте обновления: оно якобы придёт, а реально нет (т.к. от другого канала). Учитывая, что вряд ли будут драйверы, использующие insrv:: и меняющие список заказанных каналов, то реального ущерба вряд ли предвидится.
27.10.2017: продолжаем:
Rls*Slot()
.
RlsRefSlot()
: добавлен вызов "деструкторов" в зависимости
от типа (CHN/FLA/REG, в последнем случае ничего не делается).
RlsSrvSlot()
: также добавлен вызов "деструктора".
И дополнительная тонкость: в find_or_add_a_server()
сервер
может быть "недосозданным" -- когда SrvSlot для него уже создан, а
pdt_privptr еще не аллокирован.
RlsSrvSlot()
, который вызовет
si->metric->del_srv(sid, si->pdt_privptr)
и случится проблема, вплоть до SIGSEGV'а.
RlsSrvSlot()
есть guard на тему "metric!=NULL, то теперь
прописывание si->metric
перенесено в точку ПОСЛЕ
аллокирования privrec'а.
01.11.2017: ага, только
"si->metric->
..." на "metric->
..."
заменил не везде -- в bzero(si->pdt_privptr,
... осталось
...si->metric->privrecsize
, вот тут оно гарантированно и
падало.
RlsCtxSlot()
: добавлена подчистка контекстовых Ref'ов,
Sid'ов, Varparm'ов (всё Foreach'ами), а также
DestroyVarparmSlotArray()
.
Плюс там было забыто (при введении nth_sid'ов, очевидно)
safe_free(ci->sids_list)
-- оно тоже сделано.
Итак: для внутрисерверных потребностей минимум "гроханья" готов, пора проверять, а потом распространять на прочие cda_d_*.c и cda_f_fla.c.
31.10.2017: что-то крупно сломалось: любая cda-based-программа падает по SIGSEGV (НЕ падает только при указании пустого сервера, когда реально ничего не создаётся).
Вот КАК, интересно? Ведь добавлялась только подчистка ресурсов, которой сейчас не должно вызываться. Или это кто-то из Rls()'ов так глючит?
01.11.2017: падало вовсе не из-за "гроханья", а из-за
дурацкой ошибки при переделке find_or_add_a_server()
на тему
"когда прописывать metric
".
02.11.2017: для проверки дописан драйвер
test_cda_del_drv.c. Первоначальный результат: при
cda_del_context()
SIGSEGV'ится, в cda_core.c, где-то в
DestroyRefCbSlotArray() -- там бредовые данные; в т.ч., шагом выше по
цепочке вызовов RlsRefSlot(ref=-2).
03.11.2017: разбираемся.
RlsCtx_RefIterator()
и
RlsCtx_SrvIterator()
: вместо "ri-refs_list"
делалось "refs_list-ri
" (и аналогично с si
и
srvs_list
). Вот и получался отрицательный ref.
Всё потому, что эта конверсия там делается вручную, вместо определения
функций ri2ref()
и si2sid()
.
Нашёлся косяк, аналогичный предыдущему: в
DestroyInsrv_HwrIterator()
вместо
"hwr=hi-me->hwrs_list
" делалось
"hwr=me->hwrs_list
". Опять из-за "рукопашной конверсии",
вместо hi2hwr()
.
После исправления сервер с тестовым драйвером запустился и на вид нормально заработал (и занимаемая память не растёт)...
MAX_CHANS=100
каналов.
При увеличении до 200, даже 150, и т.д. вниз до 104 -- вылится по SIGABRT с
престраннейшей ошибкой
*** Error in `./sbin/cxsd': free(): invalid next size (normal): 0x0000000002531970 ***
(и далее Backtrace и Memory map).
futex(0x7f60d46dd760, FUTEX_WAIT_PRIVATE, 2, NULL
Зависон где-то в подчинённых malloc()
, вызванного в
strdup()
stroftime_msc()
, вызванного из
ll_vlogline()
, вызванного из onsig (sig=11) -- т.е., это опять
же SIGSEGV (просто память успела испортиться настолько, что наш обработчик
уже не функционален).
Но это уже вполне годная информация для разбирательства.
RegisterInsrvHwr()
при
ре-аллокировании periodics[]
не модифицировалось значение
periodics_allocd
, оставаясь всё время =0.
Резюме:
После тривиального исправления падения исчезли.
04.11.2017: (суббота, праздник, но не удержался) пилим дальше:
hwrinfo_t
, чтобы потом
разрегистрировывать в UnRegisterLocalHwr()
, который надо
сделать (и вызывать!).
05.11.2017: не всё так просто! Т.к.
RegisterLocalHwr()
работает "на уровне var'ов", там нет
никакого hwrinfo_t
(и тем более privrec'а) -- оно определяется
ниже по тексту. Единственное, что приходит в голову -- возвращать этот
n
, чтоб клиент его у себя сохранял,
06.11.2017: можно ж при удалении канала при снятии evproc'а брать его не по id (n), а поиском по параметрам -- lcn+hwr (как это делает тот же серверный API).
08.11.2017: решил выбрать вариант "поиском по параметрам". Основной резон: т.к. id идут с 0, то сразу после аллокирования слота hwrinfo там лежало бы якобы валидное значение, и по RlsHwrSlot() оно б пыталось грохнуть чьё-то чужое; для противодействия пришлось бы в поле прописывать -1 сразу после аллокирования -- ну нафиг...
09.11.2017: ага, аукнулся этот выбор -- к моменту Rls'а
me->lcn
уже успевал стать -1
, целый день
потратил на поиск косяка :).
RlsHwrSlot()
делается в ДВУХ местах
-- DestroyInsrv_HwrIterator()
и в
cda_d_insrv_del_chan()
?
@613-я часом позже: да не, всё в порядке -- первый раз разрегистрация всех hwr'ов оптом при удалении sid'а, а второй -- разрегистрация 1 канала.
05.11.2017@вечер-~21:00-пешком-из-ИЯФа по Лаврентьева: насчёт "как проверять" все прочие dat-плагины:
Тогда простой сменой конфига его можно будет перенацеливать на что угодно.
08.11.2017: далее:
Кстати, valgrind показывает какие-то ошибки -- invalid read и invalid_write -- в ppf4td_pipe.c. Сходу нифига не понятно -- на вид всё OK; но надо бы разобраться.
09.11.2017: ищем причину утечки.
n
в RegisterLocalHwr()
, он с каждой итерацией
увеличивается.
DestroyLocalPrivrec()
СНАЧАЛА делалось
me->lcn=-1
, а потом уж гроханье всех hwr'ов.
10.11.2017: приступаем. Следующим пусть будет cda_d_dircn.c -- он похож на cda_d_local, только проще из-за отсутствия работы с дескрипторами, поэтому можно делать по образу и подобию.
(Так же, как сам cda_d_local похож на cda_d_insrv, только проще -- из-за отсутствия работы с чем-то внешним (cxsd_hw), поэтому тоже делался по образу и подобию.)
cda_d_dircn_del_*()
-- добавлены аналогично предыдущим, просто скопированы.
being_processed
и
being_destroyed
плюс их обработка.
*DircnSrv*
,
lclconn_t
, lclconns_list*
,
dircn_srv_id
превратились в *Lcn*
,
lcninfo_t
, lcns_list*
, lcn
соответственно.
dircn_lcn_t
: что раньше было int
dircn_srv_id, теперь стало dircn_lcn_t
.
client_id
,channel_n
(в
var_cbrec_t
) превратились в
client_sid
,client_hwr
.
И тут сильное отличие от _local: в том используется lcn
, а
тут именно sid
. Почему такая разница:
lcn
для доступа к
var_cbrec_t
-- для fdio_send'енья client_hwr'а;
sid
для добычи privrec'а через
cda_dat_p_privp_of_sid()
-- чтобы прямо в
VarChgCB()
вызвать cda_dat_p_update_dataset()
и
иметь доступ к privrec'у.
И все client_sid'ы теперь cda_srvconn_t
, а не
int
.
13.11.2017: доделал (в пятницу 10-го было в основном только записано, "проект"), тестируется -- вроде всё OK, без утечек.
13.11.2017: насчёт "удаления в процессе использования" -- being_processed и being_destroyed:
Destroy*Privrec()
, но менеджментом самого куска памяти под
privrec занимается cda_core, и уж оно сделает free()
сразу!
Вначале была идея
модифицировать интерфейс cda_dat_p_del_srv_f()
, чтобы он
возвращал int-результат -- можно/нельзя делать free(pdt_privptr), и если
"нельзя", то ответственность за это dat-плагин берёт на себя и сделает когда
станет можно.
Но это крайне некрасивая идея из-за размазывания ответственности (нарушение инкапсуляции), поэтому так делать не будем.
insrv_fd_p()
, или cxlib'а --
ProcessCxlibEvent()
(тот изначально тоже от fdiolib'а), или
вообще от программы, как cda_d_dircn_update_chan()
.
Похоже, придётся делать И ТАМ, И ТАМ. С тем самым
"размазыванием ответственности" и добавлением в
cda_dat_p_del_srv_f()
int
-результата.
15.11.2017: следующим подшаманим cda_d_vcas.c.
Мыслишка: а если проверять, что "сейчас соединения нету" (т.е., НЕ
me->state==CDA_DAT_P_OPERATING
), то и не слать; а в
cda_d_vcas_del_srv()
уставлять во что-нибудь другое?
cda_del_context()
=>RlsCtxSlot()
-- фиг, оно
вначале делает RlsRefSlot()
всем ref'ам контекста, и лишь потом
RlsSrvSlot()
всем серверам (ну оно и логично ;)).
Единственное -- если вводить какой-нибудь cda_dat_p
-метод
для узнавания значения контекстового being_destroyed
(который,
как ни странно, УЖЕ есть! и будет использоваться), а уж плагины бы его могли
запрашивать и использовать.
Хотя конкретный вид вызова не очень ясен: видимо, параметром должен быть ref?
cda_d_vcas_del_chan()
(практически копия local'овской) и
небольшие добавления к cda_d_vcas_del_srv()
(забытое ранее
закрытие iohandle плюс удаление hwr'ов (скопированное с local)).
Ну и отписка тоже сделана.
15.11.2017: а дальше по списку уже cda_d_cx.c.
cda_dat_p_get_server()
параметр "флаги", чтоб мочь указывать
эту невидимость.
16.11.2017: да, в точности как было придумано 22-07-2016 -- ввести флаг "не добавлять в список".
cda_d_cx_del_srv()
-- цикл по своим hwr'ам с их
освобождением (необходимости отписываться нет, т.к. ниже соединение
закрывается). Цикл -- не Foreach'ем, а по списку (frs_hwr и далее по next).
cda_d_cx_del_chan()
-- DelHwrFromSrv()+RlsHwrSlot().
cx_delmon()
!
16.11.2017: допиливаем:
cx_delmon()
, полным копированием
cx_setmon
и с заменой в единственной точке:
CXC_SETMON
на CXC_DELMON
.
cda_d_cx_del_chan()
добавлен.
Нет -- похоже, умудрился запустить необновлённый бинарник сервера с непроапдейченным cda_d_cx.
22.03.2018: а вот и не всё сделано!!!
НЕ реализована cda_f_fla_p_destroy()
-- она просто
пустая.
Т.е., privrec-то её будет освобождён, RlsRefSlot()
'ом, а
содержимое -- в частности, cda_f_fla_privrec_t.buf
-- нет.
Напоминание: а вот ARG_STRING
'овы fla_cmd_t.arg.str
НЕ надо освобождать, т.к. они живут в том же fla->buf
, после
команд (аллокируется одним куском).
Но будущие таблицы для LAPPROX'а -- возможно, понадобится.
ЗЫ: и вообще на cda_f_fla.c дата -- 10-10-2016.
23.03.2018: делаем.
cda_f_fla_p_destroy()
добавлено
освобождение и =NULL.
Так вот, идея: ловить событие "обрыв соединения для данного канала", и по нему "забывать" такие свойства.
Кстати, вводить новый код cda-события незачем -- уже есть
CDA_REF_R_STATCHG
, генеримый по cda_dat_p_defunct_dataset()
(коий и вызывается при обрыве соединения).
01.07.2016: как-то мутновато.
Очевидно, речь про {r,d}? Но они вроде при установлении монитора на канал пришлются сразу, пусть даже и пустые.
Короче -- идея тут записана, если потребность опять вылезет и удастся её сформулировать повнятнее и поаргументированнее, то решение имеется.
А желательно бы -- для ИПП, конкретно для kind=spectr, уметь выполнять просто "вычислительную" формулу, которая бы И принимала параметр (значение в столбце), И возвращала бы значение.
...теоретически что-то в эту сторону должно б быть делабельно через params[num_params], но они вообще нигде никак не используются.
07.10.2016: если подумать, то становится ясно, что есть ТРИ разных свойства, могущих иметь смысл независимо:
У нас же сейчас -- вследствие изначального применения -- жестко зашиты 2 варианта:
Очевидно, что нужно разводить на 3 реально раздельных флажка. Нюансы из-за изменения API:
//cda_process_ref(), cda_add_formula().
Протокол деяний:
CDA_OPT_HAS_PARAM
и
CDA_OPT_RETVAL_RQD
.
CDA_OPT_RD_FLA
=CDA_OPT_RETVAL_RQD,
CDA_OPT_WR_FLA
=CDA_OPT_IS_W|CDA_OPT_HAS_PARAM.
10.10.2016: продолжение деяний:
cda_process_ref()
.
CallAtInitOfKnobs()
процессинг
at_init_ref
'а вызывался с флагом CDA_OPT_IS_W
--
видимо, чтоб могло что-то писаться при старте.
Обсуждение: это вроде как не очень хорошо -- если уж что пишется, то пусть с явным префиксом "allow_w". Но сейчас и запись в "регистры" (varparm) сделана так же, а основной смысл at_init'ов -- именно такая запись (инициализация).
Резюме: оставлено CDA_OPT_WR_FLA
.
CDA_OPT_IS_W
.
Вывод: никакой завязки в драйверах нет, поэтому версию двигать не нужно.
is_wr
удалено, а все проверки
переведены на проверку битов в уже имевшемся exec_options
:
proc_PUTCHAN()
-- IS_W.
proc_QRYVAL()
-- HAS_PARAM.
OP_RET
-- RETVAL_RQD (проверка, можно/нужно ли брать
значение из стека).
SleepExp()
и cda_f_fla_p_execute()
--
RETVAL_RQD (выяснение, надо ли возвращать значение).
01.11.2016: ну сделал тогда -- а что толку? Использовать пока вообще никак и нигде не потребовалось.
Это криво и так быть не должно.
23.05.2017: делаем.
ctx_ref_checker()
добавлено сравнение -- что
options
найденного и модели совпадают.
options
, ранее отсутствовавшее.
Проверено (cdaclient'ом с ключом -Dr), работает:
Идея пришла в голову на прошлой неделе, когда ЧеблоПаша выдал мысль, что вот бы иметь этакий статус-экран, на котором показывается состояние (хорошо/плохо) всех важных компонентов системы (вроде "IOC health monitor"), в т.ч. всякие температуры железок, доступные по SNMP.
Позавчера посмотрел -- да, структура SNMP-имён (OID -- object identifier) иерархическая, причём даже разделение точками '.'.
Пара мыслей (@вчера, идя по пустому ИЯФу мимо 4-го):
kind_of_reference()
,
combine_name_parts()
-- правильно его воспринимать.
В любом случае, способность pult-скринов напрямую получать информацию по SNMP -- это круто! ("прикольно", "забавно", "полезно")
Мы ж можем присылать вместе с прочими свойствами канала?
23.08.2018: изучаем вопрос.
CxV4CpointPropsChunk
, вместе с "dtype" и "nelems".
cx_rslv_info_t
.
refinfo_t
).
26.08.2018: делаем.
cx_rslv_info_t
поля добавлены, их заполнение в
async_CXT4_DATA_IO()
сделано.
refinfo_t
доабвлены поля, с префиксом
hwinfo_
.
В т.ч. -- hwinfo_srv_hwid
, куда можно складировать
именно hwid -- "внутрисерверный идентификатор канала"; то, что в EPICS/CA
называется
"SID" (Server
ID). Это чисто для отладки/диагностики.
reset_hwinfo()
, вызываемая
GetRefSlot()
.
...хотя, строго говоря, для прочих оно и не требуется.
27.08.2018: да, поскольку теперь отдача наверх только для
REF_TYPE_CHN
, то из прочих созданий ячеек убираем.
cda_dat_p_report_rslvstat()
.rslvstat!=FOUND и
cda_dat_p_set_ready()
.ready==0.
Да, как-то не совсем айс, что ДВЕ точки -- попахивает некоторой недопродуманностью. Но уж что есть.
27.08.2018: уже передумано -- теперь будем возвращать
ТОЛЬКО прямо перед cda_dat_p_report_rslvstat()
, коий и сгенерит
событие. Поэтому из cda_dat_p_set_ready()
сброс убран.
cda_dat_p_set_hwinfo()
. Работает ТОЛЬКО для
REF_TYPE_CHN-ячеек.
Предполагается, что это надо делать непосредственно перед вызовом
cda_dat_p_set_ready()
.ready=1, чтобы, когда ... а хрен знает,
почему. Идея была в том, чтобы после получения данных потом, когда клиент
получит еще какое-то уведомление, для него б уже была доступна "hwinfo".
cda_hwinfo_of_ref()
.
Тут также возвращается и hwr, который вообще client-side -- для диагностики, в пару к srv_hwid.
27.08.2018: модификация: разрешено теперь только для
REF_TYPE_CHN
; прочим возвращается -1/EINVAL.
CAR_RSLV_RESULT
, прямо перед вызовом
cda_dat_p_set_ready()
.
Негативный аспект: в случае указания каналов по номерам (server:N.nnn) резолвинг НЕ выполняется и, соответственно, информация НЕ добывается.
В прочих -- d_dircn, d_local, d_v2cx -- делать не имеет смысла.
Хотя в d_vcas -- возможно (там вроде информация об rw есть, в виде rw/ex/ro).
И в d_epics -- точно будет иметь смысл; как, возможно, и в d_tango.
...и совместить с чем-то -- вроде fresh age -- нельзя: cxsd_fe_cx пакет RESOLVE присылает последним, уже после STRS, RDS, FRESH_AGE и QUANT...
Пока в голову приходит только "запоминать, что еще не печаталось, и печатать по первому NEWVAL/CURVAL"...
26.08.2018@вечер-субботы-дорога-домой-около-мыши-и-ИПА: к
вопросу "куда вставить печать" и вообще "по какому событию считать hwinfo
полученной": а просто ОТДАВАТЬ событие
CDA_RSLVSTAT_FOUND
-- которое формально существует, но никем не
генерится (т.к. сделана "оптимизация" -- что в качестве "стал найден"
«просто любой приход данных в канал -- уже сработает как
"найден!"»).
Таким образом, делаем всё более стройным, симметричным и унифицированным.
27.08.2018: двигаемся дальше.
ProcessCxlibEvent()
и
cda_d_insrv.c::cda_d_insrv_new_chan()
вставлены
cda_dat_p_report_rslvstat(,CDA_RSLVSTAT_FOUND)
.
CDA_RSLVSTAT_FOUND
.
Проверяем -- работает!
Только почему-то на все
cpoint-каналы выдаёт тип "d", хотя железно они "i". Надо б поразбираться --
глянуть на векторные и текстовые.
Вечер: нет, нифига -- то была просто ошибка: просто проверялось на программе rfsyn, а у неё ВСЕ каналы задержек -- каналы V, которые по определению вещественные (сравнивал же с "исходными" в rfsyn_eng, которые int32). Отдельно проверил на тестовом devlist'е с каналами типов d,i,b,t -- всё работает корректно.
Засим считаем задачу выполненной, а раздел "done". Но также см. раздел-замечание ниже.
И тут опять получается проблема курицы и яйца: чтобы получить информацию о канале, надо сначала этот канал создать. Но потом, получивши, указывать эту информацию уже поздно!
И что тут можно сделать? Напрашиваются такие варианты:
Делать такое можно хоть прямо сейчас, но как-то это грубо и некрасиво.
К тому же придётся повторять это "грохнуть и пересоздать" при -- возможной при реконфигурации/рестарте сервера -- смене данных.
Типа самое элегантное. Но некий вопрос -- насколько это реализовабельно?
Ведь dtype,nelems передаются dat-плагиновым методам
_new_chan()
.
Но они используются (точнее, сейчас вообще лишь dtype) только для интерпретации получаемых от сервера данных, а не для хранения чего-нить в своих внутренних структурах.
Так что вполне могут быть изменены "на ходу", если добавить такой метод.
Остаётся вопрос о потребностях будущих _d_epics и _d_tango.
Это вообще непонятно как реализовывать в модели cda, сделанной в объектной парадигме, а не в процедурной.
Итого: наиболее разумным выглядит вариант (b) -- возможность динамической смены типа. Только надо будет при такой операции сбрасывать всё запомненное -- sndbuf, (возможно) кванты и прочие dtype-зависимые данные. И тут еще засадка: поскольку пакет RESOLVE отправляется ПОСЛЕ прочих информационных (Strs, RDs, FrAg, и Quant!), то после сброса ничего нового уже не придёт! ...с другой стороны -- а разве кванты не присылаются сразу в аппаратном типе?
28.08.2018: пожалуй, можно сделать. Технология такая:
Но также надо уметь понять, поддерживается ли сие действо. А, поскольку у тех, кто это допускает, в основном метод будет ничего не делать, то просто указывать NULL в качестве "неа" нельзя.
cda_dat_p_rec_t
поле "опции",
где б можно было указывать "да, поддерживается!".
Делаем.
CDA_DAT_P_FLAG_CHAN_TYPE_CHANGE_SUPPORTED
.
flags
добавлено в cda_dat_p_rec_t
сразу после privrecsize
.
cda_fla_p_rec_t
.
set_type()
и вставлен после
del_chan().
Он возвращает int
, чтоб мочь сигнализировать о
проблеме/невозможности.
cda_d_vcas_set_type()
, и она
тривиальна.
cda_set_type()
snd_rqd
=0, а
также устанавливает текущее значение в "нету".
Как выкручиваться?
set_type()
на "2-стадийный"
(передавая ему параметр "стадия"): на 1-й стадии только проверять, а на 2-й
уже реально переключать. Чтобы последовательность:
1. set_type(проверка); 2. переключение в cda_core;
3. set_type(переключение).
...хотя всё равно это не атомарно -- вдруг set_type()'ово переключение обломится?
cda_set_type()
атомарно-безопасной: ну буфер аллокировать, если
надо, и всё (это нерадостно, но несмертельно). А прописывание новых свойств
-- уже после успешного set_type().
Больше нравится, конечно, вариант (b), но надо б еще помозговать.
Насколько такое изменение -- сделанное одной точкой в рамках контекста-юзера -- будет корректно в отношении прочих?
29.08.2018@утро-дорога-на-работу-около-ИПА: еще вопрос: ведь
текущее значение мы в cda_set_type()
сбрасываем, а не будет ли
такого, что оно так больше и не придёт (например, на каналы записи, которые
не обновляются)?
(уже на работе): неа, не должно. Т.к. сначала генерится CDA_RSLVSTAT_FOUND
(в
реакции на который и может вызваться смена типа), а уже потом идёт запрос
чтения текущего значения (в _d_cx; а _d_insrv сразу возвращает текущее, но
тоже дальше).
11.10.2022: битым текстом: проверил
сейчас ещё раз, уже с обновлённым протоколом, "с OPEN" -- утверждение
осталось в силе, текущее значение возвращается в последнюю очередь, уже
ПОСЛЕ свойств (см. cxsd_fe_cx.c::ServeIORequest()
,
табличка OPEN_reply_seq[]
).
31.01.2019: битым текстом: судя по коду cda_set_type()
,
ре-аллокирование буферов пока не сделано. 25.05.2023: сделано.
24.05.2023: и ещё битым текстом: а ведь и прописывание
переданных dtype
и max_nelems
в
"ri->
" тоже не сделано! Так что не работает СОВСЕМ (вылезло
на тестировании epics2cda).
Делаем: сначала вызов dat-plugin'ова метода set_type()
,
затем прописывание значений, в т.ч. и их current_
-вариантов.
После этого запись в epics2cda начала работать и с "каналами заранее неопределённого типа".
Кстати, заодно сделал в cda_add_varchan()
прописывание
current_dtype
, которое было там забыто (в отличие от
current_usize
).
24.05.2023@вечер-засыпая:
ещё изначально была неопределённость в вопросе о том, как организовать смену
типа, с учётом, что она состоит из нескольких стадий, каждая из которых
может обломиться -- реаллокирование буфера, вызов dat-plugin'ова метода
set_type()
-- и любой из этих обломов должен привести к
возврату в предыдущее состояние (а КАК, если часть действий уже сделана?).
Идея такова: раз всё равно первым идёт реаллокирование буфера,
то его откатывать не нужно в любом случае -- если set_type()
и
вернёт "отказ", то просто останется буфер больше, чем реально необходимо.
Т.е., пользоваться принципом "буфер только растёт и никогда не уменьшается".
Тогда всё получается максимально просто и линейно, безо всяких сложных условий.
Но проблема в том, что в текущей архитектуре размер буфера в явном виде
отсутствует, а присутствует НЕЯВНО, вычисляясь как
csize = usize * max_nelems
-- из ТЕКУЩИХ параметров. А это
позволит знать лишь "предыдущий активный" размер, но НЕ реально
аллокированный.
Ну что -- добавлять current_val_allocd
?
25.05.2023: делаем.
current_val_allocd
введено.
cda_add_chan()
оно
заполняется.
cda_set_type()
же:
valbuf
(т.е.,
НЕОБХОДИМ отдельный буфер) и при этом требуемый объём превышает текущее
значение current_val_allocd
(т.е., надо увеличить) то...
new_current_val=safe_realloc(...)
, при
ошибке которого возвращается -1
, а при удаче -- "новая
реальность" сохраняется в ri->
.
set_type()
есть и он возвращает
<0
, то обламываемся (и да -- если буфер к этому моменту
увеличился, то он таким и останется; ну и фиг с ним).
Проверять будем на epics2cda.
26.05.2023: на вид -- вроде да, работает. Проверялось путём запуска сервера с изменением размера текстового канала: 100,200,100 -- и функционировало предсказанно: при увеличении размера -- растёт, при уменьшении -- остаётся как есть.
09.09.2009: сделана базовая инфраструктура -- в
первую очередь менеджмент массива соединений, плюс уже доделаны
cda_new_srvconn()
и
cda_add_srvconn_evproc()
+cda_del_srvconn_evproc()
.
Теперь дело за плагином.
14.09.2009: и плагин уже более-менее готов, теперь дело за frontend'ом в сервере.
Пара замечаний по результатам реализации:
cda_argv0_of()
т
cda_starg_of()
. Резон -- поскольку эти данные будут
требоваться не только при начальном соединении, но и при reconnect'ах;
и пришлось бы организовывать их хранение в плагинах; а зачем -- раз всё
и так уже хранится.
cda_dat_p_setstate()
.
13.08.2012: внедрён SLOTARRAY-GROWING вместо
былого собственного GetSrvconnSlot()
.
22.10.2012@Снежинск-каземат-11: и Cblist переведён на
GENERIC_SLOTARRAY_DEFINE_GROWING()
. Заодно поле
_used
устранено (как более не использующееся).
31.08.2014@дома: да, переименован в cda_core.c.
Битым текстом:
cda_dat_p_new_chan_f
добавлены
параметры dtype
и nelems
.
Формально они скорее всего не особо будут востребованы, но для полноты/симметрии интерфейса их присутствие крайне желательно.
06.08.2014@вечер-пляж: а вот как раз для cda_d_vcas'а они будут востребованы: ведь сам протокол VCAS вообще никак не типизирует данные, там тупо строки.
cda_dtype_of_ref()
,
cda_nelems_of_ref()
,
cda_current_nelems_of_ref()
. В основном в интересах
cdaclient.
cda_status_*()
-- с введённым на
прошлой неделе NthSid/sids_list оно становится straightforward.
05.08.2014: да, и они уже сделаны. Несколько замечаний:
cda_status_srvs_count()
-- именно КОЛИЧЕСТВО sid'ов, а не
номер последнего. Причина -- иная модель, больше нет выделенного
"main" sid'а (его роль исполняет контекст).
CDA_DAT_P_NOTREADY
) и NORMAL (при
CDA_DAT_P_OPERATING
).
Потому как ни FROZEN cda никак узнать не может, ни ALMOSTREADY даже в самих dat-плагинах пока не фигурирует.
...и CDA_DAT_P-статусов-то таких нет -- надо будет вводить.
cda_strserverstatus_short()
наличествует;
хотя вряд ли понадобится.
02.12.2015: да, API проверен на cx-starter'е -- всё работает.
CXCF_FLAG_DEFUNCT
(при cur_age>fresh_age, см.
за 09-07-2007) кто будет делать -- Пушкин?
Явное место ей в cda_dat_p_update_dataset
.
19.08.2014: да, сделано.
cda_dat_p_set_fresh_age()
, в МИЛЛИСЕКУНДАХ.
cx_time_t
, а если <=0, то уставляется
ri->fresh_age_specified=0, тем самым отключая определение DEFUNCT.
cda_dat_p_update_dataset()
.
cx_time_t
сделаны
TIMESTAMP_IS_AFTER()
и timestamp_subtract()
,
являющиеся очевидными модификациями аналогичных функций для timeval'ов
(замена имён плюс при конверсии nsec->sec используется фактор
1000000000 (10^9) вместо 1000000 (10^6) у tv).
23.02.2016: TIMESTAMP_IS_AFTER()
опубликован
в cx.h под именем CX_TIME_IS_AFTER()
(сейчас -- для
MotifKnobs_internals.c).
SuccessProc()
этот cycle_size
добывает.
01.09.2014: уже сделано и при обрыве соединения, и при долгом неприходе данных.
(Заодно, кстати, и пингование добавлено.)
cda_dat_p_defunct_dataset()
. Оно
делает rflags|=CXCF_FLAG_DEFUNCT и нулит время прихода.
А потом (на "2-м шаге", как и в update) пачкой вызывает evproc'ы (но НЕ генерит событие CDA_CTX_R_CYCLE -- это делает d_v2cx).
CDA_REF_R_STATCHG
, так что незаинтересованные программы могут и
игнорировать.
Проверено -- работает, и при отвале, и при долгом необновлении.
04.07.2015: дополнительно сделано, что при CX_TIME_SEC_NEVER_READ выставляется rflags|=CXCF_FLAG_DEFUNCT. Чтобы синели никогда-не-прочитанные каналы (даже те, у которых нет "возраста свежести").
02.09.2015: отдельная проблема -- что в нынешней модели протокола (данные присылаются только при реальном обновлении да при установлении соединения) в cda_d_cx не работает "посинение при длительном необновлении канала".
Идея решения: добавить сравнение времён с усиневанием
"rflags|=CXCF_FLAG_DEFUNCT" в cda_process_ref()
, в ветку
"не-CDA_OPT_IS_W
", при условии наличия
CDA_OPT_DO_EXEC
-- именно такая комбинация дёргается из
CdrProcessKnobs()
перед
cda_get_ref_dval()
. "Мозги" взять из
cda_dat_p_update_dataset()
, но добавить оптимизацию "чтоб
пореже проверялось", по возможности.
02.09.2015@пляж: и еще б желательно иметь посинение каналов записи, когда в них что-то записано, но так и не отработано.
В нынешней идеологии оно делабельно так же, как и при
длительном необновлении, тоже в cda_process_ref()
: в
отправке уставлять флажок "отправлено" и сохранять время, а в
update_dataset()
'е флажок сбрасывать. Посиневать по
превышению временем дельты (если у канала она ==0, то 5-10 секунд).
15.09.2015: делаем.
@утро в качестве флажка "отправлено" можно использовать само время -- что sec!=0.
snd_time
(cx_time_t), из двух точек перед вызовом snd_data()
:
SendOrStore()
(при отправке "сразу" без
конверсии)
CallSndData()
(т.е., тоже в момент отправки).
cda_dat_p_update_dataset()
.
cda_process_ref()
добавлена монструозного вида
проверка (4 уровня вложенности if()'ов, с дублированием и утроением
кода внутри). Особой оптимизации нету :)
gettimeofday()
вызывается
для каждого канала заново, хотя достаточно было бы 1 раз на всё.
После обеда: проверено посиневание при записи -- пашет. А по каналам чтения -- надо в CAN-драйверах указывать fresh_age.
16.09.2015: кстати, а чё это с каналами чтения НЕ работает? Ведь cxsd_hw ставит им по умолчанию fresh_age={5,0}!
Разбираемся...
ri->timestamp
вычитался ri->fresh_age
.
После закомментировывания того зануления -- всё работает предсказуемо.
После обеда: с "тем занулением" много отдельной возни, пока неоконченной -- всё в тамошнем разделе.
Проверено также посинение по давно-не-обновляющести -- и оно работает.
Вот теперь можно считать поддержку CXCF_FLAG_DEFUNCT
полностью реализованной и закрывать раздел как "done".
30.08.2014@дома: хоть и
введена еще 19-08-2014 cda_dat_p_set_quant()
, но:
cda_dat_p_update_dataset()
, и сейчас видна необходимость
впихивать это в две точки: в (2) простое копирование (при совпадающих
dtype и без r,d (phys_count==0)) и в (3) конверсию, но как-то некрасиво, а
как именно чтоб красиво -- пока неясно.
31.08.2014@дома: (начато вчера под полночь, доделано сегодня):
DecodeData()
скопированы "те"
мозги определения OTHEROP'а, плюс дополнительный код для обнуления mdctr в
FailureProc()
и добычи кванта в
cda_d_v2cx_new_chan()
, для поддержки чего...
stripped_cda_getphyschan_q_int()
--
поскольку стандартная v2'шная cda_getphyschan_q()
отдаёт уже
double, скорректированный на r.
set_knob_relax()
и
set_knob_otherop()
-- переходники к внутренней
set_attn()
.
CdrProcessKnobs()
добавлена проверка на
CXCF_FLAG_OTHEROP
=>set_knob_otherop().
CXCF_FLAG_ALARM_ALARM
хоть рядышком и
проверяется, но вхолостую -- вызывать пока нечего. Допилим при
вощникновении реальной потребности.
04.06.2016@Сад-Ключи-около-15:37-15:46: идея, куда для общего случая засунуть определение OTHEROP: у нас же есть выделенная точка, где сохраняется curraw. Вот там ЕГО () и надо сравнивать -- с "предыдущим" известным сохранённым. А вовсе не нужно "впихивать это в две точки".
05.06.2016@дома: некоторые уточнения/дополнения:
@вечер: а разве смысл "mdctr"
-- не сравнение времён? Ну так с snd_time
и
сравнивать! (Он как раз в самом-самом конце перед
NEXT_TO_UPDATE:
сбрасывается.)
ri->max_nelems==1
.
curraw
(она еще ДО всех проверок типов), а после
складирования/конверсии данных -- видимо, вообще в самый конец цикла, перед
NEXT_TO_UPDATE:
.
25.08.2016: предварительная подготовка -- введение интерфейсов и протоколов, чтоб затем просто наполнить их функционалом:
Посему -- cxsd_hw'шные коды идут с 0, а дополнительные cda'шные -- с 10.
CDA_REF_R_RSLVSTAT
уходит с 5 на =10 (как давно
запланировано); вместе с PZFRAME_REASON_RSLVSTAT
.
CDA_REF_R_LOCKSTAT
-- за компанию с 6 на =12.
CXSD_HW_CHAN_R_QUANTCHG
=5 и аналогично
CDA_REF_R_QUANTCHG
=5.
CxAnyVal_t
) и всё.
А в некоторых случаях -- в remcxsd_driver_v4.c и remdrv.c -- данных о типе канала вообще нет, и взять их негде.
Т.е., при передаче по сети формат будет как у данных, только с nelems===1 (и без флагов и времён).
refinfo_t.q_dtype
. По умолчанию всё
нули и оно считается за CXDTYPE_UNKNOWN, т.е. неопределённым. Тем самым НЕ
требуется "q_specified".
cda_dat_p_set_quant()
уже была, в неё только генерация
события добавлена и q_dtype
.
cda_quant_of_ref()
введена, она простая.
CxAnyVal_t
переехал из
cx_common_types.h в cx.h -- т.к. нужен и в сервере.
SetChanQuant()
.
cxsd_hw_chan_t
добавлены поля q
и
q_dtype
.
SetChanQuant()
: слизана с
SetChanFreshAge()
с минимальными модификациями.
CXC_QUANT
(='Pqa') и
REMDRV_C_QUANT
='Qant'.
Поскольку switch()'и в
cxlib_client.c::async_CXT4_DATA_IO()
и
remdrv.c::ProcessInData()
без ругательств в
"default:"'ах, то никакого кода делать и не нужно.
...но соответствующие "case"'ы всё же добавлены, для красоты.
30.08.2016@Снежинск-каземат-11: исправляем недоделанность:
сделана пустая заглушка
remcxsd_driver_v4.c::SetChanQuant()
.
Дальше вопрос о протоколе:
С другой -- ведь и dtype надо передавать, а он 1 байт; вот и получится передача ВСЕГДА по 8+8 байт (в ОБОИХ протоколах). А если б "переменно" -- то где-то могло бы экономиться. ...но вряд ли -- dtype-то должен быть первым, а после него выравнивание до границы 8 байт (ибо само значение может требовать такой кратности).
remdrv_data_set_quant_t
CxV4QuantChunk()
В обоих случаях для собственно данных сделано поле
uint8 q_data[8]; // !!! sizeof(XaAnyVal_t)
Т.е., протоколы рассчитаны на работу только с типами до 8 байт размером.
SetChanQuant()
дозаполнена.
Там стоит проверка, что размер не более 8 байт. Для неё используется
CHECK_LOG_CORRECT()
, более нигде в v4 не применяемая.
31.08.2016@Снежинск-каземат-11: далее...
ProcessInData()
-- ветка
REMDRV_C_QUANT
наполнена кодом. В т.ч. проверкой, что размер
по q_dtype
не превышает размера q_data[]
.
MonEvproc()
-- дёрганье SendAReply()
со
свежесделанным chunk-генератором...
PutQuantChunkReply()
-- сделан по образу FrAg'а, но:
CXC_SETMON
и (возможно, излишне)
по CXC_RESOLVE
.
CAR_QUANT
.
_cx_carlist[]
, давно уже некореллировавшая с кодами.
cx_quant_info_t
async_CXT4_DATA_IO()
.
ProcessCxlibEvent()
на
CAR_QUANT
-- вообще самое простое.
Итого: вся цепочка передачи от драйверов до cda сделана (хоть пока не протестирована), теперь осталась собственно реализация "orange intellect" (ну и нашпиговывание драйверов указаниями квантов).
13.09.2016: цепочка-то вся, а вот действия -- НЕ все.
Вопрос в том, что должно быть по умолчанию.
Собственно, действие очень просто -- в CxsdHwSetDb()
добавлено chn_p->q_dtype=chn_p->dtype
.
Таким образом, для вариабельных каналов -- у которых тип UNKNOWN -- будет проставляться q_dtype=CXDTYPE_UNKNOWN, эффективно отключая оранжевение.
И в cdaclient добавлена возможность печати квантов -- для отладки.
Кстати, во всех рассуждениях выше было забыто, что сравнивать надо не только с "предыдущим", но еще и при записи делать уставленное этим "предыдущим".
14.09.2016: код для определения OTHEROP в cda_core.c сделан.
physmodified
(с константами MOFIDIED_NNN
) и isinitialized
.
Для удобства они, вместе с user_raw
(реинкарнация
user_code
) помещены в под-структуру orange
.
physmodified
:
MODIFIED_USER
: при складировании для отправки в
StoreData4Snd()
либо сразу в SendOrStore()
при
"отправке сразу" (в точке ПЕРЕД оной).
Этот делается одновременно со складированием в user_raw
.
MODIFIED_SENT
: в CallSndData()
либо в
SendOrStore()
при "отправке сразу".
MODIFIED_NOT
.
MODIFIED_NOT
.
user_raw
сохраняется число совсем другого типа,
чем получается в curraw
, и сравнивать их попросту никак.
...а мы пытаемся сравнивать. И ведь union же, поэтому сравниваем что-то совсем не то ("мусор"), и OTHEROP зажигается всегда.
Засим бит временно домножаем на 0.
15.09.2016@утро-дома: в порядке размышления над вылезшими вчера косяками:
Т.е., вот если бы кто-то когда-то (через некоторое время?) его б сбрасывал?
Сбрасывать после чтения -- нельзя, т.к. читателей может быть более одного.
Взводить по 1-секундному таймеру на каждый канал -- тоже перебор.
Идей просматривается 2:
На роль этого сигнала подошел бы "начало цикла": к этому моменту как раз произведена обработка всех зависящих от sid'а ручек.
15.09.2016@на-работе-после-обеда: да что за бред?!
Дополнительные сигналы/события какие-то, и неработающесть в других
протоколах... Всё ж проще: сбрасывать надо сразу ПОСЛЕ
рассылки события CDA_CTX_R_CYCLE
! В этот момент как раз
обновления все проделаны.
user_raw
сохраняется без dtype, и молча предполагается, что он совпадает с
q_dtype
и curraw_dtype
(чьё равенство
проверяется), а это не так.
15.09.2016: продолжаем движение:
В нём, кстати, было забыто SetChanRDs()
-- добавлено. Как и
в c0609 и c0612 (и ныне ушедшее на пенсию управление V1000 так и работало --
с полностью скопированными v2'шными калибровками одним-числом).
...и некоторая неясность есть с frolov'ыми -- например, frolov_d16; как минимум RDs там точно забыто. Доделаем при использовании.
abs(user_raw-curraw)>=q
Но в v4, в отличие от v2, НЕ делается при старте модификация "if(q==0)q=1",
а с нулевым квантом даже условие abs(0-0)>=0
всегда будет
истинным.
CDA_CTX_R_CYCLE
":
Исключение -- cx-starter, в котором ВСЕ, от всей установки; но вот для него при надобности можно сделать оптимизацию-исключение -- отключение оранжевения.
16.09.2016: делаем.
Итого -- первая проблема решена. Неясно, как с оптимальностью, но работает.
DATAKNOB_B_IGN_OTHEROP
.
RGSWCH_LINE*()
.
А в v4 -- options-флаг ign_otherop, не используемый нигде.
CONTENT_fparser()
понимание ключика добавлено.
Далее /ign_otherop вставлен в
subsys_magx_macros.m4::RGSWCH()
, и в основной массе
проблема исчезла.
Теперь нужно его подобавлять в прочие индивидуальные места. Что вроде тоже сделано.
12.03.2017: оказалось, что эта недоделанная поддержка OTHEROP обходится очень дорого: cx-starter'а потребляет неприлично большое процессорное время.
cda_set_dcval()
переведена с cda_snd_ref_data()
(пока пустой) на cda_process_ref()
. Смысл -- она используется
в cda_f_fla.c::proc_PUTCHAN()
, и оно раньше просто не
работало.
11.01.2015: итак:
cda_snd_ref_data()
-- запись/отправка.
Для обычных каналов просто сбагривает переданное dat-плагину.
Для REF_TYPE_FLA
и REF_TYPE_REG
конвертирует переданное в double
(если возможно) и
передаёт в cda_process_ref()
с флагами
CDA_OPT_IS_W
и CDA_OPT_DO_EXEC
.
cda_get_ref_data()
-- чтение данных.
Аналогична v2'шной cda_getbigcdata()
-- так же
ограничивает читаемое, и так же возвращает количество скопированных
байт.
Для REF_TYPE_REG
отдельная проверка, для адресации к
регистровому полю value
вместо ref'ова
vurrent_val
.
cda_get_ref_stat()
-- чтение rflags и timestamp.
n_items
у cda_add_chan()
переименован в
max_nelems
.
CXCF_FLAG_NOTFOUND
.
10.04.2015: по пунктам:
cda_add_chan()
при результате
CDA_DAT_P_NOTREADY
проставляет FLAG_NOTFOUND.
Таким образом, изначально канал будет чёрным, а при первом же обновлении данных приобретёт "правильный" вид.
...да, это не вполне красиво, т.к. цвета JUSTCREATED у таких каналов вообще никогда не будет. Но пока лучше ничего не придумано -- как будет, так сделаем. ...Если совсем будет мешать -- можно вообще конкретно ЭТУ логику будет отключить.
cda_dat_p_set_hwr()
: если hwr<0
, то
тоже становится NOTFOUND.
И будущий "cda_dat_p_unlink_chan()" ("drop_sid") -- отвязывание от sid'а, нужное для миграции каналов -- должен будет делать то же самое.
cda_dat_p_set_notfound()
-- чисто для
проставления флага "NOTFOUND". Чтоб dat_p-модули могли по результатам
резолвинга сказать "увы, не нашлось такого канала" (ПОТОМ, не при
регистрации, а при получении ответа от сервера -- включая отсутствие
ответа на UDP-broadcast).
CXCF_FLAG_NOTFOUND
еще и
timestamp уставляется в CX_TIME_SEC_NEVER_READ
.
Засим -- вроде работает (да, проверено на cx и vcas), дальше будем наблюдать и собирать опыт.
24.04.2015: надо б еще как-то делать уведомление клиентов о событии "канал не найден" -- как минимум, чтоб они могли об этом сообщать юзеру (актуально для консольных утилит).
30.04.2015: "как-то" -- очевидно, только присылая специальное событие (CDA_DATAREF).
И совсем уж по-хорошему -- надо б ловить как переход в "не найден", так и в "стал найден" (для бессерверных каналов); но по умолчанию просто любой приход данных в канал -- уже сработает как "найден!".
07.05.2015: сделано уведомление клиентов о
ненайденности. Код события -- CDA_REF_R_RSLVSTAT
; состояние
(найден/ненайден) указывается evproc'у в info_ptr
: 0/NULL -- не
найден, иначе -- найден; т.е., формально может отдаваться также событие
"найден!", но генерить его некому.
В cdaclient и прочие console_cda_util-based добавлена ловля и печать сообщения.
07.06.2018: "дооптимизировался"! :D
Вчера вылезло: если cdaclient'а натравить на UDP-резолвлемый канал, то при убиении сервера (к которому коннектится в результате резолвинга) клиент получает уведомление "канал не найден!", по которому радостно вычёркивает этот канал из списка мониторируемых (а если он был единственным -- то и завершается).
CXCF_FLAG_NOTFOUND
и создания самих утилит.
Делаем:
cda_dataref_evproc_t
УЖЕ есть
info_ptr
, так что всё просто (раньше в нём всегда передавался
NULL, и проверки в клиентах на NULL же).
CDA_RSLVSTAT_NOTFOUND
=0,
CDA_RSLVSTAT_SEARCHING
=1, CDA_RSLVSTAT_FOUND
=2.
cda_dat_p_set_notfound()
в
cda_dat_p_report_rslvstat()
и добавляем параметр
rslvstat
.
CXCF_FLAG_NOTFOUND
производится только при
rslvstat!=CDA_RSLVSTAT_FOUND
.
Замечание: СБРОСА этого флага по FOUND не делается. Да и сам вариант FOUND покамест никем не отдаётся.
CDA_RSLVSTAT_SEARCHING
.
PzframeDataEvproc()
помечает в cur_data[].rflags (исполняет функции Cdr'а?) и рассылает
PZFRAME_REASON_RSLVSTAT
дальше, и он ловится
PzframeGuiEventProc()
'ем, почерняющим ручки.
Вот в этой парочке поставлено условие, что делать свои действия только при !=CDA_RSLVSTAT_FOUND.
P.S. Записывал сюда дольше, чем делал.
P.P.S. Да, проверено -- проблема так исправлена.
cda_fresh_age_of_ref()
; нужен чисто в отладочных целях.
Помимо самого значения, возвращает +1 при указанности и 0 при не-указанности.
12.05.2016: да,
cda_add_server_conn()
.
cda_dat_p_get_server()
(где
оно составляло основное "мясо") в отдельную
find_or_add_a_server()
. Вроде в таком виде по-прежнему
работает.
12.05.2016: далее:
cda_add_server_conn()
.
Сейчас -- по-простому, без использования имеющегося (теоретически) в контексте defpfx. В cx-starter'е оного всё равно не будет -- просто по определению (неоткуда).
Дальше надо уже в cx-starter'е делать (а там это нетривиально -- т.к.
слегка ломает парадигму "либо ds!=NULL, и тогда делаем
CdrRealizeSubsystem()
(в т.ч. и при chaninfo= -- она
просто генерит ds сама), либо не делаем ничего").
options
к cda_dat_p_new_chan_f()
. Смысл --
передавать dat-плагинам свежевведённый флаг
CDA_DATAREF_OPT_ON_UPDATE
, занадобившийся для pzframe_data.
24.05.2016: мини-протокол:
cda_add_chan()
протокольному уровню из опций передаётся
только CDA_DATAREF_OPT_ON_UPDATE
.
cda_d_cx_new_chan()
флажок уже используется.
CDA_DATAREF_OPT_rsrvd26=1<<26
-- если захочется ввести
"OPT_ON_CYCLE" (типа для оверрайденья "@u"?).
В порядке наблюдения: ситуация смахивает на ту, что была в
CreateSimpleKnob()
, когда пришлось ввести options
для передачи принудительного SIMPLEKNOB_OPT_READONLY
.
16.07.2016: собственно, это-то несложно:
CDA_REF_R_CURVAL
(=11, чтобы оставить
=10 для переноса RSLVSTAT'а).
cda_dat_p_update_dataset()
:
update_info
и
curval_info
, чьё содержимое различается значениями полей reason
и evmask.
Пока не проверяем, но не-работать там нечему, так что "done".
02.08.2016@утро-дорога-на-работу-мимо-ИХБФМ: ага, но только проверить-то действительно никак -- т.к. cdaclient не умеет. Надо бы в него ключик добавить -- "печатать CURVAL'ы".
Десятью минутами позже: да, сделал (ключ -Dc) и проверено -- да, теперь она выдаёт текущее значение.
Что оно должно в себя включать:
Замечание: да, такая прямолинейная реализация откладывания драйверами -- просто отдача им права сделать free(pdt_privptr) -- в некоторых случаях аукнется.
cda_dat_p_update*()
и прочие функции API, а...
...сразу после "удаления контекста" клиент регистрирует новый контекст, и
тот получает те же sid'ы? Ведь тогда "старый" начнёт портить жизнь новому
экземпляру.
RlsCtxSlot()
/RlsSrvSlot()
, как это постулировалось
изначально 09-01-2016.
ctxinfo_t
/srvinfo_t
ресурсы, ...
del_srv()
должен производиться
непосредственно из cda_del_context()
, с последующим подсчётом
"сколько осталось недобитых серверов", и если >0, то откладывать
освобождение cid'а до снижения этого числа до 0.
RlsSrvSlot()
надо вызывать сразу после
успешного del_srv()
(либо в API-функции-"завершаторе", но то уж
понятно).
16.11.2017: предварительные действия:
CDA_DAT_P_DEL_SRV_SUCCESS
=0 и
CDA_DAT_P_DEL_SRV_DEFERRED
=1.
cda_dat_p_del_srv_f()
переделана из void
в
int
.
cda_d_*_del_srv()
снабжены
return'ами.
17.11.2017: основная работа:
cda_dat_p_del_server_finish()
(которым плагины смогут сигнализировать об "отпускании" privrec'а).
Начинка его сделана, она не шибко сложна.
ctxinfo_t
поля being_processed
и
being_destroyed
.
srvinfo_t.being_destroyed
уже была раньше (когда
и зачем появилась -- хбз).
being_processed
существует только
на уровне контекста, на уровне же сервера оно бессмысленно.
TryToReleaseContext()
, которую теперь вызывает (при
being_processed==0!) cda_del_context()
и прочие.
"Интеллектуальность" заключается в том, что:
del_srv()
, и если в ответ получает SUCCESS, то делает
RlsSrvSlot()
, а в противном случае зажигает ему
being_destroyed=1
.
RlsCtxSlot()
, а иначе это
откладывается до cda_dat_p_del_server_finish()
.
being_destroyed
в cda_del_context()
выставляется всегда!
cda_d_*_del_srv()
в случае "не сейчас" делается
возврат кода CDA_DAT_P_DEL_SRV_DEFERRED
вместо SUCCESS.
cda_dat_p_del_server_finish()
.
cda_d_vcas и cda_d_cx -- добавить всю архитектуру с "Destroy*Privrec()".
20.11.2017: продолжаем:
ci->being_destroyed=1
и затем вызова TryToReleaseContext()
может продолжаться
какая-то активность со стороны серверов, так что может сложиться ситуация,
когда в ходе "процессинга" значение being_processed
опустится
до ==0, и, при взведённом being_destroyed, опять будет вызвана
TryToReleaseContext()
.
TryToReleaseContext()
пытается что-то делать ТОЛЬКО с не-занятыми серверами.
del_srv()
, а как тот отнесётся к повторному вызову -- вопрос.
being_processed
, и
если он взведён, то просто вернут DEFERRED.
Как это можно сделать:
being_destroyed
бывало не только 0 или 1, а еще
и 2 -- "разрушение реально началось".
cda_del_context()
уже есть.
TryToReleaseContext()
по
унулению being_processed, а при последующих вызовах уже нет.
Но вариант (b) выглядит хуже: и лишний флаг, и условие сложнее -- проверять 3 разных флага, вместо замены второго сравнения на "being_destroyed==DESTROYED_REQUESTED" (REQUESTED=1, а STARTED=2).
После анализа кода: ДА НЕТ ЖЕ, ФИГНЯ ЭТО ВСЁ!!!
srvinfo_t.being_destroyed
, и
достаточно НЕ вызывать повторно del_srv(), а просто считать этот sid за
занятый.
...для чего проще всего в DelSids_Iterator()
вставить
условие
первой альтернативой, ДО попытки вызоваif (si->being_destroyed) r = CDA_DAT_P_DEL_SRV_DEFERRED; else...
del_srv()
.
...но если вдруг всё-таки припрёт, то проект с "DESTROYED_STARTED" вон там выше есть, в пункте (a); не хотелось бы (ибо кривовато), но вдруг что недоучитываю.
ctx_evproc_caller()
и ref_evproc_caller()
.
21.11.2017:
del_srv()
из RlsSrvSlot()
.
Зато вставлен в find_or_add_a_server()
: в точку после
вызова new_srv()
, в случае если оный обломился -- чтобы
подчистить всё; код возврата игнорируется.
del_chan()
в RlsRefSlot()
, а
вместо этого вставлен вызов прямо в cda_del_chan()
.
Заодно стала ненужна do_AccessSrvSlot()
.
RlsCtxSlot()
'а ничего не стал:
Destroy*Privrec()
в cda_d_vcas.c и
cda_d_cx.c.
28.11.2017: долго-долго (неделю?) пытался понять, в
какие же точки cda_d_vcas.c и cda_d_cx.c добавить "скобки"
"being_processed++"
и
"being_processed--; if (being_destroyed) Destroy*Privrec()"
.
Вчитывался в код, анализировал -- ни к чему конкретному толковому не пришёл.
Очевидно, нужно НЕ пытаться лезть в глубину и делать "максимально оптимально", а надо просто взять внешние точки входа в модули -- это callback'и из cxscheduler, fdiolib и cxlib -- и в них окружить "скобками" минимально возможные фрагменты.
Это будут:
ReconnectProc()
,
CycleProc()
и ProcessFdioEvent()
.
ReconnectProc()
и
ProcessCxlibEvent()
.
Замечание: как в этих двух модулях, так и в
троице предыдущих НЕ стоит никаких "скобок" вокруг вызовов
cda_dat_p_*()
в методах СОЗДАНИЯ -- _new_srv()
и
_new_chan()
. Хотя они тоже приводят к вызову cda'шных
callback'ов, могущих, теоретически, вызвать удаление.
Итак -- сделано. Начинаем проверять.
TryToReleaseContext()
пользуется полем
ctxinfo_t.cid
, но это поле никто никогда не заполнял, так что в
нём всегда 0. Оно и пыталось освободить cid=0, коего нет.
cda_new_context()
-- утечки
прекратились.
Получасом позже: а, нет -- нету!
Точнее, они есть при "неправильном" использовании: в devlist'е стоял
префикс просто "cx::" вместо "cx::localhost.0", поэтому создавались
каналы-"сироты", без sid'а, никуда не привязанные и, соответственно, при
удалении контекста не удалявшиеся, т.к. cda_core ни по серверу не может их
"дёрнуть", ни по hwr (т.к. "сиротам" и cda_dat_p_set_hwr()
не
делается).
30.11.2017: вообще добавлен кусочек защиты -- при
не-w_srv
ячейка освобождается и возвращается ошибка (оставим
так до реального внедрения UDP-резолвинга). Утечки прекратились.
Резюме:
29.11.2017: проверяем.
chan_evproc()
добавлено, что когда он получает значение 12345 в канал 102, то делает
cda_del_context()
.
Причём проверка на номер канала сделана хитро: не просто ref==102 (тогда
он пытался бы грохать в еще недорегистрированном состоянии -- получая
начальное значение в момент регистрации), а
"ref==me->refs[101]
".
вместоci->being_processed && ci->being_destroyed
ci->being_processed == 0 && ci->being_destroyed
Как проверить на dircn:: и local -- не очень ясно, да и не особо-то и нужно.
Ну что -- аллилуйя, считаем, что работает?
cda_dat_p_get_server()
параметром
options
, чтоб иметь возможность указывать "не добавлять этот
сервер в список серверов контекста.
16.11.2017: м-м-м...:
CDA_DAT_P_GET_SERVER_OPT_NOLIST
.
options
к
cda_dat_p_get_server()
.
cda_add_server_conn()
такого
параметра НЕ добавлено, там всегда используется
CDA_DAT_P_GET_SERVER_OPT_NONE
=0, т.к. смысл этой функции именно
в добавлении сервера к списку "видимых".
find_or_add_a_server()
, и
если флаг "не добавлять" выставлен, то в sids_list[] и не добавляются.
si->nth<0
и не рассылаются.
28.11.2017@вечер-уходя-с-работы: кста-а-ати, у нас ведь имеется нерешенная (даже не осознанная) проблема, как создавать в cda_d_cx.c resolve-sid'ы -- ровно по одной штуке на контекст: ведь нужно какое-то имя, с которым бы cda_core сравнивало имеющиеся у контекста, а КАКОЕ, чтоб оно точно не перекрылось с именем сервера; 22-07-2016 предлагалось "с неким бредовым именем".
А можно воспользоваться свежевведённым параметром
options
: зарезервировать в нём несколько бит (8? 16?) на "тип"
сервера, и в find_or_add_a_server()
считать совпадающими только
сервера с таким же типом.
Следствия:
options
надо будет сохранять в
srvinfo_t
(для сравнения).
29.11.2017@пешком-с-обеда-дома-около-ИПА: еще следствие:
CDA_DAT_P_GET_SERVER_OPT_NOLIST
(и потенциальные будущие опции) унести в область повыше, а младшие биты
оставить в качестве "типа сервера".
29.11.2017: делаем.
CDA_DAT_P_GET_SERVER_OPT_NOLIST
перенесена с 0-го бита на
31-й.
CDA_DAT_P_GET_SERVER_OPT_SRVTYPE_mask
=0x000000FF -- пока
резервируем 8 бит, в будущем можно расширить до 16.
srvinfo_t.options
и сохранение в него.
srv_checker()
.
Проверить пока не удастся -- только при реализации UDP-резолвинга.
19.12.2017: всё проверено, всё прекрасно.
cda_dat_p_new_srv_f()
еще одним параметром
-- "тип сервера", который ему будет передаваться из того, что указано в
соответствующей части options
.
30.11.2017: реализация:
srvtype
.
options
, что
CDA_DAT_P_GET_SERVER_OPT_SRVTYPE_mask
.
CDA_DAT_P_MODREC_VERSION_MAJOR
традиционно не тронут --
смысла нет (по причине отсутствия динамически-подгружаемых dat-плагинов).
Работает?
Мысль "на вырост": а точно достаточно вот такого простого "тип
сервера"? И не понадобится просто указатель (void*
), для
возможности передавать произвольные параметры?
19.12.2017: да, проверена ж уже работоспособность, "done".
RlsRefSlot()
отсутствовало
safe_free(ri->alc_phys_rds)
. Позор! Добавлено.
И, кстати, с sndbuf
аналогично (совсем ведь недавнее,
блин!).
А очень бы хотелось -- для trig_exec_drv.c, созданного для rfsyn'а (devlist-canhw-19.lst).
07.11.2018: оно работает на "удалённом" железе, используемом на ВЭПП-5, а локально -- фиг: в момент регистрации формулы событие не приходит. Причин/аспектов тут туча (перечислены не в порядке важности, а просто по группам):
_evproc()
'ах используется не переданный им
ref
(который заведомо валиден), а их поле из privrec'а (в
момент ДО возврата из регистрации еще просто не заполненное).
Следствие: даже если б событие пришло, то в этот момент формула-исполнитель еще даже не существует -- выполнять НЕЧЕГО, реагировать на событие НЕЧЕМУ.
Решение: регистрировать сначала формулу-исполнитель.
cda_add_formula()
навешивание evproc'а делается только
в самом конце, когда возможное "событие UPDATE в момент регистрации" уже
случилось ранее.
cda_fla_p_create_t
-создателям также и evmask,evproc,privptr2.
Это, казалось бы, можно б было решать при помощи "отложенного вызова": на время регистрации взводить в формуловом privrec'е флажок "is_creating", и если приходит evproc при is_creating!=0, то вместо реального вызова просто взводить другой флажок, а по окончании (успешного) создания проверять его, и если !=0 -- вызывать.
Хотя тут другие засады:
Это проблема невеликая: mask=1<<reason.
Делать-то это несложно -- цикл сваять (да, порядок при этом нарушится; неприятно).
Но засада: каждый вызов -- может быть "фатальным", после него клиентская программа может захотеть весь объект грохнуть. ...да, грохнуть еще не формулу, чей ref-id программе еще не вёрнут, затруднительно; это как бы отсрочка, но всё равно неприятно.
Впрочем, в момент СОЗДАНИЯ формулы может быть только один-единственный evproc, переданный там параметрами.
Впрочем, ОСНОВНАЯ проблема -- даже не в этом, а в иной идеологии работы с формуловыми evproc'ами, из-за чего всё вышенаписанное просто "мимо кассы".
Очевидно, что для решения в соответсвии с описанными в предыдущем пункте мерами -- придётся сменить модель: чтобы список evproc'ев помнился прямо в объекте-формуле. То ли ею самой, то ли cda_core'ом (этот вариант явно лучше -- там уже вся инфраструктура есть).
cda_f_fla_p_destroy()
НЕ делается
удаление каналов в OP_GETCHAN/OP_PUTCHAN. Что, формулы удалябельны только
вместе с контекстом?
Какие обходные манёвры возможны сейчас:
Резюме: глядя на всю глубину проблем, напрашивается решение покамест пользоваться теми самыми обходными вариантами, а не тратить время на создания "решения общего плана".
cda_add_dchan()
, cda_get_dcval()
и
cda_set_dcval()
-- очень даже пользуются! Конкретно Беркаев (а
возможно, и ЕманоФедя) в своём софте.
И им также будет полезен аналогичный простой интерфейс для int32-каналов.
Какие проблемы -- надо сделать!
11.11.2018@пультовая,
воскресенье, после обеда/вечер: посмотрел -- ох ты ж блин, там не так
всё просто: cda_set_dcval()
вызывает не
cda_snd_ref_data()
, а cda_process_ref()
. Видимо,
чтоб корректно обращаться с формулами и регистрами.
12.11.2018: ну ёлки, да какого лешего!!! Надо разобраться да сделать.
Разбираемся:
Так что проблема реально и не стоит.
cda_snd_ref_data()
тоже умеет
различать каналы (в которые делает реальную отправку) и формулы/регистры
(для которых выполняет приведение переданного в double
и сама
вызывает cda_process_ref()
, так что эффект идентичен).
Делаем:
cda_add_ichan()
,
cda_get_icval()
и cda_set_icval()
.
Последняя использует просто cda_snd_ref_data()
, ...
...а для нужд предпоследней -- ...
cda_get_ref_ival()
, скопированная с
cda_get_ref_dval()
, но с заменой double
на
int
.
Итого: работа оказалась простецкой; теперь только оттестировать.
22.05.2019: за прошедшие полгода никто, конечно, ничего не тестировал.
Но сегодня в cda_get_ref_ival()
в вычитывание из
вещественных было добавлено round()
.
Побудительный мотив -- проявился странный косяк в еманофедином "роботе", управляющим работой Инжекционного комплекса. В некий момент он должен дёргать каналами управления Аккордов кикеров (конкретно четвёрка ic.{eKickers,pKickers}.{preKickU,kickU}.set, отображающаяся на ie_cac208.out0...out3) следующим образом (на примере eKickers.preKickU, он же out0):
Так вот: время от времени пункт 3 не выполняется -- уставка так и остаётся 4.0V. Федя утверждает, что у него всё в порядке и косяков нет. Я, со своей стороны, уверен, что у меня всё корректно и косяков нет (иначе такая проблема давно бы вылезла).
Вывод: надо как-то уметь "трассировать" прохождение данных, чтобы ТОЧНО знать, что клиентская программа запись выполнила/нет.
Только как и где это делать? Я в полной растерянности...
14.11.2018@утро-дома: идея:
ввести в dataref'овы options
флаг "трассировать", и при его
наличии выдавать на stderr диагностику в интересующих местах.
14.11.2018: делаем.
CDA_DATAREF_OPT_DEBUG
.
08.06.2020: блин, а толку-то -- с тех пор уже заведены
CDA_DATAREF_OPT_PRIV_SRV
=1<<22 и
CDA_DATAREF_OPT_EXCLUSIVE
=1>>21, оба во 2-м байте...
CDA_DATAREF_OPT_rsrvd24
.
cda_set_dcval()
. Туда пришлось внести собственный блок
обращения к ri
, чтоб было где проверять флаг.
При его наличии -- простейшая печать имени (из
ri->reference
) и значения (форматом %8.3f).
cda_add_ichan()
& Co.": Саша
Сенченко спросил, а нет ли аналогичного dchan'овскому простого интерфейса
для СТРОКОВЫХ каналов.
Ну нету, конечно; но что мешает изготовить?
09.02.2019@дома: сделаны
прототипы, с названиями, аналогичными simplified-API'шным для double и int:
cda_add_schan()
, cda_get_scval()
,
cda_set_scval()
. Первый (регистрация) даже сделан -- там всё
тривиально.
11.02.2019: сделаны оставшиеся двое.
cda_snd_ref_data()
.
Кстати, при bufsize=0 единственным действием является возврат через
len_p
текущей реально длины строки.
26.02.2019: проверил (сварганив тестик work/tests/test_sc.c) -- работает.
cda_dat_p_update_dataset()
'ова параметра
is_update
, чтобы в вызовах стояли не малопонятные 0 или 1
(сейчас, давно туда не лазивши, я даже не сразу понял, что это за "1"
-- принял за количество возвращаемых каналов).
11.06.2019: сделано --
CDA_DAT_P_IS_CURVAL
=0 и CDA_DAT_P_IS_UPDATE
=1.
Также заюзано в cda_d_dircn.c, cda_d_insrv.c, cda_d_local.c, cda_d_v2cx.c, cda_d_vcas.c.
А вот в cda_d_cx.c -- используется значение, отданное cxlib'ом (в зависимости от кода от сервера -- CURVAL или NEWVAL).
03.12.2019: смысл -- ему нужно иметь квант прямо в физических величинах, а у нас квант в аппаратных ("инженерных"?). Он много ругался, какой я редиска, но квант-то делался в интересах cda, чтоб можно было проверять, совпадает ли реально прочитанное из железки значение с записанным туда (если отличие меньше, чем квант -- то совпадает).
В принципе -- проблем ноль: аналогичная cda_rd_convert()
функция cda_r_convert()
. Сделана копированием и удалением
кусочка " - rdp[1]
".
cda_dat_p_update_dataset()
в ветке
"max_nelems == 1 && nels != 1
" (т.е., при попытке
записать вектор в скалярный канал), делая "goto NEXT_TO_UPDATE
",
пропускает также и обновление rflags.
Формально как бы почти пофиг -- значение ведь реально не обновилось.
Но было как-то немножко неприятно пытаться понять, чего это у OFFLINE-устройства в канале .0 rflags=0. Ну да, устройством был ADC250, а 0-й канал у него векторный, так что надо было cdaclient'у указать префикс "@i2:", но всё равно как-то не вполне приятно.
17.02.2020: а вообще это перекликается с записью от 29-01-2019, насчёт "...чтоб cdaclient'у не обязательно б было указывать тип канала, а он определял бы сам...".
Побудительным мотивом послужил ЕманоФедя, а конкретно то, что он пытался по некоему каналу (условно, 5Мбит/с) гонять соединение с траффиком большим (условно, 7Мбит/с), чем канал мог пропустить, и в результате большеобъёмные векторные каналы напрочь забивают маленькие скалярные -- они работают через общий сокет, так что пока не пройдут данные от векторного, то и скаляр стоит в очереди.
А если б мочь регистрировать векторные каналы в отдельных srvconn'ах, то, возможно, скалярные каналы будут успевать бегать оперативнее.
...ЕманоФеде-то я посоветовал попробовать регистрировать векторные в отдельном контексте -- так разные srvconn'ы форсятся; но лучше иметь возможность просто указать явно.
15.03.2020@дома-воскресенье: всё достаточно несложно и делается
аналогично работе с давно введённым флагом CDA_DATAREF_OPT_PRIVATE
:
CDA_DATAREF_OPT_PRIV_SRV
=1<<=22.
CDA_DAT_P_GET_SERVER_OPT_PRIV_SRV
,
значение его для унификации также =1<<=22.
CDA_DAT_P_GET_SERVER_OPT_NOLIST
было
"= 16 << 16" -- похоже, ещё с его модификации 29-11-2017.
Исправлен на надлежащее "= 1 << 31".
На совместимости это никак не скажется, т.к. используется флажок только в cda_d_cx, а он влинковывается непосредственно в libcda.a.
cda_dat_p_get_server()
если у исходного (вызвавшего
затребование сервера) ref'а флаг CDA_DATAREF_OPT_PRIV_SRV
взведён, то делается
options |= CDA_DAT_P_GET_SERVER_OPT_PRIV_SRV
.
find_or_add_a_server()
попытка найти среди
используемых контекстом соединение с таким же именем "заусловлено" -- что НЕ
взведён флажок PRIV_SRV.
srv_checker()
дополнительно проверяется, что у
соединения-кандидата НЕ взведён этот флаг.
being_destroyed
-соединений: вместо имевшейся странной логики
"если being_destroyed, то вернуть CDA_SRVCONN_ERROR" теперь сама
взведённость being_destroyed
прямо в srv_checker()
считается препятствием.
Так что если уж какое-то соединение поиском вёрнуто -- то оно спокойно отдаётся плагину.
Также и cdaclient (в лице console_cda_util.c) научен:
INTERACTIVE_COMMENTS
(ключ -k
), позволяющая всё же
"заставить считать комментариями".
(bold мой). Т.е., если решётка стоит не сразу после пробела, то символом начала комментария НЕ считается. А в комбинации "@#..." она стоит совсем не первой после пробела.In non-interactive shells, or in interactive shells with the INTERACTIVE_COMMENTS option set, a word beginning with the third character of the histchars parameter ('#' by default) causes that word and all the following characters up to a newline to be ignored.
Сейчас проверил на обычном bash'е, вот таким скриптом:
вывод его --#!/bin/sh echo a b c #d e echo q w e# r
Т.е., да -- так и есть, использование решётки сравнительно безопасно.a b c q w e# r
Проверено на простейшем тесте -- cdaclient с парой каналов, у каждого из которых префикс "@#:" -- да, создаются отдельные соединения.
06.04.2020: соображения на эту тему:
17.04.2020: а лучше сразу более общий "ioctl"?
ledCB()
генерил бы некое событие?).
Вот только пока у cda_leds нет механизма передачи информации к своему нанимателю.
07.05.2020: давно понятно, что нужно готовить
обновление API, с 4 дополнительными полями в cda_dat_p_rec_t
:
srv_ioctl, chan_ioctl, плюс пара в резерв -- чтобы потом не пришлось опять
версию двигать.
А сейчас всё же ПРИДЁТСЯ увеличить
CDA_DAT_P_MODREC_VERSION_MAJOR
до =3, иначе никак: там дальше,
после указателей на методы, идут next и ref_count.
@утро-душ: а СЕЙЧАС можно именно ГОТОВИТЬ:
определить все типы для методов и добавить параметров в
CDA_DEFINE_DAT_PLUGIN()
(так что и в cda_d_*.c они
появятся); но сам тип-метрику пока не трогать, а поменять потом, летом,
когда всё будет подготовлено.
08.05.2020: предварительные действия:
cda_dat_p_chan_ioctl_f
и
cda_dat_p_srv_ioctl_f
.
CDA_DEFINE_DAT_PLUGIN()
добавлены параметры
chan_ioctl
и srv_ioctl
плюс парочка
rsrvd_1
с rsrvd_2
.
11.05.2020@утро, во время "пробежки"
по квартире 2тыс шагов, ~700: а можно вообще ВСЁ подготовить, но
окружить #if
'ами на тему "CDA_DAT_P_MODREC_VERSION >=
CX_ENCODE_VERSION(3,0)
" (и с возвратом -1/ENOSYS).
А потом, когда всё будет подготовлено и появится время -- сдвинуть версию и проверить.
12.05.2020: после добавки в cda_leds захотелось и тут
продвинуться, вот и сделана "заготовка" cda_reconnect_srv()
.
nth
,
по той же нумерации, что и status-функциям.
Указание nth<0
означает "применить ко всем sid'ам этого
контекста".
#if
-проверка по версии.
srv_ioctl()
.
???
".
13.05.2020: код выделен, так что теперь уже он.
Как бы то ни было, уже можно заниматься реклизацией метода
srv_ioctl()
-- в первую очередь организацией принудительного
реконнекта.
13.05.2020: да чё там -- допиливаем несделанные мелочи со стороны cda:
CDA_DAT_P_SRV_IOCTL_NR_RECONNECT
(да, решено
использовать словцо "NR" -- по аналогии с системными вызовами).
cda_reconnect_srv()
опубликован в cda.h.
При помощи отладочной печати проверено, что событие нажатия на лампочку приводит к вызову запроса на реконнект нужного сервера. Так что теперь уже действительно пора переходить к реализации в cd_d_cx.c.
16.05.2020: в cd_d_cx.c вроде сделано.
22.07.2020: возвращаясь к данному вопросу.
next
и
refcount
запихивать в НАЧАЛО modrec, а лучше -- и вовсе прямо в
cx_module_rec_t
.
23.07.2020: в продолжение:
CDA_DAT_P_MODREC_VERSION_MAJOR
до =3, а оставить
=2, но сделать CDA_DAT_P_MODREC_VERSION_MINOR
=1, т.е.,
чтоб версия стала 2.1.
Возможно (надо б проверить) при этом даже и старые модули 2.0 будут грузиться.
Глянул по коду cda_plugmgr.c и
CX_VERSION_IS_COMPATIBLE()
: да, вроде должны бы.
Сделано, по вышеприведённому плану:
CDA_DEFINE_DAT_PLUGIN()
, уже присваивающий значения этим 4
полям.
(Увы, вставить #if в то же определение нельзя -- не поддерживает C/cpp #if'ы внутри макросов.)
srv_ioctl()
вставлена проверка версии модуля --
что она не ниже 2.1.
Компилируется и собирается. Теперь надо проверять загружаемость.
Проверил, попробовав крест-накрест со старой версией, где 2.0 (собрал её в /tmp/) -- да: старая плагины новой не подхватывает, ругаясь на несовместимость, а вот новая с 2.1 плагины от старой 2.0 -- БЕРЁТ!
02.07.2023: вот только РАБОТАТЬ это всё никак не
могло! Обнаружил это только сегодня, при попытке реализации
cda_req_ref_read()
(которое не компилировалось из-за отсутствия
в cda_dat_p_rec_t
поля chan_ioctl
).
Проблема в том, что там повсеместно использовалась условная компиляция с условием вида
...но определения версий при этом были не#if CDA_DAT_P_MODREC_VERSION >= CX_ENCODE_VERSION(2,1)
#define
'ами, а enum
'ом --
-- что НЕ ГОДИТСЯ для препроцессора, т.к. для него символenum { CDA_DAT_P_MODREC_MAGIC = 0xCDA0DA1A, CDA_DAT_P_MODREC_VERSION_MAJOR = 2, CDA_DAT_P_MODREC_VERSION_MINOR = 1, CDA_DAT_P_MODREC_VERSION = CX_ENCODE_VERSION(CDA_DAT_P_MODREC_VERSION_MAJOR, CDA_DAT_P_MODREC_VERSION_MINOR) };
CDA_DAT_P_MODREC_VERSION
не определён, а потому в условиях
считается нулём и условие всегда ложно.
Так что за-#if
'ленный код НИКОГДА не включался в
компиляцию...
Что будем делать?
03.07.2023: по здравому размышлению напрашивается наиболее прямолинейное решение:
#if
'ы убрать, оставив только "новый" код.
CDA_DAT_P_MODREC_VERSION_MINOR
=2, т.е., чтоб
версия стала 2.2.
04.07.2023: сделано. Теперь всё же надо проверять.
Проверяем: да, со стороны cda_core.c теперь всё работает как надо. А вот к cda_d_cx.c есть вопросы -- странно он себя ведёт...
06.07.2023: позавчерашняя странность ещё позавчера же
исправлена, а вчера пофиксен (и ДОфиксен сегодня) сопутствующий косяк в
cxlib_client.c::async_CS_OPEN_CONNECT()
, связанный с
некорректным заказом WipeUnconnectedTimed()
, из-за чего оно
пыталось произвести реконнект САМО через 60 секунд после вроде как
фатального облома.
На этом фичу можно считать окончательно реализованной, так что "done".
cda_dat_p_report()
-- чтобы
GUI-программы могли бы их выводить к себе, дабы юзерам не нужно было следить
за консольным выводом.
12.09.2020: делаем.
cda_dat_p_report_logger_t
.
Ему передаётся и sid
, но, поскольку
cda_srvconn_t
определён только в cdaP.h, то тут
параметр просто int
и делается приведение к нему при вызове.
cda_set_dat_p_report_logger()
, причём она возвращает старый
обработчик.
#include<stdarg.h>
.
cda_dat_p_report_logger_hook
, по умолчанию =NULL.
Проверено пока лишь, что БЕЗ уставки перехватчика всё работает как раньше.
Замечание: перехват делается только для сообщений от
cda_dat_p_report()
, но НЕ для
cda_ref_p_report()
cxlib_report()
хотя для полноты стоило бы и для них сделать.
И, кстати, было бы осмысленно делать такой перехват в сервере -- чтоб редиректить эти сообщения в лог.
12.09.2020@ванна, душ: а ведь если перехватывать такие сообщения в cxsd для выдачи их в серверный (точнее, ДРАЙВЕРНЫЙ) лог, то нужно префиксировать именем/devid драйвера-владельца. Для чего перехватчику сообщения нужно получать uniq.
12.09.2020: (в районе обеда) добавляем uniq:
uniq
первым параметром к
cda_dat_p_report_logger_t
.
...по-прежнему не проверено.
12.09.2020: (после обеда) ну, гулять так гулять -- делаем ВСЁ!
cda_set_ref_p_report_logger()
.
Реализация там тоже аналогична, причём добыча uniq даже попроще.
cxlib_set_report_logger()
.
Там с добычей uniq ещё проще, поскольку вызов-то внутренний, и
единственная проверка -- что cp!=NULL
.
vDoDriverLog()
'у, то нет возможности в начало строки вставить
"адресную" информацию (sid, ref, cd); поэтому пришлось её выдавать отдельным
DoDriverLog()
'ом перед тем.
Проверено -- вроде всё работает, все 3.
const
" у refinfo_t
'шного поля
strings_ptrs[]
. Глубокий смысл наличия там этого атрибута
неясен (точнее, отсутствовал), но он вызывал много warning'ов при
компиляции.
После убирания просто исчезли те warning'и, а новых не появилось. Так что считаем действие правильным.
17.09.2020: возможно, и у поля reference
тоже надо убрать "const
".
Чуть позже: ну убрал. Один warning ушёл -- на тему передачи в
safe_free()
, другой добавился -- про присвоение const'а
не-const-полю. OK, так и оставим.
errno
.
20.04.2023: побудительный мотив -- Виталя Балакин спросил "а чё это моя питонская софтина не может писать строку в канал, получает EINVAL, а число пишется?". Просмотрев cda_core.c на тему возможных причин, я сразу предположил, что дело в попытке писать вектор в скалярный канал (так оно и оказалось), но для полной уверенности решил сделать диагностику.
Общая идея такая:
errno=...
на вызов
некоей функции, которая уже проверяет включенность диагностики и выполняет
печать.
Реализация:
$CDA_DEBUG_ERRNO
, взводимая им
флаговая переменная -- _cda_debug_errno
, делается это в
cda_new_context()
, одновременно с взведением
_cda_debug_names
из $CDA_DEBUG_NAMES
и полностью
идентично (скопированно).
errno
с выдачей диагностики --
set_errno()
, ей передаются "место" возникновения ошибки (обычно
__FUNCTION__
вызывающего), код ошибки и строка описания.
errno=...
заменены на вызов.
CheckRef()
/CheckSid()
/CheckCid()
будет показывать ИХ названия, а не их "клиентов", что не совсем правильно.
Но для решения проблемы пришлось бы уже ИМ передавать лишний параметр
"const char *place
", а это бы уже реально снизило
производительность.
Так что забиваем.
08.09.2009: но потихоньку вырисовывается направление, как надо реализовывать подобные модули, и как должен выглядеть их публичный API (понадобится версионирование -- поскольку плагины inherently связаны с динамической загрузкой).
11.09.2009: да, версионирование введено. И даже
более того -- ссылки на стандартные плагины (cxv4 и local) теперь не
"зашиты", а они автоматически регистрируются в
cda_get_dat_p_info_by_scheme()
при первом вызове.
17.09.2009: но вообще-то надо будет поменять этот подход -- чтобы ЗАШИТЫХ плагинов вообще б не было, а пусть ВСЕ НУЖНЫЕ регистрируются самой программой. Пример резона -- в сервере cda нужна, но вот модуль local там совершенно ни к чему.
30.06.2014: наполнена
cda_register_fla_plugin()
-- практически копированием из
cda_register_dat_plugin()
.
24.08.2014: но на СЕЙЧАС надо всё-таки сделать,
чтоб в initialize_cda_plugmgr()
вызывались бы методы
init_m()
builtin-модулей.
25.08.2014: да, сделано.
next
из самой
метрики? Как-то это кривовато. И:
(Текущий вариант -- собираются отдельные утилиты, вроде epics_cdaclient, tango_cdaclient и прочие; при этом никакого "epics_pult" нет, как и варианта, сочетающего в себе оба плагина сразу.)
30.08.2019@дорога-на-обед-в-Гуси, ~11:50 (около логова ГУП УЭВ): суть идеи:
CX_CDA_PLUGINS_LIST
, и в cda_core найти некую точку, в которой
выполнять "инициализацию" -- проходиться по этой переменной и загружать
указанные там модули.
30.08.2019@обед-Гуси, ~12:20: зачем так сложно! Можно ж проще:
cda_get_dat_p_rec_by_scheme()
-- вот там и
загружать.
Да, для этого нужно будет добавить в cda_plugmgr.c код, аналогичный cxsd_modmgr.c'овскому.
cda_allow_plugins_loading(int allow)
-- чтоб устанавливаемый ею флаг работал and'ом с uid==euid.
30.08.2019: (уже после обеда, в ИЯФе): ещё процедурные тонкости:
30.08.2019@пляж, после обеда: насчёт диагностики:
Очевидно, надо бы где-то запоминать факт облома и диагностику более не повторять. Но тут тоже есть тонкости:
Но это решение не только очевидно, а и криво: если софтина долгоиграющая, вроде GUI-дизайнера, то плагины могут быть добавлены в доступную директорию ПОЗЖЕ, и затем повторена попытка добавить каналы. Тогда схема с "ячейка с флагом облома" не позволит ничего сделать, пока программа не будет перепущена.
Просто SLOTARRAY с strdup(scheme)
в качестве
ключа?
01.09.2019@дома: делаем.
plugins_loading_allowed
и аксессор
для управления им -- cda_allow_plugins_loading()
.
cda_get_dat_p_rec_by_scheme()
--
простейшие:
!plugins_loading_allowed
, а также при
getuid() != geteuid()
(это проверка на SUID'ность) возвращаем
NULL ("облом").
cxldr_get_module()
, и если успешно -- то...
cda_dat_p_ldr_context
, ...
cda_plugmgr_METHOD_cm()
, вместо передачи checker'у описателя
desc через privptr тут (поскольку тип ОДИН) зашито сравнение с
CDA_DAT_P_MODREC_MAGIC
и CDA_DAT_P_MODREC_VERSION
.
И вот оную нулёвую версию пробуем, вручную подсунув все пути (выставив
CX_CDA_PLUGINS_DIR
и LD_LIBRARY_PATH
) и
с-симлинковав cda_d_epics.so->libcda_d_epics.so -- фиг, чё-то не так.
Будем разбираться.
03.09.2019: неясно только, как я собирался заставить
это работать -- ведь надо было ещё как-то загрузить libca.so и libCom.so.
Через LD_PRELOAD
запихивал, что ли? Вот не помню...
02.09.2019@пультовая:
cda_get_dat_p_rec_by_scheme()
(и
cda_get_fla_p_rec_by_scheme()
аналогично, авансом на будущее).
argv0
.
cda_new_context()
, передаёт прямо свой argv0
(контекста там ещё нет), следующие 2 -- ci->argv0
, а 4-й --
...
cda_add_server_conn()
, поскольку это простой
"переходник-wrapper", а теперь понадобилось получать доступ к контексту
(ради поля argv0
), для чего требуется проверка cid'а.
Помогло -- плагины ищутся и с учётом пути бинарника.
undefined symbol: cda_dat_p_report"
Смысл ясен: символы из бинарников НЕ экспортируются для далее загружаемых модулей.
-Wl,-export-dynamic
". Он расставлен повсеместно -- в
Makefile'ах для cx-starter, в programs/xmclients/ (pult и
histplot), programs/utils/ (cdaclient & Co.),
hw4cx/pzframes/ (pzframeclient и за компанию прочие по-девайсные
утилиты-скрины).
Следствие -- больше не требуется генерить пустые libcda_d_NNN.c.
epicsCDA_FLA_P_MODREC_SUFFIX_STR
".
Несколько часов маялся, пытаясь понять, почему же в cxsd_modmgr'е всё работает, а тут не хочет, причём так странно -- закавычивает "где не надо".
В конце концов обнаружилось -- позорнейшая ошибка-опечатка: было
#define CDA_DAT_P_MODREC_SUFFIX_STR __CX_STRINGIZE(CDA_FLA_P_MODREC_SUFFIX_STR)
вместо надлежащего
#define CDA_DAT_P_MODREC_SUFFIX_STR __CX_STRINGIZE(CDA_FLA_P_MODREC_SUFFIX)
Собственно, препроцессор всё выполнял правильно, как ему и указано.
Раньше это не вылазило потому, что CDA_DAT_P_MODREC_SUFFIX_STR
ранее нигде не использовалось (по причине отсутствия динамической загрузки
таких модулей).
Исправлено (вместе с CDA_FLA_P_MODREC_SUFFIX_STR
) --
заработало!!!
Кстати, директории frgn4cx/*/util/ теперь можно вообще убирать -- они становятся нафиг не нужны (разве что только ради статически-собранного epics_cdaclient -- с TANGO-то статика не работает...).
03.09.2019: доделываем:
cda_get_dat_p_rec_by_scheme()
добавлена выдача на stderr диагностики об ошибках.
Учитываются соглашения:
cda_set_err()
для формирования информативного сообщения.
05.09.2019: обнаружилась неприятность: схемы, чьи модули уже присутствуют, распознаются на любом регистре букв ("CX" эквивалентно "cx", а "EPICS" катит как "epics"), но вот для целей ЗАГРУЗКИ модуля имя схемы передаётся "как есть", и оно пытается грузить файл cda_d_EPICS.so -- естественно, безуспешно.
Разбираемся:
cda_get_dat_p_rec_by_scheme()
сравнение ведётся при помощи
strcasecmp()
.
Осмотр исходников показал, что лучше всего засунуть унижнерегистрирование
в split_url()
:
cda_get_dat_p_rec_by_scheme()
.
Итого -- исправлено в split_url()
. Проблема ушла.
Заодно аналогичное изменение внесено в cda_add_formula()
,
где используется другой способ указания схемы -- не "scheme::", а "#!scheme"
(как в скриптах).
На этом работу можно считать завершённой. Далее:
14.09.2009: создан основной скелет, отражающий нынешнюю степень понимания требуемой функциональности cda вообще. Он пока касается только работы с собственно соединением, каналы еще не затронуты никак.
Сделана вся инфраструктура взаимодействия с cxlib'ом, и в том числе реализовано всё для reconnect'ов.
Очень четко прослеживается то, как надо разделять обязанности между cda_api и плагинами: вся "высокоуровневая" информация о соединении (сама структура, event-processer'ы, etc.) находится в ведении cda_api; а вот те же reconnect'ы -- забота плагина.
15.02.2010: модуль переименован в cda_d_cx вместо былого cda_d_cxv4 -- в связи с переходом на универсальную схему модулей, где имя плагина жестко связано с именами файла и modrec-описателя.
12.02.2015: just for record: модуль стал работоспособен еще с месяц-два назад.
07.04.2015: тогда, при начальном создании модуля
в сентябре 2009г., была заложена некая идеология с параметром
from_new
у SuccessProc()
и
FailureProc()
, плюс вызовом SuccessProc()
прямо из new_srv()
(new_conn()
изначально)
при cd>=0
-- явно бредовым и повсеместно
закомментированным. Видимо, "from_new" был каким-то отражением былого
v2'шного rcn_on_ifail
, выкинутого еще 18-10-2012.
Сейчас и закомментированный вызов удалён, и параметр
from_new
.
С одной стороны, при разработке дизайна v4 предполагалось, что все эти фокусы ("извращения") не понадобятся.
С другой же,
Авто-резолвинг вообще хбз как сочетается с туннелированием (как указывать, что во что транслировать -- учитывая, в каком виде будут получаемы ответы). Так что, возможно, надо думать о каком-то совсем ином идеологическом решении: только не отдельная софтина-gateway, а что-то типа дополнительных мозгов в самом cda_d_cx/cxlib.
25.05.2015: ввести-то трансляцию несложно, вопрос -- КУДА? В cda_d_cx.c, чтоб при установлении соединения подменялось, или же в cda_core.c, чтоб оно работало вообще для ВСЕХ протоколов, и имена б в "Knob properties" и в лампочках серверов показывались РЕАЛЬНЫЕ (но -- как? в переменной указывать и протокол связи тоже?).
После обеда: окей, делаем пока в простом варианте -- в cda_d_cx.c.
cx_connect()
унесён в отдельную
DoConnect()
(кстати, стало в точности как в v2...).
find_srvrspec_tr()
.
30.04.2017@утро-дома: кстати, тогда же (11-02-2015) была добавлена трансляция в конкретно v2cx.
Но вот чтоб понадобилось реально использовать за прошедшие 2 года именно в v4 -- даже и не припомню.
Причина -- так появляются "пустые" значения в векторных каналах.
15.03.2015: делаем.
cda_dat_p_update_dataset()
добавлен параметр
call_update
. Когда он ==0, то уведомление не вызывается.
В insrv -- пожалуй, да: ведь оно ситуативно идентично обычному сетевому протоколу, просто без сети.
А в local -- пожалуй, нет: оно ведь пассивно, там нет операции "запросить чтение", переменные ВСЕГДА считаются "актуальными".
...так и сделано.
16.03.2015: делаем возможность различать обновления и текущее значение в ветке cxsd_fe_cx->cxlib->cda_d_cx.
CXC_CURVAL
, для присылки его
в ответ на CXC_PEEK вместо CXC_NEWVAL'а.
PutDataChunkReply()
'ю кода слегка
переделана архитектура SendAReply()
-- chunk_maker'у
добавлен параметр info_int
(да, пришлось и всем прочим
функциям его добавить, ненужный).
cx_newval_info_t
добавлено поле
is_update
.
17.03.2015: проверено -- работает (только значение info_int не использовалось :), исправлено).
20.04.2015: модификация по идее от 16-04-2015: для
каналов с CX_TIME_SEC_TRUSTED
значение call_update игнорируем и
ВСЕГДА считаем это обновлением. Поскольку раз уж "TRUSTED", то значению --
по определению -- можно доверять и считать его авторитетным, а не просто
"текущим известным когда-то измеренным". 04.01.2017:
уже нет, та альтернатива убрана вместе со всем "TRUSTED".
Кстати, call_update
переименовано в is_update
.
17.07.2015: ЗАМЕЧАНИЕ:
То ли на этом оно приподсломается, то ли, наоборот, получится "как надо", и автоматом будут отдаваться (через весь тоннель, возможно, через цепочку из нескольких серверов) "текущие значения"?
18.07.2015: ЗАМЕЧАНИЕ 2: а может, для конкретно is_internal всё-таки отдавать is_update даже и в cda_d_insrv?
Это ведь канал несколько иного смысла, и желательно бы его значение "push'нуть" в драйвер-клиента сразу, иначе тому придётся вычитывать оное значение из cda (куда оно попадёт) самостоятельно, что выглядит лёгкой кривизной.
...вылезло это при создании
vdev.c::vdev_init()
.
СДЕЛАНО.
24.03.2016: ЗАМЕЧАНИЕ 3: всё же надо и в cda_d_insrv использовать ТОЧНО ту же логику "когда считать начальное чтение текущего значения за update", что и в cxsd_fe_cx+cda_d_cx.
Резоны:
Нынешние драйверы ist/v1k/v3h/mps20 работают потому лишь, что начальное чтение из устройств отдаёт результат гарантированно позже инициализации всех драйверов при старте сервера (вследствие удалённой шины CAN). Как только появится возможность делать devrestart (аналог sato) через консоль, либо на локальных устройствах (PCI, VME, ...) -- нынешний подход даст сбой.
Итого -- проверки такие добавлены, теперь надо тестировать.
...кстати, в cxsd_fe_cx отсутствует для rw-каналов проверка, что они не NEVER_READ -- а просто для rw всегда делается NEWVAL. Непорядок!
CX_MON_COND_ON_CYCLE
.
Что, строго говоря, не очень хорошо.
Но при попытке перехода на CX_MON_COND_ON_UPDATE
обнаружилось, что оно просто не работает. Причина в том, не-CYCLE-мониторы
не опрашиваются вовсе.
08.05.2015: видимо, надо ВСЕ каналы добавлять в
per_cycle_monitors
.
10.05.2015@пляж: сейчас на
каждое соединение имеется свой per_cycle_monitors
, который и
запрашивается каждый цикл. Спрашивается -- зачем так?
Можно ведь иметь ОДИН список "запрошенные каналы" (с reference
count'ом на каждый), в который и добавлять каналы по мере надобности (и
удалять тоже). Идея в том, что ИЗМЕНЕНИЕ списка мониторируемых каналов
бывает сравнительно редко, в то время как запрос -- каждый цикл. Поэтому
введение единого списка, увеличив затраты на коннект/дисконнект и сильно
уменьшив количество вызовов CxsdHwDoIO()
(содержащих кучу
проверок), в сумме даст выигрыш.
...кстати, сам поиск "зарегистрирован ли уже канал" (т.е., по массиву
32- или 64-битных int конкретно на x86 можно оптимизировать, сделав
ассемблерной вставкой -- это SCASD
или SCASQ
).
12.05.2015: некоторое обсуждение с анализом:
Казалось бы -- надо запрашивать чтение не только сразу после "оживления"
канала, но и по получению значения... Но имеющийся там флаг
VDEV_RERQ
(с устрашающим комментарием "DANGEROUS!!! ...")
просто не используется.
Анализ кода vdev.c и, например, ist_xcdac20_drv.c, показывает, что вся та архитектура нормально работает только в присутствии клиента -- там хотя бы cx-starter всегда есть.
periodics[]
и спрашивает их чтение каждый цикл; и в нём всегда
используется модель "on_update".
13.05.2015: процесс:
cx_setmon()
: добавлен параметр on_update
,
влияющий на то, какой будет заказан тип мониторинга.
hwrinfo_t.on_update
,
в которое прописывается 1 при наличии суффикса @u (да, "парсинг"
примитивный).
per_cycle_monitors[]
добавлялись каналы ВСЕХ
мониторов. Это поведение определяется #define'ом
REQ_ALL_MONITORS
.
Оно так, конечно, не очень красиво (особенно название per_cycle_monitors), но всё равно в будущем хочется заменить per-connection-списки на один общий.
Работает.
Благо, в процессе реализации "гроханья cda-ресурсов" в течение последнего
месяца и понимания прибавилось, и некоторые предварительные действия были
исполнены (конкретно cda_dat_p_get_server()
'ов параметр
options
).
Пара замечаний:
Это будет описано в разделе по cxlib_client.
30.11.2017: собственно действия:
SRVTYPE_DATA_IO
=0 и
SRVTYPE_RESOLVER
=1, для различения типа сервера (в первую
очередь -- для передачи их в options
при регистрации/поиске
объекта-сервера).
cda_d_cx_privrec_t.srvtype
, ...которое пока не
очень ясно:
cda_dat_p_new_srv_f()
, чтоб туда и этот тип передавался; ведь
НАДО же понимать, что создаётся сервер-резолвер, а не обычный (и по имени
этого делать -- нельзя!).
Возможные варианты -- find, look for, look up, seek, search. Еще есть "hunt". EPICS использует "search".
Учитывая, что смысл операции -- даже не просто "поиск", а "розыск", и "розыск" переводится как "search", то (пофиг на похожесть с EPICS'ом) пусть будет "search"; сокращённо -- "srch".
07.12.2017: поскольку на уровне cxlib всё сделано и протестировано, то приступаем к реализации основного "мяса" тут.
cda_d_cx_new_srv()
добавлена проверка
srvtype
, и для SRVTYPE_RESOLVER
выполняются иные
действия -- вызов cx_seeker()
.
Собственно, это и всё, поскольку на моменте создания seeker'а инициализация такого "сервера" и заканчивается.
ProcessSrchEvent()
.
cda_d_cx_new_chan()
вариант для не-w_srv
наполнен минимальными действиями -- добычей "сервера" и регистрацией в нём
hwr'а.
Но никакое "добавление к списку резолвлимых" (или и вовсе тут-же-отправка запроса) пока не реализовано. Надо б еще поопределённее понять, ЧТО именно делать.
rslv_
" (это
rslv_type
и rslv_state
).
Действия по обрыву соединения в FailureProc()
уже есть.
Только они там связаны со статичным "сервером" под именем
udp_resolver_rec
, который определённо надо убирать, заменяя на
отсылку к "правильному" резолверу.
udp_resolver_rec
убираем и делаем "пересаживание"
каналов на резолвер.
ProcessSrchEvent()
.
param1==hwr
.
rslv_state == RSLV_STATE_UNKNOWN
(rslv_type
при этом проверять и незачем).
Но возможности для такой проверки простым образом нет (только если идти
по списку). Т.к. cdaP.h не предоставляет интерфейса для добычи
dataref'ова sid'а, а сам cda_d_cx.c его в своём
hwrinfo_t
sid'а не хранит.
cda_context_t
) самого резолвера вообще нигде никак не
фигурирует.
udp_resolver_rec
.
Модель с ним была бы вполне работоспособна. Но вот подчищать ресурсы было б весьма неудобно, по сравнению с текущим "по своему RESOLVER-sid на каждый контекст".
08.12.2017: продолжаем...
ProcessSrchEvent()
.
Оно сделано по образу и подобию соответствующих кусков кода из
cda_d_cx_new_chan()
и FailureProc()
, подглядывая и
в SuccessProc()
.
Но создать что-то вроде "ПоселитьHwrВТакойТоСервер()" для использования
во всех вовлечённых точках не представляется возможным, т.к. код довольно
разный, в следствие разных обстоятельств (например, вариант
RSLV_TYPE_HWID
в ProcessSrchEvent()
неактуален
(хотя вреда и не будет)).
Детали --
cda_d_cx_privrec_t
'е поле
"was_cx_begin_ed
",
AskOneName()
", которая будет
проверять, что если еще НЕ-begin_ed, то сделать cx_begin()
, а
потом попробовать cx_srch()
, и если то обламывается с
+1
, то cx_run();cx_begin()
и опять
cx_srch()
.
Соответственно, пакеты запросов будут максимально оперативно наполняться прямо по мере поступления каналов.
cx_run()
-то когда
делать (кроме как по переполнению)? Только в таймауте?
cx_srch()
сразу, а оставить это таймауту. Тогда всё
выйдет проще и элегантнее, особенно с учётом идеи про "первый таймаут через
1 миллисекунду" (по тексту идёт ниже).
PeriodicSrchProc()
:
cx_srch()
, при
результате !=0 делает cx_run();cx_begin()
и повторно тот же
cx_srch()
-- такой вариант отправки+опустошения буфера по мере
заполнения.
Т.е., очень несложно, и -- вроде всё, готово.
Разбирательство оказалось коротким: проблема крылась в
SuccessProc()
, которая не была готова к
RSLV_TYPE_GLBL
. После добавления этой альтернативы --
заработало!!!
08.12.2017@по-дороге-домой:
Но надо просто при восстановлении коннекта к любому из серверов ТУТ ЖЕ делать пере-опрос. Для чего просто сбрасывать resolver-sid'у имеющийся таймаут и заказывать новый через 1 микросекунду. Для чего, конечно, придётся в privrec'ах всех серверов помнить указатель на privrec их резолвера.
10.12.2017@дома:
main()
:
он там определяет тип параметра (PK_xxx
) и те, что счёл за
PK_PARAM
, добавляет
CdrCreateSection(,DSTN_ARGVWINOPT,)
к группировке, а за base
считает то, что опознал как PK_BASE
.
И просто набор букв+цифр однозначно считается за PK_PARAM
.
Вывод: надо сделать, чтобы он опознавал потенциальные base'ы. Для этого достаточно наличия в них ':'.
11.12.2017: попробовал ":icd" -- не работает.
12.12.2017: решаем проблемы.
ProcessSrchEvent()
(что-то типа
"ts->resolver=me
").
Но это приведёт к проблеме, осознанной 07-12-2017, и отброшенной тогда как эффективно-несуществующей:
И что, делать-таки API для проверки "моего ли контекста этот ref"?
FailureProc()
; ведь
тогда явным образом получаем ссылку на ТОЧНО НАШ резолвер.
cda_d_cx_privrec_t.resolver
.
FailureProc()
добавлено сохранение ссылки в
me->resolver
, и изначально значение rs
берётся
оттуда же, а не =NULL
.
SuccessProc()
организует таймаут через 1 микросекунду
(предварительно убирая имеющийся).
Проверено -- работает, канал пере-находится сразу после реконнекта сервера.
ScheduleSearch()
.
cda_d_cx_new_srv(..., srvtype == SRVTYPE_RESOLVER)
и делался, а
самим cda_d_cx_new_chan()
-- нет.
Вылезло это вчера на испытании epics2cda при попытке обратиться к НЕСКОЛЬКИМ PV: первая-то мгновенно находилась, а последующие -- нет (как позже оказалось, не "нет", а "через время до 10 секунд"). Сначала удивлялся -- "как же так, ведь UDP-резолвинг давно отлажен!", но потом, добавив море отладочной печати, нашёл-таки виновника.
Решение:
cda_d_cx_new_chan()
заказ "через 1 микросекунду",
конечно, добавлен.
ScheduleSearch()
повыше к началу файла.
ca_flush_io_tid
: при надобности заказать отложенную отправку
таймаут заказывается только если в этот момент его нету).
rcn_us
для хранения
длительности.
rcn_tid
.
Тут пара нюансов:
rcn_tid>=0
, а не просто "rcn_us>=0" -- чисто на всякий случай;
ну и чтоб уж точно разрешать таймауты в 0 микросекунд.
rcn_us < 2
".
...ну да фиг с ним -- пусть не идеально, но работает, да и ладно.
Проверил -- да, косяк исправлен.
main()
:
в тесте на PK_BASE
дополнительно к присутствию ':'
(p_cl!=NULL
) проверяется наличие '.'
(p_dt!=NULL
). Поскольку в этом же условии предварительно
проверяется отсутствие '/', то оно не перекроется с именем файла.
Это полностью повторяет кусочек из v2'шного
pzframe_main.c, где в точно таком же сочетании условий в таком же
месте проверялось в целях PK_BIGC
(только там вместо
'.' ролял символ '!').
Ни cda_core лишнее не убирает, ни
cxsd_db.c::CxsdDbResolveName()
не считает
множественные точки за одну -- как должно бы, в стиле *nix (где
множественные '/' в именах файлов считаются за один).
CxsdDbResolveName()
такие имена явным образом
запрещены (в стиле v2 они бы считались глобальными и с ними слишком много
маеты).
Тут явно именно cda должна "leading dots" убирать; видимо, не делает этого из-за того, что оно не в имени канала, а в defpfx'е.
-d
, а -b
(base), то всё равно поведение такое же.
Вывод: надо б покорпеть над блоком "слияния" --
combine_name_parts()
, что-то еще? -- в cda_core.c,
дабы он позволял больше.
Есть еще несколько замечаний:
Ну это понятно -- в MotifKnobs_cda_leds.c нет реакции на событие
CDA_CTX_R_NEWSRV
.
17.12.2017: сделано.
nan
.
А надо бы их делать тёмно-серыми NOTFOUND.
Делать им прямо при регистрации cda_dat_p_set_notfound()
?
Стал разбираться: странно...
CDA_DAT_P_NOTREADY
,
в ответ на что cda_add_chan()
должен бы выставить
CXCF_FLAG_NOTFOUND
.
&& 0
". Почему?
14.12.2017@утро-душ: понятно, почему -- в таком случае любые не-локальные (не-{insrv,dircn,local}) каналы в скринах сразу становились чёрными, пока не успело приконнектиться. Почему не записал это тогда -- вот вопрос.
CXCF_FLAG_NOTFOUND
делать
не "двоичным" образом -- "если _DAT_P_OPERATING -- то нет, а если
_DAT_P_NOTREADY -- то да", а более гранулярно:
аналогично соглашению "init_dev()" у драйверов -- если -1
, то
просто облом, а если иное отрицательное число, то считать это число
минус-флагами, которые надо выставить?
-1
, и минус-rflags -- являются указанием на фатальную ошибку,
просто с разной степенью детализации.
А тут получится, что -1
-- это _DAT_P_ERROR, а иное
отрицательное число -- вовсе не ошибка, а вариант
_DAT_P_NOTREADY
(которое само вообще-то =0!).
cda_dat_p_set_notfound()
и при регистрации в
cda_d_cx__new_chan()
, и при передаче резолверу в
FailureProc()
.
С одной стороны, он должен быть валидным (т.е., ":99" не прокатит).
С другой стороны, оно не должно ни к чему коннектиться.
Хотя имена с подчерками и так формально невалидны.
cda_d_cx_new_chan()
просить такой тип сервера, а в
cda_d_cx_new_srv()
по нему ничего не делать (выстявляя лишь
me->state=CDA_DAT_P_NOTREADY).
determine_name_type()
, считая их невалидными.
Поэтому "_NON_EXISTENT_" не прокатит, можно только "NONEXISTENT" или, на крайний случай, "NON-EXISTENT".
12.12.2017@дома: да, в
determine_name_type()
не проверялось на возможный '-'
после ':'.
13.12.2017: проверка добавлена --
if (*p == '-') p++
-- заработало.
15.12.2017: итого, что далее:
25.12.2017: посмотрел-подумал -- сначала казалось, что все "новые" функции (касательно search) натолканы "не туда", но по факту выходит (по перекрёстным вызовам), что в другое место перетаскивать будет только хуже. Так что оставляем как есть, а пункт "withdrawn".
25.12.2017: да, сделано, с обеих сторон почти копии.
RlsMonSlot()
делается
per_cycle_monitors_count--
практически безусловно. А что, если
это вызывается как просто деструктор, еще ДО того, как монитор был
зарегистрирован и сделано ++? Тогда оно бы втихушку "откусывался" последний
из зарегистрированных мониторов; а могло б и вовсе
per_cycle_monitors_count
стать отрицательным.
Обнаружилось при наполнении cda_d_starogate, куда "слизывался" местный менеджмент мониторов.
Проявиться пока не могло потому, что единственный сценарий для проявления
(СЕЙЧАС!) -- чтоб обломилось увеличение буфера
per_cycle_monitors
.
07.03.2018@вечер-дома: может, использовать поле moninfo_t.cond -- вначале в него писать -1, и в RlsMonSlot()'е проверять, что если значение <0, то НЕ делать декремент? (А реальное значение писать уже ПОСЛЕ успешного добавления в список.)
10.03.2018: исправляем -- тривиально:
CXSD_FE_CX_MON_COND_MON_NOT_ADDED
=-1.
Кстати, а можно было остаться и с 0 -- в протоколе значения предусмотрительно идут с 1, оставив 0 неиспользуемым (этот подход стараемся применять повсеместно -- НЕ использовать 0 в роли осмысленного значения!).
RlsMonSlot()
вставлен guard, что
mp->cond
не равно этому спец.значению.
SetMonitor()
оно прописывается сразу после
аллокирования ячейки.
Возможностей проверки не просматривается, поэтому просто убедимся, что ничего не сломалось.
SO_KEEPALIVE
:=1. "Кошмар, как так?!?!?!".
Несколькими минутами позже дошло: а незачем, т.к. протокол CX и так
подразумевает постоянную отправку маркеров цикла
(CXT4_END_OF_CYCLE
), так что обрыв соединения обнаружится. А
до его установления -- есть таймаут MAX_UNCONNECTED_SECONDS
=60,
так что и недоделанные соединения долго болтаться не будут.
16.05.2020: главные вопросы были в методологии/идеологии:
Найдены наипростейшие решения, требующие минимума действий и умствований:
cd < 0
.
На взведённость rcn_tid
'а внимания не обращаем, т.к. ...
Действия:
FailureProc()
'а в
отдельную ScheduleReconnect()
, которой параметром передаётся
длина таймаута.
cda_d_cx_srv_ioctl()
, сейчас понимающая только
CDA_DAT_P_SRV_IOCTL_NR_RECONNECT
, и по нему -- если
cd < 0
-- делающая ScheduleReconnect(, 1)
.
Засим пока замораживаем, ибо проверить пока никак.
04.07.2023: проверяем -- запуская сервер с "deny all",
натравливая на него клиента (коорый обламывается), затем меняя на "allow
all", делая серверу "kill -USR1
", а потом нажимая на лампочку
сервера.
Чё-то глючит -- команда до cda_d_cx_srv_ioctl()
доходит, но
реконнекта НЕ делается.
me->cd < 0
". При НЕЖИВОМ соединении!
CAR_EACCESS
отсутствовала "подчистка", включающая присвоение cd = -1
.
FailureProc()
"подчищающего" куска
/* Forget the connection */ if (me->cd >= 0) cx_close(me->cd); me->cd = -1; MarkAllAsDefunct(me);
После этого заработало.
...но имеется какая-то странность: через минуту после облома pult-клиент реконнектится САМ!
06.07.2023: поскольку сегодня была исправлена
причина сопутствующего косяка -- в
cxlib_client.c::async_CS_OPEN_CONNECT()
, то теперь всё
работает полностью по проекту, так что "done".
cda_d_cx_new_chan()
'овы "мозги" определения не-указанности
сервера: теперь «волшебным» именем неуказанного выступает не
только "unknown", но и "unknown:0".
Смысл в том, что указать имя канала БЕЗ двоеточия в имени сервера
затруднительно: если в просто каналах и Chl/datatree-клиентах это ещё
вощможно, то PzframeMain()
'овская логика этого не возволяет
никак: строка "unknown.SOMETHING" воспринимается ею как
PK_PARAM
, что в дальнейшем приводит к ругательству "Junk at
position 8". Т.е., pzframe-утилиты никак нельзя было натравить на
устройство по без-серверному адресу.
После этого простенького дополнения -- стало возможно.
P.S. Добавлено только в cda_d_cx.c, а в cda_d_cx4old.c -- нет.
24.06.2007: программа должна зарегистрировать каждый такой доступный локальный "канал", указав его имя (по которому и будет идти доступ) и указатель на значение (чтобы было откуда брать).
При попытке получения доступа к каналу по cda он ищется в таблице,
если находится -- то в поле "номер" во внутреннем дескрипторе
записывается его номер в таблице, если нет такого -- то перманентно
проставляется флаг CXCF_FLAG_NOTFOUND
.
26.07.2008: соображение/вопрос/возражение: если "local:"-каналы реализовывать просто как ссылки на переменные в памяти программы, то возникают сложности с соответствием семантике остальных каналов:
Ответы, видимо, таковы:
conns_u
).
А если значения нужны только в дополнение к значениям "настоящих" каналов -- то программа может вообще этот вызов игнорировать, ибо актуальные значения и так будут использованы в нужный момент (хотя -- не отрыгнется ли это бесконечным возрастом логических каналов, в которых поучаствуют локальные? 06.08.2008: а что, собственно, мешает модулю cda_d_local ВСЕГДА отдавать возрасты 0?).
Резюме: надо реализовывать схему "local:" так, чтобы она в максимальной степени походила на остальные протоколы.
Вопрос: вызывать функцию после того, как cda сама произведет запись? Ответ -- нет, передавать значение функции, чтобы та могла произвести его валидацию, или вовсе не производить запись.
Отдельный вопрос -- для чего "local:"-каналы записи вообще нужны? А вот для чего:
P.S. Кстати, кроме имени и указателя на значение также надо указывать и ТИП канала (раз уж мы собираемся поддерживать не только cx_ival_t).
15.09.2009: итого -- создан скелет.
API же для клиентских программ описывается в include/cda_d_local_api.h.
01.05.2014: начато РЕАЛЬНОЕ наполнение модуля, уже под схему "контекстов", в соответствии с принципами, продуманными в 2008-м.
06.05.2014: доваян код первой версии, на прямых вызовах (НЕ через pipe'ы). Пока не проверен.
07.05.2014@утро-перед-просыпанием: как можно организовывать
уведомления об обновлениях через pipe: слать int'ы, содержащие
clientside-номер обновляемого канала (нынешний channel_n), а цикл
сервера сигнализировать значением -1
.
07.05.2014: некоторые комментарии:
Для этого придётся внести в fdiolib возможность указывать write-only -- чтоб она не пыталась спрашивать у ядра готовность write-стороны пайпа на чтение.
var_cbrec_t
, но кто и как сбрасывать будет...).
03.10.2014: по-хорошему -- НАДО б иметь этот флаг.
Держать -- в hwrinfo_t
. Для чего
придётся поменять модель "вызова уведомлений": в lcninfo_t
хранить также и указатель на cda_d_local_privrec_t
, чтоб
оно в CallVarCBs()
могло доступиться до hwrinfo и
проверить флаг. Флагов, кстати, потребуется несколько: по количеству
типов событий.
15.09.2014: тогда (в мае) реально было недоделано: не хватало исполнения записи и дёрганья циклов.
Замечание: "подтверждение" записи -- вызовом для затронутого
канала cda_d_local_update_chan()
-- ответственность реализатора
(т.е., метода do_write()
).
16.09.2014: добиваем:
Сделано несколько некрасиво:
pdt_privptr
.
cda_d_local_update_cycle()
через ForEach-итератор для
всех ячеек вызывается PerformCycleUpdate()
, объявленная
forward-декларацией.
Строго говоря, архитектура ориентирована на будущий вариант с pipe'ами.
Проверено при помощи localtest'а: работает примерно как положено. Единственная претензия -- при возврате другого значения, чем записано, обновления сразу не происходит, вследствие архитектуры wasjustset. Из-за того, что полного "чтобы она в максимальной степени походила на остальные" (26-07-2008) пока нету; точнее, скорее схема "local::" опережает время (хи-хи!) и пока cda/Cdr до модели "поканальной, а не погрупповой циклами" работы пока не доросли.
Как бы то ни было, пора переводить на pipe'ы, по проектам от 07-05-2014.
17.09.2014: за вчера-сегодня вариант на pipe'ах сделан, под именем cda_d_pipal.c ("pipal" имеет ту же длину, что "local", так что контекстной заменой одно превращается в другое).
Но пока не проверено и не отлажено (pipaltest в одну сторону не работает, а в другую падает).
Плюс есть некоторые вопросы на тему "в какой момент какие ресурсы должны освобождаться".
Хотя попытки защиты на основе being_processed
плюс
being_destroyed
введены.
20.09.2014@вечер-засыпая: а можно оставить ОБА варианта:
API -- cda_d_NNNN_api.h -- у них одинаковый, так что они взаимозаменяемы.
23.09.2014: прогресс по проверке и отладке:
ForeachVarCbSlot()
, вот и лезло к списку по адресу NULL.
filedes[1]
, а [0]
игнорировался.
После обеда: проверил явно, при помощи
check_fd_state()
с O_RDONLY и O_WRONLY, оба конца --
никаких ошибок, на оба отдаёт возможность записи и неготовность к
чтению.
24.09.2014: проверил также на SL-6.3:2.6.32/64 -- аналогично, только [0] уже не считается готовым к записи.
Баг в MotifKnobs_selector_knob.c?
Чуть позже: да, был баг там.
23.09.2014: переименовываем --
Теперь пора реализовывать cda_d_inserver.c.
cda_d_local_register_chan()
-- все запрашиваемые через
_new_chan() каналы.
Так можно будет отлаживать "экраны" устройств без серверов, при помощи обычного клиента, натравливаемого на "local::".
Включаться этот режим должен бы отдельным вызовом, но ради удобства использования при отладке можно сделать и уставлением environment-переменной.
08.07.2014: да, сделано.
cda_d_local_register_chan()
-- всё равно это навечно, да и
плевать на такие мелкие потери в отладочном режиме.
15.09.2014: добавлено присвоение 0.0 по malloc()'нутому адресу, иначе бывал мусор.
В остальном же работает, так что "done".
15.09.2014: сама утилитка живёт в programs/tests/ (тут описываем ради удобства), сделана на основе pult.c, отличается тем, что:
do_write
).
Для исполнения ею последнего пункта через cda_d_local_api.h введён следующий функционал:
cda_d_local_chans_count()
-- количество аллокированных
локальных каналов (реально -- номер_последнего+1). В их число входит и
неактивированный 0-й.
cda_d_local_chan_n_props()
-- добыча имени, типа и
адреса_значения указанного локального канала.
cda_d_local_override_wr()
-- перехват операции записи.
26.09.2014: еще некоторое время назад проверено, что этот "Hack/debug-API" работает.
Раздел помещаем сразу за cda_d_local'овым.
Назван не "inserver", а "insrv": потому, что это буквосочетание той же длины, что "local" -- сие упростит унификацию кода между ними.
24.09.2014:
30.09.2014: продолжаем потихоньку пилить.
Пока сделано халтурно-просто: оно вешается непосредственно при
помощи CxsdHwAddChanEvproc()
, указывая privptr1:lcn,
privptr2:hwr.
После обкарнывания всего остального лишнего оно стало собираться.
Недоделки:
01.10.2014: рихтуем:
_init_m()
-- чтоб при загрузке модуля оно
само регистрировалось.
Но тут мутно: не до конца продумано функционирование механизма на основе cxldr, а конкретно cda_plugmgr.c им вообще никак не пользуется -- там всё вручную.
28.10.2014: и "update_cycle" сделано, для чего введена инфраструктура в cxsd.
28.10.2014: сей момент есть поддержка только записи, а ЧТЕНИЕ отсутствует. И оно так вообще во всех cda_d_* (кроме v2cx, имеющего свой механизм "подписки").
Но сейчас УЖЕ можно реализовывать и регулярное чтение:
CXSD_HW_CYCLE_R_BEGIN
. Среди всех каналов, "читаемых раз
в цикл", список номеров которых держать в массиве, годном к указанию в
CxsdHwDoIO()
.
И, кстати, в остальных родственных реализациях связи (конкретно, cxsd_fe_cx) можно делать так же.
29.10.2014: да, сделано, пока безусловно (никакие '@' не проверяются).
lcninfo_t
добавлен растущий массив
periodics
, с полями _allocd и _used определяющими
количество аллокированных и использованных ячеек.
RegisterInsrvHwr()
, там
же и предварительное приращивание на CHANS_ALLOC_INCREMENT=100 ячеек.
cda_d_insrv_del_chan()
,
надо будет в ячейку записывать -1
(это значение должно
рассматриваться cxsd_hw как "IGNORE"), либо вообще тупо удалять
содержимое "со сжатием" (сдвигом следующих на 1 ячейку вниз).
20.11.2014: идеологическое изменение: теперь при регистрации нового канала ВСЕГДА делается возврат текущего значения с текущими параметрами (флагами, timestamp'ом).
Смысл -- как было решено 29-09-2014 для обычного протокола, вначале делать "peek", прося текущее, пусть даже еще ни разу не измеренное.
Иначе сейчас была проблема: при ошибке инициализации устройства оная ошибка никак не отображалась в окне-экране, поскольку возврат никогда не делался -- хотя флаги проставлялись уже изначально.
CxsdHwSetDb()
делается
снятие всего установленного CxsdHwAddChanEvproc()
'ом).
28.10.2014: и даже больше: вот сейчас в
cda_d_insrv_new_chan()
при обломе резолвинга возвращается
CDA_DAT_P_ERROR
и канал навсегда становится NOTFOUND.
А это неправильно! Ведь в каких-то условиях -- скорее в будущей перспективе (либо в доступаемом по remdrv (имена из драйвера поступят только после коннекта), либо еще почему) -- каналы могут "прорасти" позже. 07.04.2015: еще 05-03-2015 было постулировано, что авторитетным источником ИМЁН является конфигурация, а не драйверы. Так что каналы "прорасти" НЕ МОГУТ.
Так что, видимо, надо будет предусматривать какой-нибудь "отстойник", куда бы попадали неразрезолвленные каналы (и возвращать на них NOTREADY). И пытаться их повторно резолвить
Только отстойник должен быть per-sid (а не per-context, как для cda_d_cx).
12.06.2015: еще как касается!!! Ведь сейчас клиентам cda нет возможности получить ни то, ни другое; да и evproc'ы не вызываются (а для fresh_age даже и код события не предусмотрен).
И еще одно вылезло при написании trig_read_drv.c:
cda_add_chan()
'у тип канала CXDTYPE_UNKNOWN
,
чтоб он НЕ выполнял никакую конверсию, а складировал бы данные "как
есть", заодно запоминая вёрнутый ему плагином dtype (который тоже можно
б было получить)?
05.02.2016: да, СТОИТ "сделать возможность
указывать cda_add_chan()
'у тип канала
CXDTYPE_UNKNOWN
". Типа проект:
cda_acc_ref_data()()
-- в
смысле, что не "вычитывает", а получает указатель, который потом и будет
передавать ReturnDataSet()'у.
ri->cur_dtype=dtypes[x]
.
refinfo_t.cur_dtype
должно быть
введено, ...
cda_current_dtype_of_ref()
.
dtypes[x]
вместо
зарегистрированного ri->dtype
.
cda_add_chan()
в таких
случаях будет иметь значение "максимально разрешенный объём в БАЙТАХ" (т.к.
sizeof_cxdtype(CXDTYPE_UNKNOWN)==1).
01.03.2016: делаем.
refinfo_t.current_dtype
("current" вместо
"cur" -- для единообразия стиля с _val и _nelems).
cda_current_dtype_of_ref()
возвращает её значение при
.dtype==CXDTYPE_UNKNOWN, а иначе само .dtype.
01.03.2016: часом позже: неа, теперь само
current_dtype
поддерживается в актуальном состоянии, так что
всегда оно и вертается.
->current_dtype
всегда держать "актуальное" значение и при
конверсии ВСЕГДА использовать current_dtype
.
"Держание актуального" --
ri->current_dtype=ri->dtype
.
cda_update_dataset()
перед конверсией при
->dtype==CXDTYPE_UNKNOWN
делать
ri->current_dtype=dtypes[x]
.
cda_get_ref_dval()
-- также
переведено на current_dtype.
refinfo_t.current_usize
, поддерживаемое в
соответствии с текущим значением current_dtype.
cda_get_ref_data()
и
cda_acc_ref_data()
теперь используется оно.
cda_update_dataset()
возволяет обновлять только REF_TYPE_CHN-ячейки.
На сейчас -- видимо, запись им запретить. Хотя это, учитывая
неединичность интерфейса записи -- cda_process_ref()
и
cda_snd_ref_data()
-- тоже не совсем тривиально.
Разбираемся:
cda_process_ref()
: СЕЙЧАС она отвергает всё, где
представление -- ri->dtype
-- не INT и не FLOAT. Т.е., в
ней запись запрещена автоматом.
cda_snd_ref_data()
: непосредственно перед вызовом
SendOrStore()
(который и будет реализовывать
конверсию для REPR_UNKNOWN, если таковая появится) добавлена
проверка, что при REPR_UNKNOWN возвращать EINVAL.
02.03.2016: теперь собственно trig_read_drv.c.
data_evproc()
переведена на "кошерное" получение данных и отдачу
их наверх.
Чуть позже: неа, =sizeof(CxAnyVal_t) -- чтоб
гарантированно влазили все скаляры, и при этом не требовалось бы
аллокировать внешний буфер (т.к. refinfo_t.valbuf
имеет тип
CxAnyVal_t
).
...но по-хорошему:
CxsdHwResolveChan()
, а
внешние имена с префиксами протоколов через "::" просто не
разрезолвятся.
02.03.2016: и отдельно насчёт проблемы, означенной в заголовке секции -- туннелирование fresh_age.
CDA_REF_R_FRESHCHG
. Для
унификации то же значение 4, что и CXSD_HW_CHAN_R_FRESHCHG
(а
занимавший его ранее CDA_REF_R_LOCKSTAT
перекинут на 6 -- оно
безболезненно, ибо всё равно блокировки пока и близко не сделаны). 25.08.2016: уже на 12.
cda_dat_p_set_fresh_age()
добавлено его генерение.
04.03.2016: проверено (при помощи cdaclient и тестового конфига devlist-trig_read-test.lst с кучей триггерируемых каналов) -- работает! И тип правильно передаёт, и "векторность" функционирует -- в границах выделенного при регистрации буфера.
05.03.2016: допиливаем.
Замечание: если указана какая-то ЧАСТЬ спецификации, то остальное берётся по умолчанию, не пытаясь смотреть в БД. Умолчания -- UNKNOWN,1; это выглядит разумно:
CxsdHwResolveChan()
. При обломе оного
устанавливаются умолчания -- CXDTYPE_UNKNOWN,sizeof(CxAnyVal_t).
Замечание: у такой проверки есть один косяк: валидные cda-ссылки с префиксом "insrv::" не будут давать результата.
07.03.2016: теперь проверяем.
cda_dat_p_update_dataset()
молча
проигнорирует канал (ничего не складирует), но событие UPDATE для него
сгенерит. В результате у double-каналов читается NAN.
09.03.2016: далее -- атрибуты.
Поэтому СЕЙЧАС оно указывает серверу первый дуплет в цепочке (если он вообще есть) -- который в >99% случаев будет описывать исходный аппаратный канал (а в >90% случаев будет и единственным).
Так что и в trig_read_drv.c добавлено.
24.03.2016: ЗАМЕЧАНИЕ:: хоть этот пункт и находится вразделе про cda_d_insrv, но реально-то всё делалось в cda_core.c и trig_read.c (а сам cda_d_insrv.c вообще с прошлого августа не трогался). Соответственно, работает это всё и по протоколу cx::.
26.04.2016: был ляпец в trig_read_drv.c -- он
отдавал наверх полученные {r,d} только при phys_count>1
, а
надо было phys_count>=1
(т.е., >0); в результате оно
просто не отдавалось (т.к. data-каналы с более 1 парой в этой роли не
использовались).
Тремя часами позже:
insrv_fd_p()
заключался в том, что
оно
hwr
,
hwr
, и...
hwr
же -- в
которой уже совсем не код, а hwr канала!
Короче -- не могло работать вообще ничего из передачи свойств. Очевидно, никогда и не проверялось; точнее, "проверялись" (в ~феврале 2015-го, на cPCI+stand), но лишь те, что доступны сразу при регистрации -- вот и не было замечено, что обновление свойств не работает.
Стыдно, конечно...
Скорее нет:
Но симптомы весьма схожи, да. Где б в связке cxlib/cxsd_fe_cx могла быть такая же ошибка?
read()
); 3. делать его--}
Таким образом, почти всегда будут ПРЯМЫЕ вызовы, намного более экономичные, но как только встретится реальный рекурсивный вызов, то этот цикл будет разбит использованием pipe'а.
Дополнительно стоит еще считать количество отправленных, но необработанных
уведомлений: при write()/fdio_send() делать "++
", а после
read()
(ПОЛНОГО! 1 или 2 int32) "--
". Таким образом
соблюдём очерёдность -- иначе может оказаться, что некие уведомления, вызванные
рекурсией, пойдут через pipe, а потом следующие -- ПОСЛЕ возврата из цепочки
рекурсии -- захочет напрямую (тем самым пытаясь опередить, перепутав порядок);
так вот, надо напрямую ТОЛЬКО если обе\а счётчика ==0.
12.10.2015@дом-утро: и еще
нужно "being_destroyed
"!!! И cda_core НЕ должно делать
free()
серверовому pdt_privptr
'у -- например, если
del_srv()
возвращает что-то !=0; +1?
27.10.2015: в продолжение замечания насчёт "being_destroyed":
RlsSrvSlot()
-- т.е., видимо,
НЕ в месте вызова del_srv()
-- то придётся по "+1" делать
pdt_privptr=NULL, чтоб "забыть" про указатель.
srvinfo_t.being_destroyed
уже есть! ...хотя пока
используется мало.
CDA_DATAREF_OPT_ON_UPDATE
(а то сейчас
принудительно работает в режиме "ON_CYCLE").
Актуально для того, чтобы получать значения с максимально возможной частотой.
Сейчас же это умеется только по cx::, а по insrv:: -- нет.
GcnEvproc()
по событию
CXSD_HW_CHAN_R_UPDATE
) заказывать канал, ответ на который
приходит мгновенно, то возникнет бесконечная рекурсия.
Впрочем, её можно разорвать, заказывая чтение из "получателя"
-- insrv_fd_p()
. Тогда петля рекурсии разорвётся, в т.ч. и в
оптимизированном варианте с прямым вызовом (см. 11-10-2015).
Причина -- уже сейчас, с введением iset_walker'а на источники линз, сервер магнитной системы потребляет полторы сотни дескрипторов. Если приделать walker'ы также и на корректоры, то линуксовые 1024 (FD_SETSIZE) резво закончатся.
(Конечно, на таких количествах API epoll --
epoll_wait()
-- будет оптимальнее и позволит больше, но лучше
такой фигнёй не страдать вовсе.)
20.02.2017: как такое можно б было реализовать "безопасно" (т.е., с устранением возможности бесконечной рекурсии):
being_processed
?
hwrinfo_t
-- есть флаг "пришло уведомление". Назовём
update_pending
?
pending_count
.
...еще спасает то, что само событие "цикл" во время обработки событий от каналов возникнуть никак не сможет.
Этак оно может и до бесконечности зациклиться.
Решение напрашивается такое:
GcnEvproc()
'а (для канала-причины), и потом по мере надобности
внутри того цикла "пока pending_count>0".
...хотя можно это развернуть и прямо в самом GcnEvproc()
в
цикл вроде
НОМЕР_КАНАЛА=вызвавший; НАЧАЛО_ДЛЯ_ПОИСКА=0; while (1) { pending_count++; ВЫЗВАТЬ_ОБРАБОТКУ; pending_count--; if (pending_count == 0) break; else НАЙТИ_НОМЕР_ОЧЕРЕДНОГО_НЕОБРАБОТАННОГО; // С НАЧАЛО_ДЛЯ_ПОИСКА, закольцовыванно }
lcns_list
-slotarray'я).
В _fd_p() для этого дескриптора вычитывать номер соединения и производить для него то же самое, что в предыдущем пункте (видимо, то всё-таки надо оформить отдельной функцией?).
Наверное, сделать update_pending
не просто флагом, а битовой
маской (в которую прямо биты _CHAN_EVMASK_* и писать). И:
Единственное сомнение -- не будет ли проблем из-за перепутывания порядка событий в этом аккумуляторе? Пол-решения -- реагировать на события по уменьшению числа-номера: сначала на свойства, потом STATCHG, и в самом конце -- UPDATE.
Кстати, конкретно сейчас на STATCHG особо никто и не реагирует: vdev плюёт, а cxsd_fe_cx считает причиной для отсылки CURVAL.
Резюме:
И эти линки будут более быстрыми, чем нынешние insrv'шные.
Но эта цена приемлема, т.к. в основном никаких рекурсий быть и не должно.
Одно только неясно: как это назвать? Просто dir_insrv -- криво.
lcninfo_t.periodics[]
.
Часом позже: хотя, при постоянном удалении/добавлении
каналов в одном контексте ДОЛЖНА была случиться утечка: в
periodics[]
номера всегда добавляются в конец, так что при
10000-кратной регистрации и потом разрегистрации 50 каналов массив должен
был вырасти до 500000 ячеек.
(Заметил это, когда подглядывал, как тут реализована работа с
мониторами (список которых и есть periodics[]
), чтобы понять "а
как можно мониторировать каналы, чтоб корректно и безопасно" -- сейчас для
cxsd_fe_starogate.c, но и для общего понимания.)
06.03.2018: попробовал было "с нахрапу" исправить -- авотфиг! Может, и хорошо, что тогда (осенью) не заметил.
Ведь тот же gcid может притсутствовать и из-за других каналов -- маппирующихся на тот же аппаратный канал (коим является gcid) через другие имена или cpoint'ы.
-1
, просто пропуская их.
Но тут тоже остаётся изначальная проблема -- в КАКОЙ ячейке periodics[] со значением gcid заменять на -1?
hwrinfo_t
хранить номер ячейки, в которую был
помещён gcid (это значение periodics_used
в момент помещения) и
по этому номеру прописывать -1.
Но после первого же удаления канала и добавления нового корреляция нарушится: hwr будет даден старый, а ячейка аллокируется новая.
gcid
всегда клался в ячейку номер
hwr
.
Только придётся:
periodics_used
из "точки, куда будут
добавляться следующие" в "ТЕКУЩИЙ номер последней используемой ячейки
плюс 1".
Пока выглядит явно проще оставить как есть, т.к.:
07.03.2018@переход-из-13-го-в-1-е
(после еды в честь Ж.Д. на пультовой, ~13:20): а есть вариант проще:
вместо "хитрого" перетряхивания массива -- просто заполнить
его заново, сделав periodics_used=0 и затем пройдясь
ForeachHwrSlot()
'ом, делая
"periodics[periodics_used++]=hi->gcid".
Будет даже быстрее.
Парой часов позже: а ведь cxsd_fe_cx именно так и делает --
при любом изменении взводит per_cycle_monitors_needs_rebuild=1
,
а перед заказом проверяет флаг и при !=0 повторяет заполнение. Мда, "хорошо
забытое старое" :D
filedes[0]
-- регистрируется у
cxscheduler'а с uniq
, плюс сохраняется в
privrec_t.read_fd
и в DestroyInsrvPrivrec()
.
Кстати, все те же соображения касаются cda_d_local.c, имеющего идентичную инфраструктуру (это копии).
31.07.2019: разобрался -- нет, всё в порядке, подозрения напрасны.
Пишущая сторона ТОЖЕ сохраняется (а то куда бы писалось при возникновении
событий?), просто не в privrec'е, а в другой сущности -- в "lcn" (Local
CoNnection), так что заведует ею иная часть кода. Сохраняет функция
RegisterInsrvSid()
, а закрывает -- RlsLcnSlot()
.
...ну право слово -- не мог же я быть настолько туп, чтобы оставить такую
дыру, сделав такой ляп! Просто не совсем очевидная организация кода,
особенно на контрасте с другими использованиями технологии
pipe()
для сигнализации о событиях (конкретно -- в
cm5307_DEFINE_DRIVER.c и pxi6363_drv.c; т.е., где
используются не 0 и 1, а PIPE_RD_SIDE
и
PIPE_WR_SIDE
).
P.S. Чисто по именам видно, что cda_d_local.c была раньше: "lcn" -- LOCAL connection.
02.07.2014@утро-дома-6:20: а что с калибровками -- phys_r/phys_d?
Ответ напрашивается: а прямо в .subsys-файлах
ввести специальный тип секции, что-то типа
DSTN_PHYSDB
="physdb", где указывать таблицы в том
же стиле, что в v2.
Вопрос только в деталях реализации -- КАК содержимое этих секций
будет доводиться до cda_dat_p_v2cx? Видимо, примерно как в старой
cda_TMP_register_physinfo_dbase()
, только указывать не всю
dbase, а по-серверно. И, соответственно, в subsys-API понадобится
интерфейс "перечисления" -- пройтись по всем секциям такого-то типа.
15.07.2014@пляж-16:00: а можно и иначе выпендриться --
...Можно, но ей-богу, уж лучше б побыстрее переходить на нормальную новую версию побыстрее.
16.07.2014: "лучше б", да, но раз мысль появилась -- то сегодня по-быстрому сделана собираемость и влинковка также v2'шных datatree и Cdr. Остались вопросы:
23.07.2014: чтоб проще решить предыдущие 2 вопроса, сборка v2'шных библиотек вынесена в отдельную директорию -- lib/v2cx/ (хотя по-хорошему надо б инкапсулировать в lib/cda/v2cx/).
24.07.2014: доведено почти до конца:
encode_chanhandle(sid, physchan)
. И
cda_srcof_physchan()
возвращает в качестве номера именно
прямо младшие 24 бита.
cda_srcof_physchan()
.
Проверено -- cda_d_v2cx добывает инфу как надо. (Всю работу по auxsid'ам проделывает Cdr, передавая cda готовые имена доп.серверов.)
Осталось только работу с physinfo организовать.
25.07.2014: работа с physinfo сделана в начальном варианте -- всё, кроме поддержки "global_physinfo_db" (её надо понять, в какой момент прикручивать; а прикручивать надо к main-sid'у).
После обеда (и пляжа): и "global_physinfo_db" сделана:
stripped_cda_register_physinfo_dbase()
.
cda_add_auxsid()
'е, плюс...
GetSubsysID()
-- иного
места просто нет.
Проверено на liu (там все стойки на своих "defserver=").
29.07.2014: доделана часть установления/восстановления связи с сервером и регистрации каналов (с добавлением их в список заказываемых). Копированием из v2.
Итак -- соединение устанавливает, NewDataProc() пощелкивает.
08.08.2014: есть проблема с RGSWCH -- они и в v2 реализованы формулами, так что ссылаться просто не на что.
10.08.2014@утро-душ: ну так пусть v2subsysaccess берет и из формул номера каналов. Синтаксис -- ПУТЬ.ДО.РУЧКИ.@FNN, где F -- r|w|c -- которая из формул, а NN -- какое по порядку из обращений GETP_I/SETP_I использовать.
@день-сдирание-обоев: а еще большие каналы аналогичным образом поддерживать. Например, обращение к параметру -- @bNN.
11.08.2014: да, поддержка формульных ручек сделана по тому проекту. RGSWCH заработали.
P.S. Да и вообще за прошлую неделю использование v2cx:: было поставлено на поток -- окно IST_XCDAC20 выглядит практически как v2'шное, и всё обновляется.
29.08.2014: поскольку и Cdr, и cdaclient теперь по
умолчанию создают ссылки с CXDTYPE_DOUBLE
, то
cda_d_v2cx_snd_data()
научена распознавать данные не только
INT32, но и DOUBLE -- делается round()
. Иначе просто не
работала запись.
06.07.2014@пляж: детали:
Вопрос в API для частичной записи/чтения в cda-v4.
Но более интересный вопрос в другом: как бы эмулировать v4-API "больших каналов" (со связанностями) прозрачно?
...и еще вопрос -- а оно нам реально надо, с такими напрягами, стоит ли игра свеч?
06.08.2014: да, свеч игра стоит -- чтоб Федю целиком перевести с v2::simpleaccess прямо на v4.
10.08.2014: с учётом придуманного в предыдущем подраздельчике синтаксиса @bNN, поддерживать большие каналы довольно несложно.
Свойства -- dtype и nelems -- берутся переданные в new_chan().
(Да, потребуется поддержка от v2subsysaccess'а.)
12.08.2014@пляж: да легко: генерить уникальное имя (с использованием ref'а), а дальше всё обычно.
05.02.2015: да, БУДЕМ делать (именно ради Феди). Некоторые мысли по реализации:
Обычные скаляры работают через обычные же соединения, без описанной с следующем пункте алхимии.
(Теоретически можно бы неявно регистрировать и сам канал, но неясно, с какими свойствами -- dtype, nelems?)
06.02.2015: начинаем.
v2subsysaccess_resolve()
добавлен еще параметр
color_p
-- чтоб возвращала поле color, которое номер bigc.
cda_d_v2cx_new_srv()
теперь вычленяется "истинное"
имя сервера (до 2-го ':', если оно есть) и сохраняется для
будущих DoConnect()'ов.
is_bigc
.
13.02.2015: продолжаем.
cda_d_v2cx_new_chan()
всё касательно больших
каналов пока делается в отдельной ветке.
15.02.2015: кстати, ведь надо еще откуда-то знать ОПИСАНИЕ данных большого канала (dtype, nelems).
Guard'ом можно сделать номер bigc_n -- если он <0, то считать отсутствующим (а при создании сервера прописывать =-1).
18.02.2015: худо-бедно допилено.
Соответственно, "место в буферах" отводится под столько параметров, сколько указано в @vNN.
Теперь проверять и доделывать.
19.02.2015: поддопилено (вчерашнее работать никак не могло, чисто архитектурно).
Проверено на cdaclient -- вроде пашет (ЧТЕНИЕ; запись проверить пока не на чем).
20.02.2015:
goto REGISTER_SCALAR_CHAN
(да,
дико некрасиво, но работает).
05.09.2016@Снежинск-Снежинка-304-утро: некоторые замечания от Роговского:
CX_CACHECTL_SHARABLE
, а хочется ещё и
CX_CACHECTL_SNIFF
.
05.09.2016@Снежинск-каземат-11: пилим:
...если припрёт, то FORCE и FROMCACHE тоже можно будет сделать -- "@f" и "@c".
number=CX_MAX_BIGC_PARAMS
.
Добавлена проверка, что если w_srv
, то пытаемся добыть номер
канала через channame2chan_n()
(она при появлении любой
не-цифры (и не-'.') возвращает -1)); если получилось -- то
v2subsysaccess_resolve()
не делаем.
Проверено -- на rack0:46.4@v, для теста его же "группировочное" имя liu.bpms.:.r0adc812@v -- работает, результаты выдачи из обоих вариантов ссылки совпали.
Теперь ждём проверки Роговским.
08.09.2016@Снежинск-Снежинка-304-после-возвращения-с-полигона: Роговский прислал письмо, что проверил и всё работает как должно.
15.03.2019: вчера вылез косяк: при попытке использовать каналы @b из cdaclient он SIGSEGV'ится.
Проблема сегодня была локализована по телефону через gdb: местом падения
показывалась cda_dat_p_set_strings()
, каковой факт сделал
причину очевидной:
strs
, ...
bzero()
'ится в начале блока, отвечающего за
резловинг имени-через-группировку в канал.
goto REGISTER_SCALAR_CHAN
",
а оная метка стоит ПОЗЖЕ bzero()
.
cda_dat_p_set_strings()
передавался мусор,
вот она и падала.
Решение тривиально: bzero(&strs,)
перетащено в начало
функции. После чего проблема ушла.
Добавлено, почти копированием из старого (модификация -- что передаётся не ASCIIZ-строка, а с параметром len (поскольку там исходный srvrspec опционально шинкуется ради второго ':' (для больших каналов))).
16.02.2015: если подумать, то это решаемо:
cda_dat_p_update_dataset()
не всем каналам
скопом вслепую, а лишь тем, что реально обновились -- стандартным
алгоритмом прохода "смотрим вид первого, потом находим, сколько их
таких, и найдя группу делаем с ней всей одно и то же". Как это
делается тучу раз в cxsd_hw.c (ключевое слово Should
).
А уж прочая архитектура в v4 к такому приспособлена -- речь о Cdr/Chl/Knobs.
Как будем определять позжесть? Сравнением timestamp'ов (уже насчитанных вычитанием тэга), с начальным значением {0,0} или {1,0}?
Но из-за некоторого jitter'а моментов обработки может получиться, что (n+1)-й шаг будет обработан более через размер цикла от n-го, и тогда ВОЗРАСТ получится "позже" предыдущего. Поэтому надо отдельно учитывать еще и значение age: если оно <255 и при этом cur_age>prev_age, то считать как если бы текущее НЕ позже предыдущего (но эта проверка вторая -- только если обычные возрасты катят).
Федя утверждает, что надо.
И в EPICS'е вроде бы тоже отдаётся начально-известное.
Сделать это просто: оставить начальный timestamp {0,0}, тогда даже "устаревший" {1,0} будет позже.
24.04.2015: смысл в том, что Юра использует в v2'шном сервере ring-BPM'ов (ring1:35) каналы (кажется, вычислимые драйверами), которые отдаются как якобы обычные int32, но реально содержат float32. А уж в клиентской программе он их обрабатывает соответственно.
Тут же он захотел мочь видеть такие каналы при помощи das-experiment. А тому ну никак не объяснить, что данные вроде бы int32 надо рассматривать как вещественные.
Так что сделан мелкий хакчок, что модуль при "@f" запоминает у себя
в dtypes[chanofs] значение CXDTYPE_SINGLE
, и потом данные
возвращает наверх с этим типом.
Единственное что, ЗАПИСЬ -- cda_d_v2cx_snd_data()
-- об
этом хаке пока не знает, да и вообще ничего кроме INT32 и DOUBLE не
понимает.
Поэтому сделано, что сборка lib/v2cx/ и cda_d_v2cx.o отключается, если нету .../cx/src/include/cx.h. За это отвечают lib/Makefile и lib/cda/Makefile соответственно (последний ещё с cda_plugmgr.c сотрудничает, чтоб тот не пытался зарегистрировать отсутствующий dat-плагин).
30.11.2020: Юра Роговский обратил моё внимание на то,
что отключаемость сборки в lib/Makefile сделана не вполне
корректно: при НЕотключенности директория v2cx добавляется в КОНЕЦ
списка CSUBDIRS
, а поскольку директория cda зависит от
v2cx, то эта зависимость ниже указывается явно ("cda:
v2cx
"), причём НЕ отключается даже при отключенности самой
директории.
Почему при этом make не выдаёт ошибки и вообще всё проходит "корректно" (я проверял -- собирал 4cx/ без cx/ рядышком) -- загадка. Возможно, он почему-то считает цель "v2cx" за PHONY или просто считает её "свежей" (по причине отсутствия зависимостей и команд/правил?).
По-хорошему --
CSUBDIRS
не в конец, а в серединку, прямо перед cda -- например, поставив
туда какой-нибудь макрос, который и делать "=v2cx
" при
надобности.
Но сначала недурно бы разобраться в причинах ТЕКУЩЕГО поведения.
06.08.2014@вечер-пляж: некоторые мысли по реализации внутренностей:
...или сделать в fdiolib вариант FDIO_STRING, реализующий такие "мозги"? А вся часть connect'а и отправки останется от FDIO_STREAM.
И name указывает на то же strdup()
'нутое поле, куда и
hwrinfo_t.name -- т.е., уже с заменой '.' на '/'.
Поддерживать массивец, видимо, самостоятельно -- без SLOTARRAY.
07.08.2014: по п.2:
qsort()
нам не помощник.
03.04.2015: приступаем к РЕАЛЬНОМУ наполнению.
hwrs_list
жестко
привязан к privrec'у.
Так что --
Короче -- видимо, придётся делать гибрид: менеджмент соединений/reconnect'ов от cda_d_cx.c (или, скорее, родственно remdrv_drv.c), а менеджмент hwr'ов -- от локальных модулей.
*_iohandle
,
а НЕ "fdhandle" и не "fhandle". ВЕЗДЕ. Чтобы и с
cxscheduler'овыми fdh'ами не путать и не с чем иным.
*_fdhandle
.
Да, она работает (вроде бы), но точно не блещет...
...и еще эти мутные "from_new" (которое вроде б давно надо выкорчевать за уже-ненадобностью (и неясно, когда были нужны -- в v2'шном cda их нет, появились, видимо, в v4!!!)) и "is_suffering" (работающее не вполне очевидно как).
07.04.2015: раскопки показали, что "from_new" появилась в cda_d_cxv4.c с самого его создания в начале-середине сентября 2009 года -- видимо, таково было тогда понимание идеологии реконнектов.
08.04.2015: первоначальный вариант заработал -- данные принимаются и отправляются. Пока очень хиленький -- ни ошибок нерезолвимости не умеет возвращать, ни timestamp'ы, ни векторные данные (но они и в протоколе VCAS толком не определены).
09.04.2015: продолжаем.
mktime()
делает конверсию из struct
tm
в ЛОКАЛЬНОЕ время; а timeval-стиль -- UTC! -- должна делать
малопортабельная timegm()
.
Но mktime()
даёт правильные результаты (с точностью до
того бага на 1 час), а как раз timegm()
-- даёт время со
сдвигом на 6 часов. Т.е., как будто в struct timeval'е хранится
всё-таки ЛОКАЛЬНОЕ время, а не UTC'шное.
10.04.2015: далее.
comment
и units
соответственно.
09.04.2017: ЕманоФедя проверил -- ему надо было из своей питонской обёртки взаимодействовать с беркаевским vcas-сервером -- и вроде нормально работает, причём как вещественные каналы, так и текстовые.
10.04.2017: вся отладочная печать (на stderr) закомментирована, чтоб не портить вывод.
17.05.2023: проявился позорный ляп в
ProcessInData()
: оно отдавало значение канала с rflags, которые
нигде не инициализировались.
Проявилось у ЕманоФеди на бридже с ВЭПП-2000 (там длинная цепочка: cxout:1.v2k/bridge->cs-gateway:1.v2k/bridge->vcas::...). Почему не проявлялось раньше -- великая загадка; совпадение раскладки по памяти, которое изменилось в новой системе? Неменьшая загадка в том, что натравливание ТАМ "cdaclient vcas::..." тоже мусора во флагах не кажет.
Решил проблему тривиально: теперь прямо в объявлении делается
"rflags = 0
".
18.05.2023: ЕманоФедя проверил -- да, решение работает.
30.05.2023: а ещё оказалось, что модуль НЕ выдаёт
"положительного подтверждения" -- CDA_RSLVSTAT_FOUND
-- никогда
вообще. На обычную работу cdaclient, bridge_drv и прочих это не влияло (им
достаточно просто прихода данных), а вот epics2cda нужны именно явные
уведомления, иначе канал не будет считаться "существующим".
Сделано довольно тривиально-прямолинейно:
rslvstat_reported
для
сохранения статуса "сообщено".
ProcessInData()
при получении данных в ДВУХ местах
репортится, если rslvstat_reported==0 --
После этого epics2cda начал работать с VCAS-каналами.
А можно указывать формат прям в имени канала -- после '@', без '%', вроде "VEPP5/NO/Energy@8.3f".
И тогда можно оный dpyfmt даже "наверх" отдавать через
cda_dat_p_set_strings()
, чтоб оно сразу в экранах
испльзовалось.
20.05.2019: а ведь давно очевидно, что этот модуль надо уносить в отдельную директорию; в lib/cda/ ему делать нечего -- он ведь наверняка будет зависеть от libca.a (или libca.so).
...если только не делать собственную реализацию сетевого протокола Channel Access (и называть такой модуль типа "cda_d_epics_direct.c"). Но это вряд ли -- совершенно бессмысленная трата ресурсов/трудозатрат; плюс ещё и необходимость постоянно гнаться за актуальной версией/реализацией их протокола, вместо того, чтобы просто использовать готовое.
24.08.2014: вычисление разницы между "EPOCH" *nix и EPICS... Пытался сделать "на лету", при инициализации -- хрен!
mktime()
-- делает это с учётом TZ/DST, а time_t
-- по определению
в UTC.
Добывать так же секунды от 01-01-1970 и уже их вычитать не катит, поскольку в районе 0 секунд будут проблемы с этим самым сдвигом из-за DST.
Так что проще всего взять готовое число
POSIX_TIME_AT_EPICS_EPOCH 631152000u
из libCom/osi/epicsTime.h.
29.05.2019: работы продолжим уже в отдельной директории, а этот подраздельчик замораживаем (хотя он и так был не особо активен :-)).
20.05.2019: вчера погуглил на тему "epics c api". Найденные ссылки:
А из неё -- ссылка на...
Это презентация аж 2004 года, но выглядит вполне прилично и читабельно.
24.05.2019: вышеуказанная PDF'ка дочитана. Общее впечатление положительное, описанный там API хоть и не без странностей, но выглядит годным для оборачивания в CX.
Годность:
28.05.2019: похоже, он существует по штуке на каждый thread.
ca_create_channel()
, грохаются
-- ca_clear_channel()
.
PUSER
("In C++ it is often the
this
pointer for a class"), при подписке --
USERARG
(и передаётся через
event_handler_args.usr
).
Похоже, изначально API был ориентирован на "синхронные" (условно, т.к.
требуют ca_pend_io()
после) одиночные операции (как в CX с
первых версий), поэтому первые описываемые операции --
ca_array_get()
и ca_array_put()
, плюс их вариации
ca_array_get_callback()
и ca_array_put_callback()
,
где указывается callback для вызова по завершении.
07.06.2019: насчёт "с неясностью момента готовности" у
не-_callback-вариантов, ca_array_get()
и
ca_array_put()
-- всё проще: видимо, они БЛОКИРУЮЩИЕСЯ. По
крайней мере, у ca_array_get()
есть параметр
void *PVALUE
, должный указывать на место для складирования
значения; а у ca_array_get_callback()
-- нету, у него только
USERFUNC+USERARG.
20.06.2019: не-не -- скорее всего (по логике и судя по
исходникам в ca/client/oldChannelNotify.cpp), вышеописанное
касается только ca_array_get()
, это только он может быть
блокирующимся; поскольку ca_array_put()
является просто
отправкой, после которой надо вызывать что-то вроде
ca_pend_io()
; а ca_array_put_callback()
отличается лишь тем, что позволяет получить уведомление о завершении записи.
28.06.2019: ещё пообщался на эту тему с Гусевым, и он утверждает, что дело обстоит чуть иначе:
ca_array_get()
, имеющую параметр pValue
. Просто
содержание буфера начинает иметь смысл лишь после того, как будет вызвана
ca_pend_io()
и оная завершится успешно.
ca_array_get()
идёт ca_pend_io()
, что
было бы бессмысленным, будь первый вызов блокирующимся.
Т.е., ЕманоФедины утверждения, что в EPICS всё очень уж тормозно, возможно, вытекают отсюда.
ca_create_subscription()
(с
возможностью указать маску интересующих событий), отписка --
ca_clear_subscription()
; запись -- просто
ca_array_put()
.
Замечание: в описании event_handler_args
есть предупреждение
Do not use the value in dbr if status is not
ECA_NORMAL
В точности, как в CX.
DBR_xxx
; явно можно
сделать маппирование DBR_xxx<->CXDTYPE_nnn.
28.05.2019: но некоторые типы -- весьма специфичные, и
никаких аналогов в CX не имеют. Кроме тривиальных скаляров, остальное --
какие-то странные структуры (DBR_STS_xxx
,
DBR_TIME_xxx
, DBR_GR_xxx
,
DBR_CTRL_xxx
). Хотя нужны ли они в реальности, или просто
древнее наследие?
Но есть функция ca_poll()
, которую рекомендуется вызывать не
реже раза в 100 миллисекунд; она предназначена для ситуаций, когда "your
application has other things to do; E.g. most GUI programs".
Т.е., просто организовываем периодический таймаут, дёргающий её, и всё.
ca_connection_handler_args
: оно передаётся параметром
callback'у, устанавливаемому в ca_create_channel()
.
При вызове указывается текущее состояние, CA_OP_CONN_UP
или
CA_OP_CONN_DOWN
.
"The callback will be called whenever the connection state changes, including when first connected".
Странности:
ca_task_initialize()
-- просто обёртка вокруг
ca_context_create()
.
Смысл переименования не вполне ясен. 28.05.2019: может, из-за того, что это "per-thread context"?
ca_context_create()
.
Например, ca_create_channel()
реальный результат возвращает
в chid *PCHID
.
Пример -- тот самый chid
(это тип-указатель). Объяснение
слегка бестолковое (слайд 21):
PCHID is the address of the chid pointer (Use &CHID)
- You need to allocate space for the
chid
before making the call- Channel Access will allocate space for the
struct
and return the address
С одной стороны -- что-то надо аллокировать, с другой -- аллокирует
libCA. Видимо, дело именно в том, что сам chid
--
тип-указатель.
struct connection_handler_args ARGS
из которой конкретную информацию надо вытягивать макросами
ca_name()
, ca_state()
, ca_puser()
,
...
ca_add_exception_event()
.
29.05.2019: несколько дополнений по типам, по результатам анализа db_access.h.
typedef epicsUInt16 dbr_enum_t;
typedef epicsInt16 dbr_int_t;
...он просто унифицирован с SHORT --
#define DBF_INT 1 #define DBF_SHORT 1 ... #define DBR_INT DBF_INT #define DBR_SHORT DBF_INT
А для 32-битных -- есть long
typedef epicsInt32 dbr_long_t;
Напоминает 1980-е, с x86 и DOS, не правда ли? Странно лишь потому, что EPICS делался для VME/m68k, а оно-то было изначально 32-битным (ещё с конца 1970-х).
ЧеблоПаша говорит, что да -- обычно просто используют DBR_LONG, а 16-битность -- имеет смысл для всяких PLC, где много бит и не бывает.
typedef epicsUInt8 dbr_char_t;
epicsInt64
и epicsUInt64
имеются.
union dbrbuf { ... #ifdef EPICS_VERSION_INT # if EPICS_VERSION_INT>=VERSION_INT(3,16,1,0) epicsInt64 dbf_INT64; epicsUInt64 dbf_UINT64; # endif #endif
...как я понял, версия 3.16, значащаяся в "Downloads" как "Closed", имела отношение к 4.x, который, в свою очередь, мутировал в современную 7.0.x.
Это информация на будущее, если захочется поддерживать PVaccess (cda_d_pv.c?).
typedef epicsOldString dbr_string_t;
/* * !! Dont use this - it may vanish in the future !! * * Provided only for backwards compatibility with * db_access.h * */ #define MAX_STRING_SIZE 40 typedef char epicsOldString[MAX_STRING_SIZE];
ЧеблоПаша говорит, что ему доводилось работать только с небольшими сообщениями, так что проблем не имел.
А ЕманоФедя -- что есть какой-то другой вариант, без ограничения 40.
30.05.2019: может, массив символов? В caget есть ключ "-S" - "Print array of char as a string (long string)".
Но просто анализ .h-файлов показывает, что там кроме собственно данных (точнее, ПЕРЕД ними) передаются атрибуты:
status
и severity
(оба
16bit).
epicsTimeStamp
).
units[MAX_UNITS_SIZE]
(MAX_UNITS_SIZE=8) плюс 3 набора
пределов: {upper,lower}_{disp,alarm,warning}_limit
, все
значения того же типа, что и value.
{upper,lower}_control_limit
.
Видимо, диапазон разрешённых для ввода значений? Если так, то это очень знакомо -- в CXv4 добавился диапазон ALWD.
Т.е., вывод: надо при подписке требовать DBR_TIME_xxx (чтобы иметь timestamp), а в самом начале -- одноразово DBR_CTRL_xxx (чтоб получить свойства).
Указание формата вывода в cdaclient & Co. сделано сильно элегантнее: просто указываешь желаемый формат, и всё.
А в caget -- неочевидная спецификация "-e N" (и аналогично "-f N" и "-g N") -- буква опции указывает формат, а N -- число символов после десятичной точки. Ничего иного там указать нельзя (ни общей ширины поля, ни altformat, ни zero-pad), и формат "%a" никак не поддерживается. И, похоже, ключи указывают общий формат для ВСЕХ интересующих PV'ей (хотя это надо отдельно проверить изучением исходников).
02.07.2019: пообщавшись с Гусевым, в разговоре, на тему "как/когда делается отправка в сокет", случайно узнал крайне ценную вещь:
ca_add_fd_registration()
(да,
название дурацкое).
ca_poll()
.
sl_fdh_t
, то надо будет завести какой-нибудь
"реестр используемых дескрипторов"; напрашивается SLOTARRAY.
P.S. В Xt аналогично -- там же тоже XtInputId
, так что Гусев
держит массив[20] с дуплетами {inputId,fd}.
Итого: преспокойно можно регистрировать libca'шные дескрипторы и иметь реакцию на приход данных сразу же.
...только надо поэкспериментировать на тему сочетания этого с
необходимостью вызова какого-нибудь ca_poll()
после
ca_array_put()
.
07.07.2019: ища информацию насчёт EPICS4/EPICS7, наткнулся на некоторое количество информации касательно "классического" EPICS3/ChannelAccess.
cd ~/epics-train/examples/first_steps cat first.db softIoc -m S=training -d first.db
Вопрос: что это за "first_steps" и "first.db"? Где их взять?
09.07.2019: насчёт документации по протоколу CA:
This is an old version of the Protocol Specification. Newer versions are available from the pages specific to each version of Base.
При нём значится версия 1.5 (словенские закончились на 1.4 от 2008-02-07), автором там Michael Davidsaver, комментарий "Major revision to describe operation semantics". И да, есть картинка "TCP Message Flow", показывающая диаграмму переходов между состояниями.
29.05.2019: организационное -- о структуре директорий и системе сборки.
frgn4cx | +-epics/ | +-cda/ cda_d_epics.o, cda_d_epics.so | +-srv/ cxsd_fe_epics.o, cxsd_fe_epics.so | +-util/ epicscdaclient +-tango/ | +... +-daqmx/ | +... +-dstp/ | +... +-all/
В daqmx/, очевидно, директории srv/ не будет (а в dstp/ теоретически быть могла бы).
Последняя директория, сейчас проходящая под условным обозначением "all" -- для сборки варианта cdaclient, содержащего ВСЕ возможные плагины.
В частности, тут на данный момент --
EPICS_BASE_DIR= /tmp/base-3.15.6 EPICS_INCLUDE_DIR= $(EPICS_BASE_DIR)/include
Итак, "вариант 2014 года" на новом месте собирается, теперь пытаемся идти дальше.
#include "cadefs.h"
.
In file included from /tmp/base-3.15.6/include/cadef.h:40:0, from cda_d_epics.c:14: /tmp/base-3.15.6/include/epicsThread.h:214:23: fatal error: osdThread.h: No such file or directory #include "osdThread.h" ^ compilation terminated. make: *** [cda_d_epics.dep] Error 1
Возможно, там не так всё просто, и оно требует ещё каких-то дополнительных
телодвижений -- либо самому делать -I$(EPICS_INCLUDE_DIR)os/Linux
(искомый osdThread.h находится там), то ли include'ить какой-нибудь
файл из $(EPICS_BASE_DIR)
...
05.06.2019: подсмотрел у Гусева -- явно делается
именно -I$(EPICS_BASE)/include/os/Linux
.
-I$(EPICS_INCLUDE_DIR)os/Linux
. Та ошибка
ушла.
In file included from /tmp/base-3.15.6/include/epicsTypes.h:19:0, from /tmp/base-3.15.6/include/caerr.h:31, from /tmp/base-3.15.6/include/cadef.h:48, from cda_d_epics.c:14: /tmp/base-3.15.6/include/compilerDependencies.h:19:30: fatal error: compilerSpecific.h: No such file or directory #include "compilerSpecific.h" ^ compilation terminated. make: *** [cda_d_epics.dep] Error 1
А compilerSpecific.h есть в туче мест. В первую очередь -- в src/libCom/osi/compiler/*/, из которых в "результирующую include/" (которая как бы в "образе для инсталляции") попадает только gcc'шный, но всё равно в свою отдельную директорию include/compiler/gcc/ (нюанс: при копировании timestamp НЕ сохраняется!).
Я что, должен ещё и эту директорию руками в -I
указывать?!
If you do not use the EPICS build environment (layered make files) then it may be helpful to run one of the EPICS make files and watch the compile/link lines. This may be the simplest way to capture the latest system and compiler specific options required by your build environment. Some snapshots of typical build lines are shown below, but this information may be out of date.(bold мой).
Как именно использовать этот "EPICS build environment" сторонним программам -- там не сказано.
И за пределами src/ упоминаний "compiler/gcc" нигде не нагрепилось -- т.е., в ихнем "образе для инсталляции" никакого .mk-файла для include'нья сторонними программами -- НЕТУ!
...господи, ну и придурки!!!
#include "cadef.h"
отдельно явно сказано
This header file is located at "<EPICS base>/include/". It includes many other header files (operating system specific and otherwise), and therefore the application must also specify "<EPICS base>/include/os/<arch>" in its header file search path.(bold опять мой).
3.14ц... Просто охренительное "качество" системы сборки...
Его версия -- потому, что в одном дереве base*/ может вестись сборка под разные платформы одновременно, потому "приложение должно знать, подо что оно собирается".
06.06.2019: наполняем модуль функционалом.
Это отражается, в частности, в модели менеджмента hwr'ов: тут список привязан к sid'у, а не один сквозной по всем sid'ам.
12.06.2019: а вот теперь переделан на сквозной.
07.06.2019: продолжаем наполнять.
DBR2cxdtype()
-- возвращает "класс" DBR'а (PLAIN, STS,
...) и соответствующий cxdtype.
cxdtype2DBR()
-- обратное преобразование.
alarm2rflags()
-- в зависимости от alarm_severity
выбирает значение rflags. Соответствие записано в таблице
alarm2rflags_table[]
; пока там сплошные 0.
13.10.2024: переименованы в
alarm_status2rflags()
и
alarm_status2rflags_table[]
. Кстати, табличка первоначально
наполнена ещё 11-06-2019.
С соответствием типов есть небольшая загвоздка: из-за дурацкой 40-символьности DBR_STRING точного соответствия нету, поэтому строки пока никак не обрабатываются -- в обоих направлениях возвращается "упс!".
Плюс, в любом случае не помешает завести там какой-нибудь .mk-файл, описывающий правила сборки утилит (в частности, потребность обоих в console_cda_util.o).
cxdtype2DBR()
могла возвращать РАЗНЫЕ коды
DBR_ccc_ttt -- обычные, TIME, ... Поскольку в разные моменты нужно
заказывать разное.
08.06.2019@дома: продолжаем.
cxdtype2DBR()
добавлен параметр DBR_class
-- так что она теперь полностью парна к DBR2cxdtype()
.
hwrinfo_t.dtype
, куда в
cda_d_epics_new_chan()
сохраняется тип -- чтобы потом
запрашивать данные нужного DBR_-типа.
NewDataCB()
: при DBR_CHAR можно смотреть по
hi->dtype
.
...впрочем, проблемы "как обходиться с DBR_STRING, которая всего [40], но это 40 -- при count=1" это не решает.
11.06.2019: далее.
NewDataCB()
.
dbr_time_NNN
->status
или
->severity
?
Гусиный код берёт из status
. Но почему так, и в чём вообще
смысл этих ДВУХ полей и различий между ними -- неясно. Карнаев не смог
ответить, отправил к ЧеблоПаше или Гусеву.
12.06.2019: написано вопросительное письмо ЧеблоПаше, ждём ответа.
20.06.2019: давно посетила идея: похоже, ихнее severity --
это вроде CX'ного knobstate_t
, т.е., "высокоуровневое"
состояние, вычисляемое из аппаратного кода состояния.
Как поступим?
Единственным разумным решением выглядит перейти на СКВОЗНУЮ нумерацию hwr'ов, а не привязанную к серверу.
12.06.2019: ...
hwrinfo_t.me
, являющееся
ссылкой на сервер-"содержатель".
Заполняется в AddHwrToSrv()
.
12.06.2019@вечер, около 18:10, пешком по Лаврентьева вниз: насчёт "как обходиться с DBR_STRING, которая всего [40]..." -- можно требовать помечать такие каналы суффиксом "@s", и тогда модуль будет сохранять оный факт булевским флагом в hwrinfo и модифицировать своё поведение при получении и отправке данных.
14.06.2019: ...
StateChangeCB()
сделана. Она:
GetPropsCB()
cda_dat_p_report_rslvstat()
.
А не надо ли ввести per-hwr-флажок "запрос на CTRL послан", который взводить перед заказом и сбрасывать при получении? Смысл -- чтобы при обрыве соединения ДО получения CTRL-ответа и последующем восстановлении запросы бы не множились.
GetPropsCB()
: там разбирается, что
пришло, и возвращается наверх units (остальные 7 строк -- NULL) и диапазон.
Отдельно стоит упомянуть пару типов-нюансов:
Теоретически можно было бы сгенерить multistring "items", из dbr_ctrl_enum.strs[], но его даже возвернуть нечем -- нет в cdaP вызова для этого.
20.06.2019: после почти недельной паузы (вызванной вознёй с DAQmx) возвращаемся сюда.
cda_d_epics_snd_data()
наполнена.
Засим "примерный план" от 07-07-2019 выполнен, и модуль вроде должен быть готов к использованию (точнее, к тестированию). Естественно, с понятными ограничениями/недоделанностями:
24.06.2019@утро-дома, перед выходом на работу: ещё идея на тему "как обходиться со строками, которые в варианте DBR_STRING всего [40]":
-S
" у caget ("Print array of char as a
string (long string)"), строки передаются именно как массив char'ов.
25.06.2019: да-а-алее...
cda_d_epics_init_m
НЕ делалось
cda_register_dat_plugin()
.
Сделано.
**** The executable "caRepeater" couldn't be located **** because of errno = "No such file or directory". **** You may need to modify your PATH environment variable. **** Unable to start "CA Repeater" process.
cda_d_epics_term_m()
,
РАЗрегистрирующий модуль и вызывающий ca_context_destroy()
.
Но это так, для порядку. Вряд ли он будет использоваться.
Ура -- работает!!!
Некоторые детали:
Данные печатались на экране одновременно (хотя 0.1с разницы глазом бы всё равно не заметить).
Что любопытно, данные тех 3 каналов приходили не равномерно, а с чуть плавающей периодичностью, иногда с паузами, и не в конкретном порядке (мог два раза подряд придти 1-й, или 1,2,3,2,...). Но в обоих окнах всё было СИНХРОННО!
-DM
) и "строки", в лице одной только units (ключ
-DS
).
Что ж -- это успех! Заработало с первой же попытки :-).
Немного статистики по этой первой версии: всего 456 строк (13750 байт) в cda_d_epics.c плюс 103l/3451c в epics2cx_conv.h.
Дальнейшие действия -- что ещё проверить:
Попробовал, указать префикс @i:: ну возвращаются целые, да.
Вариант @b: показал какое-то странное число -- "-15". Но это уже прикол с конверсией (точнее, отображением) на стороне CX. Т.к. при указании @+b: отобразилось уже более осмысленное "241".
ca_poll()
.
27.06.2019: проверяем запись. Тестируем на гусином CVM, пытаясь записать что-нибудь в поле V5:PA:PhaseVoltage1M.HOPR (изначально =300).
Фиг -- никакой реакции. И в варианте "cdaclient -w" -- тоже.
28.06.2019: продолжаем разбирательство.
Писабельно -- caput туда 400 занёс.
Для проверки кроме записи в список каналов добавлен ещё один читаемый -- чтобы программа сразу не завершалась.
И всё равно фиг -- не отрабатывается.
ca_array_put()
ещё
ca_pend_io(5.0)
(чисто для теста).
Тоже не помогло, и никаких 5 секунд явно не ждало.
Значит, нужно поставить отладочную печать и посмотреть, что передаётся.
Поставил, и...
cda_d_epics_snd_data()
просто не
вызывается!!!
Т.е., косяк не в работе с EPICS, а где-то внутри cda, в связке cda_core<->cda_d_epics -- но это намного лучше, т.к. уж в своём произведении разобраться проще!
И это -- cda_dat_p_set_ready()
-- вызывается ОТДЕЛЬНО от
cda_dat_p_report_rslvstat()
(который вообще реально влияет
только на "внешний вид" -- timestamp и флаги, но НЕ на функционирование).
Ну, добавил. Не помогло...
И -- да, cda_d_epics_new_srv()
возвращал
CDA_DAT_P_NOTREADY
; осталось ещё с 2014 года.
Заменил на CDA_DAT_P_OPERATING
-- аллилуйя, заработало!!!
Запись работает!!!
ca_pend_io()
:
(По отладочной печати видно, что сначала "производится" запись, а лишь
потом вызывается StateChangeCB()
с op=6:CA_OP_CONN_UP.)
Выводы:
ca_poll()
.
Но ещё надо бы посмотреть, насколько "оперативно" выполняется запись: если через 0.1с (максимум, в среднем -- 0.05с), то это нехорошо.
Очевидно, он просто отбрасывает запрос на запись в ещё неприконнекченный канал.
02.07.2019: пообщался с Гусевым на эту тему.
ca_array_put()
ошибки не
даёт, а ругается следующий за ним ca_pend_io()
.
ca_array_{put,get}()
сами ничего не
пишут, а лишь ставят запрос в очередь.
Ну ладно -- просто примем к сведению, что в EPICS'е писать в ещё неприконнекченные каналы не положено.
Итого: запись скаляров работает. Дальше надо будет проверять на чём-нибудь повекторнее -- может, на ВЭПП-4 что будет.
28.06.2019: кстати, посмотрел на запущенную программу epics_cdaclient на тему "сколько thread'ов и куда лезет".
ps axuwww -L
" показал аж 6 thread'ов.
netstat -anp
"): один
TCP, с сервером, и аж 2 штуки UDP.
Также попробовал "поиграться с периодичностью вызовов
ca_poll()
". Поставил вместо 0.1с целых 5с.
ca_poll()
-- после старта.
Т.е., cdaclient'у было указано писать в канал HOPR, но также и
мониторировать его (в т.ч. с опцией PRIVATE ("@-:"). И своего
обновления он НЕ увидел.
Чуть позже: а, нет -- увидел! Но о-о-очень сильно позднее: через 2 цикла (10с) после получения первого обновления. И даже без PRIVATE работает.
28.06.2019: ещё -- не удержался, занялся поддержкой timestamp'ов.
Поскольку в CX используются просто POSIX'ные, с EPOCH=01.01.1970-00:00:00, то поступил по-простому: секунды берутся как
timestamp.sec = epics_stamp.secPastEpoch + POSIX_TIME_AT_EPICS_EPOCH
(а поле nsec просто копируется).
Сделать-то сделал, а вот проверить -- никак: ведь cdaclient печатает timestamp'ы числом (а не человекочитабельной строкой), а pult, умеющий показывать человечье время, пока не содержит модуля cda_d_epics...
Проверил командой/утилитой date
, умеющей конвертировать
числовые спецификации в человекочитабельные. Так вот, пример --
@1561718268.807468, команда
date --date='@1561718268'
дала результат "Fri Jun 28 17:37:48 +07 2019", что совпадает с
camonitor'овским "2019-06-28 17:37:48.".
P.S. Прикол в том, что date'овская спецификация -- число, префиксированное символом '@'. А cdaclient именно так timestamp'ы и печатает!
02.07.2019: заюзываем
ca_add_fd_registration()
.
ca_add_fd_registration()
есть в
"EPICS
R3.14 Channel Access Reference Manual, #ca_add_fd_registration".
cda_d_epics_init_m()
сразу после создания контекста
регистрируется "обработчик"...
FdRegistrationCB()
: при opened
==1 он
регистрирует дескриптор у cxscheduler'а, иначе -- разрегистрирует.
fd_info_t
{fd,handle}. Полем "in_use" работает
fd
.
HandleCaInput()
очень прост --
состоит из вызова ca_poll()
.
cda_d_epics_term_m()
добавлена куча действий по разной
"разрегистрации":
ca_context_destroy()
выполняется
ca_add_fd_registration(NULL,)
.
Это по рекомендации мануала "Specifying USERFUNC=NULL disables file descriptor registration (this is the default)." (т.е., видимо, там не "callback LIST", а всего лишь один "hook" (плюс user-pointer)).
DestroyFdiSlotArray()
.
DestroyHwrSlotArray()
.
Результаты:
ca_poll()
сразу после ca_array_put()
понять не удалось:
ca_pend_io()
либо ca_poll()
не
видна.
Будем наблюдать и экспериментировать дальше (на живом использовании посмотрим).
В общем -- полезная фича, спасибо Гусеву за подсказку.
(Надо ж на чём-то тестировать cda_d_epics!)
11.06.2019: краткий манифест:
$(FRGN)
(в данном случае она =epics).
12.06.2019: либо, возможно, вытащить определение TOPDIR=
из PrjRules.mk в отдельный PrjDefs.mk, чтоб его можно было
include'ить отдельно, ещё ДО "include $(PRJDIR)/PrjRules.mk
".
Что удалось и какие проблемы:
12.06.2019: забэкслэшить #, превратив его в
\#
?
20.06.2019: да, так и сделал -- помогло.
21.06.2019: допиливаем собираемость.
Тут-то стало ясно, что первоначальная идея делать в Makefile определение вида
и затем в файлах, могущих быть так твикнутых (cdaclient.c etc.),SPECIFIC_DEFINES=-DBUILTINS_DECLARATION_CODE='#include"main_builtins.h"'
-- не катит никак.#ifdef BUILTINS_DECLARATION_CODE BUILTINS_DECLARATION_CODE #endif /* BUILTINS_DECLARATION_CODE */
Проблема в том, что НЕЛЬЗЯ из макроса вызвать #include
.
Поэтому схема была заменена на более обычную:
иSPECIFIC_DEFINES=-DBUILTINS_DECLARATION_H_FILE='"main_builtins.h"'
#ifdef BUILTINS_DECLARATION_H_FILE #include BUILTINS_DECLARATION_H_FILE #endif /* BUILTINS_DECLARATION_H_FILE */
LD_LIBRARY_PATH=~/compile/base-3.15.6/lib/linux-x86_64
Всё -- первоначальный вариант собирается и запускается. Теперь можно проверять функциональность.
23.06.2019@дома-ванна: если подумать, то ЮЗЕРАМ нафиг не сдалось иметь отдельные утилиты, им нужна обычная "cdaclient", чтобы умела работать с EPICS, и чтобы "обычная" libcda.a, прилинкованная к программе, умела бы доступаться к EPICS/TANGO/...
Напрашиваются следующие пути:
Главный вопрос даже не в загрузке модуля (технология отработанная на драйверах), а в том, КАК УКАЗЫВАТЬ на необходимость загрузки?
Не катит, юзерские-то программы никаких ключей понимать не станут.
В принципе, нормуль. Хотя вопрос секьюрности... (сбрасывать при UID!=EUID?)
Кстати, к вопросу о "просто использовать": желательно бы всё-таки EPICS-специфичности влинковывать статикой (поскольку в конечной системе .so'шки могут лежать где попало, не иметь туда LD_LIBRARY_PATH'а, а то и вовсе быть от другой версии (тогда вообще возможен абзац)).
Сказано -- сделано. Требования к системным библиотекам узнаны путём сравнения вывода ldd от программ с указанным и неуказанным LD_LIBRARY_PATH. В результате получились следующие 2 альтернативы (сверху -- динамическая, снизу -- статика):
ifeq "1" "0" EPICS_LIBS= -L$(EPICS_OS_LIB_DIR) \ -lca -lCom else EPICS_LIBS= $(EPICS_OS_LIB_DIR)/libca.a \ $(EPICS_OS_LIB_DIR)/libCom.a \ -lpthread -lreadline -lrt -lstdc++ -lgcc_s -ltinfo endif
Нафига этой сладкой парочке (libca и libCom) нужны readline и tinfo (кусок ncurses) -- загадка, в самих исходниках src/ca/ слова "readline" не найдено (а в библиотеке -- уже есть); но их жаждут они обе.
Как бы то ни было -- собирается в обоих вариантах, и статический запускается независимо от наличия .so'шных библиотек.
14.04.2025: как тогда собиралось в обоих вариантах --
сейчас уже и не упомню. В настоящий момент в
frgn4cx/epics/cda/Makefile имеется
"LOCAL_LDFLAGS=-L$(EPICS_OS_LIB_DIR) -lca -lCom
",
а EPICS_LIBS
нигде не используется. Поэтому её определение
закомментировано, чтоб не смущать своим наличием.
23.06.2019@дома-вечер: кстати, к вопросу о том, что в frgn4cx/epics/FrgnRules.mk приходится хардкодить вещи вроде "lib/linux-x86_64".
OS=LINUX
и CPU=X86_64
.
Может, из этой информации можно что-нибудь выжать?
Скрипт коротенький и простенький; он и выдаёт "linux-x86_64".
...естественно, с надлежащими "предосторожностями" в виде проверки наличия скрипта.
плюс код возврата 255..../src/tools/EpicsHostArch.pl: Architecture 'e2k-linux' not recognized
$(OS)
и $(CPU)
вроде бы есть, и можно б было их использовать, только об-lowercase'ив. Для
чего в frgn4cx/epics/FrgnRules.mk было добавлено
("tolower_OS:= $(shell echo $(OS) | tr '[:lower:]' '[:upper:]') tolower_CPU:= $(shell echo $(CPU) | tr '[:lower:]' '[:upper:]')
:lower:
" и ":upper:
" вместо "a-z
" и
"A-Z
" использовано по найденному "makefile uppercase" рецепту
--
Stackoverflow,
GitHub.)
Но тут тоже облом: FrgnRules.mk include'ится ДО наличия
Config.mk, что было б ещё полбеды; но главная проблема -- и
самостоятельно Config.mk за-include'ить нельзя, т.к. и
$(TOPDIR)
нету, поскольку $(PRJDIR)/PrjRules.mk
включается ПОЗЖЕ. Поэтому до конца дело не доведено -- hardcoded осталось.
14.04.2025: а определения tolower_OS
и
tolower_CPU
закомменчены, чтоб не смущать своим наличием.
24.06.2019: продолжаем.
Несколько неудобно устроено, что сборка/очистка в frgn4cx/ делается не автоматически (make в верхнем узле), а индивидуально в каждой поддиректории (как минимум -- в директории под-проекта (frgn4cx/epics/).
Поэтому пытаемся сделать, чтобы оно САМО определяло возможность собирать, а при отсутствии -- отключало бы.
Но делать это можно в директории ПОД-проекта, а не в самой frgn4cx/.
SUBDIRS
директории под-проектов добавляются безусловно.
$(EPICS_INCLUDE_DIR)/cadef.h
-- если есть, то делается
SUBDIRS= cda util
, а иначе генерится $(warning
...
.
EPICS_BASE_DIR=...
из командной строки.
$(DAQMX_INCLUDE_DIR)/NIDAQmx.h
(сейчас DAQMX_INCLUDE_DIR=/tmp).
Жульничество заключается в том, что при сборке в системе с установленным libnidaqmx-devel*.rpm всё будет лежать в системных директориях, поэтому просто так фиг что проверишь.
26.06.2019: вылезла проблемка -- при "make
maintainer-clean" и прочих сначала очищается поддиректория cda/, а
затем util/, и там оно пытается заново изготовить
../cda/cda_d_epics.d (только что удалённый), и обламывается по
ошибке компиляции -- поскольку никакого -I*/base-*/include
там
не делается.
Разбираемся и решаем проблему.
*_COMPONENTS
указан ../cda/cda_d_epics.o, вот
GeneralRules.mk и пытается обращаться с ним как с обычным
компонентом, для которого надо мониторить исходники и проверять зависимости
объектников.
При подготовке .a'шки также была переименована .so, из cda_d_epics.so в libcda_d_epics.so. И тут началось... Она просто перестала собираться -- make тупо ничего не делает!
Пытаемся разобраться:
libcda_d_epics.so: cda_d_epics.o # Implicit rule search has been done. # File does not exist. # File has been updated. # Successfully updated.
Выглядит шизоидно: файла нет, но он якобы "updated"!
%.so: %.o
в
GeneralRules.mk.
Как-то уж так с давних пор исторически сложилось, что все собираемые в CX (ещё с первых версий, когда только была задействована динамическая загрузка) .so'шки -- обязательно имеют одноимённый .c-файл.
cxsd_fe_starogate.so_COMPONENTS
; причём, видимо, не особо
нужное).
27.06.2019: разбираемся с проблемой сборки libcda_d_epics.so.
LOCAL_GNTDFILES
.
27.06.2019: к вопросу "зачем нужен readline для libca?": спросил Гусева. Он припомнил, что сам с этим сталкивался (и собирал ncurses под Moxa/ARM, где это было лишней работой), но причину назвать не смог. 30.03.2023: так очевидно же -- редактирование командной строки, которая есть у IOC'а (он ведь "shell"). Но зачем оно именно libca -- хбз...
И, быстренько погрепив в configure/, указал на файл configure/os/CONFIG_SITE.Common.linux-x86, содержащий такой фрагмент:
# Depending on your version of Linux you'll want one of the following # lines to enable command-line editing and history in iocsh. If you're # not sure which, start with the top one and work downwards until the # build doesn't fail to link the readline library. If none of them work, # comment them all out to build without readline support. # No other libraries needed (recent Fedora, Ubuntu etc.): COMMANDLINE_LIBRARY = READLINE # Needs -lncurses (RHEL 5 etc.): #COMMANDLINE_LIBRARY = READLINE_NCURSES # Needs -lcurses (older versions) #COMMANDLINE_LIBRARY = READLINE_CURSES
Т.е., типа можно всё закомментировать, и тогда readline не понадобится.
28.06.2019: попробовал -- фиг, ничего не изменилось.
LDLIBS_READLINE = -lreadline
"
нашлось в CONFIG.Common.linuxCommon.
Там хоть и есть в комментах странная оговорка "Link libraries controlled by COMMANDLINE_LIBRARY", но никаких указаний на возможность отключить readline полностью уже нет.
И вот ЭТОТ файл уже используется -- судя по access time.
30.03.2023: нашёл рецепт: надо
было В КОМАНДНОЙ СТРОКЕ make указать
"COMMANDLINE_LIBRARY=EPICS
".
Указание же make'у параметром в командной строке имеет приоритет.
LDLIBS_READLINE =
": это определяет некое значение, ОДНО ИЗ
LDLIBS_*
, а какое из них выбрать -- определяется уже значением
$(COMMANDLINE_LIBRARY)
, при помощи конструкции вида
"$(BASENAME_$(VARIANT_NAME))
".
В частности, в src/libCom/osi/Makefile есть конструкция
а в configure/CONFIG_COMMON --epicsReadline_CFLAGS += -DEPICS_COMMANDLINE_LIBRARY=EPICS_COMMANDLINE_LIBRARY_$(COMMANDLINE_LIBRARY) epicsReadline_INCLUDES += $(INCLUDES_$(COMMANDLINE_LIBRARY))
(ага, там же и умолчание ставится; в какой момент include'ится этот CONFIG_COMMON -- я не разбирался).# Command-line input support default COMMANDLINE_LIBRARY = EPICS OP_SYS_LDLIBS += $(LDLIBS_$(COMMANDLINE_LIBRARY)) OP_SYS_LDFLAGS += $(LDFLAGS_$(COMMANDLINE_LIBRARY)) RUNTIME_LDFLAGS += $(RUNTIME_LDFLAGS_$(COMMANDLINE_LIBRARY))
Т.е., то определение LDLIBS_READLINE
-- это значение,
которое использовать в варианте, когда значение
COMMANDLINE_LIBRARY
равно "READLINE".
...в конце концов -- задолбало меня разбираться с запутанной EPICS'ной системой сборки, так что просто примем как данность, что эта хрень требует readline.
01.10.2019: увольняем все директории frgn4cx/*/util/ -- теперь они не более нужны, в связи с переходом на динамическую загрузку dat-плагинов, так что "obsolete".
Типа эпитафии: они сослужили свою службу, позволив протестировать и отладить плагины. А информация из данного раздела -- их наследие -- может оказаться полезной и в будущем.
06.05.2023: уж чтобы поставить точку в вопросе "зачем нужен readline для libca?" -- решил изучить вопрос подробнее:
#include
'ить что-либо оттуда:
egrep -r "echo `ls /usr/include/readline`| sed 's/ /|/g'" compile/base-3.15.9
Результат -- ТОЛЬКО файл src/libCom/osi/os/default/epicsReadline.c;
The core of iocsh is a part of libCom.
Мда... Это прямо "As if some IP implementation included an integrated ksh".
This is a side-effect of the convention we use to support both dynamic and static linking with one configuration. Since static libraries do not carry dependency information, when linking statically it is necessary to give a complete list of all dependent libraries when linking any executable. This includes libreadline when libCom depends on it. One simple way to avoid this over-linking in the dynamic case by passing "-Wl,--as-needed" when linking. (assumes GCC or clang on an ELF target)
...правда, из этого ответа никак не следует то, ЗАЧЕМ вообще libCom зависит от libreadline.
Но сделаем это чуть позже.
01.10.2019: сделано.
Может, использовать какой-нибудь make-параметр, указывабельный в командной строке -- например, FORCE_EPICS_PRESENCE и FORCE_TANGO_PRESENCE? Чтобы при их наличии НЕ определяло бы свои пути в FrgnRules.mk.
Задача настолько глобальна (и вместе с тем обособленна), что требует своего собственного раздела.
Последние пару дней занимался обдумыванием, как бы лучше организовать, и анализом того, что же и как устроено в EPICS/CA касательно строк и enum'ов.
В конце концов принят следующий проект:
Да, ограниченновато, но см. п.2.
(Тогда, например, можно будет объявлять каналы как TEXT, а "внутри" запрашивать у CA векторные DBR_CHAR; впрочем, эта часть идеи до конца не оформилась.)
Вчера такое преобразование было реализовано.
(Также была мысль "выбирать разные варианты в зависимости от nelems: <40 -- DBR_STRING[1], >=40 -- DBR_CHAR[nelems]. Да, пришлось бы чуток поменять API функций наведения соответствия между типами, и стало бы кривовато.)
06.03.2020: делаем. Оказалось весьма просто:
DBR2cxdtype()
: теперь при
dbr_type_is_STRING()
выбирается CXDTYPE_TEXT
.
cxdtype2DBR()
: тут обратно -- CXDTYPE_TEXT
возвращает base+DBR_STRING
.
NewDataCB()
чуток нетривиально:
value_p
на начало данных сделана была ешё
изначально, прошлым летом -- полностью аналогично прочим.
ARGS.count==1
), а для CX -- это вектор, некоего размера.
Следовательно, тут брать значение ARGS.count
никак нельзя.
nelems
уставляется в sizeof(...value)-1, а затем в цикле уменьшается либо до 0,
либо пока не наткнёмся на символ !='\0'.
09.03.2020: проверил -- фигово это работает! Потому что
вполне может быть всякий мусор уже ПОСЛЕ значащих символов и терминирующего
их NUL'а, но ДО (40-1)-го символа. Поэтому переделано: теперь
nelems
считается не с конца, а с НАЧАЛА -- до ПЕРВОГО
'\0', а не до последнего.
GetPropsCB()
на
!=DBR_CTRL_STRING
делать не пришлось, т.к. оно УЖЕ было в
StateChangeCB()
, где это чтение свойств заказывается.
Проверяем:
VEPP3:TableName-RB
).
VEPP3:Polarity-RB
-- тоже работает!!!
Т.е., уже сам EPICS для ENUM-каналов выполняет преобразование из числа в строку, если затребован тип DBR_STRING.
Ну что ж -- это уже успех, можно пробовать проброс (через пока что ещё bridge_drv) каналов ВЭПП-3/ВЭПП-4 в CX.
11.04.2022: да, теперь оно уже и на пульту ИК ВЭПП-5 занадобилось и БУДЕТ работать.
"Будет" -- потому, что там оказался древний вариант
cda_d_epics.so, аж от 2019-09-22 (а поддержка строк была добавлена
полугодом позже). И вот он при попытке обращения к текстовым каналам
возвращал -1/EINVAL (причём, что любопытно, как раз на том самом
VEPP3:TableName-RB
:D). Но, что приятно, доступ к числам --
прекрасно работал.
Ну теперь уж точно "done".
08.07.2022: подозреваю, что нифига не "done" -- ведь
поддержка ЗАПИСИ-то в cda_d_epics_snd_data()
некорректна: оно
скармливает ca_array_put()
'у прямо заданный
nelems
, а должно быть 1
.
Вообще-то надо бы проверить, как оно себя ведёт, но, скорее всего, плохо.
Для исправления же нужно:
nelems
=1 -- это самое тривиальное.
nelems
<40 -- точнее,
sizeof(dbr_string_t)
-- то копировать имеющийся размер в
ЛОКАЛЬНЫЙ буфер, забивать недостачу NUL'ами, и уже его потом отправлять.
Смысл в том, что если клиент пишет строку короче 40 символов, то libCA в процессе вычитывания ВСЕГДА 40 мало того, что вычитает ещё и мусор, но может и вообще за границы аллокированной памяти вылезти и SIGSEGV'нуться.
Сделано; при DBR_STRING:
nelems
=1,
nelems
меньше 40,
то указанное количество копируется в локальную
dbr_string_t local_string_buf
,
оставшиеся байты нулятся, и перекидывается
value = local_string_buf
.
Надо будет найти время проверить (в т.ч. и неработу старого варианта).
18.07.2022: проверил -- причём не на настоящем IOC'е, а на cxsd_fe_cx :D Итак:
ca_array_put() failed: Invalid element count requested
До двоеточия -- наша диагностика, после -- результат
ca_message(stat)
, что соответствует коду
ECA_BADCOUNT
.
Дальше разбираться было проблематично -- потому, что ход исполнения в тамошних исходниках не особо очевиден: там вызовы не функций, а методов; плюс, exception'ы, которые фиг знает кем генерятся.
Но вроде бы:
ECA_BADCOUNT
образуется в
ca/client/oldChannelNotify.cpp::ca_array_put()
в
результате
catch( cacChannel::outOfBounds & )
comQueSend::insertRequestWithPayLoad()
, ...
strlen ( pStr ) + 1u
-- ограничения
MAX_STRING_SIZE
Кстати, судя по содержимому того
comQueSend::insertRequestWithPayLoad()
, с
strlen()
, добивание строки до гарантированно-40-байт не
требуется. Но оставим -- так гарантированно правильно,
безотносительно деталей реализации libCA.
03.05.2023: а исчо в
cda_d_epics_new_chan()
остался ЗАКАЗ
ca_create_subscription()
'у значения параметра
count
по числу max_nelems
; в
StateChangeCB()
-то просится 0 (это для случая UNKNOWN -- когда
тип определяем по информации от сервера), а при создании канала -- вот такой
бред...
Подправил -- при DBR_STRING
форсится count равный
1
.
CDA_CTX_R_CYCLE
.
Сделана.
18.07.2022: зачем оно:
При этом мониторинг тех же каналов cdaclient'ом прекрасно работал.
Долго пытался сообразить, что же может быть не так, а потом допёрло: во frontend'е ВСЁ так, а проблема на стороне клиента -- ему нужны события циклов.
Сделано:
cycle_tid
.
cda_d_epics_new_srv()
,
в cda_d_epics_del_srv()
подчистка.
CycleProc()
сначала делает
cycle_tid = -1
(от греха подальше -- ведь дальше cda-вызов с потенциально далёкими
последствиями), затем cda_dat_p_update_server_cycle()
, и в
конце повтор заказа таймаута.
CYCLE_PERIOD_USECS = 1 * 1000000
.
Проверено -- вроде пашет.
Замечание: тут НЕТ отложенной подчистки (being_processed/being_destroyed). Вроде пока надобность не очевидна, но может аукнуться.
22.05.2023: сделана возможность УКАЗЫВАТЬ размер
цикла, в переменной окружения CDA_D_EPICS_CYCLE_USECS
.
cycle_usecs
и заказ делается на содержащееся
там число микросекунд.
CYCLE_PERIOD_USECS
, ...
Берёт примитивным atoi()
, из проверок -- только вгоняет в
диапазон [1-2e9].
Проверено, работает.
Заодно скопировано и в cda_d_tango.cpp (где вообще НИКАКОЙ
поддержки циклов не было), с переменной окружения
CDA_D_TANGO_CYCLE_USECS
. А вот в cda_d_vcas.c -- нет
(ибо без надобности).
cdaclient <-> Channel Access <-> cxsdили, детальнее,
cdaclient <-> cda_d_epics <-> libCA <-> libCAS <-> cxsd_fe_epics <-> cxsd
Странности проявлялись при попытке cdaclient'ом читать и, особенно, писать строковый канал через "epics::", и они таковы:
hwinfo(epics::asdf.2): r? x-1-- что это за "r?" и "x-1"?
Например, "cdaclient -m @t100:epics::asdf.2
" после пары
записей (хоть через "cx::", хоть через "epics::") -- сначала
="asdf1234"
, затем ="asdf"
-- показывает
"asdf1\x7f".
Почему? Первая мысль была -- нет события "обновление", по которому cdaclient и выполняет запись. Но тогда запись не должна бы происходить вовсе, а она всё же идёт -- что её вызывает?
19.07.2022: результаты разбирательства:
CDA_REF_R_RSLVSTAT
(и
CDA_RSLVSTAT_FOUND
), ...
ri->hwr_*
лежит то, что попало при инициализации
reset_hwinfo()
'й: dtype=UNKNOWN, а rw,max_nelems,srv_hwid=-1.
hwinfo_rw
ни 0, ни 1; "x" -- CXDTYPE_UNKNOWN,
"-1" -- просто значение hwinfo_nelems=-1.
Что тут можно сделать? В принципе, "cainfo" ведь многое из нужной информации получать умеет, так что и мы можем -- видимо, надо заказывать какое-то лишнее чтение (каких-то свойств?). Вопрос -- а НАДО ЛИ?
23.10.2022: сделано, cda_dat_p_set_hwinfo()
передаёт добытую информацию.
aisp->installConstBuf((char*)data, nelems, max_nelems)
(nelems: stringLength, max_nelems: bufSize) -- копирует лишь
nelems
байт, оставляя прочее содержимое промежуточных буферов
"как есть", а затем игнорирует значение "stringLength
".
Т.е., проблема -- отсутствие NUL ('\0') в исходном буфере, и
сама cas/gdd его при конверсии
aitString
->aitFixedString
НЕ добавляет.
(По сути, у них в реализации шизофрения: вроде бы длина указывается, но де-факто требуют наличия '\0' ("convertFixedStringString fails to properly NUL-terminate the data").)
20.07.2022@вечер: да,
aitConvert.cc::aitConvertFixedStringString()
-- просто никак не учитывается поле длины, а делается тупой
strncpy(,,AIT_FIXED_STRING_SIZE)
с последующим
fixed_string[AIT_FIXED_STRING_SIZE-1u] = '\0'
.
24.04.2023: заглянул в архивный
baseR3.13.1.tar.gz (он первый, где появляется gdd; а вот libcas без
него -- в 3.12) за 19th Nov 1998 -- код ровно такой же. ЗЫ: а поле len
, судя по содержимому
aitHelpers.h, ВСЕГДА поддерживается в корректном
состоянии.
21.07.2022: была мысль, что, возможно, "мусор" откуда-то
берётся прямо в cxsd'шном буфере -- ведь aitString'у даётся указатель на
него. Проверил, добавив диагностическую печать строки
("%s",data
) в mondata2gdd()
-- НЕТ, на стороне cxsd всё OK: после записи сначала
"qwerty12345" и затем "asdf" в нашем буфере лежит
предсказуемо "asdfty12345", а вот через epics:: читается
"asdft\x7f". Но вот caget отдаёт "asdfty12345" --
как?!?!?!.
22.07.2022: посмотрел WireShark'ом -- стало ещё загадочнее (короткое записывалось "zxcv"): caget'у присылается полная строка "zxcvty12345", а cdaclient'у -- "zxcvt". КАК?!?!?! Но тест надо будет повторить ещё раз -- с разнесением клиента и сервера по разным узлам, чтобы протокол-дамп легче читался. ...и надо отладочной печати повсюду понаставить...
17.11.2022: опять при тестах обратил внимание, что что-то
как-то не так подрезается при записи строки: если после "abcd1234" записать
"987", то отдастся "987d", а если потом "q", то отдастся "q8". ...а
"camonitor asdf.2
" и вовсе показывает мусор после "q8". Чуть позже: а-а-а, это ПЕРВОЕ чтение при запуске camonitor'а;
просто caget показывает тоже мусор, хоть и другой. Наверное, это
результат разных веток исполнения: "первое" -- от read()
,
дальнейшие -- от update()
? Хотя на уровне
cxsd_fe_epics_meat.cpp делается одно и то же
mondata2gdd()
, а разница в том, как до него доходит дело; ну и
самы gdd'ы разные. Но '\0'-терминирования действительно нет, да.
18.11.2022@утро-просыпаясь:
или конкретно для "aitFixedString" делать копирование в
собственный буфер размером 41 байт (размещаемый в том же valbuf?), потом
форсить valbuf[nelems]='\0'
и вертать указатель на него?
25.04.2023: только не 41 байт (зачем бы? при наличии
'\0' в строке-источника strncpy()
дальше него не
полезет), а столько, сколько надо, и не в valbuf[]
, а где надо
-- по адресу current_val
если он !=NULL, и только иначе в
valbuf. И да, БУДЕМ ЭТО ДЕЛАТЬ -- см. раздел по cxsd_fe_epics за сегодня.
CA_HEARTBEAT_USECS
.
20.07.2022: продолжаем разбираться с п.3 -- странностями "scheduling'а" связки cda_d_epics+libca.
...вот только такая "пауза" должна б была быть вечной -- пока не произойдёт реального обновления канала
CA_HEARTBEAT_USECS
: изначально 5000000 микросекунд -- оно и
тормозило на 5 секунд, меняем на 3000000 микросекунд -- 3 секунды тормозит.
ca_add_fd_registration()
работает и дескрипторы
должны вычитываться сразу же при появлении в них данных.
close()
всем
дескрипторам), а другой циклит на futex()
'е).
21.07.2022: а ещё для проверки можно заказать cdaclient'у в дополнение к каналу для записи и ВТОРОЙ канал, только чтения -- чтоб в него обновление пришло.
post*()
в casPV
-- такие,
как, например, access rights change (или оно только в
casChannel
?).
Идея в том, чтобы генерить какие-то ещё события, которые бы вызывали передачу информации клиенту и тем самым пробуждение по файловым дескрипторам.
@~17:40, М-46: "есть" --
postEvent()
с ДРУГИМИ масками событий.
StateChangeCB()
DBR_CTRL_
-вариант чтения, ...
GetPropsCB()
, а тот уж отдаёт
диапазоны через cda_dat_p_set_range()
и units (и то и другое
опционально, в зависимости от типа).
cda_dat_p_report_rslvstat()
делается уже
после того заказа, но ответ-то на него придёт гарантированно позже.)
Причём -- там прямо есть комментарий "dbr_ctrl_string is not implemented because is senseless" -- конкретно для строк свойства НЕ заказываются!!!
Напрашиваются идеи:
24.10.2022: не-а. Никак, ибо просто нечего -- нет ничего подходящего. Для решения же проблемы с "обновлением для каналов записи" придётся делать по нижеприведённому проекту "в cxsd_fe_epics СРАЗУ генерить update для rw- и autoupdated-trusted-каналов".
Вроде _rw, _dtype и _max_nelems -- cainfo откуда-то берёт.
А _srv_hwid -- это "SSID", он в принципе существует и вопрос лишь, можно ли и как его добыть от libCA.
23.10.2022: сделано, это оказалось несложно. КРОМЕ только srv_hwid/SSID -- не видно никакого способа добыть его от libCA.
casChannel
есть метод getPV()
!!! Так что не нужно
у себя хранить копию указателя на PV.
CxsdHwIsChanValReady()
? Тогда rw-каналы
будут сразу отдаваться.
17.11.2022: вчерашне-сегодняшнее разбирательство с
вопросом "почему от epics2smth+libCAS обновления camonitor'у приходят
редко и сразу пачками?" (где ответом стало "а обновление просто не
производит записи, откладывая её до
fileDescriptorManager.process()
") подсказало подозреваемого:
а может, дело в том, что и libCA тоже НЕ выполняет запись
сразу?
Но влияние CA_HEARTBEAT_USECS
с этой гипотезой не вяжется
никак.
cdaclient epics::...
"
натравив на те же каналы и
"cdaclient cx::...
"
и смотреть на одновременность/неодновременность обновления.
ca_poll()
после каждой "отправки".
...по факту -- просто раскомментировать УЖЕ ИМЕЮЩИЙСЯ в
cda_d_epics_snd_data()
закомменченный вызов.
Ну, проверяем.
"cdaclient -mDa8 @t100:epics::asdf.2=abcd1234
" висело 5
секунд, а оба мониторирующих -- и через CX, и через EPICS -- показали
обновление именно сразу после завершения записывающего.
ca_poll()
-- НЕ помогло. Почему?
Прогнал записывающий cdaclient под "strace -f
" -- у-у-у-у...
Там порождается толпа thread'ов, постоянно дрыгается на
futex()
'ах.
ca_put
& Co. сказано:
All put requests are accumulated (buffered) and not forwarded to the IOC until one of ca_flush_io, ca_pend_io, ca_pend_event, or ca_sg_pend are called. This allows several requests to be efficiently combined into one message.
Бинго -- ca_flush_io()
!
Попробовал, вместо ca_poll()
-- да!!! Помогло!!!
Работает в районе 0.3-0.4с; да, небыстро, но примерно одинаково хоть для 1 канала, хоть для 2.
...кстати, ещё ведь в 2019-м были мысли "надо поэкспериментировать на тему сочетания этого с необходимостью вызова какого-нибудь ca_poll() после ca_array_put()" от 02-07-2019 и попытки разобраться вроде "влияние наличия/отсутствия ca_poll() сразу после ca_array_put() понять не удалось" от 02-07-2019.
Косяк: почему-то НЕ отрабатывается запись
"@i:epics::asdf.1=12345
" при определении канала
"w1d4
". Вот при "w1d1
" -- работает, и через CX --
тоже работает... Или это как раз из разряда "надо на каналы записи отдавать
значения сразу"? 05.05.2023: а не может ли это быть
следствием именно несоответствия размерностей -- что СКАЛЯР пишется в
ВЕКТОР? Т.е., аналог ECA_BADCOUNT, но в libCAS?
Пробовал посмотреть на тему "flush" в libCAS -- фиг, глуховато... И в CAS_tutorial и CAS_reference это слово не встречается...
17.05.2023: косяк был в cda_d_cx.c, что UDP-резолвинг делался только для первого канала, а последующие уже раз в 10 секунд. Исправлено.
22.11.2022@вечер-засыпая: так всё проще: видимо, "обновление свойств" происходит ещё ДО того, как монитор стал WORKS и создал PV, потому и NULL. Посему надо в любом случае сохранять переданные dtype,max_nelems в мониторе, а в конструкторе _PV::_PV() передавать их в конструктор _gdd::gdd_(), и чтоб тот выполнял инициализацию аналогично cxsd_fe_epics'ному.
23.11.2022@утро: уже давно сохраняются. А
SIGSEGV оттого, что e2s_set_props()
, в отличие от
e2s_update_data()
, позволяет состояние не только WORKS, но и
SEARCH и REGISTERING. Вчерашнее решение -- сохранять всегда, и сбагривать
конструкторам -- явно годится, дополненное НЕвызовом
setDimension()
при PV_ptr==NULL.
09.05.2023@~16:00, сидя в Перчини
в Эдеме с Наташкой и с Мариной, Машей и Аней Малыхиными на "позднем"
праздновании аниного ДР, глядя, как девицы играют в настолку "коварный лис,
укравший пирог": чтобы не делать ca_flush_io()
на каждую
отправку, которых может быть толпа, но соптимизировать:
Тогда даже если будет пачка записей, то flush сделается единожды на всех и ПОТОМ, когда будет "бездействие".
=-1
.
Это, кстати, вполне общий механизм, годный для разных аналогичных применений.
09.05.2023@вечер, ~18:15+: делаем.
#if USE_DEFERRED_ca_flush_io
для возможности потестировать разницу в производительности.
ca_flush_io_tid
=-1.
static void call_ca_flush_io(int uniq, void *unsdptr, sl_tid_t tid, void *privptr) { ca_flush_io_tid = -1; ca_flush_io(); }
ca_flush_io_tid < 0
и через
0
микросекунд (да-да, НОЛЬ, а не 1
) -- тут есть
вопрос, не аукнется ли это где.
(Глянул в cxscheduler.c -- там вроде проблем возникнуть не должно.)
uniq
=0.
cda_dat_p_suniq_of_sid()
от своего sid'а: логика такая, что
"всё равно действие общее на все сервера; ну подумаешь, уберётся таймаут при
подчистке сервера -- при следующей же надобности записи добавим другой".
ca_flush_io_tid
>0 НАВЕКИ.
Потому и оставил 0.
...есть смутные воспоминания, что с CaHeartbeatProc()
в
2019-м были аналогичные соображения (но это, похоже, нигде не записано).
cda_d_epics_term_m()
не забыта.
Теперь надо будет протестировать.
15.05.2023: тестируем. Вроде работает.
Так что теперь штатно включен режим
USE_DEFERRED_ca_flush_io
=1.
...с временем отработки (17-11-2022: "Работает в районе 0.3-0.4с; да, небыстро") остаётся вопрос -- надо будет потестировать с большей нагрузкой (массивы покрупнее, число каналов поболее) и посравнивать скорость вариантов "flush сразу" и "отложенный".
01.05.2023: так вот, текущий статус-кво таков:
И он коротюсенький -- 669 строк (плюс 103 в epics2cx_conv.h). Видимо, благодаря тому, что в libCA используется идеология, очень похожая на CX'ную: регистрация канала, подписка, а чтение и запись простейшими вызовами (с зеркальным отражением в cda), плюс понятие о "данных" -- {тип, число элементов, данные}.
Но не без косяков, с которыми ещё разбираться (например, начальное чтение), да и размер вдвое больше cda_d_epics -- 1055+381 и 79+996+381 соответственно -- при зеркальной функциональности. Всё это -- следствие запутанного и переусложнённого стека aitTypes+gdd+libcas.
ЗЫ: вариант ...но хоть форсение
MON_KIND_RAW
реализован некорректно: в нём всё
равно делается {R,D}-конверсия, хотя не должна бы.ext_dtype = CXDTYPE_DOUBLE
не производится. Чуть позже: а вот и нет -- корректно! Т.к. при
!=MON_KIND_VAL форсится phys_count=0
и не ловится RDSCHG; вот
никакой конверсии и не будет.
@вечер, чистя зубы: а ведь НЕ надо добавлять не-VAL и не-RAW к списку periodics[]! Но сейчас -- добавляются!
08.05.2023: сделано -- и условное добавление/удаление в
periodics[], и kind-зависимый выбор evmask'и посредством
kind2evmask()
. Чуть позже: вот последнее, наверное, стоит
перевести на взятие из таблицы -- см. "И это явно напрашивается как-то
унифицировать в некую таблицу" за 01-07-2022.
e2s_PV::read()
недоделано. 17.05.2023: уже доделано.
new e2s_PV(...)
" делается в ДВУХ местах, а надо б
унифицировать.
select()
, в котором
висит цикл "fileDescriptorManager.process(3600)
".
...это можно решить введением pipe-пары, читающий конец которой
регистрировать у fileDescriptorManager
'а (и в callback'е
вычитывать блоками по 1024 байта), а в пишущий слать по 1 байту при каждом
обновлении (и сделать NONBLOCK, так что даже при переполнении pipe-буфера
просто пофиг на ошибку -- если переполнение, то, значит, уведомление уже
послано).
11.05.2022: за вчера-сегодня сделал (см. тот раздел за эти два дня), теперь тестировать.
TangoHeartbeatProc()
, включаемой
лишь при !USE_PUSH_MODEL
; 14.05.2023:
исправлено -- теперь всегда.
DoSubscribeIterator()
странная проверка
"if (hi->dpx == dpx)
", чей смысл неочевиден.
13.05.2023: разобрался:
TangoHeartbeatProc()
делается попытка реконнектить
все неприконнекченные dpx'ы (т.е., из списка frs_2rcn
),
dpx
,
который только что приконнектился (и, значит, можно запрашивать подписку);
cda_d_tango_new_chan()
,
частично с помощью determine_name_type()
-- крайне мутное: там
и определяются далеко не все варианты (конкретно "->" никак не
смотрится), и сам код/эвристика крайне мозгодробительно-неочевидны.
Тут надо просто ВСЁ переделывать.
CAR_CH_OPEN_RESULT
для сбагривания в
cda_dat_p_set_hwinfo()
.
Главный побудительный мотив -- для epics2cda.c, чтоб тот мог и к EPICS'ным каналам быть бриджом тоже.
...и понадобится реальная поддержка фичи
CDA_DAT_P_FLAG_CHAN_TYPE_CHANGE_SUPPORTED
-- в т.ч., уметь начинать работу с каналами
типа UNKNOWN.
То, что в принципе такое возможно -- очевидно: ведь утилита cainfo умеет добывать ВСЕ эти свойства. Вот нужно в неё заглянуть и посмотреть, как там делается.
22.10.2022: заглянул, посмотрел -- ну да, вроде несложно. Конкретно:
Это к тому, что часть служебного "мяса" именно там.
cainfo()
.
ca_element_count()
и
ca_field_type()
, плюс ca_read_access()
и
ca_write_access()
, всем им передаётся "chid".
connect_pvs()
, сводящегося к вызову своего
create_pvs()
с последующим ca_pend_io()
и с
проверкой "не таймаут ли?" -- там ещё комментарий "Wait for channels to
connect".
Кстати, описания вышеупомянутых ca_*()
-методов есть в
"EPICS R3.14 Channel Access Reference Manual".
StateChangeCB()
, непосредственно перед вызовом
cda_dat_p_report_rslvstat()
-- тем самым получим в точности ту
же цепочку/сценарий передачи информации, что и в cda_d_cx.c.
ca_*()
-методов; она сводится к возврату закэшированного ранее
(а вот как/когда оное было закэшировано и как именно возвращается -- для
типа, макс.числа_элементов и прав доступа делается РАЗНЫМИ способами; там
всё истинно-плюсово-поперезапутанно).
Теперь что касается возможной поддержки CXDTYPE_UNKNOWN
:
ca_create_channel()
-- тип НЕ требуется.
ca_create_subscription()
, и вот там уже ТРЕБУЮТСЯ и тип, и
max_nelems
.
Но надо помнить состояние "подписано ли"; плюс, это будет одноразовым "приспособленчеством" (если только не помнить предыдущий тип и не менять тип подписки при изменении).
23.10.2022: делаем "1-ю часть" отдачу HW-информации.
StateChangeCB()
и довольно тривиально: сначала
добываются свойства вызовами ca_element_count()
,
ca_write_access()
и ca_field_type()
, а затем
сбагриваются cda.
if (srv_dbf_type == DBF_STRING) srv_nelems = sizeof(dbr_string_t);
05.05.2023: только
"sizeof(dbr_string_t)-1
" -- ведь в число этих 40
(MAX_STRING_SIZE
) символов должен войти ещё и '\0',
который CX'у не требуется, но от возможного размера строки единичку
откусывает. Добавлено (точнее, "вычтено" :D).
Проверено cdaclient'ом (причём на CX'е же, посредством cxsd_fe_cx :D) -- да, возвращает осмысленную информацию.
24.10.2022: поискал на тему "что заказывать вместо бессмысленного DBR_CTRL_STRING" -- оказалось, что НЕЧЕГО.
В src/ca/client/db_access.h вместо несуществующих
dbr_gr_string
и dbr_ctrl_string
рекомендуют
использовать dbr_sts_string
(точнее, "not implemented; use
struct_dbr_sts_string" -- ага, с "struct_"...), но в ней нет
поля units
!
01.05.2023@вечер-перед-сном, штудировавши исходник cda_d_epics.c: возвращаясь к вопросу "не работает DBR_CTRL_STRING, что делать?" -- пришли в голову соображения.
GetPropsCB()
, который тут исключается -- ну так и
выполнять их прямо сразу в конце StateChangeCB()
, сразу после
cda_dat_p_set_ready()
.
cda_dat_p_set_strings()
и
cda_dat_p_set_range()
, генерящие события
CDA_REF_R_STRSCHG
и CDA_REF_R_RANGECHG
, то...
egrep 'CDA_REF_R_STRSCHG|CDA_REF_R_RANGECHG' work/*4cx/**/*.c(.)
-- ничего интересного: ловятся эти события только в cdaclient.c,
где они приводят лишь к печати информации.
OK, заменил строку поиска на
'CDA_REF_EVMASK_STRSCHG|CDA_REF_EVMASK_RANGECHG'
-- один фиг.
Может, дело в начальном чтении?
26.10.2022: за вчера-сегодня вроде доделал поддержку CXDTYPE_UNKNOWN:
evid ca_evid
-- для хранения ID подписки, чтоб можно
было её отменять и заводить новую.
Это указатель, который при неподписанном состоянии делается =NULL.
chtype ca_subs_DBR_type
-- для хранения ТЕКУЩЕГО типа,
на который оформлена подписка. Служит в GetPropsCB()
для
сравнения, совпадает ли текущий полученный от сервера тип с тем, на который
в текущий момент подписка оформлена.
Это технически long, поэтому при неподписанном состоянии (и изначально) делается =-1.
cda_d_epics_new_chan()
: теперь стоит явное разрешение
CXDTYPE_UNKNOWN
.
Подписывание же -- ca_create_subscription()
-- сделано
условным, если dtype != CXDTYPE_UNKNOWN
.
StateChangeCB()
: в случае, если канал "полиморфен"
(заказан как CXDTYPE_UNKNOWN
) проверяется соответствие
свежеполученного от сервера типа текущему значению
ca_subs_DBR_type
и при НЕсовпадении
ca_evid != NULL
.
Таким образом, оно же и исполняет первоначальное подписывание -- т.к.
изначальное значение ca_subs_DBR_type
=-1, что не совпадёт ни с
одним из типов.
CDA_DAT_P_FLAG_CHAN_TYPE_CHANGE_SUPPORTED
в метрику.
NewDataCB()
теперь идёт первым -- чтоб
можно было на него ссылаться из StateChangeCB()
.
cda_d_epics_new_chan()
, а при UNKNOWN -- из
StateChangeCB()
.
Это может создавать разные "диаграммы поведения" вследствие различия в паттернах прихода данных и свойств.
...в принципе, можно ВСЕГДА заказывать подписку из
StateChangeCB()
; но надо ли?
max_nelems
; для UNKNOWN же
заказывается число 0
-- т.е., столько, сколько есть ("A
count of zero specifies the native elemnt count.")
Может, нужно всё-таки помнить ИСХОДНО-ЗАПРОШЕННОЕ количество?
Теперь проверить бы как-нибудь, но в условиях неподдержки cdaclient'ом спецификатора @x: -- не очень пока ясно, как бы...
29.10.2022: попробовал всё-таки исследовать вопрос "а
как бы добавить поддержку @x: в cdaclient" -- НИКАК. Ну о-о-очень уж много
это будет маеты; поштучно всё описано в комментарии в
console_cda_util.c::ParseDatarefSpec()
.
15.11.2024: однако сделано -- флажок "@?".
Так что -- только посредством "заявителя", epics2cda.c.
10.12.2022@утро-умываясь:
(скорее не сюда, а в epics2smth...) кстати, к вопросу о диагностике -- была
проблема, что при тестировании "epics2cda epics::
" в связке с
сервером с модулем cxsd_fe_epics непонятно, кто откликался и к кому из них
коннектились caget/camonitor/...
Так вот: надо просто явно указывать адрес(а) в
$EPICS_CA_ADDR_LIST
клиенту и в
$EPICS_CAS_INTF_ADDR_LIST
с $EPICS_CAS_SERVER_PORT
серверу.
29.10.2024: во время тестирования
epics2tango_gw.cpp, при сравнении тамошнего кода с
cda_d_epics.c, возникли некоторые вопросы к корректности оформления
подписки в StateChangeCB()
:
hi->dtype != CXDTYPE_UNKNOWN
(т.е., НЕ полиморфные!) --
DBR_CTRL_nnn
, т.к. для
не-полиморфных выше делается
DBR_type = cxdtype2DBR(hi->dtype, DBR_class_CTRL);
NewDataCB()
, указываемая получателем подписки,
ожидает именно DBR_TIME_nnn
!
CXDTYPE_UNKNOWN
--
-- т.е., вsrv_dbf_type = ca_field_type (hi->ca_chid); srv_DBR_type = dbf_type_to_DBR(srv_dbf_type); . . . if (hi->dtype == CXDTYPE_UNKNOWN) DBR_type = srv_DBR_type; else DBR_type = cxdtype2DBR(hi->dtype, DBR_class_CTRL);
DBR_type
(используемый как тип для подписки) попадёт
просто DBR_nnn
вместо DBR_TIME_nnn
, ожидаемого в
NewDataCB()
, и даже не DBR_CTRL_nnn
, как у
не-полиморфных.
hi->ca_subs_DBR_type != srv_DBR_type
-- т.е., фиг поймёшь, какие из _DBR_type
-переменных и полей
должны быть в каком из DBR_class_NNN
-пространств.
Разобраться бы и прописать в комментариях в их объявлениях.
Короче -- надо бы сей аспект протестировать.
08.11.2024: по ходу опытов с
"ca_create_subscription()
со странными типами" смотрел на код
по оформлению подписки в
cda_d_epics.c::StateChangeCB()
и возникло впечатление,
что там просто косяк вследствие подселения туда кусков из
cda_d_epics_new_chan()
(26-10-2022?) -- из-за совпадающих имён
переменных, используемых в несколько разных целях.
12.11.2024: код StateChangeCB()
переделан:
DBR_type
там пара раздельных
props_DBR_type
и subscr_DBR_type
ca_subs_DBR_type
также переименовано в
ca_subscr_DBR_type
),
srv_DBR_type
при полиморфном варианте.
Теперь надо тестировать (с чем сложности, т.к. UNKNOWN -- это только
epics2cda
, значит, нужно 2 "сервера" "цепочкой").
13.11.2024: ещё с позавчера (да, с НЕпофиксенным
кодом) пытался тестировать, запуская "цепочку" --
caget/"cdaclient epics::
"->"epics2cda epics::
"->"cxsd -e 'load-plugin epics'
"
-- фиг, не работает.
О той неработе отдельно целая простыня за сегодня, а по теме:
cda_ad_chan(,,,,CXDTYPE_UNKNOWN,)
и
печатала бы диагностику приходов.
14.11.2024: в продолжение вопроса:
CXDTYPE_UNKNOWN
-- ведь в утилите есть всё-всё, что только
нужно для диагностики. Ну пусть будет какой-то недокументированный
формат...
CXDTYPE_UNKNOWN
?
@вечер: кстати, да -- символ '?' был бы отличным выбором для такого модификатора!
urp->dtype
, реально пришедшему типу, вёрнутому
cda_dtype_of_ref()
'ом, по ; и если нет -- то использовать
формат по умолчанию ("%d" для REPR_INT и "%g" для
REPR_FLOAT).
15.11.2024: за вчера и сегодня в programs/utils/ сделано (см. в тамошнем разделе).
Теперь тестируем командой
"cdaclient '@?d16:epics::qltr3.iset'
";
да: с новым вариантом работает, со старым -- нет.
Засим старый кривой вариант убираем.
13.11.2024: ещё с позавчера пытался тестировать полиморфизм в cda_d_epics, запуская "цепочку" -- caget/"cdaclient epics::"->"epics2cda epics::"->"cxsd -e 'load-plugin epics' -- фиг, не работает. Долго возился, проверяя разные идеи.
Была даже мысль, что проблема в eno2 -- что кабель не воткнут, но нет: "обмен" адресами (посадка cxsd на eno2 и наоборот) ничего не менял: напрямую работало, через epics2cda -- нет.
...сейчас-то понятно, что даже это было лишним: ведь НАПРЯМУЮ-то (cdaclient->libCA->libCAS->cxsd) работало.
Как бы то ни было: и убирал файрволлинг посредством
"iptables -F
",
и нужную строчку в /etc/sysconfig/iptables добавил -- ничего не
изменилось.
И по его диагностике было видно, что вроде должен отвечать положительно.
Но уходит ли пакет от libCAS'а -- фиг поймёшь.
Пробовал было wireshark'ом смотреть, но тот что-то дурит (показывает пакеты не только на нужном eno2, но и на lo): то ли путается в нескольких интерфейсах (lo и eno2), то ли как-то влияет невоткнутость кабеля в eno2, то ли ядро Linux как-то чудит ("interface index" у всех пакетов всегда было 0, хотя IP-адреса из сетей с разных интерфейсов).
...а вот если бы убедиться, что epics2cda'шному libCA пакет приходит...
ca_add_fd_registration()
.
Добавил в HandleCaInput()
и FdRegistrationCB()
диагностическую печать.
И увидел, что после ответа сервером на запрос начинает с бешеной скоростью лететь диагностика "HandleCaInput(fd=5 mask=1)" (5 -- это егойный дескриптор) -- т.е., ответ реально приходит, но почему-то не вычитывается, вот и не видит libCA ответа.
ЗЫ: в логе strace чётко видно, что сразу после
select()
есть диагностическая выдача на stderr, а более ничего.
ЗЗЫ: но вот что странно: если сначала послать запрос на
канал с именем "cx::ЧТО_ТО", то libCA'шный дескриптор всё равно остаётся
номер 5
; хотя, казалось бы, cda_d_cx+cxlib должны создать ещё
пару дескрипторов (от CT_SRCH
и CT_DATA
). Или это
libCA делает поисковый UDP-дескриптор "авансом", прямо при инициализации?
ca_poll()
его игнорирует.
И сразу вопрос -- не из-за этого ли глючит ca_poll()
?
Хорошо -- начал искать, в какой же момент происходит порождение 2-го thread'а: может, оно как-то неявно порождается?
sl_main_loop()
, а второй --
fileDescriptorManager.process(3600)
в e2s_run()
.
О чём я благополучно забыл.
epics2cda_del_chan()
, epics2cda_snd_data()
и
epics2cda_acc_curv()
локинга НЕТ.
Конкретно эта троица в ДАННОМ случае вроде бы участвовать не должна (записи ж нет), но сходу может быть неочевидно.
14.11.2024: кстати, а почему тогда, в 2022-м, не
рассматривалась идея сделать cxscheduler-API на
fileDescriptorManager
(что можно было бы сделать на основе
Xh_cxscheduler.c, т.к. Xt с fdManager
идеологически
схожи -- использованием раздельной обработки R/W/E)? Ведь это
позволило бы работать в ОДНОМ потоке. Или рассматривалась и была
отвергнута? 16.11.2024: следов такой идеи в записях
нет -- видать, просто не приходила мысль в голову.
@~15:00, сидя в конференц-зале на рабочем совещании
по КИСИ-2: сделать-то
"fileDescriptorManager_cxscheduler.cpp" можно, но вот способ
аллокирования tout_list
и fddata
придётся
переделать по образу pool'ов в epics2tango_gw.cpp (аллокирование
chunk'ами, чтобы избежать движения ячеек по памяти), т.к. эти клятые
callback-объекты должны располагаться по фиксированному в момент рождения
адресу. ...ЗЫ: а "инициализировать" их придётся при помощи "placement new",
т.к. номер файлового дескриптора передаётся КОНСТРУКТОРУ (господи, какой
идиотизм...).
16.11.2024: приступлено -- сделана копия Xh_cxscheduler.c, только в файл с более коротким именем -- fdManager_cxscheduler.cpp, пока только комментарий вначале поменян. Впрочем, это тема для раздела по gw/epics2smth/, а не тут.
15.11.2024: касательно самого epics2cda:
mt_sl_lock()
/mt_sl_unlock()
во все методы
epics2cda_NNN()
вокруг вызовов из cda, а не только
cda_add_chan()
, как раньше.
Тоже один чёрт -- ничего не изменилось.
И создаём сразу после "cda_d_epics", чтоб были рядышком.
07.07.2019: пытался нагуглить что-нибудь на тему "introduation to pv access clients" -- по аналогии с PDF'кой для CA. Фиг. Но некоторое количество информации всё же нашлось.
Там рассказано немножко интересного, в т.ч.:
Следовательно, и нам надо будет делать протокол "pva::", а не "pv::"; соответственно, и cda_d_pva.c, а не cda_d_pv.c.
Из любопытных слов/утверждений там -- "High Performance DAQ" и "Digital Video System".
08.07.2019: также некоторое количество информации нашлось при переходе с EPICS'ной "Getting Started..." по ссылке EPICS V4 -- попадаем на "EPICS Version 4 Home Page". Там есть:
и "Clone it in the same directory that holds your EPICS 7 release. Documentation is provided in pvaClientTutorialCPP/documentation/clientTutorialCPP.html".git clone https://github.com/mrkraimer/pvaClientTutorialCPP.git cd pvaClientTutorialCPP make
08.07.2019: попытка поискать описание протокола "PV Access" (EPICS4) -- гуглим "pv-access protocol". Кое-что нашлось:
Имеющаяся внутри ссылка "pvAccess_Protocol_Specification_20151016.html" не работает (404). Ссылки на предыдущие -- работают, так что можно проследить историю изменений.
15.07.2019: в презентации "EPICS7" упоминается некий sample-конфиг epics-train/examples/ExampleApp/Db/circle.db. Посмотреть бы на него -- вроде бы это наипростейший, там поля {angle,x,y}.
15.07.2019: изучаем спецификацию протокола pvAccess. Из любопытного:
Мотивируют тем, что так избавляются от "wasted space and additional complexity".
IMHO, сомнительный аргумент.
...зато сразу снимает проблему "как для композитных типов согласовывать выравнивание протокола с выравниванием языка". Может, именно ЭТО и есть реальная причина?
Мотивация -- что такой подход позволяет "all the intermediates to forward data without requiring it to be unmarshaled". Т.е., это сильно упрощает всякие gateway'и.
Пожалуй, a valid point.
Тут -- аналогично: если меньше 255, то передаётся 1 байт; если меньше 2^31-1, то передаётся байт=255, а затем 32-битная длина; если >=2^31-1, то передаётся байт=255, за ним int32=2^31-1, а за ним 64-битное число. Чем-то похоже на UTF8.
На мой взгляд -- экономия "на спичках": экономятся считанные байты, но за счёт усложнения кода. (Плюс -- в возможности отправки больших объёмов; но где это в реальности может пригодиться?)
И ещё пара аспектов:
Как хорошо, что я вопросом кодировки принципиально не занимаюсь! Не транспортного протокола это дело!
У нас же вообще этот вопрос не рассматривается: в протоколе НЕ передаются никакие сетевые адреса, а используются просто source-адреса ответных пакетов.
Но если уж использовать адреса -- то да, IPv6 является разумным выбором.
16.07.2019: ещё почитал про "introspection data" (мало что понял: это явно про описание типов, но мутновато -- видимо, понимание придёт позже, после прочтения дальнейшего), и, вкупе с ранее прочитанным, выкристаллизовалось осознание:
Как будто они до сих пор живут в 80-х/90-х, когда память была мега-дефицитом и приходилось ужиматься повсеместно.
Если же заботиться о микроконтроллерах (где, впрочем, сейчас счёт идёт как минимум в сотнях мегабайт), то:
Это, кстати, было подтверждено на примере мамкинских контроллеров с PowerPC-852, где полноценный EPICS дико тормозил, а CX'ные драйверята справлялись намного лучше, оставляя некоторую часть процессора свободной (и притом мы там работаем в режиме "CAN-АЦП постоянно выдают измерения в линию", а не "вычитываем измерения столько, сколько успеваем" -- первый вариант создаёт бОльшую нагрузку).
17.07.2019: а ещё про "introspection data": почитал
чуть внимательнее, и там есть кое-что ОЧЕНЬ ПОХОЖЕЕ на наш
cxdtype_t
: FieldDesc.
В частности, старшие 3 бита (указывающие "представление" -- int, float,
string, ...) НЕ МОГУТ иметь значение "111", а максимум 110 -- поскольку
0b11011111=0xDF, а значения выше 0xDF являются специальными кодами
*_TYPE_CODE
.
...мы в CX прекрасно обходимся "max_nelems", а массивы в принципе все переменного размера -- то, что у этих считается "bounded-size".
Причём по факту кодирование размера ровно такое же, как у нас -- указывается двоичный логарифм размера; поэтому и значения те же самые (byte -- 00, short/int16 -- 01, int32 -- 10, long/int64 -- 11; с вещественными -- аналогично).
Но: там в принципе НЕВОЗМОЖНА такая вещь, как "int128" -- код должен был бы получиться 100, но верхний бит уже занят под беззнаковость.
И у всяких массивовых типов (fixed- и bounded-) указывается оный "размер" в элементах.
Там в принципе есть пример о "timeStamp_t
" -- простая
структура из 3 полей, но он без комментариев к hexdump'у, так что
дешифрировать его нетривиально. Байт 0x03 там есть (самое начало 2-й
строчки), но почему он там -- хбз. Или "по общему правилу передачи
массивов"?
17.07.2019: приятно видеть, что EPICS4/7 использует примерно те же способы/правила обращения с соединениями, что и CX (связка cxlib<->cxsd_fe_cx). Только:
18.07.2019: проверил по исходникам: в base-3.15.6/
слово SO_KEEPALIVE
встречается; а вот в base-7.0.2.2/
-- уже нет. Вероятно, решили, что при наличии "application pings" обычные
keepalive'ы уже без надобности.
18.07.2019: дочитал на тему работы с соединениями. Из любопытного:
Это ровно та проблема, с которой мы уже сталкивались, и которая прекрасно
имитируется "kill -STOP"'еньем (или Ctrl+Z'еньем) клиента, и она была
косвенно решена 03-04-2017 путём включения ограничений на буфер отправки
через fdio_set_maxsbuf()
плюс оптимизацией сдвига буфера
(только при отсутствии сверху достаточного места).
Идея в принципе интересная, техническая проблема только в том, как вести учёт объёмов.
И, поскольку есть правило "у клиента ВСЕГДА должны быть последние данные", то в случае применения нужно будет после "опустошения буферов" (появления свободного места) слать текущие значения тех мониторов, которые попали под не-отсылку (их, очевидно, надо помечать), СРАЗУ, не дожидаясь их очередного обновления от сервера.
У меня только одна мысль на тему "зачем" -- видимо, они вычитывают не по границе пакетов, а большими блоками "до заполнения буфера целиком", и потом уже внутри этого буфера распарсивают. На это косвенно указывает фраза " bulk reads are made from the socket rather than reading message by message (because OS calls are expensive)" ранее.
...а у меня под Linux/x86 опыт противоположный, зато на PowerPC syscall'ы действительно очень дорогие. Возможно, ребята ДО СИХ ПОР стараются оптимизировать под то древнее железо (VME -- неизбежно контроллеры PowerPC).
Ну-у-у, для всяких снифферов, вероятно, это полезно.
Интересно, а две записи подряд там выполнять можно ли, отработает ли вторая или будет отброшена?
А вот в CXv4 ничего такого нет -- наоборот, все операции явно и намеренно сделаны "stateless", чтоб взаимодействие сводилось к обмену сообщениями.
18.07.2019: итак, файл дочитан до конца.
PVField
", но определения его НЕТУ.
Вот эти многостадийные "запросы" -- явно объясняют странность, когда при установке в cda_d_epics.c периода поллинга в 5с многие операции исполнялись по 10-15 секунд -- т.е., по 2-3 периода: очевидно, как раз делались те самые стадии подготовки request'ов (с лишними, на мой взгляд, round-trip'ами).
Как бы аналог нашей CXC_PEEK
, но у нас-то она используется
для первоначального чтения "последнего известного", а тут -- вроде УЖЕ РАНЕЕ
клиенту должно было быть прислано; он что, страдает склерозом и может
переспросить?
subcommand
, вроде бы ведущее себя одинаково у разных "типов
request'ов", имеет различающуюся кодировку "битовых флагов/значений":
Кстати, "grep -i destroy **/*.h*(.)" ничего из этого не нашло. Оно там что, в коде циферками захардкожено?
Совершенно отличающаяся от CX идеология, где клиенту в качестве "подтверждения" в любом случае прилетит обновление (в v2 -- как ответ на "запись", а в v4 -- через обязательно уставляемый монитор).
queueSize
.
Там шо, реально указывается размер буфера (кольцевого?) для мониторов?
Похоже на TANGO'вский вызов команд -- там предусматривается передача "аргумента", вроде бы произвольного типа.
Комментариев ноль, так что неясно, насколько это реализовано. Но и метки "// TODO" там нет.
В качестве резюме -- мои впечатления по результатам прочтения:
Т.е., как будто есть некоторая шизофрения: с одной стороны, протокол для СЕТЕВОЙ работы, но, с другой стороны -- в нём не очень-то учитывается природа сетевого взаимодействия, когда "партнёр" может в произвольный момент отвалиться, и ждать всяких "подтверждений", да ещё и делать многие операции не просто командами "выполни такое-то действие", а заводить на удалённой стороне целые объекты с машинами состояния.
А насчёт "PVField" и "0x01" отправлено мыло с вопросом к matej.sekoranja.
Вечером: тот ответил:
Here is a link to PVData for Java: http://epics-pvdata.sourceforge.net/docbuild/pvDataJava/tip/documentation/pvDataJava.html
03.05.2023: сегодня в tach-talk'е был задан вопрос "Does anyone have documented PV Access run time ENV variables?", на который Timo Korhonen ответил парой ссылок -- на протокол (не новость) и на документацию:
Чуть позже Michael Davidsaver прислал ещё ссылку
EPICS_PVA*_ADDR_LIST
,
там и про multicast'ы, и про IPv6; а вот возможности указания адреса сервера
прямо в имени PV -- нету.
04.05.2023: по результату последнего письма возникло желание узнать, не реализовали ли возможность указывать адрес сервера прямо в имени PV -- то, на возможность чего указывает фраза
A client API MAY also allow a user-specified server address; in this case, the searching process would be bypassed and the specified server address data used directly.в "pvAccess Protocol Specification" (кстати, почему-то не могу сейчас найти, где бы я в 2019-м этот аспект конкретно записал).
Для прояснения вопроса отправлено письмо, ждём ответа.
05.05.2023: ответ получен -- "да", PVXS позволяет указывать сервер явно.
И даже ссылки на кусок реализации и на пример дадены, но легче от этого не стало -- не особо понятно, да и этот C++ шибко уж непривычный (в коде реализации, а не примера).
02.11.2023: насчёт "PVA vs. PVXS": сегодня встретилось в Tech-Talk'е письмо-ответ от Ralph Lange "Re: Get the type of a PV in C++ API" с таким пассажем:
That library you are using is part of the first generation of user-facing libraries.
Meanwhile, there's a second generation (both for C++ and Java) with more modern and language-specific API designs. Better usability, easier to work with, better performance.
These second-generation libraries are stable; they will be replacing the older generation.
For your use case: The C++ second-generation library is called PVXS - https://mdavidsaver.github.io/pvxs/
For any new development - please start there. It will save you from migrating your application at some point in the near future.
Так что, видимо, "cda_d_pva" так и не появится, а если будем делать, то сразу "cda_d_pvxs".
Создаём раздел, сразу после "cda_d_pva", чтоб были рядышком.
Но под-раздел для записывания информации уже сделаем.
15.11.2023: (сюда, т.к. отдельного места нету) усовершенствована "система сборки" в лице frgn4cx/tango/FrgnRules.mk: теперь оно позволяет собираться не с "деревом сборки TANGO" в compile/tango-*/, а с уже установленным в системе (так что все *.h и *.so лежат в системных директориях).
TANGO_INSTALLED
,
который можно указать в командной строке, вроде
"make TANGO_INSTALLED=1
".
TANGO_SERVER_INCLUDE_DIR=/usr/include/tango TANGO_INCLUDES=-I$(TANGO_SERVER_INCLUDE_DIR)
TANGO_CLIENT_LIB_DIR=.
-- это потому, что ниже идёт
TANGO_LIBS= -L$(TANGO_CLIENT_LIB_DIR) ...
(так оно превращается просто в безвредное "-L.
".)
TANGO_SERVER_INCLUDE_DIR
(а не
TANGO_CLIENT_INCLUDE_DIR
) определяется потому, что в
frgn4cx/tango/Makefile проверяется наличие
$(TANGO_SERVER_INCLUDE_DIR)/tango.h ("канарейка").
Проверено на pult-l -- да, собирается с установленным в системе TANGO.
Надо отметить, что это не "знания о TANGO вообще" (оные в "Знания/TANGO"), а именно необходимое для клиентского доступа к TANGO-серверам.
20.05.2019: вчера было немного погуглено на тему "tango c api". Результаты грустноваты:
C is supported for clients written in old style C (like SPEC - https://certif.com/).
The C binding does not support all features of TANGO.
Clients are encouraged to use the C++ api if they need access to all features of TANGO.
- A minimal client binding for the good old C language
- Release 3.0.2 including features of Tango 8 but no events
Мысли по этому поводу: представляется 2 возможных варианта реализации cda_d_tango:
Да, оно должно будет как-то автоматом подтягивать libstdc++.so, но это отдельная техническая задача (возможно, всё сработает именно автоматом).
23.05.2019: анализируем c_binding_Release_3_0_2.tar.gz, конкретно c_tango.h.
И там натыкаемся на термин "device proxy" -- ВСЕМ API-вызовам первым
параметром передаётся "void *proxy
".
DeviceProxy is the high level Tango object which provides the client with an easy-to-use interface to TANGO devices. DeviceProxy provides interfaces to all TANGO Device interfaces.The DeviceProxy manages timeouts, stateless connections and reconnection if the device server is restarted.
Т.е., там просто обеспечивается удалённый доступ к ОДНОМУ устройству, чтоб с ним работать как если б оно было локальным (как во всяких LabView и прочей мелкомасштабной хрени)? А понятие "канал" там не особо приветствуется?
24.05.2019: кстати, есть типизация:
DevShort
=int16, DevLong
=int32, плюс их
unsigned-варианты DevUxxx
.
DevLong64
и DevULong64
.
DevFloat
и DevDouble
.
DevUChar
.
DevBoolean
(C++'ный
boolean
) и DevState
("Tango specific data type").
DevEnum
("only for attribute /
See chapter on advanced features") и композитный DevEncoded
("structure with 2 fields: a string and an array of unsigned char").
Как работать с массивами, более сложными композитными типами, а также с заранее неизвестными типами -- там не сказано.
27.05.2019: чуть в сторону: гугля на тему "datasocket linux", наткнулся на некую краткую презенташку 2014г от NICA 14Feb25_Gorbachev.pdf, где как раз обсуждается попытка интегрировать TANGO с иными СУ (наткнулся из-за имеющегося слов "NI DataSocket Server" и "DSTP", но к искомой теме касательства оно не имеет). И говорится -- слайд 9, "Взаимодействие со сторонними системами управления", в списке "Недостатки" -- что "Необходимость написания достаточно сложного устройства Tango для взаимодействия с другими компонентами СУ".
Т.е., делать ИЗ Танги доступ к иным СУ -- проблема.
24.06.2019@вечер-дома-перед-ужином: гугля на тему "а как в TANGO называется канал, содержащий список всех имён каналов", случайно наткнулся на "School on TANGO Controls system: Basics of TANGO" от 01-07-2016.
10.07.2019: в оном "Basics of TANGO" есть такая фраза про стандартный атрибут State:
A set of 14 device State (enum) is available:ON, OFF, CLOSE, OPEN, INSERT, EXTRACT, MOVING, STANDBY, FAULT, INIT, RUNNING, ALARM, DISABLE, UNKNOWN
По ассортименту состояний чётко видна танговская направленность -- управление подвижками (важная часть синхротронов).
И они сами осознают ограниченность этого набора и наличие потребности в расширении, но сделать ничего пока не могут:
Device State is not easily extensible/customizable in TANGO 8 (nor in TANGO 9) If you want to add additional values to the enum you need to modify the IDL; this implies a new IDL release and a new Device implementation class.
Замечание: впрочем, в CX и этого-то нет, а всего 3 состояния (-1,0,+1 -- OFFLINE,NOTREADY,OPERATING), но у нас принято решение, что "состояние вообще" постулировать вряд ли возможно, поэтому специфичные вещи отражаются специфичными же каналами (вроде vdev_state; плюс обобщение его на разные типы источников -- vdev_condition).
24.07.2019: поскольку в PDF-ке презенташки на странице 94 "TANGO bindings" сказано "C language *partial support)", то ещё раз погуглил на тему «tango "c binding"» (важно -- с кавычками!), и нашёлся результат, но крайне ограниченный и, увы, нам не подходящий:
Там сказано:
The Tango C language binding is a reduced C interface which wraps the Tango C++ API. The actual binding only contains the basic query functionality and the basic synchronous reading and writing of commands and attributes.
24.07.2019: на последней странице PDF-ки презенташки -- "Documentation" -- нашлась ссылка на более формальное описание:
25.07.2019: а есть и более свежая версия документации (найдена удалением имени файла, что дало листинг директории):
27.07.2019@утро-душ:
насчёт типа DevEncoded
и как его поддерживать/имитировать:
а ведь раз это дуплет {строка,цепочка_байтов}, то можно сие
считать за просто цепочку байтов -- сначала NUL-terminated string, а за ней
собственно байты.
В принципе, оно без проблем конвертировабельно в обе стороны. Вопрос может возникнуть лишь с менеджментом памяти.
01.08.2019: наткнулся на ещё одну презентацию о TANGO:
02.08.2019: совершенно случайно, ища, как бы заставить DeviceProxy НЕ блокироваться при первом коннекте (гуглил "tango deviceproxy connection non-blocking"), наткнулся на прелюбопытнейший документ:
Т.е., ребята выполнили как раз сравнение EPICS с TANGO, включая аспект взаимодействия между этими системами. Конкретно ИМ больше понравилось TANGO; хотя аргументация МНЕ кажется странноватой и выводы лично у меня вызывают скепсис.
Так вот: на стр.18 в разделе "3.3. CS group general conclusion" сказано, что конкретно ИМ нравится потому, что
As a group of software developers, database specialists and hardware programmers, our team expertise is very close to software solutions based on the following modern programing trends:
- Object Oriented Model.
- Modern API'S (STL, C++11).
- Unit testing.
- Agile development.
Т.е., просто используемые "технологии" соответствуют ИХ навыкам. О соответствии же этих технологий ЗАДАЧАМ, которые надо решить -- не обсуждается вовсе (более того, ниже используется термин "topnotch" -- т.е., опять эпитеты/эмоции, БЕЗ реалистичного анализа и конкретики).
И отдельно отмечу второй пункт -- "Modern"; т.е., ровно то, чем аргументировал свой выбор Энди Гётц -- "самые-пресамые новейшие технологии", игнорируя при этом адекватность технологий задаче. Ну что ж -- да, у этих граждан с ELI Beamlines система ценностей совпадает с гётцевой, ну пусть тогда Тангой и пользуются.
22.08.2022: в порядке выбора архитектуры "а как
определять типы/метаинформацию в epics2smth" изучаем "внутреннее устройство"
TANGO -- как там хранятся данные внутри
DeviceAttribute[Proxy?]
.
Результаты:
DeviceAttribute
":
EventCallBack::push_event()
'у параметре
Tango::EventData *event
;
Tango::DeviceProxy::write()
.
data_format
(скаляр/1D/2D) и data_type
-- вот последнее именно ТИП.
TimeVal time
-- очевидно, timestamp.
25.08.2022: разобрался, мда...
DevVarShortArray_var ShortSeq
и прочие *Long*, *UShort* и т.п.
Т.е., как будто можно в DeviceAttribute засунуть ОДНОВРЕМЕННО НЕСКОЛЬКО разнотипных значений.
26.08.2022: нет, НЕЛЬЗЯ: при запихивании, например,
Double, ранее запихнутый "Long" (int32) "пропадает" -- т.к. делается
LongSeq = {_pd_seq = 0x672ec0}
.
Пподсмотрено через gdb, а сначала проверено отладочной печатью, которая в int32
выдала мусор -- после цепочки "запись int32, запись float64, чтение int32,
чтение float64; затем также проверены "статусы" чтения -- и чтение int32
вернуло 0, а чтение float64 -- 1.
sizeof(Tango::DeviceAttribute)
=200.
...впрочем, sizeof(refinfo_t)
=440 -- сильно больше, да; но
там зато дофига всего, чего в DeviceAttribute нету.
Пожалуй, через-зад'ность реализации DeviceAttribute
вполне
сопоставима с оной у libGDD. Ну да, концепции-то близкие, и результат
реализации плюс-минус одинаково кривой. ...точнее, кривые даже не только
реализации, но и дизайны.
23.07.2019: пока что -- лишь скелета.
Это файл cda_d_tango.c.
Назовём файл cda_d_tango_cpp_wrapper.cpp -- он будет предоставлять функции с "C"-calling-convention, являясь обёрткой для плюсового кода.
Какое ТОЧНО будет разделение обязанностей -- пока неясно: то ли .c'шник будет реализовывать почти всё, используя обёртку только для конвертации вызовов, то ли наоборот, будет лишь редиректить cda'шные вызовы плюсовому модулю.
Ключевые моменты:
extern "C" { ... }
(да, внутри КОДА это "extern" выглядит глуповато).
...ой, это я уже в другом месте подсмотрел -- "C Wrapper for C++" на StackOverflow.
static_cast
.
Состоит из 2 файлов (как описано выше), при этом СЕЙЧАС основное в cda_d_tango.c, чьё содежимое скопировано с cda_d_epics.c.
Соответственно, модель hwr'ов -- "сквозной список" (а не per-sid).
24.07.2019: также запинана директория frgn4cx/tango/utils/ -- копированием с frgn4cx/epics/utils/ и соответствующим твикингом.
В последней изначально предполагалось изготовление "общей
инфраструктуры", настраиваемой значением переменной $(FRGN)
, но
то пока не сделано. И не факт, что будет сделано -- с учётом присутствующей
неопределённости на тему "а как лучше внедрять dat_p-модули для сторонних
СУ" (в идеале бы -- прямо в штатную libcda.a; но как?).
25.07.2019@пляж, читал "Час быка": возникло понимание, как можно организовать работу:
Как-то надо отдельно обращаться со спецификациями БЕЗ них.
...помнится, что-то подобное было реализовано в cda_d_v2cx.c для больших каналов.
Вроде бы кажется простым решением делать всё в .cpp, оставив в .c лишь адаптер-обёртку. НО: ведь в метрике нужно указывать размер privrec'а, а откуда C'шный код узнает размер C++'ной структуры?
@вечер-ИЯФ: а кто запрещает
ЦЕЛИКОМ всё сделать в .cpp, просто объявив методы и описатель как
«extern "C"
»?
26.07.2019: да, переименовал файл в cda_d_tango.cpp. В связи с этим потребовались следующие изменения:
extern "C"
{
... }
-- это понятно, как и аналогичные (за-#ifdef'ленные)
скобки в cda_d_tango.h.
cda_d_tango_privrec_t *me = pdt_privptr;
вызывали ошибку "invalid cast", так что их пришлось переделать в
cda_d_tango_privrec_t *me = (cda_d_tango_privrec_t *)pdt_privptr;
Вроде как рекомендуется использовать static_cast
<ТИП>
, но обычный кастинг тоже нормально работает.
GENERIC_SLOTARRAY_DEFINE_GROWING()
, в
Get##cname##Slot()
, где делается safe_realloc()
.
Соответственно, cda_d_tango_cpp_wrapper.{h,cpp} более не требуются.
Далее -- просто работаем.
determine_name_type()
взята из
cda_d_v2cx.c -- она там наиболее простая, по сравнению с
cda_d_vcas.c (позволяющем имени начинаться с просто ':') и
cda_d_cx.c (этот дополнительно разрешает "номеру сервера"
начинаться с '-').
Плюс:
char **channame_p
в
const char **channame_p
-- но так и корректнее.
enum
'а
CHTYPE_nnn
, где "nnn" может быть ATTRIBUTE (по умолчанию и
@a), PROPERTY (@p), COMMAND (@c), RESULT
(@r).
...вот для "pipe" пока поддержки нет совсем, и суффикс @p уже занят. При надобности использовать @i?
dup_name
, которая потом сохраняется в
hwr'овом hi
, а если нет -- то free()
'ится (вроде
всё учтено; но муторно и противно).
02.08.2019: переделка и подсчёт разделены. Смысл -- что переделывать нужно ВСЁ имя (включая префикс "HOST:PORT/", а '/' считать только в имени устройства+канала.
09.08.2019: и ещё раз подпеределано. Смысл -- что
переделывать нужно как раз НЕ всё имя: в префиксе "HOST:PORT/" надо
переделывать только конечный символ, а в компоненте "HOST" ничего трогать
нельзя, иначе фигня получается -- "192.168.130.252" превращается в
"192/168/130/252", да и с DNS-именами аналогично. Для избежания добавлена
переделка не с начала dup_name[]
, а со сдвигом на
totransofs
, который делается 0 при неуказанности хоста, а при
указанности -- strlen(srvrspec)
.
27.07.2019: продолжаем:
...и даже если регистрация hwr'а далее обломится, то dpx останется.
strdup()
'нутое имя устройства free()
'илось
когда надо, но НЕ в случае, если оно используется при регистрации нового
dpx.
cda_d_tango_init_m()
добавлена регистрация dat-плагина,
до того отсутствовавшая.
Итого: у нас теперь есть вся "обвязка" -- парсинг плюс инфраструктура sid+dpx+hwr. Далее уже наполнять функционалом связи.
28.07.2019@вечер-дома: инфраструктурное:
Отдельный вопрос -- что будет в случае ИНСТАЛЛИРОВАННОГО в системе "пакета" tango (хоть .rpm, хоть вручную).
30.07.2019: далее...
#include <tango.h>
-- ой-ё!!!
а собственно ругательство --In file included from /usr/include/c++/4.8.2/type_traits:35:0, from /home/user/compile/tango-9.2.5a/lib/cpp/client/devapi_attr.tpp:38, from /home/user/compile/tango-9.2.5a/lib/cpp/server/tango.h:89, from cda_d_tango.cpp:14: /usr/include/c++/4.8.2/bits/c++0x_warning.h:32:2: error: ...
#error This file requires compiler and library support for the ISO C++ 2011 standard. This support is currently experimental, and must be enabled with the -std=c++11 or -std=gnu++11 compiler options.
Причём, поскольку оно проверяется прямо в .h-файле, то вылазит ещё на этапе генерации dep-файла препроцессором.
Так что для решения проблемы пришлось не только добавлять в GeneralRules.mk "полноценную" поддержку C++ (которая, как потом было понято, НЕ была обязательной для решения), но ещё и указывать
SPECIFIC_CPPFLAGS=-std=c++11
-- т.е., флаги КОМПИЛЯТОРА указывать во флагах для ПРЕПРОЦЕССОРА.
НЕНАВИЖУ C++!!!
-I$(top_builddir)/lib/cpp/log4tango/include
Просто какой-то дикий набор подвязочек и махараек... И это -- одна из 2 ведущих СУ для физических установок в мире?! OMG...
Странно лишь то, что в конце пришлось добавить "-lstdc++
" --
при том, что в libtango.so зависимость от libstdc++.so.6,
судя по ldd, и так имеется. Вероятно, следствие линковки командой "gcc", а
не "g++".
...впрочем, явно пришлось также указывать
"-lomniORB4 -lomnithread
", хотя от них зависимость в
libtango.so тоже есть. WTF?!
31.07.2019: проверил с "g++" в роли линкера -- результат тот же. Явно дело в чём-то другом.
И в гугле строка "liblog4tango.a" (С КАВЫЧКАМИ!) встречается всего 4 раза. Похоже, эти господа даже не задумываются о данной проблеме.
cda_d_tango_EventCallBack
с методом
push_event()
-- пока пустым. И всё остальное отсутствует, даже
конструктора нет.
cb
и dpxinfo_t.dev
--
указатели на экземпляры классов. В Rls()'ах при !=NULL этим полям делается
delete
с последующим =NULL
.
31.07.2019: движемся дальше.
Модель берём не с cda_d_insrv/cda_d_local, а с pxi6363_drv.c. Причина -- что здесь, как и там, pipe нужен для передачи информации из callback'а, вызываемого АСИНХРОННО -- в произвольный момент и из другого thred'а (в отличие от синхронного вызова в cda_d_insrv, где pipe используется лишь для рассечения возможных петель исполнения).
И зависит сие, очевидно, в первую очередь от количества каналов: несколько штук -- всё прокатит, сотни -- буфер может переполниться.
01.08.2019: некоторая информация на эту тему есть в "man 7 pipe":
F_GETPIPE_SZ
and F_SETPIPE_SZ
operations.
01.08.2019: мыслишка, как можно уменьшить потребление буфера fifo:
Идея в том, что события "обновление" не особо имеет смысл накапливать -- обычному клиенту достаточно просто последнего значения.
С точки зрения надёжности, наиболее безопасным выглядит такой подход:
Иначе, если сначала отправить, а потом выставить, то может случиться, что произойдёт 1:отправка, 2:переключение_контекста, 3:вычитывание, 4:сброс_флага, 5:переключение_обратно, 6:взведение_флага -- тогда флаг останется навсегда взведённым, поскольку буфер пуст и вычитывать нечего (так что флаг сбросить некому), а новые события в буфер никогда не попадут, т.к. флаг-то взведён.
Т.е., при "сбое" в буфер просто попадёт ещё одно событие.
Даже если race condition и возникает, то результатом "неблагоприятного" его исхода (т.е., когда решение принимается "неверно") будет лишь неисполнение возможной оптимизации -- ну отправит лишнее событие, ну будет ещё одна обработка, ну и ладно.
ЗЫ: а как бы помогла атомарная команда "XCHG", которую б можно было применять к флагу! Установка -- "VAL=1; XCHG VAL, ФЛАГ", и если VAL!=0 -- значит, там и ранее была 1 и ничего делать не надо.
Собственно, вот ЭТОТ минус обнуляет весь смысл идеи.
...разве только завести ДВА отдельных pipe: один для компрессуемых событий CHANGE, а второй для всех остальных, НЕкомпрессуемых.
01.08.2019: приступаем к реальной работе с TANGO-объектами.
cda_d_tango_EventCallBack
:
push_event()
унесён в самый конец файла -- за
пределы блока
«extern "C" { ... }
».
Само определение класса осталось наверху, в начале файла (и ДО C'шного блока).
hwr
и сделан тривиальный
конструктор, оное поле заполняющий указанным ему значением.
push_event()
добавлена отправка event_info в
pipe. Пока что "вхолостую" -- поскольку события ещё не ловятся, то и поля
не заполняются.
dev
(DeviceProxy) и cb
(callback)
уже создаются, при создании своих структур-владельцев.
И при ошибке создания -- ==NULL -- делается соответствующий
RlsNNNSlot()
и возвращается CDA_DAT_P_ERROR
.
Таким образом,
channame
ставится обратно на начало name
(с прибавлением разницы к
channamelen
). Чтобы в TANGO сбагривать "полные" имена.
Жуть жутчайшая -- в примерах всякая фигня, в "Getting Started" --
"Writing
your first C++ TANGO client" (а лучше ничего не видно!) -- тоже ответов
немного. Чуть прояснило ситуацию чтение файла
lib/cpp/client/DeviceProxy.h, на тему
write_attribute()
и write_attributes()
.
Надо сначала создать объект то ли типа DeviceAttribute
, то
ли типа vector<DeviceAttribute>
(второе -- очевидно, для
множественной записи через write_attributes()
), в него
"добавить" значение, и лишь потом вызвать у DeviceProxy метод
write_attribute()
, передав ему этот объект-атрибут (передаётся
по ссылке -- через &
).
read_attribute()
возвращает объект-атрибут (копирующий
конструктор?), из оного атрибута оператором ">>
" берётся
нужное значение, причём оно возвращается указателем на память-объект,
которому впоследствии надо сделать delete
. ДАЖЕ ЕСЛИ ЭТО
ОДИНОЧНЫЙ double
!
Это так показано в tango_92.pdf, стр. 43-44.
Очевидно, надо отдельно просмотреть файл определений DeviceAttribute.h -- вдруг там что поосмысленней найдётся.
NOTE: There is no automatic type conversion from the user type to the attribute native type e.g. if an attribute expects a short you cannot insert it as a double (this will throw an exception) you have to insert it as a short.
Кап-пец...
И ещё: я-то всё искал примеры, вроде тех, что есть в презентации по EPICS. Искал-искал, надеялся, удивлялся, что никак не находятся... А всё было просто: примеров, похоже, НЕТ, потому, что они были бы оч-чень непросты -- чрезвычайно много танцев с бубном и ритуальных заклинаний ради простейших действий. Плюс, похоже, что TANGO больше ориентирована на GUI-использование, а консольные клиенты там не особо жалуют.
06.08.2019: не совсем так. В tango_basics.pdf примеры есть (на страницах 41-42), чуть более годные, чем в tango_92.pdf (хотя до EPICS'ных и не дотягивают).
https://slideplayer.com/slide/16479163/ -- "Презентация на TANGO Workshop на ICALEPCS" -- в раздел знаний о TANGO.
02.08.2019: продолжаем каторжные работы...
event2pipe_proc()
наполнена скелетом. Взят из
insrv_fd_p()
-- цикл с не более 100 повторов.
Для этого в начало cda_d_tango_new_chan()
добавлен фильтр,
что
...а вот INT8 -- увы, он в TANGO не поддерживается.
terminate called after throwing an instance of 'Tango::ConnectionFailed'
Откуда выводы:
Окей -- ловля вставлена, в виде try{}catch(...){}
.
Результат -- возвращается dev=NULL.
Отсюда отдельный вопрос (или это уже как раз следующий пункт?) -- а как заставить НЕ считать начальную ошибку перманентной?
Гуглил -- что-то не находится.
06.08.2019: ...
cda_d_tango_snd_data()
. Всё
тривиально и тупо -- с "new Tango::DeviceAttribute
" и
последующим его delete
.
Теперь остаётся:
EventData
(точнее, в егойном поле attr_value
), а callback-то вызван из
другого thread'а. Так что нужно научиться как-то "добывать текущее".
07.08.2019@дома: по просмотру содержимого .h-файлов совершенно неясно, как бы можно получить "последнее известное значение" атрибута. И непонятно, есть ли там вообще "буфер, хранящий последнее известное значение" -- похоже, что нет: когда событие приходит, то либо оно сразу сбагривается callback'у (push-модель), либо складывается в кольцевой буфер и сбагривается в момент проверки (pull-модель); но нигде НЕ хранится.
Похоже, придётся пользоваться pull-моделью. Мерзко, криво -- но зато работать будет.
08.08.2019: да, подготовлено.
#define
-символ USE_PUSH_MODEL
; он
ЧИСЛОВОЙ (а не для #ifdef
), и может принимать значения 0
(сейчас) и 1 (то, как было изначально).
event_pipe[]
теперь внутри скобок
#if USE_PUSH_MODEL ... #endif
, ...
#if !USE_PUSH_MODEL ... #endif
.
cda_d_tango_EventCallBack::push_event()
для pull-варианта.
>>
, примерно так:
case CXDTYPE_INT32: *(myevent->attr_value) >> val.i32; break;
-- этот вариант НЕ аллокирует "объекта" в памяти, который потом нужно делать
delete
.
template
-- получить 64-битные значения компилятор выдаёт
ругательство, суть которого сводится к
error: 'long long int' is not an enumeration type
(полный фрагмент выдачи тут в тексте закомментарен).
Суть проблемы лично мне неясна.
double
такой проблемы не возникает.
-std=c++11
).
Как бы то ни было, пришлось просто закомментировать обработку
INT64-типов, так что теперь они прямо в cda_d_tango_new_chan()
признаются неподдерживаемыми.
Итого: в начальном приближении модуль сделан, надо пытаться его запинывать.
09.08.2019: немного разборок с попытками запуска.
TANGO_HOST=192.168.130.253:1234
-- и это заработало!!!
time
, отличается при
разных способах запуска (с/без ltrace), и бывает 6 или 9.
futex()
'ами --
причём из другого thread'а; б-р-р-р...
Почему раньше не получалось -- хбз: видимо, что-то по невнимательности не так делал.
getenv("TANGO_HOST")
.
getenv()
просто руками
лезут в environ[]
(переданный main()
'у).
get_env_var()
--
который, предположительно, и вызывается для добычи $TANGO_HOST -- в начале
присутствует попытка первым делом добыть через getenv()
.
...только пришлось исправить конверсию '.'->'/' -- а то оно за компанию переделывало точки и в имени/адресе хоста.
11.08.2019@дома: разобрался!!!!
Оказывается, просто запустить СЕРВЕР с ключиком "-nodb" недостаточно. Нужно ещё и в имени устройства указать "#dbase=no".
На тестовом клиенте (x10sae:~/work/tests/test_tango.cpp) получилось приконнектиться.
А вот через tango_cdaclient -- фиг: оно символ '=' воспринимает как разделитель в паре ИМЯ=ЗНАЧЕНИЕ.
Видимо, надо какой-нибудь специальный символ (недопустимый/бессмысленный в TANGO-именах), чтоб он заменялся на '='.
12.08.2019: делаем. Резюме -- "всё плохо, TANGO -- отстой".
cda_d_tango_EventCallBack::push_event()
, в точке, где пытаемся
получить данные из атрибута.
Первопричина нашлась быстро: поле "атрибут" --
myevent->attr_value
попросту ==NULL.
(gdb) print myevent No symbol "myevent" in current context. (gdb) print cda_d_tango_EventCallBack::push_event::myevent No symbol "cda_d_tango_EventCallBack" in current context. (gdb) print cda_d_tango.cpp::cda_d_tango_EventCallBack::push_event::myevent No symbol "cda_d_tango" in current context.
И абсолютно пофиг, пытаемся мы это сделать при "обычном" SIGSEGV (он ведь
вроде как внутри вызова чего-то другого) или при вручную вставленном
raise(11)
-- там ведь тоже как бы "внутри вызова чего-то
другого".
frame N
", а потом печатай что угодно.
string
-полей -- сбагриванием "cerr<<ПОЛЕ
)
поштучно всё выдать.
err=1
.
DevErrorList errors
", а в
tango_92.pdf пример, как печатать его содержимое.
Новотхрен: тот код просто не компилируется -- компилятор говорит, что у
класса DevErrorList
отсутствует поле/метод size()
.
Не, ну нормально -- просто нету никакого простого способа напечатать диагностическое сообщение? Они там совсем уже ку-ку, танцоры, блин, танго?!
cout<<myevent->errors[0].desc
И вот та диагностика оказалась прелюбопытной...
Де-би-лизм...
Что ей там не так -- загадка. Конкретно TangoTest больной, что ли?
13.08.2019: немного адаптации:
Tango::Except::print_exception(CORBA::Exception &e);
-- то была сделана внутренняя функция report_exception()
,
печатающая strcurtime(), переданную строку, а затем вызывающая ту респечатку
exception'а.
И во всей ловле exception'ов перейдено на неё.
Но "есть нюанс": старые ветки catch (...)
оставлены, на
всякий случай, а перед ними добавлены catch (CORBA::Exception
&e)
, из которых и вызывается новый вывод. И код в этих ветках
дублирующий.
cda_d_tango_EventCallBack::push_event()
добавлена
проверка на myevent->err
.
В этом случае на консоль выдаётся strcurtime(), имена устройства и канала
(dup_name!chn_name), а затем "первое из списка сообщений об ошибках" --
myevent->errors[0].desc
.
После чего SIGSEGV'ы очевидным образом прекратились.
В CX запоминается, что о проблеме уже сообщено, и далее клиентскую программу не беспокоят -- сообщается только об ИЗМЕНЕНИЯХ.
А тут простейший вроде функционал зачем-то перевешивают на клиента.
22.08.2019: насчёт "DeviceProxy не создаётся, а генерит exception".
the DeviceProxy connection to the device server is implemented to be stateless. This means that even if there is a problem the connection will be maintained and as soon as the device is available it will be built automatically for the client i.e. behind the scenes.-- действительно происходит "в фоне", а DeviceProxy создаётся успешно (просто ещё не функционирует, так что попытки чтения/записи обламываются).
Это я проверял указанием "#dbase=no". Там всё корректно и при нерезолвинге имени, и при ETIMEDOUT/EHOSTUNREACH, и при ECONNREFUSED.
This is normal behaviour IF (1) there is no Tango database server listening at localhost:999, AND/OR (2) "a/b/c" is not the name of a device defined in the database.
Возможно, их возмутило моё признание "What I'm coding is not a Tango application, but a plugin for another control system (for it to be able to access Tango directly).".
Пара замечаний:
TangoHeartbeatProc()
сейчас вызывается ГЛОБАЛЬНО --
заказывается прямо в cda_d_tango_init_m()
, и проходится по ВСЕМ
hwr'ам.
Соответственно, и попытку "пере-создания" оно будет выполнять для всех dpx'ов вообще, а не только от одного конкретного sid'а.
...с другой стороны -- ну и что такого? Ну и пусть.
CheckEventsIterator()
у "недосозданных"
ещё hwr'ов (тех, чьи dpx'ы ещё не создались) будет cb==NULL, так что НЕЛЬЗЯ
от него пытаться делать get_events()
.
Вот конкретно на то прямо сейчас поставил ограждение
"if(hi->cb!=NULL)
".
di->dev==NULL
.
Так что ограждение заменено на "if(di->dev!=NULL)
".
В таком варианте вроде должно работать надёжно (не считая вопросов к оптимальности).
Вопрос лишь, что делать с зависанием на 6 секунд. Уносить создание в другой thread? Зело проблемно. Да и если нужно создать сразу целую пачку -- долгий будет процесс; не вытаскивать же создание КАЖДОГО в отдельный thread...
02.10.2019: возвращаемся к этой неприятной теме -- реализуем "реконнекты".
TANGO_RCN_FRQDIV
=100.
tango_rcn_ctr
, ...
TangoHeartbeatProc()
он делается ++, и когда
станет >=TANGO_RCN_FRQDIV, то и будет "прошерстение" всех нуждающихся в
реконнектах.
12.05.2023: неприятный нюанс такого подселения: реконнекты
работают ТОЛЬКО при !USE_PUSH_MODEL
; а при push-модели (для
поддержки которой много сделано в августе 2022) -- автоматом нет, т.к.
тогда самого TangoHeartbeatProc()
даже не определяется.
14.05.2023: исправлено.
dpxinfo_t
поля nx2r
и
pr2r
-- ссылки в списке "для реконнектов".
При аллокировании dpx'а им делается =-1
.
frs_2rcn
и
lst_2rcn
; изначально также =-1
.
AddDpxToRcn()
+DelDpxFromRcn()
, скопированные с
Srv-аналогов.
DeviceProxy
вытащено в отдельную
create_dpx_dev()
.
ТО действие, кстати, и из её юзереа
cda_d_tango_new_chan()
убрано, ибо теперь не-создание Proxy не
является фатальным.
is_suffering
-- для репортинга
проблем.
Если в будущем добавим реакцию на события самого Proxy -- то, может, и
поле dpxinfo_t.is_suffering
сделаем.
create_hwr_subscription()
.
_new_chan()
-- обломы
создания считаются фатальными и возвращается CDA_DAT_P_ERROR
.
...отдельный вопрос -- КОГДА может произойти такой облом: сейчас даже с
"-nodb"-сервером и каналом "#dbase=no" создаётся нормально, а ругательства
сыплются уже от cda_d_tango_EventCallBack::push_event()
.
Видимо, такой hwr станет просто "подвисшим".
create_dpx_dev()
, и если успешно, то:
DelDpxFromRcn(dpx)
.
hi->dpx == dpx
, вызываем
create_hwr_subscription()
.
Всё!
Теперь найти бы какой-нибудь способ это всё протестировать. Напрашиваться к Роговскому на ВЭПП-2000?
05.08.2022: спустя почти 3 года возвращаемся к этой штуке -- в первую очередь в интересах epics2tango, т.к. там нужно понимание работы с Тангой, а тут оно максимально (насколько сие возможно для данного кошмара).
Чисто для закрепления, вот что стало ясно/вспомнилось после чтения раздела знаний, вышенаписанного в этом разделе и исходника cda_d_tango.c:
(А вот для epics2tango, вероятно, и PUSH-модель прокатит, т.к. в libcas многопоточность вроде поддерживается.)
TangoTest
,
но с ним какие-то сложности: он, будучи запущен с ключом
"nodb
", почему-то не отдаёт данные по обновлениям (ключевая
фраза "polling thread не запускается в nodb-режиме"); а то и вовсе как-то с
ним не удаётся коннектиться -- возможно, из-за неправильно указанных имён.
@на-работе, ~16:00: там есть
ключ "-v[trace level]
", так что если указать
"-v9
", то оно валит море отладочной информации. Правда, хрен
её поймёшь...
-dlist a/b/c
" -- и чё? А какому девайсу обращаться? Или
надо указывать "a/b", а "c" -- уже атрибут?
Код "разборщика имён" в cda_d_tango_new_chan()
точно
нуждается в переделке -- как минимум чтоб опознавал синтаксис
"->property
".
(Да, сам синтаксис имён в этом французском поделии крайне мутен: те же property бывают как у атрибутов ("каналов"), так и у устройств.)
DeviceProxy
до тех пор, пока не
получится.
05.08.2022: tango-9.3.4.tar.gz: 238MB (против 62MB tango-9.2.5a.tar.gz), распакованное: 291MB, из них 129MB -- lib/java/, 46MB -- doc/html/_images/...
Попробовал собрать -- хрен:
configure: error: Couldn't find a compatible zmq.hpp
07.08.2022: оказывается, нужно было поставить пакет "cppzmq-devel" -- рецепт (точнее, подсказку) нашёл, гугля "zmq.hpp site:tango-controls.org", в "Recipe to install Tango 9 from source on MacOS X high Sierra".
OK -- собралось, 1524MB против 1299MB у tango-9.2.5a.
08.08.2022: и frgn4cx/tango/ с этой 9.3.4 собралось.
-nodb -dlist a/b/c
"
обращение к "localhost:10001/a/b/c#dbase%no.d" подвисает так же,
так что до сообщения
"d attribute not found
"
проходит те же 5 секунд.
The polling (necessary to send events) for the attribute ampli is not started
@вечер: продолжаем.
Это файл, сгенерённый POGO; в 9.3.4 прилагается также и "исходник" (?) TangoTest.xmi.
int32
, в то время как он
double
-- никакой ошибки НЕ выдалось.
Видимо, потому, что никакого чтения/записи не производилось.
10.08.2022@М-46, ~13:00:
нашёл-таки "описание" формата (куцее и слабоформализованное) --
"The property file syntax".
Форматец, мягко говоря, отвратный -- там "ключевое слово"
DEVICE
располагается в СЕРЕДИНЕ. У этих ребят степень
альтернативности совсем зашкаливает...
("Наткнулся" -- перейдя по ссылке из " Recipe to install Tango 9 from source on MacOS X high Sierra ", в свою очередь нагугленном по "10001 site:tango-controls.org".)
Там же, кстати, сказано, что при этом всего лишь навсего не будет
А вот при "-nodb
" -- длиннющий список, включающий "No
event generated by the server" (ну какое отношение event'ы имеют к
БД?!).
23.08.2022@~10:20, сидя в Kei в ожидании эвакуатора для Chaser: насчёт PUSH-модели:
hwrinfo_t
БУФЕР --
valbuf
/current_val
-- в который складировать
получаемое в cda_d_tango_EventCallBack::push_event()
значение,
а в event2pipe_proc()
его оттуда и
cda_dat_p_update_dataset()
'ить.
Собственно "вычитывание данных из attr_value
" можно вытащить
в отдельную функцию, унифицированную для работы по USE_PUSH_MODEL
и по не-ней.
23.08.2022: чуть позже: вот только тут будет та же проблема, что проявилась в cda_d_insrv: при нескольких быстро приходящих подряд обновлениях -- когда после отправки в pipe уведомления (но ДО его обработки) успеют придти ещё обновления -- отрабатываться будет только "ПОСЛЕДНЕЕ", как будто оно несколько раз, а промежуточные будут утеряны.
Возможное решение -- какой-нибудь флаг/семафор/mutex, взводимый при складировании данных в valbuf, сбрасываемый при их считывании, а складирование чтоб дожидалось бы его сбрасывания.
Но надо будет аккуратно всё продумать, чтоб не возникало deadlock'ов.
23.08.2022: "скелет" сделан -- при
"USE_PUSH_MODEL
" добавлены:
hwrinfo_t
-- пачка полей
valbuf
/current_val
со всеми надлежащими
атрибутами current_{nelems,dtype,rflags,timestamp}.
val_mutex
...
val_mutex_inited
-- этот для
того, чтобы pthread_mutex_destroy()
делался бы ТОЛЬКО
ИНИЦИАЛИЗИРОВАННОМУ (а то стандартом неясно, определяется ли поведение при
удалении НЕинициализированного; плюс неясно, можно ли по содержимому
определить инициализированность).
cda_d_tango_new_chan()
и RlsHwrSlot()
.
current_val
при надобности.
event2pipe_proc()
сделано вычитывание hwr'а, с
проверкой на корректность и последующим, окружённым
val_mutex
'ом, сбагриванием данных
cda_dat_p_update_dataset()
'у.
Надо увольнять нафиг
event_info_t
, обходясь просто hwr'ом. Эта 4-int'овая
структура ведь просто скопирована из pxi6363_drv.c, где она была
полностью к месту, просто по аналогии (потому, что тот же механизм передачи
через pipe информации о событии, прилетающем ДРУГОМУ thread'у (да, в NI
такие же рукожопы, как и TANGO'вцы, неспособные пользоваться
мультиплексированием и умеющие только в thread), а делался
cda_d_tango.cpp сразу же после pxi6363_drv.c). 24.08.2022: уволено.
cda_d_tango_EventCallBack::push_event()
пока
только "скобки" lock()/unlock(), БЕЗ добычи данных.
Кста-а-ати: ведь даже при синхронном чтении --
USE_PUSH_MODEL==0
-- для векторов всё равно понадобятся буфера,
через которые выполнять чтение (ибо непохоже, чтоб у "attr_value"(?????) был
метод вроде "get_data_pointer()"); ну так вот этот же буфер и
использовать!
24.08.2022: реализуем вчерашнюю идею про "всегда использовать буфер".
store_current_value()
.
Она возвращает код "успешно" (0
) и "неуспешно"
(-1
). Неуспешным считается также "неподходящий тип".
Она также ловит exception'ы и при их поимке возвращает -1
.
return_current_value()
, выполняющая
cda_dat_p_update_dataset()
данными из буфера.
cda_d_tango_EventCallBack::push_event()
в обоих
вариантах (PUSH/PULL) выполняет добычу+складирование посредством
store_current_value()
и -- УСЛОВНО! при успешности -- либо
отправляет pipe-уведомление, либо сразу делает возврат.
event2pipe_proc()
же просто делает
return_current_value()
.
hwrinfo_t
ВСЕГДА, независимо от значения
USE_PUSH_MODEL
.
И соответствующий "менеджмент" -- аллокирование/удаление -- тоже теперь всегда.
Ага, но вот только проверка
"if (myevent->err) {РУГНИСЬ; return;}
"
была только в не-PUSH-варианте.
26.08.2022@~12:00, гуляя с папой
в парке между М-46 и Академией: а может, при
вычитывании значения из DeviceAttribute
ориентироваться не на
dtype
, а на аттрибутов data_type
?
Тогда автоматом исчезнет проблема "читаем значение не того типа, что вызывает exception", а конверсию данных будет выполнять cda.
05.07.2024: да, это прекрасно работает -- проверено 01-07-2024 (уже на векторах).
26.08.2022: делаем.
Предварительно -- немного информации:
get_type()
.
Вот только он НЕ возвращает текущее значение приватного поля
data_type
, а вместо этого последовательно проверяет наличие
-- != NULL
-- соответствующих *Seq, кодом вот такого вида:
if (LongSeq.operator->() != NULL) da_type = Tango::DEV_LONG; else if (ShortSeq.operator->() != NULL) { da_type = data_type; } else if (DoubleSeq.operator->() != NULL) da_type = Tango::DEV_DOUBLE; else if (FloatSeq.operator->() != NULL) da_type = Tango::DEV_FLOAT; . . .
nnnSeq._pd_seq != NULL
" исполняется какая-то хиромантия с
привлечением "operator->
" (может, это такой плюсовый способ
проверки "объект не-NULL"?).
DEVVAR_*
; видимо, "массивовость"
определяется уже по data_format
, а тут всегда отдаётся
скалярный тип.
DEV_nnn
(и
DEVVAR_nnnARRAY
) определяется в
lib/cpp/server/tango_const.h. Что любопытно:
DATA_TYPE_UNKNOWN
;
да, НЕ "DEV_nnn
".
Причём оно =100
, а не =0, как казалось бы разумнее.
Потому, что =0 занято DEV_VOID
.
enum CmdArgType
".
Т.е., не просто абстрактные "типы данных", а "типы аргументов команд".
С корректным построением абстракций у авторов TANGO явно какие-то проблемы...
Собственно действие:
dtype
, значение которой прописывается
в каждой альтернативе switch()
'а, и потом именно она
складывается в hi->current_dtype
.
switch()
теперь делается по значению, добытому
посредством get_type()
.
21.11.2023: ага-ага -- фиг, ошибка/опечатка там была:
switch()
делался по hi->dtype
вместо
добытого data_type
; при том, что альтернативы были из
Tango::DEV_nnn
, так что никогда ничего не срабатывало.
Исправлено.
int32
.
Ну всё, теперь надо б опять проверить.
...а ещё как-то аналогично б с записью придумать, чтобы пыталось писать в "нативный" тип...
А ещё так возникает
потенциальная проблема переполнения: место аллокировано под один тип, а
читать пытаемся другой, возможно, более объёмный. Конкретно для СКАЛЯРОВ
при НЫНЕШНЕЙ реализации проблемы нет: вычитывается в как минимум
CxAnyVal_t
, который по определению вместит любое из
поддерживаемых значений. А вот для ВЕКТОРОВ (в т.ч. СТРОК) это уже не
будет так. 05.07.2024: всё, проблема решена --
введением 30-06-2024 поля current_val_allocd
и ограничения
(через value_bufsize
) складируемого этим объёмом; 01-07-2024
проверено, что это работает.
27.08.2022@~12:30, гуляя с папой в парке между М-46 и Академией: а можно ли как-то вычитать СВОЙСТВА указанного атрибута, включая его data_type? Тогда можно б было при отправке пытаться конвертировать передаваемые значения в нужный тип, избегая exception'а по несоответствию типов.
Стоит поискать среди методов класса DeviceProxy
.
27.08.2022: ну есть там оно, КАК БЫ:
attribute*query*()
и
get_attribute*()
(в т.ч. и "дай инфу обо всех properties" и
<vector>
'ные -- для сразу пачкой).
И среди них -- attribute_query()
.
(Определения в файле lib/cpp/client/DeviceProxy.h)
AttributeInfo
или
её расширенном варианте-наследнике AttributeInfoEx
,
отнаследованных от DeviceAttributeConfig
.
(Определена в файле lib/cpp/client/devapi.h)
data_type
.
min_value
/max_value
и
min_alarm
/max_alarm
, но "прикол" в том, что они
имеют тип string
!!!
Т.е., есть-то оно как бы есть: НО!
Как и вообще практически всё в этом клятом TANGO.
Т.е., каждый такой вызов будет зависать на весьма макроскопическое время сетевого round-trip'а.
А всю "параллельность" они, похоже, сваливают на multithreading.
И вот как эту дебильную проблему решать -- я уже не представляю...
28.08.2022@утро: вдогонку по вопросу "что же делать с этим дебилизмом?" (размышления, в первую очередь, -- в интересах epics2tango+epics2smth):
И да, в голову лезут всякие мысли о возможностях реализации, примерно такого вида:
Например, если "резолвер" создаёт объект DeviceProxy
, то он
дожидается коннекта, в случае успеха -- сигнализирует о готовности
"ведущему", после чего этого объекта уже более не касается; если нужно будет
его грохнуть -- то это делает "ведущий".
Обсуждение:
Да, при больших нагрузках pipe'ы будут затыкаться, но это будет приводить лишь к тормозам -- без каких-либо фатальных последствий.
O_NONBLOCK
на пишущем pipe со стороны "главного".
socketpair(,AF_UNIX,SOCK_DGRAM)
: датаграммы по определению
уходят либо целиком, либо никак -- это защитит от возможной отправки лишь
куска сообщения.
...Или "ДОЛЖЕН" -- ведь это потенциально зависающая операция?
Напрашивается только создание DeviceProxy ПАРАМИ.
...ну либо вообще вынос ВСЕГО взаимодействия с TANGO во 2-й thread, а основной только взаимодействует с libCAS да переправляет запросы/ответы туда-обратно...
Т.е.,
Ужас-то какой, прости господи...
28.08.2022: как минимум ПОКА -- будем делать
по-тупому, вызывать всё синхронно. Ибо реализовывать тот цирк с
конями thread'ами -- дикая маета.
14.05.2023: по результату штудирования исходника и изучения записей -- некоторые соображения:
...и потом взять от этого сервера файл конфигурации, чтобы воспроизводить у себя локально.
И чтоб в идеале вообще обходиться без суффиксов "@T" -- абсолютно-то они необходимы только для команды и результата ("@c" и "@r").
!USE_PUSH_MODEL
".
14.05.2023: переделал, так что
TangoHeartbeatProc()
теперь определяется и активируется всегда,
а в условии "!USE_PUSH_MODEL
" только определение и вызов
CheckEventsIterator()
. Проверил -- компилируется в обоих
вариантах.
03.06.2023@Томск-Беленца-6-23: по размышлениям "да как же всё-таки там всё организовать?!" в последние несколько дней сложился такой список действий хотя бы для начала:
DEVATTR
,
DEVPROP
, ATTRPROP
, COMMAND
,
RESULT
; и держать вид канала не в отдельном поле
chtype
, а прямо в in_use
.
p_ar
,
Можно сделать, чтобы последовательности символов ".." и "//" превращались бы в стрелку.
@~16:00: сделано, в процессе "общего улучшения".
Мутновато и неприятно.
cda_d_tango_privrec_t
ОТДЕЛЬНЫЙ SLOTARRAY
cmds_list, в котором создавать команды ("привязывая" их к dpx'ам), а уж к нему
"привязывать" hwr-ячейки и команд, и результатов. И чтоб они регистрировали
в cmd-ячейках свои "callback'и".
Да, громоздковато -- учитывая, что на каждую cmd-ячейку обычно будет приходиться по не более 1 команды и 1 результата. Но зато такая архитектура выглядит понадёжнее.
14.11.2023: возвращаемся к работе над модулем. Для начала улучшения, продуманные 03-06-2023:
CHTYPE_nnn
доделан до полноты в соответствии с
проектом (ATTRIBUTE и PROPERTY переименованы в DEVATTR и DEVPROP, добавился
ATTRPROP).
CHTYPE_UNUSED
=0 -- в интересах
следующего пункта.
Hwr
" переведён на использование в
in_use
этих значений; при создании ячейки ей ставится
CHTYPE_DEVATTR
, затем в cda_d_tango_new_chan()
прописывается нужное.
chtype
удалено.
dup_name
разделены по
функциям: теперь первый выполняет трансформации (в него переехало
'%'->'='), а второй уже выполняет подсчёты/учёт.
p_ar
.
sl3<p_ar
", т.к. для
DEVATTR формат вообще "DOMAIN/FAMILY/DEVICE->PROPERTY", т.е., слэшей
всего 2, а не 3, и это пока не поддерживается никак.
16.11.2023@утро: добавлена;
вместе с определением chtype по формату, если оно возможно (при наличии
стрелки: nsls=2 -- DEVPROP, nsls=3 -- ATTRPROP). Плюс далее добавлена
проверка, что если получившийся chtype != CHTYPE_DEVATTR
, то
"не поддерживается".
channame
/channamelen
в блоке «Re-add "HOST:PORT/" (if present) to channel name».
А то сейчас это всё выглядит ужасно.
Проверяем на реальном Tango-сервере на СКИФе (правда, собрано с 9.2.5a, а запускаем с 9.3.4 -- ведь требует, по данным ldd, libtango.so.9). Падает... Причём падает как с "несуществующим" устройством -- tango::RFGunPhasemeter/testZ/2/pointToTime ('Z' лишнее), так и с реальным (без 'Z').
create_hwr_subscription(RFGunPhasemeter/test/2|pointToTimeZ): subscribe_event raised an exception
(т.е., НЕИЗВЕСТНЫЙ exception, ловимый по fallback'у catch (...)
-- CORBA::Exception
выше ловится явно).
free(): invalid pointer Aborted
15.11.2023: собрал таки всё локально на pult-l, с тамошними библиотеками.
push_event(hwr=1:"GunTimerDS/srpa/1!temperature"): Event properties (abs_change or rel_change) for attribute temperature are not set
-- хотя jive показывает, что у этого атрибута присутствует 3 listener'а.push_event(hwr=1:"RFGunPhasemeter/test/2!dataLenght"): The polling (necessary to send events) for the attribute datalenght is not started
(Строки разбиты на 2 для этого файла, разделитель '!' -- это наша диагностика.)
16.11.2023: Лёша Герасёв напомнил, что в C++ постоянно вылазят проблемы с ABI: если даже просто разными версиями компилятора собраны части, то может не срастись.
Попробовать-таки вариант статической сборки, который с 9.2.5a не работал принципиально?
Очевидно, это и есть причина, по которой связка "cda_d_tango.so собрана с 9.2.5a, а запускаем с 9.3.4" не работает. Ну и не будем тогда с ней возиться :).
Найдена 2-я страница 4-страничного обсуждения "Attribute Events gets missed" за неясную дату "5 years ago".
Проверено (путём сборки с AM_DEFAULT_VERBOSITY=1
и
заглядывания в вывод) -- да-да, так и есть, кладётся в них обоих.
20.11.2023: в том обсуждении "Attribute Events gets missed" упоминалось, что (bold мой)
If you are using a generic client like atkpanel, I think it tries to subscribe to change events by default and if this fails, it will subscribe to periodic events. If you didn't change the period configuration parameter of the periodic event for the attribute you are subscribing to, you will get the default period value, which is 1 second.
Поэтому -- а не попробовать ли вместо CHANGE_EVENT
помониторить PERIODIC_EVENT
?
store_current_value()
.
Ну-с, и что теперь? :D
@вечер: а у нас ведь всё проверяется в PULL-модели; надо б попробовать PUSH-модель.
21.11.2023@утро, зарядка: учитывая, что код весьма хитрозавёрнутый, то может быть и какой-то внутренний косяк в нём.
-c
/-r
change/periodic).
-p
/-l
).
21.11.2023: пробуем USE_PUSH_MODEL=1.
Прогон под GDB показал ("frame 2; print *myevent"), что событие приходит
(event = "periodic"
), но
attr_value = 0x0
(NULL),
а оно передаётся в store_current_value()
, вот и SIGSEGV.
Что ещё интересно, в содержимом события err = true
и дальше
(переносы строк мои, а то это читать невозможно).errors = {<_CORBA_Unbounded_Sequence<Tango::DevError>> = {<_CORBA_Sequence<Tango::DevError>> = {pd_max = 3, pd_len = 3, pd_rel = true, pd_bounded = false, pd_buf = 0x55555561f748}, <No data fields>}, <No data fields>}}
Разница только в том, что event = "change"
.
__FUNCTION__
на входе в
cda_d_tango_EventCallBack::push_event()
:
event = "change", attr_value = 0x0, err = true
каждые 10с "Event properties..."
event = "change", attr_value = 0x0, err = true
каждые 10с "The polling...".
event = "periodic", attr_value = 0x0, err = true
каждые 10с "The polling...".
push_event()
в ветке
USE_PUSH_MODEL
отсутствовала проверка
"if (myevent->err) {РУГНИСЬ; return;}
"
-- вот оно и вылетало. Исправлено путём выноса проверки в начало метода, ДО
#if
.
temperature
-- 2000ms).
А вот сообщение "The polling..." -- начинается через 5 секунд.
TANGO_HEARTBEAT_USECS = 5000000
"?
Но в режиме PUSH=0 в каждом 20-секундном цикле прилетает по 2 сообщения.
Т.е.,
store_current_value()
возвращает r!=0 -- а почему?
Разобрался!!! В store_current_value()
и была ошибка,
идиотскейшая: там было
switch (hi->dtype)
вместо корректного
switch (data_type)
Исправил -- и в PERIODIC-режиме данные температуры стали приходить!!!
Выводы/результаты:
22.11.2023: вдогонку: не хватает наличия
cda_dat_p_report_rslvstat()
и cda_dat_p_set_ready()
.
CDA_REF_R_RSLVSTAT
, по которому cdaclient выдаёт hwinfo.
23.11.2023@обед, душ: а ещё оно критично для работы epics2cda -- именно по RSLVSTЕ_FOUND определяется наличие запрошенного канала.
is_ready
, без которого не будет работать запись.
Но проблема в том, что неясно, где вообще можно эти события/вызовы делать -- в TANGO сходу не видно своего сообщения "найдено/не-найдено".
23.11.2023: решил немного поразбираться, как же всё-таки организовать асинхронность.
Источники информации --
Итак:
long
"asynchronous call identifier", который можно время от
времени poll'ить вызовами вроде read_reply()
и
write_reply()
, передавая им этот идентификатор, а при
готовности получая DeviceAttribute *
с результатом.
AttributeProxy::read_asynch()
(лучше
CallBack-вариант -- с 1 параметром), ...
...сводящийся к вызову dev_proxy->read_attribute_asynch()
.
Ему передаётся просто имя атрибута (хоть char*
, хоть
string
).
При использовании CallBack'а у него должен быть определён метод
attr_read()
. Он получает
AttrReadEvent *
,
в котором кроме err
есть ВЕКТОРА имён атрибутов и собственно
атрибутов -- vector<DeviceAttribute> *argout
; как оттуда
брать 1 штуку -- фиг знает; argout[0]
?
AttributeProxy::write_asynch()
(лучше
CallBack-вариант -- с 2 параметрами),
...сводящийся к вызову dev_proxy->write_attribute_asynch()
.
Этому передаётся DeviceAttribute
, содержащий имя и значение.
При использовании CallBack'а у него должен быть определён метод
attr_written()
. Он получает
AttrWrittenEvent *
,
в котором есть только поле err
, а никакого
attr_value
НЕТ; впрочем, и не нужно.
В документации ничего на эту тему не видно.
Как не видно и ничего, могущего проканать за RSLVSTAT.
Единственное, что удалось "примерно" понять -- строка
di->dev = new Tango::DeviceProxy(di->dev_name);
отрабатывает за 3 миллисекунды (определено путём выдачи
strcurtime_msc()
вокруг неё).
24.11.2023: приступаем.
cda_d_tango_EventCallBack::attr_written()
--
специально пустая, т.к. делать вроде ничего не требуется.
cda_d_tango_EventCallBack::attr_read()
.
Внутренности -- копия
cda_d_tango_EventCallBack::push_event()
с минимальной
адаптацией.
Вот только ссылка "argout[0]
" не проканала, пришлось для
компилируемости ставить диковатую конструкцию
&((*(are->argout))[0])
(но и "&(*(are->argout))[0]
" возможно бы прокатило)
-- т.к.
argout
имеет тип не vector<>
, а
vector<>*
-- потому разыменование *
перед
индексированием [0]
*(argout[0])
" из-за приоритетов;
DeviceAttribute
,
потому взятие указателя.
cda_d_tango_snd_data()
вставлена за-#if'ленная
(26.11.2023: #if 1
заменено на
#if USE_PUSH_MODEL
)
альтернатива исполнения записи как
di->dev->write_attribute_asynch(*da, *(hi->cb));
И да, "унифицированность" ихнего API просто поражает: в
subscribe_event()
надо передавать УКАЗАТЕЛЬ --
CallBack *cb
,
тут же ССЫЛКУ -- CallBack &cb
(лавров.jpg).
cda_d_tango_req_read()
, делающая
read_attribute_asynch()
. Ритуальный скелет скопирован с
cda_d_tango_snd_data()
.
di->dev==NULL
, то выставить errno=ENOTCONN
и
вернуть CDA_PROCESS_ERR
.
25.11.2023: cda_d_cx -- а там не особо корректно проверяется: при
неприконнекченности вернётся SUCCESS. Правда, snd_data() там защищается
cda_core'ом -- при неготовности как сервера, так и ref'а оно просто
закэширует данные, но не вызовет отправку; а вот req_read()-то нет --
cda_req_ref_read
проверяет лишь корректность ref'а и
определённость метода в dat-плагине.
26.11.2023: добавлено
"else return CDA_PROCESS_ERR;
" в оба метода.
cda_d_tango_new_chan()
и
DoSubscribeIterator()
добавлена поддержка
CDA_DATAREF_OPT_NOMONITOR
в виде НЕвызова
create_hwr_subscription()
при взведённости флага.
Для чего добавлено hwrinfo_t.options
и сохранение в нём
параметра канала.
26.11.2023: остаётся только вопрос технологии/идеологии: а будет ли работать это всё "заказывание событий через cb" в PULL-режиме? Ведь в нём делается
di->dev->get_events(hi->subscr_event_id, hi->cb);
-- т.е., проверяются только события от подписки на обновления.
cb
'шными
вариантами исполнения асинхронной записи, запоминая
long
-идентификаторы запросов и полля их вместе с
subscr_event_id
.
Ну ладно, можно смотреть, что запрос на чтение уже есть
("read_event_id != -1
", аналогично cxsd_hw'шному
rd_req
) и последующий просто игнорировать -- результат-то всё
равно придёт.
Так что игнорировать их нельзя, ...
...но и перепрописывать следующими предыдущие event ID тоже нельзя -- подтверждения на старые так и останутся висеть "непрочитанными".
Мда, "охренительно продуманный" API...
@вечер: дальше-то что делать? Видимо, теперь, с минимальной отлаженностью на реальном сервере и чуть улучшившимся пониманием, сделать-таки tangodb-файл для TangoTest и тестировать на нём -- как минимум запись можно спокойно и безопасно проверить, плюс "диаграмму событий" при записи и при просто обновлении в предсказуемом окружении.
27.11.2023: попробовал -- авотхрен.
TangoTest/instname/DEVICE/TangoTest: "TangoTest/instname/1" TangoTest/instname/1->polled_attr: ampli,\ 2000
(оно после запуска добавило в файл своего мусора)LD_LIBRARY_PATH=/home/user/compile/tango-9.2.5a/lib/cpp/client/.libs:/home/user/compile/tango-9.2.5a/lib/cpp/log4tango/src/.libs \ /home/user/compile/tango-9.2.5a/cppserver/tangotest/.libs/TangoTest instname -ORBendPoint giop:tcp::10001 -file=work/frgn4cx/tango/cda/TangoTest.tangodb
Can't create notifd event supplier. Notifd event not available Tango exception Severity = ERROR Error reason = API_NotSupported Desc : Call to a Filedatabase not implemented. Origin : DbGetClassPipeProperty Tango exception Severity = ERROR Error reason = API_DatabaseAccess Desc : Can't get class pipe properties for class TangoTest Origin : MultiClassPipe::init_class_pipe Received a CORBA_Exception Exiting
В tango-9.3.4/ идентично.
(Поиск по «"Can't get class pipe properties"» даёт ровно то же)
И там Andy Gotz 25-10-2017 прямым текстом заявляет "This feature was not planned from the beginning. How urgent is it?", после чего пометил как "enhancement, low priority" и не-bug.
(Да, нумерация багов странная (из-за того, что это PyTango?); и почему Google этот баг не нашёл -- загадка, т.к. искомая строка в тексте имеется.)
БЕЗ каких-либо пояснений (как это делается у RH, со ссылкой на errata или иной бюллетень).
Хотя какие-то релизы "cppTango" с бОльшими номерами версий вроде есть -- судя по "cppTango/TangoSourceDistribution: New release 9.4.2 Release" (это НОВОСТЬ, которую я смог нарыть через ленту новостей News, куда я попал из предыдущей новости про 9.3.4). Подозрение подкрепилось нагугленной же страницей "Long Term Support Versions" с фразой "cppTango 9.4.0 was released on September 30th 2022".
Я это cppTango-9.4.2.tar.gz по ссылке со страницы "9.4.2 · tango-controls / cppTango · GitLab" даже скачал (всего 5.3MB и 14MB распакованное), но как собирать -- фиг поймёшь.
Итого: СЕЙЧАС никакого варианта решения не видно. Обломитесь.
28.11.2023: попробовал "поддерживать" DEV_ENUM, обходясь с ним как с int32:
case Tango::DEV_ENUM: *(attr_value) >> *(( int32*)value_p); dtype = CXDTYPE_INT32; break;
Смысл -- что при очередной попытке "ну пусть хоть что-нибудь заработает, давай вот этот атрибут попробуем" у атрибута "RFGunPhasemeter/test/2/modeAB" оказался тип 29, являющийся этим DEV_ENUM.
Однако фиг -- свелось к SIGSEGV, причём существенно ПОЗЖЕ, уже на
отправке hwr'а в event_pipe[PIPE_WR_SIDE]
...
28.11.2023: поговорил с Сенченко, задал ему интересующие вопросы.
Да, версия "полного дистрибутива" -- 9.3.4, но они перешли на схему с LTS и теперь есть отдельно более свежие версии, только искать их надо в git'е.
Да, "cppTango" -- это C++'ное ядро. Есть даже 9.5. А следующее обещают уже 10. И вроде как теперь уже НЕ обещают обез-CORBA-вливание.
Собирать -- "cmake".
Сенченко не знает, не пользовался. "Не работает файловая БД? Ну запусти обычную БД!"
(От меня: дичь.)
Тут у TANGO действительно косяк: никак, и это у них висит известным багом, который пока почему-то не могут исправить, возможно, в силу архитектуры.
Если требуется -- то надо поступать как с "измеряемыми" атрибутами, делая их "polled" и указывая свойства, определяющие, что "изменилось".
Это short; и список строк к нему прилагается.
В общем случае нет. Будет выброшено исключение при попытке прочитать/записать/вызвать.
Более того, tango само закрывает подключение к серверу если клиент не обращался к девайсу в течение некоторого времени (минута или что-то вроде). Если используются события, то при выключение сервера/потери соединения прилетит событие с ошибкой (API_EventTimeout). Но это не оперативное событие.
Есть еще вот такая табличка https://www.esrf.fr/computing/cs/tango/tango_doc/kernel_doc/cpp_doc/recon.html, но тут только и попытках общения с сервером. Может поможет.
Уточнение вопроса: если есть потребность уметь понимать (желательно асинхронно) существует ли некий атрибут? Например, некоей программе-аналогу caget'а говорят прочитать атрибут с указанным именем, и она должна либо прочитать его и напечатать ответ, либо выдать на stderr строку "DOESN'T EXIST". Ну или вообще просто некая утилитка командной строки does_attribute_exist, должная вернуть код 0 при наличии указанного атрибута либо код 1 при его отсутствии либо при отсутствии связи. Можно ли это как-то в cppTango элегантно сделать?
Попробуй через этот метод: Tango::DeviceProxy::get_attribute_list()
От меня: get_attribute_list()
-- т.е., получить список
атрибутов и искать в нём самому! Да ещё и получение СИНХРОННОЕ! Блин...
29.11.2023: ну и что теперь дальше делать, учитывая насколько всё убого и печально в этой французской дичи?
Напрашиваются следующие вроде несложные, наверняка реализуемые и в любом случае полезные шаги:
30.11.2023: ну пробуем.
DeviceAttribute
есть поле
TimeVal time;
TimeVal
для
не-WIN32 определяется в cppserver/database/DataBase.h следующим образом:
// Define time measuremnt type (depends on OS) #ifndef WIN32 # define TimeVal struct timeval # define GetTime(t) gettimeofday(&t, NULL); # define Elapsed(before, after) \ 1000.0*(after.tv_sec-before.tv_sec) + \ ((double)after.tv_usec-before.tv_usec) / 1000 #else
struct timeval
, хоть и под другим именем.
hi->current_timestamp.sec = attr_value->time.tv_sec; hi->current_timestamp.nsec = attr_value->time.tv_usec * 1000;
struct TimeVal { long tv_sec; long tv_usec; long tv_nsec; };
Нашёл потому, что в devapi_attr.cpp встретил
"time.tv_nsec = 0;
",
удивился и сделал "grep -r tv_nsec tango-9.3.4
".
Похоже, во время сборки из этого файла генерится lib/cpp/server/idl/tango.h (это видно по комментарию
первой строкой), который уже и// This file is generated by omniidl (C++ backend)- omniORB_4_2. Do not edit.
#include
'ится всеми. В логе
сборки этого генерения найти не удалось, а вот пропустив
cda_d_tango.cpp через "gcc -E
" -- очень даже видно
диковатый кусок (закомменчен тут ниже), в который превращается тот
struct TimeVal
в tango.h.
@вечер: проверил, после фиксенья проблемы в
следующем пункте -- да, timestamp добывается: это очевидно потому, что
выводимый cdaclient'ом по "-D8
" на несколько секунд отличается
от выводимого им же по "-DT
" локального -- явно из-за
расхождения часов.
int32
на
uint16
-- не помогло.
hi->me
==NULL;
bt
" показал, что
callback вызывается ДО завершения заказа подписки subscribe_event()
-- т.е., прямо из него, а это ещё ДО прописывания поля me
,
производимого в AddHwrToSrv()
.
Почему это не проявлялось раньше, с другими типами атрибутов -- загадка.
AddHwrToSrv()
;
cda_dat_p_set_hwr()
" в точку ДО заказа подписки.
@вечер: а присылает ли? Ведь
все "operator >>
" проверяют, что внутри лежит тот тип,
что в выходном аргументе, и если нет -- то просто вертают
false
, более ничего не делая; а 0 просто в буфере с рождения.
01.12.2023@утро-завтрак: стал разбираться. В
DeviceAttribute::get_type()
(вызвавшем удивление ещё
26-08-2022) фраза
"return Tango::DEV_ENUM
" отсутствует, в отличие от всех
остальных типов, но зато есть вот такое:
-- т.е., конкретно при обнаружении short (причём не-unsigned!) оно возвращает именно значение поляelse if (ShortSeq.operator->() != NULL) { return data_type; }
data_type
.
OK, переделал на int16
/CXDTYPE_INT16
, плюс
сделал печать булевского результата работы "operator >>
".
Проверено: при int16
результат 1, при uint16
-- 0.
Значение по-прежнему 0, но теперь уже настоящий -- проверено записью в буфер
числа 12345 перед получением.
Разобрался: это не он "присылает сразу пачку", а у меня был дебильный
косяк в event2pipe_proc()
: в качестве проверки успешности
вычитывания очередного hwr
'а стояло
"if (r != sizeof(hwr))
"
вместо
"if (r == sizeof(hwr))
"
-- не говоря уж о том, что отсутствовало условие прерывания
"else break;
".
И работала эта дичь потому, что на ПЕРВОЙ итерации добывалось значение
hwr
, но более ничего не делалось, а на СЛЕДУЮЩИХ 99 уже
возвращалось read()=-1 и чтение не делалось, но значение имелось от первой и
в ошибочно исполняемых внутренностях использовалось.
Всё потому, что так "креативно, но тупо" скопировал код из
cda_d_insrv.c::insrv_fd_p()
if (r != sizeof(hwr)) { /*!!!*/ return; }
Оба ляпа исправил, нормализовалось.
01.12.2023: ну разобрался с простыми вещами -- DEV_ENUM и timestamp'ы. А дальше-то что делать? Напрашивается такая цепочка несложных действий:
В DeviceAttribute она сделана исключительно в виде "C++ std::vector<>", а не обычных массивов. Следовательно, надо
DeviceAttribute
-- что ТАМ на тему
взаимодействия с обычными массивами, включая возможность инициализации из
них при new
.
06.12.2023: чуток вышеозначенного сделано.
cda_d_tango_req_read()
сделано, что "тело" определяется
только при USE_PUSH_MODEL
, а иначе
"return CDA_PROCESS_ERR;
".
...но это, естественно, пока не протестировано.
11.12.2023: переделано -- работа теперь есть в обоих режиммах. Плюс в обоих режимах проверено (прямым вызовом, описанным пунктом ниже).
cda_d_tango_req_read()
(26.06.2024: может,
в начало create_hwr_subscription()
, а?) вставлен вызов
cda_d_tango_req_read()
.
Фиг -- НИКАКОГО отклика. Ни на атрибуты, которые "The polling ... is not
started" (datalenght
), ни на те, которые вроде бы опрашиваются
без ошибок (modeCD
). Просто метод attr_read()
НЕ
вызывается.
В чём дело -- неясно: то ли сигнатура метода не та (какой-нибудь
const
нужен?), то ли реально что-то не пашет...
10.12.2023: ещё разок глазами посмотрел -- да нифига, сигнатура метода именно та (скопировано ведь было из прототипа в devasyn.h).
DeviceAttribute
: п.2 -- да, там ТОЛЬКО
vector<>
; ни конструкторы (за единственным исключением
инициализации из char*
), ни операторы обмена <<
и >>
-- НИКТО не понимает вариант SOMETYPE*
.
22.06.2024: см. отдельный пункт-подраздел ниже за позавчера и сегодня. В принципе, всё несложно.
11.12.2023: разбираемся с чтением дальше -- ПРОРЫВ!!!
cda_d_tango_req_read()
поддержку и
режима PULL тоже.
read_call_id
для хранения
идентификатора запроса чтения; введён надлежащий его менеджмент --
инициализация в -1
и проч.
Всё это определяется только при !USE_PUSH_MODEL
.
cda_d_tango_req_read()
в него складывается результат
вызова read_attribute_asynch(hi->chn_name)
(БЕЗ-cb'шного!).
Замечание: НЕ делается проверка "если read_call_id!=-1, то повторять запрос чтение не надо".
CheckEventsIterator()
добавлено вычитывание:
(там ещёattr_value = NULL; if (di->dev != NULL && hi->read_call_id != -1) attr_value = di->dev->read_attribute_reply(hi->read_call_id); if (attr_value != NULL) { hi->read_call_id = -1; if (store_current_value (hi, attr_value) == 0) return_current_value(hi); delete attr_value; }
try
/catch
'ами всё обёрнуто).
Т.е., при наличии отправленного запроса пытаемся получить его результат, и если его возвращают, то "передаём" обычным образом и сбрасываем read_call_id=-1.
...и вот оно стало возвращать данные!
set_asynch_cb_sub_model(Tango::PUSH_CALLBACK);
". Сейчас в конец
cda_d_tango_init_m()
добавлен вот такой кусок:
Tango::ApiUtil *aui = Tango::ApiUtil::instance(); Tango::cb_sub_model m = aui->get_asynch_cb_sub_model(); fprintf(stderr, "cb_sub_model=%d (PUSH_CALLBACK=%d, PULL_CALLBACK=%d)\n", (int)m, (int)(Tango::PUSH_CALLBACK), (int)(Tango::PULL_CALLBACK)); #if USE_PUSH_MODEL aui->set_asynch_cb_sub_model(Tango::PUSH_CALLBACK); #else aui->set_asynch_cb_sub_model(Tango::PULL_CALLBACK); #endif
Сначала сделал просто чтение "текущего" режима, и оно показало значение 1
-- т.е., PULL_CALLBACK
, в то время как нам-то нужен
PUSH_CALLBACK
!
И после того, как добавил уставление Tango::PUSH_CALLBACK
,
тогда стал вызываться attr_read()
!
Что больше всего доставляет: в "документации" "4. Writing a TANGO client using TANGO APIs", 4.5.2 Asynchronous model нормально это НЕ СКАЗАНО! Там лишь абстрактное общее перечисление "вам надо использовать вот то-то", где среди прочего упомянуто "ApiUtil::set_asynch_cb_sub_model()", но КОНКРЕТИКИ -- "для перевода библиотеки в асинхронную модель сделайте то-то" (и КОГДА сделать) -- НЕТУ!!!
ЗЫ: хотя в "6. The TANGO C++ Application Programmer Interface", 6.7.6 void ApiUtil::set_asynch_cb_sub_model(cb_sub_model model) таки сказано "By default, all Tango client using asynchronous callback model are in pull sub-model. This call must be used to switch to the push sub-model."; но НЕ сказано когда...
Итого -- задышало!
@вечер: А если
-- ApiUtil::get_asynch_replies()
использовать можно ли тогда
обойтись одним его вызовом прямо в TangoHeartbeatProc()
вместо
запоминания идентификаторов запросов и индивидуальных поштучных проверок в
CheckEventsIterator()
? Это сняло бы проблему от
26-11-2023 "запросов-то ... может поступить несколько штук подряд"
(особо актуальную в отношении записи).
12.12.2023: изучаем ApiUtil с точки зрения возможного прифита -- можно ли через него как-то повысить удобство добычи событий для PULL-режима. В ApiUtil.h всего 265 строк и в 9.2.5a и 9.3.4 он одинаковый.
ca_add_fd_registration()
в libca?".
Увы -- ничего такого не видно.
Да и понятно, почему -- оно ведь для коммуникации использует CORBA и ZMQ, а у тех свои заморочки на эту тему.
TangoHeartbeatProc()
вызывание
CheckEventsIterator()
для каждого устройства на единственный
вызов ApiUtil::get_asynch_replies()
.
Фиг -- никакого отклика вообще.
Но оно и понятно: ведь callback'и-то в не-PUSH-модели не регистрируются, так что и вызывать нечего.
read_attribute_asynch()
для не-PUSH
заунифицирован с PUSH.
Помогло -- да, отклики на чтение стали, в виде вызовов
attr_read()
, прилетать!
Поэтому пришлось-таки вернуть по-устройственные вызовы
CheckEventsIterator()
.
read_attribute_reply()
на общий
ApiUtil::get_asynch_replies()
введён препроцессорный символ
USE_GET_ASYNCH_REPLIES
, по умолчанию сделанный =1, при котором
почти всё запараллелено с USE_PUSH_MODEL
(не-определение
read_call_id
и отсутствие его менеджмента, вызов
read_attribute_asynch()
в варианте с cb
),
а вызов get_asynch_replies()
делается при нём взведённом.
Вроде всё работает.
В качестве резюме/брюзжания: всё-таки ОЧЕНЬ СУМБУРНО в Tango сделана "асинхронность":
set_asynch_cb_sub_model(PULL_CALLBACK)
".
Что создаёт впечатление, что оно и не нужно.
get_asynch_replies()
отрабатывает только callback'и от
ОДИНОЧНЫХ вызовов, а event'ы -- нет, их надо поллить индивидуально.
19.12.2023@~15:00, сидя на защите
Ремнёва: к вопросу о том, что фиг добудешь событие "канал
найден/не-найден, а состояние-то наверх передавать надо!" (как минимум для
pipe2cda необходимо) -- ну так просто завести в hwrinfo_t
булевский флажок "было подтверждено!", взводить его по любому ПОЛУЧЕНИЮ
данных, а если перед взведением оно ещё НЕ взведено, то сигнализировать
CDA_RSLVSTAT_FOUND cda_dat_p_set_ready().
21.12.2023@утро, завтрак: ЗЫ:
в cda_d_vcas.c примерно так и делается -- есть
флажок rslvstat_reported
, который если ==0, то при первом же
получении данных делается CDA_RSLVSTAT_FOUND
и флаг
взводится.
20.12.2023: только надо бы чуть более творчески:
21.12.2023@утро, завтрак:
изначально прописывать в него CDA_RSLVSTAT_SEARCHING
,
"взводить" -- в FOUND, сбрасывать -- в NOTFOUND.
21.12.2023: сделано практически по вышеприведённому проекту.
hwrinfo_t
добавлено поле
last_known_rslvstat
, при создании в
cda_d_tango_new_chan()
туда прописывается
CDA_RSLVSTAT_SEARCHING
.
return_current_value()
проверяется, что если
hi>last_known_rslvstat != CDA_RSLVSTAT_FOUND
-- взвести и вызвать cda_dat_p_report_rslvstat(,FOUND)
,
cda_dat_p_set_ready(,1)
.
Замечание: именно в нём потому, что он принципиально вызывается только из "главного" thread'а.
create_hwr_subscription()
в случае exception'а
симметричная проверка, что если
hi>last_known_rslvstat != CDA_RSLVSTAT_NOTFOUND
-- уставить это состояние и сигнализировать наверх NOTFOUND,0.
При этой деятельности пришлось уделять особое внимание тому, чтобы не ограсти проблем из-за multithreading'а.
HWR_VAL_*
,
имеющие отрицательные значения, за которыми в pipe пишется уже сам
hwr-виновник.
26.06.2024@вечер, лёжа в ванной, сделанной 10 чайниками: кстати, в TANGO ж у каждого устройства есть стандартное поле, вроде "state"; а как у НЕГО с возможностью опроса -- должон для для него запускаться поллинг или должны ли устанавливаться границы изменения? Или и так они есть? Если второе, то это поле -- как раз хорошая точка для тестирования чтения.
27.06.2024: ну посмотрел, мда... Они и ТУТ умудрились наплодить сложностей.
А есть ещё "Status", но это, похоже, описание?
чуть позже: да, Tango::DevString
,
"Reference part — Tango Controls 9.3.4 documentation",
раздел "Automatically added commands":
"This command gets the device status (stored in its
device_status data member) and returns it to the caller. The device status
is a variable of the string type.".
Tango::DevState
, код Tango::DEV_STATE
.
Сейчас-то в store_current_value()
оно закомментировано, с
комментом "convert to int32?"; видимо так и поступить.
void
). Так что -- увы, весь проект пока что State
, и Status
-- это И
команда, И атрибут.
The polling (necessary to send events) for the attribute status is not started The polling (necessary to send events) for the attribute state is not started
"А вы все тут -- молодцы!" (c) Лёша из "Квартет И"/"День радио".
27.06.2024: краткие планы на будущее:
vector<>
?).
CHTYPE_*
сделан корректно.
04.07.2024: проверил все варианты -- да.
CHTYPE_*
:
03.07.2024: ещё чуток тестирования с попыткой записи cdaclient'ом, во время которого наблюдены странности.
store_current_value()
: из attr_read()
и из
push_event()
.
Возможно, дело в том, что те оба вызова имеют место в ДРУГОМ thread'е и
потому до event2pipe_proc()
с
return_current_value()
ещё просто не успевает дойти между теми
двумя?
push_event()
, прежде чем cdaclient завершается. Причём эти 3
идут в течение целой секунды -- 18:35:40.530, 18:35:41.087, 18:35:41.530.
Возможно, причина та же -- cdaclient завершается в основном thread'е, а "прочие" всё ещё успевают потрепыхаться.
Если проблема в наличии другого thread'а, то нужно в диагностику добавить выдачу gettid()'а
04.07.2024: пробуем разобраться.
write_attribute_asynch()
с ДВУМЯ параметрами (второй -- cb) возвращает void
, так что от
неё проверять нечего, а exception'а она не генерит.
...с другой стороны -- в CX же ровно то же самое: запрос записи просто молча игнорируется.
my_gettid()
взят из
/hw4cx/drivers/daqmx/pxi6363_drv.c (а НЕ call_gettid()
из mt_cxscheduler.c).
sl_break()
делается аж дважды, но
это уже особенность устройства cda_d_tango.cpp: оно там в
event2pipe_proc()
вычитывает из входного pipe'а в цикле по
repcount до 100 раз, вот и генерит второе событие sl_break()
,
вроде как должного прервать основной цикл.
(Эти 2 события подряд -- начальный attr_read() плюс первый push_event(), приходящий через ~20ms, поэтому они успевают оба отправиться в event-pipe ещё до того, как основной thread успеет начать вычитывание.)
ProcessDatarefEvent()
после
sl_break()
-- "жизнь" продолжается и после него.
main()
после sl_main_loop()
--
т.е., прямо перед выходом -- даже после этого оно ещё пару секунд может
продрыгаться.
_exit(0)
приводит к мгновенному завершению.
Похоже, объяснение в том, что эти умники используют
atexit()
для исполнения каких-то весьма долгоиграющих действий:
EventConsumer()
делается atexit(leavefunc)
, ...
leavefunc()
значится комментарий
This function will be executed at process exit or when the main returned. It has to be executed to properly shutdown and destroy the ORB used by as a server by the event system. The ORB loop is in EventConsumer thread. Therefore, get a reference to it, shutdown the ORB and wait until the thread exit. It also destroys the heartbeat filters
Вот, видимо, это "wait" у них продолжается весьма макроскопическое время.
Прогнал cdaclient на pult-l под GDB, нажав Ctrl+C после завершающей диагностики и сделав "bt" -- ну да:
(Кстати, в этом же прогоне GDB показал, что порождается толпа thread'ов -- 7 штук.)#0 __pthread_clockjoin_ex (threadid=140737297688320, thread_return=0x7fffffffe150, clockid=<optimized out>, abstime=<optimized out>, block=<optimized out>) at pthread_join_common.c:145 #1 0x00007ffff7fbb08f in omni_thread::join(void**) () from /lib/x86_64-linux-gnu/libomnithread.so.4 #2 0x00007ffff788341b in Tango::EventConsumer::shutdown_keep_alive_thread() () from /lib/x86_64-linux-gnu/libtango.so.9 #3 0x00007ffff788348b in Tango::EventConsumer::shutdown() () from /lib/x86_64-linux-gnu/libtango.so.9 #4 0x00007ffff78835c8 in leavefunc () from /lib/x86_64-linux-gnu/libtango.so.9 #5 0x00007ffff7cc54d7 in __run_exit_handlers (status=0, listp=0x7ffff7e58718 <__exit_funcs>, run_list_atexit=run_list_atexit@entry=true, run_dtors=run_dtors@entry=true) at exit.c:108 #6 0x00007ffff7cc567a in __GI_exit (status=<optimized out>) at exit.c:139 #7 0x00007ffff7cadd11 in __libc_start_main (main=0x55555555d630 <main>, argc=3, argv=0x7fffffffe338, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe328) at ../csu/libc-start.c:342 #8 0x000055555555e2ca in _start ()
atexit(clean_lock)
, но лишь в DeviceProxy::lock()
,
так что не факт, что оно в нашем случае исполняется (да и длительность его
исполнения неочевидна).
09.07.2024: к вопросу "как бы всё-таки поддерживать запись в атрибуты и команды DEV_BOOL и DEV_VOID, учитывая, что в CX нет ни отдельного булевского типа, ни void":
int32
, то при "отправке туда" как догадаться,
что надо какой-то из INT-типов вдруг преобразовывать в bool
?
CXDTYPE_INT8
: в Tango ведь всё равно нет "знакового 8-битного".
Но а если когда-то появится? Да и вообще кривоватое и неочевидное решение.
10.07.2024: хотя идея выглядит соблазнительно потому, что INT8 -- это dtype-префикс "@b:", совпадающий с суффиксом "@b".
10.07.2024: чуть позже -- да, и этот вариант тоже реализован.
DEV_ENUM
на
запись поддерживать: ввести суффикс-префикс @e,
флаг BHVR_ENUM
, при наличии первого взводить второй, и уже при
его взведённости сбагривать в da
/dd
те же данные
под вывеской Tango::DevEnum
.
Начав было делать,
typedef DevShort DevEnum;
DeviceAttribute
каких-либо методов с DevEnum
.
Tango::DevState
.
Вопрос только -- ЗАЧЕМ такое может понадобиться?
10.07.2024: реализуем.
hwrinfo_t
поле bhvr
для хранения
"флагов поведения".
BHVR_VOID
,
BHVR_BOOL
, BHVR_STATE
.
bhvr=0
.
bhvr
, а в at_c
берётся следующий символ (который
может быть и '\0' -- это приведёт к CHTYPE_DEVATTR
).
CHTYPE_RESULT
и взводится BHVR_STATE
.
13.07.2024: появилась идея, как решить давнюю
проблему-дилемму "подписываться на CHANGE_EVENT
или на
PERIODIC_EVENT
?":
bhvr
отдельным
флагом.
Тогда, вследствие потенциальной потребности во множественных суффикс-префиксах, придётся чуть изменить алгоритм парсинга.
И парсинг менять не придётся.
Ведь по сути это и есть полный аналог: периодические -- аналог цикла сервера, а CHANGE -- это и есть update.
Вот по последнему варианту и делаем:
BHVR_ON_UPDATE
, ...
С проверкой -- туго и слабопонятно. По косвенным признакам -- похоже, работает:
tango::ADC4x250/test/srpa/thermoSensor
действительно стали приходить обновления -- правда, 1 раз в секунду, а не 5.
tango::ADC4x250/test/srpa/thermoSensor@u
--
Event properties (abs_change or rel_change) for attribute thermosensor0 are not set
Зато наконец-то разобрался с записью -- да, по крайней мере для СКАЛЯРОВ она РАБОТАЕТ! Но путь был непрост.
ADC4x250/test/srpa/clockSource
-- судя по jive, он получает
значения в диапазоне 0-2.
Фиг, не реагирует.
da
делать da->data_format=Tango::SCALAR
:
cda_d_tango_snd_data()
с выдачей get_data_format()
показала, что оно стало =0 (SCALAR)
вместо былого =3 (UNKNOWN).
И запись значения при указании префикса типа "@+i" также сработала!
da->data_format=Tango::SCALAR
-- а запись всё равно
продолжила работать!
...откуда вопрос: как эта хренотень вообще функционирует? Как оно при НЕуказанном формате понимает, какого рода значение ей приходит? Причём понимать это надо ещё на стороне КЛИЕНТА, для правильной отсылки.
Возможно, этот data_format
-- чисто информационный для
клиента, а реальная информация о формате хранится где-то внутри.
ВЫВОД: надо в
Cdr_treeproc.c добавить поддержку парсинга "@+", чтоб он
добавлял CXDTYPE_USGN_MASK
к типу.
Часом позже: вроде сделано.
23.07.2024: проверяем работу режима
"@u"/BHVR_ON_UPDATE
, приводящего к
Tango::CHANGE_EVENT
-- да, работает.
CDA_DATAREF_OPT_ON_UPDATE
?
bhvr
перед парсингом суффиксов в него помещается не 0
, а в
зависимости от наличия того флажка.
Возможности указать на уровне имён "НЕ по UPDATE" не делаем, полностью аналогично cda_d_cx.c оставляя это на откуп cdaclient'овому префиксу "@~".
25.07.2024: проверена пригодность нынешнего cda_d_tango.c для epics2cda -- да, пригоден!
Мини-протокол:
~/compile/
и собран простой командой "make
".
make -C ~/work/frgn4cx/gw EPICS_BASE_DIR=${HOME}/compile/base-3.15.9 TANGO_INSTALLED=YES
LD_LIBRARY_PATH=/home/skif/compile/base-3.15.9/lib/linux-x86_64 \ CDA_D_TANGO_DEBUG=2 CX_CDA_PLUGINS_DIR=${HOME}/work/frgn4cx/tango/cda ~/work/frgn4cx/gw/epics2smth/epics2cda
(в конце у векторного значения 50 штук нулей, т.к. указано "skif@pult-l:~$ cainfo @s100:tango::group-llrf/vmeadcx32/1/Adc4Ch03YValue @s100:tango::group-llrf/vmeadcx32/1/Adc4Ch03YValue State: connected Host: 10.0.0.61:5064 Access: read, write Native data type: DBF_FLOAT Request type: DBR_FLOAT Element count: 100 skif@pult-l:~$ caget @s100:tango::group-llrf/vmeadcx32/1/Adc4Ch03YValue @s100:tango::group-llrf/vmeadcx32/1/Adc4Ch03YValue 100 -3.48132 -3.47741 -3.48132 ... 0 0 0
@s100
", но измерений от VMEADCx32 приходит 50 штук.
1x.0x.2024:
vector<>
": ещё не дочитал всё написанное в ноябре-декабре
2023, но просто по возвращению к этому вопросу напросилась идея
new
, этот
vector<СООТВЕТСТВУЮЩЕГО_ТИПА>
нужного количества
элементов, а там уж наверняка есть варианты конструкторов, которым можно
сбагрить (ТИП*,КОЛИЧЕСТВО)
-- для совместимости с
"классическими C-массивами".
cda_d_tango_snd_data()
как
=NULL
, чтобы...
cda_d_tango_snd_data()
делать
delete
при !=NULL.
Хотя тут уже надо будет внимательнее смотреть на поведение конструкторов
этого чёртового DeviceAttribute
-- что там с ответственностью
за память: делает ли оно себе копию (тогда НАДО освобождать) или просто
забирает себе указатель (тогда НЕ надо).
Да, это опять будет "thrashing the memory pool". Но иного варианта я просто не вижу.
(Мысли на эту тему в голове стали роиться после встречи с Сенченко и Павленко в офисе Павленко 13-06.2024, когда обсуждали "как бы вкорячить на ТНК, где будет CX, готовое решение с шестилабораторной электроникой, которое на TANGO". Пока идея использовать TANGO-Modbus-gateway (альтернатива -- сваять драйверы всей используемой VME-электроники на CX); но Сенченко сказал "а чё б напрямую к TANGO из CX не обращаться -- у тебя же есть поддержка?" на что я ответствовал, что поддержка ограничена скалярами, а тут нужны будут векторы, но с ними всё непросто и их модуль не умеет. Вот с тех пор и бродили в голове мысли "а что всё-таки надо для поддержки векторов?"...)
22.06.2024: роем детали.
-- это некие методы векторов.int myArray[] = { 5, 10, 15 }; vector<int> myVector(begin(myArray), end(myArray));
(Как они могут работать, учитывая, что оный myArray
--
сторонняя для вектора сущность и методы НЕ ЕГО, мне неясно; разве что
какие-нибудь хитрые макросы или нечто подобное?)
и конкретный пример --data_type array_name[n] = {1,2,3}; vector<data_type> vector_name(arr, arr + n)
(точки-с-запятыми явно забыты); тут используются некие "итераторы", по которым предлагается отдельная страница "Iterators in C++: An Ultimate Guide to Iterators".int a1[5] = {10, 20, 30, 40, 50} vector<int> v1(a1, a1 + 5)
Гугление по "std::vector to array" дало ещё более простые и понятные рецепты:
std::vector<double> v; double* a = &v[0];
Тут используется тот факт, что внутри именно ПОСЛЕДОВАТЕЛЬНЫЙ массив, что гарантируется спецификацией: «There's a fairly simple trick to do so, since the spec now guarantees vectors store their elements contiguously».
For C++11, vector.data() will do the trick.
double arr[100]; std::copy(v.begin(), v.end(), arr);
25.06.2024: а ещё ведь надо будет уметь узнавать
количество элементов в векторе -- это нужно при ПОЛУЧЕНИИ данных, т.е., в
store_current_value()
.
size()
,
как раз возвращающий это количество.
26.06.2024: ну да, так и есть: "std::vector<T,Allocator>::size" на cppreference:
Returns the number of elements in the container, i.e. std::distance(begin(), end())
.
26.06.2024: ага, а ещё надо уметь понимать "формат" -- вектор ли это или скаляр, т.к. разные способы вытаскивания данных.
get_data_format()
, возвращает
значение поля data_format
, могущее принимать одно из значений,
определяемых в tango-9.2.5a/lib/cpp/server/idl/tango.h:
enum AttrDataFormat { SCALAR, SPECTRUM, IMAGE, FMT_UNKNOWN /*, __max_AttrDataFormat=0xffffffff */ };
Tango::FMT_UNKNOWN
-- и в скалярных, и в векторных...
get_data_format()
-- у атрибутов,
полученных от сервера и от намисозданных, в
store_current_value()
и cda_d_tango_snd_data()
соответственно. Результат:
-- т.е., SCALAR и FMT_UNKNOWN соответственно.store_current_value get_data_format()=0 cda_d_tango_snd_data da->get_data_format()=3
Т.е., СЕРВЕР-то присылает вроде осмысленное (но надо ещё на реальном векторе проверить), а вот конструкторы действительно нихрена не делают...
А то неясно, как вообще делается передача значений далее в сторону
сервера -- раз в поле data_format
бессмысленное значение
FMT_UNKNOWN
, то КАК оно определяет тип данных?
28.06.2024: приблизительный сценарий дальнейших действий для поддержки векторов:
28.06.2024: приступаем потихоньку. Начинаем с ПРИЁМА
в store_current_value()
.
data_format = attr_value->get_data_format();
-- причём окружённое try/catch'ем, т.к. там может генериться exception из-за
хрени под названием "unknown_format_flag
"; правда, взведения
его я не нашёл, но мало ли (вдруг я чего недопонимаю, или в будущем
появится).
SCALAR
; альтернатива
SPECTRUM
пока пуста и туда будем делать векторность, а что
делать с IMAGE
, которое 2D, пока неясно: может, разворачивать
матрицу в вектор?
29.06.2024: продолжаем
>>
, что и со скалярами, только параметром
"куда складывать" передаётся vector<T>& datum
-- т.е., так же по ссылке.
bool DeviceAttribute::operator >> (vector<short>& datum)
выглядит собственно копирование:
if (ShortSeq.operator->() != NULL) { if (ShortSeq->length() != 0) { datum.resize(ShortSeq->length()); for (unsigned int i=0; i<ShortSeq->length(); i++) { datum[i] = ShortSeq[i]; } } else ret = false; }
Т.е.,
get_w_dimension()
есть; 01.07.2024:
только get_r_dimension()
-- не "_w_", а "_r_"!),
так что надо смотреть методом size()
у
vector<>
'а, но оный вектор получить при длине 0 не
получится.
Какие варианты?
>>
, и если false -- то считать, что длина 0?
dim_x
?
get_w_dimension()
;
01.07.2024: только get_r_dimension()
-- не
"_w_", а "_r_"! Хрен бы их понял, этих тангоделов, зачем эти ДВА комплекта
(обычный dim_x/dim_y и w_dim_x/w_dim_y) и когда что из них меняется.
operator<<
) скаляров
делают
dim_x = 1
,
а записыватели векторов --
dim_x = datum.size();
Так что спокойно можно пользоваться get_w_dimension()
самого
атрибута вместо size()
вектора.
@вечер: похоже, надо забить на
data_format
и на .size()
, а пользоваться только
значением dim_x
: если ==1, то возвращать скаляр, а иначе --
вектор, причём при ==0 собственно ВЫЧИТЫВАТЬ ничего не нужно.
vector<T>datum
данные лежит
уже на НАС -- т.к. выдаётся копия, то уже нам решать, когда её освобождать.
30.06.2024: продолжаем.
current_val
, и тогда есть шанс, что с другим
типом данным мы этот буфер переполним".
Всё просто: добавляем в hwrinfo_t
поле
current_val_allocd
, в которое в случае аллокирования буфера
кладётся его объём. Таким образом при складировании в буфер можно будет
просто проверять, не вылезет ли объём за пределы и если "да", то сокращать
количество элементов до влезающего.
...для чего в store_current_value()
при определении адреса
буфера value_p
добавлена value_bufsize
,
принимающая значение либо оного current_val_allocd
, либо
sizeof(hi->valbuf)
.
store_current_value()
наполнение варианта
SPECTRUM
:
data_format
, а НЕ перейдено на "если ==1, то возвращать
скаляр, а иначе -- вектор".
-- по штуке на каждый тип. Ибо можно ли сделать ОДИН указатель "на вектор вообще", инициализируя его с помощьюvector< int16> v_int16; vector<uint16> v_uint16; . . .
new
нужного типа (чтоб в
конце один-единственный delete
) -- мне неясно.
Предполагается, что деструкторы им вызовет при выходе из функции сам компилятор.
switch (data_type)
с альтернативами вида
case Tango::DEV_DOUBLE: if (dims.dim_x != 0) {*(attr_value) >> v_float64; data_ptr = v_float64.data();} dtype = CXDTYPE_DOUBLE; break;
nelems=dims.dim_x
с последующим
ограничением для непревышения value_bufsize
и с копированием
при nelems > 0
.
STORE_METADATA
" -- чтоб не страдать дублированием.
На этом с приёмом вроде бы всё.
01.07.2024: допиливаем -- делаем ОТПРАВКУ.
-- опять же по штуке на каждый тип.vector< int16> *v_int16_p = NULL; vector< uint16> *v_uint16_p = NULL; . . .
switch(dtype)
, аналогичный скаляровому.
case CXDTYPE_DOUBLE: v_float64_p = new vector<float64> ( ((float64*)value), ((float64*)value) + nelems); da = new Tango::DeviceAttribute(hi->chn_name, *v_float64_p); delete v_float64_p; v_float64_p = NULL; break;
Т.е., delete
и =NULL
делается сразу после
создания экземпляра DeviceAttribute
, т.к. тот забирает данные
себе в свою копию.
catch()
делается переход в конец в
"секцию" (после основного return
) под меткой
CLEANUP_VECTORS_EIO
: там всем указателям, которые !=NULL,
делается delete
, после чего errno=EIO
и
return CDA_PROCESS_ERR
.
cda_d_tango_new_chan()
и cda_d_tango_snd_data()
.
Проверяем ПОЛУЧЕНИЕ -- оно РАБОТАЕТ!!!
@d200:tango::gun-rf/rfchannelepgate/1/osc_incidentWave
,
найденном с помощью jive. Он оказался в числе "polled".
Числовые данные были получены, толпа чиселок (200шт) в районе -0.012.
gun-rf/rfchannelepgate/1/osc_refPhase
-- т.к. у него нашлись
значения модулем более 1, что позволило поэкспериментировать с
"преобразованием типов".
В эксперименте канал указывался одному cdaclient'у 2 раза с разными
типами как
@d300:tango::gun-rf/rfchannelepgate/1/osc_refPhase
@i300:tango::gun-rf/rfchannelepgate/1/osc_refPhase
-- результаты прекрасные:
hi->dtype
), прекрасно
работает указание "другого" типа: при указании "@iNNN:
"
производилось округление до целого -- напимер,
-0.469,-0.391,-0.313,-0.586,-0.860,-0.899,-0.938,-1.055,-1.172,-1.563,-1.954,-1.915,-1.875,-1.680,-1.485,-1.211,-0.938,-0.703,-0.469,-0.195,0.078
превращалось в
0,0,0,-1,-1,-1,-1,-1,-1,-2,-2,-2,-2,-2,-1,-1,-1,-1,0,0,0
(ну да, в cda_dat_p_update_dataset()
ведь делается
round()
при сохранении в целые).
@d300
возвращались все 200 чисел, при
@i300
-- 150, а при @h300
-- 75. Т.е., сколько
байт аллокировалось (d300:300*8, i300:300*4, h300:300*2), в столько и
урезался результат.
14.07.2024: к вопросу о возврате векторных вариантов
всяких странных типов, вроде bool
и DevState
:
И оформить это в отдельную функцию "convert_INT_array()", которой передавать (src, src_usize, dst, dst_usize).
И тогда при src_usize==dst_usize она может просто делать
memcpy()
.
...но тут неудобство в том, что в store_current_value()
складирование делается в одной точке, получающей лишь dtype
и
НЕ предполагающей какой-то конверсии. Чё, делать конверсию принудительно,
введя обязательный src_usize (который у обычных типов совпадёт с
sizeof_cxdtype(dtype)
и тем самым "конверсия" сведётся к тому
же memcpy()
...)?
cxdtype_t
ведь таково, что ему его
можно сконструировать как
ENCODE_DTYPE(sizeof(SOMETHING), CXDTYPE_REPR_INT, 1)
-- ну так и указывать таким способом именно этот РЕАЛЬНЫЙ ИСХОДНЫЙ тип, а уж
cda пусть сама производит надлежащую конверсию в "локальный" тип.
Мысли в эту сторону полезли после того, как в "Список атрибутов и команд
устройства RFCavity" обнаружились атрибуты вроде
servoLimitSwitch1
, имеющие тип "bool[2]
".
...и да, для Chl-скринов всё равно останется вопрос "а как из вектора выцепить 1 элемент-скаляр для отображения".
15.07.2024: делаем, по второму варианту --
естественно, только для атрибутов (но не для команд), в
store_current_value()
:
v_uint8
.
Заодно и запись сделана в cda_d_tango_snd_data()
.
v_DevState
, а
потом делается
dtype = ENCODE_DTYPE(sizeof(Tango::DevState), CXDTYPE_REPR_INT, 1);
data_ptr = v_bool.data();
".
Гугление по "vector bool data":
It won't compile, unless your implementation has a non-standard extension.и
Does not necessarily store its data in a single contiguous chunk of memory.
Ну и я тоже сообразил: скорее всего, "массивы" булевских оптимизации ради хранятся БИТОВЫМИ цепочками, так что "указатель на данные" становится лишён смысла.
The specialization has the same member functions as the unspecialized vector, except data, emplace, and emplace_back, that are not present in this specialization.
ВЫВОД: тут так не удастся, из bool
-вектора изымать данные
только по-элементно ("побитово"), как-то совсем в обход общего
memcpy()
.
Чуть позже: ну так и сделано. Просто полностью отдельный кусок со
скопированным ограничением nelems
, по-элементным копированием в
цикле и с последующим переходом к STORE_METADATA
.
24.07.2024: ага, вот только собственно
"*(attr_value) >> v_bool;
"
было забыто, так что SIGSEGV'илось при попытке вычитать [0]й элемент из
пустого (инициализированного default-constructor'ом) вектора. Исправлено.
И ещё позже: ну и запись тоже сделана. По-тупому -- создаётся объект
vector<bool>(nelems)
, затем в цикле из исходного массива
элементы поштучно вычитываются (в зависимости от конкретного типа) в
a_bool
и засовываются в вектор, а в конце он сбагривается
конструктору DeviceAttribute
'а.
24.07.2024: проверяем запись векторов -- тоже работает!
@s100:tango::wfmode/dac16x16/1/wfVoltage0A
-- в
него радостно записалось всё указанное; причём именно как в "@s", а
в "@d" -- фиг.
Уведомление о выполнении -- да, метод attr_written()
дёргается, но более ничего.
Ну не заказывать же обратное чтение из attr_written()
?
2x.07.2024:
01.07.2024: пока предварительное действие:
csize
-- в
cda_d_tango_new_chan()
переделано по образу и подобию оного в
cda_add_chan()
, где для REPR_TEXT-каналов добавляется ещё одна
ячейка под NUL.
bzero()
буфера, так же как и в cda.
02.07.2024@гуляя в Бердске по Попова и у причала, ~16:30: учитывая "суть" строкового типа в CXv4 и отличие его от Tango (как и от EPICS) -- что это вектор символов, а не скаляр -- реализовывать чтение и запись надо ОТДЕЛЬНО от прочего, для чего проверка на него должна стоять первой, ещё ДО проверки на скалярность/векторность.
@вечер: вот только где при записи/отправке брать терминирующий NUL -- загадка: в DeviceAttribute НЕТ интерфейса для сбагривания не-NUL-terminated строк, т.е. с указанием длины.
Искать таковой интерфейс у string
'а, чтоб
генерить его и потом использовать как источник данных в конструкторе?
И, как показал просмотр в
tango-9.3.4/lib/cpp/client/devapi_attr.cpp конструкторов "из
string" и "из char*" --
DeviceAttribute::DeviceAttribute(, string &datum)
и
DeviceAttribute::DeviceAttribute(, const char *datum)
-- в них копирование переданного значения производится как
StringSeq[0] = string_dup(datum.c_str());
и
StringSeq[0] = string_dup(datum);
соответственно; т.е., реально там внутри используется обычный
char*
, а вовсе не string
(и в соответствующих
operator<<
, кстати, аналогично).
03.07.2024@утро, просыпаясь:
но раз так -- всё равно нужен именно char*
-- то чем возиться с лишним string
(который будет вызывать
аллокирование) проще самостоятельно сделать malloc(nelems+1)
,
скопировать туда данные плюс записать терминирующий NUL, и уже это давать
конструктору.
03.07.2024: реализовываем.
cda_d_tango_snd_data()
-- по
сегодня-утреннему проекту, реализация скорее похожа на работу с векторами:
if (dtype == CXDTYPE_TEXT)
" поставлена в
начало, ещё ДО "if (nelems == 1)
".
Тут были некоторые сомнения -- "а как же поддержка СИМВОЛЬНЫХ скаляров?".
Но если считать, что "символьные скаляры" -- это тип DEV_UCHAR
,
то в него правильнее отображать CXDTYPE_UINT8
(заодно и векторы
оных будут поддерживаться).
string_buf
=NULL, ...
v_*_p
-- в секции CLEANUP_VECTORS_EIO
он подчищается.
=malloc(nelems+1)
, туда копируются данные
и прописывается терминирующий NUL.
free()
и =NULL
-- ровно как с векторами; при
неуспешности из catch()
делается тот же переход к
CLEANUP_VECTORS_EIO
.
store_current_value()
-- тут уже
комбинация подхода получения скаляров (вычитывание) и векторов (объект
string the_string
):
if (data_type == Tango::DEV_STRING)
"
также поставлена в начало.
string the_string
размещён рядом с
v_*
.
*(attr_value) >> the_string
"
узнаётся длина --
"nelems = the_string.length()
"
-- с последующим ограничением её по value_bufsize
(с
"-1
" для NUL), копированием (если осталось >0) и
прописыванием NUL.
CXDTYPE_TEXT
в список разрешённых типов в
cda_d_tango_new_chan()
.
08.07.2024: проверяем чтение, посредством вычитывания
атрибута Status
первого попавшегося устройства -- да, работает!
Читается строка "The device in STANDBY state" (что соответствует
коду 7=STANDBY).
24.07.2024: проверяем также чтение ВЕКТОРОВ-строк, когда должно просто брать первый ([0]й) элемент, не обращая внимания на наличие остальных (и на саму "векторность").
@t100::tango::wfmode/dac16x16/1/rangeD0
-- в
Jive он показывается как содержащий 4 строки "V_10_10"
.
"V_10_10"
.
25.09.2024@сидя на лекции-занятии Олеси Радченко и Алексея Козырева по FPGA/ПЛИС в терминалке, ~10:00: а можно ж всё-таки сделать некоторый вариант поддержки ВЕКТОРНЫХ строк -- считать, что CX'ным аналогом вектора строк является строка, разделённая '\0':
Но конкретно ПОСЛЕДНИЙ -- не нужно учитывать в возвращаемой длине
(nelems
); таким образом получим в "скалярном" варианте ровно
то, что сейчас.
memchr()
А чтобы не добавлялась лишняя пустая строка -- если исходник, например,
'a','b','c','\0' -- надо вначале
проверять, что если в конце есть '\0', то уменьшать
nelems
на 1:
if (nelems > 0 && ((char*)value)[nelems-1] == '\0') nelems--;
@вечер: не удержался и конкретно эту строчку добавил.
Как раз вот-вот в epics2tango_gw.cpp работа с векторами строк будет сделана, так что появится практический опыт плюс пример, вот после этого можно и тут сделать в вышеописанном виде.
0x.09.2024:
CHTYPE_COMMAND
и
CHTYPE_RESULT
.
Тут стоят следующие задачи:
Тут нюанс в работе с типами, т.к.:
05.07.2024: некоторые соображения насчёт "дуплетом связанных hwr'ов":
Так вот: надо ЗАПРЕЩАТЬ подобное прямо в
cda_d_tango_new_chan()
.
Тот вариант был бы лишён проблемы "нельзя несколько ссылок на одну команду" (т.к. эти ссылки регистрировали бы свои "callback'и").
Но всё же он шибко монструозен, так что пока сделаем первоначально задуманный (ещё в 2019-м) вариант с дуплетами.
05.07.2024: изучаем.
DeviceData
-- штука,
аналогичная DeviceAttribute
по методам, но НИКАК с ним не
связанная! У этих 2 классов НЕТ общего предка, а лишь идентичные
интерфейсы: т.е., на уровне препроцессора/макросов унифицировать работу с
ними можно, а вот на уровне C++ -- НЕЛЬЗЯ.
Забавно, да: авторы Portable Channel Access догадались сделать "gdd" -- хоть и криво реализованный, но идеологически/дизайнерски вполне обоснованный; а Tango-делы -- нет, не догадались унифицировать у себя работу с данными.
06.07.2024: а ещё у
DeviceData
НЕТ "наполняющих" конструкторов, которым бы сразу
указывались данные; видимо, надо сначала надо просто создать объект, а потом
закинуть в него данные через оператор <<
.
DeviceProxy
есть метод
(плюс, методы/** * Poll a command * * Add the command "cmd_name" to the list of polled command. The polling period is specified by "polling_period" * (in mS). If the command is already polled, this method will update the polling period according to "polling_period". * * @param [in] cmd_name The command name * @param [in] polling_period The polling period */ virtual void poll_command(string &cmd_name, int polling_period);
stop_poll_command()
для останова и
is_command_polled()
c get_command_poll_period()
для получения информации).
Т.е.,
State
не дёргать).
С каким АРГУМЕНТОМ будет вызываться поллируемая команда -- не сказано.
Что -- таки можно ВКЛЮЧАТЬ ПОЛЛИНГ С КЛИЕНТА простым вызовом
poll_attribute()
(увы, асинхронного варианта не видно)?
command_inout()
, встречающегося в примерах
-- например, в
"Writing your first C++ TANGO client -- Tango Controls 9.3.4 documentation"
-- в DeviceProxy.h что-то не видно...
Но "прямой тест" в виде кусочка кода
di->dev->command_inout("State");
в create_dpx_dev()
сразу после "new
Tango::DeviceProxy(...)
" -- скомпилировался без ошибок... WTF?!
...как и
di->dev->command_inout_asynch("State");
Какого хрена?!?!?!
По прошествии нескольких часов: ф-ф-ф-у-у-у, разобрался --
просто DeviceProxy
отнаследован от
Tango::Connection
, а уж в
tango-9.3.4/lib/cpp/client/Connection.h оных методов
command_inout()
и command_inout_asynch()
плюс
command_inout_reply()
-- толпа.
...и ещё там рядышком тот самый get_asynch_replies()
с
комментарием "Fire callback methods for device asynchronous
requests (command and attributes) with already arrived replies." --
да, там именно про "asynchronous requests", но НЕ про event'ы. ...всё равно
-- какая-то совсем дурная модель работы...
06.07.2024@вчера-вечер засыпая, сегодня утром просыпаясь, утром в душе: размышления по архитектуре реализации:
State
) --
т.е., принимающих void, а возвращающих "что-то": можно регистрировать ТОЛЬКО
RESULT-канал, но чтобы исполнение команды могло вызываться
req_read()
-- т.е., посредством
cda_req_ref_read()
.
0
того типа, которым они были зарегистрированы.
State
из cdaclient:
req_read()
попросту некому, ...
State@r State@c=1
или
State@r State@r=1
А хотелось бы всё-таки иметь возможность указывать просто ОДНО имя, как канала.
06.07.2024: приступаем.
hwrinfo_t
добавляем поле
cmd_other_side
, в котором будет прописываться идентификатор
"парного" hwr'а. Инфраструктурные действия с ним:
=-1
, ...
RlsHwrSlot()
смотрится, что если
>0
, то "там" у пары этому полю делается =-1
(хотя и своему тоже).
cmd_ended()
(прототип
описан в devasyn.h), ...
CmdDoneEvent*
-- этот тип описан в
там же.
push_event()
.
store_current_valueD()
(и параметр ей передаётся
чуть иной).
Но далее по коду используется hwr
, который берётся из
callback'ова self
, и тогда ещё там пришлось бы выкручиваться.
Поэтому принято решение "саму команду исполнять «от имени» парного RESULT-канала" -- так будет меньше махинаций.
DeviceData
обозвана
store_current_valueD()
(НЕ store_devicedata()
--
чисто для унификации).
Предварительные исследования, показывающие, насколько с
DeviceData
всё печально:
DeviceData
если метод
get_type()
есть, то вот get_data_format()
-- фиг.
Зато содержимое get_type()
(в devapi_data.cpp)
совсем иное: без той дичи "если то, а если сё, ...", но зато с прямой
привязкой к CORBA.
И оно помимо обычных Tango::DEV_nnn
также
радостно возвращает и Tango::DEVVAR_nnnARRAY
.
Чуть позже: неа, там в DeviceData.h есть комментарий про "native
TANGO CORBA sequence types", из которого следует, что эти DEVVAR*
-- они как раз и есть те самые "CORBA sequence types".
Чё, пытаться лезть напрямую в поле CORBA::Any_var any
-- раз
уж оно public?
Неа -- лучше правда забить и пока ограничиться поддержкой скаляров.
Возможно, аргументация "а какой timestamp у результата команды" (утрирую, конечно; но, скорее всего, эти олухи просто не подумали или забыли).
Ну тут хоть всё просто: взять текущее время от
gettimeofday()
, да и всё.
Реализация:
store_current_value()
, но в
несколько упрощённом виде -- SPECTRUM
.
store_current_value()
добавлена
"поддержка" DEV_VOID
: в этом случае возвращается значение
0
типа CXDTYPE_INT32
.
DEV_STRING
и
SCALAR
(которая стала просто "else
"), с заменой
"attr_value
" на "data_value
".
gettimeofday()
.
cda_d_tango_req_read()
:
in_use
, ранее имевшееся
засунуто в альтернативу DEVATTR.
read_attribute_asynch()
вызываются
command_inout_asynch()
.
read_call_id
.
...но вызов поллинга пока не сделан.
07.07.2024: вроде как добавлено. С непонятным качеством.
cmd_other_side
, то запрос шлётся «от имени»
той стороны (т.е., RESULT'а).
RlsHwrSlot()
: добавлено "высвобождение TANGO-ресурсов"
-- subscr_event_id
и read_call_id
при !=-1.
create_hwr_subscription()
-- добавлено ничего не делать
при in_use!=CHTYPE_DEVATTR.
07.07.2024: добиваем.
Предварительные соображения по архитектуре:
DeviceData
НЕТУ "наполняющих"
конструкторов, получается, что операция "new ТИП(ЗНАЧЕНИЕ)" из атомарной
превращается в ДВЕ: сначала создать, потом наполнить.
delete
" -- как в
cda_d_tango_snd_data()
-- к "экземпляр объекта прямо тут в
функции, вызывается только оператор закидывания данных, а о вызове
деструктора заботится компилятор" -- как с векторами в
store_current_value()
.
Это избавляет от необходимости не забывать о подчистке
(delete
) при разных сценариях, образующихся от
exception
'ов.
Реализация:
cda_d_tango_snd_data()
"переадресует" запрос нововведённой коллеге --
snd_data_via_command()
-- название имеет ту же длину и
тоже содержит строку "snd_data".
cda_d_tango_snd_data()
с очевидными изменениями:
dpxinfo_t *di
.
Tango::DeviceData dd
вместо
указателя Tango::DeviceAttribute *da
-- в соответствии с
соображениями выше.
dd << ...
" вместо
"da = new Tango::DeviceAttribute(...)
".
delete
не требуется.
cda_d_tango_req_read()
сделано, что если
"этот" hwr -- COMMAND, у которого определена пара в
cmd_other_side
, то запрос шлётся «от имени» той
стороны (т.е., RESULT'а).
cda_d_tango_new_chan()
: во-первых, запрет "всего, кроме
CHATTR" заменён на запрет только DEVPROP и ATTRPROP, а во-вторых, добавлен
поиск "пары" и "дубля" (во втором случае -- облом по EBUSY) посредством
итератора CommandCheckIterator()
.
08.07.2024: начинаем проверять и в процессе доделываем важные мелочи.
Tango::DEV_STATE
--
пока только скалярных (см. профильны раздел ниже за сегодня).
cdaclient @t100:tango::RFGunPhasemeter/test/2/Status@r @i:tango::RFGunPhasemeter/test/2/Status@c=1
-- и ничего.
cda_dat_p_report_rslvstat()
, ни
cda_dat_p_set_ready()
-- который критичен, т.к.
cda_core.c::SendOrStore()
НЕ вызовет отправку пока
ri->is_ready
НЕ взведён.
Ибо для команд нет нормального асинхронного способа убедиться в их наличии и/или получить о них информацию.
di->dev==NULL
).
create_hwr_subscription()
.
Отключить это поведение можно ключом -w
, что и было сделано
-- да, заработало!
И сразу проверено, что только с ключом -w
, но без
FOUND+ready, работать не будет -- да, после закомментировывания не работает.
report_command_found()
, которая и вызывается из тех 2 точек.
Посидев несколько часов, всё же разобрался: дело в устройстве самого
cdaclient, который в итераторе ActivateChannel()
сначала
регистрирует канал БЕЗ evproc'а, а уже потом, при успехе, делает
cda_add_dataref_evproc()
. Вот он и не получал событие,
прилетающее прямо в момент регистрации.
OK, засим считаем, что со стороны dat-плагина обязанности по оповещению о состоянии командных каналов соблюдены, а с cdaclient'ом будем разбираться отдельно.
(Точнее, доделано -- там были косяки со сравнением (не-совпадение имени
считалось при strcasecmp()==0 вместо правильного !=0) и с указанием самого
имени -- в cmdinfo.name
писалось dup_name
вместо
надлежащего sl3+1
, как в сам chn_name
пишется.)
...хотя можно и писать в тот же @r-канал, и читать из @c-канала -- это тоже работает и проверено cdaclient'ом.
По сути, ДВА разных канала требуются, если важен тип АРГУМЕНТА и он отличается от типа РЕЗУЛЬТАТА; иначе же можно регистрировать любой из COMMAND/RESULT.
Итого: работа с командами по первому впечатлению работает как задумано.
10.07.2024: делаем поддержку "state-style"-команд -- чтоб первое исполнение производилось сразу при регистрации, имитируя начальное чтение.
Эта работа стала ключевой причиной для расширения базовой инфраструктуры
-- введения поля bhvr
(подробнее см. выше за сегодня).
BHVR_STATE
.
report_command_found()
сразу
вызывается cda_d_tango_req_read()
, в ОБЕИХ точках -- и в
cda_d_tango_new_chan()
(это подключение сразу успешно), и в
DoSubscribeIterator()
(это при отложенном подключении).
return_current_value()
и полученное значение отдаст, и
автоматом перед этим report_rslvstat+set_ready сделает, ровно как для
атрибутов.
11.07.2024: проверено -- да, @s-каналы работают как задумано.
11.07.2024: после того, как cdaclient.c был под-твикнут для умения отрабатывать события прямо в момент регистрации канала (ещё ДО завершения оной регистрации), проводим ещё тесты.
Вкратце -- всё грустно.
Если указать ключ "-m
" -- тогда да, и реальное значение
показывается (но ценой "зависания навсегда").
.wr_snt=1
, так что так никогда не сделается
num2write--
и оно так навсегда и останется >0, так что
условие завершения никогда не исполнится.
Получается, что эта модель "отображения Tango-команд на CX-каналы" слабосовместима с cdaclient.
Всё, что приходит в голову -- это:
Проблема в том, что такие изменения сделают невозможной работу "через один канал вместо пары": если "подтверждение" при записи ещё можно исполнять условно (при наличии пары), то невозврат "начального значения" условным сделать нельзя, т.к. в момент регистрации RESULT-канала парный ему COMMAND-канал может быть ЕЩЁ НЕ зарегистрирован (ибо будет позже).
...разве что:
report_command_found()
придётся вставить
проверку -- фейковое "начальное значение" возвращать ТОЛЬКО при
CHTYPE_COMMAND.
12.07.2024: модифицируем по вчерашнему проекту.
report_command_found()
делает фейковое обновление ТОЛЬКО
при CHTYPE_COMMAND.
snd_data_via_command()
делает точно такое же обновление
ТОЛЬКО при CHTYPE_COMMAND и при этом с обязательно наличествующей
RESULT-парой (hi->cmd_other_side > 0
).
Попробовал -- авотхрен!
Поэтому проверка "CHTYPE_COMMAND с парой" не срабатывает.
cmd_other_side
не прописано.
Краткий осмотр показал, что оного прописывания "у парного канала" просто нигде нет.
Добавлено в cda_d_tango_new_chan()
сразу после прописывания
своего.
return_current_value()
проверяем,
что если это "RESULT-канал с парой", то этой COMMAND-паре возвращаем
"фейковое обновление", но при этом с rflags и timestamp от RESULT-канала.
Из snd_data_via_command()
же убрано.
Теперь вроде работает. Ура!
23.07.2024: попробовано исполнить команду
Init
-- опять печаль...
2024-07-23-17:04:24.582 1076935 cmd_ended() terminate called after throwing an instance of 'Tango::WrongData' Aborted
-- т.е., тут2024-07-23-17:05:18.682 1078691 cmd_ended() terminate called after throwing an instance of 'Tango::WrongData' Thread 2 "cdaclient" received signal SIGABRT, Aborted. [Switching to Thread 0x7ffff6234700 (LWP 1078691)] __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50 50 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory. (gdb) bt #0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50 #1 0x00007ffff7cac537 in __GI_abort () at abort.c:79 #2 0x00007ffff72bd7ec in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6 #3 0x00007ffff72c8966 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6 #4 0x00007ffff72c89d1 in std::terminate() () from /lib/x86_64-linux-gnu/libstdc++.so.6 #5 0x00007ffff72c8c65 in __cxa_throw () from /lib/x86_64-linux-gnu/libstdc++.so.6 #6 0x00007ffff777268e in Tango::ApiDataExcept::throw_exception(char const*, char const*, char const*, Tango::ErrSeverity) () from /lib/x86_64-linux-gnu/libtango.so.9 #7 0x00007ffff7805f6f in Tango::DeviceData::any_is_null() () from /lib/x86_64-linux-gnu/libtango.so.9 #8 0x00007ffff7805f96 in Tango::DeviceData::get_type() () from /lib/x86_64-linux-gnu/libtango.so.9 #9 0x00007ffff7fc2666 in store_current_valueD (data_value=0x7ffff6233970, hi=0x555555611318) at cda_d_tango.cpp:641 #10 cda_d_tango_EventCallBack::cmd_ended (this=0x555555601be0, cde=0x7ffff0000d00) at cda_d_tango.cpp:2323 #11 0x00007ffff785339a in Tango::Connection::Cb_Cmd_Request(CORBA::Request*, Tango::CallBack*) () from /lib/x86_64-linux-gnu/libtango.so.9 #12 0x00007ffff7837941 in Tango::ApiUtil::get_asynch_replies(long) () from /lib/x86_64-linux-gnu/libtango.so.9 #13 0x00007ffff7840dc2 in Tango::CallBackThread::run_undetached(void*) () from /lib/x86_64-linux-gnu/libtango.so.9 #14 0x00007ffff7fb7260 in omni_thread_wrapper () from /lib/x86_64-linux-gnu/libomnithread.so.4 #15 0x00007ffff65d4ea7 in start_thread (arg=>optimized out>) at pthread_create.c:477 #16 0x00007ffff7d85a2f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
get_type()
может вывалиться!!!
try{}catch(){}
'ем.
-- т.е., эти долбоклюи умудрились сделать, что команда2024-07-23-19:08:33.169 1321815 cmd_ended() 2024-07-23-19:08:33 store_current_valueD(): data_value->get_type(): exception, details follow: -------------------- Tango exception Severity = ERROR Error reason = API_EmptyDeviceData Desc : Cannot extract, no data in DeviceData object Origin : DeviceData::any_is_null --------------------
Init
возвращает не пойми что, даже не DEV_VOID
.
24.07.2024: судя по тому, что Jive
показывает в панели View|"Test Device" команду Init
как "Argin
Type: DevVoid, Argout Type: DevVoid", похоже, что это именно
"DevVoid
" они так и возвращают -- НИЧЕМ, которое и определить
никак нельзя (ибо exception), а только заранее-зная...
24.07.2024: кстати, проверил на ещё одной команде, которая
Jive'ом показывается как "Argout Type: DevVoid" --
wfmode/dac16x16/1/SendProgLDAC@s
-- ну да, опять exception. Ну
тупы-ы-ые...
25.07.2024: погуглил по «tango "Cannot extract, no data in DeviceData object"» -- ни-фи-га, НИКТО этот вопрос не задавал. А всё, что нашлось -- это страница с описанием самих exception'ов "Exception API — PyTango 9.4.1 documentation", где есть фраза "This error occurs when trying to extract data from an empty DeviceData object".
store_current_value()
вызов
attr_value->get_type()
также засунут в
try{}catch(){}
.
26.07.2024: наткнулся сегодня на то, что (судя по
показаниям Jive/AtkPanel, подтверждаемым выдачей
tango_cmd_info
) у девайса dac16x16 есть команды
SendWfStart
и SendWfStop
, на вход принимающие
DevVarULongArray
.
DeviceData
с векторами, заглянул я в
tango-9.3.4/lib/cpp/client/DeviceData.h для освежения в памяти
темы "а что там всё-таки с API, что есть, а чего нету?" и обнаружил (освежил
:D), что...
...а нету способа определить ФОРМАТ СОДЕРЖИМОГО -- ни
get_data_format()
, ни get_r_dimension()
.
DeviceData
нельзя, но ОТПРАВИТЬ-то их туда можно -- давай
сделаем!
snd_data_via_command()
и обнаружил, что это
уже давно сделано.
Судя по записям, сделано СРАЗУ, ещё 07-07-2024, вместе со скалярами, т.к.
snd_data_via_command()
копировалась с
cda_d_tango_snd_data()
, к тому времени уже имевшей векторную
часть.
Ну коли так, то хоть сделать более полную поддержку отправляемых типов --
добавить векторные UINT8/UCHAR и bool
.
DeviceData
'й как раз только векторный
и поддерживается.
Тут копирование из аналога для атрибутов простое и прямолинейное.
bool
/"@b": он векторный в
cda_d_tango_snd_data()
был добавлен уже позже, 15-07-2024.
Вот здесь оказалось чуть сложнее.
CXDTYPE_INT8
или BHVR_BOOL
" перед
основным switch (dtype)
была скопирована опять же просто и
прямолинейно.
cda_d_tango.cpp:1807:20: error: no match for 'operator<<' (operand types are 'Tango::DeviceData' and 'std::vector<bool>') dd << *v_bool_p; ^
vector<bool>
" запихивать в
// The VERSION formula is taken from tango-9.2.5a/lib/cpp/server/utils.cpp::_convert_tango_lib_release() #if ((TANGO_VERSION_MAJOR * 100) + (TANGO_VERSION_MINOR * 10) + TANGO_VERSION_PATCH) >= 934
Теперь компилируется с обоими вариантами.
Конечно, проверить пока что просто не на чем.
Кстати,
<<
и
>>
возникло впечатление, что эти
DevVar*Array
/DEVVAR_nnnARRAY
-- это и есть те
типы, через которые передаются массивы:
operator<<(vector<>)
именно такое генерят
при запихивании данных.
operator<<(vector<>&)
при извлечении
именно из таких данные и достают.
Соответственно,
РЕЗЮМЕ: учитывая, что практическая потребность в векторных результатах команд практически отсутствует, проще на данный аспект забить. Ну нет, ну и ладно.
cda_dat_p_set_hwr()
добавлено
if (chtype == CHTYPE_COMMAND || chtype == CHTYPE_RESULT) cda_dat_p_set_fresh_age(ref, (cx_time_t){0, 0});
Записать в раздел по PVA/PVACCESS ссылки от Kay Kasemir из увиденной сегодня "вчерашней" переписки в tech-talk.
2x.07.2024:
CHTYPE_DEVPROP
.
Тут стоят следующие задачи:
04.07.2024: изучаем. Грустно всё.
get_property()
для чтения и
put_property()
для записи (плюс их вариации для списка
множественных пропертей), get_property_list()
для получения
списка пропертей (первым параметром
const string &filter
, в котором можно указывать
'*' в количестве не более 1шт), а также методы для удаления (!)
delete_property()
.
DbData
-- чтение
делается в DbData &db
, запись тоже из него (причём при этом
даже параметра "имя" нет -- ВСЯ информация из DbData), и векторные операции
также получают параметром DbData &db
.
Определён он в tango-9.3.4/lib/cpp/client/devapi.h (и одновременно в tango-9.3.4/lib/cpp/client/dbapi.h -- блин!!!) как
typedef vector<DbDatum> DbData;
А класс DbDatum
определён в том же dbapi.h и
<<
и
>>
он как будто унифицирован с DeviceAttribute
.
int value_type
и
int value_size
,
собственно значение в виде
vector<string> value_string
-- т.е., ВСЁ В СТРОКАХ.
И преобразование "из строки" все operator>>
делают
через istream, а "в строку" operator<<
делают через
ostream.
Соответственно, при передаче вещественных значений будет потеря точности.
(Вообще какой-то детско-прямолинейный подход: школьники так программируют, не задумываясь о последствиях.)
Чё-то как-то нет желания ЭТО поддерживать.
0x.07.2024:
CHTYPE_ATTRPROP
.
Тут стоят следующие задачи:
CHTYPE_DEVPROP
).
04.07.2024: смотрим. Всё печально идентично DEVPROP'ам.
DbData
,
только get_property_list()
отсутствует.
Желания делать поддержку ЭТОГО столь же мало, как и для DEVPROP'ов. Сделать можно в ровно тех же границах -- только однократное чтение и запись, с принудительным однократным чтением при регистрации. Но отсутствие асинхронности и тут ставит возможность реализации под ба-а-альшой вопрос.
0x.07.2024:
Tango::DevState
-- код
Tango::DEV_STATE
, иначе просто посмотреть результат не удастся.
08.07.2024@утро-завтрак: разбираемся.
Tango::DevState
определён в
tango-9.3.4/lib/cpp/server/idl/tango.h (первоисточник --
tango-9.3.4/lib/idl/tango.idl) ...
enum
;
int
, но это требование зарыто где-то в стандарте и
насколько оное гарантируется -- не знаю).
/*, __max_DevState=0xffffffff */
0
там -- ON
! Не UNKNOWN
(то 13
) и даже не OFF (то вообще 1
).
Ну прямо ума палата -- так "удачно" выбрать значения...
Делаем:
State
, который в векторном виде особого смысла не имеет, ...
ENCODE_DTYPE()
. Нафига
только...
store_current_value*()
вводим локальную
переменную a_DevState
, ...
>>
, после чего
*(( int32*)value_p) = a_DevState;
Проверяем, пытаясь вычитать атрибут State
первого
попавшегося устройства -- да, работает! Читается число 7 (это код
STANDBY
).
На этом ставим "done".
DEV_VOID
, DEV_UCHAR
с
DEV_BOOLEAN
и загадочным DEV_INT
: как минимум
BOOLEAN не помешает поддерживать, так что разбираемся, что же это такое.
09.07.2024: итак:
DevVoid
" встречается в комментариях и прочей
документации, под видом как бы типа, но...
DEV_VOID
в качестве селектора в
switch()
не используется нигде, кроме
tango-9.3.4/lib/cpp/server/pollring.tpp -- там для какого-то хака
касательно состояний: «we use the "type" data set to DEV_VOID when
we are dealing with the state as an attribute».
<<
или >>
для void
в
каком-либо варианте.
typedef ::CORBA::Octet DevUChar;
Причём:
DeviceAttribute
(и у DbDatum
) ЕСТЬ операторы
<<
и >>
с параметром unsigned
char
и unsigned char &
соответственно, ...
DeviceData
-- НЕТ (есть только для
vector<unsigned char>
-- хбз зачем).
DevUChar
--
typedef ::CORBA::Boolean DevBoolean;
И операторы <<
и >>
для типа
bool
есть у всей троицы классов-для-данных.
grep -rw DevInt compile/tango-9.3.4
" находит лишь пару
упоминаний в документации, ...
...хотя и реального DevInt
не видно, а строка "DevUInt"
вообще нигде не встречается.
DevInt - это какой-то рудимент который нереализован был. В 9.5 он удален. Можешь его игнорировать.
Резюме:
DEV_VOID
с 06-07-2024 уже поддерживается на чтение,
возвращая значение 0
типа CXDTYPE_INT32
; а вот как
бы на "ЗАПИСЬ" поддерживать?
DEV_UCHAR
для атрибутов (но НЕ для команд!),
сопоставляя его с CXDTYPE_UINT8
.
DEV_BOOLEAN
и у атрибутов,
и у команд,
(Запись фиг сделаешь: INT32-то маппируется на DEV_LONG
.)
DEV_INT
-- забыть о его существовании.
10.07.2024: делаем, совместно с дополнениями в базовую
инфраструктуру -- полем bhvr
и опциональными префиксами
суффиксов "@v" и "@b".
snd_data_via_command()
при взведённом
BHVR_VOID
принудительно исполняет варианты
command_inout_asynch()
БЕЗ параметров, полностью игнорируя
переданное в канал на запись.
DEV_UCHAR
соответствует
CXDTYPE_UINT8
, в обоих направлениях. Но только для атрибутов.
cda_d_new_chan()
даже проверка есть, что при
регистрации COMMAND- и RESULT-каналов с типом UINT8 выдаётся WARNING (но
запрета нет -- просто попытка исполнения команды приведёт к ошибке).
ЗАМЕЧАНИЕ: всё только для скаляров. 15.07.2024: и чтение атрибутов-векторов тоже добавлено, см. выше за сегодня в общем разделе по векторам. А заодно уж и запись -- тривиально же.
bool
размера, а
при явном присваивании компилятор сам обеспечивает надлежащее преобразование
между этим типом и целыми. Поэтому:
store_current_data*()
добавлена
переменная bool a_bool
, при DEV_BOOLEAN
значение
вытаскивается в неё, после чего делается
*(( int32*)value_p) = a_bool; dtype = CXDTYPE_INT32;
*snd_data*()
также заведена
bool a_bool
и:
else
которой теперь переехал основной switch()
по типу -- что если
взведён BHVR_BOOL
и тип является целочисленным, то...
a_bool
присваивается uintN
-значение
соответствующей битности (N=8/16/32/64).
CXDTYPE_INT8
генерится объект типа bool
.
...ну да, получается асимметрично: INT8->bool, но bool->INT32; ну да и ладно -- cda обеспечит соответствующее преобразование при надобности.
ЗАМЕЧАНИЕ: всё только для скаляров. 15.07.2024: и чтение атрибутов-векторов тоже добавлено, см. выше за сегодня в общем разделе по векторам. А потом и запись.
10.07.2024: ну изучаем -- было бы что:
AttrQuality
-- это определённый в
compile/tango-9.3.4/lib/cpp/server/idl/tango.h (с исходником
традиционно в tango.idl)
enum AttrQuality { ATTR_VALID, ATTR_INVALID, ATTR_ALARM, ATTR_CHANGING, ATTR_WARNING /*, __max_AttrQuality=0xffffffff */ };
DeviceAttribute
есть странной сигнатуры (почему
&
бы?) метод
AttrQuality &get_quality() {return quality;}
DeviceData
-- НЕТУ!
Напрашивается простая трансляция в rflags: VALID:0, INVALID:CXRF_INVAL, ALARM:CXCF_FLAG_COLOR_RED|CXCF_FLAG_ALARM_ALARM, CHANGING:CXCF_FLAG_COLOR_WEIRD WARNING:CXCF_FLAG_COLOR_YELLOW.
@вечер-засыпая: а ещё унифицировать бы эти коды rflags с cxsd_fe_epics/epics2cda -- чтоб в результате получались бы осмысленные alarm level'ы.
11.07.2024: посмотрел -- а унифицировать-то, похоже, не с
чем: в них сейчас НЕТ трансляции rflags в alarm level'ы, есть только в
обратную сторону в cda_d_epics.c. Добавлять
уставку методом gdd.setStat()
? А выбирать значение
"status/severity" примерно по алгоритму choose_knobstate()
; см.
также
"EPICS Process Database Concepts", раздел "Alarm Specification?"
-- там описаны концепции и пояснены разные состояния (и проблема "только
одно состояние в текущий момент" упомянута).
@вечер-душ-ванна:
По-хорошему -- видимо, надо уставлять ПАРАМИ: и status, и
severity (т.к. обычно это определяется конфигурацией рЕкорда (поле
"Maximize Severity"?), но в pCAS-программах никаких рЕкордов нет и они
должны это делать сами).
Кстати, надо бы в epics2smth.cpp попробовать переделать
e2s_PV::read()
с возврата текущего известного на вызов
req_read()
(появившийся уже ПОСЛЕ её реализации -- 04-07-2023,
а на epics2smth.cpp сейчас дата 26-05-2023); вероятно, для этого
понадобится использование "асинхронных классов"; хотя ещё может быть
проблема с использованием installConstBuf()
для строк в
data2gdd()
.
@вечер-душ-ванна: так-то можно завести в moninfo_t
лишнее
поле-указатель на "асинхронный объект-ответ на чтение", которое заполнять в
e2s_PV::read()
, а в e2s_PV::update()
проверять,
что если это поле !=NULL, то дополнительно "сообщать об успешном завершении
асинхронной операции"; проблема лишь в том, что этих запросов на
чтение может придти и больше одного -- а единственная точка хранения будет
уже занята...
12.07.2024@утро-душ: или чо
-- заводить какой-нибудь
c++-std::vector<>
-объект (тоже указателем), чтоб каждый
новый асинхронный запрос в него? Можно, но неприятность в том, что
придётся очень аккуратно следить, чтоб оно не росло бесконечно в ситуации,
когда чтения почему-либо не отрабатываются; а есть ли в libCAS уведомления
"клиент отцепился", чтоб подчищать запросы таких клиентов?
11.07.2024: делаем.
quality2rflags()
, возвращающая код по таблице
quality2rflags_table[]
-- всё по образу и подобию
epics2cx_conv.h::alarm2rflags()
.
store_current_value()
вместо всегда-0
теперь делается
hi->current_rflags = quality2rflags(attr_value->get_quality());
(А в store_current_valueD()
пришлось оставить тот
0
-- там у DeviceData
никакого quality нету.)
11.10.2024: спросил у Сенченко "что за значение ATTR_CHANGING у AttrQuality?". Ответ (Message-ID:f86fdedd9f06aa005009d96d584b9aa9@inp.nsk.su):
ATTR_CHANGING - означает, что атрибут меняется.
Допустим у тебя есть источник, а у него атрибут измерение тока. Ты ему отдаешь команду на рамп, он переходит в состояние MOVING или RUNNING, а атрибут возвращает текущее значени с качеством ATTR_CHANGING.
Его не часто используют, только в SARDANA видел использование.
Вот так несложно, и по сути это даже не "качество", а просто некое
временное состояние. В CX этому можно считать аналогом
CXCF_FLAG_PRGLYCHG
, но ныне используемый вариант WEIRD даже
лучше.
DeviceProxy
,
get_attribute_config()
или
get_command_config()
,
cout
оператором
"<<
".
Собственно "мяса" там буквально 3 строчки (плюс проверка
argc
на указанность параметров), но over90% -- это
try{}catch(){}
'и.
Отдельный прикол -- оператор выдачи _CommandInfo
слегка
туповат и забывает в конце сделать перевод строки; поэтому пришлось самому
делать "cout << endl;
".
DevEncoded
/DEV_ENCODED
-- раздел скорее для
порядку, для записи чисто теоретических изысканий.
28.07.2024@душ-в-районе-обеда: учитывая, что "DevEncoded" -- это дуплет {ключ-строка,данные-байтовый_массив},
uint8
.
Функционирование:
Одна проблема: с командами, принимающими или возвращающими
DevEncoded
, это не прокатит: там и так уже разбито на 2 канала.
Такой вариант синтаксически можно и для команд приспособить.
Минус -- что "ключ" будет исключительно фиксированным.
И отдельный вопрос -- сохранять ли при этом вёрнутое значение "ключа"? И
если "да", то КАК -- chan_ioctl()
'ом (но он НЕ публичен и
отсутствует в cda.h)? Или какой-нибудь из стандартных строк?
Не удивлюсь, если эта идея в голову уже приходила.
29.07.2024@утро: ну да, 27-07-2019 -- "а ведь раз это дуплет {строка,цепочка_байтов}, то можно сие считать за просто цепочку байтов -- сначала NUL-terminated string, а за ней собственно байты".
В обоих случаях менеджмент "пар" будет аналогичен таковому у COMMAND/RESULT-пар.
Но главный вопрос -- ПРИМЕНИМОСТЬ: НАФИГА оно может понадобиться? Ведь пользоваться таким неопределённым типом никакой "скрин-менеджер" всё равно не сможет, а только специальные программы.
@вечер, засыпая: и да,
поскольку стандартными CX'ными типами указать невозможно, то потребуется
@-префикс-суффикс -- например, @e -- который бы взводил
bhvr
-флаг, вроде BHVR_DEVENCODED
; естественно, при
этом регистрируемый тип должен быть 1-байтным (т.е., TEXT, INT8 или UINT8).
@утро, мытьё посуды после
завтрака: кстати, имеющаяся инфраструктура CX легко позволит такие
каналы ЗАПИСЫВАТЬ из командной строки -- с помощью утилитки
pipe2cda
, в режиме '-B
'; например,
(echo -en 'JPG\0'; cat file.jpg) | pipe2cda -B tango:ИМЯ_КАНАЛА@e
28.07.2024: в tango-9.3.4/lib/cpp/server/idl/tango.h содержится следующее определение (оставлены только поля данных):
struct DevEncoded { . . . ::CORBA::String_member encoded_format; DevVarCharArray encoded_data; . . . };
...используемый DevVarCharArray
определён в том же файле,
весьма замутным способом, начинается определение с
class DevVarCharArray : public _CORBA_Unbounded_Sequence_Octet {
-- так что да, в конечном итоге это именно массив uint8
, но с
какой-то обвязкой (как от CORBA, так и от Tango).
29.07.2024: резюме:
Так что сейчас замораживаем раздел до появления реальной потребности (чего, вероятно, никогда и не произойдёт).
05.09.2024: а вот в epics2tango_gw.c вполне можно сделать, по какому-нибудь из 3 проектов.
Тогда можно позапихивать всю поддержку 64-битных целых в
"#if MAY_USE_INT64
",
а в начале файла выставлять значение этого символа (если не определён) в
зависимости от версии компилятора.
13.08.2024: делаем.
MAY_USE_INT64
.
#if MAY_USE_INT64
".
#if
'ами.
bool
'ов поддержка добычи из 64-битных
УЖЕ была -- там ведь отдельный механизм, с прямым чтением.
14.08.2024: проверяем -- а фиг! В GCC-10 тоже не компилируется, хотя уже с другими ругательствами (причём дикого объёма); основной смысл оных -- что компилятор не может подобрать подходящую функцию/оператор; например --
и перечисляет кандидатов (толпы!!!), которые не подходят.cda_d_tango.cpp:748:56: error: no match for 'operator>>' (operand types are 'Tango::DeviceData' and 'int64' {aka 'long long int'}) 748 | case Tango::DEV_LONG64: *(data_value) >> *(( int64*)value_p); dtype = CXDTYPE_INT64; break; | ~~~~~~~~~~~~~ ^~ ~~~~~~~~~~~~~~~~~~~ | | | | | int64 {aka long long int} | Tango::DeviceData
А дальше совсем страшные и непонятные ругательства вроде
In file included from /usr/include/tango/tango.h:102, from cda_d_tango.cpp:16: /usr/include/tango/devapi_attr.tpp: In instantiation of 'bool Tango::DeviceAttribute::template_type_check(T&) [with T = long long int]': /usr/include/tango/devapi_attr.tpp:189:27: required from 'bool Tango::DeviceAttribute::operator>>(T&) [with T = long long int]' cda_d_tango.cpp:550:77: required from here /usr/include/tango/devapi_attr.tpp:414:70: error: no type named 'type' in 'struct std::underlying_type<long long int>' 414 | bool short_enum = is_same<short,typename underlying_type<T>::type>::value; | ^~~~~ /usr/include/tango/devapi_attr.tpp:415:79: error: no type named 'type' in 'struct std::underlying_type<long long int>' 415 | bool uns_int_enum = is_same<unsigned int,typename underlying_type<T>::type>::value; | ^~~~~
enum
" использовать "enum class : int
",
и у него оно отказывалось работать как обычный int (он хотел, чтобы тип вёл
себя как целое, но был бы НЕсовместим с обычным int
).
Ему ответили, что (bold мой, italic ихний):
That's the feature. Scoped enumerations are not implicitly convertible to integers, so you can't use them in lieu of integers. They are strongly typed by design. If you want the implicit conversion, use unscoped enumerations.
std::conditional_t
".
Ни в самом вопросе, ни в ответах почти нихрена не понятно, но в ответе есть пассаж (bold ихний)
std::conditional_t
always has three fully evaluated arguments:
Something boolean-like, and two types.
If one of them cannot be evaluated if the other is selected, you need to use a custom template and specialize it appropriately:
По результатам чтения (может, в списке выше что-то ещё упустил?) у меня создалось впечатление, что что-то с "совместимостью"/"различностью" типов.
int64
и uint64
, а нужно что-то
специфично-Tango'вское.
DevLong64
и DevULong64
,
определённые в tango-9.3.4/lib/cpp/server/idl/tango.h как
-- это при том, что в "исходнике" tango-9.3.4/lib/idl/tango.idl определяются какtypedef ::CORBA::LongLong DevLong64; . . . typedef ::CORBA::ULongLong DevULong64;
typedef long long DevLong64; . . . typedef unsigned long long DevULong64;
int64*
" на "DevLong64*
".
И оно скомпилировалось даже gcc-4.8.5 на CentOS-7!
(typedef Tango::DevLong64 Tint64; typedef Tango::DevULong64 Tuint64;
T
-- Tango) -- имена подобраны так, чтоб
int64
" и "uint64
" на
"Tint64
" и "Tuint64
" соответственно -- и в типах
переменных, и в их названиях; например, получилось вот такое:
vector<Tuint64> v_Tuint64;
После чего оно скомпилировалось даже под gcc-4.8.5!
#if MAY_USE_INT64
" (из
самого определения которого проверка версии GCC убрана, а оставлено просто
умолчание 1
) -- на случай, если на какой-то платформе будут
проблемы, можно будет легко отключить поддержку 64-разрядных целых, указав
make'у "CPPFLAGS=-DMAY_USE_INT64=0
".
На этом проблему поддержки 64-битных целых можно наконец-то считать решённой.
...но определения типов "::CORBA::LongLong
" надо бы найти,
чтобы лучше понять происходящее.
15.08.2024: ну поискал -- CentOS-7.3, omniORB-devel-4.2.0-3.el7.x86_64:
typedef _CORBA_LongLong LongLong;
typedef _CORBA_LONGLONG_DECL _CORBA_LongLong;
# define _CORBA_LONGLONG_DECL long long
17.08.2024: нифига: ИСПОЛЬЗУЕТСЯ
omniORB4/CORBA_sysdep_auto.h: # define _CORBA_LONGLONG_DECL long
при
"#if defined(SIZEOF_LONG) && (SIZEOF_LONG == 8)
".
Но пока причина несовместимости непонятна -- вроде как просто определения, без какого-либо "scope"/"namespace". А понятнее становится после осознания, что в CORBA.h:
# include "CORBA_primitive_types.h"
стоит "внутри"...
_CORBA_MODULE CORBA _CORBA_MODULE_BEG
#ifdef HAS_Cplusplus_Namespace # define _CORBA_MODULE namespace # define _CORBA_MODULE_BEG { # define _CORBA_MODULE_END }
Т.е., похоже, что проблема чисто в том, что "LongLong
"
определён внутри "namespace CORBA
", а потому несовместим с
обычным "long long
". 17.08.2024: а
вот и нет -- проблема с тем, что в omniORB4 тип
LongLong
-- это long int
, а не long long
int
, вот они и "несовместимы".
Непонятностей тут 2:
typedef
-тип, определённый в
namespace
, является "отдельным" ("distinct"?) и
несовместим/непреобразуем-автоматически с обычными такими же типами?
17.08.2024: видимо, НИГДЕ. Ибо это просто не так.
long
long
), а на 8-, 16- и 32-битных -- нет? 17.08.2024: потому, что ТЕ определяются корректно, без
закидонов с разным количеством модификаторов "long
".
Определяются-то они в том же omniORB4/CORBA_basetypes.h с единственным отличием, что НАПРЯМУЮ -- например, как
а не через лишнийtypedef long _CORBA_Long; typedef unsigned long _CORBA_ULong;
#define
(хотя и внутри #if
'а, с
проверкой сначала SIZEOF_LONG
, а потом SIZEOF_INT
на равенство 4).
15.08.2024: попробовал поразбираться, даже написал целый тестовый пример test_scoped_int64_conversion.cpp такого вида:
-- авотфиг! Нигде никаких ошибок, gcc-4.8.5 всё съедает. Причём в диагностике, выдававшейся на "ранней стадии" написания текста, когда оператора ещё не было, оба scoped-типа честно расшифровывались как "namespace MNS { typedef int scoped_int32; typedef long long scoped_int64; }; typedef int global_int32; typedef long long global_int64; class user { MNS::scoped_int32 v32; MNS::scoped_int64 v64; public: void operator<<(MNS::scoped_int32 &v){v32 = v;} void operator<<(MNS::scoped_int64 &v){v64 = v;} }; int main(void) { MNS::scoped_int32 s32; MNS::scoped_int64 s64; global_int32 g32; global_int64 g64; user u; u << s32; u << s64; u << g32; u << g64; }
int
" и "long long int
":
work/tests/test_scoped_int64_conversion.cpp: In function 'int main()': work/tests/test_scoped_int64_conversion.cpp:26:7: error: no match for 'operator<<' (operand types are 'user' and 'MNS::scoped_int32 {aka int}') u << s32; ^ work/tests/test_scoped_int64_conversion.cpp:27:7: error: no match for 'operator<<' (operand types are 'user' and 'MNS::scoped_int64 {aka long long int}') u << s64; ^
Дальше была мысль глянуть -- не определены ли в
DeviceAttribute
отдельно методы для обычных 32- и 16-битных int
и short
? Ну глянул, ужаснулся:
DevLong
/DevULong
.
Т.е., нестандартизованный бардак.
18.08.2024: разобрался. Вкратце -- дело в том, что
OMNIORB4 на x86_64 определяет "LongLong" как "long int
", а не
"long long int
", вот тип и не подходит.
Цепочка расследования:
и затем в#include <tango.h> typedef Tango::DevLong tangos_int32; typedef Tango::DevLong64 tangos_int64;
main()
декларации
и statement'ыtangos_int32 t32; tangos_int64 t64;
u << t32; u << t64;
-- ключевое тут то, чтоtest_tango_int64_conversion.cpp: In function 'int main()': test_tango_int64_conversion.cpp:38:7: error: no match for 'operator<<' (operand types are 'user' and 'tangos_int64 {aka long int}') u << t64; ^
tangos_int64
считается за long
int
(а не long long int
, как scoped_int64
в
самом тесте (и как int64
в CXv4)).
long int
и
long long int
НЕ считаются совместимыми и оно в результате не
может подобрать подходящего кандидата.
Вопрос же в том, почему такое странное определение.
long int
" имеет размер 64 бита, т.к. "размер
long
ВСЕГДА равен размеру size_t
".
g++ -E
", чтобы посмотреть, как же
определён тип _CORBA_LongLong
.
Оказалось -- да, просто "long
".
g++ -dM
", чтобы посмотреть источник
определения _CORBA_LONGLONG_DECL
(которое и является
первоисточником).
Увы, в листинге макросов gcc, в отличие от "make -p
", НЕ
печатает ФАЙЛ:СТРОКУ, откуда взялось определение...
grep _CORBA_LONGLONG_DECL /usr/include/omniORB4/**/*.h
" --
заметил, что кроме CORBA_sysdep_trad.h определяется также и в
CORBA_sysdep_auto.h, и вот ТАМ одно из 2 определений как раз просто
"long
".
-- а// // Most system dependencies either come from autoconf or from // hard-coded values in CORBA_sysdep_trad.h // #ifndef OMNI_CONFIG_EXTERNAL # include <omniconfig.h> #endif #ifdef OMNI_CONFIG_TRADITIONAL # include <omniORB4/CORBA_sysdep_trad.h> #else # include <omniORB4/CORBA_sysdep_auto.h> #endif
OMNI_CONFIG_TRADITIONAL
нигде не взводится, так что
используется именно CORBA_sysdep_auto.h с его "long
".
Ну вот, опять этот клятый autoconf!
Выводы:
int64
/uint64
-- чисто "ритуальная", на уровне имён
в языке, а не реальной работы железа.
LongLong
-- загадка. То ли из-за какой-то
одиночной экзотической архитектуры (оно ж должно на дофига всякой хрени
работать), то ли по дурости, то ли просто мою табличку не посмотрели :D
CORBA::Long
/Tango::DevLong
-- никакой не
long
, а просто int
, в то время как как
"long int
" определяется
CORBA::LongLong
/Tango::DevLong64
, который, в свою
очередь, вовсе НЕ "long long
".
long long
,
как у нас: в base-7.0.7/modules/libcom/src/misc/epicsTypes.h (ранее
base-3.15.6/include/epicsTypes.h) такой фрагмент (bold мой):
/** * \name epicsTypes * Architecture Independent Data Types * * These are sufficient for all our current archs * @{ */ typedef signed char epicsInt8; typedef unsigned char epicsUInt8; typedef short epicsInt16; typedef unsigned short epicsUInt16; typedef int epicsInt32; typedef unsigned int epicsUInt32; typedef long long epicsInt64; typedef unsigned long long epicsUInt64; typedef epicsUInt16 epicsEnum16; typedef float epicsFloat32; typedef double epicsFloat64; typedef epicsInt32 epicsStatus; /** @} */
Т.е., определения для поддержки N-битных целых -- В ТОЧНОСТИ, как в CX.
Надо бы поискать по архивным релизам EPICS и omniORB4 -- а когда там вообще появилась поддержка 64-битных целых и как она там выглядела?
19.08.2024: ну поискал.
typedef int64_t epicsInt64;
" и только внутри
"#if __STDC_VERSION__ >= 199901L
", а иначе не определялось
никак.
...мда, озадачивает -- R3.14.1 от 20-12-2002; но внутри там
epicsInt64
не было.
long
".
long long
".
Т.е., та порнография с просто "long
" появилась где-то в
момент перехода с ветки 3 на ветку 4 ("omniORB4").
int64
(как long long
) --
BACKUP/istc.20041212.messy/drivers/canadc40_drv.c с той же датой на
файле, что в имени директории.
01.12.2024: при написании отчёта по госзаданию за
2024г возник вопрос "а как определяются стандартные типы, вроде
__int64_t
?". Поискал -- результат грустен:
#if __WORDSIZE == 64 typedef signed long int __int64_t; typedef unsigned long int __uint64_t; #elif defined __GLIBC_HAVE_LONG_LONG __extension__ typedef signed long long int __int64_t; __extension__ typedef unsigned long long int __uint64_t; #endif
/* These types are defined by the ISO C99 header <inttypes.h>. */ # ifndef __int8_t_defined # define __int8_t_defined typedef char int8_t; typedef short int int16_t; typedef int int32_t; # if __WORDSIZE == 64 typedef long int int64_t; # elif __GLIBC_HAVE_LONG_LONG __extension__ typedef long long int int64_t; # endif # endif
Т.е., определения ОЧЕНЬ похожи на то, что в CORBA_sysdep_auto.h -- возможно, что в omniORB4 они попали из какого-то файла от C99...
20.05.2019@дорога-на-обед-мимо-ИПА: ЗАМЕЧАНИЕ: это делается НЕ из-за ПОТРЕБНОСТИ, а из-за ВОЗМОЖНОСТИ. Всё равно осваивать (хоть по минимуму) это NI'ное поделие -- ну так и формализуем в модуле; тем более, так навскидку DAQmx'ный API выглядит годням для заворачивания в синтаксис/парадигму каналов.
Плюс, возможность получить дополнительный опыт на тему "а как бывают парадигмы доступа к данным" (и, тем самым, приблизиться к пониманию "природы данных, в платоновском смысле") -- то, ради чего хочется повозиться с TANGO и EPICS.
20.05.2019: 16.05.2019 пытался (в процессе разбирательства с DAQmx под Linux) гуглить информацию/руководства о программировании под DAQmx вообще и под Linux в частности. Пока что -- фигвам, ОЧЕНЬ всё грустно. Как и вообще с NI -- сайт у них организован отвратно (Microsoft их укусил?), "воды" очень много, а толковой информации практически ноль.
22.05.2019: да, сайт NI.com очень дурацкий, и документацию там найти крайне сложно; Дубатов это даже признаёт и говорит, что "По поводу документации. Согласен, что не так просто ее найти на сайте - проблема давно известная, ее как могут устраняют.".
Также он прислал некоторое количество ссылок на документацию (он-то знает, где её находить) и комментариев на тему "что есть что".
NI-DAQmx Base - это облегченная версия драйвера, поддерживающая только USB-устройства. Раньше информация об этом была где-то на поверхности, сейчас я ее быстро не нахожу. Но это как раз та самая старая архитектура.
Вот кто б мог догадаться по названию, что "Base" -- это только для USB!
В принципе, это уже исчерпывающая документация. Да, она ориентирована на Windows, но концепция от этого не меняется.
Но см. ниже моё замечание насчёт "полезности" этой ссылки.
Если вкратце, утилита позволяет идентифицировать шасси (если это не произошло автоматически), вручную нумеровать шасси (когда их несколько в системе), смотреть список устройств, задавать им alias'ы, конфигурировать триггерные линии шасси и пр. У каждого шасси есть свои особенности, поэтому правильнее читать мануал на конкретное шасси. По конфигурации PXIe-1078 соответствующий мануал - http://www.ni.com/pdf/manuals/373204c.pdf (в частности, стр. 2.13 - 2.15). В мануале все настраивается через MAX, но большинство функций можно сделать через nipxiconfig. Но в принципе, можно ничего не конфигурировать и оставить все как есть. Единственное, что нужно будет сделать - это посмотреть список устройств, чтобы знать их имена, чтобы обращаться к ним из программы."
-- это в ответ на моё
Что делает "nipxiconfig" -- для меня загадка. Так же, как и "nidaqmxconfig" (с этим ещё хуже). Ни в самих программах, ни в их man-страницах, ни в общем описании нет ни слова про то, о какой вообще "конфигурации" речь.Мне, как человеку, избалованному шинами PCI/PCI-Express, совершенно непонятно, какая конфигурация (на системном уровне!) может требоваться с шиной plug-and-play.
(Это было прислано в ответ на моё недоумение, почему в "NI-DAQmx C Reference Help" самая вроде бы интересная ссылка "NI-DAQmx Concepts" содержит лишь отписку "For more information on important NI-DAQmx concepts, including measurement fundamentals and device considerations, refer to the NI-DAQmx Help.", причём оный Help -- Windows-only, поскольку там вместо документации лишь даётся ссылка на скачивание .zip-файла с .chm'ами.)
NI-DAQmx Key Concepts covers important concepts in NI-DAQmx such as channels and tasks. The ways that NI-DAQmx handles timing, triggering, buffering, and signal routing are also central in the NI-DAQmx API.
23.05.2019: роем дальше:
A task is a collection of one or more virtual channels with timing, triggering, and other properties. Conceptually, a task represents a measurement or generation you want to perform. All channels in a task must be of the same I/O type, such as analog input or counter output. However, a task can include channels of different measurement types, such as an analog input temperature channel and an analog input voltage channel. For most devices, only one task per subsystem can run at once, but some devices can run multiple tasks simultaneously. With some devices, you can include channels from multiple devices in a task.(bold мой).
Я-то думал, что "task" -- это контекст (вроде cda'шного). Но тут всё хитрее (и явно неудобнее).
31.05.2019: пытался нарыть информацию о том, какие имена каналов у конкретных NI'ных устройств. Для чего гуглил на тему "NI-DAQmx physical channel naming convention.
Оно зарыто глубоко-глубоко, напрямую по навигации (двигаясь вглубь) туда не придёшь, я наткнулся случайно, по ссылке со страницы "Internal Channels".
31.05.2019: идея, после прочтения NI'ных текстов на тему Task - "Configure the channel, timing, and triggering properties as necessary.": очевидно, надо будет иметь возможность этого "конфигурирования". А конфиг.атрибуты - они вовсе не "каналы", а некие сущности, к каналам привязанные, но адресуемые отдельными вызовами.
Так вот: отображать их всё-таки как псевдоканалы, в синтаксисе "Device/Channel@ATTR" - аналогично синтаксису "v2cx::".
Тогда конфигурирование, например, времени интегрирования будет сводиться к записи нужного значения в канал вроде "Dev/Chan@TIME".
Отдельный вопрос в том, как поддерживать "связь" между каналами-атрибутами и реальными каналами, к которым они относятся. Видимо, придётся делать какой-нибудь reference counting; а возможно, ВСЕ hwr'ы делать "атрибутами", а собственно "каналы DAQmx" заводить в отдельном SLOTARRAY'е, на который ссылаться из hwr'ов; и удалять оттуда каналы при уменьшении счётчика использования до нуля.
01.06.2019: в продолжение:
02.06.2019: прочитал перечисленные выше тексты на тему DAQmx -- про Tasks, Channels (всех видов) и об "in Text Based Programming Environments".
Мои ВЫВОДЫ по результатам:
А если задача чуть-чуть сложнее и включает как измерения, так и уставки -- например, произвести некие измерения (с АЦП) при последовательно изменяемых уставках (в ЦАП), например, для снятия матрицы откликов или калибровочной кривой -- то уже не катит, т.к. в один task засунуть каналы разного вида (чтения и записи) низзя.
Был бы это некий "контекст", как в CX (или per-thread, как в EPICS) -- было б ясно: хранилище ресурсов, привязанных к "задаче".
Но это не так, и в чём отличие от ситуации, когда каналы бы просто регистрировались "глобально" -- загадка.
Т.к. просто сделать 1 task (на "sid" или даже глобально) -- нельзя, ибо каналы разных видов надо пхать в разные task'и. Возможно, что и от разных девайсов тоже.
А как решать, что "пришла пора завести новый task, для вот этого канала" -- загадка:
Плохо, т.к. каналы могут регистрироваться с чередованием -- R,W,R,W, и алгоритм "если ошибка, ..." создаст 4 task'а, хотя хватило бы 2.
Потенциальные проблемы:
Можно по имени: по принятому в DAQmx (или у NI вообще) стандарту, аналоговые входы начинаются с "ai" (Analog Input), аналоговые выходы с "ao" (Analog Output), цифровые -- с "di" и "do" соответственно, счётчики -- с "ctr".
Ответ -- а хбз: "неизвестные" каналы зарегистрировать у DAQmx всё равно не удастся: сама регистрация выполняется разными функциями, в зависимости от типа канала.
Похоже, единственное решение -- объявить глобальные каналы неподдерживаемыми.
И уж у них имена могут быть ну совсем произвольными.
units
, который int32
).
DAQmxCreateAIVoltageChan()
(для типа
"AI Voltage").
Т.е., cda_d_daqmx_new_chan()
'у понадобится ДОФИГИЩА
информации, которую неясно, как узнать.
Указывать прямо в имени канала, например, через суффикс '@', в формате "ПАРАМЕТР=ЗНАЧЕНИЕ"?
Мерзковато...
И мои ВПЕЧАТЛЕНИЯ по результатам:
...но и драйверы придётся приспосабливать для конкретных использований, в то время как сами NI'ные железки дюже гибкие и позволяют кучу вариантов работы.
Вот зачем, спрашивается, делать переконфигурируемые каналы, умеющие быть как входами, так и выходами? Пины на разъёме экономили?
И как это внутри устроено: хитрая микросхема ЦАП+АЦП+регистры, которая переконфигурируется? Или раздельные чипы (ЦАП, АЦП, регистры) и коммутатор, подсоединяющий затребованное к выходным контактам?
В первом случае -- шибко монструозно; во втором -- глупо и нерационально.
03.06.2019: попробовал ещё понять, как надо работать с этими task'ами -- можно ли одновременно завести 2 штуки, одну для входов, другую для выходов, чтобы можно было реализовывать "сценарии", с измерением отклика на изменения уставок. Или, в более общем случае -- вообще несколько.
Для чего загуглил на тему "daqmx multiple tasks". В верхушке выдачи оказались 2 ссылки:
Вывод: основной смысл "task'ов" -- управление общими (разделяемыми между каналами) ресурсами устройств, в первую очередь -- всякими таймерами/PLL.
Точнее, создать-то можно много task'ов, но в состоянии "reserved" будет только один, а остальные обломятся (при попытке "захватить ресурсы").
Т.е., то, что у обычных (наших) девайсов является управляющими регистрами устройства и может отображаться в качестве каналов, тут доступно через такой интерфейс. Смысл, видимо, в том, чтобы сделать работу максимально простой для "простых юзеров".
С другой стороны, для использования в ДРАЙВЕРАХ такая модель вполне прокатит. Так что -- frgn4cx/daqmx/drivers/ (плюс pzframes/ и screens/)?
07.06.2019: вопрос: а можно ли ДО создания канала просто по имени узнать его тип? Нету ли в DAQmx такого вызова? Это решило бы проблему "для разных типов каналов (вольты, температура, ...) разные процедуры открытия".
Отдельный вопрос вдогонку: а нету ли в API также вызова "получить список ВСЕХ каналов такого-то устройства"?
12.06.2019: "гвоздь в крышку гроба cda_d_daqmx.c": посмотрел я внимательно на NIDAQmx.h:
DAQmx_<ДЕЙСТВИЕ>_NNNN_<ЧЕГО_НИБУДЬ>()
или
DAQmx<Действие>NNNN<Объект>()
, вроде
DAQmx_Val_Switch_Topology_2576_2_Wire_Quad_16x1_Mux()
или
DAQmxSetup4480Cal()
.
DAQmx_Val_Voltage
=10322 -- 8 штук), файл по
крайней мере частично генерится из каких-то иных исходников.
Итого:
Вывод: похоже, идею о cda_d_daqmx.c стоит похоронить -- не катит этот API для таких целей.
14.06.2019@дорога-с-обеда-из-Гусей, у Николаева, 14: с другой
стороны, можно сделать общий "слой-прокладку": в стиле
vdev_sodc_dsc_t
, pzframe_chinfo_t
,
pzframe_chan_dscr_t
-- табличку каналов (адресуемую по нашему
номеру канала), где для каждого будут указываться:
vdev_sodc_dsc_t.subdev_n
, не так ли?).
Для такого "табличного" использования DAQmx вполне может оказаться даже удобным -- всю работу станет выполнять "daqmx_lyr", а сами драйверы будут состоять в основном из таблицы.
(В отличие от cda'шного использования, где для всего должно быть достаточно одного лишь имени канала.)
Кстати, эта мысль родилась из той мысли, что для EPICS, возможно, API DAQmx не так уж и плох -- ну сделают себе какой-нибудь особенный driver support (или всё же record support?), которому и достаточно будет указывать в record'е все вышеперечисленные свойства (а "настроечные" параметры там как раз лягут на поля record'а, возможно, уже штатно существующие).
Вся требуемая для его изготовления информация вроде уже есть.
Но чисто для унификации -- поскольку есть много устройств в двойном исполнении, PXI-NNNN и PXIe-NNNN -- будем давать имена БЕЗ "e".
16.06.2019: начало:
daqmx_chan_dsc_t
, daqmx_chan_cur_t
(пустой) и
daqmx_context_t
; все идеологически аналогичны vdev'овским.
int8
,... (только ЗНАКОВЫЕ, а БЕЗзнаковые там имеют префикс
uInt
), float32
, ..., то нужно было как-то
потенциальный конфликт решить. К счастью, там каждый typedef
окружён #ifndef
'ом с соответствующим символом вида
_NI_ИМЯТИПА_DEFINED_
, так что решение выглядит следующим
образом:
#include "misc_types.h" // Following defines are to prevent NIDAQmx.h from typedefing types with the same names as from misc_types.h #define _NI_int8_DEFINED_ #define _NI_int16_DEFINED_ #define _NI_int32_DEFINED_ #define _NI_float32_DEFINED_ #define _NI_float64_DEFINED_ #define _NI_int64_DEFINED_ #include <NIDAQmx.h>
Да, это сработало.
17.06.2019: ЧеблоПаша уточнил, что ИМ на ВЭПП-4 нужно уметь мерять вектора -- по внешнему старту, сколько-то точек, по внешним клокам.
17.06.2019: пилим...
Почему дилемма: потому, что надо как-то передавать/получать параметры для идентификации, кто же у нас "клиент".
Кроме собственно изоляции драйверов -- ещё и потому, что DAQmx передаёт
лишь 1 pointer, а task'ов более 1 штуки. Можно callbackData
использовать как task-N, а драйвера-получателя нам идентифицирует
cxscheduler -- даст и devid+devptr (если события ловить драйверу), и
privptr2 (если библиотеке).
...но task'ов-то может быть более 1 на устройство. Решение -- в 32 бита упаковывать и номер устройства, и номер task'а в нём; скорее всего, обоим хватит по 1 байту.
Муки выбора:
Есть, конечно, халтурные варианты:
Некрасиво.
Но нет: если под *nix дескрипторы действительно являются "НЕБОЛЬШИМИ неотрицательными целыми числами", то под другими ОС -- совсем необязательно.
...да и халтурновато.
callbackData
,
а класть в некую структурку, указатель на которую уже передавать "как
указтель".
Минус: придётся аллокировать ещё и массив этих структур, по числу task'ов...
В конечном итоге был выбран вариант "библиотека" -- с поштучными
объектами daqmx_context_t
.
load-lib daqmx
") daqmx_lib.so; соответственно,
исходные файлы тогда будут daqmx_lib.c и daqmx_lib.h.
18.06.2019: допиливаем первоначальный вариант.
measuring_now
.
Да, именно в таком порядке -- чтобы если сервер прямо из обработчика Return'а захотел бы заказать ещё измерения, то они бы запустились.
19.06.2019: в pzframe_drv_drdy_p()
тоже такой
порядок. И, кстати, выше чуть слукавлено: вчера был сделан обратный
порядок, а сейчас переставлено.
pxi6363_task_done()
сделано вычитывание данных и
возврат (объёмненько получилось).
pxi6363_rw_p()
населён "базовым скелетом", бегущим по
каналам и умеющим в т.ч. понимать значения DOUBLE, а не только *INT32 (это
было взято из frolov_d16_drv.c).
Каналы же сейчас понимаются только PXI6363_ADC_n_base
+n, при
measuring_now==0 вызывается StartMeasurements()
.
StartMeasurements()
-- простейший: взводит
measuring_now=1
и делает DAQmxStartTask()
.
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libnidaqmx.so ./sbin/cxsd ...
Причину удалось найти перебором значений количества сэмплов на тестовой программке Acq-IntClk.c.
Выбранное решение: заказываем измерение 2 сэмплов, а вычитывать просим 1.
DAQmxStopTask()
, иначе при следующей попытке старта
получаем ошибку "Specified operation cannot be performed while the task
is running.".
Исправлено просто -- ну делаем DAQmxStopTask()
в
pxi6363_task_done()
, непосредственно перед
ReturnDataSet()
'ом.
DAQmxGetExtendedErrorInfo()
, в конце зачем-то есть
'\n'. В результате получаем в логах лишний перевод строки.
Надо обрезать.
Пока на вид вполне удовлетворительно. Далее надо будет:
19.06.2019: идём дальше.
Предварительный список дел.
Поехали!
daqmx_geterrdescr()
, обрезающая нежеланный
'\n', и всё переведено на неё.
pxi6363_task_done()
вставлена проверка пока что простая
-- что мы имеем дело с TASK_N_ADC
, а в противном случае ничего
не делаем.
Как с такими гермафродитами обращаться -- решительно неясно.
DAQmxCreateDIChan()
или
DAQmxCreateDOChan()
.
Как решать проблему с картой -- хбз.
...А главное -- какой, нафиг, "include": ведь карта -- не список, а именно КАРТА, индексируемая номером канала.
_init_d()
ВСЮ карту
динамически, зануляя строки "несуществующих" каналов.
Одна только сложность, в связи с кривоватым API чтения/записи данных в DAQmx: там ведь идёт работа с блоком данных ВСЕХ каналов task'а, скопом. И если список каналов фиксирован, то ещё как-то можно приспособиться, а если он создаётся динамически -- то это уже задница...
Или пользоваться тем, что по готовой карте можно высчитать оффсет нужного
канала в общем блоке? Тогда высчитывать это в самом начале, и именно это
хранить в таблице из daqmx_chan_cur_t
(пока неиспользуемой)?
DAQmxRegisterSignalEvent()
.
Резюме: пока что эту тему замораживаем.
DAQmxRegisterDoneEvent()
и
DAQmxRegisterSignalEvent()
,
где про параметр callbackFunction
(адрес обработчика) сказано
"Passing NULL
for this parameter unregisters the event
callback function.", эти Event'ы в DAQmx -- НЕ множественные. Скорее
всего, во внутреннем представлении TaskHandle
(который
структура, а не void) есть по одному полю на каждый тип события, и эти
функции просто записывают указанное в эти поля.
Они получили стандартные имена out0...out3.
TASK_N_OUT
.
pxi6363_chandescr[]
.
DCK_nnn
(Daqmx Channel Kind, ex-DCT_ ("Type")).
Тут нужен минимум байт.
1 бит.
Вроде бы тоже всего 1 бит, но неясно, не понадобится ли несколько разных "классов" настроечных каналов (для автоматического выполнения действий, вместо поштучного перебора номеров).
Потенциально может понадобиться много битов.
Мини-обсуждение:
Но тогда, при надобности иметь разные "классы" настроечных, всё же понадобится 3-е поле.
Итого: на текущий момент выбрана схема "младший бит -- R(0)/W(1), далее -- вид/природа (натура)".
ENCODE_DCK()
.
rw_of_dck()
и
nature_of_dck()
.
DCK_IS_RO
=0, DCK_IS_RW
=1.
DCK_nature_NONE
=0,
DCK_nature_VOLTAGE
=1, DCK_nature_CURRENT
=2.
ENCODE_DCK()
из вышеуказанных констант.
daqmx_init()
научена их создавать.
Для чего в ней процесс создания чуток ортогонализован -- генерация имён вытащена в отдельный, более ранешний блок.
pxi6363_rw_p()
добавлена альтернатива для каналов
PXI6363_OUT_n_base
+n, также именно для них ожидаются значения с
типом DOUBLE.
daqmx_context_t.task_map
, описатели имеют тип
daqmx_task_dsc_t
; пока в нём единственное поле
options
, пока что ничего не делающее.
20.06.2019: засада: совершенно непонятно, как в DAQmx'ном API менять значение ОДНОГО конкретного канала ЦАП (а не всех, зарегистрированных для данного task'а).
DAQmxReadAnalogF64()
приводит к ошибке "Specified property
is not supported, because the task is not an input task.".
Получасом позже: а вот тут мутно -- рекомендуют читать внутренние каналы с именами вида "_aoX_vs_aognd", где X -- номер канала. Рекомендуют, например, в обсуждении вопроса "Is there a way to get the current value of a DAQmx output?", откуда ссылаются на KB-статью "NI-DAQmx Internal Channels for Self-Diagnostics and Self-Calibration" (традиционно видную только из гуглокэша). Нашлось это при гуглении на тему «"daqmx" get current ao value».
21.06.2019: чуть в сторону: возникло желание узнать, что же происходит "за кулисами": как обстоят дела с thread'ами, как libnidaqmx делается коммуникация с ядром.
Насчёт идентификатора thread'а -- "GetThreadID":
gettid()
.
(long int)syscall(224)
-- но так плохо, поскольку
SYS_gettid в разных системах может отличаться, где-то 186 вместо 224
(неясно, как такое возможно: ведь бинарники из одной должны быть
запускабельны в другой; или речь о разных архитектурах?).
tid1 = syscall(SYS_gettid);
#include <unistd.h> #include <sys/syscall.h> #ifdef SYS_gettid pid_t tid = syscall(SYS_gettid); #else #error "SYS_gettid unavailable on this system" #endif
26.06.2019: дошли руки посмотреть, как же устроена работа DAQmx. Итак:
И ps показывает, что в процессе (pid=9965) аж 5 thread'ов: 9965, 9969, 9970, 9971, 9972; куда исчезли 9966,9967,9968 -- неясно; можно прогнать через "strace -f", но лень.
...всё-таки посмотрел. Первый "исчезновенец" действительно порождается основным thread'ом, что-то делает и завершается. А следующие 2 -- видимо, каким-то сторонним процессом, т.к. их PID'ы в логе не фигурируют.
Чем конкретно занимается оставшаяся троица -- разбираться уже реально лень. Вкратце:
clock_nanosleep(CLOCK_MONOTONIC,...)
,
перемежаемые semop()
(возвращающим -1/EAGAIN; возможно, так и
должно быть).
ioctl()
'ит с дескриптором, глядящим на
/dev/nipalk.
futex()
'ы (это тот, в котором
Event'ы присылаются).
Кстати, с futex'ами возятся все.
01.07.2019: приступаем к научению драйвера работать осциллографом.
Они поделены на 2 блока по 50шт -- запись и чтение; каждый из тех блоков поделен на 30шт int32 и 20шт double, так что в сумме w30i,w20d,r30i,r20d.
_rw_p()
в начале цикла (где из *values[] берутся значения
для записи) обучена отличать double-каналы, в т.ч. отдельно нескалярные
каналы OUT_TAB.
_init_d()
инициализируется =1000, а в _rw_p()
форсятся значения в
диапазоне [2,MAX_NUMPTS].
Для этого:
measuring_now
из булевского переделан в
3-состоянный: DAQMX_MEASURING_{NOTHING,SCALAR,OSCILL}={0,1,2}.
StartMeasurements()
запрашивает нужное количество
измерений (2 либо cur_numpts).
pxi6363_task_done()
тоже предпринимает разные действия
-- в зависимости от значения measuring_now
.
Не всё пока доделано:
02.07.2019: а не заюзать ли pzframe_drv в
pxi6363_drv.c? Если не библиотеку, то хотя бы
pzframe_drv.h -- ради PZFRAME_CHTYPE_*
?
03.07.2019: делаем отображающий компонент -- pxi6363_data.c+pxi6363_gui.c.
Но директория hw4cx/pzframes/ не поддерживает "стороннюю сборку" (нет в ней никаких DirRules.mk), да и неудобно было бы иметь ДВА (или более) вариантов pzframeclient.
Так что подселяем прямо в hw4cx/pzframes/.
Для чего пришлось выпендриваться с добавлением ссылки на frgn4cx/include/, что очень криво.
Вывод: надо переселять ДРАЙВЕРЫ в hw4cx/daqmx/, и там делать надлежащую проверку "можно ли сейчас собирать или исключить всё из процесса сборки".
04.07.2019: продолжаем.
Краткий список того, что нужно сделать с осциллограммами "для минимального счастья":
r
(addrs,
dtypes, nelems, ...).
chinfo[]
, как в pzframe-драйверах, чтобы в
_init_d()
проставлять свойства каналов, а в _rw_p()
корректно "игнорировать" запросы ко всяким статусным/автообновляемым, вместо
отдачи CXRF_UNSUPPORTED.
Процесс работ:
ReturnDoubleDatum()
.
pxi6363_task_done()
вставлена
отдача каналов PXI6363_CHAN_ALL_RANGEMIN и PXI6363_CHAN_ALL_RANGEMAX.
Плюс целочисленные -- CUR_NUMPTS и MARKER.
Но тогда может образоваться race condition: поскольку сокет от клиента и
дескриптор event_pipe[PIPE_RD_SIDE]
отслеживаются в одном
месте, а номер дескриптора клиента может оказаться и ниже, то возможен такой
сценарий:
...но ещё не успевает вычитаться и обработаться.
Т.е. -- данные есть в ОБОИХ дескрипторах.
Но это уведомление от ПРЕДЫДУЩЕГО измерения, которое по STOP'у было только что выкинуто, а в реальности в текущий момень девайс висит на ожидании/измерении, и никаких данных отдать ещё не готов!
Решение-то довольно простое:
Так что в пределе хватило бы даже 1 бита.
Вопрос в сторону: а не возможна ли такая же ситуация с ПРОЧИМИ fast-ADC? Они ж у нас тоже ловят IRQ через файловые дескрипторы.
EventCB()
находится в ведении
не драйвера, а потенциального layer'а (или lib'а) daqmx_lyr. Так что к
приватным полям в драйверовом privrec'е никакого доступа не имеет.
Решение напрашивается такое:
daqmx_context_t
(надо добавить поле); и если оный
указатель!=NULL, то и проводить все проверки.
tctrs
tctrs[NUMTASKS]
, указатель на
который и сбагривается (совсем аналогично dpls[]).
StartMeasurements()
делает me->tctrs[TASK_N_ADC]++
непосредственно перед DAQmxStartTask()
.
ctr
(плюс
rsrvd
для кратности).
EventCB()
пишет туда значение соответствующего
счётчика, ...
event2pipe_proc()
проверяет.
abort_measurements()
(нулит буфера) и
prepare_retbufs()
, после которых уже простой возврат по готовым
спискам.
А в драйвере daqmx/pxi6363 такой инфраструктуры ещё нету.
Видимо, пока достаточно обойтись тупым последовательным (в цикле) возвратом всех каналов.
Из них практически нифига не понятно.
И с примерами оно не очень связывается -- например, буквосочетания "APFI" там нет вовсе.
В конце концов -- составлен файлик notes/20190704-NI-DAQmx-EXAMPLES-timings.txt, содержащий выдержки из всех примеров, так что можно окинуть всё компактно одним взглядом.
05.07.2019@утро-пляж: некоторые мысли:
09.07.2019: а вот и нет: судя по "X Series User Manual" (6363 относится к "X Series"), "APFI<0,1>" -- это специальные входы для "аналогового" (отсюда "APFI") триггера.
Но если считать, что указание вольтажа
(DAQmxCreateAIVoltageChan()
'овы параметры minVal/maxVal, плюс
DAQmxSetAIMax()
'ом) косвенно влияет -- т.е., переключает на
самый мелкий диапазон, достаточный для указанного -- то можно "наверх"
давать ручку со списком диапазонов, а каналам ставить значение в зависимости
от указанного.
Понять бы только точно, ЧТО указывать для включения некоего диапазона: например, для 5V -- 5.0, 4.9, 5.01?
07.07.2019: pxi6363_drv.c переехал из frgn4cx/daqmx/drivers/ в hw4cx/drivers/daqmx/.
MAY_BUILD_DAQMX_DRIVERS
в =YES при "найденности
NIDAQmx.h, а hw4cx/drivers/Makefile его include'ит и
анализирует возможность сборки.
Так что при необходимости форсить сборку надо указывать в командной
строке MAY_BUILD_DAQMX_DRIVERS=YES
, а если принудительно
запретить -- то, соответственно,
MAY_BUILD_DAQMX_DRIVERS=NO
.
10.07.2019: Андрей Дубатов прислал ответы на отправленные вчера вопросы, описанные выше за 05-07-2019:
Специфика шины PXI играет лишь частичную роль, т.к. в самой шине нет линий PFI. В качестве отправной точки по изучению синхронизации я бы рекомендовал эту: http://www.ni.com/tutorial/9735/en/ Первые две ссылки дают общую теорию, третья - с привязкой к конкретным линейкам модулей. В частности, для плат сбора данных актуальна статья http://www.ni.com/product-documentation/11369/en/. По-моему, весьма подробно.
Из практики и из курсов помню, что мы указываем ожидаемый диапазон сигнала, а драйвер выбирает наиболее оптимальный из возможных, чтобы весь сигнал влез. Сделано это так для удобства пользователей, особенно, когда мы меняем модуль, но программа остается той же - так не нужно ничего менять в настройках. Статья это подтверждает:https://knowledge.ni.com/KnowledgeArticleDetails?id=kA00Z000000P8XxSAK&l=ru-RU Насчет правила выбора диапазона, например, если установить лимит 4,9 или 5 В, то будет 5 В, если 5,01, то уже 10 В.
В User Manual'е сказано, что если сигнал превышает допустимый уровень, он обрезается, информация теряется. Обрезается на уровне предела, но это нужно проверять. Среди расшифровок кодов ошибок есть ошибка, возникающая при превышении входного напряжения, но насколько я помню, такая функция есть не у всех моделей, а только у анализаторов динамических сигналов.
Вывод: ставим прямо "тот" диапазон; например, для +-5 -- [-5.0,+5.0].
10.07.2019: обнаружилась странность: почему-то осциллограмма обновляется только с периодом сервера (1s - 1.2fps, 0.5s - 2fps, ...).
DO_IGNORE_UPD_CYCLE
.
А вообще -- явно напрашивается впихивание в "daqmx_lyr" куска мозгов от pzframe_drv, чтобы автоматически выполняло настройку всех каналов по таблице их свойств.
...но разобраться в том, что же конкретно является причиной редкого обновления -- надо будет, когда Дубатов опять даст этот крейт поиграться.
Просто чтоб был -- учитывая отсутствие (доступной) реализации C API для DataSocket, возможность реализации самого модуля весьма призрачна.
27.05.2019: ищем информацию по-простому -- гуглим на тему "datasocket linux".
Что найдено:
Последовательные соображения-ответы:
While, the DataSocket Server is only available on Windows platform, other platforms can be clients to a DataSocket Server, including LabVIEW for Mac OS X, Sun Solaris, Linux, and LabVIEW Real-Time.
Т.е., СЕРВЕР -- только Форточки, а клиент хоть и может быть под Linux, но не указывается, в каком виде -- вероятно, также CVI/LabView.
Резюме: итого, по состоянию на сейчас (или, на май 2018г?) -- фигвам, "DataSocket C API for Linux" не существует.
excmd_t
?
27.06.2007: с одной стороны, хочется иметь данный тип небольшим, чтобы не тратить зря память; с другой -- хочется упхать в него побольше.
И есть соображение: ведь на 32-битовых платформах тип
double
занимает 8 байт, а указатели и int
--
4. Так что вполне можно запихивать параметры для LAPPROX прямо в
arg
.
Кроме того, из соображений выравнивания все равно разумнее сделать тип размером 16 байт, а не 12, как сейчас.
И, наконец, cmd
надо переделать с char
'а
на int
, и как-нибудь покреативнее пользоваться битиками.
(Например, можно кроме бита IMMED добавить еще поле "исполнять при...",
в котором указывать желаемую маску условных битов, как в некоторых
процессорах. Но это уже так, левые размышления :-).)
02.07.2014: да. А именно:
fla_cmd_t
.
int32
, за которыми следует union с
double
внутри.
cmd
стало int32
, а флаги (включая
IMMED) вынесены в отдельное поле flags
.
_code
, за которым идёт сама
"ассемблерная" команда, а после неё аргумент (и можно сделать
принудительное окончание по NL, а не по ';').
03.07.2014: да,
_all_code
, переключающая
парсер в режим "принудительное _code до конца" -- после неё надо писать
уже просто опкоды, без префикса.
К вечеру: и складирование тоже сделано.
15.09.2015: был косячок: два подряд ';' приводили к ошибке "Syntax error: non-letter at the beginning of command".
Решено тривиально -- в кусок "пропустим whitespace, \n, \r" в начале парсинга команды добавлено также пропускание ';'.
10.07.2014: "базовый набор" команд сделан, как и сама "виртуальная машина".
Архитектура -- запланированная давным-давно, с отдельными функциями на каждый опкод (за исключением OP_RET) и таблицей, содержащей кроме имени опкода и ссылки на его функцию-исполнитель еще некоторые свойства:
FLAG_IMMED
и
FLAG_NO_IM
, сигнализирующие о необходимости
immed-параметра или о его запрещёности соответственно.
_code
для парсинга.
НЕ сделана пока условная инфраструктура (case, test, cmp, goto/label).
14.08.2014: продолжаем наполнение командами:
arg.displacement
, а при ненайденности команда превращается
в OP_NOOP с flags=0.
14.08.2014@вечер-пляж: да чего там думать -- делать просто 6 разными опкодами, и 6 раздельными реализаторами. А чтоб не дублировать код -- реализаторы определять одним #define-макросом, там ведь различия только в названии да в операции сравнения.
15.08.2014: так и сделано. Проверено -- работает.
02.10.2014: еще по мелочи:
cda_add_chan()
делает cda_add_varchan()
(без '%').
Также для этого пришлось чуть сменить правила парсинга -- чтоб leading-'%' был разрешен.
21.12.2015: вдогонку:
Например, в find_label()
не прерываться по нахождению, а
запоминать индекс и идти дальше до конца, и при обнаружении повтора (т.е.,
strcasecmp()==0 и ret>=0) возвращать -2.
Но вопрос -- РЕАГИРОВАТЬ-то на дубль как? По-хорошему, считать всю формулу недействительной, грохать и return DO_ERR("Duplicate label \"%s\""), но само гроханье пока никак не реализовано.
02.11.2018: ох ёлки, зачем "не прерываться..."? Надо просто поиск выполнять также и в момент РЕГИСТРАЦИИ метки. И если он вернёт наличие -- то это дубль.
02.11.2018: а еще НЕ возвращается наверх ошибка при
обломе регистрации канала/varchan'а. Просто в arg.chanref
складируется результат -1
и молча идётся дальше.
Из-за этого чуть ли не полдня убил, пытаясь разобраться со "странным" поведением драйвера trig_exec, регистрирующего явно неработоспособную формулу.
02.11.2018: исправляем и то, и другое:
ARG_STRING
-- там если
opcode==OP_LABEL, то делается попытка найти такое же имя, и при успехе
генерится ошибка "duplicate label".
10.07.2014: у виртуальной машины, в её нынешней модели (таблица, из которой берётся описание команды) есть два варианта:
Первый вариант проще в реализации -- ничего сливать не надо, но противнее в исполнении (постоянно дрюкаться с селекцией таблиц).
Второй же вариант сложнее в реализации ("добавление" к таблицам), зато процесс исполнения получается проще и беспроблемнее.
Похоже, лучше выбрать второй вариант.
Так что нефиг использовать sleep_tid>=0
в качестве
флага "мы ждём...".
01.01.2016: да, КАК? Учитывая, что формулы процессятся далеко ПОСЛЕ загрузки subsys-файла, то нет решительно никакой возможности доступиться до той же директории, откуда взят сам subsys-файл (а именно оттуда желательно бы мочь брать текст таблицы).
02.01.2016@утро-душ: напрашивается сделать lapprox-таблицу не параметром формулы, а свойством группировки или контекста, и чтоб формула уже просто ссылалась на неё. Заодно "решается" проблема множественных загрузок одной таблицы -- она реально будет грузиться единожды, а из формул ссылки на разные столбцы.
Но остаются 2 крупные проблемы:
lapprox_table МЕТКА ИМЯ_ФАЙЛА
не особо подходит, т.к. у Cdr_via_ppf4td.c нет доступа к текущему
пути парсинга.
@на-работе-в-обед: или есть? Ведь
существует ppf4td_cur_ref()
!
Для этого придётся делать директиву "скобками" -- например, парсить до закрывающей ')', воспринимая NL как переход на следующую строку таблицы.
...не особо-то красиво...
P.S. Сопутствующие вопросы решаются просто:
DSTN_LAPPROX
="lapprox"),
CdrDestroySubsystem()
делает safe_free() всем
p->data
).
03.01.2016: вариант -- можно сделать ОБЕ возможности.
findfilein()
.
Отдельный вопрос остаётся в менеджменте самих таблиц.
18.04.2018: замечание: раздвижка понадобится только при всасывании ПОЛНОЙ таблицы. Если же нас интересует только выжимка с 2 колонками (X и Y) -- то не надо; в v2 именно так и сделано.
destroy()
.
Резюме: да, муторновато, но как всё делать -- уже ясно и при надобности реализуемо за денёк.
21.03.2018@дорога-на-работу после обеда, около ИПА: пришла необходимость реализовать LAPPROX -- надо сварку переводить на v4, и потому усиленно думаем на тему "как".
Без вчитывания в написанное 2 года назад, просто мысли (возможно, что и повтор):
Мнемоника -- двоеточие в именах/путях ручек означает "подняться на уровень выше"; группировка для формулы как раз "выше".
Но тут, возможно, тоже надо как-то указывать номера колонок; также через двоеточия -- ":X:Y:ИМЯ_ТАБЛИЦЫ"?
21.03.2018@лыжи-вечером: (пошел поздно, в полшестого, торкнуло и было вдохновение)
Дополнение: это немножко шибко замудрённо, потому что получится, что надо один и тот же "мозг" парсинга иметь и в Cdr, и в cda_f_fla (а они формально совсем независимы: ни datatree{,P}.h, ни Cdr{,P}.h НЕ #include"cda.h"!). Общую библиотеку сделать, что ли; ну хоть копию можно иметь.
22.03.2018: отдельный проблем: архитектура ppf4td как-то
не предполагает возможности указывать свои ЛОКАЛЬНЫЕ плагины. Придётся вводить отдельный вызов, где вместо def_scheme указывать
"принудительный metric" (а reference так же, как и при def_scheme="!...",
считать за параметр-"черный-ящик" для передачи в
ppf4td_open_t
).
22.03.2018: НЕприспособлен, кстати, а МОЖЕТ (и вынужден будет) быть приспособлен: придётся добавить возможность хранения произвольного количества произвольных "секций". Вариант -- хранить ссылки на данные из Cdr'овой subsys.
fla_val_t
-- в "ссылке" на
таблицу -- надо как-то указывать, следует ли делать ей "free()". Т.е., кто
"хозяин" таблицы -- эта формула, или же "группировка" (точнее,
cda-контекст).
22.03.2018@вечер-дома: общее впечатление: всё описанное, конечно, красиво и глобально...
Пусть даже файл грузится тупо всегда из известного списка путей (типа ~/4pult/settings/common/), игнорируя путь самого .subsys'а.
25.03.2018@дома: да, надо просто взять да сделать, максимально просто и быстро:
cda_do_linear_approximation()
взять прямо из
v2.
lapprox_table_ldr()
,
только с прямой передачей xcol
и ycol
вместо
размещения в 2 половинках int32; можно её обозвать сразу
cda_load_lapprox_table()
.
excmd_lapprox_rec
" надо бы
обозвать как-то иначе.
cda_f_fla_p_destroy()
.
18.04.2018: надо так надо -- приступаем.
Сначала немного анализа -- результаты чтения v2'шной
lapprox_table_ldr
:
excmd_lapprox_rec
), а за ним таблицу.
fgets()
'ом, то и
никакое "skip to end of line" не требуется (а просто "goto NEXT_LINE").
Теперь собственно реализация.
Поэтому всё строго локально внутри cda_f_fla.c.
lapprox_rec_t
и с удалением реально неясно избыточного
поля-указателя pts
-- оно всё равно ВСЕГДА указывает на его же
data[]
.
fla_val_t
добавлена альтернатива
lapprox_rp
с этим типом.
OP_LAPPROX
.
cda_do_linear_approximation()
-- просто копия (только
static
добавлен).
...и пришлось ради неё ввести _cda_debug_lapprox
.
load_lapprox_table()
-- адаптированная
lapprox_table_ldr()
: изменён "прототип вызова" (главное --
прямая передача xcol, ycol), другая диагностика при ошибке, убрано
заполнение исчезнувшего указателя pts
.
19.04.2018: еще добавлен в начало параметр
ref
-- чтоб для сообщения об ошибке нахождения файла
использовать cda_ref_p_report()
.
19.04.2018: доделываем, идя в обратном порядке -- от конца к началу, и заодно от простого к сложному:
cda_f_fla_p_destroy()
.
proc_LAPPROX()
, по образу и подобию
v2'шного.
ARG_LAPPROX
.
lapprox X:Y:FILEREF
Т.е. -- по факту строка.
lapprox =FILEREF
при котором должно приниматься xcol=1,ycol=2; но, поскольку в
weld_mes_up_mv2mka.lst колонки поменяны местами -- xcol=2,ycol=1 --
то такой синтаксис там бесполезен и потому эта ветвь закрыта
"&&0
".
Проверяем.
Да, всё работает.
Но при попытке проверить "более простыми способами" вылезла пара странностей:
Почему-то уставляемое в ручке EMU значение через цикл откатывается на 0.knob - EMU text r:"_all_code; getchan localhost:5.icd.0; pop; getchan %abc; ret;" \ w:"_all_code; putchan %abc; ret;" disp I "I упр.эл. изм." text - mkA %3.0f \ r:"_all_code; getchan %abc; mul 1000; lapprox 2:1:weld_mes_up_mv2mka; ret;" \ disprange:0-300
cdaclient -m '__all_code; getchan localhost:11.abc;ret;'
ругается "syntax error in (null) float-value".
20.04.2018: и ключ -1
не помогает.
20.04.2018: блин, ну так всё понятно -- ведь используемый
там cda_add_chan()
регистрирует только каналы, а формулы --
НЕТ! Для них надо cda_add_formula()
, и в
Cdr_treeproc.c::cvt2ref()
для этого даже отдельная
логика есть, с префиксом '#' форсится формула ('=' --
канал, '%' -- varchan), а иначе используется эвристика определения
по содержимому (см. в разделе cda от 13-04-2014 и 08-04-2015).
Дальнейшие разговоры на эту тему в разделе по console_cda_util.c.
20.04.2018: а про "syntax error" ругается
console_cda_util.c::ParseOneDatum()
-- ведь пробелы
рассматриваются как разделитель между именем канала и значением для записи.
Потребовалось конкретно сегодня для лебединой термостабилизации -- он там
отдаёт наверх сильно зашумлённые/нестабильные измерения, которые просто мало
смысла сравнивать с "хорошими" значениями, т.к. "гулять" могут сильнее, чем
разрешенный допуск. 04.05.2016:
bigfile-0001.html, 26-08-2004, «надо в дополнение к типам
LOGT_DEVN
и LOGT_MINMAX
иметь еще тип
"LOGT_AVG
" -- для усреднения значения по нескольким
измерениям» и далее «потребность эта возникла в
термостабилизации, ором г-на Лебедева. Дело в том, что у г-на Клющева
имеется плавание выходного сигнала. Лебедев-то хочет, чтобы значение просто
считывалось из АЦП несколько раз подряд и усреднялось».
Эта тема затрагивалась в bigfile-0001.html 20-01-2006...23-01-2006, "есть потребность как-то уметь производить операции типа AVG прямо в cda, БЕЗ использования Cdr".
04.05.2016: некоторые заметки по возможной реализации:
Только данные -- имеющие кратность выравнивания 8 (по double) -- надо хранить ПЕРЕД строками.
bigfile-0001.html, 23-01-2006:
"уже сейчас есть потребность иметь дополнительный числовой параметр
(сейчас -- число циклов) для LOGT_DEVN
,
LOGT_MINMAX
, возможного будущего LOGT_AVG
и им
подобных" :-).
fla_val_t
места на 2 поля (число циклов, адрес таблицы) не хватит; видимо, надо
хранить "адрес" блока данных, где сначала будет идти число, а потом массив
чисел.
Видимо -- в том же "заголовке" перед массивом, рядом с количеством циклов для усреднения.
...ох вот времени нету на это. А тут где-то с день работы...
15.05.2021: и опять засветилась старая потребность --
avg
.
Смысл в том, чтобы делать такие формулы в сервере, дабы можно было такие результаты обсчёта публиковать как обычные каналы.
Есть несколько хотелок в другую сторону, связанных с потребностью уметь публиковать формулы как каналы:
Тут вопрос будет в том, как к ним из формул обращаться: лезть интроспекцией в namespace, получать количество доп.каналов и их имена? А из формул к ним как обращаться -- маппировать на "параметры"?
Тогда надо всё же научиться ловить -- КОРРЕКТНО! -- callback'и от вовлечённых в формулу каналов.
Кстати, для (a) надо ставить AUTOUPDATED_TRUSTED, а не AUTOUPDATED_YES, как сделано сейчас. Для (b) же -- AUTOUPDATED_NOT.
Откуда мысль: а может -- указывать этот режим работы прямо в auxinfo?
16.05.2021: рытьё по devlist'ам показало, что драйвер formula уже СЕЙЧАС работает напостоянку -- в canhw:19
"устройствами" kick_mode
и xfr_mode
. Т.е.,
нынешняя семантика УЖЕ используется, он не просто чисто экспериментален.
16.05.2021@утро: насчёт технологии реализации:
а не вытащить ли код вычисления как минимум для MINMAX в отдельную функцию, чтобы она могла использоваться в обоих местах (в Cdr_treeproc.c и в cda_f_fla.c)?
Вопрос лишь в том, КУДА это вытаскивать -- в cda? Как-то кривовато; уж точно не в cda_core.c. Сделать новый модуль, вроде cda_math.c?
Чуть позже: а ведь это должно быть там же, где и
cda_do_linear_approximation()
с
load_lapprox_table()
. Конкретно сейчас они живут в
cda_f_fla.c, но также напрашиваются на публикацию. Кстати, а в v2 оно жило в cda_math.c вместе с
реализацией формул (вытащенной туда ради избавления корневой cda от
зависимости от libm).
17.05.2021: одна только проблема: datatreeP.h
специально отвязана от cda.h -- чтоб Knob-иерархии можно было
городить независимые от СУ. А конкретно для ссылок на каналы даже определён
тип CxDataRef_t
в cx_common_types.h (и
cda_dataref_t
typedef'лен от него). И что теперь -- городить
аналогично отдельный cda_math.h?
17.05.2021@душ: можно сделать по аналогии -- "cx_simple_math", префикс --
"csm_
", и сама библиотека -- libcx_simple_math.a, с
makefile-ссылкой $(LIBCX_SIMPLE_MATH)
. Вопрос лишь, в
какую директорию это селить.
17.05.2021: взглянул на код в cda_f_fla.c (по
факту почти неизменный со времён v2) поподробнее -- ох, блин... Много там
ещё "тонкостей": например, есть отладочная печать, управляемая флагом
_cda_debug_lapprox
, должным включаться переменной
$CDA_DEBUG_LAPPROX
, проверяемой cda при создании нового
соединения, да только в v4 это так и не было сделано.
В общем -- вот не кажется сейчас "красивым" делать этот отдельный модуль по НЫНЕШНЕМУ пониманию. Так что лучше подождать, чтоб реальная необходимость назрела -- возможно, тогда и понимание улучшится и получится "конфетка", а не потенциальное квазимодо, как выглядит сейчас.
18.05.2021: решил всё-таки сделать "нулевой" шаг -- файл cx_simple_math.h, к концу которого также приделать и за-#if0'енный .c'шный код (пока не решено, куда всё-таки библиотеку поселить).
Так вот: в *_load_lapprox_table()
есть привязка к cda --
туда передаётся ref
, чтобы при ошибке находжения файла-таблицы
можно б было ругнуться посредством cda_ref_p_report()
. Типа
проблема -- которая, конечно, решается довольно просто: надо передавать
указатель на функцию-печататель, которая уже сама будет вызывать надлежащую
диагностику, а если она ==NULL, то самостоятельно печатать на stderr.
19.05.2021: да, вроде сделал -- к
csm_load_lapprox_table()
добавлен параметр
err_reporter
свежевведённого типа
csm_err_reporter_t
плюс void*err_privptr
, а выбор
того, как именно произвести вывод сообщения, свален на
csm_report_error()
.
Замечено при воспроизведении в liu.subsys функционала db_liu.h. Пока выкрутились использованием обычных %ПЕРЕМЕННЫХ.
OP_POLY
.
Очевидно, никогда не требовалась. Расследование показало, что она
использовалась (первый и единственный раз!) в программе drc -- это софтина
для автоматизации "запяткинского клистрона", из которой в конечном итоге и
вырос CX. И на самом свежем файле, в котором применяется
OP_POLY
-- curcx.20030415/src/xmclients/drc.c, дата --
01/02 августа 2002.
Вспомнилось (и было проведено расследование) по следам вчерашнего разговора с Роговским (на тему "что и как есть в CXv4", в связи с затеваемым переводом пикапов накопителя). При объяснении работы цепочек {R,D} он спросил -- "а только линейно, полиномов не бывает"?
Смысл в том, что пользоваться обычными комментариями '#' нельзя -- они плохо взаимодействуют с кавычками и с переносами строк через '\NL', что является обычным для почти любого использования формул (что в .subsys'ах, что в devlist'ах).
08.11.2018: сделано.
OP_COMMENT
, команда comment.
cda_f_fla_p_create()
команде принудительно сбрасывается
FLAG_IMMED
(аналогично опкоду OP_LABEL).
Возможные ситуации, когда это может потребоваться:
(Это и явилось побудительным мотивом.)
08.11.2018: сделано.
proc_CMP_IF_ISNAN
, команда
cmp_if_isnan.
proc_CMP_IF_ISNAN()
скопирован с
proc_TEST()
, только сравнение "val != 0
" заменено
на "isnan(val)
".
Хотя некритичным было бы оставить и flags=0.
09.11.2018: посмотрел-посмотрел я на то, что и как в cda_f_fla.c делается... Эх!
Чтоб совсем "красиво" -- неохота заморачиваться, смысла нет.
Сделано по-простому: прямо в proc_SLEEP()
к возвращаемому
CDA_PROCESS_FLAG_BUSY
дополнительно OR'ится при надобности
CDA_PROCESS_FLAG_REFRESH
.
02.05.2024: работы:
cda_fla_p_destroy_t()
сделан
возвращаюшим int
вместо былого void
.
CDA_FLA_P_DESTROY_SUCCESS
=0,
CDA_FLA_P_DESTROY_DEFERRED
=1 (аналогично
CDA_DAT_P_DEL_SRV_*
).
CDA_FLA_P_MODREC_VERSION*
не трогаем, т.к.
сейчас единственный юзер всегда влинковывается вместе со всей прочей cda.
RlsRefSlot()
крайне
проста: теперь результат вызова проверяется, и при
!=CDA_FLA_P_DESTROY_SUCCESS
делается
ri->fla_privptr = NULL
-- чтоб защитить от free()'нья ниже.
cda_f_fla_privrec_t
добавлены поля
being_processed
и being_destroyed
.
cda_f_fla_p_destroy()
в случае being_processed взводит
being_destroyed
=1 и возвращает
CDA_FLA_P_DESTROY_DEFERRED
.
process_commands()
теперь окружает вызов
descr->proc
инкрементом/декрементом
being_processed
, и если затем being_destroyed
оказывается взведён при being_destroyed==0
, то:
cda_f_fla_p_destroy()
;
free(fla)
;
CDA_PROCESS_ERR
с errno=EBADF
(сначала была идея EINTR
, но это у нас повсеместно считается
ВРЕМЕННОЙ ошибкой, так что нефиг; а так -- как если файловый дескриптор
умудряется стать закрытым в процессе системного вызова (например, другим
thread'ом или из обработчика сигнала)).
cda_f_fla_p_stop()
при взведённом
being_processed
... хотел было нулить
being_processed
, как задумано в проекте утром, но нельзя --
т.к. это СЧИТАЮЩИЙ флаг. Что-то надо другое придумывать.
@гуляя из подвала перехода в 14-е и
обратно: а может, в "флаг исполнения" -- который в
thisflags
/nextflags
сделать? Раз уж нужен всего 1
битик.
Так и делаем:
FLAG_DO_STOP
=1<<3;
cda_f_fla_p_stop()
взводит
nextflags|=FLAG_DO_STOP
;
10.05.2024: ага, "взводит" -- забыл тогда; сейчас
сделано.
process_commands()
проверяет его взведённость сразу
после исполнения процессера и при наличии возвращает
CDA_PROCESS_ERR
с errno=EPIPE
.
Всё, теперь проверять -- наверное, проще всего как раз посредством formula_drv, указывая ему соответствующие каналы.
...посмотрел -- а вот и нет: у formula_drv НЕТ канала "stop". Так что -- sim_dir_drv.c надо делать и проверять на нём.
12.05.2024: проверено, STOP работает как задумано, проблем нет.
13.05.2024: а поскольку и гроханье формулы во время её исполнения также работает, то помечаем как "done".
21.06.2007: Итого -- получаются такие модули:
01.07.2009: с учетом соображений в разделе "cx_tsigio", не будет никаких вариантов "транспорта", а всегда станет использоваться fdiolib. И, собственно, понятие "транспорта" при этом практически исчезает, поскольку даже о разборе протокольных заголовков заботится она же, а на долю cxlib'а остаются только операции "отправить пакет" и "что же это у нас пришло".
Посему -- остаются только cx_client.c и cx_utils.c, которые, кстати, уже и зачаты еще два года назад.
02.07.2009: точнее, понятие "транспорта" будет -- это та часть cxlib'а, которая устанавливает соединение и шлет/принимает пакеты, НЕ ВНИКАЯ в их содержимое. Так что, строго говоря, cx_client.c будет состоять из 3 разделов:
CxConnection
.
А теоретически может понадобиться -- например, из нормальной программы захочется вдруг некое значение добыть ПРЯМ ТУТ (чтоб пока не вернётся -- зависнуть).
08.10.2012: вот как такое реализовывать на cxscheduler'е, а?
sl_main_loop()
бить на части, чтоб его
можно было частями вызывать (ну и глобальный флажок вводить, чтоб оно
после таких выдрепонов прерывало бы цикл по fd)?
25.07.2013: сделано без особых проблем.
Проблемы будут при обратнопортировании слегка проапдейченного кода из v2 (где БЕЗ uniq).
ЗЫ: но cx_do_cleanup()
пока нету.
25.11.2013: cx_do_cleanup()
сделан.
CEnnn
.
25.11.2013: пока чуть-чуть:
CESRVSTIMEOUT
-- теперь
серверов пакет CXT4_TIMEOUT
маппируется на неё (чтоб
различать "handshake timeout", сообщенные клиентской и серверной
стороной).
CESYNCINSELECT
убрана, и
её место занято...
CESRVINTERNAL
-- на неё маппируется
CXT4_EINTERNAL
.
10.02.2015: некий вопрос -- а нужна ли реально libcx_sync.a?
Учитывая, что и cx-console будет по асинхронной модели, синхронным остаётся только cxclient -- а) чья нужность условна; b) который тоже б лучше перевести на асинхронность.
11.09.2009: за последние несколько дней сделана работоспособная основа:
ProcessFdioEvent()
.
cx_open()
. Теперь дело уже за сервером :-).
04.10.2009: да, проверил -- соединение устанавливается, проходя всю цепочку, и username/progname пересылаются.
13.08.2012: внедрён SLOTARRAY-GROWING вместо
былого собственного GetV4connSlot()
.
25.01.2013: ...и тогда (хотя, может, и раньше)
был допущен крупный ляп: при аллокировании слота НЕ делалось
->fd=-1
, а оставалось fd==0.
В результате дальше, если сервер не резолвился, то и до создания сокета не доходило, а вызывался
RlsV4connSlot()
, радостно делавшийclose(cp->fd)
-- т.е., close(0). Дальше же 0-й дескриптор выдавался первому попросившему (в случае v2'шного liu, на котором проблема вылезла, это был плагинчик, открывавший /dev/ttyUSB0), а потом он опять прикрывался cxlib'ом по очередному нерезолвингу. В том конкретном случае -- Xt валило вопли(9=EBADF), strace показалWarning: Select failed; error code 9"
-- еще бы, ведь 0-го дескриптора к тому моменту не существовало.select(4, [0], [], [], {0, 0}) = -1 EBADF (Bad file descriptor)
В общем -- тщательнее надо инициализировать свежеотведённые слоты.
cx_open()
совсем не зависал?
04.07.2009: по крайней мере -- введём специальную стадию для резолвинга, и чтобы если такой резолвер есть, то запускаем его и возвращаем управление клиенту.
11.06.2013: да, наконец-то нашел хоть сколько-то
стандартную реализацию: getaddrinfo_a()
.
getaddrinfo_a
живёт в очень отдельной libanl
(Asynchronous Name Lookup), весьма вероятно (судя по nm
-a
), требующей multithread'овости.
20.12.2017: чуть в продолжение:
nm
показывает присутствие ссылок на всякие
pthread_mutex_lock()
.
22.12.2017@утро, дорога-в-ИЯФ, после
ИПА, перед мышью: а чего про getaddrinfo_a()
гадать --
надо глянуть исходники в .src.rpm.
16.02.2010: история вопроса: сегодня стал
разбираться, почему cdaclient НЕ реконнектился при
убийстве-и-перезапуске сервера; оказалось, что
cda_d_cx.c::ProcessCxlibEvent()
не ловило
CAR_ERRCLOSE
. И вот -- после ловли оно стало падать по
SIGPIPE.
Расследование показало, что падает оно, когда
cx_close()
делает fdio_send()
для пакета
CXT4_LOGOUT.
Поскольку в CXv2'шном варианте содержимое почти идентично (с точностью до способа отсылки -- самостоятельно/fdiolib), а тамошние программы вроде как не валятся, то стал смотреть дальше. И:
там вMarkAsClosed()
делаетсяclose(cp->fd);
иcp->fd=-1
, так что последующийSendRequest()
уже пытается послать в дескриптор -1, что просто безобидно обламывается по EBADF.В bigfile*html на эту тему ничего нету, но в самом тексте при
cp->fd=-1
имеется комментарий "This was added only 22.10.2003, but was ABSOLUTELY necessary from the very beginning...".
Так что,
MarkAsClosed()
пришибать fdio-handle,
поскольку больше с этим дескриптором всё равно никакого В/В делать уже
нет смысла/нельзя.
set_signal(SIGPIPE,SIG_IGN)
также надо
везде, где только возможно.
Потому как вероятна ситуация, что сокет будет прикрыт, когда
в буфере fdiolib'а имелись какие-то данные для отправки (а программа
в это время не в select()
, а занимается какими-то другими
делами), и первым делом fdio_io_cb()
проверит сначала
SL_WR и дёрнет StreamReadyForWrite()
-- который и
скопытится на попытке записи.
Посему:
MarkAsClosed()
добавлено закрытие fhandle и fd (с
немедленным =-1
) ПЕРЕД вызовом нотификатора.
Соответственно, cx_close()
даже не пытается слать при
fhandle<0.
в самое началоset_signal(SIGPIPE, SIG_IGN);
main()
'ов в cxclient.c,
cdaclient.c, pult.c, tstknobs.c.
Замечание: куда-нибудь в саму библиотеку это вставлять некорректно -- поскольку у использующей программы может быть своё понятие о том, как обрабатывать сигналы.
Проверено -- нынешние вылеты cdaclient'а обе этих меры лечат и поодиночке, а уж парой -- 100%.
Итого -- проблема вроде бы подзалатана, так что ставим "done".
22.01.2013: в продолжение темы -- по результатам эксплуатации v2'шного fdiolib-based cxlib'а.
MarkAsClosed()
, и вылеты всё равно
имели место быть -- изредка, при убиении сервера.
(Ну еще бы -- тупой race condition, в буфере отсылки что-то есть, по закрытию сокет становится "готовым" на запись, эта проверка происходит ДО проверки на чтение (ну что с Xt взять ;-)), вот и причина...)
fdio_register_fd()
вставить
УСЛОВНУЮ установку SIG_IGN: если текущая реакция стоит SIG_DFL.
cx_open()
'у параметра spec
--
аналогично v2'шному (см. за 19-04-2011).
И теперь выпечатывание ссылки на сервер повешено на саму
cxlib_report()
(отчего она стала совсем гадкой).
09.10.2013: в cxlib_report()
добавлено также выпечатывание номера соединения -- cd
,
который она аккуратно добывает из cp
, cp==NULL влечёт
cd=-1.
17.10.2014: пока лишь введены
udp_socket
и udp_handle
.
А вот КАК делать -- еще неясно, есть только некие соображения, изложенные в общем идейном разделе за сегодня.
18.10.2014: сделана -- пока на функции-симуляторе
cx_resolve()
-- отправка UDP-broadcast-пакета, и ответ
тоже приходит. Т.е., UDP ведёт себя вроде как надо -- ура!
26.11.2022: реально всё обсуждение реализации идёт в совсем другом месте (раздел от 30-11-2017 ниже через один), но, поскольку сама технология давно работает, то тут помечаем как "done".
24.10.2023: только уже не "ниже через один", а в своём отдельном level4-разделе.
SetReady()
есть вызов _cxlib_break_wait()
!
07.04.2015: очевидно, надо в
cx_run()
запоминать Seq отправляемого запроса, чтобы в
async_CXT4_DATA_IO()
при совпадении сделать
_cxlib_break_wait()
.
...вот только возникает потенциальная проблема переполнения: если когда-то была зарегистрирована подписка с неким Seq, а потом прошло 2^32 циклов, круг замкнулся и оно висит на ожидании ответа с тем же Seq, то произойдёт сбой. Вероятность очень низка, конечно, но раз в 2^32 такое будет происходить ГАРАНТИРОВАННО. И чё делать -- timestamp запоминать, чтоб еще и секунды сравнивать? Тоже ведь не гарантия...
Короче -- пока добавлены:
syncSeq
(кстати, overkill -- при синхронной
работе оно же должно остаться прямо в самом Seq
, ибо в
синхронном режиме нефиг что-то делать до ответа на
предыдущий).
Насчёт же переполнения -- явно надо прямо в ПРОТОКОЛЕ как-то отличать пакеты ответов и пакеты асинхронной нотификации.
10.04.2015: прикол в том, что для асинхронных
уведомлений отдельный код пакета CXT4_DATA_MONITOR
уже
есть -- он появился где-то между 09-12-2014 и 26-12-2014, и был забыт
быть выкинут 25-12-2014.
Так что:
SendAReply()
(используется
при любом мониторинге) создаётся пакет с кодом
CXT4_DATA_MONITOR
вместо былого DATA_IO.
ProcessFdioEvent()
одинаково трактует оба кода, в обоих случаях вызывая
async_CXT4_DATA_IO()
.
_cxlib_break_wait()
при
CXT4_DATA_IO
, никак не используя Seq/syncSeq.
Так что недавновведённое syncSeq
теперь никак не
используется.
async_CXT4_DATA_IO()
идёт обработка множества chunk'ов,
и, теоретически callback кого-нибудь из них может вызвать
cx_close()
.
Так что надо и тут тоже внедрять инфраструктуру
being_processed
и being_destroyed
.
А ведь это касается вообще ВСЕХ и ВЕЗДЕ! Точно так же
может измениться и значение ctxs_list
, так что надо
пере-кэшировать значение ci
после каждого вызова
evproc'а. ...а вот это уже не получится -- ибо
сделано на GROWING-SLOTARRAY'е, который на подобное никак не
рассчитан...
Будущего HandleSearchReply()
это всё касается в равной
степени.
P.S. А в v2'шном-то cda такое БЫЛО предусмотрено, в обном месте (правда,
не касающемся никакой реентрантности, а просто обычная операция, могущая
изменить указатель на список) -- в cda_add_auxsid()
.
01.12.2017: а если немножко подумать, то получается, что эта проблема вообще ставит крест на попытках сделать возможность "удаления из callback'а". И даже больше, т.к.:
Что получается:
В частности, НЕ будет проблем в случаях, когда evproc, из которого выполняется действие (удаление/создание) -- единственный. Но в случае еманофединого менеджера режимов оно именно так.
GENERIC_SLOTARRAY_DEFINE_GROWING()
на
GENERIC_SLOTARRAY_DEFINE_GROWFIXELEM()
.
Придётся, правда, хранить id'ы элементов прямо в них -- т.к. фокус-хак с "*i2*id" путём вычитания из указателя на элемент указателя на начало массива работать перестанет.
04.12.2017: подробнее вопрос рассматривается в своём собственном подрезделе в "Проблемах" -- "Нереентрантность SLOTARRAY".
А тут пока прекращаем, только если что-то конкретное/критичное по cxlib.
30.01.2018: ну да, вылезло критичное: segfault'ится
HandleSrchReply()
в случае, если в пакете ответа более 1
chunk'а -- именно на 2-м chunk'е. Правда, НЕ в общем misc_macros'овском
коде SLOTARRAY'я, так что пофиксить удалось и очень дёшево. Подробнее см. в
разделе о нереентрантности, за сегодня.
30.11.2017: самое первое --
CAR_SRCH_RESULT
.
04.12.2017: далее:
cx_srch_info_t
.
Аналогична rsvl_info, но в конце еще поле name
(чтоб клиент
мог сравнить -- на его ли вопрос это ответ).
Общий подход и потребовавшиеся для него модификации:
type
. Которое введено
аналогично тому, как было в v2 (для CONS- и BIGC-соединений).
type
функция
CheckCd()
, с дополнительно возможным вариантом
CT_ANY
. В точности как в v2.
cx_seeker()
, а вот закрываться обычным cx_close()
.
cx_begin()
и cx_run()
.
CS_xxx
вёрнуто имя собственное --
connstate_t
.
CT_xxx
-- conntype_t
.
cx_seeker()
:
cx_open()
, то общий кусок вынесен в create_a_cd()
.
И там при просмотре кода попался косячок, существующий с каких-то
стародавних пор (в нынешнем v2 он тоже есть): при отсутствующей переменной
окружения LOGNAME
оно SIGSEGV'ится (проверено -- да!).
Добавлена защита.
sendbufsize
сразу ставится
CX_V4_MAX_UDP_PKTSIZE
.
MarkAsClosed()
работало как
надо.
HandleSrchReply()
. Пока в ней только скелет, с минимальными
проверками и проходом по chunk'ам, но без мозгов.
cx_srch()
-- имеет те же
параметры, что и cx_rslv()
.
И внутри это полная копия, с одним отличием: имеется дополнительная
проверка, что объём данных в пакете после добавления этого запроса не
превысит разрешенного. Если превысит -- ничего не делается и возвращается
код +1
.
05.12.2017: продолжаем:
cx_begin()
+cx_run()
--
небольшие переделки:
Type
) пакету теперь делается в
cx_begin()
, в зависимости от типа cd.
(Раньше StartNewPacket()
'у всегда передавался
0
, а присвоение типа делалось прямо в
cx_run()
.)
cx_run()
также добавлена проверка "какую вызвать
отправку, в зависимости от типа cd".
SendRequest()
в
SendDataRequest()
, ...
SendSrchRequest()
.
SendSrchRequest()
сделана. Она чуть сложнее
своей Data-сестрицы, т.к.:
var1
и
var2
, для опознания/расшифровки пакета сервером.
fdio_send_to()
вместо простого
fdio_send()
.
Также некоторые соображения на будущее, насчёт этого самого адреса для отправки:
htonl(INADDR_BROADCAST)
.
cx_seeker()
.
Поскольку всё равно переменные окружения глобальные, то per-cd складировать всё равно смысла нет.
cx_seeker()
.
Хотя тогда встанет вопрос "а как оный список передавать в cda (точнее, донести до cda_d_cx), чтоб оттуда уже спускать в cxlib?".
Тут некий вопрос будет -- как корректнее организовать эту множественную
отправку в SendSrchRequest()
? Просто цикл? А ошибки как
ловать?
А ведь по UDP ошибки могут возвращаться асинхронно, и их будет как-то
ловить fdiolib в DgramReadyForRead()
...
Очевидно -- придётся проверять экспериментально.
HandleSrchReply()
наполнена "мясом".
Но это криво -- т.к.
inet_ntoa()
.
Поскольку inet_ntoa()
возвращает указатель на свой
статический буфер, то строка копируется в дополнительный буфер, и уже на
него указатель передаётся клиенту.
CxV4Chunk
, просто после него в
data[] прицеплена строка.
CXC_RESOLVE
, что и обычные запросы резолвинга.
Главная проблема -- ОТВЕТЫ: у них в результате структура отличается, при том
же коде, что очень неправильно. Сейчас отладимся так, но потом надо ввести
иной код. 06.12.2017: сделано.
ServeGuruRequest()
надо проверять код chunk'а. 06.12.2017: уже.
06.12.2017: сделана утилитка cx-chan-search, и с её помощью видно, что, похоже, работает.
_cxlib_break_wait()
в начало
HandleSrchReply()
вроде как "решило проблему" -- точнее,
создало видимость этого: утилитка сразу после печати ответа честно
завершается (реально -- продолжает исполнение после
cx_run()
'а).
Понятно, что корректно будет в асинхронном варианте, когда утилита сама будет вести учёт полученных ответов.
CXC_SEARCH
.
cx_srch()
и проверка ответа в
HandleSrchReply()
переведаны на него.
ServeGuruRequest()
вставлена проверка на него.
25.12.2017: добавлены, в cxlib_client.c и в cxsd_fe_cx.c практически симметричные.
24.10.2023: в свой отдельный пункт вытащено только сегодня.
14.06.2022: всплыли некоторые детали работы broadcast'ов, которые, весьма вероятно, потребуют некоторых доработок.
CA does not automatically use the loopback interface. There is actually no portable way to do so in the presence of multiple IOCS. Linux is the only OS which (implicitly) gives the loopback interface a broadcast address.
После чего я припомнил, что EPICS
...и хотел было даже спросить
но решил сначала перепроверить это своё утверждение (чтобы не опозориться), и что-то оно сходу не подтвердилось: на тестах (слушая "netcat -u -l -p 8012" и генеря пакеты с помощью "cdaclient asdf.0") выглядело, что прилетает всего по 1 пакету, через eno1, который институтский интерфейс и default; причём если отсутствие в eno2 и eno3 можно было объяснить невоткнутостью в них кабелей ("link down" -- чё зря слать-то?), то в loopback -- уже нет.May I ask a bit offtopic question: why EPICS bothers with individual interfaces at all? At least in Linux it is sufficient to listen on 0.0.0.0 and to send broadcasts to 255.255.255.255.
И стал гуглить.
I think historically, BSD stacks were routing 255.255.255.255 to the "primary interface" (whatever that means). All the code I've encountered which actually needed to perform broadcast on all interfaces was sending subnet-directed broadcasts by hand on all interfaces.
Конкретно на СЕЙЧАС напрашиваются такие выводы:
Как это организовать -- хбз: состав интерфейсов ведь может меняться (hotplug, переконфигурирование, динамичность WiFi, ...).
16.06.2022: запоминать время составления списка, и если прошло
более 10 секунд -- составлять его повторно? (А сам список аллокировать
GrowBuf()
'ом.)
19.06.2022@ИЯФ, воскресенье, вечер: провёл эксперимент, соединив 2 интерфейса (eno2 и enp1s0 на x10sae) кросс-кабелем, причём адреса на них стоят из разных сетей, 192.168.1.254 и 192.168.2.254 соответственно. Итак:
19.06.2022: остаётся вопрос: а КОГДА добывать список интерфейсов? При каждой необходимости отправки пакета -- дороговато.
Можно, конечно, как предложено 16-06-2022, смотреть, что если прошло более 10 секунд с момента составления списка, то повторять, а иначе использовать предыдущий. Но это кривовато -- как и вообще любой поллинг против работы по событиям.
Решил поискать.
Ценно в первую очередь ссылкой на "Technical Note TN1145 - Living in a Dynamic TCP/IP Environment" от Apple.
Собственно технология:
socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)
CT_SRCH
-объект (их
может быть >1 внутри сервера) по своему списку адресов.
(Пояснение: узел-то один, и даже если будет использоваться environment, то он тоже будет одинаковый для всех.)
@15:30: неа, нифига -- НЕ будет: если обновления выполнять не сразу, а лишь взводить флаг "надо обновить!" и само обновление выполнять при первой надобности в отправке вопроса, то оно и происходить будет ровно однажды.
24.07.2022: некоторые сведения на тему "как получить список интерфейсов и из broadcast-адреса":
getifaddrs()
.
osiSockDiscoverBroadcastAddresses()
.
Ход действий там без предварительного знания понять довольно сложно.
Но то было вчера. А сегодня, вооружённый знанием, описанным ниже, перечитавши, увидел, что...
ioctl()
'ы
SIOCGIF*
, что я сегодня нарыл, оттолкнувшись от
SIOCGIFINDEX
.
SIOCGIFCONF
'ом добывают список интерфейсов.
Причём там захардкожено ограничение количества в
nelem = 100
(ioctl()
'у указывается размер буфера).
...(и это при том, что буфер calloc()
'ируется)
SIOCGIFFLAGS
и
SIOCGIFBRDADDR
.
can_hal_open_and_setup_line()
присутствует ioctl()
с кодом SIOCGIFINDEX
.
Который применяется к предварительно созданному сокету
=socket(PF_CAN, SOCK_RAW, CAN_RAW)
.
SIOCGIFCONF
.
SIOCGIFCONF
, возвращающий список интерфейсов с
IP-адресами, за которым следует пачка SIOCGIFFLAGS
с
SIOCGIFBRDADDR
.
SIOCGIFFLAGS
возвращает флаги, включая
IFF_UP|IFF_LOOPBACK|IFF_RUNNING
-- для lo,
IFF_UP|IFF_BROADCAST|IFF_MULTICAST
-- для всех
eno* (включая alias'ы eno*:N), кроме...
IFF_UP|IFF_BROADCAST|IFF_RUNNING|IFF_MULTICAST
--
eno1, который default (?).
SIOCGIFBRDADDR
отдаёт broadcast-адрес.
ioctl()
'ы выполняются с СОКЕТОМ вида
PF_INET
.
ioctl()
'ы.
26.11.2022: ну и PF_INET6 там тоже нету -- очевидно, в
т.ч. и потому, что в IPv6 отсутствует концепция "broadcast". И вообще, как выяснилось (не помню уж, было ли это где-то явно
написано или общее впечатление по сумме информации) --
SIOCGIFCONF
выдаёт ТОЛЬКО интерфейсы IPv4.
Дальнейшие поиски:
SIOCGIFCOUNT
, неясно как
работающий.
...видимо, никак: "IZ33123: ERROR MESSAGES WITH IOCTL CALLS USING SIOCGIFCOUNT ON LINUX":
This flag is deprecated(from kernel version 2.6) and hence the call used to fail
И ещё гугление выдало обсуждения в мэйллисте ядра, где сказано, что этот
код никогда и не работал -- вводили его ради совместимости со всякими AIX,
но так и недореализовали, т.к. ту же информацию можно получить от
SIOCGIFCONF
, указав ему нулевой размер буфера.
27.07.2022: нет, НЕ нулевой размер буфера, а
ifc_req=NULL
-- так сказано в
man netdevice (7): низкоуровневый доступ к сетевым устройствам Linux:
"Если ifc_req равно
NULL, то SIOCGIFCONF возвращает необходимый размер буфера в байтах для
приёма всех доступных адресов в ifc_len.".
The SIOCGIFCONF interface is depreciated and superseded by the if_nameindex()
family of functions.
И там же советуют читать документацию по sockio ("sockio" -- "socket ioctl commands"; man-страницы в CentOS-7 нету...).
25.07.2022: приступаем.
Принципы:
Обоснование: сколько бы клиентских соединений и resolver'ов ни создавалось, информация о списке broadcast-адресов едина и определяется конфигурацией сети узла (а если даже какой-то "$CX_RESOLVER_LIST", то она тоже едина).
(И, в перспективе, может сбрасываться =0 и какими-то иными событиями, вроде истечения большого периода времени или получения от netlink'а события "изменилась информация об интерфейсах).
Вызов сбора информации делается при необходимости отправить запрос И при флаге==0.
SIOCGIFCONF
аллокируется единожды и
существует до конца жизни программы, НЕ высвобождаясь.
Делаем "базу":
uint32_t bcast_list[BCAST_MAX_ADDRS=100]
плюс сопутствующие bcast_list_*
, включая...
bcast_list_present
.
uint8 *IFCONF_buf
.
set_bcast_list_INADDR_BROADCAST()
.
fill_bcast_list()
.
А вот дальше...
SIOCGIFCONF
-- ifreqSize()
с
грустным комментарием:
(bold мой), где/* * Determine the size of an ifreq structure * Made difficult by the fact that addresses larger than the structure * size may be returned from the kernel. */ static size_t ifreqSize ( struct ifreq *pifreq ) { size_t size; size = ifreq_size ( pifreq ); if ( size < sizeof ( *pifreq ) ) { size = sizeof ( *pifreq ); } return size; }
ifreq_size()
определяется конкретно в
libCom/osi/os/Linux/osdSock.h как
#define ifreq_size(pifreq) (sizeof(pifreq->ifr_name))
а в osdSock.h для других ОС -- весьма разнообразно.
"addresses larger than the structure size may be returned from the kernel" -- нормально, да?!
...После чего возникло сильное желание плюнуть на это всё и ограничиться нынешним решением с отправкой на 255.255.255.255.
26.07.2022: пытаемся всё же сделать -- базовый-то алгоритм прямолинеен, как стрела:
fill_bcast_list()
.
fd
, а v4conn_t *cp
.
set_bcast_list_INADDR_BROADCAST()
и отваливает.
ioctl()
'ов считывания
информации конкретного интерфейса обламывается (а как бы?!) -- просто
пропускает этот интерфейс.
SIOCGIFFLAGS
флаги этого интерфейса,
неактивные (с отсутствующим IFF_UP
) пропускает, а далее,
IFF_BROADCAST
-- SIOCGIFBRDADDR
,
берём его broadcast-адрес.
IFF_LOOPBACK
-- SIOCGIFADDR
,
добываем его адрес и сохраняем на всякий лучай в loopback_addr
,
которая изначально =0.
IFF_POINTOPOINT
-- SIOCGIFDSTADDR
,
берём его "внешний" адрес.
Иначе интерфейс игнорируется.
loopback_addr
!=0 -- записываем его как
единственный (смысл -- для standalone-хостов без сети, на которых и сервер,
и клиент).
set_bcast_list_INADDR_BROADCAST()
.
SendSrchRequest()
сделан цикл по
bcast_list[bcast_list_count]
.
Проверил, для начала при помощи strace (чтоб убедиться в корректности адресов -- что в нужном порядке) -- да, всё шлёт как надо. И сервер отвечает как надо, на все-все запросы. ...и библиотека все ответы получает, но адрес использует из ПЕРВОГО ответа.
$CX_SERVER
09.08.2022: не терпится, хочется взять да сделать. Предварительные соображения:
Да, это добавит тормозов и -- в отличие от потенциально расшивабельного
резолвинга при коннекте -- НЕ может быть расшито. @после-обеда: хотя... ведь можно
просто сам процесс заполнения списка считать отдельным "шагом работы машины
состояний CT_SRCH
-соединения", так что НЕ отправлять запросы до
тех пор, пока этот шаг не завершится.
Но сделать, чтобы таковой парсинг производился лишь единожды --
посредством взведения bcast_list_fixed
=1.
Для начала убираем захардкоженность порта:
bcast_port[]
в параллель к bcast_list[]
.
наbcast_list[bcast_list_count++] = ЧТО_ТО_ТАМ;
bcast_list[bcast_list_count] = ЧТО_ТО_ТАМ; bcast_port[bcast_list_count] = htons(CX_V4_INET_RESOLVER); bcast_list_count++;
htons(CX_V4_INET_RESOLVER)
теперь bcast_port[n]
.
Теперь парсинг:
is_a_guru_list_separator()
-- запятая, точка с запятой и isspace()
.
cx_open()
-- из 2 попыток, на
1-й пробуется просто IP-адрес inet_addr()
, а на 2-й уже
gethostbyname()
.
#if
-условием на
MAY_USE_GETHOSTBYNAME_FOR_CX_GURU_LIST
, который, в свою
очередь, предварительно определяется в 1
, но УСЛОВНО -- если не
определён.
Т.о., отключение можно сделать из командной строки сборки, указав
-DMAY_USE_GETHOSTBYNAME_FOR_CX_GURU_LIST=0
.
bcast_list_fixed = 1
взводится в самом
начале парсинга, так что повторно парситься не будет в любом случае.
Проверил -- вроде всё пашет как надо!
24.10.2023: в свой отдельный пункт вытащено только сегодня.
26.11.2022: а не добавить ли поддержку резолвинга через multicast?
Чтобы не выпендриваться с лишними переменными окружения -- просто считать, что если указанный адрес является multicast'ным (по маске: относится к 224.0.0.0/4), то работаем с ним соответственно.
26.11.2022: стадия сбора информации.
IN_MULTICAST()
.
...и в netinet/in.h тоже.
INADDR_ALLHOSTS_GROUP
? (Его аналог в IPv6 -- видимо, ff02::1).
Правда, тогда имеет смысл это делать ЕДИНОЖДЫ, а не по количеству интерфейсов: ведь на этот адрес должны откликаться все-все, и, видимо, достаточно отправки 1 штуки, а уж ядро должно бы распределить по всем интерфейсам (вот это то, что, ПО-МОЕМУ, должно было делаться с 255.255.255.255). 28.11.2022: а вот и нет -- оно шлёт ЕДИНСТВЕННЫЙ пакет, на default-интерфейс. Так что вся эта оптимистичная идея накрывается медным тазом.
27.11.2022: Как это можно было бы делать? Как вариант -- интерфейсы с флагом IFF_MULTICAST
не
добавлять в таблицу bcast_list[]
сразу, а взводить флаг "есть
поддержка MULTICAST" и в конце цикла добавлять одну ячейку.
sendto()
всегда по указанному адресу, но в момент первой для
SRCH-соединения отправки в SendSrchRequest()
исполнять
надлежащие ритуалы в случае необходимости (например, если надо
подсоединиться к MULTICAST-группе или взводить флажок "SO_MULTICAST" (а
такой есть?)).
27.11.2022: продолжение сбора информации.
@утро-душ: некоторые мысли по теме:
И шлётся ли он на ВСЕ интерфейсы? 28.11.2022: увы, НЕТ -- ТОЛЬКО на default.
Для этого нужно отправить netcat'ом пакет на какой-нибудь порт вроде 8012 и фильтровать дамп пакетов по нему.
Проверяем:
nc -u 224.0.0.1 8013
" генерит пакет на адрес
01:00:5e:00:00:01 aka "IPv4mcast_00:00:01".
Ни ping, ни "nc -u x.x.x.x 8013" (в последнем случае слушатель БЫЛ
запущен). Слалось на v5p2 как с v5p3, так и с v5p2 самой -- один чёрт
(только локально оно успевает сказать "connection refused").
По-хорошему -- надо бы проверить между x10sae и b360mc.
Чуть позже: а, нет -- работает, если UDP между NetCat'ами.
nc -l -u 0.0.0.0 8013
"
вместо
"nc -l -u 0.0.0.0 -p 8013
" (как вроде бы следует из
man'а; но реально там синтаксис неочевидный (см. "CONNECT MODE AND LISTEN
MODE")).
28.11.2022: да, именно так -- только на default...
Но почему-то только на умолчательном порту 5064; а вот указание серверу (в данном случае epics2cda) EPICS_CAS_SERVER_PORT=8013 и клиенту EPICS_CA_ADDR_LIST=224.0.0.1:8013 -- фиг. Чуть позже: а-а-а, дошло -- это firewall на x10sae блокирует! А порты 8012 и 5064 разрешены.
Правда, на локальной машине MAC'и и source, и destination показываются "незаполненными" 00:00:00:00:00:00.
Как минимум на пульту отправка с v5p3 на 224.0.0.1:8099 была на v5p2 увидена только на ОДНОМ интерфейсе из 2 -- eno2:192.168.129.*, а на eno1:192.168.133.* нет; видимо, потому, что default'ом там работает 192.168.129.1. Вот ТАКОЕ поведение может говорить о том, что формально-multicast'ные адреса также подчиняются общим "правилам маршрутизации", как и 255.255.255.255.
Надо всё же перепроверить на x10sae, активировав там второй интерфейс (как вариант -- закольцевав его на 3-й).
28.11.2022: проверил на закольцовке -- да, увы, уходит только на ОДИН интерфейс, на default...
Ещё немного на близкие темы:
IFF_RUNNING
у интерфейса -- летом возникал вопрос, в
чём отличие от IFF_UP
.
Как подсказал StackOverflow в "What is the difference between IFF_UP and IFF_RUNNING?" в ответе за 27-07-2012,
IFF_RUNNING
is supposed to reflect the operational status on a network interface, rather than its administrative one. To provide an example, an Ethernet interface may be broughtUP
by the administrator (e.g.ifconfig eth0 up
), but it will not be considered operational (i.e.RUNNING
as per RFC2863) if the cable is not plugged in.
Т.е., он показывает, что в поднятый интерфейс также ВОТКНУТ КАБЕЛЬ.
P.S. И да, loopback ВСЕГДА "RUNNING".
28.11.2022: проверил -- да, именно так. На x10sae интерфейсы eno2 и enp1s0 соединены кросс-кабелем; eno2 был поднят, а enp1s0 нет; при этом на eno2 RUNNING отсутствовал; после поднятия enp1s0 через несколько секунд (срок инициализации/прочухивания Intel'ов) они оба стали RUNNING.
IP_MULTICAST_LOOP
, чтобы самому хосту (точнее, другим
программам на нём) тоже доходил бы пакет.
По умолчанию она вроде бы включена, как минимум под Linux. Но в исходниках EPICS'а её всё же взводят в 1 ЯВНО.
28.11.2022: ещё поразбирался. Вкратце -- всё ОЧЕНЬ ПЛОХО, отправка на 224.0.0.1 идёт ТОЛЬКО на default-интерфейс.
Чуть подробнее:
При этом eno2 и enp1s0 соединены кросс-кабелем.
Результаты (с использованием wireshark):
echo abcdEFG | nc -u 224.0.0.1 8013
-- ушло на eno1 (как default).
echo abcdEFG | nc -u -s 192.168.129.254 224.0.0.1 8013
-- ушло на eno2, было принято с enp1s0.
echo abcdEFG | nc -u -s 192.168.2.254 224.0.0.1 8013
-- ушло на enp1s0, было принято с eno2.
echo abcdEFG | nc -u -s 192.168.171.173 224.0.0.1 8013
-- идентично вар.1.
Т.е. -- отправляется ВСЕГДА НА ОДИН интерфейс, на основании либо таблицы маршрутизации, либо исходного адреса.
I think you will have to bind not to an IP but to a device with e.g.
setsockopt(sd, SOL_SOCKET, SO_BINDTODEVICE, "eth0", 4)
Там упоминается
"multicast listener / sender example"
который теперь "host not found", но есть в
web.archive.org: "Multicast Example Programs".
Это примеры sender.c и listener.c от некоего Antony Courtney аж от 25/11/94.
30.11.2022: только оно там косячное:
listener.c не компилируется из-за того, что читает в
msgbuf[]
, а вывести пытается посредством
puts(message)
; после исправления откомпилилось.
И оттуда ссылка на касающийся bind()'инга же топик
bind()
для multicast-сокета" и говорится со ссылкой на W.R Stevens, что там это
работает "фильтром".
ОТ МЕНЯ: странно как-то... Для обычного-то UDP эту роль выполняет
connect()
...
Packets will only ever go out one interface, whether they are unicast, multicast, or broadcast.и предлагает
The correct way to handle this is as you've discovered, namely to set the IP_MULTICAST_IF
option on the socket before sending the packet.
IP_MULTICAST_IF
перед каждой отправкой.
The problem I ran into seems to be that receiving broadcasts from a specific interface is handled differently by Linux, Windows,... http://www.developerweb.net/forum/showthread.php?t=5722
The main obstacle I found was in discovering/inspecting host network interfaces. With IPv6 this is both more complicated, as most interfaces will have more than one address, and less portable.
Что делать конкретно сейчас: читать документацию.
Какого лешего там фигурирует "TCP" -- загадка.
29.11.2022: пытался понять причину; для этого гуглил "224.0.0.1", "224.0.0.0", "224.0.0" и "ff02" с "ff02::1" по lwn.net и lkml.org, толку -- ноль.
Также искал по "multicast routing" -- тоже без толку.
02.12.2022: дошли руки посмотреть код "от Antony Courtney". Так вот,
struct ip_mreq mreq; /* use setsockopt() to request that the kernel join a multicast group */ mreq.imr_multiaddr.s_addr=inet_addr(HELLO_GROUP); mreq.imr_interface.s_addr=htonl(INADDR_ANY); setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq);
На этом полезность того кода заканчивается, ибо:
-- т.е., мало того, что забывает про терминирующий '\0', не делая никакого strlen()+1, но вообще в качестве длины считаетchar *message="Hello, World!"; . . . sendto(fd,message,sizeof(message), ...
sizeof(char*)
!
Попробовал проверить, заменив порт с 12345 на 8012 (чтоб firewall не мешал) -- как-то работает.
@вечер: ЧеблоПаша прислал ссылку на пример для IPv6 -- https://github.com/bjornl/ipv6_multicast_example
IPV6_JOIN_GROUP
Технически там код сильно получше:
(Хотя вместо просто чтения из fd=0 делает
fd = open("/dev/stdin", O_RDONLY, NULL);
зачем-то...)
-- т.е., при прочтении 1400 байт оно запишет '\0' за границу буфера (это вполне реальный сценарий при использовании "как netcat" -- при подаче sender'у на stdin большого файла).char buf[1400]; . . . len = read(sd, buf, 1400); buf[len] = '\0';
...и, главное -- это совершенно лишнее! Там всё равно далее делается не
puts()/printf()/..., а write(fd, buf, len);
03.12.2022: а не потому ли на Star'е было "ping: sendmsg: Invalid argument", что в IPv6 присоединение к группе требуется обязательно?
То ли я старею и тупею (первое впечатление -- "ой, это для меня слишком сложно"), то ли реально IPv6 является чрезвычайно продвинутой (хитрозавёрнутой? хитрозамороченной?) и разветвлённой технологией, сильно сложнее IPv4 и труднопонятной сходу.
@вечер, засыпая: да нет -- IPv6 РЕАЛЬНО переусложнён. Из проблем, бросающихся в глаза:
Это радикально ломает парадигму адресации, так что IP-адрес внезапно становится недостаточен для адресации (звучит как парадокс, не так ли?).
04.12.2022: поразбирался -- решение есть (адрес в квадратных скобках -- "[ADDR]:PORT"), подробнее см. ниже за сегодня.
inet_ntop()
этот формат поддерживать никак
не может (ибо складирует только АДРЕС).
getaddrinfo()
-- МОЖЕТ, о чём в её man-странице
явно сказано:
getaddrinfo()
supports the address%scope-id notation for specifying the IPv6 scope-ID.
(Наткнулся гуглением по «inet_pton "percent"».)
Там простым языком объясняется суть проблемы "просто IP-адреса в IPv6 недостаточно для определения интерфейса, в который слать".
IPV6_JOIN_GROUP
в ipv6_multicast_send.c.
По крайней мере, при запуске отправителя и получателя на одном узле.
multicast-listener
", слушающего 224.0.0.1:8012, а на star'е
"ipv6_multicast_send ::ffff:224.0.0.1 8012
"
-- listener получает данные! Т.е., "трансляция адресов IPv6->IPv4" как
минимум в таком варианте работает.
04.12.2022: поразбирался с технически-историческим вопросом "проблема с указанием HOST:PORT из-за того, что разделители адреса в IPv6 тоже двоеточия".
Для решения придуманы более извратные синтаксисы, вроде
Но такие синтаксисы сразу сильно усложняют парсинг, если он в каком-то
контексте, а не просто изолированная строка из argv[]
.
За информацию о синтаксисах спасибо найденным по "ipv6 address port colon":
Во-первых, это позволило удобнее записывать IPv4-совместимые адреса в формате вроде "::FFFF:192.168.0.1".
Ну а во-вторых, во время разработки IPv6 в начале 1990-х, задача указания
портов отсутствовала: URL'ов ещё не было, а, например, в telnet
порт указывался опционально через пробел.
И там высказывается разумное соображение, что надо было брать разделителями дефисы '-', как это сделано в UUID'ах.
24.10.2023: пришла идея, почему не сделали разделителями
дефисы: адреса ведь можно сокращать, "сжимая" нули -- например, "::1"
(localhost); и вот конкретно localhost при записи через дефисы будет
выглядеть как "--1
" -- а это путается с long options.
24.10.2023: в свой отдельный пункт вытащено только сегодня.
17.10.2023: ЕманоФедя несколько дней назад (12.10.2023?) выдал претензию, что почему-то при работе через VPN бывает, что в егойном builder'е (на основе Qt-Designer) каналы в каком-то скрине (чьей целью является canhw:19) не резолвятся (причём не 10 секунд, а НИКОГДА); а если поставить в одном из них "прямую" ссылку (с именем сервера), то и он резолвится, и потом остальные.
Замечание: поскольку broadcast'ы через VPN как-то не совсем алё, то клиенту выставлялся CX_GURU_LIST со списком IP-адресов.
История разбирательства и решения:
(Код там выглядит максимально просто-прямолинейно, как по учебнику написанный, безо всяких умничаний/неочевидностей.)
#define
-символ DEBUG_ServeGuruRequest
(по
умолчанию =0), включабельный при сборке указанием
"CPPFLAGS=-DDEBUG_ServeGuruRequest=1
", который в
ServeGuruRequest()
приводит к выдаче на stderr диагностики в
каждой обломившейся проверке.
Результат -- никаких ругательств, а пакет ответа пришёл.
Результат -- серверу запрос приходит, ответ он отправляет, но клиент этого ответа не получает!!!
CX_MAX_UDP_PKTSIZE
--
тогда оно будет использоваться вместо CX_V4_MAX_UDP_PKTSIZE
.
Выполняется вычитывание в cx_seeker()
, значение потом
складируется в
limit_MAX_UDP_DATASIZE = max_pktsize - sizeof(CxV4Header);
каковой limit_MAX_UDP_DATASIZE
теперь и используется в
cx_srch()
в качестве ограничения вместо
CX_V4_MAX_UDP_DATASIZE
.
CX_MAX_UDP_PKTSIZE=1430
клиент заработал через VPN.
CX_V4_MAX_UDP_PKTSIZE
и CX_V4_MAX_UDP_DATASIZE
--
чтобы была иерархия "PKTSIZE=1500-28, DATASIZE=PKTSIZE-sizeof(CxV4Header)".
Проблема-то решилась, но ещё чуток дополнительного обсуждения:
Но после осмотра кода решено было от этого отказаться: аукнулось бы в
других местах. В частности, регистрация дескриптора у fdiolib'а выполняется
РАНЬШЕ проверки $CX_MAX_UDP_PKTSIZE
, а в этой регистрации
указывается максимально допустимый объём,
CX_V4_MAX_UDP_PKTSIZE
.
Пользы же от "поддержки больших пакетов" немного: ну сэкономим несколько пакетов при старте крупной программы, и всё; такой "выигрыш" явно не стоит усложнения кода.
PeriodicSrchProc()
алгоритмом отправки "вызываем cx_srch()
пока не вернёт
переполнение, затем отправляем, начинаем новый пакет и опять молча вызываем
cx_srch()
" может быть проблема: а если прямо ЕДИНСТВЕННЫЙ
запрос не войдёт в пакет -- т.е., спросят имя длиннее разрешённого размера
пакета?
Такое и раньше-то было возможно (указать имя длиной 2000 символов), но сейчас появилась возможность размер пакета уменьшить ниже разумного.
Именно поэтому ограничение на минимально допустимый размер пакета сделано 200 байт -- тут хотя бы одиночные РАЗУМНЫЕ имена каналов точно должны влезть.
23.10.2023: решил разобраться, как задача "не вылезти за MTU" решена в EPICS.
Традиционно -- сильно завёрнуто/закрученно и фрагментированно (ravioli
code; искал последовательно, начиная с SOCK_DGRAM
и
sendto()
, а затем что рядом казалось связанным).
datagramFlush()
, отправляющим --
вызовом метода searchRequest()
-- xmitBuf
в объёме
nBytesInXmitBuf
, ...
xmitBuf
в
udpiiu::pushDatagramMsg()
, где заодно и делается проверка "не
превысил ли текущий объём данных некоего предела", посредством
-- т.е., максимальный размер отправляемого буфера определяется просто размером массиваif ( msgsize + this->nBytesInXmitBuf > sizeof ( this->xmitBuf ) ) { return false; }
xmitBuf[]
.
xmitBuf
определяется в
src/ca/client/udpiiu.h как
char xmitBuf [MAX_UDP_SEND];
MAX_UDP_SEND
нашлось в
src/ca/client/caProto.h, где присутствует фрагмент определений
размеров:
/* * 1500 (max of ethernet and 802.{2,3} MTU) - 20(IP) - 8(UDP) * (the MTU of Ethernet is currently independent of its speed varient) */ #define ETHERNET_MAX_UDP ( 1500u - 20u - 8u ) #define MAX_UDP_RECV ( 0xffff + 16u ) /* allow large frames to be received in the future */ #define MAX_UDP_SEND 1024u /* original MAX_UDP */ #define MAX_TCP ( 1024 * 16u ) /* so waveforms fit */ #define MAX_MSG_SIZE ( MAX_TCP ) /* the larger of tcp and udp max */
ETHERNET_MAX_UDP
используется только в libCAS --
src/ca/legacy/pcas/io/bsdSocket/casDGIntfIO.cc в
casDGIntfIO::optimumInBufferSize()
, причём несколько
сомнительно.
...причём сама эта optimumInBufferSize()
лишь определена, но
не используется НИГДЕ.
30.06.2009: если хорошенько подумать -- нет, не нужна. И вот почему:
Это же касается и уже-cxscheduler-based клиентов, типа самого cxsd или cdrclient'а.
И вот там можно использовать обычный cxscheduler, но в
"прерывном" режиме: при входе в режим ожидания вызовет
sl_main_loop()
, а по приходу синхронного ответа --
sl_break()
.
Избавление от SIGIO даст существенное упрощение библиотеки: исчезает необходимость аккуратно продумывать каждый чих и нафаршировывать всё скобками ENTER_CRITICAL()/LEAVE_CRITICAL(). Аналогичный бонус получают клиенты и библиотеки следующих уровней (типа cda). И точно можно будет забыть проблему "а чего это подвисает ndbp на linac под RedHat-7.1?".
02.07.2009: насчет "адаптера" -- можно не выпендриваться с двумя snap-ins'ами (которые пришлось бы дополнительно обязательно уазывать в Makefile'ах), а собирать прямо с ними 2 версии библиотеки: sync и async, и консольные утилиты линковать с sync-версией, а всё остальное -- с async.
04.07.2009: N.B.: а потеряем мы при этом лишь
возможность создавать странные программы, которые сами постоянно что-то
делают, но время от времени хотят проверять "не пришли ль данные по
соединению". Вот там требовалась бы реальная асинхронность по
SIGIO. Но потребности в таких программах не прослеживается, а если
возникнет -- то можно будет слабать экзотический вариант cxscheduler'а,
который бы в sl_main_loop()
выполнял select()
лишь единожды, а потом бы сразу возвращался.
cx_parse_chanref()
(бывшую копией из v2), чтоб она
работала и с именоваными каналами, а не только с нумероваными.
12.11.2013: добавлен параметр
channame_p
, являющийся указателем на место, куда
складировать указатель на часть "имя КАНАЛА" из ссылки.
Он является обязательным, а как раз chan_n
только если
не-NULL, то попробуется преобразовать имя в число; при ошибке уставится
-1
(значит -- надо использовать имя, а иначе можно сразу
номер).
cx_parse_chanref()
? По факту -- никакие номера теперь, в v4,
нафиг не требуются.
Переделанный вариант с channame_p
-- еще какой-то смысл
может иметь, для утилит, работающих на уровне cxlib, но:
PktSize
будет всё-таки
DataSize
, т.е., объём ТОЛЬКО ДАННЫХ, БЕЗ заголовка.
13.09.2009: смысл -- упрощение начального handshake'а, когда стороны еще не знают endianness друг друга. Чтобы не приходилось ни выпендриваться с чтением/не-чтением поля "размер", ни менять параметры (обоим) и endianness fdiolib-соединения (клиенту) по ходу дела. Благо, в fdiolib такой режим есть еще от рождения.
22.11.2013: кстати, по мере наполнения cx_proto_v4.h описываем всё в doc/cx_proto_v4.html.
CxV4Chunk
. Он похож на старый
v2'шный CxDataFork
, только покороче (БЕЗ _padding_'а), и
поля 3,4 названы индифферентно param1
,
param2
.
CxV4LoginRec
'а добавлено поле
ck
-- ChunK (в реальности, конечно, сервером
игнорируемое). Но клиентской реализацией заполняемое.
24.11.2013: реализовываем фичу "application ping" (keepalives), сделанную в v2 еще три года назад
CXT4_PING
и CXT4_PONG
.
HandleClientRequest()
ProcessFdioEvent()
DoHeartbeatPing()
. Период -- те же 300 секунд.
И тут аналогично, и аналогично сервер клиента не пингует.
24.11.2013: фича "WipeUnconnectedTimed()".
Заодно, кстати, обнаружилась ошибка в v2'шном -- не-сброс
clp->tid=-1
-- похоже, приводившая к глюкам с
не-переподключениями к серверу.
05.07.2023: но "скопировано" было криво -- детальный разбор см. ниже по тексту за сегодня.
25.11.2013: прошерщена cxlib_client.c -- она вполне соответствует в той части, что есть (касательно установления соединения; более там ничего -- ни транспорта, ни юзерского API -- и нету).
25.11.2013: cxsd_fe_cx.c аналогично. Но тут мелкие модификации были --
O_NONBLOCK
на слушающие сокеты.
HandleClientRequest()
.
25.11.2013: за компанию проинспектирована cda_api.c. Там вообще фактически только скелет, так что и говорить не о чем.
Засим считаем задачи раздела выполненными.
05.07.2023: фича "подчищать
свеже-при-connect()
'ченные, но недо-handshake'нувшие
соединения" из v2 скопирована была криво: поскольку в v4, по сравнению с
v2, добавилось состояние CS_OPEN_ENDIANID
, то содержимое
уставляющей WipeUnconnectedTimed()
функции
async_CS_OPEN_CONNECT()
вместо простого v2'шного
приобрело видcp->state = CS_OPEN_ACCESS; cp->clp_tid = sl_enq_tout_after(0, NULL, /*!!!uniq*/ MAX_UNCONNECTED_TIME * 1000000, WipeUnconnectedTimed, lint2ptr(cp2cd(cp)));
-- т.е., с лишним условием "успешно ли прошла отправка", и оное условие было сделано некорректно: таймаут заказывался даже при обломе....ПОДГОТАВЛИВАЕМ ПАКЕТ ДАННЫХ В pkt... if (fdio_send(cp->fhandle, amp;pkt, sizeof(pkt)) < 0) MarkAsClosed(cp, errno); else cp->state = CS_OPEN_ENDIANID; cp->clp_tid = sl_enq_tout_after(cp->uniq, NULL, MAX_UNCONNECTED_TIME * 1000000, WipeUnconnectedTimed, lint2ptr(cp2cd(cp)));
В результате оный таймаут неожиданно прорезывался через 60 секунд после
"полууспешного" коннекта (когда connect()
выполнился, но сервер
затем закрыл соединение после уведомления CXT4_ACCESS_DENIED
).
Проявлялось при очень экзотичном сочетании условий:
localhost:1
; аналогичное через localhost:-8013
проблемы НЕ проявляло (видимо, из-за НЕмгновенного ответа от сервера).
Исправлено путём внесения заказа таймаута в обищий блок с переходом в
CS_OPEN_ENDIANID
.
P.S. ВОЗМОЖНО, что те добавления были сделаны позже 24-11-2013; надо поизучать архивы.
06.07.2023: поизучал -- неа, отправка
CXT4_ENDIANID
была изначально, а уже потом 24-11-2013 просто
добавлен заказ таймаута в конец.
...слегка также напрягает однократный пере-коннект после получения
EPIPE
-- клиент сходу не понимает, что его обломили и пытается
стучаться ещё. И что делать -- "руками" вызывать fdiolib'овское чтение?
Неа, не стоит... (Да, НЕ будет "вызова callback'а с событием
CAR_EACCESS
для cxlib-соединения, чей cd
ещё даже
не вёрнут и не сохранён клиентом" -- т.к. после инициирования
connect()
'а всё же будет возврат -- но всё равно криво.)
06.07.2023@утро, мытьё
посуды: зачем так сложно -- просто в случае ошибки попробовать
прочитать из сокета заголовок и проверить, не
CXT4_ACCESS_DENIED
ли в нём, и если да, то делать
MarkAsClosed()
с соответствующим кодом.
06.07.2023: сделано, прокатило -- проблема решена, теперь с первого же раза понимает, что его послали.
16.10.2014: подготовка:
CX_V4_INET_RESOLVER
= CX_V4_INET_PORT
(=8012).
CX_V4_UNIX_RESOLVER
"/tmp/cxv4-r-socket".
CX_V4_MAX_UDP_DATASIZE
= 1500 - 28 -
sizeof(CxV4Header)
(=1440; 1500 -- Ethernet MTU, 20+8 --
заголовки IP и UDP; идея в том, чтобы обычно укладывалось в один
пакет).
Касается как cx_proto_v4.h (в нём определения), так и cxlib_client.c с cxsd_fe_cx.c
25.12.2014: записки о сделанном и выбранных решениях.
Основное:
Вместо этого делаем операции ЗАПРОСА чения и записи CXC_RQRDC и CXC_RQWRC, непосредственных ответов НЕ предполагающих -- т.е., точное отражение интерфейсов cda и cxsd_driver.
"Подписка" -- CXC_SETMON, отписка -- CXC_DELMON, плюс CXC_CHGMON для возможности менять дельту.
Т.е., никакого различения в cxlib_client и выше не нужно -- любые присланные данные обрабатываются одинаково.
...а если затронутый канал не включен в список мониторируемых, то результат прислан и не будет.
В ответ -- сразу! -- будет прислано 2 chunk'а:
Предполагается, что оное можно -- при надобности -- спросить и отдельным запросом, но практичнее, если оно будет отдаваться сразу, и cxlib складирует эти данные в свой кэш/хранилище, чтобы потом при надобности отдать клиенту сразу, без отсылания запроса.
Именно ЭТИ свойства при изменении будут автоматически отправляться для мониторируемых каналов.
Возможно, эти данные следует разбить на НЕСКОЛЬКО chunk'ов, по тем же границам, что в cxsd и cda: отдельно {r,d}, fresh_age, строки...
CXT4_DATA_MONITOR
тогда, к счастью, было забыто и НЕ
выкинуто. 26.04.2015: заодно
CXT4_DB_SERV
было забыто, а уж оно-то теперь точно лишнее
-- выкидываем.
При добавлении консольного интерефейса он будет так же гоняться вместе со всеми прочими, как и было решено еще 23-04-2009.
Ответный пакет ВСЕГДА отправляется, но, поскольку пакет запроса состоял только
Прочие детали:
При надобности несложно было б изменить формат chunk'ов на "множественный" (как в v2), но глубокого смысла пока не видно -- нынешний подход проще.
CXC_CVT_TO_RPY()
.
CXC_NEWVAL
сразу является RPY-кодом
(поскольку запроса такого не предполагается).
CxV4Chunk
.
param1
и
param2
, являющиеся как бы аналогами privptr1/privptr2:
т.е., служат для идентификации того, к какому каналу относится chunk.
Во всех ответах (и уведомлениях) клиенту присылаются те же числа, что были указаны в запросе.
Возможно, ДВА поля -- перебор, поскольку само соединение уже себя идентифицирует, и нужен еще только один "ключ", под hwr. Но уж пусть будет.
...кстати, CxV4Header.Seq
тоже остаётся: хоть *синхронных* пакетов сейчас считай, что и нету
(кроме резолвинга и консоли), но с мониторируемыми каналами оно будет
слаться, и смысл ровно тот же, что и раньше -- отличение "той"
подписки.
01.03.2015: Вроде неиспользуемый param2 может быть полезен, если захочется один и тот же канал мерить по-разному в рамках одного "вида" измерения. (Да, вопрос -- ЗАЧЕМ :)).
Хотя, похоже, всё ж надо бы добавить еще поле "статус".
CXT4*
, что используются в CxV4Header.Type
, чтоб их
можно было так же конвертировать в клиентские коды E*
через
CXT42CE
.
В cxsd_fe_cx.c заготовка под это уже есть -- функции манипуляции мониторами возвращают 0 при успехе и -CXT4* при ошибке (подход подсмотрен в Linux kernel: там разные методы драйверов вертают -E* при ошибке).
25.12.2014: некоторые генеральные мысли -- как где должна быть организована работа, чтобы всё происходило "как надо", максимально корректно и гибко.
Всё просто:
CxsdHwDoIO()
из
cx_begin_c()
.
cx_end_c()
смотреть, что если был хоть
один такой обновившийся канал, то проходить по списку цикленных и те,
что обновились, отдавать клиенту (заодно сбрасывая флаг обновлённости).
Пожалуй, незачем.
УВЕДОМЛЕНИЕ о результатах асинхронного резолвинга -- по тем же privptr1/privptr2, плюс к ним еще param1/param2, описывающие hwr.
(30.12.2014@обед-в-Амиго-ожидание-еды)
для всех каналов делаем ЕДИНЫЙ, сквозной slotarray -- аналогично
cda_core.c::refs_list[]
. Это:
СКОРЕЕ ВСЕГО не понадобится. Поскольку периодически повторять резолвинг всё же надо; возможно, неразрезолвленные помещать в список (адресуемый по hwr), а уж как дальше с ним быть -- смотрим в разделе про резолвинг.
Индекс в массиве -- аналог hwr'а в cda_d_cx.
12.01.2015: некоторые усовершенствования по выравниванию.
Т.е., удовлетворит требования по выравниванию даже на 128-битной
архитектуре. Например, с пока слабо-портабельным типом float128
(long double
, поддерживаемым Linux@x86_64).
...кстати, даже в Linux@x86 malloc() отдаёт куски, выровненные по 8 байтам.
См. также http://www.viva64.com/en/l/0021/
CXV4_CHUNK_CEIL()
, возвращающая
(size + 15) &~15U
.
CxV4Chunk
изначально был таким, как и
CxV4Header
).
CxV4Chunk
добавлено 4 штуки uint32
"на вырост" -- в первую очередь под запланированное поле "статус
ответа".
Это не очень хорошо для UDP-резолвера, поскольку там при ограничении 1440 на данные каждый байт становится на счету, и тратить 16 байт впустую жалко.
Но там можно сделать проще: чтоб пакеты UDP-резолвинга вместо chunk'ов содержали бы в разделе данных просто набор NUL-terminated-строк. А уж ответы будут слаться обычным образом.
После обеда: вариант с просто строками покатит, только если клиент по ответу будет искать у себя в запросах тоже только по строкам; что, в принципе, можно. Если же хочется в запросе гнать еще param1,param2 -- то не прокатит. Впрочем, можно просто ДРУГОЙ формат chunk'ов для резолвинга использовать. ...хотя лучше всё же всё по строкам :)
Причём не в одном экземпляре, а в двух, хотя и запущенных с linac3. После перезапуска -- всё окей.
Почему такое могло произойти -- неясно. Какой-то race condition? А
какой? В cxsd_fe_cx.c::ServeIORequest()
вроде некий
осмысленный порядок: Strs, RDs, FrAg, RESOLVE (а данные -- НЕ посылаются).
...но тут "всё" должно было произойти несколько ПОЗЖЕ ответа на RESOLVE: когда драйвер запускается, то он СРАЗУ отправляет REMDRV_C_RDS, и только потом получает запросы на чтение текущих значений (и, тем более, только по получению ответа от CAN-девайса может их отдать).
Выглядит так, будто ОДНО конкретное устройство "забыло" прислать свои {r,d}. (Вариант -- что cxlib/cda проигнорировали пакет CXC_RDS, но почему для всех каналов ОДНОГО устройства?)
16.12.2015: для возможности разбирательства в окно "Knob Properties" добавлена строка "{R,D}s"; если проблема еще проявится -- можно будет посмотреть.
30.03.2016: да, проблема проявилась, с живой программой с отображениям "{R,D}s".
Результат: вместо надлежащих ДВУХ пар
1000000.0,0.0 0.026666,0.0
имеется всего ОДНА --
0.026666,0.0
Т.е., отсутствует АППАРАТНАЯ пара (1V=1000000uV) -- как будто калибровка {r,d} от драйвера CANADC40 пришла позже, чем подключился клиент, и почему-то ему не была отправлена. ...как будто калибровки вычитываются только в момент коннекта, а при обновлениии драйвером -- уведомления не отсылаются.
Часом позже: а вот не похоже.
30.03.2016: после обеда: наблюдена корреляция: один такой девайс (cdac20 ring1:12.icd_3m1) одновременно "забыл" прислать и "AUTOUPDATED_TRUSTED" на канал out_cur, так что посинел (из-за оставшегося fresh_age=5.0 -- потому я его и заметил).
Причём та же программа, запущенная позже, уже показала наличие всех нужных сведений. Т.е., всё-таки теряется где-то на участке сервер-cxsd_fe_cx-cxlib-cda.
30.03.2016: вечером: наблюдено, что как будто бы у части каналов phys_count "протухает" со временем: на свежезапущенной программе всё было в порядке, сервер не рестартовался (и контроллеры/блоки не дёргались), но в ручках показывается не[до]конвертированные числа и phys_count'ы стоят неправильные.
31.03.2016: утром: да, за ночь несколько ручек опять так же дурканули. Но, опять -- с группировкой по устройствам (хотя вчера, вроде бы, разок была замечена одиночная глюканувшая, без соседей).
В cda_core.c::refinfo_t
вставлено поле
saved_phys_count
, куда сохраняется значение phys_count
при изменении (и заодно эта информация выводится на stderr), а в
cda_get_ref_dval()
стоит сравнение и при несовпадении тоже
выдача на stderr.
Все программы магнитной системы/коррекции запущены, ждём результата.
21.04.2016: вчера вечером опять вылезло (мне Клющев позвонил аж в 22:57), и, по закону подлости -- в linvac'е, который НЕ был запущен локально с логгингом.
CXC_RDS
никогда не посылает.
CXC_RESOLVE
);
CXSD_HW_CHAN_R_RDSCHG
.
Т.е., на вид -- вроде всегда, когда надо.
И тесты (на vepp4-pult6:2, с запуском v4c4lcanserver ПОСЛЕ cxsd) показывают, что обновляется когда/как положено.
FillPropsOfChan()
список переворачивает, так что аппаратная
становится первой.
cda_dat_p_update_dataset()
С КОНЦА.
Следовательно, если бы просто испортилось значение поля
phys_count
(на что нацелена проверка с
saved_phys_count
), став =1, то игнорировался бы как раз
"инженерный" коэффициент, а не аппаратный 1000000.
А происходит иное, следовательно, косяк всё же где-то на линии передачи калибровок, а не порча памяти в cda.
21.04.2016: была проверена еще одна гипотеза -- что глюк случается при загрузке режимов, когда большой поток данных и CANGW призатыкаются. Тестировалось загрузкой режима в linmag (магнитная система и коррекция линака).
Фиг: синеть-то синело (из-за большого количества пакетов), и 30-й блок стабильно уходил в BusOff, а потери коэффициента 1000000 -- не произошло ни разу.
05.05.2016: продолжение интриги: взбрыкнуло на linvac'е; видимо, позвчера -- когда я рестартовал сервер для введения искусственных каналов Ilim/Ulim для 124'шных в chanvac. Наблюдения:
cda_dat_p_set_phys_rds()
показала,
что ДА, пришла информация с phys_count:=0.
Что свидетельствует -- это косяк не в клиенте, а именно в сервере.
А теперь странности:
Ну оно и немудрено --
ReconnectProc()
вызывается через 1000000us);
Поиск далее показал, что чуть попозже присутствует phys_count:=1 (именно у НЕКОТОРЫХ! каналов).
Bingo -- это и есть ответ!
Итого:
"Проблемные" каналы резолвятся ДО прихода REMDRV_C_RDS
, а
SETMON им заказывается уже ПОСЛЕ прихода, а потому события RDSCHG они и не
ловят!
REMDRV_C_RDS
попадает в узкий таймслот между резолвингом канала
и подпиской на него.
PutRDsChunkReply()
и по SETMON тоже.
Например, при SetMonitor()
==0.
И, возможно, с RDS косяк был не единственным.
cx_rd_cur()
-- тоже в реакции на CAR_RSLV_RESULT; не будет ли
пропущено что-нибудь тут?
В т.ч., это делается прямо ПЕРЕД cx_setmon()
, так что при
выполнении race condition даже сейчас текущее значение придёт ДО правильных
коэффициентов и будет сконвертировано неправильно. Надо
поменять порядок!
Например, FreshAge? А ReturnType? Эта пара влияет на принятие решения о CURVAL/NEWVAL. Единственная надежда -- что эти атрибуты драйвер присылает сразу, при инициализации, а значения потом вернёт обязательно.
01.10.2016: прекрасно, блин, просто прекрасно! Ну да -- FreshAge! Источниковы OUT_CUR синели именно из-за этого -- терялось указание "TRUSTED" (т.е., fresh_age:=0) и cda по истечении 5 секунд их посиневала!!!
Напрашивается, что надо всё-таки поменять парадигму:
свойства возвращать не по CXC_RESOLVE
, а по
CXT4_SETMON
. Вопрос, чем это аукнется -- используется ли
где-то (и МОЖЕТ ли использоваться) работа БЕЗ мониторов?
Кстати, что такое
CXT4_RESOLVE
? 26.07.2016: как что
-- код пакета для UDP-резолвинга (а CXC_RESOLVE -- код chunk'а для адресного
резолвинга.) 01.12.2017: только
CXT4_RESOLVE
переименован в CXT4_SEARCH
.
Ну и отдельно замечание, вымученное долгой маетой:
23.05.2016@утро, по-пути-на-работу, задворками ИПА и мимо мыши, плетущей ДНК: вдогонку о неудачности модели:
Неприятно, конечно, начинять сервер (cxsd_hw? cxsd_fe_cx?) этой мутотой, но зато решение.
Но лучше бы воздержаться: слишком уж клиенты при этом ограничиваются.
int64
, потом деля на 1000000 (оставляя второй 10^6), чтобы
отдавать уже в микроваттах, которые вполне поместились бы в int32.
(Детали потребности -- в разделе о kurrez_cac208_drv за сегодня.)
01.10.2016: в реакцию на CXC_SETMON
добавлен также вызов PutFrAgChunkReply()
-- иначе часть каналов
(конкретно OUT_CUR'ы источниковых ЦАП/АЦП) посиневала, ровно из-за такого же
race condition. ...отсылка кванта туда была добавлена сразу же при
внедрении квантов.
Теперь вроде бы с чудесами неконвертации/посинения должно быть покончено.
HandleClientConnect()
они логгируются
вместе с username@IP:progname. Цель -- для упрощения возможных
разбирательств.
CX_V4_MAX_DATASIZE
было 4M, еще с v2. Но устройства вроде
ADC200ME и ADC812ME, имеющие на борту 4M ОЗУ, могут генерить бОльшие объёмы
данных (каждое их 16-битовое измерение превращается в 32-битовые
микровольты), поэтому в v2 лимит увеличился до 16M еще 29-05-2014.
Пора и здесь увеличивать, а то попытка взять максимум от ADC200ME
оборачивается обрывом соединения с диагностикой "Received packet is too big"
(причём это со стороны клиента, т.к. на отправку cxsd_fe_cx ничего не
лимитирует: на размер одной посылки нет лимита в fdiolib'е, а на весь буфер
никто fdio_set_maxsbuf()
не вызывает -- хотя надо бы).
Сделано -- 16M.
CXT4_END_OF_CYCLE
отправлялся бы еще как-нибудь "идентификатор
цикла"?
Например, передавать в var1
"номер" (ID) цикла, а в
var2
его период в микросекундах.
CXT4_RESOLVE
переименован в CXT4_SEARCH
.
CXC_SEARCH
, на который и переведён (с CXC_RESOLVE
)
весь UDP-резолвинг.
Не то чтобы прям сильно надо (в закрытой СУ-то), но чисто по архитектурно-идеологическим соображениям -- не помешало бы.
Надо будет тогда менять список параметров в cxlib'овском API?
12.01.2018: анализируем ситуацию.
cx_seeker()
УЖЕ передаются те же argv0+username, что и в
cx_open()
, и они так же складируются в v4conn_t
--
общей create_a_cd()
.
cx_begin()
для CT_SRCH-объектов автоматически вставлять
"нулевой" chunk с кодом CXT4_LOGIN
.
К сожалению, особых плюсов у этого варианта нет, а минусы есть:
Т.е., де-факто чтоб был "диалог" (нечто вроде handshake) вместо просто broadcast'ного запроса "эй, кто знает?".
А такой диалог откровенно противоречит самой сути подхода.
В общем, всё это -- так, скорее пустые мысли (вызванные, очевидно, грустным интеловским косяком с кэшем+out-of-order-execution (Meltdown+Spectre)), никакого практического смысла не имеющие.
Насколько сейчас форматы совпадают/различаются?
26.01.2018: как минимум, код пакета в обоих случаях
одинаковый -- CXT_SEARCH
, что плохо. Надо бы ответный с другим
кодом.
27.01.2018: да, вводим отдельный код --
CXT4_SEARCH_RESULT
=14*0x01010101
ServeGuruRequest()
: добавлена
замена CXT4_SEARCH
на CXT4_SEARCH_RESULT
. Причём
без учёта endian-нейтральности кодов -- просто явно делается
h2l_u32()
либо h2b_u32()
.
HandleSrchReply()
-- сравнение
теперь с ним.
Теперь проверять.
30.01.2018: да, проверил, работает (ну еще бы -- изменения-то тривиальные).
CX_V4_MAX_UDP_DATASIZE
-- базируется на
обычном пакете, а не на jumbo frame?
Вывод: пока оставим всё как есть -- отталкиваемся от 1500 байт.
Чуть подробнее -- суть проблемы:
Поштучно же каналы "дохнуть" пока что не могут -- единожды загруженный сервер так и продолжает сохранять неизменный набор каналов, поскольку динамического изменения конфигурации пока нету, а только при рестарте (сопровождающимс закрытием соединения).
28.10.2019@утро ~9:40, дорога на работу, около ИЦиГ, под аркой: ну и как это делать?
Очевидно -- надо в таких случаях присылать специальное по-канальное сообщение "забудь" (CXC_FORGET, 'Fgt'?).
ЗЫ: осознал и обдумал это всё (начиная с заголовка раздела) за считанные секунды, а на записывание ушло изрядное время.
29.10.2019: отдельный аспект -- как на стороне клиента
поступать с каналами, у которых УКАЗАНО имя сервера (gateway'я)? Т.е.,
которые RSLV_TYPE_NAME
.
Клиент-то должен сразу попробовать выполнить повторный резолвинг
(соединение-то уже есть!), но оный резолвинг тут же и обломится. А
cda_d_cx.c::ProcessCxlibEvent()
при этом переведёт
канал в состояние RSLV_STATE_ABSENT
, которое так и останется до
следующего обрыва соединения.
Так что надо будет предусмотреть какой-то механизм для таких случаев:
Но это некрасиво -- рулетка, да и попахивает EPICS'ом.
Вот второй вариант мне нравится намного больше -- никакого слепого
тыканья, всё осмысленно в нужный момент (кстати, подобный положительный опыт
уже был раньше -- при переводе механизма leds с периодического опроса на
ловлю события CDA_CTX_R_SRVSTAT
).
29.10.2019: кстати, к вопросу о первоначальной
-- проблеме "как сообщать клиенту": а ведь можно просто
сообщением "resolving negative" -- т.е.,
CXC_CVT_TO_RPY(CXC_RESOLVE)
"без данных", что приведёт к
CAR_RSLV_RESULT
с hwid=-1, на который
cda_d_cx.c::ProcessCxlibEvent()
и отреагирует "канал
не найден".
Тогда всё становится МЕГА-просто -- фича "забыть ОДИН канал" будет работать автоматом.
29.10.2019@вечер-пультовая: увы, всё чуть неприятнее --
ProcessCxlibEvent()
реагирует на CAR_RSLV_RESULT
только в случае, если канал находится в состоянии "жду резолвинга"
(rslv_state==RSLV_STATE_SERVER). Иначе он только выдаёт диагностическое
сообщение на stderr.
Так что -- придётся переделывать диаграмму работы.
Кстати, это всё равно надо бы сделать, т.к. иначе возможен такой сценарий с race condition:
И вместо того, чтобы перевести соответствующий dataref обратно в режим
RSLV_STATE_UNKNOWN
(и повторить резолвинг!), этот dataref так и
останется в RSLV_STATE_ABSENT
до тех пор, пока соединение с
сервером S не оборвётся.
А с исправленной диаграммой этот косяк исчезнет.
30.10.2019: делаем наипростейшие и ни к чему не обязывающие шаги -- поддержку в протоколе и в cxlib.
...и тут некоторая засада: как назвать?
Вот "MUSTER" и выбран.
Собственно шаги:
CXT4_MUSTER
=32*0x01010101.
Т.е., он endian-independent, что потенциально позволит его использовать даже с недо-приконнекченными клиентами. Хотя правильным будет НЕ слать его клиентам, у которых state<CS_USABLE.
CAR_MUSTER
.
CXT4_MUSTER
в событие CAR_MUSTER
.
На этом пока всё и проверять пока что нечего и не на чем.
Если идти дальше, то надо
ProcessCxlibEvent()
с
резолвингом,
...но тут возникает любопытное соображение: КАК реагировать на такое событие?
RSLV_STATE_ABSENT
.
RSLV_TYPE_HWID
.
Может, сделать "вариации" сообщения MUSTER? Чтобы был также
параметр "level", передаваемый в CxV4Header.var2
а далее в
cx_notifier_t().info
, означающий, что именно надо сделать.
Например, 0 -- пере-резолвить всё, 1 -- только неразрезолвленные (или
наоборот).
Что будет с общающейся по сети программой, если её хост неожиданно поменяет IP сетевого интерфейса? А то вот случилось такое в пультовой, а я даже и не представляю, что в подобных случаях происходит...
24.04.2020: модифицируем диаграмму работы каналов RSLV_TYPE_GLBL -- чтобы устранить race condition, описанный 29-10-2019.
cda_dat_p_get_server()
в отдельные функции, чтобы не страдать
дублированием кода (там изрядные "шаманские заклинания") --
get_DATA_IO_server()
и get_RESOLVER_server()
.
ProcessCxlibEvent()
в отношении
RSLV_TYPE_GLBL-каналов -- туда был скопирован кусок из
FailureProc()
, переселяющий канал в RESOLVER (по хорошему --
унести бы этот кусочек в отдельную функцию и вызывать её из обоих мест).
28.04.2020: сделать-то сделал, но ведь так и не протестировал! А надо бы...
24.04.2020@душ, вечер, заполночь: если хорошенько подумать, то становится очевидно, что все эти махинации с MUSTER и поштучной инвалидацией каналлв -- в принципе не способны дать гарантии надёжной работы: в этой схеме присутствует race condition.
28.04.2020: а всё это потому, что у нас ПРЯМАЯ адресация по gcid'ам, вместо предварительной регистрации и дальнейшей адресации по (не)прозрачному handle -- в том варианте проблема "смены hwid'ов" бы не стояла, т.к. оная смена происходила бы на стороне сервера, "атомарно" (без обменов по сети и race condition'ов), а клиент продолжал бы работать с ранее полученным handle.
Но приличия ради должен быть хотя бы 1 байт.
CXSD_DB_CPOINT_DIFF_MASK
с
30-го бита на 23-й), то cxsd_fe_cx мог бы клиентам сбагривать "композитные" hwid'ы,
состоящие из дуплетов {hwid,DB_ID}, а при получении запросов проверять
старший байт на соответствие.
...ну да, это сценарий малореалистичный, но всё же возможный.
И даже использование 32-битного кода "инкарнации" именно ГАРАНТИИ -- не даст.
CxV4Header.Seq
: можно в момент отправки клиенту
уведомления о необходимости пере-резолвинга запоминать номер последнего
присланного клиентского пакета и......нет -- чё-то тут не то. Да, это Seq в принципе исполняет роль таймера, но оно назначается КЛИЕНТОМ, а нам нужно отмерять от момента, устанавливаемого СЕРВЕРОМ.
v4clnt_t
флажок "клиент ещё не подтвердил получение уведомления
о модификации БД, так что игнорировать все его дальнейшие запросы на
чтение/запись/мониторинг".
Но и этот подход фигов -- см. анализ в следующем пункте.
Можно, конечно, отправлять в ответ код как бы ошибки "переспроси после пере-резолвинга", но это накладывает шибко высокие требования на cxlib+cda_d_cx -- там придётся эти запросы как-то помнить; да и вообще, см. п.2.
Для чего придётся перейти от нынешней простой модели "отправил и забыл" к "хранить отправленный запрос у себя, пока не придёт подтверждение".
И это повторение должно будет переплетаться с повторным резолвингом, что вместе создаст довольно неприятную диаграмму работы.
Хотя тут есть радикальный вопрос: а НАДО ЛИ повторять запросы, или их действительно надо отбрасывать? Наверное, так:
И для них уже есть "процедура повтора" -- при переходе канала из состояния "хбз" в OPERATING.
ЗАМЕЧАНИЕ: очевидно, что при такой "модификации БД", затрагивающей hwid'ы, должны автоматически сниматься все мониторы и блокировки -- прямо frontend'ом. Чтобы cda_d_cx просто повторял бы всю процедуру наложения -- как при обычном реконнекте.
...впрочем, эта часть работы сейчас делается в cda_core -- слежение за наличием калибровок и отправка при их получении. Вопрос лишь в том, что надо наладить соответствующее уведомление cda_core из cda_d_cx.
ВЫВОД: внедрение всех этих механизмов обойдётся очень дорого, ибо они сложны (и заодно эта сложность убъёт простоту и элегантности реализации), а "нужно" оно -- для весьма экзотических и даже скорее теоретических сценариев. Очевидно, что игра не стоит свеч -- не будем мы этого всего делать.
Пусть при смене БД по-прежнему делается закрытие соединения и реконнект -- оно и надёжно (точно нет никаких "запросов «в полёте» в момент неопределённости), и просто в реализации.
25.04.2020@(после записи всей этой
простыни с анализом): если делать, чтобы живой сервер -- без рестарта
-- мог бы перечитывать БД, то можно перед закрытием соединения из cxsd_fe_cx
посылать специальное сообщение, чтоб cda_d_cx бы 1) выставлял флаг
is_suffering
(для предотвращения выдачи бессмысленного
сообщения на stderr); 2) выполнял бы reconnect СРАЗУ ЖЕ, а не через 2
секунды.
Т.е.,
P.S. Обдумана большая часть написанного ("фигова эта модель, лучше пусть будут реконнекты!") буквально за секунды или минуты, а записано всё уже 25.04.2020 -- часа два вбивал.
28.04.2020: а возможно и не так всё плохо! Если сменить, как сегодня обдумано, модель обменов с "прямой адресации по hwid'ам" на "сначала резолвим/регистрируем канал, а потом работаем с handle'ом зарегистрированного", то проблема всех этих race condition'ов исчезает.
Всё получится просто само собой, отпадёт необходимость во всяких инкарнациях/таймерах/..., поскольку не будет никаких "периодов неопределённости".
А вот MUSTER -- возможно, останется, хотя и станет несложным.
Ну а вышенаписанная простыня -- да, останется теоретическим исследованием, только теперь уже по причине исчезновения потребности в столь экзотических извратах, за счёт смены концепции на более элегантную.
В любом случае, это позволяло бы cxsd_fe_cx корректно подчищать все ресурсы, как-либо аллокированные/затребованные клиентом (чего сейчас сделать невозможно -- например, блокировки, которые нигде у cxsd_fe_cx запомнены быть не могут).
Т.е. -- работа как с файлами.
И для производства данного существенного изменения в протоколе и создаётся этот раздел. Для начала тут будут записи мыслей/проектов (начались они, кстати, в предыдущем разделе -- см. за 28-04-2020 там и в разделе по локингу каналов).
30.04.2020: немного вдогонку к модели "работаем не по gcid, а через handle":
Так вот, чтобы не делать по malloc()'у на каждый
канал-монитор, можно завести общий пул строк -- аналогично
strbuf
в CxsdDbInfo_t
, и в "мониторах" записывать
не char*
, а оффсеты в этом общем пуле.
Понятно, что тут не будет такого однозначного выигрыша, как в CxsdDb -- просто потому, что дублей строк будет крайне мало (но поиск по пулу делать надо -- для случаев повторной регистрации тех же имён). Но всё же это будет существенное сокращение фрагментации памяти -- один крупный буфер вместо десятков-сотен мелких. И да, тут надо растить его аналогично CxsdDb'шному -- по килобайту; так у большинства клиентов пул будет вмещаться в первый же аллокированный килобайт.
ЗАМЕЧАНИЕ: но всё-таки эта идея на очень дальнюю перспективу -- когда появится хоть какая-то возможность динамически менять БД.
В результате чего cda_d_cx может по ошибке принять на счёт нового канала никак его не касающиеся сообщения для старого.
Типа обсуждение:
Учитывая, что назначение его -- защита от "сообщений в полёте" на время разрегистрации/новой_регистрации, тут также не нужен особо длинный счётчик, и уж 32-битного гарантированно хватит на все случаи (и это НЕ вероятность, а именно ПОЛНАЯ ГАРАНТИЯ).
hwrinfo_t
.
cda_d_cx_privrec_t
'а -- пофиг.
param2
. На стороне сервера он уже корректно
сохраняется/возвращается, просто от cda_d_cx всегда передаётся 0.
(Опять записывал намного дольше, чем придумывал.)
03.05.2020@утро-дрёма-перед-просыпанием: а можно в принципе прямо на нынешнем протоколе всё сделать, причём даже не затрагивая клиентскую сторону (cxlib+cda_d_cx): чтоб cxsd_fe_cx возвращал бы при резолвинге не gcid, а handle монитора, и чтоб операции PEEK/RQRD/RQWR и прочие (SETMON/DELMON?) воспринимались бы именно как указывающие handle, а не gcid/cpid.
Правда, это будет хак/заплатка/махарайка, так что глубокого смысла так делать нет -- уж лучше потратить чуть больше времени и сделать полноценную реализацию "через handle".
Кстати, у протокола "через handle" есть ещё один бонус: можно поддерживать обращение к всяким разным типам ресурсов, формально не являющихся "каналами" в терминологии сервера -- например, адресоваться к диапазонам напрямую. Просто cxsd_fe_cx при резолвинге будет определять что это за тип ресурса, записывать это в мониторе, а потом при клиентских запросах выполнять соответствующие операции для чтения/записи.
18.05.2020@ходьба пешком по квартире, ~5ка ~17:00: и даже больше -- можно будет адресоваться из клиентов к ресурсам, которые вообще НИКАК НЕ относятся к аппаратным каналам, например, к "другим клиентам"; пусть cxsd_fe_cx сможет распознавать такие "имена каналов", типа-резолвить их и помечать у себя в "типе монитора", что с ними надо обращаться иначе. Вопрос только в том, как такие псевдоканалы "мониторировать" -- например, ловить дисконнекты клиентов..
19.05.2020: работа будет состоять из 4 частей:
Кстати, немножко насчёт идеологии/методологии -- о векторности/единственности:
CxsdHwDoIO()
получает сразу пачку, и
ReturnDataSet()
возвращает тоже пачку.
20.05.2020: приступаем к кодингу.
CXC_CH_nnn
(CHannel),
значения -- CXC_REQ_CMD('H',,)
('H' - Handle).
CXC_NT_nnn
(NoTification), и со значениями
CXC_RPY_CMD('N',,)
.
В том числе в эту группу переходят CURVAL и NEWVAL.
CxV4_CH_nnn_Chunk
и
CxV4_NT_nnn_Chunk
.
Тут также унификация по именам: например, код CH_OPEN генерится вызовом
cx_ch_open()
.
CXC_CH_OPEN
, формат
CxV4_CH_OPEN_Chunk
(21.05.2020: для
_CH_ -- уже не нужно), вызов cx_ch_open()
.
Нет ли тут потенциального race condition в моменты удаления одних каналов и добавления других? Не нужно ли добавить (param1,param2) во ВСЕ cxlib'овские вызовы?
Надо взять паузу на подумать.
@вечер: пока делаем так, а если вдруг что вылезет -- переделаем.
22.05.2020@утро, завтрак, банан: вроде есть понимание:
param2
в
качестве проверочного "ключа", постоянно увеличивающегося на 1, как было
придумано 30-04-2020.
...но это халтурновато -- вариант (1) корректнее.
21.05.2020: продолжаем -- уже с cxlib'ом.
Но уже сейчас видно, что в cxlib.c и cxsd_fe_cx.c можно сделать поддержку прямо в существующих -- она вроде не помешает работе старого.
А для cda_d_cx всё сделаем в cda_d_cx41.c, который будет доступен через протокол-схему "cx41::", так что можно будет сразу и проверять работу. А когда настанет время полного перехода -- тогда файл переименуем и уберём "41" из имени метрики и схемы.
...выбрано "chnd
" -- это можно расшифровывать и
как "Channel HaNDle", и как "CHaNnel Descriptor".
CxV4_CH_nnn_Chunk
, или можно обойтись имеющимися
rs1/rs2/rs3/rs4?
rs1
.
Переименовать его в chnd
не удастся, поскольку оно уже
используется в CXT4_LOGIN
для передачи PID (а остальные rs2/rs3
-- для PPID и UID; rs4 -- SRV_N в UDP-ответах CXC_SEARCH
).
По итогам дня: да, всё помещается прямо в
CxV4Chunk
, так что отдельные
CxV4_CH_nnn_Chunk
не требуются (и даже заготовками не
делались).
CxV4_NT_AVALUE_Chunk
(унифицированная для
CURVAL и NEWVAL) и CxV4_NT_STRS_Chunk
-- с ними всё просто и
ясно, поскольку в них дофига специфичных данных.
Убраны поля hwid
(формально вместо них теперь будет
rs1
), которые реально и раньше не использовались -- поскольку
cda_d_cx берёт значение hwid
только из ответа на RESOLVE, а в
остальных уведомлениях его оно не интересует.
С прочими нотификациями пока не так ясно.
cx_ch_nnn()
.
В них весьма много одинакового кода -- ритуалы роста буфера пакета и NumChunks++. Но вытаскивать их в отдельную сервисную процедуру пока неохота.
CXC_CH_PEEK
и cx_ch_peek()
:
пока-то они сделаны, но вообще теперь, при переходе на handle'ы, СЕРВЕР
должен при "открывании" канала присылать клиенту текущее значение сам, без
лишних пинков.
22.05.2020@утро, мытьё посуды: пара мыслей насчёт работы с новым вариантом протокола -- в отношении динамической смены конфига сервера:
fail_on_err
: нужно, чтобы он указывался =0 для
RSLV_TYPE_NAME-каналов (даже если не разрезолвилось, всё равно канал точно
ссылается на этот сервер и никуда из него не денется) и =1 для
RSLV_TYPE_GLBL (если вдруг канала нету, то надо повторять UDP-поиск и
находить другого сервера-владельца).
22.05.2020: продолжаем.
Пора обрабатывать новые коды уведомлений в
async_CXT4_DATA_IO()
:
...для чего параллельно понадобилась определённость с форматами в
протоколе -- где нужны отдельные CxV4_NT_nnn_Chunk
, а где
достаточно обычного CxV4Chunk
:
CxV4_NT_FRH_AGE_Chunk
-- в нём всё
определённо (3 штуки uint32).
25.05.2020: удалён -- эти "3 штуки uint32" влезли в rs1,rs2,rs3.
По данным-то у нас ограничений на unit-size почти нет (там динамический
размер пакета), но вот при передаче QUANT и RANGE используются 8-байтные
поля -- т.е., максимум int64 и float64. ...и, кстати,
CxAnyVal_t
-- тоже 8 байт.
double
(float64).
А для float128 это как-то неправильно.
Можно в chunk'е CXC_NT_RDS также передавать "тип" -- сейчас
фиксированно CXDTYPE_DOUBLE
и просто игнорировать, а в будущем
это позволит расшириться.
CxV4QuantChunk
(и потом в
CxV4RangeChunk
) так было сделано просто из лени.
CxV4Chunk
, а вариабельные данные пойдут в его секции
data[]
.
У-ф-ф-ф, на душе полегчало! А то вчера никак не мог приступить к работе по кодингу -- прям с души воротило, будто держало что-то ("не надо делать то, к чему душа не лежит -- это обычно означает какую-то проблему/косяк, и надо сначала хорошенько обдумать и постараться найти НРАВЯЩЕЕСЯ решение").
24.05.2020: немножко в сторону: размышления о том, как обозначать такие "большие" типы в devlist'ах и в @-префиксах типов.
Т.е., нужно вроде как ещё 2 символа.
Но можно поступить хитрее:
Префикс "длинности" служит для удвоения размера (хотя да, "li"="q", а "lt" даст несуществующий TEXT16).
Тем более, что префиксы (без)знаковости '+'/'-' у нас уже есть, они и парсятся, и печатаются.
25.05.2020@пешая пробежка по квартире: пришло понимание, как стоит сделать в cxsd_fe_cx.c с мониторами:
Различать их по флажку -- старый/новый, и уведомления -- из
MonEvproc()
и IfMonModifiedPutIt()
-- для старых
слать старыми командами, а для новых -- новыми _NT_nnn.
in_use
, и сделать MON_TYPE_UNUSED
=0,
MON_TYPE_GCID
=1, MON_TYPE_CHAN
=2, ...
Put_NT_nnn
_Chunk()
-- унифицированно аналогично
прочему от 20-05-2020.
26.05.2020: за прошедшие дни допилены части (I) и
(III) -- определения в cx_proto_v4.h и обработка ответов в
async_CXT4_DATA_IO()
. Типа краткого резюме:
CxV4_NT_AVALUE_Chunk
и
CxV4_NT_STRS_Chunk
. Все остальные -- влезли в стандартные
поля (да-да, и запросы, и ответы/уведомления).
CXC_NT_QUANT
и CXC_NT_RANGE
потенциально есть поддержка "больших" типов (более 64bit/8byte), но там
стоит проверка, что размер пришедшего типа не должен быть больше, чем размер
соответствующего поля в уведомлении.
А поля эти все CxAnyVal_t
, так что СЕЙЧАС -- именно 8 байт.
CXC_NT_RDS
стоит проверка, что присланное в
rs2
значение dtype==CXDTYPE_DOUBLE.
Это тоже заложено в расчёте на потенциальную поддержку "больших" типов (хотя вряд ли когда-нибудь понадобится -- если они когда и появятся, то вряд ли будут пользоваться механизмом {R,D}-conversion).
CAR_
-события
и пользуются старыми же info-структурами.
Т.е., тут осталась совместимость.
(В качестве hwid
передают chnd
, присылаемый в
rs1
.)
CAR_CH_OPEN_RESULT
и
cx_ch_open_info_t
.
27.05.2020: приступаем к реализации в cxsd_fe_cx.c.
MON_TYPE_UNS
=0, MON_TYPE_OLD
=1,
MON_TYPE_CHN
=2 и менеджмент MonSlot'ов переведён на них.
put_nt_chunk_t
, аналог старого
put_reply_chunk_t
.
Существенное отличие: вместо четвёрки параметров "описания того, что и откуда отправлять"
он принимает один-единственныйint param1, int param2, cxsd_cpntid_t cpid, uint32 Seq,
moninfo_t *mp
-- предполагается, что в мониторе вся требуемая информация уже есть.
SendNotification()
, аналог старой SendAReply()
.
Пока пустая.
Put_NT_AVALUE_Chunk()
. Пока только черновик, в виде копии
PutDataChunkReply()
, даже параметры пока старого набора.
Put_NT_nnn_Chunk()
, опять
же унифицированно.
26.06.2020: возвращаемся к этой работе (да, спустя почти месяц, гы-гы!).
SendNotification()
наполнена -- практически копия
SendAReply()
с изменением только в вызове
chunk_maker
'а.
Put_NT_AVALUE_Chunk()
: переделана на заполнение
/CxV4_NT_AVALUE_Chunk
.
Put_NT_STRS_Chunk()
-- вторая и последняя со своим
собственным форматом Chunk'а,
Put_NT_RDS_Chunk()
-- тут добавилась отправка dtype'а,
Put_NT_FRH_AGE_Chunk()
.
Put_NT_QUANT_Chunk()
и
Put_NT_RANGE_Chunk()
-- эти "нового стиля": складывают данные просто
в data[], последовательно, плюс передют dtype данных в rs2
.
Put_NT_LOCKSTAT_Chunk()
-- самая простая из всех.
rs1
отсылается 0 вместо
chnd.
27.06.2020: продолжаем.
CHNEvproc()
-- аналог MonEvproc()
'а
для MON_TYPE_CHN-мониторов. Отличается использованием
SendNotification()
.
RlsMonSlot()
вызов CxsdHwDelChanEvproc()
теперь условный: снимаются РАЗНЫЕ evproc'ы в зависимости от типа монитора.
Смысл -- поскольку теперь номера/идентификаторы мониторов являются из внешними адресами, то нужно дополнительно защищаться от потенциальных багов, так что элемент [0] пусть будет зарезервированным.
28.06.2020: далее -- переходим к "мясу", собственно обработке CHN-запросов.
Тут всё хитрО и неоднозначно, поскольку именно здесь происходит смена состояния -- с "нет канала" на "есть канал", и именно с поведением OPEN'а связано потенциальное наличие/отсутствие race condition'а.
ServeIORequest()
показывает, что нынешняя модель с раздельными CXC_RESOLVE и CXC_SETMON --
кривовата: в них в ОБОИХ есть отправка RDs, FrAg, Quant, Range (плюс в
первом ещё и Strs (хбз, почему во втором нету)).
Поэтому программы получают уведомления об этих свойствах ДВАЖДЫ (я разок подгонялся из-за этого, при отладке какого-то функционала cdaclient'ом).
Т.е., переход на выделенный OPEN -- это однозначно "the right thing", текущая кривизна при этом устранится и всё станет элегантно.
cda_dat_p_set_ready(,1)
.
Там даже грустный комментарий есть:
> /*!!! Note: should put parameters-chunk FIRST, for its data to be already cached in cxlib upon arrival of RESOLVE-rpy chunk */
...подобный механизм обсуждался при обдумывании всей ситуации, которое и привело к осознанию необходимости перехода на модель "сначала ОТКРЫВАЕМ канал" -- 30-04-2020: «вместе с основным "ключом" param1=hwr ещё дополнительный проверочный, являющийся постепенно увеличивающимся числом»; а ещё раньше -- как возможное решение для старого протокола, 24-04-2020: «вместе с запросами всегда присылать "код-номер инкарнации серверовой БД", и чтоб сервер игнорировал запросы, содержащие "не тот" код». И в обоих случаях было осознано, что это снижение вероятности, но НЕ гарантия.
cda_dat_p_set_ready(,1)
.
Естественно, и в cda_d_cx.c состояние "разрезолвлено" тоже придётся разбить на 2 -- "канал есть, получаем на него свойства" и "канал готов к работе".
Первый вариант -- лишь смягчение проблемы (mitigation), но не гарантия.
Так что надо выбирать второй -- он даёт железобетонную гарантию корректности, безо всяких допущений.
CxV4Chunk
и все поля в нём уже заняты.
Связанный вопрос -- как передавать НЕГАТИВНЫЙ ответ ("нет такого
канала"): раньше-то определялось по размеру chunk'а, который при обломе
приходил базовый вместо CxV4CpointPropsChunk
, а теперь это
невозможно.
Варианты:
rs2
-- в нём передаётся
rw
, на который и 1 бита (ОК, байта) достаточно.
rs1
, которое
chnd
: если -1
, то облом.
...но такой вариант не даёт возможности передавать "FOUND_STAGE1": ведь
при нём обязательно нужно СРАЗУ отдавать значение chnd
.
Но это хак: формально нет никаких противопоказаний нулевому значению max_nelems (ну будет всегда пустое значение, ну и что?).
Плюс, с передачей "FOUND_STAGE1" ровно та же проблема, что и в предыдущем варианте.
chnd
во всех ответных Chunk'ах: сделана
mp2chnd()
-- благо, организация/структура данных позволяет:
static inline int mp2chnd(v4clnt_t *cp, moninfo_t *mp) { return mp - cp->monitors_list; }
(Т.е., НЕ понадобилось заводить в moninfo_t
лишнее поле "мой
номер".)
И грустное "0/*!!!chnd*/
" заменено на mp2chnd(cp, mp)
07.07.2020: возвращаемся к этой работе (неделя ушла на разбирательство с Modbus TCP и окучивание радиационных блокировок -- ключевое слово "modbus_tcp_rb").
Ещё "тогда" после размышлений был сделан выбор: решаем проблему путём введения 3 разных кодов ответа.
Вот -- делаем:
CXC_NT_OPEN_NOTFOUND
='N','o','-',
CXC_NT_OPEN_FOUND_STAGE1
='N','o','1' и
CXC_NT_OPEN_FOUND_STAGE2
='N','o','2'.
cx_ch_open_info_t
добавлено поле status
,
...
Начинаем наполнять; из "простого" -- сделана отправка FOUND_STAGE1 и FOUND_STAGE2.
07.07.2020@вечер, засыпая: а зачем так громоздко -- можно же проще!
put_nt_chunk_t
у нас уже есть.
info_int
, но где брать значение?
Можно глядеть на положение в списке. Но это фигово.
А лучше -- прямо в списке рядом с chunk_maker'ом указывать и info_int.
Даже странно, что эта идея не пришла в голову раньше -- там пара длинных цепочек, в RESOLVE и SETMON.
08.07.2020: делаем по вчерашне-вечернему проекту.
Put_NT_OPEN_nnn_Chunk()
.
OPEN_reply_seq[]
, состоящий из дуплетов
(chunk_maker,info_int).
Put_NT_AVALUE_Chunk()
: ей ведь нужно указывать код, CURVAL или
NEWVAL, тоже в info_int.
Так вот -- в цикле смотрится, что если собираемся вызвать конкретно её, то значение определяется по стандартному алгоритму.
Смысл в том, чтобы добиться того же поведения, что ранее: сначала сообщаются все свойства канала и он в клиенте переводится в состояние "готов к работе", а уж затем даётся его текущее значение.
GetChnd()
. В
основном скопировано из GetMonitor()
.
09.07.1010@14:30, чистка картошки: при обдумывании реализации CH_CLOSE стало ясно, что тут есть ЕЩЁ ОДИН race condition (правда, весьма явный и способный проявиться лишь при очень экзотических и малореалистичных условиях).
Сценарий такой:
cda_del_chan()
этому каналу (который хоть ещё не достиг
готовности, но для клиента-то ref уже есть).
В результате:
cx_ch_close()
? Ведь
chnd-то ещё нету.
Очевидно, надо передавать ТРОИЦУ -- (chnd,param1,param2), чтобы cxsd_fe_cx мог найти "жертву" по (param1,param2), если chnd<0.
Следствия: а) менять список параметров cx_ch_close()
;
б) Put_NT_OPEN_nnn_Chunk()
придётся где-то добывать имя
канала из запроса, чтобы слать его в ответном chunk'е (в будущем-то оно
предполагается к хранению в мониторе, а сейчас -- нету!).
И вот что выйдет:
Тут тоже просматривается очевидное решение: аналогично UDP-резолвингу, чтоб сервер присылал бы в ответе ещё и ИМЯ канала. Тогда клиент сможет проверять, что ответ он получил на "то" имя.
(Да, тут тоже остаётся мелкий race condition в случае, если клиент создайт/удаляет и потом снова создаёт канал с одним именем -- тогда (в промежутке между удалением сервером первой инкарнации и созданием в нём второй) будет "окно", когда в клиенте канал есть и типа может быть, например, писуем, но в сервере-то канала как бы нет... ...а впрочем, если считать, что запросы идут по сокету всё-таки последовательно, то команды клиент->сервер на запись придут уже ПОСЛЕ прихода второй команды CH_OPEN; так что вследствие этой последовательности "окна" не будет.)
Чуть позже: а ещё очевидно, что имя надо будет отправлять не только в ответе FOUND_STAGE1, но также и в FOUND_STAGE2, а также в NOTFOUND -- всё для того же, чтоб cda_d_cx не перепутал, на что именно ему пришёл ответ.
09.07.2020: ведём работы по "выпрямлению" ситуации.
cx_ch_close()
добавлены параметры
param1
,param2
и их отправка с запросом.
09.07.2020:
12.07.2020: пора заниматься клиентской стороной.
Он включен в сборку и зарегистрирован в plugmgr'е.
А ведь понадобится COND_NEVER -- на СЕРВЕРНОЙ стороне!
CX_MON_COND_NEVER
=0. Это-то самое простое.
REQ_ALL_MONITORS
=1.
Придётся, видимо, в дополнение к последнему делать "&&cond!=NEVER".
CXC_CH_RQRD
и чтение должно
запроситься, и данные должны клиенту отправиться. Как?
Напрашивается сделать булевский флажок, который бы взводился при явном запросе чтения, а при обновлении чтоб проверялся и если взведён -- то сбрасывался бы, но данные б отправлялись.
13.07.2020: в cxsd_fe_cx.c сделана поддержка
CX_MON_COND_NEVER
по вчерашнему "проекту":
REQ_ALL_MONITORS
добавлено
"&&cond!=CX_MON_COND_NEVER
".
mode
-- функционирующее
аналогично cda_d_cx'ному hwrinfo_t.mode; в нём пока есть 2 битика:
MONMODE_RQ_LOCK
- им было заменено поле
rq_lock
(ныне убранное).
MONMODE_SEND_UPDATE
- взводится у COND_NEVER-мониторов
по явному CXC_CH_RQRD
, а в CHNEvproc()
смотрится,
что если этот бит взведён, от обновление клиенту отсылается, после чего бит
сбрасывается.
GetChnd()
добавлен код
CX_MON_COND_NEVER
в список годных.
Более вроде ничего и не нужно -- всё в основном было сделано в 2 первых пунктах.
IfMonModifiedPutIt()
-- этот-то
итератор вызывается для ВСЕХ МОНИТОРОВ, а не только для тех, чьи gcid'ы
попадают в per_cycle_monitors[]
.
В результате для якобы-немониторируемых каналов данные тоже слались.
Хотя нет... Не должно было -- ведь в CHNEvproc()
отдельная проверка на MON_COND_NEVER, так что оно НЕ должно было взводить
mp->modified
...
Чуть позже: да, разобрался -- всё верно, ТУТ всё было сделано корректно.
А косяк был в cda_d_cx.c -- некорректно определялась
MODE_MON_TYPE_mask
.
Дальше подумываем опять о клиентской стороне. И тут поджидает засада:
тип мониторинга должен указываться cda_d_cx'ом --
do_subscribe()
, но константы CX_MON_COND_*
есть
только на уровне ПРОТОКОЛА, а клиенту они неизвестны (и не должны быть!).
14.07.2020: продолжаем.
RSLV_TYPE_HWID
и всё с ним
связанное.
...при этом из 3 вызовов do_subscribe()
остался только 1 --
в ProcessCxlibEvent()
, а в cda_new_chan()
и
SuccessProc()
ушли.
RSLV_STATE_FOUND1
.
ProcessCxlibEvent()
в обработку всех
per-channel-событий (кроме OPEN (пока всё равно отсутствующего)) добавлено
условие
"&&hi->rslv_state >= RSLV_STATE_FOUND1
".
16.07.2020: далее...
CAR_CH_OPEN_RESULT
.
Надо бы как-то получать от сервера истинный hwid, но на него места в
CxV4Chunk
не осталось...
Проверяем.
hi=AccessHwrSlot(...)
. Позорище :)
Тут было сложнее и дурнее: оказалось, что в ServeIORequest()
в обработке CXC_CH_OPEN
вместо надлежащих кодов
CXC_NT_NEWVAL
/CXC_NT_CURVAL
слались
старые CXC_NEWVAL
/CXC_CURVAL
-- видимо, просто
результат копирования.
Дальше надо проверять на живом железе -- в т.ч. на осциллограммах.
17.07.2020: ну и последний пока штрих -- избавление от старого наследия:
do_subscribe()
.
hwrinfo_t.hwid
переименовано в chnd
.
(После обеда) а, нет -- пока НЕ последний: ещё ж осталась проблема с типом мониторирования.
CX_MON_COND_*
публичными (а не только в протоколе). Но нет -- не стоит.
CX_UPD_COND_*
, полностью миррорящих протокольные (и даже с
совпадающими значениями, хотя это и неважно).
cx_ch_open()
параметр on_update
заменён на
upd_cond
, могущий принимать одно из тех значений, ...
mode2upd_cond[]
, используемая как
mode2upd_cond[hi->mode & MODE_MON_TYPE_mask]
во всех 3 точках.
Осталось, конечно, ещё проверить работу всех 3 режимов, включая NEVER/NOMONITOR.
Но в остальном -- вот теперь вроде всё.
Вечер, 22:45: а, не -- ещё осталось придумать, как
передавать клиенту реальный gcid (НЕ cpid -- оный не нужен, т.к. их всё
равно нельзя указывать в качестве номера канала --
CxsdDbResolveName()
позволяет только в пределах
db->numchans
.).
18.07.2020: много думал о том, как же всё-таки передавать клиенту gcid.
Чтобы все свойства могли прилетать отдельно -- тогда в будущем, при случае обновления конфига сервера "на лету" они могли бы присылаться как все прочие (строки, диапазоны, ...).
Надо бы глянуть, как сейчас сделана передача этих rw/dtype/max_nelems/... от dat-плагинов к cda_core -- есть ли дальше передача клиенту отдельным событием, или клиент просто может читать при надобности.
19.07.2020: посмотрел -- нет, никакого события
cda_dat_p_set_hwinfo()
не генерит. Другое дело, что следующей
же строчкой за ним везде стоит вызов
cda_dat_p_report_rslvstat()
, а он уже генерит событие,
RSLVSTAT.
...хотя в любом случае в раздельном варианте есть некрасивость: отсутствие атомарности -- канал уже как бы открыт, но свойства его ещё неизвестны.
19.07.2020: делаем.
CxV4_NT_OPEN_Chunk
; в нём есть и
hwid
, и rw,dtype,max_nelems (да, в общие rs2...rs4 их решено не
запихивать), плюс ещё rsrvd1
...rsrvd4
на будущее.
Put_NT_OPEN_nnn_Chunk()
теперь переделана на ЕГО
отправку, ...
async_CXT4_DATA_IO()
-- на ЕГО приём.
cx_ch_open_info_t
добавлено поле hwid
, ...
cda_dat_p_set_hwinfo()
.
Проверено -- да, теперь клиенту отдаётся правильный внутрисерверный номер канала, годный для сбагривания в cdaclient.
21.07.2020: как бы не сюда, но просто рядом выше уже
было аналогичное: повсеместное переименование "nelems
" в
"max_nelems
" там, где оно используется именно как
указание максимального, а не текущего количества.
refinfo_t
теперь называется
hwinfo_max_nelems
, также добавлено "max_" к именам параметров
cda_dat_p_set_hwinfo()
и cda_hwinfo_of_ref()
.
cda_dat_p_new_chan_f
и
cda_dat_p_set_type_f
параметр nelems
превратился в
max_nelems
.
24.07.2020:
30.07.2020: переходим на новую версию -- чтоб она была уже по умолчанию:
31.07.2020: "продолжение банкета".
Заменил клиентские бинарники на пульту.
И тут началось: куча серверов попадала по SIGSEGV.
Put_NT_OPEN_nnn_Chunk()
некорректно обрабатывался вариант NOTFOUND, когда mp==NULL: оно всё равно
пыталось заполнять ответ полями из mp->
.
/export/ctlhomes/oper/4pult/sbin/cxsd
сервера нормально
взлетели.
Возможно, оное изменение было замечено сильно невовремя, вот и вышло, будто бинарники неожиданно изменились прямо у запущенных cxsd, а такое неизбежно приводит к SIGSEGV.
Никаких прямых таких ссылок вроде нигде нет -- ни в *.subsys, ни в cx-starter-PULT.conf.
Остаётся подозрение, что это так ошибочно отрабатываются Cdr'ом узлы pzframe -- они есть в куче скринов от cxhw:15. Там ведь Cdr пытается зарегистрировать хоть какой-то канал, и в качестве оного использует "_devstate". Так вот -- возможно, как-то некорректно это делает.
Надо попробовать проверить -- напихать отладочной печати.
А пока оперативно опубликуем исправленный вариант как w20200731-3-cx41-cxsd-sigsegv-fixed.tar.gz.
02.08.2020: нашёл причину "корневых _devstate" -- в
Cdr_treeproc.c::pzf2ref()
при регистрации канала в
качестве "base" по ошибке передавался указатель на буфер
pzfr_base[]
вместо baseptr
, глядящего в нужное
место этого буфера. Теперь требует уже правильное имя.
И отладочную печать добавлять не пришлось -- подобные вопросы возникали и раньше, и для включения уже имеющейся отладочной печати оказалось достаточным сделать CDA_DEBUG_NAMES=1.
21.08.2020: был косяк: в ServeIORequest()
повсеместно стояли проверки на in_use==MON_TYPE_CHN, но в
GetChnd()
оное значение туда НЕ записывалось, оставляя
MON_TYPE_OLD, так что НИКАКИЕ CH_-команды не должны были работать. В
частности, не работала запись в каналы.
В GetChnd()
вставлено присвоение in_use=MON_TYPE_CHN и
проблема ушла.
Чуток истории -- как всё проявилось и расследовалось:
А та программа, как показал ps, запущена ещё в январе. Т.е., работает по СТАРОМУ протоколу.
Вот так крохотный косячок -- забытая смена типа монитора -- привел к крупной неработающести и несколькочасовым разборкам.
28.08.2020: и ещё косяк обнаружился -- с локингом.
cx_ch_rq_l_o()
-- это
cda_d_cx_lock_op()
,
31.08.2020: делаем.
<0
", чтобы если облом при отправке, то НЕ делать
последующих dat_p-вызовов (которые будут уже совсем не к месту).
Значит, при переходе в FOUND1 и надо отправлять запрос на лочку.
(...более того, сам метод lock_op() вызывает отправку запроса именно при rslv_state>=RSLV_STATE_FOUND1 -- так что как раз всё срастёся ровно как надо: если программа-клиент запросит локинг уже ПОСЛЕ установления соединения, но ДО перехода канала в RSLV_STATE_DONE, то запрос лочку будет отправлено РОВНО ОДИН раз, в зависимости от конкретного момента: если до прихода FOUND1, то именно по его приходу, а если после, то прямо в вызове.)
Проверил (cdaclient'ом, с префиксом '@!') -- вроде работает, канал залочивается.
04.09.2020: обнаружился ляп, введённый ещё 4 года назад, 27-05-2016, ещё для тогдашнего протокола, но и в новую реализацию также перекочевавший.
Вкратце:
GetChnd()
или в SetMonitor()
) при режиме
CX_MON_COND_ON_UPDATE, что для "легкообновляемых" каналов приводит к
немедленной отправке текущего значения, тем самым портя формируемый в это
время пакет в sendbuf'е.
Хронология разбирательства:
Была организована симуляция: файл devlist-cxhw-25.lst подправлен так, что vdev-устройства v5k5045 оставлены "живыми", а подчинённым РЕАЛЬНЫМ устройствам добавлены префиксы "-", чтобы они симулировались. Смысл -- чтобы ограничения от v5k5045_drv работали, а реальной аппаратуры всё равно нет.
Так что пытался "играться" приостановкой клиента Ctrl+S'ом, чтобы время запуска было более 60 секунд (таймаут в сервере между accept()'ом и моментом, когда клиент должен успеть прислать свои реквизиты), а также переживал, что проблема может зависеть от загруженности сети.
Потом, после определения проблемы, стало ясно, что дело, вероятно, было в деталях, на которые я не обращал внимания: наличие режима симуляции у конкретно ADC200 (записываю задним числом, уже после исправления, так что достоверно просто не помню). Видимо, в разные прогоны симуляция была то включена, то выключена -- наличие/отсутствие ключа "-s" в командной строке и/или префикса "-" у adc200.
CXC_NT_OPEN_FOUND_STAGE1
.
И так получалось, что по оффсету 80 от начала данных пакета, куда переходил указатель после 0-го chunk'а, т.е., там, где ожидался 1-й chunk, в "поле" ByteSize (на его месте) оказывался 0, так что ВСЕ последующие chunk'и, кроме 0го, были "несуществующими" -- оно так все оставшиеся и смотрело на это единственное место по оффсету 80, где осмысленных данных не было.
Следствие -- НИ ОДИН канал никогда не получал ни FOUND_STAGE1, ни
FOUND_STAGE2. А раз так, то каналы не переводятся в состояние
RSLV_STATE_FOUND1
и, как следствие, все приходящие для них
обновления/свойства/... игнорируются -- потому и NAN.
И понятно, почему в "старом" протоколе -- где этот косяк тоже был, с 2016 года -- он не проявлялся: там отсутствовало ограничение по текущему состоянию канала, а обновления принимались от сервера ВСЕГДА.
ServeIORequest()
(на о-о-очень большую цепочку запросов -- на
ВСЕ каналы клиента) вмешивалась отправка пакета NEWVAL, портившая пакет в
sendbuf
'е.
А вмешивалась отправка потому, что когда новый канал регистрируется, то
сначала на него вешается evproc, а потом, если режим ON_UPDATE, то сразу же
делается заказ чтения через CxsdHwDoIO()
. И если канал
"легкообновляемый" -- например, симулируемый -- то ответ приходит сразу же,
прямо ещё до завершения CxsdHwDoIO()
, и в evproc'е выполняется
отправка.
Как исправлять?
Конкретно -- чтобы InitReplyPacket()
проверял бы это условие
и возвращал бы ошибку, а его юзеры в лице конкретно
SendAReply()
и SendNotification()
при этом просто
ничего бы не делали.
...для чего нужно будет как-то фиксировать факт формирования пакета (чего сейчас НЕ делается).
04.09.2020: и ещё, похоже, есть один косяк, связанный с мониторами: на мониторы "нового стиля" шлются уведомления старой инфраструктурой.
06.09.2020: исправляем.
InitReplyPacket()
'ом он маркируется как "занятый", и последующие
вызовы InitReplyPacket()
возвращают результат "облом", так что
код-юзер просто пропускает свои действия по формированию+отправке; а
собственно отправка этот маркер сбрасывает».
Type
!=0.
Это было выбрано потому, что replybuf сразу аллокируется объёмом в
CxV4Header
, прямо при создании соединения в
AcceptCXv4Connection()
.
И сразу после аллокирования добавлена инициализация Type=0
.
(Была альтернатива добавить поле "sendbuf_busy", но не потребовалось,
поскольку и так поле Type
всегда доступно вследствие
сразу-аллокированности.)
InitReplyPacket()
переделана из void
в
int
, в начало её вставлена проверка, что если Type!=0, то
return -1
, а в конце возврат 0.
SendReplyPacket()
,
DisconnectClient()
и
возвратом -1
при обломе.
(Вот это оказалось неприятным неудобством -- пришлось вставлять forward-декларацию. Переупорядочить бы определения функций как-нибудь...)
InitReplyPacket()
(КРОМЕ
ServeIORequest()
-- предполагается, что уж он всегда вызывается
"из нулевого уровня", и в буфере ничего не может быть).
SendReplyPacket()
вместо
fdio_send()
+проверки.
Потому такое "выкидывание отправки" рассматривается как допустимое, поскольку текущие значения всё равно будут немедленно отправлены несколькими chunk'ами позже, так что никаких реальных потерь не произойдёт.
GrowReplyPacket()
либо возвращается 0
(в функциях-формирователях chunk'ов), либо
пропускается действие (в непосредственных юзерах).
...но это мы фиг проверим, конечно.
Проверяем: да, всё окей. Теперь никаких NAN'ов. Отключение же проверки Type!=0 возвращает старую ситуацию.
P.S. Да, это был реально хитрозамудрённый баг. Не то, что предыдущий "забыли сделать in_use=MON_TYPE_CHN".
07.09.2020: теперь разбираемся с тем, какие уведомления об изменении каналов отправляются клиентам -- старые или новые.
Расследование -- в виде отладочной печати данных каждого получаемого cxlib'ом пакета, включая число chunk'ов и тип первого из них -- показало, что косяк явно присутствует, причём аж целых ДВА:
Промежуточные выводы:
IfMonModifiedPutIt()
попросту отсутствует проверка на
тип монитора (OLD или CHN), а ВСЕГДА шлются уведомления "старого стиля".
Это надо исправлять.
Эти-то каналы запрашиваются в режиме ON_UPDATE, вот они и шлются сразу
(при обновлении в режиме симуляции) прямо из CHNEvproc()
.
Так что, похоже, ЭТО -- НЕ косяк.
Исправляем.
MON_TYPE_OLD
и
MON_TYPE_CHN
вызываются соответствующие формировальщики, а
ИНАЧЕ -- "return 0" (это для потенциальных будущих "иных типов" -- всякие
range_min/range_max, client_id и т.п.).
IfIsPerCycleMonRecordIt()
была добавлена
проверка, что тип - OLD или CHN, а иначе ничего не делать.
RlsMonSlot()
аналогичная ограда на
манипуляции с per_cycle_monitors_*
.
CXC_CH_PEEK
отправлялись коды chunk'ов "старого стиля" --
CXC_CURVAL
/CXC_NEWVAL
вместо
CXC_NT_CURVAL
/CXC_NT_NEWVAL
.
Учитывая, что эти коды сбагривались Put_NT_AVALUE_Chunk()
'у,
т.е., с НОВЫМ форматом chunk'а вместо старого -- должен был бы происходить
полный абзац.
Но спасло то, что это попросту никогда не вызывалось -- потому, что
cx_ch_peek()
нигде не используется, и вообще сделан "по
аналогии со старым" плюс для потенциального будущего chn_ioctl()'а.
На вид -- всё стало OK.
07.09.2020: есть ещё повод для разбирательства: ЕманоФедя утверждает, что ключ "-noprocess" в CX-starter'е не работает -- траффик и загрузка процессора (на Raspberry PI, ради которого ключ и делался, вместе со всем механизмом "CX_MON_COND_NEVER") не отличается от варианте БЕЗ этого ключика.
Вероятно, проблема тоже где-то в этой же области cxsd_fe_cx/cda_d_cx.
...поскольку изначальные тесты делались на старом протоколе, где "немониторирование" сводилось к не-уставлению монитора, то есть подозрение, что опять что-то недоделано в новой версии протокола.
Да, анализ показывает, что в отсылке в-конце-цикла проверка на MON_COND_NEVER отсутствует.
Чуть позже разобрался: там проверка и не нужна, поскольку
COND_NEVER-каналы обрабатываются иначе и у них никогда не взводится
modified
, то в "отсылке в-конце-цикла" -- в итераторе
IfMonModifiedPutIt()
они никогда и не будут рассматриваться для
отправки.
А косяк обнаружился в клиентской части: в cda_d_cx.c было по ошибке сделано
MODE_MON_TYPE_mask = MODE_ON_UPDATE | MODE_ON_UPDATE
вместо надлежащего
MODE_MON_TYPE_mask = MODE_ON_UPDATE | MODE_NOMONITOR
так что флаг NOMONITOR попросту игнорировался.
После исправления всё стало офигенно.
И далее
"бывают проги, которым нужны именно все события из системы управления. Но для проги, которая должна показывать текущие данные и принимать команды такое поведение надо считать не желательным, т.к. данные которые были 40с назад или вчера - уже ни кому не нужны. "
14.09.2023: обдумывал-обдумывал -- мрак мрачный. Возможно, ради чего-то подобного и существуют в EPICS'е разные "приоритеты", но вообще это ужас ужасный -- надёжно и корректно реализовать такую адаптировабельность практически невозможно.
В частности,
15.09.2023@~13:00, дорога по Николаева в Гуси, проходя мимо ГИПроНИИ (Николаева-8): а вообще есть идейка (зародилась ещё вчера, а сегодня созрела).
Главная фишка -- оперировать не каналами, а ЦИКЛАМИ. Т.е., подтверждать получение не данных индивидуальных каналов, а событий "конец цикла".
Детали:
CXT4_END_OF_CYCLE
) только при условии, что предыдущий сигнал
цикла клиент успел "подтвердить".
CX_MON_COND_ON_CYCLE
-мониторов, для прочих оно незачем.
Это покрывает GUI-клиенты, в которых и проблема.
Для отправки такого пакета добавляется cxlib-вызов.
Для отправки которого также добавляется cxlib-вызов.
SendEndC()
, если флаг "предыдущий
цикл подтверждён" не взведён.
mp->modified
и
cp->per_cycle_monitors_some_modified
-- будут
"накапливаться" в течение нескольких циклов, а по получении "подтверждения"
на очередном цикле данные будут отправляться и флаги сбрасываться.
$CDA_D_CX_SYNC_CYCLES
со значением '1' или
'y'; в самом её имени видно, что оно касается только ON_CYCLE.
16.09.2023: реализовываем.
CXT4_SET_CYCLE_MODE = CXT4_data + 3, CXT4_ACK_CYCLE = CXT4_data + 4, // C>S acknowledgement
CXT4_SET_CYCLE_MODE
-пакете сам режим
будет отправляться в поле var1
заголовка,
CXT4_ACK_CYCLE
в var1
будет
отправляться "код подтверждения" -- 0
означает "неа, тормози
отправку!", а !=0
наоборот "можешь слать дальше".
Следствия:
v4clnt_t
добавлены
int cycle_mode_is_sync; int cycle_send_frozen;
HandleClientRequest()
-- трансляция из пакетовых
var1
в эти поля в соответствии с "протоколом" из п.1.
case CXT4_SET_CYCLE_MODE: cp->cycle_mode_is_sync = (hdr->var1) != 0; break; case CXT4_ACK_CYCLE: cp->cycle_send_frozen = (hdr->var1) == 0; break;
Т.е., ответов эти пакеты НЕ предполагают.
SendEndC()
использование этих флагов: в начале --
пропуск отправки при "замороженности":
а в конце -- пере-взведение флага "замороженности" в синхронном режиме:if (cp->cycle_send_frozen) return 0;
if (cp->cycle_mode_is_sync) me->cycle_send_frozen = 1;
Т.е.,
Тут отличия в том, что, во-первых, так естественнее -- флаг
=0
и в обычном асинхронном режиме работы и сразу после
включения асинхронности (итог -- меньше действий), а во-вторых -- это
позволяет "замораживать" и обычные асинхронные соединения.
с очевидным содержимым для отправки соответствующих пакетов.int cx_send_cycle_mode(int cd, int mode); int cx_send_ack_cycle (int cd, int code);
ProcessFdioEvent()
код
CXT4_EBADRQC
убран из списка фатальных и вытащен в отдельный
case
, где теперь на него просто печатается диагностическое
сообщение cxlib_report()
'ом.
Смысл -- чтобы при натравливании на "старые" сервера "новых" клиентов они оставались бы работоспособны.
Это может и при будущих модификациях пригодиться.
cycle_mode_is_sync
в
cda_d_cx_privrec_t
.
cda_d_cx_new_srv()
по
значению $CDA_D_CX_SYNC_CYCLES
, ...
ProcessCxlibEvent()
при взведённости:
CAR_CONNECT
делается cx_send_cycle_mode(,1)
ПОСЛЕ "основной" реакции SuccessProc()
(ну мало ли что там
обломится).
CAR_CYCLE
делается
cx_send_ack_cycle(,1)
ПОСЛЕ основной реакции (т.к. при запуске
через X11-тоннель именн обновление GUI будет тормозить).
srv_ioctl()
'а.
Проверяем на симуляции -- сервер canhw:11, запущенный с ключом
"-b1000
" (1kHz), на который натравливается скрин linmag --
вроде работает как надо. Понято по тому, что с включенной синхронизацией
отправки циклов скрин просто работает, а с отключенной соединение постоянно
рвётся (т.к. сервер на такой частоте успевает забить буфер отправки за то
время после коннекта, пока GUI отрисовывается).
Подробнее см. рассуждения на эту тему за сегодня в bigfile-0001.html.
Состоять же библиотека будет из большего числа модулей. Директория -- lib/srv/, библиотека -- libcxsd.a.
Для удобства поиска переименовываем раздел из "CX-server-library" в "libcxsd".
n<0||n>=cxsd_hw_numchans
) надо маппировать на 0-й
канал -- который принадлежит фиктивному 0-му устройству
"!DEV-0!", и должен иметь перманентно "плохие" флаги и
бесконечный возраст (кстати, надо вставить в CxsdHwSetDb()
их заполнение).
Смысл -- чтобы работала схема блокировки каналов клиентами (где ключом является client-ID, см. за 06-07-2007), независимо от того, через что клиент приконнектился.
И даже драйверам разрешить испрашивать свои client-ID -- для работы
напрямую через CxsdHw*()
.
05.10.2014@Снежинск-гуляние-под-моросящим-дождиком-вдоль-озера: (реально мысли возникли несколько дней раньше) чуть хитрее:
22.10.2014: да, сделано по тому проекту.
CxsdHwCreateClientID()
и
CxsdHwDeleteClientID()
.
cxsd_chanid_t
-- всегда gcid
(GlobalChan ID) вместо gcn, globalchan, hwid, ...
cxsd_cpntid_t
-- всегда cpid
(Control
Point ID) вместо cpn.
hwid
оставить только на стороне клиента (в API
cxlib). Возможно, "ПОКА оставить".
29.07.2015: процесс:
cxsd_chanid_t
переименован в cxsd_gchnid_t
.
cxsd_gchnid_t
и
cxsd_cpntid_t
переехали из cxsd_hwP.h в
cxsd_dbP.h...
int
.
Но это пока не сделано, пока оставлены int'ы -- потому, что там шибко уж поперемешаны номера cpoint'ов и каналов.
Вот если б мочь как-то добывать и показывать также конечную точку, на которую этот cpoint смотрит...
16.09.2015@утро-дома: ...а для полноты отлаживаемости -- также мочь бы добывать и показывать также прочие свойства, вроде cpid+hwid, hwr, fresh_age.
16.09.2015@утро-по-пути-на-работу-мимо-НИПСа: сделать оное, наверное, можно -- понадобится содействие от всех уровней:
CXC_RESOLVE
.
CxsdHwResolveChan()
.
В идеале, видимо, оба.
char*
.
SetChanProps()
, дающая драйверу возможность
указывать клиентско-ориентированные свойства каналов: границы
разрешенных значений, кванты, возрасты свежести.
13.07.2009: первоначально идея была придумана еще 07-01-2007 в bigfile-0001.html, а сегодня на сборище по СУ ТНК оформилась окончательно. Смысл -- во-первых, чтобы НЕ хранить эту информацию в БД, а во-вторых -- чтобы при изменении (например, при переходе на другое время интегрирования в АЦП) драйвер мог бы сообщить актуальные параметры.
P.S. Пока -- только декларация+скелет, без реализации.
29.10.2013: замечание общего характера, касается как данного пункта (точно?), так и параметра "boss": такое указание не количественных (коэффициенты), а поведенческих свойств (boss, имена) драйвером (а не в конфигурации) at-run-time имеет потенциальную проблему с удалёнными драйверами: что информация поступит от драйверов уже ПОСЛЕ коннекта клиентов и принятия от них запросов.
Отсюда выводы:
В остальных же случаях devlist'ы должны содержать ПОЛНУЮ информацию.
(Тут "защита" будет за счёт того, что не-OPERATING драйверам никакие запросы НЕ передаются. Но это всё равно полумера -- запросы на имена-то успеют придти и разрезолвиться.)
30.10.2013: но HeartBeat() должен включаться сразу же после завершения connect()'а.
26.06.2015: сама SetChanProps()
удалена. Поскольку часть функционала не нужна вовсе (указание
dtypes,nelems), часть выполняется другими вызовами (fresh_ages), а
часть пока не сделана вообще нигде и никак (quants) либо неясно, будет
ли когда-нибудь (min_alwds,max_alwds), но когда/если понадобится, то
будет сделано отдельными вызовами.
19.07.2009: реализовывать-то это пока еще точно рано.
Но задел на будущее сделать лучше сейчас -- чтобы потом не пришлось править API и переделывать десятки драйверов.
Так что введено еще одно поле в CxsdDriverRec
--
cmd_table
, это указатель таблицу описаний команд. Плюс,
создана куча связанных с этим типов:
CxsdDevCmdParamType
-- enum, определяющий тип
параметра.
CxsdDevCmdParam
-- структура для передачи
параметров.
CxsdDevCmdFunc
-- функция-обработчик.
CxsdDevCmdDescr
-- собственно тип описателя, на
таблицу которых и указывает cmd_table
.
Засим пока этот вопрос замораживаем.
12.08.2012@утро-душ: проблему постояннорастущести содержимого DriverModRec'а можно решить просто -- вставить ссылку на NULL-terminated таблицу "extension". В ней -- дуплеты {тип,ссылка}, где "типом" может быть как таблица команд (сейчас торчащая в самом DriverModRec'е), так и описание требуемых data/inserver-линков.
13.08.2012: делаем:
cmd_table
убраны (нигде никогда не
использовались).
CxsdDevExtRef
состоящий из пары
{type,data}.
extensions
помещено сразу после layerver, и
параметр тоже.
14.09.2009: для этого был создан тип
CxsdChanNameRec
, и теперь в метрике после
chan_nsegs
,chan_info
идёт поле
chan_namespace
. Концом таблицы является
.name==NULL
.
15.09.2009: добавил еще 3-е поле --
"boss
", в котором надлежит указывать номер канала, для
которого данный является настроечным.
Проблема -- что значение по умолчанию, 0
, формально
является вполне легальным номером канала, так что придется явно
указывать -1
ВСЕГДА.
17.02.2009: а вообще-то это кривизна: по идее, "boss" должно бы указываться НЕ в пространстве имён, а в описании самих каналов!
26.02.2013: да, из CxsdChanNameRec
поле boss
убрано; но больше никуда пока не добавлено...
18.11.2014: в продолжение темы начинки описателя namespace: надо б переходить от поштучного указания имён каналов к групповому. Детали:
n
не только начальный канал, но и
количество. Если оно ==0, то это одиночное указание.
После обеда: если хорошенько подумать, и вспомнить слова за 17-02-2009 -- нечего в пространстве имён делать описанию отношений базовый/настроечный и основной/подчинённый.
А групповые списки имён каналов -- пожалуй, имеют смысл. Хотя всё равно вопрос, КАКОЙ: куда и когда эти имена должны помещаться? Только если генерить на их основе .devtype-файлы (для чего можно изготовить утилитку)...
14.03.2015: в связи с тем, что авторитетным источником данных об именах и проч. является всё-таки конфигурация, то надобность во всех этих "chan_namespace" становится несколько сомнительной.
Единственное потенциально-спекулятивное применение -- в автономных программах-утилитах, чтобы они всю инфу вытягивали прямо из драйвера, обходясь совсем без конфига.
Но это скорее уже из разряда избыточности (и вопрос, нужной ли).
14.03.2015: конкретно сейчас, по опыту cac208_drv и adc200me_drv -- потребности такой нет, т.к.
На уровне API указание "спец-лог" можно делать либо специальным
DRIVERLOG_C_
, либо, лучше, просто совсем отдельным флажком
-- рядышком с DRIVERLOG_ERRNO
.
22.10.2012@Снежинск-каземат-11: процесс:
_list
.
SLOTARRAY_DEFINE_FIXED()
.
Вопрос теперь -- будем ли переходить на GROWING (для Tout -- на GROWFIXELEM) и как и когда.
28.06.2013: да чё уж там -- сразу ставим "done", поскольку сейчас вообще весь этот механизм будем ликвидировать (в связи с переходом на uniq).
28.06.2013: сделано.
01.07.2013: в lib/srv/cxsd_driver.c оставались "ошмётки":
SELECT_EVENT_TARGET()
(смысл
в которой исчез вместе с cxsd_hw_eventinfo_t
) и
CHECK_SANITY_OF_MODID()
(использовавшаяся в oslike-API для
возможности работы с модулями при devid<0). Они удалены.
CHECK_POSITIVE_SANITY()
-- вытащенный в
отдельный от CHECK_SANITY_OF_DEVID()
кусок -- оставим:
так оно нагляднее.
cxsd_uniq_checker()
.
23.11.2014: а вот и некорректно она тогда была
сделана -- через DO_CHECK_SANITY_OF_DEVID()
. В результате
layer'ам не дают регистрировать ресурсы, а в свете того, что у клиентов
будут свои ID -- от CxsdHwCreateClientID()
-- станет
совсем худо.
Так что временно проверку закомментировываем, до появления идеи "как лучше поступать".
..."как лучше" -- надо отрицательные числа проверять на попадание
в [-cxsd_hw_numlyrs,-1], а положительные cвыше
cxsd_hw_numdevs
-- на наличие в текущий момент такого
активного ClientID.
И, скорее всего, функции место не в _driver, а в _hw (где и с ClientID работа). ...даже более: почему она изначально-то попала сюда, ведь к API драйверов она никакого отношения не имеет?
Итого -- cxsd_uniq_checker()
переселена в
cxsd_hw -- как раз рядышком с cxsd_hw_do_cleanup()
, к
которому она имеет отдалённое идеологическое отношение.
ReturnChanSet()
в ReturnDataSet()
, в
соответствии со стратегией из
20120413-CX-UPGRADE-TODO.txt/26-05-2012.
13.11.2013: да, ReturnDataSet()
.
Оказывается, в не столь уж малом количестве мест присутствовало. Хотя
внутренности всё равно почти пустые.
03.09.2014: внутренности давно уж (с весны) как непустые, там присутствовало зародышевое складирование получаемых данных.
Сегодня:
CxsdHwCallChanEvprocs()
-- отдельным 2-м циклом,
так что все скопом ПОСЛЕ обновления.
cda_dat_p_update_dataset()
. Осталось
еще конверсию данных оттуда взять.
04.09.2014: конверсия сделана. Но в сервере есть пара существенных отличий:
Поэтому блок преобразования сильно отличается от cda'шного:
srpr==repr && ssiz==size
, а не по совпадению dtype'ов,
так что типы, различающиеся лишь знаковостью, считаются идентичными.
int64
, но этот вариант заключён в "#if
MAY_USE_INT64", а иначе сваливается в...
int32
, причём тут проверок на INT64 нету, а он
идёт как UINT8.
float64
(т.е.,
double
).
Этот вариант заключён в "#if MAY_USE_FLOAT", так что при запрещённости использования вещественных сваливается в...
MAY_USE_*
Итак -- теперь проверять. Но это уже после реализации cda_d_inserver.c.
31.10.2014: функция перенесена в cxsd_hw.
Отдельный вопрос -- куда относится SetDevState()
?
Видимо, всё же сюда, но "того" он тоже касается.
31.10.2014: да, ReturnDataSet()
перенесена в cxsd_hw.c, там ей самое место рядом с
CxsdHwDoIO()
& Co. Насчёт остальных не так ясно,
поэтому их пока не трогаем.
20.11.2014: и SetDevState()
тоже
перенесена (вместе с ReRequestDevData()
-- про неё и
говорить нечего, очевидно, что относится к _hw). Точнее -- реальное её
наполнение уже шло там.
businfostr
" (вопрос, как указываемую -- суффиксом
через '/' или как-то префиксом?)? Чтоб там мочь указывать
специфику вроде "ttyUSB" и "canUSB". Или уж
постараться обойтись auxinfo?
CxsdDevChanProc()
: теперь вместо
first
,count
она принимает
count
,addrs[]
.
14.09.2014: изменение было предопределено давным-давно:
ReturnDataSet()
, вот и
усимметричено.
Вариант же с first
оставался наследством от v2.
10.07.2015: действия:
TRUE_DEFINE_DRIVER_H_FILE
.
Причина -- cm5307_DEFINE_DRIVER.h косвенно include'ит
remcxsd.h, в котором используется
CxsdLayerModRec
, и получалась parse error.
Сейчас это парсят сами драйверы и оно мерзко; а вот если б можно было сразу иметь количество параметров и простой доступ к N-тому параметру в стиле "argp(N)" -- как сделано в cxsd_fe_cxv2.c и в remsrv_drvmgr...
24.09.2015: задача усложняется тем, что НЕЛЬЗЯ
модифицировать auxinfo, забивая его '\0'ями -- оно не зря
const char*
; в сервере эти строки вообще лежат в
strbuf[]'е.
P.S. Кстати, родственный механизм -- datatree'шный
get_knob_item_n()
.
#include"fdiolib.h
, при
том, что ни в cxsd_driver.h интерфейс fdiolib, ни в
cxsd_driver.c сама fdiolib НИКАК не используются.
Замечено было потому, что в remdrv_drv.c -- очевидно
использующем fdiolib -- оного #include
нету.
04.05.2016: размышления по ходу расследования:
#include
!".
Кстати, этих "конкретных драйверов" СЕЙЧАС -- ровно один remdrv.
Но тут годится логика, что это бывшие когда-то частью серверного API (до перехода на "cxscheduler3" с uniq), поэтому нехай будет.
...кстати, а вот он-то используется почти всеми драйверами -- ибо без В/В и таймаутов драйверы слабо осмысленны.
Так что пусть остаются.
Занадобилось оно в интересах драйвера bridge_drv.c -- чтоб тот мог получать (по-канально!) информацию, из какого "внешнего" канала добывать данные для данного.
Принципиально такая возможность появилась после реализации концепции "dcpr", но среди имевшихся строк свободной была только rsrvd6, и её всё равно не хотелось нагружать сторонними задачами.
еще в тему диапазонов, единиц и прочего: У меня в бд есть поле "тип доступа", используемый при сохранении/загрузке режимов. Это поле несколько расширяет классификацию каналов, которая есть в CX, т.е у тебя это только r и w, а у меня кроме этого еще канл может быть помечен как: - настроечный (т.е. в обычной работе он не грузится, а служит для передачи настроек между программами) - процедурный (т.е. в него пишут чтоб выполнить процедуру, а читать его смысла нет) - производный (т.е. запись его возможна, но он отображается в оборудование через дополнительные вычисления, это как для Д16 задержка, т.е. загрузка режимов по производным каналам - грузятся времена, а по обычным каналам записи - грузятлся значения регистров.)
В ответ на уточняющие вопросы он опять начал грубить, но стало ясно, что просто строки с произвольным содержимым ему для начала будет достаточно.
Откуда напрашивается -- ну добавим в dcpr ещё пару строк, чтобы они могли указываться в devtype.
14.02.2020@дорога из 13-го, затем лифт: названия для информации -- dbprops и drvinfo.
16.02.2020@дома-воскресенье: делаем!
Записываем сюда несколько позже, и по общему результату в качестве места выбран раздел по cxsd_driver, в силу того, что описываемый функционал предназначен для драйверов, да и значительная часть работ сошлась к этому модулю (хотя было ОЧЕНЬ много где).
Также были сделаны ещё некоторые работы по связанным вопросам, они тоже описываются здесь.
Итак:
CxsdDbDcPrInfo_t
добавлены поля
dbprops_ofs
и drvinfo_ofs
.
dcpr_fields[]
добавлены соответствующие строки.
CxsdHwGetChanAuxs()
.
Она принимает на вход именно gcid -- cpid не подходит.
Потому она весьма прямолинейна и проста.
22.03.2021: её return-параметры переделаны с
"char **
"
на
"const char **
"
-- ибо так правильнее.
CxsdHwGetDevPlace()
.
Но она перетащена из cxsd_hw в cxsd_driver -- ТУТ ей место.
25.02.2020: только надо было при этом и убрать префикс
"CxsdHw", переименовав её в GetDevPlace()
. Вот сейчас это
сделано.
GetDevInstname()
из
mqtt_mapping_drv.c.
Выглядит всё перечисленное вроде несложно, но делалось это в параллель с изготовлением bridge_drv.c, в его интересах, и потребовало изрядного напряжения мозгов, чтобы всё рассовать в нужные места.
17.02.2020: проверяем, на примере единственного пока драйвера-юзера -- да, работает!
GetDevChanByName()
, позволяющая драйверу сделать резолвинг
имени своего канала в номер.
Занадобилась -- пока потенциально -- для мета-драйвера modbus_tcp_drv.c, чтоб можно было указывать границы каналов для обработки ответов на групповые запросы.
18.08.2023: мини-протокол.
CxsdDbFindChanInDevice
.
CxsdDbFindChanInDevice()
-- просто адаптер к уже
ранее имевшейся в cxsd_db.c FindChanInDevice()
,
являвшейся подмастерьем CxsdDbResolveName()
.
GetDevPlace()
и CxsdHwGetChanType()
с
CxsdHwGetChanAuxs()
-- тут возвращается номер канала ВНУТРИ
УСТРОЙСТВА, а не глобальный.
Проверено ручной симуляцией -- в драйвер напихано несколько вызовов с печатью результатов; работает как положено, "done".
25.04.2024: первое использование, в simkoz_drv.c для резолвинга имён каналов (SRC:DST) -- работает.
Используется префикс CxsdDb
, сам тип-БД --
CxsdDb
(это указатель).
Замечание: этот модуль отвечает за ОПИСАНИЕ БД, а не за "используемое сейчас сервером описание аппаратуры" -- то епархия cxsd_hw.
01.07.2013: имелся ляп общего характера в
CxsdDbAddDev()
и CxsdDbAddLyrI()
: оно трогало
содержимое передаваемой структурки. Несмертельно, но криво.
Переделано на нетрогание и использование локальных переменных (new_*).
CxsdDbLoadDb()
передаётся argv0 -- и далее по
цепочке, в интересах плагинов-загружателей (тем более, что в
CxsdDbLoadDbViaPPF4TD()
он уже предусмотрен).
mp++
вместо mp=mp->next
.
Видимо, скопированность из KnobsCore_knobset.c, где таблицы
двухуровневые (knobset/list), и на нижнем уровне действительно массив
указателей на метрики.
26.03.2015: собственно:
CxsdDbCpLine_t
-- описатель одной точки контроля.
Сейчас в ней только поля name_ofs
и cpn_n
, но
туда же будут добавляться {r,d} и прочие требуемые.
CxsdDbCpNsp_t
-- "namespace", содержит данные для
devtype плюс в конце [0]-массив описателей.
Штука простая -- создаётся malloc()'ом, потом по мере надобности растится realloc()'ом, освобождается одним free().
CxsdDbInfo_t
дополнилась:
strs
с обвязкой -- "БД строк".
nsplist
со свитой -- массив указателей на
namespace'ы.
ParseChanGroupList()
, чтоб быть доступным для
devtype_parser()
'а.
PeekCh()
/NextCh()
,
SkipToNextLine()
.
Изменения -- в ссылке на контекст: в Cdr передаётся
parse_rec_t
, а тут ppf4td_ctx_t
. Так что
унификация в общем include-файле невозможна; но разных парсеров не так
много, так что несмертельно и можно такое дублирование терпеть.
devtype_parser()
:
dev_parser()
сделана поддержка "~" --
ищется соответствующий namespace и из него берётся инфа о
списке-групп-каналов.
27.03.2015: далее -- внедрение использования в cxsd_hw.c (реализовывалось задом-наперёд от порядка использования):
CxsdDbDevLine_t
добавлено поле
nsp_id
.
CxsdHwResolveChan()
код сравнения изменён с
"проверяем, не число-номер-канала ли это" на "пытаемся найти
это как имя в его namespace, а уж если нет -- то как номер".
Замечание: поиск по cpoint'ам должен будет стоять ДО этих (для переопределяемости).
CxsdHwSetDb()
среди прочего реплицируются и
strbuf
и nsplist[]
со свитой.
Причём место аллокируется по ИСПОЛЬЗУЕМОМУ, а не по аллокированному
в исходной db -- поскольку cxsd_hw_cur_db
уже неизменна и
добавляться ничего не будет.
nsp_id
для экземпляров устройств, для которых не
были указаны секции channels
, поэтому должных быть
связанными со своими devtype -- надо производить сразу после окончания
загрузки.
И этой точкой оказалась CxsdDbLoadDb()
из, как ни
смешно, cxsd_db_plugmgr.c.
16.02.2017: поправка: с тех пор ситуация несколько
изменилась (конкретно 15-05-2015), и вместо единственного
nsp_id
(содержащего ссылку ЛИБО на типовый namespace, либо на
channels'овый) теперь есть пара -- type_nsp_id
и
chan_nsp_id
; так что у устройства могут быть имена и от типа, и
персональные от channels. А явное прописывание type_nsp_id
уже есть в самом dev_parser()
; так что связывание-задним-числом
в CxsdDbLoadDb()
, наверное, можно уже и убрать (хотя -- вдруг
иной db-loader-plugin, не такой правильный?).
Замечание о стандартизации имён переменных касательно
namespace'ов: указатель на него -- nsp
, id (номер) --
nsp_id
, номер строки в нём --
nsline
.
Собственно, хохма в том, что (после устранения пары мелких ляпов) РЕЗОЛВИНГ ИМЁН РАБОТАЕТ!!!
Дальнейшие планы:
Например, формат такой:
PREFIX<a-b>SUFFIX NUMBER
(как в zsh). При просто числе во втором поле там никакой ссылки на
"точку вставки шаблона" указывать не надо -- просто будут
последовательно прибавляться 1,2,... к базе. А вот если указывать там
тоже имена -- то некоторый вопрос (хотя зачем такое может понадобиться
-- еще больший вопрос).
29.03.2015@днём-ванна: изменяемые ДРУГИЕ компоненты (помимо имени) потребуются как минимум для cpoint'ов -- чтоб их группами задавать. Кроме того, поля label (даже у обычных каналов).
Вопрос КАК указывать множественность: <X-Y> можно, но не очень гибко. Мож, ввести префикс НАЧАЛА строки "*N", а потом, в НЕКОТОРЫХ полях (label, но НЕ dpyfmt или units) дать возможность указывать, куда вставить число и КАК (прибавив какую базу и надо ли нули в начале).
30.03.2015: замечание прямым текстом: по-хорошему, свойством "ident" конкретного канала/cpoint'а должно быть прямо его ИМЯ. И нефиг заводить отдельное поле "ident".
30.03.2015: внедряем улучшения, продуманные за прошедшие выходные:
CxsdDbDevLine_t.chancount
.
...как-то приподзаткнулось -- не пёрло совсем...
31.03.2015: так толком и неясно, как ЛУЧШЕ указывать множественные каналы, поэтому ПОКА сделан простейший вариант -- с <X-Y>. Работает, сцуко!!! :)
(Да, сделано в
cxsd_db_via_ppf4td.c::devtype_parser()
очень
просто и прямолинейно, совсем неэлегантно, но чисто чтоб продвинуться
дальше -- начать пользоваться хоть как-то, а там уж по опыту посмотрим
и постараемся улучшить.)
14.05.2015: добиваем кусок поиска, чтоб был по БД, а не по hw-dev'ам:
CxsdDbLoadDb()
: подсчёт количества каналов каждого
экземпляра устройства.
CxsdDbInfo_t.numchans
и его заполнение в
CxsdDbLoadDb()
и CxsdHwSetDb()
.
CXSD_DB_CHAN_DEVSTATE_OFS
и
CXSD_DB_CHAN_DEVSTATE_DESCRIPTION_OFS
, плюс
CXSD_DB_AUX_CHANCOUNT
определяет общее количество
"дополнительных" каналов.
CxsdDbResolveName()
. Ей
передаётся db и имя, а она возвращает:
CXSD_DB_RESOLVE_
:
ERROR
-- облом.
GLOBAL
-- глобальная ссылка (просто номер канала);
номер вертается в chann.
DEVCHAN
-- ссылка DEVNAME.CHAN; устройство
возвращается в devid, номер канала в нём в chann.
CPOINT
-- точка контроля; номер отдаётся в chann
(эффективно -- аналогично GLOBAL, просто номер >=numchans).
Т.е., она выполняет ТОЛЬКО резолвинг.
CxsdHwResolveChan()
и FillPropsOfChan()
.
Поскольку как минимум {r,d} аппаратного канала есть только в _hw, а _db их не имеет.
Проверено -- работает.
14.05.2015: считаем, что эта секция касалась только основных имён (DEVICE.CHANNEL), и её помечаем как "done", а точки контроля пусть будут в своей секции.
07.10.2018: в блоке парсинга "имён с диапазонами <X-Y>" была некорректная проверка на (не)выход указанного диапазона за диапазон каналов драйвера: стояло
if (range_max >= chan_n_limit)
(т.е., УСЛОВНЫЙ числовой индекс сравнивался с РЕАЛЬНЫМ количеством каналов)
вместо надлежащего приведения индекса к связанному с ним номеру канала:
if (refval + range_max-range_min >= chan_n_limit)
В результате ругалось на вполне корректное
devtype v5phaseconv w4d { phase<1-4> 0 }
После надлежащего исправления проблема ушла.
Для начала именно как имён, а потом добавим свойства, и следующим этапом будет GURU.
15.05.2015: вопрос о двух частях:
CxsdDbDevLine_t.nsp_id
переименовано в
type_nsp_id
, для отражения факта, что это линк на devtype.
chan_nsp_id
, для namespace'ов
от секций channels.
CxsdDbResolveName()
ищет сначала в [dev]type'овом, а
потом в chan[nels]'овом.
devtype_parser()
'а в отдельную ParseChanList()
,
которую использует и channels_parser()
.
Ссылка на namespace, правда, при этом передаётся очень противно -- по
указателю на неё, т.к. CxsdDbNspAddL()
принимает
CxsdDbCpNsp_t**
(оно там реаллокирует при добавлении блоков
строк).
CxsdDbCpNsp_t.chancount
, и вставлен его
подсчёт в ParseChanGroupList()
-- чтоб сразу можно было
проверять на невыход указываемых номеров каналов за границы.
CxsdDbResolveName()
и ищется.
17.05.2015: продолжение, по тем же частям:
Процесс (всё не так просто!):
ParseChanList()
добавлен параметр
type_nsp_id
, в котором channels_parser() передаёт его
самого, а devtype_parser() -- -1.
И поиск по нему тоже добавлен.
CxsdDbDevLine_t.type_nsp_id
прописывать СРАЗУ, а не задним числом, после парсинга всего.
Посему эта обязанность переложена с CxsdDbLoadDb()
на
dev_parser()
(он просто СРАЗУ выполняет поиск по
указанному TYPENAME).
13.02.2017: да, не то что "бесполезна" -- не работает. Подробнее см. в отдельном комментарии за сегодня ниже.
А такое сделать вполне можно!
Собственно, эти "оконечные узлы" и есть сами cpoint'ы, обладающие такими свойствами:
phys_rds[2]
--
плюс флаг их указанности -- phys_rd_specified
.
Также, вероятно, и толпу строк -- units, comment, ...
Замечание: FillPropsOfChan()
, идя по цепочке от
верхней точки к нижней (аппаратному каналу), должен будет оставлять в
строковых свойствах "максимально верхние значения" -- т.е., если уже
!=NULL, то более не трогать.
CxsdHwSetDb()
добавляет 0-й девайс со 100
каналами.
Проще будет взводить какой-нибудь дальний битик -- например, 30-й (не НЕ 31-й, ведь число должно быть положительным!). А со срезанным этим битиком оно и будет индексом в таблице "оконечных узлов".
А надо дуплетом DEVNAME.CHAN_N. Причём ссылка DEVNAME -- конечно, str_ofs'ом. 21.05.2015: уже всё-таки devid.
Следовательно, в типе-структуре "конечный узел" будет этот самый дуплет int'ов.
CxsdDbCpLine_t
, возможно, придётся
расширить -- для возможности указывать "тип" ссылки, которая сама
записана в поле cpn_n
: либо на вложенный контейнер (тогда
это nsp_id), либо на оконечный канал/cpoint (это cxsd_cpnt_t).
18.05.2015: а ЗАЧЕМ делать ветки/контейнеры именно ТАКИМИ ЖЕ namespace'ами? Проще ввести отдельный тип, заточенный под конкретные потребности.
CxsdDbCpntInfo_t
-- содержит информацию о конечном
узле. Т.е., -- описание свойств cpoint'а.
Их массив содержится в CxsdDbInfo_t.cpnts
.
CxsdDbClvlInfo_t
-- информация о ветке (аналог
namespace'а).
Содержит [0]-массив структур CxsdDbClvlItem_t
-- которые уже
и ссылаются далее, а на что -- указывает поле type
, содержащее
одно из CXSD_DB_CLVL_ITEM_TYPE_nnn
, где nnn -- CPOINT, CLEVEL,
DEVICE.
CxsdDbCpLine_t
-> CxsdDbDcLine_t
, и её
поле cpn_n
-> devchan_n
.
CxsdDbCpNsp_t
-> CxsdDbDcNsp_t
.
19.05.2015: собственно работа:
СЕЙЧАС -- там абсолютные ссылки.
А не понадобится ли как-то поддерживать ОТНОСИТЕЛЬНЫЕ?
ПРЕФИКС {
на
ПРЕФИКС_ИМЕНИ [ПРЕФИКС_TARGETа] {
И чтоб у вложенных ПРЕФИКС_TARGETа конкатенировался к предыдущему. В этой всей конкатенации тоже явно понадобится поддержка начальных '.' и ':'.
Сейчас просто думаем, а делать или нет -- посмотрим по результатам использования.
ln
--
если исходного файла (НА который делается ссылка) не существует, то имя для
создания оно и проверять не будет на создавабельность.
И если это ссылка на DEVCHN или CPOINT -- то также создаётся собственно cpoint (аллокируется структура CxsdDbCpntInfo_t).
Причём с clvl'ами применена такая парадигма:
ROOT_CLVL_ID
; а =0 --
зарезервированный пустой),
CXSD_DB_RESOLVE_DEVICE
-- устройство (ссылка по
имени без точек, либо результат резолвинга, указывающий на узел, ссылающийся
на устройство).
CXSD_DB_RESOLVE_CLEVEL
-- ветка/контейнер
(виртуальное устройство).
Но резолвер их пока не поддерживает.
20.05.2015: уже поддерживает.
21.05.2015: да, работает :)
21.05.2015: за вчера и сегодня доделано до полностью рабочего состояния (за вычетом парсинга свойств).
CXSD_DB_CPOINT_DIFF_MASK
=1<<30.
В принципе, СЕЙЧАС, когда исходная БД идентична hw'шной (т.к. добавление [0]-го девайса сделано прямо в cxsd_db.c), можно было б уже просто пустить точки контроля после каналов, но уж сделано как сделано.
А вот и нельзя б! Ведь точки контроля создаются вперемежку с устройствами, так что количество аппаратных каналов еще неизвестно!
Так что сейчас выбрана хорошая схема, позволяющая дальнейшее развитие (типа заменить "бит" на несколько битов "класс идентификатора").
CxsdDbCpntInfo_t
на аппаратные каналы хранятся прямо devid, а
не ofs'ом instname'а.
CxsdDbResolveName()
добавлена ветка поиска по
виртуальной иерархии -- ДО физической. Любая ненайденность или "ошибка"
поиска по виртуальной -- просто переходит к поиску по физической.
CxsdHwResolveChan()
воспринимает все
CXSD_DB_RESOLVE_-результаты, интерпретируя их должным образом (а CLEVEL и
DEVICE считает ошибкой), и потом сбагривает...
FillPropsOfChan()
раскручивает цепочку
FillPropsOfChan()
.
Этот вопрос еще просто недопроработан.
Получившаяся архитектура -- гремучая смесь: можно сбрендить, пытаясь понять, почему как-то работает (или НЕ работает) некий набор ссылок/редиректов. По замудрённости похоже не только на симлинки, но и на функционирование mount (включая форточный join и *nix'ный union/overlay-mount) и редиректов в shell'е (где важен порядок редиректов).
22.05.2015: есть несколько идеологических загвоздок:
Первая идея -- проходиться по всем namespace'ам (точнее, по обоим namespace'ам каждого экземпляра устройства) и прописывать их target-каналам указанные в них свойства. Но в условиях множественности это выглядит не очень хорошей идеей...
И что теперь -- запоминать еще и "ссылку" на исходное имя (дуплет nmsp:line), через которое канал был адресован? Тоже кривизна. По-хорошему, вообще не должно зависеть от способа адресации АППАРАТНОГО канала. КАК?!?!?!
26.05.2015: сделан парсинг свойств.
ParseCpointProps()
, слизанная с
Cdr_via_ppf4td.c::ParseKnobDescr()
, только вместо
гибкой таблично-плагиновой архитектуры там гвоздями забит парсинг либо
строки, либо double (в последнем случае принудительно взводится
phys_rd_specified
).
FillPropsOfChan()
правильно "собирает" эти свойства
(берёт при неуказанности (текущий_ofs<0), а иначе более не трогает).
27.05.2015:
Проверено, и после исправления нескольких багов -- работает!!! Разные отражения одного аппаратного канала отдаются с разными значениями, отличающимися пересчётом.
CxV4MonitorChunk
нет отдельного поля "cpid" (а в
CxV4CpointPropsChunk
-- есть). 28.07.2015: "косяк" исправлен -- на уровне
cxsd_fe_cx.c, подробности в его разделе.
...да и вообще -- не сократить ли количество chunk-specific полей, свалив
их на стандартный CxV4Chunk
, где зарезервировано 4 uint32?
13.02.2017: попытка воспользоваться
channels
для именования каналов noop-устройства обломилась. С
диагностикой
channel number 0 is out of range [0-0)
Странненько -- ведь работало же...
15.02.2017: пытаемся разобраться -- это
ParseChanList()
.
channels
там стоит 0.
...когда и как проверялось (в мае 2015-го!) -- хбз; возможно, до каких-нибудь крупных изменений; или, например, с подключенным devtype'ом (но это вряд ли).
chan_n_limit
(с которой потом всё и сравнивается): сначала из
*nsp_p, а если указан type_nsp, то из него.
dev_p->chancount
. Прописывается туда в
CxsdDbAddDev()
, вызываемом в конце dev_parser()
'а.
Ну и что делать? Исправлять, чтобы количества проставлялись где надо (а где надо?)?
15.02.2017@вечер-дорога-домой-около-ВЦ: а может проще -- не
выпендриваться с проставлением количеств в разные namespace'ы, а просто
передавать chan_n_limit
параметром из
вызывающего, уж он-то всегда знает, где это количество содержится и может
передать точное значение.
16.02.2017: делаем.
ParseChanList()
: добавляем параметр
chan_n_limit
, а локальную переменную и махинации в начале по её
вычислению убираем.
channels_parser()
: передаётся
devlist[devid].chancount
.
devtype_parser()
: передаётся spc.chancount
.
И оно заработало!
23.04.2018: вылез косяк парсинга -- во вложенных
cpoint-группах не отбрасывается начальное cpoint
. Т.е.,
-- не работает, ругается на вторую строчку «cpoint target "gunctl" not found».cpoint baachi { cpoint gunctl { } }
Разбирательство показало, что косяк в ParseOneCpoint()
: там
в проверке “Allow optional "cpoint" at the beginning of
line” начало строки определяется по basebufused==0
.
Но это неправильно, т.к. во вложенных группах будет гарантированно не 0, а
что-то явно >=2.
Очевидно, что надо проверять не на ==0, а что с НАЧАЛА ТЕКУЩЕЙ строки
ничего добавлено. Т.е., самое простое -- запоминать значение
basebufused
вначале и сравнивать с ним.
24.04.2018: ну делаем...
basebufused_ini
и сравнение уже на равенство с
ней. ...не помогло.
basebuf
вместо basebuf+basebufused
. Исправлено,
проехало дальше.
ppf4td_skip_white(ctx)
.
Теперь заработало.
...хотя, учитывая общую крутость реализованной тогда инфраструктуры cpoint'ов -- простительно.
cpoint cpoint cpoint name target
Чтоб избежать этого, надо б было вместо basebufused_ini
сделать флаг keyword_forbidden
и взводить его либо после
пропуска, либо если это "не оно".
25.04.2018: не, криво -- надо переделать.
Сделано по вчерашнему проекту со взводимым флагом, только флаг назван
"xtra_cpoint_fbd
". А basebufused_ini
убрана.
Это, кстати, и сериализацию/десериализацию упростит, и уберёт ограничение 32-1 символа на максимальную длину имени (точнее, ограничение будет уже длиной буфера в парсере).
...и да, оно как бы чуть усложнит процесс резолвинга (в т.ч. создание БД), но это делается сравнительно редко, и можно пренебречь, в интересах удобства и гибкости.
17.05.2015: подготовительные меры, в виде
функций-аксессоров, чтоб прочие модули поменьше лазили во внутрености
CxsdDbInfo_t
напрямую:
CxsdDbGetStr()
-- по указанному оффсету возвращает
строку (char*). Причём с bound-checking'ом, так что на "неуказанную"
(0/-1) вернёт NULL.
CxsdDbGetNsp()
-- за компанию аналогично для
namespace'ов.
18.05.2015: маленький кусочек переведён --
CxsdDbDcNsp_t.typename_ofs
на замену typename[32]. И всё новое
с Cpnt/Clvl сразу на _ofs'ах.
А дальше пока заломало делать: в CxsdDbDevLine_t
аж 4 штуки
char[32], но как минимум typename[] и instname[] используются в
vDoDriverLog()
. Вставлять туда выкапывание строк -- как-то
криво. Видимо, надо будет иметь прямо в
cxsd_hw_dev_t
char*-указатели, которые заполнять в
CxsdHwSetDb()
-- поскольку к тому моменту strbuf[]
станет фиксированным. После обеда: неа, плохая идея со
складированием указателей. Вдруг когда-нибудь "потом" список cpoint'ов
станет меняемым по ходу дела (или мало ли почему еще strbuf[] поменяется) --
всё поедет... Для оптимизации уж лучше
CxsdDbGetStr()
переделать в inline.
25.05.2015: делаем.
options
и auxinfo
.
При этом заодно упростилось как добавление устройства, так и удаление БД,
поскольку раньше это были strdup()'нутые строки.
Теоретический потенциальный минус: даже при обломе ОБЩЕГО добавления строки в БД попадут и там останутся. Таково уж свойство strbuf[]'ных строк. Но это минус именно теоретический: по факту вряд ли где и когда на него наткнёмся (учитывая смешные объёмы).
26.05.2015: добиваем:
CxsdDbLayerinfo_t
.
CxsdDbDevLine_t
'овыми свойствами instname, typename,
drvname, lyrname пришлось повозиться, да. В т.ч. к
CxsdDbAddDev()
добавлена четвёрка параметров -- чтоб она
аллокировала эти строки, а не вызывальщик.
cxsd_hw_lyr_t
'шное lyrname.
Вполне straightforward, даже сократилось кое что -- запись имени при отведении lyrid'а превратилась в копирование lyrname_ofs'а (только tolower()'енье оттуда ушло -- а хбз, зачем оно было нужно...). Хотя разнообразных ссылок на это lyrname чуть ли не больше, чем остальных, вместе взятых.
Засим с одной стороны считаем дело сделанным, но с другой надо держать в уме...
...Замечание: эта архитектура с strbuf[]/dictionary при всей своей как бы простоте содержит потенциальную ловушку: при добавлении любой строки все добытые из "словаря" указатели становятся невалидными. Т.е., НИ В КОЕМ СЛУЧАЕ нельзя давать доступ к добавлению строк драйверам (например, для передачи всяких свойств -- те надо аллокировать отдельно, в других местах). Или вообще после "realize" делать БД "readonly".
27.05.2015: да, поле
CxsdDbInfo_t.is_readonly
добавлено, оно взводится в =1 в
CxsdHwSetDb()
, а все CxsdDbAddNNN()
проверяют и
возвращают -1/EROFS при уставленности.
29.05.2015: сделано. ...но по факту cxsd_builtins.c и stand.c его включают, поскольку сами регистрируют модуль загрузки БД...
Заодно и cxsd_hwP.h, ровно в тех же целях -- чтоб "клиенты" (cxsd, stand, pult) включали себе только публичный файл, с высокоуровневыми API, а "потроха" -- только для своих.
CxsdHwSetDb()
?
Пусть это делает CxsdDbCreate()
прямо при
создании БД, там это раз плюнуть.
(Первая идея была повесить это на
CxsdDbAddDev()
, автоматически при добавлении первого же
устройства. Но тогда пустой конфиг не будет иметь никаких устройств вообще,
даже 0-го, что плохо.)
Выигрыш очевиден:
CxsdHwSetDb()
, да и совершенно нелегитимный в
cxsd_hw.c.
20.05.2015: сделано. Очень просто. И работает.
...другое дело, что глубокого смысла в этом и нет -- обычно либо/либо.
16.09.2016: пара идеологических замечаний:
Да, сейчас у нас в 4cx/hw4cx/sw4cx таких случаев нет, но это не значит, что они не возникнут в будущем.
Некий теоретический смысл может быть для каскадирования layer'ов (в EPICS'е это используется); но вот как реализовывать (уже в cxsd_hw) -- тут некоторый вопрос.
CxsdDbAddStr()
к CxsdDbAddMem()
: при записи
терминатора '\0' оно его писало не в strbuf[retval+len], как
надо, а в strbuf[retval+len+1].
Как вообще всё работало -- большая загадка. Похоже, первый аллокированный килобайт (или килобайтЫ) был заполнен нулями, и везло.
FindDevice()
опубликована под именем
CxsdDbFindDevice()
-- чтоб
cxsd_db_via_ppf4td.c::dev_parser()
мог обнаруживать
дублирующиеся имена устройств и ругаться сразу, не дожидаясь вызова
CxsdDbAddDev()
(ко времени которого контекст парсинга может
уйти далеко и ругательство будет слабо-адресным).
...кстати, до сегодняшнего дня и в CxsdDbAddDev()
тоже
проверка отсутствовала.
dev_parser()
был кривоват: оно
берёт текст ppf4td_get_string()
'ом, в результате чего кавычки
пропадали и на вход paramstr_parser'а поступала уже непарсимая строка
(вылезло при реализации formula_drv.c).
Проблема решена свежевведённым PPF4TD_FLAG_IGNQUOTES
.
Где именно косяк -- неясно.
ppf4td_nextc()<0
.
ppf4td_pipe_close()
: он
ВСЕГДА возвращает 0, хотя должен бы анализировать exitcode.
...неа, не он, а result_of_xxxxc()
: в нём стоит проверка на
тему статуса после waitpid()
.
Странно то, что раньше (когда? точно ли?) ошибка отлавливалась, и даже где-то тут в файле есть замечание про то, что в RH-7.3 старый m4 не возвращал exitcode!=0 при ненайденности, а в RHEL5 (кажется) уже возвращает.
20.12.2017: разбираемся.
IsAtEOL()
ли.
И при !=0 переходит на SKIP_TO_NEXT_LINE. А !=0 -- это и -1 тоже, так что
ошибки в этой точке игнорируются и считаются за пустую строку.
if (ppf4td_peekc(ctx, &ch) <= 0) goto END_PARSE_FILE;
-- т.е., и ошибка чтения тоже будет считаться концом файла, по коему БД
(пустая) считается успешно считанной.
ppf4td_m4_open()
НЕ проверяется существование файла,
а просто вызывается ppf4td_pipe'ное открытие.
ppf4td_pipe_open()
файл проверить уже не может -- не
её это дело, она просто делает fork()+exec() с указанной командной строкой.
И оный exec() отрабатывается БЕЗ ошибки (т.к. программа-то запустилась), а ошибка будет обнаружена уже далее.
result_of_xxxxc()
, но на неё никто толком не смотрит:
ppf4td_nextc()
, хотя и возвращая exitcode=1.
Это утверждение -- про обнаружение ошибки -- тоже надлежит проверить, но уж скорее всего оно так.
findfilein()
, который бы заметил отсутствие файла (точнее,
никакого бы файла не нашёл).
Возможно, так было где-то раньше (в v2?)?
cxldr_get_module()
так устроен, да и как иначе-то).
reference
на открывабельность.
if (access(reference, R_OK) != 0) return -1;
-- в ppf4td_m4_open()
, и аллилуйя!!!
ppf4td_cpp_open()
, для порядку (вроде бы
cpp
не занимается поиском -- это проверено
strace
'ом).
30.08.2018: как продолжение истории: аналогичное поведение при отсутствующем m4 -- ругань на stderr, но наверх ошибки не возвращается и создаётся нулёвая БД.
Обнаружилось на машине canhw (бывшей canhw-future), где пакет m4 отсутствовал.
Очевидно приходящие в голову мысли:
ENOEXEC
(просто ENOENT нельзя: тогда будет неразличимо отсутствие файла и
интерпретатора).
execv()
поиска НЕ делает, в
отличие от p-версий, вроде execvp().
ppf4td_m4_open()
и
ppf4td_cpp_open()
, вместо общей
ppf4td_pipe_open()
?
ppf4td_pipe_open()
.
...а вот фиг: если путь к интерпретатору однозначен -- cmdline[0], то сам "reference" -- формально не обязательно должен быть последним в списке cmdline[].
Так что сие -- увы, остаётся только "withdrawn"...
01.09.2018@дома: вставлена
проверка по X_OK в ppf4td_pipe_open()
.
Работает.
CxsdDbResolveName()
'ом:
Т.е. -- чтоб работало В ТОЧНОСТИ как '/' в именах файлов (за вычетом только ':' -- аналога "../").
19.12.2017: пора.
dot_p[1]=='.'
) и для аппаратной (проверка по
*after_d=='.'
).
while(*name=='.')name++
" -- т.е.,
модифицируется сам указатель name
.
Засим можно считать задачу исполненной.
strtol(,,10)
. В результате оно НЕ понимает числа с
префиксом 0x и НЕ позволяет 16-ричные данные. (Вылезло при попытке указать
0x06 в качестве адреса для ADC4X250 на bivme2.)
14.07.2017: исправляем на 0
:
dev_parser()
-- то самое место.
layerinfo_parser()
-- чтоб "bus-number" тоже можно б
было.
20.12.2017: главная неясность -- где косяк: то ли в cxsd_db_via_ppf4td.c (в чём бы?), то ли где-то в недрах PPF4TD.
21.12.2017: чуток анализа:
ReadHWConfig()
никаких
деталей не выдаётся потому, что CxsdDbLoadDb()
при ошибке лишь
возвращает NULL
, но никакого описания не даёт.
CdrLoadSubsystem()
возможность возвращать ошибки
имеет, через CdrSetErr()
+CdrLastErr()
, и для
"unknown scheme" это делает.
errno
+ppf4td_strerror()
, но их выдаёт на stderr
сам cxsd_db_via_ppf4td.c (что не вполне правильно).
Вывод: явно надо вводить API вроде "CxsdDbLastLoadErr()
".
auxinfo_buf[]
в
cxsd_db_via_ppf4td.c::dev_parser()
, с былого 1000
сразу до 4000 (чтоб уж с запасом).
Причина -- что стали не влазить формулы в devlist-canhw-19.lst:
там у "вычислителей" (в частности, d16s_fin_writer
) они
довольно длинные, т.к. делается куча однотипных операций с для толпы
устройств.
Кстати, есть воспоминание, что вроде уже увеличивал (до 2000?). Но следов нигде не видно (ни в коде, ни в 0002.html), и для какого devlist'а бы это делалось -- тоже неясно, нет таких других. Загадка? Склероз? Маразм?
18.06.2019: побудительный мотив: ЕманоФедя умудрился попробовать сделать устройство с '-' в имени, и в результате получил совершенно дикую диагностику -- что-то на тему "chan-group-0 should start with either 'r' or 'w'".
Причина явно в том, что у парсера несколько съехала крыша (точнее, съехало понимание -- ЧТО именно он сейчас парсит). Строку вида
dev name-components typename...
он, очевидно, воспринял как
dev name -components typename...
т.е., дефис был понят как указание симулировать конкретно данное устройство,
а в "components" было воспринято уже как название типа устройства. После
чего "typename", сочтённое за список групп каналов, и вызвало ошибку.
Как решить проблему: надо ПОЧТИ повсеместно заменить
"ppf4td_skip_white()
" на некий внутренний вызов, который бы:
ppf4td_skip_white()
и возвращал бы
0.
Замечания:
То ли просто разрешать конец строки (всё равно дальнейший код должен уметь его обрабатывать), то ли ввести флажок "конец строки допустим".
ppf4td_skip_white()
должен
заменяться на вызов с проверкой результата -- чтобы отваливать.
Т.е., чтобы "контейнер" в иерархии являлся бы также и "оконечным узлом" (точнее, пред-оконечным -- устройством).
Сейчас же cpoint'ы реализованы как "иерархическая файловая система", что даёт бонусы с точки зрения возможности "делать cd в под-иерархию и пользоваться относительной адресацией".
Надо ли думать в сторону реализации подобной возможности -- вопрос. Пока вроде и так обходимся.
03.07.2019@после-обеда-дома, дорога пешком на работу, около ИЦиГ: как вариант -- сделать ТРЕТЬЮ иерархию, "alias'ную", которая бы работала именно по самому старому проекту cpoint'ов (просто строковая замена).
Некоторые "нюансы":
alias
. Формат --
alias NAME RESOLUTION
CxsdDbResolveName()
,
надо на тему "а не начинается ли имя на какой-нибудь из alias'ов".
Естественно, проверять только по ЦЕЛЫМ словам -- т.е., либо совпадает с alias'ом, либо после него идёт '.'.
Т.е., под alias "a.b.c" имя "a.b.c.d" подойдёт (и заменится на RESOLUTION.d), а имя "a.b.cd.e" -- нет.
alias a b
" плюс "alias b a
") надо
ограничивать число итераций резолвинга -- как это делает ядро для симлинков,
где ограничение в районе 128.
Технически такое реализовать вроде не очень сложно (с единственной загвоздкой в "сформировать новое имя"). Вопрос -- НАДО ЛИ? Эта анархия прилично подсломает нынешнюю элегантную схему cpoint'ов.
03.12.2019: чуток обсуждения:
А реквестируемое ЕманоФедей использование "как описателя классов", a-la ООП, и близко не предусматривалось.
CxsdDbDcLine_t
'ах в
CxsdDbDcNsp_t
'ах).
CxsdDbDcLine_t
возможность кроме devchan_n
содержать также и остальную пачку
свойств (возможно, даже включая {R,D}). А поиск-раскручивание пусть умеет
использовать эти свойства ровно так же, как и от cpoint'ов: если уровнями
выше они не указаны, то взять отсюда.
CxsdDbDcLine_t
'ов, а не у каждого индивидуально.
...но как это указывать синтаксически -- совершенно неясно.
adc<0-23> 100 adc_p10v adc20 adc_0v adc21 adc_t adc22 adc_pwr adc23
И как поступать с определением свойств ТАКИХ ссылочных каналов?
По-хорошему -- тоже наследовать: если у adc_p10v что-то не указано, то пытаться взять оное у adc20; очевидно, прямо в момент определения.
CxsdDbResolveName()
.
Он возвращает сразу числовой channel-ID (chann), и НЕ производит никакой "раскрутки".
FillPropsOfChan()
,
получающий на вход уже готовый channel-ID (могущий быть как cpid'ом, так и
gcid'ом).
И он, в свою очередь, уже НЕ имеет никакого знания об использовавшихся при резолвинге devtype'ах.
Как сие противоречие разрешить -- не вполне ясно.
Напрашивается только не очень красивая идея возвращать из
CxsdDbResolveName()
также и информацию от devtype'а --
CxsdDbDcLine_t
и/или
CxsdDbDcNsp_t
,
Оба варианта выглядят не шибко элегантно -- как минимум тем, что нарушается принцип фрагментации/инкапсуляции (что каждый модуль должен заниматься СВОИМ делом).
А что, если для хранения "свойств аппаратного канала,
указанных в строке devtype'а" использовать прямо структуры
CxsdDbCpntInfo_t
?
Тогда:
devid
и ref_n
не
используются, а должны (на всякий случай) содержать специальные значения --
например, 0.
phys_rds[2]
и phys_rd_specified
-- сразу решается вопрос о возможности
указания R,D прямо в devtype.
typedef
-alias
-- например,
typedef CxsdDbCpntInfo_t CxsdDbDcPrInfo_t
(Напоминание: "Dc" -- Device Channel; см. за 18-05-2015.)
CxsdDbInfo_t
.
Т.е., единый массив на всю БД.
Обозвать поле, например, "dcprs
" (по аналогии с
cpnts
-- отрезаем имени типа префикс "CxsdDb" и суффикс
"Info_t", плюс добавляем "s").
CxsdDbDcLine_t
указывать ИНДЕКС (номер)
соответствующей структуры в этом едином массиве.
CxsdDbResolveName()
мог бы возвращать оный
индекс в дополнение к chann.
CxsdDbDcLine_t
'ах использовать то же правило.
Итого: возможный механизм реализации ЕманоФединых хотелок есть.
Надо только покрутить это всё в мозгу, чтобы понять, нет ли каких подводных камней; или, наоборот -- более элегантного способа реализации.
03.12.2019: а вот, в ту же степь -- соображение
"географически"-близкое: за 25-10-2018 было высказано пожелание
«уметь бы всё-таки свойства типа "autoupdated" указывать в конфиге
(видимо, в channels
/devtype
)».
И это свойство явно должно храниться рядышком с прочими phys_rds и строками.
03.12.2019, подумано в разное время:
CxsdHwSetDb()
.
FillPropsOfChan()
, о чём утром не стал записывать), а
корректнее их копировать прямо в свойства канала в
cxsd_hw_channels[]
-- ведь это именно "предварительная замена"
для того, что сообщает драйвер.
@вечер-дома: слова
"CxsdDbInfo_t
" в cxsd_fe_cx.c не видно.
10.12.2019: и "CxsdDbCpntInfo_t
" тоже не
видно, да и вообще -- похоже, что обмен данными идёт
исключительно в терминах его собственных Guru*Chunk
, а
структуры CxsdDb*_t
в передаче информации между экземплярами
серверов никак не используются.
10.12.2019: приступаем к работам, по проекту недельной давности.
CxsdDbDcPrInfo_t
-- копия
CxsdDbCpntInfo_t
с убранными devid,ref_n и добавленными
phys_rd*, range* и return_type.
CxsdDbInfo_t
добавлено поле dcprs
(+_used,+_allocd).
CxsdDbDcLine_t
-- поле dcpr_id
, которое
если >0, то содержит номер dcpr'а в общем массиве dcprs[]
.
safe_free(db->dcprs)
.
CxsdDbAddDcPr()
, скопированная с
CxsdDbAddCpnt()
с тривиальной заменой "cpnt" на "dcpr".
Кстати, где может быть идеологическая собака зарыта:
И может оказаться, что для ОДНОГО физического канала свойства указаны НЕСКОЛЬКО раз (при разных именах). Придётся этот момент как-то отлавливать -- правильнее всего прямо при чтении БД.
11.12.2019@утро-дома-зарядка: собственно, то и есть ГЛАВНАЯ идеологическая проблема: свойства определяются в devtype НЕЕСТЕСТВЕННЫМ образом, т.к. они определяются при ИМЕНИ, а не при "канале" (номере).
Т.е., имеем совершенно неприличную денормализацию.
Оно так просто в силу того, что devtype задумывался именно как список имён (a-la DNS), ДОПОЛНИТЕЛЬНЫХ к собственно сущности каналов; а используется -- как "описание типа-класса", где по-хорошему должен бы идти авторитетный список "КАНАЛ (номер), СВОЙСТВА".
Можно ли как-то переделать синтаксис, чтобы он отражал эту новую суть использования? А хбз...
11.12.2019@утро-дома-~11:45: раз уж мы хотим делать возможность указывать диапазоны прямо в devtype'ах, а Федя наверняка захочет этим воспользоваться и указывать их своим noop-каналам...
...то пусть StdSimulated_rw_p()
для числовых
скаляров проверяет диапазоны, и если указан -- то вгоняет в него. Тогда
noop_drv автоматически станет "уважать" диапазоны.
Запись -- операция редкая, так что на потенциальные затраты времени на исполнение проверок можно забить.
11.12.2019@дорога-на-обед-около-ИПА: кроме строк, R,D, диапазонов и return_type можно также разрешить указывать и fresh_age.
Только парсер придётся сгородить специфический: SECS.NSECS -- т.е., ЦЕЛОЕ, затем точка, затем ещё ЦЕЛОЕ.
Пара замечаний:
nnn.fresh_age.sec=-1
.
@вечер: и тогда, судя по
cxsd_driver.h'ову списку SetChan*()
, неохваченным
остаётся только квант.
Не вполне ясно, зачем бы; но технических проблем -- ноль: указывать T:VALUE.
13.12.2019: да, и поле для кванта тоже добавлено, как и строка для него в таблице парсинга.
12.12.2019: продолжаем:
CxsdHwSetDb()
добавлено "наследование" свойств из БД.
Для этого на stage=1 проходится по обоим namespace'ам (type_ и chan_), и
по каждой строчке смотрит, что если еёйный dcpr_id
>0, то для
указанного в этой строчке канала копирует в описатель канала те свойства,
что указаны.
13.12.2019: также добавлено проставление ссылки на этот dcpr.
FillPropsOfChan()
добавлен "учёт" строк
из каналова dcpr'а, если оный у канала указан.
SFT_*
: DBSTR, DOUBLE, INT, FLAG (это
для return_type), RANGE, ANY (для quant), TIME.
somefielddescr_t
.
spc_f_ofs
: если оно >0, то в нём
указывается оффсет int
'а, в который надо сделать =1 в случае
указанности данного параметра.
Смысл -- чтобы взводить phys_rd_specified
при указании R
и/или D, безо всякой лишней магии (а то в "CFD"
ParseCpointProps()
взводит его принудительно -- следсвие
ориентированности на конкретный тип).
var_int
, могущий использоваться в разных целях
разными типами.
Смысл -- чтобы у FLAG указывать значение (должное быть прописано в егойный int), а у RANGE и QUANT -- оффсет поля dtype, прилагающегося к значению (в "CFD" этого вовсе не было; хотя там опять можно б было обойтись знанием конкретного типа).
SFD_nnn()
.
ParseSomeProps()
.
ParseCpointProps()
, ...
ctable
везде заменён на
table
.
is_string
сменена на проверку
именно type
.
phys_rd_specified=1
для не-is_string
удалено, ...
spc_f_ofs
, если оный >0.
19.12.2019: в конечном итоге решено пока не делать, ибо смысла в указании для noop-каналов чего-либо, помимо IS_AUTOUPDATED_NOT, просто нет, а парсинг бы очень сильно усложнился.
CxsdDbResolveName()
'ом информации, а держать его прямо в
cxsd_hw_chan_t
.
12.12.2019@пультовая ~15:10,
вместо четверговой планёрки: как показал анализ конечных "юзеров"
CxsdDbResolveName()
-- а это, помимо самого cxsd_hw, также и
cxsd_fe_cx (и, вероятно, прочие frontend'ы) -- эта самая денормализованность
нам вылазит очень большим боком.
А именно:
CxsdHwResolveChan()
-- и да,
там есть как резолвинг, так и заполнение.
CxsdHwGetCpnProps()
, где никакого резолвинга уже нет, а просто
указывается заранее добытый cpid.
Работу по заполнению (и "раскручиванию" цепочки cpoint'ов) выполняет
FillPropsOfChan()
, получающий уже готовый cpid.
(Да, теоретически этот вызов с не-NULL-параметрами должен бы
делаться только по событию STRSCHG, которого в реальности никогда не бывает
(источника его генерации в сервере нет). Но конкретно cxsd_fe_cx.c
использует его же и при начальном резолвинге -- просто там отделён резолвинг
от добычи и отправки свойств, поскольку последняя выполняется
непосредственно в PutStrsChunkReply()
, производящей подготовку
Chunk'а к отправке.)
Откуда напрашивается мысль: прописывать dcpr_id прямо в
cxsd_hw_chan_t
конкретного канала.
При этом, правда, исчезнет "множественность": если на канал указывают разные строки в devtype/channels, то эффективно работать будет только последняя, а никакого "наследования" не будет.
@~16:15, уже у себя в 613-й:
откуда напрашивается вывод: ну и прописывать эти строковые свойства -- 8
штук string-offset'ов -- прямо в cxsd_hw_chan_t
! Тогда
денормализация останется только в исходном синтаксисе, а в результирующей
активированной БД исчезнет.
...правда, это плюс 8 int'ов, т.е. +32 байта к каждому каналу, и в основном впустую...
13.12.2019: да, сделано: добавлено поле
cxsd_hw_chan_t.dcpr_id
, куда в CxsdHwSetDb()
проставляется ссылка. И использование его при заполнении свойств каналов
тоже.
Итак, в конце концов доделано до долженствующего работать состояния -- проверяем.
Исправлено -- добавлена инициализация всего чего можно.
13.12.2019@~16:30, семинар Барнякова о детекторе для C-Tau: несколько мыслей:
strtoll()
!).
Напрашивается 2 варианта:
Симпатичнее выглядит второй вариант.
16.12.2019@лыжи, ~13:00: неа,
экономичнее будет всё же сделать по первому. Надо будет выделить
char*-переменную subfield_name
, куда запихивать "", "-min" и
"-max", плюс на итерации stage==1 принудительно ждать '-'.
14.12.2019@утро-дома-зарядка:
Надо ВСЕГДА брать его от канала.
Чуть поразмысливши: только немножко повозиться придётся -- в момент
чтения БД никакого cxsd_hw_channels[] ещё нету, а есть только changroups[],
да и то даже НЕ у ParseChanList()
, а лишь у его вызывальщика.
Придётся как-то оное передавать вглубь и вглубь, чтобы у
ParseSomeProps()
был бы dtype его target-канала.
15.12.2019: движемся в нужном направлении.
16.12.2019: оказалось -- тупой косяк: в сохранении
выпарсенного значения для REPR_INT''ов использовалась d_v
(вещественная) вместо l_v
(long). Вот оно и записывало
последнее записанное в d_v
значение -- R либо D.
17.12.2019: продолжаем двигаться.
strtoull()
.
*((int16 *)ea) = l_v
" сделано в стиле
"a_p[0].i16 = l_v
".
(Оно CxAnyVal_t *a_p
, и a_p = ea
.)
int subfield
и
const char *subfield_name
: по первой делается цикл, а во второй
в начале цикла помещается имя пол-поля для выдачи сообщений об ошибках.
a_p[0]
, а
в a_p[subfield]
.
RNG_fparser()
(с которого, как думалось 12-12-2019, "явно
будет браться многое" -- фиг!).
Отличие в том, что тут весь парсинг идёт напрямую из ppf4td'шного потока, а там -- сначала ЦЕЛИКОМ ВСЁ поле засасывается в буфер, а уж потом парсится из него.
range:d:-100000,+100000
будет всосано ВСЁ значение "-100000,+100000", а вовсе не только "-100000".
PPF4TD_FLAG_DASTERM
, чтобы '-' можно б было объявлять
терминатором.
Но тут же дошло, что '-' может быть вполне легальным символом в числе -- как знаком в первой позиции, так и знаком экспоненты далеко в глубине.
...и на этой горестной ноте пошёл домой...
ppf4td_get_double()
, с полным разбором выражений и скобок
(алгоритм Дийкстры, "сортировочной станции")?
strtod()
'шной
man-странице)?
Тоже, конечно, не сахар.
И, кстати, такую штуку в любом случае придётся реализовывать для парсинга по Дийкстре -- в качестве внутренней ppf4td'шной функции, используемой как для просто парсинга "токенов" (чисел в выражении), так и для случаев, когда первый символ в потоке НЕ является '(' (т.е., признаком, что дальше идёт именно выражение, а не просто число).
ppf4td_get_int()
надо будет и
ppf4td_get_quad()
-- для int64...
RNG_fparser()
, всасывая ВСЁ
значение поля в буфер, а потом разбирая уже его локально. Т.е., именно
ровно так, как в RNG_fparser()
:).
Для чего нужно будет ДО цикла сделать "всасывание", а в цикле -- текстовый разбор.
18.12.2019: ну, продолжаем.
PPF4TD_FLAG_DASTERM
(вёрнуты старые копии ppf4td.[ch]).
Сделано, работает. Ура!
Тут ОЧЕНЬ многоуровневая цепочка:
ParseSomeProps()
'а.
ParseChanList()
;
ParseCpointProps()
(в ближайшем будущем).
channels_parser()
и
devtype_parser()
.
CxsdDbDevLine_t
, а у второго в CxsdDbDcNsp_t
.
ParseChanList()
надо передавать дуплет
changrpcount
,changroups[]
.
(...мда, зело извратно получается -- некрасиво, однозначно. Но переделывать на что-то более элегантное (вроде унификации места хранения информации о группах каналов) не хочется -- и пустые затраты времени, и вообще вся идея с "позволять указывать свойства в devtype" не слишком элегантна, так что чего уж...)
Итак:
channels_parser()
и
devtype_parser()
-- берут changrpcount,changroups[] каждый из
своего источника и передают в...
ParseChanList()
, которому добавлена пара параметров.
ParseChanList()
сделана собственно "добыча" dtype.
refval
.
Так что проблема как бы полу-искусственная, но некрасивость всё же имеется -- вот так и проявляется денормализация.
ParseSomeProps()
, которому добавлен дополнительный параметр.
Проверено -- да, тоже работает, диапазоны и квант получают правильный тип от исходного канала (первого).
И да: проверено, что при пересечении диапазоном имён границы changroup'ов каналы второй группы получают свойства с типом от первой (ну ещё бы! с чего б было иначе?).
ParseCpointProps()
, с собственным
парсингом, заменён на простой вызов ParseSomeProps()
.
Итого:
StdSimulated_rw_p()
добавить учёт
диапазонов.
19.12.2019: научаем StdSimulated_rw_p()
уважать диапазоны.
(Записываем здесь, чтобы уж всё рядышком.)
(теперь это она РАНЬШЕ так выглядела :D)if (action == DRVA_WRITE && cxsd_hw_channels[gcid].rw) { nvp = values[n]; nel = nelems[n]; }
Поскольку именно ЭТО -- место, где обрабатывается запись.
nel = nelems[n]
в любом случае необходимо,
поэтому оно вытащено в самый верх.
nel == 1 && dt == cxsd_hw_channels[gcid].range_dtype
nvp = values[n];
.
if (dt == CXDTYPE_nnn) { } else if (dt == CXDTYPE_nnn) { } ... else nvp = values[n];
v.ПОЛЕ
значение из values[n].
Вообще-то это довольно громоздко, поэтому для начала реализовано только для int8 (первый в списке, "модель") и для int32 (уже для реальной проверки)
Этак диапазон [0,0] будет считаться "указанным" и вгонять всё в 0.
Посему -- нужно дополнительное условие, в КАЖДОЙ ветке.
if()
с проверкой типа (т.к. так отсутствие диапазона приведёт к
совсем иному результату), а оно должно быть уже ВНУТРИ ветки по данному
типу.
@ИЯФ, ~16:20: а вот и нет --
МОЖНО объединять, поскольку в таком случае ход исполнения "просколььзнёт"
дальше, последовательно пройдя мимо всех следующих проверок типов (т.к. ни
одна не сработает) и дойдёт до
"else nvp = values[n];
"
-- что, в сущности, даст ровно тот же результат. Другое дело, что это а)
неочевидно (типичный приёмчик "индусских программистов"); б) не факт, что
эффективно.
Надо делать макрос, которому передавать только имя поля!
И, поскольку он весьма многострочный, содержимое должно быть в "скобках"
do { ... } while (0)
, чтобы красиво смотрелось в
if() ... else if()
.
DO_FIT_INTO_RANGE()
.
Ему передаётся ДВА параметра: кроме имени поля (напр., i32
)
ещё и имя типа (напр., int32
) -- чтобы было к чему кастить
values[0]
.
...была мысль кастить к CxAnyVal_t
и брать от него то же
поле; но что-то застремался ("а мало ли как реально в AnyVal поля разных
типов располагаются -- вдруг не с 0"...).
StdSimulated_rw_p()
с
давних пор знаковые и беззнаковые типы обрабатываются вместе: НЕТУ полей
вроде "u32"...
Пришлось переделать, чтобы были -- теперь знаковые и беззнаковые "раздельно".
(Хотя из-за наличия в тестовом devlist'е значения R=123.45 диапазон -100000-+100000 "оканчивался" на 810.045 -- я приподофигел, думал, что где-то типы путаются :-D.)
Вот на этом проект уже реально можно считать завершённым.
...хотя в ближайшем будущем будет модификация в парсинге -- когда внедрим
ppf4td_get_double()
(тогда же и RNG_fparser()
и
ещё много где придётся переделывать). 30.03.2021: да,
переделано ещё тогда весной с год назад.
30.03.2021: факт битым текстом: ПАРСИНГА fresh_age
тогда сделано так и не было. Вот ИСПОЛЬЗОВАНИЕ в CxsdHwSetDb()
-- да, но ему достаётся всегда sec=-1, прописываемое при инициализации перед
парсингом. 07.05.2022: сделано.
07.05.2022: возвращаемся к этому блоку парсинга:
modf()
разбивается на целую и дробную части, и
полю nsec
присваивается time_frac * 1000000000
.
Для этого непосредственно перед возвратом прочитанных в
keybuf[]
символов обратно во входной поток выполняется
дополнительная попытка найти совпадение в table[]
, но тут
подходят только строки с типом SFT_FLAG.
Кстати, по исходнику -- и по общей структуре, и конкретно по
куску проверки, не начинается ли строка с "FIELDNAME:" -- чётко видно, что
ParseSomeProps()
, в девичестве ParseCpointProps()
,
слизана с Cdr_via_ppf4td.c::ParseKnobDescr()
. Что
ЯВНО сказано в комментарии за 26-05-2015.
11.05.2022: ага, и заодно сломал обычную работу
"последовательного" парсинга (fn>=0) -- из-за того, что при поиске
возможного совпадения использовалась переменная idx
, после чего
она ставилась на SFT_END, последующий код думал, что она указывает на
найденную строку. (Вот не зря меня прямо ломало при реализации той
доп.проверки -- видать, чувствовал, что как-то кривовато всё.)
Исправлено путём "idx = -1
" при НЕуспешном поиске SFT_FLAG'а.
Иные исправления:
changroups[]
.
Он туда был помещён ошибочно (вот просто промахнулся с выбором нужной точки в исходнике), в результате чего действия повторялись столько раз, сколько changroups[]'ов. Оно как бы не страшно -- никаких побочных эффектов, вроде аллокирования памяти, сие не давало -- но просто криво.
13.05.2022: убрано
MAY_USE_PPF4TD_GET_DOUBLE
-- теперь
ppf4td_get_double()
и ppf4td_get_quad()
ВСЕГДА
считаются доступными и используются.
15.08.2023: есть проблемка -- не обнаруживаются дубли, когда у НЕСКОЛЬКИХ ссылок на один канал указываются свойства, то отрабатываются ПОСЛЕДНИЕ.
Надо бы хотя бы печатать WARNING'и. Но вот где и как это сделать...
(Задача возникла после того, как по ошибке указал в modbus_tcp_iu.devtype НЕСКОЛЬКИМ именам одинаковые номера каналов -- так возникли дубли drvinfo, из-за которых каналам указывались не те адреса регистров (приоритет имели те, что позже).)
16.08.2023: посмотрел я на то, как устроено всё с dcpr'ами (сохранение в них указанного в devlist'е, а затем актуализация в hw) -- а никак! Там, конечно, всё сделано элегантно на грани гениальности:
out<0-7>
",
указывается один и тот же dcpr_id),
CxsdHwSetDb()
идётся по обоим namespace'ам устройства
(type,chan) и каналу, указанному в очередной строчке namespace'а,
уставляются указанные в dcpr'е свойства.
В т.ч. впрямую прописывается
chn_p->dcpr_id = dcpr_id;
CxsdHwGetChanAuxs()
ВСЕГДА берёт drvinfo из
каналова dcpr'а, если оный
cxsd_hw_channels[gcid].dcpr_id > 0
.
Пока единственное, что приходит в голову -- в момент
ЗАПИСИ dcpr_id в строчку namespace'а пробегаться по всем предыдущим из него
же и проверять, нет ли строки с тем же devchan_n
и при этом
dcpr_id>0
.
17.08.2023: сделал. Именно тем способом. Поиск сделан
FindNspLineChanWithDcpr()
, возвращающей номер уже имеющейся
nsline с таким же devchan_n
и определённым dcpr_id
(так что диагностика печатает имя предыдущего канала, где было определено).
Формально можно оптимизировать, убрав "лишний" проход по namespace'у, т.к.
неподалёку перед ним есть поиск дублей посредством
CxsdDbNspSrch()
-- их можно объединить в один цикл, но придётся
передавать слишком много параметров.
И это дублирование считается не warning'ом, а ОШИБКОЙ -- на нём парсинг БД прекращается.
Проверил на всех имеющихся sw4cx/configs/devlist*.lst -- проблем нет. Также проверил на пульту, где добавляются еманофедины devlist-cxout-*.lst -- тоже проблем этого вида не появилось.
17.12.2019: делаем. Вся работа сконцентрирована в
ParseChangroup()
.
Немного предварительного анализа:
ParseChangroup()
'у, оба
эти символа поступают нормально -- поскольку буфер парсится через
ppf4td_get_string()
, а у неё нет TERM-флагов ни для плюса, ни
даже для минуса (дефиса).
Собственно сделано:
Если "да", то он сохраняется в sg_char
, после чего в
ut_char
считывается следующий символ.
По умолчанию же sg_char='\0'
, что означает "(без)знаковость
не указана".
rec->dtype
делается
проверка: если знаковость указана, но тип НЕ-REPR_INT, то ругается на
бессмысленность указания такому типу знаковости.
rec->dtype |= CXDTYPE_USGN_MASK
.
Собственно, всё. Проверено (с помощью "cdaclient -DH") -- работает.
Немного комментариев:
CxAnyVal_t
?) в
CxsdDbDcPrInfo_t
; 2) что с ВЕКТОРАМИ (да и со строками тоже)?
3) а ещё надо ПОВТОРЯТЬ инициализацию при оживлении устройства (точнее, при
переходе из OFFLINE в любое другое состояние); а надо ли?
15.04.2022@утро, просыпаясь: неа, повторять при оживлении -- НЕ надо. Ведь вообще весь этот механизм нужен не для абы каких устройств, а только для "виртуальных" -- вроде noop'а; а там предполагается, что должны сохраняться значения, прописанные юзером. Собственно, даже сама идея дергать состояние устройств-"почтовых ящиков" -- совсем бессмысленна. А соображение "надо бы повторять..." возникло исключительно потому, что при реинициализации каналы становятся невалидными.
@~19:00, по пути домой мимо ИХБФМ: Псевдопроект:
CxsdDbDcPrInfo_t
иметь int-поля "defval_ofs" (по
умолчанию =-1) и "defval_nelems"; это позволит указывать также значения с
nelems=0.
CxsdHwSetDb()
; при
инициализации канала надо ему также прописывать текущий timestamp.
21.04.2022: с timestamp будет проблема: в
InitDevice()
делается RstDevTimestamps()
. А
InitDevice()
вызывается и при начальном оживлении, и по записи
в _devstate. Корень проблемы тут в том, что работа получается
разделённой между CxsdHwSetDb()
и инициализацией устройства.
Что делать? Видимо,
InitDevice()
.
InitDevice()
добавить ещё параметр "первый ли раз
вызывается" -- чтоб при повторных вызовах оно б не трогало текущие значения.
Но тут будет проблема с тем, что прямой/готовый доступ к
инициализационным данным из БД есть именно у CxsdHwSetDb()
; да
и вообще как-то громоздко получается.
Неа -- не так, а проще:
CxsdHwSetDb()
.
...хотя можно выполнять её в момент "закрытия" добавления?
И вот во втором случае вполне можно реализовать и проверку на дублирование. Да и вообще наличие структуры -- как-то полезнее.
...но будут накладные расходы -- особенно заметные при хранении небольших значений (хотя вряд ли тех будет много).
В общем, КАК делать -- примерно понятно, чистое ремесленничество и никаких принципиальных проблем.
Вопрос в том, БУДЕМ ли; прямо сейчас -- как-то неохота, лучше подождать, пока не возникнет ещё какая-нибудь аналогичная потребность, чтоб сформировала более общее направление.
15.04.2022: потенциально этот "binbuf" может иметь более широкое применение: фактически это аналог "StrDB" (strbuf), но для бинарных данных ПРОИЗВОЛЬНОГО размера. Где это может пригодиться -- покамест хбз, но концептуально оно вот оно.
21.04.2022: всё-таки сделаем -- это ж в первую очередь просто инфраструктура, а сейчас представление в голове очень чёткое, так что времени уйдёт немного.
CxsdDbInfo_t
добавлены поля
binbuf
, binbuf_used
, binbuf_allocd
.
И освобождение буфера в CxsdDbDestroy()
.
CxsdDbAddBin*()
заложен такой:
CxsdDbAddBinStart()
-- создание нового блока.
CxsdDbAddBinAddSeg()
-- добавление кусочка данных.
CxsdDbAddBinFinish()
-- завершение блока; возвращает
его offset.
CxsdDbAddBinCancel()
-- на случай "если передумалось",
просто удаляет весь текущий блок.
CxsdDbGetBin()
-- отдаёт указатель на данные, плюс
(опционально) их размер в байтах и dtype.
21.04.2022@лесок между Пирогова-26 и стадионом НГУ, ~16:30: кстати, вместе с проектом "процессеров" (начало обсуждения 30-03-2021) эта фича "указание значений по умолчанию" (в дополнение ко всем прочим свойствам) -- движение в сторону "a-la EPICS", позволяющее как-то имитировать работу тамошней "БД".
27.04.2022@Николаева, около (не доходя) Николаева-8 (ГИпроНИИ), по пути за обновлённой ОСАГО на Kei, ~10:00: а можно ввести некоторую оптимизацию, для упрощения использования и для улучшения менеджмента памяти:
CxsdDbAddBinStart()
передавать ещё и
Это позволит для "простых" случаев (единичное значение) обходиться парой
CxsdDbAddBinStart()
,CxsdDbAddBinFinish()
, БЕЗ
промежуточного CxsdDbAddBinAddSeg()
.
28.04.2022@Николаева, около (не доходя) Николаева-8 (ГИпроНИИ), по пути в Быстроном за паштетом из гусиной печени, ~11:00: да -- пусть каждый блок данных префиксируется небольшим заголовком, состоящим из размера и dtype. Причины:
CxsdDbGetBin()
размера и dtype.
CxsdDbAddBinFinish()
-- проходиться по всему binbuf'у и при
найденности возвращать уже имеющееся, а текущий создаваемый инактивировать).
01.05.2022: после размышлений и обмозговываний (в течение нескольких дней) наконец-то сделано:
ofs
'ы "указывают" на ДАННЫЕ, а не на заголовок.
По аналогии с работой malloc()
.
0
достаточно, дополнительная инициализация
не требуется.
ofs
'а в указатель служит
static-inline-функция binofs2hdr()
.
CxsdDbBinHdr_t
,
представляющим из себя union, вторым полем содержащий uint8[16] -- это
гарантирует выравнивание для любых типов данных вплоть до
int128
/float128
.
CxsdDbAddBinStart()
также
выполняет padding предыдущего таким образом, чтобы новое начиналось с
кратного 16.
strbuf
), что также кратно 16.
binbuf_cur_ofs
: там содержится ofs текущего наполняемого блока
-- именно ofs ДАННЫХ, так что умолчательное состояние "binbuf_cur_ofs==0"
означает "сейчас ничего НЕ добавляется".
CxsdDbAddBinStart()
убеждается, что в буфере достаточно
свободного места для заголовка плюс supposed_total_size
байт
(ну или если оное ==0, то, автоматом, просто для заголовка); при недостаче
-- растит буфер.
Затем "открывает" блок и, при заказанности начального сегмента, делает
ему CxsdDbAddBinAddSeg()
, каковой если обломится -- то вся
операция считается провалившейся и блок выкидывается.
CxsdDbAddBinFinish()
проходится по всем ранее созданным
блокам и проверяет на совпадение текущего с ними.
При совпадении -- делает CxsdDbAddBinCancel()
и возвращает
найденный дубль.
CxsdDbAddBinCancel()
возвращает
binbuf_used
в значение
binbuf_cur_ofs - sizeof(CxsdDbBinHdr_t)
-- т.е., как бы "выкидывает" текущий блок, после чего
binbuf_cur_ofs=0
.
Ну да, не совсем точно отменяет всю процедуру добавления (выравнивание-то никуда не денется), но почти.
size_t
, а ofs'ы
-- int
, что
Поэтому
EOVERFLOW
.
strbuf
'а тоже -- конкретно в
CxsdDbAddMem()
.
Но от просто переполнения -- например, при занятом объёме 2ГБ-1КБ заказывается ещё 2ГБ -- такая проверка не защитит. Тут нужно что-то более сложное, именно обнаружение ПЕРЕПОЛНЕНИЯ. ...одна надежда -- что подобной ситуации просто не возникнет (хотя синтаксис devlist'ов, вкупе с парсингом через cxsd_db_via_ppf4td.c, её вполне допускает).
02.05.2022: небольшая перестановка в исходниках: всё, касающееся strbuf и binbuf было перенесено вниз текста, после Nsp/DcPr/Cpnt/Clvl.
02.05.2022: итого -- как бы сделано. Надо проверять, но как? Очевидно -- реализовывать использование для defval.
Учитывая, что в общем случае задача парсинга значений весьма развесиста
(смотрим в console_cda_util.c содержимое
ParseDatarefVal()
с подмастерьями), видимо, для начала надо бы
реализовать парсинг скалярных значений INT и FLOAT. Чего, кстати, хватит
для текущих нужд (замена (текущего) const_drv для нужд tornadotest), а также
для >90% прочих применений.
03.05.2022: приступаем к махинациям с cxsd_db_via_ppf4td.c:
SFT_BINVAL
и SFD_BINVAL()
.
else if()
-альтернатива в
ParseSomeProps()
.
В неё написан "скелет" -- проверка типа (что REPR_INT или REPR_FLOAT)
плюс "скобки"
CxsdDbAddBinStart()
/CxsdDbAddBinFinish()
и
сохранение результата посредством
"*((int *)ea) = binofs
".
ParseSomeProps()
добавлен параметр max_nelems
-- для того, чтобы можно было
max_nelems
из changroups[]
слегка модифицирован код ParseChanList()
; из
ParseCpointProps()
же передаётся константа 0.
04.05.2022: работа над парсингом:
nelems
, с ограничением на
количество элементов.
Если после парсинга числа следующий символ, подсмотренный через
PeekCh()
-- запятая, то этот символ съедается и цикл
продолжается, иначе -- break
.
#if
'ы на тему
MAY_USE_PPF4TD_GET_DOUBLE
убраны (а то от них в глазах рябит и
код диковатый; надо бы и там тоже убрать).
const_init_d()
: результат складируется в
соответствующее dtype'у поле в val
и на него ставится указатель
val_p
-- для CxsdDbAddBinAddSeg()
.
Замечание: тут НЕТ полной совместимости с
console_cda_util.c::ParseDatarefVal()
-- НЕ
поддерживается вариант со списком в фигурных скобках
"{ЗНАЧЕНИЕ,ЗНАЧЕНИЕ,...}". Просто не стал возиться.
06.05.2022@утро-завтрак: следствие -- невозможно указывать векторы длиной 0 элементов...
08.05.2022: заморочился-таки с возможностью указывать список в фигурных скобках, так что теперь и векторы длиной 0 элементов работают.
Итого -- базовая инфраструктура парсинга вроде сделана, надо реализовывать её использование.
binofs
, и...
dcpr_fields[]
добавлена строка
SFD_BINVAL(,"defval",)
.
Далее дело уже за использованием в CxsdHwSetDb()
.
05.05.2022@утро-душ:
кстати, если занадобится хранить ещё какие-то свойства (хотя какие?), то
вместо напихивания в CxsdDbDcPrInfo_t
(и, возможно, в
cxsd_hw_chan_t
) ещё binofs'ов, можно хранить
таблицу с binofs'ами свойств в ещё одном бинарном "контейнере", который сам
держать в binbuf'е; элементами контейнера будут пары
{ID_ТИПА_ДАННЫХ,binofs}.
(чуть позже) вот только проблема будет в том, что поэлементное наполнение такого контейнера фиг знает как делать: ведь бинарные данные, на которые элементы ссылаются, должны наполняться по ходу дела -- так что держать в это время открытым для наполнения сам контейнер не удастся.
Как вариант решения -- в момент ПАРСИНГА dcpr'а сохранять дуплеты {ID_ТИПА_ДАННЫХ,binofs} в локальную таблицу некоего фиксированного размера (по максимуму возможного количества дуплетов), а затем, если в ней >0 элементов, складировать её в binbuf.
(после обеда) отдельный вопрос -- а что в качестве dtype указывать этому контейнеру-таблице? В принципе -- пофиг, что: да хоть CXDTYPE_UNKNOWN.
05.05.2022: приступаем к использованию.
@утро-душ: Сначала соображения на тему "а как бы НЕ СБРАСЫВАТЬ timestamp'ы каналов, которым указаны умолчательные значения":
cxsd_hw_chan_t
иметь флаг "has_defval".
uint8
-поле, надо просто
часть уже существующих преобразовать в наборы БИТОВЫХ флагов.
...конкретно сейчас на эту роль напрашивается
rw_readonly
.
@вечер: пытаемся переделать флаги...
где "rw" и "rw_readonly" стоят точно друг под дружкой, так что получается очень красиво и наглядно (не говоря уж о том, что там арифметика (булевская) перевод, которой на битовые маски сделал бы всё громоздким и уродливым)./* Filter-out non-rw channels: */ /* 1. Find first rw channel */ while (n < dev_p->count && (cxsd_hw_channels[dev_p->first + n ].rw | cxsd_hw_channels[dev_p->first + n ].rw_readonly) == 0) n++; /* 2. Find last rw channel */ last = n; while (last < dev_p->count && (cxsd_hw_channels[dev_p->first + last].rw | cxsd_hw_channels[dev_p->first + last].rw_readonly) != 0) last++;
uint32
-поле cxsd_hw_chan_t.bhvr
и маска CXSD_HW_CHAN_BHVR_HAS_DEFVAL
=1<<0.
06.05.2022: реально делаем...
CxsdHwSetDb()
:
timestamp
'а --
одного значения на все инициализированные каналы (скопировано из
ReturnChanSet()
'а, естественно).
И взведение флага CXSD_HW_CHAN_BHVR_HAS_DEFVAL
.
return_type
-- цепочка действий (ограждённая уже имевшимся условием) скопирована из
SetChanReturnType()
.
...да, фигово, что копия...
И наконец-то стало ясно, почему тогда, в декабре 2019-го, эта часть сделана не была -- именно потому, что действий несколько, а не хотелось дублировать код. (Всё казалось, что причина как-то записана тут в bigfile-0002.html, но нет -- поиск по "return_type" ничего не дал.)
@вечер, при тестировании: а ещё потому, что не был реализован парсер для SFT_FLAG.
CXSD_HW_CHAN_BHVR_HAS_DEFVAL
отсутствует":
RstDevTimestamps()
и RstDevUpdCycles()
--
очевидные места.
RstDevRflags()
добавлено чисто "за компанию": с одной
стороны, при ОБНУЛЕНИИ флагов эта проверка не факт, что вообще имеет смысл,
а с другой -- единственный вызов этой функции в ReviveDev()
давно закомментирован.
SetDevRflags()
более хитро: там условием прикрыто
только собственно выставление флагов, а вот вызов evproc'ев по CXSD_HW_CHAN_R_STATCHG
выполняется всегда.
Проверяем -- вылезла КУЧА косяков.
ParseChanList()
был косяк в поиске вмещающего канал
элемента changroups[]
: при найденности данные брались из
changroups[changrp_n++]
-- и из-за этого "++" имевшееся
ранее только одно чтение dtype'а работало, а вот добавленное второе
(max_nelems) брало уже из следующей ячейки.
*((int *)ea) = table[idx].var_int;
")
работает только вариант "ИМЯ_ФЛАГА:" -- иначе общий блок парсинга ругается
на "неоключенный параметр после ключенного".
CxsdHwSetDb()
аж по 4 раза отрабатывается
вычитывание/активация свойств канала...
Обнаружилось при поиске косяка с max_nelems -- неожиданно много одинаковой отладочной печати вылезло.
Но так -- да, после исправления этих косяков указанное значение по умолчанию действительно попадает в канал и успешно читается оттуда cdaclient'ом.
06.05.2022@вечер-душ ~18:30: кстати, а как заменить именно const_drv, конкретно для "ПОСТОЯННЫХ" -- READONLY-каналов? Ведь предполагается указывать defval ro-каналам, обслуживаемым noop'ом, а он на таковые отдаёт постоянно растущие значения...
@получасом позже: Есть, конечно,
вариант: научить StdSimulated_rw_p()
рассматривать
readonly-CXSD_HW_CHAN_BHVR_HAS_DEFVAL
-каналы наравне с
rw-каналами -- что приведёт к просто возврату текущего значения.
...а вообще, если таким каналам будет указываться флаг autoupdated_trusted, то проблемы и не будет -- просто не вызовется чтение.
@21:30: да -- при указании флага всё работает, даже в ro-каналах остаётся указанное значение!
07.05.2022: разбираемся.
В результате оно делалось столько раз, сколько у устройства changroups[]'ов. Так-то несмертельно -- просто повторяло статические действия (копирование, БЕЗ всякой аллокации), но смысла никакого.
Исправлено -- цикл по nsp_id
вытащен наружу из цикла по
changroups[].
07.05.2022@около мыши и ИПА, по дороге домой из Ярче, ~18:45: некоторые общие соображения на тему парсинга строк:
...и console_cda_util.c::ParseOneChar()
тоже
касается.
ppf4td_get_string16()
и
ppf4td_get_string32()
?
...да только ТУТ нам это не поможет: тут ведь нет никакого буфера, из-за
отсутствия верхней границы размера, а должно парситься по 1 символу, со
складыванием посредством CxsdDbAddBinAddSeg()
.
ParseSomeProps()
сделать можно: вообще реализовать единую ветку
для REPR_TEXT, а в ней вместо 2 альтернатив по размеру сделать все 3.
Как минимум -- хбз, какую букву для такого типа использовать. Это же "BMP" (Basic Multilingual Plane)? Просто 'b' нельзя -- байт; 'm'?
Кому сильно надо -- могут UINT16 пользоваться, оно совместимо.
PrintDatarefData()
по-хорошему надо бы. Неприятно, но несложно -- там инфраструктура на основе
PrintChar()
вся готова, надо только правильно выборку из строки
делать.
И тогда уж и в ParseDatarefVal()
тоже -- благо, там
аналогично (симметричная) инфраструктура на основе
ParseOneChar()
всё позволит.
08.05.2022: продолжаем пилить блок парсинга:
Реализовано по аналогии с
console_cda_util.c::ParseDatarefVal()
, где факт
наличия кавычки/скобки сохраняется в qc
. Но НЕ идентично!
Дополнение -- в режиме "заскобочено" разрешаются лишние пробелы: оно и
после открывающей скобки, и перед запятой делает
ppf4td_skip_white()
.
08.05.2022: ещё тестируем: да, работает всё -- для defval и вещественные (а не только целочисленные), и массивы (ещё вчера проверено, а сегодня добавлены заскобоченные "{ЗНАЧЕНИЕ,ЗНАЧЕНИЕ,...}"), в т.ч. сегодня заработали и пустые.
11.05.2022: уже несколько дней пилим поддержку парсинга строковых значений -- по проекту от 07-05-2022 (с вариабельным количеством 16-ричных цифр и с поддержкой 16-битных символов).
Сегодня наконец-то допилено -- ВРОДЕ бы. Начинаем проверять...
12.05.2022: проверяем и исправляем косяки (в основном они были связаны с парсингом при наличии кавычек).
IsAtEOL()
надо
завершать парсинг, нужно делать только при НЕзакавыченности (иначе EOL
внутри строки должен считаться ошибкой).
Условие добавлено.
isspace()
, то нужно также завершать парсинг. Тоже
добавлено.
(В первом варианте, кстати, была просто проверка на
isspace()
, БЕЗ учёта НЕзакавыченности. В результате оно
благополучно завершало парсинг прямо среди кавычек :D)
NextCh()
есть проверка на IsAtEOL()
.
Пришлось -- ради конкретно этого места -- городить:
IsAtEOL_IGNHASH()
, с убранной проверкой на
'#'.
NextCh_IGNHASH()
, использующий оного.
Ну и взятие очередного символа как потенциального следующего для строки
переведено на NextCh_IGNHASH()
.
Исправлено путём дополнительной проверки -- оные символы разрешаются
только в случае, если в qc
записан "другой" (одиночные
разрешены в двойных, а двойные в одиночных):
if ((ch == '\'' && qc != '\"') || (ch == '\"' && qc != '\'')) return BARK("unquoted <%c> character in \"%s\"", ch, table[idx].name);
Итого -- можно считать задачу решённой.
Но есть некоторые наблюдения общего характера:
ParseSomeProps()
!
Всё-таки такой "штучный" парсинг для частных целей -- зло, ибо выходит какой-то набор кривых самоделок/хаков, крайне плохо годный для каких-либо будущих модификаций.
Ну ОЧЕНЬ напрашивается на переделку.
13.05.2022: отладочная печать удалена, так что можно пускать в дело.
Точнее, это по факту копировние namespace'ов.
04.05.2024@утро-зарядка и потом утро-душ: некоторые соображения:
devtype ТИП_УСТРОЙСТВА:ТИП_ПРЕДОК ИНФО_ПО_КАНАЛАМ
или
devtype ТИП_УСТРОЙСТВА(ТИП_ПРЕДОК) ИНФО_ПО_КАНАЛАМ
(второй вариант позволяет в скобках указывать ещё какие-нибудь ключевые
слова перед ТИП_ПРЕДОК).
CxsdDbNspCopyLines()
", которой
передаётся ID типа-"предка" (откуда взять список, т.е., всё, что в группе
"Namespace-related") и она по сути делает malloc()
с последующим
memcpy()
.
Т.к. содержимое строк списка -- CxsdDbDcLine_t
-- просто ID
строк и dcpr'ов, то такое тупое копирование абсолютно корректно и безопасно.
Правда, не вполне ясно, где и как такое "псевдо-множественное наслодование" может пригодиться: разве что изначально изготавливать предков как непересекающиеся сегменты, из которых как из "кубиков" составлять некие наборы; ведь, в отличие от языков ООП, тут каждая строка/имя имеет фиксированное маппирование на номер канала. ...и, кстати, при таком "добавлении" появится проблема перекрытия номеров, которую не особо ясно, как решать.
Так что, ПОКА сделаем именно простое копирование.
По сути -- готовый проект для несложной реализации.
08.05.2024: а ведь вместо "наследования" достаточно было бы просто писать в ИНФО_ПО_КАНАЛАМ конкретную нужную информацию, вместо '~' -- благо, синтаксис это позволяет. Другое дело, что это не слишком-то удобно да и противоречит принципу "не плоди сущности" -- любое изменение в базовом devtype потребует модифицировать и все такие точки с "вручную изменённой спецификацией".
05.05.2024: делаем, начиная от подстилающей функциональности.
CxsdDbNspCopyLines()
сделана, но именно как ДОБАВЛЕНИЕ:
она при надобности до-аллокирует нужный объём -- кратно
ALLOC_INC
-- и затем копирует в свободное место.
Просто так оказалось проще, т.к. "items[]" -- это не указатель на массив
строк, а кусок CxsdDbDcNsp_t
, идущий в конце (объявленный как
items[0]
), так что всё равно не аллокировать с нуля известным
количеством, а ре-аллокировать, поэтому сделано копированием
CxsdDbNspAddL()
'я с очевидным расширением.
(С по-канальными (или хотя бы по-группными) проверками решено не заморачиваться.)
CxsdDbNspCopyLines()
.
Сначала всё обдумывал, как бы этот рефакторинг сделать: проблема в том,
что там 1) сначала этот парсинг '{'+EOL+NL, 2) затем
аллокирование и регистрация namespace (и добавление строк из parent'а там
же), 3) и лишь потом парсинг списка каналов
ParseChanList()
'ом. Но так-то пункты 1 и 3 при
'}'-сразу являются как бы единой сущностью -- вот и думал о
"рефакторинге". А потом -- ...
06.05.2024@~09:55, рядом с мышью по
дороге в ИЯФ: есть вариант проще -- НЕ перескакивать на следующую
строку после '{' и НЕ пропускать '}', и тогда контекст
парсинга просто останется перед закрывающей скобкой и
ParseChanList()
преспокойно "отработает", завершившись на
первом же символе.
Так и сделал -- пусть с небольшим "рефакторингом" собственно парсинга '}'-или-EOL. Работает!
Проверено на "живом" simkoz-симулируемом устройстве, чей тип "отнаследован" от cac208, но каналы входного регистра помечены как "w" вместо "r" -- да, работает как положено (запись в эти каналы стала возможно и меняются они как надо в схеме "8+1").
CxsdDb
.
Сюда уходят вещи, в v2'шном cx-server'е жившие в
cx-server_data.[ch] в массивах c_*[]
,
b_*[]
и d_*[]
.
Используется префикс CxsdHw
.
*CHECK_*SANITY*()
.
Натуральным местом выглядит cxsd_hw.h -- что и сделано, рядышком с {ENTER,LEAVE}_DRIVER_S() (которые, кстати, не надо ль привести к виду свежих remcxsd'шных?).
ENTER_DRIVER_S()
и LEAVE_DRIVER_S()
об-умнить?
17.11.2013: создаём этот модуль. Предназначение -- "ядро" работы с данными; то, что в v2 жило в cxsd_channels.c и cxsd_bigc.c.
Префикс, очевидным образом, CxsdData
.
Возможно, оно должно б быть единым целым с cxsd_hw.[ch] (разделить области ответственности нетривиально), но пока, ради разделения обязанностей, делаем так:
...а еще тут же рядышком cxsd_driver...
17.11.2013: уж проще пусть всё, связанное с каналами/железом/данными (т.е. -- "функционирование «динамической БД»") живёт совместно в одном модуле.
17.11.2013: резоны:
Вообще, конечно, для обеих целей подход небезупречен.
Короче -- надо снова и снова переобдумывать мысли из разделов "Общая схема работы" и "ВременнАя схема работы".
17.11.2013: пока же делаем в простейшем варианте, чтоб было хоть как-то (и чтоб ядро сервера годилось для использования в v2).
Для этого проверяется, что если следующий желаемый момент окончания цикла -- cycle_end=cycle_start+basecyclesize УЖЕ в прошлом, то оно ставит cycle_start=now и перевычисляет cycle_end. Т.о., оно НЕ пытается пройти через все-все шаги, а при обнаружении лага старается скакнуть к текущему моменту.
...но перевод часов НАЗАД оно НЕ отрабатывает (это вообще на уровне cxscheduler'а будет так).
CxsdHwSetCycleDur()
, от DURation.
CycleCallback()
(с предварительным
обнулением (="01-Jan-1970") cycle_end).
BeginOfCycle()
и
EndOfCycle()
пока не делается (в т.ч. вызовов
frontend'овых методов).
21.11.2013: добавлена
CxsdHwGetCurCycle()
, для нужд, например,
StdSimulated_rw_p()
.
23.05.2014@Снежинск-каземат-11: насчёт "НАЗАД". А можно проверять, что now более чем, например, на basecyclesize*2 от cycle_end (*2 -- для избежания проблем от округления), и тогда тоже адаптироваться.
14.06.2014: усовершенствуем
CycleCallback()
:
Смысл -- чтоб при слегка загруженном процессоре оно всё-таки ПОПЫТАЛОСЬ бы пройти через все циклы. Один-два пропуска (мало ли -- дисковая активность) будут терпимы, а через десяток всё же перескочит.
Если же такое адаптивное поведение будет нежелательно -- достаточно
обнулить cycle_pass_count_lim
.
Причина -- при переводе времени назад до
CycleCallback()
управление и не дойдёт. Такие "приколы"
надо обнаруживать в самом cxscheduler'е. Как?
16.06.2014: сделано обнаружение в cxscheduler'е.
Оно заказывается "клиентом" libcxsd (т.е., cxsd), а реагирующая функция
-- CxsdHwTimeChgBack()
.
28.10.2014: добавлена архитектура evproc'ев на
циклы: теперь любой может зарегистрироваться на уведомление при помощи
CxsdHwAddCycleEvproc()
. Сделано в первую очередь в
интересах cda_d_insrv.
Уведомления шлются и на конец цикла --
reason=CXSD_HW_CYCLE_R_END
, и на начало --
(CXSD_HW_CYCLE_R_BEGIN
.
03.08.2014: идеология: реализуется оно всё в cxsd_hw, а "юзерам" даётся интерфейс, состоящий из
CxsdHwAddChanEvproc()
и
CxsdHwDelChanEvproc()
-- функционально аналогичные cda'шным
cda_add_dataref_evproc()
и
cda_del_dataref_evproc()
.
CxsdHwCallChanEvprocs()
-- вызов evproc'ев для указанного
канала; дергается из ReturnDataSet()
.
29.01.2015: поскольку единственным юзером был
ReturnDataSet()
, переехавший из cxsd_driver в cxsd_hw
31-10-2014, то CxsdHwCallChanEvprocs()
убрана из
публичного интерфейса и сделана приватной.
Заметки по деталям реализации:
cxsd_hw_chan_evproc_t
,
содержащим весь набор параметров -- включая uniq, privptr1 и privptr2.
Так что этим API драйверы могут пользоваться и напрямую.
cxsd_hw_chan_t
-- добавлена пара
полей cb_list/cb_list_allocd. Менеджмент инкапсулирован в
cxsd_hw.c (static) и имеет ключевое слово (cname) "ChanCb".
cxsd_hw_do_cleanup()
. Она чуть халтурит --
разрегистрирует evproc'ы "руками" вместо RlsChanCbSlot()
, зато
это максимально быстро.
...правда, вызывать её пока некому, т.к. ни term_dev()
, ни
term_lyr()
пока нигде не делаются -- нету ни
TermDevice()
, ни TermLayer()
, ни должного их
использовать CxsdHwDeactivate()
.
CxsdHwSetDb()
выполняется
деактивация всех evproc'ев всех каналов -- вообще безусловная.
07.08.2014@пляж: да ясно, что делать: слать им уведомления, в 2 этапа, stage=0 перед удалением старой и stage=1 после активации новой.
Понятно, что понадобится добавить еще один метод -- "DB change".
А вообще, кстати, evproc'ы же -- и через них можно слать сообщения. В частности, надо будет ввести CXSD_HW_CHAN_R_PARAMSCHG, присылаемый при смене драйвером {r,d} и q (они могут "на лету" меняться только у физического канала, т.е. -- последний дуплет в цепочке {r,d}).
cxsd_chanid_t
-- для адресации каналов, вместо безликого int
.
29.09.2014: дополнительно еще
cxsd_cpntid_t
-- для ссылания на "точку контроля"
(03-03-2013 -- «идент "точки контроля"»).
CxsdHwResolveChan()
и
должно быть достаточно для ссылания на эту точку так же, как по имени
-- чтоб в будущем, когда какие-то из свойств ({r,d}?) меняются, то не
повторять резолвинг, а повторить лишь добычу. 10.01.2015: да, заюзано в cda_d_insrv.c, через
CxsdHwGetCpnProps()
.
inserver_get_bigc_data()
?
CxsdHwResolveChan()
. Это ключевая часть "статической БД"
-- её интерфейс для клиентов.
Предыдущее как бы обсуждение -- в разделе "Статическая БД".
24.10.2014: пока просто заглушка, ради
собираемости stand
с libcda_d_insrv.a.
26.10.2014@беговая-дорожка: с одной стороны, полная реализация при отсутствии пока самой БД -- никак. С другой, хоть что-то необходимо уже сейчас, для отладки insrv и прочего. Ну хотя бы возможность числового указания каналов.
А числовое-то уже можно сделать -- воспринимать "имена", состоящие только из цифр плюс имена вида ИМЯ_ЭКЗЕМПЛЯРА_УСТРОЙСТВА.цифры.
27.10.2014: да, такой простейший вариант "резолвинга" сделан. Свойства пока заполняются пустотой, включая rds_buf[] -- поскольку нечем.
28.10.2014: проверено и отлажено, теперь оба варианта работают.
10.01.2015: сделана парная функция
CxsdHwGetCpnProps()
, работающая через
FillPropsOfChan()
. Для добычи (изменившихся) свойств уже
известной точки контроля.
29.05.2015: эта работа доведена до конца -- и вообще "мясо" резолвинга переехало в _db, и FillPropsOfChan() уже делает полную добычу свойств. Так что считаем раздел исполненным.
CxsdHwDoIO()
--
"мясо" сервера; то, что должно передавать запросы драйверам.
Примерный аналог v2'шной MarkRangeForIO()
.
24.10.2014: эта пока тоже просто заглушка ради
линкуемости stand
с libcda_d_insrv.a.
29.10.2014: "мозги" её скорее всего можно будет взять от v2, но возникла неожиданная техническая проблема в связи с новым API:
do_rw()
ему надо передать
массив номеров каналов -- addrs[]
.
Но где взять этот массив -- т.е., место под него, учитывая, что количество передаваемых каналов может быть потенциально сколь угодно большим?
С прочими массивами -- dtypes[], nelems[], values[] -- проблем не
возникает, поскольку можно передавать прямо куски тех массивов, что
переданы в вызове самой CxsdHwDoIO()
.
А с addrs[]
нельзя, поскольку самой функции передаётся
массив ГЛОБАЛЬНЫХ номеров -- globalchans[]
, а она уж
должна передать методу ЛОКАЛЬНЫЕ адреса (в рамках устройства).
И что делать? Мыслей разных за последнее время было море.
dev->first
, а потом прибавляя обратно.
Плохая идея -- мало того, что лазить в чужую память нехорошо, так еще и толпа потенциальных проблем (в т.ч. вследствие того, что все вызовы могут быть рекурсивными).
count
элементов -- чтоб любой запрос уж точно
влез.
Тоже плохая идея. Например, по той же причине возможной рекурсии:
из вызванного метода драйвера какой-то канал тут же будет вёрнут, что
вызовет еще отправку, например, вследствие взведённого к тому моменту
next_wr_val_pnd
. (Возможны и более хитрые пути, вроде
дёрганья драйвером этого устройства каналов другого, которые, сразу
вернув значения, могут привести к вызову callback'ов (при прямом
использовании API "ChanEvproc", а не cda-insrv), которые как-то
активируют опять вызовы из этого устройства.)
Кстати: а что, если некий канал, ранее незапрошенный, в одном запросе встретится несколько раз -- да хоть тысячу? Ведь логика "ConsiderRequest()" все эти разы будет его считать "годящимся для отсылки", поскольку rd_req/wr_req всё еще будут ==0. И так вполне может оказаться, что не только запрос на один канал может сбагриться драйверу НЕСКОЛЬКО раз до ответа, но и само количество каналов в запросе превысит количество каналов устройства.
30.10.2014: делаем.
ConsiderRequest()
и SendChanRequest()
.
SendChanRequest()
использует тот "принцип flush".
За основу взят чуть модифицированный код из nadc502. COUNT_MAX=1000.
29.11.2014: count->seglen,
COUNT_MAX->SEGLEN_MAX.
05.12.2018: ага, только в цикле, "делающем flush", было
забыто "gcids += seglen
", из-за чего цепочки длиннее
SEGLEN_MAX
работали некорректно. Добавлено.
ConsiderRequest()
получает все исходные данные плюс
флаг may_act
, говорящий, в каком качестве она вызвана: ==1
-- когда уже идёт выбор цепочки каналов для "действия", и ==0 при
первоначальном выборе "'model' parameters".
cxsd_hw_chan_t
добавлено поле
upd_cycle
.
ReturnDataSet()
складирует туда текущее значение
current_cycle
.
ConsiderRequest()
проверяет, и при
upd_cycle==current_cycle считает DRVA_IGNORE.
current_cycle
теперь инициализировано
значением 1, так что изначально все каналы "обновлялись не в этом
цикле".
14.01.2015: и еще сравнивать в
CycleCallback()
на предмет "если первый цикл, то вызвать
BeginOfCycle()
надо было с ним же, так что значение =1
формализовано в INITIAL_CURRENT_CYCLE
.
Чётко видно, что
ReturnDataSet()
относится к cxsd_hw.c, а не к
cxsd_driver.c, где он сейчас. 31.10.2014: да, перенесена.
rd_req
/wr_req
выставляются прямо в
ConsiderRequest()
. Оно условно, при may_act
.
Причина -- идеологическая:
CxsdHwDoIO()
.
SendChanRequest()
уже оттуда ВСЕГДА и брал данные.
Вопрос лишь КАК -- ведь это решение должно приниматься по-канально,
и не в SendChanRequest()
, а еще в
CxsdHwDoIO()
, если не вообще в
ConsiderRequest()
.
Проблема...
А может, всё-таки разрешить (постулировать
возможность!) менять переданные в CxsdHwDoIO()
массивы
values,nelems,dtypes? Тогда ConsiderRequest()
сможет сам принимать решение -- если требуется конверсия, то
складировать в next_wr_val и должным образом изменить
values,nelems,dtypes, а если не требуется, то оставить как есть.
30.11.2014: уже сделано (позавчера) что ВСЕГДА складирует данные в next_wr_val -- чтоб ReRequest было откуда делать.
31.10.2014: да, надо объявить право менять значения в тех трёх массивах. 14.03.2018: ага, а вчера это аукнулось :D
Зачинаем поддержку next_wr_val и конверсии, как описано в последнем
пункте за вчера. Пока просто скелет функции
StoreForSending()
.
31.10.2014@вечер-беговая-лорожка: пусть StoreForSending() возвращает DRVA-код (WRITE или IGNORE), чтоб иметь возможность "наложить вето" на действие в случае, если данные неконвертабельны. При складировании для next_wr_val этот код должен проверяться Consider'ом, чтобы понять, выставлять ли next_wr_val_pnd, а при складировании для немедленной отправки -- можно сразу его и возвращать.
05.11.2014: неа, чуть хитрее: ведь Consider может вызываться и с may_act=0, для ОЦЕНКИ возможности записи. И тогда он должен вернуть то же самое значение WRITE или IGNORE, что и с may_act=1.
Так что надо код определения конвертабельности выносить в отдельную функцию.
IsCompatible()
. Текущий набор
условий в ней небесспорен, но потом можно улучшить.
StoreForSending()
наполнена содержимым, по образу
ReturnDataSet()
.
12.11.2014: первая попытка проверки. Облом -- не пашет.
Длительные раскопки с толпой отладочной печати обнаружили огрехи в идеологии:
ConsiderRequest()
, вызываемый
для расширения набора (т.е., для проверки, совпадает ли следующий
канал по "виду" с предыдущим), сам же
Но это неправильно: очередной оцениваемый канал может оказаться другого "вида", и к нему НЕ будет применено то действие, и в результате он останется в "зависшем" состоянии -- фоаг взведён, а запрос не будет отправлен (поскольку он будет первым в следующей группе, и сразу DRVA_IGNORE).
Что делать? Варианты:
Развесисто и трудоёмко будет, хотя и максимально ортогонально (читай -- гибко).
may_act
, но еще и
&&f_act==DRVA_NNN
, где NNN -- возвращаемое данной
веткой условия значение.
Но это-то исправить просто: достаточно не делать n++
после получения f_act
, и тогда Consider будет вызывн
повторно, уже с may_act=1.
Так и сделано -- с передаванием f_act
и убрано первое
n++. Проверено -- вроде работает.
Если что, старая версия в w20141112-buggy-4Consider.tar.gz.
13.11.2014: CxsdHwReqRofWrChs()
--
аналог v2'шной RequestReadOfWriteChannels()
. Действует
через ReqRofWrChsOf()
, которая, в свою очередь, работает
по принципу "flush", блоками не более 1000 каналов.
14.01.2015: тогда было сделано халтурно -- запрашивались все подряд каналы без разбора. Сейчас переделано, что запрашивает только rw-каналы.
14.01.2015: кстати, учитывая, что
ReqRofWrChsOf()
вызывается при первоначальном оживлении
устройства, надобность в CxsdHwReqRofWrChs()
стала
сомнительной (по факту, она ничего не делает).
29.01.2015: да, CxsdHwReqRofWrChs()
убрана. Так оно идеологически правильнее: cxsd_hw сам заведует всеми
тонкостями функционирования каналов, не сваливая куски этой работы на
своих юзеров.
28.11.2014: надо б поддержку "next_wr_val" делать. В виде:
TryWrNext()
.
30.11.2014: за вчера-сегодня сделано.
"Параметры" определяются проверятором ShouldWrNext()
.
Причина изменения -- тут проверка позаковыристей, из-за addrs[] вместо first,count.
Хотя в v2 такого НЕ сделано (та же отсрочка на 1 шаг), но там и timestamp'ов нет, а тут есть...
HandleSimulatedHardware()
.
28.10.2014: 3 аспекта:
HandleSimulatedHardware()
: сделана
немудряще, цикл по всем устройствам внутри которого цикл по каналам
устройства с вызовом StdSimulated_rw_p()
.
EndOfCycle()
.
CxsdHwActivate()
: при
MustSimulateHardware
оно просто делает
state=DEVSTATE_OPERATING, не только ничего не проверяя, но даже не
пытаясь загрузить модуль драйвера.
MustSimulateHardware
, то просто
делать "всё окей", а иначе если "загрузилось ok", то пытаться
инициализировать модуль.
MustSimulateHardware
и проблема
либо загрузки, либо инициализации, то помечать все каналы как
"CXRF_NO_DRV" (касается только драйверов).
10.04.2015: немудрящесть вылезла немного боком: в
режиме симуляции каналы записи ведут себя иначе, чем при обычной работе
-- они ЕЖЕЦИКЛЕННО "обновляются". Строго говоря, это не совсем хорошо
-- лучше бы, как в v2, симулировать чтение только
не-rw
-каналов.
23.04.2015: исправляем "немудрящесть", чтобы всё было корректно и даже в режиме симуляции вело б себя как при обычных драйверах.
HandleSimulatedHardware()
делано, что
StdSimulated_rw_p(DRVA_READ)
вызывается только для
не-rw-каналов.
ReqRofWrChsOf()
при
InitDevice()
.
InitDevice()
:
при MustSimulateHardware
там СОВСЕМ ничего не делалось, в
т.ч. не вызывался и ReviveDev()
-- который и дёргает
ReqRofWrChsOf()
.
...а еще чуть дальше -- что и сам InitDevice()
не
вызывался: CxsdHwActivate()
руками ставила
dev_p->state = DEVSTATE_OPERATING
InitDevice(devid)
, и всё стало
хорошо.
InitDevice()
за компанию управильнен -- из
него убрано имевшееся в самом начале странное дополнительное условие
metric->init_dev != NULL
, сопровождавшееся
комментарием "!!! An extremely lame check! This "!=NULL" shouldn't
be here at all!".
А оставлена только проверка на MustSimulateHardware
, и
если "да", то просто вызывается ReviveDev(dev)
.
SendChanRequest()
заменена "внутренняя
мозговитость" при симуляции и записи (рудимент от одноимённой функции
из v2) на простое действие --
if (MustSimulateHardware) do_rw = StdSimulated_rw_p;
Строго говоря, при этом наличие
HandleSimulatedHardware()
становится излишним: вместо
"чтения всего всегда" в начале цикла будет выполняться "чтение по
запросу", не чаще раза в цикл -- т.е., наиболее полно мимикрируется под
работу обычного драйвера.
...проверено -- и без него всё ведёт себя как надо.
StdSimulated_rw_p()
'ом
rw-каналам CX_TIME_SEC_TRUSTED.
21.03.2016: а TRUSTED-то почему?! Ведь оно делается только в чтении-каналов-записи, выполняемой только при начальной инициализации, и там надо отдавать NEVER_READ! Сделано, теперь отдаёт, cdaclient видит возраст 1.
15.04.2016: ню-ню! После модификаций 20-х чисел марта на тему "отдавать NEWVAL на rw-каналы только с timestamp!=NEVER_READ" эти rw-каналы перестали работать с cdaclient'ом, ПОЛНОСТЬЮ. Посему изменено на возвращение текущего времени (подробности истории см. ниже за сегодня).
23.04.2015: и еще -- в
StdSimulated_rw_p()
вставлена проверка, чтоб каналы были в
диапазоне [0,count). А то функция вызвабельна и из драйверов, а они
могут и накосячить.
30.04.2015: в самой
StdSimulated_rw_p()
был косяк в блоке записи в rw-каналы:
стояло "nvp=values" вместо "nvp=values[n]". В результате феерично
глючило (вылезло при проверке cdaclient, и сначала в нём всё было
проверено).
28.05.2015: кстати, давно и успешно работает, так что "done".
_devstate
.
31.10.2014: примерный проект реализации:
CxsdHwSetDb()
.
cxsd_hw_chan_t.drvchancount
.
ReturnDataSet()
, так что надо вызывать её. И как?
Ввести внутреннюю глобальную переменную, во взведённом состоянии "разрешающую" возврат каналов >=drvchancount? Но сразу после проверки её надо сбрасывать, чтоб не воспользовались вызываемые-по-evproc'ам.
Это удобнее делать в самом начале, еще до цикла по
count
.
DRVA_INTERNAL
, который
будет возвращаться Consider'ом для каналов с номерами
>=drvchancount.
...каковому ConsiderRequest()
'у надо будет добавить в
список параметров и dev
. Кстати, и
SendChanRequest()
'у тоже.
28.05.2015: не-а! Для определения (отделения от прочих)
достаточно в cxsd_hw_chan_t
добавить флаг
is_internal
. А отправлять вообще ничего никуда не надо -- само
вернётся при изменении; так что и DRVA_INTERNAL не нужен, достаточно
DRVA_IGNORE.
28.05.2015: а ЗАЧЕМ? Обновление и так будет делаться по мере надобности, а "текущее" вычитывается cda_d_cx'ом и так. (Проект был написан в октябре-2014, еще ДО реализации PEEK, вот и присутствуют в нём ныне ненужные детали.)
SetDevState()
тоже сделает
ReturnDataSet(,,[0]=dev->drvchancount+OFS_OF_DEVSTATE,)
.
CxsdHwResolveChan()
.
30.03.2015: кроме канала _devstate
завести б еще канал "_devstate_description" -- со строковым описанием,
которое передаётся в SetDevState()
.
28.05.2015: делаем:
CxsdDbDevLine_t
: для полного количества заведено поле
wauxcount
(With AUX).
cxsd_hw_dev_t.wauxcount
-- аналогично.
cxsd_hw_chan_t
добавлен is_internal
(взамен res1).
ConsiderRequest()
на каналы с ним сразу возвращает
DRVA_IGNORE.
После обеда: а ведь, учитывая, что _devstate* !rw,
достаточно было пользоваться флагом is_autoupdated
...
CxsdDbCreate()
: заполнение у [0]-го поля wauxcount и
использование уже ЕГО в подсчёте db->numchans.
CxsdDbAddDev()
: аналогично.
CxsdHwSetDb()
:
Вообще-то напрашивается сделать функцию-подмастерье "заполнить свойства канала", и вызывать её изо всех 3 точек.
CxsdDbResolveName()
:
finding_special
-- =1 для _devstate и =2 для
_devstate_description.
В обоих случаях проверка на finding_special!=0 идёт ДО поиска канала по имени, поэтому можно указывать вещи вроде LINK_TO_DEV.CHANNAME._devstate* и "DEVNAME.CHANNAME._devstate*" (причём количество компонентов CHANNAME может быть произвольно большим).
dev->count
-- ReturningInternal
.
ReturnDataSet()
считывает её в локальную в самом
начале, и потом сбрасывает =0.
03.08.2015: условие !internal
перед
проверкой на [0,dev->count) стояло только при сохранении данных, а
при вызове evproc'ев по R_UPDATE -- нет, в результате обновления
каналов _devstate* подписчиками не виделись (вылезло на vdev-драйверах,
которым эти каналы критичны). Исправлено.
SetDevState()
: его подмастерья NnnnnDev() пользуются
отдельной функцией report_devstate()
. Она вызывает
ReturnDataSet()
для 1 либо 2 каналов: собственно _devstate
всегда, и _devstate_description при условии description!=NULL (т.е.,
description==NULL оставляет старое значение).
Конкретно ReviveDev()
форсит description="".
Результаты:
В результате проверять cdaclient'ом просто не получается, поскольку значение всегда "когдатошнее", оно НЕ обновляется и НЕ TRUSTED (ибо и timestamp полезен), так что событие CDA_REF_R_UPDATE не генерится, а можно лишь получить текущее "последнее известное значение".
Так что проверялось das-experiment'ом с ключом "-p500".
28.05.2015@вечер-ванна: а
ведь проблему "начальное вычитывание не даёт обновлений" можно решить очень
просто: в cxsd_fe_cx в ответ на CXC_PEEK отдавать
CXC_NEWVAL вместо CXC_CURVAL при is_internal
(аналогично
rw
).
29.05.2015: так и поступаем:
ServeIORequest()
на PEEK для internal-каналов отвечает
NEWVAL.
Помогло.
is_autoupdated
и, поскольку его достаточно, то проверка на
is_internal
из ConsiderRequest()
убрана.
Таким образом, на is_internal фактически оставлена только роль "is_devstate".
Засим задачу можно считать выполненной.
20.04.2017: есть одно неудобство:
finding_special
) нельзя создавать cpoint'ы с именами вида
SOME_VIRTUAL_BRANCH._devstate -- оно пытается взять состояние от
ветки "SOME_VIRTUAL_BRANCH", а поскольку таковая не существует, то
происходит облом "нет такого канала".
Единственный путь СЕЙЧАС -- создавать какое-то фейковое устройство (noop'ом), а чьему имени донавешивать требуемые имена, а в качестве состояния будет использоваться состояние этого фейка.
finding_special
!=0 и подошли к последнему компоненту (last!=0),
а предпоследний является просто CLEVEL'ом, то проверить наличие в этом
CLEVEL'е элемента с нужным именем, и если он резолвабелен на канал, то
вернуть его.
...сейчас попробовал порыться в
CxsdDbResolveName()
на эту тему -- весьма, весьма там
неочевидно...
Получасом позже:
CxsdDbResolveName()
,
анализировал ход исполнения для разных случаев.
На вид -- да оно просто должно работать!
Вот просто код так устроен, что это должно автоматом давать нужный результат.
Вот же какой я умный, прекрасный код написал два года назад :D
SetDevState()
-- без неё уже тяжко.
20.11.2014: детали:
TerminDev()
-- самая длинная.
InitDevice()
при любой неудаче.
term_dev()
она вызывает, если текущее
состояние !=DEVSTATE_OFFLINE.
Т.е., предполагается, что даже НЕДОинициализировавшиеся драйверы должны быть способны подчищать за собой.
fdio_do_cleanup()
и
sl_do_cleanup()
.
29.01.2015: да, сделано: регистрируется через
CxsdHwSetCleanup()
, и вызывается ДО
{fdio,sl}_do_cleanup()'ов. В stand.c оно уже заюзано, а в
cxsd.c нет, поскольку там СЕЙЧАС никакие cxlib/cda не
влинковываются.
FreezeDev()
-- самая простая.
ReviveDev()
--
27.06.2016: уже нет, НЕ сбрасывает (подробнее см. "Закомментировываем RstDevRflags()").
RstDevTimestamps()
,
27.06.2016: и timestamp'ы уже тоже не нулит, а только циклы.
ReRequestDevData()
.
InitDevice()
при
успешной инициализации драйверов, то обходится тонкий аспект "а если
более ранний драйвер потребует данные от более позднего, еще не
инициализированного" --
ConsiderRequest()
'ом
выставлены флажки rd_req/wr_req, но SendChanRequest()
запрос не будет отправлен, зато...
ReqRofWrChsOf()
-- вычитывание rw-каналов.
21.11.2014: продолжение:
dev_p->metric->ЧТО_ТО
проверять, что
dev_p->metric!=NULL
-- а оно может, при
незагруженности драйвера.
В тройку мест добавлено, но мож еще где осталось...
TerminDev()
, чтобы прописалось CXRF_OFFLINE (плюс
description), а то каналы оставались просто нулями, но не
поболотовевшими.
27.02.2015: в TerminDev()
отсутствовал вызов layer'ова disconnect()
'а. Добавлен.
Кстати, в CxsdHwActivate()
НЕ делалось
ENTER_DRIVER_S()/LEAVE_DRIVER_S() при инициализации layer'ов. Тоже
добавлено (пока вроде БЕЗ последствий).
29.05.2015: помечаем как "done".
07.07.2015: вылезло, что при скопычивании девайса (например, отвал CANGW) в клиентах это НЕ отображается, в отличие от v2, где ручки сразу же бордовеют.
Причина -- иная модель протокола клиент-сервер: данные ведь шлются не каждый цикл, а только по изменению, и при изменении только флагов никакой UPDATE не дёргается.
Решение оказалось очень простым:
CXSD_HW_CHAN_R_STATCHG
=1 (единица как раз
была зарезервирована, "для унификации с cda" -- где используется именно
под STATCHG).
SetDevRflags()
, вызываемой
по DEVSTATE_OFFLINE
и DEVSTATE_NOTREADY
.
CXC_CURVAL
...
07.07.2015: кстати, небольшой косячок нынешней
реализации: поскольку в SetDevState()
стоит проверка
if (state == cxsd_hw_devices[devid].state) return;
то повторные вызовы DEVSTATE_NOTREADY с ИНЫМИ значениями
rflags_to_set
и description
ничего не
поменяют.
08.09.2015: да, не меняли. На remdrv проявлялось: сначала он стартует в DEVSTATE_NOTREADY, и всем каналам ставится CXRF_OFFLINE. А потом (указывался недоступный по сети контроллер) он пытается указать CXRF_REM_C_PROBL, но оно уже нидокуда не доходит.
Короче -- сейчас проверка закомментирована. ...но это тоже не идеал -- лишние действия/траффик при постоянной недоступности.
21.06.2016: ПРОБЛЕМА: еще некоторое время назад, при запинывании драйверов cPCI, а потом и CAN, было замечено, что некоторые каналы "навеки" остаются DEFUNCT.
Но никак не удавалось понять причину.
А иногда "оживает" даже позже -- в случае удалённых драйверов.
А переход в OPERATING -- это ReviveDev()
, который делает
RstDevTimestamps()
, тем самым о-DEFUNCT'ивая значения. Вот и
получается, что значения есть (и верные!), но посиневшие.
Для CAN-устройств -- где инициализация реального драйвера происходит ПОСЛЕ завершения init_d() -- сработало бы.
Для локальных -- вроде cPCI и vdev-based -- нет:
InitDevice()
вызывает прямо ReviveDev()
, а не
SetDevState()
, и timestamp'ы точно так же сбросились бы.
RstDevTimestamps()
из
Revive во Freeze (ну и в InitDevice() тоже делать, чтоб изначально было
"никогда").
Но вместе со сбросом timestamp'ов также делается и
RstDevRflags()
-- а флаги-то у "уже вёрнутых" каналов тоже
имеют значение (например, UNSUPPORTED), и их сбрасывать нельзя!
Т.е., как бы не вытанцовывается и такой вариант тоже -- ведь сброс флагов перенести в Freeze нельзя (там, наоборот, что-то ставится).
RstDevRflags()
? В v2 оно отсутствовало (точнее, вызов был
закомментирован), флаги нулились только при старте сервера и потом в случае
возврата нулей драйвером.
26.06.2016@ванна: пара мыслей на тему:
InitDevice()
прописать, чтоб были NEVER_READ.
upd_cycle
: вот его НАДО сбрасывать, чтоб
чтение тут же выполнилось. И да, именно в Revive.
27.06.2016: делаем:
RstDevRflags()
.
...при этом crflags
никогда не сбросятся, но они у нас
де-факто никогда никуда не отдавались. А если понадобятся -- то их
отдельный сброс сделать можно.
upd_cycle
'ов выделен в
RstDevUpdCycles()
, и вот ОНА -- вызывается из Revive.
RstDevTimestamps()
из Revive убрано и вставлено в
начало InitDevice()
.
Вроде помогло. Через некоторое время поставим такой вариант на пульт и посмотрим, где еще что вылезет.
P.S. Не очень понятно, это ли причина синести канала ISET_CUR у vdev-девайсов, т.к. вроде бы там состояние OPERATING должно уставляться ДО возврата каналов.
11.04.2018: кстати, близкая проблема -- что каналы навечно "остаются гореть болотным" -- 27-02-2017.
12.04.2018: там вроде тоже решение намечается.
ReRequestDevData()
тоже
надо б сделать.
20.11.2014: вроде ничего мега-сложного, но сам процесс отправки муторен (из-за необходимости вместо {first,count} указывать номера, а главное -- при записи еще и адреса, dtype'ы и проч.).
28.11.2014: за вчера-сегодня вроде сделано, хотя пока еще не проверено.
Тонкий момент: в StoreForSending()
сейчас НЕ всегда
сохраняется в *next_wr_val
, а брать-то при перезапросе
можно только оттуда. Так что надо делать сохранение ВСЕГДА -- для чего
сейчас там сделано считание, что "всегда force".
08.07.2015: то "ВСЕГДА" сделано еще тогда.
Сейчас же начали тестировать на реальном железе (CANGW, делая Ctrl+C canserver'у), и вылезли глюки: после возврата в DEVSTATE_OPERATING каналы НЕ перезапрашивались.
Оказалось, что косяк в ReRequestDevData()
: он
SendChanRequest()
'у давал номера каналов уже ЛОКАЛЬНЫЕ для
драйвера, а надо было глобальные. После удаления
-dev->first
проблема ушла.
Но в этой же строчке была еще одна ошибка: там НЕ делалось "+x". Т.е., изначально (с осени 2014) строка была
addrs[x] = first - dev->first;
вместо должного
addrs[x] = first + x;
-- в результате она по seglen раз спрашивала первый канал сегмента.
16.09.2019: сюда же, несколько задним числом:
Плохо, что ВСЕГДА отправка выполняется через копирование в next_wr_val, даже если типы совпадают и не нужно откладывать -- когда можно было бы напрямую, для экономии (для больших объёмов данных критично).
ReRequestDevData()
-- иначе
откуда б ей брать данные для записи, если они НЕ будут сохранены в
*next_wr_val
.
Так что, увы, придётся с таким "неоптимальным" поведением мириться.
...если только не захочется сделать этакую "опцию канала" со смыслом "не надо сохранять данные для записи; если пропадут при рестарте устройства -- то и фиг с ними".
MustSimulateHardware
, а то сейчас оно пораскидано в разных
местах и не особо-то согласовано.
29.05.2015: сделано вчера, в рамках реализации канала
_devstate_description. Как бы не супер-корректно (т.к. НЕ в
cxsd_hw_dev_t
), но приемлемо -- хранимое значение легко
добываемо (если понадобится, например, консоли).
Без этого становится невозможно двигаться дальше.
22.11.2014: первоначальная реализация
SetChanRDs()
.
Идея в том, что эти числа обычно одинаковы у толпы каналов (например, у многоканального АЦП), и организовывать передачу массива просто ни к чему.
А где понадобятся разные значения для разных каналов -- там можно и несколько вызовов сделать, операция-то однократная (ну или редкая).
Проверено на cac208 (каналы OUT_RATE) -- работает.
И новый код события введён -- CXSD_HW_CHAN_R_PRMSCHG
,
аналогично cda'шному. Правда, обработка его в cda_d_insrv.c
еще недоделана. 10.01.2015: "PRMSCHG" повсеместно
переименовано в "RDSCHG", для большей понятности.
23.11.2014: если подумать, то передавать набор каналов как {count,addrs[]} неудобно: ведь практически всегда такие однотипные каналы идут последовательной группой, так что и указывать их надо как {first,count} -- и работать с таким набором проще, и драйверам легче отдавать.
(Кстати, сама парадигма count,addrs[] была введена для упрощения реализации параметризованных запросов (замены v2'шных больших каналов) -- чтоб отдавалась одним махом куча разнородной информации. А с {r,d} совсем другой случай -- информация максимально однородная.)
Переделано.
18.12.2015: давно уже работает -- "done".
23.11.2014: начало:
cxsd_hw_chan_t.fresh_msecs
-- просто int, а не
cx_time_t
, для простоты; да и идеологически нет смысла в
сильно длинных временах, наверное.
cda_dat_p_update_dataset()
правило "неуказанным считается
timestamp с sec==0" -- ибо изначально каналы имеют как раз sec=nsec=0.
Так что теперь правило
неуказанным считается timestamp по NULL, а любой иной -- указанный, в т.ч. sec=nsec=0 -- никогда не измеренное02.12.2014: теперь sec=1 -- никогда не прочитанное, а sec=0 -- "trusted" (для каналов записи).
В v2-то на них отдавалось age=0 при отсутствии висящих запросов, и всё выходило красиво: при age=0 всегда висели "свежие", а при невыполненной записи посиневали.
Здесь надо что-то подобное придумывать. Кабы не пришлось делать sec==0 признаком "пофигу когда обновлялся, но свежий".
24.11.2014: продолжение:
SetChanFreshAge()
.
CXSD_HW_CHAN_R_FRESHCHG
.
25.11.2014: далее:
cda_dat_p_update_dataset()
.
Оно определяется константой INITIAL_TIMESTAMP_SECS
,
используемой также в RstDevTimestamps()
-- т.е., и по
оживанию устройства делается ровно то же.
CXSD_HW_TIMESTAMP_OF_CHAN()
-- аналоге v2'шной
FreshFlag()
.
Но флаг MustCacheRFW
игнорируется (в отличие от).
А собственно нулевой timestamp -- он ведь возвращается по указателю
-- лежит в cxsd_hw_zero_timestamp
.
Поэтому переименовываем в и меняем значение с 0 на
CX_TIME_SEC_NEVER_READ
.
...кстати, CXSD_HW_TIMESTAMP_OF_CHAN()
используется
только в cda_d_insrv.c.
CXSD_HW_TIMESTAMP_OF_CHAN()
-- для
не-запрошенных rw-каналов возвращать TRUSTED, а вовсе никакое не
NEVER_READ.
CXSD_HW_TIMESTAMP_OF_CHAN()
смысл секунд ==0 и ==1 уже был
переделан с "0:never_read, 1:trusted" на современное "0:trusted,
1:never_read"; это видно по тексту в зачале записи выше от 25-11-2014
(а СТАНДАРТИЗОВАНО было 02-03-2015). 17-07-2015 же почему-то
подумалось, что код делался еще ДО переделки, вот механически бездумно
и было поменяно.
cxsd_hw_trusted_timestamp V({CX_TIME_SEC_TRUSTED, 0});
Кроме того:
PutFrAgChunkReply()
.
cda_dat_p_update_dataset()
, а не в момент перерисовки на
экране).
Может, всё-таки реализовать эти мозги в общей части библиотеки? Видимо, где-то на стыке cda и Cdr.
Логика такая:
"Как-то" -- это в refinfo_t
, булевский флажок, по
умолчанию означающий "НЕ проверять". Или всё-таки по умолчанию
проверять? Ведь для ФОРМУЛ -- надо НЕ проверять, а для
никогда-не-вёрнутых, по идее, наоборот?
cda_get_ref_dval()
и его
аналогов. Следовательно, проверка должна быть там.
gettimeofday()
с
последующей конверсией в cx_time_t
-- извращение.
refinfo_t
булевский флажок, который взводить при обновлении данных и сбрасывать
при вычитывании (в конце его!); и чтоб если при входе в вычитывание он
всё ещё сброшен, то считать, что обновления не было, значит, можно
перепроверить время на тему устаревания.
НО! Один и тот же dataref может использоваться в куче мест (ручек),
поэтому такой подход не прокатит. Скорее уж надо в каждом
refinfo_t
иметь поле last_process_time (struct timeval!),
и чтоб "клиент" передавал параметром текущее время (начала обработки),
а при его несовпадении с last_... повторялась бы проверка на устарение.
Вопрос, как это корректно организовать.
И еще: ФОРМУЛЫ-то могут быть отражением/представлением обычных каналов, и формула, впрямую маппирующаяся на канал, должна себя вести в точности как этот канал.
Короче -- неясностей много, нюансы вроде записаны, так что отложим на будущее.
cx_time_t
, а не int'овыми миллисекундами.
Потому что усложнение будет плёвое, а выигрыш -- отсутствие компромиссов/исключений, и, соответственно, потенциальных проблем в будущем.
27.11.2014:
01.12.2014:
cx_time_t
.
Указанность считается при
fresh_age.sec > 0 || (fresh_age.sec == 0 && ri->fresh_age.nsec > 0
Immediate-значения указываются при помощи
(cx_time_t){SECS,NSECS}
ReturnInt32Datum()
, возвращающий
наверх ровно ОДНО 32-битное значение.
Учитывая, что бОльшая часть каналов именно такие -- может, ввести его в официальный API?
02.12.2014: да, введён.
24.04.2015: дополнительно вводим
ReturnInt32DatumTrusted()
, возвращающий с timestamp'ом
"TRUSTED" -- для каналов записи.
29.03.2016: добавлен
ReturnInt32DatumTimed()
, позволяющий указывать timestamp.
Нужен для vdev и vdev'ных драйверов.
26.11.2014: мысли:
Например, так:
Только как-то шибко замудрённо. И вопрос, корректно ли оно будет себя вести при изменении {списка мониторируемых каналов} посреди цикла.
Вообще, откуда лезут все мысли по "оптимизации"? Наверное, оттого, что "некрасиво" будет прилёт кучи разрозненных пакетиков с изменениями -- при этом скопом (у одного CAC208 режим сменили -- и вуаля). Ну и много syscall'ов получается, что в случае дохлых устройств вроде CANGW критично.
Но, может, забить на это -- ну и пусть пачкой мелких прилетает? Зато будет всё просто.
16.01.2015: да, сделано по-простому -- отправляются сразу, так что прилетают пачкой мелких. Зато всё просто и без подводных камней.
19.08.2015@пляж: более того -- malloc()'чить всё единым блоком, раскидывая 3 указателя по его содержимому (размеры округлять до кратно 16).
10.09.2015: а вот не всё так просто. Т.к.
"количество в конкретной БД" вовсе НЕ "известно сразу", а
подсчитывается в CxsdHwSetDb()
на stage=0, причём с учётом
данных из *chn_p (т.е., из места, которое "должно быть отведено в
будущем").
Явно надо больше полагаться на данные из db
-- уж
количества-то там теперь (месяца 4 как) есть. КРОМЕ layer'ов, их
генерит уже cxsd_hw.
...с другой стороны, если всё-таки добить полное разделение (независимость) между сбором статистики и аллокированием/инициализацией, то можно в тот же буфер и current_val_buf плюс next_wr_val_buf подселить.
22.05.2019: сделано в начале января. Описание в отдельном раздельчике, начинающемся 05-01-2019.
Повесить оное можно на ключик -S, который бы включал и -s тоже.
23.01.2019@лыжи-после-обеда-2-я-5-ка-конец-2-го-км: а ввести бы в сервере дополнение к ключу "-s" ещё и "-S" -- СУПЕРсимуляция, когда все read-каналы становятся rw. Смысл -- чтоб можно было симулировать поведение драйверов путём записи значений снаружи.
Как реализовывать:
Наиболее оптимальным местом выглядит CxsdHwSetDb()
: там и
переделка сводится к 1 строчке, и надо учитывать rw'шность для аллокирования
next_wr_val
.
См. также за сегодня же на тему per-device-суперсимуляции.
(Мысля пришла во время обдумывания реализации фичи "сделать N выстрелов" в kazstand (пока что v2'шном) -- ведь это надо будет как-то проверять.)
23.01.2019: итак, что мы видим: идея РОВНО такая же, как и три с половиной года назад.
Проверкой установлено, что ключ "-S" не используется ни в cxsd, ни в stand.c, так что свободно можно заюзывать.
26.01.2019@дома-утро: как делать -- понятно, но есть идеологическая засада: ведь эти "уровни симуляции" -- значения 0,1,2 -- надо будет как-то поименовать константами; а константы должны быть доступны и из cxsd_hw.c, и из cxsd_db_via_ppf4td.c, и из программ-серверов. Но у нас НЕТУ никакого cxsd_*.h-файла, общего для cxsd_hw и cxsd_db!
26.01.2019@дома-после-обеда: думал-думал -- придётся такой файл ввести. Первой идеей было "cxsd_lib_commons.h", но это будет пересекаться по первой букве с cxsd_logger.h.
Так что выбрано имя cxsd_core_commons.h. А константы сделаны
CXSD_SIMULATE_OFF
=0, CXSD_SIMULATE_YES
=1,
CXSD_SIMULATE_SUP
=2.
26.01.2019@пультовая, полседьмого вечера субботы: а ещё, оказывается, есть lib/srv/cxsd_lib_includes.h -- хоть директория и другая, но начало то же, так что внесло бы путаницы.
26.01.2019@пультовая, полседьмого вечера субботы: собственно реализация.
CxsdHwSetDb()
:
chn_p->rw
теперь форсится в "true" также при
CXSD_SIMULATE_SUP.
next_wr_val_bufsize
и
аллокировании next_wr_val
.
CxsdHwSetSimulate()
убрана "буленизация".
-S
и строчка о нём в help по -h.
28.01.2019: проверил -- ключик работает, "done".
CXDTYPE_UNKNOWN
-- аналогично реализованному недавно в
cda.
CXDTYPE_DOUBLE
-- это даже лучше всего для "публикования"
данных для внешнего использования.
Поэтому на сейчас, видимо, правильнее закрепить принцип "публикуем получающиеся double'ы".
19.03.2016@утро-лыжи: саму
реализацию можно будет скопировать из cda_core.c, в первую очередь
cda_dat_p_update_dataset()
-- у них с
ReturnDataSet()
'ом внутренности очень похожи, почти близнецы.
И точно так же надо будет ввести (временно?) запрет на запись в UNKNOWN-каналы.
19.03.2016: в описателе в devlist'ах такие каналы надо обозначать буквой 'x' (потому что "x" -- неизвестное). Только аллокировать по умолчанию 16 байт, а не 1 (это очевидным образом достаточно для любого скалярного значения).
06.04.2016: приступаем к изготовлению.
CXSD_HW_SUPPORTS_CXDTYPE_UNKNOWN
(который сейчас в
cxsd_hwP.h уставлен в 1).
cxsd_hw_chan_t.nelems
переименовано в
max_nelems
(и года не прошло с аналогичного
переименования refinfo_t.nelems
21-04-2015 :)).
CxsdChanInfoRec.nelems
(используется в
CxsdDbDevLine_t
).
CxsdHwSetDb()
.
ReturnDataSet()
:
Отличие от cda -- что НЕ нужно дополнительной ячейки для REPR_TEXT.
current_dtype
вместо
просто dtype
--
repr
и size
.
switch()
'ах при выборе формата для
складирования.
ConsiderRequest()
.
Проверять?
P.S. Замечание: в обоих случаях -- cda_core и cxsd_hw -- не совсем корректно будет обрабатываться ситуация с max_nelems=1 (т.е., формально скаляр) и ssiz>1: в такую ячейку не вместится ничего, кроме однобайтных данных, а многобайтные приведут к nels:=0. В оправдание можно сказать, что:
13.04.2016: mirror_drv.c написан (копированием trig_read'а с обкарныванием и модификацией парсинга описания канала).
14.04.2016: пытаемся испытывать... На вид в основном вроде работает, но мешают какие-то косяки непонятного свойства (к делаемому вроде отношения не имеющие) -- что-то с чтением w-noop-каналов.
И пара замечаний:
StdSimulated_rw_p()
к возможному UNKNOWN совсем не
адаптирован! А надо бы.
15.04.2016: мда, проблема крылась именно в
StdSimulated_rw_p()
:
CX_TIME_SEC_NEVER_READ
,
PutDataChunkReply()
после
модификаций в 20-х числах марта стало приводить к отправке
CXC_CURVAL
вместо CXC_NEWVAL
;
Поэтому на сейчас сделано ВСЕГДА возвращение текущего времени (указывается timestamps=NULL) и всё очухалось.
15.04.2016: дальнейшие длительные мучительные тесты показывают, что вроде бы 'x'-каналы работают как надо:
18.04.2016: StdSimulated_rw_p()
адаптирован. Это свелось к тому, что если тип обрабатываемого канала
==UNKNOWN, то он превращается, в зависимости от max_nelems
(т.е., от того, какого размера данные способен вместить канал) в INT8, INT16
или INT32.
Проверено -- функционирует.
cda_dat_p_update_dataset() incompatible dtype in localhost:9.m.0: 0 != 19
CxsdHwResolveChan()
.
23.03.2016: да и для внешней БД -- вроде еманофединой -- такое жуликоисключение будет кошмаром.
Так что ну нафиг -- "withdrawn".
CxsdHwSetDb()
при аллокировании
cxsd_hw_current_val_buf
ему НЕ делалось bzero().
В результате позавчера на vepp4-pult6 (SL-5.5, 2.6.18/i686) полезли всякие бредовые начальные значения. Причём лезло оно только в Chl-клиентах -- поскольку они показывают начальные значения, а вот cdaclient -- нет, он только обновления.
CXC_CURVAL
появился лишь 16-03-2015, а до того
значения должны были передаваться только по реальному обновлению.
Добавлен bzero()
, причём только для
cxsd_hw_current_val_buf
, а cxsd_hw_next_wr_val_buf
нет, поскольку использование его содержимого чётко флагируется.
ReviveDev()
-- запросы отправляются на исполнение,
путём вызова ReRequestDevData()
.
Вопрос -- а насколько корректно так поступать? Может, правильнее всё-таки запросы на запись ОТБРАСЫВАТЬ? (Навеяно нынешним функционированием конверсии: если запись придёт до первого оживления, то в ней НЕ поучаствуют аппаратные {r,d}, и число будет неверным.)
Например, в v2 в stop_dev()
делалось
Remit{Chan,Bigc}RequestsOf(devid)
. Правда, это именно при
останове -- т.е., при переходе в DEVSTATE_OFFLINE.
10.05.2016: подумавши --
11.09.2019: а ведь до сих пор
«при "убиении" устройства» запросы на запись в его каналы
НЕ отбрасываются: в TerminDev()
ничего на эту тему нет,
и в иных местах cxsd_hw.c также нуления wr_req
не
видно -- оно есть ТОЛЬКО в ReturnDataSet()
.
Очевидно, потенциальная проблема есть, просто она не проявляется из-за НЕиспользования механизма "гроханья/рестарта" драйверов.
27.05.2016: сделано за пять минут:
CXSD_HW_DRVA_IGNORE_UPD_CYCLE_FLAG
=1<<16,
объявлен в cxsd_hwP.h
.
ConsiderRequest()
добавлен параметр
ignore_upd_cycle
, блокирующий "проблемную" проверку.
CxsdHwDoIO()
вычленяет из action
значение
бита IGNORE_UPD_CYCLE_FLAG в переменную для передачи Consider'у, а из
action
его убирает (нулит).
26.11.2018: только с той реализацией была проблема -- бесконечно частый возврат просто-читаемых каналов, запрошенных с on_update. Подробнее см. за 11-10-2018...12-10-2018.
Поэтому введен режим DO_IGNORE_UPD_CYCLE
, взводящий
cxsd_hw_chan_t.do_ignore_upd_cycle
, дополнительно проверяемый в
ConsiderRequest()
.
Детали:
DO_IGNORE_UPD_CYCLE
=4, а значение 3
зарезервировано (чтобы разделить по битовым полям).
cxsd_hw_chan_t
зарезервированное поле res2
заменено на do_ignore_upd_cycle
.
Благодаря той зарезервированности даже внутренний ABI не изменился -- нигде никакие поля/смещения не поехали.
SetChanReturnType()
:
is_autoupdated
" переименован в
return_type
-- это давно напрашивалось.
Интерфейс от этого переименования опять же не пострадал.
is_autoupdated
выставляется
не в параметр!=0, а в
(return_type == IS_AUTOUPDATED_YES || return_type == IS_AUTOUPDATED_TRUSTED)
do_ignore_upd_cycle = (return_type == DO_IGNORE_UPD_CYCLE)
ConsiderRequest()
теперь цикл игнорируется только если ОБА флага --- каналово свойство
do_ignore_upd_cycle
и переданный запросом параметр
ignore_upd_cycle
-- не нули.
remdrv_data_set_return_type_t
поле
is_autoupdated
переименовано в return_type
.
Это также никак не скажется на совместимости -- формат-то сохранился.
DO_IGNORE_UPD_CYCLE
.
Это делают cpci_fastadc_common.h и camac_fastadc_common.h; а VME'шные adc4x250_drv.c и adc1000_drv.c самостоятельно (т.к. vme_fastadc_common.h пока нету).
А для ИПП'шностей -- u0632_drv.c и canipp_drv.c -- оно не нужно, т.к. там драйвера САМИ постоянно делают опрос максимально возможно часто.
Теперь тестировать надо. Ориентировочно -- в следующий понедельник (03-12-2018).
15.12.2018: протестировано -- вроде всё окей:
Теперь надо будет ещё на еманофедином софте проверить.
15.07.2016@утро-пляж: Если подумать -- то сплошные бонусы и удобства:
Есть, конечно, и сложности:
Что делать: менять его тип на rw=1 или ставить особое условие в CxsdHwDoIO()/ConsiderRequest()/SendChanRequest()?
А действия нужны такие:
Замечание: да, privrec будет повторно аллокироваться. Поскольку в TerminDev()'е он освобождается.
Вот со включением и ресетом некоторая неопределенность. Можно, конечно, использовать для них 1 и 0, но лучший ли это вариант?
11.07.2017@утро-душ: кстати, в случае такой реализации "sato" etc. даже потребность в cx-console реально исчезнет:
...только листинг "всего сразу" так просто не получишь; но это нужно пореже, чем рестарт.
03.12.2018@вечер-пультовая: да, потребность есть.
Надо бы как-то разделять -- чтобы запись в "специальные" каналы не группировалась бы в один пакет с записью в обычные каналы.
Это можно сделать, введя дополнительный DRVA_-код, чтоб он
возвращался ConsiderRequest()
'ом на запись в
is_internal-каналы, и тогда CxsdHwDoIO()
будет группировать
такие каналы лишь с такими же.
04.12.2018@дома: (на улице -30, решил поработать дома -- и очень успешно!): делаем.
DRVA_INTERNAL_WR
=-1000.
ConsiderRequest()
теперь возвращает его вместо
DRVA_WRITE
при авторизации записи в
is_internal
-каналы.
CxsdHwDoIO()
интерпретирует его наравне с _READ и
_WRITE -- сбагривает SendChanRequest()
'у.
SendChanRequest()
проверяет, что если первый (gcids[0]) из
переданных ему каналов имеет взведённый флаг .is_internal
, то
вместо драйверова метода do_rw()
вызывается свежевведённая...
is_internal_rw_p()
:
count
).
CXSD_DB_CHAN_DEVSTATE_OFS
:
07.12.2018: чорд!!! Брать надо было из
values[n]
, а не values[n]
. Ведь
x
-- это номер КАНАЛА, индекс в массивах -- n
.
Так что исправлено, и вообще n
заменено на chn
--
как в драйверных _rw_p()
. (Наткнулся случайно, из-за
недопеределанности (недообобщённости) в
cxsd_db.c::CxsdDbResolveName()
, когда каналу
_logmask по ошибке сопоставлялось значение 2
, теперь
принадлежащее _devstate'у.)
TerminDev()
.
TerminDev()
; 2) включение через InitDevice()
.
InitDevice()
.
CXSD_DB_CHAN_LOGMASK_OFS
.
CxsdHwSetDb()
теперь каналу _devstate
ставится rw=1
.
CxsdHwDoIO()
добавлена проверка (после
выкусывания флажка CXSD_HW_DRVA_IGNORE_UPD_CYCLE_FLAG
), что
переданный код -- либо DRVA_READ
, либо DRVA_WRITE
;
в противном случае делается return -1
.
SendChanRequest()
интерпретировать код
DRVA_INTERNAL_WR
аналогично обычному DRVA_WRITE
:
сдвигать dtypes,nelems,values на offset и seglen, а не делать им =NULL.
05.12.2018: проверяем. После исправления мелкого косячка (см. последний пункт во вчерашнем списке) -- работает!!!
Пара дополнительных проверок (тестирование работы остальныъ компонентов):
CxsdHwDoIO()
, где один и тот же
канал содержится несколько раз (например, принудительная запись {-1,+1}).
Результат: облом. Не то, что не выполняется цепочка, а даже НЕ записывается последнее: вместо него пишется только ПЕРВОЕ. Например, триплет {-1,0,+1} вместо ожидаемого {-1,+1} приводит к {-1,-1}.
Причина неясна, надо разбираться.
SEGLEN_MAX
. Причина -- есть подозрение, что в
SendChanRequest()
присутствует косяк: там НЕ делается
продвижение указателя на номера -- gcids+=seglen
.
Результат: да, косяк реально имел место. Видимо, он никогда не проявлялся из-за того, что никогда не бывало запросов на такие длинные цепочки.
Исправлено, очевидным образом.
Этот же раздел можно считать за "done".
06.12.2018: мелкая модификация: значения
DRVA_IGNORE
и DRVA_INTERNAL_WR
поменяны с
-999
(унаследовано ещё от v2) и -1000
на
1024
и 1025
-- чтоб не пересекаться по битам с
CXSD_HW_DRVA_IGNORE_UPD_CYCLE_FLAG
=1<<16.
15.04.2019: "вылез косяк": при попытке записи в канал
_devstate
получаем SIGSEGV в is_internal_rw_p()
.
Происходит оно в точке вычитывания значения ival
по адресу из
values[].
Ошибка нашлась быстро: в этой строке стояла некорректная адресация -- по
chn
(номер канала) вместо n
(индекс в массиве):
ival = *((int32*)(values[chn]));
вместо
ival = *((int32*)(values[n]));
Что произошло:
Короче -- исправлено (в 2 местах, кстати).
09.09.2019@пультовая:
чё-то плохо работает запись -1
в _devstate -- вместо
останова драйвера делается reset.
Очень это неудобно: нужно было временно тормознуть драйвер adc200 в крейте 1-го клистрона (v5-l-kls1, cxhw:25.k1_osc) для разбирательства "ручным" выполнением NAF'ов, а пришлось вместо этого делать "kill -STOP" процессу драйвлета.
10.09.2019: проверка показала, что вообще ЛЮБАЯ запись интерпретируется как "reset" -- и -1, и 0, и даже +1.
Проверено на самой свежей версии cxsd -- т.е., точно дело не в "старой версии на пульте".
10.09.2019@пульт: ещё проверки:
is_internal_rw_p()
--
на вид всё корректно.
is_internal_rw_p()
ждёт
int32
, о чём чётко сказано у него в комментариях в п.4.
Вдруг просто преобразование где-то не было выполнено, и пришло "не то" значение? Как показывает опыт, небольшие вещественные числа могут в младших разрядах иметь нули -- вот оно и было понято как 0, что и привело к reset'у.
Попробовал слать @i -- да, всё функционирует как должно! Вывод: проблема действительно в преобразовании типов.
...крамольная мысль: а в остальных-то случаях преобразование выполняется? Точно? И точно именно как надо?
Причина очевидна: ветка кода, обрабатывающая вариант "ival > 0", что-то делает только для OFFLINE-устройств, а для прочих -- ничего. В результате состояние _devstate не меняется и не возвращается "наверх", так что cdaclient и не получает обновления.
@получасом позже, в пристройке:
напрашивается решение добавить туда "else" с холостым вызовом
report_devstate()
. Оно как бы спорно с точки зрения
timestamp'а: с одной стороны -- нехорошо, что он как бы поменяется (т.к.,
реально-то ничего с устройством и не делалось); а с другой -- ведь запись-то
в _devstate была, так что типа всё верно.
@ещё получасом позже: а если просто вызвать уведомление об изменении --
CxsdHwCallChanEvprocs()
-- для _devstate и
_devstate_description? Тогда и уведомления будут, и timestamp'ы не
изменятся. Хотя это и более муторно, чем просто вызов
report_devstate()
.
10.09.2019: пытаемся разобраться и решить проблему.
report_devstate()
сходу не прокатил:
точка, где его надо вызвать, находится выше его объявления, так что при
простодушном вызове получилась ошибка "static declaration of
'report_devstate' follows non-static declaration".
CxsdHwCallChanEvprocs()
. Получилось -- cdaclient счастлив :-).
И при печати переданного значения путём "%f" видно, что действительно приходит 1.0.
ConsiderRequest()
'е в ветке "action == DRVA_WRITE" вызов
StoreForSending()
-- который и выполняет конверсию! -- делался
только при f_act == DRVA_WRITE
, а вариант
DRVA_INTERNAL_WR
проходил мимо.
Вот преобразование типа и не выполнялось, так что оставалось и передавалось ВЕЩЕСТВЕННОЕ.
Добавил альтернативу || f_act == DRVA_INTERNAL_WR
.
...и получил SIGSEGV...
11.09.2019: ну, делаем выделение next_wr_val'ов для
is_internal-каналов. Ключевое слово тут --
FillInternalChanProps()
.
current_val_bufsize
делаются именно такие махинации --
с конкретно таким использованием usize
в начале и
csize
в конце.
Понял: usize
в начале используется для выравнивания по
границе типа данного канала, а сдвиг на csize
в конце -- это
"потребление" куска буфера.
current_val_bufsize
, но и с next_wr_val_bufsize
,
то придётся перейти от передачи "текущего" по значению и возврата его
модифицированного к просто передаче по указателю и модификации прямо внутри
функции.
Окей -- переделана сама функция FillInternalChanProps()
.
Затем все 4 её вызова.
Чуть-чуть поэкспериментировав -- разобрался: всё работает, а проблема при ДВУХ ПОДРЯД попытках записи +1. Из-за того, что ветка "ival > 0" умничает и вместо реальной отработки записи просто вызывает уведомление.
А флаг wr_req
остаётся гореть, и последующие попытки записи
уже обломятся.
Добавил сброс флага -- всё стало окей.
MustCacheRFW
(ключ -wN
) -- поскольку
никаких и попыток-то делать "чтение" нету.
option_cacherfw
) библиотеке libcxsd, так что всегда было
MustCacheRFW=1.
CxsdHwSetCacheRFW()
в
ReadHWConfig()
(и в stand.c тоже).
Ясно: это cxsd_fe_cx.c "умничает", ничего не зная о
MustCacheRFW
(который вообще static
в
cxsd_hw.c) он просто отдаёт текущее значение с кодом CXC_NEWVAL,
поскольку у канала аж ДВА "нужных" свойства --
is_internal
и rw
(и timestamp.sec почему-то
не-NEVER_READ).
Короче -- похоже, затея "MustCacheRFW=0" не особо-то функциональна, по крайней мере, для is_internal-каналов, так что проще на это забить (всё равно в реальности никому нафиг не нужно -- последние лет 16, с ухода Олега Токарева в 2003-м).
13.09.2019: долго пытался понять -- почему же всё-таки
корректно работают is_internal-каналы даже при -wN
-- откуда у
них берётся не-0/1 timestamp; конкретно речь о _logmask.
И так, и эдак -- всё равно не понял. И из
is_internal_rw_p()
даже надлежащее ругательство на stderr
выдаётся -- что "action=%d, !=DRVA_WRITE". Но всё равно разумные
данные есть.
15.09.2019@утро-зарядка: надо поискать в cxsd_hw.c по "wauxcount" -- может, там просто оптом всем, в цикле, проставляется момент инициализации?
16.09.2019@утро-пультовая, после
планёрки: поискал -- неа. Притом, что в
FillInternalChanProps()
явно прописывается
chn_p->timestamp.sec = INITIAL_TIMESTAMP_SECS
(=CX_TIME_SEC_NEVER_READ).
Несколькими минутами позже: а-а-а, нашёл! В InitDevice()
есть вызов report_logmask()
, а уж он вызывает Return...(),
который timestamp и проставляет.
StoreForSending()
осенью 2014-го: она модифицирует содержимое
переданного ей массива dtypes[]
.
Вылезло это при создании cxsd_fe_starogate.c, где значение
передаваемой CxsdHwDoIO()
static-переменной
dt_double
=CXDTYPE_DOUBLE портилось, заменяясь на CXDTYPE_INT32.
14.03.2018: пытаемся детально разобраться в ситуации.
Казалось бы -- ну конвертируются после складирования данные в тот тип, что указан в chn_p->dtype, вот его и использовать/подразумевать. Но нет, так просто не получится -- этот тип нужен в элементе массива.
Что можно сделать:
cxsd_hw_chan_t
дополнительно к
next_wr_nelems
еще и next_wr_dtype
(чтоб
StoreForSending()
ВСЕГДА писал туда).
Но это не очень удобно -- в прочих местах используется массив, а тут будет поле в другом массиве.
StoreForSending()
иметь
массивец.
Но тут проблема с размером/количеством: в прочих-то ограничение
SEGLEN_MAX=1000, а в вызывальщике Store -- ConsiderRequest()
--
ограничения не предусмотрено.
StoreForSending()
и переданное ей nelems[] портит
-- в случаях, если там больше, чем max_nelems.
Чуть позже: и переданное values[] тоже меняет -- ставит туда ссылки на
chn_p->next_wr_val
.
Вывод:
CxsdHwDoIO()
параметры НЕ гарантируют сохранности
содержимого?
И именно так оно и сделано.
Вердикт: да, считаем, что так оно и должно быть, а данный раздел закрываем как "withdrawn".
После обеда: и даже комментарий в cxsd_hwP.h вставлен:
/* Note: CxsdHwDoIO MAY modify contents of dtypes[], nelems[] and values[] */
CxsdHwSetDb()
НЕ делается округления csize до размера 16, а
остаётся прям как есть. В результате может вылезти ошибка выравнивания --
например, если подряд идут каналы w1b и w1i, то буфер последнего будет
расположен по смещению 1, что неприемлемо.
14.03.2018: вариант: надо округлять не ПОСЛЕ аллокации очередного, а ПЕРЕД, по его кратности. Так можно будет избежать совершенно пустого расхода памяти -- т.к. большинство каналов int32 (1i1), то тратить 12 байт впустую глупо. ...кстати, ведь компиляторы именно так и размещают переменные и поля в структурах.
21.03.2018: делаем, в варианте "ПЕРЕД". Технология:
_bufsize = (_bufsize + usize-1) & (~(usize - 1))
(чисто арифметически "(~(usize - 1))
" эквивалентно
"-usize
, но корректнее с точки зрения знаковости/беззнаковости).
current_val_bufsize
"добивается до кратного"
всегда, а next_wr_val_bufsize
-- только если rw
.
Проверяем:
И _devstate-то -- INT32!
сервер даже без выравнивания работал и не падал.dev a noop w1i,w1b,w1d,w2b - dev b noop w1i -
#include <stdio.h> void main(void) { char a[]={1,2,3,4,5,6,7,8,9,10,11,12}; int *p = a+1; void **q = a + 2; printf("%x %p\n", *p, *q); }
Кстати, в gcc есть штучка по имени __alignof__
,
возвращающая требуемое выравнивание для типа/переменной. Синтаксис -- вроде
как у sizeof()
, но можно указывать и поля (чего sizeof() не
позволяет).
23.01.2019: давно работает, задеплоено, и даже далее -- уже аллокирование всего остального тоже делается. Так что "done".
Как?
И желательно было бы аллокировать всехнюю ОДНИМ блоком (как буферы _current_val_ и _next_wr_val_), а не поштучно.
Ясности нет совсем...
В любом случае, нужно вносить этот пункт в список хотелок на 2018 год.
21.03.2018@лыжи, вечер, конец-3-й-2-километровки: RD множественные можно указывать SLOTARRAY'ем "GROWING", искать там ячейку, ячейки аллокировать сразу на 20.
22.03.2018: а если ячейки делать на 16 -- в сумме 17, и для большинства этого хватит выше крыши, плюс оставит резерв (до 20) для дальнейшего навешивания 3 калибровок.
иначе сделать Rls*Slot() имеющейся (если есть).
22.03.2018: некоторые соображения вдогонку:
Тогда получится иметь ячейку на 15 дуплетов, а в первых sizeof(double)*2=16 байтах разместить служебную информацию, на роль которой сейчас претендует только количество дуплетов.
Вроде бы когда-то (кажется, в v2) предусматривалась возможность такого, путём указания '-' перед именем драйвера. Сейчас следов реализации сходу найти не удалось, но синтаксис выглядит разумным.
20.03.2021: следы нашлись -- bigfile-0001.html от 26-02-2007, ключевое слово
"d_simulate[]
".
04.12.2018@дома, после обеда (после реализации возможности рестартовать драйверы через запись в _devstate): надо б сделать -- и обдумано уже, да и как раз во внутренности cxsd_hw.c мозгами плотно погрузился.
Обмышлявшиеся варианты:
Вариант годившийся для v2, где имя особой роли не играло (только при загрузке драйвера надо было пропустить первый символ, если он дефис), но для v4 не катит: тут имя типа используется также и самой cxsd_db*.c, т.к. по нему берётся список каналов и их имён.
Вывод: флаг "это устройство симулируем!" надо бы выпарсивать в отдельное поле, а строку типа сохранять как есть, без лишнего мусора.
CxsdHwActivate()
смотреть, что если надо симулировать, то
подсовывать вместо драйверова metric'а некий специальный свой, с
единственным не-NULL методом do_rw
, глядящим на
StdSimulated_rw_p()
.
MustSimulateHardware
.
После прикидок, размышления и анализа выбран последний вариант.
cxsd_hw_dev_t
добавлен флаг
is_simulated
.
MustSimulateHardware
во
всех точках, где идёт обращение к методам устройства -- при загрузке
драйвера, инициализации/терминировании устройства, при вызове
do_rw()
.
CxsdHwSetDb()
из БД, где...
CxsdDbDevLine_t
также добавлен
is_simulated
.
05.12.2018: последний пункт реализации:
dev_parser()
добавлена
проверка, что если перед именем типа стоит '-', то он пропускается
и взводится dline.is_simulated.
(Думалось, что придётся дополнять список параметров
CxsdDbAddDev()
, но там отдельными параметрами передаются только
строки, а все прочие потроха -- прямо готовой структурой
CxsdDbDevLine_t
, так что достаточно оказалось писать в её
вчерадобавленное поле.)
06.12.2018: проверяем: да, работает.
Но есть в синтаксисе одно неудобство: '-' указывается перед
именем ДРАЙВЕРА (точнее, типа устройства), но у нас в куче конфигов драйвер
указывается в макросе -- вроде MAGX_IST_CDAC20_DEV()
-- так что
в самой строке объявления устройства имени типа нету и дефис подсунуть
некуда.
cpoint $1.Iset_rate $1.Iset_rate $3
ParseOneCpoint()
дополнительной проверки и отбрасывания
'-' перед считыванием обоих компонентов.
...вон в VMS'е '-' в пути (или только в начале?) означал "уровень вверх" -- как "../" в *nix/Dos/Windows. Ну у нас-то это при надобности будет ":", но всё же.
Короче: оставляем как есть, и если припрёт -- то решается просто (вон выше готовый рецепт прописан). А пока считаем за "done".
23.01.2019@лыжи-после-обеда-2-я-5-ка-середина: в дополнение к планируемому режиму "суперсимуляции" надо б уметь его включать и per-device. Символ '+' вместо '-'?
26.01.2019@пультовая, полседьмого вечера субботы: сделано. Состояло из 2 частей:
CxsdHwSetDb()
-- там оно просто в параллель
с обычной реакцией на
MustSimulateHardware >= CXSD_SIMULATE_SUP
.
28.01.2019: да, проверено, работает; причём и в
сочетании с -s
тоже работает: у всех каналов чиселки бегут, а у
'+''нутых устройств стоят нули, и можно писать -- извращайся как
хошь.
06.12.2018: делаем.
GetDrvlogCatName()
и
ParseDrvlogCategories()
?".
В v2 они обитали в cxsd_drvmgr.c, в v4 отсутствующем.
Тут первая была поселена (static'ом) в cxsd_driver.c (т.к. была
необходима vDoDriverLog()
'у), а вторая вообще отсутствовала.
Сейчас стало ясно, что селить их нужно в cxsd_hw.c.
GetDrvlogCatName()
переведён в cxsd_hw.c,
и ParseDrvlogCategories()
скопирован из v2.
07.12.2018: ага, только в вызове getopt()
в параметре optstring
отсутствовало "l:", так что
ругалось "invalid option -- 'l'". Добавлено.
Также оно добавлено и в stand.c.
07.12.2018: а в этом ещё и "M:", "F:", "E:", "L:" не было -- видать, даже не пытался проверять...
LogspecPluginParser()
скопирован из v2.
InitDevice()
-- т.е., он
производится в момент ИНИЦИАЛИЗАЦИИ устройства.
Ошибка парсинга options приводит к деактивации устройства -- точно так же, как и ошибка парсинга auxinfo.
CXSD_DB_AUX_CHANCOUNT
увеличилось до
4.
CxsdDbResolveName()
:
finding_special
там не просто 1 и 2, а принимает значение
CXSD_DB_CHAN_nnn_OFS+1 ("+1" чтоб !=0).
CxsdHwSetDb()
-- тут всё просто,
скопировано с _devstate'ового. Да, там теперь ЧЕТЫРЕ копии кода
аллокирования канала, аналогичных тому, что в цикле по основным каналам.
Специфика конкретно для _logmask:
07.12.2018: неа, нифига -- НЕ нужно никакой специфики, ведь потом при инициализации всё будет сделано. Так что то всё убрано.
InitDevice()
, то
оттуда же нужно и "возвращать" значение.
Для чего сделана вызываемая оттуда отдельная
report_logmask()
.
is_internal_rw_p()
: просто пишется и потом
вызывается report_logmask()
.
06.12.2018@вечер, дорога домой, около главного входа в ВЦ: кстати, ведь не очень удобно, что маску надо будет указывать числами, а не названиями категорий.
А можно б было сделать еще один канал -- "alias" -- именно строковый, чтоб в него писать строковую спецификацию вида +category,-category,..., а он бы должным образом её парсил и возвращал бы список "что есть сейчас" тоже текстовый.
Но -- нафиг реально не нужно.
...к тому же, была бы некоторая неприятность: раз спецификация с "+" и "-" означает "модифицируй ТЕКУЩУЮ в соответствии с указанным", то просто прописать значение (например, когда-то прочитанное) -- не получилось бы, т.к. пришлось бы к нему в начало добавлять "-all,".
07.12.2018: проверяем.
CxsdDbResolveName()
была недопеределана --
как раз вариант DEVNAME.CHANNAME остался недообобщённым, в результате чего
канал _logmask маппировался на 2 (это _devstate!) вместо
0.
Видимо, тупо из-за того, что этот кусок был ниже по тексту и остался за нижней границей окна (позор!!!).
С другой стороны, благодаря этому был найден баг в
is_internal_rw_p()
(впрочем, он в любом случае вскорости бы
обнаружился).
BeginOfCycle()
цикл по всему диапазону категорий и записав в
маску -1
. И-и-и...
GetDrvlogCatName()
был косяк: в проверке на
попадание категории в диапазон с назначенными именами вместо
"&&
" стояло "||
".
Раньше-то работало потому, что все драйвера такие "правильные" и передают исключительно одну из предопределённых категорий.
В результате SIGSEGV'илось, пытаясь проанализировать текст по адресу NULL на тему "не пустая ли там строка" (в первоначальном варианте, видимо, для категории DEFAULT было так -- чтоб не заполнять логи лишними данными; но следов того уже не осталось).
Это ещё с момента её создания в v2 08-12-2009 -- в w20091227.tar.gz он уже есть.
Короче -- исправлено. Там еще в левой границе сравнения надо было
поставить ">=
" вместо просто ">
".
-l MASK
и log=MASK:
Аллилуйя!!!
CxsdHwDoIO()
и его подчинённых: при тестировании реализации
записывабельного _devstate 05-12-2018 выяснилось, что если в одну
цепочку (передаваемую CxsdHwDoIO()
одним запросом) поместить
несколько записей ОДНОГО канала, то отработается только ПЕРВАЯ, а не
последняя, как должно бы быть (промежуточные должны быть выкинуты -- точнее,
перепрописаны последней).
Проблема, конечно, совсем не критичная -- в нормальных условиях в один вызов несколько записей одного канала попадать не должны бы.
Но в причине такого странного поведения разобраться надо.
-- на динамическое аллокирование, по мере надобности.D cxsd_hw_lyr_t cxsd_hw_layers [CXSD_HW_MAX_LYRS]; D cxsd_hw_dev_t cxsd_hw_devices [CXSD_HW_MAX_DEVS]; D cxsd_hw_chan_t cxsd_hw_channels[CXSD_HW_MAX_CHANS];
05.01.2019: предварительное обсуждение:
CXSD_HW_ALLOC_ALL
(который сейчас уставлен в 0
):
*cxsd_hw_NNN
вместо cxsd_hw_NNN[CXSD_HW_MAX_nnn]
.
23.04,2020: массивам cxsd_hw_layers[]
,
cxsd_hw_devices[]
, cxsd_hw_channels[]
по ошибке
было сделано V(0)
вместо надлежащего V(NULL)
.
Исправлено.
cxsd_hw_buffers
и аллокированный на него объём --
cxsd_hw_buf_size
.
CXSD_HW_ALLOC_ALL
тут и вводится.
CxsdHwSetDb()
только рудиментарные заготовки,
реального кода там не появилось.
Т.е., ABI всё-таки изменится, так что если наличествует какой-то динамически подгружаемый код ВНЕ ядра сервера, то он, откомпилированный под старую модель, просто перестанет работать.
Сейчас таковым внешним кодом является вроде только mqtt_mapping_drv, от готовности ещё далёкий.
cxsd_hw_channels[]
есть в trig_read_drv.c -- это когда
он пытается добыть свойства (dtype,n_items) для исходного канала, в случае
их неуказанности.
cxsd_hw_NNN[x].чего-нить
, либо к p =
cxsd_hw_NNN + x
, что, благодаря сишной фиче "массивы и указатели при
обращении синтаксически одинаковы", будет работать и в старом (массивы) и в
новом (указатели) вариантах.
CxsdHwSetDb()
-- вот там
выполняется bzero()
с пониманием реального типа объектов.
Но именно этот блок и находится внутри условия
#if CXSD_HW_ALLOC_ALL ... #else ... #endif
и будет заменяться
на на аллокирование.
cxsd_hw_buffers
,cxsd_hw_buf_size
надо через
GrowBuf()
.
Это задел на будущее, когда (если) будет перечитывание БД по ходу работы сервера: если обновлённый конфиг железа требует меньшего объёма, то будет использоваться уже аллокированное. Да, БЕЗ возможности уменьшения -- а она нужна?
Так вот: эти 2 прохода (0 и 1) УЖЕ делаются. Но сейчас они используются лишь для:
cxsd_hw_current_val_buf
и
cxsd_hw_next_wr_val_buf
.
А запись в "массивы объектов" производится на обоих проходах; да, 2 раза одно и тоже.
Требуются переделки:
if (stage)
".
current_val_bufsize
у is_internal-каналов -- идёт обращение на
чтение (к chn_p->...
).
Тут надо заменять на обращение к "исходным" данным. В частности,
конкретно там у is_internal-каналов -- предварительно прописывать
usize
и csize
, и использовать уже их.
chn_p
. В начале цикла она
инициализируется, а после каждой итерации делается chn_p++
.
Как "заусловить" эти присвоение и инкремент внутри for() -- неясно.
Более перспективной идеей выглядит вообще ВЕСЬ этот
for(){}
засунуть внутрь if (stage)
, сделав также
else
-ветку, содержащую приращения
current_val_bufsize
и next_wr_val_bufsize
сразу на
grp_p->count
(вместо по-канального на csize
).
06.01.2019@дома: начальные действия, глобально в функционировании вообще ничего не меняющие:
dtype
с nelems
и
потом из них вычисляются usize
с csize
, ...
current_val_bufsize
делается в конце
каждого блока, сводясь к +=csize
.
06.01.2019@дома-вечер: движемся далее.
FillInternalChanProps()
.
current_val_bufsize
.
current_val_bufsize
--
предварительно выравненное под usize
и затем увеличенное на
csize
.
Так что те 4 монструозных блока кода заменились на строчки вида
current_val_bufsize = FillInternalChanProps(nchans + CXSD_DB_CHAN_LOGMASK_OFS, 1, CXDTYPE_INT32, 1, devid, stage, current_val_bufsize);
Объём файла сократился на 3кБ.
11.09.2019: переделано на чуть иную схему: теперь она
void, а current_val_bufsize
и next_wr_val_bufsize
(ради которого и изменение) передаются по указателям.
cxsd_hw_layers[]
на тему "есть ли уже такой layer в списке
задействованных",
cxsd_hw_numlyrs++
.
НО: ведь сам "массив" cxsd_hw_layers
(теперь превращающийся
из массива в указатель) аллокируется только ПОСЛЕ прохода 0 -- т.к. надо
знать количество layer'ов, а количество ранее подсчитывалось как раз поиском
по нему же именно на проходе 0.
Тупик, клинч? Что делать? Надо думать...
07.01.2019: далее...
Ну так можно считать иначе: вести поиск не по
cxsd_hw_layers[]
, а по списку устройств (который ЕСТЬ в БД, уже
доступной целиком!), по принципу, что если такой layer уже встречался у
предыдущих устройств, то он уже "посчитан", а если нет, то надо его
"посчитать", увеличив количество на +1.
Обсуждение:
(Да, это мы НЕ учитываем потенциальную будущую возможность смены devlist'а на лету, когда время пере-инициализации будет как раз критично.)
Пока что только подсчёт объёма и смещений разных частей внутри него.
08.01.2019: пилим дальше...
cxsd_hw_*
сразу.
10.01.2019: проверять надо!
11.01.2019: ну-с...
Числа совпали -- 2,36,4816.
Числа опять совпали.
-s
)
клиентом -- на вид поведение тоже правильное.
-s
получаем SIGSEGV. Ура! Есть повод для
разбирательства :-)
Авотфиг!!! После полной перекомпиляции всего 4cx/src/ проблема исчезна. И даже стала ясна её причина -- модуль cda_d_insrv, который не был пересобран при смене режима (т.к. он и не в lib/srv/, и не в programs/server/).
13.01.2019@пультовая, вечер
воскресенья: аллокирование общего буфера переделано с
malloc()
на GrowBuf()
.
(Сейчас это не особо важно, но для приличия, на потенциальное будущее.)
14.01.2019@пультовая:
первая проверка на реальном оборудовании: рестартованы canhw:11 (linmag),
canhw:12 (ringmag), canhw:23 (IPP+GID25) -- сходу проблем незаметно,
работает как раньше. Хотя именно в последнем используется trig_read, ранее
лазавший в cxsd_hw_*[]
напрямую, а теперь переведённый на
CxsdHwGetChanType()
.
15.01.2019: приступаем к чистке -- удалению старого
кода, который для CXSD_HW_ALLOC_ALL
=0.
CXSD_HW_ALLOC_ALL
==0, плюс он
сам.
CXSD_HW_MAX_LYRS
,
CXSD_HW_MAX_DEVS
, CXSD_HW_MAX_CHANS
тоже.
cxsd_hw_cur_db = db
"
надо бы РЕАЛЬНО очищать всю информацию cxsd_hw:
...для чего, возможно, подойдёт DestroyChanCbSlotArray()
--
создаваемый, но неиспользуемый.
...хотя есть какая-то странная неиспользуемая функция (заготовка?)
chan_evproc_remover()
-- прилагается к
CxsdHwSetDb()
.
cxsd_hw_num*
.
TerminDev()
всем
устройствам.
...и layer'ам.
18.01.2019: завершаем чистку: удаляем все промежуточные cxsd_hw*.c, через которые в несколько этапов шло внедрение варианта с аллокированием. Если что, они остались в w20190114-cxsd_hw_pre-cleanup.tar.gz.
Ну и цель раздела засим можно считать достигнутой.
cxsd_hw_dev_t
теперь именуются
dev_p
, а не dev
(раньше было вразнобой).
(А вот cxsd_hw_lyr_t
-- УЖЕ и так все lyr_p
.)
Коснулось cxsd_hw.c и cxsd_driver.c.
cxsd_hw_channels[]
напрямую. Нехорошо, в частности, тем, что это налагает запрет на
расширение/модификацию содержимого cxsd_hw_chan_t
.
(Одно дело frontend'ы -- они де-факто являются частью сервера и меняются вместе с ним. А драйверы -- совсем иное, они должны пользоваться чётко определённым интерфейсом и не более того, чтобы свести ABI-зависимости к минимуму.)
Правильный вариант -- делать CxsdHw-методы для добычи интересующих
данных. УЖЕ есть метод CxsdHwGetCpnProps()
-- то ли перевести
trig_read_drv.c с mqtt_mapping_drv.c на неё, то ли сделать
что-нибудь менее монструозное под их потребности.
11.01.2019: ПРИДЁТСЯ делать другую функцию, потому,
что CxsdHwGetCpnProps()
возвращает только "свойства" (строки,
{R,D}, ...), а информацию о ТИПЕ -- dtype, nelems -- нет.
12.01.2019: делаем.
CxsdHwGetChanType()
, она кроме
dtype и max_nelems возвращает также rw -- оно нужно для
mqtt_mapping_drv.c.
cxsd_hw_devices[]
напрямую.
Названа CxsdHwGetDevPlace()
.
16.02.2020@дома-воскресенье: переехала в cxsd_driver.c.
25.02.2020: и переименована в GetDevPlace()
.
cxsd_hw_NNN[]
заменено
на эту пару.
14.01.2019: первая проверка (уже в сочетании с сервером с CXSD_HW_ALLOC_ALL=1) прошла вроде нормально -- никакой ругани от canhw:23'шных trig_read'ов не видно. Надо будет после обеда (когда заработает ИК после профилактики) убедиться, что реально всё функционирует.
@после-обеда: да, значения обновляются.
Чуть позже: нифига-нифига -- посмотрел внимательнее, в k500gid25s.subsys НЕ используются trig_read'овские каналы, а используются исходники. (А "обновляются" -- каналы от VSDC2, которые по внешним запускам.) Так что ничегошеньки в реальности не проверено!
Так что организовал эксперимент иначе: вручную натравил скрин gid25x4.subsys (в нём ЕСТЬ (как и в v4gid25s.subsys) триггеруемые каналы) на 0-ю группу ГИД'ов (canhw:23.gid25_group0). Да, РАБОТАЕТ!
Ладно, теперь со спокойной душой считаем за "done".
Проверено на симуляторе ("cxsd -dsc" с этим же файлом, но R=10 указан руками, т.к. его давал драйвер) -- результат тот же.
Причём:
20.05.2019: пытаемся разобраться, натравив на этот канал подсматривание через "cdaclient -m". Ну да -- видно, что по нажатию "вверх" присылается то же самое значение 0.7...
Похоже, опять какие-то проблемы с вещественными числами в аспекте округления -- что-то подобное было с драйвером Д16. Но тут-то -- наипростейший вариант, с калибровкой 10...
И да, явно это проблема не в text_knob, а где-то в стеке cda...
21.05.2019: пытаемся разобраться. Для начала -- напихиванием отладочной печати.
CdrSetKnobValue()
с форматом "%50.45f"
показывает, что там действительно совсем НЕ нули после десятичной точки.
SendOrStore()
и в
DoStoreWithConv()
... Понятнее не стало.
22.05.2019@утро-выходя-из-дома на
работу, лестница вниз: возможно, дело в каком-то
округлении (round()
или подобное), которое было в v2, но
отсутствует в v4?
Точно было какое-то округление в Knobs_selector'овом SetValue_m(), но оно тут не при делах. 22.05.2019: да, см. bigfile-0001.html от 04-05-2004.
А в v2'шном cda не было ли, после {R,D}-конверсии, при финальном переводе в int32?
22.05.2019: (получасом позже, уже на работе) да --
есть, в setphyschanvor()
, конверсия выглядит следующим образом:
round((v + ci->phys_d) * ci->phys_r)
Детали см. в bigfile-0001.html от 22-12-2009; добавлено было из-за схожей проблемы ("диапазон кодов устройства 0-255 отображается на экранный диапазон 0-100, что даёт r=255./100").
А ни в cxsd_hw.c, ни в cda_core.c
слово "round" НЕ ВСТРЕЧАЕТСЯ.
Очевидно, при конверсии double в int нужно везде повставлять
round()
(сколько сейчас таких мест? 4? Или больше?).
...и так же смотреть, не вылезет ли где какая проблема (rfsyn/Д16/ИЕ4)?
Одно всё же неясно: почему НЕКОТОРЫЕ числа вида "0.x999..." даже и при простой конверсии (int32)(double) всё же окрукляются "вверх", а другие -- нет? Видимо, "загадка" кроется в том самом домножении на 10 при {R,D}-конверсии, когда почему-то в части случаев это "0.x999..." превращается в "0.(x+1)000...".
SendOrStore()
и DoStoreWithConv()
, плюс в
cxsd_hw.c::StoreForSending()
, и на это всё смотрится
синхронно (сервер и клиент запущены из соседних xterm'ов).
float64
не имеет такой точности).
StoreForSending()
совпадали с
после-{R,D}'шными в cda_core.c::DoStoreWithConv()
(что
и понятно -- это физически то же самое число, просто переданное по сети).
Итак, вот таблица (все значения, кроме =0.8, получены нажатием стрелки вверх):
V | SendOrStore | =%a | DoStoreWithConv | =%a |
0.1 | 0.1000000000000000055511151 | 0x1.999999999999ap-4 | 1.0000000000000000000000000 | 0x1p+0 |
0.2 | 0.2000000000000000111022302 | 0x1.999999999999ap-3 | 2.0000000000000000000000000 | 0x1p+1 |
0.3 | 0.3000000000000000444089210 | 0x1.3333333333334p-2 | 3.0000000000000004440892099 | 0x1.8000000000001p+1 |
0.4 | 0.4000000000000000222044605 | 0x1.999999999999ap-2 | 4.0000000000000000000000000 | 0x1p+2 |
0.5 | 0.5000000000000000000000000 | 0x1p-1 | 5.0000000000000000000000000 | 0x1.4p+2 |
0.6 | 0.5999999999999999777955395 | 0x1.3333333333333p-1 | 6.0000000000000000000000000 | 0x1.8p+2 |
0.7 | 0.6999999999999999555910790 | 0x1.6666666666666p-1 | 7.0000000000000000000000000 | 0x1.cp+2 |
0.8 | 0.7999999999999999333866185 | 0x1.9999999999999p-1 | 7.9999999999999991118215803 | 0x1.fffffffffffffp+2 |
=0.8 | 0.8000000000000000444089210 | 0x1.999999999999ap-1 | 8.0000000000000000000000000 | 0x1p+3 |
0.9 | 0.9000000000000000222044605 | 0x1.ccccccccccccdp-1 | 9.0000000000000000000000000 | 0x1.2p+3 |
1.0 | 1.0000000000000000000000000 | 0x1p+0 | 10.0000000000000000000000000 | 0x1.4p+3 |
В таком представлении всё стало существенно понятнее.
Это, видимо, и давало эффект "эффективной/реальной дроби" -- когда при преобразовании к целому дробная часть отбрасывалась, и вместо близкого числа (которое даже выдаётся при печати c "%8.3f") получалось на 1 меньше.
Опять же -- сильно легче не стало, но просто появилась ясность (вместо полумистического "как же оно догадывается, что даже при конверсии double->int надо не просто отбросить дробную часть, а округлить!").
И понимание, что да -- надо в такие конверсии вставлять округление.
round()
во все места, где производится
конверсия.
StoreForSending()
-- под-ветка
"c. Float".
Это сразу и решило титульную проблему -- 0.7,СтрелкаВверх стало давать 0.8.
И "внезапно" потребовалось "-lm" для drvinfo.
ReturnDataSet()
-- аналогично.
cda_dat_p_update_dataset()
--
ветка "c. Conversion".
DoStoreWithConv()
-- "3. Store
datum, converting from double".
cda_get_ref_ival()
-- это недавнее
нововведение, во время создания всей инфраструктуры с преобразованием типов
его ещё не было.
Тут -- несколько спорно, насколько нужно.
П.2 вряд ли когда-нибудь на что-то повлияет: у нас нет драйверов, возвращающих float/double в каналы, объявленные целыми. П.4 -- аналогично: сходу не припомню софта, регистрирующего int-каналы, но пишущего туда вещественные. А вот п.1 и п.3 -- вполне могут ещё аукнуться.
Считаем работу вроде как сделанной, но деплоить на пульт будем в последнюю неделю сезона (в конце июня), чтобы если какие-то косяки вылезут, они бы не сильно помешали работе комплекса. А вот на сварку можно будет залить на днях, когда сингапурцы уедут.
ЗАМЕЧАНИЕ: проблема общая для cda_core.c и cxsd_hw.c, но данный пункт помещаем всё же в раздел о последнем -- поскольку непосредственно проявилась титульная проблема именно в нём.
05.09.2019: ага, "сделанной" -- именно что "вроде как": на сварке продолжали жаловаться, что нажатие стрелки вверх на 0.1 со значения 0.7 не приводит к 0.8, а сбрасывается обратно на 0.7.
Как выяснилось, я просто забыл там обновить 4pult/sbin/cxsd.
09.09.2019: засада ещё в том, что сейчас-то из-за перехода на "cxdtype-uint32" всё поменялось, и нельзя просто обновить cxsd.
Поэтому там собрано содержимое архива
w20190731-cda_d_tango-inprogress.tar.gz -- т.е., уже ПОСЛЕ введения
round()
, но ещё ДО перехода на cxdtype-uint32.
Помогло -- 0.7 после стрелки вверх (с шагом 0.1) превращается в 0.8.
26.09.2019: вчера возникло подозрение, что негативные последствия всё-таки появились -- в v5rfsyn как минимум канал "Kls 3 RF", если в нём значение вроде 18001.*, и просто зажать Enter, то начинает монотонно ехать вниз.
Однако подумал, посмотрел -- неа:
round()
никак не касается.
Засим считаем здешнее внедрение round()
(при конверсии из
вещественных в целые) к проблеме с Д16 непричастным, и пока что вроде как
безгрешным.
cxsd_hw_dev_t
, а начальным/умолчательным значением в нём было
бы как раз условие, которое сейчас в TerminDev()
.
Первоначальный смысл: чтобы драйверы (точнее, layer'ы) могли бы указывать серверу, что НЕЛЬЗЯ free()'ить devptr, для случаев, когда в него заказано асинхронное чтение, пока не завершённое -- эта потребность возникнет у VME-драйверов, если будем реализовывать поддержку такового асинхронного чтения.
Тогда этим мог бы пользоваться даже и remdrv, который сам аллокирует privrec -- аллокировать, а потом отдавать "на милость" серверу.
10.02.2020: есть проблема: там ведь кроме
free()
ещё и psp_free()
выполняется.
11.02.2020: и как ЭТУ сложность обходить? Тогда драйверу придётся как-то регистрировать у layer'а "действия, которые надо выполнить в случае ручного отложенного освобождения" (как минимум -- регистрировать просто свою PSP-табличку).
Каковое решение выглядит крайне уродливо.
Так что надо бы всеми путями постараться избежать внедрения данной фичи, пусть даже ценой отказа от асинхронного чтения.
Вариант -- перекладывать ответственность за него на layer; как минимум, чтобы БУФЕРА были в распоряжении layer'а, а не в privrec'е драйвера.
18.02.2020@вниз по Лаврентьева по дороге к Теплофизике, на автобус в Гигант, ~16:15: решение проблемы намного проще!
Драйверам надо буфера для read-async-операций аллокировать через layer, чтоб он и отвечал за её освобождение. А у драйверов туда указатели будут.
Тогда и не будет никаких проблем с "отложенным освобождением privrec'а".
19.02.2020: ну вот -- ровно та же мысль, что и неделей раньше, только более чётко оформленная :)
19.02.2020: засим считаем эту дикую идею за "withdrawn".
CxsdHwTimeChgBack()
и "timeback":
...но зато у SW_ON_ENABLE есть delay_us=0.5сек. И по прошлому опыту симптомы очень похожи на то, что бывает, когда вдруг неожиданно время скачет назад -- см. запись в конце bigfile-0001.html за 15-04-2019.
2020-10-12 17:02:57а потом, безо всякого перехода/предупреждения -- от
2020-10-12 15:06:57
12.10.2020: некоторое обсуждение и появившиеся по результатам произошедшего мысли.
"Обсуждение":
Oct 12 17:03:00 canhw qemu-ga: info: guest-ping called Oct 12 17:03:10 canhw qemu-ga: info: guest-ping called Oct 12 10:03:13 canhw chronyd[857]: Selected source 79.120.30.43 Oct 12 10:03:13 canhw chronyd[857]: System clock wrong by -25198.745715 seconds, adjustment started Oct 12 10:03:13 canhw systemd: Time has been changed Oct 12 10:03:13 canhw chronyd[857]: System clock was stepped by -25198.745715 seconds
А теперь мысли:
Но это -- ОЧЕНЬ много возни и проблем: надо КАЖДЫЙ драйвер обучать данным фокусам (которые вообще-то сильно противоречат красивой модели драйверов/данных).
Так что самое
Он что, не при каждом vdev_set_state()
текущее время
получает (для вычисления времени окончания ожидания), а ведёт счёт от
какого-то предыдущего?
Или это как-то связано с cxscheduler'ом? Например, как-то некорректно работает очередь таймаутов?
А отличие в том, что возврат производится ещё ДО возврата из вызова
do_rw(), и если раздача события CXSD_HW_CHAN_R_UPDATE
приведёт
к каким-то другим/повторным запросам на этот канал (а и хоть даже на другие,
но с чередованием), то может произойти бесконечная рекурсия.
В синхронной же модели такое невозможно в принципе.
Как это можно было бы обойти -- "расшить": по уже известной технологии флагирования, используемой уже в нескольких местах, самое яркое в виде "being_processed и being_destroyed" --
CxsdHwDoIO()
перед вызовом do_rw()
у всех
затрагиваемых каналов взводить некий флажок вроде "being_rwd",
ReturnDataSet()
'е его проверять и если взведён, то НЕ
генерить событие UPDATE непосредственно оттуда, а лишь взводить другой
флажок, например, "being_returned",
CxsdHwDoIO()
после возврата из
do_rw()
проходиться по всей группе каналов и для тех, у кого
выставлено "being_returned", генерить событие.
Есть, конечно, и некоторые сомнительности:
По идее, кабы не надо их сбрасывать, аналогично некоторым другим (cycles?). Но как это будет взаимодействовать с "основной" работой этих флагов?
13.02.2021@дорога домой от
родителей, ~17:40, стадион НГУ: ответ на вопрос #2 -- более НИГДЕ не
сбрасывать флажки, а только в CxsdHwDoIO()
.
Дома-вечером: неа -- не прокатит: тогда первый же после
того "оживляжа" CxsdHwDoIO()
после первого же возврата из
do_rw() сразу подумает, что якобы значение свежевёрнуто и сгенерит событие,
хотя реально ничего НЕ вёрнуто... А если прямо перед
вызовом do_rw() флаг "being_returned" всё же принудительно нулить?
13.02.2021@дома, ~20:00: а вообще -- насколько данная проблема действительно актуальна? Или она скорее ПОТЕНЦИАЛЬНАЯ, и проще вообще на неё забить?
Побудительный мотив -- для bridge_drv: чтоб можно было миррорить устройства по тем же devtype'ам, но в readonly-режиме.
18.03.2021: решение будет состоять из 3 компонентов:
CxsdDbDevLine_t
и
cxsd_hw_dev_t
тривиально.
dev_parser()
-- тоже тривиально.
CxsdHwSetDb()
-- вот тут будет
главная работа.
Нюанс:
Чуть позже: а для rw-каналов и так форсится fresh_age={0,0}. Так что можно в конкретно том условии просто не учитывать флаг "readonly".
Итого: проект выглядит не очень сложным, но надо будет очень аккуратно реализовывать "мясо" учёта флага.
18.03.2021: чуть позже: однако всё же не так уж "не очень сложным" -- остаётся нерешённая задача "а кто же будет требовать начального чтения экс-rw-каналов?". Да, этот вопрос актуален скорее для НАСТОЯЩИХ устройств, а не для бриджуемых (там это ложится на сервер "реального" устройства), но тем не менее.
Как вариант: а может, учитывать "readonly" не в
CxsdHwSetDb()
, корёжа поля rw
, а уже в
ConsiderRequest()
? Неа -- некрасиво, ОЧЕНЬ некрасиво (и по
отсутствию оптимальности (лишняя операция для каждого канала), и по
возникающей необходимости знать устройство-владельца канала).
Уж скорее -- ввести ещё одно поле-флаг в
cxsd_hw_chan_t
, указывающее на необходимость выполнить
начальное чтение несмотря на не-rw'шность.
Как бы то ни было: надо начать реализацию с простого (пп.1, 2), а потом
уже попробовать аккуратно всё сделать в CxsdHwSetDb()
;
возможно, какое-то решение "проблемы 4" придёт в голову само, а если нет --
ну тогда придётся ещё подумать.
19.03.2021@ночь, пробуждение между снами: пара размышлений по теме:
ConsiderRequest()
, а ReqRofWrChsOf()
! И для неё
-- очень редковызываемой -- совсем несложно запросить чтение любых каналов,
включая не-rw, а уж
CxsdHwDoIO()
+ConsiderRequest()
с радостью его
исполнят.Вопрос останется скорее в том, как помечать в cxsd_hw_chan_t
такие экс-rw-каналы. Можно заюзать зарезервированное поле
res3
.
20.03.2021: правда, весь сервер может быть запущен в режиме [супер]симуляции -- тогда и получится симулируемый и при этом readonly-драйвер. Ну да ССЗБ.
Так что тут действительно всё просто.
19.03.2021: делаем.
is_readonly
к CxsdDbDevLine_t
и
cxsd_hw_dev_t
.
cxsd_hw_chan_t.res3
переименовано в
rw_readonly
.
Да, использовано последнее из зарезервированных 6 лет назад полей...
dev_parser()
-- тривиально.
rw_readonly
в параллель -- |
-- с
rw
в ReqRofWrChsOf()
тоже элементарно.
CxsdHwSetDb()
...
Даже странности/косячки там сходу нашлись: аллокирование места в буфере для next_wr_val делается по условию "rw или SIMULATE_SUP", а предварительный "padding for alignment" -- только по rw, так что при суперсимуляции буфер будет невыровненным (на x86* это незаметно, а вот на ARM/PPC/... вылезло бы). Очевидно, просто забыл добавить условие при внедрении суперсимуляции (она ведь делалась 26-01-2019, ПОСЛЕ выравнивания 21-03-2018).
Явно надо бы ОДИН раз для группы высчитывать флаг "effective_rw" и потом его везде использовать, не дублируя 3-строчное условие в 3 местах (выравнивание, аллокирование, присвоение chn_p->rw).
20.03.2021@ночь -- засыпая, пробуждения между снами, утро: ещё мысли по реализации:
ReviveDev()
-- чтение форсить всё же
надо).
Так что им НАДО взводить флажок is_autoupdated
.
ConsiderRequest()
на запрос чтения
is_autoupdated-каналов всегда возвращает DRVA_IGNORE.
Так что придётся добавить туда дополнительное условие, расширив до
(chn_p->is_autoupdated && chn->p->rw_readonly == 0)
ConsiderRequest()
на "систему битовых масок", с булевскими операциями и ПРЕДВАРИТЕЛЬНЫМ
обсчётом? Т.е., чтобы
rd_req
и wr_req
при этом
превратятся в битики в маске СОСТОЯНИЕ.
Идея красивая, но насколько она реализовабельна -- хбз. В частности, в эту модель масок плохо укладывается проверка на "upd_cycle == current_cycle ...".
Но красивая, да.
20.03.2021: делаем:
CxsdHwSetDb()
:
is_readonly
из БД.
effective_rw
, вычисляемое 1 раз и используемое
3 раза.
chn_p->rw == 0 && dev_p->is_readonly
",
взводятся is_autoupdated
=1 и rw_readonly
=1, плюс
чуть дальше rw_readonly-каналам вместе с обычными rw- делается
fresh_age={0,0}.
ConsiderRequest()
условие is_autoupdated
расширено "rw_readonly==0
".
23.03.2021: проверил на devlist-test-bridge.lst -- да, префикс "!" вроде бы работает, устройство превращается в readonly: запись в каналы устройства "bridge" не проходит, но в нём отражаются записанные в "target".
Надо будет ещё в реальной обстановке потестировать пошире.
CxsdHwResolveChan()
,
CxsdHwGetCpnProps()
и FillPropsOfChan()
исправление типов строковых return-параметров: они переделаны с
"char **
"
на
"const char **
"
-- в порядке управильнивания.
Текущая реализация в значительной степени обусловлена ИСТОРИЧЕСКИМИ причинами -- ва ВЭПП-5 в начале 2000-х был изрядный бардак и полная неопределённость с конкретными потребностями (не говоря уж об отсутствии внятного проекта, из которого и следовали бы конкретные диапазоны и правила алармов, как это есть на всяких NSLS-II); на ЛИУ-2, кстати, ситуация повторилась в ещё более неприличном виде.
Но конкретно сейчас -- в принципе, в инфраструктуре CX есть все компоненты, потребные для реализации фединых хотелок, которые таковы:
Я, понимаю, что cda - не должно знать что там эти данные означают, а должно просто доставить и всё. А вот сервер мог бы: - знать про сущность данных, и ставить алармы и прочее для стандартных случаев. - позволять сервер-сайд модули/скрипты для предобработки данных, и выставления алармов и прочего в случаях требующих индивидуального подхода. В тех же постгресах и http-серверах и прочем аналогичные возможности есть.
Так что вполне можно рассмотреть такой проект.
30.03.2021: (реально в голове это сформулировалось ещё несколько дней назад, чуть ли не сразу после фединого возмущения, но записать руки дошли только сейчас) конкретные соображения:
21.04.2022: ПОТЕНЦИАЛЬНАЯ возможность адресации.
CxAnyVal_t
), а в cxsd_hw_chan_t
только int-оффсеты
(аналогично строкам).
21.04.2022: да-да -- работа по хранению как раз для
свежевводимого binbuf
.
cxsd_hw_chan_t
указатель на
обработчик, который если !=NULL -- то вызвать его в момент после
складирования данных в ReturnDataSet()
, и он может как-то
вернуть результат (или сразу сложить в каналовы данные?).
CxsdHwSetDb()
'ой на
основании ТЕКСТОВОГО указания из конфига (которое может передаваться как
обычная strdb-index'ed-строка).
И там могут быть наиразнейшие спецификации, вроде:
CXCF_FLAG_ALARM_ALARM
при наличии бита
CX_VALUE_LIT_MASK
.
CXCF_FLAG_COLOR_RED
или
CXCF_FLAG_COLOR_YELLOW
.
(Это используется в v5kls.subsys для дополнительной подсветки конкретной аларм-лампочки).
А вот с таким плагинингом -- КАК комбинировать?
Схема с указателем -- для скорости, и можно даже ради оптимизации использовать РАЗНЫЕ обработчики для разных размеров (int8, int16, int32, ...).
Другая -- и более сложная -- часть этой проблемы в том, что для многих устройств диапазоны навешиваются не на аппаратные каналы, а на cpoint'ы, которых в сервере как ОТДЕЛЬНЫХ сущностей -- НЕ существует. Так что и процессинг, получается, навешивать физически не на что.
Например, вроде "_norm_min" и "_norm_max", т.е.,
совпадающие с окончаниями соответствующих DATAKNOB_PARAM_*
(а
для нынешних cxsd_hw_chan_t.range[]
-- очевидно,
"_alwd_min"/"_alwd_max").
CXSD_DB_RESOLVE_PARAM
(=+5?). Так что незнакомые с ним
"юзеры" (вроде "классического" варианта cxsd_fe_cx или cda_d_insrv, которому
оно не надо) будут воспринимать такой результат как "негодный" для них и
считать канал ненайденным.
В самом же CxsdDbResolveName()
потребуются такие
модификации (вот это всё продумано, с анализом кода, уже сегодня,
30.03.2021, а не только записано):
par_n_p
?).
finding_special
чуток изменится:
finding_special
из "диапазона параметров", то
проверить наличие оного параметра, и если его нет, то вернуть RESOLVE_ERROR,
а если есть, то "*par_n_p=finding_special-1000" и вернуть RESOLVE_PARAM.
Тут есть некоторый минус:
pult
: если будем когда-нибудь делать маппирование таких
параметров на стандартные параметры ручек, то оно станет необходимым.
Или даже просто в каких-то случаях/устройствах такая адресация к параметрам почему-либо станет нужна -- ну мало ли, прямо на панель скрина понадобится что-то важное вывести.
04.04.2021: а вот есть с индивидуальной адресацией одна засада:
И вводить новый код события "смена параметров"?
30.03.2021: чуть позже -- размышления о проблеме "как быть, если навешивать свойства придётся на cpoint'ы, а не настоящие каналы":
Но это фигово. И даже ОЧЕНЬ криво. В частности, тем, что тогда либо этой получаемой frontend'ами информации не будет у остальных (всяких cda_d_insrv потенциального и cxsd_fe_epics), либо придётся в них всю эту работу дублировать.
Так вот: для добычи цепочек {R,D} используется вызов, который "раскручивает" цепочку cpoint'в.
А если и обработчики тоже реализовывать не "один делает всё-всё", а "маленькими кирпичиками" -- один занимается alarm'ами, другой проверяет диапазоны, ... -- и составлять ЦЕПОЧКИ таких обработчиков, которые и будут вызываться последовательно по списку, пока список не закончится.
Как это реализовать технически:
cxsd_hw_chan_t
добавить 2 поля:
processers
и processers_count
, первое из которых
является указателем на "массив"-последовательность указателей на функции,
количеством во втором поле.
И храниться эти "массивы" будут в общем cxsd_hw_buffers
(с
понятными требованиями по выравниванию), а CxsdHwSetDb()
будет
их пллокировать и заполнять обычным образом (высчитывать потребный объём на
stage=0 и заполнять на stage=1).
Пара замечаний общего характера по идеологии:
Смысл -- предотвращение ситуаций, когда вызов обработчика приведёт к созданию/удалению каналов и/или cda-ресурсов, что, в свою очередь, создаст проблему "объект выдёргивается из рук владельца", подобную которой пришлось решать при реализации удаления cda-объектов.
Ну и отдельный нюанс-"фича" -- что, поскольку список обработчиков упорядоченный, можно будет как-то использовать последовательность вызовов, создавая цепочки обработки, где последующие пользуются результатами предыдущих. Да, это как раз было неотъемлемой фичей STREAMS. И это весьма полезно для жаждемой ЕманоФедей "предобработки данных ... в случаях требующих индивидуального подхода".
30.03.2021@~17:30, около райадминистрации и Ярче: продолжение раздумий:
По-хорошему -- это очень тесно связано с тем, как всё же будем уметь это сочетать с cpoint'ами...
cxsd_hw_chan_t
и cpoint'ов
СОВЕРШЕННО разное; они даже расположены в разных местах (_hw и _db, так что
у cpoint'ов нету никакого "состояния" в _hw). И как тогда процессеры смогут
унифицированно работать И с аппаратными каналами, И с cpoint'ами?
Или передавать им не cxsd_hw_chan_t*
, а раздельные указатели
на данные, rflags и прочие параметры (диапазоны)
Напрашивающийся ответ -- только придавать им ДАННЫЕ: т.е., превращать cpoint'ы в как бы (полу)самостоятельные объекты.
02.04.2021@~17:00, дорога от родителей домой, вдоль Правды-3: смешивание в "cpoint" 2 разных сущностей: alias и свойств-на-канал.
Вот что с этим делать, а? Правда что ль слегка разные вещи генерить, в зависимости от наличия/отсутствия соответствующих свойств -- если только ссылка, то делать лишь alias, а если ещё что-то, то и "квази-канал" с собственными данными.
04.04.2021@утро-душ: да, технически нет никаких сложностей в "оживлении" cpoint'ов:
cxsd_hw_channels[]
после них.
CxsdHwSetDb()
при stage==0, а заполнять на stage=1, после всего
остального.
CxsdDbCpntInfo_t
понадобится добавить поле "ссылка на
«инкарнацию»", являющуюся gcid-индексом.
...в идеале бы ещё и в cxsd_hw_chan_t
добавить поле
"обратная ссылка на cpoint, чья инкарнация".
cxsd_hw_chan_t
настоящих каналов будет gcid-индекс первого
зависящего cpoint'а, у того -- индекс следующего, и т.д., а у последнего в
этом поле будет 0
(т.е., то, что и так у всех каналов без
зависящих-от-них стоит по умолчанию).
ReturnDataSet()
'е сведётся к циклу вида
next_gcid = gcid; do { proc_chn_p = cxsd_hw_channels + next_gcid; if (proc_chn_p->processers_count != 0) ...ЦИКЛ по processers[processers_count]... next_gcid = proc_chn_p->next_dependant; } while (next_gcid != 0);-- без каких-либо лишних/специальных условий. Таким образом будут отпроцессены ВСЕ зависимые псевдоканалы, даже если на базовом канале и не висит никаких ни параметров, ни обработчиков.
Потенциальные проблемы:
cxsd_hw_chan_t
'ам
используется ихнее поле devid
для доступа к драйверам -- вот
это уже реальная сложность.
Напрашивается мысль, что прямые обращения к этой группе "каналов" надо запрещать, а оставлять её только для совместимого с обычными каналами представления данных -- указатели на которые можно сбагривать функциям/плагинам-"обработчикам".
Но эти-то проблемы чисто технические и вполне решаемые.
А вот ГЛАВНАЯ проблема, с {R,D}, никуда не девается -- что диапазоны-то указываются в ОПЕРАТОРСКИХ, а не в аппаратных величинах...
04.04.2021@~18:30, моя посуду: с таким подходом -- "оживлять cpoint'ы по мере надобности" -- мы потихоньку двигаемся к совсем другой архитектуре: не "есть набор устройств, дающих аппаратные каналы, и есть набор alias'ов для этих каналов", а уже ?????????????????????
09.04.2021: Федя изъявил желание иметь и проверяние ПЕРЕД записью. И в т.ч. список разрешённых значений.
...формально, кстати, и "processer'ам" тоже.
Только вопрос -- а как реализовывать такую "фильтрацию записи" для cpoint'ов, которые могут быть и многоуровневыми? Там ведь на КАЖДОМ этапе могут иметься свои ограничения, и по-хорошему их надо бы применять друг за дружкой, по пути "раскрутки". Но сейчас запись выполняется "по gcid'у".
CxsdHwDoIO()
, и уже ОНА
проводит проверку dtype, nelems и т.д., а у неё доступа к информации о
"цепочке" уже нет.
...как-то всё же передавать оную информацию?
...хотя cpid там тоже сохраняется (для добычи строк и {R,D} при их изменениях), так что необходимая для "прохода" информация есть.
Тут идеологический/технологический нюанс в том, что:
StoreForSending()
.
10.04.2021@утро: если хорошенько поразмыслить, то
Так что если уж сильно надо что-то такое нестандартное -- то надо делать специальный драйвер, проверяющий/обрабатывающий получаемые для записи данные и затем переправляющий их в реальную железку.
А эти "плагины для чтения" -- кстати, по факту тоже будут иметь функциональность/сложность, примерно соответствующую таковой у драйверов. Разница лишь в форме: драйвер предполагает работу с некоей картой каналов, образующей устройство, а плагин-чекер может быть применён к произвольному каналу (и произвольного готового устройства).
10.04.2021@17:10, мытьё посуды: кстати, насчёт конкретно ALARM'ов: ведь у нас они -- те, которые реально работают как alarm'ы, с бибиканьем -- отдаются прямо драйвером, именно сразу как специальный канал, а не просто некий битик входного регистра. Оно так ВЭПП-5 в:
Так ничто ж не мешает прямо ДРАЙВЕРАМ и возвращать вместе со значением 1 и rflags=CXCF_FLAG_ALARM_ALARM!
Есть, конечно, и сложности:
Тут единственное решение -- изготавливать vdev-драйвер вроде "vip_ceac124", который и будет содержать таковые мозги.
%
-регистром, на который отображается экранная ручка, и в
зависимости от этого регистра просто НЕ выставляется битик по маске 1.
И как эту задачу решать -- да, уже неясно. ...специальная формульная операция "clear_rflags"?
10.04.2021@17:40, пока записывал: а ведь аналогично можно и с флагами COLOR_RED и COLOR_YELLOW: как минимум для CANIVA ведь исходные данные для пожелтения/покраснения значения Imes есть у самого драйвера -- так пусть драйвер и отдаёт в rflags нужный флаг в случае выхода значения за границы!
...правда, там какие-то хитрости, типа что <90% -- norm, 90%-95% -- yelw, >95% -- red.
16.04.2021: несколько дней назад мылом спросил Роговского, что он думает на тему этого проекта -- в т.ч., бывали ли у него потребности в подобной функциональности в сервере?
Сегодня утром пришёл ответ; довольно длинный, и с разными деталями и оговорками, но общий смысл -- да, он был бы совсем не против, им такое будет полезно!
18.04.2021@ванна ~18:00:
концептуальеый вопрос -- как назвать эту сущность, "инкарнации" cpoint'ов?
Они как бы "тень" или "отражение" -- SHADOW или RFLCTN (то же число букв,
что в слове "CPOINT" -- для равнодлинности определения маски с
CXSD_DB_CPOINT_DIFF_MASK
)?
@ложась-спать: да проще -- прямо "INCARN"! Те же 6 букв, при этом абсолютно точно и сам термин уникальный!
18.06.2021@~13:20, "гуляя" в ИЯФ и Ярче, около ИЦиГ, под аркой: кстати, такие "процессеры" можно использовать для решения задачи «преобразование вещественных чисел между форматом "Выборг-float16" (контроллер вакуумного насоса по Modbus-TCP) и стандартным float/double».
Тут, конечно, есть несколько нюансов:
uint16
и "клиентские" с типом
double
, и чтоб преобразование выполнялось бы специальным
драйвером, который при записи в его канал туннелирует (с преобразованием)
это значение в соответствующий аппаратный канал, а при обновлении целевого
аппаратного -- возвращает преобразованное значение в свой канал.
Технический вопрос только -- КАК это всё описывать? Точнее, как указывать драйверу-преобразователю схему маппирования?
Если считать случай единичным -- то просто всё захардкодить, и в драйвере-преобразователе иметь полное отображение аппаратных каналов (включая те, которые преобразовывать и не нужно).
uint16
, клиентские же --
float32
.
InitDevice()
: если где-то
изнутри метода init_dev()
устройство застрелится -- вызовом
SetDevState(,DEVSTATE_OFFLINE,)
-- а потом из оного
init_dev()
'а вернёт иное значение (обычно
DEVSTATE_OPERATING
), то устройство останется ЖИВЫМ.
Такая ситуация может возникнуть, если _init_d() в конце просто безусловно
возвращает OPERATING (или же делает "return ЧЕГО_НИБУДЬ()
", где
уже это ЧЕГО_НИБУДЬ() вернёт OPERATING), но какое-то вроде бы элементарное
действие изнутри этого всего может потенциально привести к фатальной
проблеме, ведущей к застреливанию устройства (например, обнаружение потери
связи при В/В, или ещё что).
А замечена потенциальная проблема была при реализации поддержки RUN_MODE
(точнее, инициализации из auxinfo) в adc250_drv.c: там
инициализация сводится к вызову pzframe_drv_rw_p()
, откуда, в
свою очередь, может исполняться дофига всяких действий, включающих как
вызовы из самого драйвера (В/В по шине или сети), так и из сервера; и
невозможно гарантировать, что какое-то из них не приведёт к переводу в
OFFLINE.
09.10.2021: пофиксено простейшим образом: сразу после
возврата из init_dev()
добавлена проверка "а не живо ли
устройство к этому моменту":
if (dev_p->state < 0) return;
Протестировано на временно чуть подправленном noop_drv.c,
которому в noop_init_d()
вставлено
SetDevState(,DEVSTATE_OFFLINE,)
:
(Методология теста такая: при запущенном сервере из одной консоли cdaclient натравливается мониторить каналы _devstate и _devstate_description, а из другой делается _devstate=0 (рестарт) -- при этом ДО фикса статус щёлкался в -1 и потом сразу же в +1, а ПОСЛЕ прыгать в +1 перестал.)
InitDevice()
'овское сообщение о "refusal" (что
init_dev() вернуло <0) выдаётся БЕЗ имени экземпляра устройства. Видимо,
потому, что оно выдаётся не DoDriverLog()
'ом, а самостоятельно,
с помощью logline()
(это оно так ещё со
времён CXv2, и содержимое скопировано из
cxsd_drvmgr.c::InitDev()
-- там про psp_parse(auxinfo)
ругательства выдавались DoDriverLog()'ом, а конкретно это --
вручную).
И сие весьма неудобно -- что поиском по имени устройства найдётся не всё, к нему относящееся.
10.10.2021: исправлено: теперь имя устройства выдаётся
-- сразу после "[DEVID]", как и в DoDriverLog()
'овских
сообщениях (только после него идёт ".init_dev()=", а не "/КАТЕГОРИЯ").
Фактически представляет из себя определение "общего" заголовка
CxsdModuleRec
, содержащего { magicnumber, version, name,
comment, плюс init_mod и term_mod }, и стандартные методы для
cxldr_context_t
.
09.12.2014: еще энное время назад (судя по дате
на файле, 14-02-2010) оный заголовок был перенесён в
cx_module.h под названием cx_module_rec_t
.
09.12.2014: а сегодня (реально еще вчера) начата работа по приведению всех махинаций с модулями в lib/srv и programs/server в "достойное" состояние (причиной посужило то, что в БИБЛИОТЕКЕ отсутствовал список frontend'ов -- по EndOfCycle дёргать было некого).
Раньше оно было весьма прихотливо раскидано по разным модулям в libcxsd и даже в programs/server/.
CxsdHwSetPath()
превратилась в
CxsdSetDrvLyrPath()
(надо бы и для прочих
загружабельностей аналогичные CxsdSetNNNPath() сделать).
CxsdActivateFrontends()
, а в самой libcxsd никак иначе не
фигурирует.
CxsdCallFrontendsBegCy()
и CxsdCallFrontendsEndCy()
.
И раздел переименован в "cxsd_modmgr".
errno=-1
-- чтоб "юзер" (например, cxsd_hw) знал, что
ошибка уже залоггирована.
CxsdLoadDriver()
переименовать typename
в
typ_name
: из-за дурацкого C++'ного стандарта оно воспринимает
"typename
" как ключевое слово даже внутри
extern "C" { ... }
(вот схрена ли, а?!), и потому ругалось.
Проблема возникла при начале реализации cxsd_fe_epics.cpp.
P.S. Насчёт "схрена ли" -- я явно не первый, у кого этот вопрос возникает: «extern "C" does not enable C headers to use C++ reserved words»:
extern "C"
affects essentially how the compiler/linker performs
the lookup and/or call of the relevant functions; the parsing rules are not
affected.
18.02.2022: неа, всё вёрнуто на место -- поскольку
после добавления в cxsd_fe_epics.cpp ещё #include
'ов
компилятор стал ругаться на "typename
" ещё и в
cxsd_dbP.h, то было принято решение разделить исходники
cxsd_fe_epics на C'шный интерфейс cxsd_fe_epics.c и C++'ную реализацию
cxsd_fe_epics_meat.cpp, так что проблема "typename
является зарезервированным словом" устранена в корне.
Модуль назовём "cxsd_access".
28.04.2020: а ведь у нас в 2003-м была идея "cxnetacl"! См. за 17-09-2003, @Bled (да, на какой-то из лекций пришло в голову; эх, зря я тогда не записывал как CLASS=location точный момент (только через пару лет начал, на ICALEPCS-2005 в Швейцарии), а то бы было ясно, на какой именно лекции -- наверняка что-то по security).
В v2 оно было как бы начато -- пустые файлы изготовлены, но в v4 уже не пошло.
05.05.2020: по факту, нынешний cxsd_access реализует то, что требовалось ещё тогда; с некоторой потенциальной "универсальностью" API, но без полноты/законченности (например, поддержки IPv6).
06.05.2020@утро: а вот и нет -- есть ОЧЕНЬ существенные отличия:
В cxnetacl же была предусмотрена единственная булевская
IsAllowed()
.
04.04.2020: highlights:
Связанные с этим аспекты:
CxsdAccessLoadList()
, считывающий объекты,
...
CxsdAccessSetDefACL()
.
CxsdAccessList
.
CxsdAccessCheck()
.
cxsd_access_default_acl
==NULL) возвращается "всем всё можно".
CxsdAccessSetPolicy()
. Сейчас это реально не используется.
Это сделано для реализации режима "readonly".
ServeGuruRequest()
игнорирует запрос при отсутствии
PERM_CONNECT.
ServeIORequest()
игнорирует запрос за запись при
отсутствии PERM_WRITE у данного клиента.
Игнорирует МОЛЧА, без сообщения типа "permission denied".
AcceptCXv4Connection()
выполняет роль
стража/привратника: узнаёт права свежеприконнектившегося клиента, посылает
его с CXT4_ACCESS_DENIED
, записывает права в метрику клиента
при её создании и заполнении.
v4clnt_t
добавлено поле
perms
.
CAR_EACCESS
, ...
CEACCESS
, ДО общего
CAR_CONNFAIL.
_cx_carlist[]
(индексировавшийся по коду) пришлось переделать в набор дуплетов
_cx_cardescrs[]
-- а то старая схема с индексированием была
неадекватна ещё с момента введения CAR_DATA.
cxhosts_file_parser()
открытие плюс warning при неудаче убраны,
теперь это не его забота.
Слегка в сторону: PerformAccess()
была переименована в
PerformAccessToFiles()
, для определённости и отсутствия
странных ассоциаций.
20.04.2020: сделана краткая документация -- doc/cxhosts.ru.html.
02.05.2020@вечер-душ: надо
бы добавить в права доступа дополнительный бит --
CXSD_ACCESS_PERM_LOCK
, для дополнительного ограничения.
А readonly-клиентам -- так и вовсе не разрешать локинг никогда.
03.05.2020: делаем.
(утро) Сделано, что при отсутствии PERM_WRITE и локинг не разрешается.
(после обеда) Улучшено:
CXSD_ACCESS_PERM_LOCK
. И в PERM_ALL=~0 он сразу
есть.
nolock
, при
этом ставятся права PERM_ALL&~PERM_LOCK.
Теперь же мы приступаем к реальному его наполнению. И -- большое толстое замечание: сервер теперь является лишь юзером библиотеки libcxsd, а основная работа будет идти именно там.
21.11.2013: функционал старых lib/srv и programs/server на новом уровне уже воспроизведён, так что пора двигаться дальше (тем более, что пришла пора наращивать функционал, с изменением include/*.h-файлов, а отражать это в старых уже нет смысла).
Для непотери старый rrund.c переселён и в новую директорию.
Старые директории удалены, а на их место переименованы n-директории.
Она всё равно никогда и нигде не использовалась, /var/tmp/cxd.log всегда был пустым.
07.11.2013: в связи с появлением ll_reopen()
добавлен отдельный перехват SIGHUP
(а из общего списка
interesting_signals[]
он убран).
13.12.2013: переделываем "сценарий старта" в
main()
, чтоб активация драйверов происходила ПОСЛЕ
активации frontend'ов (ради проверки, что нет другого запущенного
экземпляра и можно трогать железо).
Тут всё проще, чем в v2 -- поскольку загрузка и активация БД
разделены, то все действия по последней вынесены в отдельную
ActivateHW()
, вызываемую позже.
17.07.2015: добавлена поддержка cda и cda_d_insrv.
Что неприятно -- вместе с ними пришла и cda_f_fla, требующая libm. По-хорошему, надо б её унести в отдельную "libcda_f_fla.a".
И вообще -- ОЧЕНЬ не хватает доделанности "конструктора из модулей", чтоб просто указывать список нужных модулей, а они б все влинковывались и инициализировались автоматически.
Причина -- никакое CxsdSetNNNPath()
(NNN={Fnd,Ext,Lib}) не
делалось. И даже если бы делалось, то не работало бы, поскольку рассчитано
всё было на делание ПОСЛЕ пансинга конфига, а парные команды
load-frontend и прочие вызывают загрузку СРАЗУ, в момент парсинга.
23.12.2015: сейчас сделано по-простому -- все
NNN_path_parser()
вызывают CxsdSetNNNPath()
сами
сразу. Так что всё заработало.
Но по-хорошему надо бы load-NNN не сразу выполнять, а запоминать в списки "требуется загрузка" и потом ПОСЛЕ парсинга конфига вызывать уставку путей и загрузку модулей.
Вот сейчас piv485_lyr'у понадобился n_write()
. Потому
добавляем её во все 3 места:
Daemonize()
, а лишь позже --
CxsdActivateFrontends()
: в результате при попытке запуска
сервера, который уже работает (для данного :N) ошибка в консоли НЕ видна
(если только не с -d
), а лишь падает в лог.
Это преизрядно путает -- вроде кажется, что сервер "перезапустил", а реально работает старый экземпляр со старым же конфигом.
15.10.2019: причина в последовательности:
if (!option_dontdaemonize) Daemonize(); if (option_norun) {normal_exit = 1; exit(0);} if (CxsdActivateFrontends(option_instance) != 0) {normal_exit = 1; exit(1);}
Какие есть соображения по её поводу:
option_norun
можно с чистой совестью
отрабатывать первым -- демонизироваться ДО неё незачем.
Вероятно, это наследие от v2'шного zzz/cxsd.c, где так было,
из-за неразделённости операций "ReadHWConfig()
" и
"ActivateHW()
" (активация устройств выполнялась по мере
чтения).
С другой стороны, это вроде никак принципиально не мешает.
Пробуем?
OK, сделал -- теперь последовательность такая:
if (option_norun) {normal_exit = 1; exit(0);} if (CxsdActivateFrontends(option_instance) != 0) {normal_exit = 1; exit(1);} if (!option_dontdaemonize) Daemonize();
Теперь надо поглядывать, не вылезет ли чего.
...а ещё возникает желание перетащить в блок до демонизации также и
CreatePidFile()
-- чтобы ошибки при создании PID-файлов были бы
видны (для чего их нужно будет перевести с logline()
на обычные
fprintf(stderr)'ы). Можно ли? Или чем-то чревато?
-e
"
-- активировались бы (чем-то похоже на ключ
"-nolisten tcp
" у X-сервера).
Ключ -N
?
28.02.2025: а вот фиг, не так-то всё просто: ведь cxsd_fe_cx никак отдельно не выделен, просто он "вшивается" в бинарник сразу, а инициализируется cxsd_builtins.c, в каковой момент и регистрируется; отличить же конкретно его сервер никак не может.
02.03.2025: ну сделал -- ключ "-N
"
взводит option_no_frontends
, при которой пропускается вызов
CxsdActivateFrontends()
. Да, можно запустить хоть несколько
штук, считающих себя за ":0" -- они друг дружке не мешают.
04.10.2009: за это время сделано:
DisconnectClient()
, авторитетно
протоколирующий объяснение (заменяет пару LogConnection()
и GenericDisconnectClient()
из CXv2, сочетая в себе их
функционал).
LITTLE_ENDIAN
) -- основная работа помещена в
endianconv.h (чтоб было доступно другим frontend'ам), а также
есть пара (пока) функций host_i32()
и
clnt_i32()
, которым передаётся также cp
и они
сразу возвращают значение в нужном порядке (для сервера и для клиента,
соответственно).
22.10.2012@Снежинск-каземат-11: переведен на
GENERIC_SLOTARRAY_DEFINE_GROWING()
.
09.01.2013: была кривость и путаница в
концепциях: регистрировались сокеты в fdiolib'е, а раз-регистрировались
(DestroySockets()
'ом) в cxscheduler'е. Теперь всё идёт
через fdiolib.
...и, кстати, неясно -- нафига там параметр
"and_deregister
"? Почему это он иногда ==0?
16.10.2014: в связи с необходимостью добавления "оракула" (резолвера имён) переименованы (первый раз лет за 15!) переменные, отвечающие за слушанье:
unix_entry
и inet_entry
стали
unix_serv_socket
и inet_serv_socket
, а
unix_iohandle
и inet_iohandle
--
unix_serv_handle
и inet_serv_handle
.
Результат:
socket
и handle
)
guru
вместо serv
.
29.12.2014: за последние несколько дней сделана
основа работы с данными -- пакеты CXT4_DATA_IO
плюс
мониторинг, по проекту от 25-12-2014 (в разделе cx-протокол).
30.12.2014:
А можно ли реализовать еще и telnet-интерфейс, аналогично оному у remsrv? Было б крайне удобно.
02.02.2013: вопрос в ПОРТУ. Варианты:
Например, собственно :N использовать только чётные, а 8012+N+1 -- для telnet-подключений.
Плохая идея. С одной стороны, telnet-соединению бы СРАЗУ выдать какое-то issue и потом prompt. С другой -- по обычному соединению сервер должен дождаться прихода пакета EIDIANID. И что -- считать, что если за 1 секунду пакет не пришёл, то это telnet? А если клиент просто на другом континенте? Не-не-не, нафиг-нафиг...
16.10.2014: сделано бинденье сокетов.
SO_REUSEADDR
, в Linux для UDP работающего как BSD'шный
SO_REUSEPORT
(до ядра 3.9 отсутствовавший). Детали
описаны в ответе в
Socket
options SO_REUSEADDR and SO_REUSEPORT, how do they differ? Do they mean
the same across all major operating systems? на Stackoverflow.
SO_REUSEADDR
привело к желаемому эффекту --
теперь UDP-порт работает как блокировка.
21.10.2014: инфраструктура почти доделана --
CX_V4_UNIX_RESOLVER
'у.
Сокет получил имя "info" -- unix_info_socket
; логика в
том, что он информационный:
Не очень удобно, правда -- лучше б на другую букву начинался, не на "a" (еще apprentice (подмастерье) и aide) и не на "p" (prentice). Что-то есть на "j" -- journeyman, но тухловато...
CxV4Header
.
Нет пока только ИСПОЛЬЗОВАНИЯ гурой принятых соединений -- делается
холостой fdio_accept()
(просто чтоб снять запрос). Это
будет уже часть основного функционала.
22.10.2014: всё-таки
advisor_t
, ключевое слово -- Advisors
.
В простейшем варианте, там пока есть только поле
instance
, покамест даже никак с начального -1
не меняемое. Всё проверено чисто на fprintf(stderr)'ах.
09.06.2015: за 3 дня на прошлой неделе и вчера-сегодня с серверной стороны сделано.
CxV4Header
и
CxV4Chunk
'ов используется свой, очень простенький.
Сообщения не контейнерные, а начинаются с заголовка
GuruChunk
(состоит из int32-полей OpCOde
(коды GAC_nnn
) и ByteSize
, причём размер
указывается ВСЕГО "пакета"), за которым идут OpCode-dependent данные.
Причина -- вопрос трудозатрат: такой протокольчик на порядок проще в реализации. Но базовые принципы у него похожие (включая кодирование опкодов), поэтому если/когда припрёт -- можно перейти и на полноценный протокол V4.
GAC_NMSP
и GAC_CLEVEL
)
отправляются отдельно заголовки, а потом за ними поштучно элементы.
Теперь о недоделанностях:
InteractWithAdvisor()
всё-таки
маловато.
...а как это проверять -- неясно.
В идеале же надо идти по запросу и "набивать" пакет ответа до разрешенного объёма (MTU=1500); по превышению отсылать его и опять обнулять, а в конце отсылать, если не пустой.
01.12.2017: да, поштучные ответы -- некрасиво. И еще: можно ж даже не пытаться "набивать до объёма 1500", а считать, что раз до нас пакет дошёл (т.е., пролез по MTU), то и обратный такой дойдёт -- а форматы-то (и, следовательно, объёмы) "туда" и "обратно" совпадают. ...Только неясно, насколько это хорошая идея: формально маршрутизация может быть и асимметричной.
cx_resolve()
просто отправляет одиночный запрос, а
HandleResolveReply()
печатает ответ на консоль.
27.10.2017: небольшие модификации касательно работы с "советниками":
RlsAdvisorSlot()
использовались переменные
cd
и cp
-- очевидно, копия с
RlsV4connSlot()
; переименованы в jd
и
jp
.
cx_term_f()
делалось лишь
DestroyAdvisorSlotArray()
, чего решительно недостаточно -- ведь
поэлементные Rls'ы в Destroy*SlotArray() не выполняются (давняя проблема).
Добавлен Foreach с итератором TermAdvisorIterator()
, который
также выполняет ForgetAdvisorDb(jp->instance)
при
jp->instance>=0
.
01.12.2017: в связи с возвращением к теме
UDP-резолвинга вновь смотрим на имеющееся в ServeGuruRequest()
.
Итак:
CxV4Chunk
, что и при обычном обмене данными.
Type==CXT4_RESOLVE
.
Добавлена проверка, только уже на ==CXT4_SEARCH
(в связи с
сегодняшним переименованием).
27.01.2017: соображение по оптимизации трафика: а чё это
ServeGuruRequest()
отвечает на каждый chunk входного пакета
индивидуальным пакетом? Неправильно это!
ServeGuruRequest()
использовал бы тот же
подход, что и cxlib_client+cda_d_cx::PeriodicSrchProc()
: по
первому совпадению начинать набивать пакет ответа, если вылезает за границу
CX_V4_MAX_UDP_DATASIZE
-- имеюзщееся отправлять и начинать
следующий (с "2nd try" невлезшим chunk'ом).
reply
" надо
переходить к какому-нибудь...reply
" и растёт, так проще.
30.01.2018: кстати, глянул wireshark'ом:
Кстати, запросы скрина CANDAC16 с префиксом "icd." укладываются в 3 пакета (1456, 1472, 992 байта -- 1498, 1514, 1034 on wire).
30.01.2018: делаем.
result
и содержит 2
поля: заголовок и data[CX_V4_MAX_UDP_DATASIZE]; плюс переменные
result_DataSize
и result_NumChunks
.
BeginSearchReply()
и
SendSearchReply()
.
Их только 2, т.к. "добавить chunk ответа" осталось прямо в теле: при
схеме "всё внутри самой ServeGuruRequest()
" она заранее знает,
что места под chunk не хватит и сделает отправку текущего пакета и
инициализацию следующего прямо сразу, ДО добавления chunk'а.
var2=CX_V4_PROTO_VERSION
, а в cxsd_fe_cx ничего не
проверяется. Пока прокатит, но в будущем, при возможном изменении
протокола, чревато проблемами.
01.02.2018: сделано. Проверено, что ничего не сломалось; а на другие версии протокола проверить вряд ли удастся.
Поэтому нельзя для гуру просто тупо передавать образ структуры в сокет, а надо делать полноценный маршаллинг (с целыми по 32 бита).
30.03.2015: насчёт виртуалки, кстати, интересный вопрос -- как наша нынешняя модель "GURU" будет работать: с одной стороны, UDP/IP-порт занят (пусть и виртуалкой -- это легко возможно); с другой же, UNIX-порт в эту самую виртуалку вряд ли пробрасывабелен.
Конечно, тогда можно пытаться майстрячить что-нибудь через IP/localhost вместо unix/; только тут уж вряд ли игра стоит свеч.
09.06.2015: ПОКА сделано наполовину:
28.07.2015: обсуждение:
moninfo_t
) сохранялось одно
число -- globalchan
, являющийся номером аппаратного канала
(на который в конечном итоге смотрит cpoint).
cx_rq_rd()
не
используется нигде).
Всего делов-то -- при надобности пройтись по цепочке cpoint'ов.
SendAReply()
ВСЕГДА передаёт в
качестве параметра "номер канала" именно
mp->globalchan
. А надо иногда именно globalchan
(для PutDataChunkReply()
и
PutFrAgChunkReply()
, имеющих дело с конечным аппаратным
каналом), но иногда и cpid -- для остальных, возвращающих
результаты прохода по цепочке.
cpid2hwid()
, в
будущем cpid2gcid()
-- место бы ей в cxsd_hw.
moninfo_t
поле и для вешания
CxsdHwAddChanEvproc()
на реальный канал.
PutDataChunkReply()
-- из-за
обозначенной ранее проблемы с SendAReply()
.
Считаем, что на производительности оно должно сказаться слабо.
А если уж делать "как надо", то придётся "стандартным параметром"
сделать не cpid, а прямо mp
(что некрасиво для прямых
вызовов SendNNN()'ов).
Результат -- теперь ВСЕГДА отдаются полные цепочки свойств.
1. hwid/gcn - gcid; cpn -- cpid. 2. cda_d_insrv -- cpid/hwid?
29.07.2015: кстати, cda_d_insrv.c проверен -- в нём проблемы нет изначально.
P.S. И "guru" при этом, конечно, не нужен -- его просто вы-#if'ливать из компиляции.
23.09.2015: идея родилась при общении с Пановым на тему "как бы поселить CX-сервер в его (пановский) крейтик, работающий под Linux-ARM, в котором софт делается в NI'ном фреймворке (уже умеющем отдавать данные по Channel Access)".
24.09.2015: написано "программное" (memorandum-style) письмо Панову; его текст тут, заключённый в HTML-комментарии.
Причина очень проста: запрос -- CxsdHwDoIO()
-- делается
лишь в начале цикла. В то время как в v2 для больших каналов запрос
повторялся самим клиентом сразу по получении ответа на предыдущий.
Т.е., для решения нужно прямо в cxsd_fe_cx слать запрос сразу же после получения очередного ответа. Но, для предотвращения бесконечной рекурсии на тут-же-возвращаемых каналах, окружить отправку флагом "сейчас монитор запрашивается".
P.S.: обсуждение см. в разделе по pzframe, по словам "РАЗ В ЦИКЛ".
27.05.2016: делаем:
moninfo_t.being_reqd
.
MonEvproc()
после отправки
свежеполученного значения.
SetMonitor()
, чтоб канал начал
опрашиваться сразу, а не с начала следующего цикла.
04.09.2020: а вот это -- аукнулось! В довольно
экзотических условиях, и обнаружилось уже на обновлённом протоколе, но всё
же -- аукнулось, и дорыться до сути было непросто. Вкратце -- в случае
немедленного ответа отправка оного (из MonEvproc()
'а) портила
формируемый в буфере пакет, который создавался
ServeIORequest()
'ом в ответ на запрос. Подробности см. за
сегодня в разделе по MUSTER/"протокол через handle'ы".
Проверяем -- хрен, не работает...
Подумавши -- причина в принципе "не запрашивать readonly-канал чаще раза в цикл" (в v2 у больших каналов такого не было). Что делать?
Пожалуй, да, он осмыслен:
Острота, конечно, снижается:
cx_begin_c()
/RequestSubscription()
, а тут
сработает отсев по rd_req (в железо уйдёт только первый запрос).
Хотя тут, конечно, с ON_UPDATE-подпиской такая "защита" всё равно исчезнет.
из чего вывод: для ТЕХ каналов надо будет отключать ON_UPDATE -- в случае pzframe_data это флаг ON_CYCLE.
Да, CXSD_HW_DRVA_IGNORE_UPD_CYCLE_FLAG
введён и заюзан.
Помогло -- при включенном ISTART молотит 176FPS на 5000 точках и 700FPS на
1024.
Итого -- "done".
26.11.2018: однако не всё так гладко -- форсение
CXSD_HW_DRVA_IGNORE_UPD_CYCLE_FLAG
для on_update-каналов
привело к проблеме, что легкочитаемые readonly-каналы начинают обновляться с
бешеной частотой.
Так что сегодня реализован фикс, в виде режима возврата (return type)
DO_IGNORE_UPD_CYCLE
. Конкретно cxsd_fe_cx.c это не
коснулось, но заслуживает упоминания тут.
Прочие подробности -- в профильном разделе за сегодня.
Обнаружилось на linvac'е. Он постоянно бибикал (что-то химичили с источниками насосов), и я сделал ему Ctrl+Z. А через 3 часа "fg", и он вовсе НЕ мгновенно получил большой поток данных, а "время шло" раза примерно в 1.5-2 быстрее (смотрелось по возрасту конкретной ручки).
Кто виноват -- точно неясно. Видимо, fdiolib? Но cxsd в это время занимал 99% CPU, а занимался хрен знает чем (strace/ltrace никакой особой активности не показали). И ОЗУ было занято ~900M.
28.02.2017: близкая проблема: сервер ring1:12 сожрал 3.5G ОЗУ (колонка RES в top) и ушёл в глухой swap.
watched[*]._sysbufsize
-- кто там будет
сильно крупным.
_sysbuf
'а, но само это
ограничение никто не ставит -- НИГДЕ нет вызова
fdio_set_maxsbuf()
.
А надо бы делать.
-1
в
результате вызова fdio_send()
, а не передаётся асинхронно через
нотификатор посредством close_because()
.
cx_close()
, где соединение и так закрывается) два:
vReportThat()
. Последняя-то
результат возвращает, а вызывальщики его не проверяют.
Точнее даже "точкА" -- vReportThat()
. Маловероятно,
конечно, но такое упущение может, теоретически, вызывать проблемы -- "порча"
приходящих в remdrv данных, т.к. при частичной отправке после начала пакета
потом может добавиться (вместо невлезшего конца того же пакета) начало
следующего, а уже его конец будет воспринят как заголовок очередного -- и
получится мусор.
(Второй юзер там
remcxsd_report_to_dev()
, используемый для выдачи сообщений о
фатальной ошибке непосредственно перед FreeDevID()
.)
30.03.2017: продолжение истории -- теперь с linac1:11 и конкретным клиентом linmag, работающим на ichw1-2 под x2go-сервером (для Raspberry-PI, юзер pi1).
Как проходило разбирательство:
watched[*]._sysbufsize
, и нашелся один с аномально большим
значением -- больше гигабайта.
Сверка егошнего .fd
со списком всех
cx4clnts_list[*].fd
и помогла найти клиента.
Кстати, сама такая разборка -- просматривать список глазами
для сличения -- крайне неудобна. Стоит прямо в
cxsd_fe_cx.c::HandleClientConnect()
логгировать также
и fd
и fhandle
. 30.03.2017: после обеда -- сделано, логгирует.
_sysbufused
показало, что
оно потихоньку растёт (и объём употреблённого сервером ОЗУ рос, в среднем
примерно по мегабайту в минуту).
А вот после коннекченья Федей к этому x2go-серверу
_sysbufused
стал потихоньку уменьшаться.
Выводы (частично предположения):
31.03.2017: организуем тесты.
Условия:
Результаты:
-b10000
) клиент ничего не успевает
отрисовывать, каналы чтения у него горят гусиным, сервер жрёт память по
~5MB/s.
-b100000
) всё успевается, и это как раз
комфортный вариант для тестирования.
Характерный размер посылки -- что-то вроде 256 байт. Например, в ответ на CXC_RESOLVE одного канала отдаётся 4 chunk'а суммарным объёмом минимум 56 штук int32 (т.е., 224 байта), а реально больше.
AcceptCXv4Connection()
сразу же после
регистрации fdio-дескриптора;
CX_V4_MAX_PKTSIZE
*10.
DisconnectClient()
добавлена отдельная расшифровка
кода EOVERFLOW
(который генерит fdiolib в этом случае) как
"send-buffer overflow".
Ограничение работает, клиент дисконнектится.
Так что -- низачот, надо думать дальше.
Теоретически прокатит cx-starter, проблема только в том, что по нему никак не проконтролируешь "текущее состояние" (возраст) -- только по загрузке процессора.
...проверено:
Видимо, бедняжечка, копирует при каждом добавлении свои сотни мегабайт: очевидно, там не очень удачно получается, что не все отправки скопом, а перемежаются добавлениями данных -- вот тогда fdiolib и перемещает накопленные-но-неотправленные в начало, убирая "дырку" размером _sysbufoffset.
Кстати, в fdiolib можно бы сделать оптимизацию: если в буфере
есть место ПОСЛЕ накопленных данных (т.е.,
_sysbufsize>=_sysbufoffset+_sysbufused+size
) -- а оно
появится после первой же отправки и сдвига -- то не "схлопывать дырку", а
добавлять в конец.
Замечание: тестирование делалось с ограничением в cxsd_fe, задранным до
CX_V4_MAX_PKTSIZE
*1000 (16GB).
03.04.2017: пробуем сделать ту оптимизацию.
Как бы то ни было -- цель достигнута, от мега-тормозов сервера избавились, а уж многогигабайтные объёмы буферов -- сценарий малореалистичный, ограничение на MAX_PKTSIZE*10 его не допустит.
P.S. Чуть быстрее проверять на частоте сервера 1000HZ
(-b1000
) -- буфера растут сильно быстрее, но при этом загрузка
процессора обеими софтинами вдесятеро выше, так что сложно контролировать.
Теперь задеплоим на пульт и посмотрим на поведение.
31.10.2017: собственно, поведение ожидаемое, работает как надо, так что "done".
Но засада оказалась там, где не ждали: когда МНОГО соединений к серверу, то память заканчивается за счёт большого количества не добравшихся до ограничения буферов. Конкретно на виртуалке cxhw с 2ГБ ОЗУ, сервер cxhw:12 частенько убивался kernel OOM killer'ом (скорость роста, на вид по веб-консоли proxmox'а, около 0.5ГБ/5мин); происходило это, когда Федя ложил сеть махинациями со свитчами. Вчера проблему минимизировали ростом ОЗУ до 8ГБ, но это скорее вопрос снижения вероятности, а не гарантии.
03.04.2017: вопрос возник у Феди. Причиной послужило "погусение" толпы программ на пультовой машине linac3. Сервер был еще старый, но посинело ТОЛЬКО на linac3 и скопом программы, питающиеся от разных серверов (linac1:11 и ring1:12), так что скорее всего проблема была в "прогнувшихся" X'ах, из-за которых именно клиентские программы так затормозили (не реагировали на внешние события, а потом -- видимо, при "навёрстывании" -- реагировали с задержкой и в окне "Knob properties" показывали быстро уменьшающиеся возраста -- 30s, 20s, 12s, ...).
Но вот КАК бы такое приостановление можно было реализовать -- хбз.
Сервер-то ведь не знает, что клиент ничего не получил: сервер сделал
отправку, и всё; и даже если пытаться контролировать значение
fdiolib'овского _sysbufused
(как? что считать "хорошо", а что
"плохо"?), то данные могли уйти и лежать в отправном буфере серверовой ОС, в
приёмном буфере клиентовой ОС...
03.04.2017@вечер-душ: а может, сделать такое правило: чтоб клиент время от времени слал "подтверждения", и если оных в течение какого-то времени нету, то приостановить отправку данных?
CXT4_END_OF_CYCLE
?
Использовать имеющийся
уже сейчас CXT4_PING
?
Неа, плохо, потому, что:
06.04.2017@утро-лыжи: и однако
же именно такой вариант выглядит наиболее простым в реализации -- т.к. уже
всё готово, просто добавить
в клиента отсылку ответа на CXT4_PING
.
07.04.2017: а вот нифига! Этак получится, что старые клиенты -- БЕЗ такой фичи -- всё равно автоматом будут переставать получать обновления.
07.04.2017@утро-зарядка: и однако именно 10 секунд выглядит наиболее разумно. Т.к. это и для локальной связи достаточный интервал (локальные фризы из-за ОС заведомо короче), и даже для "из Антарктиды" скорее достаточно. А были мысли сделать "кратно периоду цикла" -- например, 100 циклов -- но для коротких циклов (100Гц) локальные фризы могут начать пакостить.
CXT4_END_OF_CYCLE
тоже приостанавливать?
Так что вся идея "приостанавливать отправку данных замёрзшим клиентам" выглядит сомнительно -- не видно нормального ("красивого", "натурального") механизма реализации, а все имеющиеся идеи дадут какую-то халтурную и ненадёжную махарайку.
Кстати, вот в v2 такой проблемы не было -- потому что там не было и подписки как таковой, а данные каждый раз запрашивались самим клиентом. Так что тормозящий клиент просто переставал присылать запросы.
04.04.2017: можно так:
SendEndC()
и
помечать глобально-для-клиента в v4clnt_t
.
modified
=1, но при выставленном "клиент замёрз" не
присылать -- и, соответственно, флаги модифицированности останутся
взведёнными.
Обсуждение:
Однозначного ответа на него быть не может в принципе, т.к. в каждом случае индивидуально.
(Этот "однозначный ответ" был бы при обязательных подтверждениях на каждый канал, но такую фигню мы делать точно не будем.)
...точнее, ГАРАНТИРОВАННО именно так и случится: в нынешних hw4cx/pzframes/*_data.c каналы-маркеры стоят вначале списков, так что заказываются раньше атрибутных, и в cxsd_fe'шный список мониторируемых тоже попадут раньше.
31.10.2017: посмотрел на вышенаписанное свежим взглядом -- похоже, идея не катит в принципе, так что полностью "withdrawn".
(А полез читать из-за сегодняшней идеи "грохать клиента, которому более 1 минуты ничего не удалось отправить".)
CXT4_END_OF_CYCLE
шлётся ВСЕМ клиентам, в т.ч.
недоприконнекченным (и даже тем, чей endianness неизвестен!).
Обнаружилось сегодня при разборках "почему глючит QL15 / почему соединение обрывается по «Received packet is too big»", побочным эффектом от анализа пакетов.
24.04.2017: в SendEndC()
вставлена
проверка, что если cp->state<CS_USABLE
, то ничего не
делать.
@по-пути-с-обеда, вдоль Будкера Собственно идея: а если отрубать не только переразмеренные буфера, но также в случае, если при непустом буфере (_sysbuf) не было отправки более чем какое-то время.
31.10.2017: некоторые детали:
fdio_set_maxnosendtime()
; например, в секундах.
По умолчанию =0, т.е. "не проверять".
fdio_send()
,
а точнее -- в StreamSend()
+DgramSend()
.
Видимо, иметь поле timestamp, в котором обычно 0. Главный вопрос -- как "начинать счёт времени" и когда "сбрасывать счётчик", чтобы при отправке сильно позже случайно не оказалось, что "прошло более лимита, стреляемся!".
Очевидно, начинать счёт надо при переходе _sysbufused
от ==0
к !=0?
26.12.2017: обнаружено при разбирательстве "а чё
это пакеты от сервера прилетают длиннее на 32 байта, чем значится у них в
DataSize
?".
rs1
).
Тут есть 2 аспекта разной степени серьёзности:
В основном-то ему так будут доставаться кусочки от предыдущих пакетов к нему же -- т.к. это тот же самый replybuf; но без гарантии.
Если первая проблема пока умозрительна (не то у CX-сервера сейчас применение, чтобы серьёзно озабочиваться "раскрытием" подобных данных), то вторая -- уже неприятнее: при возможном расширении протокола поля могут задействоваться, и мусор вместо нулей ("умолчаний") окажется вреден.
Посему -- повсеместно после вызова GrowReplyPacket()
добавлено bzero()
соответствующего chunk'а.
CxV4Chunk
плюс специфичные поля.
Но НЕ весь chunk целиком, с местом под данные (т.е., НЕ объём
rpycsize
)): в этом смысла не усматривается -- правда, не
security-critical у нас софтина.
ServeIORequest()
'а,
отвечающий за отправку ответа на CXC_RESOLVE
(соответствующего
PutNNNChunkReply()'я не существует, да и асимметрия там): она нулит именно
объём dpycsize
-- чисто по причине организации там кода
(присвоение полям стандартного заголовка делается ДО кастинга указателя на
специфичную структуру).
Проверено сниффером -- мусор исчез, везде нули.
28.12.2017: вчера в ServeIORequest()
был
замечен "warning: 'cpid' may be used uninitialized".
И правда -- в ветви CXC_SETMON
переменная cpid
используется, но значение ей не присваивается.
async_CXT4_DATA_IO()
на этот ответ всё равно не
реагирует, так что мусорность неважна".
Put*ChunkReply()
передаваемое им значение критично: там
делается gcid = cpid2gcid(cpid)
, и мусор бы неизбежно вызывал
ошибку, функции б возвращали 0 (объём данных), и клиенту б не отдавались
{R,D}, что вызывало бы ту доставучую ошибку "клиент не получил коэффициент
1000000", но этого не происходит.
cx_rd_cur(); cx_setmon
.
CXC_PEEK
,
CXC_SETMON
.
cpid
, оставшееся от 1-го.
cx_setmon()
-- тогда да.
28.12.2017: исправлено тривиально -- присвоением,
cpid = monr->cpid
.
29.12.2017: приличия ради перепроверяем все warning'и на тему "uninitialized".
Destroy*Sockets()
есть параметр
"and_deregister
".
В старом варианте -- от 07-01-2013 -- всё прекрасно видно: там сокетов
только пара, поэтому функции называются проще, CreateSockets()
и DestroySockets()
, и последняя вызывается с параметром 1 из
cx_term_f()
и 0 из cx_init_f()
-- в случае облома
с созданием.
В любом случае, выглядит это странно. Более разумным представляется
нынешний подход (он используется в cda и прочих, включая даже
RlsV4connSlot()
из этого же fe_cx): если дескриптор ресурса
>=0
, то разрегистрируем ресурс и делаем дескриптору
=-1
(каковое значение он имеет и изначально).
В создаваемом сейчас cxsd_fe_starogate.c делается так же, а сюда вся эта простыня записана именно потому, что при оном создании возник вопрос и захотелось разобраться.
uniq=cp->ID
?
СЕЙЧАС там используются нули, с комментарием /*!!!uniq*/
; а
почему не идентификатор клиента?
BYTE_ORDER == BIG_ENDIAN
пустая. Но это-то
исправляется тривиально -- копированием из ветки ==LITTLE_ENDIAN и
надлежащей заменой.
static inline flt32 b2h_f32(flt32 bige_f32) {return swab32(bige_f32);}
Но ведь это полная хрень! Ибо сначала вещественное bige_f32
будет преобразовано в int32, затем в нём байты переставятся местами, и
получившееся целое же будет обратно преобразовано в вещественное. Но это не
имеет НИКАКОГО отношения к требуемому "переставить байты в представлении
вещественного числа"!
22.03.2018: что интересно, подобные проблемы с конверсией 64-битных и вещественных уже решались, и вполне успешно:
vsdc2_in()
-- для частного случая
float32.
CX_MON_COND_ON_UPDATE
: если это
обычные readonly-каналы, не-AUTOUPDATED, то они начинают очень быстро
молотить...
11.10.2018: проблема вылезла в процессе попыток разобраться "а чё это LAM_SIG у IE4 не приходит" -- когда для проверки в список мониторируемых каналов cdaclient'у был также добавлены ie_bum (счётчик BUM-цикла) и bum_going (флажок, горящий при идущем BUM-цикле, но которому забыта уставка IS_AUTOUPDATED_YES и потому читающийся также поциклово).
Для автообновляемых каналов (и для rw-каналов) это правильный режим работы -- они всё равно не будут молотиться чаще, чем реально обновляются.
А вот для обычных -- совсем плохо. Что делать?
kurrez_cac208_rw_p()
с
комментарием "?vdev_handle_scalar_tube_rw()?", где отдаётся закэшированный
ts.)
Чуть позже: а не, не так всё просто!!! Если в параллель запустить второй cdaclient на тот же канал, то начинает лететь сильно чаще -- по 10 раз за цикл. Очевидно, КАЖДЫЙ мониторирующий присылает по запросу, драйвер отвечает, и ответы раздаются каждому; т.е., спрашивает один -- будет отвечено 1 раз, двое -- 2 раза, и т.д. Но ни 4, ни 10 (при втором клиенте) логике всё равно не поддаются...
12.10.2018: всё, что приходит в голову -- игнорировать
значение cond (ON_UPDATE/ON_CYCLE) для не-rw и не-AUTOUPDATED каналов, а
считать их за "всегда on_cycle". Вечером: вариант --
просто НЕ выставлять в запросе на их чтение флажок
CXSD_HW_DRVA_IGNORE_UPD_CYCLE_FLAG
, тогда "многократность"
будет убрана самим cxsd_hw за счёт upd_cycle
.
.is_internal
,
.rw
и .is_autoupdated
, которое используется для
определения "NEWVAL или CURVAL", которое давно руки чешутся оформить
официально, что почти сделано в
cxsd_fe_starogate.c::CxsdHwIsChanValReady()
.
Но именно "почти" -- это лишь ЧАСТЬ того условия, в котором также
участвует и fresh_age
(что ЗДЕСЬ вроде бы не требуется) и
timestamp.sec
(что уже ТОЧНО не требуется).
Т.е., в качестве "автообновляемых" остаются
.is_internal || .rw || .is_autoupdated
.
cond
?
Прямо в момент регистрации монитора -- нельзя: автообновляемость канала может измениться позднее (монитор уставляется раньше загрузке remote-драйвера, да и вообще драйвер может жонглировать автообновляемостью туда-сюда).
В момент отправки запроса DRVA_READ? Наверное.
Тем более, что в moninfo_t
поле gcid
есть (его ж
CxsdHwDoIO()
и указывают), так что к свойствам канала
доступиться можно быстро.
12.10.2018@вечер-дома: поразбирался -- не-а, проблема намного глубже, ТАК её решать нельзя в принципе.
CXSD_HW_DRVA_IGNORE_UPD_CYCLE_FLAG
, в чьих
интересах?".
Ответ: 27-05-2016, "Для pzframe-клиентов, чтоб каналы могли запрашиваться чаще, чем раз в цикл."
CDA_DATAREF_OPT_ON_UPDATE
.
Нынешняя модель в CXv4 работает постольку-поскольку -- для каналов записи и автообновляемых; для измеряемых-когда-угодно -- нет.
26.11.2018: вроде решение сделано; подробности в разделе по cxsd_hw за сегодня.
24.06.2019@после-обеда, дорога из дома на работу: а именно -- последовательность мыслей:
Т.е., чтоб можно было сказать cxsd_db "пройдись-ка по всем узлам, вызывая указанную функцию-callback".
И функций таких будет 3, вызываемых на разных этапах процесса добычи:
...нужно ли это?
@ИЯФ-~19:00: из возможности переполнения, кстати, следует, что функция-callback должна возвращать значение, и если оно !=0 (или <0), то итератор должен прервать (свернуть) работу и вернуть вызывальщику этот код.
Поэтому -- а как насчёт возможности передавать информацию по сети в "приподупакованном" виде?
И неплохим вариантом упаковки выглядит передача как раз просто
"внутренних имён" БД вместо просто строк. Т.е., прямо "словарь" str_db
(поправка уже при записывании в ИЯФе: оно называется
CxsdDbInfo_t.strbuf
), а в именах -- соответствующие оффсеты
вместо строк.
24.06.2019@ИЯФ, при записывании вышеприведённого, в районе 17:30-18:30: по ходу записывания тех мыслей возникли замечания.
Как вариант решения: можно производить нахождение "циклов" один раз, при старте сервера (в смысле -- после чтения БД, или после её актуализации), чтобы отдельно помечать узлы, смотрящие на cpoint-"контейнеры" в своей же иерархии; и потом итератор чтоб в такие закольцовки не заглядывал бы.
Для этого понадобится:
CxsdDbClvlItem_t
,
CxsdDbClvlItem_t
же:
этот флаг взводить при входе в CXSD_DB_CLVL_ITEM_TYPE_CLEVEL-cpoint и
сбрасывать при выходе из него; нужен для обнаружения, что в момент проверки
(в глубине рекуррентной функции) мы уже внутри этого clevel'а.
Кстати, сейчас проверил -- да, МОЖНО делать иерархии с закольцовками.
-- каналы a.b.c.e и a.b.c.d.c.e смотрят на один и тот же z.0, и туда же смотрит a.b.c.d.c.d.c.e; очевидно, что компонент ".c.d" в середину можно вставлять бесчисленное число раз.dev z noop r1i - cpoint a.b.c.e z.0 cpoint a.b.c.d a.b
...видимо, для ссылок на каналы -- 1 раз для этого канала, для ссылок на устройства -- дергать функцию, проходящую по "аппаратному устройству" и "перечисляющую" каналы из обоих пространств имён.
Отдельный вопрос: а ПЕРЕДАВАТЬ-ТО ЧТО этот callback?
strbuf[]
содержатся именно сами компоненты -- такой способ
"упаковки" выглядит не слишком осмысленным.
Просто потому, что в "словаре" strbuf[]
полных имён каналов
НЕ будет, а будут лишь компоненты.
Можно, конечно, передавать переменной длины список оффсетов, строки по которым надо конкатенировать через '.' для получения полного имени, но это выглядит сомнительной экономией: каждый оффсет занимает 4 байта, так что экономия получится небольшой, а вот усложнение кода -- адским.
Вроде резюме: похоже, когда я это всё обдумывал (после обеда), то пребывал в состоянии некоторой эйфории -- что при надобности всё красиво и сравнительно несложно реализовабельно. Но в реальности есть некоторое количество подводных камней/деталей, делающих реализацию вовсе не такой тривиальной.
24.06.2019@вечер-по-пути-домой: ещё несколько мыслей вдогонку:
Отдельным вызовом cxlib -- неудобно.
Как-то вешать на cda - как?
О: отдавать это значением канала! ЕМНИП, именно так в TANGO делается.
О: а пусть сервер сам собирает этот список имён каналов, и
держит у себя - например, в том же блоке cxsd_hw_buffers[]
, где
прочие cxsd_hw'шные буфера.
(Да, остаются технические вопросы -- вроде "какое имя дать каналу; как его обрабатывать (в cxsd_fe_cx особой проверкой или в cxsd_hw? лучше в cxsd_hw); куда разместить (в зарезервированную первую сотню?). Но это уже детали, которые можно решить в рабочем порядке.)
(Да, в TANGO-то наверняка возвращается объект, вроде массива строк. Ну чё уж -- переживём.)
27.06.2019@утро-дорога-на-работу-около-ИЦиГ: разделять имена не '\t', а '\n'! Это удобно: сразу будем получать список строк, удобный для обработки shell'ом.
Только за компанию бы надо научить cdaclient печатать строки "как есть", НЕ забэкслэшивая спецсимволы.
Причём раньше (с дистрибутивом за весну или февраль) такой проблемы не было.
Проверили наличие/отсутствие cxhosts, могущего ограничивать запись -- да нет, отсутствует.
Поскольку я такого вроде не замечал, то возникла мысль, что проблема может быть связана с Debian/Ubuntu, на которое это происходит (мало ли -- что-то чуть иначе компилируется, или библиотека какая-то по-другому работает).
01.12.2020: Роговский вчера вечером перепробовал толпу дистрибутивов и нашёл, что проблема появилась между w20200827-cda_lock_stat_of_ref.tar.gz и w20200912.tar.gz.
SendReplyPacket()
.
22.03.2021: всё тянул с исправлением, а вот теперь стало реально необходимо: из-за этого косяка не работает bridge_drv через cx:: -- придётся исправлять и срочно.
Проглядывание исходника наводит на следующий проект:
InitReplyPacket()
. И если она вернула +1 -- значит, пакет УЖЕ
есть, и "пакетоформировальщик" этот факт запоминает.
SendReplyPacket()
НЕ вызывается.
InitReplyPacket()
надо переделать на
возврат +1 при уже наличии начатого пакета.
rpycn
и replydatasize
должны переехать в v4clnt_t
.
replydatasize
, чтобы заполнять chunk'и;
rpycn
надо по-хорошему инкрементировать только
после УСПЕШНОГО заполнения.
SendAReply()
и SendNotification()
таковы
-- то надо уметь "сбрасывать" начатый, но ещё не отправленный пакет.
(И, естественно, сбрасывать только если пакета НЕ БЫЛО ранее, а был начат этим же "формировальщиком".)
Для этого заводим DropReplyPacket()
-- она уже даже сделана
(тривиально).
24.03.2021: касательно МЕНЕДЖМЕНТА вырисовывается такой проект:
GrowReplyPacket()
, чьи интерфейс и
функциональность значительно изменятся:
rpycsize
-- объём добавляемого chunk'а.
cp->replydatasize+=rpycsize
и cp->rpycn++
.
rpycn
только после УСПЕШНОГО
заполнения: как показывает просмотр всех нынешних юзеров
GrowReplyPacket()
'а, если оный вертает "success", то и само
заполнение тоже ВСЕГДА успешно. Оно и понятно: ведь сначала вычисляется
потребный объём, а потом уже просто ведётся заполнение, и обламываться там
нечему.
Вывод: rpycn
должно инкрементироваться после успешного
увеличения места в replybuf'е.
24.03.2021@вечер, 21:00+: сделано, по описанному проекту. Пока -- на всякий случай -- в отдельном zxsd_fe_cx.c, который можно компилировать для проверки прямо в cxsd_fe_cx.o.
И это явно правильный шаг: объём файла сократился на 6кБ и почти на сотню строк.
Теперь -- проверять...
25.03.2021: проверил -- да, на вид всё хорошо: и "cdaclient CHAN=VALUE" корректно получает отклик на запись, и bridge_drv через cx:: стал работать правильно.
Как бы устроить более масштабный тест?
Но zxsd_fe_cx.c уже сделан текущим -- переименованием cxsd_fe_cx.с.
per_cycle_monitors_some_modified
только взводится =1, но
никогда не сбрасывается =0.
Это должно приводить к тому, что если хоть раз хоть 1 канал присылался по циклу, то ВСЕГДА -- каждый цикл -- будет приходить пакет, в т.ч. с NumChunks=0.
25.03.2021: проверил -- да, так и есть.
Это, конечно, не бог весть какая проблема. Во-первых, это просто лишний пакет, ничему особо не мешающий. И во вторых, ведь не-ON_UPDATE-каналы по факту используются только в скринах, а там обычно обновления есть практически всегда.
Как бы то ни было -- исправлено: =0 делается в SendEndC()
сразу после обнаружения и перед отправкой данных.
22.02.2018: Это будет плагинчик для протокола "starogate", используемого на электронно-лучевой сварке, сейчас в CXv2, и чья реализация необходима для v4, чтобы сторонние программы (медведевские) могли бы доступаться к СУ.
Некоторый вопрос -- насколько это идеологически правильно, учитывая, что в идеале прямо cda должна быть собирабельна и использовабельна под Windows? Но сделать плагинчик очень просто, и текущую проблему он решит, так что просто сделаем его.
И да -- для юзабельности нужно будет собирать его в .so.
Создавать начинаем прямо в 4cx/src/lib/srv/, а уж потом куда-нибудь переселим.
26.02.2018@лыжи: идеологический вопрос -- КОГДА отправлять значения каналов?
Учитывая, что у Медведева были какие-то сложности с вычитыванием приходящих данных (что-то не слава богу в LabWindows СМШ; уведомления о готовности новых данных в сокете не присылаются, что ли), то разница замечена и не будет.
01.03.2018: ЗАМЕЧАНИЕ: если по циклу не слать, то нужно прямо в момент регистрации монитора сразу отправлять текущее значеиие, если оно готово. В противном случае не изменяющиеся каналы записи вообще никогда не будут присылаться.
27.02.2018: "скелет" -- все методы и регистрация сокета -- сделан. Теперь наполнять "мясом".
27.02.2018@лыжи-после-обеда: 1-й километр 2-ки: кстати, вот горевал я, что поздновато занялся этой штукой -- давно надо было сварку перевести на v4, а теперь и 3-й экземпляр (в Чёмах) будет на v2.
Но, коль там переезд тормозит (проблемы с вакуумом), а cxsd_fe_starogate.c щас добъём -- ну и сделать чёмскую сварку СРАЗУ на v4! Ведь там пока почти ничего не требуется, только скаляры, и даже CAMAC'а нет.
Это и будет первым маленьким шагом в сторону перевода на v4 всей сварки -- вместо одного глобального переезда.
01.03.2018: заодно, кстати, легко решится проблема с добавлением дополнительного канала к драйверу "weld02" -- в v4 расширение карты реализуется проще. 29.06.2018: а в weld02, оказывается, и так есть 1 зарезервированный канал -- его надо просто разблокировать при sw_ver>=9.
05.03.2018: а можно будет и "большую" сварку (verona) перевести на v4 -- там же векторные каналы не используются (т.к. нет "управления техпроцессом").
28.02.2018: пилим дальше...
AcceptConnection()
.
Тут добавлена серверная специфика -- аллокирование ClientID.
InteractWithClient()
. Вот в нём парсинг остался, а
"мясо" совсем иное.
Учитывая, что вообще сам протокол starogate -- это хак, и вряд ли будет много каналов мониторироваться, то на дикий перерасход памяти (20*2*sizeof(double)=20*2*8=320 байт на монитор) можно и плюнуть.
Т.е.,с раздельными imm_phys_rds[4]
и
*alc_phys_rds
:
imm_phys_rds[]
.
И не забывать обновлять по событию _R_RDSCHG.
01.03.2018: ...
InteractWithClient()
:
StoreRDs()
, чьё содержимое скопировано с
cda_dat_p_set_phys_rds()
.
is_internal
либо имеет не-NEVER_READ-timestramp и является
rw
или is_autoupdated
. В точности, как
проверяется в cxsd_fe_cx.c и cda_d_insrv.c.
CxsdHwIsChanValReady()
, покамест локальную,
но вообще-то её надо унести в cxsd_hw.c -- чтоб не
дублировать одни и те же нетривиальные условия в куче точек.
CxsdHwAddChanEvproc()
.
MonEvproc()
. Он очень простой: по _R_UPDATE
вызывает SendMonitor()
, а по _R_RDSCHG -- пере-добычу
калибровок и StoreRDs()
.
fdio_send()
-- что если ошибка, то
грохать соединение. 12.03.2018: сделано.
02.03.2018:
SendMonitor()
-- собственно добыча
double-значения и отправка его клиенту.
Тут надо как-то бороться с потенциальными циклами -- вроде "moninfo_t.being_reqd". Видимо, копировать с cxsd_fe_cx.
И не игнорировать ли оное для каналов is_rw/is_autoupdated? Т.е., НЕ запрашивать для них (даже и начально не надо?)?
05.03.2018: размышленческое о мониторинге:
Отправлять -- хоть раз в цикл же, хоть по обновлению, это уже непринципиально.
06.03.2018: Кстати, cda_d_insrv.c использует именно такой подход.
07.03.2018@утро-~10:20, по пути пешком от гаража мастера Саши до проходной ОК: а зачем СЕЙЧАС так надрываться, реализуя именно сейчас "максимально правильное решение задачи мониторинга"?
07.03.2018: ну, допиливаем:
SendMonitor()
-- собственно добыча
double-значения и отправка его клиенту": отправка была сделана еще
позавчера, а сейчас добыча. Вычитывание, в соответствии с dtype, плюс
RD-конверсия -- скопированы с
cda_core.c::cda_dat_p_update_dataset()
.
monitors*
, включая
monitors_needs_rebuild
.
starogate_begin_c()
с подчинёнными
функциями-итераторами -- практически подчистую скопирован с cxsd_fe_cx'шного
cx_begin_c()
, с минимальными модификациями: удалением проверки
на не-COND_ON_CYCLE и переименованиями.
periodics_needs_rebuild
.
starogate_end_c()
-- пустая, т.к. решено по циклу
ничего не слать, только по обновлениям.
Вопрос: а не включать ли KEEPALIVE'ы? 12.03.2018: ага.
+1. Добавление мониторов. +2. Удаление мониторов. +3. Удаление evproc'а. +4. KEEPALIVE'ы. 5. Проверки результатов fdio_send()'ов.
12.03.2018: добиваем:
GrowUnitsBuf()
.
mp->cpid=-1
сразу после аллокирования яцейки -- чтоб
RlsMonSlot() не делал "periodics_used--" в случаях, когда еще не сделано
"++" (всё полностью аналогично решению от 10-03-2018).
RlsMonSlot()
:
periodics_{used,needs_rebuild}
.
fdio_send()
заменены на действия
с проверкой. Для этого
send_or_close()
, проверяющая результат
и грохающая соединение при ошибке.
SendMonitor()
, где механизм свой)
заменены на неё.
Есть только один формальный повод для беспокойства: fdiolib может вызвать
нотификатор не только асинхронно (когда сам вызываем из cxscheduler'а), но и
в ответ на fdio_send()
: StreamSend()
может вызвать
StreamReadyForWrite()
, который, в свою очередь, при ошибке
дёрнет close_because()
, вызывающий и нотификатор. Впрочем, это всё обсуждается в fdiolib'овском разделе с
05-04-2015.
13.03.2018: проверяем.
load-frontend
(через ключ -e
) --
работает! Только надо еще директорию указать, а то файл же пока не в
стандартной lib/server/frontends/.
send_or_close()
"слал" в cp->fd вместо cp->fhandle, а при
создании монитора сохранение имени делалось как mp=strdup(p) вместо
mp->name=strdup(p) (вот этот косяк искал долго -- эффект-то дикий, т.к.
последующие сохранения всех параметров монитора идут чёрт-те куда).
Это при записи ТОЛЬКО через starogate, через cdaclient всё как надо.
Несколькими минутами позже: а, ч-чёрт -- с этим разобрался! Всё просто: писалось в CAC208'ский канал out1, для которого стояла скорость -1.0V/s (да еще и tac=10) -- вот оно и давало значение сначала при уставке ("подтверждение команды"), а потом финальное значение по окончанию хода.
Разбираемся с п.1.
dt_double
вместо CXDTYPE_DOUBLE(=19) становилось
CXDTYPE_INT32(=10). Вот следующие отправки и становились невалидными.
Похоже, CxsdHwDoIO()
как-то портит параметры! Ведь
реальный тип канала -- именно _INT32; значение подменяется при
преобразовании? Да, похоже -- StoreForSending()
это делает?.
Подробнее на эту тему -- в разделе по cxsd_hw.
static
" у dt_double
.
14.03.2018: фиксим.
static
" и у dt_double
, и, с
учётом найденной сегодня потенциальной порчи nelems, у nels_1
.
Итого, считаем модуль доделанным.
А ведь изначально думалось "проект на денёк-два" -- ага-ага...
periodics_used
НИКОГДА не увеличивалось (и сопутствующее
взведение periodics_needs_rebuild
отсутствовало), а только
уменьшалось.
В результате по факту НИКОГДА НИКАКИЕ данные клиенту не отправлялись. Это было проверено с помощью telnet'а.
17.06.2022: обнаружилось это случайно -- при разбирательстве "а чё это в cxsd_fe_epics не делаются обновления": оказалось, что оное увеличение и там отсутствует, а поскольку там вся инфраструктура скопирована отсюда, то полез сюда смотреть, что же было забыто при копировании, и оказалось, что "забыто" было при копировании сюда инфраструктуры из cxsd_fe_cx.c (а у v2'шного starogate.c была совсем иная архитектура -- то ж Cdr-программа, а не модуль сервера).
Исправлено, добавлением по образу/подобию из cxsd_fe_cx.c пары строчек
сразу послеcp->periodics_used++; cp->periodics_needs_rebuild = 1;
GrowUnitsBuf()
'а periodics
'а.
Проверено тем же telnet'ом -- теперь обновления шлются.
20.06.2022: Алексей Медведев на моё позавчерашнее письмо "а чё от вас нет никаких претензий, что оно не работает?" вчера ответил полным непониманием даже области проблемы.
Меня это сподвигло полезть разбираться -- "а вдруг там есть какие-то
более сложные эффекты, вроде, например, того, что после снятия монитора
значение periodics_used
становится отрицательным?".
Поразбирался, с отладочной печатью и закомментировав ту пару строк -- всё прояснилось:
periodics_used
действительно
становится отрицательным, так что внутрь RequestSubscription()
оно входит и даже "вызывает" CxsdHwDoIO()
(но, поскольку с
отрицательным значением count
, то та ничего не делает).
А поскольку всё тут per-connection, то ситуация "один клиент подписался, потом сдох, приведя к отрицательному значению, и затем уже другой подключился" также невозможна.
Ведь косяк препятствовал лишь ежецикленному ЗАПРАШИВАНИЮ каналов.
А отдаче данных из них при обновлении он никак не мешал.
Таким образом, за запрашивание отвечал скрин, обеспечивая обновление данных, приходившее уже через вполне живые evproc'ы.
13.07.2023: этот баг был родственником косяка
"не модифицировалось значение periodics_allocd
" из
cda_d_insrv.c, найденного 03-11-2017. Не копией, конечно, но в
аналогичном месте и идеологически близким. А самое позорное -- сделанным
уже ПОСЛЕ исправления того, через 5 месяцев...
Так вот: оную поддержку векторности в ТЕКСТОВОМ протоколе можно реализовать примерно такими средствами:
31.01.2019: контраргумент: поддерживать ЭТО придётся самостоятельно, и никакие усовершенствования epics'ного функционала (типа EPICS4/EPICS7) туда никак не попадут.
05.09.2019: в порядке обсуждения -- какие на таком пути будут проблемы:
...и он может быть заточен под multithreading, что создаст отдельный класс проблем.
...другое дело, что в случае EPICS можно считать, что на каждом узле/IP будет работать не более 1 штуки IOC, так что можно особо не заморачиваться, а резолвить UDP-запросы исключительно в "своей базе" (того IOC'а, в котором модуль работает).
Я-то ответил, что "да, такая возможность была заложена изначально. Вопрос лишь в том, КАК реализовать Channel Access: вручную -- как-то не хочется; заложена ли в EPICS возможность использовать ихний код как библиотеку для чего-то другого -- я не знаю.", и стал рыть интернеты -- памятуя давно слышанный термин "portable Channel Access server".
И быстро нашёл -- да, это оно, ключевое слово "CAS" или "libCAS". Этот раздельчик -- для сбора информации.
26.12.2021: итак:
Там есть энное количество "документации" (о ней подробнее ниже) и упоминание, что в EPICS3 эта libCAS входит в "base", а...
Презенташка-введение на 22 страницы.
Опять Марти Краймер -- возможно, вариант/предок предыдущего.
Ещё один пересказ того же -- укороченный, с меньшим числом деталей/примеров/иллюстраций.
Довольно подробное изложение на 70 страниц. К сожалению, картинки там все съехавшие сильно вправо (вёрстка 1999 года, похоже, плохо показывается современными вьюерами).
22.03.2022: шибко уж оно устаревшее. Например, никакого
"pvCountEstimate
" теперь нету.
Ещё более подробный референс. С одной стороны -- как бы просто куски из header'ов, но с другой -- с изрядным объёмом текстовых комментариев.
12.03.2022@~16:30, по дороге к родителям, лесок около стадиона НГУ и перед Коптюга: а почему, собственно, никто не сделал связку "EPICS+TANGO" путём интегрирования libPCAS'а в TABGO-сервер, зачем вместо этого маются с хренью вроде "EPICS to TANGO translator"?
Ведь, казалось бы -- просто слушай EPICS'ные запросы по CA, обрабатывай их обращениями к TANGO, и всё прекрасно, так? И не нужно никакого предопределённого списка каналов, всё работает автоматом динамически.
13.03.2022: посмотрел -- ну да, похоже, что нету: гугление НИЧЕГО не дало: ни по «"libpcas" tango» (тут вообще "0 results"), ни по «"portable channel access server" tango». 22.03.2022: только гуглить-то надо было «"libcas" tango» (или «tango controls "libcas"»), а не "libpcas"; но результат столь же пуст.
А может, у них там просто внутрисерверного API нету?
Но тогда ведь можно сделать просто софтинку, слушающую libPCAS'ом, и работающую как КЛИЕНТ к TANGO? Или там проблема будет с тем, что в TANGO нужно ЗАРАНЕЕ знать тип "атрибута"?
17.02.2022: пока чисто подготовка и именно "скелет":
Для собираемости
пришлось слегка покалечить cxsd_modmgr.h (и cxsd_modmgr.c)
-- из-за дурацкого C++'ного ключевого слова "typename
", которое
и в C++-то требуется из-за дурного синтаксиса (впрочем, унаследованного ещё
от C: "X * p
" -- это перемножение или же объявление переменной
p
как указателя на тип X
?); параметр с таким
именем переименован в typ_name
.
Этого можно было избежать, если бы использовался раздельный набор файлов для интерфейса и реализации (cxsd_fe_epics.c и cxsd_fe_epics_meat.cpp соответственно) -- тогда б не пришлось в C++'ный код включать cxsd_modmgr.h. Первоначальный проект таким и был, но потом передумалось.
18.02.2022: всё-таки разделил на интерфейс и реализацию -- cxsd_fe_epics.c и cxsd_fe_epics_meat.cpp соответственно.
Просто после добавления ещё нескольких #include
'ов
выяснилось, что компилятору также не нравится typename
в
cxsd_dbP.h, а уж там переименовывать нет никакого желания.
@~13:20: и файлы определений для кросс-ссылок сделаны -- cxsd_fe_epics.h и cxsd_fe_epics_meat.h.
22.03.2022: немного о "принятых решениях" и о попытках запуска хоть минимального варианта.
(Почти месяц лишь вяло исследовал вопрос, читая документацию, но почти ничего не делая, из-за пребывания в а*уе от происходящих в стране событий...)
"Решения" -- разделение обязанностей между файлами таково:
#include
'ить cxsd_hwP.h, что плюсовому коду
недоступно).
Но с точки зрения РЕАЛЬНОЙ РАБОТЫ -- просто переадресующим запросы к C'шной части.
Мытарства:
caServer()
больше НЕТУ параметра pvCountEstimate
.
-dsc -e 'load-frontend epics'
" -- обламывается с малопонятными
сообщениями, вроде
undefined symbol: _ZN15fe_epics_Server8createPVERK6casCtxPKc
Оказалось -- это отсутствующий (объявленный в классе, но НЕопределённый
метод), а такие mangled-имена дешифрируются утилиткой
c++filt
::
% c++filt -n _ZN15fe_epics_Server8createPVERK6casCtxPKc fe_epics_Server::createPV(casCtx const&, char const*)
-lcas
в
дополнение к тем -lca -lCom
, что для клиента.
exit(1)
-- и хрен поймёшь, почему:
Надо напихивать отладочной печати...
create_epics_Server()
было забыто "return 0
", вот
СЕРВЕР сам и завершался!!!
Исправил -- теперь запускается!
Возможно, дело в том, что процесс cxsd слушает порты 5064/udp и 5064/tcp,
а caget, судя по strace, пытался иметь дело с 5065; правда, по
отладочному выводу не совсем ясно, что он там делает -- ШЛЁТ ли, т.к.
делается bind()
:
bind(5, {sa_family=AF_INET, sin_port=htons(5065), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
Правда, оно там ещё вякает про невозможность найти caRepeater:
**** The executable "caRepeater" couldn't be located **** because of errno = "No such file or directory". **** You may need to modify your PATH environment variable. **** Unable to start "CA Repeater" process.
23.03.2022: пытаемся разобраться во всей этой бодяге -- гуглим на тему "carepeater 5064 5065". Результаты:
Прочитано в мэйллисте в сообщении "Re: Post numbers used by CA client" от Andrew Johnson за 05-07-2016:
There can only be one CA repeater running at once. When the CA client library starts up it looks for a repeater by trying to bind a socket to TCP port 5065; if that fails it assumes there is a repeater running already, so it that. If the bind succeeds though it will fork a separate process to run the repeater in.
$EPICS_CA_SERVER_PORT
), 5065 -- порт repeater'а
($EPICS_CA_REPEATER_PORT
).
И вообще много общей информации о функционировании протокола и настройке работы в сетях.
Плюс, это третий из нагугляемых результатов, а дальше уже малоинтересное.
23.03.2022: а теперь, с полученным осознанием, надо провести ещё немного диагностики:
Да, шлёт. Причём по КУЧЕ адресов -- на все интерфейсы на их broadcast-адреса.
Если вдруг станет -- значит, дело лишь в том, что никто не мониторит сокет, так что надо читать tutorial дальше на тему "как интегрироваться с fdmanager'ом".
Посмотрел -- нет, почему-то НЕ готов.
nc -vvvlu 0.0.0.0 5064
-- ничего не ловит.
nc -u localhost 5064
) ловит, и, будучи запущенным на порт
8012, ловит пакеты CXv4'шного UDP-резолвинга.
Вот как так, а? Почему в CX это всё просто работает, а в EPICS -- фиг?
Чуть позже: РАЗОБРАЛСЯ!!!
(А
"nc -u localhost 5064
"
работало потому, что интерфейс loopback в фильтрации не участвует.)
Итого -- да, теперь надо учиться интегрироваться с fdmanager'ом.
24.03.2022: изучаем оный fdManager, с которым есть странности -- присутствуют аж ДВА .h-файла, fdManager.h и fdmgr.h; может, кто-то из них устаревший вариант или один является обёрткой для другого? Гуглим на тему "fdManager.h fdmgr.h".
File Descriptor Manager. fdManager.h describes a C++ implementation. fdmgr.h describes a C implementation. Neither is currently documented.
Т.е.,
Крайне странное решение, особенно с учётом того, что сами реализации ОБЕ на C++.
// Loop forever osiTime delay(1000u, 0u); while (aitTrue) // fileDescriptorManager is a predeclared object found in fdMgr.h fileDescriptorManager.process(delay);
14.11.2024: и там тоже косяк: не в fdMgr.h (которого не существует), а в fdManager.h.
Проблема в том, что никакого аналога CA'шного
ca_add_fd_registration()
для "правильной интеграции" с основным
циклом программы тут НЕТ.
Как делать -- непонятно...
@~17:00, лесок около стадиона НГУ по дороге на Морской: ну, как вариант -- организовать периодический поллинг с частотой 10Hz.
@вечер: делаем.
call_fdManager_process()
, которая:
epics_init_f()
'ом регистрируется на 1-й вызов через 1us
(т.е., просто СРАЗУ после запуска основного цикла),
fdManager_process_polling_tid
, чтобы в
epics_term_f()
'е разрегистрировать.
cxsd_fe_epics_meat_do_poll()
.
cxsd_fe_epics_meat_do_poll()
же
do_call_fdManager_process()
,
fileDescriptorManager.process(0)
.
fdManager::process (double delay)
-- т.е., неочевидны правила обращения с таймаутами:
Похоже, что да. Но в примере из tutorial'а
(выше) используется тип osiTime
и конструктор.
Попытка понять путём чтения кода ничего не дала -- там как-то совсем неясно, как эти "времена" работают, т.к.
-- вот это "double minDelay = this->pTimerQueue->process(epicsTime::getCurrent()); if ( minDelay >= delay ) { minDelay = delay; }
getCurrent()
" смущает: ведь "current" применительно к
времени обычно означает ТЕКУЩЕЕ время.
Но далее оное minDelay
используется для инициализации
select()
'ового параметра timeout
.
И просто 0
сработал.
Похоже, что никак -- ни в коде ничего не видно, ни даже
идеологически неясно, как бы можно было указывать "навечно" вещественным
числом (разве что просто большое значение), т.к. никакого аналога
select()
'ового "timeout=NULL" тут не сделаешь (NaN?).
Вот так -- ради реально ОДНОЙ строки кода (всё остальное -- обвязка для её вызова) пришлось проводить кучу разбирательств. Как же я "обожаю" EPICS, с его совершенно лишней C++'ностью ("язык write-only") и "понятностью" кода...
Проверяем -- ну да, работает! Отладочная печать в
pvExistTest()
показывает приходящие запросы.
Кстати, эта же отладочная печать, самим ХАРАКТЕРОМ печати -- пачка одинаковых запросов подряд, потом реже, потом ещё реже -- показывает странность (мягко говоря; "странность" -- в плохом смысле) устройства резолвинга в EPICS.
Битым текстом: то, что СЕРВЕР (которому по определению положено СЛУШАТЬ) по собственной инициативе РАССЫЛАЕТ незваные (uninvited) пакеты -- это странно; вот просто из общих соображений "как правильно архитектурить софт" конкретно это решение -- лично для меня выглядит дурным.
25.03.2022: случайно наткнулся на статью "A Virtual Control Panel Configuration Tool for the X-Window System " от Jeffrey O. Hill, Leo R. Dalesio and Debona M. Kerstiens, 1992 год (кстати, хбз где Google это "1992" откопал -- в тексте нету).
И там есть абзац
A file descriptor manager library based on the system call select () provides the display manager with the ability to respond asynchronously to events from both the X window system and channel access simultaneously.(А наткнулся при гуглении на тему "fileDescriptorManager.process".)
А ещё наткнулся на презенташку "Channel Access Servers" Kenneth Evans, Jr. October 8, 2004 Part of the EPICS "Getting Started" Lecture Series
25.03.2022: отправил в tech-talk письмо с вопросом "How to integrate libcas fdManager with application's native main loop?". Будем ждать ответа.
28.03.2022: неа, фиг -- прошло 3 дня (из них полная пятница 25-е в США), но так никто и не отреагировал. Впрочем, оно и неудивительно -- судя по тому, что я увидел внутри fdmanager.cpp, никакого "внешнего интерфейса" для интеграции со сторонними основными циклами там и нет.
ca_add_fd_registration()
должна
бы как-то с оным fdManager'ом работать. Значит, теоретически-то возможно?
28.03.2022: пытаемся посмотреть.
ca_add_fd_registration()
определяется в
base-3.15.6/src/ca/client/access.cpp следующим образом:
/* * CA_ADD_FD_REGISTRATION * * call their function with their argument whenever * a new fd is added or removed * (for a manager of the select system call under UNIX) * */ // extern "C" int epicsShareAPI ca_add_fd_registration ( CAFDHANDLER * func, void * arg ) { ca_client_context *pcac; int caStatus = fetchClientContext ( &pcac ); if ( caStatus != ECA_NORMAL ) { return caStatus; } pcac->registerForFileDescriptorCallBack ( func, arg ); return ECA_NORMAL; }
registerForFileDescriptorCallBack()
живёт в base-3.15.6/src/ca/client/ca_client_context.cpp;
28.03.2022: технологический вопрос:
casPV
на всё время открытости PV.
moninfo_t
выделить место под наследника
casPV
, ...
А вот и нет -- НЕ МОЖЕТ. Объём-то неизвестен...
Так что придётся хранить там просто указатель на объект, аллокируемый с
помощью new
в обычном порядке...
Гугление на тему "c++ create object at specific address" сразу дало результат: да, можно, называется это "placement new" и "placement delete".
new (ADDRESS_EXPRESSION) OBJECT_CLASS_NAME
а деструктирование -- просто явный вызов деструктора:
POINTER_TO_OBJECT->~OBJECT_CLASS_NAME()
#include <new>
(это потому, что реализуется всё через "перегрузку" операторов new
и delete
).
Резюме: да, конкретно ТУТ оно не пригодится (из-за невозможности узнать в .c'шном файле требуемый объём памяти под размер плюсового объекта), но знание само по себе полезное.
Ещё одно соображение: в общем случае -- точнее, конкретно в НАШЕМ случае
-- такой штукой пользоваться чревато, т.к. у нас SLOTARRAY пользуется
realloc()
'ом, перемещающим выделенный блок в другое место, а
если где-то внутри C++'ного объекта (или где-то "глубже" в недрах его
библиотеки, при использовании) сохраняется указатель на него, то это всё
сломается нафиг.
21.09.2024: попробовал, на epics2tango_gw.cpp -- там PV размещена прямо внутри монитора:
typedef struct { ... e2t_PV the_PV; ... } MonInfo;
Поскольку ТУТ мониторы аллокируются хоть группами, но остаются в
неизменных адресах (а не бродят по памяти, realloc()
'ясь при
росте SLOTARRAY'я), то использование безопасно.
mp->the_PV.~e2t_PV();
new (&(mp->the_PV))e2t_PV();
Просто сам объект (т.е., "ОБЪЕКТ&
", "ссылка") --
-- НЕЛЬЗЯ, т.к. ругается на ненайденность оператора должного вида:new (mp->the_PV)e2t_PV();
epics2tango_gw.cpp:616:28: error: no matching function for call to 'operator new(sizetype, e2t_PV&)' new (mp->the_PV)e2t_PV(); ^
#include <new>
" НЕ обязателен -- как
показал прогон под "gcc -E
", это делается кем-то из стандартных
#include'ов (похоже, что <string>, опосредованно, через
allocator.h и т.д.).
e2t_PV
объявлять ДО типа
MonInfo
, т.к. теперь в последнем явное поле типа первого (а не
указатель, как в cxsd_fe_epics.c).
28.03.2022: движемся дальше.
Готовим инфраструктуру для создания/регистрации PV.
moninfo_t
.PV_ptr
-- для хранения
указателя на объект-наследника casPV
.
cxsd_fe_epics_set_PV_ptr()
для его уставки.
cxsd_fe_epics_createPV()
,
беря за основу аналогичное из cxsd_fe_starogate.c.
Что влечёт за себя перетаскивание также и всякого сервисного кода.
clientrec_t
переведено в
"глобальную" область файла, получив при этом префикс
"fe_epics_
".
Это включает в себя monitors_list
* и
periodics
*.
epics_term_f()
.
...а возможно, что стоит также добавить и явную инициализацию в
epics_init_f()
Тут же, возможно, понадобится аллокировать память (сохраняя указатель в
moninfo_t
) под махинации с векторными каналами.
...естественно, только для векторных. Тут можно сделать как в cda, где
есть refinfo_t.valbuf
, но только "хранить"-то не нужно, поэтому
оный valbuf
будет просто переменной в функции, выполняющей
конверсию.
Ведь у нас каналы в основном int32, но для клиентов они эффективно вещественные, так что тут "светить" их наружу целочисленными не удастся, т.к. результат преобразования ну никак не целый.
Напрашиваеются варианты:
Так себе вариант (для реально целых и прочих однобитовых просто бред).
Проблема: а если драйвер загрузится ПОСЛЕ подключения клиента?
Максимально гибко, но как-то некрасиво...
30.03.2022: у класса casPV
есть опциональный метод bestExternalType()
, с комментарием
"tbe best type for clients to use when accessing the PV".
Сходу проку не видно (ну чисто отдавать основное представление), но вдруг
что придёт в голову...
29.03.2022: немного:
cxsd_fe_epics_createPV()
потенциально есть
ситуации с обломами по этой причине, то формализованы коды
CXSD_FE_EPICS_ERROR
=-1 и
CXSD_FE_EPICS_noMemory
=-2.
fe_epics_Server::createPV()
добавлена их обработка
(реально -- скорее второго, а остальное считается "по умолчанию" при
mid<0
).
fe_epics_meat_PVrec_t
, в котором всё нужное и
будет храниться, и указатель на экземпляр такого контейнера и будет
храниться в мониторе.
Ну и сделано -- объект инстанциируется при помощи new
и, при
успехе, указатель сохраняется в мониторе.
30.03.2022: далее...
casPV
.
fe_epics_PV
.
fe_epics_meat_PVrec_t
.
getName()
, т.к. в предке он объявлен как "= 0
"
(т.е. "pure virtual", а класс тем самым становится абстрактным -- вот на
попытку инстанциировать абстрактный класс компилятор и ругнулся).
fe_epics_Server::createPV
: после успешной регистрации монитора,
аллокирования fe_epics_meat_PVrec_t
и сохранения указателя на
него в мониторе добавлено
return &(PV_ptr->PV);
Так вот -- ФИГ! Компилятор ругается
invalid conversion from 'fe_epics_PV*' to 'caStatus {aka unsigned int}'
При том, что сделано всё по tutorial'у, стр.25.
Лезем разбираться в исходники, и grep'ом по "S_casApp_pvNotFound" находим в base-3.15.6/include/casdef.h:
pvAttach()
, у которого в описании
про возвращаемое написаны как раз те варианты, что на стр.25.
// deprecated interfaces (will be deleted in a future release) virtual class pvCreateReturn createPV ( const casCtx & ctx, const char * pPVAliasName ); virtual pvExistReturn pvExistTest ( const casCtx & ctx, const char * pPVAliasName );
Т.е., tutorial дико устарел, а сейчас всё совсем по-другому.
И ключевое слово -- "pvAttach()
", надо переходить на неё.
cxsd_fe_epics_meat.cpp: In member function 'virtual pvAttachReturn fe_epics_Server::pvAttach(const casCtx&, const char*)': cxsd_fe_epics_meat.cpp:79:24: error: invalid conversion from 'fe_epics_PV*' to 'caStatus {aka unsigned int}' [-fpermissive] return &(PV_ptr->PV); ^ In file included from cxsd_fe_epics_meat.cpp:1:0: /home/user/compile/base-3.15.6/include/casdef.h:139:5: error: initializing argument 1 of 'pvAttachReturn::pvAttachReturn(caStatus)' [-fpermissive] pvAttachReturn ( caStatus statIn ); ^
ПОЧЕМУ так -- неясно: в определении класса pvAttachReturn
есть метод ("оператор-конструктор"?)
const pvAttachReturn & operator = ( casPV * pPVIn );
-- вроде бы он должен мочь создать объект из указателя.
fe_epics_PV*
к casPV*
?
Попробовал -- фиг, тот же эффект.
grep -w pvAttach compile/base-3.15.6/**/*.c*
И конкретно в
base-3.15.6/src/template/base/top/caServerApp/exServer.cc сходу
нашлось, что exServer::pvAttach()
ЕДИНСТВЕННЫЙ
не-S_casApp_
-возврат делает как
return *pPV;
-- т.е., по ссылке (by reference), а не по указателю.
Какого фига конкретно такой конструктор -- это
pvAttachReturn ( casPV & pv );
-- работает, а по указателю нет -- хбз...
Почему всё так, как оно есть -- непонятно. Как правильно -- тоже непонятно...
31.03.2022: пришла в голову мысль, как проверить,
что всё работает корректно, без лишнего копирования/дублирования casPV'ей, а
просто указатель нужным образом сохраняет: сделать "другие"
методы -- конкретно read()
и write()
(чтоб можно
было их дёрнуть посредством caget/caput) -- и в них вставить отладочную
выдачу значения this
; так будет видно, один и тот же ли это
экземпляр объекта или разные.
Сделал, проверил -- да, значения this
везде
одинаковые.
02.04.2022: fe_epics_Server::createPV()
удалена.
fe_epics_PV()
:
(А я-то гадал -- "как же оно выбирает, старым или новым способом зарегистрировать PV по имени?").pvAttachReturn caServer::pvAttach ( const casCtx &ctx, const char *pAliasName ) { // remain backwards compatible (call deprecated routine) return this->createPV ( ctx, pAliasName ); }
Наткнулся при гуглении "Kenneth Evans, Jr.October 8, 2004".
31.03.2022: вчерашняя попытка почитать на тему "что же такое GDD (libgdd?) и как с ней обращаться?" оставила весьма гнетущее впечатление: там всё сильно завёрнуто и трудоёмко. Реализовать ВСЁ и КОРРЕКТНО -- будет весьма непростой задачей (сильно сложнее, чем в cda_d_epics.c, где модели работы CX и libCA оказались весьма похожими).
Думал-думал, печалился-печалился, страдал-страдал...
А потом появилась мысль: да сделать для начала поддержку -- только скаляров для большинства случаев этого хватит; потом же, получив опыт, проще будет и векторы поддерживать.
Итак:
cxsd_fe_epics_createPV()
добавлены проверки: только
скаляры (max_nelems==1) только INT/FLOAT/TEXT, и если INT, то размер не
больше размера int32
.
02.04.2022: проверки переехали в
fe_epics_Server::pvExistTest()
.
grep 'include.*"gdd' base-3.15.6/**/*.c*
»
Уж не он ли лёг в основу EPICS7/pvAccess? 01.04.2022: да, похоже, что оно самое: Data Access -- предок pvAccess'а. Но в явной форме по «site:epics.anl.gov "data access" pvaccess» ничего не нагуглилось.
09.07.2022: В отзывах (Table 2 на стр.2/549):
Помимо содержания хороша как сборник ссылок. В т.ч. там упоминается и работа
"A Server Level API for EPICS", на которую я ссылался в кандидатской диссертации (по метке johill-icalepcs95), но теперь по той ссылке недоступная, а доступная через web.archive.org.
17.01.2023: нашёл (спасибо Александре из библиотеки). Особо легче не стало: про всякие "beacon anomalies" там вообще ни слова, а обоснования нынешнего поведения весьма сомнительные.
Особо "порадовал" алгоритм поведения клиента при дисконнекте (раздел "3. Implementation"):
When a client initially disconnects, it polls the net- work for the missing IOC. The delay between polls is doubled in between each poll and increases exponen- tially to a fixed maximum. Soon the network load from polling becomes negligible.
Т.е., они озабачиваются вопросом загрузки сети, но почему-то не задумываются о том, "ЗАЧЕМ вообще СРАЗУ после дисконнекта слать запрос?", да ещё и часто-часто. Почему-то им не приходит в голову соображение, что если IOC только что скопытился, то через долю секунды он обратно ну никак не оживёт.
ЗЫ: и, похоже, причина всех этих выкрутасов с UDP-broadcast'ами не к месту -- именно опасения за пропускную способность (каковая в районе ~1990 действительно могла быть проблемой) (bold мой):
Each client might poll the network until the missing IOC reappears. Because of concerns about unreasonable network bandwidth consumption occur- ring when many clients are disconnected, we imple- mented a variation of this design.
17.01.2023: а, нет -- можно считать, что "beacon anomalies" там как раз [типа] обосновываются, но без использования самого этого термина. А термин объясняется, например, в презентации "Channel Access Concepts" от Andrew Johnson за 2014 (стр.8):
- A Beacon Anomaly is any change from a normal beacon interval
- . . .
- Abnormal beacon interval:
- Short: IOC has restarted
- Long: IOC was disconnected
- Anomalies cause clients to retry any outstanding search requests
И ещё есть какая-то хрень под названием "ParseCASW", описание которой имеется в "ParseCASW Users Guide" от Kenneth Evans, Jr. August 2005.
ISSUES in ACCELERATOR CONTROLS
A personal view, from a distance and in soft-focus
(Conference Summary, ICALEPCS-91)
Berend Kuiper
Именно ЭТО и является первоисточником термина "standard model" (а вовсе не статья Andy Götz'а "Experience with a standard model'91 based control system at the ESRF", на которую я ссылался в кандидатской -- как раз Гётц ссылается на Куйпера).
И именно на неё ссылаются все-все, включая 2 следующих.
...Но после прочтения выяснилось, что ТЕХНИЧЕСКОЙ КОНКРЕТИКИ там НЕТ!
И что если объединить усилия (и тут EPICS приводится как пример совместной разработки), то все выиграют от снижения костов.
Возможно, просто подразумевается FreonEndControllers и Workstations на разных сторонах СУ, обзающиеся через сеть).
Но ЯВНО это проговаривается лишь в следующей презентации от Dalesio за 1993-й.
ЗЫ: CERN Courier June 2016, pp.41-42: Berend Kuiper 1930-2016 (некролог). Оказывается, он был основателем ICALEPCS, в 1985-м.
The "standard model" consists of a local area network (Ethernet or FDDI) providing communication between front end microcomputers, connected to the accelerator, and workstations, providing the operator interface and computational support.
В обоих примерах показывается РАСШИРЕНИЕ стандартной модели: в первом некие линки OC3 и T1, во втором -- reflected memory между VME-контроллерами.
Но вовсе НЕТ НИКАКОГО обсуждения того, что будет, если сделать реально что-то по СОВСЕМ ИНОЙ архитектуре, чем стандартная модель, и чем бы это грозило.
Плюс, кроме общего набора компонентов -- FEC+network+OPI -- там ничего не говорится о том, какого "рода" данные между компонентами гоняются. Потому-то у EPICS -- каналы (более-менее унифицированные), а у Энди Гётца -- дикий винегрет разных сомнительно структурированных сущностей.
Похоже на докторскую диссертацию.
31.03.2022: кстати, надо бы в явном виде записать давно роящиеся в голове планы насчёт работы с libCAS. Итак:
...кстати, попробовав погуглить на тему "tango pvaccess":
Там обсуждается построение имён каналов ("URI") в CSS, TANGO и EPICS v4. Причём в последнем, оказывается, МОЖНО указывать адрес сервера:
EPICS v4 itself does not use URI, however its command-line tool eget has implemented the ability to accept them. ...where authority is in a form of:<scheme>:[//<authority>]</path>[?<query>][#fragment]
If authority part is omitted then discovery mechanism is used. Channel Access does not support query and fragment part.[<user>@]<host>[:<port>]
02.04.2022: переделываем API "cxsd_fe_epics_createPV()"; цели:
Итак:
cxsd_fe_epics_createPV()
переименована в
cxsd_fe_epics_pvAttach()
.
mid
" через параметр-указатель
mid_p
, а непосредственно возвращаемое значение -- статус
результата.
CXSD_FE_EPICS_SUCCESS
=0 и
CXSD_FE_EPICS_ALREADY
=+1.
cxsd_fe_epics_pvAttach()
добавлена проверка "а не
зарегистрирован ли уже монитор с таким именем", при успехе которой и
возвращается CXSD_FE_EPICS_SUCCESS
.
pvAttach()
теперь реагирует -- вместо
выполнения всех дальнейших действий по инстанциированию экземпляра
fe_epics_meat_PVrec_t
и сохранению в монитор указателя на него
она лишь выполняет возврат уже существующего.
cxsd_fe_epics_get_PV_ptr()
.
static_cast
(т.к. иначе компилятор ругался при передаче
"&PV_ptr
" на месте "void**
"), но она привела к
сообщению об ошибке, так что сделано просто "(void **)
".
Кстати, в cda_d_tango.cpp, в хронике которого этот
static_cast
упоминается, также стоит обычное C-style приведение
типа.
pvExistTest()
она переведена на
"обновлённый" вариант, где:
clientAddress
(в будущем можно
использовать для access control'а);
pPVName
переименован в pPVAliasName
.
Старый вариант всё равно числится как "deprecated".
02.04.2022: пора уже делать и чтение/запись и уведомления. В порядке движения в том направлени...
Принимаем такую парадигму разделения обязанностей между C'шной и C++'ной частями:
В связи с этим очевидное решение: проверку на годность (в т.ч. сейчас --
скаляр ли) нужно выносить в fe_epics_Server::pvExistTest()
,
ради чего ей должны отдаваться свойства канала (dtype,max_nelems,rw).
Ну, поехали:
cxsd_fe_epics_pvExistTest()
добавлены pointer-параметры
dtype_p
, max_nelems_p
, is_rw_p
-- в
них возвращаются свойства канала.
fe_epics_Server::pvExistTest()
переехали проверки
"совместимости".
cxsd_fe_epics_pvAttach()
проверки "совместимости"
убраны (кстати, они там были и не к месту -- проверять-то надо в момент
тестирования НАЛИЧИЯ, а не при приконнекчивании).
MonEvproc()
донаполнена (да, тупое копирование из
cxsd_fe_starogate.c): обработка событий RDS_CHG (складирование)
и UPDATE, которое просто вызывает...
SendMonitor()
-- вот она в будущем, возможно, станет
центром "логики" конверсии, а пока в качестве прототипа взят код из
cxsd_fe_starogate.c::SendMonitor()
, так что она:
double
-- да-да, сейчас --
ПРИНУДИТЕЛЬНО.
А затем вызывает свежевведённую...
cxsd_fe_epics_meat_update()
, являющуюся
переходником/адаптером к реальной "обновлялке", покамест получившей название
do_perform_update()
-- и вот она пока пустая.
Замечание битым текстом: от MonEvproc()
далее
передаётся триплет {dtype,nelems,data}.
10.06.2022@ИЯФ, после обеда, ~14:00, перед советом по СКИФ: снова перечитывал документацию -- мало помогло: опять появилось ощущение дикой бестолковости всей той организации, и прямо отчаяние какое-то охватило.
От отчаяния, увидев в тексте tutoriala'а слово "gddScalar", полез искать таковое, для начала locate'ом -- и ну да, нашлись в base-3.15.6/src/ca/legacy/gdd/ файлы gddScalar.h и gddScalarI.h. Полез я шариться у них внутри, и "снизошло на меня откровение":
gddScalar
есть operator=
в
вариантах для Float{64,32} и {Uint,Int}{32,16,8}...
*((gdd*)this)=d
" -- т.е., к вызову того же самого метода из
родительского класса, gdd
(и нафиг вообще было что-то
определять?).
inline gdd& gdd::operator=(aitFloat64 d) { setPrimType(aitEnumFloat64); data.Float64=d; return *this; }
Т.е., как минимум для скаляров это -- ПРОСТО СКЛАДИРОВАНИЕ ДАННОГО и ЗАПОМИНАНИЕ ТИПА!!!
@вечер: сама реализация
gdd::setPrimType
в gdd.cc не так и проста, но для
скаляра по факту именно что проста.
Т.е., по факту там всё просто -- обычная парадигма "сырые данные плюс дескриптор-типа", но упакованная в ОЧЕНЬ сложную объектную реализацию ("GDD implements simple things in a very complex manner").
На всякий случай даже попробовал: написал в
fe_epics_PV::read()
строчку prototype=123.456
-- и
оно нормально откомпилировалось!!!
Откуда вырисовывается такой путь/сценарий:
read()
: берём текущее значение (с опциональным выполнением
RD-конверсии) и присваиваем его "prototype=ЗНАЧЕНИЕ
"
соответствующим типом.
Тут нюанс насчёт "типа": если RD-конверсия требуется (проверяем в момент
вызова), то тип ВСЕГДА делаем double
, иначе ставим исходный.
write()
: узнаём тип переданного значения, при помощи
primitiveType()
, после чего вычитываем само скалярное значение
в соответствующий тип, опционально выполняем RD-конверсию, после чего
сбагриваем результат CxsdHwDoIO()
.
@получасом позже, в 613-й: да, есть метод
setTimeStamp()
, ему можно передавать struct timespec
*
.
Но это, видимо, надо всё же документацию читать.
Кстати, провёл опрос "потенциальных клиентов" на тему, что именно им будет нужно -- скаляры или векторы:
Так что да -- сначала разберёмся со скалярами, а потом сразу же надо будет делать и векторности.
10.06.2022@вечер, ~22:30:
чтоб знать уж совсем точно, выполнена ещё проверка: в
fe_epics_PV::read()
было добавлено "return
S_casApp_success
". И да -- caget, натравленный на соответствующее
имя, показал "123.4560".
10.06.2022: соображение общего характера "о природе данных", осознанное при чтении исходников GDD:
Многомерные массивы, с точки зрения представления/реализации -- это просто число элементов (nelems) должно быть не скаляром, а тоже вектором, по числу измерений.
11.06.2022@утро: к вопросу "а если всё-таки захочется уметь получать СЫРЫЕ значения, в int32 (т.е., именно микровольты), а не пересчитанные в double?":
moninfo_t
поле "kind", в котором и сохранять,
что это за подвид.
А иначе и EVMASK_RDSCHG не заказывать, а если и не ==0 и не ==1, то и EVMASK_UPDATE не заказывать.
cxsd_fe_epics_meat_update()
получает уже триплет
(dtype,nelems,data).
Ну вот в cxsd_fe_epics.c и будет вся эта специфика, а meat пусть получает уже готовые данные.
...а ещё ему надо будет также и timestamp передавать.
GetChanInfo()
: и довольно очевидно, что для
функционирования fe_epics_PV::read()
нужен "accessor",
добывающий ей тот же квадруплет данных, что даётся update()'у --
(dtype,nelems,data,timestamp).
Пусть он САМ вызывает тот же самый "accessor".
11.06.2022: делаем.
kind
.
MON_KIND_VAL
=0 и MON_KIND_RAW
=1.
GetChanInfo()
:
kind
такой-то вместо
KIND_VAL, а затем от имени этот суффикс откусывается (точнее, обрезанный
вариант складируется в локальную namebuf[]
-- чтоб не трогать
исходник).
CxsdHwResolveChan()
, который
если успешен, то...
kind
.
cxsd_fe_epics_pvExistTest()
с
cxsd_fe_epics_pvAttach()
переведены на неё.
cxsd_fe_epics_pvAttach()
также в
зависимости от kind
заказываются разные маски событий: при
!=MON_KIND_VAL убирается EVMASK_RDSCHG, а при
!==MON_KIND_VAL&&!=MON_KIND_RAW также и EVMASK_UPDATE.
11.06.2022: зачинаем тот "accessor":
cxsd_fe_epics_get_data()
.
...что именно мы будем делать с rflags -- пока не вполне ясно, но для полноты они необходимы.
12.06.2022: реализуем.
cxsd_fe_epics_pvExistTest()
в
определение *is_rw_p
добавлено дополнительное условие, что
должно быть KIND_VAL или KIND_RAW.
Правда, результат сейчас реально никуда не используется, но вообще-то в
fe_epics_PV::write()
должно на не-rw-каналы (к которым
относятся и все атрибуты!) возвращаться S_casApp_noSupport
.
cxsd_fe_epics_get_data()
должна ВОЗВРАЩАТЬ
указатель на данные, то физически эти данные должны переживать вызов;
соответственно, нельзя -- даже сейчас, для скаляра -- держать результат
RD-конверсии в локальной переменной. Посему:
moninfo_t
добавлено поле CxAnyVal_t valbuf
-- аналогично cda_core.
14.05.2023: оно перетащено повыше, в начало блока "всё про
текущее значение", ПЕРЕД всеми current_*
; смысл -- чтоб более
объёмистое и с alignment-требованием "8 байт" (как у указателей) было в
начале. Сделано во всех 3 местах, использующих "архитектуру moninfo с
хранением последних значений" (cxsd_fe_epics.c,
cxsd_sfi.c, cda_d_tango.cpp).
...при добавлении поддержки векторов надо будет также и
current_val
завести, хранящий указатель на аллокированный
буфер.
cxsd_fe_epics_get_data()
:
_p
") данными из
chn_p
.
phys_count != 0
, то выполняем конверсию:
double v
;
fe_epics_PV
добавлено поле mid
-- оно
жизненно необходимо, а раньше был скорее "макет", который "и так сойдёт".
...вот только для его инициализации пришлось немножко повыкручиваться:
оно ведь в классе, который напрямую НЕ инстанциируется, а лишь как поле в
fe_epics_meat_PVrec_t
. Вот и пришлось передавать через 2
конструктора (да, синтаксис в C++ не самый очевидный, но первое же
нагугленное со Stackoverflow
"What's the reason for initializing fields inside class?"
сразу дало пример), причём для неперекрытия имён пришлось параметр обзывать
"my_mid
".
fe_epics_PV::read()
тривиально: вызываем
cxsd_fe_epics_get_data()
для получения текущей информации,
затем присваиваем prototype
нужный тип в зависимости от
dtype
и возвращаем S_casApp_success
.
@~13:00, пешком с продуктами из Быстронома:
moninfo_t
, так что нехай в любой момент может туда заглядывать
-- из-за однопоточности там всегда будет консистентная информация.
Проверяем -- хрень какая-то:
При переходе на double вроде число стало показываться верно.
13.06.2022: неа, всё было проще: дело не в типе, а в том, что отсутствовало мониторирование и первое чтение всегда вычитывало 0. А осмысленное значение вычитывается, если предварительно на этот канал натравить cdaclient.
После добавление мониторирования -- RequestSubscription()
в
начале каждого цикла -- лучше, увы, не стало: первое-то чтение всё равно
даёт 0, а второе всегда приводит к SIGSEGV...
...кстати, а не дёргать ли CxsdHwDoIO()
из
read()
(через адаптер в cxsd_fe_epics.c)? Для
реальной аппаратуры это всё равно результата не даст (мгновенного ответа не
будет), а для "локальной"/"виртуальной" придётся ещё и флажок взводить, чтоб
по update() лишнего возврата не сделать...
13.06.2022@~16:40, около стадиона НГУ по пути на Морской-46: а если
cxsd_fe_epics_pvAttach()
(тогда не понадобится никакой "адаптер"), причём...
Решение выглядит очень простым и элегантным.
13.06.2022@~19:20: сделано -- да, всё ровно так и получилось.
07.05.2023: только перед заказом чтения отсутствовала проверка, что MON_KIND_VAL или MON_KIND_RAW -- добавлена. (Пока некритично, но в будущем аукнулось бы.)
Channel connect timed out: 'asdf.0' not found.
@~16:30, на тропинке в
лесочке между Терешковой-8 и Ильича-11 по дороге на Морской-46: а
может, дело в том, что каждый вызов
fileDescriptorManager.process(0)
обрабатывает НЕ БОЛЕЕ 1
входящего пакета (а в эти моменты как раз прут, судя по отладочной печати,
запросы на какие-то чужие каналы)?
Так-то никто не запрещает вызывать обработку не одноразово, а пачками -- как "repcount" в разных наших библиотеках, вроде fdiolib.c и cda_d_insrv.c.
Но надо бы сначала проверить гипотезу про "НЕ БОЛЕЕ 1 входящего пакета" -- отладочной печатью времени.
@вечер: проверил,
отладочной печатью strcurtime_msc()
--
pvExistTest()
на каждый
вызов поллинга... Во дебилы-то, а....
process()
-- он void
, т.е.,
НЕЛЬЗЯ проверить, сделал ли он что-то (и если "да", то повторить по
repcount'у).
13.06.2022: добавил цикл по repcount
в 30 итераций -- да, стало обрабатывать пакеты сразу пачками и проблема "not
found" вроде исчезла (но ясно, что в таком варианте это всё чисто вопрос
статистики/вероятности).
#0 0x00007ffff6c18181 in casPVI::bestExternalType (this=this@entry=0x6c4ba0) at ../../../../../../src/ca/legacy/pcas/generic/casPVI.cc:516 #1 0x00007ffff6c181de in casPVI::bestDBRType (this=0x6c4ba0, dbrType=@0x7fffffffddf0: 0) at ../../../../../../src/ca/legacy/pcas/generic/casPVI.cc:437 #2 0x00007ffff6c15c63 in casStrmClient::createChanResponse (this=0x6c2360, guard=..., ctxIn=..., pvar=...) at ../../../../../../src/ca/legacy/pcas/generic/casStrmClient.cc:1698
13.06.2022: плюс однажды ещё и внутренняя ошибка при запуске caget'а:
CA.Client.Exception............................................... Fatal: "Channel Access Internal Failure" Context: "host=x10sae.inp.nsk.su:5064 ctx=C++ exception "epicsMutex::invalidMutex()" in server - disconnected client" Current Time: Mon Jun 13 2022 14:21:53.040189300 zsh: abort (core dumped) /home/user/compile/base-3.15.6/bin/linux-x86_64/camonitor asdf.0
13.06.2022: и отдача timestamp'а сделана -- ну тут всё просто и даже тривиально.
14.06.2022: продолжаем:
fe_epics_PV::write()
:
value
узнаёт егойный тип с помощью
метода primitiveType()
,
CxAnyVal_t
), с уставлением на него
указателя data
и прописыванием соответствующего типа в
dtype
,
cxsd_fe_epics_do_write()
-- тривиальный переходник к
CxsdHwDoIO(,DRVA_WRITE,)
, транслирующий mid
в
mp->gcid
, а собственно (dtype,nelems,data) передающий как
есть.
Но фиг -- caput присылает тип=11, aitString, который нам, естественно, никак не подходит и отвергается.
Так что надо делать уведомление libCAS/libGDD о реальном типе, иначе фиг.
cxsd_fe_epics_pvExistTest()
обучена при выборе dtype
учитывать наличие {R,D} -- для MON_TYPE_VAL она в таких случаях форсит тип
DOUBLE (если только и так не SINGLE).
Некоторые мысли и соображения на будущее:
См. casdef.h раздел комментариев "Deletion
Responsibility" в определении casPV
.
virtual aitEnum bestExternalType ()
-- он должен возвращать что-то вроде "dtype2aitEnum"?
15.06.2022: немного соображений о том, "за что я ненавижу libCAS и GDD":
Главная проблема -- там НЕТ внятного описания "надо делать то-то и то-то"
(в т.ч. пропущены ключевые аспекты, вроде деструктирования
casPV
и отдачи libCAS'у информации о PV), а вместо этого МОРЕ
"информации" об "атрибутах" и "таблицах конверторов", что вообще-то НЕ
относится к СУТИ задачи, а более полезно для простейшего примера.
Но вынуждены они это были сделать потому, что...
Хотя очевидно правильным вариантом было бы ни в коем случае не выпускать эту нечисть за пределы libCAS'а -- *ОН* должен был бы воспринимать эти извращения, а от своего клиента просить лишь данные (ну, или, ладно -- отдельно/опционально атрибуты).
Но нет, правила инкапсуляции категорически нарушены.
Но в результате он и ту глобальную задачу не решил, и текущую/реальную -- ПРОСТЕЙШИЕ взаимодействия -- выполняет плохо.
Итого: изначально негодный дизайн, плюс слабополезная документация.
15.06.2022: продолжаем.
bestExternalType()
.
Ради этого перекурочена идеология работы с "атрибутами" -- dtype, max_nelems, is_rw:
cxsd_fe_epics_pvExistTest()
они убраны (как и из
cxsd_fe_epics_pvAttach()
, куда только сегодня утром успели
заселиться),
cxsd_fe_epics_get_info()
, ...
fe_epics_Server::pvAttach()
, а результаты
от неё сбагриваются...
fe_epics_PV()
, складирующему в
свежедобавленные поля,
bestExternalType()
на основе dtype
и
возвращает что-то осмысленное.
Также в процессе этого проверка совместимости (на типы и что только
скаляры) переехала из fe_epics_Server::pvExistTest()
в
cxsd_fe_epics_pvAttach()
.
16.06.2022: роем далее...
void fe_epics_PV::destroy () {/* Implicitly DO NOTHING */}
Да, помогло -- с ним падения прекратились.
bestExternalType()
так и не вызывается; но ведь
GDB'шный "bt" показывал, что SIGSEGV происходит в
casPVI::bestExternalType()
в "casPVI.c:516", а строка
516 там -- это средняя во фрагменте
if ( this->pPV ) { return this->pPV->bestExternalType (); }
Откуда напрашивается мысль: раз ЧТО-ТО с этим именем всё же дёргается, то, может, просто "что-то НЕ ТО"? Т.е., сигнатура по параметрам не совпадает?
Но вроде смотрел -- да нет, всё совпадает: список параметров пустой...
postEvent()
.
Попробовал "с нахрапу" -- фиг: там нужен объект типа gdd
(для передачи значения), я хотел было просто завести поле такого типа в
fe_epics_PV
, но хрен то там: НЕЛЬЗЯ завести такой объект, т.к.
егойный деструктор ~gdd()
является protected
, что,
видимо, сразу запрещает инстанциирование.
Несколькими минутами позже: ага, именно так -- "c++ - Protected vs Private Destructor" на Stackoverflow (со ссылкой на CoderSource.net):
| If the constructor/destructor is declared as private, then the class cannot be instantiated.This is true, however it can be instantiated from another method in the class. Similarly, if the destructor is private, then the object can only be deleted from inside the class as well. Also, it prevents the class from being inherited (or at least, prevent the inherited class from being instantiated/destroyed at all).
Чёрт возьми, вот опять -- проблемы совершенно на ровном месте!!! Ну
нахрена придумали эти уродские контейнеры-GDD, при том, что реально
передавать нужно просто триплет (тип,количество,указатель_на_данные) -- как
это и есть в libCA, в ca_array_put()
...
@днём, ездя по делам: А если отнаследовать?
@вечер: Попробовал -- да, можно (хотя и с тучей сопутствующих проблем):
upd_gdd
сделано типом
fe_epics_gdd
,
gdd
*gp
.
fe_epics_gdd *gp
" то при обращении
вида
"*gp = *((float32*)data)
"
ругается
cxsd_fe_epics_meat.cpp:19:7: note: no known conversion for argument 1 from 'float64 {aka double}' to 'const fe_epics_gdd&' cxsd_fe_epics_meat.cpp:254:34: error: no match for 'operator=' (operand types are 'fe_epics_gdd' and 'float32 {aka float}') case CXDTYPE_SINGLE: *gp = *((float32*)data); break; ^
Причём ругается НЕ на всё -- на строчку CXDTYPE_DOUBLE/float64 почему-то нет.
Чем её отнаследованные от gdd
операторы не устраивают -- хбз.
17.06.2022: по "c++ operator= inheritance" нагуглилось "operator= and functions that are not inherited in C++?" на Stackoverflow:
The assignment operator is technically inherited; however, it is always hidden by an explicitly or implicitly defined assignment operator for the derived class (see comments below).(13.5.3 Assignment) An assignment operator shall be implemented by a non-static member function with exactly one parameter. Because a copy assignment operator operator= is implicitly declared for a a class if not declared by the user, a base class assignment operator is always hidden by the copy assignment operator of the derived class.
И поле сделано public
-- иначе как бы к нему
do_perform_update()
доступалась...
17.06.2022: видимо, оно должно б быть МЕТОДОМ у
fe_epics_PV
, это явно правильнее.
valueEventMask()
у СЕРВЕРА -- объекта caServer
.
Ну и как, спрашивается, его получать в casPV
?!
Пришлось делать
static casEventMask update_mask
и вычитывать его в конструкторе
fe_epics_Server::fe_epics_Server()
.
Но обновления почему-то не ходят :D
17.06.2022: а может, обязательна реализация метода
interestRegister()
? Например, при его отсутствии считает
подписывание неработающим и игнорирует обновления?
17.06.2022: сделал, но нет -- фиг...
17.06.2022: разбираемся дальше...
Проверено отладочной печатью, какое именно значение возвращает
valueEventMask()
: да, то же самое 1, что и указанное в
комментарии к его определению
"#define DBE_VALUE (1<<0)
"
(caeventmask.h).
interestRegister()
, молча возвращающий
S_casApp_success
-- фиг, не помогло: метод, судя по отладочной
печати, вызывается, но эффекта ноль.
@вечер: убрано -- не помешало.
upd_gdd.reference()
(из PV'шного
конструктора) -- увидено упоминание в Tutorial'е на стр.37.
Тоже не помогло...
@вечер: тоже убрано -- тоже не помешало.
cxsd_fe_epics_pvAttach()
НЕ делалось
fe_epics_periodics_used++
, вот никаких обновлений и не
происходило -- попросту чтение не заказывалось.
Что насторожило: а почему нет чтений?
...и как только Алексей Медведев на сварке работает -- уже ТРИ года!!! 01.11.2023: ещё тогда у него спросил: ответ в том, что у него всегда параллельно скрин запущен, который и обеспечивает запрос обновлений (вроде ещё тогда сюда записывал, но что-то не вижу; получасом позже: а-а-а, не "сюда", а в его раздел, от 17-06-2022 за 20-06-2022).
filename="../../../../../../src/ca/legacy/pcas/generic/casStrmClient.cc" line number=941 No conversion between src & dest types no conversion between event app type=0 and DBR type=14 Element count=1
Ну и чё ж ей надо? Всё-таки эта долбаная таблица конвертеров?
interestRegister()
и вызов upd_gdd.reference()
--
поведение не изменилось.
Значит, они и не обязательны.
21.06.2022: возимся дальше.
bestExternalType()
, должно же там быть какое-то несоответствие.
...но и в casPV public, так что дело не в этом.
Но всё равно угуглился на темы "c++ virtual protected ancestor public" и (подсмотрев вариант формулировки среди результатов) "c++ virtual protected ancestor public". Почитал конкретно "If a private virtual function is overridden as a public function in the derived class, what are the problems?" -- мало что понял, надо больше времени потратить :)
virtual aitEnum bestExternalType () const;
а у меня же --
virtual aitEnum bestExternalType ();
т.е., без "const
".
When you add the const keyword to a method the this pointer will essentially become a pointer to const object, and you cannot therefore change any member data. (Unless you use mutable, more on that later).
The const keyword is part of the functions signature which means that you can implement two similar methods, one which is called when the object is const, and one that isn't.
Т.е., да -- разница есть, и причина существования такой возможности понятна.
Но совершенно непонятно -- а в libCAS-то этот "const" нахрена?!?!?!
cxsd_fe_epics_meat.cpp:117:9: error: prototype for 'aitEnum fe_epics_PV::bestExternalType()' does not match any in class 'fe_epics_PV' aitEnum fe_epics_PV::bestExternalType() ^ cxsd_fe_epics_meat.cpp:38:21: error: candidate is: virtual aitEnum fe_epics_PV::bestExternalType() const virtual aitEnum bestExternalType () const; ^
Что подтверждает критичность "const"'а.
bestExternalType()
вызывается!!!
Там косяк нашёлся сразу: в cxsd_fe_epics_do_write()
в вызове
CxsdHwDoIO()
стояло "mp->gcid
" вместо
&(mp->gcid)
-- в результате этой идиотской почти
опечатки передавался указатель со значением 0x68 (=104); немудрено, что
падало.
После исправления -- работает!!!
OK, теперь краткий план того, что предстоит сделать в ближайшее время для улучшения работы:
fe_epics_meat_PVrec_t
,
храня в "PV_ptr
" указатель непосредственно на саму "PV".
casChannel
-- ради access
control'а и ради readonly.
Приступаем помаленьку -- сначала к простому :)
S_casApp_noSupport
.
Заодно добавлена "проверка" результата
cxsd_fe_epics_do_write()
-- реально от
CxsdHwDoIO()
-- при !=0 возвращается та же ошибка.
fe_epics_PV::update()
, в который
скопировано содержимое нынешнего do_perform_update()
.
do_perform_update()
переделано на
просто вызов метода update()
.
Ну -- с практической точки зрения ничего не изменилось: осталась та же ошибка "No conversion". Но так и должно было быть.
mid
и PV_ptr
, у
do_perform_update()
и cxsd_fe_epics_meat_update()
.
SendMonitor()
убрано
практически ВСЁ содержимое, а оставлен только вызов
cxsd_fe_epics_meat_update()
.
Раньше там была конверсия -- дублировавшая нынешнее содержимое
cxsd_fe_epics_get_data()
, но в более древнем варианте, ВСЕГДА
генерившем DOUBLE.
maxDimension()
и maxBound()
. Первый отдаёт число
измерений -- 0 для скаляра, 1 для вектора и т.д. (но EPICS3 поддерживает не
выше 1), а второй размер в указанном измерении.
Реализовал -- результат обескураживающий: векторные каналы попросту не находятся, как минимум по cainfo.
@вечер: а-а-а, дошло -- это же
cxsd_fe_epics_pvExistTest()
сейчас дозволяет только скаляры!!!
casChannel
и у него методы
readAccess()
и writeAccess()
.
Т.е., там "читабельность" и "писабельность" существуют только применителько к конкретному соединению с клиентом, а свойства "readonly" для PV не предусмотрено в принципе...
И-ди-о-тизм...
22.06.2022@душ: лучше сейчас
заняться векторностью -- там хоть примерно ясно, что делать: со стороны
.c -- аллокирование буферов и векторная конверсия; со стороны
_meat.c -- работа с указателями, а не с помощью "=
".
22.06.2022: да, занимаемся векторностью.
value_is_present
",
cxsd_fe_epics_get_data()
после преобразования,
SendMonitor()
его сбрасывать.
cxsd_fe_epics_do_write()
надо сбрасывать.
Ну и фиг с ней -- добавим уж сразу векторную.
moninfo_t
добавлены поля
current_val
, current_nelems
и
current_dtype
; плюс val_is_present
.
27.06.2022: добавлены current_rflags
и
current_timestamp
.
22.06.2022@около-стадиона-НГУ, по дороге на М-46, ~17:30:
load-lib
.
Но если будут соединения, то получается ДВА параметра для уникальной идентификации монитора: кроме собственно mid'а нужен ещё и cid, т.к. мониторы-то в каждом соединении нумеруются с 1.
mid
'ов, а уж в каждом из них backreference на его cid.
23.06.2022: пилим далее...
RlsMonSlot()
добавлено освобождение
current_val
.
Заодно он дооснащён и полным комплектом "подчистки": разрегистрацией
evproc'а и троганьем periodics_*
.
cxsd_fe_epics_pvAttach()
добавлено аллокирование
буфера при превышении объёмом канала того места, что доступно в
valbuf
.
moninfo_t
добавить ещё
что-то вроде своего экземпляра "max_nelems" -- чтобы мочь контролировать
размер, дабы не выходить за пределы буфера.
cda_dat_p_update_dataset()
и прикидывая, как бы это
поэлегантнее и пообщее скопировать сюда...
Покамест пришёл лишь к очень общей мысли, что надо сделать 2 функции конверсии: из инженерных в операторские и обратно, и эти функции вызывать из нужных мест.
Но ни какой именно набор параметров передавать этим функциям (и, соответственно, какое сделать разделение обязанностей по принятию решений между ними и их вызывальщиками), ни даже как их назвать -- пока так и не определился.
23.06.2022: чуть в сторону -- касательно отладки через GDB.
Причина -- заметил, что иногда сервер падает по SIGSEGV. Стал пытаться разбираться, запустил с "-D" (don't trap signals), получил core-файл, но... Как посмотреть значения переменных в ПРЕДЫДУЩИХ функциях по call-stack'у backtrace'а?
frame N
", где "N" -- номер в
backtrace, выдаваемый через "#".
Например, предыдущая функция всегда "#2", поэтому надо написать
"frame 2
".
Обнаружил рецепт посредством гугления по "gdb print bt caller", в "Inspecting caller frames with gdb" на Stackoverflow за 30-11-2017.
info locals
" -- выдаёт ВСЕ
локальные переменные.
bt full
" -- тогда backtrace выдаёт и значения
локальных переменных при каждом фрейме.
Но собственно изначальная проблема решилась проще и оказалась из разряда "вскрытие показало, что пациент умер от вскрытия": причина SIGSEGV'ов было в том, что я делал "make install" модулю, так что он попадал в ту директорию, откуда был загружен запущенным сервером, и эта "модификация" (реально замена файла на идентичный) почему-то его гробила...
А убедился я в этом, сравнив timestamp'ы core-файлов с моментам запуска
"make install" в истории zsh. А выдачи надлежащих данных я добился, долго
роясь в zsh'ной документации, где оказалось несколько неочевидно -- builtin
"fc
" вместо вроде бы положенного "history
".
fc -li -1000
fc
" -- собственно основная команда (из-за
двухбуквенности найти её на man-странице zshall очень непросто; быстрее
всего поиском по "8601").
Она делает с историей ВСЁ -- вплоть до модификации и даже редактирования.
-l
" -- list, режим выдачи.
-i
" -- "prints full time-date stamps in ISO8601
`yyyy-mm-dd hh:mm' format".
-1000
" -- номер начальной строки для выдачи (1000-я с
конца). Вот тут, возможно, надо как-то лучше...
24.06.2022: а, во -- "fc -li 1
". Последняя
строка выдачи при этом всё равно выбирается последняя, так что будет полный
листинг.
24.06.2022: пришёл-таки к решению, как обустроить конвертеры.
cxdtype_t src_dtype, int src_nelems, void *src_data, int phys_count, double *phys_rds, cxdtype_t dst_dtype, int dst_max_nelems, void *dst_data
imm_phys_rds
или
alc_phys_rds
),
dst_dtype
выбрать -- то, что в начальной
версии выполняется по принципу "если phys_count==0, то оставить исходный
тип, а иначе DOUBLE, если только и так не SINGLE".
valbuf
или
current_val
.
goto NEXT_TO_UPDATE
").
Встаёт вопрос: а как они будут возвращать результат ограничения?
Ответ элементарен: в возвращаемом РЕЗУЛЬТАТЕ -- он и так
int
пусть и возвращают. Т.е., <0 -- ошибка, >=0 --
nelems (тем более, что 0 -- легитимное значение).
eng2opr()
чтение и opr2eng()
запись.
Похоже, содержимое у них будет почти идентичным, с различием только в "направлении {R,D}-конверсии". Прямо хоть макросами делай...
Именно так и ведём разделение кода из cda_dat_p_update_dataset()
'а
на "конвертерную" и "клиентскую" части.
25.04.2023: авотфиг -- придётся-таки этим заниматься, см. отдельный пункт за сегодня ниже.
27.06.2022: возился несколько дней, продолжаем записывать.
eng2opr()
в первом приближении "доделана" -- сокращена из
cda'шного варианта.
src_nelems
.
return -1
, а при конкретно
несовместимости -- т.е., при невозможности произвести конверсию --
return -2
.
...а надо бы заформализовать возвращаемые коды, чтоб в конечном итоге можно б было в _meat.cpp возвращать более конкретные ошибки.
cxsd_fe_epics_get_data()
начиняем подготовкой
параметров, в т.ч. выбором dtype.
opr2eng()
сделана -- копированием из
eng2opr()
и заменой блока {R,D}-конверсии на таковой из
cda'шного DoStoreWithConv()
.
(Отличие там в собственно цикле другого направления и удалении начального
rdp += phys_count * 2
.)
30.06.2022: вот только при этой переделке-копировании накосячил, так что корректно конвертировался только первый элемент вектора.
28.06.2022@утро-просыпаясь: а ответ простой -- в качестве "dst_dtype" ВСЕГДА отдавать каналов dtype; ведь на него буфер и рассчитан, так что оно ВСЕГДА влезет. (Но да, с CXDTYPE_UNKNOWN-каналами это не проканает...)
28.06.2022@утро-перед-завтраком: а почему такая сконцентрированность на конверсии, складировании данных и т.п.?! А как же zero-copy?
Ведь значительная часть данных -- в первую очередь
noop-почтовые-ящики -- будут отдаваться один-в-один, безо всякой конверсии.
Ну так и надо прямо указатель на данные и использовать: при чтении --
возвращать сразу chn_p->current_val
, а при записи -- прямо
переданное cxsd_fe_epics_do_write()
'у значение
data
и сбагривать в CxsdHwDoIO()
!
28.06.2022: первым делом организуем zero-copy:
mp->ПАРАМЕТРЫ
, а должны быть в локальных
переменных.
mp
в те же локальные переменные.
И "подготовка" его -- т.е., когда val_is_present==0 -- эти значения для кжша готовит, в результате чего ещё и в локальных переменных оказываются как раз нужные значения.
chn_p
.
Теперь запись в cxsd_fe_epics_do_write()
:
CxsdHwDoIO()
.
Ну а далее пора заниматься "обвязкой" -- аллокированием буфера (при надобности), а далее уже векторной работой в _meat.c.
Так-то данные записывает метод adjust()
-- ему даётся тип и
указатель на данные; но вот как указать МАССИВ?!
(чуть позже) вроде есть там методы --
Где-то очень глубоко в недрах libCAS... Вот какого чёрта в этой
монструозине нет вменяемых проверок?!?!?!
С другой стороны -- это же чтение? Ну так...
А причина в том, что скаляры они хранят в том же union'е, что и указатель
на данные. Причём скаляром считается и результат записи от
" ...да-да, вообще-то эту работу должна делать GDD -- отдавать указатель на данные
независимо от варианта их расположения; но почему-то они перекладывают её на
меня, юзера их библиотеки.
01.07.2022: вообще-то там есть ещё метод
04.07.2022: да, переведено на использование
Для чего, видимо, придётся порыться в тамошнем коде -- посмотреть, как
именно оно указывает, что же лежит в union'е 01.07.2022: ну порылся; по коду
А надо бы проверить и с конверсией...
setDimension()
/dimension()
и
setBound(dim_to_set,first,count)
/getBound()
.
dtype2aitEnum()
, чтобы упростить
трансляцию. Для неизвестных вариантов отдаёт aitEnumInvalid
.
fe_epics_PV::update()
с передачей данных следующего вида:
value_type = dtype2aitEnum(dtype);
if (value_type == aitEnumInvalid) return;
gp->adjust(NULL, data, value_type);
cxsd_fe_epics_pvExistTest()
и
cxsd_fe_epics_pvAttach()
закомментированы проверки на
скалярность, чтобы теперь и вектора разрешались.
fe_epics_PV::read()
аналогичную
update()
'у модификацию.
aitEnum2dtype()
.
fe_epics_PV::write()
.
caput -a asdf.1 2 789 543
" выполнила
запись как надо!
caput asdf.1 789
" --
SIGSEGV...
dataPointer()
возвращает не указатель, а
тупо значение -- в данном случае было 0x315, что и ==789...
caput -a asdf.1 1 789
". Реально даунизм -- меганавороченная
библиотека GDD, а такой очевидный случай никак не обрабатывает.
dataPointer()
вызывать что-то типа dataAddress()
.
if (nelems == 0)
data = NULL;
else if (nelems == 1)
data = (void *)(value.dataAddress());
else
data = (void *)(value.dataPointer());
dataVoid()
следующего вида -- вроде он как раз и должен
исполнять примерно это (и там ещё отдельная проверка на FixedString, которая просто
char fixed_string[AIT_FIXED_STRING_SIZE]
, с предшествующим
#define AIT_FIXED_STRING_SIZE 40
):
inline const void* gdd::dataVoid(void) const
{
return (dimension()||primitiveType()==aitEnumFixedString)?
dataPointer():dataAddress();
}
dataVoid()
.
data
.
gdd::dataVoid(void)
отлично видно, "как именно оно указывает"
-- да НИКАК! Чисто догадками -- если НЕ-скаляр или FixedString, то
указатель, а иначе считаем, что просто значение. 05.07.2022: причём
для типа String (не-Fixed!) понятие "значение" -- это не строковые данные, а
сам объект aitString
.
29.06.2022: немножко с конвертерами типов:
fe_epics_PV::bestExternalType()
переведена на
dtype2aitEnum()
.
CXDTYPE_TEXT
<->aitEnumString
; тут
небесспорно -- второй альтернативой является
aitEnumFixedString
.
Теперь дальнейшие проверки:
cpoint asdf.z asdf.1 1000
"
-- т.е., ДОПОЛНИТЕЛЬНЫЙ "канал", а не коэффициент на основном.
Upd: только СКАЛЯРЫ. Читается-то верно, а вот пишется -- как-то странно, только ПЕРВОЕ значение.
Хотя вроде бы bzero(mp->current_val, csize)
исполняется.
cxsd_fe_epics_get_info()
чуток
своя логика определения dtype ("при наличии конверсии считать всё за DOUBLE,
если только не SINGLE")? Может, она как-то отличается от прочих мест?
eng2opr()
добивать отсутствующие данные --
между nelems и max_nelems -- нулями; чтобы это делать ОПЦИОНАЛЬНО, надо
добавить параметр "bzero_unset" (а в "общем cxsd_fe_api" клиенты смогут
указывать его "необходимость" при создании соединения).
@дома, ~09:00: но именно только в неё -- в
opr2eng()
оно нафиг не нужно, т.к. CX-сервер и так за пределы
nelems не лазит.
@вечер, дорога домой, выход из леса перед Пирогова-26, ~18:40: однако это будут дичайшие накладные расходы. Например, если в канале осциллограммы, рассчитанном на 100000 элементов, будет лишь 1000 измерений, то оставшиеся 99000 будут постоянно нулиться почём зря.
maxBound()
.
Надо бы всё же указывать.
30.06.2022: разбираемся с косяками при конверсии.
opr2eng()
было напихано
печатей значений v
ДО и ПОСЛЕ конверсии.
И эта диагностика показала, что на вход поступают правильные данные, но ПЕРВОЕ число конвертируется как надо, а дальше получаются сплошные 0.000 (которые caget'ом и отображаются как очень мелкий "мусор" вроде "4.16183e-317 7.70523e-313 3.06765e-320".
Что сразу указало конкретное место проблемы -- код конверсии.
rdp
:
rdp = phys_rds
,
rdp += (n-1)*2
-- для перехода на КОНЕЦ списка (т.к. конверсия в обратном направлении),
rdp -= 2
.
В результате после обработки каждого элемента вектора указатель
rdp
сползал на 1 дуплет ниже -- за пределы массива, где лежит
предшествующее содержимое moninfo_t
, каковое в качестве double
выглядело как мусор.
Причиной косяка стало то, что при копировании из
DoStoreWithConv()
была изменена парадигма работы: там-то оно
инициализировалось перед КАЖДЫМ проходом (для каждого элемента массива), а
тут сделано "один раз инициализируем, а потом просто гоняем туда-обратно".
rdp = phys_rds + (n-1)*2
;
rdp += 2
.
rdp += n*2
,
rdp -= 2
.
делать не после шага конверсии, а ПЕРЕД ним.
Т.е., полноценный "обратный" по отношению к "PTR++" код -- "--PTR".
Но выбран был 3-й, как наиболее оптимальный (и элегантный/"правильный", кстати).
DoStoreWithConv()
стоило бы сделать
так же, но покамест не будем -- "не чини то, что работает".
opr2eng()
придёт 3, и мониторирующему cdaclient'у тоже 3.
А вот сам caput в качестве "New :" напечатает уже 4 числа, последним 0.
Теперь возвращаемся к проверке работы разных типов.
current_val
аллокирован на меньший объём, чем будет генериться при конверсии.
cxsd_fe_epics_pvAttach()
: там
размер буфера определяется как
-- упс!!!csize = sizeof_cxdtype(cxsd_hw_channels[gcid].dtype) * cxsd_hw_channels[gcid].max_nelems;
В частности, если тип подстилающего канала INT8, а R есть, то "подразумеваемый" тип будет DOUBLE, который в 8 раз объёмнее...
Да даже в самом обычном случае с INT32-каналами разница будет в 2 раза.
Делаем.
По факту-то это действительно так, но лучше бы на это не полагаться и выбирать объём по МАКСИМУМУ из нативного и производного типов.
ext_dtype
-- "EXTernal".
cxsd_fe_epics_pvAttach()
её определение.
ext_dtype
коснулись:
cxsd_fe_epics_get_info()
-- теперь возвращает его
безо всяких условий.
cxsd_fe_epics_get_data()
-- на него заменено условное
определение.
IsSuitableForExport()
.
Т.е., сначала-то лишние заняты нулями.
Но вот если записать "123 234 345 456", а потом "987 876", то будет считаться "987 876 345 456" (а вот cdaclient покажет правильные 2 элемента).
04.07.2022: подразобрался -- да, просто НЕ МОГЛО НЕ ПАДАТЬ. Нужно совсем иначе со строками обращаться.
01.07.2022:
dataVoid()
, вариант, когда данные
установлены adjust()
'ом при помощи указателя, может некатить...
04.07.2022: проверил. Ну да -- вместо значения ДАННЫХ оно берёт значение УКАЗАТЕЛЯ на данные...
01.07.2022@совет-по-СКИФ в пятницу, ~15:30: некоторое количество мыслей по реальному взаимодействию с EPICS.
И как это в сумме работает? Что, если попробовать ДВАЖДЫ обратиться к одному имени в 2 разных регистрах -- нормально ли отработает или глюканёт?
Благо, готовый буфер для проведения такой замены есть --
namebuf[]
.
Только надо правильно сочетать эту замену с попыткой интерпретации суффиксов как специальных имён каналов.
Они явно разбиваются на "классы", где поведение абсолютно одинаковое, но различие лишь в порядковом номере: rangemin/rangemax; строки (с номерами 0-7).
И это явно напрашивается как-то унифицировать в некую таблицу.
Например, чтобы она индексировалась по KIND, а содержала бы "вид" (поведение) и индекс внутри этого вида; например, "units" -- вид="строковое свойство", индекс=6.
...вопрос только, как этот "вид" назвать: слово "kind" уже занято, а "type" ну совсем не хочется. Как и "class". Может, "bhvr" (behaviour)? Тоже плохо, ибо перекрывается с прочими местами.
@дома, ~19:20: напрашивается вместо нынешнего kind использовать иной термин (type-index?), освободив "kind" как раз для "вид канала".
Эти alias'ы надо бы указывать таблицей дуплетов (ВНЕШНЕЕ_ИМЯ,НАШЕ_ИМЯ).
А с учётом потенциального обобщения нынешнего cxsd_fe_epics.c в некий "упрощённый API для доступа снаружи", оная таблица должна будет указываться per-connection -- видимо, в вызове "создать соединение".
@дома, ~19:00: с другой
стороны, сама проверка "есть ли у нас такое..." -- нынешнее
cxsd_fe_epics_pvExistTest()
-- выполняется "бесконтекстно",
безо всяких соединений. Так что, возможно, таблицу alias'ов надо будет
указывать прямо самим "existTest" и "Attach".
fe_epics_PV::getName()
возвращает значение мониторова name
.
Ну сделал -- cxsd_fe_epics_get_name()
, его
getName()
и вызывает. Правда -- судя по
grep base-3.15.6/src/ca/legacy/pcas/**/*.c*
-- оно используется в основном для диагностической выдачи.
13.10.2024: именно так -- только для диагностической печати, что и сказано в src/ca/legacy/pcas/RELEASE_NOTES в разделе "Changes between epics 3.13 Beta 6 and 3.13 Beta ????".
02.07.2022: возвращаемся к разбирательству со строками.
It's important to note that when theprimitiveType()
function of a gdd object
returnsaitEnumString
, this does not mean that the void pointer points to a character
array. Instead,aitString
is a separate type. Thus, an array of typeaitString
would actually be an array ofaitString
objects and thus an array of strings, instead
of an array of typechar
.
От меня: дебилы, бл#!!! Ну и где полиморфизм, ради которого ООП и создавалось?! Получается, что для разных типов для доступа к их значениям нужны РАЗНЫЕ методы -- прямое противоречие принципу полиморфизма. Где была голова авторов этого безобразия?!
03.07.2022@около мыши по пути домой из Ярче, ~19:50:
Но как тогда указывать максимальную разрешённую длину?
04.07.2022: разбираемся ещё...
cainfo
от канала w1t10 говорит
Access: read, write Native data type: DBF_STRING Request type: DBR_STRING Element count: 10
Или это -- применительно к DBR_STRING -- и означает "массив из 10 строк"?
08.05.2023: ну дык, раз проверялось на cxsd -- то это же информация, которую МЫ САМИ и выдали libcas'у. Так что, да -- видимо, "массив из 10 строк".
NewDataCB()
обработка конкретно DBR_TIME_STRING
сделана ОЧЕНЬ отдельно: оно игнорирует значение nelems =
ARGS.count
и вручную проходится по строке в поиске первого
'\0'.
ARGS.count==1
".
aitEnumString
, а не только с
aitEnumFixedString
?
Поневоле вспоминается Задорнов с его "ну тупы-ы-ые...".
Но работу со строками отложим немножко на потом (ровно как было и с cda'шным модулем).
04.07.2022: а пока займёмся скалярами.
adjust()
'ом при помощи указателя"
не работает -- оно берёт вместо данных, на которые указатель показывает,
значение самого указателя.
Проверено было путём печати значения указателя через %p в
read()
и сравнением с тем, что показывает "caget
-0x
" -- значения совпали.
max_nelems
смотреть...
OK -- сделана проверка именно по max_nelems
, которое с
недавних пор присутствует в виде member-поля, заполняемого в конструкторе.
dataAddress()
+dataPointer()
.
@вечер: оная парочка заменена
на dataVoid()
-- так короче и правильнее.
update()
и read()
унифицировать: они ведь
делают В ТОЧНОСТИ одно и то же, с разницей лишь в GDD-объекте-получателе.
mondata2gdd(int mid, gdd *gp, int max_nelems)
;
последний параметр -- для выбора "способа передачи" (скаляр/вектор).
Туда перетащено обновлённое -- на выбор скаляр/вектор -- содержимое
read()
.
Ну а update()
и read()
переведены на неё --
после чего в них кода осталось 2 и 1 строка соответственно
(вызов+postEvent()
и просто вызов соответственно).
Ну-с, засим можно считать, что основная часть работы с ЧИСЛОВЫМИ каналами сделана, а осталось разобраться с "ait-конвертерами".
05.07.2022: а, нет -- ещё указывание длин векторов для векторных операций чтения/обновления.
05.07.2022: возвращаемся к возне со строками.
Непонятка: а как всё же отдавать максимальное число элементов в строке?
...или отдавать строки как массивы char[]?
Второй вариант кажется правильнее, но хбз: ведь в конструкторе-то, возможно, можно сразу указать максимальный размер буфера.
07.07.2022: потихоньку приступаем... Пока что самое простое -- запись: начата поддержка обоих видов входящих строк.
value
соответствующий объект -- при помощи метода getRef()
, которому
передаётся указатель на объект соответствующего класса.
return cxsd_fe_epics_do_write(...)
(хотя и унифицированный с обычным по всем параметрам).
Пока НЕ делается проверки, что нам передан скаляр. А надо бы...
aitEnumString
: добывает aitString
'овыми
методами string()
и length()
.
aitEnumFixedString
: указатель добывает просто ссыланием
на поле fixed_string
, а вот длину -- в цикле идёт по этому полю, пока
оно не закончится либо не встретится '\0'
, увеличивая
nelems
; в точности как в
cda_d_epics.c::NewDataCB()
.
void *data
у нас НЕ
const
. В результате приходится кастить
(void *)(...)
, что весьма неприятно.
Но при попытке добавить const
-- другие ошибки лезут;
конкретно -- что у cxsd_fe_epics_do_write()
параметр
data
НЕ-const.
Поэтому -- ну и переделано, что у cxsd_fe_epics_do_write()
теперь const void *data
. Ну да, там дальше оно
передаётся в не-const, но C, в отличие от C++, это позволяет.
08.07.2022: теперь -- информирование:
maxDimension()
и
maxBound()
к условию "скалярности" (т.е., когда возвращать
0
) добавлено
" || chn_dtype == CXDTYPE_TEXT
"
-- так что строки теперь "считаются скалярами".
Или, раз тут GDD и просто aitString (а не aitFixedString), всё же можно как-то указывать макс.длину?
Порылся -- нет, в casdef.h в определении
класса casPV
НЕТ никаких методов подобного рода; да и вообще
слово "string" (case-insensitive) там нигде не встречается.
09.07.2022@утро, пробуждение: по результатам вчерашнего штудирования исходников -- aitHelpers.h в первую очередь -- кристалльно ясно, как именно надо делать установку строки: указывать текущую длину, в качестве размера буфера максимальную длину, и то, что буфер "read-write, предоставляемый приложением на всё время жизни объекта".
09.07.2022: пытаемся реализовать...
aitString(data, aitStrRefConst, nelems, max_nelems)
А у gdd
такого метода -- с указанием списка свойств aitString'а
-- просто нету.
Благо, как показывает просмотр исходников
aitHelpers.*, при типе aitStrRefConst
(точнее, любом,
КРОМЕ aitStrCopy
) никакой аллокации/деаллокации не выполняется
и, соответственно, никакой утечки памяти не будет -- содержимое объекта
можно просто забывать.
Т.е., надо будет делать что-то вроде
new (gp->dataAddress()) aitString(data, aitStrRefConst, nelems, max_nelems); gp->setPrimType(aitEnumString);
Но всё-таки это некрасиво...
put(const aitString& d);
-- на него и надо смотреть.
Его код в gdd.cc, за вычетом проверок, только "мясо", выглядит так:
setPrimType(aitEnumString); aitString* s=(aitString*)dataAddress(); *s=d;
(Но сам-то он копирует из содержимого уже ГОТОВОГО ДРУГОГО объекта
aitString
, которого заводить совсем не хочется.)
init()
, применяющий все 4 параметра к уже готовому объекту --
int aitString::init(const char* p, aitStrType typeIn, unsigned strLengthIn, unsigned bufSizeIn)
-- именно её реально и вызывает конструктор "со всеми-всеми параметрами".
...но она private
...
aitStrType==aitStrRefConst
она сводится к вызову
this->installConstBuf(p, strLengthIn, bufSizeIn)
-- а уж этот "installConstBuf()
с 3 параметрами" и выполняет
реальную работу (значение aitStrType
не передаётся, т.к.
следует из названия самого метода).
(Точнее, перед ним вызывается ещё подчистка в виде init()
,
но нас это не колышет -- т.к. нечему утекать; плюс, там внутри всё равно
есть высвобождение на случай type==aitStrCopy
).
mondata2gdd()
"запихивание
строки в gdd" выглядит так:
aisp = (aitString*)(gp->dataAddress()); aisp->installConstBuf((char*)data, nelems, max_nelems); gp->setPrimType(aitEnumString);
OK, проверяем: cdaclient'ом пишем в текстовый канал "qwert12345", потом делаем caget. Результат -- ФИГ, пустая строка... И какая-то ругань...
dump()
от gdd и от строки.
Увидел, что свои-то данные корректны, и в объект-строку они ставятся
корректно, но вот сразу после setPrimType()
становятся бредом.
setPrimType()
, а уже
потом installConstBuf()
.
Анализ кода gdd::setPrimType()
в
gdd.cc показал, что после "подчистки старого" (которого реально
нет) вызывается строков метод clear()
-- который явно всё и
чистит.
aisp = (aitString*)(gp->dataAddress()); aisp->installConstBuf((char*)data, nelems, max_nelems); gp->setPrimType(aitEnumString);
(а вот на стороне сервера ругани на ЭТУ тему нет).CA.Client.Exception............................................... Warning: "No reasonable data conversion between client and server types" Context: "op=0, channel=asdf.2, type=DBR_CHAR, count=1, ctx="read failed"" Source File: ../getCopy.cpp line 92 Current Time: Sun Jul 10 2022 00:58:58.286991083 ..................................................................
filename="../../../../../../src/ca/legacy/pcas/generic/casStrmClient.cc" line number=712 server tool changed bounds on request - get notify with PV=asdf.2 type=4 count=1
Чё ему -- не нравится aitString, который НЕ fixed?!
Чуть позже:
S_cas_badBounds
.
casStrmClient::readNotifyResponse()
,
и, похоже, при обломе поиска подходящего конвертера...
(И аналогичное там есть в casStrmClient::readResponse()
,
только без слова "notify" в выдаваемом сообщении.)
read()
объект gdd (принадлежащий МНЕ!) опять сброшен на
"prim-type=aitInvalid". Какая скотина это делает?!
Похоже, это было "вскрытие показало, что пациент умер от вскрытия".
caget -S asdf.2 -d DBR_CHAR
", и
оно обламывалось.
caget asdf.2
" --
и всё стало отдаваться!
А вот для числовых данных -- не показывает!
и всё зависает намертво.A call to 'assert(this->prim_type==aitEnumContainer)' by thread '_main_' failed in ../gdd.cc line 1779. Dumping a stack trace of thread '_main_': --- тут идёт дамп стека --- EPICS Release EPICS R3.15.6. Local time is 2022-07-10 03:09:24.553067440 +07 Please E-mail this message to the author or to tech-talk@aps.anl.gov Calling epicsThreadSuspendSelf()
10.07.2022: погуглил -- благо, "DBR_STSACK_STRING" даёт всего 26 результатов. Это какой-то экзотический формат, требующийся одной-единственной софтине ALH (alarm handler?), поскольку только в нём единственном есть поле "ack", но вызывающий какие-то известные проблемы в GDD. Источники информации:
И там приводится весёленький факт (bold мой):
Regular clients will usually subscribe for DBE_VALUE events, while the alh subscribes for DBE_ALARM events, and an archiver will subscribe for DBE_LOG events.
Even if the Gateway ORed these bits together in its subscription to the IOC, Channel Access will not tell with an incoming update which event triggered this update, so the Gateway wouldn't know which clients to send the data to.
This is a known deficiency in the Channel Access network protocol which will be fixed in the next major protocol update (for EPICS V4).
Т.е., косяк-то страшеннейший, но они его почему-то не стали исправлять, а отложили аж до V4...
Только EPICS'ные строки ограничены не 40, а 39 символами -- видимо, в конце форсится '\0'. И при записи не более 39 символов, и если записать cdaclient'ом 50 символов, то caget получит строку из 39.
10.07.2022: несколько замечаний общего характера.
(Возможно, там всё делается просто и достаточно будет какой-то 1 строчки вроде "install_some_conversion_table()"; но чтобы понять что к чему и узнать эту строчку, уйдёт море времени -- как и на предыдущие разбирательства.)
Кстати, в "EPICS 7.0 Release Notes" (дата неизвестна -- почему-то EPICS'оиды почти никогда в документации не ставят...) есть раздел
Add dynamic (variable length) array support to PCAS
Dynamic array sizing support was added to the IOC server (RSRV) in the Base-3.14.12 release, but has not until now been supported in the Portable Channel Access Server (PCAS). Channel Access server applications using the PCAS may not need to be modified at all; if they already push monitors with different gdd array lengths, those variable sizes will be forwarded to any CA clients who have requested variable length updates. The example CAS server application has been modified to demonstrate this feature.
In implementing the above, the gdd method
gdd::put(const gdd *)
now copies the full-sized array from the source gdd if the destination gdd is of type array, has no allocated memory and a boundary size of 0.
Скорее всего, на поведение в рамках EPICS3 это никак не скажется -- вряд ли там есть хоть какая-то поддержка variable-length arrays. Но, во-первых, чем чёрт не шутит, а во-вторых -- и это главное! -- надо иметь максимально корректный код, потенциально условно-годный для работы с "PVA modules" (или как там конкретно называется "libPVA-server").
12.07.2022: упорно и занудно заново читаем CAS_Tutorial.pdf с самого начала. По ходу чтения появляются мысли:
gdd
, а конкретно gddScalar
; в т.ч. оно там
даже инстанциируется.
Может, действительно надо заводить не абы что (абстрактный gdd), а именно gddScalar?
Но у нас-то объекты потенциально разного рода -- И скаляры, И векторы. А держать вектор в Scalar-объекте -- тут точно что-то не то.
Плюс, у всей троицы порождённых -- gddScalar, gddArray и
gddContainer -- деструкторы тоже protected
. Так что как это
может работать -- загадка.
Ещё один нюанс: у всей этой троицы НЕТ СВОИХ ПОЛЕЙ --
все поля только от базового класса gdd
, а есть лишь МЕТОДЫ,
причём они даже не какое-то специфичное поведение определяют, а только набор
конструкторов и "operator=
". Реально же ПОВЕДЕНИЕ всё
реализовано прямо в исходном классе.
Т.е., по факту -- это "наследование" несколько фейковое, т.к. реально всё есть прямо в классе-предке, который содержит также и по факту "поля типа" (хотя и несколько завуалированные).
Это прямо какая-то профанация самой идеи ООП.
gddScalar
.
13.07.2022: продолжение страданий:
read()
'у "prototype
" и нашим
используемым в update()
"upd_gdd
".
Для этого загрузил сервер под GDB, перед запуском сделав
"break mondata2gdd
" -- чтоб оно тормознуло в момент отдачи
значения в обоих вариантах.
И там в обоих случаях делаем "print *gp
".
Так вот, различие -- в случае канала asdf.2,
который w1t100
-- только в поле appl_type
: при
чтении оно =16, а при обновлении =0. (Да, я diff'ом проверил.)
Что это значит -- а хрен его знает: это довольно абстрактный код, назначаемый динамически в зависимости от заданной программой таблицы используемых типов.
...и тут вспоминаем, что ругательство-то по update выдаётся
-- т.е., да, этой дуре не нравится значениеNo conversion between src & dest types no conversion between event app type=0 and DBR type=14 Element count=1
appl_type
, и похрен
ей, что в gdd при отдаче указывается тип данных... (BTW, 14 --
DBR_TIME_STRING
.)
setApplType()
;
gddAppType_value
(судя по
CAS_Tutorial.pdf и нагугленному сообщению
«Re: "No conversion between src & dest" warning with pcaspy»
в tech-talk от Andrew Johnson за 03-03-2015)
fe_epics_PV::fe_epics_PV()
строка
upd_gdd.setApplType(gddAppType_value);
Похоже, помогло -- обновления попёрли!
ЗЫ: пришлось в начало добавить #include <gddApps.h>
--
файл gddApps.h, кстати, похоже генерённый (хбз из чего --
gddAppTable.h?).
gddAppType_alarmHighWarning
равно 14...).
Однако не всё так просто. Со строками-то работало. И со скалярами работает. А вот с прочими векторами -- нет, фигня какая-то.
По обновлениям -- мусор.
14.07.2022: а нет, нифига -- помог! Т.к.:
read()
прилетает с
dim=1
и bounds={0,0}!
dim=1
и
bounds={start = 0, count = 5}!
Теперь вообще ВСЁ понятно: данные уходят ровно так, как указано в соответствующей gdd. А скаляры работали потому, что для них как раз ничего указывать не нужно и "dim=0" и означает скалярность.
Откуда вывод: похоже, где-то путается принцип "когда в gdd значение, а когда указатель" -- потому, что мы не указываем dimension+bounds.
А ведь НАДО указывать.
Или чтение использует размерности из casPV, а мониторирование нет? (Хотя
метод postEvent()
как раз у casPV
...)
В любом случае это ещё один индикатор "качества" кода libcas -- ну КАК можно было ОДНО и то же действие делать не унифицированно, а РАЗНЫМИ кусками кода (иначе как бы разное поведение было)?!
14.07.2022: придумываем решение.
mondata2gdd()
.
upd_gdd
достаточно при создании указывать --
прямо конструктору.
...но конкретно КОЛИЧЕСТВО всё же менять в момент обновления.
upd_gdd
указывать всё конструктору.
mondata2gdd()
указывать -- лишь при не-скаляре
(max_nelems != 1
)
-- ТОЛЬКО конкретное значение количества в 0-м измерении.
setDimension(int d,const gddBounds* = NULL)
;
что он делает?????????????
setBound(unsigned dim_to_set, aitIndex first, aitIndex count)
Тут при "номере измерения в установленных границах" выполняется просто
bounds[index_dim].set(first,count)
gddBounds
'ов метод set()
просто прописывает
указанное во внутренние поля -- БЕЗ каких-либо побочных эффектов.
P.S. "index_dim
" -- так в реализации называется прототипов
dim_to_set
. Разгильдяи...
gdd(int app,aitEnum prim,int dimen,aitUint32* size_array)
Его потроха таковы:
-- т.е.,gdd::gdd(int app, aitEnum prim, int dimen, aitUint32* val) { init(app,prim,dimen); for(int i=0;i<dimen;i++) bounds[i].set(0,val[i]); }
init()
, аллокирующий
"gddBounds1D
" -- "одномерные" границы -- и нулящий их,
P.S. Да-да, у него в прототипе и в реализации название последнего
параметра отличается -- "size_array
" и "val
".
Охренительное качество кода!
Наиболее правильным выглядит вариант, озвученный в конце п.I:
upd_gdd
создаётся прямо нужного "типа", путём передачи
параметров конструктору.
Сначала это было забыто, но в кусках кода ниже это уже учтено.
upd_gdd
"правильным образом" -- реально
оказалось непростым, сложности прямо на ровном месте во вроде бы
тривиальном деле:
Но у нас-то в зависимости от скалярности/векторности настройки могут изрядно отличаться. В результате:
dimen
" (скаляр/вектор) и
"size_array
/val
" (размер вектора).
my_max_nelems
,
который везде int
, тут пришлось сделать aitUint32
-- чтобы можно было тому конструктору gdd
передать указатель на
этот тип (ведь никакого способа переложить int
в переменную
другого типа у нас нет).
Да, тут как раз "спасло" наличие отнаследованного от gdd
типа fe_epics_gdd
-- именно с его помощью (при передаче ему
параметра) это преобразование и выполняется; иначе бы просто негде.
P.S. То ли дело было в Borland Pascal: сам решаешь, в какой момент и какой конструктор вызвать, и можешь спокойно делать "предварительные вычисления".
class fe_epics_gdd : public gdd { public: fe_epics_gdd(int app, aitEnum prim, cxdtype_t my_dtype, aitUint32 my_max_nelems) : gdd(app, prim, (my_max_nelems == 1 || my_dtype == CXDTYPE_TEXT)? 0 : 1, (my_max_nelems == 1 || my_dtype == CXDTYPE_TEXT)? NULL : &my_max_nelems) {} };
upd_gdd.setApplType(gddAppType_value)
, буквально
вчера добавленное как решение проблемы "почему обновления не работают?!",
теперь стало излишним и убрано.
mondata2gdd()
--
самое простое: в альтернативу "Vector?" сразу после adjust()
было добавлено
gp->setBound(0, 0, nelems);
Результат:
14.07.2022@лес около стадиона НГУ и у девятиэтажек на Терешковой, туда и обратно пешком на М-46: вообще, конечно, в C++ какой-то абзац прямо на базовом уровне реализации объектности:
using BASE_CLASS_NAME::BASE_CLASS_NAME
" для импорта
конструкторов не в счёт -- это уродство).
Это типа "для надёжности" -- гарантирует, что конструкторы и деструкторы всех объектов будут вызваны и в правильном порядке (компилятор обеспечивает).
Но при этом...
delete PARENT_PTR_TYPE
" при указателе, смотрящем на объект
отнаследованного класса, вызовет деструктор предка, а вовсе не реального
класса, что оставит поля (и поведение?) наследника неотдеструкторенными.
Одно это напрочь ломает "строгую" модель -- ну так и разрешили бы уж тогда и конструкторы вызывать когда удобно!
15.07.2022: ЗЫ: я точно не первый, кого этот вопрос заботит: гугление по "c++ constructor base class initialization parameters prior calculation" выдало "Call parameterised constructor after base class after some calculation", в котором лишь очевидные рецепты -- "you can use functions" (а толку-то? а если параметры надо вычислять парой -- дублировать вычисления? а если ещё что сложнее? Генерить промежуточный класс чисто ради того, чтобы его конструктор как-то манипулировал с аргументами -- типа чтобы функция-вычислитель складывала б их в структуру, а конструктор переходника вызывал бы конструктор предка, передавая ему значения индивидуальных полей структуры?).
15.07.2022: твикаем по мелочи:
is_interesting
, в конструкторе нулимое.
interestRegister()
и
interestDelete()
, делающие его =1 и =0 соответственно.
update()
проверяет, что если оно не взведено, то
ничего и не делать.
Результаты:
interestDelete()
вызывается после ЛЮБОГО
отключения "последнего" клиента, в т.ч. и того, который мониторирование не
запрашивал -- caget или caput. Видимо, в целях "подчистки".
Т.е., вызовы interestRegister()
и
interestDelete()
могут быть и несбалансированными.
Ну да, в casPVI.cc 1 вызов первой и 2 второй;
парные -- в casPVI::installMonitor()
и
casPVI::removeMonitor()
; а вот "лишнее" удаление -- в
casPVI::removeChannel()
.
if (interest == aitTrue)
-- сравнивать булевское с TRUE!!! Неужто им не рассказывали, что сравнивать
можно ТОЛЬКО с FALSE?!
16.07.2022: реализуем access control.
caNetAddr
.
Изучение caNetAddr.h и caNetAddr.cc показало, что
правильным способом добычи будет метод getSockIP()
-- он
возвращает struct sockaddr_in
.
ip_val = ntohl(inet_addr(inet_ntoa(addr.sin_addr)));
.isInet()
, иначе будет exception. ("Другой" вариант -- UDF,
"undefined".)
Вот в ином случае ip_val
и принимается за 0, как при
unix_serv_handle
.
CXSD_ACCESS_PERM_CONNECT
, то возвращаем "нету тут такого".
casChannel
; руки
дошли только сегодня, но, посмотревши внимательнее, решил забить -- см.
подробнее за сегодняшнюю дату ниже.
18.07.2022: проверил, добавив в конец команды запуска сервера фразочку
-e 'cxhosts-file '<(echo allow 192.168.171.173; echo allow local; echo deny all)
-- да, работает.
(Кстати, без "allow local" не работал доступ по cx:: т.к. cxlib -- на локальному узле старается коннектиться через UNIX, для оптимизации.)
16.07.2022: пытаемся разобраться с проблемой "от запроса DBR_STSACK_STRING ловится 'assert(this->prim_type==aitEnumContainer)' и всё падает".
test_PV::read()
для возврата значения делает
просто
prototype = the_String;
где присваеваемая определена как
aitString the_String = "A string value";
mondata2gdd()
ВПРЯМУЮ/"ВРУЧНУЮ" формирует содержимое gdd'ы, при помощи
setPrimType()
и последующего installConstBuf()
?
Для проверки также добавлен вариант
prototype = string_buf;
где присваеваемый буфер определён как
const char string_buf[100] = "A string value";
-- ага, тоже НЕ падает!!!
17.07.2022: роем дальше.
test_PV::read()
в начало
добавлено prototype.dump()
, чтобы посмотреть, что же такое
присылают.
Результат: дампится КОНТЕЙНЕР! Что соответствует той диагностической ругани -- что оно ожидало "prim_type==aitEnumContainer".
(Дамп тут в тексте закомменчен.)
Ну козлы же!!!
Получается, что они протаскивают на уровень КЛИЕНТА своей библиотеки очень интимную специфику EPICS'ного Channel Access'а!
И реализовать поддержку этого STSACK хоть сколько-то приличным образом без зарывания в ту специфику вряд ли удастся.
Бросил это занятие -- там чёрт ногу сломит (не, можно разобраться, но жаль времени).
Кстати, вполне возможно, что реально-то и "присвоение" работает не совсем корректно: ведь при том чтении caget в строке "Value:" показывает пустоту вместо реальной строки -- может, "присвоение" в неправильное место контейнера кладёт строку?
Проверил, сравнив вывод prototype.dump()
ДО и
ПОСЛЕ присваивания: да, там НИЧЕГО НЕ МЕНЯЕТСЯ!!! В обоих вариантах
"присваивания" -- и aitString'а, и char[].
prototype
'а
был Scalar или Vector, и если не они -- то возвращать отлуп?
Как оказалось, есть только isScalar()
, но НЕТУ
ни isVector()
, ни isArray()
!
Поэтому в fe_epics_PV::read()
добавлена проверка,
что если isContainer()||isAtomic() -- отлуп:
// A guard against DBR_STSACK_STRING if (prototype.isContainer() || prototype.isAtomic()) return S_casApp_noSupport;
Помогло! УРА!!!
26.05.2023: кусочек "isAtomic()
"
закомментирован -- иначе не работало чтение массивов (подробнее см. ниже за
сегодня).
18.07.2022: хотел было сделать, в рамках продолжения access control, поддержку указывания "readonly". Однако там оказалось всё устроено шибко уж казуистично.
casChannel
,
который, IMHO, является совершенно лишней сущностью.
casPV
,
методом createChannel()
коего он создаётся.
casChannel
является жёстко
привязанным к экземпляру (наследника) casPV
-- в частности,
сначала пытается вызваться Channel'ов метод read()
, и лишь при
его отсутствии -- PV'ёв;
Т.е., разделение на 2 класса -- какое-то странное и плохомотивированное;
что мешало ВСЮ эту функциональность держать прямо в casChannel
-- загадка. Возможно, какие-то EPICS-специфичные соображения, или
возможность использовать один экземпляр Channel'а для многих PV (но
последнему противоречит как раз непередача методам указателя на PV -- без
этого "один объект-контролёр-доступа на все PV" не сделать).
casChannelI
(у которого
ссылка на связанный casChannel
как раз есть) -- там опять
замороченная, так что любое разбирательство "а как вот это работает?" (чтобы
понять, правильно ли *Я* делаю) выльется в море потраченного времени.
Короче -- да ну нафиг...
18.07.2022: проводим более обширное тестирование.
caget -d DBR_STSACK_STRING asdf.0
",
где канал asdf.0 -- r1i5, вылился в SIGSEGV на попытке
prototype.dump()
сразу после выданного "----------dumping
container": судя по gdb, где-то в aitString::totalLength()
.
За-дол-ба-ло...
for i in {0..38};do; caget -d $i asdf.2; done
при натравливании на:
dump()
'ов оно и падать перестанет.
Далее:
dump()
'ов) либо закомментировываем.
Проверено -- да, при отсутствии STRING-запросов к INT/FLOAT-каналам сервер ничего на консоль не валит.
26.12.2022: в только что дописанном отчёте по госзаданию фигурирует такой фрагмент:
Поскольку интерфейс libcas требует C++, а CX-сервер написан на чистом C, то модуль cxsd_fe_epics состоит из двух частей, связанных простым API: 1. cxsd_fe_epics_meat.cpp, взаимодействующий с libcas; 2. cxsd_fe_epics.c, взаимодействующий с CX-сервером.
И при взгляде на него -- ну о-о-очень напрашивается переделка. Потому, что в cxsd_fe_epics.c сейчас реально ДВА сильно разных куска
DEFINE_CXSD_FRONTEND()
с соответствующими методами.
Напрашивается разделить его, оставив в cxsd_fe_cx.c эту инфраструктуру, неотъемлемую для модуля, а остальное вытащить, например, в cxsd_fe_epics_csfi.[ch].
...и, кстати, очень хоцца cxsd_fe_epics_meat.cpp переименовать в cxsd_fe_epics_cas.cpp.
05.01.2023: приступаем к такому разделению. Файлы:
Обнаруженные проблемы:
cxsd_fe_epics_meat_update()
.
fe_epics_periodics*
и
fe_epics_monitors_list*
.
Что будем делать:
06.01.2023: решаем проблемы:
cxsd_fe_epics_meat_update()
создана
целая окольная инфраструктура:
cxsd_sfi_mon_update_cb_t
,
...
mon_update
=NULL, ...
if (mon_update != NULL) mon_update(...)
;
cxsd_sfi_set_mon_update()
, ...
epics_init_m()
как
cxsd_sfi_set_mon_update(cxsd_fe_epics_cas_update);
epics_term_f()
специфичности вынуты в
cxsd_sfi_term_f()
, которая теперь и вызывается.
RequestSubscription()
утащена туда с переименованием в
cxsd_sfi_request_subscription()
.
fe_epics_*
" в "sfi_*
".
26.05.2023: при тестировании "родственника" в лице
epics2cda (точнее, в лице epics2smth) обнаружилось, что там не работают
ВЕКТОРА, конкретно ЧТЕНИЕ ВЕКТОРОВ -- на канал, определённый в сервере как
"w1i2", "caget asdf.3
" говорит
CA.Client.Exception............................................... Warning: "Channel read request failed" Context: "op=0, channel=asdf.3, type=DBR_TIME_LONG, count=2, ctx="read failed"" Source File: ../getCopy.cpp line 92 Current Time: Fri May 26 2023 19:08:33.742337692 .................................................................. Read operation timed out: some PV data was not read. asdf.3 2 0 0
e2s_PV::read()
параметр
prototype
(то, КУДА читать) на вопросы о своей структуре
возвращает "isContainer()=0 isAtomic()=1" -- а в read()
стоит
условие, что ЛЮБОЕ из этой пары вызывает отлуп с
S_casApp_noSupport
(для защиты от
DBR_STSACK_STRING
).
isContainer()
, но НЕ
isAtomic()
?
Подозреваю, что это и была причина "странностей с отработкой начального чтения", и что теперь, скорее всего, проблема решена. ...правда, неясно, чем аукнется удаление второй части условия.
Это однозначно правильное действие:
Таковую замену, видимо, стоит производить в
cxsd_fe_epics.c::GetChanInfo()
-- там как раз единая
точка резолвинга имён, плюс, там уже есть namebuf[1000]
(используется при наличии суффиксов вроде "_RAW").
02.09.2022: сделано, примерно по вышеприведённому
проекту, в GetChanInfo()
:
name
в namebuf[]
теперь
выполняется ВСЕГДА.
dot_p
запоминается позиция точки -- ПОСЛЕ
вышеупомянутой замены, так что формат "что_то_там:_RAW" также будет годен.
Т.о., в конце копирования в dot_p
будет указатель на
ПОСЛЕДНЮЮ точку.
dot_p != NULL
уже делается проверка на толпу
стандартных суффиксов и при совпадении в kind
ставится
соответствующее значение, а на место точки пишется терминатор '\0'.
Проверено: да, работает, и синтаксис "имя:канала:_RAW" тоже отрабатывается правильно.
04.11.2023: учитывая, что в EPICS/CA также разрешены дефисы '-' в именах, которые в CX запрещены, то стоит также переделывать дефисы '-' в подчерки '_' (подчерки-то разрешены).
(Осознал при просмотре имён для СКИФовской системы термостабилизации от Жарикова, где в его файле-описании Linac_Thermo_Stab_PVs_v4_3.xlsx значатся EPICS'ные имена вроде TC-LN.LTS-ContactorRelayGC:Sts -- явно выбранные Карнаевым.)
05.11.2023: да, добавлена такая трансляция в
GetChanInfo()
(в оба -- cxsd_fe_epics.c и
cxsd_sfi.c).
aitConvertFixedStringString()
строки отдаются
мусором".
Решений будет 2 -- независимых, каждое из которых достаточно уже само по себе (стараемся убрать дырки в максимальном количестве "листиков сыра"):
25.04.2023: действия:
static int aitConvertFixedStringString(void* d,const void* s, aitIndex c, const gddEnumStringTable *) { aitIndex i; aitString* in = (aitString*)s; aitFixedString* out = (aitFixedString*)d; unsigned len; /* 25.04.2023 D.Yu.B: Use aitString's length instead of relying on '\0'-terminatedness, which isn't guaranteed. Note: inspection of aitHelpers.h and aitHelpers.cc shows that 'len' field is maintained accurately. */ for(i=0;i<c;i++){ len = in[i].length(); if (len > AIT_FIXED_STRING_SIZE-1) len = AIT_FIXED_STRING_SIZE-1; if (len > 0) memcpy(out[i].fixed_string, in[i].string(), len); out[i].fixed_string[len] = '\0'; } return 0; }
Т.е., 1) копируем не более влазящего количества; 2) копируем пересылкой байт и только если не-0; 3) NUL прописывается в конец реальной строки, а не тупо в конец буфера. Всё максимально корректно :-).
Скомпилировалось без warning'ов.
В идеале надо будет это засабмитить багрепортом для исправления, но, увы, как-нибудь потом.
Работа (аналогично сделанному в cda_core.c) ведётся синхронно в cxsd_fe_epics.c и cxsd_sfi.c:
*_pvAttach()
: в формуле вычисления
требуемого объёма -- csize
-- теперь вместо
chn_p->max_nelems
используется ранее вычисляемое
max_nelems
, которое для REPR_TEXT-каналов делается
max_nelems++
.
*_get_data()
,
cda_dat_p_update_dataset()
, отражением чьего кода
является opr2eng()
.
Итого -- скопировал кусочек "NUL-terminate if text" непосредственно перед
вычислением is_text_int_compat
, как и в "исходнике" (да-а-а, аж
целый if()
, целых 5 строк, включая комментарий и фигурные
скобки :D).
26.04.2023: но в *_get_data()
всё же пришлось
в дополнение к
"phys_count == 0
"
добавить проверку, что не-REPR_TEXT -- т.е., что можно просто возвращать
указатель на chn_p->current_val
лишь без-{R,D}'шных И
не-текстовых каналов. Иначе ветвь с opr2eng()
и не работала.
ЗЫ: думал было, что аналогичные модификации понадобятся затем и в gateway'ях в gw/epics2smth/, но потом сообразил, что вряд ли: epics2cda и так будет получать от cda буферы с дополнительным NUL-терминированием, а epics2tango_gw -- скорее всего, аналогично от Tango.
26.04.2023: переходим к тестированию.
./sbin/cxsd -dSc configs/cxsd.conf -f devlist-test-alltypes.lst -e 'load-frontend epics :1
cdaclient -mND8 @t100:epics::text.0
либо
camonitor text.0
sleep 2"
):
cdaclient @t100:text.0=1234 cdaclient @t100:text.0=qwerty cdaclient @t100:text.0=1234 cdaclient @t100:text.0=a
Серверный логгинг (показаны только данные; первая строчка -- пустота):
1234 qwerty 1234ty a234ty
cdaclient'ов (timestamp'ы отабличены тут для читаемости):
"" @2023-04-26-13:44:44.678 "1234" @2023-04-26-13:45:11.285 "qwerty" @2023-04-26-13:45:13.293 "1234t" @2023-04-26-13:45:15.298 "a2" @2023-04-26-13:45:17.301
camonitor'ов:
text.0 2023-04-26 13:44:44.678396 text.0 2023-04-26 13:45:11.285722 1234 text.0 2023-04-26 13:45:13.293658 qwerty text.0 2023-04-26 13:45:15.298250 1234t\177 text.0 2023-04-26 13:45:17.301796 a2<\257#\177
Это было проверено запуском СНАЧАЛА cdaclient и ПОТОМ camonitor -- да, первый мусор получал, а второй нет (кроме последнего "a2", где "2" лишнее).
Особенность отдачи текущего значения?
Надо будет перепроверить -- какая-то там была особенность с отработкой начального чтения.
Теперь исправленное (проверяем поотдельности):
@вечер: а вот на e2k_128 проверить не
удалось: у yukari случался kernel panic на этапе запуска мониторирующего
"camonitor text.0
" (оный тоже 128-битный; если запускать только
его (при отсутствующем сервере) или только сервер -- всё OK, но после ответа
(который приходит!) через несколько секунд связь прерывалась; проверено 4
раза...).
Конкретные под-части будут обитать в своих подразделах level4-списка.
Оные части включают в себя (на текущий момент):
Живёт рядышком (а не в своей отдельной директории) чисто для удобства.
Причина -- в ОБЩИХ определениях LOCAL_LDFLAGS
, а те, в свою
очередь -- из-за единой директории с "предохранителями-канарейками"
директорией выше.
e2s_
?), способный работать с произвольными
backend'ами. Сам он -- да, плюсовый/объектный, чисто ради libCAS'а; но вот
"потроха"/логика его работы вполне Си'шные: адресация "мониторов" по их id,
аллокирование realloc()'ом/SLOTARRAY'ем; с использованием приёма из
avl_tid
):
свежесоздаваемые помещаются в список (по ID, а не по указателям!)
"свободных", так что при надобности берётся первый свободный. В элементах
пусть содержатся указатели на объекты e2sPV
и на backend'овы.
Такая ID-адресация позволит контролировать корректность (т.к. можно
сравнивать "не выходит ли ID за пределы аллокированного").
31.07.2022: не формализовать ли механизм "пул элементов, со ссылкой на первый свободный" на уровне misc_macros.h -- под каким-нибудь именем вроде "POOLARRAY"? Смысл -- чтоб asynchronous-completion-объекты выделять таким же пулом. 01.08.2022@вечер, ~21:36: неа, НЕЛЬЗЯ -- они должны быть недвижимы по памяти.
01.08.2022@утро: а может, ввести -- для элементов, которые нельзя двигать по памяти -- более хитрую сущность: аллокировать элементы пачками по 100, а РАСТИТЬ -- лишь массив указателей на эти пачки (причём сам массив растить сразу порциями по сколько-то -- хоть тоже по 100). Тогда доступ к N-тому элементу -- ptr_array[N/CHUNK_SIZE][N%CHUNK_SIZE].
02.08.2022: за прошедшие пару дней epics2smth.cpp доведено до минимальной степени "компилируется, собирается, запускается":
e2s_gdd
, e2s_PV
и, главное, e2s_Server
.
e2s_run()
, вызываемый из
собственно epics2tango.c -- оно всё пока чисто скелетное, но в
результате оно запускается и печатает имена искомых по сети PV'ей.
trec_t
.
is_used
не булевское, а enum, сделанный
аналогично... а фиг знает, чему аналогично -- но значения
MON_TYPE_UNUSED
=0, MON_TYPE_SEARCH
=1 и
MON_TYPE_WORKS
=2.
Но она пока никак не используется.
13.11.2022: ага, вот только о поле name
(у
tid'ов отсутствующем, а тут имеющемся!) было забыто. Сегодня исправлено.
13.11.2022: и ещё был забыт "менеджмент списка" -- работа
с frs_mon
/lst_mon
. Т.к. для tid'ов это делалось в
момент не аллокирования, а вставки в очередь, упорядоченную по времени.
16.08.2022: продолжаем. Типа "преамбула" -- что было понято при нудном чтении документации и исходников:
casAsyncPVExistIO
"
примерно понятна.
casAsyncReadIO
с
casAsyncWriteIO
, а про casAsyncPVExistIO
сказано
"ну оно аналогично" --
Asynchronous completion for the-- @#злы...pvExistTest()
andcreatePV()
functions works much the same way.
postIOCompletion()
передаётся лишь собственно
СТАТУС, но НЕ ссылка на объект-PV.
...видимо, как-то косвенно через ссылку на gdd; или, возможно, через связанный I-объект...
А вот для "Exist" -- понятно, там по имени. 17.08.2022: хотя вот сейчас смотрю -- и не вижу там имени...
11.09.2022: разобрался -- в
конструкторе IOI
-объекта вычитывают из
ctx
'а заголовок запроса. Подробнее см. ниже за сегодня.
Собственно
сделанное: 17.09.2022: удалено всё -- вся
концепция ASN/aid, за ненадобностью, т.к. раз информация для
casAsyncPVExistIO
-объекта вычитывается конструктором его
IOI
-подмастерья при создании, то и повторное
использование невозможно и держать пул этого добра просто незачем.
casAsyncPVExistIO
, а
"инфраструктура" -- пул, работающий аналогично MonSlot'ам (с чьей реализации
всё и скопировано).
Структура названа asninfo_t
.
moninfo_t
поле "void *AS_ptr
" заменено на
"int aid
" ("aid" -- "Async ID"); т.е., там будет храниться ID
приписанного к данному монитору уведомления -- и ТОЛЬКО при
is_used==MON_TYPE_SEARCH
.
#include <new>
" -- чтобы можно было прямо в
asninfo_t
и класть наследника-от-casAsyncPVExistIO
прямо полем (тогда понадобится "конструировать" объект по указанному
адресу).
17.08.2022@М-46, спускаясь вниз,
~13:10: неа, нельзя -- это ж ОБЪЕКТ, должный быть по фиксированному
адресу, а тут он оказался бы структуре, лежащей в ПУЛЕ, двигаемом
realloc()
'ом. Так что убираем.
А теперь "мысли":
pvExistTest()
'у
заводить ячейку в пуле "поиск", а уж при регистрации (по
pvAttach()
'у) создавать монитор?
Ответ: скорее всего нет, НЕ разделить. Просто потому, что в большинстве случаев, типа той же TANGO, нет возможности просто ПРОВЕРИТЬ существование канала с таким-то именем, а уже потом отдельно коннектиться к нему. Оно уж СРАЗУ генерит канал, и если получилось, то вот он, готовенький, дальше к нему можно подключаться.
name
, тем самым, вследствие постоянного
аллокирования/деаллокирования разного размера (из-за разных имён, прущих
вперемешку) плодя фрагментацию памяти?
17.08.2022@утро-душ: можно использовать технологию, аналогичную cxsd_db'шной "strbuf"
-- буфер строк, растущий шагами по 1...4kB, а вместо указателей хранить
offset'ы. Проблема только в том, что "наружу" указатели на такие
строки отдавать будет нельзя -- ведь если после отдачи добавится новая
строка и случится realloc()
, то указатель станет невалидным...
17.08.2022: пора уже делать "API backend'а" -- то, как epics2smth.c будет общаться с TANGO-частью.
Соображения:
e2s_run()
'у.
mid
(Monitor ID) -- чтоб все ответы "оттуда" шли
бы уже по нему.
pverDoesNotExistHere
.
Склоняюсь к тому, чтобы такой вызов всё же предусмотреть, но если соответствующая VMT-позиция==NULL, то просто не использовать эту функциональность.
18.08.2022@~13:00, выходя с М-46: а не сделать ли ещё один "backend" -- epics2cda, этакий "универсальный"? Т.е., чтоб backend просто редиректил бы всё к cda.
Замечания:
@~13:30: не удержался -- сделал epics2cda.c, скопировав почти пустой скелет из epics2tango.c, и буду пилить параллельно с последним.
18.08.2022: работаем...
e2s_backend_t
,
e2s_run()
'у, а тот его...
the_backend
, которой потроха
и пользуются.
Покамест там всего 2 метода:
check_name()
-- его условное-при-наличии использование
уже добавлено в e2s_Server::pvExistTest()
.
add_chan()
-- да, идейно он миррорит
cda_add_chan()
.
И сделан epics2tango.c::epics2tango_backend
, пока
пустой.
19.08.2022:
FindMonSlotWithName()
. 13.11.2022: ага, только с косяком: поиск начинала с
avl_mon
вместо frs_mon
.
e2s_Server::pvExistTest()
, делающая вроде худо-бедно "всё, что
надо".
20.08.2022: "об описаниях ТИПОВ":
Т.е., почти в точности как запаковано в cxdtype_t
, но вместо
"двоичного логарифма размера" передавать именно размер.
cxdtype_t
, то его и использовать!
Тем более, что он изначально организован "максимально правильно", т.е., и является наиболее оптимальным решением.
21.08.2022: тем более, что для конверсии TANGO<->"cxdtype" и "cxdtype"<->libcas можно использовать готовые куски кода из cda_d_tango.cpp и cxsd_fe_epics_meat.cpp соответственно.
#if
'ами.
Надо делать КОПИЮ -- всё то же самое, но назвать
"e2s_dtype_t
" (а вместо "*CXDTYPE*
" --
"*E2S_DTYPE*
").
И заодно пришлось битовые типы из misc_types.h сюда же
сдублировать -- под именами вроде "
e2s_uint32
".
21.09.2022: всё убрано, в рамках оптимизации: раз уж решили epics2tango_gw.cpp реализовать "публичным для всего мира" отдельным проектом, то для epics2smth.h и зависимость от cx.h становится некритичной.
ENCODE_DTYPE
", а не
"ENCODE_CXDTYPE
"!!!
22.08.2022: в продолжение темы "об описаниях ТИПОВ" -- изучаем "внутреннее устройство" TANGO. Детали в разделе по cda_d_tango, подраздел "информация" за сегодня.
26.08.2022: а сегодня -- изучаем то, как у объектов
DeviceAttribute
внутри устроено хранение информации о ТИПЕ
значения. Результаты в разделе cda_d_tango (уже в реализации) за сегодня.
27.08.2022: и за сегодня.
04.09.2022: возвращаемся к "мясу" -- добавляем в
e2s_Server::pvExistTest()
реальную работу по регистрации.
Напрашивается что-то типа "диаграммы состояний монитора" -- вот её надо реализовать и чётко тут описать.
12.09.2022: разобрался-таки с внутренней реализацией работы Async-объектов (как делается "привязка" объектов к запросам). Вот как это было:
10.09.2022: несколько дней подряд пытался разобраться, как же всё-таки работает "ссылание"/"идентификация" в Async-объектах -- так и не смог понять. Вот, к примеру, такая диаграмма событий:
- Приходит запрос на канал Name1 -- запоминаем у себя объект ASYNC1 и отдаём
pverAsyncCompletion
.- Приходит запрос на канал Name2 -- запоминаем у себя объект ASYNC2 и отдаём
pverAsyncCompletion
.- Получаем ответ "есть/нет" на канал Name2 и делаем
ASYNC2.postIOCompletion(есть/нет)
- Получаем ответ "есть/нет" на канал Name1 и делаем
ASYNC1.postIOCompletion(есть/нет)
Так вот, КАК оно различает, на какой из каналов даётся ответ?
- При том, что ни при возврате
pverAsyncCompletion
не указываетсяcasAsyncPVExistIO
-объект, ни при вызовеpostIOCompletion()
не указывается целевой канал.- И да, async-объекты, судя по мануалу, МОГУТ быть созданы пачкой заранее и затем по мере надобности использоваться из пула. 11.09.2022: а вот и нет -- видимо, я это перепутал с объектами
casPV
, про которые сказано, что они "however, the server tool can also precreate a series ofcasPV
objects for all the PVs it is responsible for, and then return a pointer or reference to them increatePV()
".Так что никакой возможности в момент создания выцеплять ссылку на целевой канал из
casCtx
'а тоже нет....и собственно
casAsyncPVExistIO
-объект в момент возвратаpverAsyncCompletion
также никоим образом не трогается.11.09.2022: разобрался!!! Всё-таки именно В МОМЕНТ СОЗДАНИЯ оно берёт информацию о "событии" из контекста. Нашёл это, сделав предположение "а вдруг всё-таки при создании? давай посмотрим конструктор!".
Последовательность "открытий" была такова (все файлы -- в src/ca/legacy/pcas/generic/:
- casAsyncPVExistIO.cc: конструктор
-- т.е., просто инициализирует свой I-объект ("подмастерье"?). У которого...casAsyncPVExistIO::casAsyncPVExistIO ( const casCtx & ctx ) : pAsyncPVExistIOI ( new casAsyncPVExistIOI ( *this, ctx ) ) {}- casAsyncPVExistIOI.cpp: конструктор
casAsyncPVExistIOI::casAsyncPVExistIOI()
делает много всякой инициализации, но в частности --т.е., в полеmsg ( *ctx.getMsg()msg
вычитывает какое-то "сообщение" ИЗ КОНТЕКСТА!!!- Судя по определению класса в casAsyncPVExistIOI.h, это поле --
-- т.е., это что-то, хранящее ChannelAccess-запрос.caHdrLargeArray const msg;Вечер: определяется в src/ca/legacy/pcas/generic/caHdrLargeArray.h -- это "a modified ca header with capacity for large arrays", структура из 4 полей
ca_uint32_t
и затем 2 штукca_uint16_t
.- Вишенка на торте: кроме конструктора, это
msg
более НИГДЕ НЕ УСТАНАВЛИВАЕТСЯ!!! Ни в одном из casAsync*IO*.[ch]* -- в них во всех одинаковый паттерн работы с этим полем.Т.е. -- эти граждане вместо ЯВНОЙ передачи информации используют "закулисную", "магическую" ("implicit"), что ну О-О-О-ЧЕНЬ дурной стиль...
Для простейшей операции они постоянно thrashing the memory pool. Ма-лад-цы!!!
ЗЫ:
- Общей базой для всех
casAsync*IOI
-классов являетсяcasAsyncIOI
.(В отличие от документации, где говорится об общей базе и не-
I
-классов! А тут они все сами по себе, БЕЗ базы!)- Он определяется в casAsyncIOI.h и casAsyncIOI.cc, но там чёрт ногу сломит понимать, что же делается (хотя сами файлы крохотные).
- Проблема в том, что "мясо" РАЗМЫТО между I-классом и его не-I-хозяином: например,
casAsyncPVExistIO::postIOCompletion()
сводится к-- т.е., вызывает одноимённый метод "подмастерья", который, в свою очередь, сводится к...return this->pAsyncPVExistIOI->postIOCompletion ( retValIn ); каковаяthis->retVal = retValIn; return this->insertEventQueue ();insertEventQueue()
...- ...определяется уже в классе
casAsyncIOI
какreturn this->client.addToEventQueue ( *this, this->inTheEventQueue, this->posted );- И поле "
client
" определяется какcasCoreClient & client;Будто в пинг-понг играют...
Просто ДИЧАЙШИЙ "спагетти-код" -- в наипрямейшем смысле: всё переплетено до полного непотребства; особенно доставляет постоянное прямое обращение к
this
-- наиявнейше показывает очень плохую структурированность.12.09.2022@утро-~07:00: неа, не "спагетти", а "равиоли-код" и "лазанья-код" одновременно (источники: "Спагетти-код" и "Spaghetti code" ):
- Равиоли-код -- код, состоящий из огромного числа независимых компонентов, и чтобы понять, как исправить ошибку на стыке компонентов, надо «прорываться» через межкомпонентные интерфейсы.
Ravioli code is a term specific to object-oriented programming. It describes code that comprises well-structured classes that are easy to understand in isolation, but difficult to understand as a whole.
- Пахлава-код или лазанья-код -- код, в котором слишком много (для данной задачи) слоёв абстракции.
Lasagna code refers to code whose layers are so complicated and intertwined that making a change in one layer would necessitate changes in all other layers.[16]
Это [16], кстати -- статья 2014 года от Latchezar Tomov и Valentina Ivanova "Teaching good practices in software engineering by counterexamples" -- весьма интересное чтиво (из собственно Вики -- ссылка на ResearchGate).
Вот так и теряешь веру в разумность человечества... :D
12.09.2022: с учётом (на основе) полученных знаний о
функционировании casAsyncPVExistIO
продолжена работа над
регистрацией.
12.09.2022@ЦКБ-приёмный-покой-14:00...16:00: мысли в тему "как хитро может быть устроена диаграмма работы регистрации":
add_chan()
?
Можно всячески выпендриваться с выполнением возвратов в другие моменты, но лучше...
Всё "выпрямить" -- просто чтоб e2s_set_found()
на
недо-подготовленные мониторы не возвращала бы статус, а меняла бы ВНУТРЕННЕЕ
СОСТОЯНИЕ МОНИТОРА -- т.е., например, ставила бы другой тип. А
pvExistTest()
чтоб в точке после вызова add_chan()
проверяла бы, что если тип не совпадает с пред-установленным, то считаем это
за "ответ вёрнут прямо извнутри add_chan()
" и реагируем на его
значение.
mons_list
,
asns_list
) из РАЗНЫХ потоков, а, учитывая
realloc()
в SLOTARRAY'е, это будет фатально.
Так что -- надо заводить mutex. Ровно с той же семантикой,
что в mt_cxscheduler'е, с проверкой pthread_self()
с
"epics2smth_threadid".
И прикрывать им критические секции нужно очень аккуратно, воизбежание deadlock'ов -- чтоб точно НЕ возникало из них обращений к другим частям, могущим лочить что-то своё.
13.09.2022: как вариант, что в
e2s_Server::pvExistTest()
и подобных на время вызова стороннего
кода отпускать mutex, затем захватывая его заново и тут же ПЕРЕ-КЭШИРОВАТЬ
указатели на внутренние ресурсы (mp
в первую очередь).
13.09.2022@ИЯФ, ~10:00..., в ожидании ЧеблоПаши: делаем по вчерашнему проекту.
MON_TYPE_REGISTERING
=1, MON_TYPE_DOES_EXIST
=2,
MON_TYPE_DOESN_T_EXIST
=3,
...и теперь при создании ставится именно тип REGISTERING.
add_chan()
.
the_backend->add_chan()
?
check_name()
, но оно
опционально).
add_chan()
.
14.09.2022: реально с полмесяца-месяц назад, @вечер, ~17:00, около стадиона НГУ по дороге на М-46:
надо бы для общения между epics2smth и его backend'ом
использовать в качестве backend'ова идентификатора не int
, а
указатель -- void*
; это наиболее общий подход, который позволит
каким-то "тупым" backend'ам, которые не держат пул, адресуемый по ID, а
просто аллокируют объекты, возвращать прямо эти указатели.
Цепочка размышлений тогда была такова (это реально продумывалось как часть спича на презентации "Опыт соединения EPICS с другими системами управления", где хотелось задвинуть на тему "как правильно организовывать внутренности софта"):
Смысл -- в том, что идентификатор ПРОВЕРЯЕМ: можно убедиться, что он не выходит за границы аллокированного диапазона, а затем проверить, что он валиден (объект с этим номером в пуле "активен") -- это будет существенным подспорьем в защите от программных ошибок.
Указатели же таковой проверке не поддаются (разве что можно проверять наличие magic number'а в фиксированном месте объекта, но это так себе проверка).
int
'у он тривиально приводится, а для "тупых"/"прямолинейных"
программ указатель позволит использовать и аллокируемые объекты напрямую.
Посему -- СДЕЛАНО!
e2s_backend_obj_ref_t
, ...
obj_ref
".
add_chan()
'а
считается указателем и проверяется на ==NULL
вместо
<0
.
epics2cda_add_chan()
полученный от cda
"ref
" проверяется на валидность и возвращается через
lint2ptr()
, а при невалидности -- NULL
.
15.09.2022: движемся потихоньку -- добавляем поддержку multithreading'а теперь и в epics2smth.cpp...
pthread_self()
. Только префикс "mt_sl_
" заменён
на "e2s_
", да e2s_threadid
инициализируется
чтением, а не порождением thread'а.
И зачатки использования, пока только в pvExistTest()
:
RETURN_EXISTS: e2s_unlock(); return pverExistsHere; RETURN_DOESNOT: e2s_unlock(); return pverDoesNotExistHere;
return pverNNNN
" делается
goto
на нужное.
add_chan()
теперь выглядит следующим образом:
-- т.е., разлочивается на время вызова, затем залочивается обратно и выполняется пере-кэширование.e2s_unlock(); obj_ref = the_backend->add_chan(pPVAliasName, mid); e2s_lock(); mp = AccessMonSlot(mid); // Re-cache
add_chan()
не дал определённого ответа да/нет
сразу.
del_chan()
-- для удаления недо-созданного
канала.
RlsMonSlot()
'а -- чтоб если
del_chan()
вдруг что-то вызовет, то "без последствий" (а то
вдруг параллельно будет заведён другой монитор и получит тот же
mid
, а тут ему изподтишка что-то попортят...).
MON_TYPE_DESTROYING
, которое и ставится ПЕРЕД началом удаления.
Вот о последних 3 пунктах ОЧЕНЬ много думал -- как же сделать корректно;
был выбран вот такой вариант. (Рассматривались также варианты вроде "а давайте
задекларируем, что del_chan()
'у нельзя ничего вызывать", но то
всё лишь декларации, а принятый является ЖЕЛЕЗНОЙ гарантией)
16.09.2022@Утро-~08:30:
наполнена и e2s_set_found()
.
MON_TYPE_REGISTERING
, в каковом
случае просто ставится соответствующее значение в is_used
.
MON_TYPE_SEARCH
-- уже надлежащая цепочка
действий.
postIOCompletion()
; далее ASN-объект освобождается в любом
случае, а монитор -- при ненайденности канала.
destroy()
более не определяется --
пусть libcas заботится об этом вопросе.
17.09.2022@утро-завтрак-хлопья: а на кой вообще теперь все "ASN"?! Учитывая их временность/одноразовость -- надо просто аллокировать при надобности и грохать при ненадобности. Да и собственный класс становится бессмысленным -- тоже убрать.
ЗЫ: с гроханьем при ненадобности -- сложнее... Может, после
postIOCompletion()
просто делать =NULL, а там уж пусть libcas
заботится; а в RlsMonSlot()
если !=NULL, то грохать
самостоятельно.
17.09.2022@вечер-М-46-~18:00+: делаем.
По сути -- выкинуто практически ВСЁ касательно asn/aid (последнее, где
они были -- w20220917-epics2smth-inprogress.tar.gz): в первую
очередь asn'ы с этими asns_list
, frs_asn
, GetAsnSlot()
и т.д., а также класс e2s_AsyncPVExistIO
.
А сделано следующее:
AS_ptr
-- сразу как
casAsyncPVExistIO *
.
mp->AS_ptr = new casAsyncPVExistIO(ctx);
RlsMonSlot()
делает delete
, если
AS_ptr!=NULL, ...
e2s_set_found()
прописывает =NULL непосредственно
перед вызовом postIOCompletion()
-- тем самым перекладывая
ответственность на libcas.
Всё просто и примитивно (и сократилось с ~500 строк до ~400). ...и постоянно теребит динамическую память почём зря...
Из прочего: реализован CheckMid_unlocked()
; заюзан пока в
единственном e2s_set_found()
(иных backend'овых вызовов ещё
просто нет, но скоро появятся).
18.09.2022: приступаем к реализации передачи данных в обоих направлениях; делаем по образу и подобию cda (почти копированием из cdaP.h). Покамест декларации:
snd_data()
-- аналогичен
cda_dat_p_snd_data_f
, только адресация другая.
e2s_update_data()
-- аналог
cda_dat_p_update_dataset()
, но тут уже отличие в том, что:
snd_data()
, только добавлены
int some_flags
(задел на будущее, под какой-нибудь
"alarm/severity/...") и
timestamp
.
e2s_time_t
-- точное
отражение cx_time_t
(который, в свою очередь, тривиально
конвертируется в годный для libcas'а struct timespec
).
Реализация:
e2s_update_data()
в
epics2cda.c::ProcessDatarefEvent()
-- самое простое:
там уже всё было подготовлено раньше, оставался только сам вызов да
перекладывание timestamp'а из cx_time_t
в
e2s_time_t
.
epics2cda_del_chan()
сделать!!! 19.09.2022: готово,
тривиально.
e2s_PV::write()
, не забыть подумать о
e2s_PV::read()
, а перед ними обоими -- ещё и
e2s_Server::pvAttach()
сделать...
18.09.2022@Дорога домой с М-46, идя проездом за домами параллельно Ильича, ~19:30: соображения общего характера:
31.10.2022: неа, НЕ будем делать толпу директорий. Пусть ВСЁ это будет в одной -- так проще и удобнее.
Всё равно ж epics2tango_gw.cpp будет публиковаться отдельно.
А вот переименовать нынешнюю директорию из epics2tango/ в epics2smth/ -- стоит.
epics::epics::epics::cx::ABC.DEF.GHI
-- т.е., чтоб оно
несколько петель делало бы через Channel Access, а потом уже через CX.
И чтоб это функционировало корректно и максимально похоже на прямое соединение по CX.
31.10.2022: а вот с этим может возникнуть
проблема: поскольку в CDA строка вида "[a-z]+::
" воспринимается
как указание протокола, то EPICS'ные имена вида
"SOMETHING::something:else" могут вызывать попытку загрузки
реализатора протокола "SOMETHING". Что по факту -- как бы DoS-атака.
Кстати: похоже, kind_of_reference()
позволяет "протоколы" с
'-' и '.' в именах. Надо бы это пресечь. Чуть позже:
неа, НЕ воспринимается, например, "ab.c::" и "ab-c::" как
префикс схемы/протокола. Хбз, почему -- надо б разобраться.
19.09.2022@ИЯФ-пультовая-утро ~09:30-10:30: делаем далее:
epics2cda_del_chan()
и
epics2cda_snd_data()
-- всё тривиально, они просто
перепасовывают даденное в cda_del_chan()
и
cda_snd_ref_data()
соответственно.
e2s_Server::pvAttach
. Он также несложен,
ибо предполагается, что уже РАНЕЕ был успешно исполнен и финализирован
pvExistTest()
, поэтому
S_casApp_pvNotFound
.
PV_ptr
, и если оно
==NULL, то создаётся новый экземпляр e2s_PV
.
Всё с надлежащей лочкой.
e2s_PV::getName()
, а то иначе
класс оставался абстрактным (т.к. она в casdef.h определена как
"= 0
").
А теперь пора изготавливать все подмастерья e2s_PV
--
bestExternalType()
и прочие maxDimension()
. Для
чего надо будет добавить API передачи этих данных от backend'а в epics2smth.
20.09.2022: "подмастерья" -- тут по минимуму тривиально:
getName()
.
Отличие от cxsd_fe_epics в том, что ИНФОРМАЦИЯ содержится в мониторе, а не в PV.
chn_dtype
,
max_nelems
, is_rw
скопированы из
fe_epics_PV
, но добавлены в moninfo_t
.
А вот API по их отдаче из backend'а пока нет.
20.09.2022@вечер: раз уж
принято решение сделать отдельный/самостоятельный/самодостаточный
epics2tango_gw.cpp -- то зачем выпендриваться с
e2s_dtype_t
/E2S_DTYPE_*
и
e2s_time_t
?!
Пусть уж будет зависимость от cx.h, зато всё станет унифицировано и меньше дублирования.
Бонус -- всякие dtype2aitEnum()
и т.п. можно будет обобщить
и использовать совместно.
21.09.2022: оптимизируем.
#include "cx.h"
; также убраны все
"скопированности из misc_types.h" -- определения int- и
float-типов.
А вот обобщения пока не сделано -- ещё не осознал; может, вытащить всё в frgn4cx/epics/cda/epics2cx_conv.h, "опубликовав" его в frgn4cx/include/? Или в какой-то аналогичный ОТДЕЛЬНЫЙ файл -- чтоб для server-side/libcas-dependent?
23.09.2022: вытащено в
include/libcas2cx_conv.h -- пока что только
dtype2aitEnum()
и aitEnum2dtype()
.
mondata2gdd()
--
пока оставлена на месте, т.к. ещё не вполне осознано, как именно нужно
произвести обобщение (учитывая, что сами данные добываются из
cxsd_fe_epics'ного монитора, а для epics2cda.c будет иное).
...хотя и напрашивается подленькая мыслишка "а вот было бы оно всё плюсовое, сделали бы общий базовый класс, в котором и предусмотрели бы как-нибудь..."...
09.10.2022: возвращаемся к работе:
e2s_set_props()
:
chn_dtype
,
max_nelems
, is_rw
скопом.
Т.к., по крайней мере в случае cda, эта информация и появляется скопом --
посредством cda_dat_p_set_hwinfo()
.
10.10.2022@пультовая, ~10:30:
однако оное cda_dat_p_set_hwinfo()
НИКАКОГО
CDA_DATAREF_R_
-события НЕ генерит! Что создаёт изрядные
проблемы...
e2s_update_data()
.
MON_TYPE_WORKS
, но насколько это корректно
-- хбз, и не надо ли завести ещё одно "промежуточное" состояние вроде
"РОЖАЕМ".
Теперь дело за использованием в
epics2cda.c::ProcessDatarefEvent()
.
10.10.2022@пультовая, ~10:30: хотел было по-быстрому реализовать epics2cda'шную часть -- авотфиг!
cda_dat_p_set_hwinfo()
никакого
CDA_DATAREF_R_
-события НЕ генерит. Т.е., просто впрямую его
ловить -- никак.
cda_dat_p_report_rslvstat()
-- ну просто
CX-протокол так устроен, что вся информация о канале присылается в процессе
"открытия", поэтому к моменту RSLVSTAT всё уже готово, и по оному событию
клиентская программа уже может всё о канале спрашивать.
_rslvstat()
, а вот _hwinfo()
в
GetPropsCB()
покамест не делается.
Но сделать НАДО БУДЕТ! 23.10.2022: сделано.
...причём в WORKS -- это уже будет "смена типа на лету"; хбз, как libcas на это отреагирует...
11.10.2022: доделано-таки недоделанное вчера со стороны epics2cda.c:
ProcessDatarefEvent()
конкретно ТОЛЬКО по
CDA_RSLVSTAT_FOUND
информация добывается посредством
cda_hwinfo_of_ref()
и делается e2s_set_props()
прямо ПЕРЕД вызовом e2s_set_found()
.
cda_set_type()
со свежеполученными данными.
Вот оно туда и вставлено.
12.11.2022: обнаружилась дикость: epics2cda
SIGSEGV'ится, причём -- по ВТОРОМУ запросу от pvExistTest()
.
Падает изнутри FindMonSlotWithName()
, на
__strcasecmp_l_avx()
.
13.11.2022: ну да, ну да...
mp->name
-- оно оставалось ==NULL.
И высвобождение его в RlsMonSlot()
тоже было забыто.
Вот он, результат копирования инфраструктуры tid'ов из cxscheduler.c -- там-то никаких имён нет.
Исправлено, сделано и strdup()
'енье при регистрации, и
safe_free()
при высвобождении.
Косяк крылся в FindMonSlotWithName()
: он стартовал поиск с
avl_mon
вместо frs_mon
.
frs_mon
никогда не заполнялся.
Как такое могло произойти -- учитывая, что копировалось из инфраструктуры, с менеджментом списка РАБОТАЮЩЕЙ?
А очень просто -- там-то это делается не при аллокировании в
GetToutSlot()
, а при вставке в упорядоченный по времени список в
sl_enq_tout_at()
.
Ну -- исправляем:
GetMonSlot
-- после "удаления из avl-списка"
добавлено помещение в активный список.
Причём в НАЧАЛО. Резон -- те PV, к которым обращались последними, скорее всего будут и далее более востребованными, вот пусть и стоят в начале списка.
Код почти скопирован из sl_enq_tout_at()
, только форсится
prev = -1
и за счёт этого упрощена ветка "куда записать mid?" -- всегда делается
frs_mon = mid
.
RlsMonSlot()
-- добавлено "удаление из активного
списка".
Код скопирован из sl_deq_tout()
.
(А помещение в avl-список (причём в НАЧАЛО) там уже было.)
FindMonSlotWithName()
была
кривая/инвертированная проверка на совпадение имён: стояло просто
if (name_compare(name, mp->name)) return mid;
как будто компаратор возвращает true(!=0)/false(==0), но там-то
strcmp()
/strcasecmp()
, так что надо сравнивать с
нулём --
if (name_compare(name, mp->name) == 0) return mid;
В результате оно регистрировало только ПЕРВЫЙ канал, на который приходил запрос, игнорируя остальные, причём регистрировало столько раз, сколько его спрашивали.
Исправлено.
Т.е., присутствовало аж ЧЕТЫРЕ косяка, каждый из которых был достаточным для неработы, а первые ДВА были фатальными.
Ну теперь вроде даже пашет -- печатает много диагностики о спрашиваемом.
Пора проверять: запустить какой-нибудь CX-сервер, потом epics2cda и спрашивать caget'ом какие-нибудь имена.
15.11.2022: вчера и сегодня тестировал.
Проверял оба варианта -- и через CX ("epics2cda cx::"), и через EPICS ("epics2cda epics::").
Да, имеющееся СЕЙЧАС сделанное -- работает.
Но собственно В/В -- не функционирует: нет ни обновлений (т.к.
e2s_update_data()
пустая), ни чтения/записи (т.к. методы
e2s_PV::{read,write}()
отсутствуют).
Так что -- теперь надо его реализовывать; благо, в cxsd_fe_epics_meat.cpp есть всё в практически готовом виде.
@Вечер-22:30: вопрос только -- а как быть с буфером? Там-то он аллокируется по предполагаемому объёму, а тут? Или тут буфер не нужен, а надо просто передавать данные напрямую? Но класть напрямую в gdd -- низзя: срок жизни больше, чем срок жизни буфера-со-значением от backend'а.
16.11.2022: делаем. Пока что забив на вопрос о буферах.
e2s_PV::write()
сделана на основе
fe_epics_PV::write()
.
Но несколько переделана, т.к. тут нужно исполнять локинг, поэтому
наif (value_type == aitEnumString) { ... // подготовка параметров отправки return ОТПРАВКА } else if (value_type == aitEnumFixedString) { ... // подготовка параметров отправки return ОТПРАВКА } ОСТАЛЬНОЕ ... // подготовка параметров отправки return ОТПРАВКА
if (value_type == aitEnumString) { ... // подготовка параметров отправки } else if (value_type == aitEnumFixedString) { ... // подготовка параметров отправки } else { ОСТАЛЬНОЕ ... // подготовка параметров отправки } return ОТПРАВКА
e2s_lock(); mp = AccessMonSlot(mid); ret = the_backend->snd_data(mp->obj_ref, dtype, nelems, data) == 0? S_casApp_success : S_casApp_noSupport; e2s_unlock();
e2s_update_data()
добавлен вызов объектова метода
update()
.
e2s_PV::update()
:
max_nelems
(необходимый при конверсии в
gdd), чтоб оно не лазило в монитор (где лежит значение) само.
fe_epics_PV::update()
.
data2gdd()
, являющейся копией
mondata2gdd()
, только исходные данные она не сама добывает, а
ей они передаются в готовом виде.
В остальном -- начиная с
value_type = dtype2aitEnum(dtype)
-- там всё символ-в-символ.
НО: если таковой вопрос всё же встанет, то нынешнее распределение
параметров -- то, что даже в e2s_PV::update()
передаётся уже
всё готовое -- позволит сделать ответственным складирование в него функцию
e2s_update_data()
.
Теперь проверяем.
Кстати, подобное ещё вчера наблюдалось, но я не обратил внимания (списал на свои косяки в паттерне запусков при тестах?).
Нашёл причину: НИГДЕ не делалось присвоения состояния
MON_TYPE_WORKS
.
Добавил надлежащее присвоение в e2s_set_found()
-- проблема
с ненахождением ушла.
(причём как-то не вполне "синхронно" с stderr-диагностикой обновлений, а пачками; то ли заскоки буферизации stdio, то ли специфика работы libcas'а).filename="../../../../../../src/ca/legacy/pcas/generic/casStrmClient.cc" line number=941 No conversion between src & dest types no conversion between event app type=0 and DBR type=14 Element count=1
Но это-то мы уже встречали ранее, 17-06-2022 -- надо gdd'е уставить
"application type" gddAppType_value
, каковое решение было
найдено 13-07-2022 и улучшено 14-07-2022.
Оное уставление добавлено в конструктор e2s_gdd
.
После этого обновления ЗАРАБОТАЛИ.
Только очень странно: ничего-ничего-ничего, а потом camonitor'у прилетает сразу пачка (у которой timestamp'ы все одинаковые). И такое впечатление -- очевидное при расположении окон epics2cda и camonitor рядом -- что эти прилёты происходят в момент, когда "серверу" прилетает какой-то запрос на другой канал. Проблемы с flush'ингом буферов?
@вечер-засыпая: так оно ж в разных thread'ах работает -- mt_cxscheduler/cda и
libcas! И, видимо, libcas НЕ обеспечивает уведомления, прерывающего
select()
, на котором висит
fileDescriptorManager.process(3600)
.
int dimen,aitUint32* size_array
").
...видимо, ту парочку придётся уставлять в e2s_set_props()
.
Методом setDimension()
?
17.11.2022: да, добавлено уставление. А поскольку поле
upd_gdd
в private
-секции, то пришлось сделать
вызываемый из e2s_set_props()
метод
e2s_PV::setDimension()
, принимающий
"d, d0size
"
и перепасовывающий их к upd_gdd.setDimension()
.
17.11.2022: утро -- пытаемся разобраться с проблемой разных thread'ов.
(Перепроверил в CAS_Tutorial.doc -- тоже нет.)
В CAS_Reference.pdf.
КАК?!?!?! Как вообще они планировали этой хрени работать в multithreaded-окружении (и даже локинг какой-то там повсюду разбросан), если про thread'ы ни слова?!
postEvent()
'у оно НЕ выполняет запись в сокет НЕМЕДЛЕННО?
@ИЯФ-пультовая-СКИФ-планёрка,
~11:30: или это они так пытались "блюсти realtime" -- чтобы запись не
тратила драгоценное время в основном thread'е, а исполнялась бы где-то в
стороне, в рамках времени, отпущенного для
fileDescriptorManager.process()
? И не по той же ли самой
причине оный process(0)
обрабатывает лишь по ОДНОМУ событию из
дескрипторов -- что время тут же кончается?
Точнее, косяк точно НЕ со стороны libcas, а, видимо, со стороны libCA
(Channel Access): ведь там срок обновления зависит от
CA_HEARTBEAT_USECS
, что влияет именно на cda_d_epics+libCA, а
не на cxsd_fe_epics+libCAS.
Но сам механизм тот же самый: чтение-то оно мониторирует, через слушаемые дескрипторы, а вот запись никто не пинает.
(А сначала была гипотеза «может, там не "запись происходит через
несколько секунд", а ОБНОВЛЕНИЕ отправляется libCAS'ом через несколько
секунд из-за не-записи сразу». Но влияние
CA_HEARTBEAT_USECS
эту идею сразу дискредитирует.)
22.11.2022@вечер, ~22:00:
для упрощения разбирательства -- чтобы запросы из ИЯФовской сети на всякие
VEPP4:... не портили картину -- в e2s_Server::pvExistTest()
добавлена проверка, что имя должно начинаться с "asdf".
17.05.2023: эта проверка закомментирована, а вместо неё
надо использовать перед командой указание
EPICS_CAS_INTF_ADDR_LIST=127.0.0.1
-- так оно bind'ится только
к localhost и лишнего не увидит.
23.11.2022: модифицируем.
e2s_set_props()
добавлена проверка
PV_ptr != NULL
.
e2s_PV::e2s_PV()
и e2s_gdd()
.
Проверяем -- увы, особо легче не стало: теперь SIGSEGV'ится на
e2s_PV::update()
-- а всё потому, что
e2s_update_data()
тоже вызывается ещё ДО рождения PV.
e2s_Server::pvAttach()
, хотя по факту "монитор оживает"
непосредственно в момент присвоения статуса WORKS.
Вывод: создавать надо непосредственно в ??????????????????????
e2s_Server::pvExistTest()
просто возвращает
pverExistsHere
.
А ведь НАДО исполнять переход.
Плюс, с учётом предыдущего пункта, надо и рождать PV тут же (точнее, в идеале как-то надо б объединить оба случая, вынеся код в общее место; но это позже, если получится).
Делаем:
e2s_PV
добавлено в
e2s_Server::pvExistTest()
и e2s_set_found()
в
точки после перехода в MON_TYPE_WORKS
, а в
e2s_Server::pvAttach()
оно более не требуется.
И при обломе создания эмулируется как если "notfound".
Результат -- падать перестало. Впрочем, с работающестью тоже не всё OK: канал "asdf.1" почему-то так и остаётся в состоянии "не найден3+637444".
25.11.2022: такое впечатление, что те результаты
недельной давности -- оттого, что сервер запускался с ключом
"-e load-frontend cxsd_fe_epics
"
и, похоже, он и отвечал на запросы caget'а первым.
Сейчас запустил без этого ключа -- и поведение иное (хотя фиг что поймёшь...)
Но задержки так и остались.
Плюс, timestamp'ы печатает несовпадающие с теми, что были при отправке
сервером. Возможно, это caget такой косячный; надо бы попробовать
посредством "cdaclient epics::...
".
26.11.2022: опять валит ругательства
на стороне сервера иfilename="../../../../../../src/ca/legacy/pcas/generic/casStrmClient.cc" line number=712 server tool changed bounds on request - get notify with PV=asdf.2 type=14 count=1
на стороне клиента.CA.Client.Exception............................................... Warning: "No reasonable data conversion between client and server types" Context: "op=0, channel=asdf.4, type=DBR_TIME_LONG, count=1, ctx="read failed"" Source File: ../getCopy.cpp line 92 Current Time: Sat Nov 26 2022 16:39:22.601304380
27.11.2022:
10.05.2023: ещё вчера пришла очевидная мысль, как
решить проблему "при выполнении postEvent()
НЕ прерывается
select()
, в котором висит
fileDescriptorManager.process(3600)
" -- завести pipe-пару, чей читающий конец зарегистрировать у этого
драного manager'а, а в пишущий пхать 1 байтик.
Вот сегодня сделано, ПОЧТИ всё:
#if USE_PIPE_FOR_UPDATES
".
update_pipe[2]
.
e2s_run()
она создаётся, оба конца делаются
O_NONBLOCK
, (а в конце даже close()
'атся, хотя
дотуда из-за while (1)
никогда не дойдёт)...
...но ПОКА регистрации читающего не выполняется -- ибо хбз, как.
e2s_PV::update()
делается запись 1 байта, причём
выглядит она как "::write(...)
" -- из-за того, что вызывается
из МЕТОДА объекта, который имеет метод write()
... Мда...
Остался 1 вопрос: как у этой фиготы зарегистрировать callback на чтение указанного дескриптора? Сходу -- совсем неочевидно, поэтому полез изучать src/libCom/fdmgr/fdManager.h:
fdManager
есть friend class fdReg
-- сходу такое впечатление, что нужно от него наследоваться и переопределять
в нём метод callBack()
, который изначально абстрактный
(=0
).
...где-то мы это уже встречали -- "объект-callback", где надо
наследоваться от "базового типа" для того лишь, чтобы определить в
наследнике собственно код callback'а. А-а-а, в Tango --
Tango::CallBack
(будь он неладен!).
Т.е., получается, что нужно в момент ПОСЛЕ pipe()
создавать
экземпляр "потомка fdReg"; но если объявить объект в том же scope, то он
автоматом инициализируется раньше.
И чо -- делать new
, генеря в динамической памяти, лишь из-за
тупого непродуманного API, не позволяющего установить номер дескриптора
позже?! Не страшно, конечно -- ровно один штук; но всё равно мерзко.
P.S. Вариант placement new на месте переменной
char[sizeof(fdReg)]
не рассматриваем, ибо извращение. Хотя --
да: гугление по "placement new delayed constructor" выдало первой же ссылкой
"Delayed constructor in C++"
на StackOverflow, а там чёткая формулировка "The only
standard way to call a constructor on an existing object is to use placement
new, (after the previous instance has been destructed)" за May 6,
2009.
11.05.2023: UPD: а ещё там есть ТРЕТИЙ класс --
fdRegId
, чьим наследником является fdReg
. И ещё
там template'ы от определённых в другом месте tsDLNode
и
tsSLNode
(double- и single-linked list'ы). Короче -- самый что
ни на есть ravioli code! Для решения простой вообще-то задачи. ...и, кстати, cxscheduler, решающий её же БЕЗ этой замутности
(и САМ поддерживающий списки объектов!), имеет размер
.h+.c практически идентичный (до едниц процентов) с
fdManager.h+fdManager.cpp. Ещё бы
производительность сравнить...
22.11.2024: причём fdManager НЕ
содержит работы с таймаутами -- ими занимается
epicsTimerQueuePassive
, а у cxscheduler'а оно есть и в объём
входит.
enum fdRegType {fdrRead, fdrWrite, fdrException, fdrNEnums};
-- а именно:
fdrNEnums
"?
Ответы стали очевидны из конструктора fdManager::fdManager()
в fdManager.cpp -- из единственной его строки:
fdSetsPtr ( new fd_set [fdrNEnums] )
fdrNEnums
-- число значений fdrNNN
(определение ПОСЛЕ последнего).
Напрашивается единственный путь: найти какой-нибудь пример в ихних исходниках и научиться у него (причём постараться найти наиболее ПРОСТОЙ вариант).
11.05.2023: ищем. В качестве образца взят
src/ca/legacy/pcas/generic/st/casIntfOS.cc -- в нём упоминаний
слова "callBack" было меньше всего (по результатам
"grep -rw callBack compile/base-3.15.9
").
На вид всё весьма просто, так что делаем:
e2s_pipe_fdReg
; состоит в основном из
обычных ритуальных заклинаний: конструктор (передающий предку для
регистрации всегда update_pipe[PIPE_RD_SIDE]
), деструктор.
callBack()
,
читающий из update_pipe[PIPE_RD_SIDE]
по 1024 байта покуда
читается указанное количество.
Пора возвращаться к тестированию -- о, ужас...
15.05.2023: возвращаемся.
(А вот БЕЗ этого -- нет, тормозит.)
Есть подозрение, что дело в "обновлении по циклу", для отвязки от
которого нужно выставлять всем каналам
CDA_DATAREF_OPT_ON_UPDATE
.
epics2cda_add_chan()
добавляем при регистрации
флажок CDA_DATAREF_OPT_ON_UPDATE
-- помогло: тормоза исчезли
при ЛЮБОЙ длине периода цикла сервера, обновления прилетают мгновенно.
15.05.2023: реализовываем чтение.
Краткое обсуждение:
mondata2gdd()
mondata2gdd()
САМА добывает данные посредством
cxsd_fe_epics_get_data()
/cxsd_sfi_get_data()
; тут
же data2gdd()
получает информацию в готовом виде; в
единственном на текущий момент использовании её в
e2s_PV::update()
информация приходит от backend'а.
*_get_data()
.
Реализация:
get_curv()
, должный возвращать всю
информацию, требуемую для data2gdd()
.
e2s_PV::read()
наполнен содержимым с оглядкой
на fe_epics_PV::read()
, каковая оглядка разворачивается в
громоздкий (из-за локинга), но очевидный сценарий: добыть данные, скормить
данные в data2gdd()
.
epics2cda_get_curv()
реализован, подглядыванием
в ProcessDatarefEvent()
'ову подветку
CDA_REF_R_UPDATE
. 11.06.2023: только в
КАКОМ файле подглядыванием -- cdaclient.c?
Потенциальная проблема: поскольку отдаётся указатель на данные, то он
валиден только на "ближайшее время" -- ведь данные могут располагаться в
refinfo_t.valbuf
, который в случае GrowBuf()/realloc() переедет
в другое место и указатель станет невалиден.
...кстати, это касается не только epics2cda.c, но и
cxsd_fe_epics.c/cxsd_sfi.c, где механиизм идентичен:
"небольшие" значения хранятся в moninfo_t.valbuf
, а содержащий
их fe_epics_monitors_list[]
/sfi_monitors_list[]
"растёт" realloc()'ированием точно так же, как cda'шный
refs_list[]
(с которого всё и копировалось).
refs_list[]
закончилось и он будет "увеличен", что
приведёт к переезду в другое место.
sizeof(CxAnyVal_t)
(поскольку они живут в аллокированном
индивидуально current_val
, никуда не ездящем).
ВОЗМОЖНА же она в случае "небольших" векторов -- т.е., значений, которые
уже векторны, но умещаются в объём CxAnyVal_t
.
valbuf
полностью,
оставив только current_val
.
valbuf
для не-скаляров --
т,е.,
"max_nelems>1 && csize<=sizeof(valbuf)
"!
@утро, завтрак: пытаемся сделать.
А вот для epics2cda -- увы, нет: буфер-то в cda...
cxsd_fe_epics_pvAttach()
и
cxsd_sfi_pvAttach()
добавлено.
16.05.2023: поскольку всё же не "get", а "access", то
переименовано "get" в "acc": acc_curv()
,
epics2cda_acc_curv()
.
17.05.2023: проверяем. Да, чтение работает -- метод вызывается, а camonitor получает текущие на момент своего запуска значения (при отключении метода -- НЕ получает).
(А ещё в процессе тестирования на НЕСКОЛЬКИХ PV обнаружилось, что cda_d_cx'ный UDP-резолвинг работал некорректно: "сразу" после регистрации канала поиск делался только при создании резолвера, т.е., ЕДИНОЖДЫ; а для всех последующих -- с периодом раз в 10 секунд. Раньше это не проявлялось, т.к. у всех клиентов весь рабочий набор каналов регистрировался сразу при запуске, а вылезло на epics2cda, где новые каналы регистрируются по мере жизни процесса. Подробнее см. в профильном разделе за сегодня.)
Чтоб можно было в одной локальной сети запустить НЕСКОЛЬКО экземпляров epics2tango/epics2cda, бриджующих РАЗНЫЕ блоки имён.
13.11.2022: и даже более того: если в epics2cda будут попадать имена вида "SYSTEM:SUBSYSTEM:SUBSUBSYSTEM", то оно может как-то неправильно восприниматься.
@утро, умывание, чистка зубов: Идея: разрешить указывать префикс в стиле cdaclient -- "@T[nnn]", чтоб клиент мог явно запросить, например, float ("@s") или "вектор 100 uint32" ("@+h100").
Некоторый вопрос, конечно, ГДЕ это обрабатывать: в идеале -- правильнее в
epics2smth.cpp, конкретно при
e2s_Server::pvExistTest()
, где и "создаётся" канал; но сейчас
backend'ову e2s_add_chan
передаётся только имя, а тип не
фигурирует. Поменять парадигму, чтоб было как в cda -- можно не указывать
(и разберётся потом само), а можно указать явно?
23.05.2023: делаем.
ParseTypeSpec()
, являющаяся чуть модифицированной копией
python3_calc_drv.c::ParseRefSpec()
.
Она возвращает -1: ошибка (отвалить), 0: отсутствие спецификации, +1: наличие спецификации (результат парсинга каковой складирован в переданные места).
e2s_Server::pvExistTest()
.
При отсутствии спецификации используются умолчания -- chn_dtype=CXDTYPE_UNKNOWN, max_nelems=sizeof(CxAnyVal_t).
А вот для работы с мониторами -- и для сохранения в них, и при поиске --
используется полное исходное pPVAliasName
, т.к. "ключом"
является именно оно.
e2s_add_chan
дополнительно принимает параметры dtype,nelems.
epics2cda_add_chan()
переделан -- он не
только принимает эти параметры, но и передаёт их
cda_add_chan()
'у.
ProcessDatarefEvent()
/RSLVSTAT для защиты
от dat-плагинов, НЕ делающих cda_dat_p_set_hwinfo()
(сейчас это
cda_d_vcas.c и cda_d_tango.c), вставлено условие, что
указывать dtype,nelems,is_rw "контрагентам" -- cda и e2s -- только при
chn_dtype != CXDTYPE_UNKNOWN
, чтобы не портить указанное в
спецификации.
Теперь надо проверять.
24.05.2023: пытаемся проверить -- да вообще ни черта не работает... (и как работало раньше?!)
26.05.2023: да нет, НЕ работали только векторные каналы, но с этим мы разобрались (см. за сегодня).
В остальном:
Проверить можно будет, видимо, на "vcas::"-каналах.
29.05.2023: пара других проверок:
Да, НЕ пытается.
bestExternalType()
'ом получаемого трансляцией через
dtype2aitEnum()
значения aitEnumInvalid
, в
результате чего имеем диагностику
(это 3.15.6).filename="../../../../../../src/ca/legacy/pcas/generic/casStrmClient.cc" line number=1701 Bad data type best external dbr type fetch failed filename="../../../../../../src/ca/legacy/pcas/generic/casStrmClient.cc" line number=1943 Bad data type - Server unable to create a new PV
cainfo
).
-S
), чтобы
убедиться в правильной работе всех вариантов типов данных (как векторных, так
и скалярных) -- чтоб можно было писать в любые каналы.
Да, всё работает как предполагалось.
30.05.2023: пытаемся также проверить и через VCAS.
ssh -L localhost:10000:localhost:10000
" --
через пульт/cxout, с конечным пунктом на vepp2kgw, куда вместо
":localhost:10000" было указано уже "IP:PORT" VCAS-сервера, взятые оттуда же
из devlist-cs-gateway-1.lst -- да, через эту цепочку как минимум
"cdaclient -d vcas::localhost:10000
" работает...
cda_dat_p_report_rslvstat(,CDA_RSLVSTAT_FOUND)
.
По теме:
Очевидно, потому, что в
cda_d_vcas.c::ProcessInData()
дешифрирование
полученной от сервера строки делается на основе hi->dtype
, а
CXDTYPE_UNKNOWN
в число поддерживаемых не входит, так что
dat-плагин и данные распарсить не может, и даже report_rslvstat не делает.
Однако, поскольку подписка на VCAS-каналы идёт по имени, то на каждый канал можно подписаться лишь ЕДИНОЖДЫ: например, если запросил "@t100:A.B.C", то "@d:A.B.C" уже всегда будет ненайденным. Но это и cdaclient'а касается (хотя на нём лучше заметен мелкий нюанс: начальное обновление/чтение на первый (работающий) канал приходят ДВАЖДЫ -- видимо, по штуке на каждую из команд подписки) -- такова уж модель устройства модуля (и по-другому его сделать сложно).
Вот сейчас если какой-то канал регистрируется, то он так навечно и остаётся.
А можно сделать возможность подчищать такие неиспользуемые
каналы, указывая ключиком из командной строки интервал: "-1" -- никогда, "0"
-- сразу после обезынтересивания канала в
e2s_PV::interestDelete()
, >0 -- срок в секундах (а подчистку
выполняется с периодичностью ~минута).
Создаём его.
18.08.2022:
А уж epics2smth пусть сам разбирается, актуально ли ему это сообщение (в
состоянии MON_TYPE_SEARCH
-- да, а в WORKS уже нет).
19.08.2022: следствие: воизбежание "чужих ответов" нужно,
чтобы epics2smth в pvExistTest()
/pvAttach()
ПРОВЕРЯЛ бы наличие монитора с таким именем и возвращал бы его (хотя при
повторном запросе на монитор, находящийся в MON_TYPE_SEARCH
--
надо как-то отвечать типа "позже"). Для чего нужно реализовать
ForeachMonSlot()
-- да, для ПУЛА, вручную.
cda_add_chan()
неизвестен ТИП канала -- ни dtype, ни nelems?
Ответ: а регистрировать его как CXDTYPE_UNKNOWN с nelems=16, а
потом, по получению информации о реальном канале, делать
cda_set_type()
.
Проблемы:
cda_set_type()
пока недоделана -- как
минимум, отсутствует реаллокирование/рост буфера.
CDA_DAT_P_FLAG_CHAN_TYPE_CHANGE_SUPPORTED
-- по понятным
причинам. ...как и у cda_d_tango...
CDA_DATAREF_OPT_PRIVATE
? А ведь это приведёт к
образованию "мусора" -- будет утекать память.
19.08.2022: неа, НЕ должно быть этого флага. Подробнее -- ниже.
19.08.2022: в продолжение темы "нужен ли
CDA_DATAREF_OPT_PRIVATE
"?
Т.к. пусть epics2smth следит за дублированием имён и при таковом дублировании просто возвращает уже имеющийся монитор/PV.
e2s_run()
'у. Либо прямо в
e2s_backend_t
.
FindMonSlotWithName()
,
использующий name_compare
-- указатель на функцию-компаратор,
устанавливаемый e2s_run()
'ом на основании добавленного поля
e2s_backend_t.names_case_sensitive
.
19.08.2022: дальше делаем:
ProcessDatarefEvent()
-- точнее, в основном
скопирована с cdaclient.c'шной, с некоторыми дополнениями.
"Суть"/"мясо":
e2s_set_found()
.
30.08.2022@~12:00, гуляя с папой
в парке между М-46 и Академией: есть проблема с дизайном
epics2cda.c: ведь для работы cda нужен основной цикл на основе
cxscheduler, а коль скоро libCAS никак НЕ предполагает интеграции с внешним
основным циклом -- по причине отсутствия аналога
ca_add_fd_registration()
-- и потому сейчас в
e2s_run()
стоит просто зависающее на час ожидание в виде
fileDescriptorManager.process(3600)
, то оно в пару не встанет
никак...
Похоже, придётся прибегнуть к услугам mt_cxscheduler, аналогично тому, как это было сделано в tvcapture.cpp'шном cdaCV/cda_adapter.c.
Проблема лишь в том, что тут, в отличие от tvcaptire, где поток данных однонаправленный, данные должны будут ходить в ДВУХ направлениях. При чём, вероятно, потребуется ещё и 2-й семафор/mutex, что, ещё более вероятно, будет чревато deadlock'ом.
31.08.2022@проезжая по двору вдоль Морского-64, едучи с М-46 домой ~9:30: в продолжение мыслей "а что делать для корректной передачи в ДВУХ направлениях?":
mt_sl_lock()
/mt_sl_unlock()
,
select()
основного цикла" -- т.е., именно внутри cda, откуда вызовы и происходят --
mt_sl_mutex
находится в залоченном состоянии, то попытка его
залочить приведёт к зависанию навеки, т.к. он залочен ЭТИМ же thread'ом.
Но это явный бред: ведь при одновременном-параллельном вызове из ДРУГОГО thread'а флажок-то может быть и взведён, но это НИКАК не говорит о безопасности.
Т.е., по факту решение -- для ОДНОПОТОЧНОЙ софтины, где таким флажком
можно отмечать внутренние критические секции (да и то не булевским, а
"считающим" -- именно так работает being_processed
).
mt_sl_lock()
/mt_sl_unlock()
-- они
дёргают внутренние do_lock()
/do_unlock()
лишь при
несовпадении
"!pthread_equal(pthread_self(), mt_sl_threadid)
",
см. записи от 29-05-2020, где решалась вот РОВНО ТЕКУЩАЯ ПРОБЛЕМА.
Так что реально проблемы deadlock'а нету -- надо просто спокойно лочить при любом вызове "внутрь cda" и всё будет работать.
31.08.2022: делаем (подглядывая в cdaCV/cda_adapter.c), всё в epics2cda.c:
$(LIBCXSCHEDULER)
на
$(LIBMT_CXSCHEDULER)
и добавлено требование
-lpthread
при линковке.
main()
добавлен "ритуал инициализации": предварительно
mt_sl_start()
, а создание контекста окружено
lock()/unlock()'аньем.
epics2cda_add_chan()
регистрация канала также окружена
лоченьем.
На эпом пока всё -- больше ещё ничего и нету :-).
16.06.2023: OK, добавил в
epics2cda.c::ProcessDatarefEvent()
между получением
chn_dtype
и его уставкой такую же проверку-модификатор, как в
cxsd_fe_epics.c::cxsd_fe_epics_pvAttach()
--
добывается phys_count
, и если он !=0 и тип при этом REPR_INT
или REPR_FLOAT, но не CXDTYPE_SINGLE
, то форсится
CXDTYPE_DOUBLE
.
ProcessCxlibEvent()
, похоже,
что уведомление о CDA_RSLVSTAT_FOUND
делается ДО получения RDS.
Вот и выходит, что в тот момент нужной информации ещё просто нет...
Но это изменение может повлечь последствия, ведь прочие "юзеры" имеют своё понимание последовательности прихода событий и обусловленные этим свои привычки, могущие от такого изменения сломаться.
Но, что интересно, несмотря на этот косяк -- caget получил число
285.300
как целочисленное 285
; т.е., просто
округлилось, а НЕ отдалось байтовое значение первых 4 байт вещественного
числа как целое. А-а-а, понятно: это потому, что
epics2cda_acc_curv()
возвращает не только указатель на данные,
но и их dtype, который дальше передаётся со значением (и dtype этот
соответствует типу данных -- именно на его основе они и сделаны).
08.11.2024: при возне с epics2cda
(происходившей вследствие тестирования epics2tango_gw
) вновь
встал этот вопрос -- почему int32-каналы, имеющие {R,D}, как минимум через
epics2cda
представляются как целые же (LONG), а не
вещественные?
09.11.2024: за 16-06-2023 эта тема уже обсуждалась --
вроде как косяк должен быть ТОЛЬКО в epics2cda
, т.к. только
там {R,D} приходят уже ПОСЛЕ события FOUND; в cxsd_fe_epics.so же
всё должно быть в порядке, т.к. там вся информация есть сразу.
10.11.2024: да, проверил -- из
configs/devlist-canhw-11.lst тот же канал qltr3.iset
через cxsd_fe_epics.so показывается как "Native data type:
DBF_DOUBLE", хотя через epics2cda
он отдавался как "DBR_LONG".
А битовый qltr3.opr
-- DBR_LONG
в обоих
случаях.
11.11.2024: в качестве варианта решения напрашивается в
cda_d_cx.c::ProcessCxlibEvent()
перенести вызов
cda_dat_p_report_rslvstat(,CDA_RSLVSTAT_FOUND)
из ветки
"opi->status == 0
" в "opi->status > 0
"
-- поскольку второе является отражением
CXC_NT_OPEN_FOUND_STAGE2
, присылаемой ПОСЛЕ всех свойств, то
это приведёт к тому, что в момент получения CDA_RSLVSTAT_FOUND
у epics2cda.c::ProcessDatarefEvent()
(и всех его
собратьев в других местах!) будет присутствовать вся информация о
канале.
ЗЫ: быстрое исследование -- просто
"grep RSLVSTAT_FOUND work/**/*.{c,cpp}
"
-- показало, что
Это ответ на опасения 16-06-2023 «прочие "юзеры" имеют своё понимание последовательности прихода событий и обусловленные этим свои привычки, могущие от такого изменения сломаться».
...да и "юзеров"-то -- pzframe_data.c с pzframe_gui.c и
cdaclient.c (ещё есть несколько ловцов
REPORT_RSLVSTAT_NOTFOUND
-- в основном драйверы вроде
trig_read_drv.c и утилиты вроде pipe2cda.c -- но их
интересует именно "NOTFOUND" для выдачи диагностики, а никаких "привычек"
там нет).
cda_dat_p_report_rslvstat()
cda_dat_p_set_ready()
.
CA_OP_CONN_UP
, являющимся аналогом RSLVSTAT'а.
Так что cda_d_epics.c передаёт cda_core все известные ему
свойства как раз ПЕРЕД сигнализированием
REPORT_RSLVSTAT_NOTFOUND
-- получается ровно та же
последовательность передачи информации.
11.11.2024: вызов
cda_dat_p_report_rslvstat(,CDA_RSLVSTAT_FOUND)
перенесён. Да,
поведение epics2cda
исправилось -- теперь int32-каналы, имеющие
{R,D}, представляются как DOUBLE.
Штука эта нужна для того, чтобы epics2cda мог бы работать в ОДНОМ потоке.
20.11.2024: начато было ещё 16-11-2024, посредством копирования всего Xh_cxscheduler.c, т.к. его содержимое максимально похоже на требуемое тут -- из-за аналогичной раздельной обработки R/W/E.
За позавчера/вчера/сегодня адаптирована работа с файловыми дескрипторами:
sl_fdReg
, сделанный по образцу
epics2tango_gw.cpp::e2t_update_pipe_fdReg
, ...
sl_add_fd()
, sl_del_fd()
,
sl_set_fd_mask()
.
sl_set_fd_mask()
:
sl_fdReg
-объекта присутствуют прямо в
fdrec_t
, и они "неживые" -- конструкторы для них не вызываются
(это возможно благодаря тому, что аллокирование делается chunk'ами и для
самих экземпляров fdrec_t
конструктор не вызывается, а
инициализация выполняется в GrowFddata()
"вручную"), а сами они
за-bzero()'ены.
~sl_fdReg()
с последующим
bzero()
.
GetDpxSlot()
.
...вот только "внутренний API получения свободной fdrec-ячейки" оставлен Xh_cxscheduler'овский -- БЕЗ "списка свободных ячеек".
Никакой попытки это компилировать пока не было, т.к. файл ещё категорически недоделан в отношении таймаутов.
Теперь надо разбираться, как в fdManager устроена работа с таймаутами.
21.11.2024: похоже, что конкретно в НЁМ -- НИКАК.
А он:
epicsTimerQueueNotify
и имеет приватное
поле
"epicsTimerQueuePassive * pTimerQueue;
";
epicsTimer & createTimer ();
", ...
...сводящийся к вызову
"return this->pTimerQueue->createTimer ();"
;
select()
-то в своём методе process()
,
вызывает сам, но значение таймаута для него получает посредством вызова (для
понятности приводим вместе с комментарием)
// One shot at expired timers prior to going into // select. This allows zero delay timers to arm // fd writes. We will never process the timer queue // more than once here so that fd activity get serviced // in a reasonable length of time. // double minDelay = this->pTimerQueue->process(epicsTime::getCurrent());
22.11.2024: это (с учётом написанного в комменте) выглядит
так, что вызов отрабатывает УЖЕ истекшие таймауты плюс возвращает время,
оставшееся до следующего, пока НЕ истёкшего -- вот оно и ставится таймаутом
в select()
.
23.11.2024: попытался разобраться в работе этого
epicsTimerQueuePassive
-- ну просто "слово из 6 букв, вторая И"
(ага, "фиаско"):
expire()
, вызываемый при истечении
таймаута.
Проблема в том, что метод этот, на вид -- у ОЧЕРЕДИ, а не у таймаута...
epicsTimerNotify
: так вот оно выглядит, в их извращённой
архитектуре...
Да, epicsTimerNotify
-- это именно самый что ни
на есть "callback class", его надо передавать методу
epicsTimer::start()
первым параметром. И у него будет вызван
метод expire()
(который должен сделать return
одного из вариантов: "noRestart
" для обычных одноразовых
таймеров, либо нечто типа "expireStatus(restart,30.0)
" для
повтора).
Разобраться в этом было непросто, из-за тучи классов с именами,
начинающимися с "epicsTimer
", и далее тоже слабо различающихся.
Сочетание чтения исходников с чтением вышеуказанной документации помогло.
26.11.2024: да, сделал всё в соответствии с дркументацией, исполнив все належащие шаманские ритуалы.
Осталось пока 2 вопроса:
expire()
как-нибудь сделать
destroy()
таймеру? А то там не вполне очевидно (даже после
чтения кода всяких timerQueue), возвращается ли таймер в очередь или
делается ли его подчистка (и делается ли когда-либо вообще удаление однажды
созданного объекта epicsTimer
?).
Можно будет проверить, печатая указатель на выдаваемый объект: если он будет повторяться -- значит, реюзается, а не утекает.
epicsTime
из struct timeval
: у первого база (EPOCH) 1990, а у второго
1970, но преобразования что-то не видно.
...впрочем, там всё традиционно очень замороченно; а попробовал
посмотреть код epicsTime::getCurrent()
-- вдруг увижу, как
делается EPICS'ное время из POSIX'ного -- но там ещё замороченнее: пытается
получить время от какого-нибудь из СПИСКА "провайдеров времени" 8-O..
Ну и in-place-инициализация почему-то опять не работает --
fdManager_cxscheduler.cpp: In function 'sl_tid_t GetToutSlot()': fdManager_cxscheduler.cpp:171:50: error: no matching function for call to 'operator new(sizetype, sl_TimerNotify&)' new (p->TimerNotify) sl_TimerNotify(p); ^
27.11.2024@утро-ИЯФ-1П-613:
тьфу ты -- надо ж указывать АДРЕС (как понято ещё 21-09-2024), а тут был сам
объект (ССЫЛКА -- "ОБЪЕКТ&
"), о чём компилятор и сказал
"'operator new(sizetype, sl_TimerNotify&)'
".
После исправления всё откомпилировалось.
27.11.2024@~14:00, идя от ИЯФ мимо
УркаБанка: кстати, а не будет ли проблемы из-за того, что новые
дескрипторы будут добавляться -- например, при добавлении серверов в cda --
во время висения в select()
'е, а он о них не знает и висеть
будет до посинения?
28.11.2024: посмотрел записи от 01-06-2020 при
изготовлении и исходник самого mt_cxscheduler.c -- нет, НЕ БУДЕТ проблемы: там предусмотрена pipe-пара, чей
читающий конец регистрируется у "реального" scscheduler'а, а в хуке
CXSCHEDULER_HOOK_FDSET_CHANGE()
в пишущий конец отправляется 1
байт, так что любое изменение набора дескрипторов приводит к автоматическому
завершению select()
с последующим обновлением набора
дескрипторов для слежения.
30.11.2024: допилил в
work/frgn4cx/gw/epics2smth/ также и Makefile на
использование Make-символа USE_MT_CXSCHEDULER
(при указании
"USE_MT_CXSCHEDULER=0
" используется как раз новенький), а в
epics2cda.cpp соответствующие #if
'ы (по тому же имени
#define
-параметра USE_MT_CXSCHEDULER
) были
добавлены ещё раньше.
Теперь надо проверять...
DeviceAttribute[Proxy?]
.
Собственно результаты -- в соответствующем подразделе раздела о cda_d_tango за сегодня.
26.08.2022: а сегодня разбираемся в том, как хранится информация о ТИПЕ значения.
Результаты -- в подразделе о реализации раздела cda_d_tango.
12.11.2022: собственно:
raise(11)
-- из
epics2smth.cpp::e2s_run()
.
main()
.
/usr/include/omniORB4/CORBA_sysdep.h:38:2: error: #error "Cannot use this C++ header file for non C++ programs."
Highlights:
_Server
,
_gdd
, _PV
(реализации методов -- тоже почищены,
остались только пустоты с return
в концах).
e2s_
" заменён на "e2t_
.
12.08.2024: некоторые размышления/постулаты/идеи касательно внутренней организации epics2tango_gw.cpp:
aitEnum*
<->DEV_*
) будет напрямую, БЕЗ
CX'ных CXDTYPE_nnn
(ибо незачем).
DpxInfo
-- аналог
cda_d_tango.cpp'шного dpxinfo_t
.
vector<DpxInfo*>
--
аналог SLOTARRAY'я dpxinfo_t
, только тут кроме него нужен ещё
какой-то ХЭШ для быстрого поиска по имени; значениями хэша должны быть
индексы в векторе.
Естественно, менеджмент элементов вектора и хэша должен вестись синхронно.
Идея в том, что обращения к этому списку должны происходить ТОЛЬКО в
момент обслуживания поиска, дальше же работа с индивидуальными
DpxInfo
идёт внутри них, а они аллокируются не в векторе, а
объектами в динамической памяти (вектор же содержит только указатели).
...в идеале, конечно, везде бы помнить не указатели, а именно индексы, но для трансляции индексов в указатели придётся обращаться к вектору, а такое обращение требует взведения егойного mutex'а, который по сути превращается в "global lock".
DpxInfo
есть свой mutex, взводимый на время
работы с ним.
Раз создание DeviceProxy
всё равно синхронно и занимает
макроскопическое время, то добавление к нему ещё времени на запрос списков
ситуацию принципиально уже не ухудшит (а для недоступных девайсов всё равно
выполняться эти запросы не будут).
DpxInfo
-- его DeviceProxy
ещё не создан и списков
атрибутов и команд ещё нет, то и сразу дать ответ "есть/нет" нельзя.
01.09.2024: нифига -- МОЖНО: отвечать "нет", а уж libCA
пусть потом повторит запрос.
DpxInfo
надо завести список (любой
организации -- вектор, FIFO, linked list, ...) для хранения необслуженных
асинхронных запросов (casAsyncPVExistIO
), чтобы при успешном
завершении создания DeviceProxy
пройтись по этому списку и
поотвечать да/нет.
pverDoesNotExistHere
" прямо сразу.
DpxInfo
.
01.09.2024: а нифига -- лучше не страдать таким и "забить": отвечать "нет", а libCA просто повторит запрос вскорости: в casdef.h есть комментарий
// Return pverDoesNotExistHere if too many simultaneous // asynchronous IO operations are pending against the server. // The client library will retry the request at some time // in the future.
"Деградация" производительности малозаметная (если вообще будет), а вот упрощение, за счёт отказа от поддержания списка "необслуженных асинхронных запросов" -- огромное.
ЗЫ: кстати, это означает, что при РЕГИСТРАЦИИ не будет НИКАКОЙ АСИНХРОННОСТИ.
Вечер: да, CheckTangoName()
переделана под такую
архитектуру: в любом случае, кроме найденного DpxInfo в состоянии
DPXINFO_READY
и наличия в нём в соответствующем
map
'е искомого имени, возвращается -1
(должный
приводить к pverDoesNotExistHere
).
DeviceProxy
-- надо выполнять ОТДЕЛЬНЫМ thread'ом (или
thread'ами).
Принципы организации и/или роста такого пула -- вопрос дискуссионный. Сходу напрашивается следующее простейшее решение:
Неприятность -- что этот счётчик тоже придётся как-то защищать, и mutex'ом-на-вектор нехорошо, т.к. опять получится почти "global lock".
...впрочем, учитывая, что "создание нового устройства" -- операция
предположительно нечастая, а о "завершении создания" тоже надо будет как-то
"сигнализировать" (чтоб выполнить ответ на запросы из очереди
casAsyncPVExistIO
), то можно действительно сделать на счётчик
отдельный mutex, который, по сути, будет и mutex'ом на пул
thread'ов-"создателей".
Напрашивается решение: создание нового DpxInfo
полностью
выполняется в "слушателе", а потом "создателю" передаётся короткая атомарная
ссылка --
Как вариант -- реализовать оба способа, с переключением между ними по
define-условию, выставляемому на основе архитектуры
("#if CPU_E2K128
", вот только такого в Sysdepflags.sh
нету...)
avl_tid
и GetToutSlot()
.
DpxInfo
:
DeviceProxy
: к ним стоит применять тот же подход, с
оговорками:
pverDoesNotExistHere
сразу, но учесть его нужно.
А процесс "с какой-то периодичностью ... проходиться ..." для них
сводится к тому, что проверку "а не пора ли грохнуть?" надо проводить
непосредственно перед очередной попыткой создания DeviceProxy
-- это всё равно выполняется в цикле для всех обслуживаемых ячеек каждого
thread'а-"создателя".
31.08.2024: неа, сделано не "перед", а ПОСЛЕ: раз уж создание обломилось и deadline прошёл, то чего ячейке зря висеть.
10.09.2024: ещё некоторые соображения на тему подчистки неиспользуемых (более 600 секунд) DpxInfo:
ref_count
из
>0 в ==0 и при этом перемещать такой dpx-ID в отдельный список "idle".
...а по переходе из ==0 в >0 -- убирать из этого списка.
Вот только муторно это всё: и сама необходимость добавлять в список "idle" и убирать из него (вот ЭТО точно потребует ДВУнаправленного списка), и "сортировка" (хотя при ДВУнаправленном она тривиальна -- просто добавлять в конец).
СЕЙЧАС как-то не выглядит эта овчинка стоящей выделки.
Причём не только "технический", но и "идеологический": кабы не возникло ситуации, что как раз в момент, когда одному thread'у надо обратиться к ячейке, другой её будет грохать. ...но если грохать будет "главный" thread, который обрабатывает запросы наличия, то проблемы нет. А есть она при взаимодействии оного "главного" с "создателями" -- те-то могут захотеть грохнуть ячейку, к которой "главный" как раз обращается.
@вечер: тут придётся лочить буквально каждый чих: если решил, что это "она", то ещё ДО снятия лока инкрементнуть reference count, чтобы "создатель" точно не подчистил; "создатель" же должен также грохать атомарно.
@вечер: а для невозникновения deadlock'ов можно ввести правило: если в некоей ситуации понадобятся ОБА лока, то сначала захватывать глобальный, а потом ячеечный (и освобождать в обратном порядке).
24.08.2024: ещё важное замечание:
Кстати, попробовал посмотреть, как устроена "регистрация" в самой libCAS:
ведь по логике, сразу же за успешным pvExistTest()
должен бы
следовать pvAttach()
-- тогда можно упростить работу своего
"класса", регистрируя tango-канал уже прямо из проверки наличия.
Но не тут-то было:
test_libcas
под GDB, поставив breakpoint
на test_Server::pvExistTest
, чтоб посмотреть "bt".
Посмотрел -- вызвано таки из того самого casDGClient.cc (3 шага стека внутри него).
Вот только вызовов pvAttach()
в том файле НЕТ!
Ей-богу, тупейший индусский/спагетти-код: как-то оно работает, но как -- фиг поймёшь, и что там может сломаться при какой-нибудь модификации -- хбз. Неудивительно, что никто из EPICS community не хочет этой фиготой заниматься.
26.08.2024: а ещё попробовал понять, зачем нужен метод
casPV::getName()
(на что влияет его результат и что, если метод
вернёт "") -- и тоже фиг чего поймёшь.
13.10.2024: исключительно для диагностической печати: src/ca/legacy/pcas/RELEASE_NOTES в разделе "Changes between epics 3.13 Beta 6 and 3.13 Beta ????" содержит абзац
o The server tool is now required to supply the virtual function "casPV::getName()" so that the server is able to identify the process variable when diagnostics are printed.
13.10.2024: в связи с получением этого знания
e2t_PV::getName()
дореализован -- возвращает
[mp->ent_idx]
'ное имя из соответствующей типу монитора
(атрибуты или команды) таблицы.
25.08.2024: более практические соображения насчёт парсинга имён.
determine_name_type()
ещё самодостаточна, то прочая алхимия с
разбором (и преобразованием '.' в '/' и ".." в
"->") выполняется непосредственно в
cda_d_tango_new_chan()
и несколько перезавязана с логикой
работы собственно регистрации канала.
cda_d_tango_new_chan()
имеет
длину под 400 строк, из которых почти 150 -- как раз парсинг+трансляция).
Технические соображения:
CHTYPE_nnn
, включая неподдерживаемые
CHTYPE_DEVPROP
/CHTYPE_ATTRPROP
.
const char **errstr_p
, в
которую они могли бы помещать указатель на фиксированную и статическую
строку.
...да, непосредственно в РАБОТЕ эта строка описания ошибки не нужна, но чтоб была возможность в "режиме отладки с дополнительной диагностикой" выдавать причины "неправильности" имён.
26.08.2024: ну начал...
determine_name_type()
-- да ноль проблем.
cda_d_tango_new_chan()
перетаскиваем в
parse_tango_name()
.
dev_name
) и
имя "сущности внутри устройства" (ent_name
) -- насколько теперь
нужна штука по имени "dup_name
"?
dup_name
таковое и происходит.
А выполнять потом отдельное аллокирование меньшего кусочка уже чисто под "имя сущности внутри устройства" -- глубокого смысла скорее нет.
При этом наличие dup_name
выглядит ещё более осмысленным:
ведь именно внутри этого буфера при надобности и делается "отрезание" того
"FIELDNAME" (вызывальщиком) путём за-'\0'ивания.
#if MAY_PARSE_AT_DATATYPE_MODIFIERS
",
каковой символ по умолчанию не определён.
Смысл -- что в epics2tango_gw эти префикс-суффиксы мимо кассы, т.к. тут
флаги BHVR_BOOL
и BHVR_VOID
взводятся на основании
информации из AttributeInfoList
или
CommandInfoList
.
30.08.2024: за последние пару дней сварганил основу
CreatorThreadProc()
. Структуру/алгоритм опишем позже, а пока
некоторые highlights:
creators_pool[]
решено сделать не растущим
"SLOTARRAY'ем", а статическим массивом. Резоны:
CreatorThreadProc()
'у надо передавать указатель на
какую-то структуру-описатель, чтобы в ней передать пару файловых
дескрипторов (для чтения команд и для отправки ответов), и структура эта уж
точно не должна гулять по памяти.
Так что проще всего сделать статический массив.
prv2cr
, в котором всегда поддерживается указатель на
предыдущую в списке ячейку (либо NULL
если текущая первая).
Идея в том, что раз в очереди что-то есть, то они не смогли создаться и неизвестно, когда ещё смогут, а вот новый вполне может быть живым, так что незачем его задерживать теми тормозами и надо пропустить вперёд.
select()
'у передаётся
timeout=NULL, чтоб висел бесконечно долго.
last_was_successful
-- она же используется и после попытки
создания для решения, что делать с ячейкой дальше.
#define
-символом MAY_SEND_POINTERS
, по умолчанию
выставляемым в 1
(предполагается, что для неподдерживающих
платформ, вроде E2K_128, должно будет делаться
"SPECIFIC_CPPFLAGS=-DMAY_SEND_POINTERS=0
").
CreatorThreadProc()
. Но
ведь к DpxInfo в этот момент привязана цепочка запросов existTest, на
которые отвечать имеет право только основной thread.
Поэтому такие "протухшие недосозданные" будем передавать
основному thread'у аналогично готовым, но для отличения их введём
дополнительное состояние DPXINFO_TIMEDOUT
, чтобы основной
thread отправил бы всем запросившим pverDoesNotExistHere
и
освободил бы ячейку.
...а при проверке "есть ли DpxInfo с именем устройства" если он
находится, но в состоянии DPXINFO_TIMEDOUT
, то
pverDoesNotExistHere
отдаётся сразу (как если в списке
атрибутов/команд имя отсутствует).
ЗАМЕЧАНИЕ: это всё (описанное в данном пункте) пока ещё не сделано, а только задумано.
01.09.2024@ванна, ~19:00: код сейчас складывается так, что есть ДВА вполне раздельных куска кода:
Откуда напрашивается:
...правда, вряд ли так получится -- дело в трансляции типов: в связке
EPICS<->TANGO можно пользоваться прямым преобразованием
aitEnum*
<->DEV_*
, а в общем случае придётся
вводить промежуточную "универсальную" кодировку (которой может работать
CXDTYPE_nnn).
02.09.2024: сделал регистрацию дескрипторов
rpy_pipe[PIPE_RD_SIDE]
у fileDescriptorManager
'а,
скопировав код из epics2smth.cpp -- класс
e2s_pipe_fdReg
, переобозвав его e2t_pipe_fdReg
и
внеся изменения для работы с множественными дескрипторами (вместо одного
конкретного).
Проблема в том, что при этом был прямо нарушен принцип, задекларированный вчера -- "продолжать разделение частей EPICS и TANGO" -- т.к. TANGO-часть, возящаяся с thread'ами и pipe'ами, впрямую пользуется libCAS'овским "API" слежения за файловыми дескрипторами.
Надо бы ввести "обобщённый API", которому передавать дескриптор и
callback-proc плюс privptr и чтоб он возвращал void*
, а
КОНКРЕТНУЮ реализацию уж сделать на "API fileDescriptorManager",
03.09.2024: сделана и передача DpxInfo
'ов
create-thread'ам.
@холл на 3-м этаже перез залом круглого стола: проект таков:
DeviceProxy
по возможности СВОБОДНОМУ thread'у.
Это, кстати, автоматически удовлетворяет требованиям 1 и 2 -- если предыдущий использованный свободен, то он и будет выбран для очередного задания.
Уж не знаю, насколько такой "алгоритм" годится в качестве общего решения для разравнивания загрузки между пулом "worker thread'ов" -- ведь в данном конкретном случае основная загвоздка в длительных зависаниях в случае сложностей с коннектом, а также в том, что неприконнекченные остаются в очереди надолго.
@подвал "у Кузнецова": реализация:
is_busy
--
frs2cr
в CreatorThreadProc()
, то "снаружи" понять
занятость невозможно;
is_busy
, которое при получении
"команды на создание очередного" взводится в 1, а сбрасывается в 0 при
опустошении списка после передачи (т.е., при frs2cr=NULL
).
is_busy
НЕ закрываются mutex'ом,
т.к. сам флаг атомарен и не требует железной консистентности с другими
(frs2cr
), и "промах" при race condition влечёт не критические
последствия, а всего лишь деградацию производительности вследствие
неоптимального выбора "создателя".
creators_pool_rr_n
("rr_n" -- Round Robin
N) -- тот самый номер последнего использованного "создателя".
@дорога из ИЯФ в УЭВ и по магазинам, срезая по диагонали с Лаврентьева на Николаева мимо Катализа: а ведь софтина доведена уже до полу-готового состояния -- резолвинг с подстилающей инфраструктурой thread'ов и с передачей информации (между ними и основным) готов, отсутствует только инфраструктура "мониторов", необходимая для собственно чтения/записи/мониторирования (ну и трансляция типов данных). Так что в нынешнем состоянии уже вполне можно тестировать -- как работает уже реализованная часть.
Ещё некоторые соображения:
dpxinfo_t
) каналы не будут удалены, но этого не делается -- там
вообще подчистки нет, в т.ч. cda_d_tango_del_srv()
ничего не
делает, а cda_d_tango_del_chan()
отсутствует вовсе -- как и
cda_d_epics_del_chan()
, кстати). Тут при обломе-прямо-сейчас о
повторном запросе позаботится клиентская libCA. Может,
просто обламываться СРАЗУ (а очереди оставив именно как очереди-"буфера"
запросов, растущие при превышении количеством запросов количества
"создателей")?.
DpxInfo
с таким именем и в состоянии
DPXINFO_CREATING
). Так что -- может, и не стоит устранять
очереди; а имитировать "отваливать сразу" можно уменьшением значения
option_idle_seconds
до 1 секунды (это явно больше времени
таймаута).
04.09.2024: небольшое дополнение: весь процесс поиска
"создателя" теперь окружён блокировкой свежевведённого
creators_pool_lock_mutex
-- исключительно "на всякий случай"
(или "на будущее"), чтоб этот кусок кода был бы годен для
multithread-safe-варианта, где запросы на каналы могли бы приходить разным
thread'ам.
05.09.2024: и он ДЕЛАЕТСЯ ВНУТРИ ЗАЛОЧЕННОГО
dpx_pool_lock_mutex
!!! Правильно ли это?
05.09.2024: проверяем.
Причина тривиальна:
Неприятно, но некритично.
info threads
", и в полученном списке найти
нужную.
"Нужная" определяется по TID'у: и GDB его печатает с префиксом "LWP", и
диагностическая печать программы выдаёт результат gettid()
'а --
вот и сопоставить.
thread N
" (у нас N=3).
Висело на лочке mutex'а...
CreatorThreadProc()
при проверке по
timestamp'у "не протухло ли" лочился один mutex
(di->lock_mutex
), а разлочивался другой
(dpx_pool_lock_mutex
).
Исправлено.
Вопрос скорее в том, насколько возможно определение "проблема была именно в таймауте".
06.09.2024: сходу в исходниках видно только общий
exception Tango::ConnectionFailed
; в диагностике светится некая
строка "TRANSIENT_CallTimedout", но насколько оно доставабельно кодом -- фиг
знает...
06.09.2024: а вообще нам ведь ПОФИГ на причину облома, главное
-- пауза; так что можно засекать время отработки попытки создания, и либо
а) если она длилась дольше, например, 3 секунд, то считать лишним
последующее ожидание в select()
'е; либо б) просто вычитать
это время из последующих 10 секунд, а если результат окажется отрицательным,
то считать "время прошло" и делать просто поллинг.
06.09.2024: пожалуй, на этом проверку работы резолвера
можно заканчивать: дальше проверить пока ничего не удастся, т.к. даже для
cainfo
нужно нечто, способное отдавать информацию о свойствах,
читай -- уже нужны PV, т.е., надо городить поддержку мониторов.
avl_mon
. Т.е., аналогично пулу DpxInfo.
Сюрприз-сюрприз! Ровно это уже реализовано в epics2smth.cpp для мониторов. ...и оно, кстати, тоже "is based on cxscheduler.c's trec_t".
@16:50, по дороге от П28 к
Морскому-2: однако сами MonInfo
должны будут быть
ФИКСИРОВАННЫМИ в памяти, т.к. Tango может присылать обновления в иные
thread'ы (ЕЁЙНЫЕ!), и совсем не хочется лочить mon_pool
на
каждый чих. Поэтому придётся изготовить "комбинированную" реализацию
GetMonSlot()
: c ДВУнаправленным списком индексов, как в
epics2smth.cpp, но с аллокированием самих структур "пачками"
(chunks), как в GetDpxInfoSlot()
.
DpxInfo
уже есть векторы команд
и атрибутов, содержащие их названия и индексируемые целочисленно, то:
CheckTangoName()
может делать
free(dup_name)
и далее использовать "индекс атрибута/команды
внутри вектора".
(*(di->attribute_info_list_p))[ATR_IDX].name.c_str()
-- там
ровно эта строка.
07.09.2024@утро-душ: учитывая,
что методов "дай вот-такое-то свойство" у PV не один, а процесс определения
"откуда же брать, в зависимости от chtype
" двухстадийный --
видимо, надо его вытащить в отдельную функцию, возвращающую
ВСЕ свойства, а уж конкретный метод пусть после её вызова возьмёт себе
нужное.
09.09.2024: мини-отчёт о создании инфраструктуры мониторов:
AS_ptr
,
obj_ref
), ненужный тут name
, а также
chn_dtype
,max_nelems
,is_rw
, ...
Вот и получается, что достаточно было просто скопировать ОТСЮДА ЖЕ инфраструктуру DpxInfo, а не брать и адаптировать из другого места.
GetDpxInfoSlot()
/RlsDpxInfoSlot()
в
GetDpxSlot()
/RlsDpxSlot()
-- "Info
"
тут лишнее.
Сделано.
10.09.2024: инфраструктура мониторов переделана с ДВУхсвязного списка на ОДНОсвязный.
11.09.2024@утро, по пути в ИЯФ, спускаясь по лестнице с 10-го вниз, ~08:40: бредовые идеи "на перспективу", как можно бы УДАЛЯТЬ thread'ы-"создатели":
pthread_cancel()
! -- они должны уметь завершаться сами, по
команде.
(Так-то можно развить целую инфраструктуру для РАЗНЫХ команд: ID<0 -- -1, -2, ...; указатели с числовыми значениями 0x1UL, 0x2UL, ...)
Очевидный вариант -- закрыть свой rpy_pipe[PIPE_WR_SIDE]
,
чтобы "хозяин" получил уведомление о готовности и read()
==0 --
выглядит не фонтан: будет неатомарность, т.к. неопределённо, какое из
событий наступит раньше -- реальное завершение thread'а или получение
уведомления "хозяином".
CreatorThreadInfo
, так что "хозяину" пофиг,
успел ли "создатель" завершиться к моменту получения "хозяином" уведомления.
...но тут проблема в том, что в объекте pthread_t
могут
оставаться какие-то недоподчищенные ресурсы, так что возможное повторное
использование ячейки с записью в поле threadid
их перепропишут.
А ещё ведь завершившимся thread'ам надо делать
pthread_join()
, для подчистки их ресурсов...
ВОЗМОЖНО, сработает такой вариант: если запускать "создателей" в режиме
"detached", то подчистка будет выполняться автоматически (это-то не
"возможно", а гарантируется спецификацией), а если pthread_t
является просто числом, то тогда его перепрописывание будет безопасно.
...и в любом случае стоит иметь в CreatorThreadInfo
поле
"state
", в котором у неиспользуемых UNUSED, а завершающиеся
заносят туда FINISHING, чтоб их не пытались использовать, и при получении
уведомления "хозяин" будет прописывать UNUSED.
13.09.2024: потихоньку подходим к манипуляциям с данными.
CmdArgType2aitEnum()
и aitEnum2CmdArgType()
.
CXDTYPE_nnn
на
DEV_nnn
, на основе анализа и сопоставления определений из
tango_const.h, aitTypes.h, db_access.h:
CmdArgType2aitEnum()
слегка
расширилась за счёт BOOLEAN, STATE и VOID.
DevEnum
ЗНАКОВЫЙ,
а EPICS'ный aitEnum16
/dbr_enum_t
-- БЕЗзнаковый.
Поэтому они маппируются асимметрично: первый на Int16/DBR_SHORT, а второй
на DEV_USHORT
.
В комментариях в тексте эти траблы расписаны, к каждому типу индивидуально.
Также по ходу обдумывания возникло несколько мыслей/замечаний:
BHVR_BOOL
и
BHVR_VOID
из @-суффиксов имени.
BHVR_ON_UPDATE
берётся всё же из парсинга имени (больше-то
неоткуда ведь?), то может оказаться, что разные запросы (отражающиеся на
один атрибут) от разных клиентов указывают РАЗНЫЕ режимы мониторирования.
А монитор-то будет ОДИН!
Что делать? Забить на проблему и использовать режим от ПЕРВОГО -- когда,
в момент interestRegister()
, и будет выполняться
subscribe_event()
?
strcmp()
, но
ведь Tango-имена же case-INsensitive!
Заменить на strcasecmp()
? Но тогда сломается индексация --
она же выполняется по значению указателя (а не по указываемой им строке!).
Видимо, правильнее будет в момент получения имени сразу -- в
parse_tango_name()
-- переводить его в нижний регистр
tolower()
'ом. 17.10.2024: да-да-да, а
строки .name.c_str()
и .cmd_name.c_str()
в
полученных от TANGO массивах (используемые как ключи) -- они-то в РАЗНЫХ
регистрах, и им никто tolower()
не сделает! Так что идея, увы,
нерабочая.
...но что-то это тоже мутно: а как тогда ПОИСК работает? Ведь в момент поиска мы указываем указатель, который своим числовым значением не имеет НИКАКОГО отношения к числовому значению указателя, складированному в ключе значения в map'е...
17.09.2024: а ведь можно сделать простой тест: вставить в
cmp_str::operator()()
диагностическую печать (выдачу
a
и b
плюс содержимого) и посмотреть, вызывается
ли она при ДОБАВЛЕНИИ элемента.
19.09.2024: добавил... Изучаю...
!(x<y) && !(y<x)
-- имитируется сравнение на равенство.
13.10.2024: case-insensitivity сделано по идее от
13-09-2024 (ровно через месяц) -- переводом в нижний регистр
tolower()
'ом прямо в parse_tango_name()
.
17.10.2024: и всё-таки переделано на
strcasecmp()
в cmp_str::operator()()
.
14.09.2024: переходим от размышлений к практическим шагам по реализации собственно передачи данных.
PV::write()
.
Исходные компоненты у нас уже есть, в виде
fe_epics_PV::write()
-- добыча значений из gdd;
cda_d_tango_snd_data()
+snd_data_via_command()
-- собственно исполнение отправки данных в Tango.
Нужно только их "скрестить" вместе.
16.09.2024: со вчера пилим.
*snd_data*()
параметр "value
"
переименован в "data
" -- потому, что у PV::write()
последний параметр "gdd &value
" (а вот извлекаемый оттуда
для передачи в cxsd_fe_epics_do_write()
адрес данных -- как раз
уже "data
").
*snd_data*()
, чтоб они
были прямо внутри монструозной e2t_PV::write()
, т.к.:
snd_data_via_command()
всё
равно нет (проверено: думал, вдруг оно используется в
_req_read()
, но нет).
CLEANUP_VECTORS_EIO
их
подчистка.
EIO
в принципе нет, то
CLEANUP_VECTORS_EIO
переименована в
ERREXIT_CLEANUP_VECTORS
.
if (value.dimension() == 0)
"
перетащено из конца в НАЧАЛО, сразу после проверок на допустимость типа.
Так оно будет стройнее.
Кстати, в epics2smth.cpp сделано иначе: там она идёт 3-й
альтернативой после проверки на aitEnumString
и
aitEnumFixedString
, т.к. 1) nelems там работает у строк
числом символов (CX'ный подход) и 2) массивов строк, наоборот, не
бывает. Но тут, поскольку мы хотим в перспективе добавить поддержку
массивов строк (т.к. оные есть и в EPICS, и в Tango), то векторность уже НЕ
ортогональна строковому типу.
17.09.2024: продолжаем.
char *string_buf
" на
"string_buf[sizeof(afs_p->fixed_string) + 1]
"
-- чем аллокировать не более 40 байт, проще иметь под них просто локальный
буфер.
...а сделано это в момент...
24.09.2024: и, вероятно, ЗРЯ -- т.к. для
aitString
, увы, придётся-таки аллокировать/реаллокировать буфер
в динамической памяти. Поэтому локальный буфер переименован из
string_buf
в buf4fixed_string
; причём собственно
спецификация размера заменена на
"buf4fixed_string[sizeof(dbr_string) + 1]
".
25.09.2024: а для "длинных" строк aitString
сделан обычный аллокируемый/растимый буфер в виде парочки
string_buf
+string_buf_allocd
.
И оная конверсия -- после первоначального изготовления в 2 ветках, скалярной и векторной, и после внимательного взгляда на "различия" -- сделана унифицированной:
nelems!=1
...
Таким образом, в случае скаляра просто выполняется конверсия 1шт, а при векторности -- ещё и заполнение всего массива.
nelems!=1
доп.действиями для векторов.
P.S. Приступлено было после попытки найти/узнать "у libCAS/gdd API работы
с МАССИВАМИ строк" (ха-ха!), после чего в src/ca/legacy/gdd/gddI.h
было увидено, что ВСЕ методы gdd::getRef()
де-факто возвращают
результат dataVoid()
, просто кастя его к запрошенному типу.
P.P.S. А в src/ca/legacy/gdd/README в длинном разделе "Still needed:" нашлась запись от 8/28/96:
Modify getRef() functions of gdd class: add another argument that specifies the data format the user desires. default to local data format, first access to an array that is not in the correct format will cause the entire array to be converted to the desired format.
Что, естественно, так и не было сделано и, очевидно, так НИКОГДА И НЕ БУДЕТ сделано (да и нафиг не нужно, честно говоря). И эта несделанность подтверждает верность выбранного решения -- считать, что по добытому адресу просто от 1 до N значений.
25.09.2024: а сегодня переделка вроде доделана -- при
value_type==aitEnumString
оно вытаскивает каждый элемент в
аллокируемый string_buf
(растимый по мере надобности втянутым
сюда GrowBuf()
'ом) и форсит там '\0'.
23.09.2024: как сказано у нас 20-07-2022 по результатам
расследования -- а поле len
, судя по
содержимому aitHelpers.h, ВСЕГДА поддерживается в корректном
состоянии. Так что можно считать, что ответ "да".
24.09.2024: СТОП!!! Там ведь речь про поле ДЛИНЫ, а НЕ
про terminating-NUL! И, судя по осмотру src/ca/legacy/gdd/aitHelpers.cc
и конкретно "главной копировальщицы"
aitString::copy(const char* p, unsigned newStrLength, unsigned bufSizeIn)
-- на которую ссылаются все остальные варианты aitString::copy()
-- НЕТ, НЕ поддерживает: поддерживает он именно len
, а при
передаче "наружу" делает strncpy()
и/или прописывает
'\0'. ВЫВОД: а вот ТУТ надо таки аллокировать
string_buf[]; и, учитывая потенциальную векторность, не просто аллокировать,
а "растить" по мере надобности, в стиле GrowBuf()
.
25.09.2024: да, так и сделано.
DeviceAttribute
: а есть ли там
конструктор, принимающий vector<string>
?
Ответ: да, есть, как и у DeviceData
.
23.09.2024: а хрен поймёт; но, похоже
-- судя по результатам изучения gdd::dataVoid()
от 01-07-2022
-- если вектор, то всегда хранится адрес; соответственно, по этому адресу
будет просто массив aitString
или
aitFixedString
.
Учитывая "качество" кода libCAS/gdd, впрямую статическим анализом
разобраться будет проблематично, поэтому напрашивается путь "сейчас
реализуем в вышеописанном предположении, а потом попробуем проверить вживую
и посмотреть, соответствует/работает ли"; благо, в каком-то из
Tango-VME-устройств был массив строк.
try{}catch()
вокруг ВСЕХ
new()
, плюс проверять все их результаты на ==NULL.
aitEnum2CmdArgType()
, т.е. чтоб если накая TANGO-сущность
НАМИ декларируется как некий EPICS'ный-тип-данных, то чтоб тот тип
конвертировался в ПРАВИЛЬНЫЙ исходно-TANGO-тип.
Простым языком -- чтобы BOOL и ENUM декларировались бы как типы, которые потом преобразуются именно в BOOL и ENUM.
21.09.2024: сделано аллокирование PV, теперь она полем
внутри MonInfo
, инициализируется посредством "placement new".
Только одна засада: так и не решил, в какой момент инициализировать (и уничтожать), т.к. есть 2 варианта:
next
всех новорожденных (а не уничтожать,
соответственно, никогда, т.к. пул только растёт).
Пока сделано по 1-му варианту -- он проще; но с точки зрения "не выполнять лишних действий" разумнее выглядит 2-й.
22.09.2024: ...итого -- получается, что БАЗОВАЯ
функциональность мониторов готова, так что наполнена и
e2t_Server::pvAttach()
. Она тривиальна, поскольку всю работу
делает CheckTangoName()
, так что при успехе оной делается
просто "return mp->the_PV
".
Теперь пора таки делать дОбычу информации, чтобы уже посредством
cainfo
можно было проверять.
26.09.2024: угу, дОбыча сделана --
GetMonProps()
наполнена. ...несколько дней возился с вроде бы
мелочью, сопоставляя информацию в cxsd_fe_epics_meat.cpp,
cda_d_tango.cpp и исходников libCAS и Tango (devapi.h в
первую очередь, т.к. именно там определены типы AttributeInfo
с DeviceAttributeConfig
и DevCommandInfo
).
И её вызовы в методах e2t_PV()
тоже.
27.09.2024: проверяем на pult-l: да,
даёт верные данные!!!cainfo modulatorPS/test/1/Status modulatorPS/test/1/State modulatorPS/test/1/heatingVoltage
28.09.2024: продолжаем:
CheckTangoName()
расширена также
и на команды. ...хотя и не факт, что будут работать.
Да и проверки "такой монитор уже есть!" недостаёт.
29.09.2024@утро, зарядка:
постоянно возникает вопрос -- а как делать поиск по мониторам КОМАНД?
Просто карты составлять -- увы, т.к. под одним именем может быть и команда,
и ответ. Так вот, если бы имена брались не из списков
[...ent_idx].SOME_name.c_str()
, а хранились бы самими
мониторами, то можно б было ПЕРВЫМ СИМВОЛОМ добавлять тип монитора -- просто
символ '0'+chtype
, а то и просто символ
chtype
(NUL'ом он никогда не будет), и вот эту строку и
использовать для сравнения. Увы, именно из-за хранения имён в
списках идея эта не прокатит.
e2t_PV::write()
доведена до компилируемости. Пока в
наипростейшем варианте -- только атрибуты и только скалярные.
Кстати, "имя" -- которое "attr_name", оно же в cda_d_tango.cpp
"ho->chn_name
", для оптимизации и быстроты доступа кладётся
в the_name
-- да, добывается из списка атрибутов по
[mp->ent_idx]
.
offsetof()
.
А вообще эта мысль уже несколько дней как в голове бродит -- нефиг вручную считать смещения, а по смыслу надо просто считать MonInfo 1-в-1 как PV, т.е. -- так и сделать, отнаследовав MonInfo:public casPV (кстати, в доке по libCAS что-то было: что PV могут аллокироваться заранее блоками и выдаваться оттуда поштучно по мере надобности).
29.09.2024: добил в e2t_PV::write()
остальные
3 части -- отправку векторных атрибутов, отправку скалярных и векторных
командами.
Плюс собственно отправка -- и атрибутов, и команд.
Теперь callback'и надо делать.
30.09.2024: однако вчера была проблема при линковке -- ругалось на ненайденность символов, традиционно всякой лабуды (если закомментить в коде один этот оператор, то начинает ругаться на остальные):
что после пропускания через/usr/bin/ld: /home/user/work/frgn4cx/tango/cda/cda_d_tango.o: undefined reference to symbol '_ZN5CORBA3AnylSEd' /usr/lib64/libomniDynamic4.so.2: error adding symbols: DSO missing from command line collect2: error: ld returned 1 exit status
c++filt
превратилось в
(тогда-то я не заметил подсказки про libomniDynamic4.so.2 -- только сейчас, записывая)./usr/bin/ld: /home/user/work/frgn4cx/tango/cda/cda_d_tango.o: undefined reference to symbol 'CORBA::Any::operator<<=(double)' /usr/lib64/libomniDynamic4.so.2: error adding symbols: DSO missing from command line collect2: error: ld returned 1 exit status
Хотя код тот же самый.
Пытался понять, в чём причина -- фиг.
SPECIFIC_LDFLAGS
проблему решило -- теперь собирается.
02.10.2024: по вопросам -- пытался разобраться, читая "man
ld" (проку мало; только намёк на ключ "-rpath-link
", который
может быть при делах) и даже прогнал под "strace -fs200" и "ltrace -fSs200"
3 вида линковки -- cda_d_tango.so, epics2tango_gw с
"-lomniDynamic4
" и без него (и тоже нифига не понятно -- поиск
"omniDynamic4" по логам ситуацию не прояснил никак).
01.10.2024: перетащил "-lomniDynamic4" в
tango/FrgnRules.mk в определение TANGO_LIBS
-- тоже
всё собирается, без проблем.
03.10.2024: приступаем к работе с TANGO-callback'ами.
cda_d_tango_EventCallBack
, назван
e2t_CB
. Методы пока пустые (только диагностическая печать
тоже скопирована).
the_CB
-- прямо объектом (а не
указателем!) прямо в структуре, и
GetMonSlot()
и...
RlsMonSlot()
.
Но тут, если решим перейти на создание PV в момент аллокирования chunk'ов, надо будет также перетащить конструктор в точку после аллокирования.
e2t_PV::write()
раскомментированы вызовы
write_attribute_asynch()
и command_inout_asynch()
,
ранее закомменченные, т.к. им надо передавать какой-то callback-объект,
коего раньше просто не было.
Теперь соображения касательно буферов для дОбычи и возможного хранения данных:
Муторно -- тут придётся на каждый монитор опять иметь по mutex'у, т.к. мы не знаем, в какой момент из какого thread'а кто нас дёрнет (и формально TANGO вроде не обещает, что не придут 2 разных обновления подряд через РАЗНЫЕ thread'ы, так что второе запустится в момент, когда ещё первое не завершилось...).
>>
из DeviceAttribute/DeviceData -- т.е., адреса локального скаляра либо
значения векторовых v_NNN.data()
-- сбагривать в
upd_gdd
и забывать (а дальше уж пусть GDD заботится).
Тогда никакой mutex не понадобится.
installConstBuf()
.
04.10.2024: как, кстати, и делает
cxsd_fe_epics_meat.cpp::mondata2gdd()
.
MonInfo
небольшой буферочек valbuf
размером 8 байт (с double
) плюс указатель
current_val
=NULL с размером current_val_allocd
=0
-- в точности, как в cda_d_tango.cpp (куда это перекочевало из
cxsd_fe_epics.c, только с добавлением "_allocd
"; а
вообще оно в cda_core.c::refinfo_t
давно так).
current_val
, то он всегда и будет использоваться, если даже в
следующих привязках этот MonInfo
окажется направлен на
uint8
.
05.10.2024: кстати, было забыто ВСЁ относительно
mon_pool_lock_mutex
-- и инициализация, и использование в
GetMonSlot()
/RlsMonSlot()
. Исправлено.
06.10.2024: долго думал, что же делать со строками при
ЧТЕНИИ -- как их отдавать, как aitEnumString
или
aitEnumFixedString
?
Для этого надо, чтобы CmdArgType2aitEnum()
для
DEV_STRING
возвращала бы aitEnumFixedString
, а не
aitEnumString
, как сейчас.
caget -S
"); но при отдаче принудительно
fixedString'а свыше 40 будет обрезано...
Или смотреть по конкретному содержимому и отдавать либо так,
либо эдак? Всё равно ведь обе "получательницы" -- условные
store_current_value()
и store_current_valueD()
--
будут ориентироваться на TANGO'вский
data_type
/tango_type
, а не на результат
трансформации CmdArgType2aitEnum()
'ом.
06.10.2024@у Брыксина на
геостанции, ~18:00: в любом случае всё более и более очевидно, что
вместо ПАРЫ
store_current_value()
+store_current_valueD()
нужно
иметь ОДНУ функцию,
DeviceAttribute
и DeviceData
), один из которых
будет "исходником", а второй =NULL
value_p
,value_bufsize
, ...
ЗЫ: а лочить value_mutex
надо снаружи этой функции -- в её
вызывальщике.
07.10.2024: делаем эту ОДНУ функцию сохранения -- пока "рамочные" действия:
store_current_value_to_upd_gdd()
и получает
те самые 2 аргумента в дополнение к MonInfo.
e2t_CB::attr_read()
с e2t_CB::push_event()
и e2t_CB::cmd_ended()
добавлены её вызовы (вся троица с разными
параметрами):
return_current_value()
, тут
the_PV.postEvent()
,
CHTYPE_RESULT
затем будет
делаться уведомление парного CHTYPE_COMMAND
-канала.
mp->val_mutex
.
08.10.2024: upd_gdd
перетащена из
e2t_PV
в MonInfo
.
Но есть какой-то странный косяк при placement-инициализации:
gdd_NEWDEL_FUNC
(определён в gddNewDel.h) в качестве
претендентов.
e2t_gdd
-- см. оный у
fe_epics_gdd
, там он присутствует и весьма замороченный.
operator new
будет
выполняться применительно к нему, а libCAS'овские махинации "NEWDEL" влиять
не будут.
Собственно определение класса вот такое:
...а объявление поля внутриclass e2t_upd { public: e2t_gdd d_gdd; };
MonInfo
--
...так что обращения к нему меняются с "e2t_upd u;
mp->upd_gdd
" на
"mp->u.d_gdd
-- по сути, замена 1 символа.
gdd
", а экземпляр контейнера
-- "upd
", ведь тогда полное имя будет
"upd.gdd
"?
Да, как бы нехорошо "затенять" именем поля имя типа, но C++ это позволяет -- я попробовал.
13.10.2024: да, переделано на MonInfo
'во поле
"upd
" с полем "gdd
" внутри него, так что
"upd.gdd
". Проверено, что такой фокус даже
в обычном C работает без warning'ов (даже при -Wshadow
), во
всём диапазоне от -std=c89
до -std=gnu11
.
Потому и решился.
09.10.2024: продолжаем возиться с наполнением
store_current_value_to_upd_gdd()
:
operator=(ЧЕГО_НИБУДЬ)
" с e2t_gdd
НЕЛЬЗЯ --
начинает ругаться, что не находит, бла-бла-бла...
*((gdd*)&(mp->upd_gdd)) = *(( epicsInt32*)value_p);
вместо просто
mp->upd_gdd = *(( epicsInt32*)value_p);
-- ведь в cxsd_fe_epics_meat.cpp::mondata2gdd()
по
сути так и делалось (туда параметром передаётся gdd *gp
,
указывающий на upd_gdd
(хотя и на обычную gdd
&prototype
, переданную параметром, в
fe_epics_PV::read()
тоже).
Но это криво.
gdd
вместо e2t_gdd
нельзя -- из-за дурацкого protected-деструктора ~gdd()
, что
было выяснено ещё 16-06-2022; потому и выполнено наследование.
Правда, непонятно, почему так "скрываются" (точнее, "затеняются") не
только "copy assignment operator", но и прочие "operator=
".
put()
:
mp->upd_gdd.put(*(( epicsInt32*)value_p));
12.10.2024: за вчера-сегодня доделано наполнение
store_current_value_to_upd_gdd()
:
store_current_value()
и store_current_valueD()
, с
весьма интенсивными заменами -- сплошь ручными, автоматизировать (даже
просто контекстной заменой) там особо нечего:
s_len
вместо nelems
-- в преддверии добавления поддержки массивов
строк.
dtype
" работает "value_type
".
sizeof_cxdtype()
в EPICS/GDD
нет, а оно необходимо при складировании векторных данных в буфер
ограниченного размера, то введена usize
, которую каждый
"case Tango::DEV_nnn
" заполняет самостоятельно соответствующим
"sizeof(epicsNNN)
".
...получается, что самое объёмное (и потенциально трудозатратное) вроде
как допилено. Можно пробовать испытывать, но как минимум чтение -- метод
e2t_PV::read()
-- ещё не сделан, а без него ну никак.
Глобально, нужно сделать следующее:
quality2severity()
, каковую ещё надо
сделать; и severity ли? И как у GDD оно уставляется?
setStat()
и setSevr()
? Вот правда, надо
разобраться в этих "alarm status"/"alarm severity"...
interestRegister()
?).
13.10.2024: движемся.
Поэтому функция-транслятор должна возвращать пару по указателям.
quality2alarm_severity_and_status()
,
используемая ею таблица quality2alarm_table[]
содержит в каждой
строке [quality] ДУПЛЕТ значений.
С трансляцией несчастных 5 вариантов quality в значение status (который NO_ALARM, MINOR, MAJOR, INVALID) всё просто: VALID:NO_ALARM, INVALID:INVALID_ALARM, ALARM:MAJOR_ALARM, CHANGING:NO_ALARM, WARNING:MINOR_ALARM.
А вот что отдавать в severity -- задача нерешаемая. Поэтому для не-VALID
всегда отдаётся READ_ALARM
, а для конкретно CHANGING --
SOFT_ALARM
.
setSevr()
и setStat()
.
MonInfo
инициализируются
GetMonSlot()
'ом в момент аллокирования, а не при
задействовании.
Соответственно, в RlsMonSlot()
более НЕ вызывает
деструкторы.
strcasecmp()
и отсмотра соображений от 13-09-2024
сделано по идее оттуда: конверсия выполняется прямо в момент получения имени
-- в parse_tango_name()
в секции "Perform conversions", вместе
с прочими махинациями с содержимым dup_name[]
.
Теперь надо проверить бы.
17.10.2024: проверил -- фиг.
attribute_list_query()
и command_list_query()
,
имена хранятся НЕ переведённые в нижний регистр, так что поиск при
регистрации PV не срабатывает.
tolower()
убрана, а в
cmp_str::operator()()
перейдено на strcasecmp()
.
strcmp()
в том примере на StackOverflow
рекомендовалось использовать как "std::strcmp()
", то с
strcasecmp()
такое не проходит -- получаем ошибку
epics2tango_gw.cpp:432:14: error: 'strcasecmp' is not a member of 'std' return std::strcasecmp(a, b) < 0; ^
Убираем "std::
" -- компилируется.
...кстати, и strcmp()
БЕЗ "std::
" тоже
компилируется.
И вот в таком варианте с strcasecmp()
-- работает, атрибуты
и команды находятся, указанные любым регистром.
14.10.2024: некоторые соображения (весь дель общался, не закрывая рта, так что аж горло заболело, с ЕманоФедей+ВиталейБалакиным, потом с Греховым и ЧеблоПашей, времени поработать не было, только вечером дома дорвался):
...однако облом: мы ведь НЕ храним строки (точнее, dup_name), а
адресуемся по ent_idx
к соответствующей
NNN_info_list_p
-таблице...
15.10.2024@дома-утро: вдогонку ко вчерашнему п.2 смотрим на текущий исходник:
CheckTangoName()
в случае успешной
регистрации вообще ничего не делает с полученным от
parse_tango_name()
значением dup_name
-- даже
не free()
'ит.
И его вполне можно было бы использовать для ключа в соответствующем глобальном map'е -- чтоб ключ включал и устройство, и имя атрибута/команды, и "@T".
DpxInfo
присутствует троица
attribute_mon_map_p
, command_mon_map_p
,
reply_mon_map_p
, введённая как раз для этих целей.
И её применение выглядит осмысленнее: никакой глобальности (которую пришлось бы лочить при каждой проверке), плюс, как следствие per-DpxInfo-map'ов -- более быстрый поиск.
OK, пилим:
dev_name
и dup_name
при !=NULL
даже в случае успеха -- добавлено.
usize
по data_type
, ...
GrowBuf()
по требуемому объёму.
max_dim_x
у
скаляров.
А так -- надо бы переходить на placement-создание MonInfo
, и
тогда обёртка вокруг upd.gdd не понадобится -- можно будет сразу поле
upd_gdd
иметь.
И с DpxInfo
аналогично -- инициализировать их при
аллокировании chunk'ов. А все _mon_map_p
переделать с
указателей на прямо поля там, без "_p
" (тольво в
RlsDpxSlot()
не забывать делать им очистку).
16.10.2024: ну переделываем идеологию инициализации:
MonInfo
, и DpxInfo
теперь
placement-инициализируются в момент аллокирования chunk'ов.
ЦЕЛИКОМ, а не индивидуальные поля-объекты.
e2t_upd
исчезла, MonInfo'во поле теперь просто "e2t_gdd upd_gdd;
" и все
обращения к просто "upd_gdd
" (ура-ура-ура!).
_mon_map_p
переделаны на объекты-поля.
В RlsDpxSlot()
эти карты просто очищаются вызовами
.clear()
.
17.10.2024: причёсывание:
GetMonSlot()
добавлен параметр
MonInfo **mp_p
-- туда возвращается указатель на монитор, чтобы избежать лишнего обращения
к mon_pool[]
, требующего лочки
mon_pool_lock_mutex
'а.
Чтоб хотя бы при первоначальных запросов клиентов к конкретным именам эта лочка не требовалась.
AccessMonSlot()
теперь окружён лочкой.
Пробуем проверить, как это всё работает.
e2t_PV::read()
, до того совсем
пустую, "return S_casApp_noSupport;
" -- иначе по
caget
'у падало (причём только при -O2
, а при
-O0
-- нет); видимо, какой-то мусор возвращался (в EAX?), к
которому libCAS пытался лезть и йок.
caget
обламывается, а camonitor
зависает навеки, т.к. чтение "не поддерживается", а обновлений не приходит.
Отдельно было проверено, что (по крайней мере в
CentOS-7.3, GCC-4.8.5 20150623) map<>.find()
возвращает
итератор, в котором поле .first
имеет значение ключа из
ЭЛЕМЕНТА КАРТЫ, а не переданное find()
'у.
Это критично именно в случае char*
в качестве ключа:
сравниваются-то СТРОКИ, на которые указывают ключи, но сами ключи могут быть
РАЗНЫМИ указателями, причём указатель, передаваемый find()
'у
при поиске, может указывать вообще на врЕменную переменную. И если далее
требуется в ДРУГОМ map'е -- у нас это
attribute_mon_map
/command_mon_map
/result_mon_map
-- создать элемент с тем же ключом, то необходим именно ключ из ЭЛЕМЕНТА
КАРТЫ, т.к. он (точнее, указуемая им строка) будет существовать всё время
жизни DpxInfo
, а не исчезнет после выхода из данной функции.
Итого -- вроде всё как и должно быть на настоящий момент.
18.10.2024: надо уже заниматься реализацией чтения --
поддержки e2t_PV::read()
.
Предварительные исследования:
attr_read()
-- в классе
AttrReadEvent
(как и для attr_written()
, в классе
AttrWrittenEvent
) -- НЕТ ничего, идентифицирующего запрос --
никакого ID.
Только векторы имён (т.к. запрос мог быть на пачку) и значений
argout
(а в AttrWrittenEvent
и значений нет).
...для чего придётся либо городить второй
класс-наследник-от-Tango::CallBack
, либо прямо в
e2t_CB
ВСЕГДА ЯВНО указывать указателем монитора-"владельца", а
не химичить с offsetof()
.
Тогда надо б было выделять/аллокировать сразу дуплет --
e2t_CB
,casAsyncReadIO
26.10.2024: да, именно так и сделано -- всё прошедшее время это пилил, сегодня наконец вроде допилено.
casAsyncReadIO
) показывает, что там всё ОЧЕНЬ сложно -- по
сигнализированию о завершении операции эти объекты помещаются в какую-то
очередь, в которой живут вплоть до отправки клиенту.
Замороченно там всё очень. Надо всё-таки ещё раз перечитать доки по libCAS, чтоб лучше представлять все их ритуалы и тогда в голове сформируется картинка, как можно всё реализовать.
19.10.2024: читаем документацию --
CAS_Tutorial.pdf и AS_Reference.pdf -- на тему работы с
"асинхронным чтением, т.е., насчёт кода S_casApp_asyncCompletion
и класса casAsyncReadIO
и как с ними работать.
read()
вернуть
S_casApp_asyncCompletion
,
casAsyncReadIO
(или наследника),
postIOCompletion(S_casApp_success,upd_gdd)
.
-- главная непонятка: надо ОБЯЗАТЕЛЬНО класть в объект-2. When operation is ready to complete, write current value into the
gdd
object passed toread()
.. . .
In order to complete the second step for asynchronous read and write operations, the server tool must keep track of the
gdd
object passed toread()
andwrite()
. Forread()
operations, the server tool should write the requested values into thegdd
object, and forwrite()
operations, the server tool needs to write the value contained in the gdd object to the PV. The best and most efficient way for a server tool to keep thegdd
object until the operation is ready to be completed is to reference the object with a pointer, and then callreference()
for the object. Ifreference()
is not called, the object will be deleted after eitherread()
orwrite()
returns.
prototype
, переданный read()
'у, или же
достаточно указать свой GDD-объект, который всё равно 2-м параметром у
postIOCompletion()
("and a gdd
object containing the PV's value")?
casAsyncIO
как
предок всех прочих casAsync*IO
; но в реальности такового класса
НЕ СУЩЕСТВУЕТ и конкретные классы сделаны "с нуля", БЕЗ общего предка.
Это к вопросу об "актуальности" документации.
read()
'ов prototype
(и
reference()
'ить его) -- НЕ НУЖНО: похоже, оно "требуется",
только если в момент возврата "потом" надо знать его внутреннюю структуру.
...а в примерах оно используется как "gdd, в которую пишутся данные", потому и сохраняется (запоминанием указателя).
read()
(хоть
и косвенно, посредством какого-то "Channel") -- в
src/ca/legacy/pcas/generic/casStrmClient.cc метод
casStrmClient::read()
.
pValueRead
(предаврительно "инициализировав" его свежесозданным посредством
createDBRDD()
"нулёвым" GDD, тут же
unreference()
'нутым), ...
smartGDDPointer pValueRead;
и, соответственно, ВСЕ махинации с ним -- только внутри этого файла.
pValueRead
является, по факту, чем-то вроде
глобальной переменной в рамках экземпляра объекта casStrmClient
-- разные методы класса пользуются им, на вид, не шибко понятно.
До чего ж равиольнутые ихние исходники... :-(
new
-- авторы такой вариант предусмотрели (т.к.
casAsync
-объекты НИКОГДА не создаёт сама библиотека, а ВСЕГДА
только программа).
Для удаления они не делают delete
, а вызывают виртуальный
метод destroy()
, сводящийся к "delete this;
" --
при наследовании его можно переопределить на не делание ничего.
postIOCompletion()
освобождать "ячейку с асинхронным объектом"
(в отличие от "ячейки с Tango-callback'ом", который к этому моменту свою
функцию выполнил и более не потребуется).
И когда станет "можно" -- не вполне ясно.
upd_gdd
(переданной libCAS'у)?
Например, сразу после ответа на чтение пришло просто обновление -- что,
нельзя класть туда же? А когда можно?
Видимо, "можно" только после того, как асинхронному объекту вызовут
destroy()
. Или gdd-объекту сделают
unreference()
?
Блин, очередная иллюстрация того, что libCAS сделан халтурно, с нарушением правил модульности/инкапсуляции: вместо того, чтобы забрать данные "себе", они сваливают заботу об их хранении на программу, которая вообще-то ни сном, ни духом о том, когда можно будет перестать хранить (ибо это зона ответственности libCAS'а).
I
-классами (то ли "теневыми", то ли "Intefface",
то ли "Implementation"?) -- например, casAsyncReadIO
'шный
casAsyncReadIOI
: реальную работу выполняет именно последний.
Они есть, похоже, для большинства классов -- это видно по списку файлов *I.*: есть и gddI.h, и casPVI.h, и ещё море.
Ни в .pdf
-документации про них вообще ни слова (видимо,
появились позже, в районе 3.13beta (~1998?)), ни в каких-либо .html
(искал
"egrep -rl 'gddScalarI|gddUtilsI|...' compile/base-3.15.9
")
и ни в src/ca/legacy/pcas/RELEASE_NOTES тоже.
Прямо идеальная иллюстрация к концепции "lasagna code"/"ravioli code".
По результатам "исследования" -- похоже, можно просто брать и делать
чтение примерно как задумано: как минимум, сделать в
e2t_PV::read()
собственно вызовы
read_attribute_asynch()
/command_inout_asynch()
20.10.2024: работаем.
e2t_PV::read()
"втянуто" содержимое
cda_d_tango_req_read()
, после чего переделано для адаптации к
этому файлу.
Никакого сохранения списка casAsyncReadIO
-объектов пока нет
и возвращается пока всегда S_casApp_noSupport
, хотя запрос на
чтение делается.
MonInfo
добавлено поле cmd_other_side
,
только оно тут не индекс, а сразу указатель.
Краткая проверка работы:
camonitor
'е натравливание на этот же канал
caget
'а вызывает приход значения первому.
Status
-- какая-то беда:
пустота отдаётся.
Оно не особо удивительно (учитывая "прямолинейность" работы со строками), но надо будет повозиться с диагностированием.
21.10.2024: проверено через epics'ный плагин --
@t100:epics::modulatorPS/test/1/Status
-- да, приходит пустая
строка. А через tango-плагин --
@t100:tango::modulatorPS/test/1/Status
--
"Can't ping Ceac".
22.10.2024: разобрался -- диагностика показала, что прямо
в store_current_value_to_upd_gdd()
в ветке
Tango::DEV_STRING
складирование выполнялось некорректно: в
условии "не 0 ли байт" были перепутаны s_len
и
nelems
, так что стояло
if (nelems > 0) memcpy(value_p, the_string.c_str(), s_len);
вместо надлежащего
if (s_len > 0) memcpy(value_p, the_string.c_str(), s_len);
-- следствие плохо сделанного перевода длин с nelems
на
s_len
(shame on me!); причём GCC показывал "warning: 'nelems'
may be used uninitialized".
После исправления строки стали приходить -- и caget
'у, и
cdaclient
'у.
21.10.2024: ещё вчера были 2 мысли:
stroftime_msc()
и strcurtime_msc()
.
is_used
и cda_d_tango.c'шное поле in_use
-- чтоб в обоих файлах было одинаково и проще было между ними код
копировать.
И вообще переименовать MonInfo'вское в chtype
-- как
обдумывалось ещё летом (но почему-то нигде не записал).
Так вот:
grep -wl work/{4cx,frgn4cx}/**/*.{c,cpp}(.)
")
решено этого не делать, т.к. у текущих названий есть свои причины:
in_use
-- это стандартное имя поля "используется" в
SLOTARRAY'ях, чьим примером cda_d_tango.c и является.
И там бывает, что это не просто булевское 0/1, а "тип ячейки" -- как в
cda_core.c::refinfo_t.in_use
, с
REF_TYPE_nnn
.
is_used
же -- это идёт из cxscheduler.c, где
аллокирование ячеек trec_t
делается из пула (растущего кусками
по TOUT_ALLOC_INC
ячеек) с быстрым доступом к первому
свободному элементу, чем занимается GetToutSlot()
.
И вот везде, где используется аллокирование из пула, применяется имя
is_used
, что даже удобно для различения.
22.10.2024@~14:30, дорога из Быстронома домой, проходя мими Кутателадзе-4г и по диагональной дороге к Николаева: насчёт чтения -- грызла мысль, как же всё-таки организовать, с учётом, что Async-объекты освобождаются не сразу, а "когда-то потом". При ходьбе от Инженерной вдоль примыкающего к Кут-4г "ангара" осенила идея:
GetTcbSlot()
и GetAioSlot()
;
attr_read()
, а из второго пула -- при вызове ихних методов
destroy()
.
23.10.2024: предварительные размышления:
attr_read()
(1шт/чтение) -- становится ещё более превалирующей.
command_inout_asynch()
, что, в свою очередь, по исполнению
приведёт к вызову cmd_ended()
.
Т.е., "callback'и для чтения" должны иметь этот метод иным.
Что ставит точку в вопросе "один callback-класс или два" -- явно надо делать ДВА РАЗНЫХ.
read()
для командных каналов вообще?":
S_casApp_noSupport
, ибо несколько бессмысленно
пытаться "читать команды" -- их можно вызывать лишь "записью" входных
параметров.
State
и Status
требовать какой-то записи глупо -- они по смыслу именно чисто для чтения.
Соответственно, можно разрешать чтение, если входной тип --
DEV_VOID
и запрещать в остальных случаях.
e2t_rglr_CB
, а для чтения -- e2t_read_CB
.
Похоже, надо вести счётчик отправленных запросов, чтобы при >0 не
делать RlsMonSlot()
, а делать при занулении после поступления
ответа.
...точнее, чтоб нулевое значение счётчика
отправленных-но-ещё-невыполненных запросов было бы дополнительным фактором,
запрещающим физическое удаление PV/RlsMonSlot()
.
Ибо пока вообще никак не решено, когда же таковое удаление вообще предполагается делать.
Приступаем.
e2t_AsyncReadIO
-- ради метода
destroy()
, вызывающего деструктор (ради деструктора предка) и
освобождение ячейки:
virtual void destroy() {this->~e2t_AsyncReadIO(); RlsRioSlot(this);}
(Плюс конструктор, перепасовывающий параметр ctx
конструктору предка; что любопытно, БЕЗ конструктора вовсе оно тоже
компилируется (хотя что вызывает из предка?!), а вот с конструктором без
перепасовывания -- нет, т.к. не находит в предке конструктора с пустым
списком параметров.)
24.10.2024: нифига -- ещё поле next
,
необходимое для организации пула быстрого доступа.
e2t_CB
переименован в e2t_rglr_CB
.
e2t_read_CB
25.10.2024:
e2t_read_CB::cmd_ended()
и
attr_read()
сделаны. Они по-другому получают указатель
mp
(и rio_p
) -- из this
, сразу после
чего исполняется RlsRcbSlot(this)
.
e2t_rglr_CB::attr_read()
удалён, т.к. вызов read_attribute_asynch()
теперь всегда
делается с указанием на read_CB-объект (а не rglr_CB) в качестве получателя.
casAsyncReadIO.postIOCompletion()
и является ССЫЛКОЙ
(gdd & valueRead
), а не указателем
("gdd *valueRead_p
").
If the operation is successfully completed, the status code(bold мой) -- и это всё "объяснение".S_casApp_success
should be passed as the first argument topostIOCompletion()
, and agdd
object that contains the requested value should be passed as the second argument. The server library will create a message containing the value and the status code and put this message on a queue to be sent to the client. If the operation is unsuccessful, the appropriate error code should be passed as the first argument. Agdd
object must be passed as the second argument, but this object will be ignored if the status code is notS_casApp_success
.
rio_p->postIOCompletion(S_casApp_canceledAsyncIO, mp->upd_gdd);
e2t_PV::read()
с должными заказами -- пока нет,
выдохся я на сегодня.
А в нём надо будет корректно отслеживать, что если облом прямо сразу, то
сразу же делать RlsRioSlot()
и RlsRcbSlot()
.
26.10.2024@утро субботы, ещё до
завтрака: таки сделал e2t_PV::read()
.
RlsRioSlot()
-- если уж AsyncReadIO-объект
получен, то он тут же инициализируется, и тогда для сигнализации "облом!!!"
надо делать тот же postIOCompletion(S_casApp_ОШИБКА)
, что и
поставлено в блок RETURN_ERROR
; ...
userStartedAsyncIO
),
но до
"return S_casApp_asyncCompletion
"
дойти не успело из-за ошибки в процессе запроса чтения -- непонятно.
S_casApp_canceledAsyncIO
в надежде, что это сработает правильно (по исходникам libCAS -- опухнешь
понимать, так ли).
Единственный приходящий в голову вариант -- лочить mutex'ом:
e2t_PV::read()
:
rcb_p->my_rio_p
-- в read_CB-объект.
S_casApp_asyncCompletion
.
e2t_read_CB::attr_read()
:
upd_gdd
, ...
rio_p->postIOCompletion(S_casApp_success,...)
, этот
rio_p
был бы уже заполнен.
Обсуждение:
e2t_read_CB::attr_read()
вычитывание значения указателя на AsyncReadIO-объект --
"rio_p = this->my_rio_p
"
-- стоит в самом начале, после чего делается RlsRcbSlot(this)
.
А на случай, когда само rcb_p->my_rio_p
будет иметь право
прописываться в какой-то неопределённый момент, который гарантированно ДО
успешной лочки val_mutex
'а, но, возможно, ПОСЛЕ входа в
e2t_read_CB::attr_read()
-- тогда придётся
вычитывания+RlsRcbSlot(this)
выполнять уже ПОЗЖЕ, ВНУТРИ
критической секции, окружённой локом.
mp->val_mutex
-- ровно в нужных местах:
e2t_PV::read()
-- вокруг запроса чтения,
e2t_read_CB::attr_read()
(и прочих CB-методах)
вокруг складирования значения.
...хотя даже это не факт, что предохранит от проблемы
"postIOCompletion()
вызвано ДО возврата
S_casApp_asyncCompletion
" -- тут всё зависит от корректности
реализации в libCAS, а мы вряд ли как можем повлиять.
...кроме как расшиванием через лишний pipe[], в который писать уведомления "готово", чтобы они приходили гарантированно синхронно. Но это именно то, чего хотелось избежать при создании epics2tango_gw.cpp, работающего "нативным" образом и для libCAS, и для TANGO -- таких "обходных путей", а чтоб всё делалось плюсово и multithread'ово.
Теперь проверять пора.
Попробовал проверить,
caget modulatorPS/test/1/Status modulatorPS/test/1/State modulatorPS/test/1/heatingVoltage
-- работает, да (значения caget
'у
приходят), но как-то фигово и странно:
Status
отображается пустым; ...
camonitor
'у:
caget modulatorPS/test/1/Status
-- т.е., при запросе ОДНОГО канала.
Видимо, надо напичкивать диагностикой и разбираться. (Оно там между разными экземплярами Async-объектов не путается? А вчерасозданные пулы работают корректно?)
...заодно:
destroy()
etc., чтобы
убедиться, что цикл существования AsyncReadIO-объектов таков, как написано в
"документации" и в расчёте на который я всё делал.
read()
попробовать sleep()
в разных
местах, чтобы посмотреть, как оно себя будет вести:
S_casApp_asyncCompletion
-- чтобы увидеть, как libCAS справится
с ситуацией "пришёл ответ на PV, чей read()
ещё не успел
завершиться".
27.10.2024: напичкиваем диагностической печати...
camonitor
, и от позже caget
обрабатываются ОДНИМ
tid'ом.
И pvExistTest(), и PV::read(), и последующие destroy() -- всё в одном.
...и как там что между ними передаётся/синхронизируется -- не вполне ясно.
attr_read()
, а потом 3 штуки
destroy()
-- т.е., раз первое освобождение начинается ПОСЛЕ
последующих за вызвавшим его callback'ом, то явно происходит НЕ прямо
"извнутри" оного callback'а.
Правда, возможно, эта "отложенность" обусловлена тем, что происходят callback'и и освобождения в РАЗНЫХ thread'ах.
sleep(2)
ПЕРЕД отправкой (и ПЕРЕД лочкой!) проку
немного: ну подтормаживает всё, а толку... Запросы-то всё равно ОДНИМ
thread'ом принимаются.
sleep(2)
ПОСЛЕ отправки (и ВНУТРИ лочки): вроде с ситуацией
"пришёл ответ на PV, чей read()
ещё не успел
завершиться" libCAS справляется -- результаты отдаются наверх, хотя по
диагностике видно, что attr_read()
(в ДРУГОЙ thread) прилетает
ещё ДО завершения sleep()'а и, соответственно, ДО возврата из
read()
.
А вот ВНЕ (ПОСЛЕ) лочки -- какое-то непонятное поведение...
Status
) у caget'а
вчера были, видимо, из-за задержек: у него ж таймаут 1sec, и если за это
время не придёт -- ёк.
Сегодня при добавлении sleep()
'ов за пределы 1sec вылезало
всегда и ругалось всегда.
А вот с ключом "-w100
" (ждать 100 секунд) всегда получал
данные без ругательств.
Посмотрел -- нет, всё делается корректно: ещё в
cxsd_fe_epics_meat.cpp был сделан метод
fe_epics_PV::destroy()
, который виртуальный (и в предке
делающий "delete this
") и вызывается libCAS'ом при исчезновении
надобности в PV; сюда он тоже перекочевал при копировании.
Проверено -- добавлением в него диагностической печати -- что вызывается этот метод при отключении последнего использовавшего данную PV клиента (а если был один, потом второй, потом один из них отключился -- НЕ вызывается).
Это даёт идеальный вариант "слежения за жизненным циклом PV":
после вызова оного destroy()
можно считать, что данный MonInfo
боле не нужен и можно химичить с ref_count
и начинать
отсчитывать idle_seconds для подчистки DpxInfo.
(Только СЕЙЧАС "di->ref_count--
" делается в
RlsMonSlot()
, что точно нехорошо -- надо переносить.)
Однако в PDF-документах слово "thread" вообще не встречается.
А "grep -i thread
" по
base-3.15.9/src/ca/legacy тоже мало что даёт -- в casdef.h
есть пара комментариев (в основном про deletion responsibility), а в
остальном глухо. В base-3.15.9/src/ca/legacy/pcas/README же и
вовсе устаревшая инфа: упомянуты некие build/singleThread и
build/multiThread -- которых нигде нет.
fileDescriptorManager.process()
, читай в
select()
-- попросту не в курсе, что нужно предпринимать
что-то.
Тогда эта проблема решается некрасиво, но просто: завести
pipe, читающий конец которого зарегистрировать у
fileDescriptorManager
'а (и просто вычитывать всё до упора), а в
пишущий посылать по 1 байту при каждом обновлении. В
epics2smth.cpp такое уже делается, ключевое слово
"e2s_pipe_fdReg
", и ровно для того же -- в
e2s_PV::update()
.
28.10.2024: тут, кстати, тоже race condition
корячится: если очередное обновление случится уже в момент вычитывания
(вызванного предыдущими), то оное вычитывание и егойный байт тоже вычитает,
а дополнительного "уведомления" не произойдёт; а вот корректно ли
libCAS/fileDescriptorManager
обработает такую ситуацию -- т.е.,
когда "новое" добавится в очередь уже ПОСЛЕ возврата из
select()
'а; т.е., будет ли он выполнять проверку ПЕРЕД
select()
'ом? Помнится, по коду там сложно понять логику.
28.10.2024: ну делаем -- добавляем присылку
уведомлений-для-прерывания-select()
по pipe'у:
e2t_pipe_fdReg
в e2t_creator_pipe_fdReg
.
e2s_pipe_fdReg
,
называя его e2t_update_pipe_fdReg
return_current_value()
, ...
postIOCompletion(S_casApp_canceledAsyncIO,...)
, т.е. вызова
return_current_value()
(который бы отправил байт) после него
нет. (Кроме единственной точки в e2t_PV::read()
, где вызов из
libCAS'ного thread'а и отсылка байта-прерывателя не требуется.)
some byte
" переделан в глобальный
"some_byte_for_update_pipe
".
Результаты испытаний:
cdaclient -d epics::
"
вылезли проблемы:
-- очень знакомо, не так ли?A call to 'assert(this->prim_type==aitEnumContainer)' by thread '_main_' failed in ../gdd.cc line 1779. Dumping a stack trace of thread '_main_':
Надо бы, кстати, epics2cda в аналогичной ситуации проверить -- не тукнется ли так же.
#0 __lll_lock_wait (futex=futex@entry=0x558b39f7a890, private=0) at lowlevellock.c:52 #1 0x00007f63816fb8d1 in __GI___pthread_mutex_lock (mutex=0x558b39f7a890) at ../nptl/pthread_mutex_lock.c:115 #2 0x00007f63817f6daa in epicsMutexOsdLock () from /home/skif/compile/base-3.15.9/lib/linux-x86_64/libCom.so.3.15.9 #3 0x00007f63817f01cc in epicsMutex::lock() () from /home/skif/compile/base-3.15.9/lib/linux-x86_64/libCom.so.3.15.9 #4 0x00007f63817931df in casPVI::uninstallIO(tsDLList<casAsyncIOI>&, casAsyncIOI&) () from /home/skif/compile/base-3.15.9/lib/linux-x86_64/libcas.so.3.15.9 #5 0x00007f63817947f5 in casAsyncReadIOI::cbFuncAsyncIO(epicsGuard<casClientMutex>&) () from /home/skif/compile/base-3.15.9/lib/linux-x86_64/libcas.so.3.15.9 #6 0x00007f6381794501 in casAsyncIOI::cbFunc(casCoreClient&, epicsGuard<casClientMutex>&, epicsGuard<evSysMutex>&) () from /home/skif/compile/base-3.15.9/lib/linux-x86_64/libcas.so.3.15.9 #7 0x00007f6381795bd9 in casEventSys::process(epicsGuard<casClientMutex>&) () from /home/skif/compile/base-3.15.9/lib/linux-x86_64/libcas.so.3.15.9 #8 0x00007f638179b0d1 in casStreamEvWakeup::expire(epicsTime const&) () from /home/skif/compile/base-3.15.9/lib/linux-x86_64/libcas.so.3.15.9 #9 0x00007f63817ffab6 in timerQueue::process(epicsTime const&) () from /home/skif/compile/base-3.15.9/lib/linux-x86_64/libCom.so.3.15.9 #10 0x00007f63817e355d in fdManager::process(double) () from /home/skif/compile/base-3.15.9/lib/linux-x86_64/libCom.so.3.15.9 --Type <RET> for more, q to quit, c to continue without paging-- #11 0x0000558b39897ba4 in main (argc=<optimized out>, argv=<optimized out>) at epics2tango_gw.cpp:2979
camonitor
помимо собственно
мониторирования выполняет также и чтение (в диагностической выдаче вызов
read()
виден), так что на дополнительный возврат обновления по
результату чтения реагирует "не очень хорошо" -- получается двойная выдача в
начале.
Видимо, лучше всё же вызов return_current_value()
из
e2t_read_CB::attr_read()
и
e2t_rglr_CB::cmd_ended()
убрать.
29.10.2024: запись байта вытащена в отдельную функцию
wake_up_update_pipe()
(название по аналогии со всякими
wake_up_interruptible()
из ядра Linux). И вызовы
return_current_value()
заменены на
wake_up_interruptible()
-- и дублирование обновлений в
camonitor'е ушло, и зависания на futex'е прекратились...
Итого, есть 2 проблемы:
assert(this->prim_type==aitEnumContainer)
"; как добиться --
одним из:
cdaclient modulatorPS/test/1/State
caget -d DBR_CTRL_LONG modulatorPS/test/1/State
dbr_ctrl_string
не существует и потому для строковых каналов
cda_d_epics.c свойства посредством DBR_CTRL_STRING
не
запрашивет.)
epics2tango_gw
" -- запустить шлюз;
camonitor modulatorPS/test/1/Status modulatorPS/test/1/State modulatorPS/test/1/heatingVoltage
"
-- запустить монитор.
caget modulatorPS/test/1/Status modulatorPS/test/1/State modulatorPS/test/1/heatingVoltage
"
-- дёрнуть одиночное чтение несколько раз; со 2-го/3-го раза шлюз зависает
(а иногда и с 1-го).
29.10.2024: анализ зависания, что происходит в разных обстоятельствах:
return_current_value()
при обновлениях в ответ на
read()
" зависания прекратились. Даже запуск
caget
'а в бесконечном цикле и без задержки просто молотит без
косяков.
Как будто проблемы возникают
при 2 обновлениях одной PV посредством одной и той же upd_gdd
,
происходящих быстро/сразу друг за дружкой.
А вот с "assert(this->prim_type==aitEnumContainer)
" --
e2t_PV::read()
отсутствовала защитная
проверка
if (prototype.isContainer()) return S_casApp_noSupport;
наличествующая в fe_epics_PV::read()
-- просто пропустил при
копировании.
assert()
'у
прекратились...
caget -d DBR_CTRL_LONG modulatorPS/test/1/State
ругается "Channel read request failed" и в качестве значения показывает
0, ...
cdaclient epics::modulatorPS/test/1/State
правильное значение 8
показывает.
Оно и понятно, почему: cda_d_epics.c использует запросы
DBR_CTRL_nnn
только для получения свойств, а собственно
подписку на значения, как и чтение в cda_d_epics_req_read()
,
оформляет форматом DBR_TIME_nnn
, который libCAS'ом отражается
на обычный скалярный тип GDD (точно НЕ на контейнер!).
DBR_STSACK_STRING
-- а там ведь анонсируется и
отдаётся СТРОКА (aitEnumString
, aitString
),
поэтому никакие проблемы с DBR_CTRL_nnn
увидены быть и не
могли.
Так что надо бы всё-таки провести тестирование работы
cxsd_fe_epics.so
и epics2cda
с запросами
DBR_CTRL_nnn
-- какого рода будут косяки/проблемы. И прочие
виды -- DBR_STS_nnn
, DBR_GR_nnn
-- надо будет
проверить.
epics2tango_gw
показало, что
DBR_STS_nnn
приводит к isContainer()
=0, так что
отдача работает, а вот DBR_GR_nnn
--
isContainer()
=1, с обломом аналогично
DBR_CTRL_nnn
.
DBR_CTRL_STRING
, но и
DBR_GR_STRING
к проблемам не приводит -- очевидно, потому, что
dbr_gr_string
также не существует и в db_access.c есть
комментарии
/* case(oldDBR_GR_STRING): NOT IMPLEMENTED - use DBR_STS_STRING */ . . . /* no struct dbr_gr_string - use dbr_sts_string instead */
epics2cda
c числовыми
каналами -- да, обламываются и DBR_STS_LONG
, и
DBR_GR_LONG
, с одинаковым результатом "Channel read request
failed", "read failed" и "some PV data was not read".
StateChangeCB()
:
12.11.2024: да, там был косячный код, который сегодня переделан.
15.11.2024: протестировано и подтверждено, что старый код работать не мог, а новый работает.
ca_create_subscription()
вроде как позволяет указывать
конкретный DBR_
-тип, и что мешает прямо в подписке указать хоть
DBR_CTRL_nnn
, хоть DBR_GR_nnn
; но:
camonitor
не видно ключиков, которые позволили бы
указать конкретный тип/вид/формат запроса.
interestRegister()
нет параметров
и не видно ничего, что бы указывало на тип
Поразбираться бы в вопросе:
08.11.2024: проверяем на тему
"ca_create_subscription()
со странными типами":
connection_handler()
сам выбирает тип
запроса на основании "нативного" типа:
-- т.е., просто старается всегда использоватьppv->dbfType = ca_field_type(ppv->chid); ppv->dbrType = dbf_type_to_DBR_TIME(ppv->dbfType); /* Use native type */
DBR_TIME_nnn
(с
ENUM'ами далее чуть больше приседаний, но всё равно смысл тот же).
09.11.2024: и оттрассировать с помощью GDB с установкой breakpoint'ов -- фиг, «Cannot reference virtual member function "ИМЯ_ФУНКЦИИ"».
10.11.2024: поэтому просто понаставил
raise(11)
по очереди в e2s_Server::pvAttach()
,
e2s_PV::interestRegister()
, e2s_PV::read
и прогнал
epics2cda
под GDB, делая "bt" после каждого из "вылетов.
Результат в
20241110-epics2cda-libcas-attach-interest-read-gdb-bt.txt в
notes/.
cda_d_epics_new_chan()
.
Итак, при использовании вместо DBR_class_TIME
значений
DBR_class_CTRL
и DBR_class_GR
и натравливании на
epics2cda
с запросом канала "epics::qltr3.imes
":
e2s_PV::read()
говорит "isContainer()=1 isAtomic()=0" и
на "втором" чтении тоже.
NewDataCB()
в диагностике
выдаёт "ARGS.status=152 ARGS.type=27" (27 -- это
DBR_GR_DOUBLE
, а 152 -- похоже, ECA_GETFAIL
).
NewDataCB()
выдаёт уже "ARGS.status=1 ARGS.type=27".
NewDataCB()
-то, конечно, такое обновление игнорирует, т.к.
это НЕ DBR_class_TIME
, ...
...но видно, что libCAS уже САМ из "обычного" GDD составляет "композитный", запрошенный клиентом для мониторинга.
Что ему мешало исполнять то же самое при ответе на чтение?!
(По кодам: "1" -- ECA_NORMAL
, гы-гы! Т.к.
CA_K_SUCCESS
=1, а CA_K_WARNING
=0 -- см. в
src/ca/client/caerr.h макрос DEFMSG()
, используемый в
определении
"#define ECA_NORMAL DEFMSG(CA_K_SUCCESS, 0) /* success
*/
"...)
Живого IOC'а под рукой не нашлось.
08.11.2024: а ещё странно, что
int32-каналы, имеющие {R,D}, как минимум через epics2cda
представляются как целые же (LONG), а не вещественные. Надо б поразбираться
и найти в записях за 2022-й, почему так. А то ведь в обоих есть проверка
"если phys_count!=0 и (REPR_INT || REPR_FLOAT) и !=CXDTYPE_SINGLE, то отдаём
как DOUBLE" -- надо разобраться, почему не работает.
09.11.2024: да, подтверждено -- ТОЛЬКО в
epics2cda
(и прочих БРИДЖАХ, использующих cda_d_cx).
11.11.2024: вроде проблема решена.
Таковая таблица может быть тривиальной -- просто список {alias,реальное_имя}, и храниться в текстовом файле построчно.
Но для ИСПОЛЬЗОВАНИЯ потребуется некоторая обвязка -- чтоб считанная из файла таблица хранилась в удободоступном виде. Напрашивается отдельный модулёчек, годный для использования любой из таких программ. Назовём его "alias_list". Набор закладываемых в него идей:
alias_list_t
, который создаётся, потом
при чтении в него добавляются пары {key,value}, а при использовании можно
спросить "есть ли у тебя в словаре строка с таким-то key" (в роли которого
выступает приходящее pvExistTest()
/pvAttach()
значение pPVAliasName
), а он в ответ вернёт значение-target
alias'а, которое следует использовать взамен спрошенного.
realloc()
, но не на GrowBuf()
, а с собственной
реализацией.
alias_list_item_t
,
содержащий пару указателей, но аллокируется на обе строки ОДИН буфер, в
который они кладутся друг за дружкой,так что при удалении всего делается
лишь free(->key)
.
Но в начальной реализации на это можно забить и сделать по-простому, линейно.
Поэтому при создании объекта alias_list_t
указывается пара
параметров: key_case
и val_case
--
case-sensitivity для key и val соответственно. Первый используется при
поиске (и при проверке на дубли), а второй -- в случае дубля для проверки
совпадения значений (если совпадают -- то фиг с ними, если разные -- то есть
варианты).
Пусть пока живёт в work/frgn4cx/gw/epics2smth/, в alias_list.{h,c}.
22.04.2023: за вчера-сегодня минимальный вариант (без сортировки, с добавлением в конец и линейным поиском) написан, но пока даже не проверена компилируемость. 25.04.2023: проверена -- точнее, сделана (было несколько опечаток).
22.04.2023: а ведь epics2tango{,_gw}, видимо, понадобится указывать в командной строке "адрес сервера" -- начальный URL, по которому коннектиться к Tango-БД.
24.04.2023: ещё с позавчера вертелось в голове "но ведь могут понадобиться не просто alias'ы 1-в-1, а более сложные конструкции, a-la rewrite-rule", и вот в tech-talk'е встретилось "Re: question about ALIAS" за вчера, напомнившее, что там в "Gateway: The Process Variable Gateway" (aka "ca gateway"?) как раз нужное имеется.
Собственно: можно же использовать тот кусок у себя. Можно ОПЦИОНАЛЬНО -- если указано переменной в Makefile (или из командной строки).
А ещё оттуда очевидно следует другое название для epics2tango_gw -- cagateway2tango (по аналогии с cagateway).
25.04.2023: теоретически можно б было на основе этого "CA gateway" делать бридж в Tango. Но как-то стремновато.
С другой стороны, если существует "PVA Gateway" (pva2pva?) -- то вот он может быть хорошим вариантом для изучения работы с PVA, а также на его основе можно и "pvgateway2tango" сделать.
std::map
(а НЕ std::hash
!).
Т.е., если держать в хэш-таблице целочисленные ID ячеек, то объявлять её надо как
std::map<char*,int> dpx_pool_map;
...ну или хранить в карте сразу дуплеты {ID,pointer}.
22.08.2024: ну собственно "знания":
contains()
.
Но засада в том, что он "C++20".
И я проверил -- в CentOS-7 его нет: и в /usr/include/c++/4.8.2/**/{map,map.h} нет, и при компиляции того примера выдаётся ошибка
error: 'class std::map<int, char>' has no member named 'contains'
Вот такой дебилизм: в PERL'е функция exists()
была с
незапамятных времён, а авторы C++'ного контейнера о такой базовой вещи
попросту забыли!
...там есть какие-то кривые способы: метод
find()
,
который так-то возвращает ИТЕРАТОР, а при ненайденности -- "past-the-end (see
end()
)
iterator is returned".
Ещё пара нюансов:
std::map
есть ещё std::unordered_map
,
который может быть быстрее, т.к. не требуется упорядочивание элементов.
Т.е., объявлять
std::unordered_map<char*,int> dpx_pool_map;
В остальном же оно вроде совместимо, включая косяк с "contains
только C++20".
if (m.find("f") == m.end()) { // not found } else { // found }
m.count(key) > 0 m.count(key) == 1 m.count(key) != 0
P.S. Там и сказано в комменте от 19-03-2019
If you don't need the elements to maintain a specific order, use std::unordered_map, which has near-constant look-ups and can be very beneficial when storing more than a few pairs.
P.P.S. И ещё в том же комменте сказано
If you want to use the value if it exists, store the result of ::find and use the iterator to prevent 2 look-ups:
auto it = m.find("f"); if (it != m.end()) {/*Use it->second*/}
После этого становится понятнее причина изначального отсутствия метода
contains()
-- горе-авторам в голову не приходили сценарии,
когда надо ТОЛЬКО проверить наличие.
...но, кстати, формально мы можем пользоваться тем же паттерном при получении запроса на наличие имени:
dpx_pool_lock_mutex
;
it=dpx_pool_map.find(key);
it!=dpx_pool_map.end()
залочить уже найденную
ячейку, "зарегистрировать канал" в ней (как -- зависит от состояния ячейки:
работает или создаётся);
dpx_pool_lock_mutex
.
28.08.2024: насчёт синтаксиса объявления итераторов -- описано в "Typedef a Vector Iterator" на StackOverflow от 10-02-2012, в первом же ответе -- typedef'нуть тип-контейнер и потом от того typedef'а typedef'нуть итератор:
typedef std::vector<std::string> string_vector; typedef string_vector::iterator str_vect_itr;
23.08.2024: попробовал сделать
-- фиг:typedef struct { DpxInfo *ptr; int ID; } MapInfo; static std::unordered_map<char *, MapInfo> dpx_pool_map;
epics2tango_gw.cpp:63:8: error: 'unordered_map' in namespace 'std' does not name a type static std::unordered_map<char *, MapInfo> ^
А вот просто "map
" --
-- собирается, даже без указания "static std::map<char *, MapInfo> dpx_pool_map;
LOCAL_CXX_CPPFLAGS=-std=c++11
".
Почему такая фигня -- неясно: с одной стороны, если вместо
"unordered_map
" написать любую чухню вроде
"unordered_mapZ
", то эффект тот же -- т.е., как будто просто
нет такого контейнера. С другой стороны, в /usr/include/c++/4.8.2/
куча файлов unordered_map{,.h} присутствует.
08.10.2024: возможно, потому, что, как сказано в
"What is the best way to use a HashMap in C++?",
"The unordered_map
container was introduced
with the C++11 standard revision. Thus, depending on your compiler, you
have to enable C++11 features (e.g. when using GCC 4.8 you have to add
-std=c++11
to the CXXFLAGS).".
01.09.2024: к вопросу о возможности использования в
map
'ах строковых ключей в виде const char *
--
char*
как "сравнивай их значения (а при создании --
аллокируй строки)".
You need to give a comparison functor to the map otherwise it's comparing the pointer, not the null-terminated string it points to. In general, this is the case anytime you want your map key to be a pointer.For example:
struct cmp_str { bool operator()(char const *a, char const *b) const { return std::strcmp(a, b) < 0; } }; map<char *, int, cmp_str> BlahBlah;
Кто их будет дублировать (ведь при создании это просто указатели, лежащие чёрт-те-где -- иногда в стеке, иногда в динамической памяти...
Видимо, нужно самостоятельно -- понимая, когда значение реально
"временное" и требуется strdup()
, а когда оно и так уже
где-то складировано и можно просто тот же указатель оставить (благо,
элементы map
'ов ТУТ создаются парами с элементами векторов, где
о хранении строк позабочено заранее).
Мда, и близко совсем не PERL'овские хэши...
09.09.2024: насчёт "а как бы всё-таки использовать
более оптимальный unordered_map
?" -- ввёл
#define
-символ MAP_TYPE
:
и далее везде "#ifndef MAP_TYPE #if 0 #define MAP_TYPE unordered_map #else #define MAP_TYPE map #endif #endif
std::map
" заменено на
"std::MAP_TYPE
".
Потом как разберёмся, где МОЖНО использовать unordered-вариант -- заменим
в #if
'е 0
на соответствующее условие.
.VAL
, которое также можно указать
явно, а можно указывать другие поля, вроде разрешённых минимума/максимума,
метки, единиц измерения и прочих EGU.
Учитывая, что в TANGO многие их таких свойств/сущностей тоже есть (в
AttributeInfo
, а точнее -- в его классе-предке
DeviceAttributeConfig
), то теоретически можно бы обращение к
ним поддерживать:
Тут пара замечаний.
DeviceAttributeConfig
-- string
'и, в то время как по смыслу они ЧИСЛА (всякие
min_value
/max_value
).
Ну да, тут надо будет выполнять strtod()
или
strtol()
.
...кстати, нечто подобное задумано (но пока НЕ реализовано) в
cxsd_fe_epics.c -- там для этого предусмотрено "kind
",
который при
"kind !=MON_KIND_VAL && kind != MON_KIND_RAW
"
несколько меняет поведение.
Очевидно, тут тоже понадобится заводить нечто вроде "kind" и соответствующим образом менять поведение.
...либо вводить дополнительные CHTYPE_
-коды,
чтобы при is_used>=CHTYPE_first_artificial
поведение бы
менялось.
И да, похоже, что "изменение" сведётся, в частности, к тому, что запись
будет просто запрещаться, а чтение будет отдавать значение соответствующего
поля из AttributeInfo
(а вот для CommandInfo
-- НЕ
надо, там даже description
нету).
18.09.2024@спускаясь по лестнице с 5-го этажа после терминалки: как это можно было бы реализовать:
parse_tango_name()
--
так себе идея: во-первых, нарушение разделения обязанностей, а во-вторых,
эту функцию ведь предполагается заюзать и в cda_d_tango.cpp, куда
такое совсем не к месту.
sl3
; ...
CheckTangoName()
--
пусть решает, как поступить с этой информацией.
18.09.2024@дома: делаем
минимальную подготовку в parse_tango_name()
:
char **dot4_p
" для возврата в нём указателя.
Чуть позже: учитывая, что к тому моменту там (после замены!) будет уже не '.', а '/', название выглядит misleading; но уж ладно.
sl4
для сохранения той самой позиции;
=NULL
, а при
nsls==4 в него запоминается оная позиция.
*dot4_p=sl4
".
subscribe_event()
скорее всего является блокирующимся. Тогда
получается, что если сначала к устройству успешно приконнекчено, пото связь
рвётся и тут прилетает запрос на ещё один атрибут, то шлюз зависнет. И
избежать этого нет никакой возможности: ведь участвующий в этом экземпляр
DeviceProxy
уже используется в основном потоке и подписку с его
применением ну никак нельзя утаскивать в отдельный поток-"подписыватель".
(Мысль пришла при написании куска 20241105-WHY-I-HATE-TANGO.txt -- при составлении списков операций, которые могут быть асинхронны, а которые НЕ могут; и тут сообразил, что подписка, скорее всего, тоже синхронна...)
31.12.2024: немножко обсуждения на тему "как проверить, действительно ли проблема есть".
subscribe_event()
добавить sleep(10)
, в течение
которого выдернуть Ethernet-кабель и посмотреть (диагностической печати
напихать) "диаграмму исполнения" -- будет ли подвисать на 3*N секунд (тогда
грустно) или вызов завершится сразу же (тогда удивительно хорошо).
17.07.2015: проверен -- а вроде работает, СРАЗУ же.
22.09.2015: только жутко неудобен парсинг адресов из auxinfo -- оно "вручную" идёт по строке, выделяя токены. Вот если б был API, аналогичный argv[argc] или argp()/nargs в v2 и argp[nargs] в remsrv_drvmgr.c
04.07.2023: его возможности расширены: теперь он мультиплексирует запись не только INT32-скаляров, а ПРОИЗВОЛЬНЫХ типов.
mux_write_rw_p()
теперь по типу
данных не фильтрует (как большинство драйверов железа), а просто сбагривает
переданные ему {dtype,nelems,values} cda_snd_ref_data()
.
...но ВОЗВРАЩАЕТ при этом (и при чтении тоже) всё-таки просто INT32=0.
Что и сделано, с добычей типа посредством
CxsdHwGetChanType()
.
P.S. Было подозрение, что должно сработать и полиморфное указание
UNKNOWN-типа, вроде "w1x100
" -- тогда ссылка зарегистрируется
такой полиморфной и будут приниматься данные любого типа. Но вряд ли --
ведь каналы записи типа UNKNOWN у нас вроде не поддерживаются (ни в сервере,
ни в cda).
Проверено -- работает.
(А понадобилось для современной версии "маленькой" сварки
(lhebwcc.subsys, devlist-tower-47.lst), где weldproc
приходится натравливать на ДВА разных CEAC124, и если каналы данных там
просто cpoint'ами распихиваются на разные устройства, то управляющие
do_tab_*
приходится раздваивать посредством mux_write, а
конкретно out_tab_times
ещё и векторный w2i
, вот и
потребовалось расширить возможности мультиплексора.)
29.03.2024: ещё тогда, 06-07-2023, в devlist-tower-47.lst оно было добавлено, но НЕ проверено; а строчка была с ошибкой --
dev mux_out_tab_times mux_write w2i - mag1_a.out_tab_times mag1_b.out_tab_times
-- "w2i
" вместо "w1i2
, и потому команда записи до
мультиплексора в принципе доходить не могла, т.к. запись не-скалярных
значений в скалярные каналы отсеивается в IsCompatible()
.
А проявилось сегодня, уже на сварке для Улан-Удэ, где devlist-ulan-ude-els-51.lst является копией и содержит ту же опечатку: процесс "сварки" попросту не работал, ВСЕГДА завершаясь за ~1с, т.к. не срабатывала активация таблиц, в out_tab_errdescr значилось "ACTIVATE: no table (times are empty)".
Опечатки в обоих конфигах исправлены, они розданы на tower и ulan-ude-els, дальше будем смотреть.
01.04.2024: попробовал -- да, уставление времён теперь работает и процесс "сварки" бегает, но вот сами времена кривы: ставятся {0,0}.
После некоторого размышления стало очевидно, что проблема -- в {R,D}:
mux_write_drv.c регистрирует каналы БЕЗ флага
CDA_DATAREF_OPT_NO_RD_CONV
, так что ему-то "сверху" от
weldproc'а приходят числа в правильных микросекундах, но вследствие лишней
конверсии они ПОРТЯТСЯ.
Посмотрел на применение mux_write: да везде он используется исключительно
с командными каналами -- включение/выключение источников спектрометра на
canhw:11, раздача progstart всем GID25 и т.п.; и только конкретно на этих
сварках есть целевые каналы с наличествующими {R,D} -- вот эти
out_tab_times
.
Возможные решения:
02.04.2024: но это не прокатит по той же причине, что и п.3.
mux_out_tab_times.0
"обратный" коэффициент, чтобы происходил лишний "нейтрализующий" пересчёт.
Но это очевидным образом не поможет, т.к.
SODC_TAB_TIMES
регистрируется БЕЗ флажка VDEV_DO_RD_CONV
, так что
дополнительный коэффициент не будет иметь эффекта.
02.04.2024: продолжаем.
@лабораторный круглый стол, ~10:00: а можно использовать гибридное решение: и RD-конверсию отключить, и коэффициенты целевого канала наверх отдавать.
(Вопрос, конечно, от КАКОГО из целевых каналов отдавать коэффициенты... От ВСЕХ?)
Вообще-то прочие всякие именно так и поступают: и сами работают без конверсии (чтобы передавать данные один-в-один), и коэффициенты целевых каналов отдают (чтобы "человеческим" клиентам -- cdaclient/скринам -- отдавать данные).
Ну делаем:
CDA_DATAREF_OPT_NO_RD_CONV
в
регистрацию каналов.
12.04.2024: проверяем: да, NO_RD_CONV помогает.
Сначала никак не мог понять -- но КАК была устроена неработающесть при включенной {R,D}-конверсии, почему в итоге приходило значение 0?! Потом допёрло (и экспериментально проверил): если отправить число "123", то в результате получится "123000000" -- т.к. при отправке УМНОЖИТСЯ на миллион. И когда отправлялось значение 5000000 -- 5 секунд микросекундами -- то после домножения ещё на миллион становилось пять МИЛЛИАРДОВ, т.е., переполнение, так что в результате и получался ноль.
13.04.2024: на ulan-ude-els и tower обновлённый драйвер роздан, будем смотреть.
16.04.2024: проверил -- да, работает как надо, проблема ушла.
Мысль: а может сделать специальный драйвер "formula", который бы вызывал для писуемого в него числа указанную в auxinfo cda-формулу, чтоб она уж распределяла входное значение по каким надо каналам как надо.
Главная проблема: что кроме "пишущей" (она проста) нужна еще "читающая" формула -- причём ОБЯЗАТЕЛЬНАЯ (пишущая -- опциональна). Но у формул нету "evproc'ов по обновлению входящих в неё getchan-каналов". Вводить какой-то механизм для этого -- какой и как?
24.09.2015: тогда (вчера@подземный переход в 13-е здание) были какие-то мысли на тему "оснастить и формулу возможностью evproc'ов!".
Сейчас другая мысль: проходиться по формуле, по её псевдо-коду, беря от
всех OP_GETCHAN
ссылки на каналы и делая этим .arg.chanref'ам
cda_add_dataref_evproc(, CDA_REF_EVMASK_UPDATE, Some_cda_f_fla_proc, fla)
Для множественно-встречающихся в формуле каналов сама cda не даст сделать
дублирование evproc'а, оставив лишь первый.
Какой из двух вариантов выбрать? Или это две части одного механизма?
...в любом случае, придётся добавить формулам механизм регистрации
evproc'ов. А, нет -- прикол в том, что в cda_add_formula()
параметры evproc,privptr2 уже есть!
18.12.2015: сделал в cda всё требуемое.
cda_fla_p_rec_t
-- добавлены методы
add_evproc()
и del_evproc()
.
OP_GETCHAN
(но НЕ OP_PUTCHAN
!).
cda_add_dataref_evproc()
и
cda_del_dataref_evproc()
стоят проверки, что если REF_TYPE_FLA,
то вместо обычной махинации со списком RefCb вызываются соответствующие
методы формулы.
Это несколько отличается от варианта за 24-09-2015 с
evproc,privptr2=Some_cda_f_fla_proc,fla -- тем, что события в качестве ref
будут передавать ссылку на реальный канал, а не ref формулы. Но это вряд ли
будет проблемой, т.к. "правильное" использование будет ориентироваться на
privptr2
, а не на ref (кстати, cdaclient.c именно
таков).
cvt2ref()
для SRC_IS_FLA всегда
передаёт evproc=NULL.
Теперь надо делать собственно formula_drv.c и на нём проверять.
19.12.2015: да, он сделан и на нём проверено -- работает. Обсуждение:
Правильно было бы обычным образом регистрировать обработчик в ref'овом cb_list, после чего у формулы регистрировать ссылку на какой-нибудь внутренний обработчик (cda_core'овский или cda_f_fla'шный), который бы уже вызывл зарегистрированные в cb_list обработчики с ref=ИДЕНТИФИКАТОР_ФОРМУЛЫ.
r
и
w
.
IS_AUTOUPDATED_YES
, чтоб он
обновлялся просто по событиям обновления подстилающих каналов.
Поэтому принят следующий подход:
0.0
-- тем самым "formula" считается более гибким (и
вещественным) вариантом mux_write.
20.04.2017: попробовал. Как бы работает, но всё несколько неоднозначно.
Но есть проблема: при раздаче числа-суммы в 2 канала происходит ДВА побновления: сначала возвращается сумма по изменению 1-го канала, а потом 2-го. Так, если предыдущая сумма была 500 (250+250), а потом записывается 100 (50+50), то сначала "покажется" значение 300 (50+250), и только потом 100.
Впрочем, для целей конкретно double_iset это вполне нормально -- ну уж как успели устройства отработать, так оно и есть. И, собственно -- вполне в общем стиле работ "формул, зависящих от нескольких каналов" вообще.
Главная проблема в другом -- см. далее.
IS_AUTOUPDATED_YES
и запросы на чтение игнорируются, есть дурацкий зависон при тестировании
cdaclient'ом.
Суть в том, что если f-канал никогда не был прочитан (а он изначально НИКОГДА не будет прочитан, т.к. обновляется только по обновлению каналов, входящих в r-формулу), то cdaclient никогда не сделает запись, ибо будет сначала ждать поступления значения (как сигнала об установлении связи).
Исправить ситуацию можно, лишь записав что-то в один из подстилающих каналов.
21.04.2017: очевидно, что причина -- лишь в
игнорировании чтения (в результате начальное вычитывание не срабатывает), а
AUTOUPDATED'нутность ни при чём -- на каналы записи она не влияет вовсе, что
хорошо видно в cxsd_hw.c::ConsiderRequest()
.
Очевидно, надо сравнить условия возврата (когда UPDATE, а когда CURVAL) в cda_d_insrv.c и cxsd_fe_cx.c -- последний, очевидно, работает корректнее.
...несколькими минутами позже: попробовал... Они разные, надо б подразобраться и унифицировать (и заодно убрать всёзапутывающие #if'ы).
21.04.2017: (вечер пятницы, дикая усталость после разбирательства с нерабочестью ql15 и прочим дурением) драйвер formula плохо подходит для натравливания на него iset_walker'а, т.к. работает в вещественных, а не в микровольтах, как тот.
22.04.2017@утро: это можно скорректировать r'ом cpoint'а. Хотя это и криво (надо б как-то чтоб r учитывался автоматически, берясь от target'а).
05.10.2018@утро-ljvf: да нет -- очевидно же, что это драйвер iset_walker неадекватен для double-каналов, ЕГО надо адаптировать.
11.10.2018: потребность:
...также нужно для fastadc-клиентов, работающих в других серверах, уметь прописывать значение для канала "xs per point" аналогичным образом. Но это будет делаться уже в ТЕХ серверах, и все fastadc-клиенты будут натравливаться на один (per-server) общий канал.
Соображения:
Сделан, trig_exec_drv.c, буквально за полчаса-час. Детали:
Вместе с предыдущим пунктом это позволяет решать задачу элегантно, разделив её на части:
_rw_p()
у
него отсутствует.
Протестируем в процессе заюзывания.
31.10.2018: некоторые соображения по использованию (родились не сегодня, а на днях, но надо б записать):
А именно КЛИЕНТЫ должны мониторировать интересующее их.
Следствие: надо в "клиентских" ветках, выполняющих запись, в триггеры
включать зависимости от каналов _devstate всех клиентов (т.к. эти
каналы имеют is_autoupdated
=1, то обновляются только по смене
состояния). Делать это надо так:
getchan DEVNAME._devstate; pop;
причём список клиентских устройств можно иметь именно списком, по которому
делать foreach()
(и раздавать по нему же).
01.11.2018: хотя, если подумать: ведь осциллографы устроены иначе -- там не в устройства пишется значение "xs_per_point", а pzframe-клиенты натравливаются на нужный канал; так вот, можно просто всем заинтересованным клиентам указывать прямо "ext_xs_per_point=КАНАЛ_С_НАНОСЕКУНДАМИ".
01.11.2018@вечер-дома: а если подумать еще лучше: зато все Д16 устроены именно так -- им нужно в канал Fin/Fin_ns записать частоту/период, и сами устройства могут рестартовать (при перезагрузке крейта). Поэтому с ними нужно обращаться так, как придумано 31-10-2018 -- использовать их каналы _devstate в триггере.
01.11.2018@утро-дома: формально, можно бы драйверу и дать пару каналов -- "r1d,w1i":
CX_VALUE_COMMAND
вызывала бы срабатывание.
По здравому размышлению первый выглядит потенциально полезным, а вот второй -- очень сомнительным (ибо ломает схему работы).
01.11.2018@вечер-дома: делаем первый вариант devlist'а с trig_exec'ами -- добавляем в devlist-canhw-19.lst:
Там определен список D16S_LIST
, который используется
foreach()
'ем и в формуле-триггере, и в формуле-исполнителе.
02.11.2018: проверяем.
Проблема оказалась в cda_f_fla_p_create()
, где отсутствовала
проверка результата регистрации канала. Исправлено.
Дело в том, что запись туда уходит, и, в случае insrv::, даже мгновенно отрабатывается, но вот возврат записанного значения обратно к cda идёт по insrv-дескриптору и отработается ПОЗЖЕ, а пока что у cda есть лишь старое значение.
Так что -- нужно делать локальную копию; например, через %-каналы.
19.12.2018@вечер-лыжи: основная архитектура продумана; для целостности изложения цепочки мыслей её описание находится в том же разделе "Параметризованные запросы" за сегодня.
21.12.2018: приступаем к реализации.
CxAnyVal_t
, плюс хранится его dtype.
Причина -- чтоб регистрировать каналы сразу при парсинге. Иначе пришлось бы парсинг выполнять в 2 стадии (проход #0: собираем список; проход #1: регистрируем); так тоже можно, но особого смысла не видно.
_init_d()
, заведующая аллокированием.
Пока БЕЗ парсинга и регистрации.
_rw_p()
-- полностью. Включая:
27.12.2018: с 25-го пилил оставшееся (в первую очередь парсинг), и по результатам приёдено к выводу: необходимо изменить модель данных: аллокировать не вместе с privrec'ом, в его конце, а отдельным буфером.
Переделываем...
...через час: готово.
Далее:
CXDTYPE_INT32
(чтоб при начальном вычитывании серверу не отдавалось UNKNOWN, с которым ему
неясно, что делать).
...но там стоит проверка "а работаем ли мы уже" (по data_buf!=NULL), т.к. insrv::-каналам событие будет отдаваться прямо при регистрации, еще ДО завершения cda_add_chan()'а, а в этот момент ещё даже количество каналов неизвестно (и, соответственно, их номера) -- так что рано передавать данные.
ReturnOutRDs()
.
Блок парсинга даже уже сейчас выглядит просто ужасно.
Теперь тестировать надо.
Парой часов позже: потестировал, после исправления мелкого ляпа (почти опечатки) в парсинге -- работает! И калибровки туннелирует.
28.12.2018: "внедрено" в конфиг rfsyn'а, поставлено на пульт -- ждём проверки.
29.12.2018: проверено на живой установке -- работает. Ура!
Засим считаем за "done", но в будущем может понадобиться добавить фич:
Т.е., в дополнение к каналу "selector" (который "activate_preset_n") еще канал "read_preset_n".
08.02.2025: вчера вечером пришла в голову идея "а давай сделаем возможность вытянуть из аппаратуры текущие данные в пресет" -- независимо от записанного 29-12-2018, просто ЕманоФедя упоминал, что это было бы небесполезно. Ну сегодня и сделал.
cda_get_icval()
.
Задействован под это канал номер 1. Только внутреннее
название канала не "READER", а "STORER" -- для унификации с
frolov_d16_drv.c, у которого канал называется
STORE_TO_PRESET_N
, унифицированно с
ACTIVATE_PRESET_N
.
...кстати, дата на файле была 28-12-2018 -- с тех пор так и не менялось.
Теперь проверять, хотя бы на симуляции.
19.02.2025: проверил, на свежесделанном sw4cx/configs/test-devlist-preset_selector.lst -- там noop'ом имитируется кусочек от D16 (каналы A), а preset'овы каналы cpoint'ами получают те же имена, что у D16 его родные пресеты.
Результат -- да, работает.
16.02.2020: и тут тоже давно назрело level4-списочек для отдельных драйверов -- делаем.
16.02.2020@дома-воскресенье: делаем:
Но затем оказалось, что ТУТ всё намного проще. В частности,
Аналогично и dtype с max_nelems.
map[]
очень простая, это
просто массив dataref'ов.
В результате драйвер получился довольно несложным -- фактически он просто жонглирует данными, дёргая готовые интерфейсы.
CDA_DATAREF_ERROR
.
CxsdHwGetChanAuxs()
--
добыть значение drvinfo. Если оно пусто -- то переходим к следующему
каналу.
CxsdHwGetChanType()
.
...нет, вот STRSCHG -- НЕ заказываем.
data_evproc()
: за основу взят оный из
trig_read_drv.c -- точнее, он просто скопирован, с минимальной
необходимой адаптацией.
Передача полученных от cda данных крайне проста -- они тривиально футболятся наверх.
bridge_rw_p()
просто сбагривает переданное ему,
посредством cda_snd_ref_data()
.
17.02.2020: проверяем.
Решение -- убираем флажок CDA_DATAREF_OPT_NO_RD_CONV
плюс
никак не отдаём наверх данные от события CDA_REF_R_RDSCHG
.
12.04.2022: отсутствие
CDA_DATAREF_OPT_NO_RD_CONV
аукнулось -- при бриджевании
CX-каналов. Поэтому подсматриваем из mirror_drv.c поддержку
префикса "@." (но с переводом на булевскость вместо маски), только
ещё с возможностью глобального его указания.
После этого числа в бридж-каналах стали совпадать с исходными.
Ну всё -- теперь пора его задеплоивать на ВЭПП-4. При этом, возможно, вылезут дополнительные требования. Например, возможность указывать @-префиксами к именам каналов флажки для cda -- вероятно, потребуется что-то для cda_d_epics, чтобы должным образом со строковыми каналами работать.
По смыслу это именно "gateway". Но мне этот термин не нравится (и похожестью на "getaway" (всего-то 2 гласных поменять местами!), и...).
Ну и как его переименовывать? Может, в "import"? Даже длина останется те же 6 букв.
Да, явно недоработка в ChannelAccess -- отсутствие отдельного события "текущее".
Хотя то, что timestamp даётся ТЕКУЩИЙ, а не реальный -- его удивило.
В результате клиенты, приконнектившиеся к такому бриджу, "висят", не видя обновлений, хотя в реальности каналы-то все ИСКУССТВЕННЫЕ, и их спокойно можно отдавать клиенту как есть.
Т.е., таким каналам можно ставить IS_AUTOUPDATED_TRUSTED
.
Для чего придётся использовать какой-нибудь @-префикс.
Напрашивается использовать "@!": и по смыслу это восклицание "ага!", и сам символ ни на что другое нельзя применить, поскольку для shell'а восклицательный знак имеет свой смысл (history).
09.03.2020@дома, перед сном в районе 23:30: делаем.
cvt2ref()
и ещё более упрощён.
is_trusted
=1, которая изначально =0.
SetChanReturnType((,,,IS_AUTOUPDATED_TRUSTED)
.
10.03.2020: проверяем. Был косяк -- в персинге не
делалось ++
после успешного восприятия символа (сейчас только
'!'). После исправления -- да, работает как надо.
17.02.2021: чуток больше конкретики:
...вот где-то уже как-то что-то подобное было -- что в каком-то
мирроринге или указании базы можно было что-то пропустить, и это прокатывало
как надо. Но где и что... О -- нарыл!!! vdev, за 10-08-2016 -- "нужно уметь указывать -- в auxinfo
-- имя ЕДИНСТВЕННОГО target-канала" ... "достаточно просто указывать
пустой name
-- "".". Да, ровно та же
ситуация/потребность. Но ТУТ так делать уже не будем.
Чуток позже: неа, НЕ прокатило бы:
cda_add_chan()
запрещает пустые имена.
Правда, это как-то не очень, т.к. нарушает заложенный сейчас принцип "нет drvinfo -- пропускаем канал!"; заложено же это было для EPICS'ных имён.
Можно включать такой режим указанием префикса у базового имени; например, "@?:" -- это и мнемоника "спроси (в devtype)", и никакого другого использования быть не может (из-за shell'а).
18.02.2021: чтобы не организовывать тут в _init_d() громоздкий многоуровневый цикл как в mqtt_mapping'е, можно просто сделать функцию "а дай-ка dcpr канала по его номеру". Да, сама по себе она этот цикл БУДЕТ содержать, и оптимальность по скорости будет никакая (из-за многократных повторов), но это только при старте, так что можно забить. И, поскольку в принципе возможна множественность -- пусть возвращает dcpr ПЕРВОГО найденного имени с таким номером.
Напрашиваются идеи:
Но это указание всем сразу.
CxsdHwSetDb()
оно и не копируется, но
указать-то в devtype можно).
Короче -- штука весьма интересная, но пока не вполне ясны детали, да и возни понадобится не так и мало. Так что проще пока проект заморозить.
18.03.2021: да -- НУЖНО уметь миррорить ("клонировать") целые устройства, о чём сейчас был разговор с ЕманоФедей. Затронутые в разговоре темы:
Типа обсуждение:
Вопрос будет с РЕАЛИЗАЦИЕЙ: ведь сервер-хозяин bridge'а будет считать
после запроса на запись (когда взведёт wr_req
), что ему должны
что-то вернуть, а тут -- фиг... И вернуть реально неоткуда -- локальной
копии данных же нету...
Ну не лазить же руками в current_val
etc.? Хотя,
в принципе -- можно, только надо убеждаться, что значение там действительно
есть (по timestamp
'у?).
@получасом позже: о, не -- надо это на уровне СЕРВЕРА делать! Указывать в devlist'е префикс, аналогично '+'/'-' для симуляции, который будет означать "считай все w-каналы за r".
21.03.2021: некоторые соображения:
Так что всем ro-каналам надо ставить это свойство, вопрос лишь -- YES или TRUSTED.
Вот пусть "#!:" и указывает на TRUSTED'ность.
21.03.2021@~23:00, готовясь ко сну -- было детально ясно, как делать: приступаем.
foreign_base
.
mirror_mode
.
all_trusted
.
mirror_mode
значение foreign_name спрашивается только
из namespace'а (и если нету -- то канал пропускается), а попыток парсинга
@-флагов не делается.
is_trusted
теперь
берётся =all_trusted, а не =0.
is_rw
, чтобы ...
SetChanReturnType()
, в зависимости от настройки либо
IS_AUTOUPDATED_TRUSTED
либо хотя бы
IS_AUTOUPDATED_YES
.
GetNameOf()
пусть уж по нему ищет.
22.03.2021: итак, делаем последние шаги:
GetNameOf()
-- поиск по namespace.
Довольно просто и элегантно вышло.
mirror_mode
добывается
указатель type_nsp
.
Теперь пора проверять.
22.03.2021: проверяем -- для этого сделан специальный devlist-test-bridge.lst с 2 устройствами.
Результаты тестирования любопытные:
Поэтому для теста надо было ЯВНО указывать префикс "@*:insrv::" -- иначе оно ходило через cx::.
Думал-думал, разбирался -- фиг...
Эк оно склалось одно к одному -- давно ж собирался тот косяк исправить, но теперь оно стало категорически необходимо.
25.03.2021: проаерил с уже исправленной реализацией протокола -- да, всё работает как надо! Ура!
Вылезла только одна особенность, к bridge_drv относящаяся очень косвенно -- а скорее вообще к ЛЮБЫМ драйверам, коннектящимся к другим устройствам в ЭТОМ же сервере через "cx::": этот коннект происходит с задержкой в 10 секунд.
Анализом кода и размышлениями понял причину:
Ему БД отправляется, да, но из-за нюансов scheduling'а она успевает дойти
не мгновенно, и, видимо, UDP-пакет CXT4_SEARCH
успевает
добежать и обработаться раньше, чем БД. В результате ответа у того
сервера-гуру ещё нету и он молчит.
А через 10 секунд UDP-поиск повторяется, и тогда вся информация уже на месте.
Видимо, просто ну уж очень быстро сервер стартует, а вычитывание гурой его БД идёт медленнее (что, конечно, странно -- уж у тестового конфига объём крохотный, в умолчательный maxrepcount=30 должно б было влезть).
Как такое делать?
Приемлемым кандидатом выглядит "~":
03.04.2021: сделал. Благо, оно было несложно и прекрасно легло в имеющуюся архитектуру.
25.11.2021: ПОЗОРИЩЕ!!! Распознавание '~'
было сделано кривейше -- строчка с if()
'ом была добавлена как
вместоif (*foreign_name == '!') is_trusted = 1; if (*foreign_name == '~') is_on_cycle = 1;
в результате чего ранее работавшая проверка на '!' стала оторванной от общего "селектора" и начала приводить к ошибкеif (*foreign_name == '!') is_trusted = 1; else if (*foreign_name == '~') is_on_cycle = 1;
bad @-spec for chn:0так что канал просто не работал.
Косяк исправлен, плюс в вышеупомянутое сообщение об ошибке добавлен вывод строки с точки, приведшей к ошибке.
В результате оного отсутствия если указать неправильное имя канала (или и вовсе базового устройства), то он просто молча не работает, а в логи ничего не попадает.
12.04.2022: но и логгинг просто так сделать нельзя: тогда ведь при повторяющихся событиях "NOTFOUND" (например, при периодиечски повторяющемся поиске с покамест отрицательным результатом) эти сообщения будут засорять лог.
cda_dataref_t
.
Так что -- в принципе, понятно, что и как делать, но неохота пока.
(Да, конкретно при том использовании может хватить и "гарантий последовательного исполнения" вследствие того, что все эти драйверы живут в одном сервере и записи по insrv:: отработаются гарантированно в той последовательности, в которой запрашиваются. Но если всё же занадобится именно реальная пауза -- например, если "reset" отрабатывается не мгновенно -- то придётся использовать sleep.)
Так вот: может возникнуть ситуация, что приходит ПОВТОРНАЯ команда записи в то время, когда предыдущая ещё не отработала. И тогда повторный запуск просто обломится, что не есть хорошо.
Поэтому надо бы ввести дополнительный auxinfo-флажок, говорящий, что
нужно перед каждым исполнением w-формулы делать ей
cda_stop_formula()
.
04.10.2021: вроде сделал (было б что :D) -- поле
force_restart
и одноимённый параметр.
По-правильному -- проверить надо.
11.10.2021: да, проверил -- работает как надо: БЕЗ указанного ключика вторая запись (до окончания исполнения первой) игнорируется, С ним -- отрабатывается. Ну кто б сомневался :D.
(Причиной стали странности, наблюдённые при реализации
формулы-транслятора -- см. devlist-test-formula-transformer.lst;
одна-то странность была обусловлена косяком в cda_core.c (в
cda_dat_p_set_phys_rds()
не-взведение rds_rcvd=1
),
но остальное -- именно несовершенная реализация данного драйвера.)
14.10.2023: итак,
ret;
), то канал становится навсегда-запрошенным-на-запись и
де-факто "зависшим", т.к. взведено wr_req=1
и следующие запросы
не будут отправлены до возврата результата, которого никогда не произойдёт
(Если только не обновится что-то, от чего зависит r-формула.)
Обычные-то драйверы при отбраковывании значений возвращают текущее/последнее, но тут такой возможности нет.
И, кстати, при "неподходящести" записываемого значения по типу (не-DOUBLE) тоже случалось аналогичное залипание-навсегда.
formula_evproc()
'а выполнялась добыча текущего значения, а из
formula_rw_p()
при отсутствии читающей формулы возвращался 0.0.
Но способа вызвать возврат реального текущего значения при наличии
читающей формулы -- НЕ БЫЛО (ну не вызывать же formula_evproc()
вручную...).
Решаем проблемы:
CDA_PROCESS_FLAG_REFRESH
, взводимый опкодом REFRESH.
ReturnCurVal()
(см. п.2).
ReturnCurVal()
cda_process_ref()
и cda_get_ref_dval()
, ...
Соответственно, она вызывается:
formula_evproc()
-- к этому вызову там всё и сводится.
CDA_PROCESS_FLAG_REFRESH
.
Всё это было не особо сложно, но исходный код реструктурировался преизрядно.
Поэтому приступаем и заводим раздел.
11.04.2022: краткое описание:
CXDTYPE_UNKNOWN
.
chan_evproc()
обсчёт ведётся в зависимости от
конкретного текущего типа, получаемого от
cda_current_dtype_of_ref()
.
Так что оно справится и с реально вариабельными каналами.
DO_CALCS()
, только ему передаются тип данных и то, во что
считать сумму.
ЗЫ: поддерживаются только числовые -- целые и вещественные; текстовые игнорируются.
int64
,
для вещественных -- в float64
; затем, при делении на
nelems
, в любом случае складывается в float64
, из
которого и возвращается.
12.04.2022: продолжаем.
cda_new_context()
указывалось
defpfx
=NULL, вместо надлежащего "insrv::" (более
подробная ругань в разделе onei32l). Такое наследие mirror_drv.c
вылезло.
Исправлено.
После этого -- вроде работает (для onei32l данные поставляет, но у того свои сложности).
И вот потребность стала уже конкретной: для onei32l.
Поэтому пишем драйвер и создаём для него раздельчик.
Некоторые соображения по проекту:
Максимально просто, но дюже расточительно -- 4 служебных канала на каждый полезный.
Плюс, неудобно с точки зрения конфигурирования: при возможности множественных каналов можно сразу делать цельное "искусственное устройство" (и даже devtype'ами определять), а при единичных -- придётся отдельнор ещё сорбиать такое устройство из разрозненных единичных каналов (плюс, никакой devtype на них не натянешь).
Ценой чуть более сложного парсинга получаем намного более общий и гибкий вариант.
Итого -- выбираем вариант "много каналов в одном драйвере".
devtype
/channels
и складывать их в БД; например,
по ключевому слову defval
. И чтоб у таковых сразу ставился бы
текущий timestamp.
ReturnDataSet()
) в
зависимости от типа конкретного канала.
14.04.2022: итак:
_rw_p()
, возвращающая переданные ей для
записи значения как "принятые".
...точнее, так: у noop'а в роли _rw_p()
работает
StdSimulated_rw_p()
, а тут const_rw_p()
вызывает
её только при action == DRVA_WRITE
.
_init_d()
проход по карте из
bridge_drv.c (именно только проход по карте, БЕЗ махинаций с
typename/nsp).
strtoul()
([U]INT{8,16,32}),
strtoull()
(INT64/UINT64), strtod()
(SINGLE/DOUBLE).
А проверка успешности по endptr
далее одна общая.
Вот тут уж начинаешь задумываться -- а может, всё-таки сделать
в cxsd_db+cxsd_hw поддержку возможности указывать начальные значения, что,
при условии до-запинывания возможности указывать return_type
,
позволит исполнять обязанности const_drv прямо обычному noop_drv.
Проверяем -- вроде работает.
data_evproc()
, с отработкой FRESHCHG и RSLVSTAT),
cankoz_regs_f8()
, включая макрос
SET_RET_INFO()
, только адаптированно под наличие rflags и
timestamp.
27.06.2023: нюансы:
/nn:
"
перед именем канала-источника.
В таком случае и "всё слово" возвращается обрезанное до такого числа бит, и по-битовые каналы отдаются именно в таком количестве и не более.
#if
'лен (по значению символа USE_CDA_REQ_READ
)
вариант с "форвардингом" запросов на чтение -- когда подписка делается в
режиме PASSIVE, а rdreg_rw_p()
генерит запросы на чтение (да,
БЕЗ "оптимизации" с флагом "запрос уже отправлен"; с другой стороны, в
rdreg_rw_p()
нет цикла по переданному списку каналов, а просто
один безусловный вызов).
Проверено на элементарной симуляции -- работает как запланировано.
14.07.2023: включен режим
USE_CDA_REQ_READ
, при котором используется
cda_req_ref_read()
-- проверено, работает как надо:
data_evproc()
и в wrreg_rw_p()
.
28.06.2023: нюансы изменений:
cankoz_lyr_common.c
; в частности:
kozdevinfo_t.ioregs
целиком скопировано в
privrec_t
, только с апгрейдом с uint8
до
uint32
.
send_write()
, сильно проще
send_wrreg_cmds()
.
cankoz_regs_f8()
попало в data_evproc()
.
cankoz_regs_rw()
переместилось уже в обычную wrreg_rw_p()
.
wrreg_rw_p()
в приёме данных "сверху" уже
чуток интеллекта, отличающего её от прочих драйверов -- она понимает не
только INT32/UINT32, но также и INT8/UINT8 и INT16/UINT16, преобразуя их к
32-битному (всегда через uint*
, чтобы избежать лишнего
расширения знака).
rcvd
,
#if
'но по тому же USE_CDA_REQ_READ
.
Проверено:
Но это в принципе и понятно: ПЕРЕХОД ОТВЕТСТВЕННОСТИ???????????
Кстати, заметки насчёт работы записи в регистры и нуления/не-нуления
req_msk
-- в bigfile-0001.html за
01-06-2010, 11-10-2011, 20-12-2011 (касательно WRREG) и
20-12-2011, 21-12-2011 (касательно PRESCALER'а).
30.06.2023: однако есть косяк: каналы почему-то сначала НЕ отдаются -- проверено cdaclient'ом.
Возможно, происходит следующее: он-то каналы отдаёт прямо сразу, но это
происходит ещё в течение вызова метода init_dev()
, а затем
делается ReviveDev()
, вызывающий ...
ReqRofWrChsOf()
,
но на этот запрос чтения драйвер уже не отвечает
30.06.2023@вечер-засыпая:
ни-фи-га! Дело в другом: что ReviveDev()
вызывает
RstDevUpdCycles()
, превращая вроде бы только что вёрнутые
значения в "устаревшие".
И понятно, что даже реализация и применение cda_req_rd()
тут
не спасли бы -- на запрос всё равно не будет ответа.
01.07.2023@утро, мытьё посуды:
а спасло бы оно В СОЧЕТАНИИ с использованием frontend'ом понимания сущности
такого канала, по IsAnUpdate()
-- как и было осознано
позавчера, 28-06-2023 -- чтоб на ИСХОДНЫЙ канал ответ всё же был бы получен.
01.07.2023: ну сделал:
ServeIORequest()
/CXC_CH_RQRD
при
IsAnUpdate()
просто сразу отправляет текущее значение с кодом
CXC_NT_NEWVAL
, вместо вызова чтения.
Понятно, что СЕЙЧАС это ситуацию не исправит, т.к. этот запрос никто пока не присылает (не говоря уж о том, что "исправит" изменение уже в cda_d_insrv.c).
16.07.2023: поскольку должное "исправляющее изменение" уже сделано (точнее, функциональность добавлена) -- вместе с поддержкой режима пассивного мониторинга -- то пора проверять работу с тем вариантом.
Первоначально предполагалось назвать его "sim_drv" ("симулятор"), но 01-05-2024 было решено переделать в "sim_dir"
01.05.2024@утро-душ: некоторые соображения по устройству -- в порядке потока сознания.
Что в принципе несложно -- аналогичное уже давно реализовано в Cdr_treeproc.c, где в r/w/c можно писать как каналы, так и формулы; вот оттуда и возьмём логику.
on_change=SRC_CHAN?"формула либо имя канала"
В таком случае всё же нужен какой-то НАЧАЛЬНЫЙ спецификатор "target_dev=NNN".
@после завтрака: ну так парсить в 2 прохода -- ровно как в modbus_tcp_drv.c, благо, подводные камни уже понятны.
@ещё чуть позже: ...хотя, если парсить "по штучке" (с пробелом в качестве terminators) и в локальную переменную (в буфер строки), то можно и реаллокировать.
03.05.2024@~14:30, идя из ИЯФа в Эдем
вниз по Лаврентьева: а вот и нет -- НЕЛЬЗЯ! Т.к. privptr1=devptr
сохраняется cda_add_chan()
'ом в момент вызова, и поменять его
потом никак нельзя, а если реаллокировать privrec, то адрес сменится. Так
что -- ОДНОЗНАЧНО ТОЛЬКО 2-проходный парсинг (либо privrec фиксированного
размера, а realloc()
'ировать/GrowBuf()
'ить
отдельный массив, на который в privrec'е указатель -- как в
mqtt_mapping_drv.c и bridge_drv.c (кстати, в них одиночный
malloc()
, без РЕаллокирования); но мы ведь так не хотим, да?).
02.05.2024: обмозговываем дальше.
descr->proc()
взводить
being_processed=1
, после же проверять
being_destroyed
и если взведён, то самостоятельно вызывать
cda_f_fla_p_destroy()
и отваливать.
...вот только придётся как-то "наверх" в cda_core.c сообщать,
что удаление лишь начато и НЕЛЬЗЯ освобождать
ri->fla_privptr
. Видимо, метод destroy
надо
сделать возвращающим статус "завершено"/"отложено".
И, кстати, в таком случае обязанность сделать safe_free(fla)
ложится уже на process_commands()
. ЗЫ:
проверил, что оба его вызывальщика НЕ лазят в fla
при
отрицательном коде возврата, так что надо возвращать
CDA_PROCESS_ERR
и всё сошьётся.
Чтобы не вводить лишние поля
-- можно из cda_f_fla_p_stop()
нулить
being_processed
, а в process_commands()
после
окончания команды проверять, и если занулилось -- отваливать.
Парой часов позже, уже в процессе реализации: неа, нельзя -- ведь
being_processed
является СЧИТАЮЩИМ флагом, которому делаются
"++
" и "--
", а не =1
и
=0
. Да, конкретно ТУТ вроде бы как и можно было бы обойтись
булевским флагом (т.к. повторного входа в процессинг не допустит сама
а вот и нет --
допустит, но вложенное исполнение формул вряд ли вероятно), но не
стоит отступать от общей модели.
process_commands()
03.05.2024: ещё некоторые соображения по устройству:
cda_snd_ref_data()
(получив указатель на данные от
cda_acc_ref_data()
, а от
cda_current_nelems_of_ref()
и
cda_current_dtype_of_ref()
-- прочие свойства).
double
и вызовет cda_process_ref()
с нужными
флагами, а для каналов -- сделает SendOrStore()
.
Приступаем потихоньку.
DEFINE_CXSD_DRIVER()
.
privrec_t
.
data_evproc()
сделано "отражение" по сегодняшней идее
через cda_snd_ref_data()
, ...
sim_dir_rw_p()
-- cda_stop_formula()
всем target'ам при записи 1
в любой "свой" канал.
Итого -- рутина и простое практически сделано, осталось только самое
замороченное -- парсинг, аллокирование и прочие "мозги" в
_init_d()
.
06.05.2024: пилим sim_dir_init_d()
.
Предварительные соображения:
modbus_tcp_init_d()
-- там такой же
парсинг в 2 прохода с аллокированием в конце первого (0-го!).
cx_strmemcasecmp()
, а потом парсить штучно.
Парсится не вручную, а psp_parse()
'ом, чтоб использовать
готовую инфраструктуру разбора кавычек и прочего квочения.
Итак:
NNN_opts_t
-- base_opts_t
,
at_init_opts_t
, on_change_opts_t
(да, в отличие от
прочих подобных тут подчерк '_'
перед "opts
" --
из-за подчерков в именах, вроде "on_change").
Каждый из них содержит единственное поле-строку.
text2NNN_opts[]
-- text2base_opts
,
text2at_init_opts
, text2on_change_opts
, ...
NNN_opts
--
base_opts
, at_init_opts
,
on_change_opts
.
#define
'ах, чтобы
гарантировать идентичность строк/ключевых-слов, используемых в штучных
проверках и в PSP-таблицах.
if (strmemcasecmp(S_NNN_eq, p, strlen(S_NNN_eq) == 0) {psp_parse(); ...
"
cda_add_chan()
и
cda_add_formula()
. Так ещё и сильно проще -- не надо проверять
"base= указано раньше on_change=, и ещё оно не дублируется".
07.05.2024@~09:40, по пути в ИЯФ мимо бассейна ВЦ: это даст ещё одну возможность -- можно делать МНОЖЕСТВЕННЫЕ "base=", перемежая ими "on_change=", тем самым указывая разным наборам разные базы. Это полезно при дирижировании НЕСКОЛЬКИМИ устройствами. ...а "at_init=" будет использовать значение последнего указанного (ну да, несколько неоднозначно; хотя если регистрировать формулу сразу в точке парсинга -- тогда будет использовать текущее).
Считать ли DST каналом или формулой -- пока просто захардкожено, что формула. 07.05.2024: уже переделано.
=CDA_DATAREF_ERROR
) и создаётся cda-контекст.
07.05.2024: доделываем чуток.
is_a_fla()
, определяющая по тексту спецификации,
канал это или формула. "Алгоритм" скопирован с
Cdr_treeproc.c::cvt2ref()
, как и предполагалось.
Только в том алгоритме учтены не все флаги, даже уже поддерживаемые самой Cdr -- '/' (SHY) нету.
cx_strmemcasecmp()
делалось
"p += strlen(S_base_eq)
"
-- но здесь-то не ручной парсинг, а PSP, которой как раз НАДО ОСТАВИТЬ ключ.
sizeof(privrec_t)
, из-за чего cxsd_hw не только аллокировал, но
и пытался сделать free()
при обломе устройства, что приводило к
фатальной ошибке "double free".
В таком варианте проверили на устройстве типа
"sim208/simkoz
" (вариант CAC208 с писабельными каналами
входного регистра, драйвер-симулятор) с простейшим "отражением" --
-- работает!!!dev ctd sim208/simkoz ~ - spd=-800000 tac=10 dev dir sim_dir w1i - base=ctd on_change=outrb1?inprb0
Идём дальше.
parse_chanref()
-- весьма монструозного вида.
Она спроектирована под конкретные потребности: 1) чтоб можно было
указывать "умолчательные" options/dtype/nelems, а она уж заполнит в
соответствии с указанным, если таковое указание есть; 2) пропускает префикс
'=', который можно указывать для is_a_fla()
(чтоб тот
распознал как канал, а не формулу), и возвращает указатель уже на начало
имени канала. Но должна устроить и все прочие использования в драйверах.
Напрашивается её обобществить в сервер.
08.05.2024@утро: к ней
добавлен параметр "const char *log_addr
" -- выводить "точку
проблемы" (будет "on_change#%d") при ругательствах. Теперь-то
точно годна для обобществления :-)
09.05.2024: доделываем...
parse_chanref()
:
at_init_ref
(реально нужно
только при старте, но для приличия).
cda_add_formula()
и при успехе --
cda_process_ref()
.
allow_w
; на вход подаётся 0
.
sleep
".
Список тестов для проверки работоспособности всего сделанного -- как собственно sim_dir_drv.c, так и нововведений в связке cda_core.c+cda_f_fla.c:
10.05.2024: проверяем.
(Хотя обнаружилась мелкая опечатка: вместо "dst_name
"
регистрировалось "dst_name
" -- огрех копирования, махом
исправленный.)
sleep 10
" ДО записи в _devstate) --
вроде бы должно работать, но нет: какие-то странные эффекты -- то просто
SIGSEGV, то отрабатывает 2 раза через указанные 10сек и потом зависает (даже
без реакции на Ctrl+C и Ctrl+\, и только Ctrl+Z плюс "kill -9" спасают --
висит в malloc_consolidate()
на какой-то блокировке);
добавление/удаление fprintf(stderr) влияет на поведение. После исправления
косяка в RlsRefSlot()
чуток получшало и вроде как стало
работать "правильно", но всё равно через несколько итераций обнаруживается
fla->being_processed=27804849 -- ну явно где-то промахивается мимо
указателя/границ и портит память.
11.05.2024: после исправления косяка с отсутстовавшим
bzero()
та проблема вроде тоже исчезла.
cda_f_fla.c: не делалось nextflags |= FLAG_DO_STOP; но это не сюда, а просто случайно замечено.
cda_core.c: в RlsRefSlot()
было СНАЧАЛА in_use=0,
а ПОТОМ селектор "действия", так что как минимум формулы НЕ удалялись. И
вообще подчистка выглядит сомнительно: не факт, что исполняются
del_chan()'ы, т.к. этот метод вызывается ТОЛЬКО из
cda_del_chan()
, а он при подчистке НЕ вызывается -- делаются
просто RlsRefSlot()
'ы, но там стоит комментарий "Nothing to do:
del_chan() is guaranteed by RlsRefSlot() caller", который, вероятно,
устарел. И как вообще работала подчистка при останове/рестарте драйверов с
insrv-соединениями?
11.05.2024@утро: видимо,
исключительно за счёт того, что удаление sid'ов всё же
вызывалось, а cda_d_insrv_del_srv()
посредством
DestroyInsrvPrivrec()
корректно всё подтирают, в т.ч. тамошний
RlsHwrSlot()
посредством UnRegisterInsrvHwr()
снимает evproc'ы. Кстати, несколько странный подход в
cda_dat_p_del_server_finish()
: это, по сути, не просто
завершение удаления sid'а, а ещё и удаление КОНТЕКСТА; но откуда следует,
что это следствие/часть удаления контекста, а не индивидуального сервера?
11.05.2024@утро: а-а-а,
понял: т.к. в публичном API cda нету прямого доступа к
созданию/удалению sid'ов, то раз уж идёт удаление sid'а, то это
гарантированно часть процесса удаления контекста; поэтому-то _finish() даже
и не проверяет взведённость
ci->being_destroyed
.
11.05.2024: а ещё после исправления вчера косяка
"in_use=0 ДО селектора" вылез другой ляп: из-за того, что НЕ делалось
bzero(fla_privptr)
, недо-сделанные формулы (например, с
синтаксической ошибкой) стали приводить к скопычиванию -- из-за того, что
теперь стала вызываться cda_f_fla_p_destroy()
и пытаться делать
safe_free(fla->buf)
, но в
fla->buf
ведь мусор. Исправлено -- добавлено
bzero()
, плюс в cda_f_fla_p_destroy()
добавлена
проверка на fla==NULL.
11.05.2024: проолжаем проверять.
...кстати, само исполнение формулы к состоянию "processed" НЕ приводит -- нету там этих "скобок".
сделать sleep? До или после отправки? Можно проверить и так, и эдак.
Попробовал:
cda_f_fla_p_destroy()
значение sleep_tid
не
проверяет и ничего не подчищает.
Но почему не отрабатывается cleanup в cxscheduler'е?
12.05.2024: понятно, почему: proc_SLEEP()
вызывается ПОСЛЕ cleanup'а. Поэтому главный вопрос: а почему исполнение
формулы продолжается ПОСЛЕ вроде как убиения?
Видимо (проверено отл.печатью да!), потому, что в состоянии "processed"
-- находился весь контекст, а потому в момент останова драйвера
cda_f_fla_p_destroy()
не вызывается, потому и
fla->being_destroyed
оно не взводит, а
process_commands()
на него и не реагирует; всё логично и "по
спецификации" (вот если перед записью в _devstate поставить "sleep", чтоб
исполнение продолжилось вне контекстова "processed", то "протокол
being_destroyed" отрабатывается как задумано).
Вывод: надо всё-таки в cda_f_fla_p_destroy()
о
sleep_tid
заботиться.
Сделано -- проблема решилась.
Но замечено ещё одно: в cda_f_fla_p_destroy()
отсутствовала
(была забыта?) операция "отложенного free() его privrec'а" -- то, что плагин
формул обязуется делать, возвращая cda_core'у
CDA_FLA_P_DESTROY_DEFERRED
.
Ну добавил, и -- SIGABRT, из-за "double free or corruption (fasttop)"!
Стал разбираться -- тьфу ты!!! SIGABRT не в _destroy(), а в
process_commands()
-- потому что именно там СРАЗУ 02-05-2024
было сделано free()
-при-отложенном-удалении (видимо, потому,
что так корректнее -- в ОБОИХ случаях оно делается вне _destroy(),
ВЫЗЫВАЛЬЩИКОМ, так что в самом _destroy() не нужно детектировать "а не
повторный ли это вызов?").
Так что просто убрал свежедобавленное и проблема исчезла.
А ещё в parse_chanref()
была забыта инициализация
is_unsigned_mask = 0
и целые типы почему-то выходили
беззнаковыми (хотя мог вообще любой мусор попасть).
12.05.2024: дотестировываем.
(Возможно, ранее что-то и вылезло бы, но с дошлифовыванием прочего для этого аспекта возможностей косяков вряд ли осталось :-D)
13.05.2024: поскольку вроде всё работает как надо,
закомментировываем все отладочные fprintf(stderr,...)
'ы
(отовсюду -- и из драйвера, и из cda).
14.05.2024: первая реальная проба: имитация реверсивного ИСТа.
(После уставления 1 битики через 500000us сбрасываются в 0, но эти нули надо игнорировать.)
on_change=outrb6?"_all_code; push -5; qryval; cmp_if_gt 0.5; putchan out_imm4; ret" \ on_change=outrb7?"_all_code; push +5; qryval; cmp_if_gt 0.5; putchan out_imm4; ret"
Поэтому сделано косвенно: пишется в канал ЦАПа (тоже 4-й), а
simkoz-устройству указывается ещё отражение
"on_change=out_cur4?adc4
", так что результирующая строка --
spd=-400000 tac=10 on_change=out_cur0?adc0 on_change=outrb1?inprb0 on_change=out_cur4?adc4
Результат -- да, работает: драйвер ИСТа исполняет переключение знака и ходит и туда, и обратно.
ЗЫ: что любопытно, "наследование типов" (в виде того же sim208) конкретно ТУТ так и не понадобилось.
Единственная возникшая проблема -- что в качестве "симулируемого устройства" пришлось использовать CAC208, как обладающий требуемым дополнительным каналом ЦАП; но у него нет 1) "коротких имён" для единственного у CDAC20 канала ЦАП (out0, out_imm0, ...) и 2) служебных каналов касательно калибровки ЦАП и цифровой коррекции.
channels
-- добавлять эти имена
-- (из cdac20.devtype).
15.05.2024: результаты размышлений на тему "как правильнее" вчера и сегодня.
devtype sim_cdac20_4ist:cdac20 w40i,r20i,w8i,r16i,w1i,r2i,w13i,r100i,w32i,w32i,r32i,r4i,w1i31,w1i31,w1i31,w96i,r1t100 { out_cur4 268 out_imm4 212 }
24.05.2024: переделано на
out_imm4 220
-- как в обновлённом simkoz208_drv.c.
Чуть позже: а можно ведь не использовать
канал ЦАП4, от которого больше сложностей (из-за необходимости обходить
плавное изменение) и вместо этого -- раз нам нужна просто "труба"-"почтовый
ящик" -- использовать какой-нибудь незадействованный в реальных устройствах
канал (только он должен по SIM_CHTYPE
-типу подходить).
И ещё чуть позже: проверил:
SIM_CHTYPE
-типу подходить" нету --
в simkoz_drv.c SRC-каналы вообще могут быть любыми, а
sim_flags[]
касается только каналов записи.
simkoz_rw_p()
-- чтоб тот воспринял бы как запись да ещё и
вызвал бы PerformReflection()
.
И ещё чуть позже: неа, для НЕреверсивных ведь sim_dir вообще не нужен, так что некоторую оптимизацию нужно будет сделать -- чтобы не плодишись лишние драйверы с их лишними insrv::-соединениями.
Попробовал такой вариант -- работает.
Надо это как-то интегрировать в devlist_magx_macros.m4, чтобы
для включения симуляции достаточно бы было включить какой-нибудь
define()
(а в идеале -- и вовсе переменную окружения).
Погуглил насчёт переменных окружения (после того, как не нашёл ничего в m4.info*) -- "m4 getenv" и "m4 environment":
getenv
" по
исходникам m4-1.4.16/ (от CentOS-7.3); а слова "environ" там не
нашлось вовсе, да и main()
в src/m4.c определён как
main (int argc, char *const *argv)
-- так что этот путь тоже вне использования.
esyscmd()
, примерно вот так:
define(`HOME', esyscmd(`printf \`\`%s\'\' "$HOME"'))
18.05.2024@утро и затем в Ключах: некоторые соображения насчёт "как реализовывать симуляцию в общем случае".
Но вопрос -- КЕМ он будет управлять?
А можно не наследовать и не менять, а просто помечать устройство как '+' -- суперсимулируемое, так что каналы чтения автоматом превратятся в каналы записи.
Т.е., чтобы он и записи к каналам принимал бы.
Соображения на тему такого унифицированного драйвера на основе sim_dir_drv.c:
Решение несложно -- auxinfo-параметр "stop=", значение которого по
умолчанию =-1, и _rw_p() чтоб проверял "не сделать ли stop всем формулам"
первым условием в if()'е по каналам, условием
"if (chn == me->stop_chan)
".
base_opts.base
значение GetDevInstname()
(19.05.2024:
точнее, в НАЧАЛЕ цикла прописывал бы это умолчание, ну и по "self"
тоже.)
Запись-то как делать -- через тот же insrv? Но это:
Использовать какой-то другой протокол, вроде local/dircn (указывая при регистрации формул его в качестве base)? Но та пара совсем не годится для ДРАЙВЕРОВ (только для программы целиком), да и вообще "per-driver protocol" -- сомнительная штука.
Резюме: ясно, что ничего не ясно. Похоже, надо эти работы притормозить до возникновения реальной КОНКРЕТНОЙ потребности -- тогда станет лучше понятно, что надо сделать.
21.05.2024: пытаемся таки заюзать драйвер для того, для чего изначально он задумывался -- для симуляции источников магнитной системы Инжекционного комплекса ВЭПП-5, т.е., серверов canhw:11 и canhw:12, чья работа с источниками определяется в devlist_magx_macros.m4, в который и должны идти основные изменения.
Давно (неделю?) зревший в голове "проект" -- добавляем в devlist_magx_macros.m4 поддержку симуляции, чтобы она включалась буквально установкой некоей переменной окружения, при которой автоматом бы "подправлялись" определения всего.
dev icd_NNN cdac20@sktcankoz ~ ...
" -- и вот что с этим
делать?
Ведь при симуляции нужно вместо "cdac20@sktcankoz
" ставить
"sim_cdac20_4ist/simkoz
".
Напрашивается решение: сделать макрос, который в зависимости от состояния
симуляции будет определяться в одну или другую строку, а в devlist'ах
заменить "cdac20@sktcankoz
" на него. ...плюс, конечно, ещё
один макрос с параметрами для simkoz'а -- список
"on_change=...
".
Так и сделано: макрос "тип устройства" --
MAGX_cdac20_sktcankoz
, параметры --
MAGX_cdac20_IST_SIM_AUXPARAMS
.
MAGX_IST_CDAC20_DEV()
в
$2
имя CAN-устройства начинается с '-' (что
определяется по равенству substr($2,0,1)
значению
`-'
), то...
base=
" указать $2
с
отрезанным первым символом -- "substr($2,1)
".
@подвал-у-Кузнецова-к.15: Но вот при первом же прогоне даже зачатков вышеприведённого с попыткой получить значение переменной окружения посредством
define(`MAGX_SIM_RQD', esyscmd(`/bin/printf %s "$MAGX_SIMULATE"'))
вылезла проблема -- m4 ругается
/usr/bin/m4:configs/devlist_magx_macros.m4:3: cannot run command `/bin/printf %s "$MAGX_SIMULATE"': No child processes
И дальше больше:
syscmd()
-- не-e
, чей вывод должен НЕ всасываться
обратно в m4 ("The expansion of `syscmd' is void, _not_ the output from
SHELL-COMMAND!") -- видим, что:
esyscmd()
.
e
syscmd()
: потому, что она свой вывод отправляет на stdout, а
он-то средирекчен "потребителю", поэтому потребитель получает и этот вывод
(НЕ от m4!), хотя типа не должен бы.
Надо делать "/bin/echo >/dev/tty
" -- тогда принудительно
отправится в консоль.
...а вот с "cannot run command ... No child processes" так и не понятно: тестовый прогон work/tests/syscmd.m4 ошибок не даёт.
22.05.2024@дома, утро, ~10:00+: продолжаем рыть.
Дичь какая-то...
22.05.2024: пытаемся допилить симуляцию пока хотя бы
без возможности включать её переменной окружения -- да просто вхардкоживаем
"define(`MAGX_SIM_RQD', 1)
".
Помогло -- сделанное вчера в devlist_magx_macros.m4 и заюзанное сегодня в devlist-canhw-11.lst и devlist-canhw-12.lst работает всё, и с обычными ИСТами, и с реверсивными.
26.05.2024@дома, утро,
~11:00+: возвращаемся к разбирательству с m4 и
esyscmd()
.
-- и там всё работает, никаких "cannot run command".esyscmd(`pwd') esyscmd(`/bin/printf %s "$MAGX_SIMULATE"') syscmd(`/bin/ls -l /etc/passwd >/dev/tty')
/bin/echo >/dev/tty
" для
не-e
syscmd()
-- ну да, помогло.
strace -f
-- да нет, ВЫПОЛНЯЕТСЯ команда...
(Что любопытно, в результатах поиска было сплошное "Не найдено: m4 | Нужно включить: m4"; но принудительно закавычивание для затребовывания его обязательного присутствия мало что дало, а полезные результаты и так были в первых же строках -- 2-й и 3-й результаты :D)
И вот тут в конкретном сообщении #3 есть следующее
я, повторюсь, давно не программил под unices, но матрица, AFAIR, такая:
нет обработчика SIGCHLD, нет wait -- return codes накапливаются (zombies)
есть обработчик, нет wait -- childs тихо дропаются
нет обработчика, есть wait -- ловятся все, включая сдохших ранее
есть обработчик, есть wait -- ловятся те, кто сдох во время ожидания на wait, прочие попадают на SIGCHLD
28.05.2024@утро, зарядка: к этой табличке есть претензии -- как минимум вариант 2 "есть обработчик, нет wait -- childs тихо дропаются" выглядит бредово; скорее это описание варианта с SIG_IGN.
Вывод: я очевидным образом не особо-то понимал работу связки SIGCHLD
(особенно в варианте SIG_IGN) и wait()
. По-хорошему, надо бы
подразобраться и переделать. ...хотя -- работает же вроде (в основном :D),
так что "работает -- не трожь!"?
28.05.2024@утро, зарядка:
нифига, как раз ВСЁ ПРАВИЛЬНО делается в
cxsd.c::InterceptSignals()
: если обработчик есть --
ставим его для ловли, если нет -- то и незачем зомбям образовываться, пусть
молча подчищаются. А вот то, что m4 себе не уставляет нужный режим -- это
ЕГО баг: если при запуске "вручную" цепочкой pipe()+fork()+exec()
запускающий ещё может сделать signal(SIGCHLD,SIG_DFL), то при использовании
popen()
такой возможности нет.
29.05.2024: кстати, на кой чёрт m4 при
esyscmd()
использует wait() и при его обламывании НЕ забирает
результат вывода порождённого процесса -- непонятно: ну читал бы из
дескриптора, а по EOF считал бы, что всё, запущенный под-процесс завершился
(ладно ещё syscmd()
-- там из-за НЕредирекченья EOF взять негде
и нужно ждать окончания wait()'ом).
30.05.2024@утро, после
завтрака: узнал, заглянув в исходник m4-1.4.16/src/builtin.c
-- там есть коммент «This section contains macros to handle the
builtins "syscmd", "esyscmd" and "sysval".»: вот ради этого sysval()
-- оно «Expands
to the exit status of the last shell command run with `syscmd' or
`esyscmd'.»; понятно, что тут wait()
необходим.. Дальше я разбираться не стал (в том, как там устроен
запуск под-процесса -- там используется вызов execute()
в
m4_syscmd()
и create_pipe_in()
с последующим
чтением и wait_subprocess()
в m4_esyscmd()
-- эти
детали уже неинтересны после понимания о надобности получения статуса).
syscmd()
реально давала эффект в вывод (приходивший на вход
потребителю) -- это для m4 она "обламывалась", а так-то она исполнялась и
вывод генерила.
26.05.2024: ~13:00+ в процессе поездки на переобуввку Chaser (гараж за колёсами, мойка у Мамонта, переобувка) продолжаем думать на эту тему.
wait4()
, но из-за SIG_IGN завершившиеся порождённые молча
подчищаются, а wait4()
завершается с ECHILD
.
...а игнорируемость сигналов, ЕМНИП, вроде бы наследуется при exec()'е -- вот так влияние на вроде бы отдельный порождённый процесс и реализуется.
Выглядит как самое простое решение.
ppf4td_pipe_open()
теперь стоит
signal(SIGCHLD, SIG_DFL); execv(cmdline[0], cmdline);
Помогло -- ошибка исчезла!!! И получение значения переменной окружения теперь работает.
04.06.2024: за последние несколько дней подготовил в devlist_magx_macros.m4 симуляцию для v3h_a40d16.
05.06.2024: хотя, возможно, стоит в a40.adc{N*5+2}, т.к. именно [2]й канал является DCCT1, который считается за Imes (а [0]й -- это "DAC_MES").
MAGX_V3H_SIM_8PCS_DEV
sim_dir'у
указывается цепочка из 8 комплектов вида
on_change=a40.outrb0?d16.inprb0 on_change=d16.out_cur0?a40.adc0
-- там у N-го комплекта все циферки стоят N, и только a40.adc{N*5}.
Пытаемся проверять -- упс: некоторые каналы V3H'а перманентно горят болотным, а его vdev_state==0 (т.е., UNKNOWN).
05.06.2024: продолжаем.
vdev_init()
-- т.е., их
значения реально есть раньше, а тут они образуются прямо в момент
регистрации (т.к. cda_d_insrv_new_chan()
сразу говорит
CDA_DAT_P_IS_UPDATE
) -- то перехода из UNKNOWN в DETERMINE
никогда не произойдёт (если только "вживую" не рестартовать подстилающие
"CAN"-драйверы, чтоб опять случились обновления 06.06.2024@утро, зарядка: кстати,
"._devstate=0" можно было бы делать из sim_drv'шного at_init (в т.ч. через
паузу) -- ну, если бы не удалось решить проблему иначе.).
...помнится, когда-то раньше уже что-то подобное встречалось. А с реальным железом всё работает беспроблемно потому, что от CAN данные приходят гарантированно ПОСЛЕ инициализации.
05.06.2024: уже после исправления проблемы vdev'а всё-таки вылезло неудобство: когда каналы входного регистра объявлены как "w", соответствующие им TUBE-каналы vdev-драйверов быстро становятся синими: ведь обновлений не приходит, в отличие от реальных r-каналов. А вместе с ними посиневают и Imes, т.к. там ради подсветки учитывается значение битика OPR, чтоб не подсвечивать жёлтым/красным расхождение при выключенном источнике.
Поэтому напрашивается всё же реализация той идеи от 04-06.2024: оставить каналы входного регистра как "r", а в них писать "отражением" через какие-нибудь иначе не используемые каналы.
06.06.2024: двигаемся дальше.
0x.06.2024:
Ответ -- "у большинства типов рЕкордов есть специальные поля".
На странице
"RRM 3-14 Common"
они перечислены и описаны: главное -- SIMM
(SIMulation Mode),
которое если сделать YES, то "input will be obtained from SIOL instead of
INP" и "output will be written to SIOL instead of OUT".
14.06.2024: спросил на ту же тему у Саши Сенченко. Ответ:
17.12.2009: некоторые общие идеи:
Итого -- оне служат минимально-возможной абстракцией работы с конкретным типом CAN-интерфейса.
CANHAL_FILE_H
(что
include'ить) и CANLYR_NAME
(имя для DEFINE_LAYER()'а). С
canmon_common.c ситуация аналогичная. Предполагается, что
бинарники с NNNcanhal.h будут называться NNNcankoz_lyr.so и
NNNcanmon.
08.01.2010: поддопилил canmon_common.c до юзабельного состояния -- покамест он собирается в довольно левом месте work/yweld/drivers/.
09.02.2010: еще с неделю назад сделал собираемость в drivers/can/marathon-2.4/, а сегодня -- и в drivers/can/socketcan/.
12.05.2011: за последние три дня в cankoz_lyr_common.c сделано:
cankoz_q_erase_and_send_next()
-- по
"управильненному" варианту с удалением-и-отправкой только в случае,
если указанный к удалению пакет действительно имеется в голове очереди.
cankoz_q_enq*()
-- с поддержкой
REPLACE_FIRST.
20.09.2011@Снежинск-каземат-11: несколько мелочей:
DECODE_AND_CHECK()
имелся дикий ляп -- ошибкой
считалось line>countof(lines)
, вместо >=
.
Пофиксил.
22.09.2011@Снежинск-каземат-11: по аналогии с piv485_lyr:
введен enum CANKOZ_MAXPKTBYTES=8
и заюзан везде, где
раньше в качестве длины/
22.09.2011@Снежинск-каземат-11: и еще там имелся гадкий ляпчик:
cankoz_disconnect()
-- в цикле поиска "if some other
devices are active on this line" стояло сравнение devid>=0
,
в то время как везде там считается "unused -- devid==0" (общий принцип в
v4). Видимо, пролезло из cankoz_pre_lyr.c. Исправил
">=
" на ">
".
Замечание:
Вряд ли это когда понадобится, но, формально, почему б и нет -- в CAMAC и VME ведь требуется.
02.05.2012: common_sources переименована в src -- в струе нового стиля.
03.05.2012: стоп -- а почему это у нас есть именно РАЗДЕЛЬНЫЕ ПОЛНОЦЕННЫЕ директории marathon-2.4 и socketcan? Ведь на layer'ах должно быть по-другому -- один комплект драйверов, и отдельно -- layer'ы (да, можно по штучке в директории). 04.09.2015: это уже исправлено -- давно (с июня?) драйверы собираются в local/, плюс есть отдельные директории для layer'ов (ну и в c4l-cangw/ -- всё вместе).
24.05.2012: 29-04-2012 при переводе v2 на новую систему сборки по ошибке был удалён can/common_sources/CommonRules.mk. Сейчас он восстановлен как can/src/ShadowRules.mk.
И заодно вылезла проблема -- с введением в 4cx/ поддержки OUTOFTREE "сторонние" проекты перестали собираться по причине отсутствия TOPEXPORTSDIR да и самой exports/; а drivers/ -- как раз именно сторонний.
(Увидено это было от сдуревшего вследствие наводок пановского УБСа.)
Естественно, программы от этого запутаются. Так что надо вставлять проверки.
12.01.2010: да, повставлял проверки.
(can_id&~0x7FF)!=0
", а
"(can_id!=(can_id&0x7FF))
".
А canmon (обе версии) при таких can_id печатают комментарий-предупреждение и НЕ пытаются дешифрировать ни kID/prio.r, ни 0xFF.
Плюс, поскольку can_id -- int
, то для выдачи
используется формат "%u" вместо былого "%d".
29.11.2012: протокол:
canqelem_t.datasize
-- int (знаковый, ради поиска с
частичным сравнением).
canhal_send_frame()
добавлена проверка
dlc<0
.
Сегодня же оно начато проверяться (на магнитной системе накопителя -- ring1:32), и исправляются имеющиеся косяки.
25.07.2015: итак:
disconnect()
: он вроде как освобождал,
но потом cankoz_add()
считал слот занятым. Причина -- в
разных частях было разное понимание, что считать за "свободно":
disconnect() делал .devid=0 (в стиле v4 -- это "NOT_IN_DRIVER"), а
cankoz_add() считал занятыми при .devid>=0.
Решение:
cankoz_add()
.
GETDEVATTRS: ID=0,46, DevCode=4:CAC208/CEAC208, HWver=1, SWver=5, Reason=3:WhoAreHere cankoz_fd_p: DevCode=4 differs from registered 0(+). Terminating device SetDevState(): devid=0(active=0) is inactive
А всё потому, что в cankoz_fd_p()
тоже стояла проверка
dp->devid>=0
. Она и devcodes[] проверяла от
неиспользуемого слота (со всеми нулями), и SetDevState(devid=0)
пыталась делать.
Сейчас заменена на devid!=DEVID_NOT_IN_DRIVER.
disconnect()
: теперь оно не
заканчивает поиск по первому найденному, а ВСЕГДА прошаривает ВЕСЬ
массив (как это, кстати, было сделано в v2'шном
cankoz_pre_lyr.c).
Смысл -- чтоб один драйвер (и layer?) мог зарегистрировать на себя более одного устройства, и они б корректно освободились.
(Хотя СЕЙЧАС оно корректно себя вести не будет -- поскольку пытается
делать SetDevState()
, а оное к layer'у применимо едва ли.)
d
(номер-адрес-ячейка
устройства на линии, де-факто kid), что и во включающем цикле по поиску
устройств по devid.
other_d
".
Зачем это было бэк-портировано туда -- хбз; видимо, для унификации. Ну для унификации и там пофиксено.
10.04.2024: какое ещё "бэк-портировано"?! Ведь выше за 25-07-2015 битым текстом сказано, что "как это, кстати, было сделано в v2'шном" -- т.е., там ИЗНАЧАЛЬНО было так.
Исправлено.
23.03.2016: вчера вылез еще один ляпсус в ту же степь: при попытке запустить в том же CANGW еще и линию#1 (/dev/can1), "смотрящую" в ту же сеть -- для отладки -- в логи полезли странные сообщения (парами)
DEBUGP: cankoz_q_erase_and_send_next: attempt to use unregistered dev=0:0 DEBUGP: ReturnDataSet(): devid=0(active=0) is inactive
Расследование показало, что причина крылась опять в
cankoz_fd_p()
, в самом конце, где решалось "интересует ли нас
этот пакет": стояла проверка dp->devid<0
(то return).
Сейчас заменена на надлежащую devid==DEVID_NOT_IN_DRIVER, баг пофиксен.
P.S. Чуток анализа: 1) косяк порождался в самом же cankoz_lyr_common.c (а не в драйверах), при попытке обработакть пакет 0xF8; 2) раньше не вылезало лишь потому, что никакие "неюзаемые драйверами" устройства на этой линии не присутствовали, а если бы присутствовали -- полезло бы (как и произошло с линией-alias'ом).
Проблема там в том, что вычитывание осциллограммы напрочь убивает обычную работу, т.к. это обмен кучей пакетов ("дай, на" на каждую ячейку осциллограммы). Точнее, проблем 2:
Следовательно, иметь осциллограммы "обычными" большими каналами, чьё чтение вызывается просто запросом "дай (прочитай) значение канала" -- низя.
Напрашивается простое решение: в дополнение к большому каналу иметь еще командный канал "измерь", по записи 1 в который и производится измерение, а просто запрос на чтение большого -- игнорируется.
Тем самым ответственность за потенциальные проблемы (успевающесть, синхронизация (НЕСКОЛЬКИХ устройств на одной линии, о чём по-девайсным драйверам договариваться неудобно)) перекладывается на юзера/оператора.
...кстати, такой же подход теоретически применим и для прочих "тормозных" систем -- см. описание потребностей салимовцев в разделе "Отключаемость каналов в cda" в bigfile-0001.html за 31-10-2006 (BTW, там в конце -- за 22-04-2009 -- прийдено к аналогичной идее).
03.08.2016: да, так и сделано -- по каналу "get_osc" на каждую линию. Работает.
19.09.2016: добавлены параметры
min,max, отображающиеся на privrec-поля
min_alwd
,max_alwd
. Используются при
min<max
.
Проверено -- ограничивают.
Единственный вопрос -- как отреагирует ist_cdac20_drv, когда уставка при включении так и не дойдёт до заданной; останется в состоянии IST_STATE_SW_ON_UP?
20.09.2016: как выяснилось, в самом ist_cdac20_drv.c также было ранее (~31-05-2016) сделано, но почти не использовалось (только в ql15).
min_hw_val
,max_hw_val
, заполняемые параметрами
min_hw,max_hw.
min<max
;
по умолчанию оба нули, поэтому изначально ограничение отключено.
29.10.2018: только, как оказалось, в конце таблицы было
забыто PSP_P_END()
. И как оно работало -- загадка (возможно,
никак: единственное использование -- [max_hw=1200000] у QL15 в
devlist-canhw-11.lst -- сейчас закомментировано (QL15 переехал на
vnn_ceac51, почему-то БЕЗ лимита)).
29.06.2017: переделано с device-global privrec-полей
min_awld
,max_alwd
на per-channel поля (в
advdac_out_ch_t
, они так и называются min
и
max
).
Причина -- внедрение этого и в прочие ЦАПы, которые многоканальные.
Функционируют так же -- используются при min<max
.
28.09.2016: технология:
out[N].spd
.
out[0].spd
оставлен, де-факто
alias на spd0.
common_spd
, заполняемое по spd, чтоб у 0-го не было
никаких особенных свойств.
05.10.2016: записки по теме:
q_foreach()
есть, в простейшем варианте -- только с указанием функции-компаратора и БЕЗ
возможности явного указания данных для сравнения; т.к. используется
исключительно для поиска STATUS-возвращающих пакетов.
cankoz_eq_cmp_func()
.
dlc
,data[]
(canqelem_t
передавать
нельзя ну никак, т.к. это внутренняя сущность cankoz_lyr_common.c).
06.10.2016: делаем.
CanKozCheckerProc()
.
q_foreach()
и
q_foreach_v()
.
cankoz_q_erase_and_send_next*()
, с которыми много общего -- и
указание модели с возможностью dlc<0, и наличие итератора.
foreach_checker()
.
И:
...
"?), поэтому у него параметры
checker
,privptr
вообще отсутствуют.
В smc8_drv.c вызов (c SQ_ERASE_ALL
) добавлен,
теперь как бы проверить...
Впрочем, не факт, что оно реально нужно с точки зрения производительности (точнее, экономии процессорного времени на syscall()'ах) -- см. анализ за сегодня в разделе по remdrv.
Подробности -- в bigfile-0001.html (т.к. изначально все соображения были там), раздел "CAN" за сегодня и предыдущие несколько дней.
27.12.2016: кстати, давно переводить драйверы
устройств с несколькими допустимыми кодами на add_devcode()
(вместо собственных сравнений).
04.01.2017: сделано. Коснулось всего 2 драйверов -- cdac20 (+CEAC51, +CEAC51A) и cgvi8 (+CGVI8M).
Всё тривиально:
_init()
в вызове add()
теперь указывается
"первичный" код устройства, плюс добавлено вызовов
add_devcode()
;
_ff()
убран блок проверки типа устройства.
03.04.2017: уже почти 3 месяца работает, неоднократно проверялось в реальных условиях, всё окей, глюков нету. Считаем за "done".
Смысл потребности -- поскольку магниты обладают некоторой реактивностью, то приходить в конечную точку надо максимально плавно (в идеале -- с нулевой производной), чтобы выбросов не было.
09.02.2017: форма "плавности":
14.02.2017: по результатам мыслей за прошедшие дни и разговоров с ЕманоФедей:
В качестве ускорения -- просто 1/10 от скорости.
17.02.2017: "поток сознания" по результатам обдумывания вопроса в течение нескольких дней.
А именно:
done = abs(cur-start); left = abs(cur-trg); if (done < 1*spd/10 || left < 1*spd/10) shift=1*spd/10; else if (done < (1+2)*spd/10 || left < (1+2)*spd/10) shift=2*spd/10; else if (done < (1+2+3)*spd/10 || left < (1+2+3)*spd/10) shift=3*spd/10; ... else if (done < (1+...+9)*spd/10 || left < (1+...+9)*spd/10) shift=9*spd/10; else shift = spd;
При линейном хождении это всё не проблема (и прекрасно отрабатывается в нынешнем коде). А вот при желаемых плавностях уже не так тривиально.
Т.е., есть ТЕКУЩАЯ СКОРОСТЬ, к которой и применяется ускорение (полагаем его за 1/10 от максимальной скорости).
...брать abs(cur-trg)*10/spd, и если оно в пределах [0...45], то брать коэффициент-мультипликатор из таблицы по этому индексу, а иначе считать за 10? Шибко уж завязано на то, что ускорение -- 1/10.
*_hbt()
будет отдельной веткой. И так же
из двух под-веток -- "вверх" и "вниз".
*_rw_p()
по записи в .*_CHAN_OUT_n* и
.*_CHAN_OUT_RATE_n* надо будет производить какую-то "подготовку" значений
для использования в _hbt()
-- чтоб та не делала каждый раз
толпу вычислений.
18.02.2017: еще мыслишки после прочтения письма ЕманоФеди от 14-02-2017-15:44:
Так вот: пусть это действительно будет per-channel-параметром.
#include
'ился ПОСЛЕ определения privrec, и содержал бы функции:
*_hbt()
-- просто целиком (там и таблиц касается).
Helper'ы можно сделать static
.
inline
...и полезен будет еще один файл -- с определениями, конкретно как
минимум cankoz_out_ch_t
.
18.02.2017@дома: отдельный вопрос -- полностью ли надо имитировать инерционность? При смене цели, если она в другой стороне -- надо ли продолжать идти, замедляясь, T0 шагов в старом направлении, или сразу стартовать (с начальной почти-нулевой скорости) в новом направлении?
21.02.2017: поговоривши с Федей -- НАДО продолжать идти, замедляясь. Смысл -- чтоб график оставался плавным, без изломов, так лучше для источников (изломы -- это прерывность производной, не-конечный набор гармоник (в Фурье)).
20.02.2017: немного об арифметике.
По странному совпадению сумма этой последовательности равна как раз 5.0.
21.02.2017: продолжение об арифметике.
В виде цикла по числу шагов ускорения, в котором вычислять путь, проходимый за время ускорения, а заодно -- число шагов, через которое пройденное во время ускорения расстояние превысит половину пути (и, соответственно, придётся начинать замедляться).
06.03.2017: Аскольд вернулся, проведён его опрос. У него всё просто (в его 8048?): никакие корни не считаются, а просто при начале движения запоминает, когда достигнута максимальная скорость, и начинает торможение на том же расстоянии до конца; также всё время проверяется, не дошло ли уже до середины, и если да, то также переходит к торможению. Мне же посоветовал всё считать предварительно в начале.
Йок...
23.02.2017@утро-душ:
23.02.2017: (~23:45) Федя, злобно ругаясь, ответил, что ни тот, ни другой вариант -- не катят. А надо, мол,
Ибо "тут дело в том, что если такое случится, то пролетев мимо точки ты направление подхода меняешь, т.е. влияешь на характер поведения системы, когда тебя об этом явно не просили".
23.02.2017:
Но там, как показывают прочие результаты, используется 16-битная арифметика.
Так что, увы -- чуда не произошло, магического рецепта не нашлось. Одна надежда осталась -- вдруг Аскольд что подскажет.
27.02.2017: начинаем делать.
DoCalcMovement()
, должная
выполнять все вычисления в любой момент. Она:
cankoz_out_ch_t
:
trg
-- куда требуют придти.
Идёт отдельно от ci
, чтобы при изменениях в процессе
движения старая цель оставалась известна? Хотя, с другой стороны -- а зачем
она нужна? Но так сделано в т.ч. по причине архитектуры работы
_rw_p()
, где прописывание out[l].trg
производится
в самом конце, уже ПОСЛЕ всех проверок/вычислений.
ci
(Chan Info) -- канал, над которым надлежит
произвести действие.
rp
(Result Pointer) -- куда складировать результат
вычислений.
Т.е., она НЕ меняет ничего в канале, а возвращает результат отдельно.
28.02.2017: продолжаем.
Исправлен. Проверено -- теперь пашет как надо.
30.04.2017: поскольку полностью на kinetic-вариант пока не переходим, то и в обычный cdac20_drv.c этот фикс добавлен. И в прочие -- cac208_drv.c, candac16_drv.c, ceac124_drv.c -- тоже.
DoCalcMovement()
начаты -- простой вариант цикла
"до середины диапазона или до максимальной скорости".
01.03.2017: далее.
out_cur*
и adc*
.
outrb*
и inprb*
, чтоб можно было их кидать
на график (может оказаться полезно для отсмотра корреляций работы аналоговых
каналов с двоичными у всяких блоков управления источниками).
cankoz_out_ch_t
.
_rw_p()
-- складирование полученных данных в
me->out[l].
_hbt()
-- использование при act!=0. Состоит из 3
основных частей:
Гипотеза:
Единственное сомнение: не будет ли при этом последний шаг слишком резким?
Короче -- надо еще поанализировать числа (драйвер при обсчёте в цикле печатает все шаги).
03.03.2017: несколько мыслей по результатам обдумывания:
Для этого надо как-то делать коррекцию на первом шаге торможения (или лучше перед ним) -- этот шаг должен быть не на полную (или полную минус ускорение) скорость/дистанцию, а так, чтобы придти в точку, из которой торможение закончится в нужной точке.
Как? Видимо, в DoCalcMovement()
постоянно помнить значение
суммы на предыдущем шаге, и как-нибудь так его запоминать, чтобы в
_hbt()
в нужный момент делать "подруливающий шаг".
02.06.2017: вот смотрю на код, и на числа отладочной выдачи (DO_DBG=1), как баран на новые ворота -- и нифига не понимаю... Уже даже график этого всего построил в gnuplot'е -- и всё равно идеи отсутствуют...
03.06.2017@утро-после-п.утех: ключ -- на "граничном" шаге между линейной ходьбой и торможением.
Ну так и надо сделать этот дополнительный шаг.
04.06.2017: неа, скорее по-другому: помнить, какого "рода"
был предыдущий шаг (для этого заведено неиспользуемое поле
a_dir
), и если только-только собрались переходить к торможению,
то очередной шаг сделать не на crs-acs, а на "разницу". Как вариант -- без
использования "памяти" a_dir, а просто сравнением: что собрались тормозить,
а текущая скорость всё ещё равна максимуму.
(В случае коротких перемещений (когда до максимума разогнаться не успевает) -- граница между ускорением и торможением. Тут надо какую-то вариацию решения придумать: то ли вставлять 1 "плоский" шаг (на фиксированной скорости, и вот на КАКОЙ скорости -- как раз зависит от "недостачи"), то ли как-то делить поровну пополам между последним шагом с ускорением и первым с замедлением...)
04.06.2017: по беглому взгляду на код вообще не очень
ясно, откуда берётся "недостача". Ключевое слово -- "dcd
".
04.06.2017: отдельный вопрос -- об арифметике. Как вычислить "недостачу"?
А вот как:
dcd
(это расстояния, проходимые при разгоне и
торможении),
mxs
-- это получится число шагов для прямолинейного
движения;
05.06.2017: (утро) роем.
DoCalcMovement()
: там стоит условие "pos <
d_half
", так что вычисления идут до чуть большего значения позиции.
Надо бы ограничивать не по pos<d_half
, а по
pos+spd<=d_half
.
Поскольку дальше стоит присвоение с проверкой
rp->dcd = (pos < d_half)? pos : d_half;
то фактически проблемы нет (и при реальной ходьбе будет использовано это же
ограничение); но для целей отладки выдача результата подзапутывает.
После планёрки: а вот и нифига! ВЛИЯЕТ!!! См. ниже.
После обеда: сделано.
Может, всё-таки прерывать (нулить num_isc
и всехние
.isc
)? Ведь таблицы-то прерываются.
05.06.2017: (после планёрки) делаем.
DoCalcMovement()
вдруг всё резко исправилось: никаких занудных
приближений, а ходит чётко
по нужной траектории.
Т.е., очень даже влияет тот промах не только на отладочную печать, но и на работу! Как так?!
А поскольку в присвоении dcd
стоит то условие -- "взять
наименьшее из pos,d_half" -- то раньше оно начинало тормозить слишком
ЗАРАНЕЕ.
Проблема в том, что оно вовсе НЕ ходит "чётко по нужной траектории", а втыкается в конечную точку на слишком большой скорости. Из-за того, что начинает тормозить уже слишком ПОЗДНО.
pos
(>=d_half, а не <=d_half!) от цели.
Оттого, что торможение начинается слишком рано. Что, в свою очередь, следствие НЕКРАТНОСТИ -- дистанция не делится на целое количество шагов из 3 участков (линейное ускорение, с постоянной скоростью, линейное торможение).
И да -- надо вставлять компенсационный шаг, на расстояние/"скорость" чуток меньше максимальной.
Добавлено поле dsr
(DeSiRed), в котором поддерживается это
самое "идеальное" значение:
_rw_p()
туда
копируется текущее значение .cur
.
_in()
производится
сравнение, что abs(dsr-cur)<THE_QUANT, и если не так, то делается
dsr:=cur.
_hbt()
вычисления
отталкиваются от него, и в него записывается "следующее" значение -- которое
val
, даваемое в SendWrRq()
.
Результат: ходить стало точнее. Хотя иногда всё же "перескакивает", т.к. отклонение оказывается 2uV (это квант у CDAC20).
@вечер-дорога-на-японский-мимо-ИАиЭ: а может, сравнивать не с THE_QUANT
, а с
THE_QUANT+1
? (Ну не с THE_QUANT*2 же...)
06.06.2017: пробуем. Вроде работает как надо -- уставки идут чётко по рассчитанному шагу. Но это CDAC20 с квантом 2, а надо будет еще на прочих проверить, которые с квантом 305.
06.06.2017: just for notice: примеры "проблемных" точек, когда у CDAC20 прочитанные микровольты отличаются от желаемых на квант (=2): 4500->4498, 31998->31996, 241996->241994.
Найдена: 0->1.25V на spd=0.3V@tac=30.
06.06.2017: по результатам пиления и исследований:
DoCalcMovement()
просто
немного промахивается относительно реальной ходьбы в _hbt()
,
оттого и несовпадение "предсказания" с реальностью.
07.06.2017: да, bingo!!!
Т.е., при разгоне успеваем пройти 20000 шагов, а при торможении -- всего 15000. В случае бОльших tac разница будет, соответственно, больше.
И разница интегралов -- как раз mns*(tac-1); вот оно лишние tac-1 шагов и ползёт в конце.
acs
, а на mns
(=acs/2), то вариант "0->1.2V при
spd=-0.3V@tac=30" стал ходить идеально.
...хотя что-то там и может быть -- как раз на 1 шаг, из-за того, что не факт, в нужные/скоррелированные ли момент делаются сдвиг позиции и проверка на замедление.
22.06.2017: Федя возмущается, что
И очень требует, чтобы всё-таки сбрасывалось бы мгновенно, а на моё возражение "как?" (о блокировке знает драйвер ИСТа, а плавное изменение реализует драйвер ЦАПа) предлагает завести ещё один канал, запись в который игнорировала бы скорость.
Но это-то понятный косяк, следствие недоделанности кинетического перемещения.
23.06.2017@утро-пляж: поскольку свободного места в стандартной карте каналов KOZDEV просто нет, то остаётся только занимать позиции, остающиеся из-за неиспользования полных диапазонов (которые рассчитаны на 32 канала ЦАП).
Таким образом, в нынешней карте, рассчитанной на 32-канальные ЦАП, можно будет поддерживать дополнительные каналы у 16-канальных устройств. Что вполне достаточно -- сей момент самым многоканальным ЦАПом является CANDAC16.
23.06.2017: делаем, на примере cdac20k.
CDAC20_CHAN_OUT_IMM_n_base
=KOZDEV_CHAN_OUT_n_base+CDAC20_CHAN_OUT_n_count
CDAC20_CHAN_OUT_TAC_n_base
=KOZDEV_CHAN_OUT_RATE_n_base+CDAC20_CHAN_OUT_n_count
DEVSPEC_CHAN_OUT_IMM_n_base
и
DEVSPEC_CHAN_OUT_TAC_n_base
.
_init_d()
для IMM добавлено указание RD и кванта
(TAC'ам оно вроде и не нужно).
HandleSlowmoOUT_IMM_rw()
-- тут есть отключение
slowmo, если оно идёт -- и
HandleSlowmoOUT_TAC_rw()
(этот очень прост).
HandleSlowmoREADDAC_in()
-- условную, при
DEVSPEC_CHAN_OUT_IMM_n_base>0
.
SwchToINTERLOCK()
.
Проверено -- на уровне драйвера ЦАПа работает. На уровне ИСТа будем проверять позже.
23.06.2017: мелкая модификация в преддверии
вытаскивания "мяса" в отдельный файл: advdac_out_ch_t
'шное поле
"act
" (AcCel Time) переименовано в "tac
" (Taccel),
под которым оно изначально указывалось в auxinfo и сейчас стало видно как
канал.
25.06.2017@дома-воскресенье-жара-ванна: а есть ведь место под каналы TAC и IMM даже и в более-чем-16-канальных ЦАПах! Под таблицы же отведена целая группа в 100 каналов, а реально используется не более 35 (TIMES=300, TAB_ALL=301, TAB_n_base=302...333, TAB_ERRDESCR=399). Таким образом, остаётся еще 65 штук свободных, т.е., как раз 2 по 32шт и еще один остаётся в резерве (а на ДЕЙСТВИЯ по таблицам всё равно есть место в группе CONFIG).
26.07.2017: cdac20k переименован в просто cdac20 -- оно теперь достаточно стабильно, дальнейшую доводку будем делать уже в advdac_slowmo_kinetic_meat.h.
25.11.2017: нашёлся милый ляп в
DoCalcMovement()
: первая строчка-страж имеет вид
if (ci->spd == 0 || ci->tac <= 0) return DO_CALC_R_SET_IMMED;
Т.е., она возвращает _SET_IMMED не только при отсутствии указания скорости, но и при неуказанности времени разгона/торможения. В результате -- плавное изменение не работает вовсе, а всегда уставка скачком.
История разбирательства:
(И, вроде бы, жаловался и раньше -- в сентябре-октябре, но ненастойчиво (другие дежурные замечали проблему), а я ответил, что такого быть не может, потому что не может быть никогда -- типа, ищите проблему в железе.)
Как с текущей сборкой v4c4lcanserver, так и с той от 07-09-2017, что на ctlhomes (она, впрочем, имеет тот же размер и отличается только MD5-суммой (из-за выдаваемой консольным соединениям в качестве issue даты компиляции)).
(Неудивительно -- ВЧ300, управляемые CANDAC16, тоже были в числе жертв.)
Загадка -- прямо впечатление, будто что-то сверхъестественное происходит. Но чудес ведь не бывает, ДОЛЖНА быть причина.
Так-то и нашёл причину.
26.11.2017: исправляем.
DO_CALC_R_SLOW_MOVE
.
HandleSlowmoHbt()
при tac<=0
использует только
.spd
, игнорируя поля для "плавности".
*rp
всё же нулится, чтобы избежать
обращения к неинициализированным значениям (мусор из стека -- мало ли, вдруг
какой-нибудь valgrind сочтёт трогание этого мусора ошибкой).
Проверено, работает.
27.11.2017: и на пульту задеплоено -- тоже работает.
25.02.2017: собственно (всё в разное время дня и в разных местах, по мере прогресса):
Возможно, потребуется некоторое сотрудничество от sendqlib'а -- sq_sendnext()? @вечер: а он уже есть, и, кстати, он void.
Также некоторый вопрос -- а как эта "врЕменная ошибка отправки" будет влиять на результат sq_enq() (результат проверяется при всяких REPLACE_NOTFIRST), когда мы ставим первый пакет и он тут же попытается отправиться?
@вечер: Так вот: sq_enq() никак не светит наружу результат отправки (вот незадача -- его и cankoz'у придётся узнавать окольными путями), а возвращает только результат постановки в очередь.
ip link set can0 txqueuelen 30
(вместо "30" -- да хоть 1000. И, кстати, "30" почему-то позволяет засунуть
31 пакет, и только на 32-м ругается "No buffer space available".)
28.02.2017: сделано простенькое дополнение в
ifup-can: при наличии в ifcfg-can* параметра
TXQUEUELEN
оно выставляет интерфейсу указанное значение.
Причина крайне проста:
IS_AUTOUPDATED_TRUSTED
-- и возвращаются
ТОЛЬКО из _ff()
(в _rw_p() стоит возврат из кэша, но сервер
запроса не пришлёт).
cankoz_fd_p()
вызов ffproc()
стоит ДО
дёрганья ->NOTREADY,->OPERATING.
Вот оно сначала возвращается новое значение, потом тут же болотовеет, и уже более никогда новое не возвращается.
И что тут можно сделать?
11.04.2018: да, проблема есть, и дальше будет только
хуже: получается, что ЛЮБЫЕ каналы, помеченные IS_AUTOUPDATED_*, которые
драйвер будет возвращать по собственной инициативе в
_ff()
::is_a_reset==1
, так и будут навеки
оставаться болотными (или пока по иной причине не обновятся).
_ff()
(бывший _rst()
в v2), затем щёлкается статус
->NOTREADY->OPERATING -- был принят еще 03-04-2006, и в
bigfile-0001.html за 27-03-2006 можно посмотреть причины.
_rw_p()
.
Но не поможет -- на эти каналы и запросы-то присылаться не будут: коль помечены "IS_AUTOUPDATED", то и отдавать надо всегда самостоятельно.
_ff()
, а из _in()
-- где самостоятельно ловить
GETDEVATTRS и код причины, приводящий к is_a_reset.
Оно, конечно, криво -- теряется смысл _ff()'а. Но на безрыбье...
А вообще надо б "на досуге" поразмыслить, как эту проблему -- уведомление драйвера и ПОСЛЕ щелканья тоже -- решить покрасивше.
12.04.2018: да, поразмыслил в фоновом режиме -- проклюнулась идея: пусть драйверы имеют возможность указать, КОГДА надо дёрнуть ихнюю _ff(): ДО щёлканья статусом или ПОСЛЕ.
И чтоб мог прямо парой битовых флагов указать, когда надо дёргать:
И чтоб _ff()'у как-нибудь передавалась стадия. Можно прямо теми же
битами в is_a_reset
-- оно всё равно в качестве булевского
работать будет.
(Тогда проблема болотовеющих HW_VER/SW_VER уйдёт у всех драйверов сама, автоматом.)
13.04.2018: подозреваю, что тем, у кого собственные аналоги регистровых каналов, так что им нужно сбрасывать rcvd,pend,req_mask -- CGVI8, CURVV, ...
13.04.2018: да, они самые: CGVI8, CURVV, PANOV_UBS, SMC8, TVAC320.
13.04.2018@утро-дома: одна проблемка: а КАК драйвер может указать это "КОГДА"?
add()
такового не предусмотрено (а надо,
НАДО было зарезервировать options
!).
А к какому?
devcode
однобайтный; но он может быть
-1
, а флаги могут потребоваться и таковым.
queue_size
-- но тоже криво.
08.08.2018@утро-душ: можно просто не продвинуть CANKOZ_LYR_VERSION_MAJOR
с =2 на =3 и добавить поле options
методу
add()
.
Сейчас, во время летнего перерыва, это вполне безопасно.
13.08.2018: делаем.
CANKOZ_LYR_VERSION_MAJOR
продвинута с =2 на =3.
CanKozAddDevice()
добавлен параметр
options
.
CANKOZ_LYR_OPTION_FFPROC_BEFORE_RESET
=1<<31 и
CANKOZ_LYR_OPTION_FFPROC_AFTER_RESET
=1<<30.
Плюс CANKOZ_LYR_OPTION_NONE
=0 -- чисто для порядку, чтоб её
прописывать вместо просто 0 у незаинтересованных драйверов.
kozdevinfo_t
добавлено поле options
.
cankoz_add()
-- надлежащий параметр
options
, который и сохраняется в поле options
.
cankoz_fd_p()
: собственно выполнение того, ради чего
вся бодяга и затевалась.
is_a_reset
. В
противном случае значения тех 2 битов опций игнорируются и делается всё
по-старому.
is_a_reset
, и биты ненулевые, то в
зависимости от наличия конкретного флага вызов делается ДО и/или ПОСЛЕ, с
передачей в качестве значения параметра is_a_reset
именно
соответствующей маски (так что сами _ff()
по-прежнему
использовать этот параметр как булевский).
add()
просто
CANKOZ_LYR_OPTION_NONE
.
Теперь осталось драйверам повставлять нужное... Или еще порядок вызова поменять -- чтоб по умолчанию было после?
13.08.2018@после-обеда: да, рискнём -- поменяем умолчание и понавставляем в драйверы, требующие именно пред-запроса, проверок.
cankoz_fd_p()
умолчание изменено -- теперь
обычный вызов делается ПОСЛЕ дёрганья статуса устройства.
Ура! Каналы HW_VER и SW_VER больше не остаются болотными после нажатия кнопки Reset.
...хотя каналы out_tab_errdescr остаются болотными. Но оно так и в предыдущем варианте было -- это уже косяк связки драйверов+advdac_cankoz_table_meat.h.
is_a_reset
==BEFORE.
Засим означенный косяк можно считать изведённым; хотя надо ещё понаблюдать за поведением драйверов -- не вылезет ли где чего.
MIN_ALWD_VAL
=-10000305, но в этом устройстве
используется 24-битная кодировка ЦАПа (а не 16-битная), поэтому реальный
минимум именно ровно -10000000.
Что интересно, этот косяк еще со времён v2'шного xcdac20_drv.c, но в первоначальном варианте cdac20_drv.c использовалось именно правильное -10000000 (этот драйвер делался в апреле 2008-го, и с задним числом некоторым вниманием к деталям кодировки -- т.к. там иной порядок байтов, то изначально оно не работало; см. bigfile-0001.html за 29-04-2008).
Причём этот вопрос также упомянут в bigfile-0001.html за 04-05-2008.
Вылезло это по причине 1-го глюка: при тестировании 21-04-2017 связки iset_walker+formula на живом CDAC20 было узрено, что уставка (из-за неучёта R заданная ниже -10V) плавно едет к -10V, а потом перескакивает в +10V и опять плавно едет вниз, и так почти до бесконечности (но через сколько-то циклов стабилизируется -- видимо, всё же в какой-то момент оно из-за плавной прецессии (чего, кстати?) как-то умудрялось подойти к "цели" ближе, чем rate/10).
26.04.2017: косяк увиден в пятницу 21-го, а руки дошли разобраться только сегодня.
Вся механика произошедшего (происходящего?) понятна, вопрос лишь в том, что и как исправлять. Есть несколько аспектов:
cdac20_val_to_daccode()
SendWrRq()
именно 3 байта, что в случае с
0xFFFFFFFF даёт 0xFFFFFF, что соответствует значению +9999998.
MIN_ALWD_VAL
-то в cdac20_drv.c надо ставить
равным -10000000
. Тут без вариантов.
MAX_ALWD_VAL
же, кстати --
+99999998
, т.к. до него округляется при записи +9999999
(код 0xFFFFFF).
...Или переделать конверсию?
27.04.2017: кстати, в кодировке ЦАПа CDAC20 есть какие-то странности -- см. ниже запись за сегодня.
vdev_sodc_cur_t.q
.
vdev_sodc_cb_t
никакого "reason" не
предусмотрено -- это чисто уведомление об обновлении данных.
...или брать в обход vdev -- напрямую у cda, спрашивая
cda_quant_of_ref(me->cur[n].ref)
? Но как понять, КОГДА
можно спрашивать?
27.04.2017: кстати, в описании CDAC20 имеется странная табличка (в описании CEAC51 идентичная):
Код (16-ричный) | Напряжение |
FFFFF8 | +10 В |
800000 | 5 мкВ |
7FFFF8 | -5 мкВ |
000000 | -10 В |
Это странно, т.к.:
Из таблицы следует, что 0x80000 соответствует +5uV, в то время как у нас в драйверах оно считается нулём, да и из блока при включении питания читается этот код, соответственно, должный соответствовать нулю.
Козаку написано письмо с этими вопросами, ждём ответа.
28.04.2017: ответного письма не получилось (почти :)), вместо него был продолжительный разговор по телефону.
Точнее, 21 бит включая знак.
Соответственно, младшие 3 бита в коде вообще игнорируются.
Поэтому на все эти "ой, ошибка на один битик!!!" можно забить.
Так вот: там вообще другая формула пересчёта используется, в несколько
стадий, а не наше "маппирование диапазонов через
scale32via64()
со сдвигом нуля в 0x800000".
Так что педантичная точность "до битика" неясно, насколько уместна (впрочем, кода пересчёта, работающего в самом CDAC20, у меня нет).
Итого:
Так и сделано, и в cdac20_drv.c, и в cdac20k_drv.c.
24.05.2017: ТУТ СЛОВА О КАРТЕ КАНАЛОВ (w10i) И ОБ УКАЗАНИИ "МЕТОК" -- что можно иметь "текущую", можно указывать её в auxinfo, а можно передавать значением прямо в команде записи (но лучше бы -- фиксированно, чтоб вся такая инфа была бы в конфиге); и что табличности у ЦАП-драйверов тоже должны б иметь метку.
25.05.2017@утро: на первый взгляд -- несложно, просто взять да написать.
25.05.2017: однако, нет! Ведь он должен быть БЕЗ устройства, а таковое в архитектуре kankoz_lyr никак не предусмотрено.
Хотя можно ли, БРОДКАСТНЫЙ-то? Есть ли такая возможность?
25.05.2017@вечер-офис-Бозона: и чо -- заводить какую-то дополнительную инфраструктуру, дополнительный вектор "безадресных" устройств, чтобы и их тоже проверять при закрытии линии (что её НЕ надо закрывать)?
Как-то кривовато...
26.05.2017@утро-дома: есть вариант красивее:
-1
, то оно
считается безадресным.
lineinfo_t.devs[]
, только массив увеличить вдвое и "безадресные"
устройства помещать в диапазон [64...127].
И более ничего -- т.к., "подтверждения" на broadcast-команды быть не может.
ffproc
и inproc
просто не
используются.
DECODE_AND_CHECK()
превратится в 2 разных варианта:
проверка для обычных драйверов (kid должен быть в диапазоне [0...63]) и для
"всех" ([0...127]).
Почти все вызовы будут годны "для всех", но вот
add_devcode()
, get_dev_ver()
и поддержка регистров
-- только для адресных.
Но есть сложности.
businfocount
,businfo[]
, откуда сам cankoz_lyr и
берёт нужную информацию.
Посему указывать "-1" нет никакой возможности.
line
из businfo[0]
, а драйвер-клиент заботится о проверке
businfocount сам.
Но это криво.
cankoz_add()
'у передавалось бы еще
что-то -- options, "driver class", ... -- откуда можно будет понять, что это
БЕЗадресный драйвер и трактовать его надо иначе.
Корректнее использовать 2-й вариант. Да, заодно продвинем
CANKOZ_LYR_VERSION_MAJOR
с =2 на =3 (хотя сейчас и неактуально,
т.к. не-CANGW-драйверов в эксплуатации и нет (кроме недоделанного на
ЛИУ-2), но так правильнее).
26.05.2017: поговорил с Беркаевым об опыте работы с таблицами на ВЭПП-2000.
На ВЭПП-5 пока вроде не надо, но может понадобиться.
Вывод:
Но если понадобится -- то практически готовый проект решения вон есть.
02.06.2017: кстати, есть при использовании бродкастного управления таблицами один аспект: поскольку оно происходит в обход драйвера, то оный драйвер будет не в курсе изменения состояния устройства.
DESC_GET_DAC_STAT
), анализируя биты статуса в котором драйвер
приводит своё знание в соответствие с текущим состоянием устройства.
Кстати, там (в v2'шных xcac208/xceac124) при окончании исполнения таблицы она из устройства тут же стирается, что похвально.
DESC_GET_DAC_STAT
и реализуя "машину состояний" (в
зависимости от текущего состояния анализировать биты статуса ЦАПа и
некоторым образом реагируя).
_hbt()
. Только не надо замусоривать линию посылками по 10 раз
в секунду (не надо?), а ввести счётчик, который когда достигает некоторого
значения -- пусть того же 10, чтоб было раз в секунду -- то сбрасывается и
отправляется пакет 0xFD.
TMODE_LOAD
: если вдруг недозагруженная таблица уходит на
исполнение (например, невовремя прилетел бродкастный старт), то стоит её
грохнуть. И, предварительно, удалить из очереди все пакеты, относящиеся к
загрузке.
...кстати, стоит экспериментально проверить поведение устройства -- как оно реагирует на попытки запустить незакрытую таблицу.
28.06.2023: чисто "для протокола": означенное
02-06-2017 реализовано, в advdac_cankoz_table_meat.h -- делается
опрос состояния исполнения таблицы при её наличии, посредством периодической
отправки DESC_GET_DAC_STAT
.
(Полез интересоваться вопросом broadcast'ных драйверов в связи с воскрешением ТНК, где для подъёма энергии оно может понадобиться.)
Делать будем в том же cdac20k_drv.c, который всё равно сейчас пилится.
И да -- для описания работ заводим отдельный раздел, ибо предыдущий в основном касается инфраструктуры broadcast'ных драйверов.
02.06.2017: потихоньку готовимся.
DoDriverLog()
, так что нифига видно не было.)
Вот такой канал и заводим -- самый последний, 399-й,
KOZDEV_CHAN_TAB_ERRDESCR
.
KOZDEV_CHAN_DO_TAB_*
, оказывается, были добавлены изначально.
KOZDEV_TABLE_CHAN_base
:
KOZDEV_CHAN_TAB_TIMES
(=300).
KOZDEV_CHAN_OUT_TAB_ALL
(=301).
KOZDEV_CHAN_OUT_TAB_n_base
(=302).
Таким образом, сначала идут каналы, имеющиеся всегда у всех типов устройств, а потом с переменным количеством.
04.06.2017@утро-ванна:
надо компоновать всё так, чтобы потом сразу выдернуть в отдельный
.h-файл, годный для #include
'нья в разные драйверы.
Состоять он должен из 3 частей:
cankoz_out_ch_t
.
Да, из этого первый пункт -- собственно ".h-файл" (интерфейс), а вторая пара -- реализация; 04.06.2017@на-работе, в 13-м:да-да, та самая, которая (тогда еще только в отношении таблиц? или как?) была запланирована для реализации в cankoz_lyr еще в 2009-м.
И еще некоторые соображения:
cankoz_out_ch_t
) и информацию, годную для
вызова "методов-реализаций" конкретного драйвера -- конкретно
SendWrRq()
. Оная информация включает в себя devptr (а не me!)
и номер линии (самим "библиотечным" не нужный).
НО!
pzframe_retbufs_t
в pzframe_drv,
cur_data[]/prv_data[] в pzframe_data; да и в vdev_context_t
используется аналогичная схема) -- вполне сработало бы.
Но громоздковато и неудобно -- проще прямо так, с недоинкапсуляцией.
_in()
и _fd_p()
.
cankoz_out_ch_t
в,
например, dac_out_ch_t
.
06.06.2017: думал-думал -- а может, правда всё-таки
сделать "в стиле pzframe", с объектом-описателем, содержащим количества и
ссылки на массивы? И в нём же и num_isc
с
t_mode
...
07.06.2017: работаем.
cankoz_out_ch_t
в
advdac_out_ch_t
.
Будущий файл назовём advdac.h -- ADVanced DAC.
KOZDEV_CHAN_TABLE_ID
, чтоб в ём можно было указывать
идентификатор таблицы.
Следствие: массив t_times[]
становится одномерным и
проверять консистентность времён разных каналов не требуется ввиду
отсутствия этих РАЗНЫХ времён.
Следствие: неуказанность каналов для работы в таблице можно
определять по нулевой длине их индивидуальных векторов. А по значению
_VAL_DISABLE_TABLE_CHAN
-- можно оставить для канала
_TAB_ALL
(т.е., в индивидуальных оно пусть тоже работает, но
особого смысла нет).
07.06.2017@вечер: ПЛОХО ВСЁ! Сложно заунифицировать табличный код всех типов козоЦАПов, т.к. у них разный формат таблиц, в т.ч. разный формат упаковки значений, а также формально разными могут быть даже CAN-команды работы с таблицами.
Да и глубокой потребности нет -- использование таблиц минимально, и неохота вкладываться в реализацию технологии, у которой пол-потребителя.
Проще оказывается делать всё кодом прямо в драйверах, "параметризуя" при помощи текстового редактора (при правильном выборе структуры и имён работа сводится к копированию и контекстной замене) и препроцессора.
08.06.2017@утро: а ведь некорректно ВСЁ сваливать в один файл "advdac", ибо:
Поэтому надо делить файл на 3 части:
Так что:
Если когда-то вдруг припрёт -- то, в принципе, можно это всё запараметризовать: указанием варианта "структуры таблиц", типов конверсии, ...(что еще?) -- как оно "планировалось" в 2009-м при обдумывании СУ для ТНК. Реализация этой параметризованности свалится в отдельный файл (advdac_slowmo_cankoz.??), код в котором должен будет знать и поддерживать все варианты.
...опять же, параметризовать -- тоже можно, но особого смысла не видно.
08.06.2017: посему всю дальнейную деятельность по таблицам будем записывать в отдельном разделе, а этот оставим для модульной структуры будущего "advdac в целом".
17.06.2017@дома-жара: а ведь варианты "плавного изменения" вовсе не обязаны быть фиксированы только как "прямолинейное" и "с ускорением"!
Учитывая, что реализация будет жить в отдельном (.h?)
файле, можно делать их РАЗНЫЕ, #include
'а нужный конкретному
драйверу.
...другое дело, что в иных вариантах никакого смысла пока не просматривается.
21.06.2017: теперь, когда таблицы в основном вроде бы -- работают что дальше делать? Правильно -- рефакторинг, чтобы подготовить код к вытаскиванию в отдельные файлы.
CDAC20_TABLE_MAX_NSTEPS
,
CDAC20_TABLE_MAX_STEP_COUNT
, ...
CDAC20_VAL_TABLE_DISABLE_CHAN
->
CDAC20_TABLE_VAL_DISABLE_CHAN
CDAC20_VAL_TABLE_START_FROM_CUR
->
CDAC20_TABLE_VAL_START_FROM_CUR
enum
- (НЕ
#define
-!) константы с префиксом DEVSPEC
(на него
заменяется имя устройства): например,
DEVSPEC_TABLE_MAX_NSTEPS
=CDAC20_TABLE_MAX_NSTEPS.
(Сначала хотел использовать префикс
DEVTYPE
, но он менее однозначен; а слово "DEVSPEC" используется
только в uspci_test.c.)
DEVSPEC_CHAN_ADC_n_count
.
CDAC20_
на DEVSPEC_
.
Смысл:
advdac_out_ch_t
, он давно готов.
SendWrRq()
/SendRdRq()
.
Всё перетащено в один последовательный кусок.
Туда вошли ReportTableStatus()
, SetTmode()
,
EraseTable()
, DoTabActivate()
и
SendTabCtlCmd()
с подмастерьем
OnSendTabCtlCmdCB()
.
А вот куски, сейчас являющиеся фрагментами других функций -- в первую очередь, _in() и _rw() -- пока нет.
22.06.2017: продолжаем рефакторинг.
HandleTableHbt()
вычитывание
каналов и запрос статуса ЦАПа.
HandleDESC_FILE_CLOSE()
и
HandleDESC_GET_DAC_STAT()
обработка соответствующих пакетов.
Пока напрашивается это:
Нет,
ConsiderRequest()
.
Итого -- вынесено в HandleTable_rw()
, которой передаются как
action
и chn
, так и n
и изначальные
указатели dtypes
,nelems
,values
. А
она внутри себя содержит дополнительные проверки на тему "правильных"
dtype,nelems (только INT32-скаляры для всех, кроме векторов, для которых
INT32 без учёта nelems).
HandleSlowmoOUT_rw()
.
И за компанию запись в _CHAN_OUT_RATE_n... -- в
HandleSlowmoOUT_RATE_rw()
(изменение скорости в процессе
движения тоже надо будет отрабатывать -- оно оговаривалось, просто пока не
сделано).
HandleSlowmoHbt()
.
23.06.2017: далее продолжаем рефакторинг.
HandleSlowmoOUT_IMM_rw()
и
HandleSlowmoOUT_TAC_rw()
.
HandleSlowmoREADDAC_in()
.
val_to_daccode_to_val()
. Что делает, ясно из названия, а нужна
для имитации "получения последнего пакета" в момент получения ПЕРВОГО ответа
от устройства (в этот момент и делается возврат значения канала уставки --
чтоб значение считалось изменившимся только после реального начала
модификации в железе).
Итого -- 980 строк; сам же cdac20k_drv.c стал всего 919 строк.
Засим покамест всё.
Но наверняка будут еще какие-то действия/тюнинг/оптимизации.
23.06.2017: теперь адаптируем к этой архитектуре прочие драйверы.
SendWrRq()
/SendRdRq()
, а вместо этого просто
отправка пакетов прямо из _rw_p().
Собственно действия по адаптации:
26.06.2017: далее адаптация.
KOZDEV_CHAN_TABLE_ID
превратилось в
KOZDEV_CHAN_OUT_TAB_ID
KOZDEV_CHAN_TAB_TIMES
стало
KOZDEV_CHAN_OUT_TAB_TIMES
KOZDEV_CHAN_TAB_ERRDESCR
--
KOZDEV_CHAN_OUT_TAB_ERRDESCR
Полученный результат тоже не сказать, что особо красив (в первую очередь потому, что каналы-команды имеют имена безо всякого "out"); но всё более однозначно.
27.06.2017: далее (реально делалось равномерно за 3 дня -- также и 28.06.2017 и 29.06.2017).
devname_TABLE_...
. Значения для конкретно
devname_TABLE_MAX_FILE_BYTES
посмотрены в документации.
DEVSPEC_*
.
val_to_daccode_to_val()
.
cankoz_out_ch_t
на advdac_out_ch_t
,
беримый из за-#include'нного advdac.h.
Но давно напрашивается сделать min_alwd/max_alwd per-channel-параметрами (чтоб и для многоканальных были б доступны).
29.06.2017: сделано повсеместно. И даже в табличных каналах теперь оно проверяется (правда, в момент записи канала, а не в момент активации).
_init_d()
: добавлены SetChanRDs()
(на _IMM
и на табличные) и SetChanQuant()
.
THE_QUANT
.
_ff()
: supports_unicast_t_ctl
выставляется в
зависимости от sw_ver.
_hbt()
: переведены на HandleSlowmoHbt()
и HandleTableHbt()
.
_in()
: переведены на
HandleSlowmoREADDAC_in()
, а также добавлены
HandleDESC_FILE_CLOSE()
_rw_p()
:
#if
-0'енный
возврат "значение, равное номеру канала".
HandleSlowmoOUT*_rw()
.
KOZDEV_CHAN_ADC_MODE
и KOZDEV_CHAN_OUT_MODE
, т.к.
они теперь каналы чтения и полностью обслуживаются общей веткой по
CONFIG-каналам.
_params[]
и _init_d()
.
candac16_init_d()
: указанное для 0-го канала копируется в
свойства других лишь если для них это свойство не указано.
Таким образом, можно указывать либо device-global (spd=...), либо индивидуально (spd5=...). Но если надо указать для 0-го канала, то оно становится общим для всех, и при нежелательности оного надо указывать всем в обязательном порядке.
_params[]
указываются через
макрос ONE_LINE_PARAMS()
-- иначе было б слишком длинно (кроме
одноканального cdac20).
Но! Ведь при работе с таблицами эти команды используются раздельно и по-разному: в очередь DAC_STAT кладётся ons'ом, а DEV_STAT -- INF'ом.
11.07.2018: а почему "c _ons'ом"-то?! При том, что парой
абзацев выше сказано "DEV_STAT -- INF'ом"; и в v2'шном
xcac208_drv.c сепаратор кладётся в очередь посредством
q_enqueue
(который там даёт oneshot=0), а не _ons'ом. Оно,
похоже, работает, но всё же почему так сделано? Огрех из-за копирования
предыдущей команды?
Делаем:
DEVSPEC_TLOAD_SEP_DESC
, которое если #ifdef, то
используется в DoTabActivate()
вместо умолчательного
GETDEVSTAT.
CrunchTableIntoFile()
:
30.06.2017: завершаем.
23.07.2017@вечер-~22:00-пока-ставился-CentOS-7.3-на-v5p1:
Вытащено в свой раздел. Начало -- в предыдущем, который теперь по "advdac вообще".
08.06.2017: продолжаем:
advdac_descr_t
(с
дубликатами devid
, handle
, devptr
,
lvmt
) увольняем, а поле t_mode
засовываем напрямую
в privrec.
SetTmode()
-- скопирована из v2'шного
xcac208_drv.c (который, кстати, используется как образец для всей
табличности).
EraseTable()
.
Отправка CREATE и CLOSE в ней делается не напрямую, а через
SQ_TRIES_ONS
.
09.06.2017: далее.
SQ_TRIES_DIR
.
Причём делается с проверкой на SQ_ERROR, и если ошибка -- то
SetTmode()
не дёргается.
...возможно, надо делать, что при ошибке пакет ставится в очередь с _ONS;
но тогда надо делать callback'и, чтобы SetTmode()
вызывалось
прямо после отправки пакета.
11.06.2017: добавлено -- при ошибке ставится в очередь
с _ONS и on_send-callback'ом. Как выяснилось, в cankoz_lyr'е всё сделано
"по уму" -- драйверный callback вызывается с нужными/удобными параметрами,
включая devptr; так что собственно функция-callback сделана единственная,
TabOnSendCB()
. Что немного НЕудобно -- при
SQ_TRIES_DIR
callback НЕ вызывается, даже если он указан; а то
было бы вообще всё мега-просто.
11.07.2018: результаты разборок (вчера вылезли странности при запинывании weldproc_drv.c):
DESC_FILE_START
умудряется влезть аккурат между запросом GET_DAC_STAT (отправляемым
периодически в режиме TMODE_ACTV) и ответом на него.
DAC_STAT_EXECING
и
DAC_STAT_EXEC_RQ
нулевые, так что
HandleDESC_GET_DAC_STAT()
честно считает выполнение таблицы
завершённым.
EraseTable()
, радостно
грохающий таблицу.
DESC_U_FILE_STOP
.
А исполнение таблицы прекращалось.
Так что, вероятно, и в прочих случаях не прокатило бы, и лишь удачное стечение обстоятельств (race condition -- тут как повезёт с условиями) позволяло тогда год назад не наткнуться на проблему.
DESC_GET_DAC_STAT
почему-то повсеместно делается как _ONS.
Так что даже если избавиться от _DIR-отправки команды START, она всё равно сможет влезть между запросом GET_DAC_STAT и ответом -- т.к. после отправки оного запроса он выкидывается, очередь пуста и любая поставленная в него команда будет легитимна.
Вывод: надо
_ons
в отправке DESC_GET_DAC_STAT
.
Сделано. Заодно у п.2 "how" сменено с вездешнего
SQ_IF_NONEORFIRST
на разные варианты по ситуации.
...только оказалось, что НИГДЕ не делается erase_and_send_next для пакетов GET_DAC_STAT -- видно, потому, что
В результате первая же его отправка в очередь приводила к молочению с частотой 10Гц и остановке всего прочего.
Добавлено -- теперь вроде пашет.
Как-то умудряется ловить пакет GET_DAC_STAT с mode==0x00.
Можно, конечно, отпраздновать труса и просто отключить все "мозги" по
догадыванию о реальном состоянии таблицы -- достаточно просто убрать все
запросы GET_DAC_STAT. Но некомильфо так...
q_erase_and_send_next_v()
в
HandleDESC_GET_DAC_STAT()
: оно делалось в начале, а надо было
переставить в конец -- тогда всё заработало.
Объяснение:
OnSendTabCtlCmdCB()
--
чтоб именно после реальной отправки пакета, а не при постановке его в
очередь.
И типа предпринимаются некоторые меры, чтоб обеспечить конкретную последовательность отправки/обработки пакетов -- то самое отсутствие _dir и _ons, чтобы "следующий не отсылался, пока не обработается ответ на предыдущий".
q_erase_and_send_next_v()
...
OnSendTabCtlCmdCB()
, делающий
SetTmode(,TMODE_RUNN)
.
HandleDESC_GET_DAC_STAT()
, который смотрит: mode==TMODE_RUNN?
Значит, надо проверить, что если mode=0x00, то считать исполнение таблицы
завершенным.
Вот в чём косяк: была нарушена ПОСЛЕДОВАТЕЛЬНОСТЬ (за которую так боролись), и обработка результата 0xFD происходила уже после отправки 0xF7 -- в ошибочном предположении, что устройство успело сменить режим работы и обрабатываем ответ на 0xFD, отправленный ПОСЛЕ смены tmode.
DESC_FILE_START
попадал в пустую очередь: тогда он сразу же отправлялся, и вышеописанный
сценарий с "перемешиванием" не происходил.
Итого: вызов удалить-и-отправить-следующий переставлен, всё стало зашибись.
В качестве резюме/наблюдения: очень фигово, когда приходится делать работу, которая должна была делаться самим устройством (уведомление о смене состояния девайс должен был бы отправлять сам, а не по постоянному поллингу). Работа должна выполняться в ПРАВИЛЬНОМ месте, наиболее естественном для неё. Иначе получается вот такая засада: имитировать-то можно, со всем этим догадыванием, но этот "искусственный интеллект" очень уж замутен и его реализация чревата косяками из-за неочевидного взаимодействия подстилающих механизмов. Чем проще и прямолинейнее -- тем надёжнее.
KOZDEV_CHAN_TABLE_ID
-- ID таблицы:
...кстати, забыто было добавление в карты .devtype.
10.06.2017:
SUPPORTED_chans[]
дополнен каналами _DO_TAB_* и
_TABLE_ID.
SetTmode()
.
13.06.2017: дополнена проверкой
supports_unicast_t_ctl
-- при ==0 затронутые каналы дополнительно
OR'ятся значениями массива no_unicast_v[]
, в котором
2
стоят в позициях STOP, PAUSE, RESUME. Смысл в том, чтобы для
блоков, чьи прошивки не поддерживают эти команды в unicast-вариантах,
запрещать их (не теша юзера пустыми надеждами). Проставление
supports_unicast_t_ctl
в _ff(), в зависимости от сочетания
devcode/sw_ver сделано еще несколько дней назад.
16.06.2017: из позиции STOP двойка убрана. Смысл в том, что команда STOP должна быть доступна всегда -- для ухода из TMODE_ACTV в TMODE_NONE.
11.06.2017:
TAB_MODE_IS_NORM()
-- это просто
me->t_mode<TMODE_LOAD
.
Аспекты реализации:
DATAKNOB_USER
?
Следствие: в идеале -- чтоб эти компонент(ы) были бы прямо в стандартном наборе MotifKnobs, дабы быть доступными ВСЕМ программам (т.е., чтобы присутствовать в бинарнике pult).
user_began_knob_editing()
/cancel_knob_editing()
плюс зануление k->usertime
в надлежащих местах.
Учитывая, что тут у нас МНОГО полей ввода -- хбз, как делать.
Кста-а-ати, ведь хорошо бы еще нумеровать строки! И вот над столбцом номеров -- можно сделать эту пару кнопочек.
XtManageChile()
единственного XmText'а?
12.06.2017:
OnSendTabCtlCmdCB()
.
SendTabCtlCmd()
:
16.06.2017: теперь всегда из 3 байт, ради нолика для команды RESUME (в противном случае она работает как GO_NEXT).
Кстати, а не засылать ли сразу после этих пакетов ещё (ons'ом)пакет GET_DAC_STAT, для "проверки" отработки команды?
15.06.2017: да, сделано.
_rw_p()
стоят просто вызовы её.
t_file_size
и
t_file[]
. Последний пока размером [1]
, т.к. надо
еще хорошенько подумать, по какой формуле считать максимально возможный
размер "файла" (код в xcac208_drv.c несколько неочевиден).
14.06.2017: потребный размер массива определён чтением
документации и помещён в CDAC20_MAX_FILE_BYTES
.
KOZDEV_CHAN_TAB_ERRDESCR
) сделана
ReportTableStatus()
.
13.06.2017:
ReturnInt32Vect()
.
Запись работает только при не-активированности таблицы, иначе просто игнорируется.
Обработка _TAB_ALL пока отсутствует (лень возиться).
14.06.2017: копирование дополнено вгонянием значений в диапазоны. 0-м элементам векторов вольтов выход за границы [MIN,MAX] разрешен в случае, если они за границами (-20V,+20V) -- для указания "отключен" и "стартовать с текущего".
SetChanRDs()
для табличных каналов
(а вот кванты им ни к чему) плюс
SetChanReturnType(,,,IS_AUTOUPDATED_TRUSTED)
для
KOZDEV_CHAN_TAB_ERRDESCR
и начальный возврат пустой строки.
14.06.2017:
Только пара аспектов:
t_vals[][]
трогать нельзя!
Да всё просто -- ругаться и отваливать.
Это отличается от v2'шного, где считалось aff=abs()<20V.
cda_rd_convert()
, и результат использовать.
Этот вариант надо проверить; хотя он в любом случае сомнительный.
Пожалуй, не станем: просто ради удобства пользовательского интерфейса.
(Сомнительно, ну да ладно -- посмотрим потом.)
Сделано всё, кроме
CrunchTableIntoFile()
.
15.06.2017:
Однако, можно обойти: ведь нас интересуют в первую очередь
результирующие байты, а их можно получать так:
bytes3456=delta32bit/nsteps
,
bytes01=delta32bit%nsteps
.
@после-обеда однако, нет: нам ведь нужно при формировании "файла" на каждом шаге помнить текущее значение "аккумулятора", чтобы считать дельту от него; соответственно -- всё-таки придётся иметь 48-битную арифметику...
CrunchTableIntoFile()
. На int64.
EraseTable()
:
wipe_channels
-- что НАДО очищать
также и внутренние данные о каналах.
(Изначально это в нынешнем варианте -- с
унификацией всего удаления (и из девайса, и из каналов) в
EraseTable()
-- делалось принудительно, что совсем излишне --
т.к. требуется только по _DO_TAB_DROP (кстати, а когда эта команда была
придумана и для чего?!).)
SendTabCtlCmd()
. Причина оказалась банальной --
усовершенствованный (для вызова callback'ов даже при _DIR) sendqlib
влинкован в сервер, а тот не был в /tmp/4cx/sbin/ обновлён.
16.06.2017: да, "только вверх" -- там реально было
накосячено с арифметикой или, скорее, с логикой. Там же отдельные ветки для
"вверх" и "вниз", и в обоих случаях delta
высчитывается
положительная, как и инкремент this_incr
=delta/nsteps; но
раньше оно этот инкремент сохраняло в массив f_incrs[][]
, а
сейчас использует для сваливания в файл сразу. Вот и писало ВСЕГДА
положительный. Исправлено (в ветке "вниз" добавлен минус) -- всё стало
окей. Кстати, сами эти 2 раздельных ветки выглядят
странно. Вполне можно считать всё в одной, и получится ровно что
надо.
30.06.2017: нифига, НЕЛЬЗЯ в одной! Как выяснилось сегодня при проверке аналогичной функции для 16-битной кодировки (32-битные аккумуляторы), одна ветка прокатит лишь если коды никогда не вылезут в знаковый бит. В 24/48-битной кодировке для CDAC20 они в знаковый бит 64-битных слов не вылазят, а вот в 16/32-битной у 32-битных слов -- ещё как (коды для отрицательных вольтов).
16.06.2017: там просто при отсылке пре-программирования не проверялось на тему "начинать с текущего" и всегда отправлялось значение 0-го шага; а +20...+30V при конверсии в 24-битный код ЦАПа давали переполнение, выглядевшее как -10V. Исправлено.
Явно какие-то косяки с арифметикой
P.S. А всё-таки -- почему тогда, в 2009-м, именно ТАК была выбрана архитектура, со стирающим всё DROP'ом, но без возможности просто снять активированность?
16.06.2017: а вообще БЫЛА/есть возможность снять активированность -- именно через STOP, который разрешен в состояниях ACTV, RUNN, PAUS. Блокируется это только в случае не-поддержки unicast-команд; так что просто убираем блокирование для STOP'а, и всё становится тип-топ (ну да, у девайсов со старой прошивкой реальной остановки из состояния исполнения не будет, но тут уж йок).
Кстати, тестирование ведётся с помощью скрина от CAC208 -- потому, что в нём есть панель управления таблицей, а в cdac20.subsys пока нет (ну да, горят большинство каналов чёрным NOTFOUND -- ну и ладно).
16.06.2017:
Вроде сделано. Хотя еще не проверено. Парой часов
позже: проверено, работает.
RESUME исправляется просто: ей нужен 3-й байт со значением 0.
Написан e-mail Козаку (а конкретика записана в notes/20170616-CDAC20-PROBLEMS.txt).
17.06.2017: Козак ответил: он с понедельника в отпуске на 2 месяца, с пока неясными перспективами разборок. Буду договариваться.
.spd/HEARTBEAT_FREQ
) и при дальнейших шагах). 20.06.2017: done
18.06.2017@дома-жара:
предвидится некоторая проблема с определением изменений состояния ЦАПа по
анализу поля status в реакции на DESC_GET_DAC_STAT
при
периодическом поллинге.
Возможна такая последовательность:
DESC_FILE_START
,
а заодно делается SetTmode(,TMODE_RUNN).
И в этом ответе в статусе биты b0 и b1 ещё нулевые.
Ну и что с этим делать?
Но это уже и так делается, да и не поможет оно с race condition'ом.
Но это ОЧЕНЬ плохой вариант:
На вид -- вроде бы должно дать нужный эффект, хоть механизм и некрасивоватый. Но есть пара сомнений:
Вроде несмертельно -- ну проигнорирует один лишний статус, потом же всё равно придёт либо через секунду, либо по окончанию исполнения таблицы.
Тут тоже вроде несмертельно -- аналогично п.1.
19.06.2017:
Методика эксперимента: запускаем таблицу, в которой есть длинный этап уменьшения, и в ходе него canmon'ом шлём пакет "запиши -9V".
Результат: арифметическое переполнение -- после ухода до упора вниз, за -10V, значение вылезло сверху, в +10V и далее вниз. Это на CDAC20.
Проверено -- вроде работает.
20.06.2017: добавляем проверки скорости -- первого шага (он типа мгновенный) и следующих.
Поскольку там условия были обратными -- что "можно", в то время как тут
что "нельзя" -- то, чтобы их не перекурочивать, просто поставлен
!
в начале.
.spd/HEARTBEAT_FREQ
.
Посему...
CDAC20_STEPS_PER_SEC
=100, а имевшийся ранее
CDAC20_USECS_IN_STEP
теперь считается через него (вместо былого
=10000).
21.06.2017: причёсывание.
SetTmode()
почти всегда сопровождается выдачей
краткого сообщения (БЕЗ параметров) в _TAB_ERRDESCR.
Напрашивается, чтоб ей добавить ещё один параметр, в котором бы можно было указывать оное сообщение -- опционально.
message
, который если !=NULL, то
делается
ReportTableStatus(, "%s", message);
.
message
добавлен и к
EraseTable()
.
Бонус -- теперь во все смены режима добавлено информирование о состоянии.
И, кстати, команда "broadcast'ный STOP" (1) не функционирует вовсе.
Козаку всё написано.
26.06.2017: серьёзный шаг: увеличиваем
*_TABLE_MAX_NSTEPS
с 29 до 31. Смысл -- де-факто все
устройства позволяют таблицы до 30 шагов; плюс ещё 1 шаг на инициализацию.
Почему тогда, 8 лет назад, было выбрано 29 -- вопрос. Очевидно, перестраховка.
29.06.2017: сделаны CrunchTableIntoFile()
для остальных 3 ЦАПов. Реально это всё одно и то же -- поскольку кодировка
данных и форматы "файлов" одинаковы.
30.06.2017: проверяем.
int32
. Устройство кода таково, что он
рассчитан на БЕЗзнаковую арифметику; а при сдвиге влево на 16 бит (для
имитации козачиного 32-битного аккумулятора) у кодов для отрицательных
вольтов появлялся 31-й бит.
int32
всё заработало. И с CANDAC16, и
с CAC208 (CEAC124 проверить не на чем).
int64
потому, что кодировка 24-битная, а после удвоения для
аккумулятора -- 48-битная; т.е., знаковый бит всегда 0.
Так что НАДО оставить 2 ветки (вверх и вниз), иначе в беззнаковой арифметике единая ветка, полагающаяся на правильную знаковость, может не прокатить (хотя и не факт -- учитывая свойства дополнительного кода, может и прокатить; но полагаться на это в любом случае чревато).
13.08.2018: канал out_tab_errdescr остаётся болотным по приходу 0xFF/is_a_reset.
IS_AUTOUPDATED_TRUSTED
и реально возвращается ТОЛЬКО по
инициативе advdac_cankoz_table_meat.h'а, но НЕ по щёлканью статуса
устройства.
Но что именно в нём делать -- неясно:
...а какие еще неочевидные аспекты могут вылезти?
27.02.2023: обнаружилось (на примере CAC208 у ringrf), что кнопки [Table...] горят болотным. А точнее -- каналы out_tab_errdescr.
Стал разбираться.
_init_d()
вызовы
ReportTableStatus(devid, NULL);
которые должны вернуть out_tab_errdescr="" (NULL
интерпретируется как пустая строка).
Загадка, да?
Так вот:
cac208[6]rez208/DEFAULT: GETDEVATTRS: ID=5,23, DevCode=4:CAC208/CEAC208, HWver=1, SWver=3, Reason=0:PowerOn
Т.е.: сначала сервер стартовал и всё было OK, но потом девайсу дёрнулось
питание (просадка?), а
cankoz_lyr_common.c::cankoz_fd_p()
по
CANKOZ_IAMR_POWERON
, считающемуся за is_a_reset
,
дрыгает состояние ->NOTREADY,->OPERATING, вследствие чего канал
поболотовевает и никогда не возвращается.
is_a_reset
сбрасывать состояние
таблицы, включая и строку ошибок.
Но, хоть во всех 4 драйверах и стоит вызов
DoSoftReset(me, 0)
но с таблицей он НИЧЕГО не делает. Хотя по-хорошему -- надо бы.
DSRF_TMODE_NONE
, по которому
исполнять
SetTmode(me, KOZDEV_TMODE_NONE, NULL);
is_a_reset
.
_init_d()
вызовы
DoSoftReset(me, DSRF_RESET_CHANS)
, ...
SetTmode()
убрать.
Сделал, ровно по этому проекту.
Единственный косяк -- поскольку SetTmode()
живёт в
advdac_cankoz_table_meat.h, который #include
'ится
ПОСЛЕ DoSoftReset()
, то пришлось добавить локальную
forward-декларацию. ...вообще столько общего между 4 драйверами, что
напрашивается вытаскивание в какой-то общий файл, но вот в какой...
На пульт скопировано, потом как-нибудь посмотрим (не вылезет ли чего).
10.06.2017: делаем.
KOZDEV_CHAN__T_MODE
=46 (тройной подчерк в имени).
-N
-- печатать
порядковые номера принимаемых пакетов (Панову понадобилось).
19.04.2018: детали:
option_numbers
.
@time#NNN
а иначе
@#NNN
т.е., ВСЕГДА номер "прикрыт" собачкой, чтобы при чтении списка команд из
файла эта информация не мешала бы.
25.10.2021: кстати, именно тогда разошлись ("разфорковались") исходники в v2hw/ (оставшийся старым, от 29-11-2012) и в hw4cx/ (продолживший развиваться).
А потом уже добавились ожидание N пакетов (":-N") и смена концепции имён ("CAN_HAL" вместо старого "CANHAL"); последнее привело к исчезновению совместимости -- теперь уже нельзя плюхнуть новый исходник в v2hw/, не соберётся.
25.05.2018: после некоторых размышлений сделано, возможностью указывать отрицательное число в команде ":". Например, ":-1" означает дождись 1 пакета (":-5" -- аналогично, 5 пакетов).
Т.е., если после ':' идёт положительное число, то это миллисекунды для ожидания, а если отрицательное -- то это минус-количество пакетов.
Детали реализации:
count
-- с
"начальным", которое сохраняется в start
.
count
(он был введён в апреле для ключа
-N
) чуть изменена: он теперь считается ВСЕГДА.
Т.е., теперь ":0" эквивалентно просто ":".
Немного соображений по выбору способа указания ограничения.
-c COUNT
.
Кроме того, ограничение ключиком было бы "параллельно": оно бы действовало на ВСЕ команды ":". Что несколько странно.
Но рытьё в старых исходниках ничего такого не нашло.
Зато обнаружилось, что в uspci_test, cm5307_test и bivme2_test символ '.' работает как ':', но означает "жди ВСЕ прерывания в течение указанного периода, а не только первое".
-c COUNT
; очевидно, по аналогии с v2'шным cx-rdt.
CanKozOnSendProc()
: не
передаются данные об отправляемом пакете.
А хочется (конкретно сейчас -- для canipp_drv.c.
16.11.2018@утро-дома: как показывает анализ, сама фича on_send не используется в CAN-драйверах НИГДЕ.
Так что сделать можно легко и ничего не нарушая.
16.11.2018: делаем.
CanKozOnSendProc
расширен 3 параметрами: desc
, dlc
,
data[]
.
Сделано унифицированно с CanKozPktinProc
, а то так-то
desc
не особо требуется.
cankoz_send_callback()
всё просто. Разве что desc
-- он при dlc<1 передаётся
равным -1
.
Используется с лета этого года (вот ирония!).
Что ж -- там прототип подправлен.
Аргументы добавлены с конца, а по стандарту вызова C ("C calling
convention", cdecl) в стек параметры кладутся справа-налево и удаляются
ВЫЗЫВАЮЩИМ. Т.е., поскольку добавленные параметры в
OnSendTabCtlCmdCB()
помечены
__attribute__((unused))
, то формально можно не заморачиваться
введённым изменением ABI -- оно никак не проявилось бы.
Но всё же лучше перебдеть -- так что на пульту (ctlhomes) эти 4 драйвера обновлены вместе с sktcankoz_lyr.so.
03.04.2022: что любопытно:
q_enqueue_v()
в
AbortMeasurements()
указано on_send=NULL
, а вместо
этого просто СРАЗУ ЖЕ после постановки пакета делается
read_state = READ_STATE_NONE
Или оно туда предназначалось, но было забыто?
SQ_REPLACE_NOTFIRST
: там для обратного
вычитывания используется SQ_IF_ABSENT
, так что для первого в
очереди пакета обратное чтение выполнено будет, а для последующих -- нет.
Обнаружилось при создании ckvch_drv.c, в который брался код из curvv_drv.c.
Чуть более подробное обсуждение в bigfile-0001.html за сегодня.
03.03.2019: поскольку аналогичный код должен
присутствовать и в других драйверах, потенциально требующих использования
SQ_REPLACE_NOTFIRST
, то пытаемся разобраться в ситуации
получше.
Проверяем все в hw4cx/drivers/can/src/, ища по "REPLACE":
Поскольку оная инфраструктура вызывает друг за дружкой
SendWrRq()
,SendRdRq()
, а в них стоят условия
SQ_ALWAYS
и SQ_IF_NONEORFIRST
.
Спасает, видимо, то, что в основном запись производится лишь после получения результата обратного вычитывания, так что 2 подряд записи просто не выполняется.
SQ_ALWAYS
.
SQ_ALWAYS
, а каналы *_OUT_RATE хоть в карте и предусмотрены, но
нереализованы, так что advdac там просто не используется.
SQ_IF_NONEORFIRST
...
SQ_REPLACE_NOTFIRST
, а сам девайс такой умненький, что в
ответ на запись сразу присылает результат.
Исправлять в том виде, в каком оно есть сейчас -- смысла нету. Надо сразу переходить на использование REPLACE.
А проверять на чём-нибудь безопасном. В принципе, можно попробовать на CURVV -- таковой девайс вроде имеется в кикерной (управление впуском/выпуском), причём он там реально не задействован (а то и вовсе не подключен). Вот можно будет поэкспериментировать на нём -- причём сначала потестировать нынешний вариант (выполнять быстро запись нескольких разрозненных битов), а потом переделанный. 04.03.2019: проверил вживую -- да, CURVV там стоит, подключенный к питанию, но с выдернутым CAN-DB9. После втыкания -- на линии виден (и чё я его тогда выдернул -- что-то подсаживал?).
P.S.: очевидно, что надо чуток модифицировать API slowmo: чтобы
SendWrRq()
возвращала бы результат. Но, поскольку оно всё
линкуется статически, то проблем с этой модификацией не будет.
11.03.2019@утро-пультовая-планёрка: а нельзя однако возвращать
из SendWrRq()
прямо SQ-код (как нельзя и в
SendRdRq()
передавать код) -- ведь
advdac_slowmo_kinetic_meat.h используется также и в CAMAC'овском
g0603_drv.c, которому sendqlib нафиг не сдался.
Видимо, надо возвращать не код, а true/false: true (1) -- значит, надо вызывать чтение; false (0) -- нет.
12.03.2019: и, вероятно, в SendRdRq()
придётся передавать булевский же параметр "как" (0:ALWAYS,1:IF_NONEORFIRST).
...хотя это ещё не до конца очевидно: вечер, мозги закипают...
Может, для начала на CURVV попрактиковаться?
14.03.2019: попробовал -- на том 2,31, что в кикерной (я воткнул в него разъём)... Фиг! Почему-то работает всё правильно, последующие записи НЕ пропадают.
Надо ещё поработать со сниффером.
16.03.2019@пультовая, во время разборок с крейтом 1-го клистрона (думали, что сдох контроллер, а реально -- БП): секундочку -- а ведь во время этих экспериментов был запущен скрин curvv.subsys, в котором есть каналы входного регистра, и они всё время опрашиваются; а пакет для чтения входного и выходного регистров один и тот же -- вот оно и обновляет реальное состояние постоянно! Надо тест проводить БЕЗ скрина -- например, cdaclient'ом.
16.03.2019@пультовая: сделано -- тест при помощи das-experiment'а с такой командной строкой:
~/4pult/bin/das-experiment -b canhw:59.a +@i:pwr_b{0,1,2,3,4,5,6,7}
Да -- наблюдено ожиданное поведение: обновляется только первый канал, а
остальные будто нет.
Затем драйвер был модифицирован: подтверждающее чтение переведено на
SQ_ALWAYS
. И теперь всё заработало корректно.
18.03.2019@пультовая:
доделываем: переводим curvv_drv.c на
SQ_REPLACE_NOTFIRST
(с вызовом при результирующем
SQ_NOTFOUND
чтениея через SQ_ALWAYS
).
Да -- теперь всё вообще прекрасно: отправляются лишь первый запрос и последний, в который аккумулируются все промежуточные.
18.03.2019: а тот CURVV, на котором всё испытывалось -- всё-таки больной (конкретный экземпляр), он портит линию. Потому CAN-кабель из него выдернут.
18.04.2019: ещё пообщался с Сенченко на тему "как у них решается проблема отправки подряд большой пачки записей в каналы, отображающиеся на один и тот же выходной регистр (чтобы все записи прошли корректно, выставились бы правильные значения битов, и всё бы это побыстрее)".
Предыстория:
Итак, на сегодняшней четверговой автоматизационной встрече Сенченко рассказал, что он вроде бы успешно решил проблему на ВЭПП-2000. После сборища я выспросил, как именно. Вот результат опроса.
Вместо этого введено понятие "цепочка команд", которые вначале ВСЕ обрабатываются, а уж потом конечный результат отправляется в железо.
...вот тут я не помню точно, какой из 2 вариантов реализован, а какой мы с Сашей только обсуждали:
И да, тем самым несколько убивается общность, в целях достижения нужного результата здесь и сейчас.
...но не стал это говорить вслух.
Коснулось как имён файлов, так и имён символов.
Заодно также проведена некоторая унификация -- на тему где "koz" нужно, а где нет.
10.01.2020: работа грандиозная, некоторые заметки:
Как и все реализации *canhal.h.
Как бы то ни было -- сделано, осталось протестировать живые программы (убедиться, что нигде нет висячих ссылок).
SendFrame(0:k3/6, dlc=1, cmd=0x13): ZERO
И оно так до тех пор, пока не рестартуешь CX-сервер vepp4-pult6:2.
Анализ исходников показал, что cankoz_lyr_common.c с тех пор
касательно обработки ошибок не изменился, а в sendqlib.c и вовсе
одно косметическое добавление касательно SQ_TRY_LAST
(которая
реально никем не используется).
Следовательно, проблема может быть до сих пор актуальна, и надо её искать.
Но на это надежды мало.
И даже если дело в этом, то придётся добавлять в консольный интерфейс команду вроде "reset", которую хрен знает как исполнять.
Так что пусть бы лучше это была ошибка НЕ у Мамкина -- свою-то я хоть исправить смогу.
Придётся гонять на пару с новым cxsd.
P.S. Проблема-то была увидена ещё неделю-две назад, но только сегодня обсмотрена детальнее.
25.10.2021: фокус в том, что чтение/запись там
делаются не как в прочих подобных утилитках (прямо в основном тексте), а в
отдельных функциях Canutil*()
. Смысл этого -- исторический:
И вот для них "стандартный интерфейс" был поселён в canutil_common.[ch].
Теперь же, собственно "нюансы":
option_nnn
определяются
НИЖЕ по тексту.
CanutilReadFrame()
и
CanutilSendFrame()
не делали бы exit(2)
по любой
ошибке, то им добавлен параметр option_keep_going_arg
, в
котором их юзеры передают значение option_keep_going
.
CanutilReadFrame()
теперь не
void
, а int
-- ведь он должен возвращать результат
"ошибка", чтобы его юзер не пытался печатать содержимое пакета.
Возможно, если её выдавать (всё равно ж на stderr?), то просто
strcurtime_msc()
? (А не пытаться учитывать значение
option_times
.)
26.10.2021: да, так и сделано -- текущее время выдаётся бещусловно.
Итого -- не столь мгновенно, как ожидалось, но сделано.
26.10.2021: и протестировано на ep1-berezin2 (для чего и делалось: захотелось понять, почему сообщения "No buffer space available" валятся бесконечно, а не подавляются после первого: часть сообщений всё-таки типа уходит?).
Оказалось, что да -- действительно, Сеньковский VIP конкретно там настолько странно обращается с линией, что она то "забита" (не отправляется даже 1 пакет), то "свободна" (уходят все 3). Вот и причина того, почему в /var/tmp/4drivers.log там постоянно сыплются сообщения.
Как бы то ни было, "done".
cankoz_add()
добавлено,
что при обломе can_hal_open_and_setup_line()
в диагностике об
ошибке выдаётся также и номер линии -- а то шибко уж бестолково было.
_term_d()
всех драйверов, отсылающих оттуда пакет
"прекрати измерения" -- cac208_drv.c, canadc40_drv.c,
caniva_drv.c, cdac20_drv.c, ceac124_drv.c,
vsdc2_drv.c (в последнем оно за-#if0'ено) -- добавлено условие, что
отправлять только при handle>=0
.
А то при обломе регистрации сервер всё равно вызывает
term_dev()
, оно пытается отправить пакет на handle=-1, что
вызывает ругань
cankoz_q_enqueue: invalid handle -8192 (=>-1:0)
-- раздражает!
Вопрос возник из-за странного поведения сегодня на ulan-ude-els:51 -- после утреннего включения почему-то часть каналов уставок ЦАП на CEAC124 остались болотными и значилось "age >99h", при том, что каналы АЦП обновлялись.
И в логах обнаружились строки
(эти CEAC124, mag1_a и mag1_b -- как раз 14 и 15).2024-04-10 09:17:40.327 ulan-ude-els cxsd#51: ceac124[3]vac/DEFAULT: GETDEVATTRS: ID=0,13, DevCode=20:CEAC124, HWver=1, SWver=5, Reason=0:PowerOn 2024-04-10 09:17:40.327 ulan-ude-els cxsd#51: ceac124[3]vac/DEFAULT: SendMULTICHAN:MULTICHAN beg=0 end=1 time=4 gain=0 2024-04-10 09:17:41.644 ulan-ude-els cxsd#51: weld02[2]gun/DEFAULT: GETDEVATTRS: ID=0,12, DevCode=15:WELD01:Repkov, HWver=4, SWver=11, Reason=0:PowerOn 2024-04-10 09:17:42.186 ulan-ude-els cxsd#51: lyr:sktcankoz[-1]/DEFAULT: SendFrame(0:k12/6, dlc=2, cmd=0x03): No buffer space available 2024-04-10 09:17:42.238 ulan-ude-els cxsd#51: ceac124[5]mag1_b/DEFAULT: GETDEVATTRS: ID=0,15, DevCode=20:CEAC124, HWver=1, SWver=5, Reason=0:PowerOn 2024-04-10 09:17:42.239 ulan-ude-els cxsd#51: lyr:sktcankoz[-1]/DEFAULT: SendFrame(0:k15/6, dlc=1, cmd=0xf8): No buffer space available 2024-04-10 09:17:42.239 ulan-ude-els cxsd#51: ceac124[5]mag1_b/DEFAULT: SendMULTICHAN:MULTICHAN beg=0 end=15 time=4 gain=0 2024-04-10 09:17:42.240 ulan-ude-els cxsd#51: ceac124[4]mag1_a/DEFAULT: GETDEVATTRS: ID=0,14, DevCode=20:CEAC124, HWver=1, SWver=5, Reason=0:PowerOn 2024-04-10 09:17:42.240 ulan-ude-els cxsd#51: lyr:sktcankoz[-1]/DEFAULT: SendFrame(0:k14/6, dlc=1, cmd=0x92): No buffer space available 2024-04-10 09:17:42.240 ulan-ude-els cxsd#51: ceac124[4]mag1_a/DEFAULT: SendMULTICHAN:MULTICHAN beg=0 end=15 time=4 gain=0 2024-04-10 09:17:42.246 ulan-ude-els cxsd#51: ceac124[4]mag1_a/DEFAULT: GETDEVATTRS: ID=0,14, DevCode=20:CEAC124, HWver=1, SWver=5, Reason=2:GetAttrs 2024-04-10 09:17:42.246 ulan-ude-els cxsd#51: ceac124[4]mag1_a/DEFAULT: SendMULTICHAN:MULTICHAN beg=0 end=15 time=4 gain=0 2024-04-10 09:17:42.247 ulan-ude-els cxsd#51: ceac124[5]mag1_b/DEFAULT: GETDEVATTRS: ID=0,15, DevCode=20:CEAC124, HWver=1, SWver=5, Reason=2:GetAttrs 2024-04-10 09:17:42.247 ulan-ude-els cxsd#51: ceac124[5]mag1_b/DEFAULT: SendMULTICHAN:MULTICHAN beg=0 end=15 time=4 gain=0 2024-04-10 09:17:42.251 ulan-ude-els cxsd#51: ceac124[4]mag1_a/DEFAULT: GETDEVATTRS: ID=0,14, DevCode=20:CEAC124, HWver=1, SWver=5, Reason=2:GetAttrs 2024-04-10 09:17:42.251 ulan-ude-els cxsd#51: ceac124[4]mag1_a/DEFAULT: SendMULTICHAN:MULTICHAN beg=0 end=15 time=4 gain=0 2024-04-10 09:17:42.253 ulan-ude-els cxsd#51: ceac124[5]mag1_b/DEFAULT: GETDEVATTRS: ID=0,15, DevCode=20:CEAC124, HWver=1, SWver=5, Reason=2:GetAttrs 2024-04-10 09:17:42.253 ulan-ude-els cxsd#51: ceac124[5]mag1_b/DEFAULT: SendMULTICHAN:MULTICHAN beg=0 end=15 time=4 gain=0
После рестарта драйверов посредством ._devstate=0
всё очухалось.
Но остаётся вопрос -- что это было? Не из-за неотправки ли каких-то пакетов запросов (вследствие чего каналы так и остались в запрошенном и не отвеченном состоянии)?
Что странно -- раньше ничего подобного не встречалось, в т.ч. и на более загруженных CAN-линиях. Скорее я бы подумал, что УСТРОЙСТВА умудрились не получить пакеты.
10.04.2024: однако, обнаружился странный ляп: на
ulan-ude-els в файлах ifcfg-can* отсутствовала строчка
TXQUEUELEN=100
-- обнаружилось это, когда команда
"ip -d link show can0
"
выдала "qlen 10" (кстати, тут и "-d
" необязательно); ровно как
и на x10sae (откуда, похоже, и были скопированы файлы на ulan-ude-els).
Но вот в ifcfg-can1 на x10sae эта строчка была (как и в ifcfg-can1 на ulan-ude-els) -- похоже, при введении этой фичи 28-02-2017 (именно эта дата на файлах) экспериментировалось как раз на can1.
Поэтому всё же остаётся вопрос: а какого чёрта произошло то, что произошло -- ведь даже при TXQUEUELEN=10 всё должно было отработать корректно, просто пере-отправить запрос позже; почему глюкануло?
Исследуем вопрос.
SQ_TRIES_DIR
-- для РАБОТЫ не
делается нигде.
(Шлются только DESC_STOP
и подобное в term_d()
,
где очередь не к месту; плюс конкретно в
advdac_cankoz_table_meat.h::SendTabCtlCmd()
так
отправляется команды управления исполнением таблиц, но с fallback'ом к
SQ_TRIES_ONS
при ошибке отправки.)
perform_sendnext_in_queue
в случае ошибки
пакет из очереди НЕ удаляется, а лишь заряжается таймаут пере-отправки:
/* Try to send the item */ r = q->sender(epp, q->privptr); /* In case of error -- just set timeout for retransmission and break */ if (r != 0) { enq_tout(q, q->timeout_us); return; }
cankoz_sender()
делается корректная трансляция между кодами CANHAL_*
и 0/!=0
для sendqlib'а --
return r == CAN_HAL_OK? 0 : -1;
Так что внятного объяснения произошедшему на сварке для Улан-Удэ нет. Годным выглядит только "УСТРОЙСТВА умудрились не получить пакеты", но толку от него... И как увеличение TXQUEUELEN до 100 предотвращает конкретно ТАКИЕ глюки -- тоже непонятно.
cankoz_fd_p()
касательно обработки пакетов GETDEVATTRS от
устройств: теперь они удаляются из очереди только если от устройства пришёл
пакет с id_reason==CANKOZ_IAMR_GETDEVATTRS
-- раньше же
удалялись всегда, в т.ч. если это CANKOZ_IAMR_WHOAREHERE
,
ответ на broadcast.
Сделано в интересах gin25_drv.c, т.к. неуспевшее ответить на /b:0xFF устройство пакет /u:0xFF просто игнорирует и ответ не присылает; так же unicast-запрос останется в очереди и будет повторён через 100ms. Подробнее см. в разделе "gin25_drv".
Такой вариант выглядит даже более правильно.
Но пока не проверено; вроде бы побочных эффектов быть не должно, но фиг знает, что может вылезти.
17.03.2025: проверено -- да, работает, побочных эффектов не видно.
"Прямой" эффект несколько неидеален: поскольку значение
CANKOZ_0xFF_TIMEOUT
=10s, то при неответе источником сразу
следующая посылка и ответ будут лишь через 10 секунд.
В идеале, конечно, ПЕРВЫЙ бы раз перепосылать пакет через 1s, а уж потом через 10. Но так заморачиваться ради обхода столь экзотичного глюка -- вряд ли стоит: результат есть, пусть и неидеальный, и ладушки. Оставим как есть.
Считаем за "done".
04.04.2018: давно назрело level4-списочек для отдельных драйверов -- делаем.
Так что -- будем делать драйвер.
Драйвер, конечно, назовём canipp_drv.c, но его карта каналов будет совпадать с u0632'шной.
И, кстати, почитал описание -- оно прекрасно совместимо с У0632: мало того, что сами "коробочки" те же самые, но и модель работы самого контроллера похожа (только "уведомления" добавлены) и, видимо, удастся сделать драйвер по той же схеме.
04.04.2018: делаем.
Специфичный функционал убран, минимальная адаптация (вроде DEVTYPE=9) проведена.
И, теоретически, можно б было вообще не делать canipp{_drv_i.h,.devtype.
Но если драйвер прекрасно бы обошёлся использованием
u0632_drv_i.h и U0632_CHAN_*
, то как указывать в
конфиге -- вопрос. Т.к.:
dev NNN u0632/canpp@sktcankoz ~ 63
(тут-то и пригоделась бы возможность указывать /DRIVER@LAYER вместе, ранее
считавшаяся слабоосмысленной -- см. за 17-06-2015).
dev NNN u0632/remdrv ~ 63 b:HOST
-- где тут указать "canipp"? И remdrv честно передаст canserver'у имя
-- "u0632", которого тот среди драйверов не найдёт.
А если, наоборот, указывать
dev NNN canipp/remdrv ~ 63 b:HOST
то уже серверу будет негде взять ссылку на тип "u0632".
Короче -- пришлось делать копии файлов под другими именами.
05.04.2018: потихоньку пилим дальше.
DESC_*
. Что забавно, они оказались
даже почти идентичны CPKS'ным -- запись с базой 0x00, и чтение с 0x20 (у
CPKS8 чтение идёт с 0x10, и разница потому, что тут возможно 32 субадреса).
Это автоматом организует пере-сканирование в случае смены железки (которая при включении пришлёт 0xFF).
_ff()
: там и бродкасты
отсеиваются, и вызвана она будет гарантированно при старте.
Посему -- вызов RequestScan()
вставлен в
canipp_ff()
.
unit_*[]
, и bias_*
.
SetM()
/SetP()
с
Return*Bias*()
.
SwitchTo*()
-- полностью.
StartMeasurements()
, AbortMeasurements()
(этот
принципиально пустой), ReadMeasurements()
,
PerformReturn()
(тут тоже принципиально пустота).
Включая мутные махинации с "таймаутами" -- они реально отключены через
TIMEOUT_SECONDS = 10*0
.
init_d()
-- всё, за вычетом сканирования и
навешивания poll_tout_p()
, которая в CANIPP-M вроде уже
не требуется (хотя если попадут в лапы старые не-M, то есть всё готовенькое
для копирования).
10.04.2018: еще в цикле отдачи RDs убрана проверка на
unit_online[unit]
-- они ж тут ВСЕ еще НЕ, т.к. сканирования
еще не было.
_rw_p()
-- весь целиком.
Добавлено только игнорирование HW_VER/SW_VER (возможно, ТУТ, без "default:", и ненужное).
И вылезла особенность: структура "основного селектора" в CAN- и CAMAC-драйверах разная: if()/elif()/... и select() соответственно.
А главное -- чуть разные названия переменных:
value
и val
соответственно.
Почему так получилось? И не надо ль унифицировать?
06.04.2018@лыжи: этот драйвер у нас --
Собственно:
unit_data[]
каким-нибудь
значением, не могущим взяться из аппаратуры (-1?), а при получении
подсчитывать количество ячеек с этим значением, и если 0 -- значит, всё
пришло.
06.04.2018: список вопросов касательно ИПП/CANIPP, которые надо задать Беркаеву:
Да.
poll_tout_p()
из u0632_drv.c.)
Нет, старых не-M не осталось.
IPPvalue2int()
?
Да, это определяется "коробочкой", поэтому формат данных точно такой же.
Да, тут всё сложно. После программирования на ожидание запусков нельзя делать вообще НИЧЕГО -- даже статус читать нельзя, а то разрешение прохождения запусков слетит.
"Не помню, но вреда от паузы не будет".
Паузу будем делать постановкой в очередь пакета -- например, нулящего разрешения запусков -- с timeout_us=3000, так что после него прямо сам sendqlib сделает паузу.
Всё сложно. Федя утверждал, что фоны НЕ зависят от режима e+/e-/VEPP4/VEPP2K. Но Беркаев говорит, что всё же зависит. Раз так -- надо в каждом режиме иметь СВОЙ фон. Моя вариант был "сделать каналы bias писабельными, и чтоб менеджер режимов сам решал, когда измерить фон, а потом при переключении режимов прописывал бы нужный фон". Но Беркаев предлагает повесить это всё на middleware: чтоб софтина хранила фоны у себя, получала бы данные, вычитала б фон в соответствии с режимом и потом публиковала бы результат в софтовом сервере; а все пультовые/прикладные программы чтоб брали данные именно из этого сервера, а не от сервера железа.
06.04.2018: кстати, в privrec_t
добавлено
unit_data[][]
-- тут же не "атомарное чтение и потом отдача из
локального массива", а читается по ячеечке каждым CAN-пакетом, так что нужно
накапливать в privrec'е.
09.04.2018: продолжаем -- вооружённые знанием ответов на пятничные вопросы.
Поскольку у нас именно CANIPP-M, то никакого поллинга проводить не требуется, так что во время ожидания обращения к устройству РЕАЛЬНО отсутствуют (а с не-M пришлось бы мучиться...).
RequestScan()
-- наполнено содержимым.
RequestMeasurements()
прямо в
_init_d()
никак нельзя -- надо дожидаться окончания
сканирования (как в У0632'шном драйвере!).
Получасом позже: а как понять, что это и есть "последний"? Точно ли можно полагаться на порядок прихода пакетов ответа? Обсуждается в следующем пункте списка уровнем выше.
Нет, нифига -- НЕ игнорировать, а ЗАПОМИНАТЬ!
_rw_p()
каналы данных вообще игнорируются.
Т.е., если запрос когда-то пошёл -- то это навсегда.
PerformTimeoutActions()
проверку, что "если
measuring_now==0
, то ничего не делать"Несколькими минутами позже: а вот и нет! Везде, где
PerformTimeoutActions()
вызывается (в т.ч. в реакции на
param_stop
, имеющейся в pzframe_drv.c и отсутствующей
у ИПП'шной имитации) стоят проверки на тему "measuring_now!=0".
(Если только из линии прилетит сфабрикованный (например, canmon'ом) пакет "ответа на адрес N+x", которого никто не запрашивал.)
Как? Да несложно:
unit_online[x]
не просто
булевскими (==0 -- отсутствует, !=0 -- присутствует), а ЗНАКОВЫМИ: перед
началом сканирования во все из них писать -1
, и в качестве
окончания сканирования считать момент, когда все они стали >=0.
unit_online[unit]
на
unit_online[unit] > 0
Формально -- вся проблема будет в повторном вызове
RequestMeasurements()
. Ну, типа нестрашно.
Но нехорошо. И надо вставлять проверку, чтоб НЕ пытаться запускать измерения повторно.
RequestMeasurements()
вставлено условие, что если уже
measuring_now
!=0, то ничего не делать.
Кстати, в pzframe_drv_req_mes()
-- аналоге в "настоящей"
реализации pzframe -- оная проверка есть.
_in()
,
по получении ответа на CADDR последнего unit'а.
unitcode2unit()
.
is_a_reset
.
Тут уж хочешь не хочешь, а необходимо забыть о когда-то якобы запущенном измерении, послать контроллеру принудительный запрет всего (на всякий случай) и запустить сканирование.
canipp_in()
.
...тут пока только селектор, еще ничего не делающий.
10.04.2018: уже делается.
Тут 2 части, в зависимости от адреса:
Здесь всё несложно и обсуждено выше парой пунктов основного списка.
Вот тут намного геморройнее, т.к. это "замыкание" чтения данных, а следовательно:
Также приходится вести "статистику" -- запоминать факт оного полного прихода, чтобы далее определить пришедшесть всех данных всех коробочек.
...плюс по этому флагу можно игнорировать дальнейшие пакеты от коробочки, хотя это не критично.
drdy_p()
, и уж та пусть далее вызывает
ReadMeasurements()
, которая и возится с фоном и отдачей данных.
Сценарий маловероятный, но теоретически возможный.
Такое может быть в модели общения master<->скрытые/подчинённые, используемой Пановым: у него подтверждение приёма запроса отдаёт непосредственно видимое устройство-master (и по подтверждениям из очереди запросы убираются), а вот ответит ли под-устройство -- хбз.
Так вот: если буквально один кусочек данных не придёт, то всё будет висеть навечно (у нас ведь и STOP'а никакого не предусмотрено!).
Очевидно, что:
А то мало ли -- по ходу жизни проводок от коробочки отвалился.
Напрашивается следующее:
Достаточно каналам M и P возвращать флаги на основании unit_online[].
unit_rflags[]
получать в процессе вычитывания данных:
Получасом позже: неа, нулить прямо в
StartMeasurements()
.
unit_rflags[unit]|=CXRF_CAMAC_NO_Q
.
Сделано, ровно по вышеприведённому сценарию.
10.04.2018: далее.
is_reading
. Взводится при начале
вычитывания, сбрасывается по окончанию.
Вечером: переделано -- в многовариантное
read_state
, со значениями READ_STATE_{NONE,PROG,WAIT,READ}.
Непосредственная причина -- чтоб при программировании перед запуском после
команд записи также шли команды чтения, а _in()
на них бы не
реагировала. (Да, можно было бы в качетстве "ответной" и
какую-нибудь другую команду использовать (ту же 0xFE), но сделано уж "по
всем правилам", красиво (и "с резервом на будущее").) P.S.
READ_STATE_PROG реально не используется (можно было бы -- ставить _PROG, а в
_WAIT переходить в OnSend-callback'е по взведению разрешения на запуск; но
незачем).
Сделано прямо тут.
measuring_now
, и что не-is_reading
, и на наличие
битов статуса "был старт".
Итого -- по максимуму 30*32=960 команд; к запрашиваемому в
cankoz-add()
'е размеру очереди это число прибавлено.
Поэтому, чтобы ответный пакет нам никак не помешал, используется адрес 15
-- раз он бродкастный, то чтение по нему лишено смысла и в
_in()
он специально отфильтровывается.
StartMeasurements()
-- программирование.
_in()
'е
игнорируется).
13.04.2018: заканчиваем?
read_tid
.
DropReadTout()
-- по
опыту ottcam_drv.c, оно требуется как минимум из 2 точек.
AbortMeasurements()
и
ReadMeasurements()
.
Плюс еще точка перед заказом таймаута -- чисто на всякий случай, воизбежание их размножения.
read_timeout()
-- просто вызов
PerformTimeoutActions()
, которая теперь БУДЕТ использоваться (в
противоположность тому, что думалось 09-04-2018).
CXRF_IO_TIMEOUT
.
Поэтому собственно возврат переехал из ReadMeasurements()
в
PerformReturn()
(имеющий параметр rflags
).
(В u0632_drv.c оно так не было сделано лишь потому, что там серия "читаем, возвращаем" по-коробочно, а тут -- сначала всё получено полностью, и лишь потом отдаём наверх.)
AbortMeasurements()
-- которая вызывается, посредством
PerformTimeoutActions()
, и по read_timeout'у, и по
_ff()/is_a_reset.
AbortMeasurements()
засунута отправка пакета,
сбрасывающего разрешение стартов. Делается аж 2 раза:
А очистку очереди перед этим не надо ли?
Ответ: нет, лучше не надо.
Кстати, места под сам канал навалом --
U0632_CHANR_WR_count
=50, а задействовано всего 3 (под BIAS_*).
16.04.2018: сделана первая проверка. Результат обескураживающий:
То ли косяк, то ли чего-то недопонимаю...
Надо уже с Беркаевым разбираться предметнее (всё равно надо обговаривать детали).
11.11.2018@пультовая, воскресенье, после обеда: разобрались с Беркаевым предметнее.
Поэтому в cdaclient'е он смотрит каналы с префиксом "@.", а в софтине регистрирует с NO_RD_CONV.
13.11.2018: тем более, что в режиме CHEREP калибровок быть и не должно, и они как раз теперь устранены. Теперь бы и NO_RD_CONV уже стал бы ненужен.
А после перехода на чтение всего 9 ячеек -- стало успевать.
11.11.2018@пультовая,
воскресенье, уже вечером в районе 19 часов: был косяк с прописыванием
настроек: зачем-то 32 раза (CANIPP_CELLS_PER_UNIT) пыталось писать в ячейку
с адресом cell
(т.е., де-факто -- 32-ю, ПОСЛЕ всех ячеек
данных), да еще и контрольное вычитывание затребовывало.
Обнаружилось это под вечер, когда уже была реализована работа с каналами CREG, и оказалось, что они почему-то не оказывают никакого влияния на сигнал, да и вовсе не прописываются (после рестарта сервера вычитывались старые значения).
Да еще и пропускную способность CAN-шины впустую сжирало.
StartMeasurements()
, копировалась из
u0632_drv.c, где ничего подобного нету.
Факир был пьян?
13.11.2018: а, во -- стало ясно, почему там после записи еще и чтение выполнялось: в качестве "прореживания": иначе, при отправке сразу толпы _ons-пакетов, велик шанс потери, причём неопределяемой.
Ведь "разрешение запусков", в виде обращения к "регистру ACCESS", делается тоже _ons-командой, которая точно так же может потеряться.
С другой стороны, sendqlib при ошибке отправки -- когда
sender()
вернёт !=0 -- просто уставляет таймаут для
ретрансмита, но НЕ выкидывает пакет из очереди, даже ONS'ный.
Да, это даёт race condition, но он считает такое нестрашным (ну пропустит один запуск...).
14.11.2018: была найдена причина, почему коробочки время от времени "отваливаются" и перестают присылать данные: нет, виноват НЕ софт -- виноват сам CANIPP-M!
(Записано 16.11.2018, поэтому даты от 16-го.)
@2018-11-16-15:06:10.456064 k56/u.0:0x2F,0x7F @2018-11-16-15:06:10.457462 k56/r.0:0x2F,0x7F,0x80,0x00,0x10,0x00,0x00,0x00 @2018-11-16-15:06:15.288172 k56/r.1:0xFE,0x40,0x80,0x00,0x10,0x00,0x00,0x00
Т.е., команда "разрешения стартов" (на которую приходит ответ), затем пауза, после которой прилетает 0xFE, сигнализирующий о произошедшем запуске.
@2018-11-16-15:06:15.928516 k56/u.0:0x2F,0x7F @2018-11-16-15:06:15.929943 k56/r.0:0x2F,0x7F,0x80,0x00,0x10,0x00,0x00,0x00 ...тишина...
Т.е., бридж команду разрешения стартов получил и даже подтвердил получение, но потом просто "забыл", и никакого 0xFE уже не прислал.
17.11.2018@суббота-после-обеда-пультовая: идея решения: в состоянии WAIT периодически посылать пакеты с разрешением запусков.
Разве что
ADDR_ST1
в адресах не будет).
Иных проблем не просматривается.
canipp_start_hbt()
,
read_state==READ_STATE_WAIT
отправляющая точно
такой же пакет разрешения.
_init_d()
для первого запуска; но при
этом она лишь ставит себя на срабатывание, но НЕ отправляет пакет (т.к.
state!=WAIT).
Проблем вроде не наблюдается.
Ура!
02.04.2022: не факт, что точно "ура": сегодня возникла какая-то странность. Подробнее см. ниже за сегодня.
11.12.2022: да нет -- факт, факт: "странность" обуславливалась тем, что по 0xFF/is_a_reset сканирование НЕ повторялось.
17.11.2018@вечер-дома: кстати, есть халтура с отдачей CUR_-параметров (CUR_{M,P,CREG}): ведь отдаются не те, с которыми было запущено измерение, а РЕАЛЬНО-ТЕКУЩИЕ -- т.е., если с момента старта измерения успела придти команда записи в M/P/CREG, то отдастся уже изменённое значение, НЕ соответствующее тому, с которым выполнялось измерение.
Надо б как-то буферизовать в StartMeasurements()
--
переписывать в специальное место, откуда потом и отдавать CUR_'ы.
PerformReturn()
-- чтоб было собирабельно и
под gcc-2.96@RedHat-7.3.
14.05.2018: проблема заключалась в том, что вызов
ReturnDataSet()
с "inline"-параметрами ("compound literals")
работает в gcc-3+, а в gcc-2.96 даёт ошибку "invalid use of non-lvalue
array".
История разборок такова (в логическом порядке, но не в хронологическом):
Тогда особо не стал разбираться.
Фиг -- та же 2.96.
PerformReturn()
#if __GNUC__ < 3
"
вместо прямого возврата (литералами) объявляются массивы для всех
параметров, заполняются, и уже они сбагриваются
ReturnDataSet()
'у -- что прокатило бы даже в C89.
Результат поиска (grep -A5):
Но оно собирается для CM5307, gcc-3.2.2.
А они компилируются без проблем.
Что, глюк проявляется в 2.96 только при бОльшем количестве элементов?
Засим считаем расследование завершённым и рецепт найденным (хотя сколько еще будет актуальна сборка под 2.96@RH73 -- хбз :)).
Причём сейчас у нас на кольце/канале -- ТОЛЬКО черепановские коробочки; а в будущем предполагается смесь.
Надо б это будет уметь учитывать: чтоб мочь в auxinfo указывать по-адресно типы.
25.07.2018@утро-дома: вариант решения: понятно, что надо иметь гигантскую PSP-таблицу для возможности указания индивидуально типа каждой коробочки.
Но в случае, когда они все какого-то одного типа, перечислять все -- неудобно.
Завести отдельный ключ типа "defkind", которым указывать умолчание -- repkov или cherepanov; и чтоб по-штучные типы имели значения не 0, а 1(repkov) и 2(cherepanov), 0 же оставить за "не указано"; и для тех, у кого в типе значится 0, прописывать значение из умолчания.
26.07.2018: да, именно так и сделано:
def_kind
и 30 штук индивидуальных параметров
0
...31
, ставящихся именно в той логике, что
описана вчера.
unit_kind[]
.
03.11.2018: поговорил с Беркаевым, выяснил специфику: у черепановских параметр P -- отсутствует, а M -- занимает 3 бита вместо 2.
Надо заметить, что это влияет также на BIAS etc. -- ведь съём фона делается с перебором всех комбинаций {P,T}, а тут их набор иной... С другой стороны, как говорит Беркаев -- для ЭТИХ коробочек набор фона скорее бессмыслен, т.к. там что-то сильно хитрее и его лучше реализовывать в высокоуровневом софте.
11.11.2018@пультовая, воскресенье, после обеда: сделано, вот как:
Вывод: отдавать наверх надо именно регистр, и принимать от клиентского софта регистр же целиком (16 бит), а с M его путать никак не нужно.
Результат:
11.11.2018@пультовая, воскресенье, после обеда: а вот потому, что сейчас карты разошлись: у CANIPP появились канал read_state плюс каналы CREGn и cur_CREG; а еще понадобится завести каналы NUMPTSn (вместо нынешнего const32).
Другое дело, что и для u0632 каналы CREG и NUMPTS могут оказаться нужны -- если вдруг там тоже понадобится внедрить аналогичные фичи (поддержку черепановских коробочек и настраиваемую длину вычитывания).
18.12.2018: а вообще этот вопрос был разобран ещё 04-04-2018, в самом начале раздела.
Первейшая причина -- невозможность указать для удалённых драйверов (которые через remdrv) имя драйвера, отличающееся от имени типа.
Видимо, нужно завести per-box-параметры (аналогично kind) с указанием номера количества, со значением по умолчанию =32. И наверх отдавать тоже ровно то же количество, а не 32.
Надо заметить, что, судя по анализу кода, наличие этого N влияет также на
11.11.2018@пультовая, воскресенье, после обеда: сделано.
unit_nwires[]
.
Умолчательное количество -- CELLS_PER_UNIT=32.
canipp_in()
с
вычислением wire
из addr
в виде
wire = (addr + CANIPP_CELLS_PER_UNIT+1) % CANIPP_CELLS_PER_UNIT;
(кстати, весьма сомнительные: зачем делать +CANIPP_CELLS_PER_UNIT и потом
%CANIPP_CELLS_PER_UNIT -- оно ж просто сократится?) заменены на
тождественное отображение
wire = addr;
Проверено: в devlist-canhw-23.lst всем 4 canipp поставлено def_nwires=9 -- работает (в т.ч. протестировано sktcanmon'ом; вначале из-за косяка в _init_d() параметр игнорировался (позор!), затем исправлено).
Засим можно считать за "done".
...хотя может еще потребоваться:
Для чего тогда придётся заводить параметры def_firstwire и firstwireN, плюс каналы PTSOFSn.
Это для ситуации, когда коробочки глючат или подключаются "потом". Чтоб не приходилось рестартовать сервер или драйвер.
Очевидно, нужно будет переводить машину состояния в начальное состояние; просто выполнять то же, что по 0xFF/is_a_reset.
11.11.2018@пультовая, воскресенье, после обеда: еще потребность -- иногда CANIPP-бриджи почему-то перестают реагировать на запуски (очень быстро в случае вычитывания 32 слов), и надо бы разобраться в причине. Тут возможность программного "рестарта" будет полезна.
11.11.2018@пультовая,
воскресенье, после обеда: а пока что для потенциальных разборок
«что происходит, почему "подвис" и не "ловит запуски" и не отдаёт
данные» добавлен канал _READ_STATE
, через который
отдаётся значение поля read_state
. Он помечен как
IS_AUTOUPDATED_TRUSTED и возвращается при присвоении новых значений полю
(единственное исключение -- НЕ отдаётся READ_STATE_PROG
, т.к.
оно полу-фейковое и всегда сразу же сменяется на READ_STATE_WAIT).
02.04.2022: (записываю 03-04-2022) опять вылезло подвисание; причём, из-за неожиданности, я на значение read_state не посмотрел (вообще не помнил о нём, а просто старался решить проблему побыстрее).
Хбз теперь, в чём же была причина: то ли "коробочки время от времени "отваливаются" и перестают присылать данные" от 14-11-2018 (хотя вроде должно было быть решено), то ли в том, что "иногда CANIPP-бриджи почему-то перестают реагировать на запуски" от 11-11-2018.
Но в следующий раз глянуть бы на значения ихних read_state...
10.12.2022: ой, да это же ровно та проблема, которая вылезла 30-11-2022 -- некорректная отработка 0xFF/is_a_reset!
11.12.2022: поскольку РЕАЛЬНАЯ проблема -- НЕсканирование по 0xFF/is_a_reset, являвшаяся причиной случая 02-04-2022 -- решена, то делаем "withdrawn".
А "повторить сканирование" можно и посредством
"._devstate=0
" (ну рестартует она драйвер -- да и ладно, там
никаких критичных данных в privrec'е не хранится, а M/P оно считает из
устройства; тем более, что при "повторном сканировании" сделалось бы то же
самое).
Идея подсказана Беркаевым -- он в своём софте примерно так поступал.
Весь вопрос только в том, как втиснуть это дополнительное чтение в общую диаграмму работы.
12.11.2018: анализируем код и размышляем.
unit_online[unit]
==0.
Таким образом будут "подхватываться обратно" временно отрубавшиеся коробочки.
Хотя и вопрос, как же будут переводиться в online:=0 реально убранные. Только по полному сканированию?
drdy_p()
и в
PerformTimeoutActions()
вместо нынешнего
RequestMeasurements()
вызывать RequestScan()
.
Только:
RequestScan()
делать
read_state:=READ_STATE_NONE.
11.12.2022: поскольку проблема сканирования по 0xFF
решена, то вопрос "повторного сканирования" становится не шибко актуальным.
В крайнем случае можно устройству сделать ._devstate=0
для
рестарта, и оно пере-сканирует.
Так что ставим "withdrawn".
_devstate=0
.
PerformTimeoutActions()
, из
которого вызывается RequestMeasurements()
; но почему-то этого
не хватает.
StartMeasurements()
, которая из
RequestMeasurements()
и вызывается.
01.12.2022: ещё порылся по коду, и возникла идея (точнее, НАМЁК на неё :D) о том, что там может идти не так.
Ключевое соображение -- интерференция сканирования с обычными измерениями.
Факты:
CANIPP_CADDR
.
READ_STATE_NONE
.
Обдумывание следствий:
!is_a_reset
(т.е., при старте драйвера и ответе
GETDEVATTRS) будет просто запущено сканирование;
is_a_reset
(т.е., в АВАРИЙНЫХ случаях) -- сначала
будут принудительно запрошены измерения и сделан переход в
READ_STATE_WAIT
, а уж потом запрос сканирования.
...результаты какового сканирования будут ПРОИГНОРИРОВАНЫ.
Соображения по исправлению проблемы:
RequestMeasurements()
--
вызывается по окончанию сканирования, по получению значения регистра
CANIPP_CADDR
от последней коробочки.
canipp_ff()
,
посредством PerformTimeoutActions()
.
canipp_ff()
.
do_request
", который почти всегда указывать 1, но конкретно
тут -- 0.
PerformTimeoutActions()
--
первоисточник всего подхода -- показывает
if (do_return) ReturnMeasurements (pdr, 0, CXRF_IO_TIMEOUT); else pzframe_drv_req_mes(pdr);
И:
do_return=1
.
Откуда напрашивается идея: ну так и добавить в
PerformTimeoutActions()
этот else
, а из
canipp_ff()
передавать do_return=0
.
ЗЫ:
RequestMeasurements()
появился в u0632_drv.c (на основе которого и был сделан
canipp_drv.c).
Делался тот драйвер, кажется, находясь в Снежинске, кабы не прямо в Снежинке (в выходные?), в районе 03-09-2012 (в 201208-SNEZHINSK-ACTIVITY.txt по ошибке стоит 03-01-2012); но никаких записей на эту тему в bigfile-0001.html не нашлось.
Ну -- делаем, по тому проекту (b):
PerformTimeoutActions()
добавлен else
(т.е., ЛИБО возврат данных с флагом "таймаут", ЛИБО запрос новых измерений).
canipp_ff()
теперь делается с
do_return=0
.
Осталось проверить.
10.12.2022: попробовал проверить -- результат отрицательный: НЕ происходит сканирования. Вот просто остаётся девайс в состоянии "все каналы поболотовевшие" и всё.
Надо как-то рыть дальше...
Один положительный результат: чтобы тестировать, необязательно руками
выдёргивать-вставлять блоки (или нажимать ннайденную на них кнопку Reset), а
можно просто генерить с компьютера CAN-пакет "от имени"
тестируемого устройства -- k54/r:0xFF,9,1,2,0
.
10.12.2022: модифицирована диагностка при сканировании
-- теперь там показывается и unit_online[]
-статус, как в
u0632_drv.c.
...и чисто просмотром текста осознано, что то "решение" от 01-12-2022
является некорректным: ведь передача do_return=0
запрещает лишь
ВОЗВРАТ "таймаутных" значений, а вот ЗАПРОС ЧТЕНИЯ оно вовсе НЕ запрещает
(даже с добавленным else
-- оно ведь обратное).
11.12.2022: переделываем.
Размышления:
PerformTimeoutActions()
есть ещё в 2 файлах:
pdr->abort_measurements(pdr); if (do_return) ReturnMeasurements (pdr, 0, CXRF_IO_TIMEOUT); else pzframe_drv_req_mes(pdr);
Используется в 2 точках: собственно таймаут и "принудительный стоп"; в
первом do_return=1
, а во втором может быт и 0
в
зависимости от типа стопа.
AbortMeasurements(me); if (do_return) ReturnMeasurements (me, CXRF_IO_TIMEOUT); RequestMeasurements(me);
Используется в одной-единственной точке -- по таймауту, с
do_return=1
.
READ_STATE_NONE
и последующим запуском измерений:
нужно ещё успеть выполнить то самое сканирование.
Т.е., НЕЛЬЗЯ сразу запрашивать новые измерения.
do_return
;
do_request
".
Громоздковато.
CXRF_IO_TIMEOUT
-данные, И запросить
новое измерение.
Аналог u0632_drv.c'шного do_return=1
.
Аналог общепринятого do_return=0
.
Но потом осознал, что в такой ситуации любой таймаут сразу будет навечно
(до 0xFF/is_a_reset) прекращать измерения -- заказывать-то их больше некому,
т.к. RequestMeasurements()
более ниоткуда не вызывается (кроме
как из drdy_p()
и ещё в конце сканирования).
Видимо, это и есть причина, почему в
u0632_drv.c::PerformTimeoutActions()
отсутствует тот
"else
".
Потому переделал на нынешний вариант, и код ниже уже от него.
Реализация крайне проста:
PerformTimeoutActions()
теперь содержит ДВА ОТДЕЛЬНЫХ
условия:
AbortMeasurements(me); if (do_return > 0) ReturnMeasurements (me, CXRF_IO_TIMEOUT); if (do_return >= 0) RequestMeasurements(me); // -1, <0 -- do NOTHING
_ff()
-- соответственно,
if (is_a_reset) { PerformTimeoutActions(me, -1); }
Проверка:
do_return=0
и вокруг вызова
PerformTimeoutActions(me,0)
добавлен отладочный вывод значения
read_state
: так оно показало значение 2
,
READ_STATE_WAIT
.
Это подтверждает верность вчерашней гипотезы про некорректность предыдущего решения.
Поскольку "всё сложно", то нет уверенности, что теперь сделано абсолютно правильно, а подождём результатов эксплуатации.
11.12.2022@вечер-ужин: а вообще вся эта неопределённось и "всё сложно" -- из-за того, что
measuring_now
.
В очередной раз убеждаюсь в том, что
ЗЫ: а вот если бы изначально был сделан драйвер CAN-устройства, на основе машины состояний, то уж CAMAC-вариант на его основе изобразить было бы несложно.
12.12.2022@утро-~09:40, спускаясь
по лестнице с 10-го этажа по дороге на работу на планёрку: неясно
всё-таки поведение ДО этих модификаций: ну ладно, сканирование не работало,
но ЗАПУСКИ-то почему не отрабатывались? Ведь в цепочке
canipp_ff()
->PerformTimeoutActions()
->RequestMeasurements()
->StartMeasurements()
вся подготовка к запуску ДЕЛАЕТСЯ; так какого чёрта оно не отрабатывается?
@вечер-ванна: а может, дело в том, что подготовка делается -- какой-то там битик в команде чтения выставляется, но затем тут же при инициировании сканирования генерится ещё команда чтения, и уже в ней этот же битик сброшен -- вот старты и игнорируются?
@вечер-после-ванны: да,
похоже, дело в этом: "подготовка" сводится к чтению по
адресу
"ADDR_ACCESS_CREG | ADDR_ST1
",
а "сканирование" -- это точно такое же чтение по адресу
"DESC_READ_n_base + unitn2unitcode(unit), CANIPP_CADDR
"
-- т.е., БЕЗ битика ST1!
Вчера Жариков прислал документ
(20180628-zharikov-svarka-chemy.docx), из которого следует, что
добавить надо будет только 1 канал измерения. И чтоб спрашивался он лишь
при новой прошивке -- sw_ver>=9
(на сварках в 13-м здании
sw_ver=8).
А сейчас посмотрел драйвер и карту внимательнее -- оказывается, УЖЕ есть заразервированный канал измерения, как раз 9-й. Достаточно будет его разрешать спрашивать при подходящей версии прошивки.
01.07.2018: сделано, и в hw4cx/, и в v2cx/.
WELD02_MES_n_RSRVD
->WELD02_MES_n_I_INDR
и
WELD02_CHAN_MES_RSRVD
->WELD02_CHAN_MES_I_INDR
.
supports_adc9
, ...
(sw_ver >= 9)
, ...
supports_adc9
.
weld02_init_d()
добавлен
SetChanRDs(, WELD02_CHAN_MES_I_INDR, 1, 4095.0/250.0, 0.0)
.
За компанию там отражено то, что на чёмской сварке в 5 раз отличается коэффициент при измерительном канале 2 (WELD02_CHAN_MES_UN):
weld02_ff()
принудительно делается
SetChanRDs(devid, WELD02_CHAN_MES_UN, 1, (sw_ver <= 8)? 4095.0/125 : 4095.0/25, 0.0);
ReSendSetting()
выглядит бессмысленным из-за того,
что для каналов записи НИГДЕ не взводится known.read[].
15.01.2019: поскольку канал ADC#9 работает (правда, в v2cx, но это непринципиально_, то данный раздел закрываем.
28.01.2019: уже и в hw4cx тоже проверено что работает.
sw_ver>=9
.
Тут уж точно только раздвигать карту.
15.01.2019: делаем.
WELD02_SET_n_UNS8
(т.к. канала 8 не существует, а сразу идёт
9).
WELD02_SET_n_PIND
и
канал WELD02_CHAN_SET_PIND
с именем set_p_indr.
supports_out9
, заполняемое аналогично
supports_adc9
-- = (sw_ver >= 9)
.
_rw_p()
:
WELD02_CHAN_SET_UNS8
добавлено отдельное ограждение, чтобы он всегда отдавался как UNSUPPORTED.
WELD02_CHAN_SET_PIND
-- условное ограждение,
только при не-supports_out9
.
supports_out9
=0, а устройство
"рождается" в состоянии OPERATING (плохо!!!), то первый запрос на
вычитывание уставки будет отфутболен.
Поэтому в weld02_ff()
вставлена принудительная отправка
команды вычитывания -- 0x99
-- в случае, если канал должен
поддерживаться. Тогда он вначале взморгнёт бордовым и потом тут же
онормалится.
28.01.2019: проверено -- работает.
Кстати, чёмская сварка переведена на v4.
Как выяснилось, они есть ТОЛЬКО на чёмской сварке с версией 9, а на "новой маленькой", где версия 11, их нет.
Поэтому условие заменено.
04.08.2021: а ещё оказалось, что у канала MES_UN
коэффициент R -- указываемый в той же weld02_ff()
-- равен
4095.0/25 тоже ТОЛЬКО для sw_ver==9, а для всех остальных (не только <9,
но и для >9, включая текущую 1) -- 4095.0/125, как и раньше.
Так что и тут условие заменено.
Поскольку тут весьма специфическое использование, а формально платка позволяет организовывать полноценный serial link, то для данного конкретного варианта использования ("просто один 24-битный регистр, который можно писать и читать") делаем драйвер под названием "slio24_reg".
24.09.2018: проверил -- пишет, и потом записанное читает (также "независимо" проверялось can-монитором).
Теперь для реального использования нужно будет добыть формулу пересчёта между частотой и этим значением, и постараться её в devlist-canhw-19.lst вбухать.
26.09.2018: собраны данные от гусиной программы -- пишем частоту (DRVH, DRVL, DRVL+1, DRVH-1), грохаем EPICS, вычитываем число canmon'ом, переводим в двоичку и отдаём табличку Фролову. Фролов помаялся -- вроде там какой-то бардак, биты/байты перепутаны, но принцип уже виден. Думает.
02.10.2018: навроде протокола-истории:
Формула для расчета кода (Code) в частотный детектор (24bit CAN SLIO24). Code=(val-eoff)/eslo eoff=2831316.8384 eslo=0.0122886464
dev slio slio24_reg/remdrv ~ 0,7 b:v5-sr-rfr # Code=(val-eoff)/eslo, eoff=2831316.8384, eslo=0.0122886464 # Code=(val+D)*R => D=-2831316.8384, R=1/0.0122886464=81.37592762047413 cpoint ic.linRF.frq.set slio.word24 81.37592762047413 -2831316.8384
Это так, к вопросу "как именно указывать способ преобразования инженерных единиц" от 24-08-2008.
Засим можно считать, что драйвер работает как положено, "done".
История:
К сожалению, Беркаев деталей той истории сейчас не помнит.
А у меня возникли некоторые сомнения в корректности организации проверки:
А поскольку там при отправке НЕ используется
SQ_REPLACE_NOTFIRST
, то в очереди накопятся множественные
запросы.
И анализ значений из лога показывает, что различие обычно ровно в 1 бит.
Как-то надо бы управильнить работу, избавившись от этого потенциального race condition.
16.11.2018@утро-дома: как именно можно "делать по-правильному"? А вот как: махинировать с флагами не в момент постановки пакетов в очередь, а в момент ОТПРАВКИ. Для чего использовать on_send-callback.
rd_req_sent
взводить оттуда.
last_known_mask
записываемое значение.16.11.2018: неа, last_known_mask
заполняется
по ПОЛУЧЕНИИ пакета, а не в момент отправки.
Одна проблема: в cankoz_lyr'е халтурно сделан on_send-callback: он НЕ
передаёт данные самого пакета -- dlc,data[dlc], а для п.2 желательно бы
брать записываемую маску прямо из пакета; особенно актуально будет, когда
заюзаем SQ_REPLACE_NOTFIRST
.
16.11.2018: делаем.
...и это оказалось ненужным :D
CANKOZ_DESC_GETDEVSTAT
делалась в нескольких
местах, арэтому она вытащена в send_rdmode_cmd()
, которой
передаётся "условие" how
.
q_enqueue_v()
, с тучей
параметров, в т.ч. on_send
.
OnSendRdModeCmdCB()
, вся
деятельность которой и сводится к взведению rd_req_sent=1.
Этот вариант скопирован на пульт -- ждём очередного рестарта сервера.
17.11.2018@утро-дома: да, утром сервер был рестартован и наблюдаемая картина изменилась: ругательство про "mask=%d, != last_known_mask=%d" теперь при работе не светится.
Зато появляется в обязательном порядке при старте!
Оно и понятно: всё потому, что при начальном чтении НИКАКОГО значения еще
не известно (точнее, "помнится" 0), а rd_req_sent
НЕ взведена
-- вот оно и выполняет проверку и видит несовпадение.
Вывод: нужно при первоначальном чтении тоже исполнять взведение rd_req_sent=1 в момент отправки пакета.
17.11.2018@суббота-после-обеда-пультовая: исправлено, но
халтурнее: просто делается rd_req_sent=1 прямо в _init_d()
перед начальным вызовом
cgvi8_mask_hbt()
.
Вечером: а можно было и иначе:
использовать privptr
, и в нём передавать NULL из
_init_d()
и не-NULL при регистрации таймаута, чтобы при
privptr==NULL оно б просто регистрировало, но НЕ отправляло бы запрос на
чтение.
17.11.2018@суббота-после-обеда-пультовая: однако даже во вчерашней версии вылез странный баг: сначала вроде работало, а потом, как говорит Беркаев, стали время от времени сами по себе посиневать (DEFUNCT) все каналы маски. А если щелкнуть любым из них -- то он ставится в указанное значение, а остальные "прорастают".
И потом, уже в моём присутствии, оказалось, что они такие посиневшие прямо с самого запуска.
RstDevTimestamps()
-- он единственный, помимо
CxsdHwSetDb()
, где делается сброс
timestamp.sec=INITIAL_TIMESTAMP_SECS
.
Но такого вроде бы не может быть -- ведь ответный пакет от устройства приходит ощутимо ПОСЛЕ старта и инициализации драйвера...
Надо б попробовать:
19.11.2018: проверяем...
cgvi8_mask_hbt()
": работает!!!
WTF?! Т.е., rd_req_sent
, должная быть просто
диагностической фичей, влияет на ФУНКЦИОНАЛЬНЫЕ характеристики драйвера?!
Пятью минутами позже: а-а-а, разобрался! Баран!!!
cgvi8_in()
при rd_req_sent==0 делается
return
вне зависимости от результатов проверки.
Это для того, чтобы периодический поллинг не мешал обычной работе (чтоб каналы маски не генерировали обновления без спросу).
mode.rcvd
, вызывающему дополнительное
чтение).
Тоже работает.
Этот вариант и оставляем.
22.11.2018: однако по логам видно, что события "несовпадения маски" всё же возникают, время от времени, причём:
Также оказалось, что аналогичные события всё-таки ВСТРЕЧАЮТСЯ в логах на vepp4-pult6:2:
Чтоб понять причину происходящего, на canhw было запущено логгирование всех записей/чтений масок у всех ГВИ.
...и буквально через полчаса оно дало результат:
# 20181122-canhw-23-cgvi8-mask.log @2018-11-22-13:26:00.717119 k6/u.0:0xFE @2018-11-22-13:26:00.719101 k6/r.0:0xFE,0x00,0x1E,0x00,0x00 @2018-11-22-13:27:00.714222 k6/u.0:0xFE @2018-11-22-13:27:00.716184 k6/r.0:0xFE,0x00,0x1E,0x00,0x00 @2018-11-22-13:28:00.715605 k6/u.0:0xFE @2018-11-22-13:28:00.717574 k6/r.0:0xFE,0x00,0x1E,0x00,0x00 @2018-11-22-13:28:50.230570 k6/u.0:0xF0,0x1F,0x00 @2018-11-22-13:28:50.231921 k6/u.0:0xFE @2018-11-22-13:28:50.234500 k6/r.0:0xFE,0x00,0x1F,0x00,0x00 @2018-11-22-13:29:00.716568 k6/u.0:0xFE @2018-11-22-13:29:00.718651 k6/r.0:0xFE,0x00,0x1F,0x00,0x00 @2018-11-22-13:29:28.632345 k6/u.0:0xF0,0x1E,0x00 @2018-11-22-13:29:28.633572 k6/u.0:0xFE @2018-11-22-13:29:28.635208 k6/r.0:0xFE,0x00,0x1E,0x00,0x00 @2018-11-22-13:29:28.665599 k6/u.0:0xF0,0x0E,0x00 @2018-11-22-13:29:28.666922 k6/u.0:0xFE @2018-11-22-13:29:28.668534 k6/r.0:0xFE,0x00,0x0E,0x00,0x00 # /var/tmp/4drivers.log 2018-11-22 13:29:28.668 canhw cxsd#23: cgvi8[18]gid25_group1_gvi/DEFAULT: GETDEVSTAT: mask=14, != last_known_mask=30
Т.е.:
cgvi8_mask_hbt()
отрабатывал (по счастливой
случайности) в 00 секунд каждой минуты.
КАК оно могло оказаться сброшенным?
Грустное впечатление, что все проблемы вызваны исключительно самой попыткой проверить "не меняется ли маска в устройстве сама" -- не будь этой проверки, и проблемы бы не было.
Промежуточные выводы:
А то вдруг в будущем понадобится что-то подобное -- чтоб уметь выполнять безглючно.
11.12.2018: оказалось, что фича "отладка" была всё ещё
включено, что на пульту сильно мешало. Поэтому на сейчас сделано
DEBUG_CGVI8M_MASK
:=0.
Смысл -- вчера, 28-02-2019, на еженедельной встрече по автоматизации было упомянуто, что, возможно, понадобится драйвер CKVCH -- для использования на ВЭПП-2000.
Понадобится ли реально или нет -- хбз, но чисто ради практики (и для отвлечения от рутинной возни с CAN-серверами и прочего) можно взять да изготовить драйвер.
01.03.2019: делаем "скелет", пока почти без функционала. За основу взят драйвер CPKS8.
SQ_REPLACE_NOTFIRST
.
Но собственно кодирования значений с маской и раскодирования -- пока нету.
04.03.2019: продолжаем.
req_val
и req_msk
.
_in()
, в реакции на "статус" -- взятие оттуда
"конфигурации" (биты b4b5) и складирование в me->config
и
числа коммутаторов в me->num_outs
.
Отдаёт ВСЕГДА по 4 канала, но в первых num_outs
-- значения,
а остальные -- 0 и CXRF_UNSUPPORTED
.
Всё -- теперь нарисовать скрин (там бы 4 набора селекторов, и чтоб "не те" наборы дисэйблились бы) и можно тестировать.
05.03.2019@лыжи: у самих ВЭПП-манов-2000 наверняка тип КВЧ фиксирован и прошит в софте, так что им автодетект нафиг не нужен.
Можно сделать auxinfo-флаги, которые бы форсили конкретный тип (а по умолчанию -- ABSENT=0), и тогда тип будет всегда браться оттуда, а биты b4b5 из присланного "статуса" -- игнорироваться.
07.03.2019: делаем.
privrec_t
добавлено поле force_config
.
ckvch_params[]
, ключи там названы
detect, 4x2_1, 1x8_1, 2x4_1.
_init_d()
.
_ff()
зануление обоих значений НЕ выполняется.
_in()
присланные биты типа игнорируются, а вместо них
используется значения force_config
.
Также сделан скрин ckvch.subsys -- по "проекту" от 04-03-2019: 3
набора селекторов, и в зависимости от текущего типа работает лишь 1, а
остальные дисэйблятся. Да -- скрин проверен (на симуляторе с
-S
).
08.03.2019@лыжи: а еще бы сделать канал для отображения ВСЕГО 4-битного регистра -- ведь на ВЭПП-2000 наверняка именно так и работают.
12.03.2019@дома: да, сделано.
CKVCH_CHAN_OUT_4BITS
, out_4bits.
_in()
значение отдаётся рядышком.
(18.01.2023: Файл "CAN 3x imp_V1.4.docx", у нас это 20220913-senchenko-CAN-3x-imp_V1.4.docx -- о-без-пробелено.)
Судя по переписке, устройство получило название "ГИН25", поэтому драйвер назовём "gin25".
29.09.2022: в первую очередь создаём "скелет" -- файлы gin25.devtype и gin25_drv_i.h плюс gin25_drv.c (за основу взят cgvi8_drv.c, как близкий по "навороченности" плюс имеющий ту же длину имени).
18.01.2023: вчера возникла уже реальная необходимость в драйвере, так что возвращаемся к работе над ним.
Сначала обзор того, что ТОГДА было сделано (а что нет):
rd_rcvd
и
gin25_alv()
.
(А я-то планировал сейчас начать как раз с добавления этой функциональности.)
...только определение собственно ALIVE_USECS
было забыто :D.
gin25_in()
и...
gin25_rw_p()
.
GIN25_CHAN_*
.
GIN25_CHAN_*
и
вообще карта каналов; ни в gin25.devtype, ни в
gin25_drv_i.h.
ReturnErrorBits()
и поле err_reg[]
.
Т.е., по факту -- были сделаны почти ВСЕ мозги, но недоделаны определения.
Делаем:
ALIVE_SECONDS
с ALIVE_USECS
.
err_reg[GIN25_NUM_LINES]
ReturnErrorBits()
-- собственно возврат скопирован с
cankoz_lyr_common.c::cankoz_regs_f8()
, где тоже одним
махом возвращается 8 каналов-битов данных плюс 8-битное значение.
В конце концов было решено побитовые каналы иметь одной пачкой 3*8 штук, а остальные отдельно группами по 3 штуки (а НЕ пачками по 10 штук (8 1-битовых плюс 1 8-битовый и 1 "STATUS")).
Драйвер компилируется, теперь надо будет его тестировать.
19.01.2023: и скрин gin25.subsys сделан.
А ещё в переписке с Сенченко (вроде бы отвечающим за общение со стороны ИЯФ с авторами железки) выяснились любопытные "нюансы":
Но "Формат пакета не поменялся. Как было 4 байта, так и осталось. Просто старший 0."
"Там связь ЦАП/АЦП и киловольтов не совсем простая. Детали не помню, приведение к реальным значениям требовало калибровки. Договорились, что измерение/задание будет в вольтах (3000 для 3кВ)."
Как 1-канальный девайс будет реагировать на команды, обращённые к отсутствующим 2 каналам? -- "Не знаю. Не проверял."
23.01.2023: небольшое изменение: теперь
как IS_AUTOUPDATED_TRUSTED
объявляются не все каналы группы
[STATUS_first,+STATUS_count), а только [ERROR_first,+ERROR_count).
(И, поскольку оный "Status" определяется как "Control|Разрешение с передней панели", то он может дрыгаться с передней панели независимо.)
_rw_p()
по запросу (и так оно было изначально).
gin25_init_d()
изменено -- чтоб троица
"STATUS" была обычными опрашиваемыми каналами.
08.02.2023: проведён первый тест на живой железке; ниже результаты, перечисленные в письме Сенченко и Касаеву (Message-ID: 466e35bd-344-d916-5522-4ac3ebd944d@starnew.inp.nsk.su).
Всё тестирование проводилось вручную, при помощи программки-CAN-монитора -- и отправка команд, и слежение за линией.
Сначала о странностях:
Теперь о хорошем:
- Переключатели установки адреса работают как-то странно: при переводе в "1" джампера N2, который, судя по карте на стр.2 описания, должен был дать адрес 1 в 6-битной адресации Козака (или 4 в 8-битной адресации Панова), получили адрес 8.
- Команда 0x11 НЕ изменяет значение зарядного напряжения. Ни в ручном режиме, ни в режиме от компьютера -- какое значение ей ни посылай, всё равно присылает в ответ то, что было установлено раньше с передней панели. (Я даже пробовал вместо 4-байтного пакета (команда+3байта_данных) отправить 8-байтный (дописав в конце 4шт 0x00) -- в предположении, что какой-то косяк с проверкой размера пакета. Не помогло -- не уставляет.)
- Переключение в ручной режим управления -- выключение питания, втыкание разъёма, включение питания -- вовсе НЕ отрубает работу с CAN-bus: все команды воспринимались и давали ответ, в т.ч. команда 0x01 успешно включает непрерывные измерения. Единственное, что НЕ работает -- управление зарядом посредством команды 0x31. (А 0x11 не работает в любом случае.)
- Попытки обращения к каналам 2 и 3 -- команды 0x02, 0x03, 0x22, 0x23 и прочие 0x*2 и 0x*3 -- не дают никакого ответа, устройство их просто игнорирует. Это не очень хорошо, поскольку для совместимости лучше бы как-то на них отвечать -- либо всегда нулями в полях данных, либо каким-нибудь специальным кодом (вроде кода 0x00 в CAN-Панов aka CAN-FF). В нынешней же ситуации потребуется 2 варианта драйвера -- для 3-канального на ЛИУ и для 1-канальных на ВЭПП-2000 и ВЭПП-5.
- Команда выдачи измерений зарядного напряжения с указанной периодичностью 0x01 работает -- измерения присылаются, период регулируется.
- Команда включения/выключения заряда 0x31 работает -- заряд идёт, импульсы по внешнему запуску делаются.
- Никаких ошибок поймать не удалось -- по 0x41 в байте 3 постоянно возвращался лишь 0.
Вследствие этого проверить работу команды 0xF0 -- отправка регистра статуса/ошибки при возникновении ошибок -- не удалось (было отправлено 0xF0,0xFF, но никаких уведомлений так и не поступало).
Чисто для информации:
- В пакете 0xFF от устройства присылаются: байт 1 - код устройства 131, байты 2 и 3 - HWver=2, SWver=9. Любопытно было бы узнать, какие числа отдают варианты для ЛИУ и ВЭПП-2000 -- можно ли определять тип и число каналов по ним?
Теперь формулировка для отправки товарищам авторам:
В текущей прошивке не удаётся установить зарядное напряжение от компьютера: отправляемое в CAN-команде 0x11 число игнорируется и всегда остаётся то значение, что установлено с передней панели. Просьба прокомментировать ситуацию: это дефект текущей прошивки или требуются какие-то иные CAN-команды?
09.02.2023: по результатам вчерашнего тестирования стало очевидно, что драйвер 3-канального с 1-канальным работать не сможет -- первая же команда 0x*2 или 0x*3 зависнет в очереди навечно, поскольку ответа на неё никогда не поступит.
Поэтому реализуем исправление:
enum {GIN25_CUR_LINES = 1}
GIN25_NUM_LINES
(=3) во
всех местах, могущих слать пакеты -- это SendPERIODIC()
и
gin25_rw_p()
(в которой проверяется попадание запрашиваемого
канала в диапазон [base...base+N), в ответ на который и будет посылка).
gin25_in()
.
_rw_p()
--
if()
/else if()
/...,
а в _in()
-- switch()
.
switch()
нельзя использовать в селекторах
переменные, а в if()
можно, то можно вместо захардкоживания
GIN25_CUR_LINES
использовать некое поле в privrec'е, которое
устанавливать в 1 или 3 либо из auxinfo, либо на основании данных
HWver/SWver из 0xFF.
13.02.2023: Алексей Касаев отправил письмо с вопросами производителям источников.
17.02.2023: получен ответ от производителей, главное в котором --
Некоторые из обнаруженных вами проблем связаны с неудовлетворительным описанием выбора режима управления с помощью перемычек. Это описание содержится в сопроводительном документе.
И в приаттаченном "Переключение режимов работы.docx" есть распиновка разъёма/ключика в DB9, где присутствуют и вход аналогового управления (которого, как мы думали, нам сделать забыли) на контакте 2, и раздельно ДВА контакта для выбора режимов: "локальное/CAN" (3) "аналог/цифра(CAN или локальное)" (4).
20.02.2023: покамест заменяем захаржкоженное
GIN25_NUM_LINES
на возможность указывать в auxinfo.
DEV_LINES
,
me->DEV_LINES
;
GIN25_NUM_LINES
убрано.
21.02.2023: провели с Касаевым тестирование уже с правильной распиновкой ключика.
nl = desc & 0x0F;
вместо
nl = desc & 0x0F - 1;
Косяк в том, что "нумерация" в пакетах идёт не с 0, а с 1;
nl
же считается с 0.
Надо будет завтра проверить.
22.02.2023: проверил -- заработало.
Но, похоже, чисто случайно: при очередной пересборке обратил внимание на gcc'шный warning, советующий поставить в этом выражении скобки. Скобки поставил -- теперь
nl = (desc & 0x0F) - 1;
-- и после анализа
таблицы приоритетов операторов в C
стало ясно, что GCC был прав, а работало по чистой случайности: приоритет у
вычитания '-
' выше (6), чем у bitwise and '&
'
(11), так что по факту бесскобочный вариант работал как
"desc & (0x0F - 1)
",
что по случайности давало для desc=0x11 и =0x21 нужное значение 0
.
Далее обнаружилось ещё 2 странности:
Разобрался -- shame on me, косяки были тупейшие.
вместоme->lvmt->q_enq_v(me->handle, SQ_IF_NONEORFIRST, 4, DESC_SET_U_SET_n_base + nl, ( val & 0xFF) | ((val >> 8) & 0xFF) | ((val >> 16) & 0xFF));
-- т.е., вместо 3 байт передавался 1 странного вида -- младший, а вместо старших 2 брался какой-то мусор, что и приводило к превышению 65534, каковой максимум девайс и форсил.me->lvmt->q_enq_v(me->handle, SQ_IF_NONEORFIRST, 4, DESC_SET_U_SET_n_base + nl, ( val & 0xFF), ((val >> 8) & 0xFF), ((val >> 16) & 0xFF));
Будто откуда-то копировал (или "подсматривал"?), но вот откуда? Точно не из cgvi8_drv.c -- там подобного нету.
вместоval = data[1] | (data[2] >> 8) | (data[3] >> 16);
-- т.е., и сдвиг был вправо (по факту -- деление до 0) вместо влево, и байты к 32-битности не приводились.val = (uint32)(data[1]) | ((uint32)(data[2]) << 8) | ((uint32)(data[3]) << 16);
Неприведение -- видимо, неважно, т.к. в cgvi8_drv.c его нет и
там работает (хотя в cac208_drv.c и прочих на основе
kozadc_decode_le24()
-- есть).
А вот сдвиг ВПРАВО -- это уже шиза какая-то.
"В каком бреду я был, в како-ом ды-ы-му-у-у...", что такое накалякал? (Впрочем, то было написано где-то в промежутке между 17-09-2022 и 02-10-2022; я тогда был в дикой усталости.)
Теперь -- ИСПРАВЛЕНО; и, похоже, работает (на этом варианте драйвера мы с Касаевым провели целую серию тестов работы источника на тему "сколь быстро он отрабатывает смену уставки" -- ставилось timecode=1 (20ms) и запускалось "das-experiment Iset +Imes").
24.03.2023: добавлена gin25_term_d()
,
отключающая авто-отсылку данных блоком, путём отправки ему AUTOREPORT-маски
0x00 и запроса (по числу каналов) измерений в режиме ONCE.
...сделано уже условно по "me->handle >= 0
", аналогично
вчерашней модификации остальных драйверов.
29.10.2023: поэтому драйвер переделан, чтоб возвращать ТОЛЬКО ИЗМЕНЕНИЯ.
int lsrv[GIN25_NUM_LINES][3]
;
имено int
, чтоб можно было поместить значение -1
,
не могущее появиться из устройства.
gin25_ff()
туда прописываются все -1
.
gin25_in()
возврат теперь делается условно -- только
если пришедшее значение отличается от последнего известного (куда пришедшее
при этом сохраняется). Из-за начальных -1
первое после 0xFF'а
будет ВСЕГДА возвращаться.
09.01.2024: однако, накосячено тогда было. Замечено почему-то только сейчас (сервер не перегружали с тех пор, что ли? непонятно...):
Быстрое разбирательство показало 2 вещи:
...но главное, видимо -- всё же вследствие п.2:
При том, что в предыдущем драйвере от 25-05-2023 -- слался. Хотя изменения от 29-10-2023 вроде тривиальные и отправку никак не затрагивают...
С п.2 какая-то мистика -- проверял по всякому: и код смотрел (ничего криминального не видно), и gdb натравливал на запущенный cxsd и смотрел в нём cxsd_hw_channels[...].is_autoupdated (при нём взведённом бы не опрашивалось) -- фиг, 0 в нём.
Надо добавлять отладочную печать и тестировать...
19.01.2024: насчёт п.1 правильным вариантом было бы
отказаться от err_reg[]
, оставив вместо него
lsrv[][2]
.
Но для простоты можно просто в ReturnErrorBits()
сохранять и
в lsrv[][2]
тоже.
22.01.2024@вечер: а вот нифига
-- НЕХОРОШО отказываться от err_reg[]
, т.к. он-то был сделан
ни для какого не для кэширования, а чтобы было какое значение отправлять в
команде сброса блокировок.
22.01.2024: посидел в пультовой часок с ~10:00 до ~11:00 и разобрался. Опять "ПОЗОР!!!", блин...
DESC_GET_STATUS_n_base
, в его получение, а потом и в
gin25_ff()
.
Ту-то и дошло:
Так что идея с самого начала была хромой.
Но тогда надо как-то организовывать этот самый опрос, с какой-то частотой, а с какой?
-1
?).
rd_req
НИГДЕ НЕ сбрасывается, кроме как в
ReturnDataSet()
; как тогда может работать "рестарт" устройства
(а вот в v2 -- сбрасывалось, в RemitChanRequestsOf()
, которого
в v4 нету)?
Чуть позже: разобрался всё дело в
ReRequestDevData()
, он отдельно пере-спрашивает всё
запрошенное, а вызывается он из ReviveDev()
.
@вечер: так что:
ReturnErrorBits()
сохранять и в lsrv[][2]
тоже.
Как же всё-таки можно б было организовать оптимизацию -- НЕотсылку значений при совпадении с предыдущими -- пока загадка. Ну да, в Tango и EPICS когда-то было нечто на эту тему -- указываемые клиентом "фильтры", позволяющие указывать размер отклонения, который если не превышается, то и отправлять не надо. Но сейчас осталось вроде только в Tango и только server-side (указывается в БД).
...но и с этой вещью, даже если б её реализовать в CX, был бы косячок:
каналы бы всё равно посиневали; либо пришлось бы им указывать
IS_AUTOUPDATED_TRUSTED
, что тоже плохо уже ИДЕОЛОГИЧЕСКИ, т.к.
сама идея "посиневания" при этом отключается.
28.01.2024@после лыж, выезжая со
стадиона НГУ: а вообще-то конкретно ТУТ, с блокировками, ровно тот
случай, когда нужно устраивать периодический опрос (например, 10Гц), отдавая
наверх данные только при изменениях, а самим каналам делать
IS_AUTOUPDATED_TRUSTED
, да.
08.03.2025: сделал периодический опрос, 10Гц. За
образец взял senkov_vip_drv.c -- там максимально похоже (в отличие
от *_hbt()
в разных ЦАП'ных драйверах, где оно сильно завязано
на advdac).
Пока что оно в #if
'ах.
Надо проверять на живом железе.
10.03.2025: понедельник, профилактика на ВЭПП-5, начал проверять -- и опять нифига: каналы блокировок остаются посиневшими!
gin25_ff()
-- увидел чудное:
она НЕ вызывается!
Козак ради этого на разные типы пакетов имел разные "mailbox'ы" -- это он при личном разговоре рассказывал.
DoRecv()
,
принимающий пакеты в течение не более указанного времени).)
А что делать СЕЙЧАС?
gin25_ff()
в gin25_init_d()
.
Именно СКОПИРОВАНО: заполнение в _init_d()
отвечает за
первоначальную отдачу, а в _ff()
-- для сброса при последующих
Reset/PowerOn/BusOff'ах устройства.
CANKOZ_DESC_GETDEVATTRS
только при
id_reason==CANKOZ_IAMR_GETDEVATTRS
-- в таком случае при
неответе на первоначальный 0xFF будет отправлен ещё запрос.
CANKOZ_LYR_OPTION_
-флаг "удалять из очереди по любому
reason'у".
Но пока не проверено -- видимо, в следующий понедельник во время профилактики.
17.03.2025: проверено -- да, работает, по ответам с id_reason!=2 (в т.ч. по =3 "WHOAREHERE") пакеты 0xFF из очереди не удаляются и через некоторое время посылаются повторно, уже приводя к ответу. Неудобство в том, что "некоторое время" -- аж 10 секунд, что не очень приятно, но лучше, чем ничего.
_rw_p()
их
игнорировать.
16.03.2025: да, добавлено игнорирование. Осталось проверить.
17.03.2025: проверено -- всё OK. И было чётко видно: если
раньше запросы+ответы DESC_GET_STATUS_n_base
=0x41 летали каждые
100мс (от внутреннего периодического опроса) плюс отдельно каждые 200мс (от
опроса канала GIN25_CHAN_STATUS_n_base
по циклу сервера), то
после изменения осталось только каждые 100мс.
gin25_ff()
отправка пакета DESC_SET_AUTOREPORT
=0xF0 за-if(0)'ена;
когда и почему?
13.03.2025: когда -- 21-02-2023 ещё не было, а 25-05-2023 уже есть. Чуть позже: и даже 24-03-2023 ещё не было, а вот 25-05-2023 -- это ЕДИНСТВЕННОЕ изменение. А вот записей за этот промежуток по теме вопроса нет, так что "почему" -- хбз. Возможно, ради диагностики; или из-за того, что были проблемы с командой 0x41.
13.01.2024@дорога домой после похода в Эдем/Быстроном/Ярче-на-демакова, ~15:20: несколько соображений:
@лыжи: а может, ещё и указывать, через СКОЛЬКО "тиков" отрабатывать обновление? Тогда придётся очередь сортировать "по времени" -- ближайшие вперёд.
22.01.2024: неа, дюже усложнит всё: получится почти "планировщик", плюс очередь придётся двигать (что сразу потребует заменить "запись на этот канал уже в очереди в такой-то ячейке" на сложную инфраструктуру вроде двунаправленного списка). Так что -- неа, не стоит.
И вот поверх этого уже механизм, чтоб можно было в auxinfo указать, что значение такого-то канала при записи сделать текущим значением такого-то канала чтения -- так и контрольные измерения АЦП можно имитировать, и битики входного регистра, показывающие отработку "источником" управляющих битиков в выходном регистре.
@лыжи, 10кругов~=3.5км по стадиону НГУ: ещё соображения:
21.01.2024@послеобед, смотрение
какого-то видосика: неа, "NOT" -- предполагает кодирование знаковых
целых в дополнительном коде, что излишнее требование. Лучше просто записывать НОМЕР+1, а после парсинга в
_init_d()
делать всем номерам --
(декремент):
эффект тот же, но проще и элегантнее.
on_write=out0:adc0
-- но это потребует GetDevChanByName()
, а он присутствует
только в серверном варианте, но не в remcxsd'шном, под который CAN-драйверы
тоже собираются.
Так что придётся сборку делать хитрее -- указывать не в
LISTOFCANDRIVERS
, а в какой-то другой переменной...
17.01.2024@утро, после завтрака: только, наверное, не "on_write", а "on_change" -- отражать-то, по хорошему, надо не в момент записи, а в момент ПОДТВЕРЖДАЮЩЕГО ВОЗВРАТА.
А поскольку подтверждающий возврат будет выполняться с отсрочкой, то можно будет даже что-то типа пинг-понга устраивать.
21.01.2024@дорога из Быстронома+Ярче, по Николаева: ага-ага, "пинг-понг", как же как же! Ведь предполагается, что это "on_write"/"on_change" будет указываться каналам ЗАПИСИ с отображением на каналы ЧТЕНИЯ. А чтобы делать "пинг-понг", надо будет "целевым" указывать также канал ЗАПИСИ, и чтобы по возврату исходного канала через тик выполнялся не просто возврат целевого, а именно ЗАПИСЬ в него.
...в принципе, конечно, можно смотреть по типу целевого канала -- R или W -- и выполнять разные действия: R -- просто вернуть; W -- повторить запись.
22.01.2024@дорога из ИЯФ к Морскому-2, около Мясоroob/Академия Кофе, ~12:00: а если "приделать" этот механизм к noop_drv.c, то можно было бы изготавливать почти произвольные симуляции.
Только:
_init_d()
аллокировать privrec по
количеству каналов конкретного устройства, добытому от
GetDevPlace()
.
INT32
, но и с
другими размерами и с вещественными, а также не только со скалярами.
Но это сразу изрядное усложнение.
@вечер, записывая: резюме: заниматься-то этим сейчас не будем, но при реализации первого варианта для simkoz_drv.c надо держать те хотелки в голове, чтобы не принимать решений, могущих в будущем осложнить их (хотелок) реализацию.
01.02.2024: потихоньку приступаем -- пока что убираем всё лишнее (связанное с общением с CAN и с таблицами), стараясь приблизиться к компилируемости:
#include "advdac_cankoz_table_meat.h"
,
и определения, и поля t_*
из privrec_t
, вызовы
SetTmode()
, HandleTableHbt()
,
HandleTable_rw()
и ReportTableStatus()
, плюс
собственно CrunchTableIntoFile()
.
lvmt
и layer'а вообще.
_ff()
и _in()
, а также
SendMULTICHAN()
.
rd_rcvd
плюс сама simkoz_alv()
.
supports_unicast_t_ctl
.
val_to_daccode_to_val()
сделана просто возвращающей исходное
значение.
И заодно все DESC_*
были выкинуты.
Компилируемость пока НЕ достигнута, т.к. в SendWrRq()
,
SendRdRq()
и simkoz_rw_p()
остались попытки
отправки пакетов.
02.02.2024@где-то на улице, по пути между ИЯФом и Эдемом и Коптюга-19 и П28: а ведь надо каналы АЦП возвращать не только сразу после модификации каналов ЦАП, а ПОСТОЯННО, имитируя опрос; и по-хорошему -- возвращать их потихоньку друг за дружкой, имитируя циклический опрос одним измерителем.
Следствия и идеи по реализации:
simkoz_hbt()
.
ch_beg
/ch_end
, "крутя цикл" между ними.
Для чего ввести индекс "очередной вертаемый канал" -- например,
ch_cur
.
SendMULTICHAN()
, чтобы он
ch_beg
/ch_end
и являются границами,
а они уже и есть автоматом) (04.02.2024: точнее,
ch_cfg[KOZDEV_CHAN_ADC_BEG]
/ch_cfg[KOZDEV_CHAN_ADC_END]
)
и делал бы ch_cur=0
.
03.02.2024: да не, можно забить -- незачем усложнять.
03.02.2024: делаем.
ch_cur
.
simkoz_hbt()
выполняется "перебор каналов", по
1шт за раз.
Но вот собственно "обвязка" в виде массива "свойств" каналов, куда бы сохранялась из auxinfo информация "src_chan:dst_chan", так и не сделана; поэтому ни "возвратов-отражений" пока не делается, ни просто возвратов "записи", ни даже сам возврат при переборе каналов АЦП.
04.02.2024: пытаемся таки "обвязку" начать делать.
SIM_CHTYPE_*
-- сейчас это _R,_R_AUTOUPDATED,_W ("что делать
при записи в этот канал: возвращать, НЕ возвращать, выполнять запись").
sim_one_t
-- та самая ячейка
"свойств" канала.
Что не нравится -- оказываются перемешаны в одной структуре данных
свойства канала как ИСТОЧНИКА отражения (dst_chan
) и как
ПРИЁМНИКА (flags
), и это смешано с общими для обоих случаев
(val
).
25.04.2024: да, flags
надо уносить в
отдельный статический массив ("sim_flags[]
"?), а вот остальное
что перемешано -- это нормально: ведь та информация НЕ ПРЕДОПРЕДЕЛЁННАЯ, а
существует только в процессе работы; хотя и имеет две разных сути --
взаимоотношения между каналами (src_chan
и
dst_chan
) и текущее значение (val
) -- формально
можно и разделить, но зачем?
05.02.2024@утро, зарядка: ещё со вчера ломаю голову -- базу-то подготовил, но как-то всё не складывается общая картина на тему "как организовать инфраструктуру симуляции с эффективным исполнением отложенных обновлений-отражений".
Первоначальная задумка от начала-января-2024 выглядела так, что есть 2 структуры данных:
chn
) структур), в которую
кладётся информация об исполнении "отражений" -- кладётся в момент возврата
данных в канал-источник, чтобы потом в некий момент -- в _hbt()
-- все эти отражения скопом вернуть.
Смысл оной "очереди" -- чтобы не бежать по всем NUCHANS каналам каждый такт, проверяя, "а не надо ли его вернуть?", а просто пройтись по готовому списку изменившихся (чаще всего -- 0 штук).
...иначе же "поместить" в конец очереди запрос на возврат этого канала и значение для возврата, после чего инкрементировать количество элементов в очереди.
Так вот, собственно, цепочка мыслей, пронесшихся в голове во время зарядки:
ЗАМЕЧАНИЕ: да, если в процессе прохода какой-то ещё не вёрнутый элемент будет "помещён" повторно -- т.е., фактически просто изменено его значение для возврата -- то и фиг с ним, изменённое значение и вернём. Это неприятно, конечно -- что временнАя диаграмма возвратов искажается -- но не нарушает конечности перебора.
05.02.2024@лыжи, вечер ~17:00...18:00: насчёт перебора:
И ещё: а ведь "пинг-понг" может возникать просто прямо в процессе ИНИЦИИРОВАНИЯ возврата -- если в auxinfo указано A:B и B:A, и оба являются каналами записи, то оно так и попрёт в бесконечной рекурсии писать. Есть идея, как этого избежать:
val
, а из
src_chan'ова.
src_chan
.
link
",
который у каналов-источников будет указывать на ведомый канал, а у
каналов-приёмников на ведущий.
@дома, записывая это, ~18:20: (Кстати, тогда циклы становятся невозможны -- для каналов в таком цикле потребуется сразу ДВА поля...)
dst_chan
, если нет, то за src_chan
.
05.02.2024: Записать в раздел по Modbus о косяке Сенькова, когда он в ответ на запрос "read coils" присылал результат "read holding registers", так что из самого ответного пакета невозможно было
06.02.2024@поднимаясь по лестнице
пристройки нашего крыла на лабораторный круглый стол, между 2 и 3 этажами,
~09:45: в modbus_mon: а не добавить ли флажок "print Unidentifiable
replies" -- выдавать сообщения о непонятных ответах, несоответствующих
запросам (типа вот этого сеньковского)? Ключ -- -Du
.
Диагностировать можно как last_sent.addr<0, так и по несоответствию типа
регистров и/или операции между last_sent и пакетом. ЧУТЬ ПОЗЖЕ: с другой
стороны, оно и так покажет странный адрес -1. ЕЩЁ ЧУТЬ ПОЗЖЕ: в
modbus_mon'е отсутствует forget_last_sent()
при ПОЛУЧЕНИИ
пакета (да и в timeout_end_proc()
оно бы не помешало). Так что
как оно отреагирует на такой сеньковский глюк?
06.02.2024@дорога из Эдема домой, переходя Николаева рядом с Николаева-7: записать в "почему в Modbus-TCP нет уведомлений" -- а ещё потому, что в обычном формате пакетов ответа попросту НЕТ информации о номере региста и их количестве; пришлось бы дополнительные коды функций вводить.
23.04.2024: делаем парсинг.
sim_props[]
и ссылающаяся на...
SimPluginParser()
-- исключительно
"бизнес-логика":
recsize / sizeof(sim_one_t)
-- всё чисто и красиво (пользуемся
тем, что "полем назначения" указывается весь массив).
src_chan
и dst_chan
делается надлежащее +1
(для отличения от умолчательных нулей).
А для собственно парсинга указаний каналов 2 раза (SRC и DST) вызывающий...
parse_devchan_ref()
-- сначала выделяет строку из
[[:alnum:]_\.]
на которую потом натравливает
strtol()
, который если в endptr возвращает не совпадающее с
концом строки, то делается ещё GetDevChanByName()
(ради
которого строка копируется в локальный буфер 25.04.2024: ага, только
namebuf[namelen] = '\0'
после memcpy()
забыл,
олух... Добавлено.).
parse_devchan_ref()
-то получает
devid
от SimPluginParser()
, но тому где это
значение взять?!
int devid = ((privrec_t*)((uint8*)rec - PSP_SIZEOF(privrec_t, sim_props)))->devid;
-- т.е., "вычисляя" адрес всего privrec'а отниманием от адреса
sim_props[]
его offset, ...
PSP_SIZEOF()
" -- надо же "PSP_OFFSET_OF()
"!
int devid = ((privrec_t*)((uint8*)rec - PSP_OFFSET_OF(privrec_t, sim_props)))->devid;
me
и к
нему адресоваться повсеместно, в т.ч. при складировании результата.
_init_d()
и, соответственно, ДО прописывания
me->devid
!
Так что "простые" пути ну никак не сработают!
И что делать?
simkoz_params
и вызывать парсинг вручную, уже ПОСЛЕ
прописывания me->devid
.
ENTER_DRIVER_S()
/LEAVE_DRIVER_S()
, прописывающие
"текущий devid" в active_devid
, то в принципе нужная информация
имеется -- надо только добавить способ её добыть, вроде "GetCurrentDevid()".
@вечер: однако оказалось, что
вокруг парсинга параметров
ENTER_DRIVER_S()
/LEAVE_DRIVER_S()
НЕ
делается. Добавить-то можно, но некрасиво будет...
Вердикт: хотя с одной стороны косвенный вызов драйверова кода через PSP-плагины вроде бы подталкивает к тому, чтобы и PSP-парсинг делать "в контексте устройства", но ПОКА более правильным выглядит всё же просто переход на собственный парсинг.
Чуть позже -- так и сделано.
24.04.2024: некоторые соображения по работе собственно "отображения":
PerformReflection()
, которой передавать номер канала и
значение.
SendRdRq()
, и дёргать прямо перед
HandleSlowmoREADDAC_in()
.
Чуть позже, просмотрев код оной HandleSlowmoREADDAC_in()
, в
котором только Return'ы и никаких своих действий: неа, не "перед", а сразу
ПОСЛЕ -- чтоб уж имитировать порядок изменения (сначала меняется ЦАП, а уж
потом связанный с ним АЦП).
@вечер, перед сном: а вообще
при этом касательно связанного канала НИЧЕГО ДЕЛАТЬ НЕ НУЖНО -- лишь ТОЛЬКО
прописывать значение в поле val
писуемого канала, а связанный
канал АЦП будет вёрнут сам автоматически на очередной (его) итерации
циклического перебора.
25.04.2024: ну -- делаем, пока по минимуму...
PerformReflection()
:
.val
,
SIM_CHTYPE_R_AUTOPUDATED
, то тоже отваливает.
И больше пока ничего -- для начала (отображения ЦАП на АЦП) и этого должно хватить.
_init_d()
добавлено проставление всем АЦП-каналам
.flags=SIM_CHTYPE_R_AUTOPUDATED
(но более пока никому ничего).
@вечер, записывая это всё: а
почему вообще флаги тут же, в privrec'е в sim_props[]
? Ведь это статическая/константная информация, и они могут быть
просто в статическом массиве -- аналогично pzframe'овым
chinfo[]
.
SendWrRq()
-- просто сохраняет значение в поле
.val
;
SendRdRq()
-- вызывает
HandleSlowmoREADDAC_in()
и затем PerformReflection()
(ровно по вчерашнему проекту).
Из-за этого пришлось чуток поменять порядок определений функций
относительно
#include "advdac_slowmo_kinetic_meat.h"
-- там ведь не
прототипы, а сразу определения кода, так что...
sim_props[]
используется
-- так это канала OUT_CUR (KOZDEV_CHAN_OUT_CUR_n_base + l
).
Этот выбор выглядит очевидным (по отношению к варианту "сам ЦАП" OUT): ведь нужно в связанных каналах "отражать" именно ТЕКУЩЕЕ значение, а не просто запрошенное -- чтобы плавное изменение корректно отображалось в связанные каналы АЦП.
SendMULTICHAN()
--
просто пустой (потом надо будет выкинуть всё скопом? или сделать в него
"сброс" в виде "me->ch_cur=me->ch_beg
"?).
me->lvmt->regs_rw
просто
закомментирован, тоже для компилируемости "здесь и сейчас".
Пытаемся проверять; сначала, естественно, не пошло, даже устройство не инициализировалось. Разбираемся:
OK -- сменил на "0,2".
GetDevChanByName(devid=0/active=1): devid==0
Причина была в тупом косяке: при вычислении адреса privrec'а из rec'а
позавчера написал "PSP_SIZEOF()
" вместо
"PSP_OFFSET_OF()
", вот оно и использовало бредовый адрес и
брало фигню в качестве devid. Исправил.
И-и-и... Заработало!!! Тестировалось со спецификацией
dev sim cac208/simkoz ~ - spd=-800000 tac=10 on_change=out_cur0:adc0 on_change=out_cur1:adc1
-- так вот, отображение делается: на histplot'е канал adc0 "плавненько"
меняется синхронно с out_cur0. В кавычках "плавненько" потому, что идёт оно
ступеньками, вследствие обновления 10каналовАЦП/сек. При изменении
количества "измеряемых" каналов размер ступенек также меняется
(уменьшается).
26.04.2024@утро, в ИЯФ, после отнесения со стенда ЛИУ в 14-м монитора в ОВС и винта Каплину, во время таскательства барахла из 1П-513 в подвал: глядя на "правила" работы ist_cdac20_drv.c, где "отображение" может быть весьма нетривиальным (например, при реверсе знак меняется; а где-то и отдача с множителем), напрашивается идея:
Конкретно:
И вот формулы позволят как отображение "один на многие" делать (что сейчас невозможно), так и коэффициенты при записи, да и вообще "контекст" иметь -- для того же реверса (как через каналы "управляемого" устройства, так и через локальные регистры).
Такое разделение даст произвольную гибкость. А в перспективе позволит и другие типы данных поддерживать.
@вечер: с другой стороны, у "разделённого" варианта есть и недостаток: при нём любые изменения каналов будут вызывать callback'и "изменилось", в то время как у "единого" драйвера-симулятора простое отражение их НЕ вызывает.
29.04.2024@дорога из Эдема на
Морской-16 за помидорками, проходя мимо "скверика" напротив Управления
Делами ~14: однако идея такого "драйвера-симулятора общего
назначения" sim_drv.c выглядит интересно, сделать его стоит (тем
более, что несложно). Сам он без каналов (или иметь один "reset"?), в
auxinfo указывается база и затем список пар КАНАЛ_ИСТОЧНИК:ДЕЙСТВИЕ; а в качестве ПРОСТЫХ целевых устройств -- если не требуется
всякое плавное изменение (@вечер: и работа регистров именно как "8+1", а
достаточно побитовости) -- можно использовать обычные ТИП/noop в
режиме суперсимуляции (когда каналы чтения становятся писуемыми)
"+type/noop
".
26.04.2024: из пунктов (c) и (d) касательно simkoz_drv
в списке-идее выше становится очевидно, как работать с регистрами в нынешнем
его варианте -- просто иметь пару uint8
, которые и читать как
8бит+1байт, и писать аналогичным образом (запись 1 бита -- модифицируем его
и возвращаем всё, запись байта -- модифицируем всё и потом возвращаем тоже
всё), т.е., всё очень просто (т.к. локально), безо всяких
rcvd
/pend
,
req_val
/req_msk
.
Ну и надо вызывать PerformReflection()
при "изменении" (в
случае АЦП -- возврате) каждого канала.
Делаем.
PerformReflection()
в
"симуляцию"-периодический возврат АЦП, сразу за собственно возвратом.
uint8 sim_ioregs[2]; // 0:Inp,1:Out
_rw_p()
.
В общем-то, по тому самому проекту, и с параметризацией.
if()
'е по номеру канала выставляются
параметры is_o
(0,1) и nb
(0-7, -1 для 8-битных).
DRVA_READ
отдельная простая ветка с
возвратом либо 1 бита, либо 8.
DRVA_WRITE
-- своя: вычисляем новое значение
8-битного регистра, сохраняем, возвращаем+отражаем сначала 8-битный канал,
затем в цикле все 8 штук 1-битных.
ReturnInt32Datum()
;PerformReflection()
; -- с
одинаковыми значениями chn,val (да, поэтому val
всегда
подготавливается отдельной строкой заранее).
"Выходной регистр" -- да, работает, комбинация 8+1 функционирует надлежаще, запись в одно отражается в другом.
Вот только проверить ОТРАЖЕНИЕ на уже готовом конфиге для ИСТа --
-- пока не получилось, т.к. собственно кода "отражения" для обычных (не-АЦП) каналов ещё нет.dev c20 cdac20/simkoz ~ - spd=-400000 tac=10 on_change=out_cur0:adc0 on_change=outrb1:inprb0 dev ist ist_cdac20 ~ - c20
Пора делать и это, тем самым де-факто доделывая последний кирпичик для полной работоспособности проекта.
29.04.2024: позавчера (п.1) вчера (п.1.абз.2) и сегодня (п.2) сделано отражение; работа состоит из 2 частей:
PerformReflection()
-- добавление в список (с
проверкой, не в списке ли уже).
Также добавлено ранее забытое "sim_props[dst_chan].val = val
"
-- складывание значения и в сам target-канал (это для потенциальных
"цепочек" отражений, вроде A->B->C, чтобы канал C брал значение пусть
не самое-последнее-от-A, но хотя бы от B).
_hbt()
в цикле прохода по списку для возврата -- отдача
значения ReturnInt32Datum()
'ом плюс отражение
PerformReflection()
'ом; причём отдаётся значение из
src_chan
.
Пара замечаний.
SIM_CHTYPE_*
, а просто безусловно стоит
PerformReflection()
сразу за ReturnInt32Datum()
.
30.04.2024: переделано, "анализ" добавлен.
Ну вроде всё. Теперь нужно проверять.
Ну-у-у, попробовал -- фиг, что-то подглючивает: при включении битика outrb1 битик inprb0 взмаргивает на мгновение, но потом гаснет.
sim_ioregs[0]
, а туда никто ничего не записывал, так что код
работы с регистрами не в курсе.
WriteOne()
в frolov_dl16 и frolov_ie4 (кстати,
запись "самому себе" есть также в драйверах dds300 и dds500, но там оно
производится "вручную").
...а вот ReturnInt32Datum()
там не только не обязателен, но
даже излишен -- он лишь избыточный "шум" создаст, т.к. значение прекрасно
будет вёрнуто кодом работы с регистром.
30.04.2024: пилим.
WriteOne()
взята из frolov_d16_drv.c с
минимальным изменением имён.
flags
в отдельный
статический массив sim_flags[]
со статическим же наполнением.
Прописывание в _init_d()
убрано.
_hbt()
в цикл прохода для возврата вставлено условие,
что при SIM_CHTYPE_W
вызывается WriteOne()
и лишь
иначе прямой ReturnInt32Datum()
.
Проверил: да, "включение" работает; а вот первое выключение -- почему-то нет. Но последующие -- сработали. WTF?!
Во-первых, запись в канал OUT_IMM (это к вопросу о "10V почему-то выставились одним скачком") делается только при переходе в INTERLOCK и CUTOFF, причём пишется 0.
Во-вторых, прописывание в собственно C20C_OUT
делается
корректно: в _rw_p()
уставка форвардится в него сразу ТОЛЬКО
лишь в состояниях IS_ON и SW_ON_UP.
Загадка...
01.05.2024: а может, просто впопыхах посмотрел не туда и поспешил с вводами? Например, после прописывания в скрине ist_cdac20 Iset=10.0 далее в скрине cdac20 вместо поля Cur посмотрел на поле Set, а что "выключение не сработало" -- ну так с 10.0 со скоростью -0.4 оно идёт вниз 25 секунд, вот и не дождался...
01.05.2024: со вчера не переставая обдумывал, какие бы ещё тесты погонять и что ещё можно сделать.
@утро, просыпание и зарядка: соображения:
SIM_CHTYPE_W
.
После мысленного перебора всех не-alphanumeric символов ASCII остановился на вопросительном знаке: '?' вроде и не используется нигде (в отличие от даже '=' и '>', могущих встречаться в TANGO), плюс мнемоника "SRC?DST" -- это как в OCCAM чтение из канала.
Делаем.
include $(LISTOFDRIVERS)
добавлена строчка LISTOFCANDRIVERS+=simkoz
-- получилось как
надо.
Кстати, предыдущая дата на файле была 04-09-2015.
SIM_CHTYPE_IGNORE
=0, в
PerformReflection()
с такими целевыми ничего не делается, а при
парсинге если указаны, то выдаётся WARNING.
sim_flags[]
добавлены каналы выходного регистра, ЦАП и
OUT_RATE, все как "_W".
23.05.2024: добавлена "поддержка" каналов *DIGCORR* и *CALB* -- чтоб ИСТы не светились бордовым (они-то рассчитаны на CDAC20, где эти каналы есть, а драйвер на основе CAC208, где их нет). Состояло из 2 частей:
_rw_p()
один большой if(chn==...||chn=...)
на эти 6 каналов, всегда возвращается val=0,rflags=0.
SUPPORTED_chans[]
скопирован соответствующий кусок из
cdac20_drv.c -- без этого отдельный кусок выше по селектору каналов
возвращал им UNSUPPORTED.
Далее решил рассмотреть вопрос "а как бы симулировать железо для ВЧ300/ВЧ400?" -- и тут вылезло интересное.
Во-первых, там (для управления 8 штуками) используется не один девайс (вроде CAC208), а связка из ДВУХ -- CANDAC16+CANADC40, и каналы для каждого источника размазаны между этой парой: уставка в DAC, измеренное в ADC, а входных и выходных регистров по 1 биту в том и другом.
Так что идея от 26-04-2024 о создании отдельного "дирижёра" sim_dir_drv была гениальной -- тут только он и годится, единый же драйвер-симулятор simkoz не справится в принципе, т.к. каналы "источник" и "отражение" расположены в РАЗНЫХ устройствах.
_CHAN_ADC_n_count
=24 и _CHAN_OUT_n_count
=8 взяты
от CAC208, но для симуляции CANADC40 и CANDAC16 их надо поднять как минимум
до 40 и 16 соответственно.
Решаем задачу "во-вторых" -- чтобы симулятор поддерживал ВСЕ имеющиеся типы ЦАПов и АЦП.
KOZDEV_CHAN_ADC_n_maxcnt
и
оно ни с чем никак не интерферирует.
KOZDEV_CHAN_OUT_n_maxcnt
, т.к. ещё
необходимы IMM-каналы, плюс небесполезны TAC-каналы, а они определены сразу
после OUT и OUT_RATE соответственно:
SIMKOZ_CHAN_OUT_IMM_n_base = KOZDEV_CHAN_OUT_n_base + SIMKOZ_CHAN_OUT_n_count, SIMKOZ_CHAN_OUT_TAC_n_base = KOZDEV_CHAN_OUT_RATE_n_base + SIMKOZ_CHAN_OUT_n_count,
ONE_LINE_PARAMS()
'ы для 8-15
добавлены.
24.05.2024: э-э-э...
"Спасает" только то, что в sw4cx/drivers/ эти IMM-каналы используются мало и для не шибко критичных вещей: только в ist_cdac20_drv.c для зануления при авариях (ну запишет 0 в OUT1, и фиг с ним) да ещё в неотносящемся к делу v5k5045_drv.c для аналогичного.
...ну и при симуляции реверсивных ИСТов пишется в out_imm4, но он
создаётся в типе sim_cdac20_4ist:cdac20
явно, специально для
этого и с указанием канала именно от CAC208 (что ТЕПЕРЬ уже некорректно).
@вечер: проверяем с обновлённым драйвером --
да, всё сломалось!!! Ведь драйвер теперь ждёт канал out_imm4 в
позиции 220 (=200+16+4), а в sim_cdac20_4ist:cdac20
он указан
как для CAC208 -- 212 (=200+8+4).
Меняем на 220, проверяем -- да, исправилось.
MAGX_cdac20_IST_SIM_AUXPARAMS
добавлено
"end=7".
И при всех прочих эмуляциях нужно будет добавлять аналогичное указание.
04.06.2024@утро, мытьё посуды: а ведь каналы "АЦП"-то у нас считаются за readonly, так что даже если в устройстве-симуляторе они будут объявлены как "w", записи не произойдёт.
(Так что свежеподготовленная за прошедшие несколько дней в
devlist_magx_macros.m4 симуляция v3h_a40d16 посредством sim_dir_drv
работать не будет -- там ведь отражения вида
"d16.out_cur0?a40.adc0
", и они просто обломятся.)
04.06.2024@~10:00+, после лабораторного круглого стола, в блоке Кузнецова: разбираемся.
simkoz_rw_p()
остался код от
CAC208: альтернатива в селекторе каналов есть, но пустая с комментарием
"Those are returned upon measurement".
PerformReflection()
--
me->sim_props[chn].val = val;
PerformReflection()
?
Но затем сообразил: отражение из АЦП-каналов выполняется в
simkoz_hbt()
, в момент "измерения"-возврата данного конкретного
канала. Так что -- всё ОК.
Проверено -- работает.
0x.06.2024:
10.04.2018: а также свой level4-списочек для отдельных HAL-модулей/директорий -- тоже делаем.
16.04.2018: да чё уж -- "udpcan" и есть, безо всяких вопросов.
10.04.2018: вроде протокола обсуждения -- тезисно.
Но засада в том, что основной софт самого Панова работает под LabView.
Есть желание попробовать интегрировать libcxsd прямо туда, и просматриваются 2 варианта:
23-09-2015 такой сценарий уже обсуждался.
В таком случае работать оно всё будет прямо на стандартном cxscheduler, но взаимодействовать с LabVIEW-based основой придётся через адаптеры с mutex'ами.
И да -- в любом случае, понадобится сделать CANHAL для того CAN-API, что используется в том контроллере.
Панову соответствующий серверочек сделать тоже тривиально.
Корректности ради стоит под идентификатор выделить даже 4 байта (чтоб помещались не только 11-битовые, но и 28-битовые, плюс всякие RTR'ы).
А можно даже и еще 4 байта зарезервировать.
И даже поле "dlc" предусмотреть.
А чтоб была возможность иметь "несколько линий" (мало ли -- для отладки), использовать порт 9000+НОМЕР_ЛИНИИ.
Например, вот почти что программный документ "Details and Protocol Specification: CAN via UDP" (Configuration, Usage and Protocol Specification, V 1.0) by Wolfgang Büscher, MKT Systemtechnik. Там о-о-очень развесистый проект, с возможностью передачи файлов и листинга директорий -- и это всё для микроконтроллеров.
Вышеупомянутый протокол мы, конечно, делать не будем -- незачем (да он и бессмысленно переусложнён).
Но конфликтов по именам всё же не хотелось бы.
В общем -- хбз.
Прямо сейчас это изготавливать не будем -- пусть идеи отлежатся, мысли улягутся; но очевидно, что реализация такой простой вещи тривиальна и займёт полчаса-час.
12.04.2018: после некоторой ругани (Панов хотел этот протокол унифицировать с совсем другими -- вроде доступа к ЦАП/АЦП) сошлись на простом формате.
13.04.2018: приступаем потихоньку.
16.04.2018@утро, дорога на работу, около ИПА и ИЦиГ: разумным выглядит расширить протокол 2 полями:
Позволит избежать необходимости вначале слать команду отправки ненужного пакета лишь для того, чтобы "сервер" узнал, кому гнать все приходящие пакеты -- путём введения команды "connect".
Аналогично можно сделать и команду "close", отправляемую клиентом при завершении работы.
Днём обсудил с Пановым; проще не делать, а то он опять стал нудить насчёт "ну во-о-от, началось расширение протокола...".
16.04.2018: сделано.
udp_can_frame_b_t
, в котором
все поля являются uint8[]
и имеют суффикс _b
.
09.06.2018: никакого реального кода тут не добавилось,
вся деятельность свелась к манипуляции уже готовыми ингредиентами
компонентами в Makefile.
Makefile
был взят от c4l-cangw/.
$(CANHAL_PFX)
.
-I
.
Итак:
Так что в принципе можно даже и убрать отдельную директорию sktcanserver/, переселив программу прямо в socketcan/.
Но пока этого делать не будем -- и чтоб не портить разделение, да и полностью самодостаточный v4sktcanserver тоже отдельную ценность имеет. Вот когда (если) опробуем на v4a3818, а ещё б "компоновка из кубиков"...
07.07.2017: делаем компилируемость.
bivme2_*()
, а уж потом, при появлении другого контроллера,
как-нибудь API заабстрагируем.
Итак:
V2HDIR
на
HW4DIR
.
Как показал анализ diff'ом, смысл изменений -- что теперь
.h-файлы из src/ не симлинкуются в сборочные директории, а
используются напрямую, при помощи SHD_INCLUDES=-I$(SRCDIR)
.
Аналогичная модификация и сделана.
MIN_ALWD_VAL
).
Несколько замечаний:
rdwr_p()
значение
val
в командах записи никогда из values[]
не
бралось -- т.е., в нём был мусор.
Возможно, КОГДА-ТО, в 2011-м, при создании этих драйверов, код был и OK, а потом, при смене API сервер<->драйверы, что-то было отредактировано некорректно. ...но по исходникам в архивах (за сентябрь 2011-го) -- всё тот же косяк есть.
Замечание: карта каналов у vdac20 "плохая" -- там канал DIGCORR_V в области "w" (да и сама карта НЕ как у прочих kozdev'ов; и даже не как у v2'шных).
14.08.2017: получен от Козака VADC16 для экспериментов -- убедиться, что IRQ работают/ловятся.
ReRun()
, в которой запись
KCMD_START
выполнялась иначе, чем в самом
init_d()
.
Конкретно там отсутствовал битик KCMD_START_FLAG_EACH_IRQ
.
А будучи =0 этот бит блокирует IRQ полностью.
Это видно по диагностике методом отладочной печати: при включенном битике
"EACH_IRQ" irq_p()
пишет, что ему приходят каналы с первого по
последний, единой пачкой, а вовсе не поштучно.
Переделано: добавлена vadc16_params[]
, с умолчательными
значениями -1, и теперь считывание из устройства делается только при <0
(т.е., при неуказанности).
Пока нет даже понимания, как именно будут организованы файлы:
То ли вытащить общую часть в .h-файл и #include'ить её, а сами
adc*_drv.c будут содержать минимальную обвязку; то ли вытащить в
общую часть ВСЁ, а в adc*_drv.c будет делаться только
#define
, указывающий "мясу" вид поведения...
Короче, сейчас делаются 3 файла:
(...Хотя и неясно, как у 1-канального поступать с дуализмом _CHAN_DATA/_CHAN_LINE0)
P.S. Насчёт потенциального vme_fastadc_common.h тоже пока неясно.
P.P.S. Кристалльно ясно только с форматом данных: поскольку он там 16-битный "с фиксированной точкой" -- для получения вольтов надо сдвинуть вправо на 12 бит (поделить на 4096) -- то напрашивается прямо в этих битах и передавать, указывая R=4096.
06.07.2017: пилим потихоньку.
Начальность заключается в отсутствии пока каналов _CUR_, а также статистических _STATS_, и даже _NUMCHANS пока неопределёнен.
ValidateParam()
,
InitParams()
-- чтобы
вычитывались начальные значения из устройства,
Делается на основе adc812me_drv.c (и, соответственно, cpci_fastadc_common.h); часть, касающаяся VME -- по vadc16_drv.c.
Пока именно каркас, т.к. даже из табличных данных не хватает содержимого
ни chinfo[]
, ни _params[]
.
07.07.2017@утро-дома: всё-таки ясно, что надо делать карты каналов достаточно различными -- у 1-канального ADC1000 будет единственная линия data (БЕЗ line0 и marker); соответственно, и исходники adc4x250_drv.c и adc1000_drv.c будут разными, просто #include'щами некий общий кусок.
Сейчас изготовим всё для более простого ADC4X250 (в нём не надо комбинировать данные из 4 мест), а потом, отладивши, лёгкой модификацией получим всё для ADC1000.
13.07.2017: драйвер допилен до компилируемого состояния.
chinfo[]
-- он
запустился.
...rrund: request to run ""
Где он берёт эту пустую строку -- хбз.
Косяк, видимо, на стороне rrund, т.к.
16.01.2018: да, косяк в
rrund.c::Run()
.
name[]
, из которого затем слепляется
path[]
=prefix,path,suffix, а печатается -- сразу после
получения! -- именно path[]
, в тот момент еще пустая.
name[]
--
ведь это его попрошено.
Ни с обычным -O2
, ни даже с -O3
.
...зато вся троица как один предупреждает
rrund.c: In function `Run': rrund.c:210: warning: variable `prefix' might be clobbered by `longjmp' or `vfork' rrund.c:211: warning: variable `suffix' might be clobbered by `longjmp' or `vfork'
Чего оно так кИдается на конкретно эту пару -- фиг знает...
14.07.2017: пилим дальше.
TrggrMeasurements()
и AbortMeasurements()
--
в соответствии с пониманием документации (в Abort еще зануление данных, как
в прочих fast-АЦП).
В соответствии с теми же пониманиями -- чуток инициализации в
InitParams()
(включая программирование IRQ) и проект
"отключения" по term().
PrepareRetBufs()
взята практически копированием от
ADC812ME (с контекстной заменой, но оно непринципиально).
Отличия только в "информационных" параметрах (пункт 1, "calc").
ReadMeasurements()
в варианте без калибровок получилась
ОЧЕНЬ простая.
Часть простоты обусловлена тем, что всегда читается чётное количество
точек, ради чего в конце буфера retdata[]
добавлена одна лишняя
ячейка.
10.08.2017: в продолжение (после до-о-олгой паузы).
14.08.2017: да, нашелся (даже два на всякий случай). Вставлен -- работает, IRQ ловятся. Так что проблема в ADC4X250 (или его документации).
10.08.2017: теперь подробнее о пилении ADC1000.
11.08.2017: канал MARKER в c061621 добавлен (везде где надо), а из ADC1000 просто не убираем.
Видимо, тоже для совместимости -- так что оставим.
11.08.2017: далее:
Но надлежащее вычитывание данных (из 4 банков) пока НЕ сделано.
31.10.2017: IRQ заработало. Типа.
Драйвер-то его, новое-непрошеное (когда нет запрошенности измерений) игнорирует, но... когда будет запрошено следующее измерение, то оно "сработает" сразу же, не дожидаясь реального измерения.
adc4x250_hbt()
вставлен
безусловный return.
10.11.2017: достигнут значительный прогресс.
Она должна была быть запаяна дейзи-цепочкой, но отсутствует.
Вот если платы в этой позиции нет (т.е., разрыв), то всё работает (так устроена VME-шина), а если плата есть, то должно быть запаяно.
Сегодня приходили Антон с автором девайса Евгением Котовым.
И, почему в софте Котова такой косяк не вылазил: у него команды на запись даёт не драйвер в контроллере, а софтина, обращающаяся к сидящей в контроллере софтинке по сети -- т.е., имеются макроскопические задержки.
СЕЙЧАС-то ADC_CMPLT не используется, но вообще бы надо. Так что -- не вариант.
SleepBySelect(10000)
(10ms) непосредственно перед START.
Помогло.
С батраковско-павленковско-котовским крейтом (который во 2-м зале, мерять с датчика потерь для Юли Мальцевой) что-то стряслось: котовская софтина бает о проблеме с VME. А диагностировать это -- как делалось на ЛИУ-2 с uspci_test -- никак!
Так что надо бы её сделать. Тем более, что и драйверы АЦП проще будет отлаживать.
09.07.2017@дома: за основу можно взять uspci_test.c.
Формат вызова --
bivme2_test [OPTIONS] DEVSPEC [COMMAND...]
где
И в роли U может быть b, s или i (q оставим про запас).
10.07.2017: делаем.
Всё срисовывается с USPCI'ного аналога максимально близко. Даже описание
операции В/В находится в типе vme_rw_params_t
, а описание
"адреса" устройства в vme_devspec_t
, и парсятся они туда
функциями ParseAddrSpec()
(убран лишь парсинг BAR'а) и
ParseDevSpec()
(сильно проще) соответственно.
11.07.2017: продолжаем.
10.08.2017: тем не менее, Гена Карпов умудрился на эту "чёткую привязку" ADDRESS_MODIFIER к ADDRESS_SIZE наплевать: у него используется modifier 0x0D (A32 supervisory data access), но адрес при этом он дешифрирует только 16-битный. И это работает! Т.е., при попытке обратиться к 32-битному адресу получается ошибка (несмотря на 0x0D), а при обращении к 16-битному пашет. Так что привязка вовсе не такая уж железная, а скорее вопрос соглашения.
ioctl(,USPCI_DO_IO,)
-- функция
do_io()
; ей параметром передаётся и address_size
.
PerformIO()
скопирована с изменением лишь в
строчке с тем do_io()
.
(В крейте 192.168.8.209 есть 3 девайса: пара ADC1000 в адресах 0x02{000000} и 0x04{000000} плюс один ADC4X250 по адресу 0x06{000000} -- вот из них читается что положено.)
-c
.
ioctl(,USPCI_ON_CLOSE,)
, то там НЕ проставлялось ни addr, ни
count (т.к. uspci.c'ная обработка при копировании в свой буфер всё
проставляет сама).
Вот и тут пришлось сделать своё проставление.
PAS_COMMA_TRM
, так что ParseAddrSpec()
воспринимал
запятую как разделитель списка значений и ругался (что массив нельзя).
-c
отсутствует как класс)).
exitproc()
тривиальна -- цикл с
do_io()
.
16.07.2017@вечер-дома-после-часового-дрыха-~19...20: идея, как
самой малой кровью реализовать поддержку IRQ в bivme2_test, не трогая при
этом bivme2_io.[ch] и не вводя там никакого API для "отдачи наверх"
номера дескриптора: надо убрать из списка линковки
$(LIBCXSCHEDULER)
, а сделать в bivme2_test.c прямо
СВОЮ реализацию sl_add_fd()
, сводящуюся к запоминанию
переданных параметров в статические переменные, с тем, чтобы потом в
WaitForIRQ()
сбагривать дескриптор
select()
'у.
Это будет, конечно, не шибко-то портабельная (в плане будущих других контроллеров) реализация, но "на сейчас" -- пойдёт.
А то шибко уж нужно, для разбирательства с неработающими (как вчера выяснилось) на батраковском экземпляре BIVME2 IRQ в ADC4X250.
17.07.2017: делаем по вчерашнему проекту.
$(LIBCXSCHEDULER)
убрано в пользу собственной "затычки"
sl_add_fd()
, просто сохраняющей все параметры и возвращающей 0.
main()
'е. Некрасиво, длинно, но
сойдёт.
WaitForIRQ()
наполнена. Отличие от USPCI'ного варианта
связано с эмуляцией cxscheduler'а и с "косвенным" (на стороне) способом
обработки IRQ.
sl_fd_cb
.
irq_p()
.
11.08.2017: кстати, о проблеме, упомянутой в заголовке: вчера ведь как раз и вылезла с батраковичевским контроллером "проблема с VME" -- что-то там, видимо, с контактом бо-бо.
И функции из libvmedirect при этом (у них внутри "status &
VMEIF_REG2_BERR") возвращают -1
, в errno
ничего не прописывая.
Посему -- убираем печать strerror(errno), вместо него теперь просто строка "VME error".
14.10.2017: какие-то странности с адресацией: Попытка ручного обращения -- bivme2test'ом -- к девайсу с адресом 0x8E000000 обламывается:
в то же время драйвер adc4x250 с девайсом 0x8E как-то работает.# /tmp/HOST/oper/4pult/lib/server/drivers/bivme2_drvlets/bivme2_test -x 32:0x09:0x8e000000 i:0x014 /tmp/HOST/oper/4pult/lib/server/drivers/bivme2_drvlets/bivme2_test: IO("i:0x014"): VME error
15.10.2017: оказалось -- глюк в (ppc'шном?)
strtol()
: в ответ на 0x8e000000 он возвращает 7fffffff. Он,
похоже, почему-то возвращает оное для всего, что больше 0x7fffffff --
начиная c 0x80000000. Замена strtol()
на
strtoul()
исправила ситуацию.
Почему так -- понятно: судя по man'у, это и есть правильное поведение -- при невлезании в диапазон вернуть LONG_MIN или LONG_MAX и errno=ERANGE.
16.10.2017: встаёт вопрос более глобального плана: что
делать с strtol()
, ведь проблема может быть довольно общей?
Например, указание адресов в devlist'е, указание чисел в cdaclient, ...
strtoul()
.
Вот если бы любая из них просто парсила и не выдрёпывалась!!!
А вот на 32-битной -- есть... Ровно как на PowerPC:
strtol("0x80000000",NULL,0) даёт ERANGE. И -- да, там
strtoul()
спасает ситуацию, преспокойно парся и большие числа,
и отрицательные (вот веселуха-то!!!).
При выявлении же проблем аккуратненько правим эти самые проблемные места.
strtoll()
ровно та же проблема с "большими"
64-битными числами, что и у strtol()
: вместо верного результата
возвращается 0x7fffffffffffffff.
0x7fffffff
или 0x7fffffffffffffff
, то
просто отбрасывания старших битов.
Тестик work/tests/test_strtol_strtoul.c дополнен 64-битностью и вот результат:
strtol(): 0xffffffff strtoul(): 0x00000000 strtoll(): 0x7fffffffffffffff strtoull(): 0x8000000000000000 x10sae:~/work/tests% ./test_strtol_strtoul_64 0x8765432187654321 strtol(): 0xffffffff strtoul(): 0x87654321 strtoll(): 0x7fffffffffffffff strtoull(): 0x8765432187654321 x10sae:~/work/tests% ./test_strtol_strtoul_32 0x8000000000000000 strtol(): 0x7fffffff strtoul(): 0xffffffff strtoll(): 0x7fffffffffffffff strtoull(): 0x8000000000000000 x10sae:~/work/tests% ./test_strtol_strtoul_32 0x8765432187654321 strtol(): 0x7fffffff strtoul(): 0xffffffff strtoll(): 0x7fffffffffffffff strtoull(): 0x8765432187654321
strtoull()
спасает ситуацию.
(Поэтому конкретно в cda_d_vcas.c и console_cda_util.c
произведены замены, в дополнение к некоторому числу замен
strtol()
на strtoul()
; но на тему
ERANGE
проверок так никуда и не вставлено...)
08.02.2021: ага-ага, НАДО всё же править потенциально
проблемные места, уже актуально. Видно же, что как минимум значения в
bivme_test_common.c парсятся strtol()
'ем, что
нехорошо.
11.01.2022: да-да, вот сегодня ровно этот косяк и
вылез -- что значения в bivme_test_common.c парсятся
strtol()
'ем! Из-за этого попытка записи 0x80000000 (RESET) в регистр
0x000104 (CTRL) обламывалась -- писалось 0x7fffffff.
Вот какого чёрта я тогда это не исправил, а?!
15.02.2019: непосредственная причина (побудительный мотив) -- что надо готовить поддержку для CAEN'овского адаптера A3818, а в перспективе и иных (как локальных адаптеров, так и кросс-поддержку для иных платформ (как сейчас сделана для BIVME2).
Сложностей/неопределённости доставляет вопрос о предполагаемой архитектуре работы VME-драйверов, которую потенциально можно реализовывать двумя способами:
Что выбрать?
Потому, что в BIVME2 драйверы работают отдельными процессами, а не в рамках одного процесса-сервера.
Потому, что локальных PCIe-адаптеров нужно именно так.
Вывод: надо делать layer'ами.
Пара замечаний:
Итак, предлагаемая структура:
15.02.2019: если НЕСКОЛЬКО vme_io библиотек одновременно -- как они будут жить в общем адресном пространстве сервера? Не перекроются ли по именам символов?
Если по-простому -- то перекроются. Для НЕперекрытия придётся линковать каждую к своему layer'у так, чтобы публичные символы наружу не торчали; а это хбз как сделать и хбз возможно ли вообще.
15.02.2019@лыжи-конец-5-ки: можно сделать как в cankoz - реально
делать vme_io_hal'ы просто .h-файлами, #include'имыми в юзеров
(vme_io_lyr_common.c и vmetest_common.c) по именованому
указанию из Makefile'ов, а уж внутри предоставляющими стандартные имена --
прямо vme_io_*()
.
15.02.2019@лыжи-середина-"перешеек"-2-ки: кстати, а символы
VME_OP_{NONE,READ,WRITE}
как раз прекрасно пойдут в общий
vme_io.h.
15.02.2019: но содержимое как минимум bivme2_io.с шибко сложно для .h-файла: там кроме просто функций есть также менеджмент дескрипторов (и сохранение address_modifier'ов).
15.02.2019: ну можно эти *vme_io_hal.h'и сделать списками #define-alias'ов, вида
#define vme_io_open bivme2_io_open
Тогда останутся именно ОТДЕЛЬНЫЕ И ПОЛНОЦЕННЫЕ *_vme_io.c.
16.02.2019@дома: строго говоря, имеющаяся СЕЙЧАС сложность bivme2_io.с избыточна: этот "менеджмент дескрипторов" по-хорошему должен быть прерогативой layer'а, а HAL-модуль пусть будет только HAL-модулем -- т.е., просто предоставляет именно доступ к VME.
И упростить бы API этих HAL-модулей относительно address modifier'ов -- чтоб они указывались прямо непосредственно операциям чтения/записи.
18.02.2019: поизучал нынешнюю реализацию bivme2_io.с -- мда-а-а...
Это явно была просто экономия времени: чтобы не заморачиваться менеджментом в условиях, когда дескриптор всегда нужен ровно 1. А при В/В уже идёт корректное обращение к структуре-описателю по сдвигу handle, как и должно.
Так что при переходе на layer'ность исправилось бы быстро.
непосредственно вv = am; r = libvme_ctl(VME_AM_W, &v);
bivme2_io_open()
.
Т.е., там "указание address modifier'а при регистрации устройства" сделано не только по идеологическим соображениям, но и просто не от хорошей жизни -- принудительно.
Для возможности же указывать в КАЖДОЙ операции В/В придётся либо в каждой
такой операции выполнять тот libvme_ctl(VME_AM_W,)
, либо как-то
помнить последний уставленный и вызывать уставление при несовпадении.
...либо изучить исходники libvmedirect (если удастся их найти) и убедиться,
что там эта операция дёшева.
Ну-с, что делать будем, каким путём пойдём?
Видимо, надо изучить ещё API и CAEN'овских библиотеки и драйвера -- тогда будут дополнительные основания для принятия решения.
Просто потому, что в libvmedirect оно по факту ТОЛЬКО так (ПРОВЕРИТЬ CAEN!!! 21.02.2019: проверил; там ТОЛЬКО ИНАЧЕ (с каждой операцией указывается).).
18.02.2019: отдельный вопрос -- как с IRQ обращаться.
Ведь vme_io_open() должен бы вернуть что-нибудь, годное для помещения в
select()
...
19.02.2019: конкретно для BIVME2 -- используется char-устройство /dev/vmeiN, дескриптор на которое становится готовым на EX, после чего оттуда надо считать int32-вектор.
20.02.2019@дома: во-о-от, надо считать вектор и сравнить его с требуемым. Откуда следует, что оный вектор надо где-то хранить; как и указатель на клиентов callback, кстати.
Какие варианты решения?
Второй вариант мне определённо нравится больше.
Кстати, указывается он в API CAENVMElib, а ДРАЙВЕРУ поступают уже готовые "пакеты" для обмена с линией.
Есть только синхронная:
CAENVME_IRQCheck()
-- проверить наличие IRQ (возвращает
битовую маску взведённых линий).
CAENVME_IRQWait()
-- ждать появления IRQ на указанных
(маской) линиях в течение указанного времени.
А даже возможности получить номер файлового дескриптора, используемого внутри CAENVMElib'а -- нельзя, нету такой функции.
И самое странное -- что в драйвере нету метода poll()
, но
присутствуют вызовы wake_up_interruptible()
, вроде бы
используемые как раз для сигнализирования клиентам о готовности. Причём
используются с ДВУМЯ экземплярами wait_queue_head_t
:
read_wait[][]
-- для ожидания/пробуждения при
блокирующемся чтении.
intr_wait[][]
-- для ioctl'а
IOCTL_IRQ_WAIT
, т.е., ровно для того, что должно бы
реализовываться через poll()
.
Т.е., там ПОЧТИ всё сделано, просто сделано "не так".
Обсуждение "что делать":
Можно как-то выдрёпываться с передачей "AM<0 -- значит, не уставлять". Но не спасёт.
Видимо придётся как-то на уровне HAL-include-файлов адаптироваться...
Как решать проблему?
pipe()
'а
на каждое устройство:
CAENVME_IRQWait()
,
Отдельный вопрос: а как владельцу "пишущей" части pipe'а определять закрытость читающей стороны? Ведь обычный подход -- "EOF, который показывается готовностью на чтение и результатом чтения равным 0 байт" тут не катит, т.к. пишущий конец должен быть невалиден для чтения. Надо б тестик запилить...
readfds
?
И там даётся ответ: "Oddly enough, it appears that when the last reader closes the pipe, select indicates that the pipe is readable".
read()
возвращает -1/EPIPE.
Проверялось на CentOS-7.3.1611, ядро 3.10.0-514.el7.x86_64.
Ответ: они используют "стандартную VME'шную поддержку в ядре Linux". Она вроде как уже несколько лет есть для какого-то MVME5xxx, а Котов по аналогии запилил для MVME3100.
Обещал прислать ссылку.
22.02.2019: прислал. Правда, там на вид всё -- только для программирования драйверов в ядре, а про userspace как-то глухо.
05.11.2019: начальные работы в nsrc/:
"Модификация" в основном свелась к удалению всего лишнего -- команд-то специфических для VME пока что нету (ну не прямой же доступ к VME-шине так дистанционно организовывать).
Так что файл сократился почти до пустого шаблона, если бы не следующий пункт.
vmeserver_clninfo()
, для
которой даже в "vme_hal" даже был создан метод get_dev_info()
.
Она должна выдавать адресную информацию в формате
AM:BASEADDR/SPC_SIZE I:VEC
(тут "I" -- номер IRQ, 0-7; если irq_n<0, то будет -).
Она занимает бОльшую часть файла.
27.11.2019: проверил -- да, работает, в telnet-консоль информация выдаётся.
06.11.2019: только есть проблема с упихиванием всех драйверов в vmeserver: становится невозможно делать СВОИ драйверы ОТДЕЛЬНО от директории hw4cx/vme/, как это сейчас практикует Роговский.
И что -- практиковать свои локальные сборки, добавляя к обычному списку драйверов "свои"? Крайне неудобно...
06.11.2019@вечер, дорога
домой: а ведь у нас современные платформы (BIVME2, MVME3100, MOXA)
вполне позволяют ДИНАМИЧЕСКУЮ загрузку, через dlopen()
!
MAY_USE_DLOPEN
.
Только надо будет как-нибудь это сочетнуть с тем, что сейчас первым опциональным параметром является номер порта (переделать на "-p"?).
dlclose()
.
Смысл -- чтобы можно было обновлять драйверы без необходимости рестарта remsrv (как это сейчас можно делать с обычными .drvlet'ами).
07.11.2019: и даже больше: а не ввести ли в remsrv --
ExecConsCommand()
-- команду "выгрузить всё", т.е., грохнуть
все экземпляры dlopen()'нутых драйверов? Или и вовсе "реинициализация" --
грохнуть ВСЕ устройства (это приведёт и к выгрузке)?
DirRules.mk не очень катит -- DriverRules.mk? Или всё-таки катит?
А sktcanserver так и вовсе спокойно может загружать обычные серверовы драйверы -- он же предоставляет обычную среду исполнения.
15.12.2021: "переходный период" закончился, директории переименованы: старые bivme2/ и src/ стали old-bivme2/ и old-src/, а mbivme2/ и nsrc/ превратились в bivme2/ и src/.
Все Makefile'ы и *.mk подправлены.
Засим и разделу-то уже пора ставить "done" -- что и сделано.
Вывод: даже для BIVME2 просто ПРИДЁТСЯ работать через layer, чтобы он был единственным "ловцом IRQ" и распределял бы их по драйверам в зависимости от векторов (менеджмент аналогично CAN'овским kid'ам).
Собственно, работа состоит из 2 частей:
Сейчас на BIVME2 открывшему линию /dev/vmeiN выдаются ВСЕ вектора с этой линии.
А как МОЖНО было бы, покрасивше:
Вроде как что-то подобное Паша Селиванов реализовал для can4linux на мамкинских контроллерах.
ioctl()
; тогда
конкретные вектора будут отдаваться конкретным файлодескриптородержателям.
Т.е., нечто похожее на технологию Шичкова, но только не для ядра, а для userspace.
И ещё более это аналогично подходу, используемому в USPCI -- этакий "USVME"? :-) Где бонусом является автоматическое освобождение ресурса при закрытии дескриптора.
Вариант (a) наводит на мысль посмотреть, как это сделано у Селиванова. Спросить его мылом, что ли...
25.12.2019: насчёт "как это сделано у Селиванова" -- и спрашивать не понадобилось.
и "Под каждый канал выделяется память для rxfifo"./* number of processes allowed to open a CAN channel */ #ifndef CAN_MAX_OPEN # define CAN_MAX_OPEN 1 #endif
Т.е., он просто при чтении мультиплексирует данные во всех клиентов.
Так что при желании можно рыться, изучать.
static uspci_board_t boards[NUM_BOARDS];
и при открытии клиенту выделяется ячейка в этом массиве (а уж в ячейке есть
место на всё).
Только тут массивов несколько раздельных размером [CAN_MAX_OPEN], а при
каждом open() kmalloc()'ируется структурка типа _instance_data
,
содержащая лишь
int rx_index
(индекс, [0..CAN_MAX_OPEN-1]) да
wait_queue_head_t CanRxWait
(это должно было быть для poll'а,
но не используется, поскольку есть глобальный массив (их там толпа...)).
Да, USPCI'ный подход выглядит красивше :-).
Сменить идеологию с "вычитать 1 вектор, обработать, потом вычитывать и обрабатывать следующие" на "вычитать (в буфер) до 100 векторов, а потом идти по этому буферу и обрабатывать".
01.11.2021: посмотрел в обеих текущих HAL-реализациях
(BIVME2 и A3818) -- да, так сделать МОЖНО. Неудобство только в том, что в
обоих случаях вычитывание каждого экземпляра вектора будет
индивидуальным/штучным (в BIVME2 -- read()
пока не настанет
ошибка (хотя тут-то МОЖНО было позволять вычитывание "массива" векторов -- у
драйвера они в FIFO хранятся), в A3818 -- CAENVME_IACKCycle()
также до ошибки (и тут уж без вариантов)).
02.11.2021: в a3818_hal.c реализовал (внутри
очередного #if
'а) -- работает, обрабатываются прерывания ото
всех устройств.
(Правда, паттерн срабатывания несколько неожиданный, но это уж вопрос для отдельного анализа.)
22.02.2019@институтская-сессия: на данный момент содержит 3 вещи:
VME_OP_*
и VME_COND_*
,
переехавшие (как давно (с 14-07-2017) хотелось) из bivme2_test.c в
общие определения.
vme_io_open()
. Ей передаются и
space_size
, и am
.
zzz
.
Потому как неясно, ЧТО там должно быть: файловый дескриптор, какой-то ещё дескриптор, или что?
am
.
Предполагается, что в HAL'ах, где уставка при открытии (как BIVME2/libvmedirect) этот параметр будет игнорироваться, а передавать его в любом случае будет layer, сохранив при открытии у себя.
05.11.2019: очень-очень похоже, что концепция "vme_io" уйдёт, уступив место "vme_hal".
05.11.2019: оно полувиртуально и эфемерно, но всё же уже появилось в виде конкретного файла vmehal.h (НЕ "vme_hal.h", поскольку полностью аналогично canhal.h).
VMEHAL_DECLARE_IO()
-- он парный к
bivme2hal.h'еву VMEHAL_DEFINE_IO()
.
#define
-именем
VMEHAL_STORAGE_CLASS
, которому при не-определённости
присваивается значение "static
".
VME_OP_*
с VME_COND_*
.
14.11.2019: доделано (на нынешнем уровне понимания):
vmehal_irq_cb_t
-- прототип callback'ов по
приходу IRQ.
vmehal_open_irq()
теперь принимает 3 параметра:
irq_n
-- номер.
cb
-- callback, который вызвать.
uniq
-- что передать cxscheduler'у при регистрации
файлового дескриптора (дабы, если что, мог бы сделаться cleanup).
Возвращает int
-результат -1 при ошибке либо 0 при успехе.
vmehal_close_irq()
принимает единственный
параметр -- irq_n
.
Она формально int
, но это "на всякий случай".
Замечания-комментарии:
Возможно, для прочих HAL'ов (a3818hal.h?) и, тем более, для vme_test_common.c понадобятся ещё модификации интерфейса.
31.12.2019: некоторые мысли на тему общего интерфейса, с учётом специфики A3818:
Для этого можно предусмотреть в API специальные вызовы, которые использовать исключительно в специальных "драйверах", не привязанных ни к каким устройствам, а общающиеся лишь с самим адаптером через те вызовы.
02.01.2020@дома:
И что теперь -- ДВА числа префиксов делать?
Или как-то махинировать, для разных HAL-модулей указывая разное число businfo-параметров? Но это крайне неудобно: ведь именно драйверы выполняют сдвиг jumpers->base_addr, так что просто передавать lyr'у сразу весь businfo[] не прокатит (ну не указывать же отдельным параметром, что компонент "адрес" - это реально jumpers, которые надо сдвинуть влево на столько-то...).
03.01.2020@дома:
И чтоб конкретные "варианты" могли бы указывать, какие части этого префикса осмысленны, дабы его не было, например, в BIVME2.
В т.ч., чтоб в help'е (по -h) не печаталось бы.
04.01.2020@дома: хотя, возможно, для общности стоит сделать наоборот -- чтобы ВСЕГДА можно было указывать эти префиксы.
И, кстати, если файл vme_test_common.c будет просто симлинковаться в
конкретные *_test, то "указывать, какие части этого префикса осмысленны"
будет просто некому. Возложить это на Makefile'ы, через
SPECIFIC_DEFINES=-DVMEBUSSPEC_PARTS_USABLE=n
, где n=0,1,2 (0 -
не нужно вовсе, 2 - обе части).
04.01.2020@дома:
05.01.2020@дома:
_
'...
06.01.2020@дома:
vme_hal_open_bus()
и
vme_hal_close_bus()
.
vme_hal_open_irq()
/vme_hal_close_irq()
также добавлен первым параметром bus_handle
.
Т.е., видимо, линии нумеруются всквозную по всем адаптерам в системе (как в CAN-адаптерах), а уж на каждой линии там может сидеть до скольки-то VME-контроллеров и прочих карт.
В результате: пара bus_card,bus_line переименована в безликие bus_major,bus_minor (как и предполагалось изначально).
VME_HAL_MAX_BUS_MAJOR
=0,
VME_HAL_MAX_BUS_MINOR
=0.
07.01.2020@дома:
И это тоже добавлено.
Идёт со скрипом: вроде поштучно все аспекты понятны, и все требующиеся "компоненты" где-то уже использовались, но полного понимания ("огроканности во всей полноте") пока нет. Поштучно-то ясно, но не вылезет ли при их сочетании какая-нибудь ранее не встречавшаяся проблема?
Например, принцип кодирования handle'ов и как организовывать массив со
"свойствами линий": как [VME_HAL_MAX_BUS_MAJOR+1][VME_HAL_MAX_BUS_MINOR+1]
или же линейный с последовательным заниманием ячеек? В первом случае -
можно будет взять encode_handle()
/decode_handle()
из cankoz_lyr_common.c; но пока более правильным (с точки зрения
оптимизации) представляется второй вариант -- линейный.
09.01.2020: движемся дальше -- реально переходим на «файлы с _'», заодно уже с новым API.
VMELYR
" на "VME_LYR
", возник конфликт имён -- ДВА
РАЗНЫХ имени "VME_LYR_NAME
".
Выбрано очевидное решение:
VME_LYR_API_NAME
-- тем
более, что соответствующее поле в LayerModRec'е как раз и называется
api_name
.
VME_LYR_VERSION*
переименованы в
VME_LYR_API_VERSION*
.
И то же самое нужно будет проделать со всеми остальными layer'ными инфраструктурами (CANKOZ, PCI4624, PIV485). 10.01.2020: сделано, со всей троицей.
QQQ_HANDLE
.
VmeAddDevice()
забыл добавить параметры bus_major,bus_minor,
вместе с их использованием в драйверах!!!
10.01.2020: сделал, и то, и другое, и третье.
10.01.2020@утро, дорога через студгородок к родителям: пара мыслей насчёт организации "массивов описателей шин":
Да, поиск по массиву -- несколько более затратная операция, чем просто декодирование handle'а. Но этот поиск будет очень редкой операцией -- только при регистрации нового устройства.
Размер этого массива (количество ячеек): для BIVME2 и MVME3100 -- очевидно, 1; для A3818 -- от балды (10 обычно хватит).
Это касается И vme_lyr_common'а, И a3818_hal'а.
Следствия:
VME_HAL_MAX_BUS_MAJOR
и
VME_HAL_MAX_BUS_MINOR
нафиг не нужны.
13.01.2020@пультовая,
планёрка: а не сделать ли в дополнение к параметру «сколько
ячеек нужно в таблице "свойства линий"» также и булевский
#define
-параметр "использовать ли шины вообще"? Чисто для
оптимизации, чтобы дохленьким BIVME2/MVME3100 не приходилось бы тратить
процессорное время на лишние действия, а можно б было отключать этот код
прямо #if
'ами.
Впрочем, в layer'е работа с шинами по большому счёту только в момент регистрации устройства, а там потерями можно пренебречь.
Но посмотрим -- если что-то будет и при основной работе по В/В, и
окажется легко закрыть это "что-то" в #if
-блоки, то сделаем.
13.01.2020@вечер, дорога домой: а ведь по факту предполагаемый для vme_test'а параметр "сколько компонентов адреса шины осмысленно" -- это и есть тот самый "флаг": если значение того параметра ==0 -- то НЕ имеет смысла "использовать шины", а если >0 (==1 или ==2) -- то имеет.
Назвать этот параметр BUS_COMPONENTS_SENSIBLE
.
13.01.2020@вечер, дома:
да, начальное использование BUS_COMPONENTS_SENSIBLE
в
vme_test_common.c добавлено -- при !=0 в help в описание DEVSPEC'а
включается префикс "[@BUS_MJR[/BUS_MNR]:]".
20.01.2020: возникли некоторые сомнения -- а В ТО ли место DEVSPEC'а добавлен опциональный префикс?
С точки зрения парсинга -- да, в самом начале проще всего.
Но, с другой стороны, так мы разрываем адрес шины и адрес устройства, запихивая между ними ADDRESS_SIZE:ADDRESS_MODIFIER. А красивше было бы
ADDRESS_SIZE:ADDRESS_MODIFIER[@BUS_MJR[/BUS_MNR]]:BASE_ADDR
...да, парсить неудобнее, зато всё логично. Да и простой заменой разделителей можно транслировать devlist'ные businfo[]-спецификации в DEVSPEC'ы.
После обеда, проведя много тестов с a3818_test, где надо было указывать префикс шины: неа, логика логикой, но с НЫНЕШНИМ префиксом всё же удобнее -- так что пусть остаётся как есть.
20.01.2020: BUS_COMPONENTS_SENSIBLE
переименована в VME_BUS_COMPONENTS_SENSIBLE
, для правильности.
20.01.2020: о диагностике: возникло желание научиться
получать от HAL'а внятные описания ошибок. Тем более, что в CAENVMElib'е
есть CAENVME_DecodeError()
, возвращающая текстовое описание для
указанного cv*Error
.
Но есть проблема -- у нас ДВА уровня, могущих возвращать ошибки:
errno
.
Т.е., иногда имеет смысл смотреть на одно, иногда -- на другое, а иногда -- в функциях открытия шины и/или прерывания -- на оба (от HAL'а ошибки вроде "уже используется" или "закончилось место в таблице", а от библиотеки всякие "ошибка открытия линии").
Ну и как же поступить?
vme_hal_strerror()
.
Делаем:
vme_hal_strerror()
добавлена в vme_hal.h, в
a3818_hal.h она переадресовывается к
CAENVME_DecodeError()
, а в bivme2_hal.h просто сама
делает
return errcode == 0? "NO_ERROR" : "JUST_AN_ERROR";
errno=0
.
errno != 0? strerror(errno) : vme_hal_strerror(r)
PerformIO()
ВСЕГДА использует
vme_hal_strerror(r)
, поскольку HAL-методы В/В никогда не делают
errno=0 (тут уж вопрос скорости).
23.11.2021: прикол в том, что оно там само делает
errno=0
прямо перед вызовом do_io()
.
Проверил -- да, всё работает. Хотя особого счастья это пока не дало, поскольку CAENVMElib'овские сообщения весьма скупы.
Надо бы им добавить __attribute__((unused))
.
22.01.2020: попробовал добавить это прямо в *_hal.h -- получил ругань...
Ага, ведь gcc позволяет указывать атрибуты-функций только ПРОТОТИПАМ,
из-за чего иногда приходится даже локальным-для-файла функциям прототип
прямо перед самим определениям функции -- в основном это всякие, связанные с
printf-style-форматами, где нужен __attribute__((format ...))
-- cxlib_report()
, разные reportproblem()
и
reporterror()
, cda_fla'шная DO_ERR()
.
22.01.2020@вечер, дорога домой: ёлки-палки -- так у нас же ЕСТЬ отдельные прототипы, как раз в vme_hal.h!
Ну да, раньше они были чисто для порядку -- чтобы HAL-файлы не дай бог не определили бы неправильно (тогда компилятор отловит косяк и ругнётся).
Ну а теперь будут необходимы.
22.01.2020@дома, ~20:00: да, сделал -- всё прекрасно!
vme_hal_term()
-- для a3818, который
регистрирует периодический таймаут (для поллинга прерываний), чтобы была
возможность этот таймаут подчистить.
Заодно и для bivme2_hal.h оказалось полезно -- вызывать
libvme_close()
.
vme_hal_init()
принимал параметр
uniq
: оно нужно и для a3818_hal.h (периодический
таймаут помечать), и для bivme2_hal.h (файловые дескрипторы к
/dev/vmeiN).
31.01.2020: делаем.
sl_enq_tout_after()
, а дальше уже сама
a3818_heartbeat_proc()
повторяет в заказе то же значение.
vme_hal_open_irq()
УЖЕ есть параметр uniq
, он и
используется; а layer'ов vme_add()
передаёт туда
my_lyrid
.
Т.е., как бы всё УЖЕ работает, в принципе корректно, но есть сомнения в элегантности/сбалансированности такой модели.
Окей -- оно компилируется; а поскольку выгрузки layer'ов пока не предусмотрено (НИГДЕ, включая remsrv), то проверить всё равно не удастся, так что сразу ставим "done".
18.02.2020: всё-таки чуть сложнее с тем, куда всё-таки
НАДО передавать uniq
, а куда НЕ надо: при реализации варианта
ловли прерываний от A3818 через отдельный thread (да, пусть оно всё равно не
взлетело) оказалось, что uniq
нужен также и в
vme_hal_open_bus()
-- там оно требуется при регистрации
дескриптора.
Отсюда напрашивается вывод: надо бы передавать ТОЛЬКО в
vme_hal_init()
, а уж оно пусть складывает куда-нибудь в свою
глобальную переменную (с именем вроде NNN_hal_uniq).
...и да: предполагается, что у всех экземпляров шин будет один и тот же uniq -- ==lyrid.
CAENVME_BLTReadAsync()
, запускающая чтение и отваливающая, а
затем надо вызвать "завершатора" CAENVME_BLTReadWait()
.
Да, в нынешнем виде оно криво, но в принципе асинхронная работа возможна.
Вывод: надо обдумывать какое-то расширение API для таких продолжительных синхронных операций -- как на уровне HAL, так и на уровне LYR.
05.02.2020: конкретно с A3818 проблема заключается в том, что BLTReadAsync() запрещает ЛЮБУЮ работу с этим же крейтом --
Take care to call the CAENVME_BLTReadWait function before any other call to a CAENVMElib function with the same handle.
Даже если забыть про отсутствие уведомлений (а только возможность
ожидания), то получается, что с этим CAENVMElib_handle
более
вообще НИЧЕГО делать нельзя; а у нас через него работают ВСЕ устройства этой
же шины (крейта).
Что делать? Вводить-таки разные handle для каждого устройства? Это получится радикальная перекройка vmehal-API; а стоит ли того преследуемая цель?
06.02.2020: в продолжение темы:
Так что уже не один контроллер подобное позволяет -- точно нужна общая API-модель.
И тогда окажется, что запущена -- через ядро! -- операция, трогающая память, которая уже освобождена. И-и-и -- как быть в такой ситуации?
@вечер ~16:30: спросил у Павленко, как у них с этим на MVME3100 -- а они на эту тему даже не задумывались, так что хбз.
@вечер, дорога из ИЯФа к УД СО РАН ~17:30: в CX-то формально всё в наших лапах: поскольку драйверов privrec освобождается сервером, то, в принципе, можно ввести какой-нибудь серверный вызов "не высвобождай драйверов privrec по завершению драйвера", а выполнять таковое высвобождение самостоятельно (помещая все эти драйверы в отдельный список). Вопрос только, кто и когда будет этими заклинаниями заниматься.
07.02.2020: в принципе, сервер (и
cxsd_hw, и remsrv_drvmgr) высвобождает privrec ПОСЛЕ вызова layer'ова
disconnect()
; так что можно "оптимизировать" -- запрашивать у
сервера не-высвобождение только для тех, у кого в текущий момент висит
ИСПОЛНЯЕМЫЙ запрос на асинхронное чтение, заодно помечая запрос в очереди
как "устройство отправилось к праотцам", и по завершению операции вместо
уведомления драйвера просто выполнять освобождение.
Всякие разрозненные мысли по теме:
ReadMeasurements()
, выполнялась бы в
closure-callback'е векторного чтения, и оттуда же вызывалось бы drdy(), а
ReadMeasurements()
станет пустышкой-заглушкой.
Да, у нас подобная модель уже используется в ottcam_drv.c.
0
при успешном запуске и >0
при уже завершённости операции (как на BIVME2, где никакой асинхронности
быть не может).
add()
добавить ещё
параметр "flags"/"options", где бы указывать всякие особенности работы, в
т.ч. необходимость заранее зарегистрировать ОТДЕЛЬНЫЙ bus-handle
...и гонять эти операции, очевидно, придётся в отдельном thread'е, а модификацию очереди окружать mutex'ом.
socketpair()
(2 дескриптора).
Дополнительный плюс этого подхода в том, что одновременно получаем механизм "слежения" за умиранием собеседника. Он бы как бы и так бы был, но проверка готовности на чтение обычного сокета выглядит всё же менее шизоидно, чем проверка пишущей стороны pipe'а.
09.02.2020@утро-душ: выкристаллизовался в голове проект, как это надо делать (тут много специфики A3818, что не совсем к месту в разделе о vme_hal, но что уж поделаешь):
18.02.2020: фиг, не будет это работать: как сегодня проверено на примере попытки ловить прерывания отдельным thread'ом, CAENVMElib просто не позволяет открыть ту же шину второй раз.
options
специальный флаг.
Ну и прочий менеджмент очереди -- как описано выше за 06-02-2020.
И делать этот вызов -- запрета высвобождения -- в disconnect()'е, в случае, если грохается драйвер, чья асинхронная операция сейчас в исполнении (т.е., находится в голове очереди).
18.02.2020: неа, не так: не нужно ничего лишнего от сервера, а надо просто буфера для async-чтение держать у layer'а -- чтобы он их для драйвера аллокировал, и он же "отложенно освобождал" когда станет можно (см. в разделе о cxsd_hw за сегодня).
v_async
".
...только ли для чтения, или же и для записи тоже -- вопрос.
10.02.2020@вечер-дома: подумал-подумал -- неа, не стоит это всё делать. Резон -- шибко много проблем:
Конечно, она точно может быть сделана -- принципиальных проблем там не видно.
Но это будет очень трудоёмко, сложность получившегося кода (а это затронет ВСЕ уровни -- HAL, lyr, драйверы) будет очень большой, так что из сравнительно простой и прямолинейной реализации получится весьма навороченная, трудная для восприятия и плохо поддающаяся дальнейшим модификациям (поскольку вот это добавление асинхронности весь резерв модифицируемости и сожрёт).
А выигрыш от всех этих сложностей будет не очень великим: некоторое повышение производительности.
Вывод: игра не стоит свеч.
(А то пока считывается пачка осциллограмм, проходит много времени и автоподставляемые метки времени на последних уже сильно не соответствуют реальности.)
Такая мера позволит более корректно сопоставить timestamp прерываниям: иначе -- при последовательном "вычитали, вызвали" -- нет гарантий, что очередное обрабатываемое прерывание возникло в тот же начальный момент, что и предыдущие (оно могло появиться позже).
Да, у такого варианта тоже есть недостатки:
Впрочем, при работе по общему внешнему запуску эта проблема вряд ли проявится: все осциллографы должны отработать достаточно синхронно быстро, так что пока компьютер успеет среагировать и начать вычитывать вектора, сгенерят их уже все.
bivme2_hal_irq_cb()
с поштучного
вычитывания на "вычитать 100 штук в массив" -- мелочь.
И есть шанс, что нынешнее "ближние к контроллеру успевают всегда, дальние реже, причём частенько читаются как бы следующим циклом" сменится на просто какую-нибудь иную неравномерность.
Вероятно, надо в remdrv'шные "опции" (вроде ж планировались такие -- парсимые самим remdrv для своего использования?) добавить флаг "игнорировать timestamp'ы от драйвлетов".
11.03.2020: собственно исходные письма, демонстрирующие последовательность соображений и размышлений:
4) Нужно каким-то образом реализовать "синхронный режим" измерения, т.е. чтоб при необходимости было полное измерениекартины за один выстрел, т.е. Сейчас один некоторые осциллографы неуспевают больше, некоторые меньше, и уже корреляций меджу разными картинами наводить не получится.
а. Не хватает производительности контроллера. А если CAEN? б. А по timestamp'ам наводить соответствие не получится? Кто-то чего-то не успевает, но пропуски ж должны быть равномерными, так что каждый второй-третий импульс должны б успевать все.
может и можно, а еслислучайные пропуски будут такие, что запуски одного канала взаимоисключатся с другим, то как я общую картину собирать буду?
Вряд ли - чисто по сути появления пропусков: самый "бесправный" - это самый дальний от контроллера осциллограф, и если уж он успел сработать, то и все предыдущие тоже должны были успеть.
Я тут подумал - неа, неверны обе идеи: ни timestamp'ами пользоваться нельзя, ни "статистически ловить, когда все сработают". timestamp'ами нельзя потому, что они дают время НЕ срабатывания, а ВЫЧИТЫВАНИЯ - которое при неуспевающести контроллера будет от времени срабатывания кардинально отличаться (вплоть до другого цикла запуска и даже больше). А статистика не покатит потому, что осциллографы опять же НЕ срабатывать не успевают, а ВЫЧИТЫВАТЬСЯ -- IRQ-то они выставляют, но контроллер не успевает их вычитывать, а доходит до них аж в следующем цикле (пока более близкие к нему и уже вычитанные раньше ещё не словили запуск - тогда он и вычитывает "униженных и оскорблённых"). В принципе, я могу модифицировать алгоритм ловли IRQ: чтобы сначала добывались все IRQ, которые только есть, а уж потом шло бы вычитывание всех сработавших - это несложно. Но тогда вместо нынешнего "ближние успевают всегда, дальние реже" будет "всеобщая нищета" -- ВСЕ будут не успевать почти равномерно, хотя и с приоритетом более ближних (те, будучи вычитанными первыми, будут к следующему старту уже взведены; а дальние будут только в процессе вычитывания и ближайший старт пропустят, сработав на следующем). В результате в одной примерно одновременно пришедшей пачке будут смешаны осциллограммы из предыдущего (от ближних) и текущего (от дальних) циклов.
11.03.2020: немножко анализа.
pzframe_retbufs_t
УЖЕ присутствует поле
*timestamps
, просто оно пока везде ставится =NULL.
Очевидно, что единственное ЖЕЛЕЗНОЕ решение проблемы -- именно в железе: чтобы система синхронизации передавала бы осциллографам (и прочим заинтересованным) "метку времени" -- пофиг, какого рода -- и из устройств можно б было её считать и отдать наверх вместе с данными как дополнительный параметр.
Тогда сопоставлением этих меток от разных устройств можно составить единую картину: к одному "выстрелу" будут принадлежать измерения с одинаковой меткой.
30.06.2021: типа обсуждения:
space_size
ЕСТЬ,
но на уровень HAL он никак не смотрит.
Получается, что:
Получится забавный набор реализаций: BIVME2:ничего, A3818:шина, MVME3100:диапазон.
02.07.2021: но вообще есть надежда, что это всё нафиг не понадобится: либо вообще обойдёмся без Тундры, либо реализуем её по упрощённой схеме (уставление единственного окна на начало В/В); а потом и вовсе VME наконец-то закончится.
15.12.2021: надежда не сбылась -- поддержка
Тундры/MVME3100 сделана таки была, и проблема "неизвестного на уровне HL
требуемого размера" там решена алоритмично-эвристически: размер окна всегда
выбирается максимально возможный (для A32 -- 21 * 0x01000000
),
а его начало старается сдвинуть пониже -- всё это в предположении, что
адреса устройств выбирали не идиоты и старались сгруппировать их рядом,
чтобы все могли помещаться в 1 окно.
22.02.2019@институтская-сессия-конференц-зал: принято решение переименовать "vme_io_lyr" в просто "vme_lyr".
Во-первых, чтобы не было перекрытий по именам с vme_io, а во-вторых из-за того, что словцо "io" в имени layer'а просто лишнее.
22.02.2019@институтская-сессия: тут пока мало определённого.
VmeAddDevice
-- со всеми параметрами: тут и
am
, и irq_n
+vme_irq_proc
.
am
.
Тем самым мы постулируем, что модель будет "address modifier указывается только при инициализации, а дальше весь В/В идёт с ним одним, неизменным".
libvme_ctl(VME_AM_W,)
, и действует НА ВСЕ СЛЕДУЮЩИЕ операции.
Из чего следует, что:
libvme_ctl(VME_AM_W, &am); last_am = am;
Жутковато, конечно, но радует то, что в большинстве случаев будет постоянно использоваться один и тот же AM, поскольку, например, у всех из семейства adc4x250 он идентичен.
Так вот: похоже, что libvme_ctl(VME_AM_W,)
производит запись
прямо сразу в АППАРАТНЫЙ РЕГИСТР __libvme_regs[0]
(отмаппированный на адресное пространство).
Как следствие -- если несколько пусть и разных программ работают с VME одновременно, то через libvmedirect у них будут постоянно путаться address modifier'ы.
Так что -- увы и ах, вариант "через layer" для BIVME2 является единственным возможным, если хочется надёжности.
01.11.2019: поскольку теперь будет централизованный
менеджмент векторов прерываний, то к VmeAddDevice
добавлен
параметр irq_vect
.
05.11.2019: в API/VMT добавлен новый метод --
get_dev_info()
, по смыслу аналогичный CAN'овскому
одноимённому, а по действию -- в некотором роде зеркальный методу
add()
.
В частности, add()
-то при запросе уже занятого IRQ:VECT
выдаёт в лог ругательство с handle,devid занимающего -- а тут можно будет
ознакомиться с информацией о нём подетальнее.
add()
, ДО методов wr/rd.
31.10.2019: процесс:
По крайней мере, для BIVME2 такой вариант отлично подойдёт. ...а если в будущем понадобится что-то иное -- то ТОГДА и сделаем иное.
Решение:
01.11.2019: продолжаем. Сделано минимальное
наполнение vme_add()
.
devs[]
, а handle
'ы являются просто индексами в
нём.
irqs[]
-- это
массив[8], каждая ячейка которого содержит массив vect2devid[]
05.11.2019: vect2devid[]
переименован в
vect2handle[]
-- ведь ссылаться удобнее на свои внутренние
handle'ы, а не на devid, которым ещё нужна трансляция devid2handle() (в
cankoz_lyr_common.c она есть, но используется исключительно в
get_dev_info()
-- в ней иначе никак).
05.11.2019: продвигаемся дальше -- сделано почти всё:
vme_add()
наполнена всем надлежащим функционалом (кроме
IRQ).
vme_disconnect()
создана; она проходится по ВСЕМУ массиву
устройств devs[]
, в предположении, что один devid может
регистрировать несколько аппаратных устройств.
cankoz_disconnect()
, и при оном копировании
обнаружился косяк в копируемой модели -- повторное использование во
внутреннем блоке кода управляющей переменной включающего цикла.
Оно исправлено и там, и тут, ключевое слово "other_
".
vme_init_lyr()
обеспечивает надлежащую инициализацию
devs[]
и irqs[]
.
VME_HANDLE_ABSENT
=0 и VME_HANDLE_FIRST
=1.
devs[handle].base_addr+ofs
VMELYR_DEFINE_IO()
.
handle
. Это, конечно,
сильно не айс, но сделано для ускорения В/В -- ведь если на чтение каждой
ячейки будет ещё толпа проверок, то производительность опустится ниже
плинтуса.
vme_get_dev_info()
-- который в
интересах vmeserver'а.
Кстати, вместо devid2handle()
тут просто явный проход по
всему devs[]
и при нахождении -- возврат информации, а после
цикла (т.е., при НЕнахождении) -- return -1
.
06.11.2019: кста-а-ати, СТРАШНОЕ: а ведь если некий devid будет занимать по НЕСКОЛЬКО аппаратных устройств/handle'ов, то серверова команда "list" будет показывать лишь первое...
Всё это уже доведено до компилируемости.
Дальше надо поддержку IRQ пилить -- это уже совместная задача с vme_hal и bivme2.
13.11.2019: пилим.
vme_irq_proc
() параметр vector
переименован в
irq_vect
-- для унификации.
14.11.2019: а сегодня уже реально -- ПИЛИМ.
irqinfo_t
добавлено поле state
--
состояние "используемости" данной линии.
-1
, иначе >=0.
vme_init_lyr()
туда прописывается -1
.
vme_term_lyr()
добавлен цикл, что при
>=0
надо эту линию закрыть.
vme_disconnect()
также добавлено реальное закрытие
линии в случае удаления последнего использовавшего её устройства: делается
vmehal_close_irq(irq_n)
и .state=-1
.
vme_add()
:
Не знаю точно, как в стандарте VME с этим вопросом, но оба имеющихся устройства (VADC16 и ADC250) рассматривают программирование irq_n=0 как отключение прерываний.
.state>=0
), и если нет -- то делается попытка её
открыть.
vme_lyr_irq_cb()
прост -- проверяет, что для данного
[irq_n][irq_vect] есть "юзер", и если "да" и его irq_proc!=NULL, то вызывает
её.
17.01.2020: собственно, оно не только давно сделано, но и проверено, и уже даже содержимое hw4cx/etc/ppc860-unified/BIVME2/ на него переведено.
Так что с чистой совестью ставим "done", а дальнейшие усовершенствования уже отдельно.
VmeGetDevInfo()
отдачу и bus_major/bus_minor -- чтобы
потенциальный v4a3818vmeserver мог бы выдавать эту информацию по консольному
интерфейсу.
16.01.2020@вечер-дома: а
ещё стоит добавить в VmeAddDevice()
параметр "address size" --
во-первых, для выдачи тем же сервером, а во-вторых, может понадобиться при
менеджменте окон в MVME3100.
17.01.2020: сделал, и первое, и второе.
И сервер уже выдаёт дополнительные данные (ну и громоздкая же длиннота получилась!).
Вывод: надо срочно вводить в HAL- и layer-API векторные операции. Да, на конкретно BIVME2 они будут экономить лишь время вызовов процедур (если вручную не лабать векторное расширение к libvmedirect), но уж хоть что-то.
17.01.2020: да, в layer-API функции добавлены:
v
".
uintNN *data
", плюс
параметр count
.
Покамест только определения, даже в самом lyr'е реализации нет.
18.01.2020@дома, суббота: поддержка векторности доделана везде (хотя и никак не проверена):
bivme2_hal_last_used_am
для
каждого вызова.
CAENVME_BLTWriteycle()
и
CAENVME_BLTReadCycle()
.
vme_lyr_common.c
всё добавлено.
Учитывая разнообразие и заранее-непонятность, напрашивается что-то типа ioctl().
17.01.2020: в layer-API оно добавлено -- метод
hal_ioctl()
.
Параметры аналогичны обычному ioctl()
'ю, но присутствует
дополнительный строковый hal_api_name
-- чтобы можно было
по-простому проверять что-нибудь вроде "a3818", и если нет, то и не
пытаться декодировать параметр request
.
24.01.2020: поехали!
irqinfo_t
имелось поле state
, которое если >=0, то "используется", а
если <0, то нет.
Туда по факту просто складировался результат
vme_hal_open_irq()
.
Это и неудобно (приходится инициализировать state=-1) и бессмысленно (совершенно незачем вечно хранить вёрнутый результат).
is_open
. По умолчанию оно =0, при
открывании делается =1, а результат оного открывания держится в локальной
irq_open_r
.
vmebusinfo_t
.
busi[]
:).
Такое странное название потому, что из-за последней "s" множественное от "bus" будет "buses" -- 5 букв вместо 4; вот и выбрано сокращение от "BUSInfo".
VME_HAL_BUS_MAXCOUNT
, а не
1+VME_HAL_BUS_MAXCOUNT.
Мотивировка проста: индексы в этом массиве (bus_handle) являются внутренним делом layer'а и наружу никак не светятся, так что нет смысла проверять приходящие снаружи индексы на корректность и держать 0-ю ячейку неиспользуемой.
Итого:
vme_hal_irq_cb_t
первыми параметрами
добавлены bus_handle
(скорее для порядку) и
privptr
, через который регистратор может передавать нужную
информацию -- например, номер ячейки данной шины, для чего...
vme_hal_open_irq()
добавлен параметр
privptr
, должный как-есть передаваться обработчику.
(А в a3818_hal.h пока неактуально по причине отсутствия там работы с прерываниями.)
bus_idx
.
vme_disconnect()
при поиске "было ли это
последнее устройство на данной линии прерывания?" добавлено условие
соответствия шины.
31.01.2020: замечание: вообще-то этот "privptr" -- по сути
является "bus_privptr" и по-хорошему должен бы указываться ШИНЕ,
прямо в vme_open_bus()
(он аналогичен серверно-драйверову
devptr/privptr1, являющемуся свойством всего драйвера, в противоположность
privptr (privptr2), регистрируемому для конкретного callback'а). Но из
соображений "private-pointer должен указываться рядом с указанием
функции, которой он будет передаваться" оставим как есть сейчас, пусть
это и не совсем красиво.
CAENVMElib_handle
.
bus_handle
(индексы в
a3818_hal_bus_info[]
).
bus_idx
(индексы в
busi[]
).
vme_add()
-- сначала поиск среди
уже используемых, и если не найдена, то занятие новой ячейки с дальнейшим
вызовом vme_hal_open_bus()
.
busi[bus_idx]
, ...
busi[bus_idx].bus_handle
вместо
заглушки QQQ_HANDLE
.
30.01.2020: и это сделано, но ещё надо бы проверить.
Теперь проверять надо -- не сломалось ли чего, и работает ли оно вообще после таких масштабных изменений.
24.01.2020@обед, дорога домой по
Лаврентьева: очень плохо, что в методах В/В -- которые с именами
вроде vme_a32rd32()
-- отсутствует проверка корректности
переданного handle
, а сразу обращается к
busi[devs[handle].bus_idx].bus_handle
, не глядя передавая его
HAL'у.
Это сделано в интересах непорчи производительности на дохлом BIVME2, но на x86-то проверка будет бесконечно быстрой по сравнению с системным вызовом.
Стоит использовать ГИБКИЙ ("гибридный"?) подход: для HAL'ов,
где множественные шины могут быть -- выполнять проверки, а там, где шина
одна -- не только проверок не выполнять, но и вовсе в качестве параметра
bus_handle
передавать 0.
Для определения "возможны ли множественные шины" надо использовать уже
существующий VME_BUS_COMPONENTS_SENSIBLE
.
Отдельный вопрос -- "а как выполнять эту проверку, если все функции В/В
определяются макросом?" (ведь внутри макросов #if
'ов быть не
может). Но тут ответ простой -- ДО определения того макроса иметь
#if
, делающий парочку #define
'ов, которые уже и
использовать внутри макросо-определяемых функций.
29.01.2020: да, сделано: проверка выполняется макросом
VME_LYR_CHECK_HANDLE()
, а в качестве передаваемого HAL'у
bus_handle используется VME_LYR_BUS_HANDLE_VALUE
, в отсутствие
шин ссылающийся на заводимый при этом the_bus_handle
=0.
24.01.2020@вечер-дома: поскольку вместе с layer'ом был также переделан и vme_test_common.c, то надо было и его проверить.
Да, проверен -- работает (но в нём изменения были скорее косметические).
25.01.2020@суббота-вечер ~19:30: проверено на BIVME2 -- тоже работает!!!
...да, одновременно обнаружена проблема с неработой всех 8 модулей сразу (что постоянно пашут лишь часть), но это отдельный вопрос.
22.06.2021@утро: вчера при подключении к 2-портовому A3818 второго крейта (именно через 2-й порт) обнаружился косяк: при попытке указать в devlist'е пару устройств на шине major=1,minor=0 в момент регистрации второго устройства вылазила проблема
vme_hal_open_bus(1,0) failed: Device or resource busy
Встал очевидный вопрос -- чего оно полезло второй раз ШИНУ открывать?
Вот и разобрался: в vme_lyr_common.c, от 18-02-2020, в
vme_add()
при регистрации новооткрытой
шины в busi[]
было забыто сохранение туда значений
bus_major
,bus_minor
. Вот и считалась эта ячейка
за шину (0,0) -- почему ранее проблема и не проявлялась. Но в
a3818_hal.h учёт делался корректно, так что тот повторное открытие
шины замечал и возвращал EBUSY.
Сохранение добавлено, проблема ушла.
И надо уметь как-то ловить такие события, чтобы правильно ре-инициализировать аппаратуру (и "забывать" о том, в каком режиме находились железка и драйвер) -- совершенно аналогично CAN-bus'у.
Проблема разбивается на 3 аспекта:
Кстати, вообще-то аналогичная ситуация и с PCI -- он ведь тоже hot-plug, особенно в варианте PCIe. Но там практически никто не заморачивается этим аспектом, хотя в идеале надо бы.
Задача, кстати, интердисциплинарная -- касается и vme_hal, и vme_lyr. Но, поскольку ближайшая часть реализации будет на уровне layer'а, то записываем всё здесь.
07.02.2020: предварительная мысль: для ПРОСТЫХ случаев
может оказаться достаточно просто дёрнуть состояние драйвера --
SetDevState()
'ом дрыгнув NOTREADY/OPERATING (ровно как это
делает cankoz_lyr). И это поведение можно регулировать options (о каковом
уже всё равно зашла речь).
Не откладывая в долгий ящик, первую фазу подготовки делаем прямо сразу.
vme_stat_proc
-- простой, кроме
devid+devptr принимающий пару reason+info.
VmeAddDevice
добавлены параметры stat_proc
и options
.
vmedevinfo_t
, куда
vme_add()
просто молча сохраняет переданные параметры.
VME_LYR_OPTION_NONE
=0.
Более ничего пока не делалось -- да и неясно, что да как, пока a3818_hal не умеет ловить такие события.
27.01.2021: ага-ага, но только инфраструктуру/способ передачи событий от vme_hal к его юзерам (включая vme_lyr) тогда сделать забыл...
По первоначальному заключению решено было vsdc4_drv.c
реализовать путём регистрации add()
'ом 4 устройств, со всеми
совпадающими параметрами, кроме векторов.
Но по здравому размышлению это выглядит дикостью.
А более элегантным (хотя и чуток тяжеловесным) -- ПРЯМАЯ поддержка "многовекторности", для чего потребуется:
irq_vect
на массив
irq_vects[]
-- с тем же "правилом терминирования", по значению
<0.
add_vectors()
-- ему передавать сразу
ПАЧКУ кодов векторов.
Или ВСЕГДА прямо в add()
'е передавать именно
пачку -- массивом с количеством?
vme_disconnect()
подчистку перевести с одиночного
присвоения =VME_HANDLE_ABSENT на цикл по irq_vects[]
(которые
надо будет предварительно -- ДО bzero()
-- копировать в
локальный массив).
...а начнётся, скорее всего, с перевода bivme2_io на layer.
22.02.2019@институтская-сессия: робкая попытка начать.
Создан -- в очень усечённом виде, копированием из
bivme2/bivme2_io.c -- файл bivme2_vme_io.c. В нём сейчас
есть исключительно определения (макрорасширениями) функций чтения/записи в
вариантах {16,24,32}{rd,wr}{8,16,32}. Покамест БЕЗ параметров
am
.
05.11.2019: поскольку делаем по-другому, то этот раздельчик, скорее всего, станет "withdrawn".
05.11.2019: работа:
11.11.2019: а вот и ЗРЯ затащена! Поскольку там
используются ссылки $(LIBnnn)
, определяемые только в ПОЗЖЕ
include'имых файлах.
vmehal_last_used_am
-- чтобы мочь жонглировать
address-modifier'ом.
VMEHAL_DEFINE_IO()
.
11.11.2019: после небольших шаманств всё уже собирается.
(Но работать пока нечему -- нету ни поддержки IRQ, ни самого содержимого драйверов под модель с layer'ами.)
12.11.2019: в Makefile добавлено, что
собирается оно прямо сразу вместе с $(LIBPZFRAME_DRV)
.
Сделано просто чтобы СЕЙЧАС собиралось.
Но в будущем надо переделать, чтобы библиотека подклеивалась к индивидуальным драйверам -- как, например, с cPCI'ными adc200me/adc812me. Потому, что в недалёком будущем появится модуль a3818_lyr, а драйверы нужно будет собирать под локальную платформу.
14.11.2019: bivme2hal.h доведён до должного работать состояния:
vmehal_irq_info[8]
, содержащем структуры-описатели из 3 полей:
fd, fd_fdh, cb.
Для НЕиспользуемых номеров fd и fd_fdh содержат -1.
vmehal_open_irq()
прямолинейна: открывает
/dev/vmeiN, затем рагистрирует дескриптор через
sl_add_fd()
.
При обломе любого из этих шагов -- возвращает -1.
vmehal_irq_cb()
тоже прямолинеен:
вычитывает int32
-вектор и передаёт его зарегистрированному
обработчику.
При ошибке вычитывания (результат read()
не равен ожидаемому)
-- ругается на stderr.
vmehal_close_irq()
совсем тривиальна: закрывает fd_fdh и
fd, затем записывая в них -1.
Функции открытия/закрытия просты настолько, что НЕ проверяют параметр
irq_n
: ни на попадание в [0-7], ни на повторное
открытие/закрытие -- предполагается, что юзер HAL'а достаточно разумен.
18.11.2019: первая проверка на реальном железе. Результат -- SIGSEGV при первом же обращении к VME-шине.
Разбирательство показало, что была забыта инициализация, из 2 пунктов:
libvme_init()
;
libvme_ctl(VME_SYSTEM_W, &v)
с v=1.
Сделано, для чего:
vmehal_init()
.
-1
.
vme_init_lyr()
вставлен его вызов, с проверкой
результата и облома (с сообщением) при ошибке.
Всё, после этого -- заработало; на вид драйвер adc250 ведёт себя как предыдущий (только IRQ ловятся, похоже, от всех устройств -- за что и боролись layer'изацией).
11.11.2020: модификация в mbivme2/Makefile:
для ВСЕХ юзеров libvmedirect.h делается
OPTIMIZATION=-O0
(сейчас это bivme2vme_lyr.o, bivme2_test.c
test_rfmeas_a32r16.o).
Краткое пояснение этой эпопеи (растянувшейся почти на неделю):
-O2
почему-то
начинаются лютые глюки с int16-обращениями к шине: такое впечатление,
что VME-окно ("overlay") НЕ переключается на указанный адрес, а остаётся
старым, из-за чего читается память "предыдущего" использованного устройства;
при следующем же обращении окно уже в нужном месте.
Вечер, когда записываю это всё: подумалось -- а не происходит ли, случаем, перестановка команд компилятором? Что команда переключения окна переставляется в точку ПОСЛЕ команды обращения к VME-шине/ Тогда как раз такой эффект бы получился.
И когда я послал Антону мною собранный бинарник, то он там тоже глючил.
Откуда возникла идея, что, возможно, разница в разных версиях libvmedirect.h и libvmedirect.h. Потратили полдня на поиск различий (файлы-то действительно разные, почему-то -- у меня за 2005 год "исходный из powerpc-sdk-linux-v4.tgz, а у них есть несколько разных за 2009, 2010, 2013...). Но оказалось, что файлы различаются только по whitespace. И тут Антона осенило об оптимизации :)
25.11.2019@вечер-дома: собственно -- в чём проблема? В отсутствии буферизации. Ну так сделать её!
Технология совершенно аналогична буферу клавиатуры, как он делался, например в BIOS/DOS.
Тут скорее вопрос -- а реально ли дело в ЭТОМ (т.е., в отсутствии буферизации), или же более глубокий косяк контроллера?
Это можно проверить при помощи ДВУХ козачиных VADC16.
26.11.2019@утро-дома: однако, может быть проблема: если некое устройство молотит с очень большой скоростью, то оно в одиночку забъёт весь буфер.
Поэтому -- надо делать БУФЕР С ФЛАГИРОВАНИЕМ.
was[256]
.
Размер буфера -- да хоть тоже [256]. ...по факту это будет ДО 255 (из-за алгоритмики головы+хвоста буфера), но на это можно забить.
vector
, а затем с ним:
was[vector]!=0
, то ничего не
делать.
was[vector]=1
.
vector
.
was[vector]=0
.
vector
вызывающему.
ВОЗМОЖНО -- поддерживать передачу векторов пачкой: если просят объём на чтение больше, чем sizeof(int); тогда просто
verify_area(VERIFY_WRITE,
buf, sizeof(int)*max_count)
.
(Тут текста получилось больше, чем будет кода.)
26.11.2019@вечер-дома: набросал, по-быстрому -- лежит пока в директории hw4cx/kernel_hw/bivme2/, файл new_vmei.c:
vme_data_t
поле vector
убрано, как и все
обращения к нему из кода.
q_isin[256]
(это "was[]
" из
проекта), q_buff[Q_SIZE]
(собственно буфер-очередь-FIFO),
q_head
и q_tail
.
q_head
сдивгается на 1 с заворачиванием), чтение делается из хвоста (с последующим
сдвигом q_tail
с заворачиванием).
(q_head+1)&0xFF==q_tail
.
q_head==q_tail
.
vme_interrupt()
и vme_read()
переделаны по утреннему проекту на работу с очередью.
Замечания насчёт vme_read()
:
sizeof(int)
-- пока не делается.
-EWOULDBLOCK
.
vme_poll()
: теперь вместо хитроумной
проверки (там в vector
записывалось "магическое число" -2 и
после poll_wait()
проверялось, всё оно же ли ещё там) просто
проверяется равенство q_head==q_tail
, и если "да", то
возвращается 0
, а иначе POLLPRI
.
vme_read()
и vme_poll()
переведены
на "технологию с ret
":
ret
-- это происходит ВНУТРИ критической секции, защищённой
spinlock'ом.
return ret
.
Никаких попыток компиляции пока не было, да и нечем (полная кросс-среда нужна, со всеми include-файлами).
...да и неясно пока достоверно, реально ли проблема только в быстром приходе нескольких прерываний по одной линии, или же в принципе несколько устройств на одной линии прерывания на этом контроллере жить не могут.
16.12.2019: продолжаем.
make OS_BASE=/tmp/ppc860/linux
(да, нужно дерево, распакованное из powerpc-sdk-linux-v4.tgz).
Тестовая сборка шичковского драйвера показала, что его MD5-сумма совпадает с оной у ихнего /etc/bin/vmei.o в контроллере.
Фиг -- не работает: гонит сплошные вектора 223.
...есть, конечно, надежда, что это МОЙ косяк. А если именно версия драйвера у меня (в которую я добавил буферизацию) сильно старая? Просить у Мамкина более свежую?
17.12.2019: как дальше разбираться -- краткий сценарий:
Ну чо -- пробуем.
Попробовал к собранному применить ppc-linux-strip (а вдруг?) -- вообще 2724.
Немного анализа для понимания ситуации:
Чуть позже: о -- нашёл!!! В мыле письмо от Мамкина за 19-10-2011, где есть исходники обновлённого драйвера, и там vmei.o как раз 4872 байта.
Так что добавим буферизацию векторов в ЭТОТ код. ...кстати, diff показывает, что изменений-то немного -- буквально несколько строк. Но они могут быть критичными.
18.12.2019: кстати, была мысль попробовать воспользоваться утилитой diff3, существующей как раз для таких случаев: сначала понять различия, при помощи
diff3 new_vmei.c vmei-cs.c vadc/vmei.c
а потом, возможно, и вовсе сразу "изготовить" файл с обоими комплектами
модификаций, при помощи
diff3 -ь new_vmei.c vmei-cs.c vadc/vmei.c
Но фиг: имеются конфликты (неудивительно -- я ж правил ровно те же места, что и Мамкин в 2011-м).
20.12.2019: возвращаемся к вопросу запинывания драйвера, способного и правильно обрабатывать прерывания, и буферизовать вектора.
Для начала немного в сторону -- о сборке.
ppc-linux-gcc
ругался на неопределённость
$CROSS_COMPILE
).
Итог -- фиг: .dep-файл получился в 179 строк -- т.е., 178 файлов из linux/kernel/include/.
Теперь о переделке модуля.
vector
трогать не будем, а всего лишь добавим буферизацию, плюс в
vme_read()
будем отдавать число из головы очереди (и ещё,
наверное, в vme_poll()
придётся переделать проверку...).
vme_interrupt()
исправлений нет -- только
DP(printk()) добавлено.
vector = -2
, а poll() считает признаком готовности состояние
vector != -2
.
UPD: (через несколько минут) а, не -- poll()-то и раньше использовал такой признак, но сам же его чуть выше и выставлял (вот ЭТА алхимия совершенно непонятна: как она могла работать? похоже, что и не работала).
vme_read()
цепочка "чтение, затем сброс"
НЕ защищена spinlock'ом.
vector
'а!).
q_buff[]
переименовано в q_fifo[]
.
vme_interrupt()
добавлен код складирования вектора в
буфер.
Отличия от new_vmei.c'шного варианта -- складируются ВСЕ значения, включая 0xAC (AC fail) и -1 (->0xFF).
vme_read()
заменён ВЕСЬ блок возврата: и само
копирование из .vector
, и return sizeof(int)
--
вместо этого взятие из FIFO, а при пустоте в нём возврат
-EWOULDBLOCK
; причём всё это внутри spinlock'а.
vme_poll()
-- тут изрядные модификации: не только
заменен принцип возврата значения (с проверки значения .vector
на проверку непустоты FIFO), но и вёрнут spinlock (в исправленной мамкинской
версии почему-то удалённый).
Замечание: у Мамкина между версиями много изменений, потому и мой код
сильно поменялся. Внутри spinlock'а оставлена только проверка, т.к. там
идёт обращение к ДВУМ полям (q_tail
и q_head
), а
сравнение должно быть атомарным. Вся же алхимия с обращениями к регистрам
VME-чипа вперемежку с poll_wait()
-- оставлена как есть.
Итог: загрузил в BIVME2 вместо "штатного" модуля -- работает!!! Вектора от разных модулей соответствующим драйверам даются вперемежку, как положено.
25.01.2020@суббота, вечер ~19:30: вылезла неприятность -- при запуске всех 8 скринов оказалось, что молотят не все 8 осциллографов, а лишь 5: первые 4 и последний 1 (к которому ЕманоФедя сигналы и подключил).
Они НЕ умерли -- по шине читаются, регистр 0x00 стабильно выдаёт 0x4604ADC0.
27.01.2020: а с утра живыми оказались уже лишь первые 3...
Ещё вчера понапридумал кучу сценариев для разбиралтельства -- как проверить, что же происходит с модулями, что они "умирают" (другие значения векторов, пробовать использовать модули в других наборах (не все, а поштучно, ...), ...).
Но по результатам многочисленных проверок (полдня угрохал!) вырисовалось, что модули НЕ "умирают". А дело в том, что дальние от контроллера как будто "заслоняются" ближними к нему; как только ближние перестают вычитываться (опрашиваться скринами), то дальние очухиваются.
Выглядит так, будто от дальних просто не доставляются до контроллера прерывания.
Учитывая, что в модифицированном мною драйвере теперь FIFO, да и в userspace тоже ничего потенциально-виновного в "маскировании" нет, то подозрение падает на железо.
Договорились с Антоном Павленко завтра на это всё вместе посмотреть.
28.01.2020: посмотрели. Да, я не сошёл с ума, Павленко с Котовым тоже наблюдали этот эффект. Причём Котов обнаружил, что если количество точек с умолчательных 4096 задрать до 50000 либо опустить до 100 -- то работают ВСЕ модули.
После обеда: Павленко позвонил и сказал, что нашёл в vmei.c одну
странность: в начале обработчика прерывания маскируются (сбросом
соответствующего данной линии бита в *reg1
), но в конце они НЕ
размаскируются, а размаскируются только в vme_poll()
.
(Почему так сделано -- примерно понятно: чтобы сошедшее с ума VME-устройство не могло бы намертво завесить контроллер.)
OK -- я добавил в конец обработчика размаскирование. Проверка показала, что с таким вариантом vmei РАБОТАЮТ ВСЕ осциллографы!!!
Теперь надо посмотреть денёк-другой, на тему потенциальных глюков/зависаний.
P.S. И да: для RORA-устройств (Reset On Register Access) такой вариант
принципиально негоден -- прерывание-то в модуле останется несброшенным и по
выходу из vme_interrupt()
немедленно снова вызовет процессорное
прерывание. Но у нас, вроде, все только ROAK (Reset On AcK).
29.01.2020: поскольку за сутки ничего не зависло и не проглючило, то на bivme2-rfmeas обновлённый vmei.o уже запихан в прошивку.
21.01.2020@вечер-дома, ~20:00: пытаемся приступить.
_vect
".
bytes
, words
, dwords
).
count
.
register int
; а если добавить ещё (счётчик,
указатель) -- то хватит ли?
23.01.2020: судя по Википедии, у PowerPC аж 32 регистра, так что должно хватить.
return -1
выполняются уже СНАРУЖИ критической секции.
А надо сброс ошибки делать ВНУТРИ неё, чтобы ВСЯ операция обращения к VME была бы атомарной.
return 0
.
Не очень красиво, но терпимо: в противном случае -- чтоб был ОДИН
экземпляр закрытия секции -- придётся иметь отдельную переменную
ret
, куда класть код для возврата и делать "goto
END
"; это выглядит ещё хуже, не говоря уж о меньшей оптимальности.
И перед собственно обращением производится настройка "окна".
Соответственно, просто так выполнять единый цикл -- нельзя, т.к. блок может перелезть через границу страницы.
Какие тут есть соображения:
Т.е., совсем нулёвый вариант (для тестов скорости) можно сделать и вовсе простодушно-кривым -- в предположении, что векторные операции никогда не пересекут границ страниц.
Т.е.,
overlay
и
offset
-- поместить в дополнительный "внешний" цикл.
Вот...
28.01.2020: мысль -- а можно ли вместо цикла с
по-элементным копированием использовать сразу memcpy()
?
29.01.2020: Павленко с Котовым при помощи осциллографа посмотрели на работу BIVME2 с ADC250.
Так что неясно, насколько реально можно ускорить вычитывание -- возможно, всего на 40%.
Договорились, что я сделаю векторное чтение и они придут с осциллографом ещё разок.
02.02.2020@дома: за вчера-сегодня сделано. Вкратце:
ReadMeasurements()
переведён на векторное
чтение в 1 операцию с последующей перестановкой чётных-нечётных. В
#if/#else/#endif, чтобы можно было быстро переключаться между вариантами.
Страницы по 64МБ (26 бит, 6 бит селектора страниц), а ADC250 имеют ограничение сверху в 16МБ (24 бита, 8 бит селектора устройства), а адресное пространство и вовсе 11.8МБ (0xB8000 байт -- последний адрес 0xB7FFFF).
Т.е., конкретно для ADC250 это ограничение проблемой не станет -- их адресное пространство не только меньше, но и помещается в одну страницу оверлея целиком, в силу совпадающей кратности.
libvme_{read,write}_aNN_byte_vect()
-- делаются при помощи
memcpy()
.
libvmedirect_*_{word,dword}_vect()
делаются циклами, с явным
чтением/записью по указателям соответствующих размерностей.
Не рискнул использовать memcpy()
, поскольку неясно, как оно
работает -- возможно, что там нет оптимизаций при кратности 2 и 4, а всегда
только байтами.
libvme_*_vect()
.
03.02.2020@утро, после планёрки: проверяем.
Первоначальный вариант проверку не прошёл. Точнее, ОБА первоначальных варианта: с "умеренной" векторностью (цикл в HAL'е) и с "полной" (цикл в библиотеке).
Было 2 раздельных дурацких косяка (хотя и идеологически похожих):
data += sizeof(uint##TS)
вместо
data++
.
*dst = *src
вместо
*dst++ = *src++
После исправления -- заработало!!!
Собрана статистика, в следующих условиях: оставлен работать только один модуль adc250_94 (остальным сделано _devstate=-1), у него NUMPTS=41796, внешний запуск с частотой 10Гц (вот это -- зря, надо было внутренний ещё проверить):
Налицо существенный выигрыш.
Причём это было простое линейное ускорение для ОДНОГО модуля. В ситуации же с 8 модулями, работающими от одного внешнего запуска (и с разным количеством точек), получилось ещё интереснее:
Т.е., очевидно, ближние успевают поймать прерывание, считать и отправить данные и получить запрос на следующее измерение ДО следующего внешнего запуска, а дальние уже не успевают, и пропускают каждый 2-й запуск.
Загрузка процессора BIVME2 при этом под 100%, а не ~84%, как раньше.
Симптомы:
Через некоторое время до меня дошло: конкретно этот сервер bivme2-rfmeas
-- ОЧЕНЬ ЗАГРУЖЕННЫЙ! Он пытается выполнять работы больше, чем успевает
процессор, поэтому очень большую (или просто бОльшую?) часть времени
проводит в векторном чтении VME-шины, которая происходит в состоянии с
залоченным семафором __libvme_shm_ptr[0]
. В результате при
нажатии Ctrl+C сервер завершается с ЗАЛОЧЕННЫМ же состоянием семафора, и при
последующих запусках других экземпляров они висят в ожидании разлочки,
которой никогда не произойдёт.
Поэтому был использован следующий подход:
_devstate=-1
.
_devstate=+1
.
Вуаля -- этот вариант работает!
То ли напрямую через драйвер, то ли через CAENVMElib -- пока неясно.
21.02.2019: сей момент выглядит, что придётся
21.02.2019: сей момент выглядит, что придётся через библиотеку -- просто потому, что драйверу она поставляет уже готовые "описатели для выполнения В/В", упакованные в struct'ы.
С другой стороны, API этой библиотеки несколько тошнотен, так что тянет всё же работать напрямую.
09.01.2020: этот вопрос был решён уже с неделю-другую назад, в пользу варианта "через библиотеку".
Так что раздел помечаем как "done".
15.01.2020@утро-дома-завтрак: некоторые рассуждения о ловле прерываний.
CAENVME_IRQCheck()
-- проверить текущее состояние,
ничего не ждя.
CAENVME_IRQWait()
-- ждать в течение стольки-то
миллисекунд, не появится ли что-нибудь.
Хотя в конечном итоге оно общается с драйвером через файловый дескриптор, но никакого API для добычи этого дескриптора у библиотеки (чтобы поместить в свой набор слушаемых, отдав, например, cxscheduler'у) нету.
Планируется попробовать запинать CAEN'овцев, чтобы они это сделали, но пока предполагалось следующее решение.
pipe()
, затем от-fork()
'оваться в
отдельный процесс, в котором перехватить все сигналы и висеть в цикле по
CAENVME_IRQWait()
, а при получении отправлять в pipe пару байт
{IRQ_N,IRQ_VECT}.
Таким образом, основной процесс будет получать информацию из читающей стороны pipe'а, которую сможет затолкать в список слушаемых -- примерно такой механизм используется для ловли LAM'ов в CM5307.
Очень далеко не факт, что это будет работать, с итальянским-то раздолбайским кодом...
И что делать -- окружать каждый чих (каждую операцию В/В) mutex'ом? Причём:
Проблема...
15.01.2020@вечер-дома:
напрашивается очевидное решение: завести периодический
"таймаут" (heartbeat) с частотой 10Hz, в котором и делать
CAENVME_IRQCheck()
.
Да, халтурновато, но для начала -- вполне покатит (пока CAEN'овцев не запинаем).
И не требуется никаких fork()'ов, mutex'ов и тому подобной нечисти.
Только надо будет всё же почитать доки, как правильно ловить прерывания
-- там, похоже, нужно руками "подтверждать" прерывания
путём вызова CAENVME_IACKCycle()
, который и вертает вектор
(хотя оный параметр там описан странно -- "void
*Vector
"...).
17.01.2020: ага, только одна проблема будет с этим
"10Hz" -- что придётся перевести vme_test_common.c на НАСТОЯЩИЙ
cxscheduler, т.к. в имеющемся костыле эмулируется лишь
sl_add_fd()
на 1 дескриптор, а таймауты -- нет.
18.01.2020@дома, воскресенье: за вчера и сегодня добит начальный вариант:
CAENVMElib_INSTALLED
,
На неё указывает CAENVMElib_BASE_DIR
, по умолчанию
/tmp/CAENVMELib-2.50.
04.03.2020: переделано с /tmp/ на $(HOME)/compile/CAENVMELib-2.50: 1) ибо нефиг; 2) аналогично всяким EPICS/Tango.
...и библиотека явным образом берётся из файла с тем же номером версии -- libCAENVME.so.2.50.
Замечание: НИ В КОЕМ СЛУЧАЕ нельзя использовать ихний скрипт lib/install_x64, поскольку он копирует файлы в /usr/lib/ вместо надлежащего /usr/lib64/ (и симлинк libCAENVME.so делает там же, причём ещё и абсолютный, с именем директории). Надо ТОЛЬКО ВРУЧНУЮ.
#ifdef WIN32
).
Поэтому пришлось добавить
#define LINUX
перед
#include "CAENVMElib.h"
Да, оно пока без поддержки прерываний, но уже надо проверять.
20.01.2020: пытаемся проверять a3818_test -- пока чисто "запустится или нет".
Непорядок -- надо бы как-то сделать, чтобы указывались отдельно
-lДИРЕКТОРИЯ
и отдельно имя файла (да, пусть и полное).
Но нет -- не только в RHEL-7.3 этого нет ни в man, ни в info по gcc и ld, но и в RH-7.3 и даже RH-4.2 тоже нету. ...или то я читал ещё в Slackware на damp?
If NAMESPEC is of the form ':FILENAME', 'ld' will search the library path for a file called FILENAME, otherwise it will search the library path for a file called 'libNAMESPEC.a'.
Вот этот финт с двоеточием и использован --
CAENVMElib_LIB_PARAMS= -L$(CAENVMElib_LIB_DIR) -l:libCAENVME.so.2.50
Работает.
P.S. Фиг знает, когда эта фича появилась. Но в RHEL6 -- точнее, в binutils-2.20 -- она уже есть.
CAENVME_Init()
написано).
Доп.тест:
access(/proc/a2818)
=-2/ENOENT;
access()
'ом.
a2818_N, где N=Link*16+BdNum, BdNum от 0 до 15?
access()
.
cvNNN
в
обычные Ennn
.
CAENVME_DecodeError()
.
Как? Напрашивается 2 варианта:
#define
-символом включать выдачу на stderr
прямо hal'ом.
Второй вариант выглядит лучше.
После обеда: да, реализован вариант II.2 -- см. в разделе по "vme_hal".
20.01.2020@дорога домой на обед, около мыши ~14:00: можно очень легко добавить поддержку других адаптеров, работающих через тот же драйвер -- A2818 и т.д.: учитывая, что у нас BUS_MAJOR теперь НЕ является индексом в таблице, а просто записывается в "свойства шины", то можно указывать тип устройства в "тысячах": например, 10000-10999 -- V1718, 11000-11999 -- V2718, и т.д.
20.01.2020: не откладываем в долгий ящик -- делаем!
cvV1718
, который =0.
22.01.2020: контроллер воткнут (вместо bivme2-rfmeas=192.168.130.76, утащенного в клистронку), попробовано выполнить первый реальный тест с реальными железом.
Фиг -- IO() даёт ошибку "Invalid parameter" (-4) сразу же, судя по ltrace, даже без попыток обратиться к драйверу в ядре.
OK -- надо:
23.01.2020@утро-дома: а может, "invalid parameter" касается не handle'а (как кажется на первый взгляд), а каких-то ДРУГИХ параметров -- например, Data Width?
23.01.2020: разбираемся.
Порылся в её исходниках -- она как-то сама его выставляет.
CAENVME_ReadCycle()
показывается с всего 4 параметрами -- т.е.,
без DataWidth.
Причина была найдена -- это особенность ltrace. Дело в том, что для корректного декодирования параметров ей нужно описание функции (раздел "PROTOTYPE LIBRARY DISCOVERY" в man'е). А при отсутствии -- как подсказало гугление -- она пользуется неким "общим понятием об API вызова" (calling convention), которое, похоже, предполагает 4 параметра. Так что публика жалуется на противоположное -- что ЛИШНИЕ параметры печатаются: всегда по 4 у функций, например, принимающих 1.
Чуть позже: ну что -- сделал, по образу /usr/share/ltrace/libc.so.conf и ltrace.conf(5) файл ~/.ltrace.conf, содержащий строчку
int CAENVME_ReadCycle(int, hex(uint), void*, int, int);
-- помогло!!! Теперь ltrace корректно показывает ВСЕ параметры! Причём
адрес на шине -- шестнадцатиричкой.
Нет, НЕ требуется -- в исходник той тестовой программки засунут вывод на
stderr, и там используется обычное cvD32
=0x04, а НЕ
cvD32_swapped
=0x14.
static
man_par_t man
, и поле внутри неё)), а у нас они в стеке и
имеют вид 0x7ffd12b11cf0.
OK -- временно переделал vme_hal_a##AS##rd##TS()
, вставив
туда чтение в статический буферок с последующим копированием куда просят.
Фиг -- адрес, судя по выдаче, стал маленьким, но проблема не исчезла.
Обнаружилось это, когда я просмотрел ВЕСЬ трассировоыный вывод от CAENVMEDemo -- она ж не понимает "A3818", и приходится указывать "V2718" -- и когда попробовал a3818_test'у указать в bus_major значение 11000 (т.е., BdTypecvV2718, Link=0), то оно заработало!!!
Ура! Дальше ловлю прерываний делать.
28.01.2020: пока непонятка с тем, что же указывается в
BdType (у CAEN'а там, похоже, смешаны разные вещи: типы
АДАПТЕРА и КОНТРОЛЛЕРА), по умолчанию сделано
BdType = cvV2718
.
30.01.2020: приступаем к изготовлению поддержки
прерываний. Делаем, подсматривая в
CAENVMELib-2.50/sample/CAENVMEDemoVme.c::CaenVmeIrqCheck()
.
irqstat
указанного номера level
выглядят как
man->irqstat & (1<<(man->level-1))
Смущает это "-1" -- выглядит, что маска сдвинута на 1 вправо; очевидно, в предположении, что IRQ0 никогда не бывает?
a3818_heartbeat_proc()
, с частотой 10Гц
проверяющая статус CAENVME_IRQCheck()
'ом.
Если получено что-то, то идётся по циклу от 1 до 7, и если в полученной маске прерываний есть битик для этого уровня, то
CAENVME_IACKCycle()
,
irq_mask
-- это битовая
маска, где каждому уровню level соответствует битик 1<<level. Если он
взведён -- значит, этот уровень мониторируется.
И при проверке "есть ли в полученной маске прерываний битик для этого уровня" сначала делается AND с маской мониторируемого -- чтобы не обращать внимания на не интересующее.
irq_mask
пока не сделаны.
03.02.2020: продолжаем:
a3818_hal_irq_info_t
(слизано с
bivme2_hal.h) и добавлено
a3818_hal_bus_info_t.irq_info[8]
этого типа.
vme_hal_open_irq()
и
vme_hal_close_irq()
. Там всё в сущности тривиально.
a3818_heartbeat_proc()
-- добавлен
собственно вызов callback'а.
А вот теперь уже пора тестировать.
04.02.2020: тестируем.
Это понятно: оно СНАЧАЛА пытается открыть шину и сделать хоть что-то, а лишь потом реально регистрирует устройство в таблице. А сервер при обломе в любом случае вызывает disconnect().
Пока придётся с этим жить и забить.
То был тупой косяк -- в конце vme_hal_open_irq()
оставалось
return -1
.
CAENVME_IACKCycle()
ошибку -4 --
cvInvalidParam.
Сначала почему-то показалось, что это таймаут (не в ту строчку посмотрел), так что даже натравил CAENVMEDemo -- а оно успешно прочитало вектор и сбросило прерывание.
Дополнительный результат-бонус: оно запустилось ПАРАЛЛЕЛЬНО с сервером и доступилось к VME параллельно с ним. Вывод: CAENVMElib позволяет работать параллельно нескольким процессам. Good.
Потом разобрался: оказывается, enum-константы
cvIRQ1
...cvIRQ7
имеют значения НЕ равные level
(0...7), а 1<<(level-1).
Решение просто: заведена табличка трансляции
level2cvIRQx[8]
, соответствующий номер из неё и передаётся
библиотеке.
После этого прерывания стали ловиться!
Кстати: cvIRQ0
НЕ существует (и не может
существовать при такой кодировке -- это было бы 1<<(0-1).
Проверяем код возврата от a32rd32v()
-- -1,
errno=11/EAGAIN=EWOULDBLOCK. А ещё -1 -- это cvBusError
, "VME
bus error during the cycle".
OK, переводим драйвер в скалярный (по-ячеечный) режим -- да, шум вычитывается.
А с векторным вычитыванием какие-то странности: сколько бы ни указывалось точек, оно всегда оканчивается ошибкой BusError, хотя какие-то данные вначале передаёт.
Обнадёживает, что в CAENVMEDemoVme.c присутствует
CAENVME_BLTReadCycle()
-- вот попробуем им и посмотрим на
результаты и данные.
Попробовал -- фиг!!! Оно вычитывает только первое 32-битное слово, а дальше облом.
Это, кстати, и есть причина того, что "хотя какие-то данные вначале передаёт" -- на скрине показывался мусор из буфера, а не реальные данные (присутствовавшие только в 0-й ячейке).
Откуда подозрение: а не в ADC250 ли дело?
После чего позвонил уже Павленко и Котову.
Хотя, ВРОДЕ БЫ, должен он у них работать -- Антону помнилось, что вроде делали.
CaenVmeReadBlt()
должным образом
производит подмену, вместо cvA32_U_DATA=0x09 используя cvA32_U_BLT=0x0B.
Также был проведён тест с принудительной передачей 0x0B в
vme_hal_a##AS##rd##TS##v()
-- и тоже не помогло.
Резюме:
...хотя надлежащую подмену Address Modifier'а надо добавить.
05.02.2020: доделки по мелочи:
CAENVME_IACKCycle()
теперь выполняется не
однократно, а в цикле по repcount
до 100 раз.
Это оно так потому, что у нас же делается периодический поллинг, и если выполнять ACK один раз, то будет отрабатываться лишь первый (ближний к контроллеру) из возможно многих. А так -- отрабатываются все.
Немножко результатов испытаний:
Видимо, оба "спецэффекта" происходят из-за того, что модули стоят на внутреннем запуске -- т.е., молотят максимально быстро (с точностью до времени на передачу данных серверу и повторного запроса; но тут всё локально (а не через сеть), так что по факту всё мгновенно -- повторный запрос, измерение и IRQ успевают произойти ещё внутри цикла по repcount).
И ещё немножко "анализа":
Убирание второго модуля (стоп клиента) тут же сбрасывает скорость первого до 10fps.
Но при убирании второго клиента этот "спецэффект" тоже исчезает.
Возникала мысль, что это просто сам GUI-клиент adc250 так тормозит -- но нет, он занимает минимум CPU, как и X.
vme_hal_a##AS##wr##TS##v()
и
vme_hal_a##AS##rd##TS##v()
добавлена, пардон за каламбур,
модификация Address Modifier'а -- ему делается "| 2
".
Анализ всех кодов cvAnn_*
из CAENVMEtypes.h
показал, что отличие ВСЕГДА в битике 2, после чего очевидным решением стало
его взведеие, а не монструозный if() или табличная выборка.
11.02.2020: а вот кстати не совсем всегда: конкретно для A40 разница в 3 -- cvA40=0x34, cvA40_BLT=0x37. Причина этого неясна, а код 0x36 (должный бы быть BLT-вариантом) просто зарезервирован.
14.12.2021: ответ насчёт A40 -- в том, что это вообще урезанный режим, в котором всего 3 AM'а (почти аналогично A16, только набор чуть иной). Насчёт A64 -- кстати, аналогично: оба эти режимы адресации были сделаны сильно позже (в 1990-х?), а их коды впихнуты куда удалось между существующих (где было место). (Это понимание пришло после многочасового глядения в таблицу AM'ов при попытке реализовать для MVME3100/TSI148 трансляцию дуплета (aspace,cycle) в AM.)
05.02.2020@вечер-дома: Антон Павленко позвонил, что Котов вроде придумал, как быстро добавить поддержку BLT-доступа.
06.02.2020: пара модулей, с которыми я экспериментирую на столе, отнесена на перепрошивку.
06.02.2020: прошло всего несколько часов, а модули уже перепрошиты и забраны обратно на тесты.
Итог -- работают!!! Молотят довольно быстро -- пара модулей на 4096 точках дают по 62fps, а на 50000 точках -- по 5.2fps; это около 2МБ/сек; но один модуль на 50000 даёт уже 9.8fps, что уже близко к предельной для ADC250 скорости чтения 4МБ/сек.
(Это чтоб ADC250 в крейте в клистронке при подключении через A3818 -- который встанет в некий комп -- были бы видны через cxhw:15).
10.02.2020@утро-дома: а можно проверять выключение/включение крейта периодическим поллингом регистров контроллера, но это бяка -- даже race condition возможен (когда драйвер УЖЕ выполнил успешно какую-то VME-операцию, и тут ему задним числом приходит уведомление о включении).
В какой-то момент что-то сбрендило и сервер начал подвисать и падать; gdb
(через core-файл) показал лишь порченый стек, ltrace -- падение на futex'е
(в CAENVME_Init()
, как показала напиханная отладочная печать).
Попытка "rmmod a3818" обломилась, сказав "Module a3818 is in use", поскольку
висело ещё CAENVMEDemo. А вот после выхода из неё -- всё устаканилось!
Проишедшее ставит под сомнение идею реализовывать работу с разными аспектами (обычный В/В, асинхронное чтение, ловля прерываний) через НЕСКОЛЬКО раздельных CAENVMElib_handle'ов.
18.02.2020: да, в любом случае сегодня стало ясно, что несколько раздельных CAENVMElib_handle'ов оно просто не позволяет.
Кроме того, наблюдена ещё одна особенность: вызов
CAENVME_Init()
иногда бывает очень долгим -- до секунды. И
"ltrace -S
" показал, что это время проводится в том самом
futex()
.
17.02.2020: приступаем.
USE_THREAD_FOR_IRQ
, принимающий значения 1 (новый вариант) или
0 (поллинг).
USE_THREAD_FOR_IRQ
=1 в Makefile делается
и ссылки на эти определения добавлены в соответствующие _DEFINES и _LDFLAGS.A3818_HAL_DEFINES= -DUSE_THREAD_FOR_IRQ=1 A3818_HAL_LDFLAGS= -lpthread
18.02.2020: доделываем. Для удобства чтения опишем вчерашние и сегодняшние деяния единым списком, чтобы получилась целостная картина.
a3818_hal_bus_info_t
добавлены поля:
CAENVMElib_IRQ_handle
-- тот самый "2-й handle".
irq_pipe[2]
и irq_rfdh
-- очевидно.
irq_thread
-- идентификатор thread'а.
vme_hal_open_bus()
:
Замечания:
Смысл -- чтобы натуральным образом ограничивать частоту подтверждения прерываний: если основная часть драйвера (вычитывающая сообщения из pipe'а) не успевает всё обрабатывать, то pipe переполнится и пишущая сторона (которая, собственно, и занимается ожиданием прерываний) просто заблокируется на записи до тех пор, пока читающая не подразгребётся.
Поэтому в конце сделана секция с меткой ERREXIT
, куда
выполняется goto
при обломах.
А уж там оно сохраняет errno
, выполняет подчистку всего,
восстанавливает errno
, возвращает -1
.
Для этого, естественно, значения полей, ссылающихся на
потенциально-высвобождаемые ресурсы, заранее инициализируются в
-1
.
-1
можно использовать.
Поэтому выполняемое первым шагом CAENVME_Init() снабжено своим отдельным кодом подчистки.
a3818_hal_bus_info[bus_handle]
заменены на короткие через
me
-- а то было шибко уж длинно и плохочитаемо.
vme_hal_close_bus()
добавлена симметричная подчистка:
pthread_cancel()
(убиение) а затем
pthread_join()
(ожидание завершения).
В самом том thread'е в самом начале делается
для разрешения немедленного завершения.pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
CAENVME_End(me->CAENVMElib_IRQ_handle)
;
irq_wait_proc()
.
CAENVME_IRQWait()
с маской 0x7F
и таймаутом
3600*1000, т.е., 1 час.
Описание на эту функцию, мягко говоря, куцее, поэтому делалось всё по наитию (в т.ч. значение маски).
a3818_heartbeat_proc()
, где
делается CAENVME_IRQCheck()
, и затем при непустой маске
делается цикл по всем level'ам и для каждого, чей бит в маске горит,
выполняется цикл по repcount
с CAENVME_IACKCycle()
и "отдачей" прерывания.
uint16
, где в
младшем байте вектор, а в старшем уровень прерывания.
a3818_hal_bus_info_t *me
передаётся
в параметре arg
.
irq2pipe_proc()
.
Тут всё совсем тривиально:
uint16
-значение.
level
и vect
.
level
-- вызываем его.
a3818_hal_bus_info_t *me
передаётся
через privptr2
.
Мотивировка -- потому, что privptr1
обычно привязан к
uniq
.
Ну-с, вроде доделано, пришла пора проверять.
CAENVME_Init()
той
же шины в ответ получаем ошибку cvGenericError
=-3 "Unspecified
error".
25.11.2020: стало ясно, почему нельзя в одной программе
несколько раз открыть один и тот же контроллер: это не драйвер запрещает, а
сама CAENVMElib: CAENVME_Init() вначале проходится по списку открытых
дескрипторов, и если где-то BdType,Link,BdNum совпадают с запрашиваемыми, то
она возвращает cvAlreadyOpenError
(это CAENVMElib-3.20).
Как-никак -- типа модельная реализация для таких случаев (хотя и непроверенная).
А для удовлетворения потребностей ВЭПП-5, где нужна частота срабатывания
10Гц, делаем A3818_HEARTBEAT_USECS
=50*1000, т.е., 50мс, что
соответствует 20Гц -- это должно решить задачу.
18.02.2020: вечером -- дополнительно попробовал
вызвать CAENVME_IRQEnable()
с маской 0x7F
(типа
"разрешить" ВСЕ.
Эффект -- нулевой.
Попробовал также добавить этот же вызов в
a3818_heartbeat_proc()
, в точки сразу после сначала
CAENVME_IRQCheck()
, а затем и CAENVME_IACKCycle()
-- эффект аналогично нулевой.
Посему авансом приступаем к добавлению в a3818_hal.h поддержки такого режима работы.
28.10.2020: этот режим будет включаться символом
USE_SELECT_FOR_IRQ
. Пока что он форсится в 0
, а в
будущем, когда в CAENVMElib.h появится что-нибудь вроде символа
"номер версии", определяться будет им.
22.11.2020: вроде доделал (пока даже без попытки компиляции), и полугода не прошло.
Рассчитано на использование SL_EX
-- т.е., 3-го набора
дескрипторов, exceptfds
.
23.11.2020: и компилируемость сделал (было несколько небольших косячков). Но, естественно, НЕ собираемость -- ибо пара undefined-символов, которых в CAENVMElib'е пока нету.
21.11.2020: доделана и собираемость:
CAENVMElib_*
так, чтобы использовать
эту новую самособранную библиотеку.
CAENVMELIB_SUPPORTS_POLL
, то именно по нему
включается USE_SELECT_FOR_IRQ
.
25.11.2020: приступаем. Начало --
CAENVME_GetFiledes
-- тривиально: она просто возвращает
file_handle
от указанного handle/dev.
А вот CAENVME_ProcessEvent()
идеологически посложнее: он
ведь должен возвращать уже не локальную информацию, а с уровня драйвера.
СЕЙЧАС сделано тривиально-халтурно: он воспроизводит работу
CAENVME_IRQCheck()
, сводящуюся к
CAENVME_ReadRegister(dev, IRQREG,)
. Но это халтура -- потому,
что оное чтение регистра не локально, а лазит в КОНТРОЛЛЕР (V2718), через
оптическую линию. 21.09.2021: вот НЕПРАВДА это!
CAENVME_IRQCheck()
сводится, через цепочку
вызовов, к ioctl(,IOCTL_REG_RD,)
, который, судя по a3818.c,
выполняет просто readl()
-- т.е., ЛОКАЛЬНОЕ чтение регистра из
адресного пространства карточки. А никакого обмена по оптической
линии там не видно.
26.11.2020: CAENVME_ProcessEvent()
подпеределана:
uint32_t *Mask
.
Оно (пока?) именно uint32_t
, а не CAEN_BYTE
,
чтобы более старшими битами мочь возвращать и иные события (вроде
законченности операции CAENVME_BLTReadAsync()
.
27.11.2020: по, похоже, лучше бы всё-таки маску сделать
CAEN_BYTE
-- для унификации с CAENVME_IRQCheck()
,
чтобы никому не удалось по ошибке подсунуть указатель на 8-битный байт
вместо 32-битного слова.
reserved
-- на случай, если захочется
мочь указывать маску/набор интересующих событий для проверки (IRQ, Async,
...).
27.11.2020@утро-душ: всё не даёт покоя идеологическая проблема -- ну КАК всё-таки добывать из ядрового драйвера маску "горящих" прерываний, чтобы это работало как в USPCI, где есть операция FGT_IRQ?
intr
.
a3818_handle_vme_irq()
, и
"читается" там же, просто на тему !=0.
Соответственно, и писать туда можно операцией |=
вместо
просто =
-- это и будет по смыслу, и как раз тем ioctl()'ом
будет выполняться зануление.
28.11.2020@утро-душ:
авотфиг!!! НЕЛЬЗЯ использовать то же самое поле intr
-- при
этом та проверка !=0 станет невалидной, т.к. при работе софта в "старом"
режиме, БЕЗ использования IOCTL_FGT_IRQ, после первого же IRQ значение
intr
так навсегда и останется НЕнулевым, поскольку некому будет
его сбрасывать.
Так что -- увы, придётся добавлять ещё одно поле; например,
intr4poll
, которому и делать |=intr
.
И, возможно, отдельную wait_queue -- например, intr4poll_wait
--
поскольку неясно, насколько корректно использовать ту же wait_queue.
Главная причина непонятки в том, что неясно, какие могут быть иные причины
возникновения прерываний, кроме как от VME.
21.12.2020: наконец-то, через месяц изучения внутренностей CAENVMElib'а и осторожных продвижений, модификации -- И в библиотеку, И в драйвер -- доделаны.
Есть пара нюансов организационного характера:
Надо проверять.
28.12.2020: собрался с духом, проверил -- собрал a3818vme_lyr.so с USE320=YES. Неа, НЕ работает -- не ловит прерывания. Вот с USE320=no -- ловит, а с =YES -- нет.
Ну-у-у, напрашивается изготовить отдельную маленькую утилитку для командной строки, которая бы последовательно исполняла все действия -- открыть, настроить, зависнуть на select(), ... И назвать её caenvmelib_poll_test.
31.12.2020: да, написал work/tests/caenvmelib_poll_test.c и mk-.sh-файл к ней. В ней делается Init, GetFiledes и select().
Попробовал запустить. Сама-то по себе она, ясное дело, никакого прерывания не ловит. ...но и запущенная в параллель с сервером (который с USE320=no), через который даются старты (cdaclient'ом записью shot=1) -- тоже нет.
01.01.2021@после-обеда: а
ведь надо бы ещё добавить "вычитывание" результата --
CAENVME_ProcessEvent()
. Добавил.
@вечер: ...и-и-и -- один раз сработало!!!
Результатом выдало маску 16 -- это как раз и есть соответствие IRQ5, т.к.
1<<(5-1)
. Но только единожды...
01.01.2021@23-00+,засыпая:
а может, нужно делать CAENVME_IRQEnable()
--
ведь в документации что-то говорилось на тему того, что это влияет на
доставку прерываний.
...но сначала бы посмотреть, что же именно оно делает -- и в библиотеке, и в драйвере.
02.01.2021: приступаем. Для начала смотрим, какой же эффект имеют эти IRQEnable/IRQDisable.
_AX818WriteREG(dev, IOCTL_CLR, A2818_VINTDIS)
и затем запись в A2818_IRQMASK0_SET или A2818_IRQMASK1_SET (в зависимости от
board<4 или нет) указанной маски, сдвинутой должным образом.
А по Disable -- только IOCTL_SET, A2818_VINTDIS
.
Но есть свои обращения к регистрам 0xA0 и 0xA4, с обозначениями которых среди драйверов и библиотеки разнобой -- иногда они именуются A3818_IOCTL_S и A3818_IOCTL_С (a3818.h), а иногда IOCTL_SET и IOCTL_CLR (a2818.h). И сводится их работа, видимо, к взведению и сбросу регистра IOCTL=0x08.
Теперь пробуем вставить.
Аллилуйя -- теперь прерывания ловятся при КАЖДОМ запуске!!!
vme_hal_open_bus()
-- точнее, оно там уже было, за-#if0'енное,
теперь открыто.
Не помогло, ловит только первое.
irq_fd_p()
, в самое начало -- ведь
были же где-то упоминания, что Enable нужно выполнять постоянно, ПЕРЕД
каждым ожидаемым IRQ (ну или после каждого очередного).
Неа -- первое ловит, а дальше уже нет.
Да -- помогло!!! Ловятся все!!!
Вот почему, интересно? Или что нужно разрешать IRQ уже после IACKCycle, или просто через какое-то время?
Мда, типа "удача". Но насколько это надёжно?
Кривизна какая...
Может, этот запрет выполняет ДРАЙВЕР (записью в тот самый регистр VINTDIS), и нужно это уметь отключать? Например, сделать ещё один IOCTL, которому указывать "отключать ли автозапрет IRQ", и флаг "отключать" по умолчанию "да", а этот IOCTL позволял бы сделать "нет", тем самым указывая, что программа берёт на себя ответственность.
02.01.2021: продолжаем анализ, чтением кода драйвера.
И смысл ясен: чтобы пока VME'шное IRQ не будет обработано и сброшено, оно бы больше не торчало и не вызывало бы бесконечные локальные прерывания.
В BIVME2'шном vmei.c ведь по сути делается аналогично -- сразу же в обработчике вычитывается вектор, что автоматом сбрасывает запрос.
Тут ровно такое же сделать не получится -- т.к. операция "IACKCycle" совсем не атомарна, а предполагает отправку пакета крейт-контроллеру и получение от него ответа.
irq_fd_p()
.
(Как раз "события" были бы удобнее -- это и есть то, что нам бы и получать из файлового дескриптора, как в конечном итоге и сделано в vmei.c; и PCIe'шные message-signalled interrupts (MSI) тоже в эту же степь...)
Ну что ж -- всё понятно, надо просто сделать и запустить тест.
03.01.2021: сделал:
vme_hal_open_bus()
(при
USE_SELECT_FOR_IRQ.
irq_fd_p()
-- после обработки всех прерываний.
...но НЕ в случае, если полученная маска активных не включает "наши" -- типа чтоб если не наши, то пусть кто-то другой обрабатывает.
...хотя так-то цикл потом идёт по ВСЕМ битам, не глядя на "нашу" irq_mask, так что по факту оно выгребет ВСЕ прерывания. И реально "shared access" НЕ поддерживается. Ну да и фиг с ним.
И запущен тест -- cdaclient'ом запрашивается line0 у adc250, переведённого в режим внутреннего запуска, так что молотит 100+ FPS.
04.01.2021@утро: ну вроде работает -- набралось уже 7+млн срабатываний.
Правда, такой тест не вполне корректен, т.к. тормозит связь с домом. А по-хорошему, надо бы, чтоб вывод cdaclient'а шёл исключительно в файл (чтоб он не тормозил), и запустить одновременно хотя бы 2 осциллографа, а ещё бы в идеале -- более 1 номера IRQ.
05.01.2021@ванна, ~18:00: был некий вопрос -- а как технически смотреть работу нескольких осциллографов? Одного-то, чтоб не летел бесконечный скроллинг, было просто --
-- таким образом, в ОДНОЙ строчке печатались данные плюс обновляющийся счётчик.N=1; \ ~/work/4cx/src/programs/utils/cdaclient -mDntT -b b360mc:1.d5 @i10:line0 | \ while true; do; read ALL;echo -n '\r'$ALL "$N "; N=$[N+1]; done
Хотя всё равно не очень хорошо, поскольку работа завязана на печать (и из-за этого даже пару раз сервер закрывал соединение по причине "Send buffer overflow").
Вот и стоял вопрос -- КАК? То ли как-то zsh'ем через coprocesses запускать, то ли через файлы...
Но тут наконец выкристаллизовалось:
(...&)
", где делается аналогично вышеприведённому, но
вместо печати '\r' делается "echo >ФАЙЛ
" -- каждый
раз "с нуля", заменяя предыдущее содержимое.
Файл, естественно, у каждого чтения свой.
printf
, НЕ переводящей строку
-- '\r' и считанного из тех файлов;
Т.е., раз в секунду печатается текущее состояние.
Плюсы в том, что
Но за счёт вывода фиксированной ширины это будут одноразовые эксцессы и ничего особо не поедет.
06.01.2021@вечер, 23:50...00:15: сделал. Запускаю, буду смотреть.
...какие-то подвисоны. Рестартовал сервер, вроде заработало. Завтра буду смотреть дальше.
07.01.2021@утро-раннее: посмотрел -- грусТно! Оно таки подвисло!
И как-то непонятно -- просто не обновляется, и всё тут.
Но рестарт сервера -- в виде Ctrl+C, стрелка вверх, Enter -- вроде помогает.
А потом в /var/log/messages на b360mc нашлись такие строчки (записываю позже, потому там много):
-- как раз примерно за то время, когда перестало обновляться (у b360mc с x10sae разница часов в 3 минуты).b360mc:/home/user# grep a3818 /var/log/messages* /var/log/messages:Jan 6 23:47:01 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 0 /var/log/messages:Jan 6 23:47:01 b360mc kernel: a3818: rcv-pkt: Link 0 recovered in 1 attempt(s) /var/log/messages:Jan 6 23:52:38 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 0 /var/log/messages:Jan 6 23:52:38 b360mc kernel: a3818: rcv-pkt: Link 0 recovered in 1 attempt(s) /var/log/messages:Jan 7 00:15:20 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 0 /var/log/messages:Jan 7 00:15:20 b360mc kernel: a3818: rcv-pkt: Link 0 recovered in 1 attempt(s) /var/log/messages:Jan 7 08:11:46 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 0 /var/log/messages:Jan 7 08:11:46 b360mc kernel: a3818: rcv-pkt: Link 0 recovered in 1 attempt(s) /var/log/messages:Jan 7 12:25:19 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 0 /var/log/messages:Jan 7 12:25:19 b360mc kernel: a3818: rcv-pkt: Link 0 recovered in 1 attempt(s) /var/log/messages-20210103:Jan 2 09:25:29 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 0 /var/log/messages-20210103:Jan 2 09:25:29 b360mc kernel: a3818: rcv-pkt: Link 0 recovered in 1 attempt(s)
И, что любопытно -- подобное же было и 02-01-2021, когда я с этим возился, но просто я не заметил.
Напрашивающиеся мысли:
Очевидные точки -- те, где печатаются сообщения "Timeout" и "recovered" в syslog().
Вечер: да, это уже сделано -- vme_stat_proc
,
см. за 07-02-2020 в разделе по vme_lyr.
CAENVME_DeviceReset()
-- дёргает
IOCTL_RESET
, приводящий в драйвере к вызову
a3818_reset_comm()
.
Похоже, это оно.
CAENVME_SystemReset()
-- делает что-то не до конца
ясное.
Похоже, выполняет reset УДАЛЁННОМУ КОНТРОЛЛЕРУ -- т.е., некую операцию "reset на VME-шине", путём взведения и потом сразу же сброса битика 0x80 в регистре VMECTRL.
И делает это путём КОММУНИКАЦИИ по линку.
07.01.2021@дорога-от-родителей, в лесочке уже перед двором, ~13:00: а надо бы тесты провести:
07.01.2021@~14:00...вечер: проверяем:
Т.е., ПЕРЕОТКРЫТИЕ линии (поскольку при disconnect'е последнего устройства делается закрытие, и потом линия открывается заново при первом же).
И что -- надо по ловле события "дисконнект" также выполнять
CAENVME_DeviceReset()
? Как бы проверить-то, что оно поможет...
08.01.2021@ванна: а Reset ли
нужен? Или всё же просто CAENVME_IRQEnable()
?
Вечером, когда опять зависло:
Возможно, проблема в том, что в момент какого-то обмена с линией "неожиданно" прилетает сообщение про IRQ.
08.01.2021: продолжаем тестирование.
Только включил второй -- и минут через 10 подвисло.
Но вот сообщения о таймаутах бывают, однако зависаний они не вызывают.
Надо бы поанализировать код рядом с теми сообщениями.
CAENVME_IRQEnable()
.
Вот как так, а?!?!?!
Резюме: похоже, что дело совсем не в отсутствии Enable, а лишь в какой-то странной "зависшести" линии.
SPECIE -- Simple Protocol for Ethernet-connected Intelligent Equipment.
20.01.2021@утро-~11:00, по дороге к родителям, лестница вниз: если считаем, что может потеряться присылаемое от контроллера (V2718?) к адаптеру событие "произошло IRQ", то -- может, в коде драйвера после восстановления безусловно запрашивать у контроллера текущее состояние (как при IRQCheck)?
(после обеда) неа, бессмысленно.
20.01.2021@утро-~11:00, по дороге к родителям, входя в лесок за Пирогова-26: а вот давно хотелось понять -- при таких потерях/восстановлениях (Timeout/recovered) происходят ли ошибки VME-чтения/записи?
Пихать в драйверы проверки на КАЖДУЮ операцию -- опухнешь.
Поэтому надо бы добавить детектирование ошибок прямо в сам HAL -- чтобы гарантировалась проверка КАЖДОЙ операции, и при ошибке она прямо оттуда же и печаталась на stderr.
20.01.2021@~13:00, вернувшись домой: сделал диагностику.
теперь заменилось на#define VME_HAL_DEFINE_IO(AS, name, TS) \ VME_HAL_STORAGE_CLASS int vme_hal_a##AS##wr##TS (int bus_handle, \ int am, \ uint32 addr, uint##TS value) \ { \ return CAENVME_WriteCycle (a3818_hal_bus_info[bus_handle].CAENVMElib_handle, \ addr, &value, \ am, __CX_CONCATENATE(cvD,TS)); \ } \
#ifndef DEBUG_A3818_HAL_IO #define DEBUG_A3818_HAL_IO 1 #endif #if DEBUG_A3818_HAL_IO #define A3818_HAL_IO_BEG int r = #define A3818_HAL_IO_END if (r < 0) fprintf(stderr, "%s(0x%02x:%08x)=%d\n", __FUNCTION__, am, addr, r); \ return r; #else #define A3818_HAL_IO_BEG return #define A3818_HAL_IO_END #endif #define VME_HAL_DEFINE_IO(AS, name, TS) \ VME_HAL_STORAGE_CLASS int vme_hal_a##AS##wr##TS (int bus_handle, \ int am, \ uint32 addr, uint##TS value) \ { \ A3818_HAL_IO_BEG \ CAENVME_WriteCycle (a3818_hal_bus_info[bus_handle].CAENVMElib_handle, \ addr, &value, \ am, __CX_CONCATENATE(cvD,TS)); \ A3818_HAL_IO_END; \ } \
DEBUG_A3818_HAL_IO
управляет включенностью
диагностики.
return
) и "эпилога".
A3818_HAL_IO_BEG
превращается в добычу
результата операции.
A3818_HAL_IO_END
делает проверку, при
надобности выдачу на stderr, а затем возврат.
return
'ом, а "эпилог" пуст.
;
' у "пролога"
и скобок у обоих (хотя у "эпилога" и можно было, но так будет ещё хуже из-за
асимметрии) уже раздражает.
Так что я им совсем не горжусь, просто код раздувать не хотелось.
Результат тестирования: НИ-ЧЕ-ГО. Т.е., при потерях/восстановлениях связи так и не появилось НИ ОДНОГО сообщения об ошибке выполнения VME-операции. (А тестовое чтение заведомо отсутствующего адреса -- ошибку даёт, так что диагностика работает.)
Откуда выводы:
Замечание: по крайней мере, для ТЕКУЩЕЙ проблемы.
21.01.2021: с другой стороны, никто не запрещает вызывать IRQCheck() самому a3818_hal'у.
(Возможно, там вообще просто синхронно выполняется какая-то операция.)
27.01.2021: всё, хватит раздумывать и тянуть -- надо добавлять "генерацию событий" -- уведомление userspace-клиентов о потере и восстановлении соединения.
CAENVME_ProcessEvent()
. В CAENVME_IRQCheck()
же -- 8-битное CAEN_BYTE
.
(Да-да, как раз вспомнилось рассказанное ЕманоФедей -- что для коммерческих программеров считается нормой 2 строки кода в день :D.)
Увы, но оказалось, что организация кода не самая удачная: нет единого файла с определениями, который бы использовался совместно библиотекой (с экспонированием клиентам) и драйвером.
typedef enum { cvConnectionLost = 0x20000000, cvConnectionRecovered = 0x40000000, } CVConnectionEventMasks;
#define A3818_CONNECTION_LOST (0x20000000) #define A3818_CONNECTION_RECOVERED (0x40000000)
28.01.2021: в драйвер a3818.c добавлена "генерация" событий.
Сделано по-простому -- в нужные две точки просто скопирован код из
a3818_handle_vme_irq()
(атомарное/лоченное OR'енье в
intr4poll
с последующим wake_up*'ом).
29.01.2021: пытаемся сделать "простое" решение -- чтоб
прямо a3818_hal.h::irq_fd_p()
выполнял бы холостое
чтение.
irq_fd_p()
ловлю событий.
Памятуя когда-то-шнюю проблему с перезагрузкой (оно по "shutdown -r now" зависло и не дошло до перезагрузки), заподозрил, что косяк в драйвере.
Так что драйвер rmmod'нут и машина перезагружена.
chmod 0666 /dev/a3818*
".
ОК -- подхватилось.
Оказалось -- ляп в коде драйвера, вместо "CONNECTION_RECOVERED" делалось то же самое "CONNECTION_LOST" (огрехи копирования...).
Исправлено.
30.01.2021: далее...
SleepBySelect()
.
Причём пришлось вставлять локальную копию, поскольку в publics-наборе для драйверов оно не экспонируется.
Обсуждение (по результатам просмотра "старых" записей, недельной/трёхнедельной давности, когда только начал набирать статистику по этим зависонам):
CAENVME_DeviceReset()
тоже надо
рассмотреть. Причём:
CAENVME_Init()
(нет ли там сброса/инициализации).
a3818_open()
.
А там есть -- конкретно для TypeOfBoard ==
A3818BOARD
-- вызов некоей a3818_reset_onopen()
...
CAENVME_DeviceReset()
(убрав паузу).
Неа, НЕ помогло.
Тоже не помогло.
31.01.2021: продолжаем тестировать-экспериментировать...
Так что -- надо всё-таки читать код драйвера и библиотеки...
14.02.2021: читал, смотрел (и не один день и не единожды!) -- ничего интересного/подозрительного не увидел.
Пришла в голову мысль: а что, если "пинающий запуск a3818_test" делать не
просто "открыл, выполнил чтение, завершился/закрыл", а поставить в конце
командной строки ":
", чтоб он продолжал висеть с открытым
дескриптором?
15.02.2021: проверил -- результаты крайне неожиданные!
Учитывая, что в irq_fd_p()
делается по 100
repcount-итераций, очевидно, что это как раз они 50+50=100 и есть, а
"событие"-прерывание происходит однократно.
(А осциллографы -- на внутреннем запуске, мгновенно сразу -- молотят так быстро, что пока второй вычитывается, первый уже успевает отработать и заново выставить IRQ.)
Откуда напрашиваются выводы:
":"
, что и приводило к зависанию и
НЕвыходу/НЕзакрытию. Вот я и подумал, что дело исключительно в чтении.
Еще соображения:
Или дело в Ctrl+C -- что просто закрывался дескриптор, а вот
CAENVME_End()
НЕ вызывалось. Т.е., это оно что-то такое
выполняет?
Чуть позже: да нет -- на вид ничего такого в нём нет, а есть лишь:
removesem()
, в свою очередь вызывающий shared_mutex_close()
.
_A3818CloseDriver()
, сводящийся к просто
close(file_handle).
При этом оно просто откроет и почти сразу закроет.
Проверил -- ДА, ПОЛЕТЕЛО!!!
Или, может, это вызвано уже какими-то моими действиями при реализации поддержки a3818_poll() -- какое-то событие генерится случайно, из-за ненулевого значения в регистрах текущей маски "светящихся" IRQ?
И ещё пара соображений:
Чтобы понять -- горят ли какие-то события у ЛОКАЛЬНОГО контроллера A3818, или же они есть только у удалённого V2718.
17.02.2021: а не мои ль всё-таки это косяки? Как-то не так что-то делаю, вот оно и дурит... Мысли по теме:
И где места на диске b360mc взять под /var/log/messages?
@вечер: printk() делать с префиксом "<N>" (или какие варианты ещё возможны -- см. "Debugging by printing"), и сбагривать syslog'ом такие сообщения в отдельный файл.
@дома, ~16:50: да вряд ли --
ведь IRQ локально генерится АППАРАТНО, прямо самим A3818, это не просто
чтение сообщения из линии посредством
a3818_recv_pkt()
/a3818_handle_rx_pkt()
.
Но нет -- проблема в чём-то ином, раз даже IRQCheck() не помогает, зато open()+close() очень даже.
21.02.2021: ещё некоторое количество тестов запуском a3818_test с разными командными строками в параллель к cxsd:
:
-- эффекта нет.
i:0 :
-- отрабатывается 100 циклов (1 IRQ?).
i:0 :$[20*1000000] i:0
:
-- то же самое. Т.е., ВТОРОЕ чтение, даже через паузу, ничего не
даёт.
:100
--
теперь ТОЖЕ НИЧЕГО!!!
i:0 :100
-- работает!
И это СТАБИЛЬНО воспроизводится!
Откуда опять возникает вопрос -- а может, всё-таки это МОЙ косяк?
С другой стороны -- ведь драйвер-то эти Timeout/recovered ловит (и, подозреваю, что СТАРЫЙ драйвер тоже будет; проверим?). Что смахивает уже на какой-то программно-аппаратный глюк.
:100
--
i:0 :100
--
Т.е., что же выходит -- что события доставляются только ПЕРВОМУ? Или предполагается, что "получатель" должен ровно в этот момент висеть на select()'е, а иначе -- фиг?
Как бы то ни было, уж это-то -- точно МОЙ косяк, поскольку сами
события являются МОИМ порождением.
@засыпая-~00:00: дык --
понятно, что "получатель" только ОДИН -- дело в механизме работы
FGT_IRQ
! Да, так оно и должно быть.
24.02.2021: и получал до тех пор (спустя два дня!), пока
не был ещё раз выполнен "пинок" :0 :100
-- тогда такие
"самозастрелы" закончились.
...чуть позже: а вот фиг -- НЕ прекратилось, очень даже продолжается, но
скорее "через раз": если дать про-SleepBySelect()'ить 10с и потом выполнить
чтение, то на этом заканчивается и следующий запуск нормальный, а если не
дать -- то и на следующем повторится 100% (а если дать -- то вопрос
вероятности, но не всегда).
Тут уж совершенно очевидно, что имеется какой-то аппаратный race condition, проявляющийся стохастически.
P.S. Кстати, сам "RX Timeout" -- это истечение
таймаута в wait_event_interruptible_timeout()
, причём в
качестве оного таймаута выступает поле s->timeout
,
определяемое как
s->timeout=msecs_to_jiffies(15000)
...или как (100)? Ведь 15000ms=15s, явно не оно... Надо бы логгинг
вставить в драйвер.
24.02.2021: вставил логгинг. Результат
-- 100; и msecs_to_jiffies()
, похоже, просто ничего не делает:
отладочная выдача результата от (15000) и (100) дала эти же числа.
msecs_to_jiffies()
, похоже, просто ничего не делает":
потому, что HZ=1000. (CentOS-7.3, ядро 3.10) Т.е., HZ и совпадает с
миллисекундами.
Ещё раньше нагуглил исходники -- там было видно, что делается какая-то арифметика; а потом решил проверить, чему равно значение HZ. В /proc/ и /sys/ ничего подходящего на тему "*hz*' не нашлось, но вот в теме "How to check HZ in the terminal?" на Stackoverflow было посоветовано
grep 'CONFIG_HZ=' /boot/config-$(uname -r)
-- оно и дало ответ
# CONFIG_HZ_PERIODIC is not set # CONFIG_HZ_100 is not set # CONFIG_HZ_250 is not set # CONFIG_HZ_300 is not set CONFIG_HZ_1000=y CONFIG_HZ=1000
22.02.2021: список действий, которые стоит провернуть далее:
a3818_test ... :100
".
Как минимум в CAENVME_Init()
есть какие-то
CAENVME_GetFIFOMode()
и CAENVME_WriteRegister(i,
cvVMEIRQEnaReg, 0xFF))
.
cvConnectionRecovered
.
24.02.2021: приступаем (п.2 ещё позавчера).
Как и 1.6.3, отличается только тонкостями с компиляцией под свежие ядра (ещё там что-то с некоей "mmiow()" было).
А вот дальше решил отойти он намеченного сценария:
a3818_recv_pkt()
, ожидаемого
посредством wait_event_interruptible_timeout()
.
a3818_recv_pkt()
как-то отключает
получение прерываний, а в случае таймаута включения обратно не происходит.
Причём оное "включение обратно" вполне может делать даже и не recv_pkt(),
а что-то другое, вызываемое в случае успешного получения -- например,
a3818_handle_rx_pkt()
или его подручный
a3818_dispatch_pkt()
.
...кстати,
a3818_handle_rx_pkt()
может вызываться
a3818_recv_pkt()
a3818_interrupt()
tr_stat & 0xFFFF0000
", каковое tr_stat
является значением, прочитанным из регистра A3818_LINK_TRS
.
Ответ: а потому, что эти 5 попыток -- уже вовсе не просто
recv_pkt(), а вызов друг за дружкой a3818_reset_comm()
и
a3818_send_pkt()
(исходных данных), и уж только потом, в случае
успеха отправки, пробуется снова a3818_recv_pkt()
.
Вот не a3818_reset_comm()
ли, например, отключает
прерывания?
Чуть позже: а вот хбз... Там делается несколько записей в разные регистры, а уж какой они имеют эффект -- фиг поймёшь, описание регистров нужно.
a3818_reset_comm()
-- если после этого прерывания перестанут
отключаться, то дело в нём.
Но это, конечно, именно лишь для проверки -- "открывать банку с червями", ломая отлаженную работу коммуникации, точно не стоит, а надо искать способ включения прерываний обратно.
(Более того -- идея о необходимости макроскопической задержки уже
возникала, и a3818_hal'ов irq_fd_p()
даже выжидает 10 секунд,
но без толку. Хотя, конечно, это реально пауза не там и не о том...)
Возможно, из-за более асинхронного характера потоков данных/пакетов: что отсылка IRQ контроллером V2718 в какой-то момент конфликтует с регулярными запрос-ответами.
Хотя по сути -- имея доступ только к локальной стороне, внятно что-то предположить затруднительно. (А на "той" стороне вообще FPGA, фиг знает, как запрограммированная. Да и сниффера-анализатора для CONET2 у нас нету...)
...только неясно, какой "прогрессивный" прок с этих догадок -- предпринять-то что можно?
25.02.2021: попробовал убрать
a3818_reset_comm()
...
irq_fd_p()
делается
CAENVME_DeviceReset()
, который по сути и является
ioctl()-обёрткой к a3818_reset_comm()
.)
...и дальше в том же духе -- аналогичный бред, видимо, по причине порченого стека.#0 0x00007f690186d13d in getenv () from /lib64/libc.so.6 #1 0x00007f6901864cae in __dcigettext () from /lib64/libc.so.6 #2 0x0000000000408ef8 in onsig (sig=11) at cxsd.c:56 #3 <signal handler called> #4 0x00007f68ff01523d in CAENVME_ReadCycle () from /home/user/compile/CAENVMELib-3.2.0_sources.new/libCAENVME.so.3.1.0 #5 0xfff50009fffffff5 in ?? () #6 0xffffffffffffffeb in ?? () . . .
CAENVME_ReadCycle()
, но вызванный откуда -- хбз; точно
не из irq_fd_p()
, поскольку там перед ним ещё много чего,
сопровождаемого диагностической печатью.
Вот как это драйвер так умудрился повлиять?
Что теперь делать -- хбз. Пересобрать с OPTIMIZATION=-O0, подобавлять отладочной печати, и надеяться, что эти меры не повлияют на факт вылета?
27.02.2021: как бы то ни было, вчера-сегодня прогнал по вариантам с наличием/отсутствием reset'ов:
CAENVME_DeviceReset()
в _fd_p():
Но и события так и не приходят (в _fd_p() reset закомментирован, но это уже вряд ли влияет).
09.06.2021: возвращаемся к теме (через 3 с лишним месяца, позор!), и даже открыто на p320t окно с 4 сессиями screen на x10sae (с именами/щаголовками s, c, p, m -- Server, Client, Ping, Messages; ради чего даже научился минимально пользоваться screen'ом -- ключики "-S x -t x", где "x" -- Session name и window Title).
Для тесте делаем пере-открытие адаптера по
cvConnectionRecovered
. Это заключается в сначала закрытии --
CAENVME_End()
с sl_del_fd()
-- а потом заново
открытии путём CAENVME_Init()
с sl_add_fd()
.
Так вот -- РАБОТАЕТ!!! После такого переоткрытия молочение продолжается.
...хотя -- ну кто бы сомневался...
НО: один конкретный результат всё же есть -- переоткрытие срабатывает СРАЗУ, так что никакая "задержка" реально значения не имеет.
10.06.2021: теперь опять тестируем разные варианты "ping'а":
a3818_test -x @0/0:32:0x09:0xd7000000
"i:0
" -- работает.
:1
-- нет.
:10000000 i:0 :10000000
-- ТОЖЕ нет!!!
i:0 :10000000
-- вычитывается по 50 прерываний (в сумме 100) и опять зависает.
:10000000 i:0
-- НИ-ЧЕ-ГО!
:1
" хватает для неработы...
i:0 :1
-- даёт эффект.
:100000
" -- 0.1с уже годится; а вот 1.0с --
нет.
0.5с тоже OK. И 0.6с тоже. А вот 0.7с -- уже нет.
Найденная граница -- 0.61с. Что бы это значило?!
Мысль: а если вернуть в
a3818_hal.h::irq_fd_p()
пере-открытие, но убрать
чтение?
11.06.2021: ну проверил -- да, автоматическое пере-открытие, но без чтения РАБОТАЕТ: прерывания восстанавливаются.
Пока что в голове есть единственная идея продвижения: попробовать воспроизвести все выполняемые по закрытию и открытию действия, но БЕЗ собственно закрытия и открытия.
Также обнаружилась странность: утечка файловых дескрипторов.
...точнее -- примерно ЧЕРЕЗ раз.
Попозже: а потом -- перестали. Доехали с 13 до 18, и на нём остановились.
И ещё: возникла идея проверить, будет ли работать пере-открытие, если карточка -- /dev/a3818_0 -- будет открыта ещё кем-то (т.е., reference count на линию НЕ будет ==0).
Что, если подумать, было предсказуемо -- утилита отбирала у сервера прерывания, ловя их раньше.
while true;do;sleep 1000000;done <>/dev/a3818_0
-- всё ОК.
И да, восстановление ПРОИСХОДИТ. ...ну и кто б сомневался -- обычный-то "ping" делался как раз отдельной программой, при уже открытом устройстве.
12.06.2021@утро: а с другой
стороны -- зачем? СЕЙЧАС-то базовая ситуация стала понятна, и делать всё
надо уже именно в irq_fd_p()
...
12.06.2021: @завтрак: ещё мыслишка: а зачем прямо ПЕРЕоткрывать? А если попробовать просто ЛИШНИЙ раз открыть и потом закрыть -- open("/dev/a3818_0"); close()?
И для начала это можно проверить прямо из консоли -- командой вроде
sleep 1 <>/dev/a3818_0
И-и-и...
И при четвёртом аналогично.
И более того -- самого события потеря/восстановление НЕ СЛУЧАЕТСЯ!!! Так что по сути, и пинговать-то нечего...
13.06.2021: решил сделать "ручной тест" (утилитку
a3818_u.c) -- проверить CAENVME_IRQCheck()
'ом, горять
ли какие-нибудь прерывания. Результат:
CAENVME_IACKCycle()
--
возвращает -1
.
Учитывая, что эта операция сводится к реальному обмену с VME-контроллером и попытке выполнения IACK-цикла -- можно сделать вывод, что прерывания куда-то теряются.
А может, правда просто железка глюканула?
...а может, оно и С САМОГО НАЧАЛА так подглючивало, и я почти полгода пытался разобраться с аппаратным глюком, считая это лишь косяком реализации?
14.06.2021: передёрнул питание у крейта (ТОЛЬКО у крейта -- НЕ у компа! и даже драйвер не перегружал, и даже сервер продолжал быть).
И, придя домой, рестартовал сервер -- он так и висел, а в логах были строчки
-- именно 2 attempt(s), а не 1, как обычно; да и строчек "Timeout" 2 штуки подряд.Jun 14 19:08:34 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 0 Jun 14 19:08:34 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 0 Jun 14 19:08:34 b360mc kernel: a3818: rcv-pkt: Link 0 recovered in 2 attempt(s) s->timeout=500 2j(15000)=15000 2j(100)=100
15.06.2021: проверил -- да, пашет, уже больше полусуток (это USE320=no).
16.06.2021: попробовал ещё раз с USE320=YES.
...хотя и словило -- сразу же! -- событие потеря/восстановление. Это, видимо, оставалось в драйвере запомненное от того дёрганья питания.
18.06.2021: взял (у Бобровникова через ЕманоФедю) второй экземпляр -- крейт+контроллер+адаптер, буду проверять отдельно на нём.
Но ещё Евгений Симонов сегодня ответил на моё письмо от 15-06-2021 на тему "Вопрос про работу VsDC4 через CAEN'овский интерфейс на ВЭПП-4":
Поведение CAEN контроллера действительно очень странное. После изменения прошивки VsDC4 (чтобы добавить калибровку) работа по IRQ перестала работать - больше половины прерываний стали теряться. Проверка показала, что CAENVME_IRQCheck() работала правильно, но функция CAENVME_IACKCycle() выдала неправильное значение вектора. Т.е. прерывания не терялись, но значения векторов часто искажались и поэтому программа теряла их. Сейчас после повтороного обновления прошивки этих проблем нет. Год назад, когда только начали работать с модулями VsDC3 и VsDC4, были сбои при чтении/записи регистров этих модулей. После "доработки" модуля VsDC4 проблемы исчезли. Автор модуля VsDC писал об этом. Антон Владимирович Павленко <A.V.Pavlenko@inp.nsk.su> писал 2021-05-31 17:20: > Кроме того, когда интеграторами занимался Чеблаков с студентом > выяснилось что CAEN в отличии от контроллеров > BIVME-2 (ИЯФ) и MVME-3100 (на основе VME-моста TSI-148) читает регистры > интегратора со сбоями, > что выливается в "провалы" на осциллограммах. > У меня возникла гипотеза почему так и я увеличил задержку между > установлением данных на шинt и выдачей сигнала DTACK.
Т.е., проблема вообще может быть и в ADC250!!! Надо сравнить версии прошивки в моём тестовом крейте и в клистронке (где вроде бы всё обновлялось буквально недавно -- зимой или весной).
Чуть позже: сравнил -- неа, совпадает: везде PGA_SW_VER=0x4, BASE_SW_VER=0x6, PGA_HW_VER=0x03, BASE_HW_VER=0x02.
20.06.2021: да, поставил новый крейт с новым контроллером. Получилось приключенчески...
Посему поставил 2-портовую вместо исходной 1-портовой.
i:0
вычитывается слово 0x4604ADC0.
"Поменял местами" -- воткнул кабель из 0-го порта
А потом заметил, что просто недовоткнул оптический кабель в разъём.
После втыкания -- работает!!! (из VADC16 по s:0
и
s:2
вычитываются 0x0000)
21.06.2021@утро: прогон под strace показал, что эти 3 секунды проводятся внутри
ioctl()
'а с кодом 1 -- IOCTL_COMM
.
Т.е., как будто "чувствовало", что что-то подключено (и НЕ давало ошибку сразу), но реально связь не работала.
CAENVME_Init()
выполняется какой-то обмен с
крейтом. Возможно, что именно это и есть то действие, которое "развешивает"
работу прерываний.
Это, правда, и так-то как бы "не проблема", поскольку нынешние драйверы
предполагают наличие/доступность устройств при старте -- в
_init_d()
-- и при отсутствии просто не смогут нормально
отработать (а ADC250'шный вообще просто вернёт -CXRF_CAMAC_NO_X
при ошибке чтения). Так что сейчас всё равно придётся делать всем рестарт
по "._devstate=0".
...мда, традиционно с отсутствием/потерей/восстановлением связи связаны сложности...
Смысл -- чтобы отвязаться от потенциально "некорректно" (по идее от Симонова) работающих ADC250. Т.е., уменьшить количество компонентов, в которых могут быть проблемы/ошибки.
23.06.2021: в ИЯФе должны быть вот такие модули (мыло от Беркаева за 11-09-2020 от Логашенко, форварднутое 14-09-2029 Беркаевым):
9. дискриминатор V895B - 2 шт. 10. универсальный программируемый логический блок V2495 - 2 шт. 11. плата-расширение A395D для V2495 - 4 шт. 12. аналого-цифровой преобразователь V1785NC - 2 шт. 13. оцифровщик V1730SB - 2 шт. 14. оцифровщик V1742B - 2 шт. 15. оцифровщик V1720G - 2 шт. 16. таймер V993C - 2 шт. 17. время-цифровой преобразователь V1190B - 2 шт.
И в количестве 2 штук будем иметь примерно то же самое (хотя и с существенно меньшим объёмом обмена после IRQ).
21.06.2021: попробовал. Хренушки -- какой-то косяк вылез, явно уже МОЙ собственный: первое устройство открывается нормально (и из него вычитывается вся карта памяти), а вот на второе говорится
vme_hal_open_bus(1,0) failed: Device or resource busy
Чего оно вообще полезло второй раз ШИНУ открывать? Надо разбираться.
22.06.2021@утро:
разобрался -- в vme_lyr_common.c, от 18-02-2020, в vme_add()
при регистрации новооткрытой шины в busi[]
было забыто
сохранение туда значений bus_major
,bus_minor
. Вот
и считалась эта ячейка за шину (0,0) -- почему ранее проблема и не
проявлялась.
22.06.2021: запустил пару VADC16.
Надо бы поразбираться -- смотреть на изменения счётчика пришедших
прерываний, командой "grep 138: /proc/interrupts
".
23.06.2021: по прошествии суток всё ещё работает и не было ни единой потери связи.
23.06.2021: идея насчёт обычного тестирования "зависаний" прихода прерываний от ADC250: а если попробовать сделать "рестарт" осциллографу -- т.е., adc250*._devstate:=0, чтобы драйвер сделал сброс и заново бы запросил работу?
...возможно, эта идея и раньше приходила (надо бы порыться в записях).
Но тут она будет полезна при "замерзании", а не Timeout/recover.
23.06.2021: пытаемся разобраться с поведением VADC16 касательно прерываний.
_term_d()
генерация прерываний вроде корректно
отключается, так что для набора статистики оставляем ОДНО устройство,
выключив второе по a2._devstate=-1.
sleep 100
).
Канал adc0 за такие же 100 секунд обновился 13851 раз (данные по
"cdaclient -T100 b360mc:2.a0.adc0 | wc
").
При второй попытке эти числа составили 735 и 14138 раз, что уже странновато.
Поэтому сразу повторяем за 1000 секунд: получилось 6473 прерывания и 142950 обновлений канала adc0.
...и вторая попытка -- 6433 и 144318 раз.
...время интегрирования между режимами 0 и 7 отличается в 160 раз; количество прерываний более-менее сходится (есть ведь погрешность), а вот измерений -- расхождение ВДВОЕ.
Вторая попытка: 47 прерываний и 1880 раз.
_irq_p()
-- чтобы "визуально увидеть" картину прихода
прерываний.
@вечер: попробовал -- мда, полный капут: на каждое прерывание даётся по 200 (хбз, почему не 100) "подтверждений" по IACKCycle(). Причём номера векторов там самые наипроизвольнейшие: значительная часть -- "правильные", что были запрограммированы в устройства (50 и 52); бОльшая часть -- 255; ещё часть -- просто какие-то бредовые 179, 181 и 53...
Потестировал утилиткой a3818_u (перенастроенной на Link=1 и cvIRQ7): да-а-а, цикл IACK в любой момент проходит успешно, несмотря на негорящесть соответствующего прерывания...
Чуть позже: понял, почему именно 200, а не 100: "горят" ДВА прерывания -- 6 и 7 (от двух АЦП).
24.06.2021: сделал -- теперь цикл не до 100, а до 1.
Не особо-то помогло -- вектора вычитываются через раз бредовые: на IRQ6 вместо 50 бывает 51, 179, а на IRQ7 вместо 52 бывают 53. 181, 255.
Ну теперь бы надо ещё работу ADC250 в таком режиме "без repcount" проверить.
28.06.2021: Козак ответил на сегодняшнее письмо с вопросом "а нет ли обновлённой прошивки для VADC16?" -- нет, "для VADC16 существует одна версия прошивок.".
Так что для отладки работы прерываний придётся искать какой-нибудь другой их источник.
08.07.2021: а вот и нет -- ЕСТЬ новая прошивка!
24.06.2021: очередной раунд забав с A3818 с парой ADC250: в режиме USE320=YES и "в режиме без repcount"...
Результат обескураживающий:
CAENVME_End()
+CAENVME_Init()
таки доползли до
1023, после чего CAENVME_Init()
стал возвращать
err=-2/cvCommError.
А дескрипторы все те смотрят на /dev/shm/CAENV1718_LCK_100.
08.07.2021: пробуем добавить диагностики для "замерзания" по полученному сегодня от Павленко совету (см. выше).
vme_lyr_irq_cb()
добавлена
печать векторов, для которых нет получателей.
09.07.2021: фигушки -- за вечер и ночь так ничего не "замёрзло".
09.07.2021: ещё вчера договорился с Павленко, что сегодня обменяю свои 2 штуки на 2 перепрошитых. Но, поскольку глюка пока добиться не удалось (а хочется, чтоб проверить с диагностикой!), а они всё равно до конца июля все уезжают в отпуска и Снежинск, то передоговорился просто взять 2 дополнительных -- я их поставлю во 2-й крейт вместо VADC16 (от которых всё равно проку не вышло).
Взял. Поставил. А дома обнаружил, что cxsd SIGSEGV'нулся! Т.е., запущенный на крейт/линк 0, он почему-то гикнулся при передёргивании питания крейта/линка 1. И, кстати, в /var/log/messages ОЧЕНЬ много сообщений о потере/восстановлении связи.
Записать о взятых у Павленко 2 перепрошитых ADC250 0x96 и 0x98, у которых endianness данных сменена. И о том, что можно бы сделать "перепрошивальщик" -- описания вроде так и не нашлось, но есть исходники ихнего в git'е.
10.07.2021: попробовал немножко поэкспериментировать -- что/как/почему падает. Результаты презабавны:
Проверяется просто: запускаем cxsd на 0-й линии, а потом сканирование по 1-й, командой (только в одну строку)
-- так у присутствующих по нужным адресам ADC250 показываются прочитанные первые 10 слов (можно искать по "0x4604ADC0"), а на отсутствующие лишь "Bus error".for i in {0..255};do;BASE=`printf '0x%08x' $[i<<24]`; echo BASE $BASE; LD_LIBRARY_PATH=~user/compile/CAENVMELib-3.2.0_sources.new ~user/work/hw4cx/drivers/vme/a3818/a3818_test -x @1:32:0x09:$BASE i:0/10; done
-D
-- чтоб не перехватывал сигналы, а падал бы и писал бы
core-файл.
CAENVME_ReadCycle()
, а дальше -- какой-то
мусор.
...кстати, подобное же поведение -- немотивированные SIGSEGV'ы при вроде бы безобидных попытках модификации кода -- наблюдались и полгода назад.
CAENVME_ReadCycle()
сводится к заполнению буфера
отправки и потом вызова _CAENVMECommEx()
.
Оный же сводится к вызову ioctl(,IOCTL_COMM,)
, передавая
туда указатели на буфера отправки и приёма и размеры оных буферов.
Но:
CAENVME_ReadCycle()
передаёт указатель на СВОЙ
ВНУТРЕННИЙ буфер, и уже оттуда копирует в юзерский.
(Хотя если мимо его буфера промахнуться -- то и юзерские данные/call-frame выше по стеку можно испортить.)
CAENVME_BLTReadCycle()
передаёт указатель уже
прямо на ЮЗЕРСКИЙ буфер. Но падает-то не в нём.
(Хотя если пакет-портельщик получается раньше, то порча может происходить именно в BLT-операции и просто проявляться позже.)
Правда, это сильно изменит временнУю диаграмму, но всё равно попробовать можно -- а вдруг.
a3818_ioctl()
в ветке IOCTL_COMM
отсутствует проверка размера приёмного буфера -- comm.in_count
(кроме простой >0 -- что вообще что-то принимать будут), а просто
a3818_recv_pkt()
(она читает в свой
внутренний буфер s->app_dma_in[opt_link][slave]
,
аллокированный посредством vmalloc(1024*1024)
-- 1 мегабайт)...
copy_to_user()
, которому "куда" указывается
comm.in_buf
, а "сколько" -- просто объём ПРИНЯТЫХ данных, без
учёта размера приёмного буфера.
11.07.2021: пытаемся исправить тот косяк в драйвере с непроверкой размера приёмного буфера.
copy_to_user()
'у значение
"сколько"="сколько_ПРИНЯТО" делается проверка.
bytes_to_copy
: в неё сначала складывается
объём, предполагаемый к копированию, а потом
bytes_to_copy > comm.in_count
,
то опять же printk() этих размеров и bytes_to_copy=comm.in_count
.
-- такая цепочка-паттерн повторяется, иbytes_to_copy=8704 > comm.in_count=8192 bytes_to_copy=8192 > comm.in_count=512 bytes_to_copy=8192 > comm.in_count=7680 bytes_to_copy=7684 > comm.in_count=64
-- это поштучно и довольно часто. Как раз отлично подходит как причина порчи стека.bytes_to_copy=8196 > comm.in_count=64
Вместо падений теперь отладочные сообщения
(адреса заканчиваются на "24" потому, что читается со смещения ADC4X250_TIMER_PRETRIG/2*4, где "PRETRIG" -- это 18 точек ДО триггера; 18/2*4=36==0x24.)vme_hal_a32rd32v(0x09:d5800024)=-2 vme_hal_a32rd32v(0x09:d5a00024)=-2 vme_hal_a32rd32v(0x09:d6a00024)=-2 vme_hal_a32rd32v(0x09:d5400024)=-2
Надо будет перепроверить на ихнем неправленом мною драйвере -- будет ли падать, и если "да", то сделать патч для него и потом diff отправить им в багрепорте.
Теперь понять бы ещё, а с какого лешего оно умудряется на линке 0 реагировать на активность на линке 1 -- т.е., как будто вообще чужие пакеты принимает.
12.07.2021: перепроверил: взял ихний неправленый
драйвер (это по-прежнему A3818Drv-1.6.4-build20210118.tgz); в оригинальном
варианте -- сервер на 0-й линии SIGSEGV'ится при доступе к 1-й линии, а в
исправленном -- логгирует избыточно большие пакеты и
CAENVME_BLTReadCycle()
возвращает -2=cvCommError.
Пора багрепорт писать.
13.07.2021: багрепорт написан, его текст в notes/20210713-CAEN-A3818Drv-1.6.4-in_count-BUGREPORT.txt; патч приаттачил. Бум ждать ответа.
И часа не прошло: ответил живой человек, и присвоен Ticket#1043523.
...а пока надо бы попробовать подготовить test-case для условий появления бага. Оный тест должен состоять из 2 программ, написанных на чистом CAENVMElib'е, одна из которых пытается часто-часто читать из крейта на 0-м линке, а вторая на другом. ...как вариант -- ОДНА программа, которой параметрами указывать линк и базовый адрес.
14.07.2021: вчера написал тестовую программку -- ОДНУ, work/tests/test_CAENVME_BLTReadCycle.c.
Далее проверки.
Т.е., 1-я, "основная" программа читает реальную память реального устройства BLT-операциями по 8K слов, а 2-я, "мешающая", пытается читать по 1000 слов другого устройства. При этом частенько -- по несколько раз в минуту -- вознкают ошибки: сначала "прилетает" oversize-пакет, а потом таймауты на линках 0 и 1.
Тогда ошибки начинают валить почти постоянно, и уже сами по себе, а overlong-пакеты бывают очень изредка. Но зато "основная" программа начинает очень часто выдавать диагностику "CAENVME_BLTReadCycle()=-2", чего в первом варианте почти не бывает.
20.08.2021: и на это отправлен багрепорт, текст в notes/20210820-CAEN-A3818B-LINK-INTERFERENCE-BUGREPORT.txt, исходник test_CAENVME_BLTReadCycle.c приаттачен, но там, похоже, фильтрация по расширениям (только gif,jpg,png,pdf,doc,docx), так что выложено ещё на www.inp.nsk.su/~bolkhov/.
24.08.2021: откликнулись-таки, Ticket#1043704. Сама реакция грустная -- подробнее ниже.
23.08.2021: решил ещё немного порыть по вопросу "когда что происходит".
strcurtime_msc()
-- чтобы события ошибок
префиксировались временами, дабы при разбирательствах "потом", по простыням
сообщений, можно б было видеть, когда именно что происходило (а не гадать
"при второй ли программе? или просто само спонтанно?") и можно б было
сопоставлять с временами событий в /var/log/messages.
-- по 11-12 пар в секунду. Это началось в субботу "Aug 21 08:44:49", причём всё это время было запущено лишь BLT-чтение (ещё с пятницы 20-го, когда тесты для багрепорта делал), БЕЗ "мешающего". А первым сообщением было другое --Aug 22 03:10:01 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=32768 > comm.in_count=31232 Aug 22 03:10:01 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=32768 > comm.in_count=1536 Aug 22 03:10:02 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=32768 > comm.in_count=31232 Aug 22 03:10:02 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=32768 > comm.in_count=1536 Aug 22 03:10:02 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=32768 > comm.in_count=31232 Aug 22 03:10:02 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=32768 > comm.in_count=1536 Aug 22 03:10:02 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=32768 > comm.in_count=31232 Aug 22 03:10:02 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=32768 > comm.in_count=1536 Aug 22 03:10:02 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=32768 > comm.in_count=31232 Aug 22 03:10:02 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=32768 > comm.in_count=1536 Aug 22 03:10:02 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=32768 > comm.in_count=31232 Aug 22 03:10:02 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=32768 > comm.in_count=1536
Aug 21 08:44:49 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=34304 > comm.in_count=32768
После Ctrl+C программе BLT-чтения оно прекратилось и после перезапуска более не возобновлялось.
Возникла мысль: возможно, такое происходит не с любым линком, а с тем, по которому идёт BLT-обмен; с "раздражающим" же этого не бывает.
Проверил -- да, так и есть; причём как с "обычным" тестом, когда "раздражает" просто короткое чтение, так и с "оперативным", когда читаются неотвечающие адреса.
23.08.2021: откликнулись из CAEN'а на багрепорт по взаимозависимости линков, Ticket#1043704; грустно то, что лейтмотив -- "ачётакова?":
We are aware that there could be some computers where communication timeouts could be more frequent, for this reason a loop of recovery calls was added in the driver. The prints you see were added for debug purposes, and, from the log you sent, it seems that there is always a successfully recovery after the timeout. If the recovery went fine, also the data should be fine. Could you please check that your collected data is correct and that you are not missing any data?
Надо побыстрее откликнуться, чтобы дело продвигалось.
24.08.2021: пишем ответ.
25.08.2021: ответ дописан, отправлен. Ждём реакции :)
25.08.2021: реакция воспоследовала:
Комп-то я поищу, а вот с обновлятором прошивки странно:
...но до неё так и не дошло, т.к...
Прогон под strace
показал, что оно почему-то пытается искать
библиотеку ТОЛЬКО в фиксированном месте, причём в странном -- в
/usr/lib/gcc/x86_64-redhat-linux/4.8.5/; потом, правда, НАХОДИТ --
через "../../....", в /usr/lib64/, но всё равно чем-то она его не
устраивает.
Может, надо реально скачать ихнюю дистрибутивную 3.2.0?
@вечер, около 22:50: да, скачал и поставил, но дело было не в этом: проблема была в том, что оно НЕ находило .h-файлы, а внятно сказать об этом ВОВРЕМЯ -- не могло, и говорило лишь "потом кумулятивно". В конечном итоге помогла такая командная строка:
CPPFLAGS="-I${HOME}/compile/CAENVMELib-3.2.0/include -I${HOME}/compile/CAENComm-1.4.1/include" ./configure
(также пришлось скачать и "поставить" некий CAENComm -- "библиотеку для
коммуницирования через разные интерфейсы"(?)).
Так что в конечном итоге оно сконфигурилось и собралось. Кстати, как-то обошлось без Java.
Для наших целей требуется
CAENBridgeUpgrade/src/CAENBridgeUpgrade
.
Правда, КАК именно им пользоваться -- отдельный вопрос: формат вызова там какой-то неочевидный. Видимо, читать документацию.
25.08.2021: попробовал ещё погонять обычный тест с 2 ADC250, но уже с проверкой размера принимаемого пакета. Результаты:
sleep 1 <>/dev/a3818_0
"
26.08.2021: пытаюсь разобраться с прошивками.
main()
:
CAENBridgeUpgrade A3818 0 FILE_TO_STORE_VERSION fwrev
-- тут: "0" -- видимо, номер карточки (но явно НЕ PCI-ID), "fwrev" -- такое
ключевое слово (вот не могли '-'
-ключик сделать, балбесы...).
И результат оно пишет в ФАЙЛ; так что проще вместо него указывать
/dev/tty.
На уровне библиотеки выполняется чтение прошивки вызовом
CAENVME_BoardFWRelease()
, для конкретно A3818 сводящемся к
чтению регистра (A3818_FWREV | 0x1000)
и складированию
результата sprintf()
'ом (sic!) в строковый буфер по формату
"%01u.%02u".
Надо бы добавить вычитывание и печать в
a3818_hal.h::vme_hal_open_bus()
, чтобы не полагаться
на этот дислексичный Upgrade'р.
27.08.2021: а вот нет, без толку то будет: ведь КЛИЕНТСКИЕ
программы (включая a3818_hal.h) открывают соединение с
bdtype=cvV2718
, а CAENBridgeUpgrade -- с
bdtype=cvA3818
(ибо работает с PCIe-адаптером, а не с
крейт-контроллером). И делают они РАЗНЫЕ вещи: для A3818 -- вышеупомянутое
чтение локального регистра (сводящееся к ioctl()
'у, просто
читающему по PCI), а для V2718 -- _CAENVMEReadREG(dev, FWREV,)
,
приводящее к коммуникации с крейт-контроллером. И конкретно
test_CAENVME_BLTReadCycle.c вызов делает -- получает
"2.17".
30.08.2021: в переписке Paola Garosi из CAEN очень советовала всё же обратить внимание на прошивку.
31.08.2021: я сходил в ИЯФ ногами и посмотрел на карточку вживую. Результаты забавны:
-- т.е., РОВНО эта проблема: путаются IRQ/пакеты от разных линков!!!=============================================================================== Release 0.6 (20/07/2017) =============================================================================== Bug fix: * Fixed a bug on the arbitration of the interrupt requests from different links
01.09.2021: проверяем работу прерываний с прошивкой 0.6 (aka "0.06"). Вкратце -- нет, лучше не стало.
(это "моя" версия драйвера, с диагностикой на тему jiffies).Sep 1 09:43:14 b360mc kernel: a3818: rcv-pkt: Link 0 bytes_to_copy=6144 > comm.in_count=4096 Sep 1 09:43:14 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 0 Sep 1 09:43:14 b360mc kernel: a3818: rcv-pkt: Link 0 recovered in 1 attempt(s) s->timeout=500 2j(15000)=15000 2j(100)=100
sleep 1 <>/dev/a3818_0
", но работает чтение 1 слова,
причём как успешное, так и по немаппированному адресу.
02.09.2021: ровно сутки спустя: всё ровно, ноль проблем.
-- и вот так, парами, до бесконечности.Sep 1 08:18:20 b360mc kernel: a3818: rcv-pkt: Link 0 bytes_to_copy=32768 > comm.in_count=24576 Sep 1 08:18:20 b360mc kernel: a3818: rcv-pkt: Link 0 bytes_to_copy=32768 > comm.in_count=8192 Sep 1 08:18:20 b360mc kernel: a3818: rcv-pkt: Link 0 bytes_to_copy=32768 > comm.in_count=24576 Sep 1 08:18:20 b360mc kernel: a3818: rcv-pkt: Link 0 bytes_to_copy=32768 > comm.in_count=8192
Закончились они при "kill -STOP" обоих процессов чтения.
02.09.2021: продолжаем тестирование (теперь уже всё с версией 0.6, конечно).
Несколько часов простояло -- хоть бы хны.
За полчаса -- также ничего.
Да -- есть!!! Меньше чем через минуту!
Ещё чуть позже: фиг!!! Такое ощущение, что оно не "через минуту", а прямо СРАЗУ, при старте (результат открывания линии?); в дальнейшем же -- НЕ воспроизводится.
05.09.2021: а далее происходило странное: оба ADC250 перешли в какой-то странный режим, при котором они как бы висят, но раз в несколько часов измерение cdaclient'у всё же приходит.
05.09.2021:
b360mc:/home/user# grep -i lower /var/log/messages* /var/log/messages-20210905:Aug 29 05:21:45 b360mc kernel: perf: interrupt took too long (2504 > 2500), lowering kernel.perf_event_max_sample_rate to 79000 /var/log/messages-20210905:Aug 29 18:09:24 b360mc kernel: perf: interrupt took too long (3146 > 3130), lowering kernel.perf_event_max_sample_rate to 63000 /var/log/messages-20210905:Aug 30 10:38:35 b360mc kernel: perf: interrupt took too long (3947 > 3932), lowering kernel.perf_event_max_sample_rate to 50000 /var/log/messages-20210905:Aug 31 10:23:11 b360mc kernel: perf: interrupt took too long (4948 > 4933), lowering kernel.perf_event_max_sample_rate to 40000 /var/log/messages-20210905:Sep 4 18:40:00 b360mc kernel: perf: interrupt took too long (2511 > 2500), lowering kernel.perf_event_max_sample_rate to 79000 /var/log/messages-20210905:Sep 4 21:57:57 b360mc kernel: perf: interrupt took too long (3161 > 3138), lowering kernel.perf_event_max_sample_rate to 63000 /var/log/messages-20210905:Sep 5 02:36:19 b360mc kernel: perf: interrupt took too long (3962 > 3951), lowering kernel.perf_event_max_sample_rate to 50000
Замечание: это драйвер С ПОДДЕРЖКОЙ poll/select.
CAENVME_BLTReadCycle()=-2
vme_hal_a32rd32(0x09:d5000014)=-2
а в /var/log/messages при этом попадает 4 строчки:
причём эти строки ВОСПРОИЗВОДЯТСЯ -- числа почти каждый раз те же самые ("почти" -- потому, что когда чуть другие (и всего 2 строчки), то сервер ошибки НЕ получает).Sep 5 15:17:01 b360mc kernel: a3818: rcv-pkt: Link 0 bytes_to_copy=32768 > comm.in_count=64 Sep 5 15:17:01 b360mc kernel: a3818: rcv-pkt: Link 0 bytes_to_copy=32768 > comm.in_count=64 Sep 5 15:17:01 b360mc kernel: a3818: rcv-pkt: Link 0 bytes_to_copy=32768 > comm.in_count=32764 Sep 5 15:17:01 b360mc kernel: a3818: rcv-pkt: Link 0 bytes_to_copy=32772 > comm.in_count=4
15.09.2021: некоторые результаты за прошедшее время:
IOCTL_COMM
-- и,
видимо, на него (при нахождении в kernel space) Ctrl+Z тоже действует.
Почему получается именно таймаут, а не просто пакет (который за это время гарантированно должен был прилететь) -- фиг знает, это надо отдельно разбираться.
И некоторые идеи за то же прошедшее время:
CAENVME_IRQCheck()
.
Возможно, как раз он и перекрывается по времени с каким-то другим пакетом в линке, которого типа никто не ждёт, а на него самого ответ теряется.
CAENVME_IRQCheck()
?
Хоть просто постоянно в цикле беспрерывно, хоть перемежая с чтением.
16.09.2021: ну типа сделал --
test_CAENVME_IRQCheck.c, идентичный предыдущему, но после каждой
операции чтения (и BLT, и не-BLT) добавлен вызов
CAENVME_IRQCheck()
.
Результат -- НИ-ЧЕ-ГО.
Позже@спускаясь по лестнице с 10-го этажа, в районе 15:30: а ведь если повспоминать -- проблема возникает при "правильной" ловле IRQ от ДВУХ устройств; с одним же всё работает нормально. Вывод: надо как-то запустить девайсы, чтоб они молотили прерываниями, и постоянно IRQCheck()'ить!
16.09.2021: Paola посоветовала:
Concerning the issue, the designer suggested to try to modify the following setting in the driver code: from USE_MSI_IRQ equal to 1, which is the default value, to 0 and see whether the error occurs again or not.
Ну попробовал, да. А фиг -- никаких отличий, точно так же с USE320=YES быстренько перестаёт...
17.09.2021: явно нужно лепить тестовую программку, которая бы имитировала работу сервера: инициализировала и программировала бы эту пару ADC250, а потом в цикле постоянно бы пыталась ловить IRQ и по получению -- смотрела бы, кому оно, "тормозила" бы (и "вычитывала), а затем опять рестартовала бы устройство.
Под "ловить IRQ" понимается для начала CAENVME_IRQCheck()
--
раз есть подозрение, что проблема в слишком быстро (сразу после прихода
пакета "IRQ!") производимых каких-то действиях.
20.09.2021: за выходные 18.09.2021-19.09.2021 сделано -- test_CAENVME_adc250s.c -- и проведено некоторое количество вариантов проверки.
Сделано так:
devtype_t
, в котором
указываются AM, смещения для записи номера линии IRQ и вектора, а также
таблицы команд для инициализации, старта, останова.
devinfo_t
,
содержащий ссылку на экземпляр devtype_t
и значения базового
адреса, номера линии и вектора IRQ.
Список устройств для работы содержится в массиве devlist[]
(нет, в командной строке указывать в стиле
"TYPE_NAME:BASE_OFFSET:IRQ_LINE:IRQ_VECT" не будем; хотя мысль такая была
:D).
main()
.
while(1)
делает CAENVME_IRQCheck()
,
CAENVME_IACKCycle()
, а потом
выполняет поиск полученного вектора по devlist[]
'у и при
находжении --
CAENVME_IRQEnable()
1 раз вначале и
потом в конце тела цикла while(1) при условии, что полученная маска
ненулевая.
Результаты тестов:
Тоже без толку.
select()
-- скопированное из caenvmelib_poll_test.c()
-- так тут же начало срубаться!!!
И точно те же способы пингования работают (и НЕ работают тоже).
21.09.2021@утро: похоже, есть ДВЕ проблемы:
21.09.2021: продолжаем разбирательство:
Тогда вариантом стресс-теста может быть не IRQCheck() в while(1), а IACKCycle()?
@вечер: неа, этот вызов ЛОКАЛЬНЫЙ, просто чтение регистра из PCIe-карточки, БЕЗ работы с оптикой...
Впрочем, в свете гипотезы "ДВЕ разных проблемы" всё выглядит разумно: срубается РЕЖЕ, а как восстановиться -- пока не придумано, но надо бы.
22.09.2021: провёл ещё некоторое количество тестовых запусков -- причём длинных, и результаты таковы:
23.09.2021: думаем дальше...
CAENVME_IRQWait()
.
Ну так и использовать в стресс-тесте оный Wait()!
CAENVME_IRQWait()
вставлен в
#if
перед/вместо select()
'а.
Собственно, при ОТСУТСТВИИ -- в пределах часа-двух. А при наличии -- проработало полсуток и хоть бы хны.
24.09.2021: надо дотестировывать и писать Паоле с test-case'ом.
devtype_p
добавлены
поля blt_read_count
и blt_read_count
, и чтение
выполняется в случае, если первый параметр >0.
CAENVME_BoardFWRelease()
(который лазит в
крейт-контроллер) закомментил -- всё равно работа возобновляется.
sleep 1 <>/dev/a3818_0
" -- НЕ
помогает.
26.09.2021: ещё некоторое количество тестов и разбирательств:
CAENVME_IRQCheck()
в ветку "TIMEOUT".
Результат предсказуем: ну да, печатается irq_mask=16 (т.е., 0x10, что и есть
IRQ5).
Т.е., Wait() висит, несмотря на наличествующее IRQ.
IOCTL_IRQ_WAIT
, реагирует только на ФРОНТ, а вот на УРОВЕНЬ --
НЕТ: там есть только wait_event_interruptible_timeout()
, БЕЗ
какой-либо проверки текущего статуса (каковая обязательно производится при
использовании select()/poll(), первым же шагом).
Т.е., если уже есть готовое к обработке прерывание, то оно замечено всё равно не будет, а произойдёт подвисание в ожидании следующего.
Но такой алгоритм не может работать надёжно в принципе: из-за неатомарности операции "проверка, зависание" между её частями есть зазор, в который если вклинится прерывание, то оно останется необработанным на всё время висения.
CAENVME_Init()
оказалось ДОСТАТОЧНО! БЕЗ
каких-либо операций В/В или даже задержек между открытием и закрытием!
open()
выполняется
1-2 ioctl()
'а. "1-2" -- потому, что...
futex()
и sched_yield()
.
Сами коды операций одинаковые --
_IOC(_IOC_READ|_IOC_WRITE, 0x38, 0x01, 0x28)
. Похоже -- по
A3818Drv-1.6.4/include/a3818.h, что тут "0x38" -- это
A3818_MAGIC
(='8'); с остальным не вполне ясно, но,
видимо, IOCTL_REG_WR
.
27.09.2021: а, не, не так всё просто: это в логе strace операции одинаковые, а у ltrace значение последнего параметра ioctl()'а разное (0x-указатель у 1-го вызова и 0 у второго) плюс возвращается 2 и 4 соответственно.
CAENVME_Init()
после
открывания устройства явно значится
CAENVME_WriteRegister(i, cvVMEIRQEnaReg, 0xFF)
CAENVME_GetFIFOMode()
, должная
приводить к _CAENVMEReadREG()
, которая в конечном итоге должна
приводить к IOCTL_COMM
, которого почему-то в логе трассировки
Кстати -- а не на нём ли как раз и происходит ошибка на отсутствующих/невключенных крейтах?
27.09.2021: да -- именно на нём; и при указании отсутствующего крейта именно на ПЕРВОМ ioctl()'е подвисание на 3 секунды (3 секунды -- 6 попыток (1 основная и 5 retry'ев), таймаут 500мс, 6*500мс=3с)..
Возможно, это и есть то самое "заклинание", которое я безуспешно искал с начала января, которое бы восстанавливало нормальную работу.
И если "да", то даже если от CAEN'овцев не удастся добиться исправления общего идеологического косяка с ловлей прерываний, то для НАШИХ задач решение будет достаточным.
27.09.2021: далее...
CAENVME_GetFIFOMode()
-- результат нулевой, НЕ размораживается.
CAENVME_WriteRegister(i, cvVMEIRQEnaReg, 0xFF)
в test_CAENVME_adc250s.c'шную реакцию на таймаут... и ДА, ЭТО
СРАБОТАЛО!!!
Теперь оно сразу же восстанавливается и продолжает молотить дальше!
irq_fd_p()
-- ДА, ПОМОГЛО!!!
Молотит непрерывно. Да, ошибки валятся, причём весьма резво -- по нескольку штук в минуту. Но восстанавливается и продолжает молотить.
28.09.2021: ужасная мысль: а что, если проблема всё-таки как-то вызвана именно МОИМ вмешательством -- добавлением поддержки poll()?
Проверил -- да, стоковый 1.6.4 молотит без путаницы пакетов и таймаутов...
29.09.2021: окей -- в чём может быть МОЁ вмешательство?
Разве что только в использовании intr_wait[][]
для второй
цели (помимо IOCTL_IRQ_WAIT
).
Окей -- делаем вариант с ОТДЕЛЬНОЙ очередью, именуемой
slct_wait[][]
(по общей конвенции должно б было быть
poll_wait
, но это имя занято функцией ядра).
Проверяем -- пашет! Причём вполне беспроблемно, прямо СОВСЕМ...
30.09.2021: поскольку в сентябре появился A3818Drv-1.6.6, то портируем в него. И пробуем с ним...
bytes_to_copy
/ndata_app_dma_in
(т.е., без
избыточно длинных пакетов; что, впрочем, не говорит об отсутствии путаницы
-- неправильные пакеты могут быть слишком короткими).
01.10.2021: ещё немного статистики вдогонку:
Link 0 ndata_app_dma_in=3584 > comm.in_count=2048
...а дальше надо будет с 1.6.6.poll погонять test_CAEN_IRQWait.
02.10.2021@утро: за сутки так НИ РАЗУ и не было таймаута при 1.6.4.poll. Что
крайне странно, т.к. ФУНКЦИОНАЛЬНО они с 1.6.4 не различаются вовсе
(отличие там только в описателе a3818_procdir_fops
).
02.10.2021@вечер: однако один раз
"Link 0 ndata_app_dma_in=3584 > comm.in_count=2048
"
(те же числа!) таки было -- причём вскоре после выключения+включения крейта
на Link#1. Почему -- фиг знает, но вряд ли совпадение.
.
Откуда напрашивается вывод: всё же дело не в МОЁМ вмешательстве, а в том, что там как-то очень уж тонко настроенная диаграмма обмена пакетами (прям совсем-совсем "на тоненькую"): просто добавление некоторого действия -- оживления дополнительной очереди -- ломает-таки этот обмен.
02.10.2021: а лучше -- скриптик сделать, чтоб в цикле -запускал, через некоторое время "kill INT", и снова б запускал.
Надо бы оба способа хорошенько попроверять.
02.10.2021: попробовал выключить крейт и потом включить. "Душераздирающее зрелище!". А именно:
._devstate=0
.
Фиг -- НЕ помогло.
._devstate=-1
), а потом уже включение обоих.
Смысл -- чтоб layer освободил бы линию и закрыл бы её, а потом открыл бы заново.
Вот ЭТО -- ПОМОГЛО.
Выводы из этого эксперимента:
:10000000
"), и уже там посмотреть что будет при выключении во
время такой паузы.
Назвать его, видимо, "-k
" -- по аналогии с make,
"keep going" (мой вариант -- "Kontinue after errors" :D).
@22:00: сделано.
Для чего надо реализовать какой-то метод в API vme_hal и vme_lyr.
...о чём на границе 2019/2020 вроде бы уже были мысли. Чуть позже: ага -- 17-01-2020, метод
hal_ioctl()
.
А можно сделать АВТОМАТИЧЕСКОЕ ВОССТАНОВЛЕНИЕ:
02.10.2021@~22:00: а можно и сделать 1 канал записи -- принудительный "reset".
stat_proc
-- но хоть какое-то её использование отсутствует.
...хотя это "забудет" все сделанные настройки, так ведь?
@позже: но можно же из mux_write'а (или лучше из formula?) вместо _devstate:=0 обойтись как раз stop:=2, сразу после ре-инициализации, которую делать через специальный драйвер, имеющий единственный командный канал записи.
03.10.2021: провёл оба теста на тему "как бы получить SIGSEGV/путаницу пакетов путём создания нештатных ситуаций":
while true; do /net/x10sae/home/user/work/tests/test_CAENVME_IRQWait 0 &; sleep 2; killall test_CAENVME_IRQWait; done
Сначала делалось "kill %1" -- ради чего, собственно, и использовалась интерактивность (ибо zsh в интерактивном режиме включает job control), но что-то там получилось не так и завершился прямо сам shell (оно разлогаутилось).
В таком варианте с killall'ом тоже что-то не вполне слава богу: как
минимум последний из запущенных экземпляров test_CAEN* остаётся работать
@вечер:(а, ясно -- потому,
что прерывается sleep
, не доходя до
killall
'а!), а сначала (хбз при каких точно
обстоятельствах) программы вообще множились 04.10.2021:(ясно, при каких множились: неправильно была
указана спецификация kill'у, вот оно их и не завершало.).
(В любом случае, надо бы почитать "man bash" на тему JOB CONTROL, чтобы понять, как бы такой скриптик получше сделать.)
04.10.2021: почитал, да: правильный вариант -- не
"killall", а "kill %ИМЯ_ПРОГРАММЫ
"; причём ИМЯ_ПРОГРАММЫ должно
быть ПОЛНЫМ, с ПУТЁМ (типа "/net/x10sae/.../"), т.е., что-то вроде
while true; do /net/x10sae/home/user/work/tests/test_CAENVME_IRQWait 0 &; sleep 2; \ kill %/net/x10sae/home/user/work/tests/test_CAENVME_IRQWait; done
poll()
, а просто
с добавленной проверкой!) в лог попадает следующее:
Oct 3 15:53:13 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=2050 > comm.in_count=2 Oct 3 15:53:13 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=2050 > comm.in_count=2 Oct 3 15:53:19 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=2050 > comm.in_count=2 Oct 3 15:53:19 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=2050 > comm.in_count=2 Oct 3 15:53:25 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=2050 > comm.in_count=2 Oct 3 15:53:25 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=2050 > comm.in_count=2 Oct 3 15:53:31 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=2050 > comm.in_count=2 Oct 3 15:53:31 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=2050 > comm.in_count=2
Т.е.,
Такое впечатление, что CAEN'овцы вообще не задумывались о возможных проблемах потери связи и восстановлении.
Ну и подумать бы ещё -- вдруг можно ещё какой-то тест придумать, где бы всё таки что-нибудь бы вылезло... А если отрубать питание ВО ВРЕМЯ исполнения? КАК?
09.10.2021: ещё типа "результаты наблюдений за стабильностью":
Oct 9 16:52:48 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 0 Oct 9 16:52:48 b360mc kernel: a3818: rcv-pkt: Link 0 recovered in 1 attempt(s) Oct 9 16:52:49 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 0 Oct 9 16:52:49 b360mc kernel: a3818: rcv-pkt: Link 0 recovered in 1 attempt(s) Oct 9 16:52:49 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=3584 > comm.in_count=2048 Oct 9 16:52:49 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 0 Oct 9 16:52:49 b360mc kernel: a3818: rcv-pkt: Link 0 recovered in 1 attempt(s) Oct 9 16:52:50 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=3584 > comm.in_count=2048 Oct 9 16:52:50 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 0 Oct 9 16:52:50 b360mc kernel: a3818: rcv-pkt: Link 0 recovered in 1 attempt(s)
Вот сфига ли бы такое, а?!
Но есть некоторое отличие от предыдущих тестов: из
adc250_drv.c::FASTADC_IRQ_P()
был убран "авто-рестарт"
-- pzframe_drv_req_mes()
, но вместо этого всё это время
устройства работали в режиме "AUTO_RE_RUN", причём только те, что на "Link
0" -- потому, что на ту пару, что на "Link 1", не было сделано запросов.
Т.е., дело может быть в чуть ином тайминге? Странно это -- по факту в обоих
случаях делалось _req_mes()
почти сразу же, с гомеопатическими
отличиями (с точностью до return'а из _drdy() -- либо после него (раньше),
либо внутри него (сейчас)).
А ещё прикол: ВСЕ ошибки сейчас ВСЕГДА с "Link 0", а с "Link 1" не было вообще НИ РАЗУ (за последний месяц точно).
10.10.2021: вчера вечером оставил уже все 4 осциллографа молотить в режиме "AUTO_RE_RUN" -- и хоть бы хны!!! За почти сутки -- НИ ЕДИНОГО сбоя...
OK -- ставим все 4 молотить в режиме "ON_REQUEST" (первые 2 -- обычным стресс-тестом, вторые 2 -- лишним cdaclient'ом).
15.10.2021: прошло ещё 5 дней -- один фиг, ни разу больше не было ни единого сбоя.
22.10.2021: а вот теперь, через несколько перезапусков (последний из которых был как раз тогда, 15-10-2021!) -- вдруг обнаружилось, что сбои были. Причём в какие-то непонятные моменты: началось вчера, несколько раз за одну секунду (причём в тот момент *Я* никаких тестов не выполнял, судя по /var/tmp/4access.log), а потом ночью в довольно произвольные моменты, и закончилось всё сегодня в 08:43...
(Выдержка из лога тут ниже закомменчена)
28.10.2021: и опять, в течение небольшого времени, без какой-либо осмысленной связи с какими-либо действиями (в это время гонялся тест-перебор по 15 комбинациям запущенности ADC250), и опять только исключительно "Link 0". Но превышеющее число, кроме "3584", также бывало и "3072".
(Выдержка из лога также тут ниже закомменчена)
02.11.2021: а вот сегодня при очередном прогоне ПОЛУЧИЛИ аналогичные ругательства уже с "Link 1".
intr_wait[]
, а не с отдельным slct_wait[]
.
irq_fd_p()
, когда он пытается сначала
добыть все доступные вектора в буфер, а потом уже идти по этому буферу и
вызывать соответствующие обработчики.
...сопоставить бы времена возникновения ошибок с тем, какие паттерны отрабатывались в то время (выдержка и тут тоже закомменчена, на этот раз длиннющая, 98 строк); ошибки начались совсем не сразу при запуске прогона, а закончились ещё ДО его окончания (которое является как раз комбинацией всех 4 устройств).
10.11.2021: за прошедшие ещё 8 суток были ещё
многочисленные эпизоды. Впрочем, это всё с "неправильной" версией (с общим
intr_wait[]
); хотя и с 4 активными осциллографами и с
вычитыванием векторов в буфер.
Как бы то ни было, драйвер ядра опять заменён на 1.6.6 с
slct_wait[]
.
23.11.2021: чисто для статистики: за прошедшие 2 недели было ещё всего 2 сбоя -- 13-11-2021 и 22-11-2021, причём первый -- вообще БЕЗ переполнения, а второй с часто встречающимися числами:
Nov 13 13:30:01 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 1 Nov 13 13:30:01 b360mc kernel: a3818: rcv-pkt: Link 1 recovered in 1 attempt(s) . . . Nov 22 20:30:01 b360mc kernel: a3818: rcv-pkt: Link 1 ndata_app_dma_in=9216 > comm.in_count=8192 Nov 22 20:30:01 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 1 Nov 22 20:30:01 b360mc kernel: a3818: rcv-pkt: Link 1 recovered in 1 attempt(s
13.12.2021: и ещё для статистики, за прошедшие ещё 2 недели ещё 2 сбоя, логика возникновения по-прежнему неясна:
Nov 29 04:55:01 b360mc kernel: a3818: rcv-pkt: Link 1 ndata_app_dma_in=9216 > comm.in_count=8192 Nov 29 04:55:01 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 1 Nov 29 04:55:01 b360mc kernel: a3818: rcv-pkt: Link 1 recovered in 1 attempt(s) . . . Dec 12 21:20:02 b360mc kernel: a3818: rcv-pkt: Link 1 ndata_app_dma_in=9728 > comm.in_count=8192 Dec 12 21:20:02 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 1 Dec 12 21:20:02 b360mc kernel: a3818: rcv-pkt: Link 1 recovered in 1 attempt(s)
23.12.2021: и ещё:
Dec 14 20:10:01 b360mc kernel: a3818: rcv-pkt: Link 1 ndata_app_dma_in=9216 > comm.in_count=8192 Dec 14 20:10:01 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 1 Dec 14 20:10:01 b360mc kernel: a3818: rcv-pkt: Link 1 recovered in 1 attempt(s) . . . Dec 22 15:50:01 b360mc kernel: a3818: rcv-pkt: Link 1 ndata_app_dma_in=9216 > comm.in_count=8192 Dec 22 15:50:01 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 1 Dec 22 15:50:01 b360mc kernel: a3818: rcv-pkt: Link 1 recovered in 1 attempt(s)
06.01.2022: и ещё:
Dec 25 19:55:01 b360mc kernel: a3818: rcv-pkt: Link 1 ndata_app_dma_in=9216 > comm.in_count=8192 Dec 25 19:55:01 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 1 Dec 25 19:55:01 b360mc kernel: a3818: rcv-pkt: Link 1 recovered in 1 attempt(s) . . . Jan 2 18:55:01 b360mc kernel: a3818: rcv-pkt: Link 1 ndata_app_dma_in=9216 > comm.in_count=8192 Jan 2 18:55:01 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 1 Jan 2 18:55:01 b360mc kernel: a3818: rcv-pkt: Link 1 recovered in 1 attempt(s)
Странно в этом всём то, что если последние 2 сбоя были в HH:55:01 (19:55 и 18:55), то вообще ВСЕ последние пары сбоев -- в HH:MM:01 (ну, один в :02, но это смахивает на погрешность), причём MM было кратно 5. Что-то запускается по cron'у каждые 5 минут?
Увы, ни исследования /var/log/messages (и даже вообще /var/log/* за те же HH:MM) ничего не дали, ни поиск среди /etc/cron* и /etc/anacrontab тоже (кстати, взаимоотношения между cron и anacron какие-то запутанные, чуть ли не циклические).
11.01.2022: и ещё:
Jan 7 03:00:01 b360mc kernel: a3818: rcv-pkt: Link 1 ndata_app_dma_in=9216 > comm.in_count=8192 Jan 7 03:00:01 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 1 Jan 7 03:00:01 b360mc kernel: a3818: rcv-pkt: Link 1 recovered in 1 attempt(s) . . . Jan 8 06:55:01 b360mc kernel: a3818: rcv-pkt: Link 1 ndata_app_dma_in=9216 > comm.in_count=8192 Jan 8 06:55:01 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 1 Jan 8 06:55:01 b360mc kernel: a3818: rcv-pkt: Link 1 recovered in 1 attempt(s)
Опять ТЕ ЖЕ САМЫЕ числа, и опять в кратное 5 минутам и в :01 секунду.
19.01.2022: несколько дней молочение стояло остановленным, в основном из-за реорганизации директорий в исходниках (nsrc/ стала src/); сегодня на b360mc был полностью обновлён дистрибутив и опять запущено молочение -- сейчас в интересах проверки работы работы с прошивкой (не виснет ли).
Так вот: при "умолчательном" devlist'е, в котором 2 осциллографа в 0-м крейте, а только 2 в 1-м, полезли ПОСТОЯННЫЕ СБОИ -- буквально раз в несколько секунд, а иногда и по нескольку раз в секунду.
Сами devlist'ы таковы:
dev d5 adc250@a3818vme ~ 0,0,0xd5,5,15 dev d6 adc250@a3818vme ~ 0,0,0xd6,5,16 numpts=2048 timing=int trigger=int trig_n=0 rangeA=0.5 rangeb=1 rangec=2 ranged=4 dev i6 adc250@a3818vme ~ 1,0,0x96,5,96 dev i8 adc250@a3818vme ~ 1,0,0x98,5,98
dev d5 adc250@a3818vme ~ 1,0,0xd5,5,15 dev d6 adc250@a3818vme ~ 1,0,0xd6,5,16 numpts=2048 timing=int trigger=int trig_n=0 rangeA=0.5 rangeb=1 rangec=2 ranged=4 dev i6 adc250@a3818vme ~ 1,0,0x96,5,96 dev i8 adc250@a3818vme ~ 1,0,0x98,5,98
Сбои в подавляющем числе имеют вид
...но было также и "Jan 19 22:39:26 b360mc kernel: a3818: rcv-pkt: Link 1 ndata_app_dma_in=9728 > comm.in_count=8192 Jan 19 22:39:26 b360mc kernel: a3818: rcv-pkt: Timeout on RX -> Link 1 Jan 19 22:39:26 b360mc kernel: a3818: rcv-pkt: Link 1 recovered in 1 attempt(s)
ndata_app_dma_in=9216
", плюс несколько раз
было 2 строки "Link 1 ndata_app_dma_in=...
" подряд.
Ставим в devlist все 4 осциллографа -- всё возвращается к нормальной работе, без сбоев...
23.01.2022: да, проверено -- при 4 работающих
осциллографах всё пашет прекрасно, как только отключаем пару записью
.devlist=-1
-- так начинаются проблемы.
Точнее, даже так:
04.02.2022: другая напасть -- b360mc зависла намертво при передёргивании питания крейта, подключенного к 1-й линии.
Питание пришлось дёрнуть потому, что один из ADC250 намертво встал раком (после разборок Котова через JTAG на прошлой неделе), от чего помогает только питание.
И вот -- оно успело даже вякнуть в /var/log/messages про потерю кучи TX-пакетов и слишком длинный входной пакет по ndata_app_dma_in, что было увидено "tail -f"'ом по ssh, но зависло совсем намертво и в файл это не сохранилось -- после перезагрузки отсутствует.
04.02.2022: и ещё прикол: если при запущенном через A3818 сервере сделать устройству (в данном случае i8) i:0x104=0x80000000, то сервер зависает вообще намертво. Судя по натравленному на него GDB --
(gdb) bt #0 0x00007f75e9126f53 in __select_nocancel () from /lib64/libc.so.6 #1 0x0000000000448771 in sl_main_loop () at cxscheduler.c:381 #2 0x0000000000408c0a in main (argc=<optimized out>, argv=0x7fffc12493e8) at cxsd.c:322
Проверил получше:
Значит, просто перестают приходить IRQ.
Вероятно, потому, что они, в отличие от i8, НЕ в начальном состоянии.
vme_lyr_irq_cb() unknown level=5.vector=0
Похоже, это какой-то более общий косяк в работе IRQ -- где-то что-то залипает и они перестают работать корректно. Возможно, оно и на BIVME2 воспроизведётся.
Разбирательство показало, что дело, видимо, в repcount'е: оно ж вычитывало по 100 осциллограмм с каждой линии -- а это море работы, вот и возникали такие тормоза. При торможении всех устройств (_devstate=-1) чтение стало мгновенным.
...да и вообще что-то маловато срабатываний в секунду при чтении через сервер -- test_CAEN_IRQWait молотит намного резвее. Стоит поразбираться в причинах.
10.11.2021: этот пункт перетащен из "cxsd_hw" (где он первоначально обсуждался) в "a3818". Т.к. изначально предполагалось, что проблема в сервере или cxscheduler'е, но 21-10-2021 стало кристалльно ясно, что причина -- исключительно в VME (при repcount-повторах до 100 штук оно ОЧЕНЬ долго сидит в том цикле).
02.10.2021: да, разбираться, примерно так:
sl_main_loop()
диагностику:
select()
'а.
18.10.2021@утро-душ: некоторые размышления:
FASTADC_IRQ_P()
-- там сейчас КАЖДОЕ прерывание логгируется на
stderr "бегущим числом" при помощи '\r'.
А если закомментировать?
cat /tmp/d[56].txt; sleep 100; cat /tmp/d[56].txt
и уж результаты выдачи (разницу между ними) делить на 100.
19.10.2021@утро-душ: да, извлекать числа автоматически -- проще простого; что-то типа
A0=$(awk '{print $5}' </tmp/d5.txt); B0=$(awk '{print $5}' </tmp/d6.txt); \ sleep 100; \ A1=$(awk '{print $5}' </tmp/d5.txt); B1=$(awk '{print $5}' </tmp/d6.txt); \ echo $[A1-A0] $[B1-B0]
19.10.2021: проверяем. Краткие результаты первых тестов "на вменяемость":
Так что будем мерять на задержке 100 секунд.
21.10.2021: потестировано; в принципе, и на задержке 10 секунд можно оценивать (для качественной оценки пара процентов разницы пофиг), но общие результаты странноватые -- ОГРОМНАЯ непропорциональность:
4483 d5,d6,i6,i8 4 молотят 15498 d5,d6 2 молотят: d5,d6, а i6,i8 сделано RUN_MODE=3 (disabled) 15419 d5,d6,i6 3 молотят: d5,d6,i6, а i8 сделано RUN_MODE=3 (disabled) 124 d6,i6,i8 3, но БЕЗ i5
ОСТАЛЬНЫЕ ВАРИАНТЫ ПРОВЕРИТЬ!!!
Некоторые дополнительные комментарии:
cdaclient -mDn @i:{i6,i8}.marker
"
обнаружилась любопытная картинка:
(Для количественных оценок измерения клались в файл, командой
"cdaclient -mDn @i:{i6,i8}.marker -T100 -o /tmp/i.log
".)
Видимо, дело в том, что
CAENVME_IRQCheck()
'ом он ещё не успевает прислать.
Но всё равно неясно, почему длина пачки 0.8с, а промежуток между ними всего 0.3с...
И вот тут уже вопрос -- а почему между линиями такая разница? Потому, что это разные линии, или же потому, что в крейтах на этих линиях осциллографы с разными версиями прошивки (на 0-й -- старая 0x06, на 1-й -- новая 0x07). Перепрошивку выполнять неохота, как и тасовать осциллографы между крейтами; надо задать вопрос авторам.
13:02: Антону Павленко вопрос мылом отправлен, ждём ответа.
25.10.2021: Антон Павленко просто позвонил: ответ -- нет, они впрямую ChangeLog вроде бы не ведут; ВРОДЕ бы такого эффекта (замедления в новой прошивке) быть не должно, но фиг знает; Котову письмо форварднуто, но он пока молчит; так что мне -- welcome всё это потестировать.
Итого:
irq_fd_p()
по 800 и 300 миллисекунд.
Вот тут -- да, надо бы напихать отладочной печати с timestamp'ами в
sl_main_loop()
; может, какое озарение произойдёт.
28.10.2021: давно стало ясно, что надо провести другую серию тестов, основанную исключительно на "cdaclient -T100" и с предварительным отключением остальных осциллографов (НЕ входящих в список бенчмаркируемых). Для этого используем следующую команду:
(Это "полный список" -- если считать каждый осциллограф за 1 бит, то получается 4-битное число, и прогоняются все комбинации, кроме 0.)for i in d5 d6 d5,d6 i6 d5,i6 d6,i6 d5,d6,i6 i8 d5,i8 d6,i8 d5,d6,i8 i6,i8 d5,i6,i8 d6,i6,i8 d5,d6,i6,i8; do; export ACTIVE="$i"; \ cdaclient localhost:1.{d5,d6,i6,i8}.run_mode=3; \ cdaclient localhost:1.{$ACTIVE}.run_mode=0; \ cdaclient -Dn localhost:1.{d5,d6,i6,i8}.run_mode; \ cdaclient -mDnT8 localhost:1.{$ACTIVE}.marker -T 100 -o /tmp/bench-$ACTIVE.lst; \ wc -l /tmp/bench-$ACTIVE.lst; \ done
Вот результаты:
d5 36269 d6 21747 d5 d6 30601 i6 11730 d5 i6 19238 d6 i6 16247 d5 d6 i6 30393 i8 11691 d5 i8 19198 d6 i8 16284 d5 d6 i8 30509 i6 i8 11824 d5 i6 i8 12031 d6 i6 i8 11993 d5 d6 i6 i8 16960
Сделано 2 прогона -- результаты совпадают, +/- чуть-чуть.
Выглядит, конечно, мега-странно -- вот теперь надо думать, какого же хрена так...
29.10.2021: переставил d5,d6 из крейта на 0-й линии в крейт на 1-й, к i6,i8, но ПОСЛЕ (правее) них.
(А в 0-й засунул найденный на шкафу старый экземпляр (с BASE_SW_VER=0x04 (против нынешних 0x06 и 0x07), да ещё и PGA_HW_VER=1,PGA_SW_VER=3 против нынешних 3,4; ну хорошо хоть BASE_HW_VER=2 у всех); у него базовый адрес 0x06, а устройство названо x6.)
Так вот, первый же результат: если активированы i6,i8, то d5,d6 вообще НЕ ПОЛУЧАЮТ возможности выставить IRQ: та парочка, стоящая левее, активна всё время, чередуясь, и просто не пускают более никого.
Но это-то ещё ладно -- по крайней мере, понятно. Что НЕ понятно -- это результаты прогона 15 комбинаций, когда задействуются только те же единичные или парные устройства d5,d6, что были в крейте на 0-й линии (а вот когда i6,i8, бывшие и ранее на 1-й -- то числа +/- совпадают):
d5 11252 d6 19210 d5 d6 15229 i6 11355 d5 i6 11738 d6 i6 15337 d5 d6 i6 11809 i8 11728 d5 i8 11898 d6 i8 15529 d5 d6 i8 11915 i6 i8 11869 d5 i6 i8 11785 d6 i6 i8 11787 d5 d6 i6 i8 11787
В чём может быть причина и как это проверить?
Тогда это аппаратная проблема, которую фиг исправишь.
Как проверить -- переткнуть оптические линки, чтоб этот крейт стал 0-м.
Как проверить? Да отключить нафиг 0-ю линию, закомментировав строчку со старым девайсом x6.
Проверил -- один фиг, без изменений...
31.10.2021: посмотрел внутрь самих логов -- там опять всякие странности, вроде
@16:00, по дороге к папе, за Пирогова-26 у входа в лесочек к стадиону НГУ: вообще, до чего ж мне надоело постоянно разбираться со всякими неожиданными заскоками VME!!! Вот это, похоже, и есть то, о чём предупреждал Michael Davidsaver ещё в 2014-м.
02.11.2021: попробовал вариант с буферизацией векторов.
d5 11550 d6 19953 d5 d6 14410 i6 11105 d5 i6 10957 d6 i6 14645 d5 d6 i6 13632 i8 11463 d5 i8 11560 d6 i8 14713 d5 d6 i8 13902 i6 i8 11451 d5 i6 i8 12218 d6 i6 i8 13820 d5 d6 i6 i8 13328
Первый результат -- да, работает. Хотя паттерн (порядок, в котором получаем вектора от разных устройств) несколько неожиданный.
Тут будет всё про работу с MVME3100 -- и информация, и собственно описание драйвера (и кросс-компиляции тоже).
read()
и
write()
.
А на мой вопрос "но как это возможно? ведь надо указывать АДРЕС?!" --
ответил, что адрес указывается lseek()
'ом.
Этот исходник есть даже в kernel-3.10.0-514.el7.x86_64.rpm от CentOS-7.3 (от ноября 2016-го).
А сегодня увидел в документашке (см. ниже), что используют
вызовы pread()
и pwrite()
, получающие
дополнительно параметр offset
и по эффекту включающие
предварительное позиционирование.
Рядом там нет более НИКАКИХ файлов типа "vmei*"; только 4 штуки vme_pio2*.[ch], но они относятся к чему-то другому.
И, в т.ч., нет ни слова про исполнение циклов IACK.
Причём есть какие-то "master"- и "slave"-окна. Эту концепцию я встретил вообще первый раз.
И также неясно, какие ограничения на РАЗМЕР окон.
P.S. А вот что ясно -- что там НЕТ поддержки mmap()
(хотя
она концептуально явно напрашивается).
27.11.2021: ЕСТЬ, с 07-03-2015.
The VME user module provides a mechanism for accessing the VME bus from userspace. There are a number of limitations to this approach, the prime one being that interrupts can not be handled in user space.(bold мой).
Нет, это НЕ аналог нашего *vme_test -- он не позволяет выполнять произвольные действия (СОВСЕМ -- argc/argv[] не используются), а лишь производит некоторые фиксированные тесты.
Но там есть обращение к прерываниям.
01.07.2021: а-а-а, дошло -- какие ещё argv/argv[]! Это же модуль для ядра!
Описание утилиток vme_slave и vme_master.
Ничего особо интересного (в т.ч. не объяснено, в чём разница; только по
списку ключей у master'а есть "-w WIDTH
" -- 8/16/32/64, data
width).
...Кроме того, что им AM указывается НЕ впрямую, а комбинацией из
1) "супервизорскости:-S
"/"непривилегированности:-N
",
2) "данности:-D
"/"программности:-P
"
и ширины доступа (16/24/32); видимо, оно само вычисляет AM.
МОЁ впечатление:
28.06.2021: ещё:
Там, в частности, приведён пример простенькой тестовой программки. Это тот же самый код, что на linux-vme.org.
03.07.2021: насчёт "что такое эти master- и slave-окна":
- Master windows are mappings of local physical memory to VME memory.
- Slave windows are mappings of VME memory to local physical memory.
Master windows provide access from the local processor[s] out onto the VME bus.
Slave windows provide devices on the VME bus access into mapped portions of the local memory.
Т.е., MASTER-окна -- это маппинг HOST->VME, а SLAVE-окна -- VME->HOST.
Откуда вывод: нас интересуют только master'ы, а slave'ы нам нафиг не нужны (т.к. НЕ требуется доступ VME-устройств в память ЦП).
...кстати, помнится, в доках на ALMA вроде указывалось ДВУстороннее маппирование -- ОДНО окно работало в обе стороны.
24.11.2021: итого, какие весёлости наблюдены в "VME-подсистеме ядра (для доступа из userspace)" по результатам попытки создания поддержки MVME3100:
Здесь же "способ" кодирования Address Modifier'а -- абзац какой-то...
30.11.2021: всё даже хуже -- в
vme_user.h, ОБЯЗАННОМ быть доступным для userspace, использовался
тип u32
, специфичный для потрохов ядра; так было в ядре 3.10.0.
В свежем варианте от Торвальдса (от 01-11-2017) это всё подчищено (теперь
__u32
), но зато используется ключевое слово/атрибут
"__packed
", в userspace неопределённый (и даже в этом файле ему
нефиг делать, ибо
"It's use in the exported
header files could cause problems..." -- 2011 год!), так что всё равно
не собирается...
Похоже, что это всё сделано одним конкретным Martyn Welch из GE Automation для своих локальных задач в далёких 2000-х, и с тех пор никому особо и не требовалось...
И замечание: поскольку реально это вроде как не только под этот контроллер, а под "API vme_user & Co." (в частности, и под MVME5100 должно бы подойти), так что корректнее было бы обозвать как-то по-другому. Но: а) непонятно, как именно ("vme_user_hal"?); б) скорее всего, никогда ничего не потребуется.
для простоты реализации можно забить на "интеллектуальный" менеджмент окон, а сделать по-простому: при КАЖДОМ обращении к шине настраивать ОДНО окно (0-е) и делать обмен через него.
Пара деталей:
Плюс, вследствие ОДНОпоточной архитектуры все цепочки обращений к одному
устройству -- вроде программирования в StartMeasurements()
--
будут идти друг за дружкой и не требовать модификации окна.
01.07.2021: в продолжение той же темы -- с технологией "помнить ТЕКУЩИЕ настройки и не перенастраивать при совпадении" есть один проблемный нюанс: РАЗМЕР окна. В нашем нынешнем API "vme_hal" он никак не фигурирует (можно только считать, что для скалярного доступа он равен 4 байтам, а для векторного -- размеру вектора).
Много думал вчера и сегодня, и напрашиваются 2 варианта:
Что-то типа "win0=16:0x29:0xb0000000/0x00FFFFFF", т.е., "winN=ADDRESS_SIZE:ADDRESS_MODIFIER:BASE_ADDRESS/WINDOW_SIZE" (синтаксис унифицирован с DEVSPEC в *vme_test).
26.11.2021: маленькое дополнение: поскольку интерфейс MVME3100/tsi-148/vme_user требует при настройке окна указания не только ширины адреса, но и ширины данных, то синтаксис префикса надо сменить с "ADDRESS_SIZE:" на "AnnDmm" (например, "A32D32") -- аналогично принятому в lab6_vme_firmware_common.c.
А в момент чтения/записи делается поиск по аллокированным окнам и используется первое, подходящее по ширине адреса и модификатору плюс вмещающее весь блок.
Ну, очевидно -- вводить какой-то "протокол/API":
typedef struct {} ЧТО_ТО_t
, в который
может парситься эта информация.
Т.о.,
-C bus_config
", которому указывать ту же
информацию.
...хотя это реально излишне -- там всё равно указывается ОДИН тип доступа (ADDRESS_SIZE:ADDRESS_MODIFIER).
22.11.2021: а вот нифига НЕ излишне: чтоб
mvme3100_test
мог использоваться в параллель с живым
v4mvme3100vmeserver
'ом, не портя тому егойные окна. Для чего в идеале надо серверу давать окна 0,1,2 (для
A16,A24,A32), а test'у всегда оставшееся окно 3 (ему этого одного всегда
хватит).
typedef
, #define-символ...»?! Можно же проще и даже
примитивнее:
-C
bus_config
";
psp_parse*()
, так
что при возврате ею значения <0
считать, что ошибку вернёт
psp_error()
.
Это устранит необходимость начального обращения по "максимально близкому к базе" адресу, а возможно -- при соответствующем выборе адресов устройств -- и первого обращения к устройству с самым младшим адресом (поскольку для ВСЕХ устройств "квантованное" начало окна будет общим).
И весьма вероятно, что даже для A24 можно иметь вообще статическое маппирование "всего пространства" (уж для A16, с его 64Кбайтами -- так наверняка).
...да, возможно, что ситуация будет портиться необходимостью использования разных ADDRESS_MODIFIER'ов. Но, по идее, при корректном оборудовании -- не должно, т.к. вообще-то AM определяется именно шириной адреса (только у Карпова был косяк: AM=0x0d ("A32 supervisory data access"), но работа по A16D16).
UPD: поскольку для BLT используются другие AM'ы, то можно для них аллокировать ещё 3 окна.
18.11.2021: а в том "стандартном интерфейсе ядра под
Tundra" разве есть поддержка BLT? Если нет -- то неактуально; будет
исполняться просто pread()
/pwrite()
с числом
элементов более 1.
01.12.2021@~17:10, едучи на Кее по Терешковой из Ярче в сторону НГУ, в районе гаражей, потом коттеджей и домов на Коптюга:
А ведь синтаксис должен быть не просто АНАЛОГИЧЕН на указания DEVSPEC у
vme_test_common.c и lab6_vme_firmware_common.c --
по-хорошему он должен быть ИДЕНТИЧЕН! Ведь DEVSPEC у этих утилит как раз и
определяет требуемую конфигурацию окна! Соответственно -- чтоб можно было
прямо DEVSPEC и подпихивать в качестве того bus_config, а не
VME_HAL_DEFAULT_BUS_CONFIG_FOR_VMETEST
(Всё гениальное просто!)
01.12.2021@дома, детальнее посмотревши синтаксис в тех утилитах: неа, не выйдет, т.к.:
Или определять
VME_HAL_DEFAULT_BUS_CONFIG_FOR_VMETEST
как раз ТОЛЬКО для
ОДНО-оконного варианта, и если он defined()
, то использовать
его, а если нет -- то уже DEVSPEC?
Так что, возможно, придётся как-то самостоятельно генерить строку из РЕЗУЛЬТАТА парсинга DEVSPEC'а. ...или прямо API сделать принимающим числа, а не строку?
07.12.2021@утро, просыпаясь и душ: А не добавить ли ещё один вариант конфигурации для МНОГОоконности: "считай ТЕКУЩУЮ конфигурацию из бриджа"?
mvme3100_test
'у адаптироваться к
настройкам, сделанным v4mvme3100vmeserver
'ом -- у которого уже
будет какая-то специфицированная карта.
VME_HAL_DEFAULT_BUS_CONFIG_FOR_*
: в них и будет говориться
"используй текущие настройки".
Чуток конкретики:
mN
=-109:30: а, нет -- =0: при этом как раз и получится, что автоконфигурация будет работать АВТОМАТОМ.
vme_hal_init()
должна проверять, что если затребован
такой режим, то вместо конфигурирования выполнить чтение и заполнить все
cur_*
.
...да, конкретно cur_am
будет той ещё засадой -- надо
генерить AM из "cycle", "address width" и "data width".
mN
какое-нибудь другое значение.
Единственное пока изменение -- открывается "/dev/vmeli%d" вместо "/dev/vmei%d".
22.11.2021: дошли руки пилить дальше; точнее -- сформировалось понимание, как именно лучше сделать для достижения максимального эффекта минимальными усилиями.
Выбрана следующая концепция:
vme_hal_a##AS##xx##TS[##v]()
будет
начинаться с макро-вызова CHECK_AND_SETUP_VME_WINDOW()
...
Т.е., все "мозги" для "балансировки" окон будут именно в
CHECK_AND_SETUP_VME_WINDOW()
; предполагается, что она должна
реализовать алгоритм "постараемся настроить окно так, чтобы оно подходило
для всех обращений".
("Однотипные" -- с конкретной шириной адреса.)
-C bus_config
".
...это полезно, например, чтобы при живом
v4mvme3100vmeserver
'е мочь обращаться и
mvme3100_test'ом
-- указав ему другой номер окна.
-C bus_config
".
22.11.2021@~18:50-~19:00 по дороге с Морского домой: весь день думал-думал, как же всё-таки сделать "чтоб можно было РАЗДЕЛЬНО указывать окна для разных режимов адресации" -- что-то с этим очень туго:
Так что -- ну его нафиг, пусть пока всегда работает с ОДНИМ окном. А если всё-таки придумаем, как красиво сделать гибкий набор (и/или это реально занадобится) -- тогда и реализуем/проапгрейдим.
@Коптюга, тротуар между Терешковой-2 и
Коптюга-19: и отдельная идейка о том, как бы сделать, чтобы
v4mvme3100vmeserver
и mvme3100_test'ом
уже ПО
УМОЛЧАНИЮ использовали бы разные окна (чтоб не требовалось обязательно
что-то конфигурить):
VME_HAL_DEFAULT_BUS_CONFIG_FOR_VMESERVER
и
VME_HAL_DEFAULT_BUS_CONFIG_FOR_VMETEST
, в которых указывать окна 0
и 3 соответственно.
-C
всё же выполняют psp_parse()
, но только уже из
соответствующего умолчания.
@дома, ~19:30, когда записывал вышеперечисленное: определился с названием для всей этой концепции/API: "bus_config". И все связанные вещи должны содержать это буквосочетание.
23.11.2021: итак -- всё "специфично-bivme2'шное" вычищено (вроде за-#if0'енного второго комплекта операций В/В, БЕЗ поддержки векторности через vect_io), пора уже ДЕЛАТЬ.
Сначала -- A BIG FAT NOTE:
vme_hal_open_bus()
, а не в
vme_hal_init()
.
А требование добавления этих AM, шины и т.п. -- фактически превращает VME чуть ли не в какой-то CAMAC.
Приступаем...
Пара аспектов, обнаруженных в процессе:
pread()
/pwrite()
есть ограничение по размеру --
0x20000, т.е., 128Кбайт. Вот как это выяснилось:
vme_user_read()
.
resource_to_user()
.
if (count > image[minor].size_buf) count = image[minor].size_buf;
size_buf
" -- размер внутреннего буфера
kern_buf
, через который, как чётко видно в этой функции, и
выполняется пересылка из адресного пространства VME -- сначала в этот буфер,
а затем copy_to_user()
в юзерский.
PCI_BUF_SIZE
,
определённым как
(причём тут слово "slave" -- неясно).#define PCI_BUF_SIZE 0x20000 /* Size of one slave image buffer */
kern_buf
выполняются ВСЕ пересылки -- и
чтение и запись, и с master-окнами и со slave-окнами.
Итого -- в векторных операциях надо вместо простого одиночного вызова делать цикл flush-типа пока всё не прочитается/запишется.
@~12:30 в лесу у стадиона: а почему, собственно, этот цикл должен ЮЗЕР организовывать?! Почему сам vme_user.c его не делает?!
Казалось бы -- это должно хоть как-то быть видимо для программ; но фиг -- видимо, внутренние определения в ядре.
Да, нашёл: в ядре есть include/linux/vme.h -- наружу-то он никак не отдаётся, userspace-программам его взять негде.
vme_user_ioctl()
,
код VME_SET_MASTER
, вызывает (сразу, БЕЗ какой-либо проверки
параметров)...
vme_master_set()
,
который выполняет некоторые базовые проверки, в т.ч. не вполне понятные вида
((image->cycle_attr > cycle) == cycle)
(тест "поддерживаемости операции", сравнением по маске?), после чего
вызывает с этими параметрами...
bridge->master_set()
-- т.е., метод конкретного
бриджа, что для конкретно TSI148 сводится к...
tsi148_master_set()
,
выполняющей всякие разные проверки разных параметров, но...
cycle
делающее лишь 2 проверки:
VME_SUPER
и VME_PROG
.
И всё! Т.е., ширина адреса -- отражающаяся в РАЗНЫХ кодах AM'ов -- тут в
cycle
не участвует вообще никак!
Увы, увы. Неожиданный побочный результат разбирательства --
tsi148_master_set()
стоит
проверка
if (vme_base & 0xFFFF) { dev_err(tsi148_bridge->parent, "Invalid VME Window " "alignment\n");
Откуда вывод: надо ставить базовый адрес окна в
"addr & 0xFFFF0000
" (а размер -- в максимально допустимый
чипом бриджа).
После чего напрашивается идея: конкретно для НАШЕГО применения
достаточно будет определить такой максимум в максимальное количество позиций
в крейте помножить на 0x01000000 (16Мбайт -- максимальное адресное пространство
батрачиных модулей). Т.е., 21 * 0x01000000
("VME64-BINP" вроде
как на 21 позицию).
Но это для A32, а для A24 достаточно и "родного" максимума в 16Мбайт (1<<24), а для A16 и вовсе 64Кбайт.
@моя посуду: насчёт Address Modifier'ов --
похоже, проще всего будет трансляцию сделать тупо
табличкой, состоящей из строк вида "[0x29] = 0x....
".
23.11.2021: за сегодня сделано следующее:
CHECK_AND_SETUP_VME_WINDOW()
решено, в силу её
объёмности, делать не макросом, а обычной функцией.
И выбрано такое дизайн-решение: она возвращает если не ошибку (<0), то номер "структуры-дескриптора окна"; конкретно СЕЙЧАС -- всегда 0.
mvme3100_hal_win_info_t
, лежат в массиве
mvme3100_hal_win_info
(сейчас -- из 1 элемента).
И там помимо прочих свойств имеется также поле mN
-- номер
окна; которое, как предполагается, должно заполняться из "bus_config".
vme_hal_init()
сделано открытие шины (по образцу из
примера).
CHECK_AND_SETUP_VME_WINDOW()
:
ioctl(,VME_SET_MASTER,)
входит также и
ширина ДАННЫХ (data width) -- 1, 2, 4 (указывается в байтах).
К счастью, в нашем API VME_HAL_DEFINE_IO()
таковой параметр
есть -- TS
(Type Size?); он, правда, в битах (8/16/32), но это
некритично.
26.11.2021: дальнейшая работа над
CHECK_AND_SETUP_VME_WINDOW()
.
27.11.2021: "реализация" использования Address Modifier'ов.
AM2cycle[64]
, заполнены только ячейки 0x09
(A32 unprivileged data), 0x29 (A16 unprivileged data), плюс 0x0D (A32
supervisory data -- в первую очередь для Карпова, но также на него и
Павленко/Котов откликаются) и 0x2D (A16 supervisory data).
Так что доступ к железке Карпова (где используется режим A16, но при этом modifier 0x0D (A32 supervisory data access) с MVME3100/Tundra попросту не удастся.
VME -- некроэлектроника, а желание использовать её -- это какая-то некрофилия.
Но конкретно MVME3100 с его Tundra -- это полный абзац: вместо простой эмуляции (как в A3818 и BIVME2) тут как будто сделана попытка максимально имитировать M68K, так что с Address Modifier'ами и прочими атрибутами приходится буквально продираться как сквозь джунгли. А вкупе с тем, что тут ещё траходром с этими клятыми "окнами", работа превращается в сущий ад -- простейшие вещи требуют море усилий.
Ощущение, как будто вернулся в 1980-е, в худшем их проявлении -- когда всё было неуниверсально, везде всё специфично, вместо работы над сутью задачи приходилось возиться с разными мелкими деталями. Очень напоминает прикидки того, как реализовать поддержку Одрёнка или файловой системы RT-11 под DOS -- крайне трудоёмко и с минимальным полезным выходом. Потому-то и ощущение БЕЗДАРНО потраченного времени.
P.S. Использовать VME в новых проектах -- столь же нелепо, как писать новый софт на Коболе или Фортране.
На этом, кстати, основная часть практически завершена -- осталась только инфраструктура парсинга bus_config.
27.11.2021@утро, просыпаясь: по результатам реализации уже сделанного стало кристалльно ясно, как именно делать вариант с много-оконностью:
CHECK_AND_SETUP_VME_WINDOW()
превращается в простейший
поиск по таблице открытых окон, возвращающий номер подходящего окна.
...собственно, на то и был расчёт несколько дней назад при её создании,
когда и было решено возвращать этот номер "win
".
_open_bus()
.
ParseDevSpec()
.
MVME3100_HAL_MULTIPLE_WINDOWS
.
Не удержался -- сделал заготовку:
#define
MVME3100_HAL_MULTIPLE_WINDOWS
-- но УСЛОВНОЕ, если он не
определён ранее (из системы сборки; так можно, при желании, использовать
разные варианты для vmeserver'а и для test'а.
MVME3100_HAL_NUM_WINDOWS
,
определяемая, в зависимости от режима, =1 или =4.
И mvme3100_hal_win_info[]
теперь имеет такой размер.
CHECK_AND_SETUP_VME_WINDOW()
для этого варианта -- ну
совсем тривиальный поиск по таблице окон.
30.11.2021: продолжаем готовить многооконный вариант.
mvme3100_hal_configure_window()
-- чтоб не дублировать код в
разных #if
-ветках.
30.11.2021: делаем компилируемость+собирабельность -- пока под локальную платформу.
И "bivme2" везде заменено на "mvme3100".
u32
)
и incomplete-типов...
Может, сделать-таки вырезку только НУЖНЫХ определений из vme.h?
u32
-- остался.
u32
вообще НЕ
ПОЛОЖЕНО использовать в подобных разделяемых (kernel/userspace) исходниках.
И в современной версии vme.h вместо него уже __u32
.
__packed
", являющийся специфичным для ядра...
...а он присутствует после ДВУХ определений, и потому компилятор ругается, что "conflicting types for '__packed'".
#define __packed
". ПОМОГЛО!!!
Ура, на этом считаем первоначальный вариант сделанным.
01.12.2021: а вот и нет!!! Назревало ещё вытаскивание всех определений для v4*vmeserver'а, в т.ч. генерации vmeserver_drivers.c, в отдельный .mk-файл. Оно и изначально-то напрашивалось (ещё со времён *canserver'а), а уж теперь, когда директорий более одной и есть дублирование -- тем более.
Проверено -- работает.
01.12.2021: возвращаемся к поддержке много-оконности.
vme_hal_init()
:
mvme3100_hal_configure_window()
.
goto ERREXIT
", где выполняется подчистка, с
сохранением errno
через saved_errno
.
any_rqd
вначале =0, а по конфигурации =1), и
если нет, то возвращается ошибка.
rqd_am
-- предполагается, что парсинг конфигурации должен
писАть туда.
Оно сделано отдельным от cur_am
потому, что то часто
форсится в =-1
-- и по умолчанию, и при закрытии шины.
vme_hal_term()
переделана так, чтоб быть универсальной --
она теперь прикрывает не [0]-й элемент mvme3100_hal_win_info[]
,
а ВСЕ -- в цикле.
И для подчистки уже открытого в случае ошибки открытия очередного окна в
vme_hal_init()
просто вызывается она же.
Такое впечатление, что теперь "мясо" сделано всё -- осталось реализовать парсинг конфигурации.
04.12.2021: возвращаемся -- приступаем к поддержку парсинга и передачи конфигурации.
Простого варианта не видно вовсе -- либо требовать ОБЯЗАТЕЛЬНОГО указания
bus_config (в т.ч. и при вызове mvme3100_test
!), либо
напихивать в код извращения с генерированием bus_config-строки из DEVSPEC
(хотя в любом случае остаётся вопрос, откуда брать значение РАЗМЕР окна; от
HAL'а? -- но это опять нарушает разделение между уровнями...).
Клятый VME, чёрт его дери!!!
VME_HAL_DEFAULT_BUS_CONFIG_FOR_VMESERVER
="win=0" и
VME_HAL_DEFAULT_BUS_CONFIG_FOR_VMETEST
="win=3".
mvme3100_hal_win_info[]
.
Оба варианта имеют свои плюсы и минусы: с вызовом функции -- более "правильно"/структурированно, но шибко громоздко; с прямым парсингом -- проще и короче, но нарушаются правила фрагментации/инкапсуляции.
05.12.2021: придумано простое решение для парсинга: если определён некий #define-символ, то считать его именем функции, которую можно вызывть для парсинга (и, соответственно, начинать воспринимать указания bus_config).
Делаем всё для ОДНОоконного варианта в mvme3100_hal.h:
VME_HAL_DO_PARSE_BUS_CONFIG()
--
да, это макрос-ФУНКЦИЯ, принимает 1 параметр -- строку.
mvme3100_do_parse_bus_config()
, она
сводится к просто вызову psp_parse_as()
.
PSP_P_INT("win",...
.
И ещё некоторые соображения, пришедшие в голову при делании вышеперечисленного и просмотре/анализе нынешней инфраструктуры remdrv/remsrv:
Сходу напрашивается только использование psp_parse_v()
с
толпой из 4 пар таблиц/"структур", где в роли структур и будут выступать
элементы массива. Но это как-то стрёмненько.
13.12.2021: в PSP_P_PLUGIN()
есть параметр
uptr
, передаваемый plugin-parser'у параметром
privptr
. Ну так и указывать там для разных
строчек их номера -- 0, 1, 2, ...; а
mvme3100hal_WinspecPluginParser()
пусть считает это номером и
прибавляет домноженный на него sizeof(mvme3100_hal_win_info_t)
,
тем самым эффективно и смещаясь к нужному элементу массива (а
psp_parse_as()
'у в качестве rec
будет указываться,
естественно, &(mvme3100_hal_win_info[0])
-- как уже сейчас
и сделано).
Размышления на тему реализации -- за 05-07-2014 и 08-11-2019. Но реальной потребности в реальной реализации -- до сих пор так и не было.
...причём если обойтись ОДНОоконным вариантом, то и тут реализация не потребуется :D
06.12.2021: делаем поддержку в
vme_test_common.c: всё довольно тривиально -- ТУЧА
#if
'ов.
option_bus_config
=NULL.
getopt()
'а.
vme_hal_init()
-- парсинг умолчания
VME_HAL_DEFAULT_BUS_CONFIG_FOR_VMETEST
в случае, если ключом
явно ничего указано не было (*И* если само это умолчание определено).
В lab6_vme_firmware_common.c сделано точно то же (т.к. он проходит по той же категории "утилит командной строки").
07.12.2021: реализуем модификации в МНОГОоконный вариант в соответствии с утренними мыслями на тему "чтоб поддерживал чтение ТЕКУЩИХ настроек" (см. предыдущий раздел за сегодня):
VME_HAL_DEFAULT_BUS_CONFIG_FOR_*
определены в строку
"win0=? win1=? win2=? win3=?".
vme_hal_init()
в цикле перебора окон добавлена
начальная альтернатива (ДО проверки rqd_am
)
"mN == 0
".
13.12.2021: возвращаемся к работе (неделю страдал написанием 20211205-OTCHET-PO-GOSZADANIYU-2021.txt).
uptr
/privptr
как
номер специфицируемого окна (см. выше за сегодня).
mvme3100_params[]
заполнен в
соответствии с этим принципом.
15.12.2021: только указание номера пришлось перевести с
lint2ptr(N)
на (void*)(N)
.
mvme3100hal_WinspecPluginParser()
-- копированием
кусков кода из
lab6_vme_firmware_common.c::ParseDevSpec()
с
надлежащими модификациями:
RangePluginParser()
;
14.12.2021: продолжаем...
mode2AM[]
, а для адресации в
ней используются константы TBL_*
, описывающие значения,
определяемые адресными пространствами и режимами доступа (PRIV/USER,
CODE/DATA).
Строчки таблицы имеют вид вроде
[TBL_A32 | TBL_USER | TBL_DATA] = 0x09
-- т.е., заполнены лишь отдельные ячейки, а в незаполненных нули, и сама
таблица имеет размер меньше, чем 64 (0x3F+1).
И если в ячейке по получившемуся индексу -- 0 (либо таблица меньше и такой ячейки в ней вообще нет), то данное окно отключается.
master.enable==0
), то оно также отключается.
В принципе, сделать-то можно (особенно в ОДНОоконном), но просто лень -- это усложнение логики, и без какого-либо смысла (батраковско-павленковско-котовские всё равно CR/CSR не поддерживают, судя по PDF-описаниям).
15.12.2021: ну вроде как всё было дописано -- пора проверять, для начала хотя бы компилируемость.
uptr
/privptr
) использовалось
lint2ptr(N)
; но lint2ptr()
-- это ФУНКЦИЯ, хоть и
inline static
(т.е., по сути -- как бы макрос), поэтому
компилятор ругался на "initializer element is not constant".
Пришлось исправить на (void*)(N)
.
На этом вроде пока всё -- больше сделать ничего нельзя, пока не будет реальной системы сборки под MPC8540 и живого MVME3100 для тестов.
И девайсы линий надо будет называть /dev/vmeliN; соответственно, драйвер -- vmeli.c. Вот пусть для этой реализации и будет сей раздел.
Сейчас этим если заниматься -- то чисто для развлечения и из-за "зуда в одном месте".
12.11.2021: ну "зуд" присутствует, да...
Из-за чего конкретно СЕГОДНЯ весь сыр-бор: из-за того, что ещё позавчера стало очевидно, как именно надо организовывать и API, и реализацию:
10.11.2021@пешком из Invitro, переходя через Коптюга между ИАиЭ и ИЦиГ, ~08:00: да понятно как -- полностью аналогично тому, как сделано в конце 2019-го BIVME2'шном варианте "с буферизацией векторов".
Т.е., скелет драйвера (интерфейс с VME-шиной и взаимодействие с ядром) взять от Котова, а сохранение векторов и взаимодействие с userspace -- от BIVME2'шного.
17.11.2021: приступаем к трансформации исходника vmeli.c (тот был взят 12-11-2021; тогда он был от 01-07-2021, де-факто "версия 2" после начального варианта от 27-09-2018).
N_IRQ
" переименован в "N_IRQL
", со
сменой значения с 256 на 8 (т.к. это теперь число не векторов, а линий).
MODULE_AUTHOR()
.
Комментарий: СЕЙЧАС определение в include/linux/module.h рекомендует "for multiple authors use multiple MODULE_AUTHOR() statements/lines", хотя раньше (в 2005-м точно) была рекомендация "через запятую" -- "Author, ideally of form NAME <EMAIL>[, NAME <EMAIL>]*[ and NAME <EMAIL>]". Очевидно, рекомендация сменилась 28-08-2009.
vme_irq_request()
.
Причина, почему сделано ТАК -- понятна: этим имитируется работа прерываний в M68K (где при ОБРАБОТКЕ как раз уровень вообще роли не играл), и как раз для драйверов в ЯДРЕ именно так и нужно.
Также, возможно, именно поэтому Котов реализовал по /dev/vmei-файлу на вектор, а не потому, что так считал Фатькин. Хотя, скорее всего -- по суиие этих причин.
Как бы то ни было, встаёт вопрос: а насколько оптимально (с точки зрения производительности) будет то, что драйвер станет регистрировать для каждой линии по 256 обработчиков?
bridge->irq[level - 1].callback[statid]
("statid
" -- это как раз вектор; он почему-то везде там так
именуется).
vme_irq_request()
ЕСТЬ проверка "не занята ли уже эта ячейка",
путём проверки func
(на не-NULL), а вот в
vme_irq_free()
таковой проверки НЕТ, так что если вдруг кто-то
попробует освободить незанятый вектор, то будет слепо сделано
bridge->irq[level - 1].count--
-- что при достижении count==0 приведёт к отключению всех прерываний данного
level'а.
Сейчас проверил -- да, в актуальной 5.16-rc1 этот косяк тоже есть. Надо бы написать багрепорт.
@~14:00, чистя Чейзер от снега:
кстати, а тем ведь и в vme_irq_request()
НЕТ проверки, что
callback!=NULL. А надо бы добавить -- чтоб нельзя было "установить"
отсутствующий обработчик.
Вечер: зарегистрировался в bugzilla.kernel.org.
18.11.2021: продолжаем.
КРОМЕ собственно "работы" -- vmeli_isr()
,
vmeli_cdev_read()
, vmeli_cdev_poll()
и
обслуживающей их структуры image_desc
. Там всё равно надо ВСЁ
переделывать, на по образу и подобию BIVME2'шного.
image_desc
:
q_
-поля для буферизации --
q_isin[256]
, q_fifo[Q_SIZE]
, q_head
,
q_tail
.
level
(экс-vector
), level_mask
, ready
.
vmeli_isr()
(это не обработчик, а callback для VME-подсистемы
ядра) тоже переделан, как и vmeli_cdev_read()
с
vmeli_cdev_poll()
-- всё по образу и подобию предшественника
двухлетней давности (реально в основном просто копировались куски, с
минимальной контекстной заменой (вроде "vd
" на
"image
").
q_
-полей -- в
vmeli_probe()
: там же, где инициализируются прочие поля
структуры image_desc
.
...и это, в общем-то, всё -- типа переделано. Дальше только проверять компилируемость и работу.
Это директория, в которой будут собираться VME-драйверы под ЛОКАЛЬНУЮ архитектуру -- аналогично can/local/.
И в vme/Makefile::SUBDIRS
оно добавлено.
В этот исходник преобразуется в будущем нынешний bivme2_test.c -- по мере создания общей инфраструктуры vme_io и конкретно HAL-модуля bivme2_vme_io.[ch].
14.01.2020: за вчера и сегодня файл наполнен -- копированием из старого bivme2_test.c с адаптацией под новые реалии.
Некоторые заметки:
Он сохраняется в vme_devspec_t
вместе с прочими.
do_io()
переменная ofs
переименована в
addr
.
vme_rw_params_t
, соответственно, addr
стал databuf
'ом.
Это было унаследовано от uspci.h -- увы, там было выбрано не самое удачное имя.
offset
остался как есть -- т.к. это именно OFFSET от
базового адреса.
iohandle
-- ныне
bus_handle
-- берут его сами из глобалной переменной, а не получают
параметром.
do_io()
также самостоятельно берёт
dev.addr_size
, а не получает параметром.
Она, кстати, тоже, похоже, унаследована от uspci_test и cm5307_test.
sl_fd_mask
: в зависимости от
наличия в нём битов SL_RD
и SL_EX
набор
xfds
подставляется select()
'у в
readfds
и exceptfds
соответственно (а раньше
подразумевалось SL_EX).
Есть надежда, что такая модификация позволит работать и с a3818_hal.h и с mvme3100_hal.h.
sl_del_fd()
-- УЖЕ требуется
для bivme2hal'овского vme_hal_close_irq()
.
Теперь -- проверять!
15.01.2020: проверан В/В -- работает.
Но пришлось исправить косячок -- в do_io()
в получении
исполнительного адреса отсутствовало
прибавление к offset
'у базового адреса
dev.base_addr
(в старой версии этим занималось
bivme2_io.c).
WaitForIRQ()
с эмуляции cxscheduler'а на него настоящий. Как
минимум для a3818_hal.h необходимо.
P.S. В принципе, можно для начала сделать "2-режимный вариант": либо эмуляция, либо использование реального; выбор режима в зависимости от define-символа, выставляемого в Makefile. Давно такая мысль бродила; вопрос лишь -- а реально ли оно нужно?
Очевидно, делать надо по аналогии с cdaclient и pipe2cda.
22.01.2020@ночь, ~03:00, бессонница: сделано.
-B
".
option_binary
вместо обычной печати
делается просто
write(1, buf, addr.units * addr.count);
option_quiet
=1.
flag_quiet
не взводится, а просто не доходит не до какой лишней
печати в бинарном режиме.
22.01.2020@утро, пешком через студгородок: всё-таки несоврешенный вариант с нынешней реализацией "-B": прочитать из устройства и записать в файл можно, а вот прочитать из файла и ЗАПИСАТЬ в устройство бинарные данные -- нет возможности.
И, учитывая существующий синтаксис, даже не очень ясно, КАК можно б было указывать эти бинарные данные: ведь тот же cdaclient тоже не умеет принимать их на вход, а умеет это отдельная утилита pipe2cda.
Возможный вариант синтаксиса: чтобы вместо значений для записи
указывать @ИМЯ_ФАЙЛА. Т.е., вариант COMMAND'а
U:OFFSET=@FILENAME
.
Отдельный вопрос тут будет -- а как обходиться с stdin'ом?
STDIN_FILENO
?
Но тут могут быть проблемы с совмещением этого аспекта с обычной работой с файлами: те-то, после окончания чтения, надо бы закрывать, а stdin -- вроде как нет.
Напрашивается синтаксис
U:OFFSET/COUNT=@FILENAME
-- т.е., просто явное указание
количества.
Это будет аналогично поведению cdaclient/pipe2cda -- там значения могут иметь префикс [COUNT].
U:OFFSET/COUNT=VALUE{,VALUE}
.
Впрочем, покамест это всё больше теоретизирование -- никакой РЕАЛЬНОЙ потребности в таком бинарном В/В нету, это больше из стремления к красоте и полноте реализации.
...но как общетеоретические соображения -- вполне полезно, для разработки "общей теории диагностических утилит командной строки" :).
Основной смысл -- для унификации с создаваемым lab6_vme_firmware_common, где после ширины адреса также сможет указываться ширина данных (там получится "A32D16"). Но и для читаемости будет полезно.
-k
" -- игнорировать ошибки обращений VME и НЕ завершать
работу, а продолжать.
Буква "k" выбрана по примеру "make" -- там это "keep going".
Потребно для целей диагностики -- чтоб можно было производить цепочки VME-команд, часть из которых может и обламываться.
25.10.2021: для унификации с прочими утилитами, в которых маленькая "-k" уже занята (в первую очередь canmon, где это "print IDs in kozak notation"), переделано на заглавную "-K".
(Это было обнаружено при запуске в цикле -- когда по ошибке было запущено
вместо должногоfor i in d5 d6 96 98;do; a3818_test -x @1:32:0x09:0x{i}000000 i:0x104; done
то вместо ругательства на синтаксис оно получало "Bus error", т.к. пыталось лезть в странные адреса.)for i in d5 d6 96 98;do; a3818_test -x @1:32:0x09:0x${i}000000 i:0x104; done
23.01.2022: "секрет" оказался прост: в
ParseDevSpec()
при парсинге последнего компонента была проверка
лишь на
errp == p
,
а на
*errp != '\0'
-- не было ("ну чо, всё ж отпарсили, а дальше можно и забить!").
Нужная проверка добавлена -- теперь ошибки/опечатки ловятся.
И в lab6_vme_firmware_common.c добавлена аналогичная проверка,
только уже после парсинга epcs_offs
.
Но это не всё:
ParseAddrSpec()
ровно аналогичный косяк -- там после
адреса можно писать любую муть, и оно это слопает.
Но с этим ничего не поделаешь -- это для того, чтобы можно было толпу
команд перечислять в ключике '-c
' (onclose_params), и это
унаследовано ещё от uspci_test.c.
Стоп! Так можно проверять: если end_p == NULL
--
т.е., дальнейший парсинг никого не интересует (и этот "никто" не будет брать
на себя дальнейшие проверки), то тогда можно ТУТ проверить на тему
*errp != '\0'
.
Делаем -- там как раз в конце было условие "end_p != NULL
",
для передачи обратно точки конца парсинга, вот к нему добавлен
"else
" с проверкой на *errp != '\0'
.
Также отсутствуют проверки ещё в 2 местах, но в них так и НЕ сделано исправлений (ибо лень):
WaitForIRQ()
.
-i
" -- значения irq.n
и
irq.vector
: шибко уж муторно будет тут сделать КОРРЕКТНО, т.к.
там весьма развесистый набор допустимых вариантов.
В других программах:
ParseDevSpec()
(он там замудрённый, и совсем не похож
на VME-related потомков) -- в 2 экземплярах, а 3-й вариант спецификации
обеспечивается...
ParseAddrSpec()
-- в точности как в
vme_test_common.c, т.к. они-то очень похожи.
mvme3100hal_WinspecPluginParser()
:
неактуально -- оно там всегда возвращает указатель на точку окончания
парсинга вызывальщику, и уж тот должен позаботиться далее.
DoRecv()
не имеет проверки (да и
пофиг).
Кстати, есть в модели "ошибкой считается errp==p, а иначе передаём
далее" косяк: строка "0x" (например, как фрагмент
"0xZZZ") рассматривается как валидная, возвращающая значение 0. И
ничегошеньки в такой модели не сделать. А вот МОЖНО
сделать, если перед strto*()
сбрасывать errno=0
, а
после вызова проверять на errno!=0
(это
вполне официальная рекомендация из man-страницы).
FASTADC_ON_CLS*
(а в
camac_fastadc_common.h, кстати, не делается вообще никак).
FASTADC_TERM_D()
, чтоб она уж выполняла произвольные действия.
AbortMeasurements()
, но она делает слишком много лишнего (нулит
буфер данных и STAT-параметры) и не делает нужного (не отключает прерывания
в INT_ENA и не нулит вектор в INT_VECTOR).
(Спусковым крючком послужило письмо Котова (по результатам поиска причины
проблемы "Почему при чтении прошивки при работающем осциллографировании
устройство зависает" (причина найдена -- "Проблема была в Verilog коде.")).
У него иногда происходил Segmentation fault; проблема-то явно из-за другого
(там выглядит, что FreeDevID()
вызывается повторно и при втором
вызове SIGSEGV), но всё же корректно завершать работу всё равно стоит.)
27.01.2022: сделано проще: введена обязательная
StopDevice()
, парная к InitParams()
(и даже так же
принимающая pzframe_drv_t*
); она вызывается из
FASTADC_TERM_D()
.
Конечно, ещё проверить надо будет, но тут косячить особо вроде нечему.
Для конкретных драйверов -- будут свои level5-разделы.
21.02.2019: ага, вот сегодня и создаём тот level5-список.
А что если так: инфраструктура -- adc4x250, а драйвер -- adc250 (и, аналогично, adc1000 и adc500)?
Надо бы поинтересоваться у батракоцвев/павленковцев/Котова.
21.02.2019: спросил у Сенченко. Увы, с названиями по-прежнему бардак -- и adc4x250, и чего-то-там-1CH и -4CH...
Вот не может никак 6-я лаборатория научиться давать внятные названия своим девайсам -- не только до начала разработки, но даже и на этапе внедрения...
Как бы то ни было -- драйверы назовём именно так: adc250, adc1000 (и вряд-ли-когда-будет-реализован'ный adc500).
22.02.2019@институтская-сессия-кофе-брейк-до-обеда: спросил и у Фатькина -- да, всё ровно так, нет устаканившихся названий.
И более того: ADC1000 не особо-то и нужен.
Надо б их поудалять.
Но они обусловлены наличием drivers/vme/old-src/adc4x250_drv.c -- так что пока он не исчезнет, трогать всё то просто нельзя.
Скорее всего, ограничится именно файлом adc4x250_defs.h.
21.01.2022: ну и заодно уж и регистры для APD-версии, в виде "база и инкремент" -- APD_HV_CTRL_CHx_base=0x000148 и APD_HV_CTRL_CHx_incr=4
...и регистры управления термо-измерениями -- THERMOSENSOR_CSR = 0x000158 и THERMOSENSOR_TEMP = 0x00015c.
21.01.2022@вечер: и уж для комплекта -- определения "внутренностей" новых регистров, константы
ADC4X250_ADC_DECIM_RATIO_MIN
=0 и
ADC4X250_ADC_DECIM_RATIO_MAX
=65535;
ADC4X250_THERMOSENSOR_CSR_*
плюс "имена" значений
ADC4X250_THERMOSENSOR_RESOLUTION_1div*C
('*' -- 2,4,8,16).
05.02.2022: и ещё --
ADC4X250_R_COMPILATION_TIMESTAMP_H
=0x00002C и
ADC4X250_R_COMPILATION_TIMESTAMP_L
=0x000030.
(вечером) И заодно уж для компании новые (по сравнению с 2017 годом)
биnтики в регистре 0x000100/STATUS, ADC4X250_STATUS_*
:
PGA_NOT_DETECTED, PGA_FLASH_FAILED, FPGARTX_TIMEOUT_FAILED,
CMD_QUEUE_OVERFLOW, PLL_WAS_UNLOCKED. Всё потому, что, возможно,
понадобится вытащить наверх в виде канала значение битика PLL_WAS_UNLOCKED
плюс возможность его сбрасывать.
10.02.2022: заметил
косяк, присутствовавший изначально (опечатка?):
ADC4X250_PGA_RANGE_bits
стояло = 3
вместо
надлежащего = 2
(диапазонов-то всегда было 4, а не 8!).
Исправлено.
10.02.2022@после-обеда: дЭбил!!! Все "*_bits" -- это МАСКИ, а не количества битиков! 4 варианта -- это как раз и есть маска 3 (варианты значений внутри неё 0,1,2,3).
А из-за того дурацкого исправления возник идиотизм -- при старте драйвера диапазоны (кроме указанных в auxinfo) считывались значением 3 (4V), но запоминались как 2. У меня чуть глаза на лоб не полезли -- "какого лешего происходит?!".
Вернул обратно = 3
.
01.11.2019: переименовываем заголовок раздела, добавляя "_drv" -- чтоб было как у всех прочих драйверов.
01.11.2019: итак, что мы имеем:
Так что если сначала обратиться, например, к D5'му, то он работает, прерывания ловит. А если потом к D6'му, то он НЕ работает, а драйвер D5'го получает прерывания с вектором от D6'го и, естественно, ничего не делает. Если сначала обратиться к D6'му, а потом к D5'му -- всё зеркально аналогично: D6 работает, а D5 нет, но его прерывания получает D6.
Позвонил Роговскому, как у него: а очень просто -- у него 4 устройства (карповских?), и у каждого своё IRQ. Т.е., разделение не требуется и всё работает.
Первую-то проблему придётся решать переходом на layer'ную архитектуру.
А вот для разбирательства со сторой и взят старый модуль.
(Разве что числа там читаются в осциллограмму более дикие (из воздуха-то!), но это уже может объясняться более старым железом и/или отсутствием калибровки).
Возникла мысль: а может, это где-то в драйвере делается принудительный trigger'инг? Но нет -- на вид нету.
Вариант -- пройтись по "истории модификаций", посмотреть отличия: может, всё-таки добавил какой-то кусок кода в целях отладки, да и забыл.
12.11.2019: процесс был муторный, в силу большого объёма, но говорить особо почти не о чем. Только:
ADC4X250*
"
надо переименовывать в "ADC250*
", а какие нет.
Каналы-то -- ADC4X250_CHAN_*
-- просто контекстной заменой,
а вот всё остальное -- практически индивидуально.
InterceptSignals()
, но обработчик не делал ничего, а лишь
печатал номер пойманного сигнала.
В общем, доведено до собирабельного состояния, но, естественно, пока без поддержки IRQ.
14.11.2019: и "поддержка IRQ" добавлена, при доводке остальных компонентов.
18.11.2019: первая проверка. После исправления косяка с SIGSEGV'ом в bivme2hal'е -- похоже, ВСЕ модули ловят IRQ. Но, как и раньше -- оные IRQ происходят безо всяких внешних причин, а просто сразу после программирования устройства на ожидание запуска.
Предполагается такой сценарий разбирательства:
Да, убрано -- стало намного читабельнее.
19.11.2019: ага, похоже, что в ЛЮБОМ сочетании: как только запускаешь ещё один, предыдущий "отключается". И как я вроде бы умудрялся несколько дней назад получать, что молотят оба "новых" сразу -- загадка; ошибка наблюдения?
Может, дело в имеющемся "лишнем" чтении регистра статуса прерываний, так что иногда race condition срабатывает и чтение+сброс_прерывания происходит прямо в обработчике? ...хотя всё равно цепочка не очень ясна.
Да, похоже, что так -- когда попало НЕ летят.
19.11.2019: взял у Козака пару VADC16 и немного пообщался с ним на тему "а почему у него нет команды сброса IRQ".
Тут что-то не совсем сходится: на ISA ведь не было никаких векторов, там разделение IRQ было страшной проблемой.
Ведь по факту получается, что модель -- "message-based" (сообщением является вектор), а не "level-based".
По результатам мысль: а если попробовать НЕ выполнять команду сброса прерывания -- то оно будет повторяться? Или так и останется 1 раз?
Если второе, то выходит, что команда "сброс прерывания" по факту нужна не для обработки прерываний, а лишь для того, чтобы устройство получило подтверждение "прерывание обработано" и могло бы работать дальше.
Список "извращенческих тестов":
vme_lyr_irq_cb()
.
Поставил -- пользительно: позволяет увидеть, что никаких "лишних" прерываний не прилетает.
FASTADC_IRQ_P()
.
20.11.2019@утро, дома (было -28, весь день провёл дома за чтением доков по Python): мысли о том, как ещё поразбираться с ADC250@BIVME2:
21.11.2019: выпросил, теперь надо анализировать/сравнивать. Попытка сравнить добытый vmei.c от 2010-05-24 "в лоб" с имеющимся у меня vmei-cs.c от 2011-09-13 показывает, что файлы -- родственники, но и различий реально дофига.
25.11.2019: поанализировал -- всё грустно: там "множественный" API (с указанием и vector) -- для KERNEL-драйверов, каковым является и ихний драйвер vsdc3.o (вот как, ну КАК они додумались до такого бреда?!).
25.11.2019: единым списком -- результаты разных тестирований:
ParseDevSpec()
'е для парсинга адреса использовался
strtol()
, а 15-10-2017 оно было заменено
strtoul()
.
(diff'ом искал разницу!)
Видимо, различие в поведении этих функций всё же есть.
Получасом позже: ну точно -- надо смотреть чуть выше за 15-10-2017, там этому отдельное расследование посвящено.
Кстати, проверил -- то глюк именно PowerPC'шного SDK, а под
CentOS-7.3@x86_64 -- всё ОК, результаты strtol()
и
strtoul()
совпадают; но в 32-битном варианте (при сборке с
-m32
-- нет), что, кстати, тоже было проверено 15-10-2017. Так
что дело не в PowerPC, а в стандарте и в 32-битности.
29.01.2020: за прошедшее время и мамкинский драйвер приподпофиксен, и adc250_drv введён в строй и используется аж с 8 устройствами в одном крейте.
Так что задачу "сделать драйвер, чтоб работал" считаем выполненной, а дальнейшие манипуляции -- уже в последующих разделах.
ReadMeasurements()
на блочное чтение (благо, оно в HAL- и
layer-API сегодня уже добавлено).
18.01.2020@дома, суббота, вечер: вроде должно быть полностью аналогично коду из adc200me_drv.c -- также блочным чтением получаем кусок в буфер и потом раздербаниваем на чётные и нечётные.
...но что-то не получается -- вот не идёт, и всё тут. Отупел я, что ли?!
20.01.2020: понял, в чём причина отупелости -- НЕ ТОТ драйвер брал за образец! Ведь в ADC200ME в одном 32-битном слове лежат не 2 точки одного канала, а данные от ДВУХ каналов. Правильной же моделью, где в одно слово упакованы 2 последовательные точки ОДНОГО канала, является CAMAC'овский adc200_drv.c.
19.01.2020@дома, воскресенье: кстати, а ещё там игнорируется значение PTSOFS -- непорядок!
20.01.2020: исправляем ситуацию с PTSOFS.
ValidateParam()
добавлено отбрасывание младшего бита
(да, аналогично ADC200 -- тыт мы тоже можем только парами пропускать
измерения).
01.02.2020@суббота-дома:
всё сделано, но ПО ДРУГОЙ модели (по результатам разговора с Котовым
30-01-2020; сама идея записана там же несколькими абзацами выше): вычитываем
ОДНОЙ векторной операцией прямо в retdata[]
(оно у нас уже как
раз 16-битное), а затем меняя местами чётные и нечётные (это необходимо
из-за big-endian).
А ставший ненужным buf[1024]
удалён.
03.02.2020: проверено, после устранения пары косяков в HAL и libvmedirect_vect_io.h -- работает!!!
И функционирование PTSOFS проверено -- работает как надо, а ЕманоФедя, оказывается, сразу же стал им активно пользоваться.
ro = ADC4X250_DATA_ADDR_CHx_base + nl * ADC4X250_DATA_ADDR_CHx_incr;
-- но далее его инкрементировать при чтении забывало.
Добавлено "ro++
" в цикл по numduplets
.
20.01.2020: да, проверил; результаты проверки:
Чуть позже: какой, шьорт побьери, "ro++
"?! Ведь АДРЕС надо
увеличивать на 4, а не на 1!
Исправил на "ro += 4
" -- "шум" очень резко уменьшился, став
действительно совсем рядом с 0.
Кстати, спросил Антона Павленко на тему "как ADC250 реагирует на не-кратный 4 адрес?". Ответ:
Сделан -- путём копирования adc250_{data,gui}.c из
adc4x250_{data,gui}.c с последующей контекстной заменой
adc4x250
->adc250
и
ADC4X250
->ADC250
.
Например, использовать статус (0/-1) операций начального чтения
параметров, отдавая его в rflags -- например, чтобы -1 приводил бы к
CXRF_CAMAC_NO_X
(хотя отдельный вопрос, как это будет (и будет
ли) отображаться в скринах.
11.02.2020: да, сделано: результат чтения конкретно
регистра TIMER (первого в цепочке, не считая информационных и калибровочных)
проверяется, и если <0, то срезу возвращается
-CXRF_CAMAC_NO_X
.
Да, отображается, увы, только в под-окошке "Clbr...", т.к. PZFRAME_CHAN_ON_CYCLE_MASK-каналы есть лишь там, а на основном окне нет. Но это уже проблема не драйвера -- что-то надо придумать для скринов.
А может, у скринов отображать изменением цвета чего-нибудь рядом с fps_show -- например, того же roller'а? А состояние брать, например, от канала _devstate?
Часом позже: неа, от _devstate не получится: он
не входит в список об-rflag'ливаемых в SetDevRflags()
-- флаги
трогаются только у ДРАЙВЕРНЫХ каналов, а у служебных нет.
Ну тогда от канала marker (кстати, а не его ли мы собирались использовать для посиневания roller'а)?
@вечер, ~18:20, переход из 13-го после семинара Карнаева "контроллер для всего": а почему, собственно, состояние OFFLINE отображать на отдельном виджете -- ведь это штатно делается прямо на ГРАФИКЕ.
Дальнейшие обсуждения этого вопроса будут уже в разделе по pzframe.
TrggrMeasurements()
: я там меняю TRIG_SOURCE и делаю
R_CTRL:=START ещё раз, но это не сработает, когда оно уже запущено -- надо
предварительно сделать "стоп".
Поэтому перед повторным стартом вставлено ещё R_CTRL:=BREAK_ACK.
Поэтому SleepBySelect(10000)
за-#if0'ен.
Надо будет проверить.
03.02.2020: да, проверено -- и SHOT работает, и отсутствие паузы проблем не вызывает. Так что "done".
А чё б не сделать егошний dtype/datum_t 16-битным -- int16 вместо int32? Раз там и так вычитываются 16-битные с фиксированной точкой -- вот их и использовать.
Тем более, что тогда можно обойтись безо всяких промежуточных rbuf[1024], вычитывая данные прямо в retdata[], ОДНОЙ векторной операцией. Уже на работе: авотфиг -- ведь мы вычитываем 32-битными словами, а платформа эта -- BIG-endian, так что точки окажутся перепутанными. Надо с Котовым пообщаться...
Но это именно только в adc250. А в adc1000 и adc500, с их перемешанными каналами -- нет, там придётся читать поштучно. ...хотя, можно вычитывать блоками, а потом раскладывать.
30.01.2020: уже на работе: ВСЁ ВЕСЕЛЕЕ!!! Там в драйвере и так 16-битные данные --
но в devtype стоит 32-битность --typedef int16 ADC250_DATUM_T; enum { ADC250_DTYPE = CXDTYPE_INT16};
как и в pzframes/adc250_data.c --devtype adc250 r1i3132620,r4i783155,r5i,w30i,r100i
ADC250_DTYPE = CXDTYPE_INT32
!!!
Так это что же получается: бедный сервер постоянно занимается преобразованием int16->int32? Во бардак...
30.01.2020: поговорил с Котовым.
Но препятствием к такой модификации является то, что на ЛИУ-20 уже задеплоено большое количество ADC250, так что пришлось бы модифицировать И модули, И сенченковский софт.
СЕЙЧАС Котов в своём драйвере вычитывает в отдельный массив по-канально, а потом собирает в один канал. Но этот дополнительный массив для вычитывания -- ОДИН на все экземпляры устройств (поскольку параллельного вычитывания в рамках одного процесса вроде бы не бывает).
Но, поскольку ADC1000 ещё нигде не задеплоен, то он подумывает всё-таки переделать железку, как нибудь, чтобы вычитывание упростить.
Но внятного описания (что это такое и как это делать) пока нет.
Я так и не понял, используется ли у них сейчас такая 2-уровневая модель драйверов или нет. Но для ADC250 вроде как хватает производительности и MVME3100.
Отдельный вопрос от меня: а не будет ли на big-endian проблемы при копировании данных РАЗНЫМИ размерами единиц? Т.е., не получится ли, что копирование блока данных 16-битными операциями даст иной результат, чем 32-битными? Если "да", то это уже совсем проблема...
30.01.2020@вечер: да не, не может такого быть, чтобы при ПЕРЕСЫЛКЕ внутри процессора данные портились, и пофиг, какого размера используются единицы: ведь при чтении и записи используются ОДИНАКОВЫЕ единицы.
31.01.2020@институтская-сессия-Рева, ~11:00: с другой стороны, при пересылке через CAEN A3818 не вылезет ли чего? Ведь там как раз получаются РАЗНЫЕ форматы: читается -- VME big-endian, а записывается -- x86 little endian.
Позорно, конечно, что не могу это в голове промоделировать, но не факт, что я полностью понимаю всю цепочку.
Так что проще будет взять да попробовать.
Доп.вывод: надо ОБЯЗАТЕЛЬНО оставить в коде
ReadMeasurements()
вариант, работающий по-ячеечно (да, просто
за-#if0'енным).
01.02.2020@дома-суббота: да, по-ячеечный вариант тоже оставлен.
Можно же и из устройства вычитывать только те, которые
line_rqd[nl]
(либо если data_rqd
). И наверх
отдавать уже только их.
Вроде бы инфраструктура с PrepareRetBufs()
должна такое
позволять.
Понятно, что при натравливании "скрина" на устройство он будет пониторировать все каналы и смысла от такой оптимизации не будет, но вот при чтении данных специализированными программами, мониторирующими только нужные каналы -- очень даже получится экономия, т.к. вычитываться будет ровно столько, сколько нужно.
03.02.2020: смотрим, как обстоят дела.
PrepareRetBufs()
УЖЕ устроен именно так -- наверх
отдаётся только то, что было запрошено.
Насколько такое РЕАЛЬНО требуется -- вопрос отдельный.
В первых 18 точках находятся данные до получения триггера. Таким образом можно считать, что получение данных начинается за 18 отсчетов до получения триггера запуска(цитата из описания *ADC4X250_CHx.pdf).
ADC4X250_TIMER_PRETRIG
.
И, в реальности, ещё 18 он и в конце добавляет -- это всё нужно для ADC1000.
И технология, как правильно отображать временнЫе метки к таким графикам уже тоже придумана, ещё 03-04-2018 -- значение надо вычитать из возвращаемого CUR_PTSOFS.
10.02.2020: на сейчас к вычислению ro
(адреса чтения) добавлено
(ADC4X250_TIMER_PRETRIG / 2) * 4
т.е., просто прибавление 36 к адресу.
18.02.2020@утро-дома-посудомойство: а можно доступ к пред-триггерным 18 точкам сделать проще: позволить отрицательный PTSOFS -- до -18.
В f4226-то отдельный канал PREHIST64 требовался потому, что там он был в единицах "по 64 точки", а тут можно прямо в точках указывать.
Будет только одна проблема: получится конфликт с логикой работы
Init1Param()
, где признаком неуказанности считается
отрицательное значение. Но это исправимо -- смещением границы и
умолчательного значения с -1, до например, -999.
17.02.2020@дома: сделано.
Init1Param()
там УЖЕ было, только надо было
в нём раскомментировать условие "me-^gt;nxt_args[n] < 0
".
ValidateParam()
.
adc250_params[]
(с соответсвующими
_lkp[]'ами).
21.02.2020: проверил -- работает, так что "done".
Следовательно, не может быть ни команды "забудь калибровку!", ни статусного бита "откалиброван или нет"; и понятие "visible calibration" также бессмысленно.
Вывод: надо это наследие ADC200ME/ADC812ME удалять.
05.03.2020: удаляем каналы FGT_CLB, VISIBLE_CLB и CLB_STATE.
В частности, в скрине в окне "Ctrl..." кнопка "Calibrate" перетащена в
ячейку (0,0) калибровочной сетки, а всё остальное (включая
clbgrdg
) убрано; разве что сама clbform
осталась,
содержащая сейчас единственный clbgrid
.
*Пока что*, чисто для отладки, будем записывать вручную bivme2_test'ом, но вообще-то надо бы какой-то регулярный и консистентный механизм через каналы. Но совсем неясно как; плюс, в группе "w" остались свободны лишь каналы 30...39.
10.03.2021: в порядке исследования вопроса:
10.03.2021@вечер: Антон Павленко в мыле рассказал, что они "у себя в тестовом софте мы сделали что-то типа списка профилей, аля имя (индекс) - набор фиксированных значений, а дальше можно выбрать вариант настроек из списка". По факту это то же самое -- выбор из списка пресетов. Вопрос лишь, как при старте ЧИТАТЬ "текущее значение" этого селектора: сравнивать со всеми строчками списка? А если ни одна не подошла -- зарезервированное значение 0, которое при записи ничего не делает?
0x000130 PLL1 | 9-0 R_DIVIDER | 21-12 N_DIVIDER | 26-24 FB_PATH | 0x000134 PLL2 | 4-0 R_DIVIDER | 15-8 N_DIVIDER | 25-16 D_DIVIDER | 30-28 M_DIVIDER | 31 X2 | |
---|---|---|---|---|---|---|---|---|---|---|
Умолч. | 0x0300A002 | 2 | 0x0A | 3 | 0x00040F01 | 1 | 0x0F A:0 B:15 N:60 | 4 | 0 | 0 |
Треб. | 0x02008004 | 4 | 0x08 | 2 | 0x30030F01 | 1 | 0x0F A:0 B:15 N:60 | 3 | 3 | 0 |
Смущает, что в описании "регистры ADC4X250_CHx.docx" от Котова сказано, что по умолчанию PLL1_FB_PATH=0x02 (а не =0x03, т.е., PLL2, а не PLL1), а PLL2_M_DIVIDER=3 (а не =0, хотя тут эффект тот же, /3). Антону Павленко отправлено письмо с вопросом.
10.03.2021@вечер: ответ получен: скорее всего, "косяк в документации, последний раз коррекции в документацию вносились раньше, чем коррекции в встроенный софт". А сами эти странные наборы параметров -- вынесенная в доступ настройка некоторых делителей чипа AD9523.
11.03.2021@ванна: появилась ясность, как всё-таки делать поддержку смены PLL-регистров.
...а потом сразу же выполняется чтение.
for (val = countof() - 1; val > 0; val--)
-- т.е., при ненайденности будет автоматически вёрнуто значение 0.
_init_d()
.
В принципе, можно факт запроса запомнить и отпаботать потом, после окончания измерения (или по STOP'у).
Но проще пока что не заморачиваться -- пусть это будет ответственностью юзера.
11.03.2021@вечер: делаем, по вышеприведённому проекту.
pll_presets_table[]
.
ActivatePLLPreset()
.
Если что, она и при отложенной активации пригодится.
nxt_args[ADC250_CHAN_PLL_PRESET]
; благо, в обычной
pzframe-деятельности эта ячейка никак не используется.
И как раз для этого -- ВТОРОЕ использование -- и была сделана
ActivatePLLPreset()
.
12.03.2021: и в GUI это всё добавлено.
15.03.2021: только, как выяснилось, алгоритм вычитывания для определения номера текущего пресета СРАЗУ после записи -- не работает: в регистрах ещё сколько-то (>100ms) остаются старые значения.
Так что если в записи канала ещё можно было бы извратиться, вместо реально текущих возвращая "предполагаемые будущие" значения, то при установки из auxinfo -- ну уж совсем никак...
15.03.2021@~17:30, дворами вдоль Морского по чётной стороне: чуток рассуждений на тему "какого хрена оно так?!":
Тогда тут вообще сложно что-то сделать.
Разве что ADC250 должен бы тогда иметь внутри БУФЕРНЫЕ регистры, в которые изначально при включении вычитывать из чипа (хотя -- ведь он же сам туда и прописывает значения!), а при чтении по VME отдавать уже из этих буферов.
16.03.2021: немного анализа в попытках найти решение на случай, если Павленко и Котов скажут, что "так и должно быть".
ActivatePLLPreset()
сразу же возвращает все
3 канала -- номер пресета и значения регистров.
Для чего проведён анализа работы cxsd_hw.c с точки зрения инициализации устройств.
init_dev()
сразу вызывается
ReviveDev()
, который тут же вызовет
ReqRofWrChsOf()
, что приведёт к запросу ЧТЕНИЯ каналов записи,
в т.ч. и pll_preset, какового чтения мы надеялись избежать
(потому, что СРАЗУ после записи читаться будет старое, а не
свежезаписанное).
Так что увы -- "по-простому" сделать не получится...
pll_presets_table[pll_preset_n]
.
Причём "номер текущего пресета" хранить в privrec'е, полем
pll_preset_n
.
16.03.2021: Павленко ответил в 15:00, что
А не, если 100 мс - то это какая-то ерунда, так не должно быть, будем разбираться.
Это даёт надежду на исправление проблемы в железе, потому пока ничего кодить не будем. А если не исправят -- то проект решения готов.
11.07.2021: да, это реально косяк в работе прошивки на стороне устройства (источник -- переписка, e-mail от сегодня с Message-ID: 1626004544.950815541@f480.i.mail.ru). Там какие-то сложности с сериализацией передачи команд, приходящих со стороны VME-интерфейса, PLL-чипу.
Соображения Котова на эту тему доступны в issue "Латентность записи регистров" по адресу https://git.inp.nsk.su/liu-20/adc4x250_fw/-/issues/1
Для меня самоочевидным является решение с "теневыми регистрами"; Котов примерно это же предполагает под названием «"кеш" регистров».
11.01.2022: надо всё-таки к дуплету PLL1,PLL2 в пресетах добавить также и значение CLK_SRC. Ибо оно всё же имеет прямейшее отношение.
А то, что СЕЙЧАС можно менять TIMING отдельно -- ну так пусть запись в номер пресета вызывает также "чтение" канала TIMING.
18.01.2022: сунулся было добавить -- и поле
pll_preset_t.clk_src
сделал, и нужные значения заполнил в
pll_presets_table[]
, и даже записывание третьего значения в
ActivatePLLPreset()
добавил, но...
ActivatePLLPreset()
добавить можно, да.
В любом случае, реализовывать это ДО того, как Котов пофиксит чтение-после-записи (чтоб читалось из "теневых регистров"/кэша) -- крайне противно...
19.01.2022@утро-душ-~06:50: ну так можно сделать "fallback" -- если версия прошивки меньше, например, 999, то просто вернуть указанное для записи значение, а чтения не выполнять.
19.01.2022@~07:10:
сделано. При
BASE_SW_VER < 999
выполняется возврат указанного значения и сразу
goto NEXT_CHANNEL
.
19.01.2022: делаем далее:
pll_presets_table[]
.
ЗАМЕЧАНИЕ: возврат делается не ReturnInt32Datum()
'ом, а
Return1Param()
'ом -- чтобы значение попало и в "параметры".
clk_src
переставлено в НАЧАЛО
pll_preset_t
, чтоб шло по порядку регистров (вчера было
добавлено в конец).
...а использование -- запись, чтение, диагностическая печать -- изначально было в порядке регистров.
Init1Param(me, ADC250_CHAN_TIMING)
значением из пресета.
StartMeasurements()
закомменчена запись CHAN_TIMING в
CLK_SRC -- это ведь и не нужно, и даже вреднО. Да, в отличие от всех
предыдущих осциллографических АЦП, которые БЕЗ чипа PLL и потому должны
настраиваться при программировании всего прямо перед запуском.
Кстати, отсюда следует, что такое наличие PLL-чипа (точнее, такое его программирование) сильно ломает саму модель "PZframe": мы не можем просто выполнить "чтение с такими-то параметрами", а настройка режима должна выполняться предварительно. И даже "умное кэширование" (запись параметров пресета только в случае, если включаемые пресет не совпадает с текущим) тут не спасёт: ведь при различии запись придётся-таки делать, а она НЕ может делаться прямо перед измерением -- из-за той задержки активации.
Вроде всё -- теперь перепроверять работающесть.
P.S. Общее впечатление -- уже привычное при работе с VME: на выполнение простейших вещей тратится ДИКОЕ время (и неадекватно много усилий), и всё из-за того, что в аппаратуре реализовано криво (она не позволяет прочитать только что записанное). Т.е. -- опять море бездарно потраченного времени.
Возможно, что и не только его -- там битов-то в регистре статуса много.
09.03.2021: анализ документации касательно этого регистра показал, что по факту отображать нужно ТОЛЬКО этот бит, т.к. остальные либо бессмысленны для юзера (как ADC_CMPLT и CALIB), либо уже и так отдаются (как PGA_OVERRNG и CALIB_FAILED).
Так что остаётся лишь вопрос о том, КАК организовать чтение: ведь это pzframe_drv-драйвер, в будущем на предстоящем vme_fastadc_common.h, так что с ловлей обычного индивидуального DRVA_READ могут быть проблемы...
09.03.2021@вечер: посмотрел -- да нет, как раз ТУТ проблем нет: сделано по образцу cpci-драйверов и cpci_fastadc_common.h -- с индивидуальными _rw_p(), а не общим одинаковым-на-всех FASTADC_RW_P(), как в camac_fastadc_common.h. И оные индивидуальные _rw_p() имеют отдельную ветку для CHTYPE_INDIVIDUAL-каналов, где конкретно сейчас обрабатывается CALIBRATE; туда же и PLL_LOCKED добавим.
Посему:
PZFRAME_CHAN_IMMEDIATE_MASK | PZFRAME_CHAN_ON_CYCLE_MASK
.
Проверено на симуляторе (cxsd -S) -- меняется сразу же при изменении в сервере, не дожидаясь прихода кадра.
10.03.2021@вечер: а ещё бы проверить, почему в ИСПРАВЛЕННОМ клиенте при натравливании на не-рестартованный сервер, в котором этого канала не было, ручка показывалась просто в состоянии "fail", а не чёрная NOTFOUND.
07.04.2021: да, разобрался (за почти месяц!!!) и исправил.
11.03.2021: да, работает, "done".
А также Антон сказал, что у них
перепрошивка выполняется по VME, и исходники утилитки доступны в GIT'е --
vme_firmware.c
(а вот описания, увы, нету 14.07.2021: описание
Павленко прислал мылом, файл (переименованный по моим правилам)
20210714-pavlenko-Obnovlenie-PO-po-VME.docx). В принципе,
можно слабать свою утилитку командной строки (чтоб не требовалось ихние
контроллеры втыкать).
12.07.2021: приступаем.
Для этого введено поле privrec.BASE_SW_VER
, заполняемое в
ReturnDevInfo()
одновременно с возвратом наверх.
ReadMeasurements()
попарный своппинг точек
выполняется только при BASE_SW_VER<7.
Также модифицирована и старая, не-векторная ветка (которая #else'ная): поскольку там выполняется по-int32'шное чтение и потом складирование 2 штук подряд, то введена вторая ветка, в которой складируются в обратном порядке.
22.02.2022: всё давно проверено и работает, так что "done".
Причём надо бы посмотреть ещё на то, как в других драйверах сделано:
конкретно XS_DIVISOR в разных CAMAC- и cPCI-драйверах реализован сильно
по-разному -- где-то он AUTOUPDATED, где-то STATUS, и где-то
Return1Param()
'ом возвращается значение 0
.
(Всё касательно XS_DIVISOR, похоже, относится и к прочим XS_* -- XS_PER_POINT и XS_FACTOR.)
14.10.2021: кажется, понял причину (несколько дней
"как баран на новые ворота" пялился на результат
"grep XS_DIVISOR work/**/*.c(.)|less
"
и сравнивал содержимое разных драйверов): очевидно, имеет место некоторая
шизофрения --
chinfo[]
указан режим работы
PZFRAME_CHTYPE_AUTOUPDATED
, предполагающего, что значение будет
вёрнуто единожды из InitParams()
,
PrepareRetbufs()
всё делается словно для режима
PZFRAME_CHTYPE_STATUS
-- значение просто прописывается в ячейку
cur_args[ADC250_CHAN_XS_DIVISOR]
, но собственно возврата НЕ
делается (поскольку при CHTYPE_STATUS в набор возвращаемых этот канал
добавился бы автоматически в цикле ниже);
InitParams()
не производится.
Очевидно, эти части драйвера (тогда ещё adc4x250_drv.c) слизывались из РАЗНЫХ драйверов, имеющих РАЗНЫЕ режимы работы. ...либо, возможно, весь скелет брался с какого-то одного драйвера, но потом режим работы этого канала был сменён в соответствии с моделью работы конкретно ADC250.
Вывод: надо бы ещё разок разобраться, какая же всё-таки модель работы ADC250 и сделать ОБЕ части в соответствии с этим.
Несколькими минутами позже: глянул -- неочевидно там всё: похоже, изначально всё делалось в расчёте на модель CHTYPE_STATUS -- в предположении существования канала FRQDIV; но тот код закомментирован, а по факту прописываются фиксированные значения XS_PER_POINT=4 и XS_DIVISOR=1, канала же FRQDIV реально не существует (видимо, дело в том, что вместо него PLL, с которым "всё сложно").
15.10.2021: разбираемся и фиксим:
frqdiv_scales[]
(чьё использование
закомментировано) есть лишь в драйверах ADC200 (НЕ "me") и ADC812ME; причём
она там абсолютно идентична -- {1,2,4,8}.
Скорее всего, моделью для 250-го был adc812me_drv.c.
-9
(наносекунды), возвращаемое одинаково
Return1Param()
'ом),
Теперь обращаемся к ADC250_CHAN_NUM_LINES
:
вместо надлежащихline<0-3>totalmax 110 num_lines 114
(соответствующих числам в adc250_drv_i.h).line<0-3>totalmax 120 num_lines 124
Вот теперь считаем за "done".
11.01.2022: попытка краткого анализа:
Прикол в том, что в карте каналов остались
ADC250_CHAN_FRQDIV
и ADC250_CHAN_CUR_FRQDIV
-- они
никак не работают, но в карте имеются.
Так что нужно бы внимательно всё почитать.
19.01.2022@~19:00, проезд за домами/свечками параллельно Ильича и потом лесок до Терешковой 9 и около стадиона, по дороге домой: подумал-подумал --
@уже дома, ванна: там вообще всё странно: учитывая очень ненулевое время интегрирования, по-хорошему нужно иметь отдельное IRQ "окончание измерения термодатчиков", ибо по факту это ещё один интегрирующий АЦП и работа с ним должна вестись соответственно. Но понятно, что делалась его поддержка в прошивке скорее для отладочных целей, потому и реализация "ну чтоб просто по-быстрому слабать".
20.01.2022@~18:30, проезд за домами/свечками параллельно Ильича, по дороге домой: придумал, как лучше реализовать работу с этой фичей.
РАНЬШЕ всё размышлял -- не сделать ли как-то, чтобы клиент видел этот канал прямо как делитель (по умолчанию -- 1, а так до 65536).
Но нет, нефиг, пусть будет от 0 до 65535.
А идеология такая: есть же ОТДЕЛЬНЫЕ каналы FRQDIV и XS_DIVISOR. Поэтому:
21.01.2022: и даже {R,D}={1,-1} делать не будем -- чтоб не было путаницы между вариантами "сырые данные" и "конвертированные данные".
20.01.2022@дома, ~19:00: делаем.
ValidateParam()
: при BASE_SW_VER<8 форсит значение 0,
а иначе вгоняет в диапазон [0...65535].
27.01.2022: поскольку ADC1000 сейчас вообще работает c децимацией некорректно, то для него добавлено форсение значения 0 при BASE_SW_VER<999.
InitParams()
: при BASE_SW_VER<8 использует значение 0,
а иначе читает из железки; и оно потом всё равно проходит через
ValidateParam()
, посредством Init1Param()
-- так
что может быть overriden'о через auxinfo (если туда добавить параметр).
И для "правильности" вместо констант 0 и 65535 в adc4x250_defs.h
введены определения ADC4X250_ADC_DECIM_RATIO_MIN
и
ADC4X250_ADC_DECIM_RATIO_MAX
.
StartMeasurements()
: при BASE_SW_VER>=8 делается
прописывание cur_args[FRQDIV] в регистр децимации.
PrepareRetbufs()
: БЕЗУСЛОВНО возвращается
XS_DIVISOR=FRQDIV+1.
Просто инфраструктура с VALIDATE-параметрами в pzframe никак для этого не рассчитана -- поскольку все действия выполняются автоматически (пусть даже и в самом драйвере, а не в pzframe_drv.c), то тупо некуда вставить такие проверки с возвратами.
Можно, конечно, перевести канал на INDIVIDUAL и добавить проверку и отдачу флага, но зачем? Практической-то выгоды ноль.
Т.е.:
InitParams()
и ValidateParam()
поддерживают
гарантированно-осмысленное значение cur_args[FRQDIV] безотносительно
реальной версии железа (форся =0 при неподдерживаемости фичи).
ЗАМЕЧАНИЕ: по-хорошему, это ведь не FRQDIV в смысле "делитель частоты", а скорее "число измерений, пропускаемых для складирования 1 измерения в память". Но по-факту во всех осциллографодрайверах это "FRQDIV" использовалось также не как "значение делителя частоты", а как "режим деления частоты".
21.01.2022: вроде добито (часть мелочей уже сегодня).
Проверяем -- проде реакция есть: при указании значений существенно больше 0 (10000, 1000) измерения (запрошено 10000 точек) начинают идти существенно медленнее.
...а вот ПОДПИСИ на горизонтальной шкале меняются явно не в ту сторону -- числа УМЕНЬШАЮТСЯ, а должны УВЕЛИЧИВАТЬСЯ.
Ну да: раз "XS_DIVISOR", то значение x-секунд на него должно ДЕЛИТЬСЯ
(что и происходит в FastadcDataX2XS()
--
(x * multiplier) / mes_p->xs_divisor
).
А нам надо УМНОЖАТЬ.
Ещё замечание: надо спросить Котова, как эта децимация взаимодействует с "18 первых измерений надо пропустить"? И где теперь это описано, о пропускании? И что насчёт пропускания на ADC1000?
23.01.2022: спросил, ждём ответа.
24.01.2022: ответ получен:
25.01.2022: в ответ отправлена просьба и в будущем не трогать.
Таковой драйвер в виде исходника был создан одновременно с 4-канальным,
но реально список ListOfVmeDrivers.mk::VMEDRIVERS
никогда не добавлялся, поскольку был недоделан (как минимум блок чтения с
должным перетасовыванием данных так и не реализован).
01.11.2019: переименовываем заголовок раздела, добавляя "_drv" -- чтоб было как у всех прочих драйверов.
11.01.2022: немного размышлений и обсуждений:
Минус -- дублирование кода, так что при любом следующем изменении нужно будет синхронно исправлять уже ДВА файла.
#if
'ами, который бы уже
#include
'ился самими adc*_drv.c,
#define
'ящими соответствующие имена.
Минус -- громоздкость и усложнённая читаемость.
С другой стороны, плюс -- различия будут все очевидны, т.к. всё рядышком.
...но ещё одна проблема -- что с именами каналов делать.
ADC4X250_CHAN_*
в adc4x250_common_drv_i.h, который уже
отображать в конкретные ADC*_CHAN_*
?
ADC250_CHAN_*
считать за базовые, alias'я на них
ADC1000_CHAN_*
.
С точки зрения простоты второй вариант выглядит предпочтительнее.
Пока склоняюсь ко второму варианту -- всё-таки так отсутствует дублирование, плюс, всё обозримо (плюс, если всё-таки появидся ADC500, то его сюда интегрировать будет уже совсем несложно).
И как, интересно, с этим обходиться? При записи-то ещё ладно -- писать во все 4 одно и то же; а при чтении кого из них считать за "значение", если они вдруг отличаются?
(Надо будет проверить, но если оно так -- то проблемы просто нет, это как бы реально 1 комплект настроек.)
...правда, надо бы понять, что там с пропуском измерений в начале,
которые ADC4X250_TIMER_PRETRIG
=18: видимо, нужно пропускать
18*4 слов?
16.01.2022: наступило понимание того, как именно будем делать:
#include
'нью
его с предшествующим определением того, какого устройства это драйвер.
#define
-символы
DEVTYPE_ADC250
, DEVTYPE_ADC500
,
DEVTYPE_ADC1000
.
Т.е., например, adc250_drv.c превратится в просто
#define DEVTYPE_ADC250 #include "adc4x250_meat.h"
а дальше уже будут отдельные#if (defined(DEVTYPE_ADC250) + defined(DEVTYPE_ADC500) + defined(DEVTYPE_ADC1000)) != 1 #error Exactly ONE of "DEVTYPE_ADC250", "DEVTYPE_ADC500" or "DEVTYPE_ADC1000" must be defined #endif
#if defined(DEVTYPE_ADC250) #elif defined(DEVTYPE_ADC500) #elif defined(DEVTYPE_ADC1000) #endif
ADC1000_CHAN_*
для одноканальных свойств (чтоб БЕЗ циферки
0-3).
29.01.2022: передумано -- будет именно САМОСТОЯТЕЛЬНЫЙ файл.
Но это всё имеет смысл после того, как СНАЧАЛА допилим ТЕКУЩИЙ adc250_drv.c для поддержки "всего как надо" -- и полная работа с PLL (3 канала, а не нынешние 2), и новые фичи вроде термодатчика.
19.01.2022@~19:00, проезд за
домами/свечками параллельно Ильича, по дороге домой: а пока что можно
вести работы -- добавлением #if
'ов -- прямо внутри
adc250_drv.c.
20.01.2022: начато:
DEVTYPE_ADC*
,
#define DEVTYPE_ADC250
,
DEVTYPE_ADC500
делается #error
.
28.01.2022: приступаем плотнее.
<0-3>
", были
заменены на соответствующие одиночные ещё в 2017-м (на файле была дата
11-08-2017).
run_mode
и run
плюс pll_preset
, pll_locked
,
cur_pll1_ctrl
, cur_pll2_ctrl
.
fgt_clb
, visible_clb
, clb_state
.
totalmax
и
num_lines
, по ошибке значившиеся 110 и 114 вместо 120 и 124
(это рудимент от середины июля 2017-го, когда каналов было ещё 120, а не
140).
29.01.2022: продолжаем...
Поэтому принято следующее решение:
ADC1000_CHAN_*
для одноканальных свойств (чтоб БЕЗ циферки
0-3)", как задумывалось 16-01-2022.
ADC250_CHAN_*
, но для специфичных фич -- на
ADC1000_CHAN_*
.
typedef int16 ADC4X250_DATUM_T;
-- в коде драйвера было сделано вообще изначально, а замечено несоответствие 30-01-2020
и исправлено в adc250.devtype 16-03-2020.
ADC1000_MAX_NUMPTS
сменено с
= 3132674, // =3132692-18
на
= 3132620, // =3132692-18*4
-- поскольку пропускать нужно именно такое количество (18 штук, но от
КАЖДОГО из 4 АЦП).
ADC4X240_PGA_VAR_*
в ADC1000_PGA_VAR_*
--
так оно не конфликтует с ADC250_PGA_VAR_*
(по-хорошему оно
вообще должно бы жить в adc4x250_defs.h в виде
ADC4X250_PGA_VAR_*
, но драйверу-то оно нафиг не нужно, а нужно
(возможно!) только клиентам).
30.01.2022: и далее...
DSCR_X4()
на явные штучные указания.
DSCR_X4()
, т.к. их всё же 4 штуки, по числу АЦП.
31.01.2022: (реально растянулось и на первую половину 01.02.2022) добиваем:
line_rqd[]
, теперь существует и
функционирует только НЕ с ADC1000, т.к. оно помещено внутрь
#ifndef DEVTYPE_ADC1000 // ADC1000 is 1-channel
(сделано ещё 29.01.2022)
DEVSPEC_
",
и в общем коде все использования соответствующих ADC250_*
заменены
на них.
DEVSPEC_NUMLINES
-- сравнительно простое.
DEVSPEC_MAX_NUMPTS
-- тоже сравнительно несложно.
Но тут обнаружилось, что в StartMeasurements()
проверка "PTSOFS against
NUMPTS" использовала ADC250_MAX_NUMPTS
-- что есть крупный
косяк, ибо позволяет да-а-алеко вылезти за пределы буфера
retdata[]
.
DEVSPEC_PRETRIG
-- смещение для чтения в
ReadMeasurements()
. У ADC250 и ADC1000 устанавливается
равным ADC4X250_TIMER_PRETRIG
умножить на 1 и 4 соответственно.
DEVSPEC_XS_PER_POINT
-- самое простое, т.к.
используется в 1 точке и на функционирование собственно драйвера не влияет.
P.S. Работка была немного адовая: глазами просматриваем ВЕСЬ текст файла, когда замечаем что-то устройство-зависимое, то вводим соответствующий DEVSPEC_-параметр, после чего поиском (с РУЧНОЙ заменой!) заменяем ADC250_* на него, а затем повторяем просмотр (иногда с начала); и так несколько раз, пока не показалось, что "ну теперь -- всё!".
02.02.2022: как-то кривовато, что
буквосочетание "DEVSPEC" используется в 2 разных смыслах: в утилитах
*_test
как "DEVice SPECification", а тут (и в
advdac_*.h) как "DEVice-SPECific parameter"...
Итак -- переименовано: все "DEVSPEC
" заменены на
"DEVDEP
".
(Можно, конечно, выпендриться и в PrepareRetbufs()
при
нечётном PTSOFS делать "+1
" значениям, писуемым в
val_ps[x]
, но это слишком извратно.)
chinfo[]
, чтобы в ней присутствовали только реально имеющиеся у
драйвера.
#if...
: уродство.
ADC4LINES()
": и сложно, и тоже уродство.
PZFRAME_CHTYPE_*
какие-нибудь
PZFRAM2_CHTYPE_*
и PZFRAM4_CHTYPE_*
: уродование
имён, да и поиск по тексту усложнится.
Но в конце концов придумал способ элегантнее: сделаны вот такие доп.константы:
...и всем затронутым каналам в указания типов дописаны циферкиenum { NLGE2 = (DEVSPEC_NUM_LINES >= 2), NLGE4 = (DEVSPEC_NUM_LINES >= 4), PZFRAME_CHTYPE_BIGC2 = NLGE2 * PZFRAME_CHTYPE_BIGC, PZFRAME_CHTYPE_BIGC4 = NLGE4 * PZFRAME_CHTYPE_BIGC, PZFRAME_CHTYPE_VALIDATE2 = NLGE2 * PZFRAME_CHTYPE_VALIDATE, PZFRAME_CHTYPE_VALIDATE4 = NLGE4 * PZFRAME_CHTYPE_VALIDATE, PZFRAME_CHTYPE_STATUS2 = NLGE2 * PZFRAME_CHTYPE_STATUS, PZFRAME_CHTYPE_STATUS4 = NLGE4 * PZFRAME_CHTYPE_STATUS, PZFRAME_CHTYPE_AUTOUPDATED2 = NLGE2 * PZFRAME_CHTYPE_AUTOUPDATED, PZFRAME_CHTYPE_AUTOUPDATED4 = NLGE4 * PZFRAME_CHTYPE_AUTOUPDATED, };
2
или 4
-- тем самым и визуально ничего не
замусорено, и поиск по именам этих типов продолжит работать.
DEVSPEC_PRETRIG
ссылается на
ADC4X250_TIMER_PRETRIG
, то
#include "adc4x250_defs.h"
было перетащено из блока определений ниже в точку ПЕРЕД
#if defined(DEVTYPE_ADC...)
02.02.2022: да и вообще те несколько определений --
ADC250_DATUM_T
, ADC250_DTYPE
,
MAX_ALLOWED_NUMPTS
-- перетащены в этот же блок.
01.02.2022: "момент X" -- делаем уже adc4x250_meat.h, создаём тривиальный adc1000_drv.c и пробуем компилировать.
Ох ёлки-палки -- как же много всё-таки было недоосмыслено,
непредусмотрено и недоработано!!! Ведь в ИМЕНАХ-то осталось захардкоженное
"adc250_
" -- adc250_privrec_t
,
adc250_params
, adc250_rw_p
; а код в
"vme_fastadc_common.h" ожидает всех этих имён с префиксом
"adc1000_
"!
Для "прям щас" было сделано несколько #define
'ов сразу за
определением privrec'а:
#ifndef DEVTYPE_ADC250 typedef adc250_privrec_t adc1000_privrec_t; #define adc250_rw_p adc1000_rw_p #define adc250_params adc1000_params #endif
После чего оно собралось. Но всё же надо делать "по-правильному".
02.02.2022: пытаемся сделать "по-правильному". Вчера весь вечер и сегодня всё утро обдумывал, как же надо сделать. Соображения:
В этом смысле напихивать всякие "__CX_CONCATENATE()
" вместо
"adcNNN_privrec_t" -- точно не вариант.
Т.е., просто поименовать везде "privrec_t
" и потом сделать
#define adcNNN_privrec_t privrec_t
-- плохая идея, т.к. до GDB дойдёт именно "privrec_t
", а надо
б так, чтобы имена в тексте совпадали с показываемыми GDB'ой.
Исходя из этого было выбрано следующее:
adcNNN
" -- он полностью
аналогичен "adc250
" как по длине, так и по виду.
Реализация:
#if
-селектора было добавлено
следующее:
#define adcNNN_privrec_t __CX_CONCATENATE(FASTADC_NAME,_privrec_t) #define adcNNN_params __CX_CONCATENATE(FASTADC_NAME,_params) #define adcNNN_rw_p __CX_CONCATENATE(FASTADC_NAME,_rw_p)
FASTADC_NAME
было перетащено
из конца ("перед включением vme_fastadc_common.h") в тот самый селектор.
adc250_*
на adcNNN_*
.
adc250_
" -- *_lkp[]
и adc250_hbt()
.
И, наконец, adc250_drv.c также переведён на adc4x250_meat.h.
Теперь надо проверять.
03.02.2022: первая проверка показала, что вроде бы работает.
Возможно, что-то недоподключено.
Либо всё же косяк в коде, но ADC250 уже работали раньше и как-то были доведены до состояния "всё как надо", а ADC1000 впервые потроган драйвером только сегодня утром.
Возможно, дело опять в том, что ADC250 уже
Но возможен также и косяк в драйвере (хотя и в сотрудничестве с "потроганностью" ADC250) -- попытка жульнически обойти проблему "сразу после записи в PLL-регистры из них вычитываются старые значения" как-то аукнулась.
(Парой часов позже) Что странно, ОБА эти заскока -- ТОЛЬКО на тех 2 устройствах, которые я обсматривал: pll_locked=0 лишь на adc1000_a4, cur_pll1_ctrl,cur_pll2_ctrl=0,0 лишь на adc250_90.
04.02.2022: есть какой-то косяк с отображением значений, причём конкретно в скрине adc1000 -- если натравить на устройство adc1000 скрин adc250 (полу-совместимый за счёт имён-alias'ов), то он показывает другие значения и на реперах, и на тиках вертикальных осей.
Кстати, заметил, что каналы *RANGEMIN
/*RANGEMAX
из драйвера не отдаются.
09.02.2022: разобрался. Косяк состоял из 2 частей, сработавших совместно:
chan_type
-- каналов: для каналов с
PZFRAME_CHAN_MARKER_MASK
НЕ ловились {R,D}.
default_r
было
1000000.0, и из-за отсутствия данных о реальном R именно этот миллион и
использовался.
Так-то и в adc250_data.c тоже по ошибке стояло 1000000.0, но, из-за многоканальности, там IS_FRAME каналы НЕ использовались в качестве MARKER, а потому получали правильные данные от сервера (хотя диапазон там всё равно отображался +-8V, вместо правильного +-4V; точнее, ВСЕГДА было +-8V, вне зависимости от реально используемого для данного канала).
10.02.2022: исправления:
default_r
поставлено 4096.0 у обоих.
PrepareRetBufs()
добавлена отдача ДИНАМИЧЕСКИХ значений
диапазонов (вместо фиксированных [-32767,+32767]): отдаются соответствующие
диапазону значения
range2rangemin[]
/range2rangemax[]
, равные
+-{1/2,1,2,4}*4096.
Проверено -- вродё всё работает.
15.02.2022: небольшое дополнение в соответствии с написанным в e-mail от Антона Павленко за 07-02-2022 (Message-ID: 5f2b5fd58844cece4e87960e29cb36ed@inp.nsk.su) касательно реализации диапазонов в конкретно ADC1000:
В случае выбора +-0,5В и +-1В реально будет включен диапазон +-0,5 для обоих вариантов конфигурации соответствующего регистра. В случае +-2В и +-4В будет работать диапазон +-2В." */
Собственно, в ValidateParam()
сделано, что "при BASE_SW_VER
< 999" диапазон 4V заменяется на 2V, а 1V на 0_5V.
06.02.2022: идея алгоритма --
adcbuf[4][]
(например, по
1024 измерения -- учитывая, что в VME D32 BLT ограничен 256 байтами, как раз
будет достаточно и кратно).
w0 = *a0++; w1 = *a1++; w2 = *a2++; w3 = *a3++; *wp++ = w0 & 0xFFFF; *wp++ = w1 & 0xFFFF; *wp++ = w2 & 0xFFFF; *wp++ = w3 & 0xFFFF; *wp++ = (w0 >> 16) & 0xFFFF; *wp++ = (w1 >> 16) & 0xFFFF; *wp++ = (w2 >> 16) & 0xFFFF; *wp++ = (w3 >> 16) & 0xFFFF;
Модель сделана. Детали:
USE_ADC1000_TRIG_ORDER
.
adcbuf[4][PER_ADC_BUF_LEN]
располагается прямо в
privrec'е (ну не в стеке же...).
retdata[]
), т.к. интересовал именно
этот фрагмент.
Откомпилировано в ассемблерный код с -O0
(а иначе фиг
поймёшь) на x86_64 и PowerPC. На первом -- ну да, используется толпа
регистров, хотя сам код и далёк от оптимального (никаких "lodsd" и "stosw",
увы); но это, видимо, из-за отсутствия оптимизации. На втором -- вообще
нифига не понял (даже не факт, что смог найти точку начала фрагмента).
01.11.2019: переименовываем заголовок раздела, добавляя "_drv" -- чтоб было как у всех прочих драйверов.
12.11.2019: модификации действительно несложные. А
самое "массовое" действие -- замена bivme2_io_
на
me->lvmt->
-- вообще обошлось контекстной заменой, да ещё и
длина в символах совпадает, так что не пришлось мучиться с подгонкой
пробелами.
Собралось; работоспособность проверим позже.
12.11.2019: всё сделано по образу и подобию cdac20_drv.c, только тут ещё работа с IRQ будет, которой пока нет.
ПОКА в нём надобности вроде нет, но если вдруг появится...
16.03.2020@дома: именно что "приступаем": изучаем документацию, создаём карту регистров.
Дальнейшие дела -- делать скелет драйвера, потихоньку клепать карту каналов в _drv_i.h и devtype. "Сложность" там в основном в осознании того, какие свойства есть у каждого канала
Записать за 15.03.2020@дома об изготовлении "img878" -- _drv_i.h, devtype и pzframes/-всего.
А также записать в раздел "обычные драйверы в 4cx/src/", что
уже несколько дней бродит мысль о создании const_drv.c -- чтоб он вертал
указанные ему константы. Смысл -- для всяких псевдоустройств тима img878,
чтоб можно было указывать их каналы-свойства (ширина, высота). И тут не
вполне ясно, как лучше сделать: а) проще -- значение для единственного
канала указывать в auxinfo, а тип канала чтоб он добывал сам через
CxsdHwGetChanType()
(но при большом числе каналов будет избыток
устройств -- по одному на каждый канал); б) массивнее -- сразу много
каналов на устройство, а значения указывать в ихних drvinfo, тем более, что
всё равно обычно будет devtype (тут единственный минус -- чуть сложнее
программирование).
25.03.2020: за неделю самоизоляции дома драйвер в основном допилен. Не без проблем -- как в силу монструозности устройства, так и "благодаря" 6-й лаборатории, с их экзотическими понятиями о том, как софт должен взаимодействовать с железом.
Highlights (записываю уже 05.05.2020, по памяти и глядя на код):
В результате хоть канал WAS_SHOT как бы поддерживается (весь код
скопирован из dl200_drv.c), но поле last_shot
, на основе
которого определяется, "был ли выстрел", нигде не заполняется (ибо неясно,
где надо), так что канал всегда ==0.
Это регистры READY, ENABLE, AUTODISABLE, ... (тут есть как статусные (ro), так и управляющие (rw))ю
Это регистры всегда управляющие -- BSOURCE и START_SOURCE, причём:
В карту каналов эти регистры отображаются блоками по 25: 24 -- для каждой линии индивидуально, плюс ещё один (24-й) -- "общий", но только эти общие для разных видов реализованы чуть по-разному:
Таких каналов 5 групп -- ILK_EXT, ILK_DZ0-ILK_DZ3.
Таких каналов 2 группы -- START_TYPE и START_INPUT.
При чтении каналы START_TYPE_ALL и START_INPUT_ALL проверяют, что если во всех 24 каналах одинаковое значение (например, после записи в ALL), то возвращают его, а иначе -- "невозможное" значене, МАКСИМУМ+1 (=8); на запись такое "невозможное" значение не принимается.
Все эти хитрые действия повторяются не единожды (для разных групп
каналов), так что блоки их обработки в _rw_p()
реализованы
макросами -- ON_ONEBIT_24()
, ON_ILKSRC_B4()
и
ON_START_PRM()
DL250_CHAN_CLK_MODE
) в устройстве сделано извратно: надо
ИЗМЕНИТЬ значение младшего бита в регистре CLK_SELECT, предварительно
убедившись в отсутсвии проблем (проверив пару статусных битов там же); и ещё
не факт, что это переключение сработает. В общем -- целый шаманский танец с
бубном. Вместо того, чтобы просто записывать в битик нужное значение режима
(0:INT50MHZ, 1:CLK125) -- как, кстати, сделано во всех ихних же CAMAC- и
cPCI-девайсах.
Фатькин утверждает, что
Нужно прочитать значение SOURCE, и при несовпадении сделать команду переключения. По сути это выведены ножки блока управления PLL Alter'ы.
К сожалению, пока оказалось это - единственный нормальный способ. Так уж работает Alter'овская PLL-ка. Все попытки сделать автомат внутри натыкаются проблемы, когда один из источников тактового сигнала пропадает, и что делать в таком случае становится непонятно.
Что им мешало в ПРОШИВКЕ реализовать этот "шаманский танец с бубном" -- я так и не понял.
Поэтому в функцию _autoshot_hbt(), тут превратившуюся в просто
dl250_hbt()
, добавлено чтение этих 3 регистров и при
несовпадении значений с последними известными -- отдача наверх изменений.
Непосредственно чтением-сравнением-отдачей занимается
ReportChangedState()
.
Но каков вообще смысл этого IRQ в случае, если для разных линий используются разные стартовые импульсы с некратными периодами (а это возможно!) -- неясно; и Фатькин это подтверждает.
Сделано-то оно сделано, и даже скрин сделан, но ничего пока не протестировано -- из-за карантина нет возможности взять девайс из 13-го здания (со стенда клистрона) и вставить в свой крейт.
Непосредственной причиной создания раздела (как и заготовки драйвера) послужило то, что устройство несколько нестандартно -- "многовекторно". Это потенциально влияет на всю инфраструктуру vme_lyr.
Что сразу ломает стройность имеющейся модели vme_lyr и ставит вопрос, как такое поддерживать.
07.02.2020@дома-вечер-пятницы: последовательность размышлений:
vect2handle[]
, где содержатся handle'ы устройств, использующих
указанные вектора, и ничто не запрещает из нескольких ячеек ссылаться на
один handle.
Но проблема в том, что процесс подчистки в vme_disconnect()
идёт по списку УСТРОЙСТВ, а не по vect2handle[]
. И подчистит
только ПЕРВУЮ из ячеек.
10.02.2020: напрашивается, кстати -- ну сделать
(аналогично cankoz_lyr'у, где вместо старого devcode
теперь
есть массив devcodes[10]
) вместо поля irq_vect
чтоб был массив irq_vects[]
. И подчищать ВСЕ вектора из
массива.
По некоторому размышлению приняты такие решения:
08.02.2020@дома, суббота, вечер: сделана заготовка, которая пока даже скомпилироваться не сможет. Ключевое, ради чего она делалась СЕЙЧАС:
vsdc4_privrec_t
содержатся МАССИВЫ
handles[]
и irq_vects[]
.
vsdc4_init_d()
:
jumpers << 25
--
25, а не 24!
vsdc4_irq_p()
определяет номер "линии" (сработавшего
входа) по вектору, просто последовательно сравнивая со всеми 4.
10.02.2020: спросил Беркаева, понадобятся ли нам на ВЭПП-5 или К500 эти VsDC4.
Ответ -- скорее всего, нет. По крайней мере, пока для них применения не видно (на K500 работают VsDC2 и их хватает).
Так что на этом работы замораживаем.
03.11.2020: сделаны пока что скорее скелеты
rfmeas_l_timer_drv_i.h и rfmeas_l_timer.devtype, пока без
каналов, но с картой w50i,r50i
.
03.02.2021: за прошедшее время оно было доведено до некоторой работоспособности и уже используется.
Но иногда возникает потребность "что-нибудь прочитать", поэтому добавлена фича "отладочные каналы": debug_wr_addr, debug_wr_val, debug_rd_addr, debug_rd_val -- *примерно* аналогично тому, что было в своё время сделано в dds500_drv.c (только там адресовалось по номерам регистров, а не по адресам).
05.02.2021: также сделаны соответствующие ручки-каналы в скрине rfmeas_l_timer.subsys, для чего реализована поддержка rw-inttext'ов.
26.02.2021: и всё равно мало -- что-то не срабатывает, если шибко быстро после предыдущего SHOT'а дать ещё один. Вроде как начинает требоваться какая-то пауза, а почему -- неясно. Для разбирательства добавляем канал GATESTAT, коий будет вычитываться ПЕРЕД реальной записью для отработки SHOT'а и отдаваться наверх.
Начал тестировать -- наткнулся на странное поведение: канал GATESTAT отдаётся cdaclient'у СРАЗУ ЖЕ после запуска, вместо того, чтобы в момент реальной отдачи драйвером (по SHOT=1).
Ближе к вечеру: разобрался -- косяк был в том, что всем автообновляемым каналам драйвера ставилось AUTOUPDATED_TRUSTED вместо AUTOUPDATED_YES; почему-то в момент создания драйвера у меня было лёгкое помутнение, вот и запутался, что же использовать. А TRUSTED-каналы рассматриваются почти как каналы записи -- их значения отдаются сразу же в момент запроса. Так что переделал, оставив в старом типе только DEBUG_RD_VAL (в его случае вообще неясно, какой вариант корректнее).
27.02.2021: кстати, в коде frolov_ie4_drv.c есть комментарий на эту тему (видимо, от 07-08-2018, судя по записи тут в его разделе):
/* Note: LAM_SIG *must* be just "YES", not "TRUSTED"; otherwise newly-connected clients will immediately receive NEWVAL instead of CURVAL, which will force them to think "an event has occured!" */
А вообще надо битым текстом где-нибудь записать:
IS_AUTOUPDATED_YES
используется для каналов, обновляемых
по внешним событиям (в числе которых могут быть и записи в другие каналы);
т.е., когда присылать драйверу запросы на чтение бессмысленно. Но понятие
"fresh age" у таких каналов всё же есть.
IS_AUTOUPDATED_TRUSTED
применяется к каналам, которые не
просто бессмысленно читать "по запросу", а к тем, которые по факту НЕ
обновляются:
Здесь же всякие константы -- фиксированные минимумы/максимумы, CONST32 (=32) etc.
PZFRAME_CHTYPE_AUTOUPDATED
и PZFRAME_CHTYPE_STATUS
.
(Почему и _AUTOUPDATED тоже -- вопрос; надо разбираться... В записях ничего не нашлось...)
02.03.2021: добиваем драйвер в соответствии с полученными разъяснениями:
Так что заведён отдельный канал SPECIAL_START и они с SHOT'ом выполняют чуть разные действия (но GATESTAT возвращают оба).
Решено завести "кумулятивные" каналы (по аналогии с c_ilk_-каналами в драйверах источников, вроде ist_cdac20).
Решено просто в _hbt().
Реализация (её можно использовать для всех подобных гадких, мерзких, отвратных и уродских регистров с самостоятельно сбрасывающимися битами):
last_clks[]
, и в _init_d() всем элементам делается =-1 -- чтоб
следующее вычитанное значение точно не совпало.
Также заведены -- не массивами, а отдельными int32 --
last_C_PllWasUnlocked
и last_C_ExtWasUnlocked
,
которым также делется
инициализация =-1. 03.03.2021: неа, уже не =-1,
а =0.
last_clks[]
и
возвращается наверх.
ReturnDataSet()
.
Для этого заведена пачка массивов и бежит счётчик-указатель
count
, так что если какой-то канал надо отдать, то его номер и
значение записываются в addrs[]
и vals[]
, а затем
count++
.
Поскольку всё однотипно, то заполнение также необходимых dtypes[]=INT32,nelems[]=1,val_ps[]=vals+n,rflags[]=0 делается потом в отдельном цикле скопом для всех.
03.03.2021: также доделан скрин rfmeas_l_timer.subsys, так что теперь можно проверять.
Проверка показала косяки работы "запомненных" состояний: они с самого начала "горели", как будто успели вычитаться в состоянии 1.
-1
, и когда потом в _hbt()
делалось
me->last_C_xxxWasUnlocked |= v32
,
то получалось
"-1 | 1
" и оставалось -1.
Обоснование: это ведь ЗАПОМНЕННОЕ, оно в аппаратуре не существует, а поддерживается только драйвером, так что он имеет полное право отдавать своё значение.
|=
" поставить просто
"=
": ведь присваивается только "более высокое" состояние, и
тогда будет всё работать как надо -- -1 сразу заменится на любое другое, а 0
на 1..
R_nnn
, так что теперь почти везде это вместо констант 0xNNNNNN.
А для упрощения понимания и сличения с павленковскими инструкциями шестнадцатиричные адреса справа от каждой команды чтения/записи в комментариях.
Но пока:
Ибо это часть большого блока-массива, и пришлось бы городить надлежащие
_*base
и *_incr
.
*_shift
,
*_bits
, *_mask
).
Итого: уже вроде юзабельно для нужд ВЭПП-5, но надо б ещё доразобраться с возможностью переключать в режим "пропускать на backplane ВСЕ запуски" (и обратно в "только однократно после команды").
05.03.2021: приступаем к изготовлению возможности переключать режим работы между "только однократно после команды" и "пропускать на backplane ВСЕ запуски". Благо, после сегодняшних разъяснений Антона Павленко (в ответ на моё сегодняшнее же письмо с вопросом о предполагаемой последовательности команд) стало чуток понятнее, как это сделать правильно.
06.03.2021: что, кстати, реализуется неочевидно: там ведь
как бы нет никакой "записи в канал", а есть, как в любом pzframe-драйвере,
"актуализация" параметров в StartMeasurements()
. И как/когда
реально выполнять специальную запись? Напрашивается только при
"несовпадение текущего с предыдущим и при этом _BP или _BP_SYNC".
07.03.2021: напрашивается идея сделать то троганье регистров PLL1/PLL2 именно просто отдельным каналом (или каналаМИ), в которые просто отдельно записывать отдельно, для чего ему дать тип INDIVIDUAL.
08.03.2021: а можно и иначе -- вот пара последовательно пришедших идей:
StartMeasurements()
только в случае, если текущие значения регистров не совпадают с
запланированными.
06.03.2021: продолжение:
...но надо ещё раз Антона перечитать и, возможно, что-то добавить.
09.03.2021: был длинный телефонный разговор с Антоном Павленко по обсуждению моих вопросов, заданных в письме от 07-03-2021. По результатам было 2 письма -- 1) Антон прислал значения для записи в PLL-регистры ADC250, 2) я запротоколировал всё обсуждённое. Краткое резюме:
CLK_SRC/0x00012c = 2 PLL1_CTRL = 0x02008004 PLL2_CTRL = 0x30030F01 потом PLL_UPDATE. Процесс переключения источника тактирования занимает ненулевое время. Пока считаем, что за 0.1с должно устаканится. О состоянии цепи тактирования по битику [0x000100 ? STATUS].PLL_LOCKED. Если 1, то всё ок, если 0 - то нет. Битик не "интегральный", показывает статус в момент чтения.
Т.е.,
StartMeasurements()
категорически нельзя, ни по одному из алгоритмов.
Нужно как-то выделенной командой. Как именно (учитывая, что регистров 2 штуки по 32 бита, а параметров в них вообще 8 штук) -- пока неясно. Тестировать будем просто ручной записью в регистры bivme2_test'ом.
В связи с этим придуман некоторый алгоритм, по которому исполнение
EXT_TRG_SRC/0x400204:=0 будет производиться в _hbt()
, причём и
таймаут будет детектироваться.
spec_start_rqd_ticks
" -- счётчик (и
одновременно флаг "был запущен спец.запуск"), взводимый в =20 при запросе
спец.запуска, декрементируемый при >0
в каждом
_hbt()
, его ==0 после декремента сигнализирует о таймауте, а
событие "запуск" при !=0
означает окончание исполнения
спец.запуска.
Поэтому надо прямо в инициировании спец.запуска делать
last_LOG_WR_POSITION
:=LOG_WR_POSITION.
И делать это сразу после EXT_CSR/0x400074:=0x02(CloseGate), каковой шаг предполагается в любом случае добавить в начало цепочки команд, для приведения устройства в опреденённое состояние.
Позже, когда это всё записывал сюда:
И вот как тогда эту проблему решать -- вопрос кабы не более сложный; хотя, скорее всего -- ровно так же.
И, похоже, надо заводить канал "статус отработки последнего спец.запуска" -- удачно/неудачно.
09.03.2021: делаем!
Также самое событие его прихода является маркером окончания процедуры.
special_start_rqd_ticks
-- с которым проворачиваются
предусмотренные алгоритмы.
(Написано просто, а реально полдня напряжённого кодинга.)
Теперь проверять надо -- напряжённая будет проверочка (ещё бы ошибку протестировать -- вынуть кабель).
10.03.2021: мелкое изменение с каналов
SPECIAL_START_RESULT: вчера он был сделан TRUSTED, а это очень нехорошо --
прямо при коннекте приходили бы события UPDATE, что он якобы обновился, хотя
реально нет. Поэтому он переделан на AUTOUPDATED_YES плюс ему делается
SetChanFreshAge()
с et_fresh_age.sec=0x7FFFFFFF, т.е., ~68 лет.
11.03.2021: проверено -- оно работает.
У них такая утилитка называется vme_firmware, а описание процесса перепрошивки есть в файлике 20210714-pavlenko-Obnovlenie-PO-po-VME.docx, полученном по мылу от Павленко.
Возможно, придётся обзавестись своей такой же, чтобы не требовалось при любой перепрошивке тасовать блоки (либо свои VME-устройства тащить к ним, либо в свои крейты совать ихний MVME3100).
...если сделаю -- то утилитка будет универсальной, под ВСЕ поддерживаемые платформы. Поэтому оно имеет суффикс "_common" (по аналогии с vme_test_common.c). Плюс, чтобы имя было более пристойным, а не бесстыже-декларативно-всеобъемлющим "vme_firmware", добавлен префикс "lab6_".
Поэтому синтаксис имеет смысл сделать таким:
*_lab6_vme_firmware DEVSPEC COMMAND {COMMAND...}
...где команды будут что-то вроде
writeflash FILENAME.rpd
(взять прошивку из указанного файла и записать в устройство) и
readflash FILENAME.rpd
(считать прошивку из устройства и записать в указанный файл).
Также можно добавить команды "info" (выдать информацию о текущей прошивке), "compare FILENAME.rpd" (сравнить прошивку в устройстве с указанной).
Замечание: в качестве имени файла надо также принимать и "-" -- stdin для чтения и stdout для записи. Это позволит работать в pipe'ах -- например, прошивать образы, взятые непосредственно из .tar-файла (командой "tar xf FILENAME.tar -O firmware.rpd").
[@BUS_MJR[/BUS_MNR]:]ADDRESS_SIZE:ADDRESS_MODIFIER:BASE_ADDR
-- но есть пара нюансов:
Так что надо указывать и это смещение. Напрашивается синтаксис "+EPCS_OFFSET" после BASE_ADDR.
Напрашивается возможность опционально указывать размер данных после размера адреса -- суффиксом "d16" или "d32". После чего напрашивается возможность указывать перед размером адреса буковку 'a' -- чтобы полная спецификация выглядела бы совсем стандартно: "A32D32"; или даже опциональный '/' перед битностью данных -- чтобы "A32/D32".
18.07.2021: да, в vme_test_common'овский парсинг опциональная 'a' добавлена.
Итого, общим синтаксисом выглядит
[@BUS_MJR[/BUS_MNR]:][a]ADDRESS_SIZE[[/]d16|d32]:ADDRESS_MODIFIER:BASE_ADDR+EPCS_OFFSET
24.07.2021: сделан минимум:
vme_devspec_t
скопирована из test'а и к ней добавлены
поля data_size
и epcs_offs
.
main()
, из которого выкинуто почти всё, а
оставлен только getopt()
(обрезанный до "-h") и
PRINT_HELP, причём текст help'а уже подправлен.
26.07.2021: плотно приступаем к "скелету":
ParseDevSpec()
взята из vme_test_common'а и в неё
добавлен парсинг DATA_SIZE и EPCS_OFFS.
commands[]
определяется
fw_command_desc_t
, в ней указывается имя, указатель на функцию
и "тип аргумента" -- NONE,INFILE,OUTFILE,TEXT.
fd
, ...
STDIN_FILENO
и STDOUT_FILENO
.
И помнит в should_close_fd
, надо ли делать
close(fd)
, или нет (если stdin/stdout).
Слегка напрягает, что каждую утилиту приходится регистрировать не только в nsrc/ShadowRules.mk, но и во всехних Makefile'ах.
29.07.2021: devinfo_proc()
запилена и
работает -- вычитывает и печатает значения регистров (ну да, было б чему не
работать :D).
А вот само вычитывание регистров сделано в read4b()
: она в
будущем должна будет смотреть на поддерживаемый режим адресации и размер
данных (32 или 16) и выполнять соответствующие действия, а пока просто
впрямую делает vme_hal_a32rd32()
.
31.07.2021: ещё чуток:
write4b()
.
mirror32bits()
-- простенькая, но по алгоритму,
как если бы делалось на ассемблере через флаг Carry; только вместо прямого
передвижения через флаг пришлось делать условие: тестировать младший бит в
источнике (сдвигаемом вправо) и при взведённости взводить в приёмнике (перед
тем сдвигаемом влево).
readflash_proc()
.
02.08.2021: readflash_proc()
доделана до
некоего рабочего состояния.
-v
, при
котором инкрементируется option_verbose
и на stderr выдаётся
бегущий текущий адрес (с помощью '\r').
Но для сравнения с прошивкой в файле от Павленко из прочитанных 2МБ просто вырезается ("dd bs=1c count=СКОЛЬКО_НАДО") нужное количество.
...а первого 1КБ -- совпала. И первых 10, и 100, и 600... И 691 (707794/1024) тоже.
И тут допёрло: ведь 707794 на 4 нацело НЕ делится. И что за странный объём даденного .rpd-файла -- загадка.
А вот когда вырезал 707792 байта -- и от firmware.rpd тоже -- то MD5-суммы этих вырезок уже совпали.
Итого: некоторый минимум достигнут, дальше надо спрашивать Павленко/Котова. Им письмо с 5 вопросами (3:endianness, 4:D16 в S/L-timer, 5:как определять размер залитой прошивки и почему файл не кратен 4 байтам) отправлено, ждём ответов.
11.01.2021: ответы получены ещё в августе, письма конкретно от 12-08-2021 и 27-08-2021 (Message-ID: 1628762381.287805387@f171.i.mail.ru и 1630053966.333850346@f187.i.mail.ru). Вкратце (в моём изложении) они таковы:
reading()
в
EPCS_BUFF_RD_ADDR пишет ДВАЖДЫ (перед чтением каждого блока и перед чтением
слова):
От меня: видимо, как раз по этой причине.
Ибо прошивка -- просто сырой бинарник, скармливаемый FPGA, а та "при старте просто начинает считывать свою прошивку с адреса 0 из флеш и пока не считает всю", и никаких информационных полей там не было предусмотрено.
11.01.2021: за прошедшие несколько дней доделана также
и запись -- writeflash_proc()
.
readflash_proc()
.
...а вот если прочиталось <=0 -- т.е., ничего совсем либо ошибка (откуда б?) -- то тогда очередная запись слова уже пропускается.
Но в обоих случаях взводится end_of_file = 1
.
(В том исходнике это тоже делается, но условие выглядит столь запутанным, что не сразу его и опознаешь.)
#if
, проверяющий
BYTE_ORDER
, чтоб конверсия выполнялась только на
LITTLE_ENDIAN
.
readflash_proc()
-- аналогично.
#include "cx_sysdeps.h"
.
read_r = read(fd, bytes, sizeof(bytes)); if (read_r < sizeof(bytes)) end_of_file = 1; if (read_r <= 0) goto END_READ_BLOCK; #if BYTE_ORDER == LITTLE_ENDIAN w = bytes[3] | (((uint32)bytes[2]) << 8) | (((uint32)bytes[1]) << 16) | (((uint32)bytes[0]) << 24); #elif BYTE_ORDER == BIG_ENDIAN /* Do-nothing */ #else #error Sorry, the "BYTE_ORDER" is neither "LITTLE_ENDIAN" nor "BIG_ENDIAN" #endif w = mirror32bits(w);
Т.е.,
bytes[]
прочитанные данные в
w
, которая уже используется для записи!
А попросту зеркалит туда-обратно имеющийся в w
мусор, и он
же потом пишется во флэш...
bytes[]
вручную -- он endian-agnostic!
readflash_proc()
тоже выкинул, как и
#include "cx_sysdeps.h"
.
Попробовано на одном девайсе, подключенном через A3818 -- вроде пашет.
-v
, заставшяющее печатать адреса,
в которые идёт запись, видно, что оно пишет "пачками": адреса бегут-бегут,
потом на границе EPCS_SECTOR_SIZE (=0x00010000) небольшая пауза, потом опять
бегут-бегут, потом на следующем кратном 0x10000 опять пауза, и т.д.
Павленко/Котов рассказали: пишется блоками (по BUFF_WORD_NUM в 128=0x80 слов), т.к. запись возможно в любое место (?), а стирание выполняется при записи в 0-й блок сектора -- т.к. стираться может только секторами, вот оно и выполняет стирание сектора автоматически при записи в его начало.
11.01.2021: а теперь проверка на BIVME2...
То, как оно "делалось" -- страшенный косяк, ибо на little-endian оно
работало, а на big-endian -- не делалось даже просто копирования прочитанных
добытых из файла значений bytes[]
в w
(каковая и
отправляется в VME).
Вот оно просто зеркалило туда-обратно мусор, имевшийся в w
,
потому во флэш и писались чередующиеся значения.
...подробнее -- см. comment2 выше, там же, где эта горе-конверсия и описана.
i:0x104=0x80000000
".
А это уже живо напомнило про косяк strtol()
-- что у него
числа со старшим битом вызывают "переполнение" и он возвращает значение
0x7FFFFFFF и errno=ERANGE.
strtol()
вместо надлежащей strtoul()
(и в
куче других мест тоже -- ВЕЗДЕ, кроме парсинга базового адреса в
ParseDevSpec()
.
КАКОГО ЧЁРТА?!?!?!
Итого:
...мда, ну я и балбес...
12.01.2021: добиваем недоделанное вчера:
bytes[]
перед чтением из файла
-- чтобы при записи неполного слова во флэш туда попадали бы нули, а не
мусор.
...по-хорошему -- надо бы нулить только если прочитано <4 байт, конкретно непрочитанные. Но лень.
13.01.2022: новая напасть: при попытке ЧТЕНИЯ прошивки из обновлённых ADC250 (BASE_SW_VER=0x08, в клистронке с BIVME2) получаем облом: сначала чтение отрубается по таймауту на каком-то адресе (причём НЕ по границе сектора!), а следующее чтение срубается уже сразу.
(Кусок "скриншота" закомменчен тут выше.)
P.S. Хохма в том, что чтение-то я решил попробовать исключительно для проверки того, как работает новый вариант verbose-выдачи текущего адреса (с '\r' в начале, а не в конце -- чтобы в случае ошибки адрес бы остался виден). Вот как раз и "проверил" -- да, работает, именно "нужная" ошибка и вылезла...
13.01.2022@ванна, ~19:00: надо бы ещё поддержку как минимум A32D16 сделать -- чтобы с S-Timer можно было использовать.
@~22:30: вроде сделал -- причём сразу всю матрицу вариантов, включая A16 и A24:
read4b()
и write4b()
превратились в
монструознейшие конструкции -- 2-уровневые условия, плюс при D16 каждое
чтение/запись разбивается на 2 операции.
data_size
, а внутреннее -- по
-- addr_size
; так всё равномернее.
r = vme_hal_a...()
-- изначально-то ничего
не проверялось, а уж с двойными операциями чтения и записи тем более лень.
Опять же -- надо проверить, но для этого лучше дождусь выяснения по проблеме облома чтения прошивки. 20.01.2022: проверил -- по крайней мере, ЧТЕНИЕ работает корректно, MD5-сумма совпала. Так что раскомментированы "селективные варианты" и в чтении, и в записи.
17.01.2022: сегодня наступило продолжение банкета: обнаружилось, что тогда, 13-го (спасибо, что была не пятница :D) осциллографы сразу же ещё и "зависли" -- перестали осциллографировать.
(Shame on me -- надо было тогда же сей факт и проверить; но и я не проверил, и на пультовой никто внимания не обратил.)
17.01.2022: дождался, проблема разъяснилась (Message-ID: 1642421806.662795354@f385.i.mail.ru).
Вероятно, разница в том, что в моём драйвере adc250_drv.c используются прерывания, а у себя на столе он проверял поллингом по регистру статуса.
19.01.2022: всё оказалось просто -- MVME3100 пытался работать как системный контроллер на пару с BIVME2; после успокоения его джампером виснуть перестал, и чтение прошивки тоже не зависает.
19.01.2022: проверил, как прошилось впопыхах сделанной утилитой 13-01-2022 через A3818, не накосячено ли (например, некорректной "условной конверсией"): нет, не накосячено -- результат вычитывания, будучи обрезан до тех же 710700 байт, что и исходная прошивка 0x08 в файле 20220111-kotov-adc4x250_osc.rpd, имеет ту же MD5-сумму ac012a6adb4fb25309ba7b452edc0cf2.
24.01.2022: посидели с Котовым у меня в 1П-613, проблему быстро воспроизвели (он подключился к ADC250 0x98 через JTAG и залил туда "отладочную" версию той же прошивки, позволяющую заглянуть в её состояние при помощи GDB, тормознув её работу).
25.01.2022: выдал Котову исходники CX плюс бинарники v4bivme2vmeserver и bivme2_lab6_vme_firmware, чтоб он мог воспроизвести проблему у себя на столе.
26.01.2022: Котов (Message-ID: 1643170355.645577285@f452.i.mail.ru) 11:12 NSK: "Cx запустил, воспроизвести удалось, починить пока нет.".
27.01.2022: Котов (Message-ID: 1643227424.128990379@f377.i.mail.ru) 03:04 NSK (23:03 MSK):
Починил! Плата зависала, когда одновременно выполнялось чтение EPCS и генерация VME прерывания. Проблема была в Verilog коде.
04.02.2022: проверяем, реально ли ошибка исправлена.
b360mc:~% ls -l *.rpd | head -2 -rw-r--r-- 1 user user 704446 Jan 27 09:16 20210126-kotov-adc4x250_osc_61F1A2C8_2022.01.27.rpd -rw-r--r-- 1 user user 710700 Jan 11 13:25 20220111-kotov-adc4x250_osc.rpd
04.02.2022: кстати, был косяк в main()
'е
в цикле по командам: отсутствовала проверка результата (кода возврата),
поэтому ошибка не приводила к завершению работы. Это мешало тестированию
"не подвисает ли при чтении прошивки во время осциллографирования" -- там
делалось циклом while (приведён в закомментированном виде чуть выше).
Исправлено -- код берётся, проверяется, и
if (r != 0) exit(1);
.
Предыдущие драйверы либо копировались из v2cx/, либо (быстрые АЦП/осциллографы) рассматривались в разделе pzframe.
Но сейчас pzframe уже более-менее дошёл до зрелости, так что там конкретные драйверы обсуждать смысла мало, а надо делать драйвер для F4226, потенциально могущий принести некоторое количество забот и сюрпризов, потому требующий своего раздела.
Вот и будем делать драйвер, взяв за образец c061621_drv.c.
14.03.2018: начато.
C061621
->F4226
и
c061621
->f4226
.
_MAX_NUMPTS
с 4096 на 1024.
15.03.2018: продолжаем.
q2xs[]
, с актуализированным
наполнением.
15.03.2018@лыжи: а ведь у девайса есть еще один параметр -- "история" (сколько измерений должно идти "до" импульса старта, а сколько после).
Так вот: а можно ли -- в рамках интерфейса драйверы/pzframe_drv<->pzframe_date -- отдавать наверх ОТРИЦАТЕЛЬНЫЙ сдвиг по времени (который обычно бывает положительным вследствие PTSOFS)?
16.03.2018@утро-дорога-на-работу-около-ИПА: кстати, а сдвиг истории -- не просто ли множитель на 40 0*40=0, ..., 24*40=960)?
26.03.2018: да, именно так, только множитель не на 40, а на 64: 0*64=0, ..., 15*64=960.
03.04.2018: движемся дальше:
Канал назван prehist64 и получил номер 24 -- сразу после range.
А вот ради cur-версии, cur_prehist64, пришлось сдвинуть каналы с on и далее на +1 -- иначе места не хватало. Так что совместимость с c061621 по номерам ушла. И теперь в блоке "статус" (30...49) остался один-единственный свободный канал -- 49-й.
PrepareRetbufs()
.
С другой же -- именно mes_p->cur_ptsofs
используется в
FastadcDataX2XS()
для вычисления значения времени для подписи
по X-оси графика.
И более значение CUR_PTSOFS не используется ни для чего.
Из изменений -- вместо удалённой ручки "devtype_str" добавлена ручка-селектор "prehist64", и поставлена ПОСЛЕ ручки PTSOFS.
23.11.2018: возвращаемся к драйверу. По факту, "скелет" уже сделан, осталось "мясо", работающее с железкой -- всё, что делает NAF'ы и преобразует данные.
InitParams()
сделано вычитывание TIMING и RANGE из
статусного регистра.
f4226_params[]
также добавлена возможность
указывать значения этой парочки в auxinfo.
StartMeasurements()
наконец-то сделано прописывание
параметров в устройство -- и параметров этих всего трое: TIMING, RANGE и
PREHIST64; все трое идут в статусный регистр.
25.11.2018: напихиваем "управляющие" NAF'ы. Во всех хоть сколько-то критичных местах -- инициализация, начало Start, Abort, Read -- вставлена парочка A(0)F(24) и A(0)F(10): это маскирование LAM и сброс LAM.
Однако:
По списку NAF'ов в описании не ясно ни то, ни другое.
26.11.2018: изучение документации показывает, что:
Оный NAF добавлен в Init и Abort, ПЕРЕД Mask и Drop.
28.11.2018: с возможностью использования режима "запись от ЭВМ" в качестве "стоп" -- фигушки!
Предварительно маскировать LAM?
Сделано -- вся кухня (замаскировать+сбросить, переход в "Запись", 1024
записи) вытащена в отдельную PerformStop()
-- вроде работает
как надо, больше LAM'а не возникает.
...впрочем, его и так не возникает никогда -- фиг поймёт почему: то ли реально запуски не приходят, то ли я чего не то делаю...
03.12.2018: но то в v5-l-kls3, а в v5-l-cnv (у которого проблемы с Q) -- 2080=0x820=04040.
А еще некоторые сомнения в реальной рабочести чтения -- на вид, читаются сплошь нули...
29.11.2018: умаялся я с этой железкой!!! Работает она совсем не в соответствии с описанием.
26.03.2018: попалось случайно, при поиске драйверов для железа управления клистронами.
Это СДС-16. Использовался, судя по work/pult.20070608/configs/cm5307-9.lst, в "старой", CAMAC'овской, термостабилизации. Точнее, ДОЛЖЕН был использоваться -- реально эти битики Лебедевым никогда заведены не были.
Также и впредь вряд ли будет использоваться, т.к. в списке описаний помечен как "Не поддерживаем".
Но раз уж драйвер в дереве есть, то пусть будет "всё как положено".
#
'ченной части
CAMACDRIVERS
.
TYPES_LIST
в {types,include/drv_i}/Makefile.
Также добавлены в список, собрались.
26.03.2018: вечером: оказывается, нужный для клистронов Р0610 -- близкий родственник Р0601 (развитие 1987 года девайса 1978 года, от того же автора).
Так что пригодится -- либо прям его и использовать (снизу вверх вроде совместимо), либо на его основе сделать r0610_drv.c.
27.03.2018: анализ документации показывает, что функционально Р0610 вообще не отличается от Р0601 -- добавленные NAF'ы реально никакой нагрузки не несут: F(3)A(0) идентичен F(0)A(1), смысл F(9)A(1) неясен (тупо "упрозрачивание" всего?), а F(6)A(0) тоже без надобности ("чтение дескриптора блока", Р0610 => 0314; других-то и нет).
Решение такое: пока будем использовать драйвер r0601, а если захочется реагировать на блокировки быстрее, через LAM'ы, то сгородим r0610.
17.09.2018: замечена странность: на ВСЕХ клистронных CM5307 наблюдается очень высокая загрузка процессора драйверами r0601.drvlet: они потребляют по 6-10% CPU.
Пока в голову приходит только одна идея: им каждому прилетает по 16 команд чтения (по числу каналов) каждый цикл, а частота 10Гц, вот и получается по 160 запросов в секунду. Каждый запрос же -- это 1 чтение из DRIVELET_INPUT_FD, 1 NAF (read() из CAMAC_REF), 1 отправка результата. Итого -- 160*3=480 syscall'ов в секунду. Возможно, именно они и просаживают.
Но по-хорошему надо б отпрофилировать (благо, должно быть несложно: собрать драйвлет с профилированием, запустить rrund из writable-директории, запустить сервер на минутку и потом грохнуть -- сокет закроется, драйвлет корректно завершится с записью статистики).
Для этого потребуется auxinfo-параметр mask= (умолчательно =-1, что означает "не записывать").
val_cache[]
+rfl_cache[]
, с начальными
CXRF_CAMAC_NO_Q
) -- берём из g0603_drv.c
02.04.2018@утро-пультовая: сделан, компилируется (хотя, естественно, не проверен).
_rw_p()
, а прямо вручную -- дублированным
кодом, это оказалось проще и короче.
Остальные биты форсятся в 0 (прямо в _init_d()
обнуляются),
включая биты "05 - запрет автоматической перезаписи по концу рабочего
цикла" и "04 - прерывание цикла работы ГВИ по каналу 7".
Анализ DB/*.h-файлов от v2 внятных результатов не дал: хоть вроде бы должны указываться в 100-наносекундных юнитах, но сходу не удалось найти коэффициентов ни 10.0 (до микросекунд), ни 0.01 (до наносекунд).
По результатам решено было сделать как в CGVI8 -- R=10.0, а с frolov_d16 поразбираться отдельно.
03.04.2018: после некоторых размышлений решено-таки добавить поддержку бита #3 "04 - прерывание цикла работы ГВИ по каналу 7" (но НЕ следующего, который "запрет автоматической перезаписи" -- он потребовал бы слишком сильно менять поведение драйвера).
_BREAK_ON_CH7
/break_on_ch7 и
получил следующий за period_mode номер -- 29.
break_on_ch7
-- де-факто могущее
быть 0 или 1, которое:
g0401_params[]
.
break_on_ch7<0
--
а в psp-таблице умолчание именно -1
.
Таким образом, "текущее" значение считывается из железа только в случае, если оно в конфиге нет явного задания.
Кстати, а не сделать ли аналогичную возможность указывать в auxinfo значение и для period_mode?
03.04.2018: также сделан скрин g0401.subsys (хотя для изменения просто чисел можно пользоваться скрином cgvi8.subsys).
04.04.2018: мелочи:
-1
, так что при НЕуказанности
читается текущее значение из железки (в точности, как с
break_on_ch7).
Переименование коснулось и поля в privrec'е, и имени канала.
period_mode_rfl
было переименовано в
creg_rfl
-- так корректнее, поскольку реально это флаги от
обращения к регистру управления.
quant = me->period_mode * (1 << (7 - me->period_mode));
-- теперь оное домножение убрано и стало
quant = (1 << (7 - me->t_quant_mode));
06.09.2018: дошли руки начать деплоить и проверять. Да, косяков есть у меня:
Добавлена.
Оказалось, что запись делается A(8)F(0) -- что есть ЧТЕНИЕ. А писать надо A(8)F(16). Исправлено, стало сохраняться записанное.
15.09.2018: драйвер наделён способностью "опознавать"
g0604. "Опознание" сводится к проверке, что на A8F0 не получаем XQ. При
этом НЕ взводится свежевведённый флаг creg_supported
, так что
на каналы T_QUANT_MODE и BREAK_ON_CH7 отдаётся UNSUPPORTED, плюс делается
принудительное t_quant_mode
=7.
19.09.2018: и еще пара аспектов:
Добавлено принудительное обрезание CXRF_CAMAC_NO_Q
при
creg_supported==0.
В чём конкретно дело -- хбз: возможно, просто глюк.
Тут и пригодилась возможность указывать t_quant в auxinfo: проставлено t_quant=100 и теперь при запуске сервера выполняется надлежащая инициализация.
...а возможно, надо будет всё-таки ещё добавить возможность указания там же "начальных значений" задержек -- чтоб они при старте сервера прописывались.
Основная идея: берём код от g0401_drv.c, но работа с регистром управления удаляется, а функционирование "period_mode" сводится к записи в privrec константы, указанной в auxinfo, и отдаче в канал PERIOD_MODE её же.
...хотя одна засада есть: в старом ГВИ8 (Г0604) есть режимы 100нс и 1000нс(=1мкс), а в ГВИ8М второй из них отсутствует...
Драйвер просто никогда не проверялся на живом железе.
А в старых, v2'шных -- тоже в скринах почему-то калибровок нету; точнее, в v2hw/chlclients/DB/db_nfrolov_d16.h обнаружены коэффициенты "4"...
02.08.2018: чуток разбирательств:
Т.е., по факту каналы V и работают прямо в наносекундах.
Так что, похоже, эти коэффициенты там лишние и только всё портят.
Т.е., похоже, что функционал "нового" драйвера -- nfrolov_d16 -- толком испытан и не был: этот новый использовался только на стенде ЛИУ, а там всё работало в режиме махарайки.
18.01.2019: вопрос давно решён: никаких калибровок драйверу уставлять не нужно, т.к.:
И каналы V с тех пор уже переведены на вещественные (double), а там калибровки зачастую не нужны вовсе.
Так что и раздел "done".
02.08.2018: исчо прикол: Федя желает ловить сигнал окончания от одной конкретной D16 (крейт RFSYN, позиция 20).
Ловить окончание вроде как положено LAM'ом. Но тут есть противоречие:
Т.е., в одном NAF'е (A0F10) совмещено ДВЕ разных функции. Проблемка...
При этом, очевидно, надо иметь какой-то флажок, который по команде DO_SHOT (сброс LAM'а) сбрасывать, а проверку по поллингу выполнять только при сброшенном, и при Q==1 взводить обратно?
И не вылезет ли тут race condition...
После обеда: нет, не нужно ни "флажок", ни сбрасывать/взводить его! Надо помнить предыдущее состояние (сначала =-1), и делать Return тогда, когда текущее от предыдущего отличается, сохраняя вёрнутое в предыдущее. Плюс, по команде DO_SHOT надо тут же делать проверку с условным возвратом -- так, возможно, мы минуем race condition. ...и, возможно, ПЕРЕД тоже надо делать проверку с условным возвратом -- на случай, если DO_SHOT дёргается уже после окончания цикла, но ещё ДО очередного поллинга.
Как утверждается, вроде бы Гусев именно так и делает.
В любом случае, карта каналов уже чуть подшаманена:
Появились они еще в v2'шном nfrolov_d16_drv_i.h. Видимо (судя записи в bigfile-0001.html от 24-02-2011), где-то в 2011-м.
02.08.2018: неа, по словам ЕманоФеди, вариант с поллингом не прокатит: опрашивать имеет смысл с периодом 100мс, а это слишком большое время -- характерное время накопления электронов как раз в районе 100-200мс, и лишняя сотня задержки будет очень большой потерей.
03.08.2018: поступило предложение-идея от Фролова, как обойтись прямо сейчас, не переделывая ничего в железке.
1<<4
в регистре статуса), а уже потом делать A0F10.
Естетственно, такой режим работы должен включаться отделной опцией в auxinfo.
Делаем.
use_lam
, и для его установки auxinfo-флаг use_lam
(антоним -- ign_lam).
LAM_CB()
:
_init_d()
:
IS_AUTOUPDATED_TRUSTED
.
07.08.2018: нифига НЕ TRUSTED, а надо
IS_AUTOUPDATED_YES
-- причины см. в разделе о frolov_ie4_drv за
сегодня же (вкратце: иначе клиенты получат NEWVAL вместо CURVAL).
use_lam
:
04.08.2018: хотя, если так подумать -- а зачем? Ведь если LAM уже горит, то сразу после регистрации он и сработает.
_do_rw()
отработка DO_SHOT в режиме use_lam
сводится к сбросу бита блокировки запусков.
SetALLOFF()
, которая просто
вызывает _do_rw()
с DRVA_WRITE, передавая ей нужное значение.
Таким образом, не просто меняется значение битика, но оно еще и сразу отдаётся наверх в канале ALLOFF.
06.08.2018: проверил -- такой способ обращения с LAM работает.
А вот конкретно та специальная Д16 в позиции 20 -- НЕ работает. Точнее, в "непрерывном режиме" она молотит как надо (так и проверили рабочесть), а в однократном -- нет. Просто полный игнор.
Стали с Фроловым разбираться -- разобрались.
1<<6
-- стоит в ожидании прихода START (==0) либо
уже был старт (==1).
Имеет смысл только в однократном режиме.
1<<7
-- ==1 у Д16П, так её можно отличить
программно.
...и дополнительная пара NAF'ов:
И называется она "Д16П" (или "D16P").
1<<6
желательно бы отдавать наверх каналом.
Причём максимально оперативно -- как lam_sig.
LAM_CB()
;
Но тут может быть засада.
1<<6
(он его использует в своём коде).
Теперь надо сообразить, как лучше это всё встроить в драйвер.
Может, сделать "режим работы", 3-вариантный переключатель?
06.08.2018@вечер-дорога-домой, около-ИПА: да, надо делать 3-вариантный переключатель и во всех режимо-зависимых точках if()'ы.
Режимы назвать "NONE", "MSKS" (MaSK Starts) и "D16P".
07.08.2018: делаем.
1<<6
, назван WAS_START.
Кстати, этот битик -- аналог такого же IE4'шного, где он маппируется на канал BUM_GOING; только там он отдаётся в инвертированном виде.
CXRF_UNDUPPORTED
; видимо,
бывший (когда-то давно, на заре nfrolov_d16) UNUSED1).
use_lam
" переименовано в mode
.
22.10.2018: "mode
" переименованов
lam_mode
-- чтоб терминологически не интерферировать с вводимым
init_mode
(указываемым в auxinfo значением для уставки в канал
mode).
MODE_EASY
, MODE_MSKS
,
MODE_D16P
. В auxinfo указываются эти же 4-символьные флаги.
Выбрано "easy" как более предметное, чем странное "none".
22.10.2018: а префикс "MODE_
" переделан в
LAM_MODE_
.
LAM_CB()
теперь маскирует старты только в режиме MSKS, а
в D16P дёргает возврат WAS_START.
_rw_p()
:
mode
.
_init_d()
:
1<<5
).
1<<7
).
Проверил -- вроде работает (только ONES_STOP пока проверить не удалось).
18.01.2019: драйвер давно отшлифован (в том числе с
каналами V (переделанными на double
), и с "правильной"
конверсией {A,B}<->V), и начинён новыми фичами, и давно уже
эксплуатируется на ВЭПП-5.
Так что первоначальная задача выполнена.
...и меняется частота Fin крайне редко -- обычно стоит значение вроде 21874.8098KHz (оно же 45.7146831ns).
Ну и чё -- кто-то должен это значение прописывать? А если сервер перепускается и драйвер рестартует?
Разумным выглядит завести auxinfo-параметр, чтоб значение указывать прямо в конфиге.
10.08.2018@вечер: вот только есть проблемка: канал/параметр "f_in_ns" -- в наносекундах. Но он ЦЕЛОЧИСЛЕННЫЙ, а при числе 45.7146831 отбрасывание дробной части даёт слишком большую погрешность.
Что вообще ставит всю концепцию пересчёта {A,B}<-(f_in_ns)->V под большой вопрос.
11.08.2018@дома, суббота: проблема в вещественных, так? Которые на мамкинском контроллере (powerpc-860?) без FPU крайне тормозны, так?
Ну так можно сделать драйвер-надстройку, работающий -- через vdev -- уже на хосте, имеющий cda-линки на все вовлечённые каналы реального драйвера и отдающий наверх правильно пересчитанные каналы.
12.08.2018@~15:25, воскресенье, на Будкера, пешком из Быстронома в ИЯФ через ОК: а может, не выёживаться и сделать 1 (один) именно реально вещественный канал прямо в драйвере frolov_d16?
Тем более, что в паре PowerPC-контроллерных драйверов мы вещественную арифметику уже использовали:
float32
домножался на 1e9 и отдавался наверх преобразованный к int32
Детали идеи реализации:
PSP_P_REAL()
, сводящийся к
strtod()
и вещественным сравнениям.
frolov_d16_rw_p()
-- там
арифметика (умножение, деление, остаток от деления) и сравнение с 0.0.
CxsdHwSetDb()
-- бе-е-е...).
14.08.2018: приступаем к массивной переделке драйвера.
FROLOV_D16_CHAN_nnn
приведены к современному
стандарту: "_BASE
" заменено на "_n_base
".
F_IN_NS
превращено в float64
,
плюс ему навешен auxinfo-psp-параметр "f_in", с умолчанием 0.0 и
разрешенным диапазоном [0.0,2e9] (т.е., до 2e9ns=2s).
_rw_p()
:
vp
, dt_f64
, n_1
).
ReturnDataSet()
и вместо
break
исполняется
goto NEXT_CHANNEL
-- чтобы избежать обычного ReturnInt32Datum()
'а в конце цикла.
double
: тип сменили
v_fclk
и quant
, вычисление остатка делается через
fmod()
вместо %
.
...а наверное, можно б было вычислять c_Ai
и
v_R
одной операцией -- вызовом remquo
. Но там в
документации какие-то оговорки про то, что в quo попадают "a few bits of
the quotient". И нагугленные
обсуждения
вообще говорят про "3 бита", но без конкретики, и что это так в стандарте.
Так что лучше воздержимся.
Проверено -- на вид, как ни странно, вроде работает!
16.08.2018: а вот нифига -- корректно работает только в сторону "{A,B}->V", обратное же преобразование имеет какие-то косяки по округлению.
Например, при Fin=45.7146831: {3,0}=>137.144, но запись 137.144 даёт {2,180}=>136.429. Запись же 136.429 НЕ приводит к стабилизации, а даёт уже {2,176}=>135.429; 135.429 -- {2,172}=>134.429.
Т.е., на каждой итерации отрезается по 4 единицы B, или по 4*250пс=1нс.
17.08.2018@утро-дома: а не сделать ли еще один канал -- "F_IN_HZ", чтоб можно было указывать именно частоту?
Реализация тривиальна:
if(dval<0)dval=0;
,
if (dval == 0.0) F_IN_NS = 0.0; else F_IN_NS = 1 / dval;
@на-работе: сделано. Только
пересчёт не через 1/...
, а через 1e9/...
-- у нас
же НАНОсекунды.
17.08.2018: а теперь о пересчёте каналов V.
Часть багов нашлась быстро -- следствия перехода на double
:
c_Bi / 4
вместо
c_Bi / 4.
-- в результате от 1 до 3 квантов по 0.25нс могло
отбрасываться -- этим и объясняется, что "отрезается по 4 единицы"
(стало по 1 единице; об этом ниже).
v_R
оставалась int
, а ведь ей
присваивается результат fmod()
, затем
c_Bi = v_R * 4
-- т.е., тоже могло теряться от 1 до 3 квантов
по 0.25нс.
Но после этих исправлений "спад" лишь уменьшился до 1 кванта, но не исчез.
c_Bi = v_R * 4
.
И очевидное, казалось бы, решение -- перед этой операцией добавлять
к v_R
пол-кванта -- 0.125. Что после умножения будет давать
0.5 и тем самым должно б быть эквивалентно обычному "округлению до
ближайшего целого".
Разные мысли в голову лезут.
c_Ai
:
19.08.2018@утро, воскресенье, пешком в 13-е вдоль 4-го: раз у нас теперь есть умение указывать каналом прямо частоту, а auxinfo-параметр называется "f_in" -- нехорошо получается: указываем ведь реально наносекунды, а не частоту!
А можно сделать так:
Сделано. Проверено -- работает.
19.08.2018: теперь возвращаемся к странностям при записи в V.
Еще попялившись на результаты записи разных чисел, а главное --
получающегося при этом значения v_R
(остатка от деления), понял
причину:
В реальности {3,0} -- это вовсе НЕ просто 137.144, а 137.1440493000.
И именно этого 0.0000493 и не хватало, чтобы деление на 45.7146831 дало целую часть 3, а не 2:
Вывод:
frolov_d16_rw_p()
корректно делается, не
нужно более никаких махинаций с добавлением 0.125 и т.п.
22.08.2018: (@обед, 5-й этаж): мысль -- а ведь можно было выдрепнуться проще: оставить каналы int32, и при смене F_IN_NS просто отдавать наверх R каналов V так, чтобы нужный пересчёт выполнялся на клиенте.
(@пятнадцатью минутами позже, у себя): неа, нифига:
...а вот для ИЕ4, без B -- возможно, что и подошло бы.
Да, можно сразу после смены форснуть возврат значений; но всё равно некрасиво/сомнительно.
Но сама идея забавная, хоть Д16 и не прокатит.
Это можно сделать аналогично пред-программируемым настройкам
adc200me/adc812me etc.: в auxinfo указывать желаемые значения, чтоб при
специфицированности этих значений в _init_d()
вызывалась бы
_rw_p()
для соответствующих каналов с этими значениями.
22.10.2018: делаем.
init_val_of_ИМЯ_КАНАЛА
(ИМЯ_КАНАЛА -- fclk_sel,
start_sel, mode).
_params[]
.
-1
.
WriteOne()
.
_init_d()
проверяется для каждого из
троицы, что если значение init_val_of_NNN
>=0, то вызывается
if (me->init_val_of_NNN >= 0) WriteOne(me, FROLOV_D16_CHAN_nnn, me->init_val_of_NNN);
...проверять?
24.10.2018: да, проверено, работает.
05.11.2018@дома-после-обеда: также добавлена возможность указывать Kstart. Ибо он, по крайней мере у парочки Д16 в крейте впуск-выпуск, при включении питания имеет значение 0, а это вообще блокировка запусков.
06.11.2018: и уж для комплекта Kclk. Скопировано от IE4.
Возможно, с D16 надо поступать аналогично. Но тут всё чуток усложняется наличием ещё и B -- так что его (воизбежание "раздувания") надо будет считать не как v_R=fmod(dval,quant), а как v_R=dval-c_Ai*quant, с последующей проверкой на отрицательность.
01.11.2018@вечер-дома: и
даже сделал такой код в frolov_d16_rw_p()
, за-#if0'енный.
А потом сообразил: НЕЛЬЗЯ! Нельзя тут округлять, т.к., в отличие от IE4, здесь остаток от деления не отбрасывается, а используется для B.
02.11.2018: потестировал немножко -- да, D16 тоже подвержен проблеме "при обратном пересчёте получается A на 1 меньше исходного": при Fin=21.8748098e6 -- A=100,B=0 -> V=4.571 (реально вроде 4.57075), а при записи обратно -- V=4.571 -> A=99,B=180.
04.11.2018: поразбирался еще -- стало ясно: проблемы реально НЕТ.
05.11.2018: после изложения вышеприведённых соображений ЕманоФеде пришли к "решению": надо добавить экранным полям 2 знака после запятой.
07.11.2018: сделано, на пульт скопировано. Будем посмотреть.
27.11.2018: вылезло-таки боком то "использование"
округления/round(). Там вместо "#if 0
" стояло "#if
1
" -- явно просто промахнулся мимо кнопки, а в joe в xterm, где
пропроцессорные директивы отображаются тёмно-синим на чёрном фоне, нифига не
видно.
Вот этот сомнительный код понакуролесил -- например, "кручение вниз" (колёсиком) не работало.
Странно только одно: выявился этот косяк лишь вчера -- одновременно с проблемой "с правильным F_in_ns поехали все уставки" (Беркаев позвонил в районе 8 вечера, что как-то очень уж фигово всё работает). Но если ТОТ код был еще с 05-11-2018 (да, проверено -- на пульту драйвер был за ту дату), то почему проблема не проявилась раньше?
06.02.2019: после очередного инцидента "а-а-а, у тебя неправильно значения записываются!!!" (когда после записи ТРИПЛЕТА {b,a,v} оказывалось, что конечное b (полученное из v) отличается от записанного напрямую на 1) был проведён анализ кода ещё раз.
И возникло подозрение о некорректности вычисления B. Там делается
Т.е., значение "остатка" считается как просто арифметический остаток от деления вещественногоc_Ai = dval / quant; if (c_Ai > 65535) c_Ai = 65535; v_R = fmod(dval, quant);
dval
на вещественный quant
.
Но не будет ли корректнее считать не "математический" остаток от деления, а "вычитать отделённое" от исходного значения:
для не-отбрасывания дробной части (так думалось в момент анализа)?v_R = dval - c_Ai * quant;
Или результат fmod()
будет ровно таким же? А погрешности
округления?
Надо бы взять тестовые значения -- b=6,a=24,v=1.09865239672621 (quant=47.????) -- и проверить расчётами вручную.
26.09.2019: вчера опять вылезла проблема: по словам ЕманоФеди,
в некоторых случаях многократное нажатие энтера в полее ввода в окне синхронизации приводит к систематическому снижению уставки на некий шаг, например проявлялось на канале ВЧ клистрона 3, исходное значение 18001. с каким-то хвостом, скопировать значение в браузер как водится нельзя.
Предварительный анализ:
round()
в процесс преобразования вещественных в целые, которое
было добавлено в cda и сервер 22-05-2019 -- эта версия была задеплоена на
пульт на днях, 23.09.2019.
Пара замечаний:
Но общей проблемы корректности это не решит, да и вообще метод мутноватый.
(Да, но раздельной записи/чтения A и B он не позволял.)
Разбирательство подетальнее, уже сегодня:
frolov_ie4_rw_p()
показало такой код:
c_Ai = round(dval / quant);
Отсюда радостная мысль -- «о, я тогда забыл воспроизвести это в драйвере Д16! вот сделать (не забыв "правильно" считать значение для B -- как V-A*quant), и настанет счастье!».
Но фиг --
-- т.е., там даже корректно высчитывалось B ("руками" брался "остаток от деления", путём вычитания ЧАСТНОГО*КВАНТ из начального значения).#if 0 // NO!!! May NOT round(), because remainder from division // is still used for B c_Ai = round(dval / quant); if (c_Ai > 65535) c_Ai = 65535; v_R = dval - c_Ai * quant; if (v_R < 0) v_R = 0; #else // ? c_Ai = remquo(dval, quant, &v_R) ? No, too underspecified to rely upon. c_Ai = dval / quant; if (c_Ai > 65535) c_Ai = 65535; v_R = fmod(dval, quant) /*+ 0.125*/; #endif c_Bi = v_R * 4; /* =/0.25 */ if (c_Bi > 255) c_Bi = 255;
Но, ЕМНИП, тот вариант не сказать, чтобы корректно работал, да?
02.10.2019: ОК, собираем статистику: при F_in_ns=45.7=45.7147=45.7146831969=0x1.6db7abd2ee5d3p+5 (F_in_HZ=21874809.8000000007=0x1.4dc879ccccccdp+24) последовательно "нажимаем Enter":
02.10.2019: [Enter] | 07.10.2019: | |||||||
---|---|---|---|---|---|---|---|---|
A,B | = | %.2f:V | %.30f:V | %a:V | > | old | new | |
393,144 | 18001.87 | 18001.8704963917 | 0x1.19477b6367f68p+14 | 393,143 | 393,144 | |||
393,143 | 18001.62 | 18001.6204963917 | 0x1.19467b6367f68p+14 | 393,142 | 393,143 | |||
393,142 | 18001.37 | 18001.3704963917 | 0x1.19457b6367f68p+14 | 393,141 | 393,142 | |||
393,141 | 18001.12 | 18001.1204963917 | 0x1.19447b6367f68p+14 | 393,140 | 393,141 | |||
393,140 | 18000.87 | 18000.8704963917 | 0x1.19437b6367f68p+14 | 393,139 | 393,140 | |||
393,139 | 18000.62 | 18000.6204963917 | 0x1.19427b6367f68p+14 | 393,138 | 393,139 | |||
393,138 | 18000.37 | 18000.3704963917 | 0x1.19417b6367f68p+14 | 393,137 | 393,138 | |||
393,137 | 18000.12 | 18000.1204963917 | 0x1.19407b6367f68p+14 | 393,136 | 393,137 | |||
393,136 | 17999.87 | 17999.8704963917 | 0x1.193f7b6367f68p+14 | 393,135 | 393,136 | |||
... | ||||||||
393,3 | 17966.62 | 17966.6204963917 | 0x1.18ba7b6367f68p+14 | 393,2 | 393,3 | |||
393,2 | 17966.37 | 17966.3704963917 | 0x1.18b97b6367f68p+14 | 393,1 | 393,2 | |||
393,1 | 17966.12 | 17966.1204963917 | 0x1.18b87b6367f68p+14 | 393,0 | 393,1 | |||
393,0 | 17965.87 | 17965.8704963917 | 0x1.18b77b6367f68p+14 | 392,182 | 392,183 | |||
392,182 | 17965.66 | 17965.6558131948 | 0x1.18b69f8d7e7f6p+14 | 392,182 | 392,182 |
ВСЁ!!! Дальше не снижается.
Т.е., ". . ." -- и так далее, по -0.25. Но:
Откуда возникла мысль: а может, и у Гусева тоже было совсем не идеально, просто конкретно на проблемные числа (18001.*) никогда не наступали?
Увы, попытка добиться от ЕманоФеди ответа "а какие числа бывали раньше?" особого успеха не дала -- он что-то заяснял про разницу в 3-м знаке (из 5 -- это 18Xnn, что ли?), но тут как раз произошла просадка и он убежал исправлять.
03.10.2019: начал анализировать значения, просто вручную имитируя расчёты, выполняемые драйвером.
Конкретно для первого же -- 18001.87:
Ёлки-палки!!! В голову приходит только пугающе знакомое «а
как оно с этими "бредовычислениями" вообще могло работать?!».
04.10.2019: ЧОРД!!! А зачем ROUND()-то делать?! Ведь он в той ветке, которая за-#if0'ена -- и с ней да, значения начинали ползти вверх (теперь стало понятно, почему). Как бы то ни было, ночная идея про "домножать на 4" всё равно валидна.
03.10.2019@~19:00, выходя из ИЯФа домой, крыльцо и парковка "между клумб": проблема в том, что округлять-то НАДО, но надо округлять не к целым, а "гранулярно", к ДРОБНЫМ числам. А такого функционала стандартная библиотека (libm) вроде не предоставляет.
04.10.2019@ночью, в районе 03 часов утра: (снилась всякая хрень, в т.ч. про Д16, а проснулся (или перед просыпанием?) -- и озарило:
А потом -- делить обратно.
int
, хоть
"к ближайшему (кратному 0.25)" -- round()
.
04.10.2019: вот написал всё то про "домножать на 4", и даже результаты позавчерашнего тестирования из буллет-листа в табличку переделал (чтобы добавить ещё колонок), а потом -- затык: ЧТО домножать на 4?
round()
.
Но там УЖЕ стоит "c_Bi = v_R * 4
".
А в вычислении "v_R = fmod(dval, quant)
" -- далее
закомментированное "/*+ 0.125*/
.
v_R = dval - c_Ai * quant
" -- почему этого не делается?
Однако -- а выше 06-02-2019 такой вопрос уже возникал.
x10sae:~% Q=45.7146831969; V=18001.87; echo $[V/Q] $[(V-Q*393)*4] 393.78748229454516 143.99801447319624 x10sae:~% Q=45.7146831969; V=17965.87; echo $[V/Q] $[(V-Q*393)*4] 392.99998914174472 -0.0019855268037645146 x10sae:~% Q=45.7146831969; V=17965.66; echo $[V/Q] $[(V-Q*392)*4] 392.99539543168675 182.01674726080091
Т.е., у нас тут ДВЕ разных проблемы:
И тут действительно замена fmod()'а на вычитание с последующим round() результата -- может дать нужный эффект.
Причина -- в тех самых недостающих дальних знаках. Как бороться -- неясно...
07.10.2019@~09:55, по пути на понедельничную планёрку, в переходе: надо бы посмотреть, какая "дельта" в проблемных случаях из-за отбрасывания дальних знаков; если она окажется существенно меньше "кванта B" -- 250ns, то, вероятно, должен бы существовать способ побороть проблему -- какое-нибудь "хитрое округление".
(И всё-таки, а может и не такая уж бредовая была ночная идея? Не поможет ли она справитьс с этим результатом "отбрасывания малого"?).
07.10.2019: делаем программульку work/tests/test_d16_calcs.c, имитирующую вычисления в драйвере, и на ней обкатываем разные варианты и идеи, плюс смотрим промежуточные данные вычислений.
c_Bi
не как
а как "остаток от вычитания реально полученного c_Ai" --v_R = fmod(dval, quant); c_Bi = n1_v_R * 4;
(т.е., округляется уже после "v_R = dval - c_Ai * quant; c_Bi = round(v_R * 4);
*4
" -- что и есть по факту
"округление до БЛИЖАЙШЕГО кратного 0.250).
Правда, теперь уже не в (392,182)=17965.66, а в (392,183)=17965.91 =17965.9058131948 =0x1.18b79f8d7e7f6p+14.
08.10.2019: продолжаем разбираться:
c_Bi=...
, и при первом варианте было бы то же самое.
???...откуда же различия-то берутся???
Т.е.,
Как решать оставшуюся проблему? И имеет ли она какое-то решение в принципе?
Как смотреть:
10.10.2019: х-м-м-м...
Ну печатает она числа; очевидным образом (это было ясно ещё позавчера на пляже) у варианта для %.1 максимальное отклонение 0.099(9) (т.е., уже чувствительно меньше кванта 0.250нс), для %.2 -- 0.0099(9), для %.3 -- 0.00099(9). Или, если убрать (9) -- 0.1, 0.01, 0.001 соответственно.
11.10.2019: ну пока внедряем хотя бы первую часть решения:
v_R = fmod(dval, quant);
заменен на
v_R = dval - c_Ai * quant;
...хотя это скорее "для вящей корректности".
round()
-- вместо
c_Bi = v_R * 4;
теперь
c_Bi = round(v_R * 4);
На пульт оно скопировано, будем наблюдать.
14.10.2019: сервер рестартанут, вроде "проблема 1" исправилась -- вниз не едет.
20.10.2019@дома-утро-полудрёма перед пробуждением: а можно так -- проверять, что если вычисленное отличается от вычисленного "как если бы значение было на маленькую дельту больше", то считать, что у нас как раз тот самый случай и брать результат второго вычисления.
Т.е., если обычное
c_Ai = dval / quant
и второе
c_Ai2 = (dval + DELTA) / quant
не равны, то использовать именно c_Ai2
.
Только надо будет после этого корректно "считать" B -- точнее, брать его равным 0.
Вопрос: чревато ли такое решение чем-нибудь, не вылезут ли какие-нибудь колёса? И, в любом случае -- а КАКУЮ брать дельту? 0.25-0.01? Или просто 0.1?
20.10.2019@дома-вечер: колёс вылезти не должно -- потому, что:
Соответственно, в РЕАЛЬНЫХ случаях ... ВОТ ЧТО ТУТ НАДО НАПИСАТЬ, А? Вроде всё ясно.
23.10.2019: реализовываем.
(вычислениеc_A2 = (dval + 0.1) / quant; if (c_A2 > 65535) c_A2 = 65535; if (c_A2 > c_Ai) { c_Ai = c_A2; v_R = 0; }
c_Bi = round(v_R * 4)
ниже общее для всех).
Надо б будет потестировать на реальном железе.
Смысл в том, что там эти части A и B работают по-разному: начало отсчёта A ("цифровая часть задержки") привязывается к сигналу внешних часов, а B ("аналоговая линия задержки") используется для точного сдвига (кажется, как раз относительно этих самых часов) и, следовательно, эти 2 части должны работать независимо.
Ну что, делаем такое поведение для lam_mode==LAM_MODE_D16P?
06.11.2018: поговорил с Фроловым и Емановым -- похоже, такое поведение с "раздельным управлением A и B" требуется не только для 20-й, а и для ВСЕХ Д16 "верхнего уровня" (к которым дальше кто-то цепляется каскадом).
Так что нужно вводить дополнительный параметр -- что-нибудь типа "ab_mode", со значениями вроде "AB" (default) и "A_ONLY".
А пересчёт для режима "A_ONLY" выполнять так же, как в ИЕ4 -- с
round()
.
06.11.2018: делаем.
ab_mode
.
AB_MODE_AB
=0 и
AB_MODE_A_ONLY
=1.
_rw_p()
теперь по 2 ветки на чтение и запись
_CHAN_V_n_base
. И да, в варианте A_ONLY используется
round()
.
18.01.2019: по крайней мере на 20-й Д16П фича "a_only" работает уже больше месяца и вроде без нареканий, так что "done".
ПО-ХОРОШЕМУ, эта задача должна бы выполняться на "верхнем уровне" -- в клиенте-менеджере_режимов, или хотя бы драйвером в сервере (таковой тоже предполагается, для жонглирования каналами других устройств).
Но конкретно Д16:
20.12.2018: кстати, поскольку на каждый из них будет делаться Return(), то, чтобы сэкономить производительность, можно перед группу "записей в свои каналы" окружить скобками fdio_lock_send()/fdio_unlock_send().
20.12.2018: (вечером) ага, щас! ЧЕМУ делать lock/unlock? Ведь дескриптор доступен на уровне remcxsd_driver, а эти мозги -- в самом драйвере.
18.12.2018@дома: сделан минимум: расширена карта каналов.
21.12.2018: также добавлены DEVTYPE-DOT-HACK-варианты с именами через точку, типа p0.b1 -- чтоб можно было адресоваться "к группам, нацеливая на них макросы" (или хоть обычный скрин frolov_d16.subsys).
20.12.2018: теперь собственно реализация.
presets_buf[]
.
WriteOne()
для A, B, OFF.
Проверять?
29.12.2018: да, проверено -- работает.
Но Федя затребовал добавить в состав пресетов ещё один канал: общую маску, alloff.
Сделано: ради этого размер пресета увеличен с 12 до 20 каналов, из которых 12-й стал pN_aloff, а еще 7 не используются. И остаётся ещё 20 каналов после пресетов. При желании можно будет использовать их как "маску работы пресетов": если в соответствующем канале 1, то этот канал при активации пресетов не трогаем.
Теперь надо ещё это изменение проверять.
...и, кстати, очевидная мысль: не потребуется ли канал "скопировать
текущие значения из железки в пресет" (оно же "считать текущие значения
пресетом номер N")? Единственное что неприятно -- тут, в отличие от
активации пресета, уже нельзя будет свалить работу на вызовы
_rw_p()
, а придётся дублировать код чтения из соответствующих
веток; впрочем, там буквально по 1 строчке на каждый канал -- ведь это (A.
B, маски) просто коды из железа, без какой-либо интерпретации.
29.12.2018: Федя утверждает (мыло "Re: Пресетоселектор" за Sat, 29 Dec 2018 17:52:05 +0700), что такое НЕ требуется, т.к.
С одной стороны мне это и самому не оч сложно сделать, а низкоуровневые операции тут нет смысла применять. А с другой - в основном паттерне работы действительно нет такого действия как скопировать пресет в зад. У меня данные для этих каналов есть в БД и можно конечно через оборудование копировать, а можно и сразу. есть потребность при редактировании какого-то (акивного) пресета сразу копировать канал в оборудование и наоборот, но ты сказал что тебе это сложно, и договорились, что я это если что на верхнем уровне организую.
Так что -- нафиг.
18.01.2019: фича уже запущена в обычную эксплуатацию и вроде бы работает всё как надо, так что считаем за "done".
30.01.2025@вечер, лыжи, 5-ка, участок между своротом на 4-ку и своротом на 10-ку: а почему тогда не была сделана возможность в пресетах менять не только A или B, но и V?
Вот сейчас такая фича ЕманоФеде занадобилась, так что надо бы добавить в пресеты каналы V -- чтоб можно было прямо в них писАть, а драйвер по своему алгоритму пересчитывал бы в (A,B); и наоборот -- при записи в пресетные A или B он бы и V обновлял и отдавал.
31.01.2025@утро: "обновлять" ничего не нужно, т.к. каналы V -- синтетические, в железе их не существует, запись в них сводится к модификации пары (A,B) оптом, а чтение тоже делается просто комбинированием содержимого (A,B).
Делаем:
double
, то их никак нельзя
поместить в карту каналов рядом с остальными, которые int32
(хотя там место есть -- по 20 каналов на пресет, из которых заняты 13 (по
4шт A,B,off плюс 1шт alloff)).
case
" в коде frolov_d16_rw_p()
тоже -- вычисление p_n
и p_c
.
presets_buf[]
.
03.02.2025: в скрин frolov_d16.subsys добавлена вся толпа каналов пресетов -- как минимум, чтоб удобно было проверять.
08.02.2025: решено также и сюда сделать фичу "вычитать текущие значения из устройства в указанный пресет".
Несмотря на то, что 29-12-2018 ЕманоФедя утверждал, что это не требуется, сейчас он точку зрения поменял и считает такую возможность нелишьней.
Сделано -- всё довольно несложно: простым вычитыванием из железа,
складированием и вызовами ReturnOne()
для всех затронутых
каналов.
...и да, как и с обычными каналами, канал "alloff" считается как бы "битом 5-й строки" и возвращается в общем цикле с прочими битами запрета.
10.02.2025: проверяем всё сделанное недавно, на живом железе на ВЭПП-5.
Причина оказалась в дурацкой ошибке: при вычислении номера канала
"пресетный V", передаваемого ReturnOne()
'у, номер пресета
умножался на FROLOV_D16_CHAN_PRESETS_per_one
(как у остальных
пресетных каналов) вместо FROLOV_D16_NUMOUTPUTS
(специфичного
для V, т.к. они идут отдельной группой).
Поправлено.
Хотя мне почему-то помнилось, что вроде кабы даже не делал его, где-то то ли для ЭЛС/Семёнова, то ли для ndbp... Или там использовался драйвер от Д16? Да не должен был бы -- вроде устройства несколько различны по функционированию.
27.07.2018: да, устройство ЧУТЬ-ЧУТЬ похоже на Д16 (по NAF'ам и "адресам" регистров), но отличается настолько, что использовать драйвер одного с другим точно не удастся.
30.07.2018: кстати, НИКАКИХ следов от когда-либо ранее якобы существовавшего драйвера IE4 обнаружить не удалось, в т.ч. в ARCHIVE/.
28.07.2018@дома: за основу берём всё от Д16 -- и _drv.c, и карту каналов.
Но, поскольку тут предвидится некоторое количество каналов статуса (текущий цикл, исполняется ли сейчас цикл, ...), то вместо "w30i" делаем "w50i,r50i".
30.07.2018: сделано всё, кроме "импульсности" -- работы с LAM и старта/стопа цикла BUM.
31.07.2018: делаем недопиленное вчера.
LAM_CB()
просто вызывается возврат значения
канала BUM_GOING, и более ничего -- так я понял рекомендацию Фролова.
Сам подсчёт пока не сделан, возвращаются всегда -1.
Теперь надо делать скрин и проверять работу на живом железе.
01.08.2018: начинаем тестирование.
LAM_CB()
-- чтобы именно отдельно от BUM_GOING
сигнализировать о LAM'е. Отдаётся всегда 0.
07.08.2018: какой, нафиг, TRUSTED?! При этом
свежеприконнектившийся клиент сразу получит NEWVAL, а это в корне неверно --
уведомления должны приходить только при РЕАЛЬНЫХ обновлениях! Заменено на
IS_AUTOUPDATED_YES
.
02.08.2018: что сделано по результатам вчерашнего:
LAM_CB()
вставлен сброс LAM'а (A0F10).
Т.е., в IE4 никакого "программного старта" нет и быть не может (только bum_start, который, собственно, и играет ту же роль в "однократном" режиме).
22.08.2018: ну надо -- так делаем.
F_IN_NS
стало float64
.
frolov_ie4_params[]
.
_init_d()
соответствующая конверсия (в зависимости от
знака) плюс лимитирование.
_rw_p()
.
dval
, vp
,
dtype_f64
, n_1
.
Проверено -- работает (значения совпадают с гусиными (на живом железе запускалось по очереди то одно, то другое)).
11.10.2018: протокол попытки разобраться:
У меня есть гипотеза: возможно, это один из тех косяков Фролова/ИЕ4. Если у него почему-либо залип флажок LAM'а (в Альтере), то он новый LAM и не сгенерит. А при рестарте драйвера делается сброс LAM (путём A(0)F(10), как и в обработчике) -- вот залипшесть могла и отлипнуть.
Резюме:
12.10.2018: записанное вчера -- скопировано из письма ЕманоФеде. Он ответил:
видишь ли, глюки были, но Гусев мне поставлял сигнал по тому жепринципу и за все прошлое время срывов цикла из-за чего-то подобного не было ни разу.
На что мне оставалось ответить только:
12.10.2018: Продолжение марлезонского балета.
09.02.2023: поскольку на днях опять возникала та же проблема (LAM не приходит), а недавно как раз создавалась диагностика для временно не генерившего LAM блока TIME24K4, то и сюда сготовлена пара каналов чисто для диагностики:
С именем пытался умничать, в попытке найти официальное название в официальной документации по CAMAC -- была ж где-то табличка. Но всё, что нашёл -- в документе "An Introduction to CAMAC", ныне отсутствующем (host not found) и доступном на web.archive.org, на стр.6 оно в табличке значится как "Test Look-at-Me"; так себе название, потому и взято то, которое взято.
LAM_CB()
; этакая
эмуляция LAM, чтоб сделался сброс по F10A0 и возвраты всего, чего надо.
10.02.2023: сегодня был бросок питания, так что мы с Федей воспользовались возможностью провести диагностику; заодно с ИК4 в поз.4 сдурел и Д16П в поз.20 (тоже перестал давать LAM'ы), так что и его протестировали.
Сценарий был такой:
._devstate=0
-- при этом в контроллере драйвер перезагружается полностью заново, и NAF'ы
инициализации, конечно, тоже делает.
Результаты тестирования, отправленные письмом Фролову и Еманову (Message-ID: 9c50b054-c8f6-303a-9a9a-49567f35ce78@starnew.inp.nsk.su):
- ИЕ4 по F8A0 отдаёт Q=0 -- т.е., блок не просто не генерит LAM, но и на своём "внутреннем" уровне не считает LAM зажжённым.
При этом бит 7 в статусном регистре -- "цикл <<BUM>>" -- взведён в 1, т.е., блок знает, что цикл завершился.
- Д16П по F8A0 также отдаёт Q=0 -- поведение совпадает с ИЕ4.
- ИЕ4 после рестарта драйвера (БЕЗ дёрганья питания крейту), т.е., просто после F10A0 -- очухался и начал работать, генеря LAM после окончания цикла BUM.
- Д16П же после аналогичной операции -- нет, LAM генерить не начал.
После дёрганья питания крейта всё пришло в норму.
10.02.2023: а ещё Федя недоволен, что LAM_SIG приходит не просто после BUM_GOING, но и спустя изрядное время -- в районе 10мс.
Очевидно, это из-за тормозов CM5307/PPC, с его дорогими системными вызовами.
И решение проблемы тоже очевидно -- надо отдавать единым
ReturnDataSet()
'ом вместо 2 отдельных операций.
Сделано, теперь надо будет проверить.
24.10.2018: да, работает.
29.10.2018: добавлена также несделанная тогда возможность инициализации Kclk. Для D16 она не требуется (там по умолчанию и так код 0b00, соответствующий "/1"), а вот у IE4 при включении в регистре прописываются все единички, что приводит к коду 0b11, соответствующему "/8".
05.11.2018: ага, зато в Д16 ТРЕБУЕТСЯ Kstart -- который при включении ==0, что вообще блокирует запуски.
06.11.2018: хотя и Kclk у Д16 тоже пусть будет.
01.11.2018: конкретно, на примере канала canhw:19.ic.syn.linBeam.dly.set, он же canhw:19.syn_ie4.v2 с R=1000000.0:
В качестве решения: вместо отбрасывания дробной части при делении наносекунд на квант --
c_Ai = dval / quant;
сделано предварительное округление:
c_Ai = round(dval / quant);
После чего проблема на Инжекционном комплексе вроде исчезла.
Очевидно, надо ввести несколько ro-каналов, возвращаемых по команде в rw-канал.
24.09.2019: делаем.
Вводим канал-команду FROLOV_IE4_CHAN_DO_DIAG
=39 (это конец
int32-rw-каналов) плюс группу FROLOV_IE4_CHAN_DIAG_n_base
=70 в
количестве _n_count=6 штук, с именами FROLOV_IE4_CHAN_DIAG_R*
.
_init_d()
декларируем ргуппу DIAG_R*-каналов
как IS_AUTOUPDATED_TRUSTED
.
_rw_p()
по DO_DIAG
производим вычитывание
и возврат. Список -- A,F,chan -- определяется картой
diag_map[]
: цикл просто идёт по ней.
Пока что только числами; наборами чекбоксов покамест не получилось --
какие-то сложности с fordown()
, там образуется бесконечный
цикл, вероятно, из-за каких-то конфликтов с использованием
$1
/$2
.
Надо проверять.
14.10.2019: сервер (canhw:19) рестартован, на вид работает.
Правда, скрин был халтурноват: отсутствовала кнопка [Read!] (r:do_diag) и у строки RKm стояло r:diag_RKn (из-за чего она светилась чёрным). Но это быстро поправлено.
30.07.2018: а именно:
_rw_p()
.
Оно пока не проверено, но, вследствие тривиальности, считаем за "done".
Вчера Фролов прислал описание (чуток недостаточное) "Измеритель временных интервалов Time24k4.docx", сохранённое как 20230122-frolov-Izmeritel-vremennyh-intervalov-Time24k4.docx, а сегодня после личного обсуждения с ним стало более понятно, что к чему.
Этот новый подраздельчик впихивается сюда, а не в конец списка (после adc200_drv) для того, чтобы все frolov_*_drv были рядышком.
23.01.2023: приступаем. Сначала -- информация и соображения.
Собственно, что было понято в разговоре с Фроловым:
Эти 4 времени отдаются 24-битными числами.
stat[time&2047]++
до тех пор, пока какое-либо значение не достигнет 255.
Т.е., это "распределение" времён в предположении, что все измерения ложатся примерно в одну область (потому берётся 11 младших бит значения и используется в качестве адреса).
И да -- эта статистика реально нужна юзерам, а не только автору блока для диагностики.
Наверняка в процессе реализации драйвера возникнут ещё вопросы/непонятки.
Как будем делать ("ключевые" решения):
24.01.2023: ...возможно, понадобится также канал "прерви и отдай то, что есть сейчас".
24.01.2023: а не понадобятся ли нам плагины вроде twoi32l и fouri32l?
24.01.2023: продолжаем.
ReadAndReturnTimes()
-- возвращает 4 времени, сразу
одной пачкой; код возврата скопирован с недавноизготовленной
gin25_drv.c::ReturnErrorBits()
.
ReadAndReturnStats()
-- вычитывает и возвращает 4
"осциллограммы" (реально это гистограммы) статистики. Читает посредством
DO_NAFB()
блоками по 128; это как бы "метод обратного flush",
как во всяких adc200/adc333/adc4/adc502, но реально тут всё кратно (128
вместо 100 выбрано как раз потому, что 2048 ему кратно).
25.01.2023: читал документацию, пытаясь понять, как лучше всё организовать. Так и не понял.
Замечание прямым текстом насчёт маски
ReadAndReturn{Times,Stats}()
сделаны с таким
расчётом, чтобы можно было производить возврат УСЛОВНО, при
не-замаскированности соответствующего канала
26.01.2023: роем далее.
Выдвигаем предположение, что НЕЛЬЗЯ -- т.е., надо производить запись в момент, когда измерения заведомо остановлены.
Поскольку полноценного понимания нет, то делаем в соответствии с имеющимся, так, как вроде бы достаточно универсально и должно прокатить в любом варианте:
cur_mask
и
rqd_mask
, плюс mask_rflags
для складирования
статуса последней операции чтения/записи.
SetMask()
, могущей
вызываться из того места, где нужно выполнить "прописывание".
_init_d()
производит начальное чтение.
_rw_p()
модифицирует битики в rqd_mask
в
соответствии с указаниями свыше, но SetMask()
НЕ вызывает.
ЗЫ: вот тот "условный возврат" будет проверять как раз битики прямо в
cur_mask
, не делая чтения из блока.
_rw_p()
он очевидным образом отображается на бит
STATUS_EXT_FRQ
(номер 0).
A_*
" (как во многих других
frolov_*_drv.c), а также масок битиков для статусного регистра
"STATUS_*
" и для регистра состояния "STATE_*
".
_init_d()
они помечаются как
IS_AUTOUPDATED_TRUSTED
и отдаются значения 2048,0,255
соответственно.
И ещё наконец-то сделана компилируемость и "frolov_time24k4" добавлено в ListOfCamacDrivers.mk (ранее просто пилился текст исходника без каких-либо проверок).
Что дальше -- как-то не совсем ясно.
27.01.2023: пилим потихоньку...
ClearStats()
-- с ней хотя бы всё
понятно: пара NAF'ов, пауза >1.31ms SleepBySelect()
'ом, ещё
пара NAF'ов.
LAM_CB()
-- пока очень простой и прямолинейный:
вызывает ReadAndReturnTimes()
, затем при несовпадении
rqd_mask
с cur_mask
делает SetMask()
,
а дальше будет делать старт измерений.
29.01.2023: да, в конце добавлен вызов
StartSingleMeasurement()
.
_init_d()
делается заказ.
Пока всё -- БЕЗ попыток как-то различать причину LAM'а и работать со статистикой.
29.01.2023: допиливаем первый вариант -- осталось уже специфическое программирование, для правильной реализации которого пришлось пере-изучать "Приложение 2" от Фролова.
_init_d()
:
...но НЕ РАЗблокирует, поскольку эта задача возлагается на старт измерений.
StartSingleMeasurement()
-- там сейчас пара
NAF'ов, по рецепту "Пример 3" "Однократные измерения" из описания.
Есть некоторые сомнения насчёт корректности работы с регистром маски: проблема в том, что собственно маска и бит блокирования стартов расположены в ОДНОМ регистре, который в результате пишется из нескольких разных мест, и насколько эти записи скоординированы -- у меня нет полного понимания.
Битым текстом оставшиеся непонятки по документации:
F25A1: "сброс триггера лам L в ноль. Сброс выходных регистров в ноль. Команда для однократных измерений"
F25A2: "подготовка. Обнуление (сброс регистра состояний) - бит 2 (лам L), бит 3 (тригг. max), бит 4 (max адрес)."
01.02.2023: пытаемся проверять, результаты озадачивающие.
02.02.2023: да нет, вроде после дёрганья Фроловым питания крейта в регистре оказался 0. Может, мой драйвер тогда что-то напортачил?
status2rflags()
было везде заменено на x2rflags()
.
_rw_p()
, продолжают возвращаться по
запросу (в ветке "UNSUPPORTED" для отладки было сделано
value=chn
) -- загадка...
(Феномен этот прямо бросался в глаза тем фактом, что TIME-каналы подсвечиваются бордовым с флагом UNSUPPORTED.)
02.02.2023: пытаемся разобраться.
chn
(номер канала), и в кусочек-реакцию на один из каналов
(конкретно RANGEMAX), смотрел-смотрел -- и-и-и...
Печать показала, что реакция откликается на чтение канала 46 (который вообще из WR-группы) вместо вроде бы надлежащего 96.
FROLOV_TIME24K4_CHAN_WR_base = 0
также значилось
FROLOV_TIME24K4_CHAN_RD_base = 0
вместо надлежащего
FROLOV_TIME24K4_CHAN_RD_base = 50
(А в .devtype-файле при этом всё было верно)
Вот оно и путалось в номерах.
50
эта проблема ушла.
Но оказалось, что НЕТ обновлений каналов TIMES -- т.е., похоже, LAMы не приходят. Будем разбираться...
Он пообещал проверить ("ручным контроллером"), генерится ли LAM на шину.
07.02.2023: после обеда был сеанс общения с Фроловым, во время которого я ему вживую продемонстрировал поведение, выведя на экран панельки Д16 и T24. В результате он задумался и обещал ещё разок посмотреть и перепроверить (плюс сказал, что какой-то косяк в прошивке нашёл; но работать от этого не начало).
Вечером я, по давно бродившим в голове мыслям, добавил пару неименованных каналов:
LAM_CB()
.
И в скрин тоже добавил -- и LED:99, и кнопку:49. И рестартовал драйвер, ...
...только для того, чтобы увидеть, что данные и так постоянно "обновляются" -- они всегда свежие, хотя сами значения не меняются.
Лог показал, что были перезагрузки крейта в 17:06 и 17:27 -- видимо, Фролов что-то делал; и в Д16 стоят какие-то осмысленные числа. Возможно, он реально косяк исправил, а измеряемые значения не меняются из-за постоянства в Д16.
10.02.2023: поговорил с Фроловым -- говорит, что да, там был какой-то давний косяк на тему "линия L блокировалась линией N" (я не очень понял смысл), и вот этот косяк исправлен, и LAM заработал. А то, что числа измерений стоят как вкопанные -- потому, что у Д16 и TIME24 общая синхронизация, вот съезжать просто и нечему.
10.02.2023: для полноты картины проводим тестирование
каналов 49 и 99, посредством закомментировывания
WATCH_FOR_LAM()
. Результат -- всё работает:
14.02.2023: был ещё сеанс переговоров с Виталей Балакиным и Федей Емановым, резюме -- скорее всего, возиться с фичей "статистика" не понадобится, т.к. у нас на машине недостижимы частоты выше ~10Hz, а на таких и программно всё делается (причём гибче и полнее).
Так что заводим свой раздел и для Г0603.
Конкретно для клистронов -- плавно нужно уметь только поднимать, но всё же лучше сразу сделать "всё".
03.09.2018: казалось бы, уже есть "инфраструктура advdac" -- использовать бы её. Авотфиг: она сейчас слишком привязана к kozdev -- там сейчас прямо Return'ы со ссылкой на KOZDEV_CHAN_...
Похоже, проще будет воспроизвести "вручную", следующим образом:
Делаем.
04.09.2018: переведена на "w24i,r8i" -- добавлены каналы IMM, advdac_slowmo'й поддерживаемые (а раз так, то почему бы и нет?).
03.09.2018@дома: да нафига
так извращаться! Взять и адаптировать advdac_slowmo_kinetic_meat.h
прямо СЕЙЧАС. Там ведь сложностей-то -- всего лишь ввести имена вместо
KOZDEV_nnn
, чтоб драйверы-юзеры их определяли.
04.09.2018: реализовываем адаптацию.
Поэтому старая версия отфоркована в g0603_simple_drv.c. К нему
сделан g0603_simple.devtype, а вот
drv_i/g0603_simple_drv_i.h -- нет, пусть пользуется общим (проверки
номеров каналов в _rw_p()
есть).
val_to_daccode_to_val()
.
num_isc
и out[]
.
typedef g0603_privrec_t privrec_t
-- т.к. в
CAMAC- и CAN-драйверах используются разные принципы именования (в CAMAC -- с
префиксом имени драйвера, в CAN -- без).
В CAMAC-драйверах это рудимент от времён района 2000г, когда была идея (и возможность) объединять #include'ом код нескольких драйверов в один "над-драйвер" -- позже эту работу взял на себя vdev.
SendWrRq()
и SendRdRq()
.
Последний сводится к просто вызову HandleSlowmoREADDAC_in()
.
_rw()
остался только разбор по номеру канала и
вызовы соответствующих HandleSlowmoOUT*_rw().
Вследствие чего его структура изменена с CAMAC-style (работа с каждым
каналом заполняет значения value
и rflags
, а в
конце стоит общий Return) на CAN-style (последним -- умолчательным --
элементом селектора стоит возврат CXRF_UNSUPPORTED).
_hbt()
.
18.09.2018: после первоначального ужаса ("клистрон
зашкаливает! а почему?!") и мысли, что в гусином коде есть какой-то
неочевидный пересчёт, попробовал-таки почитать ПКС, а увидев странные числа
(будто со смещением на пол-диапазона) догадался заглянуть в собственный код
-- и вуаля: там при записи к писуемому коду безусловным образом добавлялось
0x8000
.
Исправление тривиальное и очевидное:
plrty_shift
.
SendWrRq()
прибавляется его значение.
Странно, что его не было раньше: драйвер-то есть давно.
Хотя, возможно, именно в том и причина -- создан был ещё в v2hw, а в
hw4cx просто портирован с минимальной модификацией _rw_p()
.
Первую-то проблему решить несложно -- добавить alias-имён.
А со второй муторнее -- придётся ОЧЕНЬ сильно переделывать драйвер, добавляя в него использование advdac. Подсмотрим в g0603_drv.c. ...ну и карту каналов для неё расширить придётся, конечно.
11.11.2019: поехали.
SendWrRq()
и SendRdRq()
, и сами они не рядом с
#include "advdac_slowmo_kinetic_meat.h"
, а перед
_rw_p()
.
(В "образце" же оба стояли ПЕРЕД #include
, а чтоб
SendRdRq()
мог вызывать HandleSlowmoREADDAC_in()
,
последний объявлен через forward-deslaration. Что криво, т.к. нефиг
объявлять функцию, определяемую в другом модуле.)
Ну теперь проверять надо.
12.11.2019: сделан скрин -- v0308.subsys, чтоб нормально можно было тестировать.
14.11.2019: проверил. Эх! :D
-1
, хотя
инициализируются они значением 500.
HandleSlowmoOUT_RATE_rw()
показало, что ТАМ всё в порядке -- 500.
_rw_p()
построен по другой модели:
if()/else if()/...else, в то время как тут -- switch() с обязательным
ReturnInt32Datum(devid, chn, value, rflags)
в конце.
Вот этот-то Return и возвращал -1 -- она попадает в value
в
блоке получения значения (который в самом начале цикла по каналам, ПЕРЕД
селектором каналов), в виде
value = 0xFFFFFFFF; // That's just to make GCC happy
А rflags=0 тоже делается по умолчанию.
break
теперь делается goto
NEXT_CHANNEL
.
Решение не самое красивое, но рабочее и наиболее простое.
После исправления -- вроде всё вообще как надо. Теперь пусть ЕманоФедя допроверяет.
Откуда напрашивается такое решение: добавить драйверу auxinfo-параметр "b0307", булевский, при выставленности которого:
case()
, а делать
value = 0; rflags = CXRF_UNSUPPORTED;
CXRF_CAMAC_NO_Q
-- например, просто
rflags &=~ CXRF_CAMAC_NO_Q;
прямо перед возвратом.
08.09.2021: сделано, ровно по тому проекту.
10.09.2021: проверяем. Оказалось, что не у всех
каналов флаг CXRF_CAMAC_NO_Q
срезался -- не хватало в
SendRdRq()
. Добавлено.
Засим "done".
Тут вспомнилось, что 13-07-2017 были какие-то вопросы к работе то ли пересчёта коды->микровольты, то ли с вычислением диапазонов (что выражалось в странном отображении осциллограмм); как минимум вопросы были к учёту сдвига нуля.
18.04.2022: немного результатов разбирательства:
Нашлась только запись в bigfile-0001.html за 19-12-2007 с пожеланием этого.
19.04.2022: а, разобрался: то было не в adc200, а в ndbp_nadc200_elemplugin.c. В мэйнстримовую же поддержку ADC200 оно так и не прошло.
Причём со слвигом нуля отдельно поигрался на живом adc200_kkr1 -- тоже на вид всё работает верно: при изменении сдвига осциллограмма перемещается в осмысленном направлении (вверх при увеличении, вниз при вменьшении) и подписи на осях меняются согласованно.
19.04.2022@~19:00, идя мимо парковки у Ильича 1 и в290мо154: отдельный вопрос с отображением в клиентах: по-хорошему надо бы и на селекторах как-то отражать иной диапазон.
gui->k_params[RANGE{1,2}]
в зависимости от текущих значений J5,J6.
Но даже такой фокус не проканает -- по причине отсутствия у конкретных knobplugin'ов своих privrec'ов (с чем мы уже недавно столкнулись при работе над onei32l 11-04-2022).
PzframeGuiMkparknob()
регистрировал бы правильные cn'ы, а затем
копировать в неиспользуемые.
PZFRAME_CHAN_IMMEDIATE_MASK
(несмотря на
.name==NULL
) -- чтобы PzframeGuiUpdate()
не
пыталась бы их обновлять.
А правильнее -- чтоб PzframeGuiUpdate()
не
трогала бы ячейки с .name==NULL
(сейчас там эта проверка
отсутствует).
20.04.2022: делаем.
Значение "0" будет соответствовать набору диапазонов до 2V, а "1" -- до 8V.
J5
=28 и J6
=29 -- там
как раз была пара свободных, это в диапазоне после RANGEx и ZEROx.
Аналогичные "CUR_" не требуются -- т.к. это по факту не каналы измерения, а глобальный режим работы прибора.
chinfo[]
плюс сделана валидация (в форме
булеанизации
"v = (v != 0)
").
adc200_params[]
, под именами
"j5"" и "j6", в виде LOOKUP'ов, ссылающихся на табличку
adc200_j5_j6_lkp[]
с изрядным количеством вариантов: помимо
просто "0" и "1", можно указывать "2,3" и "2v" как синонимы "0" и "1,2" и
"8v" как синонимы "1".
По умолчанию ставится значение 0.
quants[]
расширен с 4 элементов до 8, с
добавлением 8000, 16000, 32000, 64000.
И далее
ReadMeasurements()
-- из кодов в вольты;
PrepareRetbufs()
-- расчёт диапазонов.
В обоих случаях используется формула "quants[(RANGE&3)+(J5*4)]".
Засим вроде всё; теперь надо проверять, но это уже по окончании разборок Витали Балакина с кикерными осциллографами в Зале 2.
22.04.2022: проверил в минимальном варианте (после
замены на пульту файла cm5307_adc200.drvlet драйверу сделано
._devstate=0
) -- "пересчёт" в диапазон +-8V работает: и на
реперах числа отображает, и на подписях к вертикальным осям. Щёлкал
туда-сюда -- отрабатывается адекватно.
Только один "косячок" вылез: при ручной смене диапазонов (записью в
J5,J6) подписи на вертикальных осях не меняются. Причина проста: флаг
change_important
взведён в 1 только у каналов
CUR_RANGE1/CUR_RANGE2, но они-то НЕ меняются, а меняются лишь
LINE{1,2}RANGE{MIN,MAX}, у которых флаг равен 0. Правильным решением было
бы уставить флаг у каналов J5,J6, но их пока вообще в карте нет. Впрочем,
ситуация настолько экзотичная, что можно вообще забить.
25.04.2022: проверил также и инициализацию через auxinfo (нагляднее всего оказалось "j5=8V j6=8V") -- тоже работает.
Занадобилось отразить странности с чтением/записью.
Попробуем исследовать вопрос.
03.02.2023: "пробуем".
cm5307_test 4,0,0 8,8,0
"
-- в обоих случаях X отдаётся.
И вот в этой цепочке от каждого NAF'а берутся флаги, которые OR'ом складываются. А для регистров частоты -- "FTW" -- используется по 6 байт, так что при чтении 12, а при записи 6 операций, чьи флаги складываются.
._devstate=0
-- поля записи побелели, хотя числа в них какие-то
бредовые.
Но первая же попытка записи любое поле приводит к его побордовению.
...и тут возникла гипотеза -- а не с питанием ли крейта проблемы?
log=all
",
Раскопки по архивам показали, что оное логгирование было включено ~26-01-2022 (в w20211231-serial-mxupcie.tar.gz его ещё нет и дата 09-09-2021).
(фиг знает, почему каждый по 2 раза), а вот при записи --2023-02-03 09:46:59.692 tower cxsd#47: dds300[9]d3_2/DATACONV: DEBUGP: read(4:09): value=67764706 FTW=0000606060606060 fq=1.563750e+06 2023-02-03 09:46:59.692 tower cxsd#47: dds300[9]d3_2/DATACONV: DEBUGP: read(5:0f): value=67764706 FTW=0000606060606060 fq=1.563750e+06 2023-02-03 09:46:59.704 tower cxsd#47: dds300[9]d3_2/DATACONV: DEBUGP: read(4:09): value=67764706 FTW=0000606060606060 fq=1.563750e+06 2023-02-03 09:46:59.708 tower cxsd#47: dds300[9]d3_2/DATACONV: DEBUGP: read(5:0f): value=67764706 FTW=0000606060606060 fq=1.563750e+06 2023-02-03 09:46:59.715 tower cxsd#47: dds300[8]d3_1/DATACONV: DEBUGP: read(4:09): value=67764706 FTW=0000606060606060 fq=1.563750e+06 2023-02-03 09:46:59.715 tower cxsd#47: dds300[8]d3_1/DATACONV: DEBUGP: read(5:0f): value=67764706 FTW=0000606060606060 fq=1.563750e+06 2023-02-03 09:46:59.715 tower cxsd#47: dds300[8]d3_1/DATACONV: DEBUGP: read(4:09): value=67764706 FTW=0000606060606060 fq=1.563750e+06 2023-02-03 09:46:59.715 tower cxsd#47: dds300[8]d3_1/DATACONV: DEBUGP: read(5:0f): value=67764706 FTW=0000606060606060 fq=1.563750e+06
-- т.е., как бы что-то пишется, но читаются всегда нули.2023-02-03 09:47:30.423 tower cxsd#47: dds300[9]d3_2/DATACONV: DEBUGP: write(4:09): value=606 FTW=00000000387bb926 2023-02-03 09:47:30.425 tower cxsd#47: dds300[9]d3_2/DATACONV: DEBUGP: read(4:09): value=0 FTW=0000000000000000 fq=1.563750e+06 2023-02-03 09:47:52.906 tower cxsd#47: dds300[9]d3_2/DATACONV: DEBUGP: write(5:0f): value=606 FTW=00000000387bb926 2023-02-03 09:47:52.908 tower cxsd#47: dds300[9]d3_2/DATACONV: DEBUGP: read(5:0f): value=0 FTW=0000000000000000 fq=1.563750e+06 2023-02-03 09:48:22.423 tower cxsd#47: dds300[8]d3_1/DATACONV: DEBUGP: write(5:0f): value=677647 FTW=000000f6b9473d7c 2023-02-03 09:48:22.425 tower cxsd#47: dds300[8]d3_1/DATACONV: DEBUGP: read(5:0f): value=0 FTW=0000000000000000 fq=1.563750e+06 2023-02-03 09:48:53.276 tower cxsd#47: dds300[8]d3_1/DATACONV: DEBUGP: write(4:09): value=67706 FTW=00000018a6a81664 2023-02-03 09:48:53.278 tower cxsd#47: dds300[8]d3_1/DATACONV: DEBUGP: read(4:09): value=0 FTW=0000000000000000 fq=1.563750e+06
04.02.2023: не успетое вчера:
(rflags&CXRF_CAMAC_NO_X)?'-':'X'
-- так что можно будет
лучше видеть, что происходит.
cm5307_test
и увидеть, на каком этапе какой результат.
07.02.2023: новый день, крейт включили, при старте всё прочиталось как обычно, и БЕЗ ошибок --
2023-02-07 11:34:51.075 tower cxsd#47: dds300[8]d3_1/DATACONV: DEBUGP: read(4:09): X value=67764706 FTW=0000606060606060 fq=1.563750e+06
А при попытке записи в логи попало следующее:
2023-02-07 11:41:59.249 tower cxsd#47: dds300[8]d3_1/DATACONV: DEBUGP: write(4:09): - value=67764706 FTW=0000606060632f03 2023-02-07 11:41:59.251 tower cxsd#47: dds300[8]d3_1/DATACONV: DEBUGP: read(4:09): - value=67764706 FTW=0000606060606060 fq=1.563750e+06
Т.е.,
И да, оно именно ТОЧНО ЧИТАЕТСЯ, а не "мусор" -- т.к. в
dds300_rd_int64()
делается зануление всех 8 байт перед вызовом
dds300_rd_data()
.
0000606060632f03
вместо прочитанного
0000606060606060
-- видимо, огрехи вычислений с плавающей
точкой; например, следствие присутствующего в обоих пересчётах
"+ 0.5
":
int32 value; double fquant; int64 FTW; . . . FTW = value * fquant + 0.5; . . . value = FTW / fquant + 0.5
Надо всё-таки пробовать исполнять NAF'ы вручную и смотреть статусы по-NAF'но.
02.03.2023: пробуем.
...# cm5307_test 4,1,16=0x0900 4,0,0 4,1,16=0x0800 4,0,0 4,1,16=0x0700 4,0,0 4,1,16=0x0600 4,0,0 4,1,16=0x0500 4,0,0 4,1,16=0x0400 4,0,0 NAF(4,1,16,2304): status:X-, dr=2304/0x900/04400 NAF(4,0,0,0): status:X-, dr=8288/0x2060/020140 NAF(4,1,16,2048): status:X-, dr=2048/0x800/04000 NAF(4,0,0,0): status:X-, dr=8288/0x2060/020140 NAF(4,1,16,1792): status:X-, dr=1792/0x700/03400 NAF(4,0,0,0): status:X-, dr=8288/0x2060/020140 NAF(4,1,16,1536): status:X-, dr=1536/0x600/03000 NAF(4,0,0,0): status:X-, dr=8288/0x2060/020140 NAF(4,1,16,1280): status:X-, dr=1280/0x500/02400 NAF(4,0,0,0): status:X-, dr=8288/0x2060/020140 NAF(4,1,16,1024): status:X-, dr=1024/0x400/02000 NAF(4,0,0,0): status:X-, dr=8288/0x2060/020140
...# cm5307_test 4,0,16=0x0900 4,0,16=0x0800 4,0,16=0x0700 4,0,16=0x0600 4,0,16=0x0500 4,0,16=0x0400 NAF(4,0,16,2304): status:X-, dr=2304/0x900/04400 NAF(4,0,16,2048): status:X-, dr=2048/0x800/04000 NAF(4,0,16,1792): status:X-, dr=1792/0x700/03400 NAF(4,0,16,1536): status:X-, dr=1536/0x600/03000 NAF(4,0,16,1280): status:X-, dr=1280/0x500/02400 NAF(4,0,16,1024): status:X-, dr=1024/0x400/02000
Т.е., вроде бы БЕЗ проблем с X.
...# cm5307_test 4,0,16=0x0901 4,0,16=0x0802 4,0,16=0x0703 4,0,16=0x0604 4,0,16=0x0505 4,0,16=0x0406 NAF(4,0,16,2305): status:X-, dr=2305/0x901/04401 NAF(4,0,16,2050): status:X-, dr=2050/0x802/04002 NAF(4,0,16,1795): status:X-, dr=1795/0x703/03403 NAF(4,0,16,1540): status:X-, dr=1540/0x604/03004 NAF(4,0,16,1285): status:X-, dr=1285/0x505/02405 NAF(4,0,16,1033): status:X-, dr=1033/0x406/02011
Прочиталось презабавное:
...# cm5307_test 4,1,16=0x0900 4,0,0 4,1,16=0x0800 4,0,0 4,1,16=0x0700 4,0,0 4,1,16=0x0600 4,0,0 4,1,16=0x0500 4,0,0 4,1,16=0x0400 4,0,0 NAF(4,1,16,2304): status:X-, dr=2304/0x900/04400 NAF(4,0,0,0): status:X-, dr=1030/0x406/02006 NAF(4,1,16,2048): status:X-, dr=2048/0x800/04000 NAF(4,0,0,0): status:X-, dr=1030/0x406/02006 NAF(4,1,16,1792): status:X-, dr=1792/0x700/03400 NAF(4,0,0,0): status:X-, dr=1030/0x406/02006 NAF(4,1,16,1536): status:X-, dr=1536/0x600/03000 NAF(4,0,0,0): status:X-, dr=1030/0x406/02006 NAF(4,1,16,1280): status:X-, dr=1280/0x500/02400 NAF(4,0,0,0): status:X-, dr=1030/0x406/02006 NAF(4,1,16,1024): status:X-, dr=1024/0x400/02000 NAF(4,0,0,0): status:X-, dr=1030/0x406/02006
Т.е., во всех байтах возвращается ПОСЛЕДНИЙ!
Попробовал заменить тот байт на 0x09 -- 0x09 же и прочиталось.
Ну вот что тут теперь думать, а? Напрашиваются такие выводы:
Возможно, причина в том, что драйвер молотит NAF'ы на максимальной скорости, а cm5307_test перемежает их парсингом, что вносит задержки.
Вставить логгинг каждого NAF'а прямо в код драйвера? Это, конечно, тоже внесёт задержки, но зато будем видеть точные данные...
Чуть не офигел, пытаясь разобраться, как же так -- все индивидуальные операции дают rflags=0, а в конечном результате печатается rflags=1...
DDS300_UPDATE()
-- а это
макрос:
#define DDS300_UPDATE() \ rflags |= x2rflags(DO_NAF(CAMAC_REF, me->N, 0, 26, &junk))
И вот когда в цепочку "запись" добавил 4,0,26 -- тогда оно и выдало "status:--".
Т.е., почему-то НЕ отрабатывается команда UPDATE.
А файл dds300_drv.c вплоть до января-2023 не менялся с 2015-07-14. Так что если он раньше работал, то он точно ни при чём.
ЗЫ: пробовал посмотреть на romeo (там до сих пор v2) где есть 1 штука DDS300 -- фиг, даже имени "romeo" теперь не существует. Видимо, как Медведев и Сизов год-два назад напортачили с сетевушками, так и с концом. Придётся сходить ногами.
06.03.2023: сходил -- а там просто отсутствует в крейте этот блок и на скрине у его каналов горит бордовый флаг "No 'X' from device". Так что проверить, увы, не на чем.
Ранее всё на эту тему обсуждалось в bigfile-0001.html (именно во время CXv2 пройдены основные грабли работы с COM-портами, а в v4 уже практически копировалось готовое, с минимальной переделкой под изменившийся API плюс под наличие настоящих layer'ов).
Но сейчас появилось уже своё, специфичное.
Вставляем его "по логике" -- после CAMAC, но ДО "cm5307", который всё равно никогда уже не наполнится (т.к. cm5307_drv.c с момента создания того раздела окончательно уволен в пользу remdrv.c).
23.11.2021: на dwarf'е (стенд Левичева-jr для какого-то тестирования, где используется КШД485 через USB/RS485-адаптер) обнаружилось, что при USB-disconnect'е дескриптор становится "готов на чтение", но, КАК МНЕ ПОКАЗАЛОСЬ, само чтение вместо r=0 возвращает r=-1/errno=11, ибо в логах такая строчка:
lyr:usbpiv485[-1]/DEFAULT: piv485_fd_p() r=0 errno=11: Resource temporarily unavailable
Я уж думал было, что то ли в ядре в какой-то момент (там "5.11.0-40-generic #44~20.04.2-Ubuntu"), то ли в конкретно Uebuntu поменяли поведение при disconnect'е -- а там был именно он, что видно по логам ядра, которые ТУТ в /var/log/syslog (закомменчены тут в тексте).
Так вот -- нифига-нифига!!! В строке-то чётко видно, что "r=0",
но просто я из-за лишнего текста этого не заметил, а зацепился за "errno=11"
(кстати, strerror()
-описание "Resource temporarily unavailable"
НЕ совпадает с комментарием "Try again" к EAGAIN в
/usr/include/asm-generic/errno-base.h).
Вывод: надо перед read()
добавить принудительное
errno=0
.
24.11.2021: добавил.
24.11.2021@тропинка в леске около НГУ, между стадионом и Коптюга: так, наболело: вообще RS232/COM/serial -- ОЧЕНЬ неудобная штука в роли "просто шины для подключения устройств".
#ifdef
'ами" уже
становится не вполне адекватен.
А нужно-таки вытаскивать специфичности настройки serial-портов в отдельные файлы, примерно таким макаром:
SPECIFIC_SERIAL_SETUP_FILE_H
, ...
-- вместо нынешних начальных#ifdef SPECIFIC_SERIAL_SETUP_FILE_H #include SPECIFIC_SERIAL_SETUP_FILE_H #endif
#ifdef MOXA...
.
#include
;
SPECIFIC_SERIAL_SETUP_CODE
.
#ifdef SPECIFIC_SERIAL_SETUP_CODE SPECIFIC_SERIAL_SETUP_CODE(); #endif
...да, тем самым у нас становится как бы 2-уровневая архитектура HAL-файлов.
27.12.2021: делаем...
SPECIFIC_SERIAL_SETUP_CODE_1()
и
SPECIFIC_SERIAL_SETUP_CODE_2()
-- для того, чтобы можно было
выполнять эту настройку либо сразу после open()
'а, либо чуть
позже, после tcflush()
+tcsetattr()
.
LOCAL_DEFINES+= -DSPECIFIC_SERIAL_SETUP_FILE_H='"moxa_serial_setup_file.h"'
...вообще-то это ПЛОХОЙ вариант. ХОРОШИЙ -- если б уже
src/ShadowRules.mk ставил бы этот
-DSPECIFIC_SERIAL_SETUP_FILE_H
конкретно в одно правило
зависимости, туда же, куда и прочие -DSERIAL...
.
НО: оно там пока что закомментировано, а оставлено пока старое.
Т.е., всё подготовлено, но ПОКА что используется старая архитектура. Почему? А потому, что старая выглядит более понятной и обозримой.
SERIAL_HAL_PFX
.
Вопрос лишь в деталях реализации:
serial_hal_opendev()
'у параметр
dev_tty_name_prefix
", который использовать повсеместно.
В layerinfo -- нельзя: оно привязывается к номеру порта, а на одном узле может быть НЕСКОЛЬКО портов с одним номером, но разными типами -- вроде ttyM0 и ttyMUE0.
А никаких СТРОКОВЫХ параметров настройки драйвера у нас не предусмотрено.
...помнится, аналогичный вопрос уже как-то возникал; в связи не то с serial-драйверами, не то с CAN'овскими...
Нашёл -- да, в связи с ОБОИМИ: в разделе по cxsd_driver, за 07-09-2014, ключевое слово "businfostr".
Не, хочется-то хочется (уменьшить зоопарк), но вот только неудобств от этого при использовании видится многовато.
28.12.2021: немножко информации про потенциальный девайс от Advantech:
...и вот в нём схема именования вроде как "/dev/ttyAP*", но, судя по readme, предусмотрен также и какой-то другой вариант, мне малопонятный, "/dev/ttyBxxPxx" ("For example,B10 means Board ID is 10,P0 means port0.").
А им, судя по drivers/tty/serial/8250/8250_core.c, используется имя "ttyS".
Т.е., раз нет никакой настройки, а нужно просто конкретное имя /dev/ttyAP* открыть -- то как раз и пригодился бы вариант "обычный драйвер для COM умеет обращаться к произвольному имени".
28.12.2021: в качестве идеи решения проблемы "а как строку указывать?!":
GetLayerInfo("compiv485", -1)
...правда, это не отвечает на вопрос, как быть с утилитами командной строки. Может, позволить им указывать вместо номера линии прямо имя файла-устройства? Тогда это потребует ещё более полной перетряски API "serial_hal.h".
Так вот: у них, кроме режима работы "виртуальный COM-порт" ("Режим Real COM") есть вариант "TCP Server", когда к нему можно коннектиться по TCP и вести обмен по оному TCP-сокету.
Собственно: можно ли сделать поддержку такой штуки в "serial_hal"?
Понятно, что в принципе-то можно, но это очень сильно сломает всю идеологию -- если /dev/tty* просто открываешь, то с TCP-соединениями море маеты: надо дожидаться, пока приконнектится; а может коннект и обломиться; а может соединение порваться по ходу дела...
Короче -- лучше не связываться с этой маетой, а пользоваться готовым "виртуальным COM-портом".
16.12.2022: P.S. а с чего я вчера вообще тот разговор с Лёшей Герасёвым и ЧеблоПашей затеял: в EPICS Tech-talk недавно (09-12-2022) в обсуждении "Ways to know if instrument connected via TCP/IP through StreamDevice has lost connection" было сообщение, где человек утверждает, что
However, for virtual comports, I have noticed at times the driver drops the backend TCP connection and to restore the connection I have to close the comport and then reconnect.
Там, правда, не сказано, о каких конкретно виртуальных COM-портах речь, но вряд ли бывают какие-то ещё.
Как бы то ни было: если оно действительно так, то надо бы как-то уметь определять такой обрыв соединения и как можно быстрее восстанавливать.
17.12.2022: а не будет ли, случаем, при обрыве от
виртуального COM-порта прилетать "EOF" -- готовность по
select()
, но читается 0 байт? Если "да", то в
piv485_lyr_common.c такая ситуация уже детектится, но пока
корректно не обрабатывается -- там есть комментарий
/*!!! Should also (schedule?) re-open */
16.12.2022: ЧеблоПаша утверждает, что там нет никаких "протоколов" и возможности каких-либо настроек порта RS232/422/485 -- просто коннектишься к нужному порту (да, для каждого RS-порта NPort'а свой TCP-порт), и что в сокет пишешь, то уходит в RS, а что приходит из RS'а, то прилетает тебе в сокет.
17.12.2022: а однако ДА -- при гуглении по "moxa nport protocol" нашлось "FAQ по преобразователям RS-232/422/485 в Ethernet (серия NPort)", где
(bold мой)Все устройства NPort поддерживают режимы работы TCP Server, TCP Client, UDP Server/Client. В этих режимах данные передаются по сети через TCP- или UDP-сокеты. Данные передаются в "сыром" виде: информация, записанная в сокет, будет в неизменном виде выдаваться в последовательный порт устройства NPort. И, наоборот, пришедшие в последовательный порт данные без изменений будут переданы в сокеты TCP и UDP.
К примеру, если в COM-порт устройства NPort надо передать строку "12345", то для этого в установленный сокет нужно записать эту же самую строку "12345".
Если же Вам в режиме online необходимо менять настройки последовательного порта (скоорсть, четность, ...) или управлять служебными сигналами (RTS/CTS), то такие манипуляции осуществляются через служебный TCP-порт преобразователя. Протокол работы по этому порту публично не распространяется. Пожалуйста, если возникнет указанная задача управления портом, напишите об этом на наш адрес технической поддержки support@moxa.ru
Передача информации с каждого последовательного порта NPort идёт по двум TCP-портам: порт для передачи данных и порт для передачи служебных команд.
В режиме Real COM во всех устройствах NPort для передачи информации используются следующие TCP-порты
Порт передачи данных (COM1 ... COM16) Командные порты (СОМ1 ... СОМ16) TCP: 950 ... 965 TCP: 966 ... 981 Порт передачи данных (COM17 ... COM31) Командные порты (COM17 ... COM31) TCP: 982 ... 997 TCP: 998 ... 1013
(Сама карточка куплена ЕманоФедей для установки "стенд измерений ускоряющих структур" у Лёши Левичева; ранее там использовался USB-адаптер от Каплина, но он постоянно давал USB-дисконнекты.)
По результатам предварительных разбирательств определено, что требуемый нам драйвер называется mxupcie.ko, а потому директория так и названа.
23.12.2021: попытки добыть какие-то драйверы и разобраться, ЧТО за драйверы надо брать.
И оно не компилируется -- в int
-функции стоит просто
"return
", что приводит к ошибке "'return' with no value, in
function returning non-void".
Плюс -- прямо В ОБЫЧНОМ ЯДРЕ, даже в 3.10 от RHEL7, есть mxser.[ch] (но только в исходниках -- сборка отключена; хотя в Ubuntu -- включена).
24.12.2021: за вчера-сегодня разбирался в том, КАК эту дрянь программировать. НЕТ НИКАКОГО описания, поэтому пришлось рыться по коду -- как утилит, так и драйверов.
Но нет: это НЕ так, и даже файла moxadevice.h тут нет.
Но тоже нет: содержимое файлов mxser.h и mxpcie.h практически не пересекается. Это два совершенно РАЗНЫХ файла.
После этого встаёт вопрос: а *userspace*-то утилиты как работают -- ведь они, по идее, должны бы одинаково взаимодействовать и с mxser'овыми /dev/ttyM*, и с mxupcie'шными /dev/ttyMUE*? А ответ -- в п.3...
#include
'ит. Ответ: НИКАКОЙ! У неё там внутри есть СВОИ
определения всего и вся, причём в основном ещё и названия отличаются.
И вишенка на торте: местами отличаются не только названия, но даже ЗНАЧЕНИЯ: конкретно интересующий нас режим "RS485 2-проводный":
mxpcie.h: #define MX_RS485_2W 4 conftool.c: #define MX_RS485_2W 0x0f
На случай, если предположить, что я ошибся и это какие-то разные вещи: нет,
это именно то, что conftool.c сбагривает драйверу при помощи
соответствующего ioctl()
'а:
mxpcie.h: #define SMARTIO_PUART_SET_INTERFACE (MOXA + 79) conftool.c: #define MX_SET_INTERFACE (MX_IOCTL_CODE+79)
Как вся эта фигня вообще может работать -- я не знаю.
24.12.2021: ЕманоФедя поразбирался:
25.12.2021: а, нет -- я НЕ ТУДА СМОТРЕЛ!!!
и#define MOXA_UIR_RS232 0x00 #define MOXA_UIR_RS422 0x01 #define MOXA_UIR_RS485_4W 0x0B #define MOXA_UIR_RS485_2W 0x0F
#define MX_RS232 1 #define MX_RS422 2 #define MX_RS485_2W 4 #define MX_RS485_4W 8
...и что вообще там означает второй блок -- загадка: в коде драйвера
ссылок на MX_RS*
нет совсем (а вот на MOXA_UIR_RS*
есть -- именно так я и понял, что к чему: там эти константы очень активно
используются, в нескольких разных местах).
Возможно, второй блок -- вообще рудимент от какого-то другого файла; иначе говоря -- просто мусор.
Как именно я (или другою юзер) должен был догадаться, что нужно обращать
внимание именно на константы с "UIR
" в именах -- хбз.
Поскольку ни описания, ни комментариев никаких нет.
Так что вывод остаётся прежний -- ОТВРАТИТЕЛЬНО написанный софт.
27.12.2021: ещё пара "деталей":
Причём это конкретно ТУТ оно так, а в том, что прилагается к RHEL7'шному ядру 3.10 -- всё именно в mxser.h.
mxser.h: #define MOXA_SET_OP_MODE (MOXA + 66) moxadevice.h: #define MOXA_SET_OP_MODE (0x400+66) // to set operating mode
...И, тем самым, ОТЛИЧАЮТСЯ от PCIe'шных! Т.е., userspace-API у mxser и mxpcie/mxupcie -- РАЗНЫЕ!!! Даже и не знаю, что про такое можно сказать (остаётся только цитировать Лаврова, Задорнова ("ну тупы-ы-ые!") или Будрайтиса ("сказочный долбо#б...")).
#include
'ить в
userspace-код, т.к.:
VERSION_CODE()
, определяемый
в mxpcie.c (прямо шЫдевр стиля -- .h требует от своего
включатора определения чего-то, НЕ являющегося "параметрами"...).
#include
'ится <asm/uaccess.h>,
который обычная система иметь совсем не обязана.
Откуда вопрос: а НАФИГА вообще было делать эти отдельные файлы mxser.h и mxpcie.h, если они в принципе не годны ни для чего, кроме как быть включены в код драйверов?
Меня вот даже не в универе, а ещё в ШКОЛЕ, на азах программирования, учили, что ТАК делать нельзя -- если у тебя какие-то числа/коды, совместно используемые несколькими разными модулями, или, тем более, определяющие интерфейс между этими модулями, то их нужно выносить в отдельные файлы, которые и должны использоваться обоими модулями.
Но тут -- вроде бы "уважаемая" контора MOXA, а такой отвратительный код...
И немножко ещё "деталей":
mxupcie_
.
#define MOXA 0x400 . . . #define SMARTIO_PUART_SET_INTERFACE (MOXA + 79) . . . #define MX_RS485_2W 4
#define MOXA_SET_OP_MODE (0x400+66) // to set operating mode . . . #define RS485_2WIRE_MODE 1
Но есть и отличие:
ioctl(fd, MOXA_SET_OP_MODE, &mode)
(как и положено --
параметр передаём по указателю),
ioctl(fd, MX_SET_INTERFACE, mode)
-- т.е., параметр передаётся
прямо значением
(И там вообще изрядный бардак в mxupcie_ioctl()
: бОльшая
часть использования параметра -- arg
-- как указателя, но
конкретно для SMARTIO_PUART_SET_INTERFACE
стоит
"(unsigned char)arg
"
и ещё в нескольких местах аналогично или "(int)arg
".)
В общем -- бардак, бардак дичайший, никакой даже попытки унификации или создания единого API, с сплошь какие-то сиюминутные/локальные решения, с хаотичным использованием отдельных фрагментов из предшествующих реализаций...
25.12.2021: да, я мало того, что потратил ДВА дня на разбирательство с кривейшей поддержкой железки, так ещё и потратил море времени на то, чтобы это всё записать. Зачем?
Затем, что это очень показательный пример -- тут ПЛОХО ВСЁ. Записано просто чтоб было под рукой, как пример, чьи проблемы можно привести детально. Вкратце:
24.12.2021: за вчера-сегодня КАК БЫ сделано.
mxu
" -- как сокращённое имя
директриии и драйвера ("mxupcie").
...хотя можно было выбрать и "mue
" -- по префиксу имён
устройств /dev/ttyMUE*.
LOCAL_DEFINES= -DMXUPCIE_KSHD485
по образцу из moxa/Makefile.
Но это всё не получилось (причины также описаны в предыдущем разделе).
#include
:
#define MX_IOCTL_CODE 0x400 #define MX_SET_INTERFACE (MX_IOCTL_CODE+79) #define MX_RS485_2W 0x0f
Причём скопировано это было не из mxpcie.h, а из утилиты
conftool.c -- чтоб было уж всё как в ней, откуда и сама строчка
настройки взята:
ioctl(fd, MX_SET_INTERFACE, mode)
.
После этого оно наконец успешно стало компилироваться.
27.12.2021: так что
given/mxser-linux-kernel-5.x.x/ удалена, как и указание её в
LOCAL_INCLUDES
.
После чего попробовали на вроде бы живой железке -- неа, не работает: пакеты вроде бы отправляет, но никаких ответов не видит.
27.12.2021: посмотрел ещё раз на код
serial_hal_opendev()
и возникло сомнение: вот там используется
такой порядок:
open()
tcflush(fd, TCIFLUSH)
и
tcsetattr(fd,TCSANOW,&newtio)
.
Собственно -- а может, надо СРАЗУ после открытия делать специфическую настройку, а ПОТОМ уже стандартную (где скорость и прочие режимы работы порта)? Ведь эта "стандартная" может по-разному работать в разных режимах.
@вечер: заглянул в
conftool.c -- ДА, там
MX_SET_INTERFACE
выполняется СРАЗУ после open()
(посредством вызова внутренней
mx_set_interface()
). Правда, там это
ЕДИНСТВЕННОЕ выполняемое действие -- т.к. этот conftool работает
дополнением к setserial'у и предполагается к использованию совместно с
собственно ПО, работающим с линией.
29.12.2021: заработало!!! Причина первоначальной
проблемы оказалась в том, что ЕманоФедя при подключении перепутал провода A
и B; он после этого даже получил консультацию Волкольда Волкольдовича,
посмотрел на линию осциллографом -- увидел пакеты 10 раз в секунду (это
"пинги" KCMD_GET_STATUS
=3), затем поменял провода и увидел уже
нормальные запрос/ответ.
На этом можно считать за "done".
Раньше потребности не возникало (сами cpci/-драйверы непринципиально отличаются от v2hw'шных по сути работы с железом, а от прочих CAMAC/VME -- по сути pzframe'овости).
Сейчас же -- захотелось довести драйвер ядра uspci.c до компилируемости под современные ядра (побудительный мотив -- "а не использовать ли USPCI для взаимодействия с 250МГц и 5ГГц осциллографами от Хильченко, чтобы драйверы были в userspace).
01.06.2022: по "unlocked_ioctl compat_ioctl" нагуглилось:
Вкратце:
unlocked_ioctl
-- для избавления от "big kernel lock'а":
раньше на время ioctl'ов он всегда лочился, но на SMP- и тем более
многоядерных системах это стало сильно мешать; вот и заменили на
unlocked-версию, когда драйвер должен сам заботиться о лоченьи в тех (не
самых частых) случаях, когда оно критично.
compat_ioctl
-- совершенно не связанная штука, вызванная
тем, что при 64-битном ядре программы могут быть и 32-битными, так что у них
и указатели тоже будут 32-битными, а при общении между userspace и ядром с
использованием указателей (обычно 3-й аргумент, void *argp
,
передаёт указатель на структуру данных; в USPCI именно так) получится, что
указатели не того размера, да и вообще всё съедет нафиг.
Вот и была введена возможность 64-битным драйверам поддерживать 32-битные программы.
...т.е., ПО-ХОРОШЕМУ надо было называть это "ioctl32" или "compat_ioctl32".
#if LINUX_VERSION_CODE >= VERSION(2,6,11)
в A3818Drv-*/src/a3818.c, началось это с 2.6.11.
04.06.2022: проапдейтил: оказалось, что для НЕработы 32-битных программ достаточно просто оставить .compat_ioctl=NULL, и будет ENOTTY/"Inappropriate ioctl for device".
Заодно исправил uspci_test.c -- там при "фиксеньи" парсинга
(24-01-2022?) был внесён косяк -- добавлена проверка
"*p != '\0'
" вместо "*errp != '\0'
";
она очевидным образом ВСЕГДА обламывалась.
05.06.2022: кстати, сразу тогда возник вопрос: а как же тогда у нас всё компилировалось под 2.6.25, если "unlocked_ioctl" появился ещё в 2.6.11? Вопрос решился заглядыванием в исходники 2.6.18 (в RHEL5 на vepp4-pult6):
ioctl
,
unlocked_ioctl
, compat_ioctl
.
ParseDevSpec()
, внесённый,
видимо, 24-01-2022.
Так что напрашиваются следующие reusable-компоненты:
26.04.2010: да, вчера продумал разделение на компоненты более подробно. Итак:
Компоненту/функции передаются port_n, cb1 (что вызывать по получению запроса на запуск драйвлета), cb2 (если !=NULL -- то слушать port_n+1 и вызывать по коннекту на него).
main()
-действия: setvbuf(), SIGPIPE=SIG_IGN,
SIGCHLD=ripper, ParseCommandLine(), и потом вызывает
remdrvlet_listener().
28.04.2010: в продолжение -- есть очень радикальная идея:
Ведь кроме CANGW/canserver могут быть и другие аналогичные задачи (та же MOXA). Ну так -- пусть "удалённый сервер" реализуется отдельной библиотекой, а canserver будёт лишь её юзером.
Так что -- переименовываем lib/remdrvlet в lib/rem, и кроме libremdrvlet.a заводим там libremsrv.a.
Таким образом -- мы получаем "близнецов" lib/srv и lib/rem, содержащих наборы библиотек для создания "настоящих" и "удалённых" серверов.
P.S. А "drvtable" обрабатывается этой же библиотечкой (и собирается
она из стандартных CxsdDriverModRec
-- никакие самопальные
drivermapping_t не нужны). Ну и с "lyrtable" (которая в каком-то виде
понадобится) -- аналогично.
28.04.2010: еще: у разных применений могут быть
разные потребности в количестве поддерживаемых одновременно драйверов.
Так вот -- можно в соответствующем месте исходников количество
элементов соответствующего массива определять при помощи
#define
'а -- например, MAX_REM_DEVS
,
уставляемого условно, по #ifndef
, а уж Makefile,
собирающий конкретное примерение, может сделать
LOCAL_CPPFLAGS=-DMAX_REM_DEVS=nnn
.
29.04.2010: замечание: естественно, этой "удалённой" реализацией API cxsd_driver НЕ будет поддерживаться часть, касающаяся внутрисерверного взаимодейтсивя драйверов.
Нерадостно, конечно -- v4'шная инфраструктура элегантнее, чем её бледное отражение в v2. Возможно, в будущем всё-таки возьмём оставшиеся неиспользованные интересные решения.
08.11.2019: да, это будет полезно со всех сторон:
(Сейчас ВСЕМ принудительно подсовывается 1-й (точнее, с lyrid=-1).)
30.06.2015: всех тонкостей уже не помню (СРАЗУ надо было записывать!), но некоторые:
Оное выравнивание формализовано в inline-функции
REMDRV_PROTO_SIZE_CEIL()
.
remdrv_pkt_header_t
-- теперь имеет размер 4
штуки int32, из которых первая пара фиксированная (pktsize,command), а
оставшиеся в поле var
, которое в любой своей ветви может
содержать не более 2 штук int32.
remdrv_data_NNN_t
, которое по
возможности так же добивается до кратности 8 байтам.
(Немного похоже на структуру с chunk'ами, только chunk всегда один)
Всё это -- БЕЗ добивки до 4 байт.
ReturnDataSet()
получилась самой монструозной) и
ProcessPacket()
.
/*!!! LAYER_HACK */
.
А вот ПО-ХОРОШЕМУ, надо делать просто "полную" инфраструктуру layer'ов прямо в librem*, чтобы их могло быть больше одного.
active_devid
и
ENTER_DRIVER_S()
/LEAVE_DRIVER_S()
(из
remcxsd.h).
CHECK_*()
/DO_CHECK_*()
.
03.07.2015: после исправления мелких идиотских косячков первый результат: без РЕАЛЬНОЙ поддержки layer'ов в rem* -- вообще никак.
Т.к. после любого скопычивания драйвера (например, из-за закрытия
соединения сервером) при FreeDevID()
никто НЕ делает
disconnect() драйвера, и он остаётся занимать позицию.
Прямо СЕЙЧАС проблема решена заплаткой: в remcxsd_dev_t
добавлено поле lyr_p
, которое
remsrv_drvmgr.c::FreeDevID()
проверяет, а
canserver_common.c, куда еще раньше был добавлен хак
GetLayerVMT()
, всегда возвращающий VMT от своего
фиксированного layer'а, в оной GetLayerVMT()
прописывает в
lyr_p
ссылку на метрику layer'а.
07.07.2015: вводим реальную поддержку layer'ов
вместо того lyr_p
.
Работа совместная в rem* и в canserver_common.c с c4l-cangw/Makefile.
remcxsd_layers
-- указатель на массив указателей
на метрики layer'ов.
remcxsd_numlyrs
-- размер оного массива в штуках
(ВКЛЮЧАЯ [0]).
lyrtable[]
(аналогично таблице драйверов), и заполняет remcxsd_layers=lyrtable
remcxsd_numlyrs=countof(lyrtable).
remsrv_main()
всем layer'ам вызываются init_mod()
и init_lyr().
Но НЕ драйверам, увы.
ReadDriverName()
(sic!)
делается пред-программирование dev->lyrid=-1.
GetLayerVMT()
один-в-один взята с серверовой.
DO_CHECK_SANITY_OF_DEVID()
тупо закомментирован, как и в cxsd_hw.c, вместо надлежащей
проверки "не в диапазоне (-remcxsd_numlyrs,1] ли".
На вид -- вроде пашет.
25.07.2015: только в vDoDriverLog()
недоставало проверки на "devid<0 и является lyrid'ом" -- оно
в v4c4lcanserver'е постоянно ругалось "message from unknown devid=-1".
Добавлено, проблема ушла.
26.07.2015: и еще косячок был, проявлялся в ругани на auxinfo -- "psp_parse(auxinfo)@remsrv: Junk at position 2" (позиция могла быть разной).
Причина оказалась в
remcxsd_driver_v4.c::ProcessPacket()
: оно
неправильно рассчитывало позицию auxinfo
в пакете CONFIG.
Брало
(char*)(data+sizeof(int32)*businfocount))
-- а поскольку data[] имеет тип int32
, то оно сильно
промахивалось. Можно было убрать "sizeof(int32)*", но сделано
(char*)(data)+sizeof(int32)*businfocount)
-- чтоб не зависеть от того, какого data[] типа.
31.08.2015: имеются странности с производительностью: на cangw-magsys (магнитная система линака) v2'шный с "disable_log_frame" занимал около 60-70% CPU, а v4'шный жрёт 97% (т.е. "под завязку", idle=0.0%). Почему так?
01.09.2015: разборки:
cxsd_logger_verbosity
.
GetDevLogPrms()
-- аналог v2'шной GetCurrentLogSettings()
.
...не сказать, что изменения отслеживаются красиво -- по-хорошему, надо б иметь драйверов метод "logmask изменился", о чём думалось еще в bigfile-0001.html за 19-01-2007 (ключевое слово "ХРЕН!").
25.10.2016: возможно, проблема в отсутствии "чтения до 30 раз
по repcount" в cankoz_fd_p()
. Т.к. select()
--
это тоже syscall, а она после каждого пакета делает возврат и опять
select(), то получается по 2 syscall'а на пакет. Парой минут позже:
нифига! Перед "goto REPEAT" делается проверка готовности дескриптора, через
check_fd_state()
, который тоже сводится к select()'у.
SetDevState()
был косяк при наличии
description
:
pkt.pktsize
вместо sizeof(pkt)
-- видимо, по
аналогии из других мест был скопирован вызов fdio_send(...).
Т.е., в качестве "description" слала мусор из стека,
...и потом еще отдельно слало саму строку.
FDIO_R_INPKT2BIG
.
Вылезло вчера при тестировании не-застреливания в CAN-драйверах по неверному devcode. И почему только никогда не вылезало раньше -- загадка.
30.04.2010: несколько замечаний:
Но зато преспокойно можно так юзать "невежливые" драйверы, желающие монопольности (типа a_adc200 или нечта, написанного на чужой закрытой библиотеке).
07.05.2010: угу, вводим правило -- что таковые драйверы указываются "." вместо имени хоста.
14.05.2010: почти сделано -- осталось только формирование имени для execv() (с использованием layerinfo).
Только легкое сомнение -- а точно такие драйверы НЕ должны авто-перезапускаться в случае закрытия соединения?
16.05.2010: добито. Оно теперь усимметризовано
-- в _init_d() только определяется "вид" драйвера
(DRVLET_FORKED
или DRVLET_REMOTE
), а
собственно запуск В ОБОИХ случаях делается в
InitiateStart()
. Так что теперь для возможности
авто-перезапуска fork+exec-драйверов достаточно убрать условие в
ScheduleReconnect()
.
А директория, в которой лежат локальные .drvlet'ы, берется из layerinfo "remdrv_fork_drvlets_dir", по умолчанию -- "lib/server/drivers".
14.05.2010: а еще ведь может захотеться другой вариант локальных драйверов -- которые б не fork()+exec()'ились, а просто от-fork()'овывались, и сразу начинали бы работать.
Фактически -- это нечто типа порождения доп. thread'а, но в другом процессе.
14.05.2010: неа, не стоит:
Но этот вариант вообще НИЧЕМ не лучше нынешнего с fork()+exec() -- т.к. тот модуль всё равно сильно отдЕлен.
Это еще хуже -- поскольку конфигурация аппаратуры будет не полностью описываться devlist'ом, а еще и вылазить в cxsd.conf.
Так что -- сразу же "withdrawn".
14.05.2010: а вот просто попробовать "мясо" remdrv_drv.c вытащить в нечто библиотекообразное -- может, и разумно; например, для тех же отдельно-thread'овых драйверов. Но это если РЕАЛЬНО понадобится -- сейчас незачем, а усложнение от этого неизбежно.
27.07.2010: видимо, когда писал код, то про это тупо забыл. А ведь могло быть чревато кучей последствий. Фиксим:
TryToReconnect()
.
CleanupEVRT()
-- одновременно с
DeregisterDevTout()
.
На этом считаем потенциальную проблему исчерпанной. Пара замечаний:
RegisterDevTout()
НЕ выдаёт
tid=0. Да и потом, при обычной работе, скорее всего проблемы бы не
возникло -- поскольку всё время под reconnect_tid выдавалось бы одно и
то же число. Другое дело, что могла бы возникнуть путаница с будущим
heartbeat_tid'ом.
04.05.2016: внедрено-то еще 09-07-2015, но выглядит халтурно:
hbt_tid
(в отличие от cxlib'а).
return
.
...а "первоначально" заказывается таймаут вызовом самой
HeartBeat()
из единственной точки --
remdrv_init_d()
, поэтому повторно НИКОГДА таймаут не появится
(и, аналогично, второй экземпляр никогда не будет зарегистрирован при
еще-не-сработавшем первом (как бывало в начале 2000-х -- кажется, с
автоповтором у стрелок DIAL'а)).
Халтура унаследована еще от v2 (скопировано), но это её не извиняет.
return
ставим goto
RE_ENQ
.
...кстати, смутно вспоминается, что вроде были какие-то странности -- типа не восстанавливалась связь с удалёнными контроллерами после их перезагрузки. Не по этой ли как раз причине? И ведь ДОЛЖНА была НЕ восстанавливаться, после 2-й потери!
Решение:
GetLayerInfo()
.
Вопросы только, где брать LAYERNAME (чисто по формату devlist'а -- откуда?) и какой брать N (считать, что если businfocount>1, то businfo[0] -- line_n?).
08.11.2019: на вопрос где брать LAYERNAME (чисто по формату devlist'а -- откуда?)" ответ как раз есть -- выше, 28-06-2015, было придумано: что layer для remote-драйвера должен указываться прямо в remdrv'шной части auxinfo, в формате "E:HOST[@LAYER]".
02.07.2015: небольшие комментарии по состоянию:
(с float32 аналогично). Причина таких извращений, вместо очевидного приведения типов указателей -- "Dereferencing type-punned pointer will break strict-aliasing rules".union {uint64 u64; float64 f64;} uif64_t; static float64 swab_f64(float64 v) { uif64_t s64; uif64_t d64; s64.f64 = v; d64.u64 = SwapBytes64(s64.u64); return d64.f64; }
Кстати, эти самые rules делают исключение для char-указателей -- предполагается, что через них могут модифицироваться любые другие типы.
03.07.2015: проверяем.
Поэтому "на той стороне" сделан хак, чтобы проверять.
06.07.2015: далее:
09.07.2015: впортированы heartbeat (PING) и повторные коннекты при BUSY.
29.07.2015: исправлен еще косячок:
ProcessInData()
при сборе из пакета данных для Return'а
двигалось между ними со сдвигом на unitsize*nelems, БЕЗ padding'а. В
результате значения возвращались бредовыми ("через одно", не те) --
все, КРОМЕ ПЕРВОГО. Замечено было на ioregs у CAN -- поскольку это
пока единственное, где делается возврат более 1 канала в пачке.
Теперь сдвигается на REMDRV_PROTO_SIZE_CEIL(unitsize*nelems).
04.05.2016: учитывая, что драйвер используется рутинно с сентября 2015г. и исполняет свои обязанности, помечаем пункт как "done".
Полезно это для отладки вот прям щас, чтобы иметь одновременно запущенными и старый canserver, и v4c4lcanserver (при запуске серверов по очереди всё будет окей -- неиспользуемую CAN-линию оба canserver'а отпускают).
20.07.2015: вроде сделано -- указывается очевидным образом, ":PORT" после имени хоста.
21.07.2015: проверил -- был косяк, что оно сразу
складывало число в me->port
, а privrec-то еще не
аллокирован! В остальном после исправления работает.
SEGLEN_MAX
=100) -- проблему
можно решить, сравнивая sizeof(int)==sizeof(int32), и если "да", то
конвертировать номера каналов прямо в пришедшем пакете и
ReturnDataSet()
с values[] прямо оттуда.
03.05.2016: ...неа, проблема-то в векторе указателей
на возвращаемые значения (vp2ret[]
) -- его в пакете разместить
точно никак никогда.
04.05.2016: кстати, в первоначальной идее было забыто:
uint32
.
В любом случае, даже вопрос вектора указателей на значения уже ставит крест на этой идее, так что "withdrawn".
09.02.2017: проверено, что триада-тв'шный девайс работает и с просто LF (т.е., "\n" вместо "\r\n"), но не суть.
Убрана CleanupEVRT()
сотоварищи, поскольку подчистку по
завершению драйвера прекрасно выполняет сервер.
22.02.2017: в triadatv_um_drv.c добавлена и работа с sendqlib'ом, и он уже начал "дышать", получая данные от живых девайсов.
Видно, что при надобности создания еще хотя бы одного
аналогичного драйвера можно и нужно будет вытащить общий функционал в
какую-нибудь libtcp_string_drv -- чтоб там и реконнекты поддерживались, и об
очереди чтоб он знал (ради sq_clear()
'енья; а помещать ли туда
собственно поддержку очереди -- вопрос, т.к. форматы/протоколы у всех
устройств могут быть разными).
18.12.2022@утро, просыпаясь: вот правда -- ну о-о-очень хочется вытащить всю эту логику коннектов/реконнектов в отдельную библиотечку. Текущий "побудительный мотив" -- потенциальная поддержка режима "TCP Server" в MOXA NPort: чтоб CX'ный драйвер мог бы сам напрямую взаимодействовать с бриджом, не требуя драйвера в ядро.
При этом добавится ещё один элемент в "набор кубиков для построения драйверов", в настоящий момент включающий sendqlib, fdiolib, vdev, pzframe. Формально эти кубики можно компоновать произвольным образом.
А ведь давно уже есть этот "ещё хотя бы один аналогичный драйвер" -- modbus_tcp_rb.c; сделан он был ещё в районе 07-2020, но как-то быстро/впопыхах/по-простому, описания его изготовления нет вовсе. Судя по содержимому, его потроха были скопированы с кого-то из предыдущих.
30.03.2017: вылезло при попытке понять, что же происходит с linac1:11 -- там почему-то горели болотным все драйверы от rst1, вот я и попробовал сделать kill 1. А он так и остался навсегда болотным.
Некоторые подробности:
Проверено -- неа: перезагрузка контроллера остальные драйверы оживила, а эти два -- нет, так и остались навсегда болотными.
_term_d()
: последующая проверка CAN-монитором
показала, что устройства (CANADC40) не сыплют пакетами периодических
измерений -- т.к. им были посланы DESC_STOP.
Но нет -- там на вид всё окей.
ProcessInData()
по получении
CHSTAT просто исполняет указанное в нём -- т.е., делает
SetDevState(,DEVSTATE_OFFLINE,...)
(так что последующее
закрытие соединения -- FDIO_R_CLOSED
-- ловить будет уже
некому).
FreeDevID()
, что на стороне remdrv будет видно как просто
закрытие соединения.
Теперь проверять (когда?).
Конкретный побудительный мотив -- предстоящая надобность запинывания драйверов для LXI (см. его раздел в "Знания" за сегодня) и MQTT, где поток текстовый (так что можно использовать ровно то же, лишь в варианте FDIO_STRING).
Плюс, уже энное время назад делался triadatv_um_drv.c как раз таким вытаскиванием и переделкой на STRING.
Надо этот текст утащить уже в секцию новой библиотеки, здесь оставив лишь краткое упоминание, что "вытащено в NNN, и сам remdrv_drv.c переведён на неё".
17.10.2018@утро-дома: некоторые предполагаемые аспекты внутренней реализации:
Для чего в контексте иметь поля как handle
, так и
fdh
, присваивая полю неиспользуемого варианта -1
.
А какой вариант использовать -- клиент указывает при инициализации.
sq_clear()
.
HEARTBEAT_PERIOD
сброшен с 300 секунд (5 минут) до 60 секунд (1
минута).
Так период пинга будет плюс-минус равен времени перезагрузки мамкинских контроллеров после reset или дёрганья питания.
(Конкретной причиной стало то, что сервер долго не обнаруживал рестарта CM5307 крейта 3-го клистрона (который пришлось дёрнуть из-за странностей после броска питания). Причём Федя умудрился рестартовать сервер уже ПОСЛЕ обнаружения им потери связи с драйверами, но еще ДО её восстановления -- притом, что интервал там всего 10 секунд; но Федя в него попал.)
Возможно, этот интервал стоило бы сделать настраиваемым per-device -- устройства-то все разные. Но пока ограничимся 1 минутой.
._devstate
, в т.ч. и timestamp, так что
нельзя по нему узнать время возникновения проблемы, а приходится лезть в
/var/tmp/4drivers.log (что и неудобно, и имеет срок 1 месяц).
18.12.2022: решение просто: в
ScheduleReconnect()
уведомление о
DEVSTATE_NOTREADY
теперь не безусловное, а лишь в случае, если
предыдущее состояние НЕ is_reconnecting
:
if (!(me->is_reconnecting)) SetDevState(devid, DEVSTATE_NOTREADY, CXRF_REM_C_PROBL, reason);
Краткое обсуждение:
is_reconnecting
как раз и отражает состояние "уже страдаем
reconnect'ом", причём выставляется ПОСЛЕ этого SetDevState()
, а
сбрасывается ПЕРЕД переходом в DEVSTATE_OPERATING
, то побочных
эффектов вроде не намечается.
is_suffering
" (а в CXv2 и в remdrv_drv.c называлось
"is_suffering
").
_devstate_description
будет оставаться "старое" объяснение
(первое, а не последнее).
Но это же и достоинство: теперь там всегда будет ПЕРВОНАЧАЛЬНАЯ причина проблемы, а не перманентное "connection failure".
P.S. И в triadatv_um_drv.c с modbus_tcp_rb_drv.c это также добавлено.
27.04.2010: например -- чтобы пакет CONFIG слался не сразу, а так: драйвлет при запуске отправляет первый пакет, содержащий лишь команду "ENDIANNESS", endianness-fingerprint и версию протокола (так что бремя проверки совместимости версий будет на remdrv).
07.05.2010: только одна маленькая проблема: первым ответным пакетом может оказаться пакет RRUNDP, а у него предусмотрено сообщение (т.е. -- данные ПОСЛЕ заголовка); да и вообще, в протоколе remdrv в заголовке указывается ПОЛНЫЙ размер пакета, БЕЗ вычета размера заголовка (хотя это-то можно и поменять, аналогично cx_proto).
А что б вообще v4'шный не перевести на 8002?
24.03.2016: да, сделано -- теперь
REMDRV_DEFAULT_PORT = 8002
.
remdrvlet_report()
(отправка диагностического
сообщения хосту) и remdrvlet_debug()
(диагностический
вывод на stderr).
remdrvlet_srvmain()
должен добывать их из
командной строки и публиковать в каких-нить переменных (ли отдавать
через аксессоры).
И, соответственно, порт должен указываться в ключике -p.
29.04.2010: да, сделано, и rrund к этому адаптирован.
30.04.2010: мозги там будут практически копией
оных из cxsd_driver.c, и подход нужен аналогичный -- описание
всех зарегистрированных устройством событий в единой структуре. Посему
скопировал из cxsd_hw.h описание структурки
cxsd_hw_eventinfo_t
сотоварищи, позаменяв
cxsd_hw_/CXSD_HW_ на remdrvlet_/REMDRVLET_.
09.02.2012@Снежинск-каземат-11: вот это "скопировал из cxsd_hw.h..." напрягает. Ведь по факту реализации всего вспомогательного функционала ("oslike") в lib/srv/ и lib/rem/ получаются идентичными -- так какого чёрта дублирование?
Можно ли как-нибудь использовать один и тот же код для управления вспомогательностями в обеих библиотеках?
Это однозначно кореллирует с тем, что записано в разделе libremsrv за 29-04-2010.
01.07.2013: удаляем модуль в связи с переходом на uniq.
01.07.2013: не будет за ненадобностью вследствие uniq.
dlopen()
.
Надо это серьёзно обдумывать, чтобы понять, как/где лучше такое реализовать.
08.11.2019: смотрим remsrv_drvmgr.c.
ReadDriverName()
.
remsrv_drivers[]
.
remcxsd_layers[]
.
FreeDevID()
.
remcxsd_dev_t
понадобится добавить
какие-то поля для указания на "источник" драйвера -- встроенный или
динамически-загруженный, и если последнее, то номер в таблице загруженных.
...хотя -- можно эти два поля совместить в одно: если ==0 -- встроенный, >0 -- загруженный, и тогда это индекс в той таблице (в которой в таком случае 0-й элемент никогда не используется).
09.11.2019@дома-ванна: да
как-как -- просто! Ровно так же, как и в rrund.c -- сохранять
ссылки на argv[] в argv_params[]
. (Парсинг argv[] -- отдельный
квест, учитывая возможность указывать порт; но тоже не мега-проблема.)
27.11.2019: приступаем (с горя, "от нечего делать" -- после обнаружения бесконечных косяков в работе BIVME2, чтобы отдохнуть и сделать хоть что-то успешно работающее):
remcxsd_dev_t
добавлено поле dlref
.
MAY_USE_DLOPEN
, определение его в 1 при неопределённости, а
далее #include<dlfcn.h>
при не-0.
dl_drv_info_t
, содержащий "описание
загруженного модуля".
Идеологически оно содрано с cxldr_used_rec_t
.
dltable[100]
(0-й элемент которого никогда не будет
использоваться).
Да, халтурновато, зато просто: занимаемый объём крошечный, зато код работы с этим очень прост. А количество загружаемых одновременно драйверов вряд ли когда-нибудь достигнет сотни.
FreeDevID()
всё просто -- если это загруженный
драйвер, то делаем ему --ref_count, и если результат ==0, то выгружаем
драйвер dlclose()
'ом и зануляем ячейку в
dltable[]
.
ReadDriverName()
:
remsrv_drivers[]
, и выполняется в случае, если там не найдено.
...все используемые переменные покамест объявлены прямо внутри
if(){}
.
Начинается цикл с поиска по dltable[]
, а если не найдено и
stage==0 -- то выполняется попытка загрузки.
По сути, такая организация имеет смысл
но всё вместе, БЕЗ вытаскивания поиска в отдельную функцию.ПОИСК_СРЕДИ_ЗАГРУЖЕННЫХ(); ПОПЫТКА_ЗАГРУЗКИ(); ПОИСК_СРЕДИ_ЗАГРУЖЕННЫХ();
Проверено, что файл компилируется и с MAY_USE_DLOPEN=1, и с =0.
28.11.2019: продолжаем.
А её и в x-compile/*/ нет...
Так что --
SPECIFIC_INCLUDES+=$(LIBDL)
-- это коснулось
can/c4l-cangw/, can/sktcanserver/, serial/moxa/,
vme/mbivme2/.
term_mod()
.
Добавлен.
DOUBLE_REPORT()
,
внутри вызывающий remcxsd_debug()
для вывода на консоль и
remcxsd_report_to_fd()
для сообщения хосту.
Да, это немного халтурно и предполагает отсутствие side-effects в параметрах.
if (action1_ok) { if (action2_ok) ... else { CLEANUP and BARK } } else { CLEANUP and BARK }
Но это шибко длинно, малоочевидно, плюс ошибки (и подчистка!) расположены в порядке, противоположном действиям.
if (!actionN_ok) { CLEANUP and BARK goto LOAD_FAILURE; }
Получилось короче и понятнее.
И да: со следующим пунктом -- многочисленными проверками (главное -- связанным с layer'ами) -- только такой "линейный" вариант и годится, а старый, со вложенными if()'ами, был бы крайне труднореализуем.
dltable[]
и заполняется.
init_mod()
.
Причём при ошибке драйвер выгружается.
CXSD_DRIVER_MODREC_VERSION
.
И вот эта толпа проверок -- особенно по layer'у -- имеет просто гигантский объём.
29.11.2019: первая попытка проверки. Делается путём cd в директорию с v4bivme2vmeserver'ом, куда также скомпилирован тривиальнейший драйвер zzz_drv.so.
dlopen()
в таком случае НЕ пытается брать файл из текущей
директории, а сразу выполняет поиск по $LD_LIBRARY_PATH -- это прямо в man'е
написано.
А вот тут уже облом: SIGSEGV. Надо разбираться.
02.12.2019: продолжаем, разбираемся.
layerrec
(ссылки на
layer'ов modrec): стояло
lyrrec = remcxsd_layers + -dev->lyrid
вместо
lyrrec = remcxsd_layers[-dev->lyrid]
А поскольку remcxsd_layers
-- это
т.е., массив указателей на указатели, то ошибочный вариант давал не указатель на modrec, а указатель на указатель на него, вот и получалась фигня.static CxsdLayerModRec * lyrtable[] = { NULL, &bivme2vme_layer_modrec, }; CxsdLayerModRec **remcxsd_layers = lyrtable;
Это исправлено, и SIGSEGV стал появляться дальше :D
DOUBLE_REPORT()
. Поставил
отладочную печать ДО этого места -- всё окей. После -- падает;
НЕПОСРЕДСТВЕННО "до" -- тоже падает...
В конце концов допёрло: пачать делается ПОСЛЕ dlclose()
;
т.е., тогда, когда сам модуль драйвера уже отмаппирован из памяти -- вот и
падает из-за обращения к уже отсутствующей странице!
Заковыристый баг, блин...
Решение очевидно: переставил ВСЕ вызовы DOUBLE_REPORT()
в
начала if(){}-секций, чтобы были РАНЬШЕ вызовов dlclose()
.
Дальше надо делать парсинг префикса/суффикса из командной строки.
02.12.2019@сидя-на-лекции-семинаре-Бондаря "О спектроскопии тяжёлого кваркония": надо парсинг делать по-простому -- "независимо": если удалось argv[argn] интерпретировать как число, то считать его указанием порта, а иначе (если pidx<2) -- как указание префикса/суффикса.
03.12.2019@вечер-дома: делаем -- для начала ИСПОЛЬЗОВАНИЕ параметров. Подсматриваем в rrund.c.
argv_params[2]
. ТОЛЬКО при MAY_USE_DLOPEN!=0.
prefix
, suffix
.
04.12.2019: а теперь собственно парсинг из командной строки.
only_digits()
: если в
argv[argn] только цифры, то пытаемся интерпретировать это как номер порта, а
иначе -- как argv_params[].
goto
.
ShowHelp()
добавлено, причём выдаётся только при
MAY_USE_DLOPEN!=0.
Проверено -- да, работает, во всех возможных комбинациях (с указанием порта как до, так и после; и с указанием лишь префикса; и "лишний" параметр всегда считается за ошибку).
Единственное замечание: если префикс -- это просто имя директории, то в конце обязательно должен быть '/', поскольку склейка делается "втупую".
Чуток позже: наполнив тестовый драйвер zzz_drv.c всеми
init_m()/term_m() и init_d()/term_d(), обнаружил, что term_m() никогда не
вызывается. Косяк был в том, что при нахождении драйвера в dltable[] в
dev->
НЕ прописывался его dlref
. Исправлено --
теперь term_m() вызывается и драйвер выгружается (а при повторном обращении
-- загружается заново).
Вот теперь всё -- можно считать технологию работающей, доведённой, а раздел за "done".
Суть: при испытании пары драйверов vadc16 внутри v4bivme2vmeserver после делания Ctrl+C cxsd-заказчику v4bivme2vmeserver падает по SIGSEGV.
FreeDevID(1)
.
in_use
просто
ничего не делается. Так что падение -- где-то в другом месте, в
вызывальщике.
05.12.2019: разбираемся:
FreeDevID()
там дофига.
Видимо, надо ставить отладочную печать перед ними всеми?
#include "remcxsd.h"
)
макрос с именем FreeDevID()
.
__LINE__
, ...
FreeDevID()
.
Таким образом мы избегаем необходимости расставлять отладочную печать в каждой точке, поскольку каждый "вызов" автоматически дополняется печатью.
Кстати, даже если бы этой фичи не-макрорасширения-изнутри-тела-макроса и
не было, то можно б было добиться того же эффекта при помощи дополнительной
функции: сначала определяем локальную helper-функцию с другим именем,
вызывающую FreeDevID()
, а далее макрос, вызывающий эту
helper-функцию. При этом в точке определения той функции имя будет ещё не
переопределённым.
(в файле оно в 1 строку, а разбито с бэкслэшеньем тут для наглядности)#define FreeDevID(devid) \ do { \ fprintf(stderr, "%s::%d %s(): About to FreeDevID(%d)\n", \ __FILE__, __LINE__, __FUNCTION__, devid); \ FreeDevID(devid); \ } while (0)
ReturnDataSet()
!!!
TERMINATE
.
А вот кто вызвал -- как бы понять?
Не на паре тестовых zzz, не на паре adc250...
ReturnDataSet()
(куда делается goto
) --
сообщение печатается, а вот в начале -- через которое исполнение вроде бы
должно было пройти -- нет!
Как такое вообще может быть?
Окей -- пересобрал всё с -O0
. НЕ помогло.
Это даже и радостно -- значит, косяк достаточно устойчивый, меньше будет сложностей с его расследованием.
Тут помог бы gdb...
Спросить у Павленко?
Но толку от него чуть: хоть он и пишет, что
но при попытке натравить его на core-файл (для проверки был взят rrund и kill-SEGV'нут после предварительного "ulimit -c 100000") выдалThis GDB was configured as "--host=i386-redhat-linux --target=ppc-linux".
GDB can't read core files on this machine.
Ну и нафиг он такой нужен?! :-( (типа для удалённой отладки; вот спасибо!)
Так что по-прежнему надо спросить у Павленко.
dev a0 vadc16/remdrv ~ 0,5,50 b:192.168.8.209:8002 dev a2 vadc16/remdrv ~ 2,5,52 b:192.168.8.209:8002
И что -- делать вывод, что падения как-то привязаны к VME-ресурсам?!
Зато по a0._devstate=-1 -- падает, хотя у него теперь devid=2!!!
Ну да, явно VME-ресурсы в деле...
Что намекает на возможное участие ADC250, у которых как раз только эта линия.
...а у одного VADC16 горел красный светодиод.
Ну-с, найдено "наименьшее множество", и что теперь делать, как искать проблему?
Авотхрен!!! Потому что
Ну и что ТЕПЕРЬ делать? Разве что попробовать дёрнуть питание...
06.12.2019: ан нет -- опять стало падать!
Чуть позже: точнее, то падает стабильно, то стабильно НЕ падает, то один прогон заказчика-cxsd с Ctrl+C не падает, а следующий прогон (с тем же экземпляром v4bivme2vmeserver'а!) -- падает.
06.12.2019: проведёна большая серия тестов совместно с Антоном Павленко. Модуль переставлялся на "выноску", плюс Антон смотрел сигналы осциллографом. Результаты:
Так что до второго (дальнего от контроллера) модуля этот сигнал сброса попросту не доходит.
Но: если в конкретный момент конкретный VADC16 НЕ запрограммирован на измерения и генерацию прерывания по окончанию цикла, то он эту линию не трогает вовсе, и вот ТОГДА сигнал до дальнего модуля доходит.
Почему получался SIGSEGV -- т.е., почему постоянно взведённая линия IRQ5 вызывала такой эффект со сдуреванием v4bivme2vmeserver'а -- хбз. Но при заведомо некорректной работе VADC16 задача разбирательства в ситуации становится несколько сомнительной. Весь комплект "потроганных" (загрязнённых лишней отладочной печатью) файлов сохранён в w20191209-1717-vadc16-debug.tar.gz.
Так что, похоже, проблема именно в некорректно работающем мамкинском драйвере vmei, который не буферизует вектора и теряет их.
Возможный рецепт решения -- запинать-таки свой вариант, с буферизацией векторов.
StartMeasurements()
-- цикл
измерений будет запущен немедленно.
Надо бы всё-таки проверить, отличается ли в этом смысле поведение "новых" блоков от "старого".
09.02.2017: предварительные соображения:
Но это уже явно работа для макросов в devlist_magx_macros.m4; тем более, что всё равно коэффициенты проставлять.
Драйверочек iset_walker_drv.c написан, теперь проверять.
09.02.2017@вечер-дорога-домой-около-ВЦ: а насколько удобно будет такой 2-шаговый подход -- сначала указывать список, потом говорить старт?
Не будет ли удобнее как с обычной записью значений -- прямо по записи списка значений и начинать его исполнение?
10.02.2017@вечер: а ведь можно бы всё-таки этот код подселить прямо в драйверы источников.
Одна эта неприятность ставит крест на идее подселения walker'а прямо в драйверы...
13.02.2017: проверен. Косяк был -- оно пыталось писать в Iset_cur вместо Iset.
Проверялось, кстати, на "эмуляторе" -- пара noop-каналов с именами BASE.Iset и BASE.Iset_cur, и в _cur ручная запись для достижения желаемого эффекта.
И еще: в devlist_magx_macros.m4 не-драйверным cpoint-"типам-устройств" корректоров добавлены каналы Iset_cur, чтобы с ними можно было использовать этот фокус.
16.02.2017: движемся к внедрению.
MAGX_WALKER_SUBDEV
, который можно добавлять (в конец) любого из
макросов MAGX_nnn_DEV_LINE()
.
05.10.2018: в iset_walker_drv.c зашито ОЧЕНЬ много специфики наших KOZAK-driven-источников:
29.06.2021: а какого чёрта, собственно, double_iset у нас
именно DOUBLE? (Да, понятно, что это типа "максимально общее решение",
могущее работать с совсем разными источниками.) Но если
источники ОДНОТИПНЫЕ, то у них одинаковые R, и тогда можно работать прямо в
int32
-- просто разделять значение на 2 получателя.
Вопрос будет только в корректном указании этого R для "двойного" канала,
чтоб совпадало с источниковыми. Но, конечно, сейчас вряд ли надо это
делать -- уже просто незачем.
Что делать? Вводить еще какой-нить способ указать в auxinfo, что наша цель -- double-канал, и надо обращаться с ним соответственно? Это-то несложно, но...
...а как определять, что значение приблизилось к заказанному? Для double'ов-то квант -- это нечто условное. И raw воспользоваться не удастся -- у изначально-double-каналов оно тоже double.
29.06.2021: ну как-как -- auxinfo-параметром указывать, "tolerance такой-то": если текущее значение отличается от целевого менее, чем столько-то, то считать, что дошли.
Помог рестарт драйверов -- запись им ._devstate=0.
29.06.2021@пешком-от-родителей, ~16:00, уже между Пирогова 22 и 26: а сейчас до меня дошло, что надо было попробовать сделать STOP=1,START=1 -- вдруг бы помогло... Надо будет в следующий "залип" проверить.
Но, тем не менее, оно "считалось недошедшим" -- walker_state=2 (WLK_STATE_WALKING).
29.06.2021: добавляем отладочности:
Идея пришла @~14:20 около стадиона НГУ по дороге к родителям:
CX_VALUE_DISABLED_MASK
всё равно никем не используется.
ReturnCommandChannels()
дёргается только в начале и в конце --
я проверял.
Так что можно спокойно публиковать в этом канале нужные данные; а в конце оно перетрётся значением 0, но это будет уже неважно (раз дошло до окончания, но и диагностировать нечего).
Итого -- вроде бы публикуется ВСЯ возможная диагностика.
29.06.2021@вечер: (ужасная догадка): а не может ли быть race condition в cda_d_insrv.c -- что в некоторых случаях оно НЕ доставляет какое-то обновление? Ну там -- под нагрузкой, или тогда, когда значения обновляются сильно часто (предыдущее ещё не вычитано, а тут уже новое)?
30.06.2021: посмотрел на
cda_d_insrv.c::insrv_fd_p()
-- ну-у-у, в принципе,
МОЖЕТ быть получение не всех значений: там же на каждое обновление канала
шлётся его int32-hwr в pipe, и реальное cda_dat_p_update_dataset()
делается уже по вычитыванию, которое может произойти и существенно позже
(зависит от sheduling'а).
НО! Конкретно в данном-то случае ведь главное -- получить ПОСЛЕДНЕЕ
обновление, когда ЦАП пришёл в указанную точку. И вот оно вроде как
потеряться не может -- если что и будет пропущено, то промежуточные
обновления, а они неважны.
12.05.2022: событие опять повторилось. К сожалению, сервер рестартовали ДО того, как позвонить мне (да и я уже нифига не помнил). На этот раз проблема произошла с одним-единственным источником -- d5m1t4: в логах у него записано событие WALK, но отсутствует событие STOP.
17.05.2022: начал делать "скрипт", добывающий cdaclient'ом значения всех каналов, полезных для диагностики, от всех 3 источников. Скрипт TEST_ists_state.sh (забэкслэшенные переносы строк только тут, для читаемости):
#!/bin/sh for i in d3m4n5 d5m1t4 d6m1t4; do \ cdaclient -Dn -d canhw:12 \ @.i20:${i}_walker.list @.i:${i}_walker.{stop,cur_step,walker_state} @.i:$i.{Iset,Iset_cur} ;\ echo;done; \ cdaclient -Dnf8 -d canhw:12 @.i:{icd_3m4,icd_i13,icd_i14}.{out0,out_cur0,198,199}
Да только каналы 198,199 почему-то всегда отдаются как UNSUPPORTED.
После анализа ситуации выяснилось, что, похоже, тогда, 28/28-06-2021,
драйвер cdac20_drv.so
вовсе НЕ был собран с
DO_RETURN_CUR_PARAMS=1 -- даже объём бинарника
cdac20_drv.so.20210628-DO_RETURN_CUR_PARAMS до байта (но НЕ md5!)
совпадает с текущим -- 89032; в то время как "правильно" собранный -- 89424.
Видимо, то ли не тот файл стёр при пересборке (.so вместо
.o?), то ли не так указал параметр make'у (например, просто
"DO_RETURN_CUR_PARAMS=1
" вместо надлежащего
"CPPFLAGS=-DDO_RETURN_CUR_PARAMS=1
"?).
22.05.2022: сейчас пересобрано как надо, скопировано на пульт. Завтра надо во время технологической остановки рестартовать сервер и убедиться, что ничего не сломалось.
23.05.2022: сервер canhw:12 рестартован, всё работает, данные репортятся.
ЕманоФедя о скрипте проинструктирован, теперь будем ждать.
20.03.2024: за прошедшие почти 2 года так и не удалось выяснить причину проблемы.
Но недавно косяк "не репортит окончание" проявился снова. И есть подозрение, что произошло это при просадке питания -- когда целевые CDAC20 перезагрузились. А они 1) могут "встать в позу"; 2) могут перезагрузиться не все.
state_unknown_val
=WLK_STATE_UNKNOWN и
state_determine_val
=WLK_STATE_STOPPED.
SwchToUNKNOWN()
физически нету (ибо что
там делать?), так что и логгинг события вставить некуда.
SwchToSTOPPED()
добавлен вывод
значения prev_state
-- так можно будет понять, попало оно туда
штатно из WALKING или "нештатно" из UNKNOWN.
Драйвер на пульт закинут, при следующем рестарте сервера активируется, будем посмотреть.
15.04.2024: сегодня "проблема" опять возникла (спойлер: оказалась "не оно").
prev_state
работает (и
пока что везде показал prev_state=2, т.е. в STOPPED=1 приходит из
WALKING=2).
"Симптомы" "проблемы" были в том, что некие пару источников ЕманоФедин ddm гонял перемагничиваться, как будто бы "не замечая" окончания, и при каждом следующем переключении режима опять заново, БЕЗ "выстрела" (выпуска?).
Почему и казалось, что "не замечает".
Конкретно гонялись d5m1t4_walker и d6m1t4_walker, по траектории в 2 шага. И логи показывали полное её прохождение:
(логи от 2 штук перемешаны, т.к. идут в параллель).2024-04-15 13:13:46.527 canhw cxsd#12: iset_walker[124]d5m1t4_walker/DEFAULT: WALK [2]={-3692800, 3619200} 2024-04-15 13:13:46.527 canhw cxsd#12: iset_walker[126]d6m1t4_walker/DEFAULT: WALK [2]={-3674000, 3597600} 2024-04-15 13:14:11.947 canhw cxsd#12: iset_walker[124]d5m1t4_walker/DEFAULT: _sodc_cb(val=-3692799) steps[0]=-3692800 2024-04-15 13:14:12.348 canhw cxsd#12: iset_walker[126]d6m1t4_walker/DEFAULT: _sodc_cb(val=-3673999) steps[0]=-3674000 2024-04-15 13:14:37.394 canhw cxsd#12: iset_walker[124]d5m1t4_walker/DEFAULT: _sodc_cb(val=3619199) steps[1]=3619200 2024-04-15 13:14:37.394 canhw cxsd#12: iset_walker[124]d5m1t4_walker/DEFAULT: STOP, prev_state=2 2024-04-15 13:14:38.694 canhw cxsd#12: iset_walker[126]d6m1t4_walker/DEFAULT: _sodc_cb(val=3597599) steps[1]=3597600 2024-04-15 13:14:38.694 canhw cxsd#12: iset_walker[126]d6m1t4_walker/DEFAULT: STOP, prev_state=2
И после исправления той блокировки -- оно всё заработало!
-- т.е., только отработка, БЕЗ инициирования "WALK [2]=...".2024-04-15 14:13:26.588 canhw cxsd#12: iset_walker[88]d3m4n5_walker/DEFAULT: _sodc_cb(val=2273598) steps[0]=2273600 2024-04-15 14:13:33.200 canhw cxsd#12: iset_walker[88]d3m4n5_walker/DEFAULT: _sodc_cb(val=0) steps[1]=0 2024-04-15 14:13:33.200 canhw cxsd#12: iset_walker[88]d3m4n5_walker/DEFAULT: STOP, prev_state=2
А инициирование нашлось в логах сильно раньше --
-- т.е., хождение было запущено (видимо, уже ПОСЛЕ появления блокировки, запретившей работу источника), но так и осталось в "ждущем" состоянии: уставка в2024-04-15 13:10:36.212 canhw cxsd#12: iset_walker[88]d3m4n5_walker/DEFAULT: WALK [2]={2273600, 0}
d3m4n5.Iset
была отправлена, но из-за
заблокированности источника она не отрабатывалась (в
ist_cdac20_drv.c::ist_cdac20_rw_p()
стоит проверка,
что текущее состояние -- одно из группы "UP").
Итого: неясно, не было ли именно ЭТО "где-то незамеченная блокировка" причиной "странного поведения iset_walker'а и неработы ddm", или всё же есть какая-то ещё проблема.
Чуть позже: а вот нет -- в начальной записи о проблеме за 24-06-2021 сказано "иногда walker'ы НЕ считают последовательность завершённой, хотя значение целевого канала дошло уже до значения в последнем шаге"; так что реальная проблема тоже есть.
16.04.2024@утро: ещё вариант,
как можно попробовать "подтолкнуть" безо всяких STOP=1,START=1: просто РУКАМИ записать конечное значение в источников
Iset
-- тогда оно должно дойти до walker'а, так что возможность
"последнее значение Iset_cur просто не дошло до walker'а" точно будет
исключена.
30.05.2024: опять просадка и опять проблемы.
03.06.2024: провели тест посредством быстрого дёрганья рубильника питания на морде корзины "ИСТ-14" (это как раз 6m1t4). Фиг -- устройство честно присылает пакет 0xFF,"PowerOn". Похоже, ёмкости платочки "питание собственных нужд" всё-таки не хватает.
А дёргать рубильник, через который запитан этот ИСТ, при подеваемом от энергоцентра питании -- запрещено, т.к. в этом рубильнике может вспыхнуть дуга.
Но всё же остаётся вопрос: ЧТО отключает рубильник на морде корзины -- ВЫХОД из корзинного БП (тогда эксперимент нерелевантен, т.к. ёмкость может быть именно в этом БП) или ВХОД (тогда эксперимент показателен и фиг знает, что же произошло 30-05-2024)?
Чернякина спросить не так просто -- он во временом увольнении ради индексации пенсии.
04.06.2024: продолжаем расспросы.
Вывод: надо бы сравнить, на каких фазах сидят какие ИСТы (в частности, срубившиеся 33 и 26 eSol и 1M:1-4).
Вело оно себя так прямо на уровне vdev_drv -- запрещался переход в
состояние ON_START, т.к. IsAlwdSW_ON_START()
постоянно
возвращала 2 ("DISABLED"). Так что vdev так и оставался в состоянии 3
(OFF).
27.11.2017: расследование показало, что причина в некорректном условии: в середине ноября (13-11-2017) по просьбе Куркина было добавлено (в дополнение к "IS_READY!=0") еще и ограничение, что напряжение (Uset) не менее, чем 3kV.
Но добавлено оно было неправильно: стояло
(т.е., проверка, что ТЕКУЩЕЕ уставленное в ЦАП) вместоme->cur[SODC_USET ].v.i32 <= -1768421
(т.е., "заданное юзером").me->uset_val <= -1768421
После исправления вроде запахало.
P.S. Число со знаком минус -- из-за того, что у Куркина значения отрицательные (по его схемотехническим соображениям).
К сожалению, деталей ситуации не запомнилось, и скриншота, увы, тоже нет. Эта запись делается для обозначения проблемы; когда удастся получить детали -- будем разбираться.
28.10.2022: опять произошла аналогичная ситуация: позвонил Лебедев, что не отрабатывается уставка.
А от компьютера -- фиг.
31.10.2022: "неидеальность" заключалась в том, что пока на компе не был загружен какой-то режим, в ручном ("BLK") оно тоже отказывалось включаться. Позвали Куркина, и тот разъяснил, что разрешения от CAN-блока (от компьютера) и от панели включены ПОСЛЕДОВАТЕЛЬНО, поэтому пока от компа не пройдёт "инициализация" с разрешением, с морды оно тоже не включится (я не до конца всё понял -- что-то говорилось про "кнопку, которую надо нажать и потом отжать").
31.10.2022: не факт, что болотное: возможно, это
"бледножёлтый" BG_NORM_YELLOW
был так воспринят ("как будто
болотное, но не совсем").
...на будущее -- запускать командой
ringrf is_freezed with_freeze_btn with_once_btn
и щёлкать "Once".
REZ_STATE_IS_OFF
, а switch_on=2, т.е., DISABLED (запрет).
rezonator._devstate=0
, после чего стало vdev_state=11, что есть
REZ_STATE_IS_ON
.
30.10.2022: т.е., весьма вероятно, хватило бы и просто
записи rezonator.reset_state=1
.
30.10.2022: то был КРАЙНЕ странный результат: судя по
SwchToDETERMINE()
, выбор между IS_OFF и IS_ON делается
исключительно по значению бита SODC_IS_ON
. Т.е., получается,
что как бы "не совсем включенный" контроллер отдавал бит ON? Опять какие-то
косяки "ручного" режима (его некорректной реализации и не-отвязанности от
компиютерного)?
state_related_channels[]
канал привязан к переходу в состояние
REZ_STATE_SW_ON_START
; а судя по коду
IsAlwdSW_ON_START()
, разрешение возвращается при горящем бите
READY плюс обязательном
me->uset_val <= USET_MIN
(с учётом обратной полярности эта запись означает "уставка не ниже 3kV").
Какое из условий тогда не сработало -- теперь фиг поймёшь; но, возможно, именно значение Uset.
29.10.2022: по результатам вчерашнего разбирательства
возникло стойкое желание сделать так, чтобы драйвер вместе со значением USET
возвращал бы также флаг CXCF_FLAG_COLOR_YELLOW
в случае того
"Uset<3kV".
Каковое делание хотелось исполнить путём введения некоей функции, которая бы принимала (me,uset_val) и возвращала бы флаг при значении ниже минимума. И эту функцию вызывать для значения rflags во всех возвратах USET.
Однако оказалось, что в скрине ringrf.subsys такое сравнение УЖЕ выполняется, с тем же результатом -- пожелтением. Так что желание отменено.
...вот только неясно -- было ли поле уставки жёлтым у Лебедева? Он же такого не упоминал...
31.10.2022: да, было -- на прямой вопрос ответил подтвердительно. И просто впрямую проверено на пульту -- при уменьшении уставки ниже 3kV поле пожелтевает.
Оба они -- каналы чтения, но если значение USET_MIN возвращается
(прямо из init_d()
, что не есть гуд -- при дрыгании
через NOTREADY канал поболотовеет), то IEXC_MIN и не отдаётся никак
никогда. Вопрос: а зачем они делались?
29.10.2022: зачем USET_MIN -- стало ясно: он используется в ringrf.subsys для подсвечивания уставки USET: там в colformula стоит сравнение, что если Uset<Uset_min, то отдаётся значение за пределами normrange, так что поле уставки должно желтеть.
IEXC_MIN же не используется вообще никак и нигде -- даже в самом
драйвере, есть лишь определение KURREZ_CAC208_CHAN_IEXC_MIN
, а
сравнения с ним нигде не делается (видимо, Кондаков и компания не выдали
этого как обязательного условия при включении).
SetErr()
.
Канал-то строковый, а в devtype это отражено не было.
Он реально нигде не используется, но в _init_d() есть начальная уставка в "".
08.05.2018: индивидуально этот канал переделан на "r1t100" -- одновременно с переделкой 6 штук в "d".
А "в продакшен" -- на cxhw.ic.local -- вариант с каналом errdescr еще не попал, там была версия от 11-12-2017 (поэтому никаких следов этой ошибки в логах не было).
05.10.2021: в SetErr()
замечена позорная
ошибка: там отсутствовало присвоение значения переменной vp
--
указателю на адрес буфера (найдено при реализации похожего строкового канала
STOP_REASON в lebedev_turnmag_drv.c). Так что работать оно НЕ МОГЛО.
Спасло от ошибки или и вовсе SIGSEGV то, что реально оно используется
ровно 1 раз -- в инициализации, со строкой "": это даёт длину
0
, при которой обращения к исходному буферу не производится
вовсе.
Исправлено.
06.10.2021: вдогонку:
Обломиссимо: единственным найденным местом оказалась реакция на
изчезновение (обнуление) IS_ON в _sodc_cb()
. И более нигде
ничего.
И их желательно бы отдавать прямо из драйвера, а не считать в скринах -- чтоб прочие программы (включая всякие журналяторы) могли б пользоваться готовыми данными.
08.05.2018: учитывая, что исходные величины там --
микровольты, с коэффициентами 1000000 для перевода в вольты, остаться в
рамках int32
не удастся, надо сразу переводить в
float64
.
Свободное место в карте каналов еще есть -- 6 штук в диапазоне [92,97]. Вот их и заюзаем: 4 штуки сейчас, а еще 2 про запас оставим.
kurrez_cac208_sodc_cb()
добавлены вычисления и возврат
(вот нету "ReturnFloat64Datum()
" -- приходится извращаться...).
Плюс ширина полей увеличена до %7 (характерная величина -- 1500W) у всех строк.
И эта "недрайверность" -- существенное отличие от прочих "устройств управления источниками".
28.03.2018: сделан "скелет", уже собирающийся.
Этот драйвер -- на сегодняшний день чемпион: он имеет в подложке аж 6 устройств (раньше максимум был у vepp4_gimn_drv.c -- 3 штуки).
18.09.2018: за последние полмесяца-месяц драйвер допилен до рабочего состояния и вполне успешно контролирует клистроны.
Некоторые замечания/highlights:
ГВИ же и ЦАП -- просто tube'атся, но при этом пока эти устройства не станут "готовы", vdev-драйвер будет неработоспособен.
Выбран подход, чуток похожий на источники: есть "уставка" высокого, которая просто помнится драйвером, а при переходе в состояние "включено" она реально отправляется в ПКС. При переходе в состояние "выключено" либо "блокировка" -- в ПКС отправляется уставка-минус-10000.
SwchToDETERMINE()
определяет,
по rflags.
Но и показывать юзеру просто HWERR/NO_Q -- некузяво.
Поэтому заведено поле out_val_rflags
, куда при нечитанной
уставке записывается CXCF_FLAG_COLOR_WEIRD
-- и юзер это видит
как поголубевшее поле (да, флаги передаются с самого низа до клиента БЕЗ
срезания, имевшегося изначально в v2).
24.10.2018: надо вводить поддержку "номинального" значения: такого, выше которого вылазить нельзя.
Кстати, то, что изначально стартуется со значения 0
-- не
очень хорошо: тратятся лишние десятки секунд. Официально полностью
безопасным считается значение 20000. А как бы его ставить "скачком"?
Поддерживаются ли драйвером g0603 каналы "IMM"?
25.10.2018: как показывает анализ -- да, каналы "IMM" поддерживаются. Правда, как их тут
использовать, всё равно не вполне ясно: проверять в
SwchToSW_ON_UP()
, что если CUR меньше 20000 (а уставка --
>20000), то перед записью в PKS_SET также попросить записать 20000 в
PKS_IMM?
25.10.2018: ЕманоФедя прислал эти номиналы для всей четвёрки: 1:45500, 2:43500, 3:44500, 4:45900.
29.10.2018: итого -- сделано, и поддержка номиналов, и вспрыг с 0 сразу дл 20000:
PKS_SET_IMM
, ссылающийся
на pks.code_imm0.
SwchToSW_ON_UP()
добавлены "мозги":
safe_val
, как
"out_val-DROP_DELTA", но не выше 20000.
PKS_SET_CUR
меньше, чем
safe_val
, то в PKS_SET_IMM
отправляется
safe_val
-- это делается ПЕРЕД отправкой значения в
PKS_SET
.
out_val_nominal
.
text2cfg[]
, покамест содержащая
только параметр nominal.
BASE [PARAMETERS]вместо извратного ist_cdac20'ского
[(PARAMETERS)]BASE
_init_d()
делается сначала ручное вычленение ссылки
на базу и помещение её в base[]
, а затем оставшееся вручную
скармливается psp_parse()
.
v3h_a40d16_init_d()
(куда потом, украсивленное, с-портировано обратно), только поиск сепаратора
другой -- '\0' или isspace()
вместо '/'.
ist_cdac20_init_d()
.
_CHAN_HVSET_NOMINAL
можно менять на ходу, в
пределах [MIN_ALWD_VAL,MAX_ALWD_VAL].
_CHAN_HVSET
.
В такой парадигме проверено -- работает.
Но неясным остаётся вопрос об отношениях указанного номинала с ныне-неработающим вычитыванием текущей уставки: не делать ли в качестве "вычитывания" просто out_val=out_val_nominal?
23.09.2019: поступила просьба, чтобы при вводе юзером уставки HVSET, более маленькой, чем текущее значение, она бы прописывалась в железо даже в режиме "выкл" (и INTERLOCK тоже -- т.е., во всех, а не только в IS_ON и SW_ON_UP, как сейчас).
Смысл такой:
DROP_DELTA
), до 35000.
Там нужно, условно, 25000.
24.09.2019: вот тут странно: перепроверил я код -- всё "включение" сводится к записи значения уставки в PKS_SET; никакого же "бита включения" там нет. Ну и как оно может при этом "сначала забрасывать до 35000"?
24.09.2019: спросил у Валеры Мусливца -- да, всё верно, но "есть нюанс": уставка действительно "просто стоит на 35000", но модулятор стоит в состоянии "не-работа" из-за наличия блокировки; если же нажать [Reset] в программе или аналогичную кнопку на стойке, то блокировки сбрасываются и НАЧИНАЕТ РАБОТАТЬ -- как будто бы сделано "включение" (да, дурной дизайн).
24.09.2019: посмотрел код драйвера -- в
v5k5045_rw_p()
достаточно в ветке
V5K5045_CHAN_HVSET
к условию "можно ли слать значение прямо
щас?" добавить доп.условие вида
val < me->cur[PKS_SET_CUR].v.i32
И можно даже не добавлять предварительную &&защиту "cur[PKS_SET_CUR].rcvd", поскольку изначально там будет значение 0, которое точне не пройдёт в основном условии выше.
После обеда: сделал, скопировал на пульт -- будем смотреть.
14.10.2019: наконец-то проверено (дошли руки рестартануть cxhw:25). Да, вроде работает.
06.12.2019: добавлена отдача диапазонов -- ключевое
слово ReturnHVSetRange()
, подробности "Диапазоны ограничения
записи..." за сегодня.
03.09.2021: реализовано так:
ignore_ilks
,
v5k5045_sodc_cb()
загорание блокировок
НЕ приводит к переходу в состояние INTERLOCK.
28.10.2021: "неожиданный" побочный эффект (озадаченное письмо от ЕманоФеди):
Сейчас 3й модулятор исползуется с отключенным переходом в состояние блокированности, и такое чувство, что побочный эффект от этого - блокировки не сбравываются, т.е. напротив кнопки их сброса, есть две лампочки, видимо отображающие каналы ура, которые при нажатии на кнопку не реагируют. Собственно хочется выяснить, это в самом деле связано с тем что сброс напряжения отключен или есть какая-то аппаратная проблема? А потому уже и решим что тут делать.
По коду -- да, ровно так и есть: IsAlwdRST_ILK_SET()
позволяет переход (т.е., по факту -- отработку канала V5K5045_CHAN_RST_ILKS)
лишь из состояния INTERLOCK (ну и плюс из "промежуточных" -- собственно
процесса сброса блокировок. А при игнорировании блокировок и перехода в
INTERLOCK не происходит, так что и сбросить никак не удастся.
28.10.2021: много думал, что же тут можно сделать.
SwchToDETERMINE()
в отдельную функцию.
28.10.2021: судя по письму ЕманоФеди от сегодня в 16:25, всё это нафиг не нужно, т.к. «Выдумывать же программное лечение проблем в силовой исполнительной электронике мне представляется глупость, поэтому я формулировать "чего хочется" для тебя на такой случай не буду.».
25.04.2022: всё-таки понадобилось -- проблемы с 3-м
модулятором продолжаются. Посему учёт ignore_ilks
был добавлен
также и в IsAlwdRST_ILK_SET()
с IsAlwdSW_ON_UP()
.
В результате при рестарте сервера все драйверы клистронов не запустились, жалуясь на ненайденные каналы --
cda_new_chan([4]="gvi.quant0"): unknown channel
Поэтому сейчас все ссылки на них заключены в #if USE_GVI
, а
оный символ уставлен в 0
.
Но СВОИ каналы оставлены -- на них даже R'ы ставятся.
И даже сами наборы каналов различаются; и ладно б просто различаются (можно б было имена-alias'ы сделать) -- но некоторые (блокировки по фазе и температуре) просто на других местах.
MAGX_IST_CDAC20_LINE()
, где
есть параметр 9, означающий "веремеенковость", когда просто метки ставятся
иные.
Что делать?
28.06.2018: так и делаем.
В нём имена каналов отличаются как положено.
MAGX_VNN_CEAC51_DEV()
-- в нём и стоит "/ist_cdac20".
И, как это ни глупо, у него тоже есть 4-й параметр -- "negated_dcct2". Потому, что у БОЛЬШИНСТВА веремеенковских измерение по этому каналу с обратной полярностью, но вот конкретно у источников spectr1 и spectr2 -- с прямой. :-(
MAGX_IST_CDAC20_LINE()
в ветки "веремеенковостей" внесены новые
имена.
29.06.2018: задеплоил на пульту -- работает.
SwchToDETERMINE()
значение канала ISET_CUR возвращается просто
равным значению me->cur[C20C_OUT_CUR].v.i32
, БЕЗ учёта
знака, который у reversable-источников может быть и -1.
В результате в момент
старта может быть парадоксальная ситуация: значения каналов
Iset
и Iset_cur
равны по модулю, но отличаются
знаком (ISET может быть отрицательным, а ISET_CUR ВСЕГДА будет
положительным).
И да -- это было проверено, что оно реально так: по
._devstate=0
(рестарт) при отрицательной уставке тока
действительно отдались значения с разным знаком...
Чуть позже -- да нет, нифига НЕ "в момент старта"!!! А просто ВСЕГДА в
момент входа в SwchToDETERMINE()
-- т.е., как при старте
сервера (или рестарте устройства), так и при "оживлении" целевого CDAC20
(например, после броска питания или busoff'а): ведь значение Iset останется
отрицательным, а ISET_CUR в будет в момент DETERMINE вёрнуто положительным.
cur_sign
, который как раз к этому
месту уже определён.
Так и сделано. Теперь надо будет проверить.
1. Исправить. 2. Записать про iset_walker (глюки: иногда на реверсивных ИСТах не замечает завершения последнего шага). 3. Записать про поведение a3818+adc250*2 при repcount<1. ...кстати, дескриптор там дорос-таки до 1023...
_init_d()
был унифицирован с v5k5045'шным (который, в свою
очередь, наполовину основан на тутошнем): вместо hiername[]
и
len
используются переменные base[]
и
base_len
, плюс секционирование и комментарии к секциям.
04.10.2018: процесс:
convertVoltToPhase()
и convertPhaseToVolt()
плюс толпа [4]-массивов констант (там
коэффициенты в формулах разные в зависимости от фазовращателя).
Итого -- сделано, надо проверять.
05.10.2018@утро-душ: в коде есть "ручное" преобразование между вольтами и микровольтами, с домножением/делением на 1000000.0.
CDA_DATAREF_OPT_NO_RD_CONV
всем каналам.
...чуть позже: да, флажок такой сделан, надо его заиспользовывать.
07.10.2018: (утро) первая проверка драйвера -- вроде работает, но числа почему-то в последнем знаке отличаются от гусиных (проверено поочерёдным запуском того и этого, с неизменным значением в ЦАПе).
07.10.2018: (вечер, ПОСЛЕ пляжа) заиспользовал
VDEV_DO_RDCONV
, избавился от всех умножений/делений на
1000000.0 -- вроде всё работает.
13.10.2018: к вопросу об отличающихся в последнем знаке числах: было подозрение, что, возможно, используемый сейчас гусиный код имеет иные наборы коэффициентов, а мне в лапы попал подустаревший исходник epics/phr/src/main_cb.c.
Так вот -- нифига, те коэффициенты идентичны приведённым в сухановском описании. Так что:
По результатам можно уже предметно общаться с Федей.
18.10.2018: еще одна проверка совпадений/несовпадений.
Так вот -- разница есть, даже в нулях:
Федя выдал гипотезу, что дело может быть в разных значениях PI: у меня
оно берётся за M_PI
, а у Гусинского используется
3.1415926
(7 знаков после точки вместо 29 в стандартном
3.14159265358979323846264338327).
Окей, проверил, использовав в драйвере гусиный укороченный вариант -- фиг, вообще никакой разницы.
Чуть позже:
Для архива: в CX эти значения читаются из ЦАПа как {1.332240,0.338550,0.333975,0.337330} -- т.е., ровно то, что читается при записи из EPICS нулевых фаз.
Засим можно считать загадку решённой, а вопрос закрытым.
Физически по 2 канала в блоке, и используются 2 блока (чтобы 2*2=4, для 4 клистронов). Однако есть 3 блока -- еще +1 резервный.
Но Фролов намерял какие-то странности в 1-м, и поставил вместо него 3-й (мне сказав сильно задним числом).
Сейчас-то я просто руками подшаманил драйвер, вписав в первые 2 колонки коэффициенты от 3-го блока (да, в бумажке от Суханова они есть).
Но если в будущем такое жонглирование будет хоть когда-нибудь случаться, то такой сценарий не катит. Нужен способ менять настройки (которые теперь видятся именно настройками, а не фиксированными параметрами) более оперативно.
31.10.2018: какие видятся варианты -- а разные, причём разного уровня конфигурябельности:
...хотя это можно и для нынешнего многоканального так сделать.
...и это тоже можно и в многоканальном сделать.
Короче: сделать -- МОЖНО, кучей разных способов. Вопрос в ПОТРЕБНОСТИ, которая пока неясна/неопределённа.
Когда появится ясность -- сделаем.
Смысл: чтобы можно было видео от TV-камеры, подоткнутой к v5p2 через frame-grabber, смотреть дистанционно -- например, в пультовой ВЭПП-4 (Беркаеву оно нужно для настройки).
14.03.2020: некоторые базовые идеи:
Причина -- openCV основан на Gtk, а для него у нас биндинг cxscheduler'а отсутствует.
Название выбираем "img878" -- в честь чипа BT878.
15.03.2020: вчера сделана инфраструктура -- img878.devtype, img878_drv_i.h, img878_data.c и надлежащие заклинания в pzframes/.
В качестве сервера выбран cxhw:26, специально для этого созданный.
22.03.2020: в основном сделано, всё по проекту.
Основная маета была с добычей исходных данных от openCV.
А вот CX'ная часть работает просто замечательно. Причём и пропускной способности с запасом (на симулационных тестах получались какие-то дикие FPS'ы -- далеко за сотню), и загрузка процессора от tvcapture+pipe2cda*4 совсем невелика.
08.05.2020: немножко по реализации самой добычи исходных данных в tvcapture.cpp (записываю задним числом, по анализу diff'ов разных версий исходников (забэкапленных в notes/) и текста писем Беркаеву):
Получилось не очень красиво, т.к. "отсутствующий сигнал" показывается синим цветом, в результате чего на картинке яркие светлые полосы.
Поэтому делаем "правильное" преобразование RGB->grayscale: R*0.3+G*0.6+B*0.1.
Но при сравнении картинки в скрине img878 с исходным изображением вживую наблюдаем странные отличия: какие-то артефакты.
Сейчас переделал преобразование на явное использование беззнаковых значений, и картинка через CX практически идентична исходной (с точностью до того, что исходная не совсем монохромна).
sl_main_loop()
, и чтоб там вызов
главного select()
окружался бы mutex'ом?
Эту библиотечку можно б было применять для простой интеграции доступа к CX в сторонние программы, в т.ч. на Gtk.
"Окружание" mutex'ом делается очень просто -- с помощью
sl_set_select_behaviour()
, позволяющего установить действия
before_select
и after_select()
.
17.04.2020: главный вопрос: а достаточно ли будет такого mutex'а для правильного "разграничения"?
08.05.2020: смотрю сейчас на эти записи, и не догоняю: а ВТОРОЙ-то юзер mutex'а кто?
Ну будет select() окружён защитой, а кто и где будет этот mutex проверять, чтобы ничего не делать?
...напрашивается вроде "все остальные действия, вроде отправки данных", но это не вполне ясно.
Блин, вот надо было ТОГДА эту идею детально записывать, СРАЗУ в конце марта/начале апреля, когда она ярко светилась в голове!
26.05.2020@пробежка по квартире,
~16:00, в райное 8-й тысячи шагов: видимо, тогда идея заключалась в
том, что в момент, когда управление находится в cxscheduler'овом
select()
'е, более НИЧЕГО со структурами
cda/cxlib/fdiolib/cxscheduler НЕ делается, и в этот момент можно какие-то
cda-вызовы делать.
Т.е.,
А на время select()'а -- ОПУСКАТЬСЯ, разрешая их.
И, соответственно, взводиться обратно он будет тоже "условно", по окончанию любых иных действий.
...и вот тут немного неясно, что делать с cda-evproc'ами: ведь они-то будут вызываться как раз когда "ВЗВЕДЁН"; получается, что из них ничего нельзя будет делать, т.к. из-за того, что это тот же самый thread, попытка вызвать любую операцию с защитой приведёт к deadlock'у.
Чуть позже: да нифига -- раз это тот же самый thread, то из него как раз МОЖНО ВСЁ!
А защищать надо обращения только из ДРУГИХ thread'ов.
Пара замечаний:
Ведь дизайнилось-то всё в расчёте на однопоточность, где (не)явно действует предположение/гарантия, что подобного "выдёргивания из рук" не произойдёт.
Что делать?
sl_del_fd()
, то этот флаг взводится и как-то там обрабатывается
(в блоке вызова callback'ов по взведённым битам готовности?).
Сейчас (~20:50), когда записываю те мысли, эта идея представляется
странной и непонятной: что за флаг, как именно sl_main_loop()
должен реагировать на его поднятость?
Тогда уж скорее не "флаг", а лишний fd_set
, по умолчанию
пустой, но чтобы sl_del_fd()
взводил бы в нём битик -- чтоб
callback'и НЕ вызывались бы для таких "покойных" дескрипторов. (А перед
select()'ом чтоб он нулился бы.)
select()
'а --
возвращая ошибку.
Это, кажется, у меня такой опыт был -- уж не припомню, при каких условиях.
А в man-странице по select есть такой раздел:
Multithreaded applicationsIf a file descriptor being monitored by select() is closed in another thread, the result is unspecified. On some UNIX systems, select() unblocks and returns, with an indication that the file descriptor is ready (a subsequent I/O operation will likely fail with an error, unless another the file descriptor reopened between the time select() returned and the I/O operations was performed). On Linux (and some other systems), closing the file descriptor in another thread has no effect on select(). In summary, any application that relies on a particular behavior in this scenario must be considered buggy.
Плюс, есть обратная проблема: если во время нахождения в select()'е какой-то дескриптор ДОБАВЛЯЕТСЯ в список слушаемых?
В любом случае, оно так и останется в подвисшем и НЕотслеживаемом
состоянии на время до 10 секунд -- DEFAULT_IDLE_USECS
.
Вот что бы нас СПАСЛО -- так это возможность принудительного "завершения" select()'а сразу после таких действий, меняющих набор дескрипторов. Но КАК?
А если завести pipe, чей читающий конец перманентно помещать в список дескрипторов, и при модификации списка дескрипторов отправлять в пишущий конец 1 байт?
...узнать бы, не придумал ли уже кто нибудь её.
"Writing Threaded Applications" в "Motif 2.1 Programmer's Guide".
23.06.2020: наконец сподобился прочитать -- нету там ничего интересного: в основном стандартные общие слова про то, как обычно положено выполнять локинг.
29.05.2020: а ведь может возникнуть deadlock: если cxscheduler'ом вызывается какой-то callback (который через fdiolib и затем cxlib/cda приведёт к вызову юзерского), то это произойдёт уже ВНЕ select'а, и при ЗАКРЫТОМ mutex'е. И когда клиентский код попробует перед вызовам cda захватить блокировку, то он просто зависнет навсегда -- поскольку это тот же самый thread, и до открытия mutex'а дело никогда не дойдёт.
Теоретически, надо бы установить правило "все, кто работает в том же thread'е, блокировку НЕ делают, а кто в других - ДЕЛАЮТ". Но по факту это может оказаться дюже сложно: код-то юзерский общий, и весьма нетривиально разбираться, какой кусок когда будет вызван одним thread'ом, а когда другим.
@вечер, после "прогулки" на
балконе: а можно сделать mt_sl_lock()/mt_sl_unlock() условными --
чтобы они ничего не делали, если
"getthreadid()==mt_sl_threadid
". Тогда всё, работающее в том
же thread'е, что и cxscheduler, автоматически будет иметь полный доступ без
блокировок и deadlock не возникнет.
Чуть позже: учитывая конкретную семантику стандарта pthreads, сравнение
будет выглядеть слегка иначе: pthread_equal(pthread_self(),
mt_sl_threadid)
.
Ещё чуть позже: гугление показывает, что, возможно, такая проверка есть уже в самой реализации mutex'ов. Но не гарантированно.
30.05.2020: а не добавить ли тогда уж за компанию в cxscheduler и вызов локинга в sl_add_fd()/sl_del_fd()/sl_set_fd_mask() плюс в таймауты?
Только тут локинг должен быть БЕЗусловным. (Поскольку тут, как и в
sl_main_loop()'е, надо защищаться от других thread'ов изолировать
критические секции, чтобы ...
31.05.2020: и тут тоже надо обдумать - не возникнет ли всё-таки какой-нибудь deadlock или что-то подобное.
01.06.2020: приступаем. Некоторые базовые принципы:
CXSCHEDULER_HOOK_nnn
, которые должны
быть #define
-символами, и в нужных местах делаются вызовы вроде
#ifdef CXSCHEDULER_HOOK_BEFORE_SELECT CXSCHEDULER_HOOK_BEFORE_SELECT(); #endif
Список оных hook'ов включает в себя:
CXSCHEDULER_HOOK_MAIN_LOOP
CXSCHEDULER_HOOK_BEFORE_SELECT
CXSCHEDULER_HOOK_AFTER_SELECT
CXSCHEDULER_HOOK_FDSET_CHANGE
Собственно реализация:
mt_sl_lock()
и mt_sl_unlock()
.
do_lock()
и do_unlock()
.
mt_sl_lock()
и mt_sl_unlock()
вызывают их УСЛОВНО, только если текущий threadid отличается от
cxscheduler'ова.
hook_before_select()
(разлочить) и
hook_after_select()
(залочить) -- безусловно ВСЕГДА.
hook_fdset_change()
пишет 1 байт в
mt_sl_wake_pipe[PIPE_WR_SIDE]
,
pipe_proc()
читает из
mt_sl_wake_pipe[PIPE_RD_SIDE]
пачками по 16 байт до 30 раз.
CXSCHEDULER_HOOK_FDSET_CHANGE
делается не только при
добавлении/удалении/изменении маски дескриптора, но и в
sl_enq_tout_at()
-- чтобы в случае добавления нового, более
близкого таймаута, он бы отработался, а не ждалось бы сначала до истечения
предыдущего-ближайшего.
А вот при УДАЛЕНИИ таймаута -- нет, НЕ делается: ну подумаешь, прервётся
select()
чуть раньше, чем теперь должен бы, ну и фиг с ним.
29.11.2024@утро, после мытья
посуды: по-хорошему при ДОБАВЛЕНИИ таймаута, могущего быть ранее, чем
текущий ближайший, надо не безусловно прерывать select()
, а
лишь если новый таймаут ДЕЙСТВИТЕЛЬНО ранее, чем текущий ближайший. Сделать
это можно, несложно, прямо в sl_enq_tout_at()
; но надо ли?
Так-то пофиг на небольшую неоптимальность...
hook_main_loop()
:
mt_sl_threadid = pthread_self();
mt_sl_mutex
и тут же лочит его.
mt_sl_wake_pipe[]
, тут же переводя его в
O_NONBLOCK, и регистрирует читающий конец.
Оная инициализация делается условно и одноразово, закрытая флагом
mt_sl_initialized
, так что sl_main_loop()
может
вызываться многократно.
...но в КРОСС-средах -- нет.
11.06.2020: и в src/TopRules.mk добавлено
определение LIBMT_CXSCHEDULER
.
Похоже, пора уже изготавливать вариант tvcapture на этом механизме -- для проверки.
01.06.2020@вечер: а ещё - надо ж выполнять некую процедуру "запуск cxscheduler'а в отдельный thread", и это ТОЖЕ должно быть в mt_cxscheduler.
И операция эта должна быть СИНХРОННОЙ -- чтобы пока в порождённом thread'е не произойдёт вся инициализация (mutex'ы, pipe-дескрипторы, ...), в родительском бы ничего не делалось.
(Примерно так делалось в старой 3-процессной архитектуре cxd/cx-server/cx-porter (только там были двунаправленные сокеты, но это непринципиально).)
mt_sl_wake_pipe[]
? Просто чтобы не плодить лишних
дескрипторов, плюс, так эта пара останется с самыми низкими номерами (а
иначе "синхронизационную" пару, созданную раньше, пришлось бы закрывать, тем
самым освобождая для дальнейшего более низкие номера, что неприятно).
Но такой вариант потребует некоторого перетряхивания сделанного кода -- в
частности, чтобы создание дескрипторов выполнялось бы не в hook_main_loop(),
а сильно раньше -- причём, ещё в РОДИТЕЛЬСКОМ thread'е.
02.06.2020: а может, вообще выкинуть "концепцию CXSCHEDULER_HOOK_MAIN_LOOP"? Пусть эта инициализация выполняется явно в некоей функции "создать thread и запустить в нём cxscheduler". В любом случае -- да, надо уже пытаться изготавливать конкретное внедрение и на нём проверять.
11.06.2020: да, приподпеределываем -- заменяя
"косвенный" hook_main_loop()
на явный
mt_sl_start()
.
mt_sl_start()
, должная выполнять
инициализацию, включая создание нового thread'а и запуск в нём
sl_main_loop()
.
Инициализация скопирована из hook_main_loop()
.
mt_sl_thread_proc
уже не требуется -- это автоматически делается вызовом
pthread_create()
.
mt_sl_start()
, прямо в изначальном thread'е.
А в качестве "средства синхронизации" будет работать прямо сам mutex, который уже сразу залочен.
Поэтому:
mt_sl_thread_proc()
состоит лишь из вызова
sl_main_loop()
в бесконечном цикле.
Вот теперь -- да, всё, пора уже делать реальное применение, в tvcapture.
12.06.2020@утро: вот только с работой mutex'а в качестве средства первоначальной синхронизации могут быть проблемы: если реализация mutex'ов предусматриваеть условность -- что заблокированный тем же thread'ом НЕ считается заблокированным -- то получится, что владельцем является как раз стартовый thread, а не "cxscheduler'ный". И тогда:
Так что, возможно, всё же надо вернуться к идее "запускаем всё с синхронизацией через pipe". Благо, способ реализации ясен.
12.06.2020: да, переделываем.
mt_sl_thread_proc()
сначала лочит mutex и
отправляет 1 байт в pipe.
mt_sl_start()
же теперь mutex только создаёт, но НЕ
лочит.
Код ожидания подсмотрен в check_fd_state()
-- также
игнорирует прерывания; но вместо таймаута указывает NULL -- "бесконечность".
13.06.2020: и адаптируем tvcapture.c -- тут всё просто.
USE_MT_CXSCHEDULER
.
CreatePipes()
, целиком в
#if'е.
Она делает mt_sl_start()
, а потом в "скобках" lock/unlock
создаются контекст и 4 канала.
SendToPipe()
просто кусочек отсылки данных
("вычислительная" же часть общая).
Сделать-то сделал, да только оно не компилируется: конфликт в
определениях типов int64
и uint64
, которые в
CV'шных .h-файлах также присутствуют.
Очевидно, нужно утаскивать непосредственное взаимодействие с cda в отдельный файл, чтобы он #include'ил CX'ные header'ы, но НЕ openCV'шные.
15.06.2020: делаем.
cda_adapter_init()
.
cda_adapter_send_one()
.
Ну сделано, добилося успешной линковки. Запускаем на v5p2 -- и видим пустые "синие" картинки и чёрные в v5p2camera. ...где бы добыть реально обновляющиеся данные...
Чуть позже: в порядке отладочного инфо-собирательства заменил в
do_lock()
'е вызов pthread_mutex_lock()
на
pthread_mutex_trylock()
с проверкой, что если НЕуспех -- то
вызываем обычный pthread_mutex_lock() и печатаем сообщение на stderr.
Просто чтобы видеть, что происходит. Результат -- О-О-ОЧЕНЬ редкие
сообщения. 02.04.2022: редкие, ага-ага!!! На v5p2 с
некоторых пор совсем не такие уж и редкие, иногда прямо пачками.
Ещё чуть позже: для лучшего понимания сообщение дополнено
выдачей gettid()
'а (пришлось его делать как
syscall(SYS_gettid)
, т.к. в libc нет "wrapper'а"). Так вот: в
основном просто разрозненные сообщения, при чём в основном от
cxscheduler'ного thread'а (как ни странно!); но несколько раз бывали
"дуплеты" в течение 1 миллисекунды -- сначала от основного, потом от
cxscheduler'ного (в т.ч. один раз -- прямо при старте).
16.06.2020: директория с текущими исходниками tvcapture складирована в work/sw4cx/misc/cdaCV/
06.04.2022: поскольку диагностика по облому
pthread_mutex_trylock()
при включенном DEBUG_LOCKS
слегка приподзадолбала, в т.ч. своей малопонятностью, то:
program_invocation_short_name
, при доступности, а иначе
"mt_cxscheduler") и PID.
DEBUG_LOCKS
в 0.
P.S. А вылез этот вопрос оттого, что я долго пытался понять, кто же мусорит в /var/tmp/cx-starter.log на v5p2. Для полной уверенности в конце концов сгородил такую цепочку:
strace -e trace=write -p "$(ps -axuwwwf | grep pts/0 | awk '{print $2}')" |& grep 'write([12],
-- смотрим все записи в дескрипторы 1 и 2 (т.е., stdout и stderr) от всех
программ, запущенных на pts/0; а pts/0 там принадлежал xterm'у, в котором
запустился cx-starter.
30.08.2022: как оказалось, ещё в первоначальном
варианте mt_cxscheduler.c::do_lock()
от 15-06-2020 был
косячок: при отключенном DEBUG_LOCKS
отключалась не только
диагностическая печать, но и попытка pthread_mutex_trylock()
--
именно перед ней стояло "#if DEBUG_LOCKS
" вместо перед печатью.
Исправлено.
select()
mutex'ами"
(mt_cxscheduler?): возникли мысли о том, что можно бы CX'ные драйверы
использовать в EPICS'е.
Создаём на эту тему специальный мини-раздельчик; пока тут, в img878, но потом надо бы переселить куда-то.
26.05.2020: типа протокола, по мере появления идей:
В любом случае будет вставать вопрос конфига: как сопоставлять CX'ную карту каналов и EPICS'ный DB; причём тут вопрос 2-слойный: и на уровне каналов конкретного экземпляра устройства, и на уровне списка устройств. Но это уж как-нибудь решаемо: можно сделать какой-нибудь препроцессор/конвертер, можно в devtype добавить ещё одно по-канальное поле наподобие dbprops, а можно просто руками изготавливать include-файлы вроде TYPENAME.devtype.db.
Возможный ответ: dbPutField()
одним из
параметров принимает dbr-тип, так что она сама выполняет конверсию.
27.05.2020: в любом случае, можно сделать специальный такой "device support", в качестве адреса принимающий cda-имя канала (хоть insrv::, для доступа к локально-подселённому cxsd, хоть cx:: -- тогда он будет работать как "бридж" к ЛЮБЫМ СУ -- хоть к CX, хоть к Tango).
01.06.2020: если этот фокус с использованием cxsd'шных драйверов в EPICS'е получится -- значит, выбранная модель драйверов является "правильной": т.е., соответствующей "природе работы с данными".
Оно работает через CEAC124, но включать это название в название драйвера не будем.
Имя же выбрано "lhebw_catctl": "lhebw" -- совпадает с именем клиента (Laser-Heated Electron Beam Welding), а "catctl" -- "CATode ConTroL".
17.06.2021: пока больше предварительные действия (в т.ч. потому, что алгоритмика работы пока не вполне огрокана):
05.07.2022: переименовываем из "turnmag" в "rotmag" по настоятельным просьбам ЕманоФеди. Аргументация -- что это "поворотный магнит" не в смысле "магнит, поворачивающий пучок", а "поворачивающийся магнит", что, якобы, на английском именуется 'rotating magnet".
Конкретно сейчас -- потому, что возникло подозрение, что кто-то сбросил лимит по току с 1.5A, и потому любое шевеление мотора сразу же вызывало останов; но убедиться в этом "задним числом" уже не удастся. А вот если бы имелся текстовый канал "причина останова" -- то всё было бы ясно сразу же.
И очевидно, что надо добавить параметр "причина" к DoStop()
(возможно, с форматом и vararg'ом?).
05.10.2021@утро-душ: такая-то диагностика -- да, несомненно полезна.
Но, учитывая, что у нас нет никакой системы архивирования, а неизбежно будут возникать ситуации, требующие разбирательства "а что же случилось час назад (или два дня назад)" -- надо бы эту диагностику выдавать также и в лог (благо, события конкретно в этой подсистеме происходят весьма редко и лог не переполнят).
05.10.2021: приступаем.
Поэтому было проведено расследование, чьи результаты описаны в разделе по vdev за сегодня, а канал переименован из vdev_state в zzz (как оно и было в lebedev_turnmag_drv_i.h).
DoStop()
добавлено:
const char *format, ...
"...
DoDriverLog()
результата.
ReportStopReason()
-- чтобы она могла вызываться и из другого
места для возврата пустой строки.
"Заклинания" для возврата текстового канала подсмотрены в
kurrez_cac208_drv.c::SetErr()
(в которой заодно была
найдена ошибка -- неприсвоение vp
).
DoStart()
-- вызов
ReportStopReason(me, "")
,
для очистки сообщения.
07.10.2021: проверил, на симуляторе: устройству ringsel_adc был поставлен флаг '+' -- суперсимуляция, так что в его каналы чтения можно были писать. Да, работает.
Только при такой симуляции выявился нюанс (если подумать -- то он должен был быть очевиден изначально): проверка срабатывает ТОЛЬКО в момент реального измерения канала (при симуляции -- "измерения"). Т.е.,
Но это-то понятно -- откуда б иначе получать информацию.
И бороться с этим бессмысленно: поскольку управление (УРом) идёт через этот же CANADC40, то и пытаться как-то отключать (при, например, посинении) -- попросту нечем будет.
А вот тут уже спорно:
IsAlwdSW_ON*()
ОБЯЗАТЕЛЬНО проверяется куча
условий, в т.ч. и подобные превышения там бы проверялись.
Короче -- ну да, как бы неидеально всё, но в данной ситуации на эту неидеальность можно просто забить.
08.10.2021: на пульт скопировано, потом посмотрим после рестарта сервера, что получится.
Это касается каналов:
05.07.2022: краткое обсуждение:
Хотя настораживает, что там есть закомментированное указание ему режима
IS_AUTOUPDATED_TRUSTED
-- какового указания, казалось бы, и
должно б быть достаточно. Но почему оно закомментировано?!
-1
-- чтоб
гарантированно отличалось от любого реального.
-1
и при сбросе
подстилающего/subord-устройства.
Но как ловить то событие -- с учётом stateless-натуры данного драйвера -- хбз.
...В конце концов, можно и забить: обновления и так будут идти корректно.
...или проблема в том, что при переходе самого ЭТОГО устройства через NOTREADY каналы поболотовеют, а обратно не вернутся?
06.07.2022: делаем, по вчерашнему проекту.
IS_AUTOUPDATED_TRUSTED
.
cur_dirn_cache
,
lim_dump_cache
, lim_ring_cache
.
lebedev_rotmag_init_d()
делает им всем
=-1
.
lebedev_rotmag_sodc_cb()
сравнивает закэшированные
значения с новыми и только при несовпадении сохраняет новые в кэше и
возвращает их.
lebedev_rotmag_rw_p()
возврата текущих
значений "по запросу" (из me->cur[]
) и раньше не было.)
Первая проверка показала, что:
Почему так -- загадка.
stat_evproc()
показывает, что реакция вроде бы ДОЛЖНА быть.
devstate_count
указан, а вовсе не 0).
Прям мистика какая-то. Надо бы поразбираться, напихав отладочного вывода.
Чуть позже: позанимался-таки расследованием -- похоже, причина найдена: дело в комбинации "искусственности" рестарта с нюансом работы "insrv::".
is_internal_rw_p()
показывает, что там
ВЫЗЫВАЕТСЯ и TerminDev()
, и InitDevice()
; а
поскольку оба они делают report_devstate()
, то состояние
отображаться должно бы.
cxsd_hw_channels[gcid]
в момент ПОЛУЧЕНИЯ события.
И к тому моменту, как поток исполнения дойдёт до vdev-insrv'шного
файлового дескриптора, значение в _devstate'овском
chn_p->current_val
уже давно опять будет +1
.
Потому, что "щёлканье" состоянием -1,+1 делается непрерывным потоком исполнения.
Так что -- всё нормально, всё как и должно быть. Ну да, вот такой неочевидный нюанс вылез, но запись _devstate=0 -- уж точно не является штатным АВТОМАТИЧЕСКИМ методом работы, а скорее отладочным; так что реальной проблемы нет.
stat_evproc()
попала, и каналы поболотовели.
-1
.
07.07.2022@утро: в голову приходят 3 варианта решения проблемы:
lebedev_rotmag_sodc_cb()
при несовпадении
хотя бы флагов?
@чуть-позже: да не, не то: ведь болотовеют-то флаги САМОГО устройства, а не подстилающего. Более того -- от подстилающего устройства события с rflags=CXRF_OFFLINE никогда и не прилетит (там они тоже поболотовеют, но вот события "обновление" с ними не будет).
hw2our_mapping[]
отдельным
каналом.
...возникало сомнение, будет ли это работать -- из-за двойной регистрации
этого канала в cda в рамках одного контекста. Но вроде должно: ведь
evproc
-то будет отличаться, а cda_add_chan()
устроена так, что сам канал второй раз создавать не будет, но дополнительный
evproc на него навесит.
Тогда понадобится сразу 2 штуки:
*_cache=-1
.
vdev_set_state()
не будет при сбросе выполнять переход в
DETERMINE/RESET (из-за совпадения nxt_state
с
cur_state
).
Ну-с, и какое решение выберем? Искусственное, но простое 2 или прямолинейное, но более громоздкое 3?
Вот и создаём раздельчик.
18.07.2024@около стадиона НГУ, по дороге из девятиэтажки домой: памятуя, что года полтора назад шло некоторое обсуждение касательно тренировки чего-то на СКИФе, и там Лёша Левичев набросал некое ТЗ+алгоритм, а я этот алгоритм преобразовал в event-driven: вот взять тот алгоритм да реализовать отдельным драйвером, чтоб можно было его "натравливать" на разные устройства через insrv:: -- на вход подавая блокировки.
21.07.2024: информация от того олуторагодичного обсуждения (на СКИФ оно вроде так ни к чему и не привело):
Программа тренировки клистрона Пользователь должен задавать следующие параметры:
- Максимальное напряжение зарядного устройства, до которого программа может его поднимать (или это считывается из уставки программы С. Вощина по управлению модулятором)
- Порог вакуума, при котором происходит сброс напряжения
- Уровень вакуума на МРН после клистрона, после которого начинается рост напряжения (может быть вшита в программу, пока я бы взял 20 мкА)
- Уровень напряжения зарядного устройства, до которого происходит сброс напряжения
- Шаг подъема напряжения (в принципе, это может быть вшито в программу, я бы взял 0.1 кВ).
- Максимальный уровень напряжения (или битов АЦП) отраженного сигнала, после которого происходит сброс напряжения высокого клистрона.
Принцип действия программы. Пользователь включает клистрон и поднимается до какого-то напряжения на клистроне и включает программу тренировки. Если произошло превышение п.2 или п.6, то программа сбрасывает напряжения до заданного нижнего порога п.4. После этого она определяет вакуум в насосе (ток насоса). Если он выше порога п.3, то программа ничего не делает, если он ниже, то начинает поднимать напряжение. Поднимает напряжение с шагом п.5, ждет 5 импульсов, после этого снова поднимает, то есть шаг за пять импульсов. На каждом этапе программа определяет максимальное напряжение отраженной волны п.6 и уровень вакуума п.2. Если один из показателей превышен (п.2, п.6), то программа опять сбрасывает напряжение до указанного уровня п.4 и все повторяется. За последний шаг до максимального заданного напряжения программа автоматически уменьшает шаг подъема напряжения на заданный п.5/10. Уже с меньшим шагом поднимается до указанного значения с тем же алгоритмом. Если на стадии подъема ни один показатель не превышен, то программа поднимает до максимального уровня п. 1 и останавливается либо до срабатывания превышения заданных уровней п.2, п.6, либо до изменения пользователем максимального уровня напряжения п.1.
- A. Программа постоянно мониторирует все задействованные каналы, в т.ч. текущие значения вакуума и напряжения (а также блокировок).
- B. По приходу измеренных значений вакуума и напряжения они сравниваются с установленными границами п.2 и п.6 соответственно, и в случае превышения границ производится сброс уставки напряжения до значения из п.4.
- C. Если текущее значение уставки напряжения ниже рабочего уровня (п.1), то производится подъём; процедура "сделать шаг подъёма" выполняется с некоторой периодичностью -- например, по событию "выстрел".
- C.1. Для того, чтобы делать шаги не каждый выстрел/период, а с некоторыми паузами, вводится переменная-счётчик, в которую после изменения уставки записывается число импульсов, сколько надо пропустить (например, 5), а при каждом импульсе она уменьшается на 1 и следующий шаг уставки производится в случае, если значение счётчика уменьшилось до 0.
Этот же счётчик позволяет организовать паузу между сбросом уставки (по превышению вакуума или напряжения) и началом её подъёма: в момент сброса уставки в него записывается число импульсов, сколько надо подождать (например, 20).
- C.2. Процедура выполнения шага подъёма:
- C.2.a: если уставка уже почти дошла до номинала -- разница между рабочим напряжением (п.1) и текущей уставкой меньше шага подъёма (п.5) -- то уставка увеличивается на 1/10 шага (но если разница меньше 1/10 шага, то ставится номинал), в противном случае...
- C.2.b: ...уставка увеличивается на 1 шаг.
Вот этот мой алгоритм и стоит превратить в драйвер-"тренер".
04.09.2018: процедура:
KOZDEV_nnn
--
{OUT,OUT_RATE,OUT_CUR}_n_base -- были заменены на DEVSPEC_nnn
.
Поскольку оно нужно только для отладки, то все обращения к ним -- в т.ч.
в драйверовых _rw_p(), где защита от возврата CXRF_UNSUPPORTED --
за-#if'лены по символу DO_RETURN_CUR_PARAMS
, который
уставляется в 0
(хотя и условно, так что можно включать этот
функционал хоть по -DDO_RETURN_CUR_PARAMS=1
.
Всё собирается -- и старые, и новые, а вот работает ли -- надо отдельно проверять.
05.09.2018: а еще есть потребность мочь передавать в
HandleSlowmoREADDAC_in()
кроме значений также и rflags.
Конкретно у ПКС-8 вполне рядовая ситуация, когда что-то почему-то не читается (да хоть потому, что блока в этой позиции нету). И у любых прочих CAMAC-ЦАПов будет аналогично, да и у CAN-устройств что-нить может быть.
Ну чо -- параметр добавлен, лёг на имеющийся код идеально (просто возврат указанного значения вместо 0).
Соответственно, CAMAC-драйвер передаёт имеющиеся у него rflags, а четвёрка CAN'овских -- 0.
По-прежнему осталось проверить.
06.09.2018: проверено.
Сброс переставлен на первое место в цепочке, и всё заработало.
Надо фиксить. Фиксим, довольно прямолинейно:
!me->out[l].isc && // No mangling with now-changing channels...
заменен на
-- т.е., НЕ вмешивается только в кинетические каналы.// No mangling with now-changing channels... (!ci->isc || ci->spd == 0 || ci->tac <= 0) // ...at least with kinetic ones &&
Кстати, тот предохранитель был явно с давних времён, кабы не изначально -- он есть и в v2hw'шном коде. Почему -- хбз: то ли на всякий случай, то ли были причины. Рытьё в bigfile-0001.html ("Плавное изменение уставок CANDAC") результата не дало (только констатация "с уже-меняющимися каналами мы НЕ пытаемся..." от 06-03-2012). Рытьё в архивах исходников показало, что так оно было, похоже, изначально: в w20090721.tar.gz в cx/src/programs/server/drivers/canbus/xcac208_src.c уже так.
А теперь общее условие выглядит просто ужасно, с дублированиями. Явно напрашивается на улучшение.
DoCalcMovement()
вернуло
DO_CALC_R_SET_IMMED), добавлено условное "отключение isc", скопированное из
HandleSlowmoOUT_IMM_rw()
:
/* Stop slowmo */ if (me->out[l].isc) { me->num_isc--; me->out[l].isc = 0; }
me->out[l].
" были заменены на
"ci->
" -- так намного короче и читабельнее.
На вид -- проблема ушла.
Но теперь надо отдельно смотреть на поведение CAN-устройств, где применение шире, и ждать "сюрпризов".
ReportOUT_Ranges()
, отдающая наверх диапазоны, известные
из по-канальных min/max, а при их неуказанности -- из
MIN_ALWD_VAL/MAX_ALWD_VAL.
14.04.2021: краткие комментарии (собственно работа описана в разделе о диапазонах за сегодня):
Поскольку объём получился очень немаленький, то данная функциональность и вытащена в advdac.
_
": этот подчерк символизирует ВСЕ каналы
"OUT": OUT, OUT_CUR и OUT_IMM.
Для начала -- "простой" драйвер, лишь бриджующий данные в MQTT.
В будущем, возможно, понадобится и что-нибудь поинтеллектуальнее, работающее с MQTT-устройствами "осмысленно" (хотя, скорее всего, это проще будет реализовывать vdev-драйверами, использующими mqtt_mapping-устройства как подстилающие).
А cda -- в лице ctx_ref_checker()
-- считает имена
case-insensitive.
Хоть и вряд ли кто-то будет в одной системе использовать имена, отличающиеся лишь регистром, но надо б подумать о том, как бы мочь выбирать режим сравнения (case-sensitive/case-insensitive) в зависимости от типа соединения.
Сейчас это выглядит проблемой: проверка в cda_add_chan()
стоит ЗАДОЛГО ДО определения "схемы".
...эта же проблема, кстати, касается и используемых символов-сепараторов:
хоть в cda_dat_p_rec_t
и есть поля sep_ch
,
upp_ch
, opt_ch
(для '.', ':',
'@' соответственно), но в реальности они не используются, да и НЕ
МОГУТ использоваться при нынешней модели парсинга имён, когда "схема" узнаётся
сильно после парсинга.
Зато cxsd_db* порадовала: там имена из devtype'ов сохраняются без изменения регистра. Так что если в девайсе присутствует имя вроде "Home/Room1/...", то надо просто определить имя канала с правильной капитализацией -- "Home.Room1....", и всё сработает как положено.
И тут теоретически есть 2 возможности:
Как с ней сдруживаться -- будет зависеть от её архитектуры.
Тогда ввод/вывод будем делать сами, по своему усмотрению.
Тогда она гарантированно сгодится и для драйверов (ВСЕХ вариантов), и для cda_d_mqtt, и для ЧеблоПаши.
30.11.2019@8-ка-в-город ~11:00, едучи по Строителей: ага -- "libmqtt4cx".
Чуть позже: нет, фиг -- не удастся. Дело в том, что длина пакета в MQTT не является фиксированным полем, а начинается со 2-го ([1]-го) байта пакета и имеет переменную длину, с кодировкой a-la UTF8 (младшие 7 бит используются как очередной компонент длины; старший же в значении 0 говорит, что на этом всё, а при =1 -- что дальше будут ещё байт(ы) длины).
05.10.2018@вечер, дорога домой: а если ввести возможность "расширять" функционал fdiolib'а, как бы "плагинами"?
StreamReadyForRead()
умеет делать прекрасно, и дублировать её как-то не хочется.
07.10.2018@утро-воскресенье-пультовая: для расширения кругозора на тему "как бы можно работать со сторонними библиотеками сериализации/десериализации" надо поанализировать не только библиотеки для MQTT, но также для SNMP, конкретно libnetsnmp.
Т.к. конкретно в libnetsnmp, похоже, есть разделение на ввод/вывод и сериализацию/десериализацию -- там кабы даже не свой I/O scheduler имеется, судя по файлу fd_event_manager.h.
Для оного анализа надо заглянуть в исходники, например, в net-snmp-5.7.2-24.el7_2.1.src.rpm: т.к. попытка что-нибудь понять путём чтения header'ов ничего не дала, то надо искать примеры и исходники утилит.
07.10.2018@пляж: "плагины" в обычном смысле никак не получится, т.к. все тонкости скрыты внутри fdiolib.c.
Но некоторые мысли о том, как бы всё же реализовать -- есть, и они уже будут идти в разделе самого fdiolib'а (от сегодняшнего числа).
10.10.2018: квазиплагины в fdiolib сделаны.
Поскольку в обоих используется связь по TCP, с поддержкой реконнектов -- вот и возьмём готовую инфраструктуру, заменив протоколы (содержимое перегоняемых данных) и, вероятно, "мозги" определения адресатов для коннекта (в соответствии с "алгоритмом обнаружения брокера").
Идея, лежащая в его основе, расписана в подразделе "MQTT" раздела "Знания" -- автоматическое составление карты MQTT-имён, "вытягивая" список имён из devtype'а и заменяя точки на слэши.
Делаем покамест в sw4cx/drivers/ (хотя потом надо б будет куда-нибудь перенести, но куда (4cx/src/drivers/? hw4cx/drivers/? совсем отдельный проект?) -- неясно совершенно.
03.10.2018:
cxsd_hw_channels[]
в сервере -- со
статического на аллокируемое. Т.к. с точки зрения синтаксиса C оно всё
одинаково, но на уровне кода доступ разный, и когда изменение всё-таки
произойдёт, то откомпилированный под старую модель драйвер просто перестанет
работать.
GetDevInstname()
", т.к.
нужна, а в драйверном API её пока нету.
16.02.2020@дома-воскресенье: она переехала в cxsd_driver.c.
Теперь надо брать да проверять.
04.10.2018: проверяем.
Итого:
Сами записи делались ещё с 07-06-2023, но в другом месте (просто в конце файла), теперь же перенесём их сюда.
Описания конкретных компонентов (драйверов и утилит) разместятся уже в своих подразделах.
Замечание: сами компоненты будут жить, вероятно, в hw4cx/drivers/eth/ и, возможно, в hw4cx/srivers/serial/ (размещение внутри serial/ неочевидно), но обсуждение/описание будет тут, в собственном разделе.
07.06.2023@Томск-Беленца-6-23, вечер: насчёт реализации "общего" драйвера Modbus была идеологическая проблема: как реализовывать "сложные" каналы, вроде битовых полей, когда из ОДНОГО аппаратного значения надо вернуть значения во МНОГО каналов (как определять, в КАКИЕ?) и, аналогично, как делать композицию нескольких битовых каналов (КАКИХ?) в одно аппаратное значение.
Так вот: идея -- в том, чтобы РАЗДЕЛИТЬ эти вещи на разные драйверы (да-да, в стиле EPICS, как и задумывалось 06-03-2014@лыжи (ну ПОЧТИ -- идеологически в ту же сторону)). А именно:
Некоторые заметки о планируемых деталях реализации:
18.06.2023: вот тогда вечером на Беленца-6 не записал полный список вариантов, а теперь уже и не вспоминается ничего, кроме того, что _ONS не катит...
Да, это будет занимать некоторое процессорное время при каждой отправке, но за счёт экономии памяти и упрощения кода: не понадобится ни держать элементы очереди до 256 байт, ни создавать отдельный memory pool для них. А "потери" производительности скорее теоретические: ведь бОльшая часть пакетов будет отправляться лишь единожды (при нормальной работе устройства) и не потребует повторного кодирования.
ЗЫ: именно такое решение было когда-то в 1990-х принято в Netscape в отношении анимированных GIF'ов: там при каждом показе каждого кадра он заново раскодируется из исходника, а не декодируется всё сразу, Именно из соображений экономии памяти за счёт использования процессора. (Я тогда об этом где-то прочитал, сейчас уже и не вспомню, где именно; то были времена версии 2 или 3)
Как именно "как-нибудь" -- надо подумать над синтаксисом. Учитывая дурнодоспецифицированность Modbus и плоходокументированность конкретных устройств --
1
); однако в таком случае нужно всё-таки иметь
возможность ТОЧНО указать адрес, например, отрицательным числом (13.06.2023: с другой стороны -- а зачем? раз можно просто
указать после "4" аппаратный адрес плюс 1
). 16.07.2023: а затем, что в некоторых описаниях -- вроде
РПМ-416 -- указываются именно реальные адреса, и лучше прямо их и указывать
в конфигах, без дурацких лишних "плюс 1".
NNNqelem_t
), в
которой указывать номер исходного канала (точнее, его индекс в таблице).
13.06.2023: всё-таки фигово получается с таким подходом: если пакет почему-то пропадёт (устройство не отреагировало? или вышеописанный таймаут), то связанный с ним канал НАВЕКИ останется запрошенным-и-неотвеченным...
18.06.2023: придумано решение для таймаутов -- "ГИБРИДНЫЙ режим", когда при несоответствии пришедшего пакета ожидаемому всё же выполняется поиск канала-получателя.
08.06.2023@Томск-Беленца-6-23, утро, ожидая хозяев квартиры: только одна проблема с таким подходом: при этом затрагиваемые int32-каналы будут опрашиваться ПОСТОЯННО, а не при наличии клиентов (т.к. сам rdreg_drv и станет таким клиентом посредством cda).
11.06.2023: продолжение обсуждения этой темы в специальном разделе за сегодня.
12.06.2023@~16:00, дорога в Онлайнтрейд за VALERA 647.01, проходя мимо Алекты на параллельный Лаврентьева проезд к ИФП и Пирогова: кажется, я догадался, почему Modbus-TCP такой никакущий и не даёт ничего сверх Modbus-serial (ни пакетов длиннее 256 байт, ни подписки на изменения): скорее всего, на устройствах его в основном реализуют посредством чип-конвертера serial-to-Ethernet (как у Орешонка/Стюфа) -- т.е., устройство принимает и шлёт практически те же данные, только работает не с настоящим линком, а с тем чип-конвертером.
Именно так напрашивалось делать для максимально дешёвого апгрейда serial-устройств до Ethernet, и при таком подходе никакой дополнительной по сравнению с Modbus-serial функциональности быть и не могло.
06.02.2024@дорога из Эдема домой, переходя Николаева рядом с Николаева-7: а ещё там нет уведомлений/подписки потому, что в обычном формате пакетов ответа попросту НЕТ информации о номере регистра и их количестве; пришлось бы дополнительные коды функций вводить.
12.06.2023@вечер, дорога из Онлайнтрейда/Эдема/Быстронома домой, подходя к Пирогова-34: весьма вероятно, что в какой-то момент захочется/занадобится сделать поддержку и Modbus-Serial.
Но, учитывая, что для serial НЕОБХОДИМ layer (по причине разделяемого доступа к линии), в то время как для TCP намного удобнее раздельные независимые драйверы (там работа с TCP изолирована и для layer'ности потребуется лишний accounting), пока несколько неясно, что между ними может быть общего. Разве что какие-то куски работы с непосредственно данными (PDU) -- закодировывание и раскодировывание.
Так что это оставим до возникновения какой-нибудь непосредственной потребности -- вот как появится, тогда и будет конкретный пример с конкретными требованиями, а не абстрактное нечто в вакууме.
13.06.2023@утро, лабораторный круглый стол, ~10:00: а ещё, вероятно, понадобится утилитка командной строки -- для всяких тестов и разбирательств.
modbus_test DEVICE_SPEC COMMAND...
serial_hal_opendev()
, надо оборачивать в какие-то файлы.
P.S. Кстати, очевидно же, что если захочется таки сделать поддержку modbus-драйверов через RS232/RS485, то надо будет иметь аналогичную структуру доступа к COM-портам -- видимо, просто в тех же директориях drivers/serial/*/ делать.
15.06.2023: (ещё в Томске, числа 7-го, вечером) кстати,
нынешний единственный serial-layer piv485_lyr_common.c не умеет
корректно отрабатывать ситуацию, когда из COM-порта читается 0 байт (т.е.,
USB-адаптер выдернут). А надо бы делать ровно то же, что и
TCP-драйверы: выполнять "ScheduleReconnect()" --
ScheduleReopen()
-- из которого повторять
serial_hal_opendev()
и прочие "заклинания" вроде
sl_add_fd()
.
15.06.2023@конференц-зал, ~11, на аспирантских защитах: а если подумать, то можно обойтись и БЕЗ внешних драйверов: раз уж всё равно будет карта каналов со "свойствами", то ничто не мешает среди свойств, наряду с форматом/кодировкой также указывать опциональный "маппинг на каналы":
При этом указывается такое свойство только ПЕРВОМУ каналу, а остальным ("зависимым") автоматом прописывается номер "базового" и их порядковый номер бита, чтобы при обращении к ним правильно вызвать чтение либо запись (в соответствующий бит) базового.
Впрочем, это не запрещает изготовить отдельные rdreg_drv и wrreg_drv.
18.06.2023@Ключи, гуляя в домике и под навесом, ~14:00 : рассуждения на тему "да как же всё-таки отправлять пакеты sendqlib'ом, чтобы и повторную отправку по TCP не делать, и чтоб таймауты обнаруживать?".
Общий вывод такой:
Так что --
sq_sendnext()
.
(19.06.2023@ИЯФ, после планёрки
~11:10: который автоматом удалит предыдущий таймаут при установке
следующего -- если сделать так, как tim_enqueuer()
в
cankoz_lyr_common.c и piv485_lyr_common.c, а это и есть
верная реализация.)
16.08.2023: НИ-ФИ-ГА! До установки следующего таймаута
дело просто не дойдёт, т.к. sq_sendnext()
а) НЕ удаляет
текущий таймаут, а только приводит к [в конечном итоге]
perform_sendnext_in_queue()
, который b) при наличии
текущего таймаута просто ничего не делает. В чём мы и убедились
08-08-2023 :-(.
Из написанного выше видно, что всё же крайне желательно, чтобы пакеты ответов, НЕ соответствующие последнему отправленному, всё же обрабатывались, а не просто отбрасывались. Это решит проблему "из-за тормозов scheduling'а или связи ответ пришёл позже, был отброшен и запрос на канал навеки остался зависшим".
@под навесом: А можно использовать ГИБРИДНЫЙ режим маппинга пришедшего пакета на канал: сначала сравнивать с "ожидаемым" (ответом на последний отправленный), а уж если не совпало -- то производить поиск по всем каналам на тему "кому сопоставлен этот {ТИП,АДРЕС}".
...но надо бы ещё проверить -- в ответных пакетах содержится ли информация о типе+адресе запроса, или же только данные?
@дома, ~16:00: да, в ответных пакетах также есть и команда, и адрес. (27.07.2023: ни-фи-га -- далеко не всё и не во всех: в ответах на ЗАПИСЬ есть адрес+количество, а на ЧТЕНИЕ -- только число БАЙТ (касается и 1-битовых, и 16-битовых)...) Более того -- есть понятие "пакет ошибки", где выставляется старший бит в коде команды (0x03->0x83), и контекст тоже как-то указывается. Очевидно, что ответы надо ПЕРВЫМ делом проверять на ошибки и обрабатывать соответственно, ДО проверки соответствия/несоответствия последнему отпарвленному.
23.06.2023: дополнение: только при приходе пакета, НЕ
соответствующего ожидаемому, НЕ надо пытаться ни удалять из очереди текущий
первый пакет (если вдруг он там будет, а не _ONS), ни вызывать отправку
следующего. Кстати, для serial-варианта надо будет использовать
SQ_TRIES_INF
, а не _ONS. Кстати,
piv485_q_erase_and_send_next()
так и делает: если первый пакет
в очереди не соответствует модели (т.е., пришедшему), то не только его удаления,
но и отправки следующего не делается.
19.06.2023@утро: а есть ведь, помимо "Modbus-TCP" и "Modbus-RTU", ещё и "гибридный вариант": для устройств, подключенных через протокол-конвертеры вроде Moxa nPort, получается вариант "упаковка данных и round-robin-scheduling Modbus-RTU, но через TCP-сокет, который надо коннектить и реконнектить к указанному IP:PORT".
И его желательно тоже делать ТЕМ ЖЕ кодом, без лишнего дублирования (да, хоть сейчас реализуем только простейший вариант TCP, рассчитывать-то надо на возможность полного спектра, для чего все части кода стараться делать максимально изолированно/модульно, чтоб реюзабельно).
(Тут "COM" -- это потенциально любой из ныне поддерживаемых в hw4cx/drivers/serial/ вариантов: com mxupcie usb (Advantech?).)
23.06.2023@утро, записывая вчерашние идеи про унификауию Modbus-ASCII с -RTU: очевидно же, что надо просто разделить на 2 параметра: 1) формат данных (способ кодирования/декодирования пакета) и 2) способ соединения (коннект через сокет или COM-порт). Ну да, получился заодно бессмысленная комбинация "формат TCP через COM-порт", но и пусть -- вреда от неё нет.
21.06.2023@утро, мытьё посуды: насчёт векторов.
Исходные данные:
Откуда пара идей:
Число элементов можно указывать в drvinfo, а можно и брать от
CxsdHwGetChanType()
.
Тут, конечно, в обоих случаях возникнут некоторые нетривиальности:
...можно аллокировать одном общим блоком, а в свойствах каналов прописывать offset'ы в нём.
...хотя тут вопрос о взаимоотношениях этих данных и тех, что хранятся в буферах для отправки: можно же иметь один ЭТОТ буфер, а при формировании пакетов на отправку брать данные прямо из него, с соответствующим для пакета-слайса смещением.
Но и в таком случае возникает вопрос о том, ЧТО хранить в таких буферах: исходные данные (от сервера) или уже должным образом сконвертированные в байты, готовые к отправке устройству.
ReturnDataSet()
.
Напрашиваются правила:
Так выглядит наиболее "правильно" -- что конверсия происходит в момент получения данных от одной стороны сразу в формат для другой.
22.06.2023@дорога домой с обеда в Гусях, проходя мимо Алекты по кривым бетонным плитам: если ASCII отличается от RTU только тем, что каждый "байт" в пакете занимает по 2 символа, то
Таким образом, различаться будет только приём/передача, причём минимально, а собственно РАБОТА с данными будет одинаковой
23.06.2023: а вот контрольные суммы считаются по-разному.
FDIO_STREAM_PLUG
и
fdio_advice_*()
, т.к. fdiolib не сможет дешифрировать длину из
символьной кодировки.
@мытьё посуды, ~10:30: а можно проще --
использовать FDIO_STRING
: тогда fdiolib
позаботится о '\r\n', но только надо будет проверять наличие
':' в начале строки и молча отваливать при отсутствии -- это будет
способ начальной синхронизации.
25.06.2023@утро-душ: а так-то, возможно, драйвер для ТЕКУЩЕЙ потребности -- вычитывание токов и напряжений из ПЛК от Боймельштейна -- можно сделать по-тупому, как для радиационных блокировок; точнее -- просто скопировать modbus_tcp_rb_drv.c в, например, modbus_tcp_iu_drv.c и чуть поменять там содержимое отправляемого пакета и расшифровку принимаемого, адаптировав под конкретные каналы.
23.07.2023: не удержался -- начал-таки делать modbus_tcp_iu_drv.c, чтоб хоть как-то начать работать с тем богопротивным девайсом РПМ-416. Там достаточно читать 108 штук 16-битных holding registers, триплетами "UINT16, ULONG" (хбз, почему так -- вот и разберусь как раз).
25.06.2023@пешком в Ярче после гуляния с Женьками, проходя вдоль Терешковой-26: похоже, что в случае развития драйвера -- если таковое произойдёт -- он таки превратится в общий modbus_drv.c, которому уже как-то будет указываться протокол общения и адрес устройства (плюс тип соединения).
А раз layer'ы становятся обязательны, то для конкретно TCP -- layer-заглушка, с именем вроде "modbus_via_tcp".
Только надо будет хорошенько подумать об API -- чтоб в нём было именно только взаимодействие со средой передачи, и более ничего.
25.06.2023@пешком в Ярче после гуляния с Женьками, проходя вдоль Терешковой-24: учитывая характер протокола/трафика Modbus, просто напрашивается делать оптимизации -- если спрашивают какие-то каналы, то слать не раздельные команды чтения, а одну групповую на все и потом дешифрировать ответ.
Но вот КАК устраивать такое группирование -- вопрос.
26.06.2023: и для такого программного обнаружения может понадобиться возможность явного указания "не включая такой-то канал в группу" -- мало ли какие особенности работы устройства, может, что-то надо сильно индивидуально читать.
Тут-то уж точно только в auxinfo указывать -- например, тип+адрес регистра для периодического чтения.
26.06.2023@~11:20, дома, вернувшись
из ИЯФа с планёрки: либо, как вариант -- у некоторых каналов в
drvinfo указывать флажок "опрашивать периодически", тогда они чтоб
добавлялись в некий список плюс им чтоб уставлялось
IS_AUTOUPDATED_YES
.
26.06.2023@утро, записывая предыдущий вчерашний пункт: кстати, при той оптимизации и процедура дешифрирования пакета ответа ("при несоответствии последнему запрошенному") должна быть весьма нетривиальной: надо не просто сопоставлять пакет каналу, а ПОШТУЧНО регистры анализировать, какой к какому каналу относится -- ведь в одном групповом ответе может быть МНОГО каналов.
Вообще вся эта "групповуха" требует весьма некислых трудовложений.
11.06.2023@утро-зарядка: причина (корень) проблемы в том, что у нас API несколько неодинаков вдоль цепочки передачи данных: если внутри сервера это запрос-ответ, то на ветке клиент-сервер это принудительная подписка (естественно, с отдельным запросом для записи).
Но формально-то ничто не мешает организовать возможность
работы БЕЗ подписки, только по запросам: флажок
"CDA_DATAREF_OPT_NOMONITOR
" (@утро-перед-завтраком:: о -- он уже есть!) плюс
операция "запроси чтение" (вот только API ради неё расширять?).
А так -- практически все поддерживаемые протоколы (возможно, кроме VCAS, но надо проверить) позволяют это.
Тогда rdreg_drv и прочие аналогичные могли бы из своих
_rw_p()
просто редиректить запросы чтения (в случае с rdreg_drv
-- возможно, с оптимизацией, взводя флаг "чтение источника запрошено" при
первом же и сбрасывая его по получению данных).
11.06.2023@утро-душ: насчёт
"только API ради неё расширять" -- в идеале-то да, но можно обойтись и
добавлением кода CDA_DAT_P_CHAN_IOCTL_NR_RQRD
, чтоб
отрабатывалось методом chan_ioctl()
(а если когда-то будем
расширять API cda_dat_p_rec_t
, то тогда и добавим отдельный
метод).
@вечер-душ: а лучше во всех
cda_d_*.c* сразу сделать готовые методы cda_d_*_rqrd()
(пока не указывая их в метрике) и из chan_ioctl()
просто
вызывать их.
07.07.2023: оказывается, ровно такая же идея была 09-06-2020.
11.06.2023: исследуем текущую ситуацию -- насколько приспособлена к такому нововведению ветка cda_d_cx/cxlib/cxsd_fe_cx:
Оный флаг CDA_DATAREF_OPT_NOMONITOR
Вызов cx_rq_rd()
уже присутствует -- так что тут всё готово.
Режим CX_MON_COND_NEVER
реализован ПОЧТИ как надо, но именно
только "почти": обычно обновления клиенту НЕ передаются, если только
предварительно не было получено CXC_CH_RQRD
(у
MON_COND_NEVER-каналов приводящая к выставлению флага
MONMODE_SEND_UPDATE
, сбрасываемого сразу по получению
обновления).
И оно так не без причины: это именно для отсекания ненужного траффика для всяких tvcapture.
Так что нужен ОТДЕЛЬНЫЙ режим работы... Назвать-то его, видимо, "PASSIVEMON" (то же число букв (10.07.2023: ага, щас! На 1 больше!)). Но вот детали реализации -- особенно на стороне cda (лишний флаг?) -- пока не совсем очевидны...
Конечно, надо бы ещё отдельно исследовать cda_d_insrv.c, но там заведомо проще -- поскольку всё в едином файле, то и согласовывать только внутри него.
28.06.2023@утро: с этим
режимом работы есть один нюанс касательно каналов ЗАПИСИ: в случае, если
будет использоваться именно нынешний вариант функционирования
CDA_DATAREF_OPT_NOMONITOR
/CX_MON_COND_NEVER
, то
каналы записи вообще будут навсегда залипать в состоянии "запрос отправлен,
но ответа не получено, т.к.
MONMODE_SEND_UPDATE
отсутствует), ...
Т.е., CDA_DATAREF_OPT_NOMONITOR
годится исключительно для
обычных каналов чтения, но НЕ для каналов записи и AUTOUPDATED.
Единственным решением могло бы быть, если cxsd_fe_cx.c (и
прочие) САМИ бы обращали внимание на "вид" канала -- что говорит
IsAnUpdate()
-- и чтоб СРАЗУ по CXC_CH_RQRD
возвращалось бы текущее значение, БЕЗ вызова CxsdHwDoIO()
(и
взведения MONMODE_SEND_UPDATE
).
19.12.2023@утро, дорога в ИЯФ, выходя
из леска перед НИПСом (после писания насчёт
CDA_DATAREF_OPT_NOMONITOR
в
20231212-OTCHET-PO-GOSZADANIYU-2023.txt): а нифига -- достаточно при отработке ЗАПИСИ взводить
MONMODE_SEND_UPDATE
так же, как при отработке чтения.
19.12.2023: в продолжение темы "NOMONITOR для rw-каналов": оказывается, pipe2cda очень даже ИСПОЛЬЗУЕТ этот флаг именно так -- взводит его для писуемого канала, причём ЧТЕНИЕ оттуда её и не интересует (evproc=NULL). Сделано явно для экономии сетевого траффика, скорее всего -- для тогдашнего варианта tvcapture, который ею пользовался.
02.07.2023: пилим потихоньку.
cda_d_cx_req_read()
-- практически копированием
cda_d_cx_snd_data()
с тривиальной заменой вызова
cx_ch_rq_wr()
на cx_ch_rq_rd()
.
cda_d_insrv_req_read()
-- аналогично копированием
cda_d_insrv_snd_data()
, только тут есть пара нюансов:
IsAnUpdate()
,
скопированной сюда из cxsd_fe_cx.c.
И в случае игнорирования -- просто СРАЗУ отправляется ответ (аналогично
тому, как поступает cxsd_fe_cx.c в ответ на
CXC_CH_RQRD
).
cda_dat_p_update_dataset()
(что могло бы дать бесконечную
рекурсию), а "генерится событие обновления", отправкой значения
hwr
в уведомительный pipe.
04.07.2023: тот старый косяк исправлен, так что и cda'шная часть:
cda_req_ref_read()
, он
вызывает метод chan_ioctl()
с кодом
CDA_DAT_P_CHAN_IOCTL_NR_REQ_READ
.
cda_dat_p_rec_t
-- то зачем ioctl, чё б сразу не добавить
метод req_read()
?
IsAnUpdate()
-- а кто будет учитывать
MustCacheRFW
? 08.07.2023: а как его
учтёшь -- он же в cxsd_hw.c локален...
07.07.2023: далее.
cda_d_cx_chan_ioctl()
-- весьма тривиально, и
выглядит она аналогично cda_d_cx_srv_ioctl()
.
cda_d_insrv_chan_ioctl()
.
ca_array_get()
? (Точно НЕ надо указывать -- для подписки,
хвала аллаху.)
ca_array_get_callback()
такая дурь
отсутствует.
GetPropsCB()
.
NewDataCB()
.
COUNT
, и, в отличие от ca_create_subscription()
,
НЕ сказано "A count of zero specifies the native elemnt count.".
А значение max_nelems
у нас в hwrinfo_t
НЕ
хранится, так что, в отличие от cda_d_epics_new_chan()
(которое
как раз могло бы указывать просто 0) взять его негде.
Пока что всё же решено рискнуть и передавать просто 0
.
Чуть позже: а вот в более свежем
CAref.html от 3.15
всё-таки добавлено "For ca_array_get_callback()
a count of zero
means use the current element count from the server.".
Сделано. Также, поскольку подписка делается не только из
cda_d_epics_new_chan()
, но и из StateChangeCB()
,
значение options
теперь сохраняется в свежевведённое поле в
hwrinfo_t
.
А для "PASSIVEMON" ничего особо делать не надо -- EPICS'ный IOC и так работает по этой модели (опрос он делает САМ, а не при наличии клиентов).
Итого: инфраструктура "запросить чтение" вроде сделана, теперь надо реализовывать режим PASSIVEMON.
09.07.2023: приступаем...
CX_MON_COND_PASSIVE=3
, а нереализованная
CX_MON_COND_ON_DELTA
передвинута на =4.
CX_UPD_COND_PASSIVE=3
cx_ch_open()
.
CHNEvproc()
по событию _R_UPDATE
для PASSIVE-мониторов просто выполняет отправку.
Пока НЕ сделано:
10.07.2023@утро, после планёрки, которая не состоялась: некоторые размышления, начавшиеся ещё вчера:
CDA_DATAREF_OPT_
-битик, и вместе с нынешним NOMONITOR они
составят 2-битовый код "условие обновления", который по умолчанию -- ==0 --
означает "присылай обновления", поодиночке взведённые битики будут означать
два варианта с ограниченным мониторированием, а ещё остаётся один пока
незадействованный код (когда оба битика горят).
В принципе, можно как раз эту комбинацию и задействовать.
(Такая мысль появилась при взгляде на табличку
cda_d_cx.c::mode2upd_cond[]
, где CX_UPD_COND_NEVER
присутствует дважды.)
Такая мысль появилась уже при взгляде на комментарии к определениям
CX_MON_COND_nnn
/CX_UPD_COND_nnn
, которых теперь 4
штуки: там у ВСЕХ, кроме ON_CYCLE, обновления шлются сразу, включая оба
"пассивных" варианта; но формально "пассивным" можно также сделать отправку
по концу цикла. Не то, чтоб это прямо реально требовалось, но из
соображений разделения/ортогонализации функциональности -- так лучше.
@вечер: да, это сделать можно, и
довольно просто: ровно так же, как обычным ON_CYCLE --
"mp->modified = 1; cp->per_cycle_monitors_some_modified = 1;
"
-- т.к. SendEndC()
проходит по ВСЕМ мониторам, не только по
ON_CYCLE, несмотря на название флага
"per_cycle_monitors_some_modified
".
11.07.2023: переходим на явное наличие метода
req_read()
и его прямой вызов без промежуточного
chan_ioctl()
:
cda_dat_p_req_read_f
;
cda_dat_p_rec_t
заменяем rsrvd_1
на
req_read
, ...
rsrvd_3
и
rsrvd_4
.
CDA_DEFINE_DAT_PLUGIN()
.
CDA_DAT_P_CHAN_IOCTL_NR_REQ_READ
закомменчено.
cda_req_ref_read()
-- теперь прямой
вызов метода.
cda_d_*_req_read()
и пара NULL, NULL
в конце, а
cda_d_*_chan_ioctl()
удалены.
NULL, NULL
в конец метрики.
11.07.2023: возвращаемся к реализации пассивного мониторирования, пока что со стороны cda.
CDA_DATAREF_OPT_PASSIVEMON
=1<<19 плюс зарезервировано
CDA_DATAREF_OPT_rsrvd18
=1<<18 (чтоб было 3
последовательных бита на "режим мониторирования", дающие потенциально 8
вариантов, из которых сейчас задействовано 3).
cda_add_chan()
--
new_chan()
.
CDA_CONTEXT_OPT_IGN_UPDATE
: теперь не просто взводится
CDA_DATAREF_OPT_NOMONITOR
, но ещё и нулятся
CDA_DATAREF_OPT_ON_UPDATE | CDA_DATAREF_OPT_PASSIVEMON
.
MODE_PASSIVEMON
=1<<2, а
MODE_RQ_LOCK
передвинут в 4-й (чтобы 3-й был свободен, для
будущего потенциального 3-битового "режима мониторирования").
И он включён в MODE_MON_TYPE_mask
.
mode2upd_cond[]
расширена до 8 элементов;
единственный из новых осмысленный элемент [4]=CX_UPD_COND_PASSIVE
,
а остальные 3 -- NEVER.
12.07.2023: вот только УЧЁТ свежедобавленного битика
(взведение MODE_PASSIVEMON
при
CDA_DATAREF_OPT_PASSIVEMON
) при формировании
hi->mode
-- индекса в mode2upd_cond[]
--
отсутствовал, так что вместо PASSIVE хваталось ON_CYCLE. Учёт добавлен,
проблема ушла.
per_cycle_monitors_count
, так? Тогда всё становится не
так уж сложно!
CX_MON_COND_PASSIVE
равнозначной "парой" к CX_MON_COND_NEVER
. Всего 5 точек,
включая проверку на допустимость значения cond
в
GetChnd()
, ...
mp->conf
" в CHNEvproc()
, добавленного ещё
позавчера.
ЗЫ: кстати, этот режим "PASSIVEMON" аналогичен
CX_CACHECTL_SNIFF
из CXv2.
12.07.2023: пытаемся проверить, покамест аналогично проверке NOMONITOR -- при помощи утилитки test_PASSIVEMON.c и подсматривания tcpdump'ом.
В cda_d_cx_new_chan()
был косяк с трансляцией флагов в код
условия подписки (см. выше за сегодня), после исправления вроде всё OK --
характер прихода пакетов ожидаемый.
Теперь остался cda_d_insrv.c -- тоже та ещё радость...
12.07.2023@дорога из девятиэтажки домой, около стройки НГУ в лесу после Коптюга, ~18:10: насчёт реализации в insrv давно бродили мысли, но тут наконец в голове склалось определённо:
periodics[]
сразу, как уже и делается.
periodics_needs_rebuild
, взводить его при удалении hwr'ов,
которые не-{"немониторируемые"/"пассивные"}, а перед заказом чтения (по
CXSD_HW_CYCLE_R_BEGIN
) при взведённости выполнять
"пересоздание" списка и сбрасывать флаг.
13.07.2023: рытьё по записям нашло аналогичную идею "заполнить его заново" за 07-03-2018.
13.07.2023: а также замечено, что ровно та же
технология уже используется в cxsd_fe_starogate.c, где
пере-заполнение делается в RequestSubscription()
. Ну а оттуда
уже и в cxsd_fe_epics.c/cxsd_sfi.c оно очевидным образом
пробралось. Так что есть откуда копировать готовый код, только с УСЛОВНЫМ
помещением в список.
Делаем:
periodics_needs_rebuild
.
options
для хранения переданных при создании флагов.
RegisterInsrvHwr()
и UnRegisterInsrvHwr()
,
поскольку они не имеют доступа к hwrs_list[]
(и специально
определены в коде раньше, для изоляции от лишних деталей) добавлены
параметры options
для передачи этого значения.
periodics*
переделано на условное, причём
условие сейчас имеет вид
(options & (CDA_DATAREF_OPT_NOMONITOR | CDA_DATAREF_OPT_PASSIVEMON)) == 0
UnRegisterInsrvHwr()
добавлено
отсутствовавшее ранее
p->periodics_used--; p->periodics_needs_rebuild = 1;
InsrvCycleEvproc()
-- "добавлено"
пере-заполнение.
Но проблема в том, что итератору нужен доступ к options
для
условного записывания, а весь блок работы с Lcn'ами ИЗОЛИРОВАН от
hwrinfo_t
и cda_d_insrv_privrec_t
, так что не
может получить доступ к требуемой информации.
Поэтому реализовано было обходным способом, изрядно кривым:
RebuildPeriodics()
, живущей
в "основной" области и потому имеющей доступ ко всему нужному.
InsrvCycleEvproc()
.
Но для вызова надо передать указатель на серверный privrec, поэтому...
pdt_privptr
, ...
RegisterInsrvSid()
, которому добавлен
дополнительный параметр, передаваемый из cda_d_insrv_new_srv()
.
Вот так аукнулось отделение Lcn'ов от серверных "мозгов" плагина...
14.07.2023: осознал: для cda_d_insrv.c режимы NOMONITOR и PASSIVEMON совершенно эквивалентны -- даже в коде они просто OR'ом идут. В принципе, не страшно: главное -- пассивность, которая работает; а "траффик" внутри сервера экономить глубокого смысла нет.
14.07.2023: проверил, уже на rdreg_drv.c,
переведённом на cda_req_ref_read()
-- вроде работает как надо.
30.07.2023: A BIG FAT NOTE: ведь надобность в layer'е определяется не типом линка (serial/TCP), а ПРОТОКОЛОМ -- для rtu_over_tcp ровно так же понадобится уметь работать с ТОЛПОЙ адресов на одном сокете.
Так что потенциальное взаимодействие с потенциальными layer'ами ("modbus_lyr.h?) пока не вполне ясно.
22.06.2023: продолжаем.
last_sent
, и различие появилось :)
drvletinfo[]
),
а не отдельным куском, как в подобных, для чего она объявляется в конце
privrec'а как map[0]
.
16.07.2023: нашёл руководство по РПМ-416 (это "Анализатор электросети (регистратор) РПМ-416", который надо окучить), и там явным образом присутствуют каналы типа "ULONG" -- т.е., 32-битные (причём используемый там порядок байт не указан).
Так что конверсия потребуется прямо сейчас, а не "когда-нибудь потом".
17.07.2023: конверсию решено вытащить в отдельный модуль, сейчас названный "modbus_conv". О нём -- в его собственном разделе ниже.
21.07.2023: изучал устройство/функционирование "1-битных" сущностей -- coils и discrete inputs, и выводы следующие:
Поэтому:
Да, возможно, что в каких-то экзотических случаях понадобится читать 16-битные значения и рассматривать их именно как многобитовые, с какими-то преобразованиями, но это пусть будет делаться на уровне сервера и обработки данных.
CreateModbusPacket()
надо передавать НЕ
num1
("число 16-битных единиц"), а просто
m_data_size
, в байтах.
modbus_conv_def_t
указывать кроме "множителя"
num_cells
ещё и "делитель" -- сколько единиц каналов/регистров
лезет в 1 единицу данных (16-байтную? 8-байтную?...).
23.07.2023@утро-зарядка: не-не-не, нифига всё не так -- мы же постулировали, что «Никакая "конверсия" тут смысла не имеет»! Поэтому для 1-битовых (COIL,DSCR) надо просто всегда наводить соответствие "до 8 битиков лезут в 1 байт" и оное понимание иметь непосредственно в ПРОГРАММЕ-ЮЗЕРЕ (т.е., modbus_drv.c и в modbus_mon.c). И да, тут надо квантовать не по 16-битным, а по 8-битным байтам -- в Modbus оно так...
22.07.2023: кстати, "нюансик": команды записи возвращают лишь подтверждение "записано с такого-то адреса столько-то штук", но НЕ сами записанные данные. Так что в драйвере после команд записи надо сразу же отправлять парные команды чтения.
23.07.2023@вечер, после попытки изготовления modbus_tcp_iu.c: по опыту изготовления времянки/махарайки для РПМ-416, где читается сразу вектор из 108 ячеек и затем результат раздербанивается на значения для возврата, довольно очевидна необходимость оптимизации коммуникации -- надо иметь возможность указывать "выполняй такое-то чтение с такой-то периодичностью".
В пределе -- чтоб можно было указывать НЕСКОЛЬКО таких периодичностей, т.к. может требоваться либо чтение сущностей разного типа, либо из нескольких блоков адресов, либо просто в одну команду диапазон по числу регистров не влезет.
24.07.2023@утро, дорога на работу мимо бассейна ВЦ и ИПА: а раз так, то необходимо будет выполнять поиск "жертв нецелевых ответов" не просто по условию "совпадают тип и {адрес,длина}", а более общо: цикл делать всегда по всем каналам (а не до первого совпавшего) и по условию "совпадает тип и {адрес,длина} канала попалают внутрь диапазона {адрес,длина} пакета".
...но при ТОЧНОМ совпадении (когда найденный канал является единственным адресатом) цикл всё же прерывать -- это исключительно для "опоздавших" ответов.
28.07.2023@утро, мытьё посуды: а можно условие прерывания расширить: помнить "границы использованности", и если в итерации цикла используется кусочек данных с границы, то сдвигать её, а условием окончания станет встреча границ. Проблема -- если сначала "потребляется" кусочек из середины блока (например, ячейка [1]), то сдвига границы не произойдёт, и даже когда потом будет взят соседний кусочек с границы (например, [0]), то после этого потребления [1] так и не случится и граница так на нём и останется.
28.07.2023@утро, записывая предыдущее: вариант проще: считать "объём использованных данных", и когда он сравняется с "объёмом данных из пакета" -- то считать, что больше ловить нечего.
24.07.2023@утро, дорога на работу мимо ИПА и мыши: а ещё понадобится где-то место для дешифрирования данных из m_data в host-side-формат, как регистров (uint16 в uint32 и т.п.), так и 1-битовых значений, которые потенциально, в векторном формате, до ~2000шт int32.
Делать это в статических переменных -- ну точно нельзя (вызов возврата может повлечь цепочку callback'ов, приводящую к вызовам опять в этот же драйвер для этого же устройства и даже для этого же канала, а там уж не дай бог будет использоваться одно место в памяти для чтения и записи -- гарантированная порча данных).
map[]
.
24.07.2023: пока начинаем работать над парсингом
drvinfo
в текущем варианте цикла.
25.07.2023: код парсинга утащен в отдельную
modbus_conv.h::modbus_parse_spec()
-- собственно,
изначально планировалось именно так.
27.07.2023:
ProcessInData()
.
28.07.2023: пора активно окучивать
ProcessInData()
.
Соображения/наблюдения:
И да, в piv485_lyr_common.c в качестве "кода
ответного пакета" используется именно "запомненный код последнего
отправленного" -- pivdevinfo_t.last_cmd
-- ВСЕГДА, без
вариантов.
last_sent
для восстановления отсутствующей в ответе информации.
mbqelem_t
ОТСУТСТВУЮТ поля kind
, addr
и num1
,
ибо предполагается, что их значения всегда будут браться из описателя канала
-- me->map[chan]
, каковой chan
у нас в "текущем
контексте данных" всегда есть -- поскольку он присутствует в
mbqelem_t
, то доступен и mb_sender()
'у (из
отсылаемого qelem'а), и ProcessInData()
(из
last_sent
'а).
И это теперь выглядит кривовато -- получается, что необходимая для сравнения "соответствует ли полученный ответ предполагаемому каналу-получателю" информация ПАКЕТА будет браться из описателя этого же КАНАЛА.
А там, соответственно, неоткуда взяться "исходному каналу" для индексации
map[]
для вычитывания его addr
, и вообще оный
addr
команды имеет право не соответствовать НИКАКОМУ каналу.
kind
и
addr
прямо в mbqelem_t
и заполнять его в момент
постановки в очередь.
Работы:
mbqelem_t
добавляем поля kind
,
addr
и num1
.
mb_sender()
'е формирование пакета переведено на
использование этих полей (с былого лазанья в
me->map[qe->chan]
).
q_enq_x()
добавлено заполнени из map[]'а.
mb_eq_cmp_func()
теперь эта троица
учитывается, а вот chan
и vect_ofs
из сравнения
убраны.
vect_ofs
? Ведь оно однозначно вычислимо при заполненном
chan
(как addr-me->map[chan].addr
), а при его
отсутствии оно бессмысленно.
Убрано.
ProcessInData()
сразу после получения пакета добавлено
вычитывание в случае <0 addr
и num1
из
last_sent
.
Сделано по придуманному сегодня утром принципу -- берётся "объём данных из пакета", из него вычитается "объём использованных данных" от каждого подошедшего канала, и когда уменьшится до нуля -- цикл прерываем.
forget_last_sent()
, присваивающей не только
chan=-1
, но и addr=0xFFFF
с
num1=0xFFFF
.
Да, не очень хороший способ "забывания", т.к. адрес-то может легитимно
быть равен 65535, но лучше никак -- т.к. поля там uint16
.
С другой стороны, количество регистров ну никак не может быть 65535 (даже битовых), так что полное совпадение всё равно невозможно.
29.07.2023:
q_enqueue()
, получающего все данные
для qelem'а явным образом через параметры и уже НЕ лазящего в
me->map[]
. И q_enq_x()
переведена на него.
cur_trans_id
, которое в
оном enqueue увеличивается на 1.
ProcessInData()
-- всякие
дополнительные проверки добавлены.
02.08.2023: продолжаем.
ReturnOneChan()
: вызывает указанный для канала конвертер
и затем возвращает данные.
"Фокус" -- чья правильная реализация вызвала у меня сначала некоторый
ступор -- в том, что корректно учитываются num_cells
и
ext_per_cell
, если последний >1 (это для строк, в которых по
2 символа в UINT16-регистре).
Пока что возвращает только данные из 16-битных регистров, работа с 1-битным отсутствует.
03.08.2023:
_init_d()
сделано более корректное вычисление
num1
, с учётом ext_per_cell
и округлением вверх.
04.08.2023: в сделанное вчера ограничение по
modbus_max_num1()
добавлено "округление вниз" для кратности
conv
'ову num_cells
.
ReturnOneChan()
научена возвращать данные и 1-битовых
каналов. Тезисно:
buf[2008]
.
addr_ofs
...
0x80
(тогда она делается =0x01
, тем самым имитируя
циклический сдвиг байта).
04.08.2023: делаем необходимую работу с очередью.
q_sendnext()
-- просто переходник к
sq_sendnext()
.
q_esn()
-- "erase and send next", скопировано с
slbpm_drv.c::esn_pkt()
, только заполнение модели для
сравнения переделано -- используются (kind,op,addr,num1).
ProcessInData()
вставлено использование: при
proto==MODBUS_TCP -- просто q_sendnext()
(т.к. ставятся с
_ONS), а для прочих -- q_esn()
.
06.08.2023: мелкое изменение в
ReturnOneChan()
: определение "buf" переделано с
наuint8 buf[2008]; // For maximum number of coils/discretes to fit
-- чтобы гарантировать выравнивание даже для 64-битных данных. Из-за этого ссылки/обращения пришлось переделать с просто "union { // A weird yet portable way to force alignment float64 f64; uint8 u8[2008]; // For maximum number of coils/discretes to fit } buf;
buf
" на
"&buf
".
08.08.2023: первая проверка на "живом" девайсе.
DecodeModbusPacket()
, где по ошибке
стояло требование "nbts должно быть нечётным" (видимо, я при написании той
строчки счёл, что сам байт nbts входит в этот объём). Исправлено на "nbts
должно быть чётным" -- и данные начали приходить!!!
...т.к. в sq_sendnext()
, являющимся прямым адаптером к
perform_sendnext()
, отсутствует вызов deq_tout()
-- он делается только sq_foreach()
'ем по ERASE-запросам.
Проблема именно в используемой технологии "для TCP делать _ONS с таймаутом". Надо придумывать другой вариант, корректно ПОДДЕРЖИВАЕМЫЙ sendqlib'ом.
Хотя, чисто теоретически -- можно в sq_sendnext()
вставить
принудительный вызов deq_tout()
, из тех соображений, что вызов
этой функции уже автоматом предполагает принудительную отправку следующего в
очереди. И вреда это прочим применениям не причинит, т.к. во всех остальных
местах оно используется исключительно изнутри
*_erase_and_send_next()
/esn_pkt()
.
09.08.2023@утро-зарядка: можно выйти из положения проще: для TCP ставить также _INF, но с гигантским таймаутом, например, час или хотя бы 60 секунд -- тогда о задержках получения ответов из-за scheduling'а точно можно не беспокоиться, да и вроде-как-ненужная повторная отправка пакетов также будет уже пофиг, т.к. если устройство за столь большое время не ответило, то проблема точно не в доставке по TCP.
09.08.2023: продолжаем.
10.08.2023@дорога в ИЯФ, лесок между П28 и НИПС: вот только будет косяк с устройствами, которые не отвечают на не нравящиеся им пакеты -- оно будет зависать на 1 минуту до отправки следующего. Так что надо б что-то другое придумывать -- в идеале всё-таки научиться снимать таймаут перед sendnext'ом.
last_sent.chan>=0
.
ReturnOneRflags()
со
значением флагов в зависимости от кода exception'а.
Нюанс 1: ставится с условием SQ_IF_ABSENT
, а даже не
NONEORFIRST -- ведь нас устроит даже если первое в очереди, т.е., уже только
что отправленное (а в основном так оно и будет автоматом, т.к. после записи
будет ставиться обратное чтение).
Нюанс 2: для WRITEONLY-каналов (см. ниже) оно НЕ запрашивается.
ex_r2rflags[]
, где
ILLEGAL_FUNCTION => CXRF_CAMAC_NO_X
("не исполнено"),
ILLEGAL_DATA_ADDRESS => CXRF_UNUPPORTED
("нет такого канала"),
ILLEGAL_DATA_VALUE => CXRF_INVAL
(неверное значение), а всё остальное -- CXRF_CAMAC_NO_Q
("нет
подтверждения от устройства"). Подобрано худо-бедно логично.
Занадобилась она потому, что даже в РПМ-416 есть как минимум набор регистров 51-63 (пароль для разблокировки записи), которые можно только писать, а в ответ на команду чтения возвращается ошибка 2 ILLEGAL_DATA_ADDRESS.
Кстати, в drvModbusAsyn.c есть специальный
псевдо-код-операции
MODBUS_WRITE_MULTIPLE_REGISTERS_F23 223
-- при указании (в .db-файле? или в .cmd-файле?) значения
223
в параметре modbusFunction
вызова
drvModbusAsynConfigure()
драйвер считает это указанием на
write-only, а реально использует код команды 23
(это "Read/Write
Multiple Registers").
chaninfo_t
добавлено поле flags
.
CHAN_FLAG_WRITEONLY
=1<<0 (пока единственный
и хбз, понадобятся ли другие).
drvinfo++
, а до
modbus_parse_spec()
оно не доходит (ибо там этому делать
нечего).
02.10.2023: такой вариант убран, т.к. уже есть более общий суффиксом "(writeonly)".
DRVA_READ
попытки запросить чтение НЕ делается, а
сразу же делается ReturnOneRflags(,,,CXRF_CAMAC_NO_Q)
, ...
DRVA_WRITE
также НЕ запрашивается обратное
чтение сразу после записи (которой самой ещё нет :D).
Для этого понадобится буфер для хранения этих данных, а таковой буфер можно будет организовать только после перехода на 2-стадийный парсинг, чтобы аллокировать всё одним куском памяти.
ReturnOneRflags()
предназначена
для возврвта флагов наиболее "аккуратно".
modbus_conv_table[conv].ext_dtype
; для
1-битовых -- UINT8.
ReturnDataSet()
--
слизан с ReturnOneChan()
.
..........
Работает. И в EPICS делается ровно то же, проверено сравнением кода на тему "какой байт/байты берётся в каком случае".
Вопрос только, КАК указывать это write-only в drvinfo. Ведь оно нужно
ТОЛЬКО для драйвера, modbus_mon'у нафиг не надо, так что в
modbus_parse_spec()
этому делать нечего. Символ-префикс ('w'? '/'?), который указывать первым символом в
drvinfo?
11.08.2023: переходим на 2-стадийный парсинг конфигурационной информации.
stage
, ...
me->map[chn]
"stage > 0
") и...
remdrv_init_d()
.
Резолвинг делается только на stage==0, НЕ внутрь me
, а в
локальную переменную hostname
, которая в конце 0-й стадии
вместе с прочим сохраняется в свежеаллокированный privrec.
mreqinfo_t
,
содержащий
kind
,op
,addr
,num1
.
Т.е., в нём НЕ ПРЕДУСМОТРЕНО пока ничего на тему записи (каковая может
быть интересна для "on_conn=
" -- записью волшебного кода в
регистр-пароль разлочивать возможности управления).
on_hbt
и
on_conn
(которые будут указывать на соответствующие куски в
общем буфере), с количествами on_hbt_count
и
on_conn_count
.
(... + 15) &~15UL
".
Общая технология подсмотрена в cxsd_hw.c::CxsdHwSetDb()
.
(Долго думал о том, как бы сделать правильнее/элегантнее, чтоб делать добивку ровно до нужной кратности, но никак. Причина -- что оная "кратность" не совпадает с размером единицы данных (могущей быть, например, 3 штуки uint16) и непонятно, к какой степени двойки привязываться при выравнивании.)
CxsdHwSetDb()
делается для
current_val_bufsize
и next_wr_val_bufsize
.
12.09.2023@утро, мытьё посуды:
да нифига -- какие там "степени"?! Ведь для записи хранить надо будет уже
MODBUS-данные, а они исключительно 2-байтные (HOLD) и даже 1-байтные (COIL).
Так что выравнивать надо будет максимум "до следующего чётного адреса" --
(datasize + 1) &~1UL
-- для
первых, а для вторых вообще ничего делать не надо. Кстати, именно так вчера
и сделано в modbus_mon.c::PerformIO()
.
13.09.2023@утро, просыпаясь:
ага, а данные для возврата из writeonly-каналов?! Ведь там нужны как раз
ИСХОДНЫЕ данные! Как вариант -- можно вертать просто
результат обратной конвертации из modbus-данных;
ReturnOneChan()
как раз прекрасно это сделает.
q_enqueue(,SQ_IF_ABSENT,)
всем элементам on_hbt[]
,
причём дополнительным условием цикла является is_ready
, так что
если какая-то из отправок приведёт к обрыву соединения, то цикл тут же
прерывается.
Заодно добавлен аналогичный цикл по on_conn[]
в
InitializeRemote()
, но там в будущем надо будет ещё добавить
поддержку записи.
13.08.2023: проверяем.
auxinfo
вместо hostname
), но ничего критичного и
всё исправлено.)
Резюме:
Соответственно, и в on_conn[] запись пока никак.
14.08.2023: с другой стороны, инфраструктура-то для по-канальных буферов для записи уже подготовлена -- можно делать.
..........
(Задача возникла после того, как по ошибке указал в devtype НЕСКОЛЬКИМ именам одинаковые номера каналов -- так возникли дубли, из-за которых каналам указывались не те адреса регистров (приоритет имели те, что позже).)
Посмотрел я на то, как устроено всё с dcpr'ами (сохранение в них указанного в devlist'е, а затем актуализация в hw) -- а никак! Там, конечно, всё сделано элегантно на грани гениальности:
out<0-7>
",
указывается один и тот же dcpr_id),
CxsdHwSetDb()
идётся по обоим namespace'ам устройства
(type,chan) и каналу, указанному в очередной строчке namespace'а,
уставляются указанные в dcpr'е свойства.
В т.ч. впрямую прописывается
chn_p->dcpr_id = dcpr_id;
CxsdHwGetChanAuxs()
ВСЕГДА берёт drvinfo из
каналова dcpr'а, если оный
cxsd_hw_channels[gcid].dcpr_id > 0
.
Пока единственное, что приходит в голову -- в момент ЗАПИСИ dcpr_id в
строчку namespace'а пробегаться по всем предыдущим из него же и проверять,
нет ли строки с тем же devchan_n
и при этом
dcpr_id>0
.
17.08.2023: сделал. Именно тем способом. Поиск сделан
FindNspLineChanWithDcpr()
, возвращающей номер уже имеющейся
nsline с таким же devchan_n
и определённым dcpr_id
Формально можно оптимизировать, убрав "лишний" проход по namespace'у, т.к.
неподалёку перед ним есть поиск дублей посредством
CxsdDbNspSrch()
-- их можно объединить в один цикл, но придётся
передавать слишком много параметров.
14.08.2023: улучшаем парсинг auxinfo-параметров:
on_hbt[]
, как было сделано вчера, а список KEY=VALUE, чтоб
можно было указывать разные сущности.
isspace()
'ов просто делаются сравнения с предопределёнными
строками вида "keyword=" (да-да, прямо с '=' внутри).
num1
, посредством надлежащей арифметики
с conv'овыми num_cells
и ext_per_cell
-- актуально
в случае указания не просто регистров и количества, а сразу в терминах
"параметров" с конверсией; не забыто и ограничение по
modbus_max_num1()
.
Сама эта арифметика скопирована из парсинга drvinfo, и это не есть гуд -- надо бы обобщить, убрав дублирование.
15.08.2023@прогулка: насчёт раздербанивания на биты прямо в драйвере, как предположено 15-06-2023:
ВЫВОД: лучше внешними драйверами. Но тогда нужны более векторные, чтоб лебединости из 5 штук uint16 могли переделывать.
16.08.2023: фигня всё, написано было от непонимания и непомнящести предложенной 15-06-2023 идеи -- "регистр отображается на 1+N последовательных каналов, первый из которых является всеми битами, а следующие N -- побитовое представление"; в том варианте оба вопроса исчезают. Так что вчерашнее в основном "withdrawn".
Однако пара рациональных зёрен во вчерашнем есть:
@вечер: префиксом, ДО обычной
Modbus-спецификации регистра? И префикс должен начинаться с особого символа
-- например, с открывающей скобки (и закрываться закрывающейся), вроде
"(bitmap)
" (точнее, "(bitmap=NUMBITS
")?
17.08.2023@утро, зарядка: тогда уж
и "writeonly" можно туда же добавить, вместо '/'. А парсить
содержимое скобок -- psp_parse()
'ом, с
terminators=")".
20.08.2023@утро, просыпаясь: а чё
сразу "префикс"? Можно и СУФФИКС -- "HOLD:1000/8(bitmap=128)
".
Так оно удобнее с точки зрения проверки: к этому моменту уже известны kind и
conv.
16.08.2023: насчёт групповых запросов по
HeartBeat()
: всё-таки неприятно, что если в блоке регистров
есть неиспользуемые "дырки" (например, каналы "Тип параметра" у РПМ-416,
которые нам нафиг не сдались, но они по 1 регистру на каждые 2 регистра
данных), то цикл перебора каналов в ProcessInData()
вынужден
каждый раз пробегать ВСЕ каналы (т.к. num1_left
никогда не
обнулится, раз в нём есть ненужное).
mreqinfo_t
, а потом умудряться передавать и в
mbqelem_t
, и чтоб mb_sender()
автоматом сохранял
бы это в last_sent
, чтоб информация становилась доступна
ProcessInData()
.
Но встаёт вопрос -- КАК указывать каналы? Номерами -- некузяво; именами? Но тогда нужен API для возможности поиска "внутри указанного устройства" (себя) по имени (как думалось для битовых отображений регистров). А он есть?
@вечер: Есть ВНУТРЕННЯЯ для
cxsd_db.c FindChanInDevice()
, делающая пару
CxsdDbNspSrch()
по обоим namespace'ам (chan и type). Её
опубликовать несложно, но надо учитывать, что она НЕ проверяет для числовой
спецификации (.NNN) попадание в диапазон устройства, т.к. это делает её
хозяйка CxsdDbResolveName()
.
18.08.2023: сделана, GetDevChanByName()
;
подробнее см. в разделе cxsd_driver за сегодня.
HOLD:101/107:0-35
"?
@дома, ~14:20: или лучше
'>' -- ">FIRST-LAST" -- так меньше мельтешит двоеточий и
выглядит мнемоничнее (указываются "получатели").
А вообще такая спецификация начинает походить на используемое в
last_sent.chan
почти до степени унификации: если указать ОДИН
канал в диапазоне, то оно и выйдет. И, с другой стороны, если при обычной
отправке по-канальных запросов указывать first=last=chan, то блок перебора
сможет обрабатывать и ответы на по-канальные запросы.
19.08.2023: парсинг dest_first
и
dest_last
подготовлен (но НЕ сделан -- ни
strtoul()
нет, ни парсинга имён с
GetDevChanByName()
после).
20.08.2023: ...и за-#if 0
'ен.
22.08.2023: за последние 2 дня накидана работа с "битовыми отображениями" по проекту от 16-08-2023 (а записывается всё 06-01-2024, ибо тогда было дико лень от усталости):
chaninfo_t
добавлено поле bitmap
, которое
если >0 -- то это количество следующих за данным "подчинённых" ему
каналов.
text2chanopts[]
.
CHAN_FLAG_WRITEONLY
) и bitmap=NUMBITS
(NUMBITS=0..32, по умолчанию =0).
opts.bitmap>0
): что тип INPR или HOLD и что конверсия
UINT16; что затребованное число бит не превышает num1 * 16
; что
затребованные каналы не вылезут за границу имеющихся у устройства.
.base_chan=chn
, а подчинённым каналам прописывается
.base_chan=idx
(они сами) и .kind=kind
(это чтоб
они потом не игнорировались).
(Напоминание: у незадействованных в bitmap'ах каналов .base_chan=-1
-- делается в момент аллокирования памяти.)
base_chan
прописывается
ТОЛЬКО для подчинённых каналов и в нём лежит номер "базового" (а не ихний
собственный, как позавчера). И у самого базового --
.base_chan=-1
, так что тут он не отличается от обычных, никак
НЕ задействованных в bitmap'ах.
modbus_tcp_rw_p()
:
chn
на bcase_chan
.
ReturnOneChan()
для INPR/HOLD-каналов после
m2h()
и возврата при .bitmap > 0
подготовлен
(пока он за-#if 0
'ен) несложный цикл (по образу и подобию
возврата 1-битных каналов ниже), раздербанивающий данные на биты и
возвращающий их в "подчинённых" каналах.
Возврат делается ПОШТУЧНО, одиночными ReturnInt32Datum()
, а
НЕ одним ReturnDataSet()
на всё.
Есть неприятное впечатление, что это всё же нечто лишнее в теле драйвера: занимает очень дофига места, а, формально, могло бы выполняться внешним драйвером, подписывающимся на каналы "реального".
24.08.2023: вчера доделана собираемость, а теперь проверяем "не сломалось ли что" и насколько корректно работает парсинг.
Надо бы всё-таки менять имена "bitmap
" и
"base_chan
" на что-то другое -- и более адекватное, и покороче
(желательно 4 символа для унификации с прочими членами
chaninfo_t
).
@холл перед залом круглого стола, ~17:00:
"srcc"? "truc" (TRUe Channel)?
25.08.2023@ключи, домик, веранда-предбанник/крыльцо, ~17:00: "hwch" -- HardWare CHannel, т.е., "реальный аппаратный канал"; не совсем "исходный" или "хозяин/владелец", но вполне годно.
25.08.2023@вечер: да,
переименованы: "base_chan"->"hwch
",
"bitmap"->"bitx
" (ВЕЗДЕ -- и в коде, и имя параметра-опции).
12.09.2023: приступаем к реализации поддержки записи.
13.09.2023@утро, просыпаясь: учитывая, что бывают регистры, которые не только writeonly, но ещё и не имеет смысла возвращать/помнить последнее записанное туда значение -- конкретно команды, вроде командного регистра HOLD:50 у того же РПМ-416 -- то стоит ввести дополнительный тип-флаг -- "null".
А "возвращать" результат записи по получению подтверждения можно с
помощью ReturnOneRflags(rflags=0)
-- как раз получится ровно
нужный результат.
13.09.2023:
20.09.2023: найден позорный баг в аллокировании
privrec'а: размер собственно privrec_t
не учитывался никак.
Как ни странно, проявлялось весьма умеренно: просто некий
незадействованный канал считался "определённым" с kind=4 и addr=100/num1=108
(вследствие попадания on_hbt[]
на map[]
);
глобального же порченья содержимого privrec'а почему-то ("везение"?) не
происходило.
Исправлено -- теперь вместо
используетсяmap_size = (sizeof(me->map [0]) * numchans + 15) &~15UL;
-- заодно исчезла шизофрения (реально недоделанная! :D) "при аллокировании перед map[]'ом есть добивка до кратного 16, а адресация идёт кme_size = (sizeof(*me) + sizeof(me->map [0]) * numchans + 15) &~15UL;
me->map
, который с другим выравниванием".
21.09.2023: bitx
теперь можно указывать
не только UINT16-каналам, но и любым INT размером 1, 2 или 4 байта.
word32
и mask32
.
maskhi
--
0x80000000
,0x8000
,0x80
-- и
запоминается этот размер.
mask32 == 1
; тут же делается и
инкремент указателя чтения, ...
uint8 *rp
-- т.е.,
просто байтовый.
if (mask32 == maskhi) mask32 = 1; else mask32 <<= 1;
Такая организация цикла даже короче и нагляднее -- благодаря
ЕДИНСТВЕННОМУ чтению, хотя и за счёт двух if()
'ов вместо былого
одного (плюс if-селектор перед циклом, но это из-за поддержки данных
разного размера).
opts.bitx > num1 * 16
, но надо бы сравнивать не
num1 * 16
, а sizeof_cxdtype() * 8
.
24.09.2023: вычисление потребных num1
для
конкретного nelems
и обратно переведены на
modbus_num1_of_nelems()
и modbus_nelems_of_num1()
.
28.09.2023: заготовки для поддержки RTU и ASCII --
аналогично сделанному в modbus_mon.c: реакция на
FDIO_R_HEADER_PART
плюс вызовы fdio_register_fd()
с чуть разными параметрами в зависимости от протокола.
Применимость этого нулевая -- из-за необходимости layer'а; сделано чисто для унификации.
02.10.2023: немножко по мелочи:
modbus_bits_h2m()
и
modbus_bits_m2h()
соответственно.
19.03.2025: в modbus_bits_h2m()
был косяк: в
case
-альтернативах 1,2,4 (байтовый размер исходных данных)
отсутствовали "break
", что должно было приводить к
"проваливанию" в следующие ("fallthrough").
Чуток анализа:
PerformIO()
парсинг выполняется в буфер UINT8, запись COIL'ов работать не могла в
принципе.
case 4:
" в преобразовании стоит последней.
Замечено вчера, в присланном ЧеблоПашей
(Message-ID:00ef07ec-4a9c-4043-9de2-e39030c0e49c@inp.nsk.su) логе сборки
каким-то более новым GCC, имеющим ключ
"-Wimplicit-fallthrough=
", включающийся по "-W
"
(ныне именуемом "-Wextra
").
Исправлено. Также проведено сравнение старой версии и новой (посредством
"-Dd /dev/ttyUSB0 COIL:0xAA55/8=1,0,1,0,0,1,0,1
"
и глядением на данные): да, старая версия писала неверные числа (странные --
видимо, на основании мусора в стеке), а новая пишет верные (байт 0xa5).
03.10.2023: доделана поддержка записи -- пока
примитивная и халтурная, 1 элемент -- для on_conn
и
on_hbt
.
Кстати, может, использовать таки парсинг командной строки из
console_mon_util.h? Но тогда придётся там заменить диагностику
посредством fprintf(stderr,...)
'ов на какой-то API.
04.11.2023: смотрел на описание "PLC управления термостабилизацией" от Жарикова; там управление включением/выключением отдельных нагревателей делается отдельными БИТИКАМИ в одном регистре.
Сейчас-то можно это реализовать посредством wrreg_drv, но в идеале надо
бы иметь "нативную" поддержку -- чтоб спецификация "bitx=NN
"
работала бы и с каналами записи тоже.
@вечер, засыпая: надо будет на каждый такой
"базовый" канал иметь толпу буферов: как минимум -- cur_val, req_val,
req_msk. Аллокировать-то их в общем буфере (вместе с privrec'ом); но вот
держать указатели на эти под-буфера в chaninfo_t
КАЖДОГО канала
(которых легко могут быть сотни, а то и тысячи) -- это уже совсем
неприличная трата памяти. Поэтому надо вместо толпы
указателей завести ОДИН int
-"offset", указывающий на смещение
всей толпы подбуферов этого канала внутри специального буфера; а уж сами
под-буфера пусть идут друг за дружкой, так что указатель на N'й из них
вычисляется как "wr_buf+data_ofs+N*one_size"
05.11.2023: а уже есть в chaninfo_t
поле
data_ofs
-- для каналов записи там хранится offset внутри
me->wr_buf
.
bitx>0
аллокировать не по datasize
байт, а по
datasize
*(1+N), где N -- число требуемых дополнительных
под-буферов.
chaninfo_t
и поле "datasize", для
упрощения арифметики.
num1*2
: ведь bitx-каналы отображаются на 16-битные
регистры, поэтому (вне зависимости от типа конверсии и типа исходных данных)
объём каждого под-буфера будет равен объёму регистрового блока, т.е. -- num1
16-битных слов.
16.12.2023: во время написания "отчёта по госзаданию" в 20231212-OTCHET-PO-GOSZADANIYU-2023.txt накатал туда фразу
Спецификация отображения канала на Modbus-регистры указывается в свойстве drvinfo-- "в СВОЙСТВЕ"!!!
А ведь недавно смотрел, как в Tango устроена работа с Modbus, и там тоже такие вещи указываются в PROPERTIES (только устройства или атрибутов -- надо перепроверить; вроде бы устройства, а у нас-то КАНАЛОВ).
Но сходство в любом случае налицо. Конвергентная эволюция? :D
01.01.2024: ЗЫ: ещё пара замечаний в ту же степь (сообразил давно, но руки дошли записать только сейчас):
08.02.2024: сделано, вслед за modbus_mon.c,
что "неожиданные" пакеты не приводят к "переходу далее" -- ни
forget_last_sent()
не делается, ни q_esn()
.
is_expected
, вычисляемая так же, и она так
же сделана условием исполнения тех двух действий.
addr
и num1
из
last_sent
'а теперь по ней.
Вот только выглядит последовательность действий сейчас не очень красиво -- какая-то каша получилась, не очень хорошо структурированная.
13.02.2024: обнаружилась странность: и
modbus_tcp_drv, и modbus_mon (14.02.2024: нифига
-- ТОЛЬКО драйвер) почему-то в запросах постоянно шлют sync_seq=0,
хотя должны бы слать увеличивающиеся числа.
(Обнаружилось при анализе wireshark-дампа того прикола Сенькова, когда он иногда неожиданно на запрос COIL/1 присылал сначала ответ на HOLD/3, а потом EXCEPTION -- хотелось посмотреть на значение sync_id в этих ответах и сравнить с запросом; а вот фиг -- везде нули...)
@вечер, дома: а вообще-то именно по значению sync_id в ответе можно было бы дополнительно проверять соответствие ответа запросу. Конечно, ТОЛЬКО для Modbus-TCP.
14.02.2024: неа, ТОЛЬКО modbus_tcp_drv -- там в
mb_sender()
в вызове modbus_create_req_packet()
всегда передаётся 0
, а cur_sync_id
, в
q_enqueue()
инкрементируемое, никуда не попадает. ...хотя
вообще некий вопрос -- а там ли надо инкрементировать, или же надо это
делать при ОТПРАВКЕ (чтобы повторные запросы имели бы разные sync_id)?
Монитор же себя ведёт корректно -- шлёт увеличивающиеся значения;
проверено сначала "вхолостую" (запустив nc на порту 2000 на x10sae, монитор
с ulan-ude-els натравив на "x10sae:2000" (при запуске на одном узле сниффер
пакетов не видел) и глядя на дампы пакетов от -Dd
плюс на лог
wireshark'а -- данные совпали), а затем на реальном Сенькове.
15.02.2024: сделал, совсем по-простому:
q_enqueue()
инкрементирование убрано, ...
mb_sender()
добавлено прямо перед
modbus_create_req_packet()
, ...
(17.02.2024: переставлено в ПОСЛЕ, чтобы унифицировать поведение с modbus_mon'ом; и да, поэтому теперь начинается с 0, а не с 1.)
cur_sync_id
вместо
былого 0
.
last_sent
-- просто
некуда, т.к. в типе mbqelem_t
(коим этот last_sent является)
такого поля нету.
И-и-и -- помогло!!! Теперь обмен идёт инкрементирующимися идентификаторами (проверено wireshark'ом).
0x.xx.2024:
Непосредственная причина -- дурная прошивка ВИП-45, в которой, похоже, за одним IP-соединением "скрывается" несколько устройств. И вообще-то такое поведение возможно и по стандарту ("шлюзы").
07.12.2023: обсуждение:
modbus_parse_spec()
умеет понимать опциональный префикс
"+UNIT_ID" и возвращать его (а при неуказанности -- -1
), только
надо флаг MODBUS_PARSE_FLAG_UNIT_ID
взвести.
chaninfo_t
очередное поле
-- unit_id
.
unit_id
и к mbqelem_t
.
q_enqueue()
добавить параметр
unit_id
для складирования туда.
q_enq_x()
и в
modbus_tcp_rw_p()
с ProcessInData()
(в обработке
exception'ов) -- передавать в этом параметре значение
me->map[chn].unit_id
.
mb_sender()
проверять значение этого
qe->unit_id
, и если >=0
, то использовать
вместо me->unit_id
.
unit_id
и к mreqinfo_t
,
при парсинге взводить MODBUS_PARSE_FLAG_UNIT_ID
и сохранять
результат, а в InitializeRemote()
и HeartBeat()
передавать значение on_NNN[n].unit_id
в
q_enqueue()
.
mb_eq_cmp_func()
не забыть добавить в цепочку
сравнения -- тут уж просто и безусловно.
08.12.2023: с другой стороны -- такие фокусы с
per-register-указанием UNIT_ID имеют смысл только для MODBUS_TCP, т.к. для
RTU и ASCII адрес является именно адресом отдельного устройства,
обслуживаемого отдельным драйвером. Поэтому можно просто
запрещать указание "+UNIT_ID" для proto!=MODBUS_TCP -- например, флажок
MODBUS_PARSE_FLAG_UNIT_ID
не взводить.
11.01.2024@вечер, засыпая: а ещё один вариант -- реализовать это посредством LAYER'а! Т.е., рассматривать каждый IP:PORT как "порт", на котором может висеть до 254 (1-254) unit'ов. Раз уж всё равно для serial-линков когда-нибудь понадобятся layer'ы, то механизм-то по сути тот же самый.
Да, сделать-то так можно, но как же это будет в реализации муторно и громоздко! Ведь нужно будет как-то маппировать "адреса" IP:PORT на внутренние handle'ы (аналогично cankoz_lyr и piv485_lyr, но там-то это композиция line:kid, а тут "line" вовсе не число...)...
17.01.2024@по дороге из девятиэтажки в Жар-Птицу, между Детским проездом 7 и 9, ~17:40: с другой стороны -- можно ВСЕГДА работать через layer'ы:
При этом, собственно, и исчезает проблема «нужно будет как-то
маппировать "адреса"...» -- в fdio_register_fd()
будет передаваться privptr2=ptr2lint(line_id), так что он будет вертаться
notifier'у готовеньким.
Да, будет более муторно, чем сейчас; да, собственно драйвер чуток усложнится. С другой же стороны, изменения по сути чисто административно-бюрократические, "мясо" работы с Modbus вряд ли сильно затронут.
05.11.2024@ИЯФ, лестница вниз в пристройке, перед 3-м этажом, ~12:10: очевидно, что "код установления и восстановления соединения" (который можно условно, по аналогии с AsynDriver, назвать "портом") будет потенциально годным вообще для ВСЕХ serial-устройств.
ProcessIO()
" обрабатывала бы только
reason
'ы соединения:
FDIO_R_CLOSED
/FDIO_R_IOERR
(хотя в
modbus_mon.c по ним rpy_tid
трогается),
FDIO_R_CONNERR
,
FDIO_R_CONNECTED
-- неэксклюзивно, т.к. по нему и
прикладному протоколу надо действия выполнять,
И да, в AsynDriver так и сделано -- там это функциональность "порта", вне зависимости от того, что за данные мы через этот порт гоняем; поэтому конкретным драйверам должно быть безралично даже, serial это порт или TCP.
06.11.2024: дальнейшие соображения по теме:
И для MODBUS_MEDIA_SERIAL
должен выполняться поиск по
сигнатурам, а если не найдено -- то брать "открыватель" от "ttyS",
как бы "умолчательный" (он, кстати, будет годиться почти для всего --
ttyUSB*, Advantech PCIE-1612C-AE, ...).
MODBUS_MEDIA_SERIAL
=0 и MODBUS_MEDIA_TCP
=1
-- очевидно, их надо менять местами первым же шагом.
07.11.2024: поменяны (и код чуток адаптирован, чтоб
сравнение ВСЕГДА делалось с нулевым значением -- теперь это
MODBUS_MEDIA_TCP
(раньше тоже в основном с ним, но не всегда;
теперь -- всегда)).
04.12.2024: и в modbus_tcp_drv.c переделано аналогично. Его в будущем трогать не будем, но уж это пусть останется унифицированным.
Главный же пока что вопрос -- откуда будут браться сами реализации "открывателей":
DEVICE_TYPE/modbus@modbus
".
conn_types[]
-- желательно унифицировать с
modbus_mon.c, как, естественно и константы
MODBUS_MEDIA_*
.
...а лучше сразу обобществить всю ParseDevSpec()
.
Для kshd485@NNNpiv485 это всегда было B9600
, но теперь-то
надо уметь указывать.
layerinfo
-- для того он и
делался.
Но при указании не номера, а прямо имени/пути COM-порта это становится невозможно: синтаксис
layerinfo /dev/ttyS0 19200,n,8,1
не прокатит, т.к. параметром "имя" должен быть идентификатор максимум с
'-', но БЕЗ слэшей.
modbus_mon
--
через опциональный префикс "@19200,n,8,1:/dev/ttyS0
" прямо в
auxinfo.
А проблему "что, если у разных устройств одной линии разные параметры?"
решать по принципу "кто первый встал, того и тапки": значения от устройства,
вызвавшего открытие "порта", будут использоваться, а у следующих будут
сравниваться (по-элементно!) с использованными первыми и при несовпадении
устройству будет даваться отлуп по CXRF_CFG_PROBL
.
@вечер: а вообще напрашивается
просто layerinfo_parser()
переделать так, чтобы он и слэши
позволял.
08.11.2024@утро-завтрак: или просто указывать там имя устройства БЕЗ "/dev/" --
вроде
"layerinfo ttyS0 19200,n,8,1
".
Криво, некрасиво, но прокатит.
08.11.2024: первый шаг -- определения из
modbus_conv.h скопированы в свежесозданный modbus_defs.h
-- и определяемые протоколом Modbus, и внутренние константы вроде
MODBUS_KIND_*
, MODBUS_OP_*
,
MODBUS_CONV_*
.
23.12.2024: а MODBUS_CONV_*
-то зачем? Это
ведь не "высокоуровневая абстракция протокола Modbus", а внутренняя
функциональность modbus_conv.h. Поэтому пока
закомментировываем.
11.11.2024: хи -- ровно аналогичный вопрос возникал ещё летом, 06-08-2024, и вылился в раздел "remote-serial-connection".
19.12.2024: приступаем к реальным работам, но описание пойдёт уже в их собственном разделе ниже.
22.02.2025: а потенциальную реализацию нижних слоёв "ПЯТИслойного стека модулей" -- "драйвера порта" (теперь линии) и "открывателей" будем записывать в разделе "remote-serial-connection".
This initial read operation is normally done at the same Modbus address as the write operations. However, Wago devices are different from other Modbus devices because the address to read back a register is not the same as the address to write the register. For Wago devices the address used to read back the initial value for a Modbus write function must be 0x200 greater than the address for the write function. This is handled by adding this 0x200 offset for the readback address if the plcType argument to drvModbusAsynConfigure contains the substring "Wago" (case sensitive). Note that this does not affect the address for Wago read functions. The user must specify the actual Modbus address for read functions.
Ну и что будем делать? Решение ведь желательно такое, чтоб работало и в драйвере, и в modbus_mon'е.
P.S. Обнаружилось благодаря аспиранту Карнаева по имени Артемий Коваленко, чью аспирантскую работу я прочитал, будучи его рецензентом, и потом погуглил "epics wago 0x200".
13.06.2024: "что будем делать" -- учитывая, что:
modbus_create_req_packet()
.
op==MODBUS_OP_READ
) к адресу,
передаваемому в modbus_create_req_packet()
, ...
...а значение этого параметра взводить в 0x200
при указании
того ключика (ну или ключик "read_addr_offs", которому прямо число и
указывать).
14.06.2024@утро, завтрак: с другой стороны -- а МОНИТОРУ-то это зачем? Там всё равно команды чтения и записи указываются ОТДЕЛЬНО, ну так и ставить командам чтения адреса с +0x200.
15.06.2024: делаем.
read_addr_offs
, оно
int16
-- знаковое, т.к. смещение может и отличаться.
16.06.2024: изучаем.
pPlc->readbackOffset
writeUInt32D()
при коде функции
MODBUS_WRITE_SINGLE_REGISTER
, а читает при этом функцией
MODBUS_READ_HOLDING_REGISTERS
, и вот именно ЕЙ сбагривается
адрес плюс pPlc->readbackOffset
.
И всё, более нигде: ни в readUInt32D
, ни в
writeInt32()
, ни где-либо ещё.
"Признаки" -- это вышеописанное поведение writeUInt32D()
плюс тема
"Wago Card Failure"
на Automation & Control Engineering Forum за 2007г, где встретилась фраза
You can read back the outputs (not set them) by doing a read FC to an address 0x200 above the actual output address.
В странном тексте "Modular I/O-System ETHERNET TCP/IP 750-342" на стр.110(PDF:116) есть таблички "Register (Word) Access Reading (with FC3 and FC4)" с и "Register (Word) Access Writing (with FC6 and FC16)"
ХРЕНЬ КАКАЯ-то............
Соображения архитектурного характера -- куда/как добавлять такую "поддержку":
modbus_create_req_packet()
передавать адрес со смещением, да и
все дела.
ProcessInData()
тоже надо учитывать это смещение.
16.07.2024: неа, НЕ надо: смещение надо только в
отправляемом пакете делать, а в last_sent
сохранять ИСХОДНЫЙ
адрес.
q_enqueue()
, т.к. у него больше
информации о контексте, в т.ч. есть ссылка на канал.
Отсюда выводы:
16.07.2024@утро-просыпаясь: насчёт проблемы "при обратном маппировании" -- что сравнивать придётся с учётом смещение: неа, НЕ придётся.
modbus_create_req_packet()
, как и запланировано (записано
16-06-2024).
last_sent
надо сохранять ИСХОДНЫЙ адрес.
last_sent
, так что проблемы нет.
Но нам-то смещение нужно прибавлять ТОЛЬКО ПРИ ЧТЕНИИ, а в тех ответах адреса нет.
Чуть позже: но для исполняемого и после ответов на запись
q_esn()
("erase_and_send_next") этот addr
всё же
роляет. Так что всё же НАДО будет учитывать и вычитать смещение из адреса
конкретно у ответов на чтение.
И вот это всё делает реализацию довольно простой: а) при отправке смещение прибавить, б) при получении НЕКОТОРЫХ ответов смещение вычесть.
@~11:20, по дороге из дома в ИЯФ:
а вычитать это смещение надо в месте, где при неполученности адреса из
ответа он берётся из last_sent
--
if (addr < 0) addr = last_sent.addr;
-- только в else
к этому if()
'у, т.к. именно тогда
адрес таки БЫЛ получен из ответа и нуждается в корректировке.
Основной же вопрос остаётся скорее идеологическим -- "КАК оно там у Wago устроено, в КАКИХ случаях это смещение вообще присутствует?".
16.07.2024@вечер, ~18:00: не очень понятно "что", но зато стало понятно "как", в предположении, что "что" -- это читать HOLD-регистры по адресам на 512=0x200 выше, чем писать.
Так что делаем.
read_addr_offs
в
wago_hold_read_addr_offs
-- так название выглядит максимально
соответствующим смыслу.
_init_d()
при наличии в auxinfo ключа wago
(БЕЗ '='!) делается wago_hold_read_addr_offs = 0x200
modbus_create_req_packet()
при
MODBUS_KIND_HOLD,MODBUS_OP_WRITE адрес передаётся с прибавлением оного поля.
modbus_decode_rpy_packet()
, в точке, где ясно,
что адрес был получен из ответа и ответ этот на
MODBUS_KIND_HOLD,MODBUS_OP_WRITE -- из адреса оное поле вычитается.
Вот и всё. Было б на чём проверить...
17.06.2024: дошли руки посмотреть даденный 09-07-2024 ЧеблоПашей файл m07500852_00000000_0en.pdf с title'ом "Handbuch 750-852" -- более дословно
-- с рекомендацией "Начиная с 76 страницы.", которая раздел "7.2 Process Data Architecture".WAGO-I/O-SYSTEM 750
Manual750-852
ETHERNET ECO Controller
PLC - ETHERNET Programmable Fieldbus Controller
ECO
Version 1.3.0
In addition, the output data can also be read back with an offset of 200hex (0x0200) added to the MODBUS address.
A memory range of 256 words (word 0...255) is initially available in the controller for the process image of the physical input and output data.
For the image of the MODBUS/PFC variables, the memory range of words 256...511 is reserved; meaning the image for the MODBUS/PFC variables is created behind the process image for the I/O module data.
If the quantity of module data is greater than 256 words, all the physical input and output data above this value is added to the end of the current process image in a memory range; i.e., attached behind the MODBUS/PFC variables (word 512...1275).
The EtherNet/IP PFC variables are then mapped behind the remaining physical I/O module data.This memory range includes words 1276 ... 1531.
The subsequent range, starting from word 1532, is reserved for future protocol expansion and other PFC variables.
Data > 256 words can be read back by using the cumulative offset!All output data greater than 256 words and, therefore located in the memory range 6000hex (0x6000) to 66F9 hex (0x66F9) can be read back with an offset of 1000 hex (0x1000) added to the MODBUS address.
Откуда выводы:
wago_hold_read_addr_offs
надо переименовать
из "read" в "readback" -- wago_hold_readback_addr_offs
-- для
отражения факта, что это касается "обратного вычитывания"
Раздел располагаем тут, не в хронологическом порядке, а сразу после modbus_tcp_drv.c'шного.
Все действия по собственно рефакторингу будем описывать прямо в "корне" подраздела, а порторм -- после первоначального рефакторинга -- можно и в level5-списках собственно драйвера и layer'а.
Если вдруг когда-либо возникнет потребность в какой-либо иной работе с Modbus-устройствами
21.12.2024@утро, просыпаясь: и вообще, modbus_lyr -- это "драйвер шины", а какие драйверы могут пользоваться его услугами -- вопрос ортогональный.
Практический вывод из этого глубоко теоретического соображения: разделение между modbus_lyr и modbus_drv нужно реализовывать так, чтобы на стороне layer'а была ТОЛЬКО "работа с шиной", т.е., connect и отправка/получение пакетов. А вся интерпретация данных -- в т.ч. даже и обработка пакетов exception -- на стороне драйвера.
19.12.2024: создаём modbus_lyr.h пока в минимальном варианте, беря за основу cankoz_lyr.h как наиболее похожий.
add()
--
ModbusAddDevice
-- и полностью отсутствующими методами
q_enq*()
.
data_ofs
-- это "адрес" (смещение в общем
wr_buf[]
) данных для команд записи.
Но layer-то вроде не должен бы иметь доступа к драйверовым спискам каналов -- это не просто не его область, а вообще принцип разделения ответственности этому препятствует.
Как будем решать?
20.12.2024: результаты исследования вчера:
data_ofs
-- да, данные для команд записи, ибо
может быть много и прямо в qelem пхать нельзя.
С учётом этого:
add()
.
add()
'е же передавать указатель на драйверов общий
буфер для данных, в котором смещением этот по-qelem'ный
data_ofs
.
@утро-душ: и сразу отдельный вопрос про менеджмент структур данных "списки драйверов" в layer'е.
(Как в cankoz_lyr_common.c каждый описатель "порта" в массиве из
lineinfo_t
содержит массив
kozdevinfo_t devs[DEVSPERLINE]
-- описателей устройств.)
-1
-- "неиспользовано", а >=0 --
handle?
Но, в принципе, можно даже и так оставить -- что лежащее в
ячейке [unit_id] значение "индекс в сквозном массиве" будет ВНУТРЕННИМ для
layer'а индексом, наружу никак не светящимся. 21.12.2024: да, так и заложено -- в lineinfo_t
поле int used[255];
является тем самым массивом индексов.
dev_list[]
.
...хотя будем ещё посматривать -- вдруг какое соображение вылезет.
21.12.2024: также приступаем к изготовлению modbus_drv.c -- посредством копирования из modbus_tcp_drv.c и модификацией.
И стало ясно, что:
is_ready
;
InitializeRemote()
,
выполняя все "on_conn=
"-посылки.
add()
'ится к уже готовому к работе порту
(это случай локальных /dev/tty* и рестартов по
_devstate=0
), то ему должно СРАЗУ вызываться
"ReadyStateChanged(ready=1)".
me->handle
ещё не
прописан -- add()
ведь ещё не завершился -- то в методе должно
использоваться переданное значение handle
от layer'а.
@вечер, записывая: ...где-то
такое недавно было. Нашёл: cdaclient.c от
11-07-2024, значение rp->ur.ref
,
RememberOneChan()
маркирует значением
CDA_DATAREF_ERROR
, и ProcessDatarefEvent()
прописывает значение из переданного параметром если всё ещё
неинициализировано. Пришлось менять код из-за Tango.
03.01.2025: да, сделано, что метод
ModbusReadyChangeProc
получает значение handle
--
в отличие от всех остальных.
05.01.2025@утро, завтрак:
а ведь ячейки в dev_list[]
и в line_list[]
связаны
отношениями "queue-port", соответственно -- хранят указатели друг на дружку.
Поэтому НЕЛЬЗЯ их размещать в realloc()
'ирующемся куске памяти.
Так что оба переведены с GENERIC_SLOTARRAY_DEFINE_GROWING
на
GENERIC_SLOTARRAY_DEFINE_GROWFIXELEM
.
Это всего лишь второе применение такого варианта -- первое в CXv2'шной cda.c, не менявшейся с 11-09-2014.
08.01.2025: промежуточные результаты за последние несколько дней.
on_hbt=
"/"on_conn=
") и drvinfo -- в обоих случаях
для определения того, сколько Modbus-элементов будет в запросе и
ограничения, если оно не влазит.
modbus_num1_of_nelems()
, в
свою очередь сбагривающей его в modbus_max_num1()
.
В качестве временного решения СЕЙЧАС захардкожено значение
MODBUS_TCP
.
12.01.2025: да, в lvmt добавлен метод
proto_of_spec()
, возвращающий протокол по имени, а в драйвер
вставлен его вызов. Реализация метода уже есть -- простейшая
modbus_proto_of_spec()
, являющаяся переходником к
parse_spec_prefix()
; и уж последнюю будет вызывать и парсинг
имени порта в add()
'е.
modbus_add()
-- выглядят
двусмысленно/неоднозначно/неконкретно.
23.01.2025: мозговой штурм подобрал синонимы "conn" (connection), "line" и "interface" (последняя идея от ЧеблоПаши).
25.01.2025@вечер ~17:20, по дороге в Добрянку+быстроном, Николаева после перехода Лаврентьева: а ещё "link" -- как в Ethernet (про Ethernet и WiFi думал).
25.01.2025@вечер ~18:20, по дороге из Быстронома, от Инженерной вдоль примыкающего к Кут-4г "ангара": и ещё "gate" -- как в аэропортах.
24.01.2025: похоже, правильнее будет выбрать термины "line" и "line_name" -- для унификации с используемой внутри layer'а терминологией, идущей от cankoz_lyr и piv485_lyr.
Итого:
sq_port_t
и не к TCP портам), заменены на "line".
add()
'а "port_name
" переименован
в "line_spec
" -- т.к. это не просто имя, а СПЕЦИФИКАЦИЯ,
могущая содержать префикс, указывающий тип подключения, а для serial-линков
-- ещё и префикс "@SERIAL_PARAMETERS:".
26.01.2025: работа с sq_port_t
позаимствована из piv485_lyr_common.c (PIV485 было до настоящего
момента единственным использованием портов), с адаптацией под местную модель
(line,handle).
27.01.2025: дело дошло до функций взаимодействия с линией -- как отправка/чтение, так и поддержание соединения.
И вот тут встаёт вопрос о "передаче идентификатора линии" -- как непосредственно функциям, так и fdiolib- и cxscheduler-callback'ам.
devid
,devptr
, а собственным функциям
передаётся аналогичный дуплет (devid
,me
).
Так что в принципе можно прямо lineinfo_t*
в качестве
privptr2
и передавать.
privptr2
числовое значение line
, из которого потом делается
lp=AccessLineSlot(line)
.
CleanupFDIO()
, ReportConnfail()
,
ReportConnect()
-- вот им таки сразу передаётся указатель
"lineinfo_t *lp
".
28.01.2025: и другой аспект: в драйвере
modbus_tcp_drv.c при всяких протокольных ошибках делается
CommitSuicide()
, т.к. непонятно, как же там продолжать. В
layer'е так делать нельзя.
ScheduleReconnect()
, чтобы "начать с чистого
листа".
Так что надо бы из порта эту информацию как-то добыть.
...возможно, добавить функцию добычи в API sendqlib.
29.01.2025@обед: или даже
проще: можно (и нужно!) добавить сохранение последнего
использованного unit_id
в mb_sender()
; например, в
last_sent_unit_id
, аналогично тому, как это делается в
piv485_lyr_common.c (там в last_kid
).
29.01.2025@вечер, после лыж:
ровно так и сделано. И в сообщениях в логи этот
last_sent_unit_id
тоже выдаётся. А вот деактивацию решено не
делать.
Просто делать SetDevState(,DEVSTATE_OFFLINE) -- так себе идейка.
Надо добавить "драйверный метод" в API layer->драйвер, чтоб уж драйвер решал, что делать -- игнорировать или застрелиться.
30.01.2025: по результатам сегодняшнего разговора на пультовой ВЭПП-2000 с Павленко, Роговским и Куркиным с Кравецом, где последний сказал, что какой-то их девайс с Modbus-TCP содержит 24-битный регистр, в который запись выполняется через 2 holding registers, а потом для активации надо ещё какую-то запись выполнить -- разные соображения.
А как МОЖНО добиться требуемого?
На какой из вариантов оринтироваться -- надо будет выбирать после получения описания.
02.02.2025: чисто "не сдержался" -- сделал заготовки для всех вариантов (на лыжах ходя обдумывал, вот и решил закрепить): kravets_test.devtype содержит определения пары каналов и должен годиться во всех 3 случаях; kravets_test_modbus_drv.c и kravets_test_vdev_drv.c как варианты драйверов.
11.02.2025: описание получено, но легче не стало: "активация" выполняется не записью в какой-то другой регистр, а пользовательскими Modbus-командами -- 65 (плавно) и 66 (мгновенно). И такое у нас не поддерживается вовсе...
В том, что первому вообще пофиг на unit_id
(значение
игнорируется), а во втором случае его значение является ключевым.
Т.е., далеко не всегда старый драйвер можно заменить новой инфраструктурой -- иногда старый вариант выиграет (что возможно для всяких криворуких девайсов, плохо обращающихся с unit_id).
Собственно идея:
unit_id
значения -1
или 0
.
...правда, при указании таких "магических" значений встанет вопрос "а что передавать в качестве РЕАЛЬНОГО unit_id".
05.02.2025: в процессе допиливания принято решение
"forget_last_sent()
" инкапсулировать в layer'е, а драйверам
доступа к нему не давать.
Идея в том, что менеджит это всё сам layer, а драйверам-клиентам он
выдаёт только готовые значения last_sent
(вычитанное ДО
forget'а) и is_expected
; а большего им и не нужно.
Поэтому из lvmt метод убран.
06.02.2025: код layer'а более-менее доделан, теперь надо тестировать -- хотя бы на "заглушке" в виде никуда неподключенного COM-порта и неконнектящегося хоста.
@вечер: а нет, нифига -- в
InitiateStart()
отсутствует код открытия COM-порта, да и в
логах соответствующих вариантов нету (копировалось-то из драйвера, а надо
ещё из монитора подмешать).
08.02.2025: по мере тестирования находятся и исправляются разнообразные косяки и упущения.
add()
'а происходило
"double-free" над privrec'ом -- из-за того, что по CLEANUP'у делалось
free(me)
, но НЕ делалось RegisterDevPtr(,NULL)
,
поэтому вызываемый далее _term_d()
исполнял
safe_free(me)
, приводя к "повторному освобождению".
Это унаследовано от modbus_tcp_drv.c, где вместо
add()
'а выполняется sq_init()
, который не
обламывается, потому там проблема не проявлялась. Тут же облом достигается
тривиально -- хоть дублем unit_id, хоть нерезолвлемым hostname.
Исправлено добавлением RegisterDevPtr(,NULL)
, и в
modbus_tcp_drv.c тоже.
modbus_add()
при поиске подходящей линии забыто было
"call_info.tcp_port = tcp_port;
", из-за чего на каждое
устройство выделялось по "своей линии".
lineinfo_t.line_path[]
не hostname, а "полный остаток spec" --
HOST:PORT. Чтоб в диагностике выдавать полные имена, с портами.
Облом: в таком случае при указании "tcp:b360mc" и
"rtu_over_tcp:b360mc:502" они считались РАЗНЫМИ линиями -- т.к. в
line_search_iterator()
выполняется сравнение строк. А должны
считаться одинаковыми, т.к. порт 502 в обоих случаях (и должна выдаваться
ошибка "несоответствие запрошенного протокола таковому у линии").
Так что вёрнуто обратно сохранение просто hostname.
InitializeRemote()
делался при взведённом
is_ready
, но взводит-то его именно сам
InitializeRemote()
!
Решение -- условие вызова заменено на
"media != MODBUS_MEDIA_TCP
",
т.к. если исполнение дошло дотуда при локальном COM-порте, то значит
открытие прошло успешно.
modbus_add()
при регистрации устройства в линии не
прописывалось значение dp->line
-- из-за этого
modbus_q_enqueue()
считало вызов некорректным и ничего не
делало.
devinfo_t
местами -- кажется, в
RlsDevSlot()
-- использовалось поле in_use
вместо
handle
; сейчас in_use
убрано вовсе.
port_tout_proc()
добавлен вызов
forget_last_sent()
, для которого handle добывается через
last_sent_unit_id
в несолькоуровневом условии.
Кстати, экспериментально проверено установленное ранее чтением кода sendqlib.c: при наличии у очереди привязки к порту используется таймаут ПОРТА, а таймаут самой очереди не используется вовсе.
11.02.2025: самый знатный косяк (с которым несколько дней не то, что разобраться -- непонятно было, как к нему подступиться!) заключался в том, что:
Причина оказалась тривиальной и тупой:
lp->used[unit_id] = devid;
вместо
lp->used[unit_id] = handle;
Как минимум потому, что именно он уникально привязан к устройству, а одному devid может принадлежать несколько устройств.
Да и вообще -- какое отношение имеют devid к адресации/учёту устройств ВНУТРИ layer'а? Понятно, что никакого, а использоваться тут должны внутренние handle (как в cda_d_-модулях -- hwr'ы).
А вот разбирательство было долгим и мучительным.
bzero()
каких-то ячеек (например, в
dev_list[]
).
q
и port
при их
инициализации,
mb_sender()
"дамп" внутренностей порта --
first_in_port
/last_in_port
и
first_to_send
/last_to_send
/current_to_send
.
add_to_send()
и
zer_to_send()
.
zer_to_send()
для очереди ВТОРОГО
устройства!
EHOSTUNREACH
) почему-то вызывались ReadyChange()
странных наборов устройств: не тех, что принадлежат этим линиям, а
"следующих".
ScheduleReconnect()
делалось и sq_clear()
, поэтому
при следующей попытке отправить пакет в линию обнаруживалась пустота очереди
и ей легитимно делалось zer_to_send()
, а слать было и нечего.
...а может, просто внимательное прочтение цикла, вызывающего
stateproc()
'ы устройств и сопоставление выданных диагностикой
значений handle'ов (должных содержаться в used[]
) с реально
содержавшимися там числами, очень уж явно совпадавшими с devid'ами
обломившихся устройств.
Выводы:
11.02.2025: итого -- на вид всё работает как предполагалось, но это проверено в тех пределах, в каких можно БЕЗ реального железа.
Где бы железки-то взять --
27.02.2025: Сеньков посоветовал отлаживаться на эмуляторе вместо реальных RTU-serial-устройств. Показал парочку GUI-софтин под Windows: "Modbus Poll" и "Modbus Slave".
...И там есть даже варианты "over UDP".
28.02.2025: проверил работу modbus_lyr.c с COM-портом при отсутствии, втыкании и вынимании USB-адаптера -- судя по диагностической печати, работает как должно. Так что можно считать готовым и проверять надо будет уже функционирование с "реальным железом".
24.03.2025: проверил, arsterm_ip_mrn_25 через /dev/ttyUSB0 -- сходу не работало, просто вообще никаких ответов не было.
Оказалось, что баги были как в драйвере (вместо handle передавалось
значение me
), так и в layer'е: в момент регистрации
отсутствовало сохранение dp->unit_id=unit_id
-- в результате
запросы слались с UNIT_ID=0.
После исправления заработало.
...вообще-то можно "поддерживать" их просто посредством
"устройства" с unit_id=0, которое все пакеты отправлял бы с
SQ_TRIES_ONS
; вот только такого режима в
modbus_lyr.c не предусмотрено (но добавить ничто не мешает).
И специальный драйвер-юзер layer'а чтоб этими отправками заведовал.
06.03.2025: учитывая немаленькость объёма modbus_drv.c, существенная часть которого приходится на парсинг и сохранение Modbus-спецификаций плюс взаимодействие с layer'ом -- а как бы этак обойтись БЕЗ отдельного драйвера, чтоб обычный "общий"?
06.03.2025@~18:00, по дороге домой после похода по магазинам за продуктами, идя вдоль подъездов Терешковой-10:
SQ_TRIES_ONS
.
@18:45, дома, заглянувши в
modbus_lyr.c: а то и вовсе прямо в
modbus_lyr.c::modbus_q_enqueue()
-- там самое удобное
место и с исторических времён там даже проверка в присвоении
item.props.tries
осталась.
А для них есть УЖЕ почти готовый механизм, который заставит драйвер НЕ пытаться читать: флаг "writeonly"; вот только надо будет сделать ещё, чтобы даже и ответов не ждало -- чтоб "подтверждения" делались прямо сразу.
...хотя в идеале не совсем сразу, а в момент ОТСЫЛКИ.
Побудительный мотив -- ВИП-48, у которого прямо в документации "Описание протокола обмена данными Modbus TCP_IP 26 03 2024 (3) для ВИП48и50.doc" сказано "если нет ответа в течение 0,5 секунды, можно закрывать соединение и каждые 1,05 сек пытаться открывать новое соединение".
09.03.2025: скорее "нет". Т.к.
on_hbt=
" можно б было с разной периодичностью отправлять?
Пока как-то не особо нужно (разве что у метеостанции -- чтобы
GPS-координаты вычитывать не только по "on_conn=
", но и,
например, раз в минуту), но в будущем может потребоваться.
Стандарт-то это нарушает, но нарушение напрашивающееся (ибо ограничение "256 байт" от балды). Поэтому -- а как бы потенциально поддерживать такое расширение?
И как указывать: modbus_mon'у -- ну ключ
"-U
" (Unlimited); а layer'у и драйверу? Layer'у -- видимо, тоже рядом с comopts (аналогично
RECONNECT_PERIOD), флажком каким-нибудь?
24.03.2025: подумал-подумал -- ну нафиг с флагом
возиться: разрешение "unlimited" влияет не только на числа-ограничения, но и
на размеры буферов. Пусть лучше меняется #define
-символом,
чтоб влиял на условную компиляцию, а уж включить его можно из командной
строки make'а.
Делаем:
UNLIMITED_MODBUS
и ...
modbus_max_num1()
--
данная точка была выбрана после попыток добавить в другие места, вроде
modbus_num1_of_nelems()
, как наиболее соответствующая
архитектуре.
26.03.2025: а ещё максимум не 65536 байт, а 65535.
RegisterWithFDIO()
-- для TCP максимум ставится 6+65535.
Что забавно, в UNLIMITED-варианте это ограничение реально ничего не ограничивает: бОльший размер посредством 16-битовой длины указать физически нельзя.
Для остальной пары пока ничего не менялось.
ProcessInData()
-- размер буфера дешифрированных
данных buf[]
теперь 65536*2
("*2
"
из-за конверсии FLOAT16_10E6M10
, раздувающей принимаемый
uint16
во float32
).
PerformIO()
-- в union
-буфер для
парсинга добавляются uint16[65536]
(для HOLD) и
uint8[65536]
(для COIL).
26.03.2025: подумавши, ясно, что сделано "от балды", но
получилось верно: 65536 штук UINT16 недостижимо, но 32768 штук
float32
ровно столько и займут.
Впрочем, вряд ли он когда-либо будет использоваться полностью: такой
объём данных ни в argv[]
указать (ограничение 128KB), ни из
файла взять (ReadFromFile()
читает в char
line[1000]
).
А пока в modbus_tcp_drv.c, modbus_drv.c и
modbus_lyr.h вставлено принудительное
"#undef UNLIMITED_MODBUS
"
перед
«#include "modbus_conv.h"
»,
от греха подальше.
Теперь проверять надо.
CPPFLAGS+=-DUNLIMITED_MODBUS
).
25.03.2025: да, реализовано в обоих --
modbus_mon-Makefile-src.mk и Makefile -- делается
добавление "-DUNLIMITED_MODBUS
" к CPPFLAGS
и
LOCAL_CPPFLAGS
соответственно.
25.03.2025: вчера взял у Краснова один источник "для тестов на столе", с вынутыми высоковольтными блоками. Тестируем (подключено к b360mc, т.к. до неё патч-корд дотягивается, а до x10sae не достаёт).
pdu[1]
. Девайс туда помещает, похоже, просто младший байт
реального размера, вот косяк и происходит.
Кстати, Wireshark'овский декодер Modbus тоже явно на это поле ориентируется: он в дешифровке показывает меньшее число регистров, хотя все байты пакета у него есть.
modbus_decode_rpy_packet()
добыча количества
байт данных заменена на более сложную конструкцию:
-- при нестандартно-больших пакетах "число байт данных" берётся из длины пакета.#ifdef UNLIMITED_MODBUS if (pdu_len > 256) nbts = pdu_len - 2; else nbts = pdu[1]; #else nbts = pdu[1]; #endif /* UNLIMITED_MODBUS */
После этого стало показываться правильное число регистров.
Разбирательство -- с использованием и показаний Wireshark, и диагностической печати, и strace -- показало, что кривизна на стороне девайса: он никогда не присылает более 2047 байт данных, так что когда по числу регистров требуется больше (для 1020шт надо 2049 байт: 6+1+2042 заголовок,UNIT_ID,PDU) -- и в заголовке это указано! -- то fdiolib ждёт ещё данные, которые никогда не придут.
По результатам тестирования написано письмо "Отчёт о попытке взаимодействия с ИПМРН-20 от Диком по Modbus-TCP" Карнаеву, Краснову, Липовому, Message-ID:84e67bb3-e5bb-6d65-9417-b2e07dff9090@starnew.inp.nsk.su (тут в тексте ниже закомменчено целиком).
24.03.2025: при сегодняшней проверке с реальным
железом диагностическая печать показала, что в q_enqueue()
передавались какие-то дикие значения handle
.
Оказывается, при портировании из modbus_tcp_drv.c первым
параметром так и осталось "me
" вместо
"me->handle
".
Исправлено, и, после исправления другого бага в самом layer'е, заработало.
Вот прямо сейчас штука носит название "modbus_conv.h", но, т.к. конверсия теперь является лишь одной из областей ответственности, явно надо переименовать во что-то более адекватное.
17.07.2023: решено работу по конверсии сосредоточить в отдельном модуле, которому покамест дано название modbus_conv.h, а помещён он в hw4cx/include/; пока что в рамках одного файла живут и определения, и код (возможно, позже разделим; а может и нет).
MODBUS_CONV_*
, сам список взят из EPICS'ного Modbus-модуля
(R2-10-1.tar.gz,
файл modbusApp/src/drvModbusAsyn.h, тип modbusDataType_t
).
При этом значение 0, получающееся по умолчанию -- у MODBUS_CONV_UINT16
, так
что по умолчанию конверсия отсутствует.
modbus_conv_table[]
, индексируемая кодом
"типа конверсии", в каждой строчке которой указатели на пару конвертеров
"туда и обратно" -- h2m
и m2h
, "Host to Modbus" и
"Modbus to Host".
count
; таким образом сразу закладываем поддержку векторов.
В целях вящей корректности и для защиты от потенциальных ошибок указатель
на Modbus-данные -- uint8 *m_data
, а на "хостовые" --
void *h_data
, чтоб конвертер приводил бы указатель к
надлежащему типу.
UINT16_h2m()
и
UINT16_m2h()
, они же используются и для строчки INT16.
MODBUS_RTU
=1, MODBUS_ASCII
=2,
MODBUS_TCP
=3) и типов регистров MODBUS_KIND_*
--
для того, чтобы модуль должным образом мог формировать (и дешифрировать)
пакеты.
chaninfo_t
по-канально, так что драйвер работает в терминах
каналов, а при обращении к среднему уровню передаёт тому свойства из
map[chn]
.
И в элементах очереди информация хранится именно в терминах каналов, транслируясь в термины среднего уровня уже в момент непосредственной отправки пакета в линию.
Это КЛЮЧЕВОЕ изобретение, "расшивающее" логику работы с данными от специфики кодирования протокола. И, по сути, это должно б было присутствовать в самом Modbus'е, но нету.
Более того: принцип "пригодность для использования в утилите командной строки" отлично подходит как критерий распределения функциональности между драйвером и модулем: если что-то представляется лишним/неподходящим для утилиты -- значит, этой функциональности не место в модуле.
18.07.2023: продолжаем.
name
-- "имя" этого типа конверсии; нужно
клиентам модуля для парсинга.
num_cells
и ext_dtype
: первое указывает число регистров, в которое
отражается 1 юнит данных, а второе просто его тип.
26.07.2023: а ведь для СТРОК, которые по 2 символа в 16-битном регистре, модель чуток сложнее -- там как-то надо учитывать эти "1 регистр отображается на 2 единицы nelems".
27.07.2023: делов-то -- ну добавлено поле
ext_per_cell
, в котором почти для всех конверсий будет значение
1
, и только для вышеозначенных строк -- 2
.
CreateModbusPacket()
-- чтоб он сам формировал
соответственно протоколу, кладя данные по нужным offset'ам и для ASCII
растягивая 1 байт в 2, а также считал бы контрольные суммы нужным образом.
20.07.2023: вчера весь день тупил, пытаясь выстроить в голове максимально стройную и "правильную" схему работы кодирования/декодирования пакетов. В голове-то худо-бедно картинка получилась, надо теперь реализовывать.
Сходу пара нюансов:
DecodeModbusPacket()
,
получающую на вход буфер с полученным пакетом плюс код протокола и
раскладывающую в переданные по указателям переменные информацию о типе
регистра, адресе, коде операции и складирующую в указанное место данные.
Поэтому убираем параметр conv
также и у
CreateModbusPacket()
и постулируем, что вызов
конвертеров находится в компетенции клиента, а обмен данными с функциями
формирования/дешифрирования пакетов идёт уже прямо готовыми
uint8
-массивами.
Сделано некоторое начальное наполнение CreateModbusPacket()
и буквально скелет DecodeModbusPacket()
(if()
по
типу протокола).
24.07.2023: продолжение наполнения
CreateModbusPacket()
.
25.07.2023: далее...
CreateModbusPacket()
в
соответствии с документацией.
modbus_parse_spec()
, реализующая парсинг
спецификаций по проектам от 17-07-2023 и 21-07-2023 --
HOLD:1000
, 4:1000
, 41001
,
401001
, 4x03E8
.
ПОКА ЧТО она делает только это, но в будущем планируется добавить к ней
CONV%DPYFMT:
"
либо просто "%DPYFMT:
" (в последнем случае считается UINT16).
/NUM1
".
Возможность присутствия оных (только для modbus_mon'а) будут указываться
в flags
, аналогично PAS_
-флагам в
uspci_test.c::ParseAddrSpec()
-- которая, кстати,
максимально близка по синтаксису.
26.07.2023: принято важное решение: в
CreateModbusPacket()
будет передаваться всё-таки не размер
данных в байтах, а именно "число единиц В/В" -- num1
;
мотивация/детали:
num1
требуется не только для команд записи, где есть
данные (кореллирующего с num1 объёма), но и для команд чтения -- там надо
указывать требуемое для чтения количество.
if()
по
типу/команде теперь не только формирует pdu_hdr[]
, но и
устанавливает значение dsiz
-- длины данных в байтах.
А уж идущий далее код формирования пакета в if()
'е по типу
протокола -- тот просто копирует в пакет нужный объём, не задумываясь о
смысле.
И приступаем к наполнению DecodeModbusPacket()
...
enum
-коды MODBUS_ERR_*
.
10.09.2023: переименованы в
MODBUS_EXC_*
.
'_'
).
m_data[]
: [0] -- function
code (со срезанным старшим битом), [1] -- exception code. Т.е. -- по факту
просто первые 2 байта PDU.
// Return value: 0:OK <0:ignore (protocol error) >0:exception (logical error)
27.07.2023: ЗАМЕЧАНИЕ В СТОРОНУ:
MODBUS_KIND_*
.
MODBUS_KIND_*
, а функции создания/декодирования пакетов будут
их понимать.
(придумано ещё давно, но явно записано только сейчас)
27.07.2023: продолжаем работы с декодированием пакетов.
DecodeModbusPacket()
доведена до некоего
минимального состояния -- понимает MODBUS_TCP, конкретно 3 вида функций
(чтение/запись holding registers, чтение input registers (идентично
holding)).
Сюда всегда входят kind
и op
-- поскольку они
однозначно определяются по коду функции, а он есть всегда.
-1
.
Сюда входят addr
и num1
для пакетов чтения, а
также trans_id
для не-TCP.
28.07.2023@душ, ~16:00: а ведь
нифига -- конкретно для ответов на чтения 16-битовых сущностей МОЖНО вывести
num1
из количества байт. ...а вот для 1-битовых -- увы, увы:
можно только проверять "предполагаемое" по last_sent
на
совместимость с числом байт.
28.07.2023@вечер, после прогулки, ~18:00: да, добавлено.
-1
, то придётся
это "что-то" смотреть в last_sent
;
last_sent
.
29.07.2023:
MODBUS_FUNC_READ_MULT_DSCR
=2,
MODBUS_FUNC_READ_MULT_COIL
=1,
MODBUS_FUNC_WRITE_MULT_COIL
=15.
CreateModbusPacket()
-- чтение вообще идентично
16-битному (только коды функций другие) и между собой, а запись отличается
от 16-битной вычислением числа байт -- (num1 + 7) / 8
вместо
num1 * 2
.
С одной стороны, дублирование практически идентичного кода напрягает; с другой -- объём не такой уж большой (меньше сотни строк на все 6 вариантов/кодов), так что унификация даст мало что, а вот запутать может.
DecodeModbusPacket()
-- тоже всё тривиально.
Тут, конечно, минус в том, что оно сейчас ТОЛЬКО для TCP-варианта, а для RTU и ASCII пока что по плану отдельный декодер. Но потом посмотрим -- может, удастся сделать выдёргивание PDU в единое место, из которого уже будут браться байты.
30.07.2023@утро-зарядка: да чё там "удастся" -- НАДО делать унифицированный парсинг PDU:
uint8 *pdu
и int pdu_len
;
inpkt
,
Дополнительным плюсом будет то, что так можно будет проще считать LRC -- в этом самом буфере, уже бинарном, а не текстовом.
30.07.2023: делаем.
pdu
и pdu_len
.
pkt[]
на pdu[]
...
modbus_rtu_crc()
пока только прототип) и выставлению pdu
,pdu_len
,
...
modbus_ascii_get_uint8()
-- которое при встречании
не-[0-9a-zA-Z] возвращает -1
, сигнализируя об ошибке и
необходимости просто выбросить пакет.
А пользующееся вышеуказанным вычитывание цепочки байт --
modbus_ascii_get_bytes()
-- форвардит -1
при
надобности.
modbus_ascii_lrc()
, и с учётом нового
понимания, когда работать надо только с бинарными данными, она будет единой
для кодирования и декодирования, и, поскольку при кодировании есть 2 блока
данных -- pdu_hdr[pdu_hdr_nbytes]
и m_data[dsiz]
-- то функция принимает 2 пары, {data,len}{1,2}, и перебирает байтики в
обоих блоках с помощью дополнительного цикла по этим "частям".
После чего собственно код разбора пакета уже аналогично RTU'шному
несложен: сначала минимальные проверки (длина не менее 7 символов, причём
нечётная), затем modbus_ascii_get_bytes()
в buf[]
и проверка LRC, а затем вычитывание station_id
и выставление
pdu
,pdu_len
-- всё!
31.07.2023:
modbus_ascii_lrc()
-- там довольно
простое сложение всех байтов пакета, кроме ':'.
modbus_rtu_crc()
сделана -- задолбался
искать описание алгоритма подсчёта контрольной суммы!
Нашёл в "How to Compute the Modbus RTU Message CRC" -- там вот такой код:
// Compute the MODBUS RTU CRC UInt16 ModRTU_CRC(byte[] buf, int len) { UInt16 crc = 0xFFFF; for (int pos = 0; pos < len; pos++) { crc ^= (UInt16)buf[pos]; // XOR byte into least sig. byte of crc for (int i = 8; i != 0; i--) { // Loop over each bit if ((crc & 0x0001) != 0) { // If the LSB is set crc >>= 1; // Shift right and XOR 0xA001 crc ^= 0xA001; } else // Else LSB is not set crc >>= 1; // Just shift right } } // Note, this number has low and high bytes swapped, so use it accordingly // (or swap bytes) return crc; }
...а в прочих местах была всякая дичь: в английской Википедии -- малополезное "Polynomial: x16 + x15 + x2 + 1 (CRC-16-ANSI also known as CRC-16-IBM, normal hexadecimal algebraic polynomial being 8005 and reversed A001). Initial value: 65,535.", в нагугленных примерах кода -- возня с какой-то таблицей (теперь понятно, что, видимо, так избегали цикла по 8 битам, заранее насчитывая результат трансформации для всех 256 значений).
03.08.2023:
modbus_max_num1()
-- теперь она принимает
параметрами ПАРУ (proto,kind), так что для не-TCP позволяет вычитывание чуть
большего числа регистров. Информация о конкретных ограничениях бралась из
статьи Modbus в англоязычной Википедии.
И в modbus_tcp_drv.c::modbus_tcp_init_d()
она
теперь задействована.
04.08.2023: только там ограничение ставится
тупорыло-некорректно: просто по значению num1
, БЕЗ учёта
требуемой по типу конверсии кратности. Сделано -- в случае превышения
ограничения конкретно для 16-битных дополнительно отрезается не-кратное
conv
'ову num_cells
(это актуально для
многоячеечных конверсий: ограничения-то 123 и 125, что не кратно 2 и 4).
count
, а ext_nelems
-- это
критично для конверсий с ext_per_cell
>1, когда объём
передаваемых по Modbus "единиц" может не совпадать с количеством единиц
клиента.
m2h()
берёт только нужный байт, а
h2m()
заполняет используемый байт и нулит неиспользуемый.
m2h()
берёт очередной байт из входящей строки и кладёт
его в "более ранний" байт слова, а следующий байт слова заполняет следующим
байтом из строки только если длина строки позволяет, иначе нулём;
h2m()
"более ранний" байт из слова кладёт в строку
сразу, а следующий байт слова -- только если запрошенная длина строки это
позволяет.
Вот как выглядит парочка конвертеров STRING_HIGH_LOW (STRING_LOW_HIGH --
аналогично-зеркально, m_data[0]
и m_data[1]
меняются местами)
static void STRING_HIGH_LOW_h2m(void *h_data, uint8 *m_data, int ext_nelems) { uint8 *rp = h_data; for (rp = h_data; ext_nelems > 0; rp += 2, m_data += 2, ext_nelems -= 2) { m_data[0] = rp[0]; if (ext_nelems > 1) m_data[1] = rp[1]; else m_data[1] = 0; } } static void STRING_HIGH_LOW_m2h(uint8 *m_data, void *h_data, int ext_nelems) { uint8 *wp = h_data; for (wp = h_data; ext_nelems > 0; wp += 2, m_data += 2, ext_nelems -= 2) { wp[0] = m_data[0]; if (ext_nelems > 1) wp[1] = m_data[1]; } }
07.08.2023: за последние несколько дней доделаны остававшиеся конвертеры.
DATA32_{LE,BE}_{h2m,m2h}()
, которые переставляют байты в
uint32
в очевидном порядке.
("Очевидный", конечно, весьма условно: для BE просто стандартная
перестановка, а вот для LE -- противно, т.к. это по факту "middle endian"
(порядок 16-битных слов -- little, а байтов внутри слова -- big).)
DATA64_{LE,BE}_{h2m,m2h}()
, но
&
'ами и сложениями, а перестановкой байт.
modbus_data64_{le,be}_{h2m,m2h}_map[8]
, в которых указывается,
какой байт из источника положить в очередной байт приёмника.
LITTLE_ENDIAN
и BIG_ENDIAN
.
modbus_data64_le_*_map[]
для
BIG_ENDIAN
-варианта были недоделаны -- в них содержалось
"тождественное отображение"
{0, 1, 2, 3, 4, 5, 6, 7}
,
очевидным образом не являющееся тут подходящим.
Заменено на
{6, 7, 4, 5, 2, 3, 0, 1}
,
соответствующее LITTLE_ENDIAN
'ному
{1, 0, 3, 2, 5, 4, 7, 6}
.
Проверено (на конверсии INT64_LE
) -- и взглядом на дампы
пакетов, и поочерёдными записью и чтением на x86_64 и PowerPC -- работает
как надо.
...отдельный вопрос, насколько это соответствует EPICS'ному формату. Надо вдумчиво ещё разок почитать их код.
_h2m_map[]
и _m2h_map[]
ИДЕНТИЧНО. ВСЕГДА.
static void DATA64_BE_h2m(void *h_data, uint8 *m_data, int ext_nelems) { uint8 *rp; int nb; for (rp = h_data; ext_nelems > 0; rp += 8, m_data += 8, ext_nelems--) for (nb = 0; nb < 8; nb++) m_data[nb] = rp[modbus_data64_be_h2m_map[nb]]; } static void DATA64_BE_m2h(uint8 *m_data, void *h_data, int ext_nelems) { uint8 *wp; int nb; for (wp = h_data; ext_nelems > 0; wp += 8, m_data += 8, ext_nelems--) for (nb = 0; nb < 8; nb++) wp[nb] = m_data[modbus_data64_be_m2h_map[nb]]; }
По-хорошему, каждую пару таблиц надо объединить в одну таблицу.
@вечер, ~20:30: да нифига, не «сами конвертеры так устроены -- "зеркально"», а просто "по определению" -- ведь перестановка должна быть обратимой, вот она и получается зеркальной.
Также расширен "API" modbus_parse_spec()
:
station_id_p
и
dpyfmt_p
(этот должен указывать на буфер
char[16]
), куда будут складываться результаты парсинга "+ID:" и
"%DPYFMT:" соответственно, ...
num1_p
переименован в nels_p
, т.к.
"/COUNT" будет указываться во "внешних" элементах данных, а не в регистрах.
("/COUNT" понадобится и в драйвере -- для указания команд периодического группового чтения.)
MODBUS_PARSE_FLAG_*
.
Кстати, немного сопутствующей информации на тему...
htons()
при отправке и
ntohs()
при получении, так что сами 16-битные значения
регистров (а они там именно epicsUInt16
) всегда в нативном
host-формате.
doModbusIO()
, вызывающая
pasynOctetSyncIO->writeRead()
.
replySize
и столько и читается
вышеупомянутым writeRead()
.
07.11.2024: тот сайт "Server Not Found", но оно же найдено на более правильном сайте -- "Modbus Data Formats".
09.08.2023: протестирована работа STRING-конвертеров.
11.08.2023: в modbus_parse_spec()
добавлен парсинг опциональных префикса "+ID:
" и суффикса
"/COUNT
"; проверка на наличие делается только при взведённости
соответствующего MODBUS_PARSE_FLAG_*
.
24.08.2023: добавлен парсинг "%DPYFMT".
CONV%DPYFMT
".
ParseDatarefSpec()
, включая
добавление перед символом формата модификаторов "ll" для
INT64/UINT64 (которые нашим Modbus-стеком пока даже не поддерживаются).
isspace()
и двоеточия также считается и
'%'.
Теперь добавлено, считается.
А вообще напрашивается заменить это диковатое условие
"isspace()
либо ещё несколько выделенных символов" --
-- на более общее на основеwhile (*p != '\0' && !isspace(*p) && *p != ':' && *p != '%' && *p != '/') p++;
isalnum()
, вроде
Это автоматом даст более полноценный и всеобъемлющий синтаксис, т.к. терминаторами станут и подчерк '_' и точка с запятой ';' (как разделители команд дляwhile (*p != '\0' && !isspace(*p) && !isalnum(*p)) p++;
modbus_mon
), и '=' (для
команд записи).
26.08.2023@утро, просыпаясь: ага, вот только *сам* DPYFMT под этот шаблон совершенно не подходит: не говоря уж о начальном '%', в нём могут быть куча разных символов, включая минус, плюс, точку, решётку и даже пробел.
26.08.2023@утро, вставши:
впрочем, посмотрел -- там всё устроено так, что парсинг
%DPYFMT вообще никак не зависит от "парсинга токена": оно игнорирует
предыдущее положение "конца токена", а просто ищет ':' и считает
своим всё до него, а указатель "что дальше" ставит на после него. Так
что МОЖНО переходить на isalnum()
.
!isalnum(*p)
"?!?!?! Безо всякого
"not" -- "!
" -- ведь надо как раз IS alnum! Т.е.,
while (*p != '\0' && !isspace(*p) && isalnum(*p)) p++;
!isspace()
"? Уж
если alphanumeric, то точно не пробел.
Убрано. После чего и проверка "!= '\0'
" стала бессмысленна
-- это была лишь защита на всякий случай, если NUL считается за
isspace()
.
Добавлена отдельная проверка.
Итого, рабочий вариант сейчас выглядит так:
while (isalnum(*p) || *p == '_') p++;
Максимально просто и коротко. А возни-то было...
dpyfmt_buf_p
,
но и размер буфера dpyfmt_buf_size
-- ибо нефиг какие-то
умолчания о минимально-гарантированных размерах.
04.09.2023: в modbus_parse_spec()
добавлена возможность парсинга "transaction ID", могущего указываться после
"station ID" как ".NNN
":
MODBUS_PARSE_FLAG_TRANS_ID
, включающий такое
расширение синтаксиса.
trans_id_p
.
...правда, пока не проверялось. 10.09.2023: проверено -- работает, во всех вариантах.
10.09.2023: переименования:
MODBUS_ERR_*
переделаны в
MODBUS_EXC_*
.
21.12.2024: добавлено
"MODBUS_EXC_NONSTANDARD_CODE_9 = 9
".
23.12.2024: и оно переехало в modbus_defs.h.
CreateModbusPacket()
и
DecodeModbusPacket()
эта парочка была поменяна местами в списке
параметров -- с sync_id,unit_id на unit_id,sync_id для большей
естественности и соответствия прочим местам.
Дополнения:
modbus_strkind()
-- для выдачи осмысленных
названий типов регистров в modbus_mon.c, пока в диагностических
сообщениях, а потом и в выдаче "имён" регистров.
modbus_strexc()
-- для выдачи сообщений об exception'ах.
13.09.2023: сделаны
modbus_num1_of_nelems()
и modbus_nelems_of_num1()
-- чтоб не дублировать вычисления повсюду.
Вторая-то проста (никаких особых случаев нет, "переполнение" невозможно),
а вот первая навороченновата -- она ж должна определять превышение
максимального значения num1
и передавать это вызывающему,
причём с конкретными числами для диагностической выдачи.
24.09.2023: и задействованы
14.09.2023: формализованы
MODBUS_OP_READ
=0 и MODBUS_OP_WRITE
=1, чтоб
избавиться от "магических чисел".
30.09.2023: пришлось в BCD_SIGNED_m2h()
переименовать B0
и B1
в b0
и
b1
соответственно. Причина -- в modbus_mon.c после
#include <termios.h>
получился конфликт с приходящим оттуда
#define B0 0000000 /* hang up */
02.10.2023: разное по мелочи:
modbus_bits_h2m()
-- из вектора указанного
целочисленного типа (INT8/16/32) формирует байтовую цепочку, годную для
команды MODBUS_FUNC_WRITE_MULT_COIL
.
modbus_bits_m2h()
-- вытаскивает битики из байтовой
Modbus-цепочки (причём с начальным сдвигом в addr_ofs
) в
uint8
-массив.
Обобщено, чтоб конверсией мог пользоваться как минимум modbus_mon.c (там это для команд записи надо).
CreateModbusPacket()
и DecodeModbusPacket()
переименованы в modbus_create_packet()
и
modbus_decode_packet()
соответственно.
Чтоб все имена были в одном стиле.
09.10.2023: ещё раз переименованы -- теперь в
modbus_create_req_packet()
и
modbus_decode_rpy_packet()
соответственно. Причина -- что они
ведь НЕ симметричны, а создают и расшифровывают РАЗНЫЕ сущности: запрос и
ответ. И формально возможны реально парные к ним функции: либо для
"Modbus-сервера", либо, как минимум modbus_decode_req_packet()
-- для сниффера.
05.10.2023: надо всё-таки доразобраться с 64-битными данными. Разбираться с ВЕЩЕСТВЕННЫМИ -- опухнешь (девайса с реальными float64 под рукой нет).
INT64_BE
и INT64_LE
, и проверять на них.
DATA64_{BE,LE}_{h2m,m2h}()
.
INT64_BE%018x:HOLD:2141
"впустую" (всё равно не даст записать)
-- в дампе данные идут в нужном порядке.
(2141:2142 и 2143:2144 -- это у РПМ-416 регистры адресов DNS-серверов, которые всё равно маломолезны.)
INT64_BE%018x:HOLD:2141=0x123456789abcdef0
и чтение -- да,
прочитанное значение совпало!
(Первой командой было string_low:hold:51/13=admin
для
разблокировки записи.)
Для этого в hw4cx/drivers/can/c4l-cangw/Makefile была добавлена shadow-сборка modbus_mon'а. Оно собралось, ...
INT64_BE%018x:HOLD:2141
ровно то же значение.
И записанное с PowerPC читается с x86_64 таким же.
0x.0x.202x:
06.10.2023: по большому счёту, работа состояла из 2 частей:
modbus_mon.tar.gz
", сводящийся к
...плюс локальный modbus_mon-Makefile-src.mk туда копируется под именем "Makefile".
Ну да, громоздковато -- особенно туча команд копирования исходников.
Но зато просто работает.
Надо как-то продумать архитектуру.
23.11.2023: продумываем.
MODBUS_FUNC_nnn
.
modbus_decode_rpy_packet()
понимать их -- ну
громоздко, 4 лишних ветки в первом if()/else-if() и во втором по второму
коду в каждом условии.
modbus_create_req_packet()
тоже несложно -- ну
угромозкится главное условие, да и всё.
MODBUS_FUNC_WRITE_SINGLE_COIL
) нужно будет подменять данные,
перебрасывая указатель m_data
на предопределённые
uint16
-цепочки {0xFF,0x00}
или
{0x00,0x00}
в зависимости от значения младшего битика в
*m_data
(там просто байт, endian-agnostic).
modbus_create_req_packet()
.
Как это сделать?
modbus_parse_spec()
.
Т.е., после адреса разрешать не только "/nnn", но и ':', после которого обязательно должно
Вопрос лишь, как возвращать это. "-1" в *nels_p
? Нет,
категорически криво. Возвращать слово флагов (которое потом можно
передавать в формирование пакета)?
MODBUS_CONV_STRING_HIGH_LOW
и
MODBUS_CONV_STRING_LOW_HIGH
валидным является сочетание
однорегистровой команды и "/2
" (а не только "/1
").
В таком случае это ":1" будет не взаимоисключающим с "/COUNT", а могущим указываться перед ним, так что спецификация "ADDR:1/2" будет вполне легитимной.
num1>1
.
Сейчас-то modbus_num1_of_nelems()
делается в клиентах, а
в целях унификации/инкапсуляции лучше бы проверку поместить в
modbus_parse_spec()
.
...впрочем, такой специфичный вариант проверки весьма несложен --
при HOLD или INPR проверить, что
modbus_conv_table[conv].num_cells==1
; сделаем именно так.
26.11.2023: итого: проект-то имеется, но внедрять его совсем не тянет, ибо нынешнюю краткость и элегантность это испортит.
OK, буду тянуть до упора, пока не припрёт.
modbus_strexc()
: для неизвестных кодов exception'ов вместо
"UNKNOWN_CODE" теперь возвращается
"NONSTANDARD_EXCEPTION".
Смысл -- чтоб сообщение было читабельнее, а то, например, фраза "EXCEPTION 13:UNKNOWN_CODE", печатаемая modbus_mon'ом при работе с ВИП-45, воспринимается так, что смысл ошибки 13 -- "неизвестный код", а не то, что "код ошибки 13 неизвестен".
18.01.2024: ещё в ту же степь -- добавлены 2 кода из более позднего стандарта "MODBUS APPLICATION PROTOCOL SPECIFICATIONV1.1b" от April 26,2012, стр.48 -- 10 и 11 (0x0A и 0x0B), которые "GATEWAY PATH UNAVAILABLE" плюс "GATEWAY TARGET DEVICE FAILED TO RESPOND".
21.12.2024: и уж для полноты -- добавлено
[MODBUS_EXC_NONSTANDARD_CODE_9] = "NONSTANDARD_CODE_9"
modbus_strproto()
-- в интересах modbus_lyr.c, для
выдачи в лог ошибок несовпадения протокола.
Так что с начала января приступил к добавлению поддержки этой фиготы прямо сюда -- чтоб можно было даже в modbus_mon'е смотреть значения.
"Как делать" -- смотрел в описании от авторов из Выборга (отвратительно описано!) и консультировался с Димой Липовым, занимавшимся этим вопросом в EPICS для СКИФ.
18.01.2025: что сделано к текущему моменту:
FLOAT16_10E6M10
" -- чтоб не
только отражало тип+размер, но и со спецификой кодировки ("10-чная
экспонента в 6 бит и мантисса в 10 бит").
(FLOAT16_10E6M10: 6bit signed dec.exp, 10bit mantissa, no sign; VIP45/VIP48)
MODBUS_CONV_FLOAT16_10E6M10
,
FLOAT16_10E6M10_h2m()
и
FLOAT16_10E6M10_m2h()
,
modbus_conv_table[]
#if MAY_USE_MATH
", каковой симвом по
умолчанию определяется в 1, однако может быть зафорсен в 0 из команды
компиляции.
...вот только добавлять возможность включения/отключения его в
hw4cx/drivers/eth/Makefile было лень; а в идеале надо бы, т.к. там
же пришлось добавлять и "-lm
", ибо математика.
float32
(как и у Липового, кстати).
FLOAT16_10E6M10_m2h()
сделал самостоятельно
по описанию от авторов из Выборга и поглядывая на код Димы Липового.
Причём там вставлена проверка на специальные значения 0xFFFF
и 0xFFFE
, с переводом их в INFINITY
и
NAN
соответственно.
Правда, насколько она реально нужна -- вопрос; поэтому пока внутри
"#if 1
".
float32
в этот
быдлотип -- каковое также необходимо для возможности указывать некоторые
параметры девайсу -- пока ниасилил.
22.01.2025: вчера спросил Липового, откуда взялся его код,
выполняющий это преобразование -- "оно разрабатывалось самостоятельно или
авторы ВИПа предоставили алгоритм?"; сегодня он ответил "Это код собственной разработки.". Учитывая, какая там
мутотень -- и стандартные libc'шные функции работы с вещественными, вроде
ldexpf()
и scalbnf()
, как и прочие
frexpf()
, использовать нельзя, из-за десятичности экспоненты --
проще будет взять готовый код от Липового.
15.02.2025: да, "взял" готовый код от Липового,
адаптировав его. Обнаружил в нём один баг (вызов modf()
с
float
вторым параметром вместо double
) и однy
плохосовместимость (запихивание экспоненты в старший байт как
"raw_ptr[1] |= ...
" -- это только для little-endian, а на
big-endian сломается).
16.02.2025: добавил проверку обратного перевода
float32
->"float16" в test_float16_10e6m10_conv.c;
проверил -- хрень какая-то: лишь для очень небольшого числа кодов результат
конверсии "float16"->float32
->"float16" совпадает с
исходным значением, для чуть большего -- НЕ совпадает (последовательная
группа кодов приводит к одному и тому же числу), а подавляющая часть --
обратная конверсия не работает вовсе, считая, что "digits_count вне
диапазона [2...9]".
float
и печатающая результат.
Ярко увидено, что кодировка ужасная: поскольку нет ни
нормализации, ни неявной-1 в старшем разряде мантиссы. В результате этой
бестолковости кодировка неоднозначная: например, пары (мантисса,экспонента)
со значениями (1000,-32), (100,-31), (10,-30), (1,-29) все кодируют число
1.0e-29 -- ЧЕТЫРЕ варианта; и таких множественных комбинаций море (всё
становится очевидным при сортировке по результирующему вещественному числу
-- "sort -gk5
").
10.03.2025: через Алексея Семёнова получил от изготовителей ВИПа ответ на вопрос "а какой алгоритм перевода float32->code16?" -- фрагмент кода на Python:
def decode(value: int): degree = value >> 10 # степень числа # если старший бит 16-разрядного числа — 1, перевод в отрицательное значение if value & 0x8000: degree += 0xC0 - 256 # выделение мантиссы и умножение на 10 в степени return (value & 0x3FF) * 10 ** degree def encode(value: float): s = '%7.1E' % value # формирование строки числа в экспоненциальной форме, пример: «1.2E-08» before = s.find('.') # Позиция «.» after = s.find('E') # Позиция «E» mantissa = int(s[:before] + s[before + 1:after]) # формирование мантиссы degree = ((int(s[after + 1:]) - 1) << 10) & 0xFFFF # формирование порядка (-1 учитывает 1 знак после «.») return degree + mantissa
Мда, грустно -- формировать ЧИСЛЕННОЕ значение с помощью СТРОКИ...
Я таким страдал в глубокой юности, когда только начинал заниматься программированием, чисто из-за незнания других способов.
14.03.2025: с учётом увиденной сегодня проблемы "1e+34" -- а не проявится ли она в этом коде? На вид -- должна, т.к. никаких проверок там не видно.
11.03.2025: поскольку нифига не понятно, но решил погуглить на тему "float extract decimal exponent"; результаты есть, все со StackOverflow:
Там для вычисления значения десятичного порядка предлагается формула
floor((log10(fabs(Value))))
-- после отдельной проверки
значения на 0 (по сути её же предложили и в предыдущем ответе на Python).
Её там критикуют с примерами, что в некоторых случаях она даёт неверный результат вследствие округлений -- из-за неточности вещественной арифметики и того, что реально в памяти лежат не десятичные порядки, а двоичные, и десятичные генерятся при создании строкового представления чисто для человека.
Также там говорится, что «Converting floating-point to decimal efficiently is notoriously "hard."» и даётся ссылка на сопутствующую интересную тему -- ...
16.03.2025: стыдно признаться, но до сих пор так и не прочитал. Впрочем, сейчас мельком глянул -- там, по сути, просто толпа ссылок (в основном на бумажные статьи), а не описание алгоритма.
12.03.2025: пробуем реализовать.
FLOAT16_10E6M10_f2s()
(Float to
S -- что под "s" подразумевалось, теперь не вспомню...).
floorf((log10f(fabsf(f32_val))))
даёт
значение, совпадающее с выдачей "%e", в т.ч. на "границах"
(переходах значения экспоненты) переходы совпадают.
13.03.2025: Ой ли? А не truncf()
ли?
Учитывая критику во 2-м результате от позавчера...
14.03.2025: неа, именно floorf()
!
u16_man = f32_val / powf(10, f32_exp)
"
и сделал это.
Результат -- ну да, похоже, но не совсем: что-то как-то не совсем то, шибко уж много несовпадений (бОльшая часть!).
test_FLOAT16_10E6M10_h2m | sort -gk4
"
в конце выдавал какую-то лажу: там было несовпадение порядков, и даже хуже
-- переполнение, когда большой положительный порядок превращался в большой
отрицательный.
Т.е.,
Анализ показал, что причина в неточности вещественной арифметики: там получается f32_man=8.099999e+00, что после умножения на 10 и оцелочисливания даёт 80 вместо желаемого 81.
15.01.2025@утро, перед завтраком, ~09:20: провёл кучу тестов на тему "какую минимальную добавку можно прибавлять перед умножением" -- 0.000001, 0.0000001, 0.0000005, 0.0000009. Результаты тестирования показали, что первоначальная 0.000001 оказалась наилучшей -- все остальные недостаточны: сравнение на равенство вещественных значений, полученных из исходного кода и из "вторичного кода", показало, что несовпадений -- 35, 15890, 1409, 104 соответственно.
Ну-с, засим теоретическую часть задачи можно считать решённой и переходить к практическим действиям по внедрению.
main()
добавлено, что если в командной строке что-то указано, то (вместо обычного
перебора всех кодов) для всех argv[1...argc] печатается результат
преобразования FLOAT16_10E6M10_f2s(atof(argv[x]))
-- это для
возможности легко проверять конверсию произвольных вещественных значений.
Сделано несколько таких проверок, включая отрицательные и т.п. -- да, всё выглядит как должно быть.
FLOAT16_10E6M10_f2s()
снабжён большим количеством
комментариев с пояснением нюансов.
FLOAT16_10E6M10_h2m()
,
с удалением старого "не очень работающего" кода.
modbus_mon -Dd /dev/ttyS0 +1:FLOAT16_10E6M10:HOLD:0x00/2=1.023e34,1.022e34
-- судя по дампу "отправляемых" пакетов, значения генерятся разумные.
В принципе, не есть великая проблема добавить поддержку такой сущности, но есть нюансы.
11.02.2025: краткое обсуждение:
Поэтому придётся вводить не 2 типа конверсии -- INT48_LE и INT48_BE, а ещё парочку беззнаковых -- UINT48_LE и UINT48_BE.
Причём
DATA48_LE_h2m()
и
DATA48_BE_h2m()
INT48_LE_m2h()
, INT48_BE_m2h()
,
UINT48_LE_m2h()
, UINT48_BE_m2h()
.
Причём где-то надо будет брать значение старших 16 бит -- 0 для беззнаковых и 0x0000 либо 0xFFFF для знаковых. И то, КАК это делать -- не вполне очевидно и на простую и прямолинейную архитектуру 64-битной конверсии с перестановкой байтов не очень-то ложится.
Возможно, надо будет вместо прямолинейного копирования байтов по индексам
сразу в h_data[]
сначала заполнять 8-байтную ячейку, потом брать
её как 64-битное слово, производить расширение знака и результат
складывать в h_data[]
.
19.02.2025: блин, да что за бред! Просто делать
формирование 64-битового значения битовыми сдвигами и OR'ами, как в
dds300_drv.c::dds300_rd_int64()
; ну да, в
_LE
- и _BE
-вариантах разные байты на разное число
бит сдвигать. А потом -- да, выполнять расширение знака (только для
знаковых) и складывать результат в h_data[]
.
17.02.2025: Кравец согласился добавить перед 48-битными значениями в его устройствах лишний пустой 16-битный регистр, так что можно будет обойтись конверсией INT64_BE (там числа всегда беззнаковые, так что проблема расширения знака не стоит).
Но на будущее -- вдруг таки попадётся что-то -- готовый проект есть.
Но у нас такое не поддерживается вообще никак (впрочем, и в EPICS'ном Modbus-модуле и в TANGO'вском "Modbus class" тоже).
Реализация принципиально затруднительна, т.к. непонятно, как наполнять пакет и как дешифрировать. Но имеет смысл порассуждать, как МОЖНО было бы реализовать, хотя бы какие-то аспекты.
11.02.2025: рассуждения.
ProcessIO()
по частичному пакету, основываясь на коде команды.
А для user-defined-команд формула вычисления неизвестна.
kind
, например, как
kind=1000+function_code: поскольку нынешние значения kind
лежат в диапазоне 0-3 (а потенциальные всякие "Read Device Identification"
тоже заведомо невелики), то гарантированно не перекроется.
op
, прямо как op=function: опять же,
поскольку нынешние MODBUS_OP_READ
=0 и
MODBUS_OP_WRITE
=1, то перекрытия не будет.
Несколько замечаний:
MODBUS_MEDIA_UDP
(и, соответственно, считать
бинарно "connect() и возможная пауза перед готовностью -- при MEDIA_TCP" уже
нельзя, т.к. для UDP надо делать
socket()
+connect()
, но паузы не надо (т.к.
"готово").
RegisterWithFDIO()
станет сильно сложнее, т.к.
добавится FDIO_DGRAM
;
Назовём утилиту -- в соответствии с принципом от 01-09-2012 из bigfile-0001.html -- "modbus_mon".
*modbus* DEVSPEC [COMMAND...]
ascii_over_tcp:IP-ADDRESS
т.е., возможные варианты видятся "tcp", "rtu_over_tcp", "ascii_over_tcp",
"rtu", "ascii"; а после двоеточия -- уже конкретный адрес, будь то IP (либо
IP:PORT, как минимум для MOXA NPort) или имя устройства ("/dev/ttyMX5"; но
остаётся вопрос настройки RS485).
[CONV:]TYPE:ADDR[/COUNT][=VALUE{,VALUE...}]
где
modbus_conv_table[*].name
, при неуказанности считается за
UINT16
(хотя для coils и discrete inputs -- хбз).
HOLD:1000
либо "Modbus-style"
41001
либо извратно:
4:1000
(все три спецификации эквивалентны, но первая и последняя указывают адрес
явно).
P.S. Кстати, указания для драйвера надо делать аналогично; в идеале -- свалить бы это в общий кусок кода. 21.07.2023: из-за CONV%DPYFMT может не получиться.
401001
-- ВСЕГДА 6 цифр, из которых последние 5 являются адресом плюс 1.
4x03E8
-- шестнадцатиричная нотация.
:[microseconds-to-wait]
(видимо, просто ради задержки перед отправкой следующей команды), ...
:
" -- это просто режим сниффинга, когда монитор запускается на
отдельном ("третьем") порту RS485-линии.
21.07.2023: вот только сам режим сниффинга -- дико
проблемная штука, из-за того, что в пакетах ОТСУТСТВУЕТ указание
"направления" (хост устройству или наоборот), а сами пакеты при этом
различаются -- причём авансом это понять невозможно; а в случае конкретно
RTU, где длина ПАКЕТА в явном виде отсутствует и должна быть угадана из PDU,
для кодов 1,2,4,3 в ответном пакете "число байт" расположено в том же месте,
что первый байт "адреса первого", а для 15,16 начала ответов те же "адрес
первого, количество", но затем просто окончание PDU. Так что даже просто
парсинг RTU -- та ещё задачка, для которой придётся использовать
FDIO_STREAM_PLUG
.
-f
FILENAME
", как в cdaclient, но только тут читать и исполнять именно
по мере поступления; тогда можно просто через pipe от другой программы
скармливать.
КАК указывать? "[@PARAM=VALUE{,PARAM=VALUE}:]
"? Сгодилось
бы для psp_parse(...,terminators=":")
.
Но там обозначением префикса-времени является как раз '@', так что нельзя.
(И чтоб в перспективе подходило для сбагривания histplot'у, но там префикса нет, так что задача чуть отдельная.)
Пока что напрашивается опять точка --
"[.PARAM=VALUE{,PARAM=VALUE}]:
"; т.е., например,
".times=0,period=1000,timeout=1000:...
".
Кроме одной мелочи: а если всё-таки захочется уметь НЕСКОЛЬКО команд выполнять периодически? Совсем-совсем никак и только через pipe, или можно придумать способ указания таких групп?
Напрашиваются какие-нибудь "скобки", для указания числа и интервала повторений содержащихся в них команд (а таймауты -- уже по-командные), и чтоб при отсутствии "скобок" они бы применялись к единственной команде. Но только КАК?
20.08.2023: вариант -- использовать подчерк '_', его можно прямо в командной строке указать и он ни с чем не пересекается (25.08.2023: кроме имен конверсий, но там синтаксически всё однозначно разделимо).
24.08.2023@холл перед залом круглого
стола, ~17:00: а ещё дозволять просто пробел -- тогда всё ложится на
чтение команд из файла: одна строка -- одна группа. Но можно и точку с
запятой ';' -- тогда из shell'а такие группы указывать в кавычках:
"CMD1; CMD2"
.
DecodeModbusPacket()
осознано, что насчёт конверсии есть
нюансик: при ОТПРАВКЕ-то конверсия берётся из спецификации; а при
ПОЛУЧЕНИИ?
-C CONV:TYPE:ADDR
",
который бы не приводил ни к какому В/В, а лишь складировал бы в табличку код
конверсии.
поэтому можно предусмотреть возможность указывать СПИСОК конверсий "по
умолчанию", для отсутствующих в таблице каналов -- что-то типа "-U
CONV{,CONV...}
", и чтоб печатались результаты преобразования во ВСЕ
эти варианты.
...естественно, с ограничением "подходит по размеру данных" -- т.к. UINT32 нельзя использовать ни для единственного UINT16-значения, ни для вектора, не подходящего по кратности (нечётной длины; а для FLOAT64 -- не кратного 4).
%DPYFMT:
" (и для "списка конверсий" указывать при каждом).
UINT16%x:
" или
"FLOAT64_LE%8.3f:
".
Так устраняется проблема "фиг знает, целочисленный или вещественный тип будет у данных, т.к. этот тип указывается позже".
24.08.2023: сделано в modbus_parse_spec()
.
...а вдруг захочется видеть все варианты скопом? В стиле "21845=0x5555=052525"?
24.07.2023@утро, чтение описания клятого РПМ-416 во время несостоявшейся планёрки, ~10:20: одно из требований к формату вывода modbus_mon -- чтоб он по умолчанию просто печатал бы прочитанные данные, как это делает cdaclient. Т.е., чтоб можно было так же использовать его в скриптах.
@613-я, ~11:10: и вообще постараться
настраиваемый формат вывода сделать аналогичным, в т.ч. с префиксами времени
по "-DT
" и с префиксами "имён" по "-Dn
". Это
позволит при надобности использовать утилиту для мониторирования и
сохранения истории, с возможностью скармливать результирующие файлы
histplot'у (как уже сейчас можно от cdaclient и das-experiment).
30.07.2023: кстати, как-то надо уметь в командной строке указывать station_id:
Напрашивается опциональный префикс "/STATION_ID:
".
(Первоначально думалось о префиксе c точки --
".STATION_ID:
", но этот вариант не подходил бы для указания в
DEVSPEC, т.к. конфликтовал бы с указанием IP-адресов или FQDN'ов без порта.)
01.08.2023@около проходной ОК, по
дороге на обед в Гуси, ~13:30: чисто по таблице ASCII остаётся
лишь 4 нейтральных (для shell'а) символа: ',', '.',
'/', ':' (не считая странно выглядящего и
не-пришей-кобыле-хвост '^').
Ну и что -- разрешать ЛЮБОЙ из этих символов, при условии, что он в конкретной точке не может иметь иного значения (например, "HOST:PORT:ID")?
@дома, ~15:30: а если плюс -- '+'? С IP:портами вообще никак не пересекается; в именах файлов в DOS/Windows запрещён (@вечер: неа, похоже, только в DOS 8.3). А начинать с него параметры команды разрешается беспроблемно -- иногда это "инверсия смысла опции".
Т.е., для MOXA Nport COM1 будет
rtu_over_tcp:192.168.1.19:950+123
для просто TCP --
tcp:192.168.1.12+123
а в командах --
+123:INT32_BE:HOLD:1000
07.08.2023@утро-зарядка:
В идеале -- не копировать, а использовать прямо тот же код. Но, увы, не удастся -- там слишком много завязок на cda.
11.08.2023: некоторые соображения по scheduling'у, вроде тривиальные, но заслуживающие явного проговаривания:
ЛУЧШЕ ВСЕГО -- просто тупым циклом
"while (fgets(...) != NULL)
".
А исполнение команды (в т.ч. цикла повторений) должно начинаться с
sl_main_loop()
и заканчиваться вызовом sl_break()
.
...хотя как именно это должно быть интегрировано в функцию "исполнить команду", в т.ч. как соотноситься с исполнением ОТСЫЛКИ -- пока не вполне ясно.
Видимо, отсылка должна исполняться не сразу-напрямую, а заказываться в таймауте с задержкой 0 -- он-то точно исполнится первым.
В таком случае нужно будет на время работы с линией "отключать" чтение из
файла, а на время ожидания команд из файла "отключать" линию. И это при том, что
регулированием текущих SL_-масок занимается сам fdiolib и вмешиваться в его
работу не стоит, так что только fdio_deregister()
.
Откуда очевидным образом следует, что НЕ стоит файл читать fdiolib'ом.
24.08.2023@холл перед залом круглого стола, кресло со стороны столовой, лицом к круглому столу ~17:00: общая структура утилиты напрашивается такая:
main()
работает "общим менеджером" -- парсит командную
строку и, при надобности, указанные файлы строка-за-строкой, сбагривая
команды (не разбирая их, т.е., это могут быть и ГРУППЫ) функции-исполнителю.
PerformIO()
" --
организует как отправку команд и печать ответов, так и надлежащие повторения
и в требуемом количестве и с требуемым периодом.
25.08.2023: также команда задержки может быть просто среди этих команд в группе, что позволит "прореживать" исполнение группы паузами при необходимости.
-K
" ("keep going after I/O errors (do NOT exit)").
25.08.2023@ключи, домик, веранда-предбанник/крыльцо, ~16:30: насчёт вопроса о "взаимодействии этого всего с собственно коннектами и реконнектами":
Т.е., в начальной, "синхронной" части "PerformIO()
" -- только
подготовка, в виде
sl_main_loop()
.
(@дома, ~18:30: или заказ
"через 0 микросекунд" делать по FDIO_R_CONNECTED
?)
@дома, ~18:40: Некоторый вопрос по тому, как реализовывать "периодичность":
06.09.2023: этот флаг стал ненужен -- логика
TryNextCommand()
обходится без него.
sl_break()
, если всё исполнено).
period=0
).
27.08.2023@утро-зарядка: ещё пара замечаний в ту же сторону, насчёт "когда слать":
is_ready
", обозначающий
приконнекченность, и использовать его как обязательное условие для
возможности отправки.
Смысл -- это чуток повысит быстродействие, т.к. действия будут выполняться без лишних пауз (например, отправка следующего пакета -- сразу после получения ответа на предыдущий).
27.08.2023: начато наполнение скелета. Парсинг ключей
командной строки копируется из cdaclient.c (включая
-D
), с удалением лишнего и небольшими изменениями, с
подглядыванием в прочие uspci_test.c и canmon_common.c.
28.08.2023: продолжаем.
main()
сделан: проверяет обязательное
присутствие DEVSPEC, вызывает его парсинг и затем инициирует подключение к
устройству вызовом StartConnect()
InitiateStart()
.
После чего идёт по оставшимся argv[], вызывая либо исполнение команды, либо чтение из указанного файла.
ReadFromFile()
целиком взято из cdaclient.c (включая
rtrim, ltrim и пропуск @TIME в начале строки); а, ну да -- ещё "отработка"
строки делается вызовом PerformIO()
вместо
"RememberChannel()
.
Оно компилируется (хотя в бинарник пока не соберётся из-за отсутствия функций-"мяса").
30.08.2023@Зеленоград: стало очевидно, как именно надо парсить параметры групп (число повторов и период) и индивидуальных команд (таймаут):
Таким образом, для первой команды будут распознаваться все параметры, а для последующих -- только таймаут.
P.S. equals_c='=', separators=",", terminators=":".
31.08.2023: насчёт возможности указания "trans_id" в описателе команды (и печати его, если попросят):
+123,45678
" (StationID=123, TransID=45678).
modbus_parse_spec()
сделан.
Но для этого надо освободить её от функции "символ-префикс-интродуктор параметров", т.к. формально возможен синтаксис с указанием ТОЛЬКО TransID (и он даже более осмыслен, т.к. оно поддерживается только в Modbus-TCP, в котором как раз StationID малоосмысленно).
Получится синтаксис вида "/timeout=100000:+123.45678:...
"
(изначально 02-05-2023 именно так планировалось для
указания поканальных параметров в histplot, но 19-05-2023 было осознано, что
это пересечётся с PK_FILE
).
31.08.2023@дорога домой из Гусей: возможно, полезен также будет ключик "выдавать побайтный дамп Modbus-пакета".
-Dd
" (Dump)? 09.09.2023:
сделано.
-DD
"?
-Ds
" (print Sends) при указанном "-Dd
"
надо и дамп отправляемого пакета показывать.
01.09.2023: сделан ParseDevSpec()
--
больше сотни строк в основном всяких проверок и ругательств.
conn_types[]
(где для
каждого указаны media
и proto
), и если есть, то
сохраняется, а если нет, то "предполагает" тип: начинается со '/'
-- SERIAL,RTU, иначе -- TCP,TCP.
string_begins_with()
, которой кроме ключа-префикса указывается,
какой за ним должен идти символ (обычно ':' или '=').
Потом пригодится и для парсинга параметров команд -- определять факт наличия этих параметров.
02.09.2023: да, сделана
string_begins_with_param()
, принимающая вторым аргументом
указатель на PSP-таблицу и перебирающая все параметры в ней на тему "а не
string_begins_with()
ли с именем этого параметра".
cmdopts_t
и парная ей таблица text2cmdopts[]
(та
самая из 3 параметров, из которых первые 2 (period и
count) "групповые", а третий (timeout) "индивидуальный".
Но больше в ту сторону пока ничего не сделано.
04.09.2023: заготовка для "машины исполнения команд":
cmdinfo_t
.
command_list
(с объёмом в байтах
command_list_bytesize
и количеством в штуках
command_count
), "контекст исполнения" --
command_idx
.
period_length
и
period_count
(на них будут отображаться PSP-параметры
period
и count
); по умолчанию 0
и
1
(count=0 -- бесконечно).
06.09.2023: period_idx
.
period_has_passed
и
all_commands_done
.
06.09.2023: all_commands_done
убрана за
ненадобностью.
05.09.2023@после обеда в Гусях дорога от Эдема домой: итого, у нас запланированы:
05.09.2023: приступаем к реализации "мяса" -- в первую
очередь PerformIO()
.
Основное применение -- файлы со "сценариями".
string_begins_with_param()
'ом
на "префикс" с PSP-параметрами (для 0-й команды все 3, для следующих только
1 последний) и если они указаны, то парсятся, ...
:NNN
", ...
modbus_parse_spec()
, причём тут можно указывать ВСЕ "нюансы" --
включая и Station-ID, и Transaction-ID (которые при неуказании считаются за
-1
и будут "наследоваться от контекста").
malloc()
'ом).
Ровно как это делается в modbus_tcp_drv.c (а туда модель взята из cxsd_hw.c).
-i
" (Ignore) делается просто
return
, а при неуказанности производится генеральный облом
exit(EC_USR_ERR)
.
Смысл такой фичи игнорирования -- для чтения из pipe'ов: мало ли где/как
пригодится (например, интерактивная работа, вроде
"modbus_mon ... /dev/tty
").
sl_main_loop()
, перед которым надо
будет добавить старт исполнения.
Теперь пора заниматься блоком исполнения.
06.09.2023: (записано утром 07.09.2023) ключевая часть "виртуальной машины асинхронного исполнения" сделана.
static void StartNewPeriod(void) { command_idx = 0; if (period_length > 0) sl_enq_tout_after(0, NULL, period_length, period_end_proc, NULL); else period_has_passed = 1; }
period_end_proc()
содержит
period_has_passed = 1; TryNextCommand();
// Return value: 0 -- done, 1 -- in progress static int TryNextCommand(void) { while (1) { if (!is_ready) return 1; // End of sequence? if (command_idx >= command_count) { if (!period_has_passed) return 1; if (period_count <= 0 || ++period_idx < period_count) StartNewPeriod(); else { sl_break(); return 0; } } if (command_list[command_idx].kind == MODBUS_KIND_NONE) SleepBySelect(command_list[command_idx++].timeout_us); else { SendNextCommand(); return 1; } } }
Философские комментарии:
process_commands()
и (в меньшей степени)
vdev_set_state()
.
А вот на seqexecauto.c::ExecuteFrom()
похоже не
особо.
Технические комментарии:
Переход между периодами делается прямо там же, после чего сразу производится попытка выполнить следующее действие.
!is_ready
" проверяется перед каждой итерацией попытки
исполнения -- на случай, если после очередной команды связь порвалась (мало
ли -- при отправке обнаружился облом).
SleepBySelect()
прямо "по
месту", т.к. реализация через cxscheduler'ные таймауты была бы шибко
проблемной: не говоря об общей громоздкости, пришлось бы ещё как-то
блокировать на это время приход пакетов.
all_commands_done
очевидным образом стала ненужной и
убрана.
int
, чтобы указывать вызывающему -- только
в лице PerformIO()
-- надо ли реально запускать цикл, или же
все действия цепочки уже выполнены (это возможно в случае, если
единственными "командами" были паузы).
PerformIO()
выглядит так:
// Reset "virtual machine" context to beginning period_idx = 0; StartNewPeriod(); if (TryNextCommand()) sl_main_loop();
StartNewPeriod()
будет вызываться
также
FDIO_R_CONNECTED
;
Теперь остались 2 больших куска:
07.09.2023: блок установления и поддержания соединения "сделан", путём копирования из modbus_tcp_drv.c; с учётом специфики утилиты там чуток упростилось, но вообще громоздкенько. Интересных деталей никаких, рассказывать особо не о чем.
09.09.2023: продолжаем.
SendNextCommand()
; там и оригинальный-то
mb_sender()
несложен, а тут минимальная адаптация под другой
источник данных плюс "наследование" station_id
.
PerformIO()
была
добавлена/скопирована "работа" с num1
и conv
:
пересчёт "количества элементов" в реальное число регистров по данным из
modbus_conv_table[conv]
плюс ограничение.
10.09.2023: и в ветку "для записи" (при наличии
'=') добавлены начальные мозги -- вычисление wr_size
,
требуемого объёма в сегменте кода, плюс проверка, что запись указывается
записывабельному типу регистров, COIL или HOLD. Парсинга же пока нет.
С пока что рудиментарной печатью.
-DD
", отображающийся на print_dumps
.
10.09.2023: переделано на "-Dd
".
DumpPacket()
, вызываемый и при
отправке, и при приёмке.
Для TCP и RTU данные выдаются шестнадцатиричкой, по 2 цифры на байт; для ASCII -- строкой, причём не-':' и не-шестнадцатиричные, которых по идее быть не должно, печатаются в формате "\xNN".
-Dt
", "-Ds
", "-Dr
" и флаги
print_timeouts
, print_sends
,
print_wr_replies
соответственно).
А потом начинаем проверять.
for()
" имелось
лишнее "p++
", из-за чего после разбора команд оно перескакивало
через '\0' и пыталось парсить environment.
p++
-- пропускание
двоеточия -- перед strtol()
.
command_count = idx;
".
Они были оперативно найдены и исправлены, и, ...
period
и count
тоже
работает, хотя тоже вылезла пара косяков:
period_end_proc()
вызывал TryNextCommand()
безусловно, прямо посередь ожидания ответа (а надо -- только при
"command_idx >= command_count
").
StartNewPeriod()
НЕ делал
"period_has_passed = 0
" -- из-за чего корректно работал только
первый период, а последующие всегда считались завершёнными и длительность
игнорировалась.
Итак, битым текстом: задуманная схема "скриптинга" РАБОТАЕТ В ПОЛНОЙ МЕРЕ! И цепочки команд, и возможность указать число повторов для цепочки, и периодичность (длительность) исполнения одного повтора.
11.09.2023: сделана печать.
PrintDataData()
, модифицированная копия
PrintDatarefData()
.
data
, dtype, nelems, dpyfmt.
name_printer
-- выдаёт "имя" в нужном формате;
вызывается при != NULL.
data_printer
-- служит для выдачи нестандартных
данных, конкретно -- 1-битовых (COIL и DSCR), которые общая инфраструктура
печатать не умеет.
При указанности -- != NULL -- используется вместо собственного цикла выдачи.
PrintRegName()
-- печатает "имя" в соответствии с
print_
-флагами.
PrintBitData()
-- печатает битовую цепочку; указывается
в ветке COIL и DSCR.
PrintDataData()
передавался buf
, в точности как для 16-битовых. Но у тех-то
предварительно делается конверсия из m_data
в этот
buf[]
, а тут -- нет.
В результате печаталась какая-то чухня -- мусор, имевшийся в стеке.
Исправлено заменой на передачу сразу m_data
.
24.09.2023: продолжаем.
-q
"/option_quiet
просто не делается никакая
печать данных.
modbus_num1_of_nelems()
и modbus_nelems_of_num1()
-- в modbus_conv.h оно было сделано и использовано в
modbus_tcp_drv.c ещё 13-09-2023, а сейчас задействовано и тут.
-hh
") добавлены
выдача списка типов конверсии, таблица с описанием форматов адресов
регистров, плюс в почти свободной форме "Simple scripting abilities".
28.09.2023: за вчера-сегодня сделана поддержка Modbus-RTU и работы с serial-link'ом.
ProcessIO()
добавлена реакция на
FDIO_R_HEADER_PART
, анализирующая function code и в зависимости
от него (и поля nbts у ответов на чтения) посредством
fdio_advice_pktlen()
указывающая fdiolib'у длину пакета.
Код получился довольно короткий и очевидный.
RegisterWithFDIO()
теперь 3 ветки-варианта
регистрации дескриптора, в зависимости от протокола -- TCP, RTU, ASCII -- и
для последней пары указывающие тип FDIO_STREAM_PLUG
и
FDIO_STRING
соответственно (это для
MODBUS_MEDIA_SERIAL
, а иначе -- _CONN
-варианты).
А параметр fdtype
убран (он был унаследован от
remdrv_drv.c, где был обусловлен
DRVLET_FORKED
-драйверами).
Всё это же есть и в modbus_tcp_drv.c (реально сначала именно там делалось, сюда копировалось).
modbus_path[PATH_MAX]
...
ParseDevSpec()
.
InitiateStart()
теперь при не-MEDIA_TCP вместо
создания и connect()
'енья сокета делается open()
.
30.09.2023: тогда была забыта настройка --
tcsetattr()
; добавлена, копированием из
stdserial_hal.h.
-K
" оно не отваливает, а продолжает попытки открытия, ровно
как коннекченье. Это имеет смысл для USB-serial-адаптеров.
ReportConnfail()
/ReportConnect()
адаптирована на
использование строки "open" вместо "connect" при
не-MEDIA_TCP.
А вот этого в драйвере нету -- там оно просто бессмысленно, т.к. без layer'ов малополезно.
29.09.2023: "шлифовка".
timeout_end_proc()
отсутствовало "command_idx++
".
-t MICROSECS
", маппирующийся на
option_def_tout_us
-- это умолчательное значение для
по-командных таймаутов (само оно по умолчанию =0).
strcurtime()
заменены на
strcurtime_msc()
.
Проверялось "вхолостую", на никуда не подключенном /dev/ttyS0, командой
modbus_mon -t1000000 -DTdst /dev/ttyS0 40001
01.10.2023: console_mon_util.h дополнен кодом парсинга, также позаимствованным из console_cda_util.h.
ParseDataVal()
как аналог ParseDatarefVal()
.
util_refrec_t *urp
передаются dtype
и n_items
.
uint8 *databuf
.
RAISE_EC_USR_ERR()
, то перед
#include "console_mon_util.h"
делается
#define RAISE_EC_USR_ERR() do{return -1;}while(0)
02.10.2023: реализована поддержка записи. Поскольку основная "сложность" -- парсинг -- была решена ещё вчера копированием готовенького, то особых трудностей не встретилось.
CXDTYPE_UINT8
), а затем
конверсии путём вызова modbus_bits_h2m()
и
modbus_conv_table[conv].h2m()
соответственно.
datasize
+wr_size
.
Работает!!! Проверялось на РПМ-416 -- в т.ч. цепочкой "запись пароля; запись регистра управления подсветкой 2072; чтение регистра управления подсветкой" -- работает в соответствии с предполагавшимся сценарием, аналогично работе через modbus_tcp_drv.
09.10.2023: сегодня попробовал чтение 1-битных каналов -- COIL и DSCR -- на жариковском PLC от Siemens; работают -- что-то читается и распечатывается. Т.е., корректно работает формирование пакетов, дешифрирование пакетов, вытаскивание данных и распечатка. 04.01.2023: ага, РАСПЕЧАТКА-то работала некорректно; но тогда проверить не смог, т.к. смысл 1-битных данных был неизвестен.
В т.ч читалась ПАЧКА из 5 регистров с адреса 65535, т.е., за границей 65536; видимо, аналогично 1MB+64kB-16B в 80286 из-за сегментной адресации.
10.10.2023: пара мелочей насчёт вывода:
-Du
, отображающийся на
print_unit_ids
и приводящий к выдаче "+UNIT_ID:".
Работает только при включенном -Dn
-- при отсутствии "имени"
регистра и префикс печатать не к чему.
PrintDataData()
разделитель, печатаемый при взведённом UTIL_PRINT_NAME
после
имени, переделан с пробела " " на "=".
Смысл -- что modbus_mon (в отличие от cdaclient) воспринимает пробелы как разделитель не между именем и значением, а между РАЗНЫМИ командами, поэтому пробел он будет считать окончанием команды, т.е. -- командой чтения, а значение попробует интерпретировать как следующую команду.
Так же получается цельная команда "РЕГИСТР=ЗНАЧЕНИЕ", так что вывод modbus_mon'а теперь можно скармливать ему же на вход (раньше -- не сработало бы).
Памятуя существующую функциональность чтения команд из файла, позволяющую в т.ч. работать "в интерактивном командном режиме", пора бы её проверить.
isatty()
смотрят на терминал, то перед чтением
команды выдаётся prompt "> ".
string_low:hold:51/13=admin
") программа abort'ится с
диагностикой
*** Error in `[ПУТЬ_ПОЧИКАН]/modbus_mon': corrupted size vs. prev_size: 0x0000000001bfcea0 **
Похоже, что проблема есть всегда, просто проявляется лишь при чтении из
файла -- диагностируется по fclose()
.
12.10.2023: нашёл причину abort'енья: в
PerformIO()
инкремент объёма данных
"datasize += wr_size
"
при парсеньи делался только на 2-м проходе (т.к. внутри общего сохранения в
"сегмент данных").
Исправлено тривиальным вынесением инкремента наружу проверки (тем более,
что wr_size
считается корректно всегда -- для OP_READ-команд он
делается =0).
25.10.2023: наконец-то проверил работу Modbus-RTU через serial-link /dev/ttyUSB0 с метеостанцией "Сокол" (Паспорт и руководство пользователя по метеостанции СОКОЛ-М.pdf, Редакция от 29.08.2022).
Вкратце главное -- РАБОТАЕТ!!! Просто сразу же в ответ на HOLD:0/90 вернуло тучу данных, которые соответствуют вычитываемому по BlueTooth.
Но есть пара нюансов конкретно с этой метеостанцией:
А вот 1-й блок -- 512-527, HOLD:512/$[527-512+1] -- читается ТОЛЬКО ЦЕЛИКОМ. Попытка прочитать любой одиночный адрес, кроме 512 -- также просто нет ответа.
Насчёт настройки serial-порта:
serial_hal_opendev()
(только вместоnewtio.c_cflag = B19200 | CRTSCTS*1 | CS8 | CLOCAL | CREAD; newtio.c_iflag = IGNPAR;
B19200
в оригинале было параметром
baudrate
, передававшимся "юзером", и уже в
/piv485_lyr_common.c было загито B9600
).
tcsetattr()
.
06.01.2023: а это решено было НЕ реализовывать. Ибо нефиг.
mode COM1: ...
"),
и вот в идеале бы сделать, чтоб прямо в том стиле и можно б было указывать.
MODE COMm[:] [b[,p[,d[,s[,r]]]]] MODE COMm[:] [BAUD=b] [PARITY=p] [DATA=d] [STOP=s] [RETRY=r]
26.10.2023: приступаем; покамест всё в
ParseDevSpec()
:
text2comopts[]
пока простая -- содержит лишь
список скоростей, от 50 до 115200, PSP_FLAG()
'ами.
29.10.2023: вроде доделано, за вчера и сегодня.
c_cflag
.
Поэтому ВСЁ можно делать PSP_FLAG()
'ами,
позиционно-независимо (это осознано ещё несколько дней назад).
int
-поля;
c_cflag
потом записываются просто OR от них 4.
PSP_FLAG()
'и, то тривиально
делаются "умолчания" -- это 9600,n,8,1.
А вариант скорости "nochg"/B0
("don't touch") был
закомментирован. Нафиг-нафиг такие неопределённости.
comopts_t
, табличка --
традиционно text2comopts[]
, переменная этого типа --
modbus_comopts
.
ParseDevSpec()
.
InitiateStart()
захардкоженное
"B19200 | ... | CS8
"
заменено на OR тех 4 int
'ов.
09.11.2023: мелкое исправление логики печати: в
PerformIO()
для беззнаковых (CXDTYPE_USGN_MASK
))
целых типов при неуказанности dpyfmt назначается по умолчанию не
"%d", а "%u" -- чтоб уж беззнаковые значения и выдавались.
06.02.2024: сегодня попробовал использовать modbus_mon для диагностики той найденной вчера проблемы Сенькова, когда он вместо ответа на COIL:0x3 присылает ответ как бы на HOLD:0x0A/3, а потом EXCEPTION.
Сходу -- облом, т.к. тут, в отличие от modbus_tcp_drv.c,
отсутствовала проверка на соответствие ответа запросу (по
last_sent
); в результате оно печатало чушь:
dpyfmt
из
command_list[]
из ячейки, занятой COIL'ом, где dpyfmt просто
пуст.
command_list[command_idx]
при
command_idx
, дошедшей до конца -- т.е., уже за пределами
инициализированной части.
Поэтому приняты меры для приведения деятельности в правильное русло и устранения проблемы (делалось не совсем в описанном порядке, а доводилось итеративно, но конечный результат именно такой).
last_sent
было добавлено поле op
, и в
forget_last_sent()
ему делается =-1
.
ProcessInData()
сделано детектирование таких
ситуаций.
is_expected
, взводимое ТОЛЬКО при совпадении
kind
и op
в ответе с ожидаемыми (от запроса).
ЗАМЕЧАНИЕ: аналогично modbus_tcp_drv.c, проверяется только эта пара; unit_id же не проверяется.
addr
и num1
берутся из
last_sent
;
rpy_tid
;
command_list[command_idx]
берутся
conv
и dpyfmt
-- иначе они делаются
MODBUS_CONV_UINT16
и "%u"
.
command_idx++; TryNextCommand();
-- теперь выполняется только при
is_expected
либо при exception'е.
@вечер, записывая: последнее слегка сомнительно: ведь exception тоже должен быть соответствующим запросу (а то Сеньков присылает как раз не по теме -- даже ДО отправки очередного запроса). Вот ЭТО можно бы и в modbus_tcp_drv.c проверить как будет отрабатываться -- там актуально.
@ещё чуть позже: да, exception из условия убрано.
forget_last_sent()
НЕ делается ни при получении пакета, ни по
таймауту.
Сделано.
print_unexpected
, взводится ключом "-Dx
", а в
ProcessInData()
добавлена печать сообщения с информацией о
реально полученном и об ожидавшемся.
(Вначале было
Но "
- @поднимаясь по лестнице пристройки нашего крыла на лабораторный круглый стол, между 2 и 3 этажами, ~09:45: а не добавить ли флажок "print Unidentifiable replies" -- выдавать сообщения о непонятных ответах, несоответствующих запросам (типа вот этого сеньковского)?
Ключ --
-Du
. Диагностировать можно как last_sent.addr<0, так и по несоответствию типа регистров и/или операции между last_sent и пакетом.- ЧУТЬ ПОЗЖЕ: с другой стороны, оно и так покажет странный адрес -1.
-Du
" уже занят под "print ...with UNIT_ID".)
Теперь вроде ведёт себя как надо.
ЗАМЕЧАНИЕ: реализованное поведение чуток отличается от драйверова, но это обусловлено различием в их идеологии: в драйвере есть пере-отправка по таймауту (а тут нет), в утилите же есть исполнение цепочки команд (а в драйвере нет).
06.02.2024@дома, ~21:30: пара дополнений:
forget_last_sent()
в приёме делается только при
is_expected
(ибо раз "не тот", то и нефиг, а надо дождаться
"того");
is_expected
, а exception вариант условия убран (по той же
причине).
И вот это уже нужно проверить вживую.
07.02.2024: проверено -- да, работает, не зависает. Диаграмма "событий" отличается -- стала короче -- надо проанализировать и объяснить.
08.02.2024: разобрался в причине укорочения диаграммы "событий": отсутствует строчка "UNEXPECTED reply R COIL", идущая префиксом к тому неожиданному EXCEPTION'у.
Почему -- хитрО:
forget_last_sent()
, так что EXCEPTION считался за UNEXPECTED.
08.02.2024: пара мелочей касательно "красоты" диагностики:
А миллисекунды тут ОЧЕНЬ нужны -- сильно помогает при разбирательствах.
Так что теперь console_cda_util.c::PrintDataData()
использует для выдачи strcurtime_msc()
.
modbus_strop()
чисто для modbus_mon'а, выдавать в
диагностику 0:"R",1:"W",-1:"-",else:"?".
Также в modbus_strkind()
добавлено отдельно "детектирование"
кода -1
, на него теперь отдаётся строка "none".
12.02.2024@~14:00, идя по
нечётной стороне Лаврентьева из ИЯФ в Эдем, проходя мимо остановки
ИЯФ: (после общения с чеблопашиными студентами Максимой и Данилом,
которые используют modbus_mon для получения данных от метеостанции Сокол-М1,
и там бывают странности ("иногда начинает присылать нули")): а не делать ли
вывод диагностики на stderr (ключ "-2
"?) или вообще в какой-то
другой файл (ключ "-O DIAGFILE
"?)?
(А подумано ещё и потому, что неделей ранее, на второй пересдаче у Иртегова 07-02-2024 (или всё-таки 01-02-2024 на первой пересдаче?)) он вёл с одним из студентов разговор "куда надо выводить диагностику? ну да, на stderr же!".)
17.02.2024@15:10, по дороге в девятиэтажку за молоком, идя между П26 и стадионом НГУ: а не сделать ли возможность, чтобы при ПОЛНОМ ОБЛОМЕ соединения -- когда до устройства достучаться вообще никак не удалось (например -- connection timed out), выдавалась бы какая-то диагностика, позволяющая программно увидеть этот облом из программ, использующих modbus_mon через shell/popen?
-T 10
": если за 10 секунд не
приконнектилось, то и абзац.
finish_proc()
выдавать какую-то диагностику.
...например, "# RUN_TIME_EXPIRED".
-D/
".
@21:00: ну делаем.
print_timelim_exp
, взводится ключом
"-D/
".
finish_proc()
при нём взведённом печать.
Да, работает.
xx.0x.2024:
04.08.2015: и есть идея делать всё не в programs/runner/, а -- для гибкости -- библиотеку в своём отдельном месте в lib/, а Motif'овский cx-starter.c вести в xmclients/.
21.08.2015@пляж: еще некоторые соображения по cx-starter'у.
Это позволит при надобности создавать кроме cx-starter'а другие GUI.
Проект решения в принципе есть, за вчера-сегодня.
Засада в том, что если в CXv2 список серверов конкретного клиента был заранее известен и фиксирован, то в v4 всё сложнее. В нынешнем варианте без broadcast-резолвинга будет пока так же, но потом-то в самом начале список будет гарантированно пустым (при старте никаких ответов еще не будет получено), а потом "подрастёт".
В голову лезут разные идеи, среди них:
Выглядит кривовато.
Не нравится неопределённостью: а почему секунда, а вдруг достаточно меньше или понадобится больше? Кроме того -- а если по ходу дела каналы переедут между серверами, тогда что?
24.11.2015: идеологическая проблема была решена проще:
введено событие CDA_CTX_R_NEWSRV
, генерящееся
при создании нового sid'а в cda_dat_p_get_server()
.
Отдельный вопрос по теме "лампочек" -- нижний уровень реализации, в cda:
В v2 был обязательный приход данных с некоей периодичностью. В v4 -- совсем НЕ обязательно; хотя есть событие "CYCLE" -- может, по нему?
А для других типов протоколов как?
...видимо, кроме srvname возвращать также и "typename".
01.12.2015: да, введена
cda_status_srv_scheme()
.
(Воспоминания появились @обед-выехав-из-ИЯФа вниз на Лаврентьева (ЧеблоПаша: как у вас кто-то смог сделать "kill -9" серверам?! (после попытки понять, куда исчезли ВСЕ запущенные cxsd на ring1 (не Роговский ли? Нет, отрицает. Хамло?))) и @обед-дорожка-от-Амиго-к-машине (ЧеблоПаша: а в чём проблема, чтоб сервера запускались из-под root? Как из-под оператора запускаются?! Чё это вдруг?!))
02.12.2015: за последние пару недель более-менее сделано. Хаковастенько, с признаками быдлокодинга, но зато работает.
reportinfo()
".
RunCommand()
& Co.
SearchForWindow()
.
PointToTargetWindow()
.
ChangeClientStatus()
и ChangeServerStatus()
.
Халтура, да -- потом надо будет по мере надобности (изготовления сопутствующих утилит и/или плагинов) украсивить.
fgets()
,
а не через ppf4td.
ChangeServerStatus()
сервера умеет запускать/убивать как
v4'шные, так и v2'шные -- различает их при помощи
cda_status_srv_scheme()
cda_del_context()
(в первую очередь
удаление серверов), то оное просто не будет функционировать корректно.
Что НЕ сделано, и надо будет произвести для "полировки":
reportinfo()
.
argv0
везде, где он нужен -- а то сейчас на
это забито, используется NULL; выезжает только за счёт того, что в
CdrLoadSubsystemFileViaPpf4td
используется Linux-specific
program_invocation_name
-- под Форточками это пахать не будет.
cfgbase_t
" надо б переименовать во что-нибудь более
приличное.
02.12.2015: еще некоторые замечания, возникшие в голове по результатам изготовления:
RunCommand()
-- впихнуть последовательность "stop; sleep
1; start;".
Потребность возникла на пульте уметь запускать и РЕстартовать беркаевский VCAS.
06.05.2016: вопрос будет стандартный -- как указывать ссылку на "канал". По-простому можно тупо какой-то реальный канал указывать -- ПОКА.
Но вообще надо бы в cda сделать API "добавь такой-то сервер".
И, мол, состояние должно считаться server-side.
06.03.2017: можно ввести per-button-параметр "не выполнять вычисления", чтобы деятельность cx-starter'а ограничивалась слежением за серверами.
07.03.2017: а можно даже и глобалистичнее -- иерархию параметров:
"-noprocess"
.
Иерархия приоритетов такая:
Тут главный вопрос в другом: процессинг-то выполняется не стартером, а в
Cdr'ном ProcessContextEvent()
-- вот как бы уметь его
отключать?
09.03.2017: заглядывание в
ProcessContextEvent()
показало, что процессинг НЕ выполняется
при взведённом subsys->is_freezed
.
is_freezed
.
CDA_CTX_R_CYCLE
.
is_freezed
не аукнется ли как-нибудь?
10.03.2017: делаем.
option_noprocess
и обработка
-noprocess
.
cfgbase_t
добавлены поля
option_noprocess
, ...
...заполняемое из CfgBaseCreate()
, которому передаётся
параметром.
cfg_noprocess
, ...
...изменяемое в CfgBaseMerge()
директивами
.noprocess и .doprocess.
cfgline_t
-- поле noprocess
, ...
...процессящееся в CfgBaseMerge()
по ранее обсуждённым
приоритетам:
cfg_noprocess
;
option_noprocess
форсится в 1.
По идее, надо б было делать просто
"cp->noprocess||=cfg->option_noprocess
", но оказалось,
что в C оператора ||=
не существует (как и прочих "boolean
{AND,OR,XOR,...} assignment").
SubsysAdder()
делает sr->ds->is_freezed =
lp->noprocess
.
SubsysRealizer()
не запрашивает
CDA_CTX_EVMASK_CYCLE
при noprocess.
(Да, прямо тест был сделан, с группировкой, где одна из ручек постоянно писала chan=10-chan, и канал щелкался.)
Исправлено: в SubsysAdder()
добавлено
CdrSetSubsystemRO(,1)
.
А теперь что получилось:
Надо профилировать и разбираться?
KeepaliveProc()
.
CDA_CTX_R_SRVSTAT
(но на CDA_CTX_R_NEWSRV
-- нету).
12.03.2017: отпрофилировал, отдельно в режимах "noprocess" и "doprocess". Прекрасный результат:
CdrProcessKnobs()
занимает не шибко много
времени.
ForeachRefSlot()
и
on_cycle_otherop_dropper()
!
Т.е., преизряднейшее время занимает то, что даже и не используется (сброс OTHEROP), а в конкретно cx-starter'е оно и в принципе никогда нужно не будет!
12.03.2017@дорога-домой_(это вечер воскресенья): напрашиваются мысли:
13.03.2017: и начать, наконец, использовать параметр
options
в cda_new_context()
.
CXCF_FLAG_OTHEROP
вести себя в условиях v4, где флаги свободно
распространяются с серверного уровня на клиентский (а не срезаются все,
кроме содержащихся в CXRF_SERVER_MASK
, как делалось в
v2)?
13.03.2017: всё будет нормально: при сравнении "не взвести
ли OTHEROP" -- в cda_dat_p_update_dataset()
, сейчас отключеннос
-- стоит альтернатива: если CXCF_FLAG_OTHEROP
не взводится, то
он обязательно сбрасывается.
14.03.2017: точнее, он там сбрасывался, если НЕ обнаружено отклонение от уставленного. А сейчас сброс вытащен в безусловное действие ПЕРЕД всеми условиями.
ForeachRefSlot()
'ом:
Не сделать ли:
refinfo_t
поля для объединения всех dataref'ов
сервера в список (это всего-то плюс пара int'ов
, +8 или +16
байт/ref),
srvinfo_t
...
ri->sid
(удаление из списка "старого" сервера и добавление в
"новый").
К сожалению, есть 2 проблемы:
#define
изнутри #define
.
GENERIC_SLOTARRAY_DEFINE_nnn()
изготовить такой макрос нельзя.
#define
делался бы уже клиентским кодом.
Так что, увы, ответ, скорее всего -- "НЕ можно".
13.03.2017: теперь чуть подробнее, с данными профайлинга.
Профилировалось на p8b2 (E3-1245v2) с 2 штуками конфигов от linac3 (файл
указывался 2 раза), в течение минуты (программа грохалась вручную, т.к. kill
не катит (gmon.out не сохраняется), а xkill
неспособен найти окно иначе как по числовому id).
(далее обходимся без заголовков и менее времязатратные строчки не приводим)% cumulative self self total time seconds seconds calls ms/call ms/call name 45.39 1.87 1.87 36511 0.05 0.07 ForeachRefSlot 14.93 2.49 0.62 256373910 0.00 0.00 on_cycle_otherop_dropper 7.77 2.81 0.32 20331 0.02 0.04 CdrProcessKnobs 6.31 3.07 0.26 3448230 0.00 0.00 choose_knob_rflags 3.64 3.22 0.15 100725783 0.00 0.00 ctx_ref_checker 3.16 3.35 0.13 1255616 0.00 0.00 cda_dat_p_update_dataset 2.18 3.44 0.09 3743942 0.00 0.00 cda_process_ref 2.18 3.53 0.09 12737 0.01 0.01 FindFreeRefSlot 1.82 3.60 0.08 40852 0.00 0.02 ctx_evproc_caller 1.46 3.66 0.06 11337129 0.00 0.00 CheckRef
58.69 1.79 1.79 36440 0.05 0.07 ForeachRefSlot 20.98 2.43 0.64 255478600 0.00 0.00 on_cycle_otherop_dropper 4.26 2.56 0.13 100725783 0.00 0.00 ctx_ref_checker 3.61 2.67 0.11 12737 0.01 0.01 FindFreeRefSlot 3.28 2.77 0.10 1252039 0.00 0.00 cda_dat_p_update_dataset 2.13 2.84 0.07 40710 0.00 0.00 ctx_evproc_caller
on_cycle_otherop_dropper()
, "doprocess"
(~7.6-8.1% CPU)
19.07 0.37 0.37 3462546 0.00 0.00 choose_knob_rflags 13.92 0.64 0.27 20416 0.01 0.05 CdrProcessKnobs 10.83 0.85 0.21 16180 0.01 0.02 ForeachRefSlot 7.22 0.99 0.14 12737 0.01 0.01 FindFreeRefSlot 6.19 1.11 0.12 100725783 0.00 0.00 ctx_ref_checker 5.15 1.21 0.10 4933031 0.00 0.00 cda_get_ref_dval 4.64 1.30 0.09 1170248 0.00 0.00 cda_dat_p_update_dataset 4.12 1.38 0.08 1266398 0.00 0.00 ForeachRefCbSlot 4.12 1.46 0.08 3759647 0.00 0.00 cda_process_ref 3.61 1.53 0.07 420878 0.00 0.00 process_commands 2.58 1.58 0.05 10925 0.00 0.00 FindFreeHwrSlot 2.32 1.63 0.05 11203602 0.00 0.00 CheckRef
on_cycle_otherop_dropper()
, "noprocess"
(~4.6-5% CPU)
20.18 0.23 0.23 16180 0.01 0.02 ForeachRefSlot 17.54 0.43 0.20 1161737 0.00 0.00 cda_dat_p_update_dataset 16.67 0.62 0.19 12737 0.01 0.01 FindFreeRefSlot 11.40 0.75 0.13 100725783 0.00 0.00 ctx_ref_checker 6.14 0.82 0.07 1257887 0.00 0.00 ForeachRefCbSlot 4.39 0.87 0.05 1268411 0.00 0.00 ProcessCxlibEvent 3.51 0.91 0.04 10925 0.00 0.00 FindFreeHwrSlot 2.63 0.94 0.03 fdio_io_cb 2.19 0.97 0.03 2505232 0.00 0.00 CheckRef 1.75 0.99 0.02 3356436 0.00 0.00 ppf4td_nextc 1.75 1.01 0.02 994342 0.00 0.00 timestamp_subtract 1.75 1.03 0.02 22551 0.00 0.02 async_CXT4_DATA_IO
Дополнительно, о pult'оклиентах:
on_cycle_otherop_dropper()
;
CdrProcessKnobs()
(как и
предполагалось) и choose_knob_rflags()
(вот это неожиданно).
Итого, как стоит поступить:
options
, дабы указывать на необходимость "не
тратить время на OTHEROP".
14.03.2017: делаем.
CDA_CONTEXT_OPT_xxx
, содержащий
сейчас CDA_CONTEXT_OPT_NO_OTHEROP
и
CDA_CONTEXT_OPT_IGN_UPDATE
(о ней позже).
Биты идут с 0 (т.к. он вроде ни с кем не пересекается).
ctxinfo_t.options
, в котором сохраняется
переданное cda_new_context()
'у.
on_cycle_otherop_dropper()
делается только
при НЕвыставленном флаге CDA_CONTEXT_OPT_NO_OTHEROP
.
cda_dat_p_update_dataset()
'ном блоке
определения OTHEROP. Т.е., если эта опция взведена, то никакие проверки не
делаются, а флаг OTHEROP просто сбрасывается.
cda_dat_p_update_dataset()
при взведённом
CDA_CONTEXT_OPT_IGN_UPDATE
не выполняются (просто делается
return
).
CdrRealizeSubsystem()
добавлен параметр
cda_ctx_options
. Он просто передаётся в
cda_new_context()
напрямую в качестве options
.
С этим параметром есть некоторые сложности: формально Cdr-клиенты НЕ
обязаны знать про cda, и у них просто не будет CDA_
-констант в
пространстве имён. Посему в этот параметр передаётся следующее:
0
.
CDA_CONTEXT_OPT_NO_OTHEROP
, ...
CDA_CONTEXT_OPT_IGN_UPDATE
(ему ж значения всё равно незачем).
vdev_init()
всегда указывает
CDA_CONTEXT_OPT_NO_OTHEROP
.
Результат:
32.91 0.26 0.26 16180 0.02 0.03 ForeachRefSlot 17.72 0.40 0.14 100725783 0.00 0.00 ctx_ref_checker 15.19 0.52 0.12 12737 0.01 0.01 FindFreeRefSlot 6.33 0.57 0.05 10925 0.00 0.00 FindFreeHwrSlot 5.06 0.61 0.04 1393560 0.00 0.00 ProcessCxlibEvent 3.80 0.64 0.03 3356436 0.00 0.00 ppf4td_nextc 2.53 0.66 0.02 160928 0.00 0.00 varchan_checker 1.27 0.67 0.01 1308060 0.00 0.00 CheckSid 1.27 0.68 0.01 1286852 0.00 0.00 cda_dat_p_update_dataset 1.27 0.69 0.01 766358 0.00 0.00 cx_memcasecmp 1.27 0.70 0.01 152306 0.00 0.00 ppf4td_skip_white 1.27 0.71 0.01 43824 0.00 0.00 ProcessFdioEvent
19.67 0.36 0.36 13636804 0.00 0.00 ProcessCxlibEvent 15.03 0.64 0.28 16180 0.02 0.03 ForeachRefSlot 9.84 0.82 0.18 139165 0.00 0.01 async_CXT4_DATA_IO 9.29 0.99 0.17 fdio_io_cb 8.74 1.15 0.16 100725783 0.00 0.00 ctx_ref_checker 8.20 1.30 0.15 12737 0.01 0.01 FindFreeRefSlot 7.10 1.43 0.13 13340797 0.00 0.00 cda_dat_p_update_dataset 2.19 1.47 0.04 13555726 0.00 0.00 CheckSid 2.19 1.51 0.04 10925 0.00 0.00 FindFreeHwrSlot 2.19 1.55 0.04 HandleRD 1.91 1.58 0.04 349128 0.00 0.00 ProcessFdioEvent 1.91 1.62 0.04 208145 0.00 0.00 ProcessContextEvent
Вот теперь с чистой совестью можно считать за "done".
05.06.2020: а однако не совсем -- есть (вчера) от ЕманоФеди вторая претензия: шибко много траффика жрёт starter, около 10Mbit/s. Для пультовых-то это пофиг, а вот для Raspberry -- уже многовато.
В принципе, он как бы прав: в режиме noprocess и данные-то получать незачем -- всё равно никто их обрабатывать не будет.
Есть только вопрос: а откуда целых 10 мегабит, если слаться должны только ИЗМЕНИВШИЕСЯ каналы? И векторные тут явно не при делах -- осциллографы не нагенерили ли бы в разы больше (10Mbit, делим на 8 ~=1.3Mbyte)? Или там осциллограммы короткие, по тысяче-другой точек?
В общем, по возможной реализации есть 2 вопроса:
...если, конечно, оная подписка делается. Надо проверить, но, весьма вероятно, что она заказывается -- это ведь Cdr делает?
Проверил: неа, тут беспокоиться не о чем. ВЕКТОРНЫЕ каналы
находятся исключительно в ведении своих knobplugin'ов. Cdr же создаёт лишь
канал _devstate_ref
, смотрящий на "src._devstate". Т.е., тут
траффик практически нулевой.
Неясно нифига -- даже не вполне понятно, ГДЕ можно добиться желаемого эффекта.
Так, навскидку -- видимо, где-то в cda, чтобы реальной регистрации
КАНАЛОВ не происходило бы; а указание на это надо б давать через Cdr, путём
передачи чего-нибудь в options
. ...или прямо в Cdr и не делать
регистрации каналов, а делать вместо этого только "cda_add_server_conn()"?
Чуть позже: а вот если бы таким каналам -- которым передан флаг "не надо мониторировать" -- именно специальный тип мониторинга ставить, "CX_MON_COND_NEVER"! Но это б сгодилось только для CXv4, а для других протоколов -- никак. Да и неправильно это, задачу КЛИЕНТА вешать на сервер.
06.06.2020@пробежка-по-квартире: последовательные соображения:
cda_add_chan()
, будет вместо вызова реального
создания канала лишь делать cda_add_server_conn()
?
Ну да, некоторый хак будет, особенно в части определения имени сервера для добавления. Но если получится -- то и фиг с ним.
Ну и пусть возвращает какой-нибудь фейковый ref (а лучше --
CDA_DATAREF_NONE
), в данном случае это некритично.
А это сразу обессмысливает половину идеи CX-starter'а (программы-то он запускать сможет, а сервера -- уже нет).
Т.е., только cx_setmon()
пусть не делает (cx_ch_open()
в новом варианте).
В прочих протоколах тоже есть отдельная операция "subscribe" -- и в EPICS, и в TANGO, и в VCAS.
cx_ch_open()
обязательно (там в нём связаны вместе собственно
открытие канала и мониторинг), то можно всё же завести код
CX_MON_COND_NEVER
=0.
(Какой я предусмотрительный, что тогда значение 0 было оставлено зарезервированным!)
Только надо будет как-то сие указывать в cx_ch_open()
--
ей-то сейчас передаётся лишь булевский флаг on_update
(переделать в "monitor_mode"?).
08.06.2020: а ещё вспоминается давно обдумывавшаяся фича "Отключаемость каналов в cda" -- в bigfile-0001.html аж за 31-10-2007.
chan_ioctl()
.
07.07.2023: забавно -- только наткнулся на эту идею, при том, что идентичная же записана за 11-06-2023.
09.06.2020: ну-с, что будем делать?
Особенно на фоне идеи про динамическое управление мониторированием.
Решение на сейчас:
rq_lock
сделать битовое поле "текущее поведение"?).
cx_rd_cur()
,cx_setmon()
,cx_rq_l_o()
-- вытащить в отдельную функцию, а то оно уже 3 раза повторяется.
(Эти действия понадобятся в любом случае.)
13.06.2020: кстати, а ведь флажок "NOMONITOR" будет полезен и для софтины tvcapture и прочих подобных бриджевателей -- там совершенно незачем генерить лишний траффик, мониторя свои же записывания.
16.06.2020: пора переходить к "предварительно-подготовительным" мероприятиям.
Так вот: у нас УЖЕ есть CDA_CONTEXT_OPT_IGN_UPDATE
-- он по смыслу своему прекрасно подходит на эту роль (всё равно события
обновления игнорируются, так незачем и подписываться!).
Да и cx-starter этот флаг УЖЕ выставляет в режиме noprocess, так что его исходники даже и модифицировать не придётся.
CDA_DATAREF_OPT_NOMONITOR
(да, =1<<20
-- хрен уж с ним).
cda_add_chan()
:
CDA_CONTEXT_OPT_IGN_UPDATE
.
Но только для предачи new_chan()'у, в ri->options
он НЕ
взводится.
cx_rd_cur()
,cx_setmon()
,cx_rq_l_o()
,
повторяющаяся уже 3 раза, вынесена в отдельную do_subscribe()
.
hwrinfo_t
добавлено поле nomonitor
.
...да, пока что так, а вообще надо б переводить все флажки на битовую
маску, причём парочку on_update
плюс nomonitor
сделать соседними младшими битами (0 и 1), чтоб в протоколе 4.1 можно б было
эти 2 бита сразу использовать как индекс в массиве "тип мониторинга"
([0]=ON_CYCLE, [1]=ON_UPDATE, [2...3]=NEVER).
_new_chan()
он выставляется в значение флага
CDA_DATAREF_OPT_NOMONITOR
.
do_subscribe()
при его взведённости НЕ делается
cx_setmon()
.
CDA_DATAREF_OPT_NOMONITOR
.
...правда, для каналов ЗАПИСИ, только которые и актуальны для pipe2cda, событий CURVAL практически не бывает. Но для корректности -- пусть будет и CURVAL.
Ну вроде пока всё -- для ЕманоФединых хотелок достаточно. Проверять надо. Как?
18.06.2020: всё-таки перевёл с 3 полей на 1 поле битовой маски.
mode
.
MODE_ON_UPDATE
,
MODE_NOMONITOR
, MODE_RQ_LOCK
.
on_update
, nomonitor
и
rq_lock
удалены.
Заодно, в процессе модификации, обнаружил сплошную халтуру в
cda_d_cx_lock_op()
-- но об этом подробнее в разделе о локинге.
21.06.2020: сделана программа-тест -- work/tests/test_NOMONITOR.c.
22.06.2020: протестировано -- вроде работает.
Суть теста была в том, что на x10sae запускался сервер с длиной цикла аж в 10 секунд, на другой машине запускался test_NOMONITOR, траффик гляделся при помощи "tcpdump -i eno1 port 8013" на x10sae, и с x10sae же делалась запись. Если бы флаг NOMONITOR не работал бы, то пакеты приходили бы и на запись, а так -- только раз в 10 секунд уведомление о цикле. ...а вот при использовании cdaclient вместо test_NOMONITOR -- приходят и при записи.
Напрашивающийся вариант синтаксиса -- если после '-' стоит еще что-то, то вместо сепаратора делаем метку.
Конкретно: Федя Еманов (и Виталя Балакин по его примеру) навострились делать "демоны" на python, которые как-то обрабатывают данные от обычных CX-серверов и кладут результаты в CX-серверы же. Сами они никаких сокетов не слушают, но от этих "демонов" зависят другие программы, уже вполне графические.
...а то сейчас пришлось делать 2 ОТДЕЛЬНЫХ строчки для запуска и останова балакинского "демона".
19.12.2017: но остаётся ещё проблема, что лампочки серверов, появившиеся ПОСЛЕ начального заполнения окна, не получают привязки меню по правой кнопке мыши.
MotifKnobs_leds_grow()
-- так избежим проблемы, что
cx-starter'овский обработчик вызовется ДО led'овского.
Очевидно, помнить старое значение cda_status_srvs_count()
и
сравнивать его с текущим?
Неа, есть вариант лучше: помнить старое, но сравнивать с
текущим (после leds'ного вызова) значением leds.count
.
Делаем.
subsys_t.oldleds_count
.
sr->leds.count
--
в BtnCreator()
.
()
для
новорожденных.
Да -- работает! И меню появляется, и по командам даже пытается сделать ssh на IP сервера (от ssh-то в нынешней ситуации толку не будет, но то вопрос следующего раздела).
18.12.2017: вообще можно считать, что cx-starter просто не подходит для работы с zeroconf-клиентами. И это скорее правда. 08.12.2022: а вообще см. обсуждение ниже за сегодня.
18.12.2017: с другой стороны, можно ж указывать
конфиги иначе -- как
$(hostname -s | tr '[A-Z]' '[a-z]')
.
Неприятность будет в необходимости аккуратного квотинга (команда-то может выполняться как локально, так и по ssh).
05.12.2022@утро-зарядка: о --
а ведь напрашивается прямо самоочевидный вариант: если именем сервера
является IP-адрес, то резолвить его в имя, например, посредством
gethostbyaddr()
.
Вопрос лишь в том, ГДЕ/КОГДА это выполнять?
05.12.2022: насчёт где/когда выполнять резолвинг:
HandleSrchReply()
.
ProcessSrchEvent()
?
Теоретически можно, но неприятны потенциальные принудительные "для всех" зависоны на резолвинге.
ChangeServerStatus()
.
В момент определения имени хоста сервера -- в точке перед
"at_host = cf_host = host;
".
Кэшировать бы как-то, но как/куда? Ведь имя добывается прямо в той
точке, посредством cda_status_srv_name()
...
gethostbyaddr()
, и тут за 11-01-2018 объясняется причина.
Ну -- делаем:
SplitSrvspec()
и tolower()
'изацией.
host[]
содержит только цифры и
'.'.
evproc()
.
05.12.2022: попробовал проверить, на x10sae -- сходу не получилось: мало того, что путается в IP-адресах (шибко много интерфейсов поднято, и на всех получаем ответы), так ещё если всё же указать один принудительно --
CX_GURU_LIST=x10sae
-- то у cx-starter'а хватает мозгов считать его за IsThisHost()
и делает не совсем точно то же самое.
Надо б с другого узла проверить.
08.12.2022: попробовал ещё раз прямо на x10sae, только поаккуратнее.
Ну OK -- добавил тривиальное обрезание всего начиная с первой '.', если есть.
После исправления всё делает как надо -- резолвит адрес в имя и правильно указывает на его основе имя devlist'а.
Думал было, что косяк в реализации в егойном evproc()
(например, ПЕРВОЕ имя получает реальное, а остальные -- просто в буфере
остаётся).
Но нет: "traceroute
" на эти адреса также показывает "имя
локального узла".
Причины такого поведения мне найти не удалось: в man'е на эту тему ни слова, а гугление по "unnamed ip address traceroute local host name" и "unnamed ip address gethostbyaddr local host name" ничего не дало, и как ещё сформулировать запрос -- хбз.
А вот тут нужно небольшое "обсуждение":
С другой стороны, как сейчас стало ясно при тестировании, если такой сервер хоть раз появлялся, то далее он уже будет известен.
Следовательно, cx-starter вполне годится для ПЕРЕзапуска таких серверов.
А IP-адреса будут почти исключительно в результате UDP-резолвинга, который, в свою очередь, в основном годен только для работы в ЛОКАЛЬНОЙ сети.
Но оба этих случая, мягко говоря, весьма экзотичны.
И да, CX-starter создавался всё-таки для работы в ЛОКАЛЬНОЙ пультовой.
Итого, что осталось как бы "НЕдоделанного":
cf_host
(вот его можно было б
делать ВСЕГДА).
Старый вариант оставлен, но закрыт #if
'ом по
MAY_USE_RUSSIAN_KOI8
.
Сделано одновременно и унифицированно с Chl_help'ом.
$*
(в
самом конце).
Смысл -- чтобы можно было прямо команде "start" передать ключик
"-noprocess
.
04.11.2020: при наличии этот файл СОРСИТСЯ (командой
".
"), а не вызывается. Причём ищется он в
4pult/configs/, хоть это и shell-файл -- чтоб был рядом с прочими
конфигами.
Занадобилось для того, чтобы на Raspberry Pi по имени term-kls сделать более крупные цифры. Так что создан конкретно cx-starter-setup-term-kls.sh (он делает "xset fp+ ДИРЕКТОРИЯ" плюс парочку "xrdb -merge").
Хотелка возникла после вчерашней ситуации, когда сервер cxhw:37 завис намертво, так что к нему не коннектилось и starter считал его "неживым", но при попытке сделать ему "Start" новый экземпляр не запускался с предсказуемой диагностикой
cxsd_fe_cx.c::CreateServSockets: bind(inet_serv_socket): Address already in use
А если бы можно было "принудительно грохать" -- то рестартовать бы удалось.
...вопрос только: всегда грохать принудительно или требовать удержания, например, кнопки Shift?
01.12.2022: немного исследований вопроса:
ServMenuCB()
информация
call_data
не используется, а пришлось бы), и во-вторых, нажатие
Shift+MB1 либо самим Motif'ом, либо какими-нибудь window manager'ами может
восприниматься "некорректно".
При взгляде на ChangeServerStatus()
приходит одна идейка,
хотя и весьма некрасивая:
on=-1
.
При этом условие "if (curstatus == on) return 0;
" никогда не
сработает.
if (on < 0) on = 0
.
Тем самым отрицательное значение будет дальше считаться за "Stop".
02.12.2022: делаем. По вчерашнему проекту (b) --
"on=-1
" воспринимается как "FORCE stop".
05.12.2022: соображение в сторону: а поможет ли такое
решение в конкретной ситуации от 01-12-2022, когда сервер намертво висел в
futex'е, вызванном glibc'ёй из localtime()
, вызванного из
ll_vlogline()
, вызванного из обработчика сигнала?
Ведь оно заново сделает "kill [-TERM]", по которому сервер опять влетит в
обработчик SIGTERM onsig()
, в котором опять будет пытаться
сделать logline()
...
И что делать? Пытаться принудительно делать затем "kill -9"? Или всё-таки добавить возможность юзеру -- при помощи того же Shift -- указывать, что требуется "-9"?
05.12.2022: проверяем.
"kill -STOP
"'нутом экземпляре.
06.12.2022@утро-просыпаясь:
напрашивается такой вариант: по обычному [Stop] передавать 0
, а
вот при нажатом Shift -- -1
, и чтоб в этом случае останов не
только выполнялся принудительно, но ещё и как "kill -9
".
07.12.2022: а почему бы не поступить проще -- дополнительный пункт меню, вроде "Force Stop" или "kill -9"?
07.12.2022: сделано по сегодняшней вариации идеи --
CMD_KILL
,
serv_menu_kill
"kill-9",
ChangeServerStatus(,,-1
.
on < 0
теперь
kill
'у добавляется строка " -9".
Проверено -- работает. Вот теперь считаем за "done".
Это всё обдумывалось ещё со времён v2 -- см. bigfile-0001.html от 05-09-2012. И даже было сделано почти всё, в следующем составе:
Для init'а оный был слабан еще тогда -- src/doc/_etc_init.d_cx-servers; он даже давно реально используется, чтобы грохать все cxsd при shutdown/reboot'е (иначе клиенты были не в курсе, т.к. соединения не закрывались).
15.08.2018: подготовка -- проект:
params=
, вроде -bNNNNNN
.
-b200000
.
Но это уже несколько overkill -- особенно учитывая, что запуск только локальный, и никакие start=/stop=/user= нафиг не интересны.
Идеи:
cat
команду
grep -v '^ *#
Но это не позволит комментировать с середины строки.
Вторая серия идеи: вырезать всё от '#' и до конца строки, sed'ом --
sed -e 's/#.*//'
Это позволить исключать сервера из списка, просто добавляя перед номером, например, '-'.
NNN_SERVERS
.
...хотя у такого способа есть и минусы -- в хитрых случаях, вроде нынешнего объединения узлов canhw и cxhw на одном (cxhw): запускаться при этом будут только сервера по primary-имени. Но это уже мелочи, легко исправимые вручную.
Реализация:
Это было самое простое :)
По сравнению с v2'шным вариантом -- добавлено красивостей (пути засунуты в переменные) и вышеописанная фильтрация (комментарии и только-цифры в номерах серверов).
просто не работает. У меня сходу оно вообще на что-то там ругалось, а еще, как выяснилось чтением нагугленного, в таком варианте "while" будет исполняться в sub-shell'е (т.е., все изменения переменных в нём же и останутся), так что пришлось сбагривать файл черезgrep '^.srvparams' | while read A B C do ... done
while read ... done < ${SRVPARAMS_FILE}
А отфильтровывать по ".srvparams" уже внутри -- лишним if'ом.
case
, у которого в качестве шаблона МОЖНО писать переменную.
09.04.2019: пытаемся двигаться дальше -- припёрло уже.
kill
". Окей -- меняем на
/usr/bin/kill
.
10.04.2019: похоже, надо просто начинать читать документацию по systemd. Чисто "по наитию" или "по аналогии" -- фиг.
Где можно почитать:
11.04.2019: почитал. Похоже, требуемое поведение В ПРИНЦИПЕ НЕРЕАЛИЗУЕМО -- об этом явно сказано в RedHat'ной документации:
The systemctl utility does not communicate with services that have not been started by systemd. When systemd starts a system service, it stores the ID of its main process in order to keep track of it. The systemctl utility then uses this PID to query and manage the service. Consequently, if a user starts a particular daemon directly on the command line, systemctl is unable to determine its current status or stop it.Т.е., НЕ получится сделать так, чтобы демон мог запускаться "руками" или cx-starter'ом, а грохаться systemd'ой.
11.04.2019: как обойти?
И запилить пару таких "действий" -- при старте системы запуск серверов, а при останове -- kill.
Но ещё раз обратил внимание на сервисы с Type=oneshot. Они считаются завершившимися (и неактивными) после своей отработки.
Так вот: напрашивается сделать 2 таких сервиса -- start-cx-servers.service и stop-cx-servers.service; первый WantedBy=multi-user.target, а второй
Отдельный материал для обмозговывания -- ответ на вопрос "How to run a script with systemd right before shutdown?" на stackexchange.com: там предлагается "думать задом наперёд" и действие засунуть в ExecStop, НЕ указывая ExecStart и потому добавив RemainAfterExit=true. Впрочем, это будет по факту наш вариант #1.
А ещё см. там же второй ответ, который с "Before=shutdown.target reboot.target halt.target" и DefaultDependencies=no.
Пытаемся сделать. Прямо сплошная "дорога боли"...
/usr/bin/kill $(cat /var/tmp/cxsd-*.pid)
не работает, т.к. не поддерживаются 1) command substitution -- $(cat...); 2)
globbing -- cxsd-*.pid. Господи, какие же уроды авторы systemd!!!!!
KillMode=none
, иначе сервера сразу после своего старта
прибивались (systemd через cgroups следит).
Засим -- вроде бы заработало.
Теперь надо б как-нибудь отвязаться от прописывания путей, чтобы оно ссылалось на home-директорию юзера. $HOME?
12.04.2019: пытаемся отвязаться от абсолютных путей вроде "/export/ctlhomes/oper/4pult/" или "/home/user/4pult/". По факту -- "дорога боли, день 2".
Получаем ругательство "Executable path is not absolute, ignoring: ...".
...это так -- в дополнение к сомнительности идеи требовать именно абсолютного пути с запретом $PATH.
/bin/sh -c КОМАНДА
(советовали /bin/bash, но не суть).
Т.е., тут у shell'а путь абсолютный, а дальше выполняется уже стандартный поиск.
Что по факту просто множит на ноль то требование "везде абсолютные пути!".
sh: /bin/sh: -c: option requires an argument
$HOME/4pult/...
а требует
${HOME}/4pult/...
Намёк на это есть в "документации":
Use "${FOO}" as part of a word, or as a word of its own, on the command line, in which case it will be replaced by the value of the environment variable including all whitespace it contains, resulting in a single argument. Use "$FOO" as a separate word on the command line, in which case it will be replaced by the value of the environment variable split at whitespace, resulting in zero or more arguments.
Т.е., спецификацию $HOME/4pult/...
оно, очевидно,
воспринимает как ${HOME/4pult/...}, а переменной с таким именем
нет, вот /bin/sh'у и попадает пустая строка.
ExecStart=${HOME}/4pult/...
(т.е., БЕЗ /bin/sh) -- тоже не работает.
ExecStart=/bin/sh -c${HOME}
" всё
заработало. Аллилуйя!!!
Итак, потрачена всего-то почти неделя (на тривиальнейшую задачу!), но решение вроде найдено.
Ставим получившиеся скрипты на пульт и будем там пробовать.
P.S. А вообще непотребство, что не удалось обойтись ОДНИМ файлом сервиса.
15.04.2019@пультовая: тестируем. Аб-зац...
sh: /bin/sh: /export/ctlhomes/oper/4pult/bin/start-all-servers: No such file or directory
Это было предсказуемо: ведь ~oper/ лежит на NFS, доступному через autofs; очевидно, start-cx-servers пытается запуститься ДО них, вот директория и отсутствует.
Решено было извратным способом, подсмотренным где-то на stackexchange: в
секцию [Unit]
была добавлен строчка
After=multi-user.target
Не очень ясно, как это работает: ведь также и
WantedBy=multi-user.target
, так что получается проблема курицы
и яйца: с одной стороны, multi-user.target для своего "активирования"
требует нашего сервиса; с другой же, он вроде как должен "полностью
активироваться" ДО запуска этого сервиса.
По-хорошему, должен был бы быть какой-нибудь специальный тэг "запустить в конце, после всего", но такого нету.
Как бы то ни было -- нынешний вариант работает; хотя и неясно, насколько это корректно и не сломается ли в будущем.
Всё предсказуемо -- все лампочки остались гореть (сервера убиты не были, вот интерфейс сервера просто и ушёл с "живыми" незакрытыми сокетами).
systemctl enable
stop-cx-servers
".
И результат оказался странным: ЧАСТЬ серверов закрылись, а часть -- нет. Т.е., часть лампочек в стартере покраснели, а часть остались зелёными.
systemctl start stop-cx-servers
".
systemctl -l status stop-cx-servers
" после
перезагрузки ничего криминального не светится, а просто
canhw:~# systemctl -l status stop-cx-servers * stop-cx-servers.service - CXv4 servers automatic stop service Loaded: loaded (/usr/lib/systemd/system/stop-cx-servers.service; enabled; vendor preset: disabled) Active: inactive (dead) since Mon 2019-04-15 11:34:23 +07; 2h 13min ago Process: 1800 ExecStart=/bin/sh -c ${HOME}/4pult/bin/stop-all-servers (code=exited, status=0/SUCCESS) Main PID: 1800 (code=exited, status=0/SUCCESS) Apr 15 11:34:23 canhw systemd[1]: Starting CXv4 servers automatic stop service... Apr 15 11:34:23 canhw systemd[1]: Started CXv4 servers automatic stop service.
Но, что странно, в /var/tmp/4cxsd.log за то же время 11:34:23 есть сообщения «signal 15 ("Terminated") arrived» от всех 8 серверов.
KillMode=none
".
Но, судя по логам о получении SIGTERM'а, это не так и сигнал доходит.
Если дело в этом, то надо ставить небольшую задержку после убиения.
Но как-то это сомнительно.
Если так, то нужно ставить какую-нибудь хитрую зависимость, чтобы убиение выполнялось в самом начале, либо ПЕРЕД остановом сервиса "network" (?) -- но это вообще неясно, как специфицировать.
Склоняюсь к последнему варианту. Понять бы, ЧТО там надо указать в зависимостях...
P.S. Гугление сходу ответа не дало: там лишь обращают внимание, что если указан некий порядок СТАРТА (зависимости After= или Before=), то СТОП будет выполняться в обратном порядке. Но у нас-то РАЗНЫЕ сервисы для старта и стопа, поэтому никаких прямых зависимостей для обращения просто нету.
Резюме: автоматический старт запинан, а автоматический стоп менее критичен, т.к. через 5 минут cda+cxlib сами обнаружат обрыв.
15.04.2019@пляж-лыжи: всё-таки НЕОБХОДИМ нормальный останов серверов при перезагрузке. Ибо сегодня проблема проявилась:
А если бы stop-cx-servers выполнялся, то сокет закрылся бы сразу и вышеописанных сложностей бы не возникло.
17.01.2022: а вот ЧеблоПаша нашёл-таки способ сделать ВСЁ в одном файле сервиса. Краткая история:
systemctl start stop-cx-servers
то всё работало штатно без проблем, а вот сам SystemD это исполнить был
почему-то неспособен.
kill -9
"?
ps
показывался "Z", а другой "D") PPID'ом значился
"systemd-shutdown", но на этом всё и застопоривалось.
stop-cx-servers.service
выполнялся уже ПОСЛЕ "nfshome.service
".
А NFS, как известно, в таких случаях завешивает все обращения намертво -- даже никакой "kill -9" не сработает, так и останется процесс в состоянии DEFUNCT.
Т.е., deadlock из-за как бы "проблемы курицы и яйца".
Вот результат его трудов:
[Unit] Description=CXv4 servers mangement service After=multi-user.target [Service] Type=oneshot User=v4-cs-worker ExecStart=/bin/sh -c ${HOME}/4pult/bin/start-all-servers ExecStop=/bin/sh -c ${HOME}/4pult/bin/stop-all-servers RemainAfterExit=true KillMode=none [Install] WantedBy=multi-user.target
Помещаем это в 4cx/src/doc/cx-servers.service, с минимальными
исправлениями (вроде значения User=
).
Кстати, любопытно сравнить это с последним вариантом моего нерабочего прототипа от 15-08-2018, лежащим в NONFUNCTIONAL-cx-servers.service:
[Unit] Description=CXv4 servers automatic start/stop service After=multi-user.target [Service] User=oper ExecStart=/export/ctlhomes/oper/4pult/bin/start-all-servers ExecStop=kill $(cat /var/tmp/cxsd-*.pid) Type=forking [Install] WantedBy=multi-user.target
ExecStop=
на должный вызов внешнего скрипта, плюс "тонкости" с
вызовом через
"/bin/sh -c
"
и ссылками на $HOME
в синтаксисе "${HOME}".
И то, и другое было сделано в дуальной версии (от которой Паша уже и отталкивался).
Type=forking
", а в пашином варианте --
триада
Type=oneshot RemainAfterExit=true KillMode=none
Понять бы ещё детально СМЫСЛ этих заклинаний и почему именно они дали нужный результат...
Надо бы перепроверить и попробовать, но, похоже, этот вариант действительно лучше моего дуально-шизофреничного.
26.01.2022: новый вариант cx-servers.service поставлен на виртуалку cxout. Будем наблюдать.
20.01.2025: некоторое время назад вылезла проблема: НЕ стартовали сервера, у которых присутствует параметр "-e=load-frontend=epics" -- в частности, canhw:12.
export LD_LIBRARY_PATH=~/epics/base-3.15.6/lib/linux-x86_64
имелась и в .zshenv (для ZSH) и в .profile (для BASH, работающего
/bin/sh
'ем).
Environment=
и
EnvironmentFile=
в .service-файле).
...ещё есть некий /etc/environment (в CentOS-7 присутствует, пустой, принесён пакетом "setup-2.8.71-7.el7.noarch", а используется хбз кем). Но пхать в СИСТЕМНЫЙ файл указания на home-директорию конкретного юзера, да ещё и находящуюся на NFS'е -- так себе решение.
-l
".
(Этот олигофренизм формально умеет парсить "environment-файл", причём при
любом запуске, но для этого вместо какого-нибудь фиксированного имени вроде
".bashenv" или ".bash_environment" ему надо указывать имя
файла в переменной окружения ENV
-- совершенно бесполезное
решение, IMHO.)
-l
".
Итого, решение -- добавить к /bin/sh
ключ "-l
",
так что теперь "/bin/sh -lc ${HOME}/...
" (причём в обоих --
ExecStart=
и ExecStop=
).
Проверено -- помогло.
NOX=1
не устанавливаются. Причина в том, что они живут в
programs/runner/, по NOX=1
исключающейся из сборки.
Что делать?
11.02.2023@~17..18, по дороге между лавкой на Морском-16 и Ярче, рядом с Морской-4: собственно, главная проблема с "создать отдельную директорию" -- то, что директория должна была бы назваться "programs/scripts/", но а) первая буква перекроется с "server" и б) может возникнуть путаница между нею и 4cx/src/scrtips.
Поэтому я полдня пытался придумать варианты названия, которое было бы осмысленным, но не начиналось бы ни на 's', ни на 'r' (run*).
Так вот: а кто сказал, что слово-название должно быть простым и надо начинать с 's'? Можно использовать словосочетание, где вторым словом будет "scripts", а перед ним ещё что-то.
Вот и придумал -- "launch scripts", "запускающие скрипты"; совращённо -- "lscripts/".
12.02.2023: так и делаем.
Единственный минус -- дублирование Makefile-кода: поскольку в
runner/ живёт cstart (привязанный к cx-starter'у), то весь
код там остался, просто список его целей SH_UTILS
сократился до
cstart
'а.
С этого момента содержимое данного раздела располагается в programs/lscripts/, хотя всё описание оставляем тут.
Расследование показало, что это из-за наличия в srvparams.conf строки
.srvparams canhw:12 params="-e 'load-frontend epics'"
Будучи считана cx-starter'ом, она отрабатывается корректно. (22.08.2023: по крайней мере, при УДАЛЁННОМ запуске команды, через ssh; что будет при локальном -- вопрос для отдельной проверки.)
А вот run-cx-server, исполняясь под /bin/sh
,
разбивает всё по пробелам, игнорируя апострофы, так что получается набор
параметров "-e" "'load-frontend" "epics'" (это
и так-то выполниться бы не могло, а ещё и приводит сразу к ошибке
"unexpected argument(s)" -- за последнее оторванное).
21.08.2023: анализ и поиск решений.
-e
-параметров для дальнейшего исполнения заменялся бы на
пробелы" -- тогда можно будет писать что-то вроде
"-eload-frontend^epics
" (а пробел после "-e"
можно
просто убрать -- это позволяет стандартный getopt()
'ный
синтаксис).
"-e"
в
cxsd_config.c::ParseCommandLine()
сводится к простому
dash_e_params[dash_e_params_used++] = optarg;
-- т.е., в очередную ячейку сохраняется указатель прямо внутрь argv[]. Так
что "заменять '^' на пробелы" просто негде.
22.08.2023: делаем.
ppf4td_skip_white()
.
skip_white_and_eq()
, которая помимо isspace()
пропускает также и '='.
setenv_parser()
удалена отдельная проверка на '=', позволявшая писать в стиле
"ENVNAME = STRING
",
т.к. теперь тот же синтаксис (даже ослабленно-контролируемый) поддерживается
автоматом.
.srvparams canhw:12 params="-b200000 -e=load-frontend=epics"
Проверяем.
(Как раз занадобилось перепустить canhw:12 вследствие изменения конфигурации: виртуальное устройство d4m1t3 на основе icd_4m1 вернулось с MAGX_VNN_CEAC51_DEV:ceac51 на MAGX_IST_CDAC20_DEV:cdac20.)
23.08.2023: по крайней мере, при УДАЛЁННОМ запуске команды, через ssh; локально -- опять же надо отдельно проверять.
run-cx-server
-- тоже теперь работает.
zsh 4pult/bin/run-cx-server
" -- сходу нет: там всё
содержимое $PARAMS
передаётся ОДНИМ параметром, так что
получается единое
"-b200000 -e=load-frontend=epics",
на что сервер ругается
"'-b' argument should be an integer (in us)".
И тут -- совершенно случайно, просматривая "man zshall" на тему специальной обработки zsh'ем символа '=', первая же встреча этого символа в man-страниче -- наткнулся на
GENERAL COMMENTS ON SYNTAXAlthough the syntax of zsh is in ways similar to the Korn shell, and therefore more remotely to the original UNIX shell, the Bourne shell, its default behaviour does not entirely correspond to those shells. General shell syntax is introduced in the section `Shell Grammar' in zshmisc(1).
One commonly encountered difference is that variables substituted onto the command line are not split into words. See the description of the shell option SH_WORD_SPLIT in the section `Parameter Expansion' in zshexpn(1). In zsh, you can either explicitly request the splitting (e.g. ${=foo}) or use an array when you want a variable to expand to more than one word. See the section `Array Parameters' in zshparam(1).
Дальнейший просмотр на тему "SH_WORD_SPLIT" дал ключик
"--sh-word-split
", в варианте с которым --
zsh --sh-word-split ~/4pult/bin/run-cx-server :12
-- работает.
P.S. Кстати, не смог найти место в этом файле, где описывается
изготовление ключика -e
(даже ни единого упоминания
dash_e_params[]
). Только за 22-10-2013 есть упоминание, что он
уже работает.
11.07.2009: сразу же вылезла очевидная и дурацкая
проблема: dlopen()
'ом драйверы просто так не грузятся,
почему-то даже с RTLD_LAZY
-- требуют функций из
cxsd_driver-API.
Очевидное решение -- подлинковывать библиотеку сервера, чтобы символы были определены.
(В принципе, есть решение попроще и похалтурнее -- просто иметь такие символы "пустышками". Тогда всё будет грузиться и резолвиться. Но, что неприятно -- будет дублирование.)
Короче -- сделано. Плюс, пришлось завести и
public_funcs_list[]
.
Утилита в первоначальном варианте работает, основную информацию выдаёт, включая даже список не-NULL методов. Осталось добавить:
params_table
.
chan_info
.
Сейчас создан просто скелет -- в первую очередь для отладки cxlib'а.
10.09.2009: исходная посылка: может понадобиться посылать НЕСКОЛЬКО пакетов запроса (мало ли -- блокировку там наложить, или еще чего). Однако, в каждом из пакетов может потребоваться по НЕСКОЛЬКО операций чтения/записи. Таким образом, нужен какой-то способ указывать границу. Хотя красиво выглядел бы симвом ';', но из-за shell'а он неприемлем, так что выберем его аналог-братца из BASIC'а -- ':', который совершенно неинтересен shell'у.
Сейчас создан только скелет -- полезный для отладки cda.
Синтаксис вызова --
cdaclient [-b DEFPFX] {COMMAND}
т.е., могут указываться совершенно разные ссылки, хоть от разных серверов и
даже протоколов.
Т.е., умолчание -- i1 /*CXDTYPE_INT32, скаляр*/.
Это может потребовать уже существенно более хитрой работы, с запоминанием параметров в список и последующим его исполнением. И работа выйдет в 2 стадии: на 1-й оно зарегистрирует все каналы, а на 2-й выполнит запись (через какое-то время).
12.05.2015: неа, ясно, что эту фичу мы делать не будем -- слишком много траблов, а пользы не просматривается. ПОКА, по крайней мере -- для тривиальных сценариев проще запускать несколько штук друг за дружкой.
02.04.2014@Снежинск-каземат-11: потихоньку приступаем к изготовлению. Пока в простом варианте, без пауз и даже без записи.
15.04.2014: синтаксис указания типа/размера сменен с суффикса "/T[count]" на префикс "@T[count]:" -- аналогично Cdr'ову.
30.07.2014: задышало! Получены реальные данные
(от linthermcan). Печать пока сделана примитивно, тупо добытое через
cda_get_dcval()
, безо всяких атрибутов.
01.08.2014: а чё б не ввести возможность указывать per-channel префикс "%DPYFMT:" -- как в experiment'е?
18.08.2014: да, сделано.
19.02.2015: печать чуток под-умнена -- теперь оно смотрит, скаляр ли это или вектор, и в векторе крутит цикл (но ПОКА реально данные не печатает, т.к. надо жонглировать разными типами; да и работа с dpyfmt не рассчитана на целочисленные типы).
22.04.2015: в дополнение ко вчерашнему добавлению
поддержки CDA_DATAREF_OPT_
'ов теперь улучшаем печать
результатов -- чтоб оно корректно выводило ВСЕ варианты.
CXDTYPE_TEXT
и
CXDTYPE_UCTEXT
. Оно говорит длину строки, а потом выдаёт
саму строку в кавычках.
Выдача 1 символа делается отдельной PrintChar()
,
обладающей некоторым интеллектом:
isprint()
, то
выдаётся как есть; отдельно проверяется на '\n' и
'\r', а иначе -- печатается \xXX (т.е., всякие \a,
\t, etc. отдельно не исследуются). 27.04.2015: да
ладно -- сделаны '\a', '\b', '\f', '\t',
'\v'.
CXDTYPE_REPR_FLOAT
/CXDTYPE_REPR_INT
вызывается либо ParseDoubleFormat()
, либо свежевведённая
ParseIntFormat()
.
Т.е., по-хорошему, формат надо указывать ПОСЛЕ типа -- "@T:%nnnC".
GetTextFormat(NULL)
или
GetIntFormat(NULL)
.
26.04.2015: НЕПРАВИЛЬНО было после процента ("%ll#15d") -- судя по man 3 printf (раздел "Format of the format string"), length modifier должен быть непосредственно перед conversion specifier'ом. Поэтому переделано: теперь символ формата (вместе с '\0') сдвигается на пару символов дальше, и перед ним вставляется "ll".
FMT_ALTFORM
, чтоб данные выдавались корректно, годными для
дальнейшего обратного парсинга.
Мозгов там уже столько, что впору вытаскивать их в отдельную бибилиотеку, дабы не дублировать код и иметь единый формат/интерфейс для консольных утилит. Назовём, видимо, console_cda_util.[ch].
23.04.2015: на будущее -- план, как реализовать функционал записи в cdaclient:
При этом флаг "отправлены" сбрасывать, а "количество висящей записи" уменьшать на единицу.
А если всё же припрёт -- видимо, а) вводить какой-то символ/команду "дождаться окончания исполнения текущего списка записи"; б) запускать запись не всю сразу, а "пачками", до команды "дождаться", и следующую пачку запускать после окончания дожидания предыдущей.
...ей-богу, шиза это -- шибко трудоёмко, правильнее делать отдельными вызовами cdaclient.
27.04.2015: подготавливаем вытаскивание в отдельную библиотеку.
util_refrec_t
.
PrintDatarefData()
, причём какие части
печатать (время, имя, timestamp) -- указывается битофлаговым параметром
parts
.
ParseDatarefSpec()
.
Имя канала она теперь берёт до опционального '=', указатель на который возвращает вызывателю в endptr.
Ну и мелкие модификации, для более качественного отображения:
А насчёт записи -- понятно, что будущие dataset-save и dataset-restore будут по факту упрощенными вариациями cdaclient'а. Так что парсинг значения для записи также надо обобщать.
29.04.2015: продолжаем -- после вчерашних продвижений в разделе о соседней утилите :):
Для этого
spec
переносим уже прямо в
util_refrec_t
.
UTIL_PRINT_RELATIVE
...
PrintDatarefData()
.
Поэтому PrintDatarefData()
добавлен первым аргументом
FILE*fp.
А всех на "-o" переведём позже.
UTIL_PRINT_DTYPE
.
PrintDatarefData()
.
Сделано просто, а для отличения от "@T..." используется факт, что в метке времени после "@" идёт цифра.
На нижнем уровне это должно отражаться в аллокации по такому spec'у МНОЖЕСТВА ячеек. А как конкретно реализовывать расширение такого шаблона -- вопрос отдельный; видимо, надо функцию в console_cda_util заводить.
Кстати, в dataset-* тоже будет полезно.
И option_mesdur
переименована в
option_timelim
.
<math.h>
оно считало совсем не то).
util_refrec_t
добавлены поля
wr_req
-- флаг запрошенности записи.
wr_snt
-- флаг "значение отправлено, но пока не
подтверждено".
val2wr
(CxAnyVal_t) -- то, что писать (для
маленьких значений).
*databuf
-- буфер для больших значений; если
==NULL, то значение в val2wr.
num2wr
-- количество элементов для записи.
num2rd
--
запрошенность чтения.
ParseDatarefVal()
.
И пара helper'ов ParseOneChar()
и
ParseOneDatum()
.
30.04.2015: продолжаем.
Полная же поддержка любой длины -- это на ppf4td на схеме plaintext (и "mem" для параметров командной строки).
Но сие трудоёмко, так что если в будущем понадобится -- как делать ясно, а пока тратить силы незачем.
-1
и складированием куда-то текста ошибки.
А еще б более адресно указывать место ошибки (например, файл:строка, как минимум; а для векторов -- еще и номер элемента).
Но это тоже трудоёмко, так что отложим.
ParseOneDatum()
.
ParseDatarefVal()
:
urp->val2wr
.
...собственно, пока только nelems==1 и сделано, поддержки векторов (с соответствующим аллокированием/ростом!) еще нету.
04.03.2016: поддержка векторов уже есть, сделано для тестирования CXDTYPE_UNKNOWN.
ParseOneChar()
, с поддержкой \xXX, \uXXXX и \UXXXXXXXX.
Проверено -- работает.
08.05.2022: ага, "работает" -- был косяк: после парсинга
\UXXXXXXXX вместо
"cp += 8
"
стояло
"cp += 4
"
(и как только проверял?). Видимо, осталось от копирования из варианта
\uXXXX; исправлено.
01.05.2015@пляж: насчёт "иметь бы возможность писать множественные спецификации" от позавчера: технологически оно сильно напоминает то, как в v2 сделана работа со скриптами/сценариями (Cdr_script.c) -- там используется специально написанный модуль fqkn.c.
Учитывая схожую потребность в cxsd_db*.c, мо-о-о-ожно изготовить некий модуль общего назначения, назвав его nameexp.[ch]. ...но не стоит -- там пусть будет своё, а тут сделаем свою реализацию, с внутренностями, скопированными из fqkn.
02.05.2015: далее:
А режим "мониторинг" включается принудительно, ключом -m. Итак:
option_monitor
.
num2read
и
num2write
соответственно, вместе со взведением в ячейке
флага rd_req
или
Если надо мониторить записываемый канал -- следует указать его еще отдельно без '='.
wr_req
сбрасывается, а wr_snt
взводится.
wr_snt
сбрасывается и
команда считается отработанной.
num2XXX
декрементируется.
option_monitor
НЕ делается
num2read--,rd_req=0. Так что и печатается при каждом обновлении, и
завершения не происходит.
(И есть хитрость: '-' в списке классов означает "сбрасывай все следующие", а '-' -- "взводи".)
12.05.2015: учитывая, что программа в общем-то работает, почитаем раздел за "done", а недоделанности (expand'инг имён каналов, парсинг векторов для записи, ключ -oFILE) допилим в рабочем порядке.
12.05.2015: учёт -o OUTFILE сделан (в обоих).
21.07.2015: дополнения в парсинг:
22.07.2015: дополнения в печать: теперь выдача ВСЕХ компонентов параметризуема. Это включает NELEMS (-DN), PARENS (-Dp), QUOTES (-Dq). Последняя пара в cdaclient включена по умолчанию, а в das-experiment и dataset-save всегда.
30.03.2016: улучшения в блоке вывода:
02.08.2016: добавлена возможность печатать ТЕКУЩИЕ значения (приходящие по CURVAL).
Т.е., это команда "дай текущее известное значение, и пофиг, если оно старое".
За компанию также добавлен более подробный help:
12.05.2015: да, так и сделано.
Это чтоб можно было в списке каналов писать комментарии (а das-experiment и dataset-save и так игнорируют всё после имени канала).
22.07.2015: вот эта "фичка" даёт проблему -- пустые значения строк (указанные в командной строке как nnn="" -- т.е., кавычки убираются shell'ом) НЕ считаются значениями для записи.
Строго говоря -- причина в нечетко определённом синтаксисе: наличие/позиция '=' толком не определено (в т.ч. относительно [COUNT]). Доопределить -- и тогда можно сделать более однозначные правила; например, что наличие '=' форсит режим записи, даже при пустоте далее.
...пока же пришлось в качестве хака в
cdaclient.c::RememberChannel()
убрать
'=' из списка пропускаемых символов -- чтобы пропуск сделал
ParseDatarefVal()
.
ВОЗМОЖНО, это вопрос не к cdaclient'у, а к какой-то иной утилите, которая бы формировала вывод иным образом. В v2 это делает cx-bigc в режиме gnuplot, и туда такая фича легла бы проще всего.
17.12.2015: тем не менее, некоторое обсуждение внешних аспектов реализации сделаем здесь.
По осмотру имеющихся ключей (-oOUTFILE у cdaclient и отсутствующего у cx-bigc) разумным выглядит "озаглавливание" -- -O DESTINATION.
Но тут неудобство в том, что у файлов будет отсутствовать вменяемое расширение.
02.08.2016: еще раз стало очевидно, что это должна будет делать другая утилита (ибо cdaclient и так перегружен мозгами), которую назовём "pzframe2text". Подробнее -- см. в его собственном разделе ниже.
...видимо, в ParseDatarefSpec()
-- туда проще всего
ложится, в цикло бработки префиксов while(1)
.
18.12.2015: да, сделано, именно там -- очень просто. Но пока не проверено.
Заодно добавлена возможность печати fresh_age -- ключ -DF.
Потребность возникла на ВЭПП-4, где почему-то загрузка v4gid25s'ных режимов "переключателем полярности", производимая через cdaclient, подвисает и не завершается.
14.02.2017: добавлен ключ -D/ (слэш --
перечёркивание, "конец"). Если он взведён, то finish_proc()
проходится ForEach'ем по всему списку и печатает имена каналов, у которых
выставлен любой из флагов rd_req, wr_req, wr_snt.
Проверено -- работает.
И даже помогло найти причину, почему загрузка режима cdaclient'ом "подвисает": в режиме записаны ВСЕ каналы -- и записи, и чтения. И cdaclient, не зная, кто там есть кто, пытается загрузить все. Но запись в каналы чтения ни к чему не приводит (DRVA_WRITE превращается в DRVA_READ, а с autoupdated-каналами -- вообще в DRVA_IGNORE). Вот cdaclient, не получая "подтверждения" записи, и продолжает висеть до посинения (точнее, пока эти каналы не обновятся).
Вывод: надо в режимах как-то помечать каналы чтения, чтобы они игнорировались. ...ставить в начале #? 15.02.2017: да, так и сделано.
20.02.2018@лыжи-после-обеда-2-я-двойка: в эту же стороннюю директорию (точнее, в стороннее ДЕРЕВО) надо будет унести и сам cda_d_epics.c.
10.05.2019: да, директория делается, называется frgn4cx/.
А утилитки надо будет назвать скорее "epics_cdaclient" и "tango_cdaclient"; т.е., не слитно, а через подчерк '_'. И аналогично "epics_das-experiment" и "tango_das-experiment" (и прочие тоже).
Плюс, возможно, будет также вариант со ВСЕМИ этими доп.плагинами; сейчас рабочее название префикса -- "all", т.е., "all_cdaclient".
08.07.2024: эти утилиты со встроенными плагинами БЫЛИ сделаны, но потом были уволены в пользу динамической загрузки dat-плагинов, почти сразу ещё в 2019-м.
Так что раздел давным-давно пора "withdrawn".
20.02.2018: посмотрел код -- вроде несложно: надо
действия, выполняемые по REF_R_UPDATE при взведённом
rp->ur.wr_req
, исполнять прямо в
ActivateChannel()
.
20.02.2018@лыжи-после-обеда-2-я-двойка: ключ -w
?
А еще пользительно будет иметь возможность включить "протоколирование"
момента записи -- когда выполняется отправка. Через какой-нибудь
-Dw
.
21.02.2018: делаем.
-w
и взводимая им
option_w_unbuff
.
-Dw
) и взводимый им флаг
print_writes
.
PerformWrite()
.
Он простой: кроме слова "WRITE" печатает еще результат (OK или ERR), плюс, при включенности соответствующих ключиков, текущее время и имя канала (в простом варианте -- всегда просто то, что указано в командной строке).
PerformWrite()
добавлен в конец
ActivateChannel()
Вроде работает.
cda_add_dchan()
, в то
время как реально давным-давно используется cda_add_chan()
(а
сокращённая форма не применяется нынче НИГДЕ).
Исправлено.
Вот как бы так сделать, чтоб cdaclient'у не обязательно б было указывать тип канала, а он определял бы сам и как минимум ПЕЧАТАЛ бы значения как надо?
Ведь у нас уже:
cda_hwinfo_of_ref()
.
cda_set_type()
.
Так что со стороны библиотеки поддержка уже имеется, вопрос именно в использовании оной в cdaclient.
29.01.2019@утро-дорога-на-работу-около-ИПА: как маркировать такие каналы -- записывать "неизвестный тип"?
А как поступать с dpyfmt? @04.06.2019: придумано -- см. ниже за сегодня.
30.01.2019@лыжи-после-обеда: а если ещё запись захотим реализовать -- вообще абзац/вертеп/бардак... Мысли на тему:
04.06.2019@лестница-в-13-м-к-Панову (ходил за кабелем RS232 DB9-DB9): разные форматы -- да дать возможность указывать ОБА (вроде "%8.3f:%5x:..."), и записывать тоже ОБА, в РАЗНЫЕ поля. Так можно будет поддерживать во всех console-utils тип @x.
10.06.2019: ну да -- в точности та же идея, что и 30-01-2019.
08.06.2019@бердск-попова-26, лёжа
на диване с покетом и пытаясь не уснуть: насчёт "чтоб cdaclient
мог при неуказании типа сам определять тип": надо чтоб не только клиент
мог менять тип канала cda_set_type()
'ом, но при UNKNOWN чтоб
отрабатывало возвращение типа dat-плагином
(cda_dat_p_set_hwinfo()
как раз отдаёт эти данные). Тогда как
раз автоматом всё заработает, без лишних телодвижений со стороны
dat-плагинов.
И да, для cda_d_epics придётся делать лишний "шаг" в пока отсутствующей
цепочке состояний (которая ЕСТЬ в cda_d_cx -- через rslv_state
)
-- "получить тип".
29.09.2020: ещё немного размышлений на ту же тему: как мне СЕЙЧАС кажется -- похоже, просто нет глубокого смысла напрягаться. Резоны:
15.04.2023@Бердск, после обеда-из-Кенгуру на бердской косе, во время прогулки по пляжу: ну да, единственный вариант -- при надобности "выяснить" тип именно ВЫЯСНИТЬ его. Да, потратив лишние усилия и время на состояние "пока неизвестно".
26.05.2023: вообще-то по результатам реализации
epics2cda, где делается cda_set_type()
, видно, что ни
"выяснять", ни "тратить время и усилия" не нужно: требуемая информация уже
готова к моменту CDA_RSLVSTAT_FOUND
. А реально останутся лишь
проблемы DPYFMT и записи.
15.11.2024: однако прямо сейчас в качестве временной меры (и, видимо, из-за "нет ничего более постоянного, чем временное", навсегда) сделана поддержка флага "@?".
27.06.2019: напрашивается ключик -De
--
по аналогии с ключом -e
у команды echo
.
Ну что ж -- делаем.
Но не будем -- пусть уж юзер сам определяется с тем, что ему нужно, и указывает соответствующие ключи, а программа будет просто выполнять то, что ей сказали.
UTIL_PRINT_UNESCAPED
.
PrintChar()
теперь принимает дополнительный булевский
параметр unescaped
, при взведённости которого просто печатает
байт как есть, не проверяя его значение.
Но только БАЙТ, т.е., если в нём нет битов за границами &0xFF.
PrintDatarefData()
туда передаёт True при указанности
в parts
флага UTIL_PRINT_UNESCAPED
.
print_unescaped
, ...
-De
, ...
Удовлетворительного решения не нашлось, т.к. сортировано там ни по алфавиту, ни по "значимости", и в разных местах порядок различается.
Так что вставлено после quotes.
Всё, работает.
Замечание: а вот СТРОКИ (которые STRSCHG, -DS
) -- те в любом
случае печатаются "как есть", просто "%s"'ом, безо всякой
интерпретации.
Смысл -- чтоб можно было добывать в исходном виде бинарные данные, хоть записанные pipe2cda (бинарные файлы так копировать :D), хоть просто от драйвера.
15.10.2019: ну делаем.
option_binary
(как в pipe2cda), ...
-B
(тоже как pipe2cda, только без
параметров).
ProcessDatarefEvent()
: при взведённом обычная печать НЕ
вызывается, а делается fwrite(,,,outfile) из буфера, добытого от
cda_acc_ref_data()
и в объёме, узнанном оттуда же.
-Da
" -- включить ВСЮ по-канальную диагностику.
Сделал!
22.06.2021: заметил, что было забыто включение
print_strings
(ключ "-DS
"). Добавлено.
Приходится переводить в человекочитаемый формат с помощью "date
--date @секунды.микросекунды
", что зело неудобно. (Вот сегодня
разбирался с осциллографами в bivme2_rfmeas -- когда они последний раз
отрабатывали, ключом "-dCt"; пришлось отдельно дешифрировать времена...)
@дома-ванна, ~16:30: а можно ж
добавить ключик -- например, "-D8" ("8" -- iso8601), чтоб оно вместо
числа печатало результат stroftime_msc()
.
09.02.2021: сделано, минут за 15-20, сразу после ванны.
UTIL_PRINT_TIMES8601
=1<<9, а _PRINT_RFLAGS сдвинут
дальше.
PrintDatarefData()
добавлена проверка на него и...
15.10.2021: халтура!!! Там в выдаче между значением и "@YYYY-..." отсутствовал пробел. Добавлен.
print_times8601
, его взведение по "-D8" и
использование-конверсия в 2 точках в ProcessDatarefEvent()
(по
UPDATE и CURVAL).
UTIL_PRINT_TIMESTAMP
, но уж там человекочитаемость излишня.
Работает, всё прекрасно.
num2read == 0
--
завершается.
Но в каких-то обстоятельствах это может оказаться проблемой: если канал почему-то отсутствует лишь временно,
Точнее, при переезде ДОЛЖНО бы приходить RSLVSTAT_SEARCHING, а его cdaclient просто игнорирует, так что формально переезд типа безопасен. Но не факт -- не вякнет ли cda_d_cx NOTFOUND'ом при обновлении конфига сервера вместо нужного SEARCHING'а?
Но вот в ручном режиме, когда утилита была запущена для мониторирования, потом канал временно исчезал и появился обратно, а утилита из-за этого перестала его мониторировать, что неприятно (и было обнаружено задним числом!) -- такой сценарий очевиден.
Так вот:
-M
" -- persistent monitoring?
-i
" -- ignore NOTFOUND events
(чтоб и с записью работало, и с однократными чтениями)?
Второй вариант выглядит правильнее.
17.03.2021@22:00: да чё там -- сделано, за несколько минут (работы-то...).
Ключ "-i
", флажок option_ignore_nf
.
Так на вид вроде работает, но по-хорошему надо будет проверить с действительным отсутствием/появлением/исчезновением/возвращением каналов.
18.03.2021: неа, не нравится "-i
",
переделал на "-N
" -- так оно мнемоничнее.
25.10.2021: синонимом теперь работает "-K
"
(keep going) -- для унификации с разными *_test/*mon.
04.04.2021: подумалось -- а не надо ли подобной же
способностью наделить и das-experiment? Заглянул в него
-- а в нём и так по CDA_RSLVSTAT_NOTFOUND
лишь печатается
сообщение, но более ничего: выбрасывание из списка отсутствует (оно и
понятно -- нарушилась бы структура выдачи, состоящей из таблицы
столбцов).
А dataset-save, в отличие от тех двоих, НЕ нужно такого: поскольку она не занимается продолжительным мониторингом (в течение которого каналы могут исчезать и появляться), а делает "мгновенный слепок", то ей фича "игнорировать события NOTFOUND" ни к чему.
fgets()
обламывается. В результате
cdaclient выдаёт лишь сообщение "no channels to work with", что несколько
сбивает с толку.
Произошло это на ВЭПП-4, когда Карнаев по ошибке сделал симлинк не на .dat-файл, а на "../", так что утилитка Polarity_switch.sh не могла загрузить позитронный режим.
Выглядит сие всё странно: в ReadFromFile()
проверка есть, но
она ошибок не ловит.
01.04.2021: разбираемся... Да, ситуация диковатая, проблема в самом C/UNIX API, но, похоже, никого оно не волнует.
open()
прекрасно отрабатывает, а вот подстилающий
read()
уже возвращает -21, т.е. EISDIR
.
O_DIRECTORY
, и почему open()
на директорию БЕЗ
этого флага не обламывается -- загадка.
Что делать -- неясно...
А может, после УСПЕШНОГО открытия пытаться посмотреть тип
файла -- что-то типа fstat(fileno(fp),&stat_buf)
с
проверкой S_ISDIR(stat_buf.st_mode)
?
Да -- сделал (только со всеми промежуточными проверками на результаты обоих вызовов), теперь выдаёт “WARNING "/etc" seems to be a directory; its reading is senseless”, во всех 3 утилитах.
...в принципе, аналогичная проблема может возникнуть и в любом другом
месте, где используется fopen()
.
01.04.2021: насчёт этих любых других мест:
fp_refers_to_dir()
filedes_refers_to_dir()
". Чтоб какое-нибудь открытие
могло б сразу обламываться.
Резон прост: работает -- не трогай! Мало ли какие косяки где могут вылезти (например, какое-нибудь особое устройство/файл (особая файловая система?) почему-то будут считаться и как директория -- битик там этот будет торчать).
Конкретно в ЭТОМ случае с утилитами -- решалась КОНКРЕТНАЯ проблема, причём всего лишь WARNING'ом, так что при надобности и на "директорию" натравить всё же можно.
-DY
", отражающийся на print_cycles
. Печатаются
"номер сервера" -- nth, передаваемый в info_int
-- и имя
сервера.
Неприятным сюрпризом стало то, что "имя сервера" НЕ включает в себя
схему. В частности, для "epics::" всегда будет просто пустота "".
Поэтому перед именем также печатается и схема, добываемая через
cda_status_srv_scheme()
(сделанную ещё 01-12-2015).
ЗЫ: сделано для возможности диагностики циклов, в частности, работы
свежевведённых сегодня $CDA_D_EPICS_CYCLE_USECS
и
$CDA_D_TANGO_CYCLE_USECS
.
Но тут вопрос именно в "на ходу":
cda_add_chan()
, БЕЗ evproc'ев, и
лишь ПОТОМ отдельно вызывается cda_add_dataref_evproc()
.
В результате при попытке отладки каналов-команд схемы "tango::" оная отладка обломилась: там события о готовности присылались прямо из регистрации, а поскольку evproc'а в этот момент ещё не было, то события игнорировались.
Кроме этих Tango'вских каналов-команд пока что таких "сразу отрабатываемых" вроде нет (т.к. insrv:: в cdaclient бессмысленны). Но в будущем могут появиться -- например, dat-плагин для доступа к каким-нибудь локальным ресурсам.
Можно попробовать "исправить", но будет это непросто: логика работы с
num2read
и num2write
рассчитана на то, что события
начинают приходить ПОСЛЕ регистрации всех каналов, так что:
Чуть позже: неа, это проверяется по nrefs
, а оно
декрементируется только при ошибках регистрации.
Соответственно, надо запускать основной цикл УСЛОВНО, если ещё осталась работа.
08.07.2024: краткая попытка понять, почему сделано именно так, раздельно.
main()
, а уже потом процесс был разделён на циклы по
RememberChannel()
и ActivateChannel()
, в
промежутке между которыми делается cda_new_context()
.
Было ли отделение навешивания evproc'а от регистрации канала обусловлено какой-то рациональной причиной или просто "а почему бы нет?" -- неясно, по записям в начале раздела ничего нет. А есть оно в разделе по das-experiment за 28-04-2015 -- в cdaclient было сделано так же вроде как для унификации.
11.07.2024: продолжение анализа -- смотрим уже архивные файлы.
Однако, судя по дополнительному разбирательству:
refrecs_list
; и добавление ячейки
в него производилось уже ПОСЛЕ регистрации канала, так что индексы
(rn
) в нём -- только для успешно зарегистрированных каналов
RememberOneChan()
, вызываемом при наполнении
списка каналов, происходящем при старте программы (список либо из
argv[]
, либо из файла).
Что согласуется с вышенайденной датой 28-04-2015 (где сказано, что "...и cdaclient.c на эту архитектуру переведён").
Вывод: возврат указания evproc'а прямо в регистрацию канала не выглядит потенциально проблемным.
11.07.2024: потихоньку...
sl_main_loop()
сделан условным -- если хоть одна из
num2read
или num2write
больше нуля. (Сделано ещё
09.07.2024.)
cda_add_chan()
.
Попробовал проверить -- и тут же нашёл причину того "навешивания позже":
ведь вся печать реально делается функциями из console_cda_util.c,
а они всю информацию берут из util_refrec_t *urp
, включая
urp->ref
, но он-то ещё незаполнен!
Решение впопыхах придумано и вроде простое:
DatarefEvproc()
проверяется, что если в
rp->ur.ref
ещё не прописано значение, то туда прописывается
переданное от cda значение ref
(которое уже "правильное"),
ref
явно
прописывается значением CDA_DATAREF_ERROR
-- чтоб было
конкретное "не прописано".
Вроде сделать такую штучку совсем несложно.
30.07.2014: конкретности -- как оно может быть устроено:
01.08.2014: выдрепнемся -- назовём тулзу "das-experiment".
19.08.2014: первоначальная -- да, несколько халтурная -- версия сделана.
Идеи? 28.04.2015: -f FILENAME, не?
22.08.2014: условия завершения сделаны -- 2 варианта:
exit()
).
28.04.2015: улучшаем -- в результате перехода на console_cda_util это стало легче:
...по-хорошему, надо б еще сделать, чтобы он старался реально
держать период (как CycleCallback()
в сервере), а не
просто +ПЕРИОД от момента вызова очередного.
RequestChannel()
, чтоб удобно были из -f
пользоваться.
ReadFromFile()
,
вычитывающая построчно, выкидывающая начальные/конечные пробелы, и
игнорирующая пустые строки и начинающиеся с '#'.
Только как-то это выглядит кривым/проблемным в реализации, так что пока не будем.
(Кривым -- потому, что парсинг getopt()
'ом сначала
даёт все ключи, и лишь потом парамеры. Поэтому смешивать BASEREF'ы
удастся только со списками из файлов.)
cda_add_chan()
'ить.
util_refrec_t
поле "spec", аллокировать ячейки прямо при
парсинге, а потом уж проходиться по всему списку скопом, "реализовывая"
его (отдельно может возникнуть вопрос -- что делать с ошибками
регистрации: Rls(), чтоб дальнейшему эккаунтингу (чтения и записи) не
мешали?).
Тут надо поразмыслить.
@после-обеда: а зачем сохранять ССЫЛКИ в список, когда можно сохранять туда имена файлов, и потом проходиться по ним итератором?
Хотя -- нет, интереснее выглядит всё-таки технология "накопить все ссылки, а потом их реализовывать".
28.04.2015: после обеда: да, переделано.
refrec_t
добавлено поле spec
.
RequestChannel()
--
разделена на 2 части:
RememberChannel()
парсит "команду", аллокирует
ячейку, сохраняет в ней результаты парсинга, в т.ч. в spec
strdup()'ится channame[].
Вызывается как для считанного из файлов, так и для указанного в командной строке.
ActivateChannel()
уже регистрирует канал у cda. И
если регистрация обломилась, то делает RlsRefRecSlot()
ячейке.
Вызывается после создания контекста, при помощи
ForeachRefRecSlot()
, поскольку является итератором.
MotifKnobs_SaveHistplotData()
.
Побудительный мотив -- чтоб полученные das-experiment'ом файлы можно б было просматривать утилитой histplot (функционал загрузки файлов в неё предполагается добавить).
Конкретно сейчас оное желание было вызвано ситуацией, когда нажурналированные на v5p3 логи от резонатора (в /var/tmp/rezonator-journal-since-YYYYMMDD-HHmm.log.gz) нормально никак не просмотреть: gnuplot'ом такие объёмы просто ужасны.
21.05.2018: Роговскому, как инициатору и вдохновителю создания утилиты, был отправлен мылом вопрос "можно ли, не вызовет ли проблем?".
22.05.2018: а еще желательно б иметь ключик, чтобы вначале перед данными выдавалась бы строка заголовков, совместимая с histplot'овской.
Ключ -H
(Headers)?
24.05.2018: ответ от Роговского получен: он не против, если будут писаться не только секунды, но и миллисекунды -- как и планировалось.
Делаем!
-H
, по нему выдаётся строка заголовков:
rp->ur.spec
; т.е., как в режиме RELATIVE -- относительно
базы.
19.08.2018: теперь только для CXDTYPE_REPR_FLOAT -- для TEXT оно не существует, а для INT всё равно бессмысленно.
Именно с ПЕРВОЙ -- время начинает "бежать" не с момента запуска, а с первого события.
Это сделано по образу и подобию canmon'а.
Работает (ну ещё бы! :)), "done".
17.06.2018: надо б добавить, чтобы das-experiment мог бы владеть информацией о disprange, для умения выдавать её в строку заголовков. Иначе в histplot'е фиговато -- диапазон [-100,+100] частенько совсем неподходящ.
Главный вопрос -- в синтаксисе...
29.06.2018: результаты размышлений и исследований на эту тему.
util_refrec_t
, чтоб она там просто была/лежала, самой утилитой
никак не используясь.
Запятулька и для shell'а индифферентна, и совпадает с синтаксисом histplot'а.
Но, на первый взгляд, проблема: оно ж используется в командах записи, для указания записываемого значения.
ParseDatarefSpec()
.
-1
каждая спецификация
из командной строки может размножиться на кучу экземпляров
refrec_t
.
И если потом кто-то из них будет признан невалидным и удалён, то НЕЛЬЗЯ в
RlsRefRecSlot()
'е делать free()
параметрам, т.к.
они будут разделяться между несколькими экземплярами.
Да, это халтурка, что -- в случае отсутствия разделения (т.е., единственный канал либо невалидны все) будет оставаться мусор в памяти, но тут на это можно забить (утилита ведь одноразово принимает параметры, и никакой постоянной утечки быть не может).
ЗЫ: это всё ПРОЕКТ -- годный для реализации, но пока лень.
ExpandName()
реальным содержимым.
12.05.2015: сделана поддержка перечислений вида
"{AAA,BBB,CCC}", практически копированием куска --
do_one_stage()
-- из v2'шного Cdr/fqkn.c.
А часом позже добавлены и диапазоны вида "<MIN-MAX>", почти
копированием из devtype_parser()
.
15.05.2015: а чтоб всё ж можно было указывать каналы с такими символами в именах, в утилиты добавлен ключ "-1".
10.11.2015: сделано:
UTIL_PRINT_RFLAGS
.
PrintDatarefData()
печатает флаги --
cx_strrflag_short()
'ом -- после timestamp'а, в угловых скобках.
07.03.2017: можно подвыпендриться:
-U
, либо -DU
(тогда его можно сделать умолчанием, а выключать по -D-U
.
Но чё-то оба варианта крайне не нравятся.
09.03.2017: да ну нафиг -- просто делаем ВСЕГДА
наличие CDA_DATAREF_OPT_ON_UPDATE
.
Тем более, что "по циклу" -- это было введено в первую очередь для GUI, а в утилитах вроде бы надо иметь просто самые свежие значения, раз-в-цикл им совершенно не нужно.
Поскольку вся троица утилит -- cdaclient.c,
das-experiment.c, dataset-save.c -- используют
ParseDatarefSpec()
(у них вообще у всех одинаковая архитектура
-- RememberChannel()
+RememberOneChan()
и последующим
ActivateChannel()
, пошедшая с cdaclient'а), то достаточно
оказалось вставить флажок в единственную точку -- в самое начало
ParseDatarefSpec()
.
Проверено на cdaclient'е, работает.
Первая мысль -- рисовать отдельную утилиту, делающую такое.
Вторая мысль -- надо бы в cdaclient добавить, чтоб можно было попросить векторный канал, но печатать из него только какой-то конкретный элемент.
16.10.2017: указывать надо явно как-то в префиксе канала, где-то среди всяких '@'.
Короче:
util_refrec_t
, чтоб там было поле "elem_n
" (какой
элемент вектора печатать, если не равно -1), и тогда выдавать его как скаляр.
(Или пару полей, если делать вырезку: "от и до", либо "от и сколько".)
18.10.2017@ВЭПП-5-кухня-~17часов-разговор-с-Валерой: а может, для ДАННОГО случая удобнее будет сваять драйверочек, который отдавал бы 1 скалярный канал, являющийся вырезкой из вектора?
Прям хоть вводи вызов SetChanAllRDs()
, чтоб он
указывал МНОГО {r,d}...
03.04.2021@вечер, 21:20, после добавления "@~:": перечитал сейчас хотелки этого пункта, и стало очевидно, что
ИМЯ_КАНАЛА[ИНДЕКС]
".
Соответственно, если дело дойдёт до "вырезки", то
"ИМЯ_КАНАЛА[НАЧАЛО-КОНЕЦ]
" или
"ИМЯ_КАНАЛА[НАЧАЛО,ДЛИНА]
"
@[ИНДЕКС]
.
А "вырезки" -- аналогично, "@[НАЧАЛО-КОНЕЦ]
" или
"@[НАЧАЛО,ДЛИНА]
".
Выглядит сие вполне реализуемо (в обоих вариантах, хотя второй проще), но делать такое стоит лишь в случае возникновения РЕАЛЬНОЙ потребности, а не "просто чтоб было, до кучи" -- ибо штука всё же нетривиальная, влекущая лишнее усложнение кода.
20.04.2018: анализ ситуации:
RememberChannel()
,
при помощи ParseDatarefSpec()
, результаты которого сохраняются
в slotarray из refrec_t
.
ActivateChannel()
, ...
cda_add_chan()
.
ParseDatarefSpec()
умела распознавать
также и формулы.
refrec_t
добавить поле srctype
, куда б
прописывался тип.
ActivateChannel()
будет выполнять нужный тип вызова
для регистрации -- в точности, как
Cdr_treeproc.c::cvt2ref()
.
А можно прямо и в console_cda_util'е сразу сделать функцию, выполняющую это (и заодно сообщение об ошибке выводящую в зависимости от типа). Тогда можно будет при желании и varchan'ами расширить.
В Cdr'е формула указывается префиксом '#', но тут такое не прокатит -- это начало комментария.
Если только анализом содержимого (что cvt2ref()
тоже делает
-- проверяя классы символов (уж пробел в формуле наверняка будет, и она
автоматом заклассифицируется как IS_FLA). 20.04.2018:
неа, нельзя -- это в Cdr просто имена каналов, а в cdaclient'е могут
указываться "КАНАЛ=ЗНАЧЕНИЕ" и "КАНАЛ ЗНАЧЕНИЕ" (команды записи). Так что
ТОЛЬКО с явным спецификатором типа.
Вариант: была ж мысль (16-10-2017) иметь возможность указывать "PSP-спецификации широкого профиля -- ОПЦИЯ=ЗНАЧЕНИЕ..."; так вот -- сделать прямо опцию srctype, могущую принимать значения chn,fla,var?
20.04.2018: кстати:
Даже если синтаксически как-то и удастся, то это несовместимо с моделью "ждём подтверждения записи путём прихода UPDATE".
Сделано тем же префиксом @~:; реализация оказалась очень проста
-- в ParseDatarefSpec()
делается
options &=~CDA_DATAREF_OPT_ON_UPDATE
Проверено -- работает.
И заодно сразу описано в help'е; заодно и @!: описано.
CXDTYPE_UNKNOWN
-- флагом "@?" вместо
спецификатора типа "@x:".
15.11.2024: исходные размышления см. в разделе по cda_d_epics.c за вчера и сегодня (ради тестирования поддержки UNKNOWN в нём и заморочился).
Собственно реализация -- почти целиком в console_cda_util.[ch]:
UTIL_FLAG_USE_UNKNOWN
, ...
util_refrec_t
-- поле util_flags
для её хранения.
ParseDatarefSpec()
взведение этого флага по
'?'.
ЗАМЕЧАНИЕ: количество элементов берётся от "основного" типа (и обычно это 1), но для CXDTYPE_UNKNOWN оно означает число байт; поэтому указывать нужно что-то вроде "@?d8:" -- тогда будет "x8", что как раз объём DOUBLE.
PrintDatarefData()
:
cda_dtype_of_ref()
, cda_current_dtype_of_ref()
,
...
urp->dtype
(при печати префикса
типа);
urp->dpyfmt
при печати значений
используется свежевведённая локальная dpyfmt
, ...
dtype
уставляется в urp->dpyfmt
, ...
16.11.2024: добавлены доп.условия -- "%u" для беззнаковых и "%lld" и "%llu" для INT64 и UINT64. Кстати, при генерации формата по умолчанию беззнаковость в расчёт не принимается.
ActivateChannel()
при взведённом
UTIL_FLAG_USE_UNKNOWN
(и не-wr_req
!) канал регистрируется
с типом CXDTYPE_UNKNOWN
вместо ur.dtype
.
Создаём.
06.12.2017: кстати, давно ведь ясно, что никакого dataset-restore не нужно, т.к. с этой ролью прекрасно справляется cdaclient.
На вид пашет.
Вопрос -- как поступать с недоступными каналами:
Во всех случаях -- писать в файл с префиксом '#'?
По ключам нужна будет максимальная совместимость с cdaclient, и пользоваться будет услугами console_cda_util'а.
Пока делать не будем (не до того; как Роговский попросит -- тогда и сподобимся), но раздел застолбим.
cx_srch()
.
В простом первоначальном варианте -- просто для тестирования этого самого интерфейса.
А потом можно будет и добавить функционала:
-N
) или не делать этого (ключ -n
).
-T SECONDS
.
Вот тут вопрос об именах ключей: напрашиваются -1
и
-2
, но первый уже занят в cdaclient под "do NOT expand {}
and <> in channel names".
-a
?).
06.12.2017: сделал, взяв основу с cxclient.c.
11.01.2018: добавлен резолвинг IP-адреса в имя.
'-n'
(Numeric, No-resolve).
evproc()
'е. С
очевидным "если облом (hp==NULL), то печатаем просто IP".
С резолвингом всё не так очевидно. История получилась длинная -- почти на два дня.
arp
.
INET_rresolve()
.
Там делается gethostbyaddr()
.
cx_open()
, решил сэкономить и
воспользоваться более простым gethostbyname()
: всего один
параметр -- имя, могущее также быть и IP-адресом.
gethostbyaddr()
(почти по образцу
net-tools, но с in_addr_t
вместо u_int32_t
) --
помогло.
If name is an IPv4 or IPv6 address, no lookup is performed and gethostbyname() simply copies name into the h_name field and its struct in_addr equivalent into the h_addr_list[0] field of the returned hostent structure.
gethostbyaddr()
(точнее,
вообще getaddrinfo()
, традиционно).
Засим начальный вариант считаем за "done".
-T DURATION
-- в течение какого времени ждать
ответа.
Очевидно, что для реализации пунктов 2-4 нужно поддерживать список запрошенных, аналогично cdaclient'овским refrecs.
Но также очевидно, что всё это сейчас делать НАФИГ НЕ НАДО! Только когда (если) появится потребность.
12.01.2018: вдогонку:
param1
и param2
.
И конкретно "-1
" и "-2
" (самые очевидные!)
выглядят не очень хорошими претендентами, т.к. в cdaclient ключ
-1
используется для "do NOT expand {} and <> in
channel names".
-T DURATION
также ввести
возможность затребовывания периодического пере-спрашивания (например,
-P PERIOD
)? Раз уж утилита готова висеть долго, то смысла это
делать, условно, дольше 1 секунды без пере-запрашивания просто нет.
Так вот: утилитка-то ПОКАЗАЛА оба ответа, но это чисто по везению -- что
второй успел придти за то время, пока печатался первый, а fdiolib'овский
DgramReadyForRead()
пытается вычитать несколько пакетов, если
они есть.
Но по-хорошему -- надо бы ждать какое-то время, а не отваливать сразу.
Появилась даже мысль сделать возможность указывать срок ожидания, и возник
вопрос "каким ключиком?" -- "-Tnnn
"? Да, ровно та же идея, что
в 2018-м.
Но ПОКА -- хотя бы чтоб печатались времена, когда получены ответы.
Например, тем же ключиком "-DT
" это указывать.
15.05.2021: да, вот печать времён и сделана --
указанием "-DT
". Только печатается с миллисекундами, при
помощи strcurtime_msc()
-- в отличие от остальных.
И для того случая на пультовой ВЭПП-5 -- оно показало, что разница между приходом ответов от cxout:3 и cxhw:3 -- буквально 1-2 миллисекунды.
15.05.2021: надо бы ещё и в самом cda_d_cx сделать
опциональную печать. Т.е., в ProcessSrchEvent()
надо бы
добавить диагностирование дубликата ответа, и если дубль, то при уставленном
в 1 или Y значении $ЧЕГО_НИБУДЬ печатать эту диагностику на stderr.
3 пункта ниже изначально были в разделе о cdaclient, в них поясняется смысл утилиты -- мочь принимать данные для отправки с stdin'а.
Насчёт названия были разные соображения: "cda-pipe-send" (длинно и начало перекрывается с "cdaclient" -- Tab использовать неудобно), "pipe-send" (неконкретно), "pipe-send-chan" (но ведь не один канал, а могут быть разные!), "pipe-send-data" (мутно как-то). В конце концов в голову пришло короткое и однозначное "pipe2cda".
ReadFromFile()
используется line[1000]
.
Использовать fdiolib в режиме FDIO_STRING? Оно как раз и даст готовый поток уже в памяти.
...вот если бы что-нибудь вроде принятого со времён pkzip "@file" (включая "@-"), но конкретно такой синтаксис не прокатит, поскольку CHANNAME=@SOMETHING является вполне валидной спецификацией, где передаётся прямо значение "@SOMETHING".
(Побудительный мотив -- желание уметь передавать cdaclient'ом картинки из беркаевской утилитки tvcapture.)
15.10.2019: утилита сделана, проверена, "done".
Смысл -- что разные экземпляры cdaclient, запущенные через
system()
/popen()
, могут, вследствие разных причин,
не соблюдать порядок; один же экземпляр программы этот порядок
гарантированно будет поддерживать.
...но это, видимо, как раз НЕ cdaclient (поскольку он-то слопывает список СНАЧАЛА, а работает с ним, готовым, ПОТОМ.
Изготовить отдельную утилиту (cda-send?), которая бы читала команды со stdin, и тут же бы их исполняла?
05.09.2019@дорога-на-обед, около ГУП УЭВ: не, не так -- cda-pipe-send!
И понятно, что если понадобится -- слабать эту штуку можно за несколько часов (из готовых кусков от cdaclient'а, дополнив FDIO_STRING-потоком.
05.09.2019: но будет отдельная тонкость в том, что использоваться в основном будет ОДИН И ТОТ ЖЕ канал, а cdaclient рассчитан на работу с РАЗНЫМИ, и аллокирование там памяти под каждый из них. И если поступить "по-простому", то каждая отправка будет отжирать буфер.
Надо:
Но аллокирование выполняет не ParseDatarefSpec()
, а уже
ParseDatarefVal()
.
Однако ситуация чуть хитрее: аллокирование в последнем не просто так по прихоти, а потому, что именно в значении может быть префикс [КОЛИЧЕСТВО]. Отсюда напрашивается решение:
util_refrec_t
по параметрам совпадает или нет),
то "выкидывать" старое (либо можно и в SLOTARRAY'е хранить).
ParseDatarefVal()
"научить" проверять "текущее
состояние" (значение поля databuf_allocd
) на годность для
текущей спецификации, и если мало -- то до-аллокировать.
15.10.2019: и потоковый режим сделан (и надлежащий менеджмент буферов), так что "done".
<
-редиректом из файла.
Напрашивается синтаксис "-B ИМЯ_КАНАЛА
", а на stdin'е --
данные. И операция ОДНОРАЗОВАЯ -- указывается один-единственный канал,
производится отправка, и потом утилита отваливает (хотя ключ -T
будет полезен).
...и это, очевидно, уже в той "отдельной утилите".
15.10.2019: и бинарный режим сделан (в т.ч.
"многократный" с -C
), и ограничение времени работы (только
через -W
).
09.09.2019: основу берём от cdaclient.c.
-d
/-b
, плюс -w
и
-T
.
-T
уже работает.
-B
-- "binary mode" (см. выше от
07-09-2019.
stdin_in_cb()
(cxscheduler-based) для режима бинарных данных и
ProcessFdioEvent()
(fdiolib-based) для обычного.
09.09.2019@вечер-дорога-домой, около ИПА: было бы удобно, если бы режим "-B" позволял бы МНОЖЕСТВЕННУЮ запись -- т.е., один раз установили соединение, а потом сбагриваем бинарные блоки по N байт, а софтина их отправляет.
Но тут на пути реализации есть несколько проблем:
...можно, конечно, считать, что:
Резюме:
А бинарный формат -- для одноразовых отправок.
10.09.2019@утро-дома: а
ведь очевидно, что таймаут -- который -T...
-- надо считать не
с момента запуска, а с момента ПОЛУЧЕНИЯ ПОСЛЕДНЕЙ ПОРЦИИ ДАННЫХ ДЛЯ
ОТПРАВКИ. Т.е.,
После чего даже идея: а не переименовать ли в этом
случае ключ -T
в -W
("Wait time")?
12.09.2019: ...далее...
-T
переименован в -W
, а его
функционал (заказ таймаута) вынесен в отдельную SetWaitLimit()
,
которую надо вызывать по EOF.
ParseDatarefSpec()
показал, что
она по ошибке просто делает exit()
.
Это правильное поведение для ранее созданных утилит, где список каналов создаётся в самом начале, и ошибку надо тут же "пресекать", но едва ли годится для долгоиграющей утилиты.
Надо вводить параметр "флаги", где указывать "не вылетать"?
13.09.2019@утро-дома, зарядка (только проснулся): некоторые мысли/соображения:
...хотя никто не мешает проверять, что если в момент FDIO_R_DATA уже
можно выполнять запись, то делать её прямо из inpkt
, а если ещё
нельзя -- тогда делать копию (для которой аллокировать буфер).
Вот надо прямо сейчас именно его и сделать, а на не-"-B" печатать сообщение с извинениями "упс, пока не реализовано".
13.09.2019: детально посмотрел схему работы fdiolib -- фиг, "пакеты фиксированного размера" там указываются как len_size=0, и считываются как "заголовок фиксированного размера без данных".
А при этом "EOF раньше времени" рассматривается как ошибка -- пакет
клиенту не доставляется вовсе, просто сразу возникает
FDIO_R_CLOSED
.
Так что идея "воспользоваться fdiolib'ом -- заказать пакеты фиксированного размера" обламывается -- придётся всё-таки читать вручную.
Что ж, тогда продолжаем "пилить" реализацию варианта "-B" в прежнем направлении.
Смысл -- в том, что при множественной отправке надо разделять буфер, из которого будет выполняться очередная отправка, и тот, в который в настоящий момент ведётся чтение. Иначе получится, что начало буфера содержит кусок "новых" данных, а следующая часть -- кусок "старых".
16.09.2019: ползём далее.
-C
(Continuous).
18.09.2019: ещё чуток:
bin_
".
stdin_in_cb()
сделан блок чтения, со всеми проверками
на тему результата (<0, ==0, else). Логика взята из fdiolib'овского
StreamReadyForRead()
, и даже имя "bin_reqreadsize" оттуда же.
В т.ч. с учётом будущего режима "-C" -- отдельный облом по EOF (а для "-B" он считается просто окончанием данных).
НЕ сделана реакция на всякие EOF и ошибки.
Также осталось сделать собственно реакцию на "все данные пришли".
bin_ur.databuf
.
PerformWrite()
взят из cdaclient'а, с
минимальными убавлениями.
19.09.2019: ...
ProcessDatarefEvent()
слизывается с cdaclient'овского, но
с отличиями:
exit()
в бинарном режиме).
А именно -- обработка флагов wr_snt
и wr_req
поменяна местами, и они теперь не if()/else(), а просто два if() друг за
дружкой (ради того порядок и изменён).
Идея проста -- использовать имеющееся rd_req
:
сначала взводить его в =1, а по первому же приходу данных сбрасывать.
Так и делаем:
А что, если такой сценарий:
Итого -- у нас в этот момент отправлено ДВА запроса на запись, но пометка "wr_snt=1" имеется в единственном экземпляре.
Тем самым считается, что ВСЕ отправленные запросы на запись уже отработаны! Хотя в реальности запрос из п.3 мог ещё даже не успеть уйти в сеть.
Итого:
Что делать?
Но тут вмешивается фактор "неопределённости выполнения запросов на запись": если отправить две штуки подряд (после ещё одного, "нулевого"), то с высокой вероятностью они будут в самом сервере склеены в один, через next_wr_val, и "ответ" придёт всего ОДИН. И программа тогда зависнет очень надолго (навеки?) в ожидании ответа, которого никогда и не будет.
Так и делаем: по получению данных для записи PerformWrite()
вызывается только при wr_snt==0.
EOF_rcvd
, взводящийся как по реальному EOF,
так и при бинарном-однократном режиме по вычитыванию всей ожидаышейся пачки
байт.
А в ProcessDatarefEvent()
добавлена проверка на него (что
всё исполнено и пора отваливать) -- пока хаковатая, с проверкой на
бинарность и отсутствие отправленности запроса.
util_refrec_t
на
refrec_t
-- чтоб в текстовом режиме было годно.
SetWaitLimit()
в точки
после окончания получения данных.
Проверено -- бинарный режим более-менее работоспособен. Результаты:
И в какой-то момент производить копирование в отправной (видимо, по окончанию чтения очередной порции).
Отключать приём данных на время обработки запроса?
Но тогда может возникнуть крайне нежелательная ситуация, что программа-отправитель (например, телекамерная -- там-то объёмы большие) заблокируется на записи, поскольку программа-читатель pipe2cda не будет вычитывать свой входной буфер.
Ох, мутновато... Пока оставим как есть, а если припрёт ("надо передавать ВСЕ!"), то придётся ключ вводить и махинировать маской (то 0, то SL_RD).
20.09.2019: последовательность соображений на тему 2-го буфера для текстового режима (почти "поток мыслей" в разных точках пространства и времени):
Ну и что, надо делать функцию "activate second buffer"?
Вывод: надо переходить на refrec_t
, в котором завести поле
вроде "buf2".
Пара соображений по менеджменту этого буфера с точки зрения аллокирования/роста:
@на работе, ~16:00: а почему именно "максимальный" и "если ещё больше"? А не в обратную сторону?
...ну разве что перейти в режим "побайтного вычитывания" до остатка текущего объёма, в надежде, что к следующему пакету текущая запись отработается и можно будет читать уже в основной буфер?
20.09.2019@пляж, ~14:10, дорожка
вниз от парковки-с-бочкой к пляжу: зря ломал голову и сокрушался
потенциальной сложности -- напрашивается простейший алгоритм, бОльшая часть
которого идёт в ProcessFdioEvent()
:
На всякий случай -- ради EPICS и подобных -- сравнение можно делать case-sensitive.
Замечание: канал регистрируем с флагом PRIVATE, чтобы он не перекрылся с аналогичным каналом, совпадающим по имени с точностью до регистра (case) -- см. "на всякий случай" к предыдущему пункту.
Тогда они будут считаться "имеющимися", так что повторных попыток регистрации более не будет, но функционировать никак не станут.
Это актуально для случаев, когда из программы-юзера передаётся ошибочное имя канала.
Буфер для парсинга выбираем либо прямо внутренний (если wr_req==0), либо в дополнительный.
n_items
!).
Всё, достаточно просто и линейно, никакого особого менеджмента/учёта ячеек-каналов не требуется.
Единственное что -- нужно будет вести учёт вроде "num2write", чтобы
понимать, когда при уже взведённом EOF_rcvd
можно считать всё
выполненным и отваливать.
20.09.2019: собственно дела:
cdaclient.c
кусок с определением
refrec_t
и менеджментом их SLOTARRAY'я.
refrec_t
.
SetWaitLimit()
поставлены после всех 3 штук
sl_del_fd()
.
...проверено -- он работает (по крайней мере, в случаях 2 и 3 (т.е., по реальному EOF и по вычитыванию всего ожидавшегося при "-B")).
24.09.2019: а теперь наконец-то реализуем текстовый режим по описанному 4 дня назад сценарию:
global_argv0
=argv[0].
ProcessFdioEvent()
набита функционалом:
goto
из всех таких мест. И там также
вызывается SetWaitLimit()
.
25.09.2019@утро-душ: к
вопросу о переделке многих функций console_cda_util, дабы по ошибке не
отваливали, а возвращали бы -1
:
exit(EC_USR_ERR)
, а иначе return -1
" надо не
дублировать в десятках мест, а запхать в макрос, который во все эти места и
вставить (эта мысль давняя :-)).
27.09.2019: эх!
EC_USR_ERR_is_fatal
, по
умолчанию =1.
console_cda_util_EC_USR_ERR_mode()
.
pipe2cda.c
вызывается с переводом в режим "0"
только если выбран текстовый режим (а для бинарного, впрочем, и неважно).
RAISE_EC_USR_ERR()
.
ParseDatarefVal()
УЖЕ приспособлена к тому, что
вызываемые ею ParseOneChar()
и ParseOneDatum()
могут вернуть -1
-- в ней присутствуют проверки и return
-1
. Видимо, изначально (в 2014-м?) так было создано.
ParseDatarefVal()
, чтобы она НЕ
пыталась аллокировать память, если уже databuf!=NULL -- это для случаев,
когда аллокированием заведует сама утилита.
buf2
=NULL и buf2_allocd
=0.
stdin_in_cb()
. При надобности (если wr_req!=0) аллокируется;
чтение тогда идёт в него; по полному прочтению его содержимое копируется в
основной буфер.
ParseDatarefSpec()
-- при ошибках (от парсинга) просто делается
return
.
buf2
--
аллокирование (если раньше не было) и использование.
Фактически, есть ДВЕ ветки кода: короткая -- когда парсинг делается в основной буфер (val2wr или databuf) и длинная -- через buf2.
Вот вроде и всё.
30.09.2019: проверяем, теперь уже всё оптом.
...из НАСТОЯЩЕГО файла -- происходит. В чём разница?
Поразбирался -- и из пайпа тоже работает, всё дело в конкретном пайпе.
for i in 1 2 3 4 5 6 7 8 9;do;echo -n 'AAA'$i; sleep 1; done | ~/work/4cx/src/programs/utils/pipe2cda -C @i:%x:localhost:2.a.0
-- почему-то всё ОК.for i in 1 2 3 4 5 6 7 8 9;do;echo -n 'AAA'$i; done | ~/work/4cx/src/programs/utils/pipe2cda -C @i:%x:localhost:2.a.0
-- т.е., с ЯВНЫМ завершением исходной цепочки команд, после чего должен придти EOF, оно НЕ завершается.(for i in 1 2 3 4 5 6 7 8 9;do;echo -n 'AAA'$i;sleep 0.001;done;exit 0) | ~/work/4cx/src/programs/utils/pipe2cda -C @i:%x:localhost:2.a.0
Т.е., ВОЗМОЖНО -- проблема в каких-то аспектах работы shell'а, и конкретно zsh.
НО:
Окей, вставляем отладочную печать, и... BINGO!!! Проблема действительно в коде: последний read() возвращает 0, но программа почему-то не отваливает.
Разбираемся:
PerformWrite()
.
А при НАЛИЧИИ sleep -- после r==0 почему-то НЕ всегда отваливает.
...ещё после нескольких тестов: неа, иногда и при отсутствии sleep НЕ отваливает, а уж при наличии -- не отваливает практически ВСЕГДА.
Поэтому далее всё работает почти как в режиме "-B" -- все данные есть, EOF_rcvd, так что выполняется короткая последовательность "получили первый CURVAL/UPDATE, отправили запись, получили второй CURVAL/UPDATE, и раз всё -- то отвалили".
EOF_rcvd
выполняется ТОЛЬКО в
ProcessDatarefEvent()
.
А раз в stdin_in_cb()
по r==0 в режиме "-C" делается только
"return", а все входные данные уже пришли и новых CURVAL/UPDATE более не
придёт, то отваливать и нечему/негде.
Блин, сколько времени потрачено из-за тупой ошибки-недоделки!!!
Итого: в ветку "r==0, при этом option_cont_bin" добавлена проверка, что если "wr_req==0 и wr_snt==0", то сразу exit(EC_OK).
После чего проблема исчезла.
ProcessDatarefEvent()
, при
"EOF_rcvd && num2write == 0
".
num2write++
делать при переходе у refrec'а дуплета
(wr_req,wr_snt) из состояния (0,0) в любое иное.
num2write--
-- соответственно, при переходе В состояние
(0,0) из любого другого.
Правила выглядят простыми.
Но тут будет некоторая засада с тем, что нужно корректно ДВАЖДЫ указывать номера дескрипторов: для редиректа (NNN<FILENAME и NNN:CHANNEL...). Хотя при использовании из другой программы (для чего и требуется) -- вроде не проблема.
Такое позволит использовать zsh'ную (а ныне и bash'евскую) фичу "process
substitution" -- "<(COMMANDs)
, когда можно будет указывать
CHANNEL-TO-WRITE=<(COMMANDs), и shell сам заменит "<(COMMANDs)" на
/proc/self/fd/NNN.
(Мысль пришла в голову, когда обдумывал перспективы возможного будущего
развития ProcessDatarefEvent()
-- ведь там refrec от бинарного
режима отрабатывается чуть отдельно, НЕ берясь из ptr2lint(privptr2). А
если бы понадобились МНОЖЕСТВЕННЫЕ бинарные ссылки -- то тогда надо будет и
бинарные тоже менеджить в SLOTARRAY'е.)
Только практического смысла в таком финте -- примерно ноль.
Проверяем -- вроде всё работает. ПОЧТИ...
Проблема оказалась идентичной утренней: в ProcessFdioEvent()
в блоке STOP_READING_STDIN
отсутствовала проверка "если более
нет неисполненных запросов на запись, то можно отвалить".
После добавления проверки -- проблема исчезла.
Причина -- технологическая: попытка записать 0 байт в скалярный (t1) канал пресекается прямо самою cda. А утилитка этого не понимает.
Вот тут "корректного" решения и не существует, так что не будем пытаться ничего исправить.
Для устранения непоняток добавлена печать ошибки от
cda_snd_ref_data()
.
Есть также некоторые странности в режиме "-C"
В остальном -- похоже, всё работает как надо.
15.10.2019: провёл дополнительный тест -- попытку использовать pipe2cda для "передачи файлов через CX".
./sbin/cxsd -dsc configs/cxsd.conf -f <(echo dev a noop w1t$[1048576*10] -) :1
pipe2cda -B @t$[1024576*10]:localhost:1.a.0 </bin/ls
cdaclient -B @t$[1024576*10]:localhost:1.a.0 >/tmp/file
md5sum /tmp/file /bin/ls
MD5-суммы совпали.
При повторении с более крупными файлами -- тоже.
Засим можно считать утилиту отлаженной и достаточно рабочей, так что "done".
Побудительный мотив: на vepp4-pult6 в очередной раз переполнился /var/tmp/, потому что карнаевский скрипт V3V4_PV2CX с дикой частотой вызывает cdaclient (точнее, он это делает 1 раз в 1 секунду, но с большим количеством каналов). Работа этого скрипта -- перекладывать данные из EPICS в CX. Но ведь такой подход -- раз в секунду читать из одного и писать в другое -- совершенно дик!
Учитывая наличие в cda поддержки протокола epics::, можно иметь просто бинарную программу, которая бы мониторировала каналы на одной стороне и при обновлении дублировала бы оные на другую.
29.01.2020@утро, ~12:10, по дороге через студгородок к родителям: некоторые соображения по деталям реализации:
Вечером: соответственно, и контекстов тоже иметь надо будет 2.
И ключ "-1
" чтоб традицилнно отключал бы такое расширение.
Только одна проблема: поскольку указываются ПАРЫ, то надо бы как-то и в
правой части уметь делать соответствующую подстановку. Ну и как,
спрашивается? 30.01.2020: учитывая, что расширители
"{}" и "<>" НЕ бывают вложенными, напрашивается простая схема с
backreference'ами $1, $2 и т.д. (причём хватит и
односимвольных до $9, а $10 вряд ли потребуется). Но тут,
вероятно, понадобится соответствующее сотрудничество от
ExpandName()
+do_one_stage()
-- получать эти
backreference'ы; и это будет весьма нетривиально, при нынешней модели работы
расширения.
Но вот прочие флаги (вроде NO_RD_CONV), если они указаны -- должны быть индивидуальны для каждого из пары.
Но стоит упомянуть 2 нюанса:
Для таких случаев полезно будет предусмотреть ключик -R
--
"Reverse the mirroring direction".
(Замечание: реверсироваться должно именно только направление; а вот правило "использовать dtype+nelems от канала слева" должно оставаться.)
30.01.2020: да, мутно как-то и непонятно -- когда ж РЕАЛЬНО такое может понадобиться? :-)
В общем случае это дело проблемное -- получится бесконечный пинг-понг. Но можно попробовать проверять, что если текущее известное значение в канале, куда хотим записать, СОВПАДАЕТ со значением, которое хотим записать -- то запись не делать. Это разорвёт цикл.
(Хотя одновременно это срежет обновления, когда именно в исходном канале заново прописывается то же самое значение -- при периодических измерениях это совершенно нормально. Тут уж ничего не поделаешь, и единственным решением -- даже чисто по теории информации -- было бы уметь получать "идентификатор того, кто выполнил запись": тогда можно б было проверять, что если это МЫ -- то далее [обратную] запись выполнять не нужно. Но асинхронная модель CX подобного не позволит...)
Так вот: для такого случая, видимо, будет ключ -2
--
"2-directional mirroring".
-v
" -- verbose operation (print data
events).
30.01.2020@утро, дорога на работу: и ещё пара мыслей:
При этом автоматически решается проблема "а какой dtype/nelems у канала?" -- он может брать прямо из свойств соответствующего аппаратного канала (да, тот для этого должен быть локальным).
@уже на улице: cписки каналов ему указывать, очевидно, надо такими же файлами, ссылки на которые давать в auxinfo (да, списком). Ну и baseref'ы как-то там же указывать.
@за-беркаевским-домом: имя драйверу можно дать "bridge_import" или "bridge_imp".
И уж тут драйвер бы просто проходился по списку своих каналов, получая их аппаратные свойства (как это делает mqtt_mapping).
Что любопытно, при этом можно вообще обойтись БЕЗ отдельного списка
соответствия каналов, а указывать ссылочные каналы прямо в devtype'ах: ведь
теперь там МОЖНО указывать строки -- вот и использовать, например,
comment
. ...хотя и напрашивается мысль использовать
rsrvd6
.
Бонус -- baseref, который теперь единственный, можно указывать просто в auxinfo.
@ИЯФ, лестница с 5-го на 6-й и перед 613-й: а минус -- в том, что имена самих каналов, видимых снаружи, будут уже не столь гибкими -- никакой иерархичности; ну либо указывать эти имена с точками '.' (уже ж можно, ради ЕманоФеди сделано), но это будет фейковая иерархия -- изнутри сервера ссылаться cpoint'ами на её компоненты нельзя.
...или всё-таки как-то модифицировать систему cpoint'ов, чтобы они умели
ссылаться "на середину имени"? Ну типа -- какой-то другой
CXSD_DB_CLVL_ITEM_TYPE_nnn
...
06.02.2020@вечер-дома: в варианте драйвера bridge_imp_drv вполне можно разрешить и ЗАПИСЬ в его "внешние" каналы -- чтобы он эту запись просто редиректил бы в соответствующий бриджуемый канал.
При этом получилась бы правильная "обычная" семантика: производится запись в канал, она редиректится в реальный бриджуемый канал, а когда там реально происходит, то наверх прилетает уведомление об обновлении значения.
17.02.2020: драйвер bridge_drv.c вчера реализован и, судя по тестам, он вполне работает.
Так что потребность в отдельной утилитке bridge_imp пока снижается (до тех пор, пока не потребуется бриджинг в обратную сторону).
19.07.2009: summary:
Или -- реализовывать диагностические программы .subsys-панелями, чтобы эта самая cxplore могла бы сама подгружать их по мере надобности?
-readonly
, имевшейся в v2.
Hint: CdrSetSubsystemRO()
.
09.03.2017: сделано, в рамках перевода
pult.c::main()
на схему с PK_nnn
(для
возможности указывать winopt-параметры в командной строке).
#include
"pzframeclient_knobset.h"
и
RegisterPzframeclientKnobset()
.
Надо б чтоб использовался тот же самый исходный файл, чтоб он позволял
добавлять те две штучки -D
-ключами из командной
строки/Makefile.
02.08.2016: сделано:
SPECIFIC_KNOBSET_H_FILE
-- имя файла; должно быть в
двойных кавычках.
SPECIFIC_REGISTERKNOBSET_CODE
-- код для регистрации.
Произвольный код -- если это функция, то должно быть с "()
";
символ ';
' после него ставится автоматом.
-D
.
Всё OK.
winopts
).
Например -- hist_period
.
01.03.2017: ситуация такова, что парсингом argv[]
занимается pult.c, а обработкой параметров --
Chl_app.c::ChlRunSubsystem()
. Спрашивается -- и как
их передавать?
Сходу в голову приходит идея: ввести в дополнение к
DSTN_WINOPTIONS
еще какой-нибудь тип секции, и чтобы pult при
парсинге argv[] ("умном", как в v2'шном ChlRunApp()
с
PK_nnn
) создавал такие секции, помещая туда argv[n].
05.03.2017: также можно добавить специальную опцию "forbid_argv_params" -- чтобы параметры командной строки игнорировались.
Поскольку сначала делается psp_parse(winoptions,...), а уж потом будет цикл с psp_parse_as(argv[],...), то реализация тривиальна -- цикл будет условным, по НЕвзведённости флага "forbid".
09.03.2017: делаем.
DSTN_ARGVWINOPT
(="argv_win_opt").
appopts_t.forbid_argv_params
и PSP-ключ
для него.
win_options
добавлен условный -- при
forbid_argv_params==0 -- парсинг всех DSTN_ARGVWINOPT
-секций, с
флагом PSP_MF_NOINIT
.
Ошибки парсинга так же фатальны, как и у основных win_options.
pult -readonly linmag notoolbar
Причина -- надо иметь живой ds
, чтобы добавлять ему секции
DSTN_ARGVWINOPT
.
10.03.2017: а избежать этого можно было бы тривиально -- парсингом в 2 стадии (что, кстати, делалось в v2'шном Chl_app'е).
argv[1]
может произойти и предварительно, ДО цикла, то там же и
инициализируется счётчик цикла arg_n
, а в случае загрузки он
++
'ится, "потребляя" argv[1]
.
PK_FILE
-параметры складируются в
file_to_load
, но значение пока не используется, по причине пока
что неспособности cda корректно отрабатывать запись ДО прихода
{R,D}-калибровок.
PK_PARAM
считается по умолчанию (если не попало в
другую категорию). Поэтому "просто слово" (вроде "notoolbar"), указанное
после "просто слова" же, взятого за имя подсистемы, считается уже
параметром.
Складирование в секцию делается тут же -- это просто.
-readonly
.
В качестве резюме:
as_is
) с новым стилем парсинга параметров.
21.03.2017: был косяк: в переделанной схеме оно
по-прежнему НЕ проверяло результат первой ("предварительной") попытки
загрузки подсистемы, но последующая загрузка при выставленном
sysname
уже не выполнялась (argv[x] уже не рассматривается как
кандидат на PK_SUBSYS).
В результате оно потом -- при ошибке в подсистеме (да хоть
синтаксической) -- ничего не грузило, и потом SIGSEGV'илось в
CdrSetSubsystemRO()
из-за ds==NULL.
Решено радикально: блок "превентивной" загрузки полностью переделан, и теперь ошибка загрузки выдаёт фатальную ошибку.
02.07.2018: доигрался тогда, в марте 2017-го -- доисправлялся до того, что код проверки «а есть ли у нас sysname?», содержавшийся в условии “Check if we are run "as is" instead of via symlink” в новой ветке ("на PK_nnn") так и не был воспроизведён.
В результате запуск просто "pult" приводит к SIGSEGV -- в точке, где оно
пытается сделать CdrSetSubsystemRO()
.
Сейчас проверка добавлена -- сразу после цикла по argv[] проверяется, что
если до сих пор ds==NULL
, то выдаётся ругательство и баста.
03.07.2018: за компанию, коль уж cda теперь умеет правильно откладывать запись до получения {R,D} -- делаем и загрузку указанного в командной строке файла режима.
Как показала проверка простейшей тестовой программкой (она тут ниже
закомментарена), таково поведение fopen()
-- он позволяет
открыть и директорию, хотя потом первый же fgets()
==NULL.
26.09.2014: еще некоторые мысли для "презентации-обоснования":
Дополнительные бонусы модульности:
- То, что во многих СУ именуется "display manager'ом", в CX является библиотекой, и на его основе можно делать свои специализированные программы.
- А добавив в программу еще и CX-сервер -- также являющийся модульной библиотекой" -- мы получаем законченную программу для автоматизации малых стендов, умеющую работать с любой управляющей электроникой.
Чуть косноязычно, конечно -- надо улучшать, но смысл ясен.
01.10.2014: ...если хочется иметь в программе CX-сервер, то он должен бы включать в себя функционал не только libcxsd.a, но и специфичности из programs/server/, вроде cxsd_config.[ch].
Следовательно,
Мда...
22.10.2014: если хорошенько подумать -- а ЗАЧЕМ тащить сюда всю обвязку сервера? Ведь:
...хотя тут понадобится поддержка от cxlogger+cxsd_logger, чтоб позволяли логгинг ТОЛЬКО на консоль, без файлов вовсе.
...возможно, аналогично для libs и exts, когда они появятся (libs-path и exts-path).
Парой часов позже: да, сделано, stand
собирается и
запускается.
P.S. По факту это сейчас нечто вроде "insrvtest" :)
22.10.2014@вечер-путь-домой: вообще всё немножко сложнее.
(Кстати, как там должно работать -- пока не совсем ясно. Программа общается с сервером по протоколу вроде local? Или что или как? 23.09.2015: вариант -- см. идею за сегодня в разделе cxsd_fe_cx.)
InitBuiltins()
.
08.11.2014: первые попытки тестирования с
загрузкой драйверов. Не грузились -- поскольку отсутствовало то, чем в
cxsd занят cxsd_builtins.c. А конкретно не хватало
DoDriverLog
, плюс в Makefile отсутствовало указание
stand: SPECIFIC_LDFLAGS=-Wl,-export-dynamic
08.11.2014: кстати, для указания путей модулей
(которое в конфиге drivers-path) введён ключ -m
(Modules path).
10.12.2014: насчёт "тащить обвязку сервера" -- может потребоваться, для сценария "локальный стенд, к которому могут коннектиться и другие клиенты".
Реализовать можно так: если в командной строке указано ":N" (номер сервера), то инициализировать frontend'ы, иначе -- нет.
Кстати, отдельно надо думать, как обходиться в stand-подобных программах вроде kozclient, которым указывается ОДНО устройство и .subsys-экран для него: надо б как-то имя стандартизовать и подсовывать, чтобы не надо было в экран (стандартный для устройства!) его запихивать. Имя -- например, "singledev".
23.12.2014: да, сделано:
builtins_list[]
.
CxsdActivateFrontends()
.
...парсинг командной строки, конечно, халтурный. Ничего и близко похожего на "проход по всем, с определением типа параметра".
-m
переименован в -M
. Смысл --
чтоб изготовить еще аналогичные ключики -F
-E
-L
(для Frontend'ов, Extensions и Libs соответственно).
04.08.2015: давно уже заюзано (еще полгода назад, при испытаниях CAN- и cPCI-драйверов в CP6003), так что раздел считаем за "done", а прочие фичи (включая придуманные выше, вроде kozclient с возможностью указания адреса единственного девайса) пойдут в свои собственные разделы.
20.09.2015@утро воскресенья, по пути на работу, около
ИХБФМ: а можно ж сделать, чтобы ошибка
CxsdActivateFrontends()
НЕ считалась фатальной, а просто
бы НЕ вызывалось ActivateHW()
.
Тогда получится, что можно в любой момент запускать одну и ту же программу-стенд (например, на сварке), и если она "первая" -- то берёт на себя управление железом.
И связанная идейка: а что, если
CxsdActivateFrontends()
не будет возвращать
-1
по первому же облому, а продолжит инициализировать
следующие, и вернёт -КОЛИЧЕСТВО_ОБЛОМИВШИХСЯ?
21.09.2015: ...неа, так просто не получится.
Но это-то решабельно -- можно ВСЕГДА ставить "cx::..." (потеря в скорости мало важна).
(Хотя возникает race condition при одновременном запуске пары программ -- могут обе обломиться частично, на НЕпересекающихся наборах frontend'ов.)
07.12.2018: интересно, вот КАК тогда проверял работу
ключей -M
, -F
, -E
, -L
--
да хотя бы первого? Ведь в вызове getopt()
в параметре
optstring
ОТСУТСТВОВАЛИ "M:", "F:",
"E:", "L:"...
Ну чо -- добавлено. Но качество "тестирования", конечно, внушаить (c)...
Надо бы всё-таки даже в stand иметь возможность читать config-файл, чтобы stand мог использоваться взаимозаменяемо с cxsd.
Для чего нужно унифицировать config-читалку -- чтобы один и тот же код использовался в обеих программах.
Например, что-нибудь вроде "sktcanstand" -- софтина, которая бы позволяла сделать "стендик" для работы с любым поддерживаемым CAN-модулем через SocketCAN.
22.10.2019: как бы предыстория:
Очевидно, что для этого, при нашей клиент-серверной архитектуре, нужно, чтобы программа:
Вот из этих хотелок и родился в конечном итоге нынешний stand.c.
Но он всё же недостаточен -- нет возможности "интерактивно указывать...": и ладно бы ещё тип устройства (это непросто с точки зрения скрина -- у нас они умеют только создаваться, а не удалятьс, так что "жонглировать" ручками и скринами мы не умеем), но и даже адрес на шине оперативно менять нельзя -- только через перезапуск.
Но тут уже явно напрашивается необходимость иметь такую софтинку, но обязательно с возможностью интерактивного указания адреса.
Теперь собственно мысли:
Так вот, речь об интерфейсе указания адреса. Мысли были всякие (типа просто поля в тулбаре), но наиболее простым и элегантным видится такой:
"dev MAIN %s@sktcankoz ~ %d,%d", typename, line_n, kid
(да, .devtype-файлы нужно будет где-то добыть -- видимо, "слить
вместе" в строку-БД ранее, а "конфиг" по шаблону вставить после них).
И делается старт сервера.
Но тут как раз всё просто: достаточно сразу после чтения скрина --
CdrLoadSubsystem()
-- подменить значение секции
"main",DSTN_WINOPTIONS
.
(Да, отдельный вопрос -- КАК подменять: готового единого API в Cdr нету,
поскольку CdrCreateSection()
сразу не глядя СОЗДАЁТ НОВУЮ. Но
придумаем.)
Ясно, что аналогичный функционал по идее потребен бы и для других "шин" и даже "подключений CAN", но там пока не очень ясно, как можно всё обобщить, а как конкретно для SocketCAN -- вполне ясно.
Потребность -- мочь сгенерить pzframeclient-аналог stand'а (с поддержкой всех осциллографов). Захотелось чисто из спортивного интереса -- смогу ли сделать "диагностический скрин" для работы с "контроллером Tornado", через EPICS (предполагается, что 1) будет использоваться devlist с bridge_drv; 2) сварганим fastadc-плагин "простейший осциллограф", адресующийся к 3 каналам: data, cur_numpts, numpts (этот чисто для удобства -- управление длиной выборки на лету)).
02.04.2022: ну технически вроде несложно.
SPECIFIC_KNOBSET_H_FILE
и
SPECIFIC_REGISTERKNOBSET_CODE
.
$(LIBCDA_D_INSRV) $(LIBCXSD_PLUGINS) $(LIBCXSD)
-- скопировал строчку из 4cx/src/programs/xmclients/Makefile.
Проверил в простейшем варианте -- вроде бы работает, так что "done".
26.05.2018: сделано -- делов-то.
21.05.2018: очевидно, что времена будут подписываться не вполне корректно: сам-то компонент histplot предполагает, что имеет дело с равномерным массивом, а при чтении из файла это не гарантируется:
Но на это проще забить -- особой необходимости тут нет, больше важна просто возможность просмотра. 23.05.2018: читать из файла!!! Подробнее -- ниже за сегодня.
22.05.2018@утро-дорога-на-работу-около-ИПА: в случае загрузки файла можно (нужно!) вообще никуда не коннектиться.
Вечером: судя по тексту histplot.c, для этого
достаточно в конце main()
-- перед XhShowWindow()
-- НЕ делать CdrRealize и не заказывать таймаут на сдвиг истории.
22.05.2018@утро-дорога-на-работу-около-ИПА/ИЦиГ: кстати, а как насчёт самодостаточности файлов? Чтоб прямо В НИХ были и dpyfmt, и hist_period?
Теоретически, можно:
В общем, с этим пока просто хбз :)
26.05.2018: впрочем, с учётом чтения из файла прямо меток времени, hist_period становится просто неактуальным -- так что проблема исчезает.
Вечером: а еще бы чтоб можно было указывать ТОЛЬКО файл, а имена каналов (и, следовательно, их количество) чтоб само брало из файла.
26.05.2018: а еще диапазоны отображения -- при неуказывании каналов в командной строке им взяться будет совсем неоткуда...
23.05.2018@утро-дома: насчёт "времена будут подписываться не вполне корректно": а ведь можно и времена тоже вычитывать из файла: 1-я колонка -- прямо в формате timeval'овых tv_sec.tv_usec!
Чуть подробнее:
MotifKnobs_histplot_t
завести дополнительное поле
--
struct timeval *timestamps_ring; int timestamps_ring_used;
Замечание: и тогда нужно прямо в histplot.c прописывать конкретное число, а не полагаться на Cdr'ное умолчание "0=>86400".
Т.к. показываться будут прямо сразу АБСОЛЮТНЫЕ времена!
С идеологической точки зрения это, конечно, халтура -- как и сама загрузка данных самой программой вместо Cdr'а: нарушается инкапсуляция. Но результат будет достигнут, и весьма занедорого.
23.05.2018@утро-дома: а еще отдельный вопрос -- КАК указывать файл?
Видимо, придётся использовать эвристику, как в
PzframeMain()
. В частности, если первый символ '%' --
то наверняка канал; если присутствует '/' -- вероятно, имя файла.
25.05.2018: да, но решили ж парсить через ppf4td -- а как ему сбагривать stdin? (Ну, кроме linux-specific /dev/stdin?)
28.05.2018: с другой стороны, сейчас нигде кроме Linux оно и не надо, так что пусть будет "-"=>/dev/stdin.
24.05.2018:@утро-дорога-на-работу-около-вивария-ИЦиГ:
Видимо, надо делать 2 варианта вызова:
Вариант "указаны и каналы, и файл" -- выглядит странно.
И да, надо из строки заголовков брать и имена, и (при указанности) %DPYFMT.
Это чуть более сложно, но зато позволит читать ЛЮБЫЕ подобные файлы (26.05.2018: хотя кто еще, кроме CX'ных утилит, сгенерит такое -- с 3 timestamp'ами в начале каждой строки... Даже у fastadc формат иной...).
В простейшем варианте -- в случае невозможности парсинга (ЛЮБОЙ невозможности!) пропускать до первого whitespace.
26.05.2018: более продвинутый вариант -- эмулировать ppf4td_get_string(), выкидывая выпарсиваемое "в никуда": понимая парные кавычки (внутри них могут быть пробелы!) и забэкслэшимости.
26.05.2018: эмулировать -- плохо: дублирование кода, да и
рассинхронизоваться можно. А что, если в
ppf4td_get_string()
добавить флаг "выкидывай результаты, никуда
не складывая, и пофиг на длину"? 30.05.2018:
сделан, начинаем пользоваться.
26.05.2018: приступаем.
MOTIFKNOBS_HISTPLOT_FLAG_VIEW_ONLY
.
28.05.2018: далее.
LoadPlot()
. Скелет взят от
cxsd_config.c::ReadConfig()
.
28.05.2018@вечер-дорога-домой-около-ИПА: если когда-нибудь захочется сделать в v4'шном Cdr логгинг, то пусть он по формату совпадает. Это позволит показывать такие log-файлы histplot'ом же (неудобно будет, из-за множественных начал, но всё же).
29.05.2018@утро-подъезд-выходя-на-работу: а еще можно выпендриться и указывать в командной строке, КАКИЕ каналы из файла брать -- из каких колонок.
Вопрос -- откуда считать? Колонками, так что 1-й канал будет ",4", или прямо каналами, чтоб ",1" означало "4-я колонка"?
...хотя это уже overkill, реально нафиг не нужно -- задача уже не для крохотной утилитки, а для всяких gnuplot/origin/...
30.05.2018: за вчера и сегодня продвинуто содержимое
LoadPlot()
:
timestamps_ring
.
histring_len
по умолчанию выставлено в
86400 вместо 0.
31.05.2018@дома: а ведь можно строку "Time(s-01.01.1970)" использовать как сигнатуру -- по ней опознавать файлы "нашего" формата!
01.06.2018: кстати, в лог-файлах от v2'шного Cdr формат ровно такой же и первая строка тоже точно такая же.
02.06.2018: вчера и сегодня:
AddGroupingChan()
-- парсинг спецификации (которая
обычно из argv[n]) и добавление канала к группировке.
ActivatePlotRow()
-- добавление N-го канала группировки
к объекту-histplot.
04.06.2018@утро-дорога-на-работу-около-ИПА: насчёт "как указывать метку и диапазоны отображения": а использовать символ ',' в качестве префикса и разделителя.
Например,
,label:abc,disprange:MIN-MAX,...
Некоторые замечания:
К тому же, и с синтаксисом консольных утилит такое почти не конфликтует: '=' там используется для указания, что это запись; а вот двоеточие парсится уже среди флагов, и парсинг не пересечётся никак.
...хотя -- всё это небесспорно (т.к. ':' удобно б использовать и как терминатор опции).
AddGroupingChan()
'ом, то синтаксис
будет универсален.
04.06.2018: расширяем успех.
MotifKnobs_UpdateHistplotGraph()
-- чтоб учлось количество
точек в историях: изначально-то, при добавлении --
MotifKnobs_AddToHistplot()
-- их по 0 штук, поэтому скроллбар
"не работал".
histring_start
--
при каждом чтении смещая его, тем самым де-факто читая задом наперёд.
Но это извратно и неудобно; к тому же, "timestamps_ring_start" нету -- там подобная фигня не предусмотрена.
Сделано.
06.06.2018:
А от histplot-wide параметров как отличать? По наличию в списке оных?
14.06.2018: поскольку косяк в MotifKnobs_histplot.c найден и исправлен, можно вернуться сюда.
о-PSP'шиваем.
globopts_t
, экземпляр его globopts
и
соответствующая text2globopts[]
.
globopts
.
16.06.2018: сделана возможность указания параметров канала -- метки и диапазона отображения.
while(1)
,
индивидуально-пошагово:
Следствие:
label=123,disprange=-10-+70:chanref
и
label=123:disprange=-10-+70:chanref
эквивалентны.
label=123,disprange=-10-+70,chanref
не прокатит: вторая ',' будет опознана и пропущена PSP, он
попытается парсить дальше (терминатора-то нету!) и ругнётся "Unrecognized
parameter 'chanref'".
RangePluginParser()
.
k->u.k.params
в количестве
DATAKNOB_NUM_STD_PARAMS
штук, и границы диапазона кладутся в
ячейки DATAKNOB_PARAM_DISP_MIN
и
DATAKNOB_PARAM_DISP_MAX
.
В общем, всё это РАБОТАЕТ!!!
Итак, работу, начатую еще 10-08-2015, можно считать завершённой.
16.06.2018: не хватает help'а -- поддержки ключика
-h
. А то синтаксис уже пренавороченный, а описания нет совсем.
17.06.2018: увы, не всё так радужно. Почему-то загрузка из файла работает кривовато.
18.06.2018: разобрались:
ppf4td_get_int()
: он воспринимает
числа вида "068" (миллисекунды) как восьмеричное (начинается с
'0'), а в оных никакой '8' быть не может.
Труды праведные:
ppf4td_get_int()
подпилен (см. в его разделе за сегодня),
и в вызовах указывается defbase=10.
MotifKnobs_SaveHistplotData()
.
19.06.2018: продолжение трудов:
k->label
, а в
k->ident
.
Причина в том, что MotifKnobs_SaveHistplotData()
сохраняет в
файл именно ident, а для показывания в метке в окне plot'а годится не только
label, но и (если последний пуст) ident.
Делаем:
AddGroupingChan()
аллокирование
параметров переделано из условного (если disprange указан) на безусловное.
Поскольку вначале делается bzero(), то по умолчанию границы будут [0.0,0.0].
20.01.2019: а вот и нифига! Подробнее см. ниже раздельчик за сегодня.
А у NAN есть свойство, что оно возвращает false при любом
"упорядочивающем" сравнении с ним -- например, и "NaN<=0
", и
"NaN>=0
" вернут false.
Поэтому НЕЛЬЗЯ искать минимум и максимум стандартным алгоритмом
-- т.е., беря в качестве начальных значений 0-й элемент, и потом исча значения меньше и больше его. Т.к. даже 0-й может оказаться NaN'ом и отравить весь поиск.for (minv = maxv = a[0], x = 1; x > count; x++) { if (minval > a[x]) minval = a[x]; if (maxval < a[x]) maxval = a[x]; }
Кроме того, при сравнениях элементы NaN игнорируются. Это как бы не очень обязательно -- устройство сравнений таково, что NaN'ы через них не пройдут (вследствие вышеупомянутого false при сравнениях), но так спокойнее.
Работает.
23.06.2018: и снова хренушки! Загруженный график почему-то показывается горизонтально-зеркально.
Лишнее зеркаленье? Неправильно высчитывается координата в буфере? Или что еще?
24.06.2018: похоже, неправильно понял, как данные должны лежать в памяти. Cdr... ...
CdrShiftHistory()
:
histring_len
.
histring_start
.» (bigfile-0001.html от
16.12.2004.)
k->u.k.histring_start = (k->u.k.histring_start + 1) % k->u.k.histring_len;
k->u.k.histring[(k->u.k.histring_start + k->u.k.histring_used - 1) % k->u.k.histring_len] = k->u.k.curv;
(при незаполненности буфера histring_used
инкрементируется ранее -- вместо сдвига
histring_start).
Вероятно, различается способ хранения/индексирования данных и времён.
timestamps_ring
не используют.
t
(и t_index
)
используется в 2 разных смыслах:
Вот и вышла путаница -- видимо, в обоих случаях используется одинаковая арифметика, а должна зеркальная.
...т.е., опять та же проблема -- в некорректном выборе системы координат (только тут их несколько перемешано).
А решение выглядит очевидным: надо РАЗДЕЛИТЬ имена:
26.06.2018: делаем.
make_time_str()
-- там это именно "расстояние от
правого края", т.е., возраст; она и введена-то была как подмастерье для
подписывания времени в координате x_index.
DrawAxis()
и DrawGraph()
-- вообще всё
поназапутано:
Это в DrawGraph()
при рисовании вертикальных линий сетки.
horz_offset
.
И всё это -- в переменной "t
".
timestamps_ring[]
, а потом можно и переименования исполнить.
LoadPlot()
убрано.
timestamps_ring_used - 1 - t
И -- ура! -- всё стало хорошо, как положено!
make_time_str()
-- t
переименовано в
age
.
MotifKnobs_SaveHistplotData()
-- аналогично.
DrawGraph()
разделено: для вертикальных линий сетки
scr_age
("экранный возраст"), для данных -- просто
age
.
Кстати: оказывается, тут есть зародыш возможности не только
сжимать график по горизонтали, но и растягивать. Это определяется наличием
(и использованием, пусть и =1) x_mul
в дополнение к
x_div
. Эта парочка функционирует аналогично Xh_plot'овым
x_xpnd
/x_cmpr
. Появилось оно, судя по
bigfile-0001.html, еще 14-07-2011. Но ТОЛЬКО в отрисовке данных:
ни в отрисовке осей, ни в каких-либо еще расчётах (работа со скроллбаром,
обработка мыши, "репер") умножения нет -- только деление.
DrawAxis()
заменено на age
.
Правда, смысл используемых там формул от меня ускользает...
...хотя работает всё вроде как положено -- проверил со сменой масштаба.
P.S. Ну вот точно -- НЕ МОЁ это, графики рисовать. А сервер и с железом работать -- моё (даже ЕманоФедя хвалит, что даже конфиг сервера удобно устроен).
Можно сделать как в консольных утилитах -- префикс %DPYFMT: (с той разницей, что поддерживать только вещественные).
В районе обеда: ну да, при проектировании программы 12-10-2016 именно такое и предполагалось.
26.05.2018@утро-конференц-зал-раб.сов.по-C-tau: а тут будет некоторая засада: ведь формат может быть и int, а не float/double. Но в таком случае надо бы как-нить проверять и форсить double (например, тупо %8.3f).
26.05.2018: сделано.
main()
, но
потом переедет в отдельную функцию -- когда будем делать парсинг строки
заголовков.
И делается проверка, что если указан int-формат, то об этом выводится отдельное ругательство.
16.06.2018: давно уже убедились, что всё работает -- "done".
RESCALE_VALUE()
, но
потом дело оказалось в арифметике геометрии) было замечено, что почему-то
одно из значений подсвечивается состоянием RED.
19.01.2019@дома: да, ситуация ещё раз воспроизведена. Для воспроизведения надо:
./sbin/cxsd -dc configs/cxsd.conf -f <(echo dev a noop w3i -) :1
~/work/4cx/src/programs/xmclients/histplot localhost:1.a.{0,1,2}
20.01.2019@дома: разобрался в причине. Она, к счастью, оказалась в самом histplot.c (а не в Cdr/datatree/...).
Суть в том, что:
DATAKNOB_NUM_STD_PARAMS
,
DATAKNOB_PARAM_DISP_MIN
и DATAKNOB_PARAM_DISP_MAX
.
DATAKNOB_PARAM_YELW_MIN
и
DATAKNOB_PARAM_YELW_MAX
) -- просто мусор, который конкретно у
верхнего канала оказался таким, что значение 0 выходило за пределы "жёлтого"
диапазона.
Проблему решило добавление bzero()
сразу после
malloc()
.
CDA_DATAREF_OPT_ON_UPDATE
"; сейчас это просто НЕГДЕ указать,
shame on me...
А вот как бы это указывать -- фиг поймёшь:
cvt2ref()
.
PK_FILE
.
behaviour = DATAKNOB_B_ON_UPDATE
всем принудительно?
19.05.2023: а не устроит ли просто указывать суффикс "@u", который поддерживается cda_d_cx'ом с начала CXv4?
Попробовал, запустив сервер с периодом 1с и 1 каналом, на него натравив histplot с histperiod=0.1 плюс for-цикл в shell'е, пишущий в канал значения от -100 до +100 с паузой 0.1с -- фиг, не помогло.
И понятно, почему: суффикс-то влияет лишь на поведение cda_d_cx, а к ловле ВСЕХ обновлений Cdr'ом он, очевидно, НЕ приводит.
...но, похоже, ровно так же себя ведёт и
DATAKNOB_B_ON_UPDATE
: в Cdr_treeproc.c нет НИКАКИХ
следов его влияния на что-либо, кроме взведения при регистрации канала флага
CDA_DATAREF_OPT_ON_UPDATE
. И аж с 25-05-2016 -- семь лет! --
ничего там не делалось, а о "ни CDA_REF_EVMASK_UPDATE не запрашивается,
ни реакции на него пока не сделано" ещё тогда же и записано.
Откуда вывод: даже взведение флажка в behaviour
-- как
задумывалось изначально -- в НЫНЕШНЕЙ ситуации проблему бы не решило.
20.05.2023: учитывая наконец-то дореализованность
DATAKNOB_B_ON_UPDATE
в Cdr -- делаем и тут, по варианту
"выставлять behaviour = DATAKNOB_B_ON_UPDATE
всем
принудительно".
Проверено -- работает.
Была также мысль «сделать ещё, чтоб можно было это поведение
отключать индивидуально, указанием префикса "@~", который для этого
пришлось бы парсить в cvt2ref()
», но решил забить на
это и не переусложнять: в конце концов, смысл histplot'а -- отображать
данные на графиках наиболее аккуратно, и в экономии на обновлениях тут
глубокого смысла нет (каналов не более 16 штук и затраты на отображение --
на ГРАФИКЕ, а не на ручках).
На этом "done".
21.05.2023: всё-таки решено сделать возможность по-циклового обновления -- чтоб можно было добиваться того же эффекта, что по умолчанию в Chl-клиентах.
Для этого добавлена пара параметров командной строки --
"on_update
" (по умолчанию) и "on_cycle
", влияющих
на значение свежевведённого флага globopts.on_update
, который,
в свою очередь, теперь является условием для взведения флажка в
behaviour
.
Прикол в том, что эта штука может использоваться для ИНДИВИДУАЛЬНОГО (по-канального) указания режима обновления: указанный параметр влияет на все каналы, указываемые ПОСЛЕ него (поэтому если указать в конце -- то вообще никакого эффекта; я при проверке вначале никак не мог понять, чё это оно не работает -- зато потом проверил указание одного и того же канала 2 раза (поскольку другой набор флагов даёт другой dataref, то это прокатывает) с разными режимами обновления -- и показывался один лесенкой, а второй линейно!).
CX_US_C
и
CX_SS_C
.
04.07.2009: Чтобы избавиться от этой проблемы, а
заодно и от различия между misclib.h в cx/ и
4cx/, вытащил все определения CX_*_C
и
CX_*_S
в отдельные файлы misc_sepchars.h.
(Кстати, в cx/ были неправильные/неадекватные определения --
копия из 4cx/, еще с 14-03-2006; тогда это было сделано intentionally,
но оно непродуктивно. Так что оставил там ТОЛЬКО
CX_US_C='\v'
и CX_SS_C='\f'
.)
int
, а должны бы
ssize_t
, т.к. на 64-битных платформах это не одно и то же.
Потенциальных минусов от перехода на ssize_t не видно (на 32-битных платформах, вроде ppc860, ничего не изменится, т.к. ssize_t==int), а плюс -- в том, что будет более правильно и появится (гипотетическая) поддержка В/В более 2Гб за порцию.
08.04.2015: сделано.
strzcpy()
:
safe_strncpy()
в lib/util.c пакета net-tools
(конкретно net-tools-2.0-0.17.20131004git.el7.src.rpm).
Она организована чуть иначе:
против/* Like strncpy but make sure the resulting string is always 0 terminated. */ char *safe_strncpy(char *dst, const char *src, size_t size) { dst[size-1] = '\0'; return strncpy(dst,src,size-1); }
static inline char *strzcpy(char *dest, const char *src, size_t n) { char *result; result = strncpy(dest, src, n); dest[n - 1] = '\0'; return result; }
Т.е., она оптимальнее (и элегантнее) -- никаких лишних сохранений.
Просто она указывает strncpy()
размер-1, тем самым гарантируя,
что последний байт тронут не будет.
SleepBySelect()
и
str*time*()
вытащены в отдельные файлы
misc_sleepbyselect.h и misc_iso8601.h.
Смысл -- сделать эти модулёчки "самодостаточными" и годными для опубликования на GutHub и прочих подобных местах.
21.06.2007: сделано -- в misclib.h
сменил определения MULTISTRING_SEPARATOR
и
MULTISTRING_OPTION_SEPARATOR
на соответствующие ссылки на
CX_??_C; заодно переделал их из #define'ов в enum. Что неприятно --
пришлось ради них за-#include'ить "cx.h".
Заодно пришлось и misc_multistrings.c потрогать -- заменил там в комментарии '\v' на '\t'.
psp_free()
.
(Понадобилось для DestroyKnob()
.)
19.06.2007: сделал. Как принято, она после
вызова free()
указатель сбрасывает в NULL.
И с-back-port'ил в work/cx/ тоже.
19.08.2007: да, а что же с плагин-парсерами? Они-то ведь тоже могут что-то аллокировать! В принципе, можно использовать то, что ПЕРЕД аллокированием надо освобождать то, что уже есть -- как это делает MSTRING, а парсенье строки NULL не приводит ни к чему. Так что -- в качестве "освобождателя" можно вызывать парсер с str=NULL.
20.08.2007: а еще ведь вложенные PSP есть!
В принципе-то правильным было бы просто пройтись по всем параметрам,
делая PspSetParameter(item, rec, NULL, 0)
, НО: при init==0
НАДО что-то передавать в качестве vp.
В конечном итоге сделано так: для PSP_T_MSTRING и PSP_T_PLUGIN
вызывается приведенная выше строка, а для PSP_T_PSP -- делается
psp_free()
с вложенной таблицей.
19.03.2014: вот то указание "vp:=NULL" для
MSTRING было вообще-то проблемным: там ведь соглашение, что передаётся
указатель-на-указатель, и на !=NULL проверяется уже *vp (SSRV), а не
vp. Так что по-хорошему надо было делать некий void
*p=NULL
и передавать &p
.
Это проявилось на днях на pzframe_gw_drv -- при ошибке парсинга при
вызове psp_free()
оно SIGSEGV'илось. И загадка, почему
глюк не вылез раньше -- ведь PSP_P_MSTRING()
'ов
используется предостаточно, и уж с кто-то из них наверняка должен был
попасть под psp_free()
(но конкретно в ДРАЙВЕРЕ это было
первое использование).
Исправлено дополнительной проверкой на vp!=NULL
.
(Может, это и не самое элегантное решение, но сам косяк явно возник в течение времени, как следствие нескольких смен/дополнений концепции, в сочетании давших непредвиденный изначально результат. Так что сейчас что-то серьёзно рыть в глубину -- очень вломы.)
И, кстати, с PSP_T_PLUGIN
тоже что-то не
слава богу: оно просто не будет отрабатываться, поскольку psp_free()
вызывает PspSetParameter(,,,init:=0)
, а он для PLUGIN'ов
при этом ничего не делает.
А пока что, похоже, это НЕ реализовано.
11.03.2014: сделано 02-01-2014 (см. bigfile-0001.html).
Непосредственная причина мыслей: у нас в CAN-драйверах очень криво сделана индивидуальная+групповая инициализация скоростей: "общая" указывается как [0], и размножаются в прочие, если те ==0. Но тут куча проблем/невозможностей: например, невозможно поставить все скорости в некое одно значение, а одну конкретную в =0.
"Правильным" же вариантом была бы иметь возможность указать в paramdescr'е, что при установке надо взвести некий флажок (ОТДЕЛЬНЫЙ от значения!).
Но теперь такая модификация уже едва ли возможна -- шибко уж велика installed base софта, не напрямую/статически линкующегося с PSP, а динамически; конкретно драйверы.
23.02.2017: теоретически, есть никак не используемые
поля rsrvd1
и rsrvd2
.
Можно было бы в rsrvd1 указывать offset, а в rsrvd2 -- размер в байтах (0 -- нет, 1,2,4 -- sizeof флага).
RET_ERR()
-- существующем,
судя по архивам, с самого начала -- есть snprintf()
, но
отсутствует принудительное терминирование строки.
Замечено было вчера, в присланном ЧеблоПашей
(Message-ID:00ef07ec-4a9c-4043-9de2-e39030c0e49c@inp.nsk.su) логе сборки
каким-то более новым GCC, имеющим ключ "-Wformat-truncation=
"
(фиг знает, почему включившейся -- в man'е от gcc-8.5.0 на
Star/Oracle-RHEL-8.10 не сказано).
В результате лог компиляции paramstr_parser.c весь
завален этими предупреждениями. Поправка через 10 минут разбирательств: в
результате не "отсутствия терминирования", а лишь самого наличия
snprintf()
.
19.03.2025: так-то "принудительно терминировать" НЕ НАДО
-- это делает сам snprintf()
, в отличие от
strncpy()
.
Но вот что делать с замусориванием лога сборки -- непонятно...
sl_???_fd*()
по дескрипторам на адресацию по неким
внутренним handle'ам. (В соответствии с соображениями из раздела "О
работе с памятью" в bigfile-0001.html.)
08.11.2012: да, и даже больше:
sl_timeout_t
trec'ов в пользу аллокируемых cxscheduler'ом tid'ов.
cxsd_events/[ch]
, с этими дурацкими trec'ами?!
Вследствие этой негибкости мышления и догматичности получена туча
неудобств, вытекающих из получившейся кривой архитектуры...
По факту это даст как бы "cxscheduler 2.0", не шибко-то совместимый по API. Но надо делать сейчас -- ДО реализации нового сервера (точнее, унифицированной lib/srv/, включающей rem), чтоб не тащить в будущее старые косяки.
08.11.2012: некоторые соображения по реализации внутренней таблицы таймаутов:
sl_timeout_t
: when, cb, privptr.
08.11.2012: косвенную же адресацию дескрипторов в
клиентах можно подготовить безболезненно и заранее: сделать, чтоб
sl_add_fd()
уже сейчас возвращала handle, который пока
сделать равным fd.
...главная засада в том, что это просочится и в API драйверов -- там
тоже придётся вести себя аналогично (для SetDevFDMask()
и
DeregisterDevFD()
). 05.12.2012:
замечание: только для
v4, для v2 пока оставим как есть -- API отличаются (v4: DevFD; v2:
Dev{Rd,Wr,ExFD}, и нет Mask). 09.01.2013: а вот и нет -- в v2 уже тоже
перетащено.
20.11.2012: реализовываем:
Вроде сделано -- и как теперь проверять будем? :-)
26.11.2012: по результатам взапинывания в
"клиентов" стало ясно, что fd_proc'ам недостаточно передавать fdh'и --
нужны и сами fd. (Пока выкрутились, но на будущее лучше
исправить ситуацию, пока не поздно и API еще в точке изменения, а не
пошёл в широкое распространение. Кстати,
XtInputCallbackProc
'ам также передаётся сам дескриптор
(правда, дебильно -- по указателю, как int
*source).)
Так что параметр fd
добавлен, вторым, после fdh.
03.12.2012: еще энное время назад проверено, что v2-cxlib/ncxlib'ные клиенты на '2' работают.
Сейчас проверен также canserver (на libremsrv) -- тоже работает.
Пора делать новую версию основной?
04.12.2012: да, старая версия заменена новой (пока только в v2, но НЕ в 4cx/).
08.01.2013: а вот теперь переходим на новую версию и в 4cx/.
09.01.2013: 08:49: готово :-)
02.05.2013: поскольку уже несколько месяцев работает, и проблем не замечено, то ставим "done".
И его предшественник -- libevent, "an event notification library".
Делаем всё в дополнительных файлах с суффиксом "3", работы ведутся в 4cx/, дабы не мешать живой системе управления.
02.05.2013: по параметрам callback'ов получается
очень удобно: достаточно добавить в начало списков парочку
"uniq
, privptr1
" -- и выйдет полная
совместимость с нынешним драйверным API: uniq<->devid,
privptr1<->devptr. (Это следствие "уразумнивания" списков
параметров в ноябре-декабре; с fdiolib аналогично).
sl_do_cleanup()
, fdio_do_cleanup()
.
30.05.2013: 4cx/ переведена на новую версию -- и файлы переименованы (убрано "3"), и все "юзеры" обновлены на новый API. Вроде деградации незаметно.
Еще через несколько часов: и cx+v2hw тоже переведены. По крайней мере клиенты работают нормально.
Добавлены вызовы fdio_do_cleanup()
и
sl_do_cleanup()
во все fdiolib+cxscheduler-based среды
исполнения -- это cx/'ная пара
zzz/cxsd_drvmgr.c::stop_dev()
и
rem/remsrv_drvmgr.c::FreeDevID()
; в 4cx/
же "убиение" устройства нигде реализовано не было.
15.09.2013: проверено достаточно широко -- работает новый вариант, всё уж который месяц функционирует на нём, так что "done".
sl_set_uniq_checker()
.
15.09.2013: в Xh_cxscheduler и Qcxscheduler также скопировано.
should_break
-- сразу после прохода по
таймаутам, чтоб они тоже могли б прерывать цикл.
18.09.2013: непосредственно занадобилось при внедрении clientside handshake-таймаутов в cxlib, иначе в консольных утилитах оно не работало.
А вообще сие было серьёзной идеологической недодумкой с самого начала.
В cxscheduler'е это сделать можно, в отличии от его клиентов (проверено вчера на cxsd).
Замечание: это на настоящем, а что с Xh_- и Q-вариантами -- неясно.
15.06.2014: c Xh_- и Q-вариантами всё просто: там ведь внутри заказываются не конкретные времена вызова (нету такой возможности), а ИНТЕРВАЛЫ. Соблюдение же интервалов -- личная проблема подстилающих тулкитов.
16.06.2014: да, сделано обнаружение, и программа
может указать вызываемость через
sl_set_on_timeback_proc()
.
23.09.2014: (реально мысль уже давно) по-хорошему, надо бы эту штуку делать не единственным hook'ом, а callback-list'ом.
...и тут вспоминается libev, где ВСЕ события доставляются унифицированно.
(Может, и таймауты тоже, но тут не так ясно.)
sl_break()
отрабатывалось сразу, а не по
завершении полных цифлок опроса fd/tout.
Это легко -- доп.условие в оба цикла (fd/tout).
16.01.2022: похоже, опять возникает эта же потребность -- у ЕманоФеди что-то там глючит, видимо, без этого условия.
Ну добавлено -- делов-то...
Вопрос только, КАК это спасёт? Так ли надо делать проверку -- ведь, казалось бы, после этого она всё равно попадёт в опрос дескрипторов?
А, да, так -- чтоб проверить fd ДО таймаута.
#define
-макросами
с именами CXSCHEDULER_HOOK_*
, вызываемыми при
#ifdef
.
Но описано оно всё было не тут, а в разделе по img878 (т.к. делалось в его интересах).
И в CXv2 оно не было скопировано -- там так и остался вариант за 16-06-2014.
sl_set_fd_mask()
при занулении битиков удаляет их не только из
соответствующих наборов
rfds
/wfds
/efds
, но и из мспользуемых
с select()
'ом
sel_rfds
/sel_wfds
/sel_efds
.
(Подробнее см. в разделе по fdiolib за сегодня.)
12.02.2022: конкретно:
FD_CLR()
'ы добавлены в sl_set_fd_mask()
, в
ветки с Remove*fds()
.
sl_del_fd()
они выкомментированы, за ненадобностью.
Самое странное в этом то, что сама потребность удалять из
sel_*fds
сразу же была осознана давным-давно (хбз когда), но
сделано это почему-то не было.
Но тогда вопрос, как "удалять дескрипторы из рабочего набора" посреди цикла, чтоб они далее не рассматривались.
@подвал-Кузнецова, ~12:00:
(то, что делает sl_del_fd()
через
sl_set_fd_mask()
, где выполняется
FD_CLR(fd, >sel_Xfds);
.)
...ну и вообще какие ещё колёса могут вылезти при таком изменении?
03.06.2024@планёрка, ~10:10: оказывается, в CentOS-7.3 на тему "kqueue" ни apropos ничего не показывает, ни в /usr/include/**/*.h ничего не находится (и в "info libc" тоже).
06.06.2024@вечер, 21:30, завершая записи по v3h_a40d16+vdev: блин, "kqueue" -- это BSD, а в Linux -- "epoll". И в CentOS-7.3 он уже есть (т.к. присутствует с ядра 2.5.44, а тут 3.10).
29.03.2006: highlights:
serverinfo_t.bigcinfo
).
Кстати, циклить по массиву не понадобилось никогда вообще -- поскольку cxscheduler нам всегда передает handle ячейки, связанной с дескриптором.
И еще: поскольку адресация по файловым дескрипторам теперь
отсутствует, то махинации с "clnidx[]
" за 08-06-2005 тож
нафиг не нужны.
(К тому же, это в connlib'е библиотека знала о структуре заголовка, а тут-то -- нет.)
fdio_io_cb()
и fdio_send()
сами
ничего не делают, а вызывают соответствующий "метод" --
{Stream,Dgram}{ReadyForRead,ReadyForWrite,Send}()
.
(Напоминает то, как ядро обходится с разными протоколами (IPv*/UNIX/IPX/..., STREAM/DGRAM) и файловыми системами.)
fdtype
) в начале fdio_register_fd()
присутствует, но пока она слабовата. В частности, пока толком не
определено: 1) как именно описывать STREAM-пакеты фиксированной
длины, а также 2) как бы поддерживать DGRAM-пакеты БЕЗ поля длины
-- ведь там длина может браться просто из размера датаграммы.
GrowBuf()
--
там уже полпакета отправлено, а остальное может обломиться из-за
нехватки памяти.
Впрочем, нехватка памяти -- в любом случае столь серьезная проблема, что с чистой совестью можно убивать соединение.
Вводить понятия "начало и конец транзакции"? Чтобы и пользоваться _sysbuf'ом, не заводя еще один лишний в программе (как бы этакий zero-copy), и чтобы fdiolib видел все сегменты как единый пакет?
Вечером: не, не так -- не "lock/unlock", а "отвести столько-то места в буфере" и "_may_send()". Поскольку multithreading не поддерживается, то от момента отведения места в буфере и до вызова "_may_send()" программа будет иметь монопольный доступ к тому месту в буфере. Для датаграмм -- аналогично. Но делать это буду уже когда понадобится, а не сейчас.
А вообще, в голове пульсирует мысль -- Чего ждал?!?!?! Это же было так просто! Блин, у меня создание fdiolib числилось (с момента появления этой идеи -- последние года три, наверное... с 03-12-2002) очень-очень сложной, почти неподъемной задачей, которая займет море времени. А реально-то -- все "сложности" были в вычитывании поля "pktlen" указанного размера и с указанным endianness. Все же остальное -- уже давным-давно было отлажено в connlib!!!
14.01.2007: начинаем испытывать, на canserver'е.
В fdio_lasterr()
проверкой была не надлежащая
DECODE_AND_LAZYCHECK()
, а DECODE_AND_CHECK()
.
Исправил.
15.01.2007: проверяем дальше:
fdio_register_fd()
умудрился забыть break после
альтернативы FDIO_{STREAM,LISTEN,DGRAM} -- в результате оно пролетало в
default=>errno=EINVAL... Исправил.
fdio_send()
с _CHECK()
на _LAZYCHECK(). Резон -- чтоб при протокольных ошибках (типа
INPKT2BIG) можно было перед застреливанием отправить ответ.
Так что -- добавил параметры inpkt,inpktsize и выкинул fdio_get_req().
fdinfo_t
оставался. Выкинут.
17.01.2007:
18.01.2007:
Так что -- вставил уставку в SIG_IGN в cm5307_dbody.c, i_cm5307.c, rrund.c и canserver.c.
14.04.2007:
Введено дополнительное поле в fdinfo_t
--
maxoutbufsize
(def=0), уставлять которое можно функцией
fdio_setmaxsbuf()
("sbuf" -- "send buf"). Значение 0
отключает проверку. 29.09.2009: переименована в
fdio_set_maxsbuf()
-- для унификации с остальными
модификаторами-параметров.
(Технология несколько отличается от connlib'овской, где была пара maxpktsize и maxsndpkts, и буфер не рос выше их произведения. Просто maxoutbufsize выглядит логичнее -- пакеты-то могут быть разными.)
31.05.2007: поскольку давно работает, да еще и с разными вариантами cxscheduler'а, то "done".
04.10.2014@Толмачёво-гуляние-по-террасе-гейтов-перед-отлётом: (реально мысли бродили раньше; в связи с предстоящей необходимостью делать "оракула" для UDP-резолвинга имён) с UDP есть и другая проблема -- соединения могут быть не-connected, и надо будет:
Собственно:
sendto()
вместо write()
.
Для реализации нынешней более простой потребности (a) достаточно варианта (2), а для более общего случая (b) понадобится (1).
В любом случае, адрес последнего полученного пакета надо хранить в
fdinfo_t
.
(Да, менять в очередной раз fdio_ntfr_t
-- не
вариант. Хотя, возможно, и стоило изначально предусмотреть "call_info",
но по факту оно нужно очень мало кому, а код из-за него стал бы
хужечитабельнее.)
05.10.2014@Снежинск-гуляние-под-моросящим-дождиком-вдоль-озера: при таком количестве РАЗНООБРАЗНЫХ методов -- не пора ли перейти с множества if()/switch(fr->fdtype) на архитектуру с vmt?
06.10.2014@Снежинск-каземат-11: по здравому размышлению после изучения содержимого этих самых switch() -- неа. Не стоит.
08.10.2014@Снежинск-каземат-11: делаем -- варианты (1) и (2) вместе.
fdinfo_t
добавлены поля last_addr
и
last_addr_len
-- предполагается, что их должна заполнять
DgramReadyForRead()
.
fdio_last_src_addr()
: возвращает их содержимое, примерно
в соответствии с правилами, описанными в man-странице по
getpeername()
.
Работает ТОЛЬКО с FDIO_DGRAM, иначе -1/EINVAL.
fdio_reply()
: если last_addr_len!=0, то вызывает
fdio_send_to()
, иначе обычный fdio_send()
.
fdio_send_to()
: для FDIO_DGRAM дёргает свежевведённую
DgramSendTo()
, иначе -- -1/EINVAL.
А теперь собственно поддержка UDP -- т.е., наполняем
Dgram*()
.
dgraminfo_t
, содержит
размер и информацию об адресе.
fdio_register_fd()
в смысле аллокирования приёмного
буфера DGRAM-соединения приравнены к STRING -- отводится место сразу по
максимуму.
(Кстати, некрасиво, что буфер отправки аллокируется тем же объёмом.)
DgramReadyForRead()
вроде сделана, творческим
копированием со Stream-варианта.
Но остаются неопределённости по обработке ошибок: нет ли каких случаев, когда ошибка есть, но нефатальная (например, от прошлой отправки)?
DgramSend()
просто отфутболивает к
DgramSendTo()
, to=NULL,tolen=0.
Некоторые мысли/замечания по ходу дела:
09.10.2014@Снежинск-каземат-11: продолжаем:
udp_sendmsg()
:
send*()
про ENOBUFS сказано
"Normally, this does not occur in Linux. Packets are just silently
dropped when a device queue overflows.".
15.10.2014: далее:
DgramReadyForWrite()
.
DgramSendTo()
. Весьма неопределённо-странно с
обработкой ошибок и осознанием, когда же складывать в буфер: если
r>0 -- то буферизуем, иначе считаем, что отправили сразу.
Итак, можно уже устраивать первые проверки.
Но гложет вопрос -- насколько вообще осмысленно применять к UDP ту же логику буферизованной отправки, что к TCP? По результатам проверок и посмотрим.
25.12.2017: при запинывании UDP-резолвинга было
замечено, что в DgramReadyForRead()
нет НИКАКИХ проверок на
размер прилетевшего пакета.
Делаем.
Только облом по ней не считается фатальным, т.к. UDP -- connectionless, и может поймать какой-то мусор, посланный фиг знает кем, так что протокольная ошибка не является поводом застрелиться (иначе это получился бы прекрасный remote DoS).
Смысл -- возможна ситуация, когда отправляется пакет большого объема (не лезущий в системный буфер), но само соединение после этого уже не требуется и программа-клиент хочет про него забыть (как делает Apache). Просто делать "send();deregister();close()" некорректно -- тогда не-успевшие-отправиться данные пропадут.
Надлежит также иметь и некий "период ожидания" -- если, например, за минуту данные так и не ушли, то просто прибиваем соединение.
12.01.2007: есть два варианта реализации, оба не вполне удовлетворительные:
Для этого в fdiolib почти ничего добавлять не требуется -- только вызов с резоном "все отправлено". Но тут есть узкое место -- данные-то могут сразу уйти все, без буферизации -- так что в таком случае также надо как-то уведомлять программу; возможно -- дать ей функцию-проверку "все_отослано()".
Недостаток же -- все сваливается на клиента.
Как реализовывать:
DECODE_AND_CHECK()
--
это корректно, поскольку сама fdiolib никакие свои API-функции сама не
вызывает (по крайней мере, сейчас));
close(fr->fd)
;
Недостатки:
ReadyForWrite()
-- сразу убивать, а не
close_because()
; не забывать оставлять маску только 2, без
1; ...;
Короче -- хотя 2-й вариант и выглядит посложнее, но именно он правилен, и делать надо будет именно по нему.
02.10.2009: создаём прототип-"прообраз" такой
функции -- fdio_deregister_flush()
. Ей передаётся
дополнительный параметр max_wait_secs
, указывающий
максимальное время ожидания отправки, по истечению которого дескриптор
будет полностью закрыт автоматически.
Пока эта функция просто вызывает обычный
fdio_deregister()
.
(Непосредственно оно понадобилось для cxsd_fe_cx.c -- чтобы закрывать соединения gracefully.)
P.S. Надо не забывать проверять, что если соединение в режиме
is_defunct
-- то НЕ надо пытаться что-то слать, а
надо закрывать сразу.
03.10.2009: еще замечание: и дескриптор-то закрывать придётся в таком случае самому fdiolib'у -- поскольку программа про него уже хочет забыть, а дескриптор нужен живым...
Пока что корректное самостоятельное закрытие вставлено в ветку
при-is_defunct
.
P.S. Вообще, конечно, это зело нехорошо, что в разных сценариях закрытия ответственность за close(fd) лежит на разных сторонах. И, вероятно, СТОИЛО бы сменить модель, и сделать так, чтобы ВСЕГДА закрытие делалось бы fdiolib'ом.
03.10.2009: и еще: сейчас при ПРОТОКОЛЬНЫХ ошибках сразу же делается "close_because()", а это нехорошо -- надо бы прибивать ТОЛЬКО ПРИЁМ, и оставлять возможность отправки и отложенного-закрытия, чтобы программа могла что-то вякнуть корреспонденту...
fdio_strreason()
?!
15.01.2007: уже сделан.
02.08.2011: угу, явно надо. Причём -- делается это тривиально.
Протокол делания:
watched[]
теперь переделаны на с 1
вместо былого 0.
find_free_watched_slot()
, ...
DECODE_AND_*CHECK()
теперь вместо
handle<0
стоит handle<=0
.
Осталось проверить.
07.02.2012@Снежинск-каземат-11: проверено еще примерно тогда же, летом, на sl_dbody -- поведение характЕрно изменилось, и это даже как-то там влияло/помогло с разборками.
20.01.2007: ввел, FDIO_OFFSET_OF()
и
FDIO_SIZEOF()
, просто слизав из paramstr_parser.h.
Так почему бы не сделать этакий cxscheduler-compatible адаптер под Xt?
Единственное что -- в линковке указывать при этом придется именно
fdiolib.o, поскольку в libuseful.a уже будут все
sl_*
.
14.05.2007: да, сделана такая штука в CXv2 --
модуль Xh_cxscheduler.c. Надо будет в свежей версии
переселить его куда-нибудь отдельно от Xh'а; вопрос только в
context
'е.
02.10.2009: замечание: "в линковке указывать fdiolib" не придется, поскольку именно cxscheduler.o и вынесен в отдельную библиотеку libcxscheduler.a, а Xh-вариант живёт в libXh_cxscheduler.a. Более того, на повестке дня стоят Qt_cxscheduler и Win32_cxscheduler :-).
На переселение отдельно от Xh'а пока забиваем -- нет потребителей
для такого решения. Если же понадобится -- то делается легко, вместо
зашитого использования context
'а создадим вызов
Xt_sl_set_context(XtAppContext*context_p)
, надлежащий быть
вызванным сразу в начале клиента, а саму библиотеку переименуем в
Xt_cxscheduler.
Засим -- наконец-то помечаем "done".
getsockopt(,SO_ERROR,)
работает "неправильно"
-- вместо считывания кода ошибки в указанную переменную оно просто
возвращает -1, а код "той" ошибки само уставляет в errno
.
Надо бы как-то это корректно обрабатывать...
16.12.2007: по ходу разбирательства искал в GoogleGroups -- и первым же оно выдало thread от Bill Fenner (того самого, который один из авторов книги, и цитируется в этом thread'е тот же самый Stevens) за июль 1996 " Non-blocking connect() and error return on Solaris?".
Напрашивающееся решение -- копировать полученную
sock_err
в errno
только если
getsockopt()>=0
. Плюс, по опыту Феннера, считать EPIPE
(всегда возвращаемую Solaris-2.4 по дурости; в 2.5 уже все окей)
за другую -- например, ETIMEDOUT. И -- на всякий случай -- перед
считыванием делать sock_err=errno=0
.
Сделано, и покамест считаем за "done".
14.06.2010: замечание -- ТОГДА трансляцию
EPIPE->ETIMEDOUT так и не сделал, добавлена только сейчас, в
#ifdef OS_SOLARIS
(хотя вряд ли она когда реально
пригодится).
usrptr
в
privptr
.
fdio_set_len_endian()
и
fdio_set_maxpktsize()
.
28.09.2009: обе понадобились для cxsd_fe_cx.
Смысл в том, чтобы (в cx_client.c) хотя бы ПЫТАТЬСЯ соблюдать последовательность/очередность -- после некоего события (например, установление соединения) сокет мог уже успеть быть закрыт с той стороны, с уведомлением о причине. Так вот -- при попытке ПРЯМОЙ отправки оно обломится по EPIPE сразу, и не получит никакого уведомления. А если делать отправку через буфер -- то был бы шанс вначале прочитать присланное, и НЕ пытаться писать в уже закрытый той стороной сокет.
Вопрос лишь в том, что fdio_io_cb()
вначале проверяет
SL_WR
, и хбз, будет ли закрытый-с-той-стороны дескриптор
готов на запись или нет...
А назвать функцию fdio_send_buffered()
?
03.10.2009: авотхрен -- проверил, при помощи
check_fd_state()
-- такой, вроде как уже закрытый с той
стороны дескриптор, всё равно считается за WRITABLE!!!
А fdio_io_cb()
вначале проверяет SL_WR
,
так что до чтения дело и не дойдёт... (И такой порядок проверок там
сделан с умыслом -- чтобы сначала избавляться от накопленных в буфере
отправки пакетов, до того, как реакция на прочитанное добавит еще
данных для отправки...)
В общем -- пока как-то не видно разумного решения...
19.09.2013: сегодня возникла аналогичная, хотя и сильно отличающаяся потребность: мочь "блокировать" физическую отправку в сокет, чтоб оно лишь складывало в _sysbuf, но не отправляло бы, пока не будет сказано "пачка закончилась, можно слать все накопившееся" (занадобилось для remcxsd@ppc860 -- там syscall'ы очень дорогие, их надо б экономить).
Делаем:
send_locked
.
StreamWrite()
не делает ни проверки
"можно ль сейчас отправить скопившееся в буфере", ни попытки отправить
даденное сразу (если буфер пуст), а только складывает в буфер.
fdio_lock_send()
и
fdio_unlock_send()
. Они там "плагинизированные" -- сейчас
реализации есть только для FDIO_STREAM, а остальные возвращают EINVAL
(впрочем, для DGRAM сие всё равно слабоосмысленно).
StreamLockSend()
делает просто
send_locked++, ...
StreamUnlockSend()
содержит мозги: если
результат --send_locked ==0, то делает проверку, аналогичную начальной
в StreamSend()
.
Проверено на canserver'е -- да, вроде бы работает, и CPU занимает меньше.
16.09.2012: обсуждение:
fdio_io_cb()
стоит проверка и на SL_WR, то ПРЯМОЛИНЕЙНО в
нём циклить нельзя (хотя и напрашивается).
Похоже, надо вместо прямого вызова
sl_set_fd_mask()
завести свой, который также будет
сохранять значение во внутреннее поле, и при цикленьи проверять
готовность дескриптора на то (RD, WR), что сейчас в этом поле указано.
17.06.2013: неа, всё намного проще и элегантнее!
- Циклить -- вообще прямо в
StreamReadyForWrite()
, тогда ничего не требуется от sl*().- И собственно цикленье -- БЕЗ проверки
check_fd_state()
'ом, а просто сразу пытаться читатьread()
'ом, и если нечего -- то вернётся -1/EWOULDBLOCK.Просто проверять -- результат
IsReadError()
-- надо аккуратней, при >0 возвращать не -1, а 0.И еще: поскольку в разных применениях "разумные" требования на число повторов могут отличаться, то придётся ввести вызов для уставки максимума per-handle.
fdio_ntfr_t
сделать возвращающим не void, а int -- если
!=0, то прекращать циклить.
И тут же вспомнилось, что в Xt аналогичная проблема (destroy виджета изнутри какого-то другого вызова) решается сложнее -- отложенным образом ("two-phase destroy process"): виджет помечается как "being_destroyed", а реально уничтожается уже потом, когда управление вернётся Xt.
Тогда вариант с откладыванием показался слишком замутным. А сейчас, по здравому размышлению -- очевидно, именно так и следует поступить.
Для чего придётся добавить поля
being_destroyed
-- как в Xt.
being_processed
-- !=0 указывает, что нельзя делать
deregister сразу, а надо выставить being_destroyed=1 и отвалить.
Оно будет взводиться =1 на время вызова notifier'а. Соответственно, после вызова -- проверка being_destroyed, и если "да" -- то реальная ликвидация.
18.06.2013: да, делаем.
being_destroyed
/being_processed
.
StreamReadyForRead()
(для Dgram надо будет тоже), поскольку только в
нём может циклить, а все остальные -- включая вызовы
close_because()
-- сразу же после и отваливают.
...да, есть некоторые сомнения насчёт LISTEN-сокетов, но вряд ли соединения станут приходить пачками, так что забъём.
fdio_deregister()
только
"забывание" fr->fd=-1
, всё остальное делается сразу.
StreamReadyForRead()
после вызова
нотификатора в случае being_destroyed просто делается
fd=-1
и return.
maxrepcount
-- может указываться
вызовом fdio_set_maxreadrepcount()
.
IsReadError()
добавлен отдельно возврат
-1
при реальных ошибках, ...
>0
считается ошибкой на первой
итерации (при repcount==0) -- но только при чтении заголовка, а при
чтении данных уже всегда не-ошибка, т.к. оно может попасть туда и без
готовности дескриптора, поскольку...
goto FALLTHROUGH
.
Кстати, как следствие: две половинки пакета считаются за 1 повтор, а не 2.
watched
переедет в другое место.
BTW, это в проекте от 16-09-2012 было забыто (а касается ВСЕХ таких использований с пачечным вычитыванием).
На вид -- функционирует, а проверять теперь только эксплуатацией.
11.02.2014: да, вроде работает, проблем не заметно.
Но насчёт IsReadError()
есть некоторое сомнение: а не надо ли по альтернативе r==0
возвращать
-1
вместо нынешнего "умолчательного" +1?
SL_CE
-- чтоб Xh_cxscheduler корректно себя
вёл (Xt, мать их...).
ProcessPacket(handle=16, dev=7): I/O error
12.11.2012: протокол решения:
FDIO_R_IOERR
генерится в 3
местах. Во все была добавлена проверка на
SHOULD_RESTART_SYSCALL()
-- по аналогии с c4lcanmon'ом,
вопившим про EINTR. Но -- не помогло.
IsReadError()
, причём
errno=11 -- EAGAIN! Прикол в том, что это возникает в
StreamReadyForRead()
, вызываемом по готовности дескриптора
на чтение -- вот такой парадокс!
close_because(fr,FDIO_R_IOERR)
была также вставлена дополнительная проверка по
ERRNO_WOULDBLOCK()
.
ProcessPacket()
'ы была добавлена выдача также и errno в
виде fdio_lasterr()
.
StreamSend()
используется
INC_PTR_BY_BYTES()
? Тяжкое наследие connlib'а...
23.01.2013: решено просто: у самих
*Send()
параметр buf
стал
uint8*
.
fdio_register_fd()
при условии, что в этот
момент стоит SIG_DFL.
23.01.2013: сделано. Текущее состояние
вычитывается sigaction()
'ом, и потом проверяются и
sa_handler
, и sa_sigaction
.
Вроде работает, почитаем за "done", хотя из программ установку SIGPIPE:=SIG_IGN пока не выкидываем.
Замечание: потенциальные сложности -- в других средах, типа Win32, где никакого SIGPIPE не существует.
Так что теперь возвращается -1/EBUSY. (А sl_add_fd()
,
кстати, вернёт -1/EINVAL.)
22.09.2014: кстати, был багчок, что при
обломившемся sl_add_fd()
оно НЕ освобождало ячейку.
Так что теперь в начале делаем fr=NULL, а в ERREXIT'е if(fr!=NULL)fr->fd=-1;
22.09.2014: кстати, возвращаемое cxscheduler'ом EINVAL изрядно неудобно -- фиг поймёшь, что ж за ошибка. Но на что заменить -- неясно, поскольку должно различаться с fdiolib'ом.
fdio_set_uniq_checker()
.
07.05.2014: поскольку параметра "flags" у
fdio_register_fd()
нету (ох уж эта
непредусмотрительность!), то можно -- и разумно -- передавать таковое
указание в fdtype
-- OR'ить какой-нибудь далёкий битик.
В самой же реализации надо будет вместо захардкоженного повсюду
SL_RD использовать значение некоего поля (кстати, его предподготовка уже
есть -- iomask). Кстати, для наглядности можно так
поле и назвать -- SL_RD_mask
.
23.09.2014: пока, как выяснилось, работает и без этого -- Linux позволяет указывать write-only дескрипторы (по крайней мере, стороны pipe'ов) в readfds.
Но в других ОС ситуация может отличаться, так что фичу надо будет всё же реализовать.
09.02.2015: позавчера появилась связанная/похожая потребность: мочь временно блокировать чтение -- чтоб дескриптор не опрашивался на тему SL_RD.
fdio_lock_recv()
и fdio_lock_recv()
.
И чтоб работали бы так же, со счётчиком recv_locked
.
sl_set_fd_mask()
чего-надо.
Сейчас же так не прокатит.
Поэтому надо также ввести
cur_SL_WR_mask
, в которое "те" места и писали бы
0/SL_WR.
sl_set_fd_mask(fr->fdhandle, fr->cur_SL_RD_mask|fr->cur_SL_WR_mask)
23.03.2015: кстати, в связи с [un]lock_recv() напрашивается довольно общий принцип:
Источник событий должен быть маскировабелен.
BTW, в Qt'шной архитектуре signal/slot маскирование сигналов НЕ предусмотрено, а при надобности рекомендуется делать собственные наколенные решения: например, с очередью; а если можно терять сигналы -- то и просто disconnect/connect.
...ну и еще надо будет FDIO_STRING_CONN, который вёл бы себя ровно как FDIO_CONNECTING, но превращался бы в FDIO_STRING.
07.08.2014@вечер-пляж: некоторые замечания о функционировании:
fdio_deregister()
с последующим
fdio_register_fd()
с другими параметрами.
А ведь хотелось бы для экономии количества syscall'ов делать read() сразу большого блока!
08.08.2014: сделано, но пока никак не проверено (дождёмся cda_d_vcas).
Но реально там сразу делается fd=-1, так что последующая регистрация должна пройти нормально.
21.01.2015: проверено на сделанном позавчера-вчера-сегодня starogate. На вид всё OK.
09.04.2015: и на cda_d_vcas.c тоже проверено -- работает, так что "done".
fdio_send()
& Co.
С одной стороны, синхронные операции -- вроде
StreamSend()
-- при ошибках просто возвращают
-1
, и более ничего не делают.
С другой же стороны, они вначале опционально вызывают свои
*ReadyForWrite()
, а уж те могут при ошибке дёрнуть
close_because()
, коий вызовет notifier.
06.04.2015: обсуждение -- как поступить?
=-1
, так что "повторное закрытие" ничего не
испортит.
...но даже и там пришлось вставить лишнюю проверку, чтоб второй раз
rcn_tid
не заказало.
(Да, это махинация в ту же степь, что и
being_processed
/being_destroyed
.)
Ведь та же StreamReadyForWrite()
не предполагает НИКАКОЙ
рекурсии и вызывается всего из 3 точек: 1) асинхронно;
2) StreamSend()
; 3) StreamUnlockSend()
.
Решение очевидно: добавить всем *ReadyForWrite()
еще один параметр -- "можно ли делать close_because()
"
(или просто "is_async_use
"), и изнутри синхронных вызовов
передавать его =0.
07.04.2015: размышление общего характера:
connect()
отрабатывается
сразу, то и нотификатор по-хорошему надо вызывать сразу -- прямо
изнутри СИНХРОННОГО библиотечного вызова!
(Исключение -- работа в _sync-режиме, но там всё проще, да и использование этого режима очень ограниченно.)
buf
у функций отправки имеет тип
void*
, а не const void*
.
08.04.2015: ага, только ЭТО изменение тянет за
собой еще одно: "const
" нету и в
uintr_write()
(который, впрочем, теперь используется уже
только в самой fdiolib).
uintr_write()
тоже добавлено.
Результат обоих действий проверен diff'ом по логам от w_mkall.sh -- новых warning'ов не появилось, только старые исчезли.
int
, а должны бы ssize_t
.
Но это уже в раздел misclib.
StreamReadyForRead()
отсутствовала проверка на корректность указываемого размера пакета: при
len_add==0 (т.е., указывается размер ВСЕГО пакета) в len могло
приходить значение меньшее, чем hdr_size. Оно при этом, "провалившись"
в блок чтения данных, пыталось читать отрицательное количество.
На 2.4.18/i386@RH-7.3 приводило к EOPNOTSUPP, а на 2.6.32/x86_64@SL-6.3 -- к EFAULT.
07.06.2015: посему вставлена проверка на
pktlen<fr->hdr_size
.
СЕЙЧАС в таком случае соединение закрывается с ошибкой
FDIO_R_INPKT2BIG
, а по-хорошему надо б заюзать
FDIO_R_PROTOERR
-- очевидно, для этого и создававшееся.
И еще: там внутри арифметика и сравнения (r
,
count
) все в int'ах, а надо бы в size_t...
fdio_register_fd()
не хватало проверок на допустимые значения
len_size
-- 0,1,2,3,4 (при том, что все прочие варианты в
StreamReadyForRead()
просто игнорируются).
11.04.2016: проверка вставлена.
P.S. Кстати, а внимание на это было обращено Роговским, при вопросе "а можно ль указывать len_size=8?". Пока ответ -- нельзя; хотя, если припрёт, то сделать поддержку (только на 64-битных) можно; условия будут в 2 точках: при регистрации и при использовании, в обоих случаях с настоящим if()'ом (НЕ-#if) на тему "sizeof(size_t)>4".
fdio_deregister_fd()
с
непосредственно следующим fdio_register_fd()
.
Потому, что
fdio_register_fd()
проверяет, не зарегистрирован ли уже
такой дескриптор, сравнением поля .fd
всех ячеек, а...
fdio_deregister()
НЕ делает fr->fd=-1
being_processed-ячейкам, откладывая это до завершения обработки -- т.е.,
*ReadyForRead()'ам.
10.04.2016: причина, заставившая задуматься о таком сценарии -- письмо-вопрос Роговского:
а есть-ли возможность работать с fdiolib'ом так, чтобы он вычитывал 512 байт заголовка (на пытаясь там искать длину оставшейся части пакета) далее возвращал эти 512 байт (чтобы их можно было распарсить + и особо извращенно вычислить оставшуюсь длину посылки) и затем как-то дочитать оставшуюся часть посылки.
Учитывая, что впрямую fdiolib таких фокусов не умеет (не делать же плагин-архитектуру, с вызыванием указанной клиентом функции по получению заголовка для вычисления объёма последующих данных), то единственным напрашивающимся решением является постоянная пере-регистрация дескриптора (сначала -- фиксированные пакеты 512 байт (заголовок), потом -- фиксированные пакеты сколько-осталось байт (данные)).
Сделать поддержку такого, *видимо*, можно -- вставив в
fdio_register_fd()
'шную проверку уже-зарегистрированности
дополнительное условие, что !being_destroyed
.
08.10.2018: с учётом идеи всё-таки сделать поддержку "квазиплагинов" (см. ниже за 07-10-2018) замечание становится несколько иррелевантным. Но вот как его пометить -- "obsolete"?
09.10.2018: да, "obsolete".
_sysbufoffset
) не при каждом добавлении данных. А
только если "сверху" нет места; если же место есть (между
_sysbufoffset+_sysbufused и _sysbufsize), то новые данные просто добавляются
в конец.
03.04.2017: сделано в интересах cxsd_fe_cx (и основные комментарии в его разделе за 31-03-2017 и за сегодня), но полезно для всех -- эта оптимизация очень дешевая (один if), а эффект даёт изрядный.
Кроме собственно if()'а также чуть изменён код строк fast_memcpy() (там
к dst прибавляется _sysbufoffset
) и обновления
_sysbufused
(теперь просто делается +=size
).
Правильно бы сделать как-нибудь более общо -- как в своё время родился сам fdiolib, обобщающий все случаи с длиной фиксированным полем в фиксированном заголовке (и даже без поля длины тоже).
07.10.2018@пляж: так
называемый "плагин" для fdiolib'а плагином в полном смысле быть не может,
т.к. все интимные детали работы со входным потоком сосредоточены внутри
fdiolib.c, в fdinfo_t
.
Что МОЖНО сделать -- так это "методы": API для доступа снаружи, уставляющий значения этих интимных деталей (в основном -- очевидно, просто объёмы "сколько ещё надо прочитать"), плюс возможность "заглядывать" в уже считанные данные.
Т.е., этот "плагин" -- как бы "внешний": просто программе-юзеру даётся чуть больше возможностей для сования своего носа в процесс приёма.
Примерно так:
08.10.2018@пультовая, в районе
16:30: внимательно поразглядывав код
StreamReadyForRead()
, пришёл к выводу, что наилучшим местом для
"внедрения" плагинности будет точка, где заголовок пакета считается
полученным и код собирается добывать длину пакета из заголовка.
И напрашивается проект реализации со следующими деталями:
fdinfo_t
:
is_asking_plugin
".
hdr_size_add
" -- сколько, на текущий момент
понимания, надо довычитать дополнительно байт заголовка.
plugin_pktlen
" -- вычисленный плагином размер
пакета.
fdio_advice_hdr_size_add()
-- уставляет
"hdr_size_add
".
fdio_advice_pktlen()
-- уставляет
"plugin_pktlen
".
Они отрабатывают ТОЛЬКО при взведённом "is_asking_plugin
",
иначе -- -1/EINVAL.
Так что исчезает потребность заводить отдельный API для "заглядывания" в прочитанные данные.
fdio_advice_hdr_size_add()
(в случае MQTT прося +1 к ранее
прошенному).
fdio_advice_pktlen()
.
Можно (хотя и не очень красиво) -- должно отработаться корректно.
Хорошо бы, но не получится -- size_t
беззнаково.
Как вариант -- НЕ вызывать ни того, ни другого: тогда заголовок будет считаться полностью принятым, а "посоветованный" размер пакета (plugin_pktlen) останется нулевым, что сигнализирует невозможную ситуацию (ведь сколько-то УЖЕ принято) -- на это можно реагировать как "PROTOERR"
hdr_size+hdr_size_add
) стала больше текущего
reqreadsize
-- значит, надо вычитать ещё.
Просто делаем "goto КОНЕЦ_ЦИКЛА" (в точку, где обрабатывается
repcount
и принимается решение о повторе).
Тут просто переходим далее, к присвоению thisreqpktsize
и
дальнейшим проверкам, до-аллокированию и вычитыванию остальной части пакета.
plugin_pktlen
==0: протокольная ошибка в заголовке, закрываем
соединение.
count
: за
"требуемый размер" брать hdr_size
ПЛЮС
hdr_size_add
.
hdr_size_add
=0? Очевидно, при переходе
в состояние reqreadstate=0. Т.е., вместе со сбросом оного (плюс, при
создании оно автоматом занулится, вместе с reqreadstate же).
plugin_pktlen
что? Видимо, делать =0 перед вызовом
FDIO_R_DATA_PART.
being_processed
=1,=0 с
последующей проверкой being_destroyed
, так что программа-клиент
(функционал "квазиплагина") имеет возможность оттуда делать закрытие
соединения.
10.10.2018: делаем.
FDIO_R_HEADER_PART
(ведь часть-то ЗАГОЛОВКА, а не данных).
FDIO_R_PROTOERR
-- при "ошибке
вычитывания длины пакета".
Теперь надо будет проверять -- когда, на чём? :)
27.09.2023: сделано первое применение -- в modbus_tcp_drv.c и modbus_mon.c (код в них одинаковый) для Modbus-RTU, поскольку в нём нет поля длины и определить длину пакета можно только анализом содержимого -- кода команды плюс поля nbts.
Проверять пока не на чем (железки нет).
29.09.2023: проверяем на "ничём" -- на
/dev/ttyS0, к которому ничего не подключено. И сразу косяк:
попытка отправки даёт EINVAL
.
Оказывается, в fdio_send()
не было "приравнивания"
FDIO_STREAM_PLUG
к обычному FDIO_STREAM
. Сделано
-- отправка пошла.
24.01.2024: реальная работа проверена ещё тогда же почти сразу, в районе 06-10-2023, на метеостанции Сокол-М1 -- да, всё получилось, пакеты (с определением длины плагином) принимает.
(Потребность -- для поддержки LXI, в котором, хоть он и текстовый, могут передаваться бинарные данные: например, вэйвформы).
17.10.2018@утро-дома: мысли на тему о том, как бы это реализовать.
Назовём его fdio_string_req_bin()
?
thisreqpktsize
.
reqreadstate
=1.
StreamReadyForRead()
. Тем более, если пользоваться
reqreadstate
так, как предложено в предыдущем пункте, то там
всё само собой отработается как надо.
repcount
), так что после вычитывания одного такого пакета оно
попытается читать следующий как будто STREAM'овый же.
Хотя проблему с repcount
можно обойти, вставив
дополнительное условие "повторять только если тип наш (FDIO_STREAM)", но это
криво.
Тем более, что кусок, в StreamReadyForRead()
вычитывающий
остаток пакета, совсем маленький, так что на дублирование можно закрыть
глаза.
FDIO_R_DATA
, и уж клиент пусть помнит, что ждёт
бинарных данных, а не строки?
FDIO_R_BIN_IN_STRING
"?
Лучше второе -- так определённее, меньше возможностей клиенту накосячить (хотя и всё равно придётся хранить "контекст" -- информацию, ЧТО за бинарные данные мы ждём).
19.10.2018: сделал.
fdio_string_req_binary()
.
FDIO_R_BIN_DATA_IN_STRING
.
StringReadyForRead()
сделано отдельной веткой -- там теперь if()/else по
reqreadstate
.
Содержимое этой бинарной ветки взято из
StreamReadyForRead()
, вместе с тут-уже-не-вполне-релевантным
вариантом реакции на errs>0
и комментариемю.
Насчёт тестирования и потенциального использования:
С другой стороны, а зачем? Проще читать вручную, через cxscheduler (или, если хочется использовать fdiolib'овский интеллект буферизации на отправку -- запрашивать пакеты фиксированного размера 1 байт).
20.10.2018@суббота-дома: а можно воспользоваться предыдущим вариантом --
FDIO_STREAM_PLUG
: указать hdr_size=1 и len_size=0, чтобы
получать по 1 байту, и дальше уже анализировать эти байты самостоятельно,
запрашивая "добавку" обычно по 1 байту, самостоятельно же отлавливая
'\n'/'\r', а при переходе в бинарный режим затребовывать
как раз нужное количество байт.
Замечание:
StringReadyForRead()
-- нет, не будет: символы там воспринимаются раздельно, без попыток анализа
разных вариантов (DOS: CR,LF; Unix: LF; MacOS(?): CR), выполняемых в
ppf4td_nextc()
, дабы воспринимать эти комбинации как единое
окончание строки (в противном случае подсчёт номеров строк бы поехал).
cxsd: fdiolib: StreamReadyForWrite(handle=9,fd=20): STREAM-READY-FOR_WRITE, but _sysbufused==0!
Произошло это на b360mc в cxsd, хостящем пачку ADC250 черезе A3818. Момент возникновения не совсем ясен, но как-то связан с подключением клиента-скрина "adc250" (отображающегося по сети на p320t) и, возможно, с какими-то манипуляциями в его GUI.
Сообщения не то чтоб прямо очень часты, но в терминале (через screen), в котором запущен "cxsd -d", их довольно много. Вполне возможно, что такие сообщения и раньше бывали где-то массово, но cxsd всегда запускаются фоновыми демонами, так что их stderr направлен на /dev/null.
09.02.2022@утро-душ: не вполне ясно, как тут вести отладку: воспроизводится при вполне неясно каких условиях (слегка рандомно), да и непонятно, на что смотреть.
Похоже, надо начинять отладочной печатью. Но эта отладочная печать напродуцирует море текста, который затеряется в консольном выводе, да и фиг разберёшься...
Можно поступить так:
Редирект на который должен обеспечиваться запускающим (через синтаксис
"cxsd 3>/PATH/TO/FILE
").
Поэтому делать надо на постоянку.
11.02.2022@вечер-ванна:
а если дело в том, что по
sl_set_fd_mask(,SL_RD)
(т.е., УБИРАНИЕ бита SL_WR
,
по окончанию отправки буфера) он всё ещё есть в битовых масках от
select()
'а -- а поскольку на запись дескриптор в этот момент
готов, то он и будет считаться "ready-for-write", вот и вызовется
callback...
Но вроде бы я этот вопрос обдумывал ещё давным-давно и вроде бы делалось
выключение битиков в FD_SET
'ах, используемых непосредственно
select()
'ом (не поэтому ли они и сделаны глобальными, а не
внутри sl_main_loop()
?)...
@совсем-вечер: да нет, НЕ
делается выключение; там только комментарий стоит
"/*!!! Shouldn't we clear bits in sel_NNN? */
".
А глобальными sel_*fds
сделаны, скорее всего, для
sl_del_fd()
-- тот-то битики из масок удаляет.
11.02.2022: ну сделал отладочный вывод по вышеприведённому сценарию от 09-02-2022:
DO_DEBUG_FDIOLIB
(целочисленная).
DEBUG_OUT()
,
"вызов" которого в тексте есть всегда, но просто при отключенности механизма
он определяется в ничто.
FILE *debug_fp
, если он !=NULL.
fdio_register_fd()
,
DEBUG_FDIOLIB_HANDLE
,
которая если определена, то из неё берётся номер файлового дескриптора.
11.02.2022@вечер-~22:46: проверил работу отладочного вывода -- ну да, работает.
Вот только сама ситуация понятнее не стала: из тех простыней неясно, почему всё же вызывается WR-callback при уже сброшенном SL_WR.
12.02.2022: ну что -- пробуем сделать в
sl_set_fd_mask()
выключение битиков прямо в масках для/от
select()
'а -- в sel_rfds
, sel_wfds
,
sel_efds
.
...помогло...
_sysbuf
опустошается и маска сбрасывается, потом успевает
пройти несколько штук обычных StreamSend()
'ов, и уже после того
происходит вызов по SL_WR?
StreamSend()
при непустом _sysbuf
'е пытается
"flush'нуть" его содержимое, предварительно проверив
select()
'ом готовность на запись? Есть ли там такая проверка?
_sysbufused!=0
он проверяет с помощью
check_fd_state(, O_WRONLY)
, и если "можно", то вызывает
StreamReadyForWrite()
.
Итого:
В w20090408.tar.gz файл от 03-02-2009 уже с этим. И более того: самый древний из найденных файлов, viper-archive/20060516/work/4cx/src/lib/useful/fdiolib.c за 30-03-2006 (а сами работы по fdiolib'у начались в марте 2006-го, судя по записям от 29-03-2006). Так что -- "нюанс", похоже, был изначально.
sl_set_fd_mask()
.
fdio_reset_read()
" -- чтобы сбрасывать состояние
чтения (с забыванием всего уже находящегося в reqbuf
)?
Смысл -- чтобы мочь "сбрасывать" состояние чтения для всяких Modbus-serial, если вдруг "что-то не то", например, по таймауту.
Замечание в сторону:
Но вот "КАК?" -- вопрос, и не столько технический, сколько идеологический. Думать надо...
24.01.2024@дорога-в-ИЯФ: с
другой стороны, нынешний вариант -- "грохать соединение при любой ошибке, в
т.ч. протокольной, чтобы потом его открыть заново" -- вполне живуч: как раз
пройдёт некоторый интервал времени, так что остаток пакета по serial-link'у
успеет придти и будет выкинут при следующем открытии делаемым там
tcflush(,TCIFLUSH)
. Единственное что, modbus_mon'у должен
будет быть указан ключик "-K
" ("keep going after I/O errors"),
но он так и так необходим для не-отвала при ошибках.
Так что, пожалуй, СЕЙЧАС реальной надобности в
"fdio_reset_read()
" и нет.
1. Название "smp4td" выбрано из соображений уникальности -- и "tfp" (text file parser) и "tdp" (text data parser) имеют некие применения, а эта 6-символьная аббревиатура -- нет.
2. Поселяем его в useful/, а не в misc/, поскольку он все-таки достаточно крупен -- и скорее в одной весовой категории с прочими файло- и В/В-ориентированными модулями. Да, paramstr_parser, который в некотором роде родственен, живет как раз в misc/, но его можно считать "модулем меньшего масштаба", так что все окей.
09.06.2008: и еще раз битым текстом "целевая аудитория" этого модуля:
05.10.2008: STPP -- Simple Text PreProcessor!
15.10.2008: поразмыслив, нет -- пусть лучше останется SMP4TD: это абсолютно уникальное буквосочетание, в отличие от STPP.
24.07.2009: свежесделан скелет --
smp4td.c с функциями smp4td_start()
,
smp4td_end()
и smp4td_readline()
(они уже
наполнены минимально-рабочим содержимым), плюс, соответственно,
заполнен и smp4td.h.
28.07.2009: первоначальная версия доделана.
Также создана проверочная утилитка programs/utils/smp4td_test.c, могущая использоваться как самостоятельный препроцессор. Её, конечно, надо бы под-улучшить, чтобы она парсила по-токенно, с разделителями в виде пробелов.
Также для полноты картины надо реализовать Cdr-плагин уже на основе smp4td -- при этом вылезет потребность еще в нескольких функциях.
28.07.2009: НЕСКОЛЬКИМИ ЧАСАМИ ПОЗЖЕ: smp4td_test под-улучшил, оно теперь пытается имитировать "интеллектуальный" парсинг, с правильным восприятием кавычек и даже '#' -- с закосом под subsys-парсер. Старый простой вариант -- за #if'лен, потом его можно будет включать при помощи ключика.
В процессе сего добавил несколько служебных функций.
Общее направление: стараться делать так, чтобы программа НЕ
ЗНАЛА, что lp
-- это char*
, указывающий в
буфер строки, а всегда вызывала бы функции smp4td, получая от них
готовые токены.
31.07.2009: продолжение:
stdio_lreader()
об-умнена:
она теперь при fgets()==NULL проверяет feof()
, и
возвращает EOF или ERR.
psp_pluginp_t
добавлен параметр
char **errstr
в smp4td_open_t
и
smp4td_lread_t
-- чтобы они могли возвращать внятное
описание ошибки, которое SMP4TD потом сможет интегрировать в своё
описание и отдать по smp4td_lasterr()
. (Это чтобы вся
диагностика делалась единообразно, без надобности лазить в диагностику
подстилающей системы чтения.)
05.08.2009: во-первых, "адаптер" Cdr_newf_ldr.c практически доделан, и начато изготовление уже собственно читалки подсистем Cdr_via_smp4td.c (в которую и перейдут функции Cdr_fromtext.c).
Во-вторых, слегка окрасивлен интерфейс: теперь началом комментария
считается не зашитый символ '#', а прямо в
smp4td_start()
передаётся параметр
cmt2eol_start
, в котором можно указать
строку-начала-комментария. Плюс, в smp4td_is_at_eol()
добавлена соответствующая проверка.
12.01.2010: легкое дополненьице: для упрощения
парсенья закавыченных строк введены флажки
Ну и для этого был введен флажок SMP4TD_PMS_SNGQUOT
, SMP4TD_PMS_DBLQUOT
и
SMP4TD_PMS_QUOTES
=smp4td_parse_mcstr()
'у, что если текущим
символом идет одинарная или двойная кавычка, то надлежит перейти в
режим "terminators:ЭТА-КАВЫЧКА, flags:SMP4TD_PMS_SKIP1TERM".
SMP4TD_PMS_SKIP1TERM
,
указывающий, что надо пропустить ОДИН терминатор (т.е. -- закрывающую
кавычку).
12.01.2010: Заодно наткнулся на такую фигню: НЕЛЬЗЯ пользоваться конструкцией вида
strchr(terminators, *p) != NULL
а только
*p != '\0' && strchr(terminators, *p) != NULL
Проблема в том, что *p=='\0'
НАХОДИТСЯ
strchr()
'ом даже в пустой строке. О чем, в принципе,
сказано и в libc.info -- "The terminating null character is
considered to be part of the string".
13.01.2010: после исправления вчерашнего бага
понадобился еще один флажок -- SMP4TD_PMS_EOLTERM
,
указывающий, что просто конец строки следует воспринимать как
terminator. Иначе оно, дойдя до конца строки и не обнаружив
терминатора, ругалось "unexpected EOL".
13.01.2010: а еще оно ведь у нас пока не понимает '\' в конце строки, и ругается "backslash-EOL".
Потом надо будет научить считать это переносом строки, "досасывая" следующую строку.
Пока же добавлен флажок SMP4TD_PMS_BSLSHEOL
,
указывающий, что сей '\' надлежит просто положить как обычный
символ. Это позволит использовать имеющуюся сейчас в
Cdr_fromtext.c::ParseKnobDescr()
модель. Но
вообще-то это уродство, которое надлежит изничтожать (еще отдельный
вопрос -- как оно интерферирует со terminators...)...
15.01.2010: да, уродство убрано -- теперь оно
по комбинации backslash+EOL считывает следующую строку и ставит
указатель на её начало. Флажок SMP4TD_PMS_BSLSHEOL
,
естественно, выкинут.
Есть легкая дурь: поскольку smp4td_skip_white()
об этих
вещах совершенно не в курсе, то последовательность
"SPACE+backslash+NL+SPACE+..." даёт ДВА пробела.
15.01.2010: и еще: поскольку часто требуется
добывать из входного потока не просто "токены", а ИДЕНТИФИКАТОРЫ, то
добавлен флажок SMP4TD_PMS_IDENT
. При его указании оно
считает терминатором любой символ, кроме
alphanumeric/'_'/'-'.
20.01.2010: и еще: для парсинга чисел введен флаг
SMP4TD_PMS_INT
.
$(funcname param1,param2,...)
". Плюс, по-хорошему надо
дозволять вложенные макросы -- т.е., "$(funcname
param1,$(MACRO),...)
"; видимо, по тому проектику, который был в
своё время разработан для srchtml.
Плюс, кроме .define
надо также иметь и
.definexline
/.endxline
.
В любом случае -- это фичи уже второго порядка, т.к. они значительно усложнят препроцессор.
01.03.2009: однако -- а решение проблемы реализации "макросов с параметрами" реально довольно просто: надо использовать вложенные вызовы "parse_macro_string()".
Размышления по теме:
Потенциальные сложности:
25.07.2009: вообще-то, сие пока излишне, и вот почему. Нам пока бы поддерживать только простые макросы, дающие 1 токен/строку, иначе придется сильно выпендриваться. А многострочные и параметрические макросы -- это как раз для более сложных конструкций.
Более навороченный вариант может оказаться полезен на будущее (аппетиты-то растут во время еды), пока же, для заявленных использований (config-, devlist- и subsys-файлов) хватит и такого SIMPLE.
25.07.2009: да, можно, причем довольно просто -- безо всяких "co-routines".
Идея такова: ввести еще один код -- SMP4TD_BREAK
,
который будет возвращаться reader'ами при пустом входном буфере.
Соответственно, все потенциально-многострочные конструкции (как в
"юзерах", так и в самой smp4td (например, .definexline
))
должны мочь уконтекстиваться -- т.е., сохранять свой текущий контекст и
отваливать.
25.07.2009: угу:
SMP4TD_BREAK=2
re_entry
.
smp4td_context_struct
введено поле
cur_mcmd
для хранения "текущей исполняющейся
макро-директивы".
smp4td_readline()
сделана поддержка сей
концепции (по BREAK делается заполнение cur_mcmd
и
отваливание, а в начале, после получения строки -- вызов, если оно
уставлено).
Скорее всего, это заработает. Но проверить пока возможности нет, так что раздел временно просто тихо замораживаем.
15.09.2009: побудительным мотивом послужила надобность реализовать smp4td-читания config- и devlist-файлов в сервере. Поскольку там всё то же самое, то пришлось копировать, что есть изврат. А в будущем явно появятся еще потребители.
Оставлено соглашение, что reference==NULL возвращает
stdin
. Это есть хорошо -- тем, что юзер НИКАК не сможет
указать ссылку на stdin в обход программы (как это было бы, например,
при "" или "-"): первичный файл всегда проходит через
программу, а в .include NULL также никак не образуется.
Возможное будущее усовершенствование -- если заиспользовать privptr в opener'е, например, для суффикса.
И, скорее всего, с нынешним подходом "полноценный" макропроцессор сделать не удастся -- там нужна честная "распаковка" входного потока, полностью независимая от вызовов клиента. Т.е. -- модель produce/consume.
(А в качестве временного решения подошло бы пропускание входного потока через cpp. А тогда понадобится, чтобы smp4td "понимал" выдаваемые оным директивы #file и #line.)
04.12.2013: при создании оно жило в отдельной libppf4td.a, что неудобно. Так что переселяем прямо в libuseful.a.
06.07.2014: подмодулей стало слишком много, так что содержимое раздела рассортировано по отдельному списку level5.
06.04.2013: по пунктам:
ДОСТОИНСТВА: в нём есть всё, что надо -- и вычисления, и условия внутри макросов (причём исполняемые в момент РАСШИРЕНИЯ макроса!).
НЕДОСТАТКИ:
СЛЕДСТВИЕ: единственный возможный вариант использования -- делать pipe()+fork(), в "основном" процессе читать выход pipe'а, а в порожденном генерить данные на вход. "Подменить" блок чтения в m4 в принципе можно, взяв за основу m4-1.4.1 (1994г.) из RH-7.3 (более свежие распухли и стали шибко навороченны).
Выдавать такие директивы надо при .include и его завершении. Что делать при расширении xline-макросов -- некоторая загадка.
$(if...)
(аналогично GNU make).
...хотя, еще правила вложения кавычек в m4 удобные, да и не-только-строкоориентированность, но для наших целей и простого варианта хватит.
Это просто чтоб не дублировать такой общеиспользуемый функционал во всех клиентах.
И, конечно, в любом варианте клиентские парсеры надо будет радикально переписать с нуля.
07.04.2013: ...а если сделать этот "средний уровень" достаточно стандартным, то вообще можно его использовать для ЛЮБЫХ препроцессоров -- хоть smp4td, хоть m4, хоть cpp.
08.04.2013: решено: делаем 3-уровневую архитектуру парсера:
Могут понадобится еще сервисные функции, типа "дай текущее положение в потоке" (для выдачи ругательства) и "не конец ли файла".
Значительное изменение по сравнению со старой концепцией: теперь входной поток потребляется сразу посимвольно/потокенно, а не построчно (с последующим разбором).
08.04.2013: начинаем реализовывать. Концепцию называем "gmp4td" (General-purpose Macro-Processor for Text Data).
10.04.2013: да, многое сделано. По ходу:
По факту имеется стандарт на информирование "верхов" --
#line LINENUM "FILENAME"
или просто
#line LINENUM
Оное есть в cpp по умолчанию, в m4 при -s
, ну а в своём
препроцессоре добавить будет несложно.
02.06.2013: а вот и нет -- в cpp не
#line
, а просто #
, т.е.,
# LINENUM "FILENAME" [FLAGS]
Так что надо будет иметь ДВА варианта парсенья и возможность плагинам указывать используемый вариант (и, в т.ч., полное его отсутствие).
18.04.2013: продолжаем:
waitpid()
'у).
26.05.2024: вставлено
signal(SIGCHLD, SIG_DFL);
непосредственно перед execve()
; основное обсуждение в разделе
по sim_dir_drv за сегодня.
30.05.2024: "ловя ошибки" -- ага, ага... А это
проверялось хоть раз, что они БУДУТ ловиться? Судя по недавно понятому, при
(SIGCHLD,SIG_IGN)
-- нифига. Но там вообще результат "ловли" и
не проверяется, а возвращается всегда 0.
В m4 под RH-7.3 обнаружился глюк: он НЕ возвращает ошибку при отсутствии файла (exitcode==0). Под RHEL-5.2 уже всё нормально.
01.10.2013: продолжаем, в основном в отношении поддержки "#line...":
ppf4td_ctx_t._ucbuf[]
, с размером 100 символов. Причём
int
, а не char.
ppf4td_peekc()
/ppf4td_nextc()
сначала
проверяют его.
ppf4td_nextc()
'шные) не
участвуют.
UnGetStr()
возвращает указанную строку {buf,len} в
буфер (складывает, естественно, задом наперёд). Единственный её юзер
-- ppf4td_nextc().
linesync_type
, плюс константы
PPF4TD_LINESYNC_nnnn
, плюс в
DEFINE_PPF4TD_PLUGIN()
оно добавлено.
ppf4td_ctx_t._is_at_bol
(BeginOfLine),
выставляемый при:
_ucbuf[]
;
ppf4td_open()
;
ppf4td_nextc()
при отдаче очередного
"реального" символа, если он не-CR/LF (реально там
уставка/сброс сделаны присвоением результата сравнения).
ppf4td_ctx_t._prev_ch
помнит предыдущий РЕАЛЬНО
вычитанный+вёрнутый символ, и если текущий -- '\n' при предыдущем
'\r', то повторяет чтение. Таким образом пара CR+LF
рассматривается как единый "newline".
_curline++
делается после вычитывания очередного
символа, на основании ПРЕДЫДУЩЕГО значения _is_at_bol. (Да, изначально
ставится _curline=0.)
Смотрится аккуратно: очередной символ для проверки не читается (nextc), а подсматривается (peekc), и лишь если подходит -- то вычитывается. Это сделано чтоб не маяться с переводами строк, встречающимися прямо посреди потенциальных #-префиксов (например, "#\n") -- иначе оно некорректно считало номера строк (поскольку '\n' улетало в _ucbuf[] и потом проходило сразу в клиента, мимо "мозгов".
Тут пришлось также ОЧЕНЬ аккуратно делать: поскольку '\n' считается за isspace(), то на строках вида #line NNN\n пропускание whitespace после номера строки заодно сжирало и перевод строки, а последующий пункт успешно скушивал следующую строку.
goto
обратно на вычитывание из
потока. Предполагается, что имеющиеся (еще от прошлого вызова)
значения _prev_ch
и _is_at_bol
сохраняют свой
смысл, так что они не трогаются.
Видимо, введено для того, чтоб можно было запускать несколько cpp подряд в конвейере, или ре-использовать результаты "когда-то'шних" прогонов, или для совокупления с какими-то другими препроцессорами.
Проверено на 2.96@RH-7.3 и на 4.7.2@FC18, поведение в этом смысле одинаковое.
По результатам ppf4td_nextc()
стал совсем непохож на
простенький ppf4td_peekc()
, хотя сначала они были
близнецами.
Всё это делалось в течение 3 дней. Теперь вроде пашет (проверено на AnotherLevel'е). Далее надо нарабатывать практику использования и, видимо, ваять более высокоуровневости (skip_white, get_ident, get_string, read_line), уделяя особое внимание корректности обработки концов строк (где надо их замечать, а где нет -- игнорировать).
15.10.2013: проблема: из-за реализации эккаунтинга целиком в _nextc() и простого _peekc() имеем расхождение: если программа сначала пытается подсмотреть следующий символ, а это "#" (начало от "#line..."), то увидит совсем не то, что было бы при вычитывании. Этот эффект проявляется при попытке программы увидеть комментарии "#...".
И что делать?
А может, вообще отказаться от отдельной сквозной реализации _peekc() (плагинами), полностью возложив это на саму библотеку? Чтоб еёйный _peekc() вызывал бы _nextc(), а потом бы при надобности вертал бы символ назад.
Похоже, придётся. Хотя сам ppf4td_nextc()
использует
vmt->peekc()
при проверке на префикс "#line...", но это
исправимо.
16.10.2013: да, делаем:
ppf4td_peekc()
с вызова vmt->peekc() на
вызов ppf4td_nextc()
, с последующим возвратом символа в
буфер посредством свежевведённой UnGetChr()
.
Но вообще это кривизна: надо б иметь ОДИН вызов "nextc()", а уж с буферизацией разбираться внутри ppf4td. Второй уровень буферизации? Мутновато...
Проверено -- вчерашняя проблема исчезла.
26.11.2013: старый за-#if0'енный код удалён.
16.10.2013: пилим API:
PPF4TD_OK
=+1,
PPF4TD_EOF
=0, PPF4TD_ERR
=-1,
...но фиг: у многих функций ==0 означает "окей". Что очевидно конфликтует с EOF, который вовсе не окей.
Так что пока забиваем и "withdrawn".
PPF4TD_FLAG_*
, передаваемые функциям "среднего уровня" в
дополнительном параметре flags
:
PPF4TD_FLAG_DASH
: разрешение
ppf4td_get_ident()
'у считать '-' допустимым для
идентификатора символом.
PPF4TD_FLAG_nnnTERM
, где nnn -- EOL
,
HSH
, SPC
: указание
ppf4td_get_string()
'у завершить парсинг по этому символу.
Т.е.,
18.12.2019: кстати, там чуднАя проверка: что isspace(), но НЕ '\n' и НЕ '\r'. Ведь из каких-то соображений я это делал (причём стоит это всё ПОСЛЕ проверки по EOLTERM). Интересно, зачем?
Эти флаги работают только вне кавычек, внутри же они неважны (об этом подробнее ниже).
03.12.2013: добавлен еще один флаг-терминатор --
BRC
(BRaCe) -- '}', в интересах парсера
.subsys-файлов.
03.12.2013: но вообще модель выглядит всё более некрасивой. Надо либо указывать терминаторы явно, "группами" (либо строкой, либо 256-битовым набором), либо переходить к постулированию парсинга ТОКЕНАМИ, при которых либо закавыченная строка, либо идентификатор [_a-zA-Z][_a-zA-Z0-9]* (тогда всё остальное автоматом будет его завершать). (Хотя что-то еще для чисел надо предусматривать.)
ppf4td_get_string()
.
(К чему уже известному это ближе, а?)
smp4td_parse_mcstr()
выполняла
собственно макрорасширения, тут на вход получаем уже отпроцессированный
поток символов.
28.07.2014: плохо -- конкатенация невозможна! Надо всё-таки shell-style. Далее см. отдельную секцию за сегодня.
04.08.2014: да, переделано.
Для других же символов он просто вставляет те символы "как есть" -- так делаются и сам бэкслэш, и кавычки, и пробел.
28.07.2014: не хватает парсинга цепочки \xNN (или \OOO) -- для возможности указывать апостроф, '\x27', с которым сложности из-за синтаксиса m4. Далее см. отдельную секцию за сегодня.
PPF4TD_Ennn
как расширение стандартных
Ennn
для errno
. Аналогично
CEnnn
(с которыми области не пересекаются), это
отрицательные коды.
Плюс функция ppf4td_strerror()
.
24.10.2013: вылезло некоторое "неудобство": как-то молча предполагается, что если некий nextc()/peekc() вернул r=0/ch=EOF, то и следующий сделает то же самое.
Но при pipe-чтении это НЕ ТАК! Первый peekc() отдаст 0/EOF, а второй уже -1 (не вполне очевидно, почему).
Вывод: надо б как-нибудь запомниать этот EOF, и если он имел место -- то дальше уже его отдавать, без попыток дополнительного чтения.
ppf4td_get_string()
, а надо б и МЕЖДУ токенами, чтоб можно
было строку в любом месте разбить.
В старой версии -- с smp4td -- единственная обработка была как прямо
в клиентском коде в
Cdr_fromtext.c::ParseKnobDescr()
, так и в самом
smp4td_parse_mcstr()
(но там ПОЧТИ ВСЁ делалось через
mcstr).
08.11.2013: вставлять оное надо вроде в
ppf4td_skip_white()
. Тем самым постулируется, что токены
(идентификаторы, числа) таким образом разбивать нельзя (ну оно и
разумно -- NL обычно катит за пробел).
Вопрос, не аукнется ли это на ppf4td_is_at_eol()
?
15.11.2013: да не, с _at_eol() интерферировать не должно: раз nextc=='\\', то это точно не-EOL, а собственно "\NL" из потока должны будут испариться полностью, не дойдя до более высокого уровня.
Реальные проблемы видятся иные:
А обрабатывать ТЕ бэкслэши прямо на нижнем уровне вроде никак нельзя -- поскольку "там" могут быть иные требования, например, сохранять все бэкслэшности as-is, для передачи куда-то еще.
Реализовываться должно ПОЛНОСТЬЮ в ppf4td_nextc()
, и
чтоб наружу (в т.ч. до ppf4td_peekc()
, которая сейчас на
ней) это даже б и не попадало.
26.11.2013: сделано, пользуясь свежевведённым _l0.
Сам кусочек кода очень простой и очевидный.
По вышеприведённым трём потенциальным проблемам:
Единственное что -- оно само делает _curline++
.
Кстати, а cpp склеивает всё в одну строку, а потом вставляет дополнительные пустые строки -- по количеству удалённых NL'ей.
Кстати, cpp (gcc-2.96) делает ровно то же самое.
Теперь вопрос -- а надо ли оставлять свою обработку "\NL" в
ppf4td_get_string()
(где теперь они уже "следующего
уровня").
26.11.2013: делаем первую часть -- основную инфраструктуру, чтоб "\NL" отрабатывалось.
_l0buf[]
("Level0", по
аналогии с процессорными кэшами).
ppf4td_nextc()
заменены на вызов ReadNextCh()
.
ReadNextCh()
при наличии чего-нибудь в _l0buf[]
возвращает оттуда, а иначе вызывает vmt->nextc().
НИКАКИХ оптимизаций не делается (включая отбрасывание лишних \n после \r), а сбагривается всё as-is.
Т.е., это полностью отдельный буферный уровень между реальным входным потоком и парсером, так что и эккаунтинг делается уже полностью ВНЕ его.
PeekNextCh()
. Что заодно позволяет
выкинуть vmt'шный peekc().
UnGetL0s()
UnGetL0c()
, копии обычных UnGet*().
Проверено -- пашет.
06.12.2013: just for note -- what's NOT done yet:
29.11.2013: реализована
ppf4td_ungetchars()
просто -- ведь внутри уже есть всё
нужное в виде UnGetStr()
, к которой новорожденная стала
тупым переходником.
Вопрос в другом -- насколько сие корректно с точки зрения размеров буферов? Сейчас 100 символов, попасть туда могут куски максимум длиной с #line либо с ключевое слово из Cdr_db-парсера (около 10 символов). Т.е., сейчас -- окей, но в общем случае ненадёжненько.
29.11.2013: кстати, еще раз к вопросу об адекватности используемой сейчас модели.
Оказывается, m4 пытается (в отличие от cpp!) блюсти номера строк, и макросы вида
расширяет не в простоеdefine(`abc', `def \ ghi') abc
а вdef \ ghi
-- и эта "#line..." сбивает парсер с толку.def \ #line 5 ghi
Надо учиться корректно обрабатывать такие фокусы: если после "\NL" идёт #-LINESYNC-последовательность, то проглатывать и еще один NL, не отдавая его наверх.
04.08.2014: решение оказалось до идиотизма простым и очевидным: сразу после парсинга \NL делается
ctx->_is_at_bol=1
И всё. Дальше желаемый эффект достигается автоматически.
ppf4td_get_string()
от
16-10-2013 с "Если первый символ -- кавычка или апостроф, то
парсим всё до появления такого символа; после чего считаем парсинг
завершенным" неудовлетворителен, т.к. не позволяет делать
конкатенацию строк, необходимую при использовании макросов.
Например, приделать в макросе к строке "abc def" еще "xyz" никак не удастся.
Так что -- надо всё-таки переходить на shell-like вариант, с возможностью множественных закавыченностей.
04.08.2014: да, переделано. Конкатенация теперь работает.
21.12.2015: сделано. Воспринимается как полный вариант
\xNN, так и короткий \xN. Парсинг одного hex-digit'а в сервисной функции
getxdigit()
.
28.12.2015: добавлена защита от \x0 -- результирующие NUL'ы просто игнорируются.
eval()
работает только с целыми числами. Это крайне неудобно при указании
{r,d} -- там частенько требуется арифметика.
24.07.2015: гугление показывает, что рассчитывать на появление вещественной арифметики в m4 не приходится -- надо делать самим.
Варианты-размышления:
Но тут проблема в том, что '$' вовсе не является каким-то специальным символом. Да и нефиг ему таким являться.
26.12.2015: но и тут вопрос -- в каком формате отдавать "в поток" результирующее число, чтоб не было потери точности. А вариант (3) с возвратом именно double этой проблемы лишен.
ppf4td_get_double()
(сейчас она пуста). А уж она чтоб
понимала арифметические выражения, если вначале стоит '('.
...кстати, сей момент даже ppf4td_get_int()
нигде не
используется... 02.06.2018: заиспользовал --
histplot.c.
Пока что придётся везде в тексте devlist-файлов использовать заранее вычисленные выражения. Но это дико неудобно и, видимо, надо будет сделать по варианту (3).
26.12.2015: "алгоритм Дийкстры" -- "алгоритм сортировочной станции", Shunting-yard algorithm.
07.04.2020: приступаем к реализации
ppf4td_get_double()
по алгоритму Дийкстры.
get_one_double()
, используемую как для добычи числа-компонента
формулы, так и для всего результата вызова в случае, если первый символ НЕ
является скобкой '(' (т.е., это просто одиночное число, а не
формула).
strtod()
.
strtod()
.
dblp_peekc()
и dblp_nextc()
(они почти близнецы),
выполняющих все эти проверки и возвращающих уже готовый код результата
-1/0/+1 (ошибка/EOF/OK) и символ.
Это аналоги Cdr'овских/cxsd_db'шных PeekCh()
и
NextCh()
.
dblp_check_eol()
, также сделанная/скопированная
по аналогии, так и не используется.
CHECK_BUF_USED
.
PPF4TD_EFLOAT
, "Floating-point
number expected".
dblp_peekc()
/dblp_nextc()
:
PPF4TD_FLAG_internal1
, чтобы внутри
ppf4rd.c иметь отображающийся на него
PPF4TD_FLAG_EOL_ERR
-- этот флажок символизирует, что EOL
следует воспринимать как ошибку.
Например, внутри формулы перевода строки быть не может, а должна быть закрывающая ')'.
Или внутри чисел куча такого -- например, после "0x" или после "e" должно идти хоть что-то.
12.04.2020: за прошедшие несколько дней допилено до рабочего состояния. Тут основная работа шла уже над реализацией собственно "алгоритма сортировочной станции".
И с ним-то возни было о-го-го, а полноценный со всеми фишками -- сил/терпежу не хватило окучивать.
Возможно, подразумевается, что там всё разделяется пробелами. Либо что парсить готовую строку в памяти несложно.
У нас же -- входной именно ПОТОК, так что нельзя произвольно заглядывать вперёд и при надобности сдавать назад, а нужно корректно распознавать всё сразу по мере поступления.
Для этой проблемы придумано решение: булевский флаг num_exp
,
своей взведённостью указывающий, что дальше должно быть число или
открывающая скобка '(', а сброшенностью -- что, наоборот, должен
быть оператор или щакрывающая скобка ')'. При несовпадении
увиденного с ожидаемым генерится ошибка PPF4TD_EEXPR
(см.
ниже).
Исполнение требует наличия стека -- вот и используется ровно тот же
istk[]
, что при парсинге.
Ну а само исполнение несложнО -- оно аналогично исполнению формул в cda;
точнее, примерно как упрощённый вариант из старого CX
(CalcExpression()
включая oldcx, а, возможно, и чего-то ещё
более старого).
В описании всё сказано очень общо; в частности, основное внимание уделено парсингу, а "исполнение" формулы никак не раскрывается. Очевидно, потому, что задача алгоритма -- преобразование из обычной инфиксной нотации в обратную польскую запись (RPN), а уж обработка последней является отдельной задачей.
typedef struct { double v; char t; } dbl_elem_t;
Т.е., элементы содержат код операции и параметр-число. Коды являются наиболее понятными символами -- прямо отражающими операции:
OP_PUSH
;
применяется в выходной очереди и в стеке в момент вычисления (в момент
парсинга числа в стек не попадают, а идут сразу в выходную очередь; стек же
только для операций).
В стеке для собственно парсинга числа не используются -- туда попадают только операции. Но числа используются, когда тот же самый стек используется при исполнении формулы для вычисления значения.
istk[]
, указатель стека --
istk_ptr
.
Для простоты/удобства растёт снизу вверх (а не сверху вниз, как обычные стеки в процессорах).
Но в остальном он как обычные стеки: ПРЕДмодификация указателя при запихивании в стек, т.е., istk[istk_ptr] -- элемент на верхушке стека.
Т.е., пустой стек -- istk_ptr=-1 (но при парсинге он сразу же делается =-1+1, поскольку в него сразу же помещается первая открывающая скобка).
outq[]
, счётчик объёма --
outq_used
.
Растёт линейно снизу вверх (пустая -- outq_used=0).
PPF4TD_EEXPR
"Syntax error in expression" и
PPF4TD_ECMPLX
"Expression is too complex" (это, например,
слишком большая глубина вложенности скобок).
Пара сопутствующих аспектов:
ppf4td_get_double()
.
Воизбежание всё окружено проверкой "#if MAY_USE_FLOAT
",
который при неопределённости ставится по умолчанию в 1
, а
конкретно в hw4cx/x-build/arm-linux/Makefile форсится в
0
.
14.04.2020: сделано и использование в libCdr и libcxsd. Формально это должно бы описываться в ихних разделах, но тема очень близкая к тутошней, плюс оба случая очень похожи и имеет смысл рассказать вместе.
Пара общих замечаний:
ppf4td_get_double()
сделано условным -- по
MAY_USE_PPF4TD_GET_DOUBLE
, которая по умолчанию теперь
1
.
Из-за большого количества #if
'ов код выглядит страшновато,
но в будущем, после их устранения, он станет проще по сравнению с прежним.
09.04.2021: да, до-за-условливался тогда -- в парсинге
диапазонов после проверки, что следующий после 1-го числа символ является
'-', было забыто его "потребление" вызовом
ppf4td_nextc()
, из-за чего вылетала ошибка
PPF4TD_EFLOAT
. Исправлено, теперь диапазоны опять парсятся
нормально.
Итак:
DBL_fparser()
,
RNG_fparser()
, PARAM_fparser()
.
ParseSomeProps()
.
ppf4td_get_double()
по результатам
реализации первых использований: теперь в начале пробелы пропускаются только
при ОТСУТСТВИИ флага PPF4TD_FLAG_SPCTERM
.
Цель в том, чтобы спецификации вида "123-456" были бы валидны, а ""123- 456" -- нет.
Это, кстати, аналогично прочим ppf4td_get_*()
-- те вообще
НЕ занимаются пропуском пробелов, поскольку это забота программы-клиента.
Здесь же ppf4td_skip_white()
была добавлена потому, что по
описанию формата в man'е по strtod()
значится "optional
leading white space as recognized by isspace(3)".
Для тестов числа в разных точках его заменялись на скобочные выражения.
16.04.2020: замечание по мотивам: теперь, по опыту реализации, появилось чёткое понимание того, как можно реализовать парсинг выражений в cda_f_fla.
20.04.2020: неа, всё совсем не так просто/очевидно. Реализация разбора синтаксиса выражений -- лишь часть проблемы. А нужно продумать весь синтаксис "языка" (там ещё ссылки на каналы; плюс, как увязывать это всё с парсингом команд).
Да и нет в этом полноценном формульном языке уже такой острой необходимости, как была во времена CXv2 в начале 2000-х: хоть сколько-то сложный скриптинг теперь продвинутые юзеры (ЕманоФедя и Виталя Балакин) делают у себя на Python, а для простых-средних вещей имеющийся ассемблер "_all_code" уже достаточно приемлем.
Так что оставляем мысль заняться реализацией полноценного языка формул в cda_f_fla и займёмся лучше локингом каналов -- оно намного полезнее и важнее.
...чисто теоретически для конкретно auxinfo вроде бы правильный вариант
-- использовать не ppf4td_get_string()
, а что-то
специализированное, этакое "parse_to_EOL". Но тогда теряются плюшки вроде
парсинга \-последовательностей.
21.12.2015: сделано -- добавлен флаг
PPF4TD_FLAG_IGNQUOTES
(=1<<1, следующие флаги (*TERM)
подвинуты).
Наверное, это было внесено либо при добавлении фичи «после "\NL" сжирать также все последующие пробелы...» (26-12-2015...28-12-2015 в разделе по Cdr_file_via_ppf4td), либо при разборках с #line-после-"\NL" (25-07-2014...04.08.2014).
В любом случае, нужно разбираться -- тупо отлаживать код (а это адская работёнка -- шибко уж он умный/элегантный/неочевидный).
ppf4td_get_string()
--
"выкидывай результаты, никуда не складывая, и пофиг на длину".
Нужно для histplot.c, чтения из файла, чтоб пропускать куски текста, не страдая эмуляцией поведения ppf4td в плане кавычек и бэкслэшей.
30.05.2018: делаем.
PPF4TD_FLAG_JUST_SKIP
=1<<31.
for()
и 2) запись
'\0' после него.
ppf4td_get_ident()
и
ppf4td_get_string()
.
И не сделать ли, чтоб mem:: и подобные НЕ-внешние схемы нельзя было указывать (из командной строки и прочих user-controlled-мест)?
Например, ввести в ppf4td_vmt_t
поле-флажок, означающий
"использовать эту схему только при указании её в
ppf4td_open()
.def_scheme
, а при указании в
.reference
чтоб она (get_plugin_by_scheme()
'ом?)
запрещалась бы.
29.05.2018: да нет, не нужно.
Дело в том, что в схеме mem:: строка указывается прямо после "::", и именно в виде строки. Так что указать "чёрт-те что" просто не удастся.
А вчера у меня в голове, видимо, был вариант либо "mem::0xADDRESS", либо дуплет scheme,path (где path -- тоже адрес).
Короче:
ppf4td_get_int()
: там парсинг выполнялся сразу
через _nextc(), вместо надлежащего "_peekc(), и если символ наш, то
_nextc()".
В результате из цепочки "12345.678" первый же вызов сжирал "12345." -- возвращал-то он 12345, но '.' уже исчезала, что ломало всю схему вызывающему.
Исправлено на надлежащий вариант.
P.S. Извиняет лишь то, что до сегодня ppf4td_get_int()
не
использовался вовсе. А сейчас занадобился в histplot.c для
парсинга времён -- SECONDS_SINCE_1970.MILLISECONDS.
ppf4td_get_int()
: числа, начинающиеся с '0', всегда
воспринимаются как восьмеричные, но это некорректно: с нолика могут
начинаться zero-padded десятичные -- вроде миллисекунд. На них и накололся:
в строке вида "123.068", когда числа парсятся раздельно, "068"
воспринималось как восьмеричное "06", а '8' его частью уже не считалась и
выглядела для клиента как "отдельный, следующий токен".
18.06.2018: исправляем.
strtol()
, указывать желаемую
базу либо 0
, означающий "можешь брать префикс".
Поскольку использование ограничивается histplot.c, то так и поступим.
defbase
.
ppf4td_get_int()
-- для будущего использования в
ParseSomeProps()
, где нужно будет в зависимости от типа парсить
либо double, либо целые, плюс отдельным вариантом int64.
(Сейчас там диапазоны парсятся сразу чтением всего A-B в строковый буфер,
а потом strto*(), и конкретно для int64 используется
strtoull()
, БЕЗ разделения по знаковости.)
Очевидно, что реализуется просто копированием
ppf4td_get_int()
с заменой типа переменной.
27.12.2019: делаем. Несколько процедурных замечаний:
ppf4td_get_quad()
", но
из-за следующего пункта признана не очень удачной; взято название
ppf4td_get_ll_int()
.
int64
, но
ppf4td -- штука отдельная от СУ и misc_types.h (хотя реально она его #include'ит, из-за
misc_macros.h, где memset32()
требуется тип
int32
).
Поэтому сейчас там используется тупо long long int
(шьорт
побьери!!!).
#if MAY_USE_INT64
". Хотя по-хорошему -- нужна бы.
Итого -- сделано. Хотя, конечно, пока никак не проверено.
09.01.2020: а впрочем -- какого чёрта?! Пусть будет именно "quad" и "int64"!
#include "misc_types.h"
.
int64
.
ppf4td_get_quad()
.
06.07.2014@пляж: реализовать это можно, сбагривая препроцессору (запущенному с параметром "-", чтоб брал с stdin'а) эти интегрированные строки через pipe.
06.07.2014: а надо ль так заморачиваться? Ведь можно пропустить экраны через препроцессор ПЕРЕД интегрированием -- тогда хватит и нынешнего mem::.
Разница, конечно, в МОМЕНТЕ препроцессирования (и что уже никакие параметры at run time не подставишь). Но будет ли это хоть где-то важно?
06.07.2014: одна тонкость -- при таком "препроцессировании заранее" желательно всё-таки включать в ppf4td_mem.c фичу "воспринимать номера строк". А какой вариант -- m4 или cpp?
Пока сделано m4 -- PPF4TD_LINESYNC_HLIN
.
Короче -- скорее всего, всё сделать можно. Но может понадобиться Win32-specific реализация ppf4td_pipe.c.
include()
НЕ пытается
искать файл в той же директории, что исходный файл (а cpp -- ищет).
Так что надо б ему дополнительно сбагривать ключик -Idirname.
09.07.2014: да, сделано. При найденности
strrchr()
'ом символа '/' в reference оно
добавляет в командную строку еще {"-I", dirname(reference)} (только
"dirname" оно делает вручную).
06.09.2015: и еще штука: очень некрасиво, что нынче директория *pult/configs/ забардачена файлами *.devtype.
Вот если б можно было к путям поиска добавлять еще "-Idirname/types"...
А желательно бы для каждого ТИПА файлов (а не схемы препроцессора!) иметь возможность указать include-директорию, чтоб она тоже добавлялась с префиксом -I.
Кстати, "-I" и путь можно указывать раздельно.
Проблем 2:
ppf4td_m4_open()
уже добывается).
13.05.2019: вот уже и ЕманоФедя выступает, чтобы devtype'ы лежали в отдельной поддиректории.
Самое простое -- сделать поддиректорию types/, складывать devtype'ы в неё, а во всех include() указывать types/ФАЙЛ.devtype. Но это неудобно.
Намного лучше -- чтобы на эту поддиректорию делалось -I. А возможно ли?
14.05.2019: посмотрел ещё раз -- неа, совсем НЕ просто.
После чего, собственно, и добрался до этой записи.
Вариант идеи, на будущее, в качестве расширения API -- чтобы в
ppf4td_open()
указывался бы NULL-terminated vararg-список
строк, передаваемый далее в реальный "открыватор". А уж тот бы как-нибудь
его использовал; в частности, в данном случае -- как список
include-директорий.
Отдельный вопрос -- КАК интерпретировать эти директории? По-хорошему, надо анализировать: если это НЕ абсолютный путь (т.е., не начинается со '/'), то надо каждый из тех элементов префиксировать "базовой директорией" (в которой сам исходник).
Да, это всё очень, ОЧЕНЬ муторно: и потенциально неизвестной длины массив
для cmdline[]/argv[], и сами строки (которые могут быть до
[PATH_MAX]
) менеджить...
07.01.2019@дома-утро-душ: а что, собственно, мешает в момент нахождения конкретного файла кроме -I,BASEDIR также добавлять и -I,BASEDIR/SUBDIR?
08.01.2020@дома:
@утро-душ:
CxsdDbLoadDb()
и CdrLoadSubsystem()
-- до
собственно ppf4td_open()
.
Между (b) и (c) выбор простой -- по опыту findfilein()
, надо
именно colon-separated-список -- с ним намного удобнее обращаться.
P.S. Можно также ввести некоторый "язык": например, если путь в
path_info
начинается со '/', то не прицеплять его к
basedir, а считать абсолютным; также можно поддерживать префиксы
'!', '$' и '~' - как в
findfilein()
'е, плюс пусть '/' понимает как абсолютный
путь.
...а с другой стороны -- да нахрена?!
Впрочем, эта проблема снимается, если обойтись ОДНИМ дополнительным путём -- тогда и менеджить нечего и просто заводится в стеке дополнительный Ipath2[PATH_MAX].
Далее работы в течение дня:
ppf4td_open()
вызывается непосредственно из
cxsd_db_file_ldr.c::file_ldr()
.
CdrLoadSubsystemViaPpf4td()
; вот в него можно спокойно
вставлять "добавить в список путей поддиректорию include" (если надо).
ppf4td_open()
всё
очень просто, плюс там НЕ требуется никаких дополнительных
include-поддиректорий.
А дальше уже (2) адаптировать к нему юзеров и, в конце, (3) добавить собственно использование.
Реально это, конечно, вряд ли станет проблемой -- т.к. де-факто у нас не используется ничего, кроме "m4:".
Но если вдруг дойдёт до РЕАЛЬНОГО распространения иных схем, и несоответствие параметров станет проблемой -- тогда можно как-то тэгировать: например, префиксом "<m4,cpp>" (что будет означать "этот путь имеет смысл для схем m4 и cpp"; пре передаче плагинам префикс, естественно, должен убираться).
Теперь надо все Makefile'ы переделать, чтобы .devtype-файлы клались в configs/types/.
09.01.2020: доделываем:
EXPORTSDIR
переделано с configs на configs/types.
Замечание: в ppf4td_cpp.c это НЕ скопировано. И за неиспользуемостью оного, и потому, что у CPP несколько иные правила указания include-файлов -- там обычно именно поддиректорию явно указывают в имени.
Требуется это:
Никакого препроцессирования/макрорасширения в с такими источниками из памяти и не будет, но в большинстве случаев оно и не особо надо.
07.10.2013: собственно РЕАЛИЗАЦИЯ проблемой не является (вообще простейшее занятие).
А проблема -- как УКАЗЫВАТЬ адрес в памяти. Ведь pointer в строку не запихнёшь, в т.ч. даже в HEX-виде как-то страннокриво (размер может быть разным -- как минимум 32 и 64 бита). Или рассчитывать на "%p" и делать printf()/scanf()?
14.10.2013: о -- а может, строка и должна идти прямо сразу после префикса "frommem::"/"immed::", т.е., "имя" и будет текстом для парсинга.
Весьма удобно -- поскольку просто указывается def_scheme="!immed" и reference=текст (и не придётся конкатенировать строки "immed::"+текст), и даже тупо "имя файла" уже автоматом будет самоописывающим.
А вариант "%p" можно реализовать отдельно.
15.10.2013: да ну, совершенно "%p" не нужно! Прямое указание с префиксом '!' решает все проблемы.
Неясно только, когда и для чего этот префикс был введён ещё в smp4td -- не записано нигде (изобретено в Cdr_treeproc 19-01-2010). Но подходит идеально.
Пятнадцатью минутами позже: плагинчик ppf4td_mem.[ch] сделан. Теперь проверить.
22.10.2013: да, проверено на "cxsd -e CONFIG_COMMAND" -- работает.
27.08.2008: главный пользователь -- CX-сервер, сие есть преемник CXv2'шного cxsd_loader.c. Одной из целей остается так и не реализованная в CXv2 возможность статической сборки, когда все требуемые модули влинковываются статически, а loader находит их у себя в таблице и отдает сразу. Другая "цель" -- что под Win32 он будет понимать .DLL, прозрачно для остальных частей системы.
И, поскольку у сервера будет компонент "Channel Manager", линковабельный и в монолитных клиентов, то очевидно, что надо сделать этот module loader отдельной библиотекой, доступной не только серверу. Более того - это должна быть useful/-библиотека, без привязки к CX.
Отдельный вопрос: а может, вообще как cxscheduler - даже не одна
библиотека, а несколько вариантов реализации API, для разных платформ?
Типа да, но в рамках одного исходного файла, просто как-нибудь
(#include
+#define
из других файлов?) будет
отключабельной кусок, обеспечивающий собственно динамическую загрузку.
А "внутреннее устройство" будет таким: 3 стадии поиска модулей:
realloc()
-- таблица будет
связанным списком, с элементами, добавляемыми программой-"клиентом").
ЗЫ: это покамест только записано сюда, но ничего еще не начато.
ЗЗЫ: там же надо будет как-то указывать список путей для поиска
.so'шек, а сам поиск будет на основе
vfindfilein()
'а.
27.01.2010: начато реальное создание модуля. Живет
в useful/, а назван cxldr вместо cxloader -- чтоб больше
отличался от cxlogger'а. Префиксы -- cxldr_
и
CXLDR_
.
03.02.2010: первоначальный вариант сделан. Highlights/features:
malloc()
'а, можно при определении контекста указывать
[статически-отведенный] буфер для списка используемых модулей (и его
размер): в этом случае она складывает информацию только в него, а при
заполнении -- обламывается.
cxldr_used_rec_t.name
? Ведь для malloc-лишенных
систем это важно.
Так вот: для builtin-модулей оно просто ставит ссылку на
cxldr_bltn_rec_t.name
, для загруженных же -- делает
strdup()
, полагая, что уж там-то динамическая память есть.
ref_count
до 0
оно честно пытается освобождать все занятые модулем ресурсы -- и память
под имя, и собственно загруженный модуль.
Требуются некоторые доработки:
argv[0]
-- нужен для
findfilein()
.
RTLD_GLOBAL
.
dlopen()
отдельно
не требуется -- во-первых, достаточно указать NULL в качестве пути, а
во-вторых -- конкретно под CPU_MCF5200
оно
вы-#if
'ивает динамическую загрузку вовсе.
dlopen_checker()
'у
в privptr'е не суффикс, а указатель на некую структурку, содержащую
разные параметры.)
Вообще, нынешняя реализация cxldr_get_module()
выглядит затолплённо и уродливовато -- видимо, из-за большого
количества #if
'ов и goto
-вниз.
04.02.2010: доработано:
CXLDR_FLAG_NONE
, он используется.
07.02.2010: добавил к
cxldr_get_module()
и cxldr_checker_t
параметр
privptr
-- для того, чтобы можно было для разных видов
модулей использовать один и тот же checker; для init и fini вроде пока
не требуется.
Сейчас используется для cxsd_loader.[ch]
-- там у всех
модулей одинаковые заголовки, а в privptr'е передаётся указатель на
структурку CxsdModuleDesc_t
, содержащую название вида,
magicnumber и version.
14.02.2010: да, ОЧЕНЬ НАДО УМЕТЬ ПЕРЕДАВАТЬ ОПИСАНИЕ ОШИБКИ!!!
Иначе никакая внятная диагностика невозможна...
init()
и
term()
при 0->1 и 1->0. Кто и как?
30.08.2008: Варианты:
init()
или term()
. Но с term()
тут засада - вызывать-то его надо еще ДО dlclose()
, иначе
модуль будет уже выгружен и вызвать станет физически нечего.
29.01.2010: да, сделано по 2-му варианту.
dlsym(RTLD_DEFAULT,...)
'30.08.2008: Стоп!!! А насколько это осмысленно?
Ведь заранее-влинкованность нужна в основном в статически-собираемых
программах, где никакого dlsym()
'а не будет.
Так что - увы, withdrawn, а модули регистрировать все-таки придется.
Это может несколько упростить процедуру "указания списка статически влинкованных модулей для конкретной сборки".
28.01.2010: да, именно так и реализовано,
"объекты" имеют тип cxldr_context_t
, а определяются и
заполняются в программе макросом CXLDR_DEFINE_CONTEXT
.
Весьма удобно во всех отношениях -- на каждый "подвид" модулей
(драйверы, layer'ы, ...) имеется свой контекст.
dlopen()
теперь зарыт глубоко, то
теперь нет диагностики в случае проблемы не с нахождением/открытием модуля,
а с тем, что ему не хватает каких-то символов в хост-программе.
Так что разобраться в проблеме бывает очень затруднительно --
приходится в dlopen_checker()
'е включать отладочную
печать.
09.01.2015: да, проблема очень неприятная -- сегодня вылезло, что на x86_64 пыталось использовать _lyr.so от 32-битной системы (не пересобранный), и понять причину было крайне сложно.
Мож расширить интерфейс, чтобы наверх кроме факта ошибки
возвращалась бы еще некая строка -- точнее, чтоб "вниз" передавался
char**
, в который низкоуровневые процедуры могли бы класть
указатель на static-буфер, куда уж сами бы клали описание, например, от
dlerror()
'а?
17.07.2015: и сегодня опять -- не хватало cda'шного символа в cxsd (cda не была прилинкована).
Еще вариант решения (точнее, компонент его): как-нибудь возвращать строку ошибки наверх ВСЕГДА, а уж печатать/логгировать ли там её -- пусть определяется ключиком командной строки (в сервере -- например, уровнем от -vN), чтоб при возникновении проблемы можно было оперативно включить и увидеть.
09.12.2015: еще "радость":
cxsd_modmgr.c::CxsdLoadDriver()
рассчитано на идею
"логгировать ошибку при r>0, а при <0 считать, что "error is already
logged". Так вот, это не так -- cxldr_get_module()
ведь ничего
никуда не логгит! В результате отсутствующие драйверы в логах никак не
заметны...
22.12.2015: проблема решена.
cxldr_get_module()
добавлен параметр
char **errstr_p
.
dlopen_checker()
'у теперь передаётся не ctx напрямую, а
структура-дуплет указателей: ctx и char*errstr; последняя инициализируется
NULL'ом.
dlopen()
делает
li->errstr=dlerror()
.
CxsdModuleChecker_cm()
'е, при логической ошибке)),
...хотя тут бы теперь можно и через errstr передавать сообщение от dlerror().
cxldr_get_module()
изо всех функций вытащен в общую обёртку
DoLoadModule()
и уж в неё напичканы все эти условия.
Засим считаем за "done".
dlopen()
, если после проблемного .so-файла по пути
поиска пробуются другие, и ошибка (ENOENT) возвращается только на
последний" решена радикально.
01.09.2019@дома: всё очень
просто: обычно закомментированная (и раскомментировываемая на время поиска
ошибок) в dlopen_checker()
выдача на stderr результата
dlerror()
теперь раскомментирована, но сделана условной по
переменной окружения $CXLDR_DEBUG
, значение которой должно
начинаться на "1", "y" или "Y".
Таким образом, для печати информации по всем пробуемым .so-файлам нужно запускать программу так:
CXLDR_DEBUG=1 программа...
Проверено -- ОЧЕНЬ удобно.
При компиляции cda_d_tango.c с помощью gcc-10.2.1 20210110 под Debian-11.8 вылезла ошибка:
../../../4cx/src/../exports/include/cdaP.h:193:5: error: narrowing conversion of 'CDA_DAT_P_MODREC_MAGIC' from 'unsigned int' to 'int' [-Wnarrowing] 193 | } | ^ cda_d_tango.cpp:1141:1: note: in expansion of macro 'CDA_DEFINE_DAT_PLUGIN'
Дело в том, что там присваивается значение
CDA_DAT_P_MODREC_MAGIC
=0xCDA0DA1A полю
cda_dat_p_rec_t.mr.magicnumber, которое
cx_module_rec_t.magicnumber
, которое имеет тип
int
.
Сходу-то решение просто -- поле переделано из int
в
unsigned int
, после чего собралось.
Но надо бы разобраться получше.
15.11.2023: стал разбираться.
unsigned char
.
Полезно чтение обсуждения, включая ссылку на стандарт.
Презанимательнеешее чтиво, показывающее кучу дурацких косяков в C/C++.
Особенно насчёт char
'ов.
А ведь '\x80' -- абсолютно легитимное значение для символа/байта.
0x8000
-- абсолютно легитимное значение как для
int16 (short), так и для uint16 (unsigned short), это любой железячник
знает.
(Тем более, что его в варианте "-0xnnnn"
записать невозможно (разве что -0x8000
, что откровенный бред),
а главное -- просто глупо и бессмысленно, так никто никогда не делает.)
There are no negative integer literals. Expressions such as -1 apply the unary minus operator to the value represented by the literal, which may involve implicit type conversions.
В результате у них 0x8000 и -32768 -- не факт, что одно и то же.
И как вообще возможно -32768 в виде 16-битного -- загадка: ведь в соответствии с процитированным, это -(32768), но 32768 НЕ представимо знаковым в 16 битах и потому будет продвинуто до 32. Или раз "no negative integer literals", то оно таки в 16 бит влезет? Б-р-р-р...
U
для указания
на беззнаковость существует, то суффикса S
для указания
*знаковости* нет.
Видимо, потому, что ПО УМОЛЧАНИЮ всё считается знаковым, блин...
19.11.2023: ладно, с непосредственной причиной проблемы -- глупое "улучшение" стандарта -- разобрались. Надо придумать наиболее корректное решение и воплотить его.
Напрашивается именно сделать всё это повсеместно unsigned
, и
даже форматы печати (где печатается) поменять с "%d" на
"%u". Воплощаем.
int
в unsigned int
поля cx_module_rec_t
:
magicnumber
и
version
(это за компанию, да и по смыслу --
отрицательные версии незачем).
Дата на файле была 14-02-2010...
За компанию и в v2'шном cxdata.h::subsysdescr_t
те
же поля аналогично.
cx_module_desc_t
(это "вторая половинка" -- образец для
сличения).
init_builtins_err_notifier_t
(смысл его описан в
этом файле за 24-11-2016, ключевое слово "err_notifier"), и у неё параметр
magicnumber
, также об-unsigned
'нен.
Хотя оно вроде реально не использовалось, т.к. единственный вызов
соответствующего/такого InitBuiltins()
-- в cxsd.c, в
виде InitBuiltins(NULL)
.
ЗЫ: файлы *_builtins.c генерятся скриптом mkbuiltins.sh, в нём реально и "код".
unsigned
'нены
CxsdDriverModRec.layerver
и
CxsdLayerModRec.api_version
-- они тоже парные.
И даже v2'шный (в нём только layerver
).
version
'а, то делаем
unsigned
и входные параметры, и возвращаемые значения в
inline-функциях CX_MAJOR_OF()
, CX_MINOR_OF()
,
CX_PATCH_OF()
и CX_SNAP_OF()
, плюс параметры
that
и my
в
CX_VERSION_IS_COMPATIBLE()
.
И в 4cx/src/include/, и в cx/src/include/.
На них на обоих дата была 31-10-2008...
Всё собралось. Сравнение логов сборки до этого изменения и после никаких различий не показывает (помимо ухода той ошибки, из-за которой весь этот раздел и появился).
Теперь можно ждать, когда же что-нибудь всё-таки вылезет :D
Живёт оно, как и предполагалось, в useful/. Файлы именуются sendqlib.[ch], используются префиксы sq_ и SQ_.
21.12.2009: несколько замечаний по ходу дела -- в основном отличия от v2'шного cankoz_pre_lyr и тамошнего проекта:
sq_eprops_t
.
sq_enq()
НЕ передаётся вся куча параметров
типа oneshot/callback/timeout, а передаются только элемент для
добавления и способ (how
). Параметры же берутся из
eprops. Вот в конкретных юзерах-wrapper'ах -- можно (нужно!) будет
параметры передавать явно.
eq_cmp_func
.
q_foreach()
умеет возвращать указатель на него --
через параметр found_p
.
Еще -- работа с
таймаутами покамест сделана напрямую через cxscheduler, что есть не
очень хорошо: "вышестоящая инстанция" (сервер), конечно, вызовет
драйверовый term_b(), что должно корректно всё высвободить, но сама-то
принудительно она подчистить все ресурсы драйвера не сможет. Надо б
обдумать какой-то способ -- то ли промежуточный слоёк, то ли магические
идентификаторы... В случае sendqlib'а, учитывая, что там всё равно при
sq_init()
делается уставка контекста -- вполне можно бы и
"методы" enq_timeout()/deq_timeout() предоставлять, которые вызовут уже
родной API сервера. Впрочем -- уставка/сброс таймаута сейчас сделан
парой внутренних функций
enq_tout()
/deq_tout()
, так что переделка
сложной не будет.
В остальном -- библиотечка вроде готова, надо проверять. (Простая
получилась -- жуть! В частности, за счет того, что покамест
sq_ers_n_sn()
-- erase_and_send_next -- отсутствует, ибо
возложена на юзера.)
22.12.2009: да, переделал архитектуру таймаутов
-- теперь нет ни прямой работы через cxscheduler, ни даже зависимости
от него, а уставка/уборка повешены на "юзера": при создании очереди ей
указывается пара методов tim_enq
/tim_deq
.
Ситуация, конечно, немного напоминает оную в CXv2, где драйверам давался ОДИН таймаут без идентификатора и добавление/удаление неявно работали с ним. Но, в отличие от драверов вообще, ОЧЕРЕДИ таймаут нужен всегда именно один (как и цыгановской камере), так что тут ограниченность (и следующие из неё простота и элегантность) -- к месту.
07.01.2010: поселил и в cx/ тоже -- будем испытывать эту библиотеку на cmltest_drv@Camilla.
11.02.2010: (уже давно ясно) вообще-то не будем -- оно там нафиг не нужно, т.к. драйвер напрямую пользуется CANHAL'ом, безо всякой буферизации. 20.04.2010: А вот на ottcam'е (драйвер для оттмаровской UDP-CCD-камеры) мы её поюзаем.
24.02.2010: насчет "SQ_REPLACE_NOTFIRST" -- там
ведь всё совсем не тривиально: sq_enq()
'у-то передаётся
ОДИН элемент, который используется И для сравнения, И для постановки в
очередь, а в случае "замени-если-не-первый" надо ДВА элемента:
сравнивать с одним (байт -- код команды), а заменять -- на другой.
27.02.2010(Обе мысли пришли в голову в душе -- вторая вчера, первая сегодня :-)): еще о "SQ_REPLACE_NOTFIRST":
elem_is_equal()
, так что
пакет сравнивался целиком (как, собственно, и требовалось).
sq_enq()
еще параметр -- "model", и в
поиске используем его вместо e
(а при model==NULL --
model:=e).
Второй вариант выглядит предпочтительнее -- поскольку он элегантнее.
10.06.2010: да, ввели
SQ_REPLACE_NOTFIRST
.
20.04.2011@Снежинск-каземат-11: реализуючи sq_ers_n_sn()
обязательно надо
не забыть сделать "новый" вариант
удаления-предыдущего-и-отправки-следующего -- ТОЛЬКО при совпадении
пакета в голове с указанным.
10.05.2011: угу, сделано в
cankoz_q_erase_and_send_next()
.
11.05.2011: сделана поддержка
SQ_REPLACE_NOTFIRST
: в sq_enq()
добавлен
параметр void*m
(m -- model), и доп.ветка, в которой при
foreach(SQ_FINDLAST)==SQ_FOUND найденный элемент заменяется на
указанный.
Да и в cankoz_lyr_common.c вся поддержка *enq*() (включая REPLACE и model_dlc) вроде сделана, так что пора проверять.
05.05.2012: в sq_enq()
было забыто
ring_used++
после добавления пакета... Естественно,
ничего никуда не отсылалось.
Короче -- это была первая реальная проверка (проведена на ottcamv_drv; предыдущие на nkshd485_drv и т.п. не делались). После исправления того бага -- вроде работает; но, конечно, требуется полноценная проверка на реально живых драйверах/железе.
16.05.2012@Снежинск-каземат-11: мелкое усовершенствование -- теперь от SQ_TRIES_DIR возвращается, была ли посылка успешной -- SQ_ERROR отдаётся, если sender вернул !=0.
22.06.2012: на sendqlib'е уже вполне успешно работает nkshd485_drv, так что почитаем за "done".
В таких случаях программа/очередь начинает бесконечно молотить этот пакет, на который никогда не будет ответа, и засим всё зависает -- поскольку следующие пакеты (теоретически могущие вывести из этого тупика) никогда не будут отправлены.
Можно ведь ввести в sq_eprops_t
еще одно
поле -- максимальное число повторов. Если оно ==0 -- то слать до
бесконечности, иначе же -- не более раз, чем указано (просто
декрементировать при каждой отсылке, а при ==1 -- выкидывать).
Сложность только в том, что сервер-то считает, что он отправил запрос -- значит, ответ придёт, и более запросов на этот канал слать не будет. А тут -- раз, и канал залипнет... Чтоб этого не происходило, sendqlib может при выкидывании вызывать sq_eprops_t.callback, а уж тот скажет серверу, что "упс..."
BTW, не ввести ли в API сервера в дополнение к ReturnChan*() еще и функцию "канал такой-то померять не могу" -- т.е., чтоб оно просто сбросило флаги запроса, а значение бы не трогало.
10.02.2010: кстати, в принципе на роль этого поля
отлично годится oneshot
: оно при использовании как раз =1,
что и означает "отправить 1 раз, после чего выкинуть из очереди".
10.06.2010: да, переименовали
oneshot
в tries
, плюс -- введены константы:
(прочие количества могут указываться просто числами).SQ_TRIES_DIR = -1, SQ_TRIES_INF = 0, SQ_TRIES_ONS = +1,
Замечание: для не-_DIR-посылок обломившаяся отправка НЕ ДОЛЖНА засчитываться и приводить к уменьшению числа использованных попыток и выкидыванию из очереди.
11.06.2010: дальше же надо реализовывать реальную
работу с этим нововведением. Покамест добавлено поле
sq_eprops_t.tries_done
-- с помощью которого и будет
подсчитываться, сколько сделано попыток.
А дальше надо модифицировать весь блок пере-посылки в соответствии со вчерашними исправлениями в старом cankoz_pre_lyr.c.
23.06.2010: да, модифицировал.
За компанию реализована и поддержка SQ_TRIES_DIR
-- оно
просто сразу вызывает отправку пакета q->sender()
'ом и
отваливает, даже не проверяя результата. 16.05.2012@Снежинск-каземат-11:
теперь проверяет и возвращает.
15.05.2012@Снежинск-каземат-11: касательно
perform_sendnext()
:
Может, поменять местами сравнения tries_done==1 и ==epp->tries, чтоб у SQ_TRY_LAST приоритет был выше? Да, тогда у ONS-пакетов всегда единственным "разом" станет LAST вместо FIRST, но что в этом плохого?
11.04.2014: да, ровно так и есть -- ONS-пакеты не выкидываются из очереди (вылезло на первом же их юзере -- slbpm_drv).
В качестве решения вместо "поменять местами сравнения" пока что
добавлено альтернативное условие для удаления --
tries==SQ_TRIES_ONS
. Проблему это решило.
А симптомы проявления проблемы выглядели неожиданно. Казалось бы, должно было слать этот пакет до посинения (ведь удаления его из очереди нигде не было); но нет -- через какое-то время бардак прекращался, причём некоторые каналы успевали проморгнуть болотным статусом OFFLINE. Как выяснилось, оно и молотило, но из-за бага у Стюфа эта команда (чтение осциллограммы, SLBCMD_RDOSC=0x01) приводила к отправке самых бредовых пакетов, и в какой-то момент по совпадению это оказывалось якобы чтение регистра 0, хранящего сигнатуру "настроенности"; естественно, значение не совпадало, и перед ре-инициализацией устройства делалась также очистка очереди.
sea_step_t.chkdelay
в seqexecauto).
11.06.2010: реально оно нужно ТОЛЬКО для _ONS- и _DIR-посылок. Смысл -- если мы знаем, что после этого пакета устройство некоторое время занимается своими делами, и ему просто незачем пытаться слать что-то еще. Так что -- можно поступить следующим образом:
.timeout_us
не нужен, то для них используем это поле как
раз в качестве "задержка до следующей посылки".
По умолчанию оно как раз ==0 -- т.е., задержка отсутствует.
sq_q_t.timeout_us
-- т.к.
ошибка относится именно к очереди вообще, а не per-item.
23.06.2010: да, пункт "для таймаута-для-перепосылки при ошибке отправки надо вообще ВСЕГДА использовать именно sq_q_t.timeout_us" исполнен, в рамках реализации предыдущего раздела.
06.07.2010: да, и концепция "минимальная задержка до следующей посылки" также реализована. Это оказалось сравнительно просто --
timeout_us
мы этот таймаут уставляем.
sq_enq()
для _TRIES_DIR-пакетов, при
условии, что сейчас таймаута нету (это реально подстраховка
-- т.к. _DIR-пакеты, да еще и с принудительной паузой, по-хорошему
должны бы использоваться только при пустой очереди).
perform_sendnext()
теперь
прерывается не только при опустошении очереди, но и при наличии
таймаута.
sq_enq()
вызов sendnext() делается
при условии не только прежде-пустой очереди, но и при отсутствии
таймаута (но это также перестраховка -- предыдущего пункта по
идее достаточно).
tout_set
.
Итого -- теперь осталось только проверить (а на чём? :-)).
А надо иметь sq_-вызов "дай указатель на n-й элемент".
28.10.2011@Снежинск-каземат-11: сделан
sq_access_e()
, возвращающий таковой указатель (или NULL при
n>=ring_used).
И _erase_and_send_next()
'ы в nkshd485_drv.c с
cankoz_lyr_common.c переведены на неё.
Пока не проверено, конечно, но они оба и не были проверены, так что спокойно ставим "done".
10.10.2012: обсуждение:
piv485_sender()
относится к компетенции PIV485, а заскоки
КШД485 -- к его драйверу.
Так что надо в layer-API вводить callback, вызываемый из sender()'а для проверки -- в нём и будет махинироваться.
16.10.2012@Снежинск-каземат-11: вроде сделано.
pivqelem_t
-- который приватен для layer'а).
nkshd485_md()
проверяет, что если
cache[NKSHD485_CHAN_STATUS_BYTE]
(а именно там -- последнее
известное значение статуса) отсутствует бит по маске 1 (READY), то некоторые
команды меняются на GET_STATUS.
Команды -- GO, GO_WO_ACCEL, SET_CONFIG, SET_SPEEDS, SWITCHOFF, SAVE_CONFIG, GO_PREC.
08.11.2012: проверено -- работает, кнопки и прочее синеть перестали.
sq_ers_n_sn()
удалён.
sq_pause()
. Она очень простая -- одна строчка, просто вызов
enq_tout()
.
Резоны -- в bigfile-0001.html за вчера и сегодня в разделе "CAN".
try_n
передаётся
SQ_TRY_LAST).
Сделано для удобства реализации команд работы с таблицами в cankoz-драйверах (конкретно сейчас -- cdac20k). Там и без этого можно было обойтись, но с этим красивее.
Ну и из общих соображений так правильнее: нет исключений, всегда поведение одинаковое -- вызываем callback после успешной отправки в первый/последний раз.
(Если бы не это -- то можно б было просто слать ВСЕГДА посылки, состоящие из 2 "пакетов" -- УСТАНОВКА_АДРЕСА,КОМАНДА.)
Т.е., главная идея для корректной работы -- что после отправки адреса надо "взять паузу" и не слать более ничего, а потом -- сразу же сопутствующий пакет с данными.
Собственно: а можно ль такое реализовать?
15.12.2022: вопрос -- КАК?
Т.е., во-первых, после первой посылки делать паузу, а во-вторых -- что и отличает это от обычной работы -- не переходить к следующей очереди данного порта, а слать следующую посылку из текущей.
Но тут будет дикая неоптимальность: перед каждой командой станет тратиться 1 секунда на переключение адреса.
Очевидным "победителем" выглядит 2-й вариант, но как его реализовать, чтобы одна очередь не могла бы навечно захватить порт?
Например, в момент "захвата" запоминать текущее количество команд в очереди, рассматривая их как "пачку"/"транзакцию", и переходить к следующей очереди порта по исчерпании этого количества -- даже если в очереди за время, прошедшее с её "захвата", появились ещё посылки.
Обсуждение:
Для чего оный "текущий адрес" нужно будет помнить.
...или сделать это "плагином" -- набором "методов" в
sq_port_t
?
Понятно, что реализовать это можно. НО: выглядит монструозненько, текущую хоть сколько-то простую/стройную логику работы sendqlib'а это сильно нарушит (и явно сильнее, чем 10 лет назад при добавлении "портов"). Поскольку бонусов СЕЙЧАС ноль, а усложнение/об-уродливание очень серьёзное, да и не хочется повторять судьбу омонструозившегося AsynDriver, то просто оставим записанным проект, а делать ничего не будем. (Если припрёт -- подумаем ещё раз.)
08.06.2010: смысл -- для serial-линий с МНОЖЕСТВОМ устройств.
Т.е., например, 32 аскольд-волковских контроллера на RS485; для каждого из них есть своя sendqlib-очередь, но линия-то ОДНА и НЕРАЗДЕЛЯЕМА, так что:
Таким образом --
Непонятностей же на данный момент три:
sender()
'ом (являющейся реально лишь постановкой
задания сериализатору) и реальной отправкой пакета в линию может пройти
произвольное время, а таймаут надлежит считать именно с момента РЕАЛЬНОЙ
отправки. 17.05.2012@Снежинск-каземат-11: не-не-не, какая там
«вызовом "отправки" sender()
'ом» -- нет!
Именно ОТПРАВКА sender()
'ом и будет настоящей, от которой надо
считать время. Но до неё будет доходить дело не сразу после попадания
пакета в голову локальной очереди, а когда еще и планировщик допустит до
реальной отправки в порт.
15.09.2011: по размышлению напрашивается 2 глобальных подхода:
Простое объяснение -- потому что это "натуральное" место для него.
Более сложное: ключевым является п.2 из вышеприведённого списка -- таймауты. ТУТ НАДО НАПИСАТЬ КАКОЕ-ТО ВНЯТНОЕ ОБЪЯСНЕНИЕ (типа что функционал sendqlib и serial-scheduler слишком взаимосвязан, и попытка его разделить по двум библиотекам приведёт к слишком бардачному и плохоизолированному коду).
Возможная реализация:
(*) Насчет "как" организовывать кольцевой список:
next_to_send = next_to_send->next; if (next_to_send == NULL) next_to_send = first;
Замечание: приведённый алгоритм -- это для простой очереди, БЕЗ приоритетов.
16.09.2011: короче -- пока что реализовываем nkshd_485/piv485_pre_lyr на обычном sendqlib'е, чтоб оно могло работать хотя бы с ОДНИМ устройством, а потом -- с уже готовым рабочим драйвером-"клиентом" будем смотреть.
03.02.2012: решено -- делаем по 1-й схеме, интегрируя "планировщик доступа к порту" прямо в sendqlib.
Для этого заводим структуру "порт" -- Привязывание конкретной очереди к порту --
sq_port_t
, с
сервисными методами
sq_port_init()
/sq_port_fini()
.
sq_set_port()
, но это кривость, надо прямо в
sq_init()
сделать доп. параметр "порт" -- поскольку едва
ли есть смысл в отвязывании и перепривязывании (reparent?).
13.02.2012: да, переделано: *port
добавлен вторым параметром к sq_init()
(==NULL -- работаем
изолированно, напрямую), а sq_set_port()
изведена. Кроме
того:
sq_q_t
добавлены поля:
port
-- ссылка на оный.
prev_in_port
/next_in_port
--
менеджмент списка.
prev_to_send
/next_to_send
--
менеджмент ОЧЕРЕДИ в порт. В неё встают при появлении чего-то на
отправку, а выходят -- при опустошении СВОЕЙ очереди.
sq_port_t
получила наполнение: парочка
first_in_port
/last_in_port
-- менеджмент
списка; first_to_send
/last_to_send
плюс
current
-- менеджмент очереди.
sq_init()
добавлен
NULL
в качестве параметра.
Но весь внешний API уже готов -- всякие добавления/удаления будут внутренним делом sendqlib'а, не торчащим наружу.
16.05.2012@Снежинск-каземат-11: потихоньку делаем работу с портами.
bind_to_port()
и rm_from_port()
.
add_to_send()
и zer_to_send()
.
21.12.2012: несколько раз мучительно пытался сделать эти "порты", и всё не так и никак.
А сегодня осенило: надо смотреть на КЛЮЧЕВЫЕ ТОЧКИ в коде (ну кто б сомневался, но всё же). Т.е., я всё пытался детали продумывать, проектируя "снизу вверх", а надо было наоборот -- "сверху вниз", сначала продумать глобальности.
Такими "ключевыми точками" по размышлению представляются а) постановка пакета в очередь; б) отправка пакета (сюда же автоматом идёт и "erase_and_send_next"); в) реакция на таймаут.
А внимательно изучение кода показывает, что, похоже, всё сводится к пункту (б) -- отправка пакета. Остальная парочка в конечном итоге вызывает его. Ну, в принципе, логично: ведь смысл "планировщика" -- разделение доступа к линии, т.е. -- отправки.
24.12.2012: да, еще тогда, позавчера, начато делание в описанную сторону.
perform_sendnext()
переименована в
perform_sendnext_no_port()
-- она делает то же, что и
раньше -- но только "результат" возвращает.
Результат -- это "чем дело закончилось" ("дело" -- отправка пакетов из данной очереди):
perform_sendnext()
принимает
дополнительно параметр from_enq
-- означающий, что она
вызвана из sq_enq()
.
sq_enq()
, и можно попробовать отправить пакет ТОЛЬКО если
сейчас ничего другого на отправку нет (и не ждётся ничего, и в очереди
на отправку нет никого -- реально проверка сводится к
current_to_send==NULL
).
from_enq==0
, то делается еще одна проверка --
что вызывальщик действительно является "держателем порта"
(current_to_send==q
) -- иначе просто ничего не делается.
current_to_send
лежат исключительно на ВЫЗЫВАЛЬЩИКАХ функций
add_to_send()
/zer_to_send()
, но НЕ на
них самих.
Также и "wrap-around" (зацикливание) при переходе к следующему элементу/очереди -- аналогично.
Мысли по общему состоянию:
(Чем-то ситуация напоминает мне душераздирающие гусиные рассказы про обрюзгновение asyn-driver'а.)
Возможно, проблема в том, что мы смотрим на ситуацию "не с той стороны" -- вот и выходит криво; а "с той" -- всё само собой получилось бы элегантно.
Так что, в принципе, можно как-то сильнее химичить с очередью -- например, на время "сна" уводить из очереди вовсе.
А как? При q->tout_set заказать таймаут "следующему" в голове? с одной стороны, он из-за этого пропустит свой ход; с другой -- если заказывать "хвосту", то perform_sendnext() проигнорирует его, ибо не голова. А если никого не осталось, то кому заказывать (ведь может добавиться кто-то позже, и тогда тому, свежепоявившемуся, прилетит "подарочек из прошлого")? Совсем правильно -- надо б уметь заказывать таймаут САМОМУ ПОРТУ. И port_p->tout_set должно быть доп.барьером для отправки. Отдельный вопрос -- СКОЛЬКО выжидать. Ведь сейчас не помнится ни момент заказа таймаута, ни его размер.
А при удалении мы ставим в качестве текущего не следующий, а ПРЕДЫДУЩИЙ -- тогда хоть сразу, хоть по таймауту очередным станет как раз следующий.
Но потенциальный улучшайзинг -- потом, может быть.
24.12.2012@по-пути-домой-по-Лаврентьева-и-мимо-НИПСа: а ведь "таймаут порту" -- это и есть естественное решение проблемы! Только помнить ничего не надо, а просто СРАЗУ заказывать таймаут ПОРТУ. В этом случае дальнейшая судьба очереди-заказчика значения уже иметь не будет.
"Следствия":
Но тут будет техническая сложность реализации -- как-то надо эту очередь временно уводить из порта... Ладно, это уж точно "на потом".
Потенциальное решение лежит в иной плоскости: надо при НЕполучении ответа не пытаться сразу же спрашивать снова, а выдерживать паузу -- НЕ равную времени ожидания; например, 10 секунд (как у remdrv).
И реализация этого решения имеет проблему, в точности аналогичную предыдущему пункту: надо уметь временно уводить очередь из порта.
25.12.2012: угу -- продолжение деятельности по результатам размышлений:
perform_sendnext_no_port()
в
perform_sendnext_in_queue()
.
sq_port_t
добавлены поля privptr
,
tim_enq
, tim_deq
, tout_set
--
функционал идентичен оному у очередей; ...
sq_port_init()
, ...
sq_port_tout()
.
sq_timeout()
проверка на
ring_used==0 убрана.
q->port!=NULL
работа идёт сразу с таймаутом порта, а таймаут очереди не используется
вовсе.
Но при этом проверяется, что current_to_send==q, и иначе отлуп.
Кроме того, в deq_tout()
добавлен параметр
upon_erase_first
, указывающий, что оно вызвано по удалению
1-го элемента очереди -- в противном случае таймаут порта также не
трогается (именно чтоб остальные таймауты (по удалению устройства и по
очистке очереди) оставались в силе).
SQ_TRIES_DIR
, использование которой
с портами вообще бессмысленно).
Итого -- предварительно сделано. Теперь надо добавлять поддержку таймаутов портов в nkshd485 и проверять работу.
27.12.2012: хех -- а ведь таймаутов для layer'ов (коим является порт) у нас в CXv2 не предусмотрено! Придётся делать...
В nkshd485 поддержка добавлена.
30.12.2012: поддержка таймаутов добавлена, в обе среды, так что проводим тесты.
03.01.2013: вроде разобрался с проблемами (с первой -- вчера, со второй, более противной -- сегодня).
Причина всего -- что таймаут может возникнуть не только из-за тормозов устройства, но еще и потому, что сам ДРАЙВЕР/сервер чем-то занят. Вот оно так и выходило -- при запуске 3 драйверов оно просто не укладывалось в 100мс, и сначала отрабатывал таймаут, а потом уж проверялись дескрипторы (где обнаруживался ожидаемый пакет, но отдававшийся уже не тому экземпляру драйвера, который его ждал).
Это некоторый удар по всей концепции sendqlib'а -- следствие конкретного ТЕКУЩЕГО устройства cxscheduler'а. А как по-другому-то можно сделать?
Но это ошибка: оно противоречит самой концепции, приведшей к появлению таймаутов ЛИНИИ -- "таймауты могут переживать драйверов, вызвавших их". И ведь port-то НЕ грохался, следовательно, и его таймаут трогать нельзя.
Теперь оно молотит на вид стабильно.
02.02.2013: за прошедший месяц вроде всё окей, камеры работают. Так что ставим "done".
30.03.2017: там делать сборку обоих внутри одной директории можно, но мутновато. Поэтому пока сделано в отдельной директории lib/Qt5cxscheduler/, собирается libQt5cxscheduler.a.
Потом, кстати, надо будет и обычный 4-шный Qcxscheduler складывать в libQt4cxscheduler.a.
31.03.2017: сделано.
05.04.2017: вот только НЕ РАБОТАЛО оно совместно. Qt4'шный вариант -- всё прекрасно, а Qt5'шный -- ошибка линковки программы, из-за наличия внутри символа, который определён в Qt4.
Как выяснилось, причина в т.н. "moc" (Meta-Object Compiler). Эта хрень для каждого .h -- точнее, для Qcxscheduler.h -- сама (или по инструкции "HEADERS += Qcxscheduler.h" из Qt?cxscheduler.pro?) генерила moc_Qcxscheduler.cpp и moc_Qcxscheduler.o. А поскольку при сборке Qt5'шной версии такой файл от Qt4'шной уже был, то она его и не делала, а брала "как есть" -- 4'шный.
Вывод: надо полностью разводить всё по именам, чтобы и header'ы были в 2 вариантах. Но и .cpp-файлы из Qcxscheduler.cpp надо получать не симлинкованием, а пропусканием через sed с заменой некоей строки на нужную циферку.
06.04.2017: сделано.
sed -e 's/_ZZZ_VERSION_ZZZ_/$*/g'
Монструозноватый Makefile получился.
07.04.2017: ЕманоФедя проверил -- вроде всё теперь собирается и работает хорошо. Засим, наконец-то, можно делать "done".
22.11.2018: еще модификация: эта, с позволения, "система сборки" переведена на использование софтины qtchooser.
NOQT=Y
.
Этот ключ отрабатывается независимо как в lib/Makefile, так и в lib/Qcxscheduler/Makefile.
Изводим.
17.07.2015: соображения и ход работ:
Что делать?
18.07.2015: далее:
РЕШЕНИЕ: возложить эту задачу на devlist, чтобы там устройства alias'ами объединялись в одну "поддиректорию" с фиксированными именами-ссылками на target'ы. Примерно так:
(фиксированные имена-ссылки -- "adc" и "dac").dev zzz_adc canadc40 ... dev zzz_dac candac16 ... cpoint v3h_a.adc zzz_adc cpoint v3h_a.dac zzz_dac
Имена target-каналов будут указываться в виде "adc.mes" и "dac.out".
РЕШЕНИЕ: а просто явным образом пусть драйвер-клиент указывает
список имён каналов (с количеством), которые надо считать за статусы.
Для однодевайсных драйверов это всегда будет
[1]={"_devstate"}
. (Ну и массив такого же
количества под текущее состояние этих каналов -- можно даже того же
типа vdev_sodc_cur_t
.)
Ближе к вечеру -- сделано.
06.06.2024: вот только тогда регистрация devstate-каналов
была добавлена ПОСЛЕ регистрации обычных (что, кстати,
отличается от v2'шного поведения: там -- просто в силу устройства API
"сначала получаем device-reference, потом по этому же ИМЕНИ устройства
получаем reference'ы его каналов" -- vdev_bind_hbt()
делал
inserver_add_devstat_evproc()
всегда ДО
inserver_add_chanref_evproc()
его каналам); а надо было
наоборот. Вчера последствия были осознаны и порядок изменён.
ПРОЕКТ РЕШЕНИЯ: состоит из 2 частей:
vdev_sodc_dsc_t
-- в
конце, поэтому для обычных каналов их можно просто не заполнять, так
что вид исходников не загромождается.
Указывать там пару полей -- dtype и nelems, которые по умолчанию (по ==0) считаются за CXDTYPE_INT32 и 1.
10.08.2016: ДА! В pzframe_data именно так и сделано -- dtype==0 считается за CXDTYPE_INT32, max_nelems==0 за 1.
vdev_sodc_cur_t
: поле
val
превращается из int32 в CxAnyVal_t
, для
хранения значений и других типов.
Несколько замечаний:
&(ctx->cur[sodc].val)
нужное число байт.
cur[sodc].val.i32
вместо просто
cur[sodc].val.i32
.
Переживём.
22.08.2016: да аксессоры надо сделать,
как в pzframe: vdev_get_int()
.
vdev_sodc_cur_t
под указатель и размер в
байтах).
17.08.2016: собственно -- в pzframe_data оная проблема
решена: там аллокируется один общий буфер, объёмом равным сумме всех
требуемых размеров, и на его последовательные части расставляются указатели
current_val
в нуждающихся ячейках.
16.08.2016: конкретный пример, где оно понадобится -- потенциальный драйвер «процесс "Электронно-лучевая сварка"»: там надо иметь дело с энным количеством каналов-таблиц для козачиных ЦАПов (причём, вероятно, именно цапОВ, а не просто одного девайса).
05.07.2018: да-да, именно там! Только еще сомневался -- "а не делать ли эту штуку knobplugin'ом?". Нет -- надо именно драйвером, и для него сделать поддержку векторностей.
Теперь проверять, отлаживать, и потом делать остальные два драйвера.
19.07.2015: v1k_cdac20_drv.c.
20.07.2015: v3h_a40d16_drv.c.
C3H_nnn()
, а вся адресация по
фиксированным номерам "виртуальных каналов", берущимся из одной из 8
карт.
14.08.2015: в v2'шном linmagx какие-то странности с отдаваемыми значениями каналов SWITCH_ON/SWITCH_OFF у V3H: почему-то почти повсеместно значения 2 (DISABLED); см. ~/20150814-linmagx-strange-sw_on_off_values.gif. Единственный 0 -- у канала SWITCH_OFF у lens20, которой только что руками было сделано "Вкл".
Вообще-то это странно, т.к. в vdev_set_state()
(в
обоих!) в самом конце есть цикл прохода по state_related_channels и
дёрганья do_rw(DRVA_READ) для каждого.
01.09.2015: (вечер после пляжа) да, присутствует.
31.08.2015: глобальное переименование: out,out_rate,out_cur в iset,iset_rate,iset_cur.
01.09.2015: (совсем вечер, после фиксенья v3h_a40d16) разобрался в проблеме "почему-то почти повсеместно значения 2 (DISABLED)".
_rw_p()
(вызываемых из vdev_set_state()
) при определении
"можно(0)/нельзя(2)" вызывали
sw_alwd(,sdp->state)
вместо надлежащего
sw_alwd(,me->ctx.cur_state)
Т.е., передавали target-state'ову проверяльщику "можно ли перейти в это состояние" в качестве текущего состояния его же само, вместо реально текущего (прикол в том, что проверка "можно ли" при DRVA_WRITE (несколькими строками выше) делалась правильно).
09.09.2015: vdev.c переехала из sw4cx/drivers/ в 4cx/src/lib/vdev/.
08.12.2015: некрасивость обнаружилась:
(Осознано по результатам анализа кода.)
09.12.2015: проверено при помощи "cdaclient -m ИМЯ_КАНАЛА@u" -- да, так и происходит.
09.12.2015: за вчера-сегодня сделан еще один драйвер -- mps20_ceac124_drv.c (для беликовского MPS-20). Сегодня отлажен и работает.
Засим можно считать, что модуль vdev.c вполне доведён, так что ставим "done".
20.03.2016: сделана первая попытка использовать в качестве базового устройства не локальное (insrv::), а удалённое, по протоколу cx::.
Косяки явно связаны с общим поведением связки cdac20_drv+cxsd+cda+vdev, которую мы сейчас и расследуем.
26.10.2016: а вот сейчас еще раз попробовано -- конкретно на v1k_cdac20_drv (q1l3n5), запущенном локально и натравленном на cx::ring1:12.vcd_1l3 -- на вид вполне функционально, отображаемые данные совпадают с таковыми от нативного ring1:12.q1l3n5.
16.09.2016: добавляем поддержку каналов не-только-INT32 и не-только-скаляров -- предварительные действия:
vdev_sodc_dsc_t
добавлены поля dtype
и max_nelems
.
vdev_get_int()
:
Вариант с ней --
vdev_get_int(&(me->ctx), CN)
-- на 13 символов длиннее, чем ранее:
me->cur[CN].val
в то время как прямая адресация к прямому доступу через val.i32 -- всего на
4:
me->cur[CN].val.i32
А если переименовать "val" в "v", то и вовсе на 2.
vdev_sodc_cur_t
переделываем:
вместо
int32 val
теперь
CxAnyVal_t v
И vdev_get_int()
адаптирована -- там куча условий и длинный
switch().
07.10.2018: только в switch()'е были перепутаны SINGLE и
DOUBLE: для первого бралось из v.f64
, а для второго из
v.f32
. Исправил.
v.i32
-- т.к. он сам же их и регистрирует,
так что это безопасно.
А вот при вызове cb -- значение берётся уже через
vdev_get_int()
.
Но dtype/max_nelems регистрация и добыча данных еще не сделаны!!!
vdev_sodc_cur_t.current_val
и vdev_context_t.buf
.
19.09.2016: проверено на ist_cdac20 на linac1:11 -- работает.
04.07.2018@вечер-дома: возобновляем работы (нужно для weldproc_drv).
vdev_init()
.
PzframeDataRealize()
.
Если результат ненулевой -- аллокируется vdev_context_t.buf
.
vdev_sodc_cur_t.current_val
.
safe_free(ctx->buf)
-- при возврате
-CXRF_DRV_PROBL
.
vdev_fini()
-- освобождение
cid
'а и safe_free(ctx->buf).
sodc_evproc()
возникла
некоторая неопределённость: ЧТО/КАК именно там делать?
А поля для этого в vdev_sodc_cur_t
не предусмотрено!
Кстати, конкретно для weldproc_drv.c это всё вообще не требуется (он только пишет в каналы векторов, но не читает их), так что на этом этапе можно просто забить.
Но так поступать не хочется -- лучше сразу сделать "правильно".
05.07.2018: доделываем.
vdev_sodc_cur_t.current_nelems
.
sodc_evproc()
изрядно усложнилась, с использованием в качестве основы
pzframe_data.c::ProcessOneChanUpdate()
.
10.07.2018: ну да, вот ровно в TUBE'инге косяк и был:
ReturnDataSet()
'у в качестве values
передавался не
&dst
, как положено, а зачем-то &vp
, где
vp=&dst
.
Обнаружилось при тестировании weldproc_drv.c; переделал как положено -- заработало как надо.
16.07.2018: почему-то поле
vdev_sodc_cur_t.current_nelems
было сделано
size_t
; исправлено на должный int
.
16.07.2018: кстати, проверил работу не-скалярности:
sodc_cb
печатает current_nelems
всех сбагриваемых
ему каналов, и у козако-табличных выдавались правильные значения: сначала 0,
после старта таблицы (т.е., записи чего-то) -- 2.
Причина очевидна: зануление кумулятивных --
Drop_c_ilks()
-- делается ПЕРЕД сбросом самих блокировок,
в SwchToRST_ILK_SET()
, а после этого успевают придти
"новые" значения битов блокировок, еще не сбросившихся, и кумулятивные
опять взводятся (да, взводятся только для тех, которые горели, а для
погасших (которых не должно бывать, и ради определения которых
кумулятивные и делались) так и остаются сброшенными).
Очевидное решение -- поставить зануление кумулятивных ПОЗЖЕ, через
какую-то паузу от команды на сброс блокировок. Такое место -- в точке
сброса бита сброса, SwchToRST_ILK_DRP()
, поскольку там
задержка в 0.5с между состояниями.
15.09.2015: да, так и сделано.
21.09.2015: вроде помогло, кумулятивные тоже сбрасываются (с хорошо заметной задержкой).
А всё потому, что этим каналам никто НЕ ставит
IS_AUTOUPDATED_YES
(как и многим другим).
Но, собственно -- для конкретно канала "state" можно ж это делать прямо в
vdev_init()
.
Сделано.
21.03.2016: ага, но только теперь во всех графических клиентах каналы "state" горят гусиным цветом -- ведь у них fresh_age-то по-прежнему 5.0с, а обновлений никаких не происходит!
Так что IS_AUTOUPDATED_YES
заменено на
IS_AUTOUPDATED_TRUSTED
. Помогло.
vdev_forget_all()
в драйверовых
SwchToUNKNOWN()
.
В много-subord'ных драйверах (вроде v3h_a40d16) переключение в UNKNOWN
может быть вызвано уходом ОДНОГО девайса из OPERATING, в то время как
остальные останутся рабочими, но их каналы будут помечены как
.rcv=0
и некоторые (rw и прочие AUTOUPDATED_TRUSTED) уже
НИКОГДА не обновятся (т.к к тому не будет никаких причин).
13.08.2016: а точно ли надо сбрасывать флаги
rcvd
, а главное -- насколько вообще эти флаги нужны?
Может, воспользоваться каким-нибудь иным средством, уже предоставляемым серверным/cda'шным API? Например, смотреть на отсутствие CXRF_OFFLINE в rflags канала?
Ведь сам-то сервер (cxsd_hw) теперь НЕ нулит rflags, зато нулит timestamp'ы. 20.10.2016: неа, начиная с 27-06-2016 уже timestamp'ы не трогает.
20.10.2016@утро-мытьё-посуды: решение-то очевидно:
stat_evproc()
.
devstate_names[]
.
(Задумался в очередной раз об этой проблеме из-за vepp4_gimn_drv.c, где подчинённых устройств ТРИ -- cpks, cgvi и vsdc2.)
20.10.2016: делаем.
vdev_sodc_dsc_t.subdev_n
.
enum
с константами вида
SUBDEV_nnn
(по штучке на каждое под-устройство).
hw2our_mapping[]
в .subdev_n
указываются
эти константы.
devstate_names[]
,
который теперь не просто {СПИСОК}, а с позиционной инициализацией вида
[SUBDEV_nnn] = "mmm._devstate"
stat_evproc()
при свежеполученном состоянии target'а
==DEVSTATE_OFFLINE
сбрасывает "полученность" всем его каналам
(т.е., с subdev_n==tdev
).
Вот это -- небесспорное решение, т.к. не до конца ясно, как правильнее поступать:
ReviveDev()
?
После анализа кода cxsd_hw.c -- неа, нельзя. Там рассылка
нотификации об изменении _devstate -- путём вызова
report_devstate()
-- выполняется в последнюю очередь, уже ПОСЛЕ
ReRequestDevData()
+ReqRofWrChsOf()
, а драйвер
данные может вернуть сразу же, т.е. после изменения _devstate их
уже не поступит.
...короче -- переделано на !=DEVSTATE_OPERATING.
vdev_forget_all()
.
SwchToUNKNOWN()
устранить -- их единственный
смысл был в вызове забывания.
06.06.2024: сделано и то, и другое. Спустя много-много лет...
stat_evproc()
есть странный пассаж: при
subord_state==DEVSTATE_NOTREADY (и соблюдении еще пары условий) делается
переход в state_determine_val
:
else if (subord_state == DEVSTATE_NOTREADY) { if (ctx->cur_state != ctx->state_unknown_val && ctx->state_determine_val >= 0) vdev_set_state(ctx, ctx->state_determine_val); }
Спрашивается -- ЗАЧЕМ? Ведь переход в DETERMINE вроде бы осуществляется
в sodc_evproc()
, при условии, что
AllImportantAreReady()
. А лишний переход в это состояние
(причём, без проверки на готовность данных!) только всё запутает/испортит.
Явно осталось еще от v2 (где этот кусок есть), но там-то оно зачем вводилось?
После обеда: раскопки в архивах показали, что этот пассаж присутствовал еще в до-vdev'ном ist_xcdac20_drv.c, появившись во ВТОРОЙ рабочей версии, от 03-04-2012.
Главная досада -- совершенно непонятна логика этого куска кода.
sodc_evproc()
уже НЕ шло в DETERMINE, т.к. это происходит
лишь при нахождении в UNKNOWN.
SwchToDETERMINE()
.
Так что считаем тот "странный пассаж" (унаследованный от 2012 года) косяком и исправляем, добавив альтернативу NOTREADY к предыдущей ветке по OFFLINE:
if (subord_state == DEVSTATE_OFFLINE || subord_state == DEVSTATE_NOTREADY) { vdev_set_state(ctx, ctx->state_unknown_val); }
Проблема с не-разболотовением кумулятивных блокировок ушла.
Пара замечаний:
chan_state_n
:
vdev_set_state()
именно это и делается.
stat_evproc()
почему-то в этом канале
возвращается свежевысчитанный devstate!
Чего в v2, кстати, НЕ было!
Когда и зачем сие появилось?
"Наверху" эта проблема не должна быть заметна -- обычно сразу после
такого "счастья" должен был идти vdev_set_state()
, возвращающий
правильное значение.
Может, это когда-то было сделано для целей отладки -- смотреть так внутренний параметр, при помощи "cdaclient ....ist_state@u"?
После обеда: раскопки по архивам показывают, что этот пассаж появился в интервале 27-03-2016...29-03-2016. Одновременно с внедрением поддержки timestamp'ов.
Считаем, что оно было для отладки, и закомментировываем.
20.10.2016@по-пути-на-обед-около-столовой-АСТС-N6: а ведь переход в _STATE_UNKNOWN никогда не происходил!!!
SwchToUNKNOWN()
никогда никем не
вызывалось, и vdev_forget_all()
тоже.
cankoz_fd_p()
, реакцию на
is_a_reset
).
...вот так и начинаешь понимать истинное функционирование своего кода -- через 3 года после его создания (07-2013!) :D.
Получасом позже: а вот нифига -- через 4 с половиной! Проверка "если у подчинённого OFFLINE, то перейдём в UNKNOWN" появилась еще в самом первом работающем варианте ist_xcdac20_drv.c, еще до-vdev'ном, за 28-03-2012. И с тех самых пор эта штука была нерабочей... Shame on me!!!
13.03.2018: появились сомнения, что "никогда не происходил!!!" -- см. рассуждения за сегодня касательно возможных причин инцидента 05-03-2018.
06.06.2024: сегодня наконец-то доделано (после того,
как вчера было осознано, что vdev_forget_all()
всё же приносит
проблемы -- по крайней мере, при локальной подстилающей аппаратуре), так что
"done".
vdev_sodc_cur_t
добавить поле timestamp (ts
), чтоб
каналы как минимум TUBE'ились бы более корректно (в
sodc_evproc()
под это уже давно заточка есть).
29.03.2016: делаем.
ts
добавлено.
sodc_evproc()
.
vdev_init()
.
(Плюс туда же в vdev_forget_all()
, но эта функция сама по
себе сомнительна -- см. предыдущий пункт за 23-03.2016.)
sodc_evproc()
, с
использованием свежевведённого в этих целях
ReturnInt32DatumTimed()
.
30.03.2016: вроде работает как положено, "done".
15.09.2018: оно РАБОТАЛО как положено, пока было поверх remdrv'шных. А при переходе на локальные @sktcankoz -- вчера вылез косяк:
Нудное разбирательство показало, что проблема --
vdev_init()
, в том, ГДЕ делалась инициализация
ts=NEVER_READ: она делалась ПОСЛЕ cda_add_chan()
.
Причём включая DIGCORR_MODE, который должен бы вначале вычитаться из
железки. Это из-за перемудрённости/ляпа в общей архитектуре CAN-драйверов:
DIGCORR_MODE относится к CONFIG-каналам (и в SUPPORTED_chans
в
его позиции стоит 1) и потому ВСЕГДА отдаётся из кэша.
Исправление тривиально -- инициализация ts'а переставлена в точку ПЕРЕД регистрацией канала.
Кто виновник -- неясно (не было возможности разобраться). Потенциальные претенденты --
10.04.2016: сегодня опять была просадка.
Таким образом, с vdev обвинение в данном случае снимается. Возможные версии:
Но раньше-то, с v2'шным, таких косяков вроде не замечалось. Поэтому еще вариант --
Если дело в требуемом reset'е чипу SJA1000 -- то, теоретически, в
can4linux.h есть некая CMD_RESET
, выполняемая через
ioctl()
, которую можно б подавать через консольный интерфейс --
для чего придётся расширить сам API canhal.h.
10.04.2016: главное, конечно -- неясно, как вообще такая ситуация могла возникнуть:
09.01.2017: еще косячок с тем же v3h_a40d16 (2!):
Обсуждение:
Видимо, [после некорректного вычитывания каналов записи] на какой-то стадии щелканья командами драйвера v3h он почему-то сбросил бит SW_ON, а потом уже не смог перенести присутствующей в D16_OUT_CUR ненулевой уставки.
...хотя анализ кода не дал ни единой точки, где б такое могло случиться: все записи в D16_SW_ON -- только в процессе включения/выключения, до которых вроде дойти не должно было (а переход в INTERLOCK -- SW_ON сбрасывается одновременно с записью 0 в уставку (как и в SW_OFF_DOWN)...).
"Идея":
_sodc_cb()
сразу же перейдёт в SW_OFF_DOWN).
10.01.2017: еще идея:
Часом позже, ~10:00, после анализа кода драйвера: а можно лучше --
IsAlwdSW_OFF_DOWN()
"разрешать" также
и при ненулевом значении D16_OUT_CUR!
Так и сделано, во всех 4 драйверах. Теперь проверять.
10.01.2017: к вопросу об "откуда взялся бит регистра в не том состоянии": поговорил с Козаком. Результаты:
Причина -- регистр отрабатывается отдельной (от процессора) Альтерой, и в неё по включению/reset'у запись не делается. Да, баг, но сейчас его исправлять смысла нет -- ибо CANADC40/CANDAC16 очень много в разных местах, и уж пусть лучше везде будет одинаковое косячное поведение, чем везде разное.
05.10.2017: позавчера опять был такой косяк после просадки, а сегодня снова, причём сразу на нескольких устройствах.
Получасом позже: обсудил с Козаком, посмотрел на логи -- не-а, не было никакого 0xFF'а. Ведь АЦП-то продолжил работать, обмерять и присылать данные, а при перевключении блока он бы отрубился и драйвер определил бы это через минуту и отправил 0xFF.
Видимо, всё же просто взглюк из-за просадки: АЦП остался жив, а ЦАП сбросился, но драйвер этого не узнал и продолжал думать, что выставлен не 0.
Плохое решение: забардачивает интерфейс, требует неочевидных действий от оператора. Withdrawn.
SwchToDETERMINE()
пытаться отлавливать такие
"неправильные" комбинации значений и стараться сразу же исправлять ситуацию
-- например, писать в ЦАП 0.
Поскольку переход в DETERMINE происходит и по кнопке [R] (RESET_STATE), и при старте драйвера, и при очухивании подчинённого устройства, то лечение получится максимально всеобъемлющим и удобным.
05.03.2017: в продолжение ситуации от 09-01-2017 "в ЦАПе значение выставлено, а в драйвере источника считается 0", или "выставленное напряжение, но сброшенный бит SW_ON": сегодня наблюдал её возникновение воочию.
SndCVal(me, D16_OUT, 0)
Нет ли тут какой-то тонкости/засады?
Сходу при просмотре кода -- нет, на вид всё корректно: нуление есть в реакции на оба события.
И нарисовать скриптик, пишущий cdaclient'ом команды v3h'у, а потом имитирующий "отработку" записью в симулируемые каналы обратной связи.
13.03.2018: на эту тему есть отдельный раздельчик, начатый 12-02-2018.
И возникло подозрение, что дело в использовании _forget_all(), могущего вызвать проблемы на многодевайсных vdev-драйверах -- коим (единственным!) и является v3h_a40d16.
Так что симулятор бы не помог, а надо тестировать на реальном железе.
Дальнешие записи, очевидно, уже в том раздельчике.
CDA_DATAREF_OPT_ON_UPDATE
при регистрации каналов.
Не с его ли отсутствием (т.е., обновлениями всего скопом "в конце цикла") были связаны проблемы за 20-03-2016 ("попытка использовать ... не локальное (insrv::), а удалённое, по протоколу cx::")?
Причём кумулятивные блокировки -- НЕ подсвечивались. Чего, по идее, быть вообще ну никак не может.
Часок посидел, покумекал -- догадался, в чём дело: у В1000 блокировки "инверсные" (0:проблема, 1:OK), а они всё равно TUBE'атся, что криво (еще с v2 это было ясно!); вот и получался race condition.
27.06.2016: что, видимо, происходило:
sodc_evproc()
по R_UPDATE делает
следующее:
cur[sodc].val
.
v1k_cdac20_sodc_cb()
:
v1k_cdac20_rw_p()
для TUBE-каналов просто молча
возвращает текущее значение из cur[sodc].val.
...а вот cdaclient'ом, с ключом "-m" и суффиксом "@u", эти "1, потом сразу 0" отлично видны.
BeginOfCycle()
-- слался запрос на чтение
регистров.
Но, в любом случае, лазейка для ошибки присутствовала.
Вывод: флаг TUBE можно ставить только тем каналам, значения которых действительно отображаются между аппаратным и vdev-каналом 1-в-1. И ни в коем случае НЕЛЬЗЯ помечать так каналы, проходящие какую-либо трансформацию.
Что сделано:
И номера соответствующих им V1K-каналов убраны, заменены на -1 (хотя это при отсутствии TUBE уже неважно).
v1k_cdac20_rw_p()
с реакцией "не делать
ничего, будут вёрнуты из sodc_cb()".
Потому, что в В1000 они (в отличие от ИСТа) никакой спецификой не обладают и просто TUBE'атся.
IS_AUTOUPDATED_YES
.
И в соседних драйверах тоже.
По-хорошему, это бы надо сделать и со всеми OPR-каналами, но лень.
Сделано то же самое.
Нужно это в интересах свежевводимых драйверов для тех систем, что в v2 делались на клиентском уровне -- lebedev_subharmonic, lincamsel, linsenctl, turnmag (вот в этом состояния могут и пригодиться), ringcamsel.
09.08.2016: типа протокола:
vdev_work_hbt()
.
Соответственно, теперь первоначальный его вызов делается только при
work_hbt_period>0
.
state_descr
-- делается безусловно, без проверки, что
state_count>0
.
Оная проверка добавлена в sodc_evproc()
и к условию из
предыдущего пункта (для защиты содержимого vdev_work_hbt()
,
лазящего в state_descr
без проверок -- ибо это и есть его
работа).
Еще таблица используется в vdev_set_state()
, но там своя
проверка в самом начале.
chan_state_n
-- что оно не только >=0
, но еще и
<state_count
.
Это позволит НЕ прописывать chan_state_n=-1
, а ограничиться
умолчательным 0.
26.10.2016: кретин!!! При чём тут
state_count
?!?!?! См. раздел за сегодня ниже.
Теперь проверять (когда? :)).
Кстати, vdev-based вариант драйвера -- 127 строк против 161 у самодостаточного. (10.08.2016: уже 125.)
10.08.2016: далее:
Конкретно -- lebedev_linsenctl, который в дополнение к инвертированию единственного бита УРа также поддерживает канал "осталось времени до окончания езды".
vdev_sodc_dsc_t
указывается ИМЯ КАНАЛА ОТНОСИТЕЛЬНО БАЗЫ (...БАЗА.КАНАЛ). Но в данном
случае мы заранее имя канала не знаем (оно может быть ЛЮБЫМ битом УРа
(outrbN), а хотим, чтобы прямо оно и указывалось в auxinfo.
Технически решение несложно: в vdev_init() при регистрации канала в качестве (base,spec) вместо обычного (base,map[sodc].name) указывать ("",base) -- это даст ровно нужный эффект.
name
--
"".
Вот по второму варианту и сделано.
Проверено -- работает (на симуляционном target-драйвере).
Собственно, основная тема тоже проверена и тоже работает.
CXRF_OFFLINE
'ивает.
А _C_ILK-каналам стоит IS_AUTOUPDATED_TRUSTED, поэтому они обновляются очень изредко, и так и останутся SFERR'нутыми до загорания настоящей блокировки.
Решение -- вертать их значения по "оживанию". Оным
оживанием проще всего считать вход в состояние DETERMINE. Вот в
SwchToDETERMINE()
и вставлен возврат текущих значений, после
чего всё и заработало.
...а изначально был сделан глупый поступок: в DETERMINE вставлено уставление c_ilk*:=-1, чтобы типа точно не равно следующему приходящему значению блокировок. Но забыто, что обновление _C_ILK-каналов -- КУМУЛЯТИВНЫХ! -- делается только по val!=0, вот то :=-1 никакого эффекта и не давало :)
Обнаружилось, что vdev-драйверы почему-то постоянно отдают свой статус
(*_state -- номер состояния state-машины). Несмотря на то, что вроде бы в
vdev_init()
делается
SetChanReturnType(,chan_state_n,IS_AUTOUPDATED_TRUSTED)
(делалось специально и отдельно -- см. за 09-12-2016).
Разборка показала, что:
chan_state_n
-- валиден, т.е., >=0 и <"количества
каналов". Но только вместо "количества каналов" (которого НЕТУ!)
используется state_count
-- количество состояний, НИКАКОГО
отношения к количеству каналов не имеющее!
И добавлен конкретно 10-08-2016 (об этом даже запись есть выше!), при допиливании vdev для возможности использования в режиме "observer only", без машины состояний.
vdev_set_state()
, т.е., и оно тоже не работало!
Т.е., значение возвращалось исключительно из драйверовых
_rw_p()
.
26.10.2016: ну чо -- исправляем.
vdev_context_t.our_numchans
(более, кроме
как в этих guard'ах, нигде пока что нафиг не нужное). В самое начало блока
"b. State machine".
_NUMCHANS
.
Проверено на полу-симуляторе -- работает, "done".
05.10.2021: в процессе работы над драйвером
lebedev_turnmag появились сомнения в корректности работы с
chan_state_n
, поэтому было проведено расследование со
следующими результатами:
chan_state_n
НЕ устанавливается, и потому ==0,
ctx->chan_state_n >= 0
",
vdev_init()
--
ещё и уставляться свойства.
ctx->chan_state_n < ctx->our_numchans
"
-- и поле "our_numchans
" было введено 26-10-2016 ровно ради
этого: чтобы у stateless-драйверов оно было бы ==0 и, следовательно,
работало бы преградой ("guard") от такой дури.
И отсутствие проблемы подтвердилось при прогоне теста в режиме симуляции.
ctx->state_count > 0
"
с длиннющим комментарием "A bit redundant check (because stateless
drivers also have their our_numchans==0), but is added for future-proofness
(in case our_numchans will somewhen become used in stateless drivers
too)".
06.07.2022: но почему в vdev_set_state()
сравнение
nxt_state > ctx->state_count
а не
nxt_state >= ctx->state_count
?!?!?!
Ведь при state_count==0 даже nxt_state=0 уже неверно!!!
07.07.2022: исправил на ">=
". Теперь
будем ждать, не аукнется ли где (хотя не должно -- прямо по сути, ведь не
должно быть состояний с номером, равным количеству состояний).
Возможно, как-то связано со вторым сегодняшним багом -- невовремя-переходом-в-DETERMINE?
06.12.2016: кажется, догадался: да, проблема СВЯЗАНА с тем багом.
sodc_cb()
. И, соответственно, прямо оттуда делается
vdev_set_state(,DETERMINE), который и приводит к возврату значения канала
_GIMN_STATE.
Должно быть, именно поэтому.
Замечания:
Попытка "починить" тем же способом, что вторую проблему -- добавлением фейковых read-каналов. Помогло.
InitDevice()
изначально проставляется
dev->state=DEVSTATE_OPERATING
?
Но это не проблема, т.к.:
InitDevice()
не использует SetDevState(), а вызывает
ReviveDev() напрямую.
И, соответственно, никаких проверок на равенство не делается.
SetDevState()
эта проверка убрана
08-09-2015.
Т.е., де-факто тут всё КАК БЫ корректно.
dev->state = DEVSTATE_NOTREADY; ENTER_DRIVER_S(devid, s_devid); state = dev->state; if (metric->init_dev != NULL) state = metric->init_dev(...); else state = DEVSTATE_OPERATING; LEAVE_DRIVER_S(s_devid);
Сейчас-то эта проблема неактуальна -- никаких сбросов rflags/timestamps уже нет. Но всё же проще оставить как есть.
Главный вопрос -- а когда было сделано, что изначально ставится OPERATING?
05.12.2016: выявилось на vepp4_gimn_drv: он вместо подхватывания текущих значений из аппаратуры всегда берёт нули.
Анализ показал, что:
vdev_init()
-- оно как бы получает "всё", но это
"всё" -- совсем неправильное.
sodc_evproc()
, при состоянии UNKNOWN.
(Тут и происходит косяк с переходом при реально НЕ полученных реальных данных.)
stat_evproc()
, при subord_state==DEVSTATE_NOTREADY и
нашем состоянии !=UNKNOWN.
И вот это второе -- слабо понятно. Или "что-то странное".
Но проблему как-то решать надо? Надо! Ведь vdev-драйвер, работающий только с управляющими каналами -- вполне легитимная вещь.
(Хотя идеологически и небезупречная: отсутствие каналов чтения -- это отсутствие обратной связи. А драйвер "законченной железки" без обратной связи -- штука странная.)
Ну и какими могут быть решения?
...в любом случае, желательно бы разобраться в логике того второго -- в
stat_evproc()
-- перехода в DETERMINE.
06.12.2016: пытаемся поразбираться.
IS_AUTOUPDATED_TRUSTED
.
(Хотя для не-синести канала _STATE -- прокатило.)
timestamp.sec!=CX_TIME_SEC_NEVER_READ
;
следовательно, проблемы-неприятности, обозначенной в заголовке, быть не
должно вообще.
Так почему она есть?!
07.12.2016: кажется, разобрался "почему" --
CXSD_HW_TIMESTAMP_OF_CHAN()
!
CXSD_HW_TIMESTAMP_OF_CHAN()
.
CX_TIME_SEC_TRUSTED
(там с
оговорками, но не суть).
cda_dat_p_update_dataset()
при TRUSTED считает, что
ВСЕГДА "update", игнорируя указанное!
CXSD_HW_TIMESTAMP_OF_CHAN()
возникла в районе
25-11-2014, в целях реализации поддержки "возрастов свежести".
Путём отрубания головы убирания у них возраста -- в cda для
timestamp==TRUSTED не делается сравнения с возрастом свежести.
CxsdHwSetDb()
для
rw-каналов проставляется возраст свежести {0,0}, что отключает посиневание.
В cxsd_fe_cx.c использование
CXSD_HW_TIMESTAMP_OF_CHAN()
было убрано, а в
cda_d_insrv.c оставалось.
CX_TIME_SEC_TRUSTED
выглядит утратившей смысл (в т.ч.
в результате появления IS_AUTOUPDATED_TRUSTED
).
Да и значение 0 больше подошло бы для CX_TIME_SEC_NEVER_READ
-- кому оно изначально и принадлежало. Ведь это ЕСТЕСТВЕННЕЕ, в отличие от
нынешнего несколько противоестественного распределения значений.
04.01.2017: да, CX_TIME_SEC_TRUSTED
уволена
-- подробнее см. в профильном разделе за сегодня.
CXSD_HW_TIMESTAMP_OF_CHAN()
, а использовать
chn_p->timestamp
.
...и безо всяких фейковых каналов, введённых вчера.
04.01.2017: поскольку со всем разобрались -- изначальная проблема решена, и даже сделаны оргвыводы -- то "done".
А ведь стоит это безобразие унифицировать, ВСЕХНИЕ каналы назвав просто "vdev_state".
03.10.2017: да, в создаваемом сейчас
kurrez_cac208_drv.c сделано именно
KURREZ_CAC208_CHAN_VDEV_STATE
и vdev_state.
04.10.2017: еще в сторону унификации и, заодно, документирования и упрощения работы при создании будущих драйверов:
10.10.2017: дописано, добавлено в doc/lib/vdev.ru.html.
SODC_
, вместо былых C20C_ etc.
sodc2ourc_mapping[]
,
вместо разнообразных cac2rez_mapping[]/cdac2ist_mapping[]/...
24.10.2018: канал переименован в просто "vdev_state" у всей четвёрки -- ist_cdac20, v1k_cdac20, v3h_a40d16, mps20_ceac124.
Для совместимости со старым софтом/скринами сделаны alias'ы вида
ist_state vdev_state
ctx->state_descr[nxt_state].next_state >= 0 && ctx->state_descr[nxt_state].delay_us == 0
Или это было так сделано специально? Но переход на следующее выполнит
ТОЛЬКО sodc_evproc()
, а vdev_work_hbt()
не станет,
т.к. у него условием оного является curs->delay_us>0
.
Часом позже: угу, в старых драйверах для этого используется delay_us=1.
10.10.2017: нифига не специально: в bigfile-0001.html от 17-06-2014 есть пассаж
надо чтоб vdev_set_state()
корректно
отрабатывал состояния с delay_us==0 и sw_alwd==NULL, и сам бы
перещелкивался в следующее состояние (и так пока можно). Аналогично
sea.
Так что это еще почти сразу после создания предусматривалось (начало v2'шного vdev -- 04-07-2013; т.е., всего через год), просто так и не было сделано.
Формально-то сделать не особо сложно -- просто надо аккуратно.
nxt_state
(с предварительным
prev_state=cur_state
!) и делать goto
на начало.
swch_to()
. Но
state_related_channels[]
, так что значения
изменятся только один раз, в конце, или...
Более симпатичен второй вариант -- он обеспечивает совместимость
поведения с поштучными переходами (без цикленья которые, как сейчас).
Считаем, что тут никаких побочных эффектов не будет, что вызов
do_rw(,,DRVA_READ,...)
никаких плохих последствий не повлечёт.
ctx->cur_state==nxt_state
-- а то в swch_to()
может быть сделано что угодно, там-то побочные эффекты, включая вызовы
vdev_set_state()
, разрешены.
Вот только лень как-то: по-хорошему, при таком радикальном изменении проверять бы надо, анализировать таблицы состояний всех vdev-драйверов. А пока и вариант с delay_us=1 работает.
10.07.2018: да, сделано.
nxt_state
и goto
на начало:
SwchToDETERMINE()
; да и
возврат state_related-каналов мог как-то косвенно привести к смене), ...
nxt_state
и
goto
на начало.
Но там никак не вытанцовывалась элегантная конструкция: в первом случае
нельзя обойтись просто условием (нужно выполнить присваивание
nxt_state
), а во втором надо как-то форсить выполнение первой
итерации (например, вводить флаг is_first, сбрасываемый в "сдвиге цикла").
Так что лучшим вариантом остался goto
.
prev_state
теперь в коде, а не в объявлении.
На weldproc_drv.c проверено -- работает. Прочие драйверы надо просмативать отдельно.
find_sdp()
в vdev_find_sdp()
.
(Ага, еще в bigfile-0001.html за 05-06-2014:
"find_sdp()
-- почему у каждого драйвера свой
экземпляр?").
10.10.2017: а может, даже больше -- не find_sdp() стандартизировать, а прямо целые куски, используемые всеми vdev-драйверами в _rw_p():
vdev_handle_state_related_rw()
-- собственно то, что и
пользуется услугами find_sdp().
vdev_handle_scalar_tube_rw()
-- работа с TUBE-каналами
(обычно первая альтернатива в if()'е).
vdev_set_state()
для
перехода в другое состояние. Это хоть и даёт максимальную гибкость, но
некрасиво с точки зрения "теории машин состояния".
А красивым было бы, чтоб и проверяльщики (check, sw_alwd), и действия (action, swch_to) могли бы возвращать номер состояния, в которое надо перейти.
10.10.2017: это в ДОПОЛНЕНИЕ к текущей возможности sw_alwd'ов говорить "да, готово" и "нет, не готово"; т.е., чтобы:
Из чего следует, что состояния с номером 0 быть не может (у всех нынешних это DETERMINE, так что проблемы нет).
Однако, сей красивой идее скорее надо сказать "НЕТ!", и вот почему.
И неудобств это доставит намного больше, чем даст плюсов:
return 1
(а где-то и
return 0
, и надо везде аккуратно выбрать!) придётся больше, чем
сэкономится нововведением.
Это при том, что номер состояния, в которое автоматически перейти далее,
уже есть в vdev_state_dsc_t.next_state
.
Т.е., введение кода возврата в swch_to -- не вариант.
В общем, слишком много проблем принесёт такое нововведение. Увы, "withdrawn".
И да, там были какие-то проблемы с железом. Но как случилась такая ситуация -- в ЦАПе записан 0, а драйвер ВЧ300 об этом не знает?
12.02.2018: расследование показало:
И по "неправильным" -- делать ЧТО? (С неожиданным нулём-то ещё понятно, а с остальными значениями что делать?)
(И да, помнится, когда я заглядывал в панель [...], никто из битов выходного регистра не горел, а только входной "On")
Вот драйвер и считал, что всё включено!
Теперь вопрос -- что делать? А ничего, видимо. Ключевая причина -- косяк в CAN-железе. Человек глазом видит проблему, а пытаться взвалить её определение на драйвер -- куча сложностей (почти "искусственный интеллект"), и может привести к ложным срабатываниям.
13.03.2018: см. также записи о разборках с той же группой QL'ей от 05-03-2018. Совсем не факт, что именно "косяк в CAN-железе" -- возможно, всё ж какой-то ляп в софте; вероятно, конкретно в v3h_a40d16_drv.c (т.к. на всяких ist_cdac20 и v1k_cdac20 такого вроде не бывало).
О-о-о -- а не в двухдевайсности ли (ADC40+DAC16) дело?
SwchToUNKNOWN()
делается
vdev_forget_all()
, у многодевайсных драйверов потенциально
могущий привести к проблемам.
Это было осознано 23-03-2016, и к 20-10-2016 реализована модификация
архитектуры, на основе поля .subdev_n
, позволившая избавиться
от vdev_forget_all()
(vdev.c::stat_evproc()
сама переводит в состояние
"неполучены" все каналы исключительно того подстилающего устройства, которое
прислало сигнал о своём !=DEVSTATE_OPERATING).
vdev_forget_all()
НЕ БЫЛИ убраны.
И надо проводить тесты -- причём на ЖИВЫХ устройствах, а не только на симуляторе, как задумано 05-03-2018.
CDA_DATAREF_OPT_NO_RD_CONV
. Но бывают случаи, когда оное
только мешает.
В частности, v5phaseconv_drv.c -- ему приходится вручную умножать и делить на 1000000.0 для конверсии между микровольтами (в которых исходный канал ЦАПа) и вольтами (используемыми в формулах пересчёта в градусы фаз). Хотя за него эту работу прекрасно бы выполнила cda.
Очевидная идея -- дать возможность драйверам указывать, что НЕ надо выставлять этот флаг.
05.10.2018: делаем.
VDEV_DO_RD_CONV
=1<<31.
cda_add_chan()
используется значение свежевведённой
переменной should_use__OPT_NO_RD_CONV
, которое ставится в
CDA_DATAREF_OPT_NO_RD_CONV
лишь при отсутствии вышеупомянутого
флага и в 0 при его наличии.
Теперь надо будет проверить, но это уже в понедельник (когда на ВЭПП-5 включат питание и будем тестировать v5phaseconv_drv.c.
07.10.2018: проверил (в воскресенье, не дожидаясь понедельника) -- работает, вроде в обе стороны данные ходят как надо.
Сейчас и то, и другое сводится к запуску выключения --
_sodc_cb()
делает
vdev_set_state(,_STATE_SW_OFF_DOWN)
, так что в результате
устройство/драйвер оказывается в состоянии _STATE_IS_OFF, без возможности
узнать причину.
А есть желание мочь узнавать, ПОЧЕМУ источник в состоянии "OFF". Нужно для высокоуровневой логики -- чтоб оно могло принять решение, надо ли пытаться включить источник повторно, или считать его "охреневшим" и звать оператора.
23.10.2018: (реально всё было продумано ещё на прошлой неделе, но руки дошли только сейчас): соображения:
SwchToINTERLOCK()
.
В нём отдавать:
Канал этот надо включить в список state_related_channels[]
,
аналогично _CHAN_IS_ON'у.
24.10.2018: делаем.
VDEV_PS_CONDITION_
.
Смысл в том, чтобы знак определял "хорошесть" (оба нехороших состояния имеют отрицательные коды), а уж по конкретному значению можно понять конкретную ситуацию.
nnn_CHAN_VDEV_CONDITION
.
_init()
отдаётся значение OFFLINE
. Чтоб не
генерить лишнего траффика, а отдавать наверх значения только при изменении
vdev-состояний (для чего он должен быть в
state_related_channels[]
, только без привязанного состояния --
с полем .state
=-1).
VDEV_PS_CONDITION_IS_ON
проверка "когда отдаётся
_CHAN_IS_ON==1" не делается -- вместо этого он отдаётся по
else
.
На первое время покатит, но вообще решение не очень хорошее, так что надо повсеместно относиться к этому пределено аккуратно.
nnn_STATE_CUT_OFF
:
SwchToCUT_OFF()
скопирована из
SwchToINTERLOCK()
.
IsAlwdSW_ON_ENABLE()
.
_sodc_cb()
именно в него переходит при
исчезновении бита OPR.
29.10.2018: наконец-то проверил. После правки пары
косяков (вроде забытого добавления в state_related_channels[]
)
работает как надо.
Только собственно переход в CUT_OFF пока не проверен -- для этого надо какой-нибудь источник руками с его локальной панели выключить.
11.04.2021: вылез этот вопрос потому, что ЕманоФедя затребовал получение квантов для каналов источников -- Iset.
vdev_sodc_cb_t()
, у которого отсутствует "reason"
(и это правильно!).
Т.е., если делать -- то вводить ещё один обратный вызов, уже с "reason", для всякого информирования. И да, это сломает совместимость.
15.04.2021: а вот и нет -- совместимость не сломается!
Т.к. нет заполнение содержимого контекста выполняется не передачей
параметров в vdev_init()
, а ПЕРЕД этим вызовом, самим драйвером
по-польно. Вот и будет по умолчанию NULL. И сама $(LIBVDEV) сейчас
влинковывается не в сервер, а в драйверы индивидуально, поэтому расширение
vdev_context_t
бинарную совместимость не затронет.
В общем, СЕЙЧАС в затронутые vdev-драйверы просто добавлено ещё одно захардкоженное -- отдача кванта 305.
14.04.2021@22:10: кста-а-ати, а ведь конкретно в CDAC20/CEAC51 квант вовсе не 305, а условное "2" -- см. "надо ставить квант равным 2" от 26.04.2017.
Что является дополнительным мотивом для возможности добывать реальный квант.
@засыпая: в принципе, в текущей ситуации -- конкретно для ist_cdac20_drv.c -- можно забить на некорректность: ну "поймает" драйвер состояние "IS_ON" на мгновение раньше, да и ладно. Опять же, в реальности шаг изменения (за 1/10с!) будет, скорее всего, больше и 2, и 305 микровольтов.
Похоже, это происходит в случае, когда подчинённые устройства локальны (а
не удалённые), т.е., данные доступны прямо в момент регистрации, а не потом,
уже после завершения vdev_init()
.
Проблема обнаружена при работе над sim_dir_drv.c, но быстро осознано, что причина где-то в vdev, так что дальнейшие разборки записываем тут.
05.06.2024: начало в разделе по sim_dir_drv за вчера и сегодня, а тут описываем уже специфичные разбирательства.
AllImportantAreReady()
: именно оно определяет
готовность всех подчинённостей, но вызывается оно ТОЛЬКО из
sodc_evproc()
.
Проверять его прямо в конце vdev_init()
, и если "да", то
производить переход в DETERMINE (со всеми теми же проверками)?
sodc_evproc()
, вызывающемся во время регистрации
каналов, в момент регистрации ПОСЛЕДНЕГО IMPR-канала?
vdev_init()
, то devstate-каналы ещё не зарегистрированы (т.к.
они регистрируются ПОСЛЕ sodc-каналов), поэтому состояния под-устройств ещё
"не готовы" (нули -- DEVSTATE_NOTREADY
, да и ).
sodc_evproc()
в проверке "Check if
we are ready to operate" состояния подчинённых устройств вообще никак не
участвуют -- предполагается, что если уж каналы все готовы, то устройства,
наверное, тоже все OPERATING.
sodc_evproc()
(только для IMPR-каналов),
stat_evproc()
, плюс в конце vdev_init()
выдача
AllImportantAreReady()
.
AllImportantAreReady()
между циклами регистрации sodc- и
devstate-каналов.
2024-06-05-15:41:57.402 vdev_init cur_state=0 2024-06-05-15:41:57.402 sodc_evproc #5 rcvd 2024-06-05-15:41:57.402 sodc_evproc #7 rcvd 2024-06-05-15:41:57.402 sodc_evproc #9 rcvd 2024-06-05-15:41:57.402 sodc_evproc #10 rcvd 2024-06-05-15:41:57.402 vdev_init cur_state=7 AIaR=1 2024-06-05-15:41:57.402 stat_evproc [0]=1 2024-06-05-15:41:57.402 stat_evproc [1]=1 2024-06-05-15:41:57.402 vdev_init cur_state=0 AIaR=0
Т.е., после регистрации sodc-каналов уже ready (и даже перешло в
состояние 7 -- V3H_STATE_IS_OFF
), а в конце (после регистрации
devstate'ов) -- уже опять нет.
stat_evproc()
, как видно в вышеприведённом дампе,
говорит, что состояние подчинённого [0]=1 (1 -- это
DEVSTATE_OPERATING
).
WTF?!
vdev_forget_all()
,
блин!!!
stat_evproc()
вычисляет "кумулятивное"
состояние подчинённых как группы посредством
CalcSubordDevState()
(получая NOTREADY) и с помощью
SelectOurDevState()
выбирает "своё" состояние, ...
vdev_set_state(ctx, ctx->state_unknown_val)
SwchToUNKNOWN()
-- всехние!!! -- вызывают
vdev_forget_all()
, ...
.rcvd=0
ВСЕМ каналам -- что и
приводит к "неготовности".
И понятно, почему эффект проявился только на ДВУХустройственном драйвере -- потому, что у ОДНОустройственного не было этого "второго", чьё состояние на первой итерации того цикла ещё неизвестно.
Это, конечно, не "вскрытие показало, что пациент скончался от вскрытия", но очень близко: проблема оттого, что не было доведено до конца исправление, сделанное ещё много лет назад...
vdev_forget_all()
'а return
-- помогло, но частично: теперь выдаётся AIaR=1 всегда, но часть каналов
осталась болотными, а состояние по-прежнему (после первой же итерации) так и
стоит "cur_state=0".
Дело, похоже, именно в этом "=0" -- это UNKNOWN, из которого так и не выйдет, по причине отсутствия "стимулов".
vdev_forget_all()
; попробовать
._devstate=0 "CAN"-устройству.
cur_state=-1
, и чтоб stat_evproc()
при
cur_state<0
не пытался бы вычислять devstate и производить
переход в UNKNOWN.
cur_state=-1-tdev
-- чтоб CalcSubordDevState()
знала, по какому количеству уже реально готовых статусов (что,
кстати, формально тоже не совсем так: а вдруг статусы начнут приходить не по
порядку?).
06.06.2024: завершаем.
Во-первых, проводим тесты.
Только делать это надо ОБОИМ устройствам.
vdev_forget_all()
: да, тоже работает.
Во-вторых, собственно исправление кода.
vdev_forget_all()
.
SwchToUNKNOWN()
-- у них у ВСЕХ единственным действием был
вызов vdev_forget_all()
; ну и в
[*_STATE_UNKNOWN].swch_to
теперь просто NULL'ы.
ЗАМЕЧАНИЕ: но вот у Роговского в work/bpms/drivers/bpmd_cfg_drv.c это было не единственное: он там ещё
me->prev_stable_state = prev_state;
исполняет; так что при переводе BPM'ов на 4cx нужно будет обратить внимание.
Засим "done".
Работы ведём в hw4cx/pzframes/, а потом общие файлы переедут в 4cx/src/ -- в include/ и lib/pzframe/.
17.04.2016: реально думки начались еще в начале недели, а сейчас уже конкретно делаем файлы. Конкретно начато с pzframe_data в лице pzframe_data.h.
PZFRAME_PARAM_RW_ONLY_MASK
: в
v2 оно было флагом в поле pzframe_paramdscr_t.param_type
, но
оного теперь нету.
По-хорошему, оное поле теперь надо бы превратить в "behaviour", "flags" или "options", но сие создаст путаницу -- т.к. похожие имена уже используются (pzframe_type_dscr_t.behaviour, rflags, ...).
Поэтому простоты ради поле пока названо "rw_only
", а когда
(если) возникнет потребность в иных флагах, помимо RW_ONLY, то переименуем
во что-нибудь более адекватное.
18.04.2016: продолжаем:
"Резолвинг" будет сводиться к поиску по указанному имени номера строчки
описания этого параметра в pzframe_type_dscr_t.param_dscrs
.
pzframe_mes_t
НЕ сможет содержать массив info[]
напрямую -- придётся как-то аллокировать.
pzframe_cfg_t
массивы param_info[] и param_iv[]
изымем -- нехай "юзеры" адресуются напрямую в
pzframe_type_dscr_t.param_dscrs[] (да, индексируясь по тем разрезолвленным
номерам).
cda_new_context()
.
cda_add_chan()
и уйдёт) -- для main' это будет argv[1], а для
knobplugin'а -- ... ну придумаем что.
...хотя тут может быть побочный эффект: ну ограничим, а сторонняя программа в fast-ADC запишет больше; и как тогда из большого вектора извлекать данные "дальних" каналов, если их там физически не будет?
18.04.2016@вечер-душ: по здравому размышлению --
Чтобы adc200me_data.c содержал именно описание СПЕЦИФИКИ блока, максимально избавленное от особенностей, продиктованных ограничениями реализации.
19.04.2016@утро-душ: насчёт "резолвинга" -- о чём думалось вчера:
Тогда надо б было ОБЯЗАТЕЛЬНО эти enum'ы заводить в NNN_data.h, чтоб "наследующие" (вроде manyadcs) имели доступ.
Но ведь проще использовать в качестве номеров drv_i/-номера! Там уже заведено всё нужное, да и доступны они без дополнительных действий всем желающим уже.
(Эта мысль пришла в голову еще до идеи с общей таблицей -- так параметры смогут быть не только int32.)
CxAnyVal_t
, и отдельно
какой-нибудь указатель вроде "current_val", используемый для не-скалярных
каналов (точнее, не помещающихся в CxAnyVal_t
) и равный NULL
для помещающихся -- в точности, как в
cda_core.c::refinfo_t
.
19.04.2016: делаем:
pzframe_paramdscr_t
переименован в
pzframe_chan_dscr_t
(и ссылка из pzframe_type_dscr_t
-- в
chan_dscrs
).
PZFRAME_PARAM_RW_ONLY_MASK
превратилось в
PZFRAME_CHAN_RW_ONLY_MASK
и влилось в enum, общий с...
PZFRAME_CHAN_IS_PARAM
=0 и
PZFRAME_CHAN_IS_FRAME
=1, значения для...
pzframe_chan_dscr_t.chan_type
-- экс-rw_only
(и экс-param_type
)
dtype
и max_nelems
.
Замечание: добавлены они в конец, именно в таком порядке, поэтому:
cda_add_chan()
это фиксит),
...а учитывая, что "обычные" значения для change_important и chan_type -- тоже нули, то по факту для большиства строк в таблице достаточно писать только имя.
pzframe_cfg_t
теперь содержит только фиксированные
"поведенческие" параметры.
pzframe_type_dscr_t.chan_dscrs
).
pzframe_chan_data_t
, содержащий поля
void*current_val и CxAnyVal_t valbuf -- в точности, как в cda'шном
refinfo_t
; только на rflags и timestamp пока сэкономлено.
Поля этого типа в pzframe_data_t
:
cur_data
.
prv_data
, куда они должны
копироваться.
Видимо, надо копировать только скаляры/параметры, а данные основного кадра так дублировать незачем.
pzframe_mes_t
пока совсем НЕ при делах.
20.04.2016: далее:
*adc*_raw2pvl()
.
Но! Он ведь в этой по-входовой модели нам уже нафиг не нужен. Да и объёи у него о-го-го.
PZFRAME_CHTYPE_TRIGG
(больше для красоты, т.к. канал такой
будет один, и каждый драйвер просто явно будет ставить его в конец списка
возврата; ну еще SetChanReturnType(, IS_AUTOUPDATED_YES)
делать).
PZFRAME_CHAN_IS_TRIGGER
, на канал с которым и вешать evproc для
обработки пришедшего кадра.
20.04.2016@вечер: только это должно быть не типом, а ФЛАГОМ -- т.к. у одноканальных устройств (vcamimg, u0632, одноканальные осциллографы) триггером будет работать прямо сам канал кадра.
20.04.2016@вечер: а чем станет сам "тип" -- вообще вопрос. Предполагалось, что он будет указывать, параметр ли это и тогда надо копировать в "previous" (для сравнения), или же "кадровый" канал и копировать не надо. Но по факту можно просто копировать поле valbuf ВСЕГДА, не заморачиваясь.
Коий cpoint в данном случае описывать даже не очень понятно как.
20.04.2016@вечер: да, если хорошенько подумать -- то ведь к этому мы и стремились, делая прямо в драйверах раздельные каналы для разных "входов" и по-входовые каналы свойств, включая всякие LINE1ON: чтобы знания о специфике устройств максимально переехали в драйверы.
...и даже некоторый вопрос -- а зачем вообще нужны станут "общие" каналы DATA?
dev DEV_NAME ADC_TYPE ~ ... cpoint DEV_NAME.line1 R D
Такие фокусы вроде бы работают.
...с чем-то подобным косяки были (у MPS20 или где?), но там всё равно надо разбираться.
21.04.2016: реализуем:
PZFRAME_CHTYPE_MARKER
.
Плюс игнорирование этого канала в _rw_p()
.
PrepareRetbufs()
.
PZFRAME_CHAN_MARKER_MASK
.
PzframeDataRealize()
вешает обработчик на каналы с ним.
21.04.2016: продолжаем далее с fastadc_data:
fastadc_dcnv_one_t
вместо
v2'шных coeff,zero добавлен массив rds_buf[]
длины
FASTADC_DATA_RDS_MAX_COUNT
*2, а оный _RDS_MAX_COUNT традиционно
=20.
Массив как бы немаленький (20*2*8байт=320, и их по FASTADC_MAX_LINES=16 в каждом fastadc, =5120байт=5К), и вроде некрасиво, что он фиксированный. Но по факту сами осциллограммы обычно намного объёмнее, а объектов-fastadc не так много (штуки/десятки), так что проще забить, чем возиться с динамическим аллокированием (что сделано в cda -- но там совсем другое дело).
Замечание: пересчёт этот нужен будет ТОЛЬКО для отображения подписей/реперов и при сохранении данных в файл -- т.е., редко, и без требований к производительности. Отрисовка же вся, как и раньше, в сырых величинах.
28.06.2016: а какого чёрта всё с {r,d} запланировано в fastadc?! Ведь работа с данными -- прерогатива pzframe_data!!! Наверное, по привычке было сделано, не думая. Сейчас перетаскиваем всё в pzframe.
...а с dpyfmt как быть? :)
21.04.2016@вечер-дома: и чего, спрашивается, понесло сразу на fastadc_data? Сделал бы сначала vcamimg_data -- он намного проще, т.к. там ни пересчёта никакого нет, ни множественных линий :).
С другой стороны, тогда бы наткнулись на проблему "как делать множественность" позже, и её решение потребовало бы бОльших усилий.
22.04.2016: продолжаем:
pzframe_chan_data_t[]
-- данные "параметров" складируются
теперь в него.
pzframe_chan_data_t[]
(также из любого экземпляра!).
Требование поддержки "любости" позволит при надобности реализовать svd.
pzframe_chan_data_t.valbuf
.
Хоть буферы для данных бОльшего объёма в cur_data
и будут
аллокироваться, но в prv_data
-- нет.
23.04.2016@дома: кстати, насчёт панели управления в *gui:
Этот вариант хоть и выглядит красивее, но имеет проблемы:
Но это-то невеликая сложность -- биндить можно и вручную (благо, тут ТОЛЬКО ссылки на каналы).
Посему -- останемся всё-таки со старой моделью "всё делаем сами, безо всяких Cdr".
Тут, конечно, есть некоторые сложности:
mkstdctl()
?
Еще даже до осознания отрицательного ответа по п.1 было ясно, что СЕЙЧАС в любом случае надо сделать по-старому, а в будущем, возможно, перейти на текстовое описание дерева.
Итого -- в нынешней парадигме (в первую очередь с отделённым от datatree/Knobs слоем Cdr) удастся всё сделать исключительно по-старому.
Возможно, надо менять саму парадигму, делать всё как-то СОВСЕМ ПО-ДРУГОМУ?
25.04.2016: пилим fastadc_data:
FastadcDataCreateText2DcnvTable()
(тут просто неясно, как
делать, а главное -- ЧТО делать, и надо ли вообще).
И dtype линии тоже можно брать оттуда же.
fastadc_line_dscr_t
, и ссылка на таблицу --
fastadc_type_dscr_t.line_dscrs
.
27.04.2016: в продолжение по
fastadc_line_dscr_t
:
А по-хорошему -- и на прочие "свойственные" каналы. Чтоб свести количество КОДА в *adc*_data.c к минимуму, переместив всё по максимуму в "описания".
line_name_prefix
, line_name_list
,
line_unit_list
.
...некоторый вопрос с именем/идентификатором остаётся -- нужно ж как-то формировать метки для параметров вроде "ch1on" в PSP-таблице.
cn
" (Channel Number). Это аналог v2'шного "pn"
(Parameter Number).
Поэтому нельзя сразу просто "вот прочитать текущее значение num_lines, и сделать под него интерфейс".
Вывод: нужно мочь переделывать интерфейс "на лету", по изменению значения канала num_lines.
...когда-то, кажется, была мысль иметь текстовый канал с описанием панели управления на языке subsys. Но это выглядит [сейчас] совсем фантастично.
Или вообще делать это заботой не драйвера, а прямо сервера --
ввести 3-й специальный _dev-канал, "_devtype". Благо, вся
инфраструктура уже есть -- надо только эту строку "возвращать" не при смене
состояния, а единожды при старте; можно даже просто запонять всё прямо в
CxsdHwSetDb()
.
Вывод:
28.04.2016: для разнообразия позанимаемся уровнем gui -- благо, он-то от v2'шного отличается незначительно.
FastadcGuiRealize()
(present_cid+base).
LOCAL_LIBDEPS
.
PzframeGuiRealize()
САМ вызывал
PzframeDataRealize()
-- как и должно быть для "правильной"
реализации наследования.
Вот в v2 и сделано, что методы "предка" вызываются наследником-по-горизонтали.
И это правильно: т.к. у нас по горизонтали реальное наследование, а по вертикали -- ссылочное; и метод предка вызывает РЕАЛЬНЫЙ НАСЛЕДНИК (в смысле ООП) -- т.е., горизонтальный.
PzframeGuiRealize()
все параметры,
кроме собственно gui
.
CHL_DATA_PATTERN_FMT
.
Но это плохо!
Вывод: нужно вытащить эти #define
'ы с паттернами для
имён файлов в какой-то отдельный .h.
18.05.2016: и с CHL_STDCMD_nnn
бы тоже.
29.04.2016: продолжаем разнообразить:
cp
) с
последующим редактированием. Модификации --
fastadc_main.c -- временно закомментированы оба вызова *CreateText2*Table().
02.05.2016@дома: покуда еще нету "привязывания в сервере {r,d} одного канала к другому": надо уметь делать это прямо на стороне клиента в pzframe_data. Для чего ввести:
23.05.2016: добавлен.
CDA_DATAREF_OPT_NO_RD_CONV
выставляется при нём либо при
PZFRAME_CHAN_IS_FRAME
.
23.05.2016: тоже введено, rd_source_cn
. Но с
оговоркой: поскольку 0 -- легитимный "номер" канала/параметра, то
использоваться будет только при указанном
PZFRAME_CHAN_NO_RD_CONV_MASK
(но НЕ при просто
PZFRAME_CHAN_IS_FRAME
!); соответственно, если нужно отключить
фичу -- ставить rd_source_cn=-1.
Нужно это всё для всяких min/max/avg/...
18.05.2016: ползём дальше:
CommandProc()
переведена на строки, но бОльшая её часть
пока закомментирована.
p_xc
.
PK_CHANBASE
.
PK_BIGC
переименовано в PK_BASE
.
19.05.2016: ага, щас!!! Надо было почти ВСЕ spec'ы для SimpleKnob'ов адаптировать к v4.
DataKnob
.
19.07.2017: дополнение: сильно надоело (при создании
под-окошка [Ctrl...] в ADC4X250) повсеместно при создании grid'ов потом
делать их настройку -- Grid, Spacing, Padding. Поэтому добавлена
MakeFreeGrid()
, комбинирующая это всё ("Free"
-- что, в отличие от "A", parent'ом не обязательно должен быть другой grid
-- и, соответственно, позиционирование в нём не делается).
EXPORTSFILES=
/EXPORTSDIR=
во
многие 4cx/src/lib/*/Makefile (в половине отсутствовало).
PzframeDataSetRunMode()
--
скопирована из старого, но "мясо" закомментировано (т.к. там
cda_run_server()
/cda_hlt_server()
, которое более
не существует -- надо менять парадигму работы); она последнее, без чего не
линковалось.
Пока, естественно, не работает (SIGSEGV) -- нет огромных кусков, в первую
очередь в PzframeDataRealize()
. Но собралось, да!!!
19.05.2016: отдельный вопрос насчёт работы будущего pzframe_knobplugin.c: ведь ему надо бы при регистрации каналов как-то учитывать текущее "baseref".
PzframeDataRealize()
(и наследниках) параметр
base
-- это уже "имя устройства" (общий префикс, к которому
через '.' добавляются имена конкретных каналов устройства).
Видимо, нужно будет в pzframe_knobplugin.c делать
cda_combine_base_and_spec()
(она как раз не добавляет defpfx --
add_defpfx:=0
) добытой где-то "baseref" со "spec" устройства, и
уже результат этой операции передавать в
...gui->d->vmt.realize
в качестве "base".
19.05.2016: запинываем:
PzframeDataRealize()
: сделано аллокирование буферов --
точнее, аллокируется ОДИН буфер, а уже внутрь него расставляются указатели
на всё нужное: refs
,
cur_data
/prv_data
, поля current_val
отдельных ячеек.
current_val
добиваются до
кратности 16. Для этого также и размеры прочих блоков добиваются до
кратности 16.
PZFRAME_B_NO_SVD
пока не сделан никак.
Точнее, ВСЕГДА делается как будто он указан:
prv_data
делается всегда (оно необходимо для
отслеживания изменений по change_important
).
В конечном итоге функция получилась монструозноватая и не слишком-то
красивая: работа по подсчёту требуемого для current_val
'ов
места и его прописывание являются разными блоками кода (дублирование) и
несколько разнесены.
PzframeGuiRealize()
:
k_params[]
и
Param_prm[]
(раздельное!),
FastadcGuiRealize()
-- на
результат всё равно не смотрит. А если б и смотрел -- то
"PzframeDataDestroy()
" у нас всё равно нету, а понадобилась бы,
т.к. PzframeDataInit() перед этим уже вызвана.
PzframeGuiMkparknob()
: сделана (пока была пустой, всё
SIGSEGV'илось), стала намного проще старой.
fprintf(stderr, "%s", NULL);
вместо того, чтобы напечатать "(nil)" (как делал это в других
системах).
Проявилось на
KnobsCore_knobset.c::fprintf_stderr_knobpath
--
пришлось там ставить проверку на !=NULL, а при ==NULL передавать
"(NULL-ident)".
Решение, в принципе, разумное, и было принято еще в 2007-м (в KnobsCore_simple.c от 06-08-2007 уже так). Но явно в bigfile-0002.html это нигде не отражено.
(Повлияло как на adc200me_gui.c, так и на
fastadc_gui.c::()
.)
Текущий результат -- запускается, окошко есть, но "недоожившее", локальные настройки пока недоделаны. Далее -- надо запинывать таблицы для PSP.
20.05.2016: таблицы:
FastadcDataCreateText2DcnvTable()
: просто копия, с
удалением coeff и zero.
Сразу "включились" каналы (отображение -- chanNN/nochanNN).
FastadcGuiCreateText2LookTable()
-- еще проще, она была
просто за-#if0
'ена, нужно было лишь раскомментировать.
Окно приобрело нормальный размер.
FastadcMain()
раскомменчено,
ессно.
...ага, ясно: это как раз из-за "всего, касающегося dsp/pvl" --
gui_pvl2dsp()
сейчас состоит из "return 123.45
".
Т.е., ни диапазонов это реально не касается, ни на отображении графиков оно
не скажется.
23.05.2016: ...
PzframeDataEvproc()
наполнен функционалом.
При этом поправлен косяк с излишне помечавшимся как is_autoupdated MARKER'ом.
Видимо, нужно ввести отдельную CDA_DATAREF_OPT_ON_UPDATE плюс в
cda_dat_p_new_chan_f()
параметр "options" для передачи этого
флажка.
24.05.2016: м-м-м:
CDA_DATAREF_OPT_ON_UPDATE
введена.
PzframeDataRealize()
теперь регистрирует с
флагами ON_UPDATE и PRIVATE (это требование cda -- иначе может смешать в
один dataref каналы с одним названием, но разными опциями).
PzframeGuiEventProc()
наполнена кодом. И получены первые
рисующиеся графики (шум около нуля; но и на CALIBRATE реагирует) -- ура!
PzframeDataEvproc()
и ProcessAdcInfo()
:
P.S. А вот эти rflags вполне тянули бы на включение в pzframe_mes_t, если б таковой был. Но нету.
info2mes()
. А в идеале бы -- автоматически, по описателю в
fastadc_line_dscr_t
.
...но пока оставим как есть -- оно так проще.
И что --
CX_MON_COND_ON_UPDATE
, запрос на чтение слался бы сразу же по
получению данных?
В принципе -- это терпимое решение, хотя и весьма некрасивое.
Видимо, какой-то косяк OpenMotif; ну или, по крайней мере, на уровне MotifKnobs.
При нормальной работе -- при обновлении ручек -- конечно, проблема не проявится; но всё же надо разобраться.
25.05.2016: да, откровенно косяк в OpenMotif. Есть и в v2 тоже, и в разных версиях RedHat.
25.05.2016: сегодня Самойлов высказал пожелания, чего бы он хотел мочь делать в программе для ADC812ME (к которому ему подключат кучу сигналов): чтоб можно было смотреть разницу показаний между любыми двумя точками ЛЮБОЙ ПАРЫ каналов (т.е., наше "разница значений между реперами (в двух точках ОДНОГО канала)" -- не катит).
Вроде запросы не бог весть какие, но вот "запросто" ж не сделать. А как? Что приходит в голову:
Возможно, чтоб табличка жила в отдельном popup-окне (как "Stats...", или даже вместе с ним).
25.05.2016: необходимые модификации внутренней организации:
info_changed
генерализуем в
info_int
, чтоб иметь возможность передавать и иную информацию
(прицел -- на номера каналов при поштучном (on_update) обновлении).
Догенерализовывать до info_ptr
смысла не видно.
evmask
,
но пока на это решено забить.
pzframe_chan_data_t
добавлены поля
rflags
и timestamp
.
Наблюдение: этак скоро _chan_data_t дорастёт по содержимому до
data_knob_t
. Тогда и надобность в отдельном
pzframe_gui_t.k_params[] отпадёт -- есть data_knob_t.w
.
pzframe_data_t.rflags
, заполняемая OR'ом
флагов ото всех IS_FRAME-каналов.
pzframe_gui_t.curstate
в
pzframe_data_t
-- рядышком к rflags, из которых оно и
получается.
Но даже тип knobstate_t
там недоступен -- стек слоёв так
устроен (ну типа что *_data -- уровень cda, а уровень Knobs начинается
только в *_gui).
PzframeDataEvproc()
, а не в pzframe_gui.
Что потребовало бы вытаскивания метода "newstate" из _gui в _data, что сей момент не выглядит хорошей идеей.
PzframeDataGetChanInt()
и
PzframeDataGetChanDbl()
DoRenew()
вместо лазанья в параметры
теперь используется ранее заготовленный
gui->a.mes.cur_numpts
(было проведено
целое исследование, подтвердившее, что ProcessAdcInfo()
(где
mes.cur_numpts
вычисляется) вызовется РАНЬШЕ
DoRenew()
-- в обоих случаях ноги растут из
PzframeDataCallCBs()
, но _data'шный обработчик вызовется как
метод vmt.evproc, а gui'шный -- далее, как один из списка
зарегистрированных).
Но НЕ в девайсовых *_data.c, где куча обращений к valbuf.i32 -- т.к. это всё сплошь в info2mes() и x2xs(), которые вот-вот уйдут.
26.05.2016: оживляем:
PZFRAME_CHAN_IMMEDIATE_MASK
.
Кстати: неприятно, конечно, что для каналов вроде ELAPSED значение возвращается СРАЗУ, и в режиме ON_UPDATE оно попрёт с максимально возможной скоростью. Единственная надежда на cxsd_fe_cx.c, который это безобразие отфильтрует (своей логикой отлова рекурсии) до частоты цикла, но всё же нехорошо.
А не делать ли таким каналам как раз СБРОС флага ON_UPDATE, чтоб они уж по циклу отображались? Или хотя бы отдельный флаг, могущий быть указан в дополнение к IMMEDIATE, что именно "поциклово" заказывать?
PZFRAME_CHAN_ON_CYCLE_MASK
тоже добавлен.
PZFRAME_REASON_PARAM
ProcessOneChanUpdate()
.
PzframeChanEvproc()
-- надо знать ДВА параметра (pfr и cn), а
privptr2 у нас всего лишь один, то:
Param_prm[]
перетащены из
pzframe_gui в PzframeDataRealize()
; массив переименован в
dpls[]
; ...
pzframe_gui_dpl_t
в
pzframe_data_dpl_t
,
PzframeGuiMkparknob()
+ParamKCB()
адаптирована -- оказалось, что в последнем никакое "gui" и не надо, там
нужен именно pfr!
Кстати, анализ кода показывает, что оно так же было и в v2; как минимум с момента перехода с только-fastadc на обобщённый pzframe.
Посмотрел я на сей переезд в dpls[]
, и сказал
"это хорошо!". Так оно намного элегантнее и натуральнее -- данные дуплеты
связаны именно с параметрами (а не с gui), и даже при NO_ALLOC всё будет
правильно (этот флаг имеет смысл только у ARTIFICIAL-экранов, где никаких
параметров нет).
PzframeChanEvproc()
очень прост --
1) вызывает вычитывание, затем 2) уведомление о событии
(info_int=cn).
UpdateParamKnob()
.
Причём колоризация делается на основе rflags, а не просто KNOBSTATE_NONE (так было отловлено, что adc200me_drv.c
Почему-то канал SERIAL показывается гусино-defunct'ным. 27.06.2016: причина понята еще 21-06-2016, а сегодня всё исправлено -- канал стал показываться нормально, без посинелости.
PZFRAME_REASON_PARAM
.
PzframeGuiUpdate()
(а НЕ прямо из реакции на
PZFRAME_REASON_DATA
) -- чтобы при загрузке из файла тоже бы всё
обновилось.
Итак -- всё зажило!!!
ProcessOneChanUpdate()
.
29.06.2016: уже сделано, теперь всё конвертится (см.
ниже за сегодня) -- и параметры (самой cda), и данные для отображения
("руками", но через cda).
ДАЛЬНЕЙШЕЕ: 0) Аксессоры GetParamInt() и GetParamDbl() 1) постоянный перезапрос ON_UPDATE-каналов в cxsd_fe_cx. 2) Что там за косяк с adc200me::SERIAL? 3) Почему по [Stop] после калибровки на экране сохраняется старая картинка (от калибровки)?
29.05.2016@~17:00-заход-на-работу-за-водой, идя назад мимо 4-го здания: надо как-то реализовывать pzframe_knobplugin'ы. Но как?
Но в нынешней ситуации всё очевидно не так.
29.09.2016: однако см. комментарий (отдельным пунктом-разделом) ниже за сегодня.
то, может, стоит реализовать их "впрямую", как некоторый хак: ввести специальный отдельный тип ручки, "pzframe", который имел бы единственный специфичный параметр -- "src"?
Такое решение позволит сделать всё хоть и не шибко красиво, зато быстро и надёжно.
29.05.2016@вечер-дома: да, делаем:
DATAKNOB_PZFR
.
dataknob_pzfr_data_t
содержит всего 2 поля:
tree_base
-- заполняется Cdr'ом из текущего (на момент
прохода узла) в CdrRealizeKnobs()
.
Да, это очень криво, но это решение текущей проблемы "где взять baseref".
src
-- указывается параметром в исходнике.
data_knob_t.u
она входит под именем "z
"
(data_knob_t.u.z
).
30.05.2016: а теперь собственно *_knobplugin.[ch]:
k->w
).
FastadcKnobpluginDoCreate()
-- int(DataKnob) вместо
CxWidget(knobinfo_t*).
kpn
, а
постулируется, что он расположен в самом начале k->privrec
-- как, де-факто, и раньше было.
k->w
) RedRectWidget'а
переложена на PzframeKnobpluginDoCreate()
(с его клиентов).
...конечно, весь этот RedRect -- лишь робкая попытка делать всё корректно, т.к. destroy'енье у нас всё равно отсутствует почти повсеместно; но всё же хоть что-то.
31.05.2016: добиваем knobplugin'овость:
extern
) и adc200me_knobplugin.c (тоже сильно
упростился -- 21 строка против 43 раньше).
...сначала -- вручную.
Работа мутная, состоявшая из 2 частей:
sed
'ом паттерн
DEVTYPE_LCASE заменяется на тип устройство маленькими буквами
(adc200me), а DEVTYPE_UCASE -- заглавными (ADC200ME; заглавность
делается tr
'ом).
Т.е., просто напихано 6 штук (по количеству пар X.dep:G.h) зависимостей для .dep-файлов. Плюс отдельная для pzframeclient_knobset.d.
...при добавлении VCAMIMGS количество этих строк-зависимостей приподувеличится.
Работа, конечно, была противной, зато впредь НЕ надо будет бесконечно дублировать/копировать код при добавлении новых типов устройств.
$(FASTADCS)
на
$(DEVS)
.
Тогда не потребуется никакой pzframeclient и дублирования кода не будет.
02.08.2016: а покамест просто исходник pult.c подшаманен так, чтобы позволять "внешние дополнения", и теперь pzframeclient.c делается симлинком туда.
Проверено на простейшем тесте -- работает!!!!!!!!!
01.06.2016: развиваем успех:
Поскольку заполнять
adc812me_data.c::adc812me_chan_dscrs[]
8-ками
одинаковых каналов было лень, то сделана пара макросов --
DSCR_X8()
и DSCR_X8RDS()
, создающие сразу по 8
строк (второй отличается тем, что проставляет поле
rd_source_cn
). Надо будет в ADC4 аналогично сделать.
Дальше надо слабать CAMAC'овский ADC200 (уже в нормальной микровольтной кодировке вместо байтовых кодов) и сделать vcamimg для ottcam'а.
03.06.2016: за вчера и сегодня:
В последней паре (4-канальных) для *_chan_dscrs[]
используются макросы DSCR_X4()
и DSCR_X4RDS()
,
аналогичные 812'шным.
03.06.2016:
pzframe_mes_t
(коий в v4 рассосался), а специфичного ничего не
было. Соответственно, и info2mes тоже отсутствует.
Если что-то из этой толпы вдруг занадобится (хотя идеологически неясно, зачем бы), то вернуть будет несложно.
data_cn
, указывающий на ячейку с данными.
data_cn
и описатель канала, на который он показывает.
23.09.2016: ну естественно -- реакции-то в
pzframe_main.c::CommandProc()
не было. Добавлена --
работает. Вот только "отражения" состояния в кнопках нету, т.к. v4'шный Xh
не поддерживает XhSetCommand*()
. Часом позже: в Xh прототипы
добавлены (реализация пока "пустышками"), посему и в pzframe_main.c
вызовы раскомментированы.
18.06.2016: разобрались, дело в лишнем вызове
read_measurements()
в pzframe_drv (см. в его разделе).
04.06.2016@дома-ближе-к-полуночи (закончено в 23:40): решил по-быстрому спортировать ottcam -- благо, там всё проще, чем у fastadc'ов:
Вопрос только -- где именно окажутся косяки или вообще будет падать.
(Ну да -- параметры настроек синие, т.к. игнорируются, но это совсем неважно.)
Ура, работает!!!
05.06.2016: сборка hw4cx/pzframes/ включена в hw4cx/Makefile на общих основаниях.
06.06.2016: неудобно, что если имя канала указано неверно, то никакой диагностики нет -- ни почернения, ни на stderr. Фиг поймёшь ошибку.
07.06.2016: кстати, ручки ПАРАМЕТРОВ уже сейчас чернеют при ненайденности канала.
10.03.2021: а вот и не всегда и не совсем! (возможно, в т.ч. из-за необходимости "маркера", которого не будет) Подробнее см. ниже в специально созданном разделе от сегодня.
Но чтоб это произошло, нужно чтобы произошло событие "маркер" (которого при ненайденности не будет).
Нужно:
cur_data[*].rflags
), после чего как-то
передавать это событие на уровень gui, чтоб он всем param-ручкам сделал
SetSimpleKnobState(,KNOBSTATE_NOTFOUND)
.
Это всё для хитрых случаев, когда часть линий по какой-то причине будет отсутствовать, а потом вдруг появится. Т.е., для хитрых комбинаторных устройств (вроде manyadcs) либо для композитных, составляемых cpoint'ами (в т.ч. из разных серверов -- актуально будет после запинывания UDP-резолвинга).
После обеда: сделано.
PZFRAME_REASON_RSLVSTAT
=5 (как cda'шный (25.08.2016: вместе с ним переведён на =10.)), это
событие генерится PzframeDataEvproc()
'ем и ловится
PzframeGuiEventProc()
'ем, уставляющим чёрное состояние всем
param-ручкам.
FastadcGuiRenewPlot()
.
label_ks[]
.
08.06.2016: следующее сделано уже сегодня, но запишем тут же, для целостности.
set_knobstate()
делается кем-то при отработке
"действия" ручки (кем-то вдоль длинной цепочки, через которую этой действие
проходит, чтобы быть -- обычно -- в конечном итоге отправлено серверу).
...как выяснилось позже, это было верное предположение, но дальше разборки пошли не в ту степь.
FastadcGuiRenewPlot()
(для всех), так и из
ChnSwchKCB()
.
SetChnSwchState()
, а
потом...
currflags
и надо бы
заодно и в них записывать CXCF_NOTFOUND.
А потом -- в порядке оптимизации/управильнивания --
choose_knobstate(k,k->currflags)
.
ChnSwchKCB()
ничего делать и не надо!
set_knobstate()
,
на основе currflags
, делается в
set_knob_controlvalue()
при fromlocal
.
Коий прямо вызывается из MotifKnobs_SetControlValue()
,
дёргаемого CB()'ами всех типов ручек (включая onoff) для передачи
"действия".
23.06.2016: в сторону избавления от info2mes() --
делаем в ProcessAdcInfo()
:
line_dscrs[]
добываются
numpts
: может быть указан константой (через '-');
on
: при неуказанности источника считается за =1;
x_buf
, x_buf_dtype
: в зависимости от on
(при on==0 -- NULL,CXDTYPE_INT32,numpts:=0).
cur_range
: оказалось самым хлопотным, ибо:
fastadc_line_dscr_t
добавились поля
range_min_cn
и range_max_cn
.
.range
тоже!), для чего -- воизбежание повторов и нечитаемости -- заполнение
переведено на макросы LINE_DSCR()
везде, кроме c061621.
По умолчанию прописывается значение константного
line_dscrs[nl].range
.
28.06.2016: и еще надо было SymmMinMax сделать. Сделал.
common_cur_numpts_cn
заменено на
выборку максимального из по-линейных numpts'ов (которые уже могут, при
неуказанности источника, браться из common_cur_numpts_cn
).
24.06.2016: продолжаем:
FastadcDataInit()
также переведена на использование
line_dscrs[]
.
fastadc_type_dscr_t.range
стало ненужно
совсем, и оно удалено, в т.ч. из FillDscr()'овых параметров (оттуда --
вместе с dtype
, требовавшимся лишь для него же).
common_dpyfmt
, поскольку
он теперь "умолчательный" -- если по-линейные не указаны.
line_units_list[]
также убран.
line_names_list[]
.
line_dscrs[]
теперь
ОБЯЗАТЕЛЬНО, размером в num_lines.
28.06.2016: делаем пересчёт по {r,d}.
В
pzframe_chan_data_t
добавлен буфер
rds_buf[PZFRAME_DATA_RDS_MAX_COUNT*2]
(да, 20*2double=320байт;
но у нас максимум 100-150 каналов в АЦП, 32кБ -- ну переживём) и количество
-- rds_count
.
ProcessOneChanUpdate()
КАЖДЫЙ РАЗ -- халтура!!!.
Так вот -- raw2pvl нужна только для REPR_INT, и она может не указываться (=NULL), тогда будет использоваться дефолтная, просто преобразующая int в double.
line_dscr_t
указывать НАЧАЛЬНЫЙ "r" -- чтоб
подписи делать.
28.06.2016@вечер-уходя-с-работы: та архитектура выглядит кривовато:
А можно бы следать лучше:
gui_pvl2dsp()
пусть его и вызывает.
29.06.2016: реализуем:
cda_rd_convert()
. Она результат конверсии
складывает по указателю, а возвращает 0/-1, как прочие, принимающие первым
параметром ref.
fastadc_line_dscr_t.default_r
и прописано
во всех TYPE_data.c.
gui_pvl2dsp()
теперь пользуется cda_rd_convert()'ом при
полученности калибровок, либо default_r'ом в противном случае.
PZFRAME_REASON_RDSCHG
(=3, как в
cda/cxsd_hw).
PzframeRDsCEvproc()
с маской RDSCHG,
который и генерит событие из предыдущего пункта.
DoEvproc()
, который тупо идёт по списку линий и
смотрит, что если указанный для этой линии data_cn
совпадает с
об-RD'шившимся, то взводит в 1 nl'ную ячейку в ...
fastadc_data_t.lines_rds_rcvd[]
, ...
gui_pvl2dsp()
.
rds_had_changed
,
взводящееся по событию и используемое в качестве старта для info_changed и
затем сбрасываемое.
10.04.2022: "взводящееся", ага! ПОДГОТОВЛЕНО тогда было, а вот взведение =1 было забыто. Сегодня добавлено; надо посматривать, не вылезет ли где чего...
gui_pvl2dsp()
напрямую к cda плохо подойдёт для manyadcs. Там
придётся как-то изворачиваться. 01.07.2016@утро-душ: очевидно, надо
подтасовывать свойства (в atd->line_dscrs[] плюс в refs[]) -- подсовывать
ref от реального source-канала. И, кстати -- такой
"составной" осциллограф надо будет попытаться обобщить, он может быть
полезен и в других местах (тот же two812ch -- не его ли
подвид?). @вечер-613-я: и да, и нет:
нет, two812ch -- НЕ "подвид" manyadcs; но да -- two812ch является как раз
тем самым общим "над-осциллографом".
P.S. И отдельно -- а с сохранёнными файлами как, при оффлайн-загрузке их?
rd_source_nc
-- раз конверсия переложена на cda (а не
выполняется самостоятельно), то поле так и не задействовано.
Хотя в принципе и можно бы "руками":
Конечно, это прокатит только со скалярами, но иное нам пока и не требуется.
14.07.2016: тогда, полмесяца назад при разбирательстве с pvl/dsp, забыто было доделать оное "для собственных нужд fastadc_gui" -- для реперов.
Соответственно, и "gui_pvl2dsp()" должен не сам всю конверсию делать, а пользоваться _data'шным методом.
FastadcDataPvl2Dsp()
, содержащая код конверсии,
сделанный 29-06-2016 в gui_pvl2dsp()
.
В отличие от v2, тут НЕТ параметра "mes".
gui_pvl2dsp()
переведена на неё.
FastadcDataGetDsp()
-- калька с v2'шной.
Для её удобства внутри есть static inline
функция
FastadcDataGetPvl()
-- также почти калька с v2'шной и
Xh_plot'овского куска кода, но только безо всяких raw2pvl.
12.09.2016: перетаскиваем библиотеки из hw4cx/pzframes/ в 4cx/src/ -- include/ и lib/pzframe/.
27.09.2016: добавлена ("доделана") поддержка leds.
PopulateCpanel()
действия сократились до простого вызова PzframeGuiMkLeds()
.
PzframeGuiMkLeds()
же проверяет, что
local_leds_form!=NULL, и считает это сигналом о необходимости сделать
"маленький" LEDs.
Ему теперь не передаётся ни parent, ни in_toolbar -- всё фиксированно и просто.
По логике, в программах с тулбаром спокойно можно делать look.noleds=1.
Да, так и сделано -- мозги на эту тему добавлены в
PzframeKnobpluginDoCreate()
.
10.01.2017: странный косяк вылез -- тогда незамеченный, но именно тогда появившийся (проверено тестированием версий за разные даты): конкретно v5h1adc200s.subsys почему-то стал пустым -- только statusLine, а workSpace пуст. Точнее, в нём лежит lrtbForm размером 1*4 пиксела, и даже её растягивание (включением resizable) не помогает -- пустота.
Включение тулбара (убирание notoolbar) ситуацию спасает: содержимое появляется. Очевидно, за счёт того, что leds появляются на тулбаре, а в pzframe_knobplugin'ах при этом отключаются.
11.01.2017: в продолжение расследования:
Заодно оно и resizable сделано.
Там есть свои косяки -- после схлопывания cpanel'а по [-] он обратно по [+] не разворачивается, пока не дрыгнешь размер (хоть ресайзом окна, хоть сдвигом "шторки" split'тера).
MotifKnobs_leds_grow()
.
Если этот блок кода отключать -- т.е., не создавать в сетке child-виджетов -- то всё окей. (Можно ей размер (в клетках) увеличивать -- не влияет; а вот создание child'ов -- ёк...)
Видимо, опять косяк с XmSepGridLayout, в обсчёте геометрии -- оные уже встречались (например, при hfill'е всех ячеек в колонке эффект аналогичный).
09.06.2016: Конкретно:
Напрашивается идея сменить один параметр "МНОЖИТЕЛЬ" на пару "*МНОЖИТЕЛЬ/ДЕЛИТЕЛЬ". У кого всё просто -- указывают только множитель, а делитель=0, и он считается =1.
10.06.2016@утро-дома: и еще:
P.S. Да, это делать не сей момент, а чуть позже. Место --
fastadc_data.c::ProcessAdcInfo()
, который, по факту,
должен полностью освободить он необходимости в *adc*_data'шных
info2mes()
и x2xs()
.
11.06.2016@утро-пляж: тогда уж надо добавлять не 1 параметр (DIVISOR), а 2. Второй -- "XS_FACTOR": показатель десятичной степени понятия "xs" (-3 -- mini, -6 -- micro, -9 -- nano, ...). Это соответствует давнему желанию украсивить подписи к горизонтальной оси -- чтобы вместо "250000ns" было "250us").
Обсуждение:
Видимо, написать в столбец несколько примеров.
11.06.2016@утро-пляж: насчёт "ext_tim_point_size": вопрос, в чём этот канал должен быть.
@там-же, пятью минутами позже: Но вот тоже нифига! Т.к. в этом случае модель "XS_FACTOR" (с авто-добавлением суффиксов ps/ns/us/ms/s) работать не сможет, по крайней мере, ТАКАЯ простая модель "if (xs!=0 && xs%1000==0) {xs/=1000; factor+=3;}".
12.06.2016@дома: а места-то под 2 канала далеко не везде есть. Особенно в c061621 -- там всё очень плотно.
13.06.2016@утро-пляж: XS_DIVISOR, XS_FACTOR -- просто НАДО взять и ввести, пусть и с раздвижкой карт.
15.06.2016@утро-душ: в fastadc ссылки на параметры, которые каналы, некоторые можно сделать "хитрыми":
Смысл в том, что так можно будет имитировать эти параметры у устройств, в которых они отсутствуют. (Вряд ли актуально, конечно, но мало ли -- вдруг пригодится для какого-нить девайса, работающего под другой СУ.)
17.06.2016: о раздвижке каналов: можно "раздвинуться вверх", в сторону управляющих каналов, где есть резерв (в картах всех устройств).
21.06.2016: замечание: -- эта модель -- включая "factor" также должна поддерживать и "вычитание", -- т.е. работать и с дельтой реперов, которая может быть отрицательной.
Впрямую это коснётся UpdateRepers()
-- там для вычисления
дельты надо будет не запоминать результирующие rpr_xs[], а прямо на месте
вычитать rpr_at[1]-rpr_at[0] и к ним применять процедуру x2xs(). 19.07.2017: да нифига, НЕЛЬЗЯ к дельте (rpr_at[1]-rpr_at[0])
применять процедуру x2xs(), потому, что оная процедура учитывает значение
PTSOFS, а значит, применима только к позициям, но НЕ к их дельте. Так что
"withdrawn".
21.06.2016: делаем.
adc->ext_xs_per_point_val
.
fastadc_mes_t
. Чтобы
ProcessAdcInfo()
выполяется:
adc->mes
текущих значений.
ext_xs_per_point
из
adc->ext_xs_per_point_val
.
Т.е., значение "внешней частоты" фиксируется в момент обновления кадра (когда она и актуальна), а при последующих её обновлениях -- на текущую картинку никакого влияния не оказывается.
ext_xs_per_point
будет браться из файла;
ProcessAdcInfo()
.
FastadcDataX2XS()
(параметр --
mes_p
), реализующая логику "как пересчитать по имеющимся данным
x в xs".
07.07.2016: вводим в драйверах каналы XS_DIVISOR и XS_FACTOR.
Кстати, в adc502.devtype был косяк -- часть каналов от adc200 например, (frqdiv).
И, за компанию, adc4 раздвинут аналогично -- тоже ж 4-канальный, всё похоже.
08.07.2016: продолжаем.
В будущем же, по-хорошему, надо бы у всех, допускающих внешнее таймирование, делать 1000 -- для хорошей точности указания времён.
...хотя с ADC200ME в 32 битах будут проблемы: у него 2^20 точек, а с фактором 1000 и "множителем" 5 (или сколько будет ns/отсчёт) оно уже вылезет из 2^32-1.
10.07.2016@вечер-душ: да
какого чёрта -- использовать
scale32via64()
.
FastadcDataFillStdDscr()
добавлены параметры
xs_per_point_cn
, xs_divisor_cn
и xs_factor_cn
-- сразу после common_cur_numpts_cn
.
08.07.2016: вылезла неприятность: в
adc333_drv.c те параметры, что "уточняются" в
StartMeasurements()
-- конкретно NUMPTS, в зависимости от
nlines -- никак не отражаются наверх в "уточнённом виде".
Причина довольно очевидна -- "уточнение"-то делается в
cur_args[]
, а сами параметры имеют тип VALIDATE, так что вместе
с измерением назад не вертаются.
@вечер-дома:
nxt_args[]
и тут же делать
Return...().
Return1Param()
.
StartMeasurements()
заменены на вызов
Return1Param()
, причём во ВСЕХ драйверах.
А "уточняется" повсеместно значение PTSOFS, так что оно бы тоже не отражалось.
12.07.2016: да, оно улучшилось -- теперь "исправления" параметров отражаются и на экране. Проблема в том, что с задержкой в 2 кадра (например, в 2 shot'а): "сразу" (0-й) -- не отрабатывается из-за запущенности измерения (см. 201012-OSCILLOSCOPE_MEMORANDUM.txt); 1-й (когда приходит 65535) -- пропускается из-за wasjustset (хотя cdaclient показывает, что значение изменилось); а уж 2-й и приносит всё.
10.07.2016@дома: допиливаем с XS_DIVISOR в драйверах:
ReadMeasurements()
аналогично 108).
03.11.2018: почему тогда НЕ было сделано xs_divisor=1000 у ВСЕХ?! Подробнее на эту тему ниже, за сегодня.
29.07.2016: одна недоработочка: для вычисления
FastadcDataX2XS()
нужно также знать значение PTSOFS. Во всех
индивидуальных *_x2xs() оно просто прибавлялось к x
, но в
fastadc_data.c его взять негде -- забыли!
Добавляем:
fastadc_type_dscr_t.common_cur_ptsofs_cn
.
FastadcDataFillStdDscr()
::common_cur_ptsofs_cn
.
fastadc_mes_t.cur_ptsofs
.
ProcessAdcInfo()
: добыча+сохранение значения в оное поле.
FastadcDataX2XS()
: x+=mes_p->cur_ptsofs
.
*_get_type_dscr()
: передача
номера CUR_PTSOFS'а Fill()'у.
03.08.2016: и еще кое-чего в модели не хватает для полноты: СДВИГА. Т.е., из "seconds=offset+X*xs/divisor" множитель -- есть, а offset -- отсутствует.
Хотя для VSDC2 вообще можно забить на всё-про-всё -- там осциллограммы являются побочным средством, а "качество" их вычитывания (кривой протокол) в принципе не позволяет рассматривать их серьёзно, поэтому от них нужна только форма сигнала, а на времена плевать совсем.
30.09.2016: раскомментирован "блок вычислений
касательно XS" в конце ProcessAdcInfo()
-- иначе не работала
FastadcDataX2XS()
(уже используемая для сохранения).
Резон -- уже во всех текущих драйверах есть отдача нужных параметров.
На вид -- всё работает корректно.
...этак скоро можно будет от info2mes()
избавляться -- всё
уже есть.
19.07.2017: завершение перехода на
FastadcDataX2XS()
-- в fastadc_gui.c оставалось 3
точки.
UpdateRepers()
fastadc_gui_mkstdctl()
, ветка
FASTADC_GUI_CTL_REPER_TIME
gui_x2xs()
Довольно несложно, и вроде всё работает.
03.11.2018: вот понадобилось наконец-то пользоваться фичей "ext_xs_per_point", чтоб осциллографичности показывали осмысленные времена, а не просто номера отсчётов. И оказалось, что фиг-фиг-фиг -- не особо-то оно готово к использованию.
Т.е., у тех, где либо есть параметр/канал FRQDIV, либо используется мультиплексируемый АЦП (adc333).
FastadcDataX2XS()
, БЕЗ использования типоспецифичных _x2xs(), а
все "вычисления" выполняются в
fastadc_data.c::ProcessAdcInfo()
?
Или секрет в еще не убранном вызове типоспецифичных _info2mes()?
Или у прочих отдаются НЕ скалированные на 1000 значения в XS_PER_POINT? Судя по adc502_drv.c -- именно так.
Например, при периоде 1.2345ms/clock получится ext_xs_per_point=1234 (или 1235). При этом для x=1000 получится xs=x*multiplier/divisor=1000*1235/1000=1235, а для x=10000 -- xs=12350, хотя должно б быть 12345.
И как сие решать, без перехода на вещественные?
03.07.2019: при попытке сделать плагин для PXI6363
аукнулось, что xs_per_point у нас принудительно целочисленный (и это
буквально "прибито гвоздями" -- например, в ExtXsPerPointEvproc()
сразу делается cda_get_ref_data()
прямо по адресу переменной,
безо всяких проверок типов и конверсий).
А проблема в том, что в DAQmx'ном API указывается "скорость" в сэмплах в секунду, ВЕЩЕСТВЕННЫМ числом. Соответственно, xs_per_point=1/rate -- без вариантов вещественное.
@вечер-Будкера, идя забирать Марка от
Саши: кстати, в такой ситуации мог бы выручить xs_divisor:
устанавливать его =rate (а xs_factor=0), вот и получалось бы нужное число.
Но xs_divisor у нас, увы, тоже принудительно целочесленно (04.07.2019: и, кстати -- оно НИКАК не используется, только
добывается; видать, придумал в июне 2016-го, но до сих пор так и не
заюзал). Да, по-хорошему надо уметь поддерживать и
вещественные тоже. Проверять -- по reprof_cxdtype()
от типа
канала. Чтоб не делать много проверок "в процессе" (при отрисовке), можно
тестировать где-нибудь вначале (или в ProcessAdcInfo()
? как
это с загрузкой файлов провзаимодействует?) и устанавливать внутренние поля
в fastadc_data_t
либо, правильнее -- в
fastadc_mes_t
.
16.07.2016: копированием из vcamimg'а, с последующей контекстной заменой vcamimg->wirebpm, Vcamimg->Wirebpm, VCAMIMG->WIREBPM.
Стандартные имена переменных/полей такие:
bpm
(wirebpm_data_t, сам объект).
wtd
(wirebpm_type_dscr_t, описатель типа).
b
(wirebpm_data_t-поле внутри wirebpm_gui_t).
21.07.2016: продолжаем.
hgrm
(как в
старом ippclient'е).
21.08.2016: на этой неделе после почти месячной паузы продолжаем.
Краткий обзор принятых рхитектурных решений:
kind
-- как раз "hist" или "spectr".
Это всё ПОКА.
Да, OVERFLOW конкретных столбиков оно теперь показывать не будет (а
реально оно никогда не происходило -- из-за 14-битного формата данных).
Зато DrawHist()
сильно упростится.
double
-- загадка, т.к. по факту обращение с ними было как с
целочисленными. Видимо, исключительно потому, что
cda_getphyschanval()
отдаёт double; но что мешало использовать
cda_getphyschanraw()
-- неясно, она ж появилась еще в 01-2004.
Или всё из-за набора фона, который мог быть дробным (ибо 5 циклов)?
U0632_CHAN_...
, а локальные C_...
, вводимые в
u0632_internals.h.
22.08.2016: продолжаем.
DrawHist()
:
wirebpm_gui_t *gui
.
PIXELS_PER_ROW
и "/ (1 << 13)
" (последнее --
диапазон).
PzframeDataGetChanInt()
и
PzframeDataGetChanDbl()
создать аналогичные аксессоры к
элементам ВЕКТОРОВ: указывать номер ячейки, и чтоб оно корректно работало и
со скалярами.
Может, через тот же plotdata? Там ведь уже есть аксессоры.
Сейчас оно живёт полностью внутри wirebpm_gui.[ch], но при крайней надобности теоретически может быть и доформализовано и выделено в отдельные файлы/модули.
wirebpm_gui_t.wiregui_vmt_p
. Да, оно полностью отделено и от
pzframe_data_vmt, и от pzframe_gui_vmt.
do_tune
-- "произвести донастройку". Вызывается из
WirebpmGuiRealize()
.
Сейчас аллокирует GC, а в перспективе может и размеры задавать.
do_draw
-- отрисовка.
wiregui_vmt_p
--
WirebpmGuiRealize()
, на основе значения look.kind.
Короче -- сейчас оно не столько "гибко-расширяемо", сколько служит для удобства реализации функционала, требуемого в linipp. И, скорее всего, останется таким ограниченным и фиксированным -- иных потребностей не видно.
23.08.2016: добиваем -- собственно сами отрисовщики изготовлены.
do_tune()
-- чтоб EventHandler вешать.
13.10.2016: добавляем к ESPECTR'у реперы и вычисления (благо, гибкая настройка формул уже сделана).
Явно нужно ДВА метода tune -- пред-настройка и пост-настройка. Так и сделано:
do_prep()
(у HIST'а есть
только он).
do_tune()
.
Заранее думал, что параметр -- число частиц. А вот нифига: число частиц на результат вычисления просто умножается, а параметром выступает номер столбца.
Детали -- в следующем пункте.
wirebpm_data_t.dcnv
, имеющее
тип wirebpm_dcnv_t
, содержащий единственное поле
ext_i_fla
(тип -- char[200]
; стыдоба, но вроде
хватит).
WirebpmDataGetDcnvTable()
,
возвращающая указатель на dcnv-таблицу.
Это именно "Get", а не "Create" (как в fastadc).
ExtIEvproc()
, добывающий значение и
складирующий в ext_i_val
.
DoEvproc
-- оно
перекладывается в mes.ext_i
.
...да, ради этого шага пришлось добавить и поле mes
, и метод
.evproc
.
Забавно, что тут dcnv есть, а в vcamimg -- нет; а понадобится ли?
cvt2ref()
.
cvt2ref()
!
...ну да, идеологически оно как бы выполняет функции уровня Cdr, но всё же.
(И вообще, в lib/pzframe/ зависимости от Cdr отсутствуют.)
Просто сейчас первый раз возникла реальная потребность в подобном.
Очевидно, что при повторении подобных потребностей нужно будет двигаться именно в сторону "USER"-узлов. Просто не очень понятно, как же это бы реализовать поудобнее, чтоб knobplugin'ам сподручнее получать доступ к этим данным -- вопрос биндинга:
У них ведь тоже парсится имя-идентификатор (и метка!) плюс всякие дополнительности.
cda_rd_convert()
, но делается пересчёт
в момент отображения и по ТЕКУЩЕМУ коэффициенту, а в момент получения данных
он мог быть иным.
А возможен race condition, когда в момент отрисовки от сервера будет получена уже новая калибровка, а сами данные еще не дойдут.
mes
?) в момент получения данных.
Снижает (нулит) остроту проблемы то, что у нынешних fastadc-драйверов у ВСЕХ коэффициенты фиксированные и отдаются один раз при старте.
Засим раздел по wirebpm можно (на пока?) считать за "done".
И это притом, что вся нужная исходная информация у Cdr присутствует.
Идея: а что, если прямо в Cdr регистрировать каналы
"_devstate" с указанным в узле src
и текущим
baseref
?
20.07.2016: некоторые соображения:
Да и вероятность pzframe-девайсов по не-CX-протоколу представляется низкой.
20.07.2016@дома-вечер: сделано, всё ОЧЕНЬ просто:
dataknob_pzfr_data_t._devstate_ref
.
CdrRealizeKnobs()
оно заполняется, ...
pzf2ref()
. Оная заключается
cda_combine_base_and_spec()
текущей baseref и ручкового ref'а
-- в точности то, что делает PzframeKnobpluginDoCreate()
-- с
последующей регистрацией на этой базе канала "_devstate".
CdrDestroyKnobs()
должно бы делаться
cda_del_chan()
, но пока, по причине нереализованности, оно (как
и везде) закомментировано.
18.07.2018: теперь делается (раскомментировано). Но, строго говоря, у обычных DATAKNOB_KNOB оного нет вовсе.
Работает, ура!!!
Актуально, например, прямо сейчас для VSDC2 -- там хрен бы знал какой диапазон float32-чисел от осциллограммы.
29.07.2016: да, сделано.
#define
'ом DEFINE_MINMAX_FINDER()
и имеют
имена вида FindMinMax_DTYPE()
.
Результат потом, соответственно, также симметризуется.
Теперь проверять.
02.08.2016@вторничная-планёрка-на-круглом-столе: только с вещественными будет одна проблема: присланное число может оказаться NAN, а с ними никакие сравнения нормально не работают, поэтому любое такое число отравит весь диапазон (он станет [NAN,NAN]).
И если сравнение еще худо-бедно можно защитить проверкой, то начальную инициализацию как? Видимо, предварительным циклом "пока очередная ячейка содержит NAN, сдвинуться дальше" (ПЕРЕД проверкой "если count==0, то вернуть [-1,+1]").
03.08.2016: проверяем, на vsdc2_2.
info2mes()
, и он делается ПОСЛЕ всей этой логики.
После закомментировывания присвоений диапазонов в
vsdc2_2_info2mes()
-- заработало.
Т.е., уже приемлемо, для полноты надо всё-таки что-то придумать с NAN'ами.
04.08.2016: только еще один косячок: при таких
"самовычисляемых" диапазонах никак не сигнализируется
info_changed
-- а надо бы, т.к. подписи к вертикальным осям не
перерисовываются.
Строго говоря, СЕЙЧАС просто и нет возможности этого сигнализирования, но
его в любом случае нужно будет ввести -- оно нужно для сигнализации о смене
значения ext_xs_per_point_val
при exttim.
05.08.2016: делаем:
info_changed
производится в
PzframeDataEvproc()
, после чего она просто дергает вызов
evproc'ев.
info_int
),
но без какой-либо возможности менять его значение (каковая возможность есть
у Xt'шных event-processer'ов в виде continue_to_dispatch
).
PZFRAME_REASON_DATA
:
ProcessAdcInfo()
, в котором
есть потребность дополнительно взвести флаг.
Она вызывается в самом начале цепочки, из DoEvproc()
,
являющейся реализацией метода pzframe_data_vmt_t.evproc
.
DoRenew()
использует флаг.
Она вызывается как реализация метода
pzframe_gui_vmt_t.do_renew
, вызываемого из
PzframeGuiUpdate
, дергаемого из
PzframeGuiEventProc()
, являющегося "одним из" обработчиков,
вешаемых на pzframe_data.
Таким образом, потребитель вызывается после производителя, но это скорее
"добрая воля" в порядке навешивания/вызова evproc'ев (гарантируемая лишь
первоочерёдностью вызова pzframe_data_vmt_t.evproc
).
pzframe_data_t.other_info_changed
, в который цепочные
обработчики смогут писать =1, а вызывальщик обновления --
PzframeGuiEventProc()
будет её подхватывать, OR'я с основным
info_changed
и тут же нуля.
Побочный эффект: при надобности можно будет воспринимать
change_important!=0 даже у PZFRAME_CHAN_IMMEDIATE_MASK
-каналов,
добавив определение info_changed туда (точнее, надо будет
ProcessOneChanUpdate()
'у передавать int*
, куда
делать =1, и саму проверку ПЕРЕНЕСТИ в него же).
Если придёт в голову, как сделать лучше -- надо будет переделать.
17.04.2022: вот только сделанная тогда архитектура приводила к ГАРАНТИРОВАННОМУ делению на 0 при numpts=1 (или просто константном значении), т.к. после "поиска" найденные минимум и максимум всегда совпадали, так что их разность ==0, что приводило к SIGFPE.
Обнаружилось при тестировании onei32l на "суперсимулируемом" устройстве, когда в канал осциллограммы писалось 1 значение (а каналы диапазонов при этом заранее НЕ уставлялись, так что были ==0, вследствие чего и выполнялся поиск, приводивший к диапазону с совпадающими границами).
Исправляем в ProcessAdcInfo()
:
Падать перестало.
И В Xh_plot надо бы перед RESCALE_VALUE проверки вставить...
17.04.2022@душ, ~22:30: в Xh_plot'е (где, собственно и происходит SIGFPE) выглядит чуть сложнее -- сначала даже не понял, как бы именно сделать проверку в том "напряжённом" месте.
А потом озарило -- да ровно так же, просто ВНАЧАЛЕ проверять
cur_range
и при надобности модифицировать.
17.04.2022@засыпая: а по-хорошему вообще проверка с "расширением диапазона" должна
быть ТОЛЬКО в XhPlotOneDraw()
: ведь это в нём проблема с
делением на 0. (А в fastadc_data -- просто сбор статистики,
так что диапазон шириной 0 -- это вполне нормально).
18.04.2022@утро, ~07:00: но тогда надо это делать при отрисовке не только графика, а и осей тоже -- чтоб там было скоординировано. Значит, надо вынести проверку+расширение в отдельную общую функцию.
ЗЫ: а в histplot нет ли аналогичной проблемы? Неа, нету:
там в GetDispRange()
стоит явная проверка диапазона, что
указанный диапазон используется ТОЛЬКО при min<max, а иначе форсится
[-100,+100].
18.04.2022: делаем в Xh_plot.c:
CheckDispRange()
.
XhPlotOneDraw()
в начале добавлен её вызов.
XhPlotOneDraw()
переделана инициализация
локальных переменных значениями из *data
: изначально-то
инициализация была прямо в объявлении этих переменных, но ниже по коду есть
проверка
if (data == NULL) return;
что подразумевает теоретическую возможность NULL'овости (хбз, при каких
условиях), а при инициализации выше получим SIGSEGV.
Поэтому инициализация перенесена НИЖЕ этой проверки, а из объявления она убрана.
DrawPlotAxis()
-- собственно отрисовка;
XhPlotCalcMargins()
-- вычисления размеров полей.
Ранее в них были просто прямые обращения к
one->data->cur_range
и
one->data->all_range
соответственно, теперь же они
заменены на обращение к локальной переменной, в которую предварительно
копируется и о-CheckDispRange()
'ивается.
...что странно -- в XhPlotCalcMargins()
именно
all_range
; видимо, чтобы зарезервировать в поля
МАКСИМАЛЬНО возможное место?
ProcessAdcInfo()
принудительное расширение убрано.
Проверено -- вроде пашет.
Замечание: такой вариант с "расширением" прямо в Xh_plot отличается от первоначального (в fastadc_data) тем, что теперь НЕ делается симметризация (а раньше бы делалась, т.к. расширение производилось до неё). Но это вряд ли важно.
13.09.2016: в принципе, можно -- по "увеличению" включать следующий коэффициент, по уменьшению -- предыдущий.
XmScrollBar, кстати, полесо понимает. Так что, по-хорошему, надо бы ButtonPress/ButtonRelease скроллинга пересылать ему.
29.09.2016@семинар-NI-в-технопарке: насчёт прошенного вчера Самойловым более удобного масштабирования графиков: а может, сделать на графике кнопочку "разрешить автомасштабирование"? При включенности ВСЕГДА использовать самопоиск min/max по данным (из пред-предыдущего пункта).
Надо, НАДО думать о том, как бы это -- т.е., всехние _gui.c -- перевести с ручного создания "обвязки" на указывание строкового описателя (в subsys-виде), чтоб обвязка генерилась обычным образом и "приклеивалась" бы в качестве обычной DATATREE_CONT-подветки.
29.09.2016: очевидные сложности:
PzframeGuiMkparknob()
принимает во
внимание флаг "readonly" и удаляет ручки с битом
PZFRAME_CHAN_RW_ONLY_MASK
.
...или положиться на то, что при "readonly" выставляется
SIMPLEKNOB_OPT_READONLY
и пусть уж кнопки на экране будут, но
нефункциональны?
...а вот не так-то всё просто! Этот _OPT_READONLY есть в Simple-API, но
у обычного создания его нету! Тут надо будет обходить дерево и
принудительно сбрасывать всехние is_rw
(коее поле,
кстати, теперь живёт прямо в data_knob_t
; видимо, с момента
создания DATAKNOB_TEXT 11-08-2015).
valbuf
):
если (когда!) это будет делаться, то надо не буферы
копировать, а перебрасывать указатели current_val
: менять их
местами (так что prv_data[cn] будет указывать на предыдущие данные).
30.09.2016: процесс:
PzframeMainLoadData()
и
PzframeMainSaveData()
уже были.
>PzframeDataFdlgFilter()
скопирован из
v2.
param_lp_s
теперь не
"#.PARAM", а "#=PARAM" -- чтоб, не дай бог, старые файлы
не подошли.
FastadcDataFilter()
идентична старой.
FastadcDataSave()
отличается в первую очередь из-за
разнообразных dtype у параметров, ну и ещё чуток по мелочи.
FastadcDataLoad()
уже посложнее:
PzframeDataPutChanInt()
и PzframeDataPutChanDbl()
,
скопированные со своих Get-дуалов.
Тут надо корректно понимать смысл слова "Put": значение кладётся в текущее, но НИКУДА НЕ ОТСЫЛАЕТСЯ.
ZeroAllData()
, идущую по всем
каналам и параметрам делающая bzero() содержимого. Логика по определению
указателя на "содержимое" и объёма позаимствована из
pzframe_data.c::ProcessOneChanUpdate()
.
03.10.2016: проверяем сделанное:
param_lp_s
поменяны также прочие
_lp_s
(т.к. иначе сигнатуры от v2 и v4 "подхватывались"
друг-друговым кодом -- проверено).
Теперь они ВСЕ (включая сигнатуру "Data:") начинаются с "#=".
Анализ:
gui->curstate
) не трогает, и vmt.newstate()
не
вызывает.
PzframeGuiEventProc()
перед вызовом
PzframeGuiUpdate()
.
Решение: дык пусть это делает сама PzframeGuiUpdate()
!
ZeroAllData()
добавлено
pfr->rflags=0
.
PzframeGuiUpdate()
.
Ура -- всё пашет!!!
Раз на вид всё окей, считаем за "done", а дальше будет пилить по необходимости.
P.S. Для liu и подобных понадобится сделать
FastadcDataSave()
публичной.
Причина возникновения -- позавчера-вчера делал для Роговского модификацию v2'шного драйвера nadc502 (она названа wadc502), в которой:
А смысл этих мер -- максимально повысить частоту опроса единственного нужного сигнала, который считывается по 32765 точек.
Но:
...к сожалению, в обсуждаемом случае это сделать не представляется возможным, т.к.
31.05.2017: собственно:
Т.е.:
fastadc_mes_t
эти 3 параметра (более от уровня
fastadc_data вроде ничего не требуется).
(В файле с сохранёнными данными, конечно, не очень красиво (с точки зрения всяких excel/gnuplot), но уж ладно.)
02.06.2017: пообщался с ЕманоФедей на тему, как оно сделано у "взрослых" осциллографов (вроде Acquiris/Agilent/KeySight). Ответ: все сегменты измерения рисуются поверх друг друга (как в manyadcs на ЛИУ).
Некоторый вопрос в том, как реагировать на одновременную включенность этих режимов (а такое вообще технологически возможно?)?
На вид ничего сверхсложного. Пока не требуется, но если вдруг -- то есть понимание, что именно делать.
adc812me_chan_dscrs[]
он отсутствовал. В результате он просто
не работал и всегда отображался с ==0.
12.07.2017: фиксим: в
pzframe_gui.c::PzframeGuiMkparknob()
добавлена
проверка, что запрашиваемый канал реально регистрируется (его
.name
!=NULL), и если нет, то на консоль валится WARNING. Для
отлова таких ошибок хватит.
Заодно, кстати, добавлена проверка, что номер запрашиваемого канала не вылазит за размер таблицы используемых.
Тогда я смог только посокрушаться "ну нет пока поддержки вертикальных scrollbar'ов!...".
Например, если указан диапазон [-1,+4], то при даденном драйвером [-0.5,+0.5] будет как отдано, [-4,+4] превратится в [-1,+4], а [-8,+8] в [-1,+4].
Это избавит от необходимости писать PSP-плагин для парсинга (кроме собственно значения нужен ещё факт, что парсинг был).
Хотя,
...хотя -- а если оне не указаны?
28.08.2020: близкая тема -- на днях ЕманоФедя опять возмутился, что при использовании в ADC200 фичи "сдвиг нуля" отображение всё равно симметризует диапазоны (которые тут становятся уже принципиально НЕсимметричными); т.е., Федя пытается использовать площадь осциллограммы по максимуму, убирая ту часть, где сигнала никогда не бывает, а программа мешает ему это сделать.
Ну что ж -- очевидно, что надо делать это симметризование отключабельным. Процесс размышлений и реализации:
(Именно adc-type, т.е., fastadc_type_dscr_t
, а не
fastadc_gui_dscr_t
-- потому, что симметризацией занимается
уровень fastadc_data.)
Но флагов таких нет.
Да и неправильно бы так было: в одном устройстве могут, в принципе, быть каналы с разными требованиями к симметризовабельности -- речь в первую очередь об искусственных/композитных, вроде manyadcs.
pzframe_chan_dscr_t.chan_type
.
Но и это тоже неправильно, потому что...
fastadc_line_dscr_t
.
Вот последний вариант и выберем.
options
FASTADC_LINE_OPTION_NO_SYMM
.
FastadcSymmMinMax*()
окружены проверками, что флаг не взведён.
LINE_DSCR()
.
Умаялся искать причину -- и отладочных печатей понаделал, которые показали, что всё вроде правильно, и значения каналов диапазонов от драйвера проверил...
Оказалось, что дело в info2mes: оно всё-таки вызывается в самом конце
ProcessAdcInfo()
, и конкретно в adc200_info2mes()
имелась принудительная симметризация.
После закомментировывания проблема ушла и теперь вроде всё работает как надо -- НЕ симметризуется.
Потребность в АЦП клистронов на ВЭПП-5: там сигнал иногда гуляет, и желательно бы легче видеть отклонение "горбов" от реперных точек (устанавливаемых в нужные места).
09.12.2018: после некоторых размышлений и обмысливания разных вариантов (включая «не сдалать ли ключик "contrast_repers", в дополнение к ключу black») было принято решение максимально простое: в качестве цвета реперов использовать цвет осей.
Реализация свелась к модификации одной строки в
Xh_plot.c::XhCreatePlot()
.
17.12.2018: всё-таки переделано на "фиолетовые" -- точнее, magenta (#ff00ff).
XhSetColorBinding("GRAPH_REPERS", "#00FF00");
Вроде неплохо.
Кстати, как говорит Беркаев, цвет "magenta" используется в стеклянных кокпитах то ли Boeing, то ли Airbus в похожих целях -- для показа стрелки "куда лететь" (или "куда рулить"?); там он и взял идею о фиолетовом.
Я-то сказал, что такая фича есть, надо просто кнопки на морды окон вывести. Но есть ли?
07.12.2018: посмотрел -- неа, в v4 точно нету. Но, может, можно быстро скопировать из v2?
10.12.2018: стал смотреть детальнее. Неа, и в v2 нету :)
Результаты анализа текущей ситуации:
В нём это было реализовано 19-12-2007.
Но в той версии -- она является текущей в v2 -- сама реализация "svd" отсутствует, а есть лишь:
gui_draw()
.
DrawPlotView()
"знает" о возможности наличия предыдущих в
количестве num_saved
, а количество оное уставляется через
XhPlotSetNumSaved()
.
Видимо, так и не потребовалось.
И в gui_draw()
код, реагирующий на age
, просто
закомментирован.
11.12.2018: делаем.
Тут были некоторые сомнения на тему "где это реализовывать -- в fastadc или всё-таки в pzframe?".
Но минусом тут то, КАК PzframeDataRealize()
должен узнавать,
для каких каналов нужно заготавливать место?
Поэтому решено аллокирование поручить FastadcDataRealize()
.
Собственно реализация:
fastadc_data_t
добавлены:
fastadc_mes_t svd; int use_svd; void *svd_buf;
FastadcDataRealize()
, сразу после
вызова PzframeDataRealize()
.
Делается 2-проходным образом: на 0-м проходе просто считает размер, в
конце его аллокирует общий буфер, а на 1-м проходе прописывает все
svd.plots[nl].x_buf
.
PzframeDataRealize()
--
там было забыто сохранение указателя на аллокированный буфер в
pfr->buf
(хотя safe_free()
по ERREXIT делается).
FastadcDataCopy2Svd()
-- интеллектуальное копирование
всех mes.plots[]
в svd.plots[]
: поля копируются
поштучно, а содержимое буферов данных -- отдельно, ровно по нужному объёму;
причём только при взведённом on
, а в противном случае ставится
x_buf=NULL, x_buf_dtype=INT32, numpts=0 -- как в
ProcessAdcInfo()
.
FastadcDataResetSvd()
-- просто сброс use_svd=0.
FastadcGuiCopy2Svd()
и
FastadcGuiResetSvd()
скопированы с версии 2012 года без
изменений -- да-да, API XhPlot остался тем же.
gui_draw()
раскомментирован выбор plots
из mes
либо
svd
в зависимости от age
.
В реализации 2012 года проблема отсутствовала потому, что там не было разделения на уровни.
12.12.2018: продолжаем:
Если не устраивать радикального повышения модульности в виде указания содержимого тулбаров из уровней-юзеров pzframe (т.е., fastadc/wirebpm/vcamimg), то создаваться тулбары будут всё равно в pzframe.
И тогда просматриваются 2 варианта того, как устроить реакцию на нажатия кнопок (т.е., передачу информации о нажатии от pzframe своим юзерам):
pzframe_gui_vmt_t
метод
"управление сохранённостью" (которому передаётся 1 для сохранения и 0 для
сброса). А pzframe_main.c::CommandProc()
и
pzframe_gui.c::*BtnCB()
станут вызывать его.
Тогда придётся добавить ещё инфраструктуру обработки команд в {fastadc,wirebpm,vcamimg}_gui.
Первый вариант (a) выглядит проще и беспроблемнее -- всё делается в рамках инфраструктуры pzframe, и не требуется никак завязываться на специфику работы Xh/Motif/...
Делаем.
svd_ctl
.
fastadc_gui_std_pzframe_vmt
добавлен указатель на
SvdCtl()
(чей функционал тривиален).
(А в wirebpm'ную и vcamimg'ную добавлены NULL'ы.)
CommandProc()
добавлена реакция на них -- вызов метода
svd_ctl()
.
*BtnCB()
так же вызывают метод
svd_ctl()
.
В отличие от прочих 3, ссылки на виджеты НЕ сохраняются ни в каком
*_gui_t
-- просто надобности к ним потом обращаться нету (а к
loop и once -- есть).
Собственно, проверено -- РАБОТАЕТ!!! Прямо с первого раза, что удивительно и подозрительно.
Дальнейшие напрашивающиеся усовершенствования:
А NO_ALLOC?
Ибо сейчас их получилось многовато и эта четвёрка уже распирает cpanel; при том, что кнопка [Once] пультовым программам уж точно не нужна.
Тут некоторая двоякость: с одной стороны, надо бы уметь управлять этим на уровне экземпляров осциллографов, а с другой, у некоторых типов кнопки не нужны никогда (wirebpm). В голову приходит мысл сделать ДВА набора флагов: один указывается в atd или вообще рядом с vmt, а второй -- уже в options. И кнопки появляются только при взведённости ОБОИХ флагов.
13.12.2018: продолжаем.
PzframeDataRealize()
при нём взведённом просто делает
return
сразу после аллокирования буферов (а при взведённом
ещё и NO_ALLOC -- не делает вообще ничего).
KNOBSTATE_NONE
вместо обычного изначального
KNOBSTATE_JUSTCREATED
.
В v4 сводится к не-аллокированию svd_buf
'а.
PZFRAME_B_ARTIFICIAL | PZFRAME_B_NO_SVD | PZFRAME_B_NO_ALLOC
-- оно так должно будет быть во всех искусственных.
PZFRAME_B_NO_ALLOC
поддерживается ещё с
2016-го.
PZFRAME_B_NO_SVD
добавлена сейчас:
FastadcDataRealize()
выполняет аллокирование лишь при
НЕвзведённости, причём невзведён должен быть и NO_ALLOC.
PzframeGuiMkCommons()
теперь НЕ делает ту пару
кнопок при взведённом NO_SVD.
Но инфраструктура-то сделана.
15.12.2018: впрочем, удаление кнопок [SetGood][RstGood] при наличии NO_SVD всё же сделано.
Сделал по-простому: в мини-тулбаре кнопка [Once] НЕ делается при том же условии, когда делаются SetGood+RstGood.
Таким образом, он продолжает оставаться компактным -- текущая проблема решена.
15.12.2018: на пульт новый pzframeclient скопирован, он там будет потихоньку вводиться в строй по мере перезапуска программ.
Итак, титульная задача решена -- возможность указывать "хорошую" осциллограмму сделана, так что "done".
А для аспектов функционирования тулбаров создан свой "тикет" за сегодня, см. ниже.
15.12.2018: чуток допилено:
VcamimgDataFillStdDscr()
и
WirebpmDataFillStdDscr()
вставлено форсение
PZFRAME_B_NO_SVD
.
А то по-дурацки смотрелись утилиты ottcam и u0632, никакого "сохранения хорошего" не поддерживающие.
Смысл -- чтоб в v5cnv не запихивать полноценный adc333, от которого показывается только 4-я линия (канал3), а составить cpoint'ами "виртуальный АЦП", состоящий из line3,numpts,ptsofs и прочих "реально необходимых" каналов, и уже на него натравливать тот компонент в v5cnv.
10.12.2018: надо только будет название выбрать.
Например, "adc1"? Не, пхохо -- начало пересекается с adc1000.
Как конкретно это делать -- не вполне ясно, но:
FastadcGuiSetReper()
уже есть, так что программно
реперы управлябельны.
Некоторая неприятность лишь в том, что у реперов есть не только позиция, но и состояние вкл/выкл -- которое, впрочем, управляется указанием позиции <0.
Ну и понятны и ограничения этой фичи:
16.12.2018: а есть идея и пострашнее: "как бы это сделать так, чтобы «сохранённые хорошие» графики тоже маппировались бы на каналы и были б общими для всех экземпляров осциллографов (и переживали бы рестарт программы, и тоже могли бы сохраняться в режим)".
Сия жуткая мысль пришла в голову пока что только мне. Вопрос, когда она придёт в головы юзеров.
Как такое делать -- и думать не хочется (понятно ж, что можно -- просто возни много): на стороне сервера надо будет заводить для каждого осциллографа набор каналов, миррорящий его набор данных; каждый экземпляр fastadc-морды будет пытаться приконнектиться также к этим дополнительным каналам, ...
Пока моя позиция проста: нынешнее "сохранённое хорошее" -- это вопрос локального удобства; морды осциллографов -- это НЕ программы для анализа данных, если нужен такой функционал, то его следует реализовывать в специализированных программах.
15.12.2018: итак, список хотелок:
А в остальном его появление управляется флагом "noleds", передаваемым от хозяина (main'а или knobplugin'а). И это как-то совсем неправильно.
...вроде были какие-то проблемы с этим, судя по закомментированности
средней строки в pzframe_main.c::ShowRunMode()
. Надо
порыться -- где-то это должно быть записано.
Хотя XhSetCommandEnabled()
делается.
А, нашёл: ВСЁ содержимое XhSetCommandEnabled()
вообще за-#if0'ено.
Что ж там было-то такое...
22.02.2020: да, функционал нажатия/отжатия и дисэйбленья на основном тулбаре в v4 с самого начала просто не работал (точнее, не был сделан). Сегодня реализован и работает.
...хотя вот это уже больше похоже на блажь
Это всё задачи не особо срочные (кроме разве что пункта 1.c), так что делаться будут расслабленно, в фоновом режиме, по мере осознания наилужших решений.
17.12.2018: быстрое и халтурное решение проблемы 1.c: создание этих кнопок вытащено из условия "по noleds", и они теперь создаются ВСЕГДА (если только не взведён NO_SVD).
Да, халтура, но зато в программе v5rfsyn в окошках осциллографов эти кнопки есть.
13.02.2020@дома: почему у
свежезапущенных pzframe-скринов кнопка "Play" не выглядит нажатой? И это
при том, что в ShowRunMode()
вроде бы выполняются надлежащие
действия.
14.02.2020: а -- нашёл! Дело в том, что в
pzframe_gui'шном ShowRunMode()
это делается с MINI-toolbar'ом.
А за основной отвечает pzframe_main'ный, где хоть и есть нужное действие, но
оно почему-то тоже эффекта не возымевает.
Чуть позже: ага, ясно, почему: там он вызывается из
EventProc()
, коий, в свою очередь, нигде НЕ регистрируется.
...но зато он вызывается из SetRunMode()
, вызываемой
непосредственно перед XhShowWindow()
. Так что загадка.
15.02.2020@дома, суббота:
всё прозаично: в pzframe-то выполняются все действия, но не работает сама
XhSetCommandOnOff()
-- см. замечание от 23-09-2016.
Идеологическая причина при переходе на v4; надо фиксить там...
22.02.2020: пофиксено.
17.02.2020@пультовая, ~16:00 (в ожидании Павленко): а почему это есть кнопки SetGood/RstGood на mini-toolbar'е при наличествующем основном тулбаре? 22.02.2020: а, ну ясно -- см. тут выше за 17-12-2018.
А ещё бы недурно сделать, чтобы кнопка "SetGood" становилась бы нажатой в состоянии, когда есть запомненные "хорошие" данные.
22.02.2020@дома-суббота: насчёт "почему mini-toolbar'е при наличествующем основном тулбаре" -- нашёл, что оно так и задумывалось "всегда делаем эти кнопки в мини-тулбаре, потому что невозможно определить наёжно, есть ли они на основном тулбаре или нет".
23.02.2020@дома-воскресенье: напрашивается решение: не пытаться умничать, "догадываясь" о наличии/отсутствии посредством анализа noleds, а просто ввести явный флажок, который и указывать в options -- вот в скринах его и проставлять при надобности.
25.02.2020: насчёт как бы сделать, «чтобы при наличии "хорошего" кнопка [SetGood] становилась бы нажатой, а при сбросе -- отжималась бы»:
ShowSvdMode()
, которая бы ставила кнопку SetGood в состояние
pressed=svd_present, а RstGood в состояние enabled=!svd_present.
И да, оная функциональность должна быть на обоих уровнях -- и
pzframe_gui, и pzframe_main; совершенно аналогично
ShowRunMode()
.
Кстати, надо бы под-изучить, как работает последняя.
use_svd
) живёт уже на
уровне fastadc.
pzframe_gui_svd_ctl_t
-- svd_state()
?
26.02.2020@пультовая: делаем.
svd_state
(добавленого
типа pzframe_gui_svd_state_t
).
SvdState()
(в
остальных двух -- vcamimg_gui.c и wirebpm_gui.c --
прописаны NULL'ы).
ShorSvdMode()
, ...
PzframeMain()
сразу после SetRunMode() -- это начальная
инициализация.
PZFRAME_MAIN_CMD_SET_GOOD
переделана с
XhXXX_TOOLCMD()
на XhXXX_TOOLCHK()
-- в противном
случае OnOff не работает.
Проверяем -- нифига не работает: постоянно считается svd_state=0.
use_svd
только СБРАСЫВАЕТСЯ в =0, а
=1 нигде не было.
XhPlotSetNumSaved(,1)
, которое в
FastadcGuiCopy2Svd()
делалось.
use_svd = 1
в конец
FastadcDataCopy2Svd()
.
После этого заработало как проектировалось.
26.02.2020@дома: добавляем аналогичный функционал на уровень _gui -- во встроенный мини-тулбар:
pzframe_gui_t
добавлены Widget-поля
setg_button
и rstg_button
.
PzframeGuiMkCommons()
виджеты создаются/настраиваются
теперь в эти поля, а не во временную b
.
ShowSvdMode()
(копия
pzframe_main'овской, адаптированная под мини-тулбар).
SetGoodBtnCB()
и
RstGoodBtnCB()
.
PzframeGuiMkCommons()
сразу после создания кнопок.
PzframeGuiRealize()
--
но "инициализации" не происходило.
FastadcGuiRealize()
, и ещё ДО PopulateCpanel()
и,
соответственно, mkctls()
, которая и вызывает, по коду
FASTADC_GUI_CTL_COMMONS
, создание мини-туобара.
loop_button
и once_button
) переехал и "начальный"
вызов ShowRunMode()
-- он раньше просто не работал, а спасал
ситуацию вызов в PzframeGuiEventProc()
'е.
Да, теперь работает.
Но -- НЕ работает "параллельность": чтобы нажимаешь кнопку на основном тулбаре, и это же состояние отражается на мини-тулбаре.
Сначала была идея "так можно же сделать соответствующий вызов в pzframe_gui, чтобы его дёргал pzframe_main для синхронизации!" (а при желании -- как-нибудь организовать и обратное сообщение).
Но потом пришло осознание, что это нафиг не нужно, а надо поработать над
НЕсозданием мини-тулбара при наличии основного (и, наоборот, чтобы он
принудительно создавался при отсутствии основного). Т.е., нужен какой-то
ещё параметр, помимо noleds
.
29.02.2020@дома-суббота: делаем "исключительность" -- чтобы при наличии основного тулбара с кнопками для осциллографа мини-тулбар бы НЕ создавался.
embed_tbar
.
PzframeGuiMkCommons()
теперь РАЗНЫЕ условия: панель
leds упарвляется флагом noleds==0
, а "куски мини-тулбара" (он
фактически состоит из 4 кнопок) управляются флагом embed_tbar
.
Именно это и определяет их НЕсоздание при наличии основного тулбара.
PzframeKnobpluginDoCreate()
делается принудительное
embed_tbar = 1
.
PzframeMain()
--
embed_tbar = opts.notoolbar
.
Ура!
noleds
переименовано в
embed_leds
.
PzframeGuiMkCommons()
условие инвертировано (точнее,
убрано "== 0
").
PzframeKnobpluginDoCreate()
и
0PzframeMain()
-- присвоения также инвертированы; стало даже
красивше.
Ну что, на этом (наконец-то!) можно считать "апгрейд красивости и корректности" клиентской инфраструктуры pzframe завершённым?
03.03.2020: а, да -- "done".
(Непосредственной причиной послужило желание заиметь хорошо-видимый график с "толстыми" линиями, чтоб вставить его как иллюстрацию в презентацию.)
09.10.2019: действия:
options
-флаг
XH_PLOT_WIDE_MASK
=1<<18.
Да, в этом изрядное отличие от "black", который передаётся отдельным
int-параметром; подумать бы заранее -- и его бы через options
передавать.
AllocThGC()
аналогична обычной
AllocXhGC()
, но ставит vals.line_width=2 вместо =0 (функция
совпадает с MotifKnobs_histplot.c'шной).
: ?
" -- в зависимости от взведённости бита
XH_PLOT_WIDE_MASK
.
Конструкция там заумноватая -- список параметров в одном экземпляре, а подменяется только имя функции. Это возможно вследствие идентичности набора параметров.
И да, в старый cx/ (v2) оно скопировано (для меня снова оказалось сюрпризом, что Xh_plot в них совпадает!).
wide
.
fastadc_gui_text2look[]
для него аж 4 строчки: "thin"
-- для обычного варианта, а "thick", "wide" и "bold" -- для широкого.
options |= XH_PLOT_NO_SCROLLBAR_MASK
.
Тут надо отметить, что этот режим переключается только изначально, в отличие от histplot'а, где оно работает динамически, по селектору "Mode".
(Это по результатам двухчасового "гипнотизирования" 8 штук ADC250, которые то работали, то нет, и приходилось вглядываться, крутится ли оно там и/или разглядывать время с момента последнего запуска.)
Обоснование, кстати, простое: когда оно стоит, то разглядывать конкретное положение крутилки незачем -- оно смысла не имеет, факт "замёрзшести" важнее. Число же FPS и секунд с последнего запуска посиневать нельзя -- последнее имеет смысл и спустя час после замерзания.
Есть, правда, сомнения -- а можно ли крутилку расцвечивать отдельно? Она для этого должна быть отдельным виджетом.
29.01.2020: посмотрел -- да, крутилка сделана
отдельным виджетом, pzframe_gui_t.roller
. ...как, кстати, и
отображалка FPS -- fps_show
! Можно и её посинять.
Очевидно, нужно будет добавить туда же:
А вот время последнего обновления УЖЕ есть в
pzframe_data_t.timeupd
.
Расцветка:
PzframeGuiEventProc()
,
событие PZFRAME_REASON_DATA
.
ShowStats()
.
Тем более, что там УЖЕ вычисляется время с последнего обновления (для отображения в time_show) -- вот его и использовать лоя сверки с fresh_age.
Краткое расследование показало -- нет, нигде не хранится, а владеет этой информацией непосредственно сама cda_core внутри себя. Так что надо будет заводить поле для хранения, плюс на какой-то канал навешивать evproc по маске FRESHCHG.
Замечание: при посинении/отсинении надо учитывать состояние "running". И
в ShowStats()
как раз вычисление времени с последнего прихода
данных УЖЕ делается внутри соответствующего условия. ...только одна засада:
оно !running там запараллелено с timeupd.tv_sec==0...
11.02.2020: (начало обсуждения -- в разделе по adc250_drv за сегодня) а ещё надо бы СРАЗУ отображать статус "неживых" устройств -- не только NOTFOUND (они сейчас видны по почерневшим ручкам), но и OFFLINE.
Сейчас же они показываются голубеньким JUSTCREATED и фиг поймёшь, в чём причина.
А надо б -- чтобы такие состояния показывались бы прямо на "морде".
Вопрос лишь в том, КАК узнавать "неживость": на канал _devstate глядеть? Так-то он практически всегда в _chan_dscrs[] есть -- идёт последним; но rflag'ами он НЕ снабжается.
Посматривать на флаги какого-то другого канала -- например, marker?
12.02.2020: делаем.
Сначала -- отображение посинения:
roller_curstate
, она инициализируется в
KNOBSTATE_NONE
.
roller_deffg
и roller_deffg
, они
заполняются при создании виджета.
SetRollerState()
.
PzframeGuiEventProc()
и
ShowStats()
.
roller_states[]
переименовано в roller_chars[]
.
Затем дОбыча fresh_age'я:
pzframe_data_t
добавлено поле
fresh_age_timeval
.
Оно struct timeval
-- т.к. в ShowStats()
вычисления ведутся в этих единицах -- и оттого имеет такой суффикс.
cx_time
-- делается в
PzframeDataEvproc()
, который теперь и FRESHCHG ловит.
12.02.2020: далее переходим к колоризации основной панели. Некоторые мысли по теме (часть вечером ~19:00 опять @8-ка, поворот со Строителей на Бердское):
13.02.2020@дома: приступаем. Вся работа пока идёт в pzframe_gui.c.
SetMainState()
.
Были некие смонения, как назвать -- SetЧЕГОИМЕННОState(), поскольку эта сущность "основная отображалка" никакого штатного названия в рамках pzframe_gui не имеет. Вот и выбрано "Main".
PzframeGuiEventProc()
в дополнение к окучиванию всех
индивидуальных каналов добавлен также вызов SetMainState(gui,
KNOBSTATE_NOTFOUND)
-- тут всё максимально просто.
Здесь проигнорировано вчерашнее соображение "...выполнять НЕ всегда", поскольку ситуация с почерневанием -- довольно неординарная и тут будет к место сигнализировать о ней сразу.
Хотя при надобности добавить защитное условие "только если curstate==KNOBSTATE_JUSTCREATED" проблемой не будет.
SetMainState(gui,
KNOBSTATE_JUSTCREATED)
.
Это некоторый компромисс -- оно не факт, что ВСЕГДА будет именно свежесозданным. Но тут важно хоть как-то отобразить тот факт, что устройство "ожило".
Начальная проверка показывает, что оно чернеет.
А вот первоначальная задача -- показывать нерабочесть устройств -- так и не решена.
Чуть позже @вечер, ванна: размышления по теме:
Но ведь скрины-то в аналогичной ситуации показывают каналы в нужной расцветке! Что б так же и не сделать?
CDA_CTX_R_CYCLE
вызывается процессинг всей группировки и
вычитываются ТЕКУЩИЕ rflags, а уж они-то у cda всегда актуальные (при
коннекте считываются, а при помирании устройства доставляются от сервера по
событию STATCHG (через cxsd_fe_cx до клиента доходящему как CURVAL).
И даже при охреневании устройства -- пусть график остаётся как есть, а проблема будет видна по расцвечиванию ручек каналов-обвязки.
14.02.2020: а вот и фиг -- НЕ видна. Даже при явном делании устройству _devstate=-1 -- скрин просто замирает, но обвязка-то тоже не обновляется. ...и при запуске скрина на УЖЕ нерабочее устройство -- аналогично, всё светло-бирюзовенькое.
PZFRAME_CHAN__DEVSTATE_MASK
.
14.02.2020: проверил вчерашние изменения в полноте (включая отбирание существовавшего устройства "на лету" при рестарте сервера и затем такое же возвращение) -- работает.
15.02.2020@дома-суббота: делаем слежение за каналом _devstate, чтобы отлавливать нерабочие устройства.
PZFRAME_CHAN__DEVSTATE_MASK
-- этот флажок надлежит
указывать в поле chan_type
для таковых каналов.
PZFRAME_REASON_DEVSTATE
-- код события "получено новое
значение канала _devstate".
PZFRAME_CHAN__DEVSTATE_MASK
регистрируются (с единственным битом UPDATE в evmask'е) на другой обработчик
событий -- ...
PzframeDevSEvproc()
: получает текущее значение
канала и отдаёт его выше в качестве info_int
к событию
DEVSTATE.
Некоторые комментарии:
Те же, что ранее уже получали данные -- так и продолжат эти данные показывать. А "поплохелость", по идее, должна бы как-нибудь стать видна через прочие, "обвязочные" ручки каналов. Хотя покамест -- нет, оно не так, поскольку...
Проверено -- работает как задумано.
03.03.2020: ставим "done".
12.03.2021: туда же были добавлены ещё 3 канала касательно PLL, и тоже IMMEDIATE|ON_CYCLE.
Пытался понять логику, почему/как происходит то, что происходит -- не особо успешно. Странно оно как-то...
13.03.2021: но напрашивается мысль, что причина -- в не-ловле RSLVSTAT каналами, как параметрами (у тех-то ладно), так и IMMEDIATE-ручками.
Я экспериментировал с наличием/отсутствием IMMEDIATE|ON_CYCLE и даже добавлял IS_PARAM (он, как видно по коду, вообще о другом) -- один чёрт, поведение одинаковое.
...к сожалению, всё это на симуляторе -- "cxsd -S" (и "-s" тоже), т.к. реальное железо в клистронке стоит без запусков.
14.03.2021: ещё результаты:
Надо бы поиграться ещё -- ведь режим суперсимуляции позволяет имитировать приход "маркера".
Причём таковы как не-IMMEDIATE, так и отсутствующие каналы.
15.03.2021@утро: т.е.,
суть проблемы битым текстом: в момент обновления ручек -- как общего по
"маркеру", так и группового (а вот с чего оно, для IMMEDIATE|ON_CYCLE?) --
уже должен иметься CXCF_FLAG_NOTFOUND
(и, видимо, статус
NOTFOUND), но почернения не происходит.
15.03.2021: пытаемся разбираться дальше: вылезло ещё какое-то посинение...
17.03.2021: с посинением разобрался: оно было из-за тормозов отображения X-окна по сети с v5p3 (к тому же изрядно нагруженного!) на домашний комп через duct, x10sae, star.
22.03.2021: очевидно, что при таком постепенном отставании (из-за неуспевания отображения) в буфере отправки сервера должны потихоньку накапливаться данные и в конце концов он должен переполняться. Так вот -- да, переполняется: судя по 4access.log'e примерно раз в 2 часа происходит "send-buffer overflow, disconnecting ...", а ещё через примерно 2 минуты клиент реконнектится (видимо, эти 2 минуты уходят на вычерпывание локального буфера).
Также было извлечено 2 важных урока:
stroftime*()
/strcurtime*()
более 1 раза: оно
возвращает указатель на статический buf[], так что последующие вызовы
перепрописывают результаты предыдущих -- в итоге везде печатается результат
самого "первого", наилевейшего вызова (потому, что при cdecl-вызовах -- а
*printf()
таковы -- параметры кладутся в стек справа налево).
cx_time_t
) и %06ld для микросекунд
(struct timeval
).
В противном случае при переводе в "вещественные" для ручных разбирательств -- СЕКУНДЫ.ДОЛИ -- дробная часть получится в десятки/сотни/... раз больше реальной, если в начале были отброшенные нули.
Более того: и печатать дуплеты лучше не через запятую ',', а сразу через точку ., чтобы иметь готовые как бы вещественные числа.
В закомментированной отладочной печати в
cda_dat_p_update_dataset()
теперь сделано и то, и другое.
Плюс, при отладочной выдаче обнаружилась дурость:
PZFRAME_REASON_PARAM
, и со всей толпой в цикле в
PzframeGuiUpdate()
.
OK -- добавлено исключение при групповом обновлении, с условием прямо обратным условию для индивидуального обновления.
Проверил -- вроде всё работает, деградации не видно.
05.04.2021: возвращаясь к титульной проблеме: проведён ещё один тест, при котором вроде бы нарыт "минимальный test-case", с которым и нужно разбираться. Сценарий его таков:
Всё отображается светлоголубым.
И вот тут в клиенте все каналы колоризуются в NORMAL, включая и проблемный.
Но эта-то реакция обеспечивается отдельно
PzframeDataEvproc()
'ем: там по RSLVSTAT_NOTFOUND маркера
принудительно проставляется CXCF_FLAG_NOTFOUND
всем-всем
каналам и генерится событие PZFRAME_REASON_RSLVSTAT
, и уже ОНО
принудительно всем-всем ручкам ставит KNOBSTATE_NOTFOUND
,
независимо от rflag'ов.
Т.е.,
@вечер: ответы -- "нет, Не используются" и "да, ДО реального получения флагов".
05.04.2021@вечер: налаживаем диагностику:
[ADC250_CHAN_CUR_PLL2_CTRL]
(его номер 99) специально
"испорчен" -- ему сделано заведомо нерезолвящееся имя.
UpdateParamKnob()
вставлена отладочная печать: при
cn=99 выдаётся значение rflags и получаемое на их основе knobstate.
raise(11)
, чтобы натравить
на это gdb и с помощью bt посмотреть, откуда ж оно такое вызвано.
Результат оказался немного постыдным:
UpdateParamKnob()
вызывается из
PzframeGuiUpdate()
, в общем цикле со всеми остальными ручками
-- т.е., по приходу маркера.
Оно показало, что значение .chan_type ==0!
adc250_chan_dscrs[]
:
стояло
[ADC250_CHAN_CUR_PLL1_CTRL] = {"cur_pll1_ctrl", PZFRAME_CHAN_IMMEDIATE_MASK | PZFRAME_CHAN_ON_CYCLE_MASK},
вместо надлежащего
[ADC250_CHAN_CUR_PLL1_CTRL] = {"cur_pll1_ctrl", 0, PZFRAME_CHAN_IMMEDIATE_MASK | PZFRAME_CHAN_ON_CYCLE_MASK},
-- т.е., флаги попадали в .change_important
(не имея никакого
особого эффекта), а не в .chan_type
; потому канал и не
считался за IMMEDIATE.
Т.е., конкретно ЭТА -- "отлаживаемая" -- проблема вызвана моим же косяком. Но титульная проблема от этого никуда не девалась -- ненайденные ручки НЕ ЧЕРНЕЮТ. Причём ни обычные, ни IMMEDIATE-варианты.
Но теперь поведение хотя бы стало логичным, так что можно спокойно разбираться дальше.
05.04.2021@ложась-баиньки: да, теперь всё выглядит логичным:
Выводы (нумерация унифицирована с предыдущим списком, а порядок -- с порядком действий):
...хотя с этими вопрос дискуссионный: может, всё же по "маркеру"?
06.04.2021: итак, "по обратному порядку":
ProcessOneChanUpdate()
, вызываемом лишь по
приходу маркера (для не-IMMEDIATE-каналов) и по приходу индивидуальных
данных для IMMEDIATE-каналов (которого для ненайденных никогда не
произойдёт).
PzframeChanEvproc()
добавлен анализ кода в
reason
; и конкретно по RSLVSTAT_NOTFOUND делается добыча у cda
текущих флагов и "пинание" обновления ручки генерацией
PZFRAME_REASON_PARAM
.
PzframeRDsCEvproc()
всё ровно то же
самое.
Включая "пинание" обновления -- без него почерневало не сразу, а лишь по следующему приходу маркера.
...переименовать ба её ещё -- а это "RDsC" теперь некорректно отражает её суть...
И при регистрации обоих долавлено CDA_REF_EVMASK_RSLVSTAT
.
Также было проведено исследование поведения при обращении к "без-серверным" устройствам -- через unknown:0.adc250_9a:
CDA_RSLVSTAT_SEARCHING
, т.е.,
!= CDA_RSLVSTAT_FOUND
,
из-за чего вроде бы должно сразу почернеть. А не чернеет!
После часа возни и смотрения "как баран на новые ворота" наконец дошло: в
этом случае событие RSLVSTAT прилетает сразу при старте -- т.е., во время
нахождения в *DataRealize()
и ещё ДО "оживления" GUI,
выполняемого в *GuiRealize()
позже. Поэтому, хотя событие и
генерится, но ловить его некому (и даже чернеть ещё физически нечему).
@после-обеда: радобрался:
-S
".
Маркер приходил сразу же после коннекта -- в режиме "-S" это канал записи, на который UPDATE/NEWVAL присылается сразу.
Но нет: "маркер"-то в таблице каналов стоит раньше, поэтому он и регистрируется раньше и значение на него приходит раньше.
А если вручную из другого терминала сделать cdaclient'ом marker=0, то всё обновляется уже в нормальное состояние -- именно потому, что к тому моменту каналы ручек также успели получить обновления.
Мда, вот такие неочевидные следствия работы с индивидуальными каналами с "маркером" в качестве сигнала готовности, вместо полноценной передачи всей информации о кадре в едином канале структурного типа.
Т.е., я ПОЛДНЯ убил на поиск "ошибки", в реальности ошибкой не являющейся, а обусловленной отличиями работы симулятора от реального железа. Прямо "вскрытие показало, что пациент умер от вскрытия"...
Итак -- титульная проблема вроде решена. Хоте решение этой вроде бы на вид несложной и рутинной неприятности оказалось очень длинным и трудоёмким. Но -- вот сюрприз! -- технически способ решения выглядит совершенно простым и очевидным.
07.04.2021: PzframeRDsCEvproc()
переименована в более соответствующее PzframePrpCEvproc()
;
"Prp" -- "Properties".
Засим задачу можно считать решённой. Отладочные печати убраны, порча имени канала тоже. Разделу ставим "done".
07.04.2021: кстати, а теперь, когда есть индивидуальная ловля RSLVSTAT с надлежащей реакцией у обычных каналов, зачем оставлять групповое почернение/о-JUSTCREATED'нье? Может, убрать его (оно делается в pzframe_data и pzframe_gui рядом с ключевым словом PZFRAME_REASON_RSLVSTAT)?
PZFRAME_CHAN_IS_FRAME
, и PZFRAME_CHAN_MARKER_MASK
,
калибровки {R,D} на такие каналы НЕ приходили.
09.02.2022: обнаружилось при расследовании замеченной 07-02-2022 странности у скрина adc1000: в нём почему-то показывались явно неправильные значения как на реперах, так и на осях.
Вот и стал разбираться, в т.ч. напихал чуток отладочной печати касательно диапазонов, а потом уже осенило "а получаем ли мы {R,D}?!".
Механизм проявления проблемы:
PzframeDataEvproc()
, а не в PzframePrpCEvproc()
.
CDA_REF_R_RDSCHG
, то и событие
PZFRAME_REASON_RDSCHG
не генерилось.
DoEvproc()
не
дёргалось, и не взводило флажок lines_rds_rcvd[]
, ...
FastadcDataPvl2Dsp()
вместо "правильных"
{R,D} (посредством вызова cda_rd_convert()
) использовалось
просто деление на значение default_r
.
default_r
конкретно в adc1000_data.c было
"обычайное" 1000000.0
вместо надлежащего 4096.0
.)
Исправление оказалось несложным, но неприятным:
PzframeDataEvproc()
также добавлена реакция на
CDA_REF_R_RDSCHG
-- просто скопирована из
PzframePrpCEvproc()
(плюс CDA_REF_EVMASK_RDSCHG
добавлено в маску интересующих событий).
privptr2
передавался сразу pfr
.
А сейчас понадобилось и номер канала всё же получать и использовать.
pfr->dpls + cn
Исключением ныне остался лишь PzframeDevSEvproc()
, но уж
_devstate-то точно можно считать уникальным (даже если их будет несколько (у
"композитных" устройств), то там важен лишь сам факт, а не номер
конкретного).
Проверил -- вроде проблема исчезла, теперь значения отображаются верно.
09.02.2022: оффтоп, уже касательно собственно поддержки ADC1000/ADC250: надо ещё у adc1000_data.c и adc250_data.c поставить 4096 вместо 1000000; плюс в драйверах всё же ИСПОЛЬЗОВАТЬ текущие диапазоны для отдачи текущих MIN/MAX -- чтобы на меньших диапазонах не тратить экранную площадь на пустоту.
10.02.2022: продолжение оффтопа: 1) 4096 сделал. 2) отдачу MIN/MAX вроде тоже сделал.
Но вылезли ещё косяки:
-- разобрался, накосячил сегодня с ADC4X250_PGA_RANGE_bits
.
-- похоже, всё там обновляется (вот сейчас вечером проверил -- да!), а просто подтормаживает из-за того, что скрины запущены по сети на p320t, и чем дольше скрин провисел, тем дольше задержка (буфера отправки потихоньку растут (но всё же ме-е-едленно, до "send buffer overflow" дело не доходит), задержка тоже растёт).
cda_current_nelems_of_ref()
от канала данных.
Смысл: чтобы для "искусственных" или "неродных для CX" осциллографов упростить жизнь -- чтоб можно было обходиться одним-единственным каналом "осциллограмма", не требуя наличия канала CUR_NUMPTS.
09.04.2022: сделал -- надо-то было добавить несколько
альтернатив к if()'у в ProcessAdcInfo()
.
Проверить бы ещё...
10.04.2022: проверил, с помощью
onei32l_data.c, меняя значения параметра
common_cur_numpts_cn
в егойном
FastadcDataFillStdDscr()
.
FASTADC_DATA_CN_MISSING
, означающее "брать
текущее значение nelems" -- ТЕПЕРЬ тоже работает.
Там добыча dataref'а канала данных линии --
а вчера накосячил и сделалpfr->refs[atd->line_dscrs[nl].data_cn]
-- т.е., брался внутренний индекс канала, а не его dataref, так что cda возвращала -1.atd->line_dscrs[nl].data_cn
-1
оно
тоже справляется (превращая в 0) -- когда ошибка возвращалась.
Засим "done".
(Основная работа описана в "О базовых и настроечных каналах", но пора уже и свой завести.)
06.06.2016: первые мысли по теме:
07.06.2016@утро-по-пути-на-работу-перед-НИПСом: можно реализовать всю логику прямо в драйвере, запоминая факт "запрещенности" и ничего не возвращая и даже не запрашивая данные у камеры, а по снятию запрета запросить добычу нового кадра заново.
Для упрощения же взаимодействия с "вышестоящими" можно
использовать имеющийся механизм, сделанный для калибровок cPCI'ных ADC --
pzframe_drv_drdy_p(,do_return=0)
.
15.04.2021: а ведь эта задача явно родственна
"operation mode", позже трансформировавшейся в "run model". Т.е.,
PZFRAME_RUN_MODE_DISABLED = 3
.
(Вот просто случайно наткнулся глазами на этот пункт, а тут такое очевидное решение...)
15.10.2021: поскольку задача решена посредством
PZFRAME_RUN_MODE_DISABLED
, то "done".
PerformTimeoutActions()
и,
в т.ч., метод abort_measurements()
, который у всех реализуется
функциями AbortMeasurements()
.
Суть проблемы в том, что по [Stop]'у данные возвращаются не сплошь нулевые, а ПРЕДЫДУЩИЕ.
18.06.2016: анализ:
ReturnMeasurements()
-- вызывается метод
read_measurements()
. Который у fastadc-драйверов радостно
производит вычитывание из железки (где и лежат предыдущие значения),
перепрописывая при этом нули.
(Кстати, очевидно, статистика (CALC_STATS) -- которая также нулится в Abort'е -- тоже, наверное, должна считаться по этим предыдущим.)
...хотя не факт, что в этот момент: предшествующий той дате snezh-2014-05-25.tar.gz содержит всё то же самое, а изменения тогда были сделаны в cpci/-драйверах.
...или такая архитектура вообще с рождения pzframe_drv.c, а "правильно работающий" STOP надо искать в пред-pzframe'ных вариантах драйверов?
Да -- похоже, при переходе на pzframe_drv почему-то впихнуто это принудительное вычитывание. Надо бы еще подробнее поразбираться на тему "когда и почему", а пока...
ReturnData()
булевский параметр "do_read", коий при вызове из
PerformTimeoutActions()
указывать =0, и посмотреть на возможные
последствия.
21.06.2016: да, так и поступаем. Параметр
do_read()
добавлен -- вторым, после pdr.
Отдельно: в ottcam_drv.c задача нуления непришедших строк
исполнялась как раз в ReadMeasurements()
. Перенесено в более
корректное место -- самое начало PrepareRetbufs()
.
22.06.2016: проверено на adc200me -- помогло.
27.11.2017: так вот: у Сенченко юзкейс сильно отличается.
И тогда же была какая-то мысль, что при надобности и у нас можно б было с vdev реализовать аналогичную модель работы -- не по запросу измерения канала (осциллограммы), а именно по команде.
Парой минут позже:
pzframe_drv_req_mes()
не по запросу измерения канала, а именно
по той команде "можно мерять!".
А для реализации понадобилось бы дополнительно 2 канала:
Короче -- ничего сложного, и даже вообще тривиально. Но как-то пока не требуется.
15.10.2021: поскольку задача решена посредством
PZFRAME_RUN_MODE_ON_RUN
, то "done".
Может, что-нибудь придумать?
21.10.2019@~14:20, дорога к родителям мимо Коптюга 7 и 11: как вариант -- ввести стандартный для pzframe_drv параметр, управляемый каналом вроде "AUTOSTART" (не возникло бы путаницы с ISTART...).
21.10.2019@вечер, ИЯФ: кстати,
MonEvproc()
, который для
ON_UPDATE-каналов делает следующий запрос СРАЗУ же при событии
_CHAN_R_UPDATE.
И типа пояснения: параметр-то является каналом, но вся реальная отработка -- внутри pzframe_drv.c, в точности, как у ISTART.
27.02.2021: вчера возникло подозрение, что такая "неуспевающесть" вследствие задержек сети может оказаться причиной пропуска запусков и навечного зависания у еманофединой софтины-менеджера/дирижёра крейта rfmeas:
Смысл -- чтобы разрешать прохождение следующего запуска только ПОСЛЕ того, как все ADC250 вычитались. (Конечно, так нужно дожидаться маркеров от ВСЕХ осциллографов, но пока, для тренировки, ловится только один.)
После этого он переходит в режим ничегонеделания -- следующего запроса на
измерение пока не было, поэтому pzframe_drv_req_mes()
ещё никем
не вызван.
CHNEvproc()
тут же запрашивает чтение этого же
канала снова.
Вуаля -- запуск осциллографом пропущен!!!
(Этой гипотезе противоречит то, что, как будто бы, именно реально не отрабатывается пуск -- по нему должно вернуться значение канала GATESTAT, а этого не происходит; как это вообще возможно -- загадка...)
Размышления и анализ кода в cxsd_fe_cx.c наводят на мысль, что проблема вроде бы не в этом, но мало ли; а готовый проект решения, если что -- уже вот он тут есть.
После того, как это всё записал, осенило --
ЭТОЙ гипотезе противоречит то, что в devlist-cxhw-15.lst l_timer
расположен после всех adc250, так что как раз наоборот он должен бы получать
данные от сервера в последнюю очередь, уже после того, как будут обработаны
все предшествующие сокеты. Но тут надо смотреть внимательнее -- зависит от
соотношения конкретных номеров дескрипторов сокетов и /dev/vmei*, а
"ls -l /proc/68/fd
"
на контроллере показал, что на /dev/vmei5 смотрит аж 14-й из 16; и
то, какие номера будут -- зависит от тайминга прихода запросов (в какой
момент относительно первоначальных connect()'ов успеет придти
REMDRV_C_CONFIG
для первого IRQ-юзера.
27.03.2021: проверено тестированием -- да, проблема оказалась именно в этом: после добавления сразу после _drdy() вызова_req_mes() проблема исчезла -- теперь запуски не теряются.
Вот так вот, сколь невероятным это ни казалось, но вот. Конкретную цепочку событий ещё надо восстановить -- порасставлю отладочных печатей в ключевых точках.
29.03.2021: да, вчера порасставил, сегодня прогнал и получил простыню событий.
Краткий вывод -- да, события происходят "вперемешку": на дальние от контроллера осциллографы ещё запрос не успел дойти, а уже исполнился запуск.
15.10.2021: поскольку задача решена посредством
PZFRAME_RUN_MODE_AUTO_RE_RUN
, то "done".
28.10.2021: ...хотя ни бинарники на bivme2-rfmeas, ни "run_mode=auto_re_run" в devlist-cxhw-15.lst ещё не задеплоены, так что формально "недопроверено".
Посему -- бинарник v4bivme2vmeserver на пульте в 4pult/lib/server/drivers/bivme2_drvlets/ проапдейчен, подождём перезагрузки bivme2-rfmeas, и тогда сменим и конфиг.
31.10.2021: и "run_mode=auto_re_run" в devlist-cxhw-15.lst добавлено, и он скопирован на пульт (это потому, что в эти выходные в 13-м здании отключение электричества на профилактику, так что bivme2-rfmeas выключен).
Остаётся ещё только cxhw:15 рестартовать.
pzframe_drv_req_mes()
, который бы мог принимать одно из 3
значений:
Смысл варианта 1 -- эмуляция работы как у Сенченко на ЛИУ-20. В этом
случае драйверов StartMeasurements()
будет вызываться только по
команде.
Смысл варианта 2 -- максимально быстрая готовность к следующему измерению, чтоб не ждало, пока измерения отдадутся наверх и оттуда придёт запрос на следующее измерение.
03.02.2020: немножко анализа: для реализации оного понадобится 2 вещи:
pzframe_drv_req_mes()
.
У большинства драйверов свободны каналы 18 и 19.
27.03.2021: да, придётся. Конкретно на bivme2_rfmeas проявилась проблема: до драйвера ADC250 слишком долго идёт команда на организацию следующего измерения, в результате чего устройство умудряется пропустить следующий запуск (команда на который успевает придти "соседнему" драйверу L_TIMER'а раньше).
Ключевым моментом является, видимо, то, что дохленький 50MHz MPC852 категорически не справляется с нагрузкой из 10 штук ADC250, так что, видимо, пока дойдёт дело до запроса на измерение line0 последнему осциллографу, уже успевает приилететь команд и произойти запуск.
Конкретно ТАМ проблема была решена добавлением
pzframe_drv_req_mes()
сразу после
pzframe_drv_drdy()
, но вообще это плохая практика и нужно ОБЩЕЕ
решение.
Некоторые соображения:
pzframe_drv_t
поля
param_run_mode
и param_run
.
pzframe_drv_init()
-- пару одноимённых параметров.
value_run_mode
(по аналогии с value_istart и value_waittime).
PZFRAME_RUN_MODE_ON_REQUEST = 0, PZFRAME_RUN_MODE_ON_RUN = 1, PZFRAME_RUN_MODE_AUTO_RE_RUN = 2
И само "учитываемое" надо будет добавить -- поле, сигнализирующее, что уже был _req_mes(), но ещё не приходило RUN:=1.
Очевидно, running_now
-- по аналогии с
measuring_now
.
...и как конкретно эти 2 поля должны взаимодействовать -- пока и есть главная неясность.
Или вообще объединить их в одно, с 3 возможными состояниями -- "IDLE"=0, "REQUESTED"=1, "RUNNING"=2?
value_run_mode
надо будет смотреть текущее состояние measuring_now
и
running_now
.
...если уж делать, то надо будет сразу про запас ещё по паре int'ов и pointer'ов завести.
28.03.2021@утро-пробуждение: для НЫНЕШНЕЙ-то цели всё элементарно -- просто лишнее
if (pdr->value_run_mode == PZFRAME_RUN_MODE_AUTO_RE_RUN) pzframe_drv_req_mes(pdr)
добавить. Но по-хорошему надо делать ВСЁ сразу -- чтоб реализовать проект
целиком (а то мало ли, что вылезет в процессе -- какие логические
конструкции или какие выводы напросятся).
15.04.2021: а ведь эта задача родственна хотелке "надо
уметь отключать функционирование pzframe-каналов" от 06-06-2016: для
реализации того желания надо ввести ещё один режим -- например,
PZFRAME_RUN_MODE_DISABLED = 3
.
(Ну да, не очень приятна "неупорядоченность" -- по-хорошему, лучше бы
сделать = -1
, но это плохо стыкуется с ручками-селекторами.
16.04.2021@~14:00, пешком идя
задами ИЦиГа, мимо теннисных кортов: кроме того, "-1" никак нельзя
было бы указать в auxinfo, т.к. там отрицательные значения ставятся в defval
и означают "значение не указано".)
Это, конечно, будет некоторое дополнительное усложнение модели работы, но уже невеликое -- примерно на уровне _AUTO_RE_RUN, что по сравнению с _ON_RUN совсем несложно.
16.04.2021: неа, не так -- усложнение скорее в стиле
_ON_RUN; точнее, просто подмножество тогойной функциональности: ведь надо
именно просто БЛОКИРОВАТЬ вызывание start_measurements()
. Да,
само "блокирование" -- это просто лишнее условие в
pzframe_drv_req_mes()
, но ведь нужно ещё и запоминать факт
"запрошенности", чтобы при смене режима произвести реальную отправку
запроса.
16.04.2021: несколько дней назад мылом спросил Роговского, что он думает на тему этого проекта -- в т.ч., бывали ли у него потребности в подобной функциональности?
Сегодня утром пришёл ответ; довольно длинный, и с разными деталями и оговорками, но общий смысл -- да, он был бы совсем не против, им такое будет небесполезно!
16.04.2021: только один нюанс: Роговский озвучил резонную хотелку "Мочь указывать этот параметр pzframe драйверам через auxinfo?"; указывать-то можно, но нынешняя инфраструктура во всех fastadc-драйверах предполагает указывание только
StartMeasurements()
.
ReadMeasurements()
.
А вот для pzframe_drv-параметров механизма передачи таких значений в
библиотеку отсутствует. Нужно будет организовать цикл по chinfo[], в котором
если chinfo[chn].chtype==PZFRAME_CHTYPE_PZFRAME_STD и nxt_args[chn]>=0,
то сбагрить значение pzframe_drv_rw_p()
.
...хотя, конкретно во всех CAMAC- и cPCI-драйверах присутствует
возможность указывать istart/noistart, но вот работает ли
оно -- вопрос; инфраструктуры/механизма что-то не видно. Скорее всего, в
драйверах оно осталось ещё от v2'шных (где pzframe_drv сам лазил в
pdr->cur_args[pdr->param_istart]
), а механизма
использования -- нету.
17.04.2021: проведён некоторый анализ кода, в результате чего сформировалось приблизительное понимание, как можно сделать.
measuring_now
, без лишнего "running_now
".
measuring_now
", с анализом, чтобы понять -- в какой ситуации
как должно будет измениться условие.
Состояние же _RQD -- просто прописывается, запрещая тем самым дальнейшие переходы (кроме как по RUN:=1), но и всё.
pzframe_drv_req_mes()
надо будет вынести в
отдельную функцию, чтобы непосредственно запуск мог бы производиться и из
других точек == вроде _RUN:=1.
19.04.2021: реализация:
PZFRAME_RUN_MODE_*
PZFRAME_MEASURING_*
chinfo[]
.
*_chan_dscrs[]
.
pzframe_drv_t
добавлены int-поля
param_run_mode
и param_run
, плюс пара резервных
reserved_param1
и reserved_param2
, а также пара
резервных указателей reserved_func_ptr1
и
reserved_func_ptr2
.
run_mode
и пара резервных
reserved_int1
и reserved_int2
.
pzframe_drv_init()
добавлены 6 параметров, отражающих
свежедобавленные поля pzframe_drv_t
, и их сохранение.
pzframe_drv_req_mes()
-- собственно запуск
измерения, с обвязкой -- вытащено в RunMeasurements()
.
PZFRAME_CHTYPE_PZFRAME_STD
-параметров
pzframe_drv_rw_p()
.
Некоторые соображения/сомнения насчёт "нештатного" завершения -- по STOP и по таймауту:
Главный вопрос -- ЗАЧЕМ? Что это даст и в какой ситуации? Ведь реально-то ничего не происходит и с точки зрения ДРАЙВЕРА никаких отличий между _NOT и _RQD нету -- параметры точно так же можно менять, пока не будет реально перейдено в _RUN.
Сейчас представляется, что лучше просто подождать -- если возникнет РЕАЛЬНЫЙ запрос, тогда и посмотрим.
Насколько это правильно, что потребуется дополнительный запрос "сверху" для перезапуска? Но запрос-то будет -- в обоих случаях будет вёрнут канал с флагом TIMEOUT и сервер заново пришлёт запрос (либо, при STOP(VALUE_DISABLED_MASK), измерения будут автоматически перезапущены).
20.04.2021: и ещё некоторые соображения/сомнения:
Откуда грызёт мысль -- а может, всё-таки как-нибудь разрешить-таки реакцию на STOP из состояния MEASURING_RQD?
measuring_now
-- никак не увидеть.
Может, нужен ещё один канал для отдачи наверх этого значения?
25.04.2021: мыслишка -- а может, в режиме ON_RUN выполнять запуск, даже если ранее и не было запросов (на чтение), т.е., даже не из состояния _RQD, а прямо из _NOT?
Сделать это, очевидно, несложно -- просто лишнее условие. Вопрос лишь в наличии реальной потребности: если никто не мониторирует -- то зачем?
Кроме того, если запросов сверху не приходило, то и ничего из флагов
*_rqd
взведено не будет и при срабатывании осциллографа наверх
ничего из векторных данных отправлено тоже не будет, а только статистика; ну
и зачем тогда вообще стартовать?
08.10.2021: надо бы всё же доделать. Битым текстом -- чего не хватает:
pzframe_drv_req_mes()
из
adc250_drv.c::FASTADC_IRQ_P()
.
ДЕЛАЕМ. Пока что -- для проверки -- на adc250_drv.c.
adc250_params[]
-- самое простое.
Lookup-табличка покамест сделана локально --
adc250_run_mode_lkp[]
.
Но потом надо её переделать в
глобальную pzframe_drv_run_mode_lkp[]
, чтоб была доступна сразу
всем драйверам.
15.10.2021: да, углобализована.
InitParams()
, в самый конец,
т.к. это точка уже непосредственно перед возвратом из
_init_d()
, и к этому моменту:
pzframe_drv_init()
уже вызвана, так что контекст
создан/инициализирован;
(Единственное сомнение -- "а что, если что-то пойдёт не так и драйвер
решит застрелиться ещё ДО возвращения из _init_d()
, а мы тут
вернём DEVSTATE_OPERATING
? Ведь в
cxsd_hw.c::InitDevice()
нет проверки "а не
застрелилось ли уже устройство к моменту возврата из метода
init_dev()
?" А надо
бы добавить! 09.10.2021: да, добавлено;
подробнее -- в разделе по cxsd_hw.)
pzframe_drv_rw_p()
.
Вот оная передача и сделана, причём вообще безусловная.
А потом, порывшись, нашёл заметки на эту тему от 16-04-2021:
pzframe_drv_rw_p()
, но только условно (при
>=0
).
Посему -- да, так и переделано.
14.05.2022: ага-ага, только "переделано" тогда (09-10-2021?) оно было ТОЛЬКО в adc250_drv.c. Сегодня же обобщено -- подробнее см. в отдельной секции ниже за сегодня.
09.10.2021: проверяем...
15.10.2021: добиваем:
adc250_run_mode_lkp[]
углобализована в
pzframe_drv_run_mode_lkp[]
.
Проверена -- работает.
measuring_now
в разных режимах", для чего в
adc250_rw_p()
добавлена отдача этого значения по запросу канала
51 (это отсутствующий ADC250_CHAN_CLB_STATE
).
На вид -- вроде бы всё как задумано работает. (Хотя, надо признать, полное тестирование -- всей матрицы вариантов -- проводить было просто лень.)
Засим проект вроде можно считать доделанным, так что "done".
pzframe_drv_rw_p()
-- istart, run_mode, wait_time.
Принципиальный кусок был сделан в октябре 2021, а сегодня оно "обобщено".
14.05.2022: тогда (09-10-2021?) оно было сделано
ТОЛЬКО в adc250_drv.c, причём прямо в конце
InitParams()
; в camac/ и cpci/ это всё бы не
работало, хотя всякие "istart"/"noistart там давно есть.
Теперь же "обобщаем":
FASTADC_INIT_D()
, в самый её конец: строка
заменена наreturn InitParams(&(me->pz));
r = InitParams(&(me->pz)); if (r < 0) return r; for (n = 0; n < countof(chinfo); n++) if (chinfo[n].chtype == PZFRAME_CHTYPE_PZFRAME_STD && me->nxt_args[n] >= 0) pzframe_drv_rw_p(>(me->pz), n, me->nxt_args[n], DRVA_WRITE); return r;
FASTADC_INIT_D()
в
camac_fastadc_common.h и cpci_fastadc_common.h.
...правда, там оно не протестировано, а cPCI сейчас даже и тестировать-то не на чем (?).
P.S. А запустилась эта вся активность тем, что Роговский позавчера попросил back-port'ировать возможность указания параметров через auxinfo в v2hw/'шные драйверы nadc200,nadc333,nadc4,nadc502. Вот полез я в них это делать, копируя из hw4cx/'ных, да и осознал, что в современных CAMAC'овских возможность указания через auxinfo есть, но работать не может.
14.05.2022: немного обсуждения выбранного решения:
Дело в том, что такое вот выбранное решение как бы "разбивает атомарность":
FASTADC_INIT_D()
просто "передавал
управление" в InitParams()
, а уж в какой точке тот если решит
прерваться и отдать ошибку -- его дело.
pzframe_drv_rw_p()
могут
иметь побочные эффекты, в т.ч. фатальные -- в первую очередь вследствие
того, что там выполняется ReturnInt32Datum()
, могущий
наткнуться на ошибку В/В и вызвать отрубание устройства.
...но тут надежда на то, что инфраструктура remsrv позаботится о
корректном завершении драйвера -- ключевые слова
being_processed
и being_destroyed
.
Кроме того,
...но так-то и работающая уже полгода в adc250_drv.c
инфраструктура (вследствие прохода по всей chinfo[]
, где у
_CHAN_SHOT значится PZFRAME_STD, а из-за отсутствия в
_params[]
значение в nxt_args[]
=0) вызывала
pzframe_drv_rw_p(, _CHAN_SHOT, 0, DRVA_WRITE)
и это не приводило ни к каким последствиям -- за счёт того, что значение 0
не приводит к сполнению команды.
15.05.2022: потенциально опасная цепочка --
"run_mode=auto_re_run,istart" (поскольку включение ISTART=1 приводит к
немедленному запуску), но лишь ПОТЕНЦИАЛЬНО, т.к. оный "немедленный запуск"
будет делаться ТОЛЬКО из состояния PZFRAME_MEASURING_RUN
, в
которое "машина состояний" без явного запроса не попадёт.
pzframe_drv_init()
, чтобы она брала оттуда
только несколько интересующих её значений -- конкретно ISTART, RUN_MODE и
WAITTIME.
Но для этого пришлось бы добавлять к ней аж 3 параметра:
chinfo[]
, chinfo_count
, nxt_args[]
.
Так что -- пусть будет так, как сделано сейчас.
Раздел создаём только сегодня, но содержимое первой записи было сделано ещё на прошлой неделе, просто записано было в ином месте, а сейчас перемещаем сюда.
_chan_dscrs[]
и _line_dscrs[]
.
Идея: на 1-й реагировать сразу, а следующие -- не ранее, чем, например, через 100мс (а если в течение этих 100мс были ещё, то по прошествию надо обновиться принудительно); хотя модель из manyadcs_knobplugin, где ждало "всех" выглядит красивше, но она не обломится ли при отказе части устройств (и неприсылании ими маркеров)?.
Как это реализовывать -- хбз.
Поизучал код в fastadc_data.c: ни-че-го не надо делать!!! УЖЕ всё есть:
fastadc_line_dscr_t
есть поле
cur_numpts_cn
.
У всех нынешних осциллографов в этой позиции стоит
FASTADC_DATA_CN_MISSING
.
ProcessAdcInfo()
выбирается максимум среди всех
по-канальных cur_numpts и общего cur_numpts.
Сделано это было, видимо, для manyadcs (которого пока так и нет), и для "осциллографа вообще" подойдёт идеально.
Тут надо дополнительно отметить пару процедурных аспектов:
Чтобы можно было и натравить из командной строки на некоторый набор каналов (программа "oscilloscope"), и использовать в скринах (библиотеку "liboscilloscope_knobplugin.a").
Да, кривовато, но уж как есть; а если пихать в xmclients/, то там библиотека knobplugin'а будет не к месту.
28.02.2020: поговорил с ЕманоФедей -- неа, сейчас оно не понадобится: мои скрины им не нужны, справятся с рисованием интерфейса сами. Так что оставляем проект просто проектом; если понадобится в будущем -- тогда и разморозим.
Появились они по результатам обмозговывания реальной задачи -- как сделать инженерный скрин для системы наблюдения за кикерами, где в cxhw:18 вместо 2 штук ADC200 стало 4, причём по паре составляют по сути "один 4-канальный осциллограф", так что желательно бы и отображать их на экране одной 4-канальной панелькой, а не плодить толпы 2-канальных (которые ещё и в экран плохо лезут).
Может, стоит часть работы выполнять на стороне СЕРВЕРА?
Т.е., прямо ДРАЙВЕР oscilloscope_drv.c, который бы по указанной в auxinfo информации собирал бы каналы от других устройств, представлясь наверх как единый осциллограф?
Вот сегодня приступаем. Штука обозвана "oneline".
Тогда "oneline" переименовать в "onei32l" ("One int32 line"), а вещественную назвать "onef64l" ("One float64 line").
08.04.2022: делаем.
_info2mes()
стала пустой (потом в неё, видимо, обсчёт
min/max/avg попадёт).
_x2xs()
устранена полностью.
dscr
'а почти все каналы превратились в
FASTADC_DATA_CN_MISSING
.
...но часть просто закомменчена (чтоб как минимум реперы потом вернуть, в одну линию с прочим).
...и запускаемости.
Теперь надо:
08.04.2022@совет-по-СКИФ-~15:00+: давно бродит соображение, что нужно уметь насчитывать минимум, максимум и среднее по полученной осциллограмме. Причём:
Ну, видимо, через int64
.
09.04.2022: сделано -- весь подсчёт min/max/avg (последний
через int64
). Пока холостое -- результаты никуда не деваются.
...а потом ещё и {R,D}-конвертировать.
10.04.2022: проверено -- панелька, в своём минимальном варианте, работает.
r:1000000
".
11.04.2022@утро: после того допиливания собственно отображения весь день вчерашнего воскресенья обдумывал задачу "а как бы всё-таки отдавать в скрин насчитанные значения min/max/avg?". Там довольно грустно:
Но с нынешней архитектурой pzframe/fastadc это так просто не получится, т.к.
Т.е., dataref'ы от %КАНАЛов даже хранить просто негде.
Соответственно, даже если бы и было куда складировать, то НЕКОМУ было бы регистрировать каналы.
Ну да, вот така "квазиобъектная" архитектура получилась. Просто не было надобности конкретным pzframe-типам устройств иметь свои privrec'и/поля, вот и не было их в архитектуре pzframe предусмотрено. Она и так-то получилась трудоёмкой, поэтому излишнего делать точно не требовалось.
11.04.2022: авотфиг -- не будет такое работать. Дело в
том, что распознавание '%'-имён выполняется вовсе не в
cda_add_chan()
, а лишь в паре его юзеров --
Cdr_treeproc.c и cda_f_fla.c. Соответственно, оно просто
попробует зарегистрировать канал с '%' в имени.
Тогда, в пятницу 01-04-2022, когда это всё задумывалось и обмозговывалось, была мыслишка сделать "vectorcalc_drv.c".
11.04.2022: о -- а может, "vector_minmaxavg_drv.c", который бы принудительно считал ровно эти 3 параметра, адаптируясь к разным исходным типам (заказывая CXDTYPE_UNKNOWN) и возвращая AVG всегда double, а min/max в зависимости от current_dtype?
11.04.2022: работаем!
12.04.2022: разбираемся.
Добавлена поддержка флага "@.", включающего NO_RD_CONV, после чего сырые значения стали осмысленными -- совпадающими с сырыми данными исходной осциллограммы.
...но на экране по-прежнему плоская горизонтальная линия в 0... Что-то с параметрами/калибровками?
Вывод: что-то накосячено в oneo32l_data.c.
Дело в том, что планировался драйвер под работу с "контроллером Tornado", где диапазон [-10V,+10V] и данные в микровольтах (R=1000000), а тестируется с ADC250 -- где и диапазон другой (по максимуму [-32768,+32767], а обычно и того меньше), и калибровка 4096 вместо 1000000.
Вот и получалась горизонтальная линия при отображении под намного более широкий диапазон.
При замене в onei32l_drv_i.h диапазона на [-32767,+32767] картинка отобразилась соответствующая оной от скрина adc250 (вдвое ниже, но то из-за конкретного текущего диапазона).
cda_new_context()
параметр
defpfx
был NULL вместо "insrv::".
...да, наследие от mirror_drv.c -- в том так же, ЕДИНСТВЕННОМ из "предназначенных для внутрисерверной работы".
После исправления обоих косяков проблема исчезла.
Вывод: концептуально-то вроде работает, но нужно бы сделать как-то понадёжнее и постабильнее.
@вечер, ~19:00 по пути домой из леса от общаги 8-ки проходя мимо Пирогова-22: сделать-таки драйвер const_drv и указывать значения каналов диапазонов с его помощью, а в onei32l требовать именно "полного" набора каналов вроде rangemin/rangemax? Кстати, для "контроллера Tornado" можно использовать именно const-каналы, а при отладке бриджеванием ADC250 -- ссылаться на егойные настоящие свойства, cpoint'ами.
15.04.2022: да, const_drv сделан.
Поэтому в onei32l_{_drv_i.h,data.c} добавлены каналы rangemin и rangemax.
Проверено по-прежнему на ADC250 -- через бриджинг "настоящих" каналов. Да, работает.
16.04.2022: раз уж в
fastadc_data.c::ProcessAdcInfo()
еще 09-04-2022 была
добавлена возможность получать CUR_NUMPTS без отдельного канала, а через cda
как количество элементов в канале данных, то пора уже тут от канала
CUR_NUMPTS избавляться.
Да -- использование его убрано, но сам канал из карты пока не удалён.
06.08.2024: цепочка:
ScheduleReconnect()
и
флаги is_suffering
/is_reconnecting
,
is_ready
, was_success
(список/имена флагов от
варианта к варианту разнятся).
Т.е., видимо, чтоб функциональность инкапсулировалась в некоем объекте
(который бы и содержал поля-флаги и всякие rcn_tid
), а
непосредственно "действия" по взаимодействию -- те же чтение/запись, да даже
взаимодействие с fdiolib -- выполнялись бы "виртуальными методами
наследника", т.е., кодом из "юзера"; ровно аналогично vdev и pzframe_drv.
По минимуму -- примерно то, что сделано в modbus_mon.c: там как раз и работа как с TCP, так и с локальными устройствами есть, и реконнекты -- причём с ОБОИМИ видами "подключений".
...да, в EPICS что-то из этого делается AsynDriver'ом, но он-то как раз монстр.
07.08.2024@утро: "rsdev" -- "Remote Serial DEVice"?
07.08.2024@по дороге в девятиэтажку, переходя около НГУ рядом с прямоугольным кольцом: "usdev" -- "Unified Serial DEVice"? "ulrdev" -- "Unified Local/Remote Device"? "rlsdev" -- "Remote/Local Serial DEVice"? Или не "dev", а "conn" -- ведь "CONNection" же?
08.08.2024: "pconn" -- "Persistent CONNection"?
15.11.2024: видимо, тогда выбрал имя "prstconn", т.к. есть файл 4cx/src/include/prstconn.h за 08-08-2024.
07.08.2024: ещё соображения:
08.08.2024: продолжаем размышлять.
Тогда название получается либо "pconn", либо "lrpconn" -- "Local/Remote Persistent CONNection".
open("/dev/...")
или прочего
открывания, дающего файловый дескриптор;
connect()
.
А какие ЕЩЁ могут быть варианты?
Может, стоит как-то обобщить, сделав "центральной" (core) функциональностью менеджмент переподключений, а собственно открывание/коннекченье -- вариантами/"плагинами"?
И да, тогда точно надо постараться понять, какие ещё могут быть варианты.
DeviceProxy
) -- вполне так пример такого "плагина",
хотя и весьма хренового (из-за блокирующести).
09.08.2024: продолжение размышлений/соображений:
O_NONBLOCK
.
И, тем не менее, имеющаяся связка прекрасно работает и никогда не
зависает. Видимо, потому, что can_hal_recv_frame()
вызывается
ТОЛЬКО при готовности дескриптора на чтение и чтение при этом делается 1
раз.
...а вот в прочих can_hal'ах, c4lcan_hal.h и marcan_hal.h -- которые открывают не интерфейс, а файл /dev/can* -- сразу делается
fd = open(devpath, O_RDWR | O_NONBLOCK);
11.11.2024: а ведь это ровно то, что сейчас (см. за 06-11-2024) предполагается сделать для Modbus-драйверов с прицелом на прочие serial: нижние 3 уровня ПЯТИслойного стека модулей -- "Менеджер портов", "Драйвер порта", "Открыватель конкретного типа порта".
24.02.2025@Зеленоград-ТНК-к316: 5-слойность фиг знает, как
делать -- переплетён уровень "поддержание соединения" с предыдущими: как
минимум, RegisterWithFDIO()
плюс ProcessIO()
,
процесс инициирования соединения/open()
. Пока забить?
01.03.2025@лыжи: чисто для записи -- поскольку у нас не так много драйверов, работающих с COM-портами, то функции этого "уровня этажом выше открывателей" должны удовлетворять требованиям: kshd485/piv485, liu_key_drv, плюс древнему impacis10.
17.03.2025: и ещё "bondarenko_mod_ubs" -- драйвер должен через UART "контроллера Торнадо" работать с платой Антона Бондаренко, по некоему специфичному байтовому протоколу. Нюанс в том, что скорость нестандартная -- 125000кбит/с.
25.02.2025@Зеленоград-ТНК-к316: хотя "открывателей" сделать несложно: массив из описателей {char*prefix; func do_open; int media_kind;} (последнее -- network/local), что-то вроде
struct { char *prefix; // "ttyS", "ttyM", "mxu" func do_open; int media_flags; // Network (requires w-ready) or local (immediately ready after open) };
А do_open() очевидным образом возвращает либо файловый дескриптор, либо
-1
при ошибке.
27.02.2025: потенциальный UDP работает как "local": он ведь сразу "готов" после открытия.
- Отдельный файл определений, годящийся для разных проектов, а не только CX -- GeneralRules.mk, а конкретности -- в специфичном для проекта Rules.mk, каковой всеми напрямую и включается.
- В списках target'ов имена указываются БЕЗ расширений (т.е., EXES=..., а не ZZZ_SOURCES=aaa.c..., как было издавна). Причем -- УНИФИЦИРОВАННО, т.е., всегда именно EXES=..., а не, например, XMCLIENTS=... или UTILS=...
- Компоненты для составных бинарников указываются как binary_COMPONENTS=c1.c..., а уж система сборки сама сгенерит из них список .d-файлов.
- Кроме того, она на основе
$(binary_COMPONENTS)
сама сгенерит .mkr-файлы с указанием зависимости бинарных файлов от соответствующих .o.- Возможность указывать в директории кроме "target'ов" еще и локальные библиотеки, которые должны быть собраны.
- "Вакантные места" -- дополнительные, по умолчанию пустые, переменные во всех потенциально интересных местах --
$(LOCAL_INCLUDES)
,$(LOCAL_DEFINES)
,$(LOCAL_LIBDEPS)
, etc.- Плюс, возможность указывать локальные дополнения ко всем
$(nnnFLAGS)
. Поскольку по стандарту юзер должен иметь возможность безопасно указывать в командной строке, например, CFLAGS=..., то все флаговые переменные должны иметь хитрую структуру: указываться программам будут "составные" переменные вида$(ACTUAL_nnnFLAGS)
, которые=$(nnnFLAGS) $(REQD_nnnFLAGS) $(LOCAL_nnnFLAGS)
.- "Клиентские" Makefile'ы будут избавлены от всей рутины -- НЕ будет требоваться указание ".PHONY:all" и "all:firsttarget", а также
include $(ALLDEPENDS)
будет делаться автоматически.
08.11.2005: ну-с, приступаем. Экспериментировать будем на директории programs/server/ -- там и составной бинарник найдется, и .so-файлы.
09.11.2005: файл порядочно подизготовлен.
10.11.2005: придумал решение, как избежать Си-центричности:
Надо НИКОГДА не указывать имена исходных .c-файлов, а всегда предоставлять список ИМЕННО .o-КОМПОНЕНТОВ.А уж make нехай сам находит, из чего можно сгенерить эти .o'шки.
Естественно, это подразумевает, что для каждого расширения
".E" должно предоставляться не только правило
%.o:%.E
, но и %.dep:%.E
. Но уж это-то не
проблема -- всегда можно либо найти генератор зависимостей, либо
сгенерить статические файлы (например, .depsrc) и "генератор"
сведется к "cp $@src $@
".
Следствие 1: а ВООБЩЕ НИКОГДА и нигде не следует иметь никаких
nnnSOURCES -- они ведь теперь абсолютно ни к чему! Далее -- все
разрозненные nnnSOURCES заменяются наборами
bin_COMPONENTS, где список этих "bin" предоставляется в
$(TARGETFILES)
.
Следствие 2: ВСЕГДА, для ВСЕХ target-файлов, включая .a, .so, и проч., будут изготавливаться .mkr-файлы. Ну и фиг с ними -- они ничему не противоречат, скорее даже наоборот -- все в струю.
10.11.2005: итак, первый этап испытаний "на кошечках" проведен -- в директории programs/server/ файл из двух .c'шников оно успешно собирает.
Теперь надо проверять работу с архивами. lib/misc/?
11.11.2005: изготовил первоначальный вариант работы с архивами; вроде работает.
Список .a-target'ов текущей директории указывается,
аналогично бинарникам, в переменной $(ARLIBS)
. Состав
библиотек указывается переменными $(libNNN.a_COMPONENTS)
-- также просто список .o-файлов.
Файлы *.a.mkr генерятся по несколько другим правилам -- так
что имеют вид не "target: dependencies...", а "libNNN.a:
libNNN.a(dependencies...)". Это обеспечивается тем, что собственно
команда, генерящая содержимое .mkr-файла, теперь указывается в
переменной $(_MKR_LINE)
, и для описаний зависимостей
библиотек эта переменная при помощи target-specific-указания делается
другой. 23.07.2014: см. также замечание насчёт
_GIVEN_COMPONENTS
ниже за сегодня.
А список TARGETFILES теперь собирается из $(EXES) $(SOLIBS)
$(ARLIBS) $(UNCFILES)
, где последнее -- это для всяких одиночных
файлов, типа генеримых/симлинковых .h'ей.
13.11.2005: отчет по пунктам:
Посему теперь target'ы классифицированы --
TARGETFILES=$(COMPOSITE_TARGETS) $(MONOLITHIC_TARGETS)
,
где
COMPOSITE_TARGETS=$(EXES) $(SOLIBS) $(ARLIBS) $(LOCAL_COMPOSITE_TARGETS)
,
а
MONOLITHIC_TARGETS=$(UNCFILES) $(LOCAL_MONOLITHIC_TARGETS)
,
и вся алхимия с .d- и .mkr-файлами выполняется только
для первого класса.
Тем самым, подразумевается, что ЛЮБЫЕ .c-программы будут считаться композитными, в т.ч. и состоящие из одного-единственного исходного файла (собственно, именно так всегда в системе *Rules.mk и было).
И еще замечание -- собственно "основной файл" также надо ВСЕГДА указывать в списке компонентов, в т.ч. если он и вовсе единственный.
$(strip ...)
вокруг содержимых
определений $(ACT_nnnFLAGS)
-- дабы лишние пробелы не
плодились.
Только при этом обнаружилось, что шибко уж многое не "подключается"
при отсутствующем define-символе _GNU_SOURCE
. Как
выяснилось, "это не баг, это фича" -- в самом наипрямейшем
смысле. Цитата из libc.info, "Feature Test Macros":
- Macro: _GNU_SOURCE If you define this macro, everything is included: ISO C89, ISO C99, POSIX.1, POSIX.2, BSD, SVID, X/Open, LFS, and GNU extensions. In the cases where POSIX.1 conflicts with BSD, the POSIX definitions take precedence.
Посему теперь при OS==LINUX выставляется
CPPFLAGS=-D_GNU_SOURCE
. Именно CPPFLAGS
, на
случай, если юзер захочет сам что-то указать (а в соответствии с
make.info, он должен иметь право их указывать -- "Users expect to be
able to specify `CFLAGS' freely themselves.").
15.11.2005: продолжение пикника:
$(DEBUGGABLE)
уставляются флажки "-g
" для
компилятора и линкера, а также оно управляет флагом "-s
"
линкера.
-ldl
, а то проблемы при запуске с нею программ, не
использующих libdl.so.
Посему -- во-первых, скомпонована переменная
ACT_LIBS=$(LIBS) $(SPECIFIC_LIBS) $(LOCAL_LIBS) $(RQD_LIBS)
,
Стоит отметить необычный (для других ACT_-переменных) порядок
компонентов -- он обусловлен тем, что вначале должны идти зависящие
библиотеки, а затем "провайдеры". Выбранный порядок представляется
наиболее универсальным.
А $(SPECIFIC_LIBS)
предназначена для указания
per-target-библиотек, в формате "target: SPECIFIC_LIBS=...".
Возможно, стоит концепцию SPECIFIC_nnn распространить и на все
остальные ACT_nnn.
Во-вторых, оная переменная указывается в самом конце команд как линковки бинарников, так и .so'шников.
В-третьих, под IRIX и SOLARIS уставляется RQD_LIBS=-lsocket
-lnsl
.
27.06.2006: да, распространил концепцию
SPECIFIC_nnn -- теперь поддерживаются и
$(SPECIFIC_CFLAGS)
, $(SPECIFIC_CPPFLAGS)
$(SPECIFIC_DEFINES)
, $(SPECIFIC_INCLUDES)
,
$(SPECIFIC_LDFLAGS)
, $(SPECIFIC_ARFLAGS)
.
И, кстати, кратко -- что для чего:
ACT_nnn | Реально передающийся программе набор ("суммирующая" переменная, в которую все и собирается) |
RQD_nnn | Необходимые -- уставляются в GeneralRules.mk |
PRJ_nnn | Project-specific/global -- уставляются в Rules.mk |
DIR_nnn | Специфичные для данной "умной" директории -- уставляются в LocalRules.mk |
LOCAL_nnn | Специфичные для данной ЛОКАЛЬНОЙ директории -- уставляются в Makefile'ах |
SPECIFIC_nnn | Per-target -- уставляются в Makefile'ах |
И -- надо бы проверить, что для всех nnn учитываются все вышеперечисленные подвиды.
03.07.2007: да, теперь у всех категорий учитываются все подвиды. Кстати, у ACT_LIBS порядок перечисления составляющих ее ttt_LIBS -- просто обратный по сравнению с обычным.
29.01.2009: нужна еще одна "категория" -- "подпроект". Например, если проектом будет CXv4, то ПОДпроектом -- qult/, CAN-драйверы, и т.п. КАК НАЗОВЕМ?!?!?! SPJ?
09.07.2009: мелкое дополнение: теперь ALLDEPENDS формируется не только из COMPOSITE_TARGETS+.d, но и из MONOLITHIC_DEPENDS+.d -- чтобы для однофайловых программ тоже зависимости генерились и использовались.
20.01.2012: да, добавляем "SPJ" -- пока называем именно так, а потом подумаем, как можно переименовать.
(Может, надо не ЭТО как-то обзывать -- а переименовать нынешний PRJ во что-то (SYS?), а уж "это" -- в PRJ?)
Да-да, точно!!! Только не в "SYS", а в "TOP": и тогда
заменить уже понятно что сильно мешающее SRCDIR
на
TOPDIR
! И, соответственно -- Rules.mk
превращается в TopRules.mk.
Кстати, словцо TOPDIR еще в 1999-м использовалось в fonts/Rules.mk в т.ч. в этой роли. А еще переменная TOPEXPORTSDIR прямо в *cx/ -- с момента появления exports.
22.01.2012: сделано:
SRCDIR
->TOPDIR
PRJ*
->TOP*
SPJ*
->PRJ*
23.01.2012: имелась лёгкая кривость: UNCFILES входили в список MONOLITHIC_TARGETS, и для них пытались (через MONOLITHIC_DEPENDS) генериться .d-зависимости. Естественно, для всяких скриптов и тому подобных НЕ-.c-файлов сие обломится. Решение таково:
UNCFILES
вынесена из
MONOLITHIC_TARGETS
напрямую в TARGET_FILES
.
Так что проблема зависимостей решена.
MONO_C_FILES
. При надобности C++ можно будет добавить
туда и MONO_CXX_FILES
. Благодаря тому, что список
зависимостей формируется не как ".c:=.d", а "$(addsuffix .d, $(basename
...))", это будет работать -- вообще ЛЮБОЕ расширение покатит.
23.01.2012: добавлена еще одна категория --
ARCH_
, для специфичностей [кросс-]компиляции под
конкретную архитектуру.
23.01.2012: командная строка для сборки бинарника
теперь определена в _O_BIN_LINE
-- по аналогии с
_C_O_LINE
. Смысл -- для простоты определения правил
сборки файлов с иными именами (типа "%.drvlet:%_drv.o").
17.04.2012: надо наспространить концепцию
модульности/категорий также на GENERATEDFILES
,
INTMFILES
и BCKPFILES
. Соответственно,
нынешние их значения должны превратиться в RQD_*
.
Сделано:
GNTDFILES
, BCKPFILES
,
INTMFILES
, WORKFILES
.
RQD_*
стандартным образом,
только SPECIFIC_*
отсутствует, ибо не в тему.
GENERATEDFILES
изведена.
LOCAL_WORKFILES
.
LOCAL_
-- поскольку де-факто оно является
расширителем обычных Makefile; только с "+=
"
вместо "=
". А так -- хоть категорию SHADOW_
вводи, в дополнение к ARCH_ :-). 02.05.2012: да,
сделана SHD_
.
20.04.2012: вроде добавлены create-exports, exports и install -- скопированы из v2'шного Rules.mk практически один-в-один.
Изменение -- INSTALLROOTDIR
переименована в
TOPINSTALLDIR
, для соответствия общей концепции.
Надо проверять.
29.04.2012: был баг еще со времен Rules.mk: у
вне-деревного варианта install
(аналог внутридеревного
exports
) не было зависимости от all
. Видимо,
почти никогда оно не использовалось, вот и осталось незамеченным.
Исправлено.
29.04.2012: вылезла проблема с
UNCFILES
: оно много где использовалось для перечисления
"имеющихся" файлов, которые нужны, но никак не генерятся (например, все
в **/drv_i/). А UNCFILES участвует в
TARGETFILES
, следовательно -- по "make clean" эти файлы
все стёрлись!
Для исправления введено GIVEN_FILES
:
files
вместе с TARGETFILES.
02.05.2012: введена категория SHD_
-- для ShadowRules.mk, чтоб они в LOCAL_ не лезли.
13.05.2012@Снежинск-каземат-11: поскольку эта система сборки успешно введена в эксплуатацию в v2, то "первоначальная версия" уж точно готова, так что "done".
Прочие дополнения пойдут уже отдельными пунктами.
23.07.2014: мелкое дополнение: в
.a.mkr-specific версию _MKR_LINE
добавлено, что
содержимым .a-файлов является указанное не только в
$($*_COMPONENTS)
, но плюс еще и в
$($*_GIVEN_COMPONENTS)
.
25.02.2016: в TopRules добавлена поддержка "install" и "exports" -- скопирована из v2 (а в GeneralRules.mk всё уже было, т.к. он идентичен).
EXPORTSFILES
в include/Makefile --
без этого ничего не работало.
Если где чего будет не хватать -- добавим в рабочем порядке.
(А то в CXv2 include-файлы ВСЕГДА берутся из src/include/.)
14.07.2008: в CXv2 это уже исправлено -- 23-05-2008.
20.04.2012: а здесь-то тогда еще нет! А "done" уже поставил!
Но сейчас действительно пофиксил.
deco
, и она оставляла backup-файлы
.b, в Rules.mk в определении BCKPFILES
упоминалось "*.b .*.b". Это явно устарело -- разумные редакторы всегда
делают backup-суффикс ~, а расширение ".b" может
где-то и использоваться.
15.05.2006: удалены.
(По опыту запинывания сегодня связки CX+cm5307_драйверы+клиенты на RH-7.3@beast в ЭЛС-2.)
15.07.2008: собственно, а проблема-то в чем? Видимо, в том, что хочется:
13.05.2012@Снежинск-каземат-11: с тех пор всё именно по такой схеме и делается -- 4cx/+drivers/, cx/+v2hw/, так что этот раздел можно считать "done".
Пожалуй, стоит перейти на такую схему.
26.01.2012: да, воистину стоит -- тем более, что .mkr именно так (просто добавлением) и делается. Вообще, нынешняя схема -- это скорее "хак", т.к. в нём недостаточно общности (хотя оно и чуток элегантнее).
Тривиально -- в exports/ надо иметь еще одну
директорию, которая содержала бы отражение обычного дерева
src/, но населением там были бы только *.mk-файлы. И
выделить для них даже отдельную "категорию" --
EXPORTSFILESM
.
13.05.2012@Снежинск-каземат-11: да, ЭТО -- надо сделать, только с учётом желания перейти со статичной схемы EXPORTS{FILES,DIR}{,2,3} на конфигурируемый (см. за 25-04-2012).
По-хорошему, надо б оттуда вообще %.o убрать, а исходники/компоненты пусть определяет по зависимостям. Единственный вопрос -- для однофайловых, чтоб оно как-то догадывалось об исходниках БЕЗ target_COMPONENTS...
Все куски/директории, допускающие "стороннее" использование (хоть симлинкованием, хоть напрямую) должны САМИ предоставлять make-код, обеспечивающий это.
А вовсе НЕ места-юзеры должны сим заниматься, как сейчас.
02.09.2010: Преимущества также очевидны:
Соответственно, любые исправления делаются легко и просто, причём в одном-единственном экземпляре.
Собственно, это вообще очевидно -- так бы и надо было делать с самого начала (но история была другой -- "симлинкуемости" были приёмышами).
Примерно так уже сейчас сделано в work/drivers/can/, но:
И даже programs/ (типа server/, drivers/ и utils/.
А, например, ShadowRules.mk.
Впрочем, в нынешней GeneralRules.mk-based системе сборки бОльшая часть содержимого Makefile'ов -- это и есть собственно списки компонентов. Всё же прочее -- это зависимости да определение специфичных флагов.
25.12.2011: да, надо сделать вот ТАК:
Другое дело, что с более красивой и стройной v4'шной системой сборки (переменные CLASS_nnn, TARGET_COMPONENTS, авто-генерация через *.mkr) надобность в таких файлах сильно падает -- фактически на них останутся лишь определения доп. библиотечных зависимостей.
libuseful.a_COMPONENTS
.
DRIVELETS=...
).
DIR_DEPENDS
.
DIR_TARGETS
.
Придумывать какую-нибудь замуть на тему преобразования имён между исходниками и бинарниками?
Неа, это надо возлагать на конкретные "DirRules.mk", в них максимальная гибкость.
17.04.2012: да, введена DIR_TARGETS
.
18.04.2012: а как можно в DirRules.mk выполнять "произвольные преобразования" -- да очень просто:
cm5307-ppc-%.drvlet:%_drv.o
).
13.08.2013: введена также и
LOCAL_DEPENDS
-- чтоб Makefile'ы могли указывать
на необходимость генерации .d
-файлов для
локально-используемых "библиотек". Это оно на замену старым
SUBFLSSOURCES
и UTILSSOURCES
(имевшихся
только в server/drivers/*.mk), для тех ленивых директорий, что
не хотят использовать более общий механизм *_COMPONENTS.
Сейчас решение простое -- помещать их в отдельную директорию, БЕЗ Makefile, и брать для линковки оттуда. В принципе -- это даже красивее.
А возможно ли сделать так, чтобы удалялись лишь те .o-файлы, которые оно генерит само?
17.04.2012: неа, скорее НЕВОЗМОЖНО. Поскольку главный принцип данной системы сборки -- указываются компоненты, из которых собираются конечные файлы, а уж каким путём make их сделает -- вопрос свободный. Поэтому взять список промежуточных .o (или каких прочих подобных) -- решительно неоткуда.
В этом, конечно, и слабость данной системы сборки -- что для всех прочих генеримых при сборке типов файлов придётся отдельно указывать необходимость их удаления. Но уж как есть -- для наших целей эта система отлично подходит.
Так что -- "withdrawn".
В общем -- конфликт, требующий разрешения :-)
23.04.2012: по здравому размышлению решено было не делать избавление от ".PHONY:firsttarget" фетишем -- пусть оно будет в тех сравнительно редких makefile'ах, где требуется включение внешних хитрых правил. Это намного меньшая проблема, чем та монструозная двухстадийная схема.
28.04.2012: собственно -- а почему в makefile'ах? Пусть уж оно тогда и будет в тех .mk-файлах, ради которых нужно!!!
25.04.2012: причина -- в разных местах требуются разные наборы пар ДИРЕКТОРИЯ/ФАЙЛЫ, и:
Как сие может быть реализовано:
exports:
это почти ничего не стоит
-- сменить три строчки на
$(foreach NNN, $(strip $(ACT_EXPORTSPAIRS)), ...)
install:
, с алхимией
_OI_...
, будет посложнее:
Но это кривовато и неприятно, т.к. тут надобность в генерении не списка зависимостей (которые синтаксически должны быть на разных строчках, что внутри-make'ным циклом не сделаешь), а просто последовательности shell-команд через ';'.
$(call)
.
cp -Rp
" вовсе НЕ должна копировать симлинки
корректно. Для симлинков официально нужен ключ "-d"
(или
"-P"
?).
Нынешнее поведение с "-R" появилось 30-01-2004 (см.
bigfile-0001.html) ради симлинков в chlclients/, и оно
выполняет задачу. Вопрос -- почему? И не переделать ли всё-таки как надо
("cp -dpR
"?)?
libNNN.a_COMPONENTS
в самих Makefile'ах на файлы
libNNN.a_COMPONENTS.mk -- libCdr.a_COMPONENTS.mk,
libdatatree.a_COMPONENTS.mk, libmisc.a_COMPONENTS.mk,
libcxscheduler.a_COMPONENTS.mk,
libuseful.a_COMPONENTS.mk.
31.01.2020: также добавлены libcda.a_COMPONENTS.mk, libcda_d_insrv.a_COMPONENTS.mk, libcx_async.a_COMPONENTS.mk, libcx_sync.a_COMPONENTS.mk -- для возможности выполнять кросс-сборку под Android NDK.
P.S. Это в дополнение к libpzframe_gui.a_COMPONENTS.mk и libpzframe_data.a_COMPONENTS сделанным в июле 2016-го и libCdr.a_COMPONENTS.mk в октябре(?) 2016-го.
Собственно, мысли КАК это реализовать:
%_builtins.c: %.modlist $(TOPDIR)/scripts/mkbuiltins.sh <$< >$@
InitBuiltins()
и TermBuiltins()
, для вызова
их из PROGNAME.c.
22.11.2016: делаем, по тому проекту. Детали:
read
возвращает exitcode, так
что цикл свёлся к while read ...
.
Ответ оказался прост: конструкция $'СТРОКА', при этом СТРОКА может содержать \-коды.
Так что надо будет либо
${SHELL} ...mkbuiltins.sh
-- чтобы учитывалось
указываебельное в командной строке SHELL=...
Лучше, наверное, второй вариант.
ТИП_МОДУЛЯ ИМЯ_МОДУЛЯ
WAS_тип
, которая изначально пустая, а при встрече такого типа
выставляемая в =Y.
И потом строки #include ...
выдаются только при
уставленности соответствующей WAS_тип
-- так в .c-файл
попадает лишь то, что реально используется.
23.11.2016@утро-дома: по-хорошему, кроме *builtins.c надо бы еще генерить списочек файлов (компонентов для влинковывания) для make, чтобы не надо было вручную поддерживать когерентность.
...и еще -- надо бы и *_builtins.h тоже уметь генерить.
23.11.2016: продолжаем:
Из соображений инкапсуляции и недублирования явно лучше второй вариант -- ОДИН скрипт.
Так и сделано.
$1
, с последующим
case
на валидность указаного -- -Mc
,
-Mh
, -Mm
.
-Mm
требует указания имени переменной, в
которую делается присвоение списка файлов.
case
, с разным выводом в зависимости от запрошенного.
<<__END__
-- корректно взаимодействуют с условными
конструкциями (if
, case
). А то были сомнения.
#define
-символ для не-включения-повторно. И этот символ
обычно делается из имени файла -- где его брать?
Но тут этот файл на програму всегда один, так что выбрано статичное имя
__MAIN_BUILTINS_H
.
Это должен будет как-то решать Makefile. Использовать
VPATH
?
cxsd
модули вообще
влинковываются в него не из .o
-файлов, а из библиотек!
Ну на СЕЙЧАС решение тривиально -- не использовать фичу -Mm
.
А в будущем -- возможно, надо будет повсеместно оставлять модули ТОЛЬКО в объектниках.
...хотя есть еще простое решение: 3-е поле в строке -- "параметры". И, например, "-" -- не добавлять в список линковки.
24.11.2016: далее:
InitBuiltins()
добавлен параметр-функция
"err_notifier
". Это замена имевшегося cxsd'шного logline() --
он вызывается при init_mod()<0
:
magicnumber
и
name
-- чтоб можно было их выдать.
InitBuiltins()
от старого cxsd'шного
-- тут есть int-код возврата.
-Mc
тоже нужен параметр -- имя .h.
-Mc
становится не нужен.
-Mc
убран.
25.11.2016: внедряем:
InitBuiltins()
добавлен параметр NULL.
Вроде работает как надо.
Расширение и углубление:
Не проверено пока, конечно.
06.09.2016@Снежинск-каземат-11: подразобрался.
Так что считаем, что всё окей, а пункт "withdrawn".
А в том проблемном случае проставлена зависимость
liu: panov_ubs.m4 kshd485.m4
т.е. "бинарного" клиента (реально симлинка) от include-файлов.
TOPDIR= $(HOME)/work/4cx/src
В результате оно нормально собирается только в ~/work/, а в других местах -- фиг, приходится править.
Вот если бы сделать ОТНОСИТЕЛЬНУЮ ссылку --
$(PRJDIR)/../4cx/src
-- тогда могло бы компилироваться из
любого места!
03.11.2016: но сегодня@по-дороге-с-обеда-перед-ОК подумалось: ведь этак, с "../", оно будет считаться за не-OUTOFTREE! И как это акунется?!
После обеда@по-пути-из-пультовой-на-лестнице-из-подвала: да вот
нифига -- НЕ будет оно так считаться! Ведь "внутридеревной" считается, если
в $(TOPDIR)
нет ничего, кроме точек '.' и слэшей
'/', а в той относительной ссылке есть "4cx/src".
10.01.2017: сделал. Работает.
28.01.2019: переделана также остальная толпа (помимо тогда сделанных hw4cx/, sw4cx/ и weld/ (этот-то почему?!)) -- v2'шные astand/, bpms/, liu/, ndbp/, qult/, v2hw/.
Понадобилось для возможности собрать на foxy файлик
kazstand_db.so для отправки снежинцам: на foxy единственный у нас
CentOS-5.2, а из-за ейного 4-гигового CF сборку надо вести не в ~/,
а в /net/ctlhomes/export/HOST/oper/BUILD/4foxy/ -- вот тут-то и
аукнулись прямые ссылки на $(HOME)
.
...заодно обнаружилась в sw4cx/screens/Makefile ссылка
V4HDIR= $(HOME)/work/hw4cx
-- позор! Исправлено.
никогда, Никогда, НИКОГДА нельзя указывать имена библиотек вида
"-lLIBNAME" в ЗАВИСИМОСТЯХ; а можно только в *LIBS
, лишь
передаваемых линкеру.
Ниже история.
08.06.2019: история вышла позорноватая, поучительная, но разрешилась быстро.
/lib/libdl.so: error adding symbols: File in wrong format
(Это НЕ xfte; а что же тогда? А-а-а, это я пытался gcc-7 собрать (для ЧеблоПаши и Славы проверял простоту сборки из исходников), чтоб он и под x86 тоже был, но не вышло, так что ограничился "--disable-multilib", но RPM'ки остались.)
Directory search applies in a special way to libraries used with the linker. This special feature comes into play when you write a prerequisite whose name is of the form `-lNAME'. (You can tell something strange is going on here because the prerequisite is normally the name of a file, and the _file name_ of a library generally looks like `libNAME.a', not like `-lNAME'.) When a prerequisite's name has the form `-lNAME', `make' handles it specially by searching for the file `libNAME.so', and, if it is not found, for the file `libNAME.a' in the current directory, in directories specified by matching `vpath' search paths and the `VPATH' search path, and then in the directories `/lib', `/usr/lib', and `PREFIX/lib' (normally `/usr/local/lib', but MS-DOS/MS-Windows versions of `make' behave as if PREFIX is defined to be the root of the DJGPP installation tree).
$(LIBDL)
.
Видимо, не находя подходящего в обычных путях поиска, make просто оставлял указание "-ldl" нетронутым, так что оно доходило до линкера и исполнялось должным образом.
После перетаскивания $(LIBDL)
из зависимостей в
SPECIFIC_LIBS
всё починилось.
Это было единственное такое косячное место (созданное, очевидно, впопыхах).
MOTIF_LIBS
(и много в старом
cx/src/TopRules.mk).
Причины этого "вудуизма" можно найти в bigfile-0001.html за 22-06-2006...26-06-2006 (там какая-то гадость в FC4) плюс за 26-01-2009.
Выглядит это всё фиговато; надо бы оный бардак устранить -- скорее всего, в СОВРЕМЕННЫХ системах большая часть уже нафиг не нужна (хотя поддержку старых систем желательно бы сохранить -- хотя бы RH-73).
23.07.2019: делаем.
%.dep: %.cpp
". Она-то
идентична обычной "%.dep: %.c
", но чтоб не дублировать код,
выбирающий ключики для "gcc -MM" в зависимости от $(ISNEWGCC)
,
теперь в ifeq
определяется переменная _C_DEP_LINE
,
вместо просто правил. А правила -- ВНЕ if'а -- уже используют её.
%.o: %.cpp
" --
скопировано с "%.o: %.c
".
Это работает, хотя минус есть: плюсовый компилятор ругается (нефатально), что
cc1plus: warning: command line option '-Wstrict-prototypes' is valid for C/ObjC but not for C++ [enabled by default] cc1plus: warning: command line option '-Wmissing-prototypes' is valid for C/ObjC but not for C++ [enabled by default]
А в варианте с AUXWARNS=all
добавляется ещё
cc1plus: warning: command line option '-Wbad-function-cast' is valid for C/ObjC but not for C++ [enabled by default]
В принципе, это терпимо (warning'и даже не расцвечиваются :-)), но в
идеале надо бы всё-таки умудриться сделать РАЗНЫЕ списки ключиков
-W...
. Для чего всё же придётся вводить отдельную
$(_CPP_O_LINE) плюс ACT_CXXFLAGS.
30.07.2019: пришлось-таки сделать "полную" поддержку
компиляции плюсовых исходников, причём даже с отдельной поддержкой
%.dep: %.cpp
.
Всё ради компиляемости cda_d_tango.cpp, где у tango.h
есть требование, через системный файл devapi_attr.tpp (ну и
расширение!), на наличие ключа -std=c++11
, причём прямо при
препроцессинге. Как потом стало ясно, для решения задачи это было
необязательно -- в конечном итоге обошлись строчкой
SPECIFIC_CPPFLAGS=-std=c++11
.
GXX
(=g++) и GXXCMD
.
CXX
=$(GXXCMD).
ACT_CXXFLAGS
формируется сложно:
$(CXXFLAGS) $(RQD_CXXFLAGS)
, ...
_CFLAGS
и _CXXFLAGS
.
Т.е., используются флаги как от обычного Си/gcc, так и специальные для C++/g++.
CXXFLAGS
же зеркалит обычный CFLAGS, но там используются
$(CXXAUXWARNS)
и $(CXXWARNINGS)
вместо обычных.
CXXAUXWARNS
и CXXWARNINGS
отличаются
убранными "-Wbad-function-cast
" и "-Wstrict-prototypes
-Wmissing-prototypes
" соответственно; добавлено пока что ничего не
было.
_CXX_O_LINE
, и правило
"%.o: %.cpp
" переведено на неё.
_CXX_DEP_LINE
, и правило
"%.dep: %.cpp
" переведено на неё.
Вот ЭТО оказалось не очень осмысленно, поскольку по большому счёту для
решения проблемы «как указывать "-std=c++11
" и
препроцессору тоже» пришлось бы ввести какую-нибудь фигню вроде
*_CXX_CPPFLAGS (флаги, специфичные для препроцессирования конкретно плюсовых
файлов).
13.06.2023: ну вся шобла *_CXX_CPPFLAGS
введена (что любопытно, сейчас пришёл к именно тому же самому имени, что и
почти четыре года назад).
30.07.2019@вечер-дома: а
вот как можно сделать, чтобы для конкретных бинарников в качестве линкера
вызывался бы g++ вместо gcc: ввести специальную
.PHONY
-цель "G++-LINKED", у которой ставить
"LD=$(GXXCMD)
", и требуемым бинарникам ставить эту цель в
зависимости (или ей их?).
31.07.2019: попробовал. Пока не очень вытанцовывается.
tango_cdaclient: LD=$(GXXCMD)
-- то эффект отсутствует. Очевидно, по причине отсутствия нужного "направления" зависимости, так что значение переменной, определяемой для GXX-LINKED, не пропагируется на её зависимости..PHONY: GXX-LINKED GXX-LINKED: LD=$(GXXCMD) GXX-LINKED: tango_cdaclient
Может, не выпендриваться, а просто пользоваться вариантом 1, работающим? Недостаток лишь в том, что во ВСЕХ использованиях придётся указывать конкретную специфику, вместо просто символьного заклинания "вот это линкуем g++".
Смысл -- чтобы можно было коммитить прямо из собранной директории, где кроме исходников лежит также море всякого (включая и бинарники, и .d'шки, и даже генерённые .c'шки), и чтоб git НЕ ругался "у вас тут есть неизвестные мне (не git-add'нутые) файлы, так что я ничего коммитить не буду".
20.09.2020: посмотрел -- начав раскручивать с
hw4cx/pzframes. В принципе, вся требуемая информация есть в
$(ACT_GNTDFILES)
, а ПОЛНЫЙ список всего удаляемого -- в
$(FILES4distclean)
.
Не нравится мне, конечно, идея использовать инфраструктуру, предназначенную совсем для другого, для git'а. Но попробовать-то можно.
OK -- сделана PHONY-target "print-gitignore", который просто выполняет
echo (поштучно, чтобы построчно) всех имён из
$(FILES4distclean)
. Ну -- работает, но 1) в начало
команды пришлось добавить @
, чтобы сама команда не попадала на
stdout; 2) также и сам Make нужно запускать с ключом "-s
",
чтоб он не печатал на stdout же свои "Entering directory/Leaving directory";
в противном случае по "make print-gitignore >.gitignore
"
будет попадать мусор.
21.09.2020: чуток допеределываем, для юзабельности:
create-gitignore
.
RQD_GNTDFILES
(к *.d *.mkr).
Пара замечаний:
Ведь подготовка директории к упаковке в дистрибутив -- это именно "distclean". А я почему-то в этих целях применяю "maintainer-clean" (ещё с конца 1990-х, когда стал использовать авто-генерацию зависимостей? тогда в ucam оно было "mostlyclean"), "distclean" же по факту не используется вовсе.
Что с этим делать -- неясно. Впрочем, и НАДО ли что-то делать -- хбз.
В принципе, способы решения проблемы описаны в 4cx/src/doc/BUILD.ru.html, но чтобы туда залезть, саму проблему надо понять, а сделать это по непонятным ругательствам при сборке может далеко не каждый.
Поэтому и мысль -- а может, можно как-то приспособиться, чтобы НЕ использовать те фичи, которых этот у$бищный dash не умеет?
15.04.2021: пытаемся посмотреть/разобраться.
echo -e "ЧЕГО-ТО\nЕЩЁ-ЧЕГО-ТО" >ФАЙЛ
Ну-у-у, в принципе -- да, можно это позаменять на
(echo "ЧЕГО-ТО"; echo "ЕЩЁ-ЧЕГО-ТО") >ФАЙЛ
16.04.2023: а можно сделать как в GeneralRules.mk
при генерации входного потока для gcc -E
, с использованием
команды tr
--
"echo "ONE_LINEzANOTHER_LINEz..." | tr 'z' '\n'
".
echo -e
"
не используется.
Так что рыть дальше.
16.04.2021: пытаюсь рыть дальше. Для симуляции Debi[li]an поставил RPM'ку dash-0.5.10.2-1.el7.x86_64.rpm, скопировал дерево в /tmp и натравил на него "make SHELL=/bin/dash" -- бинго!!! Но легче от этого результата не стало, ибо:
#!/bin/sh
" и /bin/sh указывает на быдло-shell? Почему скрипт
обрабатывается именно bash'ем, а не тем, куда симлинк указывает?
Ответ, скорее всего, в том, что Make исполняет команды при помощи "${SHELL} -c КОМАНДА", и когда указанному shell'у попадается такой скрипт, то на заголовок "!#..." уже не смотрится, а просто делается исполнение скрипта как "своего".
Итого, резюме: нет, фиг -- горбатого могила исправит.
(Осознано при изготовлении gw/Makefile, где конструкция "if...elif...else" понадобилась -- там нужно проверить наличие И EPICS, И TANGO, ругаясь при отсутствии, а если есть оба -- то включить сборку в epics2tango/.)
tar
добавлены ключики
--owner=0 --group=0 --mode=og-w
-- чтобы отвязаться от конкретного юзера и его настроек.
-std=c++11
" --
как бы это обойти?» -- да элементарно:
*_CPPFLAGS
туда бы тоже включались).
13.06.2023: делаем, все работы в GeneralRules.mk:
ACT_CXX_CPPFLAGS
-- копированием определения
ACT_CPPFLAGS
и "раздваиванием" каждого компонента на
"$(nnn_CPPFLAGS) $(nnn_CXX_CPPFLAGS)
", аналогично тому, как
в определении ACT_CXXFLAGS
.
_CXX_O_LINE
заменено
$(ACT_CPPFLAGS)
на $(ACT_CXX_CPPFLAGS)
, ...
_CXX_DEP_LINE
тоже.
Ну и использование -- ключ "-std=c++11
" теперь указывается
не в LOCAL_CPPFLAGS
, а в LOCAL_CXX_CPPFLAGS
-- в
файлах frgn4cx/tango/cda/Makefile и
frgn4cx/gw/epics2smth/Makefile (во втором расширенные копии
определений из первого).
Вот во втором результат и получен -- теперь всё красиво.
...столько возни ради вроде бы мелочи, и из-за идиотизма клятого Tango...
(зачем -- сейчас сходу не вспомню, да и неважно).#define TRUE_DEFINE_DRIVER_H_FILE "/dev/null" #include "cxsd_driver.h"
И проблема возникла тогда, когда ЕманоФедя на какой-то студенческой машине попробовал собрать дерево, то Make ругался, что "File `/dev/null' has modification time NNNN s in the future" и циклил до посинения.
Причина-то в системе -- всё в /dev/ создаётся при загрузке и имеет тогдашние времена, но из-за каких-то заскоков с переставлением таймзоны при загрузке (опять на 7 часов из-за GMT+7) оказывалось, что машина загрузилась "в будущем" и все времена в /dev/ оттуда же.
07.03.2025: ну да, косяк-то в системе, но как бы всё же от этой проблемы избавиться (и заодно сценарий для подобных случаев на будущее)?
Напрашивается просто создавать пустой файл искусственно и
#include
'ить именно его.
Итак:
#define TRUE_DEFINE_DRIVER_H_FILE "empty_DEFINE_DRIVER.h" #include "cxsd_driver.h"
test_rem_rd_rw_drv.o test_rem_rd_rw_drv.d: SPECIFIC_CPPFLAGS+=-I. test_rem_rd_rw_drv.o test_rem_rd_rw_drv.d: empty_DEFINE_DRIVER.h empty_DEFINE_DRIVER.h: touch $@ LOCAL_GNTD_FILES+= empty_DEFINE_DRIVER.h
Да, ПРИШЛОСЬ добавить "-I.
". Т.к., как сказано в info по
cpp в разделе "Include Syntax" про вариант '#include "FILE"
',
"It searches for a file
named FILE first in the directory containing the current file,", а
данном случае "текущим" является cxsd_driver.h -- следовательно, и
директория "../../include".
Как вариант: можно было бы указать в определении
TRUE_DEFINE_DRIVER_H_FILE "./empty_DEFINE_DRIVER.h"
-- но это было бы ещё более криво.
После этого всё собралось (а работает ли -- не проверялось, но причин не работать не видно).
...кстати,
"grep touch work/**/*{mk,akefile}
"
нашло, что подобная же генерация пустых файлов использовалась ещё
давным-давно в v2hw/drivers/can/ -- конкретно в
src/ShadowRules.mk и socketcan/Makefile -- тоже для
создания пустых файлов.
Итого: да, громоздковато (по сравнению с тривиальной ссылкой на /dev/null), но зато независимость от кривых реализаций ОС. И в будущем при подобных случаях лучше именно так и поступать.
Для стройности структуры ставим его первым, ПЕРЕД разделом "Кросс-компиляция".
21.02.2023: предварительное -- сбор информации.
uname
показывает архитектуру/процессор: "uname -p
" говорит
"e2k".
...тогда ключ "-p
" нашёл чтением man'а, т.к. на память не
помнил.
-m
" для всех
ОС, кроме SOLARIS (но НЕ SUNOS!) и IRIX -- для тех "-p
".
и
uname -p
is simply not portable. Certainly not across different Unixes, and not even across different Linuxes.On Linux specifically, GNU coreutils's
uname
utility itself doesn't know of a way to determine the processor type. It would outputunknown
. However some distributions have patched that to do something different. Fedora, for instance, have patched it to copy the machine hardware name (i.e.uname -m
).
The
uname
from GNU coreutils gets the output ofuname -m
from theuname
syscall. Foruname -p
it does various system-dependent things. See here.And its documentation says that
-p
is non-portable (even across GNU/Linux distributions).So it's probably best to use this as rarely as possible.
Видимо, в конце 1990-х (1998?) при разработке Sysdepflags.sh я провёл исследования, давшие аналогичный результат.
uname -m
" скажет "e2k".
CPU_MCF5200
для
dlopen()
и vsnprintf()
и CPU_X86
для
ассемблерных вставок ndbp_image_elemplugin.c), то
CPU=E2K
на основании выдачи uname
.
22.02.2023: в Sysdepflags.sh добавил в конец альтернативы определения процессора вариант
elif (echo $PLATFORM | grep -i "e2k" >/dev/null) then CPU="E2K"
01.03.2023: напросился на Yukari (Elbrus-8C) и проверил там -- собралось ВСЁ, в составе 4cx/, hw4cx/ и sw4cx/, включая SocketCAN и даже GUI на Motif (но, естественно, без кросс-собираемого). Заняло это 9:27 (487.57s user 89.84s system), при частоте 1200MHz.
Проверка работы -- сервер с "-s
" и конфигом от canhw:19 там
запустился, натравленный на него скрин ringrf показывает осмысленно.
ЗЫ: есть ещё мысль попробовать там же собрать EPICS и Tango.
./configure
" выдало
checking build system type... config/config.guess: unable to guess system type ---много всякой диагностики--- config.guess timestamp = 2008-01-23 ---много всякой диагностики--- configure: error: cannot guess build type; you must specify one
12.03.2023: нашёл, откуда: это в
config/config.guess есть строка
timestamp='2008-01-23'
.
./configure --build=e2k-unknown-linux-gnu
"
и получил уже
(имеющийся какой-то другой "orbit-2.0" её не устроил). До проверки на ZMQ/0mq дело так и не дошло, хотя как раз это-то вроде присутствует.configure: error: Package requirements (omniORB4 >= 4.1.2) were not met: No package 'omniORB4' found
05.03.2023: дальнейшие обсуждения сборки EPICS и Tango пусть будут в их собственных пунктах ниже.
P.S. И да -- это пусть всё будет ЗДЕСЬ, в одном общем разделе по Эльбрусу (а не в их собственных подразделах), чтобы вся информация была собрана в одном месте.
17.03.2023: пробуем:
compile/base-3.15.6/include/cadef.h is absent, so skipping build of EPICS-related stuff
-- что понятно, т.к. захардкожена версия 3.15.6, а собрана 3.15.9.
И это даже хорошо, т.к. если б нашлось -- то оно б попыталось взять библиотеки из захаржкоженного же $(EPICS_BASE_DIR)/lib/linux-x86_64 с гарантией облома.
-- ну тоже понятно: этот клятый omniORB был собран локально, а оно его пытается искать в стандартных системных директориях.lcc: "/export/home/bolkhov/compile/tango-9.2.5a/lib/cpp/server/tango_config.h", line 327: catastrophic error #1696: cannot open source file "omniORB4/acconfig.h" #include <omniORB4/acconfig.h> ^ 1 catastrophic error detected in the compilation of "cda_d_tango.cpp". Compilation terminated.
20.03.2023: возвращаемся к этой задаче.
Логически была выбрана ARCH_
-- она как раз для такого и
была предусмотрена 23-01-2012.
time g++ -o cda_d_tango.o ...
).)
make ARCH_INCLUDES=-I${HOME}/usr/include ARCH_LDFLAGS=-L${HOME}/usr/lib
ARCH_LDFLAGS
вместо вроде бы надлежащего
ARCH_LIBS
потому, что в правиле "%.so: %.o
" кусок
"$(ACT_LIBS) -lc
" почему-то закомменчен.
(С незапамятных времён: в w20090408.tar.gz уже так; при том, что
в тамошнем cx/src/Rules.mk в правиле ".o.so:
" кусок
"-lc
" присутствует (хотя ACT_LIBS
не упоминается
вовсе)).
make
'у из
командной строки.
И уж тут-то всё тривиально -- просто подменяем пару определений из FrgnRules.mk:
make EPICS_BASE_DIR=${HOME}/compile/base-3.15.9 EPICS_OS_LIB_DIR='$(EPICS_BASE_DIR)/lib/linux-e2k'
05.03.2023: поизучал содержимое файлов и в конце
концов создал в configure/os файл
CONFIG.linux-e2k.linux-e2k просто копией из
CONFIG.linux-x86.linux-x86. Причина -- потому, что
CONFIG.linux-x86_64.linux-x86_64 просто include
'ит
его, а сам он содержит лишь
-- т.е.,# Include common gnu compiler definitions include $(CONFIG)/CONFIG.gnuCommon STATIC_LDFLAGS_YES= -Wl,-Bstatic STATIC_LDFLAGS_NO= STATIC_LDLIBS_YES= -Wl,-Bdynamic STATIC_LDLIBS_NO= SHRLIB_LDFLAGS += -Wl,-h$@ LOADABLE_SHRLIB_LDFLAGS += -Wl,-h$@
include
'ит общие для "GNU" определения и делает
несколько небольших определений ключиков, которые вроде бы совершенно
стандартны, так что E2K должны поддерживаться (тем более, что
"ld" там -- "GNU ld (GNU Binutils) 2.36.1-26.012").
Попробовал в таком варианте запустить make вхолостую:
$ make -n ./src/tools/EpicsHostArch.pl: Architecture 'e2k-linux' not recognized configure/CONFIG:58: configure/os/CONFIG..Common: No such file or directory make: *** No rule to make target 'configure/os/CONFIG..Common'. Stop.
OK, чуть более продвинуто -- памятуя уже имевшийся 23-06-2019 опыт встречи с EpicsHostArch.pl
$ make -n EPICS_HOST_ARCH=e2k-linux configure/CONFIG:58: configure/os/CONFIG.e2k-linux.Common: No such file or directory make: *** No rule to make target 'configure/os/CONFIG.e2k-linux.Common'. Stop. $ make -n EPICS_HOST_ARCH=linux-e2k configure/CONFIG:58: configure/os/CONFIG.linux-e2k.Common: No such file or directory make: *** No rule to make target 'configure/os/CONFIG.linux-e2k.Common'. Stop.
06.03.2023: разбираемся дальше.
include'ит CONFIG.UnixCommon.Common и более не содержит ничего.
include'ит CONFIG.Common.linuxCommon и добавляет
"-m64
" к ...CFLAGS и ...LDFLAGS.
Пользуемся полученным знанием:
$ make -n EPICS_HOST_ARCH=linux-e2k make -C ./configure install make[1]: Entering directory '/export/home/bolkhov/compile/base-3.15.9/configure' perl -CSD ../src/tools/makeMakefile.pl O.linux-e2k ../.. mkdir O.Common make -C O.linux-e2k -f ../Makefile TOP=../.. \ T_A=linux-e2k install make[2]: Entering directory '/export/home/bolkhov/compile/base-3.15.9/configure' make[2]: *** O.linux-e2k: No such file or directory. Stop. make[2]: Leaving directory '/export/home/bolkhov/compile/base-3.15.9/configure' make[1]: *** [../configure/RULES_ARCHS:58: install.linux-e2k] Error 2 make[1]: Leaving directory '/export/home/bolkhov/compile/base-3.15.9/configure' make: *** [configure/RULES_DIRS:85: configure.install] Error 2
-n
"
директория не создаётся и входить некуда.
make EPICS_HOST_ARCH=linux-e2k
", безо всяких
"-n
".
Итак, "рецепт" --
make EPICS_HOST_ARCH=linux-e2k
" --
т.к. без указания EHA оно обламывается.
11.03.2023: кстати, время сборки base-3.15.9/ -- 21m12.808s; 4cx/src/ -- 5m10.570s (это БЕЗ "exports" и hw4cx/sw4cx; с ними -- 9m27.576s; это всё без v2cx и кросс-сборок)..
22.03.2023: заодно проверил и base-3.15.6/ -- 20m43.596s.
11.03.2023: итак:
в то время как должно бытьperl -CSD /export/home/bolkhov/compile/base-7.0.2.2/bin/linux-e2k/bldEnvData.pl -t linux-e2k \ -c -s Linux /export/home/bolkhov/compile/base-7.0.2.2/configure
perl -CSD /export/home/bolkhov/compile/base-7.0.2.2/bin/linux-e2k/bldEnvData.pl -t linux-e2k \ -c gcc -s Linux /export/home/bolkhov/compile/base-7.0.2.2/configure
-c $(CMPLR_CLASS) -s $(OS_CLASS)
"
-- т.е., пустой оказывается $(CMPLR_CLASS)
.
CMPLR_CLASS = gcc
".
...или он что -- НЕ включается в сборку на e2k?
Откуда возникают вопросы:
CMPLR_CLASS=gcc
в командной строке.
иD_GNU_SOURCE -D_DEFAULT_SOURCE -D_X86_64_ -DUNIX -Dlinux .........
-- их отсутствие почему-то не вызывает останова! -- и потом оно пытается использовать несуществующий файл antelope, но тут уж make отваливает по отсутствию зависимости.o antelope -L/export/home/bolkhov/compile/base-7.0.2.2/lib/linux-e2k .........
15.03.2023: полез разбираться поплотнее:
make EPICS_HOST_ARCH=linux-e2k -p | less
" -- чтоб
посмотреть "базу".
иD_GNU_SOURCE -D_DEFAULT_SOURCE -D_X86_64_ -DUNIX -Dlinux ......... make[1]: D_GNU_SOURCE: Command not found make[1]: [--путь-убран--]/base-7.0.2.2/configure/RULES_BUILD:243: epicsTempFile.o] Error 127 (ignored)
o antelope -L/export/home/bolkhov/compile/base-7.0.2.2/lib/linux-e2k ......... make[1]: o: Command not found make[1]: [--путь-убран--]/base-7.0.2.2/configure/RULES_BUILD:207: antelope] Error 127 (ignored)
и%$(OBJ): %.cpp | $(COMMON_INC) $(INSTALL_INC) @$(RM) $@ 243: $(COMPILE.cpp) -c $<
$(TESTPRODNAME) $(PRODNAME): %$(EXE): | $(INSTALL_LIB) @$(RM) $@ 207: $(LINK.cpp) $(MT_EXE_COMMAND)
COMPILE.cpp
и LINK.cpp
определены как
и# makefile (from '--путь-убран--/base-7.0.2.2/configure/CONFIG_COMMON', line 330) COMPILE.cpp = $(CCC) $(CPPFLAGS) $(CXXFLAGS) $(INCLUDES)
# makefile (from '--путь-убран--/base-7.0.2.2/configure/os/CONFIG.Common.UnixCommon', line 95) LINK.cpp = $(CCC) -o $@ $(STATIC_LDFLAGS) $(PRODDIR_LDFLAGS) $(LDFLAGS) $(PROD_LDFLAGS) $(PROD_LD_OBJS) $(PROD_LD_RESS) $(PROD_LDLIBS)
$(CCC)
".
$(CCC)
в базе НИГДЕ НЕ определена.
$(CCC)
это неопределённое
также съедает и СЛЕДУЮЩИЙ СИМВОЛ из строки?
Так что
"$(CCC) -o ...
" дает "o ...
"?
И аналогично от "-DGNU_SOURCE
" остаётся
"DGNU_SOURCE
".
perl -CSD .../bldEnvData.pl
"?
$(CCC)
не определена?
Пытаемся разобраться:
(Раз в правиле линковки есть "|
" (в "info make" искать по
"`\|'")).
grep -w CCC
" показывает, что
НЕзакомментированное определение есть в configure/CONFIG.gnuCommon:
CCC = $(GNU_BIN)/$(CMPLR_PREFIX)g++$(CMPLR_SUFFIX)
...да вот только он, судя по "make -p --debug=vj
", в списке
включаемых файлов ОТСУТСТВУЕТ! Почему?
Чуть позже: сообразил насчёт пп. 1 и 2 -- это не разные, это ОДНА И ТА ЖЕ ПРОБЛЕМА!!!
'-'
, то
make считает это за префикс "игнорировать ошибку команды". ...Естественно,
сам префикс из команды удаляется.
-o ...
" и "-DGNU_SOURCE ...
" превращаются
в "o ...
" и "DGNU_SOURCE ...
" соответственно,
Таким образом:
$(CCC)
.
...хотя и является причиной ошибки -- там ведь тоже дело в определении из того же configure/CONFIG.gnuCommon и его невключением.
Явно надо разбираться со списком include'имых файлов --
make --debug=v
" можно.
Да, сравнивал -- и в конце концов обнаружил, что косяк МОЙ: конкретно в base-7.* вместо файла CONFIG.linux-e2k.linux-e2k я умудрился сделать linux-e2k.linux-e2k -- компонент "CONFIG." пропустил. А эта чёртова "система сборки EPICS" пропустила такую идиотскую ошибку и никак о ней не сообщила...
После исправления -- сборка пошла. Собралось: 7.0.2.2 за 76m24.655s; 7.0.7 за 78m52.323s
Т.е., я потратил НЕСКОЛЬКО ДНЕЙ (с дневным перерывом на сборку Tango) из-за своей дурацкой ошибки. ...но да, "качество" системы сборки тоже поспособствовало, несомненно.
Итого: собрались и 3.15.9, и 7.0.2.2, и 7.0.7. Теперь надо будет как-то проверить работу.
-m128
вместо
-m64
) и сказав
"make EPICS_HOST_ARCH=linux-e2k_128
"
результат -- фиг:
lcc: "../../../src/libCom/osi/os/posix/osdElfFindAddr.c", line 560: error #136: struct "<unnamed>" has no field "dli_fbase" if ( ! (nes = elfRead(inf.dli_fname, (uintptr_t)inf.dli_fbase)) ) { ^ 1 error detected in the compilation of "../../../src/libCom/osi/os/Linux/osdFindAddr.c". make[3]: *** [../../../configure/RULES_BUILD:235: osdFindAddr.o] Error 1 make[3]: Leaving directory '/export/home/bolkhov/ptr128/base-3.15.9/src/libCom/O.linux-e2k_128'
25.03.2023: продолжаем.
inf
(хбз, почему
"<unnamed>") имеет тип Dl_info
, определяется в
/usr/include/dlfcn.h, и если в обычном Linux она имеет простой вид
(bold -- мой)
то на Эльбрусе --/* Structure containing information about object searched using `dladdr'. */ typedef struct { const char *dli_fname; /* File name of defining object. */ void *dli_fbase; /* Load address of that object. */ const char *dli_sname; /* Name of nearest symbol. */ void *dli_saddr; /* Exact value of nearest symbol. */ } Dl_info;
typedef struct { const char *dli_fname; /* File name of defining object. */ #if ! defined __ptr128__ void *dli_fbase; /* Load address of that object. */ #else /* defined __ptr128__ */ unsigned int dli_tbase; /* Text load address of that object. */ unsigned int dli_dbase; /* Data load address of that object. */ #endif /* defined __ptr128__ */ const char *dli_sname; /* Name of nearest symbol. */ #if !defined __ptr128__ void *dli_saddr; /* Exact value of nearest symbol. */ #else /* defined __ptr128__ */ unsigned int dli_saddr; #endif /* defined __ptr128__ */ } Dl_info;
epicsFindAddr()
используется только где-то в
libCom/osi/epicsStackTrace.c, то решено забить на
"100%-корректность" и в текст
osdElfFindAddr.c::epicsFindAddr()
просто вкорячена
"замена" на dli_tbase
при наличии __ptr128__
.
Прокатило, компиляция пошла дальше.
Installing created executable ../../../../../bin/linux-e2k_128/genApps [--путь-убран--]/base-3.15.9.BACKUP/bin/linux-e2k_128/genApps ../O.Common/gddApps.h /usr/bin/gcc [КЛЮЧИ-ПОСКИПАНЫ] -MM -MF aitGen.d ../aitGen.c /usr/bin/g++ [КЛЮЧИ-ПОСКИПАНЫ] -MM -MF genApps.d ../genApps.cc [--путь-убран--]/base-3.15.9.BACKUP/bin/linux-e2k_128/genApps ../O.Common/gddApps.h make[3]: *** No rule to make target '../O.Common/gddApps.h', needed by '../../../../../include/gddApps.h'. Stop. make[3]: Leaving directory '--путь-убран--/base-3.15.9.BACKUP/src/ca/legacy/gdd/O.linux-e2k_128' make[2]: *** [../../../../configure/RULES_ARCHS:58: install.linux-e2k_128] Error 2
sed
'ов к каждому логу отличия были
устранены и логи приведены к годному для diff
'а виду) было
понято, что проблема в почему-то не сработавшей командной строке
-- вроде запускается, но файла ../O.Common/gddApps.h почему-то не генерит.[--путь-убран--]/base-3.15.9.BACKUP/bin/linux-e2k_128/genApps ../O.Common/gddApps.h
Почему make при этом не вылетает по ошибке -- загадка (ведь что-то такое уже было).
Thread 1 "genApps" received signal SIGSEGV, Segmentation fault exc_array_bounds at 0x5017e0e8 ALS2 | ALS5 0x000000005017e0e8 in gddBounds1D::operator new(unsigned long) ()
-g
" при сборке отсутствует.
$(HOST_OPT)
-- указание
"HOST_OPT=NO
" добавляет "-g
" и убирает
"-O3
".
Найдено посредством
grep -rw -- '-g' compile/base-3.15.9
-- оно подсказало CONFIG.linux-x86-debug.linux-x86-debug где
нашёлся фрагментик
(ну и потом уже по# Removes -O optimization and adds -g compile option HOST_OPT=NO
HOST_OPT
grep'илось).
...правда, механизм его работы так и остался неясен: НИГДЕ, кроме 6 штук
с "win32" в имени, ссылок на собственно ЗНАЧЕНИЕ --
$(HOST_OPT)
-- НЕТУ.
28.04.2023: кажется, нашёл:
BUILD_CLASS = HOST
# Warnings flags WARN_CPPFLAGS = $(WARN_CPPFLAGS_$($(BUILD_CLASS)_WARN)) WARN_CFLAGS = $(WARN_CFLAGS_$($(BUILD_CLASS)_WARN)) WARN_CXXFLAGS = $(WARN_CXXFLAGS_$($(BUILD_CLASS)_WARN)) # Optimization flags OPT_CPPFLAGS = $(OPT_CPPFLAGS_$($(BUILD_CLASS)_OPT)) OPT_CFLAGS = $(OPT_CFLAGS_$($(BUILD_CLASS)_OPT)) OPT_CXXFLAGS = $(OPT_CXXFLAGS_$($(BUILD_CLASS)_OPT))
OPT_CFLAGS_YES = -O3 OPT_CFLAGS_NO = -g
Мда, совсем заигрались ребята в "Computed Variable Names"...
HOST_OPT=NO
" -- сработало, информации стало сильно
больше:
Thread 1 "genApps" received signal SIGSEGV, Segmentation fault exc_array_bounds at 0x50178800 ALS2 | ALS5 0x0000000050178800 in gddBounds1D::newdel_setNext (this=0x517d3058, n=0x0) at ../gddUtils.h:115 115 gdd_NEWDEL_FUNC(b[0]) // required for using generic new and remove (gdb) bt #0 0x0000000050178800 in gddBounds1D::newdel_setNext (this=0x517d3058, n=0x0) at ../gddUtils.h:115 #1 0x00000000501754d8 in gddBounds1D::operator new (size=8) at ../gddUtils.cc:22 #2 0x000000005010d410 in gdd::init (this=0x517ccf60, app=1, prim=aitEnumContainer, dimen=1) at ../gdd.cc:116 #3 0x00000000500f7418 in gdd::gdd (this=0x517ccf60, app=1, prim=aitEnumContainer, dimen=1) at ../gdd.cc:84 #4 0x0000000050171610 in gddContainer::gddContainer (this=0x517ccf60, app=1) at ../gddContainer.cc:34 #5 0x00000000501431d8 in gddApplicationTypeTable::GenerateTypes (this=0x50191240 <gddApplicationTypeTable::app_table>) at ../gddAppDefs.cc:79 #6 0x00000000501350e8 in gddApplicationTypeTable::gddApplicationTypeTable (this=0x50191240 <gddApplicationTypeTable::app_table>, tot=512) at ../gddAppTable.cc:58 #7 0x000000005013fe10 in __sti___14_gddAppTable_cc_9dccbb8a () at ../gddAppTable.cc:31 #8 0x000000005017b220 in __libc_csu_init () #9 0x0000000050f7a5c8 in __libc_start_main () from /lib128/libc.so.6 #10 0x00000000500e9138 in ?? () #11 0x00000000500ec330 in ?? () #12 0x00000000500f6550 in _start () (gdb) print *this $1 = {b = {{start = 152, count = 160}}, static newdel_freelist = 0x0, static pNewdel_lock = 0x517d2ed0, static once = 0x503900c0 <threadOnceComplete.1>}
А этот "gdd_NEWDEL_FUNC()
" -- это МАКРОС, определяющий толпу
методов, включая скопытившийся newdel_setNext()
. Мраки...
diff -Naurb
-- и осознал, что в EPICS7 этого просто
НЕТ: оттуда весь libCAS выселен в отдельный "модуль".
26.03.2023: OK, пробуем 7.0.7, с твиком osdElfFindAddr.c и configure/os/-файлами.
-- ну это косяк явно уже самой системы, в которой нету 128-битной версии этого "libncursesw".Converting data from ../bpt/bptTypeJdegC.data "../../../../../bin/linux-e2k_128/makeBpt" ../bpt/bptTypeJdegC.data bptTypeJdegC.dbd ../../../../../bin/linux-e2k_128/makeBpt: error while loading shared libraries: /usr/lib/libncursesw.so.6: ELF file OS ABI invalid make[5]: *** [../../../../../configure/RULES.Db:371: ../O.Common/bptTypeJdegC.dbd] Error 127 make[5]: Leaving directory '/export/home/bolkhov/ptr128/base-7.0.7/modules/database/src/ioc/O.linux-e2k_128'
./configure --build=e2k-unknown-linux-gnu --prefix=${HOME}/usr128 CPPFLAGS=-m128 LDFLAGS=-m128
./autogen.sh
", он сгенерит
(11 секунд!!!) всё для configure'а, а потом уже
обычным образом.
major()
. А
это явно что-то касательно ядра.
--enable-widec
" (чуть не поседел, пока его
нашёл -- спасибо
"ncurses vs ncursesw - Issue #526 - openwrt/packages" на GitHub).
./configure --build=e2k-unknown-linux-gnu --prefix=${HOME}/usr128 \ CPPFLAGS="-m128 -I${HOME}/usr128/include" LDFLAGS="-m128 -L${HOME}/usr128/lib" \ --enable-widec
ncurses_options
из .spec-файла
-- потому, что по умолчанию оно НЕ собирало shared-библиотеки.
-Wl,-rpath,${HOME}/usr128/lib -Wl,-rpath-link,${HOME}/usr128/lib
"
(посредством OP_SYS_LDFLAGS="-m128 -Wl..."
));
загадка "сфига ли вообще кто-то хочет этот ncursesw?!" стала ещё страньше,
когда в какой-то момент обнаружилось наличие
/usr/lib128/libncursesw.so.6.1 -- как так?!
27.03.2023: кхм...
##$ ldd /usr/lib128/libreadline.so.6 ##/usr/bin/bash: error while loading shared libraries: /usr/lib128/libncursesw.so.6: ELF file OS ABI invalid $ ldd /usr/lib128/libreadline.so.6 /usr/lib128/libreadline.so.6: error while loading shared libraries: /usr/lib/libncursesw.so.6: ELF file OS ABI invalid
У него внутри (strings -a
) прямо присутствует строка "/usr/lib".
29.03.2023: хоцца всё-таки довести до конца, поэтому продолжаем.
-rpath
"
при сборке .so'шки. Вероятно, под Эльбрус собиралось без этого
патча, вот и результат.
Как показал поиск под CentOS-7.3, libtinfo -- часть ncurses.
30.03.2023: продолжаем.
find /lib* /usr/lib* -iname '*tinfo*'
"
-- там вообще нигде ни в каком виде нет libtinfo.so*; как так?!
Отключено при сборке?
-- так нафига оно пытается сделать "checking for tgetent... no checking for tgetent in -ltermcap... no checking for tgetent in -ltinfo... no checking for tgetent in -lcurses... yes
-ltinfo
"?!
+ SHLIB_LIBS='-ltinfo'
применяемая к файлу support/shobj-conf -- видимо, RedHat'овские
игрища...
-p0
вместо обычного -p1
);
SHLIBS='-ltinfo'
" в support/shobj-conf;
03.04.2023: неа, сделать так:
в support/shobj-conf заменить
"SHLIBS='-ltinfo'
" на "SHLIBS='-lncursesw'
";
./configure --build=e2k-unknown-linux-gnu --prefix=${HOME}/usr128 \ CPPFLAGS="-m128 -I${HOME}/usr128/include" LDFLAGS="-m128 -L${HOME}/usr128/lib"
make; make install
Но легче от этого не стало -- срубается на линковке по пачке "undefined reference"...
Что делать? Напрашиваются 2 варианта:
(Хотя ещё отдельный вопрос: а не подхватывается ли всё же что-то системное, что несовместимо по режимам сборки?)
Начинаем со второго.
COMMANDLINE_LIBRARY=EPICS
; как
показало
grep -i COMMANDLINE_LIBRARY compile/base-3.15.9/configure/os/*(.) | grep READLINE
судя по содержимому configure/os/CONFIG_SITE.Common.linux-x86_64 --
умолчание устанавливается хитровато, по найденности в системе header'а
$(GNU_DIR)/include/readline/readline.h; умники фиговы... (хотя я
ведь так же с EPICS и Tango в frgn4cx/...).
Сделал, запустил сборку -- да, рецепт был верным!!!
Но потом срубилось уже по ошибке исполнения в защищённом режиме, причём
-- сюрприз! -- на том же самом makeBpt
:
Converting data from ../bpt/bptTypeJdegC.data "../../../../../bin/linux-e2k_128/makeBpt" ../bpt/bptTypeJdegC.data bptTypeJdegC.dbd [pid #16579] makeBpt: protected mode runtime error System call #248/futex: unexpected tag (0x5) in arg #5. Argument #5 is missed or uninitialized. [pid #16579] makeBpt: protected mode runtime error System call #248/futex: unexpected tag (0x5) in arg #6. Argument #6 is missed or uninitialized. [pid #16579] Execution of makeBpt terminated. Error code: 22 Аварийный останов
И GDB'шный backtrace там длиннющий, причём изнутри
libCom.so.3.22.0, откуда вызывается
pthread_mutex_init()
, отладочной информации почему-то нет:
04.04.2023: есть, просто часть backtrace-'а "вне кода".
(gdb) bt #0 0x00000000516afb30 in pthread_mutex_init () from /lib128/libpthread.so.0 #1 0x000000005010d138 in ?? () from /export/home/bolkhov/ptr128/base-7.0.7/lib/linux-e2k_128/libCom.so.3.22.0 #2 0x000000005013b0b0 in ?? () from /export/home/bolkhov/ptr128/base-7.0.7/lib/linux-e2k_128/libCom.so.3.22.0 #3 0x000000005027b2c0 in globalAttrInit () at ../osi/os/posix/osdMutex.c:78 #4 0x00000000516cd3f0 in __pthread_once_slow () from /lib128/libpthread.so.0 #5 0x00000000516cd750 in pthread_once () from /lib128/libpthread.so.0 #6 0x0000000050156710 in ?? () from /export/home/bolkhov/ptr128/base-7.0.7/lib/linux-e2k_128/libCom.so.3.22.0 #7 0x000000005027b6b0 in osdPosixMutexInit (m=0x5034f880 <onceLock>, mutextype=0) at ../osi/os/posix/osdMutex.c:96 . . .
Т.е. -- опять не может собраться из-за бага, ради обнаружения которых и
затеяна сборка под -m128
, Ура, да? :D
31.03.2023: насчёт проверки "а не из-за кривых ли
ключей -I
командной строки проблема с readline"?
make EPICS_HOST_ARCH=linux-e2k_128 HOST_OPT=NO \ OP_SYS_LDFLAGS="-m128 -Wl,-rpath,${HOME}/usr128/lib -Wl,-rpath-link,${HOME}/usr128/lib" \ OP_SYS_CFLAGS="-m128 -I ${HOME}/usr128/include" \ ARCH_DEP_CPPFLAGS="-D_E2K_128_ -I${HOME}/usr/include"
Что вызвало подозрения, т.к.:
-I
", а
"-isystem
" (упомянуто в bigfile-0001.html за
28-06-2004 и 29-06-2004).
-I${HOME}/usr128/include
" вместо
"-I${HOME}/usr/include
" не помогло.
-isystem
" вместо "-I
" -- тоже нет, ни
только в ARCH_DEP_CPPFLAGS, ни вместе с OP_SYS_CFLAGS.
make EPICS_HOST_ARCH=linux-e2k_128 HOST_OPT=NO \ OP_SYS_LDFLAGS="-m128 -Wl,-rpath,${HOME}/usr128/lib -Wl,-rpath-link,${HOME}/usr128/lib" \ OP_SYS_CFLAGS="-m128 -isystem ${HOME}/usr128/include" \ ARCH_DEP_CPPFLAGS="-D_E2K_128_ -isystem ${HOME}/usr128/include" \ COMMANDLINE_LIBRARY=EPICS
01.04.2023: пересобрал всё, требующееся для readline (gpm, ncurses, readline) "максимально аккуратно" в отдельной директории 2nd-128/, с инсталляцией в отдельную же usr128-2/ --
COMMANDLINE_LIBRARY=EPICS
" всё равно
обламывается по куче "undefined reference" из
usr128-2/lib/libreadline.so.6 -- вот как так, а?!
-lncursesw
" -- и оно собралось!!! Т.е.,
проблема лишь в том, что этот дебилизм почему-то НЕ указывал "ncursesw"
явно, а по зависимостям она почему-то -- в отличие от 64-битной сборки! --
не бралась.
strings -a
": в
штатном строка "libncursesw.so.6" есть, а в самособранном -- нету;
в CentOS-7.3'шном /usr/lib64/libreadline.so -- тоже нету; специфика
сборки?
За-дол-бало!!!
01.04.2023: некоторые мысли, пришедшие в голову чуть позже на основе полученной ранее информации:
-lreadline -lncursesw
", вспомнилось
виденное grep'еньем по configure/os/ --
у параметра COMMANDLINE_LIBRARY
есть ведь вариант
"READLINE_NCURSES", дающий именно нужный набор ключей!!! (В тех редких местах,
где используется)
а не в том ли дело, что из CentOS'ной
спецификации было убрано "-ltinfo
"? Ведь надо было не убирать
её, а заменить на "-lncursesw
"!
Если эти соображения верны, то заработать должно с ЛЮБЫМ из этих двух вариантов: хоть с "правильно" собранным libreadline, хоть с указанием
COMMANDLINE_LIBRARY=READLINE_NCURSESW LDLIBS_READLINE_NCURSESW="-lreadline -lncursesw"
(да-да, вариант "LDLIBS_READLINE_NCURSESW
" есть штатно в
CONFIG.Common.cygwin-x86)
03.04.2023: проверяем: да, катят оба варианта!
COMMANDLINE_LIBRARY
-- да, спокойно доходит до "makeBpt: protected mode runtime error".
SHLIBS='-lncursesw'
" в
support/shobj-conf: сработало! Тоже спокойно дошло до косяка
makeBpt.
(Правда, по "ldd libreadline.so.6.2
" вылазит ошибка
"libncursesw.so.5 => not found",
но это из-за отсутствия "-rpath", что нестрашно -- в EPICS он делается.)
Остановимся, пожалуй, на 2-м варианте -- поскольку он максимально похож на обычную сборку.
04.04.2023: возвращаемся к разбору собственно облома makeBpt.
globalAttrInit()
,
конкретно строчка
status = pthread_mutex_init(&temp, &globalAttrRecursive);
(и да -- файл существенно отличается даже между 7.0.7 и 7.0.2.2, не говоря
уж о 3.15.*, где потроха категорически другие.)
Добавил зануление посредством memset(,0,)
и
temp
, и globalAttrRecursive
-- не помогло.
#include <stdio.h> #include <pthread.h> int main(void) { int err; pthread_mutex_t mt; pthread_mutexattr_t ma; printf("sizeof(mt)=%zd sizeof(ma)=%zd\n", sizeof(mt), sizeof(ma)); err = pthread_mutexattr_init (&ma); printf("pthread_mutexattr_init()=%d\n", err); err = pthread_mutexattr_settype (&ma, PTHREAD_MUTEX_RECURSIVE); printf("pthread_mutexattr_settype(PTHREAD_MUTEX_RECURSIVE)=%d\n", err); err = pthread_mutexattr_setprotocol(&ma, PTHREAD_PRIO_INHERIT); printf("pthread_mutexattr_setprotocol(PTHREAD_PRIO_INHERIT)=%d\n", err); err = pthread_mutex_init(&mt, &ma); printf("pthread_mutex_init()=%d\n", err); return 0; }
PTHREAD_PRIO_INHERIT
: без неё -- всё OK, с ней -- падает,
причём только в 128-битном режиме (а и в 64-битном, и на x86_64 -- проходит
без ошибок, все результаты =0).
Вот это уже смахивает на косяк в 128-битной libpthread; но надо бы поразбираться, а то я слабо представляю смысл всех этих действий.
06.04.2023: раз косяк может быть в системе, то пробуем просто обойти проблему:
PTHREAD_PRIO_INHERIT
, добавив в
#if defined _POSIX_THREAD_PRIO_INHERIT
, которым оно огорожено, "&& 0
".
-- возможно, правильнее бы выставить#if defined(DONT_USE_POSIX_THREAD_PRIORITY_SCHEDULING) #undef _POSIX_THREAD_PRIO_INHERIT #endif
DONT_USE_POSIX_THREAD_PRIORITY_SCHEDULING
)
15.04.2023: созрели наконец проверить реальную работоспособность (а то просто холостые запуски caget/camonitor ошибок не давали, но они делают крайне мало -- отправка UDP-SEARCH и печать "ничего не найдено").
Технология -- запускаем обычный cxsd с epics-frontend'ом, и к нему коннектимся:
LD_LIBRARY_PATH=${HOME}/compile/base-3.15.9/lib/linux-e2k \ ./sbin/cxsd -dsc configs/cxsd.conf -f configs/devlist-canhw-11.lst :11 -e 'load-frontend epics'
-- работает.~/4pult/bin/cdaclient -mDa ovch300_a40_0.adc0
-- работает../compile/base-3.15.9/bin/linux-e2k/camonitor ovch300_a40_0.adc0
-- тоже работает!./ptr128/base-3.15.9.BACKUP/bin/linux-e2k_128/camonitor ovch300_a40_0.adc0
-- и оно работает!./ptr128/base-7.0.7/bin/linux-e2k_128/camonitor ovch300_a40_0.adc0
Теперь надо бы какие-нибудь более разнообразные тесты провернуть -- с разными типами данных, включая сроки.
15.04.2023@вечер, после обеда-из-Кенгуру на бердской косе и прогулки там: сделан файл programs/server/devlist-test-alltypes.lst со всевозможными типами (по устройству на тип, так что обращаться в стиле "byte.0"), включая векторные.
-- ничего не падает../ptr128/base-7.0.7/bin/linux-e2k_128/camonitor \ {byte,short,int,quad,float,double,char,uchar,v_{byte,short,int,quad,float,double},text,uctext}.0
IsSuitableForExport()
, а
UCTEXT -- где-то в недрах libcas:
мда....filename="../../../../../../src/ca/legacy/pcas/generic/casStrmClient.cc" line number=1701 Bad data type best external dbr type fetch failed filename="../../../../../../src/ca/legacy/pcas/generic/casStrmClient.cc" line number=1943 Bad data type - Server unable to create a new PV
-S
" и из другой консоли пишем
-- тоже ничего не падает.cdaclient @b10:{byte,short,int,quad,float,double,char,uchar,v_{byte,short,int,quad,float,double},text,uctext}.0=65,66,67,68
23.04.2023: не, про "в скалярах нули" -- это было неверным
наблюдением, из-за свежезапущенного сервера, которому в скаляры ничего не
было записано: ведь запись векторов в скаляры не срабатывает, она
отсеивается в cxsd_hw.c::IsCompatible()
. Если же
сначала записать скаляры (а не только векторы) и потом запустить camonitor,
то показываются записанные значения.
16.04.2023@утро-душ: что дальше можно делать:
Посмотрел -- неа, фиг: обламывается прямо ПЕРЕД генерацией libgdd.a и libgdd.so.3.15.9, увы...
Задача выглядит несложной -- подсунуть другие параметры make'у, вроде
EPICS_BASE_DIR=${HOME}/compile/base-7.0.2.2
@после-обеда: да, сработало, собралось. Проверять надо, но пока особо незачем.
EPICS_BASE_DIR=${HOME}/ptr128/base-7.0.7 EPICS_OS_LIB_DIR='$(EPICS_BASE_DIR)/lib/linux-e2k_128'
@после-обеда: да, собралось; естественно, только epics/cda/; полная командная строка --
make -C compile/work/frgn4cx/epics/cda \ TOP_CPPFLAGS=-m128 TOP_LDFLAGS=-m128 \ EPICS_BASE_DIR=${HOME}/ptr128/base-7.0.7 EPICS_OS_LIB_DIR='$(EPICS_BASE_DIR)/lib/linux-e2k_128'
После чего (не забыв скопировать собранное в ${HOME}/compile/work/4cx/exports/lib/cda/plugins/) попробовал приконнектиться через него к (вчерашнему) серверу --
-- ну и тоже работает, во всех проверенных вариантах типов.PULT_ROOT=${HOME}/compile/work/4cx/exports LD_LIBRARY_PATH=ptr128/base-7.0.7/lib/linux-e2k_128 \ ~/compile/work/4cx/exports/bin/cdaclient -m @i10:epics::v_int.0
25.04.2023: сегодня аналогичный эффект был на тривиальном x86_64 -- skifinj, на котором отсутствовал g++: make вроде и выдал сообщение "/usr/bin/g++: No such file or directory", но попёр дальше и отвалил уже по "No rule to make target 'epicsTempFile.o', needed by 'antelope'". Что наталкивает на мысль, что проблема именно в EPICS'ной системе сборки -- видимо, там зачем-то игнорируются ошибки исполнения команд.
28.04.2023: ответ вроде как найден: 26.04.2023
написал в tech-talk сообщение
"EPICS 3.15.9 build system weird behaviour upon missing g++",
и в процессе обсуждения с откликнувшимся Andrew Johnson
(ответ раз,
ответ два)
стало ясно, что причина -- в том, что часть команд
(включая как раз genApps
) исполняются make'ом на "первом,
предварительном проходе", когда у него несколько ослабленные требования к
тому, что считать фатальными ошибками, а что игнорировать; и вот, похоже,
что отсутствие команды (как с /usr/bin/g++) или её вылет по сигналу
(как с genApps) считаются НЕфатальными. "Предварительный проход"
же выполняется потому, что требуется сгенерить некие
.c/.cpp/.h-файлы, содержимое которых затем
используется для генерации .d-файлов, -include
'имых
Makefile'ами.
@вечером: "разобрался" -- см.
copy_and_SIGSEGV.c и test_make_command_SIGSEGV.mk в
work/tests/: оказывается, обычно make вовсе НЕ удаляет target-файлы
по неудачному исполнению команды (к каковому относится и вылет по сигналу),
а только если явно указать где-нибудь ".DELETE_ON_ERROR:
"
(объяснение есть в info-разделе
"Errors in Recipes").
Удаляет же он лишь если он САМ получит сигнал, например, SIGINT по Ctrl+C
(что описано в info-разделе
"Interrupting or Killing `make'").
ЗЫ: чтобы указывать такой "ключик" именно как "ключ в командной строке", в zsh (и bash?) можно прямо в командной строке дополнительно говорить
-f Makefile -f <(echo '.DELETE_ON_ERROR:')
18.04.2023: с имеющимся новым знанием об
environment-параметре PROTECTED_MODE_SOFT=1
возвращаемся к
сборке 3.15 -- проверяем, сработает ли, поможет ли...
=1
-- фиг, результат тот же.
genApps ../O.Common/gddApps.h
" -- выполнить с помощью обычной
64-битной утилиты (ровно как для Tango был использован приём
кросс-компиляции)?
22.04.2023: даже проверять не стал, т.к. ТОЧНО получится: эти файлы всегда одинаковые, с md5sum=56bde7d82fdc9e2e05614b5baac3d5c7, так что можно вообще просто скопировать из 64-битной директории.
19.04.2023:
print_ptr()
внутрь newdel_setNext()
, определяемой
в макросе gdd_NEWDEL_FUNC()
(см. ниже его обсуждение).
Видно, что оно,
gddBounds1D
:
(и с// Special managment for 1D-2D-3D bounds - others use normal new/remove. // Since bounds are maintained as arrays, where the length of the array is // the dimension of the data, we will manage the normal cases using // free lists. class epicsShareClass gddBounds1D { public: gddBounds1D(void) { } gddBounds* boundArray(void); gdd_NEWDEL_FUNC(b[0]) // required for using generic new and remove private: gddBounds b[1]; gdd_NEWDEL_DATA // required for using generic new/remove }; inline gddBounds* gddBounds1D::boundArray(void) { return b; }
gddBounds2D
и gddBounds3D
аналогично, но нас
не они волнуют.)
Т.е., макросу gdd_NEWDEL_FUNC()
сбагривается для его целей
элемент b[0]
-- который имеет тип gddBounds
gddBounds
там же выше (приведено
сокращённо, без методов, только данные):
// Describe the bounds of a single dimension in terms of the first // element and the number of elements in the dimension. class epicsShareClass gddBounds { private: aitIndex start; aitIndex count; };
т.е., размерtypedef aitUint32 aitIndex;
gddBounds
-- 8 байт.
gdd_NEWDEL_FUNC()
и gdd_NEWDEL_DATA
:
// private data to add to a class #define gdd_NEWDEL_DATA \ static char* newdel_freelist; \ static epicsMutex *pNewdel_lock; \ static epicsThreadOnceId once; // public interface for the new/delete stuff // user gives this macro the address they want to use for the next pointer #define gdd_NEWDEL_FUNC(fld) \ void* operator new(size_t); \ void operator delete(void*); \ char* newdel_next(void) { char* pfld = (char *)&fld; \ char** x = (char**)pfld; return *x; } \ void newdel_setNext(char* n) { char* pfld = (char *)&fld; \ char** x=(char**)pfld; *x=n; } \ static void gddNewDelInit (void) { pNewdel_lock = newEpicsMutex; }
operator new
и operator delete
макросами
gdd_NEWDEL_NEW()
и gdd_NEWDEL_DEL()
соответственно, а в src/ca/legacy/gdd/gddUtils.cc содержится их
"вызов".
Объём этих макросов слишком велик, чтобы заквочивать их тут, но смысл в
том, что аллокируется сразу пачка из 20 штук (gdd_CHUNK_NUM
),
помещается в newdel_freelist
, и потом отдаётся указатель на
ячейку из этого списка.
...и ещё там есть такой милый фрагмент
"newdel_setNext((char*)(-1));
" -- явным образом кастится
ОТРИЦАТЕЛЬНОЕ целое к указателю! Фантазёр без тормозов, блин...
Нет бы использовать какой-нибудь union
с полем вроде
void *
-- неа, просто надеется, что "ну влезет же". (Ага, оно
и влазило вплоть до 64-битных указателей включительно, но о гарантиях со
стороны языка автор не позаботился, хотя это было несложно.)
gdd_NEWDEL_FUNC()
.
И, замечу, это в якобы C++, где к тому времени было и множественное наследование, и template'ы.
Т.е., по факту тут даже не то что не плюсовый код, а такой, что и в обычном C был бы крив (из-за внахаловку записи указателя на место пары int32).
А может, какой-нибудь структуре сделать
__attribute__((aligned (sizeof(void*)) ))
?
gddBounds
атрибут
aligned
-- размер структуры увеличился с 8 до 16 (в отладочной
печати выдаётся) и сборка пошла дальше.
И оно собралось!!! 23m36.574s
P.S. Командная строка --
time make EPICS_HOST_ARCH=linux-e2k_128 HOST_OPT=NO \ OP_SYS_LDFLAGS="-m128 -Wl,-rpath,${HOME}/usr128/lib -Wl,-rpath-link,${HOME}/usr128/lib" \ OP_SYS_CFLAGS="-m128 -isystem ${HOME}/usr128/include" \ ARCH_DEP_CPPFLAGS="-D_E2K_128_ -isystem ${HOME}/usr128/include" \ COMMANDLINE_LIBRARY=READLINE_NCURSESW LDLIBS_READLINE_NCURSESW="-lreadline -lncursesw"
22.04.2023: пора бы уже проверить работающесть как минимум cxsd_fe_epics в защищённом режиме.
Сборка прошла без проблем.
23.04.2023: тестируем.
LD_LIBRARY_PATH=${HOME}/ptr128/base-3.15.9/lib/linux-e2k_128 \ ./sbin/cxsd -dsc configs/cxsd.conf -f configs/devlist-canhw-11.lst :11 -e 'load-frontend epics' 2023-04-23 04:48:23.817 yukari cxsd#11: signal 4 ("Illegal instruction") arrived 2023-04-23 04:48:23.836 yukari cxsd#11: exit(4), errno=0 ("Success")
...впрочем, оно и безо всякого EPICS так... С чего вдруг? Ведь проверялась же работоспособность в защищённом режиме, и кабы не на этом же devlist'е?
-S
" и записью
из третьей консоли).
С аналогичным эффектом.
CxsdDbResolveName()
ловил -- что намекает на возможную
причину):
Thread 1 "cxsd" received signal SIGILL, Illegal instruction exc_diag_ct_cond at 0x50805f60 0x0000000050805f60 in strnlen () from /lib128/libc.so.6 (gdb) bt #0 0x0000000050805f60 in strnlen () from /lib128/libc.so.6 #1 0x0000000050806740 in strncpy () from /lib128/libc.so.6 #2 0x000000005144ba20 in ?? () from /export/home/bolkhov/ptr128/base-3.15.9/lib/linux-e2k_128/libgdd.so.3.15.9 #3 0x0000000051511298 in _INTERNAL_13_aitConvert_cc_5249ea47::aitConvertFixedStringString (d=0x522d6204, s=0x50dc2680, c=1) at ../aitConvert.cc:177 #4 0x00000000513643e0 in aitConvert (desttype=aitEnumFixedString, dest=0x522d6204, srctype=aitEnumString, src=0x50dc2680, count=1, pEnumStringTable=0x522f5e40) at ../../../../../../include/aitConvert.h:72 . . .
Собственно, дальше можно не смотреть: тут фигурирует та самая
aitConvertFixedStringString()
, косяк в которой был замечен ещё
20-07-2022 (а обход был придуман 18-11-2022, но НЕ реализован).
Сценария добытия облома прост: натравить camonitor, затем выполнить
сначала "векторную запись" =65,66,67,68
-- и потом
"скалярную" =99
-- тут-то оно и скопычивается на путанице с
данными строк.
P.S. И да -- на обычных архитектурах тот косяк приводит к мусору, а в защищённом режиме ещё и к вылету по причине неинициализированных данных.
12.03.2023: давно напрашивалось -- раз фиг знает, где взять содержимое пакета omniORB для yukari, то просто собрать его из исходников.
./configure --prefix=${HOME}/usr --disable-static
-- фиг, вылазит ошибка, аналогичная TANGO'вской, только дата уже
config.guess timestamp = 2001-08-21
./configure --prefix=${HOME}/usr --disable-static --build=e2k-unknown-linux-gnu
-- сконфигурилось. Собралось за 15m5.278s.
Конфигуратору TANGO путь к результату этой сборки указывается посредством
"--with-omni=${HOME}/usr
".
Далее занадобился ZMQ --
checking for LIBZMQ... no configure: error: Missing ZMQ library - Use --with-zmq configure option or add directory containing libzmq.pc to PKG_CONFIG_PATH environment variable
А для него, как мы знаем с 24-07-2019, нужны openpgm и libsodium.
--disable-silent-rules --disable-opt
" (подсмотрено в его
.spec-файле).
--build=e2k-unknown-linux-gnu
" (там дата config.guess
значилась за 2012-02-10), как и для zeromq (2013-06-10).
$PKG_CONFIG_PATH
(хбз, как ею
пользоваться, как и pkg-config
вообще), либо индивидуально
указывая переменные окружения *LIBS и *CFLAGS вот таким способом:
-- вот этот вариант и сработал.sodium_CFLAGS=-I${HOME}/usr/include sodium_LIBS=-L${HOME}/usr/lib pgm_CFLAGS=-I${HOME}/usr/include pgm_LIBS=-L${HOME}/usr/lib \ ./configure --prefix=${HOME}/usr --build=e2k-unknown-linux-gnu
CXXLD curve_keygen /usr/bin/ld: ./.libs/libzmq.so: undefined reference to `randombytes_close' /usr/bin/ld: ./.libs/libzmq.so: undefined reference to `crypto_box_open_afternm' /usr/bin/ld: ./.libs/libzmq.so: undefined reference to `crypto_secretbox_open' /usr/bin/ld: ./.libs/libzmq.so: undefined reference to `randombytes' /usr/bin/ld: ./.libs/libzmq.so: undefined reference to `crypto_box_keypair' /usr/bin/ld: ./.libs/libzmq.so: undefined reference to `crypto_box_afternm' /usr/bin/ld: ./.libs/libzmq.so: undefined reference to `crypto_box_open' /usr/bin/ld: ./.libs/libzmq.so: undefined reference to `crypto_box' /usr/bin/ld: ./.libs/libzmq.so: undefined reference to `sodium_init' /usr/bin/ld: ./.libs/libzmq.so: undefined reference to `crypto_secretbox' /usr/bin/ld: ./.libs/libzmq.so: undefined reference to `crypto_box_beforenm' make[1]: *** [Makefile:1874: curve_keygen] Error 1 make[1]: Leaving directory '/export/home/bolkhov/compile/zeromq-4.1.4'
sodium_CFLAGS=
...", но не помогло. И не зря -- как потом
показал поиск, эти определения есть в Makefile, сгенерённом
конфигурацией.
AM_DEFAULT_VERBOSITY
, которая по
умолчанию =0. OK -- указал "AM_DEFAULT_VERBOSITY=1
" и получил
/bin/sh ./libtool --tag=CXX --mode=link g++ -g -O2 -o curve_keygen tools/curve_keygen.o libzmq.la -lrt -lpthread libtool: link: g++ -g -O2 -o .libs/curve_keygen tools/curve_keygen.o ./.libs/libzmq.so -lrt -lpthread -Wl,-rpath -Wl,/export/home/bolkhov/usr/lib
Т.е., у этих кретиноидов указанные configure'у параметры никак не сказались на командной строке!!!
-L/export/home/bolkhov/usr/lib -lsodium
проблему решает.
-Lпуть
", но и "-lБИБЛИОТЕКА
" --
sodium_CFLAGS=-I${HOME}/usr/include sodium_LIBS="-L${HOME}/usr/lib -lsodium" pgm_CFLAGS=-I${HOME}/usr/include pgm_LIBS="-L${HOME}/usr/lib -lpgm" \ ./configure --prefix=${HOME}/usr --build=e2k-unknown-linux-gnu
Вот в ЭТОМ варианте -- собралось!
Хотя совершенно непонятно, почему: при "AM_DEFAULT_VERBOSITY=9" в логах
не видно никаких отличий относительно curve_keygen
, а
"-lsodium
" присутствует совсем в других местах.
-- и тоже вроде прокатило. Собралось за 8m49.190s. И TANGO с ним тоже собралось, за 148m10.734s../configure --prefix=${HOME}/usr2 --build=e2k-unknown-linux-gnu --with-libsodium=no --with-pgm=no
Реально-то именно с ним и были связаны наши проблемы: autoconf, automake, libtool и связанный с ними pkg-config -- это части "GNU Autotools", изначально именовавшейся "GNU build system".
Теперь надо будет проверять работоспособность. Та ещё задачка...
05.04.2023: начинаем сразу с omniORB (и сразу облом):
./configure --prefix=${HOME}/usr128 --disable-static --build=e2k-unknown-linux-gnu CPPFLAGS=-m128 LDFLAGS=-m128
-- ошибка сразу понятная и даже вполне предсказуемая (чисто из размеров: при 128-битном указателе нет ни одного штатного/обычного целочесленного типа того же размера).lcc: "../../../../include/omniORB4/CORBA_sysdep.h", line 72: catastrophic error #35: #error directive: "No suitable type to do pointer arithmetic" #error "No suitable type to do pointer arithmetic" ^
// // Pointer arithmetic type // #if SIZEOF_PTR == SIZEOF_LONG typedef unsigned long omni_ptr_arith_t; #elif SIZEOF_PTR == SIZEOF_INT typedef unsigned int omni_ptr_arith_t; #elif defined (_WIN64) typedef size_t omni_ptr_arith_t; #else #error "No suitable type to do pointer arithmetic" #endif
#else
строки
плюс предварительно "#elif 1 typedef ptrdiff_t omni_ptr_arith_t;
#include <stddef.h>
"; наиболее
формально-корректное решение, т.к. именно ptrdiff_t
именно для
арифметики указателей и предназначен (и какого лешего эта библиотека что-то
своё изобретает?!).
-- оно собирает какую-то .so'шку и пытается её загрузить из python'ского кода, но python-то в системе 64-битный.make[3]: Entering directory '/export/home/bolkhov/ptr128/omniORB-4.2.0/src/lib/omniORB' + ../../../bin/scripts/omkdirhier omniORB4 /usr/bin/python ../../../bin/scripts/distdate.py <../../../update.log >omniORB4/distdate.hh ../../../bin/omniidl -bcxx -p../../../src/lib/omniORB/python -I../../../idl -Wbdebug -Wba -p../../../src/lib/omniORB/python -I../../../idl -Wbdebug -v -ComniORB4 ../../../idl/Naming.idl omniidl: ERROR! omniidl: Could not open IDL compiler module _omniidlmodule.so omniidl: Please make sure it is in directory /export/home/bolkhov/ptr128/omniORB-4.2.0/lib omniidl: (or set the PYTHONPATH environment variable) omniidl: (The error was '/export/home/bolkhov/ptr128/omniORB-4.2.0/lib/_omniidlmodule.so: ELF file OS ABI invalid')
(Сама библиотека в порядке, ссылается на /lib128/ и /usr/lib128/ -- ldd подтвердил.)
...мож правда проще Python собрать? :D
Пробуем вырулить ХОТЬ КАК-ТО...
Его смысл в том, чтобы ПЕРЕД общей сборкой конкретно 3 директории собрать ОТДЕЛЬНО, под ХОСТ-платформу; тогда общая сборка их собирать уже не будет (ибо по timestamp'ам всё свежее), а .so-модуль будет совместим с python'ом.
Сделать я это попробовал чуть иначе: если в "рецепте" предлагается
указывать make'у другой компилятор (CC=...
), то я попробовал
указывать другие CPPFLAGS и LDFLAGS --
CPPFLAGS=-m64 LDFLAGS=-m64
Так вот -- фиг: если "./configure
"'у указываются именно
специфичные флаги, то на этапе make к ним добавлена уже туча всякого,
включая "-I...
", так что с моим указанием оно резко обломилось
по ненайденности своих include-файлов.
#!/bin/sh gcc -m128 $*
./configure
"'у 128-битные версии, а make'у
при сборке тех 3 директорий -- 64-битные.
./configure --prefix=${HOME}/usr128 --disable-static --build=e2k-unknown-linux-gnu \ CC=${HOME}/bin/gcc128.sh CPP="${HOME}/bin/gcc128.sh -E" CXX=${HOME}/bin/g++128.sh
make CC=${HOME}/bin/gcc64.sh CPP="${HOME}/bin/gcc64.sh -E" CXX=${HOME}/bin/g++64.sh -C src/tool/omniidl/cxx/cccp make CC=${HOME}/bin/gcc64.sh CPP="${HOME}/bin/gcc64.sh -E" CXX=${HOME}/bin/g++64.sh -C src/tool/omniidl/cxx make CC=${HOME}/bin/gcc64.sh CPP="${HOME}/bin/gcc64.sh -E" CXX=${HOME}/bin/g++64.sh -C src/tool/omkdepend/
make
Этот вариант прокатил!!! Проблемы с генерацией IDL не возникло.
lcc: "../../../../include/omniORB4/stringtypes.h", line 584: catastrophic error #35: #error directive: "No suitable type to do pointer arithmetic" #error "No suitable type to do pointer arithmetic" ^
В этот stringtypes.h внесён точно такой же фикс, как в
CORBA_sysdep.h, только тут определяется тип
ptr_arith_t
.
После этого всего -- omniORB собрался!
Далее:
./configure --prefix=${HOME}/usr128 CPPFLAGS=-m128 LDFLAGS=-m128 --disable-silent-rules --disable-opt
...указывать "--build=e2k-unknown-linux-gnu
" ещё можно, а вот
"--build=e2k_128-unknown-linux-gnu
" -- нельзя:
checking build system type... Invalid configuration `e2k_128-unknown-linux-gnu': machine `e2k_128-unknown' not recognized configure: error: /bin/sh build-aux/config.sub e2k_128-unknown-linux-gnu failed
...всё потому, что его конфигуратор ЗНАЕТ e2k!
Собрался за уже теперь 30m14.549s.
cd openpgm/pgm ./configure --prefix=${HOME}/usr128 --build=e2k_128-unknown-linux-gnu CPPFLAGS=-m128 LDFLAGS=-m128 make AM_DEFAULT_VERBOSITY=1
./configure --prefix=${HOME}/usr128 --build=e2k_128-unknown-linux-gnu CPPFLAGS=-m128 LDFLAGS=-m128 \ sodium_CFLAGS=-I${HOME}/usr128/include sodium_LIBS="-L${HOME}/usr128/lib -lsodium" \ pgm_CFLAGS=-I${HOME}/usr128/include pgm_LIBS="-L${HOME}/usr128/lib -lpgm" make AM_DEFAULT_VERBOSITY=1
...обломатушки:
lcc: "src/msg.cpp", line 46: error #94: the size of an array must be greater than zero [2 * ((sizeof (zmq::msg_t) == sizeof (zmq_msg_t)) != 0) - 1]; ^ 1 error detected in the compilation of "src/msg.cpp". make[1]: *** [Makefile:2718: src/libzmq_la-msg.lo] Error 1
// Check whether the sizes of public representation of the message (zmq_msg_t) // and private representation of the message (zmq::msg_t) match. typedef char zmq_msg_size_check [2 * ((sizeof (zmq::msg_t) == sizeof (zmq_msg_t)) != 0) - 1];
typedef struct zmq_msg_t {unsigned char _ [64];} zmq_msg_t;
zmq::msg_t
в src/msg.hpp имеет
диковатый вид (приведено сокращённо, bold мой):
enum { msg_t_size = 64 }; enum { max_vsm_size = msg_t_size - (8 + sizeof (metadata_t *) + 3) }; . . . // the file descriptor where this message originated, needs to be 64bit due to alignment int64_t file_desc; // Note that fields shared between different message types are not // moved to the parent class (msg_t). This way we get tighter packing // of the data. Shared fields can be accessed via 'base' member of // the union. union { struct { metadata_t *metadata; unsigned char unused [msg_t_size - (8 + sizeof (metadata_t *) + 2)]; unsigned char type; unsigned char flags; } base; struct { metadata_t *metadata; unsigned char data [max_vsm_size]; unsigned char size; unsigned char type; unsigned char flags; } vsm; . . . struct { metadata_t *metadata; unsigned char unused [msg_t_size - (8 + sizeof (metadata_t *) + 2)]; unsigned char type; unsigned char flags; } delimiter; } u;
07.04.2023: продолжаем разборки с zeromq.
Сначала пробуем собрать свежую версию -- вдруг там что-то изменилось (такой-то кривой код могли и исправить) и проблема решена?
./autogen.sh
",
потом (упрощённый вариант)
ну и "./configure --prefix=${HOME}/usr128 --build=e2k_128-unknown-linux-gnu CPPFLAGS=-m128 LDFLAGS=-m128 \ --with-libsodium=no --with-pgm=no
make AM_DEFAULT_VERBOSITY=1
".
Сначала он долго-долго собирал asciidoc'ом свою документацию, а потом...
(да-да, номера строк в тамошних Makefile'ах особенно доставляют -- клятый libtool...)lcc: "src/msg.hpp", line 274: error #103: class is too large { ^ lcc: "src/msg.hpp", line 287: error #103: class is too large { ^ lcc: "src/msg.hpp", line 300: error #103: class is too large { ^ 3 errors detected in the compilation of "src/channel.cpp". make[1]: *** [Makefile:5363: src/libzmq_la-channel.lo] Error 1
Так что забиваем на новую версию и возвращаемся к 4.1.4.
zmq_msg_t
-- "заглушка" для внешнего кода;
zmq::msg_t
-- реальная структура с данными.
Они для одного и того же куска памяти, но снаружи видна "заглушка", а внутри -- реальное содержимое.
(~16:00, дорога из Эдема домой, пересекая Николаева: чё не использовали собственную аллокацию с отдачей наружу handle'ов (как в cxscheduler) -- загадка.)
(~16:00, дорога из Эдема домой, пересекая Николаева: почему не использовали средства языка -- тоже загадка.)
file_desc
,
сделанное 64-битным для того, чтобы следующий далее во всех union-вариантах
* metadata
не требовал бы padding'а от
компилятора;
unused
, чей размер и
вычисляется диковатым образом, дающим сбой при 128-битности;
type
и flags
.
vsm
: перед
последней парой есть ещё 8-битное size
-- для того случая
размер max_vsm_size
.)
Т.е., они пытаются сделать так, чтобы:
file_desc
,metadata
)...
type
,flags
,
size
,
unused
"растягивалась" бы.
file_desc
64-битным
в предположении, что указатели не более 8 байт,
zmq::msg_t
становится
64+8=72 байта, что вызывает несовпадения в "проверке" посредством
zmq_msg_size_check[]
.
#if __ptr128__ #define SIZEOF_PADDING sizeof(int64_t) #else #define SIZEOF_PADDING 0 #endif // Size in bytes of the largest message that is still copied around // rather than being reference-counted. enum { msg_t_size = 64 }; enum { max_vsm_size = msg_t_size - (8 + SIZEOF_PADDING + sizeof (metadata_t *) + 3) }; . . . struct { metadata_t *metadata; unsigned char unused [msg_t_size - (8 + SIZEOF_PADDING + sizeof (metadata_t *) + 2)]; unsigned char type; unsigned char flags; } base;
Но линковка всё равно ломается, уже по другой причине:
-nostdlib
", а стандартные библиотеки перечисляет
самостоятельно, взяв их список фиг знает где и, по ошибке, для 64-битного
режима (строки команды разорваны "\NL"'ями для наглядности):
libtool: link: g++ -fPIC -DPIC -shared -nostdlib /usr/lib64/crti.o /opt/mcst/lcc-home/1.26.17/e2k-v4-linux/lib64/crtbeginS.o \ . . . -lrt -lpthread -L/opt/mcst/lcc-home/1.26.17/e2k-v4-linux/lib64 \ -lstdc++ -lm -lc -lgcc_s -llcc -lmvec /opt/mcst/lcc-home/1.26.17/e2k-v4-linux/lib64/crtendS.o /usr/lib64/crtn.o \ -O2 -Wl,--version-script=./src/libzmq.vers -m128 -pthread -Wl,-soname -Wl,libzmq.so.5 -o .libs/libzmq.so.5.0.0 /usr/bin/ld: /opt/mcst/lcc-home/1.26.17/e2k-v4-linux/lib64/libstdc++.so: error adding symbols: file in wrong format make[1]: *** [Makefile:1795: libzmq.la] Error 1 make[1]: Leaving directory '/export/home/bolkhov/ptr128/zeromq-4.1.4'
08.04.2023: пытаемся побороть
"-nostdlib
"...
archive_cmds
с
archive_expsym_cmds
и predep_objects
с
postdep_objects
; причём там захардкожена и ссылка на директорию
конкретной версии компилятора -- кстати, в CentOS-7.3 аналогично, зашито
4.8.5.
Как эта хрень должна использоваться в случае кросс-компиляции или хотя бы наличии нескольких вариантов локальной архитектуры (32- и 64-битного) -- загадка.
./configure ...
" с надлежащими указаниями "-m128
"
и затем "grep -r lib64 .
" -- помимо config.status он
сказал "./libtool", откуда родилась идея: а что,
если сразу после этого вручную заменить в этом сгенерённом файле
libtool все "lib64" на "lib128" --
прокатит?
Прокатило!!! А лучше даже автоматизированный вариант --
sed -i -e 's|lib64|lib128|g' ./libtool
Собралось БЕЗ libsodium/libpgm за 9m21.776s, с ними за 9m57.549s.
./configure --prefix=${HOME}/usr128 --build=e2k_128-unknown-linux-gnu CPPFLAGS=-m128 LDFLAGS=-m128 \ sodium_CFLAGS=-I${HOME}/usr128/include sodium_LIBS="-L${HOME}/usr128/lib -lsodium" \ pgm_CFLAGS=-I${HOME}/usr128/include pgm_LIBS="-L${HOME}/usr128/lib -lpgm"
sed -i -e 's|lib64|lib128|g' ./libtool
make AM_DEFAULT_VERBOSITY=1
Теперь можно и Tango попробовать собрать.
09.04.2023: пробуем.
./configure --build=e2k_128-unknown-linux-gnu --prefix=${HOME}/usr128 --with-omni=${HOME}/usr128 CPPFLAGS=-m128 LDFLAGS=-m128 --without-zlib
Кстати, странно, что не требует указывать "--with-zmq
" --
наверное, хватает -I
и -L
от
"--with-omni
" (а вот оставить только "--prefix" -- фиг, тогда
не находит).
Затем
make AM_DEFAULT_VERBOSITY=1
libtool: compile: g++ -DHAVE_CONFIG_H -I. -I../include -I../include -m128 -g -O2 -Wall -Wno-unused -pedantic \ -D_REENTRANT -MT PThreads.lo -MD -MP -MF .deps/PThreads.Tpo -c PThreads.cpp -fPIC -DPIC -o .libs/PThreads.o lcc: "PThreads.cpp", line 57: error #171: invalid type conversion return static_cast<long>(::pthread_self()); ^ 1 error detected in the compilation of "PThreads.cpp". make[5]: *** [Makefile:400: PThreads.lo] Error 1 make[5]: Leaving directory '/export/home/bolkhov/ptr128/tango-9.2.5a/lib/cpp/log4tango/src'
Несовместимость pointer с long? Но вроде pthread_t
-- это
целое, в CentOS-7.3 это
/usr/include/bits/pthreadtypes.h: typedef unsigned long int pthread_t;
...а-а-а, нашёл: на Эльбрусе этот же кусок имеет вид
-- т.е., именно pointer.../* Thread identifiers. The structure of the attribute type is not exposed on purpose. */ #if ! defined __ptr128__ typedef unsigned long int pthread_t; #else /* defined __ptr128__ */ typedef void * pthread_t; #endif /* defined __ptr128__ */
pthread_equal()
" -- используют именно операторы числового
сравнения.
...с одной стороны, это лишний камень в огород multithreading'а: наверняка этот "слой" был заведён для возможности работы с разными реализациями.
С другой -- это опять характеризует "качество" Tango-кода: для них же,
козлов, был введён специальный тип pthread_t
, но нет, вместо
его использования наколхозили long'ов.
И что-то мне подсказывает, что репортить им это как баг бессмысленно -- просто не поймут...
01.08.2024: подумавши "а не заслать ли всё же в
Tango-forum вопрос насчёт какого чёрта они считают pthread_t
целочисленным?", решил поискать, какими стандартами определяется, каким
должен быть pthread_t
:
Ещё некоторое время назад начал делать специальный драйвер для возможности исполнять в CX-сервере python'ский код. Вот это -- о нём.
14.04.2023: первоначальные потребности и соображения:
ReturnDataSet()
.
"Процесс реализации":
python3-config
, и проверка реализована способом, подсмотренным
в "info make" по строке поиска "$(PATH".
ParseRefSpec()
-- точнее, взят
соответствующий кусок кода из
trig_read_drv.c::trig_read_init_d()
и обобщён (а
так-то, кстати, он же есть и в mirror_drv.c).
(Поскольку предполагалось, что надо будет регистрировать cda-каналы.)
Некоторые дополнительные соображения:
ЧеблоПаша не знает.
Случайно в tech-talk'е встретилось сообщение "Re: Is the Sequencer and SNL still widely used?" от Pearson, Matthew с фразой "there are more options these days for integrating Python into a normal IOC", после чего...
Гуглением по "epics ioc python" наткнулся на презентацию "Easy integration of Python into EPICS IOC" от Klemen Vodopivec за 19-October-2020.
Так что -- ДА, есть, именно в том виде, о котором думается: возможность использовать ОТДЕЛЬНЫЕ змеиные драйверы (а не весь сервер).
А по EPICS и Tango -- пункты в их подразделах.
Проблемы-то похожие, но у EPICS и Tango из-за их зависимостей куча своих нюансов, а главное -- баги, которые предполагается найти посредством "защищённого режима", уже их собственные специфичные.
-m128
", он
же "-mptr128
", который как бы не 128-битный режим, а просто
"128-битные указатели", но при этом реально адрес там 64-битный, а остальные
2 по 32 бита используются для хранения размера блока памяти и смещения в нём
-- это т.н. "защищённый режим".
Так вот: захотелось с этой штукой поиграться -- чисто из интереса. Для описания этих игрищ и раздел.
Источники информации:
17.03.2023: "формат" указателя описан в документации так:
Вот и пытаемся проверить что да как -- как эти указатели вообще работают, как у них с адресной арифметикой.
malloc()
'нутый кусок памяти, на функцию.
#include <stdio.h> #include <stdlib.h> int a[10]; void f(int x) { printf("x=%d\n", x); } void print_ptr(const char *name, void *ptr) { unsigned char *pb; int nb; printf("%4s=0x", name); for (nb = sizeof(ptr) - 1, pb = ((unsigned char*)&ptr) + sizeof(ptr) - 1; nb >>= 0; nb--, pb--) printf("%02x", *pb); printf("\n"); } int main(void) { int *i_p; int *m_p; i_p = a + 2; m_p = malloc(1024); printf("a=%p i_p=%p m_p=%p f=%p\n", a, i_p, m_p, f); print_ptr("a", a); print_ptr("i_p", i_p); print_ptr("m_p", m_p); print_ptr("f", f); }
a=0x5015e0d0 i_p=0x5015e0d8 m_p=0x50944030 f=0x5015aaf0 a=0x0000002800000000180000005015e0d0 i_p=0x0000002800000008180000005015e0d0 m_p=0x00000400000000001800000050944030 f=0x0000000000000000000000005015aaf0
a
и i_p
=a+2
есть и равна как бы 8
, только она не младшей части указателя, а
в средней. Т.е., арифметика указателей работает весьма неортодоксально.
a[]
), а вот
malloc()
'нутого -- не видно.
3*(size_t)0x40000000
-- привела к печати (на STDOUT!) сообщения
(да, безграмотновато -- "Gb" вместо "GB")ERROR: trying to malloc 3221225472 (0xc0000000) bytes malloc cannot allocate over 2**31 bytes (2Gb) in protected mode
Сообщение, кстати, некорректное: реально не "over 2" -- даже просто 2GB
(=0x80000000) не даёт. И даже 2GB-1 (а тут уже не ругается, а
просто возвращает NULL), и даже минус много чего ещё; найденный максимум
разрешённого -- 2GB-4KB-56B (а вот 2GB-4KB-55B уже нельзя),
2*(size_t)0x40000000-1024*4-56)
, при этом
m_p=0x7fffefc8000000001800000050a60020
(мда,
0x7fffefc8
-- странная граница).
22.03.2023: пробуем собрать в режиме
"-m128
" весь CX -- раз уж это такой интересный "защищённый"
режим, позволяющий находить баги, то грех не воспользоваться.
make TOP_CFLAGS=-m128
" -- более
$(TOP_CFLAGS)
нигде не уставляется, а сделана категория "TOP"
была ровно для этого (а используется из неё только
TOP_INCLUDES
).
-- и отвал через SIGSEGV!!!gcc -o cxsd_fe_starogate.so cxsd_fe_starogate.o -shared -Wl,-soname,cxsd_fe_starogate.so -g /usr/bin/ld: PM input file `cxsd_fe_starogate.o' is incompatible with 64-bit output /usr/bin/ld: failed to merge target specific data of file cxsd_fe_starogate.o Execution of /usr/bin/ld terminated by signal "Segmentation fault" (11) make[2]: *** [../../GeneralRules.mk:261: cxsd_fe_starogate.so] Error 139
Попытка "перепрыгнуть" через эту ошибку далее и собрать, для простоты
(нет никаких .so) директорию programs/utils/ (для чего
пришлось сделать
"make TOP_CFLAGS=-m128 -C lib/srv/ libcxsd.a
")
дала аналогичный результат:
/usr/bin/ld: PM input file `drvinfo.o' is incompatible with 64-bit output /usr/bin/ld: failed to merge target specific data of file drvinfo.o /usr/bin/ld: PM input file `drvinfo_publics.o' is incompatible with 64-bit output /usr/bin/ld: failed to merge target specific data of file drvinfo_publics.o /usr/bin/ld: PM input file `../../lib/srv/libcxsd.a(cxsd_driver.o)' is incompatible with 64-bit output /usr/bin/ld: failed to merge target specific data of file ../../lib/srv/libcxsd.a(cxsd_driver.o) . . . /usr/bin/ld: PM input file `../../lib/misc/libmisc.a(paramstr_parser.o)' is incompatible with 64-bit output /usr/bin/ld: failed to merge target specific data of file ../../lib/misc/libmisc.a(paramstr_parser.o) Execution of /usr/bin/ld terminated by signal "Segmentation fault" (11) make: *** [../../GeneralRules.mk:298: drvinfo] Error 139
P.S. И самая задница в том, что после этого SIGSEGV'а собственно ВЫХОДНОЙ ФАЙЛ остаётся, со "свежей" датой -- так что при следующем прогоне он считается "up to date"!
16.04.2023: а вот НЕудаление файла -- оказывается,
стандартное поведение make, если только НЕ указано
".DELETE_ON_ERROR:
" (см. в соседнем разделе за сегодня).
Хотя в "man ld" про такое ничего нет (только какое-то
"-m emulation
"),
но, коль скоро в качестве линкера используется сам gcc, то уж он-то должен
знать, что скормить реальному линкеру.
TOP_LDFLAGS=-m128
" -- и вот это помогло.
/usr/bin/ld: /usr/lib/libXm.so: error adding symbols: file in wrong format make[2]: *** [../../GeneralRules.mk:298: tstknobs] Error 1
Пришлось добавить "NOX=1
", с этим -- собралось.
И hw4cx/ с hw4cx/ тоже.
Теперь проверять работоспособность.
23.03.2023: проверяем.
./sbin/cxsd -dsc configs/cxsd.conf -f /dev/null
"
получаем SIGSEGV.
но деталей не показывает, ибо "No symbol table info available." (?!?!?!).Program received signal SIGSEGV, Segmentation fault exc_array_bounds at 0x5020a3e0 ALS2 0x000000005020a3e0 in ppf4td_pipe_open () (gdb) bt #0 0x000000005020a3e0 in ppf4td_pipe_open () #1 0x0000000050209518 in ppf4td_m4_open () #2 0x00000000501fef78 in ppf4td_open () #3 0x00000000501723c0 in file_db_ldr () #4 0x00000000501d10f0 in CxsdDbLoadDb () #5 0x0000000050101298 in ReadHWConfig () #6 0x0000000050101f40 in main ()
ppf4td_test m4::/dev/null
"
-- тоже падает. Ну это по крайней мере уже изучабельно путём напихивания
отладочных печатей в ppf4td_pipe_open()
.
me->pid = r;
".
me = malloc(sizeof(me));
вместо
me = malloc(sizeof(*me));
Декларация -- "pipe_privrec_t *me
", так что при типе
полеtypedef struct { FILE *fp; pid_t pid; } pipe_privrec_t;
pid
ВСЕГДА оказывалось за пределами аллокированной памяти,
но, видимо, на 32- и 64-битных платформах из-за того, что аллокируется
всегда кратно 16 байтам -- это "за пределами" просто попадало в
неиспользуемую область и потому всё молча работало.
Что ж -- действительно помогает этот "защищённый режим"!!!
CxsdDbResolveName()
.
Как показало разбирательство (долго возился, из-за почему-то не-показа GDB информации -- пришлось отладочной печатью), там идеологический косяк, вот в этом куске кода:
dot_p = strchr(p, '.'); if (dot_p == NULL) { dot_p = p + strlen(p); last = 1; } len = dot_p - p; /* Skip consecutive '.'s, if any */ while (dot_p[1] == '.') dot_p++;
Косяк в том, что если уж "dot_p == NULL
", т.е. далее в имени
точек нет и dot_p
ставится на терминирующий '\0', то
НЕЛЬЗЯ обращаться к dot_p[1]
("символу после точки"), т.к. это
будет ЗА ПРЕДЕЛАМИ строки.
Фикс -- добавлено "if (!last)
" перед while()
.
...непонятно, правда, почему оно падает (причём не сразу, а спустя фиг
знает сколько успешных резолвингов): судя по выдаче от впихнутого туда
print_ptr()
из программки выше,
CxsdDbResolveName()
'у значение
name
=0x000001f4000000001800c2deffdc2e80
;
т.е., длина 0x1f4 -- это targetbuf[500]
(0x1f4=500) у
ParseOneCpoint()
(вызывальщик);
0x000001f40000000f1800c2deffdc2e80
(смещение 0xF -- это 15, длина строки "QLB3n4.Iset_cur"),
0x000001f4000000101800c2deffdc2e80
--
предсказуемо 0x10=16,
fprintf(stderr, "dot_p[1]=0x%02x ", dot_p[1]);
вызывает SIGILL -- неясно: вроде бы 0x10 находится вполне в пределах размера
0x1f4...
Как будто "защищённый режим" как-то знает, что после терминирующего '\0', т.е. по смещению 16, уже ничего не было записано и потому запрещает обращения туда.
Чуть позже: проверил, поставив перед чтением (на котором SIGILL'илось) запись туда же -- и да, падать перестало!!!
На этом (пока?) вроде падения закончились.
24.03.2023: для этого написан простенький тестик
outofbounds.c -- принимает из командной строки число-offset, по
которому пробует прочитать из buf[]
, где инициализировано
только начало:
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { char buf[500]; int ofs; strcpy(buf, "abcde56789"); ofs = argc < 2? 5 : atoi(argv[1]); printf("buf[%d]=", ofs); printf("%d\n", buf[ofs]); return 0; }
Результат тестирования:
CxsdDbResolveName()
проявлялся не всегда, а лишь на конкретном
имени: если длина имени плюс NUL кратна 4, то тогда обращение "после" даст
ошибку, а иначе нет.)
char *p; . . . p = buf + ofs; print_ptr("p", p);
Так вот: при указании "20" программка напечатала
0x000001f4000000141800c2deffffd030
(0x14=20), а вот GDB'шное
"print p
" -- 0xc2deffffd044
; т.е., "свёрнутое"
значение -- младшие 64 бита (собственно указатель на базу) с прибавленным
значением смещения -- 0xc2deffffd044=0xc2deffffd030+0x14; вот только куда
делись старшие 16 бит с "0x1800"?
Т.е. -- GDB при выдаче указателей выполняет какую-то хитрую "свёртку" до human-readable-значения?
@вечер: проверил, добавив
рядышком с print_ptr()
'ом printf("%p",p)
-- эффект
тот же самый: выдаётся младшая 48-битная часть указателя с прибавленным
значением смещения.
c2defffdd000-c2deffffe000 rw-p 00000000 00:00 0 [stack] c2e000002000-c2e000003000 rw-p 00000000 00:00 0 [chain stack] c2e000003000-c2e000009000 rw-p 00000000 00:00 0 [procedure stack] c2ffffffe000-c2fffffff000 rw-p 00000000 00:00 0 [cut]
24.03.2023@вечер: решил проверить гипотезу "индивидуально помнится (не)инициализированность" и "по тэгу на каждые 32 бита". Для этого сваяна int_outofbounds.c вот такого вида (указание отрицательного числа вызывает запись в минус-указанную ячейку массива, положительного -- чтение из неё):
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { int buf[500]; int n; int ofs; for (n = 1; n < argc; n++) { ofs = atoi(argv[n]); if (argv[n][0] == '-') // Note: we check string instead of int for "-0" to be possible { ofs = -ofs; buf[ofs] = ofs; printf("@%d:=%d\n", ofs, buf[ofs]); } else printf("[%d]=%d\n", ofs, buf[ofs]); } return 0; }
Запущена как
"./int_outofbounds -1 0
"
-- записать в 1-ю, прочитать из 0-й: сработало предсказуемо, на чтении
срубилось.
Теперь проверяем вторую часть гипотезы, про 32-битные единицы
(не)инициализированности. Для этого вместо int
используем
short
-- чтоб по 2 ячейки на 32-битное слово. Запуск
"./short_outofbounds -1 0
" падений НЕ вызывает, как и
"0 -1
" -- из не-записанной ячейки читается 0
. А
вот "-0 2
" -- падает.
С новополученным пониманием перечитал документ "Режим Безопасных Вычислений" -- и там есть кое-что, ставшее яснее:
24.03.2023: пробуем также собрать EPICS под "e2k_128" -- записываем это в его подразделе раздела по E2K.
Погуглив "128 bit pointer", наткнулся страничку "We may end up with 128-bit pointers even without a "true" 128-bit architecture. ..." с упоминанием 3 вещей:
(Комментарий с исходной страницы -- "For example, CHERI has 128-bit pointers with 64-bit address space and 64 bits of metadata to crash harder on memory bugs in C and C++".)
Создаём раздел.
Название выбрано именно "aarch64", а не "arm64", по результатам чтения "источников" --
elif (echo $PLATFORM | grep -i "aarch64" >/dev/null) then CPU="AARCH64"
(Не то чтоб это было как-то критично/важно -- непосредственно на хостах
этой архитектуры компиляции пока не будет (не на чем), а только
кросс-компиляция из-под x86_64, а там, аналогично обычному ARM (Moxa) и
PPC860, все переменные вроде $(CPU)
уставляются явно безо
всякого детектирования.)
20.03.2023: пробуем собрать кросс-средствами. ВРОДЕ БЫ получилось, и довольно несложно (смотрелось в GeneralRules.mk на тему "что подменять" и в hw4cx/x-build/arm-linux/x_rules.mk на тему принципов/стиля подмены). Команда оказалась такой:
make CPU=aarch64 NOX=1 NOQT=1 \ CROSS_ROOT=${HOME}/tools/gcc-arm-10.2-2020.11-x86_64-aarch64-none-linux-gnu \ GCC='$(CROSS_ROOT)/bin/aarch64-none-linux-gnu-gcc' \ GXX='$(CROSS_ROOT)/bin/aarch64-none-linux-gnu-g++' \ AR='$(CROSS_ROOT)/bin/aarch64-none-linux-gnu-ar'
Без "NOQT=1
" были глюки, связанные с тем, что оно пыталось
использовать для "конфигурирования" локальные Qt-средства.
А вот почему не потребовалось указывать ни -I
, ни
-L
-- неясно; надо б проверить strace
'ом.
21.03.2023@утро-07:30:
проверил посредством "strace -fs400
" -- да, похоже, что
кросс-компилятор достаточно умён и не лезет в системные директории, а только
сразу в свои (относительно "своей" директории --
"ПУТЬ/bin/../lib/И-ДАЛЕЕ").
Так что можно считать найденное решение достаточным и более не греть голову.
Позже: и hw4cx/ с sw4cx/ тоже собрались.
И даже "CPU_X86_COMPAT=
" (чтоб не лезло кросс-собирать
драйверы) указывать не надо -- они и так уставляется правильно.
07.04.2025: а вот сейчас попробовал -- всё-таки надо
указывать "CPU_X86_COMPAT=
", по крайней мере для
hw4cx/, иначе какие-то колёса с упоминанием PowerPC.
06.05.2011: базовая идея --
Средства кросс-компиляции должны быть полностью отделены от какой-то конкретной подсистемы, и даже от самого "CX" вообще.
Они могут только быть в курсе инфраструктуры "GeneralRules.mk".
Так что --
22.10.2019: в frgn4cx/ спасает то, что директории epics/ и tango/ ДВУХуровневые: описания "хитрых правил" лежат в самих директориях и решения принимаются там же, а собираемое содержимое лежит в поддиректориях */cda/, которые при надобности выключаются из сборки.
Впрочем, drivers/daqmx/ вполне себе одноуровневая.
Собственно, какие директории требуют "особого обращения" -- условного отключения:
И в x-build/Makefile УЖЕ есть проверки: на тему
":$(OS):$(CPU_X86_COMPAT):"
и
":$(OS):$(CPU_X86_COMPAT):$(OS_GE_2_6_18):"
соответственно.
Ну так и добавить ВТОРЫЕ стадии проверок: что если MAY_BUILD_nnn, то проверять также наличие файлов-"канареек", и если их нету -- то выдавать warning и делать MAY_BUILD_nnn=NO
Весь вопрос лишь в том, как бы добывать имя файла-"канарейки", чтобы без дублирования. Т.к. местами будет по ДВА условия, а имена директорий в разных *.mk с определениями могут перекрываться.
23.10.2019: начато, пока С ДУБЛИРОВАНИЕМ, на x-build/ppc-860.
Проверка делается в 2 стадии: сначала первая (старая), на "теоретическую" возможность -- что платформа+ОС "та", а затем вторая (новая), на "практическую" возможность -- что факл компилятора физически доступен.
Работает.
24.10.2019: продолжаем:
И да -- это тоже работает, а места занимает меньше и выглядит читабельнее.
$(warning)
'ов.
PRJDIR=..
в самое начало файла.
Итого -- вроде готово.
Хотя на тему не-дублирования всех определений *_CANARY
надо
ещё подумать, так что пока "done" не ставим.
06.06.2023@Томск-Беленца-6-23, утро: неа, CAMAC низзя -- там API работы с LAM такой, что годится только для одно-LAM'ных программ (присылается сигнал и номер LAM'а подразумевается), а для нескольких LAM в одном процессе оно вообще никак не годится.
23.05.2012: детали:
Это было сделано после первого прогона, по ругательствам: дополнение
к п.1 --
ls -ltuL **/*(@)
.
Это по access-time выяснить уже невозможно (т.к., не конечные файлы,
а компоненты путей), потому было найдено явочным порядком -- по
ругательствам, с последующим
find . -name ИМЯ-ПРОБЛЕМЫ -ls
по исходному SDK.
А сборка -- нет, поскольку конкретно lib/gcc-lib/arm-linux/3.3.2/collect2 (единственный!) требует от /lib/i686/libc.so.6 символа/version GLIBC_2.3, которого там нет (максимум 2.2.4).
На RHEL5 всё пашет нормально.
(все переносы строк мои)....arm-linux/bin/ld: warning: ld-linux.so.2, needed by /home/bolkhov/work/x-compile/arm-linux_1.3//lib/libc.so.6, not found (try using -rpath or -rpath-link)
И вот тут началась традиционная ж:
-B
-- нифига не помогал.
strace -f -s200
, с фильтрацией по
open|stat|access
-- нифига не дала.
Причём даже списки того, какие файлы он пытается открыть -- отличались. Такое впечатление, что он, глядя на путь, через который вызван, как-то интеллектуально разнообразит список директорий.
-rpath-link
, уже знакомый по PPC/CANGW (см.
bigfile-0001.html за 19-01-2007).
Но с чего он вообще занадобился, когда в полном SDK всё и так работало -- загадка.
Поскольку SDK посеян, то файл был взят просто с живого контроллера из его /lib/.
Располагаем его сразу после кросс-компиляции (ибо это по сути вариант оной) и ПЕРЕД более ранним "pults".
31.01.2020: мини-протокол:
ИМЯ_ДИРЕКТОРИИ ИМЯ_БИБЛИОТЕКИ
Скрипт "тупой" -- читает с STDIN и пишет на STDOUT.
...по-хорошему, туда бы ещё Makefile для автоматического запуска генерации, но неясно, как NDK отнесётся к наличию такого файла.
LOCAL_BCKPFILES=jni/*~
Главный вопрос -- откуда брать информацию о target'е симлинка?
30.06.2016@пляж: в конце концов, сделать по-простому (вариант (а)): файл-таблица LIST_OF_PULTS.lst, содержащий дуплеты вида
ШАБЛОН_ИМЕНИ:БИНАРНЫЙ_КЛИЕНТ
Чтоб шаблон мог содержать "*", для указания клиентов группам экранов; а если подходящей строчки не нашлось (или вообще LIST_OF_PULTS.lst отсутствует) -- то используется умолчание "pult".
Как парсить -- да, вопрос: получается как бы "инверсный grep", т.к. паттерн в файле, а фиксированный как раз образец, с которым матчинг. Наверное, действительно awk'ом.
01.07.2016: приступаем потихоньку.
05.07.2016: да!!! Получилось!!!
Основное сделано вчера, а добито сегодня.
Главная проблема была в том, как сделать "инверсный grep". Делалось на awk'е.
BEGIN
) переменной CLIENT
присваивается умолчательное значение -- "pult";
CLIENT
'у переприсваивается значение $2;
END
) печатается текущее значение
CLIENT
.
Таким образом убиваем всех зайцев сразу:
ИМЯ_КЛИЕНТА ~ $2
почему-то не желал работать.
Сейчас-то, задним числом, ясно, что где-то что-то указывалось "не так" (а "как" надо -- видно через пункт ниже).
Началась эпопея с попыткой сделать compound statement. Просто ";" не катит -- это разделитель РАЗНЫХ правил, последовательно применяемых к строке. Обычная запятая "," в C'шном стиле тоже не работает (возможно, потому, что это в awk'е разделитель частей "диапазона", которые работают с группами строк и потому к построчному неприменимы).
Поэтому в конце концов удалось с вариантом
(P=$$1) && ("$(strip $1)" ~ P)
-- сначала присвоение (оно при !=0 даёт истину), а потом сравнение; ($strip) --
чтоб обрезать начальный пробел от аргумента функции (иначе не матчится; оно
нужно в любом случае).
"$(strip $1)" ~ $$1
-- и это работает!!!
Т.е., видимо:
$1
-- БЕЗ кавычек и
БЕЗ слэшей (НЕ /$1/).
Засим почитаем за "done".
Подумал я подумал -- напрашивается решение:
SW4CX_PARTS
, содержащую
space-separated-список "подпроектов", по умолчанию -- vepp5 vepp4 liu
weld
.
SW4CX_PARTS=vepp4
, что по правилам
Make'а имеет приоритет перед внутрифайловым значением.
...а при надобности "ничего" -- вообще SW4CX_PARTS=none
(это
сработает по причине указанного далее механизма функционирования).
vepp4_CLIENTS
, собираемые вместе конструкциями вида
CLIENTS=$(sort $(foreach P, $(PARTS), $($(P)_CLIENTS)) )
$(sort ...)
необходима.
19.02.2021: сделал первую итерацию --
SW4CX_PARTS
.
(Вот со сваркой и ЛИУ -- weld и liu -- проблем не возникло.)
$(ACCESSGROUPS)
и
cx-starter-setup-term-kls.sh, да и canhw_CAN_LINES_LIST.m4
тоже.
Хотя уж там-то при желании можно разделить без особых проблем -- чисто по принадлежности к специфическим установкам (кстати, даже проще, чем в конфигах).
V4HDIR=$(PRJDIR)/../hw4cx
указывается в куче мест раздельно --
в {drivers,screens,xmclients}/Makefile. А должно бы быть единое
прямо в PrjRules.mk (как это, кстати, в v2'шных подпроектах было).
Сделано -- перенёс в PrjRules.mk.
Наполнение пока отсутствует, только сделал Trove-категорирование.
17.05.2015: еще энное время назад было закомментировано упоминание о SMP4TD, а сегодня добавлено упоминание m4.
Кроме того, из описаний channels и devtype убраны ":ХОЗ.АПП.КАН" и всё связанное с ними.
19.05.2015: и еще убран параметр "ТИП" из раздела cpoint (ума не приложу -- о чём я думал, когда его туда вводил?).
01.11.2016: часть файлов touch'нута еще 12-10-2016 (но пока пусты), а сегодня делаем реальное наполнение.
02.11.2016: пишем дальше:
И правила создания своих Makefile'ов там отсутствуют как класс.
03.11.2016: продолжаем:
07.11.2016: чуть-чуть:
08.11.2016: далее:
09.11.2016: и еще:
PREFIX=
!
Сделано.
НЕ сделано -- подробное описание флагов. При надобности можно будет добавить раздельчик.
10.11.2016: и:
30.11.2016: мелочи:
01.12.2016: эх!
Собственного Makefile в ней нет, а только строка
LOCAL_BCKPFILES=etc/*~
уровнем выше.
В нём правила ротации для /var/tmp/*log.
Задеплоен на vepp4-pult6, посмотрим, как ротейтнется в выходные.
09.02.2017: модифицируем файл.
4access.log 4cxsd.log 4drivers.log
Смысл в том, чтобы не затрагивать:
tee
, а
ему ну никак не сказать "переоткрой". Я даже исходники посмотрел -- нет там
никакой спрятанной реакции на сигналы (кроме
--ignore-interrupts
, нам бесполезной).
Модифицированный файл задеплоен на ring1.
13.02.2017: проверено, работает. На обоих.
Конкретно на vepp4-pult6 оно "rotate'нуло" и cx-starter.log, но fuser показывает, что tee держит открытым на запись старый cx-starter.log.1 (как и ожидалось).
Считаем за "done".
16.02.2017: и на linac1 тоже положен (там RH-7.3).
25.03.2019: ещё летом обнаружилось, что логи на пультовых серверах (canhw - RHEL 7.4) почему-то не logrotate'ятся. На прошлой неделе поставил систему (RHEL 7.6) на машину xweld (комп для проданной на экспорт ЭЛС) -- то же самое.
Стал разбираться. Если запустить logrotate "руками" -- по команде из
/etc/cron.daily/logrotate, только с добавленным ключом отладки
"-d
" --
/usr/sbin/logrotate -d -s /var/lib/logrotate/logrotate.status /etc/logrotate.conf
-- то получаем сообщение
error: skipping "/var/tmp/4access.log" because parent directory has insecure permissions (It's world writable or writable by group which is not "root") Set "su" directive in config file to tell logrotate which user/group should be used for rotation.
А в /var/log/messages попадает лишь малопримечательное и неконкретное
logrotate: ALERT exited abnormally with [1]
Потом оказалось, что в мыле у root'а (/var/spool/mail/root) детальное сообщение тоже есть. Но кто ж догадался бы, что туда надо заглянуть...
Пытаемся разобраться в причине и найти пути решения.
- added "su" option to define user/group for rotation. Logrotate now skips directories which are world writable or writable by group which is not "root" unless "su" directive is used.
Ведь sticky bit же на директории есть -- какие вопросы?!
...с другой стороны -- таким вектором атаки воспользоваться проблематично: чтобы что-то сказать logrotate'у, надо иметь возможность положить файлы в /etc/logrotate.d/, а это уже права root. Разве что рассчитывать на уже ИМЕЮЩИЕСЯ там файлы с шаблонами, под которые подогнать своё. Хотя всё равно маловероятная экзотика.
Так что пред-предыдущее соображение на тему "Ведь sticky bit же на директории есть" авторами вообще никак не рассматривалось.
И увы -- не видно там ничего типа "используй uid/gid от файла".
findNeedRotating()
, где
выполняется проверка на тему "какие права у parent directory", оная проверка
НЕ делается при взведённом флаге LOG_FLAG_SU
.
sscanf()
, в
котором присвоение полям suUid
и suGid
делается
ТОЛЬКО при указанности этих значений.
Т.е., можно написать сокращённо -- "su root" и даже просто "su", о чём в man'е не сказано.
getpwnam()
и getgrnam()
.
Такая строчка де-факто просто отменяет то нововведение, возвращая поведение logrotate к версии 3.7 (точнее, до-3.8.0).
Итак, в режиме отладки (с ключом "-d
") работает, в "боевом"
режиме с добавленной спецификацией "daily" -- тоже, теперь осталось
подождать до утра следующего воскресенья, чтобы убедиться, что работает как
надо везде.
26.03.2019: да, на пульту тоже работает -- на canhw; а на cxhw умудрился указать "su 0 0" вместо "su root root" -- оно и ругается, но уже только в мыло, а в /var/log/messages никакого "ALERT" нету.
Исправил.
27.03.2019: а теперь и на cxhw тоже работает.
(Да, реально работы начаты еще пару недель назад, 13-03-2018, с f4226, но осознание "глобальности" пришло сегодня :).)
26.03.2018: поехали:
Сюда понадобится новый сервер. Следующий свободный -- :25; очевидно, canhw:25 -- хоть железо комбинированное, CAMAC и CAN, но коль скоро CAN есть, то обращаться нужно из canhw.
Это в сервер cxnhw:19 -- резонатор в нём уже живёт.
Тут та же "странность": сейчас ВСЯ клистронная электроника в CAMAC, но поселено будет в canhw:19 из-за того, что резонатор на CAN.
Есть один нюанс: содержимое гусиного окошка "Phase and Frequency Control" надо будет разделить: 4 строчки верхней таблицы ("Фазовращатели") разнести по окнам клистронов, а "Частота генератора [кГц]" -- очевидно, в окно RFSYN.
Dev \ N 1 2 3 4 Г0603 2 2 2 2 Г0401 4 4 4 4 <-Г0604 В0612 6 6 6 6 Ц0642 8 8 8 8 Р0610 10 10 10 10 Р0610 11 11 11 11 ADC200 13 13 13 13 Ф4226 15 15 Ф4226 16
Т.е., позиции везде одинаковые, а различия лишь в наличии/количестве Ф4226 (они, по уверению Фролова, не особо нужны) и в Г0604 на 4-м клистроне вместо Г0401 (но они оба вроде ГВИ8 -- просто ГВИ8М и ГВИ8, соответственно).
30.03.2018: а вот не факт. Чтение описаний показывает, что девайсы всё же изрядно разные: и по управлению дискретой (в g0604 оно, похоже, не программное), и по способу чтения текущих уставок (в g0604 чтение очень замутное).
30.03.2018: поговорил с Цукановым. Ответ: 1. Девайсы программно разные, одним драйвером не обойтись. 2. У g0604 дискрета ставится перемычками. 3. Чтение такое замутное потому, что там не регистр читается, а прямо меряется сигнал на выходе (потому, очевидно, и номер указывать нужно -- на какой выход скоммутировать измеритель). 4. Если ГВИ8М (g0401) еще как-то поддерживается, то старый ГВИ8 (g0604) -- нет, и ремонтировать их никто не будет. 5. А вообще лучше их заменить на козачиные/быковские CGVI8/CGVI8M.
15.09.2018: подшаманил драйвер g0401 так, чтоб он мог работать с g0604. Подробности -- в егойном разделе.
27.03.2018: ясно, что можно делать клистронокасательную набивку devlist-canhw-19.lst. Идеи/принципы:
27.03.2018: теперь касательно rfsyn, впуска/выпуска, задающего Куркина:
30.03.2018: ОДИН из D16 -- "D16П": у него есть некий счётчик, сколько запусков дать, и после отработки этого количества он останавливается.
27.07.2018: а K4X1, оказывается, тоже знаком -- драйвер frolov_k4x1_2x2x1_drv.c был сделан еще 17-04-2017 (о чём никаких записей нет); похоже, для ADC200 кикеров во 2-м зале (судя по devlist-cxhw-18.lst и v5adc200_kkrs.subsys).
30.07.2018: но тот K4X1, что стоит в крейте RFSYN -- другой, именно 4->1, и на него пока ни драйвера, ни толкового описания.
Dev N D16M1 2 D16 4 Ф20 6 K4X1(2x1) 7 K4X1(2x1) 8 adc200 10 adc200 12 "TTL" 13 K4X1(2x1) 15 D16 16 D16M1 18 Ф20 20 И0634 21 <- индикатор магистрали K0632 22 <- CAN-driver
30.03.2018: Ф20 и "TTL" -- просто формирователи, они НЕуправляемые.
Кстати, не будет ли целесообразным в эту же CAN-линию посадить и управление резонатором? Стойка-то просто соседняя, а зацеплена она аж за контроллер cangw-rst1, на котором пол-магнитной системы висит.
28.03.2018: приступаем к созданию v5k5045_drv.c. Пока делаем "скелет", чтоб собирался.
02.04.2018@утро-пультовая: сделан g0401_drv.c.
07.08.2018: чуть задним числом, но уж пусть хоть так будет:
Теперь надо деплоить конфиг целиком и делать некоего клиента, чтоб управлять всем сходу.
Правда, это разные режимы работы (впуск и выпуск кикерами), так что вместе/одновременно они работать не будут. Но как изобразить связи древовидно -- всё равно вопрос.
...впрочем, неясно, насколько он доделан и в v2 -- там шибко много стенаний от 29-08-2013, хотя в апреле-мае 2006 много наработано.
21.08.2018: его содержимое сделано. Там всё просто --
макрос ONE_KLS()
, объявляющий все устройства (в т.ч. f4226 --
условно, в указанном количестве), сводящий их в cpoint-иерархию и
объявляющий устройство klsN (на эту иерархию нацеленное).
13.10.2018: в devlist устройства (но пока НЕ cpoint'ы!) добавлены. Вот, кстати, список устройств:
Dev N Ц0642 2 # Работает 1 канал В0612 4 # Управляющий регистр Ф4226 9 # хбз зачем Ф4226 11 # Не используется Р0610 13 # Входной регистр для чтения блокировок Ф20 15 # Пассивный Акт.Дел. 17 # Пассивный (хоть и "Активный делитель" :D) Инд.Питания 18 # Пассивный Инд.Магистр.21 # Пассивный
16.10.2018: далее:
(У раздолбая Гусева включенное(красное) положение слева, выключенное(зелёное) справа -- людей путать...).
Осталось проверить функционирование на живом оборудовании.
В частности, узнать: надо ли при загорании блокировок делать выключение (ic.linCNV.swc:=0)?
25.04.2018:
-s
и на них натравлены скрины. Найдено и исправлено несколько
косяков.
26.04.2018:
-p -udp --sport 8012
" клиенты НЕ получают ответов
на резолвинг. Кроме того, почему "systemctl restart iptables" приводит к
долгой паузе в получении пакетов: неужто недостаточно первой строчки про
--state RELATED,ESTABLISHED? 04.04.2025: да,
"при отсутвии..." не работает потому, что отправляется на один адрес
(broadcast), а ответ приходит с другого (unicast). Соображено это было ещё
11-06-2021 ниже, а сегодня сообразил ещё раз. Что касается долгой паузы --
видимо, потому, что таблицы соответствия (куда были какие пакеты отправлены)
сбрасываются при загрузке правил-таблиц файрволлинга.
04.04.2025: да, именно в этом и дело -- сегодня проверено
на CXv4, где без "--sport 8012
" не работало, а с его
добавлением заработало.
--sport 8012
" я сейчас нигде в
конфигах (всякие notes/*Activity*) не нашёл.
Так что вопрос -- где же именно я те эксперименты делал и добавлял лишнюю строчку в настройки файрволлинга.
Возможно, на ep1-berezin2, но он сей момент не пингуется, как и tower.
А на пультовых машинах вообще список фильтрации пуст.
ЗЫ: а у ЧеблоПаши проблема, похоже, в специфике работы EPICS'а:
Вот и всё -- "связь не работает!!!111".
11.05.2018: начата weldcc.subsys -- для начала копированием из Вероны.
12.05.2018: продолжаем weldcc.subsys.
Ради чего пришлось изготовить tabber_cont.
14.05.2018: продолжаем -- помимо панели "ЭЛС" (с которой будет отдельная долгая возня) остались 2 штуки магнитных системы.
Плюс добавлена ручка, глядящая на rate канала LD (Линза Динамическая).
28.03.2019: за прошедшие пару недель "маленькая" сварка, которая weldcc на теперь экс-bike, была запинана для работы. Только в варианте "на экспорт" -- на машине xweld.inp.nsk.su, сервер xweld:50, скрин xweld.subsys (на будущую "нашу" (p8b2?) просто сделаем копию).
Кратко результаты:
Но там в скрине отсутствовали указания границ (-1.5-+1.5) и шага (0.01) -- добавлены.
05.04.2019: а вот нифига: ДРАЙВЕР-то weldproc_drv работал, а всё вместе -- не совсем: оно НЕ включало тумблер "Работа" в панели "Управление пучком" (блок frolov_bfp2). Косяк был туп и тривиален: в devlist'е при weldproc'е в at_beg= и at_end= стояла запись 1 и 0 (соответственно) в канал stat_wkalwd. Но это ведь канал ЧТЕНИЯ, и никакого эффекта оные записи иметь не могли. После исправления на должную отправку 1 в wrk_enable и wrk_disable -- всё заработало.
Сейчас не вполне ясно, к чему они относятся: к DDS'ам как устройствам, или же к конкретной установке.
Поэтому внесены в devlist-xweld-50.lst, в виде "замещающих cpoint'ов" -- в виде
cpoint dds.ftw0 dds.ftw0 143
Поэтому скрин теперь ссылается на каналы не vac.inprbN, а vac.ilkN, имеющие вид
cpoint vac.ilk0 vac.inprb0 -1 -1
(да, дуплет R=-1,D=-1 даёт инверсию -- 0 превращается в 1, а 1 в 0).
Файл с результатом этих расследований положен в 4cx/src/doc/etc/rhel7_Desktop_CX-starter.desktop.
Но самое обидное: как выяснилось, УЖЕ имевшийся аналогичный файл от RHEL6 с romeo -- сейчас он помещён в 4cx/src/doc/etc/rhel6_Desktop_CX-starter.desktop -- тоже подошёл бы после правки путей.
02.07.2018: приступаем.
Но никакой поддержки такого поведения нет:
CdrProcessKnobs()
-- но там и несложно, просто пройтись по
всем),
Так что увы, не прокатит.
...он же потом подойдёт и для liu'шного beamprof_knobplugin'а, который сделаем "по образу и подобию".
03.07.2018@утро-дома: творческий кризис: никак не могу толком решить, как именно организовывать работу/внутренности этого knobplugin'а. Чтоб он работал с разными вариантами, да и вообще просто -- КАК?
03.07.2018@дорога-на-работу-мимо-НИПС-и-ИПА: а может, дело в том, что это стоит делать не knobplugin'ом, а драйвером?
Указывать в формате вроде lineN=TABLE_IDX:SCALAR_NAME?
На работе: или у нас сейчас таблицы формируются из индивидуальных по-канальных векторов, а не одним массивом?
Вечером: точно -- судя по
advdac_cankoz_table_meat.h, KOZDEV_CHAN_OUT_TAB_ALL
сейчас совсем не работает, а используются исключительно по-канальные
KOZDEV_CHAN_OUT_TAB_n_base+n. Так что никакие номера не нужны, а просто
поштучные имена.
Позволить указывать формулы?
@вечер-дома: хотя конкретно тут можно схалтурить: перемножать все коэффициенты и отдавать наверх 1 пару -- воспользовавшись тем, что сдвиг нуля отсутствует.
04.07.2018@обед: а можно и еще проще -- считать, что этот драйвер всегда работает в микровольтах, а калибровки навешивать в devlist'е (совпадающие с оными для скаляров). Ведь всякие драйверы ist/v1k/v3h/... работают именно так.
04.07.2018: к вопросу о синтаксисе. Безотносительно того, ЧТО использовать -- knobplugin или драйвер -- разумным выглядит выбрать указание "маппингов" каналов из 2 вариантов:
cpoint BASENAME { vector DEVICENAME.out_tabN scalar DEVICENAME.outN }
Замечание:
...хотя, глядя на нынешние devlist'ы, где все магнитные системы идут едиными списками с калибровками -- неа, такой подход менее удобен.
После обеда: а впрочем, если использовать драйвер, то нафиг тут калибровки -- решили ж уже, что драйвер weldproc работает в микровольтах, а калибровки навешиваются на его каналы одинаковые вместе с каналами реального ЦАПа.
Таким образом, в любом из (1) или (2) количество "строк" получается автоматически -- просто смотрится номер последней указанной.
Хотя формально есть и вариант #3:
Конкретно сейчас наиболее симпатичным представляется вариант 2.
04.07.2018@вечер-дома: всё же наиболее симпатичным представляется вариант с драйвером. Так что быстренько вделываем в vdev.c поддержку векторных каналов.
06.07.2018: приступаем к изготовлению weldproc_drv.c.
strtol()
вместо вычитания '0').
sodc2ourc_mapping[]
используется лишь
как источник для заполнения privrec_t.this_mapping[]
, ...
SODC_PLACEHOLDER
=0, чья ячейка в карте смотрит на
cur0._devstate и более ничего не делает).
07.07.2018: а ведь был еще один вариант -- сильно проще: поскольку каналы в таблице карты идут парами -- curN,tabN -- то можно просто указывать vdev'у
num_sodcs = SUBORD_NUMCHANS - (WELDPROC_max_lines_count - numlines) * 2
16.07.2018: а вот фиг: в sodc2ourc_mapping[]
не перечисление, а индексация по номеру sodc. Поэтому чтоб идея с
обрезанием длины сработала, прямо НОМЕРА sodc'ов должны идти парами: curN,
tabN. А как сейчас -- фиг, я попробовал, срезается при попытке
зарегистрировать канал cur4 у 4-канального (т.е., с разрешенными 0-3).
16.07.2018: драйвер сделан было в основном еще с неделю назад, а потом происходила отладка, в первую очередь обусловленная сложностями с реализацией таблиц в драйверах козачиных устройств.
EBW_STATE_ACTIVATE
.
IsAlwdGO()
,
проверяющий путём проверки значения ЦАПова канала DO_TAB_START (==0 --
значит, можно) -- НЕ вернёт true, вследствие рассинхронизации с advdac'овым
состоянием.
DAC_STAT_EXECING
и, считая это за "detected
START", переходит в TMODE_RUNN. А в этом состоянии делать запуск уже низя,
вот значение DO_TAB_START и равняется 2.
SndDupl()
.
16.07.2018: допиливание:
Поэтому переделано на микросекунды, сам канал переназван из "time_ms" в просто "time", и на него отдаётся коэффициент 1000000 -- т.е., на клиентском уровне вообще секунды (как и у козакотаблиц, кстати).
17.07.2018: финализация.
(В weldcc.subsys такой роскоши пока нет -- мож, потом подпеределаем.)
Проверено -- работает.
Одно только странно: время между исполнением at_beg- и at_end-формул на 1
секунду больше, чем время исполнения таблицы. Проверено по
print_str
, печатающей время (хотя и всего лишь по секундам).
Теперь всё -- дальше проверять на живой машине.
20.05.2019: да проверено уже с месяц-два как (на xweld) -- работает.
Так что можно считать раздел за "done".
08.06.2018: добавлен им отключатель -- переключателем
в правом верхнем углу окна. По умолчанию (=0) алармы разрешены, но можно
выключить (=1). Делается это через канал %silent
, потому
локально для программы. Собственно обращение внимания на отключенность -- в
макросе THERMLINE()
, в определении _alm
.
06.08.2006: так что -- вводим новый класс, именуемый "newi", и присваиваем его всем неисполненным пунктам ("newi", а не "new" -- чтобы было 4 символа, как в "done). А цвет для него выбран светло-розово-фиолетовый #ffccff (подсмотрен на www.w3.org); соответствующее определение по умолчанию закомментировано.
(Цвет, правда, перекрывается с "location" (вот так весело вышло!) -- видать, тырил из одного и того же места, и в схожем состоянии ума; но это несмертельно -- поскольку оцвечивание "newi" является мерой редкоприменяемой и врЕменной.)
06.08.2006: скопировал.
10.05.2020: прогнал. Судя по тем местам, где были косяки -- предудущий раз эта операция делалась не позже ноября 2018г.
В том числе было несколько ругательств на незакрытые DIV, которые исправлять было сложно: проблема в том, что валидатор показывает только точку, в которой он понял, что тэг DIV не закрыт, но он НЕ показывает, где расположен сам незакрытый тэг (вот это уже косяк в валидаторе -- тот же GCC в таких случаях сообщает, где (по его мнению) началась "вторая точка", касающаяся проблемы -- например, где именно была открывающая кавычка).
Было найдено такое решение: выполняем команду
и там ищем строку, номером максимально близкую к указанной валидатором (но не меньшего номера, а большего), и от неё глазами просматриваем вверх. Обычно такие незакрытые DIV хорошо видны -- потому, что в основном идут пары "DIV"..."/DIV" (изредка -- вложенные), и нарушения структуры в виде пары открывающих подряд (без закрывающего "/DIV" между ними) бросаются в глаза.grep -wn DIV IDEAS/bigfile-0002.html|less
—
,
“
, ”
(они реально из
CP1251/CP1252, а не из ISO8859-1/UNICODE). Надо бы поменять.
27.05.2020: главный вопрос был -- на ЧТО? Чтобы это не были дикие 4-символьные коды (которые фиг запомнишь). OK, выяснил:
СтарыйКод | НовоеИмя | Символ |
---|---|---|
— | — | — |
“ | “ | “ |
” | ” | ” |
Сделано.
Собственно, в чём-то она напоминает мою собственную диссертацию (7+ годами раньше :-)) -- там есть обзор нескольких разных систем управления (EPICS, DOOCS, MADOCA, TANGO) и нескольких интерфейсов подключения железа (VME, cPCI, PLC, microTCA).
Но суть там в ином -- очень много слов про WebSocket.
Отличия в том, что:
Как всё-таки происходит генерация событий (на любой канал можно подписаться? или драйвер специально генерит event'ы?) -- я не понял.
В event'ах могут присылаться значения не только скаляров, а практически любого размера каналов (хотя какое-то ограничение сверху есть, определяется оно ZeroMQ).
Еще вывод от меня: похоже, там фреймворк многое никак не специфицирует и не обеспечивает, отдавая на откуп конкретного программиста то, что в EPICS и CX является функционалом самого фреймворка (та же отдача данных по подписке -- тут тупо вызовы процедуры "читай атрибут").
25.07.2019: и ещё пообщался с Сенченко на темы устройства TANGO вообще и в частности доступа из клиентов.
Единых правил что для чего использовать -- нету (это я вроде и в мануале встречал), но в общем случае примерно так:
Поэтому модель с делением команды на 2 канала -- инициатор (куда писать) и результат (который мониторировать) -- не вполне подходит: клиент не сможет быть уверен, что получил ответ на СВОЮ команду, а не на какую-то другую.
А из нетривиальных типов есть только массивы, одно- и двумерные. Причём одномерные являются частным случаем двумерных.
Для всего же иного там используется понятие "encoded": это дуплет из строки и цепочки байт. Строка содержит "схему кодирования", а далее байтами передаётся информация. И предполагается, что оба участника эту схему должны понимать.
Их вроде как можно создавать в конце init'а, и потом некий(е) метод(ы) устройства будет получать указание на запись и чтение таковых.
И, видимо, методу передаётся ИМЯ атрибута, так что он может сопоставить ему номер канала (ЦАП 0-7). Для чего можно использовать хоть "словарь" (хэш-таблицу), хоть тупо брать последний символ имени.
На вопрос "Саша, а можешь предположить, почему в TANGO всё сделано настолько иначе, чем у прочих (EPICS, NI, ...), что нет парадигмы каналов?" ответ был такой:
От меня: ужас!!! Как это поделие, реализующее несколько странную модель (на мой взгляд -- категорически неадекватную, там же нет кучи необходимого функционала!), умудрилось завоевать такой огромный кусок "рынка" СУ физических установок?!?!?!
28.07.2019@вечер-дома: судя по документу tango_92.pdf, ПОЛНЫЙ синтаксис имени устройства --
[//FACILITY/]DOMAIN/CLASS/MEMBER
В идеале, этот синтаксис бы тоже надо поддерживать (хотя что именно означает это опциональное "//FACILITY" -- хбз).
02.08.2019: UPD: Сенченко говорит, что он вариант с "FACILITY" раньше не встречал и он считает, что "На самом деле это host:port.".
02.08.2019: ещё кусочек информации от Сенченко:
Это в ответ на мой вопрос "Есть ли где-нибудь пример как быстро запустить минимальный TANGO-сервер, с какими-нибудь "синтетическим" устройством, чисто для освоения/тестов?".
Кстати, я посмотрел -- в дистрибутиве tango-9.2.5a есть директория cppserver/tangotest, и там действительно что-то собрано.
18.10.2019: ещё немного пообщался с Сенченко, на этот раз по поводу pipe'ов (про которые он на семинаре 10-10-2019 говорил, что они (введённые в TANGO-9) и есть некий аналог структурных типов.
Далее -- заметки в свободной форме, НЕ цитаты, но как я его понял (и что ещё раньше прочитал сам).
22.10.2019: точнее, "не совсем" -- см. следующие пункты.
22.10.2019: но типы-то -- только стандартные танговские "атомарные", т.е., скаляры и 1D и 2D-массивы. Следовательно -- вложенных структур тут сделать нельзя, в отличие от EPICS!
Как говорит Саша, они вроде собираются/пытаются сделать возможность асинхронной работы, но что-то там это как-то плохо ложится на архитектуру (из-за объектности, что ли...).
Т.е., если несколько клиентов подряд запросят чтение, то
Тут не очень понятно: как всё-таки устроено чтение?
Это следует из того, что там можно отдельно ловить уведомления, а потом отдельно запрашивать собственно "чтение данных".
В частности, моё наблюдение при попытке изготовить cda_d_tango.c: в tango'вской клиентской библиотеке НЕТУ локального кэша значений, совсем. Вот что пришло из сети, в пакете -- то клиенту и отдают, и после этого "отдавания" (когда пакет выкидывается) -- данные библиотекой забываются.
А вот в СЕРВЕРЕ -- похоже, последние значения хранятся.
"Мультиплексирование" -- важный аспект функционала EPICS и CX, когда результат выполнения одной операции (чтение, обновление) раздаётся ВСЕМ заинтересованным клиентам.
Замечание: бОльшая часть вышенаписанного -- со слов Сенченко, но сам он pipe'ами не пользуется и, возможно, что-то понимает не вполне верно.
24.07.2019: процесс несложный, но для этой штуки нужны "пререквизиты" (зависимости) omniORB и zeromq, доступные в виде RPM.
65888551 байт -- да-да, 64MB! (EPICS: base-3.15.6.tar.gz -- 1620KB, base-7.0.2.2.tar.gz -- 2624KB; CX -- 9MB всё вместе (v2, v4, sw4cx, hw4cx, v2hw, IDEAS, ...).
27.07.2019: просто развёрнутые исходники -- 86812KB (=86MB). Зато после сборки -- 1304604KB, т.е., 1.3GB!!!
27.07.2019: в инсталлированном виде съедают всего лишь ~18MB.
./configure; make
". Собиралось минут
10-15 (на E3-1285Lv4 и i7-8700; я ждал более долгого процесса -- видимо,
значительная часть .tar.gz'а -- документация и тому подобное, не
участвующее в сборке).
09.08.2019: оказывается, конкретно в CentOS-7 надо
было в configure
указывать ещё ключик
"--with-mysqlclient-lib=/usr/lib64/mysql
". Иначе оно ругается,
что не может собрать что-то на тему mysqlclient.
-L
не видно).
09.08.2019: просто поток информации, по мере разбирательства.
-ORBendPoint giop:tcp::10001
Тут "10001" -- это порт, а всё остальное -- предопределено: "-ORB*" -- опции, передаваемые ORB'у, в частности, "endPoint" -- что слушать; "giop" -- название протокола то ли CORBA вообще, то ли конкретно omniORB'а.
Нарыл я этот синтаксис случайно, каким-то гуглением, но он описан в документации по omniORB, раздел "8.6 Transports and endpoints".
TangoTest instname -ORBendPoint giop:tcp::10001 -nodb -dlist a/b/c,x/y/z
Тут "instname" -- имя экземпляра (хотя нафиг оно при "-nodb" -- загадка), а a/b/c и x/y/z -- имена устройств, которые надо создать.
Вопрос только в типизации: откуда возьмётся информация о типах? Зависит от конкретного API динамических атрибутов. Если возможна "динамическая типизация" -- то на лету можно выяснять тип исходного канала. Если НЕвозможна -- то только конфиг заранее создавать (список бриджуемых каналов, тогда там и маппирование сразу делать (такое-то TANGO-имя маппируется на такой-то foreign-канал)).
01.09.2024: уже несколько дней обмозговывал саму ту мысль и испытывал сомнения: чисто технологически -- возможно ли?
И вообще клиент имеет право сделать
DEVICE_PROXY->attribute_list_query()
(и подобные) для
получения ИСЧЕРПЫВАЮЩЕГО СПИСКА АТРИБУТОВ, после чего, не найдя в этом
списке искомого, посчитать "нету такого". Конкретно
epics2tango_gw.cpp именно по такой идеологии строится.
Разве что считать именем исключительно компонент "ATTRIBUTE", в котором уже могут быть ':' и '.': например, "GATEWAY/TO/EPICS/epics:name:of:record.val".
И оттуда как-то не видно способа создавать атрибуты по ходу работы, а только при старте --
Pogo generates method(s) named DynAttr::add_<Dyn attr class name>_dynamic_attribute(string att_name) which do this job for you. You have to call these methods according to your needs in another Pogo generated method named DynAttr::add_dynamic_attributes() which is executed at device creation time. In our example, in this method we have to:
Andy Gotz:
Hi Dmitry. I confirm that the goal is to make the protocol on the wire
pluggable i.e. to support other protocols in addition to CORBA. For now
the functionality of the current library has been specified in a set of
RFCs so that the future version will be functionally compatible with the
existing versions (see https://github.com/tango-controls/rfc). The next
step is to make a CORBA-free prototype. However as you pointed out this
project is advancing very slowly due to lack of resources. Ideally we
would need 3 to 5 people dedicated to this task to advance at a reasonable
pace. Would you be interested in helping? In the meantime 9-LTS is being
well maintained and is the best choice.
Andy Gotz:
Could be added but is not planned yet. Maybe pytango does this already?
...and later Thomas commented that lack of data conversion is because
how CORBA works.
Так вот: он рассказал (слайд 11 презентации), что для ZMQ-реализации событий всё равно используется код "представления форматов данных" -- маршаллинга/демаршаллинга -- от CORBA. Причём именно прямо КОД из CORBA-библиотеки, а не просто формат. Тут уж я не удержался и задал вопрос:
Мда... Ну чего они так прицепились к этой КОРБЕ... Ведь и изначально она была явно плохим решением, и в дальнейшем от неё сплошные проблемы, а вот поди ж ты...
(С неделю назад пришло в голову) Я очень не люблю TANGO, однако именно оно -- точнее, какая-то презентация, видимо, Andy Gotz'а, то ли на ICALEPCS-2001, то ли на PCaPAC-2002, натолкнула меня на мысль уметь использовать CORBA, что, в конечном итоге, и привело к идее plugin-frontend'ов.
Оценит свои потребности, прикинет бюджет и построит домик деревянный или кирпичный. И будет им пользоваться.
"Есть такой офигенный материал -- бамбук! Лёгкий, красивый, экологичный, а при постукивании звучит приятно!" И сделает домик из бамбука.
Только потом окажется, что домик этот продувает, теплоизоляция никакая, на стены даже полочку не прибить.
Но домик-то нужен -- и начнёт увлекающийся человек его апгрейдить: обошьёт снаружи вагонкой, понапихает пакли или пенопласта для утепления, воткнёт внутрь реек, чтоб можно было что-то крепить...
...однако, хоть бамбука уже почти и не видно, это по-прежнему останется тот же бамбуковый домик, просто чуток пропатченный.
В результате оказалось, что CORBA для СУ подходит очень фигово, и начали приделывать к TANGO разные костыли, да только уши CORBA до сих пор торчат там отовсюду.
И даже что-то на эту тему было сделано: имеется файл
work/ucam/utest.c от 29-05-1998, содержащий в т.ч. вызов
ucam_naf()
. Насколько там оно было сделано далее в сервере и
транспьютере -- надо отдельно рыть/вспоминать.
01.09.2024: СТОП!!! "pipes" или всё-таки DevEncoded ("тэг(строка)+данные(байтовые)")?
Т.е.: исходная идея была аналогичной, причём в CX/UCAM оно появилось ещё сильно ранее появления TANGO как такового, но в CX было решено от такого подхода отказаться, в пользу полноценной модели каналов не-скалярного содержания (хотя и огрвниченных моделью "вектор элементов любой байтности плюс массив int32-мараметров), а в TANGO так и оставили этот RPC-образный механизм.
(Недавно читал статью Бак, Батраков, et al "КОМПЛЕКС ЦИФРОВОЙ ОСЦИЛЛОГРАФИИ УСКОРИТЕЛЯ ЛИУ-20" в ПТЭ-2021-N2, и там на стр.12 tango'вские "pipe" обозваны "каналами" -- вот тут у меня и щёлкнуло: "каналы" <-> "прямые каналы".)
Так вот: понятие "interpose interface" там существует только конкретно в asynDriver (о чём можно почитать в описании "asynDriver: Asynchronous Driver Support), а вовсе не как общая концепция в EPICS. Что подтверждает и гугление -- словосочетания вроде "interpose layer" и "interpose interface" в применении к EPICS встречаются только совместно с asynDriver.
Так что -- бобёр, выдыхай! :) И механизм layer'ов у нас хорош и адекватен, и sendqlib наш (с его концепцией "портов") обеспечивает достаточную для наших целей функциональность.
26.08.2019: немного информации от Лёши Герасёва:
По поводу драйвера как сущности в EPICS'е - кажется, её нет. Насколько я понимаю, EPICS оперирует только рекордами, а отображать рекорды в экземпляры драйвера и обратно - это задача DeviceSupport'а, и как его напишешь - так и будет отображаться.
Весело, да? А ЧеблоПаша настаивал, что "на уровне КЛИЕНТОВ в EPICS драйверов нет". Но их, похоже, в обычном смысле там просто нет вовсе (как я и раньше считал ;-)).
27.08.2019: вот читаю я эту презенташку, и удивляют 2 вещи (которые надо делать в "configuration function"):
Т.е., "объект" сам не создаётся, а нужно его аллокировать.
Ну это ладно -- вопрос гибкости, да и в CX тоже изначально так было. Да и во времена DOS (когда EPICS создавался) иных вариантов не было.
Вот это непонятно совершенно. Схрена ли драйверу (который даже НЕ PnP, как в ядре Linux!) одного устройства нужно знать сразу обо ВСЕХ экземплярах такого устройства?
Чуть позже, по мере чтения: причина, похоже, в том, что само "ядро IOC'а" менеджментом ресурсов НЕ ЗАНИМАЕТСЯ!
Какой радикальный контраст с CX'ной моделью, где:
31.12.2020: что нагуглилось на тему "EPICS driver writer's guide":
Подробная, 73 слайда (плюс "шаги" внутри слайдов). На примере VME.
(Чуть позже: и да, это на неё давал ссылку Лёша Герасёв полтора года назад.)
Кратенькая, на 16 слайдов, но полезная. Тут уже на примере PCI (хотя "пример" условный -- конкретики там 0, а больше предупреждений о компиляторовых оптимизациях).
...и это, видимо, и всё... Возможно, полезным будет какой-нибудь "EPICS Application Developer's Guide", вроде
А вот что для меня было совсем новым (открытием! :D), так это аббревиатура GTACS, встретившаяся в главе "Driver Support" оного Developer's Guide:
It is not necessary to create a driver support module in order to interface EPICS to hardware. For simple hardware device support is sufficient. At the present time most hardware support has both. The reason for this is historical. Before EPICS there was GTACS. During the change from GTACS to EPICS, record support was changed drastically. In order to preserve all existing hardware support the GTACS drivers were used without change. The device support layer was created just to shield the existing drivers form the record support changes.
Гугление по "GTACS EPICS" привело к статейке
...а -- во, 1994: NIM-A, Volume 352, Issues 1-2, 15 December 1994, Pages 486-491.
02.07.2024: чисто из любопытства решил погуглить "gtacs site:anl.gov", первым же результатом наткнулся на ту же самую Chapter 10: Driver Support, но на этот раз в том же самом первом абзаце обратил внимание на другую часть:
It is not necessary to create a driver support module in order to interface EPICS to hardware. For simple hardware device support is sufficient. At the present time most hardware support has both. The reason for this is historical. Before EPICS there was GTACS. During the change from GTACS to EPICS, record support was changed drastically. In order to preserve all existing hardware support the GTACS drivers were used without change. The device support layer was created just to shield the existing drivers form the record support changes.-- вот и объяснение существования оного "device support": причина его существования скорее историческая (и даже сиюминутно-коньюнктурная), а не архитектурная. Кстати, там дальше ещё пояснение:
Since EPICS now has both device and driver support the question arises: When do I need driver support and when don't I? Lets give a few reasons why drivers should be created.(текст скопирован прямо из HTML'ного исходника, с тэгами и плэйнтекстовой "разметкой")
The only thing needed to interface a driver to EPICS is to provide a driver support module, which can be layered on top of an existing driver, and provide a database definition for the driver. The driver support module is described in the next section. The database definition is described in chapter "Database Definition".
- The hardware is actually a subnet, e.g. GPIB. In this case a driver should be provided for accessing the subnet. There is no reason to make the driver aware of EPICS except possibly for issuing error messages.
- The hardware is complicated. In this case supplying driver support helps modularized the software. The Allen Bradley driver, which is also an example of supporting a subnet, is a good example.
- An existing driver, maintained by others, is available. I don't know of any examples.
- The driver should be general purpose, i.e. not tied to EPICS. The CAMAC driver is a good example. It is used by other systems, such as CODA.
20.02.2021: особо углубляться в собственно ЧТЕНИЕ и ИЗУЧЕНИЕ -- почему-то лень, но хоть ссылки на информацию пусть тут будут.
Гуглилось по 3 фразам: "epics asyndriver tutorial", "epics streamdevice tutorial", "epics asyndriver streamdevice relationship". Сцылки:
20.02.2021: кстати, МОИ мысли насчёт всего этого безобразия:
Asyn же был добавлен задним числом, в попытке исправить ситуацию, и получился монструознейшей штукой.
Т.е., все эти навороты -- из-за дикого разбаланса, происходящего от неадекватной обслуживаемому железу модели ПО: работа драйверов (с железом) перемешана с более высокоуровневой ("БД").
Но также нашлись тексты:
А ещё по ходу встретились пара статей, оказавших в своё время большое влияние на осознание мною того, как бы должна быть устроена СУ в условиях со-существования разных СУ и необходимости взаимодействия между ними. Это не столько про EPICS, а скорее "по СУ вообще", но, поскольку история, то оставим здесь:
Это там есть картинка с "покрытием" разных уровней СУ в разных фреймворках.
(На неё я даже в кандидатской диссертации ссылался.)
Оно было на ICALEPCS в Южной Корее, куда я не смог поехать (и сайт конференции издох ещё в середине-конце 2000-х).
Кстати, есть некая публикация
...а, нет -- есть! Control Systems for Accelerators, Operational Tools на сайте Helmholtz-Berlin (нашлось гуглением по DOI-номеру "978-3-319-04507-8_17-1").
01.04.2022: и ещё чуток условно исторического -- найдено в процессе разбирательства на тему "что есть GDD и что с ним стало?" и "что такое Data Access и как оно соотносится с pvAccess?":
Попалось при гуглении про «"epics" "data access"» (да-да, без кавычек -- никак: всё фигню разную подсовывает).
Рассказ о первых впечатлениях про т.н. "Data Access" -- то, что потом превратилось в pvAccess. Описываются некоторые базовые принципы и подходы. Похоже, оно всё вусмерть объектное -- там прямо сам принцип реализации "полиморфные методы assign(), reveal(), ...; плюс функция-обходчик PropertyCatalog::traverse()". Ключевое слово для описания концепции было бы "introspection", хотя в тексте оно и отсутствует.
Нагуглилось по «site:epics.anl.gov "data access"».
Но как именно всё же работает маршаллинг -- мне так и не стало ясно (т.е., как все эти вызовы преобразуются в байтовый поток в сокет).
Попалось при гуглении про «"epics" "data access"».
Clear distinction between arrays of size 1 and scalars
Фраза прямо очень-очень понятная (т.к. в CXv4 скаляры -- это как раз "массивы" максимального размера 1), но ПРИЧИНЫ ни самого ихнего решения, ни такой его важности (что аж тут упомянули) мне НЕ ясны.
26.07.2024: ещё на близкую тему -- "Normative Types":
21.02.2025: в EPICS Tech-talk был задан вопрос "History of EPICS", на который многие ответили по своим воспоминаниям, в т.ч. Bob Dalesio -- один из ключевых авторов оригинальной разработки 1980-х.
Ну и я не удержался и написал ему письмо с вопросом
(в архив оно (Message-ID:4a3da69-1799-84dc-43ad-146aaea06f@starnew.inp.nsk.su) не попало, поскольку, похоже, было отфильтровано ANL'ем).Is there any paper describing "Why is EPICS the way it is?"? I.e., what were the reasons to design EPICS around the "database concept" (and was it designed like this from the very beginning or if that was done later)? And are there any papers regarding GTACS besides what is told in "EPICS: A Control System Software Co-Development Success Story" (https://epics.anl.gov/EpicsDocumentation/EpicsGeneral/epics_success.html) and "EPICS Application Developer's Guide" (https://epics.anl.gov/base/R3-14/12-docs/AppDevGuide.pdf)? P.S. For me it looks like the initial design was VME-centric to allow direct mapping of VAL fields to VME addresses so that reading and writing becomes trivial, thus can be performed by "database engine" itself, while Device Support and Driver Support were added later. But I couldn't find any confirmations or denials (neither any technical details of EPICS structure/constitution nor rationale earlier than 1994).
И Боб ответил (Message-ID:CAKc=vhn_VyH5cEPp7oYyfBE4t+9koATCkCO5o6v8wmdktpfxiQ@mail.gmail.com),
и про "заточенность под VME":GTACS had two major influences early on - network communication design that was done by Jeff Hill with work started on the Accelerator Control System - that became the Vsystem product, and emerging industrial control system tools which came out of Foxboro and featured a process database. I worked at two companies that were derived from Foxboro - EMC Controls with the D3 product (that ran the DESY cryo system) and Computer Products Inc that sold fully redundant hardware for nuclear power plants. The innovation came with standardizing the client on a narrow interface that defined a Process Variable as a value of some data type along with the time stamp and alarm status of that data (DBR_Types) and making all hardware communication a layer below the process database - so that all record processing did not wait for IO to complete - which made records process at the frequency specified - which is crucial to performing PID control. Marty Kraimer broke the interface to the hardware into Device Support and Driver Support - which allowed Device Support to become generic for record types and drivers specific to handling different communication protocols.
VME and vxWorks were very convenient to use. A hardware engineer at Los Alamos, Bob Dingler. The 30 usec context switching and 2 usec interrupt latency made them good candidates. Still, we had Allen Bradley PLCs, motor controllers and GPIB/Serial devices that had to be integrated. Narrow APIs, Robust operation, deterministic execution and high performance were the primary design considerations. It is important to mention that the technical issues had important but relatively small contributions to EPICS. The more relevant innovations were collaboration and open source development, which was enabled by Mike Thuot at LANL and Marty Knott at Argonne and fostered by many including Steve Lewis at LBL and Dave Gurd at the SSC, and then by Matthias Clausen at DESY Cryo, Carl (?) and Steve Hartman at Duke University, and Chip Watson / Karen White at JLab. The EPICS community is remarkable! Early on in the collaboration I was at a facility in Hawaii and had a technical issue that was answered by someone in Japan. When you attend an EPICS meeting or read through tech-talk, you find a lot of very dedicated people that have given time to help colleagues at other facilities in a wide variety of applications. That is the important part of EPICS. The community has been the most astounding accomplishment of EPCS.
Т.е. (выводы от меня):
dbr_ctrl_string
отсутствует и нет никакого
DBR_xxx_STRING с полем "units").
Т.е., IMHO, косяк в НЕотделении "свойств" (всякие units, range, ...) и "состояния"; и явного признания, что это мисдизайн, я никогда не встречал.
Ну и слово "Foxboro" для меня новое. Погуглил --
Где бы найти информацию про него из начала 1980-х -- какая архитектура, что за идеи в его основе...
27.02.2025: и отдельно Bob Dalesio ответил на мой вопрос "is Vsystem still alive?" (уже не в tech-talk, а напрямую мне, Message-ID:CAKc=vh=M1vB_fm2QUXxrK3eR47VUROU22AH_ToRcLVkxkQNpbQ@mail.gmail.com):
I am not sure. It was still alive 15 years ago. Their primary customer was Alcoa as I understand it. When they left and formed Vsystem, they had an early version of Channel Access with no dtaa types. Their visualization tool was the most interesting part. It required VMS - we were a UNIX shop. Jeff Hill and I did not join Vsystem - and we developed what became channel access, dbr_types and the process database. The entire structure that our group presented at the 1985 workshop was modelled on the Foxborough DCS architecture. What Jeff and I did with EPICS was different enough from Vsystem and Foxborough to say that both influenced but neither were a predecessor. > For me this seems a perfect illustration on a topic of "closed source" VS > "open source, community support" model. > I agree. The other commerical companies that licensed EPICS - TIS400 in particular is also dead. In all cases, the closed source model failed. Thank you for visiting all of this. It has been a great community - and the number of people that could be identified as significant contributors (especially when you include the clients) is enormous! I would say that each major piece you see developed under collaborations has well over 100 man years in the code and did not get released as a product until it was on its third version.
Ну, один клиент в лице целой Alcoa может всё объяснять.
27.02.2025: и ещё timeline, собранный топикстартером.
08.03.2025: обобщить бы 27.03.2025: проверка Modbus-ASCII показала, что в ИСХОДЯЩИХ
пакетах LRC считалась некорректно -- без учёта UNIT_ID. Вследствие того,
как в 27.03.2025: записать в раздел "не сделать ли период реконнектов
настраиваемым", что для ВИП-45/ВИП-48 это может быть актуально, т.к. там
при отсутствии запросов интерфейсу делается down и через ~1-1.5sec обратно
up, и он остаётся включенным ~5sec; так что если вдруг период реконнектов
совпадёт с периодом up/down и первая попытка подключения попадёт в фазу
"down", то установление соединения может растянуться на длительное время.
29.03.2025: при подключении точка-точка патч-кордом проблема не возникает,
видимо, потому, что у ядра Linux хватает ума не пытаться слать пакеты в
отключенный интерфейс и они отправляются сразу после включения; а вот при
подключении через коммутатор информации об отключенности интерфейса ВИПа не
будет и пакеты будут отправляться и теряться.
29.03.2025: давно напрашивается мысль так модифицировать
определение IST'ов -- 31.03.2025@по дороге в ИЯФ, по лестнице у
ИЦиГ после мыши: давно напрашивается сделать, чтобы по завершении
modbus_mon'а возвращался бы осмысленный exitcode, а не всегда 0. Например,
статус исполнения последней операции: 0: OK, 1: exception, 2: timeout.
01.04.2025: сделал. Предварительное действие -- в
console_mon_util.h добавлен код 31.03.2025: а не добавить ли конверсию "BOOL" -- чтоб любой
не-0 от хоста превращался бы в 1? У Диком'овского ИП-МРН20 куча регистров
булевских. ...хотя, чуток поразмыслив -- неа, наверное, не стоит: пусть это
будет заботой источника значения; поди уж справятся.
31.03.2025: modbus_mon: сделал ключ " 31.03.2025: проверил работу записи конверсии
07.04.2025: после наблюдения той дичи напрашивалась
мысль поменять алгоритм в 08.04.2025: ещё разок взглянул на код и на добавленную
диагностическую выдачу.
Что имеем: при формуле
09.04.2025: проверил на живом ВИП-48 -- да, работает.
31.03.2025: в modbus_mon.c "поддержка"
01.04.2025: проверено (на ВИП-48), что modbus_lyr.c
работает и распределяет запросы между разными UNIT_ID по очереди.
02.04.2025: ЕманоФедя вопросил, почему cxsd не позволяет
указывать, какой интерфейс слушать (в отличие от EPICS)? Причина вопроса --
у него на разных серверах используются одни и те же имена каналов (на
"настоящем" сервере и на бридже для ВЭПП-4), поэтому при использовании
UDP-резолвинга иногда возникала путаница -- собственный федин клиент
(изнутри сети ВЭПП-5) иногда цеплялся к серверу бриджа вместо настоящего и
управление не работало (т.к. бридж readonly). Самое странное -- что это
поведение определяется КОМПЬЮТЕРОМ: если уж началось, то перезапуск программ
не помогает, а помогает перезагрузка хоста-клиента. 03.04.2025@утро, просыпаясь: часть ответа на вопрос "почему?" в
том, что если в EPICS разные сервера на одном узле полностью независимы (и
потому возникает трудноразрешимая проблема "всем нужен UDP-порт 5064"), то в
случае CXv4 у них общий на всех резолвер, и если разные сервера на узле
запускаются с разными указаниями слушаемых интерфейсов, то возможна
ситуация, когда слушаемое резровером не совпадает со слушаемым конкретных
серверов.
02.04.2025: а ещё в cda_d_cx.c желательно бы печатать
warning'и при приходе дополнительных ответов на уже разрезолвленные каналы
(если в этих ответах отличаются IP или номер сервера). Чтоб хоть
диагностика была. Посмотрел в коде 17.04.2025: надо разбираться с тем, как устанавливать COM-порту
скорости, отличающиеся от стандартных. Нужно в интересах драйвера
bondarenko_ubs_drv.c -- там требуется скорость 125000. Некоторое
время назад при поиске чего-то другого встречал ключевое слово "termios2" с
комментарием, что оно не сочетается с обычным "termios", поскольку 2 разных
.h-файла, пересекающиеся по именам и потому их нельзя
Гуглим по 2 вариантам:
Вот только " 21.04.2025: grep по
/usr/include/ показывает, что там слова " Рытьё/поиск по ссылкам показало, что в одном-единственном
месте, где оно как раз и выглядело крайне привлекательно --
"How to set a non-standard baudrate on a serial port device on Linux?"
на StackOverflow 13/18-10-2013, где речь о том, как это делается в
ДРАЙВЕРАХ.
Таким образом, для userspace слово " И надо смотреть на 21.04.2025: для проверки набросана work/tests/ttycat.c
-- она пересылает байты между stdin/stdout и указанным /dev/tty* и
предполагается запускать её в количестве 2 штук на соединённых друг с
дружкой адаптерах. Пока установка скорости не сделана, но предполагается
именно туда запихивать разные способы, для тестирования: на паре соединённых
адаптеров будет видно, работает ли.
22.04.2025: со стандартными уставками утилитка проверена, на
пере соединённых адаптеров -- да, работает. Показателен эксперимент с
24.04.2025: добавлена также и поддержка нестандартных скоростей
-- отдельным файлом set_tty_speed_via_termios2.c, содержащим
25.04.2025: set_tty_speed_via_termios2.[ch]
скопированы в hw4cx/drivers/eth/ и поддержка нестандартных
скоростей добавлена в modbus_mon.c и modbus_lyr.c, с
надлежащими проверками на доступность этой функциональности (код в них
практически одинаковый). Для указания служит параметр
" 21.04.2025: идея о том, как можно было бы реализовать парсинг
скорости линии в предположении, что устанавливается именно через
22.04.2025@утро, зарядка: а если
скорость по умолчанию сделать 38400, то и отдельных действий никаких
выполнять не надо будет. @по
дороге в ИЯФ, проходя мимо мыши, ~09:50: но лучше всё-таки проверять
и при не-нулевом значении speed выдавать warning и форсить
18.04.2025: посетила идея, как можно Tango'вские 2-мерные
данные -- ...так что поддерживать CURVV с его 24-битным входным регистром, как и
DL250 с толпой 24-битных регистров управления режимами и блокировок в EPICS
просто не удастся (сложно -- можно :D).
Это в дополнение к тому, что в EPICS3 нет не только int64, но также нет
беззнаковых типов.
27.04.2021: продолжение о бинарном В/В:
Но, если верить документации, это ТОЛЬКО для входных -- "Input"; выходные
же -- "Output" -- по-прежнему 16-битные: в
"Multi-Bit
Binary Output Direct Record (mbboDirect)" сказано
"Во тупы-ы-ые!!!".
Кстати, а корень этой проблемы -- именно в самой системе рЕкордов.
Переделать из 16-битности в 32-битность так сложно потому, что вместо
простого драйверного API с передачей данных между драйверами и "ядром
сервера" имеются развесистейшие потроха (в виде этих разнородных рЕкордов).
Если б была просто передача данных, то и просто целочисленный ввод (и вывод)
был бы совместим с "бинарным"/регистровым; и при надобности уж внутри
сервера можно б было раскладывать результат хоть в 8, хоть в 16, хоть в 32
бита, и при надобности делать преобразование данных (как в CX и сделано).
А встретилась ссылка туда в сообщении
"RE: Beacon and caRepeater"
за 02-10-2007 от Jeff Hill (на
тот thread
я наткнулся, гугля "carepeater client search" -- вот то и не дало
результатов по исходному вопросу).
ЗЫ: также из thread'а была ссылка на
https://www.aps.anl.gov/bcda/epicsgettingstarted/specialtopics/ChannelAccessInDepth1up.pdf
полезное, но ныне 404. Искать в архиве.
RegisterWithFDIO()
-- он
уже сейчас в 3 местах, и из них как минимум в 2 (_lyr.c и
_mon.c) по определению содержит идентичный код (в
_tcp_drv.c большая часть избыточна, но и фиг с ним) -- т.к. оба на
основе modbus_conv.h. Вопрос только в том, КУДА поселить это
обобщение: вроде бы напрашивается именно modbus_conv.h, но он не
должен иметь зависимости от fdiolib, а так она появится... 11.03.2025: учитывая появившееся желание "уметь отступать от
ограничений Modbus, позволяя больше значений и большие пакеты", обобщение
становится ещё более актуальным.
modbus_create_req_packet()
устроено складирование данных
в исходящий пакет: UNIT_ID не входит в PDU, а кладётся отдельно; сумма же
считается из 2 кусков. Добавление параметра sum0
(в который
передаётся UNIT_ID отправляемого пакета и которым инициализируется сумма)
ситуацию исправило, коммуникация заработала. Но выглядит это всё криво --
надо бы как-то архитектуру улучшить.
MAGX_IST_CDAC20_DEV()
-- и прочих в
devlist_magx_macros.m4, чтоб там сразу делался cpoint на
подстилающее устройство, вроде "cpoint $1.hw_dev $2
", чтоб
можно было в инженерных скринах вроде ist_cdac20.subsys и исходное
устройство показывать, как в subsys_magx_macros.m4'шных панелях.
EC_TIMEOUT
. Введена
last_known_ec
, в которую записывается EC_
-код
последнего "результата" исполнения, а финальный return
теперь
её и возвращает. Плюс, finish_proc()
-- которая отрабатывает
ключ "-T
" -- тоже теперь генерит EC_TIMEOUT
.
-Dy
" -- для
Modbus-TCP выдавать ещё SYNC_ID. Инфраструктура была подготовлена заранее
-- все нужные if()
'ы при печати были, просто в них везде стояло
0
вместо закомментированного print_sync_ids
,
каковой сейчас и сделан и раскомменчен.
FLOAT16_10E6M10
-- фигвам: не принимает ВИП-48 эти числа,
ругается "EXCEPTION 3:ILLEGAL_DATA_VALUE". Если же записывать просто те же
коды, что читаются из HOLD:0/2, то принимает. Похоже, дело в том, что это
недоразумение требует конкретный вариант кодировки -- с конкретной
экспонентой. Прямо
https://www.youtube.com/watch?v=IYtVFNhDdVo
в чистом виде...
FLOAT16_10E6M10_h2m()
так, чтобы он
генерил число, устраивающее дурной девайс.
Например, выполнять "нормализацию" (домножение мантиссы на степень 10
одновременно с уменьшением экспоненты) не всегда, а только для когда иначе
не влезет. ...cейчас проверил -- оно так и делается. 08.04.2025: нифига -- домножается ВСЕГДА, иначе точность бы
терялась из-за того, что мантисса оставалась бы исключительно
1-значной.
Судя по скзанному в описании, "Диапазон чисел мантиссы при
одном знаке после запятой (Низкий вакуум: включение и Низкий вакуум:
выключение): 10 — 99, а диапазон порядка -9 — -2.", этой
скотине надо мантиссу обязательно 2-значную. Проверил:
да, 1-значную (0xf406,0xf405=>6*10^-3,5*10^-3) он не берёт, а вот
2-значную (0xf043,0xf02b=>67*10^-4,43*10^-4) -- берёт.
Откуда вывод: надо добавить ещё один "шаг" нормализации -- стараться вгонять
мантиссу в диапазон [10..99].
f32_man = f32_val / powf(10, f32_exp);
результирующее значение ВСЕГДА будет в диапазоне от 1.0 до 9.9(...), просто
по определению.
Значит, для получения желаемого девайсом диапазона [10..99] надо делать
просто ВСЕГДА домножение мантиссы на 10 (с декрементом экспоненты).
@вечер, уже лягши в постель, но не удержался:
сделал -- от всех 3 сдвигов оставлен только последний вариант, который как
раз сдвигает на 1 десятичный разряд.
UNLIMITED_MODBUS
была реализована крайне сыро: 1) в
SendNextCommand()
оставался buf[256]
-- который
при попытке записи будучи передан в modbus_create_req_packet()
переполнился бы; 2) в ProcessInData()
аналогично
m_data[256]
-- который при сбагривании в
modbus_decode_rpy_packet()
наверняка переполнялся.
ProcessSrchEvent()
-- ну
да, сделать несложно: надо чуть переделать if()/else(), чтобы выделить в
отдельную ветку ситуацию "да, ответ на этот канал, но он уже разрезолвлен"
(не-"hi->rslv_state == RSLV_STATE_UNKNOWN
").
03.04.2025: в процессе тестирования обнаружился милый нюанс: код GURU
устроен так, что на каждый запрос имени отвечает только ОДИН раз, даже если
это имя есть в НЕСКОЛЬКИХ серверах на данном узле. И так оно потому, что в
cxsd_fe_cx.c::ServeGuruRequest()
делается
"goto NEXT_REQ
" после добавления к ответу успешно найденного
ответа.
@после обеда:
1) добавил печать WARNING в ProcessSrchEvent()
(да, условия
переделаны; а вот добыть ТЕКУЩЕЕ имя сервера оказалось никак: в cdaP-API нет
простого способа ни sid_of_ref() получить, ни имя); 04.04.2025@вечер: напрашивается
добавить таковой метод в cdaP-API -- "получить имя сервера такого-то
ref'а". 05.04.2025: сделано -- cda_dat_p_srvrn_of_ref()
,
и его использование тоже; проверено, работает.
2) "goto NEXT_REQ
" закомменчено после этого получаем ВСЕ
-- сервера данного узла с таким именем;
3) надо бы в cx-chan-search.c добавить после получения результата
паузу 0.1-1s, чтоб дожидался прихода возможных прочих ответов (локальные -- все получает, т.к., видимо, GURU
успевает ответить на все полученные пакеты (со всех адресов/интерфейсов), а
вот от b360mc -- только 1, т.к., видимо, успевает отвалить до того, как
прочие пакеты по сети "добегут" @вечер: нифига -- просто кабель из второй карточки
b360mc был переткнут из коммутатора в ВИП-48, так что "прочим" пакетам было
неоткуда взяться, по причине отсутствия прочих связей
x10sae<>b360mc), возможно, с возможностью регулирования,
ключиком "-T USECS_TO_WAIT_MORE_AFTER_LAST
", но пока не сделано
потому, что не очень ясно, куда/как это добавить, т.к. используется
LIBCX_SYNC;
04.04.2025@утро: да чё там думать -- делать
надо: a) переделать на ASYNC; б) ВСЕГДА ставить таймаут на 0.5-1.0sec, чтобы
не висело до бесконечности (запрос-то всё равно шлёт один-единственный раз,
повторять его cxlib не будет), а после каждого ответа ещё продлевать на
столько же; в) размер таймаута регулировать ключиком "-T
"
(вопрос только -- в каких единицах).
4) в iptables нужно ЯВНО разрешать пакеты, ВХОДЯЩИЕ с порта UDP 8012 (как и
для EPICS с 5064, кстати) -- даже осознал, почему не хватает правила
"RELATED": ведь исходный-то пакет запроса broadcast'ный, а ответ unicast,
так что он никак не соответствует пакету запроса; сделал -- заработало, а до
того с соседнего узла пакеты не видело. 04.04.2025:
проверил, выставив "CX_GURU_LIST=b360mc
",
чтоб пакет слался unicast'ом, и убрав разрешение входящих с порта 8012 --
да, так ответы тоже приходят.
04.04.2025@утро: да, по слову "ESTABLISHED"
нашёлся аналогичный вопрос от 28-04-2018 касательно EPICS; тогда сходу не
дотумкал до причины того, почему без разрешения "--sport 5064
"
не работало, но 11-06-2021 уже сообразил.
#include
'ить вместе.
drivers/usb/serial/ftdi_sio.c
grep -rw B38400
" по исходникам
kernel-3.10.0-514.2.2-1.el7 (это от CentOS-7.3) не находит внятного
использования оного B38400
, а касательно
"alt_speed
" ещё грустнее -- есть ПРИСВОЕНИЯ ЕМУ, а вот
ИСПОЛЬЗОВАНИЯ его значения не видно совсем.
alt_speed
"
нигде нет (CentOS-7.3).
alt_speed
"?
alt_speed
" вообще
иррелевантно.
termios2
, а то с B38400
всё
очень уж через одно место -- там надо руками делитель указывать, вместо
желаемой скорости.
B50
-- прям видно, как символы передаются.
set_tty_speed_via_termios2()
. Проверено -- да, разные скорости
работают. Странность в том, что если указывать скорости ниже 50, то реально
работает что-то более высокое (но медленное, т.к. заметно). Но что такая
установка скорости действительно работает, видно по тому, что если поставить
на обоих сторонах скорость 76, то символы пересылаются верно, а если на
одном сделать 77, то идёт мусор.
speed=BAUDS
", парсится он в поле comopts_t.speed
,
а используется его значение, если оно >0.
alt_speed
: сделать оное alt_speed
отдельным
параметром в PSP-таблице, чтоб можно было указывать в стиле
"speed=125000,8,n,1". Тогда перед выставлением скорости после
открытия линии будет проверяться в стиле
if (modbus_comopts.alt_speed != 0) modbus_comopts.bauds = B38400;
B38400
; кстати, для "красивых" скоростей вроде
125000, 250000 и 500000 можно было бы сделать короткие ключи -- прямо в виде
этих чисел, чтобы соответствующие числа в поле alt_speed
прописывали.
Tango::IMAGE
-- хотя бы читать (касается и
cda_d_tango.cpp, и epics2tango_gw.cpp): возвращать их
1-мерными массивами, обычным/очевидным/стандартным образом, просто складывая
строки друг за дружкой (20.04.2025: а если способ
хранения в памяти позволит -- то вообще просто беря данные из памяти как
есть; правда, на то надежды мало: боюсь, внутренним типом окажется
vector<vector<T>>
, который вряд ли хранит
содержимое линейно/последовательно). Ну и размер отдавая как
"dim_x*dim_y
".
The mbbiDirect record retrieves a 32-bit hardware value and converts it to
an array of 32 unsigned characters, each representing a bit of the word.
The mbboDirect record performs the opposite function to that of the
mbbiDirect record. It accumulates bits (in the fields B0 - BF) as
unsigned characters
Mark is right: There are still two CA servers in EPICS base.
rsrv - The old server (C) that is running as part of the iocCore
cas - The new server (C++) that is running as part of several host-side applications (e.g. CA-Gateway)
The new server implements the additional configuration options (which are heavily used by e.g. the CA-Gateway), the old server doesn't (so the IOC can't make use of it).
22.12.2017: вроде как существует -- на странице https://www.vista-control.com/vsystem.html написано много красивых слов (примерно как 15-20 лет назад было в тех их рекламных материалах). Конкретики точно так же почти ноль.
Из печального: сам сайт промаркирован 2016 годом, в списке поддерживаемых ОС в Vsystem Description Windows заканчивается на Vista (даже не Win7!), *nix'овый интерфейс явно на Motif'е...
05.09.2019: наткнулся на ещё один сайт: https://www.ixvista.com/, на нём хоть copyright стоит 2018 годом.
Но сайт ещё более пустой и убогий; разве что там говорится о "госзакупках" ("government procurement") и упоминается изрядное количество "some of the many who have incorporated the Vsystem software into their operations", включая ANL, BNL, NASA, Hitachi, Honeywell, ...
27.02.2025: и ещё Bob Dalesio ответил на мой вопрос "is Vsystem still alive?"; подробнее в разделе по EPICS за сегодня (чтоб все ответы из той переписки были рядом).
Но ссылка вела на anl.gov и с тех пор протухла. Гугление эту статью найти позволяет -- например,
Также по ходу дела встретилось:
У них СУ работала под Vista с середины 1980-х (~35 лет), пережила пару смен серверного железа и ОС, а сейчас они сделали бридж Vista<->EPICS через MQTT на Python, но подумывают о полном переезде на EPICS.
02.10.2018: ЧеблоПаша сейчас мучается с поиском решений для максимально простой реализации поддержки MQTT в EPICS.
Некоторая поддержка есть в CSS, но там она своя, а "в EPICS вообще" -- нету.
02.10.2018@после обеда, дорога из дому в ИЯФ, около НИПСа: очень удобно б было иметь "шлюз" в виде псевдодрайвера, который бы просто маппировал MQTT'шные имена/каналы на CX'ные. Чтоб можно было его использовать как-нибудь вроде
dev some_ps senkov_ps/mqtt_gw ~ -
Вопрос только: а как МАППИРОВАНИЕ-то делать? Нужна ведь некая карта соответствия между MQTT'шными именами и номерами каналов.
И тут идейка:
CxsdDbFindNsp()
находит нужный
devtype-namespace (через GetDevTypename()
узнав имя типа), и
потом делает таблицу соответствия.
Естественно, таблицу эту составлять надо в init_d()
, к тому
же там надо будет заготавливать имена (у нас имена с разделителями
'.', в MQTT -- '/', плюс там еще префикс имени
устройства).
03.10.2018: ага, начато -- скелет сделан, потихоньку наполняем _init_d().
Только названо оно mqtt_mapping_drv.c, а не "mqtt_gw".
...к вечеру: "мясо" создания таблицы маппирования в _init_d() сделано, теперь надо проверять. Сделаем этому драйверу свой раздел и далнейшее будем описывать там.
Причём -- чтоб оно было доступно из cda, через какой-нибудь "cda_d_mqtt".
Смысл:
Поэтому для передачи многокилобайтных бинарных данных их придётся гонять через строковое представление (например, BASE64). А это изрядная потеря производительности.
...а то cx'ом этому народу пользоваться сложно (или западло?).
30.11.2019@8-ка-в-город ~11:00, едучи по Строителей (опять!!!): раз уж вытанцовывается потребность в работе с MQTT аж в 2 местах -- из драйвера (сейчас -- mqtt_mappnig_drv.c) и из cda (cda_d_mqtt.c), то напрашивается мысль всё-таки сделать СВОЮ реализацию библиотеки доступа к MQTT, на основе fdiolib+cxscheduler, и назвать её libmqtt4cx.
Жить ей имеет смысл в hw4cx/. Где, видимо, надо будет завести поддиректорию hw4cx/lib/ -- для создания библиотек работы с аппаратурой. Первая такая уже существует почти 3 года, но не в виде библиотеки, а как .h-файлы -- advdac.
Раздельчик создаём после сегодняшнего техсовета на ВЭПП-4, где Лёша Герасёв рассказывал об удачной попытке запинать поддержку в EPICS.
16.10.2018: итак:
17.10.2018: Переспросил. Некая liblxi, как утверждается, "...is based on the VXI-11 RPC protocol implementation which is part of the asynDriver EPICS module...".
Но проблема вроде как в том, что в некоторых случаях девайс отвечает не текстом, а байтовым потоком -- в частности, при передаче вэйвформы.
17.10.2018: ещё чуток поспрашивал Герасёва -- вот результаты:
Но внутри той функции используется именно gethostbyname()
(т.е., НЕ неблокирующийся вариант), просто окружённый блокировкой
(семафорами?).
Насчёт TANGO никто из нас не знает (ЧеблоПаша вообще заявил, что его танго танцевать не учили -- тут я его уел, что учили, в июне 2017-го несколько занятий).
А насчёт CX -- мой ответ что "проблем не будет".
16.10.2018@вечер-дома: некоторые мысли:
Аналогичные remdrv'шным "мозги" могут потребоваться для mqtt_mapping_drv.c, LXI'ного драйвера и т.д.
Вывод: а мож как-нибудь ту часть -- с коннектами/реконнектами -- из remdrv вытащить и "обобщить"?
17.10.2018: ага -- еще ведь hw4cx/drivers/eth/triadatv_um_drv.c (именно на базе remdrv_drv.c), при создании которого была сделана попытка обобщить код на будущее, создав "шаблон" _tcp_string_drv.c.
Делать поддержку и того, и другого (с отдельными полями для handle и fdh), чтоб клиент указывал желаемый механизм?
Отсюда следующая идея (п.2):
Дальнейшее обсуждение этого вопроса будем вести уже в разделе fdiolib'а.
19.10.2018: FDIO_STRING-то фокусу с бинарными данными обучен, но пользы от того не будет.
#800001234<...тут 1234 байта данных>\n
Здесь '8' -- число цифр в спецификаторе длины, "00001234" -- та самая длина, а потом, без каких-либо разделителей, прут эти данные. Что интересно -- после данных всё-таки идёт перевод строки, зачем-то.
(Это проверено на осциллографе Agilent (МОДЕЛЬ?!).)
Спрашивается: как протокол от крейтовой системы с 32-битной шиной может быть частью протокола, основанного на байтовом потоке (идущем по TCP, а сам протокол унаследован от GPIB, с 8-битной шины со строковыми данными)?
Дата последней модификации -- 18-03-2019.
11.06.2020: и ещё презентация -- "An introduction to VMEbus" by Markus Joos, CERN.
Created: Mon 08 Sep 2003, Modified: Tue 02 Feb 2010.
Из интересного, помимо вообще описания: для работы с используемым VME-контроллером (VP110, PIII, Tundra Universe) там в качестве драйвера+библиотеки для доступа применяется CERN'овский пакет vme_rcc. Так вот: для решения проблемы "у Тундры всего 8 окон, а модулей в крейте больше!" там используется (стр.40) не динамическое маппирование, а СТАТИЧЕСКАЯ конфигурация, с такой аргументацией:
Кстати, некое описание егойного API доступно в отдельной статье -- "VMEbus Application Program Interface" by R. Spiwoks, M. Joos, C. Parkman, J. Petersen (ТОТ ЖЕ Joos).
И увы, гугление показывает, что термины "vme_rcc" и "CAEN" вместе практически не встречаются -- т.е., биндинга vme_rcc под CAEN'овские адаптеры не существует.
01.07.2021: ещё статья от Dirk Zimoch (который автор руководства по написанию драйверов под EPICS) на ICALEPCS-2017:
Человеки съехали с realtime-OS (VxWorks?) на Linux, причём на довольно экзотическом контроллере IFC1210 (PowerPC, Freescale P2020). И плачутся, насколько путь оказался тяжким -- проблемы на КАЖДОМ углу: стандартный VME-API в ядре поддерживает реально только Тундру, а для ихнего чипа (что-то на Xilinx) пришлось делать нечто сильно другое; ловля прерываний -- тоже непроста (они сделали некий совсем свой API с /dev/-файлами для каждого уровня и вектора). Да ещё и API ядра часто меняется, а для экзотического процессора никто не рвётся всё обновлять.
Кстати, из того текста следует, что в AM'е не 5 бит, а 6 -- "Dynamic address sizing is made possible because of a six bit address modifier code [AM0-AM5] which accompanies each address."
11.02.2020: ну да, 6: ведь 5 бит -- это максимум код 0x1F, а используются намного бОльшие, вроде cvA16U=0x29 (у козачиных VADC16 и VDAC20).
И почему я думал, что 5? Может, из-за номера ПОСЛЕДНЕГО, 5-го, не заметив, что есть и 0-й?
06.06.2020: пообщался с Павленко, спросив его пару вещей и получив ответы:
Ответ: а хбз -- всегда в железе вроде именно 8 бит.
Но, памятуя, что "interrupt vector" в M68K -- это просто НОМЕР прерывания, который умножался (на 4?) для получения адреса ячейки, где содержался адрес процедуры-обработчика, идея о 16- и 32-битных векторах выглядит бредово (возможно, просто некое уже VME-специфичное расширение для передачи дополнительной информации?).
Ответ: причина в том, что на адресной шине VME отсутствует младший бит адреса (0), а есть только начиная с 1-го. Вот для доступа к нечётным адресам и используется какая-то другая линия с шины. (От меня: возможно, это и есть причина отдельных оговорок насчёт unaligned access -- просто нет возможности по-простому указать такой адрес.)
И тут я вспомнил, что M68K создавался как идеологический наследник LSI11, где шина была 16-битной и в реальности даже байтовые выборки работали как чтение из памяти всего слова, а уже в процессоре вычленялся нужный байт (а как запись работала? чтение, модификация, запись?). И, вполне возможно, что бита 0 там могло просто не быть (сходу подтверждений этой мысли найти не удалось, но какие-то слова в Википедии есть: "Because the bus actually contains a data path that is two bytes wide, address bit [0] is subject to special interpretation and data on the bus has to travel in the correct byte lanes").
Побудительным мотивом стал абзац
Note: the board can be used in standard VME crates where geographical pins do not exist, in this case the user may either insert jumpers to set the geographical address or use the default setting when the board's CR/CSR base address will be set to 0xf8.из MRF'ного мануала "Event Generator ... VME-EVG-230 ..." (раздел "VME-EVG-230 CR/CSR Support" на стр.25).
05.02.2021: что нагуглилось по запросу "vme geographic addressing":
07.02.2021: Page Info говорит дату модификации "April 24, 2008", а так -- это явно более ранняя версия презентации Markus Joos, нарытой 11-06-2020.
06.02.2021: так вот:
Но по нему документации найти не удалось, совсем.
Т.е., аж 1024 "окна"? Ну так это бы полностью решало проблему окон -- не нужно никакого динамического ремаппинга! Собственно, обычно хватило бы, например, даже 32 окон -- чтобы заведомо превышало максимально возможное количество модулей в крейте. А 8 -- это какой-то стыд...
08.02.2021: ещё чуток нарытой информации:
Там упомянут MEN A25 (видимо, это название чипа), а Тундры -- старая Tundra Universe II (PCI?) и "новая", discontinued, TSI148.
28.06.2021: так вот:
Там же теоретически есть возможность скачать драйвер и документацию, но мало того, что для этого надо регистрироваться, так даже и после регистрации просто так не дают -- нужно заполнить форму "А вам зачем?" и потом, *возможно*, позволят.
27.11.2021: ещё тогда, летом, зарегистрировался и попросил -- фиг, ничем не закончилось: запрос перекинули в российский офис их партнёра, оттуда какой-то чел что-то мне писал, но в какой-то кривой кодировке (никак не дешифрируемой), я ему пытался об этом сказать, но без толку; так это и заглохло.
27.11.2021: пара аспектов, незамеченных мною тогда при чтении TUALP03 "Solving...":
Т.е., ни A40/A64, ни навороченные быстрые режимы конкретно ЦЕРНу попросту не нужны (подозреваю, что они вообще в физике высоких энергий никому не требуются, а сделаны лишь для вояк).
И аналогично для TSI148, стр.37:
1.2.4.1 Features Not Supported
The following features are not supported by the Tsi148 VME Master:
- A40 address modes
- D32 MBLT transfers
- VMEbus lock commands
Так что -- A40 даже и в Tundra не реализован...
из которой вроде бы следует, что там для A32 вообще ОДНО-единственное окно -- BAR3?! Или оно сразу размером 32 бита (т.е., предназначено для использования в 64-битных системах)?BAR no. name offset BAR0 Version ROM 0 BAR0 Flash 200 BAR0 VMEbus - registers 10000 BAR0 VMEbus - A16D16 access 20000 BAR0 VMEbus - A16D32 access 30000 BAR1 SRAM 0 BAR2 VMEbus - A24D16 access 0 BAR2 VMEbus - A24D32 access 1000000 BAR3 VMEbus - A32 access 0 BAR4 VMEbus - CR/CSR access 0
28.11.2021: но даже если пространство сразу 32 бита, то всё равно нужно бы уметь разные Address Modifier'ы ставить.
30.11.2021: или считается, что уж для A32 должно быть ВСЕГДА достаточно чего-то одного? Возможно, конкретно 0x09 (A32 unprivileged data)?
11.01.2022: Котов прислал ссылку на репозиторий с драйверами (типа как) -- https://gitlab.cern.ch/mendrivers/menlinux и https://gitlab.cern.ch/mendrivers/mdis_include_com_men (где надо "искать по строке pldz002 или 16z002-01").
По правде сказать -- сильно легче не стало :D.
Чтоб был рядом с прочими бриджами, вставляем его сюда, не по хронологии добавления.
17.12.2021: немного о зоопарке Тундр (чтоб лучше понимать "что есть что"):
Судя по linux-*/drivers/vme/bridges/ -- она же CA91C142.
Источники информации:
(Tundra Semiconductor была в 2009-м куплена фирмой IDT, которая в 2018-м была куплена Renesas'ом)
FN 80A3010_MA001_03 This manual is for the Universe IIB, IID devices (CA91C142B/D) ( 3,892KB, PDF)по которому уже вполне нагугливается, например,
Last Revised : 05/14/2004
FN 80A3020_MA001_08 ( 3,852KB, PDF)по которому уже тоже можно гуглить (хотя и раньше ж была выловлена Preliminary release от 2004); например,
Last Revised : 08/31/2006
Да, "удобство" пользования сомнительно; поэтому я не стал особо рыться в поисках документации по SCV64.
17.12.2021: мои впечатления от TSI148 по опыту реализации поддержки для MVME3100:
11.01.2022: по результатам общения с Котовым (по ходу разбирательства с работой прошивальщика):
...хотя почему он не может этого сделать по тому, какая ширина данных приходит с шины -- хбз; есть ли в PCI эта ширина?
31.01.2022: пытался найти ответ на этот вопрос, несколько раз.
On the PCI bus, four signal lines called Command/Byte Enable are used to indicate the transaction type. Of the 16 possible values, 12 are currently defined. (During the data phase, these lines are used to show which of the bytes on the 32-bit bus contain valid data, hence the 'Byte Enable.')
Но вот как КОНКРЕТНО работают эти C/BE -- я так и не нашёл. Чисто по числу бит (4 на PCI32, 8 на PCI64) напрашивается, что это битовая маска байтов (какие участвуют, а какие нет); но это как-то совсем бредово -- позволяет передавать слова "с дырками".
Кстати, в PCI64 разъём длиннее, и в нём, кроме "Address/Data 32"..."Address/Data 63", есть ещё "C/BE4"..."C/BE7".
Побудительным мотивом явилось то, что в котовском драйвере vmei.c используется подход "уровни прерываний -- ничто, вектора -- всё!".
И интересующаяся этим вектором софтина должна открыть соответствующий файл, мониторить его select()'ом и читать read()'ом.
vmei_isr()
-- при
возникновении события делает
"level_mask |= 1<<level
",
каковой потом читается read()'ом -- т.е., возвращается БИТОВАЯ МАСКА
УРОВНЕЙ, на которых данный вектор возникал с последнего чтения.
Смысл такого подхода "битовая маска уровней" лично мне неясен: в какой практической ситуации оно может потребоваться? Тем более, если уж декларируется "уровни -- ничто!": тогда уровень вообще никого не интересует, а важен лишь факт наличия вектора.
Косяки данного подхода:
Тем самым нарушается модель "читать можно только после готовности на чтение по select()'у", и по факту оно работает как ioctl() "прочитай текущую известную маску".
08.07.2021: так вот: решил копнуть чуть глубже и разобраться, КАК "правильно" и ПОЧЕМУ. Результаты:
0x100 + 4 * irql
, а CAENVME_IACKCycle()
'у надо
просто явно указывать УРОВЕНЬ, для которого выполняем цикл IACK.
Так вот, там сказано:
There are seven interrupt acknowledgement lines, IACK1 through IACK7 (active low).
Я на эту информацию должен был натыкаться раньше при чтении описания шины VME.
(Что-то как-то стандартизовано только под RTEMS, с закосом под имитацию старых контроллеров на M68K под VxWorks. Но там по факту одна-единственная платформа -- MVME3100 на TSI148.)
Никаких вариантов, очевидно, и не предполагалось.
Но с исчезновением M68K простота обращения через память превратилась в адские пляски с обращениями через бридж, а со сменой парадигмы от "всё делаем прямо в ядре RTOS" в сторону многослойного структурного программирования и сама модель работы VME стала категорически неадекватна.
The address and Address Modifier (AM) codes that are generated by the Tsi148 are functions(это ЕДИНСТВЕННОЕ место в документе, где встречается словосочетание "Address Modifier").
of the mapping of the PCI/X memory space as defined above or through DMA programming
(see Section 8.4.88 on page 297 and Section 8.4.89 on page 301).
Прямо напрашивается написать статью "Regarding 32-bit VME access from 64-bit host", где бы сия идея излагалась.
Но способ настолько очевидный, что вознкиает вопрос -- а не запатентовал ли уже кто-нибудь эту идею? Поискать бы патенты...
09.06.2023: ага, попробовал на patents.google.com -- ладно, что синтаксис поиска так себе, но по запросу "(vme 32-bit 64-bit bus)" ещё и результатов толпа (и бОльшая часть -- вовсе не про VME)...
С ним в конечном итоге разрулилось проще -- сделал очень простой специализированный драйверок modbus_tcp_rb_drv.c ("rb" - Радиационные Блокировки), который по факту ничего не знает про Modbus, а просто отправляет и принимает пакеты фиксированного бинарного формата.
Но в процессе -- ДО того, как сделать драйверок, поскольку там было малопонятно, что к чему -- почитал про Modbus и даже сформулировал некие идеи/подходы на перспективу.
19.08.2020: источники информации:
Она может служить хорошим введением, плюс там кратко и наглядно описаны форматы посылок и коды команд.
Там вначале краткое резюме протокола, а потом уже специфика реализации.
Краткое резюме того, что я узнал в процессе чтения тех источников о Modbus и что я о нём думаю:
1-битовые, насколько я понял, читаются группами (пишу по памяти через пару месяцев, могу и путать).
Более сложные типы -- 32-битные, вещественные, строки, массивы -- в стандарте не специфицированы и в каждой реализации, где они требуются, делаются по-своему.
Но в реальности эти "адреса/номера" не имеют никакого гарантированного отношения к протокольным адресам соответствующих регистров, и даже само это "соглашение" не является частью стандарта. Это просто наследие от первой реализации Modicon (автора стандарта) от 1979 года.
По МОЕМУ опыту разбирательства с нашим ПЛК -- от этого соглашения только вред; лучше бы просто писали номера/адреса.
Так что -- ТОЛЬКО периодический поллинг.
У них есть идеологически как бы "общая часть", под названием PDU (Protocol Data Unit), состоящая из кода функции и данных пакета, но с точки зрения кодинга я не увидел глубокого смысла в унификации на уровен именно кода.
Дело в том, что между этими протоколами по факту нет вообще почти ничего общего: у них даже форматы пакетов разные, так что обобщать там особо нечего -- разве что операции "отправить пакет" и "принять пакет".
Но эти вещи намного удобнее было бы делать в парадигме "интерфейса" -- в стиле наших layer'ов.
Теперь что касается появившихся в процессе оного изучения (но ещё ДО решения задачи с рад.блокировками путём примитивного драйвера) идей:
Тут же был припомнен mqtt_mapping -- точнее, сделанные для него API и то, как он ими пользуется для составления карты.
Т.е., глядеть на тип канала через CxsdHwGetChanType()
по его
номеру и так понимать, к какому виду относится.
Но это решает лишь проблему определения типа, но не соотнесения номеров драйверных каналов и modbus'овских.
Но тут опять же остаётся вопрос "обратного маппирования" -- из modbus'овского пространства {ТИП,НОМЕР} в CX'ное.
На СЕЙЧАС наиболее простым и надёжным видится вариант (b) -- маппирование через auxinfo: никакого "творческого" поиска, всё линейно (только в init_d() проверить на неперекрытие).
Впрочем, если/когда встанет вопрос о РЕАЛЬНОЙ потребности в реализации такого "универсального" драйвера -- тогда и посмотрим, на примере конкретной железки.
19.12.2022@утро, мытьё посуды: так надо drvinfo пользоваться! В нём указывать каждому каналу, на какой ТИП и НОМЕР modbus-канала он маппируется.
(@при записывании заметил) Однако, ровно эта же идея приходила в голову и тогда, летом 2020-го -- вариант (c)...
А я ответил, что подозреваю её в непригодности для использования во фреймворках систем управления, т.к. там наверняка всё сделано по модели "отправили запрос и зависаем в ожидании ответа".
(Косвенным свидетельством в пользу этого было то, что EPICS'ный Modbus-support всё делает сам, не пользуясь этой библиотекой. Но мало ли -- вдруг она просто появилась позже.)
10.08.2023: но подозрения подозрениями, а лучше бы знать точно. Поэтому я взял исходники libmodbus-3.0.8 (из EPEL'а от RHEL7) и заглянул внутрь.
read_registers()
в
src/modbus.c (которая и осуществляет обмен) -- там сначала
send_msg(), а потом сразу же receive_msg()
.
/* Modbus_Application_Protocol_V1_1b.pdf (chapter 6 section 1 page 12) * Quantity of Coils to read (2 bytes): 1 to 2000 (0x7D0) * (chapter 6 section 11 page 29) * Quantity of Coils to write (2 bytes): 1 to 1968 (0x7B0) */ #define MODBUS_MAX_READ_BITS 2000 #define MODBUS_MAX_WRITE_BITS 1968 /* Modbus_Application_Protocol_V1_1b.pdf (chapter 6 section 3 page 15) * Quantity of Registers to read (2 bytes): 1 to 125 (0x7D) * (chapter 6 section 12 page 31) * Quantity of Registers to write (2 bytes) 1 to 123 (0x7B) * (chapter 6 section 17 page 38) * Quantity of Registers to write in R/W registers (2 bytes) 1 to 121 (0x79) */ #define MODBUS_MAX_READ_REGISTERS 125 #define MODBUS_MAX_WRITE_REGISTERS 123 #define MODBUS_MAX_RW_WRITE_REGISTERS 121
Оный "Modbus_Application_Protocol_V1_1b.pdf" нагуглен, ссылка добавлена к источникам информации выше.
Писать в него будем готовые рецепты для своих типичных задач, плюс разные мысли, возникающие по результатам изучения.
19.08.2020: ну-с, начнём -- с ругани.
(Хотя задачи-то очевидно крайне близкие и связанные!)
cp
(и тогда юзер берёт на себя заботы о корректной пересблоке)
-- этим прекрасным людям, похоже, в головы не пришло.
20.08.2020: осенило -- есть ведь ещё одна причина, уже вполне техническая и рациональная: в случае ПАРАЛЛЕЛЬНОЙ разработки, когда в один файл (в РАЗНЫЕ его части) коммитятся изменения из нескольких параллельных веток, не очень ясно, какой timestamp нужно ставить.
Но такие параллельные изменения, хоть и являются "основным фокусом" для разработчиков git'а -- это всё же отдельный частный случай, далеко НЕ всеобщий.
А среди индивидуальных кодеров и небольших команд более частая ситуация "одним файлом в конкретный момент занимается только ОДИН человек", и для никакой проблемы выбора нет.
Некоторое время назад (в октябре?) был найден текст-tutorial
Солидная презентация на 61 слайд.
Из Википедии удалось узнать, что конкретно MicroTCA.4 -- это под-стандарт, ориентированный на использование в High Energy Physics. 02.01.2021: только в .4 есть rear I/O (MicroRTM).
31.12.2020: некоторое количество статей, описаний и стандартов по MicroTCA:
Простым языком описаны причины появления TCA, его достоинства -- механические (размер), электрические (48V, 200W/плата), коммутационные (топологии, типы интерконнектов, rear I/O). А также причины появления MicroTCA и почему он вытеснит cPCI.
Описание преимуществ PCIe и обзор стандартов на его основе (SHB, cPCI Express, ATCA/AMC).
Расширение предыдущего материала. Детальнее описаны коннекторы (ZD, схема распределения линий по коннекторам, зоны 1-3).
Больше маркетинговое: как у MicroTCA всё зашибись, где он хорош, и много о конкретных продуктах Kontron (которые сейчас уже discontinued...).
Слайд 15 о взаимоотношениях ATCA и MicroTCA:
Чувак делится своими впечатлениями с воркшопа. И, в частности:
(Наткнулся, пытаясь нагуглить, что же используется в ITER'е -- памятуя рассказанное Денисом Степановым на институтском семинаре в районе года назад, когда он говорил, что у них в основном PXI-express и *TCA, а на вопрос "не рассматривался ли VME?" сильно удивился и ответил, что, может, только в шутку.)
Там есть краткая история модульных стандартов -- NIM, VMEbus, VXS, CompactPCI, PCIe с вариантами, TCA, AMC, MTCA (увы, без CAMAC); для каждого краткая сводка причин успеха/неуспеха и история применения в ЦЕРН.
Наиболее ценен слайд 18 "The CERN accelerator controls systems - is MTCA the future?". И там в секции "What we don't like" квинтэссенция:
(Самое странное, что наткнулся я на него случайно, гуглением по "huawei microtca"...)
P.S. Там упоминается некий "FELIX": это разработанная для ATLAS программно-аппаратная платформа (Front End LInk eXchange),
(На часть этого есть ссылки из статьи MicroTCA в Википедии, часть нагуглил.)
Так вот: судя по описанию CompactPCI Serial в Википедии, именно в НЁМ очень развит Ethernet-интерконнект -- "Ethernet Full Mesh Architecture", "8 x Ethernet 10GBASE-T".
Может, Фатькин тогда просто спутал?
31.12.2020: некоторое время назад захотелось разобраться, в чём отличие TMDS от LVDS -- просто для собственного понимания, т.к. на СКИФе предполагается использовать в системе синхронизации какой-то тип сигнала, по смыслу являющийся LVDS'ом.
Так вот -- внятного объяснения не нагуглилось, а только некое описание того, как кодируется сигнал для мониторов и LCD-панелей:
Так вот -- выяснил: там в API драйвера отсутствует возможность следить за появлением пакетов по select()/poll(), а только либо зависать на ожидании, либо периодически поллить.
tdrv010Read()
, которой можно указать
параметр Timeout
в миллисекундах, в т.ч. "-1 to wait
indefinitely, or 0 to return immediately".
tdrv010Open()
возвращает собственный handle, а не файловый
дескриптор.
Так что -- оно было категорически неюзабельно, вследствие чего TPMC810 нельзя было пользоваться вплоть до появления пашиного plx_pci, который сейчас есть в ядре и стал стандартом де-факто для большинства CAN-адаптеров под Linux.
ЗЫ: забавная деталь: на последней странице (57) в разделе
"Known issues" они жалуются, что штатный ядровый plx_pci может мешать
загрузке ихнего драйвера, и рекомендуют его запрещать путём
"blacklist plx_pci
" в /etc/modprobe.d/blacklist.conf
:) Гы-гы-гы!!!
На ICALEPCS-2017:
Статья скорее "для галочки", из стоящего только оценка потоков данных.
А далее утверждение "Experience of LIA-2 [3] shows that use of widely-usedcontrol system software could reduce costs" (камень в мой огород? :D), и позже упоминание о разрабатываемой подсистеме "Facility State and Regime Managment".
Да только мы знаем, из рассказов Бака/Старостенко-jr, что даже осенью 2020-го при каждом включении ЛИУ-20 приходилось вбивать настройки для всех сотен модуляторов ВРУЧНУЮ -- т.е., до сих пор нет сохранения/восстановления режимов; а уж в 2017-м, значит, тем более не было.
Откуда некоторый вопрос -- чему из заявлений во всех статьях можно верить, а что является скорее обещанием на будущее.
Отдельный раздельчик в "Знаниях" создавать смысла нет, а вот небольших заметок оно заслуживает.
15.02.2021: итак:
Оно даёт суб-микросекундную точность.
Отдельно нарыто, что как минимум некоторые Intel'овские адаптеры поддерживают оное -- например, I340 и I350, причём ТОЛЬКО для пакетов IEEE 1588; источник -- "Which Intel NICs support hardware timestamps (hardstamps)" 2015 года.
25.02.2021: пообщался с Чеблопашей -- он после того разговора полторы недели назад занялся изучением PTP касательно возможности использовать его с SOM'ами.
26.02.2021: Паша разобрался чуть больше: "Демон обеспечивает бОльшую часть функционирования протокола -- работает «дирижёром», отправляя пакеты, а чип занимается лишь вычислением поправок".
Так вот: у Aruba есть "младшая" линейка с поддержкой 1588, а в старшей (которую мы предварительно выбрали для СКИФ) оной поддержки нет.
Эта аппаратная поддержка заключается в том, что свитчи являются "прозрачными" для протокола, не внося искажений. Как сие возможно -- МНЕ пока не вполне ясно; может, свитчи интеллектуально корректируют содержимое PTP-пакетов для отражения там задержки на прохождение между портами этого свитча?
В силу необходимости начать хоть что-то понимать в этом вопросе.
22.01.2021: по результатам обсмотра огрызков презентации контроллера источников от 6-й лаборатории (у Карнаева нашлась):
Т.е., это похоже на CAEN'овское подключение контроллеров оптикой, а также и на CAN-bus.
Но больше всего это похоже на Token Ring.
...и есть неясность, бегают ли там кадры только в одном направлении кольца, или в обоих возможных (дуплекс ведь, так что можно -- тем самым удваивая пропускную способность до 200Mbps).
Найдено при гуглении на тему "ethercat criticism".
Кстати, очень сильно смущает отсутствие какой-либо критики EtherCAT -- мне найти не удалось, везде сплошные восхваления (в т.ч. в Википедии).
24.01.2021: спросил мылом у Дуброва (как человека, детально разбирающегося в обычном Ethernet), "может, есть какие-нибудь знания/соображения насчёт EtherCAT, какие в нём могут быть подводные камни?". Его ответ оказался очень полезен:
Отсюда следствия:
...и, видимо, обрабатываться они будут устройствами при получении не в "исходном" направлении (по противокольцу), а уже при возврате -- поскольку будут приходить с 0-го на 1-й (а у последнего в цепочке -- пройдут по кругу "1,(2),(3),(0),EPU,1").
И таким образом можно определить разрыв (без каких-то дополнительных действий?) -- что ответные пакеты начинают приходить из того же порта, а отличать их от обычно приходящих в этот порт пакетов изначально от второго порта можно по WKC (который при обычном прохождении обратной петли должен остаться ==0), плюс, по самому значению WKC можно (опять же без доп.действий?) сразу же понять, на каком именно хопе разрыв связи.
03.02.2021: создаём раздел.
03.02.2021: в качестве исходной точки берём
Он называется "COB-ID" (communication object identifier).
Ну уже сейчас вполне ясно, почему Козак предпочёл сделать свой протокол, а не использовать CANopen: шибко уж он сложен, по сравнению с CAN-Kozak.
Вот и создаём раздел.
05.02.2021: конкретные источники информации:
"Event System with Delay Compensation VME-EVM-300, mTCA-EVM-300, VME-EVR-300, mTCA-EVR-300, PCIe-EVR-300DC Technical Reference VME-EVM-300 Firmware 22050207 VME-EVR-300 Firmware 12090207 PCIe-EVR-300DC Firmware 170A0207 mTCA-EVR-300 Firmware 180E0207"
"Event GeneratorcPCI-EVG-220, cPCI-EVG-230, cPCI-EVG-300, PXIe-EVG-300 and VME-EVG-230 Modular Register Map Firmware Version 0006"
"Event Receiverc PCI-EVR-220, cPCI-EVR-230, PMC-EVR-230,VME-EVR-230, VME-EVR-230RF, cPCI-EVRTG-300, cPCI-EVR-300, PCIe-EVR-300 and PXIe-EVR-300 Technical Reference Firmware Version 0007"
13.02.2021: эх,
03.03.2021: зато у них, похоже, НЕТУ любимого батраковцами извращения "R1c" (битик сбрасывается сразу после чтения регистра).
(Всё, что нашлось "авто" -- всякие авто-остановы/авто-сбросы счётчиков после переполнения и т.п.)
04.03.2021: но документация и система команд всё же далеки от идеала:
Например, в "Table 17: Event Generator Register Map" на стр.30 указан 0x060 EvanControl "Event Analyser Control Register", но в дальнейшей детализации этого адреса вообще нет, а есть только байт 0x063 под тем же именем.
...видимо, это следствие VME'шной big-endianness'ности, но всё равно дичь -- ведь адрес в детализации попросут не соответствует таблице (не говоря уж о том, что в PCI эта endianness может и отличаться).
Т.е., тот же уродский дуализм, что и у батраковцев (например, у L_TIMER'а регистр EXT_CSR/0x400074, который на чтение "возвращает состояние «ворот»", а на запись позволяет разрешить или запретить внешний запуск).
(Кажется, ещё с полгода назад слегка изучал эту тему, но вот записей, оказывается, не оставил -- вплоть до сегодняшнего дня ни единого экземпляра буквосочетания "UIO" в этом файле не было.)
10.10.2022:
12.10.2022@М-46: ключевое слово при конфигурации UIO -- "device tree".
24.10.2022: Лёша Герасёв прислал пару ссылок на описание/документацию по "device tree" от профильных контор.
Т.е., некий "uio4624", чтобы можно было делать "adc200me@uio4624"; это, конечно, малоосмысленно, но вот какие-нибудь другие PCI/PCIe-устройства -- вполне.
18.07.2024: изучалась тема в несколько приёмов, с ясностью и документированностью тут большие проблемы -- описание крайне невнятное, фиг поймёшь что да как, так что часть информации ниже является результатом моих разбирательств.
Результаты первоначального изучения вопроса с привлечением помощи других:
Если нужно читать только давление, то проще всего впаятся прямо в кабель (один из разъемов) от датчика давления к контроллеру. Там есть аналоговый сигнал (логарифм измеряемого тока). Перевод в давление и распайку разъема можно найти в паспорте на датчик давления. Кажется, других способов читать данные с этих контроллеров мы еще не применяли.
Если паспорта на датчик давления нет, то можно будет спросить Сашу Жукова.
Результаты рытья вопроса лично мною (начато ещё в середине июня, потом шло перемежаясь с опросом вакуумщиков):
ЗЫ: но это СЕЙЧАС стало понятно, что и неудивительно -- ведь "пульт"-то и сам является клиентом.
Первый же результат -- «EPICS driver / support for "USS" Device: Leybold Turbovac iX» за 6 Oct 2022, это первое увиденное мною упоминание "USS protocol", и оттуда ссылка на...
Там видно некоторое количество информации насчёт работы через RS485, Ethernet, а также упоминается USS.
Далее адаптеру компа был назначен 169.254.157.1 и проведён...
-sX -p 1-31000
"
открытых портов не обнаружилось.
А вот тривиальный "-sT -p 1-31000
" (connecT scan) показал
единственный порт 80, т.е. http.
...а я-то надеялся, что найдётся какой-нибудь "USS TCP"...
Но описания девайса там нет.
The TURBOVAC allows through both standard and optional interfaces con--- т.е., ровно тот документ, что я ранее и нашёл. Т.е., он применим и к непосредственно контроллеру турбика.
trolling of the pump and setting up of some pump functions according to
specific requirements. The Operating Instructions 300450826 Serial
Interfaces for TURBOVAC i/iX offers detailed information on the RS 232,
RS 485, Profibus and USB interfaces of the TURBOVAC.
Короче -- проще считать, что имеем дело с просто 485-м, хотя, возможно, на стороне устройства может требоваться конфигурирование.
Ну и общий вывод:
20.07.2024@вечер, засыпая: кстати, а для утилиты командной строки есть готовая console_mon_util.h -- она как раз примерно для такого готовилась почти сразу, чтоб подходить для произвольных подобных утилиток.
connect()
& Co. на open()
& Co., благо, соответствующий код в
modbus_mon.c имеется, причём максимально совместимый (т.к. в
TCP-варианте делался на основе modbus_tcp_drv.c, который делался на
основе modbus_tcp_rb_drv.c).
18.07.2024: Создать раздел "trainer_drv" и найти мыло с описанием алгоритма тренировки. 16 Feb 2023 16:56:36 "Re: linac training" Message-ID: 7541da20-c671-2221-132e-7bfbbb1a55ef@starnew.inp.nsk.su
20.07.2024: изучаем.
|STX=0x02|LGE=len| ADR | ...data... | BCC|
-- начинается всегда с ASCII-кода STX, Ctrl+B.
ЗАМЕЧАНИЕ: Судя по Serial_Interfaces_for_TURBOVAC_300450826.pdf стр.22, у Leybold всегда LGE=22.
Биты 5,6,7 -- это Broadcast, Mirror и Special соответственно. Mirror -- вернуть пакет как есть (ping-pong).
Обычные посылки -- адрес всегда 1-31 (0 -- хбз... Master?).
ЗАМЕЧАНИЕ: У Leybold'ов адрес устанавливается параметром #37, и Serial_Interfaces_for_TURBOVAC_300450826.pdf стр.29 утверждает, что по умолчанию значение 0.
ЗАМЕЧАНИЕ: Вот только Serial_Interfaces_for_TURBOVAC_300450826.pdf стр.22 почему-то утверждает, что сумма считается БЕЗ начального байта STX, в то время как uss_24178253_spec_76.pdf стр.A-5 (PDF:11) явно показывает, что STX учитывать НАДО.
04.08.2024: хотя, возможно, на стр.22 просто так косноязычно алгоритм изложен -- фраза "Checksum (i = 0) = byte (i = 0)" означает "возьми 0-й байт в качестве начального значения" и дальше считать для прочих "i from 1 to 22", а я последнюю фразу понял как "считать НАЧИНАЯ с 1-го байта". ...но формулировка действительно кривая.
For word information (16 bit) in the net data block, the first byte is always the high byte (first character) and then the low byte (second character). The corresponding is valid for double words: First, the high word is sent,followed by the low word.
The time interval between the last character of the task telegram (BCC) and the start of the response telegram (STX) is known as the response delay time (Fig. 5.3). The maximum permissible response delay time is 20 ms; however it may not be less than the start interval. If node x does not respond within the maximum possible response delay time, the "node x does not transmit" error message is stored in the master. The master then transmits the telegram for the next slave node.
Так что наш стандартный таймаут 100мс даже либеральнее этих требований.
The structure of the net data block in the telegram is independent of the specification of the protocol with which the net data is transferred. The structure including the contents of the net data essentially correspond to the definitions for cyclic data transfer via PROFIBUS (PROFIBUS profile "variable-speed drives" /1/). Thus, the user is guaranteed, that he can access the process data (= control / status words and setpoint / actual values) with the same access mechanisms, independent as whether this is realized using USS® or PROFIBUS-DP/FMS.
И, похоже, там ВСЕГДА LGE=22.
04.08.2024: нифига -- судя по примерам, ЗАПРОС может быть и короче. В частности, "Example 3: Read parameter 150" на стр.62 включает только PWE (=PKE,IND,VALUE), вообще БЕЗ каких-либо PZD. А примеры 1 и 2 страницей ранее включают только PZD1,PZD2 -- без PZD3-PZD6. Собственно, иное было бы сложно, т.к. PZD1 в запросе у Leybold -- это (стр.22) USS Control Word, которое содержит (стр.26) в т.ч. и bit0 "Start/Stop", так что невозможно отправить вообще никакой запрос с PZD1, не повлияв на работу.
Причём прямо в типе запроса или ответа указывается размер -- 16 или 32 бита (правда, коды весьма бардачны -- нету бита "размер", просто у разных кодов разная битность значения).
И прямо там же "ответы-ошибки": 0b0111 -- "The frequency converter can not run the command", 0b1000 -- "During a write access: no permission to write".
03.08.2024: а "зарезервированный" байт 5 всегда =0 потому, что это всего лишь старший байт IND'а в big-endian (а IND'ы у Leybold не более 255): в uss_24178253_spec_76.pdf на стр.C-3/PDF:32 сказано "The index is always one word long", а в разделе 4.2.2 на стр.C-18/PDF:47 это слово рассмотрено побитово и там у старшего байта кодировка замороченная.
01.08.2024: нифига -- есть ещё строки: параметры 312-316,
350, 355, 395, 396, которые U16[0..NN] "One ASCII char per index"; очень
похоже на MODBUS_CONV_STRING_LOW
.
Стр.31 на примере параметра 171 говорит
U16 [0..253] Indexed parameter for storing the most recent 254 error codes. The individual error memory entries are accessed via this parameter with additional index number. The last error code is accessed with index 0 and the oldest with index 253."
Тут уж ПОЛНОЕ вычитывание возможно только МНОЖЕСТВЕННЫМИ командами:
примерно как задумывалось 21-06-2023 для Modbus для векторов, превышающих
размер одиночного Modbus-пакета: "слайсами" с эккаунтингом принятого -- как
делается в tsycamlib.c с rest
/ispresent[]
и в ottcam_drv.c с rest
/is_rcvd[]
.
Другое дело, что зачастую это хоть связанные (однотипные настройки для разных аксессуаров), но различные значения, просто скрывающиеся под разными индексами одного номера параметра, и адресоваться к ним стоит индивидуально.
...правда, в начале там подзаголовок "Annex: Profibus strings", откуда возникает вопрос -- а как соотносятся Profibus и USS?
Сличил ещё раз -- неа, примеры именно для Profibus. Их, конечно, можно использовать чисто для справки (переставляя байты), но той же контрольной суммы BCC там не подсмотришь.
modbus_conv.h
, со всей
инфраструктурой.
21.07.2024@утро, после завтрака,
мытьё посуды: или и вовсе НАПРЯМУЮ тот код использовать, сделав
просто
#include "modbus_conv.h"
-- ибо нефиг плодить копии; вот только надо будет изготовить отдельную
табличку, по которой вести поиск имён конверсий, чтоб а) лишние
исключить (никаких "LE", "STRING" и "BCD") и б) имена были бы просто
"INT32" и "FLOAT32"
...но в любом случае остаётся вопрос, как обходиться с тем, что прямо сами параметры могут иметь разный размер в зависимости от номера параметра. Или оставить ModBus'ный синтаксис "ADDR/COUNT", только сделать опциональный "/COUNT" как бы "авторитетным", чтоб именно так и указывать, сколько там слов в ответе?
22.07.2024: похоже, напрашивается синтаксис, аналогичный Modbus'овскому, но упрощённый: вроде "U16[%DPYFMT]:ADDR[/COUNT]"; конверсия по умолчанию U16 (=MODBUS_CONV_UINT16), COUNT=1.
ZZZZZZZZZZZZ
26.07.2024: давно бродила мысль: раз с протоколом такие непонятки, что неясно даже, считать контрольную сумму начиная с 0-го байта (STX) или с 1-го (LGE), то надо попробовать подоткнуться к "пульту" USB-485-адаптером и посмотреть, какой пакет туда "пульт" отправит при включении.
Подоткнулся -- а НИЧЕГО он туда не отправляет.
29.07.2024: после того эксперимента возникла другая идея: а может, надо было в web-интерфейсе что-то включить, чтобы "пульт" обращался к помпе не по USB, а по 485?
КАКИЕ?
30.07.2024: нашёл, какие -- "TURBOVAC i(X) TURBO.CONTROL i Software Description Operating Instructions 300702826_002_C0" говорит, что login/password=user/user.
Попробовал -- фиг: "Login not possible due to setting on display". Причём без разницы, какой пароль.
06.08.2024: ещё попытка включить в пульте работу 485:
Пробовано разными версиями Firefox: и x10sae'шным 45, и p320t'шным 78, и свежим 115 -- результат одинаков.
Попытка просмотра исходников (Ctrl+U, Page Source) показала, что какая-то реакция в HTML вроде есть. Но не работает. Или этим дятлам Internet Explorer подавай?
Так что, увы -- идея включить работу с 485 провалилась.
Спросить совета у вакуумщиков?
21.07.2024: гугление "pfeiffer rs485 protocol" дало:
Резюме: там СВОЙ протокол, чисто текстовый/ASCII, строки разделяются символом 0x0D (Ctrl+M). (А я-то думал -- вдруг там Modbus, тогда можно будет использовать готовую поддержку...)
Поиски сначала в январе 2023, а потом в январе 2024 ничего не дали -- такое впечатление, что внятного описания протокола вообще нет, а конкретно для EPICS просто есть реализация "поддержки" от самого Phytron. И было решено "пусть козилабовцы сделают всё для СКИФ, а потом посмотрим -- да хоть сниффером сети глянем, что там шлётся в serial-link через MOXA".
Но вот сегодня решил ещё разок наудачу поискать и что-то нарылось -- пока неясно, то ли это вообще, но записать надо, так что раздел создаём.
03.08.2024: просто что-то вроде протокола действий и находок.
Помнится, полгода назад я это название встречал и тогда, ЕМНИП, пришёл к выводу, что это внутренний язык, на котором идёт программирование этих контроллеров -- пишется прямо целая программа, загружается туда, и потом по единственной внешней команде контроллер всю программу исполняет. Но что это НЕ протокол общения с контроллером по 485.
Оттуда ссылка на "Manual (PDF) phyLOGIC instruction set" --
Там встречаются слова "485", "STX" и "telegram" -- т.е., какое-то отношение к взаимодействию через RS485 сие описание имеет.
В частности, есть такой фрагмент:
The send telegram from PC via RS interface is defined as: <STX> | Address | Instruction | Separator | Checksum |<ETX> The response telegram (always for address 0-9, A-F) is defined as: <STX>| ACK | Answer |Separator | Checksum |<ETX> or <STX>| ACK | Separator | Checksum | <ETX> or <STX>| NAK | Separator | Checksum | <ETX>
-- да-да, протокол ПОХОЖ (наличием STX и ETX), но ОТЛИЧАЕТСЯ.Factory setting: 57 600 Baud 8 Data bits 1 Stop bit No parity Protocol:: <STX> | Address | Data | Separator | Checksum | <ETX> | <CR> | <LF>
The ServiceBus protocol provides an extensive instruction set for bidirectional communication between Phytron +power stages and the PC or controller world. The physical connection can be made via USB, CAN, RS232 or RS485 depending on the type of power stage.
The ServiceBus protocol contains instructions for setting power stage operation parameters, reading parameters, status or e. g. error messages in up to 32 power stages. This powerful tool enables implementation of different individual functions.
- Monitoring
- Remote diagnostics
- Remote intervention
- Functions for conditional parameter change
- ... etc.
In addition to a graphical implementation of the ServiceBus in the ServiceBus-Comm software, the ServiceBus instructions can also be integrated in customer specific programs at PC or controller level.
Там опять есть слова "485", "STX" и "telegram", плюс фрагмент
и дальше табличка с расшифровкой/описанием полей. Числа, однобайтные -- адрес и checksum -- передаются парами 16-ричных символов.A well-defined protocol should be followed to assure a safe data exchange: Asynchronous transmission, 8 bits/byte, 1 stop bit, 1 parity bit Transmission rate: 57600 bps Permanent telegram format: <STX> <address_H><address_L> <instruction> <value> : <csh> <csl> < ETX>
Ну и есть таблицы с описанием разных инструкций.
Итого: теперь когда будем сниффером смотреть на трафик на СКИФе, то (надеюсь!) хоть будет лучшее понимание того, на что мы смотрим.
06.08.2024: ага, "смотреть" -- щас!!!
Как выяснилось, связь с поставленными Козилабом девайсами не через RS485, а по Ethernet -- прямо в корзине (на пару десятков устройств) стоит контроллер.
-O
) оказалось малополезным:
nmap -O -sX phytron-kg -p 1-30000
" (Xmas scan) один
раз выдало (выдача завёрнута для читабельности)
но более этот порт не проявлялся.PORT STATE SERVICE 3432/tcp open|filtered osdcp MAC Address: 70:B3:D5:7B:E4:79 (Ieee Registration Authority) Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port Aggressive OS guesses: WIZnet W5100 TCP/IP chip (90%), Compaq Tru64 UNIX 5.1B or HP OpenVMS 8.2 - 8.3 (89%), FreeBSD 5.5-STABLE (89%), HP OpenVMS 8.3 (89%), WIZnet W3150A TCP/IP chip (89%), Apple Mac OS X 10.5.6 (Leopard) (Darwin 9.6.0) (89%), FreeBSD 6.1-RELEASE (89%), FreeBSD 8.0-STABLE (89%), HP OpenVMS 8.2 (89%), OpenBSD 4.3 (89%)
nmap -O -sS -p 3400-3500,20000-24000 phytron-kg
" (TCP
SYN scan) --
а в другой раз "filtered" былPORT STATE SERVICE 20768/tcp filtered unknown 22222/tcp open easyengine 23232/tcp open unknown MAC Address: 70:B3:D5:7B:E4:79 (Ieee Registration Authority) Device type: specialized Running (JUST GUESSING): WIZnet embedded (85%) OS CPE: cpe:/h:wiznet:w5100 Aggressive OS guesses: WIZnet W5100 TCP/IP chip (85%) No exact OS matches for host (test conditions non-ideal).
(возможно, этот "filtered" -- что-то врЕменное).22590/tcp filtered unknown
nmap -O -sT -p 3400-3500,20000-24000 phytron-kg
" --
PORT STATE SERVICE 22222/tcp open easyengine 23232/tcp open unknown
nmap -O -sU -p 1-65000 phytron-kg
" -- пустота.
WIZnet W5300 Hardwired Network Protocol Embedded Ethernet Controller is a 0.18μm CMOS technology single chip integrating a 10/100 Ethernet controller, MAC, and TCP/IP."
W5100 Datasheet -- PDF на 72 страницы, где можно понять дату выхода по строке "Ver. 1.0.0 | Dec. 21, 2006 | Released with W5100 Launching", плюс там же есть фраза "0.18 μm CMOS technology".
Слова "Ethernet" там нету, но что есть:
The following Protocol settings are possible:
Only STX/ETX
STX/ETX and checksum
STX/ETX and address
Full protocol
This manual describes the "Communication Protocol" which is used to control the "Phytron Stepper motor Control units" of the IPP, GSP, GCD and GLD devices.
In the following the "Phytron Stepper motor Control unit" will be named "PSC".
09.08.2024: как выяснилось, а вот и нифига -- в "наших" контроллерах используется совсем другое подключение и другой протокол: в контроллере есть Ethernet (RJ45) и протокол "ADS Twincat".
Так что данный подраздельчик замораживаем (на случай если вдруг вылезет что-то через serial-link), а то будем записывать в отдельном месте.
Стал гуглить по "Cannot reference virtual member function", и первым же результатом -- "Dissassembling virtual methods in multiple inheritance. How is the vtable working?" на StackOverflow за 07-05-2014.
Там обсуждаются вопросы реализации множественного наследования и содержимого "vtable" (VMT).
Причём "не работает" -- это:
close()
подвисает на 30 секунд.
А modpoll ни одной из этих проблем не имеет.
20.03.2025: собственно, что происходило:
Думал всякое -- что адаптер может быть неисправен, кабель, само устройство; что МОЯ софтина может что-то не так делать.
Попробовал -- а он, зараза, работает!!! Данные получает...
-- Polling slave... (Ctrl-C to stop) [1]: 1 [2]: 1920 [3]: 0 [4]: 0 [5]: 0 [6]: 0 [7]: 42 [8]: 140 [9]: 0 [10]: 0 -- Polling slave... (Ctrl-C to stop) ^C
modbus_mon -Dd @19200,n,8,1:/dev/ttyUSB0 +1:40001/10 -T 1
и
modpoll -m rtu -a 1 -c 10 -t 4 -r 1 -b 19200 -d 8 -p none -s 1 /dev/ttyUSB0
-dD
" и натравливанием "конкурента" в режиме "-m
enc
" на свой NetCat, которому сделано
">/tmp.file.dat
" --
01 03 00 00 00 0A C5 CD
cat /tmp.file.dat >/dev/ttyUSB0
")
и слежка за ним же в другом окне
("cat </dev/ttyUSB0
")
также результатов не дали.
Стал разбираться, с привлечением "strace -vttfs200
" для
обоих.
Итак, что там с modpoll:
open()
, а openat()
.
Неожиданно, но вряд ли это принципиально (и хорошо, что я знаю, что это вообще такое -- спасибо покойному BugTraq).
close()
после каждого обмена.
Но когда перепроверил, то оказалось, что нет.
close()
.
Я пытался разобраться:
tcflush(the_fd, TCIOFLUSH)
перед
close()
-- не помогло.
close()
, и тогда
зависало на exit_group()
.
closing_wait
,
setserial -ga
"
значения не показывает, а говорит "infinite", и никакие попытки изменить не
дают эффекта, несмотря на root.
А с обычным /dev/ttyS0 -- изменяется.
strace
показывает дешифровку бестолково (БЕЗ
"-v
" -- только начало, а С -- малопонятные числа, без
расшифровки; но у совпадающих ioctl()
'ов они вроде совпадают),
а ltrace с его бинарником не работает.
ioctl(3, TCSBRK, 0x1) = 0
ioctl(3, TIOCSERGETLSR, ...)
, перемежающийся паузами
посредством
nanosleep({0, 100000}, NULL)
-- по 100mksec?, из которых
последний (7-й) явно что-то возвращает,
select()
'ов он переходит к чтению
(сразу 5 байт).
TIOCSERGETLSR
,
read()
, возвращающий 0,
select()
на ~1s, после истечения
которого говорит "Reply time-out!".
Выводы, результаты, что ещё делать:
21.03.2025: пробуем mbusd.
cmake
.
" эта тварь говорит
CMake 3.2 or higher is required. You are running version 2.8.12.2
Получилось, только в варианте
gcc -o mbusd src/*.c -DPACKAGE=\"_PACKAGE_\" -DVERSION=\"_VERSION_\"
-- т.к. эта в main.c эти 2 имени используются при печати версии по
"-h
", а берутся они, видимо, не из исходников каких-нибудь, а
из командной строки компилятора.
./mbusd -d -p /dev/ttyUSB0 -P 2001
(настройки COM-порта там по умолчанию "те" -- 19200, 8n1; ключи
"-s 19200 -m 8N1
").
modbus_mon localhost:2001 40001
но в ответ получаем таймаут --
# EXCEPTION 11:GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND kind=4=HOLD op=0 addr=0 num1=1
modbus_mon -Dd localhost:2001 +1:40001/10
-- то ответ приходит!!! "{1,1920,0,0,0,0,42,140,0,0}" -- числа идентичны
тем, что выдаёт modpoll.
ЗЫ: кстати, отправляет он строку "\1\3\0\0\0\n\305\315", которая
и есть та же цепочка "01 03 00 00 00 0A C5 CD
".
Ура!!! Надо исследовать внутренности и искать рецепт "правильных заклинаний".
@ИЯФ, ~15:00: Ещё эксперимент: соединяем адаптеры Вощина (ttyUSB0) GearMo (ttyUSB1) друг с дружкой -- там как раз на первом "мама" на конце провода, а на втором "папа" на адаптере. И:
modpoll /dev/ttyUSB0
" и "cat </dev/ttyUSB1
-- cat данные ВИДИТ (те 8 байт, как и должно быть).
modpoll /dev/ttyUSB1
" и "cat </dev/ttyUSB0
-- да даже просто "запустить" cat не удаётся, он тут же завершается, т.к.,
судя по показаниям strace
, происходит
read(0, "", 65536) = 0
-- т.е., просто сразу EOF.
После выдёргивания обоих USB-RS485-адаптеров GearMo возвращается к нормальному состоянию, а Вощин -- так и остаётся "сразу EOF".
cat </dev/ttyUSB0
"
с
"echo Abcd1234 >/dev/ttyUSB1
"
передала строку с одного на другой...
@ещё позже, дома: неа -- есть только ttyUSB0 и ttyUSB1
select()
'ом на все 3 состояния готовности.
Так вот: "test_select_file /dev/ttyUSB0
" говорит "r:0" --
т.е, файл НЕ готов на чтение, хотя попытка чтения из него в
"cat </dev/ttyUSB0
" возвращает 0.
Ну как так-то, а?! Что за дебильное нарушение базовых принципов?
Ладно бы ещё оно было O_NONBLOCK
-- но тогда по
read()
должно б было возвращаться -1/EWOULDBLOCK...
Видимо, надо лезть в исходники ядра и смотреть там, в каких
условиях драйверов метод _read()
может вернуть 0
.
Вот только ЧЬИ исходники смотреть -- драйвера FT232, USB'шной serial-подсистемы, или общей serial-подсистемы?
22.03.2025: набросал ещё один тестик --
work/tests/test_read_after_select.c -- для использования вместо
cat'а, с работой по сценарию "зависать на select()
и вычитывать
данные по готовности, после чего отправлять их на stdout".
test_read_after_select /dev/ttyS0
" даёт
результат "висим на ожидании, а не отваливаем".
22.03.2025: тем временем стал анализировать настройки, делаемые в src/tty.c, и сравнивать их с нашими. И-и-и -- нашёл!!!
CRTSCTS
, в то время как у
нас среди прочего ВЗВОДИТСЯ "CRTSCTS*1
".
CRTSCTS*0
" -- и оно заработало!!!
tcflush(the_fd, TCIFLUSH);
была заменена на
tcflush(the_fd, TCIOFLUSH);
-- по примеру modpoll и mbusd (оно видно в логе strace, по
ioctl(,TCFLSH, ТИП
): IFLUSH -- ТИП=0, IOFLUSH -- тип=0x02). Что
не помогло (но вряд ли мешает).
CRTSCTS (not in POSIX) Enable RTS/CTS (hardware) flow control. [requires_BSD_SOURCE
or_SVID_SOURCE
]
CRTSCTS*1
" -- загадка, во
всех ныне присутствующих файлах оно уже "*1
". Но чисто по
самой форме видно, что флаг вызывал вопросы и был то включаем, то отключаем.
Самое старое из найденного --
4DVD/BACKUP/istc.20041212.messy/drivers/kshd485_drv.c за
02-07-2004, и там уже "*1
".
Но: пока не отработана одна синхронная посылка, другие -- запрещены. Плохо!
А ведь -- e.g., для чтения разных каналов с разной частотой -- надо меть много запросов активными одновременно.
Списки запросов, тэгируемые номерами (Seq)?
Техническая проблема -- как поступать с ручками, завязанными на НЕСКОЛЬКО каналов:
Отдельный вопрос -- ручки, связанные лишь через local-regs (спец. "sid" -- "все"?).
SetValue()
.
Да, это экономит много "ресурсов", это проще, но...
Ведь этот способ обусловлен не сутью, "природой" решаемой задачи -- производства В/В и "связыванием" каналов аппаратуры с экранными ручками (как должно бы быть), а внутренней спецификой устройства транспорта (CX-протокол) и работы сервера -- которые обусловлены спецификой работы с давно помершими транспьютерами.
Так что -- надо переосмысливать и схему работы сервера, и протокол, и модель функционирования UI (Cdr+cda).
13.11.2006: вот и сегодня со Шпомером пообщался -- ему нужно просто измерить N раз один канал, для некоей статистики, а все эти длины циклов ему нафиг не сдались. Простая такая человеческая потребность.
Так что -- железно надо уходить от принудительной цикличности к возможности индивидуальной работы, которая бы эмулировала обычные, локальные-в-программе, измерения. Именно так можно будет максимально приблизиться ко всяким тулзам под dos/windows. А главное -- это ПО СМЫСЛУ задачи.
14.07.2007: недостаток систем клиент-сервер вообще, и 3-уровневой архитектуры в частности -- в том, что сеть вносит мало того, что просто задержку, так еще и "дребезг" по времени, и хоть сколько-то мелкие времена (примерно <0.1с) уже сложно гарантировать.
Воспоминание: вроде где-то (в DPS, или еще в каком-то конкуренте/предшественнике X11) была возможность "загрузить" кусочек кода прямо в сервер дисплея, и он исполнялся бы прямо там.
Идея: а можно ведь ввести и в наш сервер возможность "заказывать" не просто чтение/запись, а исполнение некоторой последовательности команд, между которыми могут быть и программируемые паузы. (Подобные идеи выдвигались для "умного" исполнителя NAF'ов.)
Как именно это реализовывать -- не очень понятно. Некоторые общие мысли:
А еще ведь хочется мочь в некий момент прочитать несколько каналов одновременно... Видимо, они должны указываться в одной команде-fork'е, а та может иметь флаг "считаться завершенной, когда будет прочитаны все каналы".
Но зато -- SNL@CA-server едва ли позволит динамическую загрузку!
А вообще, такая фича, конечно, позволит снизить остроту проблемы недо-реактивности, и позволит не прибегать к дурацким способам типа прямого коннекченья между 1-м и 3-м уровнями (клиент-драйвер), как жто сделали несчастные FESA'маны. И уж наверняка расширит возможности CX -- всякая алхимия типа "подъема энергии" станет проще и надежнее.
30.07.2007: пообщался с Гусевым, чтобы понять, как это делает он, и как это принято делать в EPICS. Ага, щас!!!
Во-первых, он этого старается не делать вообще никак. А если бы пришлось -- то по-простому, обновлял бы такое поле по приходу любого из исходных значений -- пусть это и не совсем корректно.
Во-вторых, EPICS подобную проблему особо решать и не пытается. Там есть специальные типы record'ов, которые могут последовательно либо параллельно опрашивать несколько других record'ов, и сигнализировать о завершении операции -- но и только. А такая "поддержка" немногого стоит. (Ага, а если исходные каналы -- в разных IOC'ах? Тогда сие и вовсе мимо кассы.)
01.08.2007: собственно постановка-то проблемы такова:
Крайний пример -- программа linmag, с сотнями каналов, в которую хотелось добавить несколько канальчиков от другого сервера, который чешет с частотой 10Гц. В результате все те сотни начинали обновляться 10 раз в секунду, нафиг убивая процессор.
Первое приходящее в голову решение -- вообще избавиться от групповой обработки дерева, а навешивать индивидуальные callback'и на каждый канал для каждой использующей его ручки, и в этом callback'е проводить обработку. Но -- это хреновое решение:
А теперь идея, как объединить плюсы обоих подходов и решить при этом присущие им проблемы:
Так решится проблема ручек-зависящих-от-нескольких-соединений.
Видимо, у них список просто оставлять NULL, так что они будут обновляться одновременно со своим "содержателем".
Ручки-BIGC, возможно, все равно будут работать по своим индивидуальным callback'ам. По крайней мере, можно ввести выбор -- либо оптом, со всеми, либо индивидуально. Это и будет значением флага "когда", указываемым при заказе такого большого канала -- "immediately" или "at end of cycle". С ручками-USER -- вообще пока некоторые мраки, их обдумаем отдельно.
Недостатки такого решения -- точнее, проблемы, им пока не адресуемые:
Тут дело в отсутствии в epics'ном CA самого понятия "цикла", да и вообще средств "синхронизации" для экранных ручек.
Возможно, проблему можно как-то решить на уровне cda_d_epics, введя там симуляцию цикла.
Но она в любом случае сильно путает карты всяким histplot'ам.
Отдельно стоит упомянуть запись истории: давно ясно, что там надо сделать ДВА варианта.
Для этого введем в Cdr "heartbeat"-функцию, которая вызывается раз в секунду и проходится по дереву и делает сдвиг истории тем, кому надо.
09.08.2010: в CXv2 технология "точки изменения" вставлена, и успешно работает. Так что -- идея абсолютно правильная, именно так тут и сделаем.
01.04.2014@Снежинск-каземат-11: насчёт "histring_frqdiv" -- ясно, что надо делать совсем по-другому, БЕЗ неё, а надо обновлять историю просто периодически -- см. bigfile-0001.html за 13-07-2011. Так что то про histring_frqdiv за 01-08-2007 ставим "withdrawn".
ParseKnobDescr()
),
требуется более хитрая работа -- например, парная работа с полями
(something+something_count). А архитектура-то для такого не очень
заточена -- она рассчитана на атомарные поля.
19.08.2007: решения этой проблемы есть, и даже два разных.
Но иногда содержимое структуры предопределено "свыше", как данность, и надо парсить туда. В таком случае работает второй, "обходной" способ:
rec_t *rec = (rec_t *)(((int8 *)field_p) - offsetof(rec_t, field));
Именно такой подход и используется в PARAM_fparser()
'е.
Вопросы эти наиявнейше вылезли при обдумывании системы управления карботроном -- да и с любым кольцевым бустером будет то же самое.
28.08.2007: в принципе-то, все это можно делать и сейчас, без добавления какой-либо еще инфраструктуры, просто при помощи "каналов" и "атрибутов больших каналов":
Например, если устройство поддерживает 8 таблиц -- то делаем 8 больших каналов, плюс 1 обычный командный канал (номер таблицы и флаг "бродкаст"), плюс 2 статусных канала (номер исполняемой таблицы и позиция в ней).
Конечно, это не шибко-то красиво, но работать будет.
P.S. Кстати, очевидно, что драйвер "CANDAC16-ЦАПИ" проще будет сделать ОТДЕЛЬНЫМ от обычного candac16 -- поскольку сочетать "в одном флаконе" работу в обоих этих режимах довольно некрасиво (точнее, -ЦАПИ-вариант может и позволять отдельные уставки, являясь как бы расширением обычного candac16, но использовать его ВСЕГДА -- overkill). 26.02.2013: а вот и неа, давно уже НЕ очевидно! И более того -- давно разработана и реализована модель функционирования табличных каналов в козачиных устройствах, хорошо комбинирующая ЦАПИ с одновременной работой не-задействованных в таблице отдельных каналов ЦАП. Так что считаем этот P.S.-абзац за "obsolete" (а предыдущие соображения -- вполне релевантны, и даже в основном реализованы).
Поэтому он жаждет, чтобы "параметры, не относящиеся к крейту, сохранять в другом конфигурационном файле, загружаемом автоматически" (при запуске программы -- грузим, при изменении -- автоматом сохраняем).
28.08.2007: ну и -- как, а?
Естественно, ежу понятно (а Малютину -- пока нет), что из-за распределенности рано или поздно (скорее -- рано) оне захотят, чтобы эти параметры хранились централизованно.
05.07.2008: учитывая, что мы собираемся дать драйверам возможность доступаться к другим серверам, а для этого им будет дадена cda, вечерком (уже лег в постель!) в голову пришла мысль:
Чтоб API доступа к каналам других драйверов изнутри сервера был таким же, как извне -- надо его сделать ТЕМ ЖЕ. Т.е. -- реально через обязательно имеющуюся (если драйверам вообще доступ к каналам нужен) cda. А такие "внутрисерверные каналы" -- просто еще один протокол доступа (аналогично "local:").
Тогда задача решается автоматом -- точнее, сама проблема отсутствует как класс.
Вопрос лишь в производительности/оптимальности.
15.07.2008: и, кстати, учитывая, что в сервере для возможности "frontend"'ам получать данные сразу в момент из прихода от драйверов -- ОБЯЗАТЕЛЬНО будет механизм "повесить callback на канал", то при желании будет возможно реализовать именно "быстрый" внутрисерверный channel API для драйверов.
И вообще надо б уметь кучу всякой информации из этого "дерева-БД" высасывать -- типа набора атрибутов большого канала, с диапазонами etc.
Вопрос -- КАКУЮ информацию стоит отдавать (чтоб именно НУЖНУЮ, а не тупо давать полный доступ ко всему, что знает сервер), и как, "с какой парадигмой"?
26.07.2008: итак:
Или, может, лучше символом? Буквы b,s,i,l,f,d (byte, short, int, long-long, float, double); целые -- фиксированной битовости (8,16,32,64), а вещественные -- в зависимости от платформы.
Реализация remote_drv.c станет весьма нетривиальной.
P.S. Если ЭТОТ вопрос решить/проработать, то из сложностей останутся только временнАя характеристика работы -- "когда" отдавать данные. Ну и, плюс, способы работы с большими каналами -- но это уже вытекает почти автоматом из типов данных и временной характеристики.
17.12.2009: маленькое дополнение насчет строк: длину надо передавать как LE-int32 (а не le-int16) -- во-первых, для унификации (раз уж всё равно все числа 32-битные), а во-вторых, чтоб уж были допустимы строки и длиннее 65535 символов.
Чуть позже: а вообще -- актуально ль это? Или всё равно рассматриваем строки как массивы типа 't'?
02.10.2018: а теперь краткое описание РЕАЛЬНО выбранных решений.
Важное отличие от EPICS и записанного в тогдашнем проекте: в конфиге и при создании каналов указывается МАКСИМАЛЬНАЯ длина, а значение в векторном канале может содержаться длиной от 0 до максимальной.
Зато на стороне клиента (в cda) для TEXT-данных всё же резервируется одна дополнительная ячейка в буфере "текущее значение", и туда кладётся NUL -- чтоб удобнее було работать из C/C++.
cxdtype_t
, который является 8-битовым
целым, и там размер указывается в младших 3 битах (записывается показатель
степени двойки: 0:1байт, 1:2байта, 2:4байта, 3:8байт, 7:error), тип -- в
следующих 4 (0:unknown, 1:int, 2:float, 3:text), а старший бит отдан под
unsignedness.
CXDTYPE_nnn
.
И реально конверсия выполняется аж в 4 точках:
Конверсия:
Полноценной атомарности пока нету, и КАК её сделать -- самая великая на текущий момент задача.
24.08.2008: исходная посылка: в преобразовании должны иметься коэффициент и сдвиг нуля. Формул, в принципе, несколько штук/вариантов, математически эквивалентных.
Сейчас -- в CXv2::cda -- используется преобразование v=c/r-d, где v -- осмысленное (операторское) значение, c -- инженерное значение, считанное из устройства, r -- коэффициент, d -- сдвиг нуля.
Но с такой схемой вечные проблемы:
DecodeData()
.
Плюс, даваемые юзерами коэффициенты еще и интерферируют с коэффициентом 1000000 (микровольты->вольты) -- надо вспоминать/разбираться, этот 1000000 умножать на тот коэфф, или делить. Но это-то следствие того, что преобразование device-int->phys-double совмещено с преобразованием инженерное->операторское.
Так что, в принципе можно рассмотреть еще несколько вариантов, тех самых математически эквивалентных:
Короче -- реально вопросов 2:
08.02.2012@Снежинск-каземат-11: по факту пришли к старому v=c/r-d.
08.02.2012@Снежинск-каземат-11: сейчас в v2 в fastadc --
fastadc_data.h::FastadcDataPvl2Dsp()
-- коэффициент
(coeff) используется как МНОЖИТЕЛЬ: dsp=pvl*coeff+zero.
Это противоречит остальным местам (в т.ч. cda), где всё с точностью до наоборот: physval=physcode/phys_r+phys_d.
02.10.2018: в порядке информации: а вот в EPICS сделано чуть иначе:
(EOFF и ESLO могут указываться в свойствах рЕкорда.)
Возможно, тамошнее "более естественное" -- указывается просто точка, в которой ноль (а то у нас в термостабилизации D=-40, т.к. нулевому измерению соответствует 40 градусов).
Хбз, какой из вариантов "правильнее", хотя математически они эквивалентны, а разница сводится к разнице между тангенсом и котангенсом.
Резюме: да, возможно (особенно с учётом замечаний 2,3 от 24-08-2008), выбрана была не самая удобная для взаимодействия с юзерами модель. Причина сего проста -- объективных резонов для выбора какого-либо из вариантов в тот момент не было, увы. На функциональность выбор не влияет, а лишь на удобство, так что теперь уж с этим жить :).
23.10.2012@автобус-из-каземата: бывает желание с некоторыми каналами обходиться в терминах не 0/1, а сразу "On"/"Off", "Yes"/"No", etc. Т.е., мочь иногда выполнять простые преобразования между числом и строкой; в основном -- в пределах простых lookup'ов.
Мысли о том, как бы это можно было сделать.
06.03.2020: всплыл один нюанс (при обсуждении возможных способов всё-таки реализовать эту конверсию между int-enum'ами и строками, записанном вчера, 05-03-2020 в раздельчике о конверсии между целочисленными и символьными):
А всегда ли эта требуемая для определения необходимости конкретно такого преобразования информация будет доступна?
Впрочем, тут опять главная проблема та же -- то, что у нас на уровне cxsd и cda нету никаких enum/lookup. Т.е., разговор аналогично беспредметен.
06.03.2020@дома: есть
"простое" решение -- ввести отдельный
CXDTYPE_REPR_ENUM
, который везде рассматривать как
числовой и считать совместимым с INT/FLOAT, и только при необходимости
конверсии в/из TEXT работать с ним иначе.
Правда, тут шибко много минусов:
Ну ладно, можно ограничиться одним 32-битным -- для которых заюзать символ 'e'.
А в EPICS, кстати, это 16 бит -- в db_access.h есть
строка "typedef epicsUInt16 dbr_enum_t;
".
А у нас архитектура другая: разделяются ОПРЕДЕЛЕНИЯ "типов" и ИНСТАНЦИИРОВАНИЕ, где все свойства берутся из типов.
Пока единственное, что приходит в голову -- отдельно определять "типы" ENUM'ов, вроде
а потом в enum-каналах ссылаться на эти "типы". Хотя как ссылаться -- тоже вопрос.enumdef ENUM_NAME { STRING_A VALUE_A STRING_B VALUE_B . . . STRING_N VALUE_N }
Но главный вопрос -- а клиенту-то с ними что делать?
А плюсов по-прежнему почти нет -- по причине малонадобности ENUM'ов в CX'е вообще.
Так что всё это -- просто умствования, упражнения из разряда "а вот если бы понадобилось, то как бы это можно было сделать?".
03.07.2014: сейчас узким (толком не только не сделанным, но даже и не продуманным) местом является конверсия данных -- то, чего в v2 не было, и что есть одна из основных фишек v4. Так вот -- оно начинает быть камнем преткновения на ВСЕХ уровнях -- и в cda, и в cxlib, и в сервере. Надо уделить внимание и продумать.
06.07.2014@пляж: кстати, сюда же относятся и тонкости с типами -- int32/double (каналы -- хоть CXDTYPE_REPR_INT, хоть CXDTYPE_REPR_FLOAT, причём любого размера; а ручки -- всегда double).
Плюс еще поддержка raw values (и в формулах не забыть).
08.07.2014: и о timestamp'ах там же рядышком надо заботиться -- и с каналов брать, и с формул чтоб отдавались правильно-"самые старые".
06.03.2020: так давным-давно же сделано -- в основном ещё в 2015-м. Так что помечаем как "done".
02.05.2015: пара замечаний:
Так что, записать -- записали, а реализовывать спешить незачем.
05.03.2020: а вот сделано! Спокойно за полдня после обеда, и в максимальном объёме -- между любыми целочисленными и символьными типами одинакового размера.
28.07.2008: исходные посылки:
Главный "прикол" тут -- что данные будут приходить не заранее определенным пакетом, а индивидуально, так что надо будет разбираться, что именно пришло (как -- давно решено: по номеру-позиции в заказе), и "дергать нужный callback". Но это касается, видимо, подписки -- поскольку для просто запроса давно было решено в качестве "способа" для всего пакета выбирать из всех fork'ов тот, что самый медленный.
Кстати, связанная тема -- это обработка в cda/Cdr с учетом
"выборочного обновления ручек по приходу данных"
(conns_u
).
30.04.2014: общий принцип API каналов: наверное, API разных уровней должны быть максимально схожими -- что серверный, что cda<->dat_p, тогда работой cda/cxlib/cx_proto будет просто туннелирование.
У нас же исторически (еще с транспьютеров) привязка к "циклам" -- вот и маемся.
(Осознано по результатам обдумывания "как бы должен выглядеть API cda-плагинов доступа к данным (dat_p)".)
Но это небесспорно, конечно:
20.08.2008: по результатам обсуждения с Карнаевым СУ для карботрона HITS: там могут быть некоторые каналы, которые можно записывать не в произвольный момент времени, а лишь по некоему внешнему импульсу.
Исходные данные: в некоем канале пролетает пучок, пролетает он быстро, раз в 100ms, занимая мгновение этого периода (а остальное время 100ms-мгновение вроде как можно писАть), но надо быть точно уверенным, что ЭТОТ пучок пролетел при ЭТИХ токах в магнитах. Поэтому -- надо обязательно производить запись в момент, когда пучка нету.
Т.е.: при получении от клиента команды записи надо не
производить запись, а складывать значение в некий буфер (это может быть
и c_next_wr_val
), выставлять флажок, что таковое значение
имеется, а потом по получении внешнего запуска (отдельный вопрос -- как
и откуда) производить собственно запись в аппаратуру.
Возможное решение на сейчас: если сие будет делаться на козачиных CANDAC16 или подобных, имеющих режим ЦАПИ, то можно работать через одношаговые таблицы. Т.е., при получении запроса на запись драйвер будет отправлять не команду уставки непосредственно в ЦАП, а команду записи в таблицу -- так, чтобы модифицировать конкретно нужный канал (благо, команда 0xF2 как раз пишет ровно 4 байта). При получении же команды внешнего запуска "можно!" драйвер будет отправлять команду F7 -- "исполняй!". Таблиц можно иметь ДВЕ -- "текущая", используемая в данном интервале-100ms, и "следующая", программирование которой выполняется.
Главная проблема в том, что у Козака программируются не значения, а инкременты, что делает все эти махинации с таблицами очень муторными. Т.е., надо в начале иметь заполненную-нулями таблицу, в которую при надобности производить запись. В идеале -- одновременно с каждой командой F7 сразу же отправлять кучу команд, приводящих к созданию "нулевой" таблицы. Радует, что изменения-то скорее всего будут происходить редко, так что драйвер таблицу заготовит, и она будет ждать момента, когда понадобится реально поменять некое значение. Так что траффик получится небольшой.
Достоинство же заключается в том, что в таком случае не придется городить какой-то хитрой инфраструктуры в самом сервере, а можно будет обойтись созданием просто хитрого специализированного драйвера.
Ой-е...
24.08.2008: обсудил эту идею с Козаком (это в воскресенье, блин!). Резюме: да, это будет работать.
А второй его совет -- надо все-таки хорошенько трясти клиентов, чтобы представляли обоснования таких требований. А пока не обоснуют (и это должен быть НЕ ТОЛЬКО Карнаев, но и иные посвященные/компетентные) -- посылать подальше.
22.08.2008: это в продолжение обсуждения с Карнаевым СУ для карботрона HITS.
Тут на запросы можно:
Отдельный вопрос -- а ДЛЯ ЧЕГО и ГДЕ такое таймированное чтение обычных АЦП может понадобиться.
30.08.2008: кстати, а ведь Ц0609 и Ц0612 (АЦП-20-256 и АЦП-20-256) -- они-то УМЕЮТ стартовать по внешнему запуску! И что теперь -- убеждать Козака реализовать такую фичу в будущих блоках?
И, кстати, коли так: а мож убедить Батракова сделать поддержку подобного режима работы в "ADC200-cPCI" -- т.е., как бы "режим чтения во-время-измерения"? Тогда -- учитывая возможность проводить измерения по внешним импульсам-стробам -- можно б было использовать этот блок как обычный АЦП с внешним запуском.
04.09.2008: поговорил с Батраковым -- хренушки, будущий "ADC200-cPCI" позволяет МИНИМАЛЬНУЮ частоту внешних стробов 50MHz. Так что -- оппаньки.
С другой стороны, для Снежинска, похоже, подобные измерения-по-внешнему-запуску будут требоваться, так что в будущем, вероятно, какой-то блок будет сделан.
23.08.2008: раз уж это вещь такая для карботрона (и Снежинска!) глобальная, то:
Не сделать ли эти запуски "событиями", и не под-вернуть ли двухшаговую структуру цикла, просто дать возможность начало (а не конец? :-() цикла определять не таймером, а внешним сигналом. И, тогда, начало второй стадии либо уже через N микросекунд, либо тоже внешним импульсом. И, поскольку даже в тактируемых-внешне серверах могут быть обычные постоянно-измеряемые каналы, то надо под-обобщить эту архитектуру, чтобы, в частности, отсутствие внешних запусков не мешало таким каналам продолжать измеряться. Ну и что, мы приходим к множественным параллельным циклам внутри сервера, аналогично epics'ным scan periods(?)?
Короче:
08.05.2009: да, по разговорам про СУ уже ТНК (Зеленоград) стала ясна необходимость подобного механизма. Точнее -- более общего его варианта:
GetPhaseByName()
" и
"GetChancolByName()
".
SetSrvPhase()
" -- может вызываться хоть драйвером
по сигналу от блока, хоть модулем по сигналу от другой программы,
etc... Это даёт максимальную гибкость.
SetChancolStatus(chancol, status)
" -- уставляет
статус плюс инициирует отработку отложенных запросов.
"()
" "()
" "()
"
SetChancolStatus()
", так и задав
серверу матрицу соответствия, каждой фазе по строке, в ячейках которой
указываются статусы-разрешения цветов для этой фазы.
14.05.2009: ну и какой-то API для циклов также нужен -- поскольку требуется понятие "номер цикла работы установки".
14.10.2013: добавлено "свойство"
cxsd_hw_chan_t.chancol
. А прочие поля, для
функционирования (типа заблокированности) -- пока еще нет.
gettimeofday()
,
насколько она сама ресурсоемка или тривиальна?
17.04.2019: случайно наткнулся на пост, могущий давать ответ на поставленный выше вопрос: "time() and gettimeofday() return different seconds" -- там наиболее популярный ответ (за May 11 '14 at 20:44) говорит, что
Both calls are implemented as kernel syscalls. Both functions end up reading a struct timekeeper, both refer to the very same instance. But they differ in what they do with it...
Это несколько расходится с тем, что мне (вроде бы) встречалось, что такие вещи реализуются через VDSO (vsyscalls) -- это штука, работающая БЕЗ переключения контекста, а просто как-то читающая доступную для userspace часть памяти ядра.
Впрочем, там же ниже в другом ответе (за Apr 7 '14 at 16:27) сказано:
Both time and gettimeofday are implemented as so called Linux vsyscalls. Means that your code will be redirected to kernel owned, but userspace mapped pages containing the results which are only periodically updated.
Кстати, по ходу дела натолкнулся на ещё любопытные вещи:
18.06.2013:
Это концепция зеркальная к структурированным запросам ("прочитать такой-то базовый канал при вот этих значениях настроечных") -- чтоб fastadc-клиент получил осциллограмму вместе с её параметрами.
Реализация: счётчик "еще неполученного" и индивидуальные булевские флаги "не получено" на каждый канал-участник группы. При переходе каждого флага из 1 в 0 делается декремент счётчика, а при переходе его в 0 считается всё готовым и запрос отправляется заказчику.
Замечание:
Вообще, скорее это просто блажь (в ИПП проблема вызвана неадекватным использованием скаляров), так что ставим "withdrawn" (хотя при желании реализовать было б и несложно).
01.10.2014@обед-Амиго: а вообще можно и ввести этот "третий тип -- a"; в EPICS'е ж есть, кажется, "process-passive".
Собственно, неудобства только в том, что в "ConsiderRequest()" добавится лишнее условие (с результатом "всегда считать за IGNORE"). Если найдутся еще какие-нибудь достоинства, оправдывающие локальное усложнение -- то сделаем.
01.03.2015: такие каналы типа "a" (автовозвращаемые) полезны не только для козачиных АЦП, но и для случаев, когда канал -- некое свойство, отдаваемое драйвером ЕДИНОВРЕМЕННО, а потом не меняющееся (вроде серийника, максимальных диапазонов АЦП и т.п.), чтоб не генерить почём зря траффик и callback'и.
Неудобство в том, что такие каналы надо будет указывать в конфиге, а они могут быть где-то посреди массива прочих read-каналов.
02.03.2015: можно сделать проще и элегантнее: считать "автовозвращаемыми" каналы, у которых стоит timestamp "trusted" (sec=1).
Имеем сплошные плюсы:
ConsiderRequest()
, где для этого достаточно добавить
элементарное условие ||chn_p->timestamp.sec==0
.
Минус же в том, что так мы решаем проблему только ЕДИНОВРЕМЕННЫХ каналов. АЦПшные же должны честно возвращать настоящий timestamp (а не {1,0}), и к ним запросы будут продолжать идти; с другой стороны, не бог весть проблема -- запросов будет не более 1 штуки, и обновления там всё равно постоянные.
Короче -- сделано, пока не проверено (испытаем на adc200me), будем посмотреть.
02.03.2015: P.S. Давно пора стандартизовать значения секунд 0 (TRUSTED) и 1 (NEVERREAD).
Стандартизовано -- CX_TIME_SEC_TRUSTED
и
CX_TIME_SEC_NEVER_READ=1
.
Вот только незадача: в ReturnDataSet()
стоит проверка,
что НЕ использовать timestamp не только при ==NULL, но и при sec==0.
Так что "trusted" вроде как никогда до потребителя не дойдёт.
Или счесть ту проверку уже устаревшей? Ведь она появилась при первоначальном внедрении концепции timestamp'ов 03-03-2014, а идея "0 означает trusted" появилась позже, 25-11-2014/02-12-2014.
Да, так и сделано.
Только теперь еще надо как-то разобраться с API формул в cda -- там
"sec==0" считалось признаком неуказанности. LATINITSA17.11.2015: da, v proc_GETCHAN()
tozhe stoit
proverka "fla->timestamp.sec==0
" s kommentariem "Missing is
always replaced". P.S. I voobsche, est' nekaya problema s vyborom znachenij
TRUSTED=0 i NEVER_READ=1: otsutstvuet "uporyadochennost'", chtoby mozhno
bylo ne glyadya na sec==0 ili sec==1 schitat' odno "staree" drogogo; ved'
NEVER_READ yavno "staree" TRUSTED, no iz znachenij sleduet rovno obratnoe.
(Pri sozdanii arith_drv.c.)
03.03.2015: что за извращение -- завязывать СВОЙСТВА канала на его ТЕКУЩИЙ timestamp!
Если потребно драйверу указывать свойства -- то надо и делать специальный вызов для этого, говорящий, что такие-то каналы являются автообновляемыми.
Так и сделано:
SetChanReturnType()
, параметры first,count -- в
стиле SetChanFreshAge()
, и внутренности практически
оттуда.
cxsd_hw_chan_t.is_autoupdated
. Оно int8
, и
для padding'а добавлена еще троица res1,res2,res3 (так даже лучше -- в
результате в начале идут не 4, а 8 байт, что приспособленнее для
64-битных архитектур).
Использование фичи вставлено в cac208_drv и adc200_drv, и проверено на последнем -- работает как надо, спрашивается однократно. Засим вопрос "автообновляемых каналов" можно считать закрытым.
23.04.2015@13-6-около-пультовой, возясь с Чернякиным с глючащим ИСТом Solenoid2: у фичи "TRUSTED" есть один недостаток -- становится неизвестным РЕАЛЬНОЕ время обновления канала. 04.01.2017: уже нет -- и так-то давно никем не использовалось, а сейчас "TRUSTED" уволено вообще, посему и недостаток исчез.
ReturnDataSet()
принудительно складировать реальный
timestamp (он там всё равно есть).
01.09.2015: надо для autoupdated-каналов ставить fresh_age={0,0}. Иначе они в клиентах синеют. (И не забывать событие послать.)
...часом позже -- сделано; отправка события скопирована из
SetChanFreshAge()
. Вроде помогло.
Тонкости:
Идея в том, что если вдруг какой-то драйвер решает сделать некий(ие)
канал(ы) НЕ-autoupdated, то пусть потом после этого вызова отдельно
делает SetChanFreshAge()
, и уж по нему клиенты будут
уведомлены о новом возрасте свежести.
16.09.2015: что-то предыдущая запись совсем странная: неясно, в КАКИХ клиентах autoupdated-каналы синели и почему?
Сегодня выяснилось как раз другое -- то ={0,0} наоборот мешало механизму легитимного посиневания при долгом необновлении (сделанному вчера).
Пока что нуление отключено, и всё прекрасно!
Чуть позже:
Он "автоматический", и отдаётся только по вычитыванию ЦАПа, коее происходит лишь при старте и при изменении значения. А fresh_age={0,0} ему драйвер НЕ делает.
Вот от него и Iset_cur синеет.
А dcct2 синел лишь на экране, поскольку в расцвечивающую формулу входит Iset_cur.
Вроде б надо драйверам принудительно для подобных каналов ставить
{0,0} САМИМ. Но есть идея: превратить параметр
SetChanReturnType()
из булевского в enum, и при значении 1
НЕ нулить, а при 2 -- нулить.
IS_AUTOUPDATED_{NOT,YES,TRUSTED}
.
"Почти" -- потому, что у ist_cdac20 всё-таки осталась проблема: он в части случаев изначально никогда-не-читан (NEVER_READ). Причина -- его исходный канал (в cdac20) IS_AUTOUPDATED_TRUSTED, но !rw, так что никакого начального "обновления" не происходит. Вопрос, скорее, почему не у всех vdev-устройств/экземпляров так...
Хотя вот есть непонятки:
Т.к. от удалённого всё придёт только уже внутри sl_main_loop()'а, после инициализации.
16.09.2015@вечер-~21:00-путь-домой: по-хорошему, надо бы факт "autoupdated-trusted" сохранять в свойствах канала -- аналогично самой autoupdated'ности. И во frontend'ах считать начальное вычитывание значений за "UPDATE" именно по этому флагу, а не по is_internal (но и internal-каналам его уставлять, конечно).
23.03.2016: по факту за флаг "autoupdated-trusted" работает условие
is_autoupdated && fresh_age.sec==0 && fresh_age.nsec==0
(т.к. fresh_age={0,0} влечётся), с дополнительным условием
timestamp.sec!=NEVER_READ.
17.09.2015@утро, по пути на работу, около ИХБФМ: а каналы с NEVER_READ надо ВСЕГДА посиневать, даже не глядя на их fresh_age.
Пришел на работу, проверил -- да, это делается, отдельной веткой (просто если fresh_age_specified, то timestamp==NEVER_READ посинеет и так, т.к. он заведомо старше любого разумного возраста).
20.09.2015: еще в ту же степь: после броска питания в ~11:28-11:29 (коснулся ТОЛЬКО CAN-блоков, но НЕ контроллера CANGW!) у части v3h'ей поле MaxSpd осталось OFFLINE и не обновилось (и в нём почему-то 0) 21.09.2015: похоже, 0 и должен быть.
04.01.2016: увольняем CX_TIME_SEC_TRUSTED
-- давно назрело (её вытеснила IS_AUTOUPDATED_TRUSTED
), а с
недавним косяком в отношении vdev'а ненужность и даже мешающесть этой фичи
стала очевидной.
ReturnInt32DatumTrusted()
был
cankoz_lyr_common.c, при отдаче CXRF_UNSUPPORTED на несуществующие
регистровые каналы. Переделано на обычный возврат.
ReturnInt32DatumTrusted()
в
cxsd_hw.c и remcxsd_driver_v4.c убраны.
CXSD_HW_TIMESTAMP_OF_CHAN()
не использовался еще с
декабря, а сейчас и из cxsd_hwP.h удалён.
#if
'ленное тоже.
cda_dat_p_update_dataset()
проверка "TRUSTED" убрана:
CX_TIME_SEC_TRUSTED
переименовано в
__UNUSED__CX_TIME_SEC_TRUSTED
.
24.09.2013: давно ясно, что надо отходить от "возрастов" в сторону "timestamp'ов". Сегодня опять пообщался с Пашей Чеблаковым на тему "как в EPICS вообще и у них на NSLS-II в частности работают с timestamp'ами", и вот результаты общения и мои мысли на тему:
gettimeofday()
(только еще с таймзонами б разобраться...)
Итого -- получится {int32,int64}, а с учётом rflags -- {int32,int32,int64} служебных данных на каждое значение канала.
Что выбрать -- неясно. С одной стороны, первый подход "правильнее": это вопрос скорее конфигурации, а не дело драйверов (задача тех -- работать с железом). С другой, второй подход даст бОльшую гибкость в реализации.
14.10.2013: в cxsd_hw_chan_t
добавлены поля timestamp_sec
и
timestamp_nsec
.
01.03.2014@утро-душ: (реально даже несколькими днями раньше, на лыжне 5км) "как брать timestamp'ы от других каналов": у каждого канала кроме собственно ячейки "timestamp" иметь параметр "индекс timestamp'а", по умолчанию равный номеру самого канала, и при возврате клиентам всегда, без лишних if()'ов отдавать не chn_p->timestamp, а channels[chn_p->timestamp_cn].timestamp.
02.03.2014@утро-душ:
еще вариант: производить "подмену" не в момент отдачи клиенту, а в
момент ReturnChan*()
: в этот момент записывать в
chn_p->timestamp не gettimeofday(), а
cxsd_hw_channels[chn_p->timestamp_cn].timestamp. Отличие этого подхода
-- значение фиксируется в момент ReturnChan*() и не поменяется, если
вдруг до отдачи клиенту успеет сделаться ReturnChan*() каналу
chn_p->timestamp_cn.
Пожалуй, этот вариант и выберем.
03.03.2014: поля timestamp_sec
и
timestamp_nsec
вынесены в отдельный typedef
cxsd_hw_time_t
, для возможности осуществления
вышезадуманного (т.е., чтоб копировать было удобнее).
И cxsd_hw_chan_t.timestamp_cn
введено.
И описанные за вчера махинации с подменой timestamp'а сделаны в
ReturnDataSet()
.
Вечер: и просто основное функционирование
ReturnDataSet()
сделано -- она складирует вёрнутые данные,
надлежаще проверяя nelems; но пока безо всякой конверсии (при
несовпадении dataunits просто заполняет нулями).
29.04.2014: поскольку информация о timestamp'ах
нужна в разных частях (еще как минимум cda и cxlib), то тип
cxsd_hw_time_t
перенесён в cx.h под
именем cx_time_t
. 08.07.2014: для
унификации со struct timeval
поля переведены с int на
long
-- так автоматом решится "проблема 2038".
29.05.2014@Снежинск-каземат-11: штука, аналогичная "взятию timestamp'ов от других каналов": иногда может потребоваться также указывать на необходимость брать от других каналов {r,d}. Например, калибровки будут совпадать у скалярного канала ЦАПа и у связанного с ним векторного.
Но тут реализация будет сложнее, поскольку надо СРАЗУ после указания драйвером "мои {r,d} поменялись!" отсылать информацию об этом не только этому каналу, но и всем ссылающимся.
19.09.2014: идея на эту тему:
А если ввести концепцию "калибровки"? Такую виртуальную сущность, обладающую свойствами {r,d}, каналам указывать ссылку на неё, так что они б "навешивали callback на изменения", и при её изменении уведомлялись бы все затронутые (т.е., использующие её).
Единственный вопрос -- а кто и как будет указывать новые значения самой калибровке?
Или как раз и считать этой сущностью "базовый" канал (т.е., для векторного -- скалярный)? И да, навешивать на него callback на изменения (тут точно будет оптимальнее иметь ДВА независимых callback-list'а, на обновление значений и на изменение калибровок).
15.08.2014: может понадобиться возвращать timestamp прямо из драйвера, а не от gettimeofday() -- для случаев, когда данные приходят из другого места (remdrv, cda (от другого сервера), да вообще от другой СУ). 22.08.2014: сделано, см. ниже за сегодня.
15.08.2014@пляж: зачем cx_time_t
? Чем
struct timeval
не устраивает?
(Использование -- отдельно, передача байтов по сети -- отдельно.)
19.08.2014: а вот почему timeval не устраивает:
22.08.2014: к ReturnDataSet()
добавлен еще параметр timestamps
; если
timestamp_cn
-- "брать от" не указана),
Т.о., обычные драйверы спокойно указывают timestamps=NULL, и всё пашет как надо, а уж всякие remdrv могут отдавать своё значение.
Поэтому надо бы вычитывать осциллограммы не сразу после интегралов, а чуть позже, когда и все остальные блоки тоже ответят.
Вопрос -- КАК?
18.11.2013@Беркаев/1-214: соображения возникли при обсуждении функционирования БИИПов на канале (К500) вечером у Беркаева. Он в своих "интеллектуальных серверах" это просто вручную явным образом отсрачивает.
Ведь запросы на осциллограммы будут приходить ЗАРАНЕЕ, еще ДО "строба запуска", и к моменту реального запуска уже все будут запрошены.
Но тут есть узкое место: в самый начальный момент после включения, когда блок еще НИЧЕГО не измерил и показывать нечего. Возвращать пустоту?
Т.е., по факту -- реализация тех же фаз, но в каждом экземпляре устройства отдельно.
17.11.2013: реализовать такое было б несложно:
int32
.
Есть пара принципиальных вопросов:
В частности, не сделать ли маркером по умолчанию (==0) как раз фазу?
17.10.2016: конкретно сейчас потребность возникла на ВЭПП-4, с БИИПами (vsdc2) на ГИД25.
Т.е. -- вставить в vsdc2_drv.c указание 10 секунд?
18.10.2016@утро-еще-в-постели: а если дать возможность указывать значение default'ного возраста (вместо фиксированных 5с) в конфиге -- например, в спецификации ИНФО_ПО_КАНАЛАМ, для каждой группы индивидуально.
...кстати, ИНФО_ПО_КАНАЛАМ парсится в тип CxsdChanInfoRec
,
используемый также для указания групп каналов в
CxsdDriverModRec
(точнее, МОГУЩИЙ быть использован -- сейчас
нигде). Соответственно, драйверы могли бы указывать свои умолчания. Хотя,
конечно, только в серверных, а не в удалённых (но и remcxsd_driver'у никто
не мешает присылать эту информацию в случае наличия, еще ДО инициализации
драйвера).
19.10.2016: в конфиге-то можно -- например, через двоеточие: r5i100:3.5 (три с половиной секунды).
Но в идеале нужна возможность указывать возраста не группам каналов, а ИНДИВИДУАЛЬНЫМ каналам -- т.к. даже у одного VsDC2 первый и второй каналы могут работать на разные подсистемы и иметь разные "разумные" возрасты свежести.
...не говоря уж о том, что ИНФО_ПО_КАНАЛАМ в основном у всех устройств стоит "~", т.е., используется то, что указано в devtype -- а та инфа столь же фиксированна, как и возвращаемая драйвером.
Симптомы -- что cdaclient, будучи натравлен на readonly-канал, не являющийся IS_AUTOUPDATED, начинает получать измерения оттуда с бешеной частотой.
А разбирательство (см. в разделе по cxsd_fe_cx за вчера и сегодня) показало, что корень проблемы кроется в так до конца и не продуманной "временнОй схеме работы", посему детально поразмыслить нужно уже в этом разделе.
13.10.2018@утро-дома: (и потом уже на работе тоже): итак, в чём проблема: отсутствует чётко прописанная "конституция" на тему того, кто-когда-как инициирует чтение (кто является "актором"). Для rw и ro-autoupdated-каналов -- такой инициатор есть, а вот для просто readonly -- нету.
Если попробовать классифицировать нынешние виды каналов в CXv4, то получается следующий список:
Причём реально есть ДВА варианта таких каналов:
...а также прочие каналы, которые хочется мочь читать как можно чаще, без ориентировки на циклы (хотя это уже сомнительно).
И получается, что для этих 2 вариантов нужно работать совершенно по-разному: для (2) прочитанность в текущем цикле нужно игнорировать, а для (1) -- нет, даже при режиме ON_UPDATE.
Но модификации при введении флага
CXSD_HW_DRVA_IGNORE_UPD_CYCLE_FLAG
была сделана исключительно
под нужды (2), так что каналы варианта (1) стали работать неадекватно.
Кстати, "о конкурентах":
When a device controlled by NI-DAQmx does something, it performs an action. Two very common actions are producing a sample and starting a waveform acquisition. Every NI-DAQmx action needs a stimulus or cause. When the stimulus occurs, the action is performed. Causes for actions are called triggers. Triggers are named after the actions they cause, such as a start trigger to start an acquisition.
Что делать?
...да и вообще схема работы с каналами не была продумана, чего уж там греха таить -- нынешняя складывалась эволюционным путём, по мере реакции на возникающие потребности.
Я -- не встречал; то ли это означает, что никто не знает или никто не озабачивался классификацией, то ли лишь то, что я малограмотно пытаюсь изобрести велосипед.
14.10.2018: к вопросу о том, "что же делать":
upd_cycle
). Например, у pzframe-драйверов это все data-каналы.
...но такая схема не кажется правильной: реально лишь КЛИЕНТ может определить, какой из вариантов (игнорировать/уважать) надо использовать. Например, каналы АЦП -- вроде Ц0601/Ц0612: иногда будет требоваться читать их раз-в-сколько-то, а иногда -- как можно чаще.
Поэтому напрашивается другой вариант: ...
CDA_DATAREF_OPT_ON_UPDATE
и
CX_MON_COND_ON_CYCLE
на 2:
upd_cycle
.
Название ON_IMMED, суффикс "@i"?
Как бы то ни было, надо не кидаться "в бой с шашкой наголо", а поразмыслить, чтобы понимание созрело (да-да, "ждание преполняет", блин...).
И, кстати, забавный факт, который надо принять во внимание:
(Что, кстати, было бы весьма чревато для локальных каналов дало бы
-- бесконечную вложенность, от которой в
cxsd_fe_cx.c::MonEvproc()
сделана защита в виде
being_reqd
.)
Так что лозунг "чтоб протоколы CX:: и INSRV:: работали одинаково" пока исполняется несколько сомнительно/посредственно.
14.10.2018@вечер-18:00-пристройка-лестница-вниз: у нас, возможно (возможно!) смешаны 2 разных сущности:
Понятно, что эти сущности взаимозависимы: для "раз в цикл" железная взаимосвязь; но вот для "по обновлению" возможны варианты.
Наверное, имеет смысл расписать набор комбинаций, получающихся при разделении этих сущностей по 2 разным параметрам.
15.10.2018@@утро-дома-лестница-вниз-по-пути-на-работу: вот тут видна разница в подходах в EPICS и CX: в первом каждый канал-рЕкорд заводится отдельно и ему "присваивается конкретный цикл поллинга", а в CX при заведении устройства каналы создаются все оптом, и часть из них может не опрашиваться вовсе, а читаются лишь те, на которые приходят запросы от клиентов -- и это явно подчёркивается в отличиях CX.
24.10.2018@вечер: де-факто свойство "autoupdated" -- аналог EPICS'ного SCAN. Только указывается драйвером, а не в конфиге.
25.10.2018@утро-дорога-на-работу-между-ИПА-и-ИЦИГ: ведь "ignore_upd_cycle" должно определяться не запросом, а каналом: для pzframe'овых -- да, для обычных -- нет.
ConsiderRequest()
, в том же if()
'е, где и нынешнее
ignore_upd_cycle
проверяется.
25.10.2018@обед-по-пути-в-Гуси-перед-входом-в-здание: уметь бы
всё-таки свойства типа "autoupdated" указывать в конфиге (видимо, в
channels
/devtype
). Чтоб:
Это важно для драйвера mqtt_mapping, который может работать для кучи устройств, и ему просто негде взять информацию об autoupdated'нутости.
После обеда: хотя какие именно "свойства" есть СЕЙЧАС, которые бы роляли ДО загрузки драйвера? Не autoupdated -- тот только после имеет смысл. Но в будущем что-то может и проявиться.
Синтаксис, правда, не вполне ясен: опять в namespace указывать? Не очень это хорошо...
26.11.2018: реализовано решение в виде «"ignore_upd_cycle" должно определяться не запросом, а каналом». Теперь обновлённость-в-текущем-цикле игнорируется, только если это указано "И в свойстве канала, И в запросе".
Свойство канала указывается драйвером, с помощью свежевведённого режима
DO_IGNORE_UPD_CYCLE
.
Подробности в разделе по cxsd_hw за сегодня.
14.10.2018: с момента написания предыдущей фразы прошло 10 лет.
Момент "2038" приблизился на 10 лет -- осталось 20 лет вместо былых 30...
14.10.2018@вечер-дома-ванна: однако надо отметить, что:
timestamp_sec_hi32
) на стороне сервера
всегда форсится =0, а на стороне клиента пока игнорируется.
Так что в ПРОТОКОЛЫ корректная поддержка уже заложена, просто пока недозаиспользована.
22.10.2008: надо на каждый канал в запросе (а не на весь запрос!) присылать некий "статус" исполнения -- чтобы, например, на попытки писАть в каналы чтения отдавалось бы "недопустимая операция". Т.е., на флаги канала ("is readonly" флагом состояния не является), а именно статус исполнения запроса. Таким образом, пока что наклевываются следующие использования:
Как назвать эту фичу? Видимо, rqstat
(request status).
Это именно статус, а не флаги.
Как реализовывать? Некоторые соображения:
В момент получения/обработки запроса это поле инициализируется в 0, и потом, по ходу разбирательства, туда могут добавляться битики RQST_RDONLY, RQST_WRITE_LCKD, RQST_MANY_WRITERS. И, при отправке ответа, этот статус уйдет клиенту.
Битов в старшей половине rflags пока навалом -- свободны с 21 по 27.
18.02.2009: в CXv2 стала очевидна потребность мочь зарегистрировать для "NOT_IN_DRIVER" не только дескриптор, но и таймаут тоже. Конкретно даже не для "NOT_IN_DRIVER", а для layer'а.
(Понадобилось это для CAN -- чтобы не каждый драйвер регистрировал бы по heartbeat-таймауту, а а это делал бы layer, и вызывал бы процедуры из каждого заинтересованного драйвера. Все-таки изрядная разница -- 10 таймаутов в секунду, или же 100 (при 10 драйверах).)
Так что очевидно -- надо бы иметь ЕДИНЫЙ пул таймаутов и дескрипторов, и каждый элемент тэгировать полем "владелец". А "владельцы" же, в свою очередь -- либо драйверы (devid>0), либо "модули" (devid<-1), частным случаем которых и будут layer'ы.
И при необходимости "грохнуть" модуль/драйвер будет вызываться некая функция, подчищающая все ресурсы, принадлежащие указанному devid.
08.01.2013: в продолжение предыдущей идеи насчёт "единого пула" -- как мог бы выглядеть "идеальный" API драйверов/модулей.
Это весьма слабоудобно.
Тогда при деактивации модуля можно для всех таких API вызвать функцию-подчищальщик, передав ей этот идентификатор.
Встаёт вопрос об отведении этих идентификаторов (сейчас >0 -- driver, <0 -- layer), но тут можно разбить int32 на два поля: старшие N бит -- "класс" (driver, layer, ...), младшие 32-N -- идентификатор внутри "класса".
Такой подход к API был бы всеобъемлющ (кстати, он похож на API ядра *nix -- там ключом работает pid). Но, надеюсь, в ближайшее время к нему прибегать не придётся -- ибо это очередное очень радикальное изменение всего и вся. Может, в CXv5? :-)
Парой часов позже:
13.01.2013: еще:
...или всё ж передавать? А нах?
uniq
.
02.02.2013: по-хорошему, для принятия решения надо бы сначала изучить API "конкурентов" cxscheduler'а -- libevent и libev.
03.03.2013@пультовая-вечер-воскресенья: при таком переходе
на "единый пул" с прямым доступом драйверов/модулей к библиотечным API
мы, помимо прочего, потеряем еще кое-что: "защиту драйверов друг от
друга" (частично). Ведь теперь некому будет делать
ENTER_DRIVER_S()
перед вызовом метода драйвера.
Другое дело -- а сколько раз реально та концепция была полезна?
14.09.2013: защита-то фиг с ней, а важнее оказалось другое: чтоб запрещать регистрировать ресурсы для не-активных devid'ов. Вылезла такая потребность во время разборок с глюками v2'шного canserver/remsrv.
Решение -- в виде hook'а "uniq_checker", который для каждой uniq-aware библиотеки можно зарегистрировать. У всех библиотек вид оного одинаков (так что можно всем регистрировать одного):
Нынешние v2'шные кроме перед просто проверкой также считают за "окей" devid==0 || devid==DEVID_NOT_IN_DRIVER (первое -- для самих сред исполнения, второе -- для layer'ов, которые там перманентны).
Вот теперь концепция не только реализована и используется, но и более-менее доведена, так что "done".
15.09.2013: а вот и нифига -- не "фиг с ней"! :-)
Это ведь не столько "защита драйверов друг от друга", сколько отслеживание текущего контекста исполнения -- как бы имитация многопроцессности. И серверу РЕАЛЬНО нужно знать, если "текущей исполняемой задачей" является драйвер -- для понимания, можно ли его спокойно грохать (см. в bigfile-0001.html насчёт remsrv).
Так что, возможно, придётся вводить ещё пару hook'ов, как раз для отработки ENTER_DRIVER_S()/LEAVE_DRIVER_S() вокруг вызова callback'а.
23.04.2009:
Сие замечание актуально, если местом преобразования между аппаратными и операторскими каналами будет избран сервер, а не cda -- тогда в протоколе (и в cda) надо будет иметь возможность доступаться как к операторским каналам, так и к аппаратным.
Если же конверсия будет на совести cda -- то проблемы нет, просто надо уметь игнорировать присылаемые сервером (r,d), уставляя принудительно (1,0); хотя и тут отдельный вопрос -- КАК? В том самом [ПРЕФИКС]?
25.04.2009: а так: указывать первым параметром в конфиге ИМЯ БЛОКА (как было в .uds) -- именно оно важно, а драйвер -- это лишь часть адресной информации. Т.е., строчка в конфиге будет иметь вид
DEVNAME DRIVER BUSINFO AUXINFO {CONTENT}
Файлы -- видимо, надо иметь таблицу (для редактирования), плюс график, показывающий результат.
А альтернативы АЦП/осциллограммы и ЦАПы/файлы -- видимо, делать tabber'ами.
23.04.2009: идея родилась глядючи на курчатниковскую программу, которая по мере старта/опроса аппаратуры выдавала в окошко табличку со списком устройств, где живые были строчками на зеленом фоне, неживые -- на розовом, а с мертвыми контроллерами -- на сером (или наоборот).
28.04.2009: и еще чуть подумавши -- а ведь варианты 1 и 2 не взаимоисключающи, а ДОПОЛНЯЮЩИ:
23.03.2010: к таким устройствам относятся батраковские ADC200ME/ADC812 и DL200, PISO-Encoder600. Ну и пановский HWADDR -- тоже можно рассматривать как этот serial.
Это позволит ВСЕГДА иметь на экране максимально полную информацию об устройстве, вне зависимости от того, как оно было сконфигурено в БД -- по адресу или по серийнику.
Хотя и указанный в БД адрес также хорошо бы мочь получать.
26.03.2010: следствие: даже adc200 надо будет перевести с uint8-данных на нормальные int16 или int32 -- чтоб оно отдавало сразу готовое в милли- или микровольтах.
29.03.2010: в ту же степь -- надо иметь также канал "множитель времени" -- для перевода номера отсчета в что-то-секунды (что сейчас делает n2xs).
28.03.2010: это мало того, что упростит добычу индивидуальных данных для клиентских программ, но еще и позволит легко делать программу "множественный осциллограф" -- чтоб на одной панели показывались данные с кучи разных осциллографов (такая задача уже сейчас встаёт на ЛИУ).
А для тех случаев, когда эти по-линейные каналы не нужны, можно, например, указывать драйверу в auxinfo ключик "noindlines", тогда он просто не станет делать дополнительные Return...().
15.02.2012@Снежинск-каземат-11: сегодня опять вылезли мысли в ту же сторону (не помня о предыдущей записи от 28-03-2010):
15.02.2012@Снежинск-утро-в-Соболе-на-полигон: (точнее, по дорожке от КПП к Соболю) для всяких обсчётов в прикладных программах нафиг не сдались "полные" данные от соседнего adc200me, с многими каналами.
Неа -- тут надо простым образом получить просто ОДИН вектор, чтоб не делать из него еще какие-то дополнительные выборки нужного канала. И в идеале -- адресовать его по группировке в стиле "ПУТЬ.adc.chan0" (это для knobplugin'ов/формул вычислений).
Но такое реализовывать в Cdr/Chl будет весьма неудобно: если ручки-параметры еще можно ("ПУТЬ.adc.numpts"), то векторы -- много геморроя с обвязкой.
может просто прямо из драйвера отдавать как бы alias'ы/представления -- кроме собственно настоящего большого канала, также N штук (adc200 -- 2, adc812 -- 8) уже вытрясенных векторов на под-каналы.
И для драйвера это будет несложно -- отдавать из того же массива, просто с точки [subchanN*numpts] и количеством numpts (вместо с 0 и количеством numpts*numlines).
Это по результатам вчерашней возни с микропервеансом в liu/y/manyadcs_knobplugin.c -- там для вычислений нужен (помимо одного из собственных векторов) еще вектор от совсем другого adc200me, и его заполучить, причём лучше б по ссылке из конфига, -- та еще оказалась работка.
25.10.2012@Снежинск-снежинка-вечер: (на лестнице, идя вниз на ужин) в ту же степь (опять не помня о прошлых записях ;-)): хорошо бы также из формул/сценариев иметь доступ к таким векторным каналам -- чтоб можно было попросить какой-нибудь обсчёт, типа "из такого-то вектора посчитай среднее в диапазоне [T0...T1]".
Это опять в сторону микропервеанса -- тогда можно б было вместо нынешней алхимии в manyadcs_knobplugin делать всё стандартными средствами.
12.11.2012: а еще и всю sukhphase можно б было на этом сделать -- там в основном довольно тупой и прямолинейный обсчёт.
02.04.2014@Снежинск-утро-полигон-около-раздевалки: с этими индивидуальными каналами вылазит проблемка: как реализовывать АТОМАРНЫЕ получения самого канала и его "настроечных" (тайминги, диапазоны, ...)? Ведь настроечные каналы могут быть настроечными для ОДНОГО базового, не для нескольких.
16.08.2014@пляж: насчёт параметризованных запросов вообще:
(Теоретически можно оставить в чисто информационных целях.)
02-10-2021 в vme_test_common.c таковой был добавлен -- ключик "-k".
Но также понятно, что он будет полезен и в других утилитках; в т.ч. конкретно в *canmon (занадобилось для разбирательства на чёмской сварке -- почему сообщения "No buffer space available" валятся бесконечно, а не подавляются после первого: часть сообщений всё-таки типа уходит?). Но там ключ "-k" уже задействован ("print IDs in kozak notation").
После чего очевидным образом напрашивается "-K".
P.S. А в cdaclient условный аналог уже есть -- "-N", "ignore NOTFOUND events".
25.10.2021: итак:
Но сейчас переделано на заглавное "-K" -- для унификации.
Но сейчас добавлено понимание и "-K" -- тоже для унификации.
ЗАМЕЧАНИЕ: запрет отваливать касается ТОЛЬКО операций В/В, но НЕ влияет на:
select()
.
Вот здесь уже вопрос -- откуда вообще такие ошибки могут возникать и можно ли их как-нибудь игнорировать?
Но в любом случае это уже за рамками изначальной задачи -- мочь игнорировать ошибки ШИНЫ.
19.03.2010: в частности, в первую очередь речь
про .subsys-файлы. Там можно иметь директиву
encoding
.
Кроме того, это затронет файлы режимов -- если оные будут в том же виде, что в CXv2 (из-за комментариев), файлы сохранённых осциллограмм от осциллографоклиентов, плюс файлы логов (если таковые будут).
Также это актуально и для "файлов", получаемых из СУБД -- т.к. они будут ровно так же просасываться через smp4td, а для собственно программ выглядеть ровно как текстовые файлы.
10.01.2011: вот записанные там соображения:
15.12.2010@Снежинск-каземат-11: по опыту реализации в liu/fastadc/liuclient плагина LIUADCS (liuadcs_knobplugin.c): применённый способ коллекционирования liuadcs'ом списка adc200 (hook и взятие всех) -- неправилен. Правильный вариант -- указывать ему в параметрах список имён: либо тупо списком, либо шаблоном типа "*.nnn.adc*", либо "перемножением" вида
"base...adc"x{"a","b","c","d","e","f"}А для этого нужен развитый API поиска ручек по именам/шаблонам.
(Плюс, конечно -- нужен будет способ определить тип "той" ручки, чтоб как-то мочь получить доступ к её приватным данным (тоже бы как-нибудь объектненько это реализовать).)
22.12.2010@Снежинск-каземат-11: по опыту реализации globact_knobplugin.c ясно, что ОБЯЗАТЕЛЬНО нужен развитый механизм поиска ручек:
- Чтобы глобальными могли быть не только однокомпонентные, но и подветки.
- Как по просто именам, так и по шаблонам (т.е. -- итераторы, чтоб можно было использовать имена вида abc.*.def.zzz).
- возможно, прямо сюда же всунуть функционал более хитрых шаблонов:
- {a,b,c,...} (а вложенные -- сможем поддерживать? :-D)
- [диапазон-символов]
- <X-Y>
Отдельный вопрос -- что из этого считать за pattern'ы, а что -- за перечисления (как {} в shell'е)? Видимо -- аналогично shell'у, {} всегда считать за generation, а () -- за pattern. И при надобности генерить ЧИСЛОВЫЕ перечисления -- использовать {X-Y}.
- Плюс, естественно -- там должны бы корректно резолвиться всякие относительности. А как? Надо ввести какой-то способ указывать как ".", так и "..".
24.10.2012@Снежинск-каземат-11: в v2 оно сделано (в виде ":...") еще при реализации тамошнего datatree.
- Для уменьшения влияния "эстетических" аспектов (введение лишних элементов исключительно ради красоты размещения) хорошо бы придумать способ делать имя элемента "прозрачным" -- т.е., чтобы его содержимое считалось будто в его содержателе. Например -- указывая его ident=":".
Это значительно усложнит поиск (возможно -- и загрузку; но НЕ сохранение), к тому же -- такая "прозрачность" ведь должна быть многоуровневой (с несколькими ":" в иерархии подряд).
Зато нужда в глобальных ".именах" отпадёт почти полностью.
24.10.2012@Снежинск-каземат-11: в v2 сегодня сделано. В v4 может подойти почти один-в-один.
- Что еще?
Упомянутая "прозрачность" имён очень сильно меняет всю архитектуру адресации.
16.04.2014: замечание: "прозрачными" надо считать также и имена просто пустые, а не только начинающиеся с ':'. См. bigfile-0001.html за сегодня.
Так вот: способом добиться результата выглядит создание
функции "datatree_foreach_node()
", которой бы передавались:
1) узел-контейнер -- в котором искать; 2) функция-компаратор и
privptr для неё.
20.03.2018@лыжи-конец-2-й-2-ки: некоторые подробности и дополнительные соображения:
Такую "стандартную реализацию" можно запрашивать, указывая
компаратор=NULL (как в findfilein()
указание checker=NULL
означает "используй fopen(), в privptr имя файла").
...при надобности можно таких "стандартных" сделать несколько, используя
константы ((void*)1)
, ((void*)2)
, ...
22.03.2018: вариант с "любое количество узлов" через '*' выглядит пугающе -- хбз, как такое делать (и точно рекуррентно). Так что с этим стоит повременить, по крайней мере, пока не припрёт.
Проблема: а как записывать? Ведь уровней-то туча, поиск может быть рекуррентным, и надо б хранить список всех "текущих проверяемых точек на каждом уровне иерархии"?
22.03.2018: а вот и нет! Ведь каждую эту точку можно "вычислить", идя вверх по дереву. Так что вопрос будет не идеологическим ("ой, рекурсия ж..."), исключительно техническо-изобретательским: как организовать реализацию поиска, чтоб она дозволяла такое продолжение.
30.03.2011@Снежинск-каземат-11: идея появилась глядя на поведение саровской SCADA: а можно ли как-то сделать возможность ПРОГРАММНОГО указания ELEM_TABBER'у, что ему надо выбрать такую-то закладку? Чтобы, например, если есть несколько экранов-табов, то чтоб блок "сценариев/техпроцесса" конкретной программы мог бы активировать в некий момент нужный из этих экранов.
Хотя, конечно -- вопрос, а как к ним адресоваться? Логично -- по номерам, но это неудобно. А если б по ident'ам?
И -- tabber не единственный такой элемент. Вырисовывается такая картинка:
17.11.2013: see also KnobsCore/11-08-2012 (SetVis).
24.09.2011@Снежинск-каземат-11: последние несколько дней опять задумывался об этом вопросе, и вот некоторые соображения:
SetValue
--
которому и будет передаваться желаемое значение (типа номера закладки), в
конечном итоге от set_knob_controlvalue()
.
25.09.2011@Снежинск-каземат-11: вообще, конечно, вылазит просто классическая цепочка наследования: unif,knob,cont,grpg. Только делается всё на НЕобъектном языке.
08.09.2013: мысли такие витали давно (найти б еще, где (если?) записывались). По результатам реализации плагинчика управления сваркой мысли выкристаллизовались поконкретнее.
12.02.2015@вечер-беговая-дорожка: в очередной раз обдумывал этот вопрос; очередная пачка мыслей (в значительной степени пересекающаяся с написанным выше и в "общих вопросах" по Cdr за 24-10-2012, 25-10-2012, 26-02-2013):
Короче -- пока наиболее осмысленным выглядит "гибридный вариант" описанный 26-02-2013.
31.03.2011@Снежинск-каземат-11: Поведение:
Такой мега-клиент во-первых будет очень сильно полезен (для всякой отладки, разборок и т.п.), а во-вторых -- будет отличным ПРОСТЫМ И УДОБНЫМ полигоном для отработки всех технологий и компонентов.
Понадобится собственно сам cda_d_direct -- "прямой" транспорт, передающий вызовы от cda почти как cxlib, только не через сеть, а напрямую. Или это и есть inserver? 07.04.2011: да, по смыслу это оно и есть. Но для наглядности, чтоб не указывать параметром какой-то непонятный "inserver::", можно сделать "direct" синонимом -- чтоб ссылался на то же самое описание.
ЗАМЕЧАНИЕ: а вот libcxsd НЕ будет позволять гонять в рамках ОДНОГО процесса НЕСКОЛЬКО независимых серверов -- как минимум потому, что во всех методах драйверов и API драйверов есть только devid, но никакой ссылки на "объект сервера" нету.
07.04.2011: а еще явно напрашивается аналогичный клиент для быстрых АЦП -- "fastadcclient".
07.04.2011: и, кстати: можно бы, чтоб в варианте direct::/inserver:: оно позволяло бы прямо из GUI менять адрес блока.
07.04.2011: а еще -- раз уж использование селивановскостей столь повсеместно, то не сделать ли всё-таки спец.layer "selcankoz", работающий именно через CANGW-Selivanov?
Более симпатичен 2-й вариант -- он хоть и менее гибок, но более удобен и практичен.
06.05.2011: а ведь вырисовывается картина, что у нас есть энное количество "сущностей" -- устройств, и у них имеется некоторое количество возможных "properties" (тип, опции, списки каналов, линки, auxinfo, ...).
Вопрос: не генерализовать ли вообще эту идею -- чтобы вместо нынешней позиционной нотации был psp-style набор пар "СВОЙСТВО=ЗНАЧЕНИЕ"?
Тогда всё стало бы очень легко расширяемо (кстати, в EPICS'е кабы не так всё и устроено...).
Неа, не хотелось бы. Читабельность и элегантность упали бы ниже плинтуса, плюс, возможно, auxinfo тогда б пришлось указывать в кавычках. И вообще -- не нравится.
06.05.2011: а собственно реализовать заготовку "ядром" "линков" для драйвера/девайса -- совсем несложно:
Проблема в другом -- всё-таки в собственно ПОЛУЧЕНИИ списка каналов:
Видимо, добавить в таблицу еще одно поле -- "смещение от базы".
С одной стороны, тогда можно будет хитровыпендриваться -- прямо в драйвер зашивать ссылки, так что можно будет в конфиге ничего и не указывать.
С другой же -- не перемудрить бы. а то начинают вылазить всякие хитроподвывернутые варианты, типа "можно часть каналов указывать полными ссылками, а часть через базу".
Еще одним полем в той "таблице"?
23.01.2014: в v2 изрядная часть "ядра линков" реализована в vdev.
06.05.2011: и есть еще одна проблема, более высокого и общего порядка: а что делать, если разные компоненты требуют один и тот же канал (т.е., с совпадающей полной ссылкой "СЕРВЕР.КАНАЛ"), но с РАЗНЫМИ способами опроса?
По умолчанию получится, что сделается так, как попросил ПЕРВЫЙ, а остальным вернут тот же handle, и с теми же "правилами" обновления.
06.03.2014@лыжи: по впечатлениям от общения с Пашей Чеблаковым получается, что в EPICS поперемешаны (разделение есть, но плохое) две разных сущности/концепции:
Собственно, можно и в v4'шном CX-сервере тоже иметь в
дополнение к обычной работе с железом -- "EPICS-style links layer",
обеспечивающий похожий функционал для “processing nodes”;
работать ему лучше через cda, а грузиться как "библиотека" (директивой
load-lib
).
И в этом смысле -- и драйверы, и всё прочее становятся примерно одного порядка, и имеющиеся сейчас различия (типа всяких хитростей в протоколе) будут потихоньку нивелироваться, в пользу неких более общих "связей".
Из-за необходимости реализовать функционал "макроустройства" ADC4M, состоящего из связки {ADC4, линия задержки, усилитель} -- и управляемого атомарно, в рамках одного большого канала -- пришлось городить завёрнутую структуру, с:
И по ходу реализации в голову лезли всякие дикие мысли -- типа не использовать ли "объектность наследование"; для чего пришлось бы полностью публиковать все потроха наследуемых драйверов, включая privrec и методы, да еще и какие-то особые правила/инфраструктуру для этого вводить.
А при наличии возможности одному драйверу дирижировать другими (возможно, эксклюзивно, через наложение блокировок) -- всё бы делалось легко и стандартно, без извратов.
25.09.2011@Снежинск-каземат-11: крупной проблемой в v2 является то, что возможности программы -- однонаправленные: она может дать некую команду (типа смены числа шагов d КШД), но просто "спокойно" исполнить некоторую последовательность действий с обратной связью -- оч-чень нетривиально. Причин 2:
Результат -- нельзя сразу выполнить несколько запросов на запись, если они должны произойти в определённом порядке, приходится их секвенсировать, по 1 за цикл.
Поэтому (в основном) и был придуман seqexecauto, чтоб позволить следить за "результатом" как бы "исполнения" команды, и уж потом передавать упарвление следующему шагу.
25.09.2011@Снежинск-каземат-11: часом позже: вообще-то такая "разделённость" -- это неизбежно просто по определению: вместо привычного человеку "запустил процесс, смотришь как он идёт, пока не завершится" тут НЕТУ такой непрерывности. Эта процедура делится именно на "инициацию" и "условие окончания" -- как и было сделано в seqexecauto.
И не зря ведь еще с давних пор есть желание мочь интегрировать sea в формулы (ныне -- с учётом сценариев) -- оно как раз и является натуральным решением проблемы.
27.09.2011@Снежинск-каземат-11: а конкретно ТА проблема, с КШД, должна решаться вообще по-другому: в дополнение к нынешним каналам "NUMSTEPS" и "GO" надо завести еще канал "GO_N_STEPS" (как бы объединяющий те два АТОМАРНО), в который можно было бы сразу отправить желаемое число шагов, и оно б поехало сколько надо. Тогда можно будет заводить в экранной панели сколько угодно кнопок "ехать на столько-то".
06.10.2011: в nkshd485 каналы
NKSHD485_CHAN_GO_N_STEPS
и
NKSHD485_CHAN_GO_WOA_N_STEPS
предусмотрел, так что эта
КОНКРЕТНАЯ проблема вроде получила своё решение.
20.02.2013: по порядку:
И все обращения на чтение/запись/подписку реально должны идти к "inode".
Но иметь раздельные коэффициенты было б ЧРЕЗВЫЧАЙНО удобно: тогда можно будет в одной и той же софтине обращаться и к аппаратному каналу (с вольтами), и к точке контроля (с амперами). 03.04.2013: да, ОЧЕНЬ нужно. Сегодняшний пример -- GID25x4: там надо видеть и пересчитанные значения от CEAC124, и (для понимания ситуации) исходные, для чего просто иметь subwin-панель "CEAC124". Но в v2 панель вынужденно пользуется коэффициентами для амперов...
Что ж делать, а?
И отдавать "inode" самого alias'а тоже можно (для чего придётся заводить их отдельным массивом каналов) -- чтоб в клиенте каналы считались разными, и cda не пыталась бы возвращать уже имеющийся handle.
Пока вменяемого решения не видно (кроме делания на каждый alias inserver-callback'ов, что совершенно неоптимально и затратно).
Напрашивается ограничение еще на одном уровне -- на среднем, в сервере. Это как бы расширение первого пункта -- ограничение по точкам контроля, т.е., как бы по логическим значениям, но централизованное, не зависящее от клиентов.
Главный вопрос -- как должны указываться эти диапазоны: в конфиге, или же как-то на живой системе, с возможностью изменения на лету, или как? И, с учётом пункта (I) -- не делать ли привязку ограничений не к аппаратным каналам, а к точкам контроля?
03.03.2013@вечер-21:00, пешком по Лаврентьева с парковки: идея -- сделать "идентификатор аппаратного канала" таким же свойством именованного канала, как и коэффициенты. Тогда:
Возможные детали реализации:
Формально по протоколу будут возможны запросы на чтение/запись/... с точками контроля, но сервер может на них отвечать "ошибка" (а правильный клиент такого запроса и не пришлёт).
В случае переконфигурации клиенты всё равно повторят резолвинг с нуля.
Тогда клиент их тоже примет к сведению (если надо), а дополнительный запрос пришлёт на отсутствующую информацию (если таковая останется).
04.03.2013: Замечание: вообще-то, если cda будет проверять "уникальность" по именам, то вся проблема почти исчезает: у аппаратных каналов и alias'ов имена будут в любом случае разными.
Хотя, нет: хоть cda такие каналы и не смешает, но не будет возможности различать их на уровне сервера в целях получения разных {r,d}. Посему -- withdrawn.
20.03.2013: еще несколько соображений в ту же степь:
А если б еще просто CAN-блоков (откликаются ль на FF/еще-что-то?).
Это сильно похоже на концепцию "IOC health monitor".
Как вариант: пусть имена вида "что.то.там._devstate" резолвит так -- т.е., чтоб "_devstate" можно ббыло приписать к любому имени канала.
25.09.2014: зачем вообще какой-то резерв? Ведь адресация ВСЯ будет по именам, на номера никто смотреть вообще не станет. Так что нефиг -- ровно 1 штучка, и всё.
03.04.2013: еще насчёт alias'ов: хорошо б иметь возможность делать alias'ы и на УСТРОЙСТВА, а не только на каналы. Как симлинки на директории.
Это позволит делать "стандартных клиентов" не только для одиночных устройств, но и для групп. Как, например, набор CEAC124+CGVI8+VSDC2*2 для GID25x4. Таких "групп" может быть толпа, а "множественные базы" указать в командной строке затруднительно. Вот можно сделать "поддерево", населённое стандартизованными alias'ами на конкретные устройства, и указывать программе в качестве базы путь к этому поддереву.
Единственная тонкость -- что попытка получать свойства таких имён в смысле каналов должна обламываться (как и для обычных имён устройств). И, естественно, никаких {r,d} у них быть не может.
02.04.2014@Снежинск-полигон-автобус-с-обеда: а "цепочка пар {r,d}" применима не только к скалярам, но и к векторным каналам -- уж к осциллографам точно (речь об индивидуальных каналах): там имеются ровно такие же калибровки (коэффициент и сдвиг нуля), требуемые быть использованными при отображении на экране и выводе числами.
Т.е., изначально так: свойства {r,d} доступны не только скалярным, но и векторным каналам; а раз и alias'ы точек контроля к ним применимы, то и цепочки {r,d} тоже.
04.06.2014: а еще в дополнение к {r,d} надо мочь каждой точке контроля указывать "units", просто строкой. Естественно, клиенту отдаваться должно только от самой точки контроля, безо всяких цепочек (да и лазить вглубь в предыдущие при неуказанности тоже вряд ли стоит).
(По опыту беркаевского CAS'а -- у них в свойствах каждого канала такое есть.)
09.06.2014: а еще в дополнение к
{r,d} и "units" надо мочь каждой точке контроля указывать разрешенный
диапазон. Это -- для cda, чтоб в клиентах ограничение делалось
на сравнительно низком уровне (без участия Cdr). Кстати, в cxsd-devlist.html оно --
[MIN MAX]
-- давно присутствует.
А еще надо бы мочь указывать разрешенный диапазон и
обычным каналам записи -- чтоб ограничение было уже совсем
жесткое server-side. Только пока что негде указывать -- задание
свойств АППАРАТНЫХ каналов отдано на откуп драйверам, в директиве
channels
такого не предусмотрено.
Нынешний формат channels
и cpoint
явно не
соответствует задаче.
Тут можно принять какое-нибудь упрощение: например, что указание
{X..Y} где-то в серединке идентификатора
(ИМЯ_АППАРАТНОГО_КАНАЛА
или
ИМЯ_ТОЧКИ_КОНТРОЛЯ
) отвечает за эту "генерацию группы", а
в target-токене (НОМЕР_АППАРАТНОГО_КАНАЛА
или
ИМЯ_АППАРАТНОГО_КАНАЛА
) должен быть указан ответный
диапазон {N..M}.
22.08.2014: еще насчёт "как с cda+inserver::" хорошо:
Автоизготовляемые каналы "DEVNAME._devstate" позволят следить за статусом УСТРОЙСТВ (что нужно для всяких vdev) через тот же самый API (и через cda!), не требуя никакого отдельного интерфейса.
Только надо будет убрать правило, что при уходе устройства в OFFLINE все линки на него снимаются (оно всё равно имеет смысл только для remcxsd, а там меж-драйверное общение под большим вопросом).
01.03.2015: уже несколько дней (в основном @лыжи :)) гложет мысль "А не сделать ли в сервере по ДВА комплекта {r,d} на канал?", чтобы и драйвер указывал свой коэффициент, и калибровочный коэффициент можно б было навесить из конфига?
Ответ: неа, нефиг -- пусть делается точка контроля, и уж НА НЕЁ навешиваются ЕЁ свойства.
...плюс такое явно противоречило бы потребностям, изложенным 13-04-2013 -- "видеть и пересчитанные значения, и исходные".
Так что "withdrawn".
Это может быть полезно для "изготовления скринов на лету", по информации прям от сервера. Да и вообще добавляет системе полноты.
17.03.2014: мысли такие грызли давно, просто сейчас наконец сподобился записать.
Сама задача разбивается на две крупных части: ПОТРЕБНОСТИ и РЕАЛИЗАЦИЯ.
У нас же подразумевается, что если "корень" (sys) в некоем сервере, то и всё его содержимое тоже в том же сервере. Точнее, даже не "sys", а server:N.sys.
Обсуждение:
Кривовато. Кроме того, это оставит "дырки" в массиве каналов у sid'а, "смотрящего" на этот сервер.
Видимо, её надо менять.
06.10.2014@Снежинск-автобус-из-каземата-на-обед: вопрос только -- а КАКОЕ stream-соединение? КУДА? /tmp/cxv4-resolver-socket?
Сами обязанности "оракула" не должны никак интерферировать с обычной работой сервера. Это как бы "задача внутри процесса": смог занять порт -- становишься оракулом; не смог -- коннектишься к оракулу и следишь за его сдыханием; не смог занять и не смог приконнектиться -- нештатная ситуация, глюк какой-то.
И -- получивший лычки оракула остаётся таковым пожизненно. Хотя, в принципе, сложить обязанности несложно -- сделать close() udp-сокета и всех приконнекченных собратьев; вопрос лишь "зачем?".
16.10.2014: о терминологии: в топку термин "оракул" (Oracle? Ню-ню!), пусть будет "гуру"!.
Например, каналы "с неизвестным корнем" могут иметь названия с каким-нибудь хитрым префиксом, вроде "???" или "UNKNOWN" на месте сервера. А подставляться такой префикс может как-нибудь там при генерации скрина.
Или еще вариант: в таких не-тривиальных случаях вначале добавляется дополнительный шаг -- спрашивание у БД им.Макеева. Правда, это не решает проблемы"навсегдашности" -- что каналы никак не смогут переехать в другой sid.
А КАК? Тут ведь есть варианты:
Возможно, правильным будет средний вариант: у каждого dataref'а имеется sid-"хозяин" (sid в этом смысле -- просто контекст), с чьим участием dataref был создан. Так вот, при гроханьи sid'а-хозяина -- ГРОХАТЬ dataref'ы, а в остальных случаях -- ОТВЯЗЫВАТЬ.
25.03.2014: в свете подчинения всего контекстам и потери sid'ами суверенитета: проблема исчезает целиком, грохать sid'ы сможет только сама библиотека, и будет это делать при гроханьи контекста, что сделает и все dataref'ы инвалидированными.
24.03.2014: смотрим запись за 19-12-2012 :)
18.03.2014: кстати, старая идея решения этой
задачи -- макроподстановки типа $LINMAG
(смотрящий на
linac1:31). Не факт, что это вообще хоть где-то записано
(скорее всего, только у меня в голове). 24.03.2014: да, записано -- см.
bigfile-0001.html за 28-04-2005.
19.03.2014: а не разделить ли вообще концепции "контекста" и сервера? И чтоб у контекста все сервера создавались по мере надобности в нужном количестве. А указанный при создании контекста "defserver" был бы префиксом по умолчанию, но НЕ чем-то выделенным -- с ним sid пусть тоже создаётся по мере надобности (и необязательно первый в списке у своего контекста).
22.03.2014@душ: UDP-порт можно использовать тот же 8012.
23.03.2014: касательно "контекста" -- а не сделать ли тот же контекст, что задуман для формул и прочего? Т.е., поменять приоритеты -- "контекст" сделать первичным, а sid'ы -- его подчинёнными.
Соответственно, сначала создаётся "контекст" (и именно ему указывается defserver ("префикс")!), а потом к нему добавляются каналы. Новые sid'ы же могут создаваться/добавляться вообще автоматически по мере надобности.
Кстати, при этом исчезнет и странная выделенность,
используемая в cda_status_*()
"ns=0 -- основной, 1-lastn
-- дополнительные". Теперь всё стандартно -- 0...(count-1).
24.03.2014: насчёт собственно "zeroconf-резолвинга":
А протокол по умолчанию будет "CX::".
Таким образом, просто имена каналов (без протокола/сервера) будут автоматом резолвиться поиском.
И ведь даже факт указания ЭТОГО именно в префиксе (defserver) использовать никак нельзя -- имена должны быть полностью определимы/расшифровываемы сами по себе. Поскольку многие каналы могут указываться FQCN'ами прямо в середине группировки; собственно, вопрос -- "а как теперь определять FQCN?".
07.06.2014@8-ка-в-город ~11:00, поворот со Строителей на Бердское: нюанс -- а КОГДА и КАК слать UDP-бродкаст резолвинга?
В любом из подвидов к моменту наступления "события" как раз все запросы и накопятся -- потому, что событие придёт от cxscheduler'а, который получит управление ПОСЛЕ всей инициализации.
Разумно выглядят варианты (2) и (3) вместе: (2) для "глупых" консольных утилит, а (3) для Cdr-based клиентов.
16.10.2014: идеологический вопрос -- ЧТО должен отвечать резолвер, какой АДРЕС? Номер сервера (он же порт) -- понятно, а адрес-то как указывать?
Конкретно на сейчас покатит очень простой ответ:
cda_dat_p_get_server()
в качестве
srvrspec
указывает просто AAA.BBB.CCC.DDD:N.
Да, некрасиво. Но для начала пойдёт, поскольку работать будет.
17.10.2014: еще идеологический вопрос: КАК должен быть сдизайнен резолвер со стороны клиента?
...по асинхронному устройству должному быть похожим на обычный API работы с данными.
И, видимо, держать в себе некий готовый "пул" ответов.
cd
клиента, поскольку оно будет reuse'нуто.
26.05.2015: давно терзали мысли -- как может функционировать резолвинг? Проблема в том, что предполагалось
И при этом проблема, что если 1. заказ; 2. заказавший (напр. драйвер) отваливается; 3 приходит ответ -- то что делать? Вызывать то, чего уже нет?
Сегодня пришло некое понимание/осознание, КАК правильно поступить.
UDP-сокет можно иметь как один общий на всё, так и по-"объект"но. Это технологически маловажно, поскольку при удалении одного клиента и создании другого он может получить сокет с тем же source-port, и на него прекрасно придут "чужие" ответы (на отправленное раньше), так что распознавание всё равно надо делать вручную, не полагаясь на механизмы ОС (ибо их подходящих НЕТ).
Идентификатор -- 64-битное число, реализованное "вручную", в виде пары 32-битных.
09.06.2015@8-ка-в-город~14:00, в районе Сеятеля: всё кристально ясно, как делать на стороне cxlib: просто операция вроде "cx_new_resolver()", создающая "объект" для резолвинга, симметричная ей "cx_del_resolver()", и "cx_call_resolve()", собственно буферизующая/шлющая запрос. Опционально еще "cx_flush_resolvings()" -- отправляющая забуферизованное (главный вопрос -- КАК, кто и когда её вызовет? как-то Chl/Cdr должны уведомить cda, а та дёрнуть "метод" всех серверов?).
09.06.2015@8-ка-из-города~17:00, тошнили в пробке до Матвеевки: всё неясно ж, как должны "жить" каналы RSLV_TYPE_GLBL в состоянии RSLV_STATE_UNKNOWN. Какая-то "магия" напрашивается.
Мысль: можно из помещать в специальный "сервер", который не нормальное соединение, а именно для резолвинга. Тогда всё симметризуется: и каналы всегда при каком-то сервере (а не в "нигде"), и подчистка объекта-резолвера выполнится автоматически при удалении контекста.
Есть, конечно, и неприятности:
...ну некоторое сходство есть, да -- список каналов (frs_hwr,lst_hwr) и cd.
Вообще напрашивается мысль: сейчас забить на фичу резолвинга и заняться другим (rem*). А потом, как появится более красивое понимание -- вернуться и добить.
22.07.2016@пляж-~10:00...11:00: вроде появилось понимание всех идеологических тонкостей функционирования резолвинга на уровне cda, cxlib и даже CX-протокола (все неподписанные "comment2" -- от после-обеда за сегодня же):
Это расходится с идеей от 16-10-2014 об ответе ("просто номер/порт").
Проблема (да, замеченная 19-06-2015): будет
светиться в LED'ах. Решение: ввести
cda_dat_p_get_server()
'у флаг "не добавлять в список"?
Это расходится с идеей от 26-05-2015 о безразличности ("маловажно").
Правила поведения:
А лучше -- бесконечно малый, вроде 1ms. Смысл -- как замечено 17-06-2014, событие "таймаут" придёт от cxscheduler'а, который активизируется уже после того, как весь список каналов будет заказан.
Смысл -- отсутствие смысла в том правиле.
А защита от "переполнения буфера" -- сама отправка: когда попытка послать вернёт ошибку, тогда и тормозим отправку.
...вообще, отправка сейчас идёт через fdiolib -- это как-то может и
проконфликтовать с таймаутами. И чо -- нужно уведомление "была сделана
отправка"? Или слать самостоятельно, напрямую sendto()
, а
возобновлять отправку по таймауту?
Пожалуй, стоит.
24.07.2016@утро-душ: пара замечаний насчёт формирования пакетов:
@после-обеда: кар-р-рамба!!! Ведь 12-01-2015 речь шла о ПРОСТО НАБОРЕ СТРОК, совсем БЕЗО ВСЯКИХ chunk'ов!!! Так имеем максимальную экономию объёма в пакете, хотя и за счёт времени на поиск.
26.07.2016: а вот и нет -- НЕЛЬЗЯ отвечать просто строками! Надо ж еще номер сервера как-то указывать (2 байта).
Поэтому промежуточное решение:
01.12.2017: а вот и нет!!! Вовсе НЕ с "префиксами", а с
обычными CxV4Chunk
было сделано при реализации 09-06-2015, и
это правильно!
NumChunks
.
CXT4_RESOLVE
.
26.07.2016: (реально -- вчера) и еще: надо всё-таки иметь в cxlib'е общий пул закэшированных ответов, чтобы не плодить одинаковые запросы пачками из всяких cx-starter'ов.
Посему:
...кстати, такое же может произойти и при обычной работе -- гуру ответит, что канал там-то, а за время хождения ответа сервер успеет перепуститься.
Или, хотя бы, при закрытии ПОСЛЕДНЕГО соединения с сервером с этим IP:N.
...а если ответы были получены, добавлены в пул, но соединения с сервером никогда не произошло? Ситуация бредовая, но пункт (a) должен в случае чего решить проблему.
30.11.2017: через почти полтора года после написания...
Хоть оный DDoS вряд ли случится в сети пультовой, но наличие самой возможности, заложенной в идеологии, выглядит крайне неприятно.
strdup()
'ом)?
Но тогда это крайне неудобный в обращении инструмент, т.к. поиск по нему возможен только линейный с самого начала, а любые махинации весьма неприятны.
Этакий объект для нечастых lookup'ов, желательно с минимумом модификаций.
Может -- ну его нафиг, этот пул, и для начала сделаем всё по-простому, с запросами и ответами, без дополнительного кэширования (коее выглядит плохо продуманным и могущим принести больше вреда, чем пользы)?
17.12.2017: в основном всё сделано.
22.12.2017: идеологически-методологический вопрос -- а насколько вообще хорошо, что в роли "гуру" работает процесс обычного сервера? Ведь тем самым на него создаётся дополнительная нагрузка.
Вот если бы можно было увести это занятие в совсем отдельный процесс -- было бы замечательно. На многоядерной машине он мог бы спокойно обслуживаться отдельным ядром, никак не мешая основной работе.
Но проблема в том, как прибить все остальные ресурсы. Дескрипторы-то
файловые еще можно все поприкрывать (циклом), но прочие ресурсы (fdiolib,
cxscheduler, stdio, ...) -- фиг. Проще всего, конечно, было бы запускать
отдельный процесс -- exec()
'ом, при этом всё прикроется
автоматом. Но это сведёт на нет достоинство "гуру запускается
автоматически".
02.04.2014@Снежинск-каземат-11: "жертв" сходу видно двое -- стек Knobs*/Cdr/cda () и cdaclient.
09.06.2014: да, введены
CXDTYPE_REPR_UNKNOWN
=0 и CXDTYPE_UNKNOWN
(у
него size=0, т.е. само значение тоже =0). 29.02.2016: а
вот и нет, у него не "size=0", а "КОД в битовом поле size равен 0", т.к.
размер в байтах -- 1; но само значение CXDTYPE_UNKNOWN -- да, 0.
Есть, конечно, некоторые сомнения -- разумно ли так занимать значение 0, не оставить ли его для "UNINITIALIZED"/"INVALID". Если что, то пока в любой момент (пока не началось внедрение CXv4, с отдельными модулями) можно всё сдвинуть на единичку.
17.10.2014: часть задачи решена нынешней
реализацией cda_dat_p-API: там есть авто-конверсия, поэтому канал может
менять своё представление -- intNN, single, double -- на лету (даже без
переконнекчивания), а уж cda_dat_p_update_dataset()
переведёт тип как надо.
НО! Это касается именно ПРЕДСТАВЛЕНИЯ, а вид -- скаляр/вектор -- меняться не может, поскольку он зафиксирован (сейчас?) в dataref'е.
Например, в магнитной системе/коррекции когда определят, какой канал ЦАП/АЦП за что отвечает, поименуют -- и сразу в клиентах всё появится.
20.07.2014: для реализации этой возможности:
...так можно и до units с dpyfmt доиграться -- а с ними неприятнее...
28.07.2014: а до ident? Чтоб и имена тоже обновлялись по мере надобности. (Конечно, чревато -- при этом взаимоссылки нифига работать не будут. Но зато какая гибкость! А цена реализации нулевая -- всё вместе с прочими строками.)
Заодно станут ненужными извращения вроде usertext.
Разница лишь в том, что если {r,d} могут меняться драйверами на лету, то текстовые строки -- нет, они указываются только в конфигурации.
Причём должно производиться должное "наследование с возможностью замены" -- если не указано в cpoint, то брать из channels; если не указано в channels, то брать из devtype.
Сейчас всё просто -- несколько свойств через пробел (а у cpoint еще и опциональные
[R [D [MIN MAX]]](коие стоило б распространить и на аппаратные каналы, умолчательно считая "1 0 0 0").
Здесь же добавится 4 СТРОКОВЫХ свойства, могущих требовать закавычивания с несколько забардачивающих вид.
@засыпая-на-ночь:
ParseKnobDescr()
: нынешние свойства идут как есть, а
если надо указать дополнительные строковые -- то тэгированно, вида
КЛЮЧ:ЗНАЧЕНИЕ.
22.07.2014: см. записи от 04-06-2014 и 09-06-2014 выше. Итого -- те же мысли по второму разу :)
Вместо тупо номера source-knob'а указывать им
в XmNuserData комбинированное значение:
((type<<24)+n)
, где type={0:chan_n, 1:colnames[n],
2:rownames[n]}.
Вопрос сдвига на nattl при этом снимается -- виджету УЖЕ передаётся сдвинутый на сколько надо номер model_k.
(А какую колонку разворачивать -- collabel и так решает по своей grid_x.)
Идеи ответов:
...приятнее кажется первый вариант: и меньше умничать, и на остальное использование этих строк (при собственно сбагривании тулкитам для отображения) не повлияет.
30.07.2014@утро-перед-уходом-на-работу: тогда был забыт еще один аспект --
reason
-- ну вот ввести при регистрации evproc'а (и канала) еще параметр
"reason_mask", где указывать набор интересующих событий.
15.08.2014: а чё б вместе со строками не отдавать наверх также и диапазоны -- все 4? Чтоб при их неуказанности в клиенте они бы использовались из конфига.
Естественно, при этом придётся добавить в CxKnobParam_t
поле "specified
".
12.08.2014@пляж: контейнерам нужен метод "ChildPropsChg", чтоб метки обновлять.
12.08.2014: сделана начальная часть со стороны cda_api:
cda_dat_p_set_strings()
. Она сохраняет всё
переданное в один буфер (строки друг за дружкой), плюс указатели на
каждую из строк.
CDA_REF_R_PROPSCHG
, и это событие
"генерится" вышеуказанной функцией.
13.08.2014: основная часть реализации:
cda_strings_of_ref()
.
data_knob_t.strsbhvr
, плюс маски
DATAKNOB_STRSBHVR_nnn
CdrRealizeKnobs()
проверяет пустость строковых
полей, и выставляет в strsbhvr соответствующие биты.
Тут дискуссионный вопрос -- а КАК проверять пустость?
NULL
либо "".
rd_ref
указывается обработчик на
CDA_REF_EVMASK_PROPSCHG
, ...
RefPropsChgEvproc()
,
DPYFMT_fparser()
).
PropsChg()
и...
ChildPropsChg()
у uplink'а.
ChildPropsChg()
был введён. Для удобства ему
передаётся не указатель на изменившийся knob, а его номер --
k - k->uplink->u.c.content
.
cda_add_chan()
вызов
cda_add_dataref_evproc()
переставлен в точку ДО вызова
new_chan()
-- в противном случае в момент "обновления"
свойств evproc был незарегистрирован и ничего не ловил.
RefPropsChgEvproc()
строки вычитываются из
переданного ему ref
, а не из k->u.k.rd_ref
-- поскольку он вызывается еще до возврата из
cda_add_chan()
, когда rd_ref=0.
Итого:
24.04.2015: ага, только был косяк: не делалось
bzero(&strs)
, так что для direct-каналов --
SERVER:N.CHAN_NUMBER -- в strs
оставался мусор и
SIGSEGV'илось. Исправлено.
10.01.2015: "PROPSCHG" повсеместно переименовано
в "STRSCHG", а RefPropsChgEvproc()
в
RefStrsChgEvproc()
.
04.07.2015: в
cda_dat_p_set_strings()
был косяк в работе с памятью:
ri->strings_buf
.
Исправлено -- сейчас при size==0 делается free() и new_strings_buf=NULL, а проверка if(safe_realloc()==NULL)return -- только в случае реального реаллокирования.
...по-хорошему -- надо б там вообще на
GrowBuf()
переходить.
Но это идеологически криво -- информация-то КОНФИГУРАЦИОННАЯ (у обычных каналов (типа коррекций), работающих без доп.драйверов, указываемая в точках контроля).
Потому, что "создание" индивидуальных аппаратных каналов происходит
в cxsd_hw -- CxsdHwSetDb()
, а не в cxsd_db.
type_nsp_id
, потом персональный
chan_nsp_id
) и прописывать их props'ы в каналовы.
cxsd_hw_chan_t
(юзуемую), так и в
CxsdDbDcLine_t
(куда складывать результат парсинга).
Это, кстати, могло бы касаться и обычных драйверовых phys_r,phys_d,
но там уже есть флаг phys_rd_specified
.
ЗЫ: отдавать ПЕРЕД драйверовыми -- как от cpoint'ов (коевыми эти r,d, по сути, и являются (от некоего "фантомного" cpoint'а)).
02.08.2015: строго говоря, это -- "вторые {r,d}" -- нехорошо.
Вот если б можно было отдельно делать namespace, и отдельно указывать "КАНАЛ:СВОЙСТВА" (например, "out0 min:-5.0 max:+5.0"; при том, что сам маппинг канала "out0" определён ранее), то было бы приемлемо, а так шибко денормализовано.
05.08.2015: но оно -- chan_r,chan_d -- нам всё-таки нужно, прямо сейчас, чтобы vdev-устройства запустить и протестировать.
Так что -- сделаем, попользуемся, а потом, если чё, выкинем.
05.08.2015@пляж: да какого чёрта -- у нас же есть cpoint'ы! Вот брать и делать на них всё как надо, и не экономить.
Кстати, за счёт того, что имя cpoint'а может перекрывать имя реального канала, получается фича "можно добавить реальному каналу {r,d}" -- тогда можно НЕ создавать полностью новое виртуальное устройство, а "модифицировать" только несколько калибруемых каналов (уставка, rate, измерения). При этом и код экранов менять не придётся -- автоматом возьмут "модифицированные" каналы, не заметив подмены.
05.08.2015: да, так и сделано. Работает!!! Всё благодаря механизму "перекрывающих cpoint'ов" -- он хорош, даёт гибкость и резерв для манёвра.
Так что на проект "вторые {r,d}" забиваем.
А вот для указания прочих свойств -- min/max, color -- разработанный сценарий можно использовать. Кривовато, конечно (что свойства смешаны с namespace'ом), но пока не придумаем что-нибудь получше -- покатит.
Но можно ж сделать возможность крутить эти настройки на лету, в живом сервере -- хоть через протокол CX, хоть через консоль. Суть сведётся к модификации чисел (да, в readoonly-БД...:-() и потом "раскрутить точку контроля до target-аппаратного-канала и вызвать ему событие RDSCHG.
(Побудительный мотив -- когда Беркаевым обнаружилось, что в v2'шном скрине ringcor45 на пульте для корректоров в RST5 стоят неправильные коэффициенты (старые; исправлено было прямо в ИП-1 при Довженко, но программы на linac3/worker1 обновить забыл). Ведь такое же может возникнуть и в v4 -- когда выясняется, что коэффициенты "не те", но зачем рестартовать сервер, если несложно подкрутить всё прямо на лету? Заодно удобный инструмент настройки и тестирования.)
06.10.2013@душ: конкретные соображения касательно покамест КЛИЕНТСКОЙ части.
Хотя отдельный вопрос, какому thread'у будут приходить уведомления от fdiolib, т.е., в каком именно исполняется cxscheduler-совместимый цикл.
Так что -- лучше б на "в каком" не закладываться.
Кстати, locking нужен:
А вот по СЕРВЕРНОЙ части всё выглядит намного непонятнее. Хотя тупой глобальный lock тоже для начала покатит.
24.01.2015@Сессия-ИЯФ-Сухарев-12:20: мысли в продолжение вопроса (независимо от ранее написанного):
...кстати, а КАК параллелить конкретно клиентов? Варианты:
...отдельный вопрос - что на один dataref могут быть навешены ссылки от разных thread'ов; и как распределять callback'и по thread'ам? Видимо, правильный ответ - каждому thread'у заводить СВОЙ контекст. (Либо ОДИН thread работает с cda, а дальше уж сам распределяет по "клиентским" thread'ам - тогда вообще ничего делать не надо, всё и так будет работать.)
26.08.2019: некоторые соображения на тему:
Но это вряд ли достижимо.
И думать надо ещё и про поддержку в СЕРВЕРЕ.
(Подумалось при обмышлении, как же работают is_internal-каналы и почему
они всё-таки функционируют при -wN
-- откуда у них берётся
не-0/1 timestamp.)
15.09.2019@утро-зарядка:
призрачное соображение насчёт потенциально множественных "polling threads":
если они будут (вместо одного фиксированного cycle), то надо будет в
ConsiderRequest()
при сравнении у не-rw-каналов "не было ли уже
померяно в текущем цикле" сравнивать chn_p->upd_cycle
не с
current_cycle
а с
pollers[chn_p-gt;pollid].current_cycle
Соответственно:
cxsd_hw_chan_t
надо будет добавить.
Как/где/откуда оно будет заполняться -- хбз.
По умолчанию оно будет =0, и будет являться индексом в...
pollers[]
-- массив оных "polling threads", где у каждого
элемента полями будут нынешние статические переменные в cxsd_hw.c:
basecyclesize
, cycle_start
,
cycle_end
, cycle_tid
,
current_cycle
=INITIAL_CURRENT_CYCLE,
cycle_pass_count
.
А вот cycle_pass_count_lim
можно иметь и один на всех.
16.09.2019: пара замечаний задним числом:
В EPICS -- оно явно прописывается в КАЖДОМ record'е (поскольку авто-создания каналов при инстанциировании ("экземпляризовании"?) устройств там нет, а описывается каждый record индивидуально, то и проблемы заполнить ещё одно поле не возникает.
В TANGO -- похоже, как-то в БД конфигурации указывается (как?).
07.09.2014: наружный вид самого API ясен: основную
работу должна делать функция CxsdHwResolveChan()
, принимающая
параметром полное имя, а возвращающая globalchan (либо -1 при
ненайденности или указании на "директорию"), массив {r,d} и указатели на
все 8 строк.
Прототип функции уже написан.
29.09.2014: чуть изменена концепция -- теперь
функция возвращает id ТОЧКИ КОНТРОЛЯ (cxsd_cpntid_t
), а id
канала (cxsd_chanid_t
) уже вместе с прочими "свойствами"
по своему указателю.
24.10.2014: дальнейшее обсуждение конкретно
CxsdHwResolveChan()
-- в её собственной секции в разделе
cxsd_hw.
03.11.2014@утро-душ: устройство собственно базы имён -- да простое:
Как-то зачеркивать строки, либо переставлять ссылки на имена...
Для начала на это просто можно забить.
В гуру, кстати, свойства не нужны и даже номера знать необязательно -- поскольку его задача лишь сказать, у кого из серверов канал с таким именем, а уж там при непосредственном резолвинге разберутся.
07.10.2014@беговая-дорожка: а зачем передавать гуру прямо полный (по-имённый) список каналов? Пусть бы он имеет в своём распоряжении ровно ту же структуру со списком устройств и каналов в них, что сам сервер, и выполняет такой же поиск.
09.10.2014@8-ка-из-города: передавать эту структуру по UNIX-сокету довольно просто, там ведь все размеры идентичны на разных концах (платформа одна), так что сериализация/десериализация тривиальна.
09.10.2014: еще некоторое количество размышлений в течение дня:
Но это не прокатит, поскольку у нас нет единой "текущей" БД, а она
раскидана по куче глобальных переменных cxsd_hw_*
.
ИДЕЯ 1 (@душ): а что, собственно, мешает все те переменные cxsd_hw_* свести в один тип-структуру и сделать переменную этого типа "cxsd_hw_cur"?
Мешает то, что там преизрядного объёма массивы
cxsd_hw_devices[]
и cxsd_hw_channels[]
, и
аллокировать на каждый из подключенных серверов по такому объёму --
транжирство.
ИДЕЯ 2 (@на-диване): постараться отделить инфу "по именам" от этих массивов, и в "типе БД" проставлять туда ссылки -- тогда можно сэкономить, аллокируя в гуру только ровно по занятому в сервере.
ИДЕЯ 3: а можно ль как-то через shared memory отдавать гуру доступ прямо к самой памяти сервера, чтоб он прямо в ней и делал поиск?
Обсуждение: ладно еще, что придётся все действия аккуратно обставлять mutex'ами; но вот с портабельностью тут будет полный абзац, плюс сервера разных uid'ов вряд ли смогут так работать.
28.03.2015@Аригато: решение найдено -- искать надо именно по БД, а не по cxsd_hw_*. Подробнее -- ниже, за сегодня.
23.03.2015@вечер-Технопарк-Zoomer-занятие-по-Arduino: (Федя с Пашей пахали, а я вышел в коридорчик+кухню поразмыслить о БД) некоторые мысли (прозрение!) об устройстве БД:
Имена (строки) держать в общем массиве (блоке
памяти), а ссылки на имена (ссылки, хранимые в
cxsd_hw_chan_t
и т.к.) пусть будут оффсетами; 0 --
пустое.
Тогда очень удобно и Realize/CxsdHwSetDb()
делать
(массив просто копируется), и в гуру его отдавать
(сериализация/десериализация становится тривиальной).
24.03.2015: вопрос только ГДЕ: в HW
(cxsd_hw_chan_t
), или всё-таки в текущей DB (как сейчас
имена устройств -- берутся по .db_ref->instname
). Видимо,
лучше в DB.
24.03.2015@после-обеда: если подумать -- а ЗАЧЕМ вообще в канале хранить ссылку на его имя? Ведь оно было бы полезно для резолвинга chanid->имя, а потребность ровно в противоположном -- имя->chanid. (Только если для сохранения режимов -- там имя канала нужно.)
24.03.2015: либо вообще реализовать devtype как этакий "namespace", и у экземпляра устройства проставлять ссылку на этот "namespace". А для без-devtype'ных (у которых личные channels) просто делаются отдельные namespace'ы, куда и проставляется namespace_ref.
24.03.2015@вечер-Технопарк-Zoomer-занятие-по-Lego: еще немного мыслей о namespace'ах:
25.03.2015: во всём этом есть еще один нюанс: devtype -- это больше, чем просто namespace; у него еще есть имя-типа и ИНФО_ПО_КАНАЛАМ. Так что и их надо будет добавить.
24.03.2015@утро-душ: а отдельные "точки контроля" ("символьные линки") так и делать отдельно, просто дополнительной таблицей, по которой и вести тупой последовательный поиск, только в ДВА этапа:
Возможно, при реализации этапы как-нибудь удобно самообъединятся.
24.03.2015: насчёт общих/по-устройственных
dev_namespace'ов есть идеологический вопрос: надо ли давать возможность
"наследования"/импорта информации в channels
из
devtype
'а?
Для ответа надо понять -- а КОГДА вообще понадобится использовать
индивидуальные channels
? Сейчас в голову приходят только
софтовые каналы -- "почтовые ящики". 31.03.2015:
не "когда", а "ЗАЧЕМ"! Для указания свойств индивидуальных каналов --
диапазоны, geoinfo и т.п.
28.03.2015@утро-душ: (или даже раньше) а всякие label/units/dpyfmt -- очень удобно хранить в том же strbuf, а в инфах записывать их оффсеты.
28.03.2015@Аригато: насчёт проблемы "а как бы в гуру использовать тот же код резолвинга, что в самом сервере?":
...кстати, внутренности Resolve() должны переехать в cxsd_db.
CxsdDbDevLine_t
), а не только в hw-строке
девайса (cxsd_hw_dev_t
).
28.03.2015@вечер: прописывание драйвером {r,d} надо бы делать прямо в БД, а не в cxsd_hw'шные данные.
29.03.2015@утро: а нифига, их ведь НЕТ в БД!
...точнее, в БД не обязательно будут ВСЕ каналы построчно -- поскольку в ней другой тип данных касательно устройств (в первую очередь об именах); обязательно же "существуют" все каналы именно в cxsd_hw.
Типа следствие: основное "мясо" Resolve() переедет в cxsd_db, но
"обвязка" останется в CxsdHwResolveChan()
, и именно она
будет делать "последний шаг", добавляя {r,d} из строки конечного
cxsd_chanid_t
'а.
29.03.2015@днём-ванна: как НЕдублировать строки в strbuf (поиск-перед-добавлением): вычислять длину образца (добавляемой), а потом идти по strbuf, ищя следующий '\0' -- если длины не совпадают, то сразу скакать дальше; так будет максимальная скорость, с минимумом потерь времени.
P.S. Давно ясно, что такая реализация -- с кучей строк, адресуемых числами, неизменяемых и неудаляемых -- аналогична X11'шным "атомам".
30.03.2015: НЕдублирование сделано, по тому алгоритму -- очень коротко и элегантно получилось.
В отличие от поиска по строке-идентификатору, сравнение при добавлении делается case-SENSITIVE, т.к. strbuf будет использоваться и для хранения всяких меток, а там надо сохранять что сказано -- ведь, например, "mV" и "MV" означают совсем разное.
31.03.2015: еще немного концептуальностей:
channels
нужны ясно
когда/зачем -- для указания свойств индивидуальных экземпляров каналов.
СЕЙ момент нет (поскольку вообще никаких свойств нету), но в близком будущем -- выглядит вполне актуально.
Для точек контроля это еще приемлемо (т.к. они только так и существуют, будучи указанными).
А для настоящих каналов -- лёгкий бред: ведь каналы существуют независимо от наличия у них имён (в EPICS всё проще-яснее -- там прямо свойства конкретного канала и прописываешь в описании рЕкорда, от имени и до всяких пределов; там ведь никакого наследования нет -- вот и проще).
Искать пытаться по обоим, но сначала по channels -- это позволит переопределять стандартные свойства (но только ВСЕ сразу, частично не удастся...).
...а сами target-имена записывать так же, как и просто имена -- оффсетами в strbuf[].
Авторитетным источником информации по именам каналов должна быть КОНФИГУРАЦИЯ, драйверы же -- чисто консультативны.
Причина очевидна: в момент запуска драйверов может еще не быть (при remdrv), или может быть режим симуляции, когда они необязательны.
Так что надобность в наличии в драйвере таблицы имён (или еще как-либо вытягивания их из какой-то таблицы с целью отдачи серверу) -- сомнительна.
12.03.2015: итак, мысль:
Что можно бы сделать как-то АВТОназначение номеров -- чтоб у драйвера были только имена, а уж сервер назначал бы каждому имени номер, единожды при загрузке драйвера.
Тогда программеру б не пришлось заниматься назначением этих чисел (что чревато ошибками).
Но:
(Да даже если сервер САМ будет складывать в предподготовленную таблицу -- один фиг.)
Плюс исчезает сортировка каналов -- "диапазоны"
Ну и т.д.
Так что -- идея вряд ли жизнеспособна. По крайней мере, в НАШЕЙ архитектуре, рассчитанной, в отличие от EPICS'а, на многоканальные устройства, и в которой номера каналов играют некоторую роль -- как минимум, они доступны драйверам, а не просто гуляют по сетевому протоколу (как CA'шный SID).
Так вот: видимо, надо признать, что идея эта -- вряд ли жизнеспособна. В первую очередь потому, что чисто идеологически она небезупречна: правильный ли это вообще подход, такое хранение экранов в БД? ...но в голове её держать надо -- потому, что оказывает влияние на архитектуру, и её учёт позволит сделать архитектуру лучше.
devtype: SERVER:N.devname
И они таковы потому, что не было адресации по именам. Но сейчас-то адресация по именам ЕСТЬ!
И назвать эту сущность-список "dataset".
Утилита сохранения режима, конечно, слегка нетривиальна -- надо после запуска еще дождаться, пока придут все запрошенные значения.
@обед-после-столовой-автобус-в-каземат: еще пара замечаний:
cda_dat_p_update_dataset()
для
не-call_update (опкод CXC_CURVAL, ответ на CXC_PEEK) генерить события с
другим reason, например, CDA_REF_R_CURVAL).
20.04.2015: да, так сделано. Прелесть в том, что не надо задумываться о типе канала (ro/rw), а решение принимается чисто по timestamp'у -- уж он должен быть TRUSTED только у rw-каналов. ...хотя, каналы типа SERIAL у cPCI-устройств тоже можно так помечать.
И да, термин "dataset" у нас теперь получается в ДВУХ смыслах (еще в драйверах DataSet), хотя и очень близких.
24.04.2015: а кто сказал, что каналы записи будут обязательно TRUSTED? Ведь совсем не факт!
По-хорошему, надо бы чтоб СЕРВЕР (или cxsd_fe_cx.c?)
как-нибудь организовывал при первоначальном вычитывании ПРИНУДИТЕЛЬНУЮ
присылку значения. Например, чтоб на rw-каналы даже в
ответ на PEEK присылалось бы обязательно
CXC_NEWVAL
.
...сделано. Теперь испытать бы это всё...
...и зачем теперь нужно TRUSTED?
26.04.2015: да, проверено -- обязательный
CXC_NEWVAL
на rw-каналы работает, и даёт нужный эффект.
Формат файлов -- можно тот же, что у dataset-*, это удобно.
Но само сохранение/считывание всё же нужно. Вопрос -- где брать список каналов для сохранения? Ведь желательно, чтоб он был не отдельным списком, а так же прямо из группировки и создавался бы. Добывать список всех REF_TYPE_CHN-каналов контекста?
09.10.2015: появилась мысль, как сделать: надо ввести behaviour-флаг -- например,
DATAKNOB_B_SAVABLE
-- и ключ "/s", выставляющий этот
флаг. Чтоб взведённость флага означала "данную ручку -- сохранять".
Т.о., идея аналогична оригинальной от 22-05-2014, но инверсно -- по умолчанию вообще ничего не сохраняется, а сохранябельности надо указывать явно.
(А проблема "behaviour -- поле в dataknob_knob_data_t" уже не
стоит, т.к. поле переехало в data_knob_t
.)
09.10.2015: и отдельный вопрос -- ЧТО сохранять в файлах в качестве "ссылок": ссылки на каналы или имена ручек? Учитывая желаемую унификацию с dataset-*, сохранять надо именно ссылки на каналы (полные?).
Но ведь и искусственные "каналы" (маппирующиеся на регистры) тоже надо уметь сохранять; как? Формально -- маппинг на регистры сработает автоматом.
Реальный вопрос -- с маппингом на ФОРМУЛЫ. Забить пока?
27.10.2015: отдельный связанный вопрос -- а с относительностью что? Коль будут записываться прямо имена каналов, то при "перенацеливании" ручек на другие каналы режим станет маловалиден.
28.10.2015: и еще вопрос: ВОССТАНОВЛЕНИЕ как делать?
Видимо, надо как-то сохранять и то, и другое -- чтоб cdaclient/dataset-restore использовали имена каналов, а Cdr -- имена ручек. Подумать о синтаксисе?
Тогда, кстати, и проблема регистров/формул решится автоматом, и относительности тоже.
10.11.2015: о синтаксисе подумано, ответ прост и ясен:
(ИМЯ_РУЧКИ)ССЫЛКА_НА_КАНАЛ
Соответственно,
ReadFromFile()
должен будет при
обнаружении '(' (после опционального "@TIME") пропустить всё до
')' включительно.
set_knob_controlvalue()
, а иначе -- берёт имя канала, конвертит его
в dataref_t и делает ему cda_set_dcval()
.
Кстати, надо бы иметь возможность сказать cda_add_chan()
'у, что
НЕ регистрировать новый канал, а только попробовать найти его среди имеющихся, и
если нет -- то дать "облом". Реализуемо легко -- добавить
флажок CDA_DATAREF_OPT_FIND_ONLY
.
...получасом позже: СДЕЛАНО (хотя и не проверено).
09.01.2016: всё-таки лучше указывать на НЕсохранение ручки/подветки, чтобы по умолчанию ВСЁ сохранялось. Поскольку теперь локальное сохранение будет уделом стендовых программ, а там опухнешь везде расставлять ключики "/s".
11.01.2016: кстати, парсить clientside-режимы имеет смысл через тот же ppf4td, только через схему "plaintext".
15.07.2018: "бла-бла-бла..." -- в смысле, много наговорено, но сделано всё по-другому и сильно проще. А именно:
Так что можно и Cdr'ные режимы восстанавливать cdaclient'ом, и, теоретически, cdaclient'овские подсовывать Cdr'у.
fopen()
со стандартным цикром
while(fgets(...)!=NULL
.
08.09.2014@вечер-лесок-от-Коптюга-у-НГУшного-стадиона: чуток в сторону, касательно блокировок cda:
Симпатичнее второй вариант: он позволит атомарно блокировать группы каналов (что необходимо для избежания race condition'ов -- по крайней мере, в рамках каждого сервера). Придётся, конечно, приложить мозги: разбивать запрос на по-sid'ные группы, сбагривая каждую кому надо.
Поскольку операция продолжительная, то надо будет асинхронно уведомлять клиентов, чему посвящен следующий пункт.
CDA_REF_R_xxx
.
А при восстановлении соединения dat-плагин должен попытаться заново получить блокировку (тут есть некоторые тонкости в случае zeroconf-резолвлемых каналов), и при успехе уведомить клиента.
И вот точно такое же (ТО ЖЕ!) уведомление должно присылаться и при первоначальном успехе наложения блокировки.
12.09.2014: продолжение подготовки:
cda_dat_p_rec_t.do_lock
, типа
cda_dat_p_lock_op_f
. Действие -- lock/unlock -- указывается в
параметре operation
.
CDA_LOCK_RLS
=0 и
CDA_LOCK_SET
=1 (на тему "блокировки даже чтения" пока не
думаем).
cda_lock_chans()
, ей передаётся массив
dataref'ов.
30.01.2025: при обсмотре
cda_lock_chans()
(как примера "групповой операции") было
замечено, что, кажется, в коде compare_ref_sids()
есть косяк:
там делается
return (ri_a->sid > ri_b->sid) - (ri_a->sid - ri_b->sid);
-- минус во вторых скобках выглядит странно: в результате мы из БУЛЕВСКОГО
вычитаем ЦЕЛОЕ (результат другого вычитания); его точно не надо заменить на
"<
"?
31.01.2025: судя по прочим использованиям
qsort()
, где делается именно "(a>b)-(a<b)", и
man-странице, где сказано возвращать (-1,0,+1) -- да, должно быть
"<
"; заменено.
Комментарий, который не были записан тогда, 12-09-2014: cda_lock_chans()
сортирует каналы посредством
qsort()
, которой передаётся функция-компаратор
compare_ref_sids()
. И вот в последней ещё тогда, в
сентябре 2014-го, был сделан этот ляп с вычитанием вместо сравнения; что
послужило моделью для использования qsort()
-- теперь уже и не
вспомнить, но явно копировалось из какого-то другого его применения в CX.
srvinfo_t
-- hwr_arr_buf
, и растёт по
мере надобности.
CDA_REF_R_LOCKSTAT
.
cda_dataref_evproc_t
добавлен параметр info_ptr
(в
той же позиции, что у context_evproc'а info_int
).
Теперь осталось завести хоть один плагин, поддерживающий блокировки. Видимо, первым будет cda_d_vcas.
12.09.2014: замечание на будущее: некоторую нетривиальность представляет процедура восстановления блокировок при обрыве+восстановлении соединения (речь конкретно про cda_d_cx.c).
...или идентификатор иметь глобальный вообще, в рамках cda? Тогда и у разных sid'ов изначальные наборы, полученные от одного вызова, будут иметь этот идентификатор совпадающим.
Встаёт вопрос о поддержании набора уникальных идентификаторов:
Да, в refinfo_t
придётся добавить поле "lock_id".
Проще всего это реализовать при сортировке: такие ref'ы считать "очень большими", чтоб загонялись в конец списка.
Результат на сейчас: к cda_dat_p_lock_op_f()
добавлен
параметр lock_id
(пока передаётся -1).
30.10.2014: пока простейшее:
cxsd_hw_chan_t
добавлено поле locker
.
ConsiderRequest()
введена проверка в стиле, как
описано 06-07-2007:
if (chn_p->locker != 0 && chn_p->locker != requester) return DRVA_IGNORE;
И предполагается, что в качестве locker
и
requester
используются либо devid/lyrid, либо
идентификаторы, генеримые CxsdHwCreateClientID()
.
05.12.2019: ЕманоФедя поинтересовался -- а как у нас с локингом, работает ли?
Ему нужно, чтобы его программа, по факту как бы изображающая из себя драйвер, но реально сторонняя и пользующаяся каналами noop'а, могла бы стать единственной, пишущей в эти каналы.
Итак -- вот оно, наконец-то потребовалось :)
05.12.2019@лыжи-~14:00...14:30: а тут, кстати, встаёт новый вопрос: раз у нас теперь не предполагается никакой "удалённой консоли", то как администрировать блокировки? Если надо удалить сделанную программой блокировку, то как?
И связанный вопрос: а КЛИЕНТА-то как грохнуть? В v2 была команда "close", а тут?
@3-й километр: а если и клиентов представлять "каналами"? Чтобы можно было управлять ими аналогично устройствам: например, сказать "_clients.SOME_ID=-1", и это бы его закрыло. Вопрос только, как менеджить этот дополнительный набор "каналов" -- ведь список клиентов ОЧЕНЬ динамический...
@ИЯФ-пультовая-~15:15: кстати -- у нас ведь выделением идентификаторов клиентов занимается cxsd_hw, так что оно frontend'о-независимо. ...но оттого не сильно-то легче... И, опять же, централизованного РЕПОЗИТАРИЯ клиентов (какой на каком frontend'е) -- нету.
И еще это как-то может взаимодействовать с "цветами" каналов и фазами...
18.03.2016: обсуждать тут пока не очень понятно, что. Ниже просто сборище мыслей на тему.
Тогда "resolve" де-факто превращается в "open()", а возвращает он как бы "дескриптор в таблице дескрипторов процесса" (тот самый список юзаемых каналов), а уж в ячейках этой таблицы хранятся сами gcid -- тем самым получается некоторая аналогия с работой файлов в *nix, где 2 уровня таблиц дескрипторов.
09.05.2020: поскольку делалось всё совместно, да и переплетено сильно, то здесь тоже всё запротоколировано совместно, без разделения по cda, cxsd_hw и размышлениям.
20.04.2020: размышления на тему локинга. И мысль: а не нужно ли при попытке залочить уже залоченный канал попробовать "пнуть" владельца лока? По аналогии с remdrv - если то соединение уже отвалилось (например, по питанию), а новолочащий - как раз реинкарнация "того". Соответственно, тот, кто накладывает лок, должен пытаться делать это 2-3 раза с паузой в несколько секунд.
21.04.2020: продолжение размышлений:
После размышления начал пилить локинг.
CxsdHwLockChannels()
-
плюс в cda_d_insrv: там всё локально, без необходимости гонять по сети и
следить за дисконнектами/реконнектами. Так что можно на простом варианте
обкатать/потестировать.
CxsdHwLockChannels()
- он простой, работает в
2 прохода:
CxsdHwLockChannels()
при ошибке
возвращает в errno "объяснение" для неё. Это единственный такой вызов в
cxsd_hw (кроме сервисных, вроде добавления/удаления evproc'ев).
Идеологические непонятки с тем, как всё-таки реализовывать локинг всё же меня не оставляют, поэтому было принято решение опросить целевую аудиторию в лице ЕманоФеди.
22.04.2020: а, нет -- есть! Надо было не гуглить, а
смотреть в документации. Так вот, в tango_92.pdf есть раздел "4.9 Device
locking". Итого: там лочится прямо всё устройство целиком, через методы
DeviceProxy -- lock()
и unlock()
. А вот снятие
блокировки crash'нутого клиента делается не по дисконнекту, а через
указанный интервал времени.
22.04.20@утро, ~09:00: насчёт lockset'ов: а зачем делать какой-то менеджмент, SLOTARRAY etc?
22.04.2020: да, делаем.
cda_dat_p_lock_op_f
параметр
lock_id
в lockset_id
.
cda_lock_chans()
добавлен параметр
lockset_id
, чьё значение и передаётся дальше в
do_lock()
.
Кстати, чисто идеологическое: танговский подход к локингу устройств целиком может иногда быть даже удобнее. Так вот: в принципе, при желании можно такое же поведение обеспечить -- лоча всё устройство через канал _devstate; выглядит вполне разумно и непротиворечиво. И реализуется довольно просто -- всего лишь проверка "залочено ли кем-то" в случае с каналом _devstate будет расширяться в проверку всех "основных" (не-is_internal) каналов устройства.
После обеда: да, сделал. Довольно несложно, хотя и очень громоздко и некрасиво.
Кстати, встаёт ведь вопрос "отладки/диагностики": как можно узнать, кем
залочен конкретный канал? Ведь никаких средств прочитать произвольное поле
структуры cxsd_hw_chan_t
N'го канала у нас нет. И тут приходит
в голову давняя идея (уж и не помню, когда её придумал): иметь
"сервис-информатор" в виде пары каналов -- в один пишется номер (gcid)
интересующего канала, а в другом появляется ответ. (Похожий принцип
использовался в dds500_drv.c для чтения регистров блока.) При
надобности более сложных "команд", где может понадобиться несколько
"параметров" -- вроде принудительного снятия блокировки
-- можно использовать векторные каналы, в ячейках которых размещать
параметры.
03.02.2021: неа, "в один пишется номер (gcid) интересующего канала, а в другом появляется ответ" -- не очень хорошо: отсутствует атомарность, поскольку при получении значения "ответа" нет уверенности, что это ответ на "то". Для РУЧНОЙ работы -- да, годится; но для чего-то автоматизированного -- нет. Нужно получать ДВА числа: (gcid,locker_id). Можно возвращать канал из ДВУХ int'ов -- первым адрес, вторым значение. А для ручного интерфейса -- оставить и скалярный канал "ответ locker_id".
03.02.2021: с другой стороны, если сразу по записи в "канал gcid" возвращать его значение как канала, а потом тут же возвращать значение "locker_id", то в момент получения оного значения в cda'шном канале "gcid" будет именно адрес канала, чей locker.
22.04.2020: продолжаем -- приступаем к
cda_d_insrv_lock_op()
. И тут становится ясно, что не всё так
просто -- есть подводные камни, о которых раньше не думалось:
Тогда нужно там хранить массив-список всех лоченных в данный момент hwr'ов, плюс как-то его поддерживать в актуальном состоянии (держать всегда сортированным?).
@душ-после-завтрака: ещё соображения:
Пара замечаний:
Впрочем, это сценарий хоть и возможный, но крайне экзотичный -- можно забить.
Ура-ура -- идея вполне рабочая, надо реализовывать.
cxsd_hw_do_cleanup()
.
Причём, с учётом сделанного вчера "лоченья устройств целиком посредством _devstate" -- лучше цикл гнать сверху вниз.
23.04.2020@утро-душ:
теоретически можно б было воспользоваться прямо переданным массивом
hwrs[]
-- поподменяв в нём hwr'ы на gcid'ы, ведь всё равно это
всё int
'ы. Но это очень плохая идея -- радикально нарушаются
правила инкапсуляции/изоляции. Так что не будем.
...кстати,
RlsLcnSlot()
НЕ вызывается
cxsd_hw_do_cleanup()
, а модуль полагается на то, что cda_core
при удалении соединения вызывает удаление всех каналов поштучно, так что в
UnRegisterInsrvHwr()
просто делается индивидуальный
CxsdHwDelChanEvproc()
.
TerminDev()
'а, но нем не менее (кстати, посмотрел -- да, там
очень тщательно выбран порядок вызова cleanuper'ов, так что cda'шный будет
вызван первым).
Чуть позже, по результатам идеи про "в момент разрегистрации hwr'а проверять, не залочен ли он данным uniq":
...впрочем, анализ кода показал, что cda_d_insrv и cxsd_fe_* и так невозбранно лазят в cxsd_hw_channels[] как к себе домой. Так что глубокого смысла и нет.
-1
было бы идеальным выделенным кодом
для ошибки.
А вот это будет в любом случае небесполезно. Несрочно, но лучше б сделать.
Итого -- делаем в cda_d_insrv.c:
hwr_arr_buf[]
, который
переименован в gcn_arr_buf[]
/
cda_d_insrv_lock_op()
: рост до
нужного объёма, затем наполнение (совмещённое с предварительной проверкой
валидности hwr'а) и в конце сбагривание в CxsdHwLockChannels().
UnRegisterInsrvHwr()
.
ПОЗЖЕ.
И снятие блокировок в cxsd_hw_do_cleanup()
тоже сделано --
циклом сверху вниз.
А вот дальше надо заниматься уже веткой cda_d_cx<-cxlib<-протокол->cxsd_fe_cx; но там пока слабопонятно, как именно делать -- там ведь "отложенный" локинг (когда разрезолвится), плюс надо повторять при реконнектах (а как? как с группами быть?). Поэтому временно отвлечёмся и чуток переделаем диаграмму работы резолвинга (см. от 29-10-2019) -- это как раз близкая тема; заодно и освежим в памяти функционирование cda_d_cx -- глядишь, снизойдёт озарение.
25.04.2020@вечер: возвращаемся к реализации локинга, после пары дней возни с поддержкой "модификации БД на лету" (отвлекался туда, чтобы лучше погрузиться в функционирование cda_d_cx; погрузился).
Консенсус внутри меня таков: раз нет чёткого понимания, как можно всё сделать, то будем делать ВСЁ и как получится ("если не знаете, что делать, то делайте хоть что-нибудь"), а потом посмотрим, что выйдет. Это "ВСЁ" включает в себя:
cda_lock_chans()
(для восстановления локов после реконнектов) -- также не вполне ясно. И
ведь РАЗблокировка должна из него удалять.
Видимо, достаточным (на сейчас?) решением будет просто помечать каналы так же, как и по OPT_EXCLUSIVE -- тогда всё получится автоматом.
Для чего ей придётся хранить у себя текущее состояние.
refinfo_t
состояние.
27.08.2020: ага, вот только этот вызов как-то был забыт -- прототип в cda.h имелся, а реализация отсутствовала, на что Федя натолкнулся. Добавлен.
26.04.2020: приступаем.
CDA_DATAREF_OPT_EXCLUSIVE
создан.
cvt2ref()
), а в
cdaclient.c -- реакция на события LOCKSTAT по ключу
-DL
.
refinfo_t
добавлено lockstat
.
cda_dat_p_report_lockstat()
проверяет, что если
новое состояние не отличается от текущего, то ничего не делает. А если
отличается -- то сохраняет его и генерит событие CDA_REF_R_LOCKSTAT.
27.04.2020: заменена на
cda_dat_p_report_dataset_lockstat()
.
27.04.2020: продолжаем.
cda_dat_p_report_lockstat()
, отдающей
статус ОДНОГО канала, изготовлена
cda_dat_p_report_dataset_lockstat()
, "групповая".
Смысл -- для lock_op()
'ов, когда меняется состояние сразу
группы. Можно б было отдавать его оттуда в цикле, но это и неудобно, и
нехорошо с точки зрения возможности, что где-то изнутри вызова evproc'а
будет вызван cda_del_context()
: cda_core-то имеет доступ к
being_destroyed, а dat-плагины -- нет.
Вечером: а ведь из-за того, что репортинг может вызываться
-- в случае insrv -- прямо изнутри cda_lock_chans()
, и в нём
таки ПРИДЁТСЯ делать цикл (flush-style по одинаковым статусам), то надо
окружать "скобками being_processed" вызов lock_op()
'а.
_new_chan()
добавлена обработка
CDA_DATAREF_OPT_EXCLUSIVE
: сразу же делает локинг и затем
отдаёт состояние.
lock_op()
после локинга отдаёт lockstat затронутых
каналов.
Пара замечаний по реализации:
CxsdHwLockChannels()
: предположение может не соответствовать
реальному из-за совпадающих gcid'ов разных каналов (по причине aliasing'а
через cpoint'ы, полнодевайсового локинга через _devstate, да и мало ли из-за
чего ещё).
cda_dat_p_report_dataset_lockstat()
указывается ОДИН статус, а у обрабатываемого набора каналов результирующие
статусы теоретически могут получиться и разными (хотя когда бы? :-)), то
делается поиск-группировка каналов с одинаковым статусом и эти группы
репортятся вместе -- аналогично тому, как делается группировка в
CxsdHwDoIO()
.
ЗАМЕЧАНИЕ: далее в cxsd_fe_cx.c надо будет делать по образу и подобию.
28.04.2020: пора переходить к ветке cx -- cda_d_cx+cxlib+cxsd_fe_cx.
CXC_LOCK_OP = CXC_REQ_CMD('C', 'l', 'o')
.
(А на будущее, с "через handle" -- скорее всего, возьмём 'lk' вместо 'lo'.)
CxV4LockOpChunk
, для использования И в запросе, И в
отклике: "туда" уходит поле operation
, "оттуда" возвращается
lockstat_result
.
CAR_LOCKSTAT
...
cx_rq_l_o()
("l_o"
-- Lock Op).
В отличие от прочих (мониторы, чтение/запись), он ОДНОКАНАЛЬНЫЙ, а не многоканальный. Резон простой: как показал опыт, для единственного юзера -- cda_d_cx -- требуются именно одноканальные операции; многоканальные же использовались в v2 и сделаны лишь по старой памяти.
hwrinfo_t
добавлено поле rq_lock
.
cx_setmon()
, также
добавлен условный вызов cx_rq_l_o()
.
MarkAllAsDefunct()
ВСЕМ каналам делается отдача lockstat=0.
Именно ВСЕМ, а не только имеющим rq_lock!=0, и вот почему: если в какой-то момент программа снимает блокировку, эта команда отправляется серверу, тут происходит обрыв соединения -- то подтверждение сброса блокировки так и не придёт, и у cda_core навечно останется lockstat=1.
А так -- просто гарантированно всем каналам говорим, что блокировка потеряна (сервер-то это обеспечит).
cda_d_cx_del_chan()
вместе с
cx_delmon()
-- ПЕРЕД ним! -- делается вызов
cx_rq_l_o(СБРОС_ЛОКА)
.
cda_d_cx_lock_op()
: если
состояние sid'а НЕ READY, то не делает ничего. Иначе идёт в цикле по
переданному набору и для каждого hwr'а,
Тогда реализация была сделана с кучей косяков:
cx_begin()
.
hwrinfo_t
запрошенного режима
залоченности;
Последнее-то делается только при (a) готовности соединения --
CDA_DAT_P_OPERATING
, и (b) готовности канала --
RSLV_STATE_DONE
.
В результате, если хоть одно из этих условий не выполнялось, то и желаемый режим "эксклюзивности" не запоминался -- при не-(a) для ВСЕХ каналов в списке, а при не-(b) для конкретного.
Т.е., оно в принципе работать не могло, да и не тестировалось
(тестировался лишь вариант CDA_DATAREF_OPT_EXCLUSIVE
).
Исправляем:
cx_begin()
.
Протестировать бы теперь именно ДИНАМИЧЕСКИЙ локинг...
21.06.2020: сделан тест -- work/tests/test_cda_lock_chans.c.
22.06.2020: потестировал -- похоже, работает.
Но в порядке проекта -- если захочется сделать, то можно заменить поле
rq_lock
на lockset_id
, в которое при отсутствии
запроса блокировки записывать -1
("не надо"), по OPT_EXCLUSIVE
-- 0
, а при явном указании -- что указано.
ServeIORequest()
просто вычитывается информация
запроса, вызывается локинг, а потом клиенту отправляется текущее состояние
этой блокировки.
PutLkStChunkReply()
Потому, что чтобы найти монитор, которому нужно прописать это состояние
-- надо знать его "cond
". А у нас нет никакой возможности
угадать значение этого поля.
Это, кстати, и есть одна из причин, почему НАДО переходить в протоколе с "прямой адресации по gcid" на "через handle".
do_lock
имеется (из CxsdHwLockChannels()
скопировано).
Будем полагаться на то, что блокировку снимает
cda_d_cx_del_chan()
(ну либо закрытие соединения).
cpid = monr->cpid;
-- вместо надлежащего конвертирования:
cpid = host_i32(cp, monr->cpid);
Когда всё с одинаковым endianness -- проблем не будет, но в общем случае это косяк.
Осмысление содеянного:
(Чтение -- вешается на сервер, мониторирующий каналы и присылающий данные по мере получения; запись -- вместо синхронной модели используется асинхронная, "отправляем ЗАПРОС на запись", а там уж как получится (обычно доходит, но может и потеряться).)
А теперь пара мыслей, пришедших по результатам реализации (точнее, даже ДО -- ещё утром, при визуальном анализе кода:
cx_run()
почему-либо обломится (например,
соединение закрыто с той стороны и отправка даст ошибку), то прямо изнутри
cx_run()
'а будет вызван MarkAsClosed()
и далее
ProcessCxlibEvent()
, в котором cxlib-соединение будет закрыто.
cx_run()
'а НЕ
проверяется.
return
какого-нибудь "success") -- то
проблема: к моменту этих действий реальное состояние уже изменится и они
только всё испортят.
SuccesProc()
и
ProcessCxlibEvent()
.
18.05.2020: поставлены проверки результата
cx_run()
в этих 2 точках.
...надо выше в comment2 написать, что не требовалось бы никакой "гарантии неповторяющести hwid".
Хотя можно сделать проще и быстрее: ведь этот аспект касается ТОЛЬКО протокола и почти не торчит, так что можно параллельно добавить второй вариант реализации "мониторы как каналы", а потом перейти на это как на основной способ работы. Протокол при этом можно проапгрейдить до 4.1; только надо явно смотреть, как где сравнивается на тему VERSION_IS_COMPATIBLE.
Мелкая идея по будущей реализации: надо будет в сетевом вызове "создай монитор для канала с таким-то именем" сделать параметр "создай, даже если канал не найден". Смысл -- чтобы cda могла так указывать каналы с RSLV_TYPE_NAME, про которые точно известно, что они именно к этому серверу относятся; а каналам RSLV_TYPE_GLBL будет указываться 0, чтобы отсутствие такого имени в сервере возвращало бы канал в состояние RSLV_STATE_UNKNOWN.
01.05.2020: начинаем тестировать -- путём запуска сервера с единственным каналом, на который натравливается один cdaclient с флагом "@!:", а потом вторым пытаемся значение менять.
Косяк был в cda_add_chan()
, при передаче
new_chan()
'у срезавшем все флаги, кроме
CDA_DATAREF_OPT_ON_UPDATE
. OK --
CDA_DATAREF_OPT_EXCLUSIVE
добавлен в маску оставляемых.
ConsiderRequest()
-- чтоб
печатало, когда кто-то пытается писать в залоченный канал.
Озадачился: попыток ДВЕ подряд.
А потом дошло: это не попыток 2, а Consider вызывается 2 раза подряд -- первый модельный, а второй при упаковке каналов в группу по одинаковым devid и action.
Разобрался: в cda_dat_p_report_dataset_lockstat()
вместо
R_LOCKSTAT рассылалось R_STATCHG (копировал из
cda_dat_p_defunct_dataset()
и забыл сменить код).
Исправил, проверяем: да, сообщение доходит.
Сходу причина не найдена, да и поздно уже -- завтра...
После исправления-то оно заработало, но есть неясности:
Остаётся только предполагать, что удалил её случайно, при каком-нибудь копировании куска текста (сделал Ctrl+K,Ctrl+M вместо Ctrl+K,Ctrl+C) -- всё-таки дистанционная работа из дома на x10sae имеет изрядную латентность и работаешь как в густом желе; возможно, косякнул.
Я проверил "backup'ы" несколькодневной давности -- уже тогда косяк был. Т.е., проверка проводилась с ним.
А каким образом при этом могло всё работать корректно -- неясно.
А-а-а, дошло!!! Та проверка проводилась просто попыткой записи в залоченный канал -- эта запись и обламывалась. А вечерняя проверка доставки событий LOCKSTAT проводилась уже с префиксом "@!:" у ОБОИХ cdaclient'ов -- и у тестирующего-пишущего тоже, вот он и "крал" лок у первого, поэтому и запись от него отрабатывалась.
У-ф-ф, отлегло... :)
Итого -- получается, что вроде как работает? Квест почти двухнедельной длительности завершён?
Ну-у-у, почти:
Ни то, ни другое в текущей модели реализовать невозможно: ни у cxsd_hw нет механизма вызова владельца, ни клиент не делает повторных попыток локинга (и последнее уже вообще неясно как исправить: это РАНЬШЕ (при задумывании с групповыми локами) предполагались какие-то и локи многих сразу, и повторы, а теперь -- осталась простая асинхронная модель).
01.05.2020: некоторые размышления на будущее: нам ведь понадобится иметь возможность запроса информации для отладки/разбирательства -- например, кто залочил такой-то канал? Так вот:
А уж при переходе на модель "через handle" реализовать это станет крайне просто.
И подобных запросов в протокол можно насовать сколько угодно -- точнее, один запрос "IOCTL", в котором куча кодов-подзапросов.
Можно добавить функционал прямо в cdaclient:
num2read
и
num2write
, ввести 3-й счётчик -- num2ioctl
(плюс
всю сопутствующую "инфраструктуру", с ожиданием какой-нибудь готовности канала,
чтоб можно было вызвать "cda_ioctl_chan()".
cd
.
02.05.2020: сделано, выдаёт.
04.05.2020: в cx.h были добавлены
enum-константы CX_LOCK_WRITE_SET
=RW|ALLORNOTHING и
CX_LOCK_WRITE_RLS
=ALLORNOTHING, чтобы можно было их
использовать в клиентах, не заморачиваясь низкоуровневыми масками.
Мысль сделать их пришла при написании "документации" locking.ru.html -- понял, что не смогу там внятно и кратко объяснить причину появления конкретно таких "заклинаний".
Ну и, кстати, сама эта "документация" написана -- три сотни строк (включая пустые).
Сегодня во время очередной переписки с ЕманоФедей появилась мысль, как сие можно исправить.
Будем обсуждать в отдельном подразделе.
01.09.2020: собственно та переписка -- мой ответ на Федино письмо:
On Mon, 31 Aug 2020, Fedor Emanov wrote: > Пересобрал, проверил, теперь если до запуска основного цикла запросить > блокировку, то она срабатывает. > Далее запускаю второй экземпляр программы, он разумеется так же до > старта основного цикла запрашивает > блокировку. Теперь убиваем первый экземпляр. Казалось бы второй уже > запросил блокировку и должен бы её > наконец-то получить? но в событиях мне этого не приходит. Это опять > баг? просто у тебя в описании сказано Это дизайн такой. > "запросил блокировку.... дождался сообщения о том что она получена", а > тут выходит что мои запросы выкидываются Они не выкидываются, а исполняются лишь в момент собственно запроса либо при коннекте. А вот постоянных повторов -- НЕ делается, поскольку совершенно неясно, по какому принципу их делать (периодиечски -- плохо, а события "владелец блокировки её снял" на уровне сервера нету, да и непонятно, КОМУ их отправлять). > и я периодически должен запрашивать заново? Да, периодически спрашивать заново.
Собственно, из написанного -- второго моего абзаца, самого большого -- можно "от противного" вывести проект реализации.
CxsdHwLockChannels()
--
тривиально: просто ещё один цикл, "Stage 3", идентичный "Stage 2".
А код события можно и унифицировать с cda, сделав
CXSD_HW_CHAN_R_LOCKSTAT
тоже =12 (ну да, будет "дырка" 7...11;
но 7,8,9 и в cda не заняты (и хбз, почему), а 10 и 11 -- RSLVSTAT и CURVAL,
переживём).
Нашёл причину "дырки" в кодах события -- см. за 25-08-2016: условное разделение между cxsd_hw ("первичные") и cda ("дополнительные" события, в сервере как бы не существующие; но вот LOCKSTAT, попавший во 2-ю группу, теперь и в сервере становится осмыслен...).
А не было ли бы правильнее эту задачу решать самому cxsd_hw? Т.е., чтобы "желающий залочить" добавлялся бы в списох "хотетелей залочить", а при освобождении лока смотреть бы в этот список и выбирать там "достойного".
Тут плюс будет в том, что обычно-то "достойным" можно считать первого в списке (т.е., просто следующего за прошлым локером), но можно и ввести некие "приоритеты".
@через час, вернувшись домой: yу и дополнительный бонус -- как-то можно будет учитывать lockset'ы и "ALLORNOTHING", которые сейчас вообще никак. А всё потому, что при перекладывании работы с "клиентов" на cxsd_hw у него можно будет иметь более полную картину (в отличие от клиентов, у которых в моменте evproc'а знания ограничены одним этим каналом).
Получается -- потребуется API для смены маски в уже зарегистрированном evproc'е (чтобыменять наличие битика LOCKSTAT в маске событий по мере надобности)? Ну-у-у, сделать то это можно -- технически проблема невеликая. Но как-то мне это не нравится...
@~10:40, пешком к родителям, Коптюга
после выхода из леса: но тут будет проблема с самим поиском мониторов
в
@~11:00, пешком от родителей домой,
вдоль Терешковой после "девятиэтажки": а может -- ну его нафиг, это
дрыганье-битика-на-ходу? Просто ВСЕГДА заказывать событие
LOCKSTAT, а уж в обработчике смотреть, нужно ли оно нам: если в мониторе
битик С придуманным подходом -- попытка захватить локи прямо в evproc'ах по
отпусканию -- ничего подобного не получится, поскольку в момент evproc'а не
будет никакой информации об АТОМАРНОЙ операции "блокировка ГРУППЫ каналов".
CxsdHwAddChanEvproc()
/CxsdHwDelChanEvproc()
:
ведь они ищут пункты с ТОЧНО ТАКОЙ ЖЕ evmask, как указана. А при
"модификации по мере надобности битика в маске запрошенных событий" сам
cxsd_fe_cx.c будет путаться в том, что он должен указывать. (@через час, уже опять дома:
можно, конечно, учитывать значение бита MONMODE_RQ_LOCK
, но это
всё равно хренововато...)
MONMODE_RQ_LOCK
взведён, то пытаться перехватить лок, а
если нет -- то ничего не делать. Тем более, что «событие
"захват/потеря лока" -- крайне редкое, так что тут на производительность
можно внимания не обращать».
В принципе -- вполне понятно, как делать; но вот ДЕЛАТЬ ли -- некий вопрос. Лучше взять паузу, пусть это всё в голове получше устаканится -- глядишь, ещё что придумается.
Ох, чревато это -- хрен отладишь.
И вообще, надо иметь 2 типа запрета изменений:
18.03.2016: а ведь "цвета"+фазы должны бы, видимо, как-то взаимодействовать с "access control". Как?...
Конкретно см. bigfile-0001.html за 09-02-2011 и 08-02-2014.
30.09.2014: мыслей, собственно, пока 2:
17.04.2015@Снежинка-утро-зарядка: (мысли, навеянные вчерашними размышлениями о clientside-режимах, в частности, вопросом, в каком порядке что записывать):
ReturnDataSet()
проверять, рядом с
TryWrNext()
?)
16.01.2016@утро-душ: пара мыслей:
УСТРОЙСТВО.КАНАЛ ЗНАЧЕНИЕ TIMESTAMP RFLAGS(последняя пара полей -- чисто для информации, ровно как и в cdaclient/dataset-save).
17.01.2016: две тонкости:
Правильнее это делать extension'ом.
Видимо, надо ввести в config-файле возможность указывать ext'у параметры -- строку, как auxinfo для драйверов.
15.06.2020@пробежка по квартире, ~14:20: некоторые идеи на тему автосохранения -- КОГДА его выполнять.
Но проблема в том, что завершение сервера выполняется по СИГНАЛУ -- т.е., "асинхронно", и уж из обработчика сигнала точно НЕЛЬЗЯ обращаться к данным, т.к. они могут быть в произвольном "некогерентном" состоянии.
sl_break()
.
sl_main_loop()
'а.
Но, на случай, если именно сохранение зависнет -- нужно, чтобы ТРЕТИЙ сигнал завершения прерывал бы и сохранение тоже.
...а можно просто при начале сохранения сбрасывать флаг "завершаемся!", и тогда всё получится само собой: второй сигнал взведёт флаг обратно, а третий уже приведёт к завершению (поскольку флаг взведён).
То, что в v2 делалось большими каналами, а тут предполагалось реализовывать привязками базовый/настроечный.
08.09.2014: краткая история вопроса:
При неподдержке драйвером эмулировать -- производить запись настроечных каналов, а потом чтение (или тоже запись) базовых.
Поэтому назрела мысль вообще отказаться от указания отношений базовый/настроечный в КОНФИГУРАЦИИ, а оставить их только в протоколе (в широком смысле -- на всех этапах путешествия запроса между клиентом и драйвером), см. за 12-04-2014.
Тогда
Эта схема выглядит красивой, но надо понимать, что она подходит именно для текущих аппаратных задач, вроде fastadc. В каком-то более хитром случае может потребоваться более общее решение, вроде упомянутого в (1).
Вот такова ситуация на текущий момент. И надо придумывать идеальное решение.
17.09.2014: собственно, наиболее правильным выглядит решение (2) -- каналам представления указывать их реальный источник, а настроечным -- его же как базовый.
Теоретическое возражение: а что делать, если настроечный канал реально настроечный для МНОГИХ АППАРАТНЫХ?
Ответ: оное у нас встречается (пока?) только в АЦП, и тут два случая:
Проблема "один настроечный для многих" исчезает.
Отдельный случай -- осциллографические измерения. Но там вполне можно поступить как в предыдущем пункте -- делать раздельные настроечные (хотя хбз... чисто идеологически неясно, как вообще лучше обращаться с этим режимом у этих АЦП....).
28.01.2015: больше часа проговорили с Роговским на тему "как вообще идеологически организовывать работу с «большими каналами» -- для решения НЫНЕШНИХ задач, текуще в v2 и перспективно в v4". Результаты обсуждения, применимые к v4:
Т.е., в терминах v2'шного pzframe управляющие параметры связаны с nxt_args[], а описывающие-текущее с cur_args[].
Это как бы немножко "некрасиво" -- смахивает на гусино-epics'ное разделение на {value,readback_value}, но в данном случае технологически необходимо, поскольку "readback_value" в используется не само по себе, а совместно с другим полем (массив), и должно быть с ним жестко скоординировано.
А главное -- такая унификация БЕССМЫСЛЕННА: ведь в v4 адресация по именам и есть резолвинг, так что незачем иметь в 1-канальном АЦП имена вроде "line3data", отдающие CXRF_UNSUPPORTED -- достаточно просто не иметь таких имён.
Вывод для "бихевиористских" параметров, унифицированных у всех pzframe-подобных устройств, и необходимых для v4'шного варианта pzframe -- SHOT, ISTART, WAITTIME, STOP, ELAPSED: их делать
pzframe_drv_t
и -1 при отсутствии);
Первый вариант мне нравится больше (и v2'шный код подойдёт один-в-один).
...а в тех случаях, когда физически требуется мерять один сигнал разными способами (например, 220V -- отдельно смотреть синусоиду и пульсации), то надо ставить разветвитель и ДВА измерителя.
Следовательно, надо б еще раз поговорить с Карнаевым для прояснения ситуации.
29.01.2015: собственно, а что не так с моделью в v2, что для v4 хочется сделать другую модель:
Сия проблема в pzframe-девайсах обходится aliasing'ом параметров на скалярные каналы, но уже это само является извращением и реализуется как раз в клиентах и драйверах, вместо сервера.
Эта проблема идеологическая, и определяется, во-первых, неудобной схемой работы с параметрами (см. предыдущий пункт), а во-вторых, ориентированностью на "отдельность" большого канала -- он существует как бы сам, изолированно, отдельно от прочих больших каналов (это даже в протокол пролезло -- 1 bigc на соединение); а вырезки потребовали бы общих параметров для нескольких связанных bigc.
30.01.2015@утро: следствие: при такой модели общая "libpzframe_drv" должна будет совершенно иначе обращаться с параметрами. Ибо:
Следовательно -- и обработка параметров в "pzframe_drv_rw_p()" становится нетривиальной, и копирование nxt->cur; да и возврат данных в "ReturnMeasurements()" тоже (оное, похоже, вообще надо сделать pzframe_drv-методом драйвера).
03.02.2015: потихоньку запиливаем, на примере adc200me_drv.c плюс cpci_fastadc_common.h (почти пустой) и пока локального pzframe_drv.c. Результаты и размышления на текущий момент:
pzframe_drv_rw_p()
обрабатывает
лишь стандартные бихевиористские параметры (причём значения -- у ISTART
и WAITTIME) складирует в свои переменные, а не в массив параметров.
_rw_p()
,
которая
pzframe_drv_req_mes()
(стартующий измерения).
Этими флагами достигается адаптивность -- наверх венрутся только те базовые/отражения/вырезки, что были запрошены.
pzframe_drv_rw_p()
для всего
остального.
ReturnMeasurements()
пока вытащена тоже прямо в
драйвер -- для реализации там:
Это она делает "интеллектуально", составляя списки для
ReturnDataSet()
'а, при надобности подсовывая в начало сами
базовые, а потом безусловно параметры по списку.
ReadMeasurements()
, подготавливая все данные в готовом для
Return* виде.
Да, монструозненько выглядит параметризация (уже похоже на pzframe_data)
Сейчас отладимся локально, а потом формализуем это в pzframe_drv.
26.02.2015: за вчера-сегодня очень
предварительная версия adc200me_drv+pzframe_drv сваяна. Собирается, но
работать пока не будет: и с параметрами работа недопилена, и
архитектура ReadMeasurements()
пока никак.
27.02.2015@утро: а теперь допилена вроде должная работать версия.
ReturnDataSet()
'а.
Функция указывается init'у в параметре prepare_retbufs
.
pzframe_drv_t.retbufs
, имеющее
специальный тип pzframe_retbufs_t
.
Надо проверять.
03.03.2015: за позвачера-вчера-сегодня потроха adc200me_drv.c сделаны.
pzframe_drv_req_mes()
.
pzframe_drv_rw_p()
.
ValidateParam()
и складировать в nxt_args[].
Но такая параметризация будет настолько громоздкой, что выглядит не стоящей выделки. Пока будем тестировать на adc200me, а в прочие драйверы просто копировать код -- благо, модификаций по "индивидуальности" там немного.
На первый взгляд -- вроде работает.
06.03.2015: имевшиеся баги пофиксены, и подход продвинут и расширен:
chtype
) -- имеет тот же смысл, что
раньше;
refchn
) -- чтобы при возврате в
PrepareRetbufs()
знать, с какого управляющего канала
копировать значение CUR-канала (то, что раньше указывалось во
внутренней PrepareRetbufs()
'овской таблице).
Это поле используется ТОЛЬКО для STATUS-каналов.
ValidateParam()
обязана обрабатывать
только VALIDATE-параметры, остальные ей теперь никогда не попадут.
SetChanReturnType(,,,1)
.
PrepareRetbufs()
. Причём:
...в обоих случаях делается копирование nxt_args[x]=cur_args[x] -- для полной корректности (чтобы даже при отсутствии флага is_autoupdated всё бы работало как надо; может пригодиться для удалённых драйверов, когда инициализация асинхронна и запросы могут быть уже "в полёте" еще до её завершения).
10.03.2015: конкретно adc200me_drv.c вроде допилен, добавлены каналы:
Уставляются при инициализации.
Считаются в ReadMeasurements()
, нулятся в
AbortMeasurements()
, возвращаются (при запрошенности!) в
PrepareRetbufs()
.
...тут, конечно, есть некоторая сложность с интегралами: в 12-битном осциллографе с 2^20 сэмплами сумма не влезла бы в 32 бита даже в исходных "сырых" кодах, а уж в микровольтах и подавно. Поэтому она считается в 64-битной переменной (12бит+10бит"uV"+20бит_адрес -- 42 бита, явно должно влезть). Хотя при сохранении в 32-битный канал INTn оно и обрежется...
11.03.2015: замечание общего характера насчёт памяти и размеров:
Но если вдруг понадобится -- то алгоритм действий вот он тут.
12.03.2015: сделан второй драйвер -- adc812me_drv.c, по образу и подобию 200-го, и, видимо, самый монструозный из всех (за вычетом 333-го с возможностью отключения линий).
Резюме:
13.03.2015: мелкое дополнение: теперь pzframe_drv
значение ELAPSED при НЕзапущенности измерений отдаёт -1
.
26.06.2015: pzframe_drv перетащена в основное дерево 4cx/, и в hw4cx/x-build/ она теперь тоже собирается.
15.07.2015: продолжаем работы, теперь уже с CAMAC'овскими.
CHTYPE_*
и тип chinfo_t
перенесены в pzframe_drv.h под именами
PZFRAME_CHTYPE_*
и pzframe_chinfo_t
соответственно. Ибо нефиг дублировать совсем идентичные определения.
16.07.2015: вроде сделан (компилируется) следующий драйвер -- c061621_drv.c. А нафига, спрашивается?
02.06.2016: возобновляем работу над драйверами.
10.06.2016: работа над собственно драйверами:
adc4_rw_p()
и
adc333_rw_p()
, хочется всё-таки унести этот код в
camac_fastadc_common.h. Только надо будет как-то параметризовать
кусок на тему PZFRAME_CHTYPE_BIGC
, чтобы:
Похоже -- тупо ввести enum-определения/параметры PARAM_DATA и PARAM_LINE_BASE. Первый будет у всех, а второй в случае c061621 как бы неважен -- не-DATA CHTYPE_BIGC никогда не встретится -- и может ставиться =-1.
Основывать можно на adc4, но не забыть CHTYPE_INDIVIDUAL (он в CAMAC'овских пока только на чтение в c061621, а на запись не используется).
11.06.2016@дома-вечер:
camac_fastadc_common: да, сделана общая _RW_P. Параметры названы
PARAM_DATA
и PARAM_LINE0
.
17.06.2016:
13.07.2017: увы, что-то там не слава богу (давно замечено): как-то не так работает сдвиг нуля.
18.04.2022: да нет -- проверил ещё раз, и вроде всё работает верно. Но странности в работе драйвера всё равно есть, так что дальнейшее обсуждение уже в его разделе начиная с сегодня.
13.07.2017: цикл "сделаем
IS_AUTOUPDATED_TRUSTED
всем каналам типа _AUTOUPDATED и _STATUS
" перенесён из всехних InitParams()
в общую
camac_fastadc_common.h::FASTADC_INIT_D()
.
Аналогично и с cPCI'ными -- перенесён в
cpci_fastadc_common.h::FASTADC_INIT_D()
.
(Всё проделано при внедрении аналогичной схемы в будущий vme_fastadc_common.h.)
23.03.2018: Роговский попросил, чтоб у CAMAC'овских осциллографов была возможность указывать в auxinfo параметры (ptsofs, numpts, ...) -- как это было в v2.
На вид вроде бы в cPCI'ном adc812me_drv.c оно есть, а архитектура у него очень похожая -- чё б не внедрить и в CAMAC'овские (копировать реально из v2'шных, убирая "pz." и меняя "PARAM" на "CHAN".
27.03.2018: немножко анализа:
-1
, что не является приемлемыми значениями, то
нужно что-то чуть хитрее -- как-то эти умолчания должны убираться.
Init1Param()
,
уставляющей указанное ей в вызове значение, только если текущее в
nxt_args[n]<0
.
Таким образом, для КАЖДОЙ строчки в PSP-таблице _params[]
должна б быть строчка в коде, "заботящаяся" о замене -1
на
надлежащее.
_params[]
и указать умолчательные значения, а более нигде в
коде ничего присваивать и не нужно!
Сначала была мысля, что в adc200me/adc812me оно так потому, что там параметры могут считываться из памяти устройства сохранённые. Но потом стало ясно, что это просто лёгкий изврат -- поскольку указываемые в auxinfo значения НЕ имеют приоритета перед сохранёнными, то такая хитрость и незачем. Почему именно так сделано -- хбз, а запись об "Init1Param" есть в 201403-SNEZHINSK-ACTIVITY.txt 31-03-2014.
И как правильнее поступать, у чего приоритет -- у считанных из железа или у указанных в параметрах -- однозначного ответа нет.
Это второе иногда можно пытаться разрулить чтением из железа параметров, которые осмысленно не могут быть нулями -- например, число точек для измерения; но не всегда такие параметры есть (у Ф4226 отсутствует число точек, всегда делает 1024), и у некоторых устройств они при включении получают осмысленные значения (вроде у adc4x250 так).
28.03.2018: а вот и нифига, НЕ БЫЛО в v2'шных CAMAC-драйверах никаких таблиц параметров, там имелось только "calcstats", и лишь в nadc4_drv.c присутствовала возможность указывать zeroN.
На вопрос "ну и где оно было?!" Роговский ответил, что да -- правда, нигде не было, но "Ответ просто: ptsofs=0 numpts=32767 range0=8192 range1=8192 задавались в настройках группировки в chlclient (в плагинах pzframe). А также noistart, timing и прочее.".
Вывод: поскольку теперь возможности указывать в клиентах/плагинах значения параметров нету, то надо добавить все эти параметры прямо к драйверам -- в т.ч. и istart/noistart.
28.03.2018: приступаем -- в adc200_drv.c таблица сделана.
02.04.2018: далее:
Вообще тут всё несколько по-своему, т.к. драйвер для НЕСКОЛЬКИХ типов сразу. Поэтому сделана только четвёрка поведенческих параметров, а прочее нетронуто.
Итого: во всю шоблу (с оговоркой о c061621) добавлены ВСЕ отсутствовавшие
параметры, а ручная инициализация в InitParams()
убрана.
Собственно, фокус в том, что {r,d} могут браться "от другого" канала (29-05-2014).
24.11.2014: набор соображений:
...так что сбагривать ДРАЙВЕРУ границы наверх серверу -- малоосмысленно, только что для передачи дальше наверх в клиента, чтоб юзера ограничивали на экране.
Так может -- вовсе освободить сервер от функции проверки на попадание в диапазон, оставив её на драйверах и клиентах?
А это может делать только сервер, где-то в
StoreForSending()
.
Но не лучше ли это как-нибудь свалить на драйвер?
...проще, конечно, свалить, но шибко уж это необщо. Тут-то EPICS'ные record-support'ы гибчее -- можно разным каналам назначать разный "функционал".
А если ввести понятие "дополнительная обработка/support" (загружаемыми модулями!), маркировать некий канал таким образом, и тогда будет вызываться некий дополнительный функционал?
25.11.2014@утро, пешком из ЦНМТ домой:
Ну и пусть тогда в драйверах будут доп.каналы "верхний лимит" и "нижний лимит", в который диапазон и вгонять значения ВСЕГДА.
Для этого придётся карту каналов kozdev раздвинуть еще на 100 -- чтоб влезли 32*2=64 канала лимитов, и в резерве останется еще 40, а общее число станет удобным 500.
25.11.2014@вечер: насчёт общих ограничений есть еще один вариант: сменить концепцию
"при резолвинге точки контроля отдавать chanid реального аппаратного канала"на
"разрешены запросы записи/чтения непосредственно в точки контроля".
Обсуждение:
Но это в принципе можно отразить в frontend'ах/cda_d_insrv.
cxsd_hw_chan_t
"devchan" -- в таком случае и с аппаратными каналами, и с
точками контроля обращение будет одинаковое.
Бежать при каждой записи по всей цепочке -- плохой вариант.
Хотя в EPICS'е "многоуровневые иерархии PV" именно так и работают (там это именно полноценные иерархии, где каждая PV самостоятельна, просто "смотрит" на другую, и callback'и на неё навешивать можно).
Собирать "самый узкий диапазон" по цепочке? Ох, муторно...
09.12.2015: в продолжение темы -- начинают надобиться ограничения.
Сначала некоторые комментарии о текущей ситуации:
CXC_CVT_TO_RPY(CXC_RESOLVE)
в
async_CXT4_DATA_IO()
:
rsi.hwid = prps->hwid*0 + prps->cpid*1;
ParseCpointProps()
посредством
cptfielddescr_t
), только с phys_rd_specified
микрохалтура (но это несложно исправить).
О хотелках:
Теперь идеи "что можно сделать":
Но с этим можно и смириться -- для достижения ТЕКУЩИХ целей.
cpid2gcid()
используется еще в куче точек, то начинять его мозгами на тему диапазонов
будет нехорошо. Придётся сделать отдельную функцию, копирующую код
раскрутки, но с дополнительным функционалом обработки диапазонов.
Короче -- всё это выглядит пугающе стремновато, так что пока забиваем (но проект вот он тут есть).
P.S. Да, вот в этот момент архитектура "client performs {r,d} conversion" начинает выглядет кривовато, по сравнению с "всё делается в одних единицах/типах, а конверсия и вгоняние выполняется сервером на границе с драйвером".
В принципе, сделать-то несложно: добавить цепочку передачи, аналогичную передаче кванта, только чтоб передавалась ПАРА значений.
...и отдельно обратить внимание на то, что диапазоны со стороны клиента требуют {R,D}-трансляции. Так что пакет RANGE надо отправлять ПОСЛЕ пакета RDs.
28.08.2018: "код события" QUANT в cda и cxsd_hw синхронизован -- =5, и значение =6 специально пока оставлено свободным. Можно использовать.
@дорога-на-обед-по-Будкера: в порядке
бурчания: ну вот еще дополнительный объём нарастёт в коде, в
cxsd_hw_chan_t
и в refinfo_t
--
по 2*8 /* 2*CxAnyVal_t */ + 1 /* cxdtype_t */; плюс paddingА оно реально ли надо, или просто пустое раздувание+забардачивание?
29.08.2018: делаем.
SetChanRange()
.
Кстати, дата на cxsd_driver.h была 04-01-2017.
SetChanQuant()
-- там различие лишь в складировании переданного.
cxsd_hw_chan_t
добавлены поля range[2]
и range_dtype
.
CXSD_HW_CHAN_R_RANGECHG
=6.
CXC_RANGE
='Prn'; структурка -- CxV4RangeChunk
.
PutRangeChunkReply()
: сделана на
основе PutQuantChunkReply()
.
MONITOR_EVMASK
добавлен битик RANGECHG.
cx_range_info_t
, CAR_RANGE --
смена чисел, в async_CXT4_DATA_IO()
обработка события.
CAR_nnn
серьёзное
изменение: теперь они идут не просто все подряд (как годилось, пока их было
всего несколько), а группами -- начальные константы групп со значениями.
NEWDATA=100 (CAR_RANGE как раз в этой группе), ECHO=200, SRCH_RESULT=200.
Т.е., номера поменялись; есть надежда, что это безопасно, т.к. все непосредственные юзеры cxlib'а (cda и утилиты) линкуются с ним статически.
cda_dat_p_set_range()
;
слизана с cda_dat_p_set_quant()
.
Тут границы передаются уже не поштучно, а по указателю массивом [2].
refinfo_t
добавлены range[2]
и
range_dtype
.
cda_range_of_ref()
, слизанный с
cda_quant_of_ref()
, но с дополнением по образу
cda_fresh_age_of_ref()
: возвращает 0 при
range_dtype==CXDTYPE_UNKNOWN и +1 иначе. Что позволяет понять, указан ли
диапазон.
CDA_REF_R_RANGECHG
=6.
ProcessCxlibEvent()
.
-DM
(Min/Max).
Сама печать взята из выдачи кванта, так что перед обоими компонентами печатается префикс "@T:".
30.08.2018: далее.
REMDRV_C_RANGE
="Rnge", структурка --
remdrv_data_set_range_t
.
SetChanRange()
традиционно слизана с соседней SetChanQuant()
.
ProcessInData()
-- обработка
традиционно скопирована с соседней от QUANT.
SetChanRange()
не встречалось нигде, кроме
remdrv.c.
06.12.2019: ЕманоФедя недавно опять попросил диапазоны, так что в качестве первого шага добавлена отдача диапазонов в v5k5045_drv.c.
ReturnHVSetRange()
.
Теперь надо проверять.
09.12.2019: пытаемся проверить. Фиг -- не работает.
И драйвер вернул на место старый.
Вот и вышло, что контроллеры имеют адреса в 130-й, а remdrv пытается коннектиться к 131-й -- с предсказуемо отрицательным результатом.
10.12.2019: разбираемся.
CxV4RangeChunk
имеет размер, НЕ кратный 16 -- всего лишь 56.
PutRangeChunkReply()
-- делается округление размера вверх до
кратного 16, т.е., до 64.
async_CXT4_DATA_IO()
конкретно в ветке по
CXC_RANGE
делается проверка, что размер принятого chunk'а
должен быть РАВЕН ожидаемому, а если нет -- то отваливаем и ничего не
делаем.
CxV4RangeChunk
(не
добавлена пара int32 для заполнения), то остаётся только проверку ослабить
-- отваливать не при неравенстве, а если принятый объём МЕНЬШЕ ожидаемого.
CxV4RangeChunk
добавлено
uint8 _padding_[8]
-- это не должно ничему помешать, но на будущее пусть будет корректнее.
09.02.2021: уже некоторое время замечаю, что клиенту отдаются какие-то странные диапазоны на каналы. Замечено в cdaclient'е при разбирательствах с l_timer и adc250 в bivme2_rfmeas -- нигде диапазоны не ставятся, но "cdaclient -DM" какие-то числа показывает.
Может, при аллокировании cxsd_hw_buffers[]
забыто делать
bzero()
всего?
13.02.2021: да нет -- ЕСТЬ там
bzero(cxsd_hw_buffers, rqd_bufsize)
(т.е., ПОЛНОГО объёма)
сразу после GrowBuf()
.
Или скорее в CxsdHwSetDb()
при "наследовании" от
dtype'ов/dcpr'ов как-то некорректно копируется дуплет
range[]
+range_dtype
, или же в
ParseChanList()
'е что-то где-то не инициализируется?
Чуть позже: да нет -- и там делается bzero(&dcpr_data)...
04.03.2021@утро-мытьё-посуды: а точно ли на стороне сервера и/или протокола проблема? Может, где-то на стороне клиента -- в cda или cxlib (например, не туда указатели "данные диапазона вон там" ставятся)?
23.03.2021@~23:00, перед сном: а вот и не угадал! :D
Косяк оказался в cxsd_fe_cx.c::Put_NT_RANGE_Chunk()
:
там стояло
rpy->rs2 = clnt_u32(cp, chn_p->q_dtype);
вместо
rpy->rs2 = clnt_u32(cp, chn_p->range_dtype);
-- очевидно, рудимент от копирования из Put_NT_QUANT_Chunk()
.
А уж КВАНТОВ dtype форсится в каналов dtype -- вот и получалось, что отправлялись КАКИЕ-то данные, являющиеся обычно нулями, но с неверным dtype'ом, вот и интерпретировалось это некорректно -- просто вылазило за границы реально присланных данных и брало условно "мусор".
Кстати, для разбирательства -- точнее, для диагностики, чтобы получить полную картину по ВСЕМ каналам сервера "где какое значение range отдаётся" -- была использована cdaclient'ова фича "диапазон каналов":
cdaclient -DM -T10 localhost:12.'<0-16121>' 2>/tmp/Elog >/tmp/Mlog
Смысл:
-T10
" -- чтобы чеерез некоторое разумное время утилита
завершилась, а то на кучу каналов ответа может так и не придти.
25.03.2021: проверил -- да, по крайней мере на "тестовом" devlist-canhw-11.lst косяка более не видно.
Итак:
(Да, код во всех 4 драйверах одинаковый -- 3 идентичные строчки (для OUT, OUT_CUR и OUT_IMM), поскольку есть сделанная ради advdac'а унификация имён.
Уродство... Как поступить-то?
11.04.2021: @мытьё-посуды, 13:40: ну сделать код, идущий по массиву и вычленяющий последовательности одинаковых. В >99% случаев и будет одним сегментом.
Получасом позже: ну сделал (для примера в candac16_drv.c). Монструознейше!
Ещё 20 минутами позже: а может, свалить эту работу на advdac?
12.04.2021@утро, ~7:30: проверяем -- запускаем сервер с 1 драйвером CANDAC16 (устройства он не найдёт, но диапазоны отдаст) с указанными в auxinfo несколькими разрозненными min/max.
Чё-то фигня какая-то...
Разобрался:
Вот вроде смешная проблема, а красивого решения в голову не приходит, блин...
14.04.2021: а может -- правда вытащить этот монструозный код в advdac, и там уж развлекаться на полную (прочее содержимое advdac_slowmo_kinetic_meat.h тоже не особо кратко).
Это "на полную" предполагает отсутствие всяких "оптимизаций"/сокращений с адресацией к out[first].{min,max}, а:
Чуть позже -- да, сделано! Работает. И получившаяся
ReportOUT_Ranges()
уже переселена в
advdac_slowmo_kinetic_meat.h.
А специфика реализации -- в используемых конкретным драйвером библиотеках, которые никак не навязываются сервером, а драйвер может их комбинировать любым удобным ему способом.
Т.е., в EPICS -- стиль "классического ООП с типизацией по классам и наследованием", а в CX -- "подмешивание" (mixin).
CXLDR_FLAG_GLOBAL
/RTLD_GROBAL
) сделаны
"явочным порядком", "по договоренности".
Тогда драйвер будет не просто адресоваться к каким-то символам, а сначала запрашивать VMT интересующего его layer'а-библиотеки. Бонус в том, что разным экземплярам устройств можно будет подсовывать РАЗНЫЕ библиотеки, отвечающие одному и тому же API.
Проблема в том, что реализация получится уж больно громоздкой -- с единственным layer'ом намного проще.
Чтобы можно было обращаться к каналам со специальными именами вида @TIMESTAMP:ИМЯ_PV или [TIMESTAMP]ИМЯ_PV, и оно бы автоматически отдавалось бы из архива (с надлежащим timestamp'ом и прочими атрибутами вроде alarm etc.). Это дало бы возможности, в частности:
- ссылаться на архивные данные в разных программах -- например, как на "точку для сравнения";
- использовать для просмотра не-тривиальных данных те же программы, что и при обычной работе с системой управления; (не-тривиальные данные -- это, например, осциллограммы, картинки, ... -- т.е., все, состоящие не из 1 канала, а из некоей более сложной структуры (осциллограмма -- waveform, диапазоны и прочие настройки)
...да мало ли ещё плюсов от такой возможности -- это сравнимо по степени унификации с Unix'ным "любой объект -- это файл!".
Идея эта родилась, конечно, в первую очередь с CX в уме: вследствие принятой в CX иерархической системы имён всё очень хорошо сходится --
11.08.2021: письмо-то тогда отправил -- с изложением идеи и вопросом, не возникала ли такая идея раньше -- но ответа так ни от кого из них не дождался.
То ли идея совсем бредовая, то ли, наоборот, слишком гениальная и потому просто никому не понятная...
По-хорошему, всё же надо будет в EPICS'ном maillist'е "Tech-talk" спросить, но, видимо, попозже -- когда буду лучше петрить: ведь наверняка будут вопросы "а вам это зачем?" (прикидка -- для просмотра вэйвформ и связанных с ними данных -- т.е., "сложных структур" -- из архива), и вряд ли кто-то прямо загорится "о, да-да, отличная идея, давайте сделаем!!!111".
11.08.2021: неприятное соображение касательно реализации такого под CX: если делать путём изготовления другого "серверного окружения" (cxsd_hw+cxsd_db "обращающиеся в архив" вместо БД железа), то будет проблема, заключающаяся в том, что
CxsdHwResolveChan()
должна дать
"мгновенный" ответ, ...
CxsdDbResolveName()
-- на СЕТЕВОЙ запрос к архиву будет
принципиально долгим.
...и к тому же ещё и асинхронным -- на что юзеры API "ResolveChan" никак не рассчитывают.
И как это можно расшить? Напрашиваются такие варианты:
Но это крайне неприятно по ряду причин:
...правда, тоже попахивает асинхронностью.
Возможно, ввести на уровне API cxsd_hw дополнительный параметр "канал ещё не готов", чтобы в таком случае cxsd_fe_cx бы НЕ пытался отдавать клиенту свойства такого канала, а отсылал бы их потом, после получения ответа от архива.
Посмотрел внимательно на код обработки
CXC_CH_OPEN
-- по сути, вариант (3) легко доводится до варианта
(2): при наличии параметра/состояния "канал ещё не готов" (точнее,
"недо-найден") ВООБЩЕ НИЧЕГО клиенту не отвечать (в нём канал просто будет
оставаться недо-открытым и нефункционирующим).
Но, естественно, придётся вводить дополнительный callback-API "уведомление о завершении поиска канала".
Как бы то ни было -- такая модификация выглядит не шибко сложной, а рассуждения пока всё равно лишь теоретические.
(Да, там предполагалось, что такую функциональность может реализовывать какой-то бридж доступа к архиву, сделанный на основе libcas.)
У этого подхода есть плюсы и минусы по сравнению с вариантом "[TIMESTAMP]ИМЯ_PV":
...тут дело в том, что в EPICS, в отличие от CX, вроде нет такой штуки как "иерархическая адресация" и тем более "baseref".
...а сделать эту ручку "внешней" по отношению к скринам -- отдельной софтиной или просто caput'ом -- нельзя, т.к. все эти каналы-указатели-времени -- per-client, так что указатель времени от другой программы не сможет влиять на отдачу данных того скрина.
28.11.2014: идея: а что, если в каждом пакете с данными присылать timestamp (результат gettimeofday) от сервера, в клиенте в момент получения делать то же самое, и разницу (клиентово-серверово) прибавлять к timestamp'ам каналов? С одной стороны, локальную проблему решит; с другой стороны, не обесценивает ли это вообще всю идею глобальных timestamp'ов?
P.S. А поле под timestamp надо в пакете с данными предусмотреть.
P.P.S. Главная проблема с этой "1 секундой" -- что пакет мог задержаться из-за активности системы/сети, и даже при точно синхронизованных временах получится расхождение...
04.08.2015: список замечаний:
CxsdHwSetDb()
при создании
каналов (заполнении cxsd_hw_channels[]
) rw-каналам возраст
свежести по умолчанию ставится {0,0} (в отличие от ro, у которых
{5,0}). Не сказать, что что-то при этом сильно изменится.
CXSD_HW_TIMESTAMP_OF_CHAN()
-- отдаёт реальный timestamp
только для ro-каналов или для rw, к которым сей момент есть запросы
(это калька с v2'шного FreshFlag()
.
Раньше оно использовалось только в cda_d_insrv, а вчера заюзано и в cxsd_fe_cx.
07.12.2016: и, похоже, сразу же и отключено: в w20150811-DATAKNOB_TEXT.tar.gz в cxsd_fe_cx.c с датой 03-08-2016 уже стоит
tp = CXSD_HW_TIMESTAMP_OF_CHAN(chn_p); tp = &(chn_p->timestamp);
т.е., игнорирование.
wr_req
клиент не увидит.
Единственное исключение -- свежезапущенный клиент, который получит текущее значение в ответ на PEEK.
28.01.2015: пока что всё больше и больше выглядит (см. обсуждение про "базовые и настроечные" за сегодня), что по крайней мере для fastadc/pzframe ничего этого не потребуется вовсе, поскольку должный эффект достижим просто "правильной" организацией каналов-параметров:
ReturnDataSet()
взамен
v2'шной ReturnChanGroup()
.
...т.е., всё, что потребуется -- это
В сервере-то это сделано, а у клиента -- некий вопрос: значения-то
придут по протоколу CX независимой пачкой, и описывающие параметры
придут ПОЗЖЕ описываемого массива. Но тут решение -- драйверам в
ReturnDataSet()
'е указывать массив после параметров;
работать оно будет, хотя это кривизна -- использование побочного
эффекта вместо корректной гарантии совместности.
Вот хорошо бы, чтоб всё так получилось, поскольку вопрос "Как
понять, что данный ответ от драйвера является ответом на вот этот
запрос с этими параметрами" -- то, что в v2 делалось хитрыми
проверками в may_return_data()
-- так и не решен, и даже
идей его решения нету.
...и никакие PZREAD/PZWRITE не потребуются (которые тоже так толком и не продуманы) -- достаточно нынешнего API, а вопрос "группового возврата (п."b") решается уже в рамках сервера/протокола, не затрагивая драйверы.
01.02.2015@на-ночь-засыпая: да,
03.02.2015: а почему ПОСЛЕ?! Ведь, во-первых, все evproc'ы вызываются уже в самом конце, после обработки ВСЕХ каналов (так что тут порядок пофиг), а во-вторых, копирование timestamp'ов производится как раз в этом месте, и там порядок уже НЕ пофиг (хотя при возврате одним блоком всё равно используется ОДИН timestamp, взятый в начале вызова). Так что -- ставим базовые как раз в НАЧАЛО списка.
25.02.2015: а ПОТОМУ ПОСЛЕ, что evproc'ы вызываются всё-таки в порядке списка (хоть и после обновления всего), и при "после" параметры прилетят по TCP уже ДО базового, и в момент дрыганья они будут готовы. При "базовый в НАЧАЛЕ" же параметры в момент обновления базового будут еще СТАРЫЕ. Жопка...
05.03.2015: короче -- СЕЙЧАС надо возвращать СНАЧАЛА СКАЛЯРНЫЕ параметры, а уж в конце списка -- вектора.
Хотя по факту -- учитывая, что связь по TCP, т.е., упорядоченная, с гарантией последовательности -- достаточно и нынешней модели: параметры и так придут в клиента (и cda) ДО базового (просто отдельными посылками, а не одним chunk'ом/пакетом) и в момент его вычитывания клиентом значения уже будут актуальными.
02.02.2015: но стоит заметить, что все эти рассуждения касаются ЧИТАЮЩИХ каналов. С каналами ЗАПИСИ всё не так просто -- там нужна гарантированная атомарность, а не "удобно получилось по факту".
С другой стороны, СЕЙЧАС таких каналов записи ровно 1 вид -- таблицы
в козачиных ЦАП. А там параметры в общем-то не особо нужны: вместо
нынешнего [0] можно использовать прямо nelems
.
03.08.2016: по результатам реализации всего стека pzframe, где решением оказался отдельный канал "маркер", видно, что в случае реализации параметризованных запросов на уровне протокола (а не API драйверов) нужно будет делать ровно так же: объявлять один из каналов в запросе "маркером", чтоб сервер отсылал всю пачку именно по обновлению этого канала.
20.05.2015: исполнитель NAF'ов.
Не то чтоб прямо мега-актуально, но всё же. И как бы его можно было иначе сделать -- совершенно неясно, никакая иная идеология пока в голову не приходит.
29.09.2016: таблицы в козачиных ЦАПах.
Там получается, что надо делать отдельный вектор "времена ступеней таблицы" и отдельные вектора (по числу каналов ЦАПа) "вольты по ступеням" (в v2 они были перемешаны внутри единого вектора (чётные/нечётные ячейки).
И для консистентности первого со вторыми желательна атомарность их записи.
06.11.2017: обмерение BPM'ов Роговским (поговорено с ним несколько дней назад).
Его мнение: ему надо, чтоб было как в v2 -- чтоб не проверять, что действительно "настроечные" были в таком-то положении, а просто можно б было указать "мне надо померить такой-то канал при таких-то значениях вот этих каналов".
07.11.2017: похоже, продумывание и реализация таких атомарных запросов будет следующей большой темой, после решения текущих "3 больших задач" (гроханье cda-ресурсов, отправка запросов на запись только после получения {r,d}, UDP-резолвинг).
27.03.2018@лыжи: Насчёт "параметризованных измерений" (да и "записей" тоже): есть некоторая идеологическая проблема, заключающаяся в том, что у нас парадигма "каналов" -- которые как бы объекты, существующие всегда, независимо от клиентов, а параметризованные измерения -- это ПРОЦЕДУРЫ.
Например, если пойти тем же путём, что в v2 -- допустим, сервер получает указание "выполни параметризованное измерение": это будет набор команд записи, заканчивающийся командой чтения (DRVA_PZREAD, да?), то вот что вылазит:
Если драйвер не умеет -- то полная Ж: невозможно гарантировать корректную передачу параметров на запись, а главное -- их отработку (учитывая, что на эти каналы могли приходить ИНДИВИДУАЛЬНЫЕ запросы записи).
@лыжи-конец-2-й-2-ки: Зарегистрированным в очереди давать идентификаторы ("тикеты"?), чтоб запросившие (frontend'ы) их знали? А зачем? (А почему-то подумалось, что нужно...)
А если клиент за время ожидания ответа отвалился? Запрос из очереди ведь вроде удалять надо? Как поступать, если запрос объединённый, от нескольких? Тут, очевидно, reference count -- при отвале клиента его уменьшать, и удалять при ==0. А если запрос уже отправлен? Тут уж никак -- дожидаться ответа.
@дома-после-лыж: А-а-а, вот зачем идентификатор/"тикет": драйверу вместе с запросом передавать его идентификатор, и чтоб в ответе он тоже был -- так можно сразу определять, кому это ответ, не мучаясь со сверкой параметров. Замечание тикеты должны быть в течение длительного времени уникальными; например, циклические 32-битные -- покатит.
Как-то это всё противно. Некрасиво, фиг реализуешь корректно.
Отдельно "доставляет" то, что каналы, участвующие в параметризованных запросах, могут в то же время функционировать и как просто каналы (тем же мордам быстрых АЦП так удобнее), и всё это будет феерически конфликтовать/плохо сочетаться.
...можно, конечно, постулировать, что параметризовабельные НЕ могут работать как обычные каналы, а только как вот такие хитрые. Но тем самым мы получим де-факто большие каналы как в v2. ...ну, может только, с возможностью в запросе указывать не весь набор параметров, а только сейчас-меняемые. Но всё равно это вряд ли будет удобно -- намучились уже в v2.
28.03.2018@лыжи: выглядит так, что проще будет реализовать "ГАРАНТИРОВАННОСТЬ измерения с указанными параметрами" средствами локинга (его пока нет, но выглядит делабельным).
30.03.2018: а ведь была мысль -- 13-05-2017 -- при помощи "атомарной записи" реализовать TANGO'вские RPC. При отсутствии же PZWRITE оно вряд ли получится (не то чтоб прямо супер-необходимо :-), но всё же).
15.07.2018: и опять таблицы в козачиных ЦАПах.
Только в ином аспекте (не разделение данные/времена): атомарность выполнения операции "исполни проход по такой-то таблице".
Вылезло на примере реализации "драйвера-клиента" weldproc_drv.c, где пришлось оный "проход" выполнять аж 3 шагами: 1) запись таблицы, 2) активация; 3) запуск. И там еще внутри может произойти косяк -- когда vdev-state-машина зависала в состоянии ACTIVATE из-за отсутствия надлежащей обратной связи.
А если б была возможность заказать все действия АТОМАРНО (записать-активировать-запустить -- т.е., по факту "записать последовательность переходов")...
19.12.2018@вечер-лыжи: хронология мыслей:
Детали "размышлений":
Или сначала понарегистрировать каналы (их, если сделать ограничение, например, 100, можно и в сиксированный массив в стеке), а потом, ПОСЛЕ парсинга, уже аллокировать?
CxAnyVal_t
; и хранить
переданный при последней записи тип, и его же и отправлять при активации
пресета.
25.12.2018: да, это уже делается в mirror_drv.c,
можно оттуда код взять. А trig_read_drv.c идёт дальше: при
отсутствии @t-спецификации пытается разрезолвить канал при помощи
CxsdHwResolveChan()
и взять его свойства (dtype ПЛЮС n_items!)
из cxsd_hw_channels[]
.
И чтоб запись таких каналов могла бы делаться скопом, и чтоб команда уходила и через сетевой протокол тоже прямо единым блоком, чтоб и на стороне сервера оно б тоже расшифровывалось как один запрос и приводило бы к ОДНОМУ вызову CxsdHwDoIO().
А чтение? Чтоб какой-то из каналов назначался бы триггером? А только ли операция PZWRITE (которая реально является просто единой операцией записи, с должным образом упорядоченными каналами, чтобы "параметры" шли ДО параметризуемого основного канала)? А как насчёт PZREAD?
Как бы то ни было, главная идея-прорыв -- о том, что "групповость" добавляется и в cda.
Но пока в эту сторону не сделано ничего, совсем.
Неясно даже, КАКОГО рода конверсия предполагается: диапазонная,
[0,LAST]<=>["String0\tString1\t...StringN"]-- явно '\t'-separated multistring, или же поштучная
(Val1<=>"String1"), (Val2<=>"String2"), ..., (ValN<=>"StringN")-- хбз как.
В общем, надо на эту тему думать -- может, удастся как-то непротиворечиво и "в струю" расширить систему типов и схему именования.
11.01.2016: замечание-наблюдение: "подветки" в
devtype'ах можно делать, если просто разрешить имена с '.' внутри в
парсинге -- ParseChanList()
(две точки
ParseAName()
-- имя определяемого канала и ссылка на target), в
остальных же местах всё и так будет работать.
Конкретно -- в CxsdDbResolveName()
поиск внутри устройства
никак не интересуется наличием '.' после первой (отделяющей имя
канала от имени устройства).
29.07.2016: и еще более это нужно для pzframe-устройств:
24.10.2016: пробуем "хак" по идее от 11-01-2016.
ParseAName()
, маппирующаяся на
ppf4td_get_ident()
, и никак не позволяющая
«просто разрешить имена с '.' внутри» то:
ParseMName()
("M" -- Multi), использующая...
local_ppf4td_get_ident()
, дозволяющая '.' при наличии
флага...
PPF4TD_FLAG_DOT
=PPF4TD_FLAG_IGNQUOTES.
isalnum()
, кстати, считавший нелегитимными
подчерки '_'; вот оно дополнено и подчерком, и '.'.
(Проверено на симуляторе с u0632.)
25.10.2016: реализуем хак "полномасштабно".
PPF4TD_FLAG_DOT
запараллелен с
PPF4TD_FLAG_IGNQUOTES
.
PPF4TD_FLAG_*TERM
идут с 10, а не с 2.
Поскольку никакими загружаемостями ppf4td не используется, то проблем от смены API нет и ничьи версии продвигать не требуется.
В их число вошли u0632.devtype и vsdc2.devtype.
А вот fastadc пока без надобности, но если припрёт -- то легко (кроме одноканального c061621, которому это не нужно).
Пара замечаний:
Но это чревато проблемами, и лучше такого не допускать.
devtype t r4i { a.x 0 a.y 1 b.x 2 b.y 3 } dev d t ~ - cpoint point1 d.a cpoint point2 d.b
Это позволило бы элегантно обходиться со "связанными" каналами в устройствах. Например, в CAN-ЦАП'ах у физического канала есть 3 канала в СУ: outN, out_rateN, out_curN. И сейчас с точки зрения имён в СУ (cda, Cdr) эти каналы совершенно несвязанные. А можно б было делать им прямо внутри devtype'а alias'ы, объединяющие их в группы: outs.N.set, outs.N.rate, outs.N.cur.
Главный вопрос -- получится ли. Скорее всего нет, т.к. при поиске target-канала для объявляемого cpoint'а будет искаться конечная точка, а "половинка имени" таковой не является.
18.05.2018: попробовал, прямо на вышеприведённом примере. Фиг -- не работает, ожидаемо ругаясь
cpoint target "d.a" not found
На нынешнем этапе устройства схемы работы cpoint'ов (сделанной весной
2015-го примерно в этих же числах мая; до сих пор умиляюсь, как я тогда был
умён :D) это никак не сделать.
Можно только пофантазировать, не доработать ли схему как-нибудь, чтобы она допускала такой частичный матчинг:
item_data
в дополнение
к ref_n еще и то самое "смещение".
Однако, по здравому размышлению -- нет! Нельзя так делать, ибо это криво.
item_data
? Ведь потенциальных
"подходящих" для указанного префикса целая толпа, и при использовании
данного cpoint'а как промежуточной точки (с дальнейшими компонентами в
имени) пришлось бы проверять ВСЕХ потенциально подходящих.
Вывод: если и захочется сделать возможность ссылаться на серединку имени "неплоских" devtype'ов, то придётся реализовывать ПОЛНОЦЕННЫЕ не-плоские типы, чтобы такая «ссылка на серединку» ссылалась на некий узел, являющийся содержателем вложенной группы имён/узлов.
А этот раздельчик метим "withdrawn".
CXSD_DB_CLVL_ITEM_TYPE_nnn
, указывающий на "частичное" имя (да,
при резолвинге цели cpoint'а придётся сильно хитрее сравнивать, с учётом
возможных '.'), и в CxsdDbClvlInfo_t
также хранить
оффсет точки, на которую в "target-имени" указывает этот cpoint-узел.
...но, подозреваю, мутновато всё это будет -- фигня на фигне, хак на хаке. Просто идея такая пришла, возможность теоретически есть -- вот и записываем.
НО! Ведь в текущей v2'шной реализации (которую хотелось с минимальными напрягами перенести в v4) каналы НЕОДНОРОДНЫ! Они состоят из дуплетов {ВРЕМЯ(cs),ЗНАЧЕНИЕ(uV)}, т.е., никакие прямолинейные {R,D}-преобразования применять нельзя!
@После обсуждения на пультовой (после понедельничной планёрки) с участием Беркаева гусевской идеи подобавлять в CEAC51A/CEAC51 команду от CPS10 "иди от текущего значения вот к этому, за такое-то время". Там же обсуждались и вообще разные применения табличной отработки -- вроде плавного подъёма, но не линейного, а "по кривой как у гистерезиса -- например, из двух сшитых парабол".
26.09.2016: ну и что делать?
Но этак никаких битов в dtype не хватит на такие выдрепоны.
Примеров достаточно: quant/OTHEROP (из-за разных типов); conns_u (из-за возможности миграции каналов между серверами), теперь вот это...
(...а еще {R,D}, которые раньше были зашиты, а теперь приходят от сервера, и пока не придут -- никакую запись делать нельзя...)
28.09.2016: вчера поговорил с ЧеблоПашей.
Сегодня поговорил с Гусевым. Он на эту тему особо не заморачивался, т.к. таблицами почти не пользуется.
...кстати, и драйвера ИСТа у него нет вовсе.
29.09.2016@утро-мытьё-посуды: да, разными каналами, а синхронизация -- "агрегирование каналов" в параметризованных запросах.
15.07.2018: собственно -- сделано еще год назад, с раздельными массивами; и даже больше -- ОБЩИЙ массив (матрица на все каналы) не используется и даже реально не поддерживается вовсе.
С чистой совестью "done".
Потребно для перестройки магнитов -- всех скопом за указанное время.
15.07.2018: эта-то фича сделана, еще в июне-2017, при реализации поддержки таблиц. Только значения там не -20, а >+20.
Но:
А правило "скорость не выше такой-то" -- увы, тут игнорируется.
С другой стороны: а зачем тогда вообще таблицы? Просто записываем режим -- и все топают к нужным значениям с разрешённым им скоростями.
Так что зря я вскинулся, и преспокойно ставим "done".
cda_del_context()
& Co.
04.12.2017: (огрокано еще 01-12-2017, но записывается сегодня) собственно, первая часть проблемы была осознана еще с месяц назад, при реализации "гроханья" cda-ресурсов, а вторая -- уже сегодня, при анализе поведения cxlib и cda.
У нас эта проблема решается путём "отложенного удаления": если объект находится в процессе обработки клиентом (т.е., в данный момент исполняется callback), то удаления не производится, а объект лишь помечается как требующий удаления, и уже после завершения callback'а тот код (принадлежащий объекту, а не его клиенту), что вызвал callback, проверяет этот флаг и выполняет реальное удаление.
being_processed
: ++
'ится перед вызовом
callback'а и --
'ится после, удаление считается разрешенным при
==0;
being_destroyed
: взводится в функции удаления
("деструкторе") при being_processed, и проверяется в точке после вызова
callback'а, что если !=0 при ==0'м being_processed, то производится удаление.
del_srv()
не просто процедуры, выполняющие
удаление беспрекословно, а ФУНКЦИИ, возвращающие результат "да, я всё
удалил" или "нет, я пока занят, сообщу позже".
cda_dat_p_del_server_finish()
.
Точнее, ДОБАВИТЬ в него новые ячейки -- в этот момент вектор может реаллокироваться, и имеющийся в находящемся в исполнении коде указатель станет невалиден.
Причём этот код --
И, при проходе итератора по вектору, расположенному в другом объекте (как список evproc'ев в контексте или сервере), еще и сам этот объект-вместитель может переехать, из-за того, что создан новый объект его типа.
Выводы, по пунктам:
(Хотя и совет "пусть callback возвращает вызывателю, что тот больше не нужен и может уничтожиться" тоже встречался.)
04.12.2017: выводы "на сейчас", в отношении cda и cxlib (более развёрнуто в разделе по cxlib_client за сегодня): пока с этим косяком проще смириться, т.к. проблемы будут крайне редко. А если припрёт, то переходить на SLOTARRAY-GROWFIXELEM (придуманный как-то в душе (утром?) в крохотной сидячей ванне в "Снежинке" :-)).
04.12.2017: а не касается ли эта проблема совсем всех-всех? Включая cxscheduler и fdiolib?
Через несколько минут, по результатам анализа их кода: нет, этой пары не касается.
notifier
'а (либо сразу после close_because()
делается return
).
У-ф-ф-ф, ну хоть тут всё хорошо :)
Вывод: одиночные callback'и ("хуки"?) радикально проще в использовании, чем evproc-/callback-list'ы, позволяющие навесить море обработчиков на событие.
30.01.2018: ну вот, сегодня реальное проявление
проблемы -- в cxlib, HandleSrchReply()
начал segfault'иться,
если в пакете более 1 ответа, на втором ответе. Кратко:
HandleSrchReply
::cp
вдруг становился невалидным.
cp
перед каждым вызовом нотификатора.
Но это удалось лишь потому, что тут нет никакого ForEach...(), а просто ручной ОДИНОЧНЫЙ вызов нотификатора.
Т.е., аналогично cxscheduler и fdiolib -- cxlib иммунен к проблеме; точнее, иммунизировабелен.
(Мысля пришла при обдумывании функционирования системы синхронизации ВЭПП-5: там в некоторые устройства уставки должны быть забиты гвоздями, менять не требуется.)
Как такое организовывать?
02.08.2018: кстати, НЕТ
возможности указать "-" в списке каналов: ни .
dev_parser()
такого не допускает (хотя мог бы -- там это просто, рядом с проверкой на
'~'), ни, тем более, ParseChanGroupList()
26.10.2018: чем я смотрел -- ЕСТЬ такая возможность!
Именно в ParseChanGroupList()
стоит специальная проверка с
комментарием «"-" means "no channels"». Вероятно, прямо
с рождения так сделано: в первой же версии, где эта функция появилась --
w20150326.tar.gz -- проверка уже присутствует. Протестировал --
да, прекрасно "-" работает в качестве указания "нет каналов".
remdrv_rw_p()
переделывать DRVA_WRITE
в
DRVA_READ
при наличии некоей опции в auxinfo?
Но: 1) а прочие драйверы? 2) в remdrv'шном синтаксисе auxinfo сейчас нет возможности указания опций (хотя в v2'шном была -- через запятую после имени драйвера; но тут такое не прокатит, понадобится иной -- префикс "(опции)" перед E:HOST:PORT?).
Не, лучше б как-то именно блокировать запись. Тогда б и отдача данных драйвером могла бы работать, просто он переводился бы в режим readonly.
02.08.2018: кстати, наилучшее место для такой проверки --
cxsd_hw.c::ConsiderRequest()
.
02.08.2018: (чуть позже) неа, нифига: наилучший вариант --
принудительное прописывание всем каналам такого устройства
rw=0
. И тогда всё автоматом сработает как надо.
02.08.2018: (исчо чуть позже) а указывать "readonly" можно в поле options -- которое "[:ОПЦИИ]" после имени устройства (04.08.2018: оно, кстати, сейчас вообще НИКАК не используется -- даже для drvlogmask (которое реально просто всегда =0, т.к. отсутствуют возможности регулирования).).
26.08.2019: первые 2 пункта давние, 3-й свежий.
И если вдруг понадобится передавать что-то свыше 4ГБ -- точнее, 2^32-1, а скорее даже 2^31-1, или 2ГБ -- то текущий протокол уже не прокатит, и придётся создавать другой, 64-битный. Который и можно назвать "v5".
20.04.2016: с другой стороны, это вопрос именно протокола -- который в нынешней архитектуре вполне заменябелен на уровне модулей, связки cda_d_cx/cxlib+cxsd_fe_cx. Прочая же система вполне 64bit-safe.
Вот надо делать поддержку таблиц в козачиных ЦАП'ах (для перевода сварки на v4), а выйдет криво: вместо массива дуплетов {вольты,время_перехода} придётся делать ОТДЕЛЬНЫЙ массив с временами. Причина -- калибровки: ведь уставки надо указывать в вольтах или даже с еще коэффициентами, а времена -- в что-то-секундах. И свести это в единый массив никак не получится.
09.05.2017@утро-душ: вот в EPICSv4 есть более сложные типы -- и матрицы всякие, и структуры. И даже стандартизация -- т.н. "normative types".
Что же нам делать?
В частности, тогда и калибровки {R,D} для таких типов тоже были бы "структурами".
Сейчас это просто байт (8 бит), но де-факто он всегда занимает 32-битное
слово, даже в протоколе (кроме единственного случая
CxV4MonitorChunk
, где пакуется вместе с cond).
(dtype&0xFFFFFF00)==0
всё было бы как сейчас, но в старших
3 байтах как-то кодировать дополнительную информацию.
И в "расширенном dtype" будет храниться индекс описателя в этом реестре.
И если от очередного сервера придёт описание типа, уже имеющееся в реестре, то оно получит уже использующийся индекс, а не будет регистрироваться новый.
Кстати, это немного напоминает систему типов C (и C++ ли тоже?), где эквивалентность типов определяется не по их именам (они в результате typedef'ов могут быть разными), а по их определениям.
Но также существует и "SID" (Server ID, реально -- Server-Side ID), в наших "hwid": идентификатор, уникальный в рамках сервера.
Так вот: чисто для некоторой экономии можно при резолвинге сделать еще один уровень словаря: чтобы сервер не с описанием каждого канала передавал бы описание его типа, а передавал бы индекс в рамках своего "реестра типов", а клиент бы держал свой кэш интересующих описаний типов для данного серверного соединения, состоящий из дуплетов {серверов_идент,описание}.
С другой стороны, это преизрядное усложнение -- ради небольшой экономии траффика придётся лишиться "гарантированного наличия информации сразу после резолвинга", т.к. в части случаев будет требоваться еще один запрос/ответ; а лишний перезапрос -- это лишняя латентность, никак не уменьшаемая (ибо оно не векторизуется), а лишняя латентность -- всегда хуже лишнего траффика.
Так что оставим эту идею, прямо сразу же "withdrawn".
А теперь -- вот, получается, что этот простой подход просто не позволяет удовлетворить несложную вроде бы потребность в "таблицах переходов".
Или как-то можно выкрутиться? Элегантнее, чем отдельным массивом?
Постоянно в последние 2-3 года (да и с самого начала, года с 2005-го) мыслилось, что нужно придумать какой-то механизм.
...но и cda придётся поучаствовать, конечно.
Вот эта пара -- поддержка нетривиальных типов и атомарности -- вполне тянет на v5.
09.05.2017@вечер-ванна: и еще:
.
' и '->
' для полей структур,
'[]
' для элементов массива.
А тут как? Вот есть какой-то блок данных -- массив байтов; и чо?
Задача, собственно, из двух частей:
Чуть пространнее:
Такой синтаксис позволил бы описывать структуры данных произвольной глубины вложенности.
Методы-аксессоры? Которым передаётся "ссылка" на этот блок полученных данных и некий (строковый?) спецификатор того, что мы хотим получить?
А у коллег-конкурентов как (TANGO, EPICS4)?
И еще:
Или подобное только для чтения? Не айс, т.к. вот прямо сейчас видно, что
клиентам по возможности надо давать возможностей максимально близко к
драйверам -- чтоб изготавливать "clientside-драйверы" (чем ЕманоФедя активно
злоупотребляет пользуется).
И, если еще как-то можно представить интерфейс к этому кошмару в
cxlib (ибо низкоуровневый), то как оно должно выглядеть в cda -- неясно.
Чтобы с такими "группами" работал бы broadcast-resolving, чтоб софтине можно б было указать "имя группы", оно бы куда-то разрезолвилось, а уже из того сервера бы вычитался бы список составляющих группу каналов.
10.05.2017: идея действительно совсем никак не оформившаяся (от приятного лежания в тёплой ванне посетила). Как оное может помочь -- неясно, и как оно вообще может быть применено -- тоже. В текущей ситуации резолвинг "группы" прекрасно делается по раздельным именам (которые заранее известны), префиксируемым указываемым общим именем "элемента". Полезно может оказаться только в ситуации, когда состав группы заранее никак не известен; но как тогда с её содержимым обращаться -- тоже не особо понятно.
10.05.2017: в качестве промежуточного резюме: попытка продумать поддержку сложных типов показывает, что это офигеть как проблемно и выглядит просто бесконечно трудоёмко и НЕкрасиво.
...но что же делать с таблицами в козачиных ЦАПах? Ведь такой вроде простой пример -- ну надо ж что-то придумать...
Итого на сейчас:
10.05.2017@дома-вечер-после-японского: пораздумавши, напрашиваются некоторые выводы:
И то, что эти составляющие группу каналы являются именно раздельными каналами, просто адресуемыми -- автоматом убирает проблему "как доступаться к данным [сложных структур] в клиенте".
Т.е., ранее обдумывавшихся "параметризованных запросов" (PZREAD/PZWRITE).
13.05.2017@дома-00:15: а ведь TANGO'вские RPC можно делать "атомарными большими каналами" -- как было в v2; только собственно атомарность надо сделать (тут это будет PZREAD).
Кстати
Seq
; но он вроде никак НЕ используется в целях такой
идентификации (ибо самой атомарности в EPICS3 нету).
ЗЫ: а в v4'шном cxlib_client.c поля cp->Seq
и
cp->syncSeq
мэинтэйнятся, но никогда никак не проверяются
(в отличие от CheckSyncSeq()
в v2).
15.05.2017: вот нифига, НЕ НАДО пытаться реализовать EPICS'ную замудрённость для решения имеющейся проблемы.
Всякая же структурность -- это уже способ передачи более высокоуровневой информации, НЕ от устройств. [SPAN CLASS=comment3]Замечание от 29.07.2019: хотя всякие картинки, особенно если в JPEG -- вроде бы от устройств, но "структурность" там требуется вполне по сути.[/SPAN]
Так что если мы просто обеспечиваем нужное "взаимодействие" ("семантику передачи данных"?) (как при помощи канала MARKER у pzframe), то можно обойтись и без сложной структурности.
17.05.2018@около-пультовой: положа руку на сердце, совсем корректно и красиво сделать АТОМАРНОЕ получение данных о связанных наборах каналов -- {картинка,ширина,высота} -- можно только в случае передачи именно структурированных типов данных. Как это сделано в TANGO, EPICS4 (включая "нормативные" типы), etc...
29.05.2019@вечер-Лаврентьева по
дороге домой: а может, зря под cxdtype_t
было отведено
всего 8 бит? Ведь в 32 удалось бы закодировать намного больше информации
(вспоминаем упаковку опкодов в процессорах -- чего там только нет, включая
immediate-знаяения): и как-нибудь описать схему кодирования конкретного
типа, и в случае чего-то вроде заранее предопределённых ("нормативных") типов
-- номер такого типа...
08.07.2019: хо -- ведь 09-05-2017 ровно те же мысли были, про 32-битность, "как было бы хорошо, если б...".
07.06.2019: проанализировал по cx_proto_v4.h, как передаются dtype'ы по сетевому протоколу.
CxV4MonitorChunk
, где
упаковывается вместе с "условием" в 32-битное поле
dtype_and_cond
.
Но:
moninfo[]
, которое НЕ
ИСПОЛЬЗУЕТСЯ.
CXDTYPE_UNKNOWN
,
а на стороне сервера значение вообще не используется.
(А сам такой chunk используется только в направлении клиент->сервер, для установки и снятия подписки (монитора).)
Вероятно, это была заготовка для более навороченных мониторов -- с
возможностью указания дельты (CX_MON_COND_ON_DELTA
), когда
понадобилось бы ещё dtype этой дельты передавать.
CxV4MonitorChunk
появился, судя по
w20141226-proto_v4_notes.tar.gz, в районе 24-12-2014 (точно после
09-12-2014).
Таким образом, если всё же захочется перейти на 32-битный
cxdtype_t
, то:
cxdtype_t
-данные передаются, и местами
захардкожено, что это 1 байт.
А так надо будет учитывать и изменившуюся байтовость, и что нужно выполнять endian-conversion. Конкретно:
ProcessInData()
, код REMDRV_C_DATA
.
remdrv_rw_p()
.
ReturnDataSet()
.
ProcessPacket()
, код REMDRV_C_WRITE
.
REMDRV_C_QUANT
,
REMDRV_C_RANGE
) УЖЕ передаётся 32-битовым и конверсится.
Только надо переделать поля с int32
на uint32
.
Несколькими минутами позже: неа, уже БОЛЕЕ -- не надо: там и так из-за того, что оно передаётся 32-битным, УЖЕ делается конверсия.
Получасом позже:
cxdtype_t
(вроде упаковки с чем-то ещё) нету. И
в случае изменений размера достаточно будет просто всё перекомпилировать.
17.07.2019@пляж-после-обеда: а
толку-то от 32-битного cxdtype_t
? Ведь ПОЛНАЯ информация о
типе туда всё равно не вместится. Так что можно пока притормозить переход
на 32-битность -- пока детально не огрокаем, как всё устроено в EPICS4 и
TANGO, чтобы достичь лучшего понимания.
07.07.2019@вечер-дома: в презентации "EPICS 7" от Kay Kasemir есть фраза, что EPICS 7 "Breaks your device support".
Так вот, почему-то (от противного? :-)) эта фраза натолкнула на мысль:
А "параметризованная запись" -- аналогично, запись значений параметров, а затем запись в базовый канал.
А по здравому размышлению -- оно просто нереализовабельно.
Так что -- реализовывать такой функционал нужно именно там, где он реализуется максимально простым и естественным образом. Т.е. -- в драйверах.
После обеда: кстати, можно PZ* делать не дополнительными кодами операций (DRVA_PZREAD/DRVA_PZWRITE), а именно отдельным МЕТОДОМ. Тогда исчезает проблема "а как же cxsd_hw будет эмулировать оное вызовом do_rw(), если это remdrv?" (причина которой -- что флаги-опции, где предполагалось указывать "драйвер поддерживает/не-поддерживает PZ", из удалённых драйверов не добыть), плюс эмуляцию на стороне удалённого контроллера сможет выполнять уже remcxsd.
Ещё чуть позже: строго говоря, "отдельный метод для PZ*" иил "тот же метод, но во флагах указывается поддерживаемость" -- это один фиг: эти варианты взаимозаменяемы. И эмуляцию точно так же можно делать в двух местах -- cxsd_hw и remcxsd.
09.07.2019: just for record: первоначально мысль "либо отдельный метод do_par_rdwr(), либо driverrec.flags; и при неподдержке драйвером -- эмулировать" возникла 16-08-2014.
08.07.2019: в той же презентации в разделе "Python" на странице "Custom PV Data in Python Client" есть фраза "Python receives data as dictionary, access to any element".
И что-то какие-то неясные мысли эта фраза пробудила в голове, на тему возможной реализации таких произвольных custom-типов: а может, правда как-то технология "dictionary" облегчит реализацию наших хотелок?
09.07.2019@утро-выходя-из-дома, лестница вниз: надо битым текстом отметить: как сделать работу "параметризованных запросов" в API драйверов -- более-менее ясно. Главная проблема в том, как эту концепцию отразить на уровне клиентов (cda) и протокола.
@в районе обеда, ИЯФ: некоторые соображения на эту тему есть в разделе "Параметризованные запросы" за 19-12-2018.
11.07.2019@утро-пляж: ("формально" записывается на презентации Лёши Левичева о клистроне (в E90), а в файл -- уже 12-07-2019): битым текстом соображения на тему реализации композитных типов -- что понятно, а какие проблемы:
getfield(dataref, "a.b.c")
, где "a.b.c" -- цепочка
вложенных полей.
12.07.2019: как вариант -- в качестве "исходников" использовать именно devlist-определения, а генерить только для языков.
12.07.2019: кстати, в bigfile-0001.html ASN.1 упоминался 15-04-2007 именно в контексте "посмотреть бы, в связи с тем, как передавать данные больших каналов".
И какое ключевое слово выберем?
typedef
" -- нельзя, оно слишком похоже на
"devtype
", возникнет путаница.
class
"? Тоже не айс -- "классы" обычно
подразумевают наличие "методов", а тут это не пришей кобыле хвост.
composite
"? Типа такого:
composite TYPENAME {CONTENT}
datatype
?
Оно и достаточно "уникально" и не может быть ни с чем спутано, и слово "type" в нём присутствует, так что файлы с определениями типов также можно будет сваливать в types/.
@презентация-Левичева-о-клистроне: а вот и нет! Надо ведь производить endian conversion, так что рассматривать сложные типы как просто последовательность байтов -- никак нельзя.
Ведь сейчас используются просто буквы, обозначающие конкретный скалярный
тип -- это очень коротко; например, r1i -- 1 скалярный
int32
. А тут как указывать -- r1WHAT?
Напрашивается идея, что "расширение синтаксиса" -- указывать что-то в фигурных скобках (по аналогии с описанием структур в C): r1{SPEC}.
Неприятно то, что фигурные скобки будут плохо сочетаться с синтаксисом
devtype
: там ведь '{' является окончанием списка групп
каналов и открывает список пространства имён. Так-то всё проходит, но очень
плохо перенесёт синтаксические ошибки.
ЕМНИП (из книги Страуструпа, что ли), C (но НЕ C++!) именно так и сравнивает типы на эквивалентность.
Это оно так ПО СМЫСЛУ; а насколько реализовабельно, и насколько оправданно по трудозатратам -- вопрос.
15.07.2019@утро, выход из перехода в 13-е здание, по пути на понедельничную планёрку: уж ЭТО-то -- разные размеры массивов -- проблемой не является, решение элементарно: как в обычном C, массив "объявляется" не двумерный (n-мерный?) [H][W], а ОДНОМЕРНЫЙ, [0]; аллокировать сколько надо, а доступ из программ "вручную" -- [y*W+x]. Естественно, такие поля могут быть только в КОНЦЕ.
15.07.2019: по результатам чтения презентации "EPICS7" (стр.47) также выглядит, что там можно попросить как композитное поле целиком (training:circle), так и любое его под-поле (training:circle:x).
Во-первых, из этого синтаксиса вроде бы следует, что в PVaccess в качестве разделителя имени поля ("field name separator") используется ':' вместо '.' в ChannelAccess. ...но прочие доки этого не подтверждают: например -- повсеместно предполагается именно '.' ("PvObject - EPICS4 PvaPy 1.1.0 documentation" ("using . as the field name separator"), "EPICS V4 Developer's Guide" ("Fully expand the name of this field using the names of its parent fields with a dot '.' separating each name.")).
Во-вторых, если реализовывать поддержку такого синтаксиса в "CXv5", то КАК? Как ссылаться на ЧАСТЬ канала?
15.07.2019@часом позже, выходя из ИЯФ, парковка за ДОЛом и пляж: да в принципе -- совсем несложно: в "результате" поиска запоминать кроме cpid/gcid также и "информацию о поле внутри канала".
Что будет представлять из себя эта "информация о поле внутри канала" -- отдельный вопрос: вероятно, некое смещение плюс ссылка на определение типа. При указании на "целый" канал -- какое-нибудь специальное значение, говорящее "всё целиком".
Соответственно, как это "будет функционировать":
CxsdDbResolveName()
/CxsdHwResolveChan()
получат дополнительный return-параметр -- эта самая "информация". И их
"клиенты" должны будут хранить её в дополнение к идентификатору канала.
ЗЫ: в трактате Kay Kasemir'а утверждается, что присылаются только ИЗМЕНЕНИЯ (а не, в т.ч., структуры целиком). Уж не знаю, как это сделано (если реально сделано) -- помнится предыдущее и сравнивается? А не проще ль в СЕРВЕРЕ ловить именно событие "обновление значения", а совпадает ли значение с предыдущим или отличается -- вопрос более верхних уровней.
(EPICS решает проблему ЯВНЫМ "созданием «канала» -- аллокирует Server-Side-ID (SSID), который уже содержит любую нужную информацию.)
Чуть позже: а ведь можно этот "2-й ID" аллокировать не cxsd_hw-wide, а лишь CONNECTION-wide -- чтобы у соединения был реестр таких "ссылок на внутрь", и клиенту отдавать номер строки в реестре. Но, раз НЕТУ никакого явного "создать канал" и "удалить канал", а есть отдельно резолвинг и отдельно использование, то такой подход чреват/сомнителен.
18.07.2019@утро-дома: последовательные соображения на эту тему:
@ИЯФ: уже задним числом: отдельный вопрос в "составе" такого реестра -- как он должен был бы выглядеть? Ведь "номера строк" в нём осмысленны только в сочетании с hwid/cpid того канала, для которого они были получены. Поэтому если б пришлось его делать, то каждая строка должна б была содержать и hwid/cpid того канала, чтобы когда клиент пришлёт дуплет {cpid,subfield_registry_id}, то можно б было убедиться в осмысленности второго id'а.
Краткое обсуждение такого варианта:
Цикл "конверсии/добычи" такие строки будет просто пропускать.
Пара замечаний:
Ну ясно, что какой-то спец.флажок, показывающий циклу, что при конверсии эту строку надо пропустить.
А ещё? Очевидно, необходимо количество под-элементов. Но также, видимо, потребуется и общая длина последующего "линейного списка описателей", относящегося к этому полю (не считая его собственной строки). Поскольку, например, для композитного типа такого вида:
у поляstruct { struct int32 a; struct { int32 b; int32 c; } level3; int32 d; } level2; } level1;
level2
число под-элементов будет 4, а длина его
"линейного списка описателей" -- 5 (a, level3, b, c, d); у поля же
level1
-- и вовсе 1 (level2) и 6 (level2, a, level3, b, c, d)
соответственно.
Второй вариант ("чуть-позже") выглядит очень красивым и решает все возможные проблемы. Если станем делать -- то стоит придерживаться именно его.
15.07.2019@вечер-выходя-из-ИЯФа: к вопросу о том, как "описывать" сложные композитные структуры и как производить сериализацию/десериализацию ("маршаллинг") -- всё обдумывал, особенно после прочтения в описании протокола pvAccess, что они не используют padding вовсе. И осенило:
Это для КОНВЕРСИИ -- если надо переделывать порядок байт.
А если просто нужно распотрошить содержимое пришедшего пакета в некий
тип-структуру, то достаточно и того описателя: просто в цикле линейно
пройтись по той таблице, делая memcpy()
указанных количеств
байт по указанным смещениям.
...да, есть некоторый вопрос "а как быть с variable-length полями", но тут что-нибудь придумываемо (проблема скорее техническая, чем идеологическая).
16.07.2019: и я лично таким пользуюсь нечасто: в
4cx/src/*.h слово "union
" встречается всего 7 раз. Но
все эти разы -- тип содержимого НЕ меняется после первоначальной
инициализации и постоянен на всё время существования объекта.
refinfo_t
индекс этой строки.
И строки ТОЛЬКО ДОБАВЛЯТЬ, никогда не удаляя.
И эту пару нужно как-то "сдружить" -- чтоб чтение канала такого типа раскладывало бы полученные данные по полям C'шной переменной, а запись в такой канал готовила бы сериализованное представление, годное для отправки серверу.
(Кстати, на стороне сервера -- в связке сервер<->драйверы -- стоит аналогичная проблема.)
Т.е., "связывание" полей C'шных типов с сетевыми будет производиться по именам полей.
16.07.2019: явно в этой схеме не хватает ещё имён самих типов. Как-то бы и их пристегнуть.
Идея пока не особо детально продуманна, главный смысл -- именно в самом факте "требования предварительной регистрации".
Следствие: в таком случае будут считаться совпадающими любые типы, имеющие одинаковое внутреннее представление. Например, "int32" и "struct {int32 x}", "int16[40]" и "struct { struct {int16[40] a} sf}".
17.07.2019@пляж-после-обеда (пробыл всего полчаса): "линейные списки описателей" -- это хорошо. Но при этом полностью теряется информация о первоначальной структуре данных (иерархия); и что, если какой-то клиент захочет, подписавшись на ЦЕЛЫЙ композитный канал, взять из него КУСОЧЕК, ТОЖЕ КОМПОЗИТНЫЙ?
Так что надо бы как-то туда внедрять и информацию о "под-полях" -- возможно, в варианте списка кортежей {"ИМЯ.ПОД.ИМЯ", НОМЕР_ПЕРВОГО_ОПИСАТЕЛЯ, КОЛИЧЕСТВО_ОПИСАТЕЛЕЙ}. Или прямо в "линейный список..." вставлять "NOOP-строки", игнорируемые копировщиком/конвертером, но обозначающие границы вложенных элементов иерархии.
P.S. Слегка напоминает дилемму, стоявщую в конце 2004-го, когда после изобретения "вложенных элементов" мучился вопросом -- не должны ли вложенные элементы брать ручки из линейного потока ручек элемента-содержателя?
@вечер-ИЯФ: а можно такое
"взятие под-полей" просто запретить: читай всё скопом! ...неа, не
вариант -- надо мочь вычитывать индивидуально любое произвольное поле, в
т.ч. и композитные.
18.07.2019: решение найдено -- в рамках идеи о том, как обеспечить простую и stateless-адресацию к под-полям из клиентов: заводить в "линейном списке описателей" по строчке и на каждое композитное поле (см. "чуть-позже").
18.07.2019@вечер-пультовая, за чтением "pvAccess Protocol Specification": ещё к вопросу о "линейных списках...": ведь должны поддерживаться и МАССИВЫ СТРУКТУР. И "линейный список" на таком массиве даст совершенно бешеный расход.
Чуть позже: а если как с нашими "простыми" типами: указывать "nelems" в каждой строке? ...увы, для "конверсии" не прокатит: там могут быть вложенности...
28.08.2019: переделываем cxdtype_t
с
uint8
на uint32
.
CDA_DAT_P_MODREC_VERSION_MAJOR
: 1->2.
CDA_FLA_P_MODREC_VERSION_MAJOR
: 1->2.
CXSD_DRIVER_MODREC_VERSION_MAJOR
: 10->11.
CXSD_LAYER_MODREC_VERSION_MAJOR
: 2->3.
CXSD_FRONTEND_MODREC_VERSION_MAJOR
: 2->3.
REMDRV_PROTO_VERSION_MINOR
: 0->1.
Тут достаточно смены MINOR -- в силу конкретики сравнения в
CX_VERSION_IS_COMPATIBLE()
, используемого в
remcxsd_driver_v4.c.
Версии "ext" и "lib" (CXSD_EXT_MODREC_VERSION_MAJOR
и
CXSD_LIB_MODREC_VERSION_MAJOR
в cxsd_extension.h)
трогать не стал, по причине отсутствия таковых модулей.
CX_V4_PROTO_VERSION_MAJOR
также трогать не требуется,
поскольку бинарное содержимое передаваемых данных не меняется.
ProcessInData()
(ветвь REMDRV_C_DATA
) и
remdrv_rw_p()
(в частности, поскольку теперь требуется endian
conversion, введён dt2snd[]
).
ReturnDataSet()
(добавлен dt2snd[]
; тут не ради
конверсии, а для гарантированно-32битности) и ProcessPacket()
(ветвь REMDRV_C_WRITE
).
Проверять теперь надо!
23.09.2019: ну проверил. Фиг -- не работает, нули кажутся. Разбираемся --
ProcessInData()
было забыто делать endian-conversion приходящих dtype'ов.
Добавлено.
dt_ptr
.
Исправлено -- введён массив cxdtype_t dt2ret[]
, куда и "копируются"
dtype'ы, и уже он передаётся ReturnDataSet()
'у.
Теперь -- да, работает. Ура!
Ну доведён CXv4 до очень юзабельного состояния, а дальше-то что, какое направление развития?
То, чем я сейчас занимаюсь -- rfsyn, клистроны, сварка; hwinfo -- это мелочи. Нужно думать глобально (2-е правило Шварценеггера -- "never think small, think BIG!").
А вот фиг знает, нет определённой ясности.
Всё, что приходит в голову -- "расширение", в виде поддержки других платформ и протоколов: порт под Windows, плагины для EPICS и Tango.
28.08.2018: чуть более формально:
27.08.2018@утро-дорога-на-работу, по Лаврентьева: ну очевидно же, что надо подизучить EPICS и Tango, с троякой целью:
Например, строительные блоки: CX и Tango -- устройства; EPICS -- каналы; имена: CX и Tango -- экземпляры устройств заводятся целиком, со всеми каналами, EPICS -- каналы объявляются поштучно, ссылаясь на устройства косвенно (и могут быть каналы, в устройствах существующие, но никак не адресуемые); CX и EPICS -- есть alias'ы, Tango -- вроде нету (?); модель обмена: CX -- асинхронная, EPICS -- рекорды в основном пассивны, процессятся только по запросу "свыше", Tango -- синхронная (хотя и с исключениями, вроде нотификаций/event'ов и особенностями, вроде таймаутов).
И понятно, что по результатам разработки cda_d_{epics,tango} вполне можно написать статью на ICALEPCS-2019.
...возможно, также и обзорную статью по той сравнительной таблице.
30.01.2025@вечер, лыжи: цепочка событий/мыслей была такой:
frolov_d16_rw_p()
скопом вернуть те значения, что
изменились -- тем самым избегая множественных возвратов V при изменении A и
B скопом.
Потому, что:
Только как это сделать?
Тем более, что локинг как раз с группами и работает.
...хотя, кажется, там до сих пор адресация по hwid, а не по handle.
31.01.2025: неа, именно по handle, т.к. локинга есть 2
варианта: "старый" cx_rq_l_o()
по hwid и "новый"
cx_ch_rq_l_o()
по chnd. Но главный прикол в другом: обе эти
операции работают с ОДИНОЧНЫМ каналом, а НЕ с группой!
И такое, возможно, не даст никаких плюсов, т.к. накладные расходы cxscheduler'а, возможно, не так уж велики, но вот то, что аналогичные действия ото всех драйверов будут совсем одновременно, а не распределены хоть сколько-то -- точно минус.
06.03.2025: реализовывать-то вряд ли будем, но чисто аргументация:
В результате получается изрядное количество по сути дублей.
А если ввести понятие "периодического события", на которое каждый драйвер устройства может подписаться, то дубли исчезнут, что, возможно, даст выигрыш.
ALIVE_SECONDS
у CAN-устройств.
RECONNECT_DELAY
у всех TCP-драйверов (remdrv,
Modbus, ...).
...хотя это НЕ периодическое событие!
Но и ALIVE_USECS
у UDP-устройств (ottcam, slbpm, будущий
Карпов).
HEARTBEAT_FREQ
=10 у CAN-устройств и прочих
advdac-based.
09.03.2025: а как в EPICS -- по информации из "EPICS Process Database Concepts: Scanning Specification":
Некоторые соображения на случай гипотетической реализации:
При отключении последнего юзера такого периода -- надо период из пула удалять.
Нашел, обсуждалась аналогичная вещь 23-08-2008 в разделе "ВременнАя схема работы".
27.06.2019: вернулся к этим 2 концепциям после разговора с ЕманоФедей на тему о том, как доступаться к СУ клиентам, работающим в ДРУГОЙ сети, а не в локальной сети СУ.
Оная "другая сеть" может доступаться к сети СУ через VPN, коий бывает 2 видов:
Собственно, какие могут быть глобально-философские средства решения проблемы второго варианта:
Тогда для клиентов из другой сети, но с маршрутизацией (но НЕ NAT!) всё бы работало.
11.07.2019: что надо прочитать побыстрее:
24.06.2005: схема: