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/

CX:
Общие схемы:
  • 24.06.2005: структура директорий

    24.06.2005: схема:

    cx/
    |
    +-
    |
    +-src/
      +-doc/
      +-scripts/
      +-include/
      | +-sysdeps/
      +-lib/
      | +-cxlib
      | +-cda
      | +-Cdr
      | +-Knobs
      | +-Chl
      | +-Xh
      | +-cxsl
      +-programs/
        +-server
        | +-drivers
        +-runner
        +-utils
    
  • 24.06.2005: список фич.

    26.06.2005: (начато)

    • Общее
      • Совместимость с Win32.
      • Разделение по "подпроектам" -- cm5307/ -- отдельным проектом-деревом, CAN-драйверы -- тоже.
      • ВЕЗДЕ приватный указатель для callback'ов имеет одно и то же имя -- 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 -- кратко и точно.

      • Постараться везде (во всех драйверах и API) времена/таймауты указывать в МИКРОсекундах, для унификации, в баню миллисекунды (@26.05.2008) (А как насчет cur_age/fresh_age?)
      • Модульный сервер -- так чтоб 1) он был подселяем к готовым программам, и 2) он мог бы вместе с драйверами подселяться прямо в программу-клиента, обеспечивая ей -- через тот же API! -- не сетевой, а прямой доступ к железу (@24.08.2008)
    • UI -- Knobs+Chl+Xh -- общее
      • Простейший i18n -- чтобы строки из библиотек могли быть локализовабельными.
      • 03.05.2006: иметь единый компонент-контейнер "ELEM_GRID", заменяющий нынешние ELEM_SINGLECOL/ELEM_MULTICOL, и умеющий как многоподъездность, так и менять "базовое направление" с "европейского" на "японское".
    • Knobs
      • Парсинг widdepinfo (options) -- библиотекой, а не конкретными knob-виджетами.
    • Уровень данных -- Cdr, cda
      • 18.09.2005: интеграция sea и формул -- чтобы можно было прямо в БД/chlclients указывать довольно хитрые действия.
      • 13.03.2006: "переменные" адресуются не по номерам, а по именам, и отводятся в момент регистрации формулы, а не заранее в фиксированном количестве.
      • 01.04.2006: нормальный формульный язык, позволяющий не маяться с польской записью.
      • 14.02.2007: "список строк" (который #T/#F у селектора) -- отдельным полем, бывающим всегда.
      • 18.04.2007: а не сделать ли "возраст для посинения" per-knob-параметром? ЧИсло-то это может и разниться, а по умолчанию пусть ставится все то же 5. 09.07.2007: неа, уже придумали другой вариант -- CXCF_FLAG_DEFUNCT будет уставляться при cur_age>fresh_age, где fresh_age -- время свежести канала (оба в миллисекундах).
      • 09.07.2007: у Cdr и/или cda должен быть "параметр" "только для чтения", чтобы из формул команды записи НЕ исполнялись. Смысл -- чтобы cx-starter не пытался реализовывать обратную связь.
    • cxlib
      • Расшивабельный connect() -- O_NONBLOCK.

        Upd@20.05.2007: хитрее -- раздельные библиотеки собственно cxlib (махинации с содержимым пакетов) и "cx-transport", могущая быть реализована как на SIGIO, так и на fdiolib'е или как-нибудь еще.

      • cx.h->cxlib.h cx_types.h->cx.h
    • CX-протокол
      1. *Server* does endian conversion.
      2. Унифиц. fork'и для chan/bigc/dbrq.
      3. Флаг "когда" у fork'а -- "немедленно", "по измерению", "по концу цикла", etc. -- обобщение cachectl и immediate. Флаг "когда" синхронного пакета является "максимумом" флагов его fork'ов. (@30.01.2006)
      4. Унифицированный логин-протокол для обычных и консольных соединений -- чтобы в обоих случаях передавались и username (32 символа -- это линуксовый лимит), и "пароль" (?), и ВСЕГДА имя программы -- видимо, последним, varlength-параметром в запросе. (@26.02.2006)
      5. "Параметры представления" у больших каналов и отдельные команды установки/чтения отдельных параметров больших каналов (так же, как сейчас читаются/пишутся обычные каналы). (@03.06.2006)
      6. Насчет внутреннего устройства самого протокола -- обратить внимание на протокол X11. (@06.04.2007)
      7. Протокол v4 надо перенести на другие порты -- для возможности параллельного обслуживания v3 и v4. (@22.07.2007)
    • CX-сервер
      • Парсинг драйверовых параметров из auxinfo -- самим сервером.
      • Layers.
      • Методы драйверов sim_do_rw и sim_do_big.
      • Обновленный API драйверов -- прототип которого реализован в cm5307_dbody: devid, businfo[businfocount], init_b() возвращает статус, etc. (@02.01.2007)
      • Унифицированный протокол "удаленных драйверов" вместо нынешних cm5307_drvlet_proto и cangw_cansrv_proto. (@02.01.2007)
      • Постараться сблизить "удаленный" (cxlib, cda) и "локальный" (in-server) API доступа к каналам. (@02.01.2007)

        05.07.2008: на эту тему см. раздельчик "In-server channel API" в "Идеях".

      • Драйвер при смене/отдаче статуса (в т.ч. из init_b()!) возвращает также и строковое описание проблемы, которое отображается в "scan". (@31.01.2007)
      • Сервер имеет лимиты [min,max] на каждый канал записи и при командах записи вгоняет значения в эти лимиты. (@19.06.2007)
      • Возможность захватить канал записи или большой в монопольное управление (блокировки), плюс режим доступа клиентов "readonly". (@06.07.2007)

        (Видимо, на каждый канал иметь поле "locker", в котором писать ID соединения, и разрешать запись только при locker==0 || locker==this_client. Ну а на readonly -- простейшая проверка прямо при обработке fork'а.)

    • БД: (начато 07.07.2008)
      • Первое необходимое требование -- серверный БД-API/протокол должен позволять клиентам запрашивать "ссылки" на каналы по ИМЕНАМ: ИМЯ_БЛОКА.ИМЯ_КАНАЛА, чтобы в клиентах не приходилось держать магические номера.

        Кроме избавления от чисел, это даст возможность "относительной адресации" в клиентах ("refbase" -- к базовому адресу вмещающего элемента через "." добавляется имя канала, указанное в ручке).

        Также это позволит делать "программы-приборы" -- всякие adc200 и d16, которым указывается имя блока, а они уже к нему конкатенируют имена каналов.

      • Кроме "настоящих" БЛОК.КАНАЛ'ов чтоб можно было делать "alias'ы" -- достаточно произвольную строку "ИМЯ1.ИМЯ2...ИМЯn" маппировать на "настоящий" канал. И, если такие alias'ы будут иметь приоритет перед обычными именами -- можно будет сверх- (избыточно-?) гибкий механизм "перенаправления".
      • Программам должна предоставляться возможность узнавания "мета-информации" -- к какому блоку принадлежит канал N, тип блока B (чтобы определить, какую программу-осциллограф использовать), и т.п. операции запроса/traversal'а дерева аппаратуры.
    • Библиотеки:
      • cxlogger содержит вызов "переоткрой все log-файлы" -- нужно для logrotate. (@01.04.2006)
      • Работа с "текстовым представлением времени" (@06.08.2006):
        1. Преимущественно используются stroftime(), strcurtime(), на крайняк -- собственная "locale-independent" замена для ctime(); сама же ctime() не используется НИКОГДА!
        2. Для используемой программно "маркировки" временем применяются просто числа -- результат time(). Безо всяких попыток парсить строку, когда-то созданную ctime()'ом.
    • Система сборки
      • Везде указываются имена ЦЕЛЕВЫХ файлов, без расширений -- никаких "$(zzzSOURCES)" -- едиными для всех директорий "$(EXES)" etc.; плюс поддерживаются подкомпоненты -- см. идеи за 04.05.2005 и 14.06.2005 соответственно.
      • Способность к кросс-компиляции.
      • Работа в 3 вариантах: внутри дерева, вне дерева, и в инсталлированном варианте.
  • 28.06.2007: немножко о типах -- в cx.h.

    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 сделан ОДНОбайтовым?

    • Поскольку всё остальное в основном int32, то экономии по факту никакой (ибо паковать не с чем и потребуется добивка 3 байта).
    • Если же подселять к нему 3-байтовое количество единиц -- то тоже смысла мало, из-за ограничения 2^24=16777215~=16e6, т.е. 16 мегабайт (или столько же int'ов), а оно выглядит чревато.

    И вообще, а когда этот тип появился? Похоже, по результатам обсуждений касательно карботрона 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: о программе действий.

    22.07.2007: программа такова:

    1. Новые клиентские библиотеки, но с реализатором протокола CXv3 -- cda_d_cxv3.c (и с соответствующим cxlib'ом).

      Это позволит прямо сейчас, не трогая сервер и протокол, дать возможность "писать программы самим физикам" -- т.е., просто составлять группировки в текстовом виде.

    2. Новый сервер с реализацией также протокола CXv3 -- cxsd_fe_cxv3.c.

      Так можно будет прозрачно начать заменять сервера по очереди.

    3. И, наконец -- протокол CXv4.

    (А по ходу дела можно будет добавить и cda_d_epics.c, тогда программы смогут доступаться и к гусиным данным.)

    Это позволит получать выгоду с нового кода прямо по мере его появления, а не дожидаться завершения ВСЕГО пакета CXv4.

  • 05.09.2019@утро-душ: а не сделать ли, чтобы считались совместимыми типы CXDTYPE_TEXT и CXDTYPE_INT8/CXDTYPE_UINT8?
    • Смысл -- чтоб можно было, как в Си, использовать взаимозаменяемо массивы байтов и строки.
    • Побудительный мотив -- чтоб можно было массивы сбагривать cdaclient'у максимально короткими последовательностями, вроде "ABC\x15" вместо {65,66,67,21}.
    • Идея возникла при обдумывании того, как бы из беркаевской tvcapture максимально просто отдавать данные в CX'ный "почтовый ящик".

      Поскольку в 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 (т.е., БАЙТОВ)).

    Проект:

    • Принято решение унифицировать не просто UINT8 с TEXT, а UINT32 с UCTEXT, а любой REPR_INT с REPR_TEXT при условии совпадения размера.
    • Тем самым мы снимаем вопрос о знаковости.
    • И надо, чтобы при таком сочетании типов копирование бы выполнялось в ветке "Simple copy?", где делается просто memcpy() -- это автоматом снимет проблему "а что с {R,D}-conversion?".

    Реализация:

    1. Начинаем с cda_dat_p_update_dataset().
      • Анализ кода показал, что, как и было замечено РОВНО полгода назад, там целая цепочка if()'ов, так что вроде бы непросто: условие "это преобразование INT<->TEXT" должно фигурировать в 2 местах.
      • Но напросилось решение: вводим флаговую переменную 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'ом мониторировать байтовые каналы как текстовые и наоборот, и оно печатает данные именно в запрошенном виде. Ура!!!

    2. Второе место в cda -- это DoStoreWithConv(); но тут неясно, что бы можно сделать:
      • При REPR_TEXT (точнее, если НЕ REPR_INT и НЕ REPR_FLOAT) флаг DO_RD_CONV и так сбрасывается.
      • Хотя флаг NTVZ_DTYPE не трогается.

        Но, судя по отсутствию проблем ранее, он вроде как и не при делах.

        А, во -- нашёл!

        • Например, cdaclient использует cda_snd_ref_data(), а тот для REF_TYPE_CHN вызывает SendOrStore(,,,,DO_RD_CONV) -- т.е., никакого NTVZ_DTYPE.
        • Флаг NTVZ_DTYPE же используется ТОЛЬКО cda_process_ref() -- т.е., интерфейсом, принципиально принимающим на вход только double, и никак ни TEXT, ни даже INT сгенерить не могущим.

      Вывод: тут ничего делать НЕ надо.

    3. Далее отправка в сервере к драйверу -- StoreForSending().

      Тут проверка на совместимость отделена от собственно отправки, и выполняется в IsCompatible(), вызываемой из ConsiderRequest().

      Соответственно, и реализация состоит из 2 частей:

      1. Добавляем в IsCompatible() дополнительную OR-альтернативу, идентичную значению is_text_int_compat выше.
      2. А в StoreForSending() к блоку "Identical" также добавлена такая же OR-альтернатива, но для красоты всё условие отрефакторено, т.к. в обоих случаях там обязательным является совпадение размеров, так что сейчас оно выглядит так:
        ssiz == size  &&
                (srpr == repr  ||
                 (srpr == CXDTYPE_REPR_INT   &&  repr == CXDTYPE_REPR_TEXT)  ||
                 (srpr == CXDTYPE_REPR_TEXT  &&  repr == CXDTYPE_REPR_INT))
        
    4. И, наконец, ReturnDataSet()... Анализ:
      • Он очень похож на cda_dat_p_update_dataset(), ...
      • ...только альтернатива "Incompatible" тут не первая, а последняя (это следствие #if'ов на MAY_USE_FLOAT), что даже упрощает дело --
      • поскольку первой идёт альтернатива "Identical", ...
      • ...имеющая условие ровно такое же, как БЫЛО в StoreForSending().

      Вывод из анализа: да просто заменяем условие ровно таким же образом.

    Итого -- вроде сделано. Теперь надо тестировать все 3 ветви (ну ладно, оставшиеся 2).

    05.03.2020: P.S. Замечание: эта реализация конверсии между char[] и byte[] ставит крест на идее "а сделать бы конверсию между числами и строками" (см. за 23-10-2012).

    • @вечер, дорога домой вдоль Лаврентьева: можно сделать хакчок: у нас ведь применительно к REPR_TEXT флажок USGN_MASK всё равно бессмыслен, так? Ну так и использовать флажок "беззнаковость" для указания, что эту строку -- char[] -- не надо пытаться преобразовать в массив byte[], а можно попробовать пройти дальше по цепочке возможных преобразований -- до конверсии строка->lookup-enum.

      06.03.2020: посмотрел в коде console_cda_util.c::ParseDatarefSpec() -- флаг '+' НЕ применяется к не-INT-типам (к ним не плюсуется is_unsigned_mask). А в cxsd_db_via_ppf4td.c::ParseChangroup() попытка указать '+' и вовсе приводит к ошибке (впрочем, реальный ТИП такой указывать незачем, так что эта часть иррелевантна). Но кто мешает это поведение исправить?

    • @вечер, дома: есть ещё вариант, не требующий извращений с USGN_MASK: можно учитывать nelems. Ведь преобразование "строка->число" подразумевает 2 особенности:
      1. Целочисленный канал -- обязательно скаляр (enum-вектора смысла не имеют).
      2. Исходные же строковые данные -- наоборот, векторны: обычно enum-строки являются словами, а не одиночными символами.

      Соответственно, можно использовать простенькое "угадывание". Тем более, что это "угадывание" будет производиться уже перед выводом о несовместимости типов, так что дополнительная как бы потеря производительности тут будет малозначимой.

      Впрочем, ГЛАВНАЯ проблема на данном пути -- то, что у нас на уровне cxsd и cda нету никаких enum/lookup. Т.е., разговор беспредметен.

Chl:
Общие вопросы:
  • 23.07.2007: сюда ли?!?!?!об "application name" и "application class", или о курице и яйце...

    23.07.2007: постановка проблемы:

    • Раньше, при OpenDescription() по argv[0], 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@пляж: пара идей:

    1. А можно ли указывать app_name/app_class в виде "ФИКСИРОВАННЫЙ_ПРЕФИКС.КОНКРЕТНОЕ_ИМЯ" -- т.е., чтоб resource manager всегда использовал одинаковый префикс, независимо от конкретного имени программы, а для cx-starter'а они б были различимы?
    2. Еще можно попробовать как-то mangle'ить argv[] еще ДО обработки его Xh/Xt'ями.

      Например, понимать ключи "-class NNN" или в варианте "/class NNN" -- требуя, чтобы они были в начале.

    20.08.2015@вечер: а еще

    1. можно в pult'е проверять argv[0] ДО вызова XhInitApplication(), и если оно !="pult", то выполнять загрузку группировки прямо там -- тогда можно будет в Xh/Xt подсунуть уже конкретные известные app_name/app_class.

      Поскольку "клиенты" cx-starter'а будут запускаться через symlink'и в pult/bin/, то для них это вполне сработает. А то, что запускается руками через "pult ИМЯ.subsys" -- тем и неважно, их starter'у искать не надо.

    21.08.2015: смотрим по пунктам.

    1. По п.1 -- похоже, не получится. Поэкспериментировал (RH-7.3, XFree86-4.2.0) с xterm'ом, который понимает ключ -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-символы -- '!', ',', ';' -- на здоровье, разрешены. Но для наших целей от этого ни горячо, ни холодно.

    2. По п.2: а зачем? Ключ -name NNN и так Xt слопает (хотя CX'ные утилиты его почему-то игнорируют). А ключ -class NAME Xt не понимает (видимо, это чисто xterm'овская фишка. А указывать эти ключи -- лишний геморрой, которого как раз бы и не хотелось.

    21.08.2015: а вот п.3 выглядит разумно. И даже более того: если argv[1] выглядит именем файла (как минимун -- НЕ '-'-ключом), то чё б не попробовать заиспользовать его сразу?

    16.11.2015: сделано!!! По проектам п.3 и "если argv[1] выглядит именем файла...".

    Сделано просто "чтоб работало", а с красотой кода изрядные проблемы -- ОЧЕНЬ криво и непоследовательно. Так что надо будет еще:

    1. Украсивить (вопрос не столько кодирования, сколько алгоритм построить вменяемо).
    2. Эту же схему использовать и в прочих программах -- как минимум в stand.c.
  • 13.08.2007: некий вопросик -- а К ЧЕМУ привязывать private-структуру Chl -- к XhWindow (как и раньше) или к subsystem?

    13.08.2007: с одной стороны, несравненно красивее привязка к подсистеме -- при этом не приходится делать дурацкую трансляцию knob->окно->структура. И средства для этого теперь есть -- get_knob_subsys().

    С другой же стороны, привязка к XhWindow позволит делать многооконные программы -- с НЕСКОЛЬКИМИ grouping'ами, каждое из которых имеет собственное окно и собственный набор служебных окошек.

    Так что -- пока оставим привязку chl_privrec_t к XhWindow.

    21.08.2015: по факту, сейчас (18-08-2015) сделана привязка к subsystem. Кривовато, конечно -- это там как бы "ad hoc", а не идеологически -- но всё же.

    • С одной стороны, тем самым признаём, что многооконность с разными XhWindow реально нафиг не нужна.
    • С другой стороны, если припрёт -- то ничто не мешает как-нибудь отдельно привязывать каждое XhWindow к конкретной группировке, наверное?
Chl_app:
  • 21.05.2008: добавил в ChlRunSubsystem() использование DSTN_WINOPTIONS -- парсинг плюс использование флагов notoolbar, nostatusline, compact и свежесделанного в CXv2 resizable.
  • 18.01.2016: делаем поддержку toolbar'а.

    18.01.2016: пока в простом варианте -- без указывабельности содержимого клиентом, чисто в интересах pult (и для отладки clientside-сохранения/чтения режимов).

    • Содержимое пока определено в локальном stdtoolslist[].
    • В CmdProc() broadcast'инг команды по группировке был и раньше, а теперь мы его дополняем реакцией на стандартные действия, вызываемой, если команда НЕ была обработана (т.е., ручки в дереве могут команду перехватить).
    • Для унификации стандартные команды #define'атся в Chl.h под именами CHL_STDCMD_nnn, сами строки имеют префикс "chl".
    • Кстати, это первое изменение в Chl.h с 31-08-2008.

      Раньше в нём были только ChlRunSubsystem() да ChlLastErr().

    19.01.2016: продолжаем:

    • Обработка стандартных команд вытащена в отдельную ChlHandleStdCommand(), чтоб программы могли вызывать её из своих обработчиков.
    • К ChlRunSubsystem() добавлены параметры:
      1. toolslist -- даёт возможность программе указать своё содержимое toolbar'а. При ==NULL используется stdtoolslist[].
      2. commandproc -- сохраняется в privrec'е (09.02.2016: как upper_commandproc) и дёргается в Chl_app.c::CmdProc() между broadcast'ингом и вызовом ChlHandleStdCommand().

        Это и есть возможность программам ставить свои обработчики -- ранее её не было.

    • Заодно для унификации и тип XhCommandProc сделан возвращающим int вместо былого void (но сам Xh результат, естественно, игнорирует).

    09.02.2016: сделана Chl'ная часть сохранения/чтения режимов, творческим копированием из v2.

    • Работа с командами -- {SAVE,LOAD}_MODE и их _ACTION'ами -- в ChlHandleStdCommand().
    • Поддерживаются только "nfn"-диалоги, не-nfn не копировались.
    • В privrec добавлена структурка stddlgs_rec_t, содержащая пару CxWidget'ов для диалогов.
    • Сделаны ChlSaveWindowMode() и ChlLoadWindowMode(), являющиеся переходниками к Cdr.
    • Скопирован ChlRecordEvent().
    • --CdrStat и вообще в Cdr начато
  • 27.07.2016: кстати, надо бы по возможности ВЕЗДЕ использовать toolbar с маленькими кнопками, чтоб не занимать лишнее место на экране.

    28.07.2016: некоторые комментарии:

    • Лучше всё-таки иметь возможность указывать малость/крупность. Т.к. для одиночных "стендовых" программ удобнее крупные кнопки, а для множественных пультовых -- мелкие.
    • При маленьком тулбаре надо б создавать его с каким-то иным именем виджета -- чтобы поля были поменьше (вообще нулевые), а также элементам LABEL шрифт помельче.

    02.08.2016: в ту же степь: а ведь лучше было бы, чтоб у каждой строчки могла указываться как крупная пиктограмма, так и мелкая, и отдельно указывалась бы крупность/мелкость тулбара, а использовалась бы по возможности нужная пиктограмма (при её отсутствии -- другая).

    В xh_actdescr_t такого поля нет, но, поскольку формат всё равно отличается от v2 (из-за строковости команд), то можно спокойно это поле добавить.

Chl_knobprops:
  • 14.04.2014: начальный вариант Chl_knobprops.c был сделан еще где-то в 2007-м, а сейчас появилась надобность развивать дальше и исправлять, так что заводим тут раздел.
  • 14.04.2014: ссылки на источники (Source/Write/Colrz) сейчас показываются просто из соотв.полей *_src, а это не вполне корректно: ведь при realize туда еще префикс/база конкатенируются.

    Правильнее будет показывать то, что у ссылки (ref) лежит в ri->reference. Для чего в cda надо завести соответствующий вызов.

    05.05.2014: чуть хитрее: может быть ситуация, когда в ref НИЧЕГО не лежит -- если ссылка "ошибочная" (CDA_DATAREF_ERROR). Например, при указании несуществующего local::-канала.

    В таком случае НАДО отобразить именно "исходник", с какой-то пометкой "невалидна". Т.е., 3 варианта, в зависимости от ref:

    1. CDA_NONE -- ничего нет.
    2. CDA_DATAREF_ERROR -- показать _src (хотя оно будет и без реального префикса).
    3. Иначе -- показать содержимое 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
    • либо !=NONE и при этом взведён CXCF_FLAG_NOTFOUND,
    то в u.k.curstate ставится KNOBSTATE_NOTFOUND (заменяя там умолчательное JUSTCREATED), и этого достаточно -- ведь именно по этому состоянию и будет сделано первоначальное расцвечивание при создании ручки.

    Теперь прекрасно чернеет с самого начала.

    18.02.2015: вопрос на 9 месяцев назад: а ПОЧЕМУ, собственно, "НЕ возвращать на CDA_DAT_P_ERROR-каналы CDA_DATAREF_ERROR"?

    • Как минимум cdaclient'у это правило жизнь портит изрядно -- нет возможности определить, что ссылка заведомо битая.
    • Да и вообще странно -- получается неравноправие между частями регистрации канала в 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() -- там эта проблема отсутствует, поскольку никаких плагинов не предусмотрено.

  • 14.04.2014: надо б менять способ ввода диапазонов: вместо отдельного окошка, вызываемого по [...] (сейчас его еще не сделано) класть пару полей ввода [min]-[max] прямо в строчку окна Knobprops.

    При запрещенности смены, естественно, просто показывать диапазон как и сейчас.

    28.06.2014: да, переделываем, по образу и подобию реализованного вчера в v2 (только уже безо всяких #if RANGES_INLINE, а сразу).

    Заодно, кстати, выкинуто fqd_inp ("Graph frqdiv") -- давно ясно, что сама концепция изжилась.

    15.01.2015: сделана отработка кнопки [Ok], с надлежащим сохранением в u.k.params[].value и взведением тамошних флагов .modified.

    Пока только для диапазонов, шага и grpcoeff. А надо б еще для прочих параметров.

    Плюс, пока не сделано:

    1. Вызывание метода PropsChg().
    2. Уведомление histplot'ов -- а это вообще неясно, как реализовывать, их же не один.

    16.01.2015: в продолжение вчерашнего:

    • И сохранение изменённых параметров тоже сделано, с надлежащим modified=1.
    1. Вызов PropsChg() добавлен -- скопировано из Cdr'ного RefStrsChgEvproc() (но БЕЗ вызова parent'ова ChildPropsChg() -- тут оно ни к чему).
    2. Про "уведомление histplot'ов" -- стало ясно: они ж будут (как осознано сегодня ;)) все регистрироваться у Chl'я в списке, чтобы быть вызываемы для перерисовки при обновлении раз в секунду -- вот ровно по этому же списку (и точно так же!) и вызывать.

    12.11.2015: вылез косячок "новой" (28-06-2014) схемы отображения диапазонов: при /fixedranges (DATAKNOB_B_RANGES_FXD) диапазоны не показываются вовсе, что неприятно. А желательно бы показывать, но, например, просто нередактируемыми полями (просто editable:=False, БЕЗ sensitive:=False).

  • 01.12.2014: сделано отображение timestamp'а канала. С миллисекундами.

    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: сделано, что бредовые отрицательные возрасты (возникающие при несовпадении настроек таймзон на клиенте и сервере), тоже отображаются -- вместо былой пустоты, чтоб понимать, что происходит.

  • 10.05.2015: добавлены строки для отображения comment и units.

    Понадобилось для проверки работы модуля cda_d_vcas.c

  • 16.12.2015: добавлена строка "{R,D}s", для разборок с проблемой от 04-12-2015. Формирование текста там довольно муторное, с хитрым форматированием (нули выдаются как "0.0", целые как "NNN.0") и защитой от переполнения буфера.
  • 27.04.2016: отображение "Raw" для вариантов SINGLE/DOUBLE и "{R,D}s" переведено с "%f" на "%g".

    Причина -- vsdc2 в GID25 на ВЭПП-4/К500: там и исходные числа (single) от девайса мелкие (e-8), и r, на который они делятся тоже (e-7), поэтому при %f они отображались нулями. А %g специально сделан для таких случаев, он сам выбирает наилучший формат (%f или %e, в зависимости от экспоненты).

  • 11.05.2016: добавлена строчка "Path" -- без неё тяжковато пользоваться histplot_noop'ом.

    11.05.2016: в основном скопировано с v2. Тонкости/отличия:

    • Имя верхнего (корневого) узла не включается -- т.к. при поиске оно никак не используется.

      Замечание: поиск histplot_noop'ом делается через datatree_find_node(), которому указывается адрес ИЩУЩЕГО, а не корень (хотя его тоже можно).

      В любом случае -- имя корневого узла не используется никак.

    • Имена контейнеров ":" отдельно не проверяются -- т.к. в настоящий момент "прозрачность" таких имён в поиске не реализована.
    • Также НЕ проверяется пустость имён -- а надо бы (сейчас -- надо б наравне с ==NULL), т.к. они в качестве последнего компонента "пути" они показываться не будут (проверка "Pre-pend '.' if not at EOL"), но на поиске отразятся.
  • 25.07.2017@утро-дома: хорошо б уметь показывать "общие" (не-типоспецифичные) свойства для не-KNOB-узлов (TEXT и VECT). В первую очередь, интересна строчка "Source".

    А неактуальные -- показывать так, словно их нету (оно так и у KNOB'ов бывает).

  • 12.04.2019: вылезла проблема, что при клике MB3 по ручкам, у которых 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().

Chl_bigvals:
  • 15.01.2015: модуль реализован, творческим копированием из v2 с минимальными модификациями; общая схема по образу и подобию Chl_knobprops.c.

    Обновление сделано так же, раз в 1 секунду.

  • 12.04.2019: пофиксен SIGSEGV в _ChlShowBigVal_m() (аналогично KnobProps'ову).
Chl_help:
  • 20.01.2016: создаём модуль. Просто копированием из v2, с минимальными модификациями под v4'шную инфраструктуру Xh/Chl, плюс раскомментирована строка цвета NOTFOUND.

    Ничего продвинутого -- что задумывалось 04-03-2006, с текстовым описанием и возможностью указывать своё в subsys-файлах -- не сделано (имевшаяся в v2'шном часть при копировании удалена). И, вероятно, никогда не будет сделано -- надобности такой нет из-за общего смещения в сторону питонопрограмм.

  • 05.06.2020: текст help'а переведён на английский, чтобы не иметь проблем с Raspberry, Xming и прочими местами, где нет русских шрифтов.

    Старый вариант оставлен, но закрыт #if'ом по MAY_USE_RUSSIAN_KOI8.

    Сделано одновременно и унифицированно с cx-starter'ом.

:
KnobsCore:
Общие вопросы:
  • Замечание: дата создания Knobs_typesP.h -- ~=16.04.2005.

    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:

    1. собственно электронного блока и ...
    2. управляемого устройства (например, ЦАП и магнит, ЦАП и термостабилизируемая секция).

    Видимо, надо там иметь какой-то разделитель, через который и указывать. ',', ';' и '/', скорее всего, будут заняты, но вот '|' вполне может оказаться свободным.

    18.06.2007: содержимое Knobs_typesP.h переехало в datatreeP.h, так что тут раздел закрываем.

    29.07.2007: и вообще старые файлы Knobs_typesP.h* удалены. Если понадобятся -- последняя версия есть в star:viper-archive/20070727.tar.bz2.

  • 18.06.2007: в связи с сегментацией libKnobs на "ядро" и конкретные реализации ручек этот раздел переименован из "Knobs" в "KnobsCore", а отдельно добавлен "MotifKnobs".
  • 18.06.2007: делаем "ядро" -- KnobsCore_knobset.c с функциями CreateKnob() и RegisterKnobset().

    18.06.2007: сделано, highlights таковы:

    • Оно понимает спецификацию-список вида "тип1,тип2,...".
    • Поскольку ядро отделено от конкретных реализаций, то при ненахождении указанного вида оно берет просто ПЕРВУЮ найденную ручку указанного типа. 28.06.2007: уже не так!

      Следствие: "умолчательный" вид (text для KNOB'ов, grid для CONT'ов) должен быть первым в списке.

    • Указание look "" или NULL приводит именно к виду по умолчанию.
    • В качестве KNOB'ов подхватываются и NOOP'ы.

      Следствие: декорации должны идти в списке ПОСЛЕ настоящих ручек.

    • Оно умеет само аллокировать privrec и даже PSP-парсить в него содержимое options.

    О VMT:

    • Теперь имя вида живет прямо в VMT, а не в set'е. Естественно -- вместе с типом.

      Т.е., реально VMT -- это скорее "описатель".

    • Методы Create() и Destroy() объявлены прямо в "универсальной" dataknob_unif_vmt_t, которая во все остальные типы VMT входит отдельным полем.
    • В ней есть даже поле ref_count.
    • Механизм подсовывания ядру умолчательного набора таков: в KnobsP.h объявлен указатель
      extern knobs_knobset_t *first_knobset_ptr;
      а уж реализация должна реально этот указатель заимевать (т.е., уже БЕЗ extern'а), уставляя его в ссылку на свой набор.

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

    • Концом набора, как и раньше, является VMT с 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,..." был сделан криво: нормально оно хватало только первый вариант, поскольку перед следующей итерацией забывало перепрыгнуть через ','.

  • 18.06.2007: поскольку мы хотим иметь полный динамизм в программе, кроме создания ручек надо также их уметь и прибивать. Это будет делать вызов DestroyKnob().

    19.06.2007: сделано. Для контейнеров также рекуррентно вызывается и DestroyKnob() для потомков -- так что для гроханья целой иерархии достаточно грохнуть корневой узел.

    Схема вызова -- сначала грохаются потомки, а уж затем вызывается Destroy() самой ручки. Порядок взят от XtDestroyWidget().

    Плюс, поскольку CreateKnob() теперь умеет сама аллокировать privrec и psp-парсить его, то при гроханьи оно освобождается, при надобности с предварительным psp_free().

  • 21.06.2007: в CXv2 "декорирование" ручки -- навешивание tooltip'а, "вычитывание" deffg+defbg и начальная колоризация -- делались функцией 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).

  • 21.06.2007: остается некоторый вопрос -- ведь в CXv2-то в "методах элемента" было смешано несколько разных сущностей:
    1. Специфичные-для-вида вещи -- создание и раскладка "детей", работа с алармами, и т.п.
    2. Отдача уставки в физический канал -- SetPhys().
    3. UI-задачи, общие для всех типов элементов -- ShowProps(), ShowBigVal(), ToHistPlot().

    Если 1-й пункт еще действительно целиком в компетенции lib*Knobs*, то 2-й -- фиг знает в чьей, а 3-й -- уж точно для Chl'я.

    Но: vmtlink-то -- один! И технология "просто вызова из таблицы, возможно -- пройдя вверх по дереву" -- очень удобна.

    Что делать будем?

    02.07.2007: угу, по-правде-то, с учетом, что поддержка разных-протоколов-уставки-данных уже будет в cda, все эти методы стоило бы иметь только на верхнем уровне -- у группировки. Но:

    1. В любом случае, стоит вопрос: раз корнем дерева может быть произвольный контейнер (хоть grid, а хоть и вовсе ручка?!), то -- как же (куда) прописывать эти методы-специфичные-для-программы?

      Создавать еще один синтетический контейнер, что ли? Которого бы реально не было, но чтоб uplink корневой ручки показывал бы на него, и уж в ЕГО VMT были бы эти методы... Видимо, именно так...

    2. Вне зависимости от этих разных-протоколов-... для simple-knobs нужен именно метод-контейнера SetPhys, который бы вызывал callback.
    3. ...что ж 3-е то было? Йо-о-о...
  • 27.07.2007: сходу -- вытащил поиск VMT по look'у в P-публичную функцию GetKnobLookVMT(). Чтоб всякие tree-knob'ы могли этим пользоваться, как записано в bigfile-0001.html за 24-04-2006 и за 14-07-2007.

    04.12.2013: эта функция переделана с "отвлечённой" -- принимающей пару параметров (type,look) -- на "приземлённую" -- теперь принимает прямо ссылку на ручку.

    Основной смысл -- чтоб при ненахождении VMT она могла б сама АДРЕСНО ругаться об этом на stderr (что делается только при непустом look).

  • 31.07.2007: забавненько -- а ведь XtDestroyWidget()-то у нас вообще нигде и никогда ни при каких обстоятельствах не вызывается!!!

    31.07.2007: два аспекта:

    • Во-первых, предполагается, что метод Destroy() может отсутствовать -- ручка ничего такого сама не аллокирует. И уж тогда-то, казалось бы, гроханьем виджета должен заниматься KnobsCore_knobset.c::DestroyKnob(). Но -- он этого делать НЕ МОЖЕТ, поскольку он не имеен никакого понятия ни о какой Xt, да и вообще о том, каким GUI toolkit'ом пользуется конкретная реализация!!!
    • Во-вторых, ручка может ПОЛУ-создаться -- например, контейнер создал себя, а потом попер создавать под-ручки и там облом. А ведь при этом сразу же будет сделан "return -1", и сам контейнер плюс часть уже созданных виджетов ручек останутся!

    Выход таков:

    1. CreateKnob() при обломе пусть вызывает DestroyKnob().
    2. А уж методы 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".

  • 06.01.2011: data_knob_t.conns_u была int8*, хотя использование везде -- uint8*. Исправил (см. bigfile-0001.html от 16.12.2010).
  • 11.08.2012@пляж-авто: надо бы сделать в Chl-клиентах фичу "поиск ручки по имени" (можно и по иным свойствам). (Это по мотивам мыслей о системе конфигурации -- zoomview+дерево+поиск.)

    А чтоб это работало (активировало ручку, хлопая по ней желтым прямоугольником), надо уметь типоконтейнеро-независимо включать отображение потенциально скрытых частей. Это касается subwin'а и tabber'а (плюс свёрнутости обычных контейнеров).

    Можно ввести метод SetVisibility(k,state,child), которому указывается требуемое состояние плюс (опционально) кого активировать (нужно tabber'у для выбора закладки). (Метод, естественно, НЕ-up'абельный, а персональный.)

    13.08.2012: подготавливаем: введен тип _k_setvis_f и поле dataknob_cont_vmt_t.SetVis. И повсюду оно сделано =NULL.

    17.11.2013: see also "О программной управляемости cont-плагинов"/30-03-2011.

  • 16.08.2015: пришлось в KnobsCore_knobset.c вставить ссылку на KnobsCore_simple.c -- конкретно CreateSimpleKnob().

    16.08.2015: смысл -- чтоб они линковались вместе. Иначе -- при реализации MotifKnobs_histplot.c -- возникла проблема "курицы и яйца": в каком порядке указывать библиотеки?

    • В текущем варианте -- $(LIBKNOBSCORE) $(LIBMOTIFKNOBS) -- получалось, что
      1. из KnobsCore брался _knobset, а _simple -- нет, поскольку предыдущими (сама xmclients-программа и Chl) он не использовался;
      2. идущий же потом MotifKnobs_histplot её хотел, а взять уже негде!
    • Если поменять местами -- то тоже проблема: из MotifKnobs не брался _knobset, содержащий first_knobset_ptr, необходимый KnobsCore_knobset'у.
KnobsCore_simple:
  • 30.06.2007: приступаем к созданию этого модуля -- нужен хотя бы для того, чтобы испытывать создаваемые реализации ручек (поскольку в отсутствие Cdr-конвертеров иначе просто никак).

    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'е, потом переходит в свежерождённую ручку, и освобождено будет уже при её конце.

  • 29.10.2012: первое изменение с 06-08-2007 -- добавляем в CreateSimpleKnob() параметр options, единственный пока поддерживаемый бит -- SIMPLEKNOB_OPT_READONLY; всё идентично v2.
KnobsCore_err:
  • 17.07.2007: создан новый модуль -- для предоставления текстового (human-readable) описания последней ошибки.

    17.07.2007: всё хорошенько описано в bigfile-0001.html за 25-01-2007. Именно так и сделано.

    Возвращает последнюю ошибку публичная функция KnobsLastErr(), а поддержкой занимаются определенные в KnobsP.h вызовы KnobsClearErr(), KnobsSetErr() и KnobsVSetErr().

    Реализаторы, например, в MotifKnobs будут пользоваться именно этим вызовом.

    Сам этот модуль может служить модельным для аналогичных модулей других библиотек.

:
MotifKnobs:
Общие вопросы:
  • 21.06.2007: сегодня начато создание библиотеки.

    Вопрос архитектуры -- общее устройство данной библиотеки, чтобы было удобно. (И другие реализации могут иметь похожую структуру -- просто она должна быть достаточно разумной и гибкой.)

    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: надо б как-то научиться указывать параметры отдельным КОЛОНКАМ сетки, а возможно -- даже и СТРОКАМ. Основное назначение -- возможность указать колонке флаг "folded" (см. bigfile-0001.html за 17-05-2005).

    16.07.2007: как сие делать -- в принципе, ясно: чтоб после тултипа, также через MULTISTRING_OPTION_SEPARATOR, можно было указывать и эти опции. Просто надо это постулировать как стандарт.

  • 02.10.2014: добавлена отдельная библиотечка libMotifKnobs_cda.a. Сейчас -- для житья в ней cda_scenario_knob.

    02.10.2014: ссылаться в makefile'ах -- $(LIBMOTIFKNOBS_CDA), для регистрации её плагинов надо вызвать MotifKnobs_cdaRegisterKnobset(), объявленную в MotifKnobs_cda.h.

  • 21.06.2015: в MotifKnobsP.h добавлено #include<stdio.h>. Хбз, как без него работало (колёса вылезли под Linux Mint); в v2 оно было в Knobs_includes.h, тут отсутствующем.
  • 12.02.2016: сильно не хватает "style" -- чтоб выделять на экране некоторые ручки, как это было можно с v2'шным color. Например, в linmag'е канал, управляющий открытостью/сбросом пучка.
  • 22.08.2016: кстати, MotifKnobs_AllocStateGCs(), в v2 использовавшаяся, тут просто пустая.

    И, судя по вчерашнему опыту wirebpm_gui, она вообще и не понадобится.

  • 03.12.2018: Беркаев высказал пожелание, чтоб в ringmag'е можно было б поля "шаг" вытащить прямо на морду скрина.

    И даже больше: чтоб они были каналами, и были бы общими для всех клиентов и могли бы сохраняться в режимы.

    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 указано, то:

    1. Регистрировать (Cdr'у!) указанный канал с флагом ON_UPDATE.
    2. Т.е., реагировать на изменение значения СРАЗУ, без привязки к циклу.
    3. Реакция заключается к прописыванию полученного значения в ячейку PARAM_STEP.
    4. ...и как-то ведь нужно обновлять значение в окошке knobprops...
    5. А по [OK] в окне knobprops кроме сохранения в ячейку также выполнять и отправку значения в "st_ref".

    30.01.2019@~15:45, по дороге с лыж в ИЯФ, после поворота налево около "Карасика": а можно вообще КАЖДОМУ параметру дать возможность иметь внешнюю ссылку; т.е., просто в CxKnobParam_t добавить некий "CxDataRef_t ext_ref". (Как хорошо, что тип CxDataRef_t как раз определён в cx_common_types.h же!)

    Краткое обсуждение:

    • И где-то ж надо будет брать src; туда же в CxKnobParam_t добавлять "char *ext_src"?
    • Тогда заполнение ext_src и ext_ref тогда свалится на Cdr_file_via_ppf4td.c и Cdr_treeproc.c, соответственно.
    • А "ловить" изменения таких параметров -- по evproc'у, которому в качестве privptr2 ЧТО передавать? Надо ж "дуплет" -- {DataKnob,param_n}...
    • И по получению такого события надо вызывать должное обновление -- чтоб и в окне KnobProps обновилось, и ручков PropsChg вызвался бы, а ещё _ChlAppUpdatePlots() не забыть.
    • И да, в случае ссылания на канал чтения будет постоянно всё "обновляться", преизрядно моргая...

    Не-а, неохота такое делать: шибко уж монструозно.

    21.10.2019@Морской-46, в подъезде, ~16:00: в ту же степь: ЕманоФедя ехидно поинтересовался -- а почему это в магнитной системе основная ручка *set и аналогичная ручка в под-окне [...] как будто бы РАЗНЫЕ? У них могут быть разные диапазоны, шаг, ...

    Напрашивается мысль: а не сделать ли, чтобы у ручки можно было указать "реальную ручку", чтобы

    1. Свойства (params) использовались бы оттуда, а не из самой ручки.
    2. И чтоб Chl_knobprops их тоже брал бы оттуда, и сохранял бы тоже туда же.

    Тогда:

    1. Ручке в под-окошке просто ставим ссылку на основную ручку.
    2. Если в колонке толпа "однородных" ручек -- ставим всем, кроме первой, ссылку на первую, и они сразу обладают общим ЕДИНСТВЕННЫМ набором параметров.

    Несколько замечаний по этой "гениальной идее":

    • Во время работы "реальная ручка" -- это, очевидно, просто ещё одно поле в dataknob_knob_data_t; например, param_k.

      И "редиректить" все обращения туда -- несложно. Что-нибудь типа

      (k->model_k != NULL? k->model_k : k)->
      хотя...
      • ...лучше, конечно, заранее, с дополнительными проверками (вроде k->model_k->type == DATAKNOB_KNOB) добывать сразу ссылку 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.

    • Очевидно, что ручки с указанным param_k должны работать как будто у них указано noparams.
    • И отдельный вопрос: а что с РЕКУРРЕНТНЫМИ ссылками? Что, если ручка A ссылается на ручку B, а та -- на ручку C?
      1. Брать значение param_k от B?

        А если та -- ПОЗЖЕ по иерархии, и у неё ещё неразрезолвлено?

      2. Просто проставлять ссылку на указанную ручку, игнорируя еёйное param_k?

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

    В качестве резюме:

    1. Такая фича в реализации существенно проще, чем "...вообще КАЖДОМУ параметру дать возможность иметь внешнюю ссылку...", хотя и тоже не сахар.
    2. А решением для задачм от 03-12-2018 («чтоб в ringmag'е можно было б поля "шаг" вытащить прямо на морду скрина») она всё равно не является.

    Так что, хоть данный путь и менее монструозен, такое делать тоже неохота.

    P.S. Философское: да, сделать-то МОЖНО, что то, что это. Но проблема в том, что элегантным решением ни одно из них не является; а архитектуру любое из них изрядно попортит. Вот и не хочется делать такое "кривое, но влияющее".

MotifKnobs_internals:
  • 30.06.2007: функции-утилиты для махинации с текстовыми виджетами -- 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: надо делать поддержку стилей. Примерно так, как прикидывал оное, шляясь по конференциям. Т.е. -- со строковым указанием свойств, с наследованием (от элементов выше по дереву), и с возможностью указывать просто имя одного из "стандартных" (или определенных в группировке) стилей вместо указания всех свойств поштучно.

    16.07.2007: технически для самих knob'ов это будет выглядеть просто -- функции Create_m() будут дергать некую стандартную функцию, передавая ей свой k, массив al[] с указанием его размера и со счетчиком al, плюс битовую маску интересующих опций (шрифт, цвета, толщина сепаратора, etc.). Та функция как-то эти вещи добудет, и складирует в al[], а уж вызывальщик может либо дополнить этот список своими параметрами и сбагрить его в XtCreateManagedWidget(), либо потом отдельно сделать с ним XtSetValues().

    12.12.2007: кое-что тогда, полгода назад, было обдумываемо, но так и не записано (поскольку отвлекся на совсем другое): менеджмент стилей должен быть ПОХИТРЕЕ, с точки зрения хождения по дереву и наследования.

    Исходные желания:

    1. Чтоб можно было указать параметры только для данного контейнера -- но чтобы они НЕ наследовались.
    2. Чтоб можно было указать параметры для этого и последующих КОНТЕЙНЕРОВ -- т.е., чтобы весь "фон" внутренности элемента выглядел единообразно (в основном, видимо, это коснется именно параметра "фон").
    3. Чтоб можно было указать свойства именно СОДЕРЖИМОМУ -- именно knob'ам, а не контейнерам.

      (Соответственно, всякие хитрые 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= срабатывали бы так же, как явно указанный набор их содержимого. И тут встают три вопроса:

    1. Как/где эти стили должны определяться -- быть предопределенными, или указываться в группировке? И если в группировке -- то КАК? (видимо -- спец.секциями, типа DSTN_KNOBSTYLE, а имя стиля и будет именем секции.)
    2. Соответственно, а где и как эта информация должна храниться? Ну ладно, "исходники" -- в спец.секциях, но надо ведь -- для скорости -- и "предкомпилированные"/подготовленные к использованию стили иметь.
    3. И -- следствие из предыдущих -- как именно это впишется в описанную стройную архитектуру парсинга-в-al[]?
  • 05.03.2009: еще насчет стилей: кроме собственно таких стилей всё-таки полезно будет иметь и нечто типа CXv2'шного "color" -- чтобы именно ПОВЕДЕНИЕ при раскрашивании менялось.

    Например, сделать поле dataknob_t.cclass ("colorization class"), добывать его точно так же при парсинге поля style, и использовать -- как былое knobinfo_t.color.

    13.06.2016@пляж: опять про колоризацию (ну о-о-очень надо):

    • Идея 1: ввести DATAKNOB_B_IMPORTANT. Но -- а VIC как?
    • Идея 2: в жопу style, вернуть v2'шный "color" под именем colz_style. Чтоб прямо параметром в subsys-файлах указывался.

    14.06.2016: делаем.

    • Добавлено поле data_knob_t.colz_style.
    • Набор констант DATAKNOB_COLZ_ -- NORMAL, HILITED, IMPORTANT, VIC, DIM, HEADING -- почти в точности, как v2'шные LOGC_nnn. Только начинаются с 0, а не с 1.
      • И после некоторых моральных терзаний решено было даже HILITED оставить -- хотя уж оно-то точно не "style", а просто другой цвет.
      • Но ведь и VIC, DIM, HEADING -- тоже в общем-то всего лишь цвета.

        Просто пока нет полноценных "стилей" (а будут ли), нужного эффекта иначе не добиться.

        А так-то -- да, вполне хватило бы DATAKNOB_B_IMPORTANT, чтоб при нём при колоризации использовались насыщенные желтый и красный вместо "обычных" бледных.

    • Парсинг в Cdr_via_ppf4td.c -- введён тип "COLZ".
    • MotifKnobs_ChooseColors():
      • Параметр "zzzzzz" переименован в colz_style.

        Замечание: в случае реализации-таки настоящих стилей этот параметр превратится в булевский флаг "is_important", беримый из _B_IMPORTANT.

      • Отличия в поведении от v2:
        1. COLZ_IMPORTANT никак НЕ расцвечивается, а влияет лишь на яркость при колоризации по диапазонам.

          Смысл -- чтобы подсвечивать некоторые каналы (в linmag'е) поярче.

        2. COLZ_VIC также считается за "IMPORTANT", т.е., колоризуется ярче.
    • Использование этого параметра:
      • MotifKnobs_ChooseKnobCs().
      • И в Chl_knobprops/Chl_bigvals тоже. И в MotifKnobs_histplot.
      • ...и в огромной толпе в разных knobplugin'ах.
      • А вот в fastadc_gui.c/vcamimg_gui.c'шных UpdateBG() всегда берётся _COLZ_NORMAL (и смысла в ином нет, и "importantness" на уровне _gui (а не _knobplugin) взять негде).

    15.06.2016@пляж-~10:30: еще некоторые мысли насчёт стилей.

    • Предыдущая более-менее полноценная запись о них -- bigfile-0001.html за 10-03-2006.
    • Но там не написано, что была мысль для содержащихся ручек "наследовать" стили от их содержателей, многоуровнево.

      А делать это -- с использованием списков 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 поле "указатель на информацию о стиле" и:

      • в _Create_m()'е контейнера прописывать в него указатель на его, контейнеровы, текущие известные данные;
      • вызывать создание child'ов;
      • прописывать в это поле NULL.
    • Немного о внутреннем устройстве:
      • Сами "стили" должны состоять из 3 параллельных сущностей (это тоже придумывалось еще в 2005-м, но записывалось ли в какой-нибудь файл -- неясно):
        1. "Общий" -- для контейнера и содержимого.
        2. "Контейнерный" -- только для оформления самого контейнера, он НЕ наследуется.
        3. "Содержимовый" -- используется только содержимым, самим же контейнером игнорируется.
      • Среди них должны действовать очевидные правила приоритета (как в CSS, да): если указано конкретное (контейнерово для контейнера, содержимовое для содержимого), то используется оно; если нет -- то используется общее; если нет ни того, ни другого -- то используются умолчания.
      • Как это реализовывать -- посмотрим по месту; наверное, как-то "последовательно заполнять" описатель, из которого будут браться данные для al[ac]:
        1. сначала прописать умолчание;
        2. потом, если есть, из общего;
        3. наконец, если есть -- из конкретного.

        Делать это надо так, чтобы корректно отрабатывались "smaller"/"larger" (см. ниже).

      • Низкоуровневые детали:
        • Очевидно, надо будет определить в MotifKnobsP.h тип-структуру, содержащую эту информацию, и состоящую из 3 одинаковых под-структур.
        • "Составление" информации о стиле конкретной ручки будет делаться компоновкой данных из той троицы в такую же структуру (простая алгебра).
        • "Умолчания", видимо, надо реализовывать при помощи флагов "указанности": изначально значение "неуказано", а при указывании прописывается значение и флаг неуказанности сбрасывается.
        • Использование: в al[ac] попадает то, что указано; а что неуказано (вроде цвета, размера) -- виджету просто не специфицируется.
        • Наследование: для под-содержимого берётся значение "Общего" от предка, к нему "добавляется, заменяя" значение "Содержимового" от предка, потом туда же накладываются местные значения "Общего" и "Содержимового".
        • Вывод: явно надо иметь функцию "AddStyleOverriding()".
        • Строка "style" должна psp_parse()'иться прямо в эту тройственную структуру.

          И надо как-то так подобрать "умолчательные" значения, чтобы они и работали бы "флагами неуказанности".

    • Для размера шрифта желательно иметь возможность указывать как абсолютные значения (tiny,small,normal,large,huge), так и относительные -- "smaller", "larger".
      • Как это можно сделать: а выбором соответствующих числовых констант, которые бы клались прямо через psp-lookup:
        1. tiny,small,normal,large,huge -- 1,2,3,4,5.
        2. "no_change" -- 0 (вот оно и "не указано").
        3. smaller,larger -- -11,-9. Реально это (после вычитания "базы" -10) является "сдвигом" от текущего размера, так что могут быть и smaller_smaller=-12 и larger_larger=-8.
      • Соответственно, "наследование" должно производиться арифметически, с проверками:
        1. 0 -- ничего не делать;
        2. >0 -- записать поверх текущего;
        3. <0 -- прибавить сдвиг, проверяя, что результат остался в границах [1...5].
    • 16.06.2016@ванна-после-пляжа: желательно также в стили загнать и параметры контейнера-сетки -- "hspc", "vspc", "colspc".

      Т.к. при указании размера шрифтов в иерархии будет желание также и межручечные расстояния там менять.

    16.06.2016: "свежеоткопанные" (сегодня) правила наследования стилей от 12-12-2007 выглядят даже интереснее свежесформулированных от 15-06-2016 (стареем, тупеем? :)).

    Надо б определиться, кого из них использовать (сама технология от этого никак не меняется -- только решения, кого кем override'ить).

  • 04.02.2011@Снежинск-каземат-11: пара соображений насчёт оформления контейнеров -- API MotifKnobs_CreateContainer():
    1. Нужна возможность помещать заголовок не только сверху, но и снизу/слева/справа, СТАНДАРТНЫМ образом, и чтоб при этом сохранялся функционал fold и nohline.

      А то в liu::kuznitsa удобно помещать заголовки слева, но пришлось их эмулировать VLABEL'ами.

      29.08.2011: в v2 titleat* сделано еще 18-04-2011.

    2. И не забываем о желании мочь вытаскивать часть ручек прямо в заголовок. Так что в API понадобится поддержка:
      1. Локального микро-тулбарчика с "Save", "Load", ...
      2. Собственно линейки ручек.

      И API должен будет САМ располагать кнопки и ручки в соответствии с положением заголовка (т.е., слева-направо для горизонтальных заголовков, и сверху-вниз для вертикальных).

      29.08.2011: в v2 nattl сделано еще 19-04-2011.

    04.02.2011@Снежинск-каземат-11: и еще в ту же тему:

    1. С учётом появления PSP_T_INCLUDE надо будет ввести ЯВНОЕ разграничение параметров: отдельно стандартно-контейнеровы и cont-specific. И, возможно, вместо флагов передавать уже сразу указатель на эту стандартную выпарсенную структурку?
    2. Потребуется какой-то аналог v2'шного параметра "one" -- т.е., контейнер, годящийся для ресайзинга (bigfile-0001.html за 21-07-2010).

      Можно и в сравнительно простом варианте (хоть вообще другим cont-типом, НЕ grid'ом), но, главное -- АТТАЧИТЬ child'а ко всем сторонам.

    15.07.2014: сделано за вчера-сегодня:

    • Пункты 1, 2, 4 (titleat*, nattl, one) скопированы с v2; число nattl берется из param3.

      Контейнер тулбарчика тоже создаётся, но наполнять его некому.

    • Пункт 3: стандартная часть вытащена в MotifKnobs_containeropts_t, а описание в text2_MotifKnobs_containeropts[], которое и INCLUDE'ится.

      Но параметры по-прежнему передаются словом флагов.

  • 29.08.2011: введена MotifKnobs_SetControlValue() -- по опыту v2, чтоб бибиканье было в одном месте.

    29.08.2011: ну и во всех *_knob.c на неё переведено с прямого вызова set_knob_controlvalue().

    В button и alarmonoffled это выглядит, конечно, странновато -- вряд ли ТАМ будут выходя за диапазон при нажатии, но, в принципе, вполне возможно, так что для унификации пусть будет.

  • 02.10.2014: введён стандартный MotifKnobs_NoColorize_m() -- чтоб всем желающим игнорировать колоризацию не городить свои. Но: средняя цена
  • 05.09.2016@Снежинск-каземат-11: вот "прям щас нужно!" иметь рудиментарную поддержку стилей оформления кнопчатостей -- для liu.subsys, в первую очередь для subwin'ов.

    Пока "настоящих" стилей нет, придётся делать хак, воспроизводящий функционал v2.

    05.09.2016@Снежинск-каземат-11: базовые постулаты:

    • Код возьмём просто один-в-один из v2 -- TuneButtonKnob(), с описателем Knobs_buttonopts_t.
    • Строку-описатель стиля будем брать из поля style (ибо нефиг портить содержимое options).
    • Чтобы оно не конфликтовало с будущими стилями, введём обязательный префикс "V2STYLE:", который перед парсингом будет пропускаться.
    • Жить оно будет в MotifKnobs_internals.c -- ну где ж еще :-).

    06.09.2016@Снежинск-каземат-11: делаем.

    • Описание API в MotifKnobsP.h очень простое: пара enum-флагов и функция MotifKnobs_TuneButtonKnob(). Ни buttonopts_t, ни psp-таблицы там нет -- потому, что...
    • ...парсинг поля style возлагается на саму Tune().
    • Модификации MotifKnobsP.h:
      • Тип структуры опций, ставший локальным, теперь назван просто buttonopts_t.
      • MotifKnobs_TuneButtonKnob():
        • Скопирована из v2'шной TuneButtonKnob().
        • В начало добавлен парсинг -- условный, если всё !=NULL и наличествует префикс "V2STYLE:".
        • В конце -- psp_free() (т.к. присутствует MSTRING).
      • Поскольку там есть возможность присваивать pixmap'ы, то из v2 скопирована также MaybeAssignPixmap(). Только:
        1. Она сделана static.
        2. Адаптирована с ~/pult/ на ~/4pult/.
        3. Избавлена от махинаций на тему "!from_widdepinfo", т.е. "из label" (префикс "=#!=") -- в текущем минимальном использовании оно нафиг не нужно.
    • Добавлено в CreateSubwinCont(), сразу после вызова MotifKnobs_DecorateKnob() -- работает.

      Со старым приколом, что флаг "bold" работает только при указанном размере (поэтому надо писать size=normal).

    • Ну и в CreateButtonKnob() тоже добавлено, проверено.

      Замечание: но НЕ добавлено в CreateArrowKnob(). И надо обратить внимание при создании полноценных стилей, что для кнопок-стрелок размер надо обрабатывать иначе (как и для пусто-меточных alarmonoffled'ов).

    24.03.2017: исправлен косяк: из-за того, что в юзерах MotifKnobs_TuneButtonKnob() вызывалась ПОСЛЕ MotifKnobs_DecorateKnob() (сохраняющей текущие цвета -- в качестве изначальных -- в deffg/defbg), спецификатор bg=ЦВЕТ работал только наполовину: цвет теней менялся, а собственно фон -- нет (он перепрописывался при колоризации). Перестановка вызовов спасла ситуацию.

    Также задействована поддержка изменения цвета символов -- color=ЦВЕТ. Оно в opts и psp-таблице было изначально, но не использовалось (и умолчание стояло RED вместо -1). Работает.

  • 21.03.2017: в MotifKnobs_colors_lkp[] добавлен еще один вариант -- "armed". ИСКЛЮЧИТЕЛЬНО для v4gid25s.

    21.03.2017: решение, конечно, так себе. И, возможно, надо было цвет назвать "cyan" или "turquoise".

    22.03.2017@утро-дорога-на-лыжи: а не ввести ли возможность указывать также и ПРОСТО цвета?

    • Например, если в младшем байте что-то есть, то это "наш индекс", а если нет, то старшие 3 байта содержат R,G,B.
    • ...или, лучше -- чтоб младшие 7 (семь!) бит кодировали индекс, а следующие 24 -- R,G,B.

      Так старший бит остаётся свободным, тип -- знаковым, и позволяет иметь значение -1 (default).

    • Ну и типа "оптимизация" -- чтобы значение 0 также считалось "неуказанным", а R,G,B использовались бы при особом значении индекса; например, 126 (т.к., 127 -- все единицы -- будет в -1).

    Вообще идейка так себе. Готовый проект размещения есть, но от реализации лучше постараться воздержаться.

  • 21.03.2017: надо бы научить text_knob и text_text уметь менять размер и жирность шрифта (можно заодно и цвет). Хаковато, временно -- аналогично кнопчатостям.

    Очевидно, что реализовывать этот общий функционал надо в _internals. Назвать, видимо, TuneTextKnob().

    24.03.2017: приступаем.

    • MotifKnobs_TuneTextKnob() сделана копированием из _TuneButtonKnob().
      1. Добавлена изменябельность цвета букв -- реакция на color=ЦВЕТ (работает). И в _TuneButtonKnob() бэкпортировано.
      2. Компонент "family" в имени шрифта заменен на XH_FONT_FIXED_FMLY.
      3. Вызов MaybeAssignPixmap() очевидным образом убран.
    • Добавлено использование в _text_knob.c и _text_text.c. И-и-и...
    • ...цвета -- работают. А со шрифтами -- ёк. Выдаётся
      Warning: Unable to find type of resource for conversion
      (это на XtVaSetValues() с XtVaTypedArg, XmNfontList, XmRString.)

      Странно -- вроде бы у XmPushButton'а тот же самый XmNfontList, что и у XmText'а.

      • Возможно, разница в том, что
        1. PushButton наследован от XmLabel (в которой и введён ресурс XmNfontList), а...
        2. Text напрямую от XmPrimitive, ресурс вводит сам, и он у него значится не просто в списке ресурсов, а в некоем "Output Resource Set".
      • Также возможно тут как-то вовлечены "render tables" (ресурс XmNrenderTable, который рекомендуется использовать вместо fontList).

      Попытка разобраться при помощи editres'а сходу результата не дала: штатный EPEL'овский xorg-x11-resutils-7.5-13.el7.x86_64 на CentOS-7.3 просто не работает (как минимум под Гномом) -- не даёт возможности добыть дерево клиента, щелканье на нужном окне ничего не делает.

MotifKnobs_lrtb_grpg:
  • 27.07.2007: сделан этот модуль, как замена CXv2'шного ChlLayOutGrouping().

    27.07.2007: имя -- это сокращение от "Left-to-Right, Top-to-Bottom".

    Оно объявляет себя и как CONT с именем "lrtb", и как умолчательный GRPG.

  • 10.08.2009: добавил placement-фичи hfill и vfill, скопировав из CXv2's Chl_gui.c. Заодно добавлена и собственно человеческая поддержка полей .layinfo; флаг "перенос строки" указывается как newline.

    21.04.2014: позорище -- НЕ делалось bzero(&placeopts), хотя ВСЕ флаги там не-default. Вот оно и пыталось учитывать мусор...

  • 05.07.2016: была пара косяков, препятствовавших работе в режиме ресайзинга:
    1. В отличие от v2, lrtb НЕ является непосредственным содержимым workspace'а, поэтому "линии" входят в него через промежуточную форму -- k->w элемента.

      А этой форме никто НЕ делал аттачменты к workspace'у.

      Исправлено -- в ChlRunSubsystem() теперь при opts.resizable "корневой" ручке делаются аттачменты со всех сторон.

    2. Отсутствовали некоторые дополнительные аттачменты, появившиеся в ChlLayOutGrouping() где-то в районе 2008/2009 годов и почему-то не попавшие в v4 (то ли забыл, то ли добавились позже создания MotifKnobs_lrtb_grpg.c).

      Конкретно -- принудительный attachleft() (или attachright() при is_vertical) для линии и собственно ручки.

      Без этого они вместо растягивания прилеплялись только справа (или снизу для вертикалки), в полном соответствии с поведением XmForm: "если аттачмент ни слева, ни справа не указан, то аттачится к форме слева; а если указан, то используется только тот аттачмент" (моё краткое изложение написанного в man'е по XmForm); вот раз делался только правый, то оно лишь справа и клеило.

  • 23.08.2018: возникло желание уметь per-element указывать, на каком расстоянии от предыдущего его расположить.
    • Желание -- для rfsyn.subsys, блоки впуск/выпуск: там равномерные колонки задержек для Д16, но ещё плюс пара каналов ЦАП@cac208, и вот их надо б куда-то приткнуть, но помещать задержки в один под-элемент, а ЦАПы в другой -- лень. Так же бы просто отдельный nodecor-элемент подклеивать к табличке снизу.
    • Указывать -- просто: layinfp-параметром "offset". Который по умолчанию пусть будет =-1 (только не забыть переделать, чтоб при ошибке парсинга layinfo делалось бы не bzero(), а парсинг из "").
    • Использовать -- тоже просто: при offset>=0 использовать его, вместо умолчательных hspc/vspc. Это всего 1 точка.
    • И да -- в смысле, "нет" -- указывабельно так будет только расстояние по "основному" направлению (между под-элементами), но НЕ между контейнерами.

    24.08.2018: сделано, работает.

MotifKnobs_grid_cont:
  • 27.07.2007: приступаем к созданию, точнее -- интеллектуальному переносу кода из CXv2.

    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 было правильно изначально).

  • 11.07.2014: надо реализовывать фичу "attitle", с указанием количества оных в param3.

    15.07.2014: да,

    • Сделано за вчера-сегодня, вместе с "titleat*" и "one"; в сотрудничестве с MotifKnobs_internals.c.
    • Сей момент там еще много за-#if0'енного старого кода, надо будет после проверок его удалить.
    • Плюс, позаменять XtVaSetValues(...) на более понятные attach*().

    05.08.2014: что-то подглючивает разворачивание coltitle'ов по double-click'у -- не те метки оно выставляет. Не с attitle ли связана проблема?

    12.08.2014: причина косяка найдена -- это было следствие ДВУХ ошибок.

    1. nattl действительно не учитывается, причём и в v2 тоже.
    2. В LabelClickHandler() были перепутаны str1 и str2: первая считалась за colnames, а вторая за rownames, хотя при создании они использовались ровно наоборот.

    В результате оно думало, что colnames==NULL и брало метки от ручек -- БЕЗ смещения на nattl.

    • Решение принято чуток радикальное: для определённости назначаем именно str1 за colnames, а str2 за rownames, чтоб были "по порядку" (да и в v2 так же).
    • Кроме того, теперь и nattl учитывается -- берётся из param3 (который теперь в момент создания приравнивается санитизированному значению nattl).

      Причём значение прибавляется к u.c.content -- просто записывать в userData сразу значение col с прибавленным nattl нельзя, поскольку col используется как индекс в colnames.

      ...а в v2 у multicol privrec'а нет, поэтому там на проблему пока просто забито.

  • 11.07.2014: сильно надоело писать длинную цепочку ключиков
    noshadow,notitle,nohline,nocoltitles,norowtitles

    Поэтому добавлен специальный ключик nodecor, делающий =1 всем тем флагам.

MotifKnobs_alarmonoffled_knob:
  • 30.07.2007: воссоздаем эту штуку в соответствии с новыми знаниями.

    30.07.2007: новость знаний заключается в следующем:

    • Во-первых, от концепции "panel" отказываемся. Проку от нее оказалось немного, зато крива она -- например, НЕ колоризуется, поскольку вся площадь занята XmNselectColor/XmNunselectColor.
    • Во-вторых, вместо дикого количества РАЗНОЦВЕТНЫХ типов теперь будет лишь 4 типа, определяющих разную сущность: alarm, onoff, led и radiobtn. Цвет же определяется параметром "color".
    • В-третьих, "по-человечески" сделана поддержка концепции "victim" -- безо всяких XmNuserData, а просто при создании указанное (причем в MSTRING, а не во всегда-имеющийся 200-символьный буфер!) имя резолвится и сохраняется в me->victim_k.

    20.05.2009: а вот и фигушки -- "panel" оказалась очень наглядной, и лучше бы её всё-таки сохранить и тут.

    24.08.2009: да, портировал сюда и "panel" тоже.

    25.05.2016: с реализацией "panel" -- точнее, с XmNfillOnSelect=True -- есть косяк.

    • Косяк явно в OpenMotif, он присутствует и в SL-6.3, и в RH-7.3, и не только в v4, но и в v2.
    • Выражается он в том, что пока ручке не прошло первое программное обновление (т.е., пока состояние XmINDETERMINATE), то при первом нажатии генерится значение 0 (как и у прочих, не-panel'ов), но выглядит она нажатой!
    • Можно ли с этим что-то сделать -- неясно. Но о-о-очень сомнительно, учитывая давнюю историю кривизны XmNfillOnSelect (см. раздел "alarmonoffled" в bigfile-0001.html).

    15.10.2018: "по-человеческость" реализации victim несколько преувеличена: сам поиск "жертвы" по имени был сделан крайне халтурно -- просто проходом по списку "соседей", так что никакие префиксы ':' и входы в под-элементы работать не могли. И это при том, что даже в v2 использовалась datatree_FindNodeFrom() и нетривиальные ссылки работали!

    Почему так странно -- очевидно, потому, что v4'шная datatree_find_node() появилась лишь 19-08-2015 (в интересах histplot'а).

    Перевёл на datatree_find_node() -- всё стало прекрасно (правда, сначала несколько часов не мог разобраться, какого ж чёрта не работает (не находится ничего), пока, напичкав искодник искалки отладочной печатью, не осознал, что последний параметр надо передавать -1, а не 0).

  • 09.11.2015: реализуем также бибиканье при alarm'ах. В основном свелось к копированию из v2.

    Распределено оно по куче файлов/библиотек, но опишем тут.

    09.11.2015: начинаем:

    • В datatree добавлен helper show_knob_alarm(), используемый CdrProcessKnobs()'ом.
    • В dataknob_cont_data_t добавлены поля alarm_on, alarm_acked, alarm_flipflop.
    • Основное "мясо" живёт в MotifKnobs_internals.c, в виде "стандартных методов":
      • MotifKnobs_CommonShowAlarm_m()
      • MotifKnobs_CommonAckAlarm_m()
      • MotifKnobs_CommonAlarmNewData_m() -- таким вот неприличным способом (скопировано из v2) делается собственно мерцание и периодическое бибиканье.

        ...а надо бы -- РАЗ В СЕКУНДУ!!!

      Там же рядом и внутренние, взятые 1-в-1 из v2 DisplayAlarm() и CycleAlarm() (оный и вызывается из NewData_m).

    • Методы ShowAlarm_m и AckAlarm_m устроены хитро: они идут вверх о дереву, пока parent'ов метод -- они же; и уже с максимально верхним элементом делает моргание.

      Смысл -- чтоб моргал максимально возможный "внешний" контейнер.

      ...и у конкретно subwin'а в этой позиции вообще NULL -- чтоб моргало в под-окне, а не в содержателе его кнопки.

    Работает -- бибикает, моргает. Из неприятностей только, что

    1. RELAX показывается только на кнопке subwin'а, а на самом ALARM'е -- нет.
    2. И на subwin'а показывается с задержкой в 1 цикл.
    3. Бибикает как-то очень уж интенсивно.

    12.11.2015: продолжаем:

    • Не показывался RELAX потому, что в choose_knobstate() отсутствовала проверка на этот флаг. Добавлена -- теперь показывается.

      Там еще рядом, возможно, будет OTHEROP. Хотя точно и не ясно.

    • Задержка была из-за того, что в CONT/GRPG сначала проходится по внутренностям, собирая флаги, потом вызывается NewData() (где у cont_subwin'а и происходит подсветка кнопки!), и лишь затем значение флагов сохраняется в k->currflags -- ПОСЛЕ общего switch(k->type).

      Сделано в ветке CONT "предварительное" сохранение прямо перед вызовом NewData().

      Помогло -- в основном. Но не полностью. Переходы NORMAL->ALARM и RELAX->NORMAL показывает сразу, а вот ALARM->RELAX -- с отсрочкой на 1 такт, через промежуточное NORMAL.

    13.11.2015:

    • Разобрался -- ну ясно, почему!
      • Потому, что у ручек выбор состояния RELAX в choose_knobstate() делается не как со всеми прочими -- по наличию флага, а в обход, по значению k->attn_state==KNOBSTATE_RELAX (и по нему же в choose_knob_rflags() выставляется CXCF_FLAG_ALARM_RELAX!)

        А от cont_subwin'а, кстати, никакого attn_state не передаётся (ибо передаётся k=NULL).

      • И расцветкой
      • А порядок действий в CdrProcessKnobs() для DATAKNOB_KNOB таков:
        1. rflags=choose_knob_rflags()
        2. if (...CXCF_FLAG_ALARM_ALARM) show_knob_alarm() (который и вызывает set_knob_relax(), в конечном итоге прописывающий attn_state).
        3. k->currflags=rflags

        Таким образом, ручковы 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

    Так что надо бы что-то делать.

    • Самое правильное -- иметь у каждого потенциального бибикальщика собственный метроном (XtIntervalId) с частотой 1Гц.

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

    • То ли вводить отдельный метод "раз в секунду" (как оно сделано для histplot и knobprops).
    • То ли запоминать время предыдущего бибиканья и не бибикать, если прошло меньше 1 секунды.

    23.02.2016: вот по последнему проекту и делаем.

    • Добавлено поле dataknob_cont_data_t.alarm_prev_time.
    • Оно имеет тип cx_time_t -- чисто "для красоты", чтобы со struct timeval не связываться.
    • Ну и проверяется, что если текущее время отстоит от "предыдущего", то ничего не делаем и сразу return.
    • ...хреновато, что при цикле сервера 200мс интервал между бибиканьями получается то 1.0с, то 1.2с -- из-за погрешностей отработки таймаутов, видимо. На слух такое нерегулярное бибиканье весьма неприятно.

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

    • ...и еще какой-то закидон: если бибикающей программе в её консоли нажать Ctrl+S, а потом Ctrl+Q только минут через 5, то она разок взбибикнет и замолчит (хотя через какое-то время всё-таки очухивается).

      Вроде с потенциальным разрывом соединения не связано (возможное переполнение отправного буфера cxsd_fe_cx при отсылке подписки) -- и ругательства нет, и управление прочими каналами продолжает работать.

MotifKnobs_button_knob:
  • 30.07.2007: создаем его, практически копируя из Knobs_button_widget.c.

    30.07.2007: основное отличие (за вычетом украсивливания вследствие новой архитектуры) -- то, что раз уж кнопчатые могут быть alarm'ами, то он по нажатию на себя в варианте !is_rw дергает ack_knob_alarm().

  • 18.05.2010: к вопросу о режимах работы кнопчатостей (по результам разговоров у Медведко по поводу ЭЛС): вместо нынешней парадигмы "юзер нажал кнопку -- инициировалась штучная запись в канал" может потребоваться сильно другая парадигма управления: НЕПРЕРЫВНАЯ. Например, на той же сварке (и прочих станкоподобных местах) требуется экранная имитация "аналоговых" элементов управления. Т.е., пока некая кнопочка нажата -- моторчик едет, отпускаем -- моторчик останавливается.

    18.05.2010: напрашивается пара идей, как это можно было бы реализовать на уровне экранных управляющих ручек:

    1. "10": при нажатии кнопка шлет в канал значение 1, при отпускании -- 0. Таким образом, если "моторчик" управляется, например, битиком в УРе, то во время нажатия он будет ехать, после отпускания -- остановится. Естественно, с вроменной погрешностью, определяемой временем бегания данных по сети и до драйверов.
    2. "pulsed": при нажатии кнопка переходит в режим "автоповтора", и начинает, с некоей частотой -- 10...30Гц генерить записи в канал (естественно, после небольшой первоначальной паузы) -- т.е., имитируется поведение кнопок на компьютерной клавиатуре. Такой вариант годится для пульсовых/шаговых железок -- если по команде от компа "моторчик" делает небольшой шажок.

    На вопрос "надо ли это? где и как?" Карнаев прислал такой ответ:

    Конечно, надо.

    Такой режим работы часто используется. У меня в голове он представляется в следующем варианте: когда нажимается объект типа "стрелка2, то программа, спустя определенную небольшую паузу, начинает высылать с известной частотой (на ВЭПП-4 это от 1 до 5 Гц)запросы на добавление(убавление) какого-либо значения, если нажимается кнопка без фиксации - на включение бита; после отпускания кнопки все немедленно прекращается, в случае кнопки - бит зануляется. Поэтому мне кажется, что актуален твой второй вариант.

    Первый вариант, мне кажется, менее гибок в случае сложных комбинаций со стороны клиентов.

    Похоже, сделать надо будет ОБА режима -- благо, сие несложно:

    • Сам режим чтоб указывался в options -- normal/10/pulsed. Возможно -- для режима pulsed отдельно указывать скорость автоповтора (еще вариант -- ввести ОДИН параметр mode: ==0:normal, <0:10, >0:частота).
    • Собственно реализация -- несложна: в EquipButtonKnob() вместо просто уставки activateCallback при надобности ставить armCallback+disarmCallback:
      1. 10: armCallback генерит отправку 1, disarmCallback генерит отправку 0.
      2. pulsed: генерим отправку 1, а потом включаем "автоповтор" в точности по алгоритму из IncDecB/dial.

    Естественно, всё это -- только о работе мышью, т.к. с клавиатуры Motif'овские кнопки сами делают нажатие+отпускание, что эффективно даёт 2-й описанный режим.

  • 19.11.2014: в arrow-кнопках частично воспроизведён оформительский функционал v2: возможность указывать размер плюс цвет стрелки (это ключом offcol вместо v2'шного LOGC_).

    Оно, конечно, лучше б было стилями, но они еще фиг знает когда (и если) появятся.

MotifKnobs_subwin_cont:
  • 02.08.2007: создаем его, с учетом новой архитектуры.

    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.

  • 26.02.2009: надо будет -- если станем делать содержимое всегда-grid'ом -- поддерживать также и параметры hspc и vspc.

    10.02.2010: поскольку содержимое будет всегда-lrtb, то "withdrawn".

    29.03.2011@Снежинск-каземат-11: точнее, не "всегда-lrtb", а "единственный child" (как решено 22-07-2010). Но на withdrawn'нутость это всё равно не влияет :-).

  • 08.09.2016@Снежинск-каземат-11: немного подзадолбало всем subwin'ам указывать одинаковые tip: и subwintitle:.

    Ввести бы правило, что при отсутствии subwintitle (str3) пытаться использовать tip?

    Сходу -- сделано, работает.

MotifKnobs_selector_knob:
  • 11.07.2014: сделан селектор был еще давным-давно (2007?), но раздел создан только сейчас -- пусть будет, для порядка.

    29.08.2014: и разноцветность отдельных пунктов добавлена, также творческим копированием из v2.

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

    23.09.2014: был багец -- в SelectorSetValue_m() неправильно ограничивались значение сверху (забыто было -1), и оно падало. Исправлен.

  • 21.03.2017: добавлена возможность менять размер/жирность шрифта. В интересах toolbar'ной loggia в v4gid25s.

    21.03.2017: подробности:

    • Код был скопирован из _internals.c -- MotifKnobs_TuneButtonKnob() с обвеской, переименованный в TuneSelButton().
    • Используется то же самое правило, что и с button'ами: спецификация должна начинаться с префикса "V2STYLE:".
    • Поскольку поддерживается также и "новый" синтаксис (lit=ЦВЕТ), то сама функция чуть модифицирована: теперь она возвращает результат, и если он <0, то считается, что должного префикса не было и делается парсинг по обычной схеме.
    • Также пришлось чуть изменить порядок действий.

      Раньше вначале делался парсинг style, потом создание виджета, потом расцвечивание; но, поскольку Tune...() сразу расцвечивает, то ей нужен уже "живой" виджет.

      Поэтому схема изменена на "создание виджета, потом парсинг/раскрашивание".

    • Часть, касающаяся pixmap'ов -- MaybeAssignPixmap() -- просто закомментирована.
    • Теперь чуток об "отрицательном" результате и выводах на будущее:
      • Была сделана попытка воспринимать также указание цвета символов (color=ЦВЕТ). Оно-то парсится, но эффект -- отсутствует. Точнее, в выпадающем списке всё как надо, а на самом поле ручки (которое в случае ro-селекторов (отображаторов) и является единственным) -- никак.

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

        Следовательно, если когда-то захочется иметь ПОЛНОЦЕННУЮ поддержку стилей с foreground'ом, то надо будет и для цвета "переднего плана" делать копирование.

      • О собственно поддержке стилей: очевидно, что эта "инфраструктура стилей в MotifKnobs" должна будет не только выставлять цвет в самих виджетах, но также и возвращать коды цветов "владельцу", чтобы тот мог производить подобные манипуляции.
MotifKnobs_choicebs_knob:
  • 11.07.2014: начато изготовление -- на основе selector'а, а "мозги" берутся из v2'шного.

    15.07.2014: да, вроде сделано. Пока без поддержки "стилей" кнопок.

    11.08.2014: поддержка стилей скопирована из v2, хоть и со злобным комментарием "!!!STYLE!!!".

    11.07.2016: кстати, уже давным-давно используется, так что "done".

  • 11.07.2016: замечена неприятная особенность: при пустом списке items оно SIGSEGV'ится. Причина -- оно в ChoicebsColorize_m() лезет за умолчательными цветами в me->items[0] (см. bigfile-0001.html за 18-08-2012).

    Понятно, что ситуация несколько нештатная, но лучше б всё же как-то защититься.

    13.07.2016: да, сделано -- теперь items[0] используется не безусловно, а только при me->numitems>0, иначе -- k->w (форма). Падать перестало.

MotifKnobs_canvas_cont:
  • 11.07.2014: надо делать местную инкарнацию (не забывая про attitle).

    15.07.2014: портировано из v2. Пока не проверено.

    16.07.2014: проверено, работает, считаем за "done".

  • 28.03.2018: в text2canvasopts[] отсутствовала строка PSP_P_END(). В результате у Роговского клиент валился по SIGSEGV.

    Почему не вылезло раньше -- загадка:

    • То ли у меня нигде параметры не использовались (хотя "nodecor" вроде должен, да и того клиента-пример Роговскому c "notitle,noshadow" написал я и проверял-показывал).
    • Или, скорее, дело в системе: у него там что-то на основе Debian, и, очевидно, из-за иного компилятора/библиотек/... получалась другая раскладка в памяти; а под CentOS-7.3, видимо, как-то удачно после таблицы идут нули -- и END как раз обозначается признаком name=NULL.
MotifKnobs_histplot:
  • 12.08.2014: создаём раздел.

    12.08.2014: пара идей:

    1. Реализация должна быть "объектом"-библиотекой -- чтоб можно было использовать и как knobplugin, и для глобального окна (по сценарию, запланированному в bigfile-0001.html 18-04-2010 и 13-07-2011).
    2. Как производить обновление: либо по тамошнему сценарию (с "hist_next"), либо держать у каждого histplot'а список подведомственных ручек, а в тех иметь поле "historian", указывающий на его privrec, и чтоб физически сдвижку выполнял только этот "ответственный" (хотя в списке отображения она может быть у многих).

    16.01.2015: после загруза проблемой (на этой неделе), обмышления "как уведомлять histplot'ы из KnobProps" (вчера) и анализа тех 0001'шных сценариев (сегодня утром) стало ясно, какую архитектуру выбрать.

    • Никакое "historian" не нужно. И вообще histplot'ы занимаются ТОЛЬКО ОТОБРАЖЕНИЕМ.
    • Производство сдвига -- работа Chl+Cdr:
      1. Chl организует "событие раз в секунду".
      2. Cdr:
        1. Имеет метод "добавить ручку в список историзируемых" -- при этом и буфер аллокируется.
        2. Имеет метод "сдвинуть историю".
    • Список историзируемых -- заведён у подсистемы.
    • Histplot'ы все добавляются в СВОЙ список. Живущий уже, очевидно, у Chl'я.

      Все из этого списка и вызываются по "событию раз в секунду".

    • Но, поскольку histplot реализован в MotifKnobs, а не в Chl, то действия "добавить histplot" и "начать историзировать канал" должны вызываться cont-методами -- в конечном итоге (путём 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().

    • Cdr:
      1. CdrAddHistory() -- аллокирует буфер и добавляет в список историзируемых. В начале проверяется на DATAKNOB_KNOB; плюс всё делается в правильном порядке -- добавление в список ПОСЛЕ аллокирования буфера, так что в неопределенном состоянии не останется.
      2. CdrShiftHistory() -- сдвиг истории всех ручек подсистемы.
      3. В CdrDestroyKnobs() буфер освобождается и удаляется из списка.

      Замечание: поскольку список хранится в DataSubsys'е, то ВСЕ ручки подсистемы будут историзироваться синхронно и с одинаковой частотой, для ВСЕХ окон/группировок.

  • 10.08.2015@утро-пультовая-планёрка: кстати, коль скоро histplot теперь будет библиотекой, а каналы самодостаточны по своим именам, то ничто не мешает сделать аналог EPICS'ного striptool'а -- назвав его, например, "histplot".

    В командной строке указывать список имён каналов, плюс, опционально, параметры вроде "частота обновления" и "длина запоминания истории".

    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".

  • 12.08.2015: приступаем к собственно изготовлению. Заголовочный файл -- include/MotifKnobs_histplot.h, реализация -- lib/MotifKnobs/MotifKnobs_histplot.c.

    15.08.2015: потихоньку пилим, копируя функционал из v2'шного Chl_histplot.c ("потихоньку" -- потому, что толпа всякой отвлекающей от работы фигни...).

    Некоторые замечания по архитектуре:

    • MotifKnobs_histplot занимается ТОЛЬКО отрисовкой и менеджментом своего содержимого. Прочие сопутствующие задачи --
      • создание окна-содержателя (модуль только свою форму в него вставляет);
      • включение историзируемости ручки -- вызов для неё CdrAddHistory();
      • сдвиг истории с какой-то периодичностью -- вызов CdrShiftHistory() -- и последующий вызов обновления MotifKnobs_UpdateHistplot()
      -- возлагаются на его "юзеров".
    • Юзерами на данный момент планируются:
      1. Chl_histplot.c -- обычная обвязка для окна истории.
      2. MotifKnobs_histplot_noop.c -- плагинчик для отображения истории прямо в обычном дереве ручек.

        Паковаться, видимо, будет в свою отдельную библиотеку -- libMotifKnobs_histplot.a.

      3. histplot.c -- отдельная программа для экранной историзации любого набора каналов.
    • Отдельный вопрос -- как пп.1 и 2 будут ловить Shift+MB3, а также как они его будут делить.
    • Кстати, все действия "высокого уровня" -- вроде MB3, закрытия окна (как кнопкой [Close], так и при удалении последней ручки из списка), кнопки [Save] -- передаются на исполнение "юзеру" через указываемый им evproc.

      Но СОЗДАЁТ кнопки [Close] и [Save] -- именно MotifKnobs_CreateHistplot().

    • Махинации с cyclesize (включая добычу оного у cda и сравнение с "последним известным") удалены (вместе с frqdiv). Теперь это значение -- cyclesize_us -- указывается при создании, а используется только при вычислении подписей к осям.

    Наполнение функционалом вроде завершено, теперь дело за использователями.

    17.08.2015@сад-Вера:

    18.08.2015: сделан первый из клиентов -- Chl_histplot.c.

    • Ключевой вопрос: как множественные "histplot'ы" (РАЗНОГО рода) могут подписываться на обновления графиков?

      Ответ: заводим контейнеров метод HistInterest.

      • Фактически это регистрация "callback'а", вызываемого по событию "сдвиг". С возможностью указания privptr.
      • Сервисная функция в datatree -- express_hist_interest().
      • Интерфейс позволяет как уставлять, так и удалять callback; за это отвечает последний параметр, set1_fgt0.
    • Собственно обновления организует Chl_app.c.
      • Он организует дрыганье своего CycleCallback() с некоторым периодом.
        • Через cxscheduler, поскольку заказывается не длительность паузы, а точный момент -- sl_enq_tout_at(); вся технология взята от циклов из cxsd_hw.c, только без определения неуспевающести (cycle_pass_count*).
        • Список клиентов хранится в SLOTARRAY.
        • "Мясо" -- вызывает CdrShiftHistory() и уведомляет всех клиентов об обновлении.

        20.08.2015: также добавлена внутренняя функция _ChlAppUpdatePlots() (объявляется в опять созданном Chl_app.h). Нужна для того, чтобы Chl_knobprops вызывала перерисовку всех графиков по [Ok] -- на случай изменения диапазонов отрисовки.

      • Размер цикла и длительность хранения истории:
        • По умолчанию 1 секунда и 86400 отсчётов.
        • Можно указывать в appopts, ключи hist_period и hist_len.
        • Сохраняются в 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.

    • А вот содержимому этого метода нужен доступ к Chl'ному privrec'у, ибо там хранится всё-всё-всё, включая список подписавшихся на обновление.
      • Ну и как это делать, учитывая, что ссылка на privrec хранится в XhWindow->higher_private?
      • Добавлено поле data_subsys_t.gui_pripvtr, куда Chl (в лице Chl_app) записывает указатель на свой privrec.
      • После чего вопрос: а точно ли ПЕРВИЧНЫМ местом хранения должно быть XhWindow? Или правильнее делать привязку Chl к subsys (gui_privptr), а не к XhWindow?

      21.08.2015: а теперь внимательно смотрим в "Общих вопросах" от Chl комментарий за 13.08.2007 :).

    • Чуть сменена работа find_knobs_nearest_upmethod() -- он теперь вначале пытается не сразу делать "шаг вверх" (holder=k->uplink), а если сама k имеет тип CONT или GRPG, то сначала пробует её (holder=k).
    • Собственно Chl_histplot.c -- всё прямолинейно, особо писать не о чем.

    19.08.2015: делаем второго клиента -- MotifKnobs_histplot_noop.c.

    • Chl_gui.c удалён. R.I.P.!

      Содержимое переехало в Chl_app.c, а созданный вчера Chl_app.h стал излишен.

    • Собственно текст MotifKnobs_histplot_noop.c никаких особых проблем не представлял -- пара сотен несложного кода, в значительной степени копирование из старого (особенно AddPluginParser()).
    • Для поиска ручек создана datatree_find_node() (детальный алгоритм внутренностей придуман @пляж-после-обеда).
      • В основном довольно простая и прямолинейная, без особых закрутов (в отличие от v2'шной).
      • Ей указывается кроме имени еще и "начальная точка для поиска", которая считается "файлом, а не директорией" -- вначале делается 1 шаг вверх (если uplink!=NULL), и уже в том контейнере начинается поиск.
      • Присутствует поддержка "filename-like characters":
        • Если первый символ '.', то поиск начинается с самого верха иерархии; аналог "/".
        • Если в любом месте имени встречается ':' (он считается разделителем, как '.'), то делается шаг на 1 уровень вверх, если возможно (если невозможно, то символ просто пропускается); аналог "../".

        Несколько подряд '.' -- разделителей компонентов -- считаются за один; в точности как "////" (точнее, не "считаются", а сам проход устроен таким образом -- перед вычленением очередного компонента пропускаются предшествующие ему '.' и ':' (тут и делаются шаги вверх)).

      • Опционально можно указать тип искомого узла (если >=0), и если он не совпадёт, то будет ошибка. Сделано для вещей вроде histplot'а, когда заранее известно, что интересует только DATAKNOB_KNOB.
      • Отсутствует (пока?), по сравнению со старым кодом, поддержка "прозрачных" контейнеров (с именем пустым или ":").

      12.05.2016: был косяк с определением "ненайденности": стояло условие rest<0, никогда не исполнявшееся (т.к. в цикле ограничение rest>0); поэтому при указании ненаходимого компонента вылетало по SIGSEGV. Сменено на rest<=0.

      15.10.2018: а еще там есть неочевидная тонкость: чтобы искать узлы ЛЮБОГО вида, надо указывать последний параметр (rqd_type) равным -1, а не 0. Т.к. сравнение проводится при rqd_dtype>=0, так что 0 по факту означает "никогда не найдётся" (типа с кодом 0 не существует).

    • Вылез идеологический косяк с технологией "_ChlBindMethods()": оно делалось ПОСЛЕ CreateKnob(группировки), и в результате используемый CreateHistplotNoop()'ом historize_knob() просто не работал -- Chl'евы методы еще не были прибиндены.
      • Переставить бинденье в точку ПЕРЕД CreateKnob() -- смысла ноль, т.к. оный перепропишет поле vmtlink.
      • А вот предыдущая идея неправильная, потому, что BindMethods работает совсем по-другому -- не vmtlink трогает, а uplink.

        И тогда вообще неясно, как оно там работало бы с подмененным uplink'ом -- поиск "вверх" по дереву в datatree_find_node() приводил бы чёрт-те-куда.

      • Поэтому сильно изменена идеология:
        • В KnobsCore добавлен новый вызов -- 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 удалена.

    • Никакой отдельной "libMotifKnobs_histplot.a" не понадобилось, поскольку плагин не пользуется функционалом ни cda, ни Cdr -- всё взаимодействие с "внешним миром" идёт через datatree.

      Плагин работает -- ура!

    • Дополнение в MotifKnobs_histplot.c, "постулат" относительно диапазона отображения: он используется, если указан (num_params>DATAKNOB_PARAM_DISP_MAX) и если min<max; в противном случае используется [-100,+100]. ВЕЗДЕ -- в расчёте, в отображении графика и осей.

    20.08.2015: и теперь третий -- histplot.c.

    • Скелет собран копированием кусков из pult.c (основная часть) и Chl_histplot.c с MotifKnobs_histplot_noop.c (касательно набивки окна графиком).
    • Работа с циклом сдвига истории -- из Chl_app.c.
    • Самое замутное -- это корректное создание группировки "руками", с проставлением всех ссылок.
    • ...окошко уже появляется, но толком не работает -- где-то что-то недопроставлено из ссылок...

    21.08.2015: добиваем histplot.c.

    • Ссылки теперь пропроставлены "как надо".
    • Конечно, он ОЧЕНЬ несовершенен. Необходимые дополнения:
      1. Воспринимать глобальные параметры, в формате PARAM=VALUE.
        • defserver=NNN (к сожалению, -dDEFPFX не удастся, т.к. Xt воспринимает "-d" как "-display").

          05.02.2016: сделано. (Начато еще 28-01-2016, но дурканул с передачей Realize()'у.)

        • baseref=NNN (хотелось бы -bBASEREF, но надо унифицироваться с defserver). 05.02.2016: сделано.
        • cyclesize=SECS 28.01.2016: сделано. Только переименовано в hist_period -- чтоб как в .subsys-файлах.
        • hist_len=POINTS. 05.02.2016: сделано.
      2. Дополнительные описатели к каналам:
        1. Префикс [%DPYFMT] -- в точности, как в console_cda_util.
        2. Параметры-суффиксы, через ',', можно psp_parse()'имые:
          • Диапазоны -- min, max. 16.06.2018: только не min, max, а единый disprange:MIN-MAX.
          • Метка для отображения -- label.

          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().
    • Соответственно -- в Chl_app выкинуты _ChlBindMethods() и ChlTopCont, а ChlTopVMT переименована в ChlSysVMT.
    • И фигня с порченьем vmtlink'а из KnobsP.h и KnobsCore_knobset.c удалена.

    07.01.2016: вчера обнаружился косяк: при смене свойств ручки (в Chl_knobprops) оси НЕ обновлялись и axis не ресайзился. И вообще MotifKnobs_UpdateHistplotProps() никогда не вызывалось.

    Сегодня разобрался: вся схема "histinterest", дёргаемая в CallUpdate(), была рассчитана на один-единственный тип события -- "Update plot", а тут другой тип -- "Update plot props", но Aоно ж никак не передаётся, а вызывается по факту то же самое "Update plot".

    Всего-то надо добавить в эту архитектуру еще "код события".

    09.01.2016: добавлено, всё сделано.

    1. datatree.h:
      • К callback-типу _m_histplot_update() добавлен параметр reason.
      • Сами коды -- HISTPLOT_REASON_UPDATE_PLOT_GRAPH и HISTPLOT_REASON_UPDATE_PROT_PROPS.
    2. Клиенты -- MotifKnobs_histplot_noop.c и Chl_histplot.c, в обоих PlotUpdateProc() -- научены вызывать разные методы в зависимости от reason.
    3. Ну а в инфраструктуру _ChlAppUpdatePlots()+CallUpdate() вставлена передача кода события -- благо, CallUpdate()'ов privptr как раз был свободен.

    10.01.2016: для симметрии MotifKnobs_UpdateHistplot() переименована в MotifKnobs_UpdateHistplotGraph().

  • 24.09.2015: не сделано еще, чтоб при нажатии мышью на некую точку графика показывались бы значения в "тот" момент времени. Давно было задумано, и юзеры (Лебедев) продолжают просить.

    24.09.2015: показывать надо просто ВМЕСТО обычных значений справа (незачем делать доп.виджеты с маппированием их точно поверх), а логику работы с мышью позаимствовать в fastadc_gui/Xh_histplot.

    22.05.2018@утро-дорога-на-работу-около-ИПА: как это удобнее реализовать:

    • Завести в MotifKnobs_histplot_t поле x_index.
      • Обычно в нём -1, что указывает "отображать текущее значение из ручки".
      • А когда юзер куда-то ткнул и елозит, то значение будет >=0.
    • Как отображаются значения из нужной точки, "закрывая" текущие:
      • Обновление всех значений -- цикл с вызовом UpdatePlotRow() для каждой строки -- вытаскивается в отдельную функцию, "UpdateAllPlotRows()".
      • Сама же UpdatePlotRow() при x_index>=0:
        1. Берёт значение не k->u.k.curv, а x_index'ное из кольцевого буфера.

          При x_index>=histring_used -- ставит пустую строку (ради чего придётся вместо MotifKnobs_SetTextString() вызывать XmTextSetString() вручную).

          15.06.2018: также пустую строку ставит при x_index<0 в режиме view_only -- т.к. нету никакого "текущего" значения для показа вне графика.

        2. Плюс вообще ничего не делает с curstate (хотя, конечно, можно форсить KNOBSTATE_NONE).
      • А обычное обновление -- DisplayPlot() -- при x_index>=0 числа не обновляет.

        ...по-хорошему, ещё б и графики чтоб замирали, но с этим никак -- любой expose-event их перерисует.

    • Как идёт взаимодействие с мышью? А по образу и подобию fastadc:
      • При нажатии мыши на графике ставится надлежащее значение в x_index и вызывается UpdateAllPlotRows().

        На движение с нажатой левой кнопкой реакция та же.

      • По ButtonRelease и LeaveNotify делается x_index=-1 и вызывается UpdateAllPlotRows(), тем самым эффективно восстанавливая периодическое обновление.
      • В программе histplot, которая при загрузке файла стоит "на паузе" (точнее, никуда не приконнекчена) и не делает никакого "периодического обновления", стоит отображать значения под курсором даже и безотносительно нажатости кнопок мыши -- просто по любому MotionNotify.

    23.05.2018@утро-дома: еще б полезно было показывать и время, к которому относятся отображаемые числа.

    Технически это несложно:

    • Создаётся виджет класса XmText, располагающийся поверх всех прочих в form'е -- т.е., его надо делать первым. И он создаётся НЕ Managed.
    • На время отображения ему делать XtManageChild(), а после отпускания кнопки (точнее, после убирания "тех" чисел) -- XtUnmanageChild().
    • Эта задача -- вкупе с прочим менеджментом "что держать в колонке чисел, текущее или историю" -- наиболее элегантно решается введением функции-записывальщицы значения в 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) происходит какая-то перверсия: то окно расхлопывается до исходного размера (график меняет размер как надо), то расхлопывается "виртуально" -- содержимое размер/позиции меняет, как для расхлопывания, но сама ширина остаётся, и в результате вся правая часть (с числами) оказывается скрыта за правым краем окна.
      • Причём такие глюки при аттаченьи time_dpy как к правому краю form'ы, так и к левому.

        Очередной глюк в XmForm...

      Так что покамест показывание time_dpy за-#if0'ено (впрочем, показ на нём времени также так и не был реализован -- не дошло до того...).

    • А еще неплохо бы -- для удобства/наглядности -- "точку" показывать вертикальным репером, зелёненьким, как в fastadc.

      Но это сделать не получится: ведь содержимое графика продолжает ехать, а "на паузе" стоят только отображаемые числа.

      О! Можно будет этот "репер" рисовать для стоячих -- при view_only.

    28.05.2018: улучшаем:

    • Показ "репера":
      • Добавлено поле reprGC и аллокирование этого GC.

        ...вот только оказалось, что цвет GRAPH_REPERS виден фиговато, посему пока используется axisGC.

      • В DrawGraph() добавлена отрисовка, при x_index>=0.
      • А в set_x_index() -- вызов оного.

      Отрисовка и вызов делаются только при view_only.

    • Подпись времени:
      • Теперь time_dpy создаётся сразу Managed и ПОСЛЕДНИМ -- т.е., внизу всех.
      • Аттачится он правым краем к форме, а верхом к сетке, так что всегда будет под столбиком чисел. (Хвала Аллаху, вроде всё нормуль -- не прыгает, ресайзу не мешает, ...)
      • Вместо появления/прятанья делается отображение метки времени либо (в качестве прятанья) строки "".
    • Формирование "строки времени" вытащено в 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():

      1. Одной в координате event.x -- и она была в нужной точке под курсором мыши! -- так и стало ясно, что не X11 косячит (аж отлегло -- а то разбираться с ошибками системы всегда тяжко).
      2. Второй в обратно-пересчитанной координате: из сбагриваемого set_x_index()'у делался пересчёт по той же формуле, что используется в DrawGraph() при отрисовке репера.

        И вот ЭТА чёрточка оказалась на пиксел левее первой -- так и стало ясно, что где-то в расчётах ошибка.

      Дальше всё просто -- косяк с отсутствием "- 1" сразу в глаза бросился.

    • Ну и отдельный ляп был уже в DrawGraph(): репер рисовался
      hp->x_index > 0
      вместо надлежащего
      hp->x_index >= 0
      -- из-за чего в самой правой точке не показывался.

    Итак -- косяки исправлены, всё работает. Конкретно (то, что нельзя было достоверно проверить сразу по изготовлении, а только потом, когда и со стороны histplot.c всё (загрузка файла) подготовлено):

    • Отображение значений под курсором -- да.

      В "статике" (view_only) отображается при просто шевелении мышью, нажатость кнопок не требуется.

    • Отображение и времени под курсором, к которому эти значения относятся.
    • И отображение абсолютного времени (в режиме view_only).
    • В статике также рисуется "репер".
    • ...к сожалению, при "живом" графике оный репер рисовать не получится -- модель совсем неподходящая (с x_index), и как бы можно было её модифицировать для подходящести -- неясно.

    Короче: цель достигнута, считаем за "done".

  • 06.01.2016: надо ввести "наследование" диапазонов при отрисовке:
    • если не указан disprange, то пытаться использовать вместо него alwdrange,
    • если и того нет, то, при не-cda_ref_is_sensible() (но саму её использовать нельзя!), последовательно, normrange и yelwrange,
    • и лишь если никого из них нет, то {-100.0,+100.0}.

    Короче, в стиле 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'ленный старый до-наследовательный код.

  • 12.01.2016: забавный аспект поведения: при значениях "nan" на графики ничего не попадает (а nan бывает при нерезолвящихся каналах). Почему? Как так странно происходит преобразование nan->int, что результирующее значение оказывается за пределами графика, вместо вроде бы логичного 0?

    14.01.2016: разобрался. Тест показал, что кастинг nan к int выдаёт значение -2147483648 (=0x80000000). А в обсуждении этого вопроса на StackOverflow сказано

    gcc produces INT_MIN for all out-of-range conversions to int
    плюс
    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 отрисовываются где-то далеко снизу нижней границы графика.

  • 19.04.2016: неприятность: при наличии и сдвинутости scrollbar'а юзеры иногда не замечают, что график смещён, а самые актуальные данные -- спрятаны за границей справа.

    Формально всё правильно, но вот такой взбрык человеческого восприятния.

    Идея от Беркаева, по опыту всяких осциллографов: показывать крупной стрелочкой, что "интересное -- вон там!". На вопрос "а как определять наличие «интересного»?" -- "а просто всегда при наличии чего-то за границей показывать стрелку".

    Идея от меня -- в меру моего понимания: можно на боковых секциях axis рисовать крупные треугольники, светло-голубого цвета, направленные в ту сторону. Т.е., справа -- треугольник направо, слева -- налево.

    • Светло/бледно-голубые -- на фоне "пергаментного" фона особо мешать не станут, но будут заметны.
    • Отрисовка проблем не составит -- axis при изменении вначале всё равно очищается, ну а тут ещё треугольнички рисовать перед тиками и подписями.

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

    • ...и еще вопрос с треугольником слева: если правый появляется/исчезает при скроллинге, то левый -- просто при добавлении данных, а перерисовываются ли при этом axis?

      Несколькими минутами позже:

      • Осмотр исходника -- SetHorzbarParams() -- показывает, что, скорее всего, перерисовываться будет, т.к. 1 (флаг перерисовать) возвращается при ЛЮБОМ изменении параметров.
      • Другое дело, что этот результат де-факто никем не используется. В отличие, видимо, от Xh_plot+Xh_viewport, откуда эта архитектура взята.
      • С третьей же стороны, для DrawAxis() указывается do_clear=1 практически ВСЕГДА (кроме ExposeEvent'а).

      Так что, видимо, всё окей.

    07.06.2016@пляж-15:30: а может, сделать проще -- при любом ненулевом положении scrollbar'а делать ему XmNbackground:=red?

    07.06.2016: сделано. Собственно действие выполняется "write-accessor'ом" SetHorzOffset(), через который теперь проходит запись в horz_offset, и если не-нулёвость уставляемого значения не совпадает с не-нулёвостью текущего, то он уставляет нужный цвет.

    Единственный "минус" -- что краснеют лишь бегунок и стрелки, а они при большой длине записи не очень бросаются в глаза, т.к. бегунок становится маленьким, а бОльшую часть места занимает "корыто" (trough), которое остаётся серым, не краснея.

  • 25.05.2016: в Chl_histplot.c и MotifKnobs_histplot_noop.c не хватает реакции на кнопку [Save] -- MOTIFKNOBS_HISTPLOT_REASON_SAVE. Юзеры (в лице Ваагна) уже интересуются, им нужно сохранение.

    30.09.2016: делаем.

    • Собственно сохранение -- публичная MotifKnobs_SaveHistplotData().

      В отличие от v2, она лишь сохраняет, не выдавая никаких сообщений в statusline и event-log, но возвращает 0/-1.

    • Использование ("обвязка"):
      • Поскольку шаблоны имён файлов теперь нужны не только в Chl, а много где, то #defineCHL_*_PATTERN_FMT переехали из Chl.h в datatree.h под именами DATATREE_*_PATTERN_FMT.
      • MotifKnobs_histplot_noop.c: просто. Но пришлось в privrec'е сохранять также указатель на свой k, для добычи subsys'а.
      • Chl_histplot.c: копия предыдущего с минимальными изменениями -- добавлены ChlRecordEvent() и subsys добывается через _ChlPrivOf().

    Итого -- готово!

    17.06.2018: есть еще потребность -- чтоб в строке заголовков печатались бы диапазоны (disprange) в histplot.c-совместимом виде.

    Отдельный вопрос: не выполнять ли в histplot.c авто-скейлинг?

    18.06.2018: а еще там не хватает выдачи префикса %DPYFMT!

    18.06.2018: делаем.

    • Префикс dpyfmt -- тривиально.
    • С disprange сложнее: он ведь может у ручки и отсутствовать (когда и берётся умолчательный [-100,+100]); а может браться не из _PARAM_DISP_{MIN,MAX}, а "наследоваться" из иных диапазонов.
      • Решение простое: добычу диапазона с наследованием выполняет GetDispRange() -- вот пусть она и возвращает: нарыла какой-то осмысленный диапазон, или взяла умолчание.
      • Она так и переделана.
      • А в MotifKnobs_SaveHistplotData() в выдаче заголовков она вызывается, и если вернёт true, то добытый диапазон тоже печатается -- "правильно", с обрезкой с использованием snprintf_dbl_trim().
    • units отныне пишутся не в скобочках после ident -- IDENT(UNITS), а отдельным параметром ПЕРЕД идентификатором: units=NNN. Этот формат теперь воспринимается histplot.c::AddGroupingChan()'ом, и загруженный из файла график выглядит точно так же, как и в оригинале.
    • Timestamp'ы берутся, при наличии, из timestamps_ring[]. Т.е., при повторном сохранении загруженных из файла данных новый файл содержит идентичные времена и информация не теряется.

      Есть, конечно, некоторый вопрос -- что будет, если вдруг timestamps_ring_used меньше, чем длина данных (значение max_used). Тогда времена за границей будут бредовыми (т.к. начнут считаться как ТЕКУЩЕЕ-t*cyclesize_usec), что создаст "разрыв"; но это ситуация невероятная (сейчас) и на неё можно забить.

  • 11.06.2016утро-пляж: насчёт человеческих времён на histplot'е.

    Предыстория: еще энное время назад юзеры (в лице Ярика Куленко, см. bigfile-0001.html за 19-10-2012) просили подписывать вдоль горизонтальных осей не относительное время (сколько-то назад), а абсолютное (HH:MM:SS).

    11.06.2016утро-пляж:

    • Писать их -- просто.
    • А перерисовывать при сдвиге графика -- тоже несложно: XClearArea() верхней и нижней полос axis'а (в идеале -- НЕ трогая tick'и).
    • Переключение между режимами отображения -- клик на этих полосках.
    • 13.06.2016@ключи-вечер: только немного некрасиво будет, что в подписях времена "бегут".
      • Естественнее (для человека) было бы, чтоб статичные времена "ехали" справа налево (вместе с тиками).
      • Но такое сделать уже технически намного сложней:
        • Для начала, надо как-то "привязываться" к круглым временам -- для периода 1с это будет "круглая минута". Вот как? Просто по "остаток_от_деления(time(),60)==0" -- вряд ли можно.
        • Система координат при этом радикально меняется: тики (и соответствующие им вертикальные пунктирные на графике) рисуются не с отступом кратно периоду от правого края, а от "ближайшей" к правому краю "круглой минуты".
      • Короче: сейчас сделаем "по-простому", а там дальше посмотрим.

    23.05.2018@утро-дома: "то" пока не делаем, зато отображение абсолютных времён и можно и нужно сделать для "статичного" варианта -- когда данные загружены из файла.

    По сегодняшнему проекту (см. детали в разделе по утилите "histplot") из файла будут читаться и времена, в буфер MotifKnobs_histplot_t.timestamps_ring.

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

    Следствие 1: надо б будет вытащить генерацию текста подписи в отдельную функцию -- чтобы использовать её же для показа времени при елозеньи мышью по графику.

    Следствие 2: и в MotifKnobs_SaveHistplotData() надо б тогда эти же числа использовать, вместо вычислений, чтобы цепочка "чтение,сохранение" не приводила к потере данных.

  • 31.08.2016@Снежинск-каземат-11: портируем свежесделанную в v2 фичу "horz_ctls" (расположение [Save], [Mode], [XScl] справа налево вместо снизу вверх).

    31.08.2016@Снежинск-каземат-11: чуть размашистее, чем в v2, т.к. тут публичный интерфейс, содержащийся в MotifKnobs_histplot.h -- туда и добавлен MOTIFKNOBS_HISTPLOT_FLAG_HORZ_CTLS.

    И в утилиту histplot добавлена возможность указать "horz_ctls=1" -- на ней и проверено.

  • 10.05.2018: давно (еще с v2?) стоял вопрос: а что делать с позицией scrollbar'а при смене масштаба? Ведь там записан сдвиг в текущих "единицах" (1, 10, 60 отсчётов, в зависимости от масштаба), и при смене "коэффициента сжатия" то значение становится слаборелевантным.

    (Сейчас найти место, где это обсуждалось, не удалось. То ли в 0001, то ли в 0002; а может, вообще в разделе не по histplot, а по XhPlot или и вовсе по fastadc.)

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

    15.10.2018: Лебедев повторно внёс то же предложение: при любом изменении масштаба ставить скроллбар на начало.

    13.02.2023: и опять Лебедев жаждет того же.

    14.02.2023: фиг с ним -- делаем. Вся работа в XScaleKCB(), которая ради этого слегка перетряхнута:

    • Теперь не просто молча ставится выбранный селектором масштаб, а проверяется, не совпадает ли он с текущим и если да, то не делается ничего.

      Смысл -- чтобы "тождественное" нажатие селектора не перекидывало бы на начало.

    • Главный же вопрос/проблема -- а как устанавливать скроллбар в 0, с учётом всех нюансов (что он не в 0 цвет меняет; да и "в 0" надо и внутреннее значение 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: на пульт закинуто.

  • 27.06.2018: еще напасть: при ресайзе окна в расчёте скроллбара как-то не учитывается горизонтальный масштаб.

    28.06.2018: разобрался -- всё там учитывается.

    • А проблема в том, что НЕ делается перерасчёт размера скроллбара при ресайзе, а делается только при поступлении новых данных (обновлении) -- вот и не было раньше заметно.

      Сейчас же при загрузке из файла никаких обновлений нет, вот и вылезло.

      И, кстати, сейчас проверил: в реальности проблема еще с v2'шного Chl_histplot.c: там, если порвать соединение с сервером (по циклам от которого идёт обновление/сдвиг самописца), тоже перестаёт реагировать на ресайз; и точно так же обновляется, если сменить горизонтальный масштаб.

    • Добавлен вызов SetHorzbarParams() в GraphResizeCB() в ветку перед DrawGraph() -- по аналогии с Xh_viewport.c::ViewResizeCB().

    Проблема ушла.

  • 04.01.2019: Алексей Медведев передал жалобы чёмцев, что у них "раньше можно было добавить интересующий канал на самописец внизу окна, а теперь открывается какая-то новая панелька" (это после переезда тамошней сварки с v2 на v4).

    Ну оно и понятно -- архитектура histplot'а в v4 иная, тут он не единственный, а может быть много независимых экземпляров.

    04.01.2019@вечер-по-пути-к-родителям-около-ЗолотойРощи (Морской, 26): ну и что -- вводить возможность указывать, что такой-то экземпляр histplot_noop'а должен ловить событие ToHistPlot (Shift+Btn3) вместо Chl_histplot'а?

  • 12.04.2019: наличествует проблема с логарифмическим отображением -- обнаружилось на xweldcc:
    1. Как утверждают Семёнов и Косачёв, некорректно ведёт себя отображение вакуума: когда вакуум ухудшается (т.е., число в паскалях РАСТЁТ), кривая на графике идёт вниз.
    2. Во всех подписях к вертикальной оси отображается "nan", вместо надлежащих границ/промежуточных.

      В старом скрине weldcc всё показывается как надо при тех же границах.

    Границы -- 1e-10-1e0.

    16.04.2019: разбираемся. После получаса глазения в код -- отличающийся от v2'шного, ради "оптимизации" -- была обнаружена странность: там ДВАЖДЫ применяется log() (показано для mindisp, а для maxdisp аналогично):

    1. Сначала делается mindisp = log(mindisp) -- это нововведение в v4.
    2. Потом RESCALE_VALUE() также выполняется с log() -- вот это было и в v2.

    Чуток исследований/расследований/археологии (излагается не по логике/временнОму развитию, а по мере разбирательства):

    • Первопричиной модификации (по сравнению с v2) было желание оптимизации.

      Чтобы не делать постоянное вычисление логарифма.

      Этим также обусловлено добавление одноразовое вычисление is_logarithmic.

    • И структура кода в DrawGraph() и DrawAxis() одинаковая.
    • Но в v2 она была РАЗНАЯ!
      • В 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:
      histplot %5.0e:disprange=1e-10-1e0:localhost:59.a.0 hist_period=0.2
    • Последовательно записываем числа 1e-10...1e0, меняя степень:
      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: обнаружилась неприятность: в скрине baachi при нажатии правой кнопки мыши на первых двух каналах самописца (vacuum и linear) программа SIGSEGV'ится.

    12.04.2019: стал разбираться.

    • Занятненько: в реальности этих 2 каналов/ручек просто не существует, они в add= остались "на всякий случай". Т.е., программа должна просто ругаться и пропускать ненайденную ручку.

      Добавление отладочной печати показало, что виноват datatree_find_node() -- он почему-то вместо NULL возвращает что-то другое (но не совсем бред, а указатели куда-то рядом с существующими ручками, идущими в списке далее).

      Почему оно не сваливалось сразу, при "добавлении" -- загадка. А сваливалось в момент нажатия MB3, судя по backtrace, потому, что пыталось сделать XhWindowOf(NULL), что приводило к XtParent(NULL).

    • (получасом разборок позже) а вот и нифига!!! Эти ручки ЕСТЬ, просто они в невидимом (invisible) контейнере.

      Так что datatree_find_node() невиновен (хотя глядя на его код, ум за разум заходит и слабо понятно, как он работает -- видать, на пляже у меня пробуждается гениальность :D).

      А виновато именно то место, которое пытается что-то получить при помощи XhWindowOf(), да ещё и не удосужившись проверить !=NULL.

    • Оным местом является Chl_knobprops.c::_ChlShowProps_m().

      Очевидно, раньше никогда не встречалась ситуация, чтоб методу скормили узел без ручки.

    • ...и да, понятно, «почему оно не сваливалось сразу, при "добавлении"» -- потому что и не было там никакой причины для падения.
    • В Chl_knobprops.c::_ChlShowProps_m() исправления внесены: оно теперь пытается найти вверх по иерархии первый же узел с w!=NULL.

      Но там надо будет ещё помозговать над решением: либо вытащить в отдельную функцию (т.к. юзеров в сумме трое -- ещё BigVals и сам Histplot), либо ещё и иной метод использовать (например, брать w от корневого узла).

    А тут проблема, пусть и неместная, решена, так что "done".

  • 12.04.2019: и в Chl_histplot.c::_ChlToHistPlot_m() сделан фикс от SIGSEGV'а.

    Фикс чуть отличается от KnobProps/BigVals'ового, т.к. тут ещё и _ChlPrivOf(win) выполняется.

  • 09.10.2019: запинана возможность указывать режим отображения (line/wide/dots/blot) программно и параметрами.

    09.10.2019: это было сделано "за компанию": возился с fastadc+Xh_plot, которые нужно было научить рисовать "толстые" линии, полез сюда на "посмотреть, как их делают и как указывают включение из параметров", и обнаружил, что в histplot'е работает только переключение вручную, хотя параметр "mode=..." в MotifKnobs_histplot_noop.c вроде бы и предусмотрен (но просто ничего не делает).

    Итак:

    1. MotifKnobs_histplot.c: тут нужно было добавить "программную управляемость".
      • Для начала k_md и k_xs (этот -- за компанию, на будущее) были вытащены из MotifKnobs_CreateHistplot() (были короткоживущими переменными) в MotifKnobs_histplot_t (теперь это поля, "долгоживущие").
      • Заведена MotifKnobs_SetHistplotMode() (и меняет режим, и отображает его в селекторе), для доступа снаружи, ...
      • ...и ModeKCB() также переведана на неё.
    2. MotifKnobs_histplot_noop.c: поскольку основа в параметрах уже была, то оставался минимум:
      • Добавлен вызов MotifKnobs_SetHistplotMode() при надобности.
      • А "надобность" -- потому, что histplot_privrec_t.mode теперь по умолчанию =-1.
    3. histplot.c: тут просто воспроизведён функционал из knobplugin'а:
      • Добавлен параметр -- поле globopts_t.mode, табличка mode_lkp[] и строка в text2globopts[].
      • И вызов MotifKnobs_SetHistplotMode() при надобности.
      • Ну и новая строчка в help не забыта.

    Всё работает.

  • 13.02.2023@планёрка-в-пультовой-~10:20: Лебедев высказал хотелку, чтобы где-нибудь на панели histplot'а писалось бы конкретное время суток -- для того, чтобы на скриншотах можно было "привязать" график ко времени (а то там на горизонтальной оси времена относительно текущего момента, а когда "текущий момент" -- из скриншота неясно (только время/имя файла смотреть, но оно не всегда легкодоступно -- например, в журнале)).

    Сначала общая моя и ЕманоФеди ракация "да ну нафиг!", но потом я решил-таки подумать.

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

    Полез в файл разбираться:

    • Поле называется time_dpy, длину оно имеет под "HH:MM:SS.nnn" -- т.е., время влезет, а вот дата никак.
    • Оно managed ВСЕГДА (после разбирательств 28-05-2018), так что да -- формально всегда есть куда выводить время.
    • Отображением в нём времени под мышью заведует 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() вместо явной выдачи "" (при осознании, что переходим из режима отображения под курсором к неотображению),
    • а уж она пусть сама смотрит, что надо отобразить -- пустоту ли или текущее время.

    Делаем:

    1. Собственно ShowCurTime(), формирующая пустую строку при ненадобности отображения, а при надобности -- строку со временем, включающим миллисекунды в случае cyclesize_us < 1000000, и затем эту строку выводящая в поле.
    2. set_x_index() переведен на её вызов.
    3. В DisplayPlot() вызов также добавлен, рядышком с UpdateAllPlotRows(), условно при не-отображении под курсором.

    Проверил -- на вид вроде работает как надо, в т.ч. при режиме просмотра файла (когда при неотображении под курсором показывает пустоту).

    20.02.2023: на пульт закинуто. Будем ждать возможных отзывов от дежурных и прочих Лебедевых.

  • 13.06.2024: (формально как бы касается КЛИЕНТОВ MotifKnobs_histplot'а, но, поскольку обоих сразу и к их разделам не совсем профильно, а самописценья касается впрямую, то помещаем пункт сюда) вчера ЕманоФедя пожаловался, что linthermcan при переводе графиков в горизонтальный масштаб "1:60" (чтоб посмотреть за сутки) начинает дико тормозить и напрягать X-сервер. Причём тормозит настолько, что фактически перестаёт реагировать на действия юзера, помогает только kill -9.

    Я-то Феде сказал, что это скорее проблема "ну не требуйте от софтины больше, чем успевает железо!", но обещал посмотреть, что можно сделать. Ведь совсем необязательно всё перерисовывать каждые 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.
    • ...но ещё со вчера не отпускала мысль, что это так себе решение: ведь оно требует от юзера указания КОНКРЕТНОГО "коэффициента замедления обновления графика", а он в разных случаях (и при разных количествах точек!) разный.
    • И нельзя ли всё-таки как-нибудь сделать АДАПТИВНОЕ решение -- чтобы
    • @~18:00, поход в Оливку за водой: а ведь -- "тормоза" это когда обновления поступают чаще, чем программа успевает на них реагировать, так? Т.е., в момент, когда только-только закончена отработка очередного обновления, уже пришло -- и даже прошло! -- время делать следующее?
      • В таком случае можно смотреть, что если в точке перед вызовом перерисовки текущее время уже ПОСЛЕ времени, на которое намечено следующее, то обновление нужно пропустить.

        Таким образом мы пресечём накапливание несделанной работы -- обычно будет оставаться максимум 1 несделанное, т.к. следующие "в очередь" просто не добавятся.

      • А чтобы (под большой нагрузкой) не возникала ситуация, что пропускаются ВСЕ обновления, надо ввести счётчик пропущенных обновлений, и хотя бы каждое 10-е всё же исполнять принудительно.

        И после обновления счётчик пропущенных сбрасывать. Это убъёт двух зайцев: и новый цикл подсчёта после принудительного обновления запустит, и в момент "мы всё-таки успели -- обновимся!" сбросит счётчик неуспеваний.

    Реализовываем.

    • Первым делом в HistoryCycleCallback() добавлено получение timenow и сравнение его с cycle_end;
    • ...если уже позже -- то сначала сделана диагностическая печать.

      Да, под нагрузкой -- "linthermcan hist_period=0.01 hist_len=864000", т.е., 100Hz -- видно, что начинает не успевать, и это неуспевание всё накапливается (добавлена выдача числовых значений обеих точек времени, и timenow опережает на всё большее время).

    • А затем к печати добавлен return (пока безо всяких счётчиков).

      И да, ситуация магически улучшилась: хоть тормоза остались (ибо цепочка v5p2,duct,x10sae,star,p320t), но отставание расти перестало.

    • @вечер, перед сном: но как бы всё-таки выбрать этот "предел пропусков для принудительной отрисовки"? Ведь для "hist_period=0.01" -- 100Hz -- эти 10 раз будут недостаточны, отставание будет накапливаться; а для 1Hz получится "раз в 10 секунд", что тоже не айс.

      Как-нибудь бы в зависимости от периода; но как?

      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" дал те же результаты -- по сети подтормаживает, но на конечное время и остаётся отзывчивой.

MotifKnobs_cda_scenario_knob:
  • 02.10.2014: создаём такой кнобплагинчик, для возможности управления "сценариями" -- т.е., формулами, исполняющимися продолжительное время; чтоб им можно было хоть "стоп" сделать, при помощи cda_stop_formula().

    Побудительный мотив непосредственно сейчас: Лебедеву надо циклировать один канал в linmagx, писать скрипт для этого под v2 крайне лень, а тут всё предусмотрено -- грех не воспользоваться.

    02.10.2014: сделан в рудиментарном варианте -- просто кнопки [Start] и [Stop]. Даже статус исполнения формулы не показывает -- ибо никак, нет никаких механизмов узнать его.

MotifKnobs_cda_leds: и MotifKnobs_cda_leds_noop:
  • 24.11.2015: создаём. Помещаться эти модули будут в libMotifKnobs_cda.a.
  • 24.11.2015: модуль MotifKnobs_cda_leds.c -- примерный аналог v2'шного Chl_leds.c.

    25.11.2015: внутренности делались копированием v2'шных, но есть идеологические отличия.

    1. Кроме
      • "объекта"-структуры "один LED" (именуемой ныне MotifKnobs_oneled_t, MotifKnobs_oneled_*() API)
      • также введён более высокий уровень "блок LED'ов", представленный структурой MotifKnobs_leds_t и API MotifKnobs_leds_*().

      Последний

      1. Заботится о создании GUI с группой -- ему в MotifKnobs_leds_create() передаются cid и parent, которых он сохраняет в свой объект.

        Ему также указывается parent_kind, могущий сейчас был как _UNKNOWN, так и MOTIFKNOBS_LEDS_PARENT_GRID -- тогда делаются все надлежащие манипуляции XhGridSet{,Child}*().

      2. Поддерживает менеджмент списка "поштучных", в MotifKnobs_leds_t.leds (см. grow() ниже).
    2. Вместо просто "создать N штук LED'ов" теперь работа динамическая: оно растёт по мере надобности. Это обеспечивается функцией MotifKnobs_leds_grow() -- собственно, _create() после создания объекта просто вызывает _grow(), а уж клиент потом должен её дёргать в реакции на событие NEWSRV.

    25.11.2015: проверено, работает, "done".

    01.12.2015: с вводом события SRVSTAT надобность в MotifKnobs_leds_update() исчезает -- теперь штучные LED'ы должны обновляться индивидуально по мере надобности.

    17.12.2017: да, переведено на ту индивидуальность. Через ДВА года с лишним, блин!!!

  • 25.11.2015: и MotifKnobs_cda_leds_noop.c -- аналог v2'шного Chl_leds_knob.c. Нехитрая обвязка вокруг MotifKnobs_cda_leds.c.

    04.12.2015: он всё еще пользуется KeepaliveProc, а надо бы перевести на CDA_CTX_R_SRVSTAT и CDA_CTX_R_NEWSRV.

    17.12.2017: переведён (хотя и не он, а сам _cda_leds.c), так что "done".

  • 17.12.2017: пора переводить MotifKnobs_cda_leds.c на автономность -- чтоб он сам ловил события SRVSTAT и NEWSRV, не требуя от "хозяина" ни этого, ни ежесекундного вызова MotifKnobs_leds_update().

    Также, поскольку теперь объект становится таким "активным", должен быть метод его удаления (для cx-starter'а, который сможет пере-считывать конфигурацию).

    17.12.2017: делаем:

    • Добавлен собственный leds_context_evproc(), реагирующий на SRVSTAT и NEWSRV.
    • "Деструктор" -- MotifKnobs_leds_destroy(). В нём и safe_free(leds->leds) делается, и context_evproc снимается.

      Вызов его в DestroyLedsNoop() добавлен.

    • Проверено:
      1. И в без-конкретно-серверных клиентах (которые zeroconf-резолвят) индикатор появляется. В т.ч. если сервер запущен ПОСЛЕ клиента -- то "прорастает" в тулбаре, сдвигая кнопки правее.
      2. И обновления статуса по дисконнектам/коннектам теперь мгновенные, а не "в течение секунды".
      3. А вот работу деструктора пока проверить не на чем.
    • Теперь удаляем старый механизм "обновление клиентами раз в 1 секунду" (LEDS_KeepaliveProc'ы):
      • В родственном MotifKnobs_cda_leds_noop.c.
      • В Chl_app.c.
      • В cx-starter.c.
        • Но там этот механизм и так был закомментирован, а использовалась самостоятельная слежка за 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)?
    • Надо было бы убрать из публичного 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: хоцца сделать поддержку "команды юзером на выполнение реконнекта" путём нажатия на лампочку сервера. Давно уже задумано, но наконец пришло время этим заняться.

    12.05.2020: невтепрёж становится -- пора!

    • @утро, зарядка: видимо, придётся пилить API: надо добавлять возможность cda_led'у передавать информацию своему нанимателю.

      Да и как-то придётся передавать в ledCB ДВА параметра: указатель на leds и номер сервера.

    • Чуть позже: а ведь можно схитрить и номер сервера НЕ передавать: просто идти по списку oneled'ов и искать тот, чей w совпадёт с получателем callback'а.

      Об эфективности тут можно не беспокоиться -- событие крайне редкое и всё равно с юзером связанное.

    • Ещё чуть позже: по результатам просмотра исходников -- а ведь можно обойтись и без изменения API!

      У нас уже есть поле cid в MotifKnobs_leds_t -- ну так можно прямо из cda_leds и дёргать соответствующий метод cda.

      ...да, как бы это не совсем красиво -- с точки зрения разделения полномочий; но зато дёшево и просто. А если станет не хватать (захочется мочь другое действие повесить на клик по лампочке; что вряд ли), то всегда можно будет переделать.

    • И ещё чуть позже: да, готово. В ledCB() добавлены:
      1. Поиск. Причём по умолчанию -- пока не найдено -- значение nth ставится =-1; так что если вдруг (как бы*) найдено не будет -- то даже так не беда, поскольку предполагаем сделать правило, что при неуказанности номера сервера операция вызывается для всех.
      2. Вызов cda_reconnect_srv() (пока закомментированный).

        13.05.2020: раскомментирован, в связи с его опубликованием.

    Ну пока и всё.

    13.05.2020: проверено при помощи отладочной печати -- да, нажатие на лампочку сервера приводит к вызову запроса на реконнект.

    Так что здесь -- "done".

MotifKnobs_split_cont:
  • 04.07.2016: создаём.
  • 04.07.2016: нужен для v5h1adc200s.

    04.07.2016: сделано, в основном копированием из v2'шного -- файл-то маленький. Вроде работает.

MotifKnobs_scroll_cont:
  • 04.07.2016: создаём.
  • 04.07.2016: тоже нужен для v5h1adc200s.

    05.07.2016: работает точно так же, как и в v2. Т.е., посредственно :)

    Но "для сейчас" -- достаточно.

MotifKnobs_slider_knob:
  • 11.08.2016: однако, а ведь не было до сих пор! Понадобилось для lincamsel'а.

    Делаем.

MotifKnobs_text_knob:
  • 01.09.2016@Снежинск-каземат-11: компонент-то давно есть (кабы не с 2007-го), но раздела для него не было; сейчас занадобился -- создаём.
  • 01.09.2016@Снежинск-каземат-11: подзадолбала невозможность указывать параметр "withunits". Оно и так default, а указывабельно его отсутствие (и это есть отличие от v2 -- как раз года с 2007-го), но хочется мочь указывать обе альтернативы ЯВНО.

    Короче -- сделано: просто добавлен один PSP_P_FLAG(), вроде бы без побочных эффектов.

  • 28.04.2018: есть проблема с возможностью указать "nounits" у ro-ручек. "Указать"-то можно, но это просто не сработает.

    28.04.2018: детали и анализ:

    1. Для не-is_rw-ручек форсится no_units=1. Выглядит странно, да?
    2. А это потому, что флаги units/no_units реально влияют на наличие/отсутствие дополнительного виджета units, добавляемого лишь rw-ручкам.

      У ro-ручек же он отсутствует, а содержимое k->u.k.units добавляется прямо к числу.

    3. Делается это добавление прямо в MotifKnobs_SetTextString() -- не обращая никакого внимания на значение флага no_units (которого на том уровне и нет), а глядя лишь на то, что виджет не-editable (даже НЕ на is_rw!).

    Таким образом, единственный способ избавиться от показа единиц у ro-ручки -- не указывать их вовсе.

  • 15.08.2022: в EPICS Tech-talk позавчера задали вопрос "а можно ли сделать так, чтобы виджет spinner при ручном вводе цифр не требовал бы нажатия Enter?". И сегодня Kay Kasemir ответил -- "Re: CSS spinner and text box instant write" -- так там логика В ТОЧНОСТИ как у меня:
    • "Режим ввода":
      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.
      -- тут просто в точности всё так же.
    • И пояснение того, ПОЧЕМУ нельзя отсылать все вводимые цифры он привёл почти точно то же, что я подумывал туда написать (но не стал...): "что, если мы меняем значение с 98 на 101 -- слать последовательно 1, 10, 101?".
MotifKnobs_inttext_knob:
  • 06.02.2020: создаём раздел здесь, чтобы шёл сразу после родственника "text_knob".
  • 06.02.2020пешком по улице между 4-м и пристройкой, забрав у Павленко перепрошитые ADC250: поднадоело, что нет никакой возможности выводить 16-ричные числа.

    А может, просто сделать ОТДЕЛЬНЫЙ knob-компонент, который бы выводил именно ЦЕЛЫЕ числа? А формат -- чтобы использовал обычный dpyfmt, превращая %f в %d, а %a в %x.

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

    22.02.2020@дома-суббота, вечер, за просмотром первых 2 серий "Триггер": делаем.

    • Предварительные мысли (ещё при начале работ пару недель назад) были такие:
      1. Делаем простейший вариант - только для readonly, чтобы удовлетворить потребности adc250_dui.c.
      2. @лыжи-с-неделю-назад: Формат добываем из обычного %dpyfmt, превращая %f в %d, a/%A в %x/%X, а %o делаем, %например, из %e.
    • Для скорости и простоты решено НЕ делать "простейший" вариант, а, наоборот, просто копируем содержимое MotifKnobs_text_knob.c целиком, со всем созданием/колоризацией IncDec'ов, grpmark'ов и т.д.

      Модификация только в определении vmt - она тут одна-единственная.

    • MotifKnobs-функции, имеющие double-специфику, скопированы локально, с префиксом 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-формата: в privrec добавлено поле int_dpyfmt, и в CreateIntTextKnob() первым делом выполняется цепочка:
      1. Распотрошение имеющегося формата посредством ParseDoubleFormat().
      2. Замена conv_c в соответствии с "правилом"; любой неизвестный символ переводится в 'd'.
      3. Сборка в int_dpyfmt посредством CreateIntFormat().
    • Ну и списки компилируемых файлов и стандартных knobplugin'ов добавлен.

    Сделано использование в adc250_gui.c -- работает! В т.ч. ALTFORM-вариант %#10.0a, добавляющий префикс "0x".

    Итого -- первоначальная задача поддержки ro-int-вывода для шестнадцатирички решена, а уж если понадобится что-то ещё, то и будем думать о реализации либо Int-вариантов для rw-ручек, либо вообще полноценной поддержки Int (23.02.2020: благо, микро-проект уже есть).

    04.02.2021: сделана поддержка rw-ручек, НЕ по микро-проекту.

    • Просто локально скопированы недостающие функции из MotifKnobs_internals.c, с тем же префиксом NEW_, и в "них" используется int вместо double.
    • Причём конкретно NEW_HandleTextUpDown() сделана пустой:
      1. во-первых, больно уж много там возни,
      2. а во-вторых, её "мозги" просто перевести с double на int нельзя никак -- там и игры с гранулярностью шага и grpcoeff (для целочисленных лишённые смысла), и групповое изменение, в случае потенциально-разнотипных (double и int) ручек становящееся странным.
      3. 05.02.2021@вечер, ~19, Быстроном: а ещё тут могут перемешаться в одной группе целочисленные и обычные вещественные поля -- что будет совсем малорадостно.

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

      Так что пока пусть стрелки вверх/вниз просто не работают.

    • Задействован IntTextKnobColorize_m(), ранее почему-то не использовавшийся -- вместо него стоял Common.

    Но пока не проверено. А надо бы -- делалось-то ради диагностического скрина для rfmeas_l_timer (адреса и значения регистров), вот его и нужно изготовить.

    05.02.2021: проверил -- да, работает. Хотя сначала казалось, что нет -- целочисленный формат "%x" себя ведёт сильно отлично от вещественных "%f", и в "обычной жизни", при обычном использовании для вывода, с этим не сталкиваешься:

    • Поле "precision" -- которое после точки, "%6.0" -- указывать НЕ надо, ни в коем случае.

      Потому, что это для целочисленных форматов -- "the minimum number of digits to appear".

      В результате формат "%04.0x" число 0 выдаёт как пустоту -- 4 пробела. Что, естественно, для полей ввода категорически неприемлемо (вообще-то это неприемлемо для всего, и зачем такое поведение -- для меня загадка).

    • Другая неприятность -- что флаг '#' ("%#06x") вовсе НЕ гарантированно выдаёт префикс "0x", а только для НЕнулевых значений. Нулевое же значение будет выдано как цепочка нулей, количеством по указанной ширине поля -- "000000", вместо ожидаемого "0x0000".

      Но это хотя бы некритично, а лишь просто некрасиво (хотя смысл такого поведения также неясен).

    06.02.2021: сделал всё-таки и инкремент/декремент по стрелкам и колесу мыши -- наполнение NEW_HandleTextUpDown().

    • Сделано ОЧЕНЬ простым.
      • Шаг при возможности использует из параметров ручки, при отсутствии -- просто 1.
      • При нажатом Alt шаг умножается на 10.

        Ни Ctrl (шаг/10), ни Shift (не-immed) не используются.

      • Никаких групповых изменений.
    • Возникла лёгкая странность: при шестнадцатиричном формате менялось только значение 0, до +1 или -1, а дальше -- ёк.

      Причиной оказалось то, что в MotifKnobs_ExtractIntValue() в вызове strtol() указывалось base=10 -- вот фиг знает, зачем так было сделано.

      После исправления на 0 всё пришло в чувство.

    09.02.2021: убрано это лишнее ".0" из dl250.subsys и adc250_gui.c.

MotifKnobs_tabber_cont:
  • 12.05.2018: понадобился, для weldcc -- похоже, первый раз за 3 года. Придётся делать.
  • 12.05.2018: приступаем.

    12.05.2018: итак:

    • Выполнены стандартные ритуалы по добавлению нового типа контейнера в Makefile и в MotifKnobs_knobset.c.
    • Поскольку XmTabStack может наличествовать не всегда, а лишь начиная с Motif версии 2.2, то есть некоторая условная замудрённость.

      Здесь определение наличия и взведение XM_TABBER_AVAILABLE выполняется в MotifKnobs_tabber_cont.h.

    • Скелет MotifKnobs_tabber_cont.c взят от split'а.
    • Содержимое -- в основном берётся из v2, подглядывая детали создания содержимого в современных *_cont.c.

    В weldcc работает, помечаем как "done".

MotifKnobs_matrix_vect: (ex-MotifKnobs_text_vect)
  • 20.07.2018: это простейший (а может, и основной) и умолчательный knobplugin для узлов типа DATAKNOB_VECT.
  • 20.07.2018: делаем.

    20.07.2018: сварганено в минимальном варианте -- скелет, в котором сделано отображение и колоризация, а создание пока простейшее, просто ячейки (с опциональными номерами), но без callback'ов/event-handler'ов для реального редактирования и отправки.

    21.07.2018: первые проверки и добито до минимально приличного вида.

    • Неиспользуемым (по nelems) ячейкам ставится строка "-" и они дисэйблятся (к сожалению, на полях ввода это видно плохо, потому и ставится "-").
    • В [nelems] для ro-данных отображается не просто "nnn", а "nnn:" -- чтоб видуально лучше отличаться от ячеек с данными.
    • Кнопки [=][X] сейчас ВСЕГДА кладутся справа от поля [nelems]. Над колонкой с номерами они б не влезли.

    23.07.2018: пора делать callback'и/event-handler'ы для реального редактирования и отправки.

    • Сделаны. Вроде все. Да, мутроно.
    • Неприятность в том, что куча полей -- это не единый виджет, так что редактирование не атомарно.

      Следствие -- и фокус мы корректно отслеживать не можем, в смысле как у обычных текстовых полей: "при потере фокуса делать cancel_knob_editing()".

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

    • Доходит до того, что перемещаешь окно мышью -- а он горит. Переключаешься AnotherLevel'ом в другой экран или даже десктоп -- а прямоугольник остаётся на том же месте.

    А так -- вроде работает.

    23.07.2018: а ведь по-хорошему можно было сделать и более простую форму -- реально ТЕКСТОВОЕ поле, в формате [ЗНАЧЕНИЕ{,ЗНАЧЕНИЕ}].

    ...либо -- N:[ЗНАЧЕНИЕ{,ЗНАЧЕНИЕ}]

    • С ним работа была бы тривиальна:
      • Выдача -- простая. Хотя некий вопрос в требуемых длинах буферов и в возможности указания ширины поля.
      • Парсинг -- аналогично console_cda_util.c::ParseDatarefVal().
    • И не было бы проблем с "атомарностью редактирования".
    • Вот как раз ЕГО надо было б называть "text" (MotifKnobs_text_vect.c), ...
    • ...а нынешнее -- "grid" (MotifKnobs_grid_vect.c).

      Не -- "matrix" (MotifKnobs_matrix_vect.c).

    • А умолчательнам оставить именно text.

    24.07.2018: да, переименовано: теперь ЭТО -- MotifKnobs_matrix_vect.c, а имя "text" оставлено для будущего реально простейшего.

    Умолчательным пока будет именно matrix, а text'у отдадим это звание, когда он подрастёт.

    Засим считаем за "done". Если захочется усовершенствований (многоколоночность etc.) -- отдельными пунктами.

  • 28.02.2023: вчера ЕманоФедя прислал жалобу, что программа ringrf SIGSEGV'ится, причём, судя по логам от gdb, происходит в 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 -- больше ничего критичного нету:

    • Встречается "cast to pointer from integer of different size" в разных вариантах cxscheduler'а (Q* и Xh_cxscheduler.c -- при передаче tid'ов и fdh'ей).
    • В dircntest.c и localtest.c в вызовах CreateSimpleKnob() отсутствует приведение n к указателю.
    • В v2'шном cx-starter.c используется указатель 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@вечер: Федя прислал результаты с его новомодного ядра, которое

    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
    
    -- n_w=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
    . . .
    

    Т.е.,

    1. Даже код программы начинается с очень высокого адреса -- да-да, ДАЛЕКО за пределами 32бит/4GB (навскидку -- адреса 48-битные).
    2. Каждый бинарник, включая код и библиотеки, и код программы, маппирован не по 2-3-4 раза, как в RHEL7 и прочих, а аж по 5 раз.

    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: в продолжение раскопок по теме "очевидная идея":

    • Насчёт переменных окружения:
      • Почитал man по ld.so -- ничего на эту тему.
      • Потом посмотрел вывод "strings -a /lib64/ld-linux-x86-64.so.2" (там имена переменных все сгруппированы толпой, я толпу искал по "LD_LIBRARY_PATH") -- тоже ничего.
      • (Есть некая $LD_USE_LOAD_BIAS, но она на код программы влияния не оказывает (проверил), а только на динамические библиотеки.)
      • Кстати, "ldd" под CentOS-7.3 -- это вообще shell-script, запускающий обычный ld.so (какой-то из списка) с какими-то ключиками.
    • А потом дошло: ведь если код программы собран БЕЗ ключа "-fPIC" (не-позиционно-независимым), то он в принципе не может быть перемещён куда попало, так что не маппировать младшие 4GB -- увы, никак (как минимум статические данные будут именно там, сразу после сегмента кода).
    • Вот heap -- да, как-то, наверное, можно. Уж теоретически-то точно.
    • Погуглил по "linux relocate heap above 4gb" -- опять фиг (55 результатов и ничего по теме).

    Уф, надоело -- бросаю это исследование.

    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
    . . .
    
MotifKnobs_text_vect:
  • 24.07.2018: создаём раздел под РЕАЛЬНО простейший (и, по завершению, реально умолчательный) knobplugin для узлов типа DATAKNOB_VECT.

    Он будет выглядеть как просто текстовое поле, с форматом [ЧИСЛО{,ЧИСЛО}].

  • 24.07.2018: приступаем.

    24.07.2018: делаем на основе MotifKnobs_text_text.c -- они очень похожи.

    • DEFAULT_COLUMNS увеличено с =10 до =20.
    • Да, формат именно просто "[ЧИСЛО{,ЧИСЛО}]", БЕЗ префикса "N:".
    • Парсинг из строки хитроумен:
      • Он идёт в 2 прохода, и на 1-м только парсится, а на 2-м уже складывается в edata[].
      • При любой ошибке -- парсинга, либо при превышении количеством указанных чисел лимита -- бибикается, курсор ставится на точку с "ошибкой", и отваливается.
      • Таким образом, edata[] НЕ портится в случае ошибки, и можно при желании делать "cancel" перечитыванием оттуда (пока -- НЕ сделано).
    • Самое неприятное, как предполагалось -- хитрое формирование строки для передачи XmText'у.

      Для этого аллокируется буфер tbuf, объёмом в max_nelems раз по GetTextColumns().

      А в TextSetVect_m() проверяется, что если места не хватает -- то оно до-XtRealloc'ируется.

    25.07.2018@утро-дома: а можно было б выпендриться проще: аллокировать ДВОЙНОЙ объём буфера, и первую половинку использовать для складирования получаемых от сервера данных (и реализации "cancel"), а вторую -- для парсинга в неё и отправки.

    25.07.2018: деделываем.

    • Теперь роль "умолчательного" перешла к text_vect.

    ...и пока что это всё -- всякие "undo" и мгновенные "cancel" при надобности сделаем потом; "done".

MotifKnobs_text_text:
  • 24.07.2018: компонент был создан еще 3 года назад, но своего раздела не имел (описывалось в "datatree"). Теперь создаём свой.
  • 24.07.2018: в text_text_privrec_t имеются поля defcols_obtained, text_deffg, text_defbg, реально не используемые (работает стандартная MotifKnobs'овская колоризация).

    Видимо, остались с 12-2009, когда оно копировалось с text_knob'а.

    Убраны.

MotifKnobs_invisible_cont:
  • 13.09.2018: появилась потребность воспроизвести эту штуку, которая в v2 была, а в v4 ранее не надобилась.

    Потребность -- в том, чтоб куда-нибудь мочь помещать vect-узел, чтобы векторный канал постоянно запрашивался (это у Роговского в скрине-конфигураторе BPM'ов накопителя).

  • 14.09.2018: делаем.

    14.09.2018: ...сделано. Пока, правда, не проверено.

  • 15.10.2018: занадобилось также уметь делать невидимый контейнер, чьё содержимое всё же создаётся (просто его не видно).

    Смысл -- для onoff'ов с присутствующими victim'ами: чтобы в v5rfsyn/v5ie уметь дисэйблить селекторы делителей частоты при работе в режиме "Ext" (а в "Lcl" -- разрешать).

    Можно, конечно, завести другой тип контекнера, но зачем? Проще добавить функционала этому.

    15.10.2018: делаем. Проект таков:

    • Режим работы указываем в auxinfo (да, для этого придётся завести privrec). По умолчанию -- "классический", НЕ создающий содержимого; а при ключике with_content -- создающий.
    • В качестве корневого виджета (который размером 1*1 пиксел) будет работать Composite -- compositeWidgetClass.
    • Ему будет ставиться XmNsensitive:=False.
    • Чтоб содержимое не просвечивало (хоть всего 1 пиксел, но вдруг) -- первым в принудительном порядке будет создаваться пустой виджет 1*1 пиксел.

    Сделано. Проверено -- работает.

  • 18.01.2019: забавный случай приключился с использованием invisible (хотя и with_content) контейнеров в v5rfsyn/v4ie.

    Заюзаны они были для того, чтобы «дисэйблить селекторы делителей частоты при работе в режиме "Ext"».

    Соответственно, имеют ссылку на канал-источник.

    Так вот: при очередной перетряске системы имён каналов (точнее, cpoint'ов) в canhw:19 имена каналов "Источник запусков" поменялись, и скрины были проверены по принципу "не горит ли где что чёрным цветом?".

    Но cx-starter напротив v5rfsyn упорно показывал чёрную метку ненайденности. Я чуть голову не сломал, пытаясь понять причину.

    Спасла только отладочная печать, вставленная в CdrProcessKnobs() по условию "k->curstate == KNOBSTATE_NOTFOUND" -- указала ручку-источник проблемы.

    Оказалось, что при перетряхивании имён в disp'ах, содержащихся в invisible-контейнере, остались старые, неисправленные имена. А тестирование глядением на скрин никакой черноты не показало -- оно ж в НЕВИДИМОМ контейнере! И показывал только cx-starter, которому пофигу на эти тонкости, он показывает кумулятивный результат всего, что есть -- вот в нём и вылезло.

:
Xh:
Общие вопросы:
  • 30.06.2007: начато реальное население директории, путем "умного" копирования из work/cx/ (самое-то начало было положено еще в ноябре 2005, когда ради научения GeneralRules.mk собиранию .so-файлов сюда был посажен bigredcur.c).

    02.07.2007: да, базовое населивание сделано. Пока что -- БЕЗ fdlg и stddlg, и без toolbar.

    Но -- теперь команды являются строками, а не числами. И неуказанной считается команда NULL -- как раньше 0.

    02.08.2007: и Xh_stddlg также сделал, для нужд subwin_cont'а.

  • 02.08.2015: сюда притащены модули Xh_monoimg, Xh_viewport, Xh_plot -- копированием из v2, т.к. исходники подшаманены для полной совместимости.
  • 31.08.2015@пляж: надо б чуть дополнить технологию "команд": для CHECKBOX'ов нужен еще info_int, чтоб передавать новое состояние (off:0, on:1).

    04.09.2015: делаем.

    • Тип XhCommandProc дополнен параметром info_int.
    • ...и в юзерах оно отражено.
    • И в Cdr отражено.

    А вот проверить пока никак -- нету ж пока ничего, что бы генерило команды.

    14.01.2015@утро-лыжи-5км: и еще: давно -- 02-07-2007 -- для Xh_fdlg было придумано, что "результат" (Ok или Cancel) они будут передавать префиксом "+" или "-", после которого уже сам текст "команды". Но то дико неудобно -- пришлось бы выпендриваться с укладыванием префикса+команды в некий буфер.

    А с info_int всё становится тривиально: Ok будет генерить info_int=0, а Cancel info_int=-1.

  • 10.09.2019: переделываем в Xh_fallbacks.h все русские надписи на английские.

    Причина -- что в (долбанутом) GNOME3 под RHEL7 все заголовки окон показываются "крокозябрами" (латиницей с акцентами), поскольку тамошний дурной window manager (gnome-shell?) игнорирует уставки локали клиента -- WM_LOCALE_NAME, которая честно стоит в "ru_RU.KOI8-R" (и, судя по гуглению на тему «"gnome-shell" "WM_LOCALE_NAME"», никого это и не парит...).

Xh_toolbar:
  • 02.07.2007: здесь надо будет иметь более навороченный модуль, умеющий создавать не только основной, но и дополнительный тулбары (в указанного parent'а), и все кнопки будут добавляться к per-XhWindow-списку. Плюс, чтобы умело и вертикальные тулбары делать.

    А еще хочется, чтобы он дозволял "tool button plugins" (см. bigfile-0001 от 10-05-2005).

  • 13.01.2016: потихоньку делаем -- в простейшем варианте, для потребностей pult и ему подобных (безо всяких дополнительных тулбаров).

    19.01.2016: за прошедшую неделю база изготовлена. В основном копированием из v2, но с модификациями (основное -- что команды теперь строковые, отсюда всё и пляшет).

    1. Принят постулат, что зарегистрированная в toolbar'е команда (указатель на строку) должна быть валидна на всё время жизни тулбара. Т.е., строки должны быть либо в статической памяти, либо в невысвобождаемой динамической (последнего у нас по факту нет).

      А вот сами описатели кнопок -- вовсе не обязаны быть персистентными, из них информация добывается при создании и больше они не нужны.

    2. От имевшегося в v2 privrec (аллокируемого per-button и с хранением указателя на него в XmNuserData) избавлено.
      • Команда по-прежнему передаётся в closure (потому и требуется персистентность строки).
      • Состояние checkbox'ов -- on/off -- хранится в XmNuserData.
      • А чтоб callback знал тип кнопки (собственно COMMAND или CHECKBOX), при создании регистрируются два разных callback'а, потом уже вызывающие реально работающую ActOnCB() с дополнительным параметром type.

    Требуемый минимум работает.

    23.09.2016: а вот зря сделан п.2 ("избавлено"). Сейчас понадобилось-таки реализовать XhSetCommandOnOff() и XhSetCommandEnabled(), а фиг -- неоткуда для конкретной кнопки взять её команду!

    15.02.2020: грустно, что оно не работает -- халтурновато выглядят скрины осциллографов.

    17.02.2020@лыжи: очевидно, что делать нужно просто по иной архитектуре: аллокировать "массив информации о тулбаре", который заполнять (по ячейке на кнопку) типами+командами плюс туда же складывать ссылки на виджеты кнопок.

    22.02.2020@дома-суббота: делаем:

    1. Для начала - просто реализуем.
      • XhP.h:
        • Введён тип XhToolBtnInfo, содержащий поля type, cmd, btn (последнее -- Widget).
        • К struct _WindowInfo добавлено XhToolBtnInfo *tbarButtons.
      • В XhCreateToolbar() выполняется основная работа:
        • Подсчитывается количество элементов туобара.
        • Аллокируется tbarButtons[].

          XtFree() его делается в XhDeleteWindow.)

        • Туда копируются "описательные свойства" -- type и cmd.
        • Позже туда в btn записывается ссылка на виджет.
      • После этого на долю XhSetCommandOnOff() осталась уже несложная работа -- проходимся по tbarButtons[] и ищем XhACT_CHECKBOX с совпадающей командой; при нахождении проверяем, в том ли они состоянии, что уставляется, и если нет -- то инвертируем.
    2. Вторая часть - полностью переходим на эту архитектуру с tbarButtons, избавляясь от использования XmNuserData.
      • К XhToolBtnInfo добавляем поле pressed.
      • И везде пользуемся им вместо XmNuserData.

        Это, кстати, сильно проще, чем вычитывать и прописывать ресурс -- простое обращение к полю.

      • Сопутствующий бонус -- с раздельных COMMAND_CB() и CHECKBOX_CB() плюс выполняющей саму работу ActOnCB() перейдено на единый toolCB() (да-да, как в v2, только место хранения данных иное -- общий массив вместо индивидуальных-в-userData).

        В качестве closure передаётся "номер кнопки в тулбаре" -- b, индекс в массиве tbarButtons[].

    Ну и за компанию --

    • На той же архитектуре реализовано и содержимое XhSetCommandEnabled().

      Да, проверено, работает. Хотя на современном Motif'е выглядит некрасивовато.

  • 19.09.2016: делаем возможность уменьшать основной тулбар, чтоб он был размером с мини-тулбар у fastadc.

    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.
    • Начато изготовление мини-пиктограмм (точнее, продолжено -- троица-то (once, start, save) уже была).

    20.09.2016: продолжаем:

    • Сделан полный (на текущий момент) требуемый комплект btn_mini_NNN.xpm. Причём всё руками, в текстовом редакторе -- ибо xpaint'а на p8b2 нету.
    • XhCreateToolbar() допилена так, чтобы при use_mini и поля вокруг кнопок делать поменьше, и XhACT_LABEL'ам назначается другой класс -- miniToolLabel, для которого в Xh_fallbacks.h назначен шрифт поменьше.
    • Включение мини-режима делается опцией "minitoolbar", поддерживаемой как в Chl_app.c, так и в pzframe_main.c.

      Но в первом умолчание именно она, а во втором -- "maxitoolbar".

    • В Chl_app.c также добавлены ключи "with_freeze_btn", "with_save_btn" и "with_load_btn", при отсутствии которых (а умолчание -- отключенность!) соответствующие кнопки удаляются.
    • Тулбар теперь в Chl_app.c по умолчанию включен.

    20.09.2016@дома: кстати, тулбарчик теперь настолько маленький, что его даже в мелких программах (вроде camsel/ringcamsel/turnmag) можно не отключать, а noop leds из них убрать.

    21.09.2016: да, так и сделано -- высота увеличилась всего на 4 пиксела.

    Далее было проделано добавление нужных кнопок (параметры with_NNN_btn) в конкретные скрины. И тут обнаружилось, что кнопка [Freeze] в linipp не работает -- просто нет этого функционала вовсе. Ну приступили к деланью...

    • Собственно всё сделано:
      • Поле data_subsys_t.is_freezed.
      • Уставление его =info_int в ChlHandleStdCommand().
      • Использование -- не-действия при взведённости:
        • Cdr::ProcessContextEvent(): не выполняется обновление дерева ручек (процессинг подсистемы).
        • Chl::CycleCallback(): история не сдвигается.
        • Chl_knobprops.c имеет свой цикл обновления, потому туда засунута своя проверка (для чего пришлось в knobprops_rec_t добавить backreference на subsys.
    • Одна маленькая проблема: ведь knobplugin'ы-то имеют своё собственное обновление и Chl'ю не подчиняются, так что на них этот [Freeze] не действует!

      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: да, так и сделано. Работает -- паузится.

    Засим считаем пункт о мини-тулбаре выполненным.

  • 24.05.2022: как бы не по Xh, а по Chl, но непосредственно связано с предыдущим пунктом:
    1. Необходимо уметь указывать из командной строки также и СРАЗУ, что запуститься в состоянии "freezed".
    2. И, соответственно, полезна также будет кнопка "once".

    Причина -- то, что при запуске chl-клиента с пульта на t320p (через цепочку star,x10sae,duct,v5p2) им пользоваться вообще невозможно -- оно дико тормозит на первом же обновлении, и после этого всё (даже запуск с ключом "with_freeze_btn" и максимально быстрое нажатие этой кнопки ничего не дают).

    Позже, уже после реализации: да, замечено, что обновление по кнопке "once" длится просто невообразимо долго -- чуть ли не минуту. Если таких обновлений валит со скоростью 5Hz, то оно сразу же и заткнётся намертво.

    24.05.2022: делаем (записано не по порядку работы, а по окончательным результатам; сама же работа делалась по принципу "найдём, где используется is_freezed, и добавим рядышком" плюс подсматриванием касательно oneshot в pzframe_data.c):

    1. Определения:
      • Chl.h: добавлено CHL_STDCMD_ONCE равное DATATREE_STDCMD_ONCE -- см. далее.
      • datatree.h: добавлено определение DATATREE_STDCMD_ONCE в "datatreeOnce" -- это для унификации, чтобы рассылаемые Chl'ем команды были понятны knobplugin'ам.
      • datatreeP.h: добавлено поле data_subsys_t.oneshot -- сразу после is_freezed.
    2. Собственно работа -- почти вся в Chl_app.c:
      • В stdtoolslist[] добавлена кнопочка CHL_STDCMD_ONCE.
      • Интерфейс для командной строки:
        1. В appopts_t добавлено поле with_once_btn;
        2. ...плюс is_freezed и oneshot для возможности указывать "замри" и "однократно всё-таки отработай!" из командной строки.
        3. В text2appopts[] -- флажки для всей этой троицы.
      • В ChlRunSubsystem() реакция на них:
        1. Обработка with_once_btn аналогична прочим with_*_btn -- при невзведённости соответствующая кнопка из toolslist[]'а превращается в XhACT_NOP.
        2. А "команды из командной строки" просто копируются в соответствующие поля subsys'а.
      • В ChlHandleStdCommand() реакция на ONCE тривиальная -- просто взводится subsys->oneshot=1.
      • Cdr_treeproc.c::ProcessContextEvent() и Chl_knobprops.c::Update1HZProc() -- принятие во внимание oneshot в дополнение к is_freezed==0.
    3. "Но есть нюанс" касательно СБРОСА oneshot=0:
      • Сначала сброс oneshot=0 был поставлен в Chl_app.c::CycleCallback(), но потом стало очевидно, что место ему -- в обработке CTX_R_CYCLE, а на ИСТОРИЮ оный oneshot вообще не должен оказывать влияния.

        Поэтому сброс oneshot0 переехал в Cdr_treeproc.c::ProcessContextEvent(), а из циклирования истории он убран, как и сам учёт oneshot.

        После этого, кстати, и указание "oneshot" в командной строке стало иметь эффект.

      • ...и, для вящей понятности/корректности, CycleCallback() переименован в HistoryCycleCallback().
      • @вечер: а вообще, конечно, это неправильно -- использовать в Chl_knobprops.c тот же самый флаг subsys->oneshot: в knobprops же обновления делаются асинхронно, по собственному 1Hz-таймеру, а выставление и сброс -- синхронно, по действию юзера и по приходу CDA_R_CYCLE соответственно.

        В результате там обновляется "через раз".

        25.05.2022@утро-просыпаясь: надо бы как-то ловить событие "ONCE" и выставлять СОБСТВЕННЫЙ флаг в knobprops_rec_t, по которому и ориентироваться.

    Итак, после рихтовки заработало -- цель достигнута, можно и тормозить прямо из командной строки, и "разово" производить отображение.

    Далее решил сделать интеграцию с pzframe-knobplugin'ами:

    • В PzframeKnobpluginHandleCmd_m() добавлена реакция на DATATREE_STDCMD_ONCE.
    • И при попытке проверки оказалось, что опции minitoolbar/maxitoolbar не имеют НИКАКОГО эффекта с pzframeclient'ами...

      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[] подтюнена.
      • И собственно использование -- if()/else if()/else, выставляющий значение toolbar_mask.

        А minitoolbar_mask изведён за ненадобностью.

      • Плюс, модифицировано условие при создании leds.

      И всё стало как надо.

      ...надо было ещё ТОГДА, в 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 проблема, идеологического свойства: просто непонятно, как же сделать -- вроде бы сделать-то можно, но оно получится "грубой силой", некрасиво, халтурно и потенциально ненадёжно для будущих модификаций.

    • По идее от 25-05-2022 "иметь собственный экземпляр флага oneshot, выставляемый собственной ловлей события ONCE" -- фиг знает, как сделать.
    • Другой потенциально неплохой вариант -- вызывать обновление окошка впрямую из Cdr_treeproc.c::ProcessContextEvent() при is_freezed && oneshot; это будет вообще самым "метким" способом. Само обновление -- просто в чистом виде работа ShowCurVals().

      Но для этого придётся расширять dataknob_cont_vmt_t для добавления метода "обновить knobprops".

      ...так-то можно воспользоваться имеющимся методом ShowProps(), но это криво.

    • Ещё вариант -- вообще никак не реагировать в Update1HZProc() на значение is_freezed и продолжать обновлять. Ну подумаешь -- будет раз в секунду несколько XmText'ов перерисовывать, невелик поток.

      ...но это тоже кривизна...

    • А ещё -- можно просто оставить как есть сейчас, с полу-рандомным обновлением (повезёт/не повезёт, успеет/не успеет).

      Так пока и оставим, за неимением лучшего.

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

    30.05.2022: ещё чуток:

    • Добавли параметр "remdbg" -- аналог цепочки "minitoolbar with_freeze_btn with_once_btn is_freezed oneshot".

      Отображается на поле remdbg, при взведённости которого все соответствующие флажки взводятся в 1 (и только toolbar_kind делается =TOOLBAR_KIND_MAXI лишь при !=TOOLBAR_KIND_MINI: если НИКАКОГО не указано, то включается MAXI, а MINI не трогается).

    • И заодно исправлен допущенный при введении кнопки ONCE косяк: при проверке кнопки на тему "убрать ли?" предполагалось, что она XhACT_CHECKBOX, вместо должного XhACT_COMMAND -- следствие копирования; в результате того косяка кнопка ONCE появлялась у всех скринов.
Xh_statusline:
  • 24.07.2007: имелась некоторая странность -- пустое (с пустым workspace'ом) Xh-окно, но СО statusline'ом имело некую существенную ширину -- за сотню пикселов.

    24.07.2007: выяснилось, что проблема была в каком-то значении ресурса XmNcolumns в XmTextовом варианте statusline'а -- значение по умолчанию, узнать которое не удалось, ибо editres его не видит.

    Решилась проблема принудительной уставкой XmNcolumns:=1. Функциональность от этого никак не пострадала, поскольку виджет приаттачен к форме и растягивается по ширине окна.

Xh_fdlg:
  • 02.07.2007: поскольку команды теперь будут строками, а не числами, то возникает вопрос -- а как же реализовывать былой подход, когда при создании файлового диалога указывалась ОДНА команда, а и она шла как Ok, а за Cancel была она+1?

    02.07.2007: да очень просто -- в качестве Ok будет идти "+"КОМАНДА, а Cancel -- "-"КОМАНДА. И это даже лучше -- вариаций будет больше (разными символами-префиксами), а не только 2.

    14.01.2015@утро-лыжи-5км: а вот и нифига: вместо махинаций со строками просто воспользуемся введённым с той поры info_int.

  • 08.02.2016: модуль портирован с v2. Единственное изменение -- переход с целочисленных команд на строковые. Заодно это потребовало модификации архитектуры callback'ов Ok/Cancel: теперь они разделены на 2 разных, но оба вызывают единую DoOkCancel(), которой передают готовый info_int.
:
AuxMotifWidgets:
Общие вопросы:
  • 30.06.2007: библиотека создана, путем просто копирования компонентов из work/cx/. В эту версию вошли следующие виджеты: IncDecB, InputOnly, XmLiteClue, XmSepGridLayout. А вот Gridbox, Table и старый LiteClue -- решено не включать, ибо смысла нету.
:
Cdr:
Общие вопросы:
  • 21.06.2007: для гибкости и поддержки РАЗНЫХ "источников" надо делить библиотеку на:
    • "Ядро" -- групповая обработка деревьев, включающая обход, всякие поиски и Destroy.
    • Менеджер "протоколов добычи", являющихся плагинами.
    • Собственно плагины для чтения группировок -- из форматов .so (как сейчас), из БД, из текстовых файлов (плюс, из "текстовых файлов" в виде просто строк char[]="...").
  • 17.07.2007: некоторые соображения о читалках...

    17.07.2007: во-первых, читалка из .so-файлов вообще-то нафиг не нужна -- намного более удобной будет читалка из текстовых файлов.

    Во-вторых, скорее всего, нам на все случаи жизни хватит ОДНОГО парсера -- а отличаться будет только то, ОТКУДА он будет брать строки.

    Посему -- изготовим некий модуль, умеющий парсить, и чтоб его функциям передавались указатель на функцию "читай следующую строку" и некий специфичный для функции указатель -- обычно "контекст", содержащий либо FILE *fp, либо char * для чтения из текстовой строки, либо что-то подобное для БД, и т.п.

    P.S. А для описания таких вещей -- заводим CdrP.h

  • 17.07.2007: надо бы уметь парсить не только подсистему целиком, но еще и просто "один узел с подузлами" -- чтобы, например, легче создавать какие-нибудь локальные иерархии, не привязанные напрямую к подсистеме.

    (Да, немного это перекрывается с областью компетенции протокольных плагинов в cda, но здесь ключевое -- это именно отвязанность от подсистемы.)

  • 17.07.2007: а еще уметь бы ТАК читать подсистему, чтобы она как бы была, но "not realized" -- т.е., чтобы имелось уже ее описание в бинарном виде, но никакие связанные с 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".

  • 17.07.2007: сделал модуль Cdr_err.c, скопировав его один-в-один с KnobsCore_err.c (только длину увеличил с 200 до 300).

    10.08.2018: мелкое дополнение: CdrClearErr() теперь делает еще и errno=0 (по следам вчерашних разборок с CdrRealizeKnobs(), когда в диагностику давалось сообщение от давнего ENOENT).

  • 14.08.2007: некоторые соображения о разделении ответственности между Cdr_treeproc и "плагинами добычи группировок".

    14.08.2007: у узлов есть несколько полей, которые в принципе могут заполняться treeproc'ом -- например, в CdrRealize*() -- uplink, u.c.subsyslink, u.k.curstate. И такое -- централизованное -- заполнение вроде бы проще и "корректнее", но... Пока что -- раз CdrRealize*() пока нету -- приходится проставлять эти поля прямо в Cdr_fromtext.c::ParseKnobDescr(). И, может, оно и правильнее -- тем самым на выходе из "добытчика" мы имеем корректное, готовое к использованию дерево. Посмотрим.

  • 04.08.2010: по опыту liucc@CXv2: ОБЯЗАТЕЛЬНО надо бы уметь считывать некие списки значений из файлов простого формата.

    04.08.2010: это включает в себя две схожих вещи:

    1. Уметь считывать некоторые НАСТРОЙКИ/КОНФИГУРАЦИЮ -- то, что в CXv2 делается параметрами, хранимыми в "регистрах". Конкретно с liucc -- это таблица "хороших" значений измерений накалов.

      Здесь-то есть "параметры ручек", но:

      1. Они локальны для ручек, а бывают именно "глобальные" для группировки переменные.
      2. В текущей концепции параметры сохраняются/считываются только вместе с самими значениями ручек, что совсем неадекватно.

      Представляются разумными такие решения этих проблем:

      1. Иметь возможность считывать переменные "напрямую", минуя ручки-переходники (rd_src="=%РЕГИСТР"). Например, конструкцией вида
        .variable ИМЯ_ПЕРЕМЕННОЙ ЗНАЧЕНИЕ
      2. Иметь возможность считывать параметры ручек отдельно от значений. Собственно, ВСЕ параметры (диапазоны в v4 ведь тоже являются оными) даже в CXv2 и так имеют в файлах формат "WHAT:VALUE". Но само-то значение указывается всегда, безо всяких префиксов... Выходов два:
        1. Считать, что если после имени ручки вместо значения указано просто "-", то никакое присвоение значения не выполнять.
        2. Считать указанное-сразу-после-ручки значением только при отсутствии в нём ':'. В противном же случае рассматривать как именованное-указание-параметра -- полностью аналогично парсеру подсистем.

        В принципе, применимы хоть оба подхода сразу, но для неизбыточности лучше остановиться на 2-м. И еще не забыть, что аналогичный же формат должен подходить и для не-скалярно-числовых каналов -- т.е., для строковых и для массивов и/или "больших каналов" и DATAKNOB_USER.

    2. Уметь считывать РЕЖИМ из файлов, содержащих таблицы простого формата в стиле
      .channel КАНАЛ ЗНАЧЕНИЕ

      Это нужно для "инициализации" -- записи в железо неких заранее известных значений, например, при старте софтины. Требуется, чтобы такой файл легко редактировался вручную (или вообще генерился).

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

    И еще в ту же степь:

    • ОЧЕНЬ полезно, что вследствие унифицированной древесной структуры автоматом получится сохранять/считывать режим не только всей группировки, но и любой подветки.
    • Вообще проблема чуть шире: надо бы уметь сохранять и считывать произвольный срез информации, а не только всё скопом. Т.е., например, чтобы операциям чтения и записи передавалась бы битовая маска с указанием, что из информации следует обрабатывать. И, если "значение" не числится в маске, то его при чтении просто игнорировать.

      26.02.2013: под "что из информации", видимо, подразумевалось "какие классы информации" -- значения, диапазоны, ...

    26.10.2010: и еще один подвид информации, требующей сохранения: "экранные данные" всяких штучек типа fastadc, а именно

    1. "Настройки" -- например, позиции реперов (и их включенность).
    2. "Числа" -- всякие результаты расчётов (минимум/максимум, центр тяжести, ...).

    Сейчас в v2 это делается просто экранными ручками/полями, специфично для программы.

    А превращение этих вещей в ручки/переменные позволит обращаться с ними стандартным образом,

    Единственное что -- ведь после считывания эти вещи надо как-то верифицировать...

    24.10.2012@Снежинск-каземат-11: и еще некоторые соображения о режимах.

    1. Как сохранять "настройки" и проч.: это часть более общей задачи -- как АДРЕСОВАТЬСЯ к таким специфичностям из других частей программы.

      Ответ: адресация должна быть всегда одинаковая, без разницы -- обычная это ручка или "настройка". Следовательно --

      • всякие там уставки и включенности реперов должны представляться ручками в обычном дереве.
      • При этом и адресоваться к ним можно будет обычным образом.
      • ...единственное "исключение" -- поскольку у ручки могут быть числовые параметры, обращения к которым производятся стандартно, то можно для "простых" чисел пользоваться ими.

        25.10.2012@Снежинск-каземат-11: лучше бы нет: юзеры хотят мочь, например, значение в репере тоже отправлять на график -- для чего и оно должно быть "ручкой".

      • Альтернатива есть в том, ОТКУДА будут браться ручки для "настроек":
        1. Браться прямо из описания группировки (неудобно!).
        2. Генериться реализатором knobplugin'а (недостаток: завязка на GUI).
        3. 26.02.2013: гибридный вариант: у таких ручек ВСЕГДА есть поле "подветка", а содержимое его может либо браться из описания группировки, либо -- при отсутствии -- генериться knobplugin'ом. Тем более, что всё равно некоторые поля-параметры имеют смысл только в GUI, ибо обеспечиваются knobplugin'ом (те же реперы).
      • И важный вопрос -- как будет отрабатываться "запись" в такие искусственные/"подчинённые" ручки.

        Правильный ответ, видимо -- knobplugin-контейнер в себе уставит у себя в cont_vmt поле SetPhys, и таким образом будет перехватывать все записи.

    2. Как ПОСЛЕДОВАТЕЛЬНО грузить режимы -- т.е., чтоб некоторые ручки отрабатывались гарантированно позже других (это продолжение обсуждения, начатого в bigfile-0001.html за 15.02.2006, с частичным повторением).
      • Надо не сразу отправлять значения при чтении режимов, а СОХРАНЯТЬ, и уже потом отрабатывать.

        • А у каждой ручки иметь поле "очередь" ("приоритет", "задержка") -- аналог поля "pass" в /etc/fstab.
        • Сюда же можно присовокупить и "задержку" -- что отработать через какое-то время. Но это будет существенно сложнее...
        • Или указывать в описании группировки размеры пауз после каждого приоритета?
      • Т.о., надо иметь для каждой ручки:
        • Поле "приоритет".
        • Поле "для неё было что-то считано из режима".
        • Место для хранения считанных данных -- ЧТО отправлять на запись.
        • Место для хранения считанных параметров.
      • Здесь же можно реализовать "частичное" чтение режима, в виде некоей фильтрации -- чтоб части ручек флаг "было" не уставлялся.
  • 22.10.2012@Снежинск-каземат-11: вполне определённо, что надо будет иметь возможность в Cdr-скриптах иметь возможность из одних ручек писать в другие.

    Но при этом надо позаботиться о защите от зацикливания (A пишет в B, B пишет в A). Т.е., (в CdrSetKnobValue()?) перед вызовом метода "запись" ручки выставлять у неё в data_knob_t некий флажок (а после -- сбрасывать), и перед записью сначала проверять, не горит ли он.

    17.11.2013: добавлено поле being_modified --

    • Пока только к dataknob_knob_data_t. Хотя, возможно, полезно будет и у data_knob_t вообще, ради "программной управляемости контейнеров").
    • И в CdrSetKnobValue() пока не используется. Хотя там вообще не вполне ясно, как это делать для
      1. контейнеров (поскольку у них, возможно, вообще не через cda это всё пойдёт);
      2. искусственно-созданных поддеревьев, создаваемых отображающими плагинами (Cdr/26-02-2013).

    17.11.2013: добавлено использование being_modified, по простому вышеприведённому проекту.

  • 22.09.2013: давно бродит мысль -- а как бы этак организовать структуру/синтаксис .subsys, чтоб можно было include'ить прямо готовые группировки внутрь других группировок в качестве элементов?

    Оснований для такого желания два:

    1. Воспоминания о CERN/CTF3'шной программе "knobs", где можно было комбинировать разные подсистемы по-кусочечно в одно окно.
    2. Чтоб из готовых группировок/окон собирать более крупные группировки (хоть таблицами, хоть закладками, хоть под-окнами).

    Вот как такое синтаксически делать -- чтоб "глобальные" директивы парсером элементов игнорировались? А как важные вещи (типа defserver) оставлять в силе?

    09.07.2014: самое простое -- действительно в CONTENT_fparser() в основном цикле :

    1. уметь воспринимать словцо "grouping" (съедая ненужный параметр section-name),
    2. а остальные из списка subsys_commands[] тупо игнорировать (надеясь, что они однострочны).

    Этот мелкий хак позволит добиться эффекта прямо сейчас -- include'ить .subsys-файлы в другие файлы, что полезно для включения стандартных экранов устройств в другие экраны.

    (Кстати, с таким подходом defserver можно было бы и воспринимать (хоть и чуть поднапрягшись). Особо незачем, правда.)

    Чуть позже:

    • Указанные выше два пункта были исполнены.
    • Но этого оказалось мало: тело CONTENT_fparser()'а было организовано в виде for-цикла, после которого в принудительном порядке ожидалась '}', и глобальные директивы вызывали фатальную ругань.
    • Так что цикл и проверки были переделаны: теперь оно в начале цикла смотрит, что дальше, и уж затем принимает решение -- считать контейнер законченным ('}'), пропустить строчку (глобальная команда), ругнуться (указание ручки, а n>=count) или парсить очередную.
    • После этого работает как задумано -- одни include'атся в другие.
  • 11.02.2015: вылез косяк, во время реализации трансляции имён (в cda_d_v2cx): "обновление" -- cda_dat_p_update_server_cycle() (возникающий из-за несуществующего имени сервера, влекущего MarkAllAsDefunct()), приводящий к генерации события CDA_CTX_R_CYCLE, вызывающего CdrProcessSubsystem() -- может произойти еще ДО окончания CdrRealizeSubsystem(), прямо в момент регистрации первого же канала (сложночитаемая цепочка-формулировка вышла, да). А на недо-realized-ветке процессинг в MINMAX-ручках попробует химичить с еще не созданным буфером -- что приводит к SIGSEGV.

    Конкретно для убирания SIGSEGV сейчас достаточно поставить проверку на аллокированность.

    Но есть некая неясность: почему оно ТАК ведёт себя только при трансляции имён, а с обычной адресацией (если в командной строке указать такое несуществующее) -- нет?

    12.02.2015: проверка добавлена.

    • Падать (пока?) перестало, но всё равно это криво: нет корректного отражения о-defunct'ченности ручек, связанных с сервером-причиной.
    • ...возможно, более логичным (всеобъёмлющим) решением было б ввести поле data_subsys_t.is_realized, но причину проблемы оно тоже не вылечит.
    • ...и для корректного решения кабы не пришлось помнить все номера серверов (conns_u[]), прилетавшие в подсистему до выставления is_realized, и потом в момент is_realized:=1 дрыгать обновления для них всех (классическое "откладывание", как в прочих местах с being_processed и being_destroyed).
    • С другой стороны -- нынешний вид, с посиневшими полями, содержащими [nan], тоже кривоват.

      Тут вопрос еще -- а НАДО ли реально "обновлять" всё первый раз, при обломе первоначального соединения?

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

      • В d_cx -- вообще hwr'ы могут мигрировать между серверами, так что сделано стандартным циклом while() по списку. 01.09.2015: и сделано было с ляпом: вместо dataref'а передавался hwr. С учётом наличия формул номера через некоторое время расходились (dataref'ов больше), и последние сколько-то ручек в linmag'е не defunct'ились.
      • В d_vcas -- Foreach'ем.
    • Кусок же с "посинением от длительной неактивности" -- DefunctHbTCB() с was_data_reply и last_data_time -- нет, поскольку та идея имела смысл только при обязательном ежецикленном обновлении всей подписки. С индивидуально-присылаемыми же каналами оно смысла не имеет: можно посиневать по таймауту, но раз-синивать ведь никто не будет.

      ...но если припрёт делать DEFUNCT'енье sid'а, то эта схема пойдёт -- только обращать внимание надо на ping-pong'и, т.к. по каналам ничего может подолгу и не приходить.

    02.09.2015: в продолжение темы:

    1. "Посинение от длительной неактивности" -- решаться должно по-другому, на уровне cda_core. См. в теме про DEFUNCT за сегодня.
    2. Посинение sid'а -- тоже по-другому, где-то в кооперации между cda_core и cda_d_XXX.
  • 28.05.2018@вечер-дорога-домой-около-ИПА: формат логгинга -- если оный в v4'шном Cdr когда-нибудь появится -- должен совпадать с histplot/das-experiment.

    (Да, там могут быть множественные записи, но это пофиг -- отрезать построчно уж как-нибудь.)

    01.06.2018: да, в v2 уже СЕЙЧАС формат ровно такой же; и даже строка заголовков начинается ровно с той же сигнатуры "Time(s-01.01.1970)".

Cdr_treeproc:
  • 17.07.2007: собственно, с этого модуля и начата Cdr (еще недели две назад). Тут будут жить все вещи, касающиеся обработки подсистем и деревьев -- загрузка, разные проходы и поиски, destroy.
  • 17.07.2007: УЖЕ реализована CdrLoadSubsystem() -- она свелась к добыче у plugmgr'а указателя на функцию-загрузчик и ее вызов.

    18.07.2007: ага, щаз-з-з! Там же надо еще стандартные поля (указатели на стандартные секции) заполнить. Так что это добавлено.

    19.01.2010: слегка изменена "концепция" использования переданных пераметров. Из (scheme,reference) они превратились в (def_scheme,reference), и reference пропускается через split_url(), так что теперь можно указывать схему прямо в командной строке всяких pult'ов.

    А чтобы оставить программам возможность ЖЕСТКОГО указания схемы, введено правило: если схема начинается с '!', то url-splitting не делается, а (def_scheme,reference) используются сразу как (scheme,location) -- с пропуском '!', естественно.

  • 06.08.2007: начал реализацию 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() сами.

  • 27.08.2007: собственно -- а с чего это у нас CreateSection() живет в Cdr_fromtext.c? По идее-то ей место в "базовой" Cdr. Плюс, тогда 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.

  • 15.04.2014: еще на прошлой неделе начата реализация CdrRealizeSubsystem() плюс CdrRealizeKnobs(). На новой схеме -- с контекстами.

    15.04.2014: некоторые детали:

    • CdrRealizeSubsystem() работает в 2 этапа: сначала делает realize всем секциям DSTN_GROUPING, а потом дрыгает все at_init, при помощи рекурсивного helper'а CallAtInitOfKnobs().

      02.10.2014: ага, щас -- не дрыгало: и рекуррентности не было (забыл), и дергало нерабочим getrefvnr() вместо process_ref(), и без CDA_OPT_IS_W.

    • Покамест с baseref'ами оно обращается халтурно -- тупо берёт текущее, а не интеллектуально конкатенирует. Просто не хватает соответствующего cda'шного API.
    • Конверсия имени в ссылку делается функцией src2ref():
      1. Понимающей префиксы типа -- @T: (по проекту от 12-04-2014).
      2. Включающей эвристику определения вида ссылки -- регистр, канал, формула (по проекту от 13-04-2014).

    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: добавлена поддержка DEVN и MINMAX (скопирована из v2).

    05.08.2014: пара замечаний:

    1. Как показал grep, тип DEVN сейчас не используется вообще НИГДЕ. В *mag* в качестве d/dT повсеместно стоит MINMAX.

      Хотя когда-то давно drc и magcorr были на LOGT_DEVN.

    2. Текущая реализация MINMAX халтурновата: там каждый шаг выполняется сдвиг 30-ячеечного буфера на 1 ячейку.

      А по-хорошему надо б использовать кольцевой буфер.

  • 06.08.2014: есть задача -- как обновлять окно Chl_knobprops?
    • В v2 организацией связи с сервером и обновлениями заведовал Chl_gui, он вызывал и CdrProcessGrouplist(), и Update*Window().
    • В v4 же вся связь сосредоточена в Cdr.

    Ну что, добавлять механизм 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: поддержка SYNTHETIC и READONLY, появившихка в v2 (в виде битовых флагов) позже лета 2007г., когда закладывался скелет новых Cdr и cda.

    07.08.2014: делание:

    1. Синтетические обновления:
      • Введён флаг CDR_OPT_SYNTHETIC.
      • В CdrProcessKnobs() он должным образом обрабатывается.
      • А в CdrSetKnobValue() при получении CDA_PROCESS_FLAG_REFRESH вызывается CdrProcessSubsystem(,,CDR_OPT_SYNTHETIC,).
      • ...заодно в _k_newdata_f добавлен параметр synthetic.
    2. Readonlyness:
      • Введён флаг CDR_OPT_READONLY плюс CDA_OPT_READONLY.
      • ...а надо ли? Или всё-таки правильнее иметь булевское data_subsys_t.readonly? ...и заодно ctxinfo_t.readonly?

    08.08.2014: "пусть цветут сто цветов" --

    1. Сделаем для начала вариант на флагах, а потом можно добавить на булевских в контекстах, и посмотрим, что удобнее.
      • Но по факту тут есть радикальное отличие от v2 -- CdrProcessSubsystem() вызывается самою Cdr, и в этот момент флаги взять неоткуда.
      • Так что введён data_subsys_t.readonly, уставляемый через CdrSetSubsystemRO(), и используемый...
      • ...в параллель (||) с options, передаваемым в CdrProcess*() и CdrSetKnobValue(), где в начале формируется готовая cda_opts, передаваемая потом всюду куда надо.
      • В cda затронуто 2 точки:
        1. cda_process_ref() -- запрещается вызов snd_data()
        2. cda_f_fla.c::proc_PUTCHAN() -- было бы даже избыточно, если бы вызывало cda_process_ref() напрямую, но дёргает упрощённый cda_set_dcval(), не имеющий options.
      • Не хватает теперь поддержки только от клиентов -- pult.c пока имеет ну совсем рудиментарный парсинг командной строки.
  • 20.08.2015: сейчас устройство чуть кривовато в том, что argv[0] до cda не доходит. В результате как минимум v2cx'ные группировки "как положено" не ищутся -- поскольку findfilein()'овский путь "!" не работает.

    21.08.2015: вопрос в функции CdrRealizeSubsystem() -- она вызывает cda_new_context(), но в качестве argv0 передаёт NULL, поскольку больше нечего.

    Кстати, аналогичная кривизна и в загрузке группировок, но CdrLoadSubsystem() -- реально конкретно CdrLoadSubsystemFileViaPpf4td() -- спасается использованием program_invocation_name, имеющейся под Linux.

    25.08.2015: делаем.

    • CdrRealizeSubsystem() -- параметр в конце добавлен, тут всё было просто.
      • Но v2cx'у это не помогло.
        1. Причина 1: в ctxinfo_t.argv0 сохранялось ОБРЕЗАННОЕ имя программы -- "short_name", после последнего '/'.
          • Окей -- переделано: увеличено с [40] до [300] и обрезание убрано.
          • Это неположительно с точки зрения диагностики -- в cda_dat_p_report() cda_ref_p_report() теперь будут выдаваться полные имена/пути, вместо коротких, но переживём.
          • ...а если задолбает -- то ничего не стоит добавить в ctxinfo_t поле "оффсет короткого имени в argv0". Именно ОФФСЕТ, а НЕ указатель, поскольку ctxinfo_t -- компонент SLOTARRAY, и может гулять по памяти.
        2. Причина 2: в нужный момент у того есть только ref, а 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() -- нет; вроде как не надо, там даётся уже готовое имя файла, поиск не делается.)

      Проверить -- не проверялось, т.к. особо не на чем.

  • 09.02.2016: приступаем к созданию сохранения/считывания файлов режимов на уровне Cdr.

    Цель -- простейший вариант, для стендовых программ, вроде сварки и 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().
        • Пустые ident'ы она выдаёт как "-" (что безопасно, т.к. оно грузиться не будет).
        • Но вообще-то это халтурка, по-хорошему надо будет подумать о корректной реализации "прозрачных" имён, которая есть в v2 (с именами ":").
      • Пустота в файле была оттого, что оно обрабатывало только DATAKNOB_KNOB'ы, а про _GRPG и _CONT было забыто. Теперь сделано и всё OK.
      • Добавлено начало поддержки DATAKNOB_TEXT'ов.
      • ...кстати, для ПОЛНОЙ корректности надо б перед именем ручки+канала выдавать DTYPE -- чтоб получаемый файл годился для cdaclient/dataset-restore.

    12.02.2016: далее:

    • Выдаётся "dtype" -- в виде просто "@d:".
    • Продолжение поддержки _TEXT. Оно полностью отделено в свою else-if()-ветку.
    • Выдача ссылки на канал для этого вытащена в отдельную FprintfKnobSrc().

      Кстати, там с самого начала были предосторожности: печаталась только если состоит из годных-для-имени-канала символов, иначе "-". Так что ни формулы, ни регистры в файл не попадут и не вызовут смущения cdaclient'а.

    • ...сама выдача ссылки сейчас некорректна: просто rd_src/wr_src, а это не учитывает возможные refbase, не говоря уж о defserver.

    13.02.2016: далее:

    • Выдача ссылки исправлена: теперь отдаёт результат cda_src_of_ref().

      02.08.2016: а вот тут вопрос -- возможно, как раз и НАДО выдавать исходный адрес, БЕЗ defserver'а, а только combine(baseref,rd_src): чтоб режимы были "относительными" и годились бы даже при перенацеливании на другой сервер (как это предусмотрено в самом cdaclient/dataset-save).

    • Выдача _TEXT доделана.
      • Данные она получает напрямую от cda (иных способов нет -- в dataknob'е ничего не хранится), и nelems в @tNN: указывает корректно, полученное от cda_max_nelems_of_ref().
      • Само содержимое выдаётся аккуратно, через PrintChar(), скопированную из console_cda_util.c.

    15.02.2016: еще --

    • В конце строки через пробел печатаются timestamp и rflags, также совместимо, это делает FprintfKnobStats().

    29.09.2016: флаг "UNSAVABLE" у контейнеров не работал, т.к. поле behaviour изначально делалось для DATAKNOB_KNOB и парсилось и прописывалось только им.

    Сейчас ситуация исправлена -- работает.

    04.10.2016: приступаем к сложной части -- CdrLoadSubsystemMode(). Активно копируется содержимое cdaclient.c+console_cda_util.c.

    Проблемы:

    1. Некоторый вопрос возникает с поиском: сохраняются-то ВСЕ секции с типом DSTN_GROUPING, а при загрузке как поступать -- искать по всем?

      Решение выбрано просто-халтурное: т.к. ChlRunSubsystem() сейчас оживляет лишь секцию "main", то и поиск делается только по ней.

    2. ...и ВОПРОС: будет ли datatree_find_node() искать, учитывая, что первый компонент имени -- имя самого корневого узла?

    15.11.2016: пытаемся доделать:

    • Обсуждение:
      1. Из п.1 выше очевидно вытекает ответ и на п.2: раз поиск делается только по корневой секции, то и при сохранении надо игнорировать верхний компонент имени (имя корневого узла).

        (...Кстати, Chl_knobprops отображает так же -- без самого верхнего узла.)

        Тогда поиск будет работать ровно как нужно.

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

      2. Для "более корректного" решения нужно будет сохранять всё, и при загрузке сначала вычленять верхний компонент (имя grouping'а), искать DSTN_GROUPING с таким именем, а потом уже делать поиск по дальнейшему имени внутри него.

        Муторновато как-то. И есть неясность-двоякость: имён-то у группировки ДВА: имя секции (у основной -- "main") и ident её узла. ...прямо как в CXv2 было с именами вложенных элементов -- они существовали в 2 экземплярах: в узле-"содержателе" и в самом описателе под-элемента.

    • Делаем:
      • По варианту 1+: сохраняется путь целиком, но при чтении первый компонент пропускается.
      • Проверено -- ручки по этим именам стали находиться.

    16.11.2016: и-и-и:

    • Добавлен парсинг:
      • dtype -- еще вчера сделано. В очень пока простом варианте: только 'd' и 't' (т.е., то, что сама Cdr генерит) и без разбора модификаторов, в т.ч. длины.
      • Имени ручки тоже еще вчера.
      • Имени канала -- почти копия от имени ручки, всё просто.
      • Вещественного значения -- тривиально.
      • Но НЕ строкового: по CXDTYPE_TEXT пока делается просто "goto NEXT_LINE".
    • Собственно -- загрузка режимов заработала!!!

      (Проверено на сервере-симуляторе.)

    • ...кстати, а зачем 10-11-2015 был придуман принцип "если есть (ИМЯ_РУЧКИ) -- то ищет её..., а иначе -- берёт имя канала..."?

      Казалось бы: графический клиент -- ну пусть только со своими каналами и общается.

      Потому вставлено "if (0)".

      01.11.2016@лыжи: понял, зачем -- чтоб Cdr могла читать файлы, созданные cdaclient'ом.

    17.11.2016: продолжаем причёсывать:

    • В datatree'шном set_knob_controlvalue() отсутствовала проверка на тему попытки записи в не-is_rw-ручки.

      ...а в v2'шной datatree_SetControlValue() она была!

      И сюда добавлена такая же.

    • Замечание: пустые -- ==NULL или "" -- компоненты имени в FprintfKnobPathPart() выдаются как "-" и потом при загрузке не находятся.

      Что -- СЕЙЧАС -- позволяет иметь под-иерархии, которые в режим писаться будут, а считываться -- нет. Так мы получаем просто информационные данные в режиме.

      Но в будущем-то предполагается, что NULL/"" -- это "прозрачность", так что сей хак пишется-но-не-читается работать перестанет.

    18.11.2016: Карнаев проверил на v4gid25s -- работает.

    15.02.2017: на днях вылезла проблема: при попытке прочитать Cdr'овский режим при помощи cdaclient -f FILENAME.dat оно вроде как всё прописывалось, но cdaclient зависал и никогда не завершался.

    Вчерашние разборки (при помощи специально введённого -D/) показали причину: он пытается загрузить ВСЁ содержимое режима, в т.ч. значения каналов чтения. А на каналы чтения (те, которые autoupdated) никакого ответа не приходит, вот он и подвисает.

    Решение очевидно: каналы чтения пишутся в режим "закомментированными" -- с префиксом '#'. Это сделано, надо будет проверить (хотя куда оно денется...).

    17.02.2017: то сохранение каналов чтения в режим как-и-каналов-записи вызывало еще одну проблему: не работала переполюсовка ИСТР'а M5 в v4gid25s при загрузке режима cdaclient'ом.

    • Выглядело так:
      • режим грузится, всё прописывается,
      • в конкретно этот ИСТР вроде бы уставка (Iset) прописывается,
      • но его CDAC20 остаётся со старой уставкой, никакой переполюсовки не происходит,
      • и сам драйвер висит в состоянии 15 -- IST_STATE_RV_ZERO, сброс уставки в 0 перед переполюсовкой.
    • Первая идея заключалась в том, что канал m5.Iset присутствует в файле дважды: в основном экране, а потом еще в под-окошке [...].

      Однако попытка руками cdaclient'ом сделать две записи подряд никакой проблемы не вызвала -- всё отработалось.

      А загрузка режима "cdaclient -f"'ом тут же воспроизвела застревание.

    • Стал смотреть, мониторя "cdaclient -mrDTn"'ом каналы m5.Iset m5.Iset_cur icd_m5.out icd_m5.out_cur.

      И увиделось (это и в GUI заметно), что значение в ЦАПе чуток дрыгается, и остаётся стоять.

      А более внимательный просмотр выдачи cdaclient'а показал, что это значение даже прыгает до 0, а потом возвращается к 3.3-что-то.

    • Итак, что происходило:
      • В файле содержались значения и m5.Iset (т.е., ИСТРа), и icd_m5.out (подчинённого ЦАПа).
      • При загрузке режима происходила следующая последовательность:
        1. Запись в канал m5.Iset значения другой полярности; драйвер ist_cdac20 запускал переполюсовку, которая в качестве первого шага вызывала...
        2. запись в icd_m5.out значения 0.
        3. Но дальше из файла шла команда записать в icd_m5.out некое положительное значение (пусть и соответствующее по модулю целевому значению Iset)...
        4. ...тем самым отменяя команду драйвера ИСТРа!
      • Но драйвер ИСТРа продолжал оставаться в состоянии 15, ожидая, когда текущее значение ЦАПа упадёт до 0 -- и никогда не дожидался!

        (Вывести его из этого состояния можно было сделав Выкл и потом опять Вкл.)

      Очевидно, что проблема заключалась в попытке восстанавливания значений ВСЕХ каналов, сохранённых в режиме, и после позавчерашнего исправления (или просто за-'#'-чивания строки icd_m5.out в файле) всё пришло в чувство.

    • А в GUI дрыг был заметен только потому, что значения в режимах e+ и e- чуток различаются по модулю (-838.8A и 833.4A соответственно (да, e+ -- -, e- -- +)).
    • А в обычном клиенте эта проблема не вылезала потому, что там попытка записи в не-rw-ручки блокируется на уровне datatree -- в set_knob_controlvalue().

    16.06.2017: вылез косметический косячок: CdrLoadSubsystemMode() ВСЕГДА возвращала -1 (там в конце так стоит -- рудимент от времён заглушки), и посему в statusline всегда писалось "Unable to open...".

    Исправлено на return 0.

  • 26.09.2016: реализовываем пропущенную пару лет назад фичу "conns_u".

    26.09.2016: в основном довольно прямолинейное портирование из v2.

    • "Подстилающая" функциональность -- в cda.
      • Интерфейс для клиентов -- cda_srvs_of_ref().

        Требуемая ей информация "номер сервера внутри контекста" уже есть -- это si>nth, созданное 01-08-2014 как раз для "conns_u".

      • А вот для добычи информации от формулы пришлось добавить в "formula API" -- cda_fla_p_rec_t -- метод srvs_of().
      • И в cda_f_fla.c добавлена cda_f_fla_p_srvs_of() -- очень прямолинейная, просто вызывающая cda_srvs_of_ref() для всех наличествующих в формуле OP_GETCHAN (но НЕ OP_PUTCHAN -- полностью аналогично v2).
    • Собственно основное мясо в Cdr:
      • Корневой точкой является CdrRealizeSubsystem() (тут, в отличие от v2, где CdrRealizeGrouplist() появилась для conns_u, имеющаяся изначально).
      • Сама работа выполняется в FillConnsOfContainer(), являющейся аналогом былой FillConnsOfElemInfo(), сразу содержащим в себе суть былой FillConnsOfKnobs() в виде цикла.

        По сравнению с v2 добавлен учёт контейнерова ap_ref.

      • Некоторая оптимизация: раньше каждая итерация -- обработка элемента, 2 раза -- сама добывала количество sid'ов. Теперь же оно добывается один раз в CdrRealizeSubsystem() и передаётся всем вниз.
    • @дорога-с-обеда-из-ТП-на-Инженерной-20: Одна только мысль гложет: ведь теперь принадлежность каналов к серверу не является фиксированной, а следовательно и принадлежность ручек к sid'ам тоже -- могут переезжать по ходу жизни.

      И даже более того: в случае без-серверных имён в момент насчёта массивов conns_u[] они будут принципиально пустыми -- т.к. в этот момент (выполнения Realize) никаких ответов еще не пришло.

      И что делать?

      • Очевидно, надо уметь пере-насчитывать все conns_u[] по мере надобности.

        А как эту надобность определять?

      • Видимо, нужно уметь ловить какое-то событие, говорящее "а вот тут у канала привязка сменилась".

        Такое событие ловить можно -- у cda_core эта информация есть, таким "событием" является момент изменения ri->sid.

        Но подобные события будут приходить "пачками" -- сразу для всех каналов сервера; реагировать на них по количеству элементов в "пачке" -- глупо.

      • Можно по этому событию просто взводить флажок (в subsys'е), а реальный пере-насчёт выполнять по событию CDA_CTX_R_CYCLE, если вышеупомянутый флажок взведён.
        1. Именно по событию "цикл" информация conns_u[] становится реально нужна.
        2. К этому моменту, весьма вероятно, вся "пачка" уже придёт.

      Пока-то забиваем (UDP-резолвинг недореализован), но проблема существует.

      P.S. Ирония в том, что сама идея была придумана именно для v4 (в 2007-м), впервые реализована в v2 (в 2010-м), а в v4 оказалась уже плохо подходящей -- т.к. парадигма с 2007-го сильно изменилась.

    27.09.2016: проверено на симулируемом ЛИУ (где 11 серверов) -- вроде работает.

    Осталось только:

    1. Проверить, как ведут себя совсем-безсерверные ручки (которые либо чисто с регистрами, либо командные -- с только "w", без "r"); в v2 они оставались голубыми.
    2. И, если проблема осталась -- ввести "DATAKNOB_B_NO_CONNS_U", как предусмотрено 07-10-2014.
    • Получасом позже: кстати, странно -- ведь в насчитывании есть проверка, что если в данном уровне "соединений нет" (т.е., conns_u[] содержит только нули), то массив выкинуть и оставить conns_u=NULL, в коем случае обрабатывать элемент вместе с содержателем. Почему это не спасает?
    • Еще часом позже: а потому, что в проверке косяк -- цикл
      for (x = 0; x > 0 ...
      не срабатывает ни разу, и последующее "x>0", соответственно, тоже.
    • Вообще та проверка изначально сделана косяковато (индусский код :D), надо было человеческий цикл с break по [x]!=0 и последующей проверкой "x<numsids".
    • Поправлено минимально -- условие в цикле стало "x>=0", и ура -- заработало!!!

      В v2 тоже портировано, но это проверим уже как-нибудь потом, на живом ЛИУ-2.

    Засим считаем раздел за "done" (на ПОКА, с учётом вчерашней гложущей мысли).

  • 18.02.2017@по-пути-из-дома-в-ИЯФ-после-обеда: есть -- у Карнаева в v4gid25s (и, возможно, на сварке) -- потребность кроме значений сохранять и параметры -- диапазоны, шаг, grpcoeff.

    Мысль:

    • Сохранять их в файл отдельной строкой, в формате вроде
      #.KNOBPARAMS ИМЯ-РУЧКИ ПАРАМЕТР=ЗНАЧЕНИЕ ...
    • Т.е.,
      • Префикс, начинающийся с '#' -- скроет эту строку от cdaclient'а.
      • Имени канала там не будет (незачем), а только имя ручки.
      • Сохраняться параметры будут и от не-rw-ручек тоже.
      • Формат ПАРАМЕТР=ЗНАЧЕНИЕ -- чтоб сохранялось только то, что было изменено (modified!=0).
    • И строка эта должна идти ДО строки со значением, чтобы сначала изменились диапазоны, а потом бы прописывалось значение (иначе в старые оно может не входить).
    • Строка должна писаться, только если есть хоть один параметр, требующий сохранения; иначе -- нет.
    • Что грузить, а что нет -- пусть указывается CdrLoadSubsystemMode() флагами в параметре options, чтоб можно было отдельно значения, отдельно параметры.

      А сохраняется пусть всегда всё (или тоже сделать параметр options, где указывать, ЧТО сохранять?).

    21.02.2017: предварительные действия:

    • К CdrSaveSubsystemMode() добавлен параметр options.

      Вставлен после filespec, ПЕРЕД comment.

    • Аналогично адаптирован единственный юзер -- ChlSaveWindowMode().
    • Cdr.h: введены enum-константы CDR_MODE_OPT_PART_DATA и CDR_MODE_OPT_PART_PARAMS, а также объединяющий их CDR_MODE_OPT_PART_EVERYTHING.
    • Тут есть тонкость:
      • С одной стороны, на уровне Chl может не быть наличия пространства имён Cdr. А вышеупомянутые константы определены в нём, и они ведь как-то должны даваться Chl'ю для передачи ниже в Cdr.

        И напрашиваются 2 решения:

        1. Вводить дублирующие константы CHL_... и "трансляцию" их в CDR'ные. Фу-фу-фу!
        2. Соглашение, что если передаваемые Chl'ю options==0, то превращать их в _PART_EVERYTHING. Тоже неприятно: так нельзя будет сделать просто "пустой парсинг файла", без модификации чего-либо.
      • С другой стороны, анализ .d-файлов показывает, что ВСЕ, кто использует Chl.h, используют также и Cdr.h (кроме Chl_err.d и pzframe_main.d -- в которых проблема нерелевантна).

        Так что можно просто забить и использовать CDR_-константы напрямую.

    • Итого -- в единственного ныне пользователя, ChlHandleStdCommand(), вставлена передача options=CDR_MODE_OPT_PART_EVERYTHING.
  • 20.02.2017: и еще: тот же Карнаев хочет, чтобы комментарий из последнего считанного режима оставался бы висеть где-нибудь на экране.

    20.02.2017: (в основном всё обдумано неделю-две назад, сейчас только записываю) задача состоит из 2 частей:

    1. GUI-часть -- уметь где-то эту надпись показывать.

      "Где-то" -- видимо, метка в тулбаре. Т.е.,

      • Возможность добавлять некий объект-текст на тулбар. Нечто, аналогичное XhACT_LABEL (и размер шрифта чтоб также ставился), но только XmText вместо XmLabel (чтоб размер не дрыгался при изменении текста).
      • Интерфейс к установке, какой текст отображать.
    2. Cdr-часть -- чтоб CdrStatSubsystemMode() позволяла добыть comment из загруженного файла.

    21.02.2017: простейшие действия (занимаюсь этим с горя, т.к. плавное изменение уставок CAN-ЦАПов не вытанцовывается):

    • Добавляем к CdrStatSubsystemMode() параметры commentbuf,commentbuf_size (как у Stat'а).
    • И в них теперь складывается комментарий -- если он есть в файле, а если нет -- то "".

      Взято из Stat'а.

    • Также к единственному юзеру -- ChlLoadWindowMode() добавлены параметры labelbuf,labelbuf_size.
    • ...и ейный единственный юзер -- ChlHandleStdCommand() -- адаптирован к изменившемуся набору аргументов; пока просто NULL,0.

    02.03.2017: потихоньку делаем GUI-часть (это совсем НЕ Cdr, но для цельности опишем здесь же).

    • Общая идея (задуманная еще с неделю назад):
      • "Инструмент" в описании тулбара в pult.c присутствует всегда, но параметр "число символов" в нём проставлено в 0.
      • И при этом соответствующий элемент при создании тулбара в нём не создаётся, а создаётся только при >0.
      • Клиентам же, которым нужен элемент "сообщение", в options указывается параметр toolbar_msg_cols=N, и тогда Chl_app в кнопку прописывает указанное число и она создаётся.
    • Xh.h: добавлены код XhACT_BANNER и макрос XhXXX_TOOLBANNER().
      • В качестве поля "число колонок" пришлось воспользовать cmd, хоть он и pointer -- иных кандидатов просто нет.
      • Кстати, обнаружил, что во всех макросах XhXXX_*() полю cmd присваивается значение 0 (явно от v2 осталось), хотя в v4 оно уже не int, а char*. Заменено на надлежащий NULL.
    • XhP.h: к _WindowInfo добавлено поле tbarBanner.
    • Xh_toolbar.c:
      • XhCreateToolbar(): обработка кода XhACT_BANNER и сохранение ссылки на созданный виджет в tbarBanner.

        Имя-виджета -- "toolBanner" или "miniToolBanner".

        При числе колонок ==0 объект не создаётся.

      • XhSetToolbarBanner() -- собственно вывешивание сообщения, тривиально.
    • Xh_fallbacks.h: fallback'и для нового вида виджета.

      Тут, увы, не всё гладко -- точной унификации с LABEL'ом не получилось, что-то там не слава богу с вертикальными полями (возможно, очередной косячок в Motif'е).

    • Chl_app.c:
      1. Организация на тулбаре:
        • Поле appopts_t.num_bnr_cols.
        • INT-параметр toolbar_bnr_cols.
        • В описание тулбара добавлено XhXXX_TOOLBANNER(0) сразу после кнопок Load/Save.
        • В цикл, перебирающий элементы для модификации, добавлено прописывание значения num_bnr_cols в banner'ово cmd.
      2. Использование:
        • Комментарий от ChlLoadWindowMode() теперь складируется в comment[]...
        • ...и сбагривается XhSetToolbarBanner().
    • Cdr_treeproc.c::CdrLoadSubsystemMode() некорректно пыталась отработать "#!COMMENT:" -- в else() от проверки на '#', так что никогда дотуда не доходило.

      Исправлено.

    Итого -- всё работает, "done".

    15.03.2017: вот тогда сделал, а на следующий же день подумал: ведь режим-то на ВЭПП-4 в основном грузится не из самой программы, а автоматикой -- скриптом, следящим за каналом "полярность" (в другом сервере).

    Следовательно, реализованная фича "показывать на тулбаре комментарий из последнего загруженного режима" оказывается полностью бесполезной.

    А надо делать показ чего-то другого -- то ли полярности (преобразовывая ей в строку (readonly-селектором?)), то ли именно некоего строкового канала.

    И сегодня Карнаев в разговоре это подтвердил.

    И давняя идея (присутствовавшая еще до реализации TOOLBANNER'а) -- такой хитрый тип container-плагина, который бы размещал своё содержимое на тулбаре (а содержателю отдавал бы виджет 1*1 пиксел).

  • 16.03.2017@утро-лыжи: ну что, надо реализовывать этот "на-тулбаре-показывающий" cont-plugin. Реализация будет состоять из:
    1. Поддержки Xh_toolbar'ом -- чтобы можно было "зарезервировать место" под наполнение.
    2. Собственно плагина.
    3. Поддержки в Chl_app -- чтобы можно было включать это "резервирование" в стандартном тулбаре.

    16.03.2017@утро-лыжи: некоторые идеи по реализации:

    • Этот cont-plugin должен использовать тот же принцип, что и subwin: он рождает ЕДИНСТВЕННОГО child'а (первого, если есть), а тот уж может быть контейнером и содержать внутри что угодно.
    • В Xh'е сделать вызов, который бы возвращал виджет-контейнер, должный использоваться в качестве parent'а для размещаемого на тулбаре.

      И чтоб этот вызов мог быть сделан только ЕДИНОЖДЫ -- следующие вызовы возвращали бы NULL, дабы не пыталось бы несколько "информаторов" туда разместиться (с труднопредсказуемыми последствиями).

    • "Алгоритм" Create_m()'а: добывается виджет-содержатель, и:
      1. если он ==NULL, то создаётся собственная XmForm'очка, в которую поселяется child, и эта же формочка прописывается в k->w;
      2. иначе поселяется в вёрнутый виджет, а в k->w создаётся виджет с типом widgetClass и размером 1*1 пиксел.

    17.03.2017: делаем.

    1. Xh_toolbar:
      • Саму концепцию называем "лоджия" (loggia).

        Первоначально идея была "ложа" (как в театре), но там не очень красивый перевод -- "lodge" или "box".

      • Введены XhACT_LOGGIA и XhXXX_TOOLLOGGIA().
      • В _WindowInfo добавлены поля tbarLoggia и tbarLoggiaAsked.
      • Создаётся "лоджия" (сейчас) в виде просто XmForm. В идеале содержимое бы как-нибудь центрировать по вертикали...
      • XhGetToolbarLoggia() используется для добычи виджета "лоджия" и работает по вчерашнему сценарию -- отдаёт лишь единожды, а потом NULL.
    2. Собственно MotifKnobs_loggia_cont.c -- довольно прост, всё по проекту.
    3. Chl_app:
      • В стандартном тулбаре LOGGIA располагается сразу после BANNER'а.
      • Включением фичи управляет параметр with_tbar_loggia.
      • Деактивация при незаказанности выполняется в том же if()'е, что и деактивация кнопок load/save/freeze.

    Итого: оно работает.

    Теперь надо придумывать, как использовать -- чтоб крупная (и цветная?) надпись в тулбаре показывалась. А то ни у text_text, ни у selector'а нету возможности менять размер.

    Заодно на v4gid25s пара проблем:

    1. Канал cx::vepp4-pult6:0.vepp3.polarityt без префикса "cx::" почему-то не пашет -- имя конкатенируется к базе, а не замещает её.

      24.03.2017: был косяк в kind_of_reference(), исправлен.

      28.03.2017: а сегодня выяснилось, что на vepp4-pult6:0 ссылаться и не надо, канал показывать прямо из своего сервера. Но косяк был обнаружен, исправлен, и это хорошо.

    2. При content:0 "{не-пустота}" имеем ds==NULL, но БЕЗ уведомления об ошибке и SIGSEGV.

      21.03.2017: был косяк в pult.c, исправлен.

    21.03.2017: selector наделён способностью менять размер/жирность шрифта. Теперь loggia в v4gid25s стала вполне юзабельна.

    27.03.2017: задеплоено на vepp4-pult6, вроде всё OK. Считаем за "done".

  • 24.07.2018: в CdrProcessKnobs() обнаружилась несколько странная работа ветвей _TEXT и _VECT с rflags: они получают от cda флаги прямо в k->currflags, НЕ трогая rflags; а общая часть после switch()'а потом радостно затирает k->currflags = rflags (в начале, перед switch()'ом, делается rflags=0).

    25.07.2018: исследование показало, что

    1. Эта "архитектура", похоже, была ошибочно перенесена из ветвей _CONT и _KNOB, где прописывание rflags прямо-сразу-там делается для того, чтобы вызываемые далее методы (речь вроде о колоризации) могли воспользоваться новыми значениями; там даже комментарии на эту тему есть.
    2. Тут же проблем не было заметно потому, что в choose_knobstate() скармливалось значение именно currflags.

      Но в колоризации контейнера эти узлы по факту не участвовали, поскольку в currflags эффективно прописывались нули.

    Вывод: надо обнормаливать.

    1. Вычитывание идёт в rflags.
    2. Колоризация тоже по ней.

    Скорее всего, последствий (деградации) не будет, но понаблюдаем.

  • 09.08.2018: был уродский косяк в CdrRealizeKnobs(): почему-то считалось ошибкой "count<=0", хотя 0 -- совершенно легитимное количество (отсутствие содержимого).

    Наткнулся вчера при попытке сделать "скелет" rfsyn_eng.subsys с пустыми панельками.

    Странно, что этот косяк раньше никогда не вылезал. А он ведь присутствует с самого появления CdrRealizeKnobs() ~14-06-2014 (w20140615.tar.gz).

    09.08.2018: исправлено:

    1. Условие заменено на "<0".
    2. Также, поскольку ошибкой там считается "list==NULL", что также вполне легитимно при count==0, то вставлено отделное условие, что count==0 приводит просто к "return 0".

    А еще добавляло "понятности" то, что ошибка там возвращается молчаливым return -1, без указания причины. И CdrLastErr(), видя пустую Cdr_lasterr_str[], честно пыталась сказать хоть что-то -- возвращая strerror(errno), где была совершенно не относящаяся к делу ENOENT:"No such file or directory", что еще больше запутывало ситуацию. Добавлены CdrSetErr()'ы на каждый возврат ошибки.

  • 19.02.2020: сделать бы оптимизацию -- чтобы для ручек, в которых значения НЕ менялись, при обновлении по циклу сервера и не дёргалось бы обновление на экране.

    Причина: сегодня звонит ЕманоФедя и говорит, что на v5p1 X-сервер жрёт 99% процессора. И виноваты, видимо, мои программы -- потому, что если их грохнуть, то X-сервер успокаивается.

    Стал я разбираться -- ну да, запущено десяток скринов, причём крупномасштабных, вроде linmag, ringcor*, linvac. И они обновляются с частотой 5Гц, давая преизрядную нагрузку на X11-сервер.

    Федя-то разверещался в стиле "ну а нафига они обновляются, если в основном там одни и те же данные?!". Но в принципе -- да, мысль разумная.

    19.02.2020: пытаемся проанализировать, как бы и что можно сделать для оптимизации.

    • Базовая идея: если уставляемое значение совпадает с текущим, то обновлять не нужно.

      Да, понятно, что для вещественных чисел сравнение на равенство вроде как некорректно, но в данном случае оно вполне катит: числа получаются из одних источников, так что при неизменности исходных данных и результаты будут совпадать.

      И тут есть 2 составляющих:

      1. Для УПРАВЛЯЮЩИХ каналов -- вообще почти никогда изменений не будет.
      2. Для каналов ИЗМЕРЕНИЯ (которых всё же большинство) -- в условиях, когда многие измерения проводятся многоканальными CAN-АЦП, успевающими все свои каналы обмерять хорошо, если раз в секунду -- реальные обновления будут происходить значительно реже 5Гц.

      Так что подход выглядит здравым.

    • А дальше некоторая сложность, заключающаяся в том, что вычисление+уставка нового значения и обновление на экране сильно разделены:
      • Вычисление и занесение нового значения в u.k.curv выполняет CdrProcessKnobs().

        И в нём же можно производить сравнение.

      • А вызов обновления -- точнее, вызов метода SetValue() -- делает set_knob_controlvalue(), вызываемый в конце процессинга ручки.
    • Отдельный нюанс: сравнивать бы надо не только само значение, но и флаги и ещё много что.

      ...или нет? И SetValue() отображает именно ЧИСЛО-значение, а за отображение расцвеченности отвечают иные функции/блоки?

    • Ну и ещё надо учитывать всякие "отсрочки обновления", вроде wasjustset и... ...а что именно "и"?

      02.09.2020: ага, вот этот неучёт wasjustset'а аукнулся -- если ввести бредовое число в поле канала, и сервер его модифицирует/ограничит, то на экране так и останется это бредовое число вплоть до следующего изменения в сервере. Подробнее -- ниже.

    19.02.2020@лыжи-~16:30: дальнейшие размышления по теме:

    • Вообще напрашивается, что "2 составляющих" функции должны работать совместно:
      • Пусть CdrProcessKnobs() определяет изменённость/неизменённость и передаёт сей факт в set_knob_controlvalue() для использования.
      • А уж тот пусть выполняет все свои обычные действия, а флаг неизменённости использует лишь для блокирования вызова метода SetValue().
      • Таким образом мы избежим как
        1. несовершенств -- с не-вызовом обновления даже когда надо, вследствие иных причин, так и...
        2. нарушения разделения обязанностей -- если бы CdrProcessKnobs() пришлось (воизбежание проблем из п.A) проверять лишние параметры.
    • Отдельно стоит вопрос "КАК передавать флаг?".
      • Тривиальный вариант -- добавить ещё один параметр к set_knob_controlvalue(). Но это криво -- смена API.
      • С другой стороны, там же УЖЕ есть параметр fromlocal.

        По семантике он в принципе не пересекается с "value not changed": уж если изменение от юзера, то отображать надо всегда.

        Фактически получаются 3 варианта:

        1. Локальное изменение -- надо обновлять.
        2. Не-локальное изменение, с изменившимся значением -- надо обновлять.
        3. Не-локальное изменение, с НЕизменным значением -- НЕ надо обновлять.

        Но, к сожалению, текущая ситуация со значениями иная: fromlocal -- булевский флаг, и "локальное изменение" имеет значение !=0.

      • Ну тут напрашивается сделать БИТОВЫЙ ФЛАЖОК "значение неизменно", который OR'ить с fromlocal (подобную технологию "подселения" мы уже много раз использовали -- например, с DoDriverLog()'овым параметром level).
    • ...отдельный вопрос будет с ПЕРВОНАЧАЛЬНЫМ обновлением: в ручке ж сначала лежит curv=0, и если при первом обновлении придёт значение тоже 0, то обновления никакого и не будет.

      И что -- инициализировать при создании curv=NAN? Чтобы последующее сравнение "v==curv", которое сведётся к somevalue==NAN, всегда дало бы false?

      Как-то не хочется.

      20.02.2020: чуток расследования:
      • Инициализация в NAN выполняется только в cda -- там оно прописывается в valbuf.f64 при CXDTYPE_DOUBLE и max_nelems==1.
      • А в Cdr (и в KnobsCore) ничего подобного нет, там curv=0.0 вследствие bzero()'енья всего при аллокировании ручек.

        И, судя по отсутствию упоминаний тут в bigfile-0002.html, никогда и не было.

    19.02.2020@дома-вечер: приступаем. Пока сделан минимум, в CdrProcessKnobs():

    • Введена флаговая переменная v_ne_curv ("ne" -- "Not Equal", а не русское "не" :D).
    • Ей, после вычисления нового значения -- v -- присваивается v != k->u.k.curv.
    • Была мысль блокировать по ней вызов обновления, но пока не стал.

    20.02.2020@дома-утро: доделываем.

    • В datatree.h добавлено определение 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: проверяем.

    • Во-первых, отображение вроде работает.
    • Запущенные по сети с v5p1 на дисплей x10sae (через ssh-тоннель) -- никакой разницы между загрузкой X-сервера "старыми" и "новыми" нет: она в обоих случаях близка к 0 (там firefox больше даёт).
    • Добавил даже диагностику (по умолчанию закомментированную): всем ручкам делается set_knob_otherop(,!v_ne_curv_flag) -- таким образом изменяемые в данном цикле ручки подсвечиваются оранжевым.
    • Окей, проверяем в пультовой, на полу-занятой (X-сервер жрёт 22-25% CPU) v5p3? путём запуска 10 одинаковых клиентов linmag (они все оказываются друг под другом, т.е., перерисовка неполная):
      • Старый вариант, без оптимизации: +20%, т.е., около 2% на клиента.
      • Новый вариант, с оптимизацией: ~+2-3-4%, т.е., около 0.2-0.4% на клиента.

        Во время переключения режимов, когда меняется сразу много каналов (сначала все управляющие, а потом все измеряемые) -- загрузка кратковременно подскакивает (точно засечь числа не удалось, но до в районе ~+7-15%).

      • Отдельный бонус -- загрузка процессора самими клиентами ниже: старые жрут в районе 4-5% каждый, а новые около 2.5-3%.

    Итого: всё замечательно, достигнута огромная оптимизация и очень задёшево!

    Бинарники 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'а получилась гадость гадская... История:

    • ЕманоФедя на прошлой неделе пожаловался, что у клистронов в v5kls не работают ограничения во всех полях ввода: например, в hvset можно вводить хоть 100000, и оно там так и останется (хотя должно б было сброситься до ограничения по nominal). Хотя в устройствах всё отрабатывается как надо.
      • Изучение драйвера показало, что "такого быть не может".
      • Даже отладочная печать была добавлена и протестировано на x10sae в полу-симуляции (подчинённым устройствам включена симуляция, а самому v5k5045 нет) -- тоже всё как положено. Проверялось cdaclient'ом.
      • ...но потом запущен таки графический клиент v5kls, в двух экземплярах (было подозрение, что чё-то не то с квантом, из-за чего модифицированное сервером значение считается "близко" к введённому и то оставляется в поле; на 2 клиентах было бы видно, что же реально попадает в сервер, поскольку userval во втором клиенте не будет).

        И тут обнаружилось -- что ДА, сказанное Федей правда: остаётся что введено.

        А во втором клиенте при этом -- отображается истинное число.

    • Поглазев на код в Cdr_treeproc.c::ProcessKnobs() быстро догадался о причине: НЕотображение совпадающих значений. Т.е., происходит следующая цепочка:
      • В момент ввода взводится wasjustset.
      • На ближайшем цикле обновления он предотвращает обновление ручки -- это было сделано для Старостенко Саши, чтобы "ручки не вырывались из рук" (реализуется напополам с квантом), но...

        ...в curv прописывается пришедшее от сервера значение.

      • В результате на втором после ввода обновлении сравнение показывает, что свежепришедшее значение совпадает со "старым", так что типа нет смысла обновлять поле.

        Но в поле-то значение всё ещё НЕПРАВИЛЬНОЕ! А об этом уже никто не помнит :-(.

    • Что-то я не вижу, где бы делалось wasjustset=0. Только условно по SYNTHETIC.

      А-а-а, нет -- наоборот, при SYNTHETIC==0. Т.е., практически всегда.

    Итого: ПРИНЦИПИАЛЬНО-то понятно, как решать проблему: надо следующий после снятия wasjustset цикл отрабатывать обновление ОБЯЗАТЕЛЬНО. Но вот КАК? Где/как сохранять уведомление "обязательно"?

    • Самое-то очевидное -- ввести ещё один флажок, типа "force_next_update".

      Но не хочется тратить ещё один int.

      ...если б там хоть битовая маска была, а так -- расточительство...

    • Была также идея "а давай при отправке значения в сервер -- в set_knob_controlvalue(), видимо -- записывать его ещё и в u.k.curv -- тогда это автоматически приведёт к несовпадению значений и обновлению! Но нет, т.к.:
      1. Как раз в ближайшем-то цикле несовпадение и так, весьма вероятно, будет -- если петля "отправка серверу, отработка, возврат обновлённого" ещё не успеет завершиться и в cda будет храниться старое значение.

        А надо ЧЕРЕЗ цикл.

      2. И значение curv используется также в других местах -- для отображения на histplot (где "фиктивное" значение уж точно не к месту), и в bigval.
    • Также что-то можно думать насчёт userval_valid (но что -- не очень понятно, поскольку "оранжевость" в v4 по факту не работает).

    08.09.2020: сделал, по простейшему и наиболее очевидному пути -- флагом "force_next_update".

    • Добавлено поле data_knob_t.force_next_update.
    • Сброс wasjustset'а в CdrProcessKnobs() сделан более хитро:
      1. Он теперь сбрасывается не безусловно, а только если он взведён.
      2. И при этом делается force_next_update=1.
    • Отработка самого force_next_update выполняется в точке после определения v_ne_curv_flag:
      1. Сбрасываем force_next_update=0.
      2. И нулим v_ne_curv_flag=0.
    • Делается это ТОЛЬКО для DATAKNOB_KNOB.

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

    11.09.2020: исправлена недоработка от 20-02-2020 в имени флаговой переменной -- v_ne_curv_flag переименована в v_eq_curv_flag: ведь флаг ВЗВОДИТСЯ при РАВЕНСТВЕ значений.

Cdr_plugmgr:
  • 17.07.2007: создаем его в минимальном варианте.

    17.07.2007: он содержит таблицу стандартных читеталей (в данный момент -- file и string), связанных в список, и позволяет добавлять к этому списку (регистрировать) новые читалки. Поскольку добавление идет в начало списка, то у "регистрируемых" приоритет перед стандартными.

    Можно также раз-регистрировать читалки -- по имени схемы. Чтобы не раз-регистрировали стандартные, в описателях есть флажок builtin, равный у них 1.

    Сами внутренности Cdr добывают у plugin-manager'а указатель на читалку при помощи CdrGetSubsysLdrByScheme().

    Проверено на симуляторе -- работает!!!

  • 17.10.2016: добавлен модулёчек Cdr_plaintext_ldr.c.

    Скопирован из "file", только схема "!m4" заменена на "!plaintext".

    17.10.2016: зачем:

    • Первопричина в том, что диагностика в связке ppf4td+Cdr_via_ppf4td очень так себе: в ppf4td съезжают номера строк (и, в случае макросов, они всё равно слабополезны), а в Cdr'е в ругани указываются только ФАЙЛ:СТРОКА, но НИКАК НЕ указывается парсимый в текущий момент узел.
    • Идея же была в том, чтобы можно прогнать текст через m4 заранее, а потом натравить pult/pzframeclient уже на готовый файл -- чтоб строки из него указывались.

      Для коего натравливания и требовалась возможность сказать "бери plaintext, безо всякого препроцессирования!".

    • Ну сделал, да. Только использовать не получилось, чисто по дурости: препроцессирование делалось "m4 -s", и в вывод попадали "#line NNN", сбивавшие ppf4td с толку -- режим-то PPF4TD_LINESYNC_NONE, вот он и считал те строки за обычные комментарии, и потому backspash-NL'и из макросов обрабатывались криво.

      А надо было запускать просто "m4", без "-s". Ладно, на будущее останется.

Cdr_fromtext:
  • 17.07.2007: создаем этот модуль.

    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:

    • Пашет и последовательное указание полей, и именованное -- с "fieldname:VALUE". Во втором случае парсер переходит в обязательно-именованный режим, так что все последующие поля также должны указываться с именами.
    • Некоторые -- реже используемые -- параметры могут быть "только-именованными": для этого их описания помещаются в таблице после специальной метки, работающей как "end of sequental parameters list".
    • После любого поля можно вставить и комментарий, начинающийся с '#' -- тогда все за ним игнорируется.
    • И можно после любого параметра поставить backslash+NL, оно тогда считает в буфер следующую строку.
    • И парсер "CONTENT" также сделан и работает. С корректным разборов разных "типов" ручек -- knob, button, selector, etc.

    07.08.2007: продолжение:

    • Сделал PARAM_pparser() и связанную с ним инфраструктуру.

      Сам парсер довольно уродлив -- просто куча выпарсиваний и проверок, все "вручную", а в конце -- добавление ячейки к массиву kparamsdescr_t.aux_params и ее заполнение.

      ParseKnobDescr() же в конце "сливает вместе" стандартные параметры и эти дополнительные.

    • А REF_pparser() пока работает просто как MSTR -- поначалу этого более чем достаточно.

    08.08.2007: перевел Cdr_file_ldr.c на свежесделанный findfilein() -- теперь он честно ищет subsys-файл в нескольких местах, причем суффикс ".subsys" прибавляет сам.

  • 24.07.2007: поскольку в chlclient'ах в CXv2 частенько требовались некие арифметические вычисления на основе #define'ов/enum'ов, дающие на выходе число, то такая фича будет желательна и здесь.

    Самым подходящим синтаксисом выглядит shell-подобный $[ВЫРАЖЕНИЕ]. Он отлично ложится на нынешнюю модель макросов вообще и ParseMacroString() в частности. И, как и в shell'е, ссылки на макросы изнутри $[...] можно делать прямо просто именами, безо всяких символов доллара.

    Вопрос только в том, что надо будет собственно корректный парсер арифметических выражений сделать :-).

  • 24.07.2007: кстати, а ведь можно было бы очень легко реализовать и условный парсинг файла -- команды .if/.ifdef/.elif/.else/.endif -- если перенести обработку .-команд из основного цикла прямо в read_line(), что в любом случае выглядит более резонно.

    А .if/.elif пользовались бы наличием $[...].

  • 26.07.2007: и вообще, если этот парсер/препроцессор получается таким умным и можным (макросы, условная обработка, арифметика) -- то надо бы его вообще вытащить в отдельный модуль, например, lib/misc/textfile_parser.c, и дадим ему префикс tfp_.

    Тогда его же услугами смогут пользоваться и читчик текстовых blklist-файлов в сервере, и конфигуратор сервера, и cx-starter для своего subsys-list файла.

    Конечно, область компетенции его несколько параллельна/ортогональна PSP, но уж с этим мы разберемся.

  • 21.05.2008: добавил парсинг директивы winopts в секцию 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.

  • 05.12.2013: в связи с переходом на ppf4td этот модуль увольняем и удаляем, вместе с его так и не наполнившимся преемником Cdr_newf_ldr.

    Полный список удалённых -- Cdr_file_ldr* Cdr_fromtext* Cdr_newf_ldr* Cdr_string_ldr* Cdr_via_smp4td*.

    Ну и "obsolete".

Cdr_file_ldr:
  • 27.11.2013: приступаем.

    Модуль создаётся пока под именем Cdr_Zfile_ldr.c. Пока тут всё вместе, а потом разделим на сам плагин и Cdr_via_ppf4td.c.

    03.12.2013: за прошедшую неделю наполнен, копированием из старого Cdr_fromtext.c с переделкой под ppf4td.

    • Реализован новый синтаксис указания узлов, в CONTENT_fparser():
      • Убран KKIND_fparser() с надобностью указывать "r" или "w", а вместо этого...
      • ...первым ключевым словом указывается knob (w) или disp (r) -- по давнему проекту от 24-10-2007@SNS.

        Также оно может быть devn или minmax.

        Ну и прочие "шорткаты" вроде selector остались.

      • И в ParseKnobDescr() добавлен параметр kind, куда результат
      • Кроме того, реализована возможность явно указывать behaviour-флаги в стиле ключей командной строки RSX/VMS/DOS (как придумано 03-07-2007@bigfile-0001.html).

        Сюда входят все индивидуальные флажки DATAKNOB_B_ (кроме NO_WASJUSTSET), так что любой из "шорткатов" имитируется при помощи knob/ФЛАГИ.

        Перед '/' разрешается whitespace (а не обязательно слитно).

        Множественные флаги могут указываться либо через тот же '/', либо через ','.

    • Некоторая сложность в CONTENT_fparser() имеется с проходом по строкам в числе count штук: ведь пустые строки надо пропускать, а ppf4td_is_at_eol() по EOF тоже отдаёт +1.

      Так что пришлось химичить, делая в IsAtEOL() отдельную проверку на EOF с возвратом -1.

    • И, кстати, свежевведённые NextCh() и PeekCh() ("интеллектуальные" адаптеры к ppf4td_nextc() и ppf4td_peekc()) тоже содержат тучу проверок.
    • Хитрее/сложнее сделан парсинг тэгированных параметров name:value. Поскольку теперь оно обязано потреблять по символу из потока, то и ':' ищет последовательным вычитыванием, а если считает ненайденным, то взятое возвращает обратно через ppf4td_ungetchars().
    • Существенное отличие в ParseKnobDescr() при обращении с параметрами: вместо странного подхода с "pret" теперь оно просто делает корректный cleanup как при ошибке (чего раньше НЕ производилось в случае ошибки где-то ПОСЛЕ указания параметров), так и при обычном переселении auxparams'ов в общий список. Для чего введена CleanKparamsDescr().

    04.12.2013: первоначальную проверку -- nexample.subsys оно прошло, окно-результат парсинга выглядит точно так же.

    Теперь можно удалять старое, разделять файлы, исправлять мелкие тонкость и проверять работоспособность всех проверок парсинга.

    05.12.2013: финальные штрихи этой первой версии:

    1. Чуть более широкое применение PeekCh() и NextCh(). Но НЕ повсеместное, т.к. далеко не везде надо проверять на EOL.
    2. Удаляем старое -- и smp4td*.[ch], и его применения.
    3. Переименовываем "Zfile" в "file".
    4. Делим (расписано по цепочке уровней, верхние пользуются нижними):
      • В Cdr_file_ldr.c остаётся только плагинная обвязка.
      • В Cdr_file_via_ppf4td.c живёт CdrLoadSubsystemFileViaPpf4td(), выполняющая предварительный поиск файла.
      • В Cdr_via_ppf4td.c уехала CdrLoadSubsystemViaPpf4td() -- "мясо" парсинга.
    5. Протестировано -- вроде синтаксис парсится корректно.

    Засим первоначальную версию ставим "done".

    16.04.2014: сделан первый m4-based-экран -- frolov_d16.subsys. Проверка прошла успешно -- генерация контента макросами работает как надо.

    21.04.2014: сделано второй m4-based-экран -- cac208.subsys, более навороченный:

    1. Используются циклы (для генерации тучи строчек DAC и ADC).
    2. Более развитая вложенность.

      Используется для тестирования эвристики сборки полного имени канала из defpfx,base,spec.

    Замечено сильное неудобство нынешней реализации связки Cdr_via_ppf4td.c+ppf4td: строки, начинающиеся с '#', не вполне эквивалентны просто пустым, и если так пытаться закомментировывать куски, то парсер на них ругается.

    27.04.2015: вроде бы как минимум с частью проблемы разобрались давно -- 04-08-2014.

  • 05.12.2013: на будущее -- для всяких "мегаклиентов" -- понадобится еще парсинг из строки (через схему "!mem").

    А желательно бы еще мочь оное пропускать через m4 -- чтоб иметь возможность использовать в builtin-экранах как минимум арифметику. Вот как? Через pipe в v4 на stdin? В любом случае, это надо будет реализовывать схемой в ppf4td.

    22.08.2023: забавно, что это тут записано -- через полтора месяца ПОСЛЕ успешной реализации схемы "mem::" 22-10-2013.

Cdr_file_via_ppf4td:
  • 18.07.2014: модуль рождён ещё в прошлом году, а сейчас создаём раздел.
  • 18.07.2014: есть некоторое неудобство -- нет возможности прямо указать полное имя .subsys-файла, т.к. к указанному имени (считаемому именем подсистемы) всегда явно добавляется суффикс ".subsys" и делается поиск по списку путей.

    А надо бы мочь -- в т.ч. чтобы можно было оформлять группировки с

    #!/PATH/TO/pult
    и делать их executable.

    18.07.2014: сделано -- первым шагом оно пытается сделать прямо fopen(reference), и только если облом, то начинает поиск.

    12.12.2015: только от той фичи вылез неприятный побочный эффект (еще давно обнаружилось): при попытке запустить pult-программу, сделанную симлинком на pult, прямо в текущей директории, из-за вышеуказанного алгоритма она пытается открыть прямо саму программу -- т.е., бинарник pult.

    Например: программа ringsqr --

    • subsys-файл ~/4pult/screens/ringsqr.subsys,
    • бинарник ~/4pult/bin/ringsqr->pult,
    • при запуске из ~/4pult/bin/ (например, Enter'ом в Midnight Commander) оно в качестве имени подсистемы берёт "ringsqr",
    • первым шагом пытается открыть прямо это имя -- а файл такой есть и смотрит на бинарник.

    Придумываются 3 варианта защиты от этой напасти:

    1. Пытаться открывать прямо reference не первым делом, а ПОТОМ, если все остальные попытки обломились. Т.е., поменять местами ветки if()'а в CdrLoadSubsystemFileViaPpf4td().

      Сработает, но это криво: получается, что в поиске приоритет у "других" директорий, вместо того, что указано прямо в командной строке прямой ссылкой.

    2. Проверять, не совпадает ли reference с argv0.

      Идея типа правильная, но не сработает: а) в качестве reference передаётся не полное argv0, а его basename(); б) собственно argv0 НЕ передаётся, так что и сравнивать не с чем (хотя вот ЭТОТ бардак -- исправить НАДО).18.12.2015: как это не передаётся -- передайтся! Еще 25-08-2015 сделано!

    3. Пытаться открыть прямо reference только если в нём присутствует хоть один '/'.

      Неприятно, конечно, что для текущей директории придётся указывать "./", но терпимо. А уж файл-менеджеры префикс пути указывать будут, проверено (SL-6.3): MC -- ./ИМЯ, Nautilus -- полный путь.

    Сделано по 3-му варианту -- работает.

    24.12.2015: вариант с необходимостью указывать "./" всё-таки неудобен -- вроде и [Tab] в shell'е работает, а файл не подхватывается.

    Поэтому добавлено альтернативное условие: что в имени есть '.' -- предполагается, что это начало расширения ".subsys". Имена подсистем вроде точек иметь не должны.

    Да, под Windows будет "проблема" -- чего-то.EXE конвенцию нарушит; но вся затея актуальна только для симлинков, а чтоб нажимать [Enter] на симлинке.EXE -- это уж совсем вряд ли. (Хотя если что -- всё-таки можно будет вариант «содержит '.'» ужесточить до «заканчивается на ".subsys"».

  • 12.03.2015: касательно диагностики (по результатам запинывания отладочных панелей adc200me.subsys и adc812me.subsys):
    1. Надо бы ругательства выдавать более адресно -- хоть упоминать имя ручки, при парсинге которой проблема.
    2. Что-то там не так с подсчётом номеров строк. И неясно, тут или в ppf4td самом.
  • 21.07.2015: врЕменная мера: по умолчанию ищет не в ~/pult/..., а в ~/4pult/... (этому-то пофиг, но всё остальное (вроде драйверов и конфигов) для не-конфликта с v2 будет временно жить в ~/4pult/).

    06.12.2015: уже энное время ВСЁ в CXv4 настроено именно на ~/4pult/, включая cx-starter. Может, и оставить именно 4pult "родной" директорией для v4?

  • 11.09.2015@утро: указывать бы в subsys-файлах (секциями) списки {knob,container,noop,user}-плагинов, которые нужны этому скрину. Потребно для вакуума -- для гистограмм, чтоб "столбики" делать плагинами прямо для pult'а, без необходимости в vacclient'е.

    И, кстати, желательно б (в datatreeP.h и Cdr) поддержку user-узлов сделать -- чтоб гистограммам исходники данных подготавливала бы Cdr: {r0,r1,...,r9}_{src,ref}.

  • 17.12.2015: какая-то странность вылезла с m4: в некоем случае он почему-то не удалял пару "`'" (хотя вопрос скорее, почему в других случаях удалял), и вследствие этого как-то "терялась" часть из списка ручек в группировке. 1. Это как-то хитро связано с наличием/отсутствием кавычек в СЛЕДУЮЩЕМ параметре макроса). 2. Также это зависит от наличия/отсутствия \NL'ей.

    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: сильно не хватает имевшейся в C/cpp возможности разбивать закавыченную строку (в смысле string) на несколько строк (в смысле lines). Тут получается, что необходимо даже длинные строки (вроде options у histplot'а и items у choicebs'а) пихать слитно, из-за чего читаемость сильно падает.

    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...", и удаление пробелов перестаёт работать.

    • Первопричина -- в том, что этот "while() с длинным условием" работает на самом низком уровне, прямо через PeekNextCh()/ReadNextCh(), и видит этот '#' как символ, на чём и прекращает пропускание пробелов.
    • Почему это раньше не проявлялось: судя по комментарию за 04.08.2014, “cpp does NOT insert "# NNN" inside string or char constants”.

      Но то было под RH-7.3, а в CentOS-5.2/m4-1.4.5-3, похоже, поведение отличается.

    • Пока (в liu.subsys, ELEM_RACK (include()'нье dl200me_f.m4), выкрутились удалением отступов -- всё идёт прямо с начала строк.

    07.09.2016@Снежинск-каземат-11: и еще в схожую степь: конструкция

    first line \
    #commented line
    second line
    
    не работает, как ожидается: "second line" НЕ является продолжением "first line".

    Впрочем, тут некий вопрос -- а КАК правильно? Пожалуй, именно нынешнее поведение...

  • 28.07.2016: задолбало писать "param1", "param2 и "param3", каждый раз мучительно вспоминая, кто же из них за что отвечает.

    Сделать бы "alias'ы" с человекочитаемыми именами...

    28.07.2016: вводим:

    1. ncols->param1, nflrs->param2, nattl->param3.
    2. coltitles->str1, rowtitles->str2, subwintitle->str3 -- за компанию.

    Всё это, конечно, ПОСЛЕ KFD_BRK(), и в обоих -- GRPG_fields[] и CONT_fields[] (кстати, а нафига они у нас разделены? чисто исторически?).

  • 29.09.2016: изменена идеология восприятия "behaviour". Т.к. оно из исключительно-для-DATAKNOB_KNOB превратилось в общую сущность, то:
    1. /ФЛАГИ теперь может указываться у любого типа ручки, а не только у knob/disp.
    2. В ParseKnobDescr() теперь k->behaviour=behaviour делается всем типам.

    Да, есть некоторая странность в том, что, например, контейнеру может указываться "/fixedranges", но это плата за универсальность.

:
datatree:
  • 18.06.2007: сегодня эта библиотека создана -- как "библиотека обслуживания структур Knobs", услугами которой и пользуются Knobs и Cdr.

    18.06.2007: и именно в ее компетенцию переведены все структуры дерева ручек.

    Для include-файлов принята следующая организация: публичные определения (в т.ч. KNOBSTATE_* -- бывшие COLALARM_*) и API расположены в datatree.h, а структуры -- в datatreeP.h, безо всяких там *types.h и typesP.h.

  • 18.06.2007: при попытке тупо скопировать CXv2::Cdr'овскую 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.

  • 21.06.2007: о структуре: явно напрашивается разделение библиотеки на несколько исходных файлов. А именно -- надо уносить в отдельные модули всякие сервисно-информационные вещи типа strknobstate_{short,long}(); текстовые сервисы типа get_knob_label(); GUI-сервисы, связанные с user-input-editing и undo.
  • 23.06.2009: (базовое, уровня cx.h) Беркаев высказал на толковище о СУ ТНК, что у них на ВЭПП2000 есть кроме "желтого" и "красного" еще третье понятие -- "совсем неправильное"; например, у значения не тот знак. И что они подсвечивают такие каналы синим.

    А ведь такое вполне осмысленно, и синий цвет у нас как раз пока незадействован, а главное -- сделать-то это очень просто! И флаг "синее" будет уставляться ТОЛЬКО формулами (потому как никакого общего правила для него существовать не может).

    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 #0000FFcx/ too).
    • В MotifKnobs_ChooseColors() добавлена соответствующая альтернатива.

    Пока не проверял, но не работать там нечему, так что "done".

    03.08.2009: полностью портировал и в CXv2. Смысл: у нас ведь уже давно есть потребность в такой фиче -- в случае многобитных входных регистров надо как-то показывать, если комбинация бит "бессмысленна". Такое есть в ringcamsel и в ringpem, а сейчас понадобилось в senkov_ebc. Итак:

    • В основном всё сделано по образу 4cx/.
    • Только из-за другой разблюдовки флагов сделан CDA_FLAG_WEIRD=1<<20. Таким образом, этот флаг находится в компетенции cda, а не Cdr.
    • Добавлена строчка в Chl_help.c.
    • Сделан опкод OP_WEIRD='\\', и добавлена его условная обработка по образу и подобию OP_CALCERR.
    • Никаких попыток поддерживать сие в cx-starter'е НЕ сделано. 24.07.2012: а вот теперь -- сделано.

    24.07.2012: по здравому размышлению -- WEIRD должен ВСЕГДА быть в компетенции cda, поскольку именно она этот флаг и уставляет.

    Так и сделано; с соответствующей корректировкой таблицы строк, и в v2 тоже скопировано.

    (При этом сменилось определение CXCF_FLAG_CDR_MASK -- на просто явный список. "Смысловые группы" же теперь в конце, под заголовком "General classification".)

  • 04.12.2009: обязательно нужен ТИП оконечного узла "текст".

    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_text.c. Плюс,
      1. Все внутренние символы в MotifKnobs_text_knob.c для однозначности переименованы из *_text_* в *_text_knob_*.
      2. Слова defknob, defnoop, defcont и defgrpg переделаны в defk_nob, def_noop, def_cont и def_grpg.

      Файл набит содержимым практически сразу -- благо, в основном было копирование кусков из MotifKnobs_text_knob.c и из MotifKnobs_internals.c -- того, что относится к API "Text", и первоначально создавалось в v2'шном Knobs_text_widget.c.

    • MotifKnobs_internals'овский MakeTextWidget() сделан публичным под именем MotifKnobs_MakeTextWidget().
    • Метод cont'а SetPhys() переименован в SndPhys() -- так явно корректнее. И введён аналогичный ему метод SndText(), в конечном итоге приводящий к...
    • ...CdrSetKnobText(), чьи внутренности практически отражают товарку CdrSetKnobValue().
    • Плюс дополняющая её datatree.c::set_knob_textvalue().
    • И еще некоторые модификации в datatree -- чтоб всякое *_knob_editing() работало не только DATAKNOB_KNOB, но и с DATAKNOB_TEXT.
    • Примерно в этот момент...
      • Пришлось добавить в dataknob_text_data_t поля-копии таких же от dataknob_knob_data_t (is_rw, timestamp, usertime, wasjustset, being_modified).
      • И замечено, что никакая колоризация с не-DATAKNOB_KNOB работать не будет, вследствие специфичности для dataknob_knob_data_t полей curstate, attn_endtime и attn_state.
      • И тут явно напрашивается идея вытащить все эти поля на общий уровень, в data_knob_t (включая behaviour -- ради DATAKNOB_B_NO_WASJUSTSET). Объём её не увеличится -- т.к. knob_data и так самая большая, а удобство возрастёт.
    • Для удобства Cdr'а, чтоб не надо было иметь свой промежуточный буфер (неизвестного размера), введена cda_acc_ref_data(): она даёт указатель непосредственно на внутренний cda'шный буфер со значением ("acc" -- access), плюс текущий размер данных в нём (этакий рудимент zero-copy).
    • И тут вдруг тупик: ведь XmText'у-то надо давать NUL-terminated-строку, а не pointer+length!

    08.08.2015@ночь: решение очень простое: пусть в cda для CXDTYPE_REPR_TEXT аллокируется на 1 ячейку больше, а при получении данных место после nelems заполняется нулём.

    09.08.2015: делаем,

    • Мелкое добавление в cda по вчерашнему ночному проекту -- буквально пара однооператорных if()'ов.
    • Cdr_treeproc.c::txt2ref() -- регистрирует ссылку, предварительно парся опциональный префикс "@t[COUNT]:". Флаги понимаются все те же, а вот тип -- только 't', зато понимает количество элементов.

      Полностью отдельная от src2ref()/cvt2ref(). Хотя в будущем напрашивается как-то это всё унифицировать (передавать туда параметром def_dtype?), чтоб оно и для потенциальных DATAKNOB_BIGC и DATAKNOB_USER сгодилось.

    • Ну и собственно поддержка в Cdr'е при обходе деревьев -- при создании (Realize) и обновлении (Process).
    • Последний штрих -- парсинг в Cdr_via_ppf4td.c. Readonly-вариант именуется mesg, read-write -- text. Список свойств базово-куцый -- ident, label, look, options, src.
    • Ура, работает!!!

    11.08.2015: вытаскиваем некоторые ранее DATAKNOB_KNOB-specific поля в data_knob_t.

    1. Собственно переносим поля.
      • Так что теперь choose_knob_rflags() (в нём чуть переупорядочены проверки, так что сравнения с диапазонами делаются только для KNOB), choose_knobstate() и прочие set_attn() годятся для ВСЕХ типов ручек.
    2. Делаем метод Colorize() общим, перенося его в dataknob_unif_vmt_t.
      • Так что пришлось поперекурочить все VMT.
      • И set_knobstate() работают со всеми типами.
      • И MotifKnobs_DecorateKnob() тоже.

    Засим считаем задачу решенной, "done".

    15.11.2016: авотфиг! Некоторое время назад, при создании панельки vepp34info.subsys, было замечено, что строковые отображаторы (mesg) почему-то НЕ обновляются и вообще остаются перманентно цвета JUSTCREATED.

    Разбираемся:

    1. Пустота:
      • Пустота -- потому, что в "r" не было указано количество символов. При указании 10 -- строка появляется.
      • Но! По умолчанию-то стоит 1 -- следовательно, этот 1 должен отображаться (место под terminating-NUL аллокируется в cda дополнительно).
        • А он не отображается! И при явном указании "@t1:" тоже.
        • А при "@t2:" -- уже сразу 2.
        • ...разобрался!!! Всё дело в постулате, что "max_nelems==1 => скаляр".

          Вот оно в cda_dat_p_update_dataset() и пропускает обновления из не-1-символа для каналов с max_nelems==1.

          (А если в канал заслать 1 символ -- то он отображается.)

      Итого, выбранное решение: в Cdr_treeproc.c::txt2ref() умолчание nelems=1 сменено на nelems=10.

    2. Нерасцвечивание: тут всё просто -- никакая колоризация никогда не вызывалась (хотя инфраструктура было модифицирована под это еще 11-08-2015).

      Вставлен вызов set_knobstate(), аналогичный DATAKNOB_KNOB'ову, и всё стало тип-топ.

  • 12.02.2016: замечание: реализация DATAKNOB_TEXT'а радикально отличается от _KNOB'а -- тем, что текущее значение в ручке НЕ хранится (как curv), а берётся от cda в момент обновления и напрямую передаётся ручке. Т.е., _TEXT'ы без GUI нефункциональны совсем.
  • 19.02.2016: надо на уровне datatree/Cdr поддерживать и _BIGC- и _USER-ручки.

    Пара мыслей о технике реализации:

    1. Регистрацию "больших" каналов (точнее, ССЫЛОК) делать функцией big2ref(), воспринимающей префикс "@T[NNN]:", где "T" -- dtype (обязательный), а "NNN" -- количество элементов (опциональное). Синтаксически совместимо с console_cda_util.
    2. Такие ручки требуют обновления скорее не по циклу, а по событию. Ну так и ввести behaviour-флаг "DATAKNOB_B_ON_UPDATE", что данную ручку обновлять прямо по приходу данных, а не по событию "цикл" (по которому, наоборот, НЕ обновлять).

      Заодно и обычные числовые ручки тоже получат такой функционал.

    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).
    • В Cdr_treeproc.c::cvt2ref()/src2ref() добавлен параметр behaviour, у которого если выставлен DATAKNOB_B_ON_UPDATE, то заказывается CDA_DATAREF_OPT_ON_UPDATE.
    • И значение k->behaviour передаётся при регистрации rd_ref.
    • ...но ни CDA_REF_EVMASK_UPDATE не запрашивается, ни реакции на него пока не сделано.

      Просто пока неясно, зачем бы (pzframe_gui пока своими силами обходится).

    • Замечание: первоначально -- 24.05.2015 -- была идейка сделать ещё один @-префикс/флаг, аналогично всяким "@-" и "@.". Чтоб он же годился и для cdaclient.

      Однако оказалось, что символы, индифферентные для shell'а, уже почти все заняты:

      • Планировавшийся '/' ("@/") -- оказывается, уже указывает на SHY.
      • Остались лишь ',', '=' и ^. Но запятая -- разделитель (можно, но чревато), "равно" -- символ присваивания (тоже в принципе можно, но некузяво и чревато ошибками при опечатках), а "крышка" выглядит странно да и мнемонический смысл с ней отсутствует (...вот если бы '!' ("немедленно!")!).
      • Кроме того, тратить последние свободные символы так дешево -- чревато: не останется возможностей для расширения.

      ...а для cdaclient'а по-прежнему что-то надо будет придумать...

    20.05.2023: возвращаемся и доделываем поддержку DATAKNOB_B_ON_UPDATE для ОБЫЧНЫХ ручек (а никаких "_BIGC- и _USER-ручек" де-факто сейчас просто нет).

    • "для cdaclient'а" уже ничего придумывать не надо -- он с 09-03-2017 ПО УМОЛЧАНИЮ работает в режиме ON_UPDATE (если не надо, то указать префикс "@~").
    • Собственно доделка:
      • При регистрации rd-ссылок для B_ON_UPDATE-ручек взводится битик CDA_REF_EVMASK_UPDATE в evmask.
      • RefStrsChgEvproc() переименована в Knob_rd_Evproc().
      • И в неё добавлена реакция на событие UPDATE.
      • Реакция эта сводится к тому, что для данной ручки вызывается CdrProcessKnobs() с cause_conn_n=-1 и options=CDR_OPT_SYNTHETIC -- т.е., оно выглядит как синтетическое обновление.
    • Чего НЕ сделано: изначально-то была задумка, чтобы такие ручки на экране обновлялись бы по событию, а по циклу пропускались бы.

      Но решено было такого не городить (хотя и можно было, введя специальный CDR_OPT_-битик для options в CdrProcessKnobs() -- обрабатывать ли событийные ручки, и для обычного процессинга ставить "нет", а изнутри события "да"). Во-первых, оптимизация сомнительная, а во-вторых (и в-главных) -- обновления по событию сейчас работают как "синтетические", но надо же и обычные исполнять.

    После этого желаемое заработало. Проверено 2 способами, в обоих случаях генерацией изменений чаще, чем цикл сервера:

    1. Значения в поле меняются оперативно, а не раз в цикл.
    2. Графики значений таких каналов обновляются оперативнее -- в т.ч., при hist_period меньшем, чем цикл сервера, на графике есть значения за каждый период истории.
  • 09.10.2017: вылезла неприятность с указанием меток/заголовков строк через метки ручек: можно, при помощи NOLABELTIP_PFX ('\n'), указать, что метка/тултип только для заголовка, а самой ручке её не ставить; но НИКАК нельзя указать, что оно только для ручки, а в заголовок не копировать.

    Понадобилось для таблицы из 2 колонок (ringrf, куркинский модулятор), чтоб в некоей строчке были только в теле колонок метки "1" и "2", а row-label чтоб "". Но вот фиг -- нет способа.

    @Вечер-дорога-домой-Лаврентьева принять, что "ну вот так оно, смиритесь"?

    09.10.2017@утро-лифт-с-1-го-на-5-й: а если ввести ЕЩЕ один такой префикс, но с обратным значением -- "эта метка/тултип только для ручки, а в заголовок её не копировать!"? Например, '\r' -- он и близок к '\n', и практического смысла в начале строки в принципе не имеет.

    10.10.2017@вечер-дорога-домой-около-ИПА: да, и -- чтобы всё чётко различалось по именам -- этому нововведению дать дополнительный префикс "TITLE", а старому -- "KNOB".

    11.10.2017: делаем.

    • Сами определения -- в misc_sepchars.h; он не менялся аж с 04-07-2009.
    • Имеющимся определениям CX_KNOB_NOLABELTIP_PFX_C и CX_KNOB_NOLABELTIP_PFX_S (нигде не используется) ничего добавлять не нужно -- там словцо "KNOB" и так фигурирует.
    • Добавлены CX_TITLE_NOLABELTIP_PFX_C и CX_TITLE_NOLABELTIP_PFX_S.
    • Собственно модификация кода, в datatree.c:
      1. get_knob_label() и get_knob_tip(): добавлена дополнительная проверка-альтернатива на CX_TITLE_NOLABELTIP_PFX_C, для сдвига указателя на 1 символ (пропуск '\r').
      2. 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".

datatreeP.h:
  • 23.06.2007: заметки об общем наполнении разных частей.

    23.06.2007: итак:

    • Еще с самого начала (в соответствии с проектом в bigfile-0001.html от 03-12-2005--06-12-2005) тип data_subsys_t содержит список секций, плюс -- готовые указатели на некоторые стандартные разделы, типа grouping'а.
    • Да, и собственно -- подсистемой является как раз этот список, а "группировка" -- одна из секций, являющаяся просто DataKnob'ом.
    • Воизбежание проблем с указанием числа колонок, этажей, и т.п. -- никакого "ncols" теперь нету, а есть целочисленные param1, param2, param3. Первые два как раз и будут за ncols и nfloors.
    • А для "colnames", "rownames" и SUBWIN'овского "text2" (реально -- "subwindow title", а метку вместо "..." пусть берет прямо из label) введены строковые str1, str2, str1 -- они и будут за rownames, colnames, subwintitle.
  • 02.07.2007: надо в dataknob_knob_data_t ВСЕГДА иметь поле string-list, для селектора -- чтобы эта штука была отвязана от look'а "селектор".

    03.07.2007: да, ввел поле items, ПЕРЕД units и dpyfmt. И в KnobsCore_simple.c его поддержка тоже добавлена.

    Теперь надо вводить функции для обработки селекторных строк-списков -- подсчет числа элементов и выдача элемента по номеру.

  • 02.07.2007: кхм, ведь мы же собирались делать, чтобы строковые команды бродкастились по всей группировке, так? Как именно будем это реализовывать?

    02.07.2007: в принципе, ничего сложного. Проект:

    • Делаем метод HandleCommand. Видимо, метод должен быть прямо в dataknob_unif_vmt_t -- поскольку такой метод нужен и контейнерам, и ручкам, и user-type'ам.
    • Метод будет возвращать int, при 0 обход продолжается, а не-0 означает, что метод команду обработал и можно завершать обход.
    • Функция "broadcast command" пусть живет, как и остальная деревообходная функциональность, в Cdr.

    03.07.2007: да, введен метод HandleCmd(), int, прямо в dataknob_unif_vmt_t.

    09.08.2007: и функция CdrBroadcastCmd() изготовлена -- тривиально. Так что этот раздел можно считать за "done".

    08.11.2010: побочный эффект, о котором тогда не задумывалось: можно делать "локальные" тулбары для подветок -- так что команды будут бродкаститься только на эту под-иерархию. Полезно, например, для fastadc-плагинов.

    Всё получится автоматически -- достаточно указать верхушку под-иерархии.

  • 05.07.2007: поскольку надо завести флажок _B_IS_SELECTOR, то -- давно пора образумить подход к behaviour-флагам, чтоб удобно было при надобности добавлять новые.

    05.07.2007: угу.

    Теперь флаги "что это есть такое" (BUTTON, LIGHT, ALARM, ...) идут вверх с 0, а "то, как с ручкой обращаться" (GROUPABLE, NO_WASJUSTSET, INCDECSTEP_FXD) -- вниз с 31.

    И DATAKNOB_B_IS_SELECTOR введен, он -- 3.

    P.S. Естественно, все _B_HALIGN_* и _B_VALIGN_* оставлены в прошлом -- они теперь и нафиг не нужны, и вообще мимо кассы.

  • 12.07.2007: а еще для ручек нужен метод "PropsChg", вызываемый для уведомления ручки, что какие-то свойства изменились. Диапазон отображения, is_grouped, и т.п.

    23.07.2007: введен такой метод, и text_knob ему уже даже обучен -- для перещелкивания пометки "загруппировано".

    Вызывать этот метод, конечно, пока что некому -- ибо Chl пока даже директории нету -- но это дело наживное.

  • 16.07.2007: о типе DATAKNOB_SYST: что и как с ним вообще делать?

    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, когда это никакая не "[под]система", а вовсе просто группировка, которых в подсистеме может быть и несколько?

    16.07.2007: да, поменял везде SYST/syst на GRPG/grpg.

  • 19.07.2007: кое-какие куски в 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" -- которое бы вызывалось при завершении работы программы?

    Смысл --

    1. Чтобы chl-"программы" имели возможность сохранить накопленную информацию.
    2. Или же, если они исполняли какое-то продолжительное действие с аппаратурой, то могли бы завершить это действие (хотя -- сие уж вряд ли, ибо:
      1. "Завершить" -- это выполнить какие-то команды записи, а они не факт, что реально сразу же будут отправлены серверу; скорее, отложены на "попозже", которое не наступит вследствие завершения программы.
      2. Вообще такой подход порочен -- столь критичные вещи должны делаться надёжнее: либо не-chlclient'ами, либо вообще спец.драйверами в сервере.

    Для реальной юзабельности этой фичи потребуются 2 основных условия:

    1. Чтобы программы при завершении делали CdrDestroySubsystem(), а не просто exit().
    2. Чтобы программы ЛОВИЛИ ВСЕ ПРИЧИНЫ ОТПАДОВ -- и убиение средствами X11 (но это, скорее всего, будет просто возврат из XtAppMainLoop() etc.), и сигналы. И чтобы по этим вещам выполняли "корректное завершение".

    И еще --

    • НЕ НАДО вызывать этот "at_term" при убиении еще-не-дореализованной подсистемы (а CdrDestroy*() вызывается при обломе создания какой-то части, для уничтожения уже-созданного).

    Не слишком ли много сложностей?

    Пожалуй, правильнее оставить возможность делать что-то по выходу только ОБЫЧНЫМ программам, а chlclient'ам это незачем.

  • 20.07.2007: поскольку типы секций у нас строковые, то надо завести некоторое количество #define'ов со стандартными именами -- чтобы не указывать всякие "grouping" каждый раз вручную (что чревато ошибками). (На эту тему даже в документации по Xt отдельно было сказано -- что всякие XtNnnn сделаны ровно для предохранения от опечаток.)

    Возьмем префикс, например, DSTN_ (Data Section Type Name).

    24.07.2007: да, DSTN_GROUPING уже введен, по мере надобности будем вводить еще имена.

    И, поскольку этот "тип" -- штука фиксированная, то в data_section_t.type всегда будет прописываться просто указатель на строку, а не на malloc()'ed memory, так что его НИКОГДА освобождать и не надо. О чем там сделан комментарий.

  • 27.07.2007: несколько перетряхнул порядок полей в data_knob_t.

    27.07.2007: во-первых, так что type, look и options -- т.е., те, которые определяют "суть" ручки -- теперь идут самыми первыми.

    Во-вторых, текстовые поля теперь идут "по мере убывания важности" -- сначала ident и label, а уж за ними -- редконужные tip, comment, style, layinfo и geoinfo, которые в описании группировки-то являются до того optional, что могут указываться только-по-имени.

  • 29.07.2007: давно ясно, что надо вместо отдельного поля dataknob_knob_data_t.group_coeff завести просто еще один параметр.

    03.08.2007: да, сделано -- введен индекс DATAKNOB_PARAM_GRPCOEFF=2, плюс резервный DATAKNOB_PARAM_RSRVD1, а нижеидущие сдвинуты на 2.

    И CreateSimpleKnob() с Cdr_fromtext.c::KNOB_fields[] проапдейчены соответственно.

  • 29.07.2007: а еще вылезло-таки некое возможно-отличие GRPG от CONT: ведь где-то должны указываться "параметры окна" -- всякие notoolbar, nostatusline, etc.

    И тут вопрос: делать это полями в 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: у нас ведь, в принципе, могут бывать всякие секции, про тип которых Cdr не будет совершенно ничего знать, так? А destroy им делать как-то надо, причем, возможно, не просто при помощи free().

    31.07.2007: выход напрашивается простой -- добавить в data_section_t еще указатель на функцию-деструктор, который если не-NULL -- то можно вызвать, и оно все сделает.

    Тогда всякие хитрые плагины (это будут уже "subsystem-sections plug-ins" :-) будут туда прописывать свои специфичности.

    В общем -- сделал поле destroy. А в CdrDestroySubsystem() у него будет приоритет -- если оно не-NULL, то дергается этот деструктор, а внутренние "знания" не используются.

  • 31.07.2007: об унификации NOOP/KNOB и CONT/GRPG -- ха-ха! Там ведб не только поля, а еще и методы в VMT!

    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: некоторые добавления.

    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: упсь... А метод-то контейнера NewData() -- забыли!

    02.08.2007: понадобился он для subwin_cont'а, чтобы отображать цветом состояние под-окна.

    Метод добавлен, и вызов его в CdrProcessKnobs() тоже.

  • 03.08.2007: переименовал-таки этот файл из datatree_P.h в просто datatreeP.h, безо всякого подчерка.

    03.08.2007: первоначально-то делал с подчерком потому, что казалось, что как-то кривовато выглядит -- если всякие CdrP.h имеют и заглавные буквы, то тут, мол, надо бы отделить подчерком. Фигня -- не надо. Только путаницу вносит из-за нестандартности.

    Так что -- переименовал, везде в коде исправил ссылки, и в этом файле тоже везде позаменял, кроме ЭТОГО раздельчика.

  • 06.08.2007: в dataknob_knob_data_t есть куча битовых флагов текущего состояния -- userval_valid, curv_raw_useful, wasjustset, undo_val_saved, is_ingroup -- а не сделать ли их (экономии ради) битиками в одном слове "текущего состояния"?
  • 11.08.2007: хочется уметь для любой ручки быстро получить ссылку на ее подсистему. Чтобы не надо было использовать дурацкий обходной путь -- по виджету получить XhWindow, а уж из того -- подсистему.

    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: и, кстати, уже понадобилось -- для Chl -- знать имя подсистемы.

    11.08.2007: да, вводим DSTN_SYSNAME и "шорткат" data_subsys_t.sysname, не забывая и его проставление.

  • 11.02.2011@Снежинск-каземат-11: надо кроме 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".

  • 13.02.2011@Снежинск-каземат-11: а еще можно ввести флажок "протоколировать все уставки в данную ручку" -- чтоб при set_knob_controlvalue() факт записи протоколировался в event_log. Нужно -- например, в kuznitsa при изменении значения накала, чтоб сей факт записывался независимо от включенности периодического логгинга.

    Вопрос только -- КТО будет это делать и как. Наверное, придётся ввести hook.

  • 22.05.2014@Снежинск-каземат-11: исторически сделано, что имя влияет и на сохранябельность/логгируемость ручки -- это определяет непустота и не-начинающесть с '-', определяемая в IdentifierIsSensible().

    А сейчас вот реализуем в manyadcs сохранение данных от "прочих" fast-ADC, и ясно, что такое должно делаться стандартными средствами "среды". Но вопрос об ограничении -- кого сохранять, а кого нет -- по-прежнему стоит, и именами оно не шибко правильно.

    А чё б это не повесить на что-то типа "behaviour" -- чтоб можно было для любого узла указать, что "начиная с этой точки сохранять не надо".

    Только behaviour -- поле в dataknob_knob_data_t, а надо бы что-то пообщее. Ввести нечто прям в data_knob_t.

    22.05.2014@Снежинск-каземат-11: несколько в другую сторону, хоть и связано -- насчёт сохранения вообще.

    • Когда-то давно -- 15.02.2006 в bigfile-0001.html -- была выдвинута идея "сохранять данные для больших каналов: строками!".
    • Но: иногда надо не "в режим" сохранить данные большого канала, а ЭКСПОРТИРОВАТЬ -- для возможности потом где-то использовать. Например, fastadc'шные.
    • И как?

    23.05.2014@Снежинск-каземат-11: и еще прикол: идентификаторы используются как

    1. для такого поиска по дереву, так и...
    2. ...для ссылки соседей друг на друга (в canvas'е).

    Т.е., (1) -- по смыслу, а (2) -- декоративное.

    Но они вступают в противоречие: по смыслу может понадобиться иметь идентификатор ":", а для оформления при этом он будет нужен!

    И добавление правила "считать прозрачными все имена, НАЧИНАЮЩИЕСЯ с ':'" не сильно поможет, из-за проблем с поиском -- см. bigfile-0001.html за 27-08-2013.

    28.07.2014: да, НАДО считать прозрачными:

    1. Имена, начинающиеся с ':'.
    2. Пустые имена.

    Пара замечаний:

    • Насчёт "начинающиеся с..." -- проверено на макете liu.subsys, где в кнопке модулятора ненужный по смыслу, но нужный для отображения элемент был назван ":subwin", и canvas прекрасно с ним работает.
    • Вся эта "прозрачность" касается, конечно, только контейнеров -- в отношении ручек она лишена смысла. Плюс, в свете фичи "строки обновляются от сервера" (19-07-2014) пустые имена РУЧЕК будут иметь совсем иной смысл.

      Но тут всё складывается наилучшим образом:

      • ручки могут обновлять свои имена, но их прозрачность не касается;
      • контейнеров же прозрачность касается, но им обновлять имена совершенно не как (не из чего).
      • хотя, идея "большой канал есть контейнер -- для fast-ADC, чтоб могли добавлять свои экранные ручки"; там как бы и то, и другое...

    11.08.2015: учитывая, что экраны избавлены (СЕЙЧАС) от функционала сохранения/чтения режимов, вопрос вообще несколько подвисает в воздухе -- не станет ли он вообще obsolete?

    09.10.2015: а вот и нет, для маленьких стендов этот функционал очень нужен, даже НЕОБХОДИМ.

    Дальнейшие рассуждения -- в раздельчике "Clientside-режимы".

  • 07.10.2014@Снежинск-каземат-11: потребность в «"behaviour" для 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, ему там самое место.

datatree -- всякое общее старое:
  • .........23.06.2007: теперь сигналом "взять метку/тултип от первого knob'а" является указание в rownames/colnames не "?", а просто пустой строки. Для указания же именно ПУСТОЙ строки предназначена комбинация "!-!". Кроме того, теперь проверка на тему "не начинается ли метка/тултип с CX_KNOB_NOLABELTIP_PFX_C" занимается прямо сама get_knob_rowcol_label_and_tip().
  • 25.06.2007: еще давно были мысли, что надо вызывать метод-parent'а не напрямую -- k->uplink->vmtlink->Method(), а поискав ближайший вверх по иерархии, которы !=NULL.

    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: делаем стандартные методы для добычи меток из селекторных строк-списков.

    05.07.2007: общие положения:

    • Указываться будут именно только метки, т.е., в старом смысле -- #F и #T. А всякие старые #L, #+, #-, #=, #H и #C -- нафиг не нужны, потому что касаются уже конкретной реализации knob'а и будут указываться через егойный options.
    • Посему -- если строка начинается с '#', то следующий символ определяет тип строки (пока что имеют смысл только #F и #T), а иначе она считается просто за multistring меток -- т.е., как #T.
    • Парсинг имеет сделать в стиле "open, {getnext}". Поскольку там разбор нетривиальный, то клиент должен завести специальную структуру для хранения парсером приватной информации о состоянии.
    • Для большей общности передаваться будет не DataKnob, а прямо сама строка -- как и в случае 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'е оно пока не используется (поскольку стили пока никак не поддерживаются).

  • 16.07.2007: пора делать вызов типа былого ChooseStateRflags(), который будет с учетом значения выбирать флаги ручки. И чтоб этот вызов мог бы использоваться и с simple-knobs.

    03.08.2007: сделан choose_knob_rflags(), пока что БЕЗ махинаций с RELAX и OTHEROP -- поскольку с ними надо разбираться отдельно, разделяя обязанности этой функции и прочих.

    И в SetSimpleKnobValue() также добавлены вызовы choose_knob_rflags() и set_knobstate().

  • 16.07.2007: у большинства ручек будут 10 стандартных параметров. А к каждому параметру прилагаются идентификатор и метка, которые вроде как положено отводить в динамической памяти, но они-то всегда одинаковые!

    16.07.2007: "пользователей" этих имен и меток двое:

    1. Отображатели этих вещей в GUI.
    2. Исполнители формул (и прочие tcl'и).

    Для первого хватит функции типа "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.

  • 30.07.2007: добавлен новый метод для ручек -- ack_knob_alarm().

    30.07.2007: причина рождения этой функции -- то, что вызывать parent'ов AckAlarm() нужно из нескольких мест, а процесс его вызова -- из-за find_knobs_nearest_upmethod() -- весьма нетривиален.

  • 01.08.2007: за компанию -- чтобы MotifKnobs_Mouse3Handler() и MotifKnobs_CommonKbdHandler() не мучались -- добавил функции show_knob_props(), show_knob_bigval() и show_knob_histplot().

    01.08.2007: кстати, а вообще, не скрыть ли тогда find_knobs_nearest_upmethod() внутри datatree? Ведь, по идее, для всех VMT-вызовов должны иметься подобные функции-переходники, а их "клиентам" совершенно незачем знать специфику выполнения вызовов.

  • 02.08.2007: а почему, собственно, у нас нету get_knob_tip(), и все вынуждены химичить с NOLABELTIP_PFX сами, в то время как get_knob_label() -- очень даже есть?

    03.08.2007: да, сделал и заиспользовал.

Ручки/узлы типа vector:
  • 11.07.2018: новый раздел, чья реализация реально касается толпы уровней -- datatree, Cdr, KnobsCore, MotifKnobs; но основное записывать будем здесь.
  • 11.07.2018@утро-пляж: надо б сделать новый тип ручек vect, чтоб легко вектора можно было в интерфейсе представлять и манипулировать ими.

    (Возникло из идеи "а на что еще можно было бы обобщить работу weldproc_drv, и насколько сам он реально нужен -- чё б не обходиться просто записью в каналы таблицы напрямую из программ?")

    11.07.2018@утро-пляж: соображения (просто поток сознания):

    • Ведь именно этого не хватало для GUI табличных каналов.
    • Кода типа -- DATAKNOB_VECT.
    • Тип ручки в .subsys-файлах -- vector, ro-вариант -- column (сомнительно, но покатит).
    • Буфер под данные вектора АЛЛОКИРОВАТЬ.

      В отличие от узлов TEXT, где схалявлено и напрямую сбагривается по указателю от cda к XmText'у.

    • Умолчательный тип knobplugin'а -- текстовый, который просто колонка.
      • Выглядеть должен как колонка с количеством сверху.
      • Вариант -- строка (количество слева).
    • Поля напрашиваются (которые в ведении datatree/Cdr'а): src+ref, dpyfmt.

      @вечер: еще что-нибудь с cur_nelems устроить?

    11.07.2018@дома-душ-после-пляжа: типа продолжение:

    • А не сделать ли еще (в будущем) возможность указывать число колонок либо столбцов, чтоб вектор отображался матрицей?

      Тут будут трудности: как поддерживать кратное количество столбцов/строк (чтоб общий размер делился нацело), ...?

    • Еще проблема: а как редактирование делать -- как совмещать его с отправкой значения канала в сервер? Какой момент считать за "редактирование закончено": любую модификацию -- изменение числа строк или редактирование числа в одной ячейке? Так плохо: будет нецелостное значение. Делать кнопки [галочка] (сохранить) и [x] (отказ)? Наверное, так наиболее правильно, но шибко муторно.

      @после-обеда-переход-в-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:

    1. Указывать явно, числом.
    2. Брать из описателя канала -- "@i100".

    Если чуть подумать, то очевидно, что 2-й вариант не катит: даже если забыть про меньшее удобство доступа, всё равно остаётся вопрос "выбора правильного места для функционала". Если без словоблудия, то: теоретически vect-плагины могут использоваться в simple-режиме, БЕЗ ссылок на каналы; и тогда остаётся только 1-й вариант, как единственный удовлетворяющий требованиям.

    12.07.2018: да, в dataknob_vect_data_t добавлены:

    1. max_nelems (указывабельное группировкой/создателем) и cur_nelems (будет заполняться Cdr'ом или потенциальным simple-интерфейсом).

      Кстати, а rflags и timestamp уже есть -- общие в data_knob_t.

    2. Забытый вчера databuf, имеющий тип double*.

      Да, API будет ориентирован на double'ы -- ровно как и у скалярных ручек.

    16.07.2018: замечание: хоть "профита немного", но всё же по минимуму делаем -- то, что ПРОСТО (и очевидно).

    16.07.2018@вечер-пляж: надо объединять поля max_nelems и src в одну структуру. Смысл:

    • Они реально составляют единое целое и должны парситься вместе.
    • Можно, конечно, пользоваться знанием о реальном расположении полей внутри data_knob_t -- как это делает CONTENT_fparser() -- для получения указателя на вмещающую ручку и уже поштучного парсинга в неё.
    • Но если в будущем подобрые дуплеты {max_nelems,src} появятся еще (в тех же USER-узлах), то понадобится парсер, независимый от вместителя.
    • Поэтому лучше сделать именно структуру вроде "dataknob_vect_src_t".

    16.07.2018@хбз-когда-дорога-на-работу-мимо-НИПС: был вопрос, как делать кнопку "отправить", которая должна выглядеть как зелёная галочка (в противовес "отмене", которая красный крестик).

    Да просто: символ "=".

    Т.е., будет пара кнопочек -- [=][X].

    17.07.2018@утро-пляж: некоторые мысли:

    • Как должен выглядеть "API работы с VECT-узлами" -- а так же, как и для KNOB-узлов, только вместо одного double указывать дуплет {int nelems, double *values}.
      • Метод узла -- _k_setvectv_f под названием SetVect().
      • Общая "уставка" -- set_knob_vectvalue() -- третья, после set_knob_controlvalue() и set_knob_textvalue().
      • Плюс еще понадобится cont'ов метод SndVect() и соответствующая поддержка в Chl_app.c в виде _ChlSndVect_m(), адаптера к тоже требующемуся CdrSetKnobVect().
    • "как редактирование делать -- как совмещать его с отправкой значения канала в сервер?": по прошествии недели всё кажется не столь уж и муторным, а скорее правильным, да и довольно очевидным.
      • Да, текстовое поле "количество" плюс кнопки [=][X].
        • Можно кнопки селить справа от количества.
        • А можно под ним; это указывать где-нить в опциях.
        • (после обеда) А 11-06-2017 есть идея ставить их над колонкой номеров строк.
      • Давно очевидно, что на время редактирования обновление ручки-столбца должно прекращаться -- в точности как с KNOB-узлами, на основе полей usertime,wasjustset,being_modified (с соответствующими API-функциями).

        Тем более, что сами поля уже давно прямо в общем data_knob_t

      • Работа с клавы: вызывать отправку можно комбинацией Ctrl+Enter; отмена редактирования -- Esc.

      Кста-а-ати, см. в пункте "о реализации таблиц" за 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/.

    1. datatreeP.h:
      • Тип dataknob_vect_src_t для дуплета сделан и переведено на него (еще вчера).
      • Введён тип _k_setvectv_f, с ним сделан метод dataknob_vect_vmt_t.SetVect().
      • Плюс тип _k_sndvect_f, для...
      • ...dataknob_cont_vmt_t.SndVect -- вот из-за его вставки пришлось пораздвигать VMT во всех cont-knobplugin'ах.
    2. datatree.c:
      • set_knob_vectvalue() -- скопирована с очевидными модификациями с set_knob_textvalue().
    3. Cdr_via_ppf4td.c:
      • Сделан 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():
        1. Выбор по типу DATAKNOB_VECT таблички VECT_fields[].
        2. Прописывание
          k->is_rw = kind == DATAKNOB_KIND_WRITE;
          -- аналогично DATAKNOB_TEXT'ову.
    4. Cdr_treeproc.c:
      • CdrDestroyKnobs(): подчистка.
      • FillConnsOfContainer(): добыча списка слизана с TEXT'ова.
      • Новенькая vec2ref():
        • По списку параметров НЕ унифицирована с src2ref()/txt2ref(): отсутствуют всякие rw, behaviour, params, evproc (а есть только реально нужное).

          Так что унификации, о которой думалось при введении txt2ref(), покамест не получится.

        • В качестве параметра "источник" передаётся прямо dataknob_vect_src_t.
        • При префиксе '@' можно указывать флаги -- @-, @., @/. Но более ничего -- ни dtype, ни nelems; и в конце флагов обязательно ':'.
        • Регистрируется всегда CXDTYPE_DOUBLE, с указанным max_nelems.
      • CdrRealizeKnobs(): тут всё тривиально и слизано с TEXT'ова.

        Плюс аллокирование databuf.

      • CdrProcessKnobs(): тоже всё просто и аналогично TEXT'ову.

        Отличие в том, что данные добываются в "свой буфер" -- databuf. ...а ЗАЧЕМ, кстати? А для возможности делать "cancel", по Esc.

      • CdrSetKnobVect() -- опять же очевидно слизано с CdrSetKnobText().
    5. Chl_app.c: _ChlSndPhys_m() -- тривиальный адаптер к предыдущему.

    Ну, собственно -- вроде как всё, теперь нужен MotifKnobs_text_vect.c.

    Хотя идеологически/методологически остался вопрос: а нафига нам databuf[]?

    • Изначальная (на пляже?) мысль была в том, чтоб таблица текстовых полей прописывала бы туда значения при редактировании каждого поля, дабы потом было откуда это всё скопом отправить.
    • Но, по факту, СЕЙЧАС оно используется в той же роли, что dataknob_knob_data_t.curv -- т.е., туда складывается полученное от сервера значение.

      А коли так, то его НЕЛЬЗЯ менять knobplugin'у, не его это владение: придёт от сервера новое значение, даже при взведённом usertime и типа заблокированном обновлении -- и всё, данные будут испорчены.

    • Для чего еще остаётся: возможность делать "cancel" по Esc? Но тогда, по-хорошему, надо бы делать всё целиком, как у DATAKNOB_KNOB -- в т.ч. с аналогом userval, а для возможности undo -- еще и с аналогом undo_val...

    Вот и получается, что:

    1. Проще вообще забить (пока?) на продвинутые возможности редактирования и поступать по-простому, как с DATAKNOB_TEXT'ом: сбагривать knobplugin'у прямо текущие данные от cda, добытые через cda_acc_ref_data().
    2. Тогда, кстати, и поле dataknob_vect_data_t.cur_nelems нафиг не нужно: оно имеет смысл только в связке с databuf[], а без него достаточно локальной переменной при передаче данных SetVect()'у.
    3. И, если в будущем всё же захочется реализовать "удобства", то аллокировать буфер(а) нужно только для is_rw-ручек, т.к. ro'шным оные удобства без надобности.

    19.07.2018@утро-дома: с синтаксисом указания ссылок N:CHANREF может быть проблема -- ParseKnobDescr() воспримет это "N:" как ключ (имя поля).

    Анализ кода показывает, что достаточно заменить в предварительной проверке isletnum(ch) на isalpha(ch) -- всё равно имя поля не может начинаться с цифры.

    20.07.2018: да, ровно эта проблема вылезла, и была решена именно тем способом.

    19.07.2018@обед-дома-лестница-по-дороге-на-работу: а еще не помешала бы возможность ограничивать диапазоны ввода.

    Только куда тут приткнуть этот alwdrange?

    • Параметров -- как у KNOB'ов -- никаких нет.
    • Сделать просто double[2]?
    • Но как бы тогда приткнуть туда же использование 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/").

:
cda:
Общие вопросы:
  • 21.06.2007: в свете желания иметь в рамках cda поддержку разных протоколов и разных формульных языков, напрашивается ее разделение на модули.

    21.06.2007: предполагаемые компоненты библиотеки:

    • "Ядро" -- как раз сложнее всего сказать, что же войдет в него. Видимо, менеджер "таблиц" серверов...
    • Менеджеры плагинов двух классов: протокольных плагинов (для протоколов CX, EPICS/CA, TINE, ...) и исполнителей формул. Но их проще будет держать в том же файле, что и ядро -- ведь только ядро и будет пользоваться их услугами.
    • Модуль добычи данных через протокол CXv4 -- cda_d_cxv4.c.
    • "Локальный" протокольный плагин -- "плагин установки значений" (bigfile-0001 за 23-04-2005: «хорошо бы иметь (для не-simple-программ) возможность указывать в "общем описании" некие "свои" элементы/ручки, которые НИКАК не будут привязаны к соединению»). Чтоб каждой программе не приходилось иметь его реализацию. Его "тип протокола" -- "local", а он -- cda_d_local.c.
    • Менеджер "классических" формул -- видимо, cda_f_fla.c, будет включать и долгожданный транслятор.
    • Биндинг для tcl'я -- cda_f_tcl.c.
    • Maybe, биндинг для Python'а -- cda_f_python.c.

    08.09.2009: во-первых, "ядро" давно уже именуется cda_api.c, а не "cda_core". 31.08.2014: уже cda_core.c.

    Во-вторых, менеджер плагинов всё-таки живёт в отдельном модуле cda_plugmgr.[ch].

  • 23.06.2007: имеется некоторый технический вопрос -- мы же хотели ссылку на сервер указывать с опциональным префиксом "протокол/", так? А раз defserver и т.п. -- то и просто ссылки на каналы также должны мочь содержать такой префикс. А значит -- и каналы в формулах. Но в формулах-то символ '/' занимается делением!!!

    Ну и как эту проблему решать будем? Выпендриваться с точками -- типа "..ПРОТОКОЛ.СЕРВЕР"? (одиночная "." начинает "абсолютную" ссылку; в 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]=='.' => eref:=defserver+ref
    • ref=~/[a-z]+:/i => eref:=ref -- т.е., в ссылке идет некоторое количество alphanumeric, сразу за которыми -- ':'; тогда это абсолютная ссылка.
    • иначе eref:=curbase+ref -- это просто обычная относительная ссылка.

    На будущее же можно ввести какой-нибудь легкий хак -- например, если 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/программах":

    1. В Cdr все ссылки (*src) окучивать функцией src2ref() -- ей передавать cid,base,src,rw.
    2. Возможность указывать ТИП канала/ссылки префиксом "@T:", где T=[bhiqsd]; именно в Cdr, а не в cda.
    3. Формулы определять в ней же, по префиксу; вопрос только по какому: "=fla"? "#!lang"...?
    4. Ввести функцию cda_add_formula(cid, base, text, flags/rw).
    5. ncdaclient -- тоже указывать тип префиксом "@T...", для унификации?

    13.04.2014@дома: nCdr: надо еще отличать ссылки на канал от формул и от регистров (bigfile-0001.html от 09-06-2007/15-06-2007):

    • Начинается с '%' -- регистр (как ссылаться -- формулу городить? неа, ведь чтение/запись отдельно);
    • Начинается с '#' -- формула;
    • 08.04.2015: начинается с '=' -- канал ('=' удаляется). Для VCAS, где имена A/B/C/...

      А ведь когда-то хотели сделать '=' префиксом принудительно-формулы -- чтоб "=ВЫРАЖЕНИЕ" (только где это записано -- не находится)...

    • содержит только [:@._] и isalnum() -- канал, иначе формула.

    А не сделать ли прямо в cda функцию "cda_src2ref()", определяющую тип ссылки и саму выполняющую вышеописанную эвристику? 15.04.2014: вряд ли получится -- ведь префикс "@T[count]" должен парсить сам клиент...

    11.10.2018: возникла потребность (в v5rfsyn.subsys) уметь "выходить" из пространства имён сервера, чтоб переходить к безсерверным (глобальным) именам.

    • Можно, конечно, указывать "с нуля" -- с префиксом протокола, "cx::ИМЯ_КАНАЛА". Но это некрасиво.
    • Делать по аналогии с http'шным "//www.example.com" (protocol-relative URL) нельзя -- т.к. "::" работает как просто подъём парой уровней выше.
    • Зато можно использовать ".:" -- это бессмысленная комбинация: корень, а потом ещё уровнем выше.

      Причём даже мнемоничность наличествует: уровнем выше от сервера -- как раз и будет уровень серверов.

    Главная задача -- понять, КУДА эти "мозги" (отбрасывать сервер; как?) засунуть в cda_core.c: там ведь то ещё шаманство, с этими combine_name_parts() (делать-то должно, видимо, она) и kind_of_reference() (эта вообще фиг с ходу поймёшь, где/как влияет...).

    Парой часов позже: отлов "'.' вначале" делается в combine_name_parts() после комментария "From-root reference?".

    Туда же надо проверку на ".:" ставить, да?

    А имя сервера как откидывать (причём, оставляя протокол)?

  • 14.08.2007: сходу -- ввел функцию cda_ref_is_sensible(), которая проверяет на не-CDA_ERR и не-CDA_NONE.
  • 04.09.2007: немного об API -- тоже сходу.

    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: еще издавна возникал вопрос, а как же иметь бы из не-наших (т.е., "сторонних") языков типа Tcl'я удобный доступ к каналам и параметрам? Ведь каналы-то надо РЕГИСТРИРОВАТЬ, а из кусочков-кода хочется ссылаться просто по именам...

    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.04.2009: а как вообще в cda делать поддержку таких разовых, не-подписных операций, типа БД и консоли?
  • 24.04.2009: и еще бредовая мысль, хоть и в ту же степь: ведь в принципе может быть желание и НЕ одинаково работать с разными каналами -- например, указывать (по проекту еще времен Nanohana :-)) частоту опроса прямо в клиенте. А что, если в ссылках на каналы указывать опциональные префицсы типа <СПОСОБ> или [СПОСОБ], где в СПОСОБе указывать, например, частоту опроса или еще что подобное.

    24.03.2014: неа, не так -- нефиг использовать такие символы, потому что:

    1. Они законфликтуют с обычным использованием -- те же <> нужны для операций сравнения, а [] для массивов.
    2. Сложно указывать из shell'а.

    А -- использовать надо суффиксы, начинающиеся с '@', за которым идёт ключевое слово (ЧТО указываем) с опциональным значением через ':'. И таких может быть не один; т.е.,

    ИМЯ.КАНАЛА{@КЛЮЧ[:ЗНАЧЕНИЕ]}

    Например, "Magnet1.Imes@freq:2@delta:0.05" (странная спецификация, да). Выглядит, конечно, не супер-красиво, но зато всё отлично алгоритмизируется.

    • Прелесть тут в том, что символ ':' просто мирно считается частью имени канала (как и alphanumeric), и сбагривается уровню-реализатору протокола связи. И '@' аналогично.
    • И проблема "разные куски кода/группировки будут хотеть один канал с разной частотой -- как быть?" при этом решается автоматом: это будут как бы РАЗНЫЕ каналы (поскольку имена различаются).
    ...вопрос только -- не захочется ли мочь такой суффикс делать сразу ГРУППЕ каналов, начиная с какой-то точки? И не поэтому ли в первоначальной идее за 24-04-2009 значится "префицс"? Но как бы такое можно реализовать -- не вполне ясно :)

    24.03.2014: несколькими часами позже: насчёт групповых суффиксов: в принципе, можно считать, что если "@..." указывается в каком-то из компонентов defserver'а (т.е., в основном окне/контексте или в под-элементе), то всё начиная с первой '@' оттуда выкусывать (оставляя обычный голый префикс) и сохранять на будущее, добавляя как суффикс к каналам.

    Правда, делать это будет очень нетривиально:

    1. Помнить "текущие модификаторы" надо не в контексте, а в обходчике дерева, делающем Realize.
    2. "Помнить" надо хитрО: если где-то в глубине дерева некая опция переопределена, то в этом "текущем" (для той ветки и глубже) префиксе надо произвести замену, а не просто долепить к нему.
    3. Вопрос, как поступать с не-CX::-ссылками. Ведь для других протоколов доступа к каналам эти '@' могут означать нечто совсем иное, но знать это может только сам реализатор протокола, а процессинг должен выполнять Cdr'ный *Realize*().

      Видимо, надо будет выдавать из cda какой-то сервис на эту тему, зависящий и от конкретного реализатора (т.е., из оного будут вызываться "справочные" функции).

    16.01.2015: с этой "дельтой" есть некоторая засада: ведь клиент будет указывать её в ЛОГИЧЕСКИХ единицах, а на стороне сервера сравнивать надо в АППАРАТНЫХ единицах. Спрашивается -- КАК?

    Мысли в порядке появления:

    1. Первоначальная идея -- в cda_d_cx пересчитывать по известным {r,d}, а при их обновлении пере-заказывать монитор (CXC_CHGMON).

      Но это дюже неудобно: громоздко и потенциально проблемно (всякие race conditions).

    2. Лучше делать пересчёт на стороне сервера: туда приходят вещественные, а он, знаючи cpn и её список {r,d}, легко сконвертирует.
    3. Неудобство, конечно, тоже есть: придётся в cxsd_fe_cx иметь код не просто пересчёта, а приведения типов -- в точности как в cda (почему, кстати, в той и выглядело логично).
    4. Вариант -- вынести эти мозги в API cxsd_hw.
    5. ...или всё-таки в cda попробовать засунуть -- аналогично вынеся в cda_core?
  • 08.07.2009: создан новый файл -- cdaP.h. В нем будет располагаться внутренний API cda -- для плагинов (формульные языки, транспорты...).
  • 03.08.2009: идея насчет возрастов:
    надо ввести соглашение, что fresh_age==0 означает "не сравнивать текущий возраст с fresh_age".

    При этом:

    1. Для каналов дурных драйверов, НЕ уставляющих fresh_age, он останется равным 0, и такие каналы просто не будут defunct'иться. Аналогично будет с local-каналами.
    2. При участии канала с fresh_age==0 в формулах он просто не будет влиять на общий defunct-статус -- поскольку CXCF_FLAG_DEFUNCT уставляется по-канально, а в формулах он OR'ится.

    19.08.2014: да, сделано именно так, работает.

  • 08.09.2009: сделал модуль cda_err.c, скопировав его с Cdr_err.c, который, в свою очередь, был передран с KnobsCore_err.c. Только длину опять сбросил до 200.

    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(), дополненной сегодня по вчерашним следам.)

  • 17.02.2012: возможно, надо бы как-то srvconn'ам мочь уставлять некий "контекст" -- кому они принадлежат. Причина -- см. bigfile-0001.html за сегодня, на тему реализации OP_LOGSTR_I.

    Или уставлять "контекст" dataref'ам?

    24.03.2014: всё становится на свои места с новой архитектурой: srvconn'ы ПОДЧИНЕНЫ контексту. И dataref'ы тоже.

  • 19.12.2012: а мы точно хотим делать каналы (которые 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: вместе с cxlib'ом переводим со схемы "privptr" на "uniq, privptr1, privptr2".

    25.07.2013: сделано. Но возникла некоторая неопределённость: поскольку тут не просто "владелец-создатель ресурса, который и указывает uniq,privptr1", а еще и ПОТОМ могут донавешиваться evproc'ы, то вопрос -- а какие у тех evproc'ев должны быть uniq,privptr1? Варианты:

    1. Считать, что дополнительные evproc'ы будет навешивать только тот же владелец. Тогда им достаточно указывать лишь специфичный privptr2, а uniq,privptr1 будут браться (и передаваться!) указанные при создании "объекта" (sid'а).
    2. Дать возможность указывать ИНДИВИДУАЛЬНЫЕ для каждого evproc'а -- тогда можно будет биндиться к "чужим" sid'ам/dataref'ам, и при убиении драйверов всё корректно подчистится.

    Сейчас выбран вариант (1), поскольку а) изначально отдельная установка evproc'ев в cda сделана чисто для удобства и красоты, а реально б) пользоваться sid'ом должен бы иметь возможность ТОЛЬКО его владелец (тем более, нефиг драйверам мешать друг дружке).

    ЗЫ: cda_do_cleanup() пока нету.

    25.11.2013: и cda_do_cleanup() сделана. Правда, пока ведь cda_del_srvconn() пуста.

  • 10.10.2013: а почему у нас тип 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.

  • 24.03.2014: приступаем к созданию "cda нового поколения" -- на основе контекстов и с zeroconf-резолвингом.

    Работа идёт во временной директории lib/ncda/.

    24.03.2014: некоторые детали: там есть 4 типа объектов, реализованных на основе SLOTARRAY:

    • ref
    • hwr -- их массив живёт в srv

      06.05.2014: удалено за некместностью.

      30.07.2014: частично вёрнуто, как int.

      17.09.2014: cda_hwcnref_t.

    • srv
    • ctx

    30.03.2014@поезд: была мыслишка в cda_add_chan() передавать не char* baseref, а

    struct _cda_base_t_struct
    {
        char                      *base;
        struct _cda_base_t_struct *uplink;
    } cda_base_t;
    
    - этакий списочек на всю цепочку вложения datatree-уровней. Но это очень НЕудобно, когда spec начинается с ':' - удобнее откусывать конечные компоненты СТРОКИ, поскольку каждый baseref может содержать более 1 компонента.

    Кстати, а что, если относительная-ссылка-вверх содержится в СЕРЕДИНЕ 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():

    • Туда упихнута логика распознавания типа ссылки в сочетании с типом префикса ("базы"), в основном по правилам от 2007г.
    • Работа по собственно распознаванию свалена на helper-функцию 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 из командной строки) -- более "авторитетен".

    • Уже сейчас ясно, что эту логику надо будет как-то опубликовать, чтоб ею мог пользоваться Cdr при работе с refbase'ами вложенных элементов.

    09.04.2014: пара замечаний:

    • Нынешний API cda_add_chan() и cda_getrefvnr() вообще никак не учитывает существование ПАРАМЕТРОВ ручек.
    • А cda_add_chan() был бы полезен параметр "флаги" -- чтоб указывать CDA_F_IS_W.
    • (15.04.2014) И никак не отражена идея базовых/настроечных каналов.

    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(). Для ссылок, прямо маппирующихся на регистры.

      Семантика должна быть такой:

      1. Ищет dataref-регистр, ссылающийся на регистр с таким именем. Если находит -- то это и возвращает (refcount++?).
      2. Ищет переменную-регистр с таким именем, если нет -- добавляет.
      3. И потом записывает в refinfo номер найденного регистра.

      Также пока пустой.

      16.04.2014: сделана, как описано.

    • Для различения видов dataref'ов поле in_use теперь считается селектором, с возможными значениями REF_TYPE_nnn (UNS=0).
    • Добавлена концепция "dataref'ов evproc" -- ключевое слово ref_cbrec, реализация скопирована с контекстовых ctx_cbrec.
    • Касательно архитектуры работы с формулами -- движение в правильном направлении:
      1. К cda_add_formula() добавлены params,num_params -- для передачи параметров ручки.
      2. Потенциально на смену нынешней странной cda_getrefvnr() ДВЕ функции для разных аспектов:
        1. 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'ных рЕкордов, технически как бы тоже "выполняющих процессинг", но должных реализовывать чтение и запись в железо.)

        2. 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: дальше:

    • Добавлен "менеджмент" регистров/переменных.
      • Slotarray'и per-context, именуются varparm, тип элемента прямо CxKnobParam_t.
      • Для создания введён вызов cda_add_varparm().
    • cda_add_varchan() наполнен содержимым, по вчерашнему проекту.

      Для хранения номера регистра используется поле hwr.

    17.04.2014: еще:

    • Добавлено использование defpfx'а.

      Ох и дико/муторно/некрасиво же вся эта логика выглядит...

      Замечания:

      1. Надо попереименовывать переменные для читабельности. 21.04.2014: сделано.
      2. Надо опубликовывать эти мозги для пользы Cdr'а. 22.04.2014: сделано.
      3. Надо вводить использование context'а в kind_of_reference(), чтоб хоть теоретически корректно поддерживались не-CX-протоколы.

    22.04.2014: и еще:

    • "Мозги" комбинирования defpfx,base,spec в цельное имя канала вынесены в отдельную combine_name_parts(), ...
    • ...а внешний интерфейс к ней опубликован в cda_combine_base_and_spec(), и CdrRealizeKnobs() этим уже пользуется.

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

      31.05.2016: только проверки на base==NULL и spec==NULL отсутствовали, из-за чего pzframeclient SIGSEGV'ился. Добавлены -- ="".

    • Единственный недостаток -- очень уж медленно. 24.04.2014: причина найдена и исправлена.

    23.04.2014: для дальнейшей работы с каналами скопирована из старой cda инфраструктура cda_plugmgr и скелеты плагинов cda_d_cx и cda_d_local.

    24.04.2014: найдена причина тормозной работы cda_add_chan()'а:

    • По ошибке были перепутаны параметры ref'ова GENERIC_SLOTARRAY_DEFINE_GROWING(), так что в качестве ALLOC_INC использовался MAX_COUNT, равный 1000000.

      Обнаружилось случайно -- при взгляде на top "а чё эта программа жрёт 73 метра ОЗУ?!".

    • Соответственно, бедняжка при каждом ForeachRefSlot() для поиска "не зарегистрирован ли уже такой канал?" перебирала по миллиону элементов.
    • Странным является то, что даже с помощью профилировщика ошибку толком обнаружить не удалось:
      • Тормозило не только на Viper/PIII-1GHz, но и на Ring1/C2D-3GHz и Ring3/i7-2600K-3.4GHz; хотя и меньше, но всё равно заметно.
      • Вроде бы показывалось, что каждый вызов ForEach() жрёт по 40+ms (это должно было натолкнуть на мысли ;-)).

        И так для каждого из 68 каналов, что в сумме давало время запуска более 4 секунд.

      • Но: при смене содержимого checker()'а с strcasecmp() на что-нибудь более тривиальное -- например, тупое сравнение *s1==*s2, или сразу возврат 0 -- всё резко ускорялось, что наводило на заключение "что-то не так со сравнением строк". Даже замена на просто strcmp() (мало ли -- с локалью сложности) или вообще самописный аналог (исходник подсмотрен в fileutils) ситуацию не исправило. Даже просто вызов strlen() (и даже самописного её варианта) всё равно вызывало тормоза, а отсутствие вызова -- резкое ускорение.
      • ВИДИМО, ключ в размерах вовлекаемой в дело памяти: при тупом сравнении всё влазило в кэш, а при реальной адресации к строкам влазить переставало, и в результате резкое замедление.

    Проблема успешно решена, и теперь в сухом остатке следующие выводы:

    • Даже на больших количествах SLOTARRAY'ный механизм поиска работает приемлемо -- визуально слабозаметно даже при 10000, и лишь со 100000 начинает быть заметно (хотя это, конечно, тупой перебор не-in_use-ячеек, без сравнения).
    • Скорее раньше перестанет хватать производительности на РЕАЛЬНУЮ работу с каналами (заказ серверу, дешифровка, отрисовка).
    • Если же именно SLOTARRAY'ная часть стала бы узким горлышком, то вполне ясны пути решения:
      1. Организация пары индексных списков свободных и занятых элементов, как таймауты в нынешнем cxscheduler'е (frs/lst/avl).
      2. Использование каких-нибудь индексов.

    06.05.2014: на данный момент сделана уже изрядная часть API cda<->dat_p. И регистрация каналов, и создание серверов (делается косвенно, из каналосоздания), и обновление значений (пока только в направлении dat_p->cda). Реализовывалось оно всё в партнёрстве с cda_d_local.

    Из результатов на текущий момент:

    1. Стало ясно, что незачем держать в самой cda массив hwr, да и сама концепция ТАМ лишняя: этим должны заниматься конкретные dat-плагины. Что в cda_d_local и сделано, а из cda_api оно выкинуто.
    2. Сейчас НИКАК -- даже с учётом будущего 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().
    • ...первый результат -- первая формула запущена и выполнила PRINT_STR!

    30.07.2014: еще прогресс:

    • Частично вёрнуто понятие "hwr": это число, указываемое конкретным dat-плагином для каждого ref'а через cda_dat_p_set_hwr() (и сохраняемое в ri->hwr) в качестве внутреннего идентификатора канала внутри sid'а -- чтоб сервер указывал его плагину при записи в канал, а не заставлял бы искать по ref'у.
    • Также введена cda_dat_p_set_phys_rds() для указания набора пар {r,d}.
    • Добавлен dat_p-метод "запись в канал" -- 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() тоже добавлена конверсия, причём:
      1. 3 варианта:
        1. "Несовместимо" -- как раньше, тупо делается bzero().
        2. "Тривиальная" -- когда типы совпадают и либо {r,d} отсутствуют, либо тип нечисловой.
        3. "Полная":
          1. Из исходных единиц (переданных в dtypes[]) в double.
          2. Преобразование по {r,d}.
          3. Получившийся double в единицы dataref'а (ri->dtype).
      2. Также делается учёт USGN-типов, причём на обоих шагах (1 и 3).
      3. {r,d}-конверсия применяется также к ВЕКТОРНЫМ числовым каналам.

      04.09.2014: старый (за-#if-0'енный) код без поддержки конверсии выкинут.

    • Добавлена поддержка типов INT64/UINT64.
    • А вот в cda_process_ref() отдельные проверки на USGN не добавлены. Предполагается, что это преобразование "однонаправленное" -- ВЕЩЕСТВЕННОЕ->ЦЕЛОЧИСЛЕННОЕ, и тут о тонкостях знаковости/беззнаковости позаботится формат "в дополнительном коде", так что потерь либо не произойдёт, либо они неизбежны традиционно для такого кодирования.

    Итого -- пашет!!! Приходящие от v2 значения корректно пересчитываются (проверялось на linthermcan), а при указании dataref'ам INT-dtype'ов еще и округляются.

    01.08.2014: далее -- сопутствующее:

    • cda_dat_p_update_server_cycle() наполнена содержимым. Для поддержки концепции "conns_u"...
    • ...сделано поддержание массива "номеров серверов" -- NthSid/sids_list. Плюс для простоты использования (в пунктом выше) добавлено 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: да, сделано:

    1. Введён тип cx_common_types.h::CxAnyVal_t, являющийся union'ом из всех возможных типов, включая раздельно целочисленные знаковые и беззнаковые. 25.08.2016: переехал в cx.h.

      И "сырые значения" теперь этого типа, вместо былого cx_ival_t.

    2. Теперь вместо "rv_useful" повсеместно используется raw_dtype, при неопределённости равная CXDTYPE_UNKNOWN.
    3. Все "rv" изведены в пользу "raw".
    4. Формулы вместо v2'шного счётчика используют дополнительный флажок "raw_count", с возможными значениями NONE=0,SNGL=1,MANY=2 (не было, ровно один раз, больше одного), и отдают не-UNKNOWN только при SNGL.
    5. Собственно сохранение сырого в cda_dat_p_update_dataset() сократилось до тривиального копирования, с единственным условием на nels==1.
    6. Отображение же в 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() переведена на двойной цикл:

    1. Сначала обрабатываются все вёрнутые данные.

      (И теперь при обнаружении ошибок вместо return делается переход к следующему каналу.)

    2. Затем уже вызываются зарегистрированные evproc'ы.

    Смысл -- чтобы при получении пачки данных из evproc'а любого из хатронутых каналов иметь свежие значения их ВСЕХ, а не греть голову, ставя обработку в "вероятно-последний".

    17.09.2014: окончательно вёрнуто/унифицировано понятие "hwr" в виде явного типа cda_hwcnref_t -- теперь он используется везде вместо былого int и локальных определений в каждом cda_d_*.c.

  • 09.06.2014: а не реализовать ли cda_d_cxv2.c?

    Потенциальные бонусы:

    1. Возможность использовать 4cx'ную инфраструктуру клиентов вместо старого Cdr+Knobs+*_db.so.
    2. Возможность обкатать клиентскую часть на работающем сервере с железом (пока нет работающего нативного v4'шного сервера и транспорта).

    09.06.2014: идея эта приходила в голову очень давно, но контраргумент -- "а как адресоваться, если в v2 всё по номерам, а в v4 по именам?". Ну так и указывать вместо имён прямо номера -- уж m4 обеспечит арифметику (только исходные константы придётся как-то подготовить).

    • Реализацию cxlib'а можно будет тупо скопировать из v2 -- среда исполнения совместима, только попереименовывать там всё, для неконфликтования имён при линковке.

      Например, добавлением префикса cxv2_ -- что можно реализовать даже при помощи #define, для минимизации изменений (коие поместить в отдельный файл v2_adapter.h).

    • Тип данных будет поддерживаться, естественно, только CXDTYPE_INT32.

      Естественно, большие каналы будут никак не задействованы. 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'а:

    • Пока прямо в lib/cda/.
    • И прямо из родных v2'шных исходников.
    • Основная работа делается в Makefile -- куча симлинков, плюс генеримые файлы с именами вида v2cxlib_lib_NNN.c, #include'ащие оригинальные v2'шные файлы после того, как за-#include'ят...
    • ...v2cxlib_renames.h -- единственный реальный файл (выше он значился как "v2_adapter.h"), содержащий кучу #define'ов для переименования.

    30.06.2014: да, скелет cda_d_v2cx.c сделан.

    Дальнейшее обсуждение -- в его разделе.

    05.08.2014: он доведён до весьма функционального состояния, и бОльшая часть остального проверена на нём, так что тут уж точно "done".

  • 09.06.2014: также полезно будет изготовить cda_d_vcas.c:
    1. Для юзабельности CXv4 на ВЭПП-2000.
    2. Для обкатки клиентской части на работающем сервере с адресацией по именам.

    Благо, там всё текстовое, и документация есть -- http://vepp2k.inp.nsk.su/trac/wiki/CAS.

    09.06.2014: только одна тонкость -- там в иерархии имён разделители '/'. Но модуль может сам заменять '.' на '/'.

    24.06.2014: скелет сделан. Пока совсем без содержимого ("мозгов").

    Дальнейшее обсуждение -- в его разделе.

  • 06.07.2014: надо в cda иметь API для ОСТАНОВКИ исполняющихся формул.

    И отдельный 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, пользующийся этим интерфейсом -- всё работает.

  • 19.09.2014: кстати, у нас есть метод snd_data, но нет никакого способа заказать ЧТЕНИЕ.

    Для v2cx это пока не особо нужно, а вот для inserver будет необходимо -- там иначе никак.

    Как поступим? Есть 2 варианта:

    1. Добавить метод req_data -- для inserver'а хватит.
    2. Сделать общий метод 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".

  • 11.01.2015: удалены никогда не наполнявшиеся cda_run_context() и cda_hlt_context() -- это были рудименты от без-контекстной архитектуры v2, где сии операции теоретически могли иметь смысл применительно к "серверам" (соединениям).

    В v4 покамест в них надобности не видно вовсе (только когда-нибудь для какой-нибудь экзотики вроде "временно приостанавливать часть В/В при узких каналах связи", описанной в bigfile-0001.html за 31-10-2006).

  • 10.04.2015: что-то не совсем слава богу в 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'ы в виде ":N.name...".

      Так вот: а что если ОТДЕЛЬНО считать такие ссылки, при указании их в качестве defpfx, за аналог "localhost:N..."? Т.е., если после двоеточия идут только цифры (с опциональным '-' впереди).

      Вопрос только -- ГДЕ это надо считать (учитывать): в cda_core, cda_d_cx, pult?

      Часом позже: а вот и нет ни проблемы, ни невозможности -- pult прекрасно сжёвывает как ":2.icd", так и даже ":-8014.icd".

    • Пример вызывавшего проблемы кода (в старом cac208.subsys) выглядел так:
              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". Там точно никаких косяков не кроется?
  • 14.04.2015@Снежинск-Икарус-на-полигон: при реализованной в v4'шной cda архитектуре у fastadc НЕ будет доступа к сырым данным -- поскольку пересчёт по {r,d} ведётся прямо в cda при получении. Мысли:
    1. Сделать пересчёт по {r,d} отключабельным, on per-dataref basis.
    2. А не дать ли, в дополнение к cda_get_ref_data(), возможность спросить у cda указатель прямо на ЕЁ буфер -- этакий рудиментарный zero-copy -- чтоб программа уже сама решала, что ей делать?

      Естественно, при этом постулируется, что указатель валиден только до следующего входа в 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-ю: в продолжение:

    • Неа, per-dataref нельзя: один канал может регистрироваться разными частями (плагинами) софтины в разных целях, и другим он нужен "традиционно".

      Так что надо тогда вводить флаг (cda_add_chan().flags) "CDA_DATAREF_FLAG_PRIVATE", означающий, что НЕ следует искать такой канал в списке уже зарегистрированных, а надо принудительно аллокировать слот (и, соответственно, при поиске игнорировать слоты с уставленным этим флагом).

    • А насчёт сырых данных: если захочется иметь и их, и пересчитанные -- то можно добавить вызов (возможно, имеющий смысл только для PRIVATE-ссылок) "аллокируй буфер такого-то объёма для сырых данных").

    20.04.2015: да, вот сегодня в драйверы ADC200ME и ADC812ME вставлено SetChanRDs() на линии данных с параметрами {1000000.0,0.0}. В результате при указании не-вещественного dtype у клиента будет дикое округление, и данные бессмысленны. А с отключением {r,d}-пересчёта -- было б всё вполне окей.

    21.04.2015: делаем:

    1. Для начала, меняем название параметра у cda_new_context(), cda_add_chan(), cda_add_formula() с flags на options -- в основном в cda.h, а в cda_core.c оно частично уже было сделано когда-то раньше.
    2. Вводим новый enum -- CDA_DATAREF_OPT_*, в котором сейчас есть:
      1. CDA_DATAREF_OPT_PRIVATE -- 31-й бит,
      2. CDA_DATAREF_OPT_NO_RD_CONV -- 30-й бит.

      Т.е., эти флаги идут "сверху" чтоб никогда не перекрылось с -- CDA_OPT_* (формально они о разном, но мало ли что в будущем -- захочется, да и лишний запас прочности не помешает).

    3. Добавлено поле refinfo_t.options, куда сохраняется переданный cda_add_chan() параметр...
    4. Собственно "мясо":
      • Реализация PRIVATE: сделано, что
        1. При выставленном CDA_DATAREF_OPT_PRIVATE поиск одноимённого канала не производится.
        2. При поиске игнорируются слоты с выставленным CDA_DATAREF_OPT_PRIVATE.
      • Реализация NO_RD_CONV: добавлена проверка CDA_DATAREF_OPT_NO_RD_CONV и не-конверсия в
        1. cda_dat_p_update_dataset() -- в паре точек, в прямом копировании memcpy() и в конверсии. Второй вариант при этом получается не самым оптимальным -- т.к. данные всегда прогоняются через double, даже есть оба REPR_INT; но на это проще забить (можно сделать еще одну ветку, но пока глубокого смысла не видно).
        2. cda_process_ref() -- всё тривиально.
        3. А вот в cda_snd_ref_data() -- НЕТ, поскольку там пока просто недореализована отправка "произвольных" данных, вместе с конверсией.
    5. За компанию поле refinfo_t.nelems было переименовано в max_nelems, а cda_nelems_of_ref() в cda_max_nelems_of_ref() -- всё для устранения неоднозначности и полной непутающести с current_nelems.

    Теперь использование этого в клиентах.

    • Первоначальная работа с cdaclient.c:
      • Добавлена возможность указывать символы опций после '@' -- вроде "@-T...". И нет, указание типа тут не становится обязательным -- можно написать просто "@-:", оно это отработает корректно, только выставит флаг, а тип требовать не станет.

        Следствие: теперь можно писать даже просто "@:", с пустотой внутри -- отработается корректно.

      • Включение флага "PRIVATE" делается символом-опцией '-'.
      • Аналогично флаг "NO_RD_CONV" включается символом-опцией '.'.
    • Ну и оно всё добавлено также в
      • das-experiment.c -- у него вообще потроха скопированы из cdaclient.
      • Cdr_treeproc.c::cvt2ref(). Сюда, конечно, немного изврат, но пусть будет, для общности и гибкости: оно позволяет отдельные ручки делать "приватными" (вопрос -- зачем ;)).

        Имевшийся там фрагмент

        rw? CDA_OPT_IS_W : CDA_OPT_NONE
        убран -- смысл его не вполне ясен, а работать оно и не могло, т.к. cda_add_chan(.options) раньше и не использовалось.

    26.04.2015: проверяем:

    • OPT_PRIVATE -- работает
    • OPT_NO_RD_CONV -- работает, отдаётся сырое значение.

      И да, без PRIVATE оно портит жизнь другим экземплярам этого канала.

    • В Cdr тоже функционирует как надо.
    • НЕТУ только в формулах -- cda_f_fla.c. Но там если добавлять, то лишь флаговые префиксы -- "@-". и "@.".

    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() (оно просто отсутствовало).

  • 22.04.2015: у нас изначально при cda_add_chan()'е поиск "зарегистрирован ли уже такой" проверял только совпадение по имени. А это весьма некорректно -- ведь клиент может заказать то же имя с другим dtype (а то и max_nelems), и неправильно ему давать ссылку на тот же канал с отличающимися характеристиками -- там даже комментарий был,
    /*!!! Check {dtype,max_nelems} compatibility? */

    Поэтому дополняем также сравнением полей detype и max_nelems.

    26.04.2015: да, проверено, работает -- при различии ссылки отдаются разные.

  • 01.05.2015@пляж: а ведь мы можем ловить событие "команда записи была отправлена и подтверждена": в протоколе CXv4 приходит chunk (пустой), в v2 просто следующий update, в vcas что-нибудь придумаем, ...

    Тогда в консольных утилитах вместо догадок ("если пришло очередное уведомление -- значит, запись отработалась") можно было бы реагировать на настоящее ПОДТВЕРЖДЕНИЕ записи.

    02.05.2015: а вот нифига -- код CXC_RQWRC, не вызывает ответа, просто приходит ответный пакет CXT4_DATA_IO, БЕЗ param1/param2.

    ...с другой стороны, в текущей архитектуре есть свой плюс: коль на канал записи придёт событие R_UPDATE, значит какая-то запись реально произошла (именно отработалась, а не просто была отправлена далее).

    Посему -- "withdrawn".

    27.05.2015: но у технологии "дождаться update, отправить запись, еще раз дождаться update" (используемой в cdaclient) есть своя кривизна: два подряд падения сервера (идеально -- в момент коннекта/резолвинга), и запись считается выполненной!

  • 01.05.2015@пляж: надо б уметь получать статус сервера, к которому принадлежит канал -- установлено ли сей момент соединение. Чтобы понимать, можно ли исполнять какую-то автоматику, в которую вовлечён данный канал.

    07.05.2015: сделана, cda_status_of_ref_sid(). Не проверена, но там всё на вид просто.

  • 02.05.2015@утро-зарядка: к вопросу "надо в cda_snd_ref_data() поддерживать отправку произвольных данныхх"...

    Вчера-позавчера гуляли в голове мысли "придётся портить переданные сверху данные, а то где ж еще мы возьмём буфер достаточно большого размера, чтоб в него влез произвольный объём данных". А сейчас стало очевидно --

    1. Надо в refinfo_t иметь "send buffer", который растить при надобности, и уж в него складывать данные.
    2. Для небольших значений -- вроде скаляров -- иметь immediate-поле вроде "val2snd", которое использовать, если запрошенное туда влазит.

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

    3. Дополнительный бонус: так можно будет реализовать "ПРАВИЛЬНЫЙ" подход к отправке данных -- чтоб они накапливались у cda даже при неустановленности соединения с сервером, а когда установится, то будут отправлены.

    02.05.2015: некоторое обсуждение:

    1. О технологии исполнения cda_snd_ref_data():
      • Для не-готовых соединений (или не-найденных каналов) надо только складировать данные в буфер, взводить флаг "modified"/snd_rqd, а возвращать CDA_PROCESS_FLAG_BUSY; для реально же отправленного -- CDA_PROCESS_DONE.
      • Следствие, для корректности: надо б уметь диагностировать ошибку отправки (мало ли -- write() обломился из-за отвала соединения) прямо в отправляторе. Чтобы:
        1. НЕ сбрасывать флаг "есть данные для отправки, пока не отправлены".
        2. Не дезинформировать юзера, а отдавать ему реальный статус -- что не отправлено.

        Для чего 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() нужно всё делать в точности по тем же правилам.
    2. О персистентности:
      • При установлении соединения -- т.е., при переходе в состоение CDA_DAT_P_OPERATING -- надо вызывать snd() для всех готовых каналов.
      • Аналогично, это же надо делать и при переходе ЯЧЕЙКИ в состояние "готова" -- в cda_dat_p_set_hwr().
      • НО! Желательно мочь часть каналов делать не "persistent", а "shy" -- например, командные каналы ни в коем случае не должны дрыгаться "через час после записи".

        И как -- введём еще один флаг -- CDA_DATAREF_OPT_SHY? И чтоб он ставился таким потенциально-опасным каналам.

        И вот ЭТОТ флаг, видимо, должен быть кумулятивным/"tainting": при регистрации очередного канала, даже если такой канал уже есть, этот флаг надо принудительно взводить в ri->options.

    03.05.2015: да, делаем:

    1. Тип 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?

      • Ну и отдельный аспект -- взаимодействие с "подстилающими" функциями. Конкретно в cda_d_local.c и cda_d_dircn.c: там WriteToVar() могут возвращать ошибку (как раз касательно совместимости типов). Поэтому:
        1. При результате WriteToVar() !=0 возвращается CDA_PROCESS_ERR.
        2. Сами WriteToVar() теперь возвращают то, что им вернут реализаторы do_write() -- там протокол "0:OK, <0:Error" (do_write==NULL -- возвращается 0 ("ушло в никуда", но ушло "успешно")).
    2. Буферизация для отправки:
      • В refinfo_t добавлена толпа полей snd*, плюс imm_val2snd.
      • Складирование туда данных -- StoreData4Snd(), вызов отправки -- CallSndData().

    04.05.2015@из-дома:

    1. Отличение "серьёзных проблем" -- введена CDA_PROCESS_SEVERE_ERR=-2, должная использоваться только между cda_core и cda_d_*, а наружу не светиться.

      И во всех cda_d_*.c при "серьёзных" проблемах (т.е., ЛОГИЧЕСКИХ (вроде неподходящего dtype), а не обрыве соединения) возвращается этот код.

    2. Введён CDA_DATAREF_OPT_SHY для маркировки не-персистентных каналов.

      Он "кумулятивный" -- принудительно проставляется (|=) уже зарегистрированным каналам, если указан в регистрируемом позже. 23.05.2017: а вот это неправильно! Надо не "принудительно проставлять", а считать каналы с/без РАЗНЫМИ. Сделано, см. ниже за сегодня.

      Для указания в @-флагах ссылок выбран символ '/'.

    05.05.2015: продолжаем:

    1. Буферизация при отправке: добавлена SendOrStore(), комбинирующая Store и CallSnd и реализующая алгоритм "если можно -- то отправим прямо сейчас (по возможности напрямую), иначе -- складируем в буфер и взведём флаг".
      • Ей, кроме данных (data/nelems/dtype) передаётся также do_conv -- надо ли выполнять {r,d}-конверсию.
        1. cda_snd_ref_data() указывает 1.
        2. cda_process_ref(), делающая конверсию сама при преобразовании типа, указывает 0.
      • Если выполняются сразу все условия:
        1. СЕЙЧАС в буфере для отправки ничего нет (snd_rqd==0);
        2. канал готов к работе (выставлены sid и hwr и соединение в состоянии OPERATING);
        3. конверсия не требуется (по любой из причин -- do_conv==0, или phys_count==0, или выставлен OPT_NO_RD_CONV, или представление не REPR_INT и не REPR_FLOAT)
        то делается попытка отправить напрямую переданные данные.

        И если успешно, то возвращается 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():
        • Наращивает буфер, при надобности.
          1. Если влезает в imm_val2snd, то ограничивается им.
          2. ...но если буфер уже аллокирован, то ВСЕГДА imm_val2snd не используется в любом случае (даже если б в него и влезло).
        • Складывает в буфер, опционально выполняя {r,d}-конверсию.
        • Взводит snd_rqd.
      • CallSndData(): вызывает отправку, и если результат DONE либо SEVERE_ERR (т.е., логическая ошибка, и данные в буфере всё равно никогда отправиться не смогут), то сбрасывает snd_rqd.

      Замечание: отсутствующий метод snd_data() считается за успех (CDA_PROCESS_DONE).

    2. Отправка буферизованных данных "при появлении возможности":
      1. cda_dat_p_set_server_state() при переходе в CDA_DAT_P_OPERATING проходится по всем каналам и вызывает отправку ждущих.

        Тут есть неоптимальность, выражающаяся в том, что проход идёт по ВСЕМ каналам ВСЕХ контекстов/sid'ов. Пока на это забиваем, но если станет проблемой -- то можно держать все ref'ы каждого sid'а в своём списке (перемещая при надобности между серверами и "ничем"), как это сделано в cda_d_cx.c.

        Замечание: итератор прерывается (возвратом -1), если после какой-то отправки состояние сервера перестанет быть OPERATING.

      2. cda_dat_p_set_hwr() при переходе канала в состояние готовности (и сервер чтоб был OPERATING) при взведённом snd_rqd тоже дергает CallSndData().

    06.05.2015: начинаем проверять:

    1. Конверсия работает.
    2. Буферизация и отправка по готовности НЕ работает.

    Роем...

    • Разборки показали, что причина в недорезолвленности каналов: CallSnd дёргается прямо при переходе соединения в состояние OPERATING, но ведь резолвинг-то еще не произошел и (конкретно у cda_d_cx) rslv_state!=RSLV_STATE_DONE, в результате чего запрос на запись молча игнорируется!
    • Очевидно, что надо заводить отдельный PER-CHANNEL флаг "готов", который
      1. Учитывать как дополнительное условие в возможности отправки данных в канал.
      2. Изначально иметь его сброшенным, и dat-плагины должны его взводить явно (у простых/локальных -- прямо после set_hwr()'а).
      3. Также сбрасывать по cda_dat_p_set_notfound().
      4. Взводить по отдельному, "зеркальному" к notfound вызову -- назовём его cda_dat_p_set_resolved(). Из которого, кстати, дрыгать CallSndData().
    • Сделано по вышеприведённому проекту, флаг назван is_found;

      Результат -- тоже мрак. Например, при обрыве соединения все каналы скопом становятся чёрными, ибо NOTFOUND.

      Вывод: нефиг махинировать, смешивая "найденность" с "готовностью".

      Надо иметь ОТДЕЛЬНЫЙ флаг именно ГОТОВНОСТИ, который dat-плагины могли бы выставлять и сбрасывать.

    • Так и сделано, несложной модификацией забракованного проекта, за 10 минут. Флаг переименован в is_ready.

      Вот теперь всё работает как надо.

    • Ну и поддержка "SHY": в отличие от v2 (где складировалось всегда (из-за цикличности), а сбрасывалось в ConnectProc()'е), тут результат отправки (удалось/обломилось) известен сразу, поэтому надо просто сразу сбрасывать snd_rqd при обломе отправки.

      Так и сделано. Работает.

      Не очень красиво, правда -- аж 2 одинаковые проверки, в разных ветвях; но SendOrStore() вообще выглядит монструозновато и напрашивается на украсивливание.

    07.05.2015: засим считаем раздел за "done".

  • 02.08.2015: (родственно предыдущему пункту) есть один идеологический косяк: если запись делается в канал, на который ЕЩЁ НЕ ПРИШЛИ данные об {r,d} (например, соединение с сервером еще не установлено), то для записи будет сохранено неправильное значение -- "логическое" НЕ будет сконвертировано в инженерное.

    Конкретно cdaclient такой проблемы не вызовет -- в нём отправка производится после первого обновления (что автоматом защищает и от записи в каналы, принадлежащие установленному соединению, где сервер еще не приконнектился к удалённому драйверу).

    Но вот "иные" программы такое могут сделать, да и из GUI тоже можно устроить.

    То ли надо как-то таким "fresh"-каналам (а как их определять?) делать принудительный временный "SHY", то ли -- лучше! -- значения складировать и производить конверсию уже в момент отправки.

    ...или вообще ВСЕГДА конверсию выполнять в момент отправки?

    27.10.2015: да, прямо сегодня этот косяк вылез: программа АстреКсюши, работающая через питонский binding by ЕманоФедя, попыталась выполнить запись сразу после создания канала -- когда он еще не был приконнекчен.

    В результате выглядело как запись в raw-value.

    13.08.2017: давно пора этот косяк исправить -- и Федя ноет, и вообще это кривизна, да и не так уж это сложно.

    Еще с несколько месяцев назад было очевидно, как именно, но почему-то не записано (хотя казалось, что писал...).

    Собственно "идея":

    • Перво-наперво -- нужно помнить, были ли получены {r,d}. Для чего иметь флажок refinfo_t.rds_rcvd. Пара замечаний:
      1. При обрыве соединения его сбрасывать (хотя первоначальная идея была "один раз получили -- и ладно, считаем, что потом оно не меняется").
      2. Поскольку иногда {r,d} могут и отсутствовать, то при получении ДАННЫХ флаг тоже можно взводить.

        Но, видимо, за "получение данных" надо считать именно ПОЛУЧЕНИЕ, т.е., чтобы НЕ 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, помечая "конверсия еще не делалась и её НАДО сделать".

      Некоторые детали:

      • Саму конверсию, похоже, надо вытаскивать в отдельную static-helper-функцию. А то нефиг плодить тонны дублей.
      • В начале 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 частей:

    1. Определение "можно/нельзя" (получены ли уже {R,D} или нет).
    2. Буферизация и отправка.

    Первая часть следит за готовностью -- поддерживает состояние флажка (в т.ч. сбрасывает его по обрыву соединения) и уведомляет вторую часть о приходе в готовность.

    Вторая же складирует в буфер (следя за применением/неприменением калибровок) и выполняет отправку по уведомлению о готовности от первой.

    14.01.2018@дома: все рассуждения выше вроде игнорируют сценарий, когда клиент подключается МЕЖДУ стартом сервера и запуском драйверов (задающих свои калибровки -- в частности, все ЦАПы). Например:

    1. Запускается сервер, в котором есть remdrv-драйверы.
    2. Сами remdrv стартуют, но до реальных драйверов еще не достукиваются -- хоть из-за паузы, хоть потому, что контроллеры пока недоступны.
    3. Коннектится клиент, желающий выполнить запись -- для определённости пусть будет cdaclient.
    4. Вроде бы получает значение, и считает это разрешением на запись.
    5. Пишет, с r={} (т.е., r=1).
    6. Значение забуферизовывается в cxsd_hw...
    7. Контроллер становится доступен, запускается реальный драйвер.
    8. ...драйверу передаётся значение "как есть", БЕЗ масштабирования на 1000000.

    15.01.2018: анализируем и проверяем.

    • Скорее всего, такой сценарий в случае с cdaclient'ом НЕ осуществится. Т.к.
      • В егойном ProcessDatarefEvent() "разрешением на запись" считается именно CDA_REF_R_UPDATE (а не CURVAL -- тот только при -dC).
      • А выбор reason-кода -- UPDATE/CURVAL -- определяется кодом в пакете от сервера -- NEWVAL/CURVAL -- который определяется единолично cxsd_fe_cx.c::ServeIORequest() в ветке по CXC_PEEK. И, похоже, там ЕСТЬ проверка на тему "НЕ считать каналы с timestamp.sec==CX_TIME_SEC_NEVER_READ обновившимися даже если они rw".
    • Но стоит проверить точно, для чего сделаем тестовый драйверочек, работающий на libremdrvlet и выставляющий калибровку 1000000.
      • Драйвер разместим прямо в 4cx/src/programs/drivers/ и назовём test_rem_rd_rw_drv.c.

        ...после обеда: сделан. Хотя сборка сделана в Makefile через одно место.

      • Пытаемся проверять (на локальном rrund): что-то глючит. Опять первый запрос печатается как «request to run ""» (это явно какая-то ошибка именно диагностики 16.01.2018: разобрался -- да, печаталась path[], которая в этот момент еще не заполнена; детали см. в разделе VME за сегодня.), а главное -- оно почему-то тут же само рвёт соединение. SIGSEGV'ится?

    16.01.2018: продолжаем:

    • ...
      • Да, был дурной косяк -- в _rw_p() было просто privrec_t *me, вместо privrec_t *me=devptr.
      • Проверено то, ради чего драйвер и создавался -- да, работает "как надо", дожидаясь реального обновления.

        Единственная тонкость:

        • Если сервер не "только что запустился", а драйвер раньше был активен и отдал данные, но сейчас в отключке, то cdaclient'ом (с подачи cxsd_fe_cx) канал считается "готовым" -- т.к. у него timestamp.sec!=CX_TIME_SEC_NEVER_READ.
        • Соответственно, если после рестарта коэффициенты вдруг изменятся, то запись произойдёт некорректная.

          Но тут возможна иная (хотя и более труднодостижимая) ситуация: драйвер рестартует ПОСЛЕ того, как клиент отправил запрос на запись

    • Какой мы из этого всего делаем вывод? А такой: "первую часть" работы можно (для начала) сделать очень простой: флаг rds_rcvd взводится просто при получении значения с is_update!=0, и никогда не сбрасывается.

      Ну и в случае наличествующей опции CDA_DATAREF_OPT_NO_WR_WAIT -- тоже просто сразу взводится.

    Делаем!

    1. "Определение":
      • Вводим флажок refinfo_t.rds_rcvd.
      • Опцию CDA_DATAREF_OPT_NO_WR_WAIT=1<<25.
      • Если она выставлена, то прямо при регистрации делается rds_rcvd=1.
      • В 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: далее:

    1. Буферизация и отправка:
      • Некоторые логические соображения:
        • Идеи про "вот: надо делать объём по МАКСИМАЛЬНОМУ из sizeof_cxdtype()'ов переданного и ref'ового" (от 13-08-2017) -- реально бессмысленны, т.к. складируется и отправляется серверу ровно тот тип, который и был передан.
          • Часом позже: а вот и нет!!! Конкретно StoreData4Snd() складирует именно в формате ri->dtype, а не в переданном ему dtype. И cda_process_ref() тоже конвертирует в туда же.
          • Еще чуть позже: и даже хуже (не-постояннее!) -- и SendOrStore(), и StoreData4Snd() при отсутствии необходимости в RD-конверсии вместо ri->dtype используют переданный dtype; первая прямо вызывает snd_data(), а вторая memcpy() в sndbuf.
          • 18.01.2018: причём в StoreData4Snd() есть отдельный шизоидный баг: размер dsize она считает по переданному dtype и в ri->snd_dtype пишет его же, а при RD-конверсии использует ri->dtype -- т.е.,
            1. Содержимое sndbuf[] не соответствует тэгу snd_type.
            2. В случае sizeof_cxdtype(ri->dtype) > sizeof_cxdtype(dtype) будет buffer overflow.

            Так что всё работало без сбоев исключительно потому, что все программы (де-факто в основном pult (т.е., Cdr), плюс фединое) какой тип регистрировали, такой и отправляли.

        • На вид надо бы вводить дополнительный флаг -- "сохранённое в буфере требует RD-конверсии".

          Но раз уже есть snd_rqd, то не перевести ли его с булевского 0/1 на enum "нет=0,да=1,да_и_конвертировать=2"?

        • (Еще чуть позже) И вообще нынешняя архитектура выглядит странноватой -- не очень хорошо сбалансированная и переусложнённая.

          Например, cda_process_ref() зачем-то САМА делает RD-конверсию, ради чего и был введён SendOrStore()'ов параметр do_conv (который только process_ref и указывает =0).

      • 18.01.2018: вывод: надо переходить на сохранение и отправку ВСЕГДА именно в переданном формате.

        Частным случаем этого будет и исправление бага в StoreData4Snd() -- там просто надо вместо ri->snd_dtype ВСЕГДА использовать переданный dtype.

        Вопрос только: где это может аукнуться? Где-нибудь произойдёт потеря точности из-за использования int-типов вместо вещественных?

    09.02.2018: возвращаемся:

    1. Буферизация и отправка -- продолжение:
      • Исправляем "шизоидный баг" в StoreData4Snd(): складируется теперь тоже на основании dtype, а не ri->dtype.

        Кстати, баг явно был следствием копирования откуда-то из более раннего места (которое сейчас, похоже, уже не сохранилось). Либо в момент введения "буферизации" -- полей snd* и собственно StoreData4Snd() 03-05-2015.

      • cda_process_ref(): там нынешний код состоит из 3 частей:
        1. RD-конверсия.
        2. Приведение типа от double к ri>dtype.
        3. Отправка.

        Анализ:

        • Формально, нынешней целью является лишь п.1 -- конверсия.
        • Но, по факту, п.2 также не шибко-то нужен: поскольку в обычной отправке -- 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 по сети, тем самым перенося конверсию с клиента в сервер. Не вылезет ли где чего?
        • 12.02.2018@лыжи: некоторые соображения -- последовательно:
          • Соображение 1: ну можно сделать 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-конверсии".
      • 10.02.2018: кстати, надо бы для типов, принципиально не поддерживающих конверсию -- т.е., с CXDTYPE_REPR_TEXT -- этот механизм не использовать; в смысле -- игнорировать rds_rcvd и отправлять данные сразу по готовности.
        • 12.02.2018: только вот незадача: КАК ИМЕННО "игнорировать"? Сбрасыванием rds_rcvd при добавлении? -- однозначно нет (а если записывают сначала TEXT, а потом туда же INT?). Или прямо в момент возможной отправки при коннекте -- в вызываемом из cda_dat_p_set_server_state()snd_rqd_checker()'е проверять, что НЕ-"требуется_конверсия"? Да, видимо, так -- оно автоматом получится "как надо", покрывая ВСЕ случаи, не требующие конверсии (включая CDA_DATAREF_OPT_NO_RD_CONV).
        • 12.02.2018: Почему в SendOrStore() нет проверки на rds_rcvd?

          Ответ: потому (16-01-2018), что "это задача второй части".

          16.02.2018: добавлена проверка.

        • 16.02.2018: По теме: да, задача решена -- путём использования введённых несколькими днями позже ri->sndflags, где флаг DO_RD_CONV при мимокассности сбрасывается, тем самым отключая отсроченное преобразование.

    13.02.2018: и-и-и...

    1. "Буферизация и отправка" -- очередное продолжение:
      • По результатам вчерашнего:
        1. Содержимое ветки REF_TYPE_CHN в cda_process_ref() оставляем тем, одним простым вызовом.
        2. А функционал RD-конверсии и преобразования типов отдаём в SendOrStore() и её подчинённых.
        3. ...для чего надо преобразовывать параметр do_conv в snd_flags (при надобности складируемый в refinfo_t), могущий сейчас содержать 2 флага: "DO_RD_CONV" и "DTYPE_CONV" (этот означает, что после RD-конверсии нужно преобразовать к ri->dtype).
      • Перевод do_conv на флаги:
        • Введены DO_RD_CONV и NTVZ_DTYPE (NTVZ -- NaTiViZe, "привести к родному"), плюс SNDFL_NONE=0.
        • Параметр do_conv заменён на snd_flags, в вызовы вместо "1" и "0" вставлены соответствующие константы.
        • Плюс, в вызов из cda_process_ref() добавлено |NTVZ_DTYPE.
        • Также добавлено поле refinfo_t.snd_flags для сохранения на будущее, выполняемого в StoreData4Snd().
      • Теперь соображения о том, как/когда делать преобразование:
        1. RD- и dtype-конверсию -- ОДНОВРЕМЕННО (в такой последовательности).
        2. После проведения этой операции нужно сбрасывать флажки "надобности" в ri->snd_flags.
        3. При несовпадающих sizeof_cxdtype()'ах используемых типов: 1) требуемый размер буфера считать по максимальному; 2) складировать "в конец".
        4. Некоторый вопрос -- КАК определять, что складировано с конца? Отдельно offset записывать (еще одно поле?) или вычислять на месте по признаку "конвертировать надо, а размеры типов разные"?

        ...неприятность в том, что -- сейчас -- и складирование, и отправка делаются в ДВУХ местах (а точно?).

      • Пока выглядит так, что "конверсию перед отправкой" нужно вставлять в CallSndData(). И, похоже, её же и вызывать по "приходу канала в готовность" -- в cda_dat_p_update_dataset() при rds_rcvd=1.
      • "Не требует конверсии" -- сбрасывать в snd_flags бит DO_RD_CONV прямо в StoreData4Snd() одновременно с memcpy().
        • 14.02.2018: сделан.
        • 15.02.2018: но дополнительно добавлена проверка к варианту "ri->phys_count == 0" -- "&& ri->rds_rcvd", иначе при просто еще не полученных калибровках считалось бы, что можно слать сразу.
        • 16.02.2018: и в SendOrStore() тоже.
      • @лыжня-~17:00: очевидный вывод -- RD- и dtype-конверсия должна быть ТОЛЬКО в CallSndData().

        В StoreData4Snd() же оно нафиг не нужно -- там должно быть просто складирование.

      • @вечер-ИЯФ-пристройка-лестница-по-дороге-домой: а вот и нет! В случае, когда прямо сейчас всё готово -- получится неоптимально: сначала memcpy(), а потом дополнительно проход с конверсиями, хотя можно было бы сразу выполнять конверсию, безо всяких memcpy().
        • Так что надо вытаскивать конверсию в отдельную функцию (которая, по смыслу, как раз и есть "store" :)), ...
        • ...которой передавать "откуда", "куда", dtype'ы источника и приёмника, плюс флаги (делать ли конверсию типа в"кудашний" -- де-факто, использовать ли dtype_куда или сразу сделать dtype_куда=dtype_откуда).
        • А она уж и рассчитывает, сколько нужно аллокировать места (по максимуму из типов -- вне зависимости от необходимости делать конверсию СЕЙЧАС).
        • ...В идеале, чтоб она также сама рассчитывала и ОТКУДА копировать, в случае, если это конверсия внутри буфера. Например, при откуда==NULL -- что будет при вызове из CallSndData().

          Тогда все мозги-арифметика на тему "по какому оффсету класть в буфер..." будут инкапсулированы в одной функции.

        • Возможно, стоит инкапсулировать также и менеджмент snd_flags -- чтоб сбрасывало флажок DO_RD_CONV (да и NTVZ_DTYPE тоже) после проведения конвертации.

          Для чего придётся сделать snd_flags in-out-параметром.

          ...или просто в ri всё делать?

        • 14.02.2018: Кстати -- а ведь никакое "куда" передавать и не нужно: будут использоваться ri'шные sndbuf и imm_val2snd.

    15.02.2018: м-м-м...

    1. "Буферизация и отправка" -- исчо:
      • Вчера была начата оная отдельная функция DoStoreWithConv(). Состоит из 2 частей:
        1. "Мозги" -- определение откуда и куда копировать. Тут же будет аллокирование/рост буфера.
        2. Собственно копирование. В случае отсутствия обоих флагов сводится к memcpy(), а иначе цикл, взятый из StoreData4Snd() -- загрузка, RD-конверсия, сохранение.
      • С помещением неконвертированных данных в конец sndbuf'а не всё так просто: надо ж ещё учитывать и выравнивание.
        • Чисто теоретически -- если просто тупо отсчитывать в минус от верхнего края "максимального размера", то результирующий адрес может получиться некратным размеру.
        • Потому принято решение: всегда добивать max_total до размера, кратного 16 ("(...+ 15) &~15U"). Ведь любой современный тип (int16/32/64/128) не просто укладывается на границу 16, но и в минус от неё (как и в плюс) тоже будет иметь правильное выравнивание.
        • Если чуток побуквоедствовать, то де-факто СЕЙЧАС проблем с выравниванием не возникнет. Т.к.:
          • "Прижимать к верхней границе" нужно только в случае, когда "конечный" тип больше "исходного". Например, DOUBLE явно объёмнее INT32, поэтому исходный INT32 надо держать у конца, а конечный DOUBLE писать в начало буфера.
          • Но все такие типы имеют "подходящую" кратность: выравнивание более крупного подойдёт и для меньшего.
          • И это не просто так, а вследствие устройства самой кодировки dtype: в ней записывается не размер в байтах, а степень двойки.
          • ...вот для сочетания "конечный:int24, исходный:int16" была бы проблема -- в случае нечётного количества, хоть прямо 1: конец буфера имел бы смещение 3, а 3-2=1, что является НЕподходящим адресом для int16.

            Ну или, для наглядности, при количестве 5: конец буфера -- 3*5=15, смещение для исходного -- 15-2*5=5; нечётное -- низзя.

        • @вечер: а можно сделать и еще проще и с меньшим количеством условий: ВСЕГДА ставить оба указателя на буфер+max_total-nnn_total.

          @вечер-дорога-домой-по-Лаврентьева: а вот и нифига!!! Ведь это "прижимать к концу" имеет смысл только в случаях, когда размер "исходных" единиц меньше, чем "конечных" (например, INT32->DOUBLE). А если наоборот (DOUBLE->INT32) -- то напротив, надо, чтобы оба были у начала.

        • @Чуть позже: а есть и вариация -- ничего не химичить с указателями на начало, но просто копировать задом наперёд. А лежало чтоб всё всегда с начала буфера.

          @вечер-дорога-домой-по-Лаврентьева: тоже нифига-нифига! Задом наперёд копировать можно тоже только если размер "исходных" единиц меньше, чем "конечных"; а если наоборот -- то нужно в обычном прямом направлении.

      • ...но это была не единственная засада -- просто что-то застрялось, как же делать.

        Сходивши по воду в пультовую -- похоже, понял причину:

      • А вариантов использования, если подумать, не 2 (1:"складируем в sndbuf", 2:"конвертируем из sndbuf в sndbuf же"), а 3:
        1. Складируем в sndbuf -- БЕЗ конверсии (вот этот был забыт).
        2. Складируем в sndbuf -- С КОНВЕРСИЕЙ (а этот толком не учитывался).
        3. Конвертируем внутри sndbuf.

        Штука в том, что вариант 2 отличается от 1 тем, как именно используются dtype'ы. Т.к. нужно при определении места помещения в буфер учитывать не "текущую" пару src_dtype/dst_dtype, а БУДУЩИЙ dst_dtype, от варианта 3.

        Рассмотрим подробнее (тем самым выпишем сценарий/алгоритм):

        1. Складируем в sndbuf -- БЕЗ конверсии, но С БУДУЩЕЙ конверсией:
          • для определения "куда" надо брать максимум не из src_dtype,dst_dtype (они будут совпадать, из-за отсутствия конверсии), а из src_dtype,ri->dtype.
          • Действие промежуточное, завершение будет произведено в вар.3.
        2. Складируем в sndbuf -- С КОНВЕРСИЕЙ:
          • всё просто -- dst ставим в начало буфера.
          • Это окончательное действие -- дальше уже только отправка. Надо сбрасывать флаг DO_RD_CONV.
        3. Конвертируем внутри sndbuf:
          • src ставим в sndbuf+max_total-src_total, dst=sndbuf.
          • Завершение работы из вар.1. Также окончательное действие, требующее сброса DO_RD_CONV.
        4. 16.02.2018: есть еще один вариант ("0-й") -- просто складируем, БЕЗ конверсии, и БЕЗ будущей конверсии.

          Но это делается прямо в StoreData4Snd(), в отдельной ветке.

      • @вечер-дорога-домой-по-Лаврентьева: очевидно, что нужно вместо "догадывания" (по data==NULL) вводить явный параметр, указывающий производимое действие -- одно из 3 вышеперечисленных, и делать его enum'ом, чтоб были символьные константы. И в "мозгах" в начале DoStoreWithConv() в зависимости от этого выставлять все параметры.

    16.02.2018: и-и-и!!!

    1. "Буферизация и отправка" -- ну же (последний рывок?):
      • Дополнительная оптимизация, чтоб не откладывать отсылку из-за требования нативизации (NTVZ), реально ненужной: если "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()-то нет!!!

      • Введены константы для обозначения 3 вариантов действий:
        1. DO_STORE_JUST_STORE
        2. DO_STORE_STORE_WITH_CONV
        3. DO_STORE_CONV_IN_SNDBUF
      • Повставлены вызовы, пока за-#if0'енные.
      • Параметр snd_flags убран, т.к. всегда используется просто ri->sndflags.
        • Часом позже: неа, это оказалось неудобно и параметр вёрнут. Т.к. при передаче параметра напрямую в нём сразу указываются потребные действия (точнее, роляет только NTVZ), а ri->snd_flags уже потом складируются те, которые стали актуальны ПОСЛЕ. А при "косвенной" передаче -- приходится сначала складировать.
        • 19.02.2018: а может, правильнее именно в DoStore модифицировать флаги? Тогда и сохранять их надо именно перед вызовом.
        • 20.02.2018: да, так и сделано. Итог:
          1. Параметра snd_flags нет, т.к. используется ri->sndflags.
          2. Оный ri->sndflags модифицируется (флаги нулятся после выполнения действий) прямо в DoStoreWithConv().
      • Замечание: дальнейшее делалось в последующие дни (в основном 20-02-2018), но для стройности изложения записывалось здесь же, чтоб было единым списком.
      • 20.02.2018: DoStoreWithConv() дополнен "мозгами".
        • Почему это тянулось так долго: потому, что обработка 3 вариантов оказалась плохо унифицируемабельна. В каждом из них есть куски "как в 2 других", но утолкать хоть какую-то пару в одну ветку никак не возможно, и ни один не является надмножеством другого.
        • В частности, рост/реаллокирование буфера пришлось оставить в 2 экземплярах, т.к. там разные параметры (точнее, имена переменных) используются. Формально, объём дублирования можно уменьшить, заменив 7 строчек "реаллокируем, если мало" на вызов GrowBuf() (по сути, делающий то же), но лень.
        • Так что получилось именно 3 ветви if()'ов, все 3 довольно объёмные.
        • И есть особенность в ветке DO_STORE_JUST_STORE:
          1. Термины/переменные src* и dst* там используются не в прямом смысле "откуда и куда копируем", а в смысле "откуда и куда копироваться будет в завершении, в ветке DO_STORE_CONV_IN_SNDBUF". И копируется В src, а в качестве источника выступает data.

            Зато это позволило унифицировать кусочки кода с веткой завершения -- и вычисления {src,dst}_{dtype,usize,total}, и адресуню арифметику (сдвиг src в сторону хвоста буфера).

          2. И там прямо внутри делается memcpy() с последующим return'ом.
      • Пара идеологическо-методологических вопросов по разделению обязанностей:
        1. Корркетно ли, что решение о выборе варианта складирования -- JUST_STORE или STORE_WITH_CONV -- принимается в StoreData4Snd(), а не в DoStoreWithConv()?

          Ведь разница в готовности к конверсии и её надобности, и решение -- ровно тем же условием -- могла бы принять и функция-исполнитель.

          Но пока оставим как есть -- ради наглядности.

        2. Аналогично имеющаяся в StoreData4Snd() проверка на тему "а всё равно ничего конвертировать не надо/нельзя", по результатам которой делается просто memcpy(): не перетащить ли это условие прямо в исполнительницу, ведь там тоже есть вариант "копирование реально без какой-либо конверсии, поэтому просто memcpy()".

          Но тоже в целях простоты и наглядности оставим как есть.

      • Код в вызывальщиках переведён на использование новой архитектуры: старые блоки уже удалены, много подпеределано -- в т.ч. с точки зрения сохранения ri->snd_*.
      • А еще нужна бы оптимизация на случай nelems==0: конвертировать ничего не требуется в любом случае (ибо нЕчего), но dtype сохранить надо.

        После обеда:

        • Решено сделать ровно наоборот. Смысл -- отправка нулевых объёмов бывает крайне редко, да и затраты на неё пренебрежимо малы, так что незачем этот случай оптимизировать.

          А достаточно поставить guard'ы перед memcpy()'ями.

        • Оные guard'ы вставлены, в 2 точках.
        • А проверка/guard "if (dsize != 0)" из StoreData4Snd() убрана.

    Вроде всё. Теперь проверять.

    Проверяем. Методика:

    • На примере простенького devlist'а, состоящего из 1 штуки cac208/noop, out-каналам которого навешены коэффициенты в увеличивающейся степенью 10 (каналы out1...out6: 10, 100, 1000, 10000, 100000, 1000000).
    • На клиентской стороне -- собственно скрин cac208 (т.е., pult). Смысл -- в стеке pult/Chl/Cdr отсутствует собственная буферизация (имеющаяся в cdaclient).
    • Тестирование заключается в том, что сначала запускается клиент, в нём задаётся значение какого-нибудь канала, затем запускается сервер.

      Т.е., клиент БЕЗ буферизации отправит значение сразу и оно НЕ будет масштабировано на коэффициент. С буферизацией же клиент дождётся калибровок и отмасштабирует число как положено.

      ...естественно, это касается исключительно ПЕРВОГО коннекта с сервером -- при последующих реконнектах калибровки считаются полученными и значения отправляются сразу хоть в новой версии, хоть в старой.

    Результаты:

    • Старый экземпляр (сохранившийся от 06-12-2017 в x10sae:~/4pult/) ведёт себя предсказуемо -- шлёт БЕЗ калибровки (ввели 3 -- он и шлёт 3, не домножая его на 1000).
    • А вот новый -- просто ПРОИГНОРИРОВАЛ запись: при первом коннекте не сделал вообще НИ-ЧЕ-ГО.

      ...несколькими минутами позже: зато при следующем реконнекте он запись произвёл! Очевидная догадка: это ж потому, что в момент перехода 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):

      • Попытка записи в неё cdaclient'ом приводила к зависанию, в т.ч. даже при ключе "-w" ("send writes to cda immediately"), при котором буферизация записи возлагается на cda.
      • Возлагаться-то она возлагалась, но при отсутствии обновлений канала ситуация "все условия для отправки выполнены" так никогда и не наступала, т.к. rds_rcvd оставалась ==0 -- из-за отсутствия =1 в set_phys_rds() и невызова update_dataset().

      Исправлено:

      • добавлена копия кода из update'а;
      • теоретически, достаточно было бы и просто взведения 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-плагин зачем-то (сдурел?) вернул значение, а соединение в реальности еще не готово).

      Помогло -- значение теперь записывается должным образом откалиброванное!

    • ЗАМЕЧАНИЕ: сейчас НЕ поддерживается вариант "только NTVZ, без DO_RD_CONV" -- оно ВСЕГДА делает конверсию.

      21.02.2018: уже сделано -- теперь конвертируется корректно во всех 3 вариантах (только NTVZ, только DO_RD_CONV, NTVZ+DO_RD_CONV). На что ушел весь день, зато стало "и это хорошо!".

    • Далее надо бы ввести в cdaclient флажок "не ждать получения результата перед записью" -- чтоб проверить все варианты (вектора, в т.ч. нулевого размера, @., ...).

    21.02.2018: проверяем дальше.

    • Флажок '-w' в cdaclient добавлен. И на нём также проверено на double-скалярах -- и тоже вроде работает как надо.
    • А вот проверить на тему действительно ли «НЕ поддерживается вариант "только NTVZ, без DO_RD_CONV"» -- не удалось, т.к. cdaclient всегда делает запись тем же dtype, с которым и зарегистрирован канал, к тому же черех snd_data(), а не через process_ref().
      • Возможно, как-то удастся проверить при помощи pzframe-клиентов -- там при изменениях ручек, в ParamKCB(), делается cda_set_dcval(), сводящийся к process_ref(), а регистрируется всё по умолчанию как INT32, так что NTVZ-конверсия поиисходить должна.
      • ...с другой стороны, там НЕ используется NO_RD_CONV. Придётся-таки писать утилиту (или руками временно всовывать куда-то для тестов?).
      • Да, вставлено явное "cda_set_dcval(, 3)" в cdaclient'овскую PerformWrite().
      • И-и-и -- результат предичайший! При указании канала "@i:..." -- т.е., INT32, при том, что записывается DOUBLE -- пишется просто 0! А вот с "@.i:" -- т.е., при NO_RD_CONV -- всё как надо. Мдя...
      • (Заметим: это мы еще НЕ исправляли "косяк" (с игнорированием NO_RD_CONV), а только пытаемся добиться предсказанного поведения.)
      • Разбираемся -- напихиванием отладочной печати. Так вот:
        • При "@i:" до конвертации доходит просто 0, а не 3.
          • И так оно себя ведёт только при -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 используем именно это поле для определения типа, а данные еще НЕконвертированные.

            Исправлено, и значение стало верным.

        • При "@.i:" оно вообще НЕ проходит через конвертацию. Даже просто в DoStore не входит.
          • Нашлась причина! SendOrStore() в "большом условии" (можно ли слать сразу) взведённость CDA_DATAREF_OPT_NO_RD_CONV является ДОСТАТОЧНЫМ условием, позволяющим игнорировать snd_flags, включая _NTVZ_DTYPE.
          • ...и в StoreData4Snd() тоже.
          • Решение проблемы "в лоб" сомнительно: не вполне ясно, как сформулировать условие, чтобы флаг _NO_RD_CONV был "достаточным" только при отсутствии _NTVZ_DTYPE.
          • Более здравым решением представляется:
            • Вообще убрать ОТТУДА проверку на _NO_RD_CONV.
            • А в начало SendOrStore() -- через который проходят ВСЕ запросы, уходящие потом на буферизацию -- вставить отдельную проверку типа
              if ((ri->options & CDA_DATAREF_OPT_NO_RD_CONV) != 0) snd_flags &=~ DO_RD_CONV
            • И более в цепочке отправки/буферизации НИГДЕ на _NO_RD_CONV не проверять.
            • А титульную проблему -- "не выполнять в DoStoreWithConv() RD-конверсию при взведённом _NO_RD_CONV" -- решать, проверяя именно snd_flags на флажок DO_RD_CONV.
          • 22.02.2018: делаем.
            • Вышеприведённая проверка вставлена.
            • Дальнейшие проверки на _NO_RD_CONV убраны.
            • Очевидная идея: надо бы и проверки на не-арифметичность -- что repr!=REPR_INT и repr!=REPR_FLOAT -- вынести в это же начальное условие.

              ...да и остальные "не могущие измениться в будущем" условия -- на пустой список {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: вроде со скалярами всё.
    • @вечер-дома: так, размышления: забавная ситуация получается.
      • Снаружи интерфейс один -- с NO_RD_CONV и "просто отправкой" (хотя и с хитрым вариантом в виде "process_ref()");
      • Внутренняя реализация первоначально была прямолинейной -- отправка, конверсия с проверкой флага, etc. В соответствии с этим API.
      • Но потом, под давлением обстоятельств и дополнительных появляющихся/осознаваемых требований, внутренняя реализация менялась, так что теперь она устроена совсем неочевидно. Но задачи свои решает.
      • И ситуация эта напоминает эволюцию процессоров, где внутренняя реализация сейчас радикально отличается от "ISA": видимая система команд -- нормальное последовательное исполнение, а внутри -- out-of-order, с переименованием регистров и прочими тонкостями.

    22.02.2018: продолжение тестирования.

    • Проверил на векторных каналах: вроде работает как надо. И значения нулевой длины пишутся, и непустые конвертируются как положено (в т.ч. при запуске сервера ПОСЛЕ софтины, вызывающей запись сразу (-w)).
    • Так что убираем то, что было напихано ради отладки: принудительный "cda_set_dcval(, 3)" из cdaclient.c и толпы отладочной печати из cda_core.c.
    • А вот что НЕ проверено: насколько функционирует тот "сдвиг исходных данных к концу буфера" при DO_STORE_JUST_STORE в случае, если размер "исходных" единиц меньше, чем "конечных".

      @вечер-дома: Проверять надо, очевидно, тоже куроченьем cdaclient'а -- вставить туда фиксированную отправку вектора нужного типа и размера.

    • 24.02.2018: авотфиг: ведь единственный случай, когда NTVZ делается -- из process_ref()'а, где лишь скаляр, причём еще и double (т.е., самый большой -- 8 байт; а нужно из небольшого в большой -- типа INT32->double). Курочить еще и cda_core, временно добавив NTVZ в обычную snd_ref_data()?

      Сделано -- ВСЁ: и 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: вот теперь считаем, что протестировано всё, и можно деплоить на комплексе (вот там если что ещё вылезет ;)).

  • 31.08.2015: (давно назревает) научиться бы всё-таки воспринимать (через Chl+Cdr) спецификации серверов вида ":N". Чтоб при запуске локального стенда не беспокоиться об имени хоста (и не требовать "localhost:"). Как-нибудь проверять, что после ':' только цифры, что ли...
  • 18.09.2015: замечен забавный баг: если какой-то канал был, Cdr через cda к нему приконнектился, а потом после перезапуска сервера канал исчез (и ручка стала "чёрной"), то ФЛАГИ останутся гореть от старого (изчезнувшего) канала. Выглядит психоделически -- у ненайденного канала флаги вроде "REM_C_PROBL"...

    Причина -- похоже, они не нулятся.

    ...даже не "похоже", а точно -- в cda_dat_p_set_notfound() делается ri->rflags|=CXCF_FLAG_NOTFOUND, а надо бы вместо "|=" просто "=" (и в cda_dat_p_set_hwr() тоже).

    18.09.2015: сделано просто "=" -- всё исправилось.

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

  • 03.01.2016: A BIG FAT NOTE: у нас совершенно НЕ реализовано гроханье (освобождение) никаких ресурсов: ни каналов, ни формул, ни контекстов (и ни серверов).

    В результате использовать cda в драйверах очень чревато -- сейчас это позволительно лишь из-за отсутствия консольного интерфейса, позволявшего бы рестартовать устройства.

    И сделать бы не особо сложно -- всё под это рассчитано, надо просто взять да аккуратно реализовать во всех компонентах и потом проверить.

    09.01.2016: потихоньку приступаем к реализации. 26.10.2017: а вот нифига не видно, где именно тогда было как "приступлено" к реализации. Следов в файлах никаких нет. Видимо, чисто намерения.

    Основной постулат -- на уровне cda_core.c освобождение полей должно делаться в соответствующих Rls*Slot().

    • К "полям" также относятся и вложенные/подчинённые структуры -- речь о cb_list'ах.
    • А вот используемые ресурсы из "параллельных структур" (sid'ы и ref'ы для контекстов) -- не стоит, т.к. это крайне неоптимально.

      ...конечно, неоптимальность только при вызове Rls() сразу в аллокаторах (при ошибках, чего почти никогда не будет), но всё же.

    21.04.2017: вот и аукнулось наконец отсутствие корректного освобождения ("гроханья") ресурсов в cda.

    1. Предыстория, за вечер 20-04-2017 (часов с 20 и до 23!):
      • Звонит Федя и говорит, что источник QL15 не управляется и вообще горит на экране болотным.
      • Первая мысль -- что-то с CAN: больной блок, глюк canserver'а, ...

        Всё последовательно перепроверил (час ушел!) -- нифига, всё в порядке: пакеты бегают, и даже экранчик cdac20.subsys, натравленный напрямую на linac1:11.icd_ql15, прекрасно функционировал. А вот ist_cdac20-девайс linac1:11.ql15 -- нет.

      • Краткое напихивание отладочными fprintf()'ами показало, что до него не доходят никакие уведомления от подстилающего девайса, кроме первоначального "_devstate==0".

        Вот так: у всех прочих vdev'ных протокол insrv данные успешно доставляет, а у ql15 -- нет.

      • В конце концов пришла шальная мысль -- а пусть он ходит по протоколу "cx::".

        Сделал -- получилось, источник стал управляться.

      • ...ну а более детальные разборки были отложены на следующий день.
    2. А теперь собственно разбирательства, происходившие уже 21-04-2017 (с утра и до ~16 часов!):
      • Утром (или вообще ночью?) в голову пришла мысль: сделать ВТОРОЕ устройство ("zl15") с тем же target-девайсом, и посмотреть, как оно работает.
      • Но при первой же попытке хоть что-то посмотреть -- прямо клиентиком ist_cdac20.subsys -- вылезла совсем дикая вещь: клиент не мог работать с сервером -- отваливался по диагностике "Received packet is too big".
        • ...с последующим реконнектом, и иногда оно таки подконнекчивалось и работало, а иногда постоянно так отрубалось.
        • И также в консоль-логе сервера (запущенного с -dc) встречалось аналогичное ругательство.
        • Что более всего запутало: оно так происходило при запуске сервера как :11, но НЕ происходило при :12.

          Вот эта загадка так и осталась неразрешенной.

      • Процесс докапывания до истины:
        • Отладочная печать в fdiolib'овском StreamReadyForRead() первых 8x int32 из пакета показала, что первым словом пакета (которое представляет собой CxV4Header.DataSize) иногда прилетает 0xFFFFFFFF.

          А это, кстати, как раз значение insrv'шного HWR_VAL_CYCLE=-1.

        • Очевидная идея: где-то путаются файловые дескрипторы и это самое -1 вместо драйвера (который и не получает уведомления о цикле!) улетает клиенту (который от этого охреневает и закрывает соединение).
        • И тут я вспомнил, что не далее как вчера -- 20-04-2017 -- добавил в devlist-linac1-11 девайс "walker" для "искусственного" источника Spectr (работающего через double_iset_drv как Spectr1+Spectr2). РЯДОМ.

          Вот карта по номерам:

          • [82] iset_walker Spectr
          • [83] ql15 -- неработающий
          • [84] qltr3 -- работающий
        • И конкретно этот iset_walker[82] работать не мог, т.к. канала Spectr.Iset_cur ему к тому моменту не было (как и Spectr._devstate, но до него дело уже не доходило).
        • Дальнейшее напихивание fprintf()'ами уже vdev'а -- stat_evproc() -- показала, что уведомления о смене target'овых состояний на +1 (OPERATING) получают [82] (мёртвый!!!) и [84], а [83] -- нет. Вот ql15[83] и выглядел болотным.
        • И тут стала ясна причина: отсутствие в cda гроханья ресурсов.
          • Из-за неподчистки серверов контекста он, [83], получал тот же cid, что и обломившийся по отсутствию канала [82] -- контексты-то "подчищались", освобождая идентификаторы (cid), но не освобождая ресурсы.
          • Получал в наследство 82'шный сервер-sid -- тот просто висел в массиве, и поиск в cda_dat_p_get_server() возвращал его, т.к. cid'ы совпадают, ...
          • ...но у того ж "хозяином" значился devid=82, с 82'шным же privptr'ом.
          • Вот оно и "слало" что-то неживому драйверу, которым никто из клиентов не интересовался.
        • Что НЕпонятно -- так это как оно умудрялось промахиваться (очевидно, от имени 82-го) и писать -1 в cxsd_fe_cx'ный дескрпитор? Ведь номера дескрипторов клиентских соединений уже сильно выше insrv'шных -- просто потому, что оные аллокируются при старте сервера, а клиентские все после.
        • Очевидно, собственно процесс отсылки делался потому, что не снимался обработчик InsrvCycleEvproc() (об этом ниже), но с самим промахом по дескрипторам всё-таки загадка.
      • В cda_d_insrv.c НЕ делается CxsdHwDelCycleEvproc(...,InsrvCycleEvproc,...) при удалении соединения. А надо делать в RlsLcnSlot().

        ...оно, конечно, СЕЙЧАС всё равно не используется, ибо cda_d_insrv_del_srv() пуста, да и из cda_core метод del_srv() пока не вызывается, но всё же.

        25.04.2017: добавлено.

    3. Общий вывод: пока нет корректной подчистки ресурсов, надо всё делать ОЧЕНЬ аккуратно, чтобы vdev/cda-использующие драйверы всегда стартовали, а не обламывались.

    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() ничего прочитать не смог":
      1. Дескриптор молча разрегистрирует и закрывает, и записывает lp->fd=-1.

        ...там в комментариях написано, что надо бы "(schedule?) re-open", но этого НЕ делается.

      2. И при этом НИКОГО не уведомляет -- драйверы остаются висеть "работающими".
    • В защитной проверке DECODE_AND_CHECK() один из признаков проблемы -- lines[line].fd<0.

      Поэтому при первом же после "молчаливого закрытия" запросе на чтение канала (или регулярной пере-отправке из очереди по истечению таймаута) обнаруживается, что линия "неживая" и генерится сообщение об ошибке.

    Т.е., всё сделано в надежде, что при проблеме чтения будет производиться пере-открытие (в v2'шном старом kshd485_drv_common.c ближе к концу его жизни добавленное, но кривовато). А этого не происходит, вот оно и сдуревает.

    26.10.2017: вот теперь действительно приступаем к реализации.

    • Из access-плагинов, для простоты, начинаем с cda_d_insrv.c (он же самый критично-необходимый). На нём натренируемся -- распространим на другие.
      1. Основная часть -- гроханье серверов.
        • "В-нулевых", была изменена декларация -- GENERIC_SLOTARRAY_DEFINE_GROWING(,Lcn,...: параметры FIRST,INC,MAX были заменены со "стандартных" и безликих 0,2,0 на LCN_MIN_VAL, LCN_ALLOC_INC, LCN_MAX_COUNT.
        • Собственно "мясо" освобождения было помещено в отдельную DestroyInsrvPrivrec(), т.к...
        • ...надо поддерживать being_processed/being_destroyed, вся работа с которыми была сделана давным-давно (изначально?), поэтому...
        • ...реальный вызов гроханья в 2 местах:
          1. В cda_d_insrv_del_srv() проверяется, что если being_processed, то лишь выставляется being_destroyed=1, а если нет, то реально вызов убиения.
          2. И в insrv_fd_p() в "закрывающей скобке" при уменьшении до being_processed==0 и выставленном being_destroyed вызывается деструктор и цикл обрывается return'ом.
        • Также снятие cycle-evproc'а. Вот тут получилось уродливовато.
          • Собственно снятие делается в UnRegisterInsrvSid() -- парной к RegisterInsrvSid().
          • Но вызывается она из RlsLcnSlot(), стоящей РАНЬШЕ, так что пришлось делать forward declaration.
            • Уши растут, видимо, еще из cda_d_local.c -- самого первого dat-плагина, где и была введена схема с RegisterDDDSid(). Но там-то никакого cycle-evproc'а нет.
            • Прикол в том, что "уровнем мельче" -- с hwr'ами (см. ниже) -- такой проблемы нет: там UnRegisterInsrvHwr() идёт физически РАНЬШЕ, чем RlsHwrSlot().
            • Вероятно, какой-то идеологический косяк, и схему надо пересмотреть?

          Надо будет потом к этой теме еще вернуться.

      2. Гроханье отдельных каналов -- что должно было быть попроще :D
        • Для начала -- даже прототип 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.

        • Потенциальная проблема: если генерятся какие-то данные для канала, уведмоления отправляются в pipe, а этот канал берёт и удаляется -- и данные прилетают на канал, которого нет!

          И хорошо еще, если просто нет (он проверкой отсеется), а если этот hwr уже успел получить другой канал, зарегистрированный сразу ПОСЛЕ удаления того?

          • Самым "правильным" было бы просто сравнивать прямо имя канала. Чего, конечно, делать никто не будет да и никак это (ведь резолвинг делается вначале).
          • Более реалистичной выглядит идея иметь какой-нибудь счётчик (вроде cxlib'овского Seq), и сравнивать.

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

            ...единственное -- можно б было изменить протокол:

            1. присылать не 1, а 2 int'а.
            2. И тогда 2-й надо было бы делать не "счётчик", а просто gcid (лучше -- cpid), и его уже сравнивать с записанным в hi->.

            Но этого утолщения протокола делать тоже совсем не охота.

          27.10.2017: Пока что, пожалуй, можно просто забить: по факту, ВСЕ ДАННЫЕ браться будут всё равно от правильного канала, а единственная реальная путаница (точнее, "потеря" информации) -- о факте обновления: оно якобы придёт, а реально нет (т.к. от другого канала). Учитывая, что вряд ли будут драйверы, использующие insrv:: и меняющие список заказанных каналов, то реального ущерба вряд ли предвидится.

        • Кстати, той самой проверки "валиден ли hwr" не было -- добавлена, в обе точки.

    27.10.2017: продолжаем:

    • Поддержка в самом cda_core.c: в соответствии с постулатом от 09-01-2017, всё делается в Rls*Slot().
      • RlsRefSlot(): добавлен вызов "деструкторов" в зависимости от типа (CHN/FLA/REG, в последнем случае ничего не делается).
      • RlsSrvSlot(): также добавлен вызов "деструктора".

        И дополнительная тонкость: в find_or_add_a_server() сервер может быть "недосозданным" -- когда SrvSlot для него уже создан, а pdt_privptr еще не аллокирован.

        • И при обломе аллокирования privrec'а всё равно будет вызван 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().

    • Тестируем далее -- падает уже в cda_d_insrv.c.

      Нашёлся косяк, аналогичный предыдущему: в 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).
      • Со 102 еще работает.
      • При уменьшении до 103 -- после запуска наглухо зависает, не реагируя ни на Ctrl+C, ни на Ctrl+\, а помогает только kill -9.
      • Натравливание на живой процесс gdb кажет мало понятного -- где-то в недрах аллокатора.
      • а strace показывает, что висит в
        futex(0x7f60d46dd760, FUTEX_WAIT_PRIVATE, 2, NULL
      • Возвращаемся к backtrace'у от gdb: вау!!!

        Зависон где-то в подчинённых malloc(), вызванного в strdup() stroftime_msc(), вызванного из ll_vlogline(), вызванного из onsig (sig=11) -- т.е., это опять же SIGSEGV (просто память успела испортиться настолько, что наш обработчик уже не функционален).

        Но это уже вполне годная информация для разбирательства.

      • Мысль: а ведь 100 -- это инкремент в аллокировании и ref'ов в cda_core, и hwr'ов в cda_d_insrv (и для periodics'ов в нём же)!
      • ДА! Мысль оказалась верной:
        • в cda_d_insrv.c::RegisterInsrvHwr() при ре-аллокировании periodics[] не модифицировалось значение periodics_allocd, оставаясь всё время =0.
        • Вот с каждым новым каналом и делалось ре-аллокирование опять до 100 элементов; первые 100 штук это "работало", потом начиналась запись за границы аллокированного (привлечённый valgrind показал запись со смещением 0 за границей отведённых 400 байт), а 104-й элемент (с которого и начинался SIGABRT) -- +16 байт, видимо, оно залазило в соседний memory chunk или в какие-то служебные структуры аллокатора.
        • Раньше ошибка не проявлялась потому, что ни у какого драйвера не было более 100 insrv-каналов.

      Резюме:

      1. Это был косяк в старом коде cda_d_insrv, а не в новом коде (реализации "гроханья").

        После тривиального исправления падения исчезли.

      2. В прочих аналогичных случаях такого бага нет, т.к.:
        1. В cda_d_local.c и cda_d_dircn.c нет надобности в "периодичностях" -- то каналы синтетические, и инициатива их обновления ВСЕГДА у программы.
        2. А в cxsd_fe_cx.c проблемы нет потому, что там периодические измерения (мониторы) делаются иначе -- SLOTARRAY'ем.
    • Теперь тест с 10000 каналами стало можно запустить и он был запущен.
      • На вид -- всё окей: номера контекста и ref'ов всегда одинаковые (1 и 1-100000), память не утекает (судя по показаниям top'а).
      • Вот только процессора этот тест жрёт -- мама не горюй! ~86% одного ядра (и это 1285Lv4, постоянно торчащий всеми ядрами на 3.7GHz (турбо минус палка?), с его 128MB L4 (в которые рабочий набор cda явно помещается))...
    • Теперь надо адаптировать прочие cda_d_*.c.

    04.11.2017: (суббота, праздник, но не удержался) пилим дальше:

    • cda_d_local.c адаптируем по образу insrv (они внутри очень похожи -- у обоих pipe для передачи уведомлений от "хозяина" каналов к dat-плагину).
      • Даже diff'ы со старыми версиями похожи, ...
      • ...с той лишь разницей, что у local'а нет никакого CycleEvproc'а.
      • Зато вместо ChanEvproc'ев -- VarCbSlot'ы. А их n'ы (id) нигде не сохраняются, а надо бы, в 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, целый день потратил на поиск косяка :).

    • @утро-пультовая-в-районе-12:00: Почему в cda_d_insrv -- RlsHwrSlot() делается в ДВУХ местах -- DestroyInsrv_HwrIterator() и в cda_d_insrv_del_chan()?

      @613-я часом позже: да не, всё в порядке -- первый раз разрегистрация всех hwr'ов оптом при удалении sid'а, а второй -- разрегистрация 1 канала.

    05.11.2017@вечер-~21:00-пешком-из-ИЯФа по Лаврентьева: насчёт "как проверять" все прочие dat-плагины:

    • можно ж драйвер test_cda_del_drv.c подшаманить, чтоб он брал defpfx из auxinfo.

    Тогда простой сменой конфига его можно будет перенацеливать на что угодно.

    08.11.2017: далее:

    • Да, test_cda_del_drv.c теперь берёт defpfx из auxinfo.
    • Плагин cda_d_local.c вроде доадаптирован (см. выше страдания насчёт снятия ChanEvproc'ев (точнее, VarCb'ов)).
    • Проверяем... утекает память.

    Кстати, valgrind показывает какие-то ошибки -- invalid read и invalid_write -- в ppf4td_pipe.c. Сходу нифига не понятно -- на вид всё OK; но надо бы разобраться.

    09.11.2017: ищем причину утечки.

    • Оказалось, что как раз VarCb и не освобождаются -- найдено печатью n в RegisterLocalHwr(), он с каждой итерацией увеличивается.
    • Причина нашлась: в DestroyLocalPrivrec() СНАЧАЛА делалось me->lcn=-1, а потом уж гроханье всех hwr'ов.
      • Это наследство от cda_d_insrv.c, где такой подход ничему не мешал (ибо lcn для идентификации hwr'ов не использовался).
      • Исправлено -- утечка памяти исчезла.
      • В cda_d_insrv.c тоже переделано -- так корректнее.
    • Засим считаем cda_d_local допиленным и надо приступать к следующим...

    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 плюс их обработка.
    • Для унификации с _insrv и _local (да и общей правильности) было много переименований:
      • Переименовано всё, касающееся "lclconn": *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_nvar_cbrec_t) превратились в client_sid,client_hwr.

        И тут сильное отличие от _local: в том используется lcn, а тут именно sid. Почему такая разница:

        • _local использует lcn для доступа к var_cbrec_t -- для fdio_send'енья client_hwr'а;
        • _dircn использует 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:

    • @утро-мытьё-посуды а ведь не прокатит то, как сейчас сделано -- проверки в плагинах cda_d_*: потому, что хоть плагиновы del_srv() и "откладывают" вызов Destroy*Privrec(), но менеджментом самого куска памяти под privrec занимается cda_core, и уж оно сделает free() сразу!
    • Вначале была идея модифицировать интерфейс cda_dat_p_del_srv_f(), чтобы он возвращал int-результат -- можно/нельзя делать free(pdt_privptr), и если "нельзя", то ответственность за это dat-плагин берёт на себя и сделает когда станет можно.

      Но это крайне некрасивая идея из-за размазывания ответственности (нарушение инкапсуляции), поэтому так делать не будем.

    • Вывод: откладывание нужно делать не в dat-плагинах, а прямо в самом cda_core.c.
    • Анализ кода оного показывает, что сделать оное можно. Но муторно.
    • С другой стороны, вероятность, что будет грохаться именно КОНТЕКСТ (а не отдельные каналы) -- весьма мала.
    • Вывод: сейчас оставим всё как есть (пометив в коде cda_d_*.c, что тамошнее откладывание слабополезно), отдавая себе отчёт, что по факту откладывание не функционирует. Если же появятся обстоятельства, где этот функционал реально потребуется (и с конкретным сценарием) -- тогда и будем делать.
    • 15.11.2017: а прокатит ли такой вариант -- полностью в cda_core? Ведь НАЧАЛЬНАЯ-то точка цепочки вызовов, касающейся pdt_privptr'а -- в dat-плагине, БЕЗ cda_core над ней, т.к. вызывается из fdiolib/cxscheduler'а; например, 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().
    • Оказывается, в cxlib отсутствует cx_delmon()!

    16.11.2017: допиливаем:

    • Сделана cx_delmon(), полным копированием cx_setmon и с заменой в единственной точке: CXC_SETMON на CXC_DELMON.
    • Ну и вызов "отписки" -- cx_begin(),cx_delmon(),cx_run() -- в cda_d_cx_del_chan() добавлен.
    • Проверяем. Не всё окей...
      1. Первый коннект почему-то заканчивается "abnormally disconnected" по "Broken pipe" (вместо просто "disconnect" у последующих).
      2. Память потихоньку утекает с каждым коннектом.

        Нет -- похоже, умудрился запустить необновлённый бинарник сервера с непроапдейченным 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.
    • А вот цикленья по всем командам не делается -- пока незачем.
  • 23.06.2016: (где-то на эту тему уже было, но вот где...) несколько раз возникал вопрос: на канал присланы некие свойства (калибровки, например), а потом сервер рестартуется, уже с иными настройками, и какие-то из свойств, будучи пустыми, заново не присылаются и в результате остаются "висеть" и использоваться старые значения, которые уже неактуальны.

    Так вот, идея: ловить событие "обрыв соединения для данного канала", и по нему "забывать" такие свойства.

    Кстати, вводить новый код cda-события незачем -- уже есть CDA_REF_R_STATCHG, генеримый по cda_dat_p_defunct_dataset() (коий и вызывается при обрыве соединения).

    01.07.2016: как-то мутновато.

    • Во-первых, непонятно -- что это за свойства такие, которые при восстановлении НЕ будут присланы (причём при неуказанности -- пустыми)?

      Очевидно, речь про {r,d}? Но они вроде при установлении монитора на канал пришлются сразу, пусть даже и пустые.

    • И конкретно {r,d} сбрасывать не стоит точно -- пусть между коннектами используется последнее известное.

    Короче -- идея тут записана, если потребность опять вылезет и удастся её сформулировать повнятнее и поаргументированнее, то решение имеется.

  • 05.10.2016: сей момент все формулы делятся на 2 вида -- "читающие" и "пишущие":
    • Читающие -- возвращают значение, но НЕ принимают параметра. И могут выполнять запись.
    • Пишущие -- принимают параметр, но НЕ возвращают значения.

    А желательно бы -- для ИПП, конкретно для kind=spectr, уметь выполнять просто "вычислительную" формулу, которая бы И принимала параметр (значение в столбце), И возвращала бы значение.

    ...теоретически что-то в эту сторону должно б быть делабельно через params[num_params], но они вообще нигде никак не используются.

    07.10.2016: если подумать, то становится ясно, что есть ТРИ разных свойства, могущих иметь смысл независимо:

    • Разрешение писать просто (IS_W). ...и отдельно READONLY, запрещающий ЛЮБУЮ запись.
    • Наличие параметра на вход (HAS_PARAM).
    • Требование возврата значения (RETVAL_RQD).

    У нас же сейчас -- вследствие изначального применения -- жестко зашиты 2 варианта:

    1. IS_W==0: HAS_PARAM=0,RETVAL_RQD=1.
    2. IS_W==1: HAS_PARAM=1,RETVAL_RQD=0.

    Очевидно, что нужно разводить на 3 реально раздельных флажка. Нюансы из-за изменения API:

    1. Аккуратно при этом подшаманить все точки вызова.

      //cda_process_ref(), cda_add_formula().

    2. Поскольку cda сейчас является частью API "cxsd_driver", то, формально, надо бы MAJOR сдвинуть. Но, поскольку де-факто нигде (похоже) формулы не используются, то можно оставить как есть.

    Протокол деяний:

    • Добавлены биты 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: продолжение деяний:

    • Подшаманиваем точки вызова. Сей момент это всего-навсего 4 файла:
      1. cda_core.c -- внутренние вызовы cda_process_ref().
      2. Cdr_treeproc.c -- основное.
        • Замечание: в CallAtInitOfKnobs() процессинг at_init_ref'а вызывался с флагом CDA_OPT_IS_W -- видимо, чтоб могло что-то писаться при старте.

          Обсуждение: это вроде как не очень хорошо -- если уж что пишется, то пусть с явным префиксом "allow_w". Но сейчас и запись в "регистры" (varparm) сделана так же, а основной смысл at_init'ов -- именно такая запись (инициализация).

          Резюме: оставлено CDA_OPT_WR_FLA.

      3. formula_drv.c -- всё тривиально, там именно читающая и пишущая формулы.
      4. liu_key_drv.c -- чуть хитрее: тут просто процессинг, БЕЗ входного параметра, поэтому оставлено просто CDA_OPT_IS_W.

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

    • Собственно реализация/использование: исключительно cda_f_fla.c. Поле is_wr удалено, а все проверки переведены на проверку битов в уже имевшемся exec_options:
      1. proc_PUTCHAN() -- IS_W.
      2. proc_QRYVAL() -- HAS_PARAM.
      3. OP_RET -- RETVAL_RQD (проверка, можно/нужно ли брать значение из стека).
      4. SleepExp() и cda_f_fla_p_execute() -- RETVAL_RQD (выяснение, надо ли возвращать значение).

    01.11.2016: ну сделал тогда -- а что толку? Использовать пока вообще никак и нигде не потребовалось.

  • 23.05.2017: издавна (два года с момента появления OPT_NO_RD_CONV и OPT_SHY) был идеологический косяк: при регистрации каналов поиск "такого же уже зарегистрированного" не обращал внимания на флаги, так что в качестве "такого же" мог зарегистрироваться канал с иным поведением -- с другим значением _RD_CONV, ON_UPDATE, ... Только для SHY было хакофиксенье -- при наличии такого флага у регистрируемого он принудительно прописывался в уже найденный.

    Это криво и так быть не должно.

    23.05.2017: делаем.

    • В ctx_ref_checker() добавлено сравнение -- что options найденного и модели совпадают.
    • А в подготовку модели для поиска -- заполнение поля options, ранее отсутствовавшее.
    • "Пропагирование" SHY убрано.

    Проверено (cdaclient'ом с ключом -Dr), работает:

    1. С разными флагами получаются разные ref'ы.
    2. С одинаковыми (хоть нулевыми, хоть совпадающими) флагами возвращается уже зарегистрированный ранее ref.
  • 12.06.2017: а можно ж сделать модуль для доступа по SNMP!

    Идея пришла в голову на прошлой неделе, когда ЧеблоПаша выдал мысль, что вот бы иметь этакий статус-экран, на котором показывается состояние (хорошо/плохо) всех важных компонентов системы (вроде "IOC health monitor"), в т.ч. всякие температуры железок, доступные по SNMP.

    Позавчера посмотрел -- да, структура SNMP-имён (OID -- object identifier) иерархическая, причём даже разделение точками '.'.

    Пара мыслей (@вчера, идя по пустому ИЯФу мимо 4-го):

    • Некоторый вопрос будет с форматом полного имени -- HOST:NAME -- и его парсингом. Самым напрашивающимся является именно HOST:NAME; вопрос будет лишь в том, как научить cda_core -- kind_of_reference(), combine_name_parts() -- правильно его воспринимать.
    • Правильно-то будет SNMP-доступную информацию складировать в сервер, чтоб клиенты оттуда брали уже по cx::. А cda_d_snmp.c даст доступ именно напрямую из клиентов.
      1. Можно вместо cda_d_snmp.c сделать snmp_drv.c.
      2. А можно оставить доступ через cda, но пользоваться им в сервере: либо по-канально через mirror_drv, либо группами через vdev.

      В любом случае, способность pult-скринов напрямую получать информацию по SNMP -- это круто! ("прикольно", "забавно", "полезно")

  • 22.08.2018: у ЕманоФеди есть желание мочь узнавать rw-тип канала. Чтоб генерить интерфейс на лету -- ручки либо отображаторы (на возражение, что этот тип может вообще меняться при рестартах сервера, отвечает, что может и в интерфейсе менять по ходу дела).

    Мы ж можем присылать вместе с прочими свойствами канала?

    23.08.2018: изучаем вопрос.

    1. Значение "rw" УЖЕ присылается -- в CxV4CpointPropsChunk, вместе с "dtype" и "nelems".
    2. Надо только добавить передачу этих данных от cxlib'а наверх -- для чего добавить поля в cx_rslv_info_t.
    3. А также добавить cda_dat_p-API для передачи сей информации cda_core (и там соответствующие поля в refinfo_t).
    4. И клиентский API для вычитывания этого всего.

    26.08.2018: делаем.

    • В cx_rslv_info_t поля добавлены, их заполнение в async_CXT4_DATA_IO() сделано.
    • В cda_core.c:
      • В refinfo_t доабвлены поля, с префиксом hwinfo_.

        В т.ч. -- hwinfo_srv_hwid, куда можно складировать именно hwid -- "внутрисерверный идентификатор канала"; то, что в EPICS/CA называется "SID" (Server ID). Это чисто для отладки/диагностики.

      • Для "сброса в состояние неопределённости" -- -1,UNKNOWN,-1,-1 -- сделана reset_hwinfo(), вызываемая
        1. При аллокировании канала -- точнее, после ЛЮБОГО создания ячейки путём GetRefSlot().

          ...хотя, строго говоря, для прочих оно и не требуется.

          27.08.2018: да, поскольку теперь отдача наверх только для REF_TYPE_CHN, то из прочих созданий ячеек убираем.

        2. При сообщении "упс, пока канал не найден" -- 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() сброс убран.

      • Отдача dat-плагинами значений -- 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.

    • Dat-плагины:
      • cda_d_cx.c: отдача hwinfo в реакции на CAR_RSLV_RESULT, прямо перед вызовом cda_dat_p_set_ready().

        Негативный аспект: в случае указания каналов по номерам (server:N.nnn) резолвинг НЕ выполняется и, соответственно, информация НЕ добывается.

      • cda_d_insrv.c -- отдача в аналогичной точке (реально нафиг не нужно, но легко и просто до комплекту).

      В прочих -- d_dircn, d_local, d_v2cx -- делать не имеет смысла.

      Хотя в d_vcas -- возможно (там вроде информация об rw есть, в виде rw/ex/ro).

      И в d_epics -- точно будет иметь смысл; как, возможно, и в d_tango.

    • В cdaclient добавляем возможность эту инфу печатать.
      • Ключик -- -DH; он кумулятивный, -DHH -- чтоб кроме rw/dtype/nelems печатались бы и hwid с hwr.
      • ...а вот куда вставить печать -- хбз: события-то выделенного и нету!

        ...и совместить с чем-то -- вроде fresh age -- нельзя: cxsd_fe_cx пакет RESOLVE присылает последним, уже после STRS, RDS, FRESH_AGE и QUANT...

        Пока в голову приходит только "запоминать, что еще не печаталось, и печатать по первому NEWVAL/CURVAL"...

    26.08.2018@вечер-субботы-дорога-домой-около-мыши-и-ИПА: к вопросу "куда вставить печать" и вообще "по какому событию считать hwinfo полученной": а просто ОТДАВАТЬ событие CDA_RSLVSTAT_FOUND -- которое формально существует, но никем не генерится (т.к. сделана "оптимизация" -- что в качестве "стал найден" «просто любой приход данных в канал -- уже сработает как "найден!"»).

    Таким образом, делаем всё более стройным, симметричным и унифицированным.

    27.08.2018: двигаемся дальше.

    • Изучение исходников показало, что введение реальной присылки RSLVSTAT_FOUND ничему не противоречит: cda_core к нему вообще индифферентна (просто передаёт событие клиентам), а клиенты либо вообще готовы (как libpzframe), либо индифферентны либо могут быть обучены (собственно cdaclient).
    • В cda_d_cx.c::ProcessCxlibEvent() и cda_d_insrv.c::cda_d_insrv_new_chan() вставлены cda_dat_p_report_rslvstat(,CDA_RSLVSTAT_FOUND).
    • Собственно cdaclient.c:
      • Собственно "место для выдачи": ловится CDA_RSLVSTAT_FOUND.
      • Сама выдача: ничего мега-хитрого, но формирование human-readable dtype'а -- длиннющий if/else/...

    Проверяем -- работает!

    Только почему-то на все cpoint-каналы выдаёт тип "d", хотя железно они "i". Надо б поразбираться -- глянуть на векторные и текстовые.

    Вечер: нет, нифига -- то была просто ошибка: просто проверялось на программе rfsyn, а у неё ВСЕ каналы задержек -- каналы V, которые по определению вещественные (сравнивал же с "исходными" в rfsyn_eng, которые int32). Отдельно проверил на тестовом devlist'е с каналами типов d,i,b,t -- всё работает корректно.

    Засим считаем задачу выполненной, а раздел "done". Но также см. раздел-замечание ниже.

  • 27.08.2018: в продолжение предыдущего раздела, о возможности получать информацию о каналах.
    • Изначально эта фича полезна для программ-билдеров интерфейсов.
    • Насчёт ro/rw -- всё хорошо: cda это вообще пофигу, а определяется только самим билдером, который может менять вид виджета.
    • А вот с ТИПОМ канала -- dtype, max_nelems -- всё сложнее. Эту информацию клиент должен задать при создании канала.

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

    И что тут можно сделать? Напрашиваются такие варианты:

    1. Создать канал, добыть данные о нём, потом этот грохнуть и создать другой, уже "правильный".

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

      К тому же придётся повторять это "грохнуть и пересоздать" при -- возможной при реконфигурации/рестарте сервера -- смене данных.

    2. Дать возможность менять тип канала "на ходу".

      Типа самое элегантное. Но некий вопрос -- насколько это реализовабельно?

      Ведь dtype,nelems передаются dat-плагиновым методам _new_chan().

      • Ну, _d_cx ими не пользуется. Как и _d_insrv с _d_local и _d_dircn.
      • Хоть _d_v2cx пользуется, но на него можно забить, ввиду отмирания
      • А вот _d_vcas реально пользуется (без них вообще не сможет).

        Но они используются (точнее, сейчас вообще лишь dtype) только для интерпретации получаемых от сервера данных, а не для хранения чего-нить в своих внутренних структурах.

        Так что вполне могут быть изменены "на ходу", если добавить такой метод.

      Остаётся вопрос о потребностях будущих _d_epics и _d_tango.

    3. Иметь прямо в cda какой-нибудь API для получения информации о канале, не создавая объекта "канал".

      Это вообще непонятно как реализовывать в модели cda, сделанной в объектной парадигме, а не в процедурной.

    Итого: наиболее разумным выглядит вариант (b) -- возможность динамической смены типа. Только надо будет при такой операции сбрасывать всё запомненное -- sndbuf, (возможно) кванты и прочие dtype-зависимые данные. И тут еще засадка: поскольку пакет RESOLVE отправляется ПОСЛЕ прочих информационных (Strs, RDs, FrAg, и Quant!), то после сброса ничего нового уже не придёт! ...с другой стороны -- а разве кванты не присылаются сразу в аппаратном типе?

    28.08.2018: пожалуй, можно сделать. Технология такая:

    • Надо добавить dat-плагинов метод "сменить тип".

      Но также надо уметь понять, поддерживается ли сие действо. А, поскольку у тех, кто это допускает, в основном метод будет ничего не делать, то просто указывать 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()
      • Проверяет, что тип -- REF_TYPE_CHN, иначе -1/EINVAL.
      • В конце -- сбрасывает буфер отправки путём snd_rqd=0, а также устанавливает текущее значение в "нету".
      • А вот собственно ПЕРЕКЛЮЧЕНИЕ -- пока нет, ибо неясно, как бы его сделать корректно: ведь оно состоит из ДВУХ шагов, делаемых разными частями -- cda_core и dat-плагином; и обломиться может любая из частей, что мешает сделать переключение атомарным.

        Как выкручиваться?

        1. Можно переделать метод set_type() на "2-стадийный" (передавая ему параметр "стадия"): на 1-й стадии только проверять, а на 2-й уже реально переключать. Чтобы последовательность: 1. set_type(проверка); 2. переключение в cda_core; 3. set_type(переключение).

          ...хотя всё равно это не атомарно -- вдруг set_type()'ово переключение обломится?

        2. Попробовать сделать "предварительную" часть переключения в cda_set_type() атомарно-безопасной: ну буфер аллокировать, если надо, и всё (это нерадостно, но несмертельно). А прописывание новых свойств -- уже после успешного set_type().

        Больше нравится, конечно, вариант (b), но надо б еще помозговать.

    • Кстати, некоторым вопросом является: а что, если некий канал был зарегистрирован несколько раз (БЕЗ флага PRIVATE), и при этом частью проверки на идентичность было совпадение dtype,max_nelems, а потом тип меняется?

      Насколько такое изменение -- сделанное одной точкой в рамках контекста-юзера -- будет корректно в отношении прочих?

    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, то обламываемся (и да -- если буфер к этому моменту увеличился, то он таким и останется; ну и фиг с ним).
      • А раз всё OK -- то сохраняем новые dtype,max_nelems,usize.

    Проверять будем на epics2cda.

    26.05.2023: на вид -- вроде да, работает. Проверялось путём запуска сервера с изменением размера текстового канала: 100,200,100 -- и функционировало предсказанно: при увеличении размера -- растёт, при уменьшении -- остаётся как есть.

cda_core:(ex-cda_api:)
  • 08.09.2009: модуль начал наполняться функциональностью.

    09.09.2009: сделана базовая инфраструктура -- в первую очередь менеджмент массива соединений, плюс уже доделаны cda_new_srvconn() и cda_add_srvconn_evproc()+cda_del_srvconn_evproc().

    Теперь дело за плагином.

    14.09.2009: и плагин уже более-менее готов, теперь дело за frontend'ом в сервере.

    Пара замечаний по результатам реализации:

    1. argv0 и server-spec плагинову методу newconn() НЕ передаются, а он их может получить у функций-аксессоров cda_argv0_of() т cda_starg_of(). Резон -- поскольку эти данные будут требоваться не только при начальном соединении, но и при reconnect'ах; и пришлось бы организовывать их хранение в плагинах; а зачем -- раз всё и так уже хранится.
    2. Реализация взаимодействия cda_api<->плагины сильно напоминает подход сервер<->драйверы.
      • Во-первых, приватная структура аллокируется "хозяином" -- в данном случае cda_api.
      • Во-вторых, метод newconn() возвращает не 0/-1, а "статус" -- ERROR=-1,NOTREADY=0,OPERATING=+1 (в ТОЧНОСТИ, как у драйверов), плюс потом, по мере [не]готовности соединения, плагин может указывать его состояние при помощи cda_dat_p_setstate().
      (Ну оно и понятно -- плагины играют в cda ту же роль, что драйверы в сервере, а уж модель reconnect'ов -- так и вовсе была в своё время в CXv2 сделана в cm5307_drv.c по образу cda.)

    13.08.2012: внедрён SLOTARRAY-GROWING вместо былого собственного GetSrvconnSlot().

    22.10.2012@Снежинск-каземат-11: и Cblist переведён на GENERIC_SLOTARRAY_DEFINE_GROWING(). Заодно поле _used устранено (как более не использующееся).

  • 24.03.2014: всё дальнейшее касается уже новой версии, "на основе контекстов".
  • 16.04.2014: по-хорошему, надо было нынешний cda_api.c называть cda_core.c.

    31.08.2014@дома: да, переименован в cda_core.c.

    Битым текстом:

    • Это -- ЯДРО cda, включающее в себя как API для клиентов, так и взаимодействие с реализаторами (и управление ими).
    • И еще менеджмент контекстов, каналов (dataref'ов) и серверов (sid'ов)
    • Идеологически и технологически оно очень похоже на сервер: средний уровень между клиентами ("верхними") и реализаторами ("нижними").
  • 02.08.2014: (в "dat_p-API") just for record: к cda_dat_p_new_chan_f добавлены параметры dtype и nelems.

    Формально они скорее всего не особо будут востребованы, но для полноты/симметрии интерфейса их присутствие крайне желательно.

    06.08.2014@вечер-пляж: а вот как раз для cda_d_vcas'а они будут востребованы: ведь сам протокол VCAS вообще никак не типизирует данные, там тупо строки.

  • 05.08.2014: добавлены простые информационные аксессоры cda_dtype_of_ref(), cda_nelems_of_ref(), cda_current_nelems_of_ref(). В основном в интересах cdaclient.
  • 05.08.2014: и можно уже преспокойно делать API cda_status_*() -- с введённым на прошлой неделе NthSid/sids_list оно становится straightforward.

    05.08.2014: да, и они уже сделаны. Несколько замечаний:

    1. Названия функций чуть отличаются от v2'шных.
    2. Ключевое отличие: возвращается -- cda_status_srvs_count() -- именно КОЛИЧЕСТВО sid'ов, а не номер последнего. Причина -- иная модель, больше нет выделенного "main" sid'а (его роль исполняет контекст).
    3. Имена состояний сохранены, но переупорядочены: теперь номер тем ниже, чем состояние "хуже" (в v2 было ровно наоборот). Реально это упорядочение никогда не использовалось, так что вряд ли на что-то повлияет.
    4. В настоящий момент отдаётся только 2 состояния: DISCONNECTED (при CDA_DAT_P_NOTREADY) и NORMAL (при CDA_DAT_P_OPERATING).

      Потому как ни FROZEN cda никак узнать не может, ни ALMOSTREADY даже в самих dat-плагинах пока не фигурирует.

      ...и CDA_DAT_P-статусов-то таких нет -- надо будет вводить.

    5. И даже cda_strserverstatus_short() наличествует; хотя вряд ли понадобится.

    02.12.2015: да, API проверен на cx-starter'е -- всё работает.

  • 15.08.2014: кстати, а поддержку CXCF_FLAG_DEFUNCT (при cur_age>fresh_age, см. за 09-07-2007) кто будет делать -- Пушкин?

    Явное место ей в cda_dat_p_update_dataset.

    19.08.2014: да, сделано.

    1. Плагины указывают fresh_age при помощи cda_dat_p_set_fresh_age(), в МИЛЛИСЕКУНДАХ.
      • Оно -- если положительное -- переводится в cx_time_t, а если <=0, то уставляется ri->fresh_age_specified=0, тем самым отключая определение DEFUNCT.
      • (Чисто на будущее: можно будет сделать и возможность сразу указывать в cx_time_t вместо миллисекунд -- вдруг когда понадобится fresh_age свыше 2000 секунд (т.е., получаса).)
    2. Проверка на старость делается в cda_dat_p_update_dataset().
      • Введено правило, что драйвер может указывать timestamps==NULL, тогда в качестве timestamp'а каналу проставляется текущее время и проверка старости не делается.
    3. Для работы с 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).

    4. В cda_d_v2cx.c:
      • Всем каналам при регистрации указывает fresh_age=5s.
      • Отдаются пометки времени, равные:
        1. При age=255 (MAX_TAG_T): 1с (т.е., бесконечно давно).
        2. Иначе текущему времени минус age, причём "age" корректно отскалирован с учётом cycle_size, ...
      • ...для чего SuccessProc() этот cycle_size добывает.
      • НЕ СДЕЛАНО пока посинение при обрыве соединения и при долгом неприходе данных.

    01.09.2014: уже сделано и при обрыве соединения, и при долгом неприходе данных.

    • Определение необходимости посиневания -- в cda_d_v2cx.c, скопировано из v2.

      (Заодно, кстати, и пингование добавлено.)

    • Реализация посинения -- 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():
      1. в SendOrStore() (при отправке "сразу" без конверсии)
      2. и CallSndData() (т.е., тоже в момент отправки).
    • Сбрасывается в cda_dat_p_update_dataset().
    • В cda_process_ref() добавлена монструозного вида проверка (4 уровня вложенности if()'ов, с дублированием и утроением кода внутри). Особой оптимизации нету :)
      • Главный "косяк" -- что gettimeofday() вызывается для каждого канала заново, хотя достаточно было бы 1 раз на всё.
      • Но что радует -- эти расходы будут только в GUI-клиентах (Cdr-based), в консольных же утилитах оно при обычном чтении не используется (только при записи).

    После обеда: проверено посиневание при записи -- пашет. А по каналам чтения -- надо в CAN-драйверах указывать fresh_age.

    16.09.2015: кстати, а чё это с каналами чтения НЕ работает? Ведь cxsd_hw ставит им по умолчанию fresh_age={5,0}!

    Разбираемся...

    • Проделан труд по добавлению отладочной инфраструктуры -- теперь Chl_knobprops умеет показывать fresh_age, добаваемый от cda.
    • Оказалось -- почему-то никаких возрастов свежести нет, сплошные !specified даже на каналах чтения.
    • ...по ходу дела -- при помощи v2_cx, где всегда {5,0} -- обнаружен косячок: из curtime'а вместо ri->timestamp вычитался ri->fresh_age.
    • И, в конце концов, нашлась причина странного поведения -- autoupdated-каналам ставится fresh_age={0,0} (см. за 01-09-2015).

      После закомментировывания того зануления -- всё работает предсказуемо.

      После обеда: с "тем занулением" много отдельной возни, пока неоконченной -- всё в тамошнем разделе.

    Проверено также посинение по давно-не-обновляющести -- и оно работает.

    Вот теперь можно считать поддержку CXCF_FLAG_DEFUNCT полностью реализованной и закрывать раздел как "done".

  • 15.08.2014: а еще ведь OTHEROP...

    30.08.2014@дома: хоть и введена еще 19-08-2014 cda_dat_p_set_quant(), но:

    • Чё-то лень и мутновато делать ПОЛНУЮ поддержку OTHEROP на общем уровне.
    • Ясно, что оно должно б функционировать где-то внутри cda_dat_p_update_dataset(), и сейчас видна необходимость впихивать это в две точки: в (2) простое копирование (при совпадающих dtype и без r,d (phys_count==0)) и в (3) конверсию, но как-то некрасиво, а как именно чтоб красиво -- пока неясно.
    • Так сделать пока прямо в cda_d_v2cx.c, копированием из v2, а уж потом, при запиливании cda_d_cx.c, можно будет и обобщить.

    31.08.2014@дома: (начато вчера под полночь, доделано сегодня):

    • В cda_d_v2cx.c в DecodeData() скопированы "те" мозги определения OTHEROP'а, плюс дополнительный код для обнуления mdctr в FailureProc() и добычи кванта в cda_d_v2cx_new_chan(), для поддержки чего...
    • ...пришлось ввести stripped_cda_getphyschan_q_int() -- поскольку стандартная v2'шная cda_getphyschan_q() отдаёт уже double, скорректированный на r.
    • В datatree добавлены сервисные 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" действительно надо что-то думать.

      @вечер: а разве смысл "mdctr" -- не сравнение времён? Ну так с snd_time и сравнивать! (Он как раз в самом-самом конце перед NEXT_TO_UPDATE: сбрасывается.)

    • Единственное условие -- помимо селектора по dtype -- то, что скаляр: ri->max_nelems==1.
    • @на-работе-19:30: только собственно "orange intellect" запихивать надо не в точку складирования curraw (она еще ДО всех проверок типов), а после складирования/конверсии данных -- видимо, вообще в самый конец цикла, перед NEXT_TO_UPDATE:.

    25.08.2016: предварительная подготовка -- введение интерфейсов и протоколов, чтоб затем просто наполнить их функционалом:

    • Перво-наперво -- номера событий.
      • Используемые правила:
        1. Коды на всех уровнях делаем одинаковыми (cxsd_hw, cda, pzframe_data).
        2. Первичным источником является cxsd_hw, посему ориентируемся на него.
          • А у него событий меньше: только те, что генерятся драйверами.
          • Всякие же RSLVSTAT и LOCKSTAT -- для него просто синхронные коды ответа внутренних API; а CURVAL вообще лишен смысла -- просто подсмотр в current_val.

          Посему -- 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 -- данных о типе канала вообще нет, и взять их негде.

      • Поэтому к кванту ВСЕГДА должен прилагаться его dtype.

        Т.е., при передаче по сети формат будет как у данных, только с nelems===1 (и без флагов и времён).

    • Реализация в cda:
      1. Добавлено поле refinfo_t.q_dtype. По умолчанию всё нули и оно считается за CXDTYPE_UNKNOWN, т.е. неопределённым. Тем самым НЕ требуется "q_specified".
      2. cda_dat_p_set_quant() уже была, в неё только генерация события добавлена и q_dtype.
      3. cda_quant_of_ref() введена, она простая.
    • Глобальное: тип CxAnyVal_t переехал из cx_common_types.h в cx.h -- т.к. нужен и в сервере.
    • Реализация в cxsd_hw:
      • Декларация cxsd_driver.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"'ы всё же добавлены, для красоты.

    • 29.08.2016@CRJ-200/YC30-где-то-посередине-между-Нском-и-Кольцово: а в lib/rem/ сделано не было!

    30.08.2016@Снежинск-каземат-11: исправляем недоделанность: сделана пустая заглушка remcxsd_driver_v4.c::SetChanQuant().

    Дальше вопрос о протоколе:

    • Делать по вышеприведённому проекту (передавать точно по размеру данных) или фиксированное поле CxAnyVal_t; кстати, и CX-протокола тоже касается.
    • С одной стороны, учитывая выравнивание по границе 8 и 16 байт -- проще фиксированно.

      С другой -- ведь и dtype надо передавать, а он 1 байт; вот и получится передача ВСЕГДА по 8+8 байт (в ОБОИХ протоколах). А если б "переменно" -- то где-то могло бы экономиться. ...но вряд ли -- dtype-то должен быть первым, а после него выравнивание до границы 8 байт (ибо само значение может требовать такой кратности).

    • Итого -- делаем ФИКСИРОВАННО.
      1. Файлы протоколов:
        • remdrv_proto_v4.h::remdrv_data_set_quant_t
        • cx_proto_v4.h::CxV4QuantChunk()

        В обоих случаях для собственно данных сделано поле

        uint8 q_data[8]; // !!! sizeof(XaAnyVal_t)
        Т.е., протоколы рассчитаны на работу только с типами до 8 байт размером.
      2. Реализация на ветке remdrv: SetChanQuant() дозаполнена.

        Там стоит проверка, что размер не более 8 байт. Для неё используется CHECK_LOG_CORRECT(), более нигде в v4 не применяемая.

    31.08.2016@Снежинск-каземат-11: далее...

    • продолжение "фиксированно":
      1. remdrv.c::ProcessInData() -- ветка REMDRV_C_QUANT наполнена кодом. В т.ч. проверкой, что размер по q_dtype не превышает размера q_data[].
      2. cxsd_fe_cx.c:
        • Добавлено мониторирование QUANTCHG и реакция на него в MonEvproc() -- дёрганье SendAReply() со свежесделанным chunk-генератором...
        • PutQuantChunkReply() -- сделан по образу FrAg'а, но:
          1. С дополнительной проверкой на размер.
          2. Пока БЕЗ корректной конверсии порядка байт -- тупо копируется 8 байт из chn_p в пакет.
        • Также он дёргается по CXC_SETMON и (возможно, излишне) по CXC_RESOLVE.
      3. cxlib_client.c:
        • cxlib.h:
          1. Добавлен код CAR_QUANT.
            • Во-первых, пришлось раздвинуть набор посрединке -- нехорошо; пора б уже разбить на диапазоны.
            • Во-вторых, подправлена не трогавшаяся как минимум с 25-11-2013 _cx_carlist[], давно уже некореллировавшая с кодами.
          2. Info-описатель cx_quant_info_t
        • Собственно реакция на QUANT async_CXT4_DATA_IO().
      4. cda_d_cx.c: реакция в ProcessCxlibEvent() на CAR_QUANT -- вообще самое простое.

    Итого: вся цепочка передачи от драйверов до cda сделана (хоть пока не протестирована), теперь осталась собственно реализация "orange intellect" (ну и нашпиговывание драйверов указаниями квантов).

    13.09.2016: цепочка-то вся, а вот действия -- НЕ все.

    Вопрос в том, что должно быть по умолчанию.

    • Если прописывание+отдача указанного кванта сделана, то умолчание -- нет, в результате нуления стоит CXDTYPE_UNKNOWN, что должно символизировать отсутствие кванта.
    • Но это не очень хорошо -- например, для noop-каналов ("почтовых ящиков") и симулируемых получится отключение механизма OTHEROP.
    • Пусть по умолчанию прописывается q_dtype=dtype, а уж сам квант пусть остаётся нулевым -- при сравнении "delta>=q" подсвечиваться будет любое изменение/несоответствие.
    • Если же какой-то драйвер действительно захочет отключить пооранжевение, то пусть указывает CXDTYPE_UNKNOWN явно (хотя кому б это могло захотеться?...).

    Собственно, действие очень просто -- в CxsdHwSetDb() добавлено chn_p->q_dtype=chn_p->dtype.

    Таким образом, для вариабельных каналов -- у которых тип UNKNOWN -- будет проставляться q_dtype=CXDTYPE_UNKNOWN, эффективно отключая оранжевение.

    И в cdaclient добавлена возможность печати квантов -- для отладки.

    Кстати, во всех рассуждениях выше было забыто, что сравнивать надо не только с "предыдущим", но еще и при записи делать уставленное этим "предыдущим".

    14.09.2016: код для определения OTHEROP в cda_core.c сделан.

    • Логика скопирована с v2, с соответствующими модификациями:
      • Непосредственно определение расхождения разделено на 2 части:
        1. Определение превышения дельтой кванта -- в зависимости от dtype.
        2. Типо-независимые действия по факту превышения.
      • Поскольку скопировано, то добавлены поля physmodified (с константами MOFIDIED_NNN) и isinitialized.

        Для удобства они, вместе с user_raw (реинкарнация user_code) помещены в под-структуру orange.

      • Механизм отправки, вследствие плагиновости, сильно отличается от v2'шного, но всё же чётко вырисовались 2 точки, в которых происходит изменение physmodified:
        1. :=MODIFIED_USER: при складировании для отправки в StoreData4Snd() либо сразу в SendOrStore() при "отправке сразу" (в точке ПЕРЕД оной).

          Этот делается одновременно со складированием в user_raw.

        2. :=MODIFIED_SENT: в CallSndData() либо в SendOrStore() при "отправке сразу".
        3. При ошибке отправки ставится MODIFIED_NOT.
      • При "неподходящести" значения -- не-скаляр либо dtype!=ri->dtype -- также просто ставится MODIFIED_NOT.
    • Пока НЕ сделано сравнение "если пришедшее значение старее, чем сделанная запись" (по snd_time).
    • А теперь о том, почему всё в результате неработоспособно:
      1. Вылезла одна ма-а-аленькая, но неприятная проблема: если в v2 обновление происходило каждый цикл и флаг OTHEROP на уровне cda вспыхивал одноразово, то в v4 обновления происходят только по изменениям, посему флаг OTHEROP остаётся навечно. И ручки на экране так и остаются навечно оранжевыми...
      2. И еще: у нас ведь Cdr_treeproc каналы по умолчанию регистрирует как CXDTYPE_DOUBLE, так что и "отправка" делается именно float64-данных, а не int32. Т.е., в user_raw сохраняется число совсем другого типа, чем получается в curraw, и сравнивать их попросту никак.

        ...а мы пытаемся сравнивать. И ведь union же, поэтому сравниваем что-то совсем не то ("мусор"), и OTHEROP зажигается всегда.

    Засим бит временно домножаем на 0.

    15.09.2016@утро-дома: в порядке размышления над вылезшими вчера косяками:

    1. Т.е., проблема "зависающего" OTHEROP в том, что он только выставляется, но никогда и никем не сбрасывается. В то время как в v2 сбрасывался "по циклу".

      Т.е., вот если бы кто-то когда-то (через некоторое время?) его б сбрасывал?

      Сбрасывать после чтения -- нельзя, т.к. читателей может быть более одного.

      Взводить по 1-секундному таймеру на каждый канал -- тоже перебор.

      Идей просматривается 2:

      1. Перевесить 5-секундное горение OTHEROP'а с datatree на cda. Как?
      2. Сбрасывать у всех пооранжевевших по общему сигналу от соответствующего server-connection'а.

        На роль этого сигнала подошел бы "начало цикла": к этому моменту как раз произведена обработка всех зависящих от sid'а ручек.

        • В CXv4 таковой сигнал есть, надо только его передавать.
        • Для прочих протоколов надо бы ввести возможность указывать "не надо OTHEROP'ить".

      15.09.2016@на-работе-после-обеда: да что за бред?! Дополнительные сигналы/события какие-то, и неработающесть в других протоколах... Всё ж проще: сбрасывать надо сразу ПОСЛЕ рассылки события CDA_CTX_R_CYCLE! В этот момент как раз обновления все проделаны.

    2. DOUBLE'овость: проблема усугубляется тем, что user_raw сохраняется без dtype, и молча предполагается, что он совпадает с q_dtype и curraw_dtype (чьё равенство проверяется), а это не так.

    15.09.2016: продолжаем движение:

    • Добавляем специфицирование квантов в драйверы.
      • CAMAC: там претендент пока только c0642.

        В нём, кстати, было забыто SetChanRDs() -- добавлено. Как и в c0609 и c0612 (и ныне ушедшее на пенсию управление V1000 так и работало -- с полностью скопированными v2'шными калибровками одним-числом).

        ...и некоторая неясность есть с frolov'ыми -- например, frolov_d16; как минимум RDs там точно забыто. Доделаем при использовании.

      • CAN: ставим q=305 только каналам OUT и OUT_CUR, но НЕ каналам OUT_RATE (как оно было в v2; но они-то искусственные и никак не квантуются).
    • И еще одна идеологическая проблема обнаружилась: сравнение имеет вид
      abs(user_raw-curraw)>=q
      Но в v4, в отличие от v2, НЕ делается при старте модификация "if(q==0)q=1", а с нулевым квантом даже условие abs(0-0)>=0 всегда будет истинным.
    • Продолжение размышления на тему "сбрасывать надо сразу ПОСЛЕ рассылки события CDA_CTX_R_CYCLE":
      • Неприятно, конечно, делать для этого цикл по ВСЕМ refs_list[refs_allocd], но какие-либо оптимизации тут бессмысленны (cda ведь не знает о readonly'вости каналов и делает проверку всем);
      • Спасает то, что в большинстве программ их не более нескольких тысяч.

        Исключение -- cx-starter, в котором ВСЕ, от всей установки; но вот для него при надобности можно сделать оптимизацию-исключение -- отключение оранжевения.

    16.09.2016: делаем.

    • Всё тривиально, и оно работает -- оранжевость на экране горит 5 секунд.

      Итого -- первая проблема решена. Неясно, как с оптимальностью, но работает.

    • Параллельно вылез маленький косячок с CHOICEBS'ами: для корректной работы RGSWCH'ей (т.е., канал чтения не совпадает с каналом(ами) записи) должен быть взведён флаг DATAKNOB_B_IGN_OTHEROP.
      • Но его нет -- просто нигде не указывается.
      • У choicebs_knobplugin'а в v2 это был спецификатор "#!", он указывался в common_elem_macros.h::RGSWCH_LINE*().

        А в v4 -- options-флаг ign_otherop, не используемый нигде.

      • Но разумно будет сделать /-ключик /ign_otherop. Так и делаем:
        1. В Cdr_via_ppf4td.c::CONTENT_fparser() понимание ключика добавлено.
        2. Из MotifKnobs_choicebs_knob.c упоминания убраны.

      Далее /ign_otherop вставлен в subsys_magx_macros.m4::RGSWCH(), и в основной массе проблема исчезла.

      Теперь нужно его подобавлять в прочие индивидуальные места. Что вроде тоже сделано.

    12.03.2017: оказалось, что эта недоделанная поддержка OTHEROP обходится очень дорого: cx-starter'а потребляет неприлично большое процессорное время.

  • 29.08.2014: cda_set_dcval() переведена с cda_snd_ref_data() (пока пустой) на cda_process_ref(). Смысл -- она используется в cda_f_fla.c::proc_PUTCHAN(), и оно раньше просто не работало.
  • 11.01.2015: сделаны функции для чтения/записи данных произвольного размера/типа (а не только double, как раньше).

    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.
  • 11.01.2015: параметр n_items у cda_add_chan() переименован в max_nelems.
  • 10.04.2015: реализуем поддержку CXCF_FLAG_NOTFOUND.

    10.04.2015: по пунктам:

    1. cda_add_chan() при результате CDA_DAT_P_NOTREADY проставляет FLAG_NOTFOUND.

      Таким образом, изначально канал будет чёрным, а при первом же обновлении данных приобретёт "правильный" вид.

      ...да, это не вполне красиво, т.к. цвета JUSTCREATED у таких каналов вообще никогда не будет. Но пока лучше ничего не придумано -- как будет, так сделаем. ...Если совсем будет мешать -- можно вообще конкретно ЭТУ логику будет отключить.

    2. cda_dat_p_set_hwr(): если hwr<0, то тоже становится NOTFOUND.

      И будущий "cda_dat_p_unlink_chan()" ("drop_sid") -- отвязывание от sid'а, нужное для миграции каналов -- должен будет делать то же самое.

    3. Добавлена cda_dat_p_set_notfound() -- чисто для проставления флага "NOTFOUND". Чтоб dat_p-модули могли по результатам резолвинга сказать "увы, не нашлось такого канала" (ПОТОМ, не при регистрации, а при получении ответа от сервера -- включая отсутствие ответа на UDP-broadcast).
    4. Общее: вместе с взведением 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-резолвлемый канал, то при убиении сервера (к которому коннектится в результате резолвинга) клиент получает уведомление "канал не найден!", по которому радостно вычёркивает этот канал из списка мониторируемых (а если он был единственным -- то и завершается).

    • Причина в том, что "ненайденность" может быть 2 видов:
      1. Перманентная. Именно она и интересует cdaclient'а и прочие утилиты, и только она и была в момент реализации поддержки CXCF_FLAG_NOTFOUND и создания самих утилит.
      2. Временная. Она появилась с введением в строй UDP-резолвинга.
    • Но в API/событиях эти 2 вида никак не различаются!
    • ...а надо!

    Делаем:

    • В 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.
    • Значение новодобавленного теперь
      1. передаётся через info_ptr=lint2ptr(rslvstat),
      2. взведение CXCF_FLAG_NOTFOUND производится только при rslvstat!=CDA_RSLVSTAT_FOUND.

      Замечание: СБРОСА этого флага по FOUND не делается. Да и сам вариант FOUND покамест никем не отдаётся.

    • Соответственно, cda_d_cx.c при переходе во "временно NOTFOUND" указывает CDA_RSLVSTAT_SEARCHING.
    • ...а утилиты теперь считают фатальным только вариант "RSLVSTAT_NOTFOUND", всё прочее игнорируя вовсе.
    • 08.06.2018: адаптация прочих "клиентов":
      • В vdev.c обработка пустая.
      • Драйверы mirror_drv.c и trig_read_drv.c -- там просто диагностика в лог.
      • В fastadc_data.c аналогично.
      • Зато pzframe_data.c::PzframeDataEvproc() помечает в cur_data[].rflags (исполняет функции Cdr'а?) и рассылает PZFRAME_REASON_RSLVSTAT дальше, и он ловится PzframeGuiEventProc()'ем, почерняющим ручки.

        Вот в этой парочке поставлено условие, что делать свои действия только при !=CDA_RSLVSTAT_FOUND.

    P.S. Записывал сюда дольше, чем делал.

    P.P.S. Да, проверено -- проблема так исправлена.

  • 16.09.2015: добавлен простой аксессор cda_fresh_age_of_ref(); нужен чисто в отладочных целях.

    Помимо самого значения, возвращает +1 при указанности и 0 при не-указанности.

  • 19.12.2015: реализовано событие UPDATE и для формул. Сделано в интересах formula_drv.c, подробности в разделе по драйверам за сегодня.
  • 03.01.2016@утро-душ: нужен API для слежения за статусом формул, плюс event-type для уведомления о завершении. Чтоб MotifKnobs_cda_scenario_knob.c мог быть покорректнее.
  • 12.05.2016: cx-starter'у нужна возможность добавлять к контексту серверные соединения -- просто серверы по имени, БЕЗ каналов (имена каналов могут быть не только неважны, но даже и неизвестны).

    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 сама), либо не делаем ничего").

  • 24.05.2016: добавлен параметр 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.
    • При поиске ранее зарегистрированного канала по-прежнему опции не сравниваются, а игнорируются -- кроме PRIVATE.
    • В cda_d_cx_new_chan() флажок уже используется.
    • И на всякий случай зарезервирован флаг CDA_DATAREF_OPT_rsrvd26=1<<26 -- если захочется ввести "OPT_ON_CYCLE" (типа для оверрайденья "@u"?).

    В порядке наблюдения: ситуация смахивает на ту, что была в CreateSimpleKnob(), когда пришлось ввести options для передачи принудительного SIMPLEKNOB_OPT_READONLY.

  • 15.07.2016: Федя желает иметь событие также и по приходу CURVAL, а не только NEWVAL -- чтобы в любом (его) клиенте смочь прочитать текущее известное значение канала (пусть и измеренное чёрт-те-как давно) и что-то там с ним сделать (например, сделать измерение, находясь в пультовой, а потом придти к себе и проанализировать его (прочитав то же самое текущее)).

    16.07.2016: собственно, это-то несложно:

    1. Вводим код события CDA_REF_R_CURVAL (=11, чтобы оставить =10 для переноса RSLVSTAT'а).
    2. В cda_dat_p_update_dataset():
      1. Одна "call_info" заменена на пару -- update_info и curval_info, чьё содержимое различается значениями полей reason и evmask.
      2. В зависимости от решения "считать ли за обновление" генерится либо одно событие, либо другое (а не просто подавляется событие, как раньше).
      3. ...ну и собственно услович "считать ли..." изменено с инверсного (когда пропускать событие) на прямое (когда update).

    Пока не проверяем, но не-работать там нечему, так что "done".

    02.08.2016@утро-дорога-на-работу-мимо-ИХБФМ: ага, но только проверить-то действительно никак -- т.к. cdaclient не умеет. Надо бы в него ключик добавить -- "печатать CURVAL'ы".

    Десятью минутами позже: да, сделал (ключ -Dc) и проверено -- да, теперь она выдаёт текущее значение.

  • 16.11.2017: надо делать поддержку being_processed/being_destroyed в cda_core. Причины -- в разделе о гроханьи.

    Что оно должно в себя включать:

    1. Собственную реализацию -- откладывание удаления контекста до окончания процессинга.
    2. Возможно, дать dat-плагинам функцию для узнавания "а не грохается ли сейчас контекст" -- чтобы не выполнять лишнюю работу, вроде отписывания.
    3. Поддержку откладывания освобождения pdt_privptr'ов dat-плагинами -- чтобы уже и они могли откладывать прикрытие серверов.

    Замечание: да, такая прямолинейная реализация откладывания драйверами -- просто отдача им права сделать free(pdt_privptr) -- в некоторых случаях аукнется.

    • А именно: если такой "освобождённый" сервер продолжит зомби-жизнь, вызывая cda_dat_p_update*() и прочие функции API, а... ...сразу после "удаления контекста" клиент регистрирует новый контекст, и тот получает те же sid'ы? Ведь тогда "старый" начнёт портить жизнь новому экземпляру.
    • Может, не делать в таком случае удаления всех ресурсов, а запоминать факт "отложенности", и чтоб потом dat-плагин, когда "освободится", вызвал бы дополнительный метод -- "доделай устранение"? Тогда и размытия ответственности не будет.
    • Следствия:
      1. И контекст тоже НЕ надо освобождать, если хоть один из его серверов еще "занят".
        • Это, кстати, избавит от титульной проблемы -- что новый контекст родится с тем же cid'ом, что был старый.
        • Т.е., выполнять "reference-counting", где в качестве "reference" выступают sid'ы.
      2. Следствие из п.1: освобождение должно делаться НЕ в RlsCtxSlot()/RlsSrvSlot(), как это постулировалось изначально 09-01-2016.
        • Точнее, ТАМ должны освобождаться принадлежащие ctxinfo_t/srvinfo_t ресурсы, ...
        • а вот вызов del_srv() должен производиться непосредственно из cda_del_context(), с последующим подсчётом "сколько осталось недобитых серверов", и если >0, то откладывать освобождение cid'а до снижения этого числа до 0.
        • ...и: RlsSrvSlot() надо вызывать сразу после успешного del_srv() (либо в API-функции-"завершаторе", но то уж понятно).

    16.11.2017: предварительные действия:

    • cdaP.h:
      • Введены enum-константы 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: основная работа:

    • Вводим dat_p-API-вызов 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() и прочие.

      "Интеллектуальность" заключается в том, что:

      • Оно проходит ForeachSrvSlot()'ом по всем серверам контекста, вызывая им del_srv(), и если в ответ получает SUCCESS, то делает RlsSrvSlot(), а в противном случае зажигает ему being_destroyed=1.
      • И заодно итератор считает количество "занятых" серверов. И если суммарное количество ==0, то делается RlsCtxSlot(), а иначе это откладывается до cda_dat_p_del_server_finish().
    • А ci->being_destroyed в cda_del_context() выставляется всегда!
    • В плагинах изменения очень простые:
      1. В cda_d_*_del_srv() в случае "не сейчас" делается возврат кода CDA_DAT_P_DEL_SRV_DEFERRED вместо SUCCESS.
      2. В точке реального освобождения privrec'а вставлен вызов cda_dat_p_del_server_finish().

      cda_d_vcas и cda_d_cx -- добавить всю архитектуру с "Destroy*Privrec()".

    • Несделанное: 1) ++/-- being_processed и при --,==0 проверка being_destroyed с вызовом TryToReleaseContext() и отвалом; 2) НЕ-вызов del_chan() в RlsRefSlot(), а вызов его только из cda_del_chan(); 3) НЕ-вызов del_srv() в RlsSrvSlot(); ЕЩЕ ЧТО-НИБУДЬ оттуда же?

    20.11.2017: продолжаем:

    • Соображение: уже после выставления ci->being_destroyed=1 и затем вызова TryToReleaseContext() может продолжаться какая-то активность со стороны серверов, так что может сложиться ситуация, когда в ходе "процессинга" значение being_processed опустится до ==0, и, при взведённом being_destroyed, опять будет вызвана TryToReleaseContext().
      • Вопрос: не чревато ли это чем-нибудь? Из-за повторных действий?
        • На вид -- вроде бы нет, т.к. TryToReleaseContext() пытается что-то делать ТОЛЬКО с не-занятыми серверами.
        • Но незанятость оно проверяет вызовом серверова del_srv(), а как тот отнесётся к повторному вызову -- вопрос.
          • Нынешние-то вроде бы должны корректно:
            • Т.к. просто проверяют свой being_processed, и если он взведён, то просто вернут DEFERRED.
            • А когда он сбросится, то сам код сервера должен вызвать _del_server_finish() и далее этот сервер существовать перестаёт, так что не должен более попасться итератору в TryToRelease'е.
          • Но мало ли что вылезет в будущем.
      • Так что по-хорошему от такого надо уметь защищаться. Чтоб условие "being_processed==0 && being_destroyed" повторно не срабатывало, а только при первом разе.

        Как это можно сделать:

        1. Чтоб being_destroyed бывало не только 0 или 1, а еще и 2 -- "разрушение реально началось".
          • Не неудобно ли -- потребует лишних проверок при взведении being_destroyed?
          • Нет, НЕ неудобно, т.к. проверка на being_destroyed!=0 в cda_del_context() уже есть.
        2. Сделать дополнительный флаг -- вроде "being_destroyed_started", который и взводить при первом вызове TryToReleaseContext() по унулению being_processed, а при последующих вызовах уже нет.

        Но вариант (b) выглядит хуже: и лишний флаг, и условие сложнее -- проверять 3 разных флага, вместо замены второго сравнения на "being_destroyed==DESTROYED_REQUESTED" (REQUESTED=1, а STARTED=2).

      После анализа кода: ДА НЕТ ЖЕ, ФИГНЯ ЭТО ВСЁ!!!

      • Во-первых, можно постулировать, что пусть сервера ведут себя как положено и корректно отрабатывают del_srv(): при вызове в занятом состоянии возвращают DEFERRED и взводят у себя being_destroyed=1, а по processed:=0 завершают уничтожение.
      • Во-вторых, УЖЕ ведь есть 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); не хотелось бы (ибо кривовато), но вдруг что недоучитываю.

    • cda_core.c: добавлены ++/-- being_processed и при --,==0 проверка being_destroyed с вызовом TryToReleaseContext() и отвалом вокруг вызовов 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()'а ничего не стал:
      • Цикл удаления ref'ов -- просто никак нельзя: ведь оными ref'ами могут быть и формулы, и varchan'ы, да и ref'ы, так и не привязавшиеся ни к какому sid'у (формально такая возможность есть, хотя смысла подобного использования не видно и выглядит оно дурным стилем).
      • А цикл удаления sid'ов -- реально как бы уже не нужен, т.к. к моменту уничтожения контекста число серверов должно быть 0, но просто не тронут.
    • Добавляем архитектуру being_processed+being_destroyed и 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 -- и в них окружить "скобками" минимально возможные фрагменты.

    Это будут:

    • cda_d_vcas.c: ReconnectProc(), CycleProc() и ProcessFdioEvent().
    • cda_d_vcas.c: ReconnectProc() и ProcessCxlibEvent().

    Замечание: как в этих двух модулях, так и в троице предыдущих НЕ стоит никаких "скобок" вокруг вызовов cda_dat_p_*() в методах СОЗДАНИЯ -- _new_srv() и _new_chan(). Хотя они тоже приводят к вызову cda'шных callback'ов, могущих, теоретически, вызвать удаление.

    Итак -- сделано. Начинаем проверять.

    • Сходу: фиг, ничего не освобождается, ресурсы утекают (и память, и cid'ы, и ref'ы).
    • Оказалось, что косяк в cda_core.c, еще очень старый: TryToReleaseContext() пользуется полем ctxinfo_t.cid, но это поле никто никогда не заполнял, так что в нём всегда 0. Оно и пыталось освободить cid=0, коего нет.
    • Добавлено заполнение в cda_new_context() -- утечки прекратились.
    • Проверено на insrv::, local:: и dircn:: -- всё окей.
    • ...а вот на cx:: утечки есть!

      Получасом позже: а, нет -- нету!

      Точнее, они есть при "неправильном" использовании: в devlist'е стоял префикс просто "cx::" вместо "cx::localhost.0", поэтому создавались каналы-"сироты", без sid'а, никуда не привязанные и, соответственно, при удалении контекста не удалявшиеся, т.к. cda_core ни по серверу не может их "дёрнуть", ни по hwr (т.к. "сиротам" и cda_dat_p_set_hwr() не делается).

      30.11.2017: вообще добавлен кусочек защиты -- при не-w_srv ячейка освобождается и возвращается ошибка (оставим так до реального внедрения UDP-резолвинга). Утечки прекратились.

    • На vcas:: проверить, к сожалению, вряд ли удастся.

    Резюме:

    1. В "простом" варианте хитрозамудрённая архитектура "с возможностью удалять контекст прямо из его evproc'а" работает.
    2. Теперь надо проверять именно в варианте "из его evproc'а".
    3. ...и, по опыту разбирательства с утечкой в cx::, уже напрашивается делать UDP-резолвинг -- всё ведь давно ясно.

    29.11.2017: проверяем.

    • Для проверки в test_cda_del_drv.c::chan_evproc() добавлено, что когда он получает значение 12345 в канал 102, то делает cda_del_context().

      Причём проверка на номер канала сделана хитро: не просто ref==102 (тогда он пытался бы грохать в еще недорегистрированном состоянии -- получая начальное значение в момент регистрации), а "ref==me->refs[101]".

    • Первый результат -- фиг, не работает отложенное освобождение, оно просто не происходит.
    • Проблема нашлась быстро: в cda_core.c "закрывающая скобка" содержала кривое условие --
      ci->being_processed  &&  ci->being_destroyed
      вместо
      ci->being_processed == 0  &&  ci->being_destroyed
    • После исправления вроде стало отрабатывать.
    • Проверено (с помощью мини-скрипта, пишущего 12345 в канал каждые 0.15с) на протоколах insrv:: и cx:: -- память не утекает.

      Как проверить на dircn:: и local -- не очень ясно, да и не особо-то и нужно.

    Ну что -- аллилуйя, считаем, что работает?

  • 16.11.2017: надо б расширить список аргументов cda_dat_p_get_server() параметром options, чтоб иметь возможность указывать "не добавлять этот сервер в список серверов контекста.

    16.11.2017: м-м-м...:

    • Добавлен enum-флаг 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[] и не добавляются.
    • Поскольку для НЕпоказываемых серверов в CDA_CTX_-событиях нет смысла, то они теперь для серверов с si->nth<0 и не рассылаются.
    • Ну и все вызовы добычи сервера в dat-плагинах проапдейчены (сейчас повсеместно на NONE).

    28.11.2017@вечер-уходя-с-работы: кста-а-ати, у нас ведь имеется нерешенная (даже не осознанная) проблема, как создавать в cda_d_cx.c resolve-sid'ы -- ровно по одной штуке на контекст: ведь нужно какое-то имя, с которым бы cda_core сравнивало имеющиеся у контекста, а КАКОЕ, чтоб оно точно не перекрылось с именем сервера; 22-07-2016 предлагалось "с неким бредовым именем".

    А можно воспользоваться свежевведённым параметром options: зарезервировать в нём несколько бит (8? 16?) на "тип" сервера, и в find_or_add_a_server() считать совпадающими только сервера с таким же типом.

    Следствия:

    1. Значение options надо будет сохранять в srvinfo_t (для сравнения).
    2. Имя серверам-"резолверам" можно будет указывать любое -- да хоть "".

    29.11.2017@пешком-с-обеда-дома-около-ИПА: еще следствие:

    1. Удобнее было бы 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: всё проверено, всё прекрасно.

  • 30.11.2017: связанное с предыдущим: необходимо расширить интерфейс cda_dat_p_new_srv_f() еще одним параметром -- "тип сервера", который ему будет передаваться из того, что указано в соответствующей части options.

    30.11.2017: реализация:

    • Добавляем параметр в конец списка, называется srvtype.
    • Передаются методу только те биты options, что CDA_DAT_P_GET_SERVER_OPT_SRVTYPE_mask.
    • И методы всех cda_d_*.c дополнены (всем, кроме cda_d_cx.c, он указан как unused).
    • CDA_DAT_P_MODREC_VERSION_MAJOR традиционно не тронут -- смысла нет (по причине отсутствия динамически-подгружаемых dat-плагинов).

    Работает?

    Мысль "на вырост": а точно достаточно вот такого простого "тип сервера"? И не понадобится просто указатель (void*), для возможности передавать произвольные параметры?

    19.12.2017: да, проверена ж уже работоспособность, "done".

  • 01.03.2018: в RlsRefSlot() отсутствовало safe_free(ri->alc_phys_rds). Позор! Добавлено.

    И, кстати, с sndbuf аналогично (совсем ведь недавнее, блин!).

  • 07.11.2018: возможность навешивать evproc'ы на формулы -- задуманная 24-09-2015 и реализованная 18-12-2015 в интересах formula_drv.c (описано в разделе по "programs/drivers/") -- работает кривовато: UPDATE-событие, должное получаться в момент РЕГИСТРАЦИИ формулы, не ловится.

    А очень бы хотелось -- для trig_exec_drv.c, созданного для rfsyn'а (devlist-canhw-19.lst).

    07.11.2018: оно работает на "удалённом" железе, используемом на ВЭПП-5, а локально -- фиг: в момент регистрации формулы событие не приходит. Причин/аспектов тут туча (перечислены не в порядке важности, а просто по группам):

    • В драйверовых _evproc()'ах используется не переданный им ref (который заведомо валиден), а их поле из privrec'а (в момент ДО возврата из регистрации еще просто не заполненное).
    • В конкретно trig_exec'е сначала регистрируется формула-триггер, и лишь потом формула-исполнитель.

      Следствие: даже если б событие пришло, то в этот момент формула-исполнитель еще даже не существует -- выполнять НЕЧЕГО, реагировать на событие НЕЧЕМУ.

      Решение: регистрировать сначала формулу-исполнитель.

    • В cda_add_formula() навешивание evproc'а делается только в самом конце, когда возможное "событие UPDATE в момент регистрации" уже случилось ранее.
    • Как это можно было бы обойти: передавать cda_fla_p_create_t-создателям также и evmask,evproc,privptr2.
      • Некрасиво выглядит идея, да? Ведь нарушается принцип "разделения ответственности": создатели -- только создают, а менеджмент evproc'ев отдельно.
      • Плюс, событие придёт прямо в момент регистрации используемого формулой канала -- т.е., когда формула еще даже и не готова.

        Это, казалось бы, можно б было решать при помощи "отложенного вызова": на время регистрации взводить в формуловом privrec'е флажок "is_creating", и если приходит evproc при is_creating!=0, то вместо реального вызова просто взводить другой флажок, а по окончании (успешного) создания проверять его, и если !=0 -- вызывать.

        Хотя тут другие засады:

        1. Могут приходить разные типы событий, а не только UPDATE. И аккумулировать бы надо их все.
          • Решение придумывабельно: сохранять не булевский флаг "было событие", а МАСКУ, в которую OR'ить.
          • Неудобство: с событием приходит не маска, а reason.

            Это проблема невеликая: mask=1<<reason.

          • Но также придётся и вызывать по количеству накопленных битов в маске.

            Делать-то это несложно -- цикл сваять (да, порядок при этом нарушится; неприятно).

            Но засада: каждый вызов -- может быть "фатальным", после него клиентская программа может захотеть весь объект грохнуть. ...да, грохнуть еще не формулу, чей ref-id программе еще не вёрнут, затруднительно; это как бы отсрочка, но всё равно неприятно.

        2. Могут быть навешены РАЗНЫЕ evproc'ы -- какой из них вызывать?

          Впрочем, в момент СОЗДАНИЯ формулы может быть только один-единственный evproc, переданный там параметрами.

        Впрочем, ОСНОВНАЯ проблема -- даже не в этом, а в иной идеологии работы с формуловыми evproc'ами, из-за чего всё вышенаписанное просто "мимо кассы".

    • Но сама модель сейчас такова, что навешивание evproc'ев на формулу реально сводится к навешиванию их на используемые в OP_GETCHAN каналы.

      Очевидно, что для решения в соответсвии с описанными в предыдущем пункте мерами -- придётся сменить модель: чтобы список evproc'ев помнился прямо в объекте-формуле. То ли ею самой, то ли cda_core'ом (этот вариант явно лучше -- там уже вся инфраструктура есть).

    • Кстати, в cda_f_fla_p_destroy() НЕ делается удаление каналов в OP_GETCHAN/OP_PUTCHAN. Что, формулы удалябельны только вместе с контекстом?

    Какие обходные манёвры возможны сейчас:

    • С реально удалённым железом -- работать будет уже сейчас, т.к. событие обновления придёт гарантированно потом.
    • Можно попробовать ставить драйвер-использователь в devlist'е РАНЬШЕ используемого. Тогда "начальное вычитывание" произойдёт после инициализации использователя.

    Резюме: глядя на всю глубину проблем, напрашивается решение покамест пользоваться теми самыми обходными вариантами, а не тратить время на создания "решения общего плана".

  • 11.11.2018@пультовая, воскресенье, после обеда/вечер: оказывается, simple-интерфейсом -- 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: ну ёлки, да какого лешего!!! Надо разобраться да сделать.

    Разбираемся:

    • Simple-интерфейс -- он же для КАНАЛОВ, зарегистрировать через него формулу никак нельзя.

      Так что проблема реально и не стоит.

    • Кроме того, 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().

  • 13.11.2018: надо как-то уметь отлаживать/диагностировать процесс передачи данных через cda между клиентом и сервером.

    Побудительный мотив -- проявился странный косяк в еманофедином "роботе", управляющим работой Инжекционного комплекса. В некий момент он должен дёргать каналами управления Аккордов кикеров (конкретно четвёрка ic.{eKickers,pKickers}.{preKickU,kickU}.set, отображающаяся на ie_cac208.out0...out3) следующим образом (на примере eKickers.preKickU, он же out0):

    1. Обычно там стоит значение 1.5V.
    2. На долю секунды надо взвести в 4.0V.
    3. Потом обратно сбросить в 1.5V.
    4. ...и этот процесс повторяется с периодичностью в 3 секунды.

    Так вот: время от времени пункт 3 не выполняется -- уставка так и остаётся 4.0V. Федя утверждает, что у него всё в порядке и косяков нет. Я, со своей стороны, уверен, что у меня всё корректно и косяков нет (иначе такая проблема давно бы вылезла).

    Вывод: надо как-то уметь "трассировать" прохождение данных, чтобы ТОЧНО знать, что клиентская программа запись выполнила/нет.

    Только как и где это делать? Я в полной растерянности...

    14.11.2018@утро-дома: идея: ввести в dataref'овы options флаг "трассировать", и при его наличии выдавать на stderr диагностику в интересующих местах.

    14.11.2018: делаем.

    • Флажок назван CDA_DATAREF_OPT_DEBUG.
      • Он поставлен 23-м...
      • ...чтобы просто весь 2-й байт (0x00FF0000) можно было использовать для потенциально могущих потребоваться разных флагов для отладки разных аспектов.

        08.06.2020: блин, а толку-то -- с тех пор уже заведены CDA_DATAREF_OPT_PRIV_SRV=1<<22 и CDA_DATAREF_OPT_EXCLUSIVE=1>>21, оба во 2-м байте...

      • Соответственно, для пропущенного 24-го бита заведён символ CDA_DATAREF_OPT_rsrvd24.
    • Использование пока в единственной точке -- cda_set_dcval(). Туда пришлось внести собственный блок обращения к ri, чтоб было где проверять флаг.

      При его наличии -- простейшая печать имени (из ri->reference) и значения (форматом %8.3f).

  • 08.02.2019: аналогично пред-предыдущему пункту с "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().
    • А вот ЧТЕНИЕ -- посложнее: вначале куча проверок на корректность (что он REF_TYPE_CHN и CXDTYPE_TEXT), потом копирование в буфер с непревышением заданного размера.

      Кстати, при bufsize=0 единственным действием является возврат через len_p текущей реально длины строки.

    26.02.2019: проверил (сварганив тестик work/tests/test_sc.c) -- работает.

  • 11.06.2019: чисто для красоты: есть желание формализовать значения для 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: ЕманоФедя вчера затребовал возможности выполнять конверсию из сырых значений в физические БЕЗ сдвига -- т.е., не {R,D}-конверсию, а просто {R}-конверсию.

    03.12.2019: смысл -- ему нужно иметь квант прямо в физических величинах, а у нас квант в аппаратных ("инженерных"?). Он много ругался, какой я редиска, но квант-то делался в интересах cda, чтоб можно было проверять, совпадает ли реально прочитанное из железки значение с записанным туда (если отличие меньше, чем квант -- то совпадает).

    В принципе -- проблем ноль: аналогичная cda_rd_convert() функция cda_r_convert(). Сделана копированием и удалением кусочка " - rdp[1]".

  • 11.02.2020: обнаружилось, что 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'у не обязательно б было указывать тип канала, а он определял бы сам...".

  • 15.03.2020@дома-воскресенье: добавлена поддержка "приватных srvconn'ов" -- чтобы для указанного канала принудительно создавалось отдельное серверное соединение.

    Побудительным мотивом послужил ЕманоФедя, а конкретно то, что он пытался по некоему каналу (условно, 5Мбит/с) гонять соединение с траффиком большим (условно, 7Мбит/с), чем канал мог пропустить, и в результате большеобъёмные векторные каналы напрочь забивают маленькие скалярные -- они работают через общий сокет, так что пока не пройдут данные от векторного, то и скаляр стоит в очереди.

    А если б мочь регистрировать векторные каналы в отдельных srvconn'ах, то, возможно, скалярные каналы будут успевать бегать оперативнее.

    ...ЕманоФеде-то я посоветовал попробовать регистрировать векторные в отдельном контексте -- так разные srvconn'ы форсятся; но лучше иметь возможность просто указать явно.

    15.03.2020@дома-воскресенье: всё достаточно несложно и делается аналогично работе с давно введённым флагом CDA_DATAREF_OPT_PRIVATE:

    1. Подготовка:
      • Для клиентов введён CDA_DATAREF_OPT_PRIV_SRV=1<<=22.
      • Для "внутри-cda" -- 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.

    2. Реализация:
      • В 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) научен:

    • В качестве символа выбран '#' -- т.е., префикс @#:.
      • Да, это не очень хорошо, поскольку "решётка" (hash) -- символ начала комментария в shell'е.
      • Но интерактивные shell'ы вроде бы по умолчанию НЕ считают этот символ комментарием. По крайней мере, zsh -- у него есть отдельная опция INTERACTIVE_COMMENTS (ключ -k), позволяющая всё же "заставить считать комментариями".
      • Кроме того, в info по zsh сказано, что
        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.
        
        (bold мой). Т.е., если решётка стоит не сразу после пробела, то символом начала комментария НЕ считается. А в комбинации "@#..." она стоит совсем не первой после пробела.

        Сейчас проверил на обычном bash'е, вот таким скриптом:

        #!/bin/sh
        
        echo a b c #d e
        echo q w e# r
        
        вывод его --
        a b c
        q w e# r
        
        Т.е., да -- так и есть, использование решётки сравнительно безопасно.
    • И в help'е это тоже опубликовано, причём с предупреждением "(quote '#' in shell!)".

    Проверено на простейшем тесте -- cdaclient с парой каналов, у каждого из которых префикс "@#:" -- да, создаются отдельные соединения.

  • 06.04.2020: некоторый вопрос встаёт -- что делать, если клиент получит CEACCESS при первоначальном соединении (или потом, при очередном реконнекте)? Он ведь после этого так и останется неприконнекченным навсегда; даже если проблема была в некорректном cxhosts (забыли адрес добавить), и её исправили -- то что делать? Рестартовать клиента?

    06.04.2020: соображения на эту тему:

    • Завести дополнительный cda_dat_p_rec_t-метод "retry connect"?

      17.04.2020: а лучше сразу более общий "ioctl"?

    • Плюс вызов для cda-контекста "retry connect for all servers", плюс кнопка в GUI (или чтоб достаточно было кликать на лампочке сервера -- чтоб ledCB() генерил бы некое событие?).

      Вот только пока у cda_leds нет механизма передачи информации к своему нанимателю.

    • 17.04.2020: добавить специальный CDA_SERVERSTATUS_nnn для "permission denied"? Или реюзнуть CDA_SERVERSTATUS_FROZEN?

    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: предварительные действия:

    1. Добавлены типы cda_dat_p_chan_ioctl_f и cda_dat_p_srv_ioctl_f.
    2. К CDA_DEFINE_DAT_PLUGIN() добавлены параметры chan_ioctl и srv_ioctl плюс парочка rsrvd_1 с rsrvd_2.
    3. А вот к метрике пока ничего не добавлено и те 4 параметра "холостые".

    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: возвращаясь к данному вопросу.

    1. Давно стало ясно, что тогда, при внедрении концепции модулей, был допущен изрядный мисдизайн: надо было поля next и refcount запихивать в НАЧАЛО modrec, а лучше -- и вовсе прямо в cx_module_rec_t.
    2. Следствие: а вот СЕЙЧАС нужно новые поля вставлять НЕ перед next и refcount, а ПОСЛЕ них. Тогда сохранится бинарная совместимость, а уж cda_core сможет проверять версию ABI модуля динамически.

    23.07.2020: в продолжение:

    1. И при выполнении п.2 -- о бинарной совместимости -- можно даже НЕ продвигать 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(): да, вроде должны бы.

    Сделано, по вышеприведённому плану:

    • Новые поля (пока что за-#if'ленные) переставлены в конец.
    • Сделан -- также внутри #if'а -- второй вариант CDA_DEFINE_DAT_PLUGIN(), уже присваивающий значения этим 4 полям.

      (Увы, вставить #if в то же определение нельзя -- не поддерживает C/cpp #if'ы внутри макросов.)

    • В вызов srv_ioctl() вставлена проверка версии модуля -- что она не ниже 2.1.
    • ...и версия продвинута до 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: по здравому размышлению напрашивается наиболее прямолинейное решение:

    1. Эти #if'ы убрать, оставив только "новый" код.
    2. Продвинуть 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".

  • 12.09.2020: по просьбе Роговского (мыло от 01-09-2020, "внешний обработчик сообщений об ошибках в CDA") сделана возможность "перехватывать" сообщения от 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(), причём она возвращает старый обработчик.
    • ...да, напоминаю -- это всё объявлено в простом cda.h, так что пришлось и в него добавить #include<stdarg.h>.
    • Хранится указаьель на "перехватчик" в cda_dat_p_report_logger_hook, по умолчанию =NULL.
    • Собственно "работа" -- тривиальна: добавлен if(cda_dat_p_report_logger_hook)!=NULL, и тогда делается вызов указанного, а иначе -- обычный вывод на stderr.

    Проверено пока лишь, что БЕЗ уставки перехватчика всё работает как раньше.

    Замечание: перехват делается только для сообщений от cda_dat_p_report(), но НЕ для

    1. cda_ref_p_report()
    2. cxlib_report()

    хотя для полноты стоило бы и для них сделать.

    И, кстати, было бы осмысленно делать такой перехват в сервере -- чтоб редиректить эти сообщения в лог.

    12.09.2020@ванна, душ: а ведь если перехватывать такие сообщения в cxsd для выдачи их в серверный (точнее, ДРАЙВЕРНЫЙ) лог, то нужно префиксировать именем/devid драйвера-владельца. Для чего перехватчику сообщения нужно получать uniq.

    12.09.2020: (в районе обеда) добавляем uniq:

    • Добавляем uniq первым параметром к cda_dat_p_report_logger_t.
    • Самое муторное -- добыча uniq по sid'у, т.к. там делаются проверки на корректность sid'а.

    ...по-прежнему не проверено.

    12.09.2020: (после обеда) ну, гулять так гулять -- делаем ВСЁ!

    • Добавлен аналогичный API и для "ref_p_report" -- cda_set_ref_p_report_logger().

      Реализация там тоже аналогична, причём добыча uniq даже попроще.

    • Также сделано и для cxlib'а -- cxlib_set_report_logger().

      Там с добычей uniq ещё проще, поскольку вызов-то внутренний, и единственная проверка -- что cp!=NULL.

    • И заюзано в cxsd.c.
      • Перехватываются все 3 штуки.
      • Неприятность -- поскольку надо передавать дуплет format,ap прямо vDoDriverLog()'у, то нет возможности в начало строки вставить "адресную" информацию (sid, ref, cd); поэтому пришлось её выдавать отдельным DoDriverLog()'ом перед тем.

    Проверено -- вроде всё работает, все 3.

  • 16.09.2020: убрано "const" у refinfo_t'шного поля strings_ptrs[]. Глубокий смысл наличия там этого атрибута неясен (точнее, отсутствовал), но он вызывал много warning'ов при компиляции.

    После убирания просто исчезли те warning'и, а новых не появилось. Так что считаем действие правильным.

    17.09.2020: возможно, и у поля reference тоже надо убрать "const".

    Чуть позже: ну убрал. Один warning ушёл -- на тему передачи в safe_free(), другой добавился -- про присвоение const'а не-const-полю. OK, так и оставим.

  • 20.04.2023: добавлено диагностики: чтоб можно было включить выдачу на stderr описаний ошибок, приводящих к уставлению 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", а это бы уже реально снизило производительность.

      Так что забиваем.

cda_plugmgr:
  • 08.09.2009: модуль создан, для начала -- по образу Cdr_plugmgr.[ch].

    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: да, сделано.

  • 30.06.2014: а насколько разумно при регистрации использовать для помещения в список поле next из самой метрики? Как-то это кривовато. И:
    1. Сервер не использует ничего подобного, а обходится функционалом cxldr -- там свои списки.
    2. Даже если бы и не, то чем лезть писать в метрику -- лучше завести свой список (на основе SLOTARRAY).
  • 30.08.2019@дорога-на-обед-в-Гуси, ~11:50: идея, как решить беспокоящую меня всё лето идеологическую проблему "как прилинковывать dat-плагины (cda_d_epics, cda_d_tango) к программам": грузить динамически!

    (Текущий вариант -- собираются отдельные утилиты, вроде epics_cdaclient, tango_cdaclient и прочие; при этом никакого "epics_pult" нет, как и варианта, сочетающего в себе оба плагина сразу.)

    30.08.2019@дорога-на-обед-в-Гуси, ~11:50 (около логова ГУП УЭВ): суть идеи:

    • Ввести специальную переменную окружения -- например, CX_CDA_PLUGINS_LIST, и в cda_core найти некую точку, в которой выполнять "инициализацию" -- проходиться по этой переменной и загружать указанные там модули.
    • В самих .so'шках нужно будет прописывать библиотеки-зависимости (вроде libca.so+libCom.so, или libtango.so), чтобы при загрузке плагина эти библиотеки-зависимости сами бы также подгружались.
    • Только надо будет не забыть о секьюрности: при uid!=euid эту загрузку не производить (ну да, перестраховка :D).

    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: (уже после обеда, в ИЯФе): ещё процедурные тонкости:

    1. Надо будет придумать формат/шаблон имён файлов: может, прямо cda_d_NNN.so?
    2. А также директорию, гда эти плагины должны храниться: $CX_ROOT/lib/cda/dat_plugins?

    30.08.2019@пляж, после обеда: насчёт диагностики:

    1. С одной стороны -- надо внятно ругаться, прямо на консоль, при обломе попытки загрузки модуля.
    2. С другой -- при регистрации десятка каналов с обломившейся схемой оно будет десяток же сообщений выдавать на консоль. А если таких каналов сотни?

    Очевидно, надо бы где-то запоминать факт облома и диагностику более не повторять. Но тут тоже есть тонкости:

    • Очевидное решение -- аллокировать "ячейку схемы" с флагом "обломилось", так что последующие загрузки даже не будут пытаться делаться.

      Но это решение не только очевидно, а и криво: если софтина долгоиграющая, вроде 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_d_NNN.so, это наиболее просто для всего (включае Makefile'ы).
    • "Инфраструктуру" подсматриваем в cxsd_modmgr.c:
      • Контекст cda_dat_p_ldr_context, ...
      • ...с очень широким ассортиментом путей для поиска, начинающимся с $CX_CDA_PLUGINS_DIR.
      • Методы взяты почти неизменными, только переименованы в 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@пультовая:

    • @утро-дома: поскольку для динамической загрузки надо знать argv[0] (для findfilein-спецификатора "!"), то был модифицирован интерфейс cda_get_dat_p_rec_by_scheme()cda_get_fla_p_rec_by_scheme() аналогично, авансом на будущее).
      • Теперь 1-м параметром идёт argv0.
      • В cda_core.c 4 вызова, из которых 1-й, в 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 и за компанию прочие по-девайсные утилиты-скрины).
    • Затем подпилил frgn4cx/{epics,tango}/cda/Makefile, чтоб было годно для загрузки:
      1. Вместо libcda_d_NNN.so оно теперь собирает просто cda_d_NNN.so.

        Следствие -- больше не требуется генерить пустые libcda_d_NNN.c.

      2. При сборке этих cda_d_NNN.so линкеру сбагриваются ключи "-lNNN" со всеми требуемыми библиотеками, с предшествующим "-LDIR". Но НЕ "-rpath..." -- пусть уж в target-системе настраивается, откуда берутся библиотеки).
    • И всё равно не айс -- ошибка какая-то...
    • Анализ отладочного вывода показал, что почему-то пытается взяться символ "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 диагностики об ошибках.

      Учитываются соглашения:

      1. "errno<0 -- ошибка уже выдана", но чуть иначе, чем в cxsd_modmgr.c: тут "выдачей" считается вызов cda_set_err() для формирования информативного сообщения.
      2. "errno==0 -- символ не найден".
    • Также введён простенький SLOTARRAY для запоминания факта, что о невозможности загрузить модуль данной схемы сообщение на stderr уже выдано.

    05.09.2019: обнаружилась неприятность: схемы, чьи модули уже присутствуют, распознаются на любом регистре букв ("CX" эквивалентно "cx", а "EPICS" катит как "epics"), но вот для целей ЗАГРУЗКИ модуля имя схемы передаётся "как есть", и оно пытается грузить файл cda_d_EPICS.so -- естественно, безуспешно.

    Разбираемся:

    • Почему уже присутствующий хватает с любым регистром -- ясно: в cda_get_dat_p_rec_by_scheme() сравнение ведётся при помощи strcasecmp().
    • А почему отсутствующий пытается грузить "как есть" -- тоже ясно: схему-то никто не пытается от-lowercase'ить...

    Осмотр исходников показал, что лучше всего засунуть унижнерегистрирование в split_url():

    1. В cda_core.c она используется перед всеми вызовами cda_get_dat_p_rec_by_scheme().
    2. Ни в каких исходниках она НЕ используется так, чтобы регистр был бы потенциально критичен (ну ещё бы! -- это из самого смысла понятия "схема" следует, что регистр там не должен играть роли).

    Итого -- исправлено в split_url(). Проблема ушла.

    Заодно аналогичное изменение внесено в cda_add_formula(), где используется другой способ указания схемы -- не "scheme::", а "#!scheme" (как в скриптах).

    На этом работу можно считать завершённой. Далее:

    • Надо использовать -- всё равно с cda_d_tango возни ещё валом.
    • Будем смотреть, как это функционирует -- не вылезет ли где каких колёс.
    • Видимо, надо избавляться от директорий frgn4cx/*/util/. 01.10.2019: избавлено.
    • P.S. Содержимое cda_plugmgr.c сейчас выглядит не очень красиво -- возможно, надо как-нибудь переупорядочить секции кода.
cda_d_cx:
  • 10.09.2009: начало создания модуля. Работа идёт в параллель с реализацией cxlib'а.

    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.

  • 11.02.2015: а не надо ли ввести трансляцию имён серверов, как оно есть в v2 (CX_TRANSLATE_NNN)?

    С одной стороны, при разработке дизайна v4 предполагалось, что все эти фокусы ("извращения") не понадобятся.

    С другой же,

    1. тогда не учитывалась потребность в подмене имён для туннелирования,
    2. да и в v2 идея трансляции была добавлена позже (на границе 2009/2010),
    3. а идея с автоматическим резолвингом имён (broadcast'ами) вообще была одобрена только весной 2014-го.

    Авто-резолвинг вообще хбз как сочетается с туннелированием (как указывать, что во что транслировать -- учитывая, в каком виде будут получаемы ответы). Так что, возможно, надо думать о каком-то совсем ином идеологическом решении: только не отдельная софтина-gateway, а что-то типа дополнительных мозгов в самом cda_d_cx/cxlib.

    25.05.2015: ввести-то трансляцию несложно, вопрос -- КУДА? В cda_d_cx.c, чтоб при установлении соединения подменялось, или же в cda_core.c, чтоб оно работало вообще для ВСЕХ протоколов, и имена б в "Knob properties" и в лампочках серверов показывались РЕАЛЬНЫЕ (но -- как? в переменной указывать и протокол связи тоже?).

    После обеда: окей, делаем пока в простом варианте -- в cda_d_cx.c.

    1. Вызов cx_connect() унесён в отдельную DoConnect() (кстати, стало в точности как в v2...).
    2. В него вставлена предварительная попытка трансляции -- содержимое от find_srvrspec_tr().

    30.04.2017@утро-дома: кстати, тогда же (11-02-2015) была добавлена трансляция в конкретно v2cx.

    Но вот чтоб понадобилось реально использовать за прошедшие 2 года именно в v4 -- даже и не припомню.

  • 12.03.2015: при начальном вычитывании данных ("текущих значений"), по CXC_PEEK, НЕ надо дергать REF-UPDATE-evproc'ы.

    Причина -- так появляются "пустые" значения в векторных каналах.

    15.03.2015: делаем.

    • К cda_dat_p_update_dataset() добавлен параметр call_update. Когда он ==0, то уведомление не вызывается.
    • ...только в протоколе пока никак не сделана возможность различать пакеты от PEEK и настоящего чтения.
    • И идеологический вопрос: а для ЛОКАЛЬНЫХ (null-link) модулей доступа -- local и insrv -- надо ли при начальной отдаче тоже делать 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: ЗАМЕЧАНИЕ:

    • Логика (от 28-05-2015/29-05-2015) "для каналов is_rw или is_internal всегда отдавать is_update=1" касается именно ТОЛЬКО cda_d_cx; но НЕ cda_d_insrv.
    • Смысл -- что insrv:: нужен в первую очередь не GUI-клиентам (где надо на экране рисовать), а драйверам-автоматам, которых интересует именно РЕАЛЬНЫЙ ФАКТ ОБНОВЛЕНИЯ, и пофиг на красивость.
    • Правда, механизм "при TRUSTED всегда считать как is_update==1" в cda_core.c всё равно есть.
    • Другое дело, что использовать с trig_read_drv каналы с TRUSTED вряд ли имеет смысл (и вообще СЕЙЧАС таких каналов -- только UNSUPPORTED-каналы в cankoz_lyr-драйверах.
    • Вопрос (или вывод?) в сторону: а как оно будет работать с драйверами, использующими вместо insrv:: именно cx:: -- чтоб исходные данные шли от других серверов?

      То ли на этом оно приподсломается, то ли, наоборот, получится "как надо", и автоматом будут отдаваться (через весь тоннель, возможно, через цепочку из нескольких серверов) "текущие значения"?

    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.

    Резоны:

    • Функционирование протоколов cx:: и insrv:: должно быть идентично. ПОЛНОСТЬЮ, насколько только это возможно.
    • Тот пассаж про "РЕАЛЬНЫЙ ФАКТ ОБНОВЛЕНИЯ": ведь rw-каналы без реальной записи НИКОГДА не будут обновляться после начального чтения, и если vdev-драйвер это начальное чтение пропустит, то так и будет навечно оставаться в ожидании обновления, которое никогда не придёт.

      Нынешние драйверы ist/v1k/v3h/mps20 работают потому лишь, что начальное чтение из устройств отдаёт результат гарантированно позже инициализации всех драйверов при старте сервера (вследствие удалённой шины CAN). Как только появится возможность делать devrestart (аналог sato) через консоль, либо на локальных устройствах (PCI, VME, ...) -- нынешний подход даст сбой.

    • Насчёт trig_read аргумент за 17-07-2015 полностью верен: совершенно бессмысленно использовать в качестве триггера TRUSTED-каналы.

    Итого -- проверки такие добавлены, теперь надо тестировать.

    ...кстати, в cxsd_fe_cx отсутствует для rw-каналов проверка, что они не NEVER_READ -- а просто для rw всегда делается NEWVAL. Непорядок!

  • 05.05.2015: сей момент cxlib_client уставляет мониторы с кодом 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: некоторое обсуждение с анализом:

    • Для начала-то достаточно сделать, чтобы ВСЕ мониторы не-rw-каналов добавлялись в список per_cycle. Чтоб уже заработало.
    • Второй вопрос -- коль схема моделировалась с v2'шного vdev & Co., то как ТО работает (например, readonly-каналы блокировок (входной регистр) у источников)?

      Казалось бы -- надо запрашивать чтение не только сразу после "оживления" канала, но и по получению значения... Но имеющийся там флаг VDEV_RERQ (с устрашающим комментарием "DANGEROUS!!! ...") просто не используется.

      Анализ кода vdev.c и, например, ist_xcdac20_drv.c, показывает, что вся та архитектура нормально работает только в присутствии клиента -- там хотя бы cx-starter всегда есть.

    • ...а cda_d_insrv.c ВСЕ каналы ставит в periodics[] и спрашивает их чтение каждый цикл; и в нём всегда используется модель "on_update".
    • Строго говоря, надо, чтобы КЛИЕНТ мог указать тип монитора -- например, суффиксом в имени канала: @u -- по обновлению, @c -- on_cycle (default).

    13.05.2015: процесс:

    • cx_setmon(): добавлен параметр on_update, влияющий на то, какой будет заказан тип мониторинга.
    • cda_d_cx.c: добавлено поле hwrinfo_t.on_update, в которое прописывается 1 при наличии суффикса @u (да, "парсинг" примитивный).
    • Сделано, чтоб в per_cycle_monitors[] добавлялись каналы ВСЕХ мониторов. Это поведение определяется #define'ом REQ_ALL_MONITORS.

      Оно так, конечно, не очень красиво (особенно название per_cycle_monitors), но всё равно в будущем хочется заменить per-connection-списки на один общий.

    Работает.

  • 30.11.2017: приступаем к реализации UDP-резолвинга (он же zeroconf-резолвинг), уже "как надо" -- чтоб работал, на основе обдуманных за последние года 3 алгоритмов.

    Благо, в процессе реализации "гроханья cda-ресурсов" в течение последнего месяца и понимания прибавилось, и некоторые предварительные действия были исполнены (конкретно cda_dat_p_get_server()'ов параметр options).

    Пара замечаний:

    1. Основные соображения и предыдущие терзания расположены в разделе "...о каналах и точках контроля", начиная с 17-03-2014.
    2. Часть функционала будет в cxlib'е -- собственно отправка запросов и приём ответов. Оно должно будет быть переделано с "глобального" варианта на "с объектами-резолверами".

      Это будет описано в разделе по cxlib_client.

    30.11.2017: собственно действия:

    • Хорошенько поразмыслив (соображения в старом разделе "терзаний", за сегодняшнюю дату) общий пул-кэш ответов делать не будем.
    • Введены константы SRVTYPE_DATA_IO=0 и SRVTYPE_RESOLVER=1, для различения типа сервера (в первую очередь -- для передачи их в options при регистрации/поиске объекта-сервера).
    • И поле cda_d_cx_privrec_t.srvtype, ...которое пока не очень ясно:
      • ни как использовать: сравнивать, подходит ли запрошенная cda_core/cxlib_client'ом операция для этого экземпляра? а нужно ли сие вообще?
      • ни как прописывать: тут -- очевидно, расширить интерфейс cda_dat_p_new_srv_f(), чтоб туда и этот тип передавался; ведь НАДО же понимать, что создаётся сервер-резолвер, а не обычный (и по имени этого делать -- нельзя!).
    • Важное изменение/постулат: поскольку термин "resolve" уже занят для обозначения операции резолвинга имени канала внутри конкретного сервера (имя->gcid), то для UDP-резолвинга нужно использовать другой термин.

      Возможные варианты -- find, look for, look up, seek, search. Еще есть "hunt". EPICS использует "search".

      • Напрашивается "find" (кратко и ясно), но константы с ним будут читаться странно: FIND_RESULT -- как будто "найди результат", а не "результат поиска". Со словом "look" аналогично (да и без второго слова (for/up) оно неадекватно).
      • Слово "hunt" тоже недурно, но это скорее поиск и УБИЙСТВО зверя.
      • Термин "seek" уже задействован в *nix для операции позиционирования.

      Учитывая, что смысл операции -- даже не просто "поиск", а "розыск", и "розыск" переводится как "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, который определённо надо убирать, заменяя на отсылку к "правильному" резолверу.

      • Т.е., переход из RSLV_STATE_DONE в RSLV_STATE_UNKNOWN уже считай что есть.
      • А надо будет еще осознать, как сделать переход RSLV_STATE_UNKNOWN->RSLV_STATE_SERVER плюс (для type==RSLV_TYPE_GLBL) RSLV_STATE_SERVER->RSLV_STATE_DONE.
    • Итого: udp_resolver_rec убираем и делаем "пересаживание" каналов на резолвер.
    • Далее "мясо" ProcessSrchEvent().
      • Предполагается, что идентификация в param1/param2 будет использоваться такая же, как и в обычном внутрисерверном резолвинге: param1==hwr.
      • Соответственно этому вставлена проверка полученного результата:
        1. Что hwr имеет смысл -- "CheckHwr()".
        2. ...кое-что, о чём ниже...
        3. Состояние -- rslv_state == RSLV_STATE_UNKNOWN (rslv_type при этом проверять и незачем).
        4. Имя из ответа совпадает с именем hwr'а.
      • Теперь отдельный пункт, который неочевиден: надо ли проверять, что указанный hwr действительно сейчас находится во владении данным resolver-sid'ом?
        • С одной стороны -- "ну надо же, а то как же!".

          Но возможности для такой проверки простым образом нет (только если идти по списку). Т.к. cdaP.h не предоставляет интерфейса для добычи dataref'ова sid'а, а сам cda_d_cx.c его в своём hwrinfo_t sid'а не хранит.

        • Но с другой стороны -- а ЗАЧЕМ проверять? :)
          • Ну пусть ответ будет обработан seeker'ом от другого контекста, но ведь когда он будет привязывать hwr/ref к конкретному "настоящему" серверу, то сервер будет взят из контекста именно этого ref'а, т.е. -- ПРАВИЛЬНЫЙ.
          • Т.е., де-факто контекст-хозяин (в смысле, cda_context_t) самого резолвера вообще нигде никак не фигурирует.
          • Очевидно, именно по этой причине в первоначальной попытке реализации и был сделан один-единственный udp_resolver_rec.

            Модель с ним была бы вполне работоспособна. Но вот подчищать ресурсы было б весьма неудобно, по сравнению с текущим "по своему RESOLVER-sid на каждый контекст".

    08.12.2017: продолжаем...

    • Продолжение "мяса" ProcessSrchEvent().
      • Собственно "пересаживание" в другой сервер.

        Оно сделано по образу и подобию соответствующих кусков кода из cda_d_cx_new_chan() и FailureProc(), подглядывая и в SuccessProc().

        Но создать что-то вроде "ПоселитьHwrВТакойТоСервер()" для использования во всех вовлечённых точках не представляется возможным, т.к. код довольно разный, в следствие разных обстоятельств (например, вариант RSLV_TYPE_HWID в ProcessSrchEvent() неактуален (хотя вреда и не будет)).

    • Далее -- нужна собственно ОТПРАВКА запросов SEARCH. И тут есть простор для фантазии.
      • Много было написано (натеоретизировано :)) в разделе "о каналах и точках контроля" в 2014-м году.
      • @лыжи: сейчас же некоторые дополнительные размышления -- как бы сделать работу хотя бы в простейшем варианте (чтобы потом, при надобности, причесать поэлегантнее).
        • Во-первых, надо запускать периодический таймаут, чтобы он слал запросы на неразрезловленные в настоящий момент каналы.
        • Когда/как отправлять:
          • @лыжи, начало 2-й 2-ки, ~0.5км: да прямо сразу же в момент добавления канала, и потом по таймауту.

            Детали --

            1. Иметь в cda_d_cx_privrec_t'е поле "was_cx_begin_ed",
            2. и функцию "AskOneName()", которая будет проверять, что если еще НЕ-begin_ed, то сделать cx_begin(), а потом попробовать cx_srch(), и если то обламывается с +1, то cx_run();cx_begin() и опять cx_srch().

            Соответственно, пакеты запросов будут максимально оперативно наполняться прямо по мере поступления каналов.

          • Тут есть всё же проблема: а cx_run()-то когда делать (кроме как по переполнению)? Только в таймауте?
          • @семинар о гравитационных волнах: и еще проблема: поскольку таймаут будет чесать последовательно по всем каналам RESOLVE-sid'а, то получится, что на свежедобавленные каналы запросы сразу же будут отправляться ДВАЖДЫ: при добавлении и потом тут же в таймауте.
          • Вывод: лучше всё же НЕ пытаться делать cx_srch() сразу, а оставить это таймауту. Тогда всё выйдет проще и элегантнее, особенно с учётом идеи про "первый таймаут через 1 миллисекунду" (по тексту идёт ниже).
        • @лыжи, конец 2-й 2-ки: как инициировать первую отправку запросов сразу же, т.е. прямо в момент запуска, но уже после того, как все каналы будут зарегистрированы: надо первый таймаут сделать не через ~10 секунд, как все последующие, а через 1 миллисекунду -- тогда (чисто по алгоритму работы основного цикла вообще cxscheduler в частности) обработчик будет дёрнут сразу же после всех инициализационных действий, когда и начнётся основной цикл.
    • Делаем эту отправку, по вышеописанному простому сценарию:
      • Таймаут запрашивается при создании резолвера через 1 микросекунду (минимальное время), а потом периодически раз в 10 секунд.
      • Сам обработчик -- PeriodicSrchProc():
        • Если в списке неразрезовленных пусто -- ничего не делает, помимо своего возобновления.
        • Идёт по своему списку, делая cx_srch(), при результате !=0 делает cx_run();cx_begin() и повторно тот же cx_srch() -- такой вариант отправки+опустошения буфера по мере заполнения.

      Т.е., очень несложно, и -- вроде всё, готово.

    • Начинаем проверять -- ничего, не оживает канал...

      Разбирательство оказалось коротким: проблема крылась в SuccessProc(), которая не была готова к RSLV_TYPE_GLBL. После добавления этой альтернативы -- заработало!!!

    • Вылезли 2 аспекта:
      1. Надо ли выкидывать канал из списка для сервера при дисконнекте? А то соединение восстановилось, а канала еще нет...
      2. GUI-клиенты -- вроде pult -- отказываются воспринимать короткие имена как базу: ни "devname", ни ".devname" их не устраивают.

    08.12.2017@по-дороге-домой:

    1. Да, выкидывать НАДО. Т.к. тот "старый" сервер мог исчезнуть полностью, например, заменяясь на новый (по другому адресу и/или номеру), и если не выкидывать, то канал долго еще не прочухается.

      Но надо просто при восстановлении коннекта к любому из серверов ТУТ ЖЕ делать пере-опрос. Для чего просто сбрасывать resolver-sid'у имеющийся таймаут и заказывать новый через 1 микросекунду. Для чего, конечно, придётся в privrec'ах всех серверов помнить указатель на privrec их резолвера.

    10.12.2017@дома:

    1. Да, проблема именно в pult.c::main(): он там определяет тип параметра (PK_xxx) и те, что счёл за PK_PARAM, добавляет CdrCreateSection(,DSTN_ARGVWINOPT,) к группировке, а за base считает то, что опознал как PK_BASE.

      И просто набор букв+цифр однозначно считается за PK_PARAM.

      Вывод: надо сделать, чтобы он опознавал потенциальные base'ы. Для этого достаточно наличия в них ':'.

      11.12.2017: попробовал ":icd" -- не работает.

    12.12.2017: решаем проблемы.

    1. Пинание резолвера по коннекту сервера.
      • @путь-на-работу, около ИПА: вчерашняя мысль была -- сохранять указатель на privrec резолвера в момент передачи канала серверу, в ProcessSrchEvent() (что-то типа "ts->resolver=me").

        Но это приведёт к проблеме, осознанной 07-12-2017, и отброшенной тогда как эффективно-несуществующей:

        • что резолвер будет отбабатывать канал чужого контекста.
        • Тогда-то это было неважно (побочные эффекты отсутствовали), но если тут же будет еще и прописываться ссылка на резолвер, то проблема станет реальной: в privrec'е сможет прописаться ссылка на чужой резолвер, который к моменту использования этой ссылки может уже не существовать.

        И что, делать-таки API для проверки "моего ли контекста этот ref"?

      • @путь-на-работу, около вивария ИЦИГа: всё проще -- надо прописывать ссылку на резолвер не в момент передачи hwr'а от него к реальному серверу, а наоборот -- в момент передачи hwr'а от сервера к резолверу, в FailureProc(); ведь тогда явным образом получаем ссылку на ТОЧНО НАШ резолвер.
      • Делаем:
        1. Добавлено поле cda_d_cx_privrec_t.resolver.
        2. В FailureProc() добавлено сохранение ссылки в me->resolver, и изначально значение rs берётся оттуда же, а не =NULL.
        3. SuccessProc() организует таймаут через 1 микросекунду (предварительно убирая имеющийся).

        Проверено -- работает, канал пере-находится сразу после реконнекта сервера.

      • Также заказ таймаута (теперь используемый уже в 3 точках) вынесен в отдельную ScheduleSearch().
      • 17.05.2023: а вот с заказом таймаута для выполнения поиска при регистрации канала было некорректно: заказ "через 1 микросекунду" выполнялся лишь ЕДИНОЖДЫ, при создании резолвера -- точнее, он именно в cda_d_cx_new_srv(..., srvtype == SRVTYPE_RESOLVER) и делался, а самим cda_d_cx_new_chan() -- нет.

        Вылезло это вчера на испытании epics2cda при попытке обратиться к НЕСКОЛЬКИМ PV: первая-то мгновенно находилась, а последующие -- нет (как позже оказалось, не "нет", а "через время до 10 секунд"). Сначала удивлялся -- "как же так, ведь UDP-резолвинг давно отлажен!", но потом, добавив море отладочной печати, нашёл-таки виновника.

        Решение:

        • В cda_d_cx_new_chan() заказ "через 1 микросекунду", конечно, добавлен.
        • ...для чего пришлось перетащить объявление прототипа ScheduleSearch() повыше к началу файла.
        • Но захотелось сделать оптимизацию, т.к. в ситуации, когда всё же приходит сразу ПАЧКА запросов (хоть при старте, хоть потом при появлении клиента) получится, что на каждое добавление канала будет убираться только что зарегистрированный таймаут и регистрироваться новый.
        • Идея оптимизации такая: помнить "срочность" последнего таймаута и если он был 1мкс -- то просто ничего не делать, считая его уже заказанным (да, это похоже на сделанную неделю назад cda_d_epics.c'шную работу с ca_flush_io_tid: при надобности заказать отложенную отправку таймаут заказывается только если в этот момент его нету).
        • "Срочность" запоминается простейшим образом -- просто помнится размер последнего заказанного, прямо в микросекундах.
        • Решение несложное, но довольно громоздкое:
          • Сделано поле cda_d_cx_privrec_t.rcn_us для хранения длительности.
          • Оно аккуратно поддерживается в "актуальном" состоянии -- при отсутствии таймаута делается =-1; модифицируется оно всегда рядом с rcn_tid.
          • Ну и при заказе нового таймаута делается проверка, что если таймаут УЖЕ заказан и текущий заказ "маленький", то ничего не делать и отвалить.

            Тут пара нюансов:

            1. Проверка "уже заказанности" делается проверкой rcn_tid>=0, а не просто "rcn_us>=0" -- чисто на всякий случай; ну и чтоб уж точно разрешать таймауты в 0 микросекунд.
            2. "Маленьким" при этом считается "rcn_us < 2".
        • Впрочем, конкретно для epics2cda, опрашиваемого как минимум camonitor'ом, эта оптимизация не особо помогла: почему-то запросы pvExistTest приходят не единой пачкой, а их успевает перемежать отработка основного цикла cxscheduler'а (вероятно, дело в multithreading'е) и отправка предыдущих поставленных на резолвинг запросов, так что всё равно выполняются повторные отправки только что высланных запросов.

          ...ну да фиг с ним -- пусть не идеально, но работает, да и ладно.

      Проверил -- да, косяк исправлен.

    2. Определение типа параметра.
      • Вносим небольшую модификацию в pult.c::main(): в тесте на PK_BASE дополнительно к присутствию ':' (p_cl!=NULL) проверяется наличие '.' (p_dt!=NULL). Поскольку в этом же условии предварительно проверяется отсутствие '/', то оно не перекроется с именем файла.

        Это полностью повторяет кусочек из v2'шного pzframe_main.c, где в точно таком же сочетании условий в таком же месте проверялось в целях PK_BIGC (только там вместо '.' ролял символ '!').

      • Да -- работает!
      • Но есть несколько замечаний.
        • Работает оно при указании defpfx'а как "icd.", т.е., с точкой на конце. И это единственный вариант. Который работает, похоже, чисто по счастливой случайности. А вот такие НЕ работают:
          • "icd.." (и больше точек) -- убирается только первая (т.к. разделитель прилепряется автоматом).

            Ни cda_core лишнее не убирает, ни cxsd_db.c::CxsdDbResolveName() не считает множественные точки за одну -- как должно бы, в стиле *nix (где множественные '/' в именах файлов считаются за один).

          • ".icd" -- очевидно, потому, что
            1. начальная точка не убирается cda,
            2. а в CxsdDbResolveName() такие имена явным образом запрещены (в стиле v2 они бы считались глобальными и с ними слишком много маеты).

            Тут явно именно cda должна "leading dots" убирать; видимо, не делает этого из-за того, что оно не в имени канала, а в defpfx'е.

            • ...хотя даже если cdaclient'у указывать ключ не -d, а -b (base), то всё равно поведение такое же.
            • А вот просто имя канала (без базы) ".icd.inprb0" -- работает.
          • ":icd" -- по идее, начальные двоеточия должны удаляться (это аналог "../"), но ни с base, ни с defpfx этого не делается.
          • Отдельное неудобство -- комбинация defpfx=".icd..." с name=":::inprb0" даёт ".icd...inprb0", а не ".icd.inprb0"; вот если вместо defpfx указывать base -- то работает как надо.

          Вывод: надо б покорпеть над блоком "слияния" -- combine_name_parts(), что-то еще? -- в cda_core.c, дабы он позволял больше.

    Есть еще несколько замечаний:

    1. Индикаторы -- server-LEDs -- в GUI-клиентах не обновляются; список в тесте (hw4cx'ный стандартный скрин девайса) вообще пустой.

      Ну это понятно -- в MotifKnobs_cda_leds.c нет реакции на событие CDA_CTX_R_NEWSRV.

      17.12.2017: сделано.

    2. Неразрезолвленные каналы показываются без цветового выделения, просто со значением nan. А надо бы их делать тёмно-серыми NOTFOUND.

      Делать им прямо при регистрации cda_dat_p_set_notfound()?

      Стал разбираться: странно...

      • На любые каналы и так возвращается CDA_DAT_P_NOTREADY, в ответ на что cda_add_chan() должен бы выставить CXCF_FLAG_NOTFOUND.
      • Но -- НЕ выставляет, там это явно отключено при помощи "&& 0". Почему?
      • Поиск в bigfile-0002.html по "NOTFOUND" никакого ответа не принёс.

        14.12.2017@утро-душ: понятно, почему -- в таком случае любые не-локальные (не-{insrv,dircn,local}) каналы в скринах сразу становились чёрными, пока не успело приконнектиться. Почему не записал это тогда -- вот вопрос.

      • Но по дисконнекту сервера всё равно надо делать NOTFOUND.
        15.12.2017: протокол:
        • @утро-дорога-на-работу-около-ИПА: возникла идейка: а если выставление CXCF_FLAG_NOTFOUND делать не "двоичным" образом -- "если _DAT_P_OPERATING -- то нет, а если _DAT_P_NOTREADY -- то да", а более гранулярно: аналогично соглашению "init_dev()" у драйверов -- если -1, то просто облом, а если иное отрицательное число, то считать это число минус-флагами, которые надо выставить?
        • Неа, нехорошо так: ведь в cxsd_driver'ном API оба варианта -- и -1, и минус-rflags -- являются указанием на фатальную ошибку, просто с разной степенью детализации.

          А тут получится, что -1 -- это _DAT_P_ERROR, а иное отрицательное число -- вовсе не ошибка, а вариант _DAT_P_NOTREADY (которое само вообще-то =0!).

        • Сделано обычным образом: вызывается cda_dat_p_set_notfound() и при регистрации в cda_d_cx__new_chan(), и при передаче резолверу в FailureProc().
        • Проверено -- чернеет как надо в обоих случаях (и сразу для бессерверных каналов, и при отвале их сервера потом).
        23.01.2018: отрицательное следствие: теперь программы-per_device-скрины, будучи запущены без параметров, сразу все чёрные.
        • Замечено-то и понято было еще давно, с месяц назад; вроде бы с идеей "указывать таким скринам какой-нибудь defserver".
        • Но неясно, КАКОЙ именно defserver?

          С одной стороны, он должен быть валидным (т.е., ":99" не прокатит).

          С другой стороны, оно не должно ни к чему коннектиться.

        23.01.2018@вечер-ванна: ну так можно зарезервировать специальное имя, типа "_NON_EXISTENT_", чтоб оно распознавалось cda отдельно (и рассматривалось бы как "и не пытайся это имя резолвить!").

        Хотя имена с подчерками и так формально невалидны.

        01.02.2018: а как, кстати, это делать? Булевский флажок или прямо "тип сервера" такой делать -- "SRVTYPE_NONEXIST"?
        02.02.2018: да, судя по анализу кода -- проще именно тип сервера. Тогда всё сведётся к очень простому добавлению в 2 точках: в cda_d_cx_new_chan() просить такой тип сервера, а в cda_d_cx_new_srv() по нему ничего не делать (выстявляя лишь me->state=CDA_DAT_P_NOTREADY).
        • Только вот незадача: имена хостов с '_' внутри не пропускает determine_name_type(), считая их невалидными.

          Поэтому "_NON_EXISTENT_" не прокатит, можно только "NONEXISTENT" или, на крайний случай, "NON-EXISTENT".

        • ...отдельный вопрос -- НАСКОЛЬКО РЕАЛЬНО НУЖНО решать эту проблему?
    3. Имена каналов вида "x10sae:-8014.icd.inprb0" почему-то тоже попадают в резолвер, вместо того, чтобы считаться за "хост X10SAE, порт 8014 -- аналог :2, но не пытаться идти через UNIX".

      12.12.2017@дома: да, в determine_name_type() не проверялось на возможный '-' после ':'.

      13.12.2017: проверка добавлена -- if (*p == '-') p++ -- заработало.

    15.12.2017: итого, что далее:

    1. Пункт I -- server-LEDs, надо научить реагировать на события.
    2. Причесать исходник cda_d_cx.c, чтоб там функции шли в более логичном порядке, а не как сейчас "всё поперемешано, потому, что так было проще в момент реализации".

      25.12.2017: посмотрел-подумал -- сначала казалось, что все "новые" функции (касательно search) натолканы "не туда", но по факту выходит (по перекрёстным вызовам), что в другое место перетаскивать будет только хуже. Так что оставляем как есть, а пункт "withdrawn".

    3. Размышлять "не оптимизировать ли еще как-то отправку broadcast'ов" -- например, в 2 стадии (сначала с малым интервалом, потом с большим.
    4. Начинить приём и парсинг UDP-пакетов проверками, с обеих сторон.

      25.12.2017: да, сделано, с обеих сторон почти копии.

    5. Улучшить парсинг имён (в смысле '.' и ':'), как в cda_core.c, так и в cxsd_db.c.
    6. Убрать отладочные printf'ы :)
  • 07.03.2018: обнаружился ляп, потенциально могущий вызвать очень странные эффекты и падения, но по факту пока не имевший шансов проявиться: в 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() оно прописывается сразу после аллокирования ячейки.
    • А "реальное значение писать уже ПОСЛЕ успешного добавления" -- оно изначально и делается именно так.

    Возможностей проверки не просматривается, поэтому просто убедимся, что ничего не сломалось.

  • 12.03.2018: обнаружилось, что тут НЕ делается включение SO_KEEPALIVE:=1. "Кошмар, как так?!?!?!".

    Несколькими минутами позже дошло: а незачем, т.к. протокол CX и так подразумевает постоянную отправку маркеров цикла (CXT4_END_OF_CYCLE), так что обрыв соединения обнаружится. А до его установления -- есть таймаут MAX_UNCONNECTED_SECONDS=60, так что и недоделанные соединения долго болтаться не будут.

  • 16.05.2020: реализовываем возможность выполнять реконнект по команде -- для соединений, которые обломились по CXT4_ACCESS_DENIED/CEACCESS/CAR_EACCESS и потому сами реконнектиться не будут, даже если отказ был по недоразумению и файл cxhosts уже подправлен.

    16.05.2020: главные вопросы были в методологии/идеологии:

    1. Как решать, что соединение "мёртвое" и его можно пнуть на реконнект?
    2. Как именно вызывать реконнект?

    Найдены наипростейшие решения, требующие минимума действий и умствований:

    1. Годными для реконнекта считаем соединения с cd < 0.

      На взведённость rcn_tid'а внимания не обращаем, т.к. ...

    2. ...инициирование реконнекта -- просто регистрируем таймаут через 1 микросекунду: т.е., мгновенно, но отложенно.

    Действия:

    • Регистрацию таймаута вытаскиваем из 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".

  • 06.04.2021: добавление в 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 -- нет.

cda_d_local:
  • 24.06.2007: некоторые соображения о том, как устроить функционирование этого модуля.

    24.06.2007: программа должна зарегистрировать каждый такой доступный локальный "канал", указав его имя (по которому и будет идти доступ) и указатель на значение (чтобы было откуда брать).

    При попытке получения доступа к каналу по cda он ищется в таблице, если находится -- то в поле "номер" во внутреннем дескрипторе записывается его номер в таблице, если нет такого -- то перманентно проставляется флаг CXCF_FLAG_NOTFOUND.

    26.07.2008: соображение/вопрос/возражение: если "local:"-каналы реализовывать просто как ссылки на переменные в памяти программы, то возникают сложности с соответствием семантике остальных каналов:

    1. Как будет происходить уведомление "клиентов" при изменениях этих переменных самой программой напрямую?
    2. Как реализовывать "local:"-каналы записи?

    Ответы, видимо, таковы:

    1. Наверное, надо иметь API-функцию для программ, которую они будут вызывать после модификации переменных, видимых как "local:"-каналы. А когда?
      • То ли для каждого значения отдельно (аналогично EPICS'у);
      • то ли с указанием списка;
      • то ли для всех скопом (событие будет аналогично приходу данных от CX-сервера в CXv2)
      -- вот с этим надо определиться! Т.к. надо все сделать максимально совместимо ("в соответствии") с семантикой реальных каналов, с учетом "выборочного обновления ручек по приходу данных" (conns_u).

      А если значения нужны только в дополнение к значениям "настоящих" каналов -- то программа может вообще этот вызов игнорировать, ибо актуальные значения и так будут использованы в нужный момент (хотя -- не отрыгнется ли это бесконечным возрастом логических каналов, в которых поучаствуют локальные? 06.08.2008: а что, собственно, мешает модулю cda_d_local ВСЕГДА отдавать возрасты 0?).

      Резюме: надо реализовывать схему "local:" так, чтобы она в максимальной степени походила на остальные протоколы.

    2. Для каналов записи -- видать, надо при регистрации указывать также функцию-записыватель, вызываемую при записи в этот канал. И, например, если она -- NULL, то это readonly-канал.

      Вопрос: вызывать функцию после того, как cda сама произведет запись? Ответ -- нет, передавать значение функции, чтобы та могла произвести его валидацию, или вовсе не производить запись.

      Отдельный вопрос -- для чего "local:"-каналы записи вообще нужны? А вот для чего:

      1. Для GUI-программ -- чтобы " мочь реагировать на какие-то действия интерфейсных ручек" (см. bigfile-0001.html за 01-05-2008).
      2. Для драйверов -- чтобы доступаться, посредством стандартного cda-API, к каналам соседних драйверов в том же сервере. Неа -- это все же будет чуть другой протокол -- например, "inserver:".

    P.S. Кстати, кроме имени и указателя на значение также надо указывать и ТИП канала (раз уж мы собираемся поддерживать не только cx_ival_t).

  • 15.09.2009: начинаем создание модуля. Собственно -- на нём-то cda отлаживать даже проще, чем на реальном сервере (хотя и "другее").

    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: некоторые комментарии:

    1. Слать можно и через fdiloib (с обеих сторон).

      Для этого придётся внести в fdiolib возможность указывать write-only -- чтоб она не пыталась спрашивать у ядра готовность write-стороны пайпа на чтение.

    2. На "не слать больше 1, чтоб не переполнялось" -- можно забить (fdiolib снимет проблему), т.к. с множественными по-канальными уведомлениями множественных local-клиентов есть проблема поддержания этого флага "отправлено/не-считано" (выставлять его можно в var_cbrec_t, но кто и как сбрасывать будет...).

      03.10.2014: по-хорошему -- НАДО б иметь этот флаг. Держать -- в hwrinfo_t. Для чего придётся поменять модель "вызова уведомлений": в lcninfo_t хранить также и указатель на cda_d_local_privrec_t, чтоб оно в CallVarCBs() могло доступиться до hwrinfo и проверить флаг. Флагов, кстати, потребуется несколько: по количеству типов событий.

    3. Совсем по-хорошему коммуникацию в обратную сторону (запись) тоже надо бы делать так же асинхронно.
      • Но для этого потребуется socketpair(), а его под Windows/WIN32/WinSock нету, как и PF_UNIX (хотя можно сымитировать, сделав 2 TCP-сокета: один сбиндить на 127.0.0.1:0, а второй приконнектить к нему).
      • Но по факту на двустороннесть можно забить, т.к. "циклов" не возникнет -- обратное уведомление уйдёт уже через pipe, и будет обработано позже (что и "разорвёт цикл").

    15.09.2014: тогда (в мае) реально было недоделано: не хватало исполнения записи и дёрганья циклов.

    • Реализована запись.

      Замечание: "подтверждение" записи -- вызовом для затронутого канала cda_d_local_update_chan() -- ответственность реализатора (т.е., метода do_write()).

    16.09.2014: добиваем:

    • Реализовано дёрганье циклов.

      Сделано несколько некрасиво:

      1. Для каждого local-sid'а аллокируется также ячейка в свежевведённом slotarray'е "LocalSrv", куда записывается sid'ов pdt_privptr.
      2. В 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@вечер-засыпая: а можно оставить ОБА варианта:

    • На pipe'ах -- local::, корректный и безопасный.
    • На прямых вызовах -- dirio:: (?), для простых программулек, где циклы не возникнут в принципе.

    API -- cda_d_NNNN_api.h -- у них одинаковый, так что они взаимозаменяемы.

    23.09.2014: прогресс по проверке и отладке:

    • Падало по идиотской ошибке -- перепутанные аргументы в ForeachVarCbSlot(), вот и лезло к списку по адресу NULL.
    • Не работало (кстати, в обе стороны) по причине другой ошибки -- дважды использовался filedes[1], а [0] игнорировался.
    • Самое странное -- что пашет прямо как есть, безо всякой маркировки WRITE-only. Т.е., добавление write-конца в select()'ов readfds никакой ошибки не вызывает. Правда, это под Linux-2.4.18, как будет под другими ядрами/ОС -- надо проверять.

      После обеда: проверил явно, при помощи 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: переименовываем --

    • local в dircn (DIRect ChaNnels)
    • pipal в local.

    Теперь пора реализовывать cda_d_inserver.c.

  • 06.07.2014@пляж: в целях отладки будет крайне полезно иметь в cda_d_local режим "авто-создавать -- cda_d_local_register_chan() -- все запрашиваемые через _new_chan() каналы.

    Так можно будет отлаживать "экраны" устройств без серверов, при помощи обычного клиента, натравливаемого на "local::".

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

    08.07.2014: да, сделано.

    • Включается уставлением переменной окружения CDA_D_LOCAL_AUTO_CREATE, значение должно начинаться с '1', 'y' или 'Y'.
    • Буфер под значение malloc()'ируется прямо в вызове cda_d_local_register_chan() -- всё равно это навечно, да и плевать на такие мелкие потери в отладочном режиме.

    15.09.2014: добавлено присвоение 0.0 по malloc()'нутому адресу, иначе бывал мусор.

    В остальном же работает, так что "done".

  • 15.09.2014: некоторые дополнения в интересах тестовой утилиты -- localtest.c.

    15.09.2014: сама утилитка живёт в programs/tests/ (тут описываем ради удобства), сделана на основе pult.c, отличается тем, что:

    • Уставляет CDA_D_LOCAL_AUTO_CREATE="1".
    • В качестве defserver используется "local::".
    • Кроме основного окна создаёт еще одно, с табличкой полей ввода для каждого из local-каналов, плюс берёт на себя исполнение записи в них (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_insrv:
  • 24.09.2014: создаём модуль, пока копированием cda_d_local.c.

    Раздел помещаем сразу за cda_d_local'овым.

    Назван не "inserver", а "insrv": потому, что это буквосочетание той же длины, что "local" -- сие упростит унификацию кода между ними.

  • 24.09.2014: собственно процесс пиления.

    24.09.2014:

    • "Hack/debug-API" выпилен за полной иррелевантностью.
    • Как и бОльшая часть "Public API" (оставлена только отправка уведомлений клиентам -- для будущей переделки).

    30.09.2014: продолжаем потихоньку пилить.

    • Практически всё со стороны "Data-access plugin" сделано.
    • Встал вопрос, как лучше реализовать "ту" сторону в аспекте "RegisterInsrvHwr()", учитывая, что, в отличие от local, как бы не на что впрямую навешивать "callback'и".

      Пока сделано халтурно-просто: оно вешается непосредственно при помощи CxsdHwAddChanEvproc(), указывая privptr1:lcn, privptr2:hwr.

    После обкарнывания всего остального лишнего оно стало собираться.

    Недоделки:

    1. На тему "update_cycle" ничего -- инфраструктуры в cxsd_hw пока нет.
    2. Возможность регистрации плагина пока отсутствует -- ни интерфейс для "пользователей" не сделан, ни init_m().

    01.10.2014: рихтуем:

    • Сделана собираемость -- в libcda_d_insrv.a. Плюс файл интерфейса для регистрации cda_d_insrv.h (это для влинковывания).
    • Добавлен _init_m() -- чтоб при загрузке модуля оно само регистрировалось.

      Но тут мутно: не до конца продумано функционирование механизма на основе cxldr, а конкретно cda_plugmgr.c им вообще никак не пользуется -- там всё вручную.

    28.10.2014: и "update_cycle" сделано, для чего введена инфраструктура в cxsd.

    28.10.2014: сей момент есть поддержка только записи, а ЧТЕНИЕ отсутствует. И оно так вообще во всех cda_d_* (кроме v2cx, имеющего свой механизм "подписки").

    Но сейчас УЖЕ можно реализовывать и регулярное чтение:

    1. Собственно чтение делать по ловле события CXSD_HW_CYCLE_R_BEGIN. Среди всех каналов, "читаемых раз в цикл", список номеров которых держать в массиве, годном к указанию в CxsdHwDoIO().
    2. А тип чтения можно указывать тем самым опциональным суффиксом после @, и по умолчанию считать за "развцикл".

    И, кстати, в остальных родственных реализациях связи (конкретно, cxsd_fe_cx) можно делать так же.

    29.10.2014: да, сделано, пока безусловно (никакие '@' не проверяются).

    • В lcninfo_t добавлен растущий массив periodics, с полями _allocd и _used определяющими количество аллокированных и использованных ячеек.
    • Добавление gcn'ов делается в RegisterInsrvHwr(), там же и предварительное приращивание на CHANS_ALLOC_INCREMENT=100 ячеек.
    • В будущем, при реализации cda_d_insrv_del_chan(), надо будет в ячейку записывать -1 (это значение должно рассматриваться cxsd_hw как "IGNORE"), либо вообще тупо удалять содержимое "со сжатием" (сдвигом следующих на 1 ячейку вниз).

    20.11.2014: идеологическое изменение: теперь при регистрации нового канала ВСЕГДА делается возврат текущего значения с текущими параметрами (флагами, timestamp'ом).

    Смысл -- как было решено 29-09-2014 для обычного протокола, вначале делать "peek", прося текущее, пусть даже еще ни разу не измеренное.

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

  • 23.10.2014: поскольку insrv будет использоваться и во всяких тестовых/диагностических утилитах, где может требоваться смена адреса на ходу (читай -- изменение конфига с последующим стоп+старт), то надо б иметь возможность сказать плагину "повтори резолвинг и биндинг" (поскольку в CxsdHwSetDb() делается снятие всего установленного CxsdHwAddChanEvproc()'ом).

    28.10.2014: и даже больше: вот сейчас в cda_d_insrv_new_chan() при обломе резолвинга возвращается CDA_DAT_P_ERROR и канал навсегда становится NOTFOUND.

    А это неправильно! Ведь в каких-то условиях -- скорее в будущей перспективе (либо в доступаемом по remdrv (имена из драйвера поступят только после коннекта), либо еще почему) -- каналы могут "прорасти" позже. 07.04.2015: еще 05-03-2015 было постулировано, что авторитетным источником ИМЁН является конфигурация, а не драйверы. Так что каналы "прорасти" НЕ МОГУТ.

    Так что, видимо, надо будет предусматривать какой-нибудь "отстойник", куда бы попадали неразрезолвленные каналы (и возвращать на них NOTREADY). И пытаться их повторно резолвить

    1. по изменению конфига сервера;
    2. возможно, как-то периодически.

    Только отстойник должен быть per-sid (а не per-context, как для cda_d_cx).

  • 10.06.2015: для tube-каналов надо, чтобы тунеллировались и атрибуты -- {r,d}, fresh_age. (Да, это скорее к модулю vdev, но и тут тоже касается.)

    12.06.2015: еще как касается!!! Ведь сейчас клиентам cda нет возможности получить ни то, ни другое; да и evproc'ы не вызываются (а для fresh_age даже и код события не предусмотрен).

    И еще одно вылезло при написании trig_read_drv.c:

    • Теперь ведь каналы не только int32, а произвольного типа. И надо бы чтоб они все поддерживались.
    • Можно делать -- указывая драйверу (в auxinfo) тип канала.
    • А можно ли как-то сделать возможность указывать cda_add_chan()'у тип канала CXDTYPE_UNKNOWN, чтоб он НЕ выполнял никакую конверсию, а складировал бы данные "как есть", заодно запоминая вёрнутый ему плагином dtype (который тоже можно б было получить)?
    • ...сделать-то можно; вопрос -- стоит ли...

    05.02.2016: да, СТОИТ "сделать возможность указывать cda_add_chan()'у тип канала CXDTYPE_UNKNOWN". Типа проект:

    • Сам trig_read чтоб поддерживал не-int32-каналы так:
      1. Регистрирует их с dtype=CXDTYPE_UNKNOWN.
      2. "Вычитывает" при помощи cda_acc_ref_data()() -- в смысле, что не "вычитывает", а получает указатель, который потом и будет передавать ReturnDataSet()'у.
    • cda_dat_p_update_dataset() же для REPR_UNKNOWN должен делать следующее:
      1. Ставить ri->cur_dtype=dtypes[x].
        • ...и само поле refinfo_t.cur_dtype должно быть введено, ...
        • ...и аксессор cda_current_dtype_of_ref().
      2. Использовать при конверсии указанное dtypes[x] вместо зарегистрированного ri->dtype.
    • Ну и параметр "max_nelems" у 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.

      "Держание актуального" --

      • При ИНИЦИАЛИЗАЦИИ везде (каналы, формулы, varchan'ы) ставить ri->current_dtype=ri->dtype.
      • В cda_update_dataset() перед конверсией при ->dtype==CXDTYPE_UNKNOWN делать ri->current_dtype=dtypes[x].
      • 02.03.2016: также введена проверка, что ТЕКУЩЕЕ nelems*usize не превысит объём буфера (=max_nelems).
        • Сама проверка делается аккуратно, чтоб избежать integer overflow при nelems*usize>MAX_SIZE_T (т.е. 2^32-1 или 2^64-1).
        • Отдельно учитывается то, что типам с REPR_TEXT требуется на 1 ячейку больше, под терминирующий NUL.
    • "Простое" чтение -- cda_get_ref_dval() -- также переведено на current_dtype.
    • Дополнительный вопрос -- как поступать с usize?
      • Введено refinfo_t.current_usize, поддерживаемое в соответствии с текущим значением current_dtype.
      • В чтениях -- cda_get_ref_data() и cda_acc_ref_data() теперь используется оно.
    • Заодно добавлено ограничение, что cda_update_dataset() возволяет обновлять только REF_TYPE_CHN-ячейки.
    • Отдельный вопрос -- что делать с UNKNOWN-каналами при записи. Конверсию-то как производить?

      На сейчас -- видимо, запись им запретить. Хотя это, учитывая неединичность интерфейса записи -- 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() переведена на "кошерное" получение данных и отдачу их наверх.
    • Канал data_r теперь регистрируется как UNKNOWN с max_nelems=16.

      Чуть позже: неа, =sizeof(CxAnyVal_t) -- чтоб гарантированно влазили все скаляры, и при этом не требовалось бы аллокировать внешний буфер (т.к. refinfo_t.valbuf имеет тип CxAnyVal_t).

      ...но по-хорошему:

      1. надо бы ввести возможность указывать максимальный объём вместо этого умолчания -- чтоб и векторные каналы были аналогично туннелировабельны; например, префиксом "@NNN:";
      2. а не помешает и тип, через префикс "@T:"; как минимум для того, чтобы при указании объёма для известного канала НЕ заниматься перемножением количества элементов на sizeof() элемента;
      3. в идеале же -- чтоб хотя бы для локальных каналов оно брало бы размер само из БД; это можно через CxsdHwResolveChan(), а внешние имена с префиксами протоколов через "::" просто не разрезолвятся.

    02.03.2016: и отдельно насчёт проблемы, означенной в заголовке секции -- туннелирование fresh_age.

    • В cda добавлен код события 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: допиливаем.

    • Сделан парсинг префикса [@[-][t][NNN]:].

      Замечание: если указана какая-то ЧАСТЬ спецификации, то остальное берётся по умолчанию, не пытаясь смотреть в БД. Умолчания -- UNKNOWN,1; это выглядит разумно:

      1. при указании только типа будет считаться nelems="1" -- скаляр;
      2. при указании только nelems они будут считаться объёмом в байтах.
    • А если префикса нету -- т.е., имя ссылки НЕ начинается с '@', то делается поиск через CxsdHwResolveChan(). При обломе оного устанавливаются умолчания -- CXDTYPE_UNKNOWN,sizeof(CxAnyVal_t).

      Замечание: у такой проверки есть один косяк: валидные cda-ссылки с префиксом "insrv::" не будут давать результата.

    • РЕЗЮМЕ: код парсинга ОЧЕНЬ длинный, и весьма забардачивает код драйвера. Так что:
      1. Надо бы вытаскивать в отдельную сервисную функцию.
      2. А чтоб было годно для, например, vdev -- надо также учитывать возможность наличия defpfx и base (defpfx в vdev просто "insrv::", а вот base -- реальное имя "базового" устройства).

    07.03.2016: теперь проверяем.

    • "Автоподстройка" -- через БД -- работает!
    • Ручное указание тоже, во всех 3 вариантах: и полное ("d3"), и только тип ("d"), и только объём в байтах ("16").
    • Есть прикол (прОкол?) при указании только типа для реально векторных ссылок: если сказать просто "d" (=="d1") для канала "d10", то при обновлении данных cda_dat_p_update_dataset() молча проигнорирует канал (ничего не складирует), но событие UPDATE для него сгенерит. В результате у double-каналов читается NAN.

    09.03.2016: далее -- атрибуты.

    • Возраст свежести -- легко, сделано.
    • А вот с цепочкой {r,d} есть проблема: API сервера предполагает не цепочку, а лишь ДУПЛЕТ r,d.

      Поэтому СЕЙЧАС оно указывает серверу первый дуплет в цепочке (если он вообще есть) -- который в >99% случаев будет описывать исходный аппаратный канал (а в >90% случаев будет и единственным).

    • ...и, кстати (особенно учитывая наличие {r,d}), каналы должны заказываться с ключом NO_RD_CONV -- тот же vdev это делает изначально.

      Так что и в 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 парой в этой роли не использовались).

    Тремя часами позже:

    • И да, проверено туннелирование {r,d} trig_read'ом. ТЕПЕРЬ оно работает (а не работало из-за косяка в cda_d_insrv.c, предотвращавшего обновление вообще ВСЕХ свойств по insrv::).
    • А позорнейший косяк в insrv_fd_p() заключался в том, что оно
      • сначала считывало канал-или-код в переменную hwr,
      • потом, если это "расширенный" код -- где реальный hwr идёт следующим параметром -- считывало ещё раз в hwr, и...
      • ...пыталось выбирать код операции по hwr же -- в которой уже совсем не код, а hwr канала!

      Короче -- не могло работать вообще ничего из передачи свойств. Очевидно, никогда и не проверялось; точнее, "проверялись" (в ~феврале 2015-го, на cPCI+stand), но лишь те, что доступны сразу при регистрации -- вот и не было замечено, что обновление свойств не работает.

      Стыдно, конечно...

    • Кстати, а не оно ль было причиной столь долго искомого бага с "потерей" время от времени аппаратных {r,d} от CAN-устройств?

      Скорее нет:

      • В части случаев проблема действительно была в том, что vdev-драйверы будто не отдавали аппаратные дуплеты, но в другой части случаев --
        1. Как раз "аппаратная" часть окошек ИСТов показывала не-масштабированные в 1000000 раз числа.
        2. Глюки наблюдались на не-vdev-каналах (например, "старая" магнитная система линака).
      • Кроме того, будь дело в этом -- неполученные vdev-драйверами калибровки -- оно оставалось бы до рестарта сервера. Но оно глючило только в рамках одной программы, а тут же запущенный еще один экземпляр показывал всё правильно.

      Но симптомы весьма схожи, да. Где б в связке cxlib/cxsd_fe_cx могла быть такая же ошибка?

  • 11.10.2015@дом-утро-диван: идейка, как оптимизировать cda_d_insrv: иметь per-connection counter/flag, означающий "сейчас процессится уведомление", и перед write()/fdio_send() проверять, что если counter==0? то {1. делать его++; 2. вызывать обработку (как по 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":

    1. Учитывая, что free() делается в RlsSrvSlot() -- т.е., видимо, НЕ в месте вызова del_srv() -- то придётся по "+1" делать pdt_privptr=NULL, чтоб "забыть" про указатель.
    2. А поле srvinfo_t.being_destroyed уже есть! ...хотя пока используется мало.
  • 09.08.2016: для полноты надо бы в insrv также уметь понимать флаг CDA_DATAREF_OPT_ON_UPDATE (а то сейчас принудительно работает в режиме "ON_CYCLE").
    • Актуально может быть для каналов чтения, которые обновляются "не мгновенно" -- т.е., заказывается чтение, ответ на которое приходит с некоторой задержкой.

      Актуально для того, чтобы получать значения с максимально возможной частотой.

      Сейчас же это умеется только по cx::, а по insrv:: -- нет.

    • С другой стороны, с этим надо обращаться очень аккуратно: если начать так (прямо из GcnEvproc() по событию CXSD_HW_CHAN_R_UPDATE) заказывать канал, ответ на который приходит мгновенно, то возникнет бесконечная рекурсия.

      Впрочем, её можно разорвать, заказывая чтение из "получателя" -- insrv_fd_p(). Тогда петля рекурсии разорвётся, в т.ч. и в оптимизированном варианте с прямым вызовом (см. 11-10-2015).

  • 20.02.2017: желательно бы в дополнение к cda_d_insrv иметь также более "легкий" внутрисерверный протокол, который бы не требовал файловых дескрипторов (как cda_d_dircn для cda_d_local'а).

    Причина -- уже сейчас, с введением iset_walker'а на источники линз, сервер магнитной системы потребляет полторы сотни дескрипторов. Если приделать walker'ы также и на корректоры, то линуксовые 1024 (FD_SETSIZE) резво закончатся.

    (Конечно, на таких количествах API epoll -- epoll_wait() -- будет оптимальнее и позволит больше, но лучше такой фигнёй не страдать вовсе.)

    20.02.2017: как такое можно б было реализовать "безопасно" (т.е., с устранением возможности бесконечной рекурсии):

    • На каждое соединение есть флажок(-счетчик?) "сейчас обрабатывается уведомление". Назовём его being_processed?
    • Также на каждый зарегистрированный канал -- т.е., в hwrinfo_t -- есть флаг "пришло уведомление". Назовём update_pending?
    • И в соединении есть "счётчик необработанностей" -- например, pending_count.
    • И обработка уведомлений выглядит так:
      • Если being_processed!=0, то:
        1. если его update_pending==0, то делаем =1 и pending_count++;
        2. отваливаем.
      • Иначе делаем being_processed++, вызываем обработку, being_processed--.
      • Если по окончании update_pending!=0, то ищем первый необработанный hwr и:
        1. Сбрасываем его update_pending=0, уменьшаем pending_count--;
        2. вызываем для него уведомление (так же, как в предыдущем пункте -- с защитой being_processed'ом!).
    • И обработка события "цикл" должна работать ровно так же -- пока оно обрабатывается, никакие иные уведомления проходить не должны, а только запоминаться.

      ...еще спасает то, что само событие "цикл" во время обработки событий от каналов возникнуть никак не сможет.

    • Тут, конечно, есть некоторые технические сложности:
      1. Как делать поиск? Просто идти линейно по списку -- а когда при очередной обработке появится новое значение в hwr'е ранее по списку? Видимо, надо циклить, пока pending_count!=0.
      2. Как именно "вызывать уведомление" для появившегося-во-время-обработки обновления? Делать goto на начало функции?

        Этак оно может и до бесконечности зациклиться.

      Решение напрашивается такое:

      • Сам "обработчик" -- being_processed++,обработка,being_pocessed-- -- является отдельной функцией. Она вызывается один раз вначале GcnEvproc()'а (для канала-причины), и потом по мере надобности внутри того цикла "пока pending_count>0".

        ...хотя можно это развернуть и прямо в самом GcnEvproc() в цикл вроде

        НОМЕР_КАНАЛА=вызвавший;
        НАЧАЛО_ДЛЯ_ПОИСКА=0;
        while (1)
        {
            pending_count++;
            ВЫЗВАТЬ_ОБРАБОТКУ;
            pending_count--;
            if (pending_count == 0) break;
            else НАЙТИ_НОМЕР_ОЧЕРЕДНОГО_НЕОБРАБОТАННОГО; // С НАЧАЛО_ДЛЯ_ПОИСКА, закольцовыванно
        }
        
      • Воизбежание бесконечного зацикливания:
        • Иметь-таки некое ограничение на количество повторов цикла (например, 100), и..
        • ОДИН файловый дескриптор на весь cda_d_???, через который пересылать только номер соединения (номер -- внутри lcns_list-slotarray'я).

          В _fd_p() для этого дескриптора вычитывать номер соединения и производить для него то же самое, что в предыдущем пункте (видимо, то всё-таки надо оформить отдельной функцией?).

        • На тему "отправили в дескриптор номер, а до вычитывания соединение успело закрыться, и открыться новое под этим же номером" можно не заморачиваться: проход по списку каналов в поиске необработанных уведомлений -- штука безопасная: при pending_count==0 ничего не будет делать, а при !=0 найдёт и уведомит.
    • Некоторое усложнение -- что кроме события _CHAN_R_UPDATE есть еще несколько штук. И все их надо каскадировать аналогично; хотя при наличии пачки -- можно внутри скобок being_processed{++,--} обработать все друг за дружкой.

      Наверное, сделать update_pending не просто флагом, а битовой маской (в которую прямо биты _CHAN_EVMASK_* и писать). И:

      1. Делать pending_count++ только при update_pending==0.
      2. А update_pending|=mask делать всегда (безотносительно ++'енья в предыдущем пункте).

      Единственное сомнение -- не будет ли проблем из-за перепутывания порядка событий в этом аккумуляторе? Пол-решения -- реагировать на события по уменьшению числа-номера: сначала на свойства, потом STATCHG, и в самом конце -- UPDATE.

      Кстати, конкретно сейчас на STATCHG особо никто и не реагирует: vdev плюёт, а cxsd_fe_cx считает причиной для отсылки CURVAL.

    Резюме:

    • Предложенный проект cda_d-протокола позволит напихивать в один сервер сотни и тысячи драйверов с линками на другие драйверы, и без размножения файловых дескрипторов.

      И эти линки будут более быстрыми, чем нынешние insrv'шные.

    • Цена -- быстрыми будут лишь ПЕРВЫЕ вызовы уведомлений, потенциальные последующие рекуррентные будут обрабатываться сильно тормознее из-за поиска по всем hwr'ам соединения.

      Но эта цена приемлема, т.к. в основном никаких рекурсий быть и не должно.

    Одно только неясно: как это назвать? Просто dir_insrv -- криво.

  • 06.03.2018: обнаружена халтурка: при разрегистрации канала он НЕ удаляется из списка периодически опрашиваемых -- lcninfo_t.periodics[].
    • При удалении sid'а -- конечно, удалится всё, как положено.
    • Почему при тестах осенью косяк не был замечен -- понятно: он ведь не вызывал никаких утечек памяти, а просто лишние действия (запрос на измерение канала, для которого даже evproc уже удалён).

      Часом позже: хотя, при постоянном удалении/добавлении каналов в одном контексте ДОЛЖНА была случиться утечка: в periodics[] номера всегда добавляются в конец, так что при 10000-кратной регистрации и потом разрегистрации 50 каналов массив должен был вырасти до 500000 ячеек.

    (Заметил это, когда подглядывал, как тут реализована работа с мониторами (список которых и есть periodics[]), чтобы понять "а как можно мониторировать каналы, чтоб корректно и безопасно" -- сейчас для cxsd_fe_starogate.c, но и для общего понимания.)

    06.03.2018: попробовал было "с нахрапу" исправить -- авотфиг! Может, и хорошо, что тогда (осенью) не заметил.

    • Идея реализации удаления из списка была немудрящей -- "пройдёмся по periodics[] и удалим все вхождения данного gcid, сдвигая следующие элементы вниз и делая periodics_used--".
    • Но так НЕЛЬЗЯ!!!

      Ведь тот же gcid может притсутствовать и из-за других каналов -- маппирующихся на тот же аппаратный канал (коим является gcid) через другие имена или cpoint'ы.

    • Единственное, что сейчас приходит в голову -- воспользоваться тем, что cxsd_hw толерантно относится к запросам на измерение канала номер -1, просто пропуская их.

      Но тут тоже остаётся изначальная проблема -- в КАКОЙ ячейке periodics[] со значением gcid заменять на -1?

      • Прямо в hwrinfo_t хранить номер ячейки, в которую был помещён gcid (это значение periodics_used в момент помещения) и по этому номеру прописывать -1.
      • Хотя тут отдельный вопрос: а не коррелирует ли оный номер ячейки с hwr?
        • Сейчас он, скорее всего, обычно равен hwr-1 (потому, что hwr'ы начинаются с 1, а ячейки прямо с 0).

          Но после первого же удаления канала и добавления нового корреляция нарушится: hwr будет даден старый, а ячейка аллокируется новая.

        • А ведь можно сделать соответствие ФИКСИРОВАННЫМ: чтобы gcid всегда клался в ячейку номер hwr.

          Только придётся:

          1. при росте periodics[] заполнять свежедобавленные ячейки значениями -1;
          2. превратить periodics_used из "точки, куда будут добавляться следующие" в "ТЕКУЩИЙ номер последней используемой ячейки плюс 1".

    Пока выглядит явно проще оставить как есть, т.к.:

    • Халтура, но некритичная.
    • Да и реально какой драйвер будет удалять по ходу дела часть используемых insrv::-каналов?
    • Зато исправление этой халтуры выглядит довольно заморочным, да еще и тестировать придётся.
    • А если вдруг реально страсть как понадобится -- сценарий решения есть.

    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

  • 31.07.2019: возникло подозрение -- чисто по анализу кода -- что cda_d_insrv.c может страдать утечкой файловых дескрипторов.
    • ЧИТАЮЩАЯ сторона -- 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.

cda_d_v2cx:
  • 30.06.2014: модуль начат.
  • 02.07.2014: процесс создания.

    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: а можно и иначе выпендриться --

    • Грузить прямо v2'шные группировки в _db.so-файлах, и брать информацию оттуда.
    • Для чего сделать локальную собирабельность v2'шной libCdr с rename'ом, аналогично сделанному с тамошним libcx'ом.
    • Тогда и имена оттуда можно было бы использовать -- при запросе СИМВОЛЬНЫХ имён считать их "тамошними".

    ...Можно, но ей-богу, уж лучше б побыстрее переходить на нормальную новую версию побыстрее.

    16.07.2014: "лучше б", да, но раз мысль появилась -- то сегодня по-быстрому сделана собираемость и влинковка также v2'шных datatree и Cdr. Остались вопросы:

    1. Взаимозависимости между ними?
    2. Фейковая cda?

    23.07.2014: чтоб проще решить предыдущие 2 вопроса, сборка v2'шных библиотек вынесена в отдельную директорию -- lib/v2cx/ (хотя по-хорошему надо б инкапсулировать в lib/cda/v2cx/).

    24.07.2014: доведено почти до конца:

    1. Взаимозависимости сделаны "как надо" -- _rename.h-файлы #include'ат уже не src_LIBNAME.h, а соответствующие v2LIBNAME_renames.h.
    2. "Фейковая cda" сделана -- stripped_cda.c, содержащая в себе:
      1. Минимальный менеджмент sid'ов -- просто аллокирование с сохранением имени. Плюс auxsid'ов.
      2. Урудиментированный "менеджмент" каналов -- заключающийся в encode_chanhandle(sid, physchan). И cda_srcof_physchan() возвращает в качестве номера именно прямо младшие 24 бита.
      3. Некоторое количество прямо скопированных функций.
      4. Остальные функции, на которые ссылается Cdr -- пустые заглушки (возвращают кто 0, кто -1).
    3. Модуль v2subsysaccess.c скрывает за собой обращения к v2'шной cda. Его внутренности сделаны на основе simpleaccess.c, и пару {servername,chan_n} добывает при помощи cda_srcof_physchan().

    Проверено -- cda_d_v2cx добывает инфу как надо. (Всю работу по auxsid'ам проделывает Cdr, передавая cda готовые имена доп.серверов.)

    Осталось только работу с physinfo организовать.

    25.07.2014: работа с physinfo сделана в начальном варианте -- всё, кроме поддержки "global_physinfo_db" (её надо понять, в какой момент прикручивать; а прикручивать надо к main-sid'у).

    После обеда (и пляжа): и "global_physinfo_db" сделана:

    • Приделывается она к mainsid'ам, посредством специфичной stripped_cda_register_physinfo_dbase().
    • Поиск по ней делается в cda_add_auxsid()'е, плюс...
    • ...для основного сервера -- ищется и прямо в v2subsysaccess.c'шном 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@пляж: а большие каналы в v2cx можно и эмулировать.

    06.07.2014@пляж: детали:

    • Считать, что при n_items>1 это большой канал, со специальным соглашением -- он всегда CXDTYPE_INT32, но первые 10 ячеек -- описатель (dataunits, nargs, datacount, ...), потом 100 -- под параметры, а уж потом данные.

      Вопрос в API для частичной записи/чтения в cda-v4.

    • Ну а свойства (numparams, dataunits) указывать в @-опциях.
    • Отдельно -- v2cx_dat_p.c должна будет САМА аллокировать по доп.sid'у для каждого большого канала.

    Но более интересный вопрос в другом: как бы эмулировать v4-API "больших каналов" (со связанностями) прозрачно?

    ...и еще вопрос -- а оно нам реально надо, с такими напрягами, стоит ли игра свеч?

    06.08.2014: да, свеч игра стоит -- чтоб Федю целиком перевести с v2::simpleaccess прямо на v4.

    10.08.2014: с учётом придуманного в предыдущем подраздельчике синтаксиса @bNN, поддерживать большие каналы довольно несложно.

    • Указывать на БОЛЬШЕСТЬ канала лучше всё-таки каким-то суффиксом. Например, @pNN, где NN -- количество параметров.

      Свойства -- dtype и nelems -- берутся переданные в new_chan().

    • Ссылаться на параметры -- синтаксисом @bNN либо @pNN: b -- как скалярный канал, p -- именно как параметр.
    • Реализация:
      1. Делается интеллектуально, аналогично pzframe_knobplugin'у: при указанности phys/pr оно может использовать и скалярный канал.
        • С суффиксом @b канал по возможности (указанность phys/pr) считается просто скалярным и не привязывается к большому.
        • С @p именно как параметр большого канала -- возвращается только из вёрнутого со значением большого (причём ДО него, чтоб из его evproc'ев уже были доступны новые данные).

        (Да, потребуется поддержка от v2subsysaccess'а.)

      2. cda_d_v2cx поддерживает список "используемых" больших каналов аналогично simpleaccess'у; и как-нибудь помнит о приделанности к ним параметров, и по приходу данных вызывает для них всех update_dataset().
      3. Главная неприятность: для каждого большого канала надо будет поддерживать собственный единоличный sid -- причём прозрачно для v4'шной cda...

        12.08.2014@пляж: да легко: генерить уникальное имя (с использованием ref'а), а дальше всё обычно.

    • ...да, предполагается, что сам большой канал должен бы "регистрироваться" ДО ссылок на его параметры.

    05.02.2015: да, БУДЕМ делать (именно ради Феди). Некоторые мысли по реализации:

    • Синтаксис имён bigc-related каналов -- с ОБЯЗАТЕЛЬНЫМ @XXX:
      • @v[NN] -- сам большой канал; NN -- количество параметров, при неуказанности -- 100.
      • @pNN -- параметр.
      • @bNN -- параметр, по возможности адресуемый как обычный скалярный канал (при её отсутствии -- как @pNN).

        Обычные скаляры работают через обычные же соединения, без описанной с следующем пункте алхимии.

    • Уникальное имя для sid'а каждого большого канала генерить надо, только не с использованием ref'а (ведь надо ж как-то к нему доступаться при регистрации параметров), а с использованием самого имени большого канала: SERVER:N:BIGCHAN_NAME.
    • Регистрация параметра до регистрации самого большого канала считается ошибкой.

      (Теоретически можно бы неявно регистрировать и сам канал, но неясно, с какими свойствами -- 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() всё касательно больших каналов пока делается в отдельной ветке.
    • Композитные имена серверов генерятся -- SRVNAME:CHANNAME, причём "CHANNAME" уже БЕЗ @-суффикса (иначе как бы параметрам ссылаться на базу).
    • Предполагается такое соглашение: сам большой канал имеет hwr==0, а параметры идут с 1 (т.е., hwr=номер_параметра+1).

    15.02.2015: кстати, ведь надо еще откуда-то знать ОПИСАНИЕ данных большого канала (dtype, nelems).

    • Так что придётся ввести правило: параметры-то могут регистрироваться ДО базового канала, но вот функционировать с незарегистрированным базовым оно не будет никак.

      Guard'ом можно сделать номер bigc_n -- если он <0, то считать отсутствующим (а при создании сервера прописывать =-1).

    • А место в буферах отводить под 100 параметров, но использовать столько, сколько указано.

    18.02.2015: худо-бедно допилено.

    • Практически копированием из v2'шного cda.c, так что и вся механика работы с памятью оттуда.

      Соответственно, "место в буферах" отводится под столько параметров, сколько указано в @vNN.

    • Различения между @p и @b пока не делается, они все считаются за @p -- всегда как привязанные к большому каналу параметры.

    Теперь проверять и доделывать.

    19.02.2015: поддопилено (вчерашнее работать никак не могло, чисто архитектурно).

    • Тонкость -- оно ВСЕГДА пытается отдавать "наверх" все параметры, просто для не-зарегистрированных каналов-параметров в bigcdata.refs[] лежат 0, так что cda_core их пропускает.

    Проверено на cdaclient -- вроде пашет (ЧТЕНИЕ; запись проверить пока не на чем).

    20.02.2015:

    • Сделано различение @b -- по возможности (при указанности скалярного канала) оно регистрирует отдельный скаляр, прибавляя к нему номер параметра и делая goto REGISTER_SCALAR_CHAN (да, дико некрасиво, но работает).

    05.09.2016@Снежинск-Снежинка-304-утро: некоторые замечания от Роговского:

    1. Для больших каналов приходится ВСЕГДА делать группировку, а указание просто номера канала не прокатывает.
    2. Не работает умолчание "@v == @v99" (у Юры опечатка, должно быть "@v100").
    3. Форсится CX_CACHECTL_SHARABLE, а хочется ещё и CX_CACHECTL_SNIFF.

    05.09.2016@Снежинск-каземат-11: пилим:

    1. SNIFF -- самое простое. Вместо "@v" можно указывать "@s", что и влечёт нужный режим доступа.

      ...если припрёт, то FORCE и FROMCACHE тоже можно будет сделать -- "@f" и "@c".

    2. Умолчание "100" при неуказанности числа параметров -- сделано отдельной веткой, что при '\0' сразу после символа она просто делает number=CX_MAX_BIGC_PARAMS.
    3. Взятие просто номера канала -- оказалось самым муторным.

      Добавлена проверка, что если 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()'ится в начале блока, отвечающего за резловинг имени-через-группировку в канал.
    • Но при @b делается "goto REGISTER_SCALAR_CHAN", а оная метка стоит ПОЗЖЕ bzero().
    • В результате в cda_dat_p_set_strings() передавался мусор, вот она и падала.

    Решение тривиально: bzero(&strs,) перетащено в начало функции. После чего проблема ушла.

  • 11.02.2015: отсутствовал мехнизм трансляции именов серверов -- environment-переменными CX_TRANSLATE_nnnn_NN, имеющийся в v2 с 11-2009...01-2010.

    Добавлено, почти копированием из старого (модификация -- что передаётся не ASCIIZ-строка, а с параметром len (поскольку там исходный srvrspec опционально шинкуется ради второго ':' (для больших каналов))).

  • 16.02.2015: несколько напрягает, что v2'шные каналы работают НЕ так, как v4'шные: update'атся ВСЕГДА, даже если устройство отключено и от сервера приходит тэг 255.

    16.02.2015: если подумать, то это решаемо:

    • Вызывать cda_dat_p_update_dataset() не всем каналам скопом вслепую, а лишь тем, что реально обновились -- стандартным алгоритмом прохода "смотрим вид первого, потом находим, сколько их таких, и найдя группу делаем с ней всей одно и то же". Как это делается тучу раз в cxsd_hw.c (ключевое слово Should).

      А уж прочая архитектура в v4 к такому приспособлена -- речь о Cdr/Chl/Knobs.

    • Главный вопрос -- выбор критерия: КАК понимать, что канал обновился или нет? Тут есть нюансы:
      • Считать недостойными только с тэгом 255 -- некорректно: так будет допускаться до клиента толпа ненужного (и при отключке, пока не доплелось до 255; и от козачиных АЦП измерения, не успевающие реально обновиться ежецикленно).
      • С другой стороны, считать достойными только с тэгом 0 -- тоже нельзя: возможны ситуации, когда клиентская библиотека почему-то пропустила цикл с реальным измерением, и получила уже с ==1. Но оно-то ведь БОЛЕЕ свежее, чем предыдущее, и его СТОИТ довести до клиента!
      • Получается -- надо передавать те, что были измерены позже последнего имеющегося.

        Как будем определять позжесть? Сравнением timestamp'ов (уже насчитанных вычитанием тэга), с начальным значением {0,0} или {1,0}?

        Но из-за некоторого jitter'а моментов обработки может получиться, что (n+1)-й шаг будет обработан более через размер цикла от n-го, и тогда ВОЗРАСТ получится "позже" предыдущего. Поэтому надо отдельно учитывать еще и значение age: если оно <255 и при этом cur_age>prev_age, то считать как если бы текущее НЕ позже предыдущего (но эта проверка вторая -- только если обычные возрасты катят).

      • Отдельный вопрос -- как поступать ПЕРВЫЙ раз: передавать ли первое присланное -- даже старое, чтоб программа знала "хоть что-то", или нет?

        Федя утверждает, что надо.

        И в EPICS'е вроде бы тоже отдаётся начально-известное.

        Сделать это просто: оставить начальный timestamp {0,0}, тогда даже "устаревший" {1,0} будет позже.

  • 24.04.2015: по просьбе Роговского сделан небольшой хак: скалярные каналы "внутри" могут считать себя не INT32, а SINGLE (float32). Делается это указанием суффикса "@f"

    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 не понимает.

  • 21.09.2015: неудобно было, что 4cx/ жестко зависит от наличия обычного v2'шного cx/.

    Поэтому сделано, что сборка 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 или просто считает её "свежей" (по причине отсутствия зависимостей и команд/правил?).

    По-хорошему --

    1. Надо бы всё же переделать lib/Makefile, чтобы зависимость присутствовала только при НЕотключенности.
    2. А можно также и саму v2cx добавлять к CSUBDIRS не в конец, а в серединку, прямо перед cda -- например, поставив туда какой-нибудь макрос, который и делать "=v2cx" при надобности.

    Но сначала недурно бы разобраться в причинах ТЕКУЩЕГО поведения.

cda_d_vcas:
  • 24.06.2014: модуль начат.

    06.08.2014@вечер-пляж: некоторые мысли по реализации внутренностей:

    1. Считывать прямо в буфер по 1 байту из sl-callback'а, до '\n' или до -1/EWOULDBLOCK.

      ...или сделать в fdiolib вариант FDIO_STRING, реализующий такие "мозги"? А вся часть connect'а и отправки останется от FDIO_STREAM.

    2. Иметь массивец дуплетов {hwr,name}, отсортированный (qsort?) по name -- для поиска при приходе данных.

      И name указывает на то же strdup()'нутое поле, куда и hwrinfo_t.name -- т.е., уже с заменой '.' на '/'.

      Поддерживать массивец, видимо, самостоятельно -- без SLOTARRAY.

    07.08.2014: по п.2:

    • С одной стороны, кошерно -- вообще добавлять в нужное место. Но тут qsort() нам не помощник.
    • С другой стороны, поиск-то всё равно надо делать самостоятельно, взяв алгоритм из книги Вирта.
    • Напрашивается такая реализация:
      1. Добавлять просто в конец, но иметь флажок "is_sorted", который при добавлении сбрасывать.
      2. В момент прихода данных -- т.е., при необходимости поиска -- проверять is_sorted, и если нет, то производить сортировку и взводить.
      3. Таким образом получим авто-адаптацию: сначала каналы будут добавляться быстро, прямо в конец, а потом отсортируется уже сразу вся сформированная пачка.
  • 07.08.2014: собственно реализация.

    03.04.2015: приступаем к РЕАЛЬНОМУ наполнению.

    • Вопрос был -- на основе КАКОГО другого модуля делать этот: cx(v4) или insrv/local? У них кардинальные отличия в организации hwr'ов:
      1. В cda_d_cx.c они сделаны отдельным сквозным массивом, и по мере надобности могут как прилинковываться к конкретному sid'у, так и отлинковываться (обратно в "отстойник") и переходить к другим.
      2. В cda_d_insrv.c же hwrs_list жестко привязан к privrec'у.

      Так что --

      • С одной стороны, VCAS не предполагает plug-and-play'я или динамической миграции, так что единожды попавши в сервер, канал там и останется навсегда.
      • С другой -- insrv/local сами существуют "вечно" и не предполагают reconnect'ов, а тут всё-таки сетевое соединение, и нужна вся инфраструктура переподключений.

      Короче -- видимо, придётся делать гибрид: менеджмент соединений/reconnect'ов от cda_d_cx.c (или, скорее, родственно remdrv_drv.c), а менеджмент hwr'ов -- от локальных модулей.

    • В порядке стандартизации:
      • Надо бы fdio-handle'ы называть *_iohandle, а НЕ "fdhandle" и не "fhandle". ВЕЗДЕ. Чтобы и с cxscheduler'овыми fdh'ами не путать и не с чем иным.
      • А вот cxscheduler'овы -- как раз *_fdhandle.
    • Основа сделана.
    • Наблюдение: наша архитектура реконнектов -- что в cda_d_cx, что в remdrv (где она чуть отличается!) -- мягко говоря, не самая элегантная и очевидная.

      Да, она работает (вроде бы), но точно не блещет...

      ...и еще эти мутные "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: продолжаем.

    • Сделана расшифровка timestamp'ов. Вроде работает, но странно и небезгрешно.
      • Небезгрешно -- потому, что конкретно на viper с НЕпроапдейченным tzdata (в данном случае RH73 -- glibc) отображается со смещением на 1 час. Но это знакомо и понятно, как править.
      • Странно потому, что, судя по man'ам, функция mktime() делает конверсию из struct tm в ЛОКАЛЬНОЕ время; а timeval-стиль -- UTC! -- должна делать малопортабельная timegm().

        Но mktime() даёт правильные результаты (с точностью до того бага на 1 час), а как раз timegm() -- даёт время со сдвигом на 6 часов. Т.е., как будто в struct timeval'е хранится всё-таки ЛОКАЛЬНОЕ время, а не UTC'шное.

    • Определение ненайденности канала тоже сделано. Хотя пока без возврата сего факта наверх -- ибо некак.

    10.04.2015: далее.

    • Отдание факта ненайденности сделано.
    • Также наверх отдаются значения полей descr и units -- в качестве 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 нужны именно явные уведомления, иначе канал не будет считаться "существующим".

    Сделано довольно тривиально-прямолинейно:

    • Поскольку в VCAS нет своего нативного события "канал найден" (а только "канал НЕ найден", которое репортится изначально), то...
    • ...добавлено поле hwrinfo_t.rslvstat_reported для сохранения статуса "сообщено".
    • И в ProcessInData() при получении данных в ДВУХ местах репортится, если rslvstat_reported==0 --
      1. Если получено "none", т.е., данные возвращаться не будут.
      2. Перед собственно возвратом данных.
      (эти места там в разных ветках if()/elseif()/..., поэтому унифицировать в одну точку нельзя -- тем более, что репортить нужно ДО возврата данных.)

    После этого epics2cda начал работать с VCAS-каналами.

  • 10.04.2015: есть проблемка -- в VCAS никак не постулирован формат (dpyfmt), поэтому сейчас используется просто "%f", что не есть хорошо.

    А можно указывать формат прям в имени канала -- после '@', без '%', вроде "VEPP5/NO/Energy@8.3f".

    И тогда можно оный dpyfmt даже "наверх" отдавать через cda_dat_p_set_strings(), чтоб оно сразу в экранах испльзовалось.

cda_d_epics:
  • 24.08.2014: модуль создан, пока совсем пустой, только разница между 01-01-1990 и 01-01-1970 высчитывается.

    20.05.2019: а ведь давно очевидно, что этот модуль надо уносить в отдельную директорию; в lib/cda/ ему делать нечего -- он ведь наверняка будет зависеть от libca.a (или libca.so).

    ...если только не делать собственную реализацию сетевого протокола Channel Access (и называть такой модуль типа "cda_d_epics_direct.c"). Но это вряд ли -- совершенно бессмысленная трата ресурсов/трудозатрат; плюс ещё и необходимость постоянно гнаться за актуальной версией/реализацией их протокола, вместо того, чтобы просто использовать готовое.

  • 24.08.2014: типа протокола реализации...

    24.08.2014: вычисление разницы между "EPOCH" *nix и EPICS... Пытался сделать "на лету", при инициализации -- хрен!

    1. Методы init_m() у builtin-cda-плагинов не вызываются...
    2. Вменяемого способа посчитать эту разницу ВПРЯМУЮ -- взяв дату 01-01-1990 и узнав еёйное количество секунд -- нет: единственная функция преобразования из struct tm в time_t -- 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: это специализированное место для записи ссылок на информацию, полезную для программирования этого модуля.

    20.05.2019: вчера погуглил на тему "epics c api". Найденные ссылки:

    24.05.2019: вышеуказанная PDF'ка дочитана. Общее впечатление положительное, описанный там API хоть и не без странностей, но выглядит годным для оборачивания в CX.

    Годность:

    • Там также есть понятие "контекст", но он 1 штука: при создании каналов контекст никак не указывается; следовательно -- он глобален и "создать контекст" по факту является просто инициализацией библиотеки.

      28.05.2019: похоже, он существует по штуке на каждый thread.

    • Каналы создаются вызовом ca_create_channel(), грохаются -- ca_clear_channel().
    • Можно указывать user-pointer для передачи callback'ам: при создании канала он называется 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: ещё пообщался на эту тему с Гусевым, и он утверждает, что дело обстоит чуть иначе:

      • НИКАКАЯ из этих 4 функций не является блокирующейся, включая и ca_array_get(), имеющую параметр pValue. Просто содержание буфера начинает иметь смысл лишь после того, как будет вызвана ca_pend_io() и оная завершится успешно.
      • Это подтверждается и PDF'кой, где сказано, что "The preceding routines are requests, hey only queue the operation"; плюс там в примере сразу после 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
    • Очевидный плюс: чтение/запись сразу массивами -- указываются тип одной единицы и количество единиц; скаляр является частным случаем с количество=1.

      В точности, как в CX.

    • Типизация -- довольно простая: DBR_xxx; явно можно сделать маппирование DBR_xxx<->CXDTYPE_nnn.

      28.05.2019: но некоторые типы -- весьма специфичные, и никаких аналогов в CX не имеют. Кроме тривиальных скаляров, остальное -- какие-то странные структуры (DBR_STS_xxx, DBR_TIME_xxx, DBR_GR_xxx, DBR_CTRL_xxx). Хотя нужны ли они в реальности, или просто древнее наследие?

    • Требования libCA к основному циклу неясны, и возможности добывать оттуда файловые дескрпиторы (с адресами соответствующих callback'ов-обработчиков) не видно.

      Но есть функция 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".

    Странности:

    • Раньше "контекст" назывался "task", а теперь именно "context": ca_task_initialize() -- просто обёртка вокруг ca_context_create().

      Смысл переименования не вполне ясен. 28.05.2019: может, из-за того, что это "per-thread context"?

    • Неясно, что именно делает параметр SELECT=ca_disable_preemptive_callback в вызове ca_context_create().
    • Функции в РЕЗУЛЬТАТЕ возвращают только статус, а собственно "результат" -- кладут по переданному указателю.

      Например, ca_create_channel() реальный результат возвращает в chid *PCHID.

    • "Объекты" возвращаются прямо указателями (аллокируемыми библиотекой), а не числовыми handle'ами.

      Пример -- тот самый 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 -- тип-указатель.

    • Информация callback'ам передаётся не списком параметров (идентификатор канала, причина, user-pointer, ...), а одним параметром-структурой -- struct connection_handler_args ARGS из которой конкретную информацию надо вытягивать макросами ca_name(), ca_state(), ca_puser(), ...
    • Есть также некие exception'ы, которые хбз что делают. Ключевое слово -- ca_add_exception_event().

    29.05.2019: несколько дополнений по типам, по результатам анализа db_access.h.

    • Тип ENUM имеет размер 16 бит (без знака):
      typedef epicsUInt16 dbr_enum_t;
    • Но и INT -- тоже:
      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, где много бит и не бывает.

    • И там НЕТУ нескольких вещей:
      1. Знакового int8 -- просто отсутствует, а есть лишь
        typedef epicsUInt8 dbr_char_t;
      2. 64-битных целых.
        • Отсутствуют как БД-типы, в т.ч. даже в base-7.0.2.2.
        • Хотя в epicsTypes.h сами по себе epicsInt64 и epicsUInt64 имеются.
        • И в base-7.0.2.2 (судя по сравнительному grep'енью) использование чуть шире:
          1. Используется как-то при работе с временами -- epicsTime.h "epicsUInt64 epicsMonotonicGet()".
          2. В pvif.h -- т.е., в PVaccess, а не в ChannelAccess -- уже есть поддержка, окружённая условиями:
            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?).

    • Насчёт строк: в обычном варианте, DBR_STRING, они очень короткие, всего до 40 символов. Это определяется так:
      • db_access.h --
        typedef epicsOldString dbr_string_t;
      • А epicsTypes.h --
        /*
         * !! 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)".

    • Структурные типы DBR_{STS,TIME,GR,CTRL}_xxx: ЧеблоПаша с ними не работал, ничего сказать не может.

      Но просто анализ .h-файлов показывает, что там кроме собственно данных (точнее, ПЕРЕД ними) передаются атрибуты:

      • STS (status) -- ещё status и severity (оба 16bit).
      • TIME -- дополнительно к предыдущим ещё "stamp" (тип epicsTimeStamp).
      • GR (graphic) -- кроме status и severity (но БЕЗ времени) ещё дофига всякого: units[MAX_UNITS_SIZE] (MAX_UNITS_SIZE=8) плюс 3 набора пределов: {upper,lower}_{disp,alarm,warning}_limit, все значения того же типа, что и value.
      • CTRL (control) -- аналогично предыдущему, но дополнительно есть 4-й диапазон, {upper,lower}_control_limit.

        Видимо, диапазон разрешённых для ввода значений? Если так, то это очень знакомо -- в CXv4 добавился диапазон ALWD.

      Т.е., вывод: надо при подписке требовать DBR_TIME_xxx (чтобы иметь timestamp), а в самом начале -- одноразово DBR_CTRL_xxx (чтоб получить свойства).

    • Чуть в сторону: посмотрел я интерфейс caget -- гы-гы-гы!

      Указание формата вывода в cdaclient & Co. сделано сильно элегантнее: просто указываешь желаемый формат, и всё.

      А в caget -- неочевидная спецификация "-e N" (и аналогично "-f N" и "-g N") -- буква опции указывает формат, а N -- число символов после десятичной точки. Ничего иного там указать нельзя (ни общей ширины поля, ни altformat, ни zero-pad), и формат "%a" никак не поддерживается. И, похоже, ключи указывают общий формат для ВСЕХ интересующих PV'ей (хотя это надо отдельно проверить изучением исходников).

    02.07.2019: пообщавшись с Гусевым, в разговоре, на тему "как/когда делается отправка в сокет", случайно узнал крайне ценную вещь:

    • У libca ЕСТЬ возможность добывать файловые дескрипторы, чтобы сбагривать их своему планировщику (основному циклу), дабы реагировать на приходящие данные/события СРАЗУ, а не раз в 0.1с.
      • Ключевое слово -- ca_add_fd_registration() (да, название дурацкое).
      • Она регистрирует функцию-callback, которой при создании и удалении libca'шного файлового дескриптора передаётся этот дескриптор и причина (создание/удаление).
      • А по приходу данных в дескриптор можно вызывать, например, ca_poll().
      • Так что можно этот дескриптор просто передавать его cxscheduler'у.
      • Одна проблема: поскольку для УДАЛЕНИЯ надо передавать уже не дескриптор, а sl_fdh_t, то надо будет завести какой-нибудь "реестр используемых дескрипторов"; напрашивается SLOTARRAY.

        P.S. В Xt аналогично -- там же тоже XtInputId, так что Гусев держит массив[20] с дуплетами {inputId,fd}.

    Итого: преспокойно можно регистрировать libca'шные дескрипторы и иметь реакцию на приход данных сразу же.

    ...только надо поэкспериментировать на тему сочетания этого с необходимостью вызова какого-нибудь ca_poll() после ca_array_put().

    07.07.2019: ища информацию насчёт EPICS4/EPICS7, наткнулся на некоторое количество информации касательно "классического" EPICS3/ChannelAccess.

    • "Introduction to EPICS" от NI! Там про интеграцию LabView с EPICS.
    • Краткий текст "Getting Started with EPICS" содержит примеры конфигов (sum.db).
    • В презентации "EPICS 7" от Kay Kasemir есть раздельчик "Review Channel Access" (стр.10), где упоминается такая цепочка команд:
      cd ~/epics-train/examples/first_steps
      cat first.db
      softIoc -m S=training -d first.db
      

      Вопрос: что это за "first_steps" и "first.db"? Где их взять?

    • Совсем отдельный вопрос -- а что такое "softIoc"? Не связана ли эта штука с "серверной библиотекой-реализацией Channel Access" -- тем, что необходимо для потенциального cxsd_fe_epics.c?

    09.07.2019: насчёт документации по протоколу CA:

    • В "классическом" описании "Channel Access Protocol Specification" (живущем ныне на anl.gov, а epics.cosylab.com "Server not found"), есть предупреждение (красным цветом):
      This is an old version of the Protocol Specification. Newer versions are available from the pages specific to each version of Base.
    • И действительно, в "веб-документации" к "3.16" (это ж предок 4?) есть свой файл "Channel Access Protocol Specification".

      При нём значится версия 1.5 (словенские закончились на 1.4 от 2008-02-07), автором там Michael Davidsaver, комментарий "Major revision to describe operation semantics". И да, есть картинка "TCP Message Flow", показывающая диаграмму переходов между состояниями.

  • 29.05.2019: приступаем к реализации, уже с новыми знаниями. В качестве основы возьмём имеющийся с 2014-го файл.

    29.05.2019: организационное -- о структуре директорий и системе сборки.

    • work/frgn4cx/ -- общая директория для всего, взаимодействующего с "внешними" системами управления.
    • В ней поддиректории по "внешним системам", а в них -- поддиректории по модулям/подсистемам. Примерно так:
      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, содержащего ВСЕ возможные плагины.

    • В директории "подпроекта" -- в данном случае frgn4cx/epics/ -- помещаем файл FrgnRules.mk, в котором определяются общие вещи, необходимые для сборки с этой внешней системой.

      В частности, тут на данный момент --

      EPICS_BASE_DIR=         /tmp/base-3.15.6
      EPICS_INCLUDE_DIR=      $(EPICS_BASE_DIR)/include
      

    Итак, "вариант 2014 года" на новом месте собирается, теперь пытаемся идти дальше.

    • Шаг номер 1: добавляем #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 указывать?!

    • (К сожалению, этот аспект у Гусева подсмотреть не удалось: у него 3.14.12.3, а не 3.15, так что, возможно, там этого ещё просто не было.)
    • ...после grep'енья 'compiler/gcc' по всей base-3.15.6/ оказалось, что, судя по документации, html/CAref.html#Building -- таки да, именно надо!
      1. Там сказано:
        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'нья сторонними программами -- НЕТУ!

        ...господи, ну и придурки!!!

      2. И даже более того: в комментарии к #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ц... Просто охренительное "качество" системы сборки...

    • Добавил и её. Откомпилировалось, наконец-то.
    • 06.06.2019: поговорил с Лёшей Герасёвым. Он сборкой C'шного кода особо не пользуется, но подтверждает, что действительно надо отдельно "указывать ОС и компилятор".

      Его версия -- потому, что в одном дереве base*/ может вестись сборка под разные платформы одновременно, потому "приложение должно знать, подо что оно собирается".

    06.06.2019: наполняем модуль функционалом.

    • Битым текстом: модуль тогда, в 2014-м, был создан на основе VCAS'овского, а не на основе cda_d_cx.c.

      Это отражается, в частности, в модели менеджмента hwr'ов: тут список привязан к sid'у, а не один сквозной по всем sid'ам.

      12.06.2019: а вот теперь переделан на сквозной.

    07.06.2019: продолжаем наполнять.

    • Функции трансляции между типами и алармами/rflags засунуты в отдельный файл epics2cx_conv.h:
      • 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 точного соответствия нету, поэтому строки пока никак не обрабатываются -- в обоих направлениях возвращается "упс!".

    • Примерный план:
      • _snd_data(): конверсия типа и отправка.
      • StateChangeCB():
        1. При FOUND заказывать одноразовое чтение CTRL.
        2. Отдавать наверх статус.
      • GetPropsCB(): отдавать наверх полученные свойства.
      • _new_chan(): оформлять подписку
      • NewDataCB(): отдавать наверх данные
      • TYPE_CHANGE_SUPPORTED=NO
    • ...кстати, раз "конструктор из кубиков" пока не вполне юзабелен, то можно в 4cx/src/programs/utils/ ввести подготовку для расширябельности cdaclient.c и das-experiment.c -- аналогично тому, как сделано в pult.c, где есть "вакантные места" для добавления списков плагинов всякими weldclient/liuclient.

      Плюс, в любом случае не помешает завести там какой-нибудь .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_-типа.
    • Как оказалось, "конструктор из кубиков" очень даже юзабелен -- сервер (cxsd) уже сделан на нём, и для cda-утилит командной строки функционала также уже достаточно.
    • @бердск-попова-9, лёжа на диване с Покетом и пытаясь не уснуть: насчёт выбора между CXDTYPE_UINT8 и CXDTYPE_TEXT в NewDataCB(): при DBR_CHAR можно смотреть по hi->dtype.

      ...впрочем, проблемы "как обходиться с DBR_STRING, которая всего [40], но это 40 -- при count=1" это не решает.

    11.06.2019: далее.

    • Наполняем NewDataCB().
      • Неясность с alarm'ами: откуда брать значение -- из dbr_time_NNN->status или ->severity?

        Гусиный код берёт из status. Но почему так, и в чём вообще смысл этих ДВУХ полей и различий между ними -- неясно. Карнаев не смог ответить, отправил к ЧеблоПаше или Гусеву.

        12.06.2019: написано вопросительное письмо ЧеблоПаше, ждём ответа.

        20.06.2019: давно посетила идея: похоже, ихнее severity -- это вроде CX'ного knobstate_t, т.е., "высокоуровневое" состояние, вычисляемое из аппаратного кода состояния.

      • Вылезла неприятность: ChannelAccess передаёт всего ОДИН "private pointer", из которого нужно добыть аж ДВА параметра: sid/me и hwr.

        Как поступим?

        Единственным разумным решением выглядит перейти на СКВОЗНУЮ нумерацию hwr'ов, а не привязанную к серверу.

    12.06.2019: ...

    • Да, перейдено на сквозную нумерацию hwr'ов -- всё скопировано из cda_d_cx.c.
    • Но добавлена особенность: поле hwrinfo_t.me, являющееся ссылкой на сервер-"содержатель".

      Заполняется в AddHwrToSrv().

    12.06.2019@вечер, около 18:10, пешком по Лаврентьева вниз: насчёт "как обходиться с DBR_STRING, которая всего [40]..." -- можно требовать помечать такие каналы суффиксом "@s", и тогда модуль будет сохранять оный факт булевским флагом в hwrinfo и модифицировать своё поведение при получении и отправке данных.

    14.06.2019: ...

    • StateChangeCB() сделана. Она:
      1. Определяет найденность канала.
      2. Если найден -- то заказывает однократное чтение CTRL's через GetPropsCB()
      3. Уведомляет "верха" посредством cda_dat_p_report_rslvstat().

      А не надо ли ввести per-hwr-флажок "запрос на CTRL послан", который взводить перед заказом и сбрасывать при получении? Смысл -- чтобы при обрыве соединения ДО получения CTRL-ответа и последующем восстановлении запросы бы не множились.

    • Также наполнена GetPropsCB(): там разбирается, что пришло, и возвращается наверх units (остальные 7 строк -- NULL) и диапазон.

      Отдельно стоит упомянуть пару типов-нюансов:

      1. DBR_CTRL_ENUM -- с ним пока вообще ничего не делается, ибо и возвращать там нечего.

        Теоретически можно было бы сгенерить multistring "items", из dbr_ctrl_enum.strs[], но его даже возвернуть нечем -- нет в cdaP вызова для этого.

      2. DBR_CTRL_CHAR: оно рассматривается как UINT8, хотя в будущем может понадобиться отдельно воспринимать как TEXT.

    20.06.2019: после почти недельной паузы (вызванной вознёй с DAQmx) возвращаемся сюда.

    • cda_d_epics_snd_data() наполнена.

    Засим "примерный план" от 07-07-2019 выполнен, и модуль вроде должен быть готов к использованию (точнее, к тестированию). Естественно, с понятными ограничениями/недоделанностями:

    1. Не дешифрируются и не используются CA'шные timestamp'ы.
    2. Поддерживаются только числовые скаляры и массивы, строки же -- нет.

    24.06.2019@утро-дома, перед выходом на работу: ещё идея на тему "как обходиться со строками, которые в варианте DBR_STRING всего [40]":

    • Судя по ключу "-S" у caget ("Print array of char as a string (long string)"), строки передаются именно как массив char'ов.
    • Массивы БАЙТОВ (как целых чисел, а не символов) вряд ли имеют широкое использование в системах управления.
    • Ну так -- и маппировать CXDTYPE_TEXT<->DBR_CHAR! А CXDTYPE_INT8 -- типа "не поддерживается".

    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().

      Но это так, для порядку. Вряд ли он будет использоваться.

    • И тут дело дошло до проверки на живых EPICS'ных каналах -- проверил на оставшейся на ВЭПП-5 гусиной системе cvm ("Current/Voltage Monitor").

      Ура -- работает!!!

      Некоторые детали:

      • Проверялось на скалярах, объявлявшихся как double.
      • В соседних окнах были запущены "cdaclient -m" на 3 канала и camonitor на эти же 3 канала.

        Данные печатались на экране одновременно (хотя 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().
    • ...и не забыть проверить timestamp'ы -- какое там "расхождение", и понять, как приводить EPICS'ные к CX'ным.

    27.06.2019: проверяем запись. Тестируем на гусином CVM, пытаясь записать что-нибудь в поле V5:PA:PhaseVoltage1M.HOPR (изначально =300).

    Фиг -- никакой реакции. И в варианте "cdaclient -w" -- тоже.

    28.06.2019: продолжаем разбирательство.

    • Сначала проверяем, насколько это поле вообще писабельно (а вдруг оно readonly?).

      Писабельно -- caput туда 400 занёс.

    • Окей, следующая гипотеза: возможно, cdaclient вызывает запись, а та не успевает реально отработаться до выхода.

      Для проверки кроме записи в список каналов добавлен ещё один читаемый -- чтобы программа сразу не завершалась.

      И всё равно фиг -- не отрабатывается.

    • Ладно, добавил сразу после ca_array_put() ещё ca_pend_io(5.0) (чисто для теста).

      Тоже не помогло, и никаких 5 секунд явно не ждало.

    • Осталось подозрение, что как-то неправильно передаю какие-то параметры (как минимум dtype транслируется -- вдруг ошибся?).

      Значит, нужно поставить отладочную печать и посмотреть, что передаётся.

      Поставил, и...

    • ...и ничего!!! Т.е., cda_d_epics_snd_data() просто не вызывается!!!

      Т.е., косяк не в работе с EPICS, а где-то внутри cda, в связке cda_core<->cda_d_epics -- но это намного лучше, т.к. уж в своём произведении разобраться проще!

    • Разбирательство: припомнил, что в cda есть понятие "готовность канала": dat-плагин должен сообщить, что канал готов к работе. Пока оное не сделано, операции записи будут только складываться в cda'шный буфер, но НЕ передаваться dat-плагину.

      И это -- cda_dat_p_set_ready() -- вызывается ОТДЕЛЬНО от cda_dat_p_report_rslvstat() (который вообще реально влияет только на "внешний вид" -- timestamp и флаги, но НЕ на функционирование).

      Ну, добавил. Не помогло...

    • Исследованием пути отправки данных (напихиванием отладочной печати в cda_core) была вспомнена вторая преграда, более общая: сам sid должен быть в состоянии OPERATING, иначе запись также просто складируется в буфер.

      И -- да, cda_d_epics_new_srv() возвращал CDA_DAT_P_NOTREADY; осталось ещё с 2014 года.

      Заменил на CDA_DAT_P_OPERATING -- аллилуйя, заработало!!! Запись работает!!!

    • Окей, теперь проверяем в варианте БЕЗ ca_pend_io():
      • Тоже работает.
      • А вот вариант вызова "cdaclient -w" (когда запись делается сразу же после регистрации канала, НЕ дожидаясь обновления) -- не работает НИ В КАКОМ варианте.

        (По отладочной печати видно, что сначала "производится" запись, а лишь потом вызывается StateChangeCB() с op=6:CA_OP_CONN_UP.)

      • А с CX -- работает.

      Выводы:

      1. Отправка данных идёт то ли сама, то ли достаточно периодического ca_poll().

        Но ещё надо бы посмотреть, насколько "оперативно" выполняется запись: если через 0.1с (максимум, в среднем -- 0.05с), то это нехорошо.

      2. Буферизация при "неустановленном соединении" в EPICS отсутствует?

        Очевидно, он просто отбрасывает запрос на запись в ещё неприконнекченный канал.

        02.07.2019: пообщался с Гусевым на эту тему.

        • Впрямую он ответить не смог, так ли это ("не буферизует").
        • Но поэкспериментировав на его софтине, установили, что на еще неприконнекченных каналах сам ca_array_put() ошибки не даёт, а ругается следующий за ним ca_pend_io().
        • Моя гипотеза:
          1. Мы знаем, что все ca_array_{put,get}() сами ничего не пишут, а лишь ставят запрос в очередь.
          2. Возможно, действует предположение, что к моменту, когда (пусть через некоторое время) дойдёт дело до исполнения этого элемента очереди, то канал "успеет разрезолвиться".

        Ну ладно -- просто примем к сведению, что в EPICS'е писать в ещё неприконнекченные каналы не положено.

    Итого: запись скаляров работает. Дальше надо будет проверять на чём-нибудь повекторнее -- может, на ВЭПП-4 что будет.

    28.06.2019: кстати, посмотрел на запущенную программу epics_cdaclient на тему "сколько thread'ов и куда лезет".

    • "ps axuwww -L" показал аж 6 thread'ов.
    • Сокетов 3 штуки (посмотрены через "netstat -anp"): один TCP, с сервером, и аж 2 штуки UDP.

    Также попробовал "поиграться с периодичностью вызовов ca_poll()". Поставил вместо 0.1с целых 5с.

    • Да, О-О-ОЧЕНЬ всё стало тормозно. Реально отдаёт раз в 5 секунд толпу обновлений.
    • А запись отрабатывается через 10 или 15 секунд -- т.е., 2 или 3 цикла вызова 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'а, иначе -- разрегистрирует.
    • Для хранения списка дескрипторов (необходимого для разрегистрации) используется SLOTARRAY "Fdi" (FileDes Info), хранящий дуплеты 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().

    Результаты:

    • Да, помогает.
    • Даже при переключении интервала периодического поллинга с 0.1с на 5с -- всё равно хорошая "реактивность", данные приходят резво.
    • С другой стороны, влияние наличия/отсутствия ca_poll() сразу после ca_array_put() понять не удалось:
      • При интервале поллинга 5с и отсутствии регистрации дескрипторов -- запись тормозит.
      • При присутствии регистрации дескрипторов зависимость от интервала поллинга и наличия ca_pend_io() либо ca_poll() не видна.

      Будем наблюдать и экспериментировать дальше (на живом использовании посмотрим).

    В общем -- полезная фича, спасибо Гусеву за подсказку.

  • 11.06.2019: приступаем к созданию и наполнению директории frgn4cx/epics/util/, и на её примере -- всех директорий frgn4cx/*/util/ (а также, возможно, frgn4cx/*/xmclients/).

    (Надо ж на чём-то тестировать cda_d_epics!)

    11.06.2019: краткий манифест:

    • Собственно "наполнение" заключается только в Makefile -- всё остальное там генерится и симлинкуется.
    • Делается содержимое этого Makefile с такимприцелом, чтобы потом вытащить общие части в отдельные .mk-файлы, доступные для include'нья из разных директорий.
    • На данный момент напрашивается 1 такой файл -- под условным названием 4cx/src/programs/utils/FrgnUtilRules.mk. В нём будут стандартные определения правил, касающиеся специфики именно этих утилит (и зависимости, и вдруг добавятся ещё какие), параметризуемые значением переменной $(FRGN) (в данном случае она =epics).
    • Очень возможно, что этот файл придётся разделить на 2 части: 1-я для включения ДО "include GeneralRules.mk", а 2-я после.

      12.06.2019: либо, возможно, вытащить определение TOPDIR= из PrjRules.mk в отдельный PrjDefs.mk, чтоб его можно было include'ить отдельно, ещё ДО "include $(PRJDIR)/PrjRules.mk".

    Что удалось и какие проблемы:

    • Проблема с указанием сложных значений переменных в SPECIFIC_DEFINES.

      12.06.2019: забэкслэшить #, превратив его в \#?

      20.06.2019: да, так и сделал -- помогло.

    • Проблема с несимлиокнванием console_cda_util.c ДО компиляции.

    21.06.2019: допиливаем собираемость.

    • Makefile ещё подпеределан, так что всё стало симлинковаться и собираться.
    • Замечание: это пока чисто локальная поделка, для проверки. В дальнейшем нужно будет изготовить что-нибудь более модулябельное, какой-нибудь .mk, годный для include'нья разными директориями frgn4cx/*/util/.
    • Из основной 4cx/src/lib/cda/ удалён cda_d_epics.c и ссылки на него из cda_plugmgr.c.
    • ...а потом пытаемся-таки добиться не просто "собираемости", а чтоб builtins'ы подключались.

      Тут-то стало ясно, что первоначальная идея делать в Makefile определение вида

      SPECIFIC_DEFINES=-DBUILTINS_DECLARATION_CODE='#include"main_builtins.h"'
      
      и затем в файлах, могущих быть так твикнутых (cdaclient.c etc.),
      #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 */
      
    • Сам "базовый набор EPICS 3.*" поселен в директорию ~/compile/base-3.15.6/ -- она теперь и указывается вместо былой /tmp/base-3.15.6/ в frgn4cx/epics/FrgnRules.mk.
    • Потом ещё масса танцев с бубном -- для подцепления нужных библиотек (-lca -lCom; поскольку тянут за собой всякое (вроде libstdc++), то проще оказалось использовать динамические)...
    • ...и в конечном итоге всё собралось. Правда, запускать пока что нужно с префиксом LD_LIBRARY_PATH=~/compile/base-3.15.6/lib/linux-x86_64

    Всё -- первоначальный вариант собирается и запускается. Теперь можно проверять функциональность.

    23.06.2019@дома-ванна: если подумать, то ЮЗЕРАМ нафиг не сдалось иметь отдельные утилиты, им нужна обычная "cdaclient", чтобы умела работать с EPICS, и чтобы "обычная" libcda.a, прилинкованная к программе, умела бы доступаться к EPICS/TANGO/...

    Напрашиваются следующие пути:

    1. Как-то держать cda_d_epics.c и прочие прямо в основном дереве, а собирать и влинковывать их "условно" -- при наличии соответствующих include'ов и библиотек.
    2. Уметь подгружать модули динамически -- из cda_d_epics.so, прямо в момент старта софтины.

      Главный вопрос даже не в загрузке модуля (технология отработанная на драйверах), а в том, КАК УКАЗЫВАТЬ на необходимость загрузки?

      • Ключ у программы?

        Не катит, юзерские-то программы никаких ключей понимать не станут.

      • Переменными окружения указывать?

        В принципе, нормуль. Хотя вопрос секьюрности... (сбрасывать при 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".

    • У нас же в CXv4 есть СОБСТВЕННАЯ система конфигурирования, в виде Config.mk+Sysdepflags.sh, и она устанавливает такие вещи, как OS=LINUX и CPU=X86_64.

      Может, из этой информации можно что-нибудь выжать?

    • Чуть поразбирался, как же устроена система конфигурирования в сборке EPICS:
      • base-3.15.6/Makefile include'ит configure/CONFIG, ...
      • ...в свою очередь, пытающийся узнать информацию (т.н. "EHA" -- EPICS_HOST_ARCH) вызовом скрипта $(EPICS_BASE)/lib/perl/EpicsHostArch.pl либо $(TOP)/src/tools/EpicsHostArch.pl (последний -- видимо, исходник).

        Скрипт коротенький и простенький; он и выдаёт "linux-x86_64".

      • Так что б нам не пользоваться этим же скриптом в FrgnRules.mk?

        ...естественно, с надлежащими "предосторожностями" в виде проверки наличия скрипта.

      • 17.03.2023: в продолжение вопроса "а как бы НЕ хардкодить":
        • Скрипт EHA не катит потому, что фиг ты им воспользуешься: на формально-не-поддержкиваемых EPICS'ом системах этот скрипт вместо результата выдаёт ругательство вроде
          .../src/tools/EpicsHostArch.pl: Architecture 'e2k-linux' not recognized
          
          плюс код возврата 255.
        • В то время как "свои" $(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 закомменчены, чтоб не смущать своим наличием.

    • А вот где оно берёт "gcc" -- необходимое для EPICS_COMPILER_INCLUDE_DIR=$(EPICS_INCLUDE_DIR)/compiler/gcc -- пока загадка.

    24.06.2019: продолжаем.

    • (Поскольку триггером к созданию frgn4cx/ стала именно поддержка EPICS, то записываем тут.)

      Несколько неудобно устроено, что сборка/очистка в frgn4cx/ делается не автоматически (make в верхнем узле), а индивидуально в каждой поддиректории (как минимум -- в директории под-проекта (frgn4cx/epics/).

      Поэтому пытаемся сделать, чтобы оно САМО определяло возможность собирать, а при отсутствии -- отключало бы.

      • Определять можно с использованием информации из FrgnRules.mk -- например, проверкой наличия .h-файла.

        Но делать это можно в директории ПОД-проекта, а не в самой frgn4cx/.

      • Итого: в frgn4cx/Makefile к списку SUBDIRS директории под-проектов добавляются безусловно.
      • А в frgn4cx/epics/Makefile include'ится тамошний FrgnRules.mk и затем проверяется наличие файла $(EPICS_INCLUDE_DIR)/cadef.h -- если есть, то делается SUBDIRS= cda util, а иначе генерится $(warning ....
      • Дополнительный плюс такого подхода -- он корректно работает при указании EPICS_BASE_DIR=... из командной строки.
      • В frgn4cx/daqmx/Makefile пришлось сжульничать и поступить аналогично, проверяя наличие файла $(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 и пытается обращаться с ним как с обычным компонентом, для которого надо мониторить исходники и проверять зависимости объектников.
    • Решение очевидно: надо переходить с нынешней кривой схемы (НЕФИГ использовать .o-файл из другой директории!) на влинковывание заранее подготовленной .a-библиотеки.
    • Благо, в epics/cda/Makefile это уже подготовлено, просто не включено.
    • "Включаем", заодно меняя имя библиотеки с некорректного cda_d_epics.a на libcda_d_epics.a.
    • И использование делаем -- всё стало хорошо.

    При подготовке .a'шки также была переименована .so, из cda_d_epics.so в libcda_d_epics.so. И тут началось... Она просто перестала собираться -- make тупо ничего не делает!

    Пытаемся разобраться:

    • Не делается ничего по непонятной причине: в выводе "make -p" значится:
      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"!

    • При переименовании обратно в cda_d_epics.so -- собирается.
    • Очевидно, причина в строке %.so: %.o в GeneralRules.mk.

      Как-то уж так с давних пор исторически сложилось, что все собираемые в CX (ещё с первых версий, когда только была задействована динамическая загрузка) .so'шки -- обязательно имеют одноимённый .c-файл.

    • И убирать эту строку НЕЛЬЗЯ, т.к. именно на ней основывается сборка однофайловых модулей -- драйверов и layer'ов (а потенциально -- и ext'ов с lib'ами; разве что единственный на данный момент frontend -- cxsd_fe_starogate.so -- имеет определение cxsd_fe_starogate.so_COMPONENTS; причём, видимо, не особо нужное).
    • В этих условиях наиболее простым решением выглядит создавать пустой файл с именем "libcda_d_epics.c" -- тогда make будет счастлив.

    27.06.2019: разбираемся с проблемой сборки libcda_d_epics.so.

    • Добавил создание пустого libcda_d_epics.c, а также стирание его путём добавления в 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: попробовал -- фиг, ничего не изменилось.

    • И, судя по access time от stat, во время сборки никто этот файл не читал.
    • Может, дело в том, что файл -- CONFIG_SITE.Common.linux-x86, а нужно что-нибудь на тему x86_64?
    • Да, есть такой -- CONFIG.linux-x86_64.linux-x86_64; он сводится к include $(CONFIG)/os/CONFIG.linux-x86.linux-x86, в том ещё include, и т.д.; и указано в комментариях, что "Sites may override these definitions in CONFIG_SITE.linux-x86.linux-x86". А вот CONFIG_SITE.Common.linux-x86 в этой цепочке, похоже, никак не фигурирует.
    • Прямое указание "LDLIBS_READLINE = -lreadline" нашлось в CONFIG.Common.linuxCommon.

      Там хоть и есть в комментах странная оговорка "Link libraries controlled by COMMANDLINE_LIBRARY", но никаких указаний на возможность отключить readline полностью уже нет.

      И вот ЭТОТ файл уже используется -- судя по access time.

      30.03.2023: нашёл рецепт: надо было В КОМАНДНОЙ СТРОКЕ make указать "COMMANDLINE_LIBRARY=EPICS".

      • В самих configure/os/-файлах на эту тему толком ничего не сказано -- надо обладать некоторым пред-знанием, чтобы осознать, что значение "EPICS" означает "использовать внутреннюю реализацию редактора командной строки".
      • Не получилось тогда в 2019-м отключить, видимо, из-за навёрнутой системы CONFIG.-файлов: где-то уставляешь значение (и фиг знает, используется ли ЭТОТ файл при сборке), а другим файлом оно меняется.

        Указание же make'у параметром в командной строке имеет приоритет.

      • Кстати, grep'я по этим файлам и прочим, составляющим ихнюю "систему сборки", понял, что означало нашедшееся тогда указание "LDLIBS_READLINE =": это определяет некое значение, ОДНО ИЗ LDLIBS_*, а какое из них выбрать -- определяется уже значением $(COMMANDLINE_LIBRARY), при помощи конструкции вида "$(BASENAME_$(VARIANT_NAME))".

        В частности, в src/libCom/osi/Makefile есть конструкция

        epicsReadline_CFLAGS += -DEPICS_COMMANDLINE_LIBRARY=EPICS_COMMANDLINE_LIBRARY_$(COMMANDLINE_LIBRARY)
        epicsReadline_INCLUDES += $(INCLUDES_$(COMMANDLINE_LIBRARY))
        
        а в configure/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))
        
        (ага, там же и умолчание ставится; в какой момент include'ится этот CONFIG_COMMON -- я не разбирался).

        Т.е., то определение 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;

    • А вот егойный epicsReadline.h включается только в src/libCom/iocsh/iocsh.cpp -- и при чём тут libca.so?
    • Догадался погуглить на тему «libca.so readline site:anl.gov», и среди прочего встретился вполне конкретный ответ от Andrew Johnson за 30 Jan 2017 "Re: Problem installing PyEpics", в котором есть ключевая фраза:
      The core of iocsh is a part of libCom.

      Мда... Это прямо "As if some IP implementation included an integrated ksh".

    • Так-то я и раньше уже заимел подозрение, что реальная зависимость -- у libCom.so, потому и libca.so вместе с libcas.so страдают.
    • 07.05.2023: тем не менее, не удержался и написал в tech-talk вопрос "Why does libca.so depend on libreadline.so?"
    • 08.05.2023: Michael Davidsaver ответил:
      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.

  • 11.09.2019@утро-дома: некоторые соображения "насчёт frgn4cx вообще":
    1. Понятно, что от frgn4cx/*/util/ надо избавляться -- незачем они теперь, при наличии динамической загрузки dat-плагинов в cda.

      Но сделаем это чуть позже.

      01.10.2019: сделано.

    2. А вот что НЕОБХОДИМО будет проделать -- так это как-то улучшить систему сборки плагинов, чтобы "профильные" зависимости (include-файлы библиотеки) могли бы подхватываться не только из определынных в frgn4cx/*/FrgnRules.mk мест, но и из "общесистемных" -- когда соответствующий фреймворк как-то установлен в систему и это настроено (т.е., "просто #include" добудет файл, а "-lNAME" -- библиотеку).

      Может, использовать какой-нибудь make-параметр, указывабельный в командной строке -- например, FORCE_EPICS_PRESENCE и FORCE_TANGO_PRESENCE? Чтобы при их наличии НЕ определяло бы свои пути в FrgnRules.mk.

    3. И нужно будет завести директорию frgn4cx/doc/, где описать эти вещи -- для упрощения использования сторонними юзерами.
  • 31.01.2020@институтская-сессия-ИЯФ-Пиминов, ~11:00: а если два РАЗНЫХ контекста/sid'а (например, от двух bridge_imp-драйверов в сервере) попросят один канал? Не получится ли, что он будет отдаваться только первому?
  • 06.03.2020: надо делать хоть какую-то поддержку строк.

    Задача настолько глобальна (и вместе с тем обособленна), что требует своего собственного раздела.

    Последние пару дней занимался обдумыванием, как бы лучше организовать, и анализом того, что же и как устроено в EPICS/CA касательно строк и enum'ов.

    В конце концов принят следующий проект:

    1. Наводим соответствие DBR_STRING<->CXDTYPE_TEXT.

      Да, ограниченновато, но см. п.2.

    2. Пусть CX умеет сам делать преобразование типа между векторами CXDTYPE_UINT8 и CXDTYPE_TEXT (uint8[] и char[]).

      (Тогда, например, можно будет объявлять каналы как TEXT, а "внутри" запрашивать у CA векторные DBR_CHAR; впрочем, эта часть идеи до конца не оформилась.)

      Вчера такое преобразование было реализовано.

    (Также была мысль "выбирать разные варианты в зависимости от nelems: <40 -- DBR_STRING[1], >=40 -- DBR_CHAR[nelems]. Да, пришлось бы чуток поменять API функций наведения соответствия между типами, и стало бы кривовато.)

    06.03.2020: делаем. Оказалось весьма просто:

    • Типопреобразования в epics2cx_conv.h
      • DBR2cxdtype(): теперь при dbr_type_is_STRING() выбирается CXDTYPE_TEXT.
      • cxdtype2DBR(): тут обратно -- CXDTYPE_TEXT возвращает base+DBR_STRING.
    • Собственно работа в cda_d_epics.c:
      • В NewDataCB() чуток нетривиально:
        • Установка value_p на начало данных сделана была ешё изначально, прошлым летом -- полностью аналогично прочим.
        • Но в случае с DBR_STRING есть засада: ведь в терминах EPICS это ОДИН элемент данных (да, проверено -- 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(), где это чтение свойств заказывается.

    Проверяем:

    • Сначала был косячок с некорректным отрезанием нулей в конце -- оно смотрело символ [nelems] вместо [nelems-1]. Исправлено.
    • Далее: получение из строк (DBR_STRING) -- работает (проверено на VEPP3:TableName-RB).
    • Затем попробовано на DBR_ENUM-канале, 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.

    Вообще-то надо бы проверить, как оно себя ведёт, но, скорее всего, плохо.

    Для исправления же нужно:

    1. Передавать nelems=1 -- это самое тривиальное.
    2. Но если изначальное nelems <40 -- точнее, sizeof(dbr_string_t) -- то копировать имеющийся размер в ЛОКАЛЬНЫЙ буфер, забивать недостачу NUL'ами, и уже его потом отправлять.

      Смысл в том, что если клиент пишет строку короче 40 символов, то libCA в процессе вычитывания ВСЕГДА 40 мало того, что вычитает ещё и мусор, но может и вообще за границы аллокированной памяти вылезти и SIGSEGV'нуться.

    Сделано; при DBR_STRING:

    1. Форсится nelems=1,
    2. но предварительно проверяется, что если nelems меньше 40, то указанное количество копируется в локальную dbr_string_t local_string_buf, оставшиеся байты нулятся, и перекидывается value = local_string_buf.

    Надо будет найти время проверить (в т.ч. и неработу старого варианта).

    18.07.2022: проверил -- причём не на настоящем IOC'е, а на cxsd_fe_cx :D Итак:

    • НОВЫЙ вариант -- работает.
    • СТАРЫЙ -- работал только вариант с 1 символом (сводившийся к 1 строке).
    • Старый хотя бы с 2 символами -- обламывался с сообщением
      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 & )
      • а таковой exception генерит, в частности, ca/client/comQueSend.cpp'шный 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.

  • 18.07.2022: отсутствовала "поддержка циклов" -- генерации событий CDA_CTX_R_CYCLE. Сделана.

    18.07.2022: зачем оно:

    • Оно как бы нативно-то и нафиг бы не нужно ибо в EPICS на уровне -- клиентов нет никаких "циклов".
    • Но занадобилось для экзотической цели: проверить работу cxsd_fe_epics путём коннекченья к CX-серверу по протоколу "epics::".
    • При первой попытке так приконнектиться скрин был с бледноголубыми-JUSTCREATED каналами и безо всякого обновления.

      При этом мониторинг тех же каналов cdaclient'ом прекрасно работал.

      Долго пытался сообразить, что же может быть не так, а потом допёрло: во frontend'е ВСЁ так, а проблема на стороне клиента -- ему нужны события циклов.

    Сделано:

    • Добавлено поле cda_d_epics_privrec_t.cycle_tid.
    • Срабатывание заказывается прямо в cda_d_epics_new_srv(), в cda_d_epics_del_srv() подчистка.
    • Собственно CycleProc() сначала делает cycle_tid = -1 (от греха подальше -- ведь дальше cda-вызов с потенциально далёкими последствиями), затем cda_dat_p_update_server_cycle(), и в конце повтор заказа таймаута.
    • Период цикла захардкожен -- 1 секунда, 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 -- нет (ибо без надобности).

  • 20.07.2022: разбирался с 3 странностями, вылезающими при работе с CX посредством Channel Access -- т.е., цепочка
    cdaclient <-> Channel Access <-> cxsd
    или, детальнее,
    cdaclient <-> cda_d_epics <-> libCA <-> libCAS <-> cxsd_fe_epics <-> cxsd

    Странности проявлялись при попытке cdaclient'ом читать и, особенно, писать строковый канал через "epics::", и они таковы:

    1. Команда "cdaclient -Da epics::asdf.2" выводит какую-то странную информацию "hwinfo":
      hwinfo(epics::asdf.2): r? x-1
      -- что это за "r?" и "x-1"?
    2. Если сначала записать длинную строку, а потом покороче, то после короткой отображается какой-то мусор.

      Например, "cdaclient -m @t100:epics::asdf.2" после пары записей (хоть через "cx::", хоть через "epics::") -- сначала ="asdf1234", затем ="asdf" -- показывает "asdf1\x7f".

    3. Запись в такой канал почему-то происходит через несколько секунд, а не сразу.

      Почему? Первая мысль была -- нет события "обновление", по которому cdaclient и выполняет запись. Но тогда запись не должна бы происходить вовсе, а она всё же идёт -- что её вызывает?

    19.07.2022: результаты разбирательства:

    1. Тут всё просто:
      • Выдача hwinfo делается по событию CDA_REF_R_RSLVSTATCDA_RSLVSTAT_FOUND), ...
      • ...и в случае CX этому событию -- точнее, NT_OPEN_FOUND_STAGE2 -- предшествует получение пачки chunk'ов с информацией о канале.
      • Потому cdaclient предполагает, что именно по RSLVSTAT и надо выдавать hwinfo (а когда б ещё? в оном свойства и присылаются -- они как раз при успешном резолвинге становятся известны).
      • Но в случае с "epics::" это не так -- никто никакие свойства не отдаёт.
      • И в ri->hwr_* лежит то, что попало при инициализации reset_hwinfo()'й: dtype=UNKNOWN, а rw,max_nelems,srv_hwid=-1.
      • А они именно так и показываются: "r?" -- это hwinfo_rw ни 0, ни 1; "x" -- CXDTYPE_UNKNOWN, "-1" -- просто значение hwinfo_nelems=-1.

      Что тут можно сделать? В принципе, "cainfo" ведь многое из нужной информации получать умеет, так что и мы можем -- видимо, надо заказывать какое-то лишнее чтение (каких-то свойств?). Вопрос -- а НАДО ЛИ?

      23.10.2022: сделано, cda_dat_p_set_hwinfo() передаёт добытую информацию.

    2. Судя по тестам, "стек библиотек" libcas/libgdd/aitString ведёт себя некорректно: оно -- по результатам
      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 за сегодня.

    3. Время паузы до отработки записи равно CA_HEARTBEAT_USECS.

    20.07.2022: продолжаем разбираться с п.3 -- странностями "scheduling'а" связки cda_d_epics+libca.

    • Ещё вчера осознано, что возможная причина "паузы" -- в том, что на каналы записи не отдаётся никакого "текущего значения":
      • Обычный-то EPICS'ный IOC значение присылает сразу, причём с timestamp'ом "сейчас", а не с временем последнего изменения.
      • А cxsd_fe_epics -- нет, т.к. клиент чтения не заказывал, а причин по своей инициативе что-то слать просто нету.

      ...вот только такая "пауза" должна б была быть вечной -- пока не произойдёт реального обновления канала

    • Проверено, что размер паузы до отработки записи равен периоду поллинга, было при помощи изменения значения CA_HEARTBEAT_USECS: изначально 5000000 микросекунд -- оно и тормозило на 5 секунд, меняем на 3000000 микросекунд -- 3 секунды тормозит.
    • ЧТО именно вызывает такой эффект -- категорически неясно: ведь механизм с ca_add_fd_registration() работает и дескрипторы должны вычитываться сразу же при появлении в них данных.
    • Может, какой-то ТАЙМАУТ? Ведь для оных никакого механизма регистрации их в основном цикле нету...
    • Пытался разобраться при помощи strace+ltrace -- фиг: оно порождает несколько лишних thread'ов, которые занимаются фиг знает чем (в т.ч. один запускает caRepeater (предварительно делая close() всем дескрипторам), а другой циклит на futex()'е).
    • Видимо, придётся напихивать толпы отладочной печати для установления цепочки происходящих событий.

      21.07.2022: а ещё для проверки можно заказать cdaclient'у в дополнение к каналу для записи и ВТОРОЙ канал, только чтения -- чтоб в него обновление пришло.

    • @~17:00, подходя к М-46: посмотреть, есть ли ещё какие-то методы post*() в casPV -- такие, как, например, access rights change (или оно только в casChannel?).

      Идея в том, чтобы генерить какие-то ещё события, которые бы вызывали передачу информации клиенту и тем самым пробуждение по файловым дескрипторам.

      @~17:40, М-46: "есть" -- postEvent() с ДРУГИМИ масками событий.

    • @~17:30, М-46: просмотр кода показывает, что "свойства" добываются ПОСЛЕ события RSLVSTAT: в StateChangeCB()
      • Заказывается DBR_CTRL_-вариант чтения, ...
      • ...с получателем GetPropsCB(), а тот уж отдаёт диапазоны через cda_dat_p_set_range() и units (и то и другое опционально, в зависимости от типа).
      • (Да, формально cda_dat_p_report_rslvstat() делается уже после того заказа, но ответ-то на него придёт гарантированно позже.)

      Причём -- там прямо есть комментарий "dbr_ctrl_string is not implemented because is senseless" -- конкретно для строк свойства НЕ заказываются!!!

      Напрашиваются идеи:

      • Может, заказывать чтение чего-то ещё? Ну хотя бы для конкретно STRING?

        24.10.2022: не-а. Никак, ибо просто нечего -- нет ничего подходящего. Для решения же проблемы с "обновлением для каналов записи" придётся делать по нижеприведённому проекту "в cxsd_fe_epics СРАЗУ генерить update для rw- и autoupdated-trusted-каналов".

      • И можно ли как-нибудь добывать hwinfo-данные?

        Вроде _rw, _dtype и _max_nelems -- cainfo откуда-то берёт.

        А _srv_hwid -- это "SSID", он в принципе существует и вопрос лишь, можно ли и как его добыть от libCA.

        23.10.2022: сделано, это оказалось несложно. КРОМЕ только srv_hwid/SSID -- не видно никакого способа добыть его от libCA.

    • @~17:40, М-46: у casChannel есть метод getPV()!!! Так что не нужно у себя хранить копию указателя на PV.
    • @~20:40, дома: может, в cxsd_fe_epics СРАЗУ генерить update для rw- и autoupdated-trusted-каналов? Ну, тех, что CxsdHwIsChanValReady()? Тогда rw-каналы будут сразу отдаваться.

    17.11.2022: вчерашне-сегодняшнее разбирательство с вопросом "почему от epics2smth+libCAS обновления camonitor'у приходят редко и сразу пачками?" (где ответом стало "а обновление просто не производит записи, откладывая её до fileDescriptorManager.process()") подсказало подозреваемого: а может, дело в том, что и libCA тоже НЕ выполняет запись сразу?

    • Первым вариантом этого подозрения было "может, виновник было не libCA, а как раз libCAS -- обновления не генерил?".

      Но влияние CA_HEARTBEAT_USECS с этой гипотезой не вяжется никак.

    • В любом случае, стоит повторить те эксперименты, на пару с "cdaclient epics::..." натравив на те же каналы и "cdaclient cx::..." и смотреть на одновременность/неодновременность обновления.
    • Если верна гипотеза "просто libCA не выполняет запись сразу", то решением будет исполнять ca_poll() после каждой "отправки".

      ...по факту -- просто раскомментировать УЖЕ ИМЕЮЩИЙСЯ в cda_d_epics_snd_data() закомменченный вызов.

    • И, кстати, если всё окажется так, как предполагается -- то это ответ и на проблему с отправкой обновлений в cxsd_fe_epics+libCAS: там тоже достаточно по записям вызывать аналогичный поллинг.

    Ну, проверяем.

    • ОДНОВРЕМЕННЫЙ МОНИТОРИНГ: да, дело именно в отправке записи.

      "cdaclient -mDa8 @t100:epics::asdf.2=abcd1234" висело 5 секунд, а оба мониторирующих -- и через CX, и через EPICS -- показали обновление именно сразу после завершения записывающего.

    • РАСКОММЕНЧЕНЬЕ ca_poll() -- НЕ помогло. Почему?

      Прогнал записывающий cdaclient под "strace -f" -- у-у-у-у... Там порождается толпа thread'ов, постоянно дрыгается на futex()'ах.

    • С горя полез гуглить на тему "ca_array_put", чтобы потом добавить к запросу ещё что-то вроде "force send", и первой же ссылкой открылся древнючий, 2005 года "EPICS R3.14 Channel Access Reference Manual", где в разделе по 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 это слово не встречается...

    • ПЕЧАТАТЬ TIMESTAMP перед "update()" в epics2smth.
    • ВЫЗОВ fdManager.process(0)
    • АНАЛИЗ КОДА в ca_poll() и process()
    • 22.11.2022: ПОЧЕМУ на asdf.2 сначала отвечается "DOESNOT"?

      17.05.2023: косяк был в cda_d_cx.c, что UDP-резолвинг делался только для первого канала, а последующие уже раз в 10 секунд. Исправлено.

    • 22.11.2022: в e2s_set_props() надо проверять, что PV_ptr!=NULL

      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.

    • 22.11.2022: с "системой сборки" epics2smth/ всё-таки проблемы: epics2cda требует libtango.

    09.05.2023@~16:00, сидя в Перчини в Эдеме с Наташкой и с Мариной, Машей и Аней Малыхиными на "позднем" праздновании аниного ДР, глядя, как девицы играют в настолку "коварный лис, укравший пирог": чтобы не делать ca_flush_io() на каждую отправку, которых может быть толпа, но соптимизировать:

    • А если делать "отложенный" вызов -- при возникновении надобности регистрировать таймаут "на ближайшее время", т.е., когда вернётся к select()'у?

      Тогда даже если будет пачка записей, то flush сделается единожды на всех и ПОТОМ, когда будет "бездействие".

    • И взводить флаг об этом, который сбрасывать в таймауте -- чтоб дублирующих заказов не делать.
    • @чуть позже: можно даже элегантнее считать -- флагом "уже заказано" саму переменную, хранящую tid: если она >=0, то считать флаг взведённым, а изначально делать ему =-1.

    Это, кстати, вполне общий механизм, годный для разных аналогичных применений.

    09.05.2023@вечер, ~18:15+: делаем.

    • Новый вариант заключён в #if USE_DEFERRED_ca_flush_io для возможности потестировать разницу в производительности.
    • Переменная-"флаг"-tid -- 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: (НЕ СЮДА!!!) штудируем исходники frgn-модулей плюс читаем записи (об их создании, проблемах и недоделках) в соответствующих разделах, чтоб восстановить в памяти текущее состояние дел.

    01.05.2023: так вот, текущий статус-кво таков:

    • 01.05.2023: cda_d_epics.c -- приемлемо, вроде должен работать.

      И он коротюсенький -- 669 строк (плюс 103 в epics2cx_conv.h). Видимо, благодаря тому, что в libCA используется идеология, очень похожая на CX'ную: регистрация канала, подписка, а чтение и запись простейшими вызовами (с зеркальным отражением в cda), плюс понятие о "данных" -- {тип, число элементов, данные}.

    • 07.05.2023: связка cxsd_fe_epics.c+cxsd_fe_epics_meat.cpp и её более рационально разделённый вариант cxsd_fe_zpics.c+cxsd_sfi.c+cxsd_fe_epics_cas.cpp -- тоже худо-бедно работают, в т.ч. со строками теперь получше.

      Но не без косяков, с которыми ещё разбираться (например, начальное чтение), да и размер вдвое больше 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.

    • 08.05.2023: gw/epics2smth/, конкретно epics2smth.cpp и epics2cda.c -- многое сделано, но видно, что там ещё пилить и пилить:
      • Например, даже e2s_PV::read() недоделано. 17.05.2023: уже доделано.
      • И "new e2s_PV(...)" делается в ДВУХ местах, а надо б унифицировать.
      • И косяк с заторможенной отработкой обновлений -- возможно, потому, что при генерации записи НЕ прерывается select(), в котором висит цикл "fileDescriptorManager.process(3600)".

        ...это можно решить введением pipe-пары, читающий конец которой регистрировать у fileDescriptorManager'а (и в callback'е вычитывать блоками по 1024 байта), а в пишущий слать по 1 байту при каждом обновлении (и сделать NONBLOCK, так что даже при переполнении pipe-буфера просто пофиг на ошибку -- если переполнение, то, значит, уведомление уже послано).

        11.05.2022: за вчера-сегодня сделал (см. тот раздел за эти два дня), теперь тестировать.

    • 12.05.2023: cda_d_tango.cpp -- весьма сыро, даже хбз, работает ли; конкретные "недостатки":
      • ТОЛЬКО скаляры,
      • ТОЛЬКО [u]int16/[u]int32/float/double;
      • reconnect'ы делаются в TangoHeartbeatProc(), включаемой лишь при !USE_PUSH_MODEL; 14.05.2023: исправлено -- теперь всегда.
      • в DoSubscribeIterator() странная проверка "if (hi->dpx == dpx)", чей смысл неочевиден.

        13.05.2023: разобрался:

        • в TangoHeartbeatProc() делается попытка реконнектить все неприконнекченные dpx'ы (т.е., из списка frs_2rcn),
        • и если получится -- то вызывается итератор по ВСЕМ hwr'ам (список-то сквозной), в качестве параметра передаётся dpx, который только что приконнектился (и, значит, можно запрашивать подписку);
        • вот та проверка -- это и есть фильтр, что у hwr'а, для которого вызыван итератор, используется как раз указанный -- свежеприконнекченный -- dpx.
      • Определение "тип имени" в cda_d_tango_new_chan(), частично с помощью determine_name_type() -- крайне мутное: там и определяются далеко не все варианты (конкретно "->" никак не смотрится), и сам код/эвристика крайне мозгодробительно-неочевидны.

        Тут надо просто ВСЁ переделывать.

      • ...не говоря уж о том, что в идеале надо б рассчитать на полную "линейку" -- атрибуты, property, property атрибутов, ...
  • 29.04.2023: (НЕ СЮДА!!!) ТЕСТЫ НА E2K!!!
  • 22.10.2022: давно назревает потребность "уметь в cda_d_epics определять нативные серверовы свойства канала" -- то, что делает cda_d_cx.c по 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.c, как и прочие в src/ca/client/tools/ (caget.c, caput.c, camonitor.c), пользуется услугами "локальной библиотечки" tool_lib.[ch] -- в некотором роде аналог CX'ной console_cda_util.[ch].

      Это к тому, что часть служебного "мяса" именно там.

    • Собственно добыча и вывод информации делается в cainfo().
    • Там вызываются ca_element_count() и ca_field_type(), плюс ca_read_access() и ca_write_access(), всем им передаётся "chid".
    • Делается это ПОСЛЕ "коннекченья", выполняемого посредством tool_lib'ового 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/client/oldChannelNotify.cpp содержит реализацию тех ca_*()-методов; она сводится к возврату закэшированного ранее (а вот как/когда оное было закэшировано и как именно возвращается -- для типа, макс.числа_элементов и прав доступа делается РАЗНЫМИ способами; там всё истинно-плюсово-поперезапутанно).

    Теперь что касается возможной поддержки CXDTYPE_UNKNOWN:

    • С одной стороны, в момент РЕГИСТРАЦИИ канала -- для ca_create_channel() -- тип НЕ требуется.
    • С другой же, сразу после успешной регистрации организуется подписка -- ca_create_subscription(), и вот там уже ТРЕБУЮТСЯ и тип, и max_nelems.
    • @выйдя с М-46 домой, на переходе через Ильича около Ильича-1, ~19:10: Напрашивается идея -- при dtype==CXDTYPE_UNKNOWN нужно регистрацию ОТКЛАДЫВАТЬ до получения данных.

      Но надо помнить состояние "подписано ли"; плюс, это будет одноразовым "приспособленчеством" (если только не помнить предыдущий тип и не менять тип подписки при изменении).

    • 23.10.2022@утро, перед завтраком, ~10:18: а можно вообще ВСЕГДА делать регистрацию отложенной.

    23.10.2022: делаем "1-ю часть" отдачу HW-информации.

    • ВСЁ в StateChangeCB() и довольно тривиально: сначала добываются свойства вызовами ca_element_count(), ca_write_access() и ca_field_type(), а затем сбагриваются cda.
    • Но есть 2 нюанса:
      1. Из dbf_type получить dtype -- потребовалось 2 этапа конверсии.
      2. И с вариантом STRING пришлось, традиционно, повозиться отдельно, указывая ему вместо скаляра (а вдруг и массива?) фиксированную длину fixed-string'а --
        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, то...
      • ...поискал посредством (zsh!)
        egrep 'CDA_REF_R_STRSCHG|CDA_REF_R_RANGECHG' work/*4cx/**/*.c(.)
        -- ничего интересного: ловятся эти события только в cdaclient.c, где они приводят лишь к печати информации.
      • 02.05.2023: а если где-то ждётся единственное событие и потому проверка "reason == CDA_REF_R_..." отсутствует и не на-grep'ится?

        OK, заменил строку поиска на 'CDA_REF_EVMASK_STRSCHG|CDA_REF_EVMASK_RANGECHG' -- один фиг.

        Может, дело в начальном чтении?

    26.10.2022: за вчера-сегодня вроде доделал поддержку CXDTYPE_UNKNOWN:

    • В privrec добавлены 2 поля:
      1. evid ca_evid -- для хранения ID подписки, чтоб можно было её отменять и заводить новую.

        Это указатель, который при неподписанном состоянии делается =NULL.

      2. chtype ca_subs_DBR_type -- для хранения ТЕКУЩЕГО типа, на который оформлена подписка. Служит в GetPropsCB() для сравнения, совпадает ли текущий полученный от сервера тип с тем, на который в текущий момент подписка оформлена.

        Это технически long, поэтому при неподписанном состоянии (и изначально) делается =-1.

    • Собственно "мясо" -- модификации сосредоточены в:
      1. cda_d_epics_new_chan(): теперь стоит явное разрешение CXDTYPE_UNKNOWN.

        Подписывание же -- ca_create_subscription() -- сделано условным, если dtype != CXDTYPE_UNKNOWN.

      2. StateChangeCB(): в случае, если канал "полиморфен" (заказан как CXDTYPE_UNKNOWN) проверяется соответствие свежеполученного от сервера типа текущему значению ca_subs_DBR_type и при НЕсовпадении
        • Сначала удаляется старая подписка -- при наличии, т.е., ca_evid != NULL.
        • Затем оформляется новая.

        Таким образом, оно же и исполняет первоначальное подписывание -- т.к. изначальное значение ca_subs_DBR_type=-1, что не совпадёт ни с одним из типов.

    • Ну и добавлен флаг CDA_DAT_P_FLAG_CHAN_TYPE_CHANGE_SUPPORTED в метрику.
    • Косметическое изменение: в тексте переставлены определения callback'ов, так что NewDataCB() теперь идёт первым -- чтоб можно было на него ссылаться из StateChangeCB().
    • Пара существенных отличий работы UNKNOWN от "определённых" типов:
      1. При "определённом" типе подписка заказывается сразу, прямо из cda_d_epics_new_chan(), а при UNKNOWN -- из StateChangeCB().

        Это может создавать разные "диаграммы поведения" вследствие различия в паттернах прихода данных и свойств.

        ...в принципе, можно ВСЕГДА заказывать подписку из StateChangeCB(); но надо ли?

      2. При для каналов "определённого" типа заказывается чтение того числа элементов, что запрошено в 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-пространств.

        Разобраться бы и прописать в комментариях в их объявлениях.

    • ...но при этом в составе epics2cda оно как-то работает (или потому лишь, что тип PV в сервере не меняется и этот кусок кода попросту никогда не исполняется?).

    Короче -- надо бы сей аспект протестировать.

    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: в продолжение вопроса:

    • но наилучшим вариантом было бы всё-таки научить cdaclient поддержке CXDTYPE_UNKNOWN -- ведь в утилите есть всё-всё, что только нужно для диагностики. Ну пусть будет какой-то недокументированный формат...
    • @душ перед обедом: учитывая сложности с реализацией просто типа "@x:", может, лучше сделать какой-то флаг-МОДИФИКАТОР, чтобы указывалось в стиле "@?d:" и тем самым выбирался бы "формат по умолчанию", а просто РЕГИСТРАЦИЯ делалась бы с 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: почему-то не работает схема "epics::" из epics2cda, как минимум при коннекченьи к cxsd_fe_epics: напрямую из cdaclient -- да, а из шлюза -- нет.

    13.11.2024: ещё с позавчера пытался тестировать полиморфизм в cda_d_epics, запуская "цепочку" -- caget/"cdaclient epics::"->"epics2cda epics::"->"cxsd -e 'load-plugin epics' -- фиг, не работает. Долго возился, проверяя разные идеи.

    • Чтобы сделать "цепочку" в рамках одной машины и чтоб посторонние из сети ИЯФ не мешали, "сервера" были посажены на разные интерфейсы: cxsd на 127.0.0.1, а epics2cda на 192.168.129.254 (eno2).
    • И вот что странно: запрос к cxsd НАПРЯМУЮ -- работает; через epics2cda -- нет, ни в каком варианте (ни с CXDTYPE_UNKNOWN "qltr3.Iset", ни со спецификатором типа "@d::qltr3.Iset".

      Была даже мысль, что проблема в eno2 -- что кабель не воткнут, но нет: "обмен" адресами (посадка cxsd на eno2 и наоборот) ничего не менял: напрямую работало, через epics2cda -- нет.

    • Была ещё мысль "а вдруг дело в firewall'е?" -- ну там правила какого-нибудь не хватает, вроде "пропускать ответы с 5064" (хотя вроде и так всегда работало -- раз ушёл запрос, то это не "RELATED" ли?).

      ...сейчас-то понятно, что даже это было лишним: ведь НАПРЯМУЮ-то (cdaclient->libCA->libCAS->cxsd) работало.

      Как бы то ни было: и убирал файрволлинг посредством "iptables -F", и нужную строчку в /etc/sysconfig/iptables добавил -- ничего не изменилось.

    • Понапихал всякой диагностики в разные места -- и видно, что до cxsd_fe_epics.so запрос доходит, а вот отвечает ли он на него -- неясно.

      И по его диагностике было видно, что вроде должен отвечать положительно.

      Но уходит ли пакет от 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-дескриптор "авансом", прямо при инициализации?

    • OK -- стал гонять под strace и ltrace, чтобы посмотреть "что да как": какая цепочка событий происходит, что вроде как уведомление есть, но ca_poll() его игнорирует.
    • По ходу разбирательств увидел, что в программе живут ДВА thread'а.

      И сразу вопрос -- не из-за этого ли глючит ca_poll()?

      Хорошо -- начал искать, в какой же момент происходит порождение 2-го thread'а: может, оно как-то неявно порождается?

    • ...и в конце концов оказалось, что второй thread -- МОЙ! Это epics2cda.c сделан 2-поточным, на основе mt_cxscheduler -- чтоб запустить ДВА основных цикла одновременно: один поток с sl_main_loop(), а второй -- fileDescriptorManager.process(3600) в e2s_run().

      О чём я благополучно забыл.

    • Т.е., проверять надо то, насколько корректно делается локинг в epics2cda.c -- и уже сейчас видно, что не особо: в 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:

    • Проверено через другую машину в лице b360mc (ну мало ли что): сервер и клиент-запросчик на x10sae, а epics2cda на b360mc; один чёрт -- так же начинает "не видеть" ответов в 5-й дескриптор (которые валят лавиной).
    • Добавлены "скобки" mt_sl_lock()/mt_sl_unlock() во все методы epics2cda_NNN() вокруг вызовов из cda, а не только cda_add_chan(), как раньше.

      Тоже один чёрт -- ничего не изменилось.

cda_d_pva:
  • 07.07.2019: создаём раздел. Пока также просто чтоб был, т.к. с деталями реализации пока непонятно всё.

    И создаём сразу после "cda_d_epics", чтоб были рядышком.

  • 07.07.2019: это специализированное место для записи ссылок на информацию, полезную для программирования этого модуля.

    07.07.2019: пытался нагуглить что-нибудь на тему "introduation to pv access clients" -- по аналогии с PDF'кой для CA. Фиг. Но некоторое количество информации всё же нашлось.

    • "Spring 2019 EPICS 7 Intro B - SNS Controls Software Tools", оно же "EPICS 7", авторства Kay Kasemir (знакомого нам по ICALEPCS-2015), причём документ от June-2015.

      Там рассказано немножко интересного, в т.ч.:

      • "Support for 64bit numbers"
      • "locking tweaks" (что бы это быто?)
      • "RELEASE.local mechanism" - ?
      • Некий компонент "Area Detector" -- это "EPICS framework for image manipulation".
    • Отдельно надо сказать, что протокол там называется "pva", а не "pv" (адресация, правда, http/URI-style -- "pva://ПУТЬ", но не суть).

      Следовательно, и нам надо будет делать протокол "pva::", а не "pv::"; соответственно, и cda_d_pva.c, а не cda_d_pv.c.

    • "EPICS7 at APS" -- это стоит просто почитать.

      Из любопытных слов/утверждений там -- "High Performance DAQ" и "Digital Video System".

    08.07.2019: также некоторое количество информации нашлось при переходе с EPICS'ной "Getting Started..." по ссылке EPICS V4 -- попадаем на "EPICS Version 4 Home Page". Там есть:

    • "EPICS V4 Developer's Guide"...
    • ...где, как утверждается, есть "C++ Tutorial", доступный отдельно: следующим образом:
      git clone  https://github.com/mrkraimer/pvaClientTutorialCPP.git
      cd pvaClientTutorialCPP
      make
      
      и "Clone it in the same directory that holds your EPICS 7 release. Documentation is provided in pvaClientTutorialCPP/documentation/clientTutorialCPP.html".
    • "Programmers Reference Documentation"
    • "Getting Started with EPICS V4" (с пометкой "This page has not been updated since 2015. It needs work").

    08.07.2019: попытка поискать описание протокола "PV Access" (EPICS4) -- гуглим "pv-access protocol". Кое-что нашлось:

    15.07.2019: в презентации "EPICS7" упоминается некий sample-конфиг epics-train/examples/ExampleApp/Db/circle.db. Посмотреть бы на него -- вроде бы это наипростейший, там поля {angle,x,y}.

    15.07.2019: изучаем спецификацию протокола pvAccess. Из любопытного:

    1. Они НЕ используют выравнивание по границам слов СОВСЕМ. И даже явно его запрещают -- "implementation MUST NOT try to align data on word boundaries". А считают, что данные -- это просто последовательный поток байтов.

      Мотивируют тем, что так избавляются от "wasted space and additional complexity".

      IMHO, сомнительный аргумент.

      ...зато сразу снимает проблему "как для композитных типов согласовывать выравнивание протокола с выравниванием языка". Может, именно ЭТО и есть реальная причина?

    2. С кодировкой данных, в аспекте endianness, они используют такой подход: сервер шлёт в своём порядке, но в КАЖДОМ сообщении есть "endianness flag".

      Мотивация -- что такой подход позволяет "all the intermediates to forward data without requiring it to be unmarshaled". Т.е., это сильно упрощает всякие gateway'и.

      Пожалуй, a valid point.

    3. С указанием размеров -- продолжается плюшкинство, имевшееся ещё в старом протоколе, рассчитанном на 16-битные длины, а бОльшие размеры передавались специальным значением (все 1?) в 16-битном поле, а потом за ним шло уже реальное 32-битное.

      Тут -- аналогично: если меньше 255, то передаётся 1 байт; если меньше 2^31-1, то передаётся байт=255, а затем 32-битная длина; если >=2^31-1, то передаётся байт=255, за ним int32=2^31-1, а за ним 64-битное число. Чем-то похоже на UTF8.

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

    И ещё пара аспектов:

    • У них постулировано использование UTF8. И при этом есть некоторая шизофрения: с одной стооны, длины указываются не в символах, а именно в байтах. С другой, рвать multibyte-символы запрещается.

      Как хорошо, что я вопросом кодировки принципиально не занимаюсь! Не транспортного протокола это дело!

    • У них там сразу IPv6, а IPv4 идёт как "совместимое".

      У нас же вообще этот вопрос не рассматривается: в протоколе НЕ передаются никакие сетевые адреса, а используются просто source-адреса ответных пакетов.

      Но если уж использовать адреса -- то да, IPv6 является разумным выбором.

    16.07.2019: ещё почитал про "introspection data" (мало что понял: это явно про описание типов, но мутновато -- видимо, понимание придёт позже, после прочтения дальнейшего), и, вкупе с ранее прочитанным, выкристаллизовалось осознание:

    • Похоже, эти ребята до сих пор пытаются экономить байтики -- то ли ради памяти, то ли ради сетевого траффика. В том числе на операциях, производимых вообще ЕДИНОЖДЫ (как передача той "introspection data").

      Как будто они до сих пор живут в 80-х/90-х, когда память была мега-дефицитом и приходилось ужиматься повсеместно.

    • Но сейчас память считается гигабайтами, так что даже с тысячами каналов лишние несколько байт выльются в десятки килобайт, которые особой погоды не сделают -- требования даже ОС перекрывают это всё на порядки.

      Если же заботиться о микроконтроллерах (где, впрочем, сейчас счёт идёт как минимум в сотнях мегабайт), то:

      1. На каждый конкретный экземпляр контроллера вряд ли лягут тысячи каналов, так что суммарные "потери" будут невелики даже по меркам этого контроллера.
      2. У контроллеров обычно надо экономить ВЫЧИСЛИТЕЛЬНУЮ МОЩНОСТЬ ПРОЦЕССОРА, а сложная метода кодирования её как раз транжирит.
      3. Если уж какой-то контроллер совсем дохленький, то проще сделать/использовать для него свой экономичный протокол, а не заставлять маяться с основным "общим и высокоуровневым".

        Это, кстати, было подтверждено на примере мамкинских контроллеров с PowerPC-852, где полноценный EPICS дико тормозил, а CX'ные драйверята справлялись намного лучше, оставляя некоторую часть процессора свободной (и притом мы там работаем в режиме "CAN-АЦП постоянно выдают измерения в линию", а не "вычитываем измерения столько, сколько успеваем" -- первый вариант создаёт бОльшую нагрузку).

    17.07.2019: а ещё про "introspection data": почитал чуть внимательнее, и там есть кое-что ОЧЕНЬ ПОХОЖЕЕ на наш cxdtype_t: FieldDesc.

    • Это тоже 8 бит, в которые они пытаются засунуть "концентрированное описание типа".
    • Но распределение битов несколько иное.
    • И их, вероятно, сильно ограничивает то, что этот FieldDesc участвует в кодировании описателей "introspection data", т.н. "*_TYPE_CODE", и потому не может иметь произвольных значений, а существенно ограничен.

      В частности, старшие 3 бита (указывающие "представление" -- int, float, string, ...) НЕ МОГУТ иметь значение "111", а максимум 110 -- поскольку 0b11011111=0xDF, а значения выше 0xDF являются специальными кодами *_TYPE_CODE.

    • Разбиение по битам таково:
      1. На "представление" там отдано 3 бита. Из 8 вариантов 1 запрещён (тот самый "111"), 2 зарезервированы, а используются 5: boolean, integer, float, string, complex (?).
      2. Ещё 2 бита -- "массивовость". Вот этого смысл мне неясен: нафига все эти "fixed-size", "bounded-size", "variable-size", "scalar"?

        ...мы в CX прекрасно обходимся "max_nelems", а массивы в принципе все переменного размера -- то, что у этих считается "bounded-size".

      3. Ещё 3 бита (тоже младшие, кстати :)) -- якобы "type-dependent", но де-факто там указывается размер и беззнаковость (для целых).

        Причём по факту кодирование размера ровно такое же, как у нас -- указывается двоичный логарифм размера; поэтому и значения те же самые (byte -- 00, short/int16 -- 01, int32 -- 10, long/int64 -- 11; с вещественными -- аналогично).

        Но: там в принципе НЕВОЗМОЖНА такая вещь, как "int128" -- код должен был бы получиться 100, но верхний бит уже занят под беззнаковость.

    • А "complex" -- это как раз всякие union, variant union, structure, и некая "bounded string".

      И у всяких массивовых типов (fixed- и bounded-) указывается оный "размер" в элементах.

    • Насчёт структурных типов есть пара НЕпоняток:
      1. Фраза "A structure REQUIRES its identification string and...": что такое "identification string"? Это словосочетание встречается в тексте лишь 3 раза (тут плюс дважды в нижеследующей таблице), и нигде не объясняется.
      2. Там передаётся массив/список описаний полей, но НЕясно, где/как указывается количество элементов в этом списке.

      Там в принципе есть пример о "timeStamp_t" -- простая структура из 3 полей, но он без комментариев к hexdump'у, так что дешифрировать его нетривиально. Байт 0x03 там есть (самое начало 2-й строчки), но почему он там -- хбз. Или "по общему правилу передачи массивов"?

    17.07.2019: приятно видеть, что EPICS4/7 использует примерно те же способы/правила обращения с соединениями, что и CX (связка cxlib<->cxsd_fe_cx). Только:

    1. Не видно SO_KEEPALIVE.

      18.07.2019: проверил по исходникам: в base-3.15.6/ слово SO_KEEPALIVE встречается; а вот в base-7.0.2.2/ -- уже нет. Вероятно, решили, что при наличии "application pings" обычные keepalive'ы уже без надобности.

    2. Странноватый выбор таймаутов: время поиска каналов после дисконнекта оказывается очень большим (хотя это скорее у нас на ВЭПП-5, из-за блокировки beacon'ов), а "application pings" шлются каждые 15 секунд.

    18.07.2019: дочитал на тему работы с соединениями. Из любопытного:

    • Там, в разделе "Flow Control", описан весьма любопытный алгоритм предотвращения переполнения буферов в ситуациях, когда отправитель шлёт данные быстрее, чем получатель успевает их вычитывать.
      • Идея в том, что вначале собеседники обмениваются данными о размерах своих приёмных буферов (как socket'ных (где возьмут?), так и прикладных), а затем ведётся постоянный (каждые сколько-то байт пересланных данных) взаимный учёт того, сколько было отправлено и сколько получено. Когда разница между объёмом отправленного и последним известным "подтверждённо-полученным" объёмом достигает суммы размеров приёмных буферов, то считается, что мы приблизились к переполнению и надо прекращать слать.
      • Там явно указано, что этот алгоритм предназначен только для предотвращения переполнения сообщениями "ПОДПИСКА", а "For other messages TCP flow control is sufficient".
      • Процедура "прекращать слать" там описывается как "monitors would start piling up in the monitors' circular buffer queues".

      Это ровно та проблема, с которой мы уже сталкивались, и которая прекрасно имитируется "kill -STOP"'еньем (или Ctrl+Z'еньем) клиента, и она была косвенно решена 03-04-2017 путём включения ограничений на буфер отправки через fdio_set_maxsbuf() плюс оптимизацией сдвига буфера (только при отсутствии сверху достаточного места).

      Идея в принципе интересная, техническая проблема только в том, как вести учёт объёмов.

      И, поскольку есть правило "у клиента ВСЕГДА должны быть последние данные", то в случае применения нужно будет после "опустошения буферов" (появления свободного места) слать текущие значения тех мониторов, которые попали под не-отсылку (их, очевидно, надо помечать), СРАЗУ, не дожидаясь их очередного обновления от сервера.

    • У них зачем-то все сообщения "MUST BE aligned on a 64-bit boundary". Это при том, что при передаче данных какой-либо padding явно запрещается.

      У меня только одна мысль на тему "зачем" -- видимо, они вычитывают не по границе пакетов, а большими блоками "до заполнения буфера целиком", и потом уже внутри этого буфера распарсивают. На это косвенно указывает фраза " bulk reads are made from the socket rather than reading message by message (because OS calls are expensive)" ранее.

      ...а у меня под Linux/x86 опыт противоположный, зато на PowerPC syscall'ы действительно очень дорогие. Возможно, ребята ДО СИХ ПОР стараются оптимизировать под то древнее железо (VME -- неизбежно контроллеры PowerPC).

    • В заголовке имеется байт флагов, в котором присутствуют пара битиков:
      • bit6: "направление" -- сообщение от клиента (=0) или сервера (=1).
      • bit7: "endianness" -- little-endian (=0) или big-endian (=1).

      Ну-у-у, для всяких снифферов, вероятно, это полезно.

    • Там отдельно имеется раздельчик "Channel Request Life-cycle", где явно запрещаются какие-либо действия с каналом до тех пор, пока не отработал предыдущий запрос.

      Интересно, а две записи подряд там выполнять можно ли, отработает ли вторая или будет отброшена?

    • ...и вообще, в порядке замечания: похоже, там реально на каждый канал заводятся "машины состояния", а каждый "запрос" является отдельным объектом.

      А вот в CXv4 ничего такого нет -- наоборот, все операции явно и намеренно сделаны "stateless", чтоб взаимодействие сводилось к обмену сообщениями.

    18.07.2019: итак, файл дочитан до конца.

    • Косяк самого описания: там постоянно используется условное обозначение поля "PVField", но определения его НЕТУ.
    • С запросом SEARCH как-то мутно, но будто он позволяет МАССИВ запросов в одном пакете -- "поле" channels[].
    • У них реально "request" является отдельным объектом: всякие channelGetRequest, channelPutRequest, channelMonitorRequest -- сначала создаются (с ответным пакетом подтверждения!), потом отдельные команды для get или put (а с "monitor" -- start и stop), и отдельно destroy.

      Вот эти многостадийные "запросы" -- явно объясняют странность, когда при установке в cda_d_epics.c периода поллинга в 5с многие операции исполнялись по 10-15 секунд -- т.е., по 2-3 периода: очевидно, как раз делались те самые стадии подготовки request'ов (с лишними, на мой взгляд, round-trip'ами).

    • И ещё:
      • Причём конкретно для "putRequest" есть «"get-put" request», который «retrieves the remote put structure. This MAY be used by user applications to show data that was set the last time by the application» (WTF?!).
      • И аналогичное упомянуто для "GetPutRequest" (о нём самом ниже). А ещё для него есть «A "get-get" request retrieves remote get structure. This MAY be used by user applications to show data that was retrieved the last time.» -- очевидно, прочитать последнее из кэша.

        Как бы аналог нашей CXC_PEEK, но у нас-то она используется для первоначального чтения "последнего известного", а тут -- вроде УЖЕ РАНЕЕ клиенту должно было быть прислано; он что, страдает склерозом и может переспросить?

    • И отдельная совершенно необъяснимая странность: поле subcommand, вроде бы ведущее себя одинаково у разных "типов request'ов", имеет различающуюся кодировку "битовых флагов/значений":
      • GetRequest: GET=0x40, маска "DESTROY"=0x10.
      • PutRequest: PUT=0x00, маска "DESTROY"=0x10.
      • PutGetRequest: PUT_GET=0x00, маска "DESTROY"=0x01 (опечатка?).
      • StartMonitorRequest: START=0x44, STOP=0x04, маска "DESTROY"=0x10.

      Кстати, "grep -i destroy **/*.h*(.)" ничего из этого не нашло. Оно там что, в коде циферками захардкожено?

    • Есть специальный тип запроса "put-get": сначала делается запись, а потом клиенту отправляется результат. А "обычно" -- просто запись, вслепую.

      Совершенно отличающаяся от CX идеология, где клиенту в качестве "подтверждения" в любом случае прилетит обновление (в v2 -- как ответ на "запись", а в v4 -- через обязательно уставляемый монитор).

    • У команды "channelMonitorRequestInit" (command=0x0D, subcommand=0x08) есть поле queueSize.

      Там шо, реально указывается размер буфера (кольцевого?) для мониторов?

    • Из любопытного:
      1. Есть отдельный код команды "Channel array (0x0E)", предназначенный для манипуляции отдельными элементами массивов -- читать и писать, а также устанавливать длину массива.
      2. Можно отдельно дёргать операцию PROCESS -- «are used to indicate to the server that the computation actions associated with a channel should be executed. In the language of EPICS, this means that the channel should be "processed".»
      3. Предусмотрен зародыш для RPC -- "Channel RPC (0x14)".

        Похоже на TANGO'вский вызов команд -- там предусматривается передача "аргумента", вроде бы произвольного типа.

        Комментариев ноль, так что неясно, насколько это реализовано. Но и метки "// TODO" там нет.

    В качестве резюме -- мои впечатления по результатам прочтения:

    • На МОЙ взгляд, протокол сделан странновато: как будто он пытается имитировать "локальный доступ" -- просто чтение, просто запись, но с подтверждениями.

      Т.е., как будто есть некоторая шизофрения: с одной стороны, протокол для СЕТЕВОЙ работы, но, с другой стороны -- в нём не очень-то учитывается природа сетевого взаимодействия, когда "партнёр" может в произвольный момент отвалиться, и ждать всяких "подтверждений", да ещё и делать многие операции не просто командами "выполни такое-то действие", а заводить на удалённой стороне целые объекты с машинами состояния.

    • Да, возможно, это только на МОЙ взгляд, а у авторов есть какие-то свои соображения, поскольку они исходят из каких-то СВОИХ потребностей, развившихся по результатам ИХ ИСТОРИИ.
    • Но там в разделе "Future Protocol Changes/Updates" присутствует пункт
      • "one-phase" get/put/get-put/process
      -- как я понимаю, это намекает на осознание не вполне адекватности нынешней схемы.
    • В том же списке есть и пункт
      • bulk message transfer/trottle public API
      -- очевидно, они осознают и потребность в более явной возможности группировки запросов (что в CX с давних пор (ещё с UCAM) делается скобками begin()/run(), с операциями внутри).

    А насчёт "PVField" и "0x01" отправлено мыло с вопросом к matej.sekoranja.

    Вечером: тот ответил:

    03.05.2023: сегодня в tach-talk'е был задан вопрос "Does anyone have documented PV Access run time ENV variables?", на который Timo Korhonen ответил парой ссылок -- на протокол (не новость) и на документацию:

    Чуть позже Michael Davidsaver прислал ещё ссылку

    • "PVA Network Configuration" -- варианты того, что можно указывать в 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_pvxs:
  • 06.05.2023@утро-зарядка: памятуя вчерашний ответ про возможность явно указывать сервер, напрашивается -- учитывая, что PVXS -- это отдельная от PVA библиотека, может, стоит задуматься о реализации dat-плагина именно на ЕЁ основе? Да-да, отдельного.

    Создаём раздел, сразу после "cda_d_pva", чтоб были рядышком.

cda_d_tango:
  • 20.05.2019: создаём раздел. Пока просто чтоб был, пока что без собственно реализации -- поскольку пути этой реализации ещё предстоит изучить.

    Но под-раздел для записывания информации уже сделаем.

    15.11.2023: (сюда, т.к. отдельного места нету) усовершенствована "система сборки" в лице frgn4cx/tango/FrgnRules.mk: теперь оно позволяет собираться не с "деревом сборки TANGO" в compile/tango-*/, а с уже установленным в системе (так что все *.h и *.so лежат в системных директориях).

    • Указанием на это служит make-параметр 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.

  • 20.05.2019: это специализированное место для записи ссылок на информацию, полезную для программирования этого модуля.

    Надо отметить, что это не "знания о TANGO вообще" (оные в "Знания/TANGO"), а именно необходимое для клиентского доступа к TANGO-серверам.

    20.05.2019: вчера было немного погуглено на тему "tango c api". Результаты грустноваты:

    • Writing a TANGO client using TANGO C++ APIs -- но это о C++.
    • Страница Bindings, раздел "C language":

      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
    • И оттуда есть линк на скачивание (НЕ надо жать!), редиректящий на загрузку c_binding_Release_3_0_2.tar.gz от 12-02-2015.

    Мысли по этому поводу: представляется 2 возможных варианта реализации cda_d_tango:

    1. На C -- да, он будет с ограничениями, но будет и простой.
    2. Сделать cda_d_tango.c простым файлом-интерфейсом, а основную часть реализации -- на C++, в дополнительном .cxx-файле.

      Да, оно должно будет как-то автоматом подтягивать libstdc++.so, но это отдельная техническая задача (возможно, всё сработает именно автоматом).

    23.05.2019: анализируем c_binding_Release_3_0_2.tar.gz, конкретно c_tango.h.

    И там натыкаемся на термин "device proxy" -- ВСЕМ API-вызовам первым параметром передаётся "void *proxy".

    • Очевидная мысль: "это типа контекста"?
    • Гугление показывает, что не совсем. Как гласит страница "DeviceProxy - PyTango 9.3.0 documentation",
      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.
    • Также есть 64-битные DevLong64 и DevULong64.
    • Вещественные -- DevFloat и DevDouble.
    • НЕТУ отдельно байтовых типов -- int8/uint8, а есть только 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"» (важно -- с кавычками!), и нашёлся результат, но крайне ограниченный и, увы, нам не подходящий:

    • "C Language - Tango Controls 9.3.3 documentation".

      Там сказано:

      • "The C binding does not support all features of TANGO."
      • "Release 3.0.2 including features of Tango 8 but no events"
    • Скачать можно c_binding_Release_3_0_2.tar.gz в исходниках.
    • И там внутри в файле Tango_C_Binding.pdf (он фигово сделан, лучше смотреть doc_html/index.html) сказано:
      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: а есть и более свежая версия документации (найдена удалением имени файла, что дало листинг директории):

    • "The TANGO Control System Manual Version 9.2". Оно на сотню с лишним страниц короче -- видимо, за счёт удаления разделов по Java, с оставлением лишь C++. Причём некоторые небольшие подрездельчики даже добавлены.

    27.07.2019@утро-душ: насчёт типа DevEncoded и как его поддерживать/имитировать: а ведь раз это дуплет {строка,цепочка_байтов}, то можно сие считать за просто цепочку байтов -- сначала NUL-terminated string, а за ней собственно байты.

    В принципе, оно без проблем конвертировабельно в обе стороны. Вопрос может возникнуть лишь с менеджментом памяти.

    01.08.2019: наткнулся на ещё одну презентацию о TANGO:

    02.08.2019: совершенно случайно, ища, как бы заставить DeviceProxy НЕ блокироваться при первом коннекте (гуглил "tango deviceproxy connection non-blocking"), наткнулся на прелюбопытнейший документ:

    • "Tango EPICS report", он же "Tango vs EPICS technical comparison ELI-Beamlines". Дата 12-02-2014, 127 страниц, "код документа" ELI-BL-4400-REP-00000067-2.

      Т.е., ребята выполнили как раз сравнение EPICS с TANGO, включая аспект взаимодействия между этими системами. Конкретно ИМ больше понравилось TANGO; хотя аргументация МНЕ кажется странноватой и выводы лично у меня вызывают скепсис.

    • 19.03.2022: ЗЫ: забыл тогда сказать, какая именно аргументация мне кажется странноватой.

      Так вот: на стр.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().
    • Определение этого класса содержится в lib/cpp/client/DeviceAttribute.h (1323 строки в 9.2.5a).
    • Реализация -- в lib/cpp/client/devapi_attr.cpp (6390 строк!!!).
    • То, ЧТО внутри -- определяется полями data_format (скаляр/1D/2D) и data_type -- вот последнее именно ТИП.
    • Также там есть поле TimeVal time -- очевидно, timestamp.
    • А где собственно ДАННЫЕ?

      25.08.2022: разобрался, мда...

      • там внутри есть ТОЛПА полей DevVarShortArray_var ShortSeq и прочие *Long*, *UShort* и т.п.
      • -- т.е., ВМЕСТЕ, все СРАЗУ, а не union!

        Т.е., как будто можно в DeviceAttribute засунуть ОДНОВРЕМЕННО НЕСКОЛЬКО разнотипных значений.

        26.08.2022: нет, НЕЛЬЗЯ: при запихивании, например, Double, ранее запихнутый "Long" (int32) "пропадает" -- т.к. делается LongSeq = {_pd_seq = 0x672ec0}. Пподсмотрено через gdb, а сначала проверено отладочной печатью, которая в int32 выдала мусор -- после цепочки "запись int32, запись float64, чтение int32, чтение float64; затем также проверены "статусы" чтения -- и чтение int32 вернуло 0, а чтение float64 -- 1.

      • Кстати, на x84_64 sizeof(Tango::DeviceAttribute)=200.

        ...впрочем, sizeof(refinfo_t)=440 -- сильно больше, да; но там зато дофига всего, чего в DeviceAttribute нету.

    Пожалуй, через-зад'ность реализации DeviceAttribute вполне сопоставима с оной у libGDD. Ну да, концепции-то близкие, и результат реализации плюс-минус одинаково кривой. ...точнее, кривые даже не только реализации, но и дизайны.

  • 23.07.2019: приступаем к реализации.

    23.07.2019: пока что -- лишь скелета.

    • Директория -- frgn4cx/tango/.
    • Совершенно ясно, что модуль будет состоять из 2 частей:
      1. C'шная часть -- то, с чём непосредственно взаимодействует cda_core.

        Это файл cda_d_tango.c.

      2. C++'ная часть, работающая уже с API TANGO. Потому, что чисто C'шного API у TANGO не существует.

        Назовём файл cda_d_tango_cpp_wrapper.cpp -- он будет предоставлять функции с "C"-calling-convention, являясь обёрткой для плюсового кода.

      Какое ТОЧНО будет разделение обязанностей -- пока неясно: то ли .c'шник будет реализовывать почти всё, используя обёртку только для конвертации вызовов, то ли наоборот, будет лишь редиректить cda'шные вызовы плюсовому модулю.

    • На тему "c wrapper for c++ class" нагуглился текст "Wrapping C++ objects in C".

      Ключевые моменты:

      1. Для объявления в .cpp-файле функции как использующей "C"-calling-convention применяется тот же способ, что и для .h-файлов -- окружения функции (и переменных?) "скобками"
        extern "C" { ... }
        (да, внутри КОДА это "extern" выглядит глуповато).

        ...ой, это я уже в другом месте подсмотрел -- "C Wrapper for C++" на StackOverflow.

      2. Для приведения передаваемого "из C++ наружу в C" указателя обратно к классу объекта используется static_cast.
    • В общем -- скелет сделан, даже собирается (для чего пришлось добавить поддержку .cpp в GeneralRules.mk).

      Состоит из 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@пляж, читал "Час быка": возникло понимание, как можно организовать работу:

    • Отдельные sid'ы отводить на каждый HOST:PORT.

      Как-то надо отдельно обращаться со спецификациями БЕЗ них.

    • В каждом sid'е иметь SLOTARRAY по именам device'ов, к которым привязывать ссылающиеся на них "каналы".
    • Раз вместо просто "каналов вообще" существуют отдельно атрибуты, properties и команды, для обращения с которыми РАЗНЫЕ интерфейсы -- то пусть можно будет в конце указывать @-суффиксом "вид" канала. Например, так:
      • @a (по умолчанию) -- attribute.
      • @p -- property.
      • @c -- команда.
      • @r -- результат команды.
    • Вероятно, при появлении ЛЮБОГО из @c и @r надо заводить сущность "команда", и при регистрации в дальнейшем второго из пары направлять его уже на готовую сущность.

      ...помнится, что-то подобное было реализовано в cda_d_v2cx.c для больших каналов.

    • Резюме: весь парсер имени канала можно взять из cda_d_cx.c.
    • И да: "сверху" пусть приходят разделители компонентов '.', сами переделаем в '/' (как в cda_d_vcas.c).
    • Остаётся вопрос о разделении обязанностей между .c- и .cpp-модулями.

      Вроде бы кажется простым решением делать всё в .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 <ТИП>, но обычный кастинг тоже нормально работает.

    • Аналогичное исправление потребовалось в misc_macros.h::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
        -- но так и корректнее.
      • Добавлено, что разделителем между PORT и DOMAIN может быть также '/', а не только '.'.
    • Введено понятие "тип канала" -- chtype, в виде enumCHTYPE_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).

        1. Позиция конкретно 3-го '/' запоминается, и далее он заменяется на '\0' -- так имя устройства отделяется от имени канала внутри него.
        2. Если количество!=3, то отваливаем с ошибкой.

    27.07.2019: продолжаем:

    • Введена концепция "dpx" -- DeviceProXy.
      • Их SLOTARRAY сквозной (также ради callback'ов).
      • Весь код менеджмента взят от hwr'ов.
      • Ещё ДО аллокирования hwr'а ищется, есть ли у данного sid'а уже dpx с таким именем устройства, и если нет, то создаётся новый.

        ...и даже если регистрация hwr'а далее обломится, то dpx останется.

      • Цепочка кода там, конечно, противноватая: оно аккуратно следит, чтобы strdup()'нутое имя устройства free()'илось когда надо, но НЕ в случае, если оно используется при регистрации нового dpx.
    • В cda_d_tango_init_m() добавлена регистрация dat-плагина, до того отсутствовавшая.
    • Проведена минимальная проверка работы полученного скелета -- вроде работает.

    Итого: у нас теперь есть вся "обвязка" -- парсинг плюс инфраструктура sid+dpx+hwr. Далее уже наполнять функционалом связи.

    28.07.2019@вечер-дома: инфраструктурное:

    • Наполнен tango/FrgnRules.mk:
      1. С проверкой наличия tango.h. Правда, оно в tango-9.2.5a/ в странном месте: в lib/cpp/server/.
      2. Плюс описание требуемых библиотек. А они в ещё более странном месте -- lib/cpp/client/.libs/ (следствие клятого libtool'а).

      Отдельный вопрос -- что будет в случае ИНСТАЛЛИРОВАННОГО в системе "пакета" tango (хоть .rpm, хоть вручную).

    • И добавлены соответствующие ссылки в {cda,util}/Makefile.

    30.07.2019: далее...

    • Попытка вставить в свой файл строчку
      #include <tango.h>
      -- ой-ё!!!
      1. Эти охломоны используют что-то такое, что требует "-std=c++11":
        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++!!!

      2. Распределение .h-файлов по директориям у этих граждан несколько странное.
        • Если tango.h почему-то в lib/cpp/server/, ...
        • ...то требует она файла apiexcept.h, расположенного в lib/cpp/client/!
        • Плюс ещё оно требует log4tango/Logger.hh, который вообще в lib/cpp/log4tango/include. И в куче ихних Makefile'ов делается
          -I$(top_builddir)/lib/cpp/log4tango/include

        Просто какой-то дикий набор подвязочек и махараек... И это -- одна из 2 ведущих СУ для физических установок в мире?! OMG...

    • Далее пытаемся сделать собираемость.
      1. Динамический вариант: добавлено несколько библиотек -- и засобиралось.

        Странно лишь то, что в конце пришлось добавить "-lstdc++" -- при том, что в libtango.so зависимость от libstdc++.so.6, судя по ldd, и так имеется. Вероятно, следствие линковки командой "gcc", а не "g++".

        ...впрочем, явно пришлось также указывать "-lomniORB4 -lomnithread", хотя от них зависимость в libtango.so тоже есть. WTF?!

        31.07.2019: проверил с "g++" в роли линкера -- результат тот же. Явно дело в чём-то другом.

      2. Статический вариант: авотхрен! Т.к. liblog4tango.a отсутствует -- есть только liblog4tango.so.

        И в гугле строка "liblog4tango.a" (С КАВЫЧКАМИ!) встречается всего 4 раза. Похоже, эти господа даже не задумываются о данной проблеме.

    • Добавлены первые "плюсовости" --
      • Класс cda_d_tango_EventCallBack с методом push_event() -- пока пустым. И всё остальное отсутствует, даже конструктора нет.
      • Поля hwrinfo_t.cb и dpxinfo_t.dev -- указатели на экземпляры классов. В Rls()'ах при !=NULL этим полям делается delete с последующим =NULL.

    31.07.2019: движемся дальше.

    • Делаем инфраструктуру pipe'ов.

      Модель берём не с cda_d_insrv/cda_d_local, а с pxi6363_drv.c. Причина -- что здесь, как и там, pipe нужен для передачи информации из callback'а, вызываемого АСИНХРОННО -- в произвольный момент и из другого thred'а (в отличие от синхронного вызова в cda_d_insrv, где pipe используется лишь для рассечения возможных петель исполнения).

    • Только есть одно сомнение: сколько "событий" можно записать в pipe до переполнения его буфера?
      • В cda_d_insrv.c запись идёт через fdiolib, со своей буферизацией, так что там железная надёжность.
      • В DAQmx'ных драйверах это безопасно -- там события редки, и "следующее" событие вряд ли возникнет до обработки "предыдущего" (т.к. отработавший task рестартовать надо).
      • А вот событий TANGO прилететь может много -- особенно при старте, из-за вычитывания текущих значений каналов.

        И зависит сие, очевидно, в первую очередь от количества каналов: несколько штук -- всё прокатит, сотни -- буфер может переполниться.

      01.08.2019: некоторая информация на эту тему есть в "man 7 pipe":

      • In Linux versions before 2.6.11, the capacity of a pipe was the same as the system page size (e.g., 4096 bytes on i386).
      • Since Linux 2.6.11, the pipe capacity is 16 pages (i.e., 65,536 bytes in a system with a page size of 4096 bytes).
      • Since Linux 2.6.35, the default pipe capacity is 16 pages, but the capacity can be queried and set using the fcntl(2) F_GETPIPE_SZ and F_SETPIPE_SZ operations.

      01.08.2019: мыслишка, как можно уменьшить потребление буфера fifo:

      • Завести per-hwr булевский флаг, взводить его при отправке и сбрасывать при получении, а если перед отправкой он !=0, то отправку не производить.

        Идея в том, что события "обновление" не особо имеет смысл накапливать -- обычному клиенту достаточно просто последнего значения.

      • Это немножко похоже на идею от 11-10-2015 в cda_d_insrv. Но там смысл был чисто в оптимизации (чтобы избежать окольного пути с отправкой сообщения в случае, когда и так процессинг не идёт и рекурсии не возникнет). И там никаких race conditions быть не может.
      • Главный вопрос -- КОГДА можно делать флагу =1, а когда обратно =0, чтобы не возникло потери событий?

        С точки зрения надёжности, наиболее безопасным выглядит такой подход:

        • Выставлять =1 -- ПЕРЕД отправкой.

          Иначе, если сначала отправить, а потом выставить, то может случиться, что произойдёт 1:отправка, 2:переключение_контекста, 3:вычитывание, 4:сброс_флага, 5:переключение_обратно, 6:взведение_флага -- тогда флаг останется навсегда взведённым, поскольку буфер пуст и вычитывать нечего (так что флаг сбросить некому), а новые события в буфер никогда не попадут, т.к. флаг-то взведён.

        • Сбрасывать =0 -- ПЕРЕД вычитыванием, т.е., когда реально ещё не вычитано.

          Т.е., при "сбое" в буфер просто попадёт ещё одно событие.

        • Проверять флаг -- очевидно, что перед цепочкой "взвести =1, отправить".

        Даже если race condition и возникает, то результатом "неблагоприятного" его исхода (т.е., когда решение принимается "неверно") будет лишь неисполнение возможной оптимизации -- ну отправит лишнее событие, ну будет ещё одна обработка, ну и ладно.

        ЗЫ: а как бы помогла атомарная команда "XCHG", которую б можно было применять к флагу! Установка -- "VAL=1; XCHG VAL, ФЛАГ", и если VAL!=0 -- значит, там и ранее была 1 и ничего делать не надо.

      • 02.08.2019: краткое обсуждение:
        • Плюс такого подхода -- экономия места в буфере pipe'а и избежание его переполнения при "лавинообразном" потоке событий.
        • Минус же -- получение клиентом не всех событий, а лишь "последних".
        • Ещё минус -- такое годится лишь для варианта, когда передаются ОДНОТИПНЫЕ события. Поскольку в противном случае принцип "Сбрасывать =0 -- ПЕРЕД вычитыванием" не катит, т.к. перед вычитыванием тип события ещё неизвестен и неясно, тот ли это, о котором флаг.

          Собственно, вот ЭТОТ минус обнуляет весь смысл идеи.

          ...разве только завести ДВА отдельных 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.

    • Процедурные тонкости в работе с именами:
      1. Теперь при отсутствии части "HOST:PORT/" устанавливается srvrspec="(INDIRECT)".

        Таким образом,

        1. Мы теперь ПОЗВОЛЯЕМ "непрямую" адресацию (раньше это было EINVAL).
        2. В cda_leds у лампочки будет отображаться осмысленный тултип.
      2. После всего парсинга/проверок указатель 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 -- вдруг там что поосмысленней найдётся.

      • В-третьих, там НЕТ автоматического преобразования типов. В комментариях при всех этих методах в DeviceProxy.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() добавлен фильтр, что

      1. nelems==1
      2. Разрешенные типы -- INT16, INT32, INT64 и их U-варианты.

        ...а вот INT8 -- увы, он в TANGO не поддерживается.

    • При попытке тестового запуска tango_cdaclient'а он вылетел по exception:
      terminate called after throwing an instance of 'Tango::ConnectionFailed'
      

      Откуда выводы:

      1. Надо бы как-то ловить эту хрень.

        Окей -- ловля вставлена, в виде try{}catch(...){}. Результат -- возвращается dev=NULL.

        Отсюда отдельный вопрос (или это уже как раз следующий пункт?) -- а как заставить НЕ считать начальную ошибку перманентной?

      2. Оно может и зависнуть при начальном коннекте -- а как сделать прямо НАЧАЛЬНЫЙ КОННЕКТ неблокирующимся?

        Гуглил -- что-то не находится.

    06.08.2019: ...

    • Примитивненько наполнен cda_d_tango_snd_data(). Всё тривиально и тупо -- с "new Tango::DeviceAttribute" и последующим его delete.

    Теперь остаётся:

    1. Разобраться с обломом создания DeviceProxy.
    2. Запрашивать начальное чтение канала.
    3. Научиться ПОЛУЧАТЬ значение канала. Что может быть совсем не так просто: значение прилетает callback'у в параметре 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, ...
    • ...а для poll-модели -- внутри #if !USE_PUSH_MODEL ... #endif.
    • В частности, сделана и начинка cda_d_tango_EventCallBack::push_event() для pull-варианта.
      • Оно добывает значение -- да, пока скалярное -- при помощи оператора >>, примерно так:
        case CXDTYPE_INT32: *(myevent->attr_value) >> val.i32; break;
        -- этот вариант НЕ аллокирует "объекта" в памяти, который потом нужно делать delete.
      • И затем отдаёт полученное наверх update_dataset()'ом.
    • Но тут вылезли грабли: при попытке таким оператором -- а он определён через template -- получить 64-битные значения компилятор выдаёт ругательство, суть которого сводится к
      error: 'long long int' is not an enumeration type
      (полный фрагмент выдачи тут в тексте закомментарен).

      Суть проблемы лично мне неясна.

      • С типом double такой проблемы не возникает.
      • Судя по файлу, вызывающему ошибку -- type_traits -- это что-то замудрённое C++-стандарто-специфичное (именно этот файл косвенно потребовал ключа -std=c++11).
      • ВОЗМОЖНО, дело в конкретной версии gcc -- 4.8.5, а в других будет иначе.

      Как бы то ни было, пришлось просто закомментировать обработку INT64-типов, так что теперь они прямо в cda_d_tango_new_chan() признаются неподдерживаемыми.

    Итого: в начальном приближении модуль сделан, надо пытаться его запинывать.

    09.08.2019: немного разборок с попытками запуска.

    • Ещё раз пробуем запустить, указывая TANGO_HOST=192.168.130.253:1234 -- и это заработало!!!
      • Оно сколько-то секунд висит (видимо, пытается подконнектиться), а уже только потом вылетает по "Tango::ConnectionFailed".
      • Число секунд, померянное через time, отличается при разных способах запуска (с/без ltrace), и бывает 6 или 9.
      • ...чуть позже: подсмотрено через ltrace -- да, оно пытается 3 раза делать цепочку socket(), TCP_NODELAY, O_NONBLOCK, connect(), poll(, timeout=3000). Это всё как-то перемежается с futex()'ами -- причём из другого thread'а; б-р-р-р...

      Почему раньше не получалось -- хбз: видимо, что-то по невнимательности не так делал.

    • Деталь: в выводе "ltrace -fSs200" почему-то отсутствует getenv("TANGO_HOST").
      • Как они этого добиваются -- загадка.
      • Была идея, что, может, вместо getenv() просто руками лезут в environ[] (переданный main()'у).
      • Но нет: в lib/cpp/client/devapi_base.cpp::get_env_var() -- который, предположительно, и вызывается для добычи $TANGO_HOST -- в начале присутствует попытка первым делом добыть через getenv().
    • Аналогично работает и при указании HOST:PORT прямо в имени канала.

      ...только пришлось исправить конверсию '.'->'/' -- а то оно за компанию переделывало точки и в имени/адресе хоста.

    • А вот дальше -- ну запустил я TangoTest, а оно к нему не коннектится, ну никак -- хоть ты тресни, только 'Tango::ConnectionFailed'.

    11.08.2019@дома: разобрался!!!!

    Оказывается, просто запустить СЕРВЕР с ключиком "-nodb" недостаточно. Нужно ещё и в имени устройства указать "#dbase=no".

    На тестовом клиенте (x10sae:~/work/tests/test_tango.cpp) получилось приконнектиться.

    А вот через tango_cdaclient -- фиг: оно символ '=' воспринимает как разделитель в паре ИМЯ=ЗНАЧЕНИЕ.

    Видимо, надо какой-нибудь специальный символ (недопустимый/бессмысленный в TANGO-именах), чтоб он заменялся на '='.

    12.08.2019: делаем. Резюме -- "всё плохо, TANGO -- отстой".

    • Сделал трансляцию '%' в '=', так что tango_cdaclient стал годен.
    • ...но зато теперь SIGSEGV в cda_d_tango_EventCallBack::push_event(), в точке, где пытаемся получить данные из атрибута.

      Первопричина нашлась быстро: поле "атрибут" -- myevent->attr_value попросту ==NULL.

    • Долго, О-О-ОЧЕНЬ долго пытался разобраться, что же к чему.
      • Посмотреть всё содержимое *myevent в GDB -- фиг: как ни пытайся, но просто НЕТ возможности сослаться на переменную-параметр для печати:
        (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) -- там ведь тоже как бы "внутри вызова чего-то другого".

        21.11.2023: давно, 23-06-2022, найден рецепт -- сначала "frame N", а потом печатай что угодно.
      • Окей -- пытаемся просто вручную, printf'ом (а для string-полей -- сбагриванием "cerr<<ПОЛЕ) поштучно всё выдать.
      • Оказалось, что err=1.
      • Там ещё есть поле "DevErrorList errors", а в tango_92.pdf пример, как печатать его содержимое.

        Новотхрен: тот код просто не компилируется -- компилятор говорит, что у класса DevErrorList отсутствует поле/метод size().

        Не, ну нормально -- просто нету никакого простого способа напечатать диагностическое сообщение? Они там совсем уже ку-ку, танцоры, блин, танго?!

      • В конце концов, рытьём по разным .h-файлам, нашлось, что можно напечатать просто начальный элемент этого списка (это некий "error stack"), примерно таким кодом:
        cout<<myevent->errors[0].desc

        И вот та диагностика оказалась прелюбопытной...

    • ТУТ РАССКАЗАТЬ ПРО ОШИБКИ -- что про всякую ненайденность печатает.
    • ОДИН ВОПРОС: нахрена присылать событие "change" при ошибке? Ведь на ОШИБКУ я не подписывался, это ОТДЕЛЬНОЕ сообщение!
    • Уведомления -- неа, НЕ работают. В принципе. Даже подписаться не даёт. Потому, что "polling thread" не запущена. А она у nodb-сервера запущена быть не может вообще никак -- ну нету такой возможности.

      Де-би-лизм...

    • Далее, для простоты, был проведён "тест" на test_tango.cpp.
      • ЧТЕНИЕ-то сработало -- простое, синхронное (асинхронный вариант пока не проверен).
      • А вот ЗАПИСЬ приводит к каким-то крайне странным результатам: в readonly-канал -- да на здоровье, просто без эффекта; а в _rw и _rww -- 'Tango::DevFailed'.

      Что ей там не так -- загадка. Конкретно TangoTest больной, что ли?

    13.08.2019: немного адаптации:

    • Поскольку, как вчера выяснилось, есть специальная функция, распечатывающая (на stderr) содержимое объекта-exception --
      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'ы очевидным образом прекратились.

    • Удивляют 2 вещи:
      1. Какого лешего уведомление "не могу приконнектиться..." присылается в качестве события CHANGE?! Это ведь по смыслу совсем иная штука, и даже не числится в списке причин для "change" (п.4.6.5.1.1).
      2. Зачем присылать сообщения "не могу..." постоянно, каждые 10 секунд?

        В CX запоминается, что о проблеме уже сообщено, и далее клиентскую программу не беспокоят -- сообщается только об ИЗМЕНЕНИЯХ.

        А тут простейший вроде функционал зачем-то перевешивают на клиента.

    22.08.2019: насчёт "DeviceProxy не создаётся, а генерит exception".

    1. Позавчера задал этот вопрос на танговском форуме.
      • "Question about DeviceProxy initialization: is there a way to ignore ConnectionFailed?".
      • Судя по моим тестам, собственно соединение с сервером, как они и утверждают --
        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.

      • А вот соединение с БД -- фиг, оно таких проблем не переносит (ни одной из 3) и генерит exception.
      • Andy Gotz подтверждает, что так и должно быть:
        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.
      • На мой уточняющий вопрос -- "It looks like an error when connecting to a DB server is a fatal error, while an error connecting to a DeviceServer is treated as temporary. Is it right?" -- ответа уже не последовало.

        Возможно, их возмутило моё признание "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).".

      • Кстати, что крайне неприятно: попытка соединения с БД зависает на 6 секунд.
    2. Появилась идейка, как можно обойти проблему "DeviceProxy не создаётся при отсутствии связи с БД":
      • @дома-вечер: А пытаться создавать их периодически, как выполняются соединения к CX-серверу в cda_d_cx. Только "тут" гранулярность чуть иная: более мелкие единицы DeviceProxy против целого sid'а "там".
      • 23.08.2019@дома-утро: а можно, чтобы не заводить по таймауту на каждый такой DeviceProxy, пользоваться готовым heartbeat'ом, который поллит события от каналов. Просто ввести "делитель частоты": если тот дёргается раз в 0.1с, то проходиться по списку несозданных DeviceProxy каждый 100-й раз -- тогда эта проверка будет происходить раз в 10с.

        Пара замечаний:

        1. Очевидно, для оптимизации и НЕвыполнения дурной работы необходимо будет dpx'ы держать кроме как в "списке dpx'ов sid'а" также и в "списке недосозданных dpx'ов", чтобы только ЕГОЙНЫЕ пытаться создавать повторно.
        2. TangoHeartbeatProc() сейчас вызывается ГЛОБАЛЬНО -- заказывается прямо в cda_d_tango_init_m(), и проходится по ВСЕМ hwr'ам.

          Соответственно, и попытку "пере-создания" оно будет выполнять для всех dpx'ов вообще, а не только от одного конкретного sid'а.

          ...с другой стороны -- ну и что такого? Ну и пусть.

        3. 28.08.2019@дома-утро: замечание: надо будет ВЕЗДЕ проверять, реально ли уже "создалось" содержимое hwr'а. В частности, в CheckEventsIterator() у "недосозданных" ещё hwr'ов (тех, чьи dpx'ы ещё не создались) будет cb==NULL, так что НЕЛЬЗЯ от него пытаться делать get_events().

          Вот конкретно на то прямо сейчас поставил ограждение "if(hi->cb!=NULL)".

      • 02.10.2019: концепция поменялась -- cb теперь создаются сразу ВСЕГДА, а признаком "неготовости" является di->dev==NULL.

        Так что ограждение заменено на "if(di->dev!=NULL)".

      В таком варианте вроде должно работать надёжно (не считая вопросов к оптимальности).

      Вопрос лишь, что делать с зависанием на 6 секунд. Уносить создание в другой thread? Зело проблемно. Да и если нужно создать сразу целую пачку -- долгий будет процесс; не вытаскивать же создание КАЖДОГО в отдельный thread...

    02.10.2019: возвращаемся к этой неприятной теме -- реализуем "реконнекты".

    • Для начала, инфраструктура "выполнять проверку каждый 100-й раз" ("деление heartbeat-частоты на 100").
      • "Делитель" -- TANGO_RCN_FRQDIV=100.
      • Счётчик -- tango_rcn_ctr, ...
      • ...в TangoHeartbeatProc() он делается ++, и когда станет >=TANGO_RCN_FRQDIV, то и будет "прошерстение" всех нуждающихся в реконнектах.

      12.05.2023: неприятный нюанс такого подселения: реконнекты работают ТОЛЬКО при !USE_PUSH_MODEL; а при push-модели (для поддержки которой много сделано в августе 2022) -- автоматом нет, т.к. тогда самого TangoHeartbeatProc() даже не определяется.

      14.05.2023: исправлено.

    • Далее -- организовываем "список недосозданных dpx'ов".
      • Добавляем к dpxinfo_t поля nx2r и pr2r -- ссылки в списке "для реконнектов".

        При аллокировании dpx'а им делается =-1.

      • Ссылки на голову и конец списка -- frs_2rcn и lst_2rcn; изначально также =-1.
      • Менеджмент этого списка -- AddDpxToRcn()+DelDpxFromRcn(), скопированные с Srv-аналогов.
    • Собственно использование:
      • Создание DeviceProxy вытащено в отдельную create_dpx_dev().
        • Она занимается ТОЛЬКО созданием DeviceProxy, но НЕ принимает никаких решений типа "обломилось -- делаем RlsHwrSlot()!".

          ТО действие, кстати, и из её юзереа cda_d_tango_new_chan() убрано, ибо теперь не-создание Proxy не является фатальным.

        • И добавлен параметр is_suffering -- для репортинга проблем.
          1. В начальном вызове он ==0, так что репортится только начальная ошибка.
          2. В последующих -- ==1, так что дальнейшие ошибки НЕ сообщаются, но зато репортится "created!" (чего НЕ делается при первом вызове).
          3. И эта штука, в отличие от cda_d_cx.c и прочих подобных -- ОДНОРАЗОВАЯ (по крайней мере, пока), поскольку единожды создавшись, объект DeviceProxy более не исчезнет (в отличие от соединения, могущего оборваться).

            Если в будущем добавим реакцию на события самого Proxy -- то, может, и поле dpxinfo_t.is_suffering сделаем.

      • Оформление подписки на события для канала (точнее, ATTRIBUTE) сейчас вытащено в create_hwr_subscription().
        1. При "начальном" вызове -- из _new_chan() -- обломы создания считаются фатальными и возвращается CDA_DAT_P_ERROR.

          ...отдельный вопрос -- КОГДА может произойти такой облом: сейчас даже с "-nodb"-сервером и каналом "#dbase=no" создаётся нормально, а ругательства сыплются уже от cda_d_tango_EventCallBack::push_event().

        2. При вызовах "потом" -- ничего не делается, даже результат не проверяется.

          Видимо, такой hwr станет просто "подвисшим".

      • "Прошерстение".
        • Проход по списку идёт от начала к концу. Была мысль сделать с конца в начало -- это требовалось бы, если б при "попытке реконнекта" dpx'ы бы ВСЕГДА сначала удалялись из списка, а при обломе опять бы добавлялись. Но, поскольку удалять будем только при успехе, то задомнаперёдность не требуется.
        • Каждому элементу в списке "для реконнектов" делается create_dpx_dev(), и если успешно, то:
          1. DelDpxFromRcn(dpx).
          2. ForEachHwrSlot()'ом проходимся по всем hwr'ам и тем, у кого hi->dpx == dpx, вызываем create_hwr_subscription().

      Всё!

    • Изрядной -- ИДЕОЛОГИЧЕСКОЙ! -- проблемой является то, что при первоначальном обломе создания DeviceProxy мы не можем выполнить запрос подписки, так что его (в случае каких-то проблем) облом будет не немедленным, а "потом", и тогда грохнуть уже созданный hwr уже никак не удастся.

    Теперь найти бы какой-нибудь способ это всё протестировать. Напрашиваться к Роговскому на ВЭПП-2000?

    05.08.2022: спустя почти 3 года возвращаемся к этой штуке -- в первую очередь в интересах epics2tango, т.к. там нужно понимание работы с Тангой, а тут оно максимально (насколько сие возможно для данного кошмара).

    Чисто для закрепления, вот что стало ясно/вспомнилось после чтения раздела знаний, вышенаписанного в этом разделе и исходника cda_d_tango.c:

    • Пришлось использовать PULL-модель, поскольку PUSH-модель функционирует только через multithreading, причём присылает именно ДАННЫЕ в "произвольный" thread и НЕЛЬЗЯ оттуда просто слать уведомления (через pipe) и реально вычитывать в основном thread'е.

      (А вот для 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 бывают как у атрибутов ("каналов"), так и у устройств.)

    • В любом случае, присутствуют какие-то странные подвисания при коннектах -- порядка секунд.
    • ВРОДЕ бы реализованы "reconnect'ы" при первоначальном подключении -- модуль пытается создавать DeviceProxy до тех пор, пока не получится.
    • Очень хреново в TANGO реализованы уведомления об ошибках: оно присылает "событие ошибка" CHANGE-callback'у, который на них НЕ подписывался.
    • Главное: оно толком ТАК И НЕ БЫЛО ПРОВЕРЕНО!
    05.08.2022: кстати, а если попробовать собрать СОВРЕМЕННУЮ версию Tango -- tango-9.3.4.tar.gz -- и проверить поведение с ней, изменилось ли что-нибудь?

    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 секунд.
    • Попробовал атрибут, который ЕСТЬ -- "ampli", "localhost:10001/a/b/c#dbase%no.ampli"; результат печален:
      The polling (necessary to send events) for the attribute ampli is not started
    • @~16:40, по дороге на М-46: ну и чё, разбираться-таки с тем, как запускать этой хрени СУБД?

    @вечер: продолжаем.

    • Кстати, список поддерживаемых атрибутов с указанием их типов содержится в cppserver/tangotest/TangoTest.cpp.

      Это файл, сгенерённый POGO; в 9.3.4 прилагается также и "исходник" (?) TangoTest.xmi.

    • Что любопытно, при попытке обратиться к этому "ampli" как к @i: -- т.е., к int32, в то время как он double -- никакой ошибки НЕ выдалось.

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

    • Наткнулся в документации на раздел "Running a device server without SQL database", где упоминается, что можно вместо БД использовать и ФАЙЛ. Правда, какой формат этого файла -- там не сказано, просто "The file is an ASCII file and follows a well-defined syntax with predefined keywords. The simplest way to generate the file for a specific device server is to use the Jive application."...

      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".)

      Там же, кстати, сказано, что при этом всего лишь навсего не будет

      1. проверки на дублирование имён и
      2. alias'ов

      А вот при "-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()'ить.
    • А для предотвращения одновременного доступа завести к нему mutex (благо, в mt_cxscheduler.c технология отработана.

    Собственно "вычитывание данных из 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: реализуем вчерашнюю идею про "всегда использовать буфер".

    • Создана пара функций для складывания данных в буфер и передачи их "наверх":
      1. Добыча текущего значения переведена на добычу в буфер и вытащена в store_current_value().

        Она возвращает код "успешно" (0) и "неуспешно" (-1). Неуспешным считается также "неподходящий тип".

        Она также ловит exception'ы и при их поимке возвращает -1.

      2. А для отдачи сделана 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: делаем.

    Предварительно -- немного информации:

    • "Узнавание текущего типа" выполняется методом DeviceAttribute::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_nnnDEVVAR_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, так что никогда ничего не срабатывало. Исправлено.

    • В нём пока присутствуют лишь базовые типы -- SHORT/USHORT, LONG/ULONG, FLOAT/DOUBLE.
    • Нету ни UCHAR, ни STRING, ни INT (вот ЭТО что?!), ни "специфичностей" BOOLEAN, STATE, ENUM -- этих, видимо, надо вычитывать и приводить к 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):

    • Для cda_d_tango -- видимо, придётся принять правило "извольте регистрировать с нужным типом".
    • А вот для epics2tango.c* -- ПРИДЁТСЯ уметь вычитывать свойства атрибута (т.к. именно оно и будет исходником информации, а более ей взяться неоткуда...).

    И да, в голову лезут всякие мысли о возможностях реализации, примерно такого вида:

    • Главная идея -- заводится отдельный thread для резолвинга.
    • Взаимодействие:
      • Он получает команды через pipe, исполняет их, возвращает результаты и возвращается к чтению команд.
      • Возврат результатов может выполняться прямо готовыми объектами в памяти, а "обратный сигналинг" о готовности -- через второй pipe.
      • Т.е., объект создаётся в одном thread'е, после чего тот про него "забывает", передавая (уведомлением) другому, и уже тот с ним работает монопольно.

        Например, если "резолвер" создаёт объект DeviceProxy, то он дожидается коннекта, в случае успеха -- сигнализирует о готовности "ведущему", после чего этого объекта уже более не касается; если нужно будет его грохнуть -- то это делает "ведущий".

      Обсуждение:

      • Таким образом, мы избавляемся от необходимости использования семафоров/mutex'ов и тому подобных неприятных средств. А просто каждый из 2 будет заниматься своими делами ЛИНЕЙНО.

        Да, при больших нагрузках pipe'ы будут затыкаться, но это будет приводить лишь к тормозам -- без каких-либо фатальных последствий.

      • Единственное "но": учитывая 2 противонаправленных pipe'а, есть вероятность deadlock'ов, когда забиты ОБА канала, и каждый из отправляющих ждёт, пока собеседник не вычитает свою часть. Защитой от такого, видимо, будет O_NONBLOCK на пишущем pipe со стороны "главного".
      • @чуть позже, завтрак, "The Wire" S03E01: а можно вместо pipe'ов использовать датаграммные сокеты -- socketpair(,AF_UNIX,SOCK_DGRAM): датаграммы по определению уходят либо целиком, либо никак -- это защитит от возможной отправки лишь куска сообщения.
    • Он МОЖЕТ выполнять создание объектов вроде DeviceProxy.

      ...Или "ДОЛЖЕН" -- ведь это потенциально зависающая операция?

    • А вот весь менеджмент SLOTARRAY'я -- только на "ведущем".
    • @душ, чуть позже: а вот с "созданием DeviceProxy и дальнейшей передачей их ведущему" будет проблема: ведь ведущему они будут нужны для В/В, но и резолвер при дальнейших запросах тоже будут нужны эти же DeviceProxy; а в параллель их использовать точно не стоит (в т.ч. потому, что ведущий может захотеть их и грохнуть -- а что тогда? заводить-таки считающие семафоры?).

      Напрашивается только создание DeviceProxy ПАРАМИ.

      ...ну либо вообще вынос ВСЕГО взаимодействия с TANGO во 2-й thread, а основной только взаимодействует с libCAS да переправляет запросы/ответы туда-обратно...

    • @попозже, днём ~14:00: а можно пойти ещё дальше -- держать целый ПУЛ таких thread'ов резолверов/исполнителей, по которым распределять вновь появляющиеся DeviceProxy ЦИКЛИЧЕСКИ.

      Т.е.,

      • Для каждого уже зарегистрированного адреса DeviceProxy помнить, какому thread'у-резолверу он назначен, и при запросах к уже зарегистрированным именам -- передавать запросы соответствующим ответственным.
      • А вот для НОВЫХ имён Device'ов -- назначать следующему, и тут "следующий" -- просто циклически перебираются из пула.
      • Размер этого "пула" в штуках чтоб настраивался, например, из командной строки.

      Ужас-то какой, прости господи...

    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: по размышлениям "да как же всё-таки там всё организовать?!" в последние несколько дней сложился такой список действий хотя бы для начала:

    1. Завести набор "видов" каналов: DEVATTR, DEVPROP, ATTRPROP, COMMAND, RESULT; и держать вид канала не в отдельном поле chtype, а прямо в in_use.
    2. Парсинг имён:
      • в процессе подсчёта '/'й также обращать внимание на "->", складируя указатель на оную, например, в p_ar,
      • и при нахождении очередной стрелки проверяя, что если p_ar!=NULL -- значит, это уже ДВЕ стрелки в имени, т.е., синтаксическая ошибка.
      • А после цикла проверять, что если p_ar!=NULL, то должно быть p_sl<p_ar -- т.е., стрелка должна быть ПОСЛЕ слэшей (иначе опять синтаксическая ошибка).
      • 14.11.2023@сидя у Беркаева при обсуждении по ТНК, ~13:00: поскольку указывать "->" и в командной строке неудобно ('>' -- это stdout redirect), и по синтаксису имён каналов оно плохо проходит (из-за '-'), то...

        Можно сделать, чтобы последовательности символов ".." и "//" превращались бы в стрелку.

        @~16:00: сделано, в процессе "общего улучшения".

    3. Команды+результаты -- общая парадигма работы:
      1. Первоначальный вариант (подразумевавшийся ещё с 2019-го): сделать в стиле больших каналов в cda_v2cx -- "при запросе на создание кого-то из них создавать сразу оба, и искать пару".

        Мутновато и неприятно.

      2. Иметь в cda_d_tango_privrec_t ОТДЕЛЬНЫЙ SLOTARRAY cmds_list, в котором создавать команды ("привязывая" их к dpx'ам), а уж к нему "привязывать" hwr-ячейки и команд, и результатов. И чтоб они регистрировали в cmd-ячейках свои "callback'и".

        Да, громоздковато -- учитывая, что на каждую cmd-ячейку обычно будет приходиться по не более 1 команды и 1 результата. Но зато такая архитектура выглядит понадёжнее.

    14.11.2023: возвращаемся к работе над модулем. Для начала улучшения, продуманные 03-06-2023:

    • Насчёт CHTYPE -- всё сделано:
      • Список CHTYPE_nnn доделан до полноты в соответствии с проектом (ATTRIBUTE и PROPERTY переименованы в DEVATTR и DEVPROP, добавился ATTRPROP).
      • Также добавлен CHTYPE_UNUSED=0 -- в интересах следующего пункта.
      • SLOTARRAY "Hwr" переведён на использование в in_use этих значений; при создании ячейки ей ставится CHTYPE_DEVATTR, затем в cda_d_tango_new_chan() прописывается нужное.
      • Поле chtype удалено.
    • Модификации в парсинге имени:
      • Два последовательных прохода dup_name разделены по функциям: теперь первый выполняет трансформации (в него переехало '%'->'='), а второй уже выполняет подсчёты/учёт.
      • В частности, в первый добавлено преобразование ".." и "//" в "->" (реально любая пара из '.' и '/' превратится в стрелку).
      • Во второй добавлено распознавание стрелки и запоминание её позиции в p_ar.
      • НЕ СДЕЛАНО:
        1. Нет проверки "должно быть sl3<p_ar", т.к. для DEVATTR формат вообще "DOMAIN/FAMILY/DEVICE->PROPERTY", т.е., слэшей всего 2, а не 3, и это пока не поддерживается никак.

          16.11.2023@утро: добавлена; вместе с определением chtype по формату, если оно возможно (при наличии стрелки: nsls=2 -- DEVPROP, nsls=3 -- ATTRPROP). Плюс далее добавлена проверка, что если получившийся chtype != CHTYPE_DEVATTR, то "не поддерживается".

        2. Пока не "украсивлена" общая последовательность парсинга -- чтоб не было жонглирования значением 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
      
    • И под GDB разобраться не удалось -- никакой логики не видно в мешанине цепочки вызовов.

    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
      
    • либо
      push_event(hwr=1:"RFGunPhasemeter/test/2!dataLenght"):
      The polling (necessary to send events) for the attribute datalenght is not started
      
      -- хотя jive показывает, что у этого атрибута присутствует 3 listener'а.

      (Строки разбиты на 2 для этого файла, разделитель '!' -- это наша диагностика.)

    16.11.2023: Лёша Герасёв напомнил, что в C++ постоянно вылазят проблемы с ABI: если даже просто разными версиями компилятора собраны части, то может не срастись.

    • (ЕМНИП, по идее, объектники должны б как-то маркироваться специальными символами, чтоб не линковалось при несовместимых ABI, но вот увы...)
    • А тут и вовсе с 9.2.5a было собрано gcc-4.8.5 20150623 под CentOS-7.3, а с 9.3.4 запускалось на Debian-11.8, где gcc-10.2.1-6 20210110 -- т.е., версии компилятора ну о-о-очень разные.
    • Ну и реально отличается -- в 9.3.4 log4tango не нужен: libtango.so.9.3.4 его не требует.

      Попробовать-таки вариант статической сборки, который с 9.2.5a не работал принципиально?

    Очевидно, это и есть причина, по которой связка "cda_d_tango.so собрана с 9.2.5a, а запускаем с 9.3.4" не работает. Ну и не будем тогда с ней возиться :).

    • Гуглим по фразам "necessary to send events" и "abs_change or rel_change" в сочетании со словом "tango".

      Найдена 2-я страница 4-страничного обсуждения "Attribute Events gets missed" за неясную дату "5 years ago".

    • И grep'им -- содержатся они в файле lib/cpp/server/eventcmds.cpp
    • ...но результат попадает кроме lib/cpp/server/.libs/libserver.a также и в lib/cpp/client/.libs/libtango.a

      Проверено (путём сборки с 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?

    • Попробовано.
    • Атрибут "RFGunPhasemeter/test/2/dataLenght" -- ругательство "Event properties ... are not set" ушло.
    • Но обновлений -- нет, ни через секунду, ни через час. 21.11.2023: реально были, не отрабатывались из-за косяка в store_current_value().
    • А вот для атрибута "GunTimerDS/srpa/1/temperature" -- ругательство "The polling (necessary to send events) ... is not started" осталось.

    Ну-с, и что теперь? :D

    @вечер: а у нас ведь всё проверяется в PULL-модели; надо б попробовать PUSH-модель.

    21.11.2023@утро, зарядка: учитывая, что код весьма хитрозавёрнутый, то может быть и какой-то внутренний косяк в нём.

    • Ещё вариант -- написать какой-нибудь простенький test case, минимальную утилитку, которой указывается "имя канала" (дуплет DEVICE,ATTRIBUTE), а она пытается оформить подписку и получать данные.
    • И чтоб ключиками командной строки можно б было указывать тип события для мониторирования (-c/-r change/periodic).
    • А в идеале и "модель работы" -- PUSH или PULL (-p/-l).
    • Плюс,
    • @лабораторный круглый стол по вторникам, ~09:50: назвать её "tangomonitor", условно по аналогии с camonitor.

    21.11.2023: пробуем USE_PUSH_MODEL=1.

    • Включен этот режим, тестируем.
      • PERIODIC, первый атрибут (для которого поллинг запущен) -- то же самое.
      • PERIODIC, второй атрибут (для которого поллинга нет) -- SIGSEGV.

        Прогон под 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>}}
        
        (переносы строк мои, а то это читать невозможно).
      • CHANGE, первый атрибут -- ...забыл, а воспроизвелось как-то не так... (не то/разное откомпилировал?).
      • CHANGE, второй атрибут -- опять SIGSEGV.

        Разница только в том, что event = "change".

    • Полная матрица, с печатью __FUNCTION__ на входе в cda_d_tango_EventCallBack::push_event():
      • PUSH=0
        • CHANGE:
          • t: каждые 10с "Event properties ... are not set"
          • d: каждые 10с "The polling (necessary to send events) ... is not started"
        • PERIODIC:
          • t: каждые 5с по 2-3 события БЕЗ ПОСЛЕДСТВИЙ.
          • d: каждые 10с "The polling...".
      • PUSH=1
        • CHANGE:
          • t: SIGSEGV, event = "change", attr_value = 0x0, err = true

            каждые 10с "Event properties..."

          • d: SIGSEGV, event = "change", attr_value = 0x0, err = true

            каждые 10с "The polling...".

        • PERIODIC:
          • t: события БЕЗ ПОСЛЕДСТВИЙ, вначале сразу, потом раз в 2000мс; ровно как сказано в документации относительно change ("At event subscription time").
          • d: SIGSEGV, event = "periodic", attr_value = 0x0, err = true

            каждые 10с "The polling...".

    • Разобрался: косяк был в том, что push_event() в ветке USE_PUSH_MODEL отсутствовала проверка "if (myevent->err) {РУГНИСЬ; return;}" -- вот оно и вылетало. Исправлено путём выноса проверки в начало метода, ДО #if.
    • После чего результаты (точнее, сообщения об ошибках) совпадают с режимом PUSH=0.
    • Немного наблюдений:
      • В режиме PUSH=1 сообщение "Event properties..." выдаётся СРАЗУ, и потом уже с периодом 10с, хотя ПЕРВЫЙ интервал 12с (10с+2000мс? период опроса temperature -- 2000ms).

        А вот сообщение "The polling..." -- начинается через 5 секунд.

      • 5 секунд -- не потому ли, что для отладки стоит "TANGO_HEARTBEAT_USECS = 5000000"?
      • Имевшееся 20-11-2023 "обновлений -- нет, ни через секунду, ни через час" явно следствие "событий БЕЗ ПОСЛЕДСТВИЙ": т.е., события-то ПРИХОДЯТ, просто как-то не так обрабатываются.
    • Ещё немного разбирательств:
      • "каждые 5с по 2-3 события" -- видимо, это за цикл поллинга 5 секунд успевает прилететь то 2, то 3 события с периодом 2с.
      • Меняем TANGO_HEARTBEAT_USECS:
        1. с 5с на 20с:
          1. Период 10с остался (как и 12=10+2) -- в режиме PUSH=1.

            Но в режиме PUSH=0 в каждом 20-секундном цикле прилетает по 2 сообщения.

          2. А "события БЕЗ ПОСЛЕДСТВИЙ" приходят по 10шт.
        2. с 5с на 1с:
          1. Период 10с остался (с 12=2+10 сложнее: выглядит как 11=1+10, т.к. в PUSH=0 первое получаем с задержкой).
          2. И "события БЕЗ ПОСЛЕДСТВИЙ" приходят по 1шт с интервалом в 2с.

        Т.е.,

        1. ОШИБКИ не зависят от нашего опроса и приходят с 10-секундным периодом.
        2. А "опрос" при PUSH=0 -- зависит (ну кто б сомневался!).
      • "события БЕЗ ПОСЛЕДСТВИЙ" откуда они могут браться? Явно store_current_value() возвращает r!=0 -- а почему?

        Разобрался!!! В store_current_value() и была ошибка, идиотскейшая: там было

        switch (hi->dtype)
        вместо корректного
        switch (data_type)

        Исправил -- и в PERIODIC-режиме данные температуры стали приходить!!!

    Выводы/результаты:

    1. Вчерашнее подозрение "код весьма хитрозавёрнутый, то может быть и какой-то внутренний косяк в нём" оказалось верным -- косяк был, и не один, а сразу два, и оба дурацкие ляпы :)
    2. Режим USE_PUSH_MODEL=1 протестирован, он реально работает -- с точки зрения TANGO-функциональности примерно наравне с PULL-режимом, но оперативнее.
    3. Данные получаем!

    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 определяется наличие запрошенного канала.

    • Второе же необходимо для взведения в cda_core.c флага is_ready, без которого не будет работать запись.

    Но проблема в том, что неясно, где вообще можно эти события/вызовы делать -- в TANGO сходу не видно своего сообщения "найдено/не-найдено".

    23.11.2023: решил немного поразбираться, как же всё-таки организовать асинхронность.

    Источники информации --

    Итак:

    • Варианты запуска асинхронных операций описаны в "4. Writing a TANGO client using TANGO APIs", раздел "4.5.2 Asynchronous model ",
      1. С передачей cb -- "push"-вариант -- тогда по возвращению результата "генерится событие" в виде "вызова" оного cb.
      2. Без него -- "pull" -- тогда вместо void возвращается 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: приступаем.

    1. Методы callback'а:
      • Заведена 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, потому взятие указателя.
    2. Методы dat-плагина:
      • В 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);
    -- т.е., проверяются только события от подписки на обновления.
    • Похоже, в PULL-режиме надо таки пользоваться без-cb'шными вариантами исполнения асинхронной записи, запоминая long-идентификаторы запросов и полля их вместе с subscr_event_id.
    • Но запросов-то на чтение может поступить несколько штук подряд, и как помнить их все?!

      Ну ладно, можно смотреть, что запрос на чтение уже есть ("read_event_id != -1", аналогично cxsd_hw'шному rd_req) и последующий просто игнорировать -- результат-то всё равно придёт.

    • ...но с записью так нельзя: нужно исполнить ВСЕ запросы записи.

      Так что игнорировать их нельзя, ...

      ...но и перепрописывать следующими предыдущие event ID тоже нельзя -- подтверждения на старые так и останутся висеть "непрочитанными".

    Мда, "охренительно продуманный" API...

    @вечер: дальше-то что делать? Видимо, теперь, с минимальной отлаженностью на реальном сервере и чуть улучшившимся пониманием, сделать-таки tangodb-файл для TangoTest и тестировать на нём -- как минимум запись можно спокойно и безопасно проверить, плюс "диаграмму событий" при записи и при просто обновлении в предсказуемом окружении.

    27.11.2023: попробовал -- авотхрен.

    • Сделал файл work/frgn4cx/tango/cda/TangoTest.tangodb такого содержания:
      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/ идентично.

    • Стал искать по строке «"Call to a Filedatabase not implemented"» (в кавычках, чтоб фраза!).

      (Поиск по «"Can't get class pipe properties"» даёт ровно то же)

      • Результат -- по факту ровно 1, "File db does not support pipes #300" от 18-10-2016 (изначально это "tango-cs/bugs/821").
      • Ключевое там не "Filedatabase not implemented", а именно "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.

      • И 16-11-2018 зафайлен ещё один багрепорт -- "Can't get class pipe properties for class #234", ссылающийся на #300 (а там ссылка обратно).

        (Да, нумерация багов странная (из-за того, что это PyTango?); и почему Google этот баг не нашёл -- загадка, т.к. искомая строка в тексте имеется.)

      • Но 24-02-2022 #300 закрыто как "completed", а 07-04-2022 и #234 тоже.

        БЕЗ каких-либо пояснений (как это делается у RH, со ссылкой на errata или иной бюллетень).

    • Засада в том, что последняя на данный момент версия, судя по странице "Downloads - TANGO Controls" -- вроде как 9.3.4, о чём нагугливается новость Tango source distribution: 9.3.4 от 26-10-2020 (в тексте даты нету, только в URL'е).

      Хотя какие-то релизы "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 распакованное), но как собирать -- фиг поймёшь.

    • P.S. Да, сайт (точнее, сайтЫ -- их туча слабосвязанных!) у них устроен отвратительно: просто так с самого сайта информацию фиг найдёшь, только Google'ом. Напоминает National Instruments -- у тех тоже ни структуры на сайте толком не было, ни встроенный поиск не помогал, а постоянно находил устаревшую информацию вместо более свежей, так что лишь гугление спасало.

    Итого: СЕЙЧАС никакого варианта решения не видно. Обломитесь.

    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 или что-то есть ещё?

      Да, версия "полного дистрибутива" -- 9.3.4, но они перешли на схему с LTS и теперь есть отдельно более свежие версии, только искать их надо в git'е.

    • cppTango-9.4.x -- ? А собирать его как?

      Да, "cppTango" -- это C++'ное ядро. Есть даже 9.5. А следующее обещают уже 10. И вроде как теперь уже НЕ обещают обез-CORBA-вливание.

      Собирать -- "cmake".

    • TangoTest -- что с ним касательно ключа "-file"?

      Сенченко не знает, не пользовался. "Не работает файловая БД? Ну запусти обычную БД!"

    • Кстати, pipe'ы собираются в следующей версии выкинуть, т.к. ими практически никто не пользуется. И сам Сенченко тоже ими разочарован: он-то думал, что это такой способ передачи структурированных данных (в стиле "тэг,значение"), но нет.
    • Насчёт CHANGE_EVENT: что такое abs_change и rel_change и где они уставляются -- со стороны клиента или в БД?
      • Это свойства, которые можно уставить в БД с помощью jive.
      • Для тех атрибутов, которые помечены как "polled", запускается поллинг с указанной периодичностью, и получаемое чтением значение проходит проверку с этими свойствами, чтобы понять, считать ли новое значение за "изменение".
      • Если атрибут "polled", но эти свойства не выставлены -- обновления на него присылаться не будут.

        (От меня: дичь.)

      • Но для атрибутов, которые обновляются автоматически со стороны устройства -- как измерения от козачиных АЦП -- драйвер может указывать серверу, что это обновление ВСЕГДА надо проталкивать как "change".
      • А как с атрибутами ЗАПИСИ -- там-то можно просто любое обновление считать изменением?

        Тут у TANGO действительно косяк: никак, и это у них висит известным багом, который пока почему-то не могут исправить, возможно, в силу архитектуры.

        Если требуется -- то надо поступать как с "измеряемыми" атрибутами, делая их "polled" и указывая свойства, определяющие, что "изменилось".

    • Что такое DEV_ENUM?

      Это short; и список строк к нему прилагается.

    • (мылом позже) Можно ли как-нибудь получать уведомления об установлении соединения и о его разрыве? В идеале -- не только device'а, но и атрибута?

      В общем случае нет. Будет выброшено исключение при попытке прочитать/записать/вызвать.

      Более того, tango само закрывает подключение к серверу если клиент не обращался к девайсу в течение некоторого времени (минута или что-то вроде). Если используются события, то при выключение сервера/потери соединения прилетит событие с ошибкой (API_EventTimeout). Но это не оперативное событие.

      Есть еще вот такая табличка https://www.esrf.fr/computing/cs/tango/tango_doc/kernel_doc/cpp_doc/recon.html, но тут только и попытках общения с сервером. Может поможет.

    • 29.11.2023: в продолжение предыдущего пункта:

      Уточнение вопроса: если есть потребность уметь понимать (желательно асинхронно) существует ли некий атрибут? Например, некоей программе-аналогу caget'а говорят прочитать атрибут с указанным именем, и она должна либо прочитать его и напечатать ответ, либо выдать на stderr строку "DOESN'T EXIST". Ну или вообще просто некая утилитка командной строки does_attribute_exist, должная вернуть код 0 при наличии указанного атрибута либо код 1 при его отсутствии либо при отсутствии связи. Можно ли это как-то в cppTango элегантно сделать?

      Попробуй через этот метод: Tango::DeviceProxy::get_attribute_list()

      От меня: get_attribute_list() -- т.е., получить список атрибутов и искать в нём самому! Да ещё и получение СИНХРОННОЕ! Блин...

    29.11.2023: ну и что теперь дальше делать, учитывая насколько всё убого и печально в этой французской дичи?

    Напрашиваются следующие вроде несложные, наверняка реализуемые и в любом случае полезные шаги:

    1. Доразобраться с DEV_ENUM и понять, как корректно из него читать.
    2. Научиться получать timestamp'ы.

    30.11.2023: ну пробуем.

    • С timestamp'ами на вид вообще всё тривиально до полного неприличия:
      • В классе 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;
        
      • ЗЫ: а вот в lib/idl/tango.idl обнаружилось такое:
        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" локального -- явно из-за расхождения часов.

    • Что касается DEV_ENUM:
      • Заменил "принимающий тип" с int32 на uint16 -- не помогло.
      • Запустил ещё разок под GDB и увидел, что hi->me==NULL;
      • ...внимательный взгляд на результат "bt" показал, что callback вызывается ДО завершения заказа подписки subscribe_event() -- т.е., прямо из него, а это ещё ДО прописывания поля me, производимого в AddHwrToSrv().

        Почему это не проявлялось раньше, с другими типами атрибутов -- загадка.

      • Посему -- перетащил дуплет "AddHwrToSrv(); cda_dat_p_set_hwr()" в точку ДО заказа подписки.
      • Помогло -- падать перестало, присылает значение 0.

        @вечер: а присылает ли? Ведь все "operator >>" проверяют, что внутри лежит тот тип, что в выходном аргументе, и если нет -- то просто вертают false, более ничего не делая; а 0 просто в буфере с рождения.

        01.12.2023@утро-завтрак: стал разбираться. В DeviceAttribute::get_type() (вызвавшем удивление ещё 26-08-2022) фраза "return Tango::DEV_ENUM" отсутствует, в отличие от всех остальных типов, но зато есть вот такое:

        else if (ShortSeq.operator->() != NULL)
        {
            return data_type;
        }
        
        -- т.е., конкретно при обнаружении short (причём не-unsigned!) оно возвращает именно значение поля data_type.

        OK, переделал на int16/CXDTYPE_INT16, плюс сделал печать булевского результата работы "operator >>". Проверено: при int16 результат 1, при uint16 -- 0. Значение по-прежнему 0, но теперь уже настоящий -- проверено записью в буфер числа 12345 перед получением.

      • Правда, теперь присылает сразу пачку из 99 штук.

        Разобрался: это не он "присылает сразу пачку", а у меня был дебильный косяк в 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'ы. А дальше-то что делать? Напрашивается такая цепочка несложных действий:

    • Сделать КОРРЕКТНУЮ работу запросов чтения и записи: т.к. callback'и там возможны только в PUSH-режиме, то пусть только в нём и определяются.
    • "Вручную" проверить чтение -- просто заказывать однократное чтение в момент подписки.
    • На будущее понадобится работа с векторными данными.

      В DeviceAttribute она сделана исключительно в виде "C++ std::vector<>", а не обычных массивов. Следовательно, надо

      1. Почитать об этих векторах -- некие основы, а также то, как с ними работать: преобразование туда/обратно с обычными массивами, некие "итераторы" (могут понадобиться, если нормального преобразования нет и придётся поэлементно).
      2. Посмотреть методы DeviceAttribute -- что ТАМ на тему взаимодействия с обычными массивами, включая возможность инициализации из них при new.
    • 02.12.2023@утро: возможно, придётся-таки научиться запускать MySQL'ную БД, чтобы смочь проверить работу записи на TangoTest.

    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: разбираемся с чтением дальше -- ПРОРЫВ!!!

    1. Сначала сделал в cda_d_tango_req_read() поддержку и режима PULL тоже.
      • Добавлено поле hwrinfo_t.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.

      ...и вот оно стало возвращать данные!

    2. Главное: чтобы работал PUSH-режим, надо ЯВНО сделать "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 он одинаковый.

    • Шальная мысля "а может, оттуда можно добывать файловые дескрипторы, чтобы мониторить их в своём event loop'е -- примерно как ca_add_fd_registration() в libca?".

      Увы -- ничего такого не видно.

      Да и понятно, почему -- оно ведь для коммуникации использует CORBA и ZMQ, а у тех свои заморочки на эту тему.

    • Затем попробовал, в соответствии со вчерашней вечерней идеей, заменить в TangoHeartbeatProc() вызывание CheckEventsIterator() для каждого устройства на единственный вызов ApiUtil::get_asynch_replies().

      Фиг -- никакого отклика вообще.

      Но оно и понятно: ведь callback'и-то в не-PUSH-модели не регистрируются, так что и вызывать нечего.

    • Поэтому вызов read_attribute_asynch() для не-PUSH заунифицирован с PUSH.

      Помогло -- да, отклики на чтение стали, в виде вызовов attr_read(), прилетать!

    • А вот EVENT'ы -- нет.

      Поэтому пришлось-таки вернуть по-устройственные вызовы 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 сделана "асинхронность":

    1. В PUSH-режиме event'ы прилетают и так, безо всякого "set_asynch_cb_sub_model(PULL_CALLBACK)".

      Что создаёт впечатление, что оно и не нужно.

    2. А вот callback'и от ОДИНОЧНЫХ вызовов -- фиг, без того переключения режима не работают.
    3. И, "как бы наоборот" -- в PULL-режиме get_asynch_replies() отрабатывает только callback'и от ОДИНОЧНЫХ вызовов, а event'ы -- нет, их надо поллить индивидуально.
    4. И описано это всё отвратительно.

    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: только надо бы чуть более творчески:

    • Иметь не столько "флаг", сколько СОСТОЯНИЕ вроде "last known rslvstat".

      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'а.

    • В частности, всё вышеперечисленное делается из "главного" thread'а.
    • А вот из прочих мест, где тоже присутствуют try/catch -- нет.
    • И если вдруг понадобится, то нужно будет использовать тот же механизм, что в cda_d_insrv.c -- "специальные" коды HWR_VAL_*, имеющие отрицательные значения, за которыми в pipe пишется уже сам hwr-виновник.

    26.06.2024@вечер, лёжа в ванной, сделанной 10 чайниками: кстати, в TANGO ж у каждого устройства есть стандартное поле, вроде "state"; а как у НЕГО с возможностью опроса -- должон для для него запускаться поллинг или должны ли устанавливаться границы изменения? Или и так они есть? Если второе, то это поле -- как раз хорошая точка для тестирования чтения.

    27.06.2024: ну посмотрел, мда... Они и ТУТ умудрились наплодить сложностей.

    • "Поле"-то (атрибут) (чуть позже: нифига -- КОМАНДА!) называется "State".

      А есть ещё "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.".

    • Вот только оно тип имеет ОТДЕЛЬНЫЙ, НЕ-int -- Tango::DevState, код Tango::DEV_STATE.

      Сейчас-то в store_current_value() оно закомментировано, с комментом "convert to int32?"; видимо так и поступить.

    • Но вот при ЗАПИСИ-то такого типа что делать -- вводить для него отдельное CHTYPE_DEVSTATE (которое определять по имени)? Или считать, что этот атрибут readonly, а потому не париться?
    • Но главное -- это не атрибут, а КОМАНДА (с входным типом аргумента void). Так что -- увы, весь проект пока что накрывается медным тазом откладывается до реализации команд, а вопрос "надо ли запускать поллинг или устанавливать границы изменения?" просто отпадает как нерелевантный.
    • 12.07.2024: всё сложнее (но и печальнее):
      • Сложнее: и 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: краткие планы на будущее:

    1. Разобраться с векторными данными -- как запись, так и получение (как узнать "векторность"? как получить сам vector<>?).
    2. Разобраться со строками -- аналогично векторам.
    3. Убедиться, что парсинг возможных вариантов имён в CHTYPE_* сделан корректно. 04.07.2024: проверил все варианты -- да.
    4. Сделать поддержку РАБОТЫ с разными вариантами CHTYPE_*:
      1. Регистрация (и связанные с ней шаманские ритуалы).
      2. Запись (наверное, самое простое).
      3. Получение значений.
    5. 24.07.2024: Сделать поддержку команд.

    03.07.2024: ещё чуток тестирования с попыткой записи cdaclient'ом, во время которого наблюдены странности.

    • Попытка ЗАПИСИ в readonly-атрибут -- никакой реакции: ни сообщения об ошибке сразу, ни потом.
    • Перед отработкой записи почему-то ДВА вызова 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 же ровно то же самое: запрос записи просто молча игнорируется.

    • Также добавлена печать tid'а -- my_gettid() взят из /hw4cx/drivers/daqmx/pxi6363_drv.c (а НЕ call_gettid() из mt_cxscheduler.c).
    • Результат (закомменчен тут в тексте выше) просто удручающий:
      • Деятельность идёт в толпе РАЗНЫХ thread'ов.
      • Причём события приходят в разные же thread'ы: attr_read() -- в один, первый push_event() -- в другой (причём в ОСНОВНОЙ!), следующие 3 штуки "посмертных" push_event()'а -- в третий.
      • ...отдельный нюанс -- sl_break() делается аж дважды, но это уже особенность устройства cda_d_tango.cpp: оно там в event2pipe_proc() вычитывает из входного pipe'а в цикле по repcount до 100 раз, вот и генерит второе событие sl_break(), вроде как должного прервать основной цикл.

        (Эти 2 события подряд -- начальный attr_read() плюс первый push_event(), приходящий через ~20ms, поэтому они успевают оба отправиться в event-pipe ещё до того, как основной thread успеет начать вычитывание.)

      • Была добавлена диагностическая печать в cdaclient.c::ProcessDatarefEvent() после sl_break() -- "жизнь" продолжается и после него.
      • И даже в main() после sl_main_loop() -- т.е., прямо перед выходом -- даже после этого оно ещё пару секунд может продрыгаться.
      • _exit(0) приводит к мгновенному завершению.

      Похоже, объяснение в том, что эти умники используют atexit() для исполнения каких-то весьма долгоиграющих действий:

      • В tango-9.3.4/lib/cpp/client/event.cpp в конструкторе 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" -- ну да:

        #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 ()
        
        (Кстати, в этом же прогоне GDB показал, что порождается толпа thread'ов -- 7 штук.)
      • А ещё в tango-9.3.4/lib/cpp/client/devapi_base.cpp делается atexit(clean_lock), но лишь в DeviceProxy::lock(), так что не факт, что оно в нашем случае исполняется (да и длительность его исполнения неочевидна).

    09.07.2024: к вопросу "как бы всё-таки поддерживать запись в атрибуты и команды DEV_BOOL и DEV_VOID, учитывая, что в CX нет ни отдельного булевского типа, ни void":

    • BOOL:
      • Проблема в том, что если при "получении оттуда" он преобразуется в int32, то при "отправке туда" как догадаться, что надо какой-то из INT-типов вдруг преобразовывать в bool?
      • ...теоретически, можно для этого использовать знаковый CXDTYPE_INT8: в Tango ведь всё равно нет "знакового 8-битного".

        Но а если когда-то появится? Да и вообще кривоватое и неочевидное решение.

        10.07.2024: хотя идея выглядит соблазнительно потому, что INT8 -- это dtype-префикс "@b:", совпадающий с суффиксом "@b".

        10.07.2024: чуть позже -- да, и этот вариант тоже реализован.

    • VOID:
      • "Оттуда"-то он уже возвращается как int32(0), но как догадаться, что при ОТПРАВКЕ команды нужно НЕ указывать ей параметров?
    • @П28, спускаясь по лестнице с 10-го этажа вниз, по пути в ИЯФ:
      • можно ввести дополнительные опциональные @-суффиксы -- 'b' и 'v', которые бы сочетались с прочими (точнее, ПРЕФИКСИРОВАЛИ бы их).
      • Это и с давно задуманным "@s" (State-result) сочетается, "@bs" или "@vs".
    • 15.07.2024: а ведь можно так же и DEV_ENUM на запись поддерживать: ввести суффикс-префикс @e, флаг BHVR_ENUM, при наличии первого взводить второй, и уже при его взведённости сбагривать в da/dd те же данные под вывеской Tango::DevEnum.

      Начав было делать,

      • обнаружил в tango_const.h определение:
        typedef DevShort DevEnum;
      • А потом -- уже чисто для проверки -- и отсутствие у DeviceAttribute каких-либо методов с DevEnum.
      • Откуда очевидный вывод: нечего тут делать, тип этот ненастоящий.
    • 15.07.2024: чуть позже: кстати, при желании ровно так же можно сделать на запись и Tango::DevState.

      Вопрос только -- ЗАЧЕМ такое может понадобиться?

    10.07.2024: реализуем.

    • Добавляем к hwrinfo_t поле bhvr для хранения "флагов поведения".
    • И заводим собственно флаги -- BHVR_VOID, BHVR_BOOL, BHVR_STATE.
    • Парсинг несложен:
      1. Сначала делается bhvr=0.
      2. ПЕРЕД общим if()else if() добавлена отдельная проверка на 'b' или 'v', и если они, то взводится соответствующий флаг в bhvr, а в at_c берётся следующий символ (который может быть и '\0' -- это приведёт к CHTYPE_DEVATTR).
      3. В конце общей проверки добавлена альтернатива 's' -- тогда считается CHTYPE_RESULT и взводится BHVR_STATE.

    13.07.2024: появилась идея, как решить давнюю проблему-дилемму "подписываться на CHANGE_EVENT или на PERIODIC_EVENT?":

    • @утро-душ: позволить указывать этот режим ещё одним @-суффиксом-префиксом: 'g' или 'i' (chanGe и perIodic), и чтоб оно прописывалось в bhvr отдельным флагом.

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

    • @после-душа: или даже проще: пусть "@a" означает нынешнее CHANGE, а для включения PERIODIC указывать "@i".

      И парсинг менять не придётся.

    • @после обеда: и ЕЩЁ проще: унифицировать с cda_d_cx.c -- пусть по умолчанию будет PERIODIC, а CHANGE включается по "@u" -- on_Update.

      Ведь по сути это и есть полный аналог: периодические -- аналог цикла сервера, а CHANGE -- это и есть update.

    Вот по последнему варианту и делаем:

    • Вводим BHVR_ON_UPDATE, ...
    • ...по "@u" он взводится, ...
    • ...а в момент подписки при нём взведённом заказывается CHANGE, а иначе PERIODIC.

    С проверкой -- туго и слабопонятно. По косвенным признакам -- похоже, работает:

    • Нашётся атрибут ADC4x250/test/srpa/thermoSensor со значащимся поллингом 200ms.
    • И при указании 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.

      Фиг, не реагирует.

    • С горя попробовал даже вручную после создания DeviceAttribute'а da делать da->data_format=Tango::SCALAR:
      • Во-первых, это ВОЗМОЖНО -- поле даже не private!
      • Во-вторых, диагностика в cda_d_tango_snd_data() с выдачей get_data_format() показала, что оно стало =0 (SCALAR) вместо былого =3 (UNKNOWN).
      • В-третьих, это никак не изменило ситуацию.
    • Но тут обратил внимание, что при чтении начального значения показывается data_type=22 -- а 22=DEV_UCHAR!
    • И вот после указания префикса типа "@+b" запись заработала!
    • Аналогично начальное значение атрибута ADC4x250/test/srpa/numOfSamples показало data_type=7 -- 7=DEV_ULONG.

      И запись значения при указании префикса типа "@+i" также сработала!

    • ...а вот БЕЗ '+' -- нет, НЕ работает.
    • После чего я убрал те присвоения da->data_format=Tango::SCALAR -- а запись всё равно продолжила работать!

      ...откуда вопрос: как эта хренотень вообще функционирует? Как оно при НЕуказанном формате понимает, какого рода значение ей приходит? Причём понимать это надо ещё на стороне КЛИЕНТА, для правильной отсылки.

      Возможно, этот data_format -- чисто информационный для клиента, а реальная информация о формате хранится где-то внутри.

    ВЫВОД: надо в Cdr_treeproc.c добавить поддержку парсинга "@+", чтоб он добавлял CXDTYPE_USGN_MASK к типу. Часом позже: вроде сделано.

    23.07.2024: проверяем работу режима "@u"/BHVR_ON_UPDATE, приводящего к Tango::CHANGE_EVENT -- да, работает.

    • Проверено при тестировании связи с VMEADCx32, у которого каналы векторных измерений, похоже, функционируют в режиме "проталкивать CHANGE_EVENT при любом обновлении, БЕЗО всяких границ (abs_change or rel_change)" -- ...
    • ... -- обновления прилетают, безо всякой предварительной ругани.
    • @вечер, засыпая: раз уж сделано "ON_UPDATE" по "@u" -- может, пойти дальше по тому же пути, что в cda_d_cx.c, и реализовать то же поведение и при наличии CDA_DATAREF_OPT_ON_UPDATE?
    • 24.07.2024@утро-завтрак: сделано, довольно тривиально -- при инициализации значения bhvr перед парсингом суффиксов в него помещается не 0, а в зависимости от наличия того флажка.

      Возможности указать на уровне имён "НЕ по UPDATE" не делаем, полностью аналогично cda_d_cx.c оставляя это на откуп cdaclient'овому префиксу "@~".

    25.07.2024: проверена пригодность нынешнего cda_d_tango.c для epics2cda -- да, пригоден!

    Мини-протокол:

    • На pult-l скачан base-3.15.9.tar.gz и распакован в ~/compile/ и собран простой командой "make".
    • Затем собрано frgn4cx/gw/ командой
      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
      
    • После чего проверка
      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
      
      (в конце у векторного значения 50 штук нулей, т.к. указано "@s100", но измерений от VMEADCx32 приходит 50 штук.

    1x.0x.2024:

  • 20.06.2024: к вопросу о "что делать с не-скалярными данными, учитывая, что TANGO сплошь полагается на 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: роем детали.

    • Насчёт сбагривания данных ТУДА: гугление по "std::vector constructor c array" дало следующее:
      1. "C++ Vector - How to Initialize a Vector in a Constructor in C++" среди прочего предлагает вот такое:
        int myArray[] = { 5, 10, 15 };
            
        vector<int> myVector(begin(myArray), end(myArray));
        
        -- это некие методы векторов.

        (Как они могут работать, учитывая, что оный myArray -- сторонняя для вектора сущность и методы НЕ ЕГО, мне неясно; разве что какие-нибудь хитрые макросы или нечто подобное?)

      2. "How to Initialize a Vector in C++ in 6 Different Ways" в подразделе "Using an Existing Array" предлагает общий синтаксис:
        data_type array_name[n] = {1,2,3};
        
        vector<data_type> vector_name(arr, arr + n)
        
        и конкретный пример --
        int a1[5] = {10, 20, 30, 40, 50}
        
        vector<int> v1(a1, a1 + 5)
        
        (точки-с-запятыми явно забыты); тут используются некие "итераторы", по которым предлагается отдельная страница "Iterators in C++: An Ultimate Guide to Iterators".
    • Насчёт забирания данных ОТТУДА себе: ну должно же быть какое-то обратное действие -- какое-нибудь "copyout"?

      Гугление по "std::vector to array" дало ещё более простые и понятные рецепты:

      1. "c++ - How to convert vector to array" на StackOverflow от 2010 года предлагает варианты:
        • Просто получить указатель на первый элемент внутреннего массива:
          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);
          
      2. "Different Ways to Convert Vector to Array in C++ STL" (Last Updated : 23 Dec, 2023) с этим согласна.

    25.06.2024: а ещё ведь надо будет уметь узнавать количество элементов в векторе -- это нужно при ПОЛУЧЕНИИ данных, т.е., в store_current_value().

    • Можно ли это делать средствами DeviceAttribute -- надо посмотреть.
    • А средствами вектора -- кажется, там есть метод 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 */ };
    • Но есть неясность: в devapi_attr.cpp в конструкторах, инициализирующих по переданным данным, этому полю повсеместно присваивается значение Tango::FMT_UNKNOWN -- и в скалярных, и в векторных...
    • OK, попробовал добавить диагностическую печать -- просто значение, которое вернёт этот get_data_format() -- у атрибутов, полученных от сервера и от намисозданных, в store_current_value() и cda_d_tango_snd_data() соответственно. Результат:
      store_current_value get_data_format()=0
      cda_d_tango_snd_data da->get_data_format()=3
      
      -- т.е., SCALAR и FMT_UNKNOWN соответственно.

      Т.е., СЕРВЕР-то присылает вроде осмысленное (но надо ещё на реальном векторе проверить), а вот конструкторы действительно нихрена не делают...

    • Откуда вопрос: а у нас ЗАПИСЬ точно ли работает?

      А то неясно, как вообще делается передача значений далее в сторону сервера -- раз в поле data_format бессмысленное значение FMT_UNKNOWN, то КАК оно определяет тип данных?

    28.06.2024: приблизительный сценарий дальнейших действий для поддержки векторов:

    1. Отправка.
    2. Приём.
    3. Удалить запрет "только скаляры!".

    28.06.2024: приступаем потихоньку. Начинаем с ПРИЁМА в store_current_value().

    • Сделано вычитывание
      data_format = attr_value->get_data_format();
      -- причём окружённое try/catch'ем, т.к. там может генериться exception из-за хрени под названием "unknown_format_flag"; правда, взведения его я не нашёл, но мало ли (вдруг я чего недопонимаю, или в будущем появится).
    • Дальше добавлен "if()/else if()" по её значению. Имевшееся ранее помещено в альтернативу SCALAR; альтернатива SPECTRUM пока пуста и туда будем делать векторность, а что делать с IMAGE, которое 2D, пока неясно: может, разворачивать матрицу в вектор?
    • Остался вопрос: а собственно ПОЛУЧЕНИЕ-то векторных данных из DeviceAttribute как -- какими методами?

    29.06.2024: продолжаем

    • Чуть не рехнулся, пытаясь полдня найти что-то логичное. Ибо всяких разных методов там -- тьма.
    • И в конце концов оказалось, что делается это теми же самыми операторами >>, что и со скалярами, только параметром "куда складывать" передаётся vector<T>& datum -- т.е., так же по ссылке.
    • Но главный прикол -- КАК эти операторы устроены внутри. Например, вот как в tango-9.3.4/lib/cpp/client/devapi_attr.cpp в 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;
      }
      

      Т.е.,

      1. Копирование делается -- та-дам! -- ПОЭЛЕМЕНТНО!!!
      2. Массивы длиной 0, очевидно, у них за данные не признаются.
    • С массивами нулевой длины получается "проблема курицы и яйца":
      • забирать массивы длины 0 нельзя;
      • но узнать, что длина 0 -- тоже нельзя: ведь метода "сколько элементов?" у DeviceAttribute не видно (авот и нет -- 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: продолжаем.

    • Перво-наперво решаем давнюю проблему "а может ведь оказаться, что нам сбагривают данные не того типа, с которым зарегистрирован ref и для которого аллокирован буфер 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.
    • Собственно складирование "метаданных" dtype/nelems/rflags/timestamp вытащено из SCALAR в отдельную секцию в конце, под меткой "STORE_METADATA" -- чтоб не страдать дублированием.

    На этом с приёмом вроде бы всё.

    01.07.2024: допиливаем -- делаем ОТПРАВКУ.

    • Добавлен "if()/else" по значению nelems, в альтернативу "nelems==1" перенесена уже имевшаяся работа со скалярами, а в "else" работаем с векторами.
    • Заводим ОТДЕЛЬНЫЕ указатели на объекты-векторы
      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.
    • Ну и за-#if0'иваем ограничения "только скаляры!" из 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 -- результаты прекрасные:

      1. Благодаря тому, что при получении и вычитывании из DeviceAttribute мы ориентируемся на ИСХОДНЫЙ тип, получаемый от Tango (а не на 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() при сохранении в целые).
      2. Проверена и работа невылезания за пределы аллокированного буфера: при том, что длина осциллограммы составляет 200 чисел и её тип 5=DEV_DOUBLE, при префиксе @d300 возвращались все 200 чисел, при @i300 -- 150, а при @h300 -- 75. Т.е., сколько байт аллокировалось (d300:300*8, i300:300*4, h300:300*2), в столько и урезался результат.

    14.07.2024: к вопросу о возврате векторных вариантов всяких странных типов, вроде bool и DevState :

    • @дорога с П28 по магазинам, проходя лесок, мимо НИПСа и ИПА, ~16:00: можно делать поэлементную конверсию: просто в цикле читать кусочек исходного размере, а писать в кусочек размера-получателя.

      И оформить это в отдельную функцию "convert_INT_array()", которой передавать (src, src_usize, dst, dst_usize).

      И тогда при src_usize==dst_usize она может просто делать memcpy().

      ...но тут неудобство в том, что в store_current_value() складирование делается в одной точке, получающей лишь dtype и НЕ предполагающей какой-то конверсии. Чё, делать конверсию принудительно, введя обязательный src_usize (который у обычных типов совпадёт с sizeof_cxdtype(dtype) и тем самым "конверсия" сведётся к тому же memcpy()...)?

    • @дома, вечер, ~18:00: а можно поступить тупее и проще: устройство cxdtype_t ведь таково, что ему его можно сконструировать как
      ENCODE_DTYPE(sizeof(SOMETHING), CXDTYPE_REPR_INT, 1)
      -- ну так и указывать таким способом именно этот РЕАЛЬНЫЙ ИСХОДНЫЙ тип, а уж cda пусть сама производит надлежащую конверсию в "локальный" тип.

    Мысли в эту сторону полезли после того, как в "Список атрибутов и команд устройства RFCavity" обнаружились атрибуты вроде servoLimitSwitch1, имеющие тип "bool[2]".

    ...и да, для Chl-скринов всё равно останется вопрос "а как из вектора выцепить 1 элемент-скаляр для отображения".

    15.07.2024: делаем, по второму варианту -- естественно, только для атрибутов (но не для команд), в store_current_value():

    • DEV_UCHAR: тривиально -- т.к. это просто UINT8, то возвращается как прочие векторы, через свежевведённый v_uint8.

      Заодно и запись сделана в cda_d_tango_snd_data().

    • DEV_STATE: сделано по проекту: забирается в v_DevState, а потом делается
      dtype = ENCODE_DTYPE(sizeof(Tango::DevState), CXDTYPE_REPR_INT, 1);
    • DEV_BOOLEAN: авотфиг! Ошибка "error: void value not ignored as it ought to be" на конструкцию "data_ptr = v_bool.data();".

      Гугление по "vector bool data":

      • Первым же результатом дало "What happens when you call data() on a std::vector<bool>?" на StackOverflow за 15-05-2013, где с кучей ссылок говорится, что
        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.

        Ну и я тоже сообразил: скорее всего, "массивы" булевских оптимизации ради хранятся БИТОВЫМИ цепочками, так что "указатель на данные" становится лишён смысла.

      • Третьим (на него ссылка из выше тоже есть) -- std::vector<bool>" на cplusplus.com, где битым текстом сказано (bold мой):
        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" -- фиг.
    • Одна загвоздка: при "отсутствующем поллинге" -- "The polling (necessary to send events) for the attribute wfvoltage0a is not started" -- обновлённый результат НЕ присылается.

      Уведомление о выполнении -- да, метод attr_written() дёргается, но более ничего.

      Ну не заказывать же обратное чтение из attr_written()?

    2x.07.2024:

  • 01.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) -- что это вектор символов, а не скаляр -- реализовывать чтение и запись надо ОТДЕЛЬНО от прочего, для чего проверка на него должна стоять первой, ещё ДО проверки на скалярность/векторность.

    • Запись: генерить строку такой длины.
    • Чтение: считывать String и возвращать CXDTYPE_TEXT длиной "длина строки".

    @вечер: вот только где при записи/отправке брать терминирующий 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: реализовываем.

    1. Сначала ОТПРАВКУ, в cda_d_tango_snd_data() -- по сегодня-утреннему проекту, реализация скорее похожа на работу с векторами:
      • Альтернатива "if (dtype == CXDTYPE_TEXT)" поставлена в начало, ещё ДО "if (nelems == 1)".

        Тут были некоторые сомнения -- "а как же поддержка СИМВОЛЬНЫХ скаляров?". Но если считать, что "символьные скаляры" -- это тип DEV_UCHAR, то в него правильнее отображать CXDTYPE_UINT8 (заодно и векторы оных будут поддерживаться).

      • Заведён char *string_buf=NULL, ...
      • ...приравненный по статусу к указателям на векторы v_*_p -- в секции CLEANUP_VECTORS_EIO он подчищается.
      • Он делается =malloc(nelems+1), туда копируются данные и прописывается терминирующий NUL.
      • Сразу после успешного создания DeviceAttribute'а делается free() и =NULL -- ровно как с векторами; при неуспешности из catch() делается тот же переход к CLEANUP_VECTORS_EIO.
    2. Потом ПРИЁМ, в 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.
      • ЗАМЕЧАНИЕ: по сути, эти махинации -- с аллокированием +1 символа на NUL и его прописыванием после данных -- для САМОГО CX нафиг не нужны, т.к. там всё работает по длине, а полезны скорее для диагностической печати, если таковая когда-нибудь потребуется.
    3. Добавляем 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':

    • При передаче TANGO->CX складывать все элементы вектора в один буфер, разделяя их NUL'ами (которые там и так как раз будут, в роли терминаторов).

      Но конкретно ПОСЛЕДНИЙ -- не нужно учитывать в возвращаемой длине (nelems); таким образом получим в "скалярном" варианте ровно то, что сейчас.

    • При передаче CX->TANGO проходиться по строке в поисках NUL'ов посредством memchr()

      А чтобы не добавлялась лишняя пустая строка -- если исходник, например, 'a','b','c','\0' -- надо вначале проверять, что если в конце есть '\0', то уменьшать nelems на 1:

      if (nelems > 0 && ((char*)value)[nelems-1] == '\0') nelems--;

      @вечер: не удержался и конкретно эту строчку добавил.

    • ЗАМЕЧАНИЕ: ну да, будет косяк в случае, если последняя строка окажется пустой: триплет "abc\0def\0" превратится в дуплет "abc\0def", но тут уж ничего не поделаешь -- это обычная проблема текстовых файлов, "нужен ли перевод строки в конце последней строки?". Но ОСМЫСЛЕННОЕ появление таких данных -- с реальной пустой строке в последней позиции -- в системе управления крайне маловероятно.

    Как раз вот-вот в epics2tango_gw.cpp работа с векторами строк будет сделана, так что появится практический опыт плюс пример, вот после этого можно и тут сделать в вышеописанном виде.

    0x.09.2024:

  • 04.07.2024: и ещё раздел для реализации поддержки команд -- то, что предполагалось связать с CHTYPE_COMMAND и CHTYPE_RESULT.

    Тут стоят следующие задачи:

    1. Узнать, КАК с этой хренью работать: какой API, какие концепции (в т.ч., что там с асинхронностью).
    2. Придумать архитектуру: делать ли "дуплетом связанных hwr'ов (1-й на инициирование команды, 2-й на получение результата)" или как-то иначе.

      Тут нюанс в работе с типами, т.к.:

      • Входной и выходной типы могут быть разными.
      • В т.ч. любой из них может быть и void (например, State -- это по сути функция без параметров).
      • В т.ч. и ОБА могут быть void (всякие ВКЛ/ВЫКЛ раздельными командами -- не факт, что они возвращают результат).
    3. Собственно реализовать.

    05.07.2024: некоторые соображения насчёт "дуплетом связанных hwr'ов":

    • Если делать пару, то надо в каждой части пары прописывать hwr второй половинки.
    • @по дороге из дома в девятиэтажку, около Пирогова-18а/ОбщN9: теоретически клиент может попробовать зарегистрировать одну из сторон ДВА раза с РАЗНЫМИ dtype/nelems, и тогда cda_core это позволит, в результате чего получится м-м-м... неприятность.

      Так вот: надо ЗАПРЕЩАТЬ подобное прямо в cda_d_tango_new_chan().

    • @вечер, после поиска этих чёртовых command_inout_*(): 03-06-2023 на Беленца-6-23 был придуман другой вариант -- ОТДЕЛЬНЫЙ SLOTARRAY cmds_list, а из hwr-ячеек команд и результатов делать туда ссылки.

      Тот вариант был бы лишён проблемы "нельзя несколько ссылок на одну команду" (т.к. эти ссылки регистрировали бы свои "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() для получения информации).

      Т.е.,

      1. Есть понятие "поллинг команд" (видимо, чтоб сервер его делал -- например, чтоб тот же State не дёргать).
      2. Оным поллингом можно управлять из КЛИЕНТА?!

      С каким АРГУМЕНТОМ будет вызываться поллируемая команда -- не сказано.

    • ...и, что любопытно -- аналогичный API есть и для атрибутов!

      Что -- таки можно ВКЛЮЧАТЬ ПОЛЛИНГ С КЛИЕНТА простым вызовом 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@вчера-вечер засыпая, сегодня утром просыпаясь, утром в душе: размышления по архитектуре реализации:

    1. Замечания по общей схеме реализации работы с командами, представляемыми каналами, чтобы минимизировать лишние действия и неудобства.
      • Для команд "чтения состояния" (вроде той State) -- т.е., принимающих void, а возвращающих "что-то": можно регистрировать ТОЛЬКО RESULT-канал, но чтобы исполнение команды могло вызываться
        1. Вызовом метода req_read() -- т.е., посредством cda_req_ref_read().
        2. Записью в этот же канал (пофиг, чего).
      • Для команд "управления" -- всякие SwitchOn, SwitchOff и подобных -- т.е., принимающих что-то (или даже void): можно регистрировать ТОЛЬКО COMMAND-канал, поскольку результат никому не интересен (и даже может быть void), а точнее, он выражается в функционировании других каналов.
      • Но чтобы было некое "подтверждение записи", для COMMAND-каналов (хотя бы имеющих пару) можно просто сразу возвращать значение 0 того типа, которым они были зарегистрированы.
      • @вечер, засыпая (и утро 07.07.2024, просыпаясь и душ): но даже так получается не очень удобно опрашивать каналы типа того же State из cdaclient:
        • поскольку просто при создании командного канала нельзя запрашивать исполнение команды (а то бы всякие SwitchOn/SwitchOff сразу исполнялись), ...
        • ...а выполнить req_read() попросту некому, ...
        • ...то придётся использовать кривоватые дуплеты вроде
          State@r State@c=1
          или
          State@r State@r=1

        А хотелось бы всё-таки иметь возможность указывать просто ОДНО имя, как канала.

        • Предусмотреть ещё один вид суффикса, например, "@s" -- чтоб на такие каналы всё же делался принудительный запрос исполнения при регистрации?
    2. Насчёт типов -- а конкретно что делать с void (как принимаемым, так и отправляемым):
      • В CX-то нативного void нет -- вот не требовался никогда, обычно когда "значение неважно" использовался int32.
      • Формально за void можно считать ЛЮБОЙ тип при nelems=0.
      • Если ПОЛУЧАЕМ тип void, то в CHTYPE_RESULT-каналах можно отдавать наверх (int32)0.

    06.07.2024: приступаем.

    • Сначала инфраструктурное: к hwrinfo_t добавляем поле cmd_other_side, в котором будет прописываться идентификатор "парного" hwr'а. Инфраструктурные действия с ним:
      • При инициализации делается =-1, ...
      • ...а в RlsHwrSlot() смотрится, что если >0, то "там" у пары этому полю делается =-1 (хотя и своему тоже).
    • Далее заводим собственно callback.
      • Метод cda_d_tango_EventCallBack::cmd_ended() (прототип описан в devasyn.h), ...
      • ...получает параметр CmdDoneEvent* -- этот тип описан в там же.
      • Скелет берём от соседней push_event().
      • Его действия полностью идентичны исходнику, только для складирования вызывается store_current_valueD() (и параметр ей передаётся чуть иной).
      • ЗЫ: сначала была мысль в самом начале сделать проверку "если это CHTYPE_COMMAND и cmd_other_side>0, то переключиться на hi пары и далее работать там".

        Но далее по коду используется 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.

      • И чё делать?
        • ПОТЕНЦИАЛЬНО можно из этого типа добывать информацию: например, по type==DEVVAR_DOUBLEARRAY делать type=DEV_DOUBLE и format=SPECTRUM.

          Чуть позже: неа, там в DeviceData.h есть комментарий про "native TANGO CORBA sequence types", из которого следует, что эти DEVVAR* -- они как раз и есть те самые "CORBA sequence types".

        • Но СЕЙЧАС, учитывая, что основной набор команд работает со скалярами, можно просто забить и всё, что не скаляры -- считать "не поддерживаем".
      • Ещё проблема в ту же степь: не видно никакого способа узнать РАЗМЕР значения -- dims, dimensions, ...

        Чё, пытаться лезть напрямую в поле CORBA::Any_var any -- раз уж оно public?

        Неа -- лучше правда забить и пока ограничиться поддержкой скаляров.

      • ...а ещё у него НЕТ timestamp'а.

        Возможно, аргументация "а какой timestamp у результата команды" (утрирую, конечно; но, скорее всего, эти олухи просто не подумали или забыли).

        Ну тут хоть всё просто: взять текущее время от gettimeofday(), да и всё.

      Реализация:

      • Код-то копируем из store_current_value(), но в несколько упрощённом виде -- примерно том, что был ДО поддержки векторов. ...неа, просто с отрезанным SPECTRUM.
      • Для начала в store_current_value() добавлена "поддержка" DEV_VOID: в этом случае возвращается значение 0 типа CXDTYPE_INT32.
      • Затем скопировано содержимое веток DEV_STRING и SCALAR (которая стала просто "else"), с заменой "attr_value" на "data_value".
      • А timestamp берётся из gettimeofday().
    • cda_d_tango_req_read():
      • Введен селектор по значению in_use, ранее имевшееся засунуто в альтернативу DEVATTR.
      • В ветку COMMAND||RESULT сделана почти копия имевшегося, только вместо read_attribute_asynch() вызываются command_inout_asynch().
      • ...и да -- при не-callback-варианте ID транзакции складывается в тот же read_call_id.

        ...но вызов поллинга пока не сделан.

        07.07.2024: вроде как добавлено. С непонятным качеством.

      • Также сделано, что если "этот" hwr -- COMMAND, у которого определена пара в 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'ов.

      Реализация:

      • Для COMMAND- и RESULT-каналов cda_d_tango_snd_data() "переадресует" запрос нововведённой коллеге --
      • snd_data_via_command() -- название имеет ту же длину и тоже содержит строку "snd_data".
      • Внутренности сделаны копированием cda_d_tango_snd_data() с очевидными изменениями:
        1. Передаётся сразу готовый dpxinfo_t *di.
        2. Тут "экземпляр объекта" Tango::DeviceData dd вместо указателя Tango::DeviceAttribute *da -- в соответствии с соображениями выше.
        3. Соответственно, "запихивание" значения в объект для отправки сменилось на "dd << ..." вместо "da = new Tango::DeviceAttribute(...)".
        4. И, естественно, никакой подчистки объекта-значения посредством delete не требуется.
      • Плюс,
        1. В начале отсутствуют проверки вроде "живо ли устройство", т.к. они уже сделаны переадресанткой.
        2. Аналогично cda_d_tango_req_read() сделано, что если "этот" hwr -- COMMAND, у которого определена пара в cmd_other_side, то запрос шлётся «от имени» той стороны (т.е., RESULT'а).
    • cda_d_tango_new_chan(): во-первых, запрет "всего, кроме CHATTR" заменён на запрет только DEVPROP и ATTRPROP, а во-вторых, добавлен поиск "пары" и "дубля" (во втором случае -- облом по EBUSY) посредством итератора CommandCheckIterator().
    • ПРОВЕРИТЬ ВСЕ МЕСТА, где что-то может делаться безусловно, и везде поставить проверки по значению in_use

    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().
      • ...сделано. Работать всё равно не начало.
      • И тут осенило: а ведь уже сам cdaclient для исполнения записи должен получить какое-то "текущее" значение -- как подтверждение, что канал "жив".

        Отключить это поведение можно ключом -w, что и было сделано -- да, заработало!

        И сразу проверено, что только с ключом -w, но без FOUND+ready, работать не будет -- да, после закомментировывания не работает.

      • Вывод: надо для командных каналов не только "разрезолвлен, готов" делать, но и возвращать какое-нибудь (очевидно, нулевое с nelems=1) значение.
      • OK -- это сделано (причём для CXDTYPE_TEXT возвращается nelems=1 -- пустая строка) и всё вытащено в отдельную report_command_found(), которая и вызывается из тех 2 точек.
      • Попробовал -- фиг, cdaclient'у не полегчало.

        Посидев несколько часов, всё же разобрался: дело в устройстве самого cdaclient, который в итераторе ActivateChannel() сначала регистрирует канал БЕЗ evproc'а, а уже потом, при успехе, делает cda_add_dataref_evproc(). Вот он и не получал событие, прилетающее прямо в момент регистрации.

      OK, засим считаем, что со стороны dat-плагина обязанности по оповещению о состоянии командных каналов соблюдены, а с cdaclient'ом будем разбираться отдельно.

    • Также проверено, что
      1. "Дубли" находятся и запрещаются.

        (Точнее, доделано -- там были косяки со сравнением (не-совпадение имени считалось при strcasecmp()==0 вместо правильного !=0) и с указанием самого имени -- в cmdinfo.name писалось dup_name вместо надлежащего sl3+1, как в сам chn_name пишется.)

      2. CHTYPE_COMMAND-каналы при наличии "пары" отправляют запрос от её имени, поэтому ответ приходит именно парному CHTYPE_RESULT-каналу.

        ...хотя можно и писать в тот же @r-канал, и читать из @c-канала -- это тоже работает и проверено cdaclient'ом.

        По сути, ДВА разных канала требуются, если важен тип АРГУМЕНТА и он отличается от типа РЕЗУЛЬТАТА; иначе же можно регистрировать любой из COMMAND/RESULT.

    Итого: работа с командами по первому впечатлению работает как задумано.

    10.07.2024: делаем поддержку "state-style"-команд -- чтоб первое исполнение производилось сразу при регистрации, имитируя начальное чтение.

    Эта работа стала ключевой причиной для расширения базовой инфраструктуры -- введения поля bhvr (подробнее см. выше за сегодня).

    • Суффикс "@s" делает chtype=CHTYPE_RESULT, но взводит 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 был под-твикнут для умения отрабатывать события прямо в момент регистрации канала (ещё ДО завершения оной регистрации), проводим ещё тесты.

    Вкратце -- всё грустно.

    • При указании "name@r name@c=1" имеем фигню: тот самый "возврат нулевого значения сразу же" то и делает -- возвращается нулевое значение (или пустая строка), и на этом всё.

      Если указать ключ "-m" -- тогда да, и реальное значение показывается (но ценой "зависания навсегда").

    • Но и так "зависает навсегда" -- потому, что командный канал (который "@c") шлёт запрос "от имени" парного ему RESULT-канала, в результате чего сам командный канал обновления никогда не получает и так и висит в состоянии .wr_snt=1, так что так никогда не сделается num2write-- и оно так навсегда и останется >0, так что условие завершения никогда не исполнится.

    Получается, что эта модель "отображения Tango-команд на CX-каналы" слабосовместима с cdaclient.

    Всё, что приходит в голову -- это:

    1. НЕ возвращать RESULT-каналам сразу же при регистрации фейковое нулевое "начальное значение";
    2. при записи команды в канал сразу же давать "подтверждение" путём возврата опять же фейкового нулевого значения.

    Проблема в том, что такие изменения сделают невозможной работу "через один канал вместо пары": если "подтверждение" при записи ещё можно исполнять условно (при наличии пары), то невозврат "начального значения" условным сделать нельзя, т.к. в момент регистрации RESULT-канала парный ему COMMAND-канал может быть ЕЩЁ НЕ зарегистрирован (ибо будет позже).

    ...разве что:

    • ввести правило, что ОДИНОЧНЫМ каналом может быть только COMMAND-канал --
    • -- тогда проблема "а если ЕЩЁ НЕ зарегистрирован?" исчезнет, т.к. ...
    • ...НЕвозврат "начального значения" будет только у RESULT-каналов, а COMMAND'ам пусть вертается всегда.
    • Да, это в report_command_found() придётся вставить проверку -- фейковое "начальное значение" возвращать ТОЛЬКО при CHTYPE_COMMAND.

    12.07.2024: модифицируем по вчерашнему проекту.

    1. report_command_found() делает фейковое обновление ТОЛЬКО при CHTYPE_COMMAND.
    2. snd_data_via_command() делает точно такое же обновление ТОЛЬКО при CHTYPE_COMMAND и при этом с обязательно наличествующей RESULT-парой (hi->cmd_other_side > 0).

    Попробовал -- авотхрен!

    • И отладочная печать сразу показала причину: команда-то при ПАРНЫХ каналах посылается от имени RESULT-канала!

      Поэтому проверка "CHTYPE_COMMAND с парой" не срабатывает.

    • Кстати, оказалось, что у этого парного RESULT-канала cmd_other_side не прописано.

      Краткий осмотр показал, что оного прописывания "у парного канала" просто нигде нет.

      Добавлено в cda_d_tango_new_chan() сразу после прописывания своего.

    • Ещё когда только вылезла проблема, возникла мысля: правильнее ведь "фейковое обновление" для COMMAND-канала генерить не при отправке, а именно в момент получения ОТВЕТА.
    • Делаем именно так: в 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
      
    • OK, запускаем под gdb и делаем "bt" (bold мой, для выделения причины):
      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".

    • OK, на всякий случай и в 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.

    • UINT8/UCHAR: ну он DeviceData'й как раз только векторный и поддерживается.

      Тут копирование из аналога для атрибутов простое и прямолинейное.

    • INT8/bool/"@b": он векторный в cda_d_tango_snd_data() был добавлен уже позже, 15-07-2024.

      Вот здесь оказалось чуть сложнее.

      • Ветка "CXDTYPE_INT8 или BHVR_BOOL" перед основным switch (dtype) была скопирована опять же просто и прямолинейно.
      • Вот только компилироваться оно не стало -- GCC ругался
        cda_d_tango.cpp:1807:20: error: no match for 'operator<<' (operand types are 'Tango::DeviceData' and 'std::vector<bool>')
                         dd << *v_bool_p;
                            ^
        
      • Оказалось, что в 9.2.5a этого оператора действительно почему-то нет, а в 9.3.4 уже появился.
      • Так что пришлось ветку "создаём и используем 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
        

      Теперь компилируется с обоими вариантами.

    Конечно, проверить пока что просто не на чем.

    Кстати,

    1. При просмотре исходников операторов << и >> возникло впечатление, что эти DevVar*Array/DEVVAR_nnnARRAY -- это и есть те типы, через которые передаются массивы:
      • operator<<(vector<>) именно такое генерят при запихивании данных.
      • А operator<<(vector<>&) при извлечении именно из таких данные и достают.

      Соответственно,

      • Идея от 06-07-2024 "например, по type==DEVVAR_DOUBLEARRAY делать type=DEV_DOUBLE и format=SPECTRUM" не такая уж и дурная, а очень даже здравая.
      • Но всё же есть проблема: КОЛИЧЕСТВО элементов массива предварительно -- узнать так и нельзя нету API. Только сначала забрать вектор (потенциально гигантского размера), и лишь потом из него узнать объём.

      РЕЗЮМЕ: учитывая, что практическая потребность в векторных результатах команд практически отсутствует, проще на данный аспект забить. Ну нет, ну и ладно.

    2. @вечер, засыпая: кстати, а ведь надо для каналов-команд отключать fresh age, возвращая 0s0ns. Хотя бы для того, чтобы кнопки в Chl-скринах не горели гусиным.
      27.07.2024: сделано, сразу после 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:

  • 04.07.2024: и ещё раздел касательно реализации поддержки свойств устройств (device properties) -- CHTYPE_DEVPROP.

    Тут стоят следующие задачи:

    1. Узнать, как с ними работать: как оно на уровне API, есть ли поддержка асинхронности, есть ли возможность мониторирования.
    2. Собственно реализовать.

    04.07.2024: изучаем. Грустно всё.

    • API определён в DeviceProxy.h, он весьма тривиален.
    • Никакой ни асинхронности там нет, ни мониторирования.
    • Только тупые операции get_property() для чтения и put_property() для записи (плюс их вариации для списка множественных пропертей), get_property_list() для получения списка пропертей (первым параметром const string &filter, в котором можно указывать '*' в количестве не более 1шт), а также методы для удаления (!) delete_property().
    • Краеугольным камнем этого API служит тип 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:

  • 04.07.2024: и ещё раздел по реализации поддержки свойств атрибутов (attribute properties) -- CHTYPE_ATTRPROP.

    Тут стоят следующие задачи:

    1. Узнать, как с ними работать: как устроен API, что с асинхронностью и возможностью мониторирования, и какие отличия от API свойств устройств (device properties, CHTYPE_DEVPROP).
    2. Собственно реализовать.

    04.07.2024: смотрим. Всё печально идентично DEVPROP'ам.

    • API определяется в AttributeProxy.h, он практически идентичен таковому в DeviceProxy.
    • Асинхронности нет, мониторирования нет.
    • Практически тот же набор операций, тоже на основе DbData, только get_property_list() отсутствует.

    Желания делать поддержку ЭТОГО столь же мало, как и для DEVPROP'ов. Сделать можно в ровно тех же границах -- только однократное чтение и запись, с принудительным однократным чтением при регистрации. Но отсутствие асинхронности и тут ставит возможность реализации под ба-а-альшой вопрос.

    0x.07.2024:

  • 07.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, но это требование зарыто где-то в стандарте и насколько оное гарантируется -- не знаю).
    • Там явно были поползновения гарантии размера, но они в tango.h закомментированы в виде
      /*, __max_DevState=0xffffffff */
    • Кстати, значение 0 там -- ON! Не UNKNOWN (то 13) и даже не OFF (то вообще 1).

      Ну прямо ума палата -- так "удачно" выбрать значения...

    Делаем:

    • Учитывая, что нужно в первую очередь для собственно State, который в векторном виде особого смысла не имеет, ...
    • ...делаем поддержку скаляров. 15.07.2024: и чтение атрибутов-векторов тоже добавлено, извратным образом с использованием ENCODE_DTYPE(). Нафига только...
    • Причём только чтения (смысл записи вообще неясен).
    • Для этого в обеих store_current_value*() вводим локальную переменную a_DevState, ...
    • ...в которую делаем чтение из DeviceAttribute/DeviceData посредством >>, после чего
      *(( int32*)value_p) = a_DevState;

    Проверяем, пытаясь вычитать атрибут State первого попавшегося устройства -- да, работает! Читается число 7 (это код STANDBY).

    На этом ставим "done".

  • 09.07.2024: к вопросу об остальных не совсем стандартных типах -- DEV_VOID, DEV_UCHAR с DEV_BOOLEAN и загадочным DEV_INT: как минимум BOOLEAN не помешает поддерживать, так что разбираемся, что же это такое.

    09.07.2024: итак:

    • VOID:
      • Слово "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 в каком-либо варианте.
    • UCHAR: в tango-9.3.4/lib/cpp/server/idl/tango.h (исходник в lib/idl/tango.idl) определён как
      typedef ::CORBA::Octet DevUChar;

      Причём:

      • У DeviceAttribute (и у DbDatum) ЕСТЬ операторы << и >> с параметром unsigned char и unsigned char & соответственно, ...
      • ...а вот у DeviceData -- НЕТ (есть только для vector<unsigned char> -- хбз зачем).
    • BOOLEAN: там же, где DevUChar --
      typedef ::CORBA::Boolean DevBoolean;

      И операторы << и >> для типа bool есть у всей троицы классов-для-данных.

    • INT: хрень какая-то.
      • "grep -rw DevInt compile/tango-9.3.4" находит лишь пару упоминаний в документации, ...
      • ...но никаких ОПРЕДЕЛЕНИЙ нигде НЕТ.
      • И в PDF'е "Tango Controls DocumentationRelease 9.3.4" (на стр.368=PDF.372) он тоже есть (видимо, PDF собран из тех же источников).
      • Что добавляет неясности -- сам Andy Gotz где-то в районе 2018-го (как я "люблю" датировки в их форуме! ...причём в HTML-тэгах реальные даты есть -- "2017-08-11T13:05:32.754382+00:00") в теме "DevUInt support in PyTango" заявил "DevInt and DevUInt are supported in C++".

        ...хотя и реального DevInt не видно, а строка "DevUInt" вообще нигде не встречается.

      • 11.07.2024: Саша Сенченко ответил на позавчерашнее письмо:
        DevInt - это какой-то рудимент который нереализован был. В 9.5 он удален. Можешь его игнорировать.

    Резюме:

    1. DEV_VOID с 06-07-2024 уже поддерживается на чтение, возвращая значение 0 типа CXDTYPE_INT32; а вот как бы на "ЗАПИСЬ" поддерживать?
    2. Надо поддерживать DEV_UCHAR для атрибутов (но НЕ для команд!), сопоставляя его с CXDTYPE_UINT8.
    3. Поддерживать ЧТЕНИЕ скалярного DEV_BOOLEAN и у атрибутов, и у команд,

      (Запись фиг сделаешь: INT32-то маппируется на DEV_LONG.)

    4. DEV_INT -- забыть о его существовании.

    10.07.2024: делаем, совместно с дополнениями в базовую инфраструктуру -- полем bhvr и опциональными префиксами суффиксов "@v" и "@b".

    1. VOID:
      • snd_data_via_command() при взведённом BHVR_VOID принудительно исполняет варианты command_inout_asynch() БЕЗ параметров, полностью игнорируя переданное в канал на запись.
      • Запись же в атрибуты не поддерживается: всё равно атрибут типа void вряд ли имеет смысл.
    2. UCHAR:
      • Тут всё просто: DEV_UCHAR соответствует CXDTYPE_UINT8, в обоих направлениях. Но только для атрибутов.
      • Для команд же -- увы, увы.
      • Поэтому в cda_d_new_chan() даже проверка есть, что при регистрации COMMAND- и RESULT-каналов с типом UINT8 выдаётся WARNING (но запрета нет -- просто попытка исполнения команды приведёт к ошибке).

      ЗАМЕЧАНИЕ: всё только для скаляров. 15.07.2024: и чтение атрибутов-векторов тоже добавлено, см. выше за сегодня в общем разделе по векторам. А заодно уж и запись -- тривиально же.

    3. BOOLEAN: формально не определено, какого bool размера, а при явном присваивании компилятор сам обеспечивает надлежащее преобразование между этим типом и целыми. Поэтому:
      1. Получение: в обе store_current_data*() добавлена переменная bool a_bool, при DEV_BOOLEAN значение вытаскивается в неё, после чего делается
        *(( int32*)value_p) = a_bool; dtype = CXDTYPE_INT32;
      2. Отправка: в обеих *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: а ещё бы как-то анализировать СОСТОЯНИЕ возвращаемых значений -- кажется, там это именуется "quality" -- и отражать его в rflags.

    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 */ };
    • Что такое CHANGING -- хбз.
    • У 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 даже лучше.

  • 26.07.2024: задолбавшись разбираться с типами атрибутов и команд, написал пару консольных утилиток -- tango_attr_info.c и tango_cmd_info.c, состоящие из:
    1. создания DeviceProxy,
    2. затем синхронного вызова get_attribute_config() или get_command_config(),
    3. выдачи полученной информации на cout оператором "<<".

    Собственно "мяса" там буквально 3 строчки (плюс проверка argc на указанность параметров), но over90% -- это try{}catch(){}'и.

    Отдельный прикол -- оператор выдачи _CommandInfo слегка туповат и забывает в конце сделать перевод строки; поэтому пришлось самому делать "cout << endl;".

  • 28.07.2024: насчёт DevEncoded/DEV_ENCODED -- раздел скорее для порядку, для записи чисто теоретических изысканий.

    28.07.2024@душ-в-районе-обеда: учитывая, что "DevEncoded" -- это дуплет {ключ-строка,данные-байтовый_массив},

    • Можно обходиться по тому же принципу, что с командами -- разбить такую сущность на ДВА разных канала, с идентичными базовыми именами, но отличающиеся @-суффиксами:
      1. канал-"ключ", строковый;
      2. канал-"значение", uint8.

      Функционирование:

      • При записи -- запись осуществлять только при записи в канал-"значение".
      • При получении результата чтения -- вертать оба.
      • ...естественно, придётся где-то в дополнительном буфере хранить значение канала-"ключа".

      Одна проблема: с командами, принимающими или возвращающими DevEncoded, это не прокатит: там и так уже разбито на 2 канала.

    • Чуть другой вариант, годный ТОЛЬКО для ЗАПИСИ: указывать "ключ" прямо в имени канала, как-нибудь в @-суффиксе, а значение -- просто писАть туда.

      Такой вариант синтаксически можно и для команд приспособить.

      Минус -- что "ключ" будет исключительно фиксированным.

      И отдельный вопрос -- сохранять ли при этом вёрнутое значение "ключа"? И если "да", то КАК -- chan_ioctl()'ом (но он НЕ публичен и отсутствует в cda.h)? Или какой-нибудь из стандартных строк?

    • @вечер, засыпая: а есть вариант проще: раз и "ключ", и "значение" -- по сути просто цепочки байт, первая из которых NUL-terminated, то представлять ВЕСЬ ДУПЛЕТ как цепочку байт, начинающийся с "ключ"-имени, затем NUL, за которым "значение".

      Не удивлюсь, если эта идея в голову уже приходила.

      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: резюме:

    1. СЕЙЧАС нет ни реальной потребности, ни возможности протестировать результат реализации.
    2. Если же потребность возникнет, то технология реализации выглядит понятной и не особо сложной, сделать можно будет буквально за день.

    Так что сейчас замораживаем раздел до появления реальной потребности (чего, вероятно, никогда и не произойдёт).

    05.09.2024: а вот в epics2tango_gw.c вполне можно сделать, по какому-нибудь из 3 проектов.

  • 13.08.2024: насчёт поддержки 64-битных целых -- INT64/UINT64<->DEV_LONG64/DEV_ULONG64: а может, та проблема "error: 'long long int' is not an enumeration type" специфична для СТАРЫХ gcc, вроде RHEL7'шного 4.8.5 20150623, а в более новых она отсутствует?

    Тогда можно позапихивать всю поддержку 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;
          |                                                                               ^~~~~
    
    • Гуглим...
      • «error: 'long long int' is not an enumeration type»:
      • "why can enum class values of type int not be used as int" на StackOverflow за 30-06-2020: там чувак пытался вместо "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.
      • «error: 'long long int' "is not an enumeration type"»:
      • "Problem with false case in conditional_t being compiled and evaluated" на StackOverflow 11-12-2020: там у человека ровно это ругательство, плюс слова, похожие на наши странные -- "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 как
      typedef ::CORBA::LongLong DevLong64;
      . . .
      typedef ::CORBA::ULongLong DevULong64;
      
      -- это при том, что в "исходнике" tango-9.3.4/lib/idl/tango.idl определяются как
      typedef long long                               DevLong64;
      . . .
      typedef unsigned long long              DevULong64;
      
    • OK, попробовано для примера в одном месте поменять "int64*" на "DevLong64*".

      И оно скомпилировалось даже gcc-4.8.5 на CentOS-7!

    • Что ж, идём по этой дорожке:
      1. Вводим в самом начале определения
        typedef Tango::DevLong64  Tint64;
        typedef Tango::DevULong64 Tuint64;
        
        (T -- Tango) -- имена подобраны так, чтоб
        1. были не длиннее обычных ("Tuint64" как "float64"),
        2. контекстный поиск по "int64" и "uint64" находил.
      2. Повсеместно заменяем "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:

    • omniORB4/CORBA_primitive_types.h: typedef _CORBA_LongLong LongLong;
    • omniORB4/CORBA_basetypes.h: typedef _CORBA_LONGLONG_DECL _CORBA_LongLong;
    • omniORB4/CORBA_sysdep_trad.h: # 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
      
    • ...каковые символы определены в omniORB4/CORBA_sysdep.h как
      #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:

    1. Где в стандарте сказано, что typedef-тип, определённый в namespace, является "отдельным" ("distinct"?) и несовместим/непреобразуем-автоматически с обычными такими же типами?

      17.08.2024: видимо, НИГДЕ. Ибо это просто не так.

    2. Почему несовместимость проявляется только на 64-битных (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 такого вида:

    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;
    }
    
    -- авотфиг! Нигде никаких ошибок, gcc-4.8.5 всё съедает. Причём в диагностике, выдававшейся на "ранней стадии" написания текста, когда оператора ещё не было, оба scoped-типа честно расшифровывались как "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? Ну глянул, ужаснулся:

    • Определены для float, double, bool и short, ...
    • ...но НЕ для int, а...
    • ...для DevLong/DevULong.

    Т.е., нестандартизованный бардак.

    18.08.2024: разобрался. Вкратце -- дело в том, что OMNIORB4 на x86_64 определяет "LongLong" как "long int", а не "long long int", вот тип и не подходит.

    Цепочка расследования:

    • Сделал test_tango_int64_conversion.cpp, где к содержимому test_scoped_int64_conversion.cpp добавлено ещё
      #include <tango.h>
      
      typedef Tango::DevLong   tangos_int32;
      typedef Tango::DevLong64 tangos_int64;
      
      и затем в main() декларации
      tangos_int32      t32;
      tangos_int64      t64;
      
      и statement'ы
      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)).
    • Понятно, что:
      1. При подборе функций/операторов типы long int и long long int НЕ считаются совместимыми и оно в результате не может подобрать подходящего кандидата.
      2. И понятно, почему это проявляется только с 64-битными -- с 32-битными

      Вопрос же в том, почему такое странное определение.

    • Насчёт собственно типа:
      • Сначала подумал "у них что, якобы 64-битные типы реально 32 бита?!".
      • Но потом заглянул в табличку от 27-06-2006 в bigfile-0001.html и вспомнил, что на 64-битных платформал "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".
      • ...и далее просмотр вывода "g++ -E" показал, что CORBA_sysdep_trad.h в нём не упоминается, а вот CORBA_sysdep_auto.h как раз есть.
      • Далее пошарился ещё и нашёл в omniORB4/CORBA_sysdep.h такой фрагмент:
        //
        // 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!

    Выводы:

    • Самое главное -- реализованное в cda_d_tango.cpp решение для 64-битных целых БУДЕТ РАБОТАТЬ. Т.к. "несовместимость", вызывавшая некомпилируемость при прямом использовании int64/uint64 -- чисто "ритуальная", на уровне имён в языке, а не реальной работы железа.
    • Всё это "scoped"/"unscoped" не имеет отношения к делу, а проблема исключительно в различии типов.
    • За каким лешим дятлы-авторы omniORB4 выбрали такой странный способ определения LongLong -- загадка. То ли из-за какой-то одиночной экзотической архитектуры (оно ж должно на дофига всякой хрени работать), то ли по дурости, то ли просто мою табличку не посмотрели :D
    • ЗЫ: отдельная шизофрения в полнейшем несоответствии имён: тип CORBA::Long/Tango::DevLong -- никакой не long, а просто int, в то время как как "long int" определяется CORBA::LongLong/Tango::DevLong64, который, в свою очередь, вовсе НЕ "long long".
    • Надо бы ещё в EPICS7 посмотреть определение...
    • 18.08.2024: посмотрел -- там 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: ну поискал.

    • EPICS:
      • В нынешнем виде -- похоже, base-3.15.1/src/libCom/misc/epicsTypes.h от 01-12-2014: именно там появилось прямое определение, ...
      • ...а в ветке 3.14 вплоть до R3.14.12.8 от 14-09-2018 шло "typedef int64_t epicsInt64;" и только внутри "#if __STDC_VERSION__ >= 199901L", а иначе не определялось никак.
      • Появилось оно там в R3.14.9 от 05-02-2007, а дата на файле 18-11-2006.
      • Гугление по "epicsInt64 site:anl.gov" находит "Release Notes" для двух веток:
        • "EPICS Base Release 3.15.0.2" (R3.15.1 -- 01-12-2014) где в разделе "Changes between 3.15.0.1 and 3.15.0.2" есть секциия "Support routines for 64-bit integers".
        • "EPICS Base Release 3.16.1" (01-06-2017?) где в разделе "Changes made between 3.16.0.1 and 3.16.1" есть секция "IOC Database Support for 64-bit integers".
      • "EPICS Base Release 3.14.9" (URL угадан) в разделе "Changes between 3.14.0beta2 and 3.14.1" имеет секцию "Support for 64 bit long".

        ...мда, озадачивает -- R3.14.1 от 20-12-2002; но внутри там epicsInt64 не было.

      • Обсуждения примерения имели место быть ещё в https://epics.anl.gov/core-talk/2011/msg00071.php
    • omniORB:
      • Гугление по дуплету "omniorb4 _CORBA_LONGLONG_DECL" пользы не принесло: всего 10 результатов, и в основном ссылки на GitHub.
      • Рытьё по сайту -- https://omniorb.sourceforge.io/ -- по "максимуму глубины" дало файл CORBA_sysdep_auto.h из ветки 4.0, на котором значится дата 2004-10-17, и там УЖЕ тот же самый кусок с использованием просто "long".
      • А вот в нарытом по адресу https://sourceforge.net/projects/omniorb/files/omniORB/omniORB-305/ архиве omniORB_305.tar.gz файлом определений работает omni/include/omniORB3/CORBA_sysdep.h за 16-01-2002, и в нём уже просто повсеместное "long long".

        Т.е., та порнография с просто "long" появилась где-то в момент перехода с ветки 3 на ветку 4 ("omniORB4").

    • CX: самое старое, где нашёл определения int64 (как long long) -- BACKUP/istc.20041212.messy/drivers/canadc40_drv.c с той же датой на файле, что в имени директории.

    01.12.2024: при написании отчёта по госзаданию за 2024г возник вопрос "а как определяются стандартные типы, вроде __int64_t?". Поискал -- результат грустен:

    • В /usr/include/bits/types.h (CentOS-7.3, glibc-headers-2.17-157.el7.x86_64.rpm) нашёлся такой фрагмент:
      #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
      
    • В /usr/include/sys/types.h (тот же пакет) -- аналогичный код, но с дополнительным комментарием о происхождении:
      /* 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
      
    • Поиски по всем inttypes.h в системе результата не дали (вечер, устал...) -- самих определений найти не смог, так что пока неясно, откуда их GCC берёт.

    Т.е., определения ОЧЕНЬ похожи на то, что в CORBA_sysdep_auto.h -- возможно, что в omniORB4 они попали из какого-то файла от C99...

cda_d_tine:
  • 19.07.2019: создаём раздел. Пока просто чтоб был, так, на будущее -- в первую очередь чтоб было где сделать раздел для записывания информации. И создаём в месте ПОСЛЕ cda_d_epics/cda_d_pva, и ДО cda_d_daqmx.
  • 19.07.2019: это специализированное место для записи ссылок на информацию, полезную для программирования этого модуля.
cda_d_doocs:
  • 19.07.2019: создаём раздел. Пока просто чтоб был, так, на будущее -- в первую очередь чтоб было где сделать раздел для записывания информации. И создаём в месте ПОСЛЕ cda_d_epics/cda_d_pva, и ДО cda_d_daqmx.
  • 19.07.2019: это специализированное место для записи ссылок на информацию, полезную для программирования этого модуля.
cda_d_daqmx:
  • 20.05.2019@утро-из-дома-на-работу, по лестнице вниз: а ведь можно изготовить cda-плагин для доступа к DAQmx-девайсам. Прямо так его и назвать -- cda_d_daqmx.c.
  • 20.05.2019: да, создаём раздел. Также пока пустой, но будем собирать информацию/знания, необходимые для программирования.

    20.05.2019@дорога-на-обед-мимо-ИПА: ЗАМЕЧАНИЕ: это делается НЕ из-за ПОТРЕБНОСТИ, а из-за ВОЗМОЖНОСТИ. Всё равно осваивать (хоть по минимуму) это NI'ное поделие -- ну так и формализуем в модуле; тем более, так навскидку DAQmx'ный API выглядит годням для заворачивания в синтаксис/парадигму каналов.

    Плюс, возможность получить дополнительный опыт на тему "а как бывают парадигмы доступа к данным" (и, тем самым, приблизиться к пониманию "природы данных, в платоновском смысле") -- то, ради чего хочется повозиться с TANGO и EPICS.

  • 20.05.2019: это специализированное место для записи ссылок на информацию, полезную для программирования этого модуля.

    20.05.2019: 16.05.2019 пытался (в процессе разбирательства с DAQmx под Linux) гуглить информацию/руководства о программировании под DAQmx вообще и под Linux в частности. Пока что -- фигвам, ОЧЕНЬ всё грустно. Как и вообще с NI -- сайт у них организован отвратно (Microsoft их укусил?), "воды" очень много, а толковой информации практически ноль.

    • Единственная страница, где есть хоть что-то конкретное об азах программирования на DAQmx -- "Using NI-DAQmx in Text Based Programming Environments". Что-то хоть как-то похожее на полезное описание, написанное программистом для программистов (а не обычные мантры для олигофренов). Но очень краткое.

    22.05.2019: да, сайт NI.com очень дурацкий, и документацию там найти крайне сложно; Дубатов это даже признаёт и говорит, что "По поводу документации. Согласен, что не так просто ее найти на сайте - проблема давно известная, ее как могут устраняют.".

    Также он прислал некоторое количество ссылок на документацию (он-то знает, где её находить) и комментариев на тему "что есть что".

    • На тему "NI-DAQmx Base":
      NI-DAQmx Base - это облегченная версия драйвера, поддерживающая только USB-устройства. Раньше информация об этом была где-то на поверхности, сейчас я ее быстро не нахожу. Но это как раз та самая старая архитектура.

      Вот кто б мог догадаться по названию, что "Base" -- это только для USB!

    • NI-DAQmx Help
    • NI-DAQmx C Reference Help
      В принципе, это уже исчерпывающая документация. Да, она ориентирована на Windows, но концепция от этого не меняется.

      Но см. ниже моё замечание насчёт "полезности" этой ссылки.

    • "Конфигурирование шасси" -- Configuring PXI Trigger Lines in Linux.
      Если вкратце, утилита позволяет идентифицировать шасси (если это не произошло автоматически), вручную нумеровать шасси (когда их несколько в системе), смотреть список устройств, задавать им 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 Key Concepts

      (Это было прислано в ответ на моё недоумение, почему в "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'ами.)

    • Там, в частности, есть раздел "Channels and Tasks in NI-DAQmx", где сказано
      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: роем дальше:

    • Насчёт "channels" и "tasks":
      • "Channels: Physical, Virtual, Local Virtual, and Global Virtual" -- многабукафф, но, похоже, что в DAQmx "канал" -- это как обычный канал (хотя надо ещё разобраться с их вариациями).
      • "Tasks in NI-DAQmx" -- тут хуже:
        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.

    • Нагуглилась страница "NI-DAQmx Syntax for Specifying Physical Channel Strings", но она "традиционно" для NI показывается пустой, так что пришлось читать...
    • ...сохранённую копию из гуглокэша.
    • 03.06.2019: ещё нашлось: "Physical Channels" -- содержит списки с диапазонами имён для каждого типа/серии устройств; только надо заранее знать, к какой "серии" устройство относится, т.к. там полтора десятка подстраниц. Видимо, вариант -- гуглить на тему "ИМЯ_УСТРОЙСТВА physical channels". 03.06.2019: попробовал для имеющихся PXIe-6363 и PXI-5114 -- фиг, не нагуглилось.

      Оно зарыто глубоко-глубоко, напрямую по навигации (двигаясь вглубь) туда не придёшь, я наткнулся случайно, по ссылке со страницы "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".

    Мои ВЫВОДЫ по результатам:

    1. Зачем вообще нужна концепция "task" -- загадка. Т.к.:
      1. Она (в декларируемом виде) годна лишь для примитивных работ, типа просто взять да произвести несколько измерений.

        А если задача чуть-чуть сложнее и включает как измерения, так и уставки -- например, произвести некие измерения (с АЦП) при последовательно изменяемых уставках (в ЦАП), например, для снятия матрицы откликов или калибровочной кривой -- то уже не катит, т.к. в один task засунуть каналы разного вида (чтения и записи) низзя.

      2. Что вообще она даёт -- неясно.

        Был бы это некий "контекст", как в CX (или per-thread, как в EPICS) -- было б ясно: хранилище ресурсов, привязанных к "задаче".

        Но это не так, и в чём отличие от ситуации, когда каналы бы просто регистрировались "глобально" -- загадка.

    2. Следствие такого понятия "task" -- непонятно, как сделать (в cda_d_daqmx.c и подобных прослойках) общую операцию "зарегистрировать канал".

      Т.к. просто сделать 1 task (на "sid" или даже глобально) -- нельзя, ибо каналы разных видов надо пхать в разные task'и. Возможно, что и от разных девайсов тоже.

      А как решать, что "пришла пора завести новый task, для вот этого канала" -- загадка:

      • Пробовать привязать к уже существующему и если ошибка, то заводить новый?

        Плохо, т.к. каналы могут регистрироваться с чередованием -- R,W,R,W, и алгоритм "если ошибка, ..." создаст 4 task'а, хотя хватило бы 2.

      • Заводить по task'у на каждый канал (т.е., task -- поле в hwrinfo_t)? Знатный overkill и дикая пустая трата ресурсов.
      • 03.05.2019: пришла идейка в момент записывания этого всего: можно отдельно "учитывать" устройства (т.е., то, что идёт в имени канала до '/'), и иметь на них отдельно SLOTARRAY, в каждой ячейке которого по task'у на каждый вид каналов.

        Потенциальные проблемы:

        1. Как определять тип канала?

          Можно по имени: по принятому в DAQmx (или у NI вообще) стандарту, аналоговые входы начинаются с "ai" (Analog Input), аналоговые выходы с "ao" (Analog Output), цифровые -- с "di" и "do" соответственно, счётчики -- с "ctr".

        2. Но что, если каналы -- заранее неизвестного вида?

          Ответ -- а хбз: "неизвестные" каналы зарегистрировать у DAQmx всё равно не удастся: сама регистрация выполняется разными функциями, в зависимости от типа канала.

        3. А если канал -- "глобальный виртуальный", т.е., с как-то назначенным именем, уже не отражающим его тип? И имя устройства в нём будет отсутствовать, что лишает возможности найти task для привязывания.

          Похоже, единственное решение -- объявить глобальные каналы неподдерживаемыми.

        4. Но бывают ещё и т.н. "внутренние каналы" -- см. "Internal Channels". Например, источники калибровочных сигналов, могущие быть измеренными аналогично обычным каналам.

          И уж у них имена могут быть ну совсем произвольными.

    3. Неудобно, что куча настроек канала должна выполняться прямо в момент его регистрации.
      • Например, при создании измерительного канала надо указывать "input terminal configuration", минимум и максимум, плюс ещё ожидаемые единицы измерения (параметр units, который int32).
      • И сама регистрация выполняется РАЗНЫМИ функциями, в зависимости от типа канала. Например, DAQmxCreateAIVoltageChan() (для типа "AI Voltage").

      Т.е., cda_d_daqmx_new_chan()'у понадобится ДОФИГИЩА информации, которую неясно, как узнать.

      Указывать прямо в имени канала, например, через суффикс '@', в формате "ПАРАМЕТР=ЗНАЧЕНИЕ"?

      Мерзковато...

    И мои ВПЕЧАТЛЕНИЯ по результатам:

    • DAQmx (как и вся архитектура NI'ного софта) приспособлена для решения НЕБОЛЬШИХ задач, в рамках стенда. Когда всё делается по месту, каждый канальчик регистрируется индивидуально, с предварительным знанием о деталях его использования (не только что он входной, но и что это вольты, и что использоваться он будет так-то, поэтому может быть прикреплён к такому-то task'у).
    • С масштабируемостью же там -- очень грустно. Крайне сложно изготавливать некие "общие прослойки" (вроде cda_d_daqmx.c), из-за сильной детализованности API и отсутствию общности.
    • А вот в ДРАЙВЕРЫ засунуть его использование -- наверное, можно: драйвер-то каждый экземпляр работает с 1 устройством, и уж драйвер будет знать, какой канал он как будет использовать.

      ...но и драйверы придётся приспосабливать для конкретных использований, в то время как сами NI'ные железки дюже гибкие и позволяют кучу вариантов работы.

    • Отдельное удивление ("поднятие бровей") у меня вызывает эта самая гибкость: ЗАЧЕМ?

      Вот зачем, спрашивается, делать переконфигурируемые каналы, умеющие быть как входами, так и выходами? Пины на разъёме экономили?

      И как это внутри устроено: хитрая микросхема ЦАП+АЦП+регистры, которая переконфигурируется? Или раздельные чипы (ЦАП, АЦП, регистры) и коммутатор, подсоединяющий затребованное к выходным контактам?

      В первом случае -- шибко монструозно; во втором -- глупо и нерационально.

    03.06.2019: попробовал ещё понять, как надо работать с этими task'ами -- можно ли одновременно завести 2 штуки, одну для входов, другую для выходов, чтобы можно было реализовывать "сценарии", с измерением отклика на изменения уставок. Или, в более общем случае -- вообще несколько.

    Для чего загуглил на тему "daqmx multiple tasks". В верхушке выдачи оказались 2 ссылки:

    1. "How Many DAQmx Tasks Can I Run Simultaneously?", которую традиционно надо смотреть из гуглокэша.
    2. "NI-DAQmx Multiple Tasks on one card? " (начато в 2004г), где в конечном итоге рекомендуется почитать предыдущую статью (возможно, как раз для ответа на это обсуждение и написанную).
    3. И в п.2 рекомендуют почитать "Key NI-DAQmx Concepts->Tasks->Task State Model chapter in the NI-DAQmx Help file".

    Вывод: основной смысл "task'ов" -- управление общими (разделяемыми между каналами) ресурсами устройств, в первую очередь -- всякими таймерами/PLL.

    Точнее, создать-то можно много task'ов, но в состоянии "reserved" будет только один, а остальные обломятся (при попытке "захватить ресурсы").

    Т.е., то, что у обычных (наших) девайсов является управляющими регистрами устройства и может отображаться в качестве каналов, тут доступно через такой интерфейс. Смысл, видимо, в том, чтобы сделать работу максимально простой для "простых юзеров".

    С другой стороны, для использования в ДРАЙВЕРАХ такая модель вполне прокатит. Так что -- frgn4cx/daqmx/drivers/ (плюс pzframes/ и screens/)?

    07.06.2019: вопрос: а можно ли ДО создания канала просто по имени узнать его тип? Нету ли в DAQmx такого вызова? Это решило бы проблему "для разных типов каналов (вольты, температура, ...) разные процедуры открытия".

    Отдельный вопрос вдогонку: а нету ли в API также вызова "получить список ВСЕХ каналов такого-то устройства"?

    12.06.2019: "гвоздь в крышку гроба cda_d_daqmx.c": посмотрел я внимательно на NIDAQmx.h:

    • 950581 байт, 9641 строка, 3234 функции (судя по "grep __CFUNC").
    • Комментариев среди этого не так много (в основном, к каждой #define-константе, в той же строке).
    • Определения функций идут сплошным потоком, по 1 в строке, почти без комментариев, лишь с разделением на блоки/секции.
    • Сами функции -- нифига не ортогональный набор: там для каждого чиха свой вызов, и для многих типов устройств свои специфичные наборы вызовов с именами формата DAQmx_<ДЕЙСТВИЕ>_NNNN_<ЧЕГО_НИБУДЬ>() или DAQmx<Действие>NNNN<Объект>(), вроде DAQmx_Val_Switch_Topology_2576_2_Wire_Quad_16x1_Mux() или DAQmxSetup4480Cal().
    • Судя по количеству дубликатов среди констант (например, DAQmx_Val_Voltage=10322 -- 8 штук), файл по крайней мере частично генерится из каких-то иных исходников.

    Итого:

    1. API монструозен, а не элегантен.
    2. Использовать NIDAQmx.h как справку/документацию не получится.

    Вывод: похоже, идею о cda_d_daqmx.c стоит похоронить -- не катит этот API для таких целей.

    14.06.2019@дорога-с-обеда-из-Гусей, у Николаева, 14: с другой стороны, можно сделать общий "слой-прокладку": в стиле vdev_sodc_dsc_t, pzframe_chinfo_t, pzframe_chan_dscr_t -- табличку каналов (адресуемую по нашему номеру канала), где для каждого будут указываться:

    • DAQmx'ное имя.
    • Свойства -- r/w, подвид (voltage, temperature, ...), тип (dtype, nelems).
    • Вероятно, ещё какие-то свойства, специфичные для DAQmx и/или для разных классов устройств.
    • Для "дополнительных", "настроечных" каналов (вроде минимума/максимума у вольтовых АЦП) -- номер "базового" канала.
    • Опционально -- номер task'а, к которому прицепить (напоминает vdev_sodc_dsc_t.subdev_n, не так ли?).

    Для такого "табличного" использования DAQmx вполне может оказаться даже удобным -- всю работу станет выполнять "daqmx_lyr", а сами драйверы будут состоять в основном из таблицы.

    (В отличие от cda'шного использования, где для всего должно быть достаточно одного лишь имени канала.)

    Кстати, эта мысль родилась из той мысли, что для EPICS, возможно, API DAQmx не так уж и плох -- ну сделают себе какой-нибудь особенный driver support (или всё же record support?), которому и достаточно будет указывать в record'е все вышеперечисленные свойства (а "настроечные" параметры там как раз лягут на поля record'а, возможно, уже штатно существующие).

  • 16.06.2019: приступаем к созданию первого, пока что экспериментального драйвера, работающего через DAQmx -- pxi6363_drv.c.

    Вся требуемая для его изготовления информация вроде уже есть.

    • Пока попробуем сделать ПРОСТЕЙШИЙ вариант -- измерение 16 скалярных напряжений (для начала -- даже без лимитов).
    • Директория frgn4cx/daqmx/drivers/.
    • Покамест сделаем всё в одном файле -- и драйвер, и будущий layer к нему.
    • Замечание: де-факто НЕ существует устройства "PXI-6363", а только "PXIe-6363".

      Но чисто для унификации -- поскольку есть много устройств в двойном исполнении, PXI-NNNN и PXIe-NNNN -- будем давать имена БЕЗ "e".

    • Технологическое: event'ы, которые DAQmx присылает в отдельный thread, будем ловить через pipe -- как это делается во всяких CAMAC-драйверах, получающих сигналы вместо желаемых синхронных уведомлений.

    16.06.2019: начало:

    • Сделан "скелет" драйвера -- privrec, методы init_d, term_d, rw_p и метрика. Просто чтоб уже было компилябельно -- для проверки остальной части.
    • И начало будущего daqmx_lyr.[ch] -- пока типы, daqmx_chan_dsc_t, daqmx_chan_cur_t (пустой) и daqmx_context_t; все идеологически аналогичны vdev'овским.
    • Отдельно надо отметить способ #include'нья NIDAQmx.h: поскольку там определяются типы с именами, пересекающимися с нашими 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: пилим...

    • Сделаны также pxi6363_drv_i.h и pxi6363.devtype.
    • Плюс Makefile'ы в соответствующие директории.
    • Нюанс: каналы АЦП пущены не с 0, а с 10. Смысл -- чтобы у будущих "настроечных" каналов никогда не было бы базового канала номер 0.
    • Пришла пора реализовывать работу с DAQmx'ными event'ами, и тут встала дилемма: делать всё-таки layer (т.е., "контекст" хранится внутри него, а драйверу отдаётся только handle) или же библиотеку (аналогично vdev -- контекст хранится прямо в privrec'е).

      Почему дилемма: потому, что надо как-то передавать/получать параметры для идентификации, кто же у нас "клиент".

      1. Если библиотека -- то нужно по pipe'у на каждый драйвер.

        Кроме собственно изоляции драйверов -- ещё и потому, что DAQmx передаёт лишь 1 pointer, а task'ов более 1 штуки. Можно callbackData использовать как task-N, а драйвера-получателя нам идентифицирует cxscheduler -- даст и devid+devptr (если события ловить драйверу), и privptr2 (если библиотеке).

      2. А если layer -- то можно иметь 1 pipe на все драйверы, просто писать в него handle-ID.

        ...но task'ов-то может быть более 1 на устройство. Решение -- в 32 бита упаковывать и номер устройства, и номер task'а в нём; скорее всего, обоим хватит по 1 байту.

      Муки выбора:

      • С одной стороны, вариант "layer" красивее.
      • С другой, прямо СЕЙЧАС вариант "библиотека" сделать проще.
      • Так что склоняюсь к более простому варианту библиотеки.
      • Получасом позже (пока это всё записал, и потом приступил к кодированию): авотфиг!!! Ведь в EventCB передаётся ОДИН-единственный pointer. А нам нужно не только номер task'а, но ещё и как-то добыть "ссылку на драйвер" -- чтоб узнать дескриптор, куда писать.

        Есть, конечно, халтурные варианты:

        1. Указатель передавать на context, а в нём искать по списку task'ов, какой номер в нём соответствует переданному.

          Некрасиво.

        2. Упаковывать в pointer пару {номер task'а, файловый дескриптор} -- они ж оба не очень большие, в 16 бит влезут.

          Но нет: если под *nix дескрипторы действительно являются "НЕБОЛЬШИМИ неотрицательными целыми числами", то под другими ОС -- совсем необязательно.

          ...да и халтурновато.

        3. Не передавать информацию напрямую через callbackData, а класть в некую структурку, указатель на которую уже передавать "как указтель".

          Минус: придётся аллокировать ещё и массив этих структур, по числу task'ов...

      В конечном итоге был выбран вариант "библиотека" -- с поштучными объектами daqmx_context_t.

    • Кстати, и в будущем тоже можно НЕ делать layer, а обойтись "библиотекой" -- динамически загружаемой (config-командой "load-lib daqmx") daqmx_lib.so; соответственно, исходные файлы тогда будут daqmx_lib.c и daqmx_lib.h.

    18.06.2019: допиливаем первоначальный вариант.

    • @утро, дорога на работу, в районе ИПА: идеологию проведения измерений выбираем аналогичную pzframe'овской:
      • Заводим флажок-защиту measuring_now.
      • При запросе ЛЮБОГО канала АЦП взводим флаг =1 и запускаем процесс измерения.
      • По окончанию -- сбрасываем флаг =0 и возвращаем результаты.

        Да, именно в таком порядке -- чтобы если сервер прямо из обработчика 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 ...
    • В конечном итоге -- заработало!!!
    • Из замеченных неприятностей/странностей:
      1. Минимальное число сэмплов -- 2, вариант 1 оно не принимает, отдавая ошибку "Requested value is not a supported value for this property. The property value may be invalid because it conflicts with another property."

        Причину удалось найти перебором значений количества сэмплов на тестовой программке Acq-IntClk.c.

        Выбранное решение: заказываем измерение 2 сэмплов, а вычитывать просим 1.

      2. После вроде как завершения task'а по DoneEvent'у всё равно надо делать DAQmxStopTask(), иначе при следующей попытке старта получаем ошибку "Specified operation cannot be performed while the task is running.".

        Исправлено просто -- ну делаем DAQmxStopTask() в pxi6363_task_done(), непосредственно перед ReturnDataSet()'ом.

      3. В сообщении об ошибке, возвращаемом DAQmxGetExtendedErrorInfo(), в конце зачем-то есть '\n'. В результате получаем в логах лишний перевод строки.

        Надо обрезать.

    Пока на вид вполне удовлетворительно. Далее надо будет:

    1. Проверить "скорострельность" -- как оно пашет на циклах не 1с, а 0.1 и 0.01.
    2. Подключить макетку, скоммутировать каналы ЦАП на АЦП и убедиться, что читается "то".
    3. Добавить поддержку каналов ЦАП (пока хоть скаляры, а уж позже и табличные), входных и выходных регистров.
    4. Поддержку осциллографических измерений.
    5. Для чего -- "служебные" каналы, вроде выбора типа стартов и таймингов.
    6. ...а также настроечные каналы (всякие минимумы/максимумы).
    7. ...для чего -- более полную поддержку разных видов каналов.
    8. Также надо научиться ловить всякие ошибки (возвращаемое через DoneEvent значение status) и переполнения, и должным образом выставлять rflags.

    19.06.2019: идём дальше.

    Предварительный список дел.

    • Обрезание \n в текстовом сообщении
    • Перетащить DAQmxCfgSampClkTiming в _init_d(), для чего в оном делать не сразу return, а смотреть код.
    • Завести 4 task'а: ADC (уже имеющийся), OUT (DAC), DIN и DOU (входной/выходной регистры).
    • Не завести ли также массив task'ов, передаваемый в daqmx_init() -- чтобы там было сказано, как с этим task'ом обращаться (например, какое событие регистрировать)?
    • В pxi6363_task_done() проверять, от какого из task'ов событие.

    Поехали!

    • Сделана daqmx_geterrdescr(), обрезающая нежеланный '\n', и всё переведено на неё.
    • В pxi6363_task_done() вставлена проверка пока что простая -- что мы имеем дело с TASK_N_ADC, а в противном случае ничего не делаем.
    • Насчёт работы с входными и выходными регистрами: оказалось всё ОЧЕНЬ непросто.
      • В PXIe-6363 (и, видимо, в большинстве NI'ного железа) нету отдельных пинов для входных и выходных регистров, а есть некие общие "DIO"-каналы, каждый из которых может конфигурироваться либо как вход, либо как выход.

        Как с такими гермафродитами обращаться -- решительно неясно.

        • В карте каналов иметь оба варианта, но конфигурировать в auxinfo, чтобы часть каналов становилась DO, а часть DI, и те каналы из общей карты, что сконфигурированы "не того пола", отдавались бы как UNSUPPORTED?
        • А можно ль сделать "пол" динамически изменяемым -- отдельными (булевскими) каналами?
      • Написано письмо Карнаеву и Чеблакову "Идеологический вопрос (NI, железки, каналы, ...)" -- может, что подскажут по опыту NSLS-II.
      • Насчёт "иметь в карте каналов оба варианта..." -- есть проблема: дело в том, что физические каналы "вход" и "выход" имеют одно и то же имя (port0/line...), а "пол" устанавливается способом регистрации канала -- какая функция его добавляет в task: DAQmxCreateDIChan() или DAQmxCreateDOChan().
        • Поэтому не то, что "динамически" менять "пол" канала, ...
        • ...а даже просто указывать такие каналы в заранее подготовленной карте (в исходнике драйвера, используемой для всех экземпляров устройства) -- и то проблема.

        Как решать проблему с картой -- хбз.

        • Вводить понятие "include" (как в PSP)? Но это проблемы не решает -- всё равно генерить таблицу.

          ...А главное -- какой, нафиг, "include": ведь карта -- не список, а именно КАРТА, индексируемая номером канала.

        • Разве что генерировать в _init_d() ВСЮ карту динамически, зануляя строки "несуществующих" каналов.

          Одна только сложность, в связи с кривоватым API чтения/записи данных в DAQmx: там ведь идёт работа с блоком данных ВСЕХ каналов task'а, скопом. И если список каналов фиксирован, то ещё как-то можно приспособиться, а если он создаётся динамически -- то это уже задница...

          Или пользоваться тем, что по готовой карте можно высчитать оффсет нужного канала в общем блоке? Тогда высчитывать это в самом начале, и именно это хранить в таблице из daqmx_chan_cur_t (пока неиспользуемой)?

      • Кстати, насчёт "можно ли через DAQmx ловить события изменения состояния битов входного регистра": да, можно. Ключевое слово -- "сигналы".
        • Регистрация -- DAQmxRegisterSignalEvent().
        • Пример имеется в ReadDigChan-ChangeDetectionEvent.c (расположенной в "NI-DAQmx 18.1 Linux C Examples/Events/Signal/Change Detection/Read Dig Chan-Change Detection Event/").

      Резюме: пока что эту тему замораживаем.

    • Кстати, судя по описаниям от DAQmxRegisterDoneEvent() и DAQmxRegisterSignalEvent(), где про параметр callbackFunction (адрес обработчика) сказано "Passing NULL for this parameter unregisters the event callback function.", эти Event'ы в DAQmx -- НЕ множественные. Скорее всего, во внутреннем представлении TaskHandle (который структура, а не void) есть по одному полю на каждый тип события, и эти функции просто записывают указанное в эти поля.
    • Вместо "return daqmx_init()" теперь сохраняем результат, и если он <0 -- то return его, а иначе делаем дальнейшие действия, вроде настройки параметров task'ов.
    • Подготавливаем поддержку каналов ЦАП.
      • Добавляем их в карту -- как раз 4 штуки было незадействовано, после АЦП'шных.

        Они получили стандартные имена out0...out3.

      • Заводим второй task -- TASK_N_OUT.
      • Добавлены в pxi6363_chandescr[].
    • Далее надо расширять схему кодирования типов каналов -- DCK_nnn (Daqmx Channel Kind, ex-DCT_ ("Type")).
      • Общая идея такая: чтобы это была агрегация нескольких битовых полей, в т.ч.:
        1. "Вид" канала -- Voltage, Current, Therm*, Digital, ...

          Тут нужен минимум байт.

        2. "Пол" канала -- чтения (0) или записи (1).

          1 бит.

        3. "Реальность" канала: физический или настроечный.

          Вроде бы тоже всего 1 бит, но неясно, не понадобится ли несколько разных "классов" настроечных каналов (для автоматического выполнения действий, вместо поштучного перебора номеров).

        4. Вероятно, не помешает и дополнительное поле "класс настроечного канала".

          Потенциально может понадобиться много битов.

        Мини-обсуждение:

        • Понятно, что поля (3) и (4) можно совместить: сделать одно поле "категория" канала, которое если ==0, то это канал физический, а иначе -- настроечный, и конкретное значение этого поля определяет, что же за настроечный канал.
        • Также возможна альтернатива: если указывается, что канал настроечный, то "вид" уже неважен, и то поле может использоваться как "категория".
        • ...а ещё вариант, что "настроечный" -- это такой специальный "вид" канала (0 -- никакой, 1 -- настроечный, 2 -- Voltage, 3 -- Current, ...).

          Но тогда, при надобности иметь разные "классы" настроечных, всё же понадобится 3-е поле.

      • Дополнительное соображение: очень желательно иметь возможность использовать "тип" как индекс в таблице -- для унифицированного вызова методов (например, "запись").
      • С учётом последнего соображения наиболее разумным выглядит бит R/W сделать МЛАДШИМ.

      Итого: на текущий момент выбрана схема "младший бит -- R(0)/W(1), далее -- вид/природа (натура)".

      • Важное отличие от предыдущих размышлений -- название "nature" (природа, натура): так мы теперь будем называть подвиды каналов (voltage, current, ...).
      • Кодирующий макрос -- 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.
    • Также заведена "карта task'ов" -- 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».

    • Пока единственная приходящая в голову мысль -- делать по 1 task'у на каждый канал ЦАП, устанавливать атрибут "оставлять значение после завершения task'а", и запись выполнять последовательностью "WriteAnalogF64, StartTask, StopTask".
    • (Остаётся, конечно, ещё вариант тупо хранить значения всех каналов, и при записи в один из них менять соответствующую ячейку и выполнять WriteAnalogF64. Но тоже извращение, немногим отличающееся от предыдущего.)

    21.06.2019: чуть в сторону: возникло желание узнать, что же происходит "за кулисами": как обстоят дела с thread'ами, как libnidaqmx делается коммуникация с ядром.

    Насчёт идентификатора thread'а -- "GetThreadID":

    • В Linux он добывается системным вызовом gettid().
    • ...но он есть только как syscall, а как функции в glibc -- нету, это даже явно сказано в man-странице gettid(2): "Glibc does not provide a wrapper for this system call; call it using syscall(2).".
    • На StackOverflow (в обсуждении «getting error in c program "undefined reference to gettid"») решать эту проблему рекомендуют путём прямого вызова syscall. Варианты по степени управильнивания:
      1. (long int)syscall(224) -- но так плохо, поскольку SYS_gettid в разных системах может отличаться, где-то 186 вместо 224 (неясно, как такое возможно: ведь бинарники из одной должны быть запускабельны в другой; или речь о разных архитектурах?).
      2. tid1 = syscall(SYS_gettid);
      3. С проверками:
        #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. Итак:

    • События присылаются в другом thread'е. Путём вставки печати gettid() увидено, что, например, daqmx_init tid=9965, а EventCB tid=9972.

      И ps показывает, что в процессе (pid=9965) аж 5 thread'ов: 9965, 9969, 9970, 9971, 9972; куда исчезли 9966,9967,9968 -- неясно; можно прогнать через "strace -f", но лень.

      ...всё-таки посмотрел. Первый "исчезновенец" действительно порождается основным thread'ом, что-то делает и завершается. А следующие 2 -- видимо, каким-то сторонним процессом, т.к. их PID'ы в логе не фигурируют.

      Чем конкретно занимается оставшаяся троица -- разбираться уже реально лень. Вкратце:

      1. Сплошные clock_nanosleep(CLOCK_MONOTONIC,...), перемежаемые semop() (возвращающим -1/EAGAIN; возможно, так и должно быть).
      2. Почти ничего не делает -- пара десятков строк.
      3. ioctl()'ит с дескриптором, глядящим на /dev/nipalk.
      4. Практически исключительно futex()'ы (это тот, в котором Event'ы присылаются).

      Кстати, с futex'ами возятся все.

    • Насчёт коммуникации: "ls /proc/PID-cxsd/fd/" показывает ТУЧУ открытых в разные места дескрипторов.
      • В /dev смотрит один-единственный -- на /dev/nipalk.
      • А ещё -- в /var/tmp/nipal/ толпа разных.
      • В /var/tmp/natinst/shared/namedEvent/; в самой этой директории дофига всяких файлов (софтина смотрит не на все!) размером по 9 байт (?!); именами напоминающие какой-то реестр, в именах присутствует нечто, похожее на UUID'ы.
      • В /var/lib/ni-daqmx/lock/ -- там файлы nidaqmxSystemStorageRrWr.lock и nidaqmxSystemStorageWrite.lock.

    01.07.2019: приступаем к научению драйвера работать осциллографом.

    • Карта каналов сильно расширена:
      • Первые 100 каналов зарезервированы под "конфигурацию" -- аналогично KOZDEV'овским.

        Они поделены на 2 блока по 50шт -- запись и чтение; каждый из тех блоков поделен на 30шт int32 и 20шт double, так что в сумме w30i,w20d,r30i,r20d.

      • Дальше идут группы по 100 штук: ADC, OSC, OUT, OUT_TAB, OUTRB, INPRB, DIODIR (последние указывают "пол" каналов dio: 0:inp, 1:out).
    • Также введены "формальные характеристики" -- всякие MAX_NUMPTS и т.п.
    • _rw_p() в начале цикла (где из *values[] берутся значения для записи) обучена отличать double-каналы, в т.ч. отдельно нескалярные каналы OUT_TAB.
    • Простейшая поддержка параметра/канала NUMPTS: в _init_d() инициализируется =1000, а в _rw_p() форсятся значения в диапазоне [2,MAX_NUMPTS].
    • Также вчерне сделана работа с осциллографическим режимом: пока не в отдельном режиме, а "по запросу": если приходит запрос на канал OSC, то запускается измерение осциллограммы. А если измерение УЖЕ идёт, то просто взводится флаг "было запрошено".

      Для этого:

      • Флаг measuring_now из булевского переделан в 3-состоянный: DAQMX_MEASURING_{NOTHING,SCALAR,OSCILL}={0,1,2}.
      • StartMeasurements() запрашивает нужное количество измерений (2 либо cur_numpts).
      • pxi6363_task_done() тоже предпринимает разные действия -- в зависимости от значения measuring_now.
      • ...в частности, даже после измерения осциллограмм последние значения из них возвращаются как скаляры в каналах ADC.

    Не всё пока доделано:

    • Нет возврата "сопутствующих каналов" (CUR_NUMPTS и даже MARKER).
    • Не говоря уж об отсутствии пока что каналов RANGE (для отдачи наверх диапазона сигналов, для отрисовки).
    • Да и вообще возврат данных пока сделан халтурно -- вместо "кошерного" варианта с заполнением массивов и отдачей всего одной пачкой, как в настоящих pzframe-драйверах.

    02.07.2019: а не заюзать ли pzframe_drv в pxi6363_drv.c? Если не библиотеку, то хотя бы pzframe_drv.h -- ради PZFRAME_CHTYPE_*?

    03.07.2019: делаем отображающий компонент -- pxi6363_data.c+pxi6363_gui.c.

    • Первая давняя мысль была сделать его в frgn4cx/pzframes.

      Но директория hw4cx/pzframes/ не поддерживает "стороннюю сборку" (нет в ней никаких DirRules.mk), да и неудобно было бы иметь ДВА (или более) вариантов pzframeclient.

      Так что подселяем прямо в hw4cx/pzframes/.

      Для чего пришлось выпендриваться с добавлением ссылки на frgn4cx/include/, что очень криво.

      Вывод: надо переселять ДРАЙВЕРЫ в hw4cx/daqmx/, и там делать надлежащую проверку "можно ли сейчас собирать или исключить всё из процесса сборки".

    • Сами файлы сделаны на основе adc812'шных, с удалением всего лишнего.
    • Огромный скрин получился, хотя и нелепый: море пустого места -- справа от таблицы каналов и вся панель "device-wide".

    04.07.2019: продолжаем.

    Краткий список того, что нужно сделать с осциллограммами "для минимального счастья":

    1. Возвращать данные скопом, через набор буферов r (addrs, dtypes, nelems, ...).
    2. Завести реестр chinfo[], как в pzframe-драйверах, чтобы в _init_d() проставлять свойства каналов, а в _rw_p() корректно "игнорировать" запросы ко всяким статусным/автообновляемым, вместо отдачи CXRF_UNSUPPORTED.

    Процесс работ:

    • Сделан свой собственный ReturnDoubleDatum().
    • С его использованием в pxi6363_task_done() вставлена отдача каналов PXI6363_CHAN_ALL_RANGEMIN и PXI6363_CHAN_ALL_RANGEMAX.

      Плюс целочисленные -- CUR_NUMPTS и MARKER.

    • Затем в pxi6363_data.c подправлены имена каналов осциллограмм: они были просто oscN, а надо adc_oscN.
    • И -- заработало!!! Осциллограмма отдаётся, хотя сами данные "из воздуха" бредововаты:
    • @дома-обед: если поддерживать возможность использования внешних стартов и клоков -- то нужно поддерживать и канал-команду STOP. Иначе при конфигурировании (хоть по ошибке) чего-то внешнего, чего нет, устройство может встать в режим ожидания навечно.

      Но тогда может образоваться race condition: поскольку сокет от клиента и дескриптор event_pipe[PIPE_RD_SIDE] отслеживаются в одном месте, а номер дескриптора клиента может оказаться и ниже, то возможен такой сценарий:

      1. Возникает прерывание готовности данных, производится запись в pipe.

        ...но ещё не успевает вычитаться и обработаться.

      2. От клиента прилетает команда STOP.

        Т.е. -- данные есть в ОБОИХ дескрипторах.

      3. Готовые дескрипторы перебираются последовательно, и первым обрабатывается клиентский.
      4. Исполняются операции по STOP.
      5. Затем программируется следующее измерение, и пометка об этом делается в privrec'е.
      6. Обрабатывается event-дескриптор, из которого вычитывается информация о завершении измерения и готовности данных.

        Но это уведомление от ПРЕДЫДУЩЕГО измерения, которое по STOP'у было только что выкинуто, а в реальности в текущий момень девайс висит на ожидании/измерении, и никаких данных отдать ещё не готов!

      Решение-то довольно простое:

      • Вместе с уведомлением о завершении также отправлять некий последовательный номер измерения, а в обработке сообщения -- проверять, что переданный номер совпадает с текущим активным.
      • А при каждом новом программировании измерения номер увеличивать на 1.
      • И, кстати, особо многобитовости тут не требуется: скорее всего, обычно надо будет отличать только соседние запуски (вряд ли события будут буферизоваться в очереди).

        Так что в пределе хватило бы даже 1 бита.

      Вопрос в сторону: а не возможна ли такая же ситуация с ПРОЧИМИ fast-ADC? Они ж у нас тоже ловят IRQ через файловые дескрипторы.

    • Маленькая засада:
      1. В нынешней модели функция EventCB() находится в ведении не драйвера, а потенциального layer'а (или lib'а) daqmx_lyr. Так что к приватным полям в драйверовом privrec'е никакого доступа не имеет.
      2. Task'ов более одного, а счётчики у каждого должны быть свои.

      Решение напрашивается такое:

      • При необходимости подобной фильтрации заводить в драйвере массив счётчиков, [NUMTASKS], указатель на который класть daqmx_context_t (надо добавить поле); и если оный указатель!=NULL, то и проводить все проверки.
      • А если конкретному драйверу для части task'ов проверки нужны, а для другой части -- нет, то он может счётчики, соответствующие нетребовательным, просто не трогать -- они будут всегда ==0 и всегда совпадать.
    • Делаем.
      • Добавляем поле daqmx_context_t.tctrs
      • В privrec добавлен массив tctrs[NUMTASKS], указатель на который и сбагривается (совсем аналогично dpls[]).
      • StartMeasurements() делает me->tctrs[TASK_N_ADC]++ непосредственно перед DAQmxStartTask().
      • Добавлено поле event_info_t.ctr (плюс rsrvd для кратности).
      • EventCB() пишет туда значение соответствующего счётчика, ...
      • ...а event2pipe_proc() проверяет.
    • @~15:00, дорога домой (на пляж), около мыши: ещё технологическая засада будет с реализацией STOP: надо ж вернуть все запрошенные каналы, с флагом CXRF_IO_TIMEOUT. В pzframe_drv это делается "как надо" путём последовательного вызова abort_measurements() (нулит буфера) и prepare_retbufs(), после которых уже простой возврат по готовым спискам.

      А в драйвере daqmx/pxi6363 такой инфраструктуры ещё нету.

      Видимо, пока достаточно обойтись тупым последовательным (в цикле) возвратом всех каналов.

    • Вводим канал STOP, каналы-параметры STARTS_SEL, TIMING_SEL, OSC_RATE (последний -- double).
    • Пытаемся разобраться с тем, как управлять стартами и таймингами (клоками). Сложно и запутанно!!!
      • В "NI-DAQmx 18.1 Linux C Examples/Analog In/Measure Voltage/" есть толпа примеров.

        Из них практически нифига не понятно.

      • В учебнике "Сбор данных и согласование сигналов" (320733N-01) немножко про общие принципы синхронизации написано, но скорее "для чайников, а не программистов".

        И с примерами оно не очень связывается -- например, буквосочетания "APFI" там нет вовсе.

      В конце концов -- составлен файлик notes/20190704-NI-DAQmx-EXAMPLES-timings.txt, содержащий выдержки из всех примеров, так что можно окинуть всё компактно одним взглядом.

    05.07.2019@утро-пляж: некоторые мысли:

    • @спускаясь-вниз, уже внизу: а может, всякие источники стартов "APFI0" -- это как раз сигналы по PXI'ной магистрали?

      09.07.2019: а вот и нет: судя по "X Series User Manual" (6363 относится к "X Series"), "APFI<0,1>" -- это специальные входы для "аналогового" (отсюда "APFI") триггера.

    • @сам пляж, бродя взад-вперёд: да, фиговато, что нет явной возможности указать диапазон, хотя сам девайс, судя по PXIe-6363 Specifications, имеет диапазоны 0.1V, 0.2V, 0.5V, 1V, 2V, 5V, 10V (стр.2).

      Но если считать, что указание вольтажа (DAQmxCreateAIVoltageChan()'овы параметры minVal/maxVal, плюс DAQmxSetAIMax()'ом) косвенно влияет -- т.е., переключает на самый мелкий диапазон, достаточный для указанного -- то можно "наверх" давать ручку со списком диапазонов, а каналам ставить значение в зависимости от указанного.

      Понять бы только точно, ЧТО указывать для включения некоего диапазона: например, для 5V -- 5.0, 4.9, 5.01?

    07.07.2019: pxi6363_drv.c переехал из frgn4cx/daqmx/drivers/ в hw4cx/drivers/daqmx/.

    • Файл frgn4cx/daqmx/FrgnRules.mk превратился в hw4cx/drivers/daqmx/DAQmxDefs.mk, в нём теперь определяется символ MAY_BUILD_DAQMX_DRIVERS в =YES при "найденности NIDAQmx.h, а hw4cx/drivers/Makefile его include'ит и анализирует возможность сборки.

      Так что при необходимости форсить сборку надо указывать в командной строке MAY_BUILD_DAQMX_DRIVERS=YES, а если принудительно запретить -- то, соответственно, MAY_BUILD_DAQMX_DRIVERS=NO.

    • Ключевой причиной переезда стало желание не-кривить в hw4cx/pzframes/Makefile со ссылкой на frgn4cx/include/.
    • Ну и всё равно ж давно ясно, что никакого cda_d_daqmx.c сделать не удастся, так что и смысла держать daqmx/ в frgn4cx/ нету.

    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, чтобы автоматически выполняло настройку всех каналов по таблице их свойств.

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

cda_d_dstp:
  • 27.05.2019: создаём раздел, посвящённый модулю для доступа через NI'ный "datasocket" (он же "dstp" (DataSocket Transport Protocol?)).

    Просто чтоб был -- учитывая отсутствие (доступной) реализации C API для DataSocket, возможность реализации самого модуля весьма призрачна.

  • 27.05.2019: это специализированное место для записи ссылок на информацию, полезную для программирования этого модуля.

    27.05.2019: ищем информацию по-простому -- гуглим на тему "datasocket linux".

    Что найдено:

    • Судя по обсуждению "Accessing data socket server from linux client" на NI'ном форуме, от 2011 года -- фиг.

      Последовательные соображения-ответы:

      • "Unfortunately the only C api to datasocket is for CVI."
      • "CVI does work on Linux: http://sine.ni.com/nips/cds/view/p/lang/en/nid/203157"
      • "Unfortunately, while the CVI environment is supported on Linux, the interfaces for reading and writing DSTP data were implemented using Microsoft's ActiveX technology, so that code is not available on Linux."
    • Отдельная страница "Which Products Support DataSocket" (дурной NI'ный сайт её не показывает из-за русского, так что надо смотреть копию из гуглокэша). По состоянию на "Updated May 23, 2018" --
      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" не существует.

cda_f_fla:
  • 27.06.2007: давнее размышление -- как бы именно поразумнее определить excmd_t?

    27.06.2007: с одной стороны, хочется иметь данный тип небольшим, чтобы не тратить зря память; с другой -- хочется упхать в него побольше.

    И есть соображение: ведь на 32-битовых платформах тип double занимает 8 байт, а указатели и int -- 4. Так что вполне можно запихивать параметры для LAPPROX прямо в arg.

    Кроме того, из соображений выравнивания все равно разумнее сделать тип размером 16 байт, а не 12, как сейчас.

    И, наконец, cmd надо переделать с char'а на int, и как-нибудь покреативнее пользоваться битиками. (Например, можно кроме бита IMMED добавить еще поле "исполнять при...", в котором указывать желаемую маску условных битов, как в некоторых процессорах. Но это уже так, левые размышления :-).)

    02.07.2014: да. А именно:

    1. Сам тип назван fla_cmd_t.
    2. Состоит из 2 штук int32, за которыми следует union с double внутри.
    3. Поле cmd стало int32, а флаги (включая IMMED) вынесены в отдельное поле flags.
  • 30.05.2014@Снежинск-Икарус-на-полигон: (в продолжение записи в IDEAS.html за 15-02-2011 о ключевой необходимости формульного языка) Технически есть обходной путь -- устроить возможность делать вставки прямо на "коде".
    • Например, префикс _code, за которым идёт сама "ассемблерная" команда, а после неё аргумент (и можно сделать принудительное окончание по NL, а не по ';').
    • И дальнейший парсинг (параметра) вести по таблице команд -- по той же самой, которую использует и сама исполняющая виртуальная машина.
    • Кстати: компиляцию формул следует делать двухпроходной: на первом проходе подсчитывает объём (включая строковые константы), потом аллокирует и на втором проходе уже заполняет.

    03.07.2014: да,

    • Такая архитектура вчера заготовлена (остались мелочи, само складирование).
    • Даже сама исполнялка (читай -- виртуальная машина) уже сделана, причём с фичей "sleep". Пока только команд поддерживает немного (но это вопрос техники).
    • И писать кучу-кучу с префиксированием "_code" оказалось неудобно, так что введена еще команда _all_code, переключающая парсер в режим "принудительное _code до конца" -- после неё надо писать уже просто опкоды, без префикса.
    • Кроме того, сделана возможность писать кучу команд и опкодов через ';' -- так сильно удобнее (а парсер на эту тему у команд и опкодов общий).

    К вечеру: и складирование тоже сделано.

    15.09.2015: был косячок: два подряд ';' приводили к ошибке "Syntax error: non-letter at the beginning of command".

    Решено тривиально -- в кусок "пропустим whitespace, \n, \r" в начале парсинга команды добавлено также пропускание ';'.

  • 30.06.2014: начинаем наполнять cda_f_fla.c.

    10.07.2014: "базовый набор" команд сделан, как и сама "виртуальная машина".

    Архитектура -- запланированная давным-давно, с отдельными функциями на каждый опкод (за исключением OP_RET) и таблицей, содержащей кроме имени опкода и ссылки на его функцию-исполнитель еще некоторые свойства:

    1. Количество входных параметров (сколько берёт из стека).
    2. Количество результатов (сколько кладёт в стек).
    3. Флаги -- конкретно сейчас только FLAG_IMMED и FLAG_NO_IM, сигнализирующие о необходимости immed-параметра или о его запрещёности соответственно.
    4. Тип immed-параметра -- double, string, chanref, varparm; используется "ассемблером" _code для парсинга.

    НЕ сделана пока условная инфраструктура (case, test, cmp, goto/label).

    14.08.2014: продолжаем наполнение командами:

    • OP_RET сделан условным -- на замену v2'шному BREAK.
    • Чуть продвинутая арифметика -- SQRT, PW2, PWR, EXP, LOG (или это уже алгебра?).
    • Булевскости -- BOOLEANIZE, BOOL_OR, BOOL_AND
    • LABEL и GOTO. Они вначале парсятся как обычные команды с обязательными STRING-параметрами, а потом по уже готовой формуле делается еще проход, где:
      1. У OP_LABEL сбрасывается флаг IMMED -- чтоб в стек не мусорили, становясь эффективно NOOP'ами.
      2. Для OP_GOTO делается поиск по всей формуле команд OP_LABEL с такой меткой. При найденности прописывается arg.displacement, а при ненайденности команда превращается в OP_NOOP с flags=0.
    • CASE и TEST.
    • ...осталось CMP в его разных подвидах, только не очень ясно, как лучше делать (парсинг+реализацию).

    14.08.2014@вечер-пляж: да чего там думать -- делать просто 6 разными опкодами, и 6 раздельными реализаторами. А чтоб не дублировать код -- реализаторы определять одним #define-макросом, там ведь различия только в названии да в операции сравнения.

    15.08.2014: так и сделано. Проверено -- работает.

    • DUP еще не хватало -- тоже добавлена.

    02.10.2014: еще по мелочи:

    • Чтоб к varchan'ам можно было доступаться уже прямо сейчас, мелкое расширение семантики OP_GETCHAN и OP_PUTCHAN (точнее, ARG_CHANREF): если имя начинается с '%', то оно вместо 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: ох ёлки, зачем "не прерываться..."? Надо просто поиск выполнять также и в момент РЕГИСТРАЦИИ метки. И если он вернёт наличие -- то это дубль.

    • Неудобно, что строковые параметры -- включая аргументы PRINT_STR и PRINT_DBL -- парсятся все одинаково, в стиле CHANREF.

    02.11.2018: а еще НЕ возвращается наверх ошибка при обломе регистрации канала/varchan'а. Просто в arg.chanref складируется результат -1 и молча идётся дальше.

    Из-за этого чуть ли не полдня убил, пытаясь разобраться со "странным" поведением драйвера trig_exec, регистрирующего явно неработоспособную формулу.

    02.11.2018: исправляем и то, и другое:

    1. Проверка дубля метки: поскольку отдельная обработка опкодов в процессе регистрации отсутствует (только в самом конце есть отдельный проход по OP_LABEL и OP_GOTO), а селективность лишь по типам аргументов, то пришлось вставить дополнительную проверку в ветвь ARG_STRING -- там если opcode==OP_LABEL, то делается попытка найти такое же имя, и при успехе генерится ошибка "duplicate label".
    2. При обломе регистрации возвращается ошибка "unknown channel ...".
  • 10.07.2014: изначально планировалась расширяемость виртуальной машины формул (как и языка). Вопрос в реализации -- КАК?

    10.07.2014: у виртуальной машины, в её нынешней модели (таблица, из которой берётся описание команды) есть два варианта:

    1. Разделить опкод на 2 части: младшая -- собственно опкод, старшая -- номер таблицы (0 -- основная).
    2. При регистрации дополнительных операций сливать все таблицы вместе, и дальше уж работать как сейчас.

    Первый вариант проще в реализации -- ничего сливать не надо, но противнее в исполнении (постоянно дрюкаться с селекцией таблиц).

    Второй же вариант сложнее в реализации ("добавление" к таблицам), зато процесс исполнения получается проще и беспроблемнее.

    Похоже, лучше выбрать второй вариант.

  • 30.07.2014: надо бы заложить возможность "зависать в процессе" не только по команде sleep, но и, например, по какой-нибудь условной конструкции -- вроде "дождаться, пока не придёт значение такого-то канала", или "пока не выполнится такое-то условие".

    Так что нефиг использовать sleep_tid>=0 в качестве флага "мы ждём...".

  • 30.12.2015: не хватает опкода LAPPROX. Для сварки начинает надобиться. Как будем реализовывать загрузку таблицы?

    01.01.2016: да, КАК? Учитывая, что формулы процессятся далеко ПОСЛЕ загрузки subsys-файла, то нет решительно никакой возможности доступиться до той же директории, откуда взят сам subsys-файл (а именно оттуда желательно бы мочь брать текст таблицы).

    02.01.2016@утро-душ: напрашивается сделать lapprox-таблицу не параметром формулы, а свойством группировки или контекста, и чтоб формула уже просто ссылалась на неё. Заодно "решается" проблема множественных загрузок одной таблицы -- она реально будет грузиться единожды, а из формул ссылки на разные столбцы.

    Но остаются 2 крупные проблемы:

    1. Всё-таки как производить чтение?
      • Очевидный вариант -- директива
        lapprox_table МЕТКА ИМЯ_ФАЙЛА
        не особо подходит, т.к. у Cdr_via_ppf4td.c нет доступа к текущему пути парсинга.

        @на-работе-в-обед: или есть? Ведь существует ppf4td_cur_ref()!

      • Другой вариант -- использовать для загрузки прямо подстилающий препроцессор (обычно m4).

        Для этого придётся делать директиву "скобками" -- например, парсить до закрывающей ')', воспринимая NL как переход на следующую строку таблицы.

        ...не особо-то красиво...

    2. И доступаться-то из формулы как? ...видимо, делать какой-то cda_p-вызов, передавая ref в качестве параметра, а уж cda_core по нему доберётся до контекста с таблицами.

    P.S. Сопутствующие вопросы решаются просто:

    1. все загружаемые таблицы складываются в специальные секции (например, тип DSTN_LAPPROX="lapprox"),
    2. регистрируются при создании контекста (тупо ВСЕ секции такого типа), с аллокированием места,
    3. грохаются при удалении контекста (а из подсистемы удалится при её гроханьи -- CdrDestroySubsystem() делает safe_free() всем p->data).

    03.01.2016: вариант -- можно сделать ОБЕ возможности.

    • Если PeekCh() возвращает '(', то считать это вариантом (СОДЕРЖИМОЕ), с разбивкой по строкам NL'ями.
    • Иначе считать следующий параметр именем файла для чтения, который открывать при помощи того же ppf4td с протоколом "plaintext", ища через findfilein().
    • Парсинг же выполнять в любом случае одной и той же функцией, передавая ей параметром либо основной контекст, либо этот plaintext'овый (ну еще указывать, считать ли ')' за закрыватель).

    Отдельный вопрос остаётся в менеджменте самих таблиц.

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

      18.04.2018: замечание: раздвижка понадобится только при всасывании ПОЛНОЙ таблицы. Если же нас интересует только выжимка с 2 колонками (X и Y) -- то не надо; в v2 именно так и сделано.

    • Поскольку для непосредственного использования в LAPPROX таблицы должны быть сортированы по ключевой (1-й) колонке, то в формулы надо делать (аллокируя) выжимку из базовой таблицы, состоящую всего из 2 колонок.
      • Структура пусть используется та же -- для неплождения сущностей.
      • А грохаться эти доп.аллокированности будут формуловыми destroy().
    • Сама операция аппроксимации пусть будет реализована библиотечной cda'шной функцией.

    Резюме: да, муторновато, но как всё делать -- уже ясно и при надобности реализуемо за денёк.

    21.03.2018@дорога-на-работу после обеда, около ИПА: пришла необходимость реализовать LAPPROX -- надо сварку переводить на v4, и потому усиленно думаем на тему "как".

    Без вчитывания в написанное 2 года назад, просто мысли (возможно, что и повтор):

    • Желательно сделать, чтоб таблицы можно было указывать как ссылками на файлы (тогда надо указывать и номера колонок X и Y), так и "непосредственно" просто список дуплетов).
    • И, видимо, как в исходном .subsys-файле, так и прямо в самой формуле (точнее, прямо в команде lapprox).
    • Синтаксис -- как различать варианты файл/immediate:
      1. Immediate-список заключается в скобки; следовательно, если первый символ скобка -- это оно, иначе ссылка на файл либо на указанное в группировке.
      2. Указанное в группировке -- пусть имеет префикс ':'.

        Мнемоника -- двоеточие в именах/путях ручек означает "подняться на уровень выше"; группировка для формулы как раз "выше".

        Но тут, возможно, тоже надо как-то указывать номера колонок; также через двоеточия -- ":X:Y:ИМЯ_ТАБЛИЦЫ"?

      3. Из файла -- всё остальное. И как-то надо указывать номера колонок.

    21.03.2018@лыжи-вечером: (пошел поздно, в полшестого, торкнуло и было вдохновение)

    • @серединка-3-й-2-ки("перешеек"): LAPPROX скорее всего потребует, во-первых, отдельного cda_math.c; во-вторых, более хитрого взаимодействия с основной частью cda_f_fla (возможно, как "подключаемые функции").
    • @серединка-4-й-2-ки("перешеек"): а можно выдрепнуться и сделать чтоб парсинг из встроенной в формулу строки делался тоже через ppf4td: просто специальный парсер (похожий на ppf4td_mem) и как-нибудь ему мочь указывать, что просто голая закрывающая скобка (но не внутри формул; точнее, не внутри формул, которые будет потом парсить ppf4td по get_int/get_double) -- это EOF.
      • Как это определять -- отдельный вопрос: на уровне ppf4td-плагина эти вещи неразличимы. Всё, что приходит в голову: поскольку в этом коде обычно известен ТИП парсера (вот этот внутренний), то можно считать, что imp_privptr указывает на его privrec, и там взводить флажок "верни EOF".
      • Кроме того, надо как-то возвращать "наверх" точку, в которой парсинг закончился и надлежит "вернуть входной поток хозяину" -- т.е., символ после ')'.

      Дополнение: это немножко шибко замудрённо, потому что получится, что надо один и тот же "мозг" парсинга иметь и в 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).

    • @начало-5-й-2-ки: Тем не менее, это идея хорошая, поскольку тем самым можем пользоваться всеми бонусами ppf4td.
    • @конец-4-й-2-ки (ближе к концу, после горочки с овражком): в качестве контекста, в котором хранятся распарсенные таблицы для LAPPROX (уже просто из {x,y}, а не кучи колонок) можно использовать cda'шный же контекст, ибо он к этому худо-бедно приспособлен.

      22.03.2018: НЕприспособлен, кстати, а МОЖЕТ (и вынужден будет) быть приспособлен: придётся добавить возможность хранения произвольного количества произвольных "секций". Вариант -- хранить ссылки на данные из Cdr'овой subsys.

    • @(обдумывалось на протяжении почти всего лыжехождения): остаётся нерешенным вопрос "менеджмента памяти и структур данных". Соображения:
      1. Готовые сведения о таблице надо хранить в некоей структурке-дескрипторе.
      2. В информации, хранящейся в fla_val_t -- в "ссылке" на таблицу -- надо как-то указывать, следует ли делать ей "free()". Т.е., кто "хозяин" таблицы -- эта формула, или же "группировка" (точнее, cda-контекст).

    22.03.2018@вечер-дома: общее впечатление: всё описанное, конечно, красиво и глобально...

    • Но стоит ли городить такую мощную общую инфраструктуру ради ОДНОГО-ЕДИНСТВЕННОГО использования на сварке?
    • Может, стоит придумать какое-нибудь решение попроще -- не общее, но чтоб с минимальными трудозатратами решало КОНКРЕТНУЮ задачу?

      Пусть даже файл грузится тупо всегда из известного списка путей (типа ~/4pult/settings/common/), игнорируя путь самого .subsys'а.

    25.03.2018@дома: да, надо просто взять да сделать, максимально просто и быстро:

    • Всё делать прямо в cda_f_fla.c.
    • В частности, 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:

    1. Она возвращает единый блок памяти, содержащий в начале дескриптор (excmd_lapprox_rec), а за ним таблицу.
    2. В таблицу сразу выфильтровываются лишь 2 числа, из позиций xcol и ycol.
    3. Следовательно, никакую "раздвижку" (как думалось 03-01-2016) делать не надо.
    4. Кстати, поскольку читает она файл fgets()'ом, то и никакое "skip to end of line" не требуется (а просто "goto NEXT_LINE").

    Теперь собственно реализация.

    • Делаем в ПРОСТЕЙШЕМ варианте, чисто для решения задачи на сварке.

      Поэтому всё строго локально внутри cda_f_fla.c.

    • Тип описателя таблицы взят из v2, но под именем lapprox_rec_t и с удалением реально неясно избыточного поля-указателя pts -- оно всё равно ВСЕГДА указывает на его же data[].
    • В fla_val_t добавлена альтернатива lapprox_rp с этим типом.
    • Опкод OP_LAPPROX.
    • Взяты из v2 helper'ы:
      • 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: доделываем, идя в обратном порядке -- от конца к началу, и заодно от простого к сложному:

    1. Освобождение -- ну тут всё просто и было очевидно ещё при создании cda_f_fla_p_destroy().
    2. Использование -- proc_LAPPROX(), по образу и подобию v2'шного.
    3. Парсинг:
      • Новый тип параметра назван ARG_LAPPROX.
      • Синтаксис выбран так, чтоб быть совместимым с описанным 21-03-2018 (хотя и туманно) вариантом для "из файла":
        lapprox X:Y:FILEREF
        Т.е. -- по факту строка.
      • Поэтому парсинг подселён к прочему строковому -- ARG_STRING/ARG_CHANREF/ARG_VARPARM.
      • Там последовательно парсится с тучей проверок, на все двоеточия.
      • ...предусмотрен еще сокращённый вариант синтаксиса --
        lapprox =FILEREF
        при котором должно приниматься xcol=1,ycol=2; но, поскольку в weld_mes_up_mv2mka.lst колонки поменяны местами -- xcol=2,ycol=1 -- то такой синтаксис там бесполезен и потому эта ветвь закрыта "&&0".
      • Реальный парсинг выполняется только на 2-м проходе (stage==1), как и прочие "регистрации".
      • При ошибке загрузки таблицы команда переделывается в CALCERR(1.0).

    Проверяем.

    • На baachi.subsys...
    • ...после исправления нескольких мелких косяков вроде парсинг выглядит рабочим. Да вот только надо б проверить еще и на живом железе -- убедиться в реальной работе аппроксимации.
    • Проверил на "симуляторе": в качестве исходного X для аппроксимации берётся rw-канал из локального сервера, в который пишем разные значения.

      Да, всё работает.

    Но при попытке проверить "более простыми способами" вылезла пара странностей:

    1. Не получилось с "эмуляцией" в варианте, когда данными обмениваемся через %-канал (по факту varchan), а в качестве метронома (чтоб обновления по циклу приходили) используем некий иной канал, но настоящего сервера, в варианте "getchan NNN; pop;" в начале формулы:
      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
      
      Почему-то уставляемое в ручке EMU значение через цикл откатывается на 0.
    2. Не удалось скормить формулу cdaclient'у. Команда
      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() -- ведь пробелы рассматриваются как разделитель между именем канала и значением для записи.

  • 27.04.2016: что-то не то с определением "raw_useful": конкретно в новой linvac, где у 124'шных источников Ialm и Ualm эмулируются формулами прямо в ручках, эти формулы, читающие 3 канала (2 раза lim и 1 раз mes), почему-то всё же отображают в Chl_knobprops'е строку "Raw", причём в ней значение mes (которое в формуле и не первое, и не последнее, а посерединке).
  • 04.05.2016: надо уметь делать усреднение по N циклам, чтобы N не было фиксировано (как у DEVN и MINMAX в Cdr), а могло бы указываться.

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

    1. Где хранить данные для усредения?
      • Формула должна быть совершенно автономна.
      • Очевидно, хранить данные надо в каком-то внутреннем буфере.
      • Делать по личному буферу на каждую команду -- извращение.
      • Следовательно, надо хранить это всё в общем буфере, который содержит и транслированную формулу, и строки ("auxdatabuf").

        Только данные -- имеющие кратность выравнивания 8 (по double) -- надо хранить ПЕРЕД строками.

    2. Надо как-то добывать число циклов для усреднения.

      bigfile-0001.html, 23-01-2006: "уже сейчас есть потребность иметь дополнительный числовой параметр (сейчас -- число циклов) для LOGT_DEVN, LOGT_MINMAX, возможного будущего LOGT_AVG и им подобных" :-).

      • Явно оно должно указываться параметром в операции, но сохраняться статически, а не как прочие числовые параметры "класться в стек".
      • Надо будет ввести новый тип параметра -- вроде "ARG_FIXED_INT".
      • ...а куда сохранять -- второй вопрос. В fla_val_t места на 2 поля (число циклов, адрес таблицы) не хватит; видимо, надо хранить "адрес" блока данных, где сначала будет идти число, а потом массив чисел.
    3. Также надо б где-то хранить текущее количество занятых ячеек в буфере (изначально там будет пусто, потом 1, 2, ...).

      Видимо -- в том же "заголовке" перед массивом, рядом с количеством циклов для усреднения.

    ...ох вот времени нету на это. А тут где-то с день работы...

    15.05.2021: и опять засветилась старая потребность --

    1. "уметь выполнять операции DEVN и MINMAX прямо в формулах", и опять для термостабилизации.
    2. И надо уметь также и УСРЕДНЕНИЕ -- AVG. Это вроде несложно -- тот же код/алгоритм, что и в DEVN, только отдавать прямо значение avg.
    3. И количество точек бы уметь указывать.

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

    Есть несколько хотелок в другую сторону, связанных с потребностью уметь публиковать формулы как каналы:

    • Надо бы уметь давать им настроечные каналы-параметры. Вот как?
      1. Создать отдельный драйвер "const_drv": чтоб можно было указывать там значение по умолчанию, и чтоб "режим работы" -- константа или изменяемое -- указывалось бы типом канала (r или w).
      2. Оные "настроечные" каналы держать прямо в том же самом драйвере, следующими каналами после 0-го.

        Тут вопрос будет в том, как к ним из формул обращаться: лезть интроспекцией в namespace, получать количество доп.каналов и их имена? А из формул к ним как обращаться -- маппировать на "параметры"?

    • А ещё хоцца иметь возможность указывать количество циклов для усреднения/отклонения/расчёта максимума-минимума в ручках, вместо умолчательных NUMAVG=MMXCAP=30. Вот только как -- некоторый вопрос. Видимо, добавить параметр в dataknob_knob_data_t.kind_var.{devn,minmax}? Но как ПАРСИНГ организовывать -- вопрос: ведь он должен быть доступен ТОЛЬКО для ручек с соответствующим флагом, т.к. это всё в union'е...
    • Пора уже для formula_drv завести свой раздел.
    • Вопрос о "парадигме" работы этого драйвера:
      1. По обновлению?

        Тогда надо всё же научиться ловить -- КОРРЕКТНО! -- callback'и от вовлечённых в формулу каналов.

      2. По запросу -- по DRVA_READ -- просто каждый раз исполнять её заново?

      Кстати, для (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.hcda_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().

  • 04.09.2016@Снежинск-каземат-11: оказалось, что тут напрочь отсутствуют "врЕменные регистры", имевшиеся в v2.

    Замечено при воспроизведении в liu.subsys функционала db_liu.h. Пока выкрутились использованием обычных %ПЕРЕМЕННЫХ.

  • 23.03.2018: оказывается, в нынешней версии отсутствует команда вычисления полинома -- OP_POLY.

    Очевидно, никогда не требовалась. Расследование показало, что она использовалась (первый и единственный раз!) в программе drc -- это софтина для автоматизации "запяткинского клистрона", из которой в конечном итоге и вырос CX. И на самом свежем файле, в котором применяется OP_POLY -- curcx.20030415/src/xmclients/drc.c, дата -- 01/02 августа 2002.

    Вспомнилось (и было проведено расследование) по следам вчерашнего разговора с Роговским (на тему "что и как есть в CXv4", в связи с затеваемым переводом пикапов накопителя). При объяснении работы цепочек {R,D} он спросил -- "а только линейно, полиномов не бывает"?

  • 06.11.2018: очень полезен будет опкод "comment" -- чтоб можно было вписывать произвольный текст посреди кода формулы.

    Смысл в том, что пользоваться обычными комментариями '#' нельзя -- они плохо взаимодействуют с кавычками и с переносами строк через '\NL', что является обычным для почти любого использования формул (что в .subsys'ах, что в devlist'ах).

    08.11.2018: сделано.

    • Опкод OP_COMMENT, команда comment.
    • Параметр -- ARG_STRING (причём он НЕобязателен (flags=0)).
    • Следовательно, текст комментария надо писать в кавычках либо апострофах.
    • В свойствах указано nargs=0,nrslt=0.
    • Хоть параметр и сохраняется в auxdatabuf'е, но в конце cda_f_fla_p_create() команде принудительно сбрасывается FLAG_IMMED (аналогично опкоду OP_LABEL).
    • И, аналогично OP_LABEL'у же, в описателе прописано proc=NULL.
  • 07.11.2018@лыжи: а еще нужна бы команда для проверки "не NAN ли" -- cmp_if_isnan, чтоб отсеивать NAN'ы, лежащие в каналах "по умолчанию".

    Возможные ситуации, когда это может потребоваться:

    • Если в trig_exec-устройствах в состав триггера входит несколько каналов, и один-то обновился (вызвав срабатывание), а прочие еще нет -- в них будет NAN. Тогда надо в формуле-исполнителе этот NAN обнаруживать и НИЧЕГО НЕ ДЕЛАТЬ, а не записывать бредовое число в вычисляемые каналы.

      (Это и явилось побудительным мотивом.)

    • Любая формула, зависящая от нескольких каналов -- там тоже часть может быть неинициализированной. В простом случае всё пройдёт как надо -- NAN "распространится" и попадёт в результирующее значение. Но если формула выполняет запись куда-либо, то зачастую такую запись бредовых значений лучше просто пресечь.

    08.11.2018: сделано.

    • Опкод -- proc_CMP_IF_ISNAN, команда cmp_if_isnan.
    • Скелет proc_CMP_IF_ISNAN() скопирован с proc_TEST(), только сравнение "val != 0" заменено на "isnan(val)".
    • Параметры тоже аналогичны -- nargs=1,nrslt=0.
    • Но flags=FLAG_NO_IM, т.к. immediate-параметр в данном случае выглядит странно.

      Хотя некритичным было бы оставить и flags=0.

  • 09.11.2018: нехорошо, что НЕЛЬЗЯ сделать "refresh; sleep N" -- чтоб можно было запустить некий процесс и СРАЗУ же просигнализировать об этом скрину.

    09.11.2018: посмотрел-посмотрел я на то, что и как в cda_f_fla.c делается... Эх!

    Чтоб совсем "красиво" -- неохота заморачиваться, смысла нет.

    Сделано по-простому: прямо в proc_SLEEP() к возвращаемому CDA_PROCESS_FLAG_BUSY дополнительно OR'ится при надобности CDA_PROCESS_FLAG_REFRESH.

  • 02.05.2024: делаем отложенное удаление формул и возможность формулам "косвенно делать STOP самим себе" (в cda_f_fla.c -- первое изменение с 09-11-2018), всё по проекту из раздела о sim_dir_drv.c за сегодня.

    02.05.2024: работы:

    • Модификация API в cdaP.h:
      • Тип 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.
    • Работа с этим в cda_core.c::RlsRefSlot() крайне проста: теперь результат вызова проверяется, и при !=CDA_FLA_P_DESTROY_SUCCESS делается ri->fla_privptr = NULL -- чтоб защитить от free()'нья ниже.
    • Ну и главное -- в собственно cda_f_fla.c:
      • В 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, то:
        1. Вызывает cda_f_fla_p_destroy();
        2. делает free(fla);
        3. возвращает 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".

cda_f_tcl:
:
cxlib:
Общие вопросы:
  • 20.06.2007: как уже месяц ясно (см. bigfile-0001.html за 20-05-2007), cxlib будет разделен на "мозги" и "транспорт" (могущий быть разным, как SIGIO-, так и fdiolib-based). Плюс, "утилиты" -- типа cx_str* и cx_parse_chanref() -- стоит вынести в отдельный модуль.

    21.06.2007: Итого -- получаются такие модули:

    • cx_client.c собственно "мозги" реализации протокола CXv4.
    • cx_tsigio.c транспорт на основе SIGIO.
    • cx_tfdiolib.c транспорт на основе fdiolib.
    • cx_utils.c вспомогательные функции.

    01.07.2009: с учетом соображений в разделе "cx_tsigio", не будет никаких вариантов "транспорта", а всегда станет использоваться fdiolib. И, собственно, понятие "транспорта" при этом практически исчезает, поскольку даже о разборе протокольных заголовков заботится она же, а на долю cxlib'а остаются только операции "отправить пакет" и "что же это у нас пришло".

    Посему -- остаются только cx_client.c и cx_utils.c, которые, кстати, уже и зачаты еще два года назад.

    02.07.2009: точнее, понятие "транспорта" будет -- это та часть cxlib'а, которая устанавливает соединение и шлет/принимает пакеты, НЕ ВНИКАЯ в их содержимое. Так что, строго говоря, cx_client.c будет состоять из 3 разделов:

    • Менеджер "соединений" -- динамического массива структур CxConnection.
    • Транспорт.
    • Собственно "мозги".
  • 23.07.2012: все файлы lib/cx/cx_* переименованы в lib/cxlib/cxlib_*.
  • 08.10.2012: с нынешней архитектурой -- "всё на fdiolib, но работает либо синхронный режим, либо асинхронный" -- есть одна сложность: нельзя использовать СМЕСЬ.

    А теоретически может понадобиться -- например, из нормальной программы захочется вдруг некое значение добыть ПРЯМ ТУТ (чтоб пока не вернётся -- зависнуть).

    08.10.2012: вот как такое реализовывать на cxscheduler'е, а?

    • И "контекстов"-то тут нет, в отличие от Xt -- там можно б было доп. контекст создать...
    • Пытаться sl_main_loop() бить на части, чтоб его можно было частями вызывать (ну и глобальный флажок вводить, чтоб оно после таких выдрепонов прерывало бы цикл по fd)?
  • 25.07.2013: переводим со схемы "privptr" на "uniq, privptr1, privptr2".

    25.07.2013: сделано без особых проблем.

    Проблемы будут при обратнопортировании слегка проапдейченного кода из v2 (где БЕЗ uniq).

    ЗЫ: но cx_do_cleanup() пока нету.

    25.11.2013: cx_do_cleanup() сделан.

  • 25.11.2013: перетряхиваем список кодов ошибок CEnnn.

    25.11.2013: пока чуть-чуть:

    1. Добавлен отдельный код CESRVSTIMEOUT -- теперь серверов пакет CXT4_TIMEOUT маппируется на неё (чтоб различать "handshake timeout", сообщенные клиентской и серверной стороной).
    2. Давно устаревшая даже в v2 CESYNCINSELECT убрана, и её место занято...
    3. CESRVINTERNAL -- на неё маппируется CXT4_EINTERNAL.
cxlib_client:
  • 03.07.2009: ввел модули-адаптеры cx_async.c и cx_sync.c, и с ними линкуются библиотеки libcx.a и libcx_async.a -- по вчерашнему проекту из раздела cx_tsigio.

    10.02.2015: некий вопрос -- а нужна ли реально libcx_sync.a?

    Учитывая, что и cx-console будет по асинхронной модели, синхронным остаётся только cxclient -- а) чья нужность условна; b) который тоже б лучше перевести на асинхронность.

  • 03.07.2009: начинаем населять cx_client.c и наполнять cxlib.h реальным описанием API.

    11.09.2009: за последние несколько дней сделана работоспособная основа:

    1. "База #1" -- менеджмент массива соединений (точнее, в основном оно скопировано с аналогичного куска в cda_api.c).
    2. "База #2" -- взаимодействие с fdiolib. Реакцией на события занимается большая, но единая и легко воспринимаемая ProcessFdioEvent().
    3. Преодолен самый тонкий момент -- то, как именно обрабатывать окончания API-функций в синхронном и асинхронном режимах.
    4. Реализована цепочка установления соединения -- 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 валило вопли

    Warning: Select failed; error code 9"
    (9=EBADF), strace показал
    select(4, [0], [], [], {0, 0}) = -1 EBADF (Bad file descriptor)
    -- еще бы, ведь 0-го дескриптора к тому моменту не существовало.

    В общем -- тщательнее надо инициализировать свежеотведённые слоты.

  • 04.07.2009: вопрос -- а можно ли как-то применять неблокирующийся резолвинг имён? Чтобы cx_open() совсем не зависал?

    04.07.2009: по крайней мере -- введём специальную стадию для резолвинга, и чтобы если такой резолвер есть, то запускаем его и возвращаем управление клиенту.

    11.06.2013: да, наконец-то нашел хоть сколько-то стандартную реализацию: getaddrinfo_a().

    • Оно было предложено U.Drepper'ом еще чёрт-те-когда (первое упоминание я нашел за 2004г., кой-какое обсуждение "getaddrinfo is not just for IPv6" за март 2007-го, там есть ссылка и на документ "Asynchronous Hostname Lookup API" (локальная копия)).
    • Есть оно как минимум в RHEL-5.2 -- в /usr/include/netdb.h прототип имеется, а в FC18 уже и man-страница присутствует. И, судя по последней, оно появилось аж с glibc-2.2.3, так что даже в RH-7.3/glibc-2.2.5 (2002!) должно быть; прототип присутствует, а вот работает ли -- надо проверять, поскольку сам символ getaddrinfo_a живёт в очень отдельной libanl (Asynchronous Name Lookup), весьма вероятно (судя по nm -a), требующей multithread'овости.
    • По дороге, кстати, наткнулся на некую библиотеку c-ares -- "library for asynchronous name resolves". Но, учитывая наличие стандартного API, её вряд ли имеет смысл пробовать.
    • И еще в libevent есть свой модуль на эту тему -- см. "Using DNS with Libevent: high and low-level functionality". И тоже вряд ли нужно.
    • Кстати, слегка в сторону: любопытное обсуждение "Kernel events without kevents" за 2007г, на которое наткнулся по ходу поиска. Там упоминается толпа механизмов асинхронного I/O под *nix и Форточки.

    20.12.2017: чуть в продолжение:

    • Да, multithread'овости явно требует: в статической libanl.a nm показывает присутствие ссылок на всякие pthread_mutex_lock().
    • А в RHEL-7.3 этой статической уже нет (только в compat-пакете для RHEL6 -- compat-glibc-2.12-4.el7.centos.x86_64).

    22.12.2017@утро, дорога-в-ИЯФ, после ИПА, перед мышью: а чего про getaddrinfo_a() гадать -- надо глянуть исходники в .src.rpm.

  • 16.02.2010: к вопросу о SIGPIPE... Кратко: он реально бывает, и надо стараться от него защищаться -- благо, способы есть...

    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...".

    Так что,

    1. Надо по MarkAsClosed() пришибать fdio-handle, поскольку больше с этим дескриптором всё равно никакого В/В делать уже нет смысла/нельзя.
    2. Но и ставить set_signal(SIGPIPE,SIG_IGN) также надо везде, где только возможно.

      Потому как вероятна ситуация, что сокет будет прикрыт, когда в буфере fdiolib'а имелись какие-то данные для отправки (а программа в это время не в select(), а занимается какими-то другими делами), и первым делом fdio_io_cb() проверит сначала SL_WR и дёрнет StreamReadyForWrite() -- который и скопытится на попытке записи.

    Посему:

    1. В MarkAsClosed() добавлено закрытие fhandle и fd (с немедленным =-1) ПЕРЕД вызовом нотификатора.

      Соответственно, cx_close() даже не пытается слать при fhandle<0.

    2. Вставлена строка
      set_signal(SIGPIPE, SIG_IGN);
      в самое начало main()'ов в cxclient.c, cdaclient.c, pult.c, tstknobs.c.

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

    Проверено -- нынешние вылеты cdaclient'а обе этих меры лечат и поодиночке, а уж парой -- 100%.

    Итого -- проблема вроде бы подзалатана, так что ставим "done".

    22.01.2013: в продолжение темы -- по результатам эксплуатации v2'шного fdiolib-based cxlib'а.

    1. "Первая мера" проблему не решает. В старую систему копировался уже обновлённый код MarkAsClosed(), и вылеты всё равно имели место быть -- изредка, при убиении сервера.

      (Ну еще бы -- тупой race condition, в буфере отсылки что-то есть, по закрытию сокет становится "готовым" на запись, эта проверка происходит ДО проверки на чтение (ну что с Xt взять ;-)), вот и причина...)

    2. А вставлять В БИБЛИОТЕКУ -- как раз вполне корректно. Страдает этим fdiolib, значит, можно в fdio_register_fd() вставить УСЛОВНУЮ установку SIG_IGN: если текущая реакция стоит SIG_DFL.
  • 23.07.2012: переименован из cx_client в cxlib_client.
  • 17.10.2012@Снежинск-каземат-11: сходу -- добавлено сохранение переданного cx_open()'у параметра spec -- аналогично v2'шному (см. за 19-04-2011).

    И теперь выпечатывание ссылки на сервер повешено на саму cxlib_report() (отчего она стала совсем гадкой).

    09.10.2013: в cxlib_report() добавлено также выпечатывание номера соединения -- cd, который она аккуратно добывает из cp, cp==NULL влечёт cd=-1.

  • 17.10.2014: приступаем к подмодулю резолвинга.

    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-разделе.

  • 07.04.2015: кстати -- а ведь в _sync-режиме не будет работать ничего, кроме установки соединения, т.к. только в SetReady() есть вызов _cxlib_break_wait()!

    07.04.2015: очевидно, надо в cx_run() запоминать Seq отправляемого запроса, чтобы в async_CXT4_DATA_IO() при совпадении сделать _cxlib_break_wait().

    ...вот только возникает потенциальная проблема переполнения: если когда-то была зарегистрирована подписка с неким Seq, а потом прошло 2^32 циклов, круг замкнулся и оно висит на ожидании ответа с тем же Seq, то произойдёт сбой. Вероятность очень низка, конечно, но раз в 2^32 такое будет происходить ГАРАНТИРОВАННО. И чё делать -- timestamp запоминать, чтоб еще и секунды сравнивать? Тоже ведь не гарантия...

    Короче -- пока добавлены:

    1. Сохранение отправляемого Seq в свежедобавленном syncSeq (кстати, overkill -- при синхронной работе оно же должно остаться прямо в самом Seq, ибо в синхронном режиме нефиг что-то делать до ответа на предыдущий).
    2. Сравнение с Seq пришедшего пакета.

    Насчёт же переполнения -- явно надо прямо в ПРОТОКОЛЕ как-то отличать пакеты ответов и пакеты асинхронной нотификации.

    10.04.2015: прикол в том, что для асинхронных уведомлений отдельный код пакета CXT4_DATA_MONITOR уже есть -- он появился где-то между 09-12-2014 и 26-12-2014, и был забыт быть выкинут 25-12-2014.

    Так что:

    1. В cxsd_fe_cx.c::SendAReply() (используется при любом мониторинге) создаётся пакет с кодом CXT4_DATA_MONITOR вместо былого DATA_IO.
    2. А cxlib_client.c::ProcessFdioEvent() одинаково трактует оба кода, в обоих случаях вызывая async_CXT4_DATA_IO().
    3. ...а тот уж теперь делает _cxlib_break_wait() при CXT4_DATA_IO, никак не используя Seq/syncSeq.

      Так что недавновведённое syncSeq теперь никак не используется.

  • 01.12.2017: некоторые соображения по корректности работы, с учётом возможных действий callback'ов:, в первую очередь касается
    1. В async_CXT4_DATA_IO() идёт обработка множества chunk'ов, и, теоретически callback кого-нибудь из них может вызвать cx_close().

      Так что надо и тут тоже внедрять инфраструктуру being_processed и being_destroyed.

    2. Также там может быть вызвано и создание нового соединения, так что надо бы

      А ведь это касается вообще ВСЕХ и ВЕЗДЕ! Точно так же может измениться и значение ctxs_list, так что надо пере-кэшировать значение ci после каждого вызова evproc'а. ...а вот это уже не получится -- ибо сделано на GROWING-SLOTARRAY'е, который на подобное никак не рассчитан...

    Будущего HandleSearchReply() это всё касается в равной степени.

    P.S. А в v2'шном-то cda такое БЫЛО предусмотрено, в обном месте (правда, не касающемся никакой реентрантности, а просто обычная операция, могущая изменить указатель на список) -- в cda_add_auxsid().

    01.12.2017: а если немножко подумать, то получается, что эта проблема вообще ставит крест на попытках сделать возможность "удаления из callback'а". И даже больше, т.к.:

    • Не только "удаления", но и СОЗДАНИЯ -- т.к. оно тоже модифицирует SLOTARRAY.

    Что получается:

    • Работать-то будет, но весьма ограниченно.

      В частности, НЕ будет проблем в случаях, когда 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'я, так что пофиксить удалось и очень дёшево. Подробнее см. в разделе о нереентрантности, за сегодня.

UDP-резолвинг:
  • 24.10.2023: решено создать свой раздел, т.к. количество разных тем стало зашкаливать, так что держать их все в одном бесконечном пункте уже просто неразумно.
  • 30.11.2017: приступаем к "правильной" реализации UDP-резолвинга (ныне UDP-search).

    30.11.2017: самое первое --

    • Введён код CAR_SRCH_RESULT.

    04.12.2017: далее:

    • Структурка для передачи информации о разрезолвленном -- cx_srch_info_t.

      Аналогична rsvl_info, но в конце еще поле name (чтоб клиент мог сравнить -- на его ли вопрос это ответ).

    • API для создания объектов-резолверов. (И поле "тип соединения"!)

      Общий подход и потребовавшиеся для него модификации:

      • Объекты-дескрипторы аллокируются из того же пространства, что и обычные В/В, ...
      • ...но отличаются значением поля type. Которое введено аналогично тому, как было в v2 (для CONS- и BIGC-соединений).
      • Также аналогично расширена параметром type функция CheckCd(), с дополнительно возможным вариантом CT_ANY. В точности как в v2.
      • Создаваться соединения-резолверы будут отдельным вызовом cx_seeker(), а вот закрываться обычным cx_close().
      • И группирование запросов в них будет делаться теми же "скобками" cx_begin() и cx_run().
    • Детали реализации:
      • Enum'у CS_xxx вёрнуто имя собственное -- connstate_t.
      • Аналогично для CT_xxx -- conntype_t.
      • Создание -- cx_seeker():
        • Поскольку инициализация объекта "cd" такая же, как в cx_open(), то общий кусок вынесен в create_a_cd().

          И там при просмотре кода попался косячок, существующий с каких-то стародавних пор (в нынешнем v2 он тоже есть): при отсутствующей переменной окружения LOGNAME оно SIGSEGV'ится (проверено -- да!). Добавлена защита.

        • Размер sendbufsize сразу ставится CX_V4_MAX_UDP_PKTSIZE.
        • При успешном создании сразу делается:
          1. state=CS_READY -- поскольку созданием объекта (плюс сокета и fdio-объекта) всё и завершается.
          2. isconnected=1 -- чтоб MarkAsClosed() работало как надо.
        • Функция-обработчик, регистрируемая в fdiolib -- 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-сестрицы, т.к.:
      1. Дополнительно заполняет в заголовке пакета поля var1 и var2, для опознания/расшифровки пакета сервером.
      2. Подготавливает broadcast-адрес для отправки.
      3. Делает fdio_send_to() вместо простого fdio_send().

      Также некоторые соображения на будущее, насчёт этого самого адреса для отправки:

      • Сейчас там ставится просто htonl(INADDR_BROADCAST).
      • А может, добавить возможность указывать адрес в переменных окружения? Тогда оное надо будет парсить где-то при инициализации:
        1. Либо по-соединЕнно, в cx_seeker().
        2. Либо из него же, но "глобально" -- только при первом вызове, делать куда-то в глобальную переменную.

          Поскольку всё равно переменные окружения глобальные, то per-cd складировать всё равно смысла нет.

        3. ...если, конечно, не дать возможность указывать список адресов per-cd явно -- например, параметром у cx_seeker().

          Хотя тогда встанет вопрос "а как оный список передавать в cda (точнее, донести до cda_d_cx), чтоб оттуда уже спускать в cxlib?".

      • И, собственно, о СПИСКАХ -- т.е., спецификация множественных адресов: чтоб пакет слался не по 1 адресу, а по нескольким, последовательно.

        Тут некий вопрос будет -- как корректнее организовать эту множественную отправку в SendSrchRequest()? Просто цикл? А ошибки как ловать?

    • Насчёт ловли ошибок: пока она не выполняется вообще НИКАК.

      А ведь по UDP ошибки могут возвращаться асинхронно, и их будет как-то ловить fdiolib в DgramReadyForRead()...

      Очевидно -- придётся проверять экспериментально.

    • HandleSrchReply() наполнена "мясом".
      • Она теперь вызывает notifier, предварительно сформировав info-структуру.
      • Главный вопрос был -- как отдавать адрес сервера?
        • Первое напрашивавшееся -- да прямо в виде sockaddr_t+socklen_t.

          Но это криво -- т.к.

          1. клиенту придётся самому извращаться, переводя это в строковый вид (т.к., нет никакого интерфейса, позволяющего просить соединение прямо по IP);
          2. нерасширяемо -- на тот же потенциально поддерживаемый в будущем IPv6 никак не натянешь.
        • Поэтому перед вызовом нотификатора оно само конвертирует адрес в строку, при помощи inet_ntoa().

          Поскольку inet_ntoa() возвращает указатель на свой статический буфер, то строка копируется в дополнительный буфер, и уже на него указатель передаётся клиенту.

        • Таким образом, в будущем при надобности легко можно будет расширить синтаксис на другие транспорты -- хоть IPv6, хоть SHM.
      • Ну а номер сервера передаётся просто числом -- его уж клиент должен будет при надобности добавить (через ':') сам.
      • Замечание: ответ передаётся не специализированным форматом "CxV4ЧегоНибудьChunk", а базовым CxV4Chunk, просто после него в data[] прицеплена строка.
    • А дальше надо бы не сразу в cda_d_cx поддержку делать, а слабать для тестирования утилитку cx-chan-search.
    • Еще некоторые заметки на тему о том, что было осознано, но еще не сделано:
      • Добавить проверку размера chunk'а -- что не меньше минимума, и что общий размер не выходит за границу пакета.
      • Отдельный вопрос: а что у нас с поддержкой синхронного режима работы резолвером? 06.12.2017: сделана.
      • Почему во всех Chunk'ах есть поле hwid, а в CxV4StrsChunk -- нет?
      • Всё-таки плохо, что эти запросы имеют тот же код CXC_RESOLVE, что и обычные запросы резолвинга. Главная проблема -- ОТВЕТЫ: у них в результате структура отличается, при том же коде, что очень неправильно. Сейчас отладимся так, но потом надо ввести иной код. 06.12.2017: сделано.
      • 06.12.2017: в cxsd_fe_cx'ном ServeGuruRequest() надо проверять код chunk'а. 06.12.2017: уже.

    06.12.2017: сделана утилитка cx-chan-search, и с её помощью видно, что, похоже, работает.

    • Что касается синхронного режима: ничего и никак, утилита продолжает висеть.
      • Тут, конечно, вопрос -- насколько вообще адекватен синхронный режим для изначально асинхронного протокола?
      • Но добавление _cxlib_break_wait() в начало HandleSrchReply() вроде как "решило проблему" -- точнее, создало видимость этого: утилитка сразу после печати ответа честно завершается (реально -- продолжает исполнение после cx_run()'а).
      • Хотя очень "странный" эффект бывает, если пакет пришёл, а в нём ответа нет (это наблюдалось в период времени, когда cxlib уже был переведён на CXC_SEARCH, а cxsd_fe_cx еще нет): ожидание заканчивается и утилитка завершается, но без печати какого-либо ответа.

        Понятно, что корректно будет в асинхронном варианте, когда утилита сама будет вести учёт полученных ответов.

    • Теперь исправление огрехов протокола:
      • Введён новый код CXC_SEARCH.
      • Отправка в cx_srch() и проверка ответа в HandleSrchReply() переведаны на него.
      • И в ServeGuruRequest() вставлена проверка на него.
    • Проверки? Размеры в первую очередь.

      25.12.2017: добавлены, в cxlib_client.c и в cxsd_fe_cx.c практически симметричные.

  • 14.06.2022: насчёт того, как правильно слать UDP-broadcast'ы.

    24.10.2023: в свой отдельный пункт вытащено только сегодня.

    14.06.2022: всплыли некоторые детали работы broadcast'ов, которые, весьма вероятно, потребуют некоторых доработок.

    • "Всплыла" эта тема при прочтении одного конкретного сообщения в EPICS Tech-talk (Message-ID: fb81b675-81e2-a781-912d-afb0ee114a7b@gmail.com) "Re: Allowing localhost in access control files" от Michael Davidsaver, в котором говорится
      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

      1. Рассылает свои broadcast'ы ИНДИВИДУАЛЬНО во все интерфейсы по конкретным broadcast-адресам этих интерфейсов (что меня тогда сильно удивило -- а чё не просто на 255.255.255.255?).
      2. Как мне помнилось, также и слушает все интерфейсы отдельно -- а вот это, как показал явно запущенный "netstat -anp", неверно: слушает он по 1 сокету на UDP и TCP.

      ...и хотел было даже спросить

      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.
      
      но решил сначала перепроверить это своё утверждение (чтобы не опозориться), и что-то оно сходу не подтвердилось: на тестах (слушая "netcat -u -l -p 8012" и генеря пакеты с помощью "cdaclient asdf.0") выглядело, что прилетает всего по 1 пакету, через eno1, который институтский интерфейс и default; причём если отсутствие в eno2 и eno3 можно было объяснить невоткнутостью в них кабелей ("link down" -- чё зря слать-то?), то в loopback -- уже нет.

      И стал гуглить.

    • По "linux send broadcast 255.255.255.255 multihome" из интересного нашлось:
      • Целый thread "255.255.255.255 won't broadcast to multiple NICs" (начало) в Linux-Kernel Archive от 02-11-2000. Среди прочего, там в одном из ответов есть такой пассаж:
        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.
        
      • Тема "UDP-Broadcast on all interfaces" на Stackoverflow от 25-03-2009; там немножко обсуждений и примеры кода.
    • И вообще возникает такое впечатление, что даже к бродкасту 255.255.255.255 применяются обычные правила маршрутизации, по которым он и отправляется на default gateway.
    • Надо РЕАЛЬНО проверить, что пакет уходит только в default-интерфейс, а в остальные -- нет (туда сейчас не воткнуты кабели, так что может и независимо ничего не отправляться).

    Конкретно на СЕЙЧАС напрашиваются такие выводы:

    1. ОТПРАВЛЯТЬ пакеты придётся, похоже, вручную по всем интерфейсам.

      Как это организовать -- хбз: состав интерфейсов ведь может меняться (hotplug, переконфигурирование, динамичность WiFi, ...).

      16.06.2022: запоминать время составления списка, и если прошло более 10 секунд -- составлять его повторно? (А сам список аллокировать GrowBuf()'ом.)

    2. А вот слушать достаточно 1 сокет по адресу 0.0.0.0.

    19.06.2022@ИЯФ, воскресенье, вечер: провёл эксперимент, соединив 2 интерфейса (eno2 и enp1s0 на x10sae) кросс-кабелем, причём адреса на них стоят из разных сетей, 192.168.1.254 и 192.168.2.254 соответственно. Итак:

    • Главное -- да, бродкаст 255.255.255.255 уходит ТОЛЬКО в default-интерфейс, в остальные же -- нет.
    • Пока кабель был не воткнут, никаких пакетов и не ходило -- делаешь "ping -b 192.168.1.255", а в листинг захваченных пакетов ничего не добавляется.
    • Wireshark не мониторит набор интерфейсов: enp1s0 сначала был не поднят, и после поднятия в списке не появился -- пришлось рестартовать.

    19.06.2022: остаётся вопрос: а КОГДА добывать список интерфейсов? При каждой необходимости отправки пакета -- дороговато.

    Можно, конечно, как предложено 16-06-2022, смотреть, что если прошло более 10 секунд с момента составления списка, то повторять, а иначе использовать предыдущий. Но это кривовато -- как и вообще любой поллинг против работы по событиям.

    Решил поискать.

    • Гугление по "linux network interface change notification" показало, что да -- МОЖНО, конкретно под Linux, хбз с какой версии ядра. Вот результаты со Stackoverflow:

      Собственно технология:

      • Ключевые слова netlink(7) и rtnetlink(7), магическое заклинание
        socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)
      • Идея в том, что ядро присылает события об изменениях в маршрутизации -- при изменении набора интерфейсов и при изменении адресной информации в интерфейсах.
    • #IF на OS_LINUX или нет и поддержку NETLINK; при отсутствии -- fallback на получение списка если с предыдущего получения прошло более 10 секунд.
    • Процедурное: сама-то отправка бродкаста не в cda_d_cx.c, а в cxlib_client.c.
    • 20.06.2022@утро, завтрак, мытьё посуды:
      • Если хорошенько подумать, то чисто по архитектуре выходит, что НЕТУ никакого глубокого смысла держать на КАЖДЫЙ CT_SRCH-объект (их может быть >1 внутри сервера) по своему списку адресов.

        (Пояснение: узел-то один, и даже если будет использоваться environment, то он тоже будет одинаковый для всех.)

      • Соответственно, можно иметь вообще ОДИН массив IP-адресов -- хоть аллокируемый, хоть статический (размером, например, не более десятка).
      • А уж обновлять его -- да, по индивидуальным уведомлениям; ну да, будет при много-SRCH-объектности делаться дублирующая работа, но так она и при индивидуальных массивах тоже будет делаться.

        @15:30: неа, нифига -- НЕ будет: если обновления выполнять не сразу, а лишь взводить флаг "надо обновить!" и само обновление выполнять при первой надобности в отправке вопроса, то оно и происходить будет ровно однажды.

      • Один нюанс: поскольку возможно "мёртвое время" -- когда не будет живым НИ ОДНОГО объекта -- в каковое ремя может произойти обновление, то надо при создании SRCH-объекта нулить флаг "есть актуальный список адресов", чтобы при первом же вопросе он бы сгенерился.
    • 20.06.2022@15:40: битым текстом пропишем "алгоритм":
      • получение по-интерфейсного списка адресов выполняется ТОЛЬКО в момент необходимости отправить вопрос.
      • Хоть мы подписываемся на уведомления об изменениях в интерфейсах, хоть запоминаем время последнего получения списка.
      • И это применимо к ЛЮБЫМ вариантам реализации -- хоть по-объектные списки, хоть один.
      • Действием "надо обновить список!" управляет флажок "список получен", по умолчанию =0, и при создании нового SRCH-объекта флаг также нулится (это актуально для единого списка).
      • @16:45: и добыча из environment'а в эту схему ложится тривиально: парсим в тот же самый список, но значение флага ставим "=-1", что означает "список готов, можно использовать сразу".

    24.07.2022: некоторые сведения на тему "как получить список интерфейсов и из broadcast-адреса":

    • В примере выше за 25-03-2009 рекомендуют getifaddrs().
    • ВЧЕРА: Порывшись в исходниках EPICS, выяснил, что:
      • Нужное содержится в src/libCom/osi/os/default/osdNetIntf.c (это для Linux; точнее, не-WIN32).
      • Конкретно -- osiSockDiscoverBroadcastAddresses().

        Ход действий там без предварительного знания понять довольно сложно.

        Но то было вчера. А сегодня, вооружённый знанием, описанным ниже, перечитавши, увидел, что...

      • ...там используют те самые ioctl()SIOCGIF*, что я сегодня нарыл, оттолкнувшись от SIOCGIFINDEX.
        • Сначала SIOCGIFCONF'ом добывают список интерфейсов.

          Причём там захардкожено ограничение количества в nelem = 100 (ioctl()'у указывается размер буфера).

        • Т.е., как получить КОЛИЧЕСТВО интерфейсов -- там никак.

          ...(и это при том, что буфер calloc()'ируется)

        • А затем идут по списочку и делают SIOCGIFFLAGS и SIOCGIFBRDADDR.
    • Памятуя, что работа с SocketCAN требует каких-то аналогичных махинаций с интерфейсами,
      • Залез в sktcan_hal.h, и там в can_hal_open_and_setup_line() присутствует ioctl() с кодом SIOCGIFINDEX.

        Который применяется к предварительно созданному сокету =socket(PF_CAN, SOCK_RAW, CAN_RAW).

      • Затем по-grep'ил по этому ключевому слову в исходниках ядра, и оно, помимо .c-файлов, встретилось в usr/include/linux/sockios.h -- там определение.
      • В нём же нашлась ещё толпа кодов, включая SIOCGIFCONF.
    • Просмотрев strace-лог от "cdaclient epics::...", нашёл там вызов 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-адрес.
    • Замечания:
      1. ioctl()'ы выполняются с СОКЕТОМ вида PF_INET.
      2. В получаемом списке есть ТОЛЬКО интерфейсы, годные для IP: никаких can* там нету. Видимо, определяется типом сокета, от которого приходят ioctl()'ы.

        26.11.2022: ну и PF_INET6 там тоже нету -- очевидно, в т.ч. и потому, что в IPv6 отсутствует концепция "broadcast". И вообще, как выяснилось (не помню уж, было ли это где-то явно написано или общее впечатление по сумме информации) -- SIOCGIFCONF выдаёт ТОЛЬКО интерфейсы IPv4.

    Дальнейшие поиски:

    25.07.2022: приступаем.

    Принципы:

    • Информация собирается ТОЛЬКО ПО ТРЕБОВАНИЮ. Т.е., по необходимости отправить broadcast-запрос; до того -- ничего в эту сторону не делается.
    • Информация помещается в СТАТИЧЕСКИЕ переменные, а никак не привязана к конкретному resolver'у.

      Обоснование: сколько бы клиентских соединений и resolver'ов ни создавалось, информация о списке broadcast-адресов едина и определяется конфигурацией сети узла (а если даже какой-то "$CX_RESOLVER_LIST", то она тоже едина).

    • Факт "наличия списка адресов" определяется булевским флагом, который изначально =0, взводится =1 после заполнения списка.

      (И, в перспективе, может сбрасываться =0 и какими-то иными событиями, вроде истечения большого периода времени или получения от netlink'а события "изменилась информация об интерфейсах).

      Вызов сбора информации делается при необходимости отправить запрос И при флаге==0.

    • Буфер под результат SIOCGIFCONF аллокируется единожды и существует до конца жизни программы, НЕ высвобождаясь.

    Делаем "базу":

    • "Список" адресов uint32_t bcast_list[BCAST_MAX_ADDRS=100] плюс сопутствующие bcast_list_*, включая...
    • ...флаг bcast_list_present.
    • Буфер для ifreq-информации uint8 *IFCONF_buf.
    • Заполнялка "списка по умолчанию" set_bcast_list_INADDR_BROADCAST().
    • Заполнялка fill_bcast_list().

    А вот дальше...

    • В osdNetIntf.c какой-то очень уж мудрёный код прохода по списку-результату от SIOCGIFCONF -- ifreqSize() с грустным комментарием:
      /*
       * 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;
      }
      
      (bold мой), где 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" -- нормально, да?!

    • И гугление на тему вроде "UDP broadcast application" дало ссылки на обсуждение в EPICS Tech-talk касательно проблем с SIOCGIFCONF в NetBSD и FreeBSD.

    ...После чего возникло сильное желание плюнуть на это всё и ограничиться нынешним решением с отправкой на 255.255.255.255.

    26.07.2022: пытаемся всё же сделать -- базовый-то алгоритм прямолинеен, как стрела:

    • Вся работа -- в fill_bcast_list().
    • Для возможности выдачи диагностических сообщений в качестве параметра передаётся не fd, а v4conn_t *cp.
    • При любой фатальной проблеме она вызывает set_bcast_list_INADDR_BROADCAST() и отваливает.
    • При НЕфатальной -- какой-то из ioctl()'ов считывания информации конкретного интерфейса обламывается (а как бы?!) -- просто пропускает этот интерфейс.
    • Собственно алгоритм:
      • идёт по всем интерфейсам,
      • не-AF_INET'ные пропускает (хотя таких в этом списке вроде, оказывается, быть не должно),
      • спрашивает SIOCGIFFLAGS флаги этого интерфейса, неактивные (с отсутствующим IFF_UP) пропускает, а далее,
      • в зависимости от флагов, выбирает один из 3 вариантов:
        1. Есть IFF_BROADCAST -- SIOCGIFBRDADDR, берём его broadcast-адрес.
        2. Есть IFF_LOOPBACK -- SIOCGIFADDR, добываем его адрес и сохраняем на всякий лучай в loopback_addr, которая изначально =0.
        3. Есть IFF_POINTOPOINT -- SIOCGIFDSTADDR, берём его "внешний" адрес.

        Иначе интерфейс игнорируется.

      • Если после прохода по списку интерфейсов список адресов пуст, то:
        1. Если loopback_addr!=0 -- записываем его как единственный (смысл -- для standalone-хостов без сети, на которых и сервер, и клиент).
        2. Если же по-прежнему список пуст (т.е., и loopback'а нет), то сваливаемся в 255.255.255.255 путём set_bcast_list_INADDR_BROADCAST().
    • И в SendSrchRequest() сделан цикл по bcast_list[bcast_list_count].

    Проверил, для начала при помощи strace (чтоб убедиться в корректности адресов -- что в нужном порядке) -- да, всё шлёт как надо. И сервер отвечает как надо, на все-все запросы. ...и библиотека все ответы получает, но адрес использует из ПЕРВОГО ответа.

    08.08.2022@вечер, валяясь в постели перед сном: насчёт "как назвать переменную окружения для указания списка АДРЕС[:ПОРТ]" -- так "CX_GURU_LIST" же! По аналогии с $CX_SERVER

    09.08.2022: не терпится, хочется взять да сделать. Предварительные соображения:

    • Пусть указывается в формате "HOST[:PORT]{,HOST[:PORT]}", т.е., порт -- опционально, при неуказанности обычный 8012.
    • Причём разделителем могут быть как запятая ',', так и -- на всякий случай -- точка с запятой ';', а главное -- пробел (точнее, whitespace): чтоб можно было список генерить, в т.ч. средствами shell'а.
    • Указывать чтоб можно было не только IP-адреса, но и имена хостов.

      Да, это добавит тормозов и -- в отличие от потенциально расшивабельного резолвинга при коннекте -- НЕ может быть расшито. @после-обеда: хотя... ведь можно просто сам процесс заполнения списка считать отдельным "шагом работы машины состояний 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().
    • Цикл парсинга довольно простой:
      • сначала пропускаем разделители,
      • затем "потребляем" символы, легитимные для IP-адресов и имён хостов -- причём '.' и '-' могут быть только НЕ в начале;
      • если первый "другой" ("не-легитимный") символ -- ':', то дальше выпарсиваем номер порта, а иначе считаем его за умолчательный.
      • Ну а потом "резолвинг" -- преобразование строки в адрес -- и складирование.
    • Сам резолвинг скопирован из cx_open() -- из 2 попыток, на 1-й пробуется просто IP-адрес inet_addr(), а на 2-й уже gethostbyname().
    • Причём 2-я может быть отключена, т.к. управляется #if-условием на MAY_USE_GETHOSTBYNAME_FOR_CX_GURU_LIST, который, в свою очередь, предварительно определяется в 1, но УСЛОВНО -- если не определён.

      Т.о., отключение можно сделать из командной строки сборки, указав -DMAY_USE_GETHOSTBYNAME_FOR_CX_GURU_LIST=0.

    • Если результат парсинга оказался пуст (ну мало ли -- мусор или всё "host not found"), то оно проваливается в авто-вычитывание списка интерфейсов из ядра.
    • ...но всё равно bcast_list_fixed = 1 взводится в самом начале парсинга, так что повторно парситься не будет в любом случае.

    Проверил -- вроде всё пашет как надо!

  • 26.11.2022: насчёт потенциальной реализации поддержки UDP-multicast'ов.

    24.10.2023: в свой отдельный пункт вытащено только сегодня.

    26.11.2022: а не добавить ли поддержку резолвинга через multicast?

    Чтобы не выпендриваться с лишними переменными окружения -- просто считать, что если указанный адрес является multicast'ным (по маске: относится к 224.0.0.0/4), то работаем с ним соответственно.

    26.11.2022: стадия сбора информации.

    • В linux/in.h есть макрос IN_MULTICAST().

      ...и в netinet/in.h тоже.

    • А может, "по умолчанию" слать на 224.0.0.1, который 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" (а такой есть?)).
    • Попробовал "ping 224.0.0.1" -- работает! Ответы прилетают от тучи хостов.
    • 27.11.2022: а вот "ping6 ff02::1" на Star'е даёт ошибку "ping: sendmsg: Invalid argument", и ни ключ "-b", ни запуск из-под root не помогают. Возможно, специфика Oracle Linux.

    27.11.2022: продолжение сбора информации.

    @утро-душ: некоторые мысли по теме:

    1. Надо бы проверить wireshark'ом, что происходит при отправке пакета на 224.0.0.1 -- а не broadcast ли?

      И шлётся ли он на ВСЕ интерфейсы? 28.11.2022: увы, НЕТ -- ТОЛЬКО на default.

      Для этого нужно отправить netcat'ом пакет на какой-нибудь порт вроде 8012 и фильтровать дамп пакетов по нему.

    2. А если на пульту сделать тот же ping? Там по 2 сетевых интерфейса, и можно смотреть, ответят ли узлы из второй сети.
    3. А можно ли указать EPICS_CA_ADDR_LIST=224.0.0.1 -- будет ли это работать?

    Проверяем:

    1. "nc -u 224.0.0.1 8013" генерит пакет на адрес 01:00:5e:00:00:01 aka "IPv4mcast_00:00:01".
    2. Пульт:
      • Там вообще НИ ЧЕРТА не работает -- куда ни отправляй (255.255.255.255, 224.0.0.1, broadcast'ы интерфейсов eno1 и eno2), ничего не пашет.

        Ни ping, ни "nc -u x.x.x.x 8013" (в последнем случае слушатель БЫЛ запущен). Слалось на v5p2 как с v5p3, так и с v5p2 самой -- один чёрт (только локально оно успевает сказать "connection refused").

      • Часть проблемы может быть в том, что смотреть приходилось tcpdump'ом (т.к. wireshark'а просто нет).
      • Но может и из-за какой-то настройки свитчей (а firewall не при делах -- iptables пустые).

      По-хорошему -- надо бы проверить между x10sae и b360mc.

      Чуть позже: а, нет -- работает, если UDP между NetCat'ами.

      • Проблема была в том, что
        1. Запускать слушателя надо "nc -l -u 0.0.0.0 8013" вместо "nc -l -u 0.0.0.0 -p 8013" (как вроде бы следует из man'а; но реально там синтаксис неочевидный (см. "CONNECT MODE AND LISTEN MODE")).
        2. "Слушатель" печатает лишь ПЕРВЫЙ приходящий пакет, а для следующего его приходится рестартовать.
      • А так -- "слушатель" получает пакеты со всех вариантов адресов (255.255.255.255, 224.0.0.1, broadcast'ы интерфейсов eno1 и eno2).
      • Но вообще NetCat для диагностики "UDP-передачи в ОБЕ стороны" -- так себе средство.
      • Но ping всё же не пашет, в отличие от институтской сети.
      • А вот парочка "epics2cda+cainfo" -- вполне коннектится!
      • Но такое впечатление, что отправляется всё же лишь ОДИН пакет, на единственный интерфейс, а не на оба.

        28.11.2022: да, именно так -- только на default...

    3. EPICS_CA_ADDR_LIST=224.0.0.1 -- похоже, работает!

      Но почему-то только на умолчательном порту 5064; а вот указание серверу (в данном случае epics2cda) EPICS_CAS_SERVER_PORT=8013 и клиенту EPICS_CA_ADDR_LIST=224.0.0.1:8013 -- фиг. Чуть позже: а-а-а, дошло -- это firewall на x10sae блокирует! А порты 8012 и 5064 разрешены.

    4. CX_GURU_LIST=224.0.0.1 -- работает!

      Правда, на локальной машине MAC'и и source, и destination показываются "незаполненными" 00:00:00:00:00:00.

    5. Однако неясно с "размножением" пакетов: будут ли они идти во ВСЕ интерфейсы?

      Как минимум на пульту отправка с 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 brought UP 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-интерфейс.

    Чуть подробнее:

    • Ещё вчера при тестах на пульту возникло впечатление такого поведения.
    • Сегодня было проверено ЯВНО. Конфигурация x10sae -- 3 интерфейса,
      1. eno1 -- 192.168.171.173, default.
      2. eno2 -- 192.168.129.254;
      3. enp1s0 -- 192.168.2.254.

      При этом eno2 и enp1s0 соединены кросс-кабелем.

      Результаты (с использованием wireshark):

      1. echo abcdEFG | nc -u 224.0.0.1 8013 -- ушло на eno1 (как default).
      2. echo abcdEFG | nc -u -s 192.168.129.254 224.0.0.1 8013 -- ушло на eno2, было принято с enp1s0.
      3. echo abcdEFG | nc -u -s 192.168.2.254 224.0.0.1 8013 -- ушло на enp1s0, было принято с eno2.
      4. echo abcdEFG | nc -u -s 192.168.171.173 224.0.0.1 8013 -- идентично вар.1.

      Т.е. -- отправляется ВСЕГДА НА ОДИН интерфейс, на основании либо таблицы маршрутизации, либо исходного адреса.

    • Пишут, что "ну вот так вот оно"; и советуют уставлять интерфейс для каждой отсылки либо заводить по отдельному сокету на каждый интерфейс:
      • "C - choose interface for UDP/multicast socket" от 01-10-2012. Там ответы скорее о "привязывании к интерфейсам"; помеченных зелёной галочкой ответов нет, но в первом же комменте есть рекомендация
        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()'инга же топик

      • "What does it mean to bind a multicast (UDP) socket?" от 21-05-2012, в котором обсуждается нюанс "что такое bind() для multicast-сокета" и говорится со ссылкой на W.R Stevens, что там это работает "фильтром".

        ОТ МЕНЯ: странно как-то... Для обычного-то UDP эту роль выполняет connect()...

      • "c - Send UDP datagrams to multiple interfaces with multicast address" от 27-03-2022. Некто dbush делает утверждение
        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.
      • "How to multicast send to all network interfaces?" от 22-05-2012; там тоже предлагают вместо N сокетов на N интерфейсов использовать IP_MULTICAST_IF перед каждой отправкой.
      • 29.11.2022: "Problems with SO_BINDTODEVICE Linux socket option" от 30-07-2009, и там в одном из ответов говорится
        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
    • Ну и разные Michael Davidsaver'ы плакались, что с мультикастом и IPv6 всё сложно: "Re: EPICS IOC and its client application with ONLY IPv6" от 28-10-2020:
      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.
      
    • На мой -- да, субъективный -- взгляд, ситуация дебильная, ровно как и с 255.255.255.255:
      • Хосты радостно откликаются на пакеты с такими адресами (255.255.255.255 и 224.0.0.1), вроде как "не своими", но при этом нормально ОТПРАВИТЬ пакет на такой адрес не могут.
      • Как следствие: без явной конфигурации (как-то указывающей программе то ли конкретные адреса (broadcast'ы или multicast'ы), то ли как минимум интерфейсы) ничего "правильно" работать не будет.
      • Из каких дурных соображений такие сильно специфичные адреса подвергаются обычному процессу маршрутизации -- мне неясно. Ведь их числовые значения, в отличие от обычных (в т.ч. и directed broadcasts, вроде 192.168.1.255), никакого отношения к маршрутизации не имеют -- это просто некие условные индексы.
      • Единственная причина, приходящая в голову -- разработчикам IP-стека "так было проще".

    Что делать конкретно сейчас: читать документацию.

    • "Multicast over TCP/IP HOWTO", Juan-Mariano de Goyeneche, 20 March 1998.

      Какого лешего там фигурирует "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". Так вот,

    • listener делает
      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);
      
    • а вот sender -- просто шлёт датаграммы на HELLO_GROUP:HELLO_PORT, вовсе НЕ присоединяясь ни к какой группе.

    На этом полезность того кода заканчивается, ибо:

    1. Качество кода -- отвратительное: в частности, там
      char *message="Hello, World!";
      . . .
      sendto(fd,message,sizeof(message), ...
      
      -- т.е., мало того, что забывает про терминирующий '\0', не делая никакого strlen()+1, но вообще в качестве длины считает sizeof(char*)!
    2. "Гибкость" тоже никакая: ни данные с stdin оно не читает, ни HOST:PORT указать из командной строки нельзя.

    Попробовал проверить, заменив порт с 12345 на 8012 (чтоб firewall не мешал) -- как-то работает.

    @вечер: ЧеблоПаша прислал ссылку на пример для IPv6 -- https://github.com/bjornl/ipv6_multicast_example

    • Вот в нём -- и sender тоже делает IPV6_JOIN_GROUP

    Технически там код сильно получше:

    1. И данные ipv6_multicast_send.c читает с stdin.

      (Хотя вместо просто чтения из fd=0 делает fd = open("/dev/stdin", O_RDONLY, NULL); зачем-то...)

    2. И в командной строке HOST PORT указываются обоим.
    3. Но косяк всё же есть: ipv6_multicast_recv.c имеет buffer overflow в виде
      char buf[1400];
      . . .
      len = read(sd, buf, 1400);
      buf[len] = '\0';
      
      -- т.е., при прочтении 1400 байт оно запишет '\0' за границу буфера (это вполне реальный сценарий при использовании "как netcat" -- при подаче sender'у на stdin большого файла).

      ...и, главное -- это совершенно лишнее! Там всё равно далее делается не puts()/printf()/..., а write(fd, buf, len);

    03.12.2022: а не потому ли на Star'е было "ping: sendmsg: Invalid argument", что в IPv6 присоединение к группе требуется обязательно?

    • Для проверки гипотезы я стал гуглить "sendto EINVAL multicast".
    • "netcat producing EINVAL when sending to UDP IPv6 multicast" за 01-07-2021, и единственный ответ аж от 16-08-2021 -- любопытнейший: очень подробный и со ссылками на полезную информацию:
      1. Что IPv6-адреса можно указывать в хитром формате, с указанием интерфейса через '%' после адреса; для нас -- например, "ff02::1%eth2".
      2. Вместо NetCat'а советует использовать socat по причине поддерживающести multicast'ов.
    • Разбираемся с синтаксисом "АДРЕС%ИНТЕРФЕЙС".
      • Указываемое через '%' в "спецификации" адреса -- "zone_id" "scope ID", RFC4007 (2005!), раздел 11 "Textual Representation".
        • После '%' указывается не обязательно имя интерфейса, а может его индекс; в общем случае -- просто некая абстрактная спецификация "зоны".
        • Это всё описано в том разделе 11, довольно просто и понятно.
        • А вот предыдущие разделы, с разъяснениями о СМЫСЛЕ указания оных зон -- мрак жуткий.

          То ли я старею и тупею (первое впечатление -- "ой, это для меня слишком сложно"), то ли реально IPv6 является чрезвычайно продвинутой (хитрозавёрнутой? хитрозамороченной?) и разветвлённой технологией, сильно сложнее IPv4 и труднопонятной сходу.

          @вечер, засыпая: да нет -- IPv6 РЕАЛЬНО переусложнён. Из проблем, бросающихся в глаза:

          • Не-уникальность IP-адреса в рамках хоста: то, что один и тот же адрес может быть на нескольких интерфейсах (как те "link-local", вроде fe80::1, могущие присутствовать на всех интерфейсах сразу).

            Это радикально ломает парадигму адресации, так что IP-адрес внезапно становится недостаточен для адресации (звучит как парадокс, не так ли?).

          • Запись через двоеточия сразу ломает синтаксис "HOST:PORT": например, "::1234:2345" -- это просто адрес из двух компонентов, или же адрес "::1234" с указанием порта "2345"?

            04.12.2022: поразбирался -- решение есть (адрес в квадратных скобках -- "[ADDR]:PORT"), подробнее см. ниже за сегодня.

      • Вот только inet_ntop() этот формат поддерживать никак не может (ибо складирует только АДРЕС).
      • А вот getaddrinfo() -- МОЖЕТ, о чём в её man-странице явно сказано:
        getaddrinfo() supports the address%scope-id notation for specifying the IPv6 scope-ID.
      • Рассказ на эту тему есть в заметке "What's the Deal with IPv6 Link-Local Addresses?" от Philip Homburg за 11 Mar 2020.

        (Наткнулся гуглением по «inet_pton "percent"».)

        Там простым языком объясняется суть проблемы "просто IP-адреса в IPv6 недостаточно для определения интерфейса, в который слать".

    • Как бы то ни было, проверил всё же гипотезу путём закомментировывания IPV6_JOIN_GROUP в ipv6_multicast_send.c.
      • НЕТ, без присоединения отправка всё равно ПРОДОЛЖАЕТ РАБОТАТЬ.

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

      • На разных узлах пока не проверял (нету нескольких), но попытка отправки на произвольный левый адрес:порт также успешна.
      • Кстати, при запуске IPv4'шного на x10sae "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 тоже двоеточия".

    Для решения придуманы более извратные синтаксисы, вроде

    • "[ADDR]:PORT" -- [::1234]:2345 (тут проблема с задействованием квадратных скобок, могущих использоваться для иных целей) или
    • ::ffff:10.1.2.3:1234 (при указании для IPv4-маппинга последнего компонента через '.' неоднозначность убирается).

    Но такие синтаксисы сразу сильно усложняют парсинг, если он в каком-то контексте, а не просто изолированная строка из argv[].

    За информацию о синтаксисах спасибо найденным по "ipv6 address port colon":

    • Краткому обсуждению "How should we represent an IPv6 address with port number in text?" от 29-02-2016, где ссылаются на RFC 3986 "Uniform Resource Identifier (URI): Generic Syntax" и RFC 5952 "A Recommendation for IPv6 Address Text Representation".
    • "Why IPv6 use colon as delimiter instead of dot?" за 25-03-2011 содержит попытку объяснения причины, почему разделителями работают двоеточия вместо точек.

      Во-первых, это позволило удобнее записывать IPv4-совместимые адреса в формате вроде "::FFFF:192.168.0.1".

      Ну а во-вторых, во время разработки IPv6 в начале 1990-х, задача указания портов отсутствовала: URL'ов ещё не было, а, например, в telnet порт указывался опционально через пробел.

      И там высказывается разумное соображение, что надо было брать разделителями дефисы '-', как это сделано в UUID'ах.

      24.10.2023: пришла идея, почему не сделали разделителями дефисы: адреса ведь можно сокращать, "сжимая" нули -- например, "::1" (localhost); и вот конкретно localhost при записи через дефисы будет выглядеть как "--1" -- а это путается с long options.

  • 17.10.2023: насчёт настраиваемости максимального размера UDP-search-пакетов.

    24.10.2023: в свой отдельный пункт вытащено только сегодня.

    17.10.2023: ЕманоФедя несколько дней назад (12.10.2023?) выдал претензию, что почему-то при работе через VPN бывает, что в егойном builder'е (на основе Qt-Designer) каналы в каком-то скрине (чьей целью является canhw:19) не резолвятся (причём не 10 секунд, а НИКОГДА); а если поставить в одном из них "прямую" ссылку (с именем сервера), то и он резолвится, и потом остальные.

    Замечание: поскольку broadcast'ы через VPN как-то не совсем алё, то клиенту выставлялся CX_GURU_LIST со списком IP-адресов.

    История разбирательства и решения:

    • Я за 15.10.2023 обсмотрел соответствующее содержимое всех вовлечённых частей -- и cxlib_client.c, и cxsd_fe_cx.c, и cda_d_cx.c. Было несколько гипотез, в т.ч. связанных с таймаутами, но внимательный осмотр кода их не подтвердил.

      (Код там выглядит максимально просто-прямолинейно, как по учебнику написанный, безо всяких умничаний/неочевидностей.)

    • 16.10.2023 Wireshark'ом были сняты дампы сетевого траффика, показывающие, что на некоторые отправляемые запросы ответов не приходит.
    • Поскольку имелось подозрение "а вдруг пакеты запросов серверу чем-то не нравятся и он их просто отбрасывает", то в cxsd_fe_cx.c был добавлен #define-символ DEBUG_ServeGuruRequest (по умолчанию =0), включабельный при сборке указанием "CPPFLAGS=-DDEBUG_ServeGuruRequest=1", который в ServeGuruRequest() приводит к выдаче на stderr диагностики в каждой обломившейся проверке.
    • Затем была сделана "имитация" -- со включенной диагностикой на v5p2 запущен сервер с devlist-canhw-19.lst и ему NetCat'ом отправлен тот пакет из дампа (Wireshark позволяет сохранить данные в файл).

      Результат -- никаких ругательств, а пакет ответа пришёл.

    • Следующая проверка уже сегодня -- тот же сервер со включенной диагностикой на v5p2, на который натравлен уже еманофедин клиент через VPN.

      Результат -- серверу запрос приходит, ответ он отправляет, но клиент этого ответа не получает!!!

    • К этому моменту уже было (ещё со вчера) подозрение, что чё-то может быть не так с VPN и UDP: возможно, из-за VPN'овского дополнительно оборачивания реальный MTU не 1500 (как утверждается в интерфейсе tun0), а меньше?
    • Поэтому была добавлена возможность УКАЗЫВАТЬ максимальный размер пакета в байтах в переменной окружения 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.
    • 23.10.2023: для унификации поменял в cx_proto_v4.h порядок определений CX_V4_MAX_UDP_PKTSIZE и CX_V4_MAX_UDP_DATASIZE -- чтобы была иерархия "PKTSIZE=1500-28, DATASIZE=PKTSIZE-sizeof(CxV4Header)".

    Проблема-то решилась, но ещё чуток дополнительного обсуждения:

    1. Была идея позволить указывать так размеры БОЛЬШЕ, чем 1500 -- для Gigabit Ethernet, где до 9000 (?).

      Но после осмотра кода решено было от этого отказаться: аукнулось бы в других местах. В частности, регистрация дескриптора у fdiolib'а выполняется РАНЬШЕ проверки $CX_MAX_UDP_PKTSIZE, а в этой регистрации указывается максимально допустимый объём, CX_V4_MAX_UDP_PKTSIZE.

      Пользы же от "поддержки больших пакетов" немного: ну сэкономим несколько пакетов при старте крупной программы, и всё; такой "выигрыш" явно не стоит усложнения кода.

    2. С принятым в cda_d_cx.c::PeriodicSrchProc() алгоритмом отправки "вызываем cx_srch() пока не вернёт переполнение, затем отправляем, начинаем новый пакет и опять молча вызываем cx_srch()" может быть проблема: а если прямо ЕДИНСТВЕННЫЙ запрос не войдёт в пакет -- т.е., спросят имя длиннее разрешённого размера пакета?

      Такое и раньше-то было возможно (указать имя длиной 2000 символов), но сейчас появилась возможность размер пакета уменьшить ниже разумного.

      Именно поэтому ограничение на минимально допустимый размер пакета сделано 200 байт -- тут хотя бы одиночные РАЗУМНЫЕ имена каналов точно должны влезть.

    23.10.2023: решил разобраться, как задача "не вылезти за MTU" решена в EPICS.

    Традиционно -- сильно завёрнуто/закрученно и фрагментированно (ravioli code; искал последовательно, начиная с SOCK_DGRAM и sendto(), а затем что рядом казалось связанным).

    • раскручивание привело к src/ca/client/udpiiu.cpp, где
      • отправка выполняется в 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 */
      
    • Т.е., с одной стороны -- похоже на наше "1500-28", с другой -- у них ограничение размера от балды 1024 байта.
    • ЗЫ: а ETHERNET_MAX_UDP используется только в libCAS -- src/ca/legacy/pcas/io/bsdSocket/casDGIntfIO.cc в casDGIntfIO::optimumInBufferSize(), причём несколько сомнительно.

      ...причём сама эта optimumInBufferSize() лишь определена, но не используется НИГДЕ.

:
cx_tsigio:
  • 30.06.2009: а нужна ли нам реализация на основе SIGIO?

    30.06.2009: если хорошенько подумать -- нет, не нужна. И вот почему:

    • SIGIO нам в UCAM и CXv2 требовался исключительно для эмуляции select()'а -- чтобы реализовывать асинхронное получение данных "за кулисами", прозрачно для прикладной программы.
    • Сейчас же можно ВСЕГДА предполагать использование fdiolib и cxscheduler'а. При этом:
      1. Графические клиенты отлично работают по модели select, в нативно-асинхронном режиме. Используется libXh_cxscheduler или libQt_cxscheduler.

        Это же касается и уже-cxscheduler-based клиентов, типа самого cxsd или cdrclient'а.

      2. Синхронный же режим нужен ТОЛЬКО утилитам командной строки, которым надо дожидаться ответа.

        И вот там можно использовать обычный cxscheduler, но в "прерывном" режиме: при входе в режим ожидания вызовет sl_main_loop(), а по приходу синхронного ответа -- sl_break().

    • Таким образом, некий дополнительный заменяемый модуль-адаптер всегда нужен, но очень простой:
      1. Вариант для консольных программ будет обеспечивать вышеописанную прерывность.
      2. Для графических же клиентов ничего этого делать не нужно -- программа и так ПОСТОЯННО находится в основном цикле тулкита (и даже сами те два вызова неопределены), так что методы модуля "зайти в цикл" и "прервать цикл" будут пустыми.
    • И автоматом исчезает проблема "Отправка данных всегда производится сразу" (prep_ucam, Препринт 98-53), поскольку возражение "...время от времени при помощи select() проверять, нельзя ли отослать очередную порцию данных. Поскольку такой подход потребовал бы явной кооперации со стороны клиентской программы, тем самым существенно усложняя ее, он неприемлем." становится неактуально -- обо всём позаботится fdiolib.

    Избавление от 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_tfdiolib:
  • 01.07.2009: модуль не потребуется -- раздел закрываем.
cxlib_utils:
  • 20.06.2007: создаем этот модуль -- в нем всякие информационные и вспомогательные функции типа cx_strrflag_{short,long}, в основном скопированные из старого cxlib'а. Разве что флаги теперь там обслуживаются все -- и серверные, и cda'шные, и Cdr'овские.
  • 23.07.2012: переименован из cx_utils в cxlib_utils.
  • 12.11.2013: переделываем cx_parse_chanref() (бывшую копией из v2), чтоб она работала и с именоваными каналами, а не только с нумероваными.

    12.11.2013: добавлен параметр channame_p, являющийся указателем на место, куда складировать указатель на часть "имя КАНАЛА" из ссылки.

    Он является обязательным, а как раз chan_n только если не-NULL, то попробуется преобразовать имя в число; при ошибке уставится -1 (значит -- надо использовать имя, а иначе можно сразу номер).

  • 18.05.2016: а зачем вообще нужен cx_parse_chanref()? По факту -- никакие номера теперь, в v4, нафиг не требуются.

    Переделанный вариант с channame_p -- еще какой-то смысл может иметь, для утилит, работающих на уровне cxlib, но:

    1. Нынешний cxclient.c прост до омерзения -- в нём только номера каналов (БЕЗ поддержки имён!), а имя сервера указывается отдельно.
    2. Никаких иных cxlib-level утилит нету.
:
CX-протокол:
  • 28.04.2009: некоторые общие соображения по структуре протокола -- записано в основном как бы задним числом.
    • 24.10.2006@PCaPAC-2006: Надо консольный интерфейс пускать по тому же, обычному каналу (CT_DATA/CXT_LOGIN/CLT_DATA) -- и вообще избавиться от CT_*/CLT_*. Это даст возможность, e.g., cx-starter'у спрашивать у сервера всякие параметры типа "uptime". So, no "connection type" -- less restrictions.
    • 24.10.2006@PCaPAC-2006: Regarding "CT_CONS": вообще-то лучше для запросов вместо консольного интерфейса использовать отдельное семейство синхронных команд (кроме _DATA, ex-_BIGC, _DB -- еще и _INFO). Binary!
    • 24.10.2006@PCaPAC-2006: Regarding "CT_CONS" again: вообще-то можно и оставитьотдельный под-протокол _CONS. Хотя... С одной стороны -- сепарируются изрядно разные вещи; а с другой -- придется по-прежнему иметь излишнюю кучу "отдельностей". Так что -- соединяем. (Но: можно иметь "маску разрешенных действий", для разных "под-протоколов" разную.)
    • 23.04.2009@Зеленоград: консольный интерфейс -- по обычному протоколу, совместно с данными, а не CT_CONS.
  • 13.09.2009: крупное изменение: в заголовке вместо поля PktSize будет всё-таки DataSize, т.е., объём ТОЛЬКО ДАННЫХ, БЕЗ заголовка.

    13.09.2009: смысл -- упрощение начального handshake'а, когда стороны еще не знают endianness друг друга. Чтобы не приходилось ни выпендриваться с чтением/не-чтением поля "размер", ни менять параметры (обоим) и endianness fdiolib-соединения (клиенту) по ходу дела. Благо, в fdiolib такой режим есть еще от рождения.

  • 22.11.2013: крупное изменение (в т.ч. по сравнению с v2): теперь ВСЕ данные в пакете сверх заголовка начинаются с унифицированного заголовка Chunk'а, в т.ч. и LoginRec.

    22.11.2013: кстати, по мере наполнения cx_proto_v4.h описываем всё в doc/cx_proto_v4.html.

    • Вводим собственно тип CxV4Chunk. Он похож на старый v2'шный CxDataFork, только покороче (БЕЗ _padding_'а), и поля 3,4 названы индифферентно param1, param2.
    • Т.е., радикальное отличие: если там были специализированные Fork'и на разные вещи, со своим специфичным наполнением (даже CxDbasFork был запроектирован), то теперь у всех стандартный заголовочек в 4 int32, а потом уже идёт специфичная часть.
    • В начало CxV4LoginRec'а добавлено поле ck -- ChunK (в реальности, конечно, сервером игнорируемое). Но клиентской реализацией заполняемое.
  • 24.11.2013: подтягиваем cxlib_client.c и cxsd_fe_cx.c к свежим вариантам из v2.

    24.11.2013: реализовываем фичу "application ping" (keepalives), сделанную в v2 еще три года назад

    • Введены CXT4_PING и CXT4_PONG.
    • Добавлена пустая реакция на оба этих пакета в:
      1. cxsd_fe_cx.c::HandleClientRequest()
      2. cxlib_client.c::ProcessFdioEvent()
    • Периодическая отправка возложена на cxlib (а не на cda), DoHeartbeatPing(). Период -- те же 300 секунд.
    • ...кстати, в v2 пакеты PONG не использовались совсем.

      И тут аналогично, и аналогично сервер клиента не пингует.

    24.11.2013: фича "WipeUnconnectedTimed()".

    • В cxlib_client.c просто добавлена (с мелкими модификациями вследствие отличающейся последовательности handshake-пакетов).

      Заодно, кстати, обнаружилась ошибка в v2'шном -- не-сброс clp->tid=-1 -- похоже, приводившая к глюкам с не-переподключениями к серверу.

    • В cxsd_fe_cx.c убрана старая "глобальная раз в 60 секунд" выкашивалка (ничего, кстати, не делавшая).
    • ...и добавлена per-connection, скопированная из cxsd_fe_cxv2.c.

    05.07.2023: но "скопировано" было криво -- детальный разбор см. ниже по тексту за сегодня.

    25.11.2013: прошерщена cxlib_client.c -- она вполне соответствует в той части, что есть (касательно установления соединения; более там ничего -- ни транспорта, ни юзерского API -- и нету).

    25.11.2013: cxsd_fe_cx.c аналогично. Но тут мелкие модификации были --

    • O_NONBLOCK на слушающие сокеты.
    • Логгирование IP-адресов (логика сделана более корректно, чем в v2).
    • Добавлена генерация ID. 22.10.2014: удалена.
    • Преобразование типа в принимаемом пакете в 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). Проявлялось при очень экзотичном сочетании условий:

    1. Коннектящийся запрещён правилами access control'а.
    2. Соединение идёт через AF_UNIX -- т.е., например, localhost:1; аналогичное через localhost:-8013 проблемы НЕ проявляло (видимо, из-за НЕмгновенного ответа от сервера).
    3. Сам клиент должен быть "заторможенным": например, cdaclient такого поведения НЕ показывал, а вот pult-клиенты -- показывали, причём (будучи запущены дистанционно: на x10sae удалённо с p320t) просто так -- лишь первый раз (ловя EPIPE из-за задержки при создания окна), а из-под strace -- как минимум дважды.

    Исправлено путём внесения заказа таймаута в обищий блок с переходом в 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: создание резолвера.

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

    Касается как cx_proto_v4.h (в нём определения), так и cxlib_client.c с cxsd_fe_cx.c

    25.12.2014: записки о сделанном и выбранных решениях.

    Основное:

    1. Самое главное: НЕ делаем операций "чтение" и "запись" -- т.е., таких, на которые бы требовался ответ.

      Вместо этого делаем операции ЗАПРОСА чения и записи CXC_RQRDC и CXC_RQWRC, непосредственных ответов НЕ предполагающих -- т.е., точное отражение интерфейсов cda и cxsd_driver.

      1. Отказ от операций с обязательным ответом очень упростил реализацию (иначе приходилось бы помнить: храня где-то информацию, и не забывая по готовности отправить ответ).
      2. Протокол стал максимально приспособлен к потребностям как верхнего, так и нижнего уровней, что тоже сделает реализацию straightforward.
    2. Парадигма работы такова:
      • Каналы, в которых клиент заинтересован, он серверу указывает -- "ставит в список мониторируемых".
        1. На них присылаются уведомления о смене свойств ({r,d}, fresh_age, ...).
        2. На них присылаются данные, в одном из вариантов:
          1. По обновлению.
          2. Раз в цикл.
          3. По изменению более чем на сколько-то.
          (последнее для каналов записи вряд ли осмысленно -- там скорее ВСЕГДА нужно первое)

        "Подписка" -- CXC_SETMON, отписка -- CXC_DELMON, плюс CXC_CHGMON для возможности менять дельту.

      • Первоначальные (текущие) значения вычитываются отдельным запросом CXC_PEEK, НЕ вызывающим чтения...
      • ...и присылающим данные в том же самом виде, что по подписке -- код CXC_NEWVAL (только сразу, в ответе на этот же пакет).

        Т.е., никакого различения в cxlib_client и выше не нужно -- любые присланные данные обрабатываются одинаково.

      • Когда даётся команда (запроса!) записи или чтения, то результат её исполнения -- уже в аппаратуре (а не восприятия сервером) будет прислан по мониторингу.

        ...а если затронутый канал не включен в список мониторируемых, то результат прислан и не будет.

      • Операция "запрос чтения" в протоколе (в отличие от API сервера) глубокого смысла не имеет -- чтение будет запрашиваться cxsd_fe_cx'ом. Но для симметрии/полноты пусть будет.
      • Отдельным запросом является CXC_RESOLVE: предполагается, что клиент пришлёт его до всех прочих, чтобы узнать маппирование имени и прочее.

        В ответ -- сразу! -- будет прислано 2 chunk'а:

        1. Низкоуровневых свойств данного канала -- {r,d}, строки, ...; ключ -- cpid. 25.12.2014: пока не реализовано.

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

          Именно ЭТИ свойства при изменении будут автоматически отправляться для мониторируемых каналов.

          Возможно, эти данные следует разбить на НЕСКОЛЬКО chunk'ов, по тем же границам, что в cxsd и cda: отдельно {r,d}, fresh_age, строки...

        2. Собственно маппирование (cpid, hwid) и основные свойства аппаратного канала (rw, dtype, nelems).
    3. Разнообразные chunk'и укладываются ВСЕ ВМЕСТЕ -- нет разных кодов пакетов "DATA_IO", "RESOLVE", "MONITOR", ..., а любые chunk'и идут прямо в пакетах DATA_IO. 10.04.2015: нифига -- про CXT4_DATA_MONITOR тогда, к счастью, было забыто и НЕ выкинуто. 26.04.2015: заодно CXT4_DB_SERV было забыто, а уж оно-то теперь точно лишнее -- выкидываем.

      При добавлении консольного интерефейса он будет так же гоняться вместе со всеми прочими, как и было решено еще 23-04-2009.

      Ответный пакет ВСЕГДА отправляется, но, поскольку пакет запроса состоял только

    Прочие детали:

    • Каждый chunk содержит информацию по ОДНОМУ каналу -- хоть чтение, хоть запись (хоть результат мониторирования).

      При надобности несложно было б изменить формат chunk'ов на "множественный" (как в v2), но глубокого смысла пока не видно -- нынешний подход проще.

    • А при добавлении параметризованных запросов/ответов -- там да, уже понадобится упаковывать наборы данных в один chunk.
    • Коды операций -- CXC_* -- организованы точно так же, как в v2:
      1. Состоят из 4 символов, первый из которых '>' для запросов клиент->сервер и '<' для ответов/уведомлений сервер->клиент.
      2. При отвечании коды запросов конвертируются в коды ответов заменой первого символа (младшего байта) макросом CXC_CVT_TO_RPY().
      3. Конкретно CXC_NEWVAL сразу является RPY-кодом (поскольку запроса такого не предполагается).
    • Немножко о внутреннем устройстве chunk'ов:
      • Все типы chunk'ов первым полем имеют стандартный заголовок CxV4Chunk.
      • Прямо в нём содержатся поля param1 и param2, являющиеся как бы аналогами privptr1/privptr2: т.е., служат для идентификации того, к какому каналу относится chunk.

        Во всех ответах (и уведомлениях) клиенту присылаются те же числа, что были указаны в запросе.

        Возможно, ДВА поля -- перебор, поскольку само соединение уже себя идентифицирует, и нужен еще только один "ключ", под hwr. Но уж пусть будет.

        ...кстати, CxV4Header.Seq тоже остаётся: хоть *синхронных* пакетов сейчас считай, что и нету (кроме резолвинга и консоли), но с мониторируемыми каналами оно будет слаться, и смысл ровно тот же, что и раньше -- отличение "той" подписки.

        01.03.2015: Вроде неиспользуемый param2 может быть полезен, если захочется один и тот же канал мерить по-разному в рамках одного "вида" измерения. (Да, вопрос -- ЗАЧЕМ :)).

      • "Ошибки" (наример, ненайденность канала при резолвинге) отдаются chunk'ами с соответствующими RPY-OpCode и с теми же значениями param1/param2.

        Хотя, похоже, всё ж надо бы добавить еще поле "статус".

      • (28.12.2014) в этом поле можно возвращать ровно те же коды ошибок CXT4*, что используются в CxV4Header.Type, чтоб их можно было так же конвертировать в клиентские коды E* через CXT42CE.

        В cxsd_fe_cx.c заготовка под это уже есть -- функции манипуляции мониторами возвращают 0 при успехе и -CXT4* при ошибке (подход подсмотрен в Linux kernel: там разные методы драйверов вертают -E* при ошибке).

    25.12.2014: некоторые генеральные мысли -- как где должна быть организована работа, чтобы всё происходило "как надо", максимально корректно и гибко.

    cxsd_fe_cx

    Всё просто:

    1. Обычные, безконтекстные запросы -- элементарно: исполнили, опционально отправили ответ, и забыли.
    2. Мониторируемые каналы: надо помнить список.
      • Всех-всех -- в общем SLOTARRAY-массиве.
      • И дополнительно по-цикловых -- иметь отдельный вектор cxsd_chanid_t[], годный для сбагривания CxsdHwDoIO() из cx_begin_c().
    3. Отправка клиентам -- зависит от типа мониторинга:
      1. По обновлению -- прямо из evproc'а и отправлять.
      2. По-цикленные:
        • в evproc'е помечать как "было обновление";
        • из cx_end_c() смотреть, что если был хоть один такой обновившийся канал, то проходить по списку цикленных и те, что обновились, отдавать клиенту (заодно сбрасывая флаг обновлённости).
      3. По дельте -- прямо из evproc'а проверять, что если дельта превышена, то отправлять (аналогично п.1).
    4. Прочие операции -- пока неясно, по причине их недопродуманности. Но весь локинг, по-видимому, будет похож на мониторирование, с держанием массивов затронутых каналов.
    cxlib
    • Большинство операций -- тривиальны: при запросе просто упаковываем этот запрос в сеть, а при получении ответа столь же просто дёргаем нотификатор.
    • Конкретно с мониторируемостями можно еще подумать: не сохранять ли факт мониторируемости в массивчике, где кроме номера канала хранить также и Seq?

      Пожалуй, незачем.

    • Обязательно же надо хранить результаты РЕЗОЛВИНГА. Соображения по этому вопросу:
      • ЧТО хранить?
        1. Резолвинг СВОЙСТВ -- да, всегда.
        2. А имён? Надо ли?
      • Инвалидировать кэш надо по специальным сообщениям от сервера, которые сигнализируют о событии "перезагрузка БД". Но, скорее всего, оная перезагрузка никогда реально сделана не будет, так что на эту часть можно и забить (при рестарте же сервера инвалидирование произойдёт автоматически, вследствие сдыхания соединения).
      • Функции API резолвинга могут как вернуть результат сразу (из кэша), так и вызвать отправку запроса серверу. Самым разумным выглядит, чтобы эти операции возвращали бы
        1. -1: ошибка,
        2. 0:запрос_послан,
        3. +1:результат_есть,
        и в последнем случае как-то отдавали бы запрошенные данные -- например, клиент может передавать указатель на структуру того же типа, указатель на которую передаётся при соответствующей нотификации, и заполняться будет она.

        УВЕДОМЛЕНИЕ о результатах асинхронного резолвинга -- по тем же privptr1/privptr2, плюс к ним еще param1/param2, описывающие hwr.

    • Broadcast-резолвинг: ???
    cda_d_cx

    (30.12.2014@обед-в-Амиго-ожидание-еды) для всех каналов делаем ЕДИНЫЙ, сквозной slotarray -- аналогично cda_core.c::refs_list[]. Это:

    • Позволит занедорого сделать миграцию каналов (hwr'ов) между серверами (sid'ами).
    • Упростит резолвинг каналов (для которых не указан сервер), поскольку не потребуется иметь отдельный массив-отстойник для неразрезолвленных каналов.

      СКОРЕЕ ВСЕГО не понадобится. Поскольку периодически повторять резолвинг всё же надо; возможно, неразрезолвленные помещать в список (адресуемый по hwr), а уж как дальше с ним быть -- смотрим в разделе про резолвинг.

    • Следствие: при использовании только cx'ных каналов их ref'ы и hwr'ы будут совпадать -- по причине синхронного одноалгоритменного отведения.
    cda_d_cx - резолвинг
    cxclient
    • При парсинге командной строки складывать описания каналов в массив.

      Индекс в массиве -- аналог hwr'а в cda_d_cx.

    • После cx_open() пройтись по массиву, делая:
      1. если ссылка выглядит просто числом -- то сразу заказывать мониторирование;
      2. иначе заказывать резолвинг; при положительном результате которого заказывать мониторирование.
    • Вопрос 1: как обходиться с {r,d}: делать ли конвертацию по ним перед печатью, или уж печатать по тому dtype, что пришел?

    12.01.2015: некоторые усовершенствования по выравниванию.

    1. Все chunk'и теперь выравниваются не по границе 8 байт, как раньше, а по границе 16.

      Т.е., удовлетворит требования по выравниванию даже на 128-битной архитектуре. Например, с пока слабо-портабельным типом float128 (long double, поддерживаемым Linux@x86_64).

      ...кстати, даже в Linux@x86 malloc() отдаёт куски, выровненные по 8 байтам.

      См. также http://www.viva64.com/en/l/0021/

    2. Для формализации выравнивания введёна inline-функция CXV4_CHUNK_CEIL(), возвращающая (size + 15) &~15U.
    3. Все типы chunk'ов добиты до кратности 16 байтам (а CxV4Chunk изначально был таким, как и CxV4Header).
    4. В CxV4Chunk добавлено 4 штуки uint32 "на вырост" -- в первую очередь под запланированное поле "статус ответа".

      Это не очень хорошо для UDP-резолвера, поскольку там при ограничении 1440 на данные каждый байт становится на счету, и тратить 16 байт впустую жалко.

      Но там можно сделать проще: чтоб пакеты UDP-резолвинга вместо chunk'ов содержали бы в разделе данных просто набор NUL-terminated-строк. А уж ответы будут слаться обычным образом.

      После обеда: вариант с просто строками покатит, только если клиент по ответу будет искать у себя в запросах тоже только по строкам; что, в принципе, можно. Если же хочется в запросе гнать еще param1,param2 -- то не прокатит. Впрочем, можно просто ДРУГОЙ формат chunk'ов для резолвинга использовать. ...хотя лучше всё же всё по строкам :)

  • 04.12.2015: (16:20:38) какой-то странный косяк вылез: при рестарте сервера ring1:12 (ringmag+ringcor*) почему-то ЧАСТЬ каналов -- у ОДНОГО устройства rst4_r3 -- в программе ringcor45 почему-то стала отображаться будто без драйверова r=1000000. Т.е., дикой длины числа. (Gif'ка 20151204-ringcor45-rd-bug-3L7-1000000times.gif лежит в p8b2:~bolkhov/.)

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

    Часом позже: а вот не похоже.

    • Сделан для тестов test_rd_drv.c, с каналами w3i {result:0; input:1; coeff:2} -- возвращает в канал result число, записанное в канал input, плюс при записи числа в канал coeff ставит его как R для канала result.
    • Так вот -- фиг, на нём всё работает как должно.

    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'е, который НЕ был запущен локально с логгингом.

    • Судя по клющиным словам, оно было в CEAC124. По логам -- ничего, совсем ничего; хотя все теоретические косяки, могущие воспрепятствовать передаче калибровки драйвером серверу, снабжены логгингом.
    • Еще раз проверил, что да как в реализации протокола с передачей калибровок:
      • Сам cxlib команду CXC_RDS никогда не посылает.
      • Зато cxsd_fe_cx шлёт её RPY-вариант в 2 случаях:
        1. при начальном коннекте (в ответе на CXC_RESOLVE);
        2. по событию CXSD_HW_CHAN_R_RDSCHG.

        Т.е., на вид -- вроде всегда, когда надо.

        И тесты (на vepp4-pult6:2, с запуском v4c4lcanserver ПОСЛЕ cxsd) показывают, что обновляется когда/как положено.

      • Единственный косячок, могущий запутать -- Chl_knobprops обновляет строчку {R,D}s только первоначально, при смене target-ручки. Потом же, по обновлению данных от сервера -- нет.
      • ...и, кстати:
        • Цепочка собирается при раскрутке cpoint'а так, что аппаратная калибровка оказывается в конце списка, но потом 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. Наблюдения:

    • Сервер СВЕЖИЙ -- собран прямо тогда перед запуском из последнейших исходников (потому, что в имевшейся версии от 29-03-2016 косячила работа simulated/noop-каналов в отношении определения is_update (CURVAL/NEWVAL)).
    • Дуркануло опять чётко по границам устройств -- часть да, часть нет.
    • Отладочная печать в cda_dat_p_set_phys_rds() показала, что ДА, пришла информация с phys_count:=0.

      Что свидетельствует -- это косяк не в клиенте, а именно в сервере.

    А теперь странности:

    • Косячности присутствуют и в запущенной на пульту программе -- "-" вместо {10.0,0.0}.
    • И более того -- в ней часть пределов видится как "256" и raw тоже "256"; а в нормальной программе они видны как "26".
      • Т.е., из режима были загружены именно они --- raw=256; т.е., не была проведена конверсия при считывании.
      • Т.е., выходит, что НЕСКОЛЬКО программ практически одновременно получили такой "подарок" в виде неполных коэффициентов.

        Ну оно и немудрено --

        • программы реконнектятся примерно одновременно, в течение 1 секунды (ReconnectProc() вызывается через 1000000us);
        • а скорее -- даже СОВСЕМ одновременно, т.к. эта 1с таймаута начинает отсчитываться с момента обрыва связи, каковой происходит со всеми одновременно (вследствие закрытия дескрипторов при убитии сервера), ...
        • и разница только из-за задержки "доставки" события до каждой софтины.
    • На некоторые каналы, судя по консольному выводу, пришло phys_count=0, но Chl_knobprops показывает у них присутствие [1]{r,d}!!!

      Поиск далее показал, что чуть попозже присутствует phys_count:=1 (именно у НЕКОТОРЫХ! каналов).

      Bingo -- это и есть ответ!

    Итого:

    • Вывод: это позорный тупой race condition!!!

      "Проблемные" каналы резолвятся ДО прихода REMDRV_C_RDS, а SETMON им заказывается уже ПОСЛЕ прихода, а потому события RDSCHG они и не ловят!

      • Собственно результат "race" заключается в том, что приход REMDRV_C_RDS попадает в узкий таймслот между резолвингом канала и подпиской на него.
      • Если поразмыслить, то не такой уж слот и узкий: в этот момент "на приём" к серверу толпится море посетителей -- куча клиентов и куча удалённых драйверов.
    • Решение: чтоб cxsd_fe_cx делал PutRDsChunkReply() и по SETMON тоже.

      Например, при SetMonitor()==0.

    • Да, так и сделано. 2 замечания:
      1. Проверить, что работает и нет деградаций (проверить пофиксенность бага -- затруднительно, надо как-то попадать в слот).
      2. Получается, что теперь RPY(CXC_RDS) отправляется ДВАЖДЫ. Что явно неоптимально.

        И, возможно, с RDS косяк был не единственным.

        • Например, cda_d_cx.c делает запрос на чтение текущего -- 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@утро, по-пути-на-работу, задворками ИПА и мимо мыши, плетущей ДНК: вдогонку о неудачности модели:

    • Проблему модели "с пересчётом на стороне клиента" можно и решить, явочным путём: научить СЕРВЕР производить пересчёт, и производить его именно непосредственно в момент отправки драйверу (это, кстати, решит оставшуюся проблему ("race condition") «конверсия произошла хоть и после коннекта к серверу и прихода "первого транша {r,d} (логических)", но еще ДО инициализации драйвера и, следственно, ДО прихода "аппаратной" калибровки»).
    • Для этого нужна какая-нибудь модификация в протокол -- например, чтобы клиент указывал серверу, что он НЕ делает конверсию (а лучше, наоборот -- что ДЕЛАЕТ). Ну и, учитывая распределённость -- надо, чтобы и с данными приходил флажок "конвертированные/сырые" (по умолчанию (флаг=0) -- сырые).

      Неприятно, конечно, начинять сервер (cxsd_hw? cxsd_fe_cx?) этой мутотой, но зато решение.

    • ...а проблему оставшегося race condition можно, теоретически, решить на уровне cda так же, как она решена в cdaclient: слать запись только после прихода данных.

      Но лучше бы воздержаться: слишком уж клиенты при этом ограничиваются.

    • 08.05.2018: увидена еще одна проблема модели "стараемся держать всё в целых": понадобилось завести прямо в драйвере канал, содержащий квадрат от измеряемых вольтов. Но коэффициент пересчёта микровольты->вольты -- 1000000, т.е. 10^6, и при возведении микровольтов в квадрат получались бы числа порядка 10^12, что в 32 бита уже ну никак не полезет.
      • Сейчас было решено путём делания этого канала FLOAT64; а в v2 нормального решения бы вообще не было, разве что...
      • ...можно б было пересчитывать хоть через double, хоть через int64, потом деля на 1000000 (оставляя второй 10^6), чтобы отдавать уже в микроваттах, которые вполне поместились бы в int32.

      (Детали потребности -- в разделе о kurrez_cac208_drv за сегодня.)

    01.10.2016: в реакцию на CXC_SETMON добавлен также вызов PutFrAgChunkReply() -- иначе часть каналов (конкретно OUT_CUR'ы источниковых ЦАП/АЦП) посиневала, ровно из-за такого же race condition. ...отсылка кванта туда была добавлена сразу же при внедрении квантов.

    Теперь вроде бы с чудесами неконвертации/посинения должно быть покончено.

  • 29.02.2016: добавлено, что в CxV4LoginChunk.ck.res{1,2,3} передаются клиентовы pid, ppid и uid, соответственно. А в HandleClientConnect() они логгируются вместе с username@IP:progname. Цель -- для упрощения возможных разбирательств.
  • 23.06.2016: исторически значение 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.

  • 06.04.2017@утро-лыжи: чисто на будущее, когда, возможно, будет БОЛЕЕ одного "цикла" (типа как в EPICS'е есть несколько периодов): может, сделать, чтобы вместе с CXT4_END_OF_CYCLE отправлялся бы еще как-нибудь "идентификатор цикла"?

    Например, передавать в var1 "номер" (ID) цикла, а в var2 его период в микросекундах.

  • 01.12.2017: код CXT4_RESOLVE переименован в CXT4_SEARCH.
  • 06.12.2017: введён код CXC_SEARCH, на который и переведён (с CXC_RESOLVE) весь UDP-резолвинг.
  • 11.01.2018@лыжи-после-обеда, конец 2-й 2-ки: некоторое соображение насчёт текущей реализации: сейчас SEARCH-запросы, в отличие от обычных соединений, не обеспечивают передачи никаких данных о запрашивающем. Следовательно -- на этом уровне никакого "access control" и "защиты информации" нет.

    Не то чтобы прям сильно надо (в закрытой СУ-то), но чисто по архитектурно-идеологическим соображениям -- не помешало бы.

    Надо будет тогда менять список параметров в cxlib'овском API?

    12.01.2018: анализируем ситуацию.

    • Нет, в cxlib'овском API ничего менять не надо: там в cx_seeker() УЖЕ передаются те же argv0+username, что и в cx_open(), и они так же складируются в v4conn_t -- общей create_a_cd().
    • А как можно было бы передавать credentials -- в cx_begin() для CT_SRCH-объектов автоматически вставлять "нулевой" chunk с кодом CXT4_LOGIN.

      К сожалению, особых плюсов у этого варианта нет, а минусы есть:

      • Не-плюс: информация всё равно определяется клиентом, доверять ей не особо можно (да и бегает в открытом виде), так что в текущем варианте использовать argv0/username для авторизации бесполезно -- так, скорее чисто для информации.
      • Минус: будет отжираться драгоценное место в 1500-байтном UDP-пакете.
    • Формально, если что и было бы корректным -- чтоб сервера, жаждущие авторизации, в ответ на CXT4_SEARCH спрашивали бы "а ты кто такой", требуя прислать пакет запроса еще раз, но с credentials.

      Т.е., де-факто чтоб был "диалог" (нечто вроде handshake) вместо просто broadcast'ного запроса "эй, кто знает?".

      А такой диалог откровенно противоречит самой сути подхода.

    В общем, всё это -- так, скорее пустые мысли (вызванные, очевидно, грустным интеловским косяком с кэшем+out-of-order-execution (Meltdown+Spectre)), никакого практического смысла не имеющие.

  • 22.01.2018@вечер-лестница-пристройки: с UDP надо аккуратно, чтобы нельзя было послать такой хитрый пакет с source-IP самого сервера и source-port=8012, когда он начал бы сам себе бесконечно отвечать. Т.е., надо чтобы пакет ответа никак не мог бы распознаться как пакет запроса.

    Насколько сейчас форматы совпадают/различаются?

    26.01.2018: как минимум, код пакета в обоих случаях одинаковый -- CXT_SEARCH, что плохо. Надо бы ответный с другим кодом.

    27.01.2018: да, вводим отдельный код --

    • CXT4_SEARCH_RESULT=14*0x01010101
    • cxsd_fe_cx.c::ServeGuruRequest(): добавлена замена CXT4_SEARCH на CXT4_SEARCH_RESULT. Причём без учёта endian-нейтральности кодов -- просто явно делается h2l_u32() либо h2b_u32().
    • cxlib_client.c::HandleSrchReply() -- сравнение теперь с ним.

    Теперь проверять.

    30.01.2018: да, проверил, работает (ну еще бы -- изменения-то тривиальные).

  • 24.01.2018: кстати, а насколько правильно, что у нас размер UDP-пакета -- CX_V4_MAX_UDP_DATASIZE -- базируется на обычном пакете, а не на jumbo frame?
    • Ведь, с учётом возможности фрагментации UDP-пакетов, объём 10kB вполне безобиден и в случае 100Мбит- и даже 10Мбит-сети.
    • А уж в 10kB влезло бы большинство запросов от большинства софтин.
    • С другой стороны, размер jumbo frame толком не определён и может быть от 9000 до 16000 байт.

    Вывод: пока оставим всё как есть -- отталкиваемся от 1500 байт.

  • 28.10.2019@утро ~9:40, дорога на работу, около ИЦиГ, под аркой: если когда-нибудь сделаем нечто вроде "gateway'ев", которые "frontend'ом" отдаются клиентам, а "backend'ом" смотрят на какие-нибудь сервера, то встанет проблема "а как сказать клиенту, что backend-side сервер, обслуживающий конкретный канал, отвалился?".

    Чуть подробнее -- суть проблемы:

    • Обычно клиенты осознают, что надо повторить резолвинг канала (или просто перейти в состояние "давай-ка приконнектимся!") по факту закрытия соединения.

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

    • Но в случае gateway'я этот паттерн работать не будет, поскольку ОДИН gateway будет за собой маскировать МНОГО серверов.
    • И надо бы как-то уметь сообщать клиентам о необходимости перевести в состояние "неразрезолвлен".

    28.10.2019@утро ~9:40, дорога на работу, около ИЦиГ, под аркой: ну и как это делать?

    Очевидно -- надо в таких случаях присылать специальное по-канальное сообщение "забудь" (CXC_FORGET, 'Fgt'?).

    • Покамест -- просто в протоколе добавить код, в клиентской библиотеке ничего срочного предпринимать не требуется. Т.к. это на будущее, "на вырост" -- когда сделаем, тогда и будет.
    • И версию протокола это менять не потребует -- когда сделаем, тогда и "прорастёт"; критичного в этом функционале нет.
    • И реализация СЕРВЕРНОЙ стороны будет касаться только будущего/потенциального gateway'я, а cxsd_fe_cx.c (нынешний) это не затрагивает.

    ЗЫ: осознал и обдумал это всё (начиная с заголовка раздела) за считанные секунды, а на записывание ушло изрядное время.

    29.10.2019: отдельный аспект -- как на стороне клиента поступать с каналами, у которых УКАЗАНО имя сервера (gateway'я)? Т.е., которые RSLV_TYPE_NAME.

    Клиент-то должен сразу попробовать выполнить повторный резолвинг (соединение-то уже есть!), но оный резолвинг тут же и обломится. А cda_d_cx.c::ProcessCxlibEvent() при этом переведёт канал в состояние RSLV_STATE_ABSENT, которое так и останется до следующего обрыва соединения.

    Так что надо будет предусмотреть какой-то механизм для таких случаев:

    1. То ли в cda_d_cx.c -- чтобы он с некоторой периодичностью стучался с попытками повторить резолвинг.

      Но это некрасиво -- рулетка, да и попахивает EPICS'ом.

    2. То ли в протокол добавить специальное сообщение со смыслом «у меня тут кое-что случилось ("пополнение в семействе"), так что можешь попробовать повторить резолвинг».

    Вот второй вариант мне нравится намного больше -- никакого слепого тыканья, всё осмысленно в нужный момент (кстати, подобный положительный опыт уже был раньше -- при переводе механизма 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:

    1. Некий сервер S владеет каналом X.
    2. Клиент C запускается и спрашивает UDP-резолвингом "у кого канал X?".
    3. Гуру (которым может быть и сам S) отвечает: "каналом X владеет S!".
    4. ...в этот момент S перезапускается, уже с другой конфигурацией, НЕ включающей X.
    5. Клиент C коннектится к серверу S и просит канал X.
    6. ...но канала X у сервера S уже нету!

    И вместо того, чтобы перевести соответствующий dataref обратно в режим RSLV_STATE_UNKNOWN (и повторить резолвинг!), этот dataref так и останется в RSLV_STATE_ABSENT до тех пор, пока соединение с сервером S не оборвётся.

    А с исправленной диаграммой этот косяк исчезнет.

    30.10.2019: делаем наипростейшие и ни к чему не обязывающие шаги -- поддержку в протоколе и в cxlib.

    ...и тут некоторая засада: как назвать?

    • Всякие "update" и "refresh" -- ну никак не катят, поскольку намекают скорее на обновление ДАННЫХ, а не списка имён.
    • Потом пришла идея -- "перепись" или "перекличка".
    • Перепись -- это "census". Для русскоязычного тоже так себе, т.к. перекликается с "цензура", сиречь фильтрация (access control etc.).
    • А среди толпы переводов "переклички" нашлось слово "muster", которое относится в первую очередь к военным, плюс никаких аллюзий в русском вроде не имеет.

      Вот "MUSTER" и выбран.

    Собственно шаги:

    • cx_proto_v4.h: введён код CXT4_MUSTER=32*0x01010101.

      Т.е., он endian-independent, что потенциально позволит его использовать даже с недо-приконнекченными клиентами. Хотя правильным будет НЕ слать его клиентам, у которых state<CS_USABLE.

    • cxlib.h: добавлен CAR_MUSTER.
    • cxlib_client.c: тривиальная трансляция пакета CXT4_MUSTER в событие CAR_MUSTER.

    На этом пока всё и проверять пока что нечего и не на чем.

    Если идти дальше, то надо

    1. Модицифировать диаграмму работы ProcessCxlibEvent() с резолвингом,
    2. а потом делать в cda_d_cx.c обработку события MUSTER.

    ...но тут возникает любопытное соображение: КАК реагировать на такое событие?

    1. По поставленной в данном разделе задаче -- пройтись по всем hwr'ам сервера и повторить резолвинг тех, которые в состоянии RSLV_STATE_ABSENT.
    2. Но если использовать механизм MUSTER также для сообщения клиентам об обновлении состава оборудования (пере-чтение БД без рестарта сервера) -- каковое использование прямо напрашивается, то надо повторять резолвинг ВСЕХ каналов, кроме 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 (по хорошему -- унести бы этот кусочек в отдельную функцию и вызывать её из обоих мест).
    • Но сделано ТОЛЬКО это -- поддержки возможности принимать negative-resolving_result на уже разрезолвленные каналы (т.е., с !=RSLV_STATE_SERVER) так и НЕ сделано.

    28.04.2020: сделать-то сделал, но ведь так и не протестировал! А надо бы...

    24.04.2020@душ, вечер, заполночь: если хорошенько подумать, то становится очевидно, что все эти махинации с MUSTER и поштучной инвалидацией каналлв -- в принципе не способны дать гарантии надёжной работы: в этой схеме присутствует race condition.

    • Возможна ситуация, когда сервер произвёл какие-то модификации в карте каналов, отправил клиенту уведомление о необходимости пере-резолвинга, но по сети УЖЕ летят запросы, отправленные ранее, и в них указаны hwid, ставшие некорректными. И у сервера нет НИКАКОГО способа определить, что эти запросы были отправлены ранее и их следует игнорировать.
    • Т.е., корень проблемы в том, что у нас hwid'ы повторно используются. Вот если бы в "обновлённой БД" все новые каналы получали бы новые id'ы, а старые становились бы просто невалидны -- особой проблемы бы не было; но с нашей моделью cxsd_hw_channels[] и cpoint'ов это невозможно.

      28.04.2020: а всё это потому, что у нас ПРЯМАЯ адресация по gcid'ам, вместо предварительной регистрации и дальнейшей адресации по (не)прозрачному handle -- в том варианте проблема "смены hwid'ов" бы не стояла, т.к. оная смена происходила бы на стороне сервера, "атомарно" (без обменов по сети и race condition'ов), а клиент продолжал бы работать с ранее полученным handle.

    • Чисто теоретически можно было бы использовать тот же приём со счётчиками, что и в протоколах CCD-камер: вместе с запросами всегда присылать "код-номер инкарнации серверовой БД", и чтоб сервер игнорировал запросы, содержащие "не тот" код.
      • Учитывая, что переконфигурация -- событие редкое, особо большого диапазона этих кодов даже и не требуется, хватит буквально пары бит: ведь основная цель -- отлавливать такие граничные ситуации, когда на одной стороне БД изменилась, а другая ещё не узнала и успела ранее отправить команды для старой БД. Тут, по идее, и вовсе бы 1 бита хватило бы.

        Но приличия ради должен быть хотя бы 1 байт.

      • И вот куда этот байт запихивать? Конкретно БАЙТ -- в принципе, влез бы и в gcid'ы: если сделать на стороне сервера, что номера cpoint'ов не вылазят за 24 бита (т.е., сменить CXSD_DB_CPOINT_DIFF_MASK с 30-го бита на 23-й), то cxsd_fe_cx мог бы клиентам сбагривать "композитные" hwid'ы, состоящие из дуплетов {hwid,DB_ID}, а при получении запросов проверять старший байт на соответствие.
      • Но тут становится ясно, что даже и 1 байт никак не даст гарантии: если клиент "далеко", или, например, SIGSTOP'нут, то может пройти много циклов.

        ...ну да, это сценарий малореалистичный, но всё же возможный.

        И даже использование 32-битного кода "инкарнации" именно ГАРАНТИИ -- не даст.

    • Ещё напрашивается использование timestamp'ов, но этого делать нельзя -- нет вообще НИКАКОЙ гарантии синхронизации часов между узлами. Для посинения -- OK, это уже вопрос оценки юзером; а вот транспортный протокол на такое завязываться не имеет права.
    • Что же теоретически реально ГОДИТСЯ в качестве такого таймера -- так это поле CxV4Header.Seq: можно в момент отправки клиенту уведомления о необходимости пере-резолвинга запоминать номер последнего присланного клиентского пакета и... (Дальше тут было о том, что все запросы, полученные "в период неопределённости" -- после модификации БД, но ДО того, как клиент получит уведомление о модифиуации -- считать невалидными и просто игнорировать.)

      ...нет -- чё-то тут не то. Да, это Seq в принципе исполняет роль таймера, но оно назначается КЛИЕНТОМ, а нам нужно отмерять от момента, устанавливаемого СЕРВЕРОМ.

    • А -- во: в момент отправки уведомления о смене БД выставлять в v4clnt_t флажок "клиент ещё не подтвердил получение уведомления о модификации БД, так что игнорировать все его дальнейшие запросы на чтение/запись/мониторинг".

      Но и этот подход фигов -- см. анализ в следующем пункте.

    • Все эти идеи имеют общие недостатки:
      1. Запросы, полученные сервером "в период неопределённости" будут просто выброшены -- это прямое нарушение правила "раз клиент отправил запрос, то он будет рассмотрен". Т.е., получается этакая "потеря пакетов".

        Можно, конечно, отправлять в ответ код как бы ошибки "переспроси после пере-резолвинга", но это накладывает шибко высокие требования на cxlib+cda_d_cx -- там придётся эти запросы как-то помнить; да и вообще, см. п.2.

      2. Ну ОЧЕНЬ сильно усложнится функционирование клиентской части:
        1. при получении уведомлений о модификации БД ей (cda_d_cx) придётся проводить повторный "ритуал резолвинга";
        2. а главное -- если были отправлены какие-то запросы, попавшие в период неопределённости, то их придётся повторять.

          Для чего придётся перейти от нынешней простой модели "отправил и забыл" к "хранить отправленный запрос у себя, пока не придёт подтверждение".

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

        Хотя тут есть радикальный вопрос: а НАДО ЛИ повторять запросы, или их действительно надо отбрасывать? Наверное, так:

        1. СЛУЖЕБНЫЕ запросы -- вроде заказа мониторинга -- действительно нужно повторять.

          И для них уже есть "процедура повтора" -- при переходе канала из состояния "хбз" в OPERATING.

          ЗАМЕЧАНИЕ: очевидно, что при такой "модификации БД", затрагивающей hwid'ы, должны автоматически сниматься все мониторы и блокировки -- прямо frontend'ом. Чтобы cda_d_cx просто повторял бы всю процедуру наложения -- как при обычном реконнекте.

        2. А вот запросы на запись, похоже, надо ОТБРАСЫВАТЬ, а не помнить. Т.к. у этих каналов, как минимум, могут поменяться калибровки.

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

    ВЫВОД: внедрение всех этих механизмов обойдётся очень дорого, ибо они сложны (и заодно эта сложность убъёт простоту и элегантности реализации), а "нужно" оно -- для весьма экзотических и даже скорее теоретических сценариев. Очевидно, что игра не стоит свеч -- не будем мы этого всего делать.

    Пусть при смене БД по-прежнему делается закрытие соединения и реконнект -- оно и надёжно (точно нет никаких "запросов «в полёте» в момент неопределённости), и просто в реализации.

    25.04.2020@(после записи всей этой простыни с анализом): если делать, чтобы живой сервер -- без рестарта -- мог бы перечитывать БД, то можно перед закрытием соединения из cxsd_fe_cx посылать специальное сообщение, чтоб cda_d_cx бы 1) выставлял флаг is_suffering (для предотвращения выдачи бессмысленного сообщения на stderr); 2) выполнял бы reconnect СРАЗУ ЖЕ, а не через 2 секунды.

    Т.е.,

    1. Исправление "диаграммы работы в отношении каналов RSLV_TYPE_GLBL" -- сделано, оно в любом случае было необходимо.
    2. А вот всё остальное -- включая MUSTER -- похоже, так и останется теоретическим исследованием.

    P.S. Обдумана большая часть написанного ("фигова эта модель, лучше пусть будут реконнекты!") буквально за секунды или минуты, а записано всё уже 25.04.2020 -- часа два вбивал.

    28.04.2020: а возможно и не так всё плохо! Если сменить, как сегодня обдумано, модель обменов с "прямой адресации по hwid'ам" на "сначала резолвим/регистрируем канал, а потом работаем с handle'ом зарегистрированного", то проблема всех этих race condition'ов исчезает.

    Всё получится просто само собой, отпадёт необходимость во всяких инкарнациях/таймерах/..., поскольку не будет никаких "периодов неопределённости".

    А вот MUSTER -- возможно, останется, хотя и станет несложным.

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

  • 30.04.2020: на днях пришло осознание, что нужно переделать протокол: чтобы клиенты адресовались бы не напрямую к аппаратным каналам по gcid'ам, а:
    • cначала бы выполняли операцию "открыть канал" (open), получали бы в ответ некий handle (указывающий на внутренний объект в cxsd_fe_cx в рамках данного соединения),
    • и потом бы уже все операции делали бы через этот handle;
    • в конце же делали бы "close", либо автоматическая подчистка по закрытию соединения.

      В любом случае, это позволяло бы cxsd_fe_cx корректно подчищать все ресурсы, как-либо аллокированные/затребованные клиентом (чего сейчас сделать невозможно -- например, блокировки, которые нигде у cxsd_fe_cx запомнены быть не могут).

    Т.е. -- работа как с файлами.

    И для производства данного существенного изменения в протоколе и создаётся этот раздел. Для начала тут будут записи мыслей/проектов (начались они, кстати, в предыдущем разделе -- см. за 28-04-2020 там и в разделе по локингу каналов).

    30.04.2020: немного вдогонку к модели "работаем не по gcid, а через handle":

    1. @пешеходство по квартире с рюкзаком с 5л, ~17:00: Очевидно, что в той модели -- клиент общается с сервером в терминах handle'ов, маппирующихся на "мониторы", а при смене БД "на лету" модуль cxsd_fe_cx перерезолвливает имена в gcid'ы -- понадобится эти запрошенные имена хранить на стороне сервера, для возможности повторного резолвинга.

      Так вот, чтобы не делать по malloc()'у на каждый канал-монитор, можно завести общий пул строк -- аналогично strbuf в CxsdDbInfo_t, и в "мониторах" записывать не char*, а оффсеты в этом общем пуле.

      Понятно, что тут не будет такого однозначного выигрыша, как в CxsdDb -- просто потому, что дублей строк будет крайне мало (но поиск по пулу делать надо -- для случаев повторной регистрации тех же имён). Но всё же это будет существенное сокращение фрагментации памяти -- один крупный буфер вместо десятков-сотен мелких. И да, тут надо растить его аналогично CxsdDb'шному -- по килобайту; так у большинства клиентов пул будет вмещаться в первый же аллокированный килобайт.

      ЗАМЕЧАНИЕ: но всё-таки эта идея на очень дальнюю перспективу -- когда появится хоть какая-то возможность динамически менять БД.

    2. @при реализации локинга в cxsd_fe_cx.c: есть потенциальный косяк и в модели "сначала регистрируем, а потом работаем через handle": если клиент регистрирует/разрегистрирует каналы и потом опять снова регистрирует, то опять же может возникнуть race condition:
      • Клиент создаёт канал, ему даётся hwr=N, а при регистрации он получает handle=M.
      • После каких-то действий канал грохается, ...
      • ...при этом серверу уходит запрос на удаление.
      • Но ещё ДО получения подтверждения операции удаления клиент регистрирует новый канал (с иным именем), которому даётся тот же hwr=N, и серверу посылается запрос на регистрацию.
      • И тут вдруг прилетают сообщения, находившиеся "в полёте" в сети ещё до удаления старого канала. И у них значится param1=hwr.

      В результате чего cda_d_cx может по ошибке принять на счёт нового канала никак его не касающиеся сообщения для старого.

      Типа обсуждение:

      • В БУДУЩЕЙ модели с handle'ами, возможно, проблема как бы не возникнет: свежесозданный канал будет находиться в состоянии "регистрируемся", и пока от сервера не придёт ответ "ты зарегистрирован с handle=M", какие-либо иные сообщения для него принимать нет смысла.
      • А вот СЕЙЧАС эта проблема как раз стоит: ведь сейчас нет процесса/стадии регистрации, а полученные сообщения просто транслируются наверх в cda_core после простого восприятия param1 как hwr.
      • Решение пришло в голову быстро:
        • Передавать туда и обратно вместе с основным "ключом" param1=hwr ещё дополнительный проверочный, являющийся постепенно увеличивающимся числом, инкрементирующимся при каждом GetHwrSlot().

          Учитывая, что назначение его -- защита от "сообщений в полёте" на время разрегистрации/новой_регистрации, тут также не нужен особо длинный счётчик, и уж 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 частей:

    1. CX-протокол:
      1. Новые коды chunk'ов.
      2. Определения структур chunk'ов.
    2. cxsd_fe_cx: модификации будут небольшими - можно использовать имеющуюся инфраструктуру мониторов, а просто добавить поддержку операций OPEN и CLOSE, плюс модифицировать всё остальное на взаимодействие с клиентом в chnh вместо gcid/cpid.
    3. cxlib - отражение модификации протокола:
      1. Новые API-вызовы - "open_chan()" и "close_chan()" вместо setmon()/delmon().
      2. Новый/обновлённый механизм уведомлений о событиях в терминах chnh.
    4. cda_d_cx: на вид всё тривиально.

    Кстати, немножко насчёт идеологии/методологии -- о векторности/единственности:

    • Внутри сервера операции с каналами множественные ("векторные"): и CxsdHwDoIO() получает сразу пачку, и ReturnDataSet() возвращает тоже пачку.
    • Также в протоколе/cxlib запросы по аналогии сделаны векторными.
    • ...но по факту оно не используется: cda_d_cx ВСЕГДА работает с каналами поштучно.
    • Так что и протокол надо переводить на поштучность.
    • Но вот ВНУТРИСЕРВЕРНЫЙ API пусть будет векторным - чтобы серверовы модули имели бы чуть более широкие возможности.

    20.05.2020: приступаем к кодингу.

    • Начинаем с определений протокола в cx_proto_v4.h.
      • Для начала -- коды chunk'ов.
        1. Коды запросов получают имена CXC_CH_nnn (CHannel), значения -- CXC_REQ_CMD('H',,) ('H' - Handle).
        2. Коды уведомлений выделяются в отдельную группу, с именами CXC_NT_nnn (NoTification), и со значениями CXC_RPY_CMD('N',,).

          В том числе в эту группу переходят CURVAL и NEWVAL.

      • Далее форматы данных -- сами chunk'и. Пока только заготовки.
      • Имена форматов унифицированы с кодами: CxV4_CH_nnn_Chunk и CxV4_NT_nnn_Chunk.
    • И делаем определения cxlib-вызовов.

      Тут также унификация по именам: например, код CH_OPEN генерится вызовом cx_ch_open().

    • Итого -- полная унификация: код CXC_CH_OPEN, формат CxV4_CH_OPEN_Chunk (21.05.2020: для _CH_ -- уже не нужно), вызов cx_ch_open().
    • Возникло некоторое сомнение: а насколько корректно будет передавать серверу param1,param2 только в OPEN, а потом ограничиваться передачей channel-handle'а (рассчитывая, что у сервера они уже сохранены)?

      Нет ли тут потенциального race condition в моменты удаления одних каналов и добавления других? Не нужно ли добавить (param1,param2) во ВСЕ cxlib'овские вызовы?

      Надо взять паузу на подумать.

      @вечер: пока делаем так, а если вдруг что вылезет -- переделаем.

      22.05.2020@утро, завтрак, банан: вроде есть понимание:

      • Возможный race condition -- это когда мы делаем CH_CLOSE и потом сразу же CH_OPEN, и новый канал имеет тот же param1=hwr, а от сервера в этот момент уже летят по сети какие-то данные для старого канала с этим же hwr; вот тогда они могут быть ошибочно восприняты как для нового.
      • Как бороться:
        1. Для каналов, которым отправлено CH_OPEN, но ещё не получен ответ на него -- игнорировать все уведомления.
        2. Альтернативный вариант -- использовать param2 в качестве проверочного "ключа", постоянно увеличивающегося на 1, как было придумано 30-04-2020.

          ...но это халтурновато -- вариант (1) корректнее.

    21.05.2020: продолжаем -- уже с cxlib'ом.

    • @утро, пробежка по лестнице вверх с продуктами из магазина: было несколько разных мыслей о том, как именно делать новые исходники, чтобы не портить работу старого протокола; в т.ч. делать файлы вроде "*_new.c (cda_d_cx_new.c), которые потом надо будет просто переименовать.

      Но уже сейчас видно, что в cxlib.c и cxsd_fe_cx.c можно сделать поддержку прямо в существующих -- она вроде не помешает работе старого.

      А для cda_d_cx всё сделаем в cda_d_cx41.c, который будет доступен через протокол-схему "cx41::", так что можно будет сразу и проверять работу. А когда настанет время полного перехода -- тогда файл переименуем и уберём "41" из имени метрики и схемы.

    • Стоит вопрос выбора термина для имён переменных, ссылающихся на "channel handle". Ранее был вариант "chnh", но он шибко уж непроизносимый, так что...

      ...выбрано "chnd" -- это можно расшифровывать и как "Channel HaNDle", и как "CHaNnel Descriptor".

    • Размышление насчёт форматов: а НУЖНЫ ЛИ нам отдельные CxV4_CH_nnn_Chunk, или можно обойтись имеющимися rs1/rs2/rs3/rs4?
      • Под channel-handle мы repurpose'им поле rs1.

        Переименовать его в chnd не удастся, поскольку оно уже используется в CXT4_LOGIN для передачи PID (а остальные rs2/rs3 -- для PPID и UID; rs4 -- SRV_N в UDP-ответах CXC_SEARCH).

      • Всё остальное вроде влазит в rs2/rs3/rs4.

      По итогам дня: да, всё помещается прямо в 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@утро, мытьё посуды: пара мыслей насчёт работы с новым вариантом протокола -- в отношении динамической смены конфига сервера:

    1. Если "мониторы" постоянно будут жить в сервере (в течение времени жизни соединения) и перерезолвливать имена в gcid'ы при смене конфига, то никакого пакета MUSTER и не нужно -- просто незачем, т.к. вся работа выполняется на стороне сервера. К клиенту нужно будет только присылать уведомление об обновлённом результате резолвинга (это на случай перехода из состояния канала "отсутствует" в "присутствует" и обратно).
    2. Возникло понимание, как именно cda_d_cx должен использовать параметр 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.

    • Глядя на всё имеющееся, возникла мысль -- а не заложить ли возможность поддержки 16-байтных данных (int128 (octa-int) и float128 (quadruple-precision floating-point).

      По данным-то у нас ограничений на unit-size почти нет (там динамический размер пакета), но вот при передаче QUANT и RANGE используются 8-байтные поля -- т.е., максимум int64 и float64. ...и, кстати, CxAnyVal_t -- тоже 8 байт.

    • ...впрочем, у нас {R,D} имеют фиксированный тип double (float64). А для float128 это как-то неправильно.

      Можно в chunk'е CXC_NT_RDS также передавать "тип" -- сейчас фиксированно CXDTYPE_DOUBLE и просто игнорировать, а в будущем это позволит расшириться.

    • 23.05.2020: собственно, а зачем вообще в QUANT и RANGE иметь поля фиксированного размера (хоть 8, хоть 16 байт)?!
      • Ведь в CxV4QuantChunk (и потом в CxV4RangeChunk) так было сделано просто из лени.
      • Надо делать ВАРИАБЕЛЬНО -- просто класть столько байт, сколько определяется dtype'ом; в QUANT -- 1 штуку данных, а в RANGE -- 2.
      • Это будет абсолютно в струю со вчерашней идеей передавать и RDS с типом.
      • Следствие -- отдельные типы не потребуются, хватит просто CxV4Chunk, а вариабельные данные пойдут в его секции data[].

      У-ф-ф-ф, на душе полегчало! А то вчера никак не мог приступить к работе по кодингу -- прям с души воротило, будто держало что-то ("не надо делать то, к чему душа не лежит -- это обычно означает какую-то проблему/косяк, и надо сначала хорошенько обдумать и постараться найти НРАВЯЩЕЕСЯ решение").

    24.05.2020: немножко в сторону: размышления о том, как обозначать такие "большие" типы в devlist'ах и в @-префиксах типов.

    • Потенциально напрашивается 2 таких типа (это СЕЙЧАС): int128 и float128.

      Т.е., нужно вроде как ещё 2 символа.

    • Но с выбором таковых символов проблема. Т.к. возможные претенденты:
      • 'o' - Octa-int (по аналогии с 'q'): ни в коем случае нельзя, из-за сходства заглавного 'O' с нулём '0'.
      • 'e' - Extended precision (для float128): нехорошо, поскольку "extended" ассоциируется с ЧУТЬ расширенными типами, вроде 80-битного.
      • 'w' - Wide: мутно. И первая ассоциация "Word", т.е., int16. Плюс, 'w' используется как префикс-типа каналов записи, и желательно бы поэтому воздержаться от 'w' и 'r' в обозначениях типов данных.
      • 'l' - Long: ну типа как бы можно, но не очень хорошо: формально "long" -- это int32 или int64, а вовсе не int128.

    Но можно поступить хитрее:

    • Вместо введения штучных символов для каждого типа пойти по пути C: сделать ДОПОЛНИТЕЛЬНЫЙ ПРЕФИКС, взяв для него как раз 'l' -- "long".
    • Т.е., "ld" -- long double, float128, "lq" -- long quad, int128.

      Префикс "длинности" служит для удвоения размера (хотя да, "li"="q", а "lt" даст несуществующий TEXT16).

    • И логика будет совпадать с C'шной (хотя синтаксис чуток и отличается -- там ЗАГЛАВНОЕ L, "%Lf").

    Тем более, что префиксы (без)знаковости '+'/'-' у нас уже есть, они и парсятся, и печатаются.

    25.05.2020@пешая пробежка по квартире: пришло понимание, как стоит сделать в cxsd_fe_cx.c с мониторами:

    • Да, надо унифицировать "нынешние" мониторы и новые "для через handle".

      Различать их по флажку -- старый/новый, и уведомления -- из MonEvproc() и IfMonModifiedPutIt() -- для старых слать старыми командами, а для новых -- новыми _NT_nnn.

    • Но в качестве "флажка" сразу использовать ТИП монитора: сделать поле "type", и для "старых" сделать MON_OLD=0, а для новых сразу MON_TYPE_CHAN=1 (и дальше можно плодить MON_TYPE_RANGE_MIN, ...).
    • 26.05.2020: неа, лучше даже так: не вводить лишнее поле "type", а использовать имеющееся in_use, и сделать MON_TYPE_UNUSED=0, MON_TYPE_GCID=1, MON_TYPE_CHAN=2, ...
    • А функции, кладущие соответствующие Chunk'и в буфер отправки, называть 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).

    • Все новые обработчики CXC_NT_* генерят старые CAR_-события и пользуются старыми же info-структурами.

      Т.е., тут осталась совместимость.

      (В качестве hwid передают chnd, присылаемый в rs1.)

    • И только для CH_OPEN введены 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'ов переведён на них.
    • Подготовка инфраструктуры для отправки "уведомлений" (notifications):
      • Введён тип 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'ы в зависимости от типа монитора.
    • Промежуточный необходимый шаг: FIRST_INDEX у мониторова SLOTARRAY'я изменён с 0 на 1.

      Смысл -- поскольку теперь номера/идентификаторы мониторов являются из внешними адресами, то нужно дополнительно защищаться от потенциальных багов, так что элемент [0] пусть будет зарезервированным.

    28.06.2020: далее -- переходим к "мясу", собственно обработке CHN-запросов.

    • Перво-наперво -- самое главное и сложное, OPEN.

      Тут всё хитрО и неоднозначно, поскольку именно здесь происходит смена состояния -- с "нет канала" на "есть канал", и именно с поведением OPEN'а связано потенциальное наличие/отсутствие race condition'а.

      • Первый же взгляд на ТЕКУЩЕЕ содержимое ServeIORequest() показывает, что нынешняя модель с раздельными CXC_RESOLVE и CXC_SETMON -- кривовата: в них в ОБОИХ есть отправка RDs, FrAg, Quant, Range (плюс в первом ещё и Strs (хбз, почему во втором нету)).

        Поэтому программы получают уведомления об этих свойствах ДВАЖДЫ (я разок подгонялся из-за этого, при отладке какого-то функционала cdaclient'ом).

        Т.е., переход на выделенный OPEN -- это однозначно "the right thing", текущая кривизна при этом устранится и всё станет элегантно.

      • С другой же стороны, как раз тут и кроется потенциальный race condition:
        • Чтоб у клиента к моменту перехода канала в состояние готовности на него уже были получены все свойства, СНАЧАЛА отправляются свойства, а уж ПОТОМ -- CXC_CVT_TO_RPY(CXC_RESOLVE), являющийся сигналом к 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 */
          
        • НО! Ведь тем самым как раз и открывается окно для race condition:
          • если клиент закрывает канал,
          • но ещё ДО получения этой команды сервером там происходит изменение свойств и они отправляются в сеть;
          • тем временем клиент пытается открыть новый канал, присваивая ему тот же самый hwr, что был у удалённого,
          • то вуаля -- свойства СТАРОГО канала, полученные ПОСЛЕ отправки запроса на новый, будут поняты как свойства НОВОГО.
          • И то, что совсем вскорости прилетят свойства нового и типа исправят врЕменную ошибку -- ситуацию никак не спасёт, поскольку старый и новый могут быть каналами совсем разных типов, и, например, свойства от строкового канала, переданные как для вещественного, могут свести клиента с ума.
        • Напрашиваются 2 варианта решения:
          1. Использовать монотонно растущий счётчик, и если значение, пришедшее в свойствах, не совпадает со "значением, полученным этим hwr'ом при рождении", то считать полученные данные иррелевантными и просто отбрасывать.

            ...подобный механизм обсуждался при обдумывании всей ситуации, которое и привело к осознанию необходимости перехода на модель "сначала ОТКРЫВАЕМ канал" -- 30-04-2020: «вместе с основным "ключом" param1=hwr ещё дополнительный проверочный, являющийся постепенно увеличивающимся числом»; а ещё раньше -- как возможное решение для старого протокола, 24-04-2020: «вместе с запросами всегда присылать "код-номер инкарнации серверовой БД", и чтоб сервер игнорировал запросы, содержащие "не тот" код». И в обоих случаях было осознано, что это снижение вероятности, но НЕ гарантия.

          2. Разделить "ответ на OPEN" на 2 стадии: на первой подтверждается существование такого канала, так что клиент спокойно ПРИМЕР присланные свойства, а вторая является сигналом, что все данные уже присланы и можно делать cda_dat_p_set_ready(,1).

            Естественно, и в cda_d_cx.c состояние "разрезолвлено" тоже придётся разбить на 2 -- "канал есть, получаем на него свойства" и "канал готов к работе".

          Первый вариант -- лишь смягчение проблемы (mitigation), но не гарантия.

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

    • Однако остаётся вопрос о том, КАК передавать эти "стадии" -- физически, в каком поле: проблема в том, что ответ на CH_OPEN приходит в просто CxV4Chunk и все поля в нём уже заняты.

      Связанный вопрос -- как передавать НЕГАТИВНЫЙ ответ ("нет такого канала"): раньше-то определялось по размеру chunk'а, который при обломе приходил базовый вместо CxV4CpointPropsChunk, а теперь это невозможно.

      Варианты:

      1. Завести НЕСКОЛЬКО разных кодов ответов: NOTFOUND, FOUND_STAGE1, FOUND_STAGE2.
      2. Подселить код к полю rs2 -- в нём передаётся rw, на который и 1 бита (ОК, байта) достаточно.
      3. Сигнализировать значением rs1, которое chnd: если -1, то облом.

        ...но такой вариант не даёт возможности передавать "FOUND_STAGE1": ведь при нём обязательно нужно СРАЗУ отдавать значение chnd.

      4. Как вариант, об обломах можно сигнализировать нулевым значением nelems.

        Но это хак: формально нет никаких противопоказаний нулевому значению 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, ...
    • ...куда cxlib в зависимости от полученного кода записывает -1, 0 или +1.
    • Ну а дальше возвращаемся к отработке CH_OPEN.

      Начинаем наполнять; из "простого" -- сделана отправка FOUND_STAGE1 и FOUND_STAGE2.

    07.07.2020@вечер, засыпая: а зачем так громоздко -- можно же проще!

    • Сделать табличку-список chunk_maker'ов, которые нужно вызвать друг за дружкой. Прямо указателями на функции -- благо, и тип put_nt_chunk_t у нас уже есть.
    • Только придётся сделать и функцию-maker для CXC_NT_OPEN_FOUND_STAGE*.
    • ...отдельный вопрос, как передавать ей код (STAGE1/STAGE2)? Явно в 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.

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

    • Вторая тонкость: AVALUE стоит в самом конце, уже ПОСЛЕ STAGE2.

      Смысл в том, чтобы добиться того же поведения, что ранее: сначала сообщаются все свойства канала и он в клиенте переводится в состояние "готов к работе", а уж затем даётся его текущее значение.

    • Самое нудное -- создание нового монитора GetChnd(). В основном скопировано из GetMonitor().

    09.07.1010@14:30, чистка картошки: при обдумывании реализации CH_CLOSE стало ясно, что тут есть ЕЩЁ ОДИН race condition (правда, весьма явный и способный проявиться лишь при очень экзотических и малореалистичных условиях).

    Сценарий такой:

    • Клиентская библиотека (cda_d_cx) отправляет запрос CH_OPEN.
    • ...тот бежит по сети, но сервером ещё не обработан (или, по крайней мере, ответ ещё не успел придти)...
    • Клиент -- собственно программа -- передумывает и делает cda_del_chan() этому каналу (который хоть ещё не достиг готовности, но для клиента-то ref уже есть).

    В результате:

    1. ЧТО должна указывать cda_d_cx в cx_ch_close()? Ведь chnd-то ещё нету.

      Очевидно, надо передавать ТРОИЦУ -- (chnd,param1,param2), чтобы cxsd_fe_cx мог найти "жертву" по (param1,param2), если chnd<0.

      Следствия: а) менять список параметров cx_ch_close(); б) Put_NT_OPEN_nnn_Chunk() придётся где-то добывать имя канала из запроса, чтобы слать его в ответном chunk'е (в будущем-то оно предполагается к хранению в мониторе, а сейчас -- нету!).

    2. А ещё если клиент сразу после гроханья "этого" канала захочет создать другой.

      И вот что выйдет:

      • Клиент отправил запрос CH_OPEN.
      • ...он ещё НЕ дошёл до сервера (или, по крайней мере, ответ ещё не успел придти)...
      • Клиент отправляет CH_CLOSE.
      • ...тот ещё тоже не успевает (или, ...)...
      • Клиент отправляет ВТОРОЙ запрос CH_OPEN, причём с ТЕМИ ЖЕ (param1,param2).
      • И тут прилетает ответ на первый CH_OPEN!
      • И-и-и -- клиент (в лице cda_d_cx) будет считать это ответом на ВТОРОЙ запрос CH_OPEN!

      Тут тоже просматривается очевидное решение: аналогично 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: пора заниматься клиентской стороной.

    • Сделан cda_d_cx41.c -- простым копированием и заменой имени модуля на "cx41" (внутренние же имена функций оставлены нетронутыми).

      Он включен в сборку и зарегистрирован в 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 по вчерашнему "проекту":

    1. В условие к REQ_ALL_MONITORS добавлено "&&cond!=CX_MON_COND_NEVER".
    2. Добавлено поле moninfo_t.mode -- функционирующее аналогично cda_d_cx'ному hwrinfo_t.mode; в нём пока есть 2 битика:
      • MONMODE_RQ_LOCK - им было заменено поле rq_lock (ныне убранное).
      • MONMODE_SEND_UPDATE - взводится у COND_NEVER-мониторов по явному CXC_CH_RQRD, а в CHNEvproc() смотрится, что если этот бит взведён, от обновление клиенту отсылается, после чего бит сбрасывается.
    3. Ну и в GetChnd() добавлен код CX_MON_COND_NEVER в список годных.

      Более вроде ничего и не нужно -- всё в основном было сделано в 2 первых пунктах.

    4. 07.09.2020: ага, "ничего"! Забыта была фильтрация MON_COND_NEVER-мониторов в 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-состояние -- RSLV_STATE_FOUND1.
    • В ProcessCxlibEvent() в обработку всех per-channel-событий (кроме OPEN (пока всё равно отсутствующего)) добавлено условие "&&hi->rslv_state >= RSLV_STATE_FOUND1".

    16.07.2020: далее...

    • Самое "масштабное" -- обработка CAR_CH_OPEN_RESULT.
    • ...и вот тут пришло осознание, что теперь наверх в качестве "hwid" будет отдаваться chnd, который имеет смысл исключительно для данного соединения, а в качестве диагностической/отладочной информации смысла лишен полностью.

      Надо бы как-то получать от сервера истинный hwid, но на него места в CxV4Chunk не осталось...

    Проверяем.

    • Сначала SIGSEGV. Оказалось, что в обработке CAR_CH_OPEN_RESULT забыл сделать hi=AccessHwrSlot(...). Позорище :)
    • Затем - какая-то фигня вместо значений.

      Тут было сложнее и дурнее: оказалось, что в ServeIORequest() в обработке CXC_CH_OPEN вместо надлежащих кодов CXC_NT_NEWVAL/CXC_NT_CURVAL слались старые CXC_NEWVAL/CXC_CURVAL -- видимо, просто результат копирования.

    • Зато после исправления -- на вид даже скрин linmag на симулируемых данных выглядит одинаково через протоколы "cx::" и "cx41::".

    Дальше надо проверять на живом железе -- в т.ч. на осциллограммах.

    17.07.2020: ну и последний пока штрих -- избавление от старого наследия:

    • Убрана обработка RSLV_RESULT.
    • Плюс do_subscribe().
    • Поле hwrinfo_t.hwid переименовано в chnd.

    (После обеда) а, нет -- пока НЕ последний: ещё ж осталась проблема с типом мониторирования.

    • Была даже крамольная мысль сделать константы CX_MON_COND_* публичными (а не только в протоколе). Но нет -- не стоит.
    • В cxlib.h заведена троица констант CX_UPD_COND_*, полностью миррорящих протокольные (и даже с совпадающими значениями, хотя это и неважно).
    • У cx_ch_open() параметр on_update заменён на upd_cond, могущий принимать одно из тех значений, ...
    • ...и производится "трансляция" UPD_COND'ов в MON_COND'ы (причём по умолчанию -- если значение не из этих 3 -- считается NEVER).
    • А в cda_d_cx41.c заведёна таблица трансляции 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.

    • В самом chunk'e -- ну никак: нету там места. Были мысли "а вот бы упаковать как-нибудь хитро" -- ведь, например, для rw реально достаточно 1 бита; но не стоит.
    • @сидя на пороге балкона, ~22:30: а какого чёрта?! Зачем так стараться упихаться в стандартные поля?! Раз есть потребность -- ну так и сделать для ответов на CH_OPEN отдельный тип chunk'а, со всеми нужными полями, включая gcid; можно даже несколько штук резервом на будущее оставить.
    • @засыпая: ещё вариант: а если РАЗДЕЛИТЬ ответ на CH_OPEN и отдачу свойств?

      Чтобы все свойства могли прилетать отдельно -- тогда в будущем, при случае обновления конфига сервера "на лету" они могли бы присылаться как все прочие (строки, диапазоны, ...).

      Надо бы глянуть, как сейчас сделана передача этих 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_d_cx*'ом теперь уже ЕГО значение сбагривается в cda_dat_p_set_hwinfo().
    • Плюс, в cda_d_cx41.c все БЫЛЫЕ упоминания "hwid" заменены на "chnd".
    • И, КСТАТИ: во всех затронутых местах "nelems" переименовано в "max_nelems".

    Проверено -- да, теперь клиенту отдаётся правильный внутрисерверный номер канала, годный для сбагривания в cdaclient.

    21.07.2020: как бы не сюда, но просто рядом выше уже было аналогичное: повсеместное переименование "nelems" в "max_nelems" там, где оно используется именно как указание максимального, а не текущего количества.

    • Всё касательно hwinfo: поле в 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.
    • Ну и во всех cda_d_*.c* в реализации этих методов соответствующее переименование произведено.

    24.07.2020:

    30.07.2020: переходим на новую версию -- чтоб она была уже по умолчанию:

    • Сначала проверяем на пульту: cxsd заменён на новый и проверено, что к нему коннектятся и показывают одинаковое и старые, и новые клиенты.
    • Этот вариант -- последний со СТАРЫМ протоколом по умолчанию -- заархивирован в w20200730-1-cx41-cxsd.tar.gz.
    • Производим "перекидку протоколов":
      1. Старые cda_d_cx.[ch] скопированы в cda_d_cx4old.[ch], и схема "cx4old" зарегистрирована в cda_plugmgr'е.
      2. Новый cda_d_cx41.c скопирован в cda_d_cx.ccda_d_cx.h оставлен старый -- за 15-02-2010 :D).
      3. Всё касательно "cx41" разрегистрировано из cda_plugmgr'а и списка файлов, после чего cda_d_cx41.[ch] удалены.
    • Этот новый вариант будет опубликован как w20200730-2-cx41-cda.tar.gz.

    31.07.2020: "продолжение банкета".

    Заменил клиентские бинарники на пульту.

    И тут началось: куча серверов попадала по SIGSEGV.

    • Причина оказалась банальна: в Put_NT_OPEN_nnn_Chunk() некорректно обрабатывался вариант NOTFOUND, когда mp==NULL: оно всё равно пыталось заполнять ответ полями из mp->.
    • Окей, поправлено -- там было несложно.
    • После этого и замены /export/ctlhomes/oper/4pult/sbin/cxsd сервера нормально взлетели.
    • Правда, ЧАСТЬ серверов, возможно, упала не из-за этого бага, а из-за странного косяка в NFS: серверные виртуалки -- cxhw и canhw -- почему-то не сразу обнаруживают замену файла: даже если файл на ctlhomes просто стереть, то на клиентах он как бы есть -- его даже ls показывает (при указании точного имени! а в листинге директории -- нет).

      Возможно, оное изменение было замечено сильно невовремя, вот и вышло, будто бинарники неожиданно изменились прямо у запущенных cxsd, а такое неизбежно приводит к SIGSEGV.

    • Ну и отдельный вопрос: а с чего вдруг NOTFOUND? Наблюдено было на cxhw:15 -- спрашивался канал "_devstate", безо всякого префикса.

      Никаких прямых таких ссылок вроде нигде нет -- ни в *.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 и проблема ушла.

    Чуток истории -- как всё проявилось и расследовалось:

    • Внезапно обнаружилось, что в вакууме на пульте не работает запись: при попытке нажать чекбокс Вкл/Выкл у 124-х строк он просто синеет, но ничего не делается.
    • Долго думал, где же может быть проблема -- и sktcanmon с grep'ом запустил, и увидел, что при нажатии на этот чекбокс НИКАКИХ соответствующих пакетов (0xF9+0xF8) не шлётся.
    • ...а при нажатии из другого экземпляра программы linvac, запущенного с МРН'овской Raspberry Pi через devpult -- шлются.

      А та программа, как показал ps, запущена ещё в январе. Т.е., работает по СТАРОМУ протоколу.

    • Тут-то и стукнула меня мысль, что, вероятно, проблема может быть в реализации нового протокола.
    • Ну а потом уже было дело техники: запущенный локально в режиме симуляции (даже СУПЕРсимуляции) cxsd, на нём проблема подтвердилась (т.е., железо совсем ни при чём), последовательное начинение отладочной печатью всего пути данных от cda до сервера.

    Вот так крохотный косячок -- забытая смена типа монитора -- привел к крупной неработающести и несколькочасовым разборкам.

    28.08.2020: и ещё косяк обнаружился -- с локингом.

    • По уверениям Феди, локинг работает только в случае вызове его при УСТАНОВЛЕННОМ соединении. А если вызвать сразу при создании канала, ДО установления соединения -- то фиг.
    • Зыринг на код в cda_d_cx.c и сравнение его с cda_d_cx4old.c показало, что да -- единственная точка, где вызывается cx_ch_rq_l_o() -- это cda_d_cx_lock_op(),
    • Очевидно, при переезде соответствующий кусочек потерялся -- всё потому, что диаграмма работы иная: РАНЬШЕ локинг делался сразу в момент установления соединения; сейчас же диаграмма работы с каналами иная, и нужно лочить в иной момент.

    31.08.2020: делаем.

    • Несмотря на кажущуюся простоту -- ну сделать где-то cx_begin(),cx_ch_rq_l_o(),cx_run() -- задача требовала изрядного обмозговывания.
      1. КУДА (точнее, "в КОГДА") ставить локинг? У нас ведь ДВЕ стадии результата резолвинга -- FOUND1 и FOUND2, отражающиеся в RSLV_STATE_FOUND1 и RSLV_STATE_DONE; и в какой из этих 2 моментов ставить локинг?
      2. Сама цепочка 3 вызовов должна делаться "аккуратно", с проверками на "<0", чтобы если облом при отправке, то НЕ делать последующих dat_p-вызовов (которые будут уже совсем не к месту).
    • Но после внимательного изучения исходников всё оказалось довольно просто и элегантно, т.к.:
      1. Канал считается по факту "юзабельным" уже в состоянии FOUND1.

        Значит, при переходе в FOUND1 и надо отправлять запрос на лочку.

        (...более того, сам метод lock_op() вызывает отправку запроса именно при rslv_state>=RSLV_STATE_FOUND1 -- так что как раз всё срастёся ровно как надо: если программа-клиент запросит локинг уже ПОСЛЕ установления соединения, но ДО перехода канала в RSLV_STATE_DONE, то запрос лочку будет отправлено РОВНО ОДИН раз, в зависимости от конкретного момента: если до прихода FOUND1, то именно по его приходу, а если после, то прямо в вызове.)

      2. А код в этом месте -- где делается rslv_state=RSLV_STATE_FOUND1 -- таков, что отправка запроса на лочку будет последним действием, так что проверок не требуется.
    • Вот так и сделано.

    Проверил (cdaclient'ом, с префиксом '@!') -- вроде работает, канал залочивается.

    04.09.2020: обнаружился ляп, введённый ещё 4 года назад, 27-05-2016, ещё для тогдашнего протокола, но и в новую реализацию также перекочевавший.

    Вкратце:

    • Баг проявляется в очень экзотических условиях:
      1. Клиентом канал запрашивается в режиме ON_UPDATE (например, pzframe -- осциллограммы или их параметры), и при этом...
      2. ...канал "легкообновляемый" -- результат возвращается сразу же прямо во время CxsdHwDoIO(); например, в режиме симуляции.
    • Причина -- заказ чтения непосредственно в момент регистрации монитора (т.е., в GetChnd() или в SetMonitor()) при режиме CX_MON_COND_ON_UPDATE, что для "легкообновляемых" каналов приводит к немедленной отправке текущего значения, тем самым портя формируемый в это время пакет в sendbuf'е.

    Хронология разбирательства:

    • Вылезла проблема при попытке разобраться с глюком в v5kls, где введённое юзером значение почему-то НЕ сбрасывалось к ограничению от драйвера.

      Была организована симуляция: файл devlist-cxhw-25.lst подправлен так, что vdev-устройства v5k5045 оставлены "живыми", а подчинённым РЕАЛЬНЫМ устройствам добавлены префиксы "-", чтобы они симулировались. Смысл -- чтобы ограничения от v5k5045_drv работали, а реальной аппаратуры всё равно нет.

    • И в какой-то момент "вылезло странное" -- в скрине постоянно отображались "nan" вместо значений.
    • Причина была совершенно неочевидной -- первое время было впечатление, что тут присутствует какой-то race condition, поскольку проблема проявлялась не всегда, и иногда "всё работало".

      Так что пытался "играться" приостановкой клиента Ctrl+S'ом, чтобы время запуска было более 60 секунд (таймаут в сервере между accept()'ом и моментом, когда клиент должен успеть прислать свои реквизиты), а также переживал, что проблема может зависеть от загруженности сети.

      Потом, после определения проблемы, стало ясно, что дело, вероятно, было в деталях, на которые я не обращал внимания: наличие режима симуляции у конкретно ADC200 (записываю задним числом, уже после исправления, так что достоверно просто не помню). Видимо, в разные прогоны симуляция была то включена, то выключена -- наличие/отсутствие ключа "-s" в командной строке и/или префикса "-" у adc200.

    • Поскольку "по-хорошему" -- анализом кода -- не получалось, то пришлось "по-плохому" -- было напихано очень много отладочной печати.
    • В конце концов обнаружилось, что 0-й chunk клиенту приходит не тот, что должен быть от сервера (размером 96 байт), а обкорнанный (80 байт), и другого типа -- "NEWVAL" вместо 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'е выполняется отправка.

    Как исправлять?

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

        • А в конце -- нуление Type=0.
        • И возврат 0.
    • Соответственно, юзеры подправлены --
      1. На проверку результата InitReplyPacket() (КРОМЕ ServeIORequest() -- предполагается, что уж он всегда вызывается "из нулевого уровня", и в буфере ничего не может быть).
      2. И использование SendReplyPacket() вместо fdio_send()+проверки.
    • A BIG FAT NOTE: такое игнорирование необходимости отправки пакетов -- не есть хорошо, но предполагается, что всё это сделано исключительно ради разрешения ситуации, когда отправка вызывается "невовремя" сработавшим evproc'ем во время обработки регистрации нового канала, вызванной при обработке цепочки chunk'ов.

      Потому такое "выкидывание отправки" рассматривается как допустимое, поскольку текущие значения всё равно будут немедленно отправлены несколькими chunk'ами позже, так что никаких реальных потерь не произойдёт.

    • ТАКЖЕ везде добавлено, что при обломе GrowReplyPacket() либо возвращается 0 (в функциях-формирователях chunk'ов), либо пропускается действие (в непосредственных юзерах).

      ...но это мы фиг проверим, конечно.

    Проверяем: да, всё окей. Теперь никаких NAN'ов. Отключение же проверки Type!=0 возвращает старую ситуацию.

    P.S. Да, это был реально хитрозамудрённый баг. Не то, что предыдущий "забыли сделать in_use=MON_TYPE_CHN".

    07.09.2020: теперь разбираемся с тем, какие уведомления об изменении каналов отправляются клиентам -- старые или новые.

    Расследование -- в виде отладочной печати данных каждого получаемого cxlib'ом пакета, включая число chunk'ов и тип первого из них -- показало, что косяк явно присутствует, причём аж целых ДВА:

    1. Группа уведомлений в конце цикла действительно прилетает "старого стиля" -- код 'Cvl' (Channel,VaLue) вместо надлежащего 'Nvl' (Notification,VaLue).
    2. Но также прилетает и большая пачка (для v5kls -- 112 штук) уведомлений нового стиля, причём по 1 chunk'у в пакете.

    Промежуточные выводы:

    1. В IfMonModifiedPutIt() попросту отсутствует проверка на тип монитора (OLD или CHN), а ВСЕГДА шлются уведомления "старого стиля".

      Это надо исправлять.

    2. Вероятно, поштучно-присылаемые обновления -- это от adc200: их в скрине как раз 4 штуки, 112/4=28, а в adc200_data.c в списке есть 40+ каналов, из которых 27 штук -- чтения (да, хбз, откуда недостача 1, но, возможно, я просто ошибся в подсчёте).

      Эти-то каналы запрашиваются в режиме 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 попросту игнорировался.

    После исправления всё стало офигенно.

  • 13.09.2023: ЕманоФедя выкатил претензию -- мыло (Message-ID: b18ead100f2f4568a3f5bbda73b65748@binp.local) "поведение CX при плохой сети" -- что при подключении по WiFi в конференц-зале (во время RuPAC-2023) при ping'ах до cxhw (через VPN) порядка 300ms и выше начинаются дикие тормоза:
    1. некоторые программы (с малым числом каналов, ringcurmon) работают приемлемо,
    2. другие (со средним, kls) "периодически. т.е. измеряемые поля половину времени синие, половину белые. и так же иногда дает поуправлять.",
    3. а третьи (сильно многоканальные, ringmag) "как очнётся после запуска может обновить значения и дать поуправлять, потом впадает в синее состояние полностью и ни когда из него не выходит.".

    И далее

    "бывают проги, которым нужны именно все события из системы управления. Но для проги, которая должна показывать текущие данные и принимать команды такое поведение надо считать не желательным, т.к. данные которые были 40с назад или вчера - уже ни кому не нужны. "

    14.09.2023: обдумывал-обдумывал -- мрак мрачный. Возможно, ради чего-то подобного и существуют в EPICS'е разные "приоритеты", но вообще это ужас ужасный -- надёжно и корректно реализовать такую адаптировабельность практически невозможно.

    В частности,

    1. Как прикажете отличать те, которым нужны ВСЕ, от тех, которым достаточно ТЕКУЩИХ?
    2. И сама "синхронизация" -- это что же, на КАЖДЫЙ канал

    15.09.2023@~13:00, дорога по Николаева в Гуси, проходя мимо ГИПроНИИ (Николаева-8): а вообще есть идейка (зародилась ещё вчера, а сегодня созрела).

    Главная фишка -- оперировать не каналами, а ЦИКЛАМИ. Т.е., подтверждать получение не данных индивидуальных каналов, а событий "конец цикла".

    Детали:

    • Можно ввести специальный "синхронный" режим, при котором...
    • ...cxsd_fe_cx будет присылать очередные данные (вместе с CXT4_END_OF_CYCLE) только при условии, что предыдущий сигнал цикла клиент успел "подтвердить".
    • И да -- такое касается ТОЛЬКО CX_MON_COND_ON_CYCLE-мониторов, для прочих оно незачем.

      Это покрывает GUI-клиенты, в которых и проблема.

    • Техническая реализация несложна:
      1. С точки зрения клиента и протокола:
        • О необходимости такого режима клиент уведомляет после соединения (или ПРИ соединении?) специальным CXT4-кодом, ещё ДО заказа каналов.

          Для отправки такого пакета добавляется cxlib-вызов.

        • "Подтверждение" приёма очередного цикла -- ещё один специальный CXT4-код.

          Для отправки которого также добавляется cxlib-вызов.

      2. Со стороны сервера:
        • Для соединений, помеченных как "синхронные", cxsd_fe_cx просто игнорирует ничего не делает в SendEndC(), если флаг "предыдущий цикл подтверждён" не взведён.
        • Таким образом, флаги изменений -- mp->modified и cp->per_cycle_monitors_some_modified -- будут "накапливаться" в течение нескольких циклов, а по получении "подтверждения" на очередном цикле данные будут отправляться и флаги сбрасываться.
        • При получении пакета "хочу синхронности!" флаг подтверждённости будет взводиться, чтобы первое обновление пришло.
    • Включение синхронного режима будет делаться переменной окружения $CDA_D_CX_SYNC_CYCLES со значением '1' или 'y'; в самом её имени видно, что оно касается только ON_CYCLE.

    16.09.2023: реализовываем.

    1. cx_proto_v4.h:
      • добавлены
        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 наоборот "можешь слать дальше".

      Следствия:

      1. Режим "синхронности" можно менять по ходу дела.
      2. Тормозить/возобновлять отправку можно даже у обычных, не-синхронных соединений.
    2. cxsd_fe_cx.c: тут решено чуток изменить технологию по сравнению со вчерашней идеей.
      • В 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 и в обычном асинхронном режиме работы и сразу после включения асинхронности (итог -- меньше действий), а во-вторых -- это позволяет "замораживать" и обычные асинхронные соединения.

    3. cxlib_client.c:
      • Тут всё тривиально -- введена парочка
        int  cx_send_cycle_mode(int cd, int mode);
        int  cx_send_ack_cycle (int cd, int code);
        
        с очевидным содержимым для отправки соответствующих пакетов.
      • Дополнительно -- в ProcessFdioEvent() код CXT4_EBADRQC убран из списка фатальных и вытащен в отдельный case, где теперь на него просто печатается диагностическое сообщение cxlib_report()'ом.

        Смысл -- чтобы при натравливании на "старые" сервера "новых" клиентов они оставались бы работоспособны.

        Это может и при будущих модификациях пригодиться.

    4. cxsd_fe_cx.c:
      • Добавлено поле cycle_mode_is_sync в cda_d_cx_privrec_t.
      • Его инициализация выполняется в cda_d_cx_new_srv() по значению $CDA_D_CX_SYNC_CYCLES, ...
      • ...а в ProcessCxlibEvent() при взведённости:
        1. По CAR_CONNECT делается cx_send_cycle_mode(,1) ПОСЛЕ "основной" реакции SuccessProc() (ну мало ли что там обломится).
        2. По CAR_CYCLE делается cx_send_ack_cycle(,1) ПОСЛЕ основной реакции (т.к. при запуске через X11-тоннель именн обновление GUI будет тормозить).
      • Никакого "управления режимом на лету" пока нет; если захочется -- то это явно работа для srv_ioctl()'а.

    Проверяем на симуляции -- сервер canhw:11, запущенный с ключом "-b1000" (1kHz), на который натравливается скрин linmag -- вроде работает как надо. Понято по тому, что с включенной синхронизацией отправки циклов скрин просто работает, а с отключенной соединение постоянно рвётся (т.к. сервер на такой частоте успевает забить буфер отправки за то время после коннекта, пока GUI отрисовывается).

libcxsd:
  • 26.07.2008: поскольку, при реализации сервера в соответствии с превалирующей на данный момент схемой, задача создания "CX-server-library" решается автоматически -- эта "библиотека" состоит из модулей cxsd_data.c+cxsd_fe_cx.c, то раздел замораживаем.

    Подробнее см. рассуждения на эту тему за сегодня в bigfile-0001.html.

  • 11.07.2009: а вот и нифига -- как раз наоборот, те модули и будут "жить" в этой библиотеке, а сервер cxsd -- станет лишь частным случаем её применения.

    Состоять же библиотека будет из большего числа модулей. Директория -- lib/srv/, библиотека -- libcxsd.a.

    Для удобства поиска переименовываем раздел из "CX-server-library" в "libcxsd".

  • 14.11.2012: принцип для всех серверовых внутренностей (касается, видимо, cxsd_hw): "несуществующие" каналы (n<0||n>=cxsd_hw_numchans) надо маппировать на 0-й канал -- который принадлежит фиктивному 0-му устройству "!DEV-0!", и должен иметь перманентно "плохие" флаги и бесконечный возраст (кстати, надо вставить в CxsdHwSetDb() их заполнение).
  • 08.09.2014: немного о блокировках: надо сделать аллокирование client-ID функционалом самого сервера (libcxsd), а не конкретных frontend'ов (как сейчас).

    Смысл -- чтобы работала схема блокировки каналов клиентами (где ключом является client-ID, см. за 06-07-2007), независимо от того, через что клиент приконнектился.

    И даже драйверам разрешить испрашивать свои client-ID -- для работы напрямую через CxsdHw*().

    05.10.2014@Снежинск-гуляние-под-моросящим-дождиком-вдоль-озера: (реально мысли возникли несколько дней раньше) чуть хитрее:

    • Аллокировать должно числа так, чтобы
      1. Всегда больше примерно 65535 -- чтоб не было перекрытий с devid.
      2. Всегда положительные -- чтоб не было перекрытий с lyrid.
    • Драйверы тогда смогут указывать прямо свои devid.
    • Собственно аллокирование ID можно делать так:
      1. Младшие 16 бит -- номер ячейки в SLOTARRAY'е; при деаллокации она освобождается и может использоваться повторно.
      2. Биты 16-30 -- последовательно циклически увеличивающееся число, при достижении 32768 (1 и 15 нулей) превращающееся в 1 (для всегда >65535).
      3. Бит 31 всегда 0 (для положительности).

    22.10.2014: да, сделано по тому проекту.

    • Поселено в cxsd_hw (поскольку используются эти ID в нём, и никакого более подходящего модуля ("cxsd_core") нет).
    • Функции CxsdHwCreateClientID() и CxsdHwDeleteClientID().
    • При генерации никакие данные от "юзера" (как раньше fd клиента) не используются.
    • Из cxsd_fe_cx кусок генерации ID (и махинаций с RCC) удалён.
  • 28.07.2015@пляж-после-обеда: с учётом сегодняшних кувырканий по правильной реализации возврата "свойств" всегда-цепочки в cxsd_fe_cx.c: напрашивается идея навести порядок с именами переменных, ссылающихся на каналы и точки контроля -- чтоб везде было унифицировано, а не несколько разных вариантов.
    • cxsd_chanid_t -- всегда gcid (GlobalChan ID) вместо gcn, globalchan, hwid, ...
    • cxsd_cpntid_t -- всегда cpid (Control Point ID) вместо cpn.
    • hwid оставить только на стороне клиента (в API cxlib). Возможно, "ПОКА оставить".

    29.07.2015: процесс:

    1. Переименование произведено.
    2. Для вящей определённости тип cxsd_chanid_t переименован в cxsd_gchnid_t.
    3. Затем определения типов cxsd_gchnid_t и cxsd_cpntid_t переехали из cxsd_hwP.h в cxsd_dbP.h...
    4. ...чтоб и cxsd_db.c мог ими пользоваться вместо int.

      Но это пока не сделано, пока оставлены int'ы -- потому, что там шибко уж поперемешаны номера cpoint'ов и каналов.

  • 15.09.2015: учитывая наличие cpoint'ов, теперь становится затруднительно находить РЕАЛЬНОЕ исполнительное УСТРОЙСТВО.КАНАЛ конкретной ручки просто по строке KnobProperties.Source.

    Вот если б мочь как-то добывать и показывать также конечную точку, на которую этот cpoint смотрит...

    16.09.2015@утро-дома: ...а для полноты отлаживаемости -- также мочь бы добывать и показывать также прочие свойства, вроде cpid+hwid, hwr, fresh_age.

    16.09.2015@утро-по-пути-на-работу-мимо-НИПСа: сделать оное, наверное, можно -- понадобится содействие от всех уровней:

    • Chl_knobprops -- просто завести доп.строки (хотя они будут несколько забардачивать и так немаленькое окошко).
    • cda -- предоставлять API для добычи. Вопрос, какой...
    • cda_d_cx и прочие cda_d_NNN -- собственно давать. С cpid и hwid всё просто -- они есть. В EPICS'е в т.ч. есть и аналоги hwid и hwr -- SID и CID.
    • В CX-протокол добавить пакеты запроса и ответа с этими вещами.
      • Дополнительно к уже сделанным нужен только cpoint-target.
      • И собственно cxsd_fe_cx может свойства отправлять сам, при ответе на CXC_RESOLVE.
    • libcxsd же должна эти свойства предоставлять. Видимо, прямо в ответе на CxsdHwResolveChan().
      • Вопрос, в каком виде. Базовое устройство -- именем, конечно. А target-канал -- именем или номером?

        В идеале, видимо, оба.

      • Отдельный вопрос -- как отдавать строки. А вот он решается просто: на уровнях cxsd_db+cxsd_hw передавать ofs'ы в strbuf'е, а наверх отдавать уже char*.
cxsd_driver:
  • 11.07.2009: модуль, реализующий API для драйверов.
  • 13.07.2009: в API добавлена принципиально новая функция -- SetChanProps(), дающая драйверу возможность указывать клиентско-ориентированные свойства каналов: границы разрешенных значений, кванты, возрасты свежести.

    13.07.2009: первоначально идея была придумана еще 07-01-2007 в bigfile-0001.html, а сегодня на сборище по СУ ТНК оформилась окончательно. Смысл -- во-первых, чтобы НЕ хранить эту информацию в БД, а во-вторых -- чтобы при изменении (например, при переходе на другое время интегрирования в АЦП) драйвер мог бы сообщить актуальные параметры.

    P.S. Пока -- только декларация+скелет, без реализации.

    29.10.2013: замечание общего характера, касается как данного пункта (точно?), так и параметра "boss": такое указание не количественных (коэффициенты), а поведенческих свойств (boss, имена) драйвером (а не в конфигурации) at-run-time имеет потенциальную проблему с удалёнными драйверами: что информация поступит от драйверов уже ПОСЛЕ коннекта клиентов и принятия от них запросов.

    Отсюда выводы:

    1. Совсем безопасно пользоваться этой информацией от драйверов только с ЛОКАЛЬНЫМИ драйверами (касается в основном всяких локально-стендовостей и подселения libsrv к GUI).

      В остальных же случаях devlist'ы должны содержать ПОЛНУЮ информацию.

    2. Удалённые драйверы должны изначально считаться NOTREADY, и даже после отсылки пакета CONFIG, а переходить в OPERATING только по ЯВНОМУ получению такового пакета от драйвлета, который перед этим успеет отправить вышеуказанную информацию.

      (Тут "защита" будет за счёт того, что не-OPERATING драйверам никакие запросы НЕ передаются. Но это всё равно полумера -- запросы на имена-то успеют придти и разрезолвиться.)

      30.10.2013: но HeartBeat() должен включаться сразу же после завершения connect()'а.

    26.06.2015: сама SetChanProps() удалена. Поскольку часть функционала не нужна вовсе (указание dtypes,nelems), часть выполняется другими вызовами (fresh_ages), а часть пока не сделана вообще нигде и никак (quants) либо неясно, будет ли когда-нибудь (min_alwds,max_alwds), но когда/если понадобится, то будет сделано отдельными вызовами.

  • 19.07.2009: припоминаем желание за 11-06-2005 в bigfile-0001.html, о возможности иметь "динамические методы драйверов".

    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, и параметр тоже.
    • В v2 тоже добавлено, просто поле void*.
  • 14.09.2009: еще дополнение -- кроме списка ГРУПП каналов, теперь драйвер может в метрике указывать и своё пространство имён (список пар {ИМЯ,НОМЕР}).

    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, то это одиночное указание.
    • Имена для множественных указаний могут содержать расширяемые шаблоны (по 1 штучке в каждом имени):
      • '*' -- номер, начиная с 0. ЧИСЛО, а не цифра; может быть и выше 9.
      • '+' -- номер, начиная с 1 (мнемоника -- как в regexp'ах).
      • '?' -- буква A-Z, 0-му соответствует A.
      ...да, "официальные" имена каналов ADC812ME так не представишь, придётся перечислять.
    • Так же может быть указано имя базового канала (для настроечных) и основного (для отражений) -- см. за 18-09-2014. Эти имена образуются по тем же правилам с шаблонами.

    После обеда: если хорошенько подумать, и вспомнить слова за 17-02-2009 -- нечего в пространстве имён делать описанию отношений базовый/настроечный и основной/подчинённый.

    А групповые списки имён каналов -- пожалуй, имеют смысл. Хотя всё равно вопрос, КАКОЙ: куда и когда эти имена должны помещаться? Только если генерить на их основе .devtype-файлы (для чего можно изготовить утилитку)...

    14.03.2015: в связи с тем, что авторитетным источником данных об именах и проч. является всё-таки конфигурация, то надобность во всех этих "chan_namespace" становится несколько сомнительной.

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

    Но это скорее уже из разряда избыточности (и вопрос, нужной ли).

  • 17.02.2010: потребуется специальный "сервисный" вызов -- "развыпарсить таблицу chan_info в индексируемый-номером-канала массив cxdtype'ов". Это чтоб _rw-методы драйверов проверяли, того ли типа данные им присланы. В первую очередь актуально для удалённых драйверов -- внутрисерверные-то могут надеяться, что сервер сам проверит/сконвертирует.

    14.03.2015: конкретно сейчас, по опыту cac208_drv и adc200me_drv -- потребности такой нет, т.к.

    1. Там вся проверка сводится к "INT32/UINT32 ли";
    2. и конкретно для осциллографов связана с еще одной таблицей, индексируемой номером канала.
  • 08.06.2010: надо бы иметь ДВА log-файла для драйверов: один обычный, для их внутренней отладочной информации, а второй -- для "специальных" данных, которые, в силу каких-либо причин, лучше публиковать не через каналы, а через логи (всякие там перезагрузки модуляторов); и, раз уж есть logging-API, то пусть и "специальный" лог реализуется через него же.

    На уровне API указание "спец-лог" можно делать либо специальным DRIVERLOG_C_, либо, лучше, просто совсем отдельным флажком -- рядышком с DRIVERLOG_ERRNO.

  • 22.10.2012@Снежинск-каземат-11: переводим внутреннюю реализацию API DevTout, DevFD, DevIO на SLOTARRAY.

    22.10.2012@Снежинск-каземат-11: процесс:

    • К именам всех трёх массивов добавляем суффикс _list.
    • Вводим троицу SLOTARRAY_DEFINE_FIXED().
    • И переводим реализации API на них.

    Вопрос теперь -- будем ли переходить на GROWING (для Tout -- на GROWFIXELEM) и как и когда.

    28.06.2013: да чё уж там -- сразу ставим "done", поскольку сейчас вообще весь этот механизм будем ликвидировать (в связи с переходом на uniq).

  • 28.06.2013: пришла пора изводить механизмы DevTout, DevFD и DevIO -- в пользу прямой работы драйверов с cxscheduler/fdiolib на основе uniq.

    28.06.2013: сделано.

    • "Драйверов" реально было только remdrv_drv.c да cankoz_lyr_common.c.
    • Среды исполнения -- lib/srv/cxsd_driver.c (оч-чень изрядно похудел) и эфемерный canserver.c.
    • Из .h-файлов -- собственно cxsd_driver.h, cxsd_hw.c (-40%) и remdrvlet.h (-60%).
    • (01.07.2013) еще был пустой lib/rem/remsrv_oslike.c.

    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() кусок -- оставим: так оно нагляднее.
  • 16.09.2013: сделана 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(), к которому она имеет отдалённое идеологическое отношение.

  • 13.11.2013: переименовываем ReturnChanSet() в ReturnDataSet(), в соответствии со стратегией из 20120413-CX-UPGRADE-TODO.txt/26-05-2012.

    13.11.2013: да, ReturnDataSet(). Оказывается, в не столь уж малом количестве мест присутствовало. Хотя внутренности всё равно почти пустые.

    03.09.2014: внутренности давно уж (с весны) как непустые, там присутствовало зародышевое складирование получаемых данных.

    Сегодня:

    • Переделано, что при обнаружении косяков вместо "return" делается "goto NEXT_TO_UPDATE".
    • Добавлен вызов подписавшихся на эти каналы по свежевведённому в cxsd_hw API CxsdHwCallChanEvprocs() -- отдельным 2-м циклом, так что все скопом ПОСЛЕ обновления.
    -- т.е., всё по образу cda_dat_p_update_dataset(). Осталось еще конверсию данных оттуда взять.

    04.09.2014: конверсия сделана. Но в сервере есть пара существенных отличий:

    1. Тут НЕ требуется пересчёт по {r,d} -- только преобразование типов, причём опциональное.
    2. Может быть нежелательной работа с вещественными или с int64.

    Поэтому блок преобразования сильно отличается от cda'шного:

    • Сам набор вариантов преобразования:
      1. "Идентично" -- просто memcpy(). Причём идентичность считается по srpr==repr && ssiz==size, а не по совпадению dtype'ов, так что типы, различающиеся лишь знаковостью, считаются идентичными.
      2. "Оба целочисленные" -- цикл {чтение,сохранение}, причём стоит отдельная проверка, что хоть один из вовлечённых типов 64-битный, и тогда цикл делается
        1. через int64, но этот вариант заключён в "#if MAY_USE_INT64", а иначе сваливается в...
        2. через int32, причём тут проверок на INT64 нету, а он идёт как UINT8.
      3. "Хоть один да вещественный, а второй вещественный или целочисленный" -- цикл {чтение,сохранение}, через float64 (т.е., double).

        Этот вариант заключён в "#if MAY_USE_FLOAT", так что при запрещённости использования вещественных сваливается в...

      4. "Несовместимо" -- так же просто делается bzero().
    • Т.е., оптимизировано на вариант "передача как есть". В 99.9(9)% случаев -- при корректной конфигурации, когда возвращаемые драйверами типы совпадают со сконфигурированными -- будет производиться просто копирование без какого-либо интеллекта. В т.ч. даже и копирование float-типов и int64 при невключенных соответствующих MAY_USE_*
    • Знаковость:
      1. На шаге "чтение" учитывается всегда, чтоб происходило корректное расширение со знаком (кроме вариантов *int32->int32 и *int64->int64 -- тут никакого расширения в принципе не будет).
      2. На шаге "сохранение":
        1. Из целочисленных -- знаковость игнорируется, поскольку "сужение" просто отбрасывает старшие байты, безотносительно знаковости.
        2. Из вещественных -- всегда.

    Итак -- теперь проверять. Но это уже после реализации cda_d_inserver.c.

    31.10.2014: функция перенесена в cxsd_hw.

  • 17.11.2013: идеологический вопрос: а не унести ли из cxsd_driver работу с каналами ("данными") в другой модуль (cxsd_data), оставив тут лишь собственно функционирование драйверов как модулей (и сервис для них -- логгинг, информирование)?

    Отдельный вопрос -- куда относится SetDevState()? Видимо, всё же сюда, но "того" он тоже касается.

    31.10.2014: да, ReturnDataSet() перенесена в cxsd_hw.c, там ей самое место рядом с CxsdHwDoIO() & Co. Насчёт остальных не так ясно, поэтому их пока не трогаем.

    20.11.2014: и SetDevState() тоже перенесена (вместе с ReRequestDevData() -- про неё и говорить нечего, очевидно, что относится к _hw). Точнее -- реальное её наполнение уже шло там.

  • 07.09.2014@дорога-с-пляжа-переход-над-Бердским: а не ввести ли в дополнение к businfo[businfocount] еще опциональную "businfostr" (вопрос, как указываемую -- суффиксом через '/' или как-то префиксом?)? Чтоб там мочь указывать специфику вроде "ttyUSB" и "canUSB". Или уж постараться обойтись auxinfo?
  • 14.09.2014@дома: радикальное изменение в CxsdDevChanProc(): теперь вместо first,count она принимает count,addrs[].

    14.09.2014: изменение было предопределено давным-давно:

    1. Последовательно-групповых запросов практически не бывает -- сплошь наборы. Даже возврат теперь только ReturnDataSet(), вот и усимметричено.
    2. Только так могут быть реализованы параметризованные запросы (аналоги бывших больших каналов).

    Вариант же с first оставался наследством от v2.

  • 29.10.2014: надо подумать о введении идеологии "being_processed, being_destroyed" -- как в librem*. Ведь с обычными драйверами вполне могут возникать аналогичные ситуации.
  • 10.07.2015: изменения cxsd_driver.h в связи с реализацией remdrvlet'ов (конкретно hw4cx/drivers/camac/cm5307_ppc_drvlets/).

    10.07.2015: действия:

    1. Введена поддержка TRUE_DEFINE_DRIVER_H_FILE.
    2. Определения для layer'ов (методы и метрика) перенесены в точку ПЕРЕД определениями для драйверов.

      Причина -- cm5307_DEFINE_DRIVER.h косвенно include'ит remcxsd.h, в котором используется CxsdLayerModRec, и получалась parse error.

  • 22.09.2015: хоцца иметь для драйверов возможность простого парсинга auxinfo для не-psp-случаев, а когда там указан некий набор слов через whitespace (например, имён "соседских" каналов -- нужно для mux_write_drv.c и double_iset_drv.c).

    Сейчас это парсят сами драйверы и оно мерзко; а вот если б можно было сразу иметь количество параметров и простой доступ к N-тому параметру в стиле "argp(N)" -- как сделано в cxsd_fe_cxv2.c и в remsrv_drvmgr...

    24.09.2015: задача усложняется тем, что НЕЛЬЗЯ модифицировать auxinfo, забивая его '\0'ями -- оно не зря const char*; в сервере эти строки вообще лежат в strbuf[]'е.

    P.S. Кстати, родственный механизм -- datatree'шный get_knob_item_n().

  • 04.05.2016: замечена странность -- в cxsd_driver.h присутствует #include"fdiolib.h, при том, что ни в cxsd_driver.h интерфейс fdiolib, ни в cxsd_driver.c сама fdiolib НИКАК не используются.

    Замечено было потому, что в remdrv_drv.c -- очевидно использующем fdiolib -- оного #include нету.

    04.05.2016: размышления по ходу расследования:

    • Первая мысль была -- "а фигли -- убрать из cxsd_driver.h, и пусть уж каждый конкретный драйвер делает #include!".

      Кстати, этих "конкретных драйверов" СЕЙЧАС -- ровно один remdrv.

    • Потом было замечено наличие и cxscheduler.h, также самими cxsd_driver.[ch] никак не используемым.

      Но тут годится логика, что это бывшие когда-то частью серверного API (до перехода на "cxscheduler3" с uniq), поэтому нехай будет.

      ...кстати, а вот он-то используется почти всеми драйверами -- ибо без В/В и таймаутов драйверы слабо осмысленны.

    • После чего, для единообразия, было решено, что присутствие fdilib'а -- как и cxscheduler'а -- в cxsd_driver.h можно считать как бы декларацией предоставления этих интерфейсов сервером.

      Так что пусть остаются.

  • 14.02.2020: некоторое время назад возникли 2 различных, но близких по предполагающейся реализации потребности:
    1. Уметь указывать для каналов некое дополнительное "строковое свойство".

      Занадобилось оно в интересах драйвера bridge_drv.c -- чтоб тот мог получать (по-канально!) информацию, из какого "внешнего" канала добывать данные для данного.

      Принципиально такая возможность появилась после реализации концепции "dcpr", но среди имевшихся строк свободной была только rsrvd6, и её всё равно не хотелось нагружать сторонними задачами.

    2. ЕманоФедя захотел мочь указывать в devtype'ах какую-то информацию для себя. Его хотелки -- весьма мутносформулированные -- были в письме от 13-02-2020:
      еще в тему диапазонов, единиц и прочего:
      У меня в бд есть поле "тип доступа", используемый при
      сохранении/загрузке режимов.
      Это поле несколько расширяет классификацию каналов, которая есть в CX,
      т.е у тебя это только r и w, а у меня кроме этого еще канл может быть
      помечен как:
      - настроечный (т.е. в обычной работе он не грузится, а служит для
      передачи настроек между программами)
      - процедурный (т.е. в него пишут чтоб выполнить процедуру, а читать
      его смысла нет)
      - производный (т.е. запись его возможна, но он отображается в
      оборудование через дополнительные вычисления,
      это как для Д16 задержка, т.е. загрузка режимов по производным каналам
      - грузятся времена,
      а по обычным каналам записи - грузятлся значения регистров.)
      

      В ответ на уточняющие вопросы он опять начал грубить, но стало ясно, что просто строки с произвольным содержимым ему для начала будет достаточно.

    Откуда напрашивается -- ну добавим в dcpr ещё пару строк, чтобы они могли указываться в devtype.

    14.02.2020@дорога из 13-го, затем лифт: названия для информации -- dbprops и drvinfo.

    16.02.2020@дома-воскресенье: делаем!

    Записываем сюда несколько позже, и по общему результату в качестве места выбран раздел по cxsd_driver, в силу того, что описываемый функционал предназначен для драйверов, да и значительная часть работ сошлась к этому модулю (хотя было ОЧЕНЬ много где).

    Также были сделаны ещё некоторые работы по связанным вопросам, они тоже описываются здесь.

    Итак:

    • Инфраструктура парсинга:
      • cxsd_dbP.h: к CxsdDbDcPrInfo_t добавлены поля dbprops_ofs и drvinfo_ofs.
      • cxsd_db_via_ppf4td.c: к dcpr_fields[] добавлены соответствующие строки.
    • Усовершенствования драйверного API:
      • Для доступа [драйверов] к данной информации введена 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: проверяем, на примере единственного пока драйвера-юзера -- да, работает!

  • 18.08.2023: введена GetDevChanByName(), позволяющая драйверу сделать резолвинг имени своего канала в номер.

    Занадобилась -- пока потенциально -- для мета-драйвера modbus_tcp_drv.c, чтоб можно было указывать границы каналов для обработки ответов на групповые запросы.

    18.08.2023: мини-протокол.

    • Поселена в cxsd_driver.c, и сводится к вызову CxsdDbFindChanInDevice.
    • Оная CxsdDbFindChanInDevice() -- просто адаптер к уже ранее имевшейся в cxsd_db.c FindChanInDevice(), являвшейся подмастерьем CxsdDbResolveName().
    • Поэтому в адаптер добавлена проверка на попадание в диапазон каналов устройства (актуальна для числовых спецификаций вида 123456).
    • ЗАМЕЧАНИЕ: в отличие от всех прочих родственных API -- GetDevPlace() и CxsdHwGetChanType() с CxsdHwGetChanAuxs() -- тут возвращается номер канала ВНУТРИ УСТРОЙСТВА, а не глобальный.

    Проверено ручной симуляцией -- в драйвер напихано несколько вызовов с печатью результатов; работает как положено, "done".

    25.04.2024: первое использование, в simkoz_drv.c для резолвинга имён каналов (SRC:DST) -- работает.

cxsd_db:
  • 20.01.2010: модуль, реализующий «API для БД -- "создать объект-БД", "добавить в объект-БД строчку"» (см. bigfile-0001.html за 22-06-2005.

    Используется префикс CxsdDb, сам тип-БД -- CxsdDb (это указатель).

    Замечание: этот модуль отвечает за ОПИСАНИЕ БД, а не за "используемое сейчас сервером описание аппаратуры" -- то епархия cxsd_hw.

    01.07.2013: имелся ляп общего характера в CxsdDbAddDev() и CxsdDbAddLyrI(): оно трогало содержимое передаваемой структурки. Несмертельно, но криво. Переделано на нетрогание и использование локальных переменных (new_*).

  • 13.12.2013: слегка поменян API: теперь CxsdDbLoadDb() передаётся argv0 -- и далее по цепочке, в интересах плагинов-загружателей (тем более, что в CxsdDbLoadDbViaPPF4TD() он уже предусмотрен).
  • 13.12.2013: был непотребный ляп в cxsd_db_plugmgr.c: оно пыталось идти по списку плагинов при помощи mp++ вместо mp=mp->next. Видимо, скопированность из KnobsCore_knobset.c, где таблицы двухуровневые (knobset/list), и на нижнем уровне действительно массив указателей на метрики.
  • 25.03.2015: потихоньку приступаем к реализации системы имён каналов, идеологически-технологически описанной в разделе "Статическая БД" за 23/24/25-03-2015.

    26.03.2015: собственно:

    • Добавление типов и полей:
      • CxsdDbCpLine_t -- описатель одной точки контроля. Сейчас в ней только поля name_ofs и cpn_n, но туда же будут добавляться {r,d} и прочие требуемые.
      • CxsdDbCpNsp_t -- "namespace", содержит данные для devtype плюс в конце [0]-массив описателей.

        Штука простая -- создаётся malloc()'ом, потом по мере надобности растится realloc()'ом, освобождается одним free().

      • CxsdDbInfo_t дополнилась:
        1. strs с обвязкой -- "БД строк".
        2. nsplist со свитой -- массив указателей на namespace'ы.
    • CxsdDb-функции работы с этим хозяйством.
    • Использование в cxsd_db_via_ppf4td.c:
      • Парсинг списка-групп-каналов вынесен в отдельную ParseChanGroupList(), чтоб быть доступным для devtype_parser()'а.
      • Скопирована из Cdr_via_ppf4td.c (с мин.изменениями) "обвязка парсинга" -- PeekCh()/NextCh(), SkipToNextLine().

        Изменения -- в ссылке на контекст: в Cdr передаётся parse_rec_t, а тут ppf4td_ctx_t. Так что унификация в общем include-файле невозможна; но разных парсеров не так много, так что несмертельно и можно такое дублирование терпеть.

      • Собственно devtype_parser():
        • Устройство простое: сначала парсит ТИП_УСТРОЙСТВА и ИНФО_ПО_КАНАЛАМ, потом аллокирует nsp и в цикле парсит по строчке, пока не встретится закрывающая '}'.
        • ИМЯ и НОМЕР каждого канала пока берёт как-есть -- без поддержки диапазонов/расширений (желательных в будущем, чтобы каждую группу имён определять одной строкой).
        • Зато в качестве НОМЕРа позволяет указывать ранее указанное ИМЯ -- чтобы делать alias'ы.
        • После получения всего списка регистрирует nsp в db.
      • В 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.

    Собственно, хохма в том, что (после устранения пары мелких ляпов) РЕЗОЛВИНГ ИМЁН РАБОТАЕТ!!!

    Дальнейшие планы:

    1. Возможность указывать диапазоны каналов (иначе с нашими многоканальными устройствами вешаться можно).

      Например, формат такой:

      PREFIX<a-b>SUFFIX NUMBER
      (как в zsh). При просто числе во втором поле там никакой ссылки на "точку вставки шаблона" указывать не надо -- просто будут последовательно прибавляться 1,2,... к базе. А вот если указывать там тоже имена -- то некоторый вопрос (хотя зачем такое может понадобиться -- еще больший вопрос).
    2. Поддержка cpoint'ов.
    3. ...для которой надо будет добавить в свойства еще {r,d}.

    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().
    • Задел на будущее: добавлены определения для каналов _devstate и _devstate_description; CXSD_DB_CHAN_DEVSTATE_OFS и CXSD_DB_CHAN_DEVSTATE_DESCRIPTION_OFS, плюс CXSD_DB_AUX_CHANCOUNT определяет общее количество "дополнительных" каналов.
    • Собственно резолвинг -- CxsdDbResolveName(). Ей передаётся db и имя, а она возвращает:
      1. Статус резолвинга в return value, константы с префиксом CXSD_DB_RESOLVE_:
        • ERROR -- облом.
        • GLOBAL -- глобальная ссылка (просто номер канала); номер вертается в chann.
        • DEVCHAN -- ссылка DEVNAME.CHAN; устройство возвращается в devid, номер канала в нём в chann.
        • CPOINT -- точка контроля; номер отдаётся в chann (эффективно -- аналогично GLOBAL, просто номер >=numchans).
      2. devid_p -- (по указателю) номер устройства, для DEVCHAN.
      3. chann_p -- (по указателю) номер канала, либо внутри устройства, либо глобальный.

      Т.е., она выполняет ТОЛЬКО резолвинг.

    • Заполнение свойств -- по-прежнему задача 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
    }
    

    После надлежащего исправления проблема ушла.

  • 15.05.2015: приступаем к до-реализации системы имён -- поддержке "точек контроля", по идеям, рождённым в разделе "Статическая БД" в конце марта 2015г.

    Для начала именно как имён, а потом добавим свойства, и следующим этапом будет GURU.

    15.05.2015: вопрос о двух частях:

    1. Связанная тема -- channels:
      • 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() -- чтоб сразу можно было проверять на невыход указываемых номеров каналов за границы.
      • И сама проверка тоже добавлена.
    2. Собственно cpoint'ы:
      • Соображения:
        • Жить они должны ТОЛЬКО в БД (в hw им делать нечего).
        • Ссылки на имена пусть записываются номерами-"адресами" в namespace.
        • Ссылаться можно только на РАНЕЕ определённые сущности.
        • Надо составить список возможных типов ссылок:
          • На аппаратный канал -- пусть оно прямо через CxsdDbResolveName() и ищется.
          • На устройство ("симлинк на директорию")?
          • А на ссылку на устройство ("симлинк на симлинк на директорию")?

    17.05.2015: продолжение, по тем же частям:

    1. Фича "channels" в таком виде -- как просто копия парсинга -- бесполезна: ведь осмысленным cpoint'ам надо мочь ссылаться на ИМЕНА каналов, определённые в devtype.

      Процесс (всё не так просто!):

      • К ParseChanList() добавлен параметр type_nsp_id, в котором channels_parser() передаёт его самого, а devtype_parser() -- -1.

        И поиск по нему тоже добавлен.

      • Но надо его и в CxsdDbDevLine_t.type_nsp_id прописывать СРАЗУ, а не задним числом, после парсинга всего.

        Посему эта обязанность переложена с CxsdDbLoadDb() на dev_parser() (он просто СРАЗУ выполняет поиск по указанному TYPENAME).

      13.02.2017: да, не то что "бесполезна" -- не работает. Подробнее см. в отдельном комментарии за сегодня ниже.

    2. Возможности cpoint'ов -- дальнейшие соображения:
      • Если
        • Отбросить (хотя бы временно) всякие экзотические потребности (вроде direct-ссылок, "обходящих мимо" иерархии, содержа в имени сразу несколько компонентов.через.точку и ссылаясь куда-то -- для overriding'а имеющихся в иерархии имён).
        • И попробовать прикинуть МАКСИМАЛЬНЫЕ требования, позволяющие наиболее гибко строить виртуальные иерархии.
        ...то получается, что нужна возможность строить просто дерево "ссылок" -- этакая виртуальная файловая система.
        1. Контейнерами/директориями/ветвями будут "виртуальные устройства" (в т.ч. возможны вложенные).
        2. Оконечными узлами будут ссылки на конкретные каналы в устройствах.

        А такое сделать вполне можно!

      • Прикидка проекта:
        1. Оконечные узлы могут ссылаться как на аппаратные каналы, так и на другие оконечные узлы.

          Собственно, эти "оконечные узлы" и есть сами cpoint'ы, обладающие такими свойствами:

          1. Таблица оконечных узлов -- cpoint'ов -- единая сквозная на всю БД (т.е., общая на ВСЕ "виртуальные устройства").
          2. Строки содержат в себе {r,d} -- phys_rds[2] -- плюс флаг их указанности -- phys_rd_specified.

            Также, вероятно, и толпу строк -- units, comment, ...

            Замечание: FillPropsOfChan(), идя по цепочке от верхней точки к нижней (аппаратному каналу), должен будет оставлять в строковых свойствах "максимально верхние значения" -- т.е., если уже !=NULL, то более не трогать.

          3. Конечно, соблазнительно выглядит идея "если id канала >cxsd_hw_numchans, то это ссылка на точку контроля (надо вычесть cxsd_hw_numchans)"; но cxsd_hw_numchans заранее неизвестно:
            1. на этапе парсинга оно потихоньку растёт;
            2. CxsdHwSetDb() добавляет 0-й девайс со 100 каналами.

            Проще будет взводить какой-нибудь дальний битик -- например, 30-й (не НЕ 31-й, ведь число должно быть положительным!). А со срезанным этим битиком оно и будет индексом в таблице "оконечных узлов".

          4. Ссылки на аппаратные каналы НЕЛЬЗЯ делать сразу номерами -- поскольку их номера изначально не фиксированы и могут двигаться по мере вставки устройств (тот самый 0-й девайс со 100 каналами).

            А надо дуплетом DEVNAME.CHAN_N. Причём ссылка DEVNAME -- конечно, str_ofs'ом. 21.05.2015: уже всё-таки devid.

            Следовательно, в типе-структуре "конечный узел" будет этот самый дуплет int'ов.

        2. Каждая "ветка"/"контейнер" будет отдельным namespace'ом (да, место под changroups[] будет пропадать впустую...).
          • В т.ч. КОРНЕВОЙ контейнер, в который помещаются и ссылки (на конечные узлы) с именами без точек -- эдакие "глобальные".
          • Критичные особенности функционирования:
            1. Имя должно быть пустым (т.к. это как бы инод, имени, как таковой, иметь не могущий). А доступны ветки только по ссылкам.
            2. Ссылки "извне" будут по nsp_id, а не указателями -- поскольку по ходу наполнения БД указатели могут и меняться.
          • А вот содержимое CxsdDbCpLine_t, возможно, придётся расширить -- для возможности указывать "тип" ссылки, которая сама записана в поле cpn_n: либо на вложенный контейнер (тогда это nsp_id), либо на оконечный канал/cpoint (это cxsd_cpnt_t).
          • Как исполняются линки НЕ на конечные узлы:
            • На устройство: А ХБЗ!!! ПОКА ЗАПРЕТИТЬ? А ВЕДЬ ЭТО *ОЧЕНЬ* НУЖНАЯ ФИЧА! Видимо, указывать какой-то еще более другой, "3-й" тип ссылки. Но как использоваться такие линки? Только в
            • На имеющийся контейнер (виртуальное устройство): очень просто -- в поле "cpn_n" будет записываться то же значение nsp_id, что и у target-контейнера. Т.е., в чистом виде hardlink.
        3. Решение потенциальной проблемы циклических/бесконечных ссылок:
          1. Требование: при парсинге оконечного узла его target уже должен быть определён.
          2. Если после п.1 и останутся какие-то лазейки для зацикливания, то решается в стиле Unix/symlinks: ограничиваем длину цепочки ссылок, например, 20 штуками.
      • Замечания:
        • Для реализации этой схемы ПРИДЁТСЯ перейти на хранение всех имён в strbuf'е.
        • У нас получаются в смысле директорий не СИМлинки, а самые что ни на есть ХАРДлинки!

    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.

    • Поскольку namespace'ы теперь освобождаются от планировавшейся для них "общей" роли, с потенциальным ссыланием на cpoint'ы, то отражаем это в именах, заменяя "Cp" на "Dc" (Device Channel):
      • CxsdDbCpLine_t -> CxsdDbDcLine_t, и её поле cpn_n -> devchan_n.
      • CxsdDbCpNsp_t -> CxsdDbDcNsp_t.

    19.05.2015: собственно работа:

    • Еще вчера сделан базовый парсинг -- и имён, и ссылок (обе части парсятся последовательно, "кусками", разделёнными '.'); и групп (в {}) тоже.
    • Кстати, гложут мысли -- а что должно указываться в TARGET'е?

      СЕЙЧАС -- там абсолютные ссылки.

      А не понадобится ли как-то поддерживать ОТНОСИТЕЛЬНЫЕ?

      • Либо в стиле файловой системы -- чтоб считалось от текущего уровня вложенности, но:
        1. Начальная '.' -- ссылка от корня (аналог "/").
        2. Начальные ':' в каком-то количестве -- переход на сколько-то уровней выше (аналог "../").
        ...короче -- в точности, как в cda.
      • Либо указывать "базовый target" в контейнерных cpoint'ах -- расширить синтаксис с
        ПРЕФИКС {
        на
        ПРЕФИКС_ИМЕНИ [ПРЕФИКС_TARGETа] {

        И чтоб у вложенных ПРЕФИКС_TARGETа конкатенировался к предыдущему. В этой всей конкатенации тоже явно понадобится поддержка начальных '.' и ':'.

      Сейчас просто думаем, а делать или нет -- посмотрим по результатам использования.

    • Собственно СОХРАНЕНИЕ:
      • Сначала резолвится TARGET. Чтобы потом понять, возможно ли сделать такой "линк". Оно, кстати, так и в Linux'ной утилите ln -- если исходного файла (НА который делается ссылка) не существует, то имя для создания оно и проверять не будет на создавабельность.
      • Потом идётся по очереди по составляющим компонентам полного имени cpoint'а, углубляясь по ним в виртуальную иерархию -- для проверки, можно ли создать такую "поддиректорию" (clvl):
        1. Если такого имени на данном уровне нету -- то всё окей, проверка прекращается, а clvl будет создан позже.
        2. Если есть, и TYPE_CLEVEL -- тоже всё окей.
        3. А вот если есть, и НЕ CLEVEL -- значит, ошибка.
      • Затем идёт дальнейший проход по компонентам имени, для создания отсутствующих под-уровней (если нужно).
      • Наконец, создаётся сама "запись в директории" (clvl'е), с информацией о ссылке.

        И если это ссылка на DEVCHN или CPOINT -- то также создаётся собственно cpoint (аллокируется структура CxsdDbCpntInfo_t).

      • Менеджмент растущих массивов cpnts[] и clvls_list[] возложен на cxsd_db.c -- в нём туча создавателей, аксессоров и модификаторов.

        Причём с clvl'ами применена такая парадигма:

        1. "Клиент" (читай cxsd_db_via_ppf4td) имеет дело только с числовыми clvl_id, а при добавлении "строки в директорию" realloc() делается при надобности автоматом, и значение указателя в ячейке clvls_list[clvl_id] подменяется.
        2. "Корневая директория":
          • имеет clvl_id=1 (ROOT_CLVL_ID; а =0 -- зарезервированный пустой),
          • снаружи адресуется по clvl_id=-1,
          • создаётся автоматически при надобности.
      • На будущее добавлена пара типов-результата резолвинга:
        • CXSD_DB_RESOLVE_DEVICE -- устройство (ссылка по имени без точек, либо результат резолвинга, указывающий на узел, ссылающийся на устройство).
        • CXSD_DB_RESOLVE_CLEVEL -- ветка/контейнер (виртуальное устройство).

        Но резолвер их пока не поддерживает.

        20.05.2015: уже поддерживает.

    • Кстати: с нынешней архитектурой "экзотические потребности вроде direct-ссылок, обходящих мимо иерархии" удовлетворятся автоматом. Поскольку:
      1. У виртуальных устройств приоритет перед реальными, так что можно при наличии девайса DEV1 создать и виртуальный девайс DEV1, в котором перенаправить часть имён каналов, а остальные будут браться из реального, поскольку...
      2. ...при ненайденности в виртуальной иерархии (даже если в ней имя является некорректным) будет с нуля произведён поиск в реальной.

      21.05.2015: да, работает :)

    21.05.2015: за вчера и сегодня доделано до полностью рабочего состояния (за вычетом парсинга свойств).

    • Cpoint'ы отличаются от аппаратных каналов наличием 30-го бита -- 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() раскручивает цепочку
    • НЕ сделано пока то, что связано со свойствами (должное собираться последовательно при проходе от исходного cpoint'а к аппаратному каналу):
      1. Парсинг свойств -- и {r,d}, и строк. Его надо делать/разрешать ТОЛЬКО для ссылок на CPOINT и DEVCHN.
      2. Сбор этих свойств (включая накопление {r,d}) в FillPropsOfChan().
      3. Разделение на cpn и gcn, с точки зрения клиента: свойства собирать надо с cpn'а, а производить чтение/запись и вешать монитор надо на gcn.

        Этот вопрос еще просто недопроработан.

    Получившаяся архитектура -- гремучая смесь: можно сбрендить, пытаясь понять, почему как-то работает (или НЕ работает) некий набор ссылок/редиректов. По замудрённости похоже не только на симлинки, но и на функционирование mount (включая форточный join и *nix'ный union/overlay-mount) и редиректов в shell'е (где важен порядок редиректов).

    22.05.2015: есть несколько идеологических загвоздок:

    1. Первая загвоздка с указанием свойств (units, label, ...) аппаратным каналам: ведь указываются свойства НЕ каналам, а ИМЕНАМ; и на один канал может ссылаться НЕСКОЛЬКО имён.

      Первая идея -- проходиться по всем namespace'ам (точнее, по обоим namespace'ам каждого экземпляра устройства) и прописывать их target-каналам указанные в них свойства. Но в условиях множественности это выглядит не очень хорошей идеей...

      И что теперь -- запоминать еще и "ссылку" на исходное имя (дуплет nmsp:line), через которое канал был адресован? Тоже кривизна. По-хорошему, вообще не должно зависеть от способа адресации АППАРАТНОГО канала. КАК?!?!?!

    2. Вторая загвоздка -- желаемая возможность брать {r,d} от другого канала. Опять же -- как?

    26.05.2015: сделан парсинг свойств.

    • Разрешается только для target'ов типа DEVCHN и CPOINT, для прочих запрещено и при наличии чего-нибудь выдаётся ошибка.
    • Сам парсинг выполняет ParseCpointProps(), слизанная с Cdr_via_ppf4td.c::ParseKnobDescr(), только вместо гибкой таблично-плагиновой архитектуры там гвоздями забит парсинг либо строки, либо double (в последнем случае принудительно взводится phys_rd_specified).
    • FillPropsOfChan() правильно "собирает" эти свойства (берёт при неуказанности (текущий_ofs<0), а иначе более не трогает).

    27.05.2015:

    • И сбор {r,d} тоже сделан (потом массив "зеркалится", поскольку собирается задом-наперёд (от верхнего к нижнему, а {r,d} в cda процессятся от нижнего к верхнему).

      Проверено, и после исправления нескольких багов -- работает!!! Разные отражения одного аппаратного канала отдаются с разными значениями, отличающимися пересчётом.

    • А вот косяк с различением gcn/cpn (hwid/cpid) присутствует, даже после исправления багов. Зарыт он сейчас в ПРОТОКОЛЕ: в CxV4MonitorChunk нет отдельного поля "cpid" (а в CxV4CpointPropsChunk -- есть). 28.07.2015: "косяк" исправлен -- на уровне cxsd_fe_cx.c, подробности в его разделе.

      ...да и вообще -- не сократить ли количество chunk-specific полей, свалив их на стандартный CxV4Chunk, где зарезервировано 4 uint32?

    • Отправка клиенту строк сделана. На КУЧЕ уровней: fe_cx, cxlib, d_cx, и даже отображение в cdaclient'е (чуток халтурное).

    13.02.2017: попытка воспользоваться channels для именования каналов noop-устройства обломилась. С диагностикой

    channel number 0 is out of range [0-0)

    Странненько -- ведь работало же...

    15.02.2017: пытаемся разобраться -- это ParseChanList().

    • По первому впечатлению -- оно всегда берёт лимит (количество каналов в девайсе) из namespace'а, для которого парсит таблицу имён, а в случае channels там стоит 0.

      ...когда и как проверялось (в мае 2015-го!) -- хбз; возможно, до каких-нибудь крупных изменений; или, например, с подключенным devtype'ом (но это вряд ли).

    • Идея: а давай проверять, что если второй namespace (типовый -- type_nsp) есть, то брать количество из него!
      • Попробовал -- количество добывается в самом начале в переменную chan_n_limit (с которой потом всё и сравнивается): сначала из *nsp_p, а если указан type_nsp, то из него.
      • А вот фиг, не помогло -- всё ровно так же.
    • В конечном итоге стало ясно, что в разных случаях всё по-разному, и количество каналов то указано в одном из namespace'ов, то в другом, то не указано нигде вообще.
      1. devtype: количество есть в *nsp_p. Основное использование, с ним всё и работает.
      2. channels для устройств с "~": в type_nsp.
      3. channels для устройств с явным chan-grouplist: в 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 baachi {
      cpoint gunctl {
      }
    }
    
    -- не работает, ругается на вторую строчку «cpoint target "gunctl" not found».

    Разбирательство показало, что косяк в ParseOneCpoint(): там в проверке “Allow optional "cpoint" at the beginning of line” начало строки определяется по basebufused==0. Но это неправильно, т.к. во вложенных группах будет гарантированно не 0, а что-то явно >=2.

    Очевидно, что надо проверять не на ==0, а что с НАЧАЛА ТЕКУЩЕЙ строки ничего добавлено. Т.е., самое простое -- запоминать значение basebufused вначале и сравнивать с ним.

    24.04.2018: ну делаем...

    1. Добавлена basebufused_ini и сравнение уже на равенство с ней. ...не помогло.
    2. Оказалось, что сравнение с "cpoint" делалось по адресу basebuf вместо basebuf+basebufused. Исправлено, проехало дальше.
    3. Стало ругаться "Identifier expected". Ну правильно -- надо ж было еще пробелы пропустить; добавлено ppf4td_skip_white(ctx).

    Теперь заработало.

    • Вывод: этот "пропуск опционального в начале строки" в своём изначальном виде 3 года назад работать не мог ну вообще никак, и он просто не тестировался.

      ...хотя, учитывая общую крутость реализованной тогда инфраструктуры cpoint'ов -- простительно.

    • Замечание: зато теперь будет валидным синтаксис типа
      cpoint cpoint cpoint name target

      Чтоб избежать этого, надо б было вместо basebufused_ini сделать флаг keyword_forbidden и взводить его либо после пропуска, либо если это "не оно".

    25.04.2018: не, криво -- надо переделать.

    Сделано по вчерашнему проекту со взводимым флагом, только флаг назван "xtra_cpoint_fbd". А basebufused_ini убрана.

  • 15.05.2015@пляж: учитывая появление концепции "strbuf" (по факту это dictionary) можно переходить с имён типов/экземпляров/... прямо char[]'ами в структурах на dictionary-ссылки. Вкючая сами Nsp, кстати.

    Это, кстати, и сериализацию/десериализацию упростит, и уберёт ограничение 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: добиваем:

    • Ничем не отличалась и обработка info в 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 при уставленности.

  • 19.05.2015: надо бы, чтоб не замусоривать общее пространство, тонкости манипуляций с внутренностями db перенести в отдельный приватный файл -- например, cxsd_dbP.h.

    29.05.2015: сделано. ...но по факту cxsd_builtins.c и stand.c его включают, поскольку сами регистрируют модуль загрузки БД...

    Заодно и cxsd_hwP.h, ровно в тех же целях -- чтоб "клиенты" (cxsd, stand, pult) включали себе только публичный файл, с высокоуровневыми API, а "потроха" -- только для своих.

  • 20.05.2015: спрашивается -- а почему у нас добавление 0-го девайса возложено на CxsdHwSetDb()?

    Пусть это делает CxsdDbCreate() прямо при создании БД, там это раз плюнуть.

    (Первая идея была повесить это на CxsdDbAddDev(), автоматически при добавлении первого же устройства. Но тогда пустой конфиг не будет иметь никаких устройств вообще, даже 0-го, что плохо.)

    Выигрыш очевиден:

    1. Выкинется весь громоздкий код дублирования, забардачивающий CxsdHwSetDb(), да и совершенно нелегитимный в cxsd_hw.c.
    2. Номера каналов перестанут меняться между загружаемой и актуализированной БД.

    20.05.2015: сделано. Очень просто. И работает.

  • 17.06.2015: мелкая модификация чтения опциональных @LAYER и /DRIVER: теперь они могут указываться в произвольном порядке, а не только [/DRIVER][@LAYER].

    ...другое дело, что глубокого смысла в этом и нет -- обычно либо/либо.

    16.09.2016: пара идеологических замечаний:

    • "Глубокий смысл" может быть, если layer используется как HAL для доступа к чему-то, а /DRIVER приходится указывать тогда, когда просто "ИМЯ" используется как "typedef" (это очень любит ЕманоФедя с noop_drv).

      Да, сейчас у нас в 4cx/hw4cx/sw4cx таких случаев нет, но это не значит, что они не возникнут в будущем.

    • Синтаксис в принципе позволяет указание МНОЖЕСТВЕННЫХ layer'ов -- например, через запятую (@layer1,layer2,layer3).

      Некий теоретический смысл может быть для каскадирования layer'ов (в EPICS'е это используется); но вот как реализовывать (уже в cxsd_hw) -- тут некоторый вопрос.

  • 18.06.2015: вчера случайно обнаружился страшенный ляп, внедрённый при переходе от CxsdDbAddStr() к CxsdDbAddMem(): при записи терминатора '\0' оно его писало не в strbuf[retval+len], как надо, а в strbuf[retval+len+1].

    Как вообще всё работало -- большая загадка. Похоже, первый аллокированный килобайт (или килобайтЫ) был заполнен нулями, и везло.

  • 22.11.2015: былая static FindDevice() опубликована под именем CxsdDbFindDevice() -- чтоб cxsd_db_via_ppf4td.c::dev_parser() мог обнаруживать дублирующиеся имена устройств и ругаться сразу, не дожидаясь вызова CxsdDbAddDev() (ко времени которого контекст парсинга может уйти далеко и ругательство будет слабо-адресным).

    ...кстати, до сегодняшнего дня и в CxsdDbAddDev() тоже проверка отсутствовала.

  • 21.12.2015: парсинг auxinfo в cxsd_db_via_ppf4td.c::dev_parser() был кривоват: оно берёт текст ppf4td_get_string()'ом, в результате чего кавычки пропадали и на вход paramstr_parser'а поступала уже непарсимая строка (вылезло при реализации formula_drv.c).

    Проблема решена свежевведённым PPF4TD_FLAG_IGNQUOTES.

  • 07.03.2016: есть какой-то косяк с чтением БД: при указании серверу отсутствующего файла хоть m4 и ругается на stderr, но наверх никакой ошибки не возвращается; более того -- создаётся нулёвая БД из 102 каналов.

    Где именно косяк -- неясно.

    1. Вероятно, где-то в коде чтения в cxsd_db_via_ppf4td.c, т.к. утилитка ppf4td_test.c прекрасно ошибку отлавливает (она реагирует на ppf4td_nextc()<0.
    2. Также, вероятно, виновен и ppf4td_pipe_close(): он ВСЕГДА возвращает 0, хотя должен бы анализировать exitcode.

      ...неа, не он, а result_of_xxxxc(): в нём стоит проверка на тему статуса после waitpid().

      Странно то, что раньше (когда? точно ли?) ошибка отлавливалась, и даже где-то тут в файле есть замечание про то, что в RH-7.3 старый m4 не возвращал exitcode!=0 при ненайденности, а в RHEL5 (кажется) уже возвращает.

    20.12.2017: разбираемся.

    • А вот НЕ отлавливает ppf4td_test ошибку!
      • Так КАЗАЛОСЬ; при указании ей просто имени файла она использует схему plaintext, которая и обламывается c ошибкой.
      • Если же указывать явно m4::/non/existent, то ругательство лишь от самого /usr/bin/m4.
      • Хотя exitcode потом 1 (вместо 0), но это уже из-за того, что обламывается последующее чтение.
    • Далее, почему сервер умудряется "схавывать" такой несуществующий файл: открытие, как уже выяснилось, "работает" (не возвращая ошибки), а далее двухходовка:
      1. В начале цикла парсинга проверяется, не IsAtEOL() ли. И при !=0 переходит на SKIP_TO_NEXT_LINE. А !=0 -- это и -1 тоже, так что ошибки в этой точке игнорируются и считаются за пустую строку.
      2. А в SKIP_TO_NEXT_LINE делается "проверка на EOF" в виде
        if (ppf4td_peekc(ctx, &ch) <= 0) goto END_PARSE_FILE;
        -- т.е., и ошибка чтения тоже будет считаться концом файла, по коему БД (пустая) считается успешно считанной.
    • По анализу ppf4td_m4.c и ppf4td_pipe.c -- почему оно "схавывает" несуществующий файл, НЕ возвращая ошибку при открытии:
      • В ppf4td_m4_open() НЕ проверяется существование файла, а просто вызывается ppf4td_pipe'ное открытие.
      • А ppf4td_pipe_open() файл проверить уже не может -- не её это дело, она просто делает fork()+exec() с указанной командной строкой.

        И оный exec() отрабатывается БЕЗ ошибки (т.к. программа-то запустилась), а ошибка будет обнаружена уже далее.

      • Далее ppf4td_pipe'ом ошибка, очевидно, обнаруживается -- уже в result_of_xxxxc(), но на неё никто толком не смотрит:
        • Парсер БД игнорирует (сценарий выше).
        • А ppf4td_test просто молча отваливает по ошибке от ppf4td_nextc(), хотя и возвращая exitcode=1.

        Это утверждение -- про обнаружение ошибки -- тоже надлежит проверить, но уж скорее всего оно так.

      • Шокирующий факт: при открытии НЕ делается поиск -- нет там никакого findfilein(), который бы заметил отсутствие файла (точнее, никакого бы файла не нашёл).
        • А почему-то всегда казалось (при анализе причин этой ошибки), что оно там ищет, но не находя -- просто берёт последнее по списку.

          Возможно, так было где-то раньше (в v2?)?

        • Неа -- скорее всего, это воспоминание о "последнем" обусловлено тем, что при поиске _drv.so-файлов если выдаётся какая-то ошибка, то в ней присутствует путь к ПОСЛЕДНЕМУ "попробованному" (cxldr_get_module() так устроен, да и как иначе-то).
    • Вывод: надо перед запуском m4 всё-таки проверять 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 отсутствовал.

    Очевидно приходящие в голову мысли:

    1. Проверять не только файл, но и интерпретатор, с mode=X_OK.
      • При отсутствии -- возвращать -1/ENOEXEC (просто ENOENT нельзя: тогда будет неразличимо отсутствие файла и интерпретатора).
      • (А можно и на stderr ругаться.)
      • На вопрос "насколько такая проверка корректна, а не ищется ли при exec()'е указанный интерпретатор через $PATH?" ответ -- да, проверка корректна, т.к. используемый execv() поиска НЕ делает, в отличие от p-версий, вроде execvp().
    2. Какого лешего проверка была засунута в 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().

    • Принудительная -- проверяется всегда.
    • При обломе возвращается -1/ENOEXEC.
    • Но также и печатается ругательство на stderr, включая strerror(errno).

    Работает.

  • 18.05.2016: (12-05...18-05 -- даты не стояло) Надо бы сделать, чтобы CxsdDbResolveName()'ом:
    • Начальные '.' в именах каналов бы пропускались.
    • Как и множественные '.' внутри.

    Т.е. -- чтоб работало В ТОЧНОСТИ как '/' в именах файлов (за вычетом только ':' -- аналога "../").

    19.12.2017: пора.

    • НАЧАЛЬНЫЕ -- похоже, нет. Там прявно явный комментарий стоит «Forbid ".NNN" names (they'd present way too many difficulties)».
    • А вот множественные внутри -- да, сделано пропускание. Это там в двух точках: отдельно для виртуальной иерархии (проверяется по dot_p[1]=='.') и для аппаратной (проверка по *after_d=='.').
    • Замечание: если всё-таки захочется сделать игнорирование начальных точек, то надо будет вставлять это вместо проверки на "Forbid...", а сводиться это будет к "while(*name=='.')name++" -- т.е., модифицируется сам указатель name.

    Засим можно считать задачу исполненной.

  • 04.10.2016@лабораторный-круглый-стол-по-вторникам: в дополнение к возможному каналу "_devtype" может потребоваться еще один ПСЕВДО-канал -- "_devroot", чтобы это имя, будучи встречено в цепочке '.'-separated-компонентов, "перескакивало" бы в то устройство, на которое смотрит предыдущая под-цепочка.
    • Смысл -- что при нынешней навороченной схеме cpoint'ов приходится продираться через devlist-файл (занимаясь буквально реверс-инжинирингом), чтобы понять, какое же физическое устройство реально отвечает за конкретный экранный канал.
    • Прикладное значение -- если захочется посмотреть "пульсации" некоего значения, то нужно будет переводить его козак-АЦП в осциллографический режим, а это делается уже на уровне устройства, а не канала.
    • ...впрочем, там всё равно надо будет как-то указывать, КАКОЙ из каналов осциллографировать, а где этот номер брать?
    • Или для всех таких потребностей городить cpoint'ы (благо, они в магнитных системах всегда генерятся макросами)?
  • 13.07.2017: почему-то в cxsd_db_via_ppf4td.c при парсинге businfo использует strtol(,,10). В результате оно НЕ понимает числа с префиксом 0x и НЕ позволяет 16-ричные данные. (Вылезло при попытке указать 0x06 в качестве адреса для ADC4X250 на bivme2.)

    14.07.2017: исправляем на 0:

    1. В dev_parser() -- то самое место.
    2. В layerinfo_parser() -- чтоб "bus-number" тоже можно б было.
  • 19.12.2017: косяк при парсинге БД: если строка "cpoint..." не заканчивается NL'ем, то вылетает с ошибкой "target-name-component expected, No child processes" (последнее -- из errno, явно оставшейся от ppf4td_pipe'а и тут иррелевантной).

    20.12.2017: главная неясность -- где косяк: то ли в cxsd_db_via_ppf4td.c (в чём бы?), то ли где-то в недрах PPF4TD.

    • Например, пофиксенная сегодня проблема с незамечанием ошибки "нет такого файла" реально лежала в ppf4td_m4.c.
    • Также в пользу второго варианта говорит то, что в PPF4TD уже замечались какие-то сложности с корректным определением EOF'а -- ЕМНИП, не всегда корректно различались ситуации EOF и ошибка.
  • 20.12.2017: еще неудобство: если указывать имя БД с неизвестной схемой (например, zzz::/tmp/f.lst), то никакого описания проблемы не выдаётся, а просто "unable to read database".

    21.12.2017: чуток анализа:

    • В {cxsd.c,stand.c}::ReadHWConfig() никаких деталей не выдаётся потому, что CxsdDbLoadDb() при ошибке лишь возвращает NULL, но никакого описания не даёт.
    • А вот CdrLoadSubsystem() возможность возвращать ошибки имеет, через CdrSetErr()+CdrLastErr(), и для "unknown scheme" это делает.
    • ...и от самого PPF4TD ошибки возвращаются, в errno+ppf4td_strerror(), но их выдаёт на stderr сам cxsd_db_via_ppf4td.c (что не вполне правильно).

    Вывод: явно надо вводить API вроде "CxsdDbLastLoadErr()".

  • 07.11.2018: пришлось увеличить размер 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: надо делать парсинг в cxsd_db_via_ppf4td.c более корректным: чтобы whitespace ТРЕБОВАЛОСЬ, а не было бы "опциональным".

    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()" на некий внутренний вызов, который бы:

    1. вначале проверял, что в ТЕКУЩЕЙ точке действительно есть whitespace;
    2. при его отсутствии ругался бы и возвращал -1;
    3. а потом уж вызывал бы ppf4td_skip_white() и возвращал бы 0.

    Замечания:

    • Заменять надо ПОЧТИ потому, что в некоторых местах вместо whitespace допустим конец строки.

      То ли просто разрешать конец строки (всё равно дальнейший код должен уметь его обрабатывать), то ли ввести флажок "конец строки допустим".

    • Нынешний слепой вызов ppf4td_skip_white() должен заменяться на вызов с проверкой результата -- чтобы отваливать.
  • 19.06.2019: еще одна задача, порождённая ЕманоФедей: у него была потребность делать устройства с именами вида K500, K500.subdev, K500.subdev.subsubdev, ...

    Т.е., чтобы "контейнер" в иерархии являлся бы также и "оконечным узлом" (точнее, пред-оконечным -- устройством).

    • Нынешняя архитектура cpoint'ов такого никак не позволяет.
    • ...в отличие, кстати, от первоначального проекта (~2008 года?), когда предполагалось, что cpoint'ы будут просто симлинками-именами, СТРОКАМИ, без какой-либо интерпретации сервером самих их имён.

      Сейчас же cpoint'ы реализованы как "иерархическая файловая система", что даёт бонусы с точки зрения возможности "делать cd в под-иерархию и пользоваться относительной адресацией".

    • И EPICS, конечно, позволяет городить произвольные имена -- но это потому, что у него вообще отсутствует иерархичность и хоть какая-то интерпретация оных имйн: они являются просто строками и могут содержать произвольный мусор.

    Надо ли думать в сторону реализации подобной возможности -- вопрос. Пока вроде и так обходимся.

    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'а надо как бы "возвращаться на начало" и повторять попытку; и если более ничего не подходит -- переходить к последующим иерархиям.
      • Для предотвращения бесконечных циклов (которые в такой схеме делаются легко: "alias a b" плюс "alias b a") надо ограничивать число итераций резолвинга -- как это делает ядро для симлинков, где ограничение в районе 128.
      • ...некоторый вопрос -- КАК "возвращаться на начало": ведь придётся как бы сформировать новое имя, вида RESOLUTION.TAIL (где "TAIL" -- то, что шло в исходном имени после '.', на которой закончился alias).
    • Проверять эту иерархию, соответственно, надо ПЕРЕД виртуальной.

    Технически такое реализовать вроде не очень сложно (с единственной загвоздкой в "сформировать новое имя"). Вопрос -- НАДО ЛИ? Эта анархия прилично подсломает нынешнюю элегантную схему cpoint'ов.

  • 03.12.2019: ещё одна хотелка ЕманоФеди (высказанная вчера, в очередной раз): чтоб строковые свойства можно б было указывать прямо в devtype'ах, а не только в cpoint'ах.

    03.12.2019: чуток обсуждения:

    • На этапе разработки всей архитектуры devtype'ы предназначались исключительно для "сервиса имён" -- это просто карты соответствия ИМЯ:НОМЕР.

      А реквестируемое ЕманоФедей использование "как описателя классов", a-la ООП, и близко не предусматривалось.

    • Но нельзя не согласиться, что получившаяся схема, чисто по синтаксису, действительно наталкивает на мысль, что было бы удобно её использовать для задания ВСЕХ свойств данных имён.
    • Мысль: у нас ведь используется "раскрутка" -- поиск идёт сверху вниз по cpoint'ам, заканчиваясь на аппаратных каналах, в основном на ИМЯ:НОМЕР в devtype'ах (точнее, на CxsdDbDcLine_t'ах в CxsdDbDcNsp_t'ах).
    • Так вот: ну и добавить оным CxsdDbDcLine_t возможность кроме devchan_n содержать также и остальную пачку свойств (возможно, даже включая {R,D}). А поиск-раскручивание пусть умеет использовать эти свойства ровно так же, как и от cpoint'ов: если уровнями выше они не указаны, то взять отсюда.
    • Некоторые размышления о возможных деталях реализации УКАЗАНИЯ и хранения свойств в devtype'ах:
      • Один нюанс: учитывая возможность группового указания имён -- NAME<MIN-MAX> -- возможно, рациональнее будет свойства всей группы (а они ж одинаковые) хранить в одном экземпляре, ссылаясь на него (по номеру) изо всех CxsdDbDcLine_t'ов, а не у каждого индивидуально.
      • ...хотя -- а не будет ли иметь смысла этот N (который от MIN до MAX) уметь интегрировать в строковые свойства: например, АЦП'шные каналы adc<0-23> вполне могли бы получать и метки "ADC$n".

        ...но как это указывать синтаксически -- совершенно неясно.

      • Сюда же: а ведь внутри devtype'а есть СВОИ ссылки -- например, в cac208.devtype:
                adc<0-23>       100
        
                adc_p10v        adc20
                adc_0v          adc21
                adc_t           adc22
                adc_pwr         adc23
        

        И как поступать с определением свойств ТАКИХ ссылочных каналов?

        По-хорошему -- тоже наследовать: если у adc_p10v что-то не указано, то пытаться взять оное у adc20; очевидно, прямо в момент определения.

    • Техническая проблема с реализацией идеи (о хранении кроме номера канала также и пачки свойств):
      • Дело в том, что ПОИСК выполняет CxsdDbResolveName().

        Он возвращает сразу числовой channel-ID (chann), и НЕ производит никакой "раскрутки".

      • Раскрутку же выполняет cxsd_hw'шный FillPropsOfChan(), получающий на вход уже готовый channel-ID (могущий быть как cpid'ом, так и gcid'ом).

        И он, в свою очередь, уже НЕ имеет никакого знания об использовавшихся при резолвинге devtype'ах.

      Как сие противоречие разрешить -- не вполне ясно.

      Напрашивается только не очень красивая идея возвращать из CxsdDbResolveName() также и информацию от devtype'а --

      1. хоть ссылкой на CxsdDbDcLine_t и/или CxsdDbDcNsp_t,
      2. хоть прямо структуру "свойства аппаратного канала, указанные в строке devtype'а".

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

    • Ещё мысль, могущая послужить решением проблем, обозначенных в предыдущей паре пунктов:

      А что, если для хранения "свойств аппаратного канала, указанных в строке 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.
      • ...либо 0 как признак неуказанности -- и в CxsdDbDcLine_t'ах использовать то же правило.

    Итого: возможный механизм реализации ЕманоФединых хотелок есть.

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

    03.12.2019: а вот, в ту же степь -- соображение "географически"-близкое: за 25-10-2018 было высказано пожелание «уметь бы всё-таки свойства типа "autoupdated" указывать в конфиге (видимо, в channels/devtype.

    И это свойство явно должно храниться рядышком с прочими phys_rds и строками.

    03.12.2019, подумано в разное время:

    1. @дорога-на-обед-домой, около мыши: пара замечаний:
      1. У нас получается РАЗНОРОДНАЯ информация: строки свойств берутся только по запросу, а вот autoupdated сервер должен будет взять оттуда явно, в момент создания -- видимо, в CxsdHwSetDb().
      2. R,D - аналогично. И, видимо, правильнее не делать dbpr'овы R,D дополнительным уровнем, как задумывалось утром (это ещё и усложнит работу FillPropsOfChan(), о чём утром не стал записывать), а корректнее их копировать прямо в свойства канала в cxsd_hw_channels[] -- ведь это именно "предварительная замена" для того, что сообщает драйвер.
    2. @лыжи после обеда: а не будет ли тут интерференции с механизмом GURU - точно ли cxsd_fe_cx не передаёт между своими экземплярами ничего важного из CxsdDbInfo_t (включая саму эту структуру, чей размер мы собираемся изменить)?

      @вечер-дома: слова "CxsdDbInfo_t" в cxsd_fe_cx.c не видно.

      10.12.2019: и "CxsdDbCpntInfo_t" тоже не видно, да и вообще -- похоже, что обмен данными идёт исключительно в терминах его собственных Guru*Chunk, а структуры CxsdDb*_t в передаче информации между экземплярами серверов никак не используются.

    3. @вечер-Быстроном-после-лыж: А ещё туда же можно сбагрить "диапазоны" для ЕманоФеди: range:T:MIN-MAX, где "T" - dtype-символ.
    4. @вечер-душ-после-лыж: похоже, надо всё-таки РАЗНЫЕ структуры, с разными таблицами-описателями для парсинга, но с ОДНИМ общим парсером. Тогда проблема "тут есть поле autoupdated, а там не нужно" решается автоматом.

    10.12.2019: приступаем к работам, по проекту недельной давности.

    • cxsd_dbP.h:
      • Введён тип CxsdDbDcPrInfo_t -- копия CxsdDbCpntInfo_t с убранными devid,ref_n и добавленными phys_rd*, range* и return_type.
      • В CxsdDbInfo_t добавлено поле dcprs (+_used,+_allocd).
      • В CxsdDbDcLine_t -- поле dcpr_id, которое если >0, то содержит номер dcpr'а в общем массиве dcprs[].
    • cxsd_db.c:
      • В CxsdDbDestroy() добавлено safe_free(db->dcprs).
      • CxsdDbAddDcPr(), скопированная с CxsdDbAddCpnt() с тривиальной заменой "cpnt" на "dcpr".

    Кстати, где может быть идеологическая собака зарыта:

    • если указания свойств "первого рода" (строковых) относятся в первую очередь к имени и будут использоваться именно при резолвинге,
    • то свойства "второго рода" (R,D, диапазон, return_type) относятся уже к физическому каналу.

    И может оказаться, что для ОДНОГО физического канала свойства указаны НЕСКОЛЬКО раз (при разных именах). Придётся этот момент как-то отлавливать -- правильнее всего прямо при чтении БД.

    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 -- т.е., ЦЕЛОЕ, затем точка, затем ещё ЦЕЛОЕ.

    Пара замечаний:

    • В качестве НЕуказанного надо считать с sec<0.
    • Соответственно, ПЕРЕД парсингом надо будет всей CxsdDbDcPrInfo_t-переменной делать bzero(), а затем отдельно nnn.fresh_age.sec=-1.

    @вечер: и тогда, судя по cxsd_driver.h'ову списку SetChan*(), неохваченным остаётся только квант.

    Не вполне ясно, зачем бы; но технических проблем -- ноль: указывать T:VALUE.

    13.12.2019: да, и поле для кванта тоже добавлено, как и строка для него в таблице парсинга.

    12.12.2019: продолжаем:

    • cxsd_hw.c:
      • В CxsdHwSetDb() добавлено "наследование" свойств из БД.

        Для этого на stage=1 проходится по обоим namespace'ам (type_ и chan_), и по каждой строчке смотрит, что если еёйный dcpr_id>0, то для указанного в этой строчке канала копирует в описатель канала те свойства, что указаны.

        13.12.2019: также добавлено проставление ссылки на этот dcpr.

      • 13.12.2019: В FillPropsOfChan() добавлен "учёт" строк из каналова dcpr'а, если оный у канала указан.
    • cxsd_db_via_ppf4td.c (начато ещё вчера):
      • Подготовка инфраструктуры для парсинга -- делаем что-то среднее между PSP и Cdr_via_ppf4td.c'шной (с неё явно будет браться многое). В отличие от имеющейся инфраструктуры "CFD" ("cptfielddescr"), эта должна быть способна парсить в ЛЮБЫЕ типы. Итак:
        • Сама инфраструктура имеет условное название "somefield".
        • Коды "типов" -- SFT_*: DBSTR, DOUBLE, INT, FLAG (это для return_type), RANGE, ANY (для quant), TIME.
        • Структура-описатель -- somefielddescr_t.
          • Есть очевидные поля -- type, name, offset.
          • Но также есть 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(), ...
        • ...с добавлением параметра "table" (описание свойств, могущих быть выпарсенными), и захардкоженный ctable везде заменён на table.
        • Также проверка типа по is_string сменена на проверку именно type.
        • Захардкоженное phys_rd_specified=1 для не-is_string удалено, ...
        • ...а добавлено прописывание =1 в поле, указываемое смещением в spc_f_ofs, если оный >0.
        • Парсинга диапазонов пока нет -- это задача на будущее.
        • А парсинг флагов даже сомневаюсь, стоит ли делать.

          19.12.2019: в конечном итоге решено пока не делать, ибо смысла в указании для noop-каналов чего-либо, помимо IS_AUTOUPDATED_NOT, просто нет, а парсинг бы очень сильно усложнился.

    • cxsd_db.c: а тут более ничего делать не потребовалось -- в связи с идеей НЕ добавлять dcpr_id к возвращаемой 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'а к отправке.)

    • И в результате получается, что резолвинг даёт frontend'ам некий идентификатор (cpid/gcid), который затем используется для получения свойств, ...
    • ...но с нынешним предполагаемым механизмом DcPr'ов этот идентификатор вдруг перестаёт быть достаточным, а требуется ещё как бы "история пути, которым идентификатор найден" -- т.е., dcpr_id, откуда надо дополнительно вычитывать строковые свойства (на случай, если они так и не определились в цепочке cpoint'ов).

    Откуда напрашивается мысль: прописывать 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() проставляется ссылка. И использование его при заполнении свойств каналов тоже.

    Итак, в конце концов доделано до долженствующего работать состояния -- проверяем.

    • Работает!!! Отдаются и строки, и R,D.
    • Был косячок, что из-за невыполнения инициализации всех строковых оффсетов в =-1 перед парсингом местами присутствовал мусор.

      Исправлено -- добавлена инициализация всего чего можно.

    13.12.2019@~16:30, семинар Барнякова о детекторе для C-Tau: несколько мыслей:

    1. О реализации: как-то громоздко выглядит обработка ANY: и сам парсинг, и складирование значения (и это ещё нормальной поддержки INT64 нету, с strtoll()!).

      Напрашивается 2 варианта:

      1. Унифицировать их -- сделать цикл, который будет для RANGE идти от 0 до 1, а для ANY от 0 до 0.
      2. Вытащить "громоздкие" операции в отдельные функции.

      Симпатичнее выглядит второй вариант.

      16.12.2019@лыжи, ~13:00: неа, экономичнее будет всё же сделать по первому. Надо будет выделить char*-переменную subfield_name, куда запихивать "", "-min" и "-max", плюс на итерации stage==1 принудительно ждать '-'.

    2. О типах:
      • Можно ж и unsigned поддерживать - в parse_numeric_dtype() и '+' проверять.
      • А ведь при парсинге ANY и RANGE нам dtype канала известен, так что можно позволить юзеру указывать "возьми оттуда". Например, "?:".

    14.12.2019@утро-дома-зарядка:

    • Не, "?:" -- некрасиво, пусть лучше "x:".
    • СТОП: а зачем вообще позволять юзеру указывать dtype повторно, если он от канала уже известен?

      Надо ВСЕГДА брать его от канала.

    Чуть поразмысливши: только немножко повозиться придётся -- в момент чтения БД никакого cxsd_hw_channels[] ещё нету, а есть только changroups[], да и то даже НЕ у ParseChanList(), а лишь у его вызывальщика. Придётся как-то оное передавать вглубь и вглубь, чтобы у ParseSomeProps() был бы dtype его target-канала.

    15.12.2019: движемся в нужном направлении.

    • Для начала -- сейчас, пока dtype указывается префиксом явно -- обнаружилось, что при указании типа, несовпадающего с типом канала, cdaclient показывает неправильное значение. Причём, похоже, что это значение совпадает со значением R либо D (кто из них был указан последним).

      16.12.2019: оказалось -- тупой косяк: в сохранении выпарсенного значения для REPR_INT''ов использовалась d_v (вещественная) вместо l_v (long). Вот оно и записывало последнее записанное в d_v значение -- R либо D.

    17.12.2019: продолжаем двигаться.

    • Начинаем с простого с украсивливания всего, пока НЕ трогая метод -- получения dtype.
      • Добавлен отдельный парсинг для INT64 -- через strtoull().
      • Складирование: вместо криво выглядящего вида "*((int16 *)ea) = l_v" сделано в стиле "a_p[0].i16 = l_v".

        (Оно CxAnyVal_t *a_p, и a_p = ea.)

      • Причём с ОТДЕЛЬНЫМИ ветками для unsigned-типов.
    • Далее -- расширяем парсинг ANY, чтобы он работал и для RANGE (по проекту, продуманному вчера на лыжах).
      • Введены 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 флажок PPF4TD_FLAG_DASTERM, чтобы '-' можно б было объявлять терминатором.

        Но тут же дошло, что '-' может быть вполне легальным символом в числе -- как знаком в первой позиции, так и знаком экспоненты далеко в глубине.

      • Временно, для проверки работающести концепции, переделал разделитель диапазона с '-' на ','.

        ...и на этой горестной ноте пошёл домой...

      • @вечер, дорога домой: море разных злых мыслей в голове:
        • @лестница-с-6-го-этажа-вниз: это что же получается -- теперь реально СРОЧНО надо будет реализовывать 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]).
      • И затем переделываем по модели "сначала всасываем всё значение в буфер, а потом простой парсинг из этой строки".

      Сделано, работает. Ура!

    • А теперь дОбыча dtype прямо из changroups.

      Тут ОЧЕНЬ многоуровневая цепочка:

      • Добыть dtype должен вызывальщик ParseSomeProps()'а.
      • А это:
        1. ParseChanList();
        2. ParseCpointProps() (в ближайшем будущем).
      • Второму-то оно вообще НЕ НАДО -- у cpoint'ов нету типо-зависимых полей (ни кванта, ни диапазона). Так что можно передавать UNKNOWN.
      • ...а вот первому -- придётся искать по changroups'ам, которые ему должны передать уже ЕГО вызывальщики.
      • А его вызывальщики -- это channels_parser() и devtype_parser().
      • У них changroups'ы содержатся в разных местах: у первого в CxsdDbDevLine_t, а у второго в CxsdDbDcNsp_t.
      • Откуда вывод: в ParseChanList() надо передавать дуплет changrpcount,changroups[].

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

      Итак:

      • "Вызывальщики верхнего уровня" -- channels_parser() и devtype_parser() -- берут changrpcount,changroups[] каждый из своего источника и передают в...
      • ...ParseChanList(), которому добавлена пара параметров.
      • В ParseChanList() сделана собственно "добыча" dtype.
        • Выполняется ТОЛЬКО в случае, если надо будет производить парсинг дополнительных свойств.
        • Делается простейшим перебором -- идём по changroups[], пока не найдём ту, в которую попадает канал с номером refval.
        • И тут как раз кроется небольшая идеологическая засада: ведь диапазонная ссылка -- "ch<0-4>" -- указывается чисто по номерам, к группам каналов никак не привязана и может пересекать НЕСКОЛЬКО разнотипных групп.
          • В результате dtype будет взят от ПЕРВОГО из каналов диапазона, хотя у последующих он, в принципе, может и отличаться.
          • (Правда и в старой модели, когда тип указывался префиксом "T:" в значениях, тоже получался ОДИН тип на потенциально разные каналы, т.к. им проставляется ссылка на один dcpr.)
          • Другое дело, что в разумной ситуации диапазоны обычно указываются ровно по группам.

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

      • Вот это дОбытое значение и передаётся в ParseSomeProps(), которому добавлен дополнительный параметр.
      • ...а вычитывание "T:" из входного потока убрано.

      Проверено -- да, тоже работает, диапазоны и квант получают правильный тип от исходного канала (первого).

      И да: проверено, что при пересечении диапазоном имён границы changroup'ов каналы второй группы получают свойства с типом от первой (ну ещё бы! с чего б было иначе?).

    • И старый вариант ParseCpointProps(), с собственным парсингом, заменён на простой вызов ParseSomeProps().

    Итого:

    1. Основную часть проекта считаем исполненной.
    2. Но особой удовлетворённости содеянным нет: НЕ элегантно и по-хорошему нужно какое-то совсем другое решение.
    3. Осталось же только в StdSimulated_rw_p() добавить учёт диапазонов.

    19.12.2019: научаем StdSimulated_rw_p() уважать диапазоны.

    (Записываем здесь, чтобы уж всё рядышком.)

    • Вставляем в ветку
              if      (action == DRVA_WRITE  &&  cxsd_hw_channels[gcid].rw)
              {
                  nvp = values[n];
                  nel = nelems[n];
              }
      
      (теперь это она РАНЬШЕ так выглядела :D)

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

    • Что перекурочено -- обзор крупным планом:
      • Присвоение 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];
      
    • ...т.е., если тип не один из "поддерживаемых", то действия опять сводятся к простому возврату присланного значения.
    • Содержимое же каждой ветки тривиально:
      1. Перекладываем в v.ПОЛЕ значение из values[n].
      2. Проверяем, не меньше ли это значение нижней границы диапазона, и если да, то корректируем.
      3. Проверяем, не больше ли это значение нижней границы диапазона, и если да, то корректируем.

      Вообще-то это довольно громоздко, поэтому для начала реализовано только для int8 (первый в списке, "модель") и для int32 (уже для реальной проверки)

    • Проверено -- ура, работает!!!
    • @лыжи, ~13:00+/-: некоторые соображения, пришедшие в голову на лыжне:
      • Вообще-то такая реализация не совсем корректна, поскольку НЕ проверяет осмысленность диапазона -- что минимум меньше максимума.

        Этак диапазон [0,0] будет считаться "указанным" и вгонять всё в 0.

        Посему -- нужно дополнительное условие, в КАЖДОЙ ветке.

      • Причём, это условие НИ В КОЕМ СЛУЧАЕ нельзя объединять в один if() с проверкой типа (т.к. так отсутствие диапазона приведёт к совсем иному результату), а оно должно быть уже ВНУТРИ ветки по данному типу.

        @ИЯФ, ~16:20: а вот и нет -- МОЖНО объединять, поскольку в таком случае ход исполнения "просколььзнёт" дальше, последовательно пройдя мимо всех следующих проверок типов (т.к. ни одна не сработает) и дойдёт до "else nvp = values[n];" -- что, в сущности, даст ровно тот же результат. Другое дело, что это а) неочевидно (типичный приёмчик "индусских программистов"); б) не факт, что эффективно.

      • Но такой код получается уж о-о-очень громоздким.

        Надо делать макрос, которому передавать только имя поля!

        И, поскольку он весьма многострочный, содержимое должно быть в "скобках" do { ... } while (0), чтобы красиво смотрелось в if() ... else if().

      • ...а ещё надо бы не забыть проверки на тему MAY_USE_INT64 и MAY_USE_FLOAT.
    • Итак -- улучшаем, по проекту с лыж:
      • Для начала добавлена проверка на осмысленность диапазона.
      • Затем весь этот блок проверок вытащен в макрос 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: возвращаемся к этому блоку парсинга:

    • Сделана поддержка SFT_TIME -- довольно примитивно: парсится double, потом с помощью modf() разбивается на целую и дробную части, и полю nsec присваивается time_frac * 1000000000.
    • Реализована ПОЛНОЦЕННАЯ возможность указывать параметры-флаги (SFT_FLAG) просто их именами, БЕЗ обязательного ':' в конце.

      Для этого непосредственно перед возвратом прочитанных в 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'а.

    Иные исправления:

    • Вытащен цикл по namespace'ам из цикла по 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) -- а никак! Там, конечно, всё сделано элегантно на грани гениальности:

    • Сами dcpr'ы к каналам не привязаны.
    • А просто заполняются и потом строкам в namespace'ах прописываются ссылки на dcpr_id (группам имён, вроде "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: кстати, а чё это у нас в devlist'е нельзя указывать беззнаковость типов (например, "+i" для UINT32)?

    17.12.2019: делаем. Вся работа сконцентрирована в ParseChangroup().

    Немного предварительного анализа:

    • Туда поступает уже готовый буфер, так что в ней парсинг уже очень прост.
    • Для симметрии хочется сделать, чтоб можно было указывать не только беззнаковость символом '+', но и знаковость символом '-'.
    • Проверено: в буфер, передаваемый ParseChangroup()'у, оба эти символа поступают нормально -- поскольку буфер парсится через ppf4td_get_string(), а у неё нет TERM-флагов ни для плюса, ни даже для минуса (дефиса).

    Собственно сделано:

    • Для начала то, что должно быть символом типа, проверяется, не является ли он плюсом или минусом.

      Если "да", то он сохраняется в sg_char, после чего в ut_char считывается следующий символ.

      По умолчанию же sg_char='\0', что означает "(без)знаковость не указана".

    • А уже после успешной проверки символа типа и складирования соответствующего ему значения в rec->dtype делается проверка: если знаковость указана, но тип НЕ-REPR_INT, то ругается на бессмысленность указания такому типу знаковости.
    • ...а если всё же REPR_INT, и ut_char=='+', то делается rec->dtype |= CXDTYPE_USGN_MASK.

    Собственно, всё. Проверено (с помощью "cdaclient -DH") -- работает.

  • 14.04.2022: надо бы крепко поразмыслить о возможности указывать "значение по умолчанию" для каналов прямо в devlist'ах.

    Немного комментариев:

    • Потребность -- создать общий механизм для возможности реализации того, что сейчас сделано в const_drv (коий понадобился в интересах onei32l).
    • Вместе с autoupdated_trusted (которому надо будет просто доделать копирование из БД в hw) это позволит прописывать значения не только каналам записи, но и каналам чтения -- драйвером в такой ситуации будет выступать прямо noop.
    • "За" такое решение -- что в cxsd_db_via_ppf4td.c уже есть готовый парсинг для всех типов значений.
    • Сложность же -- 1) заводить лишнее поле (CxAnyVal_t?) в CxsdDbDcPrInfo_t; 2) что с ВЕКТОРАМИ (да и со строками тоже)? 3) а ещё надо ПОВТОРЯТЬ инициализацию при оживлении устройства (точнее, при переходе из OFFLINE в любое другое состояние); а надо ли?

      15.04.2022@утро, просыпаясь: неа, повторять при оживлении -- НЕ надо. Ведь вообще весь этот механизм нужен не для абы каких устройств, а только для "виртуальных" -- вроде noop'а; а там предполагается, что должны сохраняться значения, прописанные юзером. Собственно, даже сама идея дергать состояние устройств-"почтовых ящиков" -- совсем бессмысленна. А соображение "надо бы повторять..." возникло исключительно потому, что при реинициализации каналы становятся невалидными.

    @~19:00, по пути домой мимо ИХБФМ: Псевдопроект:

    • В дополнение к strbuf иметь также "binbuf" -- для хранения бинарных данных.
    • В CxsdDbDcPrInfo_t иметь int-поля "defval_ofs" (по умолчанию =-1) и "defval_nelems"; это позволит указывать также значения с nelems=0.
    • Инициализацию надо исполнять в CxsdHwSetDb(); при инициализации канала надо ему также прописывать текущий timestamp.

      21.04.2022: с timestamp будет проблема: в InitDevice() делается RstDevTimestamps(). А InitDevice() вызывается и при начальном оживлении, и по записи в _devstate. Корень проблемы тут в том, что работа получается разделённой между CxsdHwSetDb() и инициализацией устройства. Что делать? Видимо,

      1. НЕ СБРАСЫВАТЬ timestamp'ы каналов, которым указаны умолчательные значения.
      2. Инициализацию выполнять в InitDevice().
      3. К InitDevice() добавить ещё параметр "первый ли раз вызывается" -- чтоб при повторных вызовах оно б не трогало текущие значения.

      Но тут будет проблема с тем, что прямой/готовый доступ к инициализационным данным из БД есть именно у CxsdHwSetDb(); да и вообще как-то громоздко получается.

      Неа -- не так, а проще:

      1. Просто НЕ СБРАСЫВАТЬ timestamp'ы каналов, которым указаны умолчательные значения.
      2. А инициализацию, включая выставку timestamp'ов, выполнять в CxsdHwSetDb().
    • Поскольку "растить" binbuf надо будет прямо поэлементно (а не добавлением сразу готовых строк), то:
      1. Никакой проверки на дублирование, увы, не будет.

        ...хотя можно выполнять её в момент "закрытия" добавления?

      2. API придётся делать хитрым: a) "открыть" процесс добавления (заодно получив offset?); б) "добавить" такой-то кусочек данных (тут cxsd_db будет вести эккаунтинг аллокированного и использованного); в) "закрыть".
      3. В момент "открытия" надо будет указывать кратность -- чтобы и требования выравнивания соблюдались, и не тратить впустую место (если, например, подрят туча значений по 1 байту -- не делать же им всем padding до кратного 16).
    • Соображения по структуре "хранилища":
      1. Можно, конечно, поступить совсем примитивно -- хранить бинарные данные друг за дружкой (с выравниванием при начале добавления).
      2. А можно каждый блок предварять "заголовочком" из длины в байтах и размера выравнивания.

      И вот во втором случае вполне можно реализовать и проверку на дублирование. Да и вообще наличие структуры -- как-то полезнее.

      ...но будут накладные расходы -- особенно заметные при хранении небольших значений (хотя вряд ли тех будет много).

    В общем, КАК делать -- примерно понятно, чистое ремесленничество и никаких принципиальных проблем.

    Вопрос в том, БУДЕМ ли; прямо сейчас -- как-то неохота, лучше подождать, пока не возникнет ещё какая-нибудь аналогичная потребность, чтоб сформировала более общее направление.

    15.04.2022: потенциально этот "binbuf" может иметь более широкое применение: фактически это аналог "StrDB" (strbuf), но для бинарных данных ПРОИЗВОЛЬНОГО размера. Где это может пригодиться -- покамест хбз, но концептуально оно вот оно.

    21.04.2022: всё-таки сделаем -- это ж в первую очередь просто инфраструктура, а сейчас представление в голове очень чёткое, так что времени уйдёт немного.

    • Собственно "хранилище": в CxsdDbInfo_t добавлены поля binbuf, binbuf_used, binbuf_allocd.

      И освобождение буфера в CxsdDbDestroy().

    • API с именами CxsdDbAddBin*() заложен такой:
      1. Наполнение:
        • CxsdDbAddBinStart() -- создание нового блока.
        • CxsdDbAddBinAddSeg() -- добавление кусочка данных.
        • CxsdDbAddBinFinish() -- завершение блока; возвращает его offset.
        • CxsdDbAddBinCancel() -- на случай "если передумалось", просто удаляет весь текущий блок.
      2. Добыча:
        • CxsdDbGetBin() -- отдаёт указатель на данные, плюс (опционально) их размер в байтах и dtype.

    21.04.2022@лесок между Пирогова-26 и стадионом НГУ, ~16:30: кстати, вместе с проектом "процессеров" (начало обсуждения 30-03-2021) эта фича "указание значений по умолчанию" (в дополнение ко всем прочим свойствам) -- движение в сторону "a-la EPICS", позволяющее как-то имитировать работу тамошней "БД".

    27.04.2022@Николаева, около (не доходя) Николаева-8 (ГИпроНИИ), по пути за обновлённой ОСАГО на Kei, ~10:00: а можно ввести некоторую оптимизацию, для упрощения использования и для улучшения менеджмента памяти:

    • В CxsdDbAddBinStart() передавать ещё и
      1. Опциональный предполагаемый ОБЩИЙ размер -- чтоб оно могло аллокировать сразу, в один приём.
      2. Опциональный дуплет "первый сегмент" (size,len) -- чтоб можно было прямо при создании блока сделать ему наполнение.

    Это позволит для "простых" случаев (единичное значение) обходиться парой CxsdDbAddBinStart(),CxsdDbAddBinFinish(), БЕЗ промежуточного CxsdDbAddBinAddSeg().

    28.04.2022@Николаева, около (не доходя) Николаева-8 (ГИпроНИИ), по пути в Быстроном за паштетом из гусиной печени, ~11:00: да -- пусть каждый блок данных префиксируется небольшим заголовком, состоящим из размера и dtype. Причины:

    1. Это НЕОБХОДИМО для возможности отдачи из CxsdDbGetBin() размера и dtype.
    2. Так можно проверять на дублирование (в момент CxsdDbAddBinFinish() -- проходиться по всему binbuf'у и при найденности возвращать уже имеющееся, а текущий создаваемый инактивировать).

    01.05.2022: после размышлений и обмозговываний (в течение нескольких дней) наконец-то сделано:

    • Общая модель:
      • Выбрана модель "перед каждым блоком данных идёт заголовок".
      • Сами ofs'ы "указывают" на ДАННЫЕ, а не на заголовок.

        По аналогии с работой malloc().

      • Бонус такого подхода -- ofs=0 считается "невалидным"; т.е., начального значения 0 достаточно, дополнительная инициализация не требуется.
      • Для преобразования ofs'а в указатель служит static-inline-функция binofs2hdr().
    • О выравнивании:
      • Заголовок определяется типом CxsdDbBinHdr_t, представляющим из себя union, вторым полем содержащий uint8[16] -- это гарантирует выравнивание для любых типов данных вплоть до int128/float128.
      • При "открытии" нового блока CxsdDbAddBinStart() также выполняет padding предыдущего таким образом, чтобы новое начиналось с кратного 16.
      • Ну а растёт буфер инкрементами, кратными 1024 (как и 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, что
      1. На 64-битной архитектуре, вследствие соответственно 64- и 32-битности этих вещей может привести к проблеме после превышения 2ГБ-объёма.
      2. И даже при sizeof(size_t)==sizeof(int) может приводить к переполнению при заказе больших объёмов.

      Поэтому

      • Добавлена проверка, что при превышении границы 2ГБ (точнее, INT_MAX) выдаётся ошибка 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".

    • Решено, что всё-таки будем поддерживать парсинг ВЕКТОРОВ -- списков чисел, через запятую, числом не более, чем max_nelems канала.
    • В ParseSomeProps() добавлен параметр max_nelems -- для того, чтобы можно было
    • ...а для дОбычи значения max_nelems из changroups[] слегка модифицирован код ParseChanList(); из ParseCpointProps() же передаётся константа 0.

    04.05.2022: работа над парсингом:

    • Сделан цикл, со счётчиком nelems, с ограничением на количество элементов.

      Если после парсинга числа следующий символ, подсмотренный через PeekCh() -- запятая, то этот символ съедается и цикл продолжается, иначе -- break.

    • Собственно парсинг скопирован из альтернативы SFT_ANY/SFT_RANGE, но
      1. Все ещё имеющиеся в том #if'ы на тему MAY_USE_PPF4TD_GET_DOUBLE убраны (а то от них в глазах рябит и код диковатый; надо бы и там тоже убрать).
      2. Обращение с результатом парсинга переделано по образу const_drv.c::const_init_d(): результат складируется в соответствующее dtype'у поле в val и на него ставится указатель val_p -- для CxsdDbAddBinAddSeg().

      Замечание: тут НЕТ полной совместимости с console_cda_util.c::ParseDatarefVal() -- НЕ поддерживается вариант со списком в фигурных скобках "{ЗНАЧЕНИЕ,ЗНАЧЕНИЕ,...}". Просто не стал возиться.

      06.05.2022@утро-завтрак: следствие -- невозможно указывать векторы длиной 0 элементов...

      08.05.2022: заморочился-таки с возможностью указывать список в фигурных скобках, так что теперь и векторы длиной 0 элементов работают.

    • Чисто на будущее сделана заготовка-альтернатива под REPR_TEXT.

    Итого -- базовая инфраструктура парсинга вроде сделана, надо реализовывать её использование.

    • Добавлено поле CxsdDbDcPrInfo_t.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'ы каналов, которым указаны умолчательные значения":

    • Чтобы не лазить для каждого канала в db/dcpr, надо прямо в cxsd_hw_chan_t иметь флаг "has_defval".
    • А чтобы не вводить очередное uint8-поле, надо просто часть уже существующих преобразовать в наборы БИТОВЫХ флагов.

      ...конкретно сейчас на эту роль напрашивается 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++;
      
      где "rw" и "rw_readonly" стоят точно друг под дружкой, так что получается очень красиво и наглядно (не говоря уж о том, что там арифметика (булевская) перевод, которой на битовые маски сделал бы всё громоздким и уродливым).
    • Поэтому введено новое uint32-поле cxsd_hw_chan_t.bhvr и маска CXSD_HW_CHAN_BHVR_HAS_DEFVAL=1<<0.

    06.05.2022: реально делаем...

    • CxsdHwSetDb():
      • D самое начало добавлено "вычисление" 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) брало уже из следующей ячейки.
    • Парсинг SFT_FLAG был не реализован. После реализации (тривиальной -- "*((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: разбираемся.

    • Причина "аж по 4 раза отрабатывается вычитывание/активация свойств канала" найдена: цикл по namespace'ам почему-то был засунут внутрь цикла по changroups[], хотя должен был идти отдельно после него.

      В результате оно делалось столько раз, сколько у устройства changroups[]'ов. Так-то несмертельно -- просто повторяло статические действия (копирование, БЕЗ всякой аллокации), но смысла никакого.

      Исправлено -- цикл по nsp_id вытащен наружу из цикла по changroups[].

    • Сделана возможность указывать флаги без ':' в конце.

    07.05.2022@около мыши и ИПА, по дороге домой из Ярче, ~18:45: некоторые общие соображения на тему парсинга строк:

    • Надо бы сделать "правильный" парсинг шестнадцатирички \xNN, \uNNNN, \UNNNNNNNN и восмирички \NNN -- чтоб оно не требовало точно столько цифр, а позволяло бы меньше.

      ...и console_cda_util.c::ParseOneChar() тоже касается.

    • А не реализовать ли в ppf4td парсинг не только 8-битных строк, но и 16- и 32-битных? ppf4td_get_string16() и ppf4td_get_string32()?

      ...да только ТУТ нам это не поможет: тут ведь нет никакого буфера, из-за отсутствия верхней границы размера, а должно парситься по 1 символу, со складыванием посредством CxsdDbAddBinAddSeg().

    • А вот поддержку работы с 16-битными строками в ParseSomeProps() сделать можно: вообще реализовать единую ветку для REPR_TEXT, а в ней вместо 2 альтернатив по размеру сделать все 3.
    • ...но общую поддержку в CXv4 16-битных строк (CXDTYPE_U16TEXT?) делать как-то неохота.

      Как минимум -- хбз, какую букву для такого типа использовать. Это же "BMP" (Basic Multilingual Plane)? Просто 'b' нельзя -- байт; 'm'?

      Кому сильно надо -- могут UINT16 пользоваться, оно совместимо.

    • Хотя в console_cda_util.c::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)

    • Потом вылезла другая проблема: символ '#' внутри строки стал считаться ошибкой: он считается за EOL, т.к. внутри NextCh() есть проверка на IsAtEOL().

      Пришлось -- ради конкретно этого места -- городить:

      1. IsAtEOL_IGNHASH(), с убранной проверкой на '#'.
      2. NextCh_IGNHASH(), использующий оного.

      Ну и взятие очередного символа как потенциального следующего для строки переведено на NextCh_IGNHASH().

    • ...далее выяснилось, что в НЕзакавыченных строках прокатывают незаквоченные символы кавычки и апострофа.

      Исправлено путём дополнительной проверки -- оные символы разрешаются только в случае, если в qc записан "другой" (одиночные разрешены в двойных, а двойные в одиночных):

      if ((ch == '\''  &&  qc != '\"')  ||
          (ch == '\"'  &&  qc != '\''))
          return BARK("unquoted <%c> character in \"%s\"", ch, table[idx].name);
      
    • Также проверена работа "не-дублирования": при одинаковых defval'ах (у каналов одинаковых типов!) получаются одинаковые bofs'ы -- последующим выдаётся первый.

    Итого -- можно считать задачу решённой.

    Но есть некоторые наблюдения общего характера:

    • Ох и монструозная же вышла ParseSomeProps()!
    • И общее впечатление, чисто с точки зрения эстетики -- жутковатый и некрасивый код получился.

      Всё-таки такой "штучный" парсинг для частных целей -- зло, ибо выходит какой-то набор кривых самоделок/хаков, крайне плохо годный для каких-либо будущих модификаций.

      Ну ОЧЕНЬ напрашивается на переделку.

    13.05.2022: отладочная печать удалена, так что можно пускать в дело.

  • 04.05.2024@утро-зарядка и потом утро-душ: раз понадобится делать "модификации" devtype'ов -- например, cdac20 с writable-каналами АЦП, чтоб в них можно было писАть из sim_dir_drv, то не реализовать ли НАСЛЕДОВАНИЕ типов --
    • чтоб можно было указать "возьми список имён от такого-то устройства", ...
    • ...с возможностью дополнить (раз уж "наследование"), ...
    • но ИНФО_ПО_КАНАЛАМ определим иначе"?

    Точнее, это по факту копировние namespace'ов.

    04.05.2024@утро-зарядка и потом утро-душ: некоторые соображения:

    • Синтаксис -- видимо,
      devtype ТИП_УСТРОЙСТВА:ТИП_ПРЕДОК ИНФО_ПО_КАНАЛАМ
      или
      devtype ТИП_УСТРОЙСТВА(ТИП_ПРЕДОК) ИНФО_ПО_КАНАЛАМ
      (второй вариант позволяет в скобках указывать ещё какие-нибудь ключевые слова перед ТИП_ПРЕДОК).
    • Нужно будет обязательно проверять "совместимость" типов:
      • по минимуму, чтобы у "наследника" число каналов было не меньше, чем у "предка" (т.к. в namespace могут быть ссылки хоть на последний канал "предка"), если не так -- то это ошибка;
      • желательно также идентичность типов (REPR и SIZE) тех каналов "наследника", что есть и в "предке" (вот r/w -- пусть меняется), тут несовпадение -- скорее предмет для WARNING'а.
    • А поскольку в момент парсинга имени "предка" строки ИНФО_ПО_КАНАЛАМ ещё нет (т.к. она идёт позже), то придётся просто запоминать это имя как строку и ID типа (при трансляции в ID и проверить наличие).
    • ...и понадобится операция "CxsdDbNspCopyLines()", которой передаётся ID типа-"предка" (откуда взять список, т.е., всё, что в группе "Namespace-related") и она по сути делает malloc() с последующим memcpy().

      Т.к. содержимое строк списка -- CxsdDbDcLine_t -- просто ID строк и dcpr'ов, то такое тупое копирование абсолютно корректно и безопасно.

    • 05.05.2024@утро, просыпание: в перспективе можно и "CxsdDbNspAddLines()" -- чтобы не 1-в-1 копию "предка" делать, а добавлять его namespace; это позволит сливать вместе НЕСКОЛЬКО namespace'ов от потенциально НЕСКОЛЬКИХ предков.

      Правда, не вполне ясно, где и как такое "псевдо-множественное наслодование" может пригодиться: разве что изначально изготавливать предков как непересекающиеся сегменты, из которых как из "кубиков" составлять некие наборы; ведь, в отличие от языков ООП, тут каждая строка/имя имеет фиксированное маппирование на номер канала. ...и, кстати, при таком "добавлении" появится проблема перекрытия номеров, которую не особо ясно, как решать.

      Так что, ПОКА сделаем именно простое копирование.

    По сути -- готовый проект для несложной реализации.

    08.05.2024: а ведь вместо "наследования" достаточно было бы просто писать в ИНФО_ПО_КАНАЛАМ конкретную нужную информацию, вместо '~' -- благо, синтаксис это позволяет. Другое дело, что это не слишком-то удобно да и противоречит принципу "не плоди сущности" -- любое изменение в базовом devtype потребует модифицировать и все такие точки с "вручную изменённой спецификацией".

    05.05.2024: делаем, начиная от подстилающей функциональности.

    • CxsdDbNspCopyLines() сделана, но именно как ДОБАВЛЕНИЕ: она при надобности до-аллокирует нужный объём -- кратно ALLOC_INC -- и затем копирует в свободное место.

      Просто так оказалось проще, т.к. "items[]" -- это не указатель на массив строк, а кусок CxsdDbDcNsp_t, идущий в конце (объявленный как items[0]), так что всё равно не аллокировать с нуля известным количеством, а ре-аллокировать, поэтому сделано копированием CxsdDbNspAddL()'я с очевидным расширением.

    • Собственно основная работа:
      • Парсинг: если после имени типа идёт ':', то выпарсиваем следующее за ним имя, ...
      • ...и ищем ранее определённый namespace с таким именем (ругаясь при отсутствии).
      • Далее после парсинга ИНФО_ПО_КАНАЛАМ делается простейшая проверка, что количество каналов в "потомке" не меньше, чем в предке.

        (С по-канальными (или хотя бы по-группными) проверками решено не заморачиваться.)

      • А после аллокирования нового namespace'а ему делается CxsdDbNspCopyLines().
    • 06.05.2024: Также понадобилось сделать некоторый рефакторинг кода, чтобы была возможность писать "пустые" devtype'ы как "{}", не требуя закрывающей скобки на отдельной строке.

      Сначала всё обдумывал, как бы этот рефакторинг сделать: проблема в том, что там 1) сначала этот парсинг '{'+EOL+NL, 2) затем аллокирование и регистрация namespace (и добавление строк из parent'а там же), 3) и лишь потом парсинг списка каналов ParseChanList()'ом. Но так-то пункты 1 и 3 при '}'-сразу являются как бы единой сущностью -- вот и думал о "рефакторинге". А потом -- ...

      06.05.2024@~09:55, рядом с мышью по дороге в ИЯФ: есть вариант проще -- НЕ перескакивать на следующую строку после '{' и НЕ пропускать '}', и тогда контекст парсинга просто останется перед закрывающей скобкой и ParseChanList() преспокойно "отработает", завершившись на первом же символе.

      Так и сделал -- пусть с небольшим "рефакторингом" собственно парсинга '}'-или-EOL. Работает!

    Проверено на "живом" simkoz-симулируемом устройстве, чей тип "отнаследован" от cac208, но каналы входного регистра помечены как "w" вместо "r" -- да, работает как положено (запись в эти каналы стала возможно и меняются они как надо в схеме "8+1").

cxsd_hw:
  • 21.01.2010: модуль, хранящий свойства "текущего описания аппаратуры, используемого сервером". Также предоставляется API для заполнения этих вещей из CxsdDb.

    Сюда уходят вещи, в v2'шном cx-server'е жившие в cx-server_data.[ch] в массивах c_*[], b_*[] и d_*[].

    Используется префикс CxsdHw.

  • 17.11.2013: в свете возможного переезда драйверного API каналов из cxsd_driver.c в другой модуль возникла потребность опубликовать макросы-проверяльщики *CHECK_*SANITY*().

    Натуральным местом выглядит cxsd_hw.h -- что и сделано, рядышком с {ENTER,LEAVE}_DRIVER_S() (которые, кстати, не надо ль привести к виду свежих remcxsd'шных?).

  • 17.11.2013: кстати, а действительно -- не пора ли "скобки" ENTER_DRIVER_S() и LEAVE_DRIVER_S() об-умнить?
  • 17.11.2013: была мыслишка выделить часть деятельности в еще один модуль -- cxsd_data; и он даже был создан, но потом, по некоторому размышлению, решено, что не стоит -- уж слишком неочевидно разделение. Ниже та запись с мотивировкой, уже "withdrawn".

    17.11.2013: создаём этот модуль. Предназначение -- "ядро" работы с данными; то, что в v2 жило в cxsd_channels.c и cxsd_bigc.c.

    Префикс, очевидным образом, CxsdData.

    Возможно, оно должно б быть единым целым с cxsd_hw.[ch] (разделить области ответственности нетривиально), но пока, ради разделения обязанностей, делаем так:

    • cxsd_hw -- "конструктор", заведует наполнением HW-БД.
    • cxsd_data -- "жизнедеятельность", т.е. всё остальное.

    ...а еще тут же рядышком cxsd_driver...

    17.11.2013: уж проще пусть всё, связанное с каналами/железом/данными (т.е. -- "функционирование «динамической БД»") живёт совместно в одном модуле.

  • 17.11.2013: восстанавливаем/создаём "циклы" сервера.

    17.11.2013: резоны:

    1. Возможность клиентам запрашивать данные "присылай раз в цикл".
    2. Отправка драйверам запросов на чтение не чаще раза в цикл.

    Вообще, конечно, для обеих целей подход небезупречен.

    1. Сама "циклическая работа" под некоторым вопросом. И как реально данные нужны клиентам (09-11-2006), и насколько адекватно иметь такое ОДНО выделенное событие, а не расширябельную инфраструктуру (23-08-2008).
    2. Разумная "частота" отправки запросов чтения в принципе может различаться у разных устройств, и единый (общий для всех) период не факт, что всегда будет хорош.

    Короче -- надо снова и снова переобдумывать мысли из разделов "Общая схема работы" и "ВременнАя схема работы".

    17.11.2013: пока же делаем в простейшем варианте, чтоб было хоть как-то (и чтоб ядро сервера годилось для использования в v2).

    • Сразу рассчитываем на возможность изменения длительности at-run-time.
    • Кроме того, оно теперь не только старается держать точно заданный период (см. bigfile.html/11-06-2003, но "адаптируется", допуская скачок времени вперёд.

      Для этого проверяется, что если следующий желаемый момент окончания цикла -- 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():

    1. Меняем схему "обнаружения лага": теперь оно старается скакнуть к текущему моменту не сразу, а только если "желаемый момент окончания цикла ... УЖЕ в прошлом" 10 раз подряд.

      Смысл -- чтоб при слегка загруженном процессоре оно всё-таки ПОПЫТАЛОСЬ бы пройти через все циклы. Один-два пропуска (мало ли -- дисковая активность) будут терпимы, а через десяток всё же перескочит.

      Если же такое адаптивное поведение будет нежелательно -- достаточно обнулить cycle_pass_count_lim.

    2. ...А вот с обнаружением скачка времени назад как-то не сложилось...

      Причина -- при переводе времени назад до 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: добавлена архитектура callback'ов (evproc'ев) на каналы, необходимая для функционирования inserver:: и frontend'ов.

    03.08.2014: идеология: реализуется оно всё в cxsd_hw, а "юзерам" даётся интерфейс, состоящий из

    1. CxsdHwAddChanEvproc() и CxsdHwDelChanEvproc() -- функционально аналогичные cda'шным cda_add_dataref_evproc() и cda_del_dataref_evproc().
    2. CxsdHwCallChanEvprocs() -- вызов evproc'ев для указанного канала; дергается из ReturnDataSet().

      29.01.2015: поскольку единственным юзером был ReturnDataSet(), переехавший из cxsd_driver в cxsd_hw 31-10-2014, то CxsdHwCallChanEvprocs() убрана из публичного интерфейса и сделана приватной.

    Заметки по деталям реализации:

    • Вид evproc'ев определяется типом 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'ев всех каналов -- вообще безусловная.
    • Вопрос с тем, как должны поступать frontend'ы при смене конфига ("статической БД") пока остаётся открытым. Вроде они должны б сами "забыть" об уставленности evproc'ев...

    07.08.2014@пляж: да ясно, что делать: слать им уведомления, в 2 этапа, stage=0 перед удалением старой и stage=1 после активации новой.

    Понятно, что понадобится добавить еще один метод -- "DB change".

    А вообще, кстати, evproc'ы же -- и через них можно слать сообщения. В частности, надо будет ввести CXSD_HW_CHAN_R_PARAMSCHG, присылаемый при смене драйвером {r,d} и q (они могут "на лету" меняться только у физического канала, т.е. -- последний дуплет в цепочке {r,d}).

  • 25.09.2014: вводим тип cxsd_chanid_t -- для адресации каналов, вместо безликого int.

    29.09.2014: дополнительно еще cxsd_cpntid_t -- для ссылания на "точку контроля" (03-03-2013 -- «идент "точки контроля"»).

    • Оно должно возвращаться из CxsdHwResolveChan() и должно быть достаточно для ссылания на эту точку так же, как по имени -- чтоб в будущем, когда какие-то из свойств ({r,d}?) меняются, то не повторять резолвинг, а повторить лишь добычу. 10.01.2015: да, заюзано в cda_d_insrv.c, через CxsdHwGetCpnProps().
    • Скорее всего, это должен быть просто некий порядковый номер строки "в базе" -- в таком же списке, какой будет отдаваться резолверу, висящему на 8012/udp.
  • 09.10.2014@Снежинск-автобус-из-каземата-на-обед: в принципе, желающие могут доступаться к текущим данным в каналах напрямую, но -- может, для унификации и упрощения юзеров CxsdHw стоит сделать функцию, правильно ограничивающая вычитывание векторных данных, аналогично v2'шной inserver_get_bigc_data()?
  • 24.10.2014: делаем 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() уже делает полную добычу свойств. Так что считаем раздел исполненным.

  • 24.10.2014: вводим 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[], а она уж должна передать методу ЛОКАЛЬНЫЕ адреса (в рамках устройства).

    • ...а в v2 указывался последовательный блок -- там была пара {first,count}, никаких массивов.
    • и в EPICS'е тоже всё просто -- там вообще обработка поштучная, никаких групповых запросов и нет.

    И что делать? Мыслей разных за последнее время было море.

    1. Использовать прямо массив globalchans[], перед вызовом вычитая из всех элементов dev->first, а потом прибавляя обратно.

      Плохая идея -- мало того, что лазить в чужую память нехорошо, так еще и толпа потенциальных проблем (в т.ч. вследствие того, что все вызовы могут быть рекурсивными).

    2. Аллокировать при создании БД каждому устройству массивчик размером в count элементов -- чтоб любой запрос уж точно влез.

      Тоже плохая идея. Например, по той же причине возможной рекурсии: из вызванного метода драйвера какой-то канал тут же будет вёрнут, что вызовет еще отправку, например, вследствие взведённого к тому моменту next_wr_val_pnd. (Возможны и более хитрые пути, вроде дёрганья драйвером этого устройства каналов другого, которые, сразу вернув значения, могут привести к вызову callback'ов (при прямом использовании API "ChanEvproc", а не cda-insrv), которые как-то активируют опять вызовы из этого устройства.)

      Кстати: а что, если некий канал, ранее незапрошенный, в одном запросе встретится несколько раз -- да хоть тысячу? Ведь логика "ConsiderRequest()" все эти разы будет его считать "годящимся для отсылки", поскольку rd_req/wr_req всё еще будут ==0. И так вполне может оказаться, что не только запрос на один канал может сбагриться драйверу НЕСКОЛЬКО раз до ответа, но и само количество каналов в запросе превысит количество каналов устройства.

    3. Естественно, просто один статический массив после аргументов из предыдущего пункта даже не рассматривался.
    4. Короче, на настоящий момент наиболее разумным выглядит компромиссный вариант:
      • Иметь в функции АВТОМАТИЧЕСКИЙ массив на 1000 элементов, с которым обращаться по принципу "flush" -- если надо сделать посылку больше его объёма, то она разбивается на сегменты по 1000.
      • Да, это вводит некоторое ограничение, но вряд ли оно сильно страшнО:
        1. При параметризованных запросах (будущие PZREAD/PZWRITE) вряд ли понадобится больше 999 параметров (сейчас-то в v2 ограничение 100, и об него сильно не бъёмся).
        2. Для обычных, не-параметризованных запросов вообще без разницы, сколько слать в пачке -- это вопрос лишь оптимизации (в том же v2 по факту всегда шлётся по штучке, поскольку cda генерит запросы SET, а не GROUP, обрабатывающиеся последовательно по-канально).

    30.10.2014: делаем.

    • Мозги сделаны по аналогии с v2, даже функции-helper'ы те же, 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".
    • Сделана v2'шная семантика запросы на "чтение каждого канала делаются не чаще раза в цикл":
      • В 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.
    • НЕ СДЕЛАНА пока поддержка "next_wr_val". Точнее, отсутствует складирование в next_wr_val,next_wr_nelems.

      Причина -- идеологическая:

      • Сейчас при "записи сразу" передаются прямо те dtypes,nelems,values, что дадены CxsdHwDoIO().
      • А в next_wr_val-то места выделено ровно nelems*usize. Так что перед складированием надо делать конверсию.
      • Но по-хорошему надо ВСЕГДА делать конверсию в dtypes. Как раз в next_wr_val, и чтоб 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: первая попытка проверки. Облом -- не пашет.

    Длительные раскопки с толпой отладочной печати обнаружили огрехи в идеологии:

    1. Предполагается, что ConsiderRequest(), вызываемый для расширения набора (т.е., для проверки, совпадает ли следующий канал по "виду" с предыдущим), сам же
      • и вызовет складирование/конверсию данных,
      • и уставит флажок rd_req/wr_req.

      Но это неправильно: очередной оцениваемый канал может оказаться другого "вида", и к нему НЕ будет применено то действие, и в результате он останется в "зависшем" состоянии -- фоаг взведён, а запрос не будет отправлен (поскольку он будет первым в следующей группе, и сразу DRVA_IGNORE).

      Что делать? Варианты:

      1. Разнести оценивание и действия.

        Развесисто и трудоёмко будет, хотя и максимально ортогонально (читай -- гибко).

      2. Передавать Consider'у еще и f_act, и чтобы действия делались не только при may_act, но еще и &&f_act==DRVA_NNN, где NNN -- возвращаемое данной веткой условия значение.
    2. С другой стороны, для самого первого канала в группе -- используемого для получения f_act -- Consider вызывается с may_act=0, поэтому ему ни данные не будут складированы/сохранены, ни флаги взведены.

      Но это-то исправить просто: достаточно не делать 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" делать. В виде:

    1. Функции TryWrNext().
    2. Её вызова.

    30.11.2014: за вчера-сегодня сделано.

    • Логика работы чуть отличается от v2'шной: вместо двух-шагового
      1. Skip non-next_p channels.
      2. Find how many channels can be packed.
      она действует по тому же алгоритму, что и DoIO и ReRequest:
      1. Get "model" parameters.
      2. Find how many channels can be packed.
      т.е., не-требующие записи следующего значения каналы пропускаются всей группой (вместе с возможными невалидными).

      "Параметры" определяются проверятором ShouldWrNext().

      Причина изменения -- тут проверка позаковыристей, из-за addrs[] вместо first,count.

    • Проверено -- ОНО вроде работает как надо.
    • Вопрос теперь в GUI: можно ли сделать, чтобы числа в ручках не обновлялись до прихода ответа?

      Хотя в v2 такого НЕ сделано (та же отсрочка на 1 шаг), но там и timestamp'ов нет, а тут есть...

  • 28.10.2014: реализуем симуляцию -- HandleSimulatedHardware().

    28.10.2014: 3 аспекта:

    1. Собственно HandleSimulatedHardware(): сделана немудряще, цикл по всем устройствам внутри которого цикл по каналам устройства с вызовом StdSimulated_rw_p().
    2. Ну и дёрганье её из EndOfCycle().
    3. Но еще модификация в CxsdHwActivate(): при MustSimulateHardware оно просто делает state=DEVSTATE_OPERATING, не только ничего не проверяя, но даже не пытаясь загрузить модуль драйвера.
      • С одной стороны, это работает, и сделать было просто и быстро.
      • С другой, кривовато, и отличается от того, что делается с layer'ами.
      • А по-хорошему надо всё делать "как надо":
        1. В любом случае пытаться грузить модуль, но не принимать решение о дальнейших действиях прямо по результату, а сохранять результат "ok/error".
        2. Затем, если MustSimulateHardware, то просто делать "всё окей", а иначе если "загрузилось ok", то пытаться инициализировать модуль.
        3. Затем, если НЕ MustSimulateHardware и проблема либо загрузки, либо инициализации, то помечать все каналы как "CXRF_NO_DRV" (касается только драйверов).

    10.04.2015: немудрящесть вылезла немного боком: в режиме симуляции каналы записи ведут себя иначе, чем при обычной работе -- они ЕЖЕЦИКЛЕННО "обновляются". Строго говоря, это не совсем хорошо -- лучше бы, как в v2, симулировать чтение только не-rw-каналов.

    23.04.2015: исправляем "немудрящесть", чтобы всё было корректно и даже в режиме симуляции вело б себя как при обычных драйверах.

    • В HandleSimulatedHardware() делано, что StdSimulated_rw_p(DRVA_READ) вызывается только для не-rw-каналов.
    • ...после чего чтение 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".

  • 31.10.2014: рано или поздно надо будет сделать и каналы _devstate.

    31.10.2014: примерный проект реализации:

    • Добавляется к основному списку каналов устройства ровно по штучке -- сам _devstate. Если еще что-то понадобится, то дальше.
    • Собственно добавление должно делаться в CxsdHwSetDb().
    • Для разделения "полного" количества каналов устройства и "обслуживаемого драйвером количества" надо будет ввести cxsd_hw_chan_t.drvchancount.
    • ...отдельный вопрос, как делать "return": с одной стороны, драйверам возвращение не-их каналов надо запретить, а с другой, сам процесс обновления (вместе с вызовами evproc'ев) производится именно в ReturnDataSet(), так что надо вызывать её. И как?

      Ввести внутреннюю глобальную переменную, во взведённом состоянии "разрешающую" возврат каналов >=drvchancount? Но сразу после проверки её надо сбрасывать, чтоб не воспользовались вызываемые-по-evproc'ам.

      Это удобнее делать в самом начале, еще до цикла по count.

    • Поскольку отправлять этот канал в общем запросе с обычными крайне неудобно, надо ввести отдельный код DRVA_INTERNAL, который будет возвращаться Consider'ом для каналов с номерами >=drvchancount.

      ...каковому ConsiderRequest()'у надо будет добавить в список параметров и dev. Кстати, и SendChanRequest()'у тоже.

      28.05.2015: не-а! Для определения (отделения от прочих) достаточно в cxsd_hw_chan_t добавить флаг is_internal. А отправлять вообще ничего никуда не надо -- само вернётся при изменении; так что и DRVA_INTERNAL не нужен, достаточно DRVA_IGNORE.

    • А уж Send пусть отдельно смотрит, что action==DRVA_INTERNAL, и сам возвращает статус.

      28.05.2015: а ЗАЧЕМ? Обновление и так будет делаться по мере надобности, а "текущее" вычитывается cda_d_cx'ом и так. (Проект был написан в октябре-2014, еще ДО реализации PEEK, вот и присутствуют в нём ныне ненужные детали.)

    • SetDevState() тоже сделает ReturnDataSet(,,[0]=dev->drvchancount+OFS_OF_DEVSTATE,).
    • Резолвинг имён вида DEVNAME._devstate легко сделать и прямо в нынешней хаковатой CxsdHwResolveChan().

    30.03.2015: кроме канала _devstate завести б еще канал "_devstate_description" -- со строковым описанием, которое передаётся в SetDevState().

    28.05.2015: делаем:

    1. Дополнительные поля:
      • CxsdDbDevLine_t: для полного количества заведено поле wauxcount (With AUX).
      • cxsd_hw_dev_t.wauxcount -- аналогично.
      • В cxsd_hw_chan_t добавлен is_internal (взамен res1).

        ConsiderRequest() на каналы с ним сразу возвращает DRVA_IGNORE.

        После обеда: а ведь, учитывая, что _devstate* !rw, достаточно было пользоваться флагом is_autoupdated...

    2. Добавление пары искусственных каналов в БД:
      1. db:
        • CxsdDbCreate(): заполнение у [0]-го поля wauxcount и использование уже ЕГО в подсчёте db->numchans.
        • CxsdDbAddDev(): аналогично.
      2. hw: CxsdHwSetDb():
        • "Вручную" заполняются свойства пары искусственных каналов.
        • Так же вручную добавляется количество.

        Вообще-то напрашивается сделать функцию-подмастерье "заполнить свойства канала", и вызывать её изо всех 3 точек.

    3. Резолвинг -- в CxsdDbResolveName():
      • В самом начале проверяется, не является ли последний компонент имени "специальным", и если да, то выставляется значение переменной finding_special -- =1 для _devstate и =2 для _devstate_description.
      • Дальше есть отдельные блоки проверки в виртуальной (LINK_TO_DEV._devstate*) и реальной (DEVNAME._devstate*) иерархиях.

        В обоих случаях проверка на finding_special!=0 идёт ДО поиска канала по имени, поэтому можно указывать вещи вроде LINK_TO_DEV.CHANNAME._devstate* и "DEVNAME.CHANNAME._devstate*" (причём количество компонентов CHANNAME может быть произвольно большим).

      • И еще более отдельный кусок -- для ссылок вида LINK.TO.DEVCHN._devstate* и LINK.TO.CPOINT._devstate*. Там симлинки раскручиваются вплоть до конечной точки, и от еёшнего target-devid'а берётся искомый _devstate-канал.
    4. Собственно отдача значений:
      1. Внутренняя переменная, разрешающая возврат за границей dev->count -- ReturningInternal.
        • ReturnDataSet() считывает её в локальную в самом начале, и потом сбрасывает =0.
        • Проверка отключается ПОЛНОСТЬЮ -- никакое +2 не проверяется.
        • Кроме разрешения такого возврата, она еще и отключает проверки в -- начале SANITY_OF_DEVID (чтоб не мешало вертать OFFLINE -- свежескопытившимся) и count (чтоб работало даже с девайсами с нулём каналов).

        03.08.2015: условие !internal перед проверкой на [0,dev->count) стояло только при сохранении данных, а при вызове evproc'ев по R_UPDATE -- нет, в результате обновления каналов _devstate* подписчиками не виделись (вылезло на vdev-драйверах, которым эти каналы критичны). Исправлено.

      2. SetDevState(): его подмастерья NnnnnDev() пользуются отдельной функцией report_devstate(). Она вызывает ReturnDataSet() для 1 либо 2 каналов: собственно _devstate всегда, и _devstate_description при условии description!=NULL (т.е., description==NULL оставляет старое значение).

        Конкретно ReviveDev() форсит description="".

    Результаты:

    • Работает.
    • Функционирование несколько неудобно для отладки/диагностики: каналы де-факто работают в режиме "autoupdated", игнорируя запросы на чтение, но при этом обновлений в обычном смысле не происходит (т.к. устройства состояние почти никогда не меняют).

      В результате проверять cdaclient'ом просто не получается, поскольку значение всегда "когдатошнее", оно НЕ обновляется и НЕ TRUSTED (ибо и timestamp полезен), так что событие CDA_REF_R_UPDATE не генерится, а можно лишь получить текущее "последнее известное значение".

      Так что проверялось das-experiment'ом с ключом "-p500".

    • Оно, конечно, странновато выглядит для искусственных/pobox-каналов (на основе noop), которые генерятся mk_pobox_devlist.sh: по два совершенно избыточных канала на один полезный. Но что уж тут поделаешь: можно, конечно, вводить что-то для возможности их не-создания (например, ключик в device'овом options) но вряд ли оно того стоит.

    28.05.2015@вечер-ванна: а ведь проблему "начальное вычитывание не даёт обновлений" можно решить очень просто: в cxsd_fe_cx в ответ на CXC_PEEK отдавать CXC_NEWVAL вместо CXC_CURVAL при is_internal (аналогично rw).

    29.05.2015: так и поступаем:

    • ServeIORequest() на PEEK для internal-каналов отвечает NEWVAL.

      Помогло.

    • Дополнительно оптимизация: _devstate-каналам уставляется is_autoupdated и, поскольку его достаточно, то проверка на is_internal из ConsiderRequest() убрана.

      Таким образом, на is_internal фактически оставлена только роль "is_devstate".

    Засим задачу можно считать выполненной.

    20.04.2017: есть одно неудобство:

    • С нынешней схемой обработки запросов имён вида "*._devstate*" (через finding_special) нельзя создавать cpoint'ы с именами вида SOME_VIRTUAL_BRANCH._devstate -- оно пытается взять состояние от ветки "SOME_VIRTUAL_BRANCH", а поскольку таковая не существует, то происходит облом "нет такого канала".
    • А бывает надо -- когда создаётся полностью виртуальное устройство из cpoint'ов, долженствующее имитировать настоящее, то нужен и канал _devstate (для vdev-драйверов-юзеров).
      • Надобность вылезла при попытке сделать устройство linac1:11.Spectr (сумма из Spectr1+Spectr2) годным для натравливания iset_walker'а.
      • Само оно -- канал-уставка Iset -- делается double_iset'ом; канал "текущее значение" Iset_cur имитируется драйвером formula (как Spectr1.Iset_cur+Spectr2.Iset_cur) (формулой же можно и сам Iset сделать).
      • А вот необходимый _devstate можно бы сделать просто линком на состояние одного из двух входящих каналов, но подобные линки не работают ну никак.

        Единственный путь СЕЙЧАС -- создавать какое-то фейковое устройство (noop'ом), а чьему имени донавешивать требуемые имена, а в качестве состояния будет использоваться состояние этого фейка.

    • Явно надо давать возможность делать и линки с именами, заканчивающимися на "_devstate"; точнее -- делать-то их можно и сейчас, но вот чтоб работали...
      • Видимо, надо как-то проверять, что если даже если finding_special!=0 и подошли к последнему компоненту (last!=0), а предпоследний является просто CLEVEL'ом, то проверить наличие в этом CLEVEL'е элемента с нужным именем, и если он резолвабелен на канал, то вернуть его.

        ...сейчас попробовал порыться в CxsdDbResolveName() на эту тему -- весьма, весьма там неочевидно...

      • Будет у такого решения и минус -- получается "инверсия приоритетов": обычно виртуальные иерархии имеют приоритет и могут "затенять" настоящие каналы, а тут виртуальный канал будет использоваться лишь в случае, если реального такого канала нет.
    • Кстати, в идеале надо б было делать не линк на состояние одного из компонентов, а делать линк на ВЫЧИСЛЯЕМОЕ значение -- минимум из состояний всех участвующих. Можно делать так же формулой.

    Получасом позже:

    • Долго-долго вглядывался в код CxsdDbResolveName(), анализировал ход исполнения для разных случаев.

      На вид -- да оно просто должно работать!

      Вот просто код так устроен, что это должно автоматом давать нужный результат.

    • Проверил на простеньком примере -- да, оно работает!

      Вот же какой я умный, прекрасный код написал два года назад :D

    • Более сложный пример с walker'ом тоже работает (косяк с собственно значениями из-за {R}, но то уже другой вопрос).
  • 20.11.2014: наполняем свежепереехавшую сюда SetDevState() -- без неё уже тяжко.

    20.11.2014: детали:

    • Сама функция коротенькая, практически копия v2'шной, а собственно действия по переходу в состояния делаются отдельными функциями, доступными для прочего использования:
      1. TerminDev() -- самая длинная.
        • Она же вызывается из InitDevice() при любой неудаче.
        • Метод term_dev() она вызывает, если текущее состояние !=DEVSTATE_OFFLINE.

          Т.е., предполагается, что даже НЕДОинициализировавшиеся драйверы должны быть способны подчищать за собой.

        • Вызываются _cleanup'ы, но пока НЕ от cxlib и cda. Может, как-нибудь модуляризовать это -- отдать на откуп "юзеру" libcxsd, который уж знает, что в него влинковано? И дергать это надо ДО "базовых" fdio_do_cleanup() и sl_do_cleanup().

          29.01.2015: да, сделано: регистрируется через CxsdHwSetCleanup(), и вызывается ДО {fdio,sl}_do_cleanup()'ов. В stand.c оно уже заюзано, а в cxsd.c нет, поскольку там СЕЙЧАС никакие cxlib/cda не влинковываются.

      2. FreezeDev() -- самая простая.
      3. ReviveDev() --
        • сбрасывает флаги,

          27.06.2016: уже нет, НЕ сбрасывает (подробнее см. "Закомментировываем RstDevRflags()").

        • нулит timestampы, вызывая RstDevTimestamps(),

          27.06.2016: и timestamp'ы уже тоже не нулит, а только циклы.

        • дергает ReRequestDevData().
        • Поскольку она вызывается из InitDevice() при успешной инициализации драйверов, то обходится тонкий аспект "а если более ранний драйвер потребует данные от более позднего, еще не инициализированного" --
          1. при запросе будут ConsiderRequest()'ом выставлены флажки rd_req/wr_req, но SendChanRequest() запрос не будет отправлен, зато...
          2. ...при переходе устройства в готовность всё и отработается.
        • (Записано 15.01.2015) вызывает ReqRofWrChsOf() -- вычитывание rw-каналов.
    • НЕ сделан пока отдельный helper (и вызов его), возвращающий _devstate.

    21.11.2014: продолжение:

    • Надо ВЕЗДЕ при обращении к dev_p->metric->ЧТО_ТО проверять, что dev_p->metric!=NULL -- а оно может, при незагруженности драйвера.

      В тройку мест добавлено, но мож еще где осталось...

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

    Решение оказалось очень простым:

    1. Введён код CXSD_HW_CHAN_R_STATCHG=1 (единица как раз была зарезервирована, "для унификации с cda" -- где используется именно под STATCHG).
    2. Это событие генерится из SetDevRflags(), вызываемой по DEVSTATE_OFFLINE и DEVSTATE_NOTREADY.
    3. cxsd_fe_cx.c мониторит его, и по получению вызывает отправку значения с CXC_CURVAL...
    4. ...успешно передающееся от cda_d_cx в cda_core как "НЕ обновление".
    5. В результате при следующей перерисовке ручки меняют цвет нужным образом.

    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.

    • у cPCI это SERIAL, у CAN (кажется) HW_VER/SW_VER, а у vdev (ist_cdac20) -- ISET_CUR.
    • Т.е., во всех случаях это каналы, которым ставится IS_AUTOUPDATED*.

      Но никак не удавалось понять причину.

    • А сейчас, похоже, нашлась:
      • Ведь устройства "рождаются" как бы в NOTREADY, затем вызывается init_dev(), и уже потом -- возможно -- устройство "оживает".

        А иногда "оживает" даже позже -- в случае удалённых драйверов.

      • Но возврат значений проблемных каналов всегда делается в init_d(), еще ДО "return DEVSTATE_OPERATING".

      А переход в OPERATING -- это ReviveDev(), который делает RstDevTimestamps(), тем самым о-DEFUNCT'ивая значения. Вот и получается, что значения есть (и верные!), но посиневшие.

    • Что делать?
      • Первой идеей было "а пусть драйвер перед выдачей значений сделает SetDevState(,DEVSTATE_OPERATING)".

        Для CAN-устройств -- где инициализация реального драйвера происходит ПОСЛЕ завершения init_d() -- сработало бы.

        Для локальных -- вроде cPCI и vdev-based -- нет: InitDevice() вызывает прямо ReviveDev(), а не SetDevState(), и timestamp'ы точно так же сбросились бы.

      • Более разумным выглядит перенести RstDevTimestamps() из Revive во Freeze (ну и в InitDevice() тоже делать, чтоб изначально было "никогда").

        Но вместе со сбросом timestamp'ов также делается и RstDevRflags() -- а флаги-то у "уже вёрнутых" каналов тоже имеют значение (например, UNSUPPORTED), и их сбрасывать нельзя!

        Т.е., как бы не вытанцовывается и такой вариант тоже -- ведь сброс флагов перенести в Freeze нельзя (там, наоборот, что-то ставится).

      • С третьей стороны -- а чего это вообще делается RstDevRflags()? В v2 оно отсутствовало (точнее, вызов был закомментирован), флаги нулились только при старте сервера и потом в случае возврата нулей драйвером.

    26.06.2016@ванна: пара мыслей на тему:

    1. А зачем вообще сбрасывать timestamp'ы (хоть в Revive, хоть во Freeze)? Когда было вёрнуто -- тот timestamp пусть и стоит; только с самого начала, в InitDevice() прописать, чтоб были NEVER_READ.
    2. Другое дело -- 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: там вроде тоже решение намечается.

  • 20.11.2014: 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: сюда же, несколько задним числом:

    • 10.09.2019 при разбирательстве, почему это запись не-int32 в канал _devdstate не работает, возникла острая мысль:
      Плохо, что ВСЕГДА отправка выполняется через копирование в next_wr_val, даже если типы совпадают и не нужно откладывать -- когда можно было бы напрямую, для экономии (для больших объёмов данных критично).
    • А сегодня, ища следы/концы, дорылся до причины такого поведения: чтоб можно было повторить запись в ReRequestDevData() -- иначе откуда б ей брать данные для записи, если они НЕ будут сохранены в *next_wr_val.

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

    ...если только не захочется сделать этакую "опцию канала" со смыслом "не надо сохранять данные для записи; если пропадут при рестарте устройства -- то и фиг с ними".

  • 22.11.2014: надо б как-нибудь упорядочить процесс инициализации с точки зрения "когда что вызывать/делать или нет", с точки зрения наличия ошибок конфигурации/драйверов и флага MustSimulateHardware, а то сейчас оно пораскидано в разных местах и не особо-то согласовано.
  • 22.11.2014: надо организовать хранение description.

    29.05.2015: сделано вчера, в рамках реализации канала _devstate_description. Как бы не супер-корректно (т.к. НЕ в cxsd_hw_dev_t), но приемлемо -- хранимое значение легко добываемо (если понадобится, например, консоли).

  • 22.11.2014: пришла пора реализовать отдачу драйвером параметров пересчёта значения -- {r,d}.

    Без этого становится невозможно двигаться дальше.

    22.11.2014: первоначальная реализация

    • SetChanRDs().
    • Парадигма count,addrs[] -- для унификации с прочими...
    • ...но с ОДНИМ дуплетом {phys_r,phys_d}.

      Идея в том, что эти числа обычно одинаковы у толпы каналов (например, у многоканального АЦП), и организовывать передачу массива просто ни к чему.

      А где понадобятся разные значения для разных каналов -- там можно и несколько вызовов сделать, операция-то однократная (ну или редкая).

    Проверено на 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: и отдачу драйвером "возраста свежести" тоже надо делать.

    23.11.2014: начало:

    • Поле cxsd_hw_chan_t.fresh_msecs -- просто int, а не cx_time_t, для простоты; да и идеологически нет смысла в сильно длинных временах, наверное.
    • По умолчанию ставится 5000ms.
    • Проверена работающесть в cda, ...
    • ...для чего пришлось поменять в 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: далее:

    1. Решаем проблему "а с каналами ЗАПИСИ как будет?".
      • Основная идея -- да, sec==0 означает "хбз когда обновлялся, но считать свежим".
      • Само решение принимается в cda_dat_p_update_dataset().
      • Чтобы никогда-не-вёрнутые каналы не считались свежими, им изначально ставится sec=1, т.е. 1970-01-01_00:00:01.

        Оно определяется константой INITIAL_TIMESTAMP_SECS, используемой также в RstDevTimestamps() -- т.е., и по оживанию устройства делается ровно то же.

      • Алгоритм "для каналов записи использовать {0,0}, если они не запрошены на чтение/запись; иначе и для остальных всегда реальный timestamp" формализован в inline-функции CXSD_HW_TIMESTAMP_OF_CHAN() -- аналоге v2'шной FreshFlag().

        Но флаг MustCacheRFW игнорируется (в отличие от).

        А собственно нулевой timestamp -- он ведь возвращается по указателю -- лежит в cxsd_hw_zero_timestamp.

        17.07.2015: Вышеуказанное стало некорректно с перераспределением обязанностей значений секунд: ведь теперь 0=TRUSTED, а в качестве "никогда/бесконечно-давно" выступает 1=NEVER_READ.

        Поэтому переименовываем в и меняем значение с 0 на CX_TIME_SEC_NEVER_READ.

        ...кстати, CXSD_HW_TIMESTAMP_OF_CHAN() используется только в cda_d_insrv.c.

        03.08.2015: тогда, 17-07-2015, была сделана откровенная глупость:
        • Ведь смысл 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});

        Кроме того:

        1. Заиспользовано и в cxsd_fe_cx.c.
        2. ...в нём также был ляп -- в момент резолвинга канала возраст свежести НЕ отправлялся, в отличие от прочих свойств. Добавлен вызов PutFrAgChunkReply().
      • Проверено, работает как надо.
      • Проблема только в cda_d_insrv, который НЕ делает update_dataset каждый цикл, поэтому когда-то измеренные, но давно не обновлявшиеся данные НЕ синеют (ведь решение о синении принимается в cda_dat_p_update_dataset(), а не в момент перерисовки на экране).
      • ...с другой стороны -- а правильно ли сваливать такую работу на cda_d-плагины, ведь действия-то одинаковые. Да и идеологически "обновлять когда ничего не было" -- как-то странно.

        Может, всё-таки реализовать эти мозги в общей части библиотеки? Видимо, где-то на стыке cda и Cdr.

        Логика такая:

        • Чтобы в момент ОБНОВЛЕНИЯ проверялось время еще раз.
        • Но надо как-то запоминать факт "вёрнутое sec==0", чтобы НЕ сравнивать времена таких каналов. 02.12.2014: теперь timestamp'ы "sec==0" прямо так и сохраняются, так что признаком для непроверяния и будет само sec==0.

          "Как-то" -- это в refinfo_t, булевский флажок, по умолчанию означающий "НЕ проверять". Или всё-таки по умолчанию проверять? Ведь для ФОРМУЛ -- надо НЕ проверять, а для никогда-не-вёрнутых, по идее, наоборот?

        • Собственно проверяние:
          • Работать оно должно бы всё чисто внутри cda, а Cdr пусть получает лишь готовый результат (ну не его это задача; да и кроме него могут быть другие "пользователи", вроде das-experiment'а).
          • Событие "обновления" с точки зрения клиента/GUI для cda может быть ясен по факту дёрганья cda_get_ref_dval() и его аналогов. Следовательно, проверка должна быть там.
          • Но каждый раз дергать gettimeofday() с последующей конверсией в cx_time_t -- извращение.
          • Напрашивается идея: иметь в refinfo_t булевский флажок, который взводить при обновлении данных и сбрасывать при вычитывании (в конце его!); и чтоб если при входе в вычитывание он всё ещё сброшен, то считать, что обновления не было, значит, можно перепроверить время на тему устаревания.

            НО! Один и тот же dataref может использоваться в куче мест (ручек), поэтому такой подход не прокатит. Скорее уж надо в каждом refinfo_t иметь поле last_process_time (struct timeval!), и чтоб "клиент" передавал параметром текущее время (начала обработки), а при его несовпадении с last_... повторялась бы проверка на устарение.

        • Отдельный вопрос: как этот механизм должен вести себя с не-REF_TYPE_CHN-сущностями -- формулами и регистрами. Очевидно, максимально прозрачно-совместимым образом, т.е., чтобы функционировали бы как обычные каналы, но просто не синели бы.

          Вопрос, как это корректно организовать.

          И еще: ФОРМУЛЫ-то могут быть отражением/представлением обычных каналов, и формула, впрямую маппирующаяся на канал, должна себя вести в точности как этот канал.

        Короче -- неясностей много, нюансы вроде записаны, так что отложим на будущее.

    2. По-хорошему, надо бы делать "fresh_age" на ВСЕХ уровнях сразу в cx_time_t, а не int'овыми миллисекундами.

      Потому что усложнение будет плёвое, а выигрыш -- отсутствие компромиссов/исключений, и, соответственно, потенциальных проблем в будущем.

    3. Раздачу событий тоже надо делать -- на всех уровнях.

    27.11.2014:

    1. Раздача событий сделана. Собственно, в cxsd_hw всё тривиально, а основная работа была в cda_d_insrv (хотя и там объём кода тоже невелик).

    01.12.2014:

    1. И "fresh_age" везде переведено на cx_time_t.

      Указанность считается при

      fresh_age.sec > 0 || (fresh_age.sec == 0 && ri->fresh_age.nsec > 0

      Immediate-значения указываются при помощи

      (cx_time_t){SECS,NSECS}
  • 23.11.2014: кстати, еще неделю назад, при изготовлении драйверов cac208 и dl200me, оказалось удобно сделать wrapper ReturnInt32Datum(), возвращающий наверх ровно ОДНО 32-битное значение.

    Учитывая, что бОльшая часть каналов именно такие -- может, ввести его в официальный API?

    02.12.2014: да, введён.

    24.04.2015: дополнительно вводим ReturnInt32DatumTrusted(), возвращающий с timestamp'ом "TRUSTED" -- для каналов записи.

    29.03.2016: добавлен ReturnInt32DatumTimed(), позволяющий указывать timestamp. Нужен для vdev и vdev'ных драйверов.

  • 26.11.2014: общие соображения о работе событий "изменения свойств" -- с точки зрения frontend'ов и cda_d_insrv.

    26.11.2014: мысли:

    1. Во-первых, cda_d_insrv -- может всё отдавать сразу.
    2. Остальные:
      1. Диапазоны (которых пока всё равно нет) нужно отдавать клиенту сразу. В любом случае и на любой канал.
      2. Строки -- аналогично.
      3. {r,d} -- некоторый вопрос: с одной стороны, можно как следующие ("позже"); с другой -- клиент может прислать запрос на запись в любой момент, так что надо б ему сбагрить обновления как можно раньше.
      4. Остальные же параметры -- вроде времени свежести -- главное, чтобы прилетели клиенту ДО нового значения. Следовательно, можно делать так:
        1. НЕ-по-цикленные каналы ("немедленные", либо со своей периодичностью, либо по по отклонению) -- свойства можно слать сразу.
        2. По-цикленные же -- красиво было б слать в конце цикла, перед самими данными.

          Например, так:

          • У каждой записи о канале завести поле props_chg_mask.
          • Плюс на всё соединение кумулятивное props_chg_mask.
          • При обновлении какого-то из свойств делать OR в поле канала и в кумулятивном.
          • Перед отправкой по-цикленных данных проверять, что если общее !=0, то проходиться по списку и для затронутых отправлять клиенту (причём можно все одним пакетом!), затем флаг сбрасывая.

      Только как-то шибко замудрённо. И вопрос, корректно ли оно будет себя вести при изменении {списка мониторируемых каналов} посреди цикла.

      Вообще, откуда лезут все мысли по "оптимизации"? Наверное, оттого, что "некрасиво" будет прилёт кучи разрозненных пакетиков с изменениями -- при этом скопом (у одного CAC208 режим сменили -- и вуаля). Ну и много syscall'ов получается, что в случае дохлых устройств вроде CANGW критично.

      Но, может, забить на это -- ну и пусть пачкой мелких прилетает? Зато будет всё просто.

    16.01.2015: да, сделано по-простому -- отправляются сразу, так что прилетают пачкой мелких. Зато всё просто и без подводных камней.

  • 16.05.2015: а зачем у нас cxsd_hw_{layers,devices,channels} фиксированными массивами? Можно их realloc()'ить по мере надобности -- тем более, что количество в конкретной БД известно сразу.

    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.

  • 01.07.2015@после-обеда-в-Амиго: можно для симуляции сделать еще одну штуку: чтоб каналы "r" считались за каналы "w" -- тогда они смогут меняться внешней программой по некоему закону/правилу, имитируя работу железа (например, ИСТа).

    Повесить оное можно на ключик -S, который бы включал и -s тоже.

    23.01.2019@лыжи-после-обеда-2-я-5-ка-конец-2-го-км: а ввести бы в сервере дополнение к ключу "-s" ещё и "-S" -- СУПЕРсимуляция, когда все read-каналы становятся rw. Смысл -- чтоб можно было симулировать поведение драйверов путём записи значений снаружи.

    Как реализовывать:

    • Можно сделать флаг "симуляция" не булевским, а числовым: 0:нет, 1:симуляция, 2:суперсимуляция.
    • Как/когда/где переделывать ro-каналы в 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():
      1. chn_p->rw теперь форсится в "true" также при CXSD_SIMULATE_SUP.
      2. Оное учитывается при подсчёте next_wr_val_bufsize и аллокировании next_wr_val.
    • В CxsdHwSetSimulate() убрана "буленизация".
    • В cxsd_config.c и stand.c добавлена поддержка ключа -S и строчка о нём в help по -h.

    28.01.2019: проверил -- ключик работает, "done".

  • 19.03.2016@утро-лыжи: было бы здорово и в cxsd ввести поддержку CXDTYPE_UNKNOWN -- аналогично реализованному недавно в cda.
    • Было бы полезно для mirror_drv.c (или "proxy_drv.c") -- драйвера для проксирования/туннелирования каналов (этакий "gateway").
    • В большинстве случаев, конечно, достаточно будет CXDTYPE_DOUBLE -- это даже лучше всего для "публикования" данных для внешнего использования.
    • Дополнительная проблема с {r,d}s: поскольку cxsd_driver (и cxsd_hw) позволяет только ОДНУ пару {r,d}, то невозможно будет туннелировать больше.

      Поэтому на сейчас, видимо, правильнее закрепить принцип "публикуем получающиеся 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: приступаем к изготовлению.

    • Оно пока всё за-#if'лено по значению CXSD_HW_SUPPORTS_CXDTYPE_UNKNOWN (который сейчас в cxsd_hwP.h уставлен в 1).
    • Поле cxsd_hw_chan_t.nelems переименовано в max_nelems (и года не прошло с аналогичного переименования refinfo_t.nelems 21-04-2015 :)).
    • ...заодно аналогично переименовано cxsd_driver.h::CxsdChanInfoRec.nelems (используется в CxsdDbDevLine_t).
    • Собственно модификации кода:
      1. "Интерфейсы" -- cxsd_fe_cx.c и cda_d_insrv.c -- изменения тривиальны и минимальны.
      2. Парсер в cxsd_db_via_ppf4td.c -- простенькая адаптация под тип 'x' с default'ным max_nelems=sizeof(CxAnyVal_t).
      3. Основное же -- в cxsd_hw.c.
        • Простое -- заполнение полей в CxsdHwSetDb().
        • Главный объём -- в ReturnDataSet():
          • Специальный блок для UNKNOWN, с сохранением dtype/usize в current_ и доп.проверкой на nelems.

            Отличие от cda -- что НЕ нужно дополнительной ячейки для REPR_TEXT.

          • Далее повсеместное использование current_dtype вместо просто dtype --
            1. В "вычислении" repr и size.
            2. В switch()'ах при выборе формата для складирования.
        • Игнорирование UNKNOWN-каналов для попыток записи -- в ConsiderRequest().

    Проверять?

    P.S. Замечание: в обоих случаях -- cda_core и cxsd_hw -- не совсем корректно будет обрабатываться ситуация с max_nelems=1 (т.е., формально скаляр) и ssiz>1: в такую ячейку не вместится ничего, кроме однобайтных данных, а многобайтные приведут к nels:=0. В оправдание можно сказать, что:

    1. Сама ситуация дебильна -- нет никакого смысла указывать тип канала "x1".
    2. Проверки на скалярность по принципу "max_nelems==1" для вариабельных UNKNOWN-каналов несколько некорректны.

    13.04.2016: mirror_drv.c написан (копированием trig_read'а с обкарныванием и модификацией парсинга описания канала).

    14.04.2016: пытаемся испытывать... На вид в основном вроде работает, но мешают какие-то косяки непонятного свойства (к делаемому вроде отношения не имеющие) -- что-то с чтением w-noop-каналов.

    И пара замечаний:

    • StdSimulated_rw_p() к возможному UNKNOWN совсем не адаптирован! А надо бы.
    • cdaclient.c (+console_cda_util.c) совершенно не готовы регистрировать каналы типа 'x' -- в первую очередь из-за сложностей с dpyfmt. Это несколько усложняет отладку.

    15.04.2016: мда, проблема крылась именно в StdSimulated_rw_p():

    • оно при начальном чтении отдавало rw-каналам CX_TIME_SEC_NEVER_READ,
    • что в cxsd_fe_cx.c::PutDataChunkReply() после модификаций в 20-х числах марта стало приводить к отправке CXC_CURVAL вместо CXC_NEWVAL;
    • в результате cdaclient НИКОГДА не получал на noop_drv'шные rw-каналы событие UPDATE, и...
    • ...как следствие, никогда не отправлял запроса на запись -- получился замкнутый круг.

    Поэтому на сейчас сделано ВСЕГДА возвращение текущего времени (указывается timestamps=NULL) и всё очухалось.

    15.04.2016: дальнейшие длительные мучительные тесты показывают, что вроде бы 'x'-каналы работают как надо:

    18.04.2016: StdSimulated_rw_p() адаптирован. Это свелось к тому, что если тип обрабатываемого канала ==UNKNOWN, то он превращается, в зависимости от max_nelems (т.е., от того, какого размера данные способен вместить канал) в INT8, INT16 или INT32.

    Проверено -- функционирует.

    • Единственная мелкая неприятность -- вначале клиент (cda) ругается, что тип канала несовместим:
      cda_dat_p_update_dataset() incompatible dtype in localhost:9.m.0: 0 != 19
    • Видимо, при подключении cxsd_fe_cx присылает текущее (CURVAL) значение, где тип еще ни разу не был обновлён и висит UNKNOWN.
    • Странно только, что оно так ТОЛЬКО при типе "x1".
  • 19.03.2016@утро-лыжи: а не разрешить ли адресовать ЕДИНСТВЕННЫЙ канал устройств как просто "devname"?
    • Проверять на это можно в CxsdHwResolveChan().
    • Но сия фича внесёт неоднозначность для target'ов директивы "cpoint".

      23.03.2016: да и для внешней БД -- вроде еманофединой -- такое жуликоисключение будет кошмаром.

    Так что ну нафиг -- "withdrawn".

  • 04.04.2016: вылез дурной ляп: в CxsdHwSetDb() при аллокировании cxsd_hw_current_val_buf ему НЕ делалось bzero().

    В результате позавчера на vepp4-pult6 (SL-5.5, 2.6.18/i686) полезли всякие бредовые начальные значения. Причём лезло оно только в Chl-клиентах -- поскольку они показывают начальные значения, а вот cdaclient -- нет, он только обновления.

    • Почему раньше не замечалось -- видимо, система другая, да и совпадать могло (во многих каналах ведь нули были). ...вряд ли на это как-то влияли недавние изменения в логике "что отдавать при начальном чтении", т.к. NEVER_READ в любом случае генерит только CURVAL и никогда NEWVAL.
    • Но сам косяк -- недодумка -- из-за смены парадигмы, еще в прошлом году: код CXC_CURVAL появился лишь 16-03-2015, а до того значения должны были передаваться только по реальному обновлению.

    Добавлен bzero(), причём только для cxsd_hw_current_val_buf, а cxsd_hw_next_wr_val_buf нет, поскольку использование его содержимого чётко флагируется.

  • 10.05.2016: у нас значения, запрошенные на запись, сохраняются до момента, когда устройство перейдёт в состояние OPERATING, и тогда -- в ReviveDev() -- запросы отправляются на исполнение, путём вызова ReRequestDevData().

    Вопрос -- а насколько корректно так поступать? Может, правильнее всё-таки запросы на запись ОТБРАСЫВАТЬ? (Навеяно нынешним функционированием конверсии: если запись придёт до первого оживления, то в ней НЕ поучаствуют аппаратные {r,d}, и число будет неверным.)

    Например, в v2 в stop_dev() делалось Remit{Chan,Bigc}RequestsOf(devid). Правда, это именно при останове -- т.е., при переходе в DEVSTATE_OFFLINE.

    10.05.2016: подумавши --

    1. Проблему неполной конверсии надо решать в правильном месте -- т.е., производить конверсию непосредственно перед отправкой.
    2. А вот при "убиении" устройства запросы действительно надо обнулять -- чтобы они не достались его "следующей инкарнации", в случае ре-старта устройства с консоли ("dev_restart DEVID").

    11.09.2019: а ведь до сих пор «при "убиении" устройства» запросы на запись в его каналы НЕ отбрасываются: в TerminDev() ничего на эту тему нет, и в иных местах cxsd_hw.c также нуления wr_req не видно -- оно есть ТОЛЬКО в ReturnDataSet().

    Очевидно, потенциальная проблема есть, просто она не проявляется из-за НЕиспользования механизма "гроханья/рестарта" драйверов.

  • 27.05.2016: занадобилось научиться при чтении игнорировать у ro-каналов условие "upd_cycle==current_cycle". Для pzframe-клиентов, чтоб каналы могли запрашиваться чаще, чем раз в цикл.

    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 его убирает (нулит).
    • В cxsd_fe_cx.c флаг задействован для ON_UPDATE-мониторов. Работает.

    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 -- это давно напрашивалось.

        Интерфейс от этого переименования опять же не пострадал.

      • Теперь флаг cxsd_hw_chan_t.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_proto_v4.h::remdrv_data_set_return_type_t поле is_autoupdated переименовано в return_type.

      Это также никак не скажется на совместимости -- формат-то сохранился.

    • Ну и в pzframe-драйверах теперь для каналов типа PZFRAME_BIGC уставляется режим 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: протестировано -- вроде всё окей:

    1. Запрошенные с ON_UPDATE каналы теперь приходят раз в цикл, а не "сколько мы там успеем".
    2. Осциллографы же продолжили гнать картинку с бОльшими FPS, чем просто раз в цикл.

    Теперь надо будет ещё на еманофедином софте проверить.

  • 15.07.2016@утро-пляж: к вопросу о консольном интерфейсе: с одной стороны, делать его лень, а с другой -- потребность рестартовать драйверы всё же есть.
    • А можно сделать управление этими действиями через обычный интерфейс каналов -- чтоб пишешь в некий специальный канал, и делается либо "стоп", либо "старт", либо "рестарт".
    • И даже более того -- на роль этого канала напрашивается "_devstate".

    15.07.2016@утро-пляж: Если подумать -- то сплошные бонусы и удобства:

    • Простой интерфейс доступа -- обычный cdaclient.
    • Разграничение доступа тоже просто -- как у обычных каналов.
    • Сам по себе канал "_devstate" играет ту же роль, что символ +/-/. в строке драйвера в консольном интерфейсе. А его timestamp -- это как раз время последней смены состояния.

    Есть, конечно, и сложности:

    • Перво-наперво -- это канал ЧТЕНИЯ, с rw=0.

      Что делать: менять его тип на rw=1 или ставить особое условие в CxsdHwDoIO()/ConsiderRequest()/SendChanRequest()?

    • Далее -- нужна какая-то кодировка, т.к. эта "запись" -- не просто перевод девайса в указанное состояние, а запуск ДЕЙСТВИЯ, и, соответственно, с обычной троицей OFFLINE/NOTREADY/OPERATING оно не совпадает.

      А действия нужны такие:

      • Отключить: да, тут подойдёт и -1.
      • "Включить": если OFFLINE, то сделать InitDevice().

        Замечание: да, privrec будет повторно аллокироваться. Поскольку в TerminDev()'е он освобождается.

      • "Ресетнуть": 1) если не-OFFLINE, то отключить TerminDev()'ом; 2) сделать InitDevice().

      Вот со включением и ресетом некоторая неопределенность. Можно, конечно, использовать для них 1 и 0, но лучший ли это вариант?

    11.07.2017@утро-душ: кстати, в случае такой реализации "sato" etc. даже потребность в cx-console реально исчезнет:

    • Статус получить можно чтением канала _devstate.
    • Рестарт -- записью в него же.

    ...только листинг "всего сразу" так просто не получишь; но это нужно пореже, чем рестарт.

    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():
      • Идёт по переданному списку, рассматривая переданные ей номера каналов сразу в номенклатуре "CXSD_DB_CHAN_nnn_OFS" (вычитая девайсов count).
      • И предпринимает разные действия в зависимости от конкретного номера.
      • Для конкретно CXSD_DB_CHAN_DEVSTATE_OFS:
        1. Берёт из *(values[x]) значение, считая его сразу за int32, т.к. соответствующее преобразование должно было быть выполнено ранее в цепочке вызова.

          07.12.2018: чорд!!! Брать надо было из values[n], а не values[n]. Ведь x -- это номер КАНАЛА, индекс в массивах -- n. Так что исправлено, и вообще n заменено на chn -- как в драйверных _rw_p(). (Наткнулся случайно, из-за недопеределанности (недообобщённости) в cxsd_db.c::CxsdDbResolveName(), когда каналу _logmask по ошибке сопоставлялось значение 2, теперь принадлежащее _devstate'у.)

        2. Смотрит по знаку:
          • <0 (-1) -- отключение посредством TerminDev().
          • ==0 -- "reset": 1) если не-OFFLINE, то отключение через TerminDev(); 2) включение через InitDevice().
          • >0 (+1) -- "включение": если OFFLINE, то InitDevice().
      • 06.12.2018: добавлена поддержка CXSD_DB_CHAN_LOGMASK_OFS.
    • Ну и в CxsdHwSetDb() теперь каналу _devstate ставится rw=1.
    • Кстати, в CxsdHwDoIO() добавлена проверка (после выкусывания флажка CXSD_HW_DRVA_IGNORE_UPD_CYCLE_FLAG), что переданный код -- либо DRVA_READ, либо DRVA_WRITE; в противном случае делается return -1.
    • 05.12.2018: было забыто -- в SendChanRequest() интерпретировать код DRVA_INTERNAL_WR аналогично обычному DRVA_WRITE: сдвигать dtypes,nelems,values на offset и seglen, а не делать им =NULL.

    05.12.2018: проверяем. После исправления мелкого косячка (см. последний пункт во вчерашнем списке) -- работает!!!

    Пара дополнительных проверок (тестирование работы остальныъ компонентов):

    1. Запись одним вызовом CxsdHwDoIO(), где один и тот же канал содержится несколько раз (например, принудительная запись {-1,+1}).

      Результат: облом. Не то, что не выполняется цепочка, а даже НЕ записывается последнее: вместо него пишется только ПЕРВОЕ. Например, триплет {-1,0,+1} вместо ожидаемого {-1,+1} приводит к {-1,-1}.

      Причина неясна, надо разбираться.

    2. Запись одним вызовом блока, размером превышающим 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]));

    Что произошло:

    • Функционал делался 04-12-2018...05-12-2018, тогда же и проверялся.
    • Там была допущена ошибка с chn вместо n, но в обоих случаях значение было =0.
    • А потом 06-12-2018 при запинывании динамической изменябельности logmask (канал _logmask) значение _DEVSTATE_OFS было сдвинуто с =0 на =2.
    • Тут-то оно и стало пытаться адресоваться к values[2] вместо values[0], а 2-го элемента массива НЕ СУЩЕСТВУЕТ!

    Короче -- исправлено (в 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() -- на вид всё корректно.
    • Возникла мысль: а что, если дело в типах? Ведь cdaclient по умолчанию шлёт CXDTYPE_DOUBLE, а is_internal_rw_p() ждёт int32, о чём чётко сказано у него в комментариях в п.4.

      Вдруг просто преобразование где-то не было выполнено, и пришло "не то" значение? Как показывает опыт, небольшие вещественные числа могут в младших разрядах иметь нули -- вот оно и было понято как 0, что и привело к reset'у.

      Попробовал слать @i -- да, всё функционирует как должно! Вывод: проблема действительно в преобразовании типов.

      ...крамольная мысль: а в остальных-то случаях преобразование выполняется? Точно? И точно именно как надо?

    • Заодно было наблюдено, что при записи @i:..._devstate=+1 для и без того работающего устройства cdaclient зависает "навеки".

      Причина очевидна: ветка кода, обрабатывающая вариант "ival > 0", что-то делает только для OFFLINE-устройств, а для прочих -- ничего. В результате состояние _devstate не меняется и не возвращается "наверх", так что cdaclient и не получает обновления.

      @получасом позже, в пристройке: напрашивается решение добавить туда "else" с холостым вызовом report_devstate(). Оно как бы спорно с точки зрения timestamp'а: с одной стороны -- нехорошо, что он как бы поменяется (т.к., реально-то ничего с устройством и не делалось); а с другой -- ведь запись-то в _devstate была, так что типа всё верно.

      @ещё получасом позже: а если просто вызвать уведомление об изменении -- CxsdHwCallChanEvprocs() -- для _devstate и _devstate_description? Тогда и уведомления будут, и timestamp'ы не изменятся. Хотя это и более муторно, чем просто вызов report_devstate().

    10.09.2019: пытаемся разобраться и решить проблему.

    • Начинаем с простого: с удовлетворения cdaclient'а.
      • Наипростейший вариант с report_devstate() сходу не прокатил: точка, где его надо вызвать, находится выше его объявления, так что при простодушном вызове получилась ошибка "static declaration of 'report_devstate' follows non-static declaration".
      • Посему реализовано по 2-му проекту -- с "ручным" дёрганьем CxsdHwCallChanEvprocs(). Получилось -- cdaclient счастлив :-).
    • Далее: отладочная печать показала, что передаётся dtype=19 (что и есть CXDTYPE_DOUBLE) вместо надлежащего CXDTYPE_INT=10.

      И при печати переданного значения путём "%f" видно, что действительно приходит 1.0.

    • Поанализировав цепочку вызовов, нашёл-таки причину: в ConsiderRequest()'е в ветке "action == DRVA_WRITE" вызов StoreForSending() -- который и выполняет конверсию! -- делался только при f_act == DRVA_WRITE, а вариант DRVA_INTERNAL_WR проходил мимо.

      Вот преобразование типа и не выполнялось, так что оставалось и передавалось ВЕЩЕСТВЕННОЕ.

      Добавил альтернативу || f_act == DRVA_INTERNAL_WR.

      ...и получил SIGSEGV...

    • Причина очевидна: для is_internal-каналов НЕ выделяются next_wr_val'ы.

    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) -- поскольку никаких и попыток-то делать "чтение" нету.
      • Попробовал: неа, работает.
      • Как оказалось, программа-сервер -- cxsd.c -- НИКОГДА и не передавала значение ключа (отображающегося в переменную 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 и проставляет.

  • 13.03.2018: обнаружилась проблема, вызванная дизайн-решением при реализации StoreForSending() осенью 2014-го: она модифицирует содержимое переданного ей массива dtypes[].

    Вылезло это при создании cxsd_fe_starogate.c, где значение передаваемой CxsdHwDoIO() static-переменной dt_double=CXDTYPE_DOUBLE портилось, заменяясь на CXDTYPE_INT32.

    14.03.2018: пытаемся детально разобраться в ситуации.

    • Используется для складирования прямо тот массив dtype, что передан.
    • Зачем вообще складировать -- неясно.

      Казалось бы -- ну конвертируются после складирования данные в тот тип, что указан в chn_p->dtype, вот его и использовать/подразумевать. Но нет, так просто не получится -- этот тип нужен в элементе массива.

    Что можно сделать:

    1. Вариант 1 -- завести в cxsd_hw_chan_t дополнительно к next_wr_nelems еще и next_wr_dtype (чтоб StoreForSending() ВСЕГДА писал туда).

      Но это не очень удобно -- в прочих местах используется массив, а тут будет поле в другом массиве.

    2. Вариант 2: в вызывальщике StoreForSending() иметь массивец.

      Но тут проблема с размером/количеством: в прочих-то ограничение SEGLEN_MAX=1000, а в вызывальщике Store -- ConsiderRequest() -- ограничения не предусмотрено.

    3. Кстати, StoreForSending() и переданное ей nelems[] портит -- в случаях, если там больше, чем max_nelems.

      Чуть позже: и переданное values[] тоже меняет -- ставит туда ссылки на chn_p->next_wr_val.

    Вывод:

    • Не проще ли -- особенно с учётом п.3 -- просто задекларировать, что переданные CxsdHwDoIO() параметры НЕ гарантируют сохранности содержимого?
    • Кстати, при создании всей инфраструктуры, 31-10-2014, так и сказано: "да, надо объявить право менять значения в тех трёх массивах".

      И именно так оно и сделано.

    Вердикт: да, считаем, что так оно и должно быть, а данный раздел закрываем как "withdrawn".

    После обеда: и даже комментарий в cxsd_hwP.h вставлен:

    /* Note: CxsdHwDoIO MAY modify contents of dtypes[], nelems[] and values[] */
  • 14.03.2018: в процессе расследования предыдущего пункта обнаружилось, что CxsdHwSetDb() НЕ делается округления csize до размера 16, а остаётся прям как есть. В результате может вылезти ошибка выравнивания -- например, если подряд идут каналы w1b и w1i, то буфер последнего будет расположен по смещению 1, что неприемлемо.

    14.03.2018: вариант: надо округлять не ПОСЛЕ аллокации очередного, а ПЕРЕД, по его кратности. Так можно будет избежать совершенно пустого расхода памяти -- т.к. большинство каналов int32 (1i1), то тратить 12 байт впустую глупо. ...кстати, ведь компиляторы именно так и размещают переменные и поля в структурах.

    21.03.2018: делаем, в варианте "ПЕРЕД". Технология:

    • Формула --
      _bufsize = (_bufsize + usize-1) & (~(usize - 1))
      (чисто арифметически "(~(usize - 1))" эквивалентно "-usize, но корректнее с точки зрения знаковости/беззнаковости).
    • Махинации исполняются в начале обработки группы, а не перед каждым каналом (т.к. каналы группы однотипны и выравнивание одного влечёт выравнивание следующих).
    • Делается это "выравнивание" только при csize>0 (но это вроде всегда так), ...
    • ...плюс стоит проверка, что usize является степенью двойки (это тоже всегда так, просто вследствие способа кодирования dtype).
    • Конкретно current_val_bufsize "добивается до кратного" всегда, а next_wr_val_bufsize -- только если rw.

    Проверяем:

    • Для проверки вставлен отладочный вывод оффсетов для каждой группы каналов.
    • На вид -- работает арифметика как надо.
    • И обнаружилось -- забытое изначально -- что есть же еще каналы _devstate и _devstate_description, которые ни к какой группе не относятся, а добавляются отдельными кусками кода.

      И _devstate-то -- INT32!

    • Поэтому для _devstate было добавлено индивидуальное выравнивание (а для _devstate_description -- нет, т.к. он _TEXT).
    • Самое забавное: похоже, x86_64 плюёт на неваравненность данных так же, как и обычный x86 -- ибо на тестовом devlist'е
      dev a noop w1i,w1b,w1d,w2b -
      dev b noop w1i             -
      
      сервер даже без выравнивания работал и не падал.
      1. И был даже написан небольшой тестик, продемонстрировавший, что выравнивание необязательно:
        #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);
        }
        
      2. Как показало гугление, x86_64 (и x86 вообще) действительно не требует выравнивания, КРОМЕ команд SSE: "On x86 the alignment problems only happen when using SSE"; ключевые слова "movdqa" и "movdqu" (Aligned и Unaligned) -- там выравнивание требуется вообще по границе 16 байт.

    Кстати, в gcc есть штучка по имени __alignof__, возвращающая требуемое выравнивание для типа/переменной. Синтаксис -- вроде как у sizeof(), но можно указывать и поля (чего sizeof() не позволяет).

    23.01.2019: давно работает, задеплоено, и даже далее -- уже аллокирование всего остального тоже делается. Так что "done".

  • 16.03.2018@сессия-ИЯФ-Пиминов(ВЭПП4, упомянул будущие MPS*): надо что-то делать с "единственностью R,D" от драйвера. Это запрещает "полное" туннелирование свойств.

    Как?

    1. Разрешать 2 пары на канал -- не вариант: лишняя потеря памяти (для 99% каналов) и всё равно зачастую не хватит.
    2. Аллокировать дополнительную память по-канально? А по сколько пар и каким каналам "да", а каким "нет"?

      И желательно было бы аллокировать всехнюю ОДНИМ блоком (как буферы _current_val_ и _next_wr_val_), а не поштучно.

    Ясности нет совсем...

    В любом случае, нужно вносить этот пункт в список хотелок на 2018 год.

    21.03.2018@лыжи, вечер, конец-3-й-2-километровки: RD множественные можно указывать SLOTARRAY'ем "GROWING", искать там ячейку, ячейки аллокировать сразу на 20.

    • Обоснование: таких каналов/устройств, требующих (из-за туннелирования) отдавать наверх более 1 пары RD -- скорее всего, будет кот наплакал. Так что дополнительный расход памяти просто смешной, по сравнению с вариантом "прямо в cxsd_hw_chan_t.phys_rds[] иметь место под 20 пар". Плюс, оное указание драйверами будет в основном выполняться при старте, так что и на производительность влияния также не окажет.
    • Вариант: первый дуплет вектора ВСЕГДА хранить в cxsd_hw_chan_t.phys_rds, а остальные -- в ячейке slotarray'я (это позволило б сэкономить, но получится менее красивый размер ячейки в байтах).

      22.03.2018: а если ячейки делать на 16 -- в сумме 17, и для большинства этого хватит выше крыши, плюс оставит резерв (до 20) для дальнейшего навешивания 3 калибровок.

    • Следствие: можно потенциальную функцию "SetChanRDsList()" организовать так (на уровне работы с 1 каналом):
      • Шаг1. Если число_дуплетов>1, то аллокировать ячейку и сохранить туда дуплеты [1...N-1],

        иначе сделать Rls*Slot() имеющейся (если есть).

      • Сделать SetChanRDs(), она же заодно и _CHAN_R_RDSCHG дёрнет.

    22.03.2018: некоторые соображения вдогонку:

    • Ч-чёрт, еще надо где-то держать количество элементов в ячейке... Использовать для этого поле in_use?

      Тогда получится иметь ячейку на 15 дуплетов, а в первых sizeof(double)*2=16 байтах разместить служебную информацию, на роль которой сейчас претендует только количество дуплетов.

    • Кстати, такой вызов позволит снимать калибровки полностью, указывая число_дуплетов=0, что приведёт к сбросу phys_rd_specified=0 (сейчас такой возможности нет вовсе).
    • Учитывая, что калибровки указываются сразу ГРУППЕ каналов, просто вызывать SetChanRDs() не получится, а надо воспроизводить все её действия.
  • 07.11.2018: нужен бы простой способ поштучно делать индивидуальные устройства "симулируемыми" -- чтоб часть работала в режиме симуляции (реальные устройства), а часть -- по-настоящему (всякие формульные вычислители).

    Вроде бы когда-то (кажется, в v2) предусматривалась возможность такого, путём указания '-' перед именем драйвера. Сейчас следов реализации сходу найти не удалось, но синтаксис выглядит разумным.

    20.03.2021: следы нашлись -- bigfile-0001.html от 26-02-2007, ключевое слово "d_simulate[]".

    04.12.2018@дома, после обеда (после реализации возможности рестартовать драйверы через запись в _devstate): надо б сделать -- и обдумано уже, да и как раз во внутренности cxsd_hw.c мозгами плотно погрузился.

    • Главный идеологически-технологически-методологический вопрос: КАК делать симулируемость -- чтоб где и как определялось, что надо производить симуляцию?

      Обмышлявшиеся варианты:

      1. Прямо в нужных точках смотреть, не начинается ли имя типа устройства с '-'.

        Вариант годившийся для v2, где имя особой роли не играло (только при загрузке драйвера надо было пропустить первый символ, если он дефис), но для v4 не катит: тут имя типа используется также и самой cxsd_db*.c, т.к. по нему берётся список каналов и их имён.

        Вывод: флаг "это устройство симулируем!" надо бы выпарсивать в отдельное поле, а строку типа сохранять как есть, без лишнего мусора.

      2. Уже в cxsd_hw.c где-нибудь вроде CxsdHwActivate() смотреть, что если надо симулировать, то подсовывать вместо драйверова metric'а некий специальный свой, с единственным не-NULL методом do_rw, глядящим на StdSimulated_rw_p().
      3. Проверять per-drvice-флаг в каждой точке его влияния, наравне с MustSimulateHardware.

      После прикидок, размышления и анализа выбран последний вариант.

    • Реализация:
      • В cxsd_hw_dev_t добавлен флаг is_simulated.
      • Он проверяется параллалельно с MustSimulateHardware во всех точках, где идёт обращение к методам устройства -- при загрузке драйвера, инициализации/терминировании устройства, при вызове do_rw().
      • Значение флага копируется в CxsdHwSetDb() из БД, где...
      • ...в CxsdDbDevLine_t также добавлен is_simulated.

    05.12.2018: последний пункт реализации:

    • В cxsd_db_via_ppf4td.c::dev_parser() добавлена проверка, что если перед именем типа стоит '-', то он пропускается и взводится dline.is_simulated.

    (Думалось, что придётся дополнять список параметров CxsdDbAddDev(), но там отдельными параметрами передаются только строки, а все прочие потроха -- прямо готовой структурой CxsdDbDevLine_t, так что достаточно оказалось писать в её вчерадобавленное поле.)

    06.12.2018: проверяем: да, работает.

    Но есть в синтаксисе одно неудобство: '-' указывается перед именем ДРАЙВЕРА (точнее, типа устройства), но у нас в куче конфигов драйвер указывается в макросе -- вроде MAGX_IST_CDAC20_DEV() -- так что в самой строке объявления устройства имени типа нету и дефис подсунуть некуда.

    • Может, пусть '-' указывается перед именем ЭКЗЕМПЛЯРА?
    • Но в devlist_magx_macros.m4 имя устройства используется также в куче cpoint'ов. Причём как в имени создаваемого cpoint'а, так и в имени цели: там таким образом навешиваются калибровки -- строками вида
      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 частей:

    1. Реализация в CxsdHwSetDb() -- там оно просто в параллель с обычной реакцией на MustSimulateHardware >= CXSD_SIMULATE_SUP.
    2. Парсинг -- просто распознавание символа '+'.

    28.01.2019: да, проверено, работает; причём и в сочетании с -s тоже работает: у всех каналов чиселки бегут, а у '+''нутых устройств стоят нули, и можно писать -- извращайся как хошь.

  • 06.12.2018: надо реализовывать поддержку logmask:
    • Парсинг символьных спецификаций.
    • Возможность указывать драйверовы per-device logmask'и в :OPTIONS в конфиге.
    • Возможность менять их "на лету" через "замену консольного интерфейса" -- канал _logmask.

    06.12.2018: делаем.

    • Присутствовала идеологическая неясность "где должны жить GetDrvlogCatName() и ParseDrvlogCategories()?".

      В v2 они обитали в cxsd_drvmgr.c, в v4 отсутствующем.

      Тут первая была поселена (static'ом) в cxsd_driver.c (т.к. была необходима vDoDriverLog()'у), а вторая вообще отсутствовала.

      Сейчас стало ясно, что селить их нужно в cxsd_hw.c.

    • Итак: GetDrvlogCatName() переведён в cxsd_hw.c, и ParseDrvlogCategories() скопирован из v2.
    • В cxsd_config.c скопирован парсинг ключа -l из v2'шного одноимённого аналога.

      07.12.2018: ага, только в вызове getopt() в параметре optstring отсутствовало "l:", так что ругалось "invalid option -- 'l'". Добавлено.

      Также оно добавлено и в stand.c.

      07.12.2018: а в этом ещё и "M:", "F:", "E:", "L:" не было -- видать, даже не пытался проверять...

    • Использование options:
      • LogspecPluginParser() скопирован из v2.
      • Парсинг вставлен в InitDevice() -- т.е., он производится в момент ИНИЦИАЛИЗАЦИИ устройства.

        Ошибка парсинга options приводит к деактивации устройства -- точно так же, как и ошибка парсинга auxinfo.

    • Удалённый доступ:
      • Перетряхиваем список internal-каналов:
        • Сдвигаем _DEVSTATE_OFS и _DEVSTATE_DESCRIPTION_OFS с 0,1 на 2,3.
        • Вводим _LOGMASK_OFS=0 и _RESERVED_1_OFS=1 (это просто зарезервинованный канал -- мало ли что ещё числовое занадобится в будущем).
        • Таким образом, CXSD_DB_AUX_CHANCOUNT увеличилось до 4.
      • Резолвинг в CxsdDbResolveName():
        1. Она обучена понимать имя "_logmask"
        2. Обработка специальных каналов там обобщена: теперь finding_special там не просто 1 и 2, а принимает значение CXSD_DB_CHAN_nnn_OFS+1 ("+1" чтоб !=0).
      • Аллокирование в CxsdHwSetDb() -- тут всё просто, скопировано с _devstate'ового. Да, там теперь ЧЕТЫРЕ копии кода аллокирования канала, аналогичных тому, что в цикле по основным каналам.

        Специфика конкретно для _logmask:

        1. В качестве timestamp берётся текущее время.
        2. ГДЕ-ТО НАДО ДЕЛАТЬ ПРОПИСЫВАНИЕ В ЗНАЧЕНИЕ текущего .logmask.

        07.12.2018: неа, нифига -- НЕ нужно никакой специфики, ведь потом при инициализации всё будет сделано. Так что то всё убрано.

      • Поскольку маска добывается/создаётся в InitDevice(), то оттуда же нужно и "возвращать" значение.

        Для чего сделана вызываемая оттуда отдельная report_logmask().

      • Запись в is_internal_rw_p(): просто пишется и потом вызывается report_logmask().

    06.12.2018@вечер, дорога домой, около главного входа в ВЦ: кстати, ведь не очень удобно, что маску надо будет указывать числами, а не названиями категорий.

    А можно б было сделать еще один канал -- "alias" -- именно строковый, чтоб в него писать строковую спецификацию вида +category,-category,..., а он бы должным образом её парсил и возвращал бы список "что есть сейчас" тоже текстовый.

    Но -- нафиг реально не нужно.

    ...к тому же, была бы некоторая неприятность: раз спецификация с "+" и "-" означает "модифицируй ТЕКУЩУЮ в соответствии с указанным", то просто прописать значение (например, когда-то прочитанное) -- не получилось бы, т.к. пришлось бы к нему в начало добавлять "-all,".

    07.12.2018: проверяем.

    1. Канал _logmask:
      • Оказывается, CxsdDbResolveName() была недопеределана -- как раз вариант DEVNAME.CHANNAME остался недообобщённым, в результате чего канал _logmask маппировался на 2 (это _devstate!) вместо 0.

        Видимо, тупо из-за того, что этот кусок был ниже по тексту и остался за нижней границей окна (позор!!!).

        С другой стороны, благодаря этому был найден баг в is_internal_rw_p() (впрочем, он в любом случае вскорости бы обнаружился).

      • Далее: просто "как канал" оно работает -- записываемое одним клиентом значение прекрасно мониторится другим.
      • Пытаемся проверять функционирование, запихав в BeginOfCycle() цикл по всему диапазону категорий и записав в маску -1. И-и-и...
      • ...В функции GetDrvlogCatName() был косяк: в проверке на попадание категории в диапазон с назначенными именами вместо "&&" стояло "||".

        Раньше-то работало потому, что все драйвера такие "правильные" и передают исключительно одну из предопределённых категорий.

        В результате SIGSEGV'илось, пытаясь проанализировать текст по адресу NULL на тему "не пустая ли там строка" (в первоначальном варианте, видимо, для категории DEFAULT было так -- чтоб не заполнять логи лишними данными; но следов того уже не осталось).

        Это ещё с момента её создания в v2 08-12-2009 -- в w20091227.tar.gz он уже есть.

        Короче -- исправлено. Там еще в левой границе сравнения надо было поставить ">=" вместо просто ">".

      • Теперь работает как надо.
    2. Указание -l MASK и log=MASK:
      • Указание в командной строке -- заработало (после добавления "l:" getopt()'у).
      • Индивидуально для устройства, через ":OPTIONS" -- тоже.

    Аллилуйя!!!

  • 07.12.2018: есть непонятка в функционировании CxsdHwDoIO() и его подчинённых: при тестировании реализации записывабельного _devstate 05-12-2018 выяснилось, что если в одну цепочку (передаваемую CxsdHwDoIO() одним запросом) поместить несколько записей ОДНОГО канала, то отработается только ПЕРВАЯ, а не последняя, как должно бы быть (промежуточные должны быть выкинуты -- точнее, перепрописаны последней).

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

    Но в причине такого странного поведения разобраться надо.

  • 05.01.2019: давно пора переводить массивы "объектов" со статики --
    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: предварительное обсуждение:

    • Подготовительные действия были проведены уже довольно давно (когда?!?!?!): они заключаются в защищенных define-символом CXSD_HW_ALLOC_ALL (который сейчас уставлен в 0):
      1. Изменениях в cxsd_hwP.h:
        • Те 3 массива переведены со статики на указатели -- *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 тут и вводится.
      2. В CxsdHwSetDb() только рудиментарные заготовки, реального кода там не появилось.
    • Понимание функционирования массивов и указателей в Си подсказывает, что "с точки зрения синтаксиса C оно всё одинаково, но на уровне кода доступ разный" (03-10-2018).

      Т.е., ABI всё-таки изменится, так что если наличествует какой-то динамически подгружаемый код ВНЕ ядра сервера, то он, откомпилированный под старую модель, просто перестанет работать.

      Сейчас таковым внешним кодом является вроде только mqtt_mapping_drv, от готовности ещё далёкий.

    • Исследуем этот вопрос детальнее.
      • А вот и не только!!! Еще обращение к cxsd_hw_channels[] есть в trig_read_drv.c -- это когда он пытается добыть свойства (dtype,n_items) для исходного канала, в случае их неуказанности.
      • Ещё есть в cxsd_fe_starogate.c, который загружается отдельно, но сей момент его загрузка сконфигурена только для ep1-berezin2:49, где реально до сих пор работает CXv2, а v4 пока не внедрён.
      • В остальном же обращения к троице массивов помимо самого cxsd_hw.c есть в cxsd_driver.c и cxsd_fe_cx.c плюс cda_d_insrv.c, но они все влинковываются в сервер статически.
    • Все же обращения сводятся:
      • Либо к cxsd_hw_NNN[x].чего-нить, либо к p = cxsd_hw_NNN + x, что, благодаря сишной фиче "массивы и указатели при обращении синтаксически одинаковы", будет работать и в старом (массивы) и в новом (указатели) вариантах.
      • КРОМЕ блока зануления в CxsdHwSetDb() -- вот там выполняется bzero() с пониманием реального типа объектов.

        Но именно этот блок и находится внутри условия #if CXSD_HW_ALLOC_ALL ... #else ... #endif и будет заменяться на на аллокирование.

    • Пара соображений о деталях реализации
      1. Работать с cxsd_hw_buffers,cxsd_hw_buf_size надо через GrowBuf().

        Это задел на будущее, когда (если) будет перечитывание БД по ходу работы сервера: если обновлённый конфиг железа требует меньшего объёма, то будет использоваться уже аллокированное. Да, БЕЗ возможности уменьшения -- а она нужна?

      2. Очевидно, что надо будет использовать двухпроходную схему: сначала подсчитываем количество layer'ов/устройств/каналов, между проходами аллокируем буфер и расставляем в него указатели на "массивы", а на втором проходе уже заполняем эти массивы.

        Так вот: эти 2 прохода (0 и 1) УЖЕ делаются. Но сейчас они используются лишь для:

        • Назначения layer'ов.
        • Аллокирования буферов cxsd_hw_current_val_buf и cxsd_hw_next_wr_val_buf.

        А запись в "массивы объектов" производится на обоих проходах; да, 2 раза одно и тоже.

        Требуются переделки:

        1. Эту запись окружить условиями "if (stage)".
        2. Местами -- как минимум в вычислении размеров current_val_bufsize у is_internal-каналов -- идёт обращение на чтение (к chn_p->...).

          Тут надо заменять на обращение к "исходным" данным. В частности, конкретно там у is_internal-каналов -- предварительно прописывать usize и csize, и использовать уже их.

        3. В цикле по каналам группы ("Iterate individual channels") используется переменная chn_p. В начале цикла она инициализируется, а после каждой итерации делается chn_p++.

          Как "заусловить" эти присвоение и инкремент внутри for() -- неясно.

          Более перспективной идеей выглядит вообще ВЕСЬ этот for(){} засунуть внутрь if (stage), сделав также else-ветку, содержащую приращения current_val_bufsize и next_wr_val_bufsize сразу на grp_p->count (вместо по-канального на csize).

    06.01.2019@дома: начальные действия, глобально в функционировании вообще ничего не меняющие:

    • Для групп "обычных" каналов подсчёт объёмов буферов для current_val и next_wr_val для случая stage==0 вытащен в отдельную ветку (просто умножением объёма для одного канала на количество каналов, как и предлагалось во вчерашней "идее").
    • Для is_internal-каналов всё под-унифицировано:
      • вначале прописываются dtype с nelems и потом из них вычисляются usize с csize, ...
      • ...а дальше код "заполнения свойств" во всех 4 случаях одинаковый, и выполняется теперь только при stage!=0.
      • Увеличение же current_val_bufsize делается в конце каждого блока, сводясь к +=csize.

    06.01.2019@дома-вечер: движемся далее.

    • Сделал-таки давно напрашивавшееся радикальное изменение: для is_internal-каналов заполнение свойств вытащено в отдельную FillInternalChanProps().
      • Ей передаются:
        1. Номер канала (gchan) и его базовые свойства (rw, dtype, nelems), плюс devid.
        2. Номер прохода (stage).
        3. Текущее значение 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 (ради которого и изменение) передаются по указателям.

    • И вот далее наткнулся-таки на реальную технологическую проблему: ведь "аллокирование" layer'ов заключается в том, что:
      • для каждого устройства выполняется поиск по cxsd_hw_layers[] на тему "есть ли уже такой layer в списке задействованных",
      • и если нету, то он прописывается и делается cxsd_hw_numlyrs++.

      НО: ведь сам "массив" cxsd_hw_layers (теперь превращающийся из массива в указатель) аллокируется только ПОСЛЕ прохода 0 -- т.к. надо знать количество layer'ов, а количество ранее подсчитывалось как раз поиском по нему же именно на проходе 0.

      Тупик, клинч? Что делать? Надо думать...

    07.01.2019: далее...

    • @лыжи (мысли бродили и ранее, но тут оформились): нам ведь на 0-м проходе нужно только количество посчитать, так?

      Ну так можно считать иначе: вести поиск не по cxsd_hw_layers[], а по списку устройств (который ЕСТЬ в БД, уже доступной целиком!), по принципу, что если такой layer уже встречался у предыдущих устройств, то он уже "посчитан", а если нет, то надо его "посчитать", увеличив количество на +1.

      Обсуждение:

      • Конечно, это заведомо менее оптимально: вместо перебора по numlyrs (что даёт в максимуме numlyrs*numdevs-numlyrs/2 сравнений) перебор идёт по устройствам, так что сравнений будет до numdevs*numdevs/2, что явно больше, т.к. numdevs>=numlyrs.
      • Но для устройств БЕЗ layer'а поиск не выполняется вовсе, ...
      • ...а при наличии layer'ов их будет немного (сейчас -- вообще 1 штука) и они будут сразу учитываться, так что в основном поиск будет идти не до devid, а до нескольких единиц.
      • И в любом случае общее количество устройств на одном сервере вряд ли превысит несколько сотен, так что общее количество переборов тоже будет не бог весть каким большим (ну да, полу-квадрат количества устройств, но для современного проца это немного).
      • Да и вообще, действия эти выполняются один-единственный раз при старте сервера, когда время выполнения не входит в задачу.

        (Да, это мы НЕ учитываем потенциальную будущую возможность смены devlist'а на лету, когда время пере-инициализации будет как раз критично.)

    • @пультовая, 17-18: сделано.
    • Начинаем блок собственно аллокирования (в конце тела цикла по stage).

      Пока что только подсчёт объёма и смещений разных частей внутри него.

    08.01.2019: пилим дальше...

    • Блочок "резервирование layer'а номер 0" перенесён внутрь цикла по stage.
    • Аллокирование доделано: теперь и аллокирование, и расстановка указателей на разные "массивы" внутри общего буфера.
    • Плюс начинаем потихоньку адаптировать "эккаунтинг" -- инициализацию/увеличение счётчиков, вместо былого использования глобальных cxsd_hw_* сразу.
    • Проведена первая компиляция; ошибки устранены (в основном -- не были объявлены переменные), модуль собирается.

    10.01.2019: проверять надо!

    11.01.2019: ну-с...

    • Стадия 1: проверяем работу модифицированного модуля в старом режиме (CXSD_HW_ALLOC_ALL=0) "поверхностно" -- сравнивая получаемое (на примере devlist-canhw-19.lst) количество layer'ов/устройств/каналов с получаемым от старого варианта модуля.

      Числа совпали -- 2,36,4816.

    • Стадия 2: проверяем то же самое уже с CXSD_HW_ALLOC_ALL=1.

      Числа опять совпали.

    • Стадия 3: приконнекчиваемся к серверу (запущенному с -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.

    1. cxsd_hwP.h:
      • Убрано за-#if'ленное по CXSD_HW_ALLOC_ALL==0, плюс он сам.
      • И enum'ы CXSD_HW_MAX_LYRS, CXSD_HW_MAX_DEVS, CXSD_HW_MAX_CHANS тоже.
    2. cxsd_hw.c:
      • Собственно удаление старого кода -- ясно. Сократилось аж на целых 2кБ.
      • Плюс стало очевидно, что сразу после "cxsd_hw_cur_db = db" надо бы РЕАЛЬНО очищать всю информацию cxsd_hw:
        1. Выбрасывать старые по-канальные evproc'ы (пока о них ещё есть информация!).

          ...для чего, возможно, подойдёт DestroyChanCbSlotArray() -- создаваемый, но неиспользуемый.

          ...хотя есть какая-то странная неиспользуемая функция (заготовка?) chan_evproc_remover() -- прилагается к CxsdHwSetDb().

        2. Занулять все "количества" cxsd_hw_num*.
      • ...а ещё ведь должно быть сделано TerminDev() всем устройствам.

        ...и layer'ам.

    18.01.2019: завершаем чистку: удаляем все промежуточные cxsd_hw*.c, через которые в несколько этапов шло внедрение варианта с аллокированием. Если что, они остались в w20190114-cxsd_hw_pre-cleanup.tar.gz.

    Ну и цель раздела засим можно считать достигнутой.

  • 08.01.2019: небольшое изменение в именах: ВСЕ переменные с типом cxsd_hw_dev_t теперь именуются dev_p, а не dev (раньше было вразнобой).

    (А вот cxsd_hw_lyr_t -- УЖЕ и так все lyr_p.)

    Коснулось cxsd_hw.c и cxsd_driver.c.

  • 08.01.2019: кстати, к вопросу об ABI: нехорошо, что драйверы лазят в 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.
    • Также потребовалось добывать информацию об УСТРОЙСТВЕ -- диапазон его каналов (номер первого и общее количество), для того же 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".

  • 20.05.2019: странная вещь вылезла на "экспортной сварке" (xweld): на некоторых ручках (конкретно "I катода нижн." и "I катода верх." в под-панели "Режим стабилизации тока" в "Контроллер пушки"):
    • При установленном шаге 0.1
    • R=10 (т.е., 0.1 экранное соответствует 1.0 "сырого")
    начинаем "стрелочкой вверх" с 0 увеличивать, и до 0.7 оно идёт нормально, а выше -- нет: остаётся на 0.7.

    Проверено на симуляторе ("cxsd -dsc" с этим же файлом, но R=10 указан руками, т.к. его давал драйвер) -- результат тот же.

    Причём:

    • Замечено было ТОЛЬКО на 0.7, иных вариантов пока НЕ встречалось.
    • Если руками вбить 0.8 -- оно остаётся.
    • Если просто дважды быстро нажать "вверх", чтоб стало 0.9 -- то 0.9 останется.

    20.05.2019: пытаемся разобраться, натравив на этот канал подсматривание через "cdaclient -m". Ну да -- видно, что по нажатию "вверх" присылается то же самое значение 0.7...

    Похоже, опять какие-то проблемы с вещественными числами в аспекте округления -- что-то подобное было с драйвером Д16. Но тут-то -- наипростейший вариант, с калибровкой 10...

    И да, явно это проблема не в text_knob, а где-то в стеке cda...

    21.05.2019: пытаемся разобраться. Для начала -- напихиванием отладочной печати.

    • Выдача в CdrSetKnobValue() с форматом "%50.45f" показывает, что там действительно совсем НЕ нули после десятичной точки.
      • В числах 0.1, 0.2, 0.3, 0.4 идёт 15 нулей.
      • 0.5 -- ровное, там ТОЛЬКО нули (ну немудрено -- это 1/2, круглое в двоичной системе число).
      • А вот далее "погрешность" начинает накапливаться в минус:
        • 0.6 - 0.599999999999999977795539507496869191527366638
        • 0.7 - 0.699999999999999955591079014993738383054733276
        • 0.8 - 0.799999999999999933386618522490607574582099915
      • И вот это, видимо, "округляется" уже почему-то не в 0.8, а в 0.7.
      • Далее надо смотреть, что же происходит при конверсии/округлении.
    • Напихано отладочной печати и в 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...".

    • Сначала пытаемся разобраться, для чего добавляем отладочной печати.
      • Печать делается в cda_core.c'шных SendOrStore() и DoStoreWithConv(), плюс в cxsd_hw.c::StoreForSending(), и на это всё смотрится синхронно (сервер и клиент запущены из соседних xterm'ов).
      • Сначала понятнее не стало вовсе, скорее наоборот: домножение на 10 даёт странный результат, заключающийся в том, что после него число становится кругленьким -- N.0(0), вместо "1.9999..." или "2.000...1110223".
      • Затем появилась идея кроме десятичного числа также печатать внутреннее представление -- "%a".
      • Поэтому десятичный формат для краткости сменен на "%30.25f" (всё равно дальние цифры были малополезны -- скорее всего, они являлись мусором, т.к. float64 не имеет такой точности).
      • Результаты ДО конверсии в cxsd_hw.c::StoreForSending() совпадали с после-{R,D}'шными в cda_core.c::DoStoreWithConv() (что и понятно -- это физически то же самое число, просто переданное по сети).

      Итак, вот таблица (все значения, кроме =0.8, получены нажатием стрелки вверх):
      VSendOrStore=%aDoStoreWithConv=%a
      0.10.10000000000000000555111510x1.999999999999ap-41.00000000000000000000000000x1p+0
      0.20.20000000000000001110223020x1.999999999999ap-32.00000000000000000000000000x1p+1
      0.30.30000000000000004440892100x1.3333333333334p-23.00000000000000044408920990x1.8000000000001p+1
      0.40.40000000000000002220446050x1.999999999999ap-24.00000000000000000000000000x1p+2
      0.50.50000000000000000000000000x1p-15.00000000000000000000000000x1.4p+2
      0.60.59999999999999997779553950x1.3333333333333p-16.00000000000000000000000000x1.8p+2
      0.70.69999999999999995559107900x1.6666666666666p-17.00000000000000000000000000x1.cp+2
      0.80.79999999999999993338661850x1.9999999999999p-17.99999999999999911182158030x1.fffffffffffffp+2
      =0.80.80000000000000004440892100x1.999999999999ap-18.00000000000000000000000000x1p+3
      0.90.90000000000000002220446050x1.ccccccccccccdp-19.00000000000000000000000000x1.2p+3
      1.01.00000000000000000000000000x1p+010.00000000000000000000000000x1.4p+3

      В таком представлении всё стало существенно понятнее.

      • Почему прндыдущие "0.N999..." (0.5999..., 0.6999...) округлялись-таки после {R,D}-конверсии до нужных целых -- ясно: они после домножения на 10.0 уже не имели никаких ".999...".
      • При сложении же "0.7+0.1" почему-то происходит потеря одного-единственного битика, и вместо нужного 0x1p+3 получается 0x1.fffffffffffffp+2.

        Это, видимо, и давало эффект "эффективной/реальной дроби" -- когда при преобразовании к целому дробная часть отбрасывалась, и вместо близкого числа (которое даже выдаётся при печати c "%8.3f") получалось на 1 меньше.

      • Также похожий "сбой на 1 бит" происходит и в точке 0.3, но в обратную сторону, так что ничему не мешает.

      Опять же -- сильно легче не стало, но просто появилась ясность (вместо полумистического "как же оно догадывается, что даже при конверсии double->int надо не просто отбросить дробную часть, а округлить!").

      И понимание, что да -- надо в такие конверсии вставлять округление.

    • Итак, вставляем round() во все места, где производится конверсия.
      1. cxsd_hw.c::StoreForSending() -- под-ветка "c. Float".

        Это сразу и решило титульную проблему -- 0.7,СтрелкаВверх стало давать 0.8.

        И "внезапно" потребовалось "-lm" для drvinfo.

      2. cxsd_hw.c::ReturnDataSet() -- аналогично.
      3. cda_core.c::cda_dat_p_update_dataset() -- ветка "c. Conversion".
      4. cda_core.c::DoStoreWithConv() -- "3. Store datum, converting from double".
      5. cda_core.c::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, то начинает монотонно ехать вниз.

    Однако подумал, посмотрел -- неа:

    • Этот канал -- double (и в драйвере в т.ч.), а их нововведение с round() никак не касается.
    • Так что, скорее всего, это проявление старой (бесконечной?) проблемы с кодировкой Д16, где
      1. задержка в железке задаётся ДВУМЯ полями, и мало того, что
      2. преобразование ВЕЩЕСТВЕННАЯ_ЗАДЕРЖКА->КОДЫ_A,B неоднозначно, так ещё и
      3. сложности с округлением -- "кабы при округлении не отбросить лишнее, а то оно повлияет",
      4. плюс в поле ввода отображается обрезанное число, не со всеми значащими цифрами, а они критически важны.

    Засим считаем здешнее внедрение round() (при конверсии из вещественных в целые) к проблеме с Д16 непричастным, и пока что вроде как безгрешным.

  • 07.02.2020@дома, лестница-вниз по-дороге-с-обеда-на-работу ~14:30: можно бы ввести cxsd_driver-API-вызов "разрешать ли серверу free()'ить devptr", которому мочь указывать не только запрет, но вообще статус (можно/нельзя).
    • Чтобы это разрешение прописывалось бы в поле в cxsd_hw_dev_t, а начальным/умолчательным значением в нём было бы как раз условие, которое сейчас в TerminDev().
    • И тем самым задача корректного освобождения перекладывается на драйвер; точнее -- layer, а ещё точнее -- "забирается" layer'ом себе.

    Первоначальный смысл: чтобы драйверы (точнее, 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".

  • 12.10.2020: ещё на тему CxsdHwTimeChgBack() и "timeback":
    • Сегодня глючили драйверы источников (разных и на разных серверах) -- например, ist_cdac20 при нажатии на "On" лишь включали битик ENABLE и зависали в этом состоянии (IST_STATE_SW_ON_ENABLE=8), так никуда не идя.
    • Анализ кода показал, что так быть просто не может: там диаграмма состояний такова, что из SW_ON_ENABLE оно просто переходит в SW_ON_SET_ON -- у оного нет IsAlwd.

      ...но зато у SW_ON_ENABLE есть delay_us=0.5сек. И по прошлому опыту симптомы очень похожи на то, что бывает, когда вдруг неожиданно время скачет назад -- см. запись в конце bigfile-0001.html за 15-04-2019.

    • Как выяснилось -- да, проблема была именно в неожиданном сдвиге времени! В /var/tmp/4drivers.log на canhw сначала шли строчки от
      2020-10-12 17:02:57
      а потом, безо всякого перехода/предупреждения -- от
      2020-10-12 15:06:57

    12.10.2020: некоторое обсуждение и появившиеся по результатам произошедшего мысли.

    "Обсуждение":

    • Не вполне ясно происшедшее: почему оный сдвиг времени происходил прямо кабы не специально в момент старта сервера?
    • Вечером: позвонил Федя, результат расследования -- на canhw и cxhw просто отсутствовал ntpd или какая-нибудь подобная служба. Но почему произошёл вдруг скачок времени -- всё равно непонятно.
    • Ещё чуть позже: пошарился я в/var/log/messages на canhw -- виновный нашёлся! Это некто chronyd; причём 15-04-2019 виновник был он же и сдвиг времени (в СИСТЕМНОМ логе) был такой же -- -7 часов. Вот полный фрагмент сегодняшнего лога:
      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
      

    А теперь мысли:

    1. СОБЫТИЯ сделать, раздавабельные всем драйверам? misc-event-handler (ioctl наоборот)?

      Но это -- ОЧЕНЬ много возни и проблем: надо КАЖДЫЙ драйвер обучать данным фокусам (которые вообще-то сильно противоречат красивой модели драйверов/данных).

      Так что самое

    2. ПОЧЕМУ произошло? Почему vdev при получении новой команды (SW_ON) не запускал новый таймаут?

      Он что, не при каждом vdev_set_state() текущее время получает (для вычисления времени окончания ожидания), а ведёт счёт от какого-то предыдущего?

      Или это как-то связано с cxscheduler'ом? Например, как-то некорректно работает очередь таймаутов?

    3. Связанная тема: ПОСИНЕНИЕ скринов через некоторое время и отсиневание после движения мышью.
  • 13.02.2021@дорога домой от родителей, ~17:30, дома АЖС-1 на Коптюга: идеологическо-технологическое -- в чём отличие СИНХРОННОГО вызова чтения у драйверов (когда они обязаны СРАЗУ вернуть ответ) от АСИНХРОННОГО, когда ответ может быть вёрнут и позже, но реально вертается сразу?

    А отличие в том, что возврат производится ещё ДО возврата из вызова 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", генерить событие.

    Есть, конечно, и некоторые сомнительности:

    1. На производительности это отразится негативно. Но вроде бы и фиг с ним, некритично.
    2. Как обращаться с этими флагами с точки зрения ре-инициализации устройств -- всякие TerminDev()/FreezeDev()/ReviveDev()?

      По идее, кабы не надо их сбрасывать, аналогично некоторым другим (cycles?). Но как это будет взаимодействовать с "основной" работой этих флагов?

    3. А когерентность данных? События могут начать приходить не совсем в том порядке, в котором они реально происходили.

    13.02.2021@дорога домой от родителей, ~17:40, стадион НГУ: ответ на вопрос #2 -- более НИГДЕ не сбрасывать флажки, а только в CxsdHwDoIO().

    Дома-вечером: неа -- не прокатит: тогда первый же после того "оживляжа" CxsdHwDoIO() после первого же возврата из do_rw() сразу подумает, что якобы значение свежевёрнуто и сгенерит событие, хотя реально ничего НЕ вёрнуто... А если прямо перед вызовом do_rw() флаг "being_returned" всё же принудительно нулить?

    13.02.2021@дома, ~20:00: а вообще -- насколько данная проблема действительно актуальна? Или она скорее ПОТЕНЦИАЛЬНАЯ, и проще вообще на неё забить?

  • 18.03.2021: нужна возможность по-девайсно помечать устройства как "readonly" -- чтобы их w-каналы считались бы r-каналами (т.е., чтобы прямо сам сервер бы рассматривал бы запросы на запись в эти каналы как запросы на чтение).

    Побудительный мотив -- для bridge_drv: чтоб можно было миррорить устройства по тем же devtype'ам, но в readonly-режиме.

    18.03.2021: решение будет состоять из 3 компонентов:

    1. Поля "readonly" в CxsdDbDevLine_t и cxsd_hw_dev_t тривиально.
    2. Опознавание символа префикса в cxsd_db'шном dev_parser() -- тоже тривиально.
    3. Учёт флага "readonly" в CxsdHwSetDb() -- вот тут будет главная работа.

    Нюанс:

    1. такие экс-w-каналы надо будет помечать как "autoupdated_trusted"?
      • Да нет, не факт -- ведь хотя бы ОДИН раз чтение для них вызвать бы требуется (речь не про bridge_drv-каналы -- там эта ответственность на сервере "реального" устройства).
      • Но:
        1. бесконечный возраст свежести (fresh_age=0) им ставить надо -- а это как раз TRUSTED;

          Чуть позже: а для rw-каналов и так форсится fresh_age={0,0}. Так что можно в конкретно том условии просто не учитывать флаг "readonly".

        2. Ежецикленно слать запросы на чтение незачем -- а это как раз AUTOUPDATED...

    Итого: проект выглядит не очень сложным, но надо будет очень аккуратно реализовывать "мясо" учёта флага.

    18.03.2021: чуть позже: однако всё же не так уж "не очень сложным" -- остаётся нерешённая задача "а кто же будет требовать начального чтения экс-rw-каналов?". Да, этот вопрос актуален скорее для НАСТОЯЩИХ устройств, а не для бриджуемых (там это ложится на сервер "реального" устройства), но тем не менее.

    Как вариант: а может, учитывать "readonly" не в CxsdHwSetDb(), корёжа поля rw, а уже в ConsiderRequest()? Неа -- некрасиво, ОЧЕНЬ некрасиво (и по отсутствию оптимальности (лишняя операция для каждого канала), и по возникающей необходимости знать устройство-владельца канала).

    Уж скорее -- ввести ещё одно поле-флаг в cxsd_hw_chan_t, указывающее на необходимость выполнить начальное чтение несмотря на не-rw'шность.

    Как бы то ни было: надо начать реализацию с простого (пп.1, 2), а потом уже попробовать аккуратно всё сделать в CxsdHwSetDb(); возможно, какое-то решение "проблемы 4" придёт в голову само, а если нет -- ну тогда придётся ещё подумать.

    19.03.2021@ночь, пробуждение между снами: пара размышлений по теме:

    1. Проблема "кто же будет требовать начального чтения экс-rw-каналов?" почти исчезает, если взглянуть внимательнее -- с другой стороны: ведь технически-то запрос выполняет вовсе не ConsiderRequest(), а ReqRofWrChsOf()! И для неё -- очень редковызываемой -- совсем несложно запросить чтение любых каналов, включая не-rw, а уж CxsdHwDoIO()+ConsiderRequest() с радостью его исполнят.

      Вопрос останется скорее в том, как помечать в cxsd_hw_chan_t такие экс-rw-каналы. Можно заюзать зарезервированное поле res3.

    2. Был также вопрос -- тут не запротоколированный -- а как указывать readonly'ness устройств префиксом: во-первых, каким символом, а во-вторых, не будет ли это конфликтовать с префиксами '-'/'+'. Так вот:
      1. Символ можно взять почти от балды -- пусть хоть '!'. Поскольку с обычными @-префиксами для cdaclient etc. эти символы никак не соотносятся.
      2. И "конфликтовать" никак не будет, поскольку использовать эти префиксы вместе никакого смысла нет:
        • Суперсимуляция -- '+' -- вместе с readonly'нностью вообще смысла не имеет (суперсимуляция -- ВСЕ каналы становятся записываемыми, с readonly никак не сочетается).
        • Просто симуляция -- '-' -- ещё бы что-то как-то (читаемые симулировать, а в писуемые просто не пускать писать), но глубокого смысла в таком применении нет.

        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@ночь -- засыпая, пробуждения между снами, утро: ещё мысли по реализации:

    1. rw-каналы в режиме ro -- это именно эффективно AUTOUPDATED_TRUSTED (только вначале -- в ReviveDev() -- чтение форсить всё же надо).

      Так что им НАДО взводить флажок is_autoupdated.

    2. Но ConsiderRequest() на запрос чтения is_autoupdated-каналов всегда возвращает DRVA_IGNORE.

      Так что придётся добавить туда дополнительное условие, расширив до

      (chn_p->is_autoupdated && chn->p->rw_readonly == 0)
    3. И навороченность условий там уже такая, что возникает мысль: а возможно ли переделать логику в ConsiderRequest() на "систему битовых масок", с булевскими операциями и ПРЕДВАРИТЕЛЬНЫМ обсчётом? Т.е., чтобы
      • "Тип" канала был бы маской, его "текущее состояние" тоже, и "операция" тоже.
      • Чтобы проверка в Consider'е сводилась бы к чему-то вроде "ТИП&ОПЕРАЦИЯ&~СОСТОЯНИЕ!=0".
      • Очевидно, что rd_req и wr_req при этом превратятся в битики в маске СОСТОЯНИЕ.

      Идея красивая, но насколько она реализовабельна -- хбз. В частности, в эту модель масок плохо укладывается проверка на "upd_cycle == current_cycle ...".

      Но красивая, да.

    20.03.2021: делаем:

    • "Мясо" в CxsdHwSetDb():
      • Копирование значения is_readonly из БД.
      • Введено effective_rw, вычисляемое 1 раз и используемое 3 раза.
      • Для искусственно-readonly-каналов, каковыми считаются "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".

    Надо будет ещё в реальной обстановке потестировать пошире.

  • 22.03.2021@~23:00: у троицы CxsdHwResolveChan(), CxsdHwGetCpnProps() и FillPropsOfChan() исправление типов строковых return-параметров: они переделаны с "char **" на "const char **" -- в порядке управильнивания.
  • 30.03.2021: ЕманоФедя в мыле от 21-03-2021 высказал возмущение на тему "почему это процессинг алармов и диапазонов делается в клиенте, причём даже не на cda, а не в сервере?".

    Текущая реализация в значительной степени обусловлена ИСТОРИЧЕСКИМИ причинами -- ва ВЭПП-5 в начале 2000-х был изрядный бардак и полная неопределённость с конкретными потребностями (не говоря уж об отсутствии внятного проекта, из которого и следовали бы конкретные диапазоны и правила алармов, как это есть на всяких NSLS-II); на ЛИУ-2, кстати, ситуация повторилась в ещё более неприличном виде.

    Но конкретно сейчас -- в принципе, в инфраструктуре CX есть все компоненты, потребные для реализации фединых хотелок, которые таковы:

    Я, понимаю, что cda - не должно знать что там эти данные означают, а
    должно просто доставить и всё.
    А вот сервер мог бы:
    - знать про сущность данных, и ставить алармы и прочее для стандартных случаев.
    - позволять сервер-сайд модули/скрипты для предобработки данных, и
    выставления алармов и прочего
    в случаях требующих индивидуального подхода.
    В тех же постгресах и http-серверах и прочем аналогичные возможности есть.
    

    Так что вполне можно рассмотреть такой проект.

    30.03.2021: (реально в голове это сформулировалось ещё несколько дней назад, чуть ли не сразу после фединого возмущения, но записать руки дошли только сейчас) конкретные соображения:

    • Имеющиеся компоненты инфраструктуры:
      • Возможность указания диапазонов в конфигах -- парсинг в _db и передача для использования в _hw.
      • Передача диапазонов по сети как атрибутов каналов (если понадобится).
      • Модульная архитектура с плагинами -- конкретно есть понятие "extensions", пока что никак не используемое.
      • Возможность адресации к компонентам диапазонов индивидуально, как к каналам (это была одна из целей внедрения варианта "через handle'ы").

        21.04.2022: ПОТЕНЦИАЛЬНАЯ возможность адресации.

    • ХРАНЕНИЕ: в буфере (причём прямо по размеру конкретных dtype, а не CxAnyVal_t), а в cxsd_hw_chan_t только int-оффсеты (аналогично строкам).

      21.04.2022: да-да -- работа по хранению как раз для свежевводимого binbuf.

    • "Процессинг": иметь в cxsd_hw_chan_t указатель на обработчик, который если !=NULL -- то вызвать его в момент после складирования данных в ReturnDataSet(), и он может как-то вернуть результат (или сразу сложить в каналовы данные?).
    • Указатель этот будет прописываться CxsdHwSetDb()'ой на основании ТЕКСТОВОГО указания из конфига (которое может передаваться как обычная strdb-index'ed-строка).

      И там могут быть наиразнейшие спецификации, вроде:

      • "is_alarm" -- означает, что взводить бит CXCF_FLAG_ALARM_ALARM при наличии бита CX_VALUE_LIT_MASK.
      • "check_ranges" -- проверять попадание значений в диапазоны и при выходе за них взводить CXCF_FLAG_COLOR_RED или CXCF_FLAG_COLOR_YELLOW.
      • Какая-то возможность указывать из плагинов: то ли просто строка и по ней должен быть поиск, то ли "plugin:ИМЯ".
      • Некоторый вопрос с комбинированием: сейчас-то datatree'шная обработка позволяет навешивать одновременно И alarm, И раскраску.

        (Это используется в v5kls.subsys для дополнительной подсветки конкретной аларм-лампочки).

        А вот с таким плагинингом -- КАК комбинировать?

      Схема с указателем -- для скорости, и можно даже ради оптимизации использовать РАЗНЫЕ обработчики для разных размеров (int8, int16, int32, ...).

    • Осознано только сейчас, при записи: проблемой для работы с диапазонами может быть то, что они указываются в "логических"/ОПЕРАТОРСКИХ величинах, а сервер работает с "физическими"/ИНЖЕНЕРНЫМИ.

      Другая -- и более сложная -- часть этой проблемы в том, что для многих устройств диапазоны навешиваются не на аппаратные каналы, а на cpoint'ы, которых в сервере как ОТДЕЛЬНЫХ сущностей -- НЕ существует. Так что и процессинг, получается, навешивать физически не на что.

    • Индивидуальная адресация по сети:
      1. Вводятся дополнительные предопределённые имена, начинающиеся с '_'.

        Например, вроде "_norm_min" и "_norm_max", т.е., совпадающие с окончаниями соответствующих DATAKNOB_PARAM_* (а для нынешних cxsd_hw_chan_t.range[] -- очевидно, "_alwd_min"/"_alwd_max").

      2. Добавляется новый CXSD_DB_RESOLVE_-вариант -- CXSD_DB_RESOLVE_PARAM (=+5?). Так что незнакомые с ним "юзеры" (вроде "классического" варианта cxsd_fe_cx или cda_d_insrv, которому оно не надо) будут воспринимать такой результат как "негодный" для них и считать канал ненайденным.

        В самом же CxsdDbResolveName() потребуются такие модификации (вот это всё продумано, с анализом кода, уже сегодня, 30.03.2021, а не только записано):

        1. Для возврата "номера параметра" придётся добавить ещё один return-аргумент самой функции (par_n_p?).
        2. Обращение с finding_special чуток изменится:
          • Надо будет выделить "дополнительный диапазон" -- например, что с +1000 идут номера параметров, а не спец.каналы.
          • И соответствующее обращение: в точках перед возвратом проверять, что если значение finding_special из "диапазона параметров", то проверить наличие оного параметра, и если его нет, то вернуть RESOLVE_ERROR, а если есть, то "*par_n_p=finding_special-1000" и вернуть RESOLVE_PARAM.
          • Ну и отдельно в ВИРТУАЛЬНОЙ иерархии проверять -- там есть доп.условие.

      Тут есть некоторый минус:

      • Раз cda_d_insrv НЕ получит умения работы с такими именами, то возникнет неэквивалентность протоколов -- нельзя будет одинаково и взаимозаменяемо использовать ссылки "cx::" и "insrv::" для всяких vdev-драйверов.
      • Но конкретно для ДРАЙВЕРОВ-то это вроде и не нужно: драйверам просто незачем адресоваться к параметрам как к каналам.
      • А кому это может быть нужно -- скринам, используемым посредством утилиты pult: если будем когда-нибудь делать маппирование таких параметров на стандартные параметры ручек, то оно станет необходимым.

        Или даже просто в каких-то случаях/устройствах такая адресация к параметрам почему-либо станет нужна -- ну мало ли, прямо на панель скрина понадобится что-то важное вывести.

      • Ну тогда придётся и cda_d_insrv.c научить.

      04.04.2021: а вот есть с индивидуальной адресацией одна засада:

      • Если мы будем разрешать не только чтение, но и МОДИФИКАЦИЮ (на что cxsd_fe_cx'ный вариант "через handle'ы" был рассчитан изначально), то пидётся как-то ловить события "изменение параметров" -- чтобы при подобной модификации уведомлять всех клиентов.

        И вводить новый код события "смена параметров"?

      • Плюс мини-проблема -- а при параметров когда они должны "вступить в действие": по следующему обновлению (это легко и будет автоматом) или сразу (вот это проблема -- процессинг повторять? а отправку?).

    30.03.2021: чуть позже -- размышления о проблеме "как быть, если навешивать свойства придётся на cpoint'ы, а не настоящие каналы":

    • Можно свалить процессинг на frontend'ы. У них-то и cpoint'ы являются равноправными объектами -- там и наборы {R,D} свои, вот можно и процессинг там же делать.

      Но это фигово. И даже ОЧЕНЬ криво. В частности, тем, что тогда либо этой получаемой frontend'ами информации не будет у остальных (всяких cda_d_insrv потенциального и cxsd_fe_epics), либо придётся в них всю эту работу дублировать.

    • Идея в продолжение, но уже насчёт "множественности" -- комбинирования разных обработчиков. Идея возникла по результату обмышления предыдущего пункта, где frontend'ы добывают информацию "сервисными" вызовами.

      Так вот: для добычи цепочек {R,D} используется вызов, который "раскручивает" цепочку cpoint'в.

      А если и обработчики тоже реализовывать не "один делает всё-всё", а "маленькими кирпичиками" -- один занимается alarm'ами, другой проверяет диапазоны, ... -- и составлять ЦЕПОЧКИ таких обработчиков, которые и будут вызываться последовательно по списку, пока список не закончится.

      Как это реализовать технически:

      1. В dcpr'ах указывать тип процессинга списком строк через запятую -- вроде "is_alarm,check_ranges".
      2. В cxsd_hw_chan_t добавить 2 поля: processers и processers_count, первое из которых является указателем на "массив"-последовательность указателей на функции, количеством во втором поле.

        И храниться эти "массивы" будут в общем cxsd_hw_buffers (с понятными требованиями по выравниванию), а CxsdHwSetDb() будет их пллокировать и заполнять обычным образом (высчитывать потребный объём на stage=0 и заполнять на stage=1).

      Пара замечаний общего характера по идеологии:

      1. Это напоминает технологию, используемую в STREAMS (ссылки: Wikipedia, "A Stream Input-Output System" от Денниса Ричи; я-то читал то ли на Sun'е в Solaris, то ли на Sky в IRIX (не помню название helptool'а), т.к. там постоянно бросался в глаза раздел вроде "Sockets to TLI migration" -- совершенно бессмысленный, ибо миграция реально шла в строго обратном направлении).
      2. Такие обработчики должны быть очень лимитированы в том, что им можно делать: реально -- ТОЛЬКО статическая обработка, но никак не операции В/В с каналами.

        Смысл -- предотвращение ситуаций, когда вызов обработчика приведёт к созданию/удалению каналов и/или 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'ы, глядящие на аппаратный канал, в связанный список того канала, и по его обновлению вызывать "процессинг" для всех элементов списка?
    • 31.03.2021@ночью-между-снами: всё равно остаётся вопрос "а ЧЕГО делать процессинг -- если cpoint'ы являются лишь ссылками, но НЕ самостоятельными объектами с данными?".

      Напрашивающийся ответ -- только придавать им ДАННЫЕ: т.е., превращать 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, чья инкарнация".

    • @~14:20, когда записывал: кстати, такое расположение "инкарнаций" среди обычных каналов даёт простой и элегантный способ реализации проекта от 30-03-2021 «не помещать ли все 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);
        
        -- без каких-либо лишних/специальных условий. Таким образом будут отпроцессены ВСЕ зависимые псевдоканалы, даже если на базовом канале и не висит никаких ни параметров, ни обработчиков.

    Потенциальные проблемы:

    1. Слом совместимости по протоколу GURU: нет, не произойдёт, т.к. там обмен идёт ДРУГИМИ, специализированными структурами данных, а не _db'шными/_hw'шными, и содержащими лишь краткие выжимки из последних.
    2. А вот то, что при обращении к cxsd_hw_chan_t'ам используется ихнее поле devid для доступа к драйверам -- вот это уже реальная сложность.

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

    Но эти-то проблемы чисто технические и вполне решаемые.

    А вот ГЛАВНАЯ проблема, с {R,D}, никуда не девается -- что диапазоны-то указываются в ОПЕРАТОРСКИХ, а не в аппаратных величинах...

    04.04.2021@~18:30, моя посуду: с таким подходом -- "оживлять cpoint'ы по мере надобности" -- мы потихоньку двигаемся к совсем другой архитектуре: не "есть набор устройств, дающих аппаратные каналы, и есть набор alias'ов для этих каналов", а уже ?????????????????????

    09.04.2021: Федя изъявил желание иметь и проверяние ПЕРЕД записью. И в т.ч. список разрешённых значений.

    • Добавить к "processer'ам" ещё и "forcer'ы"?
    • А им могут понадобиться описания "правил" (тот самый список значений), плюс приватные данные (куда отпроцессенный складывать).

      ...формально, кстати, и "processer'ам" тоже.

    • Сделать "обработчик" объектом, с кучей методов -- "check_and_get_private_size", "process"?

    Только вопрос -- а как реализовывать такую "фильтрацию записи" для cpoint'ов, которые могут быть и многоуровневыми? Там ведь на КАЖДОМ этапе могут иметься свои ограничения, и по-хорошему их надо бы применять друг за дружкой, по пути "раскрутки". Но сейчас запись выполняется "по gcid'у".

    • Теоретически, в cxsd_fe_cx "раскрутка" cpid->gcid производится прямо по получении команды записи, так что информация о "цепочке cpoint'ов" есть;
    • но далее это всё передаётся в CxsdHwDoIO(), и уже ОНА проводит проверку dtype, nelems и т.д., а у неё доступа к информации о "цепочке" уже нет.

      ...как-то всё же передавать оную информацию?

    • А вот в cda_d_insrv "раскрутка" делается прямо в момент регистрации, так что там уже увы...

      ...хотя cpid там тоже сохраняется (для добычи строк и {R,D} при их изменениях), так что необходимая для "прохода" информация есть.

    Тут идеологический/технологический нюанс в том, что:

    1. С одной стороны, наиболее "красиво" было бы исполнять форсинг прямо на переданных клиентом для записи данных -- прямо в этом буфере.
    2. С другой стороны, правильным было бы совместить оный форсинг с точкой, где при надобности выполняется ПРЕОБРАЗОВАНИЕ типа этих данных -- т.е., в StoreForSending().

    10.04.2021@утро: если хорошенько поразмыслить, то

    1. ПОСТпроцессинг после ЧТЕНИЯ -- смысл имеет: это некое преобразование/обсчёт полученных данных, и тут возможно широчайшее разнообразие.
    2. А вот плагин-чекинг ПЕРЕД ЗАПИСЬЮ -- уже сомнительно: вообще-то насчёт допустимости записываемых значений у нас царь и бог -- ДРАЙВЕР, только он достоверно знает, что можно, а что нельзя.

    Так что если уж сильно надо что-то такое нестандартное -- то надо делать специальный драйвер, проверяющий/обрабатывающий получаемые для записи данные и затем переправляющий их в реальную железку.

    А эти "плагины для чтения" -- кстати, по факту тоже будут иметь функциональность/сложность, примерно соответствующую таковой у драйверов. Разница лишь в форме: драйвер предполагает работу с некоей картой каналов, образующей устройство, а плагин-чекер может быть применён к произвольному каналу (и произвольного готового устройства).

    10.04.2021@17:10, мытьё посуды: кстати, насчёт конкретно ALARM'ов: ведь у нас они -- те, которые реально работают как alarm'ы, с бибиканьем -- отдаются прямо драйвером, именно сразу как специальный канал, а не просто некий битик входного регистра. Оно так ВЭПП-5 в:

    1. Вакуум линака+накопителя.
    2. Клистроны.

    Так ничто ж не мешает прямо ДРАЙВЕРАМ и возвращать вместе со значением 1 и rflags=CXCF_FLAG_ALARM_ALARM!

    Есть, конечно, и сложности:

    1. В вакууме половина сделана не на caniva со своими драйверами, а просто на CEAC124, и "алармы" создаются именно из битиков.

      Тут единственное решение -- изготавливать vdev-драйвер вроде "vip_ceac124", который и будет содержать таковые мозги.

    2. У скринов клистронов есть отключение звуковой сигнализации -- локальным %-регистром, на который отображается экранная ручка, и в зависимости от этого регистра просто НЕ выставляется битик по маске 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.
      • Напрашивается мысль «описывать devtype с клиентским типом, а драйвер пусть вертает аппаратные, и они будут преобразовываться/расширяться».
      • Но проблема в том, что числа-то ВЕЩЕСТВЕННЫЕ, а с ними простое расширение не прокатывает -- число может попортиться.
    • Но ГЛАВНАЯ идея, в самом общем случае -- что такими "процессерами" можно выполнять НЕтривиальные преобразования, не укладывающиеся в {R,D}-конверсию.
  • 08.10.2021: замечена такая особенность текущей реализации 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,):

    • ДО фикса устройство "оставалось живым" -- точнее, "убивалось и тут же возрождалось к жизни".
    • ПОСЛЕ фикса оно так и оставалось OFFLINE.

    (Методология теста такая: при запущенном сервере из одной консоли cdaclient натравливается мониторить каналы _devstate и _devstate_description, а из другой делается _devstate=0 (рестарт) -- при этом ДО фикса статус щёлкался в -1 и потом сразу же в +1, а ПОСЛЕ прыгать в +1 перестал.)

  • 09.10.2021: и ещё заметил, что конкретно InitDevice()'овское сообщение о "refusal" (что init_dev() вернуло <0) выдаётся БЕЗ имени экземпляра устройства. Видимо, потому, что оно выдаётся не DoDriverLog()'ом, а самостоятельно, с помощью logline() (это оно так ещё со времён CXv2, и содержимое скопировано из cxsd_drvmgr.c::InitDev() -- там про psp_parse(auxinfo) ругательства выдавались DoDriverLog()'ом, а конкретно это -- вручную).

    И сие весьма неудобно -- что поиском по имени устройства найдётся не всё, к нему относящееся.

    10.10.2021: исправлено: теперь имя устройства выдаётся -- сразу после "[DEVID]", как и в DoDriverLog()'овских сообщениях (только после него идёт ".init_dev()=", а не "/КАТЕГОРИЯ").

cxsd_module -> cxsd_modmgr:
  • 07.02.2010: кусочек, обеспечивающий унифицированную работу со всеми загружабельностями сервера -- драйверами, layer'ами, frontend'ами, extension'ами, lib'ами.

    Фактически представляет из себя определение "общего" заголовка 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 дёргать было некого).

    • ВЕСЬ функционал сведён в cxsd_modmgr.c, определения в cxsd_modmgr.h.

      Раньше оно было весьма прихотливо раскидано по разным модулям в libcxsd и даже в programs/server/.

    • Выкинуты: сама библиотекова cxsd_module.[ch], плюс серверовы cxsd_loader.[ch] и cxsd_fndmgr.[ch].
    • Бывшая CxsdHwSetPath() превратилась в CxsdSetDrvLyrPath() (надо бы и для прочих загружабельностей аналогичные CxsdSetNNNPath() сделать).
    • Номер instance указывается в вызове CxsdActivateFrontends(), а в самой libcxsd никак иначе не фигурирует.
    • В интересах cxsd_hw введены CxsdCallFrontendsBegCy() и CxsdCallFrontendsEndCy().

    И раздел переименован в "cxsd_modmgr".

  • 28.10.2014: добавлено правило, что при любых обломах кроме ругани также делается errno=-1 -- чтоб "юзер" (например, cxsd_hw) знал, что ошибка уже залоггирована.
  • 17.02.2022: пришлось в 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:
  • 01.04.2020: приступаем к реализации access-control'а, так что создаём раздел.

    Модуль назовём "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@утро: а вот и нет -- есть ОЧЕНЬ существенные отличия:

    1. Введено понятие "дискретных прав", позволяющих гибко разрешать/запрещать разные операции.

      В cxnetacl же была предусмотрена единственная булевская IsAllowed().

    2. Access-control-list держится в памяти (после считывания вначале), что существенно ускоряет работу.
  • 04.04.2020: за прошедшие несколько дней сделано.

    04.04.2020: highlights:

    • Ключевое изменение по сравнению с версией из старого CX (или даже ещё UCAM): файл теперь читается не при каждом коннекте клиента, а только при старте сервера, складывается в память, а при обращении клиента лишь выполняется поиск по считанному в память.

      Связанные с этим аспекты:

      • "Нулевой" пункт -- идеология работы с этими "access-list'ами" была взята от cxsd_db:
        • Есть "объекты", которые можно считывать из файла.
        • Инфраструктура чтения -- плагинная, аналогично cxsd_db'шной, с plugmgr'ом; только схема по умолчанию -- "file:" -- использует ppf4td-схему "plaintext", а не "m4".
        • Есть вызов CxsdAccessLoadList(), считывающий объекты, ...
        • ...а потом можно сделать этот объект "текущим используемым" в libcxsd при помощи CxsdAccessSetDefACL().
        • Сервер можно заставить пере-считать файл контроля доступа, прислав ему SIGUSR1 (и при ошибке чтения остаётся старый ACL).
      • За хранение в памяти отвечает тип CxsdAccessList.
      • Поиск по указанному ACL'ю -- CxsdAccessCheck().
      • При НЕуказанности ACL'я -- используется "текущий умолчательный".
      • При отсутствии умолчательного ACL'я (т.е., файл отсутствует и потому cxsd_access_default_acl==NULL) возвращается "всем всё можно".
      • Также можно установить, какие права возвращать "по умолчанию" -- при ненайденности адреса в списке; для этого служит CxsdAccessSetPolicy(). Сейчас это реально не используется.
    • Второе ключевое изменение -- вместо просто разрешено/запрещено теперь возвращается битовая маска "прав доступа", а пользователь маски уже сам решает, что с этим делать -- что разрешено, а что нет.

      Это сделано для реализации режима "readonly".

    • Собственно биты прав сейчас заведены такие:
      • CONNECT
      • READ - не используется (предполагается, что раз уж есть CONNECT, то и оно тоже будет).
      • WRITE
      • ADMIN - тоже пока не используется. Предполагалось как отдельное право на запись в каналы _devstate.
    • В cxsd_fe_cx.c добавлено:
      • ServeGuruRequest() игнорирует запрос при отсутствии PERM_CONNECT.
      • ServeIORequest() игнорирует запрос за запись при отсутствии PERM_WRITE у данного клиента.

        Игнорирует МОЛЧА, без сообщения типа "permission denied".

      • А AcceptCXv4Connection() выполняет роль стража/привратника: узнаёт права свежеприконнектившегося клиента, посылает его с CXT4_ACCESS_DENIED, записывает права в метрику клиента при её создании и заполнении.
      • Для хранения прав в v4clnt_t добавлено поле perms.
    • В cxsd_festarogate.c добавлены аналогичные проверки на PERM_CONNECT (только сокет закрывается молча) и PERM_WRITE (ругательство с кодом 403).
    • Изменения на клиентской стороне:
      • Добавлен новый код CAR_EACCESS, ...
      • ...генеримый отдельным условием по CEACCESS, ДО общего CAR_CONNFAIL.
      • Чтобы cda_d_cx НЕ пытался реконнектиться. И в нём по CAR_EACCESS лишь выдаётся ообщение об обломе, с пометкой "will NOT reconnect.".
      • Кстати, линейный массив _cx_carlist[] (индексировавшийся по коду) пришлось переделать в набор дуплетов _cx_cardescrs[] -- а то старая схема с индексированием была неадекватна ещё с момента введения CAR_DATA.
    • В cxsd.c добавлена надлежащая обвязка, включающая чтение файла cxhosts при старте и перечитывание по SIGUSR1. А из cxhosts_file_parser() открытие плюс warning при неудаче убраны, теперь это не его забота.

      Слегка в сторону: PerformAccess() была переименована в PerformAccessToFiles(), для определённости и отсутствия странных ассоциаций.

    • А вот stand.c ничему этому так и не обучен -- считаем, что не требуется.

    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.
    • Проверка в cxsd_fe_cx теперь требует именно бита PERM_LOCK, а PERM_WRITE неважен.
    • В результате теоретически возможна странная комбинация прав: взведынный LOCK, но отсутствующий WRITE. Т.е., можно сделать "евнуха-охранника", не могущего писать, но при этом могущего не давать писать другим.
:
:
cxsd:
  • 11.07.2009: типа "создаём" этот раздел. Директория-то programs/server/ существует уже давно, и cxsd давно запускается, но лишь в качестве макета.

    Теперь же мы приступаем к реальному его наполнению. И -- большое толстое замечание: сервер теперь является лишь юзером библиотеки libcxsd, а основная работа будет идти именно там.

  • 06.11.2013: уже некоторое время ведём работы по более свежей версии в соседней директории nserver/, совместно с lib/nsrv/.

    21.11.2013: функционал старых lib/srv и programs/server на новом уровне уже воспроизведён, так что пора двигаться дальше (тем более, что пришла пора наращивать функционал, с изменением include/*.h-файлов, а отражать это в старых уже нет смысла).

    Для непотери старый rrund.c переселён и в новую директорию.

    Старые директории удалены, а на их место переименованы n-директории.

  • 06.11.2013: радикальное решение -- устранена отдельная LOGF_DEFAULT (отдельная от LOGF_SYSTEM).

    Она всё равно никогда и нигде не использовалась, /var/tmp/cxd.log всегда был пустым.

  • 07.11.2013: всякие действия по оживлению cxsd (реально ведутся уже несколько недель).

    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".

    И вообще -- ОЧЕНЬ не хватает доделанности "конструктора из модулей", чтоб просто указывать список нужных модулей, а они б все влинковывались и инициализировались автоматически.

  • 23.12.2015: config-директивы frontends-path, exts-path и libs-path были по факту нерабочими.

    Причина -- никакое CxsdSetNNNPath() (NNN={Fnd,Ext,Lib}) не делалось. И даже если бы делалось, то не работало бы, поскольку рассчитано всё было на делание ПОСЛЕ пансинга конфига, а парные команды load-frontend и прочие вызывают загрузку СРАЗУ, в момент парсинга.

    23.12.2015: сейчас сделано по-простому -- все NNN_path_parser() вызывают CxsdSetNNNPath() сами сразу. Так что всё заработало.

    Но по-хорошему надо бы load-NNN не сразу выполнять, а запоминать в списки "требуется загрузка" и потом ПОСЛЕ парсинга конфига вызывать уставку путей и загрузку модулей.

  • 03.09.2016@Снежинск-каземат-11: насчёт "publics" -- функций, не используемых в сервере, но требующихся драйверам.

    Вот сейчас piv485_lyr'у понадобился n_write(). Потому добавляем её во все 3 места:

    1. cxsd_publics.c (был за 03-02-2010).
    2. drvinfo_publics.c (был за 07-01-2010).
    3. stand.c.
  • 15.10.2019: есть некоторое неудобство в том, что СНАЧАЛА делается 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);}
    

    Какие есть соображения по её поводу:

    1. Во-первых, option_norun можно с чистой совестью отрабатывать первым -- демонизироваться ДО неё незачем.

      Вероятно, это наследие от v2'шного zzz/cxsd.c, где так было, из-за неразделённости операций "ReadHWConfig()" и "ActivateHW()" (активация устройств выполнялась по мере чтения).

      С другой стороны, это вроде никак принципиально не мешает.

    2. А вот можно ли поставить активацию frontend'ов ДО демонизации -- хбз.

      Пробуем?

    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)'ы). Можно ли? Или чем-то чревато?

  • 22.02.2025@идя по Морскому около ДУ: сделать бы возможность запуска cxsd БЕЗ активации cxsd_fe_cx -- чтоб можно было иметь среду-"песочницу" для жизни драйверов вроде trainer (которые бы работали с чем угодно -- например, через epics::, чтоб СКИФовские модуляторы можно было тренировать CX'ным драйвером); а остальные frontend'ы -- хоть указанные в config'е, хоть через ключ "-e" -- активировались бы (чем-то похоже на ключ "-nolisten tcp" у X-сервера).

    Ключ -N?

    28.02.2025: а вот фиг, не так-то всё просто: ведь cxsd_fe_cx никак отдельно не выделен, просто он "вшивается" в бинарник сразу, а инициализируется cxsd_builtins.c, в каковой момент и регистрируется; отличить же конкретно его сервер никак не может.

    02.03.2025: ну сделал -- ключ "-N" взводит option_no_frontends, при которой пропускается вызов CxsdActivateFrontends(). Да, можно запустить хоть несколько штук, считающих себя за ":0" -- они друг дружке не мешают.

Frontends:
cxsd_fe_cx:
  • 04.10.2009: это самый первый/главный frontend -- для протокола CXv4. Покамест он живёт прямо в programs/server/.
    30.12.2014: кстати, уже давно в lib/srv/.
  • 04.10.2009: работы над этим модулем были начаты еще с полмесяца назад, без него сервер будет бессмыслен.

    04.10.2009: за это время сделано:

    1. Cкелет --
      • инициализация,
      • слушанье пары (UNIX/INET) сокетов,
      • инфраструктура работы с клиентами (менеджмент списка скопирован из cx_client.c), ...
      • ...в т.ч. отлуп их -- DisconnectClient(), авторитетно протоколирующий объяснение (заменяет пару LogConnection() и GenericDisconnectClient() из CXv2, сочетая в себе их функционал).
      • поддержка преобразования порядка байт (пока только LITTLE_ENDIAN) -- основная работа помещена в endianconv.h (чтоб было доступно другим frontend'ам), а также есть пара (пока) функций host_i32() и clnt_i32(), которым передаётся также cp и они сразу возвращают значение в нужном порядке (для сервера и для клиента, соответственно).
    2. Плюс -- вся процедура приёма соединения (включая определение endianness клиента).

    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.

    Результат:

    1. Симметричность, с равной длиной компонентов (socket и handle)
    2. У "оракула" будет guru вместо serv.

    29.12.2014: за последние несколько дней сделана основа работы с данными -- пакеты CXT4_DATA_IO плюс мониторинг, по проекту от 25-12-2014 (в разделе cx-протокол).

    30.12.2014:

  • 02.02.2013: cx-console -- это хорошо, и полноценный консольный протокол необходим для GUI-средств администрирования.

    А можно ли реализовать еще и telnet-интерфейс, аналогично оному у remsrv? Было б крайне удобно.

    02.02.2013: вопрос в ПОРТУ. Варианты:

    1. Иметь отдельный порт -- по одному на каждый 8012+N. Какой?

      Например, собственно :N использовать только чётные, а 8012+N+1 -- для telnet-подключений.

    2. Пытаться наводить какой-то интеллект, разбирая, кто ж к нам пришёл, по присылаемому траффику?

      Плохая идея. С одной стороны, telnet-соединению бы СРАЗУ выдать какое-то issue и потом prompt. С другой -- по обычному соединению сервер должен дождаться прихода пакета EIDIANID. И что -- считать, что если за 1 секунду пакет не пришёл, то это telnet? А если клиент просто на другом континенте? Не-не-не, нафиг-нафиг...

  • 16.10.2014: начинаем делать блок "UDP-резолвера".

    16.10.2014: сделано бинденье сокетов.

    • Конкретно с UDP вышло любопытно: он позволял НЕСКОЛЬКИМ процессам захватывать один порт.
    • Как выяснилось, это следствие уставленного 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; логика в том, что он информационный:

      1. как для данного экземпляра -- отслеживаем состояние гуру, не сдох ли,
      2. так и для гуру -- он отслеживает состояние данного сервера и получает информацию о его каналах.
    • ...но коннекченье только при обломе захвата должности гуру.
    • Плюс сама "акробатика захвата должности":
      1. При обрыве info-соединения (FDIO_R_CLOSED) делается 2-шаговая попытка -- занять место, а если облом, то приконнектиться к занявшему.
      2. Периодически, раз в 10 секунд, проверяется, что если и мы не гуру, и соединения с ним нет, то то же самое. Это уже на всякий случай -- защита от всяких race-condition'ов (если победитель успел занять UDP-порт, но еще не сделал listen() unix-точке).
    • Для не-ставших-гуру выбран термин "советник" (advisor).

      Не очень удобно, правда -- лучше б на другую букву начинался, не на "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.

    • БД пересылается толпой небольших посылочек, в т.ч. chunk'и переменного размера (GAC_NMSP и GAC_CLEVEL) отправляются отдельно заголовки, а потом за ними поштучно элементы.
    • Пересылается ТОЛЬКО значащая для поиска по БД информация. В частности, changroups НЕ пересылаются.
    • Заводить базу для instance==my_server_instance не позволяется.
    • При поиске идётся последовательно по всему списку [0...CX_MAX_SERVER], себе приоритета не делается.

    Теперь о недоделанностях:

    • Проверок при приёме в InteractWithAdvisor() всё-таки маловато.
    • Проверка "а не пытаются ли нам сбагрить базу УЖЕ подключенного советника" -- НЕ делается. Старая база просто забывается.

      ...а как это проверять -- неясно.

    • На каждый chunk из клиентского запроса шлётся ОТДЕЛЬНЫЙ пакет ответа. Это сделано для простоты, чтоб побыстрее закодить.

      В идеале же надо идти по запросу и "набивать" пакет ответа до разрешенного объёма (MTU=1500); по превышению отсылать его и опять обнулять, а в конце отсылать, если не пустой.

      01.12.2017: да, поштучные ответы -- некрасиво. И еще: можно ж даже не пытаться "набивать до объёма 1500", а считать, что раз до нас пакет дошёл (т.е., пролез по MTU), то и обратный такой дойдёт -- а форматы-то (и, следовательно, объёмы) "туда" и "обратно" совпадают. ...Только неясно, насколько это хорошая идея: формально маршрутизация может быть и асимметричной.

    • Ну и на клиентской стороне реализация пока рудиментарная -- cx_resolve() просто отправляет одиночный запрос, а HandleResolveReply() печатает ответ на консоль.

    27.10.2017: небольшие модификации касательно работы с "советниками":

    1. В RlsAdvisorSlot() использовались переменные cd и cp -- очевидно, копия с RlsV4connSlot(); переименованы в jd и jp.
    2. В 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'ом).
    • Следствие: код отвечания сильно изменится.
      1. Вместо внутреннего/локального буфера в "reply" надо переходить к какому-нибудь... нет, не надо -- пусть прямо в локальном "reply" и растёт, так проще.
      2. Операции "начать пакет", "добавить chunk ответа", "отправить пакет" надо вытащить в отдельные функции -- ибо каждая будет встречаться в коде ДВАЖДЫ.
    • Побочный эффект: в один пакет будут помещаться ответы на каналы РАЗНЫХ серверов с одного IP.

    30.01.2018: кстати, глянул wireshark'ом:

    1. Да, на один сгруппированный UDP-broadcast-пакет запросов идёт толпа коротюсеньких пакетов ответов (хоть и unicast'ных, но толпа).

      Кстати, запросы скрина CANDAC16 с префиксом "icd." укладываются в 3 пакета (1456, 1472, 992 байта -- 1498, 1514, 1034 on wire).

    2. А так-то ответ по объёму идентичен запросу (прям видно сниффером), так что в идеале на ответы нужно ровно столько же пакетов, как и на запросы.

    30.01.2018: делаем.

    • "Буфер отправки" теперь называется result и содержит 2 поля: заголовок и data[CX_V4_MAX_UDP_DATASIZE]; плюс переменные result_DataSize и result_NumChunks.
    • "Операции" сделаны макросами -- BeginSearchReply() и SendSearchReply().

      Их только 2, т.к. "добавить chunk ответа" осталось прямо в теле: при схеме "всё внутри самой ServeGuruRequest()" она заранее знает, что места под chunk не хватит и сделает отправку текущего пакета и инициализацию следующего прямо сразу, ДО добавления chunk'а.

    • Итого -- сделано, работает.
    • Хотя вылез и первый косяк с "реаллокируемыми на ходу SLOTARRAY'ями": клиент начал segfault'иться на втором ответе. Подробнее см. в разделе по нереентрантности за сегодня.
    • Кстати, заметил отсутствие проверки версии. В cxlib-то заполняется поле var2=CX_V4_PROTO_VERSION, а в cxsd_fe_cx ничего не проверяется. Пока прокатит, но в будущем, при возможном изменении протокола, чревато проблемами.

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

  • 28.03.2015@утро-душ: ЗАМЕЧАНИЕ: даже на этом же узле программы могут быть иной архитектуры: либо просто иной битности (32/64), либо иного endianness (например, в виртуалке).

    Поэтому нельзя для гуру просто тупо передавать образ структуры в сокет, а надо делать полноценный маршаллинг (с целыми по 32 бита).

    30.03.2015: насчёт виртуалки, кстати, интересный вопрос -- как наша нынешняя модель "GURU" будет работать: с одной стороны, UDP/IP-порт занят (пусть и виртуалкой -- это легко возможно); с другой же, UNIX-порт в эту самую виртуалку вряд ли пробрасывабелен.

    Конечно, тогда можно пытаться майстрячить что-нибудь через IP/localhost вместо unix/; только тут уж вряд ли игра стоит свеч.

    09.06.2015: ПОКА сделано наполовину:

    1. Битность -- реализована: передаётся всё в int32.
    2. Конверсия endianness -- НЕ реализована. Ибо вряд ли понадобится (в силу вопроса "как пробросить UNIX-порт в виртуалку").
  • 28.07.2015: модификация внутренностей, вызванная необходимостью корректно обновлять свойства (строки и {r,d}) точек контроля, а не только аппаратных каналов.

    28.07.2015: обсуждение:

    1. Суть проблемы:
      • Раньше в мониторе (moninfo_t) сохранялось одно число -- globalchan, являющийся номером аппаратного канала (на который в конечном итоге смотрит cpoint).
      • В результате при начальном резолвинге клиенту отдавались полные цепочки свойств (по цепочке cpoint'ов), а при любых дальнейших обновляниях (например, при рестарте драйвлета) -- только от аппаратного канала, что давало неверные данные.
    2. Размышления:
      • Сначала при осознании этой проблемы (10-01-2015 и 27-05-2015) предполагалось, что недоработка в протоколе.
      • Но потом -- вчера, при анализе -- стало ясно, что
        1. По-хорошему -- неправильно вываливать такие интимные тонкости функционирования cxsd_db+cxsd_hw на клиента. Это вполне может делать прямо сам cxsd_fe_cx:
          • CXC_PEEK и CXC_RQWRC являются нечастыми (первая одноразова, вторая редка по сравнению с ежецикденным мониторингом).
          • CXC_RQRDC вообще не применяется (cx_rq_rd() не используется нигде).
          • CXC_SETMON столь же одноразова, как PEEK.

          Всего делов-то -- при надобности пройтись по цепочке cpoint'ов.

        2. Главная засада в самом устройстве cxsd_fe_cx -- даже безотносительно протокола: SendAReply() ВСЕГДА передаёт в качестве параметра "номер канала" именно mp->globalchan. А надо иногда именно globalchan (для PutDataChunkReply() и PutFrAgChunkReply(), имеющих дело с конечным аппаратным каналом), но иногда и cpid -- для остальных, возвращающих результаты прохода по цепочке.
    3. Процесс исправления:
      • Сделана функция трансляции -- cpid2hwid(), в будущем cpid2gcid() -- место бы ей в cxsd_hw.
      • Оная трансляция выполняется:
        1. При "одноразовых" операциях (PEEK, RQWRC, RQRDC).
        2. При установке монитора -- для сохранения значение в добавленное в moninfo_t поле и для вешания CxsdHwAddChanEvproc() на реальный канал.
        3. Увы, и в PutDataChunkReply() -- из-за обозначенной ранее проблемы с SendAReply().

          Считаем, что на производительности оно должно сказаться слабо.

          А если уж делать "как надо", то придётся "стандартным параметром" сделать не cpid, а прямо mp (что некрасиво для прямых вызовов SendNNN()'ов).

    Результат -- теперь ВСЕГДА отдаются полные цепочки свойств.

    1. hwid/gcn - gcid; cpn -- cpid. 2. cda_d_insrv -- cpid/hwid?

    29.07.2015: кстати, cda_d_insrv.c проверен -- в нём проблемы нет изначально.

  • 23.09.2015: к вопросу "о подселении сервера к готовой программе" -- а может, сделать, чтоб cxsd_fe_cx запускался в ДРУГОМ окружении: С cxscheduler+fdiolib, но БЕЗ остальной части libcxsd, с подменным cxsd_hw -- эмулятором-прослойкой, работающей как адаптер к, например, LabVIEW?

    P.S. И "guru" при этом, конечно, не нужен -- его просто вы-#if'ливать из компиляции.

    23.09.2015: идея родилась при общении с Пановым на тему "как бы поселить CX-сервер в его (пановский) крейтик, работающий под Linux-ARM, в котором софт делается в NI'ном фреймворке (уже умеющем отдавать данные по Channel Access)".

    • CX-то под тот ARM-Linux собрали, и cxsd там запустился и работает, но возник резонный вопрос: а КАК взаимодействовать cxsd с паново-NI'ным софтом?
    • И стало ясно, что весь-то cxsd там совершенно не нужен, а нужен лишь реализатор сетевого CX-протокола -- cxsd_fe_cx.

    24.09.2015: написано "программное" (memorandum-style) письмо Панову; его текст тут, заключённый в HTML-комментарии.

  • 24.05.2016: при запинывании clientside-части pzframe/fastadc выяснилось, что даже при cond==CX_MON_COND_ON_UPDATE данные из каналов отдаются не чаще, чем раз в цикл.

    Причина очень проста: запрос -- 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 у больших каналов такого не было). Что делать?

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

      Пожалуй, да, он осмыслен:

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

        Острота, конечно, снижается:

        1. для авто-обновляемых каналов,
        2. да и для обычных каналов (вроде CAN-регистров и C0609/C0612), запрашиваемых обычными клиентами: вообще-то, все запросы будут лететь друг за дружкой одной пачкой -- из cx_begin_c()/RequestSubscription(), а тут сработает отсев по rd_req (в железо уйдёт только первый запрос).
      2. В каких-то драйверах (в каких?) не-заказ-чаще-раза-в-цикл использовался как дополнительный уровень защиты.

        Хотя тут, конечно, с ON_UPDATE-подпиской такая "защита" всё равно исчезнет.

    Кстати:
    • в cda_d_insrv ведь есть та же самая проблема с запросом только раз в цикл!
    • Спасает только отсутствие потребности в таких последовательно-срочных обновлениях (точнее, отсутствие требующих оного vdev-драйверов).
    • А если вдруг его сделать в insrv, то вся CAN-линия сразу забъётся бесконечным чтением регистров.

      из чего вывод: для ТЕХ каналов надо будет отключать 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 это не коснулось, но заслуживает упоминания тут.

    Прочие подробности -- в профильном разделе за сегодня.

  • 03.06.2016: есть какая-то странность с отправкой данных: если клиенту нажать Ctrl+Z, а потом через изрядное время сделать ему "fg", то досылка данных от сервера идёт о-о-о-очень долго.

    Обнаружилось на 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.

    • Попытка разобраться на месте ничего не дала -- оно так дико тормозило, что даже strace почти ничего не показывал, так что просто грохнул его и рестартовал.
    • Позже стало ясно, что можно б попробовать посмотреть все fdiolib.c::watched[*]._sysbufsize -- кто там будет сильно крупным.
    • Идея -- что кто-то из fdiolib'ных соединений разросся до неприличия. А соединений там 2 типа: клиентские и remdrv'шные.
    • И, почему считаем эту проблему близкой к первоначальной: хотя fdiolib и умеет ограничивать максимальный размер _sysbuf'а, но само это ограничение никто не ставит -- НИГДЕ нет вызова fdio_set_maxsbuf().

      А надо бы делать.

    • Замечание:
      • Ошибка отправки -- любая, в т.ч. и по переполнению буфера -- в fdiolib'е является синхронной: она возвращается как -1 в результате вызова fdio_send(), а не передаётся асинхронно через нотификатор посредством close_because().
      • И у нас юзеры fdiolib'а практически повсеместно корректно обрабатывают такой результат и грохают соединения -- cxlib_client, cxsd_fe_cx, remdrv, remcxsd_driver_v4. Исключений (помимо мест вроде cx_close(), где соединение и так закрывается) два:
        1. cda_d_insrv и cda_d_local.
        2. remcxsd_driver_v4.c -- точки, связанные с отправкой лог-сообщения посредством vReportThat(). Последняя-то результат возвращает, а вызывальщики его не проверяют.

          Точнее даже "точкА" -- vReportThat(). Маловероятно, конечно, но такое упущение может, теоретически, вызывать проблемы -- "порча" приходящих в remdrv данных, т.к. при частичной отправке после начала пакета потом может добавиться (вместо невлезшего конца того же пакета) начало следующего, а уже его конец будет воспринят как заголовок очередного -- и получится мусор.

          (Второй юзер там remcxsd_report_to_dev(), используемый для выдачи сообщений о фатальной ошибке непосредственно перед FreeDevID().)

    30.03.2017: продолжение истории -- теперь с linac1:11 и конкретным клиентом linmag, работающим на ichw1-2 под x2go-сервером (для Raspberry-PI, юзер pi1).

    Как проходило разбирательство:

    • С чего всё началось: на пультовом linmag'е каналы частенько посиневали, а top показывал, что cxsd-linac1:11 жрёт больше 1GB оперативки и процентов 20-50 процессора (под конец дошло до рывками до 99%).
    • На этот раз БЫЛИ в gdb посмотрены все watched[*]._sysbufsize, и нашелся один с аномально большим значением -- больше гигабайта.

      Сверка егошнего .fd со списком всех cx4clnts_list[*].fd и помогла найти клиента.

      Кстати, сама такая разборка -- просматривать список глазами для сличения -- крайне неудобна. Стоит прямо в cxsd_fe_cx.c::HandleClientConnect() логгировать также и fd и fhandle. 30.03.2017: после обеда -- сделано, логгирует.

    • Клиент -- посмотренный на ichw1-2 по PID'у из лога -- находился в нормальном состоянии, НЕ SIGSTOP'нут.
    • Периодическое посматривание на _sysbufused показало, что оно потихоньку растёт (и объём употреблённого сервером ОЗУ рос, в среднем примерно по мегабайту в минуту).

      А вот после коннекченья Федей к этому x2go-серверу _sysbufused стал потихоньку уменьшаться.

    • Также аналогичная ситуация была на ring1:12 с клиентами ringcor23 и ringcor45 оттуда же на тот же pi1. Но просто не доросло до тех объёмов -- всё потребление сервера было ~650M.

    Выводы (частично предположения):

    • Похоже, какой-то системный заскок, связанный с x2go. Видимо, оно при отключенном дисплей-клиенте (та Raspberry) как-то "подтормаживает" графических клиентов -- например, не отвечает на синхронные запросы (вроде "дай координаты мыши"), или отвечает с задержкой.
    • И получается, что сервер шлёт данные быстрее, чем клиент успевает их вычитывать, вот выходной буфер сервера и растёт.

    31.03.2017: организуем тесты.

    Условия:

    • Всё проверяется локально на x10sae (E3-1285Lv4, 3.4GHz (реально 3.7), 32GB RAM), CentOS-7.3.
    • Сервер от магнитной системы линака -- devlist-linac1-11.lst, в режиме симуляции (cxsd -dsc), в качестве клиента -- linmag.

    Результаты:

    • При частоте 100Hz (-b10000) клиент ничего не успевает отрисовывать, каналы чтения у него горят гусиным, сервер жрёт память по ~5MB/s.
    • При частоте 10Hz (-b100000) всё успевается, и это как раз комфортный вариант для тестирования.
    • Немного из анализа логгинга (добавленного в fdiolib) и кода:
      • Объёмчики по сети гоняются немаленькие -- протокол нифига не прижимистый, а очень даже щедрый.

        Характерный размер посылки -- что-то вроде 256 байт. Например, в ответ на CXC_RESOLVE одного канала отдаётся 4 chunk'а суммарным объёмом минимум 56 штук int32 (т.е., 224 байта), а реально больше.

      • В cxsd_fe_cx НЕ делается уставка SO_SNDBUF. И всё равно до заполнения системного буфера отправки туда успевает влезть что-то в районе 512кБ, и только потом начинает задействоваться _sysbuf.
    • На этом месте разбирательством заниматься надоело, и было внедрено исправление -- ограничение объёма буфера отправки.
      1. Собственно ограничение:
        • делается в AcceptCXv4Connection() сразу же после регистрации fdio-дескриптора;
        • в качестве ограничения взято CX_V4_MAX_PKTSIZE*10.
      2. В DisconnectClient() добавлена отдельная расшифровка кода EOVERFLOW (который генерит fdiolib в этом случае) как "send-buffer overflow".

      Ограничение работает, клиент дисконнектится.

    • Ну и попытка ответа на изначальный вопрос от 03-06-2016 почему клиент "вовсе НЕ мгновенно получил большой поток данных": может, потому, что клиент -- тормоз?
      • Т.е., клиент не успевает вычитывать данные и обновлять их на экране?
      • Увы, эта теория не прокатывает: в вышеописанных условиях локального подсоединения linmag обычно жрёт ~10% CPU, а после Ctrl+Z, некоторой паузы и возвращения к жизни посредством "fg" -- всего лишь ~25%; т.е., процессор его никак не ограничивает.

      Так что -- низачот, надо думать дальше.

      • Может, ограничение на уровне графической части -- связка Motif+Xt+Xlib не чемпион по скорости/реактивности, а еще и соединение с X-сервером обладает некоторой латентностью.
      • В пользу этой гипотезы говорит:
        1. С чего вчера начались разборки -- что без-дисплейная x2go как-то подтормозила клиента.
        2. При тестировании во время "навёрстывания времени" (когда от сервера сплошным потоком летели данные) linmag переставал реагировать на действия юзера -- только отрисовывал, а нажатия кнопок (например, [...]) были отработаны задним числом, когда все данные от сервера пришли и были переварены.
      • Как бы это проверить? Нужен какой-нибудь без-GUI'ный клиент, не тратящий время на отрисовку (типа v2'шного cdrclient'а).

        Теоретически прокатит cx-starter, проблема только в том, что по нему никак не проконтролируешь "текущее состояние" (возраст) -- только по загрузке процессора.

        ...проверено:

        • Да, у него загрузка процессора поднимается тоже невысоко, и очень ненадолго -- с ~100МБ данных буквально на считанные секунды.
        • А если дождаться сильно большого объёма, то уже сервер начинает напрягаться -- жрёт по 99% CPU.

          Видимо, бедняжечка, копирует при каждом добавлении свои сотни мегабайт: очевидно, там не очень удачно получается, что не все отправки скопом, а перемежаются добавлениями данных -- вот тогда fdiolib и перемещает накопленные-но-неотправленные в начало, убирая "дырку" размером _sysbufoffset.

          Кстати, в fdiolib можно бы сделать оптимизацию: если в буфере есть место ПОСЛЕ накопленных данных (т.е., _sysbufsize>=_sysbufoffset+_sysbufused+size) -- а оно появится после первой же отправки и сдвига -- то не "схлопывать дырку", а добавлять в конец.

          Замечание: тестирование делалось с ограничением в cxsd_fe, задранным до CX_V4_MAX_PKTSIZE*1000 (16GB).

    03.04.2017: пробуем сделать ту оптимизацию.

    • Помогло!
      • Даже при накоплении ~1GB данных рассасываются они в течение нескольких секунд (напоминание: через unix/localhost!). В течение этого времени cx-starter жрёт ~99% CPU (top перенастроен на 5HZ -- d0.2[Enter], иначе при d=3.0 нифига не видно), а cxsd почти ничего (крохотный всплеск и потом ~0%.
      • ~10GB -- аналогично, релаксация занимает около 50 секунд. В течение этого времени есть некоторые "неравномерности": иногда загрузка cx-starter'ом падает до ~0% CPU, а cxsd'шная подскакивает до 30%; сложно сказать, одновременно ли происходят эти события и почему они вообще есть. Какие-то приколы scheduling'а?

      Как бы то ни было -- цель достигнута, от мега-тормозов сервера избавились, а уж многогигабайтные объёмы буферов -- сценарий малореалистичный, ограничение на 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: в продолжение тех разборок с тормозящим клиентом и накоплением в сервере данных для него: не ввести ли какую-то синхронизацию, чтобы сервер приостанавливал отправку данных, если клиент их не принимает?

    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?

        Неа, плохо, потому, что:

        1. Не стоит смешивать совершенно разные механизмы. "Подтверждения" -- функционал прикладного уровня; application-ping же -- транспортного.

          06.04.2017@утро-лыжи: и однако же именно такой вариант выглядит наиболее простым в реализации -- т.к. уже всё готово, просто добавить в клиента отсылку ответа на CXT4_PING.

          07.04.2017: а вот нифига! Этак получится, что старые клиенты -- БЕЗ такой фичи -- всё равно автоматом будут переставать получать обновления.

        2. Он шлётся раз в 5 минут (для подтверждений это ОЧЕНЬ долго).
    • Как считать, что "надо приостановить"? Если с последнего "подтверждения" прошло сколько -- 10 секунд, минута, 5 минут? 10 секунд -- мало: будет совершенно ненужный траффик; даже 1 минута -- много, в случае осциллограмм/картинок/... могут накопиться даже гигабайты.

      07.04.2017@утро-зарядка: и однако именно 10 секунд выглядит наиболее разумно. Т.к. это и для локальной связи достаточный интервал (локальные фризы из-за ОС заведомо короче), и даже для "из Антарктиды" скорее достаточно. А были мысли сделать "кратно периоду цикла" -- например, 100 циклов -- но для коротких циклов (100Гц) локальные фризы могут начать пакостить.

    • ЧТО приостанавливать?
      • Поциклово присылается мало что -- только изменившиеся каналы. А on_update-каналы шлются сразу.
      • А on_update'ные останавливать -- как, когда?
      • А отправку CXT4_END_OF_CYCLE тоже приостанавливать?
    • Главное -- что при приостановке отправки данных окажется, что клиент НЕ имеет полноценной "картинки", чего допускать никак нельзя.
    • ...и отдельно -- что pzframe-каналы (которые композитные!) накладывают некоторые требования на последовательность прихода данных, и могут оказаться "нецелостными".

    Так что вся идея "приостанавливать отправку данных замёрзшим клиентам" выглядит сомнительно -- не видно нормального ("красивого", "натурального") механизма реализации, а все имеющиеся идеи дадут какую-то халтурную и ненадёжную махарайку.

    Кстати, вот в v2 такой проблемы не было -- потому что там не было и подписки как таковой, а данные каждый раз запрашивались самим клиентом. Так что тормозящий клиент просто переставал присылать запросы.

    04.04.2017: можно так:

    • Факт "не надо присылать" определять в SendEndC() и помечать глобально-для-клиента в v4clnt_t.
    • Обновившиеся каналы (ОБОИХ типов -- и per-cycle, и on_update) флагировать modified=1, но при выставленном "клиент замёрз" не присылать -- и, соответственно, флаги модифицированности останутся взведёнными.
    • По приходу подверждения от очухавшегося клиента: замечать, что он был замёрзший, и высылать ему ВСЕ modified-каналы.

    Обсуждение:

    • Такая схема выглядит рабочей и целостной.
    • Но вопрос "КОГДА?" (через какое время неактивности) приостанавливать остаётся.

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

      (Этот "однозначный ответ" был бы при обязательных подтверждениях на каждый канал, но такую фигню мы делать точно не будем.)

    • И главный недостаток -- проблема pzframe-каналов никак не решается. Т.к. при "размораживании" каналы будут присланы в произвольном порядке, и маркер вполне может придти РАНЕЕ остальных каналов.

      ...точнее, ГАРАНТИРОВАННО именно так и случится: в нынешних hw4cx/pzframes/*_data.c каналы-маркеры стоят вначале списков, так что заказываются раньше атрибутных, и в cxsd_fe'шный список мониторируемых тоже попадут раньше.

    31.10.2017: посмотрел на вышенаписанное свежим взглядом -- похоже, идея не катит в принципе, так что полностью "withdrawn".

    (А полез читать из-за сегодняшней идеи "грохать клиента, которому более 1 минуты ничего не удалось отправить".)

  • 21.04.2017: косяк: CXT4_END_OF_CYCLE шлётся ВСЕМ клиентам, в т.ч. недоприконнекченным (и даже тем, чей endianness неизвестен!).

    Обнаружилось сегодня при разборках "почему глючит QL15 / почему соединение обрывается по «Received packet is too big»", побочным эффектом от анализа пакетов.

    24.04.2017: в SendEndC() вставлена проверка, что если cp->state<CS_USABLE, то ничего не делать.

  • 31.10.2017: в продолжение борьбы с ростом буферов: вчера на пульту вылезла проблема, когда при оторванной связи (отключенный свич) cxhw:12 быстро сожрал память и был убит OOM killer'ом. Ограничение на размер буферов не спасло, т.к. коннектов было очень много и их общий объём вылез за границы ОЗУ до достижения ограничения хоть кем-то.

    @по-пути-с-обеда, вдоль Будкера Собственно идея: а если отрубать не только переразмеренные буфера, но также в случае, если при непустом буфере (_sysbuf) не было отправки более чем какое-то время.

    31.10.2017: некоторые детали:

    • Указывать это время дополнительным вызовом, вроде fdio_set_maxnosendtime(); например, в секундах.

      По умолчанию =0, т.е. "не проверять".

    • Собственно проверку делать при отправке -- в fdio_send(), а точнее -- в StreamSend()+DgramSend().
    • ...вот как конкретно -- вопрос: надо "элегантно" встроиться, чтобы не делать лишних прооверок и избежать косяков... В идеале, обойтись ОДНИМ полем timestamp, БЕЗ дополнительной булевской переменной.

      Видимо, иметь поле timestamp, в котором обычно 0. Главный вопрос -- как "начинать счёт времени" и когда "сбрасывать счётчик", чтобы при отправке сильно позже случайно не оказалось, что "прошло более лимита, стреляемся!".

      Очевидно, начинать счёт надо при переходе _sysbufused от ==0 к !=0?

  • 26.12.2017: обнаружена некрасивость -- в пакетах от сервера к клиенту присутствует "мусор": в незаполненных полях остаётся то, что было в replybuf'е.

    26.12.2017: обнаружено при разбирательстве "а чё это пакеты от сервера прилетают длиннее на 32 байта, чем значится у них в DataSize?".

    • Проблема-причина реально не существует: там соль в том, что это DATAsize, и в нём не учитывается объём заголовка, равный как раз 32 байтам (8 штук int32). Протокол так устроен -- чтоб заголовок считывался без знания длины всего пакета.
    • Зато при просмотре дампов пакетов в Wireshark было замечены характерные символьные сигнатуры (вроде "<Pst" -- CXC_STRS) во внутренностях других форков (конкретно в "<Cvl" -- CXC_NEWVAL), в весьма странных местах (конкретно в позиции rs1).

    Тут есть 2 аспекта разной степени серьёзности:

    1. Это "дыра в security" -- таким образом клиенту могут быть раскрыты некие данные, его никоим образом не касающиеся.

      В основном-то ему так будут доставаться кусочки от предыдущих пакетов к нему же -- т.к. это тот же самый replybuf; но без гарантии.

    2. В незадействованных (сейчас!) полях присутствует мусор вместо полагающихся нулей.

    Если первая проблема пока умозрительна (не то у CX-сервера сейчас применение, чтобы серьёзно озабочиваться "раскрытием" подобных данных), то вторая -- уже неприятнее: при возможном расширении протокола поля могут задействоваться, и мусор вместо нулей ("умолчаний") окажется вреден.

    Посему -- повсеместно после вызова GrowReplyPacket() добавлено bzero() соответствующего chunk'а.

    • Нулится именно только ЗАГОЛОВОК -- расположенный в начале стандартный CxV4Chunk плюс специфичные поля.

      Но НЕ весь chunk целиком, с местом под данные (т.е., НЕ объём rpycsize)): в этом смысла не усматривается -- правда, не security-critical у нас софтина.

    • Некоторым исключением является кусок ServeIORequest()'а, отвечающий за отправку ответа на CXC_RESOLVE (соответствующего PutNNNChunkReply()'я не существует, да и асимметрия там): она нулит именно объём dpycsize -- чисто по причине организации там кода (присвоение полям стандартного заголовка делается ДО кастинга указателя на специфичную структуру).

    Проверено сниффером -- мусор исчез, везде нули.

  • 28.12.2017: еще поучительная история о косяке -- как серьёзная ошибка может не только оставаться незамеченной, но даже и не иметь шансов проявиться.

    28.12.2017: вчера в ServeIORequest() был замечен "warning: 'cpid' may be used uninitialized".

    И правда -- в ветви CXC_SETMON переменная cpid используется, но значение ей не присваивается.

    • Сначала была мысль, что "ну ладно, пишется в ответ мусор вместо cpid, а async_CXT4_DATA_IO() на этот ответ всё равно не реагирует, так что мусорность неважна".
    • Однако внимательный анализ показал, что в вызываемых там функциях Put*ChunkReply() передаваемое им значение критично: там делается gcid = cpid2gcid(cpid), и мусор бы неизбежно вызывал ошибку, функции б возвращали 0 (объём данных), и клиенту б не отдавались {R,D}, что вызывало бы ту доставучую ошибку "клиент не получил коэффициент 1000000", но этого не происходит.
    • Потом стало ясно: клиент -- в лице cda_d_cx.c -- всегда делает вызовы ПАРОЙ: cx_rd_cur(); cx_setmon.
      • Таким образом, прилетают 2 chunk'а подряд: CXC_PEEK, CXC_SETMON.
      • Вот ветвь обработки 2-го chunk'а и получала правильное значение cpid, оставшееся от 1-го.
    • Таким образом, при нынешнем сценарии использования ошибка проявиться не могла. Вот если б какой-то другой клиент (не на cda, а прямо на cxlib'е) пользовался одиночным cx_setmon() -- тогда да.

    28.12.2017: исправлено тривиально -- присвоением, cpid = monr->cpid.

    29.12.2017: приличия ради перепроверяем все warning'и на тему "uninitialized".

  • 27.02.2018: несколько странно выглядит архитектура работы с сокетами, где у всех Destroy*Sockets() есть параметр "and_deregister".
    • Растёт это, видимо, еще со старых (до-cxscheduler/до-fdiolib'овских?) времён?
      • А вот и нет! Даже в "свежем" однопроцессном сервере в v2 (programs/zzz/cxsd_fe_cxv2.c) ничего подобного нет. Там вообще НЕТ никаких deregister для слушающих сокетов -- ибо не предполагалось никакой остановки frontend'а, а только жизнь до конца жизни сервера.
      • Так что добавлено это было где-то в процессе создания CXv4. Причём, уже 09-01-2013 имеется вопрос о смысле этого параметра (в т.ч. "Почему это он иногда ==0?"). А это ДО даже начала работ над однопроцессным (которое, судя по bigfile-0001.html, началось 21-05-2013).
      • ...и вообще, если немножко подумать -- то не могло быть в v2'шном сервере ничего подобного, ибо он был 3-процессным, и ну никак даже в принципе не мог предусматривать освобождения ресурсов.
    • А первопричиной, очевидно, является "не-атомарность" создания слушающих сокетов -- ведь их 2, и сначала они ОБА создаются, а потом так же для обоих регистрируются fdio-handle'ы.

      В старом варианте -- от 07-01-2013 -- всё прекрасно видно: там сокетов только пара, поэтому функции называются проще, CreateSockets() и DestroySockets(), и последняя вызывается с параметром 1 из cx_term_f() и 0 из cx_init_f() -- в случае облома с созданием.

    В любом случае, выглядит это странно. Более разумным представляется нынешний подход (он используется в cda и прочих, включая даже RlsV4connSlot() из этого же fe_cx): если дескриптор ресурса >=0, то разрегистрируем ресурс и делаем дескриптору =-1 (каковое значение он имеет и изначально).

    В создаваемом сейчас cxsd_fe_starogate.c делается так же, а сюда вся эта простыня записана именно потому, что при оном создании возник вопрос и захотелось разобраться.

  • 28.02.2018: и еще идеологический вопрос: а не надо ли в frontend'ах файловые дескрипторы (fdio'шные handle'ы) и таймауты регистрировать с uniq=cp->ID?

    СЕЙЧАС там используются нули, с комментарием /*!!!uniq*/; а почему не идентификатор клиента?

  • 22.03.2018: содержимое endianconv.h совершенно неудовлетворительно. Оно функционирует только в случае, если реально ничего преобразовывать не нужно (как для нынешнего использования LE_client<->LE_server), а иначе будут проблемы:
    1. Ветка для BYTE_ORDER == BIG_ENDIAN пустая. Но это-то исправляется тривиально -- копированием из ветки ==LITTLE_ENDIAN и надлежащей заменой.
    2. Конверсия u64 не делается -- возвращается просто исходное число (f64 аналогично, но это уже часть следующей проблемы).
    3. Конверсия вещественных сделана некорректно. Например, вот такой код:
      static inline flt32 b2h_f32(flt32 bige_f32) {return swab32(bige_f32);}

      Но ведь это полная хрень! Ибо сначала вещественное bige_f32 будет преобразовано в int32, затем в нём байты переставятся местами, и получившееся целое же будет обратно преобразовано в вещественное. Но это не имеет НИКАКОГО отношения к требуемому "переставить байты в представлении вещественного числа"!

    22.03.2018: что интересно, подобные проблемы с конверсией 64-битных и вещественных уже решались, и вполне успешно:

    • В vsdc2_drv.c::vsdc2_in() -- для частного случая float32.
    • В remdrv.c -- для общего случая: там и int64, и float64 (по факту, там они просто не различаются -- конвертируются ячейки размером 8 байт).
  • 11.10.2018: (вечер) имеется проблема с каналами, запрашиваемыми с режимом CX_MON_COND_ON_UPDATE: если это обычные readonly-каналы, не-AUTOUPDATED, то они начинают очень быстро молотить...

    11.10.2018: проблема вылезла в процессе попыток разобраться "а чё это LAM_SIG у IE4 не приходит" -- когда для проверки в список мониторируемых каналов cdaclient'у был также добавлены ie_bum (счётчик BUM-цикла) и bum_going (флажок, горящий при идущем BUM-цикле, но которому забыта уставка IS_AUTOUPDATED_YES и потому читающийся также поциклово).

    • Фигово, что просто-читаемые-каналы (вроде входного регистра) при попытке смотреть их значения через "cdaclient -m" начинают сыпаться с дикой скоростью. А причина в том, что cdaclient запрашивает мониторинг с режимом "on_update", так что опрос идёт максимально быстро.

      Для автообновляемых каналов (и для rw-каналов) это правильный режим работы -- они всё равно не будут молотиться чаще, чем реально обновляются.

      А вот для обычных -- совсем плохо. Что делать?

    • Отдельный вопрос -- поведение прочих каналов (частота их выдачи cdaclient'ом) с учётом периода работы сервера.
      1. Так, r-каналы noop'а почему-то отдаются ДВАЖДЫ за цикл, причём с ОДНИМ значением. Непонятно: ну молотили бы они по многу раз за цикл -- ясно. По одному -- тоже б было ясно. Но вот ДВАЖДЫ -- как?
      2. А вот у устройства "резонатор" (драйвер kurrez_cac208, канал canhw:19.rezonator.is_ready) -- при том же -b200000 почему-то данные отдаются 20 раз в секунду, т.е., ЧЕТЫРЕЖДЫ за цикл. Причём все эти 4 раза имеют одинаковый (до микросекунды) timestamp -- очевидно, он отдаётся vdev'ом из кэша, где значение от cac208 (реально -- момент получения сервером). (Пять минут спустя: да, точно -- канал IS_READY имеет флаг VDEV_TUBE и обрабатывается в 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-клиентов, чтоб каналы могли запрашиваться чаще, чем раз в цикл."

    • Откуда вывод: да НЕЛЬЗЯ решать проблему "чё так часто обновления прилетают" путём отключения запросов более раза в цикл -- потому, что эта возможность вводилась преднамеренно, именно для случаев, когда НУЖНО мочь спрашивать чаще раза в цикл.
    • А проблема "чё cdaclient так молотит" является комбинацией введённого 27-05-2016 флажка и того факта, что с 09-03-2017 cdaclient стал ВСЕГДА регистрировать каналы с включенным CDA_DATAREF_OPT_ON_UPDATE.
    • Вообще это проблема более общая: КТО должен инициировать получение данных?

      Нынешняя модель в CXv4 работает постольку-поскольку -- для каналов записи и автообновляемых; для измеряемых-когда-угодно -- нет.

    • Поскольку вопрос идеологический (и даже академический), то дальнейшее обсуждение будет уже в разделе "ВременнАя схема работы".

    26.11.2018: вроде решение сделано; подробности в разделе по cxsd_hw за сегодня.

  • 24.06.2019@после-обеда, дорога из дома на работу: кстати, к вопросу (давно волновавшему меня), насколько реально сделать поддержку всяких TAURUS -- для чего должно быть можно добыть и отдать "наверх" список ВСЕХ каналов: а ведь можно!

    24.06.2019@после-обеда, дорога из дома на работу: а именно -- последовательность мыслей:

    1. @лестница-с-10-го-этажа-вниз: ведь у нас в реальности граф без циклов (и даже без петель) -- поскольку используются не настоящие "симлинки", а скорее "хардлинки". Так что можно спокойненько всё дерево взять и обойти, в т.ч. "руками". Просто оный обход нужно будет производить в 2 этапа: сначала устройства (и тут дампить оба 2 пространства имён), а затем виртуальная иерархия.
    2. @вдоль ВЦ и ИПА: а как можно б было оформить реализацию с точки зрения API cxsd_db: сделать ИТЕРАТОР.

      Т.е., чтоб можно было сказать cxsd_db "пройдись-ка по всем узлам, вызывая указанную функцию-callback".

      И функций таких будет 3, вызываемых на разных этапах процесса добычи:

      1. Подсчёт числа имён. Тут каждый вызов функции будет делать ++ счётчику, передаваемому ей по указателю через privptr, а перед вызовом итератора оный счётчик нулится.
      2. Подсчёт объёма данных. Аналогично предыдущему, только вместо инкремента счётчика будет увеличиваться "объём" на длину имени.

        ...нужно ли это?

      3. Собственно "отправка данных".
    3. @около ИЦиГ, перед Коптюга: отдельный вопрос -- как это всё передавать по сети. Учитывая, что управление будет у итератора, а не у "клиента" (cxsd_fe_cx), возможны всякие неприятности, вроде переполнения буфера отправки.

      @ИЯФ-~19:00: из возможности переполнения, кстати, следует, что функция-callback должна возвращать значение, и если оно !=0 (или <0), то итератор должен прервать (свернуть) работу и вернуть вызывальщику этот код.

      Поэтому -- а как насчёт возможности передавать информацию по сети в "приподупакованном" виде?

      И неплохим вариантом упаковки выглядит передача как раз просто "внутренних имён" БД вместо просто строк. Т.е., прямо "словарь" str_db (поправка уже при записывании в ИЯФе: оно называется CxsdDbInfo_t.strbuf), а в именах -- соответствующие оффсеты вместо строк.

    24.06.2019@ИЯФ, при записывании вышеприведённого, в районе 17:30-18:30: по ходу записывания тех мыслей возникли замечания.

    1. А вот и нет -- у нас хоть и "хардлинки", но циклы возможны! Ведь ничто не мешает узлу с именем "a.b.c.d" ссылаться, например, на "a.b" -- вот и получаем циклические ссылки, вроде "a.b.c.d.c.d{.c.d}".

      Как вариант решения: можно производить нахождение "циклов" один раз, при старте сервера (в смысле -- после чтения БД, или после её актуализации), чтобы отдельно помечать узлы, смотрящие на cpoint-"контейнеры" в своей же иерархии; и потом итератор чтоб в такие закольцовки не заглядывал бы.

      Для этого понадобится:

      1. Добавить поле "цикл!!!" к CxsdDbClvlItem_t,
      2. плюс поле "мы сейчас внутри!" к CxsdDbClvlItem_t же: этот флаг взводить при входе в CXSD_DB_CLVL_ITEM_TYPE_CLEVEL-cpoint и сбрасывать при выходе из него; нужен для обнаружения, что в момент проверки (в глубине рекуррентной функции) мы уже внутри этого clevel'а.

      Кстати, сейчас проверил -- да, МОЖНО делать иерархии с закольцовками.

      dev z noop r1i -
      cpoint a.b.c.e z.0
      cpoint a.b.c.d a.b
      
      -- каналы a.b.c.e и a.b.c.d.c.e смотрят на один и тот же z.0, и туда же смотрит a.b.c.d.c.d.c.e; очевидно, что компонент ".c.d" в середину можно вставлять бесчисленное число раз.
    2. Есть один тонкий аспект: для КАКИХ узлов надо вызывать "указанную функцию"? Нам ведь КАНАЛЫ нужны.
      • Для устройств -- понятно: для каждого канала по 1 разу.
      • А для виртуальной иерархии как? Там же могут быть ссылки как на устройства, так и на каналы.

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

      Отдельный вопрос: а ПЕРЕДАВАТЬ-ТО ЧТО этот callback?

      • Похоже, просто имя a.b.c.d.e, где "e" -- как раз узел, ради которого -- callback вызывается.
      • Но тогда встаёт вопрос об осмысленности п.III.
    3. С учётом того, что единицей передачи по сети является всё-таки КАНАЛ (для которого итератором и будет вызываться функция-callback), и имя канала состоит из последовательности компонентов через '.', а в strbuf[] содержатся именно сами компоненты -- такой способ "упаковки" выглядит не слишком осмысленным.

      Просто потому, что в "словаре" strbuf[] полных имён каналов НЕ будет, а будут лишь компоненты.

      Можно, конечно, передавать переменной длины список оффсетов, строки по которым надо конкатенировать через '.' для получения полного имени, но это выглядит сомнительной экономией: каждый оффсет занимает 4 байта, так что экономия получится небольшой, а вот усложнение кода -- адским.

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

    24.06.2019@вечер-по-пути-домой: ещё несколько мыслей вдогонку:

    • @выходя-из-ИЯФа: как бы то ни было, сделать действительно можно -- вон уже все острые углы обсуждены.
    • @около-мыши: вопрос: а КАК отдавать список всех каналов по сети?

      Отдельным вызовом cxlib -- неудобно.

      Как-то вешать на cda - как?

      О: отдавать это значением канала! ЕМНИП, именно так в TANGO делается.

    • @ИПА: а коли так - то как: чтобы при каждом запросе списка каналов cxsd_fe_cx проводил бы тот квест с итератором? Кривенько...

      О: а пусть сервер сам собирает этот список имён каналов, и держит у себя - например, в том же блоке cxsd_hw_buffers[], где прочие cxsd_hw'шные буфера.

      (Да, остаются технические вопросы -- вроде "какое имя дать каналу; как его обрабатывать (в cxsd_fe_cx особой проверкой или в cxsd_hw? лучше в cxsd_hw); куда разместить (в зарезервированную первую сотню?). Но это уже детали, которые можно решить в рабочем порядке.)

    • @в-подъезде/лифте: ещё вопрос: в каком формате должен быть этот список? Проще всего сделать CXDTYPE_TEXT, с nelems=общая_длина, а внутри имена, разделённые либо '\0', либо чем-нибудь вроде '\t'.

      (Да, в TANGO-то наверняка возвращается объект, вроде массива строк. Ну чё уж -- переживём.)

    27.06.2019@утро-дорога-на-работу-около-ИЦиГ: разделять имена не '\t', а '\n'! Это удобно: сразу будем получать список строк, удобный для обработки shell'ом.

    Только за компанию бы надо научить cdaclient печатать строки "как есть", НЕ забэкслэшивая спецсимволы.

  • 30.11.2020: Юра Роговский обратился со странной проблемой: почему-то cdaclient при записи -- формат КАНАЛ=ЗНАЧЕНИЕ -- зависает (иногда навсегда, иногда потом отвисает). И вроде как из GUI-клиентов иногда тоже не управляется (эта проблема наблюдалась и раньше).

    Причём раньше (с дистрибутивом за весну или февраль) такой проблемы не было.

    Проверили наличие/отсутствие cxhosts, могущего ограничивать запись -- да нет, отсутствует.

    Поскольку я такого вроде не замечал, то возникла мысль, что проблема может быть связана с Debian/Ubuntu, на которое это происходит (мало ли -- что-то чуть иначе компилируется, или библиотека какая-то по-другому работает).

    01.12.2020: Роговский вчера вечером перепробовал толпу дистрибутивов и нашёл, что проблема появилась между w20200827-cda_lock_stat_of_ref.tar.gz и w20200912.tar.gz.

    • Был сделан diff между ними, показавший, что на клиентской стороне вроде ничего особо не менялось -- ничего подозрений не вызвало.
    • ...зато как раз там было сделано кое-что в cxsd_fe_cx.c, подозрения ВЫЗВАВШЕЕ: модификация в логику отправки пакетов, за 06-09-2020 (причина -- за 04-09-2020), ключевое слово 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'а.
      • Возвращать же он будет сразу УКАЗАТЕЛЬ на место в буфере под этот chunk.
      • ...либо NULL при обломе.
      • Соответственно, он же и будет выполнять cp->replydatasize+=rpycsize и cp->rpycn++.
    • Chunk_maker'ы же теперь будут возвращать не rpycsize (или 0 при обломе), а лишь булевское успех/облом -- +1/0.
    • Касательно инкрементирования 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.с.

  • 24.03.2021: при переделке схемы отправки на "rpycn и replydatasize хранятся в v4clnt_t" заметил, что per_cycle_monitors_some_modified только взводится =1, но никогда не сбрасывается =0.

    Это должно приводить к тому, что если хоть раз хоть 1 канал присылался по циклу, то ВСЕГДА -- каждый цикл -- будет приходить пакет, в т.ч. с NumChunks=0.

    25.03.2021: проверил -- да, так и есть.

    Это, конечно, не бог весть какая проблема. Во-первых, это просто лишний пакет, ничему особо не мешающий. И во вторых, ведь не-ON_UPDATE-каналы по факту используются только в скринах, а там обычно обновления есть практически всегда.

    Как бы то ни было -- исправлено: =0 делается в SendEndC() сразу после обнаружения и перед отправкой данных.

cxsd_fe_starogate:
  • 22.02.2018: приступаем к созданию первого "иного" frontend-плагина после штатного CX'ного.

    22.02.2018: Это будет плагинчик для протокола "starogate", используемого на электронно-лучевой сварке, сейчас в CXv2, и чья реализация необходима для v4, чтобы сторонние программы (медведевские) могли бы доступаться к СУ.

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

    И да -- для юзабельности нужно будет собирать его в .so.

    Создавать начинаем прямо в 4cx/src/lib/srv/, а уж потом куда-нибудь переселим.

  • 22.02.2018: делаем потихоньку, общую инфраструктуру беря от cxsd_fe_cx.c, а "мясо" и реализацию протокола подсматривая в старом cx/work/cx/src/programs/gate/.

    26.02.2018@лыжи: идеологический вопрос -- КОГДА отправлять значения каналов?

    • В starogate.c они шлются раз в цикл -- в силу идеологии устройства v2 вообще и Cdr/simpleaccess.c в частности.
    • А тут можно слать и чисто по обновлению.

      Учитывая, что у Медведева были какие-то сложности с вычитыванием приходящих данных (что-то не слава богу в 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: пилим дальше...

    • Менеджмент структур -- *MonSlot(), *ConnSlot().
    • AcceptConnection().

      Тут добавлена серверная специфика -- аллокирование ClientID.

    • InteractWithClient(). Вот в нём парсинг остался, а "мясо" совсем иное.
      • В обработчик "write" была добавлена RD-конверсия.
      • Также RD-конверсия потребна для мониторов. И тут идеологический вопрос: где брать или как хранить?
        1. Добывать каждый раз при отправке значения канала -- варварство.
        2. Следующая мысль: иметь фиксированный массив на 20 пар {R,D}.

          Учитывая, что вообще сам протокол starogate -- это хак, и вряд ли будет много каналов мониторироваться, то на дикий перерасход памяти (20*2*sizeof(double)=20*2*8=320 байт на монитор) можно и плюнуть.

        3. Но, учитывая, что создаваемое сейчас может в будущем использоваться как основа для иных frontend'ов -- лучше б всё сделать корректно, по образу и подобию cda.

          Т.е.,с раздельными imm_phys_rds[4] и *alc_phys_rds:

          1. В подавляющем большинстве случаев -- phys_count<=2 -- будет теряться лишь 4 или 8 байт объёма указателя.
          2. В редких случаях с phys_count>2 -- 4*8=32 байта массива imm_phys_rds[].

        И не забывать обновлять по событию _R_RDSCHG.

    01.03.2018: ...

    • Продолжаем InteractWithClient():
      • Поддержка RD-конверсии: складирование ("умное") калибровок повешено на StoreRDs(), чьё содержимое скопировано с cda_dat_p_set_phys_rds().
      • Также сделана вся реакция на команду "monitor":
        • Проверка совместимости типа -- должен быть скаляр, причём INT или FLOAT.
        • Если канал уже мониторится -- то просто пропускается процесс добавления (аллокирования нового монитора (и складирования в нём {R,D}) и подписывания на события).
        • Также добавлена отправка текущего значения, если оно годно -- канал is_internal либо имеет не-NEVER_READ-timestramp и является rw или is_autoupdated. В точности, как проверяется в cxsd_fe_cx.c и cda_d_insrv.c.
    • Кстати, об оной проверке на "годность" значения для отправки: она оформлена в функцию CxsdHwIsChanValReady(), покамест локальную, но вообще-то её надо унести в cxsd_hw.c -- чтоб не дублировать одни и те же нетривиальные условия в куче точек.
    • Идеологическое: в отличие от starogate.c, тут решено НЕ пытаться объединять мониторинг одинаковых каналов несколькими клиентами.
      • Там-то это было вызвано необъодимостью -- API simpleaccess.
      • А тут делаем в точности, как в cxsd_fe_cx.c -- всё изолированно: на каждый канал в каждом клиенте заводится по отдельному монитору; т.е., отдельная регистрация через CxsdHwAddChanEvproc().
    • Да, тем самым можно будет использовать "мозги" из этого файла при реализации иных frontend'ов.
    • Также сделан MonEvproc(). Он очень простой: по _R_UPDATE вызывает SendMonitor(), а по _R_RDSCHG -- пере-добычу калибровок и StoreRDs().
    • Надо бы повсеместно повставлять проверки при fdio_send() -- что если ошибка, то грохать соединение. 12.03.2018: сделано.

    02.03.2018:

    • Теперь надо делать собственно мониторинг:
      1. Наполнение SendMonitor() -- собственно добыча double-значения и отправка его клиенту.
      2. Постоянное запрашивание чтения! Вот это чуть не забыл.

        Тут надо как-то бороться с потенциальными циклами -- вроде "moninfo_t.being_reqd". Видимо, копировать с cxsd_fe_cx.

        И не игнорировать ли оное для каналов is_rw/is_autoupdated? Т.е., НЕ запрашивать для них (даже и начально не надо?)?

    05.03.2018: размышленческое о мониторинге:

    • По факту -- именно СЕЙЧАС и будем решать общий вариант проблемы "как избегать бесконечных циклов при мониторировании каналов", которая в частном варианте была решена в протоколе CX путём отдельного режима ON_UPDATE, как бы перекладывающего ответственность на "заказчика".
    • @после-обеда-в-Гусях-по-дороге-в-Эдем: у нас просматривается 3 возможных варианта реализации.
      1. Простой, в стиле v2: каждый цикл заказывать чтение всех мониторируемых каналов.

        Отправлять -- хоть раз в цикл же, хоть по обновлению, это уже непринципиально.

        06.03.2018: Кстати, cda_d_insrv.c использует именно такой подход.

      2. Хитрокомбинированный, как в v4'шном cxsd_fe_cx.c: мониторы делятся на "per-cycle" и "on_update", и обрабатываются по-разному.
      3. (Забыл, какой вариант третий...)

    07.03.2018@утро-~10:20, по пути пешком от гаража мастера Саши до проходной ОК: а зачем СЕЙЧАС так надрываться, реализуя именно сейчас "максимально правильное решение задачи мониторинга"?

    • Сейчас сделать по-простому -- в начале цикла запрос всех мониторируемых, и всё.
    • А уж когда ПОНАДОБИТСЯ сделать другой frontend, вот ТОГДА ДЛЯ НЕГО и решать задачу, под ЕГО потребности.

    07.03.2018: ну, допиливаем:

    1. "Наполнение SendMonitor() -- собственно добыча double-значения и отправка его клиенту": отправка была сделана еще позавчера, а сейчас добыча. Вычитывание, в соответствии с dtype, плюс RD-конверсия -- скопированы с cda_core.c::cda_dat_p_update_dataset().
    2. Мониторинг:
      • Добавлены все поля clientrec_t.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: добиваем:

    1. Мониторинг:
      • Добавление монитора:
        • "Рост" periodics[] взят из cxsd_fe_cx, на основе GrowUnitsBuf().
        • Также добавлено прописывание защитного значения mp->cpid=-1 сразу после аллокирования яцейки -- чтоб RlsMonSlot() не делал "periodics_used--" в случаях, когда еще не сделано "++" (всё полностью аналогично решению от 10-03-2018).
      • Удаление монитора в RlsMonSlot():
        • Добавлены должные махинации с periodics_{used,needs_rebuild}.
        • С учётом guard'а "mp->cpid>=0" ("=" может даже и лишнее).
        • Плюс снятие evproc'а.
      • Также добавлена команда "ignore" --
        • В starogate_proto.h.
        • И собственно обработка.
    2. Добавлено включение TCP'шных SO_KEEPALIVE:=1, плюс заодно и close-on-exec:=1.
    3. "Слепые" отправки через fdio_send() заменены на действия с проверкой. Для этого
      1. Введена функция send_or_close(), проверяющая результат и грохающая соединение при ошибке.
      2. Все отправки (кроме SendMonitor(), где механизм свой) заменены на неё.

      Есть только один формальный повод для беспокойства: fdiolib может вызвать нотификатор не только асинхронно (когда сам вызываем из cxscheduler'а), но и в ответ на fdio_send(): StreamSend() может вызвать StreamReadyForWrite(), который, в свою очередь, при ошибке дёрнет close_because(), вызывающий и нотификатор. Впрочем, это всё обсуждается в fdiolib'овском разделе с 05-04-2015.

    13.03.2018: проверяем.

    • В-нулевых, в lib/srv/Makefile сделана сборка cxsd_fe_starogate.so (а вот складирование его куда-нибудь -- через EXPORTS* -- не делается).
    • Во-первых, кажется, первый раз в истории проверена работа серверовой config-директивы load-frontend (через ключ -e) -- работает! Только надо еще директорию указать, а то файл же пока не в стандартной lib/server/frontends/.
    • Далее была исправлена пара идиотских ошибок (скорее опечаток): send_or_close() "слал" в cp->fd вместо cp->fhandle, а при создании монитора сохранение имени делалось как mp=strdup(p) вместо mp->name=strdup(p) (вот этот косяк искал долго -- эффект-то дикий, т.к. последующие сохранения всех параметров монитора идут чёрт-те куда).
    • А потом -- ура, работает!!!
    • ...но обнаружилась иная странность:
      1. При установке монитора на канал записи и последующей записи в него же вместо значения клиенту отдаётся "0.000".

        Это при записи ТОЛЬКО через starogate, через cdaclient всё как надо.

      2. И второй прикол, совсем мистический: после такой "записи" почему-то уведомление приходит ДВАЖДЫ: сначала сразу, а потом через несколько секунд (2...9). Причём и при записи через starogate, и через cdaclient (именно ПОСЛЕ записи через 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.
    • После этого всё стало окей.

    Итого, считаем модуль доделанным.

    А ведь изначально думалось "проект на денёк-два" -- ага-ага...

  • 22.01.2019: в Makefile была забыта организация "инсталляции" cxsd_fe_starogate.so в lib/server/frontends/ -- сделана.
  • 17.06.2022: обнаружился косяк, существовавший с момента создания модуля: значение 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, то та ничего не делает).
    • Но вряд ли медведевско-сизовские клиенты используют команду "ignore".

      А поскольку всё тут per-connection, то ситуация "один клиент подписался, потом сдох, приведя к отрицательному значению, и затем уже другой подключился" также невозможна.

    • И потом до меня допёрло: у них же всё время запущен штатный скрин!!!

      Ведь косяк препятствовал лишь ежецикленному ЗАПРАШИВАНИЮ каналов.

      А отдаче данных из них при обновлении он никак не мешал.

      Таким образом, за запрашивание отвечал скрин, обеспечивая обновление данных, приходившее уже через вполне живые evproc'ы.

    13.07.2023: этот баг был родственником косяка "не модифицировалось значение periodics_allocd" из cda_d_insrv.c, найденного 03-11-2017. Не копией, конечно, но в аналогичном месте и идеологически близким. А самое позорное -- сделанным уже ПОСЛЕ исправления того, через 5 месяцев...

  • 21.06.2022: кстати, уже некоторое время (с полмесяца как?) ясно, что может понадобиться какая-то поддержка векторности. Смысл -- для "gateway'я" на стенд магнитных измерений, между стендовым софтом и EPICS'ным channel archiver'ом. Там, судя по опросу Старостенко ~03.06.2022, нечасто нужны векторные данные примерно до 1000 чисел.

    Так вот: оную поддержку векторности в ТЕКСТОВОМ протоколе можно реализовать примерно такими средствами:

    • Сами числа передавать через пробел; возможно -- с префиксом "количество".
    • Команды для работы с векторами лучше отдельные: monitor_vect, ignore_vect, data_vect, write_vect.
    • Ради точности и НЕпотери данных, возможно, лучше вместо формата %f пользоваться %a -- он обеспечивает передачу бит-в-бит, без промежуточной конверсии в десятичку.
cxsd_fe_epics:
  • 30.01.2019: создаём раздельчик.
  • 30.01.2019@лыжи-после-обеда, конец 3-й 2-ки: мыслишка насчёт реализации: учитывая доступность описания протокола Channel Access, а также то, что с прошлой осени в fdiolib добавилась поддержка "извратных" протоколов (где поле длины не совсем фиксированно) -- можно ведь сделать полностью СВОЮ реализацию, не пользующуюся epics'ными библиотеками, а просто самостоятельно дешифрирующую протокол.

    31.01.2019: контраргумент: поддерживать ЭТО придётся самостоятельно, и никакие усовершенствования epics'ного функционала (типа EPICS4/EPICS7) туда никак не попадут.

  • 03.09.2019: как бы не сюда, но на связанную тему, о том, как бы можно было сделать "обратную вещь" -- плагин в EPICS'ный сервер для доступа к нему по протоколу CX: это надо было бы делать на основе ТОГО ЖЕ КОДА взаимодействия с сетью из cxsd_fe_cx.c, но "общение со стороной сервера" уже делать иным. А хоть при помощи условной компиляции.

    05.09.2019: в порядке обсуждения -- какие на таком пути будут проблемы:

    • Ну, что сам внутренний API у EPICS'ного IOC'а может быть совсем иным -- самоочевидно.

      ...и он может быть заточен под multithreading, что создаст отдельный класс проблем.

    • Далее -- а откуда там возьмётся cxscheduler, необходимый для функционирования cxsd_fe_cx.c?
    • И если говорить о полной поддержке протокола, включая UDP-резолвинг и, соответственно, концепцию "guru": там ведь сильно завязано на CxsdDb, включая её передачу (в сокращённом виде) между экземплярами.

      ...другое дело, что в случае EPICS можно считать, что на каждом узле/IP будет работать не более 1 штуки IOC, так что можно особо не заморачиваться, а резолвить UDP-запросы исключительно в "своей базе" (того IOC'а, в котором модуль работает).

  • 26.12.2021: ЕманоФедя выдал хотелку "А ты можешь изобразить серверный модуль, чтоб у сервера можно было по CA данные получить? и сколько времени это примерно стоит?".

    Я-то ответил, что "да, такая возможность была заложена изначально. Вопрос лишь в том, КАК реализовать Channel Access: вручную -- как-то не хочется; заложена ли в EPICS возможность использовать ихний код как библиотеку для чего-то другого -- я не знаю.", и стал рыть интернеты -- памятуя давно слышанный термин "portable Channel Access server".

    И быстро нашёл -- да, это оно, ключевое слово "CAS" или "libCAS". Этот раздельчик -- для сбора информации.

    26.12.2021: итак:

    1. Общая информация:
      • Центральная страница -- "CAS: Channel Access Server Library".

        Там есть энное количество "документации" (о ней подробнее ниже) и упоминание, что в EPICS3 эта libCAS входит в "base", а...

      • ...в EPICS7 она вытащена в отдельный проект "epics-modules/pcas".
      • Существует ли "portable PV access server" -- неясно; возможно, что это "epics-pva ", он же "pvAccessCPP".
    2. Презентации:
      • "Portable Channel Access Server", Kazuro Furukawa, KEK (Marty Kraimer, APS, USPAS1999); дата модификации стоит July 17, 2003.

        Презенташка-введение на 22 страницы.

      • "Portable Channel Access Server", Marty Kraimer, 1999/Ph 514: Portable CA Server.

        Опять Марти Краймер -- возможно, вариант/предок предыдущего.

      • "Channel Access Server Tool Developers Training", Jeff Hill, Kay-Uwe Kasemir, LANL, 09/20/06.

        Ещё один пересказ того же -- укороченный, с меньшим числом деталей/примеров/иллюстраций.

    3. Документация:
      • "Channel Access Portable Server Application Interface (API) Tutorial", Philip Stanley, August 20, 1999, EPICS Release 3.13.

        Довольно подробное изложение на 70 страниц. К сожалению, картинки там все съехавшие сильно вправо (вёрстка 1999 года, похоже, плохо показывается современными вьюерами).

        22.03.2022: шибко уж оно устаревшее. Например, никакого "pvCountEstimate" теперь нету.

      • "Channel Access Portable Server: Reference Guide", Philip Stanley, November 1997 (Draft), EPICS Release 3.13.

        Ещё более подробный референс. С одной стороны -- как бы просто куски из header'ов, но с другой -- с изрядным объёмом текстовых комментариев.

    4. Добытая оттуда информация:
      • "Сервер" -- это объект некоего класса (точнее, ОТНАСЛЕДОВАННЫЙ от того класса), у которого нужно определить некоторые методы.

    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: приступаем к изготовлению скелета в work/frgn4cx/epics/srv/.

    17.02.2022: пока чисто подготовка и именно "скелет":

    • Собственно директория.
    • Makefile -- за основу взят work/frgn4cx/epics/cda/Makefile, т.к. в нём вроде все требуемые определения и "ритуальные заклинания".
    • Собственно будущий исходник -- cxsd_fe_epics.cpp:
      • В нём сделан скелет (копированием кусков из cxsd_fe_starogate.c).
      • А общий принцип "сожительства C++'ного кода и C'шного интерфейса к CX" взят из work/frgn4cx/tango/cda/cda_d_tango.cpp.
    • Для собираемости пришлось слегка покалечить cxsd_modmgr.hcxsd_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: немного о "принятых решениях" и о попытках запуска хоть минимального варианта.

    (Почти месяц лишь вяло исследовал вопрос, читая документацию, но почти ничего не делая, из-за пребывания в а*уе от происходящих в стране событий...)

    "Решения" -- разделение обязанностей между файлами таково:

    • cxsd_fe_epics.c является "нативным" интерфейсом для сервера, плюс он же реализует и всю функциональность (т.к. для доступа к потрохам cxsd_hw нужно #include'ить cxsd_hwP.h, что плюсовому коду недоступно).
    • А cxsd_fe_epics_meat.cpp является лишь интерфейсом к libcas, реализующим надлежащие отнаследованные классы.

      Но с точки зрения РЕАЛЬНОЙ РАБОТЫ -- просто переадресующим запросы к C'шной части.

    • Таким образом, ход исполнения "вышивает" туда-обратно между C и C++.

    Мытарства:

    • Во-первых, tutorial несколько устаревш: в частности, у конструктора 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*)
      
    • В Makefile нужно указывать ещё -lcas в дополнение к тем -lca -lCom, что для клиента.
    • После устранения этой проблемы модуль стал грузиться, но тут же делается exit(1) -- и хрен поймёшь, почему:
      • gdb показывает, что создаётся ещё один процесс, который и исполняет exit(1), но ничего не говорит о причине.
      • "strace -fs200" почему-то НЕ показывает порождения второго процесса.
      • "ltrace -fSs200" показывает порождение, но в его выводе надо отдельно нетривиально разбираться...

      Надо напихивать отладочной печати...

    • Чуть позже: тьфу ты!!! Это у меня в create_epics_Server() было забыто "return 0", вот СЕРВЕР сам и завершался!!!

      Исправил -- теперь запускается!

    • ...но вот о приходящих запросах ни гу-гу -- а я пробовал caget'ом.

      Возможно, дело в том, что процесс 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". Результаты:

    • Тот bind() к порту 5065 -- это способ проверить уже-запущенность caRepeater'а, чтобы если его нету (т.е., bind() удался), то запустить.

      Прочитано в мэйллисте в сообщении "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 R3.14 Channel Access Reference Manual" --
      • тут рассказывается и о том, зачем нужен CA Repeater (чтобы размножать broadcast'ы ОТ СЕРВЕРА клиентам, внутри клиентского хоста -- т.н. "beacons"; нафиг они нужны, мне неясно -- представляются странной концепцией),
      • и о назначении портов 5064 и 5065: 5064 -- порт сервера ($EPICS_CA_SERVER_PORT), 5065 -- порт repeater'а ($EPICS_CA_REPEATER_PORT).

      И вообще много общей информации о функционировании протокола и настройке работы в сетях.

    • "Channel Access Protocol Specification" -- тоже пользительно прочитать для общего понимания.

      Плюс, это третий из нагугляемых результатов, а дальше уже малоинтересное.

    23.03.2022: а теперь, с полученным осознанием, надо провести ещё немного диагностики:

    1. Шлёт ли реально caget при ЗАПУЩЕННОМ caRepeater'е что-то на 5064/udp?

      Да, шлёт. Причём по КУЧЕ адресов -- на все интерфейсы на их broadcast-адреса.

    2. А если раз в 1 секунду проверять готовность сокета/дескриптора 7 (который привязан к 5064) на чтение?

      Если вдруг станет -- значит, дело лишь в том, что никто не мониторит сокет, так что надо читать tutorial дальше на тему "как интегрироваться с fdmanager'ом".

      Посмотрел -- нет, почему-то НЕ готов.

      • Более того -- попробовал netcat'ом:
        nc -vvvlu 0.0.0.0 5064
        -- ничего не ловит.
      • Хотя и пакет от другого netcat'а (nc -u localhost 5064) ловит, и, будучи запущенным на порт 8012, ловит пакеты CXv4'шного UDP-резолвинга.

      Вот как так, а? Почему в CX это всё просто работает, а в EPICS -- фиг?

      Чуть позже: РАЗОБРАЛСЯ!!!

      • Дело было в фильтрации/firewalling'е: CX'ные порты пропускаются, а EPICS'ные -- фильтруются.

        (А "nc -u localhost 5064" работало потому, что интерфейс loopback в фильтрации не участвует.)

      • Добавил в /etc/sysconfig/iptables соответствующие 4 строчки...
      • ...и всё заработало: и netcat ловит, и сокет/дескриптор 7 (который привязан к 5064) становится готов на чтение. Ура-ура-ура!

    Итого -- да, теперь надо учиться интегрироваться с fdmanager'ом.

    24.03.2022: изучаем оный fdManager, с которым есть странности -- присутствуют аж ДВА .h-файла, fdManager.h и fdmgr.h; может, кто-то из них устаревший вариант или один является обёрткой для другого? Гуглим на тему "fdManager.h fdmgr.h".

    • В EPICS Application Developer's Guide (EPICS Base Release 3.14.12, 17 December 2012) в разделе "19.12. fdmgr" говорится, что
      File Descriptor Manager. fdManager.h describes a C++ implementation. fdmgr.h describes a C implementation. Neither is currently documented.

      Т.е.,

      1. Это не старое/новое и не обёртки, а разные реализации для разных языков.
      2. Документация отсутствует.
    • Ручной просмотр файлов fdManager.cpp и fdmgr.cpp из base-3.15.6/src/libCom/fdmgr/ показывает, что это ДЕЙСТВИТЕЛЬНО РАЗНЫЕ И НЕЗАВИСИМЫЕ реализации.

      Крайне странное решение, особенно с учётом того, что сами реализации ОБЕ на C++.

    • А в CAS_tutorial.pdf есть такой фрагмент:
      // 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.

    @вечер: делаем.

    1. В cxsd_fe_epics.c добавлена call_fdManager_process(), которая:
      • epics_init_f()'ом регистрируется на 1-й вызов через 1us (т.е., просто СРАЗУ после запуска основного цикла),
      • а далее через каждые 100мс.
      • Причём tid запоминается в fdManager_process_polling_tid, чтобы в epics_term_f()'е разрегистрировать.
      • Ну а сама она вызывает cxsd_fe_epics_meat_do_poll().
    2. cxsd_fe_epics_meat_do_poll() же
      • Является просто C'шным переходником-адаптером к
      • полноценной C++'ной 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 сработал.

        • Как указывать "зависни в этом основном цикле process() навеки"?

          Похоже, что никак -- ни в коде ничего не видно, ни даже идеологически неясно, как бы можно было указывать "навечно" вещественным числом (разве что просто большое значение), т.к. никакого аналога select()'ового "timeout=NULL" тут не сделаешь (NaN?).

        Вот так -- ради реально ОДНОЙ строки кода (всё остальное -- обвязка для её вызова) пришлось проводить кучу разбирательств. Как же я "обожаю" EPICS, с его совершенно лишней C++'ностью ("язык write-only") и "понятностью" кода...

    Проверяем -- ну да, работает! Отладочная печать в pvExistTest() показывает приходящие запросы.

    Кстати, эта же отладочная печать, самим ХАРАКТЕРОМ печати -- пачка одинаковых запросов подряд, потом реже, потом ещё реже -- показывает странность (мягко говоря; "странность" -- в плохом смысле) устройства резолвинга в EPICS.

    • Вот нафига, спрашивается, слать пачку?
    • Ну не ответили тебе на первый запрос -- а с чего ты взял, что на сразу же посланные второй (и третий) ответят? Чем следующий отличаются от первого? Просто создаётся этакий мини-шторм.
    • Почему просто не слать запросы раз в несколько секунд (как в CXv4, да)?
    • Если сервер сразу не ответил -- значит, нету его; а значит, ни через несколько миллисекунд, ни через полсекунды, ни через секунду он, скорее всего, и не появится.
    • Достаточно слать запросы с некоторой периодичностью; когда появится, -- тогда и ответит.
    • Ведь ОТСУТСТВИЕ сервера -- это нештатная ситуация, если сервера нет, то работать всё равно низзя, а эти "штормы" от каждого клиента только мешают работе сети.
    • И да, сама концепция рассылки "beacon'ов" -- странноватая.

      Битым текстом: то, что СЕРВЕР (которому по определению положено СЛУШАТЬ) по собственной инициативе РАССЫЛАЕТ незваные (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, никакого "внешнего интерфейса" для интеграции со сторонними основными циклами там и нет.

    27.03.2022: а ведь CA работает вроде бы на том же fdManager'е, и, следовательно, 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: технологический вопрос:

    • Ведь, судя по принципам устройства libCAS, оно требует наличия (точнее, СУЩЕСТВОВАНИЯ) объектов класса, отнаследованного от casPV на всё время открытости PV.
    • Но у нас-то предполагается, что аллокирование всей информации per-PV-объект производится на стороне C'шного cxsd_fe_epics.c, в SLOTARRAY'е.
    • Он там, конечно, может в moninfo_t выделить место под наследника casPV, ...

      А вот и нет -- НЕ МОЖЕТ. Объём-то неизвестен... Так что придётся хранить там просто указатель на объект, аллокируемый с помощью new в обычном порядке...

    • но КАК ИНИЦИАЛИЗИРОВАТЬ ОБЪЕКТ по указанному адресу (а в конце и деструктировать)?

    Гугление на тему "c++ create object at specific address" сразу дало результат: да, можно, называется это "placement new" и "placement delete".

    • Есть даже статья в Википедии -- "Placement syntax"
    • Собственно синтаксис такой:
      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*.

      • Плюс, пришлось добавить инициализацию (0 и NULL) и "подчистку" в epics_term_f().

        ...а возможно, что стоит также добавить и явную инициализацию в epics_init_f()

    • Отдельная мысль: учитывая наличие у нас концепции "{R,D}", EPICS'у неизвестной, понадобится также {R,D}-конверсию выполнять тут же. В cxsd_fe_starogate она тоже есть, но только для скаляров, что радикально упрощает её проведение.

      Тут же, возможно, понадобится аллокировать память (сохраняя указатель в moninfo_t) под махинации с векторными каналами.

      ...естественно, только для векторных. Тут можно сделать как в cda, где есть refinfo_t.valbuf, но только "хранить"-то не нужно, поэтому оный valbuf будет просто переменной в функции, выполняющей конверсию.

    • 29.03.2022: отдельный вопрос -- а что делать с ТИПАМИ ДАННЫХ?

      Ведь у нас каналы в основном int32, но для клиентов они эффективно вещественные, так что тут "светить" их наружу целочисленными не удастся, т.к. результат преобразования ну никак не целый.

      Напрашиваеются варианты:

      1. ВСЕГДА светить наружу как double.

        Так себе вариант (для реально целых и прочих однобитовых просто бред).

      2. Светить наружу как double только те, у кого присутствуют {R,D}.

        Проблема: а если драйвер загрузится ПОСЛЕ подключения клиента?

      3. Мочь указывать тип как-то в имени канала ("@i", "@d"?).

        Максимально гибко, но как-то некрасиво...

      30.03.2022: у класса casPV есть опциональный метод bestExternalType(), с комментарием "tbe best type for clients to use when accessing the PV". Сходу проку не видно (ну чисто отдавать основное представление), но вдруг что придёт в голову...

    29.03.2022: немного:

    • Поскольку в libCAS есть возможность на запрос создания отвечать "нет памяти", а в cxsd_fe_epics_createPV() потенциально есть ситуации с обломами по этой причине, то формализованы коды CXSD_FE_EPICS_ERROR=-1 и CXSD_FE_EPICS_noMemory=-2.
    • И в fe_epics_Server::createPV() добавлена их обработка (реально -- скорее второго, а остальное считается "по умолчанию" при mid<0).
    • Поскольку надо будет хранить в мониторе указатель на "наследника casPV", плюс, возможно, понадобится ещё какая-то своя информация, то создан класс-контейнер 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:

      • Некий метод caServer::pvAttach(), у которого в описании про возвращаемое написаны как раз те варианты, что на стр.25.
      • А ниже, после поиска по "createPV", такой фрагмент (bold мой):
            // 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 );
        -- работает, а по указателю нет -- хбз...
      • Переделал свой код на такой вариант -- оно откомпилировалось.
      • Более примеров, увы, не на-grep'илось.

      Почему всё так, как оно есть -- непонятно. Как правильно -- тоже непонятно...

      31.03.2022: пришла в голову мысль, как проверить, что всё работает корректно, без лишнего копирования/дублирования casPV'ей, а просто указатель нужным образом сохраняет: сделать "другие" методы -- конкретно read() и write() (чтоб можно было их дёрнуть посредством caget/caput) -- и в них вставить отладочную выдачу значения this; так будет видно, один и тот же ли это экземпляр объекта или разные. Сделал, проверил -- да, значения this везде одинаковые.

      02.04.2022: fe_epics_Server::createPV() удалена.

    • Возвращаемся к fe_epics_PV():
      • Оказывается, теперь конструктор casPV:casPV() стал БЕЗ параметров, а вариант с параметром "caServer &" числится "deprecated".
    • Немножко информации, полученной в процессе:
      • Кстати, в base-3.15.6/src/ca/legacy/pcas/generic/caServer.cc нашёлся такой кусочек, иллюстрирующий взаимоотношения "старого" и "нового":
        pvAttachReturn caServer::pvAttach ( const casCtx &ctx, const char *pAliasName )
        {
            // remain backwards compatible (call deprecated routine)
            return this->createPV ( ctx, pAliasName );
        }
        
        (А я-то гадал -- "как же оно выбирает, старым или новым способом зарегистрировать PV по имени?").
      • А встречалось ли уже "Channel Access Servers" Kenneth Evans, Jr. October 8, 2004?

        Наткнулся при гуглении "Kenneth Evans, Jr.October 8, 2004".

      • Надо использовать base-3.15.6/include/casdef.h в качестве документации -- там простыни комментариев, в которых не только пояснения (собственно "комментарии"), но и подробные описания правил работы, вроде того, когда и кем должна освобождаться занятая объектами память.

    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().

    • Информация по GDD:
      • "GDD User's Guide" -- древний текст, updated 9/13/96, автор Jim Kowalkowski.
      • "GDD Reference Manual" -- ещё древнее, updated 4/12/95, автор тот же Jim Kowalkowski; на него ссылка из предыдущего.
      • "Next Generation EPICS Interface to Abstract Data" -- это доклад THAP014 с ICALEPCS-2001, авторы Jeffrey Hill и Ralph Lange.
        • Там даётся обзор существовавших на тот момент подходов: CORBA, GDD, CDEV.
        • "с GDD что-то пошло не так":
          • Видимо, потому, что (стр.4, начало) "Users appear to find interfacing with this approach daunting, probably because they must constantly convert between their native storage formats and the communication system's imposed data container." (да-да, ЭТО я прекрасно вижу).
          • По результатам МОИХ поисков -- не используется libGDD более нигде, кроме как в libPCAS; по крайней мере, судя по «grep 'include.*"gdd' base-3.15.6/**/*.c*»
          • 01.04.2022: и в EPICS7 -- base-7.0.2.2/ -- буквосочетание "gdd" в именах файлов не встречается вовсе.
        • Поэтому они предлагают (стр.3, "5 INTERFACING WITH PROPRIETARY DATA -- ANOTHER APPROACH") иной вариант: "a C++ abstract base class (an interface) that is used to introspect the structure of the arbitrarily complex proprietary data".

          Уж не он ли лёг в основу EPICS7/pvAccess? 01.04.2022: да, похоже, что оно самое: Data Access -- предок pvAccess'а. Но в явной форме по «site:epics.anl.gov "data access" pvaccess» ничего не нагуглилось.

      • 01.04.2022: "Data Access - Experiences Implementing an Object Oriented Library on Various Platforms" связанный доклад THAP015 на том же ICALEPCS-2001 от тех же авторов.
        • Там немного говорится о том, что Data Access задуман на смену Channel Access'у и приводятся некоторые соображения.
        • Далее же в основном рассказывается о борьбе с компиляторами C++: и что с соответствием стандартам там проблемы и с производительностью (речь про 2001г), а главное -- вопросы оптимизации: по умолчанию template'ы генерят дичайший объём кода (15*15=225 экземпляров), и говорится, что предпринималось много мер для уменьшения объектника с 8MB до 193KB.
      • 01.04.2022: и ещё чуток от Jeff Hill в то же сторону:
        • "EPICS Server-Level API Developers Survey" -- постерный доклад MC1P58 на ICALEPCS-1999 в Trieste.

          09.07.2022: В отзывах (Table 2 на стр.2/549):

          1. критикуется GDD за сложность и
          2. за "difficulties occurred when using the string class";
          3. а также за то, что приходится связываться с "data conversion".

          Помимо содержания хороша как сборник ссылок. В т.ч. там упоминается и работа

          "A Server Level API for EPICS", на которую я ссылался в кандидатской диссертации (по метке johill-icalepcs95), но теперь по той ссылке недоступная, а доступная через web.archive.org.

        • "Next Generation EPICS Communication Protocols" -- постерный доклад MC1P57 там же.
        • ...а ещё интересно было бы найти текст публикации "EPICS Communication Loss Management", ICALEPCS'93, Berlin, 1993, pp 218-220 (DOI 10.1016/0168-9002(94)91505-9). Весьма вероятно, что там как-то обосновывается существование "beacon'ов" и "beacon anomalies", caRepeater'а, и прочей фиготы с broadcast'ами.

          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.

      • 05.07.2022: и ещё немножко в ту же сторону "общая информация про EPICS и системы управления вообще" (нашлось это гуглением на тему «Kuiper "Issues in Accelerator Controls"» -- это первая статья, на которую очень много кто ссылается, почему и стал искать первоисточник; причём роляет, ОТКУДА гуглить: из РФ часть не показывает, ссылаясь на какие-то запреты (которые сами фиг узнаешь)):
        • "ICALEPCS-91 Proceedings", конкретно на страницах 602-611 (618-627 в PDF) оральный спич
          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 следующих.

          ...Но после прочтения выяснилось, что ТЕХНИЧЕСКОЙ КОНКРЕТИКИ там НЕТ!

          • Сначала говорится о том, что проблемы, считавшиеся сложными, реально проблемами быть перестают -- по причине расширявшегося тогда ассортимента готовых аппаратных решений, которые можно просто купить: PC (дешевле workstations), сети, карты В/В. Плюс TCP/IP вместо самодельных протоколов.
          • А главное, что там сказано -- что "надо бы перестать каждой лаборатории изобретать свой велосипед", поскольку хоть установки и их СУ и различаются, но постоянно вылазит общая функциональность, присутствующая практически повсеместно.

            И что если объединить усилия (и тут EPICS приводится как пример совместной разработки), то все выиграют от снижения костов.

          • Только для определения "общих потребностей" надо бы собрать воркшоп человек из 20.
          • Термин "standard model" тут хоть и вводится, но НИКАК не объясняется. Просто "вот надо бы что-то стандартное придумать, что наиболее правильно и будет работать для всех", но ЧТО -- в тексте не прописано (может, на слайдах было).

            Возможно, просто подразумевается FreonEndControllers и Workstations на разных сторонах СУ, обзающиеся через сеть).

            Но ЯВНО это проговаривается лишь в следующей презентации от Dalesio за 1993-й.

          ЗЫ: CERN Courier June 2016, pp.41-42: Berend Kuiper 1930-2016 (некролог). Оказывается, он был основателем ICALEPCS, в 1985-м.

        • "Control System Architecture: The Standard and Non-Standard Models", M. E. Thuot, L. R. Dalesio, PAC1993
          • В abstract'е наконец явно говорится:
            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.
          • Далее обсуждаются 3 компонента этой стандартной модели, всякие нюансы, вроде времени реакции/частоты обновления ("Operator interface factors"); способы организации сети ("Data communication factors") -- Ethernet, FDDI (в т.ч. цены в $ упоминаются -- теперь, 30 лет спустя, они выглядят дико, а те проблемы теперь и не проблемы вовсе); нюансы исполнения closed loop'ов за заданное время.
          • А в конце речь о НЕстандартных случаях -- "Non-standard models". И там говорится в основном о случаях с нестандартно высокими требованиями: очень много каналов (SSC) либо требования быстрой обратной связи (APS).

            В обоих примерах показывается РАСШИРЕНИЕ стандартной модели: в первом некие линки OC3 и T1, во втором -- reflected memory между VME-контроллерами.

          Но вовсе НЕТ НИКАКОГО обсуждения того, что будет, если сделать реально что-то по СОВСЕМ ИНОЙ архитектуре, чем стандартная модель, и чем бы это грозило.

          Плюс, кроме общего набора компонентов -- FEC+network+OPI -- там ничего не говорится о том, какого "рода" данные между компонентами гоняются. Потому-то у EPICS -- каналы (более-менее унифицированные), а у Энди Гётца -- дикий винегрет разных сомнительно структурированных сущностей.

        • "Создание комплекса унифицированных средств управления электрофизическим оборудованием и применение их на каналах частиц и стендах ИФВЭ", Алферов, Владимир Николаевич, 2003

          Похоже на докторскую диссертацию.

    31.03.2022: кстати, надо бы в явном виде записать давно роящиеся в голове планы насчёт работы с libCAS. Итак:

    • Главная идея: реализовать cxsd_fe_epics, а потом, с полученным опытом, заняться интеграцией libCAS'а и TANGO.
    • cxsd_fe_epics:
      • Для начала -- просто скаляры, хотя бы в простейшем варианте.
      • Затем -- также векторные каналы.
      • ...ну и по ходу дела, по мере получения опыта, добавлять всякие нюансы вроде отдачи базового типа; возможно -- также диапазонов и прочих деталей.
    • Интеграция с TANGO: тут вопрос также в названии; TANGO-CAS?
      • Допилить cda_d_tango, включая поддержку синтаксиса "->attribute".
      • Спросить у Cosylab'овского автора "EPICS to TANGO translator" (из статьи), почему они не стали использовать libCAS, а сделали отдельный "gateway".
      • Спросить Сашу Сенченко на тему "есть ли в TANGO внутрисерверный API?".
      • Затем спросить об этом же самих танговодов.
    • 07.04.2022: а когда это всё освоим, то можно будет и на pvAccess замахнуться (и на cda_d_pvaccess, и на cxsd_fe_pva), и на "TANGO-PVA".

      ...кстати, попробовав погуглить на тему "tango pvaccess":

      • Никаких свидетельств существования "обще-годной реализации" (типа плагина) не нашёл.
      • Только упоминание этих 2 слов в описаниях вроде Tango2Epics.
      • Плюс любопытный документ "Data Addressing Specification" за 2018 год от Matej Sekoranja, Cosylab Sweden.

        Там обсуждается построение имён каналов ("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. ...
        <scheme>:[//<authority>]</path>[?<query>][#fragment]
        where authority is in a form of:
        [<user>@]<host>[:<port>]
        If authority part is omitted then discovery mechanism is used. Channel Access does not support query and fragment part.

    02.04.2022: переделываем API "cxsd_fe_epics_createPV()"; цели:

    1. Поддержка проверки "с таким именем канала монитор уже зарегистрирован".
    2. Приведение имени в соответствие -- что не "createPV", а "pvAttach".

    Итак:

    • 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() она переведена на "обновлённый" вариант, где:
      1. Добавился параметр clientAddress (в будущем можно использовать для access control'а);
      2. pPVName переименован в pPVAliasName.

      Старый вариант всё равно числится как "deprecated".

    02.04.2022: пора уже делать и чтение/запись и уведомления. В порядке движения в том направлени...

    Принимаем такую парадигму разделения обязанностей между C'шной и C++'ной частями:

    • cxsd_fe_epics.c -- взаимодействие с сервером; но НИКАКОГО принятия решений.
    • cxsd_fe_epics_meat.cpp -- взаимодействие с libCAS, плюс на нём же всякая логика вроде "можно ли этот канал отдавать по EPICS?".

    В связи с этим очевидное решение: проверку на годность (в т.ч. сейчас -- скаляр ли) нужно выносить в 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 -- да-да, сейчас -- ПРИНУДИТЕЛЬНО.
        • Производит {R,D}-конверсию.

        А затем вызывает свежевведённую...

      • 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}...
    • ...сводящийся (см. в gddScalarI.h) к "*((gdd*)this)=d" -- т.е., к вызову того же самого метода из родительского класса, gdd (и нафиг вообще было что-то определять?).
    • А gdd'шные операторы присваивания имеют в gddI.h определения такого вида:
      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().
    • Исполнение RD-конверсии в нужном виде -- внутри сервера, для скаляра -- уже есть в cxsd_fe_starogate.c, в обоих направлениях.
    • @Совет-по-СКИФ, ~15:20: но ведь "чтение" могут запросить в произвольный момент, и совсем не факт, что имеется свежее значение. Поэтому нужно вместе со значением отдавать и его timestamp.

      @получасом позже, в 613-й: да, есть метод setTimeStamp(), ему можно передавать struct timespec *.

    • Остаётся лишь один вопрос: а ОБНОВЛЕНИЯ (по событию UPDATE) как делать?

      Но это, видимо, надо всё же документацию читать.

    Кстати, провёл опрос "потенциальных клиентов" на тему, что именно им будет нужно -- скаляры или векторы:

    1. Старостенко на стенде магнитных измерений нужно логгировать и скаляры (всякие температуры), и вектора (и векторные измерения (чисел по 20) и осциллограммы "развёртки по времени" (512 чисел, 4*128)).
    2. ЕманоФеде для начала хватит и скаляров, но и вектора нужны -- конкретно осциллограммы.

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

    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?":

    • Уметь понимать имена вида "ИМЯ_КАНАЛА._raw", и в таких случаях просто отключать RD-конверсию.
    • Для чего достаточно будет даже не "отключать конверсию", а никогда не складировать наборы {R,D}, и тогда автоматом будет всегда использоваться нативный тип.
    • КАК реализовывать это "._raw":
      • ввести в moninfo_t поле "kind", в котором и сохранять, что это за подвид.
      • По умолчанию 0 -- это обычный канал, 1 -- "raw".
      • Этим же способом можно поддерживать и иные варианты, обращающиеся к АТРИБУТАМ канала: всякие "_units", "_rangemin" и т.д.
      • И, соответственно, RD получать и использовать только при kind==0.

        А иначе и EVMASK_RDSCHG не заказывать, а если и не ==0 и не ==1, то и EVMASK_UPDATE не заказывать.

    • (Чуть позже) быстрый просмотр кода показал, что
      • ВСЁ в cxsd_fe_epics.c -- включая RD.
      • А cxsd_fe_epics_meat_update() получает уже триплет (dtype,nelems,data).

      Ну вот в cxsd_fe_epics.c и будет вся эта специфика, а meat пусть получает уже готовые данные.

      ...а ещё ему надо будет также и timestamp передавать.

    • @~15:00, после реализации GetChanInfo(): и довольно очевидно, что для функционирования fe_epics_PV::read() нужен "accessor", добывающий ей тот же квадруплет данных, что даётся update()'у -- (dtype,nelems,data,timestamp).
    • @~16:30, около стадиона НГУ по дороге на Морской-46: а вообще надо ли update()'у сразу отдавать хоть что-то?

      Пусть он САМ вызывает тот же самый "accessor".

    11.06.2022: делаем.

    • Добавляем поле moninfo_t.kind.
    • И константы MON_KIND_VAL=0 и MON_KIND_RAW=1.
    • Процедура резолвинга вытащена в GetChanInfo():
      • Сначала проверяется, не оканчивается ли имя на один из специальных суффиксов. Если да, то запоминается, что kind такой-то вместо KIND_VAL, а затем от имени этот суффикс откусывается (точнее, обрезанный вариант складируется в локальную namebuf[] -- чтоб не трогать исходник).
      • Затем делается обычный CxsdHwResolveChan(), который если успешен, то...
      • ...по указателям возвращаются результаты резолвинга (причём при kind!=MON_KIND_VAL форсится phys_count=0) и возвращается сам 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().
    • И возвращает он не триплет и даже не квадруплет, а КВИНТЕТ -- (dtype,nelems,data,rflags,timestamp).

      ...что именно мы будем делать с 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():
      1. Инициализируем все значения (ЛОКАЛЬНЫЕ переменные с именами, совпадающими с параметрами, но без "_p") данными из chn_p.
      2. ЕСЛИ phys_count != 0, то выполняем конверсию:
        • Вычитываем 1 значение в double v;
        • производим RD-конверсию;
        • складируем результат: если исходный тип CXDTYPE_SINGLE -- то, так и быть, его и оставим; а иначе -- превращаем в CXDTYPE_DOUBLE.
      3. Возвращаем значения по указателям, проверяя каждый на !=NULL.
    • К 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, пешком с продуктами из Быстронома:

    • А вообще надо бы получше посмотреть на "общий" API складирования данных в GDD ("adjust"?), который позволяет и просто указатель на реальные данные сохранить:
    • у нас ведь всё равно персистентное хранилище в moninfo_t, так что нехай в любой момент может туда заглядывать -- из-за однопоточности там всегда будет консистентная информация.
    • И это автоматом покатит для векторных данных -- тем самым мы просто сведём всю передачу до того же (тип,количество_элементов,данные), что и с libca.

    Проверяем -- хрень какая-то:

    • Целочисленные почему-то доходят до caget'а как "0.0000".

      При переходе на 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() (тогда не понадобится никакой "адаптер"), причём...
      • ...делать это ДО регистрации evproc'а -- тогда не понадобится и никакой флаг, т.к. событие физически не сможет дойти.

      Решение выглядит очень простым и элегантным.

      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() --

      • да, действительно, по 1 штуке pvExistTest() на каждый вызов поллинга... Во дебилы-то, а....
      • И метод process() -- он void, т.е., НЕЛЬЗЯ проверить, сделал ли он что-то (и если "да", то повторить по repcount'у).

      13.06.2022: добавил цикл по repcount в 30 итераций -- да, стало обрабатывать пакеты сразу пачками и проблема "not found" вроде исчезла (но ясно, что в таком варианте это всё чисто вопрос статистики/вероятности).

    • А главное -- валится по SIGSEGV'у!!! Причём -- в коде libCAS:
      #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():
        1. из переданного ей value узнаёт егойный тип с помощью метода primitiveType(),
        2. затем "вычитывает" значение простым присвоением полю соответствующего типа (в CxAnyVal_t), с уставлением на него указателя data и прописыванием соответствующего типа в dtype,
        3. после чего вызывает специально для этого созданный "метод" из C'шной части --
      • 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).

    Некоторые мысли и соображения на будущее:

    • А может, дело в НЕреализованности метода destroy()? И потому ВТОРОЙ вызов гикается?

      См. casdef.h раздел комментариев "Deletion Responsibility" в определении casPV.

    • Видимо, надо реализовывать virtual aitEnum bestExternalType () -- он должен возвращать что-то вроде "dtype2aitEnum"?

    15.06.2022: немного соображений о том, "за что я ненавижу libCAS и GDD":

    • Отвратительная документация (Tutorial). Ладно ещё, что она 1999 года и часть изменилась/устарела.

      Главная проблема -- там НЕТ внятного описания "надо делать то-то и то-то" (в т.ч. пропущены ключевые аспекты, вроде деструктирования casPV и отдачи libCAS'у информации о PV), а вместо этого МОРЕ "информации" об "атрибутах" и "таблицах конверторов", что вообще-то НЕ относится к СУТИ задачи, а более полезно для простейшего примера.

      Но вынуждены они это были сделать потому, что...

    • использована кривая архитектура "клиент libCAS'а должен быть в курсе EPICS'ных извращений с форматами сетевого обмена (все это _STS_, _GR_, _TIME_, _CTRL_)".

      Хотя очевидно правильным вариантом было бы ни в коем случае не выпускать эту нечисть за пределы libCAS'а -- *ОН* должен был бы воспринимать эти извращения, а от своего клиента просить лишь данные (ну, или, ладно -- отдельно/опционально атрибуты).

      Но нет, правила инкапсуляции категорически нарушены.

    • Притом, что сама EPICS'ная идея с "DBR_STS_", "DBR_GR_", "DBR_TIME_", "DBR_CTRL_" является жутким мисдизайном: тут смешиваются совершенно ортогональные сущности: сами ЗНАЧЕНИЯ и их СТАНДАРТНЫЕ АТРИБУТЫ.
    • А ещё в GDD есть отдельный "aitEnumEnum16". Но это же чистый EPICS'изм, а в реальности никакого типа "enum16" не существует (как и enum вообще, кстати).
    • ...недосформулированно: и даже примерно понятно, ПОЧЕМУ такое перезамудрённое решение сгородили: очевидно, ещё тогда, в конце 1990-х, пытались придумать какой-то способ поддерживать произвольные структуры данных, вот и сварганили этого монстра.

      Но в результате он и ту глобальную задачу не решил, и текущую/реальную -- ПРОСТЕЙШИЕ взаимодействия -- выполняет плохо.

    Итого: изначально негодный дизайн, плюс слабополезная документация.

    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: роем далее...

    • Первым делом реализуем destroy() вот такого вида:
      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 ();
      }
      

      Откуда напрашивается мысль: раз ЧТО-ТО с этим именем всё же дёргается, то, может, просто "что-то НЕ ТО"? Т.е., сигнатура по параметрам не совпадает?

      Но вроде смотрел -- да нет, всё совпадает: список параметров пустой...

    • Обновления -- update -- делаются посредством casPV'шного метода 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()...

      @днём, ездя по делам: А если отнаследовать?

      @вечер: Попробовал -- да, можно (хотя и с тучей сопутствующих проблем):

      • Поле fe_epics_PV.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, это явно правильнее.

      • И дОбыча значения параметра "casEventMask & select" для value -- тот ещё квест: оно НЕ константа, а добывается методом 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++, вот никаких обновлений и не происходило -- попросту чтение не заказывалось.
      • Обнаружил, заметив, что единодлы прочитанное при первом коннекте первого клиента значение так и остаётся, пока на этот же канал не натравить cdaclient.

        Что насторожило: а почему нет чтений?

      • Полез смотреть, где делается "periodics++", и обнаружил, что НИГДЕ.
      • А причина была в том, что этого не было и в cxsd_fe_starogate.c, с которого скопирован код.
      • Вот в cxsd_fe_starogate.c и исправил, предварительно проверив telnet'ом при нём загруженном, что обновлений реально нет.

        ...и как только Алексей Медведев на сварке работает -- уже ТРИ года!!! 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: возимся дальше.

    • 20.06.2022@вечер-душ: ну чудес ведь не бывает -- надо бы внимательно/пристально ещё раз пересмотреть кусок кода с bestExternalType(), должно же там быть какое-то несоответствие.
    • И, судя по тому, что fe_epics_PV'шный метод НЕ вызывается, а вообще вызов есть (о чём свидетельствует stack trace от былого SIGSEGV'а) -- видимо, как-то не совпадает сигнатура, из-за чего оно и вызывает родительский метод, а этот считает несоответствующим.
    • Первая мысль: может, дело в "режиме доступа" -- protected или public? У нас-то public.

      ...но и в 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?" -- мало что понял, надо больше времени потратить :)

    • А потом вчитался внимательнее, и заметил, что в casdef.h определение
      virtual aitEnum bestExternalType () const;
      а у меня же --
      virtual aitEnum bestExternalType ();
      т.е., без "const".
      • Гугление по "c++ method const" привело на "Meaning of 'const' last in a function declaration of a class?" от 15-04-2009 на Stackoverflow, где сказано

        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" нахрена?!?!?!

      • Чисто для проверки пробуем добавить "const" только к прототипу в определении класса -- voila, ошибка несоответствия:
        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() вызывается!!!
    • Пробуем опять caput -- да, теперь до записи доходит с primitiveType()=6, добытое число соответствует указанному caget'у, но получаем SIGSEGV.

      Там косяк нашёлся сразу: в cxsd_fe_epics_do_write() в вызове CxsdHwDoIO() стояло "mp->gcid" вместо &(mp->gcid) -- в результате этой идиотской почти опечатки передавался указатель со значением 0x68 (=104); немудрено, что падало.

      После исправления -- работает!!!

    OK, теперь краткий план того, что предстоит сделать в ближайшее время для улучшения работы:

    • Перевести обновления на get_info(), убрав прямую передачу данных параметрами.
    • Отсеивать попытки записи в не-rw-каналы. Для чего учитывать rw'шность, узнанную при создании.
    • Научиться сообщать libCAS'у свойства канала: rw'шность, число элементов (длину вектора).
    • Как-то разобраться с клятыми таблицами конвертеров... 07.05.2023: не понадобилось, вычёркиваем.
    • Перейти с штучной работы со скалярами на общую модель векторов -- и получать, и записывать данные в виде указателей.
    • Возможно, избавиться от контейнера fe_epics_meat_PVrec_t, храня в "PV_ptr" указатель непосредственно на саму "PV".
    • 22.06.2022: отнаследовать casChannel -- ради access control'а и ради readonly.

    Приступаем помаленьку -- сначала к простому :)

    • Отсеивание попыток записи в не-rw-каналы. При !is_rw возвращается S_casApp_noSupport.

      Заодно добавлена "проверка" результата cxsd_fe_epics_do_write() -- реально от CxsdHwDoIO() -- при !=0 возвращается та же ошибка.

    • Модификация обновлений:
      • Заведён метод fe_epics_PV::update(), в который скопировано содержимое нынешнего do_perform_update().
      • Содержимое оного do_perform_update() переделано на просто вызов метода update().

        Ну -- с практической точки зрения ничего не изменилось: осталась та же ошибка "No conversion". Но так и должно было быть.

      • cxsd_fe_epics_meat.[ch]: убраны лишние параметры "данные", с оставлением только mid и PV_ptr, у do_perform_update() и cxsd_fe_epics_meat_update().
      • cxsd_fe_epics.c: из SendMonitor() убрано практически ВСЁ содержимое, а оставлен только вызов cxsd_fe_epics_meat_update().

        Раньше там была конверсия -- дублировавшая нынешнее содержимое cxsd_fe_epics_get_data(), но в более древнем варианте, ВСЕГДА генерившем DOUBLE.

    • Пытаемся разобраться со свойствами:
      • Размеры указываются -- точнее, ВОЗВРАЩАЮТСЯ -- методами maxDimension() и maxBound(). Первый отдаёт число измерений -- 0 для скаляра, 1 для вектора и т.д. (но EPICS3 поддерживает не выше 1), а второй размер в указанном измерении.

        Реализовал -- результат обескураживающий: векторные каналы попросту не находятся, как минимум по cainfo.

        @вечер: а-а-а, дошло -- это же cxsd_fe_epics_pvExistTest() сейчас дозволяет только скаляры!!!

      • А вот rw'шность -- там свойство не PV, а "канала": СОЕДИНЕНИЯ с PV, которое класс casChannel и у него методы readAccess() и writeAccess().

        Т.е., там "читабельность" и "писабельность" существуют только применителько к конкретному соединению с клиентом, а свойства "readonly" для PV не предусмотрено в принципе...

        И-ди-о-тизм...

    22.06.2022@душ: лучше сейчас заняться векторностью -- там хоть примерно ясно, что делать: со стороны .c -- аллокирование буферов и векторная конверсия; со стороны _meat.c -- работа с указателями, а не с помощью "=".

    22.06.2022: да, занимаемся векторностью.

    • Соображение "о кэшировании": чтобы минимизировать количество {R,D}-конвертирований в сторону "от сервера к клиентам, из аппаратных единиц в логические", ...
      • ...надо завести флажок "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:

    • а ведь у нас cxsd_fe_epics.c получается по факту ОБЩИМ модулем доступа ("упрощённого") всяких протоколов к серверу.
    • И если его оформить как-то отдельно, то вполне можно на него перевести даже и starogate.
    • И это "отдельно" можно сделать "библиотекой" -- которые *_lib.so и загружаются config-командой load-lib.
    • 03.09.2022@М-46, ~13:00: "csfi" -- CxsdSimpleFrontendInterface?
    • @М-46, ~18:00: но понадобится ввести -- вернуть! -- понятие соединения (в cxsd_fe_epics всё "глобальное").

      Но если будут соединения, то получается ДВА параметра для уникальной идентификации монитора: кроме собственно mid'а нужен ещё и cid, т.к. мониторы-то в каждом соединении нумеруются с 1.

    • @выйдя из М-46, по дорожке между ним и М-42, по пути за мороженым, ~18:45: сделать как в cda -- сквозная нумерация 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'а?

    • Оказалось, что в команде "print" указать нужный контекст/функцию нельзя никак.
    • А вот МОЖНО посмотреть значения, предварительно указав, какой "контекст" интересует, командой "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: пришёл-таки к решению, как обустроить конвертеры.

    • Передаваться им должны наборы параметров, состоящие из 3 групп: 1) откуда, 2) RD, 3) куда:
      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
      
    • А вот ВЫБОР должен производиться в вызывающем:
      • и откуда RD брать (imm_phys_rds или alc_phys_rds),
      • и какой именно dst_dtype выбрать -- то, что в начальной версии выполняется по принципу "если phys_count==0, то оставить исходный тип, а иначе DOUBLE, если только и так не SINGLE".
      • Не говоря уж о выборе места для данных -- valbuf или current_val.
    • Но вот проверки совместимости -- в самих конвертерах, и при несоответствии они возвернут ошибку (в cda_core.c и в cxsd_hw.c при этом делается "goto NEXT_TO_UPDATE").
    • @совет-по-СКИФ, зал большого совета, ~15:30: дополнение: а и ограничение по nelems тоже должны накладывать конвертеры.

      Встаёт вопрос: а как они будут возвращать результат ограничения?

      Ответ элементарен: в возвращаемом РЕЗУЛЬТАТЕ -- он и так int пусть и возвращают. Т.е., <0 -- ошибка, >=0 -- nelems (тем более, что 0 -- легитимное значение).

    • Рабочие названия -- eng2opr() чтение и opr2eng() запись.

      Похоже, содержимое у них будет почти идентичным, с различием только в "направлении {R,D}-конверсии". Прямо хоть макросами делай...

    • 26.06.2022: по результатам попиливания -- уточнение общей схемы:
      • Проверки совместимости и собственно конверсии -- в конвертерах.
      • Взаимодействие с "местами хранения" src и dst -- включая обработку варианта с CXDTYPE_UNKNOWN -- в их клиентах.

      Именно так и ведём разделение кода из cda_dat_p_update_dataset()'а на "конвертерную" и "клиентскую" части.

    • 27.06.2022: а ещё убрана забота об "для TEXT-каналов добавим 1 ячейку и будем заполнять туда NUL" -- пусть это будет исключительно на стороне cda, а тут чистые данные.

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

    27.06.2022: возился несколько дней, продолжаем записывать.

    • eng2opr() в первом приближении "доделана" -- сокращена из cda'шного варианта.
      • Выкинуто очень многое: и недоделанное с orange, и добыча raw-значений, timestamp и rflags.
      • Минус, естественно, и проверки с UNKNOWN.
      • И алхимия с добавлением NUL в концы TEXT-каналов -- для этого пришлось бы задумываться о наличии +1 ячейки в конце.
      • А сделан возврат результирующего (после проверок) 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: вот только при этой переделке-копировании накосячил, так что корректно конвертировался только первый элемент вектора.

    • Размышление на закате: при ЧТЕНИИ-то понятно -- известен желаемый тип и максимальное количество; а вот при ЗАПИСИ как? Ведь там может быть, что буфер рассчитан, например, на 10 штук SINGLE, а в записи будет указано 10 же штук, но DOUBLE, и результат конверсии просто не влезет... Где/как проверять влезающесть в буфер и ограничивать?

      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->ПАРАМЕТРЫ, а должны быть в локальных переменных.
    • Соответственно, в случае "val_is_present" просто производится добыча закэшированного из mp в те же локальные переменные.

      И "подготовка" его -- т.е., когда val_is_present==0 -- эти значения для кжша готовит, в результате чего ещё и в локальных переменных оказываются как раз нужные значения.

    • А вариант zero-copy сводится к тому, что в оные локальные переменные вытаскиваются значения из chn_p.

    Теперь запись в cxsd_fe_epics_do_write():

    • Первым делом -- "zero-copy", что тут вообще тривиально: просто готовые данные и отправляем, при условии phys_count==0.
    • А иначе -- превентивный сброс val_is_present=0 и 3 несложных шага:
      1. Подготовка параметров;
      2. вызов opr2eng();
      3. вызов CxsdHwDoIO().

    Ну а далее пора заниматься "обвязкой" -- аллокированием буфера (при надобности), а далее уже векторной работой в _meat.c.

    • ...аллокирование буфера, оказывается, давно сделано :)
    • Надо переходить к работе с gdd-векторами, и вот тут-то как раз встаёт вопрос -- а КАК?

      Так-то данные записывает метод adjust() -- ему даётся тип и указатель на данные; но вот как указать МАССИВ?!

      (чуть позже) вроде есть там методы -- setDimension()/dimension() и setBound(dim_to_set,first,count)/getBound().

    • @вечер, ~19:30: OK -- ладно, пытаемся сделать хотя бы по минимуму.
      • Для начала сделана 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() закомментированы проверки на скалярность, чтобы теперь и вектора разрешались.
      • Пробуем, запуская сервер с векторным каналом и натравливая caget -- SIGSEGV...

        Где-то очень глубоко в недрах libCAS... Вот какого чёрта в этой монструозине нет вменяемых проверок?!?!?!

        С другой стороны -- это же чтение? Ну так...

      • ...делаем и в fe_epics_PV::read() аналогичную update()'у модификацию.
      • Да, работает!!! Одиночное чтение -- работает!!!
    • За компанию сделана и обратная конверсия типов -- aitEnum2dtype().
    • @~22:30: После чего примерно в том же ключе модифицирована и fe_epics_PV::write().
      • Что любопытно -- "caput -a asdf.1 2 789 543" выполнила запись как надо!
      • А вот скалярная запись -- "caput asdf.1 789" -- SIGSEGV...
      • Как выяснилось анализом через GDB, в случае скаляра это дауниссимо из метода dataPointer() возвращает не указатель, а тупо значение -- в данном случае было 0x315, что и ==789...

        А причина в том, что скаляры они хранят в том же union'е, что и указатель на данные. Причём скаляром считается и результат записи от "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());
        

        ...да-да, вообще-то эту работу должна делать GDD -- отдавать указатель на данные независимо от варианта их расположения; но почему-то они перекладывают её на меня, юзера их библиотеки.

        01.07.2022: вообще-то там есть ещё метод 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();
        }
        

        04.07.2022: да, переведено на использование dataVoid().

      • Но как сейчас -- это халтура: надо бы без дурацких догадок "скаляр или нет", а как-то ФОРМАЛЬНО узнавать, как доступиться к данным.

        Для чего, видимо, придётся порыться в тамошнем коде -- посмотреть, как именно оно указывает, что же лежит в union'е data.

        01.07.2022: ну порылся; по коду gdd::dataVoid(void) отлично видно, "как именно оно указывает" -- да НИКАК! Чисто догадками -- если НЕ-скаляр или FixedString, то указатель, а иначе считаем, что просто значение. 05.07.2022: причём для типа String (не-Fixed!) понятие "значение" -- это не строковые данные, а сам объект aitString.

    • ЗАМЕЧАНИЕ: это всё делалось БЕЗ конверсионных коэффициентов. Т.е., работает лишь режим zero-copy.

      А надо бы проверить и с конверсией...

    29.06.2022: немножко с конвертерами типов:

    • Они перетащены в начало файла.
    • Также и fe_epics_PV::bestExternalType() переведена на dtype2aitEnum().
    • Добавлено соответствие CXDTYPE_TEXT<->aitEnumString; тут небесспорно -- второй альтернативой является aitEnumFixedString.

    Теперь дальнейшие проверки:

    • Проверяем конверсию при помощи "cpoint asdf.z asdf.1 1000" -- т.е., ДОПОЛНИТЕЛЬНЫЙ "канал", а не коэффициент на основном.
    • С конверсией чтение/запись вроде тоже работают.

      Upd: только СКАЛЯРЫ. Читается-то верно, а вот пишется -- как-то странно, только ПЕРВОЕ значение.

    • ...но странно: записанное-то из CX читается записанным, а вот НЕзаписанное (последние ячейки) -- мусор.

      Хотя вроде бы bzero(mp->current_val, csize) исполняется.

    • Может, дело в том, что в cxsd_fe_epics_get_info() чуток своя логика определения dtype ("при наличии конверсии считать всё за DOUBLE, если только не SINGLE")? Может, она как-то отличается от прочих мест?
    • Но странность в том, что если "целевой" канал cpoint'а сделать double -- т.е., объёмом в байтах заведомо не меньше любого "производного" -- то всё равно в первое значение пишется нужное число, а дальше -- мусор...
    • 30.06.2022@утро, тропинка от Морского к Хозяйственному, прямо перед Учёных, ~08:30: можно в eng2opr() добивать отсутствующие данные -- между nelems и max_nelems -- нулями; чтобы это делать ОПЦИОНАЛЬНО, надо добавить параметр "bzero_unset" (а в "общем cxsd_fe_api" клиенты смогут указывать его "необходимость" при создании соединения).

      @дома, ~09:00: но именно только в неё -- в opr2eng() оно нафиг не нужно, т.к. CX-сервер и так за пределы nelems не лазит.

      @вечер, дорога домой, выход из леса перед Пирогова-26, ~18:40: однако это будут дичайшие накладные расходы. Например, если в канале осциллограммы, рассчитанном на 100000 элементов, будет лишь 1000 измерений, то оставшиеся 99000 будут постоянно нулиться почём зря.

    • 30.06.2022@утро, дома, ~09:00: возможно, кстати, что часть проблемы -- в том, что при отдаче данных в GDD мы НЕ указываем число элементов; видимо, оно в результате ориентируется на значение из maxBound().

      Надо бы всё же указывать.

    30.06.2022: разбираемся с косяками при конверсии.

    • Для диагностики в блок конверсии opr2eng() было напихано печатей значений v ДО и ПОСЛЕ конверсии.

      И эта диагностика показала, что на вход поступают правильные данные, но ПЕРВОЕ число конвертируется как надо, а дальше получаются сплошные 0.000 (которые caget'ом и отображаются как очень мелкий "мусор" вроде "4.16183e-317 7.70523e-313 3.06765e-320".

      Что сразу указало конкретное место проблемы -- код конверсии.

    • Внимательный взгляд показал, что там несбалансированный набор сдвигов указателя rdp:
      1. ДО общего цикла делается rdp = phys_rds,
      2. для каждого элемента перед конверсией исполняется rdp += (n-1)*2 -- для перехода на КОНЕЦ списка (т.к. конверсия в обратном направлении),
      3. а ПОСЛЕ очередного шага пересчёта -- rdp -= 2.

      В результате после обработки каждого элемента вектора указатель rdp сползал на 1 дуплет ниже -- за пределы массива, где лежит предшествующее содержимое moninfo_t, каковое в качестве double выглядело как мусор.

      Причиной косяка стало то, что при копировании из DoStoreWithConv() была изменена парадигма работы: там-то оно инициализировалось перед КАЖДЫМ проходом (для каждого элемента массива), а тут сделано "один раз инициализируем, а потом просто гоняем туда-обратно".

    • Обзор вариантов исправления:
      1. Вернуть инициализацию перед проходом для каждого элемента. Точнее, совместить инициализацию с переходом на конец списка -- rdp = phys_rds + (n-1)*2;
      2. делать ПОСЛЕ цикла конверсии лишнее rdp += 2.
      3. Изменить парадигму до упора:
        • Заменить переход на конец списка на rdp += n*2,
        • а шаг rdp -= 2. делать не после шага конверсии, а ПЕРЕД ним.

        Т.е., полноценный "обратный" по отношению к "PTR++" код -- "--PTR".

    • Были проверены все 3 варианта -- да, все 3 годятся.

      Но выбран был 3-й, как наиболее оптимальный (и элегантный/"правильный", кстати).

    • ЗЫ: по-хорошему, и в DoStoreWithConv() стоило бы сделать так же, но покамест не будем -- "не чини то, что работает".
    • Кстати, проверено, что если в массив размером 4 элемента caput'ом отправить 3, то он именно 3 и присылает -- так что и в opr2eng() придёт 3, и мониторирующему cdaclient'у тоже 3.

      А вот сам caput в качестве "New :" напечатает уже 4 числа, последним 0.

    Теперь возвращаемся к проверке работы разных типов.

    • Сначала -- о "вменённом" типе.
      • Вчера при разбирательстве "почему вместо нужных данных мусор" возник вопрос, не возникнет ли путаницы в типах?
        • Т.е., что будет, если "нативный" тип канала меньше размером, чем "производный", который при наличии R будет double?
        • Ведь тогда окажется, что буфер current_val аллокирован на меньший объём, чем будет генериться при конверсии.
        • Чисто в качестве конкретного примера: т.е., если в devlist'е указать НЕ-double?
        • А раз так, то не надо ли всё же помнить в moninfo_t "начальный/присвоенный" dtype?
      • И очевидная мысль: что в таком случае надо прямо при СОЗДАНИИ монитора определять его dtype, сохранять его и ориентироваться уже только на него, более нигде не использовать if()'ы, чтобы везде всё было фиксированно-одинаково.
      • Смотрим на содержимое cxsd_fe_epics_pvAttach(): там размер буфера определяется как
        csize = sizeof_cxdtype(cxsd_hw_channels[gcid].dtype) * cxsd_hw_channels[gcid].max_nelems;
        
        -- упс!!!

        В частности, если тип подстилающего канала INT8, а R есть, то "подразумеваемый" тип будет DOUBLE, который в 8 раз объёмнее...

        Да даже в самом обычном случае с INT32-каналами разница будет в 2 раза.

      • Вывод: НЕОБХОДИМО определять "производный" тип в момент регистрации монитора и затем придерживаться его ВЕЗДЕ.

      Делаем.

      • Замечание: в обсуждении подразумевалось, что "производный"/"подразумеваемый" тип всегда объёмнее, поэтому можно ориентироваться на него.

        По факту-то это действительно так, но лучше бы на это не полагаться и выбирать объём по МАКСИМУМУ из нативного и производного типов.

      • Вводим moninfo_t.ext_dtype -- "EXTernal".
      • В cxsd_fe_epics_pvAttach() её определение.
        • В варианте с дополнительной проверкой, что REPR_INT или REPR_FLOAT (т.е., для НЕчисловых типов "type promotion" не делается вовсе).
        • ...и там много текста кода перетряхнуто.
        • Плюс, при аллокировании выбирается максимум из размеров "нативного" и "мониторового" типов.
      • Переделки на использование ext_dtype коснулись:
        • cxsd_fe_epics_get_info() -- теперь возвращает его безо всяких условий.
        • cxsd_fe_epics_get_data() -- на него заменено условное определение.
    • За компанию с этими действиями была унифицирована проверка -- по типам -- годности канала для "экспорта" обобщена в IsSuitableForExport().
    • Проверяем.
      • Чтение и запись теперь работают как положено.
      • Один нюанс: ЧИТАЕТ libcas ВСЕГДА столько элементов, сколько указано в размере PV.

        Т.е., сначала-то лишние заняты нулями.

        Но вот если записать "123 234 345 456", а потом "987 876", то будет считаться "987 876 345 456" (а вот cdaclient покажет правильные 2 элемента).

    • Проверяем работу строк... -- Падает... После вызова read()...

      04.07.2022: подразобрался -- да, просто НЕ МОГЛО НЕ ПАДАТЬ. Нужно совсем иначе со строками обращаться.

    01.07.2022:

    • ...а ещё надо бы заново проверить работу скаляров. Судя по узретой логике функционирования dataVoid(), вариант, когда данные установлены adjust()'ом при помощи указателя, может некатить...

      04.07.2022: проверил. Ну да -- вместо значения ДАННЫХ оно берёт значение УКАЗАТЕЛЯ на данные...

    01.07.2022@совет-по-СКИФ в пятницу, ~15:30: некоторое количество мыслей по реальному взаимодействию с EPICS.

    • А что у нас с case-sensitivity? Ведь CX -- case-insensitive, но для EPICS имена с разным регистром -- это РАЗНЫЕ PV.

      И как это в сумме работает? Что, если попробовать ДВАЖДЫ обратиться к одному имени в 2 разных регистрах -- нормально ли отработает или глюканёт?

    • Учитывая, что для EPICS стандартный разделитель ':' (а в CX'ных именах их быть не может), то стоило бы при наличии в имени хоть одного ':' проводить замену их всех на '.', после чего повторять попытку поиска.

      Благо, готовый буфер для проведения такой замены есть -- namebuf[].

      Только надо правильно сочетать эту замену с попыткой интерпретации суффиксов как специальных имён каналов.

    • Пора уже думать о поддержке всяких искусственных каналов-свойств -- rangemin/rangemax, units, comment.

      Они явно разбиваются на "классы", где поведение абсолютно одинаковое, но различие лишь в порядковом номере: rangemin/rangemax; строки (с номерами 0-7).

      И это явно напрашивается как-то унифицировать в некую таблицу.

      Например, чтобы она индексировалась по KIND, а содержала бы "вид" (поведение) и индекс внутри этого вида; например, "units" -- вид="строковое свойство", индекс=6.

      ...вопрос только, как этот "вид" назвать: слово "kind" уже занято, а "type" ну совсем не хочется. Как и "class". Может, "bhvr" (behaviour)? Тоже плохо, ибо перекрывается с прочими местами.

      @дома, ~19:20: напрашивается вместо нынешнего kind использовать иной термин (type-index?), освободив "kind" как раз для "вид канала".

    • ЗАМЕЧАНИЕ: а ведь диапазоны при отдаче их наружу тоже надо {R,D}-конвертировать. Т.е., для них как минимум нужно ловить RDSCHG.
    • Учитывая, что у EPICS свои понятия о "стандартных" свойствах -- вроде того, что наше "_raw" там RVAL, а наше "_units" -- EGU, надо бы уметь и такие имена распознавать.

      Эти alias'ы надо бы указывать таблицей дуплетов (ВНЕШНЕЕ_ИМЯ,НАШЕ_ИМЯ).

      А с учётом потенциального обобщения нынешнего cxsd_fe_epics.c в некий "упрощённый API для доступа снаружи", оная таблица должна будет указываться per-connection -- видимо, в вызове "создать соединение".

      @дома, ~19:00: с другой стороны, сама проверка "есть ли у нас такое..." -- нынешнее cxsd_fe_epics_pvExistTest() -- выполняется "бесконтекстно", безо всяких соединений. Так что, возможно, таблицу alias'ов надо будет указывать прямо самим "existTest" и "Attach".

    • @дома, ~18:40: пусть 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: возвращаемся к разбирательству со строками.

    • CAS_Tutorial.pdf стр.55 (PDF хренового качества, для поиска лучше DOC):
      It's important to note that when the primitiveType() function of a gdd object
      returns aitEnumString, this does not mean that the void pointer points to a character
      array. Instead, aitString is a separate type. Thus, an array of type aitString
      would actually be an array of aitString objects and thus an array of strings, instead
      of an array of type char.

      От меня: дебилы, бл#!!! Ну и где полиморфизм, ради которого ООП и создавалось?! Получается, что для разных типов для доступа к их значениям нужны РАЗНЫЕ методы -- прямое противоречие принципу полиморфизма. Где была голова авторов этого безобразия?!

    03.07.2022@около мыши по пути домой из Ярче, ~19:50:

    • Может, там предполагается, что, раз aitEnumString -- "как бы скалярный" тип, то надо отдавать maxDimension()=0, maxBound()=1?

      Но как тогда указывать максимальную разрешённую длину?

    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 строк".

    • Кстати, в cda_d_epics.c в NewDataCB() обработка конкретно DBR_TIME_STRING сделана ОЧЕНЬ отдельно: оно игнорирует значение nelems = ARGS.count и вручную проходится по строке в поиске первого '\0'.
      • ...там вообще аж ТРИ "стадии" попытки разобраться с длиной: 1. по sizeof(); 2. поиск первого не-'\0' с конца; 3. поиск первого '\0' с начала.
      • За 06-03-2020 целый раздел посвящён акробатике со строками, где в том числе подробно обсуждено и то, как добывается длина (те 3 "стадии").
      • И да, 08-06-2019 есть фраза "DBR_STRING, которая всего [40], но это 40 -- при count=1", а 06-03-2020 "да, проверено -- ARGS.count==1".
    • Вывод: в EPICS реально всё очень плохо с поддержкой строк, она реализована сильно "не так", поэтому и в cxsd_fe_epics_meat.c придётся отдельно извращаться.
    • ...хотя тут сразу встаёт вопрос: нафига это было делать с вариантом aitEnumString, а не только с aitEnumFixedString?

      Поневоле вспоминается Задорнов с его "ну тупы-ы-ые...".

    Но работу со строками отложим немножко на потом (ровно как было и с cda'шным модулем).

    04.07.2022: а пока займёмся скалярами.

    • Как и предполагалось 01-07-2022, для скаляров "вариант, когда данные установлены adjust()'ом при помощи указателя" не работает -- оно берёт вместо данных, на которые указатель показывает, значение самого указателя.

      Проверено было путём печати значения указателя через %p в read() и сравнением с тем, что показывает "caget -0x" -- значения совпали.

    • @поездка-на-Kei-в-диспансер: Напрашивающаяся идея: ОК, для конкретно скаляров просто использовать СТАРЫЙ код -- где GDD-объекту просто ПРИСВАИВАЛОСЬ значение, с-(cast)'ированное к нужному типу в зависимости от dtype.
    • Так и сделано -- да, работает.
    • Вопрос только: а как правильно узнавать "скалярность"? Ведь и в векторе может быть 1 элемент. Надо б именно max_nelems смотреть...

      OK -- сделана проверка именно по max_nelems, которое с недавних пор присутствует в виде member-поля, заполняемого в конструкторе.

    • ...А в направлении libCAS->CX -- т.е., запись -- всё и раньше работало корректно с момента перехода на dataAddress()+dataPointer().

      @вечер: оная парочка заменена на dataVoid() -- так короче и правильнее.

    • @~15:00: Кстати, напрашивается отдачу данных в update() и read() унифицировать: они ведь делают В ТОЧНОСТИ одно и то же, с разницей лишь в GDD-объекте-получателе.
    • Унифицируем -- делаем mondata2gdd(int mid, gdd *gp, int max_nelems); последний параметр -- для выбора "способа передачи" (скаляр/вектор).

      Туда перетащено обновлённое -- на выбор скаляр/вектор -- содержимое read().

      Ну а update() и read() переведены на неё -- после чего в них кода осталось 2 и 1 строка соответственно (вызов+postEvent() и просто вызов соответственно).

    Ну-с, засим можно считать, что основная часть работы с ЧИСЛОВЫМИ каналами сделана, а осталось разобраться с "ait-конвертерами".

    05.07.2022: а, нет -- ещё указывание длин векторов для векторных операций чтения/обновления.

    05.07.2022: возвращаемся к возне со строками.

    • Работа состоит из 3 частей: "информирование", чтение, запись.
      • Информирование: надо ВСЕГДА возвращать, что это якобы "скаляр".

        Непонятка: а как всё же отдавать максимальное число элементов в строке?

        ...или отдавать строки как массивы char[]?

      • Чтение: самое сложное -- нужно как-то
        1. то ли прямо конструктором создавать gdd-объекты сразу с соответствующим строковым содержимым,
        2. то ли как-то "копировать" (устанавливать) строки.

        Второй вариант кажется правильнее, но хбз: ведь в конструкторе-то, возможно, можно сразу указать максимальный размер буфера.

      • Запись: тут самое простое -- ведь данные-то нам дают готовые, надо просто "правильными" вызовами до них доступиться.
    • Видно, что в чтении и записи обработку строк лучше реализовывать совершенно отдельными от остального ветками -- по причине слабой совместимости/унифицированности с остальным: там и "количество элементов" имеет другой смысл, и обращение к объектам-"строкам" совсем иное.

    07.07.2022: потихоньку приступаем... Пока что самое простое -- запись: начата поддержка обоих видов входящих строк.

    • Общий подход: на каждую из альтернатив совершенно ОТДЕЛЬНАЯ ветка, ...
      1. Добывающая указатель на "хранящийся" в value соответствующий объект -- при помощи метода getRef(), которому передаётся указатель на объект соответствующего класса.
      2. Затем от этого объекта добывающая указатель+длину.
      3. И в конце собственный 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: теперь -- информирование:

    • Во-первых, принято решение, что TEXT-каналы будут отдаваться именно как STRING, а не CHAR.
    • Собственно действие: в maxDimension() и maxBound() к условию "скалярности" (т.е., когда возвращать 0) добавлено " || chn_dtype == CXDTYPE_TEXT" -- так что строки теперь "считаются скалярами".
    • Что же до "как всё же отдавать максимальное число элементов" -- а у DBR_STRING ведь всегда ровно 40...

      Или, раз тут 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'а -- просто нету.

    • Как вариант -- можно каждый раз реально "конструировать" объект заново, при помощи placement new.

      Благо, как показывает просмотр исходников aitHelpers.*, при типе aitStrRefConst (точнее, любом, КРОМЕ aitStrCopy) никакой аллокации/деаллокации не выполняется и, соответственно, никакой утечки памяти не будет -- содержимое объекта можно просто забывать.

      Т.е., надо будет делать что-то вроде

      new (gp->dataAddress()) aitString(data, aitStrRefConst, nelems, max_nelems);
      gp->setPrimType(aitEnumString);
      

      Но всё-таки это некрасиво...

    • Просмотр gdd.h показал, что примерно нужное делает вызов put(const aitString& d); -- на него и надо смотреть.

      Его код в gdd.cc, за вычетом проверок, только "мясо", выглядит так:

      setPrimType(aitEnumString);
      aitString* s=(aitString*)dataAddress();
      *s=d;
      

      (Но сам-то он копирует из содержимого уже ГОТОВОГО ДРУГОГО объекта aitString, которого заводить совсем не хочется.)

    • Рытьё (точнее, поиск по "aitStrType") нашло ещё метод 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);
      
    • Проверяем новый вариант:
      • Теперь "по ходу исполнения" данные в строке осмысленные.
      • Однако всё равно нифига до caget'а не доходит, с диагностикой
        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
        ..................................................................
        
        (а вот на стороне сервера ругани на ЭТУ тему нет).
      • Но есть такое, с предварительным дампом gdd:
        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?!

        Чуть позже:

        • Поиск в исходниках по строке "server tool changed bounds on request" показал, что это (errSymTbl.c, файл генерённый makeStatTbl.pl'ом из, в частности, casdef.h, строки там генерятся из комментариев) -- код ошибки S_cas_badBounds.
        • И ошибку эту генерит casStrmClient.cc::casStrmClient::readNotifyResponse(), и, похоже, при обломе поиска подходящего конвертера...

          (И аналогичное там есть в casStrmClient::readResponse(), только без слова "notify" в выдаваемом сообщении.)

      • И что самое странное -- если выполнить caget 2 раза, то при втором вызове read() объект gdd (принадлежащий МНЕ!) опять сброшен на "prim-type=aitInvalid". Какая скотина это делает?!
    • БЛИН!!! Разобрался!!!

      Похоже, это было "вскрытие показало, что пациент умер от вскрытия".

      • Я вызывал чтение как "caget -S asdf.2 -d DBR_CHAR", и оно обламывалось.
      • Но достаточно было перейти на просто "caget asdf.2" -- и всё стало отдаваться!
      • И, что самое интересное -- даже camonitor при старте показал текущее значение (но обновления всё равно не пашут).

        А вот для числовых данных -- не показывает!

      • Игры с разными запрашиваемыми "-d DBR_*STRING" показали, что работают почти все -- DBR_STS_STRING, DBR_TIME_STRING, DBR_GR_STRING и даже DBR_CTRL_STRING; но не DBR_STSACK_STRING (хбз, что это вообще такое): попытка такого запроса приводит к ошибке В СЕРВЕРЕ!!! Печатается сообщение
        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. Источники информации:

        • "bugs in gateway and gdd" -- пост в EPICS tech-talk за 03-08-2005 от Dirk Zimoch.
        • "Re: bugs in gateway and gdd" -- ответ на него за 04-08-2005 от Ralph Lange.

          И там приводится весёленький факт (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...

        • "Add missing DBR types DBR_STSACK_STRING and DBR_CLASS_NAME" -- багрепорт на PyEPICS за 09-12-2021 (дату видно лишь косвенно по ссылке ниже) от того же Ralph Lange, ссылающийся на
        • "subscriptions to waveforms suddenly break" -- багрепорт на CA-gateway за 07-12-2020.
    • 10.07.2022: проверил запись -- тоже работает. Вот просто работает и всё.

      Только 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 с самого начала. По ходу чтения появляются мысли:

    • Там в тексте, где показан пример (конкретно на стр.21) заводится не просто gdd, а конкретно gddScalar; в т.ч. оно там даже инстанциируется.

      Может, действительно надо заводить не абы что (абстрактный gdd), а именно gddScalar?

      Но у нас-то объекты потенциально разного рода -- И скаляры, И векторы. А держать вектор в Scalar-объекте -- тут точно что-то не то.

      Плюс, у всей троицы порождённых -- gddScalar, gddArray и gddContainer -- деструкторы тоже protected. Так что как это может работать -- загадка.

    • Ещё один нюанс: у всей этой троицы НЕТ СВОИХ ПОЛЕЙ -- все поля только от базового класса gdd, а есть лишь МЕТОДЫ, причём они даже не какое-то специфичное поведение определяют, а только набор конструкторов и "operator=". Реально же ПОВЕДЕНИЕ всё реализовано прямо в исходном классе.

      Т.е., по факту -- это "наследование" несколько фейковое, т.к. реально всё есть прямо в классе-предке, который содержит также и по факту "поля типа" (хотя и несколько завуалированные).

      Это прямо какая-то профанация самой идеи ООП.

    • И да, в реализации его "методов" (конструкторов) нет не только никакой особой специфики (а только вызовы подстилающего), но и никакой там установки конвертеров.
    • Вывод: нет, НЕТУ смысла использовать именно gddScalar.

    13.07.2022: продолжение страданий:

    • Решил сравнить, в чём заключается различие между разными gdd: передаваемым 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?).

    • Чисто по идеологии -- у меня глобальное непонимание, нафиг вообще этот "application type" нужен: какая разница при передаче по сети (и конверсии в этих целях), является какое-то вещественное значением (value) или какой-то границей (например, "gddAppType_alarmHighWarning" -- gddAppType_alarmHighWarning равно 14...).

    Однако не всё так просто. Со строками-то работало. И со скалярами работает. А вот с прочими векторами -- нет, фигня какая-то.

    • По начальному чтению camonitor'ом печатается ПУСТОТА.

      По обновлениям -- мусор.

    • И просмотр под GDB не помогает.

      14.07.2022: а нет, нифига -- помог! Т.к.:

      • От camonitor'ова read() прилетает с dim=1 и bounds={0,0}!
      • А от caget'ова -- dim=1 и bounds={start = 0, count = 5}!

      Теперь вообще ВСЁ понятно: данные уходят ровно так, как указано в соответствующей gdd. А скаляры работали потому, что для них как раз ничего указывать не нужно и "dim=0" и означает скалярность.

    • Но при "camonitor -0x" -- выдача в шестнадцатиричке -- видно, что получаемые целочисленные значения ну очень похожи на значения указателей, выдаваемых диагностической печатью; не равны, а где-то рядом -- различия в пределах килобайтов.

      Откуда вывод: похоже, где-то путается принцип "когда в gdd значение, а когда указатель" -- потому, что мы не указываем dimension+bounds.

      А ведь НАДО указывать.

    • ...отдельный вопрос -- почему ЧТЕНИЕ работает, а МОНИТОРИРОВАНИЕ -- нет?

      Или чтение использует размерности из casPV, а мониторирование нет? (Хотя метод postEvent() как раз у casPV...)

      В любом случае это ещё один индикатор "качества" кода libcas -- ну КАК можно было ОДНО и то же действие делать не унифицированно, а РАЗНЫМИ кусками кода (иначе как бы разное поведение было)?!

    14.07.2022: придумываем решение.

    1. Что нужно:
      • "Идеальный вариант" -- указывать размер/длину прямо в mondata2gdd().
      • Но, поскольку bounds[] аллокируется в динамической памяти, то просто постоянно указывать как-то непрмятно -- не будет ли совершенно незачем постоянно деаллокироваться/аллокироваться?
      • Плюс, конкретно для обычного чтения вообще ничего указывать было бы и не надо, но даже и для camonitor'ова чтения достаточно указывать только длину вектора, НЕ указывая размерности.
      • А для upd_gdd достаточно при создании указывать -- прямо конструктору.

        ...но конкретно КОЛИЧЕСТВО всё же менять в момент обновления.

      • Т.е., идеальный вариант выглядит таким:
        1. upd_gdd указывать всё конструктору.
        2. В mondata2gdd() указывать -- лишь при не-скаляре (max_nelems != 1) -- ТОЛЬКО конкретное значение количества в 0-м измерении.
    2. Что есть в распоряжении:
      • Так-то есть метод gdd.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]);
        }
        
        -- т.е.,
        1. Он вызывает init(), аллокирующий "gddBounds1D" -- "одномерные" границы -- и нулящий их,
        2. а затем в цикле устанавливает РАЗМЕРЫ каждому измерению.

        P.S. Да-да, у него в прототипе и в реализации название последнего параметра отличается -- "size_array" и "val". Охренительное качество кода!

    3. Как поступим?

      Наиболее правильным выглядит вариант, озвученный в конце п.I:

      1. upd_gdd создаётся прямо нужного "типа", путём передачи параметров конструктору.
      2. При обновлении меняется лишь количество элементов.
    4. Делаем.
      • Замечание, сделанное задним числом, но важное для понимания: сама проверка на скалярность должна учитывать не только "max_nelems==1", но также и строковость: CXDTYPE_TEXT должен всегда считаться скаляром.

        Сначала это было забыто, но в кусках кода ниже это уже учтено.

      1. Создание upd_gdd "правильным образом" -- реально оказалось непростым, сложности прямо на ровном месте во вроде бы тривиальном деле:
        • Корень проблемы в том, что в C++ объектам-полям и объектам-предкам (что по сути одно и то же) конструкторы вызываются безусловным образом прямо в начале конструктора самого класса, и перед этим НИЧЕГО НЕЛЬЗЯ ДЕЛАТЬ -- никакого кода, никаких предварительных вычислений значений параметров, которые тем конструкторам надо передать; только простые выражения в самих параметрах.

          Но у нас-то в зависимости от скалярности/векторности настройки могут изрядно отличаться. В результате:

          1. Условие "скаляр ли?" пришлось дублировать -- при передаче параметров "dimen" (скаляр/вектор) и "size_array/val" (размер вектора).
          2. Параметр "число элементов вектора", 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), буквально вчера добавленное как решение проблемы "почему обновления не работают?!", теперь стало излишним и убрано.
      2. Указывание количества элементов в mondata2gdd() -- самое простое: в альтернативу "Vector?" сразу после adjust() было добавлено
        gp->setBound(0, 0, nelems);

    Результат:

    • Да, работает -- вроде всё корректно (тьфу-тьфу-тьфу), насколько удалось проверить (там же надо все-все комбинации тестировать).
    • Только вначале было забыто, что STRING/CXDTYPE_TEXT тоже считается скаляром -- без этого оно SIGSEGV'илось; после добавления доп.проверки (см. выше) проблема исчезла.
    • Любопытный нюанс: camonitor, в отличие от caget, действительно "понимает" массивы переменного размера, как и говорит фраза "dynamic arrays supported" в его help'е: он печатает ровно столько элементов, сколько реально записано в CX'ный векторный канал.

    14.07.2022@лес около стадиона НГУ и у девятиэтажек на Терешковой, туда и обратно пешком на М-46: вообще, конечно, в C++ какой-то абзац прямо на базовом уровне реализации объектности:

    • Конструкторы НЕ наследуются -- обязательно надо объявлять свои, даже если это просто тупейшие "переходники" к предковым (C++11'ное расширение "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: твикаем по мелочи:

    • Оптимизация, чтобы не напрягать libcas обновлениями каналов, на обновления которых в текущий момент никто не подписан:
      • Вводим поле is_interesting, в конструкторе нулимое.
      • Определены надлежащие методы interestRegister() и interestDelete(), делающие его =1 и =0 соответственно.
      • Ну а update() проверяет, что если оно не взведено, то ничего и не делать.

      Результаты:

      • Да, работает.
      • Что любопытно, interestDelete() вызывается после ЛЮБОГО отключения "последнего" клиента, в т.ч. и того, который мониторирование не запрашивал -- caget или caput. Видимо, в целях "подчистки".

        Т.е., вызовы interestRegister() и interestDelete() могут быть и несбалансированными.

        Ну да, в casPVI.cc 1 вызов первой и 2 второй; парные -- в casPVI::installMonitor() и casPVI::removeMonitor(); а вот "лишнее" удаление -- в casPVI::removeChannel().

      • ЗЫ: в CAS_Tutorial.pdf на стр.39 есть фрагмент
        if (interest == aitTrue)
        -- сравнивать булевское с TRUE!!! Неужто им не рассказывали, что сравнивать можно ТОЛЬКО с FALSE?!

    16.07.2022: реализуем access control.

    • Главной проблемой было извлечение IP-адреса из caNetAddr.

      Изучение caNetAddr.h и caNetAddr.cc показало, что правильным способом добычи будет метод getSockIP() -- он возвращает struct sockaddr_in.

    • А дальше уж аналогично cxsd_fe_cx.c --
      ip_val = ntohl(inet_addr(inet_ntoa(addr.sin_addr)));
    • ...только перед добычей адреса предварительно надо проверять, что .isInet(), иначе будет exception. ("Другой" вариант -- UDF, "undefined".)

      Вот в ином случае ip_val и принимается за 0, как при unix_serv_handle.

    • Ну и дальше проверка, что если в правах для этого адреса нет бита CXSD_ACCESS_PERM_CONNECT, то возвращаем "нету тут такого".
    • ЗЫ: и проверка делается СНАЧАЛА -- до проверки наличия указанного имени.
    • 18.07.2022: хотел ещё тогда же сделать и "полную" поддержку access control, с использованием 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_libcas.cc -- простейший клиент libcas'а, предоставляющий единственную PV, строковую.
    • Еёйный test_PV::read() для возврата значения делает просто
      prototype = the_String;
      где присваеваемая определена как
      aitString the_String = "A string value";
    • На ней проверено -- почему-то НЕ падает.
    • @дорога домой с М-46: а может, дело в том, что сделано всё "простым присваиванием" (при котором вызываются всякие методы/копирующие конструкторы), а mondata2gdd() ВПРЯМУЮ/"ВРУЧНУЮ" формирует содержимое gdd'ы, при помощи setPrimType() и последующего installConstBuf()?
    • @вечер, дома: сделано "ВПРЯМУЮ" -- да, падает точно так же, как во frontend'е!!!

      Для проверки также добавлен вариант

      prototype = string_buf;
      где присваеваемый буфер определён как
      const char string_buf[100] = "A string value";
      -- ага, тоже НЕ падает!!!

    17.07.2022: роем дальше.

    • В test_libcas.cc::test_PV::read() в начало добавлено prototype.dump(), чтобы посмотреть, что же такое присылают.

      Результат: дампится КОНТЕЙНЕР! Что соответствует той диагностической ругани -- что оно ожидало "prim_type==aitEnumContainer".

      (Дамп тут в тексте закомменчен.)

    • Грусть-тоска-печаль-депрессия...

      Ну козлы же!!!

      Получается, что они протаскивают на уровень КЛИЕНТА своей библиотеки очень интимную специфику EPICS'ного Channel Access'а!

      И реализовать поддержку этого STSACK хоть сколько-то приличным образом без зарывания в ту специфику вряд ли удастся.

    • ...пытался порыться во внутренних деталях реализации gdd -- как же там устроено, что такие "присвоения" не меняют структуры этого "контейнера".

      Бросил это занятие -- там чёрт ногу сломит (не, можно разобраться, но жаль времени).

      Кстати, вполне возможно, что реально-то и "присвоение" работает не совсем корректно: ведь при том чтении 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'ёв;
      • но его (Channel'а) методам НЕ передаётся указателя на PV-владельца (хотя у вызывающего оный указатель точно есть), и для функционирования ПРИДЁТСЯ оный указатель дублировать у себя, полем.

      Т.е., разделение на 2 класса -- какое-то странное и плохомотивированное; что мешало ВСЮ эту функциональность держать прямо в casChannel -- загадка. Возможно, какие-то EPICS-специфичные соображения, или возможность использовать один экземпляр Channel'а для многих PV (но последнему противоречит как раз непередача методам указателя на PV -- без этого "один объект-контролёр-доступа на все PV" не сделать).

    • Внутренняя реализация -- с использованием, как вообще в pcas принято, отдельного "I"-класса (Interface?) casChannelI (у которого ссылка на связанный casChannel как раз есть) -- там опять замороченная, так что любое разбирательство "а как вот это работает?" (чтобы понять, правильно ли *Я* делаю) выльется в море потраченного времени.

    Короче -- да ну нафиг...

    18.07.2022: проводим более обширное тестирование.

    • Первый же тест -- "caget -d DBR_STSACK_STRING asdf.0", где канал asdf.0 -- r1i5, вылился в SIGSEGV на попытке prototype.dump() сразу после выданного "----------dumping container": судя по gdb, где-то в aitString::totalLength().

      За-дол-ба-ло...

    • Тест в цикле с перебором всех КОДОВ -- от 0 до 38 -- посредством команды
      for i in {0..38};do; caget -d $i asdf.2; done
      при натравливании на:
      • текстовый канал asdf.2 дал кучу ругани с обеих сторон, но без фатальных последствий;
      • целочисленный asdf.0 -- тоже кучу ругани, но SIGSEGV только на 38 (тот самый STSACK);
      • вещественный asdf.1, который w1d4 -- аналогично.
    • Итого, есть надежда, что после удаления диагностических 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 сейчас реально ДВА сильно разных куска

    1. Будущий "csfi".
    2. Собственно штатная инфраструктура cxsd_fe-модуля в виде 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.c -- собственно "модуль"-обвязка. Временно -- cxsd_fe_zpics.c, пока делается.
    • cxsd_fe_epics_cas.cpp -- взаимодействие с libCAS.
    • cxsd_sfi.c -- Simple Frontend Interface.

    Обнаруженные проблемы:

    1. Из "SFI-кода" есть прямой вызов cxsd_fe_epics_meat_update().
    2. Из кода модуля есть толпа прямых обращений к локальным для "простого интерфейса" переменным fe_epics_periodics* и fe_epics_monitors_list*.

    Что будем делать:

    • Потом, В ИДЕАЛЕ -- надо вводить какие-то либо "контексты", либо per-connection/monitor-"методы".
    • А вот СЕЙЧАС -- сделаем по-простому, всего лишь обеспечив компилируемость и рабочесть путём завуалированных, но по-прежнему почти прямых вызовов.

    06.01.2023: решаем проблемы:

    1. Вместо прямого вызова 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);
    2. Обращения из модуля к приватным переменным:
      • Из 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).
    • А с ЗАКОММЕНТИРОВАННЫМ тем условием -- чтение работает, ошибок не генеря.
    • Кстати, проверил ещё раз: при запросе "caget -d DBR_STSACK_STRING" диагностика говорит "isContainer()=1 isAtomic()=0".
    • Собственно: может, проверять ТОЛЬКО isContainer(), но НЕ isAtomic()?
    • Сделал, и в epics2smth.c и в cxsd_fe_epics_meat.cpp/cxsd_fe_epics_cas.cpp; да -- после этого стало работать начальное чтение массивов в обоих.

    Подозреваю, что это и была причина "странностей с отработкой начального чтения", и что теперь, скорее всего, проблема решена. ...правда, неясно, чем аукнется удаление второй части условия.

  • 02.09.2022: надо б в приходящих от libCAS именах заменять все ':' на '.' (и чё раньше не додумался?!).

    Это однозначно правильное действие:

    1. В EPICS'е именно двоеточия используются как разделители компонентов (потому как точка там может быть лишь в конце -- дятлы...).
    2. Никакого смысла доводить двоеточия до CX'ного уровня нет: внутри сервера символ ':' никакого отдельного значения не имеет, там таже в смысле "на уровень выше" (аналог "../") не используется.

    Таковую замену, видимо, стоит производить в 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).

  • 25.04.2023: пытаемся решить проблему "из-за кривой реализации aitConvertFixedStringString() строки отдаются мусором".

    Решений будет 2 -- независимых, каждое из которых достаточно уже само по себе (стараемся убрать дырки в максимальном количестве "листиков сыра"):

    1. Фикс самой функции-конвертера.
    2. Перед отдачей GDD копировать строку в собственный буфер и NUL-терминировать.

    25.04.2023: действия:

    1. Фикс функции-конвертера: тут всё весьма прямолинейно (хбз почему сами авторы не смогли это сделать), теперь она выглядит так:
      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'ов.

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

    2. Копирование строки в собственный буфер с NUL-терминированием.

      Работа (аналогично сделанному в cda_core.c) ведётся синхронно в cxsd_fe_epics.c и cxsd_sfi.c:

      • Аллокирование в *_pvAttach(): в формуле вычисления требуемого объёма -- csize -- теперь вместо chn_p->max_nelems используется ранее вычисляемое max_nelems, которое для REPR_TEXT-каналов делается max_nelems++.
      • Принудительное копирование в случае REPR_TEXT и прописывание NUL:
        • сначала хотел поместить в *_get_data(),
        • но затем, внимательнее обсмотревши образец в лице cda_core.c (с которого бралась архитектура), увидел, что там оно делается в 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: переходим к тестированию.

    • "Стенд":
      • Сервер -- с нашим devlist-test-alltypes.lst, конкретно канал text.0, который r1t100:
        ./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, но мусор они печатали разный; точнее -- такое впечатление, что ВТОРОЙ из запущенных мусора почти НЕ печатает.

        Это было проверено запуском СНАЧАЛА cdaclient и ПОТОМ camonitor -- да, первый мусор получал, а второй нет (кроме последнего "a2", где "2" лишнее).

      • Но вот если ПОСЛЕ цепочки тех 4 записей запустить одновременно cdaclient и camonitor, то они покажут ОДИНАКОВЫЙ мусор -- например, "a2\xa8}v\x7f" и "a2\250}v\177" соответственно.

        Особенность отдачи текущего значения?

        Надо будет перепроверить -- какая-то там была особенность с отработкой начального чтения.

    Теперь исправленное (проверяем поотдельности):

    1. С пофиксенной функцией-конвертером: всё показывается корректно.
    2. С копированием в собственный буфер: тоже всё показывается без мусора (но уже после сегодняшнего дополнения).

    @вечер: а вот на e2k_128 проверить не удалось: у yukari случался kernel panic на этапе запуска мониторирующего "camonitor text.0" (оный тоже 128-битный; если запускать только его (при отсутствующем сервере) или только сервер -- всё OK, но после ответа (который приходит!) через несколько секунд связь прерывалась; проверено 4 раза...).

:
GW:
  • 06.11.2022: это всё о директории frgn4cx/gw/.
frgn4cx/gw/epics2smth/:
  • 06.11.2022: создаём раздел. Заметки, идущие сюда, создавались уже несколько месяцев (с 30-07-2022), но жили в других местах.

    Конкретные под-части будут обитать в своих подразделах level4-списка.

    Оные части включают в себя (на текущий момент):

    • epics2smth.[ch] -- собственно сама "инфраструктура".
    • epics2cda.c -- реализация "гейтвея общего назначения" через CDA на основе E2S.
    • epics2tango.c -- реализация "гейтвея из EPICS в TANGO" (слушает EPICS/CA, коннектится к TANGO-серверам) на основе E2S.
    • epics2tango_gw.cpp -- отдельная реализация "гейтвея из EPICS в TANGO", самостоятельным .cpp-файлом, НЕ на основе E2S.

      Живёт рядышком (а не в своей отдельной директории) чисто для удобства.

  • 01.11.2022: переименована frgn4cx/gw/epics2tango/ в frgn4cx/gw/epics2smth/.
  • 22.11.2022: всё-таки проблемы с "системой сборки": бинарник epics2cda требует libtango, которая вообще-то ему совсем не обязательна.

    Причина -- в ОБЩИХ определениях LOCAL_LDFLAGS, а те, в свою очередь -- из-за единой директории с "предохранителями-канарейками" директорией выше.

epics2smth:
  • 30.07.2022: зачатье epics2tango. frgn4cx/gw/, frgn4cx/gw/epics2tango/.
  • 30.07.2022: Желание -- сделать ОБЩИЙ API "epics2smth" (префикс e2s_?), способный работать с произвольными backend'ами. Сам он -- да, плюсовый/объектный, чисто ради libCAS'а; но вот "потроха"/логика его работы вполне Си'шные: адресация "мониторов" по их id, аллокирование realloc()'ом/SLOTARRAY'ем; с использованием приёма из cda_d_cx (@вечер: неа, cxscheduler.c -- пул таймаутов, 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'ей.
    • Также сделана "инфраструктура "MonSlot" -- скопирована с cxscheduler.c'шной поддержки trec_t.
      • Т.е., это именно ПУЛ: всегда есть "первый свободный", а уж если нету -- ну тогда аллокируется ещё сотня и отдаётся первый из них (но никогда НЕ 0-й!).
      • И значение 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" примерно понятна.
    • А вот из документации фиг что поймёшь: в CAS_Tutorial.pdf вообще описаны с примерами лишь casAsyncReadIO с casAsyncWriteIO, а про casAsyncPVExistIO сказано "ну оно аналогично" --
      Asynchronous completion for the pvExistTest() and createPV() functions works much the same way.
      -- @#злы...
    • И я так и не понял, как же для чтения и записи работает "completion": ведь там методу postIOCompletion() передаётся лишь собственно СТАТУС, но НЕ ссылка на объект-PV.

      ...видимо, как-то косвенно через ссылку на gdd; или, возможно, через связанный I-объект...

      А вот для "Exist" -- понятно, там по имени. 17.08.2022: хотя вот сейчас смотрю -- и не вижу там имени...

      11.09.2022: разобрался -- в конструкторе IOI-объекта вычитывают из ctx'а заголовок запроса. Подробнее см. ниже за сегодня.

    Собственно сделанное: 17.09.2022: удалено всё -- вся концепция ASN/aid, за ненадобностью, т.к. раз информация для casAsyncPVExistIO-объекта вычитывается конструктором его IOI-подмастерья при создании, то и повторное использование невозможно и держать пул этого добра просто незачем.

    1. Добавлена "инфраструктура для ASN'ов": "ASN" -- "ASync Notification", т.е., объект наследника-от-casAsyncPVExistIO, а "инфраструктура" -- пул, работающий аналогично MonSlot'ам (с чьей реализации всё и скопировано).

      Структура названа asninfo_t.

    2. В moninfo_t поле "void *AS_ptr" заменено на "int aid" ("aid" -- "Async ID"); т.е., там будет храниться ID приписанного к данному монитору уведомления -- и ТОЛЬКО при is_used==MON_TYPE_SEARCH.
    3. Добавлено "#include <new>" -- чтобы можно было прямо в asninfo_t и класть наследника-от-casAsyncPVExistIO прямо полем (тогда понадобится "конструировать" объект по указанному адресу).

      17.08.2022@М-46, спускаясь вниз, ~13:10: неа, нельзя -- это ж ОБЪЕКТ, должный быть по фиксированному адресу, а тут он оказался бы структуре, лежащей в ПУЛЕ, двигаемом realloc()'ом. Так что убираем.

    А теперь "мысли":

    • @М-46, ~18:30: Вопрос: не разделить ли ПОЛНОСТЬЮ мониторы и "поиск"? Т.е., чтобы в по pvExistTest()'у заводить ячейку в пуле "поиск", а уж при регистрации (по pvAttach()'у) создавать монитор?

      Ответ: скорее всего нет, НЕ разделить. Просто потому, что в большинстве случаев, типа той же TANGO, нет возможности просто ПРОВЕРИТЬ существование канала с таким-то именем, а уже потом отдельно коннектиться к нему. Оно уж СРАЗУ генерит канал, и если получилось, то вот он, готовенький, дальше к нему можно подключаться.

    • @около стадиона НГУ, по дороге домой, ~19:40: ...ну а если запрошенное НЕ найдено -- тогда что? Оставлять монитор висеть "мусором"? Или подчищать -- вместе со строкой name, тем самым, вследствие постоянного аллокирования/деаллокирования разного размера (из-за разных имён, прущих вперемешку) плодя фрагментацию памяти?

      17.08.2022@утро-душ: можно использовать технологию, аналогичную cxsd_db'шной "strbuf" -- буфер строк, растущий шагами по 1...4kB, а вместо указателей хранить offset'ы. Проблема только в том, что "наружу" указатели на такие строки отдавать будет нельзя -- ведь если после отдачи добавится новая строка и случится realloc(), то указатель станет невалидным...

    17.08.2022: пора уже делать "API backend'а" -- то, как epics2smth.c будет общаться с TANGO-частью.

    Соображения:

    • Напрашивается сделать это структурой-"VMT", указатель на которую передавать e2s_run()'у.
    • Очевидно, что при регистрации канала нужно сразу передавать туда epics2smth'ный mid (Monitor ID) -- чтоб все ответы "оттуда" шли бы уже по нему.
    • Есть сомнение в том, как организовывать "регистрацию каналов":
      1. Нужен ли отдельный вызов "проверь указанное имя на корректность", чтобы при НЕкорректности не тратить ресурсы на создание монитора, который потом тут же будет грохнут.
      2. Или же единственный "пробуй зарегистрировать канал с таким-то именем", и уж если он СРАЗУ обломится -- то грохать монитор и СРАЗУ возвращать pverDoesNotExistHere.

      Склоняюсь к тому, чтобы такой вызов всё же предусмотреть, но если соответствующая VMT-позиция==NULL, то просто не использовать эту функциональность.

    18.08.2022@~13:00, выходя с М-46: а не сделать ли ещё один "backend" -- epics2cda, этакий "универсальный"? Т.е., чтоб backend просто редиректил бы всё к cda.

    Замечания:

    1. Некоторый вопрос в ФОРМАТЕ имён. Очевидно, вариант только один -- cda'шный, [СХЕМА::]ИМЯ.
    2. В любом случае, "API backend'ов" надо делать так, чтоб легко сварганился бы этот "epics2cda.c".

    @~13:30: не удержался -- сделал epics2cda.c, скопировав почти пустой скелет из epics2tango.c, и буду пилить параллельно с последним.

    18.08.2022: работаем...

    • Добавлен тип e2s_backend_t,
      • таковой указатель передаётся e2s_run()'у, а тот его...
      • ...складирует в глобальную the_backend, которой потроха и пользуются.

      Покамест там всего 2 метода:

      1. check_name() -- его условное-при-наличии использование уже добавлено в e2s_Server::pvExistTest().
      2. add_chan() -- да, идейно он миррорит cda_add_chan().

      И сделан epics2tango.c::epics2tango_backend, пока пустой.

    19.08.2022:

    • Сделана FindMonSlotWithName(). 13.11.2022: ага, только с косяком: поиск начинала с avl_mon вместо frs_mon.
    • ...и уже с её использованием -- e2s_Server::pvExistTest(), делающая вроде худо-бедно "всё, что надо".
    • Подробнее о сегодня сделанном -- в разделе о epics2cda.c.

    20.08.2022: "об описаниях ТИПОВ":

    • Сначала была мысль "а давай будем общаться в терминах (ВИД_КОДИРОВКИ,РАЗМЕР_В_БАЙТАХ)", где "ВИД_КОДИРОВКИ" -- это условная константа, вроде "INT:1, FLOAT:2, ...".

      Т.е., почти в точности как запаковано в cxdtype_t, но вместо "двоичного логарифма размера" передавать именно размер.

    • Потом подумал: а чё размер -- давай именно двоичный логарифм ("size_log2")!
    • Но потом вспомнил, что надо б и беззнаковость передавать.
    • После чего уже очевидное решение: раз ВСЁ это уже есть в cxdtype_t, то его и использовать!

      Тем более, что он изначально организован "максимально правильно", т.е., и является наиболее оптимальным решением.

      21.08.2022: тем более, что для конверсии TANGO<->"cxdtype" и "cxdtype"<->libcas можно использовать готовые куски кода из cda_d_tango.cpp и cxsd_fe_epics_meat.cpp соответственно.

    • ...только впрямую нельзя -- т.к. нельзя делать зависимость от cx.h; с другой стороны, нельзя и те же имена оставлять -- чтоб конфликтов имён не устраивать, и чтоб не валандаться с #if'ами.

      Надо делать КОПИЮ -- всё то же самое, но назвать "e2s_dtype_t" (а вместо "*CXDTYPE*" -- "*E2S_DTYPE*").

    • Так и сделано, в epics2smth.h.

      И заодно пришлось битовые типы из 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-объектах -- так и не смог понять. Вот, к примеру, такая диаграмма событий:

    1. Приходит запрос на канал Name1 -- запоминаем у себя объект ASYNC1 и отдаём pverAsyncCompletion.
    2. Приходит запрос на канал Name2 -- запоминаем у себя объект ASYNC2 и отдаём pverAsyncCompletion.
    3. Получаем ответ "есть/нет" на канал Name2 и делаем ASYNC2.postIOCompletion(есть/нет)
    4. Получаем ответ "есть/нет" на канал Name1 и делаем ASYNC1.postIOCompletion(есть/нет)

    Так вот, КАК оно различает, на какой из каналов даётся ответ?

    • При том, что ни при возврате pverAsyncCompletion не указывается casAsyncPVExistIO-объект, ни при вызове postIOCompletion() не указывается целевой канал.
    • И да, async-объекты, судя по мануалу, МОГУТ быть созданы пачкой заранее и затем по мере надобности использоваться из пула. 11.09.2022: а вот и нет -- видимо, я это перепутал с объектами casPV, про которые сказано, что они "however, the server tool can also precreate a series of casPV objects for all the PVs it is responsible for, and then return a pointer or reference to them in createPV()".

      Так что никакой возможности в момент создания выцеплять ссылку на целевой канал из casCtx'а тоже нет.

      ...и собственно casAsyncPVExistIO-объект в момент возврата pverAsyncCompletion также никоим образом не трогается.

    11.09.2022: разобрался!!! Всё-таки именно В МОМЕНТ СОЗДАНИЯ оно берёт информацию о "событии" из контекста. Нашёл это, сделав предположение "а вдруг всё-таки при создании? давай посмотрим конструктор!".

    Последовательность "открытий" была такова (все файлы -- в src/ca/legacy/pcas/generic/:

    • casAsyncPVExistIO.cc: конструктор
      casAsyncPVExistIO::casAsyncPVExistIO ( const casCtx & ctx ) :
          pAsyncPVExistIOI ( new casAsyncPVExistIOI ( *this, ctx ) ) {}
      
      -- т.е., просто инициализирует свой I-объект ("подмастерье"?). У которого...
    • casAsyncPVExistIOI.cpp: конструктор casAsyncPVExistIOI::casAsyncPVExistIOI() делает много всякой инициализации, но в частности --
      msg ( *ctx.getMsg() 
      
      т.е., в поле msg вычитывает какое-то "сообщение" ИЗ КОНТЕКСТА!!!
    • Судя по определению класса в casAsyncPVExistIOI.h, это поле --
      caHdrLargeArray const msg;
      
      -- т.е., это что-то, хранящее ChannelAccess-запрос.

      Вечер: определяется в 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: мысли в тему "как хитро может быть устроена диаграмма работы регистрации":

    1. А если "плагин" вернёт результат прямо изнутри вызова add_chan()?

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

      Всё "выпрямить" -- просто чтоб e2s_set_found() на недо-подготовленные мониторы не возвращала бы статус, а меняла бы ВНУТРЕННЕЕ СОСТОЯНИЕ МОНИТОРА -- т.е., например, ставила бы другой тип. А pvExistTest() чтоб в точке после вызова add_chan() проверяла бы, что если тип не совпадает с пред-установленным, то считаем это за "ответ вёрнут прямо извнутри add_chan()" и реагируем на его значение.

    2. А ещё, учитывая multithreading -- причём ГАРАНТИРОВАННЫЙ, поскольку как минимум cda работает в другом thread'е -- может возникнуть ситуация обращения к "ресурсам epics2smth'а" (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().
    • И вот тут возникла мысль:
      • А не заводить ли asn-объект только ПОСЛЕ возврата из the_backend->add_chan()?
      • Как минимум, это исключило бы совершенно излишнее создание/удаление при заведомо негодном имени (ну да, есть check_name(), но оно опционально).
      • А также при том, ради чего новые "типы" вводятся -- для случаев, когда результат становится известен прямо в течение вызова add_chan().
      • Замечание: вот тут тогда ТОЧНО понадобится VMT-метод "разрегистрировать канал" -- чтоб если вдруг обломилось создание asn'а, то прибить и недозарегистрированный канал.

    14.09.2022: реально с полмесяца-месяц назад, @вечер, ~17:00, около стадиона НГУ по дороге на М-46: надо бы для общения между epics2smth и его backend'ом использовать в качестве backend'ова идентификатора не int, а указатель -- void*; это наиболее общий подход, который позволит каким-то "тупым" backend'ам, которые не держат пул, адресуемый по ID, а просто аллокируют объекты, возвращать прямо эти указатели.

    Цепочка размышлений тогда была такова (это реально продумывалось как часть спича на презентации "Опыт соединения EPICS с другими системами управления", где хотелось задвинуть на тему "как правильно организовывать внутренности софта"):

    • Лучше обмениваться между разными слоями софта -- epics2smth и backend'ом -- не указателями на их объекты, а ИДЕНТИФИКАТОРАМИ.

      Смысл -- в том, что идентификатор ПРОВЕРЯЕМ: можно убедиться, что он не выходит за границы аллокированного диапазона, а затем проверить, что он валиден (объект с этим номером в пуле "активен") -- это будет существенным подспорьем в защите от программных ошибок.

      Указатели же таковой проверке не поддаются (разве что можно проверять наличие magic number'а в фиксированном месте объекта, но это так себе проверка).

    • Но, тем не менее, в качестве передаваемого от backend'а ТИПА "ссылка на внутренний backend'ов объект" лучше всё же использовать УКАЗАТЕЛЬ: к int'у он тривиально приводится, а для "тупых"/"прямолинейных" программ указатель позволит использовать и аллокируемые объекты напрямую.

    Посему -- СДЕЛАНО!

    • Формализован тип e2s_backend_obj_ref_t, ...
    • ...соответственно, хранимые в epics2smth ссылки будут именоваться "obj_ref".
    • Ну и переделано всё на эту концепцию:
      • В epics2smth.cpp результат вызова add_chan()'а считается указателем и проверяется на ==NULL вместо <0.
      • А в epics2cda_add_chan() полученный от cda "ref" проверяется на валидность и возвращается через lint2ptr(), а при невалидности -- NULL.

    15.09.2022: движемся потихоньку -- добавляем поддержку multithreading'а теперь и в epics2smth.cpp...

    • Изготовлена работа с mutex'ом -- скопирована из mt_cxscheduler.c подчистую, включая проверку на pthread_self(). Только префикс "mt_sl_" заменён на "e2s_", да e2s_threadid инициализируется чтением, а не порождением thread'а.

      И зачатки использования, пока только в pvExistTest():

      • В самом начале делается lock.
      • В конце такой фрагмент:
         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
        
        -- т.е., разлочивается на время вызова, затем залочивается обратно и выполняется пере-кэширование.
    • Реализовано "отложенное" аллокирование ASN'а. Оно выполняется только в случае, если add_chan() не дал определённого ответа да/нет сразу.
      • В случае обоих возможных обломов при заведении ASN'а вызывается свежевведённый метод del_chan() -- для удаления недо-созданного канала.
      • Причём на время вызова выполняется разблокировка.
      • И выполняется это ДО RlsMonSlot()'а -- чтоб если del_chan() вдруг что-то вызовет, то "без последствий" (а то вдруг параллельно будет заведён другой монитор и получит тот же mid, а тут ему изподтишка что-то попортят...).
      • А чтоб реально ничего не сделалось, введено состояние MON_TYPE_DESTROYING, которое и ставится ПЕРЕД началом удаления.

      Вот о последних 3 пунктах ОЧЕНЬ много думал -- как же сделать корректно; был выбран вот такой вариант. (Рассматривались также варианты вроде "а давайте задекларируем, что del_chan()'у нельзя ничего вызывать", но то всё лишь декларации, а принятый является ЖЕЛЕЗНОЙ гарантией)

    16.09.2022@Утро-~08:30: наполнена и e2s_set_found().

    • Всё довольно пряморлинейно-алгоритмично:
      1. Отдельно проверка на MON_TYPE_REGISTERING, в каковом случае просто ставится соответствующее значение в is_used.
      2. А для MON_TYPE_SEARCH -- уже надлежащая цепочка действий.
      3. Все же остальные варианты игнорируются -- таким образом, все вызовы после первоначального автоматически не будут иметь никакого эффекта, что и требуется.
    • "Надлежащая цепочка" делает разлочку на время вызова postIOCompletion(); далее ASN-объект освобождается в любом случае, а монитор -- при ненайденности канала.
    • А чтоб не маяться с высвобождением AsynIO-объектов, раз уж они всё равно одноразовые, метод 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.

    А сделано следующее:

    • Добавлено поле moninfo_t.AS_ptr -- сразу как casAsyncPVExistIO *.
    • Аллокирование -- прямо в момент надобности делается mp->AS_ptr = new casAsyncPVExistIO(ctx);
    • Удаление -- по утреннему проекту:
      1. RlsMonSlot() делает delete, если AS_ptr!=NULL, ...
      2. ...а e2s_set_found() прописывает =NULL непосредственно перед вызовом postIOCompletion() -- тем самым перекладывая ответственность на libcas.

    Всё просто и примитивно (и сократилось с ~500 строк до ~400). ...и постоянно теребит динамическую память почём зря...

    Из прочего: реализован CheckMid_unlocked(); заюзан пока в единственном e2s_set_found() (иных backend'овых вызовов ещё просто нет, но скоро появятся).

    18.09.2022: приступаем к реализации передачи данных в обоих направлениях; делаем по образу и подобию cda (почти копированием из cdaP.h). Покамест декларации:

    • Backend'ов метод snd_data() -- аналогичен cda_dat_p_snd_data_f, только адресация другая.
    • Зеркальный ему e2s_update_data() -- аналог cda_dat_p_update_dataset(), но тут уже отличие в том, что:
      1. Передаётся всегда ОДИН канал (а не набор).
      2. Другой порядок параметров -- унифицированный с snd_data(), только добавлены int some_flags (задел на будущее, под какой-нибудь "alarm/severity/...") и timestamp.
    • ...и ради 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: готово, тривиально.
    • А вот для вызова snd_data() надо реализовывать e2s_PV::write(), не забыть подумать о e2s_PV::read(), а перед ними обоими -- ещё и e2s_Server::pvAttach() сделать...

    18.09.2022@Дорога домой с М-46, идя проездом за домами параллельно Ильича, ~19:30: соображения общего характера:

    1. Надо бы реализовать ОТДЕЛЬНЫЙ epics2tango_gw.cpp, прямо отдельным одиночным файлом, где максимально кратко и просто делается бриджинг EPICS/TANGO. Так, как и было задумано изначально, а то эта инфраструктура с epics2smth.* получается шибко монструозной.
    2. И всё распихать по своим отдельным поддиректориям-подпроектам:
      • epics2tango_gw/epics2tango_gw.cpp -- самостоятельный.
      • epics2smth/epics2smth.cppepics2smth.h опубликовать в frgn4cx/include/) -- библиотека для потенциально широкого круга "пользователей".
      • epics2cda/epics2cda.c -- использующая epics2smth.
      • epics2tango/epics2tango.c -- вариант также на основе epics2smth.

      31.10.2022: неа, НЕ будем делать толпу директорий. Пусть ВСЁ это будет в одной -- так проще и удобнее.

      Всё равно ж epics2tango_gw.cpp будет публиковаться отдельно.

      А вот переименовать нынешнюю директорию из epics2tango/ в epics2smth/ -- стоит.

    3. @лесок-подходя-к-стадиону-НГУ: В epics2cda должны мочь работать ссылки вида 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.c сделаны epics2cda_del_chan() и epics2cda_snd_data() -- всё тривиально, они просто перепасовывают даденное в cda_del_chan() и cda_snd_ref_data() соответственно.
    • Также "сделан" e2s_Server::pvAttach. Он также несложен, ибо предполагается, что уже РАНЕЕ был успешно исполнен и финализирован pvExistTest(), поэтому
      1. Сначала проверяется, а есть ли монитор с таким именем, и если нет -- то просто отлуп S_casApp_pvNotFound.
      2. Затем проверяется значение поля PV_ptr, и если оно ==NULL, то создаётся новый экземпляр e2s_PV.
      3. И в конце выполняется возврат.

      Всё с надлежащей лочкой.

    • Также пришлось реализовать e2s_PV::getName(), а то иначе класс оставался абстрактным (т.к. она в casdef.h определена как "= 0").

    А теперь пора изготавливать все подмастерья e2s_PV -- bestExternalType() и прочие maxDimension(). Для чего надо будет добавить API передачи этих данных от backend'а в epics2smth.

    20.09.2022: "подмастерья" -- тут по минимуму тривиально:

    1. Сами методы -- "логика" -- скопированы с cxsd_fe_epics_meat.cpp, и обёрнуты в локинг по шаблону getName().

      Отличие от cxsd_fe_epics в том, что ИНФОРМАЦИЯ содержится в мониторе, а не в PV.

    2. Ну и собственно информационные поля 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: оптимизируем.

    • epics2smth.h: все скопированности из cx.h убраны, а взамен вставлено #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().

    • В т.ч. на него переведён и cxsd_fe_epics_meat.cpp
    • Более продвинутое -- конверсия в лице mondata2gdd() -- пока оставлена на месте, т.к. ещё не вполне осознано, как именно нужно произвести обобщение (учитывая, что сами данные добываются из cxsd_fe_epics'ного монитора, а для epics2cda.c будет иное).

      ...хотя и напрашивается подленькая мыслишка "а вот было бы оно всё плюсовое, сделали бы общий базовый класс, в котором и предусмотрели бы как-нибудь..."...

    09.10.2022: возвращаемся к работе:

    • Для указания backend'ом свойств канала -- 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_d_cx.c сделано так, что оно вызывается непосредственно перед cda_dat_p_report_rslvstat() -- ну просто CX-протокол так устроен, что вся информация о канале присылается в процессе "открытия", поэтому к моменту RSLVSTAT всё уже готово, и по оному событию клиентская программа уже может всё о канале спрашивать.
      • И cda_d_insrv.c использует тот же порядок вызовов.
      • ...а вот в cda_d_epics.c всё хуже: там есть ТОЛЬКО _rslvstat(), а вот _hwinfo() в GetPropsCB() покамест не делается.

        Но сделать НАДО БУДЕТ! 23.10.2022: сделано.

      • В cda_d_tango.cpp даже и того нет.
    • Вывод (и это ответ на вчерашний вопрос "в КАКОМ состоянии дозволять?"): надо воспринимать "props" ("hwinfo") в состоянии SEARCH (и REGISTERING тоже).
    • @М-46, ~12:30: сделано -- теперь воспринимает в любом из 3 состояний.

      ...причём в 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() при высвобождении.

    • Во-вторых, РЕГИСТРИРОВАЛСЯ первый запрошенный канал в mid=1, а вот поиск для следующего начинался с mid=2 -- т.е., оно лезло в пустую ячейку, где всё равно было name==NULL.

      Косяк крылся в 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().

      Но несколько переделана, т.к. тут нужно исполнять локинг, поэтому

      1. Альтернатива "по типам -- aitEnumString, aitEnumFixedString, остальное" переделана с
        if      (value_type == aitEnumString)
        {
            ... // подготовка параметров отправки
            return ОТПРАВКА
        }
        else if (value_type == aitEnumFixedString)
        {
            ... // подготовка параметров отправки
            return ОТПРАВКА
        }
        ОСТАЛЬНОЕ
        ... // подготовка параметров отправки
        return ОТПРАВКА
        
        на
        if      (value_type == aitEnumString)
        {
            ... // подготовка параметров отправки
        }
        else if (value_type == aitEnumFixedString)
        {
            ... // подготовка параметров отправки
        }
        else
        {
            ОСТАЛЬНОЕ
            ... // подготовка параметров отправки
        }
        return ОТПРАВКА
        
      2. Собственно отправка теперь выполняется в ОДНОЙ точке, окружённая локингом:
        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():
      1. Ему добавлена полная пачка параметров (dtype, nelems, value, some_flags, timestamp)...
      2. ...плюс ещё max_nelems (необходимый при конверсии в gdd), чтоб оно не лазило в монитор (где лежит значение) само.
      3. А содержимое один-в-один взято с fe_epics_PV::update().
    • Конверсия делается в data2gdd(), являющейся копией mondata2gdd(), только исходные данные она не сама добывает, а ей они передаются в готовом виде.

      В остальном -- начиная с value_type = dtype2aitEnum(dtype) -- там всё символ-в-символ.

    • И да: на вопрос буферов (точнее, их времён жизни) приподзабито.

      НО: если таковой вопрос всё же встанет, то нынешнее распределение параметров -- то, что даже в e2s_PV::update() передаётся уже всё готовое -- позволит сделать ответственным складирование в него функцию e2s_update_data().

    Теперь проверяем.

    • Какое-то странное поведение:
      1. Обновления camonitor'у НЕ приходят.
      2. И даже просто "поиск" средствами cainfo срабатывает только 1 раз, а потом -- нет, сколько бы раз ни пробовал.

        Кстати, подобное ещё вчера наблюдалось, но я не обратил внимания (списал на свои косяки в паттерне запусков при тестах?).

      Нашёл причину: НИГДЕ не делалось присвоения состояния MON_TYPE_WORKS.

      Добавил надлежащее присвоение в e2s_set_found() -- проблема с ненахождением ушла.

    • После чего при обновлениях опять пошли валиться сообщения
      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
      
      (причём как-то не вполне "синхронно" с stderr-диагностикой обновлений, а пачками; то ли заскоки буферизации stdio, то ли специфика работы libcas'а).

      Но это-то мы уже встречали ранее, 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).

    • Только, в отличие от cxsd_fe_epics_meat.cpp, тут конструктору передаются лишь ДВА параметра, а не 4 (отсутствуют "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.pdf слова "thread" вообще нету!

      (Перепроверил в CAS_Tutorial.doc -- тоже нет.)

      В CAS_Reference.pdf.

      КАК?!?!?! Как вообще они планировали этой хрени работать в multithreaded-окружении (и даже локинг какой-то там повсюду разбросан), если про thread'ы ни слова?!

    • В любом случае возникает вопрос: а какого это хрена по postEvent()'у оно НЕ выполняет запись в сокет НЕМЕДЛЕННО?

      @ИЯФ-пультовая-СКИФ-планёрка, ~11:30: или это они так пытались "блюсти realtime" -- чтобы запись не тратила драгоценное время в основном thread'е, а исполнялась бы где-то в стороне, в рамках времени, отпущенного для fileDescriptorManager.process()? И не по той же ли самой причине оный process(0) обрабатывает лишь по ОДНОМУ событию из дескрипторов -- что время тут же кончается?

    • А не в этом ли "не работает flush" (точнее, не выполняется запись данных) причина странной паузы в отработке, наблюдённой в cxsd_fe_epics 19-07-2022?
    • @вечер-дома, ~18:15: посмотрел -- неа, НЕ СОВСЕМ.

      Точнее, косяк точно НЕ со стороны 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: модифицируем.

    1. В e2s_set_props() добавлена проверка PV_ptr != NULL.
    2. Добавлена передача значений dtype и max_nelems в цепочку конструкторов e2s_PV::e2s_PV() и e2s_gdd().

    Проверяем -- увы, особо легче не стало: теперь SIGSEGV'ится на e2s_PV::update() -- а всё потому, что e2s_update_data() тоже вызывается ещё ДО рождения PV.

    • Но как?!?!?! Ведь проверка-то на WORKS есть?!
    • Разобрался-сообразил: всё потому, что оное рождение выполняется лишь в e2s_Server::pvAttach(), хотя по факту "монитор оживает" непосредственно в момент присвоения статуса WORKS.

      Вывод: создавать надо непосредственно в ??????????????????????

    • Но нашёлся ещё косяк, просто просмотром диаграммы состояний: отсутствует переход из DOES_EXIST в WORKS. Вот просто забыто -- e2s_Server::pvExistTest() просто возвращает pverExistsHere.

      А ведь НАДО исполнять переход.

      Плюс, с учётом предыдущего пункта, надо и рождать PV тут же (точнее, в идеале как-то надо б объединить оба случая, вынеся код в общее место; но это позже, если получится).

    Делаем:

    • Добавлен переход из DOES_EXIST в WORKS.
    • Создание экземпляра 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 байтик.

    Вот сегодня сделано, ПОЧТИ всё:

    • В качестве образца взят cda_d_tango.cpp, оттуда прямо копируется.
    • Всё закрыто в "#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};
      -- а именно:
      1. Что такое "fdrNEnums"?
      2. А где там "fdrReadWrite"? Или для каждого бита маски регистрировать отдельный объект-callback?

      Ответы стали очевидны из конструктора fdManager::fdManager() в fdManager.cpp -- из единственной его строки:

      fdSetsPtr ( new fd_set [fdrNEnums] )
      1. fdrNEnums -- число значений fdrNNN (определение ПОСЛЕ последнего).
      2. И да -- там именно ТРИ РАЗНЫХ "набора", на каждое из действий свой.
    • Главное, что мне непонятно: автором этого файла значится Jeffrey O. Hill; вроде бы он совсем не идиот, но КАК он мог сгородить такую переусложнённую и при этом неспособную на очевидные задачи (R+W) дичь?!

    Напрашивается единственный путь: найти какой-нибудь пример в ихних исходниках и научиться у него (причём постараться найти наиболее ПРОСТОЙ вариант).

    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: возвращаемся.

    • На первый взгляд -- работает.

      (А вот БЕЗ этого -- нет, тормозит.)

    • Но всё равно как-то подтормаживает: какая-то доля секунды между timestamp'ом от сервера и временем непосредственно момента обновления.

      Есть подозрение, что дело в "обновлении по циклу", для отвязки от которого нужно выставлять всем каналам CDA_DATAREF_OPT_ON_UPDATE.

    • Проверяем подозрение: запускаем сервер с "-b5000000" -- да, стало подтормаживать на несколько секунд (при "удачном" попадании записывающей утилитой в начало цикла).
    • Окей, в epics2cda_add_chan() добавляем при регистрации флажок CDA_DATAREF_OPT_ON_UPDATE -- помогло: тормоза исчезли при ЛЮБОЙ длине периода цикла сервера, обновления прилетают мгновенно.

    15.05.2023: реализовываем чтение.

    Краткое обсуждение:

    • Делать надо аналогично cxsd_fe_epics_meat.cpp/cxsd_fe_epics_cas.cpp, где mondata2gdd()
    • Но там mondata2gdd() САМА добывает данные посредством cxsd_fe_epics_get_data()/cxsd_sfi_get_data(); тут же data2gdd() получает информацию в готовом виде; в единственном на текущий момент использовании её в e2s_PV::update() информация приходит от backend'а.
    • Напрашивается решение: добавить в backend метод "добыть данные" -- аналогично тем *_get_data().

    Реализация:

    • Добавлен backend-метод 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[] (с которого всё и копировалось).

    • Предполагаемый сценарий возникновения проблемы:
      1. Выполняется "возврат" данных.
      2. При регистрации нового канала/монитора оказывается, что свободное место в refs_list[] закончилось и он будет "увеличен", что приведёт к переезду в другое место.
      3. libcas пытается обратиться к указанному значению -- возможно, из другого thread'а.
    • Ограничения по условиям возникновения: НЕ будет проблемы ни со скалярами (т.к. они "присваиваются" в gdd, при этом происходит копирование), ни с массивами "большого" объёма, превышающего sizeof(CxAnyVal_t) (поскольку они живут в аллокированном индивидуально current_val, никуда не ездящем).

      ВОЗМОЖНА же она в случае "небольших" векторов -- т.е., значений, которые уже векторны, но умещаются в объём CxAnyVal_t.

    • Как её корректно решить -- хбз.
      • Сама возможность проявления обуславливается неопределённостью работы libcas (в какой момент она может обращаться к значению?) и multithreading'ом (вносит дополнительную неопределённость).
      • Решение "в лоб" -- избавиться от valbuf полностью, оставив только current_val.
      • 17.05.2023@утро-зарядка: или НЕ использовать valbuf для не-скаляров -- т,е., "max_nelems>1 && csize<=sizeof(valbuf)"!

        @утро, завтрак: пытаемся сделать.

        • Решение годится ТОЛЬКО для cxsd_fe_epics -- поскольку именно там контроль за аллокированием буфера.

          А вот для epics2cda -- увы, нет: буфер-то в cda...

        • И условие сводится по сути к "НЕ использовать valbuf при max_nelems>1" -- т.к. условие аллокирования будет автоматически комплементарно к "csize<=sizeof(valbuf)".
        • В 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, где новые каналы регистрируются по мере жизни процесса. Подробнее см. в профильном разделе за сегодня.)

  • 01.09.2022@Академия-холл-~11:00: а не сделать ли "фильтр", указываемый как-то из командной строки -- чтоб отрабатывать не все запросы, а лишь те, что фильтром пропускаются?

    Чтоб можно было в одной локальной сети запустить НЕСКОЛЬКО экземпляров epics2tango/epics2cda, бриджующих РАЗНЫЕ блоки имён.

    13.11.2022: и даже более того: если в epics2cda будут попадать имена вида "SYSTEM:SUBSYSTEM:SUBSUBSYSTEM", то оно может как-то неправильно восприниматься.

  • 14.05.2023: есть идеологическая проблема "в момент прихода запроса на канал тип (dtype,nelems) его реального (в сервере, КУДА будет gateway'иться) неизвестен, а с некоторыми backend'ами (конкретно TANGO) без знания типа невозможно в принципе".

    @утро, умывание, чистка зубов: Идея: разрешить указывать префикс в стиле cdaclient -- "@T[nnn]", чтоб клиент мог явно запросить, например, float ("@s") или "вектор 100 uint32" ("@+h100").

    Некоторый вопрос, конечно, ГДЕ это обрабатывать: в идеале -- правильнее в epics2smth.cpp, конкретно при e2s_Server::pvExistTest(), где и "создаётся" канал; но сейчас backend'ову e2s_add_chan передаётся только имя, а тип не фигурирует. Поменять парадигму, чтоб было как в cda -- можно не указывать (и разберётся потом само), а можно указать явно?

    23.05.2023: делаем.

    • Парсинг возможно-указанной "@Tnnn"-спецификации -- ParseTypeSpec(), являющаяся чуть модифицированной копией python3_calc_drv.c::ParseRefSpec().

      Она возвращает -1: ошибка (отвалить), 0: отсутствие спецификации, +1: наличие спецификации (результат парсинга каковой складирован в переданные места).

    • Её вызов добавлен, как и планировалось, в начало e2s_Server::pvExistTest().

      При отсутствии спецификации используются умолчания -- chn_dtype=CXDTYPE_UNKNOWN, max_nelems=sizeof(CxAnyVal_t).

    • И теперь при вызовах backend'а используется "подправленное" имя -- ПОСЛЕ "@Tnnn:".

      А вот для работы с мониторами -- и для сохранения в них, и при поиске -- используется полное исходное pPVAliasName, т.к. "ключом" является именно оно.

    • "Парадигма поменяна" -- теперь метод e2s_add_chan дополнительно принимает параметры dtype,nelems.
    • И конкретно epics2cda_add_chan() переделан -- он не только принимает эти параметры, но и передаёт их cda_add_chan()'у.
    • Также в epics2cda.c::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: да нет, НЕ работали только векторные каналы, но с этим мы разобрались (см. за сегодня).

    В остальном:

    • Парсинг @-спецификаций, похоже, работает корректно (ещё бы -- копия готового же).
    • Отработка, похоже, тоже -- по косвенным признакам.
    • Но конкретно с "cx::"-каналами оно почти не работает: по получению hwinfo от реального канала она замещает указанную в спецификации.

      Проверить можно будет, видимо, на "vcas::"-каналах.

    29.05.2023: пара других проверок:

    1. Натравлено на канал, определённый в сервере как "w1x16" -- в попытке убедиться, что действительно не будет пытаться менять тип каналов, если их известный hwinfo_dtype==CXDTYPE_UNKNOWN.

      Да, НЕ пытается.

      • В результате чего если не указать "@Tnnn" в имени, то канал в epics2smth так и остаётся типа UNKNOWN, что приводит к возврату bestExternalType()'ом получаемого трансляцией через dtype2aitEnum() значения aitEnumInvalid, в результате чего имеем диагностику
        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
        
        (это 3.15.6).
      • А если указать -- то создаётся канал ровно того типа/количества, который указан (проверено посредством cainfo).
    2. Также сервер запущен в режиме суперсимуляции (-S), чтобы убедиться в правильной работе всех вариантов типов данных (как векторных, так и скалярных) -- чтоб можно было писать в любые каналы.

      Да, всё работает как предполагалось.

    30.05.2023: пытаемся также проверить и через VCAS.

    1. Поскольку своего нет, то проверяем через пульт, длинной цепочкой команд "ssh -L localhost:10000:localhost:10000" -- через пульт/cxout, с конечным пунктом на vepp2kgw, куда вместо ":localhost:10000" было указано уже "IP:PORT" VCAS-сервера, взятые оттуда же из devlist-cs-gateway-1.lst -- да, через эту цепочку как минимум "cdaclient -d vcas::localhost:10000" работает...
      • ...а вот epics2cda -- сходу нет.
      • Краткое расследование показало, что он НЕ получает уведомления о найденности канала (вот о НЕнайденности при "error" от VCAS-сервера -- пжалста).
      • Потому, что cda_d_vcas.c НЕ делает cda_dat_p_report_rslvstat(,CDA_RSLVSTAT_FOUND).
      • После решения этой проблемы (см. в его разделе за сегодня) заработало.

      По теме:

      • Да, поведение то, которое и предполагалось.
      • С одним дополнением: при НЕуказании типа в имени канала оно не только не пытается менять тип, но даже и обновлений от cda не получает.

        Очевидно, потому, что в cda_d_vcas.c::ProcessInData() дешифрирование полученной от сервера строки делается на основе hi->dtype, а CXDTYPE_UNKNOWN в число поддерживаемых не входит, так что dat-плагин и данные распарсить не может, и даже report_rslvstat не делает.

      Однако, поскольку подписка на VCAS-каналы идёт по имени, то на каждый канал можно подписаться лишь ЕДИНОЖДЫ: например, если запросил "@t100:A.B.C", то "@d:A.B.C" уже всегда будет ненайденным. Но это и cdaclient'а касается (хотя на нём лучше заметен мелкий нюанс: начальное обновление/чтение на первый (работающий) канал приходят ДВАЖДЫ -- видимо, по штуке на каждую из команд подписки) -- такова уж модель устройства модуля (и по-другому его сделать сложно).

  • 15.06.2023: давно, ещё с Томска, ходит в голове идея: а не сделать ли "ПОДЧИСТКУ МУСОРА"?

    Вот сейчас если какой-то канал регистрируется, то он так навечно и остаётся.

    А можно сделать возможность подчищать такие неиспользуемые каналы, указывая ключиком из командной строки интервал: "-1" -- никогда, "0" -- сразу после обезынтересивания канала в e2s_PV::interestDelete(), >0 -- срок в секундах (а подчистку выполняется с периодичностью ~минута).

epics2cda:
  • 18.08.2022: для epics2cda.c нужен свой собственный раздел.

    Создаём его.

  • 18.08.2022: приступаем.

    18.08.2022:

    • В качестве ответа "канал найден"/"канал не найден" возвращать просто состояние, передаваемое RSLVSTAT'ом -- при FOUND/NOTFOUND (игнорируя SEARCHING).

      А уж epics2smth пусть сам разбирается, актуально ли ему это сообщение (в состоянии MON_TYPE_SEARCH -- да, а в WORKS уже нет).

      19.08.2022: следствие: воизбежание "чужих ответов" нужно, чтобы epics2smth в pvExistTest()/pvAttach() ПРОВЕРЯЛ бы наличие монитора с таким именем и возвращал бы его (хотя при повторном запросе на монитор, находящийся в MON_TYPE_SEARCH -- надо как-то отвечать типа "позже"). Для чего нужно реализовать ForeachMonSlot() -- да, для ПУЛА, вручную.

    • Делается БЕЗ собственного менеджмента "мониторов"/"объектов" -- целиком полагается на cda, которому передаётся privptr2=lint2ptr(mid).
    • Но что делать с тем, что на момент вызова cda_add_chan() неизвестен ТИП канала -- ни dtype, ни nelems?

      Ответ: а регистрировать его как CXDTYPE_UNKNOWN с nelems=16, а потом, по получению информации о реальном канале, делать cda_set_type().

      Проблемы:

      1. Реализация cda_set_type() пока недоделана -- как минимум, отсутствует реаллокирование/рост буфера.
      2. У cda_d_epics отсутствует флаг 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.

    • Следствие: возникает вопрос case-sensitivity ведь в разных случаях оно -- может разниться. Видимо, надо указывать параметром e2s_run()'у. Либо прямо в e2s_backend_t.
    • Чуть позже: да, так и сделано -- FindMonSlotWithName(), использующий name_compare -- указатель на функцию-компаратор, устанавливаемый e2s_run()'ом на основании добавленного поля e2s_backend_t.names_case_sensitive.

    19.08.2022: дальше делаем:

    • Сделана ProcessDatarefEvent() -- точнее, в основном скопирована с cdaclient.c'шной, с некоторыми дополнениями.

      "Суть"/"мясо":

      1. По RSLVSTAT отдаёт состояние найденности свежевведённому e2s_set_found().
      2. По UPDATE "добывает" данные, вместе с timestamp/rflags/..., для отдачи их e2s'у. Но пока не отдаёт по причине отсутствия соответствующего API.

    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: в продолжение мыслей "а что делать для корректной передачи в ДВУХ направлениях?":

    • Собственно, суть проблемы -- в том, что
      • все вызовы "внутрь cda" должны быть окружены mt_sl_lock()/mt_sl_unlock(),
      • но в ЭТОЙ-то софтине кроме них будут и вызовы "изнутри cda в epics2smth", потенциально могущие привести к вызову ОБРАТНО "внутрь cda";
      • а поскольку в любой момент, когда НЕ "рядом с select() основного цикла" -- т.е., именно внутри cda, откуда вызовы и происходят -- mt_sl_mutex находится в залоченном состоянии, то попытка его залочить приведёт к зависанию навеки, т.к. он залочен ЭТИМ же thread'ом.
    • Первая мысль -- «а давай сделаем простой булевский флажок, который взводить на время вызова "изнутри cda", и при нём взведённом игнорировать mutex, считая, что всё OK».

      Но это явный бред: ведь при одновременном-параллельном вызове из ДРУГОГО thread'а флажок-то может быть и взведён, но это НИКАК не говорит о безопасности.

      Т.е., по факту решение -- для ОДНОПОТОЧНОЙ софтины, где таким флажком можно отмечать внутренние критические секции (да и то не булевским, а "считающим" -- именно так работает being_processed).

    • Вторая мысль: надо проверять threadid -- если он совпадает с threadid cda-thread'а, то тогда точно всё OK.
    • @дома, ~10:10: а эта проверка УЖЕ делается в 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:

    • В Makefile заменён $(LIBCXSCHEDULER) на $(LIBMT_CXSCHEDULER) и добавлено требование -lpthread при линковке.
    • В main() добавлен "ритуал инициализации": предварительно mt_sl_start(), а создание контекста окружено lock()/unlock()'аньем.
    • В epics2cda_add_chan() регистрация канала также окружена лоченьем.

    На эпом пока всё -- больше ещё ничего и нету :-).

  • 16.06.2023: серверные каналы, которые INT32, но с {R,D}, считаются не за DOUBLE, а за INT32...

    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.

    • Не помогло.
    • Диагностическая печать показала, что phys_count==0.
    • Судя по cda_d_cx.c::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}" -- показало, что

    1. На первый взгляд, НИГДЕ нет никакой зависимости от порядка прихода событий.

      Это ответ на опасения 16-06-2023 «прочие "юзеры" имеют своё понимание последовательности прихода событий и обусловленные этим свои привычки, могущие от такого изменения сломаться».

      ...да и "юзеров"-то -- pzframe_data.c с pzframe_gui.c и cdaclient.c (ещё есть несколько ловцов REPORT_RSLVSTAT_NOTFOUND -- в основном драйверы вроде trig_read_drv.c и утилиты вроде pipe2cda.c -- но их интересует именно "NOTFOUND" для выдачи диагностики, а никаких "привычек" там нет).

    2. Прочие cda_d_-плагины -- начиная от cda_d_insrv.c и включая cda_d_epics.c с cda_d_tango.c -- делают просто подряд вызовы cda_dat_p_report_rslvstat() cda_dat_p_set_ready().
    3. И, кстати, Channel Access устроен так, что свойства канала присылаются в событии 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.

fdManager_cxscheduler:
  • 20.11.2024: создаём раздел.

    Штука эта нужна для того, чтобы epics2cda мог бы работать в ОДНОМ потоке.

  • 20.11.2024: заметки по реализации.

    20.11.2024: начато было ещё 16-11-2024, посредством копирования всего Xh_cxscheduler.c, т.к. его содержимое максимально похоже на требуемое тут -- из-за аналогичной раздельной обработки R/W/E.

    За позавчера/вчера/сегодня адаптирована работа с файловыми дескрипторами:

    • Реализованы "шаманские заклинания" работы с callback-объектами -- класс sl_fdReg, сделанный по образцу epics2tango_gw.cpp::e2t_update_pipe_fdReg, ...
    • ...плюс под эту модель переделана реализация API файловых дескрипторов -- sl_add_fd(), sl_del_fd(), sl_set_fd_mask().
    • Используется следующий подход для регистрации/разрегистрации callback-объектов при изменении желаемой маски событий в sl_set_fd_mask():
      • 3 штуки sl_fdReg-объекта присутствуют прямо в fdrec_t, и они "неживые" -- конструкторы для них не вызываются (это возможно благодаря тому, что аллокирование делается chunk'ами и для самих экземпляров fdrec_t конструктор не вызывается, а инициализация выполняется в GrowFddata() "вручную"), а сами они за-bzero()'ены.
      • При взведении бита маски объект "оживляется" посредством "placement new", ...
      • ...а при сбрасывании бита маски объект "уничтожается" посредством "placement delete" в виде вызова ~sl_fdReg() с последующим bzero().
    • Менеджмент как файловых дескрипторов, так и таймаутов переведён на модель "пул, хранящий указатели на неподвижные в памяти объекты, аллокируемые chunk'ами по N штук" -- по образу и подобию epics2tango_gw.cpp::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 букв, вторая И" (ага, "фиаско"):

    • Разобраться СТАТИЧЕСКИМ АНАЛИЗОМ кода -- фиг: определения всех классов в src/libCom/timer/epicsTimer.h, а код в timerQueuePassive.cpp, но там опять всё весьма равиольно -- и методы "=0", и вызовы из вызовов и т.п.
    • Стал гуглить "epicstimer site:anl.gov" и нашлась
    • ...страница "EPICS Application Developer's Guide - libCom"...
    • ...с разделом "19.11 epicsTimer" -- вот там есть примерное описание всех классов.
    • И ключевое -- метод expire(), вызываемый при истечении таймаута.

      Проблема в том, что метод этот, на вид -- у ОЧЕРЕДИ, а не у таймаута...

    • @вечер, засыпая после изучения того файла: и вообще у меня возникло впечатление (не могу объяснить аргументированно -- просто догадка, интуиция), что реализация таймеров в EPICS -- это прямое следствие базирования на vxWorks, всё это обусловлено многопоточностью, приоритетами и т.п.; файловые дескрипторы же изначально были где-то "сбоку", а уж реализация для signle-threaded (используемая libCAS'ом) появилась, возможно, и вовсе позже.
    • 25.11.2024: возможно, ключевое слово для "callback'а" -- epicsTimerNotify: так вот оно выглядит, в их извращённой архитектуре...
    • 25.11.2024@вечер: ещё рылся, разбирался --

      Да, epicsTimerNotify -- это именно самый что ни на есть "callback class", его надо передавать методу epicsTimer::start() первым параметром. И у него будет вызван метод expire() (который должен сделать return одного из вариантов: "noRestart" для обычных одноразовых таймеров, либо нечто типа "expireStatus(restart,30.0)" для повтора).

      Разобраться в этом было непросто, из-за тучи классов с именами, начинающимися с "epicsTimer", и далее тоже слабо различающихся.

      Сочетание чтения исходников с чтением вышеуказанной документации помогло.

    26.11.2024: да, сделал всё в соответствии с дркументацией, исполнив все належащие шаманские ритуалы.

    Осталось пока 2 вопроса:

    1. Не надо ли из метода expire() как-нибудь сделать destroy() таймеру? А то там не вполне очевидно (даже после чтения кода всяких timerQueue), возвращается ли таймер в очередь или делается ли его подчистка (и делается ли когда-либо вообще удаление однажды созданного объекта epicsTimer?).

      Можно будет проверить, печатая указатель на выдаваемый объект: если он будет повторяться -- значит, реюзается, а не утекает.

    2. Непонятка с "инициализацией" объекта класса 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) были добавлены ещё раньше.

    Теперь надо проверять...

epics2tango:
  • 06.11.2022: сейчас лишь раздельчик заводим, а так epics2tango.c была создана давным-давно (в районе 30-07-2022), но она крохотная -- практически только пустой скелет, даже описывать пока нечего.
  • 22.08.2022: в порядке выбора архитектуры "а как определять типы/метаинформацию в epics2smth" изучаем "внутреннее устройство" TANGO -- как там хранятся данные внутри DeviceAttribute[Proxy?].

    Собственно результаты -- в соответствующем подразделе раздела о cda_d_tango за сегодня.

    26.08.2022: а сегодня разбираемся в том, как хранится информация о ТИПЕ значения.

    Результаты -- в подразделе о реализации раздела cda_d_tango.

  • 12.11.2022: всё-таки начинаем какое-никакое наполнение (да, пока именно "никакое", но уж как есть).

    12.11.2022: собственно:

    • Захотелось унести временный "исследовательский" код -- печать некоторых TANGO-специфичностей, сделанный 25-08-2022 из любопытства, и заканчивающийся raise(11) -- из epics2smth.cpp::e2s_run().
    • Наилучшим местом для него очевидным образом является epics2tango.c::main().
    • Ну, перенёс, и -- схлопотал
      /usr/include/omniORB4/CORBA_sysdep.h:38:2: error: #error "Cannot use this C++ header file for non C++ programs."
      
    • В связи с чем пришлось трансформировать epics2tango.c в epics2tango.cpp.
epics2tango_gw:
  • 06.11.2022: создаём раздел, а сам исходник был зачат неделю назад.
  • 31.10.2022: зачинаем epics2tango_gw.cpp.

    Highlights:

    • Сделан копированием из epics2smth.cpp с отрезанием практически всего, кроме определений классов _Server, _gdd, _PV (реализации методов -- тоже почищены, остались только пустоты с return в концах).
    • Только префикс "e2s_" заменён на "e2t_.
    • Делать решено ТАМ ЖЕ, БЕЗ создания отдельной epics2tango_gw/.

    12.08.2024: некоторые размышления/постулаты/идеи касательно внутренней организации epics2tango_gw.cpp:

    • Делать ли его единым .cpp-файлом, парой файлов (отдельно EPICS- и Tango-части) или троицей (EPICS- и Tango-части плюс некая "промежуточная" или "общеполезная") -- пока неясно.
    • Сопоставление типов данных EPICS<->Tango (aitEnum*<->DEV_*) будет напрямую, БЕЗ CX'ных CXDTYPE_nnn (ибо незачем).
    • Оно будет thread-based и thread-safe, так что будет работать в PUSH-model и переправлять запросы/ответы между сторонами в тех же thread'ах, в которых будет их получать.
    • Организация внутренних структур данных:
      • Единицей "жизни"/"аллокации" будет DpxInfo -- аналог cda_d_tango.cpp'шного dpxinfo_t.
      • Они будут организовываться в vector<DpxInfo*> -- аналог SLOTARRAY'я dpxinfo_t, только тут кроме него нужен ещё какой-то ХЭШ для быстрого поиска по имени; значениями хэша должны быть индексы в векторе.

        Естественно, менеджмент элементов вектора и хэша должен вестись синхронно.

    • Локинг для multithreading'а:
      • У вектора+хэша свой единичный mutex, но это не "global lock", а именно защита критических секций поиска и модификации глобального списка DpxInfo'в.

        Идея в том, что обращения к этому списку должны происходить ТОЛЬКО в момент обслуживания поиска, дальше же работа с индивидуальными DpxInfo идёт внутри них, а они аллокируются не в векторе, а объектами в динамической памяти (вектор же содержит только указатели).

        ...в идеале, конечно, везде бы помнить не указатели, а именно индексы, но для трансляции индексов в указатели придётся обращаться к вектору, а такое обращение требует взведения егойного mutex'а, который по сути превращается в "global lock".

      • У каждого DpxInfo есть свой mutex, взводимый на время работы с ним.
    • Поскольку асинхронно проверить "есть или нет такой атрибут или команда" возможность отсутствует, то надо сразу после создания получать полные списки атрибутов и команд данного устройтсва и хранить их внутри себя (можно сразу вместе с типами).

      Раз создание DeviceProxy всё равно синхронно и занимает макроскопическое время, то добавление к нему ещё времени на запрос списков ситуацию принципиально уже не ухудшит (а для недоступных девайсов всё равно выполняться эти запросы не будут).

    • Т.к. в момент первого обращения к конкретному "девайсу" -- т.е., к DpxInfo -- его DeviceProxy ещё не создан и списков атрибутов и команд ещё нет, то и сразу дать ответ "есть/нет" нельзя. 01.09.2024: нифига -- МОЖНО: отвечать "нет", а уж libCA пусть потом повторит запрос.
      • Поэтому в DpxInfo надо завести список (любой организации -- вектор, FIFO, linked list, ...) для хранения необслуженных асинхронных запросов (casAsyncPVExistIO), чтобы при успешном завершении создания DeviceProxy пройтись по этому списку и поотвечать да/нет.
      • Чтобы оный список не рос бесконечно -- что неизбежно случится при обращениях к несуществующим именам устройств -- то воизбежание "DoS-атак" надо ввести ограничение, например, в 100 элементов, при превышении которого принудительно отвечать "pverDoesNotExistHere" прямо сразу.
      • Список защищается mutex'ом самого 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'ами).
      • Поскольку запрос единственного недоступного устройства может сразу завесить целый thread-"создатель", то, возможно, нужен ПУЛ таковых.

        Принципы организации и/или роста такого пула -- вопрос дискуссионный. Сходу напрашивается следующее простейшее решение:

        • Каждый следующий запрос передаётся следующему участнику пула, с закольцовыванием по их количеству.
        • Насчёт количества роста: а фиг знает. Например, можно
          • вести счётчик количества "необслуженных запросов" (именно на создание устройств, а не на резолвинг имён!) и...
          • ...в момент надобности отправки запроса при превышении оным количеством количества "создателей" создавать новый thread-"создатель".
          • Но не более скольки-то штук (10? 30? параметр командной строки, по умолчанию 10?).

          Неприятность -- что этот счётчик тоже придётся как-то защищать, и mutex'ом-на-вектор нехорошо, т.к. опять получится почти "global lock".

          ...впрочем, учитывая, что "создание нового устройства" -- операция предположительно нечастая, а о "завершении создания" тоже надо будет как-то "сигнализировать" (чтоб выполнить ответ на запросы из очереди casAsyncPVExistIO), то можно действительно сделать на счётчик отдельный mutex, который, по сути, будет и mutex'ом на пул thread'ов-"создателей".

      • Отдельный вопрос -- как передавать информацию "создай такой-то девайс" между thread'ом-"слушателем запросов" и thread'ами-"создателями".

        Напрашивается решение: создание нового DpxInfo полностью выполняется в "слушателе", а потом "создателю" передаётся короткая атомарная ссылка --

        1. числовой идентификатор (плохо из-за global lock'а)
        2. или сразу указатель (а вот это обломится на e2k-128: считанное из pipe'а значение НЕ будет указателем, т.к. не будет обладать должным тэгом).

        Как вариант -- реализовать оба способа, с переключением между ними по define-условию, выставляемому на основе архитектуры ("#if CPU_E2K128", вот только такого в Sysdepflags.sh нету...)

    • ЗЫ: предыдущие рассуждения на тему этой архитектуры были в разделе по cda_d_tango от 28-08-2022.
    • @ждя у трансформаторной будки рядом с Бердск-Микрорайон-68 ~17:00: вот только что делать в ситуации, когда надо ОСВОБОДИТЬ ячейку (например, нулевое количество клиентов для такого-то DeviceProxy в течение некоего времени)? SLOTARRAY-то эту функциональность обеспечивает, а ТУТ как?
    • 19.08.2024@~21:30, лёжа на большом диване при отключенном электричестве ("авария сетей, что-то переключают на подстанции Академическая"): да очень просто сделать -- добавлять свободные элементы в список свободных и при надобности нового просто брать первый оттуда, как это делает cxscheduler.c с avl_tid и GetToutSlot().
    • 19.08.2024@~21:30, лёжа на большом диване при отключенном электричестве ("авария сетей, что-то переключают на подстанции Академическая"): кстати, к вопросу, КОГДА освобождать:
      • Иметь в DpxInfo:
        1. reference count -- сколько каналов/PV в текущий момент зарегистрировано на него;
        2. поле "время последнего обращения", используемое при нулевом количестве зарегистрированных.
      • Когда присутствуют зарегистрированные каналы/PV -- ничего не делать.
      • В момент разрегистрации последнего (т.е., при обнулении reference count) в поле "время последнего обращения" сохранять текущее время.
      • И с какой-то периодичностью -- например, раз в минуту -- проходиться по всем ячейкам с проверкой: если ноль зарегистрированных и с момент последнего обращения прошло больше 10 минут (интервал настраиваемый!), то ячейку освобождаем.
      • И нюанс касательно НЕДОсозданных -- т.е., тех, для кого не удалось создать DeviceProxy: к ним стоит применять тот же подход, с оговорками:
        1. количество зарегистрированных можно игнорировать -- какая разница, сколько юзеров с ним всё равно "не работают";
        2. за "время последнего обращения" надо считать момент последнего запроса канала для него -- даже если тот был бы уже 100-м в очереди и получил pverDoesNotExistHere сразу, но учесть его нужно.

        А процесс "с какой-то периодичностью ... проходиться ..." для них сводится к тому, что проверку "а не пора ли грохнуть?" надо проводить непосредственно перед очередной попыткой создания DeviceProxy -- это всё равно выполняется в цикле для всех обслуживаемых ячеек каждого thread'а-"создателя".

        31.08.2024: неа, сделано не "перед", а ПОСЛЕ: раз уж создание обломилось и deadline прошёл, то чего ячейке зря висеть.

      10.09.2024: ещё некоторые соображения на тему подчистки неиспользуемых (более 600 секунд) DpxInfo:

      • Можно было бы определять момент перехода ref_count из >0 в ==0 и при этом перемещать такой dpx-ID в отдельный список "idle".

        ...а по переходе из ==0 в >0 -- убирать из этого списка.

      • И уже по этому списку (а не по всем существующим) проходиться раз в минуту для проверки "не пора ли грохнуть".
      • А уж если держать список СОРТИРОВАННЫМ -- чтоб самые старые элементы были в начале -- то подчистка становится совсем простой: "если текущий в голове списка старее 600 секунд, то удалить и перейти к следующему, а иначе завершить подчистку".

      Вот только муторно это всё: и сама необходимость добавлять в список "idle" и убирать из него (вот ЭТО точно потребует ДВУнаправленного списка), и "сортировка" (хотя при ДВУнаправленном она тривиальна -- просто добавлять в конец).

      СЕЙЧАС как-то не выглядит эта овчинка стоящей выделки.

    • 21.08.2024: вот только надо ЛОКИНГ аккуратно делать -- т.к. освобождение ячейки предполагает лочку "global mutex'а" плюс обязательно ещё и mutex'а конкретной ячейки, то не нарваться бы на deadlock.

      Причём не только "технический", но и "идеологический": кабы не возникло ситуации, что как раз в момент, когда одному thread'у надо обратиться к ячейке, другой её будет грохать. ...но если грохать будет "главный" thread, который обрабатывает запросы наличия, то проблемы нет. А есть она при взаимодействии оного "главного" с "создателями" -- те-то могут захотеть грохнуть ячейку, к которой "главный" как раз обращается.

      @вечер: тут придётся лочить буквально каждый чих: если решил, что это "она", то ещё ДО снятия лока инкрементнуть reference count, чтобы "создатель" точно не подчистил; "создатель" же должен также грохать атомарно.

      @вечер: а для невозникновения deadlock'ов можно ввести правило: если в некоей ситуации понадобятся ОБА лока, то сначала захватывать глобальный, а потом ячеечный (и освобождать в обратном порядке).

    24.08.2024: ещё важное замечание:

    • Хотя, скорее всего, запросы на создание (Attach) будут прилетать в ОДИН thread, но реализовывать обработку как их, так и прочих запросов (чтение, запись, ...) нужно в thread-safe манере -- чтобы при надобности была возможность раскидывать эти работы по произвольному количеству thread'ов.

    Кстати, попробовал посмотреть, как устроена "регистрация" в самой libCAS: ведь по логике, сразу же за успешным pvExistTest() должен бы следовать pvAttach() -- тогда можно упростить работу своего "класса", регистрируя tango-канал уже прямо из проверки наличия.

    Но не тут-то было:

    • Просто "статически" найти место не удалось -- "grep по всем исходникам" дал какие-то непонятные места в файлах с малопонятными названиями вроде "casDGClient.cc" ("client", блин!!! я и подумал, что это пример какой-то).
    • Хорошо, запустил 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: более практические соображения насчёт парсинга имён.

    • С одной стороны, желательно бы унифицировать код epics2tango_gw.c с cda_d_tango.cpp -- парсинг по сути одинаковый и нефиг дублировать.
    • С другой стороны, в cda_d_tango.cpp если determine_name_type() ещё самодостаточна, то прочая алхимия с разбором (и преобразованием '.' в '/' и ".." в "->") выполняется непосредственно в cda_d_tango_new_chan() и несколько перезавязана с логикой работы собственно регистрации канала.
    • Что-то переделывать ТАМ для возможности вытащить СЮДА -- вломы да и не комильфо.
    • Поэтому лучше поступить так:
      1. Сначала сделать ТУТ "правильный" вариант -- с парсингом/трансляцией, вытащенными в отдельную функцию.
      2. А потом уже перевести и ТАМ на такой вариант -- оно будет красивше и структурированнее (а то сейчас 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 таковое и происходит.

      А выполнять потом отдельное аллокирование меньшего кусочка уже чисто под "имя сущности внутри устройства" -- глубокого смысла скорее нет.

    • 18.09.2024: сегодня функциональность расширена распознаванием ".FIELDNAME", оно же "4-й '/'"; именно РАСПОЗНАВАНИЕМ, БЕЗ за-'\0'ивания.

      При этом наличие dup_name выглядит ещё более осмысленным: ведь именно внутри этого буфера при надобности и делается "отрезание" того "FIELDNAME" (вызывальщиком) путём за-'\0'ивания.

    • 18.09.2024: а ещё распознавание префикс-суффиксов "@b" и "@v" сделано условным -- путём помещения в "#if MAY_PARSE_AT_DATATYPE_MODIFIERS", каковой символ по умолчанию не определён.

      Смысл -- что в epics2tango_gw эти префикс-суффиксы мимо кассы, т.к. тут флаги BHVR_BOOL и BHVR_VOID взводятся на основании информации из AttributeInfoList или CommandInfoList.

    30.08.2024: за последние пару дней сварганил основу CreatorThreadProc(). Структуру/алгоритм опишем позже, а пока некоторые highlights:

    • creators_pool[] решено сделать не растущим "SLOTARRAY'ем", а статическим массивом. Резоны:
      1. Размер структуры небольшой, максимальное количество thread'ов вряд ли превысит 100, так что общий объём будет невелик.
      2. А CreatorThreadProc()'у надо передавать указатель на какую-то структуру-описатель, чтобы в ней передать пару файловых дескрипторов (для чтения команд и для отправки ответов), и структура эта уж точно не должна гулять по памяти.

      Так что проще всего сделать статический массив.

    • Очень лень было держать ячейки в ДВУнаправленном списке, поэтому сделан prv2cr, в котором всегда поддерживается указатель на предыдущую в списке ячейку (либо NULL если текущая первая).
    • Всё-таки немножко логики по "scheduling'у" -- насколько зависать и по распределению ячеек. Придумано в основном 29.08.2024@~16-17 дорога из магазина домой через студгородок, проходя вдоль ФМШ'шных общаг.
      • Пришедший ставится в НАЧАЛО очереди.

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

      • Если очередь пуста, то select()'у передаётся timeout=NULL, чтоб висел бесконечно долго.
      • Если же НЕпуста, то
        1. Если последняя попытка создания была обломной, то зависаем на 10 секунд (время до следующей попытки создания).
        2. Если же успешной, то указывается 0 секунд -- просто поллинг, т.к. у новых всё равно приоритет -- т.к. раз получилось у кого-то из очереди, то с высокой вероятностью следующий от того же сервера, который только что ожил или стал доступен, так что и следующий тоже тут же получится приконнектить.
      • Успешность/обломность определяются булевской last_was_successful -- она же используется и после попытки создания для решения, что делать с ячейкой дальше.
    • Ячейки могут передаваться как указателями, так и ID'ми, в коде поддерживаются оба варианта; какой из них выбрать, определяется #define-символом MAY_SEND_POINTERS, по умолчанию выставляемым в 1 (предполагается, что для неподдерживающих платформ, вроде E2K_128, должно будет делаться "SPECIFIC_CPPFLAGS=-DMAY_SEND_POINTERS=0").
    • При "освобождении недосозданных DpxInfo" решение об освобождении -- в соответствии с 19-08-2024 -- принимает CreatorThreadProc(). Но ведь к DpxInfo в этот момент привязана цепочка запросов existTest, на которые отвечать имеет право только основной thread.

      Поэтому такие "протухшие недосозданные" будем передавать основному thread'у аналогично готовым, но для отличения их введём дополнительное состояние DPXINFO_TIMEDOUT, чтобы основной thread отправил бы всем запросившим pverDoesNotExistHere и освободил бы ячейку.

      ...а при проверке "есть ли DpxInfo с именем устройства" если он находится, но в состоянии DPXINFO_TIMEDOUT, то pverDoesNotExistHere отдаётся сразу (как если в списке атрибутов/команд имя отсутствует).

      ЗАМЕЧАНИЕ: это всё (описанное в данном пункте) пока ещё не сделано, а только задумано.

    01.09.2024@ванна, ~19:00: код сейчас складывается так, что есть ДВА вполне раздельных куска кода:

    1. взаимодействие с Tango и его обвязка в виде работы с thread'ами (верхняя часть файла);
    2. взаимодействие с EPICS/libCAS, пользующееся услугами первой части.

    Откуда напрашивается:

    • И вот если так и продолжать разделение, то "верхняя" часть будет годна и для перспективного pvxs2tango_gw.cpp -- безо всяких изменений (ну, может, с иным подходом к 64-битным целым).
    • @ложась спать: а если делать таки отдельным файлом, то его можно назвать smth2tango.cpp: тогда получится, что собственно "epics2tango_gw" состоит из epics2smth+КЛЕЙ+smth2tango -- как бы собираем из "кубиков".

      ...правда, вряд ли так получится -- дело в трансляции типов: в связке 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-м этаже перез залом круглого стола: проект таков:

    1. Главная идея -- стараться сбагривать создание очередного DeviceProxy по возможности СВОБОДНОМУ thread'у.
    2. А если какой-то ОДИН thread (например, умолчательный 0-й) успевает обработать предыдущий запрос -- ну пусть тогда он постоянно этим и занимается.
    3. Искать свободный -- начиная с предыдущего использованного, среди всех "по кругу, пока не вернёмся к предыдущему".

      Это, кстати, автоматически удовлетворяет требованиям 1 и 2 -- если предыдущий использованный свободен, то он и будет выбран для очередного задания.

    4. ...и вот если среди уже запущенных не нашлось свободного -- тогда можно запустить ещё один, если пул "создателей" ещё не заполнен.
    5. А если заполнен (и свободных нет), то использовать просто следующий, по циклу (Round Robin).

    Уж не знаю, насколько такой "алгоритм" годится в качестве общего решения для разравнивания загрузки между пулом "worker thread'ов" -- ведь в данном конкретном случае основная загвоздка в длительных зависаниях в случае сложностей с коннектом, а также в том, что неприконнекченные остаются в очереди надолго.

    @подвал "у Кузнецова": реализация:

    • Введено поле CreatorThreadInfo.is_busy --
      • поскольку "занятость" thread'а определяется наличием/отсутствием у него списка DpxInfo, а оный список определяется локальной переменной frs2cr в CreatorThreadProc(), то "снаружи" понять занятость невозможно;
      • поэтому и введено is_busy, которое при получении "команды на создание очередного" взводится в 1, а сбрасывается в 0 при опустошении списка после передачи (т.е., при frs2cr=NULL).
      • ЗАМЕЧАНИЕ: обращения к is_busy НЕ закрываются mutex'ом, т.к. сам флаг атомарен и не требует железной консистентности с другими (frs2cr), и "промах" при race condition влечёт не критические последствия, а всего лишь деградацию производительности вследствие неоптимального выбора "создателя".
    • Также создана creators_pool_rr_n ("rr_n" -- Round Robin N) -- тот самый номер последнего использованного "создателя".
    • Ну а дальше -- довольно несложно по вышеприведённому алгоритму.

    @дорога из ИЯФ в УЭВ и по магазинам, срезая по диагонали с Лаврентьева на Николаева мимо Катализа: а ведь софтина доведена уже до полу-готового состояния -- резолвинг с подстилающей инфраструктурой thread'ов и с передачей информации (между ними и основным) готов, отсутствует только инфраструктура "мониторов", необходимая для собственно чтения/записи/мониторирования (ну и трансляция типов данных). Так что в нынешнем состоянии уже вполне можно тестировать -- как работает уже реализованная часть.

    Ещё некоторые соображения:

    • @дома, записывая всё это (в частности, "неприконнекченные остаются в очереди надолго"), ~18:30: а насколько реально имеет смысл оставлять обломившиеся в очередях и пытаться продолжать их коннектить? Это ведь не cda_d_tango.cpp, который ОБЯЗАН пытаться приконнектиться, пока не получится (или пока все ссылающиеся на устройство (и привязанные к dpxinfo_t) каналы не будут удалены, но этого не делается -- там вообще подчистки нет, в т.ч. cda_d_tango_del_srv() ничего не делает, а cda_d_tango_del_chan() отсутствует вовсе -- как и cda_d_epics_del_chan(), кстати). Тут при обломе-прямо-сейчас о повторном запросе позаботится клиентская libCA. Может, просто обламываться СРАЗУ (а очереди оставив именно как очереди-"буфера" запросов, растущие при превышении количеством запросов количества "создателей")?.
    • @дома, моя посуду после ужина, ~21:20: с другой стороны, если отваливать после первого же облома -- ведь libCA будет продолжать присылать запросы с немаленькой частотой, и сразу после облома тут же стартует следующая попытка (единственное что -- запросы, прилетающие ВО ВРЕМЯ коннекта, будут игнорироваться, т.к. в этот момент будет существовать DpxInfo с таким именем и в состоянии DPXINFO_CREATING). Так что -- может, и не стоит устранять очереди; а имитировать "отваливать сразу" можно уменьшением значения option_idle_seconds до 1 секунды (это явно больше времени таймаута).
    • ЗЫ: но вот некоторые "штатные" ошибки/exception'ы Tango надо бы научиться распознавать -- всякие "device doesn't exist in server" и тому подобные. ...хотя в свете предыдущего -- а толку-то от этого? Может, лучше наоборот, оставлять их висеть в очереди на подключение, чтобы хоть эти 10 секунд паузы между попытками оставались бы спокойными...

    04.09.2024: небольшое дополнение: весь процесс поиска "создателя" теперь окружён блокировкой свежевведённого creators_pool_lock_mutex -- исключительно "на всякий случай" (или "на будущее"), чтоб этот кусок кода был бы годен для multithread-safe-варианта, где запросы на каналы могли бы приходить разным thread'ам.

    05.09.2024: и он ДЕЛАЕТСЯ ВНУТРИ ЗАЛОЧЕННОГО dpx_pool_lock_mutex!!! Правильно ли это?

    05.09.2024: проверяем.

    • Напихано диагностической печати.
    • И сразу же обнаружился непредвиденный (но очевидный эффект): если приходит пара (или пачка) запросов подряд, то они оба (или все) отдаются ОДНОМУ (сначала -- 0-му) прежде свободному thread'у.

      Причина тривиальна:

      • Выбирается "свободный" thread, но "свободность" сбрасывается самим thread'ом при получении запроса.
      • А между отправкой команды в pipe и её вычитыванием "создателем" проходит некоторое время (хоть и небольшое), в течение которого thread по-прежнему считается свободным, а потому вторая команда опять уходит ему.

      Неприятно, но некритично.

    • А потом вдруг стало подвисать. И найти причину оказалось непросто -- пришлось разбираться, как в GDB переключиться на нужный thread, чтоб сделать ему "bt".
      1. Сказать "info threads", и в полученном списке найти нужную.

        "Нужная" определяется по TID'у: и GDB его печатает с префиксом "LWP", и диагностическая печать программы выдаёт результат gettid()'а -- вот и сопоставить.

      2. Затем сказать "thread N" (у нас N=3).

      Висело на лочке mutex'а...

    • Оказалось, что в CreatorThreadProc() при проверке по timestamp'у "не протухло ли" лочился один mutex (di->lock_mutex), а разлочивался другой (dpx_pool_lock_mutex).

      Исправлено.

    • После этого вроде работает как предусматривалось.
    • @вечер, перед сном: только вопрос -- а насколько имеет смысл соблюдать паузу 10с в случае, если предыдущий не просто обломился, а конкретно по таймауту? Ведь по факту этот таймаут и так организует паузу (то ли 6, то ли 9 секунд).

      Вопрос скорее в том, насколько возможно определение "проблема была именно в таймауте".

      06.09.2024: сходу в исходниках видно только общий exception Tango::ConnectionFailed; в диагностике светится некая строка "TRANSIENT_CallTimedout", но насколько оно доставабельно кодом -- фиг знает...

      06.09.2024: а вообще нам ведь ПОФИГ на причину облома, главное -- пауза; так что можно засекать время отработки попытки создания, и либо а) если она длилась дольше, например, 3 секунд, то считать лишним последующее ожидание в select()'е; либо б) просто вычитать это время из последующих 10 секунд, а если результат окажется отрицательным, то считать "время прошло" и делать просто поллинг.

    06.09.2024: пожалуй, на этом проверку работы резолвера можно заканчивать: дальше проверить пока ничего не удастся, т.к. даже для cainfo нужно нечто, способное отдавать информацию о свойствах, читай -- уже нужны PV, т.е., надо городить поддержку мониторов.

    • Массив надо делать "сквозным", без привязки к DpxInfo (в ячейках только ссылки на них), и для быстрого аллокирования с "указателем-индексом" 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 уже есть векторы команд и атрибутов, содержащие их названия и индексируемые целочисленно, то:
      1. В мониторах незачем хранить имена:
        • После поиска CheckTangoName() может делать free(dup_name) и далее использовать "индекс атрибута/команды внутри вектора".
        • При заказе чтений/записей можно использовать (*(di->attribute_info_list_p))[ATR_IDX].name.c_str() -- там ровно эта строка.
      2. И для ссылки монитора на "конкретную сущность внутри устройства" надо использовать тот самый индекс.
    • А вот информацию о каналах (типы, размеры) в мониторах хранить не надо -- лучше брать из хранящихся в DpxInfo векторов каждый раз, когда спрашиваются.

      07.09.2024@утро-душ: учитывая, что методов "дай вот-такое-то свойство" у PV не один, а процесс определения "откуда же брать, в зависимости от chtype" двухстадийный -- видимо, надо его вытащить в отдельную функцию, возвращающую ВСЕ свойства, а уж конкретный метод пусть после её вызова возьмёт себе нужное.

    09.09.2024: мини-отчёт о создании инфраструктуры мониторов:

    • Скопировано из epics2smth.cpp, ...
    • ...убраны libCAS-специфичные вещи (AS_ptr, obj_ref), ненужный тут name, а также chn_dtype,max_nelems,is_rw, ...
    • ...а затем переделано на "pool" вместо "list" -- аналогично менеджменту DpxInfo.
    • И после внимательного анализа -- и функционирования epics2smth.cpp, и тутошних потребностей -- понято, что ДВУнаправленный (ДВУХсвязный) список нам тут нафиг не нужен: связность вообще требуется только для списка свободных, а при работе -- нет, т.к. здесь нет необходимости в последовательном проходе по списку мониторов.

      Вот и получается, что достаточно было просто скопировать ОТСЮДА ЖЕ инфраструктуру DpxInfo, а не брать и адаптировать из другого места.

    • А ещё надо переименовать GetDpxInfoSlot()/RlsDpxInfoSlot() в GetDpxSlot()/RlsDpxSlot() -- "Info" тут лишнее.

      Сделано.

    10.09.2024: инфраструктура мониторов переделана с ДВУхсвязного списка на ОДНОсвязный.

    11.09.2024@утро, по пути в ИЯФ, спускаясь по лестнице с 10-го вниз, ~08:40: бредовые идеи "на перспективу", как можно бы УДАЛЯТЬ thread'ы-"создатели":

    • Постулат: ГРОХАТЬ thread'ы нельзя -- никаких pthread_cancel()! -- они должны уметь завершаться сами, по команде.
    • "Командой завершиться" может быть присылка ячейки с di==NULL (или ID==0 -- у нас специально эта ячейка оставлена неиспользуемой).

      (Так-то можно развить целую инфраструктуру для РАЗНЫХ команд: ID<0 -- -1, -2, ...; указатели с числовыми значениями 0x1UL, 0x2UL, ...)

    • А вот как сам thread может подтверждать исполнение команды?

      Очевидный вариант -- закрыть свой rpy_pipe[PIPE_WR_SIDE], чтобы "хозяин" получил уведомление о готовности и read()==0 -- выглядит не фонтан: будет неатомарность, т.к. неопределённо, какое из событий наступит раньше -- реальное завершение thread'а или получение уведомления "хозяином".

    • @дома, ~14:00: разве что можно ввести правило: в момент отправки сам thread более НЕ доступается к содержимому своего CreatorThreadInfo, так что "хозяину" пофиг, успел ли "создатель" завершиться к моменту получения "хозяином" уведомления.

      ...но тут проблема в том, что в объекте pthread_t могут оставаться какие-то недоподчищенные ресурсы, так что возможное повторное использование ячейки с записью в поле threadid их перепропишут.

      А ещё ведь завершившимся thread'ам надо делать pthread_join(), для подчистки их ресурсов...

      ВОЗМОЖНО, сработает такой вариант: если запускать "создателей" в режиме "detached", то подчистка будет выполняться автоматически (это-то не "возможно", а гарантируется спецификацией), а если pthread_t является просто числом, то тогда его перепрописывание будет безопасно.

      ...и в любом случае стоит иметь в CreatorThreadInfo поле "state", в котором у неиспользуемых UNUSED, а завершающиеся заносят туда FINISHING, чтоб их не пытались использовать, и при получении уведомления "хозяин" будет прописывать UNUSED.

    13.09.2024: потихоньку подходим к манипуляциям с данными.

    • Позавчера много думал на тему "как вообще там всё сделать -- практически?". Цельной картины в голове так и не сложилось: хотя все шаги на опыте cxsd_fe_epics_meat.cpp и cda_d_tango.cpp уже пройдены, но как конкретно их сложить ВМЕСТЕ -- пока не осознал.
    • Вчера втянул из libcas2cx_conv.h пару функций-"сопоставляторов", переименовав их в CmdArgType2aitEnum() и aitEnum2CmdArgType().
    • А сегодня их содержимое переделано с CXDTYPE_nnn на DEV_nnn, на основе анализа и сопоставления определений из tango_const.h, aitTypes.h, db_access.h:
      1. При этом таблица в CmdArgType2aitEnum() слегка расширилась за счёт BOOLEAN, STATE и VOID.
      2. Да и с ENUM всё непросто: Tango'вский DevEnum ЗНАКОВЫЙ, а EPICS'ный aitEnum16/dbr_enum_t -- БЕЗзнаковый.

        Поэтому они маппируются асимметрично: первый на Int16/DBR_SHORT, а второй на DEV_USHORT.

      В комментариях в тексте эти траблы расписаны, к каждому типу индивидуально.

    Также по ходу обдумывания возникло несколько мыслей/замечаний:

    1. ЗАМЕЧАНИЕ 1:
      • Поскольку ТУТ (в отличие от cda_d_tango.cpp) типы целевых атрибутов/команд известны, то незачем парсить BHVR_BOOL и BHVR_VOID из @-суффиксов имени.
      • Но, поскольку информация эта нужна (в момент отправки), то...
      • ...надо флаги выставлять в момент создания монитора именно на основании типов целевых атрибутов/команд.
    2. ЗАМЕЧАНИЕ 2: есть потенциальный косяк: флаг BHVR_ON_UPDATE берётся всё же из парсинга имени (больше-то неоткуда ведь?), то может оказаться, что разные запросы (отражающиеся на один атрибут) от разных клиентов указывают РАЗНЫЕ режимы мониторирования.

      А монитор-то будет ОДИН!

      Что делать? Забить на проблему и использовать режим от ПЕРВОГО -- когда, в момент interestRegister(), и будет выполняться subscribe_event()?

    3. ЗАМЕЧАНИЕ 3: у нас в map'ах используется 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", !(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().

      Исходные компоненты у нас уже есть, в виде

      1. cxsd_fe_epics_meat.cpp::fe_epics_PV::write() -- добыча значений из gdd;
      2. cda_d_tango.cpp::cda_d_tango_snd_data()+snd_data_via_command() -- собственно исполнение отправки данных в Tango.

      Нужно только их "скрестить" вместе.

    • Итого -- эта троица методов "втянута" в текст, +~600 строк.

    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(), но нет).
      • Будет устранено дублирование: в обеих есть туча переменных v_NNN_p=NULL и в конце в секции 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), то векторность уже НЕ ортогональна строковому типу.

    • Создание копии строки (в cda_d_tango.cpp требовавшееся из-за отсутствия в CX terminating-NUL'а, а тут унаследованно-от-cxsd_fe_epics_meat.cpp исполняемое из-за пофигистичного отношения libCAS к наличию NUL'а в 40-байтных строках) перетащено в точку сразу после "нахождения" длины оной 40-байтной строки.
    • Также стало понятно, что по аналогии со строками, надо вытаскивать в начало -- для унификации между атрибутами и командами -- всех "нетривиальных преобразований", в качестве которых у нас сейчас превращение целочисленных данных (любой битности!) в булевские скаляры и векторы.

    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 ветках, скалярной и векторной, и после внимательного взгляда на "различия" -- сделана унифицированной:

      • Цикл от 0 до nelems-1 делается в ЛЮБОМ случае, в нём производится собственно конверсия 1 элемента в зависимости от типа исходных данных.
      • Но при nelems!=1 ...
        1. ДО цикла создаётся вектор, ...
        2. ...а сразу после конверсии -- в idx'ный элемент кладётся результат оной конверсии.

      Таким образом, в случае скаляра просто выполняется конверсия 1шт, а при векторности -- ещё и заполнение всего массива.

    • -- вот после того и стало очевидно, что векторы строк стоит создавать ровно так же, а потому удобнее "подготавливать" строки в локальном буфере.
    • 23.09.2024: работа со строками будет переделана по образу и подобию работы с булевскими -- унифицированно скаляры/векторы, с при 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'.

    • Сделано "дерево" вложенных условий, в сумме на 4 варианта: наружное -- DEVATTR/COMMAND||RESULT, внутренние -- скаляр/вектор.
    • И начато заполнение -- с простейшего, DEVATTR-скаляр.
    • По ходу возникло 3 вопроса, требующих разбирательства ДО продолжения дальнейщих работ:
      1. Надо проверить libCAS'овский API не-фиксированных строк: обеспечивает/поддерживает ли он NUL в конце строки? (или держит её как просто байты+длина?)

        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: да, так и сделано.

      2. Проверить API DeviceAttribute: а есть ли там конструктор, принимающий vector<string>?

        Ответ: да, есть, как и у DeviceData.

      3. Какой у libCAS/gdd API работы с МАССИВАМИ строк?

        23.09.2024: а хрен поймёт; но, похоже -- судя по результатам изучения gdd::dataVoid() от 01-07-2022 -- если вектор, то всегда хранится адрес; соответственно, по этому адресу будет просто массив aitString или aitFixedString. Учитывая "качество" кода libCAS/gdd, впрямую статическим анализом разобраться будет проблематично, поэтому напрашивается путь "сейчас реализуем в вышеописанном предположении, а потом попробуем проверить вживую и посмотреть, соответствует/работает ли"; благо, в каком-то из Tango-VME-устройств был массив строк.

    • И ещё 2 замечания на будущее:
      1. Надо делать try{}catch() вокруг ВСЕХ new(), плюс проверять все их результаты на ==NULL.
      2. Проверить все конверсии, чтобы они были "обратимы" и совместимы с aitEnum2CmdArgType(), т.е. чтоб если накая TANGO-сущность НАМИ декларируется как некий EPICS'ный-тип-данных, то чтоб тот тип конвертировался в ПРАВИЛЬНЫЙ исходно-TANGO-тип.

        Простым языком -- чтобы BOOL и ENUM декларировались бы как типы, которые потом преобразуются именно в BOOL и ENUM.

    21.09.2024: сделано аллокирование PV, теперь она полем внутри MonInfo, инициализируется посредством "placement new".

    Только одна засада: так и не решил, в какой момент инициализировать (и уничтожать), т.к. есть 2 варианта:

    1. Инициализировать в момент отдачи монитора из пула свободных в использование (а уничтожать при освобождении и возврате в пул).
    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].

    • И явно надо делать "MonInfo" как бы "наследником" e2t_PV -- чтобы избавиться от 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 превратилось в
    /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
    
    (тогда-то я не заметил подсказки про libomniDynamic4.so.2 -- только сейчас, записывая).
    • При сборке cda_d_tango.so эта проблема почему-то не вылезала.

      Хотя код тот же самый.

      Пытался понять, в чём причина -- фиг.

    • В конце концов собрал из исходников из omniORB-4.2.0-3.el7.src.rpm и увидел, что результат компиляции any.cc (в котором содержатся искомые операторы) кладётся в libomniDynamic4.so.2.0
    • Библиотека эта в RPM'ке тоже есть и она установлена.
    • Тупое добавление "-lomniDynamic4" к SPECIFIC_LDFLAGS проблему решило -- теперь собирается.
    • Вопрос 1: почему при линковке .so'шки линкер сам подхватывает нужные дополнительные библиотеки, а при линковке бинарника -- нет?
    • Вопрос 2: а как он догадывается, какие библиотеки предложить с рекомендацией "DSO missing from command line"?

    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.cpp::cda_d_tango_EventCallBack, назван e2t_CB. Методы пока пустые (только диагностическая печать тоже скопирована).
    • Добавлено поле MonInfo.the_CB -- прямо объектом (а не указателем!) прямо в структуре, и
      • Вызывается placement-конструктор в GetMonSlot() и...
      • ...placement-деструктор в RlsMonSlot().

      Но тут, если решим перейти на создание PV в момент аллокирования chunk'ов, надо будет также перетащить конструктор в точку после аллокирования.

    • В e2t_PV::write() раскомментированы вызовы write_attribute_asynch() и command_inout_asynch(), ранее закомменченные, т.к. им надо передавать какой-то callback-объект, коего раньше просто не было.

    Теперь соображения касательно буферов для дОбычи и возможного хранения данных:

    • Иметь при каждом мониторе свой буфер, куда складывать данные, получаемые от TANGO, чтобы потом передавать их libCAS?

      Муторно -- тут придётся на каждый монитор опять иметь по mutex'у, т.к. мы не знаем, в какой момент из какого thread'а кто нас дёрнет (и формально TANGO вроде не обещает, что не придут 2 разных обновления подряд через РАЗНЫЕ thread'ы, так что второе запустится в момент, когда ещё первое не завершилось...).

    • ...можно прямо из тех "буферов", куда делается >> из DeviceAttribute/DeviceData -- т.е., адреса локального скаляра либо значения векторовых v_NNN.data() -- сбагривать в upd_gdd и забывать (а дальше уж пусть GDD заботится).

      Тогда никакой mutex не понадобится.

    • Но лучше-то избавить GDD от этой работы, складировав всё в СВОЙ буфер и указав его посредством installConstBuf().

      04.10.2024: как, кстати, и делает cxsd_fe_epics_meat.cpp::mondata2gdd().

    • И дальше только вопрос -- как организовать менеджмент этого "своего буфера", учитывая, что конкретная Mon-ячейка по мере подключения/отключения клиентов (и запрошенности PV) может менять свою принадлежность и требования к буферу.
    • Напрашивается такой подход:
      • Иметь в 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?

    • Главный вопрос -- менеджмент памяти: ведь надо ещё в момент создания монитора понять требуемый объём и аллокировать его; но для строк переменной длины (особенно для их массивов!) эта задача принципиально не имеет решения. А вот для фиксированных -- легко: либо 40 байт, либо 40*max_dim_x для векторов.
    • С другой стороны, поскольку EPICS'ные строки обычно ограничены 40 символами, то незачем выпендриваться с variable-length-строками и можно ВСЕГДА ориентироваться на фиксированные.

      Для этого надо, чтобы CmdArgType2aitEnum() для DEV_STRING возвращала бы aitEnumFixedString, а не aitEnumString, как сейчас.

    • Но с третьей стороны -- а если строка длиннее 40 символов, то клиент может попросить её отдать в виде CHAR-array (печать -- "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, ...
    • ...затем делала бы добычу данных из DeviceAttribute или DeviceData
    • (в т.ч. заботилась бы о получении timestamp, quality и nelems, которые во втором случае берутся НЕ из DeviceData (т.к. там их нет)), ...
    • ...а в конце УНИФИЦИРОВАННО для обоих вариантов выполняла бы собственно складирование и засылку информации в GDD.

    ЗЫ: а лочить 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-инициализации:

    • не находит какого-то "operator new", ссылаясь на внутренности макроса gdd_NEWDEL_FUNC (определён в gddNewDel.h) в качестве претендентов.
    • О, возможно, дело в том, что отсутствует корректный конструктор-инициализатор e2t_gdd -- см. оный у fe_epics_gdd, там он присутствует и весьма замороченный.
    • 09.10.2024: ну сделал конструктор -- не помогло.
    • 09.10.2024@вечер, засыпая: а можно поместить его внутрь другого класса/структуры, единственным полем, и тогда уже этот внешний контейнер placement-инициализировать -- тогда поиск operator new будет выполняться применительно к нему, а libCAS'овские махинации "NEWDEL" влиять не будут.
    • 10.10.2024: попробовал вариант с дополнительным контейнером -- да, прокатило.

      Собственно определение класса вот такое:

      class e2t_upd
      {
       public:
          e2t_gdd d_gdd;
      };
      
      ...а объявление поля внутри MonInfo --
          e2t_upd     u;
      
      ...так что обращения к нему меняются с "mp->upd_gdd" на "mp->u.d_gdd -- по сути, замена 1 символа.
    • 12.10.2024: собственно, а что мешает назвать поле в контейнере просто "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():

    • Туда скопирован блок "записи данных в 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; потому и выполнено наследование.
    • Как тут же нашлось гуглением по "c++ operator inheritance" на "inheritance - operator= and functions that are not inherited in C++?" на StackOverflow (она же находилась ещё 17-06-2022), "The assignment operator is technically inherited; however, it is always hidden by an explicitly or implicitly defined assignment operator for the derived class".

      Правда, непонятно, почему так "скрываются" (точнее, "затеняются") не только "copy assignment operator", но и прочие "operator=".

    • Но они прекрасно заменяются на вызовы метода put():
      mp->upd_gdd.put(*(( epicsInt32*)value_p));

    12.10.2024: за вчера-сегодня доделано наполнение store_current_value_to_upd_gdd():

    • Копированием соответствующих кусков из cda_d_tango.cpp'шных 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() -- ещё не сделан, а без него ну никак.

    Глобально, нужно сделать следующее:

    • Инициализация MonInfo в момент аллокирования chunk'ов, а не при задействовании (и, соответственно, НЕуничтожение при RlsMonSlot()'е).
    • Буфер аллокировать!!!
    • Уставлять в gdd "состояние" -- результат трансляции quality2severity(), каковую ещё надо сделать; и severity ли? И как у GDD оно уставляется? setStat() и setSevr()? Вот правда, надо разобраться в этих "alarm status"/"alarm severity"...
    • 13.10.2024: case-insensitivity.
    • 13.10.2024: Уметь проверять, что монитор с запрашиваемым именем уже есть.
    • Подписка на TANGO-атрибуты при регистрации (или при появлении "интереса" -- interestRegister()?).
    • Сохранение асинхронных запросов на чтение в per-PV-очереди и отдача в момент получения ответа (глобальный пул? "забирание" per-PV-очереди в момент получения ответа и потом возврат всего куска в глобальный пул оптом?)
    • Поддержка команд в виде парных мониторов.
    • Учёт клиентов -- включая адреса, для access control'а.
    • Освобождение Mon/PV при каком-то времени неиспользования.
    • Массивы строк.

    13.10.2024: движемся.

    • Касательно "quality":
      • Нужно из 1 параметра "quality" получать ДВА значения -- status (4 варианта: NO, MINOR, MAJOR, INVALID) и severity (22 варианта).

        Поэтому функция-транслятор должна возвращать пару по указателям.

      • Функция -- 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() более НЕ вызывает деструкторы.

    • Корректная поддержка case-insensitivity: после обмышления варианта перехода на strcasecmp() и отсмотра соображений от 13-09-2024 сделано по идее оттуда: конверсия выполняется прямо в момент получения имени -- в parse_tango_name() в секции "Perform conversions", вместе с прочими махинациями с содержимым dup_name[].

      Теперь надо проверить бы.

      17.10.2024: проверил -- фиг.

      • Дело в том, что в таблицах, полученных от attribute_list_query() и command_list_query(), имена хранятся НЕ переведённые в нижний регистр, так что поиск при регистрации PV не срабатывает.
      • И поскольку подозрение от 13-09-2024, что "сломается индексация -- она же выполняется по значению указателя (а не по указываемой им строке!)" уже давно очевидно, что неверно, то...
      • Трансляция 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: некоторые соображения (весь дель общался, не закрывая рта, так что аж горло заболело, с ЕманоФедей+ВиталейБалакиным, потом с Греховым и ЧеблоПашей, времени поработать не было, только вечером дома дорвался):

    1. @вечер-дома: Аллокировать буфер -- вычитывать/"симулировать" тип/формат данных из AttributeInfo/CommandInfo
    2. @вечер, умываясь перед сномВ карте мониторов сохранять прямо "/COMMAND@c" или "/COMMAND@r", и вот по этим "@c"/"@r" разные типы мониторов будут различаться даже по карте. А место под "@T" в строках имён для командных каналов гарантированно есть -- иначе эти имена не отображались бы на командным каналы.

      ...однако облом: мы ведь НЕ храним строки (точнее, dup_name), а адресуемся по ent_idx к соответствующей NNN_info_list_p-таблице...

    15.10.2024@дома-утро: вдогонку ко вчерашнему п.2 смотрим на текущий исходник:

    • С одной стороны, CheckTangoName() в случае успешной регистрации вообще ничего не делает с полученным от parse_tango_name() значением dup_name -- даже не free()'ит.

      И его вполне можно было бы использовать для ключа в соответствующем глобальном map'е -- чтоб ключ включал и устройство, и имя атрибута/команды, и "@T".

    • С другой, ещё с 09-09-2024 в 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() теперь окружён лочкой.

    Пробуем проверить, как это всё работает.

    • Сначала пришлось пофиксить сравнение -- из-за недопродуманности реализации case-insensitivity ничего не находилось (см. выше за сегодня).
    • Потом пришлось добавить в e2t_PV::read(), до того совсем пустую, "return S_casApp_noSupport;" -- иначе по caget'у падало (причём только при -O2, а при -O0 -- нет); видимо, какой-то мусор возвращался (в EAX?), к которому libCAS пытался лезть и йок.
    • И вот после этого -- да, вроде работает. В т.ч. и множественные обращения к одному имени -- последующим выдаётся тот же монитор/PV.
    • Только 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().

    Предварительные исследования:

    • @17.10.2024, ~20:00, ИЯФ, 1П-613: в информации для attr_read() -- в классе AttrReadEvent (как и для attr_written(), в классе AttrWrittenEvent) -- НЕТ ничего, идентифицирующего запрос -- никакого ID.

      Только векторы имён (т.к. запрос мог быть на пачку) и значений argout (а в AttrWrittenEvent и значений нет).

    • Так что нет смысла (точнее, ВОЗМОЖНОСТИ) пытаться возвращать данные только запросившему чтение клиенту, а можно, как и предполагалось ранее, по КАЖДОМУ обновлению возвращать ВСЕМ.
    • @~11:30, на мойке у Мамонта, в ожидании очереди на мойку Чейзера: ...конечно, можно для каждого чтения заказывать/создавать ОТДЕЛЬНЫЙ callback-объект -- это и было бы "идентифицирующей информацией"; но вот надо ли?

      ...для чего придётся либо городить второй класс-наследник-от-Tango::CallBack, либо прямо в e2t_CB ВСЕГДА ЯВНО указывать указателем монитора-"владельца", а не химичить с offsetof().

      Тогда надо б было выделять/аллокировать сразу дуплет -- e2t_CB,casAsyncReadIO

      26.10.2024: да, именно так и сделано -- всё прошедшее время это пилил, сегодня наконец вроде допилено.

    • @вечер, дома: хотя изучение base-3.15.9/src/ca/legacy/pcas/generic/casdef.h (комментарии к 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).
    • Настораживают слова про необходимость "reference" gdd-объекта (CAS_Tutorial.pdf, стр.42):

      2. When operation is ready to complete, write current value into the gdd object passed to read().

      . . .

      In order to complete the second step for asynchronous read and write operations, the server tool must keep track of the gdd object passed to read() and write(). For read() operations, the server tool should write the requested values into the gdd object, and for write() 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 the gdd object until the operation is ready to be completed is to reference the object with a pointer, and then call reference() for the object. If reference() is not called, the object will be deleted after either read() or write() returns.

      -- главная непонятка: надо ОБЯЗАТЕЛЬНО класть в объект-prototype, переданный read()'у, или же достаточно указать свой GDD-объект, который всё равно 2-м параметром у postIOCompletion() ("and a gdd object containing the PV's value")?
    • А ещё -- у них там упоминается класс casAsyncIO как предок всех прочих casAsync*IO; но в реальности такового класса НЕ СУЩЕСТВУЕТ и конкретные классы сделаны "с нуля", БЕЗ общего предка.

      Это к вопросу об "актуальности" документации.

    • После рытья в коде стало понятно вот что:
      • Хранить ссылку на read()'ов prototypereference()'ить его) -- НЕ НУЖНО: похоже, оно "требуется", только если в момент возврата "потом" надо знать его внутреннюю структуру.

        ...а в примерах оно используется как "gdd, в которую пишутся данные", потому и сохраняется (запоминанием указателя).

        20.10.2024: допроверил по коду:
        • Собственно главный вызывальщик метода read() (хоть и косвенно, посредством какого-то "Channel") -- в src/ca/legacy/pcas/generic/casStrmClient.cc метод casStrmClient::read().
        • В качестве "prototype" он передаёт pValueRead (предаврительно "инициализировав" его свежесозданным посредством createDBRDD() "нулёвым" GDD, тут же unreference()'нутым), ...
        • ...каковой является полем в классе, объявленным как
          smartGDDPointer pValueRead;
          и, соответственно, ВСЕ махинации с ним -- только внутри этого файла.
        • И этот pValueRead является, по факту, чем-то вроде глобальной переменной в рамках экземпляра объекта casStrmClient -- разные методы класса пользуются им, на вид, не шибко понятно.

        До чего ж равиольнутые ихние исходники... :-(

      • Асинхронные объекты можно делать в фиксированных местах (например, placement-инициализацией), а не только посредством new -- авторы такой вариант предусмотрели (т.к. casAsync-объекты НИКОГДА не создаёт сама библиотека, а ВСЕГДА только программа).

        Для удаления они не делают delete, а вызывают виртуальный метод destroy(), сводящийся к "delete this;" -- при наследовании его можно переопределить на не делание ничего.

      • Но есть другая проблема: поскольку "server library" (libCAS) может поместить асинхронный объект просто в очередь, а реальную отправку клиенту выполнить "когда-нибудь потом", то НЕЛЬЗЯ сразу же после 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".

      • ЗЫ: а ещё там часть C++'ных файлов имеют расширение .cc, а часть -- .cpp, и это прямо в одной директории. Бардак!

    По результатам "исследования" -- похоже, можно просто брать и делать чтение примерно как задумано: как минимум, сделать в 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 мысли:

    1. Пора уже сюда втащить из misc_iso8601.c функции stroftime_msc() и strcurtime_msc().
    2. Надо бы ЗАУНИФИЦИРОВАТЬ epics2tango_gw.cpp'шное поле is_used и cda_d_tango.c'шное поле in_use -- чтоб в обоих файлах было одинаково и проще было между ними код копировать.

      И вообще переименовать MonInfo'вское в chtype -- как обдумывалось ещё летом (но почему-то нигде не записал).

    Так вот:

    1. Функции втащены, зависимость от CX'ного misc_iso8601.c убрана.
    2. А вот с переименованием полей -- после изучения ситуации ("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г "ангара" осенила идея:

    • ДВА пула -- один для tango-callback'ов, второй для AsyncReadIO
    • Получать двумя отдельными вызовами Get*Slot() -- вроде GetTcbSlot() и GetAioSlot();
    • и Tcb-объекты из первого пула пусть освобождаются прямо в своём методе attr_read(), а из второго пула -- при вызове ихних методов destroy().
    • Важный нюанс: работать тут надо ТОЛЬКО с указателями, БЕЗ ID.

    23.10.2024: предварительные размышления:

    • @~09:00, по дороге в ИЯФ, тропинка от П28 к НИПСу: учитывая, что в "callback'ах для чтения" нужно хранить ДВА указателя (на MonInfo и на casAsyncReadIO) идея сделать ДВА разных CB-класса -- для обычных уведомлений (1шт/MonInfo) и для attr_read() (1шт/чтение) -- становится ещё более превалирующей.
    • @09:30, сидя на лекции-занятии Олеси Радченко по FPGA/ПЛИС в терминалке: но ведь запрос чтения может отражаться на COMMAND/RESULT-канал, что заставит вызвать command_inout_asynch(), что, в свою очередь, по исполнению приведёт к вызову cmd_ended().

      Т.е., "callback'и для чтения" должны иметь этот метод иным.

      Что ставит точку в вопросе "один callback-класс или два" -- явно надо делать ДВА РАЗНЫХ.

    • Хотя тут есть предмет для обсуждения в вопросе "а позволять ли метод read() для командных каналов вообще?":
      • В принципе, можно вообще запретить исполнение таких чтений, сразу возвращая S_casApp_noSupport, ибо несколько бессмысленно пытаться "читать команды" -- их можно вызывать лишь "записью" входных параметров.
      • С другой, для команд вроде State и Status требовать какой-то записи глупо -- они по смыслу именно чисто для чтения.
      • Так что лучше всё-таки позволить.
      • @11:10, 1П-613: а с третьей стороны -- в epics2tango_gw.cpp ведь есть преимущество перед cda_d_tango.cpp: мы ЗНАЕМ требуемый тип входного параметра.

        Соответственно, можно разрешать чтение, если входной тип -- DEV_VOID и запрещать в остальных случаях.

    • Об именах:
      1. Видимо, "обычный" callback, которых 1шт/MonInfo -- e2t_rglr_CB, а для чтения -- e2t_read_CB.
      2. А пулы -- Rcb (Read CallBack) и Rio (Read IO).
    • @11:00, 1П-613: возможна ситуация, когда отправлен запрос на чтение или команду, но потом PV удаляется (клиент закрыл соединение или просто закрыл PV) -- окажется, что отправлены запросы на "уже несуществующий монитор".

      Похоже, надо вести счётчик отправленных запросов, чтобы при >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:

    • СДЕЛАНА ПАРА "СУЩНОСТЕЙ"-пулов -- rio и rcb; код второго является дублем кода первого.
    • И да, оно всё на указателях. Так что не перевести ли на указатели (с ID) и все прочие? ...хотя DPX не получится -- там на E2K_128 именно ID должны пересылаться.
    • И методы e2t_read_CB::cmd_ended() и attr_read() сделаны. Они по-другому получают указатель mprio_p) -- из this, сразу после чего исполняется RlsRcbSlot(this).
    • 26.10.2024: а e2t_rglr_CB::attr_read() удалён, т.к. вызов read_attribute_asynch() теперь всегда делается с указанием на read_CB-объект (а не rglr_CB) в качестве получателя.
    • Но главная дурь -- в libCAS очень плохо продумано/определено, что/как делать, если операция асинхронного чтения завершилась с ошибкой:
      1. Фиг поймёшь, какие коды передавать при ошибке.
      2. Даже при status!=S_casApp_success ВСЁ РАВНО нужно передавать какую-нибудь GDD, т.к. она присутствует 2-м параметром в casAsyncReadIO.postIOCompletion() и является ССЫЛКОЙ (gdd & valueRead), а не указателем ("gdd *valueRead_p").
      В CAS_Reference.pdf на стр.35 есть пассаж
      If the operation is successfully completed, the status code S_casApp_success should be passed as the first argument to postIOCompletion(), and a gdd 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. A gdd object must be passed as the second argument, but this object will be ignored if the status code is not S_casApp_success.
      (bold мой) -- и это всё "объяснение".
    • Так что в обоих CB-методах при ошибке делается
      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; ...
    • ...вот только какой код "ОШИБКА" надо возвращать в случае, когда AsyncReadIO-объект создан (т.е. взвелось userStartedAsyncIO), но до "return S_casApp_asyncCompletion" дойти не успело из-за ошибки в процессе запроса чтения -- непонятно.
    • Поэтому используется тот же код S_casApp_canceledAsyncIO в надежде, что это сработает правильно (по исходникам libCAS -- опухнешь понимать, так ли).
    • Если НЕ так -- то даже и непонятно, как.

      Единственный приходящий в голову вариант -- лочить mutex'ом:

      1. В запросе -- e2t_PV::read():
        1. Сначала, ПЕРЕД инициированием запроса TANGO-чтения, лочить mutex.
        2. Потом запрашивать оное чтение, и если оно успешно (не вызвало exception), то...
        3. ...создавать AsyncReadIO-объект и прописывать указатель на него в rcb_p->my_rio_p -- в read_CB-объект.
        4. Разлочивать mutex.
        5. Возвращать S_casApp_asyncCompletion.
      2. В callback'е -- e2t_read_CB::attr_read():
        • лочить вокруг складирования в upd_gdd, ...
        • ...чтобы к его окончанию -- после которого надо сделать rio_p->postIOCompletion(S_casApp_success,...), этот rio_p был бы уже заполнен.

      Обсуждение:

      • Но это просто напрашивается на разнообразные deadlock'и и прочие race condition'ы.
      • Например, вот ПРЯМО СЕЙЧАС в 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) выполнять уже ПОЗЖЕ, ВНУТРИ критической секции, окружённой локом.

      • ...а, кстати, у нас прямо УЖЕ (изначально, унаследованно от cda_d_tango.cpp?) лочка mp->val_mutex -- ровно в нужных местах:
        1. и в e2t_PV::read() -- вокруг запроса чтения,
        2. и в 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 отображается пустым; ...
    • ...сообщения -- 1шт "Read operation timed out: some PV data was not read." плюс на все 3 имени по "CA.Client.Exception" "Virtual circuit disconnect".
    • Параллельно запущенному на эти же 3 имени camonitor'у:
      • Вся троица прилетает по 2-3 раза, ...
      • ...причём из них 2 раза -- с идентичными (между ответами на один канал) timestamp'ами.
      • И иногда ВСЯ ТРОИЦА прилетает при caget modulatorPS/test/1/Status -- т.е., при запросе ОДНОГО канала.

    Видимо, надо напичкивать диагностикой и разбираться. (Оно там между разными экземплярами Async-объектов не путается? А вчерасозданные пулы работают корректно?)

    ...заодно:

    1. Напихать диагностики во всякие destroy() etc., чтобы убедиться, что цикл существования AsyncReadIO-объектов таков, как написано в "документации" и в расчёте на который я всё делал.
    2. В read() попробовать sleep() в разных местах, чтобы посмотреть, как оно себя будет вести:
      • при ДО отправки -- чтобы успеть прислать ещё запросы на ту же PV от других клиентов (если всё, как я думаю, то придёт в других thread'ах);
      • при ПОСЛЕ отправки, но ДО возврата S_casApp_asyncCompletion -- чтобы увидеть, как libCAS справится с ситуацией "пришёл ответ на PV, чей read() ещё не успел завершиться".

    27.10.2024: напичкиваем диагностической печати...

    • Предположение, что для разных клиентов libCAS'ом будут создаваться РАЗНЫЕ thread'ы -- не оправдалось: запросы и от сначала запущенного camonitor, и от позже caget обрабатываются ОДНИМ tid'ом.

      И pvExistTest(), и PV::read(), и последующие destroy() -- всё в одном.

    • А вот ответы от Tango -- CB::attr_read() -- прилетают в ДРУГОЙ thread.

      ...и как там что между ними передаётся/синхронизируется -- не вполне ясно.

    • И да, asyncReadIO-объекты освобождаются совсем не сразу: сначала отображается 3 штуки 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 секунд) всегда получал данные без ругательств.

    • Ещё была версия "а вдруг я как-то не так обращаюсь с жизненным циклом PV-объектов: например, их удаляют, а я не замечаю и продолжаю работать с зомби".

      Посмотрел -- нет, всё делается корректно: ещё в 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(), что точно нехорошо -- надо переносить.)

    • ЗЫ: а ещё возникла версия, что проблемы могут быть связано с multithreading'ом -- вдруг я как-то неправильно делаю.

      Однако в 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 -- которых нигде нет.

    • @вечер-засыпая: а не вызваны ли проблемы/странности именно задержками? А задержки, в свою очередь, тем, что обновление приходит в другой thread и основной -- сидящий в 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.
    • Затем копируем из epics2smth.cpp e2s_pipe_fdReg, называя его e2t_update_pipe_fdReg
    • Запись 1 байта добавлена в 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 в аналогичной ситуации проверить -- не тукнется ли так же.

    • И даже просто прогон разных клиентов несколько раз быстро привёл к зависанию шлюза на каком-то futex'е -- похоже на deadlock, но не мой! -- backtrace GDB:
      #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 проблемы:

    1. libCAS'у не нравится "тип" возвращаемого gdd -- "assert(this->prim_type==aitEnumContainer)"; как добиться -- одним из:
      1. cdaclient modulatorPS/test/1/State
      2. caget -d DBR_CTRL_LONG modulatorPS/test/1/State
      (Проблема НЕ возникает с "modulatorPS/test/1/Status", т.к. dbr_ctrl_string не существует и потому для строковых каналов cda_d_epics.c свойства посредством DBR_CTRL_STRING не запрашивет.)
    2. libCAS зависает где-то глубоко внутри себя на futex'е. Сценарий достижения ситуации:
      • "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: анализ зависания, что происходит в разных обстоятельствах:

    • Похоже, косяк в нескольких клиентах И ОДНОВРЕМЕННО в нескольких PV: если caget'ом просить любые 2 из того списка вместо 3 -- не зависает, как и если любые 2 просить camonitor'ом, а caget'ом всё равно 3...
    • Но, что любопытно: после убирания "вызова 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 (точно НЕ на контейнер!).

    • Кстати, заглянул сейчас в test_libcas.cpp, которым пытался проверять глюк с 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 */
        
      • 08.11.2024: проверено через cxsd_fe_epics.so и через epics2cda c числовыми каналами -- да, обламываются и DBR_STS_LONG, и DBR_GR_LONG, с одинаковым результатом "Channel read request failed", "read failed" и "some PV data was not read".
    • ЗЫ: есть некоторые вопросы к корректности оформления подписки в cda_d_epics.c::StateChangeCB():

      12.11.2024: да, там был косячный код, который сегодня переделан.

      15.11.2024: протестировано и подтверждено, что старый код работать не мог, а новый работает.

    • И отдельный методологический вопрос: ведь ca_create_subscription() вроде как позволяет указывать конкретный DBR_-тип, и что мешает прямо в подписке указать хоть DBR_CTRL_nnn, хоть DBR_GR_nnn; но:
      • Ни у camonitor не видно ключиков, которые позволили бы указать конкретный тип/вид/формат запроса.
      • Ни у libCAS в методе interestRegister() нет параметров и не видно ничего, что бы указывало на тип

      Поразбираться бы в вопросе:

      1. глянуть код camonitor'а, как он заказывает;
      2. посмотреть, что в этом вопросе делается у libCAS;
      3. и вообще попробовать прямо в cda_d_epics.c поиграться с разными типами/видами/классами запросов -- как на них будут реагировать обычный EPICS'ный IOC и как libCAS.

    08.11.2024: проверяем на тему "ca_create_subscription() со странными типами":

    1. да, camonitor.c::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'ами далее чуть больше приседаний, но всё равно смысл тот же).
    2. Что делается в libCAS -- опухнешь разбираться, с тамошним-то кодом.

      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/.

    3. 10.11.2024: "поигрался" с разными типами в cda_d_epics.c::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" и на "втором" чтении тоже.
      • А cda_d_epics.c::NewDataCB() в диагностике выдаёт "ARGS.status=152 ARGS.type=27" (27 -- это DBR_GR_DOUBLE, а 152 -- похоже, ECA_GETFAIL).
      • Но вот ДАЛЕЕ идут обновления по цепочке cxsd:11->cda->epics2cda->libCAS->..., и там 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: вроде проблема решена.

  • 20.04.2023: epics2tango и прочим "gw" весьма вероятно понадобится возможность указывать "таблицу трансляции" -- например, для преобразования из имён, желаемых EPICS'ом (например, тот "канонический список PV", что написан для guntimer'а) в реальные имена, имеющиеся в Tango-сервере.

    Таковая таблица может быть тривиальной -- просто список {alias,реальное_имя}, и храниться в текстовом файле построчно.

    Но для ИСПОЛЬЗОВАНИЯ потребуется некоторая обвязка -- чтоб считанная из файла таблица хранилась в удободоступном виде. Напрашивается отдельный модулёчек, годный для использования любой из таких программ. Назовём его "alias_list". Набор закладываемых в него идей:

    • Есть "объект" типа alias_list_t, который создаётся, потом при чтении в него добавляются пары {key,value}, а при использовании можно спросить "есть ли у тебя в словаре строка с таким-то key" (в роли которого выступает приходящее pvExistTest()/pvAttach() значение pPVAliasName), а он в ответ вернёт значение-target alias'а, которое следует использовать взамен спрошенного.
    • Список alias'ов растёт обычным образом, по принципу realloc(), но не на GrowBuf(), а с собственной реализацией.
    • Для хранения пар используется alias_list_item_t, содержащий пару указателей, но аллокируется на обе строки ОДИН буфер, в который они кладутся друг за дружкой,так что при удалении всего делается лишь free(->key).
    • В идеале нужно будет иметь список сортированным, для быстрого двоичного поиска, и производить "добавление вставкой".

      Но в начальной реализации на это можно забить и сделать по-простому, линейно.

    • В разных применениях могут быть разные требования на case-sensitivity: где-то sensitive (имена EPICS-PV), а где-то insensitive (CX и Tnago).

      Поэтому при создании объекта 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" сделать.

  • 22.08.2024: свой собственный раздельчик для знаний о том, как в C++ положено реализовывать хэш-таблицы -- это std::map (а НЕ std::hash!).

    Т.е., если держать в хэш-таблице целочисленные ID ячеек, то объявлять её надо как

    std::map<char*,int> dpx_pool_map;

    ...ну или хранить в карте сразу дуплеты {ID,pointer}.

    22.08.2024: ну собственно "знания":

    1. Описание "std::map" на cppreference.com.
    2. Главный косяк -- как проверять наличие такого-то элемента: формально есть метод 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".

    Ещё пара нюансов:

    1. Кроме std::map есть ещё std::unordered_map, который может быть быстрее, т.к. не требуется упорядочивание элементов.

      Т.е., объявлять

      std::unordered_map<char*,int> dpx_pool_map;
      В остальном же оно вроде совместимо, включая косяк с "contains только C++20".
    2. Нашлась тема "How to find if a given key exists in a std::map" на StackOverflow от 21-12-2009, где советуют 2 вещи:
      1. Use map::find and map::end:
        if (m.find("f") == m.end()) {
          // not found
        } else {
          // found
        }
        
      2. To check if a particular key in the map exists, use the count member function in one of the following ways:
        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* как "сравнивай их значения (а при создании -- аллокируй строки)".
    • Но нет: гугление по "c++ map const char*" дало
      • "Using char* as a key in std::map" на StackOverflow за 11-11-2010, и там в ответе сказано вот что:
        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-вариант -- заменим в #if0 на соответствующее условие.

  • 18.09.2024@сидя на занятии по ПЛИС в терминалке, ~10:00: на перспективу -- надо бы поддерживать EPICS'ный подход/синтаксис, где если указывается просто имя рЕкорда, то де-факто берётся его поле .VAL, которое также можно указать явно, а можно указывать другие поля, вроде разрешённых минимума/максимума, метки, единиц измерения и прочих EGU.

    Учитывая, что в TANGO многие их таких свойств/сущностей тоже есть (в AttributeInfo, а точнее -- в его классе-предке DeviceAttributeConfig), то теоретически можно бы обращение к ним поддерживать:

    • например, при запросе "DOMAIN/FAMILY/DEVICE/ATTRIBUTE.VAL" оное ".VAL" просто игнорировать, ...
    • ...а при запросе "DOMAIN/FAMILY/DEVICE/ATTRIBUTE.DESC" отдавать значение AttributeInfo.description.

    Тут пара замечаний.

    1. Во-первых, значительная часть полей DeviceAttributeConfig -- string'и, в то время как по смыслу они ЧИСЛА (всякие min_value/max_value).

      Ну да, тут надо будет выполнять strtod() или strtol().

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

      ...кстати, нечто подобное задумано (но пока НЕ реализовано) в 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, куда такое совсем не к месту.
    • Но "к месту" выглядит сделать туда распознавание именно местоположения компонента ".ЧТО_ТО": там ведь (после замены '.' на '/') выполняется подсчёт количества '/' и запоминание позиции конкретно 3-го в sl3; ...
    • ...вот и сделать там разпознавание конкретно 4-го '/' и возврат его позиции вызывающему (НЕ прописывая туда '\0'!), ...
    • ...а уж вызывающий -- сейчас это CheckTangoName() -- пусть решает, как поступить с этой информацией.

    18.09.2024@дома: делаем минимальную подготовку в parse_tango_name():

    • Добавлен параметр "char **dot4_p" для возврата в нём указателя.

      Чуть позже: учитывая, что к тому моменту там (после замены!) будет уже не '.', а '/', название выглядит misleading; но уж ладно.

    • Указатель sl4 для сохранения той самой позиции;
    • при парсинге/подсчёте он инициализируется =NULL, а при nsls==4 в него запоминается оная позиция.
    • И в конце это значение (причём обычно =NULL) возвращается вызывающему посредством "*dot4_p=sl4".
    • А условие "считать ошибкой nsls!=3" расширено вариантом "и nsls!=4".
  • 24.12.2024: ужасающая мысль: а ведь метод subscribe_event() скорее всего является блокирующимся. Тогда получается, что если сначала к устройству успешно приконнекчено, пото связь рвётся и тут прилетает запрос на ещё один атрибут, то шлюз зависнет. И избежать этого нет никакой возможности: ведь участвующий в этом экземпляр DeviceProxy уже используется в основном потоке и подписку с его применением ну никак нельзя утаскивать в отдельный поток-"подписыватель".

    (Мысль пришла при написании куска 20241105-WHY-I-HATE-TANGO.txt -- при составлении списков операций, которые могут быть асинхронны, а которые НЕ могут; и тут сообразил, что подписка, скорее всего, тоже синхронна...)

    31.12.2024: немножко обсуждения на тему "как проверить, действительно ли проблема есть".

    • Для начала -- глянуть код. Хотя там, скорее всего, мало что понятно.
    • Поставить эксперимент прямо с нынешним cda_d_tango: перед вызовом subscribe_event() добавить sleep(10), в течение которого выдернуть Ethernet-кабель и посмотреть (диагностической печати напихать) "диаграмму исполнения" -- будет ли подвисать на 3*N секунд (тогда грустно) или вызов завершится сразу же (тогда удивительно хорошо).
:
:
:
Drivers:
  • 10.09.2015: поскольку всё касательно драйверов сейчас сделано в hw4cx/, то директория work/drivers/ (и её тень work/_drivers) более не требуется и уходит в ARCHIVE/work/drivers.20141117/.
programs/drivers/:
  • 12.06.2015: trig_read_drv.c создан.

    17.07.2015: проверен -- а вроде работает, СРАЗУ же.

  • 22.09.2015: еще один -- mux_write_drv.c, раздаёт приходящее ему на запись значение по указанным в auxinfo адресам.

    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.

    • Во-вторых, поскольку писать произвольные данные в INT32-скаляр нельзя, то надо и ссылки на целевые каналы регистрировать "соответствующего" типа, в качестве которого очевидным образом брать тип своего канала.

      Что и сделано, с добычей типа посредством 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.

    Возможные решения:

    1. Отключить RD-конверсию при регистрации целевых каналов.
    2. Наоборот, отдавать "наверх" коэффициенты целевого канала.

      02.04.2024: но это не прокатит по той же причине, что и п.3.

    3. Навесить на конкретный канал mux_out_tab_times.0 "обратный" коэффициент, чтобы происходил лишний "нейтрализующий" пересчёт.

      Но это очевидным образом не поможет, т.к.

      1. В weldproc_drv.c канал SODC_TAB_TIMES регистрируется БЕЗ флажка VDEV_DO_RD_CONV, так что дополнительный коэффициент не будет иметь эффекта.
      2. ...к тому же канал не вещественный, а INT32, так что при таком пересчёте "туда-и-обратно", выполняемом не одной цепочкой, а разделённым и с передачей результата через INT32, всё равно данные будут безвозвратно портиться.

    02.04.2024: продолжаем.

    @лабораторный круглый стол, ~10:00: а можно использовать гибридное решение: и RD-конверсию отключить, и коэффициенты целевого канала наверх отдавать.

    (Вопрос, конечно, от КАКОГО из целевых каналов отдавать коэффициенты... От ВСЕХ?)

    Вообще-то прочие всякие именно так и поступают: и сами работают без конверсии (чтобы передавать данные один-в-один), и коэффициенты целевых каналов отдают (чтобы "человеческим" клиентам -- cdaclient/скринам -- отдавать данные).

    Ну делаем:

    • Для начала -- добавляем CDA_DATAREF_OPT_NO_RD_CONV в регистрацию каналов.
    • ...а вот туннелирование RD-коэффициентов наверх пока делать не стал.

    12.04.2024: проверяем: да, NO_RD_CONV помогает.

    Сначала никак не мог понять -- но КАК была устроена неработающесть при включенной {R,D}-конверсии, почему в итоге приходило значение 0?! Потом допёрло (и экспериментально проверил): если отправить число "123", то в результате получится "123000000" -- т.к. при отправке УМНОЖИТСЯ на миллион. И когда отправлялось значение 5000000 -- 5 секунд микросекундами -- то после домножения ещё на миллион становилось пять МИЛЛИАРДОВ, т.е., переполнение, так что в результате и получался ноль.

    13.04.2024: на ulan-ude-els и tower обновлённый драйвер роздан, будем смотреть.

    16.04.2024: проверил -- да, работает как надо, проблема ушла.

  • 23.09.2015: (по результатам изготовления поддержки для веремеенковского "сдвоенного ИСТа/V1000"):
    • пришлось в linmag.subsys городить умножения для каналов статусов,
    • а для разделения канала уставки по двум ИСТам -- double_iset_drv.c, все мозги которого заключаются в простой арифметике, но для её выполнения (ведь нужна связь с другими каналами!) потребовались полторы сотни строк сопутствующего кода.
    • И если еще подобная потребность возникнет -- придётся опять ваять аналогичный драйвер, опять содержащий море обвязки и минимум реального "мяса".

    Мысль: а может сделать специальный драйвер "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 всё требуемое.

    • В VMT формул -- cda_fla_p_rec_t -- добавлены методы add_evproc() и del_evproc().
    • Их реализация в cda_f_fla.c -- проходить по всей формуле и регистрировать/дерегистрировать указанный {evmask,evproc,privptr2} всем 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 именно таков).

    • Замечание: Cdr'овский cvt2ref() для SRC_IS_FLA всегда передаёт evproc=NULL.

    Теперь надо делать собственно formula_drv.c и на нём проверять.

    19.12.2015: да, он сделан и на нём проверено -- работает. Обсуждение:

    1. О реализации evproc'ев для формул:
      • Резюме: сделано халтурновато, но работать будет.
      • "Халтурновато" потому, что семантика отличается от оной для простых каналов.

        Правильно было бы обычным образом регистрировать обработчик в ref'овом cb_list, после чего у формулы регистрировать ссылку на какой-нибудь внутренний обработчик (cda_core'овский или cda_f_fla'шный), который бы уже вызывл зарегистрированные в cb_list обработчики с ref=ИДЕНТИФИКАТОР_ФОРМУЛЫ.

      • А "работать будет" потому, что при evproc вешается не на формулу, а на подстилающие каналы, и при _del_dataref_evproc(ref_формулы) так же удалится. Но тут "минус на минус даёт плюс" -- всё будет пахать при аккуратном использовании.
      • ...по-хорошему, рано или поздно ПРИДЁТСЯ сделать "как надо" (хотя как это реализовать -- пока не очень ясно).
    2. О конкретно formula_drv.c:
      • Формулы указываются auxinfo-параметрами r и w.
      • Каналу ставится тип IS_AUTOUPDATED_YES, чтоб он обновлялся просто по событиям обновления подстилающих каналов.
      • Есть неясность с функционированием write-формул: если указана w-формула, то после записи сервер будет ждать ответа и более запроса на запись не пришлёт. А тогда всё может зависнуть...

        Поэтому принят следующий подход:

        1. Если r-формула отсутствует, то драйвером сразу выполняется возврат значения 0.0 -- тем самым "formula" считается более гибким (и вещественным) вариантом mux_write.
        2. Если же r-формула указана, то она должна быть связана с w-формулой таким образом, чтобы запись вызывала обновление чтения (например, r="chan1+chan2", а w="chan1=val/2;chan2=val/2").

    20.04.2017: попробовал. Как бы работает, но всё несколько неоднозначно.

    1. В качестве "замены" double_iset худо-бедно подойдёт.

      Но есть проблема: при раздаче числа-суммы в 2 канала происходит ДВА побновления: сначала возвращается сумма по изменению 1-го канала, а потом 2-го. Так, если предыдущая сумма была 500 (250+250), а потом записывается 100 (50+50), то сначала "покажется" значение 300 (50+250), и только потом 100.

      Впрочем, для целей конкретно double_iset это вполне нормально -- ну уж как успели устройства отработать, так оно и есть. И, собственно -- вполне в общем стиле работ "формул, зависящих от нескольких каналов" вообще.

      Главная проблема в другом -- см. далее.

    2. Из-за того (видимо!), что канал помечен как IS_AUTOUPDATED_YES и запросы на чтение игнорируются, есть дурацкий зависон при тестировании cdaclient'ом.

      Суть в том, что если f-канал никогда не был прочитан (а он изначально НИКОГДА не будет прочитан, т.к. обновляется только по обновлению каналов, входящих в r-формулу), то cdaclient никогда не сделает запись, ибо будет сначала ждать поступления значения (как сигнала об установлении связи).

      Исправить ситуацию можно, лишь записав что-то в один из подстилающих каналов.

    21.04.2017: очевидно, что причина -- лишь в игнорировании чтения (в результате начальное вычитывание не срабатывает), а AUTOUPDATED'нутность ни при чём -- на каналы записи она не влияет вовсе, что хорошо видно в cxsd_hw.c::ConsiderRequest().

    • Отдельный вопрос: а ПОЧЕМУ изначально не получаем обновлений? Ведь оба "подстилающих" канала -- каналы записи, и при регистрации с них должны "капать" UPDATE, а не CURVAL.
    • Попытка разобраться: в r-формуле с insrv:: переделано на cx:: (localhost:2). ...и значение стало возвращаться!!!

      Очевидно, надо сравнить условия возврата (когда 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: нужен ещё один драйвер -- trig_exec_drv. Чтобы выполнять "по триггеру" некую запись. Причём запись хитрую -- вычисленного (в данном случае -- на основе триггера) значения, в несколько разных каналов.

    11.10.2018: потребность:

    • Надобность возникла в сервере rfsyn -- canhw:19.
    • Нужно при изменении значения одного канала (ic.linRF.frq.set, что является {R,D}-cpoint'ом на аппаратный канал slio.word24) записывать вычисленные из этой частоты значения периодов в каналы f_in_ns всех D16.

      ...также нужно для fastadc-клиентов, работающих в других серверах, уметь прописывать значение для канала "xs per point" аналогичным образом. Но это будет делаться уже в ТЕХ серверах, и все fastadc-клиенты будут натравливаться на один (per-server) общий канал.

    • Т.е., что-то слегка похожее на v2'шный "trig_write", но только записывать нужно не константы, а именно результат вычислений.

    Соображения:

    • Очевидно, что вариант с "триггером" будет работать хорошо: событие UPDATE прилетит не только при изменении значения канала, но также и при старте, в момент коннекта -- тем самым вызвав прописывание надлежащих значений в "каналы-паству" прямо при старте.
    • Т.е., следим за каналом, да?
    • Что должно работать "реакцией" на триггер -- вопросов нет: формула.

    Сделан, trig_exec_drv.c, буквально за полчаса-час. Детали:

    • Сделан на основе formula_drv.c.
    • Триггером работает не канал, а ФОРМУЛА -- это полезно для случаев, когда записывать требуется значение, вычисленное на основе нескольких каналов.
    • Синтаксис в auxinfo -- t=ФОРМУЛА_ТРИГГЕР e=ФОРМУЛА_ЗАПИСЫВАТЕЛЬ.
    • Значение триггера не игнорируется (как можно бы), а передаётся входным параметром исполняемой формуле.

      Вместе с предыдущим пунктом это позволяет решать задачу элегантно, разделив её на части:

      1. Собственно вычисление выполняется в формуле-триггере. При этом автоматически делается "подписка на обновление" всех каналов-источников.
      2. Формула-записыватель содержит только запись.
    • Особенностью этого драйвера является то, что он БЕЗКАНАЛЬНЫЙ. Он лишь выполняет свою работу, никак не взаимодействуя с клиентами; так что у него в качестве списка каналов проставляется "-", а _rw_p() у него отсутствует.

    Протестируем в процессе заюзывания.

    31.10.2018: некоторые соображения по использованию (родились не сегодня, а на днях, но надо б записать):

    • Триггеруемый вычислитель должен ТОЛЬКО считать, но НЕ записывать в юзерские поля (типа осциллографовых каналов ext_xs_per_point_src), которых может быть море.

      А именно КЛИЕНТЫ должны мониторировать интересующее их.

    • Соответственно, получается иерархия из нескольких уровней: в canhw:19 -- лишь сами расчёты, складирующие результаты в местные noop-rw-каналы, а следующий уровень (возможно, в "клиентских" серверах) -- мултиплексирование по устройствам/каналам-клиентам.
    • Можно в формулу-триггер добавить дополнительный холостой канал (хостимый в noop'е), весь смысл которого -- при записи в него форсить исполнение записывающей формулы.
    • Если нужно что-то записывать по изменениям (триггера) в "каналы-клиенты" УСТРОЙСТВ: сами эти устройства могут рестартовать (например, при перезагрузке крейта). Очевидно, в таких случаях нужно запись ПОВТОРЯТЬ.

      Следствие: надо в "клиентских" ветках, выполняющих запись, в триггеры включать зависимости от каналов _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":

    • Значение триггера: IS_AUTOUPDATED_TRUSTED, отдаётся в момент триггирования.
    • Дополнительный триггер: чтоб запись в него значения CX_VALUE_COMMAND вызывала бы срабатывание.

    По здравому размышлению первый выглядит потенциально полезным, а вот второй -- очень сомнительным (ибо ломает схему работы).

    01.11.2018@вечер-дома: делаем первый вариант devlist'а с trig_exec'ами -- добавляем в devlist-canhw-19.lst:

    1. Для хранения вычисленных частот и периодов создан devtype syn_data, содержащий {F,T}{ie4,d16,osc}, и экземпляр его syn_info (syn_data/noop).
    2. Вычислением занимается "устройство" syn_info_calculator типа trig_exec, где триггером является ic.linRF.frq.set.
    3. Раздачу в "локальные" D16 выполняет еще одно trig_exec-"устройство" -- d16s_fin_writer, содержащее в своём триггере каналы _devstate всех своих клиентов.

      Там определен список D16S_LIST, который используется foreach()'ем и в формуле-триггере, и в формуле-исполнителе.

    4. Аналогично передачу в IE4 выполняет "устройство" ie4_fin_writer (только там всего 1 реципиент и никаких циклов).

    02.11.2018: проверяем.

    • Сначала, при тестировании "кусочка" devlist'а, в котором отсутствовал сам триггерный канал ic.linRF.frq.set обнаружилась странность: при формула НЕ генерила ошибку, несмотря на то, что ссылалась на несуществующий канал.

      Проблема оказалась в cda_f_fla_p_create(), где отсутствовала проверка результата регистрации канала. Исправлено.

    • Затем наткнулся на еще один косячок: НЕЛЬЗЯ в формуле, посчитав значение для какого-то канала, записать в этот канал, а потом для дальнейших вычислений делать getchan из этого канала.

      Дело в том, что запись туда уходит, и, в случае insrv::, даже мгновенно отрабатывается, но вот возврат записанного значения обратно к cda идёт по insrv-дескриптору и отработается ПОЗЖЕ, а пока что у cda есть лишь старое значение.

      Так что -- нужно делать локальную копию; например, через %-каналы.

    • После исправления двух вышеописанных ляпов -- работает!!!
  • 19.12.2018: нужен ещё один драйвер -- "общего назначения" preset_selector_drv: чтоб можно было для некоего набора каналов заранее запрограммировать несколько наборов значений, и потом "одним прыжком" активировать нужный набор.
    • Смысл -- оно ЕманоФеде нужно для быстрого переключения между режимами {e-,e+}{ВЭПП-4,ВЭПП-2000}.
    • По-хорошему-то это должно делаться в софте на стороне клиента, но тут хочется максимально уменьшить время переключения.
    • Для каналов Д16 этот функционал засунут прямо в frolod_d16_drv.c -- для устранения лишней задержки при обмене с CM5307 по сети (а это, учитывая тормознутость его 50MHz PowerPC 852, весьма серьёзные времена).
    • Но для прочего-то железа, которому требуется так же щёлкать пресетами -- такой финт не пройдёт. Например, драйвер CAC208, на котором висит управление кикерами, уродовать не будем.
    • Вот и нужно сделать "общий" драйвер, которому указывался бы набор целевых каналов.

    19.12.2018@вечер-лыжи: основная архитектура продумана; для целостности изложения цепочки мыслей её описание находится в том же разделе "Параметризованные запросы" за сегодня.

    21.12.2018: приступаем к реализации.

    • Скелет.
    • Идеология выбрана такая:
      • Под каждый элемент пресета резервируется ячейка CxAnyVal_t, плюс хранится его dtype.
      • Место под значения и dtype'ы аллокируется единым блоком, размещаемым в КОНЦЕ privrec'а. Сначала значения (они ж выравниваются по 16 байтам), а потом типы (всего по 1 байту).
      • Естественно, место под privrec аллокируется самостоятельно -- аналогично remdrv_drv.
      • А вот под dataref'ы выделяется 100 ячеек прямо в privrec'а, НЕ динамически аллокируется.

        Причина -- чтоб регистрировать каналы сразу при парсинге. Иначе пришлось бы парсинг выполнять в 2 стадии (проход #0: собираем список; проход #1: регистрируем); так тоже можно, но особого смысла не видно.

    • Часть _init_d(), заведующая аллокированием.

      Пока БЕЗ парсинга и регистрации.

    • _rw_p() -- полностью. Включая:
      1. Менеджмент значений: переданное складируется "как есть", вместе с dtype.
      2. Активация пресета при записи его номера в канал 0: эти "как есть" отпарвляются в целевые каналы.

    27.12.2018: с 25-го пилил оставшееся (в первую очередь парсинг), и по результатам приёдено к выводу: необходимо изменить модель данных: аллокировать не вместе с privrec'ом, в его конце, а отдельным буфером.

    • Причина -- в том, что (в отличие от remdrv!) тут идёт работа с cda, которой нужно указывать свой devptr, а в момент регистрации контекста privrec еще не аллокирован и указывать нечего.
    • Но нам НЕОБХОДИМО получать события от cda -- в первую очередь для туннелирования {R,D}.
    • Вот и получается, что стоит плюнуть на "аллокирование одним куском" и privrec отдать в ведение сервера, а буфер аллокировать/освобождать самостоятельно.

    Переделываем...

    ...через час: готово.

    Далее:

    • Собственно парсинг сделан -- и параметры defpfx= с base=, и имён каналов.
    • Но имена каналов берутся "как есть" -- БЕЗ парсинга опциональных префиксов "@t[NNN]", а всегда считается за "@i1" (INT32, n_items=1).
    • И в хранимых dtypes[] сначала форсится CXDTYPE_INT32 (чтоб при начальном вычитывании серверу не отдавалось UNKNOWN, с которым ему неясно, что делать).
    • И "туннелирование" {R,D} реализовано:
      1. Оно делается по событию RDSCHG,

        ...но там стоит проверка "а работаем ли мы уже" (по data_buf!=NULL), т.к. insrv::-каналам событие будет отдаваться прямо при регистрации, еще ДО завершения cda_add_chan()'а, а в этот момент ещё даже количество каналов неизвестно (и, соответственно, их номера) -- так что рано передавать данные.

      2. И для компенсации этого "не передаём" в конце _init_d() делается принудительная отдача всех {R,D}.
    • Собственно отдачу для n'го канала по количеству пресетов выполняет ReturnOutRDs().

    Блок парсинга даже уже сейчас выглядит просто ужасно.

    Теперь тестировать надо.

    Парой часов позже: потестировал, после исправления мелкого ляпа (почти опечатки) в парсинге -- работает! И калибровки туннелирует.

    28.12.2018: "внедрено" в конфиг rfsyn'а, поставлено на пульт -- ждём проверки.

    29.12.2018: проверено на живой установке -- работает. Ура!

    Засим считаем за "done", но в будущем может понадобиться добавить фич:

    • Возможность указывать типы и даже n_items (а при неуказанности -- взятие из БД).
    • Возможность зачесть текущие значения в аппаратуре за указанный пресет.

      Т.е., в дополнение к каналу "selector" (который "activate_preset_n") еще канал "read_preset_n".

    08.02.2025: вчера вечером пришла в голову идея "а давай сделаем возможность вытянуть из аппаратуры текущие данные в пресет" -- независимо от записанного 29-12-2018, просто ЕманоФедя упоминал, что это было бы небесполезно. Ну сегодня и сделал.

    • Идея в том, что у cda имеются текущие данные значений каналов. Вот у неё и спросить.
    • Так и сделано. Вычитывается тривиальной 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-списочек для отдельных драйверов -- делаем.

bridge_drv:
  • 16.02.2020: заводим раздел для давно (месяц! :D) напрашивавшегося драйвера.
  • 16.02.2020@дома-воскресенье: пора приступать к его реализации. Благо, понимание того, как именно он должен выглядеть, появилось давно.

    16.02.2020@дома-воскресенье: делаем:

    • Первоначальным планом было взять за основу mqtt_mapping_drv.c и чуток его подпеределать.

      Но затем оказалось, что ТУТ всё намного проще. В частности,

      1. НЕ нужно знать имена своих каналов. 17.02.2021: а вот не факт -- полезно бы, чтоб их использовать как "внешние" при мирроринге.
      2. НЕ нужно хранить имена "внешних" каналов, поскольку они используются только при регистрации в cda.

        Аналогично и dtype с max_nelems.

      3. Как следствие -- "карта" map[] очень простая, это просто массив dataref'ов.

      В результате драйвер получился довольно несложным -- фактически он просто жонглирует данными, дёргая готовые интерфейсы.

    • Инициализация:
      • Сначала узнаём номер нулевого канала и общее количество каналов.
      • Далее аллокируем "карту".
      • Создаём cda-контекст, используя auxinfo в качестве defpfx.
      • И, наконец, просто идём по списку всех каналов от 0 до numchans-1, для каждого выполняя следующее:
        • Инициализируем ячейку в карте значением CDA_DATAREF_ERROR.
        • Затем пытаемся -- посредством CxsdHwGetChanAuxs() -- добыть значение drvinfo. Если оно пусто -- то переходим к следующему каналу.
        • Добываем тип канала -- посредством CxsdHwGetChanType().
        • С полученными данными регистрируем канал, с флагом ON_UPDATE и заказывая события UPDATE, RDSCHG, FRESHCHG.

          ...нет, вот STRSCHG -- НЕ заказываем.

    • Обработчик событий data_evproc(): за основу взят оный из trig_read_drv.c -- точнее, он просто скопирован, с минимальной необходимой адаптацией.

      Передача полученных от cda данных крайне проста -- они тривиально футболятся наверх.

    • Реализована и функция записи (т.е., отправка записи в бриджуемые каналы) -- bridge_rw_p() просто сбагривает переданное ему, посредством cda_snd_ref_data().

    17.02.2020: проверяем.

    • Проверка делалась на крохотном "стенде" -- паре каналов магнитной системы, живущих на canhw:11.
    • Ожидаемо, возникли проблемы с {R,D}-конверсией: ведь у нас драйвер может отдавать только одну пару, а в оригинале их может быть больше.

      Решение -- убираем флажок 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, чтобы должным образом со строковыми каналами работать.

  • 25.02.2020: насчёт названия: пообсуждав с ЕманоФедей и ЧеблоПашей, лишний раз убедился, что термин "bridge" тут не подходит.

    По смыслу это именно "gateway". Но мне этот термин не нравится (и похожестью на "getaway" (всего-то 2 гласных поменять местами!), и...).

    Ну и как его переименовывать? Может, в "import"? Даже длина останется те же 6 букв.

  • 09.03.2020@дома-понедельник-выходной-вместо-8-го: при тесте с EPICS'ным "backend'ом" вылезла неприятная особенность: каналы, которые "там" не меняются, клиенту и не отдаются -- точнее, отдаётся CURVAL, а значение -- нет, поскольку реального обновления не было.
    • Да, это связано с разницей моделей работы с данными: CX различает отдачу клиенту ТЕКУЩЕГО значения (последнего известного) и ОБНОВЛЕНИЙ, а EPICS/CA -- нет (там при оформлении подписки в любом случае сразу же присылается значение, причём с ТЕКУЩИМ timestamp'ом!!!).

      Да, явно недоработка в ChannelAccess -- отсутствие отдельного события "текущее".

    • Поговорил с Гусевым -- да, он подтверждает, что оно действительно так и есть.

      Хотя то, что timestamp даётся ТЕКУЩИЙ, а не реальный -- его удивило.

    В результате клиенты, приконнектившиеся к такому бриджу, "висят", не видя обновлений, хотя в реальности каналы-то все ИСКУССТВЕННЫЕ, и их спокойно можно отдавать клиенту как есть.

    Т.е., таким каналам можно ставить IS_AUTOUPDATED_TRUSTED.

    Для чего придётся использовать какой-нибудь @-префикс.

    Напрашивается использовать "@!": и по смыслу это восклицание "ага!", и сам символ ни на что другое нельзя применить, поскольку для shell'а восклицательный знак имеет свой смысл (history).

    09.03.2020@дома, перед сном в районе 23:30: делаем.

    • Парсинг очень простенький, в качестве образца взят Cdr'овский cvt2ref() и ещё более упрощён.
    • По '!' взводится флаговая переменная is_trusted=1, которая изначально =0.
    • Ну и далее, после удачной регистрации канала "в backend'е", при ней взведённой каналу делается SetChanReturnType((,,,IS_AUTOUPDATED_TRUSTED).

    10.03.2020: проверяем. Был косяк -- в персинге не делалось ++ после успешного восприятия символа (сейчас только '!'). После исправления -- да, работает как надо.

  • 17.02.2021: появилась ещё потребность -- миррорить целые устройства (конкретно -- img878, чтоб v5p2camera на ВЭПП-4 можно было смотреть), и тут уже новая хотелка:
    • чтоб можно было указывать оное устройство максимально просто, без перечисления имён "внешних" каналов;
    • в идеале -- просто указанием типа устройства "SOMETYPE/bridge", и в auxinfo просто ссылку на исходное устройство, а уж имена каналов оно пусть само находит прямо из СВОЕГО devtype/nsp.

    17.02.2021: чуток больше конкретики:

    • Первой идеей было "а можно сделать, что если канал ЕДИНСТВЕННЫЙ, то позволить отсутствующее drvinfo" -- конкретно для img878 это бы прокатило, просто в базовом имени пришлось бы указывать ".data".

      ...вот где-то уже как-то что-то подобное было -- что в каком-то мирроринге или указании базы можно было что-то пропустить, и это прокатывало как надо. Но где и что... О -- нарыл!!! vdev, за 10-08-2016 -- "нужно уметь указывать -- в auxinfo -- имя ЕДИНСТВЕННОГО target-канала" ... "достаточно просто указывать пустой name -- "".". Да, ровно та же ситуация/потребность. Но ТУТ так делать уже не будем.

      Чуток позже: неа, НЕ прокатило бы: cda_add_chan() запрещает пустые имена.

    • Более общее решение: при отсутствии drvinfo считать, что надо брать "основное" имя канала из devtype/nsp.

      Правда, это как-то не очень, т.к. нарушает заложенный сейчас принцип "нет drvinfo -- пропускаем канал!"; заложено же это было для EPICS'ных имён.

      Можно включать такой режим указанием префикса у базового имени; например, "@?:" -- это и мнемоника "спроси (в devtype)", и никакого другого использования быть не может (из-за shell'а).

      18.02.2021: чтобы не организовывать тут в _init_d() громоздкий многоуровневый цикл как в mqtt_mapping'е, можно просто сделать функцию "а дай-ка dcpr канала по его номеру". Да, сама по себе она этот цикл БУДЕТ содержать, и оптимальность по скорости будет никакая (из-за многократных повторов), но это только при старте, так что можно забить. И, поскольку в принципе возможна множественность -- пусть возвращает dcpr ПЕРВОГО найденного имени с таким номером.

    • Но тогда встаёт отдельный вопрос -- а с "trusted" что делать?
      • Коль отдельного указания "внешних" имён не будет, то и флаг указывать негде.
      • А делать ВСЕМ "trusted" по умолчанию -- совсем не хорошо.

      Напрашиваются идеи:

      1. Указывать префиксом у базового имени -- тот же "@!:".

        Но это указание всем сразу.

      2. Как-то брать из devtype/nsp/dcpr -- там в принципе предусмотрено "return_type" (хотя в CxsdHwSetDb() оно и не копируется, но указать-то в devtype можно).

    Короче -- штука весьма интересная, но пока не вполне ясны детали, да и возни понадобится не так и мало. Так что проще пока проект заморозить.

    18.03.2021: да -- НУЖНО уметь миррорить ("клонировать") целые устройства, о чём сейчас был разговор с ЕманоФедей. Затронутые в разговоре темы:

    1. Нужно уметь так миррорить, чтобы не требовалось создавать дополнительные devtype'ы с дублированием информации.
    2. Также ему хочется уметь делать readonly-клоны: чтобы если в оригинальном девайсе каналы "w", то в клоне они бы работали как "r".
    3. И ещё есть хотелка, чтобы множественные обновления "кэшировались бы вместе" -- т.е., чтобы обновление приходило бы раз в цикл.

    Типа обсуждение:

    1. Как сделать -- ясно: выше проект месяц назад написан.
    2. Как считать "w" за "r" -- тоже ясно: указывать ещё каким-нибудь @-ключиком (какой символ?). Как минимум в auxinfo, но и в per-channel-drvinfo тоже не помешает.

      Вопрос будет с РЕАЛИЗАЦИЕЙ: ведь сервер-хозяин bridge'а будет считать после запроса на запись (когда взведёт wr_req), что ему должны что-то вернуть, а тут -- фиг... И вернуть реально неоткуда -- локальной копии данных же нету...

      Ну не лазить же руками в current_val etc.? Хотя, в принципе -- можно, только надо убеждаться, что значение там действительно есть (по timestamp'у?).

      @получасом позже: о, не -- надо это на уровне СЕРВЕРА делать! Указывать в devlist'е префикс, аналогично '+'/'-' для симуляции, который будет означать "считай все w-каналы за r".

    3. Включение режима "ON_CYCLE" -- ну тоже флажком. Опять же вопрос, какой символ выбрать.

    21.03.2021: некоторые соображения:

    • @днём: в режиме "@?:" -- когда имена внешних каналов берутся из карты каналов самого устройства -- их надо брать ТОЛЬКО из карты каналов, но никак не из drvinfo. Резон тривиален: ведь смысл режима "@?:" -- бриджевать другое устройство указанного devtype'а, а если в том devtype и будут какие-то drvinfo, то в них будет содержаться информация для ТОГО драйвера, но никак не для bridge_drv.
    • @~17:30, спускаясь по лестнице чистить Марка: надо не "@?:" использовать, а "@*:" -- мнемоника "бери ВСЁ!". Для shell'а же это столь же неприемлемое сочетание.
    • @вечер: по здравому размышлению становитя очевидно, что ВСЕ бриджуемые ro-каналы по сути своей AUTOUPDATED: ведь никакого способа запустить чтение нету, а полагаемся именно на добычу/мониторирование средствами cda.

      Так что всем ro-каналам надо ставить это свойство, вопрос лишь -- YES или TRUSTED.

      Вот пусть "#!:" и указывает на TRUSTED'ность.

    21.03.2021@~23:00, готовясь ко сну -- было детально ясно, как делать: приступаем.

    • Вместо просто передачи auxinfo в качестве defpfx'а для контекста теперь сделан более полноценный парсинг.
      • Указатель назван foreign_base.
      • Префикс "@*:" взводит флаг mirror_mode.
      • Префикс "@!:" -- флаг all_trusted.
    • При mirror_mode значение foreign_name спрашивается только из namespace'а (и если нету -- то канал пропускается), а попыток парсинга @-флагов не делается.
    • В drvinfo же заглядывает только при mirror_mode==0.
    • В качестве умолчания для по-канального is_trusted теперь берётся =all_trusted, а не =0.
    • Среди свойств "своего" канала теперь также добывается и is_rw, чтобы ...
    • Для не-rw-каналов -- и теперь ТОЛЬКО для них -- ВСЕГДА делается SetChanReturnType(), в зависимости от настройки либо IS_AUTOUPDATED_TRUSTED либо хотя бы IS_AUTOUPDATED_YES.
    • По анализу работы mqtt_mapping_drv.c стало ясно, что в целях оптимизации лучше СНАЧАЛА добывать указатель на namespace, а GetNameOf() пусть уж по нему ищет.

    22.03.2021: итак, делаем последние шаги:

    • Реализовано наполнение GetNameOf() -- поиск по namespace. Довольно просто и элегантно вышло.
    • А в _init_d() при включенном mirror_mode добывается указатель type_nsp.

    Теперь пора проверять.

    22.03.2021: проверяем -- для этого сделан специальный devlist-test-bridge.lst с 2 устройствами.

    Результаты тестирования любопытные:

    • Во-первых, поскольку драйвер bridge -- в отличие от vdev-based и прочих trig_read -- предназначен для работы в первую очередь не с соседями, а с совсем внешними устройствами, то умолчательного префикса "insrv::" тут нет.

      Поэтому для теста надо было ЯВНО указывать префикс "@*:insrv::" -- иначе оно ходило через cx::.

    • Далее, после добавления "insrv::" -- заработало.
    • ...но вот через "cx::" -- неа: ПЕРВАЯ запись до target-канала доходит, но в bridge-канале она не возвращается, а дальнейшие записи и вовсе не работают.

      Думал-думал, разбирался -- фиг...

    • А чуть позже осенило: это ведь и есть проявление того косяка в реализации протокола со стороны cxsd_fe_cx.c, когда мгновенно вёрнутый ответ на запись пытается вклиниться в уже имеющийся в sendbuf'е пакет, но обламывается и отбрасывается -- вот отклик на запись и не доходит и на этом всё стопорится.

      Эк оно склалось одно к одному -- давно ж собирался тот косяк исправить, но теперь оно стало категорически необходимо.

    25.03.2021: проаерил с уже исправленной реализацией протокола -- да, всё работает как надо! Ура!

    Вылезла только одна особенность, к bridge_drv относящаяся очень косвенно -- а скорее вообще к ЛЮБЫМ драйверам, коннектящимся к другим устройствам в ЭТОМ же сервере через "cx::": этот коннект происходит с задержкой в 10 секунд.

    Анализом кода и размышлениями понял причину:

    • Всё дело в том, что в роли guru работает ДРУГОЙ сервер.

      Ему БД отправляется, да, но из-за нюансов scheduling'а она успевает дойти не мгновенно, и, видимо, UDP-пакет CXT4_SEARCH успевает добежать и обработаться раньше, чем БД. В результате ответа у того сервера-гуру ещё нету и он молчит.

      А через 10 секунд UDP-поиск повторяется, и тогда вся информация уже на месте.

    • И да, тут не помогает даже то, что физически отправка UDP-запроса производится не в момент регистрации канала (т.е., в процессе инициализации), а "с отсрочкой в 1 микросекунду" -- уже в основном цикле.

      Видимо, просто ну уж очень быстро сервер стартует, а вычитывание гурой его БД идёт медленнее (что, конечно, странно -- уж у тестового конфига объём крохотный, в умолчательный maxrepcount=30 должно б было влезть).

    • 14.10.2023: или это всё-таки косяк "заказ таймаута резолвинга выполнялся лишь ЕДИНОЖДЫ, при создании резолвера", обнаруженный 17.05.2023?
  • 03.04.2021: у ЕманоФеди есть хотелка: чтоб бриджуемые каналы обновлялись бы не по реальному обновлению, а "раз в цикл". У него для этого смысл в некоем "кэшировании" -- когда в некий канал данные могут писаться (или обновляться в нём) многократно, а клиенту этого канала оно нафиг не нужно и достаточно по-цикловых обновлений.

    Как такое делать?

    1. Флага "ON_CYCLE" у нас нет (его роль исполняет отсутствие ON_UPDATE), так что придётся на уровне программы указывать, что НЕ взводить ON_UPDATE.
    2. Указывать, очевидно, каким-то @-префиксом. Причём ЖЕЛАТЕЛЬНО, чтобы этот префикс годился бы также и для cdaclient'а.

      Приемлемым кандидатом выглядит "~":

      1. Оно символизирует с одной стороны "всё" (по аналогии со смыслом в chaninfo), т.е. "со всеми", а с другой стороны "волну", т.е., "обновления в стандартной фазе".
      2. Не только в текстовом конфиге можно указывать, но и shell цепочку "@~" игнорирует -- это и экспериментально проверено, и в man'е по bash явно сказано "If a word begins with an unquoted tilde character".

    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
    так что канал просто не работал.

    Косяк исправлен, плюс в вышеупомянутое сообщение об ошибке добавлен вывод строки с точки, приведшей к ошибке.

  • 11.04.2022: замечено, что в коде bridge_drv.c отсутствует ловля событий RSLVSTAT_NOTFOUND и логгинг сего факта.

    В результате оного отсутствия если указать неправильное имя канала (или и вовсе базового устройства), то он просто молча не работает, а в логи ничего не попадает.

    12.04.2022: но и логгинг просто так сделать нельзя: тогда ведь при повторяющихся событиях "NOTFOUND" (например, при периодиечски повторяющемся поиске с покамест отрицательным результатом) эти сообщения будут засорять лог.

    • Так что по-хорошему надо бы воспользоваться подходом "is_suffering" -- т.е., логгировать только переходы из состояния "FOUND" (или первоначальное "неизвестно") в NOTFOUND.
    • Но для этого надо бы иметь где-то место для "помненья", что "is_suffering". А такового нет -- на каждый маппируемый канал только 1 ячейка cda_dataref_t.

    Так что -- в принципе, понятно, что и как делать, но неохота пока.

formula_drv:
  • 04.10.2021: пора уже свой раздел делать -- стало очевидно при возникновении очередной хотелки, для описания которой не нашлось готового места по причине отсутствия этого раздела.
  • 04.10.2021: в некоторых случаях может занадобиться по записи исполнять ПРОДОЛЖИТЕЛЬНЫЕ формулы -- со sleep'ом. Например, для "восстановления работы драйверов в VME-крейте под управлением A3818" требуется исполнить одну запись (reset линии), а через некоторую паузу выполнить stop=2 всем драйверам ADC250.

    (Да, конкретно при том использовании может хватить и "гарантий последовательного исполнения" вследствие того, что все эти драйверы живут в одном сервере и записи по insrv:: отработаются гарантированно в той последовательности, в которой запрашиваются. Но если всё же занадобится именно реальная пауза -- например, если "reset" отрабатывается не мгновенно -- то придётся использовать sleep.)

    Так вот: может возникнуть ситуация, что приходит ПОВТОРНАЯ команда записи в то время, когда предыдущая ещё не отработала. И тогда повторный запуск просто обломится, что не есть хорошо.

    Поэтому надо бы ввести дополнительный auxinfo-флажок, говорящий, что нужно перед каждым исполнением w-формулы делать ей cda_stop_formula().

    04.10.2021: вроде сделал (было б что :D) -- поле force_restart и одноимённый параметр.

    По-правильному -- проверить надо.

    11.10.2021: да, проверил -- работает как надо: БЕЗ указанного ключика вторая запись (до окончания исполнения первой) игнорируется, С ним -- отрабатывается. Ну кто б сомневался :D.

  • 14.10.2023: крупная переделка внутренностей драйвера, для приведения его в бОльшее соответствие работе обычных каналов.

    (Причиной стали странности, наблюдённые при реализации формулы-транслятора -- см. devlist-test-formula-transformer.lst; одна-то странность была обусловлена косяком в cda_core.ccda_dat_p_set_phys_rds() не-взведение rds_rcvd=1), но остальное -- именно несовершенная реализация данного драйвера.)

    14.10.2023: итак,

    1. Если пишущая формула почему-то решает, что поданное на вход значение чем-то "неправильное" и воздерживается от выполнения записи (делает просто ret;), то канал становится навсегда-запрошенным-на-запись и де-факто "зависшим", т.к. взведено wr_req=1 и следующие запросы не будут отправлены до возврата результата, которого никогда не произойдёт

      (Если только не обновится что-то, от чего зависит r-формула.)

      Обычные-то драйверы при отбраковывании значений возвращают текущее/последнее, но тут такой возможности нет.

      И, кстати, при "неподходящести" записываемого значения по типу (не-DOUBLE) тоже случалось аналогичное залипание-навсегда.

    2. Возврат значения делался несколько хаотично -- из formula_evproc()'а выполнялась добыча текущего значения, а из formula_rw_p() при отсутствии читающей формулы возвращался 0.0.

      Но способа вызвать возврат реального текущего значения при наличии читающей формулы -- НЕ БЫЛО (ну не вызывать же formula_evproc() вручную...).

    Решаем проблемы:

    1. Сигнализирование "облом, так что верни текущее сам" повешено на давно существующий механизм -- флаг результата CDA_PROCESS_FLAG_REFRESH, взводимый опкодом REFRESH.
      • Так что теперь в этом случае вызывается "возврат текущего" -- свежевведённого ReturnCurVal() (см. п.2).
      • А это условие запараллелено с "отсутствует читающая формула" (в каковом случае надо обязательно вернуть значение (да, 0.0), т.к. иного шанса уже не случится).
      • И -- структурным уровнем выше -- при неправильном типе значения делается то же самое.
      • И ещё (следствие предыдущего, в силу устройства кода, но и так было нужно) -- оно же вызывается в ответ на попытку чтения.
    2. Операция "вернуть текущее значение" оформлена в отдельную ReturnCurVal()
      • При наличии читающей формулы делается cda_process_ref() и cda_get_ref_dval(), ...
      • ...а при отсутствии -- просто val=0 и rflags=0;
      • и затем возврат имеющегося double'а (только при отсутствии читающей формулы ещё и timestamp не указывается -- чтоб проставился текущий).

      Соответственно, она вызывается:

      • Из formula_evproc() -- к этому вызову там всё и сводится.
      • При возврате записывающей формулой CDA_PROCESS_FLAG_REFRESH.
      • При action!=DRVA_WRITE или при "неподходящем" типе записываемого значения.

    Всё это было не особо сложно, но исходный код реструктурировался преизрядно.

vector_minmaxavg_drv:
  • 11.04.2022: занадобился драйвер для наведения статистики на векторный канал -- вычисление среднего, минимального и максимального значений. Историю возникновения потребности см. в разделе про onei32l, с кульминацией за сегодня.

    Поэтому приступаем и заводим раздел.

  • 11.04.2022: делаем.

    11.04.2022: краткое описание:

    • За основу взят mirror_drv.c, как немножко похожий.
    • В auxinfo указывается ссылка на базовый канал-вектор, с опциональным префиксом "@ЧИСЛО_ЭЛЕМЕНТОВ:". А вот dtype -- НЕ указывается, т.к. ...
    • ...канал регистрируется с типом 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 данные поставляет, но у того свои сложности).

const_drv:
  • 14.04.2022: давно напрашивались хотелки "сделать такой квази-драйвер, чтоб просто константы были в каналах" -- например, для указания фиксированных значений параметров для всяких скринов-осциллографов.

    И вот потребность стала уже конкретной: для onei32l.

    Поэтому пишем драйвер и создаём для него раздельчик.

  • 14.04.2022: приступаем.

    Некоторые соображения по проекту:

    • О базовом устройстве рассматривались разные варианты:
      1. По драйверу на каждый канал, значение указывается в auxinfo.

        Максимально просто, но дюже расточительно -- 4 служебных канала на каждый полезный.

        Плюс, неудобно с точки зрения конфигурирования: при возможности множественных каналов можно сразу делать цельное "искусственное устройство" (и даже devtype'ами определять), а при единичных -- придётся отдельнор ещё сорбиать такое устройство из разрозненных единичных каналов (плюс, никакой devtype на них не натянешь).

      2. МНОГО каналов в одном драйвере.

        Ценой чуть более сложного парсинга получаем намного более общий и гибкий вариант.

      Итого -- выбираем вариант "много каналов в одном драйвере".

    • Способ указания значений для множественных каналов:
      1. Самое рациональное -- в drvinfo, по штуке на канал.
      2. Также рассматривался вариант указания в auxinfo в формате "ИМЯ/НОМЕР_КАНАЛА=ЗНАЧЕНИЕ...": вроде бы запись получается компактной (единая строка), но парсинг выйдет шибко нетривиальным.
      3. ..."ход песцом" ((c) Старостенко): а если научить прямо сам СЕРВЕР -- cxsd_db_via_ppf4td.c -- парсить значения из спецификации devtype/channels и складывать их в БД; например, по ключевому слову defval. И чтоб у таковых сразу ставился бы текущий timestamp.
        • Плюсом тут была бы всеобщность (работало бы с ЛЮБЫМИ типами устройств), да и есть уже в cxsd_db_via_ppf4td.c должный набор парсеров.
        • Но жирный минус, множащий идею на 0 -- то, что в БД не предусмотрено никакого описания ЗНАЧЕНИЙ, а лишь СВОЙСТВА.
    • В любом случае, отдельный вопрос -- что делать с векторами (и строками). Покамест на них просто забъём, поддерживая лишь числовые скаляры.
    • О типах: надо поддерживать все возможные типы -- и все целые, и все вещественные. Просто выполнять парсинг (и ReturnDataSet()) в зависимости от типа конкретного канала.
    • О возможности записи: а ещё надо бы разрешить w-каналы -- чтоб в devlist'е указывались бы начальные значения, которые потом можно при надобности модифицировать (актуально, например, если захочется мудрить с каналами диапазонов при изменении измеряемых диапазонов).

    14.04.2022: итак:

    • Скелет копируем из noop_drv.c -- т.к. там всё максимально коротко плюс есть _rw_p(), возвращающая переданные ей для записи значения как "принятые".

      ...точнее, так: у noop'а в роли _rw_p() работает StdSimulated_rw_p(), а тут const_rw_p() вызывает её только при action == DRVA_WRITE.

    • Добавляем в _init_d() проход по карте из bridge_drv.c (именно только проход по карте, БЕЗ махинаций с typename/nsp).
    • Там внутри выполняется парсинг значения, в зависимости от devtype -- по индивидуальному strtoul() ([U]INT{8,16,32}), strtoull() (INT64/UINT64), strtod() (SINGLE/DOUBLE).

      А проверка успешности по endptr далее одна общая.

    • ...да, для SINGLE/DOUBLE НЕ будет работать возможность писать формулы "(ВЫРАЖЕНИЕ)".

      Вот тут уж начинаешь задумываться -- а может, всё-таки сделать в cxsd_db+cxsd_hw поддержку возможности указывать начальные значения, что, при условии до-запинывания возможности указывать return_type, позволит исполнять обязанности const_drv прямо обычному noop_drv.

    • Ну и, естественно, всем (прошедшим инициализацию, т.е., годного типа и с указанными значениями) каналам ставится IS_AUTOUPDATED_TRUSTED.

    Проверяем -- вроде работает.

rdreg_drv:
  • 27.06.2023: создаём задуманный ещё 07-06-2023.
  • 27.06.2023: собственно --
    • Скелет -- noop_drv.c,
    • работа с cda -- от trig_read_drv.c (включая всю data_evproc(), с отработкой FRESHCHG и RSLVSTAT),
    • размножение значения исходника по каналам -- из cankoz_lyr_common.c::cankoz_regs_f8(), включая макрос SET_RET_INFO(), только адаптированно под наличие rflags и timestamp.

    27.06.2023: нюансы:

    • Карта каналов -- 1+32, т.е., сначала канал "всё слово", далее отражения одиночных битов.
    • Файлы rdreg.devtype и rdreg_drv_i.h решено было не делать, т.к. формально в разных случаях могут использоваться разные карты каналов: и с числом битов <32 (драйверу указывается, см. след.пункт), и иных типов -- {INT,UINT}{8,16,32}, а дальше уж вопрос конверсии сервером.
    • Число битов может указываться в auxinfo, префиксом "/nn:" перед именем канала-источника.

      В таком случае и "всё слово" возвращается обрезанное до такого числа бит, и по-битовые каналы отдаются именно в таком количестве и не более.

    • ПОКА ЧТО делается обычная подписка на канал-источник, но за-#if'лен (по значению символа USE_CDA_REQ_READ) вариант с "форвардингом" запросов на чтение -- когда подписка делается в режиме PASSIVE, а rdreg_rw_p() генерит запросы на чтение (да, БЕЗ "оптимизации" с флагом "запрос уже отправлен"; с другой стороны, в rdreg_rw_p() нет цикла по переданному списку каналов, а просто один безусловный вызов).

    Проверено на элементарной симуляции -- работает как запланировано.

    14.07.2023: включен режим USE_CDA_REQ_READ, при котором используется cda_req_ref_read() -- проверено, работает как надо:

    • при отсутствии запросов -- ноль активности;
    • при запросах на чтение -- читает и отдаёт;
    • при "сторонних" запросах на чтение целевого канала -- также получает обновления.
wrreg_drv:
  • 28.06.2023: и этот тоже.
  • 28.06.2023: основа взята копированием rdreg_drv.c, в которую изменения вносились в 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*, чтобы избежать лишнего расширения знака).
    • Также на будущее заложена поддержка режима PASSIVE: не только "форвардинг" чтения, но и заказ чтения при не-rcvd, #if'но по тому же USE_CDA_REQ_READ.

    Проверено:

    • Работает, и ровно с указанным числом бит (и на вход берёт лишь биты не выше указанного, а выше оставляет как есть).
    • Попытка избавиться от "странного не-нуления req_msk=0" при отправке привела к предсказуемо неуспешному результату: ???????????????.

      Но это в принципе и понятно: ПЕРЕХОД ОТВЕТСТВЕННОСТИ???????????

    Кстати, заметки насчёт работы записи в регистры и нуления/не-нуления 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_dir_drv:
  • 13.05.2024: заводим раздел несколько задним числом: записи велись в разделе по simkoz_drv.c, а сейчас переносим сюда.

    Первоначально предполагалось назвать его "sim_drv" ("симулятор"), но 01-05-2024 было решено переделать в "sim_dir"

  • 01.05.2024: приступаем к работе над драйвером более активно; базовые соображения расписаны в разделе по simkoz_drv от 26-04-2024, в подпункте «sim_drv -- драйвер-симулятор-"кукловод", которому указываются соответствия "при изменении канала A выполни такие-то действия"».

    01.05.2024@утро-душ: некоторые соображения по устройству -- в порядке потока сознания.

    • Желательно бы синтаксис сделать совместимым с simkoz_drv.c. Т.е., чтобы в качестве DST принимал не только формулы, но и просто имена каналов.

      Что в принципе несложно -- аналогичное уже давно реализовано в Cdr_treeproc.c, где в r/w/c можно писать как каналы, так и формулы; вот оттуда и возьмём логику.

    • КАК парсить? Лучше -- с помощью того же PSP, т.к. таким образом решается проблема "формулу нужно указывать закавыченной". Т.е., синтаксис будет
      on_change=SRC_CHAN?"формула либо имя канала"

      В таком случае всё же нужен какой-то НАЧАЛЬНЫЙ спецификатор "target_dev=NNN".

    • Поскольку количество пар отображения заранее неизвестно, то нужно аллокировать privrec динамически, в процессе парсинга. Т.е., и РЕаллокировать... Но как тогда делать psp_parse()? Ведь ему-то ссылка на весь privrec даётся сразу, и её в процессе реаллокировать нельзя.

      @после завтрака: ну так парсить в 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(), без РЕаллокирования); но мы ведь так не хотим, да?).

    • @~16:50, пешком по лестнице вниз по пути в магазины: назвать-то лучше не "sim", а как-нибудь вроде "sim_director" или "sim_helper": ведь по сути это не устройство-симулятор, а "дирижёр" или "кукловод" для другого устройства. Видимо, sim_dir_drv.c -- и правильно, и достаточно коротко.

    02.05.2024: обмозговываем дальше.

    • @утро-душ: а ведь нужно будет ещё что-то вроде "at_init" -- параметр-формула, исполняемая однократно при старте и позволяющая, например, инициализировать значения локальных регистров.
    • @~10:30, обдумывая варианты "как делать reset из самих формул": а ведь если сами формулы будут делать либо .devstate=0 своему собственному sim_dir, либо .0=1 (stop) ему же, то возникнет ситуация, требующая механизма "being_processed/being_destroyed": в первом случае именно его (т.к. при рестарте устройства будет выполнена подчистка), а во втором -- как-то обнаружить факт остановленности формулы.
      1. Для отложенной подчистки -- придётся именно этот механизм, без вариантов: перед 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 и всё сошьётся.

      2. А для обнаружения останова в процессе исполнения?

        Чтобы не вводить лишние поля -- можно из 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().
      • Это позволит в будущем "отображать" не только double-скаляры, но, в принципе, ПРОИЗВОЛЬНЫЕ типы и вектора.
    • Вопрос будет в том, с КАКИМ ТИПОМ регистрировать каналы -- как источник, так и получатель.
      • Сейчас -- просто double, оба.
      • Потом -- видимо, придётся уметь указывать тип префиксом "@T[nnn]:".
      • Поддерживать ли также UNKNOWN -- "@x:" -- хбз.

    Приступаем потихоньку.

    • Сделан скелет -- init_d/term_d/rw_p и 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-го!).
    • Также предполагается использовать аналогичный подход к парсингу сущностей, как там с параметрами как раз из auxinfo -- "on_hbt" и т.п. -- сначала проверять его наличие посредством cx_strmemcasecmp(), а потом парсить штучно.

      Парсится не вручную, а psp_parse()'ом, чтоб использовать готовую инфраструктуру разбора кавычек и прочего квочения.

    • И при парсинге указывать terminators=whitespace, чтобы на первом же и останавливалось.

    Итак:

    • Парсинг по-штучный:
      • Заведены:
        1. 3 типа NNN_opts_t -- base_opts_t, at_init_opts_t, on_change_opts_t (да, в отличие от прочих подобных тут подчерк '_' перед "opts" -- из-за подчерков в именах, вроде "on_change").

          Каждый из них содержит единственное поле-строку.

        2. 3 PSP-таблицы text2NNN_opts[] -- text2base_opts, text2at_init_opts, text2on_change_opts, ...
        3. ...и 3 собственно parsing-target'а 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(); ..."
      • И да, как по проекту -- separators="", terminators=" \t".
    • Параметр "base=": сначала была мысль его значение использовать как defpfx при создании контекста. Но потом перерешено: т.к. во всех драйверах используется defpfx="insrv::", то тут сделано так же, а base используется в качестве аргумента base в cda_add_chan() и cda_add_formula(). Так ещё и сильно проще -- не надо проверять "base= указано раньше on_change=, и ещё оно не дублируется".

      07.05.2024@~09:40, по пути в ИЯФ мимо бассейна ВЦ: это даст ещё одну возможность -- можно делать МНОЖЕСТВЕННЫЕ "base=", перемежая ими "on_change=", тем самым указывая разным наборам разные базы. Это полезно при дирижировании НЕСКОЛЬКИМИ устройствами. ...а "at_init=" будет использовать значение последнего указанного (ну да, несколько неоднозначно; хотя если регистрировать формулу сразу в точке парсинга -- тогда будет использовать текущее).

    • "on_change=" -- основная работа:
      • спецификация разделяется на 2 части по '?',
      • на место которого -- это ж НАШ буфер! (а не auxinfo в БД) -- пишется '\0', чтоб терминировать SRC;
      • после чего регистрируется канал SRC (ошибка считается фатальной)...
      • ...и канал либо формула DST (ошибка -- просто WARNING).

      Считать ли DST каналом или формулой -- пока просто захардкожено, что формула. 07.05.2024: уже переделано.

    • А "at_init=" пока вообще ничего не делает. 09.05.2024: теперь делает.
    • В конце первого (нулевого!) прохода выполняется аллокирование privrec, его инициализация (в т.ч. все ref'ы славятся =CDA_DATAREF_ERROR) и создаётся cda-контекст.

    07.05.2024: доделываем чуток.

    • Сделана is_a_fla(), определяющая по тексту спецификации, канал это или формула. "Алгоритм" скопирован с Cdr_treeproc.c::cvt2ref(), как и предполагалось.

      Только в том алгоритме учтены не все флаги, даже уже поддерживаемые самой Cdr -- '/' (SHY) нету.

    • А также при тестировании далее поправлена пара косяков:
      1. При копировании из modbus_tcp_drv.c в блоке парсинга после успешного cx_strmemcasecmp() делалось "p += strlen(S_base_eq)" -- но здесь-то не ручной парсинг, а PSP, которой как раз НАДО ОСТАВИТЬ ключ.
      2. По ошибке копирования же в метрике оставалось указание 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():
      • Собственно парсим.
      • При регистрации каналов используем её результаты (вместо ON_UPDATE,DOUBLE,1), ...
      • ...причём для DST умолчаниями являются результаты от SRC -- так что оно автоматически наследует все его свойства.
    • Задействовываем at_init:
      • В privrec добавлено поле at_init_ref (реально нужно только при старте, но для приличия).
      • Туда при наличествующем at_init делается cda_add_formula() и при успехе --
      • исполняется cda_process_ref().
      • И это ПИШУЩАЯ формула -- чтоб не заморачиваться с постоянными allow_w; на вход подаётся 0.
      • Причём раз-регистрация тут же -- НЕ делается, так что оная формула преспокойно может запускать длительные процессы посредством команды "sleep".

    Список тестов для проверки работоспособности всего сделанного -- как собственно sim_dir_drv.c, так и нововведений в связке cda_core.c+cda_f_fla.c:

    • Простое отражение в каналы.
    • Отражение в формулы.
    • Исполнение at_init.
    • Запись из at_init в собственный _devstate=0 (рестарт прямо из инициализации).
    • Запись из on_change в собственный _devstate=0 (рестарт по внешней команде).
    • Запись из on_change в собственный .0=1 (стоп по внешней команде через отражение).
    • Другие типы данных -- не-double и/или векторы.

    10.05.2024: проверяем.

    • Простое отражение работать не перестало.
    • Отображение через формулы: вроде тоже работает.

      (Хотя обнаружилась мелкая опечатка: вместо "dst_name" регистрировалось "dst_name" -- огрех копирования, махом исправленный.)

    • at_init -- работает.
    • Попытка записи из at_init в собственный _devstate=0 -- ожидаемо SIGSEGV (должно быть переполнение стека).
    • А вот через паузу ("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: проолжаем проверять.

    • on_change=?_devstate=0 -- работает, но формулов механизм being_processed/being_destroyed даже и не задействуется: т.к. работает это из UPDATE-evproc'а, то весь КОНТЕКСТ находится в состоянии being_processed, так что подчистка откладывается.

      ...кстати, само исполнение формулы к состоянию "processed" НЕ приводит -- нету там этих "скобок".

      сделать sleep? До или после отправки? Можно проверить и так, и эдак.

      Попробовал:

      • ДО -- OK, отрабатывается отсроченный destroy.
      • После -- НЕ ok, SIGSEGV. Причём в SleepExp -- понятно почему: в состоянии "sleep" оно и за "processed" не считается, и 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() не нужно детектировать "а не повторный ли это вызов?").

        Так что просто убрал свежедобавленное и проблема исчезла.

    • "Другие типы", вроде векторов: при указании в SRC префикса @i100 -- работает, иначе, конечно, нет (путалось из-за несовместимости скаляров с векторами, и в DOUBLE-канале оставалось NAN, отправлявшееся в DST и там превращавшееся в "{-2147483648}" -- вектор из 1 значения, причём "странного" (это взведённый старший бит 32-битного целого -- 0x80000000)).

      А ещё в parse_chanref() была забыта инициализация is_unsigned_mask = 0 и целые типы почему-то выходили беззнаковыми (хотя мог вообще любой мусор попасть).

    12.05.2024: дотестировываем.

    • on_change=?.0=1 -- просто работает, исполняет STOP формулам без каких-либо проблем/побочных эффектов.

      (Возможно, ранее что-то и вылезло бы, но с дошлифовыванием прочего для этого аспекта возможностей косяков вряд ли осталось :-D)

    13.05.2024: поскольку вроде всё работает как надо, закомментировываем все отладочные fprintf(stderr,...)'ы (отовсюду -- и из драйвера, и из cda).

    14.05.2024: первая реальная проба: имитация реверсивного ИСТа.

    • После внимательного вглядывания в код ist_cdac20_drv.c, работающий с реверсом, оказалось, что там имитировать надо минимум:
      • при уставлении 1 в outrb6 (включение реверса) нужно в adc4 наличие <=-4V,
      • а при уставлении 1 в outrb7 (выключение реверса) нужно в adc4 наличие >=+4V.

      (После уставления 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"
      
    • Тут есть нюанс: поскольку у симулируемых "каналов АЦП" есть своя модель работы (simkoz_drv имитирует периодический опрос), то декларировать эти каналы как "w" и просто писАть в них -5V или +5V -- не вариант.

      Поэтому сделано косвенно: пишется в канал ЦАПа (тоже 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).
    • Позже понял, что другим вариантом было бы использовать тип CDAC20, но ему добавить единственную пару имён out4 и out_imm4.
    • Но как тут поступить ПРАВИЛЬНЕЕ -- пока не вполне очевидно.

    15.05.2024: результаты размышлений на тему "как правильнее" вчера и сегодня.

    • 14.05.2024@пешком от Wienerwald'а на Демакова в Быстроном, идя по тропинке между Демакова-12/1 и -14 и потом дальше между гаражами: в том конфиге канал DAC4/out4_imm/out4_cur использовался исключительно как "труба", этакий "почтовый ящик"; то же, что он ещё и ЦАП -- не роляло вообще никак (скорее мешало).
    • 15.05.2024@утро-завтрак: и из предыдущего пункта очевидно следует, что за основу надо брать именно CDAC20, которому добавлять в namespace пару каналов.
    • 15.05.2024@чуть позже, мытьё посуды: и делать его надо как раз с использованием "наследования", но БЕЗ смены ИНФО_ПО_КАНАЛАМ, а только с дополнением списка имён: какой-нибудь тип "sim_cdac20_4ist", примерно такого вида:
      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().
    • 15.05.2024@ещё чуть позже: и сделать ЕДИНЫЙ тип и единый список "on_change=..." для обычных и реверсивных ИСТов, просто для не-реверсивных отражение outrb6/outrb7 в out_imm4 и потом в adc4 не будет использоваться.

      И ещё чуть позже: неа, для НЕреверсивных ведь sim_dir вообще не нужен, так что некоторую оптимизацию нужно будет сделать -- чтобы не плодишись лишние драйверы с их лишними insrv::-соединениями.

    Попробовал такой вариант -- работает.

    Надо это как-то интегрировать в devlist_magx_macros.m4, чтобы для включения симуляции достаточно бы было включить какой-нибудь define() (а в идеале -- и вовсе переменную окружения).

    Погуглил насчёт переменных окружения (после того, как не нашёл ничего в m4.info*) -- "m4 getenv" и "m4 environment":

    • Хоть как-то "нативно" -- вообще никак: просто нет в M4 ни прямого доступа к нему, ни "getenv()"...
    • ...что потдверждается grep'еньем по слову "getenv" по исходникам m4-1.4.16/ (от CentOS-7.3); а слова "environ" там не нашлось вовсе, да и main() в src/m4.c определён как
      main (int argc, char *const *argv)
      -- так что этот путь тоже вне использования.
    • На Stackoverflow в теме "In m4, how do you include a file that has an environment variable in its name?" рекомендуют использовать esyscmd(), примерно вот так:
      define(`HOME', esyscmd(`printf \`\`%s\'\' "$HOME"'))

    18.05.2024@утро и затем в Ключах: некоторые соображения насчёт "как реализовывать симуляцию в общем случае".

    • "Работу"-то может выполнять sim_dir, в роли стороннего управлятора -- для этой роли он и создавался изначально, по проекту от 26-04-2024.

      Но вопрос -- КЕМ он будет управлять?

    • Можно "наследовать" тип симулируемого устройства, меняя каналы "r" на "w".

      А можно не наследовать и не менять, а просто помечать устройство как '+' -- суперсимулируемое, так что каналы чтения автоматом превратятся в каналы записи.

    • А можно ли как-нибудь обойтись ОДНИМ драйвером, и чтоб БЕЗ insrv-связей?
    • Варианты:
      1. Ещё один -- третий! -- драйвер, объединяющий в себе функциональность simkoz_drv.c (отработка внутри себя) и sim_dir_drv.c (отработка формул).
      2. Добавить возможность "отработки внутри себя", БЕЗ insrv-связей, в сам sim_dir_drv.c -- превратив его в универсальный-мастер-на-все-руки.

        Т.е., чтобы он и записи к каналам принимал бы.

    Соображения на тему такого унифицированного драйвера на основе sim_dir_drv.c:

    • Первый вопрос при записи -- ведь нынешний sim_dir воспринимает запись 1 в ЛЮБОЙ канал как "stop всем формулам"; что с этим делать?

      Решение несложно -- auxinfo-параметр "stop=", значение которого по умолчанию =-1, и _rw_p() чтоб проверял "не сделать ли stop всем формулам" первым условием в if()'е по каналам, условием "if (chn == me->stop_chan)".

    • "Адресация к себе" -- несложно: чтоб по умолчанию (при пустом base или при base=self) драйвер бы подставлял в качестве base_opts.base значение GetDevInstname() (19.05.2024: точнее, в НАЧАЛЕ цикла прописывал бы это умолчание, ну и по "self" тоже.)
    • Главный вопрос: а как сочетать ФОРМУЛЫ с "собственными" каналами?

      Запись-то как делать -- через тот же insrv? Но это:

      1. тот самый insrv, от которого хотелось избавиться (ибо совсем лишнее утяжеление);
      2. отсутствие "локальной" отработки записи -- всё будет идти длинной петлёй через cda, ВНЕ контроля самого драйвера (так что никакие оптимизации и прочие детектирования циклических зависимостей не сделать).

      Использовать какой-то другой протокол, вроде local/dircn (указывая при регистрации формул его в качестве base)? Но та пара совсем не годится для ДРАЙВЕРОВ (только для программы целиком), да и вообще "per-driver protocol" -- сомнительная штука.

    Резюме: ясно, что ничего не ясно. Похоже, надо эти работы притормозить до возникновения реальной КОНКРЕТНОЙ потребности -- тогда станет лучше понятно, что надо сделать.

    21.05.2024: пытаемся таки заюзать драйвер для того, для чего изначально он задумывался -- для симуляции источников магнитной системы Инжекционного комплекса ВЭПП-5, т.е., серверов canhw:11 и canhw:12, чья работа с источниками определяется в devlist_magx_macros.m4, в который и должны идти основные изменения.

    Давно (неделю?) зревший в голове "проект" -- добавляем в devlist_magx_macros.m4 поддержку симуляции, чтобы она включалась буквально установкой некоей переменной окружения, при которой автоматом бы "подправлялись" определения всего.

    • Есть нюанс: основная часть "мозгов" действительно в devlist_magx_macros.m4, но сами CAN-устройства определяются прямо в devlist'ах напрямую, как "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.

    • А для опционального -- только для реверсивных ИСТов -- добавления ещё и sim_dir'а придуман такой проект:
      • Если поданное на вход MAGX_IST_CDAC20_DEV() в $2 имя CAN-устройства начинается с '-' (что определяется по равенству substr($2,0,1) значению `-'), то...
      • ...дополнительно создать устройство sim_dir, которому...
      • ...в качестве "base=" указать $2 с отрезанным первым символом -- "substr($2,1)".
      • А создание устройства реализовать отдельным макросом, который при не-симуляции создаёт noop (а не пустоту -- чтобы списки устройств и каналов в обоих режимах совпадали).

    @подвал-у-Кузнецова-к.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!") -- видим, что:
      1. с ней ровно та же проблема "cannot run command ... No child processes",
      2. но вот если что-то она таки выводит, то этот вывод как раз ВСАСЫВАЕТСЯ обратно, ровно как от esyscmd().
    • @дома, @17:00: пытаемся найти следы аналогичной проблемы в энторнетах -- фиг: гугление по ключевому слову "m4" в сочетании с фразами "cannot run command" и "no child process" в разных комбинациях (с обеими, с одной из, с обеими БЕЗ m4) ничего не дало.
    • 25.05.2024@суббота, утро: сообразил, в чём причина странного поведения не-e syscmd(): потому, что она свой вывод отправляет на stdout, а он-то средирекчен "потребителю", поэтому потребитель получает и этот вывод (НЕ от m4!), хотя типа не должен бы.

      Надо делать "/bin/echo >/dev/tty" -- тогда принудительно отправится в консоль.

      ...а вот с "cannot run command ... No child processes" так и не понятно: тестовый прогон work/tests/syscmd.m4 ошибок не даёт.

    22.05.2024@дома, утро, ~10:00+: продолжаем рыть.

    • Поиск по bugzilla.redhat.com на тему m4 ничего не дал.
    • И просмотр исходников из m4-1.4.16-10.el7.src.rpm с поиском по слову "syscmd" не показал каких-либо хитростей работы с child'ами, ...
    • ...и файл NEWS в m4-1.4.18-7.el8.src.rpm и m4-1.4.19-1.el9.src.rpm показал, что там нет ничего касательно *syscmd начиная с версии 1.4.13.

    Дичь какая-то...

    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().

    • Во-первых, вчера создан тестовый файлик work/tests/syscmd.m4 содержания
      esyscmd(`pwd')
      esyscmd(`/bin/printf %s "$MAGX_SIMULATE"')
      syscmd(`/bin/ls -l /etc/passwd >/dev/tty')
      
      -- и там всё работает, никаких "cannot run command".
    • Во-вторых, пробуем рецепт "/bin/echo >/dev/tty" для не-e syscmd() -- ну да, помогло.
    • Хотя непонятно, почему: раз "cannot run command" -- то как вообще это перенаправление вывода во вроде бы невыполненной команде может влиять?
    • Гоняем под strace -f -- да нет, ВЫПОЛНЯЕТСЯ команда...
    • Гуглим на тему "m4 sigchld sig_ign": bingo!!!

      (Что любопытно, в результатах поиска было сплошное "Не найдено: m4 | Нужно включить: m4"; но принудительно закавычивание для затребовывания его обязательного присутствия мало что дало, а полезные результаты и так были в первых же строках -- 2-й и 3-й результаты :D)

      • "Do I need to do anything with a SIGCHLD handler if I am just using wait() to wait for 1 child to finish at a time?" на StackOverflow за 2013-2014
      • "Linux: wait и сигнал SIGCHLD" на iXBT вообще за 17/18-08-2004.

        И вот тут в конкретном сообщении #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 (гараж за колёсами, мойка у Мамонта, переобувка) продолжаем думать на эту тему.

    • Т.е., похоже, что перехваченный в состояние SIG_IGN сигнал SIGCHLD мешает нормальной работе pipe-запуска (и чтения_вывода!) в m4: тот ждёт в wait4(), но из-за SIG_IGN завершившиеся порождённые молча подчищаются, а wait4() завершается с ECHILD.

      ...а игнорируемость сигналов, ЕМНИП, вроде бы наследуется при exec()'е -- вот так влияние на вроде бы отдельный порождённый процесс и реализуется.

    • Что, как-то умничать с установкой SIGCHLD? Но ведь формально ppf4td может использоваться в разном софте, а работать-то корректно желательно бы всегда.
    • @~14, в ожидании переобувки Chaser: а если просто прямо перед exec()'ом вернуть SIGCHLD в состояние SIG_DFL?

      Выглядит как самое простое решение.

    • @~22, после возвращения с празднования ДР Петича в Болгарском Доме/"4 Сезона": да, сделано -- в ppf4td_pipe.c::ppf4td_pipe_open() теперь стоит
      signal(SIGCHLD, SIG_DFL);
      execv(cmdline[0], cmdline);
      

      Помогло -- ошибка исчезла!!! И получение значения переменной окружения теперь работает.

    04.06.2024: за последние несколько дней подготовил в devlist_magx_macros.m4 симуляцию для v3h_a40d16.

    • По анализу v3h_a40d16_drv.c оказалось, что там всё, что нужно -- это чтоб во входной битик OPR (d16.inprb0) попадало бы значение выходного битика SW_OFF (a40.outrb0):
      • Его значение 0 означает "выключить", а 1 "НЕ выключать", так что при включении туда записывается 1 (это аналог "enable" у ИСТа), а при выключении и авариях -- 0.
      • Таким образом, достаточно просто копировать значение SW_OFF в OPR, а значение выходного битика SW_ON (d16.outrb0) вообще можно игнорировать.
    • Ну и для красоты также копируется значение Iset_cur ("OUT_CUR") в канал измерения; сейчас это a40.adc{N*5}.

      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).

    • Это каналы Iset_cur, Iset_rate, opr, ilk и c_ilk.
    • --- Долго думал, прикидывал, сопоставлял, и дальнейшее записано уже после обеда ---
    • Есть мысль: возможно, проблема в том, что битики входного регистра, которые раньше были r-каналами, постоянно опрашиваемыми, теперь стали w-каналами, так что их значения "готовы" сразу и никаких обновлений не происходит.
    • Чтоб излечиться от проблемы "битики входного регистра, будучи объявлены как w-каналы, не обновляются и vdev считает их старыми" -- можно не объявлять их w, а оставить r, но писать в другие каналы (неиспользуемые АЦП'шные, например) и делать из них отражение в биты входного регистра уже внутри simkoz'а.
    • Но всё равно остаётся вопрос: а канал Iset_rate почему считается "старым"?

    05.06.2024: продолжаем.

    • Основная-то проблема в vdev: в том, что если значения IMPR-каналов есть в наличии уже к моменту завершения 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 данные приходят гарантированно ПОСЛЕ инициализации.

    • Ладно -- продолжим разбирательства, но записывая результаты уже в разделе по vdev.

    05.06.2024: уже после исправления проблемы vdev'а всё-таки вылезло неудобство: когда каналы входного регистра объявлены как "w", соответствующие им TUBE-каналы vdev-драйверов быстро становятся синими: ведь обновлений не приходит, в отличие от реальных r-каналов. А вместе с ними посиневают и Imes, т.к. там ради подсветки учитывается значение битика OPR, чтоб не подсвечивать жёлтым/красным расхождение при выключенном источнике.

    Поэтому напрашивается всё же реализация той идеи от 04-06.2024: оставить каналы входного регистра как "r", а в них писать "отражением" через какие-нибудь иначе не используемые каналы.

    06.06.2024: двигаемся дальше.

    0x.06.2024:

  • 27.05.2024: спросил у ЧеблоПаши на тему "а как в EPICS с симуляцией?".

    Ответ -- "у большинства типов рЕкордов есть специальные поля".

    На странице "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: спросил на ту же тему у Саши Сенченко. Ответ:

    • Штатно в TANGO такого ничего нет.
    • Конкретно Сенченко сделал драйвер-эмулятор Козака, который СЛУШАЕТ пакеты и отвечает соответственно, и вот его он сажает вместе с "драйверами источников" на виртуальную CAN-линию и так отлаживает.
???:
CAN:
  • 15.12.2009: начинаем делать поддержку/инфраструктуру CAN. Покамест оно живёт в drivers/cankoz/.

    17.12.2009: некоторые общие идеи:

    • О HAL-модулях:
      1. Интерфейс HAL-модулей определяется в canhal.h, сами модули именуются NNNcanhal.h.
      2. Интерфейс -- максимально простой, БЕЗ привязки как к серверу, так даже и к Козаку. Т.е.,
        1. Вся печать диагностики (ошибки, "setting speed=...") делается "клиентом" hal-модуля, которому тот лишь возвращает результаты (успех/ошибка/busoff).
        2. Эти же hal-модули годятся для создания произвольных CAN-программ, работающих хоть с сырым CAN'ом.

        Итого -- оне служат минимально-возможной абстракцией работы с конкретным типом CAN-интерфейса.

      3. На настоящий момент сделаны уже все 3 hal-модуля, впору испытывать.
      4. И, кстати -- по-хорошему, надо бы и v2 на них же перевести.
    • Исходник с кодом собственно CANKOZ-layer'а находится в cankoz_lyr_common.c, и предполагается, что ему при компиляции передаётся пара define-макросов, CANHAL_FILE_H (что include'ить) и CANLYR_NAME (имя для DEFINE_LAYER()'а). С canmon_common.c ситуация аналогичная. Предполагается, что бинарники с NNNcanhal.h будут называться NNNcankoz_lyr.so и NNNcanmon.
    • Соответственно, сама эта директория -- репозиторий с ИСХОДНИКАМИ всего, а драйверы/бинарники под конкретный интерфейс/платформу должны собираться в отдельных директориях.
    • P.S. Надо бы с системой сборки покумекать -- можно ль обойтись БЕЗ symlink'ования, чтоб оно использовало исходники напрямую из source-директории?

    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), вместо >=. Пофиксил.
    • Заодно добавил проверку номера девайса (реально сейчас ненужную, поскольку число битов как раз соответствует DEVSPERLINE).
    • Также поменял словцо "minor" (остававшееся в наследство от cankoz_pre_lyr.c) на "line" (по аналогии с nkshd485).

    22.09.2011@Снежинск-каземат-11: по аналогии с piv485_lyr: введен enum CANKOZ_MAXPKTBYTES=8 и заюзан везде, где раньше в качестве длины/размера/ограничения была просто 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. Исправил ">=" на ">".

    Замечание:

    1. А ведь, в отличие от v2, тут считается, что есть однозначное соответствие: один "девайс" (devid) может иметь лишь ЕДИНСТВЕННЫЙ kid. Просто потому, что цикл в _disconnect()'е завершается после нахождения первого [].devid==devid.
    2. Хотя (опять же в отличие от v2), как раз тут отсутствует поиск magic2handle() -- поскольку драйверный OS-like API сразу передаёт нам нужный privptr.

    Вряд ли это когда понадобится, но, формально, почему б и нет -- в 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: вчера обнаружилось любопытное обстоятельство: что, по крайней мере, в SocketCAN, всякая алхимия -- типа RTR-frames -- отдаётся старшими битами в поле идентификатора (при этом dlc=0). И, соответственно, расширенные (29-битные) идентификаторы -- также выходят за границы 0x7FF.

    (Увидено это было от сдуревшего вследствие наводок пановского УБСа.)

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

    12.01.2010: да, повставлял проверки.

    1. Ради паранойи, проверяется не "(can_id&~0x7FF)!=0", а "(can_id!=(can_id&0x7FF))".
    2. CX'ный cankoz_pre_lyr.c (в новом _fd_p() пока нету) такие пакеты игнорирует.

      А canmon (обе версии) при таких can_id печатают комментарий-предупреждение и НЕ пытаются дешифрировать ни kID/prio.r, ни 0xFF.

      Плюс, поскольку can_id -- int, то для выдачи используется формат "%u" вместо былого "%d".

  • 29.11.2012: крупное изменение в API: делаем "dlc" везде int, а не size_t.

    29.11.2012: протокол:

    • Вызвано поддержкой 64-битных хостов -- там int и size_t имеют разный размер (4, 8).
    • Именно int удобнее, поскольку для поле canqelem_t.datasize -- int (знаковый, ради поиска с частичным сравнением).
    • Во всех canhal_send_frame() добавлена проверка dlc<0.
    • ...по-хорошему, надо бы еще в recv добавить проверку на невыход ПОЛУЧАЕМОЙ длины за границы [0,8], но пока забъём.
  • 25.07.2015: вся поддержка CAN для v4 была сделана в hw4cx/drivers/can/ еще в июне "как надо" (по директориям собираются только layer'ы, и только в c4lcangw -- еще и драйверы для canserver'а, именуемого теперь "v4c4lcanserver").

    Сегодня же оно начато проверяться (на магнитной системе накопителя -- ring1:32), и исправляются имеющиеся косяки.

    25.07.2015: итак:

    1. Плохо работал disconnect(): он вроде как освобождал, но потом cankoz_add() считал слот занятым. Причина -- в разных частях было разное понимание, что считать за "свободно": disconnect() делал .devid=0 (в стиле v4 -- это "NOT_IN_DRIVER"), а cankoz_add() считал занятыми при .devid>=0.

      Решение:

      • Считаем свободными слоты с .devid==DEVID_NOT_IN_DRIVER; и инициализируются они в init_lyr() этим значением.
      • Заодно такое теперь запрещается в cankoz_add().
      • Замечание: т.о., теперь использование интерфейса должно быть доступно не только драйверам, но и layer'ам -- т.к. разрешены devid<0.
    2. После этого вылезла еще одна проблема, связанная с devcodes[]: лезли странные сообщения
      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.

    3. Еще модификация в disconnect(): теперь оно не заканчивает поиск по первому найденному, а ВСЕГДА прошаривает ВЕСЬ массив (как это, кстати, было сделано в v2'шном cankoz_pre_lyr.c).

      Смысл -- чтоб один драйвер (и layer?) мог зарегистрировать на себя более одного устройства, и они б корректно освободились.

      (Хотя СЕЙЧАС оно корректно себя вести не будет -- поскольку пытается делать SetDevState(), а оное к layer'у применимо едва ли.)

      05.11.2019: вылезла пара косяков, разного рода:
      • Ага, вот только эта "модификация" была крайне некорректной: поскольку "проверка, был ли это последний драйвер, использующий линию" делалась с той же самой переменной d (номер-адрес-ячейка устройства на линии, де-факто kid), что и во включающем цикле по поиску устройств по devid.
        • В результате по факту включающий цикл либо всегда завершается (если более устройств на этой линии не найдено), либо "прыгает" на ячейку после следующего найденного устройства.
        • Причина в том, что в изначальной версии _disconnect()'а -- от 04-11-2009, практически ровно 10 лет назад! -- НЕ предполагалось продолжение включающего цикла, а
        • Спасало нас от проблем лишь то, что НИКОГДА не было драйверов с несколькими kid'ами (собственно, то, ради чего это "теперь оно не заканчивает поиск по первому найденному" и делалось.
        • Решение: теперь для поиска "был ли это последний драйвер..." используется другая переменная, названная "other_d".
        • Кстати, и в v2'шном cankoz_pre_lyr.c сейчас тоже включающий цикл не прерывается, а пробегается полный набор устройств линии.

          Зачем это было бэк-портировано туда -- хбз; видимо, для унификации. Ну для унификации и там пофиксено.

          10.04.2024: какое ещё "бэк-портировано"?! Ведь выше за 25-07-2015 битым текстом сказано, что "как это, кстати, было сделано в v2'шном" -- т.е., там ИЗНАЧАЛЬНО было так.

      • Собственно проверка "есть ли другое устройство..." осталась ещё старой -- devid>0 вместо надлежащего devid!=DEVID_NOT_IN_DRIVER, так что де-факто использование интерфейса другими layer'ами функционировало бы некорректно.

        Исправлено.

    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'ом).

  • 29.12.2015: мысли о том, как реализовать полный функционал VSDC2 -- конкретно "осциллограммы".

    Проблема там в том, что вычитывание осциллограммы напрочь убивает обычную работу, т.к. это обмен кучей пакетов ("дай, на" на каждую ячейку осциллограммы). Точнее, проблем 2:

    1. Вычитывать можно осциллограмму ОДНОГО модуля, т.к. обмен с несколькими просто переполнит CAN-шину.
    2. Чтение осциллограммы мешает обычной работе.

    Следовательно, иметь осциллограммы "обычными" большими каналами, чьё чтение вызывается просто запросом "дай (прочитай) значение канала" -- низя.

    Напрашивается простое решение: в дополнение к большому каналу иметь еще командный канал "измерь", по записи 1 в который и производится измерение, а просто запрос на чтение большого -- игнорируется.

    Тем самым ответственность за потенциальные проблемы (успевающесть, синхронизация (НЕСКОЛЬКИХ устройств на одной линии, о чём по-девайсным драйверам договариваться неудобно)) перекладывается на юзера/оператора.

    ...кстати, такой же подход теоретически применим и для прочих "тормозных" систем -- см. описание потребностей салимовцев в разделе "Отключаемость каналов в cda" в bigfile-0001.html за 31-10-2006 (BTW, там в конце -- за 22-04-2009 -- прийдено к аналогичной идее).

    03.08.2016: да, так и сделано -- по каналу "get_osc" на каждую линию. Работает.

  • 19.09.2016: по требованию юзеров для CANDAC20/CEAC51, обслуживающих ИСТы/ВЧ300/... в cdac20_drv.c сделано "железное" ограничение выставлябельных в ЦАП вольтов.

    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.
    • Только тут заполняются не напрямую в privrec -- т.к. формат auxinfo иной, а в privrec_t.cfg. И "cfg-info" -- опциональный префикс перед именем target-CAN-девайса, указываемый в скобках.
    • А работает так же -- указанным считается при 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: в candac16_drv.c сделана возможность по-канального указания скорости. Потребовалась для старых источников на линаке -- там все разные, с разными калибровками.

    28.09.2016: технология:

    • Добавлена толпа параметров spdN (N:0-15), маппирующихся прямо на out[N].spd.
    • Старый spd->out[0].spd оставлен, де-факто alias на spd0.
    • Размножение его по spdN переделано:
      1. Оно делается ТОЛЬКО при out[0].spd!=0;
      2. Если целевой out[N].spd!=0, то он не трогается.
    • ...вообще это кривовато: по-хорошему, надо иметь ОТДЕЛЬНОЕ поле common_spd, заполняемое по spd, чтоб у 0-го не было никаких особенных свойств.
  • 04.10.2016: при создании нынешнего API cankoz_lyr была забыта фича "foreach" -- в результате отсутствует возможность удалять пакеты из очереди, что использовалось в драйвере SMC8 (при STOP'е удаляет START'ы).

    05.10.2016: записки по теме:

    • Учитывая, что в layer'ном (а не статически линкуемом, как в CANGW) варианте у нас CAN-драйверы еще нигде не используются (тест на ЛИУ-2 не в счёт ;)), можно при смене API версию не сдвигать.
    • Кстати, а в piv485_lyr метод q_foreach() есть, в простейшем варианте -- только с указанием функции-компаратора и БЕЗ возможности явного указания данных для сравнения; т.к. используется исключительно для поиска STATUS-возвращающих пакетов.
    • Для CAN же нужна более гибкая функция (как минимум в SMC8 требуется указывать данные из 4 байт).
      • ...в простейшем варианте -- как сейчас для SMC8 -- хватит именно просто указания модели data[dlc] (с возможностью dlc<0).
      • Но вдруг понадобится что-то похитрее?
    • Решено: делаем полноценный вариант, с возможностью указания как модели (data[dlc]), так и функции-компаратора.
      • При неуказании компаратора -- ==NULL==SQ_ELEM_IS_EQUAL -- используется обычная cankoz_eq_cmp_func().
      • При указанности же компаратора ему передаётся пара dlc,data[] (canqelem_t передавать нельзя ну никак, т.к. это внутренняя сущность cankoz_lyr_common.c).

    06.10.2016: делаем.

    • Тип для "юзерских" компараторов -- CanKozCheckerProc().
    • В API добавлено 2 метода -- q_foreach() и q_foreach_v().
    • Общая канва реализации скопирована с cankoz_q_erase_and_send_next*(), с которыми много общего -- и указание модели с возможностью dlc<0, и наличие итератора.
    • ...отличие же в том, что наличие модели не является обязательным. Более того, есть 2 альтернативы работы (прямо полностью разные ветки):
      1. Если checker==NULL, то подготавливается item с моделью для сравнения и используется стандартная eq_cmp_func() со стандартной семантикой "dlc может быть отрицательным для сравнения только начала данных".
      2. Если checker!=NULL, то модель игнорируется и просто вызывается checker для каждого элемента очереди -- через функцию-переходник foreach_checker().

      И:

      • Неуказанность же И checker'а И модели считается ошибкой.
      • И, нюанс -- _v-вариант имеет смысл ТОЛЬКО с указанием модели (иначе что там передавать в "..."?), поэтому у него параметры checker,privptr вообще отсутствуют.

    В smc8_drv.c вызов (c SQ_ERASE_ALL) добавлен, теперь как бы проверить...

  • 25.10.2016: обнаружилось (во время разговора с Гусевым), что в v4'шной версии забыто "цикличное вычитывание пакетов, до 30 раз, по repcount", имевшееся в v2'шной.

    Впрочем, не факт, что оно реально нужно с точки зрения производительности (точнее, экономии процессорного времени на syscall()'ах) -- см. анализ за сегодня в разделе по remdrv.

  • 31.10.2016@утро-дорога-в-ИЯФ-около-ИПА: к вопросу об имени линии -- что может потребоваться вместо стандартных canN использовать что-то иное (vcanN или rtcanN): можно использовать layerinfo, сделав его PSP-строкой (baudrate=NNN ifbasename=ZZZ.
  • 27.12.2016: за вчера и сегодня была реализована "незастреливающесть при неправильном коде устройства" (вместо этого переходит в "режим недоверия" и пытается дождаться устройства с правильным devcode).

    Подробности -- в bigfile-0001.html (т.к. изначально все соображения были там), раздел "CAN" за сегодня и предыдущие несколько дней.

    27.12.2016: кстати, давно переводить драйверы устройств с несколькими допустимыми кодами на add_devcode() (вместо собственных сравнений).

    04.01.2017: сделано. Коснулось всего 2 драйверов -- cdac20 (+CEAC51, +CEAC51A) и cgvi8 (+CGVI8M).

    Всё тривиально:

    1. Убрано ставшее ненужным DEVTYPE=-1;
    2. в _init() в вызове add() теперь указывается "первичный" код устройства, плюс добавлено вызовов add_devcode();
    3. в _ff() убран блок проверки типа устройства.

    03.04.2017: уже почти 3 месяца работает, неоднократно проверялось в реальных условиях, всё окей, глюков нету. Считаем за "done".

  • 09.02.2017: у юзеров -- Беркаев, Еманов -- возникла потребность уметь перестраивать источники "плавно" не просто линейно, а так, чтобы начало и конец перестройки происходили "максимально плавно": в начале плавный разгон, в конце плавное торможение.

    Смысл потребности -- поскольку магниты обладают некоторой реактивностью, то приходить в конечную точку надо максимально плавно (в идеале -- с нулевой производной), чтобы выбросов не было.

    09.02.2017: форма "плавности":

    • Кошерный вариант -- чтобы весь переход делался двумя кусками параболы, сшитыми посерединке.
    • Но покатит и просто прямая (с указанной скоростью), у которой начало и конец вытянуты в параболы.
    • Однако: ведь драйверы CAN-ЦАПов у нас работают в Мамкине, который тормозен и не имеет FPU. Так что делать всё надо просто в целых числах.
    • Вопрос: КАК в целых числах? Время разгона/торможения должно быть пропорционально длине перехода (как было бы при двух сшитых параболах), или достаточно фиксированной длины, одинаковой в начале и в конце?

    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. Цель-уставку могут поменять прямо в процессе идения.
      2. А могут поменять и саму скорость!

      При линейном хождении это всё не проблема (и прекрасно отрабатывается в нынешнем коде). А вот при желаемых плавностях уже не так тривиально.

    • Принимаем парадигму "как в КШД":
      • Считаем, что уставка у нас работает не как виртуальная сущность, умеющая мгновенно набирать нужную скорость, а лишь с некоторым ускорением.
      • Из этого вытекает и то, как поступать, если прямо в процессе движения меняется "направление" (т.е., шли вверх, а новая уставка оказалась ниже текущего): ПЛАВНО замедляемся, и потом так же плавно ускоряемся.

        Т.е., есть ТЕКУЩАЯ СКОРОСТЬ, к которой и применяется ускорение (полагаем его за 1/10 от максимальной скорости).

      • ...по-прежнему остаётся вопрос, как обходиться с определением, что пора замедляться (и на сколько?). Но, видимо, в КАЖДЫЙ момент надо смочь понять, что находимся на таком-то расстоянии от цели и, соответственно, выставить такую-то скорость.

        ...брать abs(cur-trg)*10/spd, и если оно в пределах [0...45], то брать коэффициент-мультипликатор из таблицы по этому индексу, а иначе считать за 10? Шибко уж завязано на то, что ускорение -- 1/10.

    • @~14:30-15:00, пультоваяНекоторые заметки по реализации:
      • Надо уметь в драйвере вести себя и традиционно (линейно), и "плавно". По умолчанию -- традиционно, а "плавность" пусть включается параметром в auxinfo (и НЕ может быть изменена at-run-time?).
      • Реализация в *_hbt() будет отдельной веткой. И так же из двух под-веток -- "вверх" и "вниз".
      • !!!видимо, в *_rw_p() по записи в .*_CHAN_OUT_n* и .*_CHAN_OUT_RATE_n* надо будет производить какую-то "подготовку" значений для использования в _hbt() -- чтоб та не делала каждый раз толпу вычислений.

    18.02.2017: еще мыслишки после прочтения письма ЕманоФеди от 14-02-2017-15:44:

    • @по-пути-на-обед-в-Амиго-около-Юности: он в письме предлагает использовать параметр "время разгона до максимальной скорости (T0)".

      Так вот: пусть это действительно будет per-channel-параметром.

      • Указывается в десятых долях секунды (точнее -- его R=HEARTBEAT_FREQ);
      • значение "0" означает "ускоряться мгновенно" (аналогично скорости: "0" -- перестраиваться сразу).
    • @Амиго-ждя-обеда: чтобы не дублировать код по куче драйверов (только сейчас -- 4 штуки), но и не возиться с вынесением в layer, можно вытащить код в .h-файл, который бы #include'ился ПОСЛЕ определения privrec, и содержал бы функции:
      1. *_hbt() -- просто целиком (там и таблиц касается).
      2. Helper-функцию к *_in() для обработки пакетов с текущими значениями уставок ЦАПов. Но декодирование надо оставить самому драйверу (ибо форматы разные).
      3. Helper-функции к *_rw_p() для реакции на *_CHAN_OUT_n_* и, видимо, прочие *_CHAN_OUT_RATE_n* и будущий *_CHAN_OUT_ACCEL_TIME_n*.

      Helper'ы можно сделать static inline.

      ...и полезен будет еще один файл -- с определениями, конкретно как минимум cankoz_out_ch_t.

    18.02.2017@дома: отдельный вопрос -- полностью ли надо имитировать инерционность? При смене цели, если она в другой стороне -- надо ли продолжать идти, замедляясь, T0 шагов в старом направлении, или сразу стартовать (с начальной почти-нулевой скорости) в новом направлении?

    21.02.2017: поговоривши с Федей -- НАДО продолжать идти, замедляясь. Смысл -- чтоб график оставался плавным, без изломов, так лучше для источников (изломы -- это прерывность производной, не-конечный набор гармоник (в Фурье)).

    20.02.2017: немного об арифметике.

    • В математически-идеальном варианте при равноускоренном движении за время ускорения должно проходиться расстояние D=a*T0^2/2.
      • Однако в целых числах это очевидно не так. Например, при T0=1s ускорение будет a=MaxSpeed/1s=MaxSpeed/10ds, т.е., 0.1 за децисекунду, и пройденное расстояние -- 0.1+0.2+...+0.9=4.5, при том, что должно было б быть 5.0.
      • Но если вспомнить "рекомендации ведущих собаководов" по, например, рисованию линий, то там надо "начинать с половинки".
      • Т.е., ускорение надо вести не 1*a/10, 2*a/10, ... 9*a/10, а 0.5*a/10, 1.5*a/10, ..., 9.5*a/10.

        По странному совпадению сумма этой последовательности равна как раз 5.0.

    21.02.2017: продолжение об арифметике.

    • Поговорил с Федей. По результатам, к сожалению, облегчения/просветления не наступило.
      • Если путь меньше, чем двойной путь ускорения (т.е., прямолинейного участка нет вовсе, а макс.скорость не достигается, то всё равно нужен корень.
      • Сумма 1+2+...+N называется "прогрессия".
      • Возможно, её надо считать именно циклом; хотя, вроде и какие-то формулы должны быть.
    • Мои мысли:
      • Для начала попробовать действительно в начале пути (или при смене цели и/или ускорения на ходу) проводить "начальные расчёты".

        В виде цикла по числу шагов ускорения, в котором вычислять путь, проходимый за время ускорения, а заодно -- число шагов, через которое пройденное во время ускорения расстояние превысит половину пути (и, соответственно, придётся начинать замедляться).

      • Слабо определившаяся идея: надо начинать тормозить на расстоянии от цели не просто то насчитанное:
        • Т.к., из-за не-кратности может оказаться, что предыдущий шаг еще "рано" (хоть на 1uV!), а следующий -- уже далеко перепрыгнули (в пределе -- на spd/10-1uV), и как бы поздно.
        • Следовательно, надо начинать тормозить на расстоянии "расстояние_ускорения плюс сколько_то". Главный вопрос -- сколько?
          • spd/10-1?
          • spd/10/T0? (минимальный сдвиг (шаг на минимальной скорости))
          • spd/10/T0/2? (половинка минимального сдвига)
    • Алексей Медведев с такой проблемой не сталкивался.
    • А Аскольда Волкова нет в институте с прошлой недели.

      06.03.2017: Аскольд вернулся, проведён его опрос. У него всё просто (в его 8048?): никакие корни не считаются, а просто при начале движения запоминает, когда достигнута максимальная скорость, и начинает торможение на том же расстоянии до конца; также всё время проверяется, не дошло ли уже до середины, и если да, то также переходит к торможению. Мне же посоветовал всё считать предварительно в начале.

    • И отдельно: по словам ЕманоФеди, возможно, надо для движения вниз (в сторону 0) иметь отдельный параметр "максимальная скорость". Т.к., у источников (и магнитов?) разные требования в разных направлениях.

      Йок...

    23.02.2017@утро-душ:

    1. Экспериментальный драйвер назовём cdac20k, "k" -- kinetic.
    2. Идея общего алгоритма: в некий момент -- при старте движения, при смене параметров (цели, макс.скорости, T0) -- производить итеративные вычисления (по числу T0 или менее), чтобы понять, когда -- на каком расстоянии от цели -- надо начинать замедляться.
      • Для старта с нулевой скорости: если в цикле насчитываем, что "прошли" более половины дистанции (минус поправка для округления, то "0.5кванта_ускорения"), то прерываем цикл и считаем то значение точкой, когда начинать замедляться.
      • Собственно, всякие сложности -- смена ускорения "на лету", смена цели на сильно близкую, ... -- могут корректно учитываться именно в этой вычислялке; её "продвинутость" и определяет умелость софтины.
      • И это предварительное вычисление и есть замена того хитрого многозвенного "с разных сторон" if()'а, рассматривавшегося для проверки на каждом шаге.
    3. А если новую цель ставят так близко, что оставшееся расстояние невозможно пройти, замедляясь с указанным ускорением. Как поступить:
      1. Остановиться рывком.
      2. Проскочить, замедляясь, а потом так же замедляясь вернуться?

      23.02.2017: (~23:45) Федя, злобно ругаясь, ответил, что ни тот, ни другой вариант -- не катят. А надо, мол,

      1. "считаем корень и повышаем ускорение так чтоб равноускоренно затормозить за оставшееся расстояние."

      Ибо "тут дело в том, что если такое случится, то пролетев мимо точки ты направление подхода меняешь, т.е. влияешь на характер поведения системы, когда тебя об этом явно не просили".

    23.02.2017:

    • Камиль ответил (в 20:27) на письмо с вопросом (от 12:14), как он делал. Так вот:
      1. Он честно считал корни. Но не "в лоб", а при помощи некоей библиотечки для texas'овского МК, считающей оптимизированно (видимо, итеративно).
      2. Также существует некая AVR'овская библиотека, вычисляющая корни рекуррентно; ключевое слово -- "avr446", и даже PDF-файл легко нагугливается.

        Но там, как показывают прочие результаты, используется 16-битная арифметика.

      3. Что же касается торможения, то "Про то когда тормозить - я смотрел сколько шагов разгона задано, столько же шагов торможения выдавал".

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

    27.02.2017: начинаем делать.

    • Введена внутренняя функция DoCalcMovement(), должная выполнять все вычисления в любой момент. Она:
      1. Принимает параметрами целевое значение и пару указателей на cankoz_out_ch_t:
        • trg -- куда требуют придти.

          Идёт отдельно от ci, чтобы при изменениях в процессе движения старая цель оставалась известна? Хотя, с другой стороны -- а зачем она нужна? Но так сделано в т.ч. по причине архитектуры работы _rw_p(), где прописывание out[l].trg производится в самом конце, уже ПОСЛЕ всех проверок/вычислений.

        • ci (Chan Info) -- канал, над которым надлежит произвести действие.
        • rp (Result Pointer) -- куда складировать результат вычислений.

        Т.е., она НЕ меняет ничего в канале, а возвращает результат отдельно.

      2. Возвращаемое значение -- "что делать".

    28.02.2017: продолжаем.

    • По ходу дела был найден косяк в обработке варианта "Or is it an absolute-value-decrement?", заложенный еще в 2009-м. Подробнее см. в bigfile-0001.html за сегодня.

      Исправлен. Проверено -- теперь пашет как надо.

      30.04.2017: поскольку полностью на kinetic-вариант пока не переходим, то и в обычный cdac20_drv.c этот фикс добавлен. И в прочие -- cac208_drv.c, candac16_drv.c, ceac124_drv.c -- тоже.

    • Мозги DoCalcMovement() начаты -- простой вариант цикла "до середины диапазона или до максимальной скорости".

    01.03.2017: далее.

    • Предварительное: оказалось, что у всех скринов CAN-ЦАП/АЦП отсутствует disprange на каналах out_cur* и adc*.
      1. Поставлено disprange:-10.0-+10.0 всем таким каналам во всех скринах.
      2. Также -- за компанию -- поставлено disprange:-2-+2 всем каналам outrb* и inprb*, чтоб можно было их кидать на график (может оказаться полезно для отсмотра корреляций работы аналоговых каналов с двоичными у всяких блоков управления источниками).
    • Пилим-пилим. Пока простой вариант -- считающий, что стартуем всегда из стоячего положения.
      • Тот самый цикл и его обвязка вроде доведены до некоей кондиции.
      • Также добавлено всяких полей в cankoz_out_ch_t.
      • _rw_p() -- складирование полученных данных в me->out[l].
      • _hbt() -- использование при act!=0. Состоит из 3 основных частей:
        1. Сдвиг текущего значения на текущую скорость -- двумя ветками (вверх и вниз), в точности как с прямолинейным вариантом.
        2. Изменение скорости
        3. Прописывание текущего значения -- полная копия из прямолинейного.
    • Результат:
      1. На "длинную дистанцию" ходит хорошо, красиво (правда, надо T0 ставить большим, чтоб видно было -- тестировал на 30 шагах).
      2. А вот на короткие -- когда не успевает полностью разогнаться, а надо уже тормозить -- хужее: тормозить начинает будто чуток рановато, так что в конце едет на минимальной скорости (spd/act/2) очень долго.

        Гипотеза:

        • Оно начинает тормозить не на середине отрезка, а на том расстоянии, что насчитано в цикле "до достижения точки-середины". Но это расстояние всегда будет БОЛЬШЕ, чем половина пути, а следовательно -- и тормозить начнёт несколько заранее, и потому слишком рано скорость опустится до минимальной, еще ДО подхода к цели.
        • А если прописывать в "расстояние, ближе которого начинать тормозить" не то насуммированное в цикле расстояние pos, а min(d_half,pos)?

          Единственное сомнение: не будет ли при этом последний шаг слишком резким?

        • Попробовал. Вроде получилось. Но неидеально оно:
          1. Нет уверенности, что никогда не будет "втыкаться" слишком быстро.
          2. По графику видно, что как минимум иногда оно всё же подползает долго на минимальной скорости (например, переход 0V->1.2V при spd=-1V/s tac=30).

        Короче -- надо еще поанализировать числа (драйвер при обсчёте в цикле печатает все шаги).

    03.03.2017: несколько мыслей по результатам обдумывания:

    • Проблема явно из-за того, что начинает тормозить чуть-чуть раньше -- буквально на шаг, но этот шаг -- на большой скорости, так что компенсировать его потом мелкими (половинными) будет долго.
    • Следствие: время "компенсации" -- не более удвоенного T0 (удвоенного -- потому, что шаги половинные).
    • Как бы это поправить: надо сделать так, чтобы конечную часть траектории (торможение) оно бы шло ТОЧНО по пути, заканчивающемся в конечной точке (с последним шагом на половинной скорости).

      Для этого надо как-то делать коррекцию на первом шаге торможения (или лучше перед ним) -- этот шаг должен быть не на полную (или полную минус ускорение) скорость/дистанцию, а так, чтобы придти в точку, из которой торможение закончится в нужной точке.

      Как? Видимо, в DoCalcMovement() постоянно помнить значение суммы на предыдущем шаге, и как-нибудь так его запоминать, чтобы в _hbt() в нужный момент делать "подруливающий шаг".

    • ...кстати, в КШД всё проще -- потому, что там довольно высокая частота дискретизации (килогерцы или десятки килогерц, не чета нашим 10Гц), к тому же, скорее всего, как-то привязанная к гранулярности скорости (например, скорость -- максимум 65535, и тактовая тоже 65535).

    02.06.2017: вот смотрю на код, и на числа отладочной выдачи (DO_DBG=1), как баран на новые ворота -- и нифига не понимаю... Уже даже график этого всего построил в gnuplot'е -- и всё равно идеи отсутствуют...

    03.06.2017@утро-после-п.утех: ключ -- на "граничном" шаге между линейной ходьбой и торможением.

    • Выходит, что мы начинаем тормозить слишком рано, а надо бы сделать еще один шажок на почти полной скорости (а не на max-accel), так?

      Ну так и надо сделать этот дополнительный шаг.

    • Технический вопрос -- КАК? Шаги считать, и помнить, когда "предпоследний"?

      04.06.2017: неа, скорее по-другому: помнить, какого "рода" был предыдущий шаг (для этого заведено неиспользуемое поле a_dir), и если только-только собрались переходить к торможению, то очередной шаг сделать не на crs-acs, а на "разницу". Как вариант -- без использования "памяти" a_dir, а просто сравнением: что собрались тормозить, а текущая скорость всё ещё равна максимуму.

    • Кстати, ведь перемещение -- это интеграл от скорости; вот и надо так подрихтовать график скорости, чтобы интеграл равнялся целевому значению микровольтов, а сам график был бы в должной степени "гладким".

    (В случае коротких перемещений (когда до максимума разогнаться не успевает) -- граница между ускорением и торможением. Тут надо какую-то вариацию решения придумать: то ли вставлять 1 "плоский" шаг (на фиксированной скорости, и вот на КАКОЙ скорости -- как раз зависит от "недостачи"), то ли как-то делить поровну пополам между последним шагом с ускорением и первым с замедлением...)

    04.06.2017: по беглому взгляду на код вообще не очень ясно, откуда берётся "недостача". Ключевое слово -- "dcd".

    04.06.2017: отдельный вопрос -- об арифметике. Как вычислить "недостачу"?

    А вот как:

    1. из требуемого расстояния (разницы между текущим и целью) вычесть удвоенное dcd (это расстояния, проходимые при разгоне и торможении),
    2. и результат (это расстояние, должное быть пройдено по прямой) поделить на mxs -- это получится число шагов для прямолинейного движения;
    3. а остаток от того деления и будет искомым числом -- т.е., тем "куском", что не уложился в кратные шаги.

    05.06.2017: (утро) роем.

    1. Найден косячок с вычислением "позиции до d_half" в DoCalcMovement(): там стоит условие "pos < d_half", так что вычисления идут до чуть большего значения позиции. Надо бы ограничивать не по pos<d_half, а по pos+spd<=d_half.

      Поскольку дальше стоит присвоение с проверкой

      rp->dcd = (pos < d_half)? pos : d_half;
      то фактически проблемы нет (и при реальной ходьбе будет использовано это же ограничение); но для целей отладки выдача результата подзапутывает.

      После планёрки: а вот и нифига! ВЛИЯЕТ!!! См. ниже.

    2. Также обнаружен нюанс ((c) Чапаев): скорость прибавляется к РЕАЛЬНОМУ текущему значению вольтов, вычитанному из устройства, а не к "желаемому".
      • В результате потихоньку накапливается ошибка -- до кванта за шаг.
      • Оно так было изначально, с 2009 года. И понятно почему: чтобы привязываться к реальному значению, которое сейчас в устройстве.
      • Стоит всё же "идти" своим, "идеальным" значением в микровольтах. А чтобы не лишаться привязки к реальному значению (мало ли -- вдруг кто-то посреди хода вручную canmon'ом туда что запишет, или сбросится (см. след.пункт насчёт is_a_reset)), надо использовать Knobs/cda'шный подход: если разница между идеальным и реальным менее кванта, то оставлять в силе идеальное, а если больше -- то делать идеальное:=реальное.

      После обеда: сделано.

    3. Кстати: судя по анализу кода, движение НЕ прерывается по получению 0xFF/is_a_reset. А сие выглядит несколько сомнительно.

      Может, всё-таки прерывать (нулить num_isc и всехние .isc)? Ведь таблицы-то прерываются.

    05.06.2017: (после планёрки) делаем.

    • После исправления "условия окончания предварительного обсчёта" в DoCalcMovement() вдруг всё резко исправилось: никаких занудных приближений, а ходит чётко по нужной траектории.

      Т.е., очень даже влияет тот промах не только на отладочную печать, но и на работу! Как так?!

      • Внимательное наблюдение показало причину. Например, при движении 0V->1.2V на spd=-1V/s@tac=30:
        1. Раньше: distance=1200000 d_half=600000 pos=601597
        2. Теперь: distance=1200000 d_half=600000 pos=539937

        А поскольку в присвоении dcd стоит то условие -- "взять наименьшее из pos,d_half" -- то раньше оно начинало тормозить слишком ЗАРАНЕЕ.

      • Однако это НЕ является правильным решением.

        Проблема в том, что оно вовсе НЕ ходит "чётко по нужной траектории", а втыкается в конечную точку на слишком большой скорости. Из-за того, что начинает тормозить уже слишком ПОЗДНО.

      • Т.е., правильнее всё же оставить старый вариант -- чтобы оно начинало тормозить достаточно заранее, дабы успеть сбросить скорость.
      • И тут как раз надо делать так, как придумано в том субботнем озарении: неким образом "плавно" сбрасывать скорость, делая первый в замедлении шаг так, чтобы после него встать в позицию на расстоянии pos (>=d_half, а не <=d_half!) от цели.
    • В итоге есть ответ на вчерашний вопрос «откуда берётся "недостача"?».

      Оттого, что торможение начинается слишком рано. Что, в свою очередь, следствие НЕКРАТНОСТИ -- дистанция не делится на целое количество шагов из 3 участков (линейное ускорение, с постоянной скоростью, линейное торможение).

      И да -- надо вставлять компенсационный шаг, на расстояние/"скорость" чуток меньше максимальной.

    • (после обеда) Первый шаг (для красоты) -- исправляем п.2, чтобы скорость прибавлялась не к реальному текущему значению, а к "желаемому".

      Добавлено поле dsr (DeSiRed), в котором поддерживается это самое "идеальное" значение:

      1. При запуске "плавного изменения" в _rw_p() туда копируется текущее значение .cur.
      2. При получении текущего значения в _in() производится сравнение, что abs(dsr-cur)<THE_QUANT, и если не так, то делается dsr:=cur.
      3. При вычислении следующего значения в _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.

      • И что имеем в результате: по той вчерашней "формуле вычисления недостачи" получаем (1250000-(450000+450000))%30000=20000.
      • НО! При том, что максимальная скорость 30000 -- вставлять такой шажок при замедлении весьма некомильфо.
      • Вставлять его надо там, где скорость меняется от превышающей его до не-превышающей?
      • Да! Причём, такое решение будет работать совершенно одинаково как для ситуации "успели ускориться", так и для "не успели ускориться".

    06.06.2017: по результатам пиления и исследований:

    • Такое впечатление, что обсчёт в DoCalcMovement() просто немного промахивается относительно реальной ходьбы в _hbt(), оттого и несовпадение "предсказания" с реальностью.
    • И та "формула вычисления недостачи" на 0->1.2V при spd=-0.3V@tac=30 (mxs=30000, dcd=450000) даёт 1200000-450000-450000=300000, а 300000%30000=0.

    07.06.2017: да, bingo!!!

    • Косяк нашелся! Дело оказалось в том, что разгон начинается со скорости в ПОЛОВИНКУ шага ускорения, а торможение -- сразу на полный шаг.
      • Поэтому замедление не было симметрично торможению. Например, если spd=0.2V (=200000uV) и tac=2, то: mxs=20000, a1=10000, mns=a1/2=5000; следовательно, диаграмма скоростей при разгоне: 5000, 15000, 20000..., а при торможении -- ...20000, 10000, 5000...!

        Т.е., при разгоне успеваем пройти 20000 шагов, а при торможении -- всего 15000. В случае бОльших tac разница будет, соответственно, больше.

        И разница интегралов -- как раз mns*(tac-1); вот оно лишние tac-1 шагов и ползёт в конце.

      • Как только было сделано, что и замедляться начинает не на acs, а на mns (=acs/2), то вариант "0->1.2V при spd=-0.3V@tac=30" стал ходить идеально.
      • Однако, не всё так радужно:
        1. ...а вот иные, "менее кратные" варианты -- увы, нет: "втыкается" на слишком большой скорости.
        2. Случая, когда до максимальной скорости разгоняться не успевает, этот баг и не касается.

          ...хотя что-то там и может быть -- как раз на 1 шаг, из-за того, что не факт, в нужные/скоррелированные ли момент делаются сдвиг позиции и проверка на замедление.

      • Откуда вообще взялось это "сдвиг на 1/2 шага"? Видимо, 20-02-2017 -- «там надо "начинать с половинки"»; это давало какой-то -- бонус по результирующему интегралу.
    • Итак: у нас имелся не один косяк, а ДВА РАЗНЫХ!
      1. Отсутствие компенсации при не-кратности расстояния шагам.
      2. Несимметричность замедления разгону.
      3. Явно еще что-то третье -- что "втыкается на скорости".

    22.06.2017: Федя возмущается, что

    1. При отключении источника по блокировке уставка ЦАПа не мгновенно сбрасывается в 0, а медленно ползёт (кто-то поставил скорость -10A/s, так что с 1720A ползло о-о-очень долго).

      И очень требует, чтобы всё-таки сбрасывалось бы мгновенно, а на моё возражение "как?" (о блокировке знает драйвер ИСТа, а плавное изменение реализует драйвер ЦАПа) предлагает завести ещё один канал, запись в который игнорировала бы скорость.

    2. Неприятно, что увеличение разрешённой скорости во время движения уже никак не влияет -- оно так и ползёт с первоначальной.

      Но это-то понятный косяк, следствие недоделанности кинетического перемещения.

    23.06.2017@утро-пляж: поскольку свободного места в стандартной карте каналов KOZDEV просто нет, то остаётся только занимать позиции, остающиеся из-за неиспользования полных диапазонов (которые рассчитаны на 32 канала ЦАП).

    • Каналы "прям щас!" -- _CHAN_OUT_IMM_n... -- поместим после обычных каналов OUT.
    • Каналы, дающие доступ к времени ускорения -- _CHAN_OUT_TAC_n... -- после каналов скорости OUT_RATE.

    Таким образом, в нынешней карте, рассчитанной на 32-канальные ЦАП, можно будет поддерживать дополнительные каналы у 16-канальных устройств. Что вполне достаточно -- сей момент самым многоканальным ЦАПом является CANDAC16.

    23.06.2017: делаем, на примере cdac20k.

    1. Интерфейс:
      • В карту каналов добавлены -- и как out_imm0+out_tac0, и как alias'ы out_imm+out_tac.
      • В cdac20_drv_i.h пришлось добавить
        • 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
      • В "определение свойств устройства" в _drv.c они помещены как DEVSPEC_CHAN_OUT_IMM_n_base и DEVSPEC_CHAN_OUT_TAC_n_base.
      • В _init_d() для IMM добавлено указание RD и кванта (TAC'ам оно вроде и не нужно).
    2. "Мясо":
      • _rw-обработка повешена на
        • HandleSlowmoOUT_IMM_rw() -- тут есть отключение slowmo, если оно идёт -- и
        • HandleSlowmoOUT_TAC_rw() (этот очень прост).
      • Отдельно пришлось добавить отдачу значения и его тоже при получении ответа от ЦАПа, в HandleSlowmoREADDAC_in() -- условную, при DEVSPEC_CHAN_OUT_IMM_n_base>0.
    3. В ist_cdac20_drv.c добавлен VDEV_PRIV-канал C20C_OUT_IMM и отправка в него 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 не только при отсутствии указания скорости, но и при неуказанности времени разгона/торможения. В результате -- плавное изменение не работает вовсе, а всегда уставка скачком.

    История разбирательства:

    • На проблему пожаловался ЕманоФедя несколько дней назад: что оно так себя ведёт у кучи источников, причём разных.

      (И, вроде бы, жаловался и раньше -- в сентябре-октябре, но ненастойчиво (другие дежурные замечали проблему), а я ответил, что такого быть не может, потому что не может быть никогда -- типа, ищите проблему в железе.)

    • Я проверил на примере одного девайса -- canhw:11.fedcor -- и действительно, скорость игнорируется.
    • Окей, пытаемся воспроизвести проблему, чтобы потом напихать отладочной печати и найти причину.
      • Проверил у себя, на SocketCAN, с CANDAC16 -- всё работает плавненько.
      • Закралась мысль, что причина в архитектуре (32/64): ведь всё касательно slowmo делалось и тестировалось на sktcankoz, на x10sae, которая x86_64. А вдруг вкралась ошибка в арифметику, что на 32 битах (CANGW/PowerPC) где-то что-то не так?
      • Окей, взял живой CANGW, попробовал на нём -- тоже работает.

        Как с текущей сборкой v4c4lcanserver, так и с той от 07-09-2017, что на ctlhomes (она, впрочем, имеет тот же размер и отличается только MD5-суммой (из-за выдаваемой консольным соединениям в качестве issue даты компиляции)).

      • Следующая мысль (уже от отчаяния) -- что дело в устройстве. Вместо CANDAC16 попробовал CAC208 -- фиг, то же самое.

        (Неудивительно -- ВЧ300, управляемые CANDAC16, тоже были в числе жертв.)

      Загадка -- прямо впечатление, будто что-то сверхъестественное происходит. Но чудес ведь не бывает, ДОЛЖНА быть причина.

    • Сегодня, наконец, получил возможность прогнать тест на живой установке. Для этого собрал v4c4lcanserver с отладочной печатью и запустил его вручную на cangw-rst4n5.

      Так-то и нашёл причину.

    • А суть оказалась в том, что у себя на x10sae (+CANGW) я тесты гонял с конфигом test-devlist-cdac20k-x10sae.lst, в котором есть указание tac=10, а в devlist-canhw-11.lst и devlist-canhw-12.lst ничего такого нет (т.к. при экспериментальной работе с cdac20k число шагов разгона было намертво зашито в драйвер, и при внедрении драйверов на advdac в конфиги никаких "tac=" добавлено не было).

    26.11.2017: исправляем.

    • Всё тривиально: условие разбито на 2, и при неуказанном tac возвращается DO_CALC_R_SLOW_MOVE.
    • Собственно, оно так просто потому, что код в HandleSlowmoHbt() при tac<=0 использует только .spd, игнорируя поля для "плавности".
    • ...но содержимое *rp всё же нулится, чтобы избежать обращения к неинициализированным значениям (мусор из стека -- мало ли, вдруг какой-нибудь valgrind сочтёт трогание этого мусора ошибкой).

    Проверено, работает.

    27.11.2017: и на пульту задеплоено -- тоже работает.

  • 25.02.2017: Некоторые соображения на тему длины очереди отправки в SocketCAN (с проблемами которой мы уже сталкивались в v5gid25s на v2k-k500-1). (10.04.2024: может, "v4gid25s"?)

    25.02.2017: собственно (всё в разное время дня и в разных местах, по мере прогресса):

    • @утро-по-пути-в-ИЯФ-мимо-НИПС: ведь при переходе на socketcan будут проблемы из-за всего лишь 10 пакетов, помещающихся в выходной буфер чипа (в отличие от мамкинско-селивановской внутренней очереди пакетов на 400).
    • @физио: ну дык можно отдельную очередь создать в cankoz, в которую перемещать пакеты при ошибке отправки. Но надо бы не более 1 штуки на каждый kid. И при такой ошибке заказывать дескриптор на запись -- когда будет готов, то уведомиться и начать отправлять.

      Возможно, потребуется некоторое сотрудничество от sendqlib'а -- sq_sendnext()? @вечер: а он уже есть, и, кстати, он void.

      Также некоторый вопрос -- а как эта "врЕменная ошибка отправки" будет влиять на результат sq_enq() (результат проверяется при всяких REPLACE_NOTFIRST), когда мы ставим первый пакет и он тут же попытается отправиться?

      @вечер: Так вот: sq_enq() никак не светит наружу результат отправки (вот незадача -- его и cankoz'у придётся узнавать окольными путями), а возвращает только результат постановки в очередь.

    • @по-пути-с-физио-вдоль-забора: а можно не пакеты в очередь помещать, а прямо сами kid'ы -- тогда автоматом будет по 1шт/kid; плюс, если устройство за это время отпадёт, то и автоматом не потребуется вычищать его пакеты из очереди; плюс#2 -- при очистке kid'овой очереди также не понадобится отдельно вычищать его пакет из line'овой.
    • @вечер: Одна маленькая печаль: как показал тест -- 12 подряд send()'ов с предварительной проверкой готовности select()'ом, сокет ВСЕГДА считается готовым на запись, даже если попытка записи тут же обломится по "No buffer space available".
    • @вечер, далее: Стал гуглить, как же решить эту проблему. И вместо решения "как узнать, что в сокет можно писать" -- наткнулся на решение проблемы длины выходного FIFO:
      ip link set can0 txqueuelen 30
      (вместо "30" -- да хоть 1000. И, кстати, "30" почему-то позволяет засунуть 31 пакет, и только на 32-м ругается "No buffer space available".)

    28.02.2017: сделано простенькое дополнение в ifup-can: при наличии в ifcfg-can* параметра TXQUEUELEN оно выставляет интерфейсу указанное значение.

  • 27.02.2017: забавный косяк: после нажатия на кнопку Reset на морде (да и при прочих причинах is_a_reset) каналы HW_VER и SW_VER остаются гореть болотным.

    Причина крайне проста:

    1. Они помечены как IS_AUTOUPDATED_TRUSTED -- и возвращаются ТОЛЬКО из _ff() (в _rw_p() стоит возврат из кэша, но сервер запроса не пришлёт).
    2. А в cankoz_fd_p() вызов ffproc() стоит ДО дёрганья ->NOTREADY,->OPERATING.

    Вот оно сначала возвращается новое значение, потом тут же болотовеет, и уже более никогда новое не возвращается.

    И что тут можно сделать?

    11.04.2018: да, проблема есть, и дальше будет только хуже: получается, что ЛЮБЫЕ каналы, помеченные IS_AUTOUPDATED_*, которые драйвер будет возвращать по собственной инициативе в _ff()::is_a_reset==1, так и будут навеки оставаться болотными (или пока по иной причине не обновятся).

    • Вернуться к этому вопросу заставило написание canipp_drv.c -- там по is_a_reset надо сбрасывать фон, отдавая наверх соответствующие каналы bias*.
    • В порядке историографии: такой порядок -- сначала вызывается _ff() (бывший _rst() в v2), затем щёлкается статус ->NOTREADY->OPERATING -- был принят еще 03-04-2006, и в bigfile-0001.html за 27-03-2006 можно посмотреть причины.
    • Как решить проблему?
      1. Первая идея: реагировать на все эти каналы в _rw_p().

        Но не поможет -- на эти каналы и запросы-то присылаться не будут: коль помечены "IS_AUTOUPDATED", то и отдавать надо всегда самостоятельно.

      2. Следующая идея -- некрасивая, но вроде должна сработать: отдавать не из _ff(), а из _in() -- где самостоятельно ловить GETDEVATTRS и код причины, приводящий к is_a_reset.

        Оно, конечно, криво -- теряется смысл _ff()'а. Но на безрыбье...

      А вообще надо б "на досуге" поразмыслить, как эту проблему -- уведомление драйвера и ПОСЛЕ щелканья тоже -- решить покрасивше.

    12.04.2018: да, поразмыслил в фоновом режиме -- проклюнулась идея: пусть драйверы имеют возможность указать, КОГДА надо дёрнуть ихнюю _ff(): ДО щёлканья статусом или ПОСЛЕ.

    • Надо, чтоб драйвер мог сам ЯВНО указать, когда именно его интересует уведомление.

      И чтоб мог прямо парой битовых флагов указать, когда надо дёргать:

      • 0,0 -- по умолчанию.
      • 0,1 -- ДО.
      • 1,0 -- ПОСЛЕ.
      • 1,1 -- 2 раза.

      И чтоб _ff()'у как-нибудь передавалась стадия. Можно прямо теми же битами в is_a_reset -- оно всё равно в качестве булевского работать будет.

    • И более того: желательно б сделать, чтоб прямо по умолчанию дёргалось бы именно "после" (как было до изменения 25-06-2010).

      (Тогда проблема болотовеющих 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!).
    • Добавлять метод в lvmt -- плохо, поедет версия интерфейса.
    • Как-нибудь хитрО присоседить к другому параметру?

      А к какому?

      • Вроде бы devcode однобайтный; но он может быть -1, а флаги могут потребоваться и таковым.
      • Еще есть только queue_size -- но тоже криво.

    08.08.2018@утро-душ: можно просто не продвинуть CANKOZ_LYR_VERSION_MAJOR с =2 на =3 и добавить поле options методу add().

    Сейчас, во время летнего перерыва, это вполне безопасно.

    13.08.2018: делаем.

    1. cankoz_lyr.h:
      • 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 у незаинтересованных драйверов.

    2. cankoz_lyr_common.c:
      • К kozdevinfo_t добавлено поле options.
      • К cankoz_add() -- надлежащий параметр options, который и сохраняется в поле options.
      • В cankoz_fd_p(): собственно выполнение того, ради чего вся бодяга и затевалась.
        • Махинации делаются ТОЛЬКО при is_a_reset. В противном случае значения тех 2 битов опций игнорируются и делается всё по-старому.
        • Если же и is_a_reset, и биты ненулевые, то в зависимости от наличия конкретного флага вызов делается ДО и/или ПОСЛЕ, с передачей в качестве значения параметра is_a_reset именно соответствующей маски (так что сами _ff() по-прежнему использовать этот параметр как булевский).
    3. Драйверы: пока что всем добавлено в add() просто CANKOZ_LYR_OPTION_NONE.

    Теперь осталось драйверам повставлять нужное... Или еще порядок вызова поменять -- чтоб по умолчанию было после?

    13.08.2018@после-обеда: да, рискнём -- поменяем умолчание и понавставляем в драйверы, требующие именно пред-запроса, проверок.

    1. В cankoz_fd_p() умолчание изменено -- теперь обычный вызов делается ПОСЛЕ дёрганья статуса устройства.

      Ура! Каналы HW_VER и SW_VER больше не остаются болотными после нажатия кнопки Reset.

      ...хотя каналы out_tab_errdescr остаются болотными. Но оно так и в предыдущем варианте было -- это уже косяк связки драйверов+advdac_cankoz_table_meat.h.

    2. Небезразличные к месту вызова драйверы:
      • cgvi8_drv.c -- в опциях указывает BEFORE|AFTER, а сброс флагов выполняет только при is_a_reset==BEFORE.
      • curvv_drv.c -- идентично.
      • panov_ubs_drv.c -- тут достаточно было указать add()'у флаг BEFORE, поскольку ничего кроме внутреннего сброса _ff() не делает (каналов HW_VER/SW_VER просто нет).
      • smc8_drv.c -- аналогично первой паре.
      • tvac320_drv.c -- тоже как почти все.

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

  • 26.04.2017: знатный косяк в драйвере CDAC20:
    1. Значение MIN_ALWD_VAL =-10000305, но в этом устройстве используется 24-битная кодировка ЦАПа (а не 16-битная), поэтому реальный минимум именно ровно -10000000.

      Что интересно, этот косяк еще со времён v2'шного xcdac20_drv.c, но в первоначальном варианте cdac20_drv.c использовалось именно правильное -10000000 (этот драйвер делался в апреле 2008-го, и с задним числом некоторым вниманием к деталям кодировки -- т.к. там иной порядок байтов, то изначально оно не работало; см. bigfile-0001.html за 29-04-2008).

    2. Квант еще со времён v2 считается за 305 микровольт. Но ведь тут он намного мельче, чуть ли не 1 микровольт (чуть крупнее: диапазон [-10_000_000...+9_999_999]=20_000_000 микровольт маппируется на 2^24=16_777_216 квантов, итого около 1.19uV/квант).

      Причём этот вопрос также упомянут в bigfile-0001.html за 04-05-2008.

    Вылезло это по причине 1-го глюка: при тестировании 21-04-2017 связки iset_walker+formula на живом CDAC20 было узрено, что уставка (из-за неучёта R заданная ниже -10V) плавно едет к -10V, а потом перескакивает в +10V и опять плавно едет вниз, и так почти до бесконечности (но через сколько-то циклов стабилизируется -- видимо, всё же в какой-то момент оно из-за плавной прецессии (чего, кстати?) как-то умудрялось подойти к "цели" ближе, чем rate/10).

    26.04.2017: косяк увиден в пятницу 21-го, а руки дошли разобраться только сегодня.

    Вся механика произошедшего (происходящего?) понятна, вопрос лишь в том, что и как исправлять. Есть несколько аспектов:

    1. Диапазоны:
      1. Как, собственно, происходит переполнение из-за неверной нижней границы:
        • Значения <-10000001 (-10000002 и далее) дают переполнение при конвертации в cdac20_val_to_daccode()
        • Так, корректное -10000000 даёт код 0x000000, а -10000002 -- уже 0xFFFFFFFF.
        • Но 0xFFFFFFFF очевидным образом вылезает из 3 байтов, а отправляются-то в SendWrRq() именно 3 байта, что в случае с 0xFFFFFFFF даёт 0xFFFFFF, что соответствует значению +9999998.
        • Таким образом, при плавном изменении оно всё пытается подойти на расстояние ближе 1 шажка (rate/10) к цели, равной -10000305, постоянно перепрыгивая на ~+10V, и так постоянно гоня значение вниз и вниз (пока случайно не окажется в ближе rate/10).
      2. MIN_ALWD_VAL-то в cdac20_drv.c надо ставить равным -10000000. Тут без вариантов.
      3. MAX_ALWD_VAL же, кстати -- +99999998, т.к. до него округляется при записи +9999999 (код 0xFFFFFF).

        ...Или переделать конверсию?

        27.04.2017: кстати, в кодировке ЦАПа CDAC20 есть какие-то странности -- см. ниже запись за сегодня.

    2. С квантами всё сложнее.
      1. В самом cdac20_drv.c надо ставить квант равным 2.
      2. А в драйверах-"клиентах" как?
        • В ist_cdac20_drv.c -- видимо, тоже 2. Т.к. он работает ТОЛЬКО с CDAC20/CEAC51.
        • А в iset_walker'е?
      3. По-хорошему -- надо бы чтоб квант брался от target-канала.
        • Для чего потребуется содействие от vdev:
          1. Поле vdev_sodc_cur_t.q.
          2. Какой-нибудь "уведомлятор" -- callback. Причём дополнительный к уже существующему, т.к. в vdev_sodc_cb_t никакого "reason" не предусмотрено -- это чисто уведомление об обновлении данных.

          ...или брать в обход vdev -- напрямую у cda, спрашивая cda_quant_of_ref(me->cur[n].ref)? Но как понять, КОГДА можно спрашивать?

        • ...но в случае с искусственными каналами -- вроде formula и double_iset -- и это не поможет, т.к. у них (формульных и де-факто вещественных) никакого кванта и нет.

    27.04.2017: кстати, в описании CDAC20 имеется странная табличка (в описании CEAC51 идентичная):
    Код (16-ричный)Напряжение
    FFFFF8+10 В
    8000005 мкВ
    7FFFF8-5 мкВ
    000000-10 В

    Это странно, т.к.:

    • Как соответствующий +10В указан код FFFFF8. А код FFFFF9 чему будет соответствовать? А FFFFFF? Или они считаются запрещёнными?
    • Что является нулём?

      Из таблицы следует, что 0x80000 соответствует +5uV, в то время как у нас в драйверах оно считается нулём, да и из блока при включении питания читается этот код, соответственно, должный соответствовать нулю.

    Козаку написано письмо с этими вопросами, ждём ответа.

    28.04.2017: ответного письма не получилось (почти :)), вместо него был продолжительный разговор по телефону.

    • Главное: ЦАП вообще 20-битовый!

      Точнее, 21 бит включая знак.

      Соответственно, младшие 3 бита в коде вообще игнорируются.

    • Числа в таблице -- строго говоря, совсем не абсолют. Т.к. каждый экземпляр устройства проходит ручную настройку самим Козаком (или Чекавинским).

      Поэтому на все эти "ой, ошибка на один битик!!!" можно забить.

    • Ноль -- да, в позиции 0x800000.
    • Также Козак прислал выдержки из своего кода для утилитки-подсматривателя, выводящей коды из полученных пакетов в человекочитаемом виде (эти выдержки приведены тут в тексте внутри HTML-комментариев).

      Так вот: там вообще другая формула пересчёта используется, в несколько стадий, а не наше "маппирование диапазонов через scale32via64() со сдвигом нуля в 0x800000".

      Так что педантичная точность "до битика" неясно, насколько уместна (впрочем, кода пересчёта, работающего в самом CDAC20, у меня нет).

    Итого:

    • Пересчёт менять не надо -- он и так в порядке.
    • Поменять надо границы в микровольтах, чтобы они соответствовали кодам 0x000000 и 0xFFFFFF.
    • Квант сделать равным 2.

    Так и сделано, и в cdac20_drv.c, и в cdac20k_drv.c.

  • 24.05.2017: надо бы готовиться к поддержке таблиц. А для них надо будет также драйвер, шлющий бродкасты (общий старт, общий стоп, ...). Т.е. -- БЕЗадресный.

    24.05.2017: ТУТ СЛОВА О КАРТЕ КАНАЛОВ (w10i) И ОБ УКАЗАНИИ "МЕТОК" -- что можно иметь "текущую", можно указывать её в auxinfo, а можно передавать значением прямо в команде записи (но лучше бы -- фиксированно, чтоб вся такая инфа была бы в конфиге); и что табличности у ЦАП-драйверов тоже должны б иметь метку.

    25.05.2017@утро: на первый взгляд -- несложно, просто взять да написать.

    25.05.2017: однако, нет! Ведь он должен быть БЕЗ устройства, а таковое в архитектуре kankoz_lyr никак не предусмотрено.

    • Просто отправить пакет -- наверное, как-то можно.

      Хотя можно ли, БРОДКАСТНЫЙ-то? Есть ли такая возможность?

    • Но вот КУДА отправить? Ведь линия-то может быть и вовсе не открыта!

    25.05.2017@вечер-офис-Бозона: и чо -- заводить какую-то дополнительную инфраструктуру, дополнительный вектор "безадресных" устройств, чтобы и их тоже проверять при закрытии линии (что её НЕ надо закрывать)?

    Как-то кривовато...

    26.05.2017@утро-дома: есть вариант красивее:

    1. Если адрес устройства (kid) указан как -1, то оно считается безадресным.
    2. Устройства такие помещать В ТОТ ЖЕ массив lineinfo_t.devs[], только массив увеличить вдвое и "безадресные" устройства помещать в диапазон [64...127].
      • Считаем, что дополнительных 64 штук должно хватить -- ибо это получится по 1 штуке безадресной на 1 обычный, явно более чем достаточно.
      • Тем самым "особость" таких устройств нивелируется -- у них аналогичные методы работы с очередью.
      • И проблема "как указать, что слать с CANKOZ_PRIO_BROADCAST?" решается автоматом -- для безадресных устройств просто оно и будет ставиться вместо CANKOZ_PRIO_UNICAST.
      • Единственное что -- сами драйверы должны будут аккуратно пользоваться постановкой в очередь:
        1. Команды вроде "старт" надо заказывать с SQ_TRIES_DIR (чтоб если посылка обломится, то сразу и с концом, а не пыталась бы слать "потом", когда очередь очистится).
        2. Команды вроде "стоп" надо заказывать с SQ_TRIES_ONS -- чтоб гарантированно отработались.

        И более ничего -- т.к., "подтверждения" на broadcast-команды быть не может.

      • ...а "методы" ffproc и inproc просто не используются.
    3. DECODE_AND_CHECK() превратится в 2 разных варианта: проверка для обычных драйверов (kid должен быть в диапазоне [0...63]) и для "всех" ([0...127]).

      Почти все вызовы будут годны "для всех", но вот add_devcode(), get_dev_ver() и поддержка регистров -- только для адресных.

    4. ??? (что тут должно было быть?)

    Но есть сложности.

    • Указание "адреса"-то сейчас делается просто передачей пары businfocount,businfo[], откуда сам cankoz_lyr и берёт нужную информацию.

      Посему указывать "-1" нет никакой возможности.

    • Варианты решения:
      1. Считать, что если указано businfocount=-1, то брать line из businfo[0], а драйвер-клиент заботится о проверке businfocount сам.

        Но это криво.

      2. Расширить API: чтобы cankoz_add()'у передавалось бы еще что-то -- options, "driver class", ... -- откуда можно будет понять, что это БЕЗадресный драйвер и трактовать его надо иначе.

      Корректнее использовать 2-й вариант. Да, заодно продвинем CANKOZ_LYR_VERSION_MAJOR с =2 на =3 (хотя сейчас и неактуально, т.к. не-CANGW-драйверов в эксплуатации и нет (кроме недоделанного на ЛИУ-2), но так правильнее).

    26.05.2017: поговорил с Беркаевым об опыте работы с таблицами на ВЭПП-2000.

    1. Да, они ими пользовались. Причём -- ОДНОступенчатыми.

      На ВЭПП-5 пока вроде не надо, но может понадобиться.

    2. А вот запуск исполнения повсеместно был unicast'ным, broadcast'ы же не использовали. Причина -- на одной CAN-линии сидели устройства от разных подсистем, и сложно было сделать неперекрывающиеся маркеры таблиц.
    3. А у повторного запуска таблиц -- нынешнего козачиного формата -- есть один крайне нежелательный эффект: поскольку там записаны не конечные значения, а ИНКРЕМЕНТЫ, то повторный запуск может вывести значения за разрешенные рамки и в результате -- убить пучок, сжечь катушки, ...

    Вывод:

    1. Поддержку таблиц делать НАДО.
    2. А вот поддержка бродкастов -- похоже, пока не очень актуальна.

      Но если понадобится -- то практически готовый проект решения вон есть.

    02.06.2017: кстати, есть при использовании бродкастного управления таблицами один аспект: поскольку оно происходит в обход драйвера, то оный драйвер будет не в курсе изменения состояния устройства.

    • Похожий аспект есть давно/изначально: ведь "останавливается" исполнение таблицы по её исчерпанию тоже не по команде драйвера. Но при окончании устройство само присылает пакет 0xFD (DESC_GET_DAC_STAT), анализируя биты статуса в котором драйвер приводит своё знание в соответствие с текущим состоянием устройства.

      Кстати, там (в v2'шных xcac208/xceac124) при окончании исполнения таблицы она из устройства тут же стирается, что похвально.

    • Так вот: надо во время "существования таблицы" -- т.е., в состояниях LOAD, ACTV, RUNN, PAUS -- постоянно отслеживать состояние, периодически запрашивая 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'ных драйверов в связи с воскрешением ТНК, где для подъёма энергии оно может понадобиться.)

  • 02.06.2017: займёмся-ка, с горя (что никак не выходит добить "плавный" разгон и торможение в cdac20k), реализацией поддержки таблиц. Там-то ничего шибко сложного нет -- в основном копировать уже готовые решения, адаптируя их под другую модель каналов.

    Делать будем в том же cdac20k_drv.c, который всё равно сейчас пилится.

    И да -- для описания работ заводим отдельный раздел, ибо предыдущий в основном касается инфраструктуры broadcast'ных драйверов.

    02.06.2017: потихоньку готовимся.

    • Поскольку в момент активации таблицы делается куча проверок, могущих заканчиваться отлупом, то разумно будет сделать текстовый канал, в который отдавать человекочитаемое описание ошибки (в т.ч. с номером линии и, возможно, с номером шага (если где-то окажется превышение разрешенной скорости)). (В v2'шных эта диагностика шла через 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 частей:

    1. Определения -- точнее, определениЕ: cankoz_out_ch_t.
    2. Код для работы с плавным изменением каналов.
    3. Код для работы с таблицами.

    Да, из этого первый пункт -- собственно ".h-файл" (интерфейс), а вторая пара -- реализация; 04.06.2017@на-работе, в 13-м:да-да, та самая, которая (тогда еще только в отношении таблиц? или как?) была запланирована для реализации в cankoz_lyr еще в 2009-м.

    И еще некоторые соображения:

    • Функциям-"библиотечным методам" надо будет передавать только указатель на описатель канала (cankoz_out_ch_t) и информацию, годную для вызова "методов-реализаций" конкретного драйвера -- конкретно SendWrRq(). Оная информация включает в себя devptr (а не me!) и номер линии (самим "библиотечным" не нужный).
    • И да:
      • Это всё, казалось бы, напрашивается на ООП-style-реализацию, где класс-предок (виртуальный) содержит "мозги", а класс-потомок -- знание о реализации в конкретном типе устройства.
      • Причём, с МНОЖЕСТВЕННЫМ наследованием: плавное изменение -- один виртуальный предок, таблицы -- другой (хотя они и кооперируются через aff/lkd/isc).

      НО!

      • Из-за переменного количества каналов таковую реализацию делать было бы неудобно.
      • Зато в стиле pzframe -- "конкретный тип содержит массивы нужного размера, плюс стандартизованный описатель, содержащий указатели на массивы" (как pzframe_retbufs_t в pzframe_drv, cur_data[]/prv_data[] в pzframe_data; да и в vdev_context_t используется аналогичная схема) -- вполне сработало бы.

        Но громоздковато и неудобно -- проще прямо так, с недоинкапсуляцией.

    • Постараться в тот кусок (будущий отдельный .h) вытащить всё по максимуму -- включая не-тривиальные фрагменты из нынешних _in() и _fd_p().
    • Кстати, при правильной (аккуратной) реализации оно ведь вполне подошло бы не только для CAN, но и для любых иных ЦАПов -- да хоть CAMAC'овских.
    • Поэтому надо будет переименовать 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, чтоб в ём можно было указывать идентификатор таблицы.
    • Добавляем в privrec массивы. И тут есть пара существенных отличий от v2:
      1. Массив "времён" идёт ОТДЕЛЬНО, времена НЕ указываются в самих таблицах с вольтами.

        Следствие: массив t_times[] становится одномерным и проверять консистентность времён разных каналов не требуется ввиду отсутствия этих РАЗНЫХ времён.

      2. В v4 векторные каналы могут быть переменной длины.

        Следствие: неуказанность каналов для работы в таблице можно определять по нулевой длине их индивидуальных векторов. А по значению _VAL_DISABLE_TABLE_CHAN -- можно оставить для канала _TAB_ALL (т.е., в индивидуальных оно пусть тоже работает, но особого смысла нет).

    07.06.2017@вечер: ПЛОХО ВСЁ! Сложно заунифицировать табличный код всех типов козоЦАПов, т.к. у них разный формат таблиц, в т.ч. разный формат упаковки значений, а также формально разными могут быть даже CAN-команды работы с таблицами.

    Да и глубокой потребности нет -- использование таблиц минимально, и неохота вкладываться в реализацию технологии, у которой пол-потребителя.

    Проще оказывается делать всё кодом прямо в драйверах, "параметризуя" при помощи текстового редактора (при правильном выборе структуры и имён работа сводится к копированию и контекстной замене) и препроцессора.

    08.06.2017@утро: а ведь некорректно ВСЁ сваливать в один файл "advdac", ибо:

    • Если плавное изменение имеет смысл для любого ЦАП, то работа с таблицами -- специфичность козачиных CAN-устройств.
    • Да и НИЗКОУРОВНЕВАЯ реализация плавного изменения (на уровне -- CAN-команд) тоже специфична для CAN-устройств (и, в принципе, может иметь смысл даже для не-совсем-ЦАПов, вроде GVI, PKS, etc.).

    Поэтому надо делить файл на 3 части:

    1. advdac.h -- интерфейс.
    2. advdac_slowmo.? (?=[ch]) -- реализация плавного изменения.
    3. advdac_cankoztab.? -- работа с таблицами.

    Так что:

    1. Первая пара может пока жить и внутри одного файла (как изначально планировалось), но низкоуровневая работа с CAN пусть пока лежит на самом конкретном драйвере.

      Если когда-то вдруг припрёт -- то, в принципе, можно это всё запараметризовать: указанием варианта "структуры таблиц", типов конверсии, ...(что еще?) -- как оно "планировалось" в 2009-м при обдумывании СУ для ТНК. Реализация этой параметризованности свалится в отдельный файл (advdac_slowmo_cankoz.??), код в котором должен будет знать и поддерживать все варианты.

    2. Реализация же таблиц вообще пока пусть будет ЦЕЛИКОМ на конкретном драйвере.

      ...опять же, параметризовать -- тоже можно, но особого смысла не видно.

    08.06.2017: посему всю дальнейную деятельность по таблицам будем записывать в отдельном разделе, а этот оставим для модульной структуры будущего "advdac в целом".

    17.06.2017@дома-жара: а ведь варианты "плавного изменения" вовсе не обязаны быть фиксированы только как "прямолинейное" и "с ускорением"!

    Учитывая, что реализация будет жить в отдельном (.h?) файле, можно делать их РАЗНЫЕ, #include'а нужный конкретному драйверу.

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

    21.06.2017: теперь, когда таблицы в основном вроде бы -- работают что дальше делать? Правильно -- рефакторинг, чтобы подготовить код к вытаскиванию в отдельные файлы.

    1. Часть первая -- нумерологическая именовальческая: забота об удобных именах.
      • Предварительное действие: все table-related константы получили в имя префикс "TABLE": CDAC20_TABLE_MAX_NSTEPS, CDAC20_TABLE_MAX_STEP_COUNT, ...
      • А уже содержавшие это словцо VAL'ы приведены, перестановкой префикса, к тому же стандарту:
        • CDAC20_VAL_TABLE_DISABLE_CHAN -> CDAC20_TABLE_VAL_DISABLE_CHAN
        • CDAC20_VAL_TABLE_START_FROM_CUR -> CDAC20_TABLE_VAL_START_FROM_CUR
    2. Параметризация:
      • Все устройство-специфичные константы --
        1. общие характеристики устройства -- конкретно количество каналов (*_CHAN_OUT_n_count);
        2. вышеупомянутые table-related (количество шагов, значения, ...);
        3. (но значения -- MIN_VAL, MAX_VAL, THE_QUANT -- пока НЕТ)
        -- для общего кода alias'ятся в enum- (НЕ #define-!) константы с префиксом DEVSPEC (на него заменяется имя устройства): например, DEVSPEC_TABLE_MAX_NSTEPS=CDAC20_TABLE_MAX_NSTEPS.

        (Сначала хотел использовать префикс DEVTYPE, но он менее однозначен; а слово "DEVSPEC" используется только в uspci_test.c.)

      • За компанию также сделана и DEVSPEC_CHAN_ADC_n_count.
      • И, в пароксизме унификации, произведена ТОТАЛЬНАЯ контекстная замена: теперь во всём последующем коде драйвера заменено CDAC20_ на DEVSPEC_.

        Смысл:

        1. Тем самым подготовлен код для вытаскивания в advdac*.h.
        2. Остальной код, который останется в специфичных c*_drv.c, станет унифицированным и в дальнейшем при внесении изменений и при распространении их на соседние драйверы не придётся заниматься контекстной заменой.
    3. Перетаскиваем куски кода ближе к началу, где их потом можно будет целиком вырезать в отдельные файлы.
      • Будущий advdac.h -- самое простое: это лишь typedef на advdac_out_ch_t, он давно готов.
      • Будущий advdac_slowmo_kinetic_meat.h пока не трогаем (сначала доразберёмся с косяками кинетики).
      • Будущий advdac_cankoz_table_meat.h -- оно должно жить уже ПОСЛЕ определения privrec'а и необходимых сервисных функций, вроде SendWrRq()/SendRdRq().

        Всё перетащено в один последовательный кусок.

        Туда вошли ReportTableStatus(), SetTmode(), EraseTable(), DoTabActivate() и SendTabCtlCmd() с подмастерьем OnSendTabCtlCmdCB().

        А вот куски, сейчас являющиеся фрагментами других функций -- в первую очередь, _in() и _rw() -- пока нет.

    22.06.2017: продолжаем рефакторинг.

    1. (continued)
      • Из _hbt() вытащены в HandleTableHbt() вычитывание каналов и запрос статуса ЦАПа.
      • Из _in() вытащена в HandleDESC_FILE_CLOSE() и HandleDESC_GET_DAC_STAT() обработка соответствующих пакетов.
      • А вот _rw()'шное содержимое не очень ясно, как вытаскивать: там и много очень, и параметров валом -- которые dtypes[],nelems[],values[], и ведь просто индексироваться внутрь них нельзя (при action==DRVA_READ они =NULL).

        Пока напрашивается это:

        • то ли в #define вытащить (но ОЧЕНЬ неудобно -- и syntax highlight не будет, и диагностика отврат),
        • то ли в отдельный .h-файл, который #include'ить прямо там -- тоже кривизна...

        Нет,

        • Всё-таки надо это делать функцией.
        • ПРОСТО ПЕРЕДАВАТЬ ей "базовые указатели" на массивы и текущий n.
        • ...Где-то такое решение уже встречалось, но вот где -- никак не могу вспомнить. Похоже, это в cxsd_hw.c::ConsiderRequest().

        Итого -- вынесено в HandleTable_rw(), которой передаются как action и chn, так и n и изначальные указатели dtypes,nelems,values. А она внутри себя содержит дополнительные проверки на тему "правильных" dtype,nelems (только INT32-скаляры для всех, кроме векторов, для которых INT32 без учёта nelems).

      • И всё-таки всё касательно slowmo тоже вытащено. А именно:
        • Запись в _CHAN_OUT_n... -- в HandleSlowmoOUT_rw().

          И за компанию запись в _CHAN_OUT_RATE_n... -- в HandleSlowmoOUT_RATE_rw() (изменение скорости в процессе движения тоже надо будет отрабатывать -- оно оговаривалось, просто пока не сделано).

        • _hbt()'шная работа по изменению -- в HandleSlowmoHbt().

    23.06.2017: далее продолжаем рефакторинг.

    1. (continued исчо)
      • Всё про slowmo:
        • Для реализации свежевведённых _CHAN_OUT_IMM_n... и _CHAN_OUT_TAC_n... сделаны HandleSlowmoOUT_IMM_rw() и HandleSlowmoOUT_TAC_rw().
        • И по ходу реализации первого из вышеупомянутых стало очевидно, что код в _in()'е, обрабатывающий получение пакета от ЦАПа, тоже надо вынести в стандартное место -- кусок ПОСЛЕ декодирования данных (т.е. , собственно обработку ФАКТА получения).
          • Оный функционал вынесен в HandleSlowmoREADDAC_in().
          • В "предоставляемый драйвером функционал" добавилась функция val_to_daccode_to_val(). Что делает, ясно из названия, а нужна для имитации "получения последнего пакета" в момент получения ПЕРВОГО ответа от устройства (в этот момент и делается возврат значения канала уставки -- чтоб значение считалось изменившимся только после реального начала модификации в железе).
    2. Разделение по файлам.
      • advdac.h -- 33 строки
      • advdac_slowmo_kinetic_meat.h -- 359 строк
      • advdac_cankoz_table_meat.h -- 588 строк

      Итого -- 980 строк; сам же cdac20k_drv.c стал всего 919 строк.

    Засим покамест всё.

    Но наверняка будут еще какие-то действия/тюнинг/оптимизации.

    23.06.2017: теперь адаптируем к этой архитектуре прочие драйверы.

    • В список этих "прочих" сейчас входят CANDAC16, CAC208 и CEAC124.
    • Потенциально годные CGVI8 и CPKS8 -- нет.
      • Во-первых, СЕЙЧАС незачем (особенно ГВИ).
      • Во-вторых, у них даже внутренняя архитектура иная -- нет SendWrRq()/SendRdRq(), а вместо этого просто отправка пакетов прямо из _rw_p().
      • Кроме того, в cgvi8_drv.c используются двойные каналы -- и в 100ns-квантах, и в сырых кодах

    Собственно действия по адаптации:

    1. _drv_i.h-файлы -- дополняем OUT_IMM'ами и OUT_TAC'ами.

    26.06.2017: далее адаптация.

    1. Исправляем сомнительную ситуацию, что далеко не во всех именах каналов есть буквосочетание "tab":
      1. *drv_i.h:
        • 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
      2. *.devtype:
        • table_id - > out_tab_id
        • tab_times -> out_tab_times
        • tab_all -> out_tab_all
        • tab0 -> out_tab0
        • tab_errdescr -> out_tab_errdescr

      Полученный результат тоже не сказать, что особо красив (в первую очередь потому, что каналы-команды имеют имена безо всякого "out"); но всё более однозначно.

    2. .devtype-файлы:
      • ИНФО_ПО_КАНАЛАМ (кстати, в _drv_i.h тоже).
      • Каналы таблиц.
      • Каналы imm и acc

    27.06.2017: далее (реально делалось равномерно за 3 дня -- также и 28.06.2017 и 29.06.2017).

    1. Теперь уже код самих *_drv.c (*={candac16,cac208,ceac124):
      • Добавлен THE_QUANT -- у этой троицы он у всей 305.
      • Имевшийся куцый enum с параметрами таблиц заменён на полноценный с 5+2 штуками devname_TABLE_.... Значения для конкретно devname_TABLE_MAX_FILE_BYTES посмотрены в документации.
      • Добавлены alias-спецификации DEVSPEC_*.
      • Добавлены val_to_daccode_to_val().
      • Замена cankoz_out_ch_t на advdac_out_ch_t, беримый из за-#include'нного advdac.h.
      • В privrec добавлены все table-related поля.
      • ...плюс min_alwd/max_alwd.

        Но давно напрашивается сделать min_alwd/max_alwd per-channel-параметрами (чтоб и для многоканальных были б доступны).

        29.06.2017: сделано повсеместно. И даже в табличных каналах теперь оно проверяется (правда, в момент записи канала, а не в момент активации).

      • _init_d(): добавлены SetChanRDs() (на _IMM и на табличные) и SetChanQuant().
      • ...заодно, кстати, повсюду "305" заменено на THE_QUANT.
      • _ff(): supports_unicast_t_ctl выставляется в зависимости от sw_ver.
      • Собственно использование:
        1. _hbt(): переведены на HandleSlowmoHbt() и HandleTableHbt().
        2. _in(): переведены на HandleSlowmoREADDAC_in(), а также добавлены HandleDESC_FILE_CLOSE()
        3. _rw_p():
          • Во-первых, повыкинут артефакт 2015 (или 2014?) года, когда драйверы под v4 только начинали писаться -- за-#if-0'енный возврат "значение, равное номеру канала".
          • Во-вторых, в начало тела цикла перебора запрошенных каналов добавлены более хитрые проверки -- поддерживающие векторные каналы для таблиц.
          • Обработка каналов CHAN_OUT_*_n... (*={,RATE,IMM,TAC}) переведена на HandleSlowmoOUT*_rw().
          • Обработка каналов таблиц добавлена.
          • Мелочь: убраны отдельные else-if'ы на KOZDEV_CHAN_ADC_MODE и KOZDEV_CHAN_OUT_MODE, т.к. они теперь каналы чтения и полностью обслуживаются общей веткой по CONFIG-каналам.
      • Возможность указания параметров через auxinfo -- затрагивает _params[] и _init_d().
        • Во все драйверы распространён подход из candac16_init_d(): указанное для 0-го канала копируется в свойства других лишь если для них это свойство не указано.

          Таким образом, можно указывать либо device-global (spd=...), либо индивидуально (spd5=...). Но если надо указать для 0-го канала, то оно становится общим для всех, и при нежелательности оного надо указывать всем в обязательном порядке.

        • Переделано, что можно указывать tac=-1 -- тогда оно считается за ==0, а из out[0].tac НЕ копируется.
        • Сами параметры (их теперь 4 штуки: spd, tac, min, max) в _params[] указываются через макрос ONE_LINE_PARAMS() -- иначе было б слишком длинно (кроме одноканального cdac20).
      • Неприятность конкретно с CANDAC16: у него нет команды 0xFD (GET_DAC_STAT), а есть только 0xFE (GETDEVSTAT), хотя и содержащая ровно ту же информацию, что 0xFD у прочих.

        Но! Ведь при работе с таблицами эти команды используются раздельно и по-разному: в очередь DAC_STAT кладётся ons'ом, а DEV_STAT -- INF'ом.

        • На время переделки -- для собираемости -- было просто сделано DESC_GET_DAC_STAT=CANKOZ_DESC_GETDEVSTAT; и в качестве GET_DAC_STAT это будет работать.
        • Но напрашивается, что в качестве сеператора при загрузке таблицы (кем только и работает GETDEVSTAT) нужно какой-то другой код использовать, чтоб он помещался c _ons'ом и удалялся бы из очереди.

          11.07.2018: а почему "c _ons'ом"-то?! При том, что парой абзацев выше сказано "DEV_STAT -- INF'ом"; и в v2'шном xcac208_drv.c сепаратор кладётся в очередь посредством q_enqueue (который там даёт oneshot=0), а не _ons'ом. Оно, похоже, работает, но всё же почему так сделано? Огрех из-за копирования предыдущей команды?

        Делаем:

        • В качестве такового кода выбран 0x1F -- чтение текущего значения из канала 15 (=DESC_READ_n_BASE+15).
        • А для возможности ОПЦИОНАЛЬНО указывать такой код выбрано #define-имя DEVSPEC_TLOAD_SEP_DESC, которое если #ifdef, то используется в DoTabActivate() вместо умолчательного GETDEVSTAT.
      • CrunchTableIntoFile():
        • была сделана для CAC208 -- точнее, адаптирована под 16/32-битовость из 24/48-битной от CDAC20, ...
        • а потом скопирована для CANDAC16 и CEAC124 -- у них кодировка данных и форматы абсолютно одинаковы, отличие только в количестве каналов (которое функция использует как file-wide-параметр).

    30.06.2017: завершаем.

    1. Проверяем. CANDAC16 и CAC208 -- работают (хотя и был косячок); CEAC124 проверить не на чем, но он почти идентичен (отличие в числе ступеней (27 вместо 30) и обязательно-нулевом номере таблицы).

    23.07.2017@вечер-~22:00-пока-ставился-CentOS-7.3-на-v5p1:

    1. За позавчера (candac16) и сейчас (ceac124 (включена) и cdac20) добавлена "поддержка" таблиц (управление) в скрины.
  • 08.06.2017: о реализации таблиц.

    Вытащено в свой раздел. Начало -- в предыдущем, который теперь по "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.

    • Замечание: пока всё делаем с фиксированным TABLE_ID.

    09.06.2017: далее.

    • Реализованы все _DO_TAB_*-команды, кроме ACTIVATE.
      • Они теперь ВСЕ работают через unicast-команды.
      • Отправка пакетов делается через 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):

        1. Вот НЕФИГ было делать отсылку табличных команд DIR'ом!
          • В результате оказалось, что команда DESC_FILE_START умудряется влезть аккурат между запросом GET_DAC_STAT (отправляемым периодически в режиме TMODE_ACTV) и ответом на него.
          • А в этом ответе -- отправленном еще ДО получения блоком команды -- START битики DAC_STAT_EXECING и DAC_STAT_EXEC_RQ нулевые, так что HandleDESC_GET_DAC_STAT() честно считает выполнение таблицы завершённым.
          • ...после чего вызывается EraseTable(), радостно грохающий таблицу.
            • Возможно, при обычной работе оно бы еще прокатило и доотработало бы последний шаг -- хбз, куда там козачиная прошивка успевает закэшировать его данные.
            • Но weldproc_drv по декларированному окончанию еще и STOP делает, что приводит к отправке DESC_U_FILE_STOP.
            • ...хотя эксперименты делались на CAC208 с SW_VER=3, который НЕ поддерживает unicast-STOP (нужна минимум =4).

              А исполнение таблицы прекращалось.

              Так что, вероятно, и в прочих случаях не прокатило бы, и лишь удачное стечение обстоятельств (race condition -- тут как повезёт с условиями) позволяло тогда год назад не наткнуться на проблему.

          • (Понятно, почему прочие команды шлются напрямую: их надо б исполнить поскорее, да и завязок у них особых нет -- ну там пауза или возобновление, они и так вилами по воде (из-за отсутствия синхронизации с работой ЦАПа); а стоп -- так и вовсе ASAP.)
        2. Второй частью причины проблемы выглядит то, что отправка DESC_GET_DAC_STAT почему-то повсеместно делается как _ONS.

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

        Вывод: надо

        1. конкретно START отправлять ВСЕГДА _ONS'ом через буфер;
        2. убирать _ons в отправке DESC_GET_DAC_STAT.

        Сделано. Заодно у п.2 "how" сменено с вездешнего SQ_IF_NONEORFIRST на разные варианты по ситуации.

        ...только оказалось, что НИГДЕ не делается erase_and_send_next для пакетов GET_DAC_STAT -- видно, потому, что

        1. они всегда запрашивались _ons'ом;
        2. а раньше -- в v2hw -- не требовалось потому, что вообще никогда не запрашивался: там он присылался только девайсом по окончании работы таблицы.

        В результате первая же его отправка в очередь приводила к молочению с частотой 10Гц и остановке всего прочего.

        Добавлено -- теперь вроде пашет.

        12.07.2018: а вот и нет, НЕ пашет! Первый раз обычно отрабатывает, а второй (и несколько последующих) -- фиг: сразу после запуска останавливается.

        Как-то умудряется ловить пакет GET_DAC_STAT с mode==0x00.

        13.07.2018: пытаемся разобраться.
        • Анализируем логи sktcanmon'а -- на вид всё окей.
        • Но не работает. WTF?!?!?!

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

        • А смотрим логи от самого драйвера -- там мистика какая-то: выглядит так, будто пакеты умудряются перемешиваться. Точнее, ПОСЛЕ отправки пакета 0xF7 (START) приходит ответ на предыдущий 0xFD (GET_DAC_STAT, как раз с mode==0x00), который должен был быть обработан ДО отправки этого 0xF7.
        • А-а-а, разобрался!!! Прикол был в точке вызова q_erase_and_send_next_v() в HandleDESC_GET_DAC_STAT(): оно делалось в начале, а надо было переставить в конец -- тогда всё заработало.

          Объяснение:

          • Переключение режима делается в OnSendTabCtlCmdCB() -- чтоб именно после реальной отправки пакета, а не при постановке его в очередь.

            И типа предпринимаются некоторые меры, чтоб обеспечить конкретную последовательность отправки/обработки пакетов -- то самое отсутствие _dir и _ons, чтобы "следующий не отсылался, пока не обработается ответ на предыдущий".

          • Но вот же незадача: удаление-и-отправка-следующего делались в НАЧАЛЕ обработчика GET_DAC_STAT, и происходило следующее:
            1. В очередь ставится пакет 0xFD -- периодический опрос.
            2. Он отправляется, но остаётся в голове очереди в ожидании ответа.
            3. Тут в очередь ставится команда 0xF7 (START). Она не первая, поэтому НЕ отправляется.
            4. Приходит ответ на 0xFD -- пока что старый, с mode=0x00.
            5. Обработчик первым делом вызывает q_erase_and_send_next_v()...
            6. ...который выполняет отправку следующего пакета -- того самого 0xF7.
            7. Отправка проходит успешно, и вызывается OnSendTabCtlCmdCB(), делающий SetTmode(,TMODE_RUNN).
            8. Потом возобновляется исполнение HandleDESC_GET_DAC_STAT(), который смотрит: mode==TMODE_RUNN? Значит, надо проверить, что если mode=0x00, то считать исполнение таблицы завершенным.

            Вот в чём косяк: была нарушена ПОСЛЕДОВАТЕЛЬНОСТЬ (за которую так боролись), и обработка результата 0xFD происходила уже после отправки 0xF7 -- в ошибочном предположении, что устройство успело сменить режим работы и обрабатываем ответ на 0xFD, отправленный ПОСЛЕ смены tmode.

          • А хорошо работало всё в случаях, когда DESC_FILE_START попадал в пустую очередь: тогда он сразу же отправлялся, и вышеописанный сценарий с "перемешиванием" не происходил.
          • Кстати, разобраться помог небольшой трюк: пакеты 0xFD слались не из 1 байта (команда), а из 2: команда и некий условный код, разный во всех 3 случаях. Это позволило понять -- глядя в лог пакетов -- какая из 3 точек вызывала отправку пакета, ответ на который с mode=0x00 смутил "догаду".

        Итого: вызов удалить-и-отправить-следующий переставлен, всё стало зашибись.

        В качестве резюме/наблюдения: очень фигово, когда приходится делать работу, которая должна была делаться самим устройством (уведомление о смене состояния девайс должен был бы отправлять сам, а не по постоянному поллингу). Работа должна выполняться в ПРАВИЛЬНОМ месте, наиболее естественном для неё. Иначе получается вот такая засада: имитировать-то можно, со всем этим догадыванием, но этот "искусственный интеллект" очень уж замутен и его реализация чревата косяками из-за неочевидного взаимодействия подстилающих механизмов. Чем проще и прямолинейнее -- тем надёжнее.

    • Канал KOZDEV_CHAN_TABLE_ID -- ID таблицы:
      • В него теперь можно писать (вгоняется в диапазон [0...15]).
      • Но только при не-активной таблице (t_mode<TMODE_LOAD), а при активной запись игнорируется.
      • ...использования пока нет -- сначала проверим работающесть остального, а потом добавим изменяемый ID.
      • 21.06.2017: и использование сделано.

        ...кстати, забыто было добавление в карты .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.
    • Слегка оффтопик (вопрос грызёт уже давно, как минимум всю эту неделю): для GUI явно нужен какой-то тип (bigc-?)knob'а, позволяющий редактировать вектора.
      1. В простейшем варианте -- просто вектор.
        • Отображать, видимо, как вертикальную колонку чисел.
        • Сверху, вероятно, отдельно число-размер -- при его изменении меняется и количество строк.
      2. В более сложном варианте может понадобиться и "композитная" ручка, состоящая из нескольких колонок, чтобы
        • Синхронизированно менялось количество строк у всех столбцов -- и у времён, и у всех каналов ЦАП.
        • Дизайн, очевидно, такой: над столбцом времён -- ввод количества строк; над столбцами каналов -- чекбоксы их используемости.

        Аспекты реализации:

        • Неясно: то ли работать с отдельными по-линейными векторными каналами (tabN), то ли с общим (tab_all).
        • Но, в любом случае, канал времён (tab_times) будет отдельным, так что компонент должен будет мочь общаться с НЕСКОЛЬКИМИ векторными cda-каналами. Тогда -- DATAKNOB_USER?
      3. Касательно сценариев использования:
        • Желательно, чтобы оный компонент(ы) был не железно привязан к козак-таблицам, а годился бы и для работы с ПРОИЗВОЛЬНЫМИ векторными каналами.
        • Оный компонент (компонентЫ?) должен стоять в subwin'ах [Table...] слева.

        Следствие: в идеале -- чтоб эти компонент(ы) были бы прямо в стандартном наборе MotifKnobs, дабы быть доступными ВСЕМ программам (т.е., чтобы присутствовать в бинарнике pult).

      4. Аспекты реализации в рамках Knobs/MotifKnobs:
        • Желательно, чтобы использовались механизмы
          1. usertime -- user_began_knob_editing()/cancel_knob_editing() плюс зануление k->usertime в надлежащих местах.

            Учитывая, что тут у нас МНОГО полей ввода -- хбз, как делать.

            • Видимо, отдельно нужны кнопки "Отправить" и "Отмена"; кстати, в электронных таблицах оное обычно есть -- зелёная галочка и красный крестик.

              Кста-а-ати, ведь хорошо бы еще нумеровать строки! И вот над столбцом номеров -- можно сделать эту пару кнопочек.

            • Но чисто интуитивно -- данные бы должны отправляться и при просто редактировании одного числа, по нажатию [Enter]...
          2. wasjustset -- но этот вроде не шибко сложен.
        • Учитывая, что высота столбцов может быть великой, надобна поддержка скроллируемости.
        • Главный вопрос -- как вообще делать поддержку отображения и ввода на уровне Motif.
          • Большую матрицу XmText'ов (по штуке на каждую клетку) -- расточительно, а главное -- оно на нескольких сотнях начнёт дико тормозить.
          • Сделать всё canvas'ом, на котором рисовать/печатать всё самостоятельно, а при клике на нужной клетке позиционировать туда и делать XtManageChile() единственного XmText'а?

    12.06.2017:

    • Поскольку в sendqlib добавлено, что по успешной отправке _DIR-пакетов callback также вызывается, то...
    • ...схема отработки команд управления исполнения таблицами переделана:
      • Все "локальные действия" (смена режима, вызов чтения текущих значений) выполняются в функции-callback'е, переименованной в OnSendTabCtlCmdCB().
      • Собственно отправка командных пакетов сосредоточена в единственной функции-процедуре SendTabCtlCmd():
        • Вначале пытаемся отправить пакет как _DIR -- с указанием callback'а, ...
        • ...а если обломилось -- то как _ONS, также с указанием callback'а.
        • Пакеты всегда отправляются одинакового формата -- из 2 байт. В т.ч. и пакет STOP, у которого второй байт (номер таблицы и метка) является просто балластом (бесполезным, но, вроде, безвредным).

          16.06.2017: теперь всегда из 3 байт, ради нолика для команды RESUME (в противном случае она работает как GO_NEXT).

        Кстати, а не засылать ли сразу после этих пакетов ещё (ons'ом)пакет GET_DAC_STAT, для "проверки" отработки команды?

        15.06.2017: да, сделано.

      • ...а в _rw_p() стоят просто вызовы её.
    • _hbt(): раскомментирован поллинг текущих значений и добавлена периодическая (1 раз в секунду) отправка пакета GET_DAC_STAT.
    • _in(): добавлен начальный (пока v2'шный) вариант реакции на _CLOSE и _GET_DAC_STAT.
    • Для чего в privrec добавлено t_file_size и t_file[]. Последний пока размером [1], т.к. надо еще хорошенько подумать, по какой формуле считать максимально возможный размер "файла" (код в xcac208_drv.c несколько неочевиден).

      14.06.2017: потребный размер массива определён чтением документации и помещён в CDAC20_MAX_FILE_BYTES.

    • Для выдачи строковых сообщений о статусе таблицы (через KOZDEV_CHAN_TAB_ERRDESCR) сделана ReportTableStatus().

    13.06.2017:

    • Подредактирована строка ИНФО_ПО_КАНАЛАМ в cdac20k.devtype для отражения там таблицевых векторных и ERRDESCR'а.
    • Для удобства сделана ReturnInt32Vect().
    • Сделано чтение и запись каналов _TAB_TIMES и _TAB_n_base+N.

      Запись работает только при не-активированности таблицы, иначе просто игнорируется.

      Обработка _TAB_ALL пока отсутствует (лень возиться).

      14.06.2017: копирование дополнено вгонянием значений в диапазоны. 0-м элементам векторов вольтов выход за границы [MIN,MAX] разрешен в случае, если они за границами (-20V,+20V) -- для указания "отключен" и "стартовать с текущего".

    • В _init_d() добавлены SetChanRDs() для табличных каналов (а вот кванты им ни к чему) плюс SetChanReturnType(,,,IS_AUTOUPDATED_TRUSTED) для KOZDEV_CHAN_TAB_ERRDESCR и начальный возврат пустой строки.
    • Собственно -- вроде почти всё готово, осталась только "активация".
    • Замечание общего характера: теперь определение aff возложено на активацию (в v2 оно выполнялось в момент записи векторов).

    14.06.2017:

    • Предварительное соображение:
      • В v2 в нулевых элементах массивов указывались начальные значения -- с которых надо стартовать; эти значения перед загрузкой таблиц прописывались в устройство.
      • Однако это не всегда удобно: иногда может захотеться сказать "стартуй с текущего".
      • Так вот: пусть это указывается помещением в 0-й элемент вектора специального значения "очень большое" -- больше 20V. Что станет означать "помести в файл значение из out[l].cur".

        Только пара аспектов:

        1. Это надо именно в ФАЙЛ помещать, а массивы t_vals[][] трогать нельзя!
        2. А что, если значения еще не прочитаны (out[l].rcv==0)?

          Да всё просто -- ругаться и отваливать.

      • Т.е., значение <-20V -- канал не участвует в таблице, >+20V -- стартует с текущего.

        Это отличается от v2'шного, где считалось aff=abs()<20V.

      • Кстати, отдельный аспект -- как указывать эти значения: ведь значения-то {r,d}-калиброванные.
        • Вариант 1: прогонять значение (+/-)20*1000*1000 через cda_rd_convert(), и результат использовать.
        • Вариант 2: ставить очень-очень большое/маленькое число и надеяться, что конверсия double->int32 выдаст INT_MIN/INT_MAX.

          Этот вариант надо проверить; хотя он в любом случае сомнительный.

    • И да -- надо ли сделать, чтобы указание в 0-м элементе значения <=-20V прописывало бы прямо t_nsteps[l]=0, сразу в _rw_p()?

      Пожалуй, не станем: просто ради удобства пользовательского интерфейса.

      (Сомнительно, ну да ладно -- посмотрим потом.)

    • Замечание: времена -- в МИКРОсекундах. См. bigfile-0001.html за 05-09-2009.
    • ПИЛИМ АКТИВАЦИЮ.

      Сделано всё, кроме

      1. Проверки на соответствие скоростям (лень, потом).
      2. Собственно генерации образа файла -- что в любом случае будет device-dependent. Вот оное dependent и вынесено в предоставляемую драйвером CrunchTableIntoFile().

    15.06.2017:

    • @утро-~11:00-НГУ-ТСАНИ-курс-TANGO насчёт вычисления значений для файла: инкременты там 6-байтовые, так что считать их на 32-битном процессоре вроде бы неудобно -- придётся гонять через 64-битную арифметику.

      Однако, можно обойти: ведь нас интересуют в первую очередь результирующие байты, а их можно получать так: bytes3456=delta32bit/nsteps, bytes01=delta32bit%nsteps.

      @после-обеда однако, нет: нам ведь нужно при формировании "файла" на каждом шаге помнить текущее значение "аккумулятора", чтобы считать дельту от него; соответственно -- всё-таки придётся иметь 48-битную арифметику...

    • Пилим CrunchTableIntoFile(). На int64.
      • Сильное отличие от v2'шного -- что всё делается прямо в один проход (а не "сначала насчитаем данные в матрицах, потом отдельным циклом сгенерим файл").
      • В остальном устройство "шага расчёта" взято от старого (даже имена переменных), но арифметика заменена на специфичную для 24-битного CDAC20, как и складирование в файл (6-байтовые инкременты).
    • Пара доделок к EraseTable():
      1. Добавлен параметр wipe_channels -- что НАДО очищать также и внутренние данные о каналах.

        (Изначально это в нынешнем варианте -- с унификацией всего удаления (и из девайса, и из каналов) в EraseTable() -- делалось принудительно, что совсем излишне -- т.к. требуется только по _DO_TAB_DROP (кстати, а когда эта команда была придумана и для чего?!).)

      2. При оной очистке каналов надо ж их "опустевшие" еще и наверх отдавать -- тоже сделано (код скопирован из _rw_p() (нехорошо, что дублирование...)).
    • Вроде всё. Проверяем -- много всякого повылезло.
      • Во-первых, сначала "не работали" никакие команды, активируемые через SendTabCtlCmd(). Причина оказалась банальной -- усовершенствованный (для вызова callback'ов даже при _DIR) sendqlib влинкован в сервер, а тот не был в /tmp/4cx/sbin/ обновлён.
      • В процессе отладки была-таки добавлена ons-отправка GET_DAC_STAT сразу после отправки пакетов старта/стопа/паузы/продолжения таблиц, из-за чего...
      • Далее сразу после запуска таблицы всё вдруг отключалось.
        • Как выяснилось, дело в пакетах GET_DAC_STAT (которые теперь запрашиваются постоянно, плюс сразу после старта): оказывается, непосредственно после принятия команды START еще НЕ горит статусный бит b0 ("файл исполняется"), а сначала (до 10мс?) горит b1 ("принят запрос на исполнение файла").
        • Посему в _in() теперь "закончившесть" исполнения определяется не по отсутствию одного только b0, а если ОБА бита -- b0|b1 -- нулевые.
      • А вот потом, когда таблица стала-таки исполняться -- и нужное время, причём -- видно, что значения какие-то не те делаются.
        • ПЕРВЫЙ шаг делается как надо. И начальные значения прописываются.
        • А вот следующие шаги -- какой-то мрак: прёт не туда, и не очень ясно, по какому принципу (как зависит от нужного значения). Но, похоже, всегда только вверх.

          16.06.2017: да, "только вверх" -- там реально было накосячено с арифметикой или, скорее, с логикой. Там же отдельные ветки для "вверх" и "вниз", и в обоих случаях delta высчитывается положительная, как и инкремент this_incr=delta/nsteps; но раньше оно этот инкремент сохраняло в массив f_incrs[][], а сейчас использует для сваливания в файл сразу. Вот и писало ВСЕГДА положительный. Исправлено (в ветке "вниз" добавлен минус) -- всё стало окей. Кстати, сами эти 2 раздельных ветки выглядят странно. Вполне можно считать всё в одной, и получится ровно что надо.

          30.06.2017: нифига, НЕЛЬЗЯ в одной! Как выяснилось сегодня при проверке аналогичной функции для 16-битной кодировки (32-битные аккумуляторы), одна ветка прокатит лишь если коды никогда не вылезут в знаковый бит. В 24/48-битной кодировке для CDAC20 они в знаковый бит 64-битных слов не вылазят, а вот в 16/32-битной у 32-битных слов -- ещё как (коды для отрицательных вольтов).

        • Также неправильно работает указание 0-го элемента как +20V -- "начни с текущего": скачет в -10V.

          16.06.2017: там просто при отсылке пре-программирования не проверялось на тему "начинать с текущего" и всегда отправлялось значение 0-го шага; а +20...+30V при конверсии в 24-битный код ЦАПа давали переполнение, выглядевшее как -10V. Исправлено.

        Явно какие-то косяки с арифметикой

    • @вечер-дорога-домой-мимо-ИПА: нужны бы команда, чтобы из MODE_ACTV переходить в MODE_NONE, но НЕ стирая каналы (в отличие от DROP).
      • Повесить это на STOP? Но она мало того, что шлёт не особо-то нужную DESC_U_FILE_STOP, так еще и маскируется для не-unicast-capable драйверов.
      • Лучше всё-таки на отдельную команду. Благо, что место есть, и не используется пока нигде, так что при желании даже раздвинуть набор команд можно.

      P.S. А всё-таки -- почему тогда, в 2009-м, именно ТАК была выбрана архитектура, со стирающим всё DROP'ом, но без возможности просто снять активированность?

      16.06.2017: а вообще БЫЛА/есть возможность снять активированность -- именно через STOP, который разрешен в состояниях ACTV, RUNN, PAUS. Блокируется это только в случае не-поддержки unicast-команд; так что просто убираем блокирование для STOP'а, и всё становится тип-топ (ну да, у девайсов со старой прошивкой реальной остановки из состояния исполнения не будет, но тут уж йок).

    Кстати, тестирование ведётся с помощью скрина от CAC208 -- потому, что в нём есть панель управления таблицей, а в cdac20.subsys пока нет (ну да, горят большинство каналов чёрным NOTFOUND -- ну и ладно).

    16.06.2017:

    • @утро-дома: при длине таблицы 1 шаг надо по ACTIVATE сразу после засылки начальных значений переходить в NONE.
      • Т.к. ИСПОЛНЯЕМЫХ шагов 0, то собственно запускать таблицу смысла нет.
      • А так это получится -- для многоканальных ЦАП'ов -- возможность одной командой быстро выдать значения во все предзаказанные каналы.
      • И да, для соответствия обычной работе -- пусть проходит и через LOAD, и через ACTV; а вот в RUNN смысла нет -- пусть сразу в NONE уходит.

      Вроде сделано. Хотя еще не проверено. Парой часов позже: проверено, работает.

    • Косяк с первым шагом "начни с текущего" исправлен.
    • Косяк с только-растущими значениями также исправлен -- таблица ходит как надо.
    • Но вылезла иная странность: похоже, в CDAC20 unicast-команды управления таблицами работают не так, как положено по документации: RESUME работает как GO_NEXT (НЕописанная), а STOP вообще игнорируется.

      RESUME исправляется просто: ей нужен 3-й байт со значением 0.

      Написан e-mail Козаку (а конкретика записана в notes/20170616-CDAC20-PROBLEMS.txt).

      17.06.2017: Козак ответил: он с понедельника в отпуске на 2 месяца, с пока неясными перспективами разборок. Буду договариваться.

    • Что осталось доделать:
      1. Проверки на соответствия скоростям (при выставлении начального значения (что дельта с текущим не более .spd/HEARTBEAT_FREQ) и при дальнейших шагах). 20.06.2017: done
      2. Проверки на переходы между состояниями (вследствие broadcast-команд, например) в _in().DESC_GET_DAC_STAT. 19.06.2017: done

    18.06.2017@дома-жара: предвидится некоторая проблема с определением изменений состояния ЦАПа по анализу поля status в реакции на DESC_GET_DAC_STAT при периодическом поллинге.

    Возможна такая последовательность:

    1. Таблица (какое-то время назад) загружена в устройство.
    2. Отправлена очередная GET_DAC_STAT.
    3. Поступает команда START и девайсу шлётся DESC_FILE_START, а заодно делается SetTmode(,TMODE_RUNN).
    4. Приходит ответ GET_DAC_STAT (на тот запрос, отправленный ДО старта таблицы).

      И в этом ответе в статусе биты b0 и b1 ещё нулевые.

    5. Вуаля -- код решит, что исполнение таблицы закончилось и перейдёт в состояние NONE, заодно затерев таблицу в девайсе.

    Ну и что с этим делать?

    • Первое, что приходит в голову -- сменять состояние не в момент получения команды, а в момент отсылки пакета START.

      Но это уже и так делается, да и не поможет оно с race condition'ом.

    • Запоминать timestamp отправки команды START, и после него некоторое время (100мс? или сколько?) игнорировать онуления b0|b1.

      Но это ОЧЕНЬ плохой вариант:

      1. Сколько выдерживать паузу -- хбз.
      2. Сейчас сразу после отправки любой команды-управления-таблицей отправляется запрос GET_DAC_STAT. Так вот -- ответ на него станет игнорироваться!!!
    • 19.06.2017: исчо вариант: ВСЕ отправки GET_DAC_STAT делать с on_send-callback'ом, и:
      • По отправке взводить некоторый флажок "отправлено".
      • По получению ответа сбрасывать его.
      • При отправке команды START проверять флаг, и если взведён, то взводить еще флажок "игнорировать состояние b0|b1 в следующем ответе", который также сбрасывать при получении любого ответа.

      На вид -- вроде бы должно дать нужный эффект, хоть механизм и некрасивоватый. Но есть пара сомнений:

      1. А если пакет запроса (который до-отправки-START) потеряется и придёт сразу ответ только на второй?

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

      2. А вот не сольются ли -- при сильно загруженной CAN-линии -- оба ответа в один?

        Тут тоже вроде несмертельно -- аналогично п.1.

    19.06.2017:

    • Провёл эксперимент на тему "что будет, если указать инкременты и количество шагов так, чтобы аккумулятор вылез за границы [-10V,+10V]" (возможно, например, если таблица случайно запустится повторно).

      Методика эксперимента: запускаем таблицу, в которой есть длинный этап уменьшения, и в ходе него canmon'ом шлём пакет "запиши -9V".

      Результат: арифметическое переполнение -- после ухода до упора вниз, за -10V, значение вылезло сверху, в +10V и далее вниз. Это на CDAC20.

    • Обработчик _GET_DAC_STAT набит некоторым количеством логики, которая обнаруживает изменения состояния вследствие "сторонних действий":
      • Запуск из состояния ACTV.
      • Паузение из состояния RUNN.
      • Продолжение из состояния PAUS.

      Проверено -- вроде работает.

    20.06.2017: добавляем проверки скорости -- первого шага (он типа мгновенный) и следующих.

    • К сожалению, унифицировать их не удалось, т.к. у первого шага "время исполнения" отсутствует.
    • Сами проверки-условия взяты из _rw_p(), из обработчика записи в ЦАП.

      Поскольку там условия были обратными -- что "можно", в то время как тут что "нельзя" -- то, чтобы их не перекурочивать, просто поставлен ! в начале.

    • Проверка на первый шаг -- попроще: там проверяется, что дельта между текущим и начальным значениями не более .spd/HEARTBEAT_FREQ.
    • Проверка на следующие -- посложнее. Пришлось учитывать размер шага и число шагов (хотя выглядит оно небезупречно: там ведь считается в int32, и есть погрешность округления, так что в реальности скорость может оказаться чуть выше и превысить лимит). @вечер-баня-Б12-бассейн: анализ погрешности: она может приближаться к 1uV/шаг, при 100шагах/сек это будет 100uV/s; т.е., будет дозволяться скорость, превышающая разрешенную не более чем на 100 микровольт -- вроде выглядит терпимо, хотя и нехорошо. Вот для CEAC121, с его 10000Гц/100us -- там получится уже 10000uV/s=10mV/s, многовато; тогда надо будет что-то думать (менять арифметику с деления скоростей (на частоту) на умножения шагов (на частоту же)?).

      Посему...

    • Для удобства расчётов был введён CDAC20_STEPS_PER_SEC=100, а имевшийся ранее CDAC20_USECS_IN_STEP теперь считается через него (вместо былого =10000).

    21.06.2017: причёсывание.

    • Видно, что SetTmode() почти всегда сопровождается выдачей краткого сообщения (БЕЗ параметров) в _TAB_ERRDESCR.

      Напрашивается, чтоб ей добавить ещё один параметр, в котором бы можно было указывать оное сообщение -- опционально.

      • Сделано -- параметр message, который если !=NULL, то делается ReportTableStatus(, "%s", message);.
      • Заодно параметр message добавлен и к EraseTable().

      Бонус -- теперь во все смены режима добавлено информирование о состоянии.

    • Делаем возможность менять TABLE_ID.
      • Главное -- само использование кода в командах.
      • И в .devtype-файлы во все добавлено (что изначально было забыто).
      • Проверено -- работает как надо.
      • Но обнаружен косяк в поведении самого CDAC20 (прошивка 9): broadcast'ные команды PAUSE и RESUME (6 и 7) игнорируют этот код группы и срабатывают в любом случае, даже если в них указан не тот код, что в таблице. Команда же START (2) работает как положено.

        И, кстати, команда "broadcast'ный STOP" (1) не функционирует вовсе.

        Козаку всё написано.

    26.06.2017: серьёзный шаг: увеличиваем *_TABLE_MAX_NSTEPS с 29 до 31. Смысл -- де-факто все устройства позволяют таблицы до 30 шагов; плюс ещё 1 шаг на инициализацию.

    Почему тогда, 8 лет назад, было выбрано 29 -- вопрос. Очевидно, перестраховка.

    29.06.2017: сделаны CrunchTableIntoFile() для остальных 3 ЦАПов. Реально это всё одно и то же -- поскольку кодировка данных и форматы "файлов" одинаковы.

    30.06.2017: проверяем.

    • В области положительных значений всё OK, с отрицательными -- косяк...
    • Разбирательство показало, что причина в используемом для вычислений типе данных: знаковом int32. Устройство кода таково, что он рассчитан на БЕЗзнаковую арифметику; а при сдвиге влево на 16 бит (для имитации козачиного 32-битного аккумулятора) у кодов для отрицательных вольтов появлялся 31-й бит.
    • После перевода на int32 всё заработало. И с CANDAC16, и с CAC208 (CEAC124 проверить не на чем).
    • Кстати, у CDAC20 всё работает со знаковым int64 потому, что кодировка 24-битная, а после удвоения для аккумулятора -- 48-битная; т.е., знаковый бит всегда 0.

      Так что НАДО оставить 2 ветки (вверх и вниз), иначе в беззнаковой арифметике единая ветка, полагающаяся на правильную знаковость, может не прокатить (хотя и не факт -- учитывая свойства дополнительного кода, может и прокатить; но полагаться на это в любом случае чревато).

    13.08.2018: канал out_tab_errdescr остаётся болотным по приходу 0xFF/is_a_reset.

    • Причина в том, что канал помечен как IS_AUTOUPDATED_TRUSTED и реально возвращается ТОЛЬКО по инициативе advdac_cankoz_table_meat.h'а, но НЕ по щёлканью статуса устройства.
    • И что реально делать -- хбз:
      • По идее, если таблица реально исполнялась, то поллинг GET_DAC_STAT'ом обнаружит изменение состояния и будет произведён переход в TMODE_NONE, что вызовет обновление канала.
      • Но если причиной is_a_reset являлся BUSOFF (recovery), то таблица будет продолжать исполняться. Впрочем, по окончанию исполнения -- которое наступит через небольшое время -- канал опять же обновится.
      • Можно ввести в _tavle_meat дополнительный вызов -- nnnOnReset(), который вызывать из драйверовых _ff()'ов.

        Но что именно в нём делать -- неясно:

        • Ведь сообщение вроде бы надо обновлять только по реальному изменению, а не когда попало. Можно, конечно, делать там возврат канала толко при TMODE_NONE -- т.е., когда иных причин изменения не предвидится.
        • А в TMODE_ACTV как поступать? Ведь таблица как бы и не исполняется, но мы не знаем, осталась ли она там реально (и проверять, еще одной командой закрытия -- совсем неохота (лишняя причина запутаться в собственных ногах).

        ...а какие еще неочевидные аспекты могут вылезти?

    • Пока забиваем -- некритично; может, потом какое решение в голову придёт.

    27.02.2023: обнаружилось (на примере CAC208 у ringrf), что кнопки [Table...] горят болотным. А точнее -- каналы out_tab_errdescr.

    Стал разбираться.

    • Сначала причина показалась в том, что эти каналы помечаются как AUTOUPDATED_TRUSTED, но изначально никак не возвращаются, а лишь по реальному изменению состояния таблицы.
    • Но потом заметил в конце всехних _init_d() вызовы
      ReportTableStatus(devid, NULL);
      которые должны вернуть out_tab_errdescr="" (NULL интерпретируется как пустая строка).

      Загадка, да?

    • Затем посмотрел у болотногенерящего устройства rez208 timestamp'ы каналов out_tab_errdescr и _devstate; они оказались 2023-01-27-15:34:47.505 и 2023-02-16-02:50:50.066 соответственно.

      Так вот:

      1. "Jan 27 15:34" -- это время старта сервера canhw:19 (судя по /var/tmp/cxsd-19.pid),
      2. за 2023-02-16-02:50:50 в /var/tmp/4drivers.log* присутствует строка
        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 драйверами, что напрашивается вытаскивание в какой-то общий файл, но вот в какой...

    На пульт скопировано, потом как-нибудь посмотрим (не вылезет ли чего).

  • 07.06.2017: кстати, надо бы каналы ADC_MODE и OUT_MODE унести из rw-области [0-19] в ro-область [40-59].

    10.06.2017: делаем.

    • kozdev_common_drv_i.h:
      • Сами имена перенесены в ro-область и новые номера -- 44 и 45.
      • Также за компанию добавлен (в целях отладки) KOZDEV_CHAN__T_MODE=46 (тройной подчерк в имени).
      • Старые позиции (=1 и =9) переведены в резерв.
    • *.devtype-файлы: из rw-области убрано, в ro-добавлено.
  • 19.04.2018: в canmon_common.c был добавлен ключик -N -- печатать порядковые номера принимаемых пакетов (Панову понадобилось).

    19.04.2018: детали:

    • Параметр называется option_numbers.
    • Печатает "#NNN", причём хитрО: если включена хоть какая-то печать времён, то выдаётся
      @time#NNN
      а иначе
      @#NNN
      т.е., ВСЕГДА номер "прикрыт" собачкой, чтобы при чтении списка команд из файла эта информация не мешала бы.

    25.10.2021: кстати, именно тогда разошлись ("разфорковались") исходники в v2hw/ (оставшийся старым, от 29-11-2012) и в hw4cx/ (продолживший развиваться).

    А потом уже добавились ожидание N пакетов (":-N") и смена концепции имён ("CAN_HAL" вместо старого "CANHAL"); последнее привело к исчезновению совместимости -- теперь уже нельзя плюхнуть новый исходник в v2hw/, не соберётся.

  • 25.05.2018: Панов попросил сделать в canmon возможность делать ограничение по количеству пакетов -- ловить N штук, после чего отваливать.

    25.05.2018: после некоторых размышлений сделано, возможностью указывать отрицательное число в команде ":". Например, ":-1" означает дождись 1 пакета (":-5" -- аналогично, 5 пакетов).

    Т.е., если после ':' идёт положительное число, то это миллисекунды для ожидания, а если отрицательное -- то это минус-количество пакетов.

    Детали реализации:

    • Сравнивается текущее количество -- count -- с "начальным", которое сохраняется в start.
    • Для чего работа с count (он был введён в апреле для ключа -N) чуть изменена: он теперь считается ВСЕГДА.
    • Также чуть сменена работа ограничения времени: существовавшее ранее условие "msecs>=0" (на вид, странное) было переделано на "msecs>0".

      Т.е., теперь ":0" эквивалентно просто ":".

    Немного соображений по выбору способа указания ограничения.

    • Первой мыслью было сделать ключик -- например, -c COUNT.
    • Но вариант с командой гибче: так можно делать сценарии ("отправить такой-то пакет, дождаться стольки-то пакетов, отправить...").

      Кроме того, ограничение ключиком было бы "параллельно": оно бы действовало на ВСЕ команды ":". Что несколько странно.

    • Потом была мысль использовать другой символ префикса -- '.' или ','; и даже что-то в памяти всплывает, что якобы когда-то, в какой-то из старых версий какой-то подобной утилитки кто-то из них использовался.

      Но рытьё в старых исходниках ничего такого не нашло.

      Зато обнаружилось, что в uspci_test, cm5307_test и bivme2_test символ '.' работает как ':', но означает "жди ВСЕ прерывания в течение указанного периода, а не только первое".

    • ...и также обнаружилось, что в candump'е (предок canmon'а, сделанный 12-08-2009 и просуществовавший меньше года) БЫЛО ограничение по количеству пакетов -- -c COUNT; очевидно, по аналогии с v2'шным cx-rdt.
  • 16.11.2018@утро-дома: неудачно устроен "on_send-callback" -- CanKozOnSendProc(): не передаются данные об отправляемом пакете.

    А хочется (конкретно сейчас -- для canipp_drv.c.

    16.11.2018@утро-дома: как показывает анализ, сама фича on_send не используется в CAN-драйверах НИГДЕ.

    Так что сделать можно легко и ничего не нарушая.

    16.11.2018: делаем.

    • В cankoz_lyr.h прототип CanKozOnSendProc расширен 3 параметрами: desc, dlc, data[].

      Сделано унифицированно с CanKozPktinProc, а то так-то desc не особо требуется.

    • В cankoz_lyr_common.c::cankoz_send_callback() всё просто. Разве что desc -- он при dlc<1 передаётся равным -1.
    • А вот насчёт "сама фича on_send не используется в CAN-драйверах НИГДЕ" оказалась неправда: используется в advdac_cankoz_table_meat.h. В примерно тех же целях, что понадобилось для canipp_drv.c -- чтоб выполнять некоторые действия (смену режима) именно по отправке пакета, а не по постановке в очередь.

      Используется с лета этого года (вот ирония!).

      Что ж -- там прототип подправлен.

      Аргументы добавлены с конца, а по стандарту вызова C ("C calling convention", cdecl) в стек параметры кладутся справа-налево и удаляются ВЫЗЫВАЮЩИМ. Т.е., поскольку добавленные параметры в OnSendTabCtlCmdCB() помечены __attribute__((unused)), то формально можно не заморачиваться введённым изменением ABI -- оно никак не проявилось бы.

      Но всё же лучше перебдеть -- так что на пульту (ctlhomes) эти 4 драйвера обновлены вместе с sktcankoz_lyr.so.

    03.04.2022: что любопытно:

    • конкретно в canipp_drv.c фича on_send вообще НЕ используется --
    • в единственном имеющемся q_enqueue_v() в AbortMeasurements() указано on_send=NULL, а вместо этого просто СРАЗУ ЖЕ после постановки пакета делается read_state = READ_STATE_NONE

    Или оно туда предназначалось, но было забыто?

  • 01.03.2019: был замечен очень общий ляп, присутствующий в драйверах при постановке в очередь пакетов, которые ДОЛЖНЫ были бы ставиться при помощи 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":

    • ВСЕ, задействующие инфраструктуру advdac_slowmo_kinetic_meat.h (CAC208, CANDAC16, CDAC20, CEAC124) -- должны быть ПОТЕНЦИАЛЬНО подвержены.

      Поскольку оная инфраструктура вызывает друг за дружкой SendWrRq(),SendRdRq(), а в них стоят условия SQ_ALWAYS и SQ_IF_NONEORFIRST.

      Спасает, видимо, то, что в основном запись производится лишь после получения результата обратного вычитывания, так что 2 подряд записи просто не выполняется.

    • cgvi8_drv.c -- удивительно, но нет: там и для записи, и для обратного вычитывания ВЕЗДЕ используется SQ_ALWAYS.
    • cpks8_drv.c -- также нет: там тоже запись и чтение делаются с SQ_ALWAYS, а каналы *_OUT_RATE хоть в карте и предусмотрены, но нереализованы, так что advdac там просто не используется.
    • curvv_drv.c -- установлено ж, что да.
    • panov_ubs_drv.c -- похоже, что да: там во всех 3 случаях применяется SQ_IF_NONEORFIRST...
    • smc8_drv.c -- НЕТ!!! Там УЖЕ повсеместно используется SQ_REPLACE_NOTFIRST, а сам девайс такой умненький, что в ответ на запись сразу присылает результат.
    • tvac320_drv.c -- да...

    Исправлять в том виде, в каком оно есть сейчас -- смысла нету. Надо сразу переходить на использование 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 сие работало (через VCAS, что ли) не всегда корректно: иногда в регистрах (КВЧ? который CKVCH?) в конечном итоге оказывались не те значения, что следовало бы.
    • Поэтому на одном из четверговых сборищ по автоматизации (28-02-2019) Беркаев спросил, как у меня это делается (я кратко рассказал про технологию REPLACE), и можно ли по-быстрому заюзать CX для управления этим железом на ВЭПП-2000.
    • Сенченко особого энтузиазма на тему заюзывания CX, конечно, не проявил.
    • Но я всё же сваял ckvch_drv.c (в основном ради развлечения)...
    • ...а заодно посмотрел, как в HW4CX'ных CAN-драйверах с использованием технологии REPLACE, и ужаснулся: использовалось мало (реально только для работы с регистрами в cankoz_lyr_common.c), плюс там, где НЕ использовалось -- некорректное обратное вычитывание.
    • Вот в результате того ужасновения и родился данный раздел. Результаты на текущий момент:
      1. Драйвер CURVV переведён на REPLACE.
      2. Остальные -- пока нет.
      3. И инфраструктура advdac_slowmo_kinetic_meat.h пока не переделана (а она нуждается, чтоб функции-отправляльщики возвращали результат -- поставлен новый запрос в очередь или лишь заменён старый).

    Итак, на сегодняшней четверговой автоматизационной встрече Сенченко рассказал, что он вроде бы успешно решил проблему на ВЭПП-2000. После сборища я выспросил, как именно. Вот результат опроса.

    • Решил он проблему радикальной переделкой: не стал разбираться, почему имевшийся код работал неправильно, а просто заменил беркаевский код на прослойку на TANGO.
    • Суть решения: запись НЕ делается сразу по команде записи от клиента.

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

      ...вот тут я не помню точно, какой из 2 вариантов реализован, а какой мы с Сашей только обсуждали:

      1. Клиент именно присылает цепочку команд, и она вся целиком пережёвывается (пользуясь каким-то аспектом объектно-ориентированности Танги), отправляя в железку лишь последний результат.
      2. Или просто всё "стоит", команды записи шлются-шлются-шлются, а в конце идёт команда "commit!".
    • Как бы то ни было, суть именно в использовании "транзакций".

      И да, тем самым несколько убивается общность, в целях достижения нужного результата здесь и сейчас.

      • Впрочем, Саша считает, что даже само использование любой СУ -- будь то EPICS, CX или TANGO -- уже накладывает некоторые ограничения на общность
      • На что мне, впрочем, захотелось возразить, что CXv4 делался как раз с расчётом, чтобы быть соединябельным с любой другой СУ.

        ...но не стал это говорить вслух.

    • Т.е., напоминает подход, используемый Сенченко для ADC4X250 -- где сначала неспеша программируются все настройки, а потом всем модулям говорится "можно приступить к ожиданию внешнего запуска!".
  • 10.01.2020: крупная переделка имён: все "canhal"/CANHAL заменены на "can_hal"/CAN_HAL, для украсивливания -- аналогично тому, как это уже сделано для VME.

    Коснулось как имён файлов, так и имён символов.

    Заодно также проведена некоторая унификация -- на тему где "koz" нужно, а где нет.

    10.01.2020: работа грандиозная, некоторые заметки:

    • Файл canhal.h был аж от 29-11-2012, и он совпадал с v2hw'шным.

      Как и все реализации *canhal.h.

    Как бы то ни было -- сделано, осталось протестировать живые программы (убедиться, что нигде нет висячих ссылок).

  • 10.03.2020: наблюдена некоторая странность в поведении v4c4lcanserver на cangw-gimn25 на ВЭПП-4: если в параллель с ним запускать аналогичный сервер на can2.vepp4.local, то приперно через раз CANGW'шный серверок перестаёт работать -- он НЕ присылает никаких данных, а в логах от него есть сообщения вида
    SendFrame(0:k3/6, dlc=1, cmd=0x13): ZERO

    И оно так до тех пор, пока не рестартуешь CX-сервер vepp4-pult6:2.

    Анализ исходников показал, что cankoz_lyr_common.c с тех пор касательно обработки ошибок не изменился, а в sendqlib.c и вовсе одно косметическое добавление касательно SQ_TRY_LAST (которая реально никем не используется).

    Следовательно, проблема может быть до сих пор актуальна, и надо её искать.

    • Лёгкая надежда есть на то, что, возможно, в том конкретном CANGW что-то не слава богу с CAN-драйвером -- например, почему-то не очухивается после ошибки отправки.

      Но на это надежды мало.

      И даже если дело в этом, то придётся добавлять в консольный интерфейс команду вроде "reset", которую хрен знает как исполнять.

      Так что пусть бы лучше это была ошибка НЕ у Мамкина -- свою-то я хоть исправить смогу.

    • Просто запустить там свежий v4c4lcanserver нельзя: с тех пор чуток изменился remdrv-протокол (cxdtype_t стал 32-битным, а он там передаётся массивами).

      Придётся гонять на пару с новым cxsd.

    P.S. Проблема-то была увидена ещё неделю-две назад, но только сегодня обсмотрена детальнее.

  • 25.10.2021: захотелось в canmon_common.c добавить ключ "-K" -- "Keep going after I/O errors". Вроде простейшая на первый взгляд вещь, но оказалось не так уж тривиально.

    25.10.2021: фокус в том, что чтение/запись там делаются не как в прочих подобных утилитках (прямо в основном тексте), а в отдельных функциях Canutil*(). Смысл этого -- исторический:

    • Изначально-то, в 2009-м, вместо canmon'а были раздельные candump_common.c и cansend_common.c (отправленные в отставку чуть позже в 2009-м же).

      И вот для них "стандартный интерфейс" был поселён в canutil_common.[ch].

    • Затем, после отправления вышеупомянутых раздельных утилит в отставку, функции переехали прямо в canmon_common.c.
    • Но когда (в 2014-м?) занадобилось сделать для Панова на ЛИУ утилитки flash_ubs и flash_subord, то функции были скопированы и в них.

    Теперь же, собственно "нюансы":

    • Поскольку предполагалось, что это всё "отдельная самостоятельная библиотечка canutil", то её содержимое было помещено прямо в начало файла. И:
      1. оно должно быть НЕЗАВИСИМО от конкретного "юзера";
      2. и даже если бы зависимо, то все option_nnn определяются НИЖЕ по тексту.
    • Соответственно, чтобы функции CanutilReadFrame() и CanutilSendFrame() не делали бы exit(2) по любой ошибке, то им добавлен параметр option_keep_going_arg, в котором их юзеры передают значение option_keep_going.
    • Плюс, конкретно CanutilReadFrame() теперь не void, а int -- ведь он должен возвращать результат "ошибка", чтобы его юзер не пытался печатать содержимое пакета.
    • Также сообщение об ошибке (которое просто старое, ещё с 2009-го) НЕ печатает метку времени.

      Возможно, если её выдавать (всё равно ж на stderr?), то просто strcurtime_msc()? (А не пытаться учитывать значение option_times.)

      26.10.2021: да, так и сделано -- текущее время выдаётся бещусловно.

    • (Собственно объявление и парсинг ключика на этом фоне -- сущая мелочь, особого упоминания и не достойная :D).

    Итого -- не столь мгновенно, как ожидалось, но сделано.

    26.10.2021: и протестировано на ep1-berezin2 (для чего и делалось: захотелось понять, почему сообщения "No buffer space available" валятся бесконечно, а не подавляются после первого: часть сообщений всё-таки типа уходит?).

    Оказалось, что да -- действительно, Сеньковский VIP конкретно там настолько странно обращается с линией, что она то "забита" (не отправляется даже 1 пакет), то "свободна" (уходят все 3). Вот и причина того, почему в /var/tmp/4drivers.log там постоянно сыплются сообщения.

    Как бы то ни было, "done".

  • 23.03.2023: небольшие улучшения/укорректнивания, нужные в основном для украсивливания диагностики (в первую очередь в случае, когда CAN-линия отсутствует):
    1. В cankoz_lyr_common.c::cankoz_add() добавлено, что при обломе can_hal_open_and_setup_line() в диагностике об ошибке выдаётся также и номер линии -- а то шибко уж бестолково было.
    2. В _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)
      -- раздражает!
  • 10.04.2024: вопрос -- а насколько корректно отрабатывается ситуация "ошибка записи в CAN-линию": останется ли пакет в очереди отправки и будет ли отправлен позже?

    Вопрос возник из-за странного поведения сегодня на ulan-ude-els:51 -- после утреннего включения почему-то часть каналов уставок ЦАП на CEAC124 остались болотными и значилось "age >99h", при том, что каналы АЦП обновлялись.

    И в логах обнаружились строки

    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
    
    (эти CEAC124, mag1_a и mag1_b -- как раз 14 и 15).

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

    Исследуем вопрос.

    1. ПРЯМОЙ отправки -- через SQ_TRIES_DIR -- для РАБОТЫ не делается нигде.

      (Шлются только DESC_STOP и подобное в term_d(), где очередь не к месту; плюс конкретно в advdac_cankoz_table_meat.h::SendTabCtlCmd() так отправляется команды управления исполнением таблиц, но с fallback'ом к SQ_TRIES_ONS при ошибке отправки.)

    2. При отправке из очереди в sendqlib.c::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;
      }
      
    3. ...а в cankoz_lyr_common.c::cankoz_sender() делается корректная трансляция между кодами CANHAL_* и 0/!=0 для sendqlib'а --
      return r == CAN_HAL_OK? 0 : -1;

    Так что внятного объяснения произошедшему на сварке для Улан-Удэ нет. Годным выглядит только "УСТРОЙСТВА умудрились не получить пакеты", но толку от него... И как увеличение TXQUEUELEN до 100 предотвращает конкретно ТАКИЕ глюки -- тоже непонятно.

  • 10.03.2025: модификация в 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:
  • 04.04.2018: поговорил с Беркаевым -- он предпочтёт, чтоб вознёй с железкой CANIPP (реально CANIPP-M) занимался бы я.

    Так что -- будем делать драйвер.

  • 04.04.2018: основной постулат: оно должно быть поканально совместимо с CAMAC'овским u0632.

    Драйвер, конечно, назовём canipp_drv.c, но его карта каналов будет совпадать с u0632'шной.

    И, кстати, почитал описание -- оно прекрасно совместимо с У0632: мало того, что сами "коробочки" те же самые, но и модель работы самого контроллера похожа (только "уведомления" добавлены) и, видимо, удастся сделать драйвер по той же схеме.

    04.04.2018: делаем.

    • Исходным вариантом для canipp_drv.c послужил cpks8_drv.c -- он самый маленький по объёму, и состоит почти исключительно из "скелета" -- в нём почти нет специфичного функционала.

      Специфичный функционал убран, минимальная адаптация (вроде DEVTYPE=9) проведена.

    • Файлы canipp.devtype и canipp_drv_i.h были скопированы из u0632'шных аналогов и в них проведена тривиальная контекстная замена.
    • С оными файлами есть тонкость: формально совместимость с типом u0632 вообще полная -- там даже каналы hw_ver и sw_ver.

      И, теоретически, можно б было вообще не делать canipp{_drv_i.h,.devtype.

      Но если драйвер прекрасно бы обошёлся использованием u0632_drv_i.h и U0632_CHAN_*, то как указывать в конфиге -- вопрос. Т.к.:

      1. Для локальных драйверов -- всё просто:
        dev NNN u0632/canpp@sktcankoz ~ 63
        (тут-то и пригоделась бы возможность указывать /DRIVER@LAYER вместе, ранее считавшаяся слабоосмысленной -- см. за 17-06-2015).
      2. А для удалённых на базе remdrv -- проблема:
        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 субадреса).
    • Некий вопрос: ведь при старте нужно просканировать все 32 потенциальных адреса на наличие коробочек; оно сводится к отправке 30 команд чтения; и -- КУДА вставлять это сканирование?
      • Просто "когда попало" -- чревато (контроллер да и сами коробочки славятся нежностью и нервностью).
      • По-хорошему, нужно его выполнять по получению 0xFF, и НЕ-бродкастного.

        Это автоматом организует пере-сканирование в случае смены железки (которая при включении пришлёт 0xFF).

      • Вроде хорошо подходит _ff(): там и бродкасты отсеиваются, и вызвана она будет гарантированно при старте.

      Посему -- вызов RequestScan() вставлен в canipp_ff().

    • Также копируем крупные куски из u0632_drv.c:
      • В начало помещено всё определение специфики адресации (преобразование диапазона 0...29 в адреса), кодировки данных ИПП, формата управляющего регистра.
      • В privrec скопировано вообще практически ВСЁ -- и 3 поля "имитации pzframe", и unit_*[], и bias_*.
      • И SetM()/SetP() с Return*Bias*().
      • Вся "машина состояний" касательно дОбычи bias'ов -- SwitchTo*() -- полностью.
      • От "кода поддержки pzframe" -- пока просто заготовки (пустые) StartMeasurements(), AbortMeasurements() (этот принципиально пустой), ReadMeasurements(), PerformReturn() (тут тоже принципиально пустота).
      • А "код имитации pzframe" -- просто один-в-один, даже безо всякой контекстной замены.

        Включая мутные махинации с "таймаутами" -- они реально отключены через 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@лыжи: этот драйвер у нас --

    • Почти первый на pzframe, работающий с не-локальной аппаратурой ("почти" -- потому, что есть ottcam/ottucc, плюс у Роговского BPM'ы накопителя).
    • Также почти совсем первый на "имитации pzframe" (тут "почти" из-за karpov_monster, уволенного еще в v2 (в v4 нету), но использовавшего ту же модель).

    Собственно:

    1. Поскольку тут чтение железки НЕ мгновенное -- не надо ль сделать дополнительно какой-нибудь таймаут, запускающийся по приходу сигнала готовности (и началу вычитывания), чтоб если затем процесс чтения "затянется", то обрывать его, с какими-нибудь флагами вроде "_TIMEOUT" и/или "_NO_Q"?
    2. Как понимать "все данные пришли": надо ПЕРЕД началом вычитывания заполнять все ячейки unit_data[] каким-нибудь значением, не могущим взяться из аппаратуры (-1?), а при получении подсчитывать количество ячеек с этим значением, и если 0 -- значит, всё пришло.

    06.04.2018: список вопросов касательно ИПП/CANIPP, которые надо задать Беркаеву:

    1. То, что раньше CANIPP не умел сообщать о получении запуска и приходилось его постоянно опрашивать -- это был "старый" CANIPP, а в CANIPP-M добавлена высылка пакета 0xFE?

      Да.

    2. А остались ли (на ВЭПП-2000, например) старые контроллеры, НЕ уведомляющие? (Добавить в canipp_drv.c поллинг несложно -- тупо взять кусочек касательно poll_tout_p() из u0632_drv.c.)

      Нет, старых не-M не осталось.

    3. В CANIPP надо ли данные читать "хитро", сдвигая на бит и "означивая", как делает IPPvalue2int()?

      Да, это определяется "коробочкой", поэтому формат данных точно такой же.

    4. Есть ли еще какие-то тонкости/хитрости, вроде "ни в коем случае не слать пакетов чтения во время ожидания измерения (или во время оцифровки, после прихода запуска)"?

      Да, тут всё сложно. После программирования на ожидание запусков нельзя делать вообще НИЧЕГО -- даже статус читать нельзя, а то разрешение прохождения запусков слетит.

    5. В У0632 надо было после запуска подождать 3мс, прежде чем девайс всё у себя считает и разложит. А с CAN-версией подобное требуется?

      "Не помню, но вреда от паузы не будет".

      Паузу будем делать постановкой в очередь пакета -- например, нулящего разрешения запусков -- с timeout_us=3000, так что после него прямо сам sendqlib сделает паузу.

    6. Набор фона -- надо ж делать, да?

      Всё сложно. Федя утверждал, что фоны НЕ зависят от режима e+/e-/VEPP4/VEPP2K. Но Беркаев говорит, что всё же зависит. Раз так -- надо в каждом режиме иметь СВОЙ фон. Моя вариант был "сделать каналы bias писабельными, и чтоб менеджер режимов сам решал, когда измерить фон, а потом при переключении режимов прописывал бы нужный фон". Но Беркаев предлагает повесить это всё на middleware: чтоб софтина хранила фоны у себя, получала бы данные, вычитала б фон в соответствии с режимом и потом публиковала бы результат в софтовом сервере; а все пультовые/прикладные программы чтоб брали данные именно из этого сервера, а не от сервера железа.

    06.04.2018: кстати, в privrec_t добавлено unit_data[][] -- тут же не "атомарное чтение и потом отдача из локального массива", а читается по ячеечке каждым CAN-пакетом, так что нужно накапливать в privrec'е.

    09.04.2018: продолжаем -- вооружённые знанием ответов на пятничные вопросы.

    • Что касается "ни в коем случае не слать пакетов чтения во время ожидания измерения": вроде это не должно стать проблемой, т.к. используется парадигма работы pzframe -- сначала всё программируется, а потом лишь ждётся запуск, и никакого общения с устройством не происходит.

      Поскольку у нас именно CANIPP-M, то никакого поллинга проводить не требуется, так что во время ожидания обращения к устройству РЕАЛЬНО отсутствуют (а с не-M пришлось бы мучиться...).

    • Сканирование -- RequestScan() -- наполнено содержимым.
    • Стало очевидно, что делать RequestMeasurements() прямо в _init_d() никак нельзя -- надо дожидаться окончания сканирования (как в У0632'шном драйвере!).
      • Т.е., например -- вызывать по получению ответа на ПОСЛЕДНИЙ из пакетов чтения регистров CADDR (где хранится M, P).

        Получасом позже: а как понять, что это и есть "последний"? Точно ли можно полагаться на порядок прихода пакетов ответа? Обсуждается в следующем пункте списка уровнем выше.

      • А что делать, если запрос на измерение уже пришел, но сканирование еще не закончено (или и вовсе устройство пока не на линии)?
      • В общем случае, кстати, требуется идеологическая модификация в "парадигму pzframe" -- помнить состояние "готовности", и если оная еще не наступила, то как-то игнорировать запросы на чтение.

        Нет, нифига -- НЕ игнорировать, а ЗАПОМИНАТЬ!

      • Но конкретно с У0632/CANIPP эта проблема не стоит, т.к.:
        1. Измерения делаются не по запросу, а "всегда". И в _rw_p() каналы данных вообще игнорируются.
        2. Таймаут тут НЕ используется -- TIMEOUT_SECONDS=0.

          Т.е., если запрос когда-то пошёл -- то это навсегда.

        3. И даже каналов _SHOT и _STOP тут нету -- а _STOP как раз и эквивалентен таймауту.
        4. А если б таймауты либо _STOP работали, то пришлось бы вставить в начало PerformTimeoutActions() проверку, что "если measuring_now==0, то ничего не делать".

          Несколькими минутами позже: а вот и нет! Везде, где PerformTimeoutActions() вызывается (в т.ч. в реакции на param_stop, имеющейся в pzframe_drv.c и отсутствующей у ИПП'шной имитации) стоят проверки на тему "measuring_now!=0".

    • Вопрос: а насколько корректно считать сканирование завершённым, когда пришёл ответ на чтение CADDR последнего адреса?
      • Конкретно для CAN-bus (а точнее -- при использовании sendqlib, где реально пакет ответа "потеряться" не может, т.к. пока не придёт ответ на адрес N, то и запрос на адрес N+1 отправлен не будет) -- да, всё выглядит вполне корректно и проблем вызвать не должно

        (Если только из линии прилетит сфабрикованный (например, canmon'ом) пакет "ответа на адрес N+x", которого никто не запрашивал.)

      • Для прочих же реализаций -- например, через Ethernet/UDP, где пакеты имеют право приходить не в том порядке, в котором отправлены -- надо будет вводить поддержку такого поведения: т.е., определять не по приходу ответа на последний адрес, а именно проверять, что ВСЕ ответы пришли.

        Как? Да несложно:

        • Считать флаги онлайновости unit_online[x] не просто булевскими (==0 -- отсутствует, !=0 -- присутствует), а ЗНАКОВЫМИ: перед началом сканирования во все из них писать -1, и в качестве окончания сканирования считать момент, когда все они стали >=0.
        • Соответственно, все проверки на присутствие нужно будет переделать с простого булевского
          unit_online[unit]
          на
          unit_online[unit] > 0
      • ...хотя при условии использования sendqlib проблемы всё равно быть вроде не должно:
        • Если прилетит вне очереди "опоздавший" пакет ответа на какой-то из предыдущих адресов -- ну и некритично.
        • Пакет ответа на последний из адресов -- не может, т.к. до получения ответов на все предыдущие на него запроса не будет послано.
        • Единственный возможный сценарий -- запоздало прилетит ответ на последний адрес, уже ПОСЛЕ конфигурирования/старта измерений.

          Формально -- вся проблема будет в повторном вызове RequestMeasurements(). Ну, типа нестрашно.

          Но нехорошо. И надо вставлять проверку, чтоб НЕ пытаться запускать измерения повторно.

    • По результатам предыдущего пункта: в RequestMeasurements() вставлено условие, что если уже measuring_now!=0, то ничего не делать.

      Кстати, в pzframe_drv_req_mes() -- аналоге в "настоящей" реализации pzframe -- оная проверка есть.

    • Ну и, наконец, вызов старта измерений перенесён в _in(), по получении ответа на CADDR последнего unit'а.
    • Попутно для трансляции "адресов на шине" (0x00...0x1F) во внутренние номера (0...29) добавлена unitcode2unit().
    • Но вот НУЖНО всё-таки уметь выполнять "как бы AbortMeasurements": когда от устройства прилетает 0xFF с is_a_reset.

      Тут уж хочешь не хочешь, а необходимо забыть о когда-то якобы запущенном измерении, послать контроллеру принудительный запрет всего (на всякий случай) и запустить сканирование.

    • Делаем наполнение canipp_in().
      1. По GETDEVSTAT (0xFE) -- он присылается в качестве уведомления о приходе запуска.

        ...тут пока только селектор, еще ничего не делающий.

        10.04.2018: уже делается.

      2. По ответам чтения.

        Тут 2 части, в зависимости от адреса:

        1. Регистр настроек -- CADDR, 070: это "замыкание" сканирования.

          Здесь всё несложно и обсуждено выше парой пунктов основного списка.

        2. Ячейки данных.

          Вот тут намного геморройнее, т.к. это "замыкание" чтения данных, а следовательно:

          • Надо подсчитывать, все ли данные от коробочки уже пришли.

            Также приходится вести "статистику" -- запоминать факт оного полного прихода, чтобы далее определить пришедшесть всех данных всех коробочек.

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

          • Cобственно определение что "всё пришло": определили, и что дальше?
            • Сначала была дурная мысль прямо ВСЕ действия запхать туда же: и возврат данных наверх (включая подсчёт и вычитание фона), и запрос следующего измерения.
            • Но потом осенило -- неа, иначе надо: тут оставить только складирование полученного в буфер(а), а все прочие действия оставить "стандартной модели pzframe": определив, что реально пришло ВСЁ, просто дёрнуть drdy_p(), и уж та пусть далее вызывает ReadMeasurements(), которая и возится с фоном и отдачей данных.
          • Но тут встаёт технический вопрос: а что, если по какой-либо причине данные от одной из коробочек не придут -- что тогда?
            • СЕЙЧАС, у CANIPP, они могут не придти, наверное, только в случае отвала самого контроллера посреди передачи.

              Сценарий маловероятный, но теоретически возможный.

            • У какого-нибудь иного аналогичного девайса -- в случае, если уже ПОСЛЕ начального сканирования под-девайс вдруг сдохнет и перестанет отдавать данные вовсе (а прочие под-девайсы останутся жить).

              Такое может быть в модели общения master<->скрытые/подчинённые, используемой Пановым: у него подтверждение приёма запроса отдаёт непосредственно видимое устройство-master (и по подтверждениям из очереди запросы убираются), а вот ответит ли под-устройство -- хбз.

            Так вот: если буквально один кусочек данных не придёт, то всё будет висеть навечно (у нас ведь и STOP'а никакого не предусмотрено!).

            Очевидно, что:

            1. НАДО всё-таки вводить таймаут, который стартовать по приходу запуска (и запросу вычитывания).
            2. Потребуется также нечто вроде ottcam'овского "state" -- чтоб помнить ФАЗУ состояния measuring_now: еще ждём запуска, или же уже читаем данные и таймаут тИкает. 10.04.2018: сделано.
    • Надо что-то делать с rflags и/или unit_rflags.
      • Сейчас оно модифицируется 1 раз, при сканировании, а затем возвращается OR'ом с unit_online (который ==0 влечёт CAMAC_NO_Q).
      • Но желательно бы как-то возвращать именно ТЕКУЩИЙ статус прочитанности данных.

        А то мало ли -- по ходу жизни проводок от коробочки отвалился.

      Напрашивается следующее:

      1. В сканировании unit_rflags[] вообще не трогать.

        Достаточно каналам M и P возвращать флаги на основании unit_online[].

      2. А unit_rflags[] получать в процессе вычитывания данных:
        • В начале (при постановке запросов) -- нулить.

          Получасом позже: неа, нулить прямо в StartMeasurements().

        • При получении пакетов в случае состояния там в статусе битика Rack==0 взводить 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'е по взведению разрешения на запуск; но незачем).

    • Сделана реакция на 0xFE.
      • Были сомнения: делать прямо в теле, или же вытащить в отдельную функцию?

        Сделано прямо тут.

      • В самом начале -- куча проверок: и на dlc, и что measuring_now, и что не-is_reading, и на наличие битов статуса "был старт".
      • Потом фейковая команда, типа нулящая разрешение запуска (а оно и так должно автоматом сброситься) и делающая после себя паузу 10 миллисекунд.
      • Затем в очередь ставятся команды на чтение всех ячеек всех активных коробочек.

        Итого -- по максимуму 30*32=960 команд; к запрашиваемому в cankoz-add()'е размеру очереди это число прибавлено.

    • Замечание: регистр статуса/управления контроллера CANIPP напрямую никак не адресуется, а работает "косвенно" -- при чтении либо записи коробочек, если адрес содержит биты 7 (ST2, программный запуск) или 6 (ST1, разрешение внешнего запуска).

      Поэтому, чтобы ответный пакет нам никак не помешал, используется адрес 15 -- раз он бродкастный, то чтение по нему лишено смысла и в _in() он специально отфильтровывается.

    • Теперь StartMeasurements() -- программирование.
      1. Еще вчера была сделана "инициализация" -- нуление rflags и сброс флагов полученности.
      2. Во все online-коробочки записываются уставки (сначала _ons-команда записи, потом фиктивное чтение; "фиктивное" потому, что используется лишь для приостановки очереди, а возвращаемое значение в _in()'е игнорируется).
      3. Затем взводится флаг ST1 -- тоже, естественно, _ons-командой.
    • Осталось:
      1. Заполнить AbortMeasurements() - чтоб он запрещал запуски.
      2. Таймаут, начинающий тикать по приходу запуска.
      3. Отработка _ff()/is_a_reset.
      4. Кстати, по ЛЮБОЙ из этих причин нужно делать "if (bias_state!=_PRESENT) bias_state=_ABSENT".

    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 оно так не было сделано лишь потому, что там серия "читаем, возвращаем" по-коробочно, а тут -- сначала всё получено полностью, и лишь потом отдаём наверх.)

    • Сброс фона при "!=ABSENT && !=PRESENT" помещён в AbortMeasurements() -- которая вызывается, посредством PerformTimeoutActions(), и по read_timeout'у, и по _ff()/is_a_reset.
    • В AbortMeasurements() засунута отправка пакета, сбрасывающего разрешение стартов. Делается аж 2 раза:
      1. Отправка через SQ_TRIES_DIR, чтоб отработался как можно быстрее.
      2. Также ставится в очередь, с TRIES_ONS -- на случай, если немедленная отправка обломится, чтоб сброс ST1/ST2 до устройства всё же дошёл бы.

      А очистку очереди перед этим не надо ли?

      Ответ: нет, лучше не надо.

      • Из общих соображений -- в очереди могут сидеть какие-то иные команды (запросы на чтение чего-то (да, пока нечего, но всё же), сканирование), и не стоит их грохать, т.к. предысторию мы не знаем.
      • А если причина вызова -- _ff/is_a_reset, то очередь к этому моменту уже очищена cankoz_lyr'ом.
    • Теперь хоть CHAN_STOP делай.

      Кстати, места под сам канал навалом -- U0632_CHANR_WR_count=50, а задействовано всего 3 (под BIAS_*).

    16.04.2018: сделана первая проверка. Результат обескураживающий:

    • Оно вроде работает, да (после исправления бага, что все данные отдавались от 0-й коробочки). Но:
    • Беркаевский can_serv_qt падает по SIGSEGV.
    • У меня же данные обновляются (ура-а-а!), но не с той частотой, с какой идут запуски.

      То ли косяк, то ли чего-то недопонимаю...

    Надо уже с Беркаевым разбираться предметнее (всё равно надо обговаривать детали).

    11.11.2018@пультовая, воскресенье, после обеда: разобрались с Беркаевым предметнее.

    • Еще вчера выяснилось, что ему нужны СЫРЫЕ данные -- т.е., без калибровок, которые по M.

      Поэтому в cdaclient'е он смотрит каналы с префиксом "@.", а в софтине регистрирует с NO_RD_CONV.

      13.11.2018: тем более, что в режиме CHEREP калибровок быть и не должно, и они как раз теперь устранены. Теперь бы и NO_RD_CONV уже стал бы ненужен.

    • Почему данные обновлялись не с той частотой, с какой идут запуски: да потому, что CAN-шина на 125kbps не успевает передавать данные для 32 ячеек от каждой используемой коробочки -- не хватает пропускной способности.

      А после перехода на чтение всего 9 ячеек -- стало успевать.

    11.11.2018@пультовая, воскресенье, уже вечером в районе 19 часов: был косяк с прописыванием настроек: зачем-то 32 раза (CANIPP_CELLS_PER_UNIT) пыталось писать в ячейку с адресом cell (т.е., де-факто -- 32-ю, ПОСЛЕ всех ячеек данных), да еще и контрольное вычитывание затребовывало.

    Обнаружилось это под вечер, когда уже была реализована работа с каналами CREG, и оказалось, что они почему-то не оказывают никакого влияния на сигнал, да и вовсе не прописываются (после рестарта сервера вычитывались старые значения).

    • Естественно, что при таком косяке смена настроек тупо не работала.

      Да еще и пропускную способность CAN-шины впустую сжирало.

    • Как такое умудрилось попасть в код -- загадка; особенно учитывая, что общая схема, включая этот StartMeasurements(), копировалась из u0632_drv.c, где ничего подобного нету.

      Факир был пьян?

    • Как бы то ни было, дурацкий цикл заменен на 1 правильную запись, и всё заработало.

    13.11.2018: а, во -- стало ясно, почему там после записи еще и чтение выполнялось: в качестве "прореживания": иначе, при отправке сразу толпы _ons-пакетов, велик шанс потери, причём неопределяемой.

    • Так что надо б туда какое-нибудь чтение засунуть.
    • Кста-а-ати, а не по этой ли причине время от времени некоторые коробочки отваливаются?!

      Ведь "разрешение запусков", в виде обращения к "регистру ACCESS", делается тоже _ons-командой, которая точно так же может потеряться.

      С другой стороны, sendqlib при ошибке отправки -- когда sender() вернёт !=0 -- просто уставляет таймаут для ретрансмита, но НЕ выкидывает пакет из очереди, даже ONS'ный.

    • Поговорил с Беркаевым, на тему как у него делается и как можно делать.
      • У него CREG-регистры ВЫЧИТЫВАЮТСЯ сразу после данных, чтобы если что-то изменится, заметить это.
      • ПРОПИСЫВАЮТСЯ -- в момент прихода команды на запись от юзера: он прописывает регистр, а потом тут же взводит ожидание стартов.

        Да, это даёт race condition, но он считает такое нестрашным (ну пропустит один запуск...).

      • "Разрешение запуска" -- да, тоже обращением к "broadcast'ному" адресу.
      • Как МОЖНО сделать: не записывать каждый раз, а по приходу запроса на запись в регистр (или {M,P}) взводить флажок, и потом в момент "программирования на ожидание запусков" для тех, у кого флажок взведён -- запись выполнять.

    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 периодически посылать пакеты с разрешением запусков.

    • Могут ли тут быть какие-нибудь побочные эффекты?
    • Сходу -- вроде ничего.

      Разве что

      • Попадём отправкой такого пакета в узкое временнОе окно, когда бридж уже отправил 0xFE, но тот до нас ещё не доставлен и потому состояние всё ещё WAIT.
      • Тогда бриджу будут вновь разрешены запуски, и он теоретически сможет без спросу стартануть вновь.
      • Но до следующего импульса запуска ещё далеко, а через считанные миллисекунды ему повалятся команды чтения, которые разрешение запусков тут же сбросят (т.к. бита ADDR_ST1 в адресах не будет).

      Иных проблем не просматривается.

    • Беркаев тоже сходу ничего против придумать не смог (и, возможно, его собственный софт именно так и делал; надо будет отрыть его на ВЭПП-2000).
    • Делаем:
      • Заведена canipp_start_hbt(),
      • ставящая себя на следующее срабатывание через 10 секунд,
      • и при read_state==READ_STATE_WAIT отправляющая точно такой же пакет разрешения.
      • Она вызывается из _init_d() для первого запуска; но при этом она лишь ставит себя на срабатывание, но НЕ отправляет пакет (т.к. state!=WAIT).
    • Результаты:
      • Вроде работает -- за час не было ни единого подвисания, хотя раньше в течение минут 10 обязательно хоть один бридж да отваливался.
      • По sktcanmon'у видно, что время от времени бриджу отсылается пара пакетов подряд -- при частоте запусков раз в 3сек и периоде "повторного прошения" 10сек доп.пакеты довольно часто попадают в интервал между штатным пакетом и запуском.

      Проблем вроде не наблюдается.

    Ура!

    02.04.2022: не факт, что точно "ура": сегодня возникла какая-то странность. Подробнее см. ниже за сегодня.

    11.12.2022: да нет -- факт, факт: "странность" обуславливалась тем, что по 0xFF/is_a_reset сканирование НЕ повторялось.

    17.11.2018@вечер-дома: кстати, есть халтура с отдачей CUR_-параметров (CUR_{M,P,CREG}): ведь отдаются не те, с которыми было запущено измерение, а РЕАЛЬНО-ТЕКУЩИЕ -- т.е., если с момента старта измерения успела придти команда записи в M/P/CREG, то отдастся уже изменённое значение, НЕ соответствующее тому, с которым выполнялось измерение.

    Надо б как-то буферизовать в StartMeasurements() -- переписывать в специальное место, откуда потом и отдавать CUR_'ы.

  • 14.05.2018: внесена модификация в код возврата в 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".

    История разборок такова (в логическом порядке, но не в хронологическом):

    • Как-то в прошлом году (хотя, судя по датам -- аж 16.08.2016...) накололся при попытке сборки hw4cx на bike -- ругалось на vsdc2_drv.c.

      Тогда особо не стал разбираться.

    • Но помнилось, что, вроде бы, на linac1 -- под тем же gcc-2.96@RH73 -- всё собралось.
    • Вчера попробовал еще раз собрать свежие исходники на bike -- фиг, ругается (только уже на canipp_drv.c, на что я не обратил особого внимания -- а это было ключом!).
    • Сегодня уговорил Пивоварова включить linac1 (он уже уволен с пульта) для разбирательства. Типа -- а вдруг там просто другая версия gcc.

      Фиг -- та же 2.96.

    • Окей -- стал пробовать собирать свежий вариант дистрибутива в /tmp/, и-и-и... та же проблема, что и на bike!
    • А имевшееся в ~/work/hw4cx/ проблем не давало.
    • Сравнение дат исходных файлов в ~/work на обеих машинах с текущими файлами показало, что vsdc2_drv.c на bike за 04-08-2016, а на linac1 -- уже за 19-09-2016, как и текущий.
    • Что, видимо, происходило:
      • 19.09.2016 при сборке "всего" на linac1, очевидно, вылезла проблема не-компилируемости.
      • Видимо, были внесены изменения в исходник, конкретно vsdc2_in(), чтоб оно стало собираться.
      • О чём было благополучно забыто.
      • А canipp_drv.c делался уже в этом апреле, и в него аналогичные экзерсисы НЕ вставлялись.
      • И проблема "ой, на bike не собирается!!!" вылезла снова.
    • После внесения аналогичных модификаций в canipp_drv.c::PerformReturn()
    • Собственно изменения в исходник (вроде ж записывал, а нету нигде...) -- вставлено условие, что при "#if __GNUC__ < 3" вместо прямого возврата (литералами) объявляются массивы для всех параметров, заполняются, и уже они сбагриваются ReturnDataSet()'у -- что прокатило бы даже в C89.
    • Некоторый вопрос возник -- а что, больше нигде не используется возврат inline-литералами?

      Результат поиска (grep -A5):

      1. hw4cx/drivers/camac/src/u0632_drv.c (откуда в canipp_drv.c и скопировано!).

        Но оно собирается для CM5307, gcc-3.2.2.

      2. hw4cx/drivers/camac/src/c061621_drv.c -- можно было бы сказать, что "аналогично", если бы не следующий пункт.
      3. В нескольких файлах используется возврат 1 канала, т.е., "массивы" там из 1 элемента. Это formula_drv.c (в bike:~/work/ аж от 23-12-2015!) и iset_walker_drv.c.

        А они компилируются без проблем.

        Что, глюк проявляется в 2.96 только при бОльшем количестве элементов?

    Засим считаем расследование завершённым и рецепт найденным (хотя сколько еще будет актуальна сборка под 2.96@RH73 -- хбз :)).

  • 24.07.2018: как выяснилось в разговоре с Беркаевым, черепановские "коробочки" имеют формат регистра управления, отличающийся от репковских: там индивидуальных задержек (T) нет вовсе, зато на усиление отдано больше битов.

    Причём сейчас у нас на кольце/канале -- ТОЛЬКО черепановские коробочки; а в будущем предполагается смесь.

    Надо б это будет уметь учитывать: чтоб мочь в auxinfo указывать по-адресно типы.

    25.07.2018@утро-дома: вариант решения: понятно, что надо иметь гигантскую PSP-таблицу для возможности указания индивидуально типа каждой коробочки.

    Но в случае, когда они все какого-то одного типа, перечислять все -- неудобно.

    Завести отдельный ключ типа "defkind", которым указывать умолчание -- repkov или cherepanov; и чтоб по-штучные типы имели значения не 0, а 1(repkov) и 2(cherepanov), 0 же оставить за "не указано"; и для тех, у кого в типе значится 0, прописывать значение из умолчания.

    26.07.2018: да, именно так и сделано:

    • Параметр def_kind и 30 штук индивидуальных параметров 0...31, ставящихся именно в той логике, что описана вчера.
    • Возможные варианты значений -- r, repkov, c, cherepanov.
    • Результаты этих танцев попадают в массив unit_kind[].
    • Вот только значения в нём пока никак не используются -- формат черепановского регистра мне ещё неизвестен.

    03.11.2018: поговорил с Беркаевым, выяснил специфику: у черепановских параметр P -- отсутствует, а M -- занимает 3 бита вместо 2.

    Надо заметить, что это влияет также на BIAS etc. -- ведь съём фона делается с перебором всех комбинаций {P,T}, а тут их набор иной... С другой стороны, как говорит Беркаев -- для ЭТИХ коробочек набор фона скорее бессмыслен, т.к. там что-то сильно хитрее и его лучше реализовывать в высокоуровневом софте.

    11.11.2018@пультовая, воскресенье, после обеда: сделано, вот как:

    • Исходная посылка: биты в регистре могут быть вусмерть поперепутаны -- это зависит от распайки в конкретной коробочке. Беркаевский софт умеет с этим работать, но для всего прочего какое-либо использование битов управляющего регистра (в т.ч. для получения M) теряет смысл.

      Вывод: отдавать наверх надо именно регистр, и принимать от клиентского софта регистр же целиком (16 бит), а с M его путать никак не нужно.

    • Добавляем каналы CREG:
      • Блоки по 30 каналов в областях r и w в принципе найти можно, но это неправильно -- лучше добавить.
      • Поэтому карта расширена еще на 100 каналов (с 400 до 500) -- плюс r30i,w30i,r40i.
      • Первые 30 -- это cur_CREGn, вторые 30 -- CREGn, и еще 40 -- резерв для круглого числа.
      • Также добавлены alias'ы на них -- box<0-29>.cur_CREG и box<0-29>.CREG.
    • "Читаются" и "отдаются наверх" при сканировании и {M,P} и CREG всегда (а при возврате данных к триплет {CUR_P,CUR_M,DATA} превратился в квадруплет {CUR_P,CUR_M,CUR_CREG,DATA}), и принимаются сверху запросы на запись и в те и в другие всегда (хотя лучше бы это подправить).
    • Но в устройство отправляется либо комбинация {M,P} (в режиме REPKOV), либо CREG (в режиме CHEREP).
    • Также в режиме CHEREP игнорируется понятие "фон": и при накоплении такие коробочки пропускаются, и при отдаче наверх вычитание фона не делается.
    • А еще калибровка -- RDs -- отдаётся наверх только в режиме REPKOV.

    Результат:

    • По факту получилось такое деление:
      1. Тип REPKOV -- управление через {M,P}, поддержка фона и калибровок в-зависимости-от-M.
      2. Тип CHEREP -- управление CREG напрямую и отсутствие какой-либо интерпретации его содержимого, в т.ч. отсутствие использования фона.
    • Проверено Беркаевым -- вроде работает как надо.
  • 26.07.2018: отдельный вопрос: а нафига у нас canipp_drv_i.h и canipp.devtype? Ведь списки каналов идентичны, и в devlist'е можно указывать "u0632/canipp@sktcankoz" (проверено, работает).

    11.11.2018@пультовая, воскресенье, после обеда: а вот потому, что сейчас карты разошлись: у CANIPP появились канал read_state плюс каналы CREGn и cur_CREG; а еще понадобится завести каналы NUMPTSn (вместо нынешнего const32).

    Другое дело, что и для u0632 каналы CREG и NUMPTS могут оказаться нужны -- если вдруг там тоже понадобится внедрить аналогичные фичи (поддержку черепановских коробочек и настраиваемую длину вычитывания).

    18.12.2018: а вообще этот вопрос был разобран ещё 04-04-2018, в самом начале раздела.

    Первейшая причина -- невозможность указать для удалённых драйверов (которые через remdrv) имя драйвера, отличающееся от имени типа.

  • 03.11.2018: новая потребность: надо уметь вычитывать не все 32 слова, а лишь первые N. Смысл в том, что у ИПП в канале K500 используются лишь первые штук 8 проволочек, причём вычитывание "всего" тупо не успевается (125kbps, по 2 пакета на чтение каждого значения (32*2=64 пакета на коробочку), толпа коробочек на 4 контроллерах -- не хватает пропускной способности линии).

    Видимо, нужно завести per-box-параметры (аналогично kind) с указанием номера количества, со значением по умолчанию =32. И наверх отдавать тоже ровно то же количество, а не 32.

    Надо заметить, что, судя по анализу кода, наличие этого N влияет также на

    1. процесс определения "всё прочитано" -- CELLS_PER_UNIT,
    2. плюс адреса в запросе (+1/-1) -- сейчас-то запрашивается тупо 32 адреса, а там есть сдвиг на 1 проволочку, но мы пользуемся тем, что всё равно просим всё.

    11.11.2018@пультовая, воскресенье, после обеда: сделано.

    • Введены по-канальные unit_nwires[].
    • Указание по аналогии с типом: можно указывать на всех по умолчанию -- def_nwires=COUNT, либо индивидуально nwiresN=COUNT.

      Умолчательное количество -- CELLS_PER_UNIT=32.

    • Теперь и инициализируются только первые Nwires ячеек, и запрашивается чтение их же, и проверка "всё ли пришло?" делается тоже по этому количеству, и наверх отдаётся столько же.
    • Касательно "сдвига" адресов -- махинации в 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".

    ...хотя может еще потребоваться:

    1. Завести каналы NUMPTSn для отдачи наверх количества (хотя lib/pzframe/wirebpm* и hw4cx/pzframes/u0632* это использовать вряд ли будут -- смысла не видно, там проще считать всегда 32.
    2. Уметь вычитывать не с 0-й ячейки, а с N-й (конкретно с 1-й).

      Для чего тогда придётся заводить параметры def_firstwire и firstwireN, плюс каналы PTSOFSn.

  • 03.11.2018: возможно, понадобится сделать командный канал "повторить сканирование".

    Это для ситуации, когда коробочки глючат или подключаются "потом". Чтоб не приходилось рестартовать сервер или драйвер.

    Очевидно, нужно будет переводить машину состояния в начальное состояние; просто выполнять то же, что по 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 не посмотрел (вообще не помнил о нём, а просто старался решить проблему побыстрее).

    • Звонит Лебедев, что там почему-то перестали отображаться данные от ИПП-каналов, ну он возьми да передёрни питание (видимо, ipp_gw'ей, т.к. от них ото всех 4 пришли 0xFF); причём делал это дважды, и с изрядным интервалом (кусок из логов тут ниже закомменчен).
    • Приход данных не возобновился.
    • Я сделал всем 4 ipp_gw'ям рестарт драйвера с помощью "._devstate=-1,+1" -- работа возобновилась.

    Хбз теперь, в чём же была причина: то ли "коробочки время от времени "отваливаются" и перестают присылать данные" от 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 оно считает из устройства; тем более, что при "повторном сканировании" сделалось бы то же самое).

  • 11.11.2018@пультовая, воскресенье, после обеда: некоторые размышления на тему "повторного сканирования", в автоматическом режиме:
    • Можно ведь оное выполнять сразу после вычитывания данных всех коробочек (после произошедшего старта).
    • Тут как раз время на эту операцию почти гарантированно есть -- следющий старт вряд ли будет впритык по производительности/пропускной способности.
    • И дополнительное чтение -- как бы всего +1 операция к предыдущим 32 (ну или к 9 :)).

    Идея подсказана Беркаевым -- он в своём софте примерно так поступал.

    Весь вопрос только в том, как втиснуть это дополнительное чтение в общую диаграмму работы.

    12.11.2018: анализируем код и размышляем.

    • Насчёт "сканирования" после вычитывания всех: можно ж запрашивать чтение CREG не у всех, а лишь у тех, где unit_online[unit]==0.

      Таким образом будут "подхватываться обратно" временно отрубавшиеся коробочки.

      Хотя и вопрос, как же будут переводиться в online:=0 реально убранные. Только по полному сканированию?

    • Как впихнуть: в drdy_p() и в PerformTimeoutActions() вместо нынешнего RequestMeasurements() вызывать RequestScan().

      Только:

      1. По таймауту, возможно, надо принудительно проводить ПОЛНОЕ сканирование, а не только тех, кто ранее-не-online.
      2. Надо прямо в RequestScan() делать read_state:=READ_STATE_NONE.

    11.12.2022: поскольку проблема сканирования по 0xFF решена, то вопрос "повторного сканирования" становится не шибко актуальным. В крайнем случае можно устройству сделать ._devstate=0 для рестарта, и оно пере-сканирует.

    Так что ставим "withdrawn".

  • 30.11.2022: оказывается, и просто 0xFF/is_a_reset корректно НЕ отрабатывается: вот Федя дёрнул питание коробочек, и они просто перестали что-либо присылать; для восстановления пришлось им делать _devstate=0.
    • Заглядывание в логи показало, что никакого сканирования НЕ проводилось. Хотя в такой ситуации -- возможно, и фиг бы с ним.
    • Хотя вроде бы делается PerformTimeoutActions(), из которого вызывается RequestMeasurements(); но почему-то этого не хватает.
    • Какие-то значения не прописываются? Что странно -- оное прописывание выполняется в StartMeasurements(), которая из RequestMeasurements() и вызывается.
    • Видимо, придётся повторять "эксперимент" -- дёргать питание, одновременно наблюдая за состоянием каналов read_state.

    01.12.2022: ещё порылся по коду, и возникла идея (точнее, НАМЁК на неё :D) о том, что там может идти не так.

    Ключевое соображение -- интерференция сканирования с обычными измерениями.

    Факты:

    • "Сканирование" -- это просто запрос чтения ячейки CANIPP_CADDR.
    • Результаты же оного чтения обрабатываются ТОЛЬКО в состоянии READ_STATE_NONE.

    Обдумывание следствий:

    • Получается так, что
      1. при !is_a_reset (т.е., при старте драйвера и ответе GETDEVATTRS) будет просто запущено сканирование;
      2. а при is_a_reset (т.е., в АВАРИЙНЫХ случаях) -- сначала будут принудительно запрошены измерения и сделан переход в READ_STATE_WAIT, а уж потом запрос сканирования.

        ...результаты какового сканирования будут ПРОИГНОРИРОВАНЫ.

    • А не "рубит" ли сам факт чтения ячейки битик готовности к приёму запусков в контроллере CANIPP?

    Соображения по исправлению проблемы:

    • Обычно-то запрос измерений -- RequestMeasurements() -- вызывается по окончанию сканирования, по получению значения регистра CANIPP_CADDR от последней коробочки.
    • А при 0xFF/is_a_reset -- прямо из canipp_ff(), посредством PerformTimeoutActions().
    • Напрашивается, что в такой ситуации надо НЕ запрашивать чтение напрямую из canipp_ff().
    • Встаёт вопрос -- КАК это лучше сделать?
      1. Очевидный вариант -- добавить ещё один булевский параметр, вроде "do_request", который почти всегда указывать 1, но конкретно тут -- 0.
      2. Но заглядывание в pzframe_drv.c::PerformTimeoutActions() -- первоисточник всего подхода -- показывает
        if (do_return)
            ReturnMeasurements (pdr, 0, CXRF_IO_TIMEOUT);
        else
            pzframe_drv_req_mes(pdr);
        

        И:

        • Причём в canipp_drv.c ВСЕГДА указывается do_return=1.
        • А в pzframe_drv.c -- единственное исключение по "слепому STOP'у" -- когда делается STOP=2.
        • ...аналогом которого, по факту, и является "аварийный 0xFF".

        Откуда напрашивается идея: ну так и добавить в 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 не нашлось.

        • 11.12.2022: а вот сегодня причину понял: смысл в том, что если уж делаем "действия по таймауту" -- "TimeoutActions" -- то после них нужно В ЛЮБОМ СЛУЧАЕ сразу же заказывать измерения заново.

    Ну -- делаем, по тому проекту (b):

    1. В PerformTimeoutActions() добавлен else (т.е., ЛИБО возврат данных с флагом "таймаут", ЛИБО запрос новых измерений).
    2. И вызов её из 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 файлах:
      1. pzframe_drv.c --
        pdr->abort_measurements(pdr);
        if (do_return)
            ReturnMeasurements (pdr, 0, CXRF_IO_TIMEOUT);
        else
            pzframe_drv_req_mes(pdr);
        

        Используется в 2 точках: собственно таймаут и "принудительный стоп"; в первом do_return=1, а во втором может быт и 0 в зависимости от типа стопа.

      2. u0632_drv.c --
        AbortMeasurements(me);
        if (do_return)
            ReturnMeasurements (me, CXRF_IO_TIMEOUT);
        
        RequestMeasurements(me);
        

        Используется в одной-единственной точке -- по таймауту, с do_return=1.

    • Но конкретно с CANIPP у нас есть существенное отличие от прочих pzframe-подобных: тут есть потребность в промежуточном "состоянии" между сбросом в READ_STATE_NONE и последующим запуском измерений: нужно ещё успеть выполнить то самое сканирование.

      Т.е., НЕЛЬЗЯ сразу запрашивать новые измерения.

    • Откуда и вытекает надобность иметь ДВА РАЗНЫХ параметра-флага:
      1. "выполнять ли возврать CXRF_IO_TIMEOUT'нутых данных" -- нынешнее do_return;
      2. "запрашивать ли новое измерение" -- условный "do_request".

      Громоздковато.

    • Но можно и оптимизировать:
      • +1 -- И вернуть CXRF_IO_TIMEOUT-данные, И запросить новое измерение.

        Аналог u0632_drv.c'шного do_return=1.

      • 0 -- ТОЛЬКО запросить новое измерение.

        Аналог общепринятого do_return=0.

      • -1 -- не делать НИ того, НИ другого.
    • @вечер: изначально сделал иначе:
      • +1 -- ТОЛЬКО вернуть данные;
      • 0 -- ТОЛЬКО запросить новое;
      • -1 -- ничего.

      Но потом осознал, что в такой ситуации любой таймаут сразу будет навечно (до 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);
      }
      

    Проверка:

    • Тестировалось придуманным вчера способом -- отправкой с компьютера CAN-пакета "от имени" тестируемого устройства.
    • Да, теперь работает правильно -- сканирование по 0xFF/is_a_reset выполняется.
    • И, чисто для проверки было временно вёрнуто do_return=0 и вокруг вызова PerformTimeoutActions(me,0) добавлен отладочный вывод значения read_state: так оно показало значение 2, READ_STATE_WAIT.

      Это подтверждает верность вчерашней гипотезы про некорректность предыдущего решения.

    Поскольку "всё сложно", то нет уверенности, что теперь сделано абсолютно правильно, а подождём результатов эксплуатации.

    11.12.2022@вечер-ужин: а вообще вся эта неопределённось и "всё сложно" -- из-за того, что

    • драйвер сделан схалтуренно: вместо ПОЛНОЦЕННОЙ машины состояний -- с отдельным состоянием "сканирование" и надлежащими функциями перехода --
    • "по-простому", а состояния лишь задним числом прилеплены к коду, изначально взятому от CAMAC'овского u0632_drv.c,
    • где надобность в машине состояний отсутствует (по причине локальности) и достаточно простого булевского флага measuring_now.
    • ...хотя даже в pzframe_drv были добавлены рудиментарные состояния, уже после изготовления canipp_drv.c, 19-04-2021.

    В очередной раз убеждаюсь в том, что

    1. Хоть сколько-то нетривиальная (более чем просто запрос-ответ) работа с удалёнными устройствами требует машины состояний.
    2. Криво сделанные устройства на локальной шине (как тот же У0632) -- это неприятно; но вот криво сделанные устройства на УДАЛЁННОЙ шине -- это кошмар.

    ЗЫ: а вот если бы изначально был сделан драйвер 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!

weld02_drv:
  • 29.06.2018: дата создания раздела.
  • 29.06.2018: давно было известно, что понадобится добавлять канал(ы), что сильно печалило -- карта-то под завязку.

    Вчера Жариков прислал документ (20180628-zharikov-svarka-chemy.docx), из которого следует, что добавить надо будет только 1 канал измерения. И чтоб спрашивался он лишь при новой прошивке -- sw_ver>=9 (на сварках в 13-м здании sw_ver=8).

    А сейчас посмотрел драйвер и карту внимательнее -- оказывается, УЖЕ есть заразервированный канал измерения, как раз 9-й. Достаточно будет его разрешать спрашивать при подходящей версии прошивки.

    01.07.2018: сделано, и в hw4cx/, и в v2cx/.

    1. Именование:
      • Переименовано WELD02_MES_n_RSRVD->WELD02_MES_n_I_INDR и WELD02_CHAN_MES_RSRVD->WELD02_CHAN_MES_I_INDR.
      • В weld02.devtype добавлено mes_i_indr=17.
    2. Поддержка:
      • Введено поле supports_adc9, ...
      • ...которое выставляется в _ff() в (sw_ver >= 9), ...
      • ...а в rw_p() разрешается запрос каналов измерения либо при !=WELD02_CHAN_MES_I_INDR, либо при взведённом supports_adc9.
    3. Клиентская сторона:
      1. Коэффициенты:
        • В hw4cx'ном weld02_init_d() добавлен SetChanRDs(, WELD02_CHAN_MES_I_INDR, 1, 4095.0/250.0, 0.0).
        • В db_baachi.h -- соотв. строчку в physinfo[].

        За компанию там отражено то, что на чёмской сварке в 5 раз отличается коэффициент при измерительном канале 2 (WELD02_CHAN_MES_UN):

        • В v4'шном weld02_ff() принудительно делается
          SetChanRDs(devid, WELD02_CHAN_MES_UN, 1,
                     (sw_ver <= 8)? 4095.0/125 : 4095.0/25, 
                     0.0);
          
        • В db_baachi.h просто сменен захардкоженный коэффициент.
      2. Наэкранные ручки в нижней панели -- "Измерения контроллера пушки":
        • 2-я строка переименована из "U высокого" в "U косв.нак.".
        • Сам элемент сделан 2-колоночным, в 1-й и 3-й строках справа noop'ы, ...
        • ...во 2-й добавлено "I=" со ссылкой на I_INDR.
        • Имевшиеся ранее махинации с horz=fill в элементе и align=right в клетках -- убраны.

    ReSendSetting() выглядит бессмысленным из-за того, что для каналов записи НИГДЕ не взводится known.read[].

    15.01.2019: поскольку канал ADC#9 работает (правда, в v2cx, но это непринципиально_, то данный раздел закрываем.

    28.01.2019: уже и в hw4cx тоже проверено что работает.

  • 15.01.2019: понадобилось добавить ещё 1 канал ЦАП. Тоже для sw_ver>=9.

    Тут уж точно только раздвигать карту.

    15.01.2019: делаем.

    • Карта каналов:
      • Введён "неиспользуемый" идентификатор DAC#8 WELD02_SET_n_UNS8 (т.к. канала 8 не существует, а сразу идёт 9).
      • И собственно нужное -- идентификатор WELD02_SET_n_PIND и канал WELD02_CHAN_SET_PIND с именем set_p_indr.
      • С соответствующим расширением карты -- с w8i,r10i до w10i,r10i, ...
      • ...плюс сдвигом номеров каналов чтения на 2 вверх.
    • Реализация:
      • Добавлено поле supports_out9, заполняемое аналогично supports_adc9 -- = (sw_ver >= 9).
      • В _rw_p():
        1. Для несуществующего канала WELD02_CHAN_SET_UNS8 добавлено отдельное ограждение, чтобы он всегда отдавался как UNSUPPORTED.
        2. А для WELD02_CHAN_SET_PIND -- условное ограждение, только при не-supports_out9.
      • Поскольку изначально supports_out9=0, а устройство "рождается" в состоянии OPERATING (плохо!!!), то первый запрос на вычитывание уставки будет отфутболен.

        Поэтому в weld02_ff() вставлена принудительная отправка команды вычитывания -- 0x99 -- в случае, если канал должен поддерживаться. Тогда он вначале взморгнёт бордовым и потом тут же онормалится.

    28.01.2019: проверено -- работает.

    Кстати, чёмская сварка переведена на v4.

  • 04.08.2021: изменение, общее для двух предыдущих пунктов: каналы номер 9 и ЦАП, и АЦП следует поддерживать ТОЛЬКО для sw_ver==9, а не для ">=9".

    Как выяснилось, они есть ТОЛЬКО на чёмской сварке с версией 9, а на "новой маленькой", где версия 11, их нет.

    Поэтому условие заменено.

    04.08.2021: а ещё оказалось, что у канала MES_UN коэффициент R -- указываемый в той же weld02_ff() -- равен 4095.0/25 тоже ТОЛЬКО для sw_ver==9, а для всех остальных (не только <9, но и для >9, включая текущую 1) -- 4095.0/125, как и раньше.

    Так что и тут условие заменено.

slio24_reg:
  • 21.09.2018: занадобился драйвер SLIO24 для железки Куркина, управляющей задающим генератором.

    Поскольку тут весьма специфическое использование, а формально платка позволяет организовывать полноценный serial link, то для данного конкретного варианта использования ("просто один 24-битный регистр, который можно писать и читать") делаем драйвер под названием "slio24_reg".

  • 21.09.2018: сделан за полчаса, на основе драйвера от CPKS8 (самый маленький). Теперь проверять.

    24.09.2018: проверил -- пишет, и потом записанное читает (также "независимо" проверялось can-монитором).

    Теперь для реального использования нужно будет добыть формулу пересчёта между частотой и этим значением, и постараться её в devlist-canhw-19.lst вбухать.

    26.09.2018: собраны данные от гусиной программы -- пишем частоту (DRVH, DRVL, DRVL+1, DRVH-1), грохаем EPICS, вычитываем число canmon'ом, переводим в двоичку и отдаём табличку Фролову. Фролов помаялся -- вроде там какой-то бардак, биты/байты перепутаны, но принцип уже виден. Думает.

    02.10.2018: навроде протокола-истории:

    • Еще 25-09-2018 Фролов прислал мыло, где рекомендовалось
      Формула для расчета кода (Code) в частотный детектор (24bit CAN SLIO24).
      Code=(val-eoff)/eslo
      eoff=2831316.8384
      eslo=0.0122886464
      
    • Тогда я был занят (статью на RuPAC-2018 писал), а вчера попробовал проверить с собранными 26-09-2018 данными -- что-то не совпадало.
    • Сегодня же проверил еще раз -- вроде худо-бедно сходится (с точностью до 1, из-за округления).
    • Те числа свелись к изготовлению cpoint'а (соответствующий фрагмент файла devlist-canhw-19.lst приведён целиком):
      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
      
    • Т.е., при переходе от EPICS'ного {ESLO,EOFF} к {R,D} преобразование простое: R=1/ESLO, D=-EOFF.

      Это так, к вопросу "как именно указывать способ преобразования инженерных единиц" от 24-08-2008.

    • Сделанное проверено Фроловым при помощи частотомера (выставлял в программке число и смотрел измерение) -- всё сходится с хорошей точностью.

    Засим можно считать, что драйвер работает как положено, "done".

cgvi8_drv:
  • 15.11.2018: заводим раздел.
  • 15.11.2018: еще года два-три назад в драйвер была вставлена фича под условным названием "DEBUG_CGVI8M_MASK":
    • Оно периодически (раз в 60с) вычитывает текучую маску (mask) и сравнивает её с "последней известной".
    • При несовпадении выдаёт сообщение в log.

    История:

    • Сделано было по просьбе Беркаева, у которого возникло подозрение, что маски в CGVI8M в стойках ГИД25 канала K500 (в области ответственности ВЭПП-5) ведут себя странно. Тогда для них еще использовался его софт, а мой уже тогда работал на ВЭПП-4, и чтоб я посмотрел, не бывает ли там подобных финтов.
    • На ВЭПП-4 ничего подобного замечено не было.
    • А вот на ВЭПП-5 сейчас перешли на мой софт (тот же самый), и в логи время от времени стали сыпаться сообщения "mask=%d, != last_known_mask=%d".

    К сожалению, Беркаев деталей той истории сейчас не помнит.

    А у меня возникли некоторые сомнения в корректности организации проверки:

    • Реально ли в девайсе что-то само меняется, или же это race condition при вычитывании?
    • Потенциально проблемы могут вылезти при следующих сценариях (анализ не точный, а "вдруг?"):
      1. Может возникнуть ситуация, когда сначала отправляется heartbeat'ный запрос, ПОТОМ идёт пара {ЗАПИСЬ,ЧТЕНИЕ}, и
      2. Может быть несколько модификаций разных битов маски подряд, так что последующие происходят ещё ДО отправки предыдущих (особенно если линия занята вычитыванием из CANIPP, то у пакетов CGVI8 есть шанс застрять до окончания всплеска).

        А поскольку там при отправке НЕ используется SQ_REPLACE_NOTFIRST, то в очереди накопятся множественные запросы.

    • Ну да, на ВЭПП-4 проблем никогда не замечалось.
    • Но, с одной стороны, они там, возможно, ручки крутят реже, а с другой, там и устройств на линии в разы меньше (в том числе, CANIPP-M нету, которые большой трафик дают).

      И анализ значений из лога показывает, что различие обычно ровно в 1 бит.

    Как-то надо бы управильнить работу, избавившись от этого потенциального race condition.

    16.11.2018@утро-дома: как именно можно "делать по-правильному"? А вот как: махинировать с флагами не в момент постановки пакетов в очередь, а в момент ОТПРАВКИ. Для чего использовать on_send-callback.

    1. Флаг rd_req_sent взводить оттуда.
    2. Также, вероятно, оттуда запоминать в last_known_mask записываемое значение.

      16.11.2018: неа, last_known_mask заполняется по ПОЛУЧЕНИИ пакета, а не в момент отправки.

    Одна проблема: в cankoz_lyr'е халтурно сделан on_send-callback: он НЕ передаёт данные самого пакета -- dlc,data[dlc], а для п.2 желательно бы брать записываемую маску прямо из пакета; особенно актуально будет, когда заюзаем SQ_REPLACE_NOTFIRST.

    16.11.2018: делаем.

    • Во-первых, в cankoz_lyr'е улучшен on_send-callback -- добавлен триплет desc,dlc,data[].

      ...и это оказалось ненужным :D

    • Во-вторых, собственно взведение rd_req_sent=1 только по отправке пакета.
      • Отправка 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) все каналы маски. А если щелкнуть любым из них -- то он ставится в указанное значение, а остальные "прорастают".

    И потом, уже в моём присутствии, оказалось, что они такие посиневшие прямо с самого запуска.

    • Несколько раз проверил -- да, прямо сразу такие, ВСЕГДА.
    • Выглядит так, будто пакет GETDEVSTAT игнорируется и данные не приходят вовсе; либо драйвер их успевает отдать, но потом делается RstDevTimestamps() -- он единственный, помимо CxsdHwSetDb(), где делается сброс timestamp.sec=INITIAL_TIMESTAMP_SECS.

      Но такого вроде бы не может быть -- ведь ответный пакет от устройства приходит ощутимо ПОСЛЕ старта и инициализации драйвера...

    • А с предыдущим драйвером, в котором нету вчерашней модификации с "уставкой rd_req_sent=1 в момент отправки" -- всё прекрасно.
    • Хотя логи пакетов выглядят одинаково.

    Надо б попробовать:

    1. Вариант с сегодняшним исправлением.
    2. Вариант по вечерней идее, с не-отправкой пакета при privptr==NULL.

    19.11.2018: проверяем...

    1. Вариант с исправлением "выставляем rd_req_sent=1 перед первым вызовом cgvi8_mask_hbt()": работает!!!

      WTF?! Т.е., rd_req_sent, должная быть просто диагностической фичей, влияет на ФУНКЦИОНАЛЬНЫЕ характеристики драйвера?!

      Пятью минутами позже: а-а-а, разобрался! Баран!!!

      • Всё просто: в cgvi8_in() при rd_req_sent==0 делается return вне зависимости от результатов проверки.

        Это для того, чтобы периодический поллинг не мешал обычной работе (чтоб каналы маски не генерировали обновления без спросу).

      • Соответственно, ответ на первый пакет GETDEVSTAT, отправляемый при rd_req_sent==0, просто игнорируется.
      • А запросы на первоначальное чтение каналов маски реализуются через постановку в очередь с how=SQ_IF_ABSENT. Т.е., при наличии в очереди пакета от диагностического запроса -- более ничего в очередь не попадёт.
      • Вот в результате ответ игнорируется, более запросов не шлётся, и канал остаётся в состоянии NEVER_READ навечно или пока не поступит запроса на запись (который будет отработан корректно благодаря "дополнительному уровню защиты" в виде mode.rcvd, вызывающему дополнительное чтение).
    2. Второй вариант тоже проверен. Только, для упрощения, выбрана противоположная кодировка: privptr==NULL означает "можно слать", а privptr!=NULL означает "НЕ шли".

      Тоже работает.

      Этот вариант и оставляем.

    22.11.2018: однако по логам видно, что события "несовпадения маски" всё же возникают, время от времени, причём:

    1. На нескольких устройствах одновременно.
    2. Разница почти всегда в 1 бит.

    Также оказалось, что аналогичные события всё-таки ВСТРЕЧАЮТСЯ в логах на vepp4-pult6:2:

    • С такими же симптомами (несколько устройств скопом, разница в 1 бит).
    • Причём всегда только на CGVI8M, и никогда на CGVI8 (который там есть 1 штука для ГИМНа).
    • Да, там старая версия драйвера, с потенциальным race condition, но 1) оно именно лишь потенциальное; 2) там на линии нету ИПП, которые могли бы её надолго занимать.
    • И, кстати, у нас-то на К500 эти CANIPP-M тоже не должны мешать: у них идентификаторы устройств имеют номера больше, чем у CGVI8 -- значит, приоритет отправки в шину ниже.

    Чтоб понять причину происходящего, на 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 секунд каждой минуты.
    • В районе 13:29:28 дежурный решил подёргать битиками маски.
    • Дёрнул, похоже, сразу парой бит подряд: 0x1F->0x1E->0x0E (14=0x0E, 30=0x1E).
    • ...и вот тут что-то пошло не так -- почему-то в дело вступила проверка "DEBUG_CGVI8M_MASK", хотя должно было быть взведено rd_req_sent=1.

      КАК оно могло оказаться сброшенным?

    Грустное впечатление, что все проблемы вызваны исключительно самой попыткой проверить "не меняется ли маска в устройстве сама" -- не будь этой проверки, и проблемы бы не было.

    Промежуточные выводы:

    1. Отключать эту диагностику нафиг -- видно, что изменения она детектирует некорректно, а сам девайс никаких косяков не даёт.
    2. Но желательно б всё же разобраться в причинах косячного поведения.

      А то вдруг в будущем понадобится что-то подобное -- чтоб уметь выполнять безглючно.

    11.12.2018: оказалось, что фича "отладка" была всё ещё включено, что на пульту сильно мешало. Поэтому на сейчас сделано DEBUG_CGVI8M_MASK:=0.

ckvch_drv:
  • 01.03.2019: создаём раздел.

    Смысл -- вчера, 28-02-2019, на еженедельной встрече по автоматизации было упомянуто, что, возможно, понадобится драйвер CKVCH -- для использования на ВЭПП-2000.

    Понадобится ли реально или нет -- хбз, но чисто ради практики (и для отвлечения от рутинной возни с CAN-серверами и прочего) можно взять да изготовить драйвер.

  • 01.03.2019: приступаем.

    01.03.2019: делаем "скелет", пока почти без функционала. За основу взят драйвер CPKS8.

    • ckvch.devtype.
    • ckvch_drv_i.h.
    • Скелет ckvch_drv.c -- уже собирается, и чуток "мяса".
    • Добавлены во все 3 Makefile'а.
    • @вечер, пультовая: добавлено ещё "мясо":
      • Сначала из curvv_drv.c взят блок "работы с регистром" -- как есть, БЕЗ REPLACE_NOTFIRST.
      • Потом переделано по образцу cankoz_lyr_common.c -- уже с 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.
    • Использование поля:
      • Каналы CONFIG_BITS и NUM_OUTS возвращаются прямо из _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.
    • Обрабатывается вместе с обычными outN: при записи -- отдельная if()-ветка, где ставится mask=0x0F, а чтение -- запрашивается так же (а как иначе?) и в _in() значение отдаётся рядышком.
gin25_drv:
  • 29.09.2022: решено начать делать драйвер будущего "контроллера накала кикеров" (?), изготавливаемого в СПб и унифицированного с "контроллером 3-х импульсного модулятора", описание CAN-протокола для которого получено от Сенченко 13-09-2022.

    (18.01.2023: Файл "CAN 3x imp_V1.4.docx", у нас это 20220913-senchenko-CAN-3x-imp_V1.4.docx -- о-без-пробелено.)

    Судя по переписке, устройство получило название "ГИН25", поэтому драйвер назовём "gin25".

  • 29.09.2022: приступаем.

    29.09.2022: в первую очередь создаём "скелет" -- файлы gin25.devtype и gin25_drv_i.h плюс gin25_drv.c (за основу взят cgvi8_drv.c, как близкий по "навороченности" плюс имеющий ту же длину имени).

    18.01.2023: вчера возникла уже реальная необходимость в драйвере, так что возвращаемся к работе над ним.

    Сначала обзор того, что ТОГДА было сделано (а что нет):

    • В скелет (от cgvi8_drv.c) была добавлена (очевидно, из какого-то АЦП-драйвера -- cac208_drv.c?) поддержка проверки "а приходили ли вообще данные?" в виде rd_rcvd и gin25_alv().

      (А я-то планировал сейчас начать как раз с добавления этой функциональности.)

      ...только определение собственно ALIVE_USECS было забыто :D.

    • Определены все DESC-коды.
    • И даже реакция на них в gin25_in() и...
    • ...и даже их отсылка в gin25_rw_p().
    • Т.е., использование GIN25_CHAN_*.
    • А вот что НЕ было -- определений этих GIN25_CHAN_* и вообще карта каналов; ни в gin25.devtype, ни в gin25_drv_i.h.
    • И ещё отсутствует "«мясо» работы с масками ошибок" -- функция ReturnErrorBits() и поле err_reg[].

    Т.е., по факту -- были сделаны почти ВСЕ мозги, но недоделаны определения.

    Делаем:

    • Первым делом исправляем мелочи с alv -- копируем определения ALIVE_SECONDS с ALIVE_USECS.
    • Затем работа с масками ошибок:
      1. err_reg[GIN25_NUM_LINES]
      2. ReturnErrorBits() -- собственно возврат скопирован с cankoz_lyr_common.c::cankoz_regs_f8(), где тоже одним махом возвращается 8 каналов-битов данных плюс 8-битное значение.
    • Ну и gin25_drv_i.h с gin25.devtype заполнены -- без этого ничего б не компилировалось.
    • ...пришлось изрядно поломать голову насчёт раскладки "статусовых" каналов -- на каждый канал по 8 бит ошибок, 1 8-битный канал с ними же, 1 канал "STATUS".

      В конце концов было решено побитовые каналы иметь одной пачкой 3*8 штук, а остальные отдельно группами по 3 штуки (а НЕ пачками по 10 штук (8 1-битовых плюс 1 8-битовый и 1 "STATUS")).

    Драйвер компилируется, теперь надо будет его тестировать.

    19.01.2023: и скрин gin25.subsys сделан.

    А ещё в переписке с Сенченко (вроде бы отвечающим за общение со стороны ИЯФ с авторами железки) выяснились любопытные "нюансы":

    1. Уставка и измерение будут не по 24 бита (как в описании), а по 16.

      Но "Формат пакета не поменялся. Как было 4 байта, так и осталось. Просто старший 0."

    2. "Когда они приезжали обсуждать Акимовские генераторы, мы договорились, что они переделают платы управления и ЦАП/АЦП будет в честных кило вольтах."

      "Там связь ЦАП/АЦП и киловольтов не совсем простая. Детали не помню, приведение к реальным значениям требовало калибровки. Договорились, что измерение/задание будет в вольтах (3000 для 3кВ)."

    3. "У нас (ИК и ВЭПП-2000) одноканальные источники. Три канал это специальная версия для Акимова."

      Как 1-канальный девайс будет реагировать на команды, обращённые к отсутствующим 2 каналам? -- "Не знаю. Не проверял."

    23.01.2023: небольшое изменение: теперь как IS_AUTOUPDATED_TRUSTED объявляются не все каналы группы [STATUS_first,+STATUS_count), а только [ERROR_first,+ERROR_count).

    • Оная группа "ERROR" (свежевведённая в _drv_i.h) -- это группа "STATUS", но без собственно 3 каналов "STATUS".
    • Смысл -- в том, что, судя по документации, автоматическая присылка по изменению делается только при изменении (точнее, ВЗВЕДЕНИИ!) битов ОШИБОК, но НЕ значения бита "Status".

      (И, поскольку оный "Status" определяется как "Control|Разрешение с передней панели", то он может дрыгаться с передней панели независимо.)

    • Поэтому троица [STATUS_n_base,+3) вычитывается из _rw_p() по запросу (и так оно было изначально).
    • Соответственно, маркировать троицу "STATUS" как AUTOUPDATED* категорически неправильно.
    • Вот и было в gin25_init_d() изменено -- чтоб троица "STATUS" была обычными опрашиваемыми каналами.

    08.02.2023: проведён первый тест на живой железке; ниже результаты, перечисленные в письме Сенченко и Касаеву (Message-ID: 466e35bd-344-d916-5522-4ac3ebd944d@starnew.inp.nsk.su).

    Всё тестирование проводилось вручную, при помощи программки-CAN-монитора -- и отправка команд, и слежение за линией.

    Сначала о странностях:

    1. Переключатели установки адреса работают как-то странно: при переводе в "1" джампера N2, который, судя по карте на стр.2 описания, должен был дать адрес 1 в 6-битной адресации Козака (или 4 в 8-битной адресации Панова), получили адрес 8.
    2. Команда 0x11 НЕ изменяет значение зарядного напряжения. Ни в ручном режиме, ни в режиме от компьютера -- какое значение ей ни посылай, всё равно присылает в ответ то, что было установлено раньше с передней панели. (Я даже пробовал вместо 4-байтного пакета (команда+3байта_данных) отправить 8-байтный (дописав в конце 4шт 0x00) -- в предположении, что какой-то косяк с проверкой размера пакета. Не помогло -- не уставляет.)
    3. Переключение в ручной режим управления -- выключение питания, втыкание разъёма, включение питания -- вовсе НЕ отрубает работу с CAN-bus: все команды воспринимались и давали ответ, в т.ч. команда 0x01 успешно включает непрерывные измерения. Единственное, что НЕ работает -- управление зарядом посредством команды 0x31. (А 0x11 не работает в любом случае.)
    4. Попытки обращения к каналам 2 и 3 -- команды 0x02, 0x03, 0x22, 0x23 и прочие 0x*2 и 0x*3 -- не дают никакого ответа, устройство их просто игнорирует. Это не очень хорошо, поскольку для совместимости лучше бы как-то на них отвечать -- либо всегда нулями в полях данных, либо каким-нибудь специальным кодом (вроде кода 0x00 в CAN-Панов aka CAN-FF). В нынешней же ситуации потребуется 2 варианта драйвера -- для 3-канального на ЛИУ и для 1-канальных на ВЭПП-2000 и ВЭПП-5.
    Теперь о хорошем:
    1. Команда выдачи измерений зарядного напряжения с указанной периодичностью 0x01 работает -- измерения присылаются, период регулируется.
    2. Команда включения/выключения заряда 0x31 работает -- заряд идёт, импульсы по внешнему запуску делаются.
    3. Никаких ошибок поймать не удалось -- по 0x41 в байте 3 постоянно возвращался лишь 0.

      Вследствие этого проверить работу команды 0xF0 -- отправка регистра статуса/ошибки при возникновении ошибок -- не удалось (было отправлено 0xF0,0xFF, но никаких уведомлений так и не поступало).

    Чисто для информации:

    1. В пакете 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), в ответ на который и будет посылка).
    • А вот во всех прочих местах оставлено именно NUM_LINES -- в первую очередь в 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.

    • Добавлено privrec-поле int DEV_LINES,
    • PSP-строчка "lines" для него, позволяющая указывать количество в диапазоне от 1 до 3, по умолчанию 1;
    • все релевантные сравнения переведены с GIN25_NUM_LINES на me->DEV_LINES;
    • GIN25_NUM_LINES убрано.

    21.02.2023: провели с Касаевым тестирование уже с правильной распиновкой ключика.

    • Работать-то работает -- при отправке CAN-пакетов вручную canmon'ом.
    • А вот через каналы из скрина -- фиг: работает всё (измерение напряжения, включение/выключение заряда, байт ошибок), кроме уставки напряжения; причём текущее уставленное напряжение читается, но показывается как для ВТОРОГО канала ("2").
    • Разобрался: косяк был в халтурном вычислении номера канала для пришедших пакетов 0x11-0x13/0x21-0x23: поскольку там общий кусок кода на оба типа пакетов (0x1x -- ответ-подтверждение на уставку, 0x2x -- чтение текущей уставки), то вместо обычной формулы "КОД_ПАКЕТА-БАЗОВЫЙ_КОД_ПАКЕТА" брался младший ниббл кода пакета, но ошибочно
      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 странности:

    1. Попытка записать любую уставку приводила к записи "максимума" 65534.
    2. Отображаемое значение измерения всегда было мелким, скача в пределах одного байта.

    Разобрался -- shame on me, косяки были тупейшие.

    1. Код отправки выглядел как
      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));
      
      вместо
      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, каковой максимум девайс и форсил.

      Будто откуда-то копировал (или "подсматривал"?), но вот откуда? Точно не из cgvi8_drv.c -- там подобного нету.

    2. А декодирование значения из байтов пакета -- как
      val = data[1] | (data[2] >> 8) | (data[3] >> 16);
      
      вместо
      val = (uint32)(data[1]) | ((uint32)(data[2]) << 8) | ((uint32)(data[3]) << 16);
      
      -- т.е., и сдвиг был вправо (по факту -- деление до 0) вместо влево, и байты к 32-битности не приводились.

      Неприведение -- видимо, неважно, т.к. в 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: у ЕманоФеди возникали вопросы "а чё постоянно возвращает одни и те же значения блокировок?"; причина-то в том, что в ответ на 0x41 (запрос статуса) возвращается, кроме байтов-битов CHARGE_CTL и STATUS также и байт ошибок (точнее, по авто-уведомлению по изменению тоже присылается 0x41).

    29.10.2023: поэтому драйвер переделан, чтоб возвращать ТОЛЬКО ИЗМЕНЕНИЯ.

    • Добавлено privrec-поле int lsrv[GIN25_NUM_LINES][3]; имено int, чтоб можно было поместить значение -1, не могущее появиться из устройства.
    • В gin25_ff() туда прописываются все -1.
    • А в gin25_in() возврат теперь делается условно -- только если пришедшее значение отличается от последнего известного (куда пришедшее при этом сохраняется). Из-за начальных -1 первое после 0xFF'а будет ВСЕГДА возвращаться.
    • Этот условный возврат делается для всех 3 байтов из 0x41.

    09.01.2024: однако, накосячено тогда было. Замечено почему-то только сейчас (сервер не перегружали с тех пор, что ли? непонятно...):

    1. Некорректно возвращаются биты ошибок: они НЕ возвращаются, и лишь по команде "сбросить!" взмаргивают и исчезают.
    2. Битик статуса постоянно горит DEFUNCT.

    Быстрое разбирательство показало 2 вещи:

    1. Накосячено с "кэшированием" предыдущего значения байта ошибки: по ответу 0x41 он сохраняется в lsrv[][2], а по ответу 0x51 -- в err_reg; так что сравнивается НЕПРАВИЛЬНО, с "не всегда предыдущим" значением.

      ...но главное, видимо -- всё же вследствие п.2:

    2. Пакет запроса 0x41 (GET_STATUS) почему-то НЕ шлётся -- потому-то и горит DEFUNCT; если послать запрос sktcanmon'ом -- обновится.

      При том, что в предыдущем драйвере от 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().
    • И увидел, что отправляются 2 запроса, получаются 2 ответа, ...
    • ...после чего всё замирает.

    Ту-то и дошло:

    • Косяк как раз в самой идее "фильтрации" -- ведь каналы-то эти обновляются не сами, а по запросу от сервера, но если ему не вернуть значение ("отфильтровав" его), то следующего запроса и не последует!

      Так что идея с самого начала была хромой.

      • Т.к. нужен какой-нибудь "стимул" (терминология от National Instruments).
      • Фильтровать можно только каналы, которые САМ ДРАЙВЕР же и опрашивает.

        Но тогда надо как-то организовывать этот самый опрос, с какой-то частотой, а с какой?

      • А опрашиваемые СЕРВЕРОМ фильтровать нельзя.
    • Так что никакая не "мистика", а мистика -- скорее в том, почему при отправке запроса 0x41 (GET_STATUS) вручную оно-таки обновляется: ведь вроде бы не должно (или я перед тем /u:0xFF отправлял, вот и прописалось -1?).
    • ...отдельный вопрос: судя по коду cxsd_hw.c, флаг rd_req НИГДЕ НЕ сбрасывается, кроме как в ReturnDataSet(); как тогда может работать "рестарт" устройства (а вот в v2 -- сбрасывалось, в RemitChanRequestsOf(), которого в v4 нету)?

      Чуть позже: разобрался всё дело в ReRequestDevData(), он отдельно пере-спрашивает всё запрошенное, а вызывается он из ReviveDev().

    @вечер: так что:

    1. "На сейчас" фильтрация убрана, просто путём закомментировывания сравнений полученных данных с предыдущими.
    2. "На будущее" сделано по поза-позавчерашнему проекту: в 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() -- увидел чудное: она НЕ вызывается!
    • Внимательнее посмотрев на логи и понапосылав 0xFF'ов sktcanmon'ом, разобрался -- косяк в устройстве: если GIN25 получает пакет 0xFF/unicast ДО того, как успеет отправить ответ на 0xFF/broadcast, то присылает единственный ответ с readon "WHOAREHERE"; т.е., второй пакет он просто игнорирует (ну или "выходной буфер занят -- ничего не делаем!").

      Козак ради этого на разные типы пакетов имел разные "mailbox'ы" -- это он при личном разговоре рассказывал.

    • Обстоятельства:
      • (Проверено ПАРОЙ CAN-мониторов: один отправляет запросы (в т.ч. с паузой), а второй только смотрит (чтобы получать ответы сразу, не задерживаясь паузами; хотя почему спецификации вроде ":100" влияли на времена получения -- загадка, т.к. они вызывают DoRecv(), принимающий пакеты в течение не более указанного времени).)
      • Шлём 0/b:0xFF и потом 4/u:0xFF:
        • если без паузы -- ответ на второй не приходит;
        • с паузой >=5ms приходит;
        • чем меньше адрес (т.е., чем раньше успевает встрять и ответить), тем меньше нужна пауза: 0-му не нужна вовсе, 7-му побольше.
    • В идеале, конечно, исправлять косяк в устройстве. Но это вряд ли возможно.

      А что делать СЕЙЧАС?

    • "Сейчас" -- заполнение lsrv[*][*]=-1 скопировано из gin25_ff() в gin25_init_d().

      Именно СКОПИРОВАНО: заполнение в _init_d() отвечает за первоначальную отдачу, а в _ff() -- для сброса при последующих Reset/PowerOn/BusOff'ах устройства.

    • Проверено -- да, заработало: блокировки стали отдаваться, причём однократно, по изменению, а не постоянно по 10 раз в секунду.
    • Очевидно, надо в cankoz_lyr удалять из очереди запросы CANKOZ_DESC_GETDEVATTRS только при id_reason==CANKOZ_IAMR_GETDEVATTRS -- в таком случае при неответе на первоначальный 0xFF будет отправлен ещё запрос.
      • Ну да, не будет считать "валидными" ответами все прочие (вроде RESET, BUSOFF и т.д.), ну да и ладно.
      • Проблемой может стать ситуация, если какой-нибудь кривой девайс на обычные 0xFF/unicast будет отвечать с id_reason!=2 (как должно быть); по крайней мере, были в нашей практике такие, что по кнопке Reset присылали POWERON -- то-то не страшно, но вот если реально найдётся оригинал, некорректно шлющий ответ на обычный запрос...
      • Тогда придётся вводить CANKOZ_LYR_OPTION_-флаг "удалять из очереди по любому reason'у".
    • @вечер, ~17:00: сделал -- добавил перед удалением из очереди условие.

      Но пока не проверено -- видимо, в следующий понедельник во время профилактики.

      17.03.2025: проверено -- да, работает, по ответам с id_reason!=2 (в т.ч. по =3 "WHOAREHERE") пакеты 0xFF из очереди не удаляются и через некоторое время посылаются повторно, уже приводя к ответу. Неудобство в том, что "некоторое время" -- аж 10 секунд, что не очень приятно, но лучше, чем ничего.

    • И чуток обсуждения:
      1. Так-то теперь -- с учётом постоянного опроса -- и каналы статусов можно считать за AUTOUPDATED_TRUSTED и в _rw_p() их игнорировать.

        16.03.2025: да, добавлено игнорирование. Осталось проверить.

        17.03.2025: проверено -- всё OK. И было чётко видно: если раньше запросы+ответы DESC_GET_STATUS_n_base=0x41 летали каждые 100мс (от внутреннего периодического опроса) плюс отдельно каждые 200мс (от опроса канала GIN25_CHAN_STATUS_n_base по циклу сервера), то после изменения осталось только каждые 100мс.

      2. Обнаружено, что в gin25_ff() отправка пакета DESC_SET_AUTOREPORT=0xF0 за-if(0)'ена; когда и почему?

        13.03.2025: когда -- 21-02-2023 ещё не было, а 25-05-2023 уже есть. Чуть позже: и даже 24-03-2023 ещё не было, а вот 25-05-2023 -- это ЕДИНСТВЕННОЕ изменение. А вот записей за этот промежуток по теме вопроса нет, так что "почему" -- хбз. Возможно, ради диагностики; или из-за того, что были проблемы с командой 0x41.

simkoz_drv:
  • 13.01.2024: создаём раздел -- мыслей о технологии его работы уже предостаточно, а поскольку делаться будет на основе cac208_drv.c, то и жить самому исходнику в hw4cx/srivers/can/src/.
  • 11.01.2024: Нужен драйвер "имитатор CDAC20/CEAC124/... для драйверов источников".

    13.01.2024@дорога домой после похода в Эдем/Быстроном/Ярче-на-демакова, ~15:20: несколько соображений:

    1. @перекрёсток Николаева/Лаврентьева: делать будем на основе cac208, назовём "simkoz" -- длина совпадает с "cac208".
    2. @тропинка от НИПСа к дому: чтобы не было мгновенной отработки операций записи (приводящей к петлям исполнения) -- ОЧЕРЕДЬ длиной в CHANCOUNT плюс флаги[CHANCOUNT] "запись на этот канал уже в очереди в такой-то ячейке".

      @лыжи: а может, ещё и указывать, через СКОЛЬКО "тиков" отрабатывать обновление? Тогда придётся очередь сортировать "по времени" -- ближайшие вперёд.

      22.01.2024: неа, дюже усложнит всё: получится почти "планировщик", плюс очередь придётся двигать (что сразу потребует заменить "запись на этот канал уже в очереди в такой-то ячейке" на сложную инфраструктуру вроде двунаправленного списка). Так что -- неа, не стоит.

      И вот поверх этого уже механизм, чтоб можно было в auxinfo указать, что значение такого-то канала при записи сделать текущим значением такого-то канала чтения -- так и контрольные измерения АЦП можно имитировать, и битики входного регистра, показывающие отработку "источником" управляющих битиков в выходном регистре.

    3. @подходя к дому: и нужно сымитировать периодический опрос каналов АЦП и входного регистра.

    @лыжи, 10кругов~=3.5км по стадиону НГУ: ещё соображения:

    • "инициализация" -- PSP_PLUGIN on_write=src_chan:dst_chan, а чтоб не иметь проблемы с 0 по умолчанию -- писать ~НОМЕР_КАНАЛА и в _init_d() проходиться по всем и считать определёнными только !=0, которым делать NOT, а остальным ставить =-1.

      21.01.2024@послеобед, смотрение какого-то видосика: неа, "NOT" -- предполагает кодирование знаковых целых в дополнительном коде, что излишнее требование. Лучше просто записывать НОМЕР+1, а после парсинга в _init_d() делать всем номерам -- (декремент): эффект тот же, но проще и элегантнее.

    • Вопрос ещё в другом: а как указывать эти src_chan и dst_chan? Номерами -- фигововато; лучше бы именами -- вроде 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, то можно было бы изготавливать почти произвольные симуляции.

    Только:

    1. Надо будет в егойном _init_d() аллокировать privrec по количеству каналов конкретного устройства, добытому от GetDevPlace().
    2. В идеале бы уметь работать не только с 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().
    • А также всё, касательно "heartbeat/alive" -- поле rd_rcvd плюс сама simkoz_alv().
    • Ну и за компанию поле supports_unicast_t_ctl.
    • 02.02.2024: и функции конверсии в/из кодировки устройства тоже выкинуты, т.к. принято решение всё делать просто в микровольтах, не пытаясь имитировать округление устройством. А val_to_daccode_to_val() сделана просто возвращающей исходное значение.

      И заодно все DESC_* были выкинуты.

    Компилируемость пока НЕ достигнута, т.к. в SendWrRq(), SendRdRq() и simkoz_rw_p() остались попытки отправки пакетов.

    02.02.2024@где-то на улице, по пути между ИЯФом и Эдемом и Коптюга-19 и П28: а ведь надо каналы АЦП возвращать не только сразу после модификации каналов ЦАП, а ПОСТОЯННО, имитируя опрос; и по-хорошему -- возвращать их потихоньку друг за дружкой, имитируя циклический опрос одним измерителем.

    Следствия и идеи по реализации:

    • Следствие: для таких каналов НЕ надо инициировать возврат по изменению, а надо просто прописывать им текущее значение, которое и будет вёрнуто следующей периодической отдачей.
    • Оную отдачу напрашивается запихнуть в simkoz_hbt().
    • И напрашивается отдавать там именно последовательно, по 1 каналу за такт.
    • Более того: напрашивается уважать значения ch_beg/ch_end, "крутя цикл" между ними.

      Для чего ввести индекс "очередной вертаемый канал" -- например, ch_cur.

    • И тогда также напрашивается вернуть SendMULTICHAN(), чтобы он прописывал границы (03.02.2024: нечего там прописывать -- 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 -- та самая ячейка "свойств" канала.
    • В privrec БУДЕТ добавлено "sim_one_t sim_props[NUMCHANS]".

    Что не нравится -- оказываются перемешаны в одной структуре данных свойства канала как ИСТОЧНИКА отражения (dst_chan) и как ПРИЁМНИКА (flags), и это смешано с общими для обоих случаев (val).

    25.04.2024: да, flags надо уносить в отдельный статический массив ("sim_flags[]"?), а вот остальное что перемешано -- это нормально: ведь та информация НЕ ПРЕДОПРЕДЕЛЁННАЯ, а существует только в процессе работы; хотя и имеет две разных сути -- взаимоотношения между каналами (src_chan и dst_chan) и текущее значение (val) -- формально можно и разделить, но зачем?

    05.02.2024@утро, зарядка: ещё со вчера ломаю голову -- базу-то подготовил, но как-то всё не складывается общая картина на тему "как организовать инфраструктуру симуляции с эффективным исполнением отложенных обновлений-отражений".

    Первоначальная задумка от начала-января-2024 выглядела так, что есть 2 структуры данных:

    1. Массив [NUMCHANS], где на каждый канал ячейка "свойства", среди которых указан (при >=0) "целевой" канал, куда надо отразить значение при записи в этот, возможно, текущее значение, и ещё что понадобится -- см. п.2.
    2. "Очередь" (просто массив [NUMCHANS] же (чисто чтоб все каналы могли влезть, но НЕ индексируемый по chn) структур), в которую кладётся информация об исполнении "отражений" -- кладётся в момент возврата данных в канал-источник, чтобы потом в некий момент -- в _hbt() -- все эти отражения скопом вернуть.

      Смысл оной "очереди" -- чтобы не бежать по всем NUCHANS каналам каждый такт, проверяя, "а не надо ли его вернуть?", а просто пройтись по готовому списку изменившихся (чаще всего -- 0 штук).

      • Но тут возникает необходимость корректно отрабатывать ПОВТОРНЫЕ изменения, происходящие ещё до возврата первоначальных (формально такое возможно, хотя архитектура вроде бы должна это предотвращать).
      • Для чего ещё тогда возникла идея при помещении в очередь сохранять в "свойствах" индекс ячейки массива очереди, куда помещён запрос-уведомление о необходимости возврата, чтобы...
      • ...если этот индекс>=0, то
        1. заменить значение в ячейке очереди,
        2. НЕ увеличивать количество элементов в очереди;

        ...иначе же "поместить" в конец очереди запрос на возврат этого канала и значение для возврата, после чего инкрементировать количество элементов в очереди.

    Так вот, собственно, цепочка мыслей, пронесшихся в голове во время зарядки:

    • Перво-наперво -- "заменять" ничего не нужно, т.к. в очереди достаточно хранить просто номер канала, а значение для возврата брать из "свойства[chn].cur_val".
    • Вторая мысль тут же -- а зачем вообще делать эту отдельную "очередь" в виде какого-то массива, когда можно прямо ячейки "свойств" и объединять в очередь?
    • И тут тогда возникают вопросы "как организовать очередь?". То ли однонаправленным списком -- как проще; то ли двунаправленным -- так формально корректнее (да и проще для организации "двух списков", см. ниже).
    • Главная проблема -- чтоб перебор элементов списка был конечным: чтобы в него входили только те элементы, что были в нём на момент НАЧАЛА перебора; а те, что (возможно) добавятся туда в процессе, уже остались бы на следующую итерацию.

      ЗАМЕЧАНИЕ: да, если в процессе прохода какой-то ещё не вёрнутый элемент будет "помещён" повторно -- т.е., фактически просто изменено его значение для возврата -- то и фиг с ним, изменённое значение и вернём. Это неприятно, конечно -- что временнАя диаграмма возвратов искажается -- но не нарушает конечности перебора.

    • Идеи в голову приходят диковатые: вроде "помещать добавляемые элементы в начало списка, а при возврате идти по списку обычным образом" -- в таком случае автоматом обеспечивается конечность перебора, т.к. в конец ничего добавлено не будет. Но вот диаграмма возврата данных становится "задом наперёд".
    • @при записи всей этой простыни, после поедания 1-го блюда завтрака: а если в момент начала перебора запоминать индекс ПОСЛЕДНЕГО элемента списка и при его достижении просто прекращать проход? (да, список для этого должен быть двунаправленным) Тогда получится ровно желаемое -- все добавляемые ПОСЛЕ начала прохода автоматом останутся на СЛЕДУЮЩИЙ проход.

    05.02.2024@лыжи, вечер ~17:00...18:00: насчёт перебора:

    • Очевидно, что нужно реализовывать ВТОРОЙ вариант -- с запоминанием индекса последнего элемента списка и прерыванием перебора по его достижанию.
    • И да, список будет "полуторасвязным" -- в каждом элементе только ссылка на следующий, но в privrec'е хранить индекс последнего и при добавлении ставить "после последнего".

    И ещё: а ведь "пинг-понг" может возникать просто прямо в процессе ИНИЦИИРОВАНИЯ возврата -- если в auxinfo указано A:B и B:A, и оба являются каналами записи, то оно так и попрёт в бесконечной рекурсии писать. Есть идея, как этого избежать:

    1. Возвращать не из dst_chan'ова поля val, а из src_chan'ова.
    2. Но для этого нужна обратная ссылка -- поле src_chan.
    3. А можно обойтись ОДНИМ полем ссылки -- просто "link", который у каналов-источников будет указывать на ведомый канал, а у каналов-приёмников на ведущий.

      @дома, записывая это, ~18:20: (Кстати, тогда циклы становятся невозможны -- для каналов в таком цикле потребуется сразу ДВА поля...)

    4. Чтобы различать, кто есть кто, можно просто добавить ещё один битик во флаги -- "IS_SRC": если он взведён, то считаем поле за 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: делаем парсинг.

    • Строчка "on_change" в PSP-таблице, "полем назначения" указывающая весь массив 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, ...
        25.04.2024: тьфу ты, какой "PSP_SIZEOF()" -- надо же "PSP_OFFSET_OF()"!
        int devid = ((privrec_t*)((uint8*)rec - PSP_OFFSET_OF(privrec_t, sim_props)))->devid;
      • ...затем хотел даже просто сначала вычислить me и к нему адресоваться повсеместно, в т.ч. при складировании результата.
      • Но потом дошло: ведь этот парсинг выполняется СЕРВЕРОМ, т.е., ещё ДО вызова _init_d() и, соответственно, ДО прописывания me->devid!

      Так что "простые" пути ну никак не сработают!

    И что делать?

    1. Примитивный вариант -- убрать из определения драйвера указание simkoz_params и вызывать парсинг вручную, уже ПОСЛЕ прописывания me->devid.
    2. Другой способ -- всё-таки твикнуть API сервера: поскольку у нас есть 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 писуемого канала, а связанный канал АЦП будет вёрнут сам автоматически на очередной (его) итерации циклического перебора.

    • А вот с регистрами сложнее: нужно будет имитировать работу этих 8+1; то ли прямо "на уровне устройства" (всегда превращать любую запись в запись 8-битного), то ли ещё как...

    25.04.2024: ну -- делаем, пока по минимуму...

    • Заведена PerformReflection():
      1. прописывающая переданное значение в .val,
      2. затем проверяет, что если НЕТ dst-канала, то отваливает.
      3. Также если dst-канал имеет тип 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 просто закомментирован, тоже для компилируемости "здесь и сейчас".

    Пытаемся проверять; сначала, естественно, не пошло, даже устройство не инициализировалось. Разбираемся:

    • Первый косяк был в указании "2,2" в min_businfo_n,max_businfo_n в определении драйвера, так что указание "-" в devlist'е не работало.

      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, где "отображение" может быть весьма нетривиальным (например, при реверсе знак меняется; а где-то и отдача с множителем), напрашивается идея:

    • А может, РАЗДЕЛИТЬ обязанности? Чтобы "симуляцией" CAN-устройств занимался один драйвер, а симуляцией "сторонних" устройств (ИСТ и т.д.) -- другой, связываемый с ним через insrv::-линки, как для получения оттуда обновлений, так и для отправки изменений?

      Конкретно:

    • simkoz_drv --
      1. имитация плавного изменения на основе advdac_slowmo_kinetic_meat.h (ровно как сейчас сделано);
      2. каналы АЦП -- отдаются периодически (как сейчас), но являются каналами записи, так что в них может писать сторонний драйвер;
      3. выходной регистр -- просто работа "8+1", с мгновенным отображением;
      4. входной регистр -- аналогично выходному, и (аналогично "АЦП") -- является каналами записи. ...но, возможно, нужна периодическая отдача, для более полной эмуляции реальной железки.
    • sim_drv -- драйвер-симулятор-"кукловод", которому указываются соответствия "при изменении канала A выполни такие-то действия", и действия могут быть
      1. как простое "скопируй значение в канал B",
      2. так и "выполни такую-то формулу".

      И вот формулы позволят как отображение "один на многие" делать (что сейчас невозможно), так и коэффициенты при записи, да и вообще "контекст" иметь -- для того же реверса (как через каналы "управляемого" устройства, так и через локальные регистры).

    Такое разделение даст произвольную гибкость. А в перспективе позволит и другие типы данных поддерживать.

    @вечер: с другой стороны, у "разделённого" варианта есть и недостаток: при нём любые изменения каналов будут вызывать 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() в "симуляцию"-периодический возврат АЦП, сразу за собственно возвратом.
    • Далее -- регистры.
      • В privrec добавлено
        uint8 sim_ioregs[2]; // 0:Inp,1:Out
      • Идея в том, чтобы с обоими регистрами работать одинаково -- для этого мы их параметризуем, считай регистр чтения за [0], а записи за [1].
      • ...и да, делается по проекту для "отделённой симуляции CAN-устройств", т.е., с ВХОДНЫМ регистром обращаемся ровно как с выходным -- как будто это тоже каналы записи.
      • Ну и сделана обработка запросов на каналы регистров -- всё прямо в _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 частей:

    1. В PerformReflection() -- добавление в список (с проверкой, не в списке ли уже).

      Также добавлено ранее забытое "sim_props[dst_chan].val = val" -- складывание значения и в сам target-канал (это для потенциальных "цепочек" отражений, вроде A->B->C, чтобы канал C брал значение пусть не самое-последнее-от-A, но хотя бы от B).

    2. В _hbt() в цикле прохода по списку для возврата -- отдача значения ReturnInt32Datum()'ом плюс отражение PerformReflection()'ом; причём отдаётся значение из src_chan.

      Пара замечаний.

      • Замечание 1: пока что НЕ делается никакого анализа значения "типа" канала SIM_CHTYPE_*, а просто безусловно стоит PerformReflection() сразу за ReturnInt32Datum().

        30.04.2024: переделано, "анализ" добавлен.

      • Замечание 2: устройство самого цикла было изрядно переделано: в первоначальном варианте оно было просто неработоспособно (некорректное условие окончания цикла, отсутствие продвижения к следующему каналу).

    Ну вроде всё. Теперь нужно проверять.

    Ну-у-у, попробовал -- фиг, что-то подглючивает: при включении битика outrb1 битик inprb0 взмаргивает на мгновение, но потом гаснет.

    • Сначала я думал, что это ist_cdac20 так влияет, но потом оказалось, что и при включении outrb1 вручную эффект тот же.
    • Подумал-подумал и сообразил: "отражение"-то делается простой записью в sim_props[INPRB0].val, но последующие ЧТЕНИЯ отрабатываются чтением из 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?!

    • Попробовал погонять цикл "включить, подождать 60 секунд, выключить" -- работает.
    • С вариацией "сначала занулить уставку, потом включить, а через несколько секунд (ещё в процессе включения) увеличить уставку до максимума" (потому, что, какпомнится, первый тест вроде был как раз по такому сценарию, причём тогда 10V почему-то выставились одним скачком, а не плавно) -- тоже работает; гонял несколько часов, и всё ОК.
    • Анализ кода ist_cdac20_drv.c косяков не выявил.

      Во-первых, запись в канал 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: со вчера не переставая обдумывал, какие бы ещё тесты погонять и что ещё можно сделать.

    @утро, просыпание и зарядка: соображения:

    • Надо ещё проверить работу "цепочек" -- когда запись в A отражается в B, тот в C, тот в D, ...
    • А для этого надо каналам ЦАПа и выходного регистра тоже проставить SIM_CHTYPE_W.
    • А ещё надо бы ввести вариант "IGNORE" -- чтобы, например, в сами каналы CUR ничего бы не прописывалось. И пусть именно это IGNORE и будет =0 -- чтоб по умолчанию каналы бы не являлись приёмниками отражений, а только если явно сконфигурированы как таковые.
    • Кстати, разделитель ':' в "SRC:DST" надо бы поменять на что-то, НЕ могущее быть в имени канала -- для совместимости синтаксиса с будущим sim_drv.c, которому могут указываться каналы со ссылками на сервера, а там возможны двоеточия.

      После мысленного перебора всех не-alphanumeric символов ASCII остановился на вопросительном знаке: '?' вроде и не используется нигде (в отличие от даже '=' и '>', могущих встречаться в TANGO), плюс мнемоника "SRC?DST" -- это как в OCCAM чтение из канала.

    • Как организовать сборку так, чтобы собиралось только в can/local/, но НЕ во всяких c4l-cangw/ -- да просто: в сам can/local/Makefile и добавить указание о сборке.

    Делаем.

    • В can/local/Makefile сразу после 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 частей:

    1. В _rw_p() один большой if(chn==...||chn=...) на эти 6 каналов, всегда возвращается val=0,rflags=0.
    2. В SUPPORTED_chans[] скопирован соответствующий кусок из cdac20_drv.c -- без этого отдельный кусок выше по селектору каналов возвращал им UNSUPPORTED.

    Далее решил рассмотреть вопрос "а как бы симулировать железо для ВЧ300/ВЧ400?" -- и тут вылезло интересное.

    • Во-первых, там (для управления 8 штуками) используется не один девайс (вроде CAC208), а связка из ДВУХ -- CANDAC16+CANADC40, и каналы для каждого источника размазаны между этой парой: уставка в DAC, измеренное в ADC, а входных и выходных регистров по 1 биту в том и другом.

      Так что идея от 26-04-2024 о создании отдельного "дирижёра" sim_dir_drv была гениальной -- тут только он и годится, единый же драйвер-симулятор simkoz не справится в принципе, т.к. каналы "источник" и "отражение" расположены в РАЗНЫХ устройствах.

    • Во-вторых, в simkoz_drv.c значения _CHAN_ADC_n_count=24 и _CHAN_OUT_n_count=8 взяты от CAC208, но для симуляции CANADC40 и CANDAC16 их надо поднять как минимум до 40 и 16 соответственно.

    Решаем задачу "во-вторых" -- чтобы симулятор поддерживал ВСЕ имеющиеся типы ЦАПов и АЦП.

    • Меняем числа с 24 и 8 на 100 и 16 соответственно.
    • С АЦП всё просто: это значение KOZDEV_CHAN_ADC_n_maxcnt и оно ни с чем никак не интерферирует.
    • А вот 16 -- это лишь ПОЛОВИНА от 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,
      
    • 25.05.2024: и ONE_LINE_PARAMS()'ы для 8-15 добавлены.

    24.05.2024: э-э-э...

    • 24.05.2024@утро-зарядка: а вот с каналами IMM есть проблема: коль в разных типах устройств они в разных позициях (CDAC20:201, CEAC124:204, CAC208:208, CANDAC16:216), то будет пересечение и эмуляция станет работать некорректно...

      "Спасает" только то, что в 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, проверяем -- да, исправилось.

    • Но и с 100 тоже проблемка: когда так стоит по умолчанию, то цикл получается ну очень уж долгим (100шт/10Hz=10секунд). Поэтому:
      1. Умолчание переделано на 40 (чтоб CANADC40 эмулировались "корректно").
      2. В определение 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-модулей/директорий -- тоже делаем.

udpcan:
  • 10.04.2018: создаём раздельчик, чтоб был, под пока не до конца ясным названием (сейчас временно ставим "?udpcan?").

    16.04.2018: да чё уж -- "udpcan" и есть, безо всяких вопросов.

  • 10.04.2018: по результатам разговора с Пановым на тему "а как бы запинать CX-сервер на его контроллере модулятора" родилась идейка: запилить еще один canhal, работающий через UDP.

    10.04.2018: вроде протокола обсуждения -- тезисно.

    • Причина: есть желание гонять CX-сервер прямо в контроллере модулятора, чтоб он был поближе к железу. И не просто лишь CX-интерфейс, коим является cxsd_fe_cx (как обсуждалось 23-09-2015), а именно полноценный сервер, чтоб там и vdev-драйверы могли функционировать.
    • Обстоятельства: процессора там какой-то 32-битный ARM, под Linux.

      Но засада в том, что основной софт самого Панова работает под LabView.

      Есть желание попробовать интегрировать libcxsd прямо туда, и просматриваются 2 варианта:

      1. "Правильный": сделать "labviewcxscheduler" (и тогда понадобится лишь небольшая обвязочка, стартующая сервер с неким конфигом и активирующая cxsd_fe_cx).

        23-09-2015 такой сценарий уже обсуждался.

      2. "Халтурный": запускать всё то же самое (библиотеку с обвязкой) в отдельном thread'е.

        В таком случае работать оно всё будет прямо на стандартном cxscheduler, но взаимодействовать с LabVIEW-based основой придётся через адаптеры с mutex'ами.

      И да -- в любом случае, понадобится сделать CANHAL для того CAN-API, что используется в том контроллере.

    • Обходное решение: сделать специальный CANHAL, который бы работал через UDP: в качестве "устройства" открывается UDP-сокет, bind()'ящийся к localhost:SOMEPORT, CAN-пакеты в обе стороны тривиально обёртываются в датаграмму, "ожидание готовности" сводится к готовности данных в этом UDP-сокете.

      Панову соответствующий серверочек сделать тоже тривиально.

    • Детали:
      • "Протокол" или "формат" представляется тривиальным: сначала 2 байта идентификатора, далее 0-7 байтов данных.

        Корректности ради стоит под идентификатор выделить даже 4 байта (чтоб помещались не только 11-битовые, но и 28-битовые, плюс всякие RTR'ы).

        А можно даже и еще 4 байта зарезервировать.

        И даже поле "dlc" предусмотреть.

      • Сетевитость: задействовать, например, порт 9000.

        А чтоб была возможность иметь "несколько линий" (мало ли -- для отладки), использовать порт 9000+НОМЕР_ЛИНИИ.

      • Предполагается именно localhost:9000+N; т.е., AF_INET, а не AF_UNIX.
      • Насчёт множественности клиентов: она отсутствует, а пановский "сервер" тупо шлёт ответы на тот же IP:PORT, с которого получал последний "запрос".
      • Как назвать протокол -- некий вопрос:
        • Напрашивается udpcan (директория udpcan/, файл udpcanhal.h и готовый layer udpcankoz_lyr.so).
        • С другой стороны, идея-то очевидная, и реализации "CAN via UDP" уже существуют.

          Например, вот почти что программный документ "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: приступаем потихоньку.

    • Создана директория hw4cx/drivers/can/udpcan/.
    • В неё простенький файл udpcan_proto.h с определением структуры данных.
    • Хотя корректнее будет (особенно, с учётом потенциальной возможности гонять клиента на big-endian) собирать всё из байтов.

    16.04.2018@утро, дорога на работу, около ИПА и ИЦиГ: разумным выглядит расширить протокол 2 полями:

    1. "Код команды": указывает действие.

      Позволит избежать необходимости вначале слать команду отправки ненужного пакета лишь для того, чтобы "сервер" узнал, кому гнать все приходящие пакеты -- путём введения команды "connect".

      Аналогично можно сделать и команду "close", отправляемую клиентом при завершении работы.

    2. "Код ошибки": чтоб клиент имел возможность узнавать об ошибке отправки точно так же, как при локальной работе с CAN-адаптером (например, если на линии никого нет, или что выходной буфер переполнился).

    Днём обсудил с Пановым; проще не делать, а то он опять стал нудить насчёт "ну во-о-от, началось расширение протокола...".

    16.04.2018: сделано.

    • udpcanhal.h: скелет взят от sktcanhal.h, работа с UDP подсмотрена в ottcam_drv.c.
    • Адрес используется ТОЛЬКО "127.0.0.1"; брать какой-то иной из layerinfo (как думалось изначально) никак не удастся -- у *canmon оных нет.
    • "Структура пакета" делается из байтов, но не в массиве, а дополнительно определённым типом udp_can_frame_b_t, в котором все поля являются uint8[] и имеют суффикс _b.
    • Ну и добавлена собираемость в drivers/can/Makefile.
    • Панов проверил -- вроде работает.
sktcanserver:
  • 09.06.2018: создаём директорию для сборки canserver'а на основе sktcanhal.
  • 09.06.2018: сделано всё буквально за полчаса -- легко и просто, и сразу всё получилось.

    09.06.2018: никакого реального кода тут не добавилось, вся деятельность свелась к манипуляции уже готовыми ингредиентами компонентами в Makefile.

    • Директория названа sktcanserver/; бинарник -- v4sktcanserver, аналогично v4c4lcanserver'у, что сильно упростило работу по адаптации.
    • Makefile был взят от c4l-cangw/.
      • Для чего в тот предварительно внесены небольшие изменения: "v4l" повсеместно заменено на $(CANHAL_PFX).
      • Это позволило локально почти ничего не переделывать -- ограничилось убиранием кросс-компиляции и ненужного тут *canmon'а.
      • Минусом является дублирование: монструозный скрипт генерации canserver_drivers.c и прочее.
    • Файл sktcanhal.h не симлинкуется из ../socketcan, а просто туда делается -I.
    • Извращение, конечно, что теперь драйверы "нативно" собираются дважды: в local/ и в sktcanserver/. Но в противном случае -- использования local/'овских -- появилась бы зависимость от собранности в той директории, что совсем не было бы гуд.

    Итак:

    • Проверено -- работает, на canhw-future (будущая виртуалка для CAN-серверов), на которую перенацелены с cangw-rst1 и cangw-magsys remdrv-драйверы в devlist-cangw:11.
    • Кстати, описатель systemd-службы покладен в work/4cx/src/doc/v4sktcanserver.service (он несколько {canhw-future}-specific...).
  • 31.01.2020@научная-сессия-ИЯФ-Логашенко, ~10:30: кста-а-ати: учитывая, что теперь есть динамическая загрузка драйверов, теперь собирать ДРАЙВЕРЫ в sktcanserver формально уже не особо-то и нужно -- он спокойно может использовать из 4pult/lib/server/drivers/.

    Так что в принципе можно даже и убрать отдельную директорию sktcanserver/, переселив программу прямо в socketcan/.

    Но пока этого делать не будем -- и чтоб не портить разделение, да и полностью самодостаточный v4sktcanserver тоже отдельную ценность имеет. Вот когда (если) опробуем на v4a3818, а ещё б "компоновка из кубиков"...

:
VME:
  • 06.07.2017: в интересах начала работ по драйверам для семейства ADC4X250 создаём директорию drivers/vme/ и в ней поддиректорию src/. Никакой компилируемости пока нет и близко.

    07.07.2017: делаем компилируемость.

    1. Идея потренироваться/отладить на драйверах VADC16 и VDAC20, которые просты.
    2. И да -- пока всё будет сделано чисто под BIVME2, с В/В через функции bivme2_*(), а уж потом, при появлении другого контроллера, как-нибудь API заабстрагируем.

    Итак:

    • Создана drivers/vme/bivme2/.
    • Система сборки в ней взята из v2hw/'шной один-в-один, с единственной заменой в DirRules.mk имени V2HDIR на HW4DIR.
    • Файл bivme2_DEFINE_DRIVER.h адаптирован тривиально, взятием куска из cm5307_DEFINE_DRIVER.h.
    • С vme/src/ShadowRules.mk чуть посложнее: он полностью аналогичен CAMAC'овскому, но тот при переходе v2->v4 изменился.

      Как показал анализ diff'ом, смысл изменений -- что теперь .h-файлы из src/ не симлинкуются в сборочные директории, а используются напрямую, при помощи SHD_INCLUDES=-I$(SRCDIR).

      Аналогичная модификация и сделана.

    • Подопытные vadc16_drv.c и vdac20_drv.c адаптированы (в последнем за компанию поправлено значение MIN_ALWD_VAL).

      Несколько замечаний:

      1. Функционал реализован далеко не полностью. В частности, имеющиеся в девайсах регистры SWversion/HWversion наверх никак не светятся.
      2. Алгоритмика работы с каналами АЦП крайне мутная. Причём, VDAC20 обходится без IRQ.
      3. А главное -- то, что сейчас в v2hw/, работать и не могло: конкретно в vadc20_drv.c::rdwr_p() значение val в командах записи никогда из values[] не бралось -- т.е., в нём был мусор.

        Возможно, КОГДА-ТО, в 2011-м, при создании этих драйверов, код был и OK, а потом, при смене API сервер<->драйверы, что-то было отредактировано некорректно. ...но по исходникам в архивах (за сентябрь 2011-го) -- всё тот же косяк есть.

    • Заодно были изготовлены их .devtype'ы, а _drv_i.h'и чуток адаптированы ("base", "count").

      Замечание: карта каналов у vdac20 "плохая" -- там канал DIGCORR_V в области "w" (да и сама карта НЕ как у прочих kozdev'ов; и даже не как у v2'шных).

    14.08.2017: получен от Козака VADC16 для экспериментов -- убедиться, что IRQ работают/ловятся.

    • Да, они ловятся.
    • А вот с работой настроечных каналов было не слава богу:
      1. Изменение во время работы приводило к "зависанию" АЦП -- измерения прекращались.
        • Оказалось -- было очевидно по тексту, простым сравнением глазами -- что дело в функции ReRun(), в которой запись KCMD_START выполнялась иначе, чем в самом init_d().

          Конкретно там отсутствовал битик KCMD_START_FLAG_EACH_IRQ.

        • А в "правильном" варианте стоит комментарий "!!! Bug in vadc16r.pdf".
        • Видимо, дело в том, что в описании действительно косячок: в многоканальном режиме этот бит не "разрешает IRQ по каждому измерению", а разрешает IRQ вообще, и оно происходит в КОНЦЕ измерения.

          А будучи =0 этот бит блокирует IRQ полностью.

          Это видно по диагностике методом отладочной печати: при включенном битике "EACH_IRQ" irq_p() пишет, что ему приходят каналы с первого по последний, единой пачкой, а вовсе не поштучно.

      2. Нет возможности указывать их в auxinfo, а только лишь читаются текущие из устройства.

        Переделано: добавлена vadc16_params[], с умолчательными значениями -1, и теперь считывание из устройства делается только при <0 (т.е., при неуказанности).

  • 06.07.2017: начало работ над драйверами ADC4X250 и ADC1000.

    Пока нет даже понимания, как именно будут организованы файлы:

    • Бинарники драйверов точно должны быть разными, чтоб были разные типы (adc4x250 и adc1000), т.к. у них разные MAX_NUMPTS (у 1-канального вчетверо больше, чем у 4-канального).
    • А вот что делать с исходниками -- неясно:
      1. С одной стороны, в коде очень уж много общего (устройство-то реально одно и то же).
      2. С другой же, делать просто общий файл -- как-то нехорошо: как из него получать разные бинарники?

      То ли вытащить общую часть в .h-файл и #include'ить её, а сами adc*_drv.c будут содержать минимальную обвязку; то ли вытащить в общую часть ВСЁ, а в adc*_drv.c будет делаться только #define, указывающий "мясу" вид поведения...

    Короче, сейчас делаются 3 файла:

    1. adc4x250_defs.h -- описания регистров. Общеполезно.
    2. adc4x250_drv_i.h -- карта будет общей для обоих вариантов.

      (...Хотя и неясно, как у 1-канального поступать с дуализмом _CHAN_DATA/_CHAN_LINE0)

    3. adc4x250_drv.c -- здесь пока заготовки внутренностей.

    P.S. Насчёт потенциального vme_fastadc_common.h тоже пока неясно.

    P.P.S. Кристалльно ясно только с форматом данных: поскольку он там 16-битный "с фиксированной точкой" -- для получения вольтов надо сдвинуть вправо на 12 бит (поделить на 4096) -- то напрашивается прямо в этих битах и передавать, указывая R=4096.

    06.07.2017: пилим потихоньку.

    • Первоначальная работа -- именно над adc4x250_defs.h, заполняемым по мере чтения описания регистров.
    • Параллельно "начальный" вариант adc4x250_drv_i.h.

      Начальность заключается в отсутствии пока каналов _CUR_, а также статистических _STATS_, и даже _NUMCHANS пока неопределёнен.

    • adc4x250_drv.c:
      • изначально заполнялся заготовками -- всякие там ValidateParam(),
      • ...потом захотелось добавить InitParams() -- чтобы вычитывались начальные значения из устройства,
      • ...а потом уж и всё заполнение ("каркас"/"скелет") было сделано.
      • В том числе содержимое будущего vme_fastadc_common.h.

      Делается на основе 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[] -- он запустился.
    • "Статические" каналы (всякие версии) возвращает. Ну еще бы :)
    • По ходу дела обнаружен какой-то косячок, видимо, в remdrv или, скорее, в rrund: при первом коннекте от сервера вначале вылезает диагностика
      ...rrund: request to run ""

      Где он берёт эту пустую строку -- хбз.

      Косяк, видимо, на стороне rrund, т.к.

      • он есть только после старта rrund, а даже рестарт сервера его не воспроизводит;
      • зато после рестарта самого rrund (при висящем в ожидании сервере) оно повторяется.

      16.01.2018: да, косяк в rrund.c::Run().

      • Явно наследие старого rrund, времён ДО возможности указывать префикс и суффикс.
      • Читается в буфер name[], из которого затем слепляется path[]=prefix,path,suffix, а печатается -- сразу после получения! -- именно path[], в тот момент еще пустая.
      • Исправлено тривиально: печатается теперь прямо name[] -- ведь это его попрошено.
      • Самое странное, что компилятор НЕ определяет проблемы -- он не даёт warning'а "uninitialized...". Ни один из трёх: 4.8.5/x86_64 (RHEL-7.3), 3.2.2/ppc (YellowDog ppc860), 3.3.2/arm (MOXA arm-linux_1.3).

        Ни с обычным -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[] добавлена одна лишняя ячейка.

    • !!!надо прямо в bivme2_io.h ввести коды операций -- VME_OP_{NONE,READ,WRITE}, перетащив их из bivme2_test, чтобы в драйверах можно было указывать "в term_d() сделай вот то-то".

    10.08.2017: в продолжение (после до-о-олгой паузы).

    • Тогда, в июле, после вроде бы допиливания драйвера была сделана попытка с ним поработать. Авотфиг -- IRQ не ловит.
      • Точнее, ОДИН раз после загрузки крейта -- ловит (так было проверено, что драйвер данные вычитывает и отдаёт как надо, а GUI отображает).
      • А потом -- больше нет, хотя битик INT_STATUS_ADC_CMPLT горит.
    • Тогда же была даже написана утилитка bivme2_test, чтобы исследовать эту проблему подробнее. Не помогло -- IRQ одноразово.
    • Сегодня была сделана вторая попытка с помощью Юры Роговского: он принёс свой контроллер и некий VME-девайс от Карпова.
      • Контроллер оказался очень кстати: батраковщический после переноса из 2-го зала ко мне в 613-ю почему-то отказался работать (ошибка при обращении по VME), а юрин работает.
      • А вот карповский блок, увы, не помог: ему нужна периферия, а просто так от него IRQ не добиться.
      • Попросил у Козака VADC16 (у того мы прерывание получали), отвечено, что в понедельник, возможно, найдётся.

        14.08.2017: да, нашелся (даже два на всякий случай). Вставлен -- работает, IRQ ловятся. Так что проблема в ADC4X250 (или его документации).

    • А пока с горя запиливаем ADC1000.

    10.08.2017: теперь подробнее о пилении ADC1000.

    • _drv_i.h-файл был получен из 4x250'шного путём замены sed'ом и удаления лишнего.
    • И тут возник вопрос: может, стоит всё-таки у одноканальных fastadc (сейчас это ADC1000 и c061621) сделать alias line0:data (и аналогично для по-линейных каналов свойств) и фиктивный канал MARKER -- чтоб такие осфиллографы годились для натравливания на них "чужих" клиентов? Ну и чтоб потенциальная софтина "быстрый осциллограф вообще" был проще (хотя, из-за нефиксированного начала нумерации -- у adc200 идёт с line1 -- всё равно сложности будут).

      11.08.2017: канал MARKER в c061621 добавлен (везде где надо), а из ADC1000 просто не убираем.

    • И противоположный вопрос: а зачем у нас каналы "lineNon" для девайсов с заведомо-неотключаемыми линиями?

      Видимо, тоже для совместимости -- так что оставим.

    11.08.2017: далее:

    • Прототип adc1000_drv.c изготовлен, копированием из adc4x250_drv.c, контекстной заменой и многочисленными подпиливаниями.

      Но надлежащее вычитывание данных (из 4 банков) пока НЕ сделано.

    • Заодно стало ясно, что надо бы как-то "разделить" имена, сделать их полностью уникальными, а то сейчас "ADC4X250" является одновременно и именем/префиксом девайса/драйвера, и именем общей части (файл adc4x250_defs.h), а-та-та!!!

    31.10.2017: IRQ заработало. Типа.

    • После отправки утром Антону Павленко того списка из 20170706-ADC4X250_DOC_GOOFS.txt он ответил, что, из-за косяка в прошивке, прерывания не выставляются при vector=0 (а почему один раз после перезагрузки всё-таки выставляется -- тоже косяк).
    • Сделал вектор =1, и прерывания полетели, лавиной. Но тоже косячно, т.к.:
      1. Вне зависимости от указанного значения, девайс всегда присылает vector=0xFF.
      2. IRQ, похоже, просто не сбрасывается и сразу после "обработки" возникает заново.

        Драйвер-то его, новое-непрошеное (когда нет запрошенности измерений) игнорирует, но... когда будет запрошено следующее измерение, то оно "сработает" сразу же, не дожидаясь реального измерения.

    • Эмуляция же задисэйблена -- в adc4x250_hbt() вставлен безусловный return.

    10.11.2017: достигнут значительный прогресс.

    • После письменного общения с Антоном Павленко вчера была решена проблема не-снятия IRQ.
      • Причина была в том, что между контроллером и ADC стояла плата-размножитель (стартов и клоков), а в ней отсутствует поддержка некоей линии IACK, отвечающей за снятие прерываний.

        Она должна была быть запаяна дейзи-цепочкой, но отсутствует.

        Вот если платы в этой позиции нет (т.е., разрыв), то всё работает (так устроена VME-шина), а если плата есть, то должно быть запаяно.

      • Т.е., подтверждение отработки IRQ делается на аппаратном уровне, а те команды "ADC_BREAK_ACK" и чтения INT_STATUS -- чисто информационные и для сброса битов в регистрах статуса, на поведение на низком уровне они не влияют никак.
      • После перестановки плат всё заработало "как надо".
      • Также заработало указание произвольных векторов прерываний -- 0xFF было тоже из-за криво сделанной платы-размножителя.
    • Но осталось время-от-времени зависание -- когда ADC перестаёт присылвть IRQ/бит-в-INT_STATUS.

      Сегодня приходили Антон с автором девайса Евгением Котовым.

      • Котов глазами смотрел на код драйвера -- криминала не увидел (хотя и удивился, почему всегда всё прописывается).
      • Вставили в крейт т.н. "выноску" и глазами смотрели на поведение сигналов на магистрали.
      • Экспериментально -- закомментировыванием кусков кода, чтобы обращений к шине было поменьше -- установлено, что проблема исчезает, если убрать всё обращение к девайсу непосредственно перед командой START.

        И, почему в софте Котова такой косяк не вылазил: у него команды на запись даёт не драйвер в контроллере, а софтина, обращающаяся к сидящей в контроллере софтинке по сети -- т.е., имеются макроскопические задержки.

      • Т.е., требуется добавить задержку перед командой START.
      • 13.10.2017: Котов прислал письмо (по результатам анализа его кода), что:
        • Проблема не просто в START, а в том, что пауза нужна между ADC_BREAK_ACK и START.
        • И что если бит ADC_CMPLT не используется, то можно просто ADC_BREAK_ACK не делать (это во втором письме).

        СЕЙЧАС-то ADC_CMPLT не используется, но вообще бы надо. Так что -- не вариант.

      • 14.10.2017: добавлено SleepBySelect(10000) (10ms) непосредственно перед START. Помогло.
  • 08.07.2017@дома: а еще очень сильно не хватает утилиты bivme2_test -- вот ПРЯМО СЕЙЧАС.

    С батраковско-павленковско-котовским крейтом (который во 2-м зале, мерять с датчика потерь для Юли Мальцевой) что-то стряслось: котовская софтина бает о проблеме с VME. А диагностировать это -- как делалось на ЛИУ-2 с uspci_test -- никак!

    Так что надо бы её сделать. Тем более, что и драйверы АЦП проще будет отлаживать.

    09.07.2017@дома: за основу можно взять uspci_test.c.

    Формат вызова --

    bivme2_test [OPTIONS] DEVSPEC [COMMAND...]
    где
    • DEVSPEC -- ADDRESS_MODIFIER:BASE_ADDR
    • COMMAND -- либо команда В/В, либо ожидание:
      • U:OFFSET -- чтение
      • U:OFFSET/COUNT -- чтение COUNT unit'ов
      • U:OFFSET=VALUE{,VALUE...} -- запись
      • :[microseconds-to-wait] -- ожидание

      И в роли U может быть b, s или i (q оставим про запас).

    • Из точно-полезных опций --
      1. Управление выводом -- -q,-t,0T,-x -- сделаем сразу.
      2. Работа с IRQ -- -i: какие параметры? Видимо, -iIRQ_N[.VECTOR][,CHECK[,RESET]] -- аналогично PCI, но параметров, конечно, многовато.
      3. "Правила закрывания" -- -cOP1[,OP2] -- полезно в качестве дополнения для поддержки IRQ.

    10.07.2017: делаем.

    Всё срисовывается с USPCI'ного аналога максимально близко. Даже описание операции В/В находится в типе vme_rw_params_t, а описание "адреса" устройства в vme_devspec_t, и парсятся они туда функциями ParseAddrSpec() (убран лишь парсинг BAR'а) и ParseDevSpec() (сильно проще) соответственно.

    11.07.2017: продолжаем.

    • Поскольку в VME есть такая хрень, как "address size" (могущая принимать значения 16, 24, 32), то надо мочь её указывать.
      • Причём как одноразово, для всех операций, так и индивидуально для каждой операции (вряд ли понадобится, но вдруг найдутся экземпляры, требующие РАЗНЫХ адресных пространств).
      • Кстати, если понадобится указывать разные ADDRESS_SIZE, то может понадобиться указывать и разные ADDRESS_MODIFIER'ы для каждой операции индивидуально. Как, синтаксически?
      • ...почитал документацию по VME -- неа: похоже, ADDRESS_MODIFIER чётко привязаны к ADDRESS_SIZE, так что в modifier'е прямо закодировано, какой это размер адреса. Так что достаточно будет вообще ОДИН раз указывать этот ADDRESS_SIZE, прямо в DEVSPEC.

        10.08.2017: тем не менее, Гена Карпов умудрился на эту "чёткую привязку" ADDRESS_MODIFIER к ADDRESS_SIZE наплевать: у него используется modifier 0x0D (A32 supervisory data access), но адрес при этом он дешифрирует только 16-битный. И это работает! Т.е., при попытке обратиться к 32-битному адресу получается ошибка (несмотря на 0x0D), а при обращении к 16-битному пашет. Так что привязка вовсе не такая уж железная, а скорее вопрос соглашения.

      • Посему формат оного переделан в ADDRESS_SIZE:ADDRESS_MODIFIER:BASE_ADDR (первое поле может принимать значения 16, 24, 32; для VME64 теоретически валидны еще 40 и 64).
    • Сделан аналог USPCI'ного ioctl(,USPCI_DO_IO,) -- функция do_io(); ей параметром передаётся и address_size.
    • Собственно PerformIO() скопирована с изменением лишь в строчке с тем do_io().
    • Проверяем чтение -- работает!!!

      (В крейте 192.168.8.209 есть 3 девайса: пара ADC1000 в адресах 0x02{000000} и 0x04{000000} плюс один ADC4X250 по адресу 0x06{000000} -- вот из них читается что положено.)

    • Делаем "on_close" -- ключ -c.
      • Парсинг-то прост -- копия uspci'ного.
      • Но была тонкость: поскольку делалось для тамошнего ioctl(,USPCI_ON_CLOSE,), то там НЕ проставлялось ни addr, ни count (т.к. uspci.c'ная обработка при копировании в свой буфер всё проставляет сама).

        Вот и тут пришлось сделать своё проставление.

      • Еще косяк вылез: оказывается, парсинг МНОЖЕСТВЕННЫХ on_close-операций никогда работать и не мог: там был забыт флажок PAS_COMMA_TRM, так что ParseAddrSpec() воспринимал запятую как разделитель списка значений и ругался (что массив нельзя).
        • Флаг добавлен -- исправилось.
        • В uspci_test.c тоже добавлено (дата на .c-файле была 07-12-2010, бинарник в дереве был последний раз собран 16-05-2011, а крейтов бинарник (есть на foxy) вообще от 02-03-2010 (и в нём поддержка -c отсутствует как класс)).
      • Ну и собственно exitproc() тривиальна -- цикл с do_io().
    • Итого, НЕ СДЕЛАНА только работа с IRQ -- лень и пока не нужно.
    • Также отсутствует печать времён -- этот косяк унаследован от uspci_test, куда сама идея о временах пришла из canmon_common.c (а туда, видимо, из cx-rdt_wrt.h), но реализована печать так никогда и не была.

    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.
        1. Более хитрый select().
        2. Вместо прямо обработки по готовности дескриптора -- вызов запрошенного в sl_fd_cb.
      • "Обработка" IRQ -- в callback'е irq_p().

    11.08.2017: кстати, о проблеме, упомянутой в заголовке: вчера ведь как раз и вылезла с батраковичевским контроллером "проблема с VME" -- что-то там, видимо, с контактом бо-бо.

    И функции из libvmedirect при этом (у них внутри "status & VMEIF_REG2_BERR") возвращают -1, в errno ничего не прописывая.

    Посему -- убираем печать strerror(errno), вместо него теперь просто строка "VME error".

    14.10.2017: какие-то странности с адресацией: Попытка ручного обращения -- bivme2test'ом -- к девайсу с адресом 0x8E000000 обламывается:

    # /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
    
    в то же время драйвер adc4x250 с девайсом 0x8E как-то работает.

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

      Вот если бы любая из них просто парсила и не выдрёпывалась!!!

    • Конкретно на 64-битной x86_64 проблемы нет, т.к. sizeof(long)=8.

      А вот на 32-битной -- есть... Ровно как на PowerPC: strtol("0x80000000",NULL,0) даёт ERANGE. И -- да, там strtoul() спасает ситуацию, преспокойно парся и большие числа, и отрицательные (вот веселуха-то!!!).

    • Решение НА СЕЙЧАС: ничего нигде не трогаем (а мест-то -- навалом!!!).

      При выявлении же проблем аккуратненько правим эти самые проблемные места.

    • 25.09.2023: экспериментально проверено, что у 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: дальнейшие обсуждения будем вести в конкретных подсекциях, а этот список замораживаем.
Общие вопросы:
  • 15.02.2019: обсуждения "вообще" переносятся сюда из изначального "корня раздела", коий корень отныне замораживается.
  • 15.02.2019: надо продумать структуру как директорий в hw4cx/drivers/vme/, так и подразделов тут.

    15.02.2019: непосредственная причина (побудительный мотив) -- что надо готовить поддержку для CAEN'овского адаптера A3818, а в перспективе и иных (как локальных адаптеров, так и кросс-поддержку для иных платформ (как сейчас сделана для BIVME2).

    Сложностей/неопределённости доставляет вопрос о предполагаемой архитектуре работы VME-драйверов, которую потенциально можно реализовывать двумя способами:

    1. Как CAMAC-драйверы: сами по себе, выполняется только инициализация (которую у CAMAC'овских выполняет даже сам DEFINE_CXSD_DRIVER(), а с bivme2_io она явная).
    2. Как CAN-драйверы: ВСЕГДА работают через layer.

    Что выбрать?

    • Конкретно СЕЙЧАС поддержка BIVME2 сделана скорее в стиле CAMAC.

      Потому, что в BIVME2 драйверы работают отдельными процессами, а не в рамках одного процесса-сервера.

    • Но на перспективу она ИЗНАЧАЛЬНО задумывалась layer'ной (см. bigfile-0001.html за 02-09-2011).

      Потому, что локальных PCIe-адаптеров нужно именно так.

    Вывод: надо делать layer'ами.

    Пара замечаний:

    1. Да, реализацию для BIVME2 придётся переделывать, чтобы перевести её на layer'ы.
    2. А вот PCI-устройства -- hw4cx/drivers/cpci/ -- УЖЕ работают через layer'ы, прямо изначально.

    Итак, предлагаемая структура:

    1. Директории:
      • src/ -- все исходники, сюда же должна переехать утилитка "*test".
      • bivme2/ -- самоочевидно.
      • a3818/ -- реализация API "VME I/O layer" под CAEN A3818.
      • local/ -- место для сборки драйверов под локальную платформу, layer'о-независимых (т.е., требующих для работы @LAYER).
    2. Подразделы:
      • vme_io -- некий общий API для общения с VME.
      • ?vme_io_lyr? -- общее по API layer'а. Сейчас представляется просто обёрткой для vme_io.
      • bivme2 -- опять самоочевидно.
      • a3818 -- всё, связанное с реализацией доступа через CAEN'овский драйвер.
      • vme_test_common -- об исходнике утилиты *vme*_test.
      • vme_fastadc_common -- для будущего vme_fastadc_common.h.
      • Для драйверов -- свои индивидуальные подразделы, и, в частности...
      • ?adc4x250? (или как будет называться общий модуль?) и в нём отдельно adc4x250_defs.h, adc4x250 и adc1000.

    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=1.

      Это явно была просто экономия времени: чтобы не заморачиваться менеджментом в условиях, когда дескриптор всегда нужен ровно 1. А при В/В уже идёт корректное обращение к структуре-описателю по сдвигу handle, как и должно.

      Так что при переходе на layer'ность исправилось бы быстро.

    2. А вот с address modifier'ами всё хуже. Этот "am" указывается не операциям В/В, а уставляется предварительно при помощи
      v = am;
      r = libvme_ctl(VME_AM_W, &v);
      
      непосредственно в bivme2_io_open().

      Т.е., там "указание address modifier'а при регистрации устройства" сделано не только по идеологическим соображениям, но и просто не от хорошей жизни -- принудительно.

      Для возможности же указывать в КАЖДОЙ операции В/В придётся либо в каждой такой операции выполнять тот libvme_ctl(VME_AM_W,), либо как-то помнить последний уставленный и вызывать уставление при несовпадении. ...либо изучить исходники libvmedirect (если удастся их найти) и убедиться, что там эта операция дёшева.

    Ну-с, что делать будем, каким путём пойдём?

    Видимо, надо изучить ещё API и CAEN'овских библиотеки и драйвера -- тогда будут дополнительные основания для принятия решения.

    18.02.2019@лыжи-середина-"перешеек"-1-й-2-ки: а пока:
    • Наиболее разумным выглядит воспроизвести API в том же варианте -- с ПРЕДВАРИТЕЛЬНЫМ указанием address modifier'а.

      Просто потому, что в libvmedirect оно по факту ТОЛЬКО так (ПРОВЕРИТЬ CAEN!!! 21.02.2019: проверил; там ТОЛЬКО ИНАЧЕ (с каждой операцией указывается).).

    • И пусть в API vme_io_lyr он в любом случае указывается именно при регистрации устройства.

    18.02.2019: отдельный вопрос -- как с IRQ обращаться. Ведь vme_io_open() должен бы вернуть что-нибудь, годное для помещения в select()...

    19.02.2019: конкретно для BIVME2 -- используется char-устройство /dev/vmeiN, дескриптор на которое становится готовым на EX, после чего оттуда надо считать int32-вектор.

    20.02.2019@дома: во-о-от, надо считать вектор и сравнить его с требуемым. Откуда следует, что оный вектор надо где-то хранить; как и указатель на клиентов callback, кстати.

    Какие варианты решения?

    1. "В лоб" -- как сейчас, сваливать на уровень vme_io менеджмент описателей.
    2. "Упрощённо" -- оставить в vme_io только "механические" операции взаимодействия с подстилающим драйвером, а проверку векторов повесить на vme_io_lyr и прочих клиентов (vme_test).

    Второй вариант мне определённо нравится больше.

    21.02.2019@дома: насчёт "как у CAEN устроена работа с address modifier'ом и с IRQ":
    • Address modifier указывается в КАЖДОЙ операции В/В.

      Кстати, указывается он в API CAENVMElib, а ДРАЙВЕРУ поступают уже готовые "пакеты" для обмена с линией.

    • А вот с IRQ оно работает просто НИКАК. Нет там поддержки асинхронной работы.

      Есть только синхронная:

      1. CAENVME_IRQCheck() -- проверить наличие IRQ (возвращает битовую маску взведённых линий).
      2. CAENVME_IRQWait() -- ждать появления IRQ на указанных (маской) линиях в течение указанного времени.

      А даже возможности получить номер файлового дескриптора, используемого внутри CAENVMElib'а -- нельзя, нету такой функции.

      И самое странное -- что в драйвере нету метода poll(), но присутствуют вызовы wake_up_interruptible(), вроде бы используемые как раз для сигнализирования клиентам о готовности. Причём используются с ДВУМЯ экземплярами wait_queue_head_t:

      1. read_wait[][] -- для ожидания/пробуждения при блокирующемся чтении.
      2. intr_wait[][] -- для ioctl'а IOCTL_IRQ_WAIT, т.е., ровно для того, что должно бы реализовываться через poll().

        Т.е., там ПОЧТИ всё сделано, просто сделано "не так".

    Обсуждение "что делать":

    • С клятым AM -- хбз...

      Можно как-то выдрёпываться с передачей "AM<0 -- значит, не уставлять". Но не спасёт.

      Видимо придётся как-то на уровне HAL-include-файлов адаптироваться...

    • Ну а с IRQ просто абзац -- видно, что API делался не для крупных систем, где собирается вместе толпа разнородных драйверов (EPICS, TANGO, CX), а для мелких стендов. Примером такой "мелочи" и является их программка CAENVMEDemo.

      Как решать проблему?

      • Вот прямо СЕЙЧАС -- можно выпендриваться с имитацией поддержки select()'а, при помощи порождения процесса/thread'а и pipe()'а на каждое устройство:
        • чтоб оный процесс просто постоянно висел в CAENVME_IRQWait(),
        • а по приходу IRQ отправлял в "пишущий" конец (например, вектор),
        • а "клиент" добавлял бы "читающий" на ожидание в select().

        Отдельный вопрос: а как владельцу "пишущей" части pipe'а определять закрытость читающей стороны? Ведь обычный подход -- "EOF, который показывается готовностью на чтение и результатом чтения равным 0 байт" тут не катит, т.к. пишущий конец должен быть невалиден для чтения. Надо б тестик запилить...

        22.02.2019@институтская-сессия-после-обеда: разобрался.
        • Была шальная мыслишка "а вдруг можно такой дескриптор (вроде бы write-only) помещать в набор readfds?
        • Гуглением по "linux detect closed pipe write endpoint select" нашлось обсуждение "Detect when reader closes named pipe (FIFO)", где ставится ровно нужный вопрос -- "Is there any way for a writer to know that a reader has closed its end of a named pipe (or exited), without writing to it?".

          И там даётся ответ: "Oddly enough, it appears that when the last reader closes the pipe, select indicates that the pipe is readable".

        • Проверил на тестовой программке (она тут ниже закомменчена) -- да, так и есть: по закрытию читательского конца pipe'а писательский становится готов на чтение (а изначально -- не был). Хотя попытка сделать из него read() возвращает -1/EPIPE.

          Проверялось на CentOS-7.3.1611, ядро 3.10.0-514.el7.x86_64.

          22.02.2019@лыжи: можно ещё вот как сделать: не по 1 штуке pipe на каждый драйвер, а ОДИН процесс отфорковывать, и чтобы он ждал ВСЕХ прерываний, а через pipe передавал бы полученный номер (и вектор?), а уж получатель -- в основном, layer -- разбирался бы и раздавал страждущим.
      • А на будущее -- пилить CAEN'овцев, чтобы сделали "как надо".
    21.02.2019@после обеда, дорога-на-работу-около-ИПА: нужен "3-й вариант" -- спросить Сенченко, как у них на MVME3100.
    21.02.2019@пультовая, после четвергового сборища-по-автоматизации-в-15:00: спросил.

    Ответ: они используют "стандартную VME'шную поддержку в ядре Linux". Она вроде как уже несколько лет есть для какого-то MVME5xxx, а Котов по аналогии запилил для MVME3100.

    Обещал прислать ссылку.

    22.02.2019: прислал. Правда, там на вид всё -- только для программирования драйверов в ядре, а про userspace как-то глухо.

  • 22.02.2019: на время "переходного периода" (от старого, BIVME2-only, к новому унифицированному/годному для разных платформ) будем вести работы в drivers/vme/mbivme2/ и drivers/vme/nsrc/.

    05.11.2019: начальные работы в nsrc/:

    • "Система сборки":
      • Создан ShadowRules.mk, копированием из can/src/ShadowRules.mk и тривиальным допиливанием.
      • Список драйверов будет жить в файле LISTOFVMEDRIVERS.mk -- как в can/ и serial/, всё заглавными буквами (а не camel-case, как было раньше и всё ещё есть в camac/).
    • Сделан vmeserver_common.c.
      • Копированием из canserver_common.c и модификацией, конечно.

        "Модификация" в основном свелась к удалению всего лишнего -- команд-то специфических для 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()!

    • Ну так пусть *server'ы и умеют загружать модули динамически -- при наличии соответствующего "разрещения" вроде MAY_USE_DLOPEN.
    • Грузить можно хоть с помощью cxldr, хоть вручную dlopen()'ом.
    • В любом случае, "префикс" и "суффикс" пусть указываются в командной строке, как у rrund.

      Только надо будет как-нибудь это сочетнуть с тем, что сейчас первым опциональным параметром является номер порта (переделать на "-p"?).

    • И надо, чтобы по окончанию использования -- по завершению последнего экземпляра драйвера -- он бы "выкидывался", при помощи dlclose().

      Смысл -- чтобы можно было обновлять драйверы без необходимости рестарта remsrv (как это сейчас можно делать с обычными .drvlet'ами).

      07.11.2019: и даже больше: а не ввести ли в remsrv -- ExecConsCommand() -- команду "выгрузить всё", т.е., грохнуть все экземпляры dlopen()'нутых драйверов? Или и вовсе "реинициализация" -- грохнуть ВСЕ устройства (это приведёт и к выгрузке)?

    • И да, надо будет какую-нибудь "поддержку сторонней сборки" сделать, в виде какого-нибудь .mk-файла.

      DirRules.mk не очень катит -- DriverRules.mk? Или всё-таки катит?

    • 07.11.2019@утро-душ: а имена таким драйверам давать -- bivme2_NNN_drv.so (cm5307_NNN_drv.so, moxa_NNN_drv.so, mvme3100_NNN_drv.so).

      А sktcanserver так и вовсе спокойно может загружать обычные серверовы драйверы -- он же предоставляет обычную среду исполнения.

    15.12.2021: "переходный период" закончился, директории переименованы: старые bivme2/ и src/ стали old-bivme2/ и old-src/, а mbivme2/ и nsrc/ превратились в bivme2/ и src/.

    Все Makefile'ы и *.mk подправлены.

    Засим и разделу-то уже пора ставить "done" -- что и сделано.

  • 31.10.2019: попробовал запустить ДВА драйвера adc4x250 на BIVME2. И оказалось, что это принципиально нерабочая конфигурация, т.к.
    • Эти девайсы работают только с IRQ=5.
    • Но если несколько разных процессов открывают один /dev/vmeiN, то реально ловит прерывания только ОДИН из них (видимо, первый).

    Вывод: даже для BIVME2 просто ПРИДЁТСЯ работать через layer, чтобы он был единственным "ловцом IRQ" и распределял бы их по драйверам в зависимости от векторов (менеджмент аналогично CAN'овским kid'ам).

  • 25.12.2019@утро-дома: планы на будущее -- что сейчас ещё не сделано в процессе перевода на layer'ность, плюс что желательно сделать для расширения репертуара.
    • Довести vme_test_common.c до рабочего состояния, пока хотя бы на примере bivme2_test.
    • Поддержка MVME3100.
    • Поддержка A3818.
  • 25.12.2019@утро-дома: мысли общего характера на тему "а как вообще разумнее/правильнее всего работать с VME?".

    Собственно, работа состоит из 2 частей:

    1. Ввод/вывод -- тут всё просто: набор операций чтения и записи, в вариантах a{16,24,32}{byte,word,dword}, плюс аналогичные блочные операции.
    2. Главный вопрос -- в ловле прерываний.

      Сейчас на BIVME2 открывшему линию /dev/vmeiN выдаются ВСЕ вектора с этой линии.

      А как МОЖНО было бы, покрасивше:

      1. Позволить множественное открытие /dev/vmeiN, чтобы ВСЕ вектора отдавались ВСЕМ открывшим.

        Вроде как что-то подобное Паша Селиванов реализовал для can4linux на мамкинских контроллерах.

      2. Позволять открывшим /dev/vmeiN программам изъявлять свой интерес к конкретным векторам -- очевидно, через ioctl(); тогда конкретные вектора будут отдаваться конкретным файлодескриптородержателям.

        Т.е., нечто похожее на технологию Шичкова, но только не для ядра, а для userspace.

        И ещё более это аналогично подходу, используемому в USPCI -- этакий "USVME"? :-) Где бонусом является автоматическое освобождение ресурса при закрытии дескриптора.

      Вариант (a) наводит на мысль посмотреть, как это сделано у Селиванова. Спросить его мылом, что ли...

    25.12.2019: насчёт "как это сделано у Селиванова" -- и спрашивать не понадобилось.

    • Ответ нашёлся в мыле от 26-05-2017 "Re: Вопрос про драйвер can4linux на BIVME".
    • Во-первых, там сказано
      /* number of processes allowed to open a CAN channel */
      #ifndef CAN_MAX_OPEN
      # define CAN_MAX_OPEN 1
      #endif
      
      и "Под каждый канал выделяется память для rxfifo".

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

    • Во-вторых, там же ссылка на скачивание -- ВОТ (это l6-1-521-2).

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

    • Немножко порылся -- там подход, аналогичный USPCI'ному, где есть массив
      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'ный подход выглядит красивше :-).

  • 31.10.2021@~16:05, по дороге к папе, около стадиона НГУ: пришла в голову идея, как решить проблему "из-за daisy-chain-топологии линий IRQ в VME, пока обрабатывается IRQ 2-го устройства, то 1-е успевает отработать и выставить снова, так что до 3-го очередь никогда и не доходит":

    Сменить идеологию с "вычитать 1 вектор, обработать, потом вычитывать и обрабатывать следующие" на "вычитать (в буфер) до 100 векторов, а потом идти по этому буферу и обрабатывать".

    01.11.2021: посмотрел в обеих текущих HAL-реализациях (BIVME2 и A3818) -- да, так сделать МОЖНО. Неудобство только в том, что в обоих случаях вычитывание каждого экземпляра вектора будет индивидуальным/штучным (в BIVME2 -- read() пока не настанет ошибка (хотя тут-то МОЖНО было позволять вычитывание "массива" векторов -- у драйвера они в FIFO хранятся), в A3818 -- CAENVME_IACKCycle() также до ошибки (и тут уж без вариантов)).

    02.11.2021: в a3818_hal.c реализовал (внутри очередного #if'а) -- работает, обрабатываются прерывания ото всех устройств.

    (Правда, паттерн срабатывания несколько неожиданный, но это уж вопрос для отдельного анализа.)

vme_io:
  • 15.02.2019: тут будет всё про API (HAL) "vme_io".
  • 22.02.2019@институтская-сессия: начинаем описание интерфейса. Файл vme_io.h, живёт в hw4cx/include/.

    22.02.2019@институтская-сессия: на данный момент содержит 3 вещи:

    1. Определения VME_OP_* и VME_COND_*, переехавшие (как давно (с 14-07-2017) хотелось) из bivme2_test.c в общие определения.
    2. "Открывание линии" -- vme_io_open(). Ей передаются и space_size, и am.
    3. Функции чтения/записи -- vme_io_a{16,24,32}{wr,rd}{8,16,32}.
      • Первым параметром им передаётся пока что невнятный zzz.

        Потому как неясно, ЧТО там должно быть: файловый дескриптор, какой-то ещё дескриптор, или что?

      • Также им всем передаётся (вторым параметром) am.

        Предполагается, что в HAL'ах, где уставка при открытии (как BIVME2/libvmedirect) этот параметр будет игнорироваться, а передавать его в любом случае будет layer, сохранив при открытии у себя.

    05.11.2019: очень-очень похоже, что концепция "vme_io" уйдёт, уступив место "vme_hal".

  • 05.11.2019: собственно разработка "vme_hal".

    05.11.2019: оно полувиртуально и эфемерно, но всё же уже появилось в виде конкретного файла vmehal.h (НЕ "vme_hal.h", поскольку полностью аналогично canhal.h).

    • Пока тут, в отличие от canhal.h, одни сплошные определения-прототипы функций.
    • Прототипы функций В/В генерятся макросом VMEHAL_DECLARE_IO() -- он парный к bivme2hal.h'еву VMEHAL_DEFINE_IO().
    • Из каких-то "на-всякий-случай'шных" соображений все функции префиксируются #define-именем VMEHAL_STORAGE_CLASS, которому при не-определённости присваивается значение "static".
    • ...но при добавлении работы с IRQ понадобится добавить и всякие VME_OP_* с VME_COND_*.

    14.11.2019: доделано (на нынешнем уровне понимания):

    1. Введён тип vmehal_irq_cb_t -- прототип callback'ов по приходу IRQ.
    2. Функция vmehal_open_irq() теперь принимает 3 параметра:
      • irq_n -- номер.
      • cb -- callback, который вызвать.
      • uniq -- что передать cxscheduler'у при регистрации файлового дескриптора (дабы, если что, мог бы сделаться cleanup).

      Возвращает int-результат -1 при ошибке либо 0 при успехе.

    3. Функция vmehal_close_irq() принимает единственный параметр -- irq_n.

      Она формально int, но это "на всякий случай".

    Замечания-комментарии:

    • СЕЙЧАС сделано всё под bivme2hal.h в паре с vme_lyr_common.c.

      Возможно, для прочих HAL'ов (a3818hal.h?) и, тем более, для vme_test_common.c понадобятся ещё модификации интерфейса.

    • "Нынешний уровень понимания" заключается в том, что нет ни блочных операций, ни CHECK/RESET для IRQ.

    31.12.2019: некоторые мысли на тему общего интерфейса, с учётом специфики A3818:

    • @дорога-из-ИЯФ-в-Быстроном: явно надо добавить к VME-адресам ещё один параметр-префикс -- "номер шины". И для BIVME2 всегда указывать там просто 0.
    • @дорога-из-ИЯФ-в-Быстроном: и ещё надо будет как-то уметь указывать её в vme_test_common.c.
    • @дорога-из-Быстронома-домой: а ещё может понадобиться средство как-то узнавать статус самого VME-адаптера (особенно с удалённой линией), уметь делать ему всякую диагностику (а ещё там битики регистра есть!) и сброс.

      Для этого можно предусмотреть в API специальные вызовы, которые использовать исключительно в специальных "драйверах", не привязанных ни к каким устройствам, а общающиеся лишь с самим адаптером через те вызовы.

    02.01.2020@дома:

    • С "номером шины" проблема: как минимум у A3818 (и в CAENVMElib вообще) это не 1 число, а 2: номер адаптера в компе и номер линии в адаптере.

      И что теперь -- ДВА числа префиксов делать?

      Или как-то махинировать, для разных HAL-модулей указывая разное число businfo-параметров? Но это крайне неудобно: ведь именно драйверы выполняют сдвиг jumpers->base_addr, так что просто передавать lyr'у сразу весь businfo[] не прокатит (ну не указывать же отдельным параметром, что компонент "адрес" - это реально jumpers, которые надо сдвинуть влево на столько-то...).

    03.01.2020@дома:

    • Может и не слишком красиво, но зато просто: да, сделать ВСЕГДА два дополнительных префикс-параметра, adapter_N,line_N.
    • А насчёт vme_test_common.c -- префикс сделать опциональным, [@ADAPTER_N[/LINE_N]:]. Не указано -- =0.

      И чтоб конкретные "варианты" могли бы указывать, какие части этого префикса осмысленны, дабы его не было, например, в 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@дома:

    • А ведь у нас НЕТУ в vme_hal операции "открыть модуль", не говоря уж об "открыть линию". А эта операция будет необходима, как минимум для a3818.
    • Надо всё-таки добавить "_" в имена hal-файлов: vme_hal.h вместо vmehal.h, bivme2_hal.h вместо bivme2hal.h etc.

    05.01.2020@дома:

    • ...но тогда ведь придётся и в префиксы вставить '_'...

    06.01.2020@дома:

    • Ну начал vme_hal.h с обновлённым интерфейсом. Итак:
      • Имя с "_".
      • Соответственно, все префиксы пришлось переделать с vmehal/VMEHAL на vme_hal/VME_HAL. Не очень красиво.
      • В частности, введена "поддержка нескольких шин":
        • Добавлены вызовы vme_hal_open_bus() и vme_hal_close_bus().
        • При открытии передаётся пара bus_card,bus_line, а возвращается "непрозрачный handle", должный затем передаваться всем операциям с этой шиной.
        • В vme_hal_open_irq()/vme_hal_close_irq() также добавлен первым параметром bus_handle.
      • Чуть позже: после осмотра CAENVMElib.h осознано, что там дуплетом являются не "номер PCIe-адаптера, номер линии в адаптере", а "номер линии, номер VME-контроллера на линии".

        Т.е., видимо, линии нумеруются всквозную по всем адаптерам в системе (как в CAN-адаптерах), а уж на каждой линии там может сидеть до скольки-то VME-контроллеров и прочих карт.

        В результате: пара bus_card,bus_line переименована в безликие bus_major,bus_minor (как и предполагалось изначально).

      • Также *_hal.h должны предоставлять "лимиты" VME_HAL_MAX_BUS_MAJOR и VME_HAL_MAX_BUS_MINOR - чтобы HAL-юзеры могли бы проверять осмысленность параметров и использовать лимиты+1 как размерности своих массивов для хранения "объектов-шин".
    • Переделанный вариант bivme2hal.h, с подчерками и прочими изменениями делается в bivme2_hal.h.

      VME_HAL_MAX_BUS_MAJOR=0, VME_HAL_MAX_BUS_MINOR=0.

    • Также из него копированием зачат a3818_hal.h, живущий в свежесозданной drivers/vme/a3818/.
    • Плюс создана, пока пустая, drivers/vme/local/.

    07.01.2020@дома:

    • А ведь забыл, что надо bus_handle передавать ВСЕМ операциям - в т.ч. и чтениям/записям!

      И это тоже добавлено.

    • И потихоньку пытаюсь делать a3818_hal.h.

      Идёт со скрипом: вроде поштучно все аспекты понятны, и все требующиеся "компоненты" где-то уже использовались, но полного понимания ("огроканности во всей полноте") пока нет. Поштучно-то ясно, но не вылезет ли при их сочетании какая-нибудь ранее не встречавшаяся проблема?

      Например, принцип кодирования 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".
      • Первое -- в смысле имени API, указываемого в LayerModRec'е; это всегда "vme".
      • Второе -- "имя модуля", включающее также имя HAL'а.

      Выбрано очевидное решение:

      • Первое переименовано в VME_LYR_API_NAME -- тем более, что соответствующее поле в LayerModRec'е как раз и называется api_name.
      • Аналогично VME_LYR_VERSION* переименованы в VME_LYR_API_VERSION*.

      И то же самое нужно будет проделать со всеми остальными layer'ными инфраструктурами (CANKOZ, PCI4624, PIV485). 10.01.2020: сделано, со всей троицей.

    • Затем в vme_lyr_common.c были подшаманены HAL-вызовы: первым параметром добавлен пока что фейковый символ QQQ_HANDLE.
    • @~18:00, дорога домой: блин -- а ведь VmeAddDevice() забыл добавить параметры bus_major,bus_minor, вместе с их использованием в драйверах!!!

      10.01.2020: сделал, и то, и другое, и третье.

    10.01.2020@утро, дорога через студгородок к родителям: пара мыслей насчёт организации "массивов описателей шин":

    1. Массив "свойства линий" пусть будет ЛИНЕЙНЫМ: просто ищется свободная ячейка, и в неё записываются bus_major,bus_minor. И handle -- просто номер этой ячейки.

      Да, поиск по массиву -- несколько более затратная операция, чем просто декодирование handle'а. Но этот поиск будет очень редкой операцией -- только при регистрации нового устройства.

      Размер этого массива (количество ячеек): для BIVME2 и MVME3100 -- очевидно, 1; для A3818 -- от балды (10 обычно хватит).

    2. Стоит СОВМЕСТИТЬ в одном цикле поиск свободной ячейки с проверкой "а не используется ли уже": прямо в цикле искать первую свободную (в начале -- first_free=-1), и там же проверять, что есть/нет ячейка с такими bus_minor,bus_major.

    Это касается И vme_lyr_common'а, И a3818_hal'а.

    Следствия:

    • Параметры VME_HAL_MAX_BUS_MAJOR и VME_HAL_MAX_BUS_MINOR нафиг не нужны.
    • Ибо нефиг layer'у их проверять -- пусть просто передаёт hal'у, а тот либо сам проверит (bivme2_hal, mvme3100_hal), либо просто передаст подстилающей библиотеке (a3818_hal).
    • А вот количество ячеек в массиве "свойства линий" -- это уже пусть hal-модуль и указывает.

    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.

    Но есть проблема -- у нас ДВА уровня, могущих возвращать ошибки:

    1. Подстилающая библиотека -- возвращает некие отрицательные коды.
    2. Сам HAL-модуль -- возвращает код через errno.

    Т.е., иногда имеет смысл смотреть на одно, иногда -- на другое, а иногда -- в функциях открытия шины и/или прерывания -- на оба (от HAL'а ошибки вроде "уже используется" или "закончилось место в таблице", а от библиотеки всякие "ошибка открытия линии").

    Ну и как же поступить?

    • @дорога домой на обед, около мыши ~14:00: как назвать функцию, возвращающую описание по коду? Да очевидно же -- vme_hal_strerror().
    • @поход в пультовую, после обеда: а довольно просто -- если errno!=0, то считаем это кодом от HAL'а и используем её, а иначе используем вёрнутый код.

    Делаем:

    • vme_hal_strerror() добавлена в vme_hal.h, в a3818_hal.h она переадресовывается к CAENVME_DecodeError(), а в bivme2_hal.h просто сама делает
      return errcode == 0? "NO_ERROR" : "JUST_AN_ERROR";
    • Функции открытия в *_hal.h при возврате кодов ошибок от CAENVMElib'а делают errno=0.
    • В использование в выдачи сообщений об ошибках повсеместно вставлен код
      errno != 0? strerror(errno) : vme_hal_strerror(r)
    • ...за единственным исключением: vme_test_common.c::PerformIO() ВСЕГДА использует vme_hal_strerror(r), поскольку HAL-методы В/В никогда не делают errno=0 (тут уж вопрос скорости).

      23.11.2021: прикол в том, что оно там само делает errno=0 прямо перед вызовом do_io().

    Проверил -- да, всё работает. Хотя особого счастья это пока не дало, поскольку CAENVMElib'овские сообщения весьма скупы.

  • 22.01.2020: слегка надоело, что при компиляции *_test'ов выдаются warning'и "defined but not used" на кучу функций -- в данном случае на векторные.

    Надо бы им добавить __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: да, сделал -- всё прекрасно!

  • 30.01.2020: понадобилось добавить метод vme_hal_term() -- для a3818, который регистрирует периодический таймаут (для поллинга прерываний), чтобы была возможность этот таймаут подчистить.

    Заодно и для bivme2_hal.h оказалось полезно -- вызывать libvme_close().

  • 30.01.2020: прямо напрашивается сделать, чтобы vme_hal_init() принимал параметр uniq: оно нужно и для a3818_hal.h (периодический таймаут помечать), и для bivme2_hal.h (файловые дескрипторы к /dev/vmeiN).

    31.01.2020: делаем.

    • В прототип -- добавлено.
    • В оба вызова (lyr и test) -- тоже.
    • Использование в a3818_hal.h тривиально -- тут же передаётся sl_enq_tout_after(), а дальше уже сама a3818_heartbeat_proc() повторяет в заказе то же значение.
    • А в bivme2_hal.h даже не понадобилось: в 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.

  • 05.02.2020: насчёт векторных операций -- они же могут быть ДОЛГИМИ, и если внутри BIVME2 всё равно ничего нельзя делать, пока операция выполняется, то в A3818 это уже не совсем так: есть функция 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: в продолжение темы:

    1. @у Антона Павленко: а ведь для MVME3100 тоже возможен "асинхронный" режим -- оно ж умеет работать через DMA, когда даже одноядерный процессор занимается другими делами в то время, пока DMA-контроллер выполняет копирование. У них-то используется отдельный thread, висящий на ожидании, но идеальнее, конечно же, было бы какое-нибудь уведомление.

      Так что уже не один контроллер подобное позволяет -- точно нужна общая API-модель.

    2. @на защите кандидатской Борисовой (электролюминесценция в аргоне, для детекторов тёмной материи) ~10:30: другое соображение -- по надёжности: ведь может быть ситуация, что запущена операция асинхронного чтения, но ещё ДО её завершения драйвер "прибивают" (хоть _devstate=-1, хоть просто закрытием соединения, хоть ещё как).

      И тогда окажется, что запущена -- через ядро! -- операция, трогающая память, которая уже освобождена. И-и-и -- как быть в такой ситуации?

      @вечер ~16:30: спросил у Павленко, как у них с этим на MVME3100 -- а они на эту тему даже не задумывались, так что хбз.

      @вечер, дорога из ИЯФа к УД СО РАН ~17:30: в CX-то формально всё в наших лапах: поскольку драйверов privrec освобождается сервером, то, в принципе, можно ввести какой-нибудь серверный вызов "не высвобождай драйверов privrec по завершению драйвера", а выполнять таковое высвобождение самостоятельно (помещая все эти драйверы в отдельный список). Вопрос только, кто и когда будет этими заклинаниями заниматься.

      07.02.2020: в принципе, сервер (и cxsd_hw, и remsrv_drvmgr) высвобождает privrec ПОСЛЕ вызова layer'ова disconnect(); так что можно "оптимизировать" -- запрашивать у сервера не-высвобождение только для тех, у кого в текущий момент висит ИСПОЛНЯЕМЫЙ запрос на асинхронное чтение, заодно помечая запрос в очереди как "устройство отправилось к праотцам", и по завершению операции вместо уведомления драйвера просто выполнять освобождение.

    Всякие разрозненные мысли по теме:

    • При переводе adc250_drv.c на асинхронное векторное чтение придётся радикально менять модель работы вычитывания: чтобы вся работа, сейчас делаемая в ReadMeasurements(), выполнялась бы в closure-callback'е векторного чтения, и оттуда же вызывалось бы drdy(), а ReadMeasurements() станет пустышкой-заглушкой.

      Да, у нас подобная модель уже используется в ottcam_drv.c.

    • Кстати, как вариант реализации API: асинхронные операции могут возвращать 0 при успешном запуске и >0 при уже завершённости операции (как на BIVME2, где никакой асинхронности быть не может).
    • @переход-в-13-е: ещё мысль, насчёт LYR-интерфейса "vme": можно к методу add() добавить ещё параметр "flags"/"options", где бы указывать всякие особенности работы, в т.ч. необходимость заранее зарегистрировать ОТДЕЛЬНЫЙ bus-handle
    • Если же НЕ делать отдельных bus-handle для каждого устройства, а держать все блочные чтения на ОДНОМ bus-handle (отдельном от обычного В/В), то придётся там сделать "очередь операций".

      ...и гонять эти операции, очевидно, придётся в отдельном thread'е, а модификацию очереди окружать mutex'ом.

      • Основное время тот thread должен будет висеть на ожидании завершения.
      • Но во время этого ожидания mutex будет незанят -- т.к. само ожидание НЕ лазит в очередь. ПОСЛЕ же ожидания будет производиться удаление дожданного элемента из очереди -- да, в mutex'е -- и отправка сообщения о готовности (через pipe?).
      • А в отсутствие запросов в очереди -- придётся висеть на чём-то другом; очевидно, на чтении/готовности-на-чтение некоего pipe'а
      • Мысль: поскольку модификации очереди -- по факту являющиеся одновременно переходами между состояниями "очередь пуста" и "очередь непуста" -- окружаются mutex'ами, то можно безопасно ограничить запись в тот pipe "появился новый запрос": выполнять её только при переходе из состояния "очередь пуста" в "очередь непуста", поскольку только в состоянии "пуста" ожидатель будет висеть на ожидании pipe'а, а не VME.
      • Кстати, поскольку у нас есть 2 разных pipe, то можно вместо них (4 дескриптора) использовать 1 пару сокетов, добытых от socketpair() (2 дескриптора).

        Дополнительный плюс этого подхода в том, что одновременно получаем механизм "слежения" за умиранием собеседника. Он бы как бы и так бы был, но проверка готовности на чтение обычного сокета выглядит всё же менее шизоидно, чем проверка пишущей стороны pipe'а.

    09.02.2020@утро-душ: выкристаллизовался в голове проект, как это надо делать (тут много специфики A3818, что не совсем к месту в разделе о vme_hal, но что уж поделаешь):

    • Действительно, при открытии шины дополнительно открывать ВТОРОЙ CAENVMElib_handle и создавать thread.

      18.02.2020: фиг, не будет это работать: как сегодня проверено на примере попытки ловить прерывания отдельным thread'ом, CAENVMElib просто не позволяет открыть ту же шину второй раз.

    • ...была мысль делать это "по запросу" -- т.е., когда первый драйвер попросит такой функционал, указав в options специальный флаг.
    • Этот thread будет только висеть на ожидании готовности либо на чтении из "pipe'а" (реально -- сокета).

      Ну и прочий менеджмент очереди -- как описано выше за 06-02-2020.

    • А для решения проблемы "драйвер убивают ДО завершения асинхронной операции" придётся вводить серверный вызов "не free()'ить devptr".

      И делать этот вызов -- запрета высвобождения -- в disconnect()'е, в случае, если грохается драйвер, чья асинхронная операция сейчас в исполнении (т.е., находится в голове очереди).

      18.02.2020: неа, не так: не нужно ничего лишнего от сервера, а надо просто буфера для async-чтение держать у layer'а -- чтобы он их для драйвера аллокировал, и он же "отложенно освобождал" когда станет можно (см. в разделе о cxsd_hw за сегодня).

    • Для доступа к асинхронному чтению -- дополнительный набор методов, с суффиксом "v_async".

      ...только ли для чтения, или же и для записи тоже -- вопрос.

    • Эти методы должны возвращать -1/0/+1 -- чтобы те HAL'ы, где оно не реализовано (bivme2_hal), могли бы сводить это всё к обычным v-операциям, из которых возвращать +1.
    • Ну и до кучи: тогда и прерывания в A3818, похоже, надо ловить аналогично -- ТРЕТЬИМ handle'ом и тоже отдельным thread'ом.

    10.02.2020@вечер-дома: подумал-подумал -- неа, не стоит это всё делать. Резон -- шибко много проблем:

    • Серверный вызов "не free()'ить devptr" -- реальный challenge: трудоёмкий, могущий иметь неочевидные последствия; плюс это добавление сильно испортит элегантность нынешней модели.
    • Использование множественных CAENVMElib_handle'ов может глючить -- это было увидено на примере странных подвисаний и SIGSEGV'ов сервера при запущенном CAENVMEDemo.
    • Да и вообще сама реализация всей инфраструктуры с очередями и thread'ами...

      Конечно, она точно может быть сделана -- принципиальных проблем там не видно.

      Но это будет очень трудоёмко, сложность получившегося кода (а это затронет ВСЕ уровни -- HAL, lyr, драйверы) будет очень большой, так что из сравнительно простой и прямолинейной реализации получится весьма навороченная, трудная для восприятия и плохо поддающаяся дальнейшим модификациям (поскольку вот это добавление асинхронности весь резерв модифицируемости и сожрёт).

    А выигрыш от всех этих сложностей будет не очень великим: некоторое повышение производительности.

    • Да, тем самым мы сможем выжимать ВСЕ ресурсы/резервы из системы.
    • Но по факту для имеющихся потребностей даже сейчас скорости A3818 хватит с запасом.

    Вывод: игра не стоит свеч.

  • 11.03.2020@дома: вчера ЕманоФедя прислал письмо, породившее некоторые обсуждения и результирующие мысли. Вывод вкратце: надо бы поменять идеологию и API ловли прерываний, чтобы:
    1. Вместе с прерыванием выдавался бы timestamp момента его ловли -- чтобы драйвер мог отдать серверу конкретное время, когда же произошло измерение.

      (А то пока считывается пачка осциллограмм, проходит много времени и автоподставляемые метки времени на последних уже сильно не соответствуют реальности.)

    2. Также для этого надо сменить идеологию с нынешней "100 раз пытаемся вычитать вектор, потом сразу же вызвать обработчик" на "100 раз пытаемся вычитать вектор, всё вычитанное складываем в массив, затем идём по массиву и вызываем обработчики".

      Такая мера позволит более корректно сопоставить timestamp прерываниям: иначе -- при последовательном "вычитали, вызвали" -- нет гарантий, что очередное обрабатываемое прерывание возникло в тот же начальный момент, что и предыдущие (оно могло появиться позже).

    Да, у такого варианта тоже есть недостатки:

    • Прерывания, возникшие чуть-чуть позже (ПОСЛЕ начала обработки, когда успел вычитаться первый вектор, а остальные ещё не поступили) будут считаться возникшими СУЩЕСТВЕННО позже -- как бы после полного вычитывания первой осциллограммы.

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

    • Для BIVME2 это, скорее всего, будет почти бесполезно.
      1. Ну что придётся менять bivme2_hal_irq_cb() с поштучного вычитывания на "вычитать 100 штук в массив" -- мелочь.
      2. У него попросту не хватает производительности успеть "вычитать всё" за цикл (1/12.5с), поэтому он в любом случае не будет успевать.

        И есть шанс, что нынешнее "ближние к контроллеру успевают всегда, дальние реже, причём частенько читаются как бы следующим циклом" сменится на просто какую-нибудь иную неравномерность.

      3. Также проблема, что в CM5307/CANGW/BIVME2 нет нормальных часов: даже если при загрузке взять rdate'ом время с хоста, то потом оно потихоньку уплывает, а ntpd там штатно нету (хотя в powerpc-sdk-linux-v4.tgz'шном initrd_big есть -- можно взять оттуда).

        Вероятно, надо в remdrv'шные "опции" (вроде ж планировались такие -- парсимые самим remdrv для своего использования?) добавить флаг "игнорировать timestamp'ы от драйвлетов".

    11.03.2020: собственно исходные письма, демонстрирующие последовательность соображений и размышлений:

    • Фрагмент ЕманоФединого от 10-03-2020:
      4) Нужно каким-то образом реализовать "синхронный режим" измерения,
      т.е. чтоб при необходимости было полное измерениекартины за один
      выстрел, т.е.
      Сейчас один некоторые осциллографы неуспевают больше, некоторые
      меньше, и уже корреляций меджу разными картинами наводить не
      получится.
      
    • Мой ответ на то:
              а. Не хватает производительности контроллера.  А если CAEN?
      
              б. А по timestamp'ам наводить соответствие не получится?  Кто-то 
      чего-то не успевает, но пропуски ж должны быть равномерными, так что каждый
      второй-третий импульс должны б успевать все.
      
    • ...его ответ:
      может и можно, а еслислучайные пропуски будут такие, что запуски
      одного канала взаимоисключатся с другим, то как я общую картину
      собирать буду?
      
    • ...мой очередной ответ:
              Вряд ли - чисто по сути появления пропусков: самый "бесправный" - 
      это самый дальний от контроллера осциллограф, и если уж он успел сработать,
      то и все предыдущие тоже должны были успеть.
      
    • И моё "прозрение" -- вдогонку к вышенаписанному:
              Я тут подумал - неа, неверны обе идеи: ни timestamp'ами 
      пользоваться нельзя, ни "статистически ловить, когда все сработают".
      
              timestamp'ами нельзя потому, что они дают время НЕ срабатывания, а 
      ВЫЧИТЫВАНИЯ - которое при неуспевающести контроллера будет от времени
      срабатывания кардинально отличаться (вплоть до другого цикла запуска и даже
      больше).
      
              А статистика не покатит потому, что осциллографы опять же НЕ 
      срабатывать не успевают, а ВЫЧИТЫВАТЬСЯ -- IRQ-то они выставляют, но
      контроллер не успевает их вычитывать, а доходит до них аж в следующем цикле
      (пока более близкие к нему и уже вычитанные раньше ещё не словили запуск -
      тогда он и вычитывает "униженных и оскорблённых").
      
              В принципе, я могу модифицировать алгоритм ловли IRQ: чтобы 
      сначала добывались все IRQ, которые только есть, а уж потом шло бы
      вычитывание всех сработавших - это несложно.  Но тогда вместо нынешнего
      "ближние успевают всегда, дальние реже" будет "всеобщая нищета" -- ВСЕ будут
      не успевать почти равномерно, хотя и с приоритетом более ближних (те, будучи
      вычитанными первыми, будут к следующему старту уже взведены; а дальние будут
      только в процессе вычитывания и ближайший старт пропустят, сработав на
      следующем).  В результате в одной примерно одновременно пришедшей пачке
      будут смешаны осциллограммы из предыдущего (от ближних) и текущего (от
      дальних) циклов.
      

    11.03.2020: немножко анализа.

    • В идеологию/API "pzframe_drv" изменений вносить не нужно: в pzframe_retbufs_t УЖЕ присутствует поле *timestamps, просто оно пока везде ставится =NULL.
    • Т.е., "внедрять" надо в собственно ловле прерываний (в т.ч. передачу обработчикам) плюс в использовании -- драйверами и/или *_fastadc_common'ами.
    • Но в любом случае всё это -- паллиативные меры.

      Очевидно, что единственное ЖЕЛЕЗНОЕ решение проблемы -- именно в железе: чтобы система синхронизации передавала бы осциллографам (и прочим заинтересованным) "метку времени" -- пофиг, какого рода -- и из устройств можно б было её считать и отдать наверх вместе с данными как дополнительный параметр.

      Тогда сопоставлением этих меток от разных устройств можно составить единую картину: к одному "выстрелу" будут принадлежать измерения с одинаковой меткой.

  • 30.06.2021: имеет место некоторая недоработка API -- никак не фигурирует РАЗМЕР адресного пространства устройства.
    • Для BIVME2 и A3818 это неважно -- у первого всего 1 окно (фиксированного размера), устанавливаемое на нужное место, а у второго окон нет вовсе.
    • Но вот для всех bridge-based адаптеров -- и на Tundra, и на A25, и даже на ALMA2f -- этот размер всё же необходим.

    30.06.2021: типа обсуждения:

    • Прикол в том, что в API vme_lyr параметр space_size ЕСТЬ, но на уровень HAL он никак не смотрит.
    • И более того: в vme_hal есть понятие лишь "открыть шину", но нет понятия "открыть устройство" -- которое могло бы выполнять поиск/сопоставление окна.
    • А обойтись просто уставлением единственного окна всегда перед обменом (как предполагается сейчас), но С ЗАПОМИНАНИЕМ -- ставя размер равным размеру запрошенного обмена -- не удастся, т.к. следующий обмен из-за размера будет не совпадать.

    Получается, что:

    • К понятию "шина", с открыванием/закрыванием, надо будет добавить ещё понятие "диапазон адресов", который тоже можно будет открывать/закрывать, чтобы HAL-модуль мог бы в момент открытия находить подходящее окно.
    • И придётся "handle диапазона" также передавать операциям чтения/записи.
    • И оное понятие также будет опциональным (как и шина).

      Получится забавный набор реализаций: BIVME2:ничего, A3818:шина, MVME3100:диапазон.

    02.07.2021: но вообще есть надежда, что это всё нафиг не понадобится: либо вообще обойдёмся без Тундры, либо реализуем её по упрощённой схеме (уставление единственного окна на начало В/В); а потом и вовсе VME наконец-то закончится.

    15.12.2021: надежда не сбылась -- поддержка Тундры/MVME3100 сделана таки была, и проблема "неизвестного на уровне HL требуемого размера" там решена алоритмично-эвристически: размер окна всегда выбирается максимально возможный (для A32 -- 21 * 0x01000000), а его начало старается сдвинуть пониже -- всё это в предположении, что адреса устройств выбирали не идиоты и старались сгруппировать их рядом, чтобы все могли помещаться в 1 окно.

vme_lyr:
  • 15.02.2019: здесь будет обсуждаться API layer'а "vme_io".

    22.02.2019@институтская-сессия-конференц-зал: принято решение переименовать "vme_io_lyr" в просто "vme_lyr".

    Во-первых, чтобы не было перекрытий по именам с vme_io, а во-вторых из-за того, что словцо "io" в имени layer'а просто лишнее.

  • 22.02.2019@институтская-сессия: пытаемся сварганить описание интерфейса layer'а. Файл vme_lyr.h, живёт в hw4cx/include/.

    22.02.2019@институтская-сессия: тут пока мало определённого.

    • Открывание -- VmeAddDevice -- со всеми параметрами: тут и am, и irq_n+vme_irq_proc.
    • А операции чтения/записи (нет, не так -- записи/чтения) -- уже БЕЗ am.

      Тем самым мы постулируем, что модель будет "address modifier указывается только при инициализации, а дальше весь В/В идёт с ним одним, неизменным".

    • 01.11.2019: ага-ага -- только в случае libvmedirect оный address modifier выставляется ЯВНЫМ ВЫЗОВОМ libvme_ctl(VME_AM_W,), и действует НА ВСЕ СЛЕДУЮЩИЕ операции.
      • Пока это шло в каждом процессе отдельно -- ну да и ладно (хотя см. ниже).
      • Но теперь всё в ОДНОМ процессе, и тут уж придётся постоянно жонглировать этим AM'ом.

        Из чего следует, что:

        1. bivme2hal.c должен помнить последний из использованных AM'ов, и во всех операциях чтения/записи сначала проверять, что если am!=last_am, то делать
          libvme_ctl(VME_AM_W, &am);
          last_am = am;
          
        2. А для возможности этого -- ВСЕ HAL'овские вызовы read/write должны включать параметр "am".

        Жутковато, конечно, но радует то, что в большинстве случаев будет постоянно использоваться один и тот же AM, поскольку, например, у всех из семейства adc4x250 он идентичен.

      • А ещё сейчас заглянул в исходники -- libvmedirect.c (лежат в powerpc-sdk-linux-v4.tgz в linux/apps/bivme2_direct_lib/).

        Так вот: похоже, что 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().
    • Сделан в интересах canserver'а, чтобы тот мог на удалённую консоль выдавать детальную информацию.

      В частности, add()-то при запросе уже занятого IRQ:VECT выдаёт в лог ругательство с handle,devid занимающего -- а тут можно будет ознакомиться с информацией о нём подетальнее.

    • И добавлен метод не в конец (как в CAN, где он добавлялся сильно позже создания API), а прямо сразу после add(), ДО методов wr/rd.
  • 31.10.2019: приступаем. Делаем по образу и подобию cankoz_lyr_common.c, даже временами копируя куски (заменяя "CANKOZ" на "VME").

    31.10.2019: процесс:

    • Следствие: да, тут будет инфраструктура "с HAL'ами" -- т.е., общий vme_lyr_common.c, который symlink'уется в конкретные директории под конкретными именами (например, bivme2_lyr.c).

      По крайней мере, для BIVME2 такой вариант отлично подойдёт. ...а если в будущем понадобится что-то иное -- то ТОГДА и сделаем иное.

    • Начато, покамест просто базовый "скелет", пока и близко не годный для компиляции, просто копируем с camkoz_lyr_common.c с заменой "can"/"cankoz" на "vme".
    • ПРОБЛЕМА:
      • Начальная "идея" была в том, чтобы с IRQ:VECT обращаться так же, как в CAN'овском lyr'е делается с LINE:KID. Т.е., иметь матрицу [8][256], в которой и держать devid'ы использующих соответствующий IRQ:VECT, и это типа и будет "эккаунтинг" драйверов.
      • Но тут так нельзя: в отличие от LINE:KID, которые в CAN обязательны, тут использование IRQ совсем НЕ обязательно.

      Решение:

      • А с bivme2_io брать, там handle'ы как раз на то и заточены.
      • Оно ещё тогда, при создании (в 2012-м?) делалось с расчётом на использование в layer'ах.

    01.11.2019: продолжаем. Сделано минимальное наполнение vme_add().

    • Да, базовая парадигма "менеджмента" взята с bivme2_io.c (в будущем, при переходе на layer'ы, он, очевидно, исчезнет).
    • Информация по устройствам содержится в одномерном массиве devs[], а handle'ы являются просто индексами в нём.
    • Информация об IRQ будет содержаться в 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_".

      • И да, оно уже подготовлено к умению освобождать IRQ (в случае BIVME2 это будет соответствующий /dev/vmeiN) при освобождении последнего использовавшего его устройства.
    • vme_init_lyr() обеспечивает надлежащую инициализацию devs[] и irqs[].
    • В интересах всей этой шоблы заведены определения VME_HANDLE_ABSENT=0 и VME_HANDLE_FIRST=1.
    • Методы В/В -- переходники к vme_hal'овским функциям, в качестве адресов передают 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():
      • Существенное идеологическое изменение: линия считается УКАЗАННОЙ, если она БОЛЬШЕ 0; т.е., IRQ0 -- НЕ используется.

        Не знаю точно, как в стандарте 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", а дальнейшие усовершенствования уже отдельно.

  • 16.01.2020: а внести бы в VmeGetDevInfo() отдачу и bus_major/bus_minor -- чтобы потенциальный v4a3818vmeserver мог бы выдавать эту информацию по консольному интерфейсу.

    16.01.2020@вечер-дома: а ещё стоит добавить в VmeAddDevice() параметр "address size" -- во-первых, для выдачи тем же сервером, а во-вторых, может понадобиться при менеджменте окон в MVME3100.

    17.01.2020: сделал, и первое, и второе.

    И сервер уже выдаёт дополнительные данные (ну и громоздкая же длиннота получилась!).

  • 17.01.2020@утро-дома: а ещё проверил на BIVME2 top'ом (пришлось отдельно добывать из SDK) загрузку процессора при запуске пары ADC250 по 4096 точек и с внутренним запуском -- так вот, оба скрина-осциллографа показывают ~6fps, а загрузка процессора в районе 80%.

    Вывод: надо срочно вводить в HAL- и layer-API векторные операции. Да, на конкретно BIVME2 они будут экономить лишь время вызовов процедур (если вручную не лабать векторное расширение к libvmedirect), но уж хоть что-то.

    17.01.2020: да, в layer-API функции добавлены:

    • Имена аналогичны скалярным, но с суффиксом "v".
    • Данные всегда передаются как "uintNN *data", плюс параметр count.

    Покамест только определения, даже в самом lyr'е реализации нет.

    18.01.2020@дома, суббота: поддержка векторности доделана везде (хотя и никак не проверена):

    • Определения в vme_hal.h (это ещё вчера).
    • Реализация в bivme2_hal.h -- там всё делается в цикле обычными одиночными операциями, а оптимизация только в сокращении числа вызовов и в одной проверке на bivme2_hal_last_used_am для каждого вызова.
    • Релизация в a3818_hal.h -- вот тут уже задействуются имеющиеся BLT-вызовы, CAENVME_BLTWriteycle() и CAENVME_BLTReadCycle().
    • Ну и собственно в vme_lyr_common.c всё добавлено.
  • 17.01.2020: а ещё напрашивается добавить дополнительный метод для общения с конкретным HAL-модулем -- например, чтобы спрашивать статус A3818, моргать лампочками и т.д.

    Учитывая разнообразие и заранее-непонятность, напрашивается что-то типа ioctl().

    17.01.2020: в layer-API оно добавлено -- метод hal_ioctl().

    Параметры аналогичны обычному ioctl()'ю, но присутствует дополнительный строковый hal_api_name -- чтобы можно было по-простому проверять что-нибудь вроде "a3818", и если нет, то и не пытаться декодировать параметр request.

  • 24.01.2020: наконец-то дошли руки добавить поддержку множественных шин и в vme_lyr_common.c.

    24.01.2020: поехали!

    • Первым делом меняем то, как определяется "используемость" ("открытость") конкретного IRQ:
      • Раньше (последние несколько месяцев :D) в 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: ведь раньше-то callback'и получали только дуплет (irq_n,irq_vect) и этого для локальных контроллеров было достаточно, а теперь прерывания могут доставляться от разных шин!

      Итого:

      • В определение vme_hal_irq_cb_t первыми параметрами добавлены bus_handle (скорее для порядку) и privptr, через который регистратор может передавать нужную информацию -- например, номер ячейки данной шины, для чего...
      • В vme_hal_open_irq() добавлен параметр privptr, должный как-есть передаваться обработчику.
      • Ну и в bivme2_hal.h соответствующее сохранение и передача.

        (А в a3818_hal.h пока неактуально по причине отсутствия там работы с прерываниями.)

      • А в vme_lyr_common.c -- соответственно, в качестве privptr передаётся и принимается обратно "номер ячейки шины" -- bus_idx.
      • Связанное: в vme_disconnect() при поиске "было ли это последнее устройство на данной линии прерывания?" добавлено условие соответствия шины.

      31.01.2020: замечание: вообще-то этот "privptr" -- по сути является "bus_privptr" и по-хорошему должен бы указываться ШИНЕ, прямо в vme_open_bus() (он аналогичен серверно-драйверову devptr/privptr1, являющемуся свойством всего драйвера, в противоположность privptr (privptr2), регистрируемому для конкретного callback'а). Но из соображений "private-pointer должен указываться рядом с указанием функции, которой он будет передаваться" оставим как есть сейчас, пусть это и не совсем красиво.

    • Замечание по названиям: "handle" шин на разном уровне называются по-разному:
      • От CAENVMElib'а -- CAENVMElib_handle.
      • От vme_hal'а -- bus_handle (индексы в a3818_hal_bus_info[]).
      • Внутренне-layer'овские -- 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.

    Сохранение добавлено, проблема ушла.

  • 07.02.2020@душ-после-лыж: а ведь с A3818 есть ещё один аспект, ранее в голову не приходивший: крейт может непредвиденно отключаться и затем включаться, независимо от собственно хоста с контроллером!

    И надо уметь как-то ловить такие события, чтобы правильно ре-инициализировать аппаратуру (и "забывать" о том, в каком режиме находились железка и драйвер) -- совершенно аналогично CAN-bus'у.

    Проблема разбивается на 3 аспекта:

    1. Собственно ловля такого события. Это задача конкретно a3818_hal.h (и пока неясно, как именно реашбельная, и решабельная ли).
    2. Способ передачи таких уведомлений от HAL'а к его юзерам.
    3. Инфраструктура для передачи таких уведомлений драйверам.

    Кстати, вообще-то аналогичная ситуация и с PCI -- он ведь тоже hot-plug, особенно в варианте PCIe. Но там практически никто не заморачивается этим аспектом, хотя в идеале надо бы.

    Задача, кстати, интердисциплинарная -- касается и vme_hal, и vme_lyr. Но, поскольку ближайшая часть реализации будет на уровне layer'а, то записываем всё здесь.

    07.02.2020: предварительная мысль: для ПРОСТЫХ случаев может оказаться достаточно просто дёрнуть состояние драйвера -- SetDevState()'ом дрыгнув NOTREADY/OPERATING (ровно как это делает cankoz_lyr). И это поведение можно регулировать options (о каковом уже всё равно зашла речь).

    Не откладывая в долгий ящик, первую фазу подготовки делаем прямо сразу.

    • Заводим callback-тип 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) тогда сделать забыл...

  • 10.02.2020: потенциальное расширение на будущее: возможность указывать НЕСКОЛЬКО векторов, для "многовекторных" устройств, вроде VsDC4.

    По первоначальному заключению решено было vsdc4_drv.c реализовать путём регистрации add()'ом 4 устройств, со всеми совпадающими параметрами, кроме векторов.

    Но по здравому размышлению это выглядит дикостью.

    А более элегантным (хотя и чуток тяжеловесным) -- ПРЯМАЯ поддержка "многовекторности", для чего потребуется:

    1. Заменить в privrec'е поле irq_vect на массив irq_vects[] -- с тем же "правилом терминирования", по значению <0.
    2. Добавить метод add_vectors() -- ему передавать сразу ПАЧКУ кодов векторов.

      Или ВСЕГДА прямо в add()'е передавать именно пачку -- массивом с количеством?

    3. В vme_disconnect() подчистку перевести с одиночного присвоения =VME_HANDLE_ABSENT на цикл по irq_vects[] (которые надо будет предварительно -- ДО bzero() -- копировать в локальный массив).
bivme2:
  • 15.02.2019: сюда переедет обсуждение всех аспектов, специфичных для реализации поддержки BIVME2.

    ...а начнётся, скорее всего, с перевода bivme2_io на layer.

  • 22.02.2019@институтская-сессия: приступаем.

    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: начинаем заново -- уже в другой парадигме, с *hal.h-файлами, #include'имыми *lyr.c-файлами (которые сами являются симлинками на vme_lyr_common.c); т.е., в точности как с CAN.

    05.11.2019: работа:

    • "Система сборки" -- Makefile:
      • Скопирован с can/c4l-cangw/Makefile, с надлежащими переименованиями CAN* в VME (в соответствии с nsrc/ShadowRules.mk.
      • И он также заточен под вытаскивание "общих мест" в отдельный src/.mk-файл, тут это будет VmeserverShadowRules.mk.
      • И сделаны некоторые улучшения по сравнению с CAN'овским прототипом (которые бы и там не помешали): БОЛЬШАЯ часть определений/зависимостей, касающихся vmeserver'а, затащена туда.

        11.11.2019: а вот и ЗРЯ затащена! Поскольку там используются ссылки $(LIBnnn), определяемые только в ПОЗЖЕ include'имых файлах.

    • Начальный вариант bivme2hal.h.
      • Для начала, УЖЕ (01.11.2019) был сделан со статической переменной vmehal_last_used_am -- чтобы мочь жонглировать address-modifier'ом.
      • Сегодня же сделаны собственно методы *wr* и *rd*. Они определяются макросом VMEHAL_DEFINE_IO().
      • А вот работы с IRQ пока нет, ни в каком виде.

    11.11.2019: после небольших шаманств всё уже собирается.

    (Но работать пока нечему -- нету ни поддержки IRQ, ни самого содержимого драйверов под модель с layer'ами.)

    12.11.2019: в Makefile добавлено, что собирается оно прямо сразу вместе с $(LIBPZFRAME_DRV).

    Сделано просто чтобы СЕЙЧАС собиралось.

    Но в будущем надо переделать, чтобы библиотека подклеивалась к индивидуальным драйверам -- как, например, с cPCI'ными adc200me/adc812me. Потому, что в недалёком будущем появится модуль a3818_lyr, а драйверы нужно будет собирать под локальную платформу.

    14.11.2019: bivme2hal.h доведён до должного работать состояния:

    • Информация о регистрируемых callback'ах хранится в массиве vmehal_irq_info[8], содержащем структуры-описатели из 3 полей: fd, fd_fdh, cb.

      Для НЕиспользуемых номеров fd и fd_fdh содержат -1.

    • vmehal_open_irq() прямолинейна: открывает /dev/vmeiN, затем рагистрирует дескриптор через sl_add_fd().

      При обломе любого из этих шагов -- возвращает -1.

    • Собственно fd_proc vmehal_irq_cb() тоже прямолинеен: вычитывает int32-вектор и передаёт его зарегистрированному обработчику.

      При ошибке вычитывания (результат read() не равен ожидаемому) -- ругается на stderr.

    • vmehal_close_irq() совсем тривиальна: закрывает fd_fdh и fd, затем записывая в них -1.

    Функции открытия/закрытия просты настолько, что НЕ проверяют параметр irq_n: ни на попадание в [0-7], ни на повторное открытие/закрытие -- предполагается, что юзер HAL'а достаточно разумен.

    18.11.2019: первая проверка на реальном железе. Результат -- SIGSEGV при первом же обращении к VME-шине.

    Разбирательство показало, что была забыта инициализация, из 2 пунктов:

    1. libvme_init();
    2. libvme_ctl(VME_SYSTEM_W, &v) с v=1.

    Сделано, для чего:

    • В vmehal.h ведён HAL-вызов vmehal_init().
    • Его реализация в bivme2hal.h делает как раз те 2 пункта сверху; при обломе 1-го -- возвращает -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-шине/ Тогда как раз такой эффект бы получился.

    • Догадка о причине проблемы -- оптимизации -- пришла в голову Антону Павленко (shame on me!)
    • А до того мы проводили толпу разных тестов -- и bivme2_test у них гоняли (глючило!), и я даже утилитку test_rfmeas_a32r16.c слабал, выполняющую последовательность чтений указанных адресов указанным размером (16/32), и она у меня глючила, а откомпидированная Антоном у них -- нет!

      И когда я послал Антону мною собранный бинарник, то он там тоже глючил.

      Откуда возникла идея, что, возможно, разница в разных версиях libvmedirect.h и libvmedirect.h. Потратили полдня на поиск различий (файлы-то действительно разные, почему-то -- у меня за 2005 год "исходный из powerpc-sdk-linux-v4.tgz, а у них есть несколько разных за 2009, 2010, 2013...). Но оказалось, что файлы различаются только по whitespace. И тут Антона осенило об оптимизации :)

    • А собственно косяк обнаружился 06-11-2020 в том, что при попытке обратиться к новому девайсу L_TIMER, использующему D16 вместо обычного D32, почему-то считывался какой-то из ADC250 вместо L_TIMER'а.
  • 25.11.2019: некоторые соображения -- пока больше теоретические -- на тему "а как бы можно было именно ДРАЙВЕР ЯДРА подшаманить, чтобы он работал корректно?".

    25.11.2019@вечер-дома: собственно -- в чём проблема? В отсутствии буферизации. Ну так сделать её!

    Технология совершенно аналогична буферу клавиатуры, как он делался, например в BIOS/DOS.

    • Завести на каждый номер прерывания кольцевой буфер какого-нибудь разумного размера (устройств-то больше чем, например, 20, в крейте не бывает).
    • В обработчике -- добавлять вектор в буфер, в хвост.
    • В операции чтения -- вычитывать, из головы.

    Тут скорее вопрос -- а реально ли дело в ЭТОМ (т.е., в отсутствии буферизации), или же более глубокий косяк контроллера?

    Это можно проверить при помощи ДВУХ козачиных VADC16.

    26.11.2019@утро-дома: однако, может быть проблема: если некое устройство молотит с очень большой скоростью, то оно в одиночку забъёт весь буфер.

    Поэтому -- надо делать БУФЕР С ФЛАГИРОВАНИЕМ.

    • На каждую линию завести и буфер, и булевский массив для векторов was[256].

      Размер буфера -- да хоть тоже [256]. ...по факту это будет ДО 255 (из-за алгоритмики головы+хвоста буфера), но на это можно забить.

    • По приходу прерывания -- сначала получить значение vector, а затем с ним:
      • Проверять, что если УЖЕ was[vector]!=0, то ничего не делать.
      • А иначе -- добавить в буфер и взвести was[vector]=1.
    • В вычитывании:
      1. Взять из головы очереди значение очередного vector.
      2. Сделать was[vector]=0.
      3. Вернуть значение vector вызывающему.

      ВОЗМОЖНО -- поддерживать передачу векторов пачкой: если просят объём на чтение больше, чем sizeof(int); тогда просто

      • делать операции 1-3 в цикле,
      • предварительно найдя минимальное из (СКОЛЬКО_ЕСТЬ_В_БУФЕРЕ,СКОЛЬКО_ЗАПРОШЕНО),
      • и СРАЗУ, одной операцией, сделать verify_area(VERIFY_WRITE, buf, sizeof(int)*max_count).
    • Все операции -- добавление в очередь с взведением was[vector], выборку с удалением из очереди, циклом -- делать внутри критических секций.

    (Тут текста получилось больше, чем будет кода.)

    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.
    • Работа с очередью сделана по памяти, как в BIOS/DOS:
      • Добавление делается в голову (после чего q_head сдивгается на 1 с заворачиванием), чтение делается из хвоста (с последующим сдвигом q_tail с заворачиванием).
      • Очередь считается переполненной, если (q_head+1)&0xFF==q_tail.
      • А пустой -- при q_head==q_tail.
    • Соответственно, vme_interrupt() и vme_read() переделаны по утреннему проекту на работу с очередью.

      Замечания насчёт vme_read():

      1. Множественного вычитывания -- размером более sizeof(int) -- пока не делается.
      2. При попытке чтения из пустой очереди возвращается -EWOULDBLOCK.
    • Также переделан и vme_poll(): теперь вместо хитроумной проверки (там в vector записывалось "магическое число" -2 и после poll_wait() проверялось, всё оно же ли ещё там) просто проверяется равенство q_head==q_tail, и если "да", то возвращается 0, а иначе POLLPRI.
    • Кстати, vme_read() и vme_poll() переведены на "технологию с ret":
      1. В коде производятся разные проверки, результат которых сохраняется в ret -- это происходит ВНУТРИ критической секции, защищённой spinlock'ом.
      2. А в конце, уже ВНЕ критической секции под spinlock'ом, просто делается return ret.

    Никаких попыток компиляции пока не было, да и нечем (полная кросс-среда нужна, со всеми include-файлами).

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

    16.12.2019: продолжаем.

    • Сначала научились собирать драйверы -- модули ядра -- под BIVME2. Для этого просто взята директория от Антона Павленко (их с Шичковым модификация мамкинского), в ней заменяем vmei.c на свой, а дальше делаем
      make OS_BASE=/tmp/ppc860/linux
      (да, нужно дерево, распакованное из powerpc-sdk-linux-v4.tgz).

      Тестовая сборка шичковского драйвера показала, что его MD5-сумма совпадает с оной у ихнего /etc/bin/vmei.o в контроллере.

    • Затем с собранным своим драйвером (с буферизацией) пытаемся тестировать.

      Фиг -- не работает: гонит сплошные вектора 223.

    ...есть, конечно, надежда, что это МОЙ косяк. А если именно версия драйвера у меня (в которую я добавил буферизацию) сильно старая? Просить у Мамкина более свежую?

    17.12.2019: как дальше разбираться -- краткий сценарий:

    1. Попробовать поработать с шичковским драйвером. Если он даёт правильные вектора -- то точно косяк у меня, и точно можно всё запилить.
    2. Попробовать скомпилировать имеющийся vmei-cs.c от 13-09-2011.
      1. Сравнить размер и MD5 -- совпадёт ли с имеющимся в контроллере /lib/modules/2.4.22/vmei.o.
      2. Даже если и нет, то попробовать поработать с ним: опять же, если работать будет, то тогда уж 100% моя вина в векторе 223.
    3. И если предыдущие пункты не дадут результатов, то просить у Мамкина.

    Ну чо -- пробуем.

    1. Проба работы с шичковским драйвером: никаких IRQ не приходит.
    2. Сборка vmei-cs.c:
      1. Размер категорически отличается: 5532, а у контроллерного -- 4872...

        Попробовал к собранному применить ppc-linux-strip (а вдруг?) -- вообще 2724.

      2. Попытка использования -- вообще SIGSEGV. Мда...
    3. Вывод: раз первые пара пунктов результатов не дали, то надо всё-таки просить у Мамкина.

    Немного анализа для понимания ситуации:

    • Просмотр раздела "bivme2" в bigfile-0001.html показал, что переделки для корректной работы IRQ были там сделаны аж в ОКТЯБРЕ 2011-го (а 13-09-2011 -- это дата начала рпроверки работы vadc16_drv, так что, видимо, тогда я и получил мылом исходник драйвера (проверил -- да)). Вывод: тот исходник В ПРИНЦИПЕ реботать не будет.
    • Шичковский же драйвер -- за 24-05-2010, т.е., он основан на старой, не-годной для userspace-программ, версии мамкинского. Неудивительно, что от него не приходит никаких уведомлений о прерываниях -- это ровно то, как вёл себя мамкинский до сентября 2011-го включительно.

    Чуть позже: о -- нашёл!!! В мыле письмо от Мамкина за 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: возвращаемся к вопросу запинывания драйвера, способного и правильно обрабатывать прерывания, и буферизовать вектора.

    Для начала немного в сторону -- о сборке.

    • Есть мысль будущий результат поместить прямо в hw4cx/ -- в kernel_hw/bivme2/, где сейчас работа и идёт.
    • Но так, чтобы он собирался прямо при обычной сборке.
    • (А в будущем можно б и work/uspci/ переселить туда же.)
    • Откуда возник вопрос: а реально ли для сборки модуля нужно ВСЁ полное дерево из SDK, или достаточно небольшой выжимки?
    • Так вот: чтобы не возиться с поштучным копированием файлов (по мере возникновения ошибок) или с отсмотром изменения access-timestamp'ов после компиляции (что с нынешним relatime очень муторно) решил поступить проще: прогнать компиляцию с ключиками "-MM -MFvmei.dep" (да, пришлось определять несколько переменных окружения, предоставляемых Mekefile'ом, иначе ppc-linux-gcc ругался на неопределённость $CROSS_COMPILE).

      Итог -- фиг: .dep-файл получился в 179 строк -- т.е., 178 файлов из linux/kernel/include/.

    • Такую толпу тащить в обычный x-compile/ppc860-linux/ совершенно не хочется, так что пусть для возможности сборки модуля ядра всё-таки требуется полное дерево из SDK.

    Теперь о переделке модуля.

    • Основная концепция: поскольку там не вполне ясен смысл исправлений от октября 2011-го, превращающих неработающий драйвер в работающий, то постараемся изменения вносить минимально: даже поле vector трогать не будем, а всего лишь добавим буферизацию, плюс в vme_read() будем отдавать число из головы очереди (и ещё, наверное, в vme_poll() придётся переделать проверку...).
    • Кстати, первые результаты анализа diff'а между неработающим vmei-cs.c и работающим vadc/vmei.c:
      • В vme_interrupt() исправлений нет -- только DP(printk()) добавлено.
      • Зато управильнена пара read()/pol(): теперь после чтения делается vector = -2, а poll() считает признаком готовности состояние vector != -2.

        UPD: (через несколько минут) а, не -- poll()-то и раньше использовал такой признак, но сам же его чуть выше и выставлял (вот ЭТА алхимия совершенно непонятна: как она могла работать? похоже, что и не работала).

      • ...кстати, в vme_read() цепочка "чтение, затем сброс" НЕ защищена spinlock'ом.
    • Итак, модификации -- на основе new_vmei.c:
      • В vme_data_t добавлены поля (БЕЗ удаления 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...

    Ещё вчера понапридумал кучу сценариев для разбиралтельства -- как проверить, что же происходит с модулями, что они "умирают" (другие значения векторов, пробовать использовать модули в других наборах (не все, а поштучно, ...), ...).

    Но по результатам многочисленных проверок (полдня угрохал!) вырисовалось, что модули НЕ "умирают". А дело в том, что дальние от контроллера как будто "заслоняются" ближними к нему; как только ближние перестают вычитываться (опрашиваться скринами), то дальние очухиваются.

    • Поштучно -- работает любой.
    • В количестве 3 штук -- работают и последние 3.
    • Если запустить все 8, то при закрытии скрина от 1-го начинает работать первый неработавший, при закрытии скрина от 2-го -- следующий.

    Выглядит так, будто от дальних просто не доставляются до контроллера прерывания.

    Учитывая, что в модифицированном мною драйвере теперь 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: давно хочется сделать "векторное расширение" для libvmedirect.h. Всё равно все операции чтения/записи прямо в этом .h'е, всё там на вид вполне просто и понятно -- так чё б не сваять дополнительные функции, с циклами?

    21.01.2020@вечер-дома, ~20:00: пытаемся приступить.

    • Файл помещён рядом с libvmedirect.h в hw4cx/given/bivme2/, назван libvmedirect_vect_io.h (первоначально предполагалось обозвать libvmedirect_vect_exts.h, но нынешний вариант выглядит адекватнее).
    • Первоначальное, "нулевое" приближение -- скопировано из обычных функций и прототипы все переделаны:
      • К именам добавлено "_vect".
      • Параметры "данные" всегда передаются по указателю и имеют суффикс "s" (bytes, words, dwords).
      • Добавлен параметр count.
    • Первая потенциальная "проблема" -- там обычно объявляется по 4 переменных register int; а если добавить ещё (счётчик, указатель) -- то хватит ли?

      23.01.2020: судя по Википедии, у PowerPC аж 32 регистра, так что должно хватить.

    • Ну и надо как-то "корректно прерывать цикл" при детектировании VME-ошибки.
      • Сейчас-то у Мамкина сделано не вполне корректно: у него сброс ошибки и return -1 выполняются уже СНАРУЖИ критической секции.

        А надо сброс ошибки делать ВНУТРИ неё, чтобы ВСЯ операция обращения к VME была бы атомарной.

      • Ну и получается, что, похоже, будет ДВА экземпляра завершения критической секции: один -- внутри цикла, при ошибке; второй -- в конце функции, перед return 0.

      Не очень красиво, но терпимо: в противном случае -- чтоб был ОДИН экземпляр закрытия секции -- придётся иметь отдельную переменную ret, куда класть код для возврата и делать "goto END"; это выглядит ещё хуже, не говоря уж о меньшей оптимальности.

    • Другая проблема уже пореальнее и понеприятнее:
      • 16- и 24-битный режимы адресации работают "напрямую" -- все 64KB и 16MB их адресных пространств полностью отображаются в адресное пространство процессора.
      • А вот 32-битный режим -- уже через окна, 64 штуки страниц по 64MB.

        И перед собственно обращением производится настройка "окна".

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

      Какие тут есть соображения:

      • В наших ТЕКУЩИХ РЕАЛЬНЫХ использованиях проблема не возникнет в принципе: ВСЕ батрачиные (точнее, котовские) устройства имеют не только размер адресного пространства сильно меньше 64MB, но ещё и базовые адреса у них всегда имеют вид 0xNN000000 -- т.е., они кратны 16MB (и в "окно" их влезет по 4 штуки).

        Т.е., совсем нулёвый вариант (для тестов скорости) можно сделать и вовсе простодушно-кривым -- в предположении, что векторные операции никогда не пересекут границ страниц.

      • А делать "по-правильному" -- очевидно, примерно «в стиле "flush"» (идеологически аналогично XhPlot'овской отрисовке, fastadc'шному чтению и т.п.).

        Т.е.,

        • Всё -- начиная с вычисления значений overlay и offset -- поместить в дополнительный "внешний" цикл.
        • Перед началом "внутреннего" цикла (собственно доступа) ограничивать количество повторов так, чтобы оно в максимуме доходило бы до конца страницы.
        • Условие окончания "внешнего" цикла -- что счётчик "сколько осталось" стал ==0 (и после каждого "внутреннего" цикла уменьшать его на количество выполненных операций).

      Вот...

    28.01.2020: мысль -- а можно ли вместо цикла с по-элементным копированием использовать сразу memcpy()?

    29.01.2020: Павленко с Котовым при помощи осциллографа посмотрели на работу BIVME2 с ADC250.

    • И, в частности, на вычитывание блока памяти: там получилось что-то вроде 5 микросекунд на слово. Сколько из них собственно работа VME-чипа, а сколько процессора (накладные расходы) -- хбз.
    • В режиме ядра у Шичкова получалось что-то в районе 3 микросекунд на слово.

      Так что неясно, насколько реально можно ускорить вычитывание -- возможно, всего на 40%.

    Договорились, что я сделаю векторное чтение и они придут с осциллографом ещё разок.

    02.02.2020@дома: за вчера-сегодня сделано. Вкратце:

    • adc250'шный ReadMeasurements() переведён на векторное чтение в 1 операцию с последующей перестановкой чётных-нечётных. В #if/#else/#endif, чтобы можно было быстро переключаться между вариантами.
    • Cобственно libvmedirect_vect_io.h:
      • Идеологические решения:
        1. a32-варианты сделаны ограниченные -- БЕЗ надлежащего цикла по страницам. Т.е., работать корректно будут только в случае, если вся операция помещается на 1 страницу.

          Страницы по 64МБ (26 бит, 6 бит селектора страниц), а ADC250 имеют ограничение сверху в 16МБ (24 бита, 8 бит селектора устройства), а адресное пространство и вовсе 11.8МБ (0xB8000 байт -- последний адрес 0xB7FFFF).

          Т.е., конкретно для ADC250 это ограничение проблемой не станет -- их адресное пространство не только меньше, но и помещается в одну страницу оверлея целиком, в силу совпадающей кратности.

        2. Статус доступа проверяем не при каждой операции вектора, а один раз в конце.
      • Байтовые операции -- libvme_{read,write}_aNN_byte_vect() -- делаются при помощи memcpy().
      • 16- и 32-битовые же -- -- libvmedirect_*_{word,dword}_vect() делаются циклами, с явным чтением/записью по указателям соответствующих размерностей.

        Не рискнул использовать memcpy(), поскольку неясно, как оно работает -- возможно, что там нет оптимизаций при кратности 2 и 4, а всегда только байтами.

    • В bivme2_hal.h добавлена, в #if/#else/#endif, вторая альтернатива реализации v-операций -- через вызовы libvme_*_vect().

    03.02.2020@утро, после планёрки: проверяем.

    Первоначальный вариант проверку не прошёл. Точнее, ОБА первоначальных варианта: с "умеренной" векторностью (цикл в HAL'е) и с "полной" (цикл в библиотеке).

    Было 2 раздельных дурацких косяка (хотя и идеологически похожих):

    1. Вариант "векторные операции в HAL'е" не пахал потому, что в bivme2_hal.h в векторных операциях было data += sizeof(uint##TS) вместо data++.
    2. Вариант "векторные операции в libvmedirect_vect_io.h" не пахал потому, что во всех функциях было *dst = *src вместо *dst++ = *src++

    После исправления -- заработало!!!

    Собрана статистика, в следующих условиях: оставлен работать только один модуль adc250_94 (остальным сделано _devstate=-1), у него NUMPTS=41796, внешний запуск с частотой 10Гц (вот это -- зря, надо было внутренний ещё проверить):

    • Старый, скалярный "по-ячеечный" вариант -- 1.4fps.
    • Вариант с векторизацией в HAL -- 2.0fps.
    • Вариант с векторизацией в libvmedirect -- 2.4-2.6fps.

    Налицо существенный выигрыш.

    Причём это было простое линейное ускорение для ОДНОГО модуля. В ситуации же с 8 модулями, работающими от одного внешнего запуска (и с разным количеством точек), получилось ещё интереснее:

    • При старом варианте они все ползли в районе 2-2.5fps, у ближних к контроллеру модулей больше, чем у дальних.
    • Новый же вариант -- первые 3 модуля почти 10fps, 4-й около 5.2, остальные по 5.

    Т.е., очевидно, ближние успевают поймать прерывание, считать и отправить данные и получить запрос на следующее измерение ДО следующего внешнего запуска, а дальние уже не успевают, и пропускают каждый 2-й запуск.

    Загрузка процессора BIVME2 при этом под 100%, а не ~84%, как раньше.

  • 29.03.2021: любопытный опыт при "рестарте"-замене VME-сервера на bivme2-rfmeas.

    Симптомы:

    • захожу telnet'ом на крейт, делаю "kill 68" для убиения штатно-запущенного сервера, затем обычной командой запускаю другой экземпляр (с дополнительной отладочной печатью), он начинает принимать соединения, получает первую команду инициализации драйвера, и...
    • всё -- зависает!
    • И telnet к нему на 8003 как бы коннектится, но никакой выдачи нет.
    • Делаю Ctrl+C и повторяю -- один чёрт!
    • Думал было, что дело в неуказанных параметрах PREFIX и SUFFIX; указал -- пофиг.
    • Запускаю вместо модифицированного отладочно-печатающего варианта штатный -- а то же самое!!!

    Через некоторое время до меня дошло: конкретно этот сервер bivme2-rfmeas -- ОЧЕНЬ ЗАГРУЖЕННЫЙ! Он пытается выполнять работы больше, чем успевает процессор, поэтому очень большую (или просто бОльшую?) часть времени проводит в векторном чтении VME-шины, которая происходит в состоянии с залоченным семафором __libvme_shm_ptr[0]. В результате при нажатии Ctrl+C сервер завершается с ЗАЛОЧЕННЫМ же состоянием семафора, и при последующих запусках других экземпляров они висят в ожидании разлочки, которой никогда не произойдёт.

    Поэтому был использован следующий подход:

    1. Тормозим все драйверы, работающие с этим крейтом, записью им _devstate=-1.
    2. Подменяем VME-сервер, делая kill старому (а перед этим убедившись командой list на management-порте, что он пуст) и запустив новый.
    3. Включаем драйверы обратно, записью им _devstate=+1.

    Вуаля -- этот вариант работает!

a3818:
  • 15.02.2019: это раздел для обсуждения HAL-модуля реализующего доступ через CAEN'овское железо.

    То ли напрямую через драйвер, то ли через CAENVMElib -- пока неясно.

    21.02.2019: сей момент выглядит, что придётся

  • 21.02.2019: надо бы принять решение, КАК всё-таки работать: через драйвер или через CAENVMElib?

    21.02.2019: сей момент выглядит, что придётся через библиотеку -- просто потому, что драйверу она поставляет уже готовые "описатели для выполнения В/В", упакованные в struct'ы.

    С другой стороны, API этой библиотеки несколько тошнотен, так что тянет всё же работать напрямую.

    09.01.2020: этот вопрос был решён уже с неделю-другую назад, в пользу варианта "через библиотеку".

    Так что раздел помечаем как "done".

  • 09.01.2020: изготовление собственно a3818_hal.h было начато ещё позавчера.

    15.01.2020@утро-дома-завтрак: некоторые рассуждения о ловле прерываний.

    • Этот аспект в CAENVMElib'е продуман из рук вон плохо: асинхронная ловля отсутствует, а есть лишь поллинг, в 2 вариантах:
      1. CAENVME_IRQCheck() -- проверить текущее состояние, ничего не ждя.
      2. CAENVME_IRQWait() -- ждать в течение стольки-то миллисекунд, не появится ли что-нибудь.

      Хотя в конечном итоге оно общается с драйвером через файловый дескриптор, но никакого API для добычи этого дескриптора у библиотеки (чтобы поместить в свой набор слушаемых, отдав, например, cxscheduler'у) нету.

      Планируется попробовать запинать CAEN'овцев, чтобы они это сделали, но пока предполагалось следующее решение.

    • Сделать pipe(), затем от-fork()'оваться в отдельный процесс, в котором перехватить все сигналы и висеть в цикле по CAENVME_IRQWait(), а при получении отправлять в pipe пару байт {IRQ_N,IRQ_VECT}.

      Таким образом, основной процесс будет получать информацию из читающей стороны pipe'а, которую сможет затолкать в список слушаемых -- примерно такой механизм используется для ловли LAM'ов в CM5307.

    • Но -- суть данной записи -- тут есть проблема: получится, что к одном и тому же "handle" одновременно обращаются ДВА процесса -- один пытается ждать прерывания, а второй занимается обычным В/В.

      Очень далеко не факт, что это будет работать, с итальянским-то раздолбайским кодом...

    И что делать -- окружать каждый чих (каждую операцию В/В) mutex'ом? Причём:

    1. Он должен быть через разделяемую память.
    2. А если процесс-ждатель висит на ожидании (что он будет делать ВСЁ ВРЕМЯ!), то 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@дома, воскресенье: за вчера и сегодня добит начальный вариант:

    • Добавлена поддержка векторных операций.
    • Сделан Makefile -- традиционно пришлось комбинировать из нескольких примеров; в этот раз поучаствовали socketcan/Makefile и frgn4cx/epics/FrgnRules.mk.
      • Определения пока внутри, но подготовлены к вытаскиванию в отдельный CAENVMElibDefs.mk.
      • Предусмотрена возможности сборки как при установленной в систему CAENVMElib, так и при распакованной в отдельную директорию. Для этого предусмотрен символ CAENVMElib_INSTALLED,
        1. который если сделать =YES, то считается, что уже всё есть в системе и нужно сделать только "-lCAENVME",
        2. а иначе берётся всё из некоей директории (в которую должно быть всё распаковано).

          На неё указывает 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 делает там же, причём ещё и абсолютный, с именем директории). Надо ТОЛЬКО ВРУЧНУЮ.

      • Поскольку у CAEN'овцев библиотеки для x86 и x64 лежат раздельно по директориям (а иначе фиг сделаешь), то явно прописано использование x64.
    • У раздолбаев-итальянцев почему-то системой по умолчанию считается WIN32 (хотя иногда -- не совсем: в CAENVMElib.h стоит #ifdef WIN32).

      Поэтому пришлось добавить

      #define LINUX
      перед
      #include "CAENVMElib.h"
    • Затем исправлено некоторое количество опечаток...
    • ...и оно стало собираться!

    Да, оно пока без поддержки прерываний, но уже надо проверять.

    20.01.2020: пытаемся проверять a3818_test -- пока чисто "запустится или нет".

    • Касательно системы сборки: поскольку сейчас при сборке с "развёрнутой в конкретную директорию библиотекой" явно указывается /tmp/CAENVMELib-2.50/lib/x64/libCAENVME.so.2.50, то оно её и пытается брать.

      Непорядок -- надо бы как-то сделать, чтобы указывались отдельно -lДИРЕКТОРИЯ и отдельно имя файла (да, пусть и полное).

      21.01.2020: да, разобрался, исправил. Некоторые детали:
      • Были давние воспоминания, что при "-lNAME" линкер ищет файлы последовательно по некоему списку, приставляя к NAME префиксы и суффиксы. И вроде как я где-то в документации видел этот список.

        Но нет -- не только в RHEL-7.3 этого нет ни в man, ни в info по gcc и ld, но и в RH-7.3 и даже RH-4.2 тоже нету. ...или то я читал ещё в Slackware на damp?

      • Зато в описании ld сказано (это цитата из info):
        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 -- она уже есть.

    • Далее, запуск:
      • На произвольной машине (x10sae) -- ошибка открытия шины.
      • На машине с A3818 -- аналогично, но уже потому, что на /dev/a3818_0 (к которому оно лезет) стоят права rw----- при владельце root.root.
      • При запуске из-под root -- успешно запускается.
      • И при попытке исполнить команду говорит "VME error".
      • Кстати, в зависимости от bus_major/bus_minor -- точнее, Link,BdNum -- оно лезет к /dev/a3818_N, где N=Link*8+BdNum.
        • Как было уже ранее замечено, имеющаяся единственная плата с 1 линком даёт в /dev/ файлы a3818_0...a3818_7.
        • Второй линк, очевидно, отразится на minor'ы 8...15.
        • Вывод: BdNum может быть от 0 до 7.
        • (следующие данные получены после обеда, после внедрения возможности указывать старшими цифрами в bus_major'е тип адаптера; подсматривалось посредством "ltrace -fSs200")
        • BdType=cvV1718 (10001/7): лезет к /dev/usb/v1718_7, т.е., значение Link не влияет (впрочем, это и в CAENVMElib.h в комментарии к CAENVME_Init() написано).
        • BdType=cvV2718 (11001/7): сначала лезет к /dev/a2818_23, потом (после облома) к /dev/a3818_15. Загадка...

          Доп.тест:

          • 11000/7 -- /dev/a2818_7 и /dev/a3818_7, а между ними -- access(/proc/a2818)=-2/ENOENT;
          • 11002/7 -- /dev/a2818_39 и /dev/a3818_23, и тоже с access()'ом.

          a2818_N, где N=Link*16+BdNum, BdNum от 0 до 15?

        • BdType=cvA2818 (12001/7): /dev/a2818_23 -- тоже Link*16+BdNum.
        • BdType=cvA2719 (13001/7): /dev/a2818_23 и /dev/a3818_15, БЕЗ промежуточного access().
    • Выводы насчёт диагностики:
      1. Надо хотя бы пытаться транслировать коды cvNNN в обычные Ennn.
      2. Также конкретно для a3818_hal.h явно будет осмысленным использование CAENVME_DecodeError().

        Как? Напрашивается 2 варианта:

        1. Неким #define-символом включать выдачу на stderr прямо hal'ом.
        2. Добавить в HAL-API функцию вроде "вернуть по коду ошибки её описание".

        Второй вариант выглядит лучше.

      После обеда: да, реализован вариант II.2 -- см. в разделе по "vme_hal".

    20.01.2020@дорога домой на обед, около мыши ~14:00: можно очень легко добавить поддержку других адаптеров, работающих через тот же драйвер -- A2818 и т.д.: учитывая, что у нас BUS_MAJOR теперь НЕ является индексом в таблице, а просто записывается в "свойства шины", то можно указывать тип устройства в "тысячах": например, 10000-10999 -- V1718, 11000-11999 -- V2718, и т.д.

    20.01.2020: не откладываем в долгий ящик -- делаем!

    • Используется формула bus_major=10000+BdType*1000+Link.
    • Выбран такой вариант, а не 1000*BdType+Link, для того, чтобы иметь возможность указывать BdType=cvV1718, который =0.
    • Оно работает, и даже получены результаты "как оно себя ведёт при указании других вариантов BdType" -- они выше, в результатах запуска.

    22.01.2020: контроллер воткнут (вместо bivme2-rfmeas=192.168.130.76, утащенного в клистронку), попробовано выполнить первый реальный тест с реальными железом.

    Фиг -- IO() даёт ошибку "Invalid parameter" (-4) сразу же, судя по ltrace, даже без попыток обратиться к драйверу в ядре.

    OK -- надо:

    1. Попробовать собрать прилагающуюся тестовую программку и поработать ею, а можно и её под ltrace прогнать.
    2. Внимательно почитать код -- не накосячил ли где...

    23.01.2020@утро-дома: а может, "invalid parameter" касается не handle'а (как кажется на первый взгляд), а каких-то ДРУГИХ параметров -- например, Data Width?

    23.01.2020: разбираемся.

    • Попробовал прилагающейся тестовой программкой -- CAENVMEDemo: читает! Причём в её скрине даже негде указать AddressModifier, но всё равно читает!

      Порылся в её исходниках -- она как-то сама его выставляет.

    • Далее всё тестировалось с привлечением "ltrace -fSs200" -- как a3818_test, так и CAENVMEDemo. И смутило, что 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 корректно показывает ВСЕ параметры! Причём адрес на шине -- шестнадцатиричкой.
    • Дальше была гипотеза "а не требуется ли _swapped-вариант"?

      Нет, НЕ требуется -- в исходник той тестовой программки засунут вывод на stderr, и там используется обычное cvD32=0x04, а НЕ cvD32_swapped=0x14.

    • Затем была гипотеза "а не проблема ли в плохой поддержке 64-битности CAEN'овским драйвером в ядре?". Дело в том, что, судя по отладочной печати, в CAENVMEDemo адреса буферов для данных расположены в пределах 32 бит (видимо, статические переменные (позже: ага, static man_par_t man, и поле внутри неё)), а у нас они в стеке и имеют вид 0x7ffd12b11cf0.

      OK -- временно переделал vme_hal_a##AS##rd##TS(), вставив туда чтение в статический буферок с последующим копированием куда просят.

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

    • Оказалось: библиотека НЕ РАБОТАЕТ с кодом типа бриджа cvA3818!

      Обнаружилось это, когда я просмотрел ВЕСЬ трассировоыный вывод от 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, и если в полученной маске прерываний есть битик для этого уровня, то

      1. делаем "цикл подтверждения прерывания" при помощи CAENVME_IACKCycle(),
      2. (03.02.2020) а затем вызываем зарегистрированный callback.
    • Чтобы помнить, какие уровни прерываний заказаны клиентом HAL'а, введено поле a3818_hal_bus_info_t.irq_mask -- это битовая маска, где каждому уровню level соответствует битик 1<<level. Если он взведён -- значит, этот уровень мониторируется.

      И при проверке "есть ли в полученной маске прерываний битик для этого уровня" сначала делается AND с маской мониторируемого -- чтобы не обращать внимания на не интересующее.

    • ...а вот заполнение/очистка a3818_hal_bus_info_t.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: тестируем.

    • Для начала была мелочь -- драйвер local/adc250_drv.so линковался без LIBPZFRAME_DRV, и потому не грузился. Ну добавлено в тамошний Makefile.
    • Дальше -- традиционная проблема с правами rw----- на /dev/a3818_0. Сделал chmod 666.
    • Потом -- при обломах ругается "vme_disconnect: request to disconnect unknown devid=3".

      Это понятно: оно СНАЧАЛА пытается открыть шину и сделать хоть что-то, а лишь потом реально регистрирует устройство в таблице. А сервер при обломе в любом случае вызывает disconnect().

      Пока придётся с этим жить и забить.

    • Дальше -- проблема "vme_hal_open_irq(IRQ5): No such file or directory".

      То был тупой косяк -- в конце 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-й ячейке).

    • (после обеда) провёл опрос коллег.
      • Спросил у Бобровникова -- у него тоже CAEN'овские адаптер+контроллер и CAEN'овские же VME-устройства. Так вот: у него BLT-операции работают.
      • Также на ВЭПП-4 у Ивана Николаева студент/аспирант Вячеслав Каминский тоже работает через такой контроллер (со своим модулем), и у него -- в CAENVMEDemo -- тоже всё работает.

      Откуда подозрение: а не в ADC250 ли дело?

      После чего позвонил уже Павленко и Котову.

      • Максимальная полученная на MVME3100 (в DMA-режиме) скорость -- 4MB/sec, это определяется уже ADC250, а не контроллером.
      • Есть подозрение, что BLT в ADC250 просто не реализован.

        Хотя, ВРОДЕ БЫ, должен он у них работать -- Антону помнилось, что вроде делали.

      • Кроме того, для BLT-операций нужен ДРУГОЙ address-modifier: 0x0B вместо 0x09.
      • Проверил -- CAENVMEDemoVme.c::CaenVmeReadBlt() должным образом производит подмену, вместо cvA32_U_DATA=0x09 используя cvA32_U_BLT=0x0B.

        Также был проведён тест с принудительной передачей 0x0B в vme_hal_a##AS##rd##TS##v() -- и тоже не помогло.

      • И через некоторое время отзвонился Антон Павленко: да, действительно в ADC250 BLT-режим НЕ работает -- они провели тест.

    Резюме:

    1. В a3618_hal.h всё сделано более-менее правильно.

      ...хотя надлежащую подмену Address Modifier'а надо добавить.

    2. БЫСТРО Павленко+Котов ADC250 правильно работать с BLT не научат, увы.
    3. Надо попробовать найти в CAENVMElib какой-то режим/функцию, чтобы выполняла "векторное чтение" не в BLT-режиме -- т.е., чтобы просто пересылала по линку команду "произведи столько-то одиночных чтений, а потом прищли результат одним блоком".

    05.02.2020: доделки по мелочи:

    • Попытка CAENVME_IACKCycle() теперь выполняется не однократно, а в цикле по repcount до 100 раз.

      Это оно так потому, что у нас же делается периодический поллинг, и если выполнять ACK один раз, то будет отрабатываться лишь первый (ближний к контроллеру) из возможно многих. А так -- отрабатываются все.

      Немножко результатов испытаний:

      • "Все" теперь отрабатываются даже с избытком: как-то получилось аж 12fps на ДВУХ модулях сразу (и это при частоте поллинга 10Гц!).
        • Для этого пришлось одному из них поставить 610 точек вместо 4096.
        • И прикол в том, что он смену количества точек отрабатывает ОЧЕНЬ -- не сразу с задержкой секунд в 5 или больше.

        Видимо, оба "спецэффекта" происходят из-за того, что модули стоят на внутреннем запуске -- т.е., молотят максимально быстро (с точностью до времени на передачу данных серверу и повторного запроса; но тут всё локально (а не через сеть), так что по факту всё мгновенно -- повторный запрос, измерение и IRQ успевают произойти ещё внутри цикла по repcount).

        И ещё немножко "анализа":

        1. Первый-то результат легко объясним: пока идёт обработка следующего модуля, этот успевает произвести измерение и выставить прерывание.

          Убирание второго модуля (стоп клиента) тут же сбрасывает скорость первого до 10fps.

        2. А вот со вторым механизм неясен.

          Но при убирании второго клиента этот "спецэффект" тоже исчезает.

          Возникала мысль, что это просто сам GUI-клиент adc250 так тормозит -- но нет, он занимает минимум CPU, как и X.

      • А без этого цикла -- да, проверено, всегда "молотил" только ближний к контроллеру, "дальний" же стоял; оно и понятно -- до него ACK никогда не доходил, всегда "ближний" успевал влезть вперёд.
    • В 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МБ/сек.

  • 31.01.2020: потенциальные эксперименты с A3818:
    1. Кабель от оптического Ethernet; 100м?
    2. 2 карты в 1 компе.
    3. 2 крейта на 1 линии.
    4. "Резервирование" -- 2 карты на 1 линии: возможно ли?
  • 31.01.2020@научная-сессия-ИЯФ-Логашенко, ~10:30: можно сделать и v4a3818vmeserver, прямо в a3818/ -- на динамической загрузке драйверов, в качестве которых подойдут обычные драйверы из 4pult/lib/server/drivers/.

    (Это чтоб ADC250 в крейте в клистронке при подключении через A3818 -- который встанет в некий комп -- были бы видны через cxhw:15).

  • 07.02.2020: надо б научиться ловить событие "крейт включился". Если такое вообще возможно -- надо общаться с CAEN'овцами.

    10.02.2020@утро-дома: а можно проверять выключение/включение крейта периодическим поллингом регистров контроллера, но это бяка -- даже race condition возможен (когда драйвер УЖЕ выполнил успешно какую-то VME-операцию, и тут ему задним числом приходит уведомление о включении).

  • 10.02.2020: к вопросу о "надёжности" CAENVMElib'а: сегодня случилась неприятная ситуация.

    В какой-то момент что-то сбрендило и сервер начал подвисать и падать; 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: надо бы попробовать сделать поллинг прерываний отдельным thread'ом -- открыв ту же шину 2-й раз.

    17.02.2020: приступаем.

    • Вариант на thread'ах делаем не заменой поллингового, а в #if/#else/#endif. Управляющим символом работает USE_THREAD_FOR_IRQ, принимающий значения 1 (новый вариант) или 0 (поллинг).
    • 18.02.2020: также добавлена поддержка указания варианта сборки через параметры make'а: при указании make-символа USE_THREAD_FOR_IRQ=1 в Makefile делается
      A3818_HAL_DEFINES=	-DUSE_THREAD_FOR_IRQ=1
      A3818_HAL_LDFLAGS=	-lpthread
      
      и ссылки на эти определения добавлены в соответствующие _DEFINES и _LDFLAGS.

    18.02.2020: доделываем. Для удобства чтения опишем вчерашние и сегодняшние деяния единым списком, чтобы получилась целостная картина.

    • Работа с pipe'ом подсматривалась в pxi6363_drv.c.
    • В a3818_hal_bus_info_t добавлены поля:
      • CAENVMElib_IRQ_handle -- тот самый "2-й handle".
      • irq_pipe[2] и irq_rfdh -- очевидно.
      • irq_thread -- идентификатор thread'а.
    • vme_hal_open_bus():
      • Последовательно делается:
        1. Открытие 2-го handle'а.
        2. Создание pipe'а.
        3. Создания дополнительного thread'а.

        Замечания:

        1. У pipe'а только ЧИТАЮЩЕМУ концу делается O_NONBLOCK:=1, а пишущему -- явным образом :=0.

          Смысл -- чтобы натуральным образом ограничивать частоту подтверждения прерываний: если основная часть драйвера (вычитывающая сообщения из pipe'а) не успевает всё обрабатывать, то pipe переполнится и пишущая сторона (которая, собственно, и занимается ожиданием прерываний) просто заблокируется на записи до тех пор, пока читающая не подразгребётся.

        2. Поскольку созидательных действий довольно много (а создание pipe'а 2-стадийное), и обломиться может любое, то выполнение подчистки отдельно после каждого облома было бы шибко громоздко.

          Поэтому в конце сделана секция с меткой ERREXIT, куда выполняется goto при обломах.

          А уж там оно сохраняет errno, выполняет подчистку всего, восстанавливает errno, возвращает -1.

          Для этого, естественно, значения полей, ссылающихся на потенциально-высвобождаемые ресурсы, заранее инициализируются в -1.

        3. ...но есть нюанс: поскольку для CAENVMElib'овских handle'ов НЕ задекларирован диапазон разрешённых значений, то и кода "невозможное/неинициализированное значение" там нет, и не факт, что обычную для прочих handle'ов -1 можно использовать.

          Поэтому выполняемое первым шагом CAENVME_Init() снабжено своим отдельным кодом подчистки.

      • Ну и ко всему прочему, "полные" обращения к a3818_hal_bus_info[bus_handle] заменены на короткие через me -- а то было шибко уж длинно и плохочитаемо.
    • В vme_hal_close_bus() добавлена симметричная подчистка:
      1. Корректное (как мне представляется) завершение дополнительного thread'а: сначала pthread_cancel() (убиение) а затем pthread_join() (ожидание завершения).

        В самом том thread'е в самом начале делается

        pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,       NULL);
        pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
        
        для разрешения немедленного завершения.
      2. Прикрытие pipe'а.
      3. CAENVME_End(me->CAENVMElib_IRQ_handle);
    • Тело дополнительного thread'а -- irq_wait_proc().
      • Там бесконечный цикл, в котором выполняется CAENVME_IRQWait() с маской 0x7F и таймаутом 3600*1000, т.е., 1 час.

        Описание на эту функцию, мягко говоря, куцее, поэтому делалось всё по наитию (в т.ч. значение маски).

      • А далее скопирован кусок из a3818_heartbeat_proc(), где делается CAENVME_IRQCheck(), и затем при непустой маске делается цикл по всем level'ам и для каждого, чей бит в маске горит, выполняется цикл по repcount с CAENVME_IACKCycle() и "отдачей" прерывания.
      • Только в качестве отдачи -- не вызов, а отправка информации в pipe.
      • Протокол передачи данных прост: шлются 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).

    • Абыдна!!! Столько времени потратил, чтобы сделать корректную реализацию через дополнительный thread, а всё коту под хвост...
    • Ну раз уж работа выполнена, то удалять не будем, а оставим тот multithreaded-код в #if'ах.

      Как-никак -- типа модельная реализация для таких случаев (хотя и непроверенная).

    А для удовлетворения потребностей ВЭПП-5, где нужна частота срабатывания 10Гц, делаем A3818_HEARTBEAT_USECS=50*1000, т.е., 50мс, что соответствует 20Гц -- это должно решить задачу.

    18.02.2020: вечером -- дополнительно попробовал вызвать CAENVME_IRQEnable() с маской 0x7F (типа "разрешить" ВСЕ.

    Эффект -- нулевой.

    Попробовал также добавить этот же вызов в a3818_heartbeat_proc(), в точки сразу после сначала CAENVME_IRQCheck(), а затем и CAENVME_IACKCycle() -- эффект аналогично нулевой.

  • 28.10.2020: пришло (ЧеблоПаше) письмо от CAEN'овцев (касательно Ticket#1042128), что они, вероятно, позволят нам, под NDA, добавить в CAENVMElib и драйвер нужную функциональность для поддержки select().

    Посему авансом приступаем к добавлению в 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: доделана и собираемость:

    1. Makefile -- при USE320=YES (надо указывать из командной строки) он меняет значения CAENVMElib_* так, чтобы использовать эту новую самособранную библиотеку.
    2. a3818_hal.h -- поскольку в новой версии CAENVMElib.h теперь выставляется CAENVMELIB_SUPPORTS_POLL, то именно по нему включается USE_SELECT_FOR_IRQ.
  • 25.11.2020: отдельный раздельчик для описания хода модификации CAEN'овских исходников -- CAENVMElib.c и a3818.c.

    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() подпеределана:

    1. Теперь она возвращает маску активных прерываний не в результате, а через отдельный uint32_t *Mask.

      Оно (пока?) именно uint32_t, а не CAEN_BYTE, чтобы более старшими битами мочь возвращать и иные события (вроде законченности операции CAENVME_BLTReadAsync().

      27.11.2020: по, похоже, лучше бы всё-таки маску сделать CAEN_BYTE -- для унификации с CAENVME_IRQCheck(), чтобы никому не удалось по ошибке подсунуть указатель на 8-битный байт вместо 32-битного слова.

    2. Добавлен параметр reserved -- на случай, если захочется мочь указывать маску/набор интересующих событий для проверки (IRQ, Async, ...).

    27.11.2020@утро-душ: всё не даёт покоя идеологическая проблема -- ну КАК всё-таки добывать из ядрового драйвера маску "горящих" прерываний, чтобы это работало как в USPCI, где есть операция FGT_IRQ?

    • Напрашивается идея вот ровно так же и сделать: ввести ещё один IOCTL_-код (всё равно придётся), и чтоб он возвращал текущее известное значение и нулил бы его.
    • А в качестве оного "текущего состояния" использовать прямо поле 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'а и осторожных продвижений, модификации -- И в библиотеку, И в драйвер -- доделаны.

    Есть пара нюансов организационного характера:

    1. Я всё делал в драйвер "A3818Drv-1.6.2-build20181214.tgz" (с которым и имел дело ещё с конца 2019-го), а сейчас есть уже более свежий "A3818Drv-1.6.3-build20200203.tgz", так что надо бы портировать в него.
    2. Файл a3818.h, имеющийся в исходниках как библиотеки, так и драйвера, должен бы быть одинаковым. Но он РАЗНЫЙ -- даже списки IOCTL'ов различаются, причём в библиотековом есть такие, которых в драйверовом и нету.

    Надо проверять.

    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.

    1. В библиотеке по Enable выполняется
      _AX818WriteREG(dev, IOCTL_CLR, A2818_VINTDIS)
      и затем запись в A2818_IRQMASK0_SET или A2818_IRQMASK1_SET (в зависимости от board<4 или нет) указанной маски, сдвинутой должным образом.

      А по Disable -- только IOCTL_SET, A2818_VINTDIS.

    2. В драйвере же -- ничего конкретного нет (поскольку из библиотеки всё делается записью регистров).

      Но есть свои обращения к регистрам 0xA0 и 0xA4, с обозначениями которых среди драйверов и библиотеки разнобой -- иногда они именуются A3818_IOCTL_S и A3818_IOCTL_С (a3818.h), а иногда IOCTL_SET и IOCTL_CLR (a2818.h). И сводится их работа, видимо, к взведению и сбросу регистра IOCTL=0x08.

    Теперь пробуем вставить.

    • В caenvmelib_poll_test добавлено, ПЕРЕД select()'ом.

      Аллилуйя -- теперь прерывания ловятся при КАЖДОМ запуске!!!

    • Добавляем и в a3818_hal.h. Для начала в vme_hal_open_bus() -- точнее, оно там уже было, за-#if0'енное, теперь открыто.

      Не помогло, ловит только первое.

    • Добавляем также и в irq_fd_p(), в самое начало -- ведь были же где-то упоминания, что Enable нужно выполнять постоянно, ПЕРЕД каждым ожидаемым IRQ (ну или после каждого очередного).

      Неа -- первое ловит, а дальше уже нет.

    • ОК, добавляем также и в самый конец.

      Да -- помогло!!! Ловятся все!!!

      Вот почему, интересно? Или что нужно разрешать IRQ уже после IACKCycle, или просто через какое-то время?

    Мда, типа "удача". Но насколько это надёжно?

    • Так-то смущает ситуация: имеем явный race condition, так что оно может молотить-молотить, а потом в один прекрасный момент бац -- и перестанет...
    • Ну и чо, считаем весь проект не очень удачным и нужно всё-таки периодически выполнять IRQEnable просто так (раз в секунду?)?

      Кривизна какая...

    • А может, проблема всё-таки в модели -- почему IRQ вообще запрещаются?

      Может, этот запрет выполняет ДРАЙВЕР (записью в тот самый регистр VINTDIS), и нужно это уметь отключать? Например, сделать ещё один IOCTL, которому указывать "отключать ли автозапрет IRQ", и флаг "отключать" по умолчанию "да", а этот IOCTL позволял бы сделать "нет", тем самым указывая, что программа берёт на себя ответственность.

    02.01.2021: продолжаем анализ, чтением кода драйвера.

    • Прерывания ЗАПРЕЩАЮТСЯ в обработчике, конкретно пришедшие (по маске).

      И смысл ясен: чтобы пока VME'шное IRQ не будет обработано и сброшено, оно бы больше не торчало и не вызывало бы бесконечные локальные прерывания.

    • Если немножко вдуматься, то это как раз очевиднейший и неизбежный вариант -- иначе никак.

      В BIVME2'шном vmei.c ведь по сути делается аналогично -- сразу же в обработчике вычитывается вектор, что автоматом сбрасывает запрос.

      Тут ровно такое же сделать не получится -- т.к. операция "IACKCycle" совсем не атомарна, а предполагает отправку пакета крейт-контроллеру и получение от него ответа.

    • Откуда вытекает очевидный сценарий работы: выполнять IRQEnable нужно в самом начале, при открытии платы, а также в КОНЦЕ irq_fd_p().
    • Что же касается потенциального race condition, то его НЕТ (ладно, "не должно бы быть" :-)):
      • Предположение о его существовании основывалось на мысли, что существуют некие "события" доставки IRQ, могущие быть пропущены, если таковые события произойдут во время замаскированности.
      • Но тут мы имеем дело не с "событием" (фронтом), а с "уровнем" -- если уж прерывание зажглось, то оно так и будет светиться, пока не будет явно сброшено (чтением вектора), и при размаскировании немедленно вызовет локальное аппаратное IRQ.

        (Как раз "события" были бы удобнее -- это и есть то, что нам бы и получать из файлового дескриптора, как в конечном итоге и сделано в vmei.c; и PCIe'шные message-signalled interrupts (MSI) тоже в эту же степь...)

    Ну что ж -- всё понятно, надо просто сделать и запустить тест.

    03.01.2021: сделал:

    1. Вначале Enable'ится в vme_hal_open_bus() (при USE_SELECT_FOR_IRQ.
    2. И в конце 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 >ФАЙЛ" -- каждый раз "с нуля", заменяя предыдущее содержимое.

      Файл, естественно, у каждого чтения свой.

    • А процесс отображения заключается в том, что в цикле делается:
      1. чтение поштучно из всех файлов,
      2. потом печать -- командой printf, НЕ переводящей строку -- '\r' и считанного из тех файлов;
      3. ...учитывая, что объём выдачи от каждого чтения может разниться, выдавать надо форматом вроде "%65s" -- чтоб занимало постоянную ширину.
      4. sleep 1

      Т.е., раз в секунду печатается текущее состояние.

    Плюсы в том, что

    • Каждый мониторинг фигачит в файл и потому никак не ограничивается коннектом с домом.
    • Обновление идёт равномерно, раз в секунду -- так что объём небольшой и линк не напрягается.
    • Иногда будет попадать -- да, race condition -- что из файлов будет считываться пустота: когда перед очередным echo файл уже создался и опустошился, но содержимое туда ещё не попало.

      Но за счёт вывода фиксированной ширины это будут одноразовые эксцессы и ничего особо не поедет.

    06.01.2021@вечер, 23:50...00:15: сделал. Запускаю, буду смотреть.

    ...какие-то подвисоны. Рестартовал сервер, вроде заработало. Завтра буду смотреть дальше.

    07.01.2021@утро-раннее: посмотрел -- грусТно! Оно таки подвисло!

    И как-то непонятно -- просто не обновляется, и всё тут.

    Но рестарт сервера -- в виде Ctrl+C, стрелка вверх, Enter -- вроде помогает.

    А потом в /var/log/messages на b360mc нашлись такие строчки (записываю позже, потому там много):

    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)
    
    -- как раз примерно за то время, когда перестало обновляться (у b360mc с x10sae разница часов в 3 минуты).

    И, что любопытно -- подобное же было и 02-01-2021, когда я с этим возился, но просто я не заметил.

    Напрашивающиеся мысли:

    1. @#$%!!!
    2. Вот так весело совпало, что становится НЕОБХОДИМОЙ вторая часть работы, ради которой выцыганивались исходники CAENVMElib'а: ловить потерю и восстановление связи с крейтом.
    3. И явно надо изучать код драйвера, чтобы понять, как именно это ловить.

      Очевидные точки -- те, где печатаются сообщения "Timeout" и "recovered" в syslog().

    4. Только вопрос -- КАК это делать на уровне CX'ных VME-драйверов? Вводить дополнительный метод, который layer'ом мог бы вызываться?

      Вечер: да, это уже сделано -- vme_stat_proc, см. за 07-02-2020 в разделе по vme_lyr.

    5. А ведь есть какая-то что-то-Reset -- что она делает? Может, её как-то уметь вызывать? Хотя как (методом layer'а? каким, откуда?) и в какой момент?
      Вечером: да, есть парочка.
      • CAENVME_DeviceReset() -- дёргает IOCTL_RESET, приводящий в драйвере к вызову a3818_reset_comm().

        Похоже, это оно.

      • CAENVME_SystemReset() -- делает что-то не до конца ясное.

        Похоже, выполняет reset УДАЛЁННОМУ КОНТРОЛЛЕРУ -- т.е., некую операцию "reset на VME-шине", путём взведения и потом сразу же сброса битика 0x80 в регистре VMECTRL.

        И делает это путём КОММУНИКАЦИИ по линку.

    07.01.2021@дорога-от-родителей, в лесочке уже перед двором, ~13:00: а надо бы тесты провести:

    1. Поможет ли stop=1? // Нет
    2. Поможет ли _devstate=0 (рестарт драйвера)? // Тоже нет

    07.01.2021@~14:00...вечер: проверяем:

    1. Запись stop=1 НЕ помогла.
    2. Рестарт драйвера посредством _devstate=0 -- тоже НЕ помог. И последовательное =-1, =+1 -- тоже нет.
    3. А помогло -- сначала ОБОИМ _devstate=-1, а потом уже =+1.

      Т.е., ПЕРЕОТКРЫТИЕ линии (поскольку при disconnect'е последнего устройства делается закрытие, и потом линия открывается заново при первом же).

      И что -- надо по ловле события "дисконнект" также выполнять CAENVME_DeviceReset()? Как бы проверить-то, что оно поможет...

      08.01.2021@ванна: а Reset ли нужен? Или всё же просто CAENVME_IRQEnable()?

    Вечером, когда опять зависло:

    • Делаем _devstate=-1 обоим, а =+1 только одному. Смысл -- проверить, что проблема бывает только при ДВУХ молотящих одновременно.
    • 08.01.2021@утро: да, так оно НЕ зависло.

      Возможно, проблема в том, что в момент какого-то обмена с линией "неожиданно" прилетает сообщение про IRQ.

    • 08.01.2021: ...а ещё надо бы погонять стресс-тест в варианте с USE320=no, но с ДВУМЯ молотящими устройствами. Чтоб посмотреть -- будут ли тут "сбои" связи?

    08.01.2021: продолжаем тестирование.

    • С одним ADC250 так и не зависло за полсуток, накрутив несколько миллионов кадров.

      Только включил второй -- и минут через 10 подвисло.

    • Погонял ДВА устройства в варианте с USE320=no. Оно НЕ зависало

      Но вот сообщения о таймаутах бывают, однако зависаний они не вызывают.

    • Откуда мысль: а может, косяк всё-таки в моём коде в драйвере? Что после таймаута почему-то не дрыгается IRQ?

      Надо бы поанализировать код рядом с теми сообщениями.

    • О -- а может, при том Timeout/recovered всего лишь отключается трансляция IRQ в локальные прерывания и нужно заново делать Enable?
    • @ванна: как проверить -- собрать всё с USE320=YES, дождаться зависания и потом из другого окна обратиться к любому устройству через a3818_test, при котором линии сделается CAENVME_IRQEnable().
    • 09.01.2021@утро-~7:00: в очередной раз зависло, так что попробовал. Результат:
      • просто открывание линии -- не помогает.
      • А вот прочитать что-то помогло.

        Вот как так, а?!?!?!

      • (чуть позже, ~11:00) а может, как раз сие и объясняет, почему НЕ зависает обычный вариант с поллингом -- по USE320=no -- даже при Timeout/recovered: там ведь постоянно идёт обмен, при попытках что-то прочитать; вот оно и "не замечает" проблемы.
      • (ещё чуть позже, ~13:00) ещё проверил, что годится ЛЮБАЯ попытка чтения -- даже несуществующего адреса, заканчивающаяся ошибкой. Ну оно и понятно -- обмен-то по линии произошёл.

      Резюме: похоже, что дело совсем не в отсутствии 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, вернувшись домой: сделал диагностику.

    • Технология извратновата: чтобы не пихать в каждую из 4 функций код диагностики, воспользовано препроцессором. Вот иллюстрация -- начало (#define'ы и первая функция, для иллюстрации)
      #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;                                                            \
      }                                                                                \
      
    • Т.е.,
      1. Символ DEBUG_A3818_HAL_IO управляет включенностью диагностики.
      2. Модификации к каждой функции заключаются в добавлении "пролога" (который теперь вместо начального return) и "эпилога".
      3. При включенной диагностике
        1. "Пролог" A3818_HAL_IO_BEG превращается в добычу результата операции.
        2. "Эпилог" A3818_HAL_IO_END делает проверку, при надобности выдачу на stderr, а затем возврат.
      4. А при выключенной генерится тот же код, что и раньше -- "пролог" является просто return'ом, а "эпилог" пуст.
    • Решение "краткое, эффективное, элегантное", но по факту -- нифига не очевидное и не легкопонятное. Одно отсутствие ';' у "пролога" и скобок у обоих (хотя у "эпилога" и можно было, но так будет ещё хуже из-за асимметрии) уже раздражает.

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

    • ...и, для успокоения совести (и чтоб потом умом не тронуться разбираясь, а остались бы комментарии) пришлось писать это объяснение.

    Результат тестирования: НИ-ЧЕ-ГО. Т.е., при потерях/восстановлениях связи так и не появилось НИ ОДНОГО сообщения об ошибке выполнения VME-операции. (А тестовое чтение заведомо отсутствующего адреса -- ошибку даёт, так что диагностика работает.)

    Откуда выводы:

    1. Смысла после восстановления выспрашивать у контроллера текущее состояние прерываний -- похоже, нету.

      Замечание: по крайней мере, для ТЕКУЩЕЙ проблемы.

      21.01.2021: с другой стороны, никто не запрещает вызывать IRQCheck() самому a3818_hal'у.

    2. Достаточно, видимо, просто выполнять "пустую" операцию чтения (да хоть 32:0x09:0x00000000).
    3. И вообще пора бы уже детально почитать тот кусок кода драйвера, где это Timeout/recovered.

      (Возможно, там вообще просто синхронно выполняется какая-то операция.)

    27.01.2021: всё, хватит раздумывать и тянуть -- надо добавлять "генерацию событий" -- уведомление userspace-клиентов о потере и восстановлении соединения.

    • Понятно, что оные "события" должны быть просто дополнительными битиками в возвращаемой irq-mask.
    • Выяснился отдельный нюанс: эта маска отдаётся 32-битовым значением ТОЛЬКО из CAENVME_ProcessEvent(). В CAENVME_IRQCheck() же -- 8-битное CAEN_BYTE.
    • ЦЕЛЫЙ ДЕНЬ изучал вопрос, куда же будет правильно засунуть 2 строчки определений.

      (Да-да, как раз вспомнилось рассказанное ЕманоФедей -- что для коммерческих программеров считается нормой 2 строки кода в день :D.)

      Увы, но оказалось, что организация кода не самая удачная: нет единого файла с определениями, который бы использовался совместно библиотекой (с экспонированием клиентам) и драйвером.

    • В результате пришлось делать аж в 3 РАЗНЫХ местах:
      1. В библиотековом CAENVMEtypes.h
        typedef enum {
                cvConnectionLost      = 0x20000000,
                cvConnectionRecovered = 0x40000000,
        } CVConnectionEventMasks;
        
      2. В драйверовом a3818.h -- унифицированные с предыдущим значения:
        #define A3818_CONNECTION_LOST           (0x20000000)
        #define A3818_CONNECTION_RECOVERED      (0x40000000)
        
      3. Во внутри-библиотековом его доппельгангере.
    • Замечание: да, значения специально были выбраны так, чтобы не не использовать старший (знаковый) бит 0x80000000, дабы никак не могло возникнуть проблем со знаковостью/беззнаковостью.

    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() ловлю событий.
    • Перезагружаем драйвер (чтоб был новый).
      • Сначала -- "облом": при запуске сервера "communication error".

        Памятуя когда-то-шнюю проблему с перезагрузкой (оно по "shutdown -r now" зависло и не дошло до перезагрузки), заподозрил, что косяк в драйвере.

        Так что драйвер rmmod'нут и машина перезагружена.

      • После перезагрузки загружаем новый драйвер и... опять тот же облом!!!
      • Что, проблема ппаратная -- питание надо дёргать?!
      • НЕТ!!! Как-то сообразил, что всего лишь навсего не те права на файлах устройств /dev/a3818* -- надо сделать "chmod 0666 /dev/a3818*".

      ОК -- подхватилось.

    • Дожидаемся зависона, и... Ловится только "connection lost".

      Оказалось -- ляп в коде драйвера, вместо "CONNECTION_RECOVERED" делалось то же самое "CONNECTION_LOST" (огрехи копирования...).

      Исправлено.

    • Ну -- сделано, запускаем молотилку с проверкой.
    • Да, ловит оба события, ура!!!
    • Добавляем прямо туда холостое чтение и запускаем молотилку на ночь.

    30.01.2021: далее...

    • Утром проверяем: авотфиг -- событие-то поймалось, холостое чтение выполнено, но молочение дальше не восстановилось...
    • Идея: а может, дело в том, что при "ручном" выполнении холостого чтение до него успевает пройти какое-то макроскопическое время? И при автоматическом чтении надо выдержать паузу?
    • Окей -- всунул SleepBySelect().

      Причём пришлось вставлять локальную копию, поскольку в publics-наборе для драйверов оно не экспонируется.

    • Сначала попробовал 100мс=0.1с -- фиг.
    • Окей, ладно -- 10с (ну мало ли, вдруг там время реально макроскопическое). Тоже фиг.
    • Последняя попытка -- 300с=5мин. И ТОЖЕ ФИГ!
    • Откуда возвращаемся к мысли: видимо, при "пинке" сторонней утилитой a3618_test влияет всё-таки не только (или не столько?) чтение, сколько ещё сам факт открытия линии из клиента -- вероятно, при этом выполняется какой-то сброс.

      Обсуждение (по результатам просмотра "старых" записей, недельной/трёхнедельной давности, когда только начал набирать статистику по этим зависонам):

      • Однако тогда просто открытия a3818_test'ом НЕ хватало.
      • Так что, вероятно, роляют ОБА фактора -- И открытие, И чтение.
      • ...но и вариант с CAENVME_DeviceReset() тоже надо рассмотреть. Причём:
        1. И просто попробовать,
        2. и понять, что же делается при открытии /dev/a3818* --
          1. В библиотековом CAENVME_Init() (нет ли там сброса/инициализации).
          2. В драйверовом a3818_open().

            А там есть -- конкретно для TypeOfBoard == A3818BOARD -- вызов некоей a3818_reset_onopen()...

    • @вечер: попробовал "с наскоку" -- добавил перед ReadCycle'ом ещё вызов CAENVME_DeviceReset() (убрав паузу).

      Неа, НЕ помогло.

    • 31.01.2021@утро-~7:30: окей -- для "очистки совести" вернул паузу, поставив 10с.

      Тоже не помогло.

    31.01.2021: продолжаем тестировать-экспериментировать...

    • @душ-~13:00: пара мыслей:
      1. А не в том ли проблема, что при обрыве+восстановлении связи теряется "событие IRQ" и его можно попробовать "восстановить", сделав IRQCheck?
      2. И/или не запрещаются ли локальные прерывания, так что их надо вернуть при помощи IRQEnable?
    • Пробуем: ставим после ReadCycle ещё IRQCheck и IRQEnable.
    • ФИ-Г!!! Вычитывание статуса IRQ даёт маску 0x10 (т.е., IRQ5 -- 1<<(5-1)=16=0x10), но нового прерывания так и не прилетает...
    • @17:50: а если эти Check и, главное, Enable, поставить ДО ReadCycle?
    • Пробуем... Неа, без разницы -- не помогло.

    Так что -- надо всё-таки читать код драйвера и библиотеки...

    14.02.2021: читал, смотрел (и не один день и не единожды!) -- ничего интересного/подозрительного не увидел.

    Пришла в голову мысль: а что, если "пинающий запуск a3818_test" делать не просто "открыл, выполнил чтение, завершился/закрыл", а поставить в конце командной строки ":", чтоб он продолжал висеть с открытым дескриптором?

    15.02.2021: проверил -- результаты крайне неожиданные!

    1. Да, такой вариант НЕ приводит к возобновлению постоянной работы.
    2. Но отрабатывается 50 событий. По 50 событий с КАЖДОГО из 2 осциллографов, при КАЖДОМ таком "пинке".

      Учитывая, что в irq_fd_p() делается по 100 repcount-итераций, очевидно, что это как раз они 50+50=100 и есть, а "событие"-прерывание происходит однократно.

      (А осциллографы -- на внутреннем запуске, мгновенно сразу -- молотят так быстро, что пока второй вычитывается, первый уже успевает отработать и заново выставить IRQ.)

    Откуда напрашиваются выводы:

    1. "Фишка" в том, что выполняется по ЗАКРЫТИЮ, а не по открытию.
    2. А то, что 09-01-2021 был сделан вывод о "просто открывание линии -- не помогает. А вот прочитать что-то помогло." -- ошибочен: ведь при отсутствии операции чтения (точнее, вообще каких-либо команд) автоматически подставляется команда ":", что и приводило к зависанию и НЕвыходу/НЕзакрытию. Вот я и подумал, что дело исключительно в чтении.

    Еще соображения:

    • Ведь повторные "пинки" выполнялись посредством "Ctrl+C, стрелка-вверх, Enter" -- т.е., закрытие-то происходило. И почему оно не приводило к возобновлению работы?

      Или дело в Ctrl+C -- что просто закрывался дескриптор, а вот CAENVME_End() НЕ вызывалось. Т.е., это оно что-то такое выполняет?

      Чуть позже: да нет -- на вид ничего такого в нём нет, а есть лишь:

      1. removesem(), в свою очередь вызывающий shared_mutex_close().
      2. _A3818CloseDriver(), сводящийся к просто close(file_handle).
    • Тогда идея ещё теста: вместо бесконечного зависания (и БЕЗ чтения!) поставить короткую паузу -- вроде ":100".

      При этом оно просто откроет и почти сразу закроет.

      Проверил -- ДА, ПОЛЕТЕЛО!!!

    • ...хотя всё равно остаётся вопрос: а ОДНО-то прерывание почему происходило?

      Или, может, это вызвано уже какими-то моими действиями при реализации поддержки a3818_poll() -- какое-то событие генерится случайно, из-за ненулевого значения в регистрах текущей маски "светящихся" IRQ?

    • Ещё крамольная мысль: а не может ли быть, что вообще ВСЯ эта проблема не столько аппаратная/драйверная, сколько вызвана каким-нибудь косяком в библиотеке -- например, некорректной работой с семафорами?

    И ещё пара соображений:

    • Насчёт repcount: а если сбросить лимит repcount до 1 -- чтоб оно КАЖДОЕ IRQ ловило бы отдельно? И это вообще сработает ли -- будут ли IRQ реально генериться, они "по уровню" ли, а точно не "по фронту"?
    • А как бы этак прочитать "вручную" в момент зависа состояние регистров "текущих светящихся IRQ"?

      Чтобы понять -- горят ли какие-то события у ЛОКАЛЬНОГО контроллера A3818, или же они есть только у удалённого V2718.

    17.02.2021: а не мои ль всё-таки это косяки? Как-то не так что-то делаю, вот оно и дурит... Мысли по теме:

    • @утро-ванна: похоже, придётся-таки добавлять логгинг на IRQ и poll().

      И где места на диске b360mc взять под /var/log/messages?

      @вечер: printk() делать с префиксом "<N>" (или какие варианты ещё возможны -- см. "Debugging by printing"), и сбагривать syslog'ом такие сообщения в отдельный файл.

    • @дорога к родителям, около стадиона НГУ: может, гасится СОБЫТИЕ прихода IRQ, и потом больше ничего не приходит, хотя биты "горят" в контроллере?

      @дома, ~16:50: да вряд ли -- ведь IRQ локально генерится АППАРАТНО, прямо самим A3818, это не просто чтение сообщения из линии посредством a3818_recv_pkt()/a3818_handle_rx_pkt().

    • @дорога от родителей: неа -- не мой это косяк: всё б можно было предположить, но то, что после open()+close() работа восстанавливается -- точно указывает на то, что существует какое-то "магическое" действие, которое если сделать, оно проблему решает.
    • @дома, ~16:20: кстати, если б косяк был лишь мой, то в рамках a3818_hal.h проблема решалась бы элементарно: оставить поллинг с частотой ~1Гц, вот оно б и отлавливало такие "залипшие" IRQ, а дальше уж работа бы восстанавливалась сама, за счёт правильного программирования осциллографов на дальнейшие измерения и прихода затем прерываний уже от тех измерений.

      Но нет -- проблема в чём-то ином, раз даже 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 -- в лог падают эти 2 строчки и серверу даются те 2 события,
      • ...и молочение НЕ продолжается, пока не будет выполнен "пинок" заново.

      И это СТАБИЛЬНО воспроизводится!

      Откуда опять возникает вопрос -- а может, всё-таки это МОЙ косяк?

      С другой стороны -- ведь драйвер-то эти Timeout/recovered ловит (и, подозреваю, что СТАРЫЙ драйвер тоже будет; проверим?). Что смахивает уже на какой-то программно-аппаратный глюк.

    • 22.02.2021: дополнение:
      1. ПУСТАЯ команда -- :100 --
        1. НЕ приводит к отвисанию.
        2. Зато ПРИВОДИТ к таймауту. Причём иногда СРАЗУ, а не только после рестарта сервера. Похоже, там какая-то вероятность -- иногда не с 1-го, и даже не со 2-го раза, а с 3-го.
      2. Команда с ЧТЕНИЕМ и паузой -- i:0 :100 --
        1. ПРИВОДИТ К ОТВИСАНИЮ. Стабильно.
        2. А к таймауту -- НЕ приводит: ни сразу, ни даже после рестарта. Но...
        3. ...зато однажды она САМА СЕБЕ обеспечила получение пары Timeout/recovered. Причём:
          1. Сервер этого события НЕ получил!!!

            Т.е., что же выходит -- что события доставляются только ПЕРВОМУ? Или предполагается, что "получатель" должен ровно в этот момент висеть на select()'е, а иначе -- фиг?

            Как бы то ни было, уж это-то -- точно МОЙ косяк, поскольку сами события являются МОИМ порождением.

            @засыпая-~00:00: дык -- понятно, что "получатель" только ОДИН -- дело в механизме работы FGT_IRQ! Да, так оно и должно быть.

          2. И потом сервер стал стабильно при рестарте сразу же получать эту парочку -- сам себе делал.

            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) дала эти же числа.

      24.02.2021@вечер, 21:24: разобрался, почему же "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: список действий, которые стоит провернуть далее:

    1. Попробовать со старым (не-poll()-capable вариантом) драйвера и USE320=no -- будет ли в логи падать Timeout/recovered при параллельном запуске "a3818_test ... :100".
    2. Сделать diff'ы:
      1. Между 1.6.2 и 1.6.3 -- чтобы понять, что добавилось.
      2. Между 1.6.2.old и 1.6.2.new -- это список добавлений для поддержки select()/poll().
    3. Перетащить доделки из нынешнего A3818Drv-1.6.2-build20181214 в A3818Drv-1.6.3-build20200203 (?).
    4. Попробовать с 1.6.3 -- изменится ли что-нибудь.
    5. Попробовать сократить repcount до 1.
    6. Почитать-таки ещё раз, что же именно делается в open()/Init() и close()/End() в драйвере/библиотеке.

      Как минимум в CAENVME_Init() есть какие-то CAENVME_GetFIFOMode() и CAENVME_WriteRegister(i, cvVMEIRQEnaReg, 0xFF)).

    7. Попробовать добавлять находимое в a3818_hal'ову реакцию на cvConnectionRecovered.

    24.02.2021: приступаем (п.2 ещё позавчера).

    1. Попробовал с этим, poll()-capable вариантом, но с USE320=no: НЕ таймаутится
    2. diff'ы:
      1. С тех пор появился уже A3818Drv-1.6.4-build20210118.

        Как и 1.6.3, отличается только тонкостями с компиляцией под свежие ядра (ещё там что-то с некоей "mmiow()" было).

      2. И свой A3818Drv-1.6.2.diff сделан -- пара сотен строк, довольно простых.
    3. Перетащил в 1.6.4. Собралось без проблем.
    4. Попробовал с ним -- ну кто бы сомневался, ничего не изменилось.
    5. Снизил количество повторений по repcount до 1 -- тоже ну кто бы сомневался, срубилось через полчаса.

    А вот дальше решил отойти он намеченного сценария:

    • Как ранее выяснилось анализом кода, "таймаут" -- это истечение 00-миллисекундного таймаута в a3818_recv_pkt(), ожидаемого посредством wait_event_interruptible_timeout().
    • @ванна: Отсюда идея: а если попробовать этот таймаут увеличить?
    • Окей, поставил 100*5. Увы, не помогло (кто бы сомневался) -- всё равно через некоторое время таймаутнулось.
    • Впрочем, кажется разумным иное направление поиска:
      • "Отрубание" получения прерываний происходит после таких таймаутов, так? А в них ли реально дело -- может, это скорее совпадение? Точнее, КОСВЕННАЯ причина?
      • Например, так: a3818_recv_pkt() как-то отключает получение прерываний, а в случае таймаута включения обратно не происходит.

        Причём оное "включение обратно" вполне может делать даже и не recv_pkt(), а что-то другое, вызываемое в случае успешного получения -- например, a3818_handle_rx_pkt() или его подручный a3818_dispatch_pkt().

        ...кстати,

        • a3818_handle_rx_pkt() может вызываться
          1. прямо из a3818_recv_pkt()
          2. из a3818_interrupt()
        • -- в обоих вариантах при условии, что "tr_stat & 0xFFFF0000", каковое tr_stat является значением, прочитанным из регистра A3818_LINK_TRS.
      • Вот и стоит заняться изучением этих возможных действий, производимых при разных ветвях исполнения -- при нормальном получении и при таймауте.
      • ...вопрос только, почему это включение обратно не происходит при последующих 5 попытках восстановления связи?

        Ответ: а потому, что эти 5 попыток -- уже вовсе не просто recv_pkt(), а вызов друг за дружкой a3818_reset_comm() и a3818_send_pkt() (исходных данных), и уж только потом, в случае успеха отправки, пробуется снова a3818_recv_pkt().

        Вот не a3818_reset_comm() ли, например, отключает прерывания?

        Чуть позже: а вот хбз... Там делается несколько записей в разные регистры, а уж какой они имеют эффект -- фиг поймёшь, описание регистров нужно.

      • В любом случае -- надо попробовать найти список регистров. Вроде в каком-то из орписаний он был...
      • @засыпая: для проверки гипотезы о виновнике можно временно закомментировать выхов a3818_reset_comm() -- если после этого прерывания перестанут отключаться, то дело в нём.

        Но это, конечно, именно лишь для проверки -- "открывать банку с червями", ломая отлаженную работу коммуникации, точно не стоит, а надо искать способ включения прерываний обратно.

    • 25.02.2021@утро: учитывая вчерашний эксперимент с увеличением таймаута с 100мс до 500мс, окончившийся "неудачей":
      • Первая мысль: а если проблема в том, что аппаратное "восстановление после сбоя" занимает существенно больше времени, и потому ни 0.1с, ни 0.5с никак не помогут?
      • Да нет, не выдерживает критики то предположение: ведь следующая же попытка через -- в штатном варианте -- 0.1с уже оказывается успешной.

        (Более того -- идея о необходимости макроскопической задержки уже возникала, и a3818_hal'ов irq_fd_p() даже выжидает 10 секунд, но без толку. Хотя, конечно, это реально пауза не там и не о том...)

      • А успешной оказывается операция после ПОВТОРНОЙ отправки исходного пакета.
      • Откуда предположение: может, происходит не "сбой коммуникации", а тупо ПОТЕРЯ ПАКЕТА? Причём фиг знает -- может, отправляемого, а может, принимаемого (отправляемого контроллером V2718).
      • Вопрос ещё, конечно -- а почему проблема возникает почти исключительно в режиме select(), а не при поллинге.

        Возможно, из-за более асинхронного характера потоков данных/пакетов: что отсылка IRQ контроллером V2718 в какой-то момент конфликтует с регулярными запрос-ответами.

        1. Либо из-за не-дуплексности CONET2.
        2. Либо из-за того, что в момент отправки сообщения "IRQ!" контроллер не принимает из линии запросы и пакет теряется.

        Хотя по сути -- имея доступ только к локальной стороне, внятно что-то предположить затруднительно. (А на "той" стороне вообще FPGA, фиг знает, как запрограммированная. Да и сниффера-анализатора для CONET2 у нас нету...)

      ...только неясно, какой "прогрессивный" прок с этих догадок -- предпринять-то что можно?

    25.02.2021: попробовал убрать a3818_reset_comm()...

    • (Кстати, в irq_fd_p() делается CAENVME_DeviceReset(), который по сути и является ioctl()-обёрткой к a3818_reset_comm().)
    • Ну, собственно, закомментировал в драйвере вызов перед повторной передачей. Драйвер откомпилировал, загрузил...
    • Эффект неожиданный: cxsd вылетает по SIGSEGV. Причём:
      • Вылетает сразу, без вызова обработчика.
      • Попытка запустить под gdb, чтобы посмотреть где-что -- фиг, под gdb не падает, а просто логгирует события и далее как обычно.
      • Окей -- сделал "ulimit -c 1000000", дождался падения и core-файла.
      • Так вот: там внутри --
        #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 ?? ()
        . . .
        
        ...и дальше в том же духе -- аналогичный бред, видимо, по причине порченого стека.
        • Видно, что до обработчика-то оно доходит, но далее рубится сразу же (видимо, не только стек порчен).
        • И также видно, что сам SIGSEGV произошёл не вполне ясно где: упомянут CAENVME_ReadCycle(), но вызванный откуда -- хбз; точно не из irq_fd_p(), поскольку там перед ним ещё много чего, сопровождаемого диагностической печатью.

        Вот как это драйвер так умудрился повлиять?

      Что теперь делать -- хбз. Пересобрать с OPTIMIZATION=-O0, подобавлять отладочной печати, и надеяться, что эти меры не повлияют на факт вылета?

    27.02.2021: как бы то ни было, вчера-сегодня прогнал по вариантам с наличием/отсутствием reset'ов:

    1. С закомментированным в драйвере _reset_comm(), но оставшимся CAENVME_DeviceReset() в _fd_p():
      • События после таймаута приходить всё равно перестают.
      • ...и, что интересно -- при ловле таймаута прямо на старте сервер НЕ SIGSEGV'ится. А вот позже -- да.
    2. С убранным также из userspace'а ресетом -- всё аналогично.
    3. После вертания comm_reset'а в драйвере -- SIGSEGV'иться перестало.

      Но и события так и не приходят (в _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'а":

    • Просто 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, и на нём остановились.

    • И это подтверждается листингом /proc/PID-OF-cxsd/fd/ -- там множатся дескрипторы, глядящие на /dev/shm/CAENV1718_LCK_100, а дескриптор на /dev/a3818_0 всё увеличивается в номере (что и было замечено по отладочной печати).

    И ещё: возникла идея проверить, будет ли работать пере-открытие, если карточка -- /dev/a3818_0 -- будет открыта ещё кем-то (т.е., reference count на линию НЕ будет ==0).

    • Сначала простодушно попробовал a3818_test -- без команд, т.е., по сути с ":". И серверова работа зависла!

      Что, если подумать, было предсказуемо -- утилита отбирала у сервера прерывания, ловя их раньше.

    • Потом подумал, что достаточно просто открыть сам файл устройства. Что можно сделать прямо из командной строки. И сделал, командой
      while true;do;sleep 1000000;done <>/dev/a3818_0
      -- всё ОК.

      И да, восстановление ПРОИСХОДИТ. ...ну и кто б сомневался -- обычный-то "ping" делался как раз отдельной программой, при уже открытом устройстве.

    • А вообще тестировать по-хорошему бы надо не a3818_test'ом, а отдельной программкой, содержащей прямые вызовы CAENVMElib'а.

      12.06.2021@утро: а с другой стороны -- зачем? СЕЙЧАС-то базовая ситуация стала понятна, и делать всё надо уже именно в irq_fd_p()...

    12.06.2021: @завтрак: ещё мыслишка: а зачем прямо ПЕРЕоткрывать? А если попробовать просто ЛИШНИЙ раз открыть и потом закрыть -- open("/dev/a3818_0"); close()?

    И для начала это можно проверить прямо из консоли -- командой вроде

    sleep 1 <>/dev/a3818_0

    И-и-и...

    • Попробовал -- просто за-#if0'ил блок пере-открытия, оставив так же отключенным блок чтения.
    • При первом запуске оно сделало SIGSEGV. WTF?!
    • При втором -- запустилось, но встало колом, ничего не бежало.
    • При третьем -- немножко пробежало, но вскорости опять встало колом...

      И при четвёртом аналогично.

    • Причём обычное пингование НИКАК НЕ ПОМОГАЕТ!

      И более того -- самого события потеря/восстановление НЕ СЛУЧАЕТСЯ!!! Так что по сути, и пинговать-то нечего...

    • Вернул обратно и чтение, и пере-открытие -- всё равно замерзает!!!
    • И, как оказалось, зависает через некоторое время ДАЖЕ В РЕЖИМЕ USE320=no!!!

    13.06.2021: решил сделать "ручной тест" (утилитку a3818_u.c) -- проверить CAENVME_IRQCheck()'ом, горять ли какие-нибудь прерывания. Результат:

    • Прерывания -- НЕ горят, lit_mask=0.
    • Но вот ручное чтение из регистра 0x000014/INT_STATUS показывает наличие оных -- читается значение 2/ADC_CMPLT.
    • Чуть позже: и попробовал даже CAENVME_IACKCycle() -- возвращает -1.

      Учитывая, что эта операция сводится к реальному обмену с VME-контроллером и попытке выполнения IACK-цикла -- можно сделать вывод, что прерывания куда-то теряются.

    А может, правда просто железка глюканула?

    ...а может, оно и С САМОГО НАЧАЛА так подглючивало, и я почти полгода пытался разобраться с аппаратным глюком, считая это лишь косяком реализации?

    14.06.2021: передёрнул питание у крейта (ТОЛЬКО у крейта -- НЕ у компа! и даже драйвер не перегружал, и даже сервер продолжал быть).

    И, придя домой, рестартовал сервер -- он так и висел, а в логах были строчки

    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
    
    -- именно 2 attempt(s), а не 1, как обычно; да и строчек "Timeout" 2 штуки подряд.

    15.06.2021: проверил -- да, пашет, уже больше полусуток (это USE320=no).

    16.06.2021: попробовал ещё раз с USE320=YES.

    1. При первом запуске оно вообще SIGSEGV'нулось.

      ...хотя и словило -- сразу же! -- событие потеря/восстановление. Это, видимо, оставалось в драйвере запомненное от того дёрганья питания.

    2. Потом заработало, но через буквально минуту зависло. При перезапуске -- ещё поработает и опять зависает.
    3. А после этого и вариант USE320=no тоже начинает зависать.

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

    • Вставить вторую карточку не удалось: она x8, а в B360M-C слоты x1 -- ЗАКРЫТЫЕ (т.е., НЕ open-ended), так что увы.

      Посему поставил 2-портовую вместо исходной 1-портовой.

    • А в новый крейт воткнул пару козаковских VADC16, у которых в A16D16 читается пара регистров по смещениям 0 и 2.
    • Проверил канал 0 (с первой попытки оказался верхний) -- работает, из обоих ADC250 по i:0 вычитывается слово 0x4604ADC0.
    • Попробовал канал 1 -- фиг, ноль реакции и просто подвисает.

      "Поменял местами" -- воткнул кабель из 0-го порта

      А потом заметил, что просто недовоткнул оптический кабель в разъём. После втыкания -- работает!!! (из VADC16 по s:0 и s:2 вычитываются 0x0000)

    • Но было замечено вот что:
      • Когда крейт включен и связь есть -- реакция мгновенная.
      • Когда крейт ВЫключен или разъём НЕ воткнут -- реакция также мгновенная, "Comminication error".
      • А вот при попытке обращения к "дальним" крейтам -- /1, /2, ... -- сначала задержка 3 секунды, и уже потом ошибка.

        21.06.2021@утро: прогон под strace показал, что эти 3 секунды проводятся внутри ioctl()'а с кодом 1 -- IOCTL_COMM.

      • И когда кабель был недовоткнут, то оно подвисало так же.

        Т.е., как будто "чувствовало", что что-то подключено (и НЕ давало ошибку сразу), но реально связь не работала.

    • Откуда выводы:
      1. Прямо по CAENVME_Init() выполняется какой-то обмен с крейтом. Возможно, что именно это и есть то действие, которое "развешивает" работу прерываний.
      2. Такое поведение создаёт проблемы с точки зрения начального включения: если крейт ВЫключен, то дальнейшее его запитывание ситуацию уже не спасёт -- нечему будет reconnect'иться, т.к. и начального-то "открытия шины" так и не произойдёт.

        Это, правда, и так-то как бы "не проблема", поскольку нынешние драйверы предполагают наличие/доступность устройств при старте -- в _init_d() -- и при отсутствии просто не смогут нормально отработать (а ADC250'шный вообще просто вернёт -CXRF_CAMAC_NO_X при ошибке чтения). Так что сейчас всё равно придётся делать всем рестарт по "._devstate=0".

      ...мда, традиционно с отсутствием/потерей/восстановлением связи связаны сложности...

    • Также (ещё до тестов) появилась мысль: взять у Бобровникова какие-нибудь CAEN'овские модули и попробовать от них получить прерывания.

      Смысл -- чтобы отвязаться от потенциально "некорректно" (по идее от Симонова) работающих 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 шт.
      
    • А для начала надо прямо с VADC16 попробовать: перевести его в режим adc_timecode=0, чтобы он молотил с высокой частотой (судя по описанию -- 5мс/измерение (плюс задержка 11-12мс в начале цикла).

      И в количестве 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.

    • Работает, сервером :2.
    • Сервер :1 (на major=0 с парой ADC250) в процессе "упал", и для чистоты эксперимента так и оставлен остановленным.
    • С прерываниями как-то странно: с cdaclient, натравленнм на b360mc:2.a2.adc0, в режиме adc_timecode=7 оно стоит молча 20 секунд, а потом выдаёт сразу пачку данных штук 38...45, с минимальным зазором между измерениями.

      Надо бы поразбираться -- смотреть на изменения счётчика пришедших прерываний, командой "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.
    • В режиме adc_timecode=0 за 100 секунд сгенерилось 735 прерываний (данные по /proc/interrupts, между 2 "grep 138:" сделано sleep 100).

      Канал adc0 за такие же 100 секунд обновился 13851 раз (данные по "cdaclient -T100 b360mc:2.a0.adc0 | wc").

      При второй попытке эти числа составили 735 и 14138 раз, что уже странновато.

      Поэтому сразу повторяем за 1000 секунд: получилось 6473 прерывания и 142950 обновлений канала adc0.

      ...и вторая попытка -- 6433 и 144318 раз.

    • Проверки ради сделал и a0._devstate=-1 -- счётчик прерываний в /proc/interrupts увеличиваться перестал.
    • В режиме adc_timecode=7 сразу проверяем на 1000 секундах: 47 прерываний и 1932 раза.

      ...время интегрирования между режимами 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 (от двух АЦП).

    • @ночью, проснувшись: а если убрать цикл по repcount? Ведь BIVME2'шный vmei.c работает безо всяких циклов, т.к. VME IRQ работают по УРОВНЮ и если останутся "неподтверждёнными" (когда 2 устройства одновремено выставляют), то так и останутся гореть и вызовут повторное аппаратное перрывание.

    24.06.2021: сделал -- теперь цикл не до 100, а до 1.

    Не особо-то помогло -- вектора вычитываются через раз бредовые: на IRQ6 вместо 50 бывает 51, 179, а на IRQ7 вместо 52 бывают 53. 181, 255.

    Ну теперь бы надо ещё работу ADC250 в таком режиме "без repcount" проверить.

    28.06.2021: Козак ответил на сегодняшнее письмо с вопросом "а нет ли обновлённой прошивки для VADC16?" -- нет, "для VADC16 существует одна версия прошивок.".

    Так что для отладки работы прерываний придётся искать какой-нибудь другой их источник.

    08.07.2021: а вот и нет -- ЕСТЬ новая прошивка!

    • По словам Антона Павленко, в старой был презабавнейший глюк, *у них* в ADC250 не проявлявшийся, но теоретически присутствующий: если прерывания от 2 осциллографов приходили *почти* одновременно (но всё же с крошечным сдвигом по времени -- так что при одинаковом внешнем запуске, как в клистронке, проблема возникать не должна, а вот при внутреннем запуске -- может), то на IACK могут ответить сразу ДВА устройства, и при этом прочитанный с шины вектор будет смесью ихних (не то по OR, не то по AND).
    • А поскольку тем самым прочитанный вектор будет не совпадать ни с одним из ихних, то они-то ОБА прерывание снимут, а программа не поймёт, что они отработали. В результате они ОБА "замёрзнут".
    • Т.е. -- РОВНО то, что и происходило 12-06-2021 ("зависает через некоторое время ДАЖЕ В РЕЖИМЕ USE320=no").
    • Идея для диагностики: печатать неизвестные вектора.

    24.06.2021: очередной раунд забав с A3818 с парой ADC250: в режиме USE320=YES и "в режиме без repcount"...

    Результат обескураживающий:

    1. Сбои связи ПОСТОЯННО: где-то по разу в минуту, иногда чаще.
    2. И файловые дескрипторы при постоянных переоткрытиях CAENVME_End()+CAENVME_Init() таки доползли до 1023, после чего CAENVME_Init() стал возвращать err=-2/cvCommError.

      А дескрипторы все те смотрят на /dev/shm/CAENV1718_LCK_100.

    08.07.2021: пробуем добавить диагностики для "замерзания" по полученному сегодня от Павленко совету (см. выше).

    • В vme_lyr_common.c::vme_lyr_irq_cb() добавлена печать векторов, для которых нет получателей.
    • И пытаемся воспроизвести "те" условия: пере-открытия и чтение за-#if0'ены, repcount вёрнут (до 100).

      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: попробовал немножко поэкспериментировать -- что/как/почему падает. Результаты презабавны:

    • Падает не от выключения/включения крейта, а именно ОТ ОБМЕНА ПО 1-й ЛИНИИ!!!

      Проверяется просто: запускаем cxsd на 0-й линии, а потом сканирование по 1-й, командой (только в одну строку)

      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
      
      -- так у присутствующих по нужным адресам ADC250 показываются прочитанные первые 10 слов (можно искать по "0x4604ADC0"), а на отсутствующие лишь "Bus error".
    • Так вот: после такого сканирования сервер падает -- где-то в серединке.
    • Немного результатов тестов разного рода сканирования:
      • Проверено, что падает не только от реальной "работы", но даже и от ПОПЫТОК чтения -- т.е., если сканирование натравить на диапазон БЕЗ устройств, то SIGSEGV всё равно будет.
      • От попыток сканирования по ТОЙ же ветке -- @0/1 вместо @1 -- НЕ падает. Сканирует о-о-очень долго (из-за тех таймаутов), но и только.
    • Попытка найти концы:
      • Сделано "ulimit -c 100000" и cxsd запускается с ключом -D -- чтоб не перехватывал сигналы, а падал бы и писал бы core-файл.
      • Ну записал; на него натравлен gdb, и что -- а почти ничего! На верхушке "bt" -- CAENVME_ReadCycle(), а дальше -- какой-то мусор.
      • OK, пересобрал CAENVMELib-3.2.0_sources.new в отдельной директории, с ключами "-O0" и "-g" -- чтоб хоть показало, в какой именно точке падает.
      • Авотфиг!!! В таком варианте вообще ТОЛЬКО мусор -- аж целых 2 строки bt.
      • Такое впечатление, что стек портится где-то со стороны ядра (драйвером?).

        ...кстати, подобное же поведение -- немотивированные SIGSEGV'ы при вроде бы безобидных попытках модификации кода -- наблюдались и полгода назад.

    • Мысли/разбирательства на тему "а как со стороны ядра может портиться?".
      • CAENVME_ReadCycle() сводится к заполнению буфера отправки и потом вызова _CAENVMECommEx().

        Оный же сводится к вызову ioctl(,IOCTL_COMM,), передавая туда указатели на буфера отправки и приёма и размеры оных буферов.

      • Была мысль "а нет ли где-то факапа с размером приёмного буфера" -- что из-за приёма не того пакета, что ожидается, пишется за пределы читаемой переменной и портит стек.

        Но:

        1. CAENVME_ReadCycle() передаёт указатель на СВОЙ ВНУТРЕННИЙ буфер, и уже оттуда копирует в юзерский.

          (Хотя если мимо его буфера промахнуться -- то и юзерские данные/call-frame выше по стеку можно испортить.)

        2. А вот CAENVME_BLTReadCycle() передаёт указатель уже прямо на ЮЗЕРСКИЙ буфер. Но падает-то не в нём.

          (Хотя если пакет-портельщик получается раньше, то порча может происходить именно в BLT-операции и просто проявляться позже.)

      • ...впрочем, всё равно можно попробовать модифицировать adc250_drv.c так, чтоб читалось толпой скалярных операций вместо одной векторной.

        Правда, это сильно изменит временнУю диаграмму, но всё равно попробовать можно -- а вдруг.

    • Неожиданный результат: в 1.6.4'шном a3818.c::a3818_ioctl() в ветке IOCTL_COMM отсутствует проверка размера приёмного буфера -- comm.in_count (кроме простой >0 -- что вообще что-то принимать будут), а просто
      1. Вызывается a3818_recv_pkt() (она читает в свой внутренний буфер s->app_dma_in[opt_link][slave], аллокированный посредством vmalloc(1024*1024) -- 1 мегабайт)...
      2. ...и затем делается копирование посредством copy_to_user(), которому "куда" указывается comm.in_buf, а "сколько" -- просто объём ПРИНЯТЫХ данных, без учёта размера приёмного буфера.

    11.07.2021: пытаемся исправить тот косяк в драйвере с непроверкой размера приёмного буфера.

    • Сделан форк A3818Drv-1.6.4.new-in_count_CHECKED/.
    • В котором вместо тупой передачи copy_to_user()'у значение "сколько"="сколько_ПРИНЯТО" делается проверка.
    • Ключевое слово -- bytes_to_copy: в неё сначала складывается объём, предполагаемый к копированию, а потом
      1. Проверяется, не ==0 ли, и если да, то printk() об этом.
      2. Проверяется, что если bytes_to_copy > comm.in_count, то опять же printk() этих размеров и bytes_to_copy=comm.in_count.
    • Результаты презабавнейшие:
      1. МОРЕ сообщений "bytes_to_copy=0!" -- настолько много, что пришлось закомментить.
      2. И есть превесёленькие сообщения о превышении:
        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
        
        -- это поштучно и довольно часто. Как раз отлично подходит как причина порчи стека.
    • И SIGSEGV'иться перестало!!!

      Вместо падений теперь отладочные сообщения

      vme_hal_a32rd32v(0x09:d5800024)=-2
      vme_hal_a32rd32v(0x09:d5a00024)=-2
      vme_hal_a32rd32v(0x09:d6a00024)=-2
      vme_hal_a32rd32v(0x09:d5400024)=-2
      
      (адреса заканчиваются на "24" потому, что читается со смещения ADC4X250_TIMER_PRETRIG/2*4, где "PRETRIG" -- это 18 точек ДО триггера; 18/2*4=36==0x24.)

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

    • Ей указываются номер линка, начальный адрес для чтения и COUNT. Если COUNT положительный, то чтение ведётся BLT-операциями, если отрицательный -- то обычными поштучными операциями, в количестве -COUNT.
    • Сегодня сделал ей make-скрипт -- mk_test_CAENVME_BLTReadCycle.sh.
    • И запинал собираемость.

    Далее проверки.

    1. Да, через несколько попыток подобрана комбинация, воспроизводимо генерящая проблему:
      1. test_CAENVME_BLTReadCycle 0 0xd6000000 8192
      2. test_CAENVME_BLTReadCycle 1 0x96000000 -1000

      Т.е., 1-я, "основная" программа читает реальную память реального устройства BLT-операциями по 8K слов, а 2-я, "мешающая", пытается читать по 1000 слов другого устройства. При этом частенько -- по несколько раз в минуту -- вознкают ошибки: сначала "прилетает" oversize-пакет, а потом таймауты на линках 0 и 1.

    2. Но самое весёлое происходит, если "мешающая" программа обращается к НЕсуществующим адресам:
      1. test_CAENVME_BLTReadCycle 1 0x86000000 -1000

      Тогда ошибки начинают валить почти постоянно, и уже сами по себе, а 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: решил ещё немного порыть по вопросу "когда что происходит".

    • В test_CAENVME_BLTReadCycle.c была добавлена собственная локальная копия strcurtime_msc() -- чтобы события ошибок префиксировались временами, дабы при разбирательствах "потом", по простыням сообщений, можно б было видеть, когда именно что происходило (а не гадать "при второй ли программе? или просто само спонтанно?") и можно б было сопоставлять с временами событий в /var/log/messages.
    • Обнаружилась странная штука: в /var/log/messages постоянно пёрли сообщения
      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
      
      -- по 11-12 пар в секунду. Это началось в субботу "Aug 21 08:44:49", причём всё это время было запущено лишь BLT-чтение (ещё с пятницы 20-го, когда тесты для багрепорта делал), БЕЗ "мешающего". А первым сообщением было другое --
      Aug 21 08:44:49 b360mc kernel: a3818: rcv-pkt: Link 0 ndata_app_dma_in=34304 > comm.in_count=32768
      

      После Ctrl+C программе BLT-чтения оно прекратилось и после перезапуска более не возобновлялось.

    • А ещё заметил, что вся ругань на тему "ndata_app_dma_in=..." относится только к "Link 0". Но grep'ом по /var/log/messages* обнаружил, что и на "Link 1" было -- ДАВНО и одной пачкой.

      Возникла мысль: возможно, такое происходит не с любым линком, а с тем, по которому идёт 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: реакция воспоследовала:

    1. "У вас PCIe версии 3.0, а мы проверяли только с 2.0". ОК, попробую найти комп с 2.0.
    2. "Обновите прошивку, если не последняя".

    Комп-то я поищу, а вот с обновлятором прошивки странно:

    • Он вроде как в C'шных исходниках, но при этом требует Java.

      ...но до неё так и не дошло, т.к...

    • ...егойный скрипт configure отказывается видеть библиотеку libCAENVME версии 3.2 (которая ".3.1.0"), как ты ему её ни подсовывай -- что через LD_LIBRARY_PATH, что в /usr/lib64/.

      Прогон под 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, но уже с проверкой размера принимаемого пакета. Результаты:

    • ДА -- паре Timeout/recovered ПРЕДШЕСТВУЕТ сообщение о слишком большом пакете.
    • Восстановление прихода IRQ:
      1. НЕ происходит по "sleep 1 <>/dev/a3818_0"
      2. ПРОИСХОДИТ по чтению 1 слова (причём БЕЗ какой-либо дополнительной задержки, ни до, ни после!)
      3. ТАКЖЕ ПРОИСХОДИТ при обращении к немаппированному адресу.

    26.08.2021: пытаюсь разобраться с прошивками.

    • Синтаксис у CAENBridgeUpgrade весьма странный -- разобраться с ним удалось посредством анализа исходника, CAENBridgeUpgrade.c::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".

      • Чтение прошивки: а похоже, что НИКАК...
      • Загрузка прошивки:
    • Как бы то ни было, информация о прошивке -- "Firmware revision = 0.05". 0.05, блин -- на купленной в 2021-м карточке, при том, что на сайте последняя прошивка -- a3818int_rev_0.6.bin от "July 20th, 2017"!

    30.08.2021: в переписке Paola Garosi из CAEN очень советовала всё же обратить внимание на прошивку.

    31.08.2021: я сходил в ИЯФ ногами и посмотрел на карточку вживую. Результаты забавны:

    • У карточки есть возможность иметь аж 4 прошивки -- 0...3 -- одновременно, при прошивании утилите указывается номер банка (судя по исходникам, это просто страница-"четвертинка" во флэше, так что в зависимости от номера банка меняется начальный адрес записи), а на карточке есть пара 2-позиционных переключателей S1,S2, чьё положение вместе и выбирает, какую из 4 прошивок загрузить при включении.
    • Так вот: переключатели "firmware bank select" стояли в положении S1=B,S2=A; по мануалу, это выбирает банк #1.
    • А прикол в том, что при переборе всех вариантов -- да, я проверил все 4 (shutdown, переключаем, загрузка) оказалось, что во ВСЕХ ТРЁХ ОСТАЛЬНЫХ банках (0,2,3) залита прошивка версии 0.6, и только в злосчастном #1 -- 0.5!
    • Также тест показал, что параллельное чтение из 2 линков теперь -- с версией 0.6 -- работает без проблем.
    • А чтение их ChangeLog'а в A3818_Firmware_ReleaseNotes.txt нашло такую запись о причине появления текущей прошивки:
      ===============================================================================
      Release 0.6 (20/07/2017)
      ===============================================================================
            
          Bug fix:
      
      	  * Fixed a bug on the arbitration of the interrupt requests from different
      	    links
      
      -- т.е., РОВНО эта проблема: путаются IRQ/пакеты от разных линков!!!

    01.09.2021: проверяем работу прерываний с прошивкой 0.6 (aka "0.06"). Вкратце -- нет, лучше не стало.

    • Ещё вчера запустил тестирование с USE320=YES, и оно всё так же "подвисает" -- Timeout/recovered, и на этом всё (но пере-открытие там закомменчено!!!). Это может произойди через 5 минут, а может и через несколько часов -- в этом смысле отличий нет.
    • В ОСНОВНОМ оно сейчас происходит просто Timeout и потом recovered, БЕЗ overlong-пакетов.
    • ...но бывает и с ними; например,
      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
      
      (это "моя" версия драйвера, с диагностикой на тему jiffies).
    • А с ping'ами странновато по-прежнему, как и 25-08-2021: НЕ канает "sleep 1 <>/dev/a3818_0", но работает чтение 1 слова, причём как успешное, так и по немаппированному адресу.
    • Теперь пробуем вариант USE320=no...

      02.09.2021: ровно сутки спустя: всё ровно, ноль проблем.

    • ЗЫ: а ещё при запущенных ещё вчера параллельных чтениях из линков после запуска сервера (с USE320=YES?) в лог начали постоянно валиться сообщения о переполнениях (и это при версии 0.6/0.06!!!):
      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, конечно).

    • В режиме USE320=no при работающем сервере запущено "мешающее" чтение из link#1.

      Несколько часов простояло -- хоть бы хны.

    • Теперь запущено "основное" (блочное) чтение уже из того же link#0.

      За полчаса -- также ничего.

    • А теперь уже "мешающее" и тоже уже из link#0.

      Да -- есть!!! Меньше чем через минуту!

      Ещё чуть позже: фиг!!! Такое ощущение, что оно не "через минуту", а прямо СРАЗУ, при старте (результат открывания линии?); в дальнейшем же -- НЕ воспроизводится.

      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.

    • А ещё странность при нажатии Ctrl+Z "мешающей" утилите:
      • Скорость работы сервера (КОТОРОМУ мешают) при этом НЕ возросла -- так и осталось по одному измерению раз в несколько часов.
      • ...но после "разморозки" (fg) утилита получает
        CAENVME_BLTReadCycle()=-2
      • И иногда по её "fg" сервер также получает
        vme_hal_a32rd32(0x09:d5000014)=-2
        а в /var/log/messages при этом попадает 4 строчки:
        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
        
        причём эти строки ВОСПРОИЗВОДЯТСЯ -- числа почти каждый раз те же самые ("почти" -- потому, что когда чуть другие (и всего 2 строчки), то сервер ошибки НЕ получает).
    • Кстати, а не будет ли возникать "переполняющего" пакета при Ctrl+Z ЕДИНСТВЕННОЙ программе? Тогда это было бы воспроизводимым test-case'ом и годным к отправке багрепорта.

    15.09.2021: некоторые результаты за прошедшее время:

    • Да нет, ЕДИНСТВЕННОЙ программе никакого переполнения НЕ прилетает.
    • Зато она получает -- почти гарантированно -- ошибку -2. Видимо, Ctrl+Z приходится аккурат на время зависания в ожидании ответа. А зависание происходит ВНУТРИ ДРАЙВЕРА -- в обработке IOCTL_COMM -- и, видимо, на него (при нахождении в kernel space) Ctrl+Z тоже действует.

      Почему получается именно таймаут, а не просто пакет (который за это время гарантированно должен был прилететь) -- фиг знает, это надо отдельно разбираться.

    И некоторые идеи за то же прошедшее время:

    • Раз роляет именно ОТКРЫТИЕ линка -- может, накатать тест, который постоянно делает открывание?
    • И есть факт, что более ярко проявляется при "прямой" ловле прерываний -- которое, видимо, выражается в том, что вместо обычного поллинга 50 раз в секунду (фиг знает как привязанного к моменту самого прерывания) практически СРАЗУ выполняется 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(),
      • потом бредёт по маске (код копировался из a3818_hal.h) и если бит взведён, то...
      • ...в repcount-цикле делает CAENVME_IACKCycle(), а потом выполняет поиск полученного вектора по devlist[]'у и при находжении --
      • делает "тормознуть устройство" и "запустить устройство заново".
      • Плюс -- выполняет CAENVME_IRQEnable() 1 раз вначале и потом в конце тела цикла while(1) при условии, что полученная маска ненулевая.
    • А, да: единственное, что указывается в командной строке -- номер линка (обычно 0).
    • Замечание: предполагается, что работа по режиму A32D32 (да, AM в этом смысле избыточен).
    • Для наглядности сделана печать ("бегущая", при помощи '\r') времени, номера устройства и количества полученных прерываний.

    Результаты тестов:

    • Просто запуск -- ничего и никак (сначала даже БЕЗ IRQEnable(), а потом с ним -- ничего не изменилось).
    • Затем было добавлено блочное чтение куска памяти -- просто впрямую, прямо с фиксированного смещения и фиксированного размера 512 слов.

      Тоже без толку.

    • ...но как только добавил использование интерфейса select() -- скопированное из caenvmelib_poll_test.c() -- так тут же начало срубаться!!!

      И точно те же способы пингования работают (и НЕ работают тоже).

    21.09.2021@утро: похоже, есть ДВЕ проблемы:

    1. Почему происходит таймаут и packet mismatch?
    2. Как восстановиться (чтоб IRQ стали снова приходить?

    21.09.2021: продолжаем разбирательство:

    • @лесок-около-стадиона-НГУ, по дороге к моим ~17:00: после того, как вчерашний тест со ВКЛЮЧЕННЫМ IRQCheck() отработал больше часа без сбоев: а может, проблема в том, что при USE320=YES в a3818_hal.h НЕТУ IRQCheck()'ов? А они, может, работают как "маркеры синхронизации" (по оптике)?

      Тогда вариантом стресс-теста может быть не IRQCheck() в while(1), а IACKCycle()?

      @вечер: неа, этот вызов ЛОКАЛЬНЫЙ, просто чтение регистра из PCIe-карточки, БЕЗ работы с оптикой...

    • @у родителей, ~17:30: обнаружилось, что таки срубилось. Но аж через 2 часа!!! И, похоже, НЕ восстановилось...

      Впрочем, в свете гипотезы "ДВЕ разных проблемы" всё выглядит разумно: срубается РЕЖЕ, а как восстановиться -- пока не придумано, но надо бы.

    22.09.2021: провёл ещё некоторое количество тестовых запусков -- причём длинных, и результаты таковы:

    • Наличие или отсутствие IRQCheck()'а, похоже, НЕ влияет.
    • Как и наличие или отсутствие BLT-чтения.
    • А впечатление такое, что влияют какие-то совершенно посторонние факторы: загрузка процессора, что ли?
      • Одно событие в логах ровно в ту же секунду, что запускалась какая-то системная служба.
      • Да и сервер ведь постоянно ещё взаимодействием с клиентами занимается...

    23.09.2021: думаем дальше...

    • А ведь у нас механизм, реализующий poll() -- точно тот же, что используется и для CAENVME_IRQWait().

      Ну так и использовать в стресс-тесте оный Wait()!

    • Сказано -- сделано: CAENVME_IRQWait() вставлен в #if перед/вместо select()'а.
    • Ура-ура-ура!!! Да, срубается примерно так же, как при работе через select().
    • НА ВИД, при НАЛИЧИИ IRQCheck()'а работает и не рубится долго. При отсутствии -- рубится быстрее. И работает ровно тот же способ "ping"'а.

      Собственно, при ОТСУТСТВИИ -- в пределах часа-двух. А при наличии -- проработало полсуток и хоть бы хны.

    24.09.2021: надо дотестировывать и писать Паоле с test-case'ом.

    • BLT-чтение тоже параметризовано: в devtype_p добавлены поля blt_read_count и blt_read_count, и чтение выполняется в случае, если первый параметр >0.
    • Также прямо в эту утилиту добавил "пингование": если указан argv[2], то он рассматривается как адрес, по которому выполняется 1 чтение слова и программа завершается.
    • ...и вот с пингованием ОЧЕНЬ странно:
      • оказалось, что достаточно просто ОТКРЫВАНИЯ линии, БЕЗ всякого обмена. И даже CAENVME_BoardFWRelease() (который лазит в крейт-контроллер) закомментил -- всё равно работа возобновляется.
      • А команда "sleep 1 <>/dev/a3818_0" -- НЕ помогает.
      • Видимо, открывание линии выполняет какие-то обмены (оно ведь подвисает на отсутствующих "дальних" крейтах и выдаёт ошибку при отключенном 0-м).
    • Надо теперь делать test_CAENVME_IRQWait.c -- stress-test-case, который можно отправить итальянцам.

    26.09.2021: ещё некоторое количество тестов и разбирательств:

    • Добавлен вызов CAENVME_IRQCheck() в ветку "TIMEOUT". Результат предсказуем: ну да, печатается irq_mask=16 (т.е., 0x10, что и есть IRQ5).

      Т.е., Wait() висит, несмотря на наличествующее IRQ.

    • Чисто логические рассуждения:
      • Такое впечатление, что IRQWait, судя по дравйерной реализации IOCTL_IRQ_WAIT, реагирует только на ФРОНТ, а вот на УРОВЕНЬ -- НЕТ: там есть только wait_event_interruptible_timeout(), БЕЗ какой-либо проверки текущего статуса (каковая обязательно производится при использовании select()/poll(), первым же шагом).

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

      • Вероятно, предполагается, что софтина будет сначала проверять наличие Check()'ом, а если нет, то уже зависать на Wait()'е.

      Но такой алгоритм не может работать надёжно в принципе: из-за неатомарности операции "проверка, зависание" между её частями есть зазор, в который если вклинится прерывание, то оно останется необработанным на всё время висения.

    • Для выяснения, что же именно происходит при открытии линии, сделана крохотная test_CAENVME_Init.c.
      • Да, ПРОСТО CAENVME_Init() оказалось ДОСТАТОЧНО! БЕЗ каких-либо операций В/В или даже задержек между открытием и закрытием!
      • Судя по strace/ltrace, сразу после open() выполняется 1-2 ioctl()'а. "1-2" -- потому, что...
        • CAENVMElib-2.50 -- 2 штуки.
        • CAENVMElib-3.2.0 -- 1 штука. 27.09.2021: неа -- тоже 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 соответственно.

      • ...а в CAENVMElib.c::CAENVME_Init() после открывания устройства явно значится
        CAENVME_WriteRegister(i, cvVMEIRQEnaReg, 0xFF)
      • ...правда, там есть ещё CAENVME_GetFIFOMode(), должная приводить к _CAENVMEReadREG(), которая в конечном итоге должна приводить к IOCTL_COMM, которого почему-то в логе трассировки НЕ видно... 27.09.2021: видно-видно -- это я вчера невнимателен был.

        Кстати -- а не на нём ли как раз и происходит ошибка на отсутствующих/невключенных крейтах?

        27.09.2021: да -- именно на нём; и при указании отсутствующего крейта именно на ПЕРВОМ ioctl()'е подвисание на 3 секунды (3 секунды -- 6 попыток (1 основная и 5 retry'ев), таймаут 500мс, 6*500мс=3с)..

    • @засыпая: а может, просто взять да вставить оную операцию в реакцию на восстановление связи?

      Возможно, это и есть то самое "заклинание", которое я безуспешно искал с начала января, которое бы восстанавливало нормальную работу.

      И если "да", то даже если от CAEN'овцев не удастся добиться исправления общего идеологического косяка с ловлей прерываний, то для НАШИХ задач решение будет достаточным.

    27.09.2021: далее...

    • Проверено, что попытка открытия другого крейта на том же линке НЕ развешивает (но того, 1-го, крейта НЕ было).
    • Сделал test_CAENVME_IRQWait.c и накатал длиннющее письмо с объяснениями CAEN'у.
    • Ещё пара результатов:
      1. Добавил в реакцию по таймауту вызов CAENVME_GetFIFOMode() -- результат нулевой, НЕ размораживается.
      2. Проверил "скорость реакции на лечение" -- ожидание по таймауту 10с прерывается СРАЗУ по открытию линии из соседней консоли, НЕ дожидаясь истечения этих 10с.
    • ГЛАВНОЕ: добавил то "заклинание" CAENVME_WriteRegister(i, cvVMEIRQEnaReg, 0xFF) в test_CAENVME_adc250s.c'шную реакцию на таймаут... и ДА, ЭТО СРАБОТАЛО!!!

      Теперь оно сразу же восстанавливается и продолжает молотить дальше!

    • После чего добавил его же и в a3818_hal.h::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, то портируем в него. И пробуем с ним...

    • Однако, всё же всё далеко не так радужно и безоблачно: изредка -- раз в полчаса-час-полтора -- бывает Timeout/recovered, но БЕЗ проблем по bytes_to_copy/ndata_app_dma_in (т.е., без избыточно длинных пакетов; что, впрочем, не говорит об отсутствии путаницы -- неправильные пакеты могут быть слишком короткими).

      01.10.2021: ещё немного статистики вдогонку:

      • Как-то оно неравномерно: с 16:24 по 23:49 не было ни единого сбоя, а потом возобновились, раз в несколько часов ("несколько" -- то 1 час, то 3, то 4).
      • Плюс, пару раз всё-таки было
        Link 0 ndata_app_dma_in=3584 > comm.in_count=2048
      • На всякий случай запускаем аналогичный тест с версией 1.6.4.poll.

        ...а дальше надо будет с 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. Почему -- фиг знает, но вряд ли совпадение. .

      Откуда напрашивается вывод: всё же дело не в МОЁМ вмешательстве, а в том, что там как-то очень уж тонко настроенная диаграмма обмена пакетами (прям совсем-совсем "на тоненькую"): просто добавление некоторого действия -- оживления дополнительной очереди -- ломает-таки этот обмен.

    • Также ещё утром пришла в голову мысля, что можно написать CAEN'овцам по обоим тикетам (#1043523 про ndata_app_dma_in и #1043704 про путаницу пакетов): что в принципе проблему "приходят какие-то неправильные пакеты и программа пытается SIGSEGV'нуться" можно воспроизвести прямо почти мгновенно, но в не вполне "штатных" условиях:
      1. Запустить программу, нажать ей Ctrl+C, запустить снова.

        02.10.2021: а лучше -- скриптик сделать, чтоб в цикле -запускал, через некоторое время "kill INT", и снова б запускал.

      2. Выключить питание крейта и снова включить.

      Надо бы оба способа хорошенько попроверять.

    02.10.2021: попробовал выключить крейт и потом включить. "Душераздирающее зрелище!". А именно:

    • НИКАКОГО уведомления о восстановлении связи не было: ни в /var/log/messages, ни (как следствие) A3818_CONNECTION_LOST/A3818_CONNECTION_RECOVERED.
    • Ошибки никакой также НЕ БЫЛО.
    • Далее о восстановлении работы, уже в ручном режиме:
      • Сначала -- попытка "рестартовать" драйверы записью ._devstate=0.

        Фиг -- НЕ помогло.

      • Видимо, связь совсем "гикнулась" и её нужно "пнуть".
      • Окей -- сделано сначала полное отключение ОБОИХ (._devstate=-1), а потом уже включение обоих.

        Смысл -- чтоб layer освободил бы линию и закрыл бы её, а потом открыл бы заново.

        Вот ЭТО -- ПОМОГЛО.

    Выводы из этого эксперимента:

    • Причина отсутствия каких-либо действий от сервера/драйверов -- видимо, в том, что драйвер запрограммировал устройство и после этого ждёт уведомления о прерывании, которое никогда не произойдёт потому, что именно в момент после программирования и ДО уведомления крейт был выключен.
    • Надо попробовать повторить не с сервером, а с a3818_test, которому скормить цепочку команд чтения с паузами по 10 секунд (":10000000"), и уже там посмотреть что будет при выключении во время такой паузы.
    • ...для чего в vme_test_common.c надо добавить ключик "игнорировать ошибки", чтоб цепочка не прерывалась по cvCommError/cvTimeoutError.

      Назвать его, видимо, "-k" -- по аналогии с make, "keep going" (мой вариант -- "Kontinue after errors" :D).

      @22:00: сделано.

    • Явно требуется возможность "принудительно дёргать сброс/восстановление" -- чтоб a3818_hal хотя бы использовал бы то "заклинание" с размаскированием прерываний.

      Для чего надо реализовать какой-то метод в API vme_hal и vme_lyr.

      ...о чём на границе 2019/2020 вроде бы уже были мысли. Чуть позже: ага -- 17-01-2020, метод hal_ioctl().

    • @~21:00, после просмотра "Фемида видит" s1e09 и начала s1e01 "Stumptown": но ведь просто "метод" -- довольно гадко: потребуется РУЧНОЕ вмешательство, после РУЧНОГО же диагностирования.

      А можно сделать АВТОМАТИЧЕСКОЕ ВОССТАНОВЛЕНИЕ:

      • Специальный драйвер "a3818_keepalive_drv.c", без каналов, который...
      • Раз в сколько-то секунд пытается выполнить чтение по указанному адресу (сам адрес может быть немаппированным), ...
      • ...и если в результате -- "ошибка связи" (точнее, !=cvSuccess && != cvBusError), то выполнить "reset".

      02.10.2021@~22:00: а можно и сделать 1 канал записи -- принудительный "reset".

    • 03.10.2021@утро: но ведь просто «выполнить "reset"» -- МАЛО, надо ещё все драйверы уведомить, чтобы они ре-инициализировали бы устройства...
      • Для adc250_drv.c (и прочих осциллографичностей) процедура "ре-инициализации" несложна -- можно обойтись "записью stop:=2" ("невидимый STOP") при условии, что measuring_now==MEASURING_YES.
      • ...инфраструктура-то для уведомления драйверов есть -- метод stat_proc -- но хоть какое-то её использование отсутствует.
      • А вообще всё это -- море трудозатрат. Точно ли игра стоит свеч?
      • Можно ведь вообще обойтись одним экземпляром mux_write, который по приходу в него команды -- от кнопки на экране клиента -- будет делать запись ._devstate=0 указанному списку, тем самым рестартуя драйверы

        ...хотя это "забудет" все сделанные настройки, так ведь?

        @позже: но можно же из mux_write'а (или лучше из formula?) вместо _devstate:=0 обойтись как раз stop:=2, сразу после ре-инициализации, которую делать через специальный драйвер, имеющий единственный командный канал записи.

    03.10.2021: провёл оба теста на тему "как бы получить SIGSEGV/путаницу пакетов путём создания нештатных ситуаций":

    1. "Запустить программу, нажать ей Ctrl+C, запустить снова", с помощью скрипта: ДА, РЕЗУЛЬТАТ ЕСТЬ!
      • Результат получен при помощи "интерактивного скрипта"
        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
        
      • Как бы то ни было, с драйвером версии A3818Drv-1.6.4.in_count_CHECKED (т.е., БЕЗ поддержки 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
        

        Т.е.,

        1. Числа ВСЕГДА одинаковые -- 2050 против 2.
        2. Сообщения идут ПАРАМИ.
        3. 04.10.2021: кстати -- а ведь сплошь путание пакетов, но практически БЕЗ таймаутов/восстановлений!
      • ...а с A3818Drv-1.6.6.orig просто падало по SIGSEGV'у (и в /var/log/messages валились сообщения от abrt.
    2. "Выключить питание крейта и снова включить", "с a3818_test, которому скормить цепочку команд чтения с паузами по 10 секунд": ОБЛОМ.
      • При выключении питания и последующем включении ВООБЩЕ НИЧЕГО НЕ ПРОИСХОДИТ: не присылается НИКАКОГО уведомления; по крайней мере, в логи НИЧЕГО не попадает.
      • Попытка выполнить VME-операцию при выключенном питании приводит к cvCommError=-2.
      • Операция после включения крейта просто выполняется, без каких-либо сообщений о таймаутах/восстановлении.

      Такое впечатление, что CAEN'овцы вообще не задумывались о возможных проблемах потери связи и восстановлении.

      Ну и подумать бы ещё -- вдруг можно ещё какой-то тест придумать, где бы всё таки что-нибудь бы вылезло... А если отрубать питание ВО ВРЕМЯ исполнения? КАК?

    09.10.2021: ещё типа "результаты наблюдений за стабильностью":

    • 5-го числа оставлял просто "пахать круглосуточно" -- с тех пор НИ ОДНОГО сообщения о сбое/таймауте.
    • А вот сегодня, обрадовавшись стабильности, начал экспериментировать с проверкой свежедоделанной фичи "RUN_MODE", и-и-и... -- ТУЧА сообщений: как просто о таймаутах, так и о путании пакетов. Причём, что любопытно:
      1. Сообщения о путании пакетов -- все с одинаковыми числами (3584 против 2048).
      2. Изрядное число сообщений -- ПАЧКАМИ, по паре-тройке, друг за дружкой с расстоянием в секунду; в основном "пачки" состоят из 2 пар, но есть и более длинные цепочки, вроде такой:
        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".

    • То, что 1-й линк -- неизбежно, т.к. сейчас 4 осциллографа висят именно на нём, а на 0-м один-единственный, причём неиспользуемый.
    • Случилось это со СТАРЫМ драйвером A3818Drv-1.6.4.new-in_count_CHECKED), который с общим intr_wait[], а не с отдельным slct_wait[].
    • ...и прикол в том, что целых 4 дня гоняний (с 29-10-2021) никаких ошибок не вылазило, а вот сегодня -- пожалуйста!!! Возможно, потому, что...
    • ...это было с МОДИФИЦИРОВАННЫМ a3818_hal.c::irq_fd_p(), когда он пытается сначала добыть все доступные вектора в буфер, а потом уже идти по этому буферу и вызывать соответствующие обработчики.
    • Во время теста "прогон 15 комбинаций" -- когда запускаются разные паттерны генерации прерываний (разными комбинациями устройств).

    ...сопоставить бы времена возникновения ошибок с тем, какие паттерны отрабатывались в то время (выдержка и тут тоже закомменчена, на этот раз длиннющая, 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'ы таковы:

    • 2+2, когда 1-й сбоит --
      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
      
    • 0+4, когда 1-й работает спокойно --
      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
      
    (это выдержки критичных строк из test-devlist-b360mc-adc4x250.lst).

    Сбои в подавляющем числе имеют вид

    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 -- так начинаются проблемы.

    Точнее, даже так:

    • Отключаем 1 из 4, так что остаются 3 -- всё OK.
    • Отключаем 2-й, так что остаются 2 -- начинаются проблемы.
    • Включаем 2-й обратно, так что опять 3 -- проблемы прекращаются.

    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
    

    Проверил получше:

    • Нет, НЕ намертво. Обращения к нему через cdaclient работают.

      Значит, просто перестают приходить IRQ.

    • Сделал localhost:1.{d5,d6,i6,i8}.devstate=-1, а потом =+1 -- заработало обратно.
    • Что странно: сделал -1,+1 конкретно только i8 -- и заработал только он!
    • А вот остальным можно делать -1,+1 -- неа, они не восстанавливаются.

      Вероятно, потому, что они, в отличие от i8, НЕ в начальном состоянии.

    • И ещё: в какой-то момент было сообщение
      vme_lyr_irq_cb() unknown level=5.vector=0
      

    Похоже, это какой-то более общий косяк в работе IRQ -- где-то что-то залипает и они перестают работать корректно. Возможно, оно и на BIVME2 воспроизведётся.

  • 01.10.2021: при отладке работы IRQ на A3818 на примере 4 штук ADC250 (контекст см. в разделе по a3818 за сегодня) было обнаружено странное поведение: попытка считать b360mc:1.i{6,8}._devstate с x10sae отрабатывала очень долго -- результат печатается аж через 3 секунды после запуска (а чтение 4 штук при всех 4 запущенных -- и вовсе 8 секунд).

    Разбирательство показало, что дело, видимо, в repcount'е: оно ж вычитывало по 100 осциллограмм с каждой линии -- а это море работы, вот и возникали такие тормоза. При торможении всех устройств (_devstate=-1) чтение стало мгновенным.

    ...да и вообще что-то маловато срабатываний в секунду при чтении через сервер -- test_CAEN_IRQWait молотит намного резвее. Стоит поразбираться в причинах.

    10.11.2021: этот пункт перетащен из "cxsd_hw" (где он первоначально обсуждался) в "a3818". Т.к. изначально предполагалось, что проблема в сервере или cxscheduler'е, но 21-10-2021 стало кристалльно ясно, что причина -- исключительно в VME (при repcount-повторах до 100 штук оно ОЧЕНЬ долго сидит в том цикле).

    02.10.2021: да, разбираться, примерно так:

    1. Попробовать уменьшить количество repcount-повторов и посмотреть на результат.
    2. Добавить в sl_main_loop() диагностику:
      1. По завершению select()'а.
      2. По каждому cb-вызову.
      -- это покажет "картину" того, как именно происходят события.

    18.10.2021@утро-душ: некоторые размышления:

    • Возможно, что проблема частично обусловлена отладочной печатью в FASTADC_IRQ_P() -- там сейчас КАЖДОЕ прерывание логгируется на stderr "бегущим числом" при помощи '\r'.

      А если закомментировать?

    • Как считать скорость -- конкретно в ЭТИХ условиях ЭТОГО тестирования: чем вручную смотреть счётчик, засекать время, потом опять смотреть счётчик,
      • Можно воспользоваться тем, что счётчики и так пишутся в файлы (см. 20210106-VME-POLL-STRESS-TEST.txt):
        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: проверяем. Краткие результаты первых тестов "на вменяемость":

    • Тестировалось при молотящих всех 4.
    • Числа срабатываний D5 и D6 всегда получаются совпадающими.
    • При задержке 100 они были при 1-м прогоне 4472, при 2-м прогоне 4457. Т.е., разница меньше 1%.
    • При задержке 1000 результат 44650. Т.е., опять же совпадает -- те же примерно 44.5 срабатываний 1 штуки в секунду. Так что спокойно можно мерять на 100 секундах задержки -- результаты вполне достоверные.
    • А вот на задержке 10 секунд результаты уже менее стабильные -- 437 449 450 450 440.

    Так что будем мерять на задержке 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
    

    ОСТАЛЬНЫЕ ВАРИАНТЫ ПРОВЕРИТЬ!!!

    Некоторые дополнительные комментарии:

    • Загадка "почему вариант с 3 работает так же, как и с 2?":
      • При натравливании "cdaclient -mDn @i:{i6,i8}.marker" обнаружилась любопытная картинка:
        1. Когда активен всего 1 осциллограф на линке, то происходит 1 срабатывание, после чего длительная пауза (занимается другим линком?).
        2. Когда же активно 2 осциллографа, то прилетает пачка событий в 100 за ~800мс (а разница между timestamp'ами ~8мс), после чего видимая глазом пауза в ~325мс, а потом опять пачка.

        (Для количественных оценок измерения клались в файл, командой "cdaclient -mDn @i:{i6,i8}.marker -T100 -o /tmp/i.log".)

        Видимо, дело в том, что

        1. Если активен всего 1, то после получения IRQ он программируется заново, но к моменту следующей repcount-проверки (практически сразу!) CAENVME_IRQCheck()'ом он ещё не успевает прислать.
        2. Когда же активно 2, то пока после обработки первого идёт работа со вторым, то первый уже успевает снова отработать и прислать следующее событие, которое и подхватывается и так они и летят чередуясь, до достижения предела repcount=100.

        Но всё равно неясно, почему длина пачки 0.8с, а промежуток между ними всего 0.3с...

      • ...поэтому выполняем аналогичный тест с оценкой скорости срабатывания уже осциллографов на 0-й линии -- d5 и d6. И тут нас ждёт любопытный сюрприз:
        • Вся пачка из 100 измерений на 0-й линии действительно укладывается в 319мс, а расстояние между пачками -- 801мс.
        • Но ещё любопытнее время между самими измерениями: между d5 и d6 -- 3-5мс (в основном по 4мс), а между d6 и d5 -- 2-3мс (в основном 2мс).
        • А ЕСЛИ ОСТАВИТЬ 1?

        И вот тут уже вопрос -- а почему между линиями такая разница? Потому, что это разные линии, или же потому, что в крейтах на этих линиях осциллографы с разными версиями прошивки (на 0-й -- старая 0x06, на 1-й -- новая 0x07). Перепрошивку выполнять неохота, как и тасовать осциллографы между крейтами; надо задать вопрос авторам.

        13:02: Антону Павленко вопрос мылом отправлен, ждём ответа.

        25.10.2021: Антон Павленко просто позвонил: ответ -- нет, они впрямую ChangeLog вроде бы не ведут; ВРОДЕ бы такого эффекта (замедления в новой прошивке) быть не должно, но фиг знает; Котову письмо форварднуто, но он пока молчит; так что мне -- welcome всё это потестировать.

    Итого:

    • Очевидно, что "проблема" -- не в cxscheduler'е, а в VME: и в самих девайсах, и в скорости работы VME-интерфейса, и в repcount-повторах числом 100 -- т.е., во всём, что приводит к временам исполнения irq_fd_p() по 800 и 300 миллисекунд.
    • Также понятно, почему обращения cdaclient'ом к простейшим каналам отрабатываются так долго: по (800мс+300мс)=1.1с оказывается задержка перед отработкой каждого CX-пакета, которых при подсоединении по нескольку штук (собственно соединение плюс запрос на канал).
    • И очевидно, что надо всё это обсуждение переносить из раздела "cxsd_hw" в раздел "a3818".
    • Единственный остающийся для исследования вопрос -- почему сервер "молотит" всё же на вид существенно медленнее, чем прямой опрос проверочными утилитами work/tests/test_CAEN*.

      Вот тут -- да, надо бы напихать отладочной печати с timestamp'ами в sl_main_loop(); может, какое озарение произойдёт.

    28.10.2021: давно стало ясно, что надо провести другую серию тестов, основанную исключительно на "cdaclient -T100" и с предварительным отключением остальных осциллографов (НЕ входящих в список бенчмаркируемых). Для этого используем следующую команду:

    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
    
    (Это "полный список" -- если считать каждый осциллограф за 1 бит, то получается 4-битное число, и прогоняются все комбинации, кроме 0.)

    Вот результаты:

    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
    

    В чём может быть причина и как это проверить?

    • Самая потенциально неприятная причина -- косячность 1-й линии по сравнению с 0-й.

      Тогда это аппаратная проблема, которую фиг исправишь.

      Как проверить -- переткнуть оптические линки, чтоб этот крейт стал 0-м.

    • Менее неприятный вариант, но в котором виноват могу быть я -- софтовый: что доступ к 1-му линку в инфраструктуре cxsd+cxscheduler+a3818_hal+vme_lyr почему-то медленнее.

      Как проверить? Да отключить нафиг 0-ю линию, закомментировав строчку со старым девайсом x6.

      Проверил -- один фиг, без изменений...

    31.10.2021: посмотрел внутрь самих логов -- там опять всякие странности, вроде

    • Отсутствия в наборе d5,d6,i6 срабатываний от d6 (хотя ДОЛЖНО б было: поскольку запрограммирован он был, то уж когда пришло (ДОшло!) прерывание от d5, то во время его обработки и d6 должен был бы успеть сработать и прислать своё прерывание, после чего должна была 100 раз молотить парочка d5,d6, но НЕТ...).
    • Сильно отличающимися результатами комбинаций d5,i6 и d6,i6 -- технически-то ситуации идентичны и должны б совпадать...
    • Не говоря уж про ОДИНОЧНО работающие d5 и d6, с разницей в полтора раза между результатами (а вот i6 и i8 -- одинаково...).

    @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
    
  • 02.11.2021: реализовал другой принцип вычитывания векторов: не "вычитать 1 вектор, обработать, потом вычитывать и обрабатывать следующие", а "вычитать (в буфер) до 100 векторов, а потом идти по этому буферу и обрабатывать" (см. обсуждение в общем разделе за вчера).

    Первый результат -- да, работает. Хотя паттерн (порядок, в котором получаем вектора от разных устройств) несколько неожиданный.

mvme3100:
  • 27.06.2021: создаём раздел.

    Тут будет всё про работу с MVME3100 -- и информация, и собственно описание драйвера (и кросс-компиляции тоже).

Информация:
  • 27.06.2021: лёд тронулся -- немного информации таки появилось.
    • Неделю назад опять спросил у Саши Сенченко на тему "как всё-таки вы работаете с MVME3100?". И на этот раз ответил более конкретно и даже прислал ссылку на файл у Торвальдса:
      • Что теперь вроде бы как всё-таки есть какой-то драйвер для доступа из userspace.
      • И что обращение к шине делается при помощи read() и write().

        А на мой вопрос "но как это возможно? ведь надо указывать АДРЕС?!" -- ответил, что адрес указывается lseek()'ом.

      • Ссылка -- https://github.com/torvalds/linux/blob/master/drivers/staging/vme/devices/vme_user.c

        Этот исходник есть даже в kernel-3.10.0-514.el7.x86_64.rpm от CentOS-7.3 (от ноября 2016-го).

      • И да, в исходнике видно, что чтение/запись выполняются именно через read()/write(), а установка адреса -- lseek()'ом.

        А сегодня увидел в документашке (см. ниже), что используют вызовы pread() и pwrite(), получающие дополнительно параметр offset и по эффекту включающие предварительное позиционирование.

      • Чего оттуда никак НЕ понятно:
        1. Как работать с IRQ.

          Рядом там нет более НИКАКИХ файлов типа "vmei*"; только 4 штуки vme_pio2*.[ch], но они относятся к чему-то другому.

          И, в т.ч., нет ни слова про исполнение циклов IACK.

        2. Как именно работать с "окнами" -- тоже тёмный лес.

          Причём есть какие-то "master"- и "slave"-окна. Эту концепцию я встретил вообще первый раз.

          И также неясно, какие ограничения на РАЗМЕР окон.

        P.S. А вот что ясно -- что там НЕТ поддержки mmap() (хотя она концептуально явно напрашивается). 27.11.2021: ЕСТЬ, с 07-03-2015.

    • А сегодня я решил ещё погуглить на тему "vme_user" и наткнулся на сайт linux-vme.org, на котором есть немножко документации. Совсем-совсем немножко:
      • Kernel Drivers -- тут есть
        • Ссылка на "VME Device Drivers" -- описание VME-инфраструктуры ядра;
        • Краткое объяснение, как это всё работает, и в т.ч. битым текстом:
          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-mod (в текущий момент исходник от 2016 года), которая "exercises the read, write, interrupt and location monitor paths".

          Нет, это НЕ аналог нашего *vme_test -- он не позволяет выполнять произвольные действия (СОВСЕМ -- argc/argv[] не используются), а лишь производит некоторые фиксированные тесты.

          Но там есть обращение к прерываниям.

          01.07.2021: а-а-а, дошло -- какие ещё argv/argv[]! Это же модуль для ядра!

      • VME Utils.

        Описание утилиток vme_slave и vme_master.

        Ничего особо интересного (в т.ч. не объяснено, в чём разница; только по списку ключей у master'а есть "-w WIDTH" -- 8/16/32/64, data width).

        ...Кроме того, что им AM указывается НЕ впрямую, а комбинацией из 1) "супервизорскости:-S"/"непривилегированности:-N", 2) "данности:-D"/"программности:-P" и ширины доступа (16/24/32); видимо, оно само вычисляет AM.

      МОЁ впечатление:

      • Похоже, "стек VME" сделан аналогично SocketCAN'у -- разделяются
        1. драйверы железа (которые и образуют инфраструктуру для драйверов VME-железа в ядре, о существовании которой стало ясно ещё 22-02-2019);
        2. userspace-API, образуемый этим vme_user.c (коий "подцепляется" к созданной драйвером бриджа шине уже в качестве "клиента", и потому способен работать с РАЗНЫМИ бриджами).
      • Так что, возможно, что работа с IRQ является прерогативой просто чего-то другого -- то ли какого-то совсем отдельного драйвера, то ли вообще непосредственно драйверов конкретных VME-бриджей.

    28.06.2021: ещё:

    • письмо-ответ на вопрос "How to use vme_user module" за 12-04-2013, написанное Martyn Welch -- автором модуля vme_user.

      Там, в частности, приведён пример простенькой тестовой программки. Это тот же самый код, что на linux-vme.org.

    03.07.2021: насчёт "что такое эти master- и slave-окна":

    • Нагуглился Usenet-thread в comp.os.vxworks от июня 2004: "VME Master/Slave Window Setup" -- всего один вопрос и один ответ. Формулировка прямо в вопросе -- по результатам чтения какой-то WindRiver'овской документации:

      - Master windows are mappings of local physical memory to VME memory.

      - Slave windows are mappings of VME memory to local physical memory.

    • А также это описано в том "VME Device Drivers":
      • Master windows:
        Master windows provide access from the local processor[s] out onto the VME bus.
      • Slave windows:
        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:

    • Поддержки ловли IRQ из userspace -- НЕТ. Женя Котов свою написал, и, возможно, был первым (больше никому не надо)?
    • В инфраструктуре IRQ для kernel-space -- бросающийся в глаза баг при "double-free".
    • Чтение/запись блоками больше 128Кбайт -- фиг.
    • Поддержка mmap() -- вроде как ЕСТЬ (и на том спасибо), но никаких комментариев к ней нет, как и примеров. 27.11.2021: появилась она 07-03-2015.
    • Куча значений параметров вообще никак не стандартизованы и в userspace-утилитах требуются самостоятельные определения: коды для address spaces (16/24/32/64:0x1,2,4,8), data widths (8/16/32/64:0x1,2,4,8).

      Здесь же "способ" кодирования 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-х, и с тех пор никому особо и не требовалось...

Кросс-сборка под MPC8540:
  • 27.06.2021: этот раздел создаём авансом -- писать в нём пока нечего.
mvme3100_hal.h:
  • 27.06.2021: и этот -- тоже авансом, в него также писать пока нечего.

    И замечание: поскольку реально это вроде как не только под этот контроллер, а под "API vme_user & Co." (в частности, и под MVME5100 должно бы подойти), так что корректнее было бы обозвать как-то по-другому. Но: а) непонятно, как именно ("vme_user_hal"?); б) скорее всего, никогда ничего не потребуется.

  • 27.06.2021@~17:00, воскресенье, зайдя в ИЯФ проверить на сварке в зале 14-2 связь с ВИПом: хотя одна идейка всё-таки напрашивается:

    для простоты реализации можно забить на "интеллектуальный" менеджмент окон, а сделать по-простому: при КАЖДОМ обращении к шине настраивать ОДНО окно (0-е) и делать обмен через него.

    Пара деталей:

    • Конечно, ТЕКУЩИЕ настройки надо помнить и при совпадении текущих с запрашиваемыми ничего не перенастраивать.
    • Даже если при работе с парой разнородных устройств будет постоянно дёргаться перенастройка, на это можно забить: ведь основные-то времязатратные операции -- ВЕКТОРНЫЕ, а на их накладные расходы на настройку должны стать пренебрежимо малыми.

      Плюс, вследствие ОДНОпоточной архитектуры все цепочки обращений к одному устройству -- вроде программирования в StartMeasurements() -- будут идти друг за дружкой и не требовать модификации окна.

    01.07.2021: в продолжение той же темы -- с технологией "помнить ТЕКУЩИЕ настройки и не перенастраивать при совпадении" есть один проблемный нюанс: РАЗМЕР окна. В нашем нынешнем API "vme_hal" он никак не фигурирует (можно только считать, что для скалярного доступа он равен 4 байтам, а для векторного -- размеру вектора).

    Много думал вчера и сегодня, и напрашиваются 2 варианта:

    1. @(вчера): можно указывать СТАТИЧЕСКУЮ карту маппирования окон, в layerinfo.

      Что-то типа "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.

      А в момент чтения/записи делается поиск по аллокированным окнам и используется первое, подходящее по ширине адреса и модификатору плюс вмещающее весь блок.

      02.07.2021@~13:00, около Ильича 1 по дороге от родителей: засада -- ведь информацию эта нужна HAL-МОДУЛЮ, а layerinfo получает LAYER. Что делать?

      Ну, очевидно -- вводить какой-то "протокол/API":

      1. Опциональная функция "заюзать информацию" (должна вызываться где-то рядом с "открытием шины").
      2. Какой-нибудь typedef struct {} ЧТО_ТО_t, в который может парситься эта информация.
      3. #define-символ, содержащий PSP-таблицу, соответствующую тому типу.

      Т.о.,

      1. В vme_lyr_common.c проверять #ifdef'ом, что если тот символ определён, то 1) объявить переменную (или поле в privrec_t?) того типа; 2) выполнять парсинг из layerinfo; 3) вызвать ту функцию.
      2. В vme_test_common.c при наличии того символа можно поддерживать ключик "-C bus_config", которому указывать ту же информацию.

        ...хотя это реально излишне -- там всё равно указывается ОДИН тип доступа (ADDRESS_SIZE:ADDRESS_MODIFIER).

        22.11.2021: а вот нифига НЕ излишне: чтоб mvme3100_test мог использоваться в параллель с живым v4mvme3100vmeserver'ом, не портя тому егойные окна. Для чего в идеале надо серверу давать окна 0,1,2 (для A16,A24,A32), а test'у всегда оставшееся окно 3 (ему этого одного всегда хватит).

      05.12.2021@~17:30, сидя у папы на кухне за столом: зачем так сложно -- «функция "заюзать", typedef, #define-символ...»?! Можно же проще и даже примитивнее:
      1. В этом #define-символе и указывать сразу имя функции, так что если символ определён, то
        1. Разрешать поддержку layerinfo и ключа "-C bus_config";
        2. вызывать функцию по этому символу.
      2. Считать, что функция делает ТОЛЬКО psp_parse*(), так что при возврате ею значения <0 считать, что ошибку вернёт psp_error().
    2. @(сегодня): использовать всё-таки ОДНО окно, помня текущие настройки и переаставляя при неподходящести, но:
      • ВСЕГДА маппировать максимально возможный размер окна (определяется чипом бриджа).
      • Проверять, что если требуемая операция попадает ВНУТРЬ ТЕКУЩЕГО настроенного окна -- то выполнять прямо через него (с соответствующим смещением базового адреса).
      • Таким образом, первое обращение к устройству по "максимально близкому к базе" адресу сразу выставит окно, годное и для всех последующих обращений.
      • И более того: если ВСЕ устройства суммарно помещаются в размер окна, то обращение к устройству с самым младшим адресом сразу же выставит окно, годное для ВСЕХ устройств.
      • А можно ещё ставить начало окна не на указанный адрес, а "квантовать" -- "округлять вниз" до некоего размера страницы (возможно, прямо до максимального размера окна).

        Это устранит необходимость начального обращения по "максимально близкому к базе" адресу, а возможно -- при соответствующем выборе адресов устройств -- и первого обращения к устройству с самым младшим адресом (поскольку для ВСЕХ устройств "квантованное" начало окна будет общим).

    3. Вариант: возможно, для разных ширин адреса можно использовать просто разные окна: 0:A32, 1:A24, 2:A16.

      И весьма вероятно, что даже для 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@дома, детальнее посмотревши синтаксис в тех утилитах: неа, не выйдет, т.к.:

    1. Синтаксис DEVSPEC'ев всё же неидеально подходит, поскольку
      1. В них отсутствует требуемый РАЗМЕР окна.
      2. В них есть иная информация, HAL'у нафиг не нужная: у обоих разрешается указание шины ([@BUS_MJR[/BUS_MNR]:]), а у firmware ещё и EPCS_OFFS обязателен.
    2. Для одно-оконного варианта, с которым это всё должно быть совместимо, не катит никак -- там ведь предполагается синтаксис "win=N".

      Или определять VME_HAL_DEFAULT_BUS_CONFIG_FOR_VMETEST как раз ТОЛЬКО для ОДНО-оконного варианта, и если он defined(), то использовать его, а если нет -- то уже DEVSPEC?

    Так что, возможно, придётся как-то самостоятельно генерить строку из РЕЗУЛЬТАТА парсинга DEVSPEC'а. ...или прямо API сделать принимающим числа, а не строку?

    07.12.2021@утро, просыпаясь и душ: А не добавить ли ещё один вариант конфигурации для МНОГОоконности: "считай ТЕКУЩУЮ конфигурацию из бриджа"?

    • Тут предполагается, что настройка выполняется стандартными средствами -- vme_master -- при загрузке контроллера.
    • ...но также это позволит mvme3100_test'у адаптироваться к настройкам, сделанным v4mvme3100vmeserver'ом -- у которого уже будет какая-то специфицированная карта.
    • И вот для ТАКОГО варианта уже вполне имеют смысл определения VME_HAL_DEFAULT_BUS_CONFIG_FOR_*: в них и будет говориться "используй текущие настройки".

    Чуток конкретики:

    • Синтаксис -- очевидно, "winN=?";
    • ...при таком указании куда-то должно будет складироваться некое "магическое число" -- например, поле mN=-1 (оно всё равно в многооконном режиме не используется).

      09:30: а, нет -- =0: при этом как раз и получится, что автоконфигурация будет работать АВТОМАТОМ.

    • А vme_hal_init() должна проверять, что если затребован такой режим, то вместо конфигурирования выполнить чтение и заполнить все cur_*.

      ...да, конкретно cur_am будет той ещё засадой -- надо генерить AM из "cycle", "address width" и "data width".

    • В парсинге же не забывать при "прямом" указании ставить в mN какое-нибудь другое значение.
  • 14.11.2021: раз уж зуд, то изготавливаем прототип -- путём копирования bivme2_hal.h, чтоб потом его подпеределать.

    Единственное пока изменение -- открывается "/dev/vmeli%d" вместо "/dev/vmei%d".

    22.11.2021: дошли руки пилить дальше; точнее -- сформировалось понимание, как именно лучше сделать для достижения максимального эффекта минимальными усилиями.

    Выбрана следующая концепция:

    • Настройка окна будет выполняться перед КАЖДОЙ операцией обращения.
      • Для чего каждая vme_hal_a##AS##xx##TS[##v]() будет начинаться с макро-вызова CHECK_AND_SETUP_VME_WINDOW()...
      • ...которая проверит параметры окна -- AM и базу.
      • А при инициализации будет делаться AM:=-1, чтобы первое же обращение произвело настройку.

      Т.е., все "мозги" для "балансировки" окон будут именно в CHECK_AND_SETUP_VME_WINDOW(); предполагается, что она должна реализовать алгоритм "постараемся настроить окно так, чтобы оно подходило для всех обращений".

    • Т.е., ВСЕ однотипные обращения будут идти ВСЕГДА ЧЕРЕЗ ОДНО ОКНО.

      ("Однотипные" -- с конкретной шириной адреса.)

    • О номере используемого окна:
      • По умолчанию будет использоваться окно номер 0 (/dev/bus/vme/m0?).
      • Но можно будет указать другой номер в "параметрах" -- layerinfo либо в ключике "-C bus_config".

        ...это полезно, например, чтобы при живом v4mvme3100vmeserver'е мочь обращаться и mvme3100_test'ом -- указав ему другой номер окна.

      • Причём чтоб можно было РАЗДЕЛЬНО указывать окна для разных режимов адресации -- для A16, A24, A32.
    • Т.е., БУДЕТ реализована инфраструктура передачи HAL-модулю параметров из layerinfo или ключа "-C bus_config".

    22.11.2021@~18:50-~19:00 по дороге с Морского домой: весь день думал-думал, как же всё-таки сделать "чтоб можно было РАЗДЕЛЬНО указывать окна для разных режимов адресации" -- что-то с этим очень туго:

    • Сама реализация получается чем-то похожа на указание логгинга в cxlogger/сервере: хочется ведь мочь указывать весьма вразброс, и чтобы при этом открывались ровно "какие надо" окна.
    • А "вразброс" -- это весьма нетривиальный набор вариантов:
      • default=m0 -- ВСЕ работают через m0.
      • a32=m0 -- указывается ТОЛЬКО для A32.
      • default=m0,a24=m2 -- конкретно A24 через m2, а остальные (A16 и A32) через m0.
      • a16=m0,a24=m2,a32=m2 -- поштучное исчерпывающее указание для всех 3 режимов.
      • ????????
    • И все они должны "работать как надо" -- т.е., с одной стороны, при указании "a24=m2" должно бы открываться ТОЛЬКО m2, а с другой, и другие режимы адресации должны мочь рботать, но как, если файл открыт ТОЛЬКО для A24?

    Так что -- ну его нафиг, пусть пока всегда работает с ОДНИМ окном. А если всё-таки придумаем, как красиво сделать гибкий набор (и/или это реально занадобится) -- тогда и реализуем/проапгрейдим.

    @Коптюга, тротуар между Терешковой-2 и Коптюга-19: и отдельная идейка о том, как бы сделать, чтобы v4mvme3100vmeserver и mvme3100_test'ом уже ПО УМОЛЧАНИЮ использовали бы разные окна (чтоб не требовалось обязательно что-то конфигурить):

    • В дополнение к (см. 02-07-2021) "протокол/API" сделать ещё пару #define-символов, определяемых HAL-модулем при наличии этого API -- например, VME_HAL_DEFAULT_BUS_CONFIG_FOR_VMESERVER и VME_HAL_DEFAULT_BUS_CONFIG_FOR_VMETEST, в которых указывать окна 0 и 3 соответственно.
    • А юзеры HAL'а пусть при НЕУКАЗАННОСТИ layerinfo или ключа -C всё же выполняют psp_parse(), но только уже из соответствующего умолчания.

    @дома, ~19:30, когда записывал вышеперечисленное: определился с названием для всей этой концепции/API: "bus_config". И все связанные вещи должны содержать это буквосочетание.

    23.11.2021: итак -- всё "специфично-bivme2'шное" вычищено (вроде за-#if0'енного второго комплекта операций В/В, БЕЗ поддержки векторности через vect_io), пора уже ДЕЛАТЬ.

    Сначала -- A BIG FAT NOTE:

    • Формально-то тут у нас есть понятие "шины" -- даже инфраструктура ядра рассчитывает на множественность "шин"/чипов.
    • Но конкретно MVME3100 -- точно нет (ибо незачем), и конкретно в котовском vmei.c есть проверка, что если номер шины больше 1 (а чё не 0?), то он ругается, что не может.
    • И ФОРМАЛЬНО-то всю "инициализацию" в виде открытия шины надо бы выполнять в vme_hal_open_bus(), а не в vme_hal_init().
    • Но реально смысла нет: это лишь увеличит сложность кода (лишняя маета с bus_handle плюс лишние проверки), а ПРАКТИЧЕСКОЙ выгоды -- никакой.
    • Так что сделаем всё же в точности по модели BIVME2 -- как будто понятия "идентификатор шины" и нет вовсе.
    • @~11:20, пешком по лестнице вниз чистить машины от снега: Кстати, по ИЗНАЧАЛЬНОЙ сути VME никакой "шины" и не должно быть: смысл VME как раз в простоте -- для обращения к устройству там нужен только адрес, т.е., достаточно вообще указателя.

      А требование добавления этих AM, шины и т.п. -- фактически превращает VME чуть ли не в какой-то CAMAC.

    Приступаем...

    Пара аспектов, обнаруженных в процессе:

    1. В операциях чтения/записи по pread()/pwrite() есть ограничение по размеру -- 0x20000, т.е., 128Кбайт. Вот как это выяснилось:
      • В исходнике vme_user.c видно, что чтение выполняется в vme_user_read().
      • А она для master-окон сводится к вызову 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, определённым как
        #define PCI_BUF_SIZE  0x20000	/* Size of one slave image buffer */
        
        (причём тут слово "slave" -- неясно).
      • Через этот kern_buf выполняются ВСЕ пересылки -- и чтение и запись, и с master-окнами и со slave-окнами.
      • Радость в том, что возвращается в результате объём РЕАЛЬНО СКОПИРОВАННОГО -- т.е., при превышении объёма буфера возвращается именно объём буфера.

      Итого -- в векторных операциях надо вместо простого одиночного вызова делать цикл flush-типа пока всё не прочитается/запишется.

      @~12:30 в лесу у стадиона: а почему, собственно, этот цикл должен ЮЗЕР организовывать?! Почему сам vme_user.c его не делает?!

    2. Отдельный вопрос -- где взять файл vme_user.h, требуемый для "предварительной-тестовой" компиляции под обычной x86_64? Просто так его нет. НИГДЕ В ДИСТРИБУТИВЕ.
    3. 26.11.2021: это изучено и записано позже, но весь пункт "зеленить" не будем: проблема даже шире, чем "где взять файл vme_user.h": там реально есть толпа определений, которые userspace-программам вообще никак недоступны:
      • Коды для address spaces (16/24/32/64:1,2,4,8), data widths (8/16/32/64:1,2,4,8).
      • И вот САМАЯ-САМАЯ жопа из этого всего -- клятый Address Modifier: он кодируется каким-то совсем уж хитрым образом.
      • Отдельный вопрос -- где добыть максимально возможный размер окна. С этим совсем-совсем глухо, даже изнутри ядра.

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

      Да, нашёл: в ядре есть include/linux/vme.h -- наружу-то он никак не отдаётся, userspace-программам его взять негде.

    4. 26.11.2021: это тоже изучено и записано позже, и тоже весь пункт "зеленить" не будем: Насчёт «как всё-таки работает трансляция Address Modifier в код cycle» я даже прошёл всю цепочку вызовов (как оно идёт от юзера до драйвера бриджа), и там тоже печально:
      • vme_user.c::vme_user_ioctl(), код VME_SET_MASTER, вызывает (сразу, БЕЗ какой-либо проверки параметров)...
      • vme.c::vme_master_set(), который выполняет некоторые базовые проверки, в т.ч. не вполне понятные вида
        ((image->cycle_attr > cycle) == cycle)
        (тест "поддерживаемости операции", сравнением по маске?), после чего вызывает с этими параметрами...
      • bridge->master_set() -- т.е., метод конкретного бриджа, что для конкретно TSI148 сводится к...
      • vme_tsi148.c::tsi148_master_set(), выполняющей всякие разные проверки разных параметров, но...
      • ...конкретно с cycle делающее лишь 2 проверки:
        1. На всякие BLT/MBLT/2eVME/2eSST*
        2. На биты VME_SUPER и VME_PROG.

        И всё! Т.е., ширина адреса -- отражающаяся в РАЗНЫХ кодах AM'ов -- тут в cycle не участвует вообще никак!

      Увы, увы. Неожиданный побочный результат разбирательства --

      • Зато там есть явное упоминание того, что базовый адрес должен быть кратен 64Кбайтам (0x10000):
        • в vme_tsi148.c::tsi148_master_set() стоит проверка
          if (vme_base & 0xFFFF) {
                  dev_err(tsi148_bridge->parent, "Invalid VME Window "
                          "alignment\n");
          
        • (А вот для SLAVE-окон там разная "granularity" в зависимости от адресного пространства: A16:0x10, A24:0x1000, A32/A64:0x10000.)

        Откуда вывод: надо ставить базовый адрес окна в "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():
      • Сама её работа --
        1. Проверка, соответствует ли запрошенная операция текущей конфигурации окна. Проверяется КУЧА параметров.
        2. При НЕсоответствии -- переконфигурация окна и сохранение этих араметров как текущих.
      • И, что неожиданно (после простейшего интерфейса BIVME2): тут в число параметров для вызова 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().

    • Причём собственно КОДИНГА -- минимум, а основное время и усилия потрачены на сбор информации -- изучение того, как обращаться с этой клятой Тундрой. Результаты изучения -- пункты 3 и 4 выше в списке "Пара аспектов...", помеченные сегодняшней датой.
    • Много чертыханий и матов.
    • Одно из практических решений, принятых по результатам -- какой требовать размер окна: A16 -- 64КБайт, A24 -- 0x01000000 (16Мбайт), A32 -- 0x01000000*21 (максимальный размер батрачиного модуля (там это даже в схеме jumper'ов отражено: уставляются старшие 8 бит) помножить на максимальное количество позиций в крейте).

    27.11.2021: "реализация" использования Address Modifier'ов.

    • Поскольку по результатам вчерашнего разбирательства с исходниками VME-инфраструктуры в ядре стало ясно, что никакого логичного соответствия между кодами Address Modifier'ов и тундровскими "cycle" просто НЕТ, то очевидно, что надо просто сделать таблицу трансляции первых во вторые.
      • Сделано -- AM2cycle[64], заполнены только ячейки 0x09 (A32 unprivileged data), 0x29 (A16 unprivileged data), плюс 0x0D (A32 supervisory data -- в первую очередь для Карпова, но также на него и Павленко/Котов откликаются) и 0x2D (A16 supervisory data).
      • Незаполненные ячейки (т.е., ==0) считаются за "неправильные" и такие AM'ы приводят к -1/EINVAL.
      • Для заполнения таблицы нужны "магические числа", определяемые в include/linux/vme.h. Чтобы не плодить "магичности", пришлось завести локальную копию этого файла (взят из kernel-devel-3.10.0-514.el7.x86_64.rpm).
      • И есть важная особенность: в тех кодах "cycle" есть ТОЛЬКО режимы доступа -- VME_USER/VME_SUPER и VME_DATA/VME_PROG, а вот ширину адреса (16/24/32) там впрямую указать просто никак нельзя: нет в той группе такого кода.

        Так что доступ к железке Карпова (где используется режим 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().
    • Ну а парсинг параметров -- да, надо будет делать PSP-плагином; благо, он несложен -- скопируем куски из lab6_vme_firmware_common.c::ParseDevSpec().
    • Можно даже сделать ОБА варианта одновременно -- в #if'е, по символу MVME3100_HAL_MULTIPLE_WINDOWS.

    Не удержался -- сделал заготовку:

    • В самом начале #define MVME3100_HAL_MULTIPLE_WINDOWS -- но УСЛОВНОЕ, если он не определён ранее (из системы сборки; так можно, при желании, использовать разные варианты для vmeserver'а и для test'а.
    • Введена enum-константа 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: делаем компилируемость+собирабельность -- пока под локальную платформу.

    • Скопирован mbivme2/Makefile, из него убраны:
      • ссылки на кросс-компиляцию, а также
      • на LIBVMEDIRECT и
      • zzz_drv (вот ЭТОГО надо бы вообще как-нибудь обобщить в nsrc/, не?).

      И "bivme2" везде заменено на "mvme3100".

    • Пробуем -- фиг:
      • Ругается на отсутствие файла vme_user.h.
      • OK, пришлось его скопировать из исходников ядра (в компанию к взятому оттуда же vme.h).
      • Далее -- ругается на кучу неизвестных типов (вроде u32) и incomplete-типов...

        Может, сделать-таки вырезку только НУЖНЫХ определений из vme.h?

      • Сделал -- stripped_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'а), а уж теперь, когда директорий более одной и есть дублирование -- тем более.

    • Вытащено в VmeserverShadowRules.mk.
    • И mbivme2/Makefile также подправлен.
    • ...а заодно аналогичные изменения в hw4cx/drivers/can/ -- создан CanserverShadowRules.mk и на него переведены c4l-cangw/Makefile и sktcanserver/Makefile.

    Проверено -- работает.

    01.12.2021: возвращаемся к поддержке много-оконности.

    • Наполняем vme_hal_init():
      • Она в цикле перебирает все окна, и если некое окно "затребовано", то выполняет открытие /dev/bus/vme/mN, а затем mvme3100_hal_configure_window().
      • В случае ошибки в любом из этих действий на любом из окон -- делается "goto ERREXIT", где выполняется подчистка, с сохранением errno через saved_errno.
      • После цикла проверяется, что было сконфигурировано хотя бы 1 окно (булевский влфг any_rqd вначале =0, а по конфигурации =1), и если нет, то возвращается ошибка.
      • Факт "затребованности" определяется по свежевведённому полю rqd_am -- предполагается, что парсинг конфигурации должен писАть туда.

        Оно сделано отдельным от cur_am потому, что то часто форсится в =-1 -- и по умолчанию, и при закрытии шины.

    • vme_hal_term() переделана так, чтоб быть универсальной -- она теперь прикрывает не [0]-й элемент mvme3100_hal_win_info[], а ВСЕ -- в цикле.

      И для подчистки уже открытого в случае ошибки открытия очередного окна в vme_hal_init() просто вызывается она же.

    Такое впечатление, что теперь "мясо" сделано всё -- осталось реализовать парсинг конфигурации.

    04.12.2021: возвращаемся -- приступаем к поддержку парсинга и передачи конфигурации.

    • Для МНОГОоконного варианта -- как-то грустно; детали см. в предыдущем разделе за 01-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".
    • А вот дальше -- надо думать, т.к.
      1. В проекте от 02-07-2021 предполагается парсить в некую переменную, а потом вызывать некую HAL-функцию, передавая ей эту отпарсенную переменную;
      2. но СЕЙЧАС весь код сделан в предположении, что парситься будет сразу в mvme3100_hal_win_info[].

      Оба варианта имеют свои плюсы и минусы: с вызовом функции -- более "правильно"/структурированно, но шибко громоздко; с прямым парсингом -- проще и короче, но нарушаются правила фрагментации/инкапсуляции.

    05.12.2021: придумано простое решение для парсинга: если определён некий #define-символ, то считать его именем функции, которую можно вызывть для парсинга (и, соответственно, начинать воспринимать указания bus_config).

    Делаем всё для ОДНОоконного варианта в mvme3100_hal.h:

    • #define-символ назван VME_HAL_DO_PARSE_BUS_CONFIG() -- да, это макрос-ФУНКЦИЯ, принимает 1 параметр -- строку.
    • Реализация -- mvme3100_do_parse_bus_config(), она сводится к просто вызову psp_parse_as().
    • Таблица -- тривиальная, из единственной строчки PSP_P_INT("win",....

    И ещё некоторые соображения, пришедшие в голову при делании вышеперечисленного и просмотре/анализе нынешней инфраструктуры remdrv/remsrv:

    • Как делать для МНОГОоконного варианта, чисто синтаксически -- хбз: ведь там для win0...win3 надо указывать ссылки на разные элементы МАССИВА.

      Сходу напрашивается только использование 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]) -- как уже сейчас и сделано).

    • В связке remdrv+remsrv сейчас НЕ реализована передача layerinfo удалённым драйверам.

      Размышления на тему реализации -- за 05-07-2014 и 08-11-2019. Но реальной потребности в реальной реализации -- до сих пор так и не было.

      ...причём если обойтись ОДНОоконным вариантом, то и тут реализация не потребуется :D

    • Пора бы уже переименовать drivers/vme/'шные директории src/ и bivme2/ в old-src/ и old-bivme2/, а нынешние nsrc/ и mbivme2/ в src/ и bivme2/.

    06.12.2021: делаем поддержку в vme_test_common.c: всё довольно тривиально -- ТУЧА #if'ов.

    • Объявление option_bus_config=NULL.
    • Добавление "C:" в список getopt()'а.
    • Собственно обработка ключа 'C'. Тут есть пара нюансов:
      1. РАЗРЕШАЕТСЯ множественное указание (может быть полезно для МНОГОоконного варианта, чтобы указывать конфигурацию нескольких окон раздельно; а зачем такое может требоваться -- разве vme_test'у 1 окна недостаточно? а-а-а, вот зачем: если в одной команде указывается несколько команд с РАЗНЫМИ единицами В/В -- 8/16/32).
      2. Сбагривание для парсинга производится сразу (а не потом, после перебора всех ключей командной строки): и для возможности множественности, а главное -- чтобы сразу же ругнуться в случае ошибки.
    • Перед вызовом vme_hal_init() -- парсинг умолчания VME_HAL_DEFAULT_BUS_CONFIG_FOR_VMETEST в случае, если ключом явно ничего указано не было (*И* если само это умолчание определено).
    • ...ну и строчка в help'е.

    В 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() с надлежащими модификациями:
      • в первую очередь -- адаптацией под PSP-плагин, подсматриванием в programs/xmclients/histplot.c::RangePluginParser();
      • плюс подпиливанием под чуть иной синтаксис.
      • А также переделкой под иную принимающую структуру (имена полей),
      • плюс в начале воспринимание префиксов '?' (считать текущую конфигурацию) и '-' (запретить использование этого окна).

    14.12.2021: продолжаем...

    • Реализуем "вычитывание текущих настроек". Там бОльшая часть -- не бог весть какая проблема,
    • ...но вот трансляция клятого дуплета (aspace,cycle) в ещё более клятый AM -- реально голову сломаешь: в битовой кодировке AM'ов реально НЕТ НИКАКОГО СМЫСЛА!!! Кое-что там вроде бы как транслируется в конкретные битики, но именно что "кое-что" -- ОЧЕНЬ далеко не всё. Это просто некие магические числа.
    • После нескольких часов пяления в таблицу, попыток найти там логику (спойлер: нет, логики там НЕТУ: если в A24 и A32 ещё прослеживаются зачатки разбиения функциональности (program/data, privileged/unprivileged) по битам, то всё остальное просто впихнуто абы как -- на свободные места) пришла в голову мысль: раз в AM'ах нет логики и не удастся просто транслировать биты VME_SUPER/VME_USER и VME_PROG/VME_DATA в биты AM'ов, то сделать СВОЮ табличную трансляцию: использовать вышеупомянутые режимы как 2 младших бита номера, а режим адресации как следующие несколько бит .
    • Сделано -- таблица названа mode2AM[], а для адресации в ней используются константы TBL_*, описывающие значения, определяемые адресными пространствами и режимами доступа (PRIV/USER, CODE/DATA).

      Строчки таблицы имеют вид вроде

      [TBL_A32 | TBL_USER | TBL_DATA] = 0x09
      -- т.е., заполнены лишь отдельные ячейки, а в незаполненных нули, и сама таблица имеет размер меньше, чем 64 (0x3F+1).
    • "Собирание" индекса делается анализом информации, полученной от ядра.

      И если в ячейке по получившемуся индексу -- 0 (либо таблица меньше и такой ячейки в ней вообще нет), то данное окно отключается.

    • ЗЫ: кстати, если окно не сконфигурировано (master.enable==0), то оно также отключается.
    • ЗЗЫ: режим CR/CSR (0x2F) не поддерживается, НИ В ОДНОМ из вариантов.

      В принципе, сделать-то можно (особенно в ОДНОоконном), но просто лень -- это усложнение логики, и без какого-либо смысла (батраковско-павленковско-котовские всё равно CR/CSR не поддерживают, судя по PDF-описаниям).

    15.12.2021: ну вроде как всё было дописано -- пора проверять, для начала хотя бы компилируемость.

    • Вылез дурацкий косяк в PSP-таблице: там для указании номеров строк (параметром uptr/privptr) использовалось lint2ptr(N); но lint2ptr() -- это ФУНКЦИЯ, хоть и inline static (т.е., по сути -- как бы макрос), поэтому компилятор ругался на "initializer element is not constant".

      Пришлось исправить на (void*)(N).

    • После этого -- всё собралось.
    • Проверил "работу" -- парсинг работает, вроде бы даже все возможные ошибки отлавливаются.
    • В качестве альтернативы спецификации '?' был добавлен вариант 'q' -- чтоб не нужно было квотить, в отличие от вопросительного знака.

    На этом вроде пока всё -- больше сделать ничего нельзя, пока не будет реальной системы сборки под MPC8540 и живого MVME3100 для тестов.

vmeli.c:
  • 10.11.2021: давным-давно уже ясно, что раз изготовленный Евгением Котовым драйвер vmei.c категорически не годится (см. записи за 08-07-2021), то в случае изготовления-таки поддержки MVME3100 всё равно придётся городить другую реализацию (пусть и на основе котовской).

    И девайсы линий надо будет называть /dev/vmeliN; соответственно, драйвер -- vmeli.c. Вот пусть для этой реализации и будет сей раздел.

  • 12.11.2021: собственно "типа изготовление"...

    Сейчас этим если заниматься -- то чисто для развлечения и из-за "зуда в одном месте".

    12.11.2021: ну "зуд" присутствует, да...

    • Сделана hw4cx/kernel_hw/mvme3100/.
    • ...и туда скачан котовский, но уже под именем vmeli.c.

    Из-за чего конкретно СЕГОДНЯ весь сыр-бор: из-за того, что ещё позавчера стало очевидно, как именно надо организовывать и 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).

    • Собственно "vmei" везде заменено на "vmeli";
    • символ "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" регистрируются обработчики именно конкретных ВЕКТОРОВ -- точнее, дуплетов (линия,вектор). Это делает vme_irq_request().

      Причина, почему сделано ТАК -- понятна: этим имитируется работа прерываний в M68K (где при ОБРАБОТКЕ как раз уровень вообще роли не играл), и как раз для драйверов в ЯДРЕ именно так и нужно.

      Также, возможно, именно поэтому Котов реализовал по /dev/vmei-файлу на вектор, а не потому, что так считал Фатькин. Хотя, скорее всего -- по суиие этих причин.

      Как бы то ни было, встаёт вопрос: а насколько оптимально (с точки зрения производительности) будет то, что драйвер станет регистрировать для каждой линии по 256 обработчиков?

      • Реализация в drivers/vme/vme.c; заглянул -- нет, проблем быть не должно, т.к. там не перебор по списку, а табличная адресация к 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: продолжаем.

    • Все открытия, закрытия, регистрации и т.п. переделаны с "открывается с minor=vector, цикл по level" на "открывается с minor=level, цикл по vector".

      КРОМЕ собственно "работы" -- vmeli_isr(), vmeli_cdev_read(), vmeli_cdev_poll() и обслуживающей их структуры image_desc. Там всё равно надо ВСЁ переделывать, на по образу и подобию BIVME2'шного.

    • Содержимое типа-структуры image_desc:
      1. Добавлены q_-поля для буферизации -- q_isin[256], q_fifo[Q_SIZE], q_head, q_tail.
      2. Закомментированы ненужные level (экс-vector), level_mask, ready.
    • А теперь и собственно "РАБОТА": "обработчик прерываний" vmeli_isr() (это не обработчик, а callback для VME-подсистемы ядра) тоже переделан, как и vmeli_cdev_read() с vmeli_cdev_poll() -- всё по образу и подобию предшественника двухлетней давности (реально в основном просто копировались куски, с минимальной контекстной заменой (вроде "vd" на "image").
    • Инициализация всех q_-полей -- в vmeli_probe(): там же, где инициализируются прочие поля структуры image_desc.

    ...и это, в общем-то, всё -- типа переделано. Дальше только проверять компилируемость и работу.

???:
local:
  • 12.11.2019: создаём раздел.

    Это директория, в которой будут собираться VME-драйверы под ЛОКАЛЬНУЮ архитектуру -- аналогично can/local/.

  • 09.01.2020: сделан Makefile, теперь драйверы тут собираются.

    И в vme/Makefile::SUBDIRS оно добавлено.

vme_test_common:
  • 15.02.2019: это раздел для работы над общим для всех утилит *vme*_test исходником vme_test_common.c.

    В этот исходник преобразуется в будущем нынешний bivme2_test.c -- по мере создания общей инфраструктуры vme_io и конкретно HAL-модуля bivme2_vme_io.[ch].

  • 05.11.2019: файл создан, пока просто пустой -- для симлинковабельности "системой сборки vme/nsrc".

    14.01.2020: за вчера и сегодня файл наполнен -- копированием из старого bivme2_test.c с адаптацией под новые реалии.

    Некоторые заметки:

    • Добавлен парсинг компонента "шина" адресе устройства -- опциональный префикс "@BUS_MJR[/BUS_MNR]:".

      Он сохраняется в vme_devspec_t вместе с прочими.

    • Некоторые модификации в именах -- для большей корректности:
      1. В do_io() переменная ofs переименована в addr.
      2. В vme_rw_params_t, соответственно, addr стал databuf'ом.

        Это было унаследовано от uspci.h -- увы, там было выбрано не самое удачное имя.

      3. Но offset остался как есть -- т.к. это именно OFFSET от базового адреса.
    • Далее проведена некоторая "перебалансировка":
      1. Все заинтересованные в бывшем iohandle -- ныне bus_handle -- берут его сами из глобалной переменной, а не получают параметром.
      2. А do_io() также самостоятельно берёт dev.addr_size, а не получает параметром.
    • О ловле IRQ:
      • Пока оставлена старая инфраструктура, с "имитацией cxscheduler'а".

        Она, кстати, тоже, похоже, унаследована от 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().
    • Также в help'е наконец-то задокументирован вариант ".[microseconds-to-wait]".

    Теперь -- проверять!

    15.01.2020: проверан В/В -- работает.

    Но пришлось исправить косячок -- в do_io() в получении исполнительного адреса отсутствовало прибавление к offset'у базового адреса dev.base_addr (в старой версии этим занималось bivme2_io.c).

  • 17.01.2020: надо переделывать WaitForIRQ() с эмуляции cxscheduler'а на него настоящий. Как минимум для a3818_hal.h необходимо.

    P.S. В принципе, можно для начала сделать "2-режимный вариант": либо эмуляция, либо использование реального; выбор режима в зависимости от define-символа, выставляемого в Makefile. Давно такая мысль бродила; вопрос лишь -- а реально ли оно нужно?

  • 21.01.2020: есть желание уметь выдавать вычитываемые данные в БИНАРНОМ формате -- для возможности записи в файл, передачи в pipe, всякого анализа и т.п.

    Очевидно, делать надо по аналогии с cdaclient и pipe2cda.

    22.01.2020@ночь, ~03:00, бессонница: сделано.

    • Ключ "-B".
    • При взведённом option_binary вместо обычной печати делается просто
      write(1, buf, addr.units * addr.count);
    • Также, для подавления сообщений об IRQ, одновременно взводится option_quiet=1.
    • Кстати, аналогичный ключ был, из всех подобных утилит, только в v2'шной cx-bigc: там тоже в help'е сказано "(implies -q)", но там flag_quiet не взводится, а просто не доходит не до какой лишней печати в бинарном режиме.

    22.01.2020@утро, пешком через студгородок: всё-таки несоврешенный вариант с нынешней реализацией "-B": прочитать из устройства и записать в файл можно, а вот прочитать из файла и ЗАПИСАТЬ в устройство бинарные данные -- нет возможности.

    И, учитывая существующий синтаксис, даже не очень ясно, КАК можно б было указывать эти бинарные данные: ведь тот же cdaclient тоже не умеет принимать их на вход, а умеет это отдельная утилита pipe2cda.

    Возможный вариант синтаксиса: чтобы вместо значений для записи указывать @ИМЯ_ФАЙЛА. Т.е., вариант COMMAND'а U:OFFSET=@FILENAME.

    Отдельный вопрос тут будет -- а как обходиться с stdin'ом?

    • По синтаксису очевидно, "=@-" -- ссылка на stdin всегда указывается как "-".
    • Но вот как это РЕАЛИЗОВЫВАТЬ -- хбз.
    • Читать -- прямо из STDIN_FILENO?

      Но тут могут быть проблемы с совмещением этого аспекта с обычной работой с файлами: те-то, после окончания чтения, надо бы закрывать, а stdin -- вроде как нет.

    • А как бы уметь ОГРАНИЧИВАТЬ объём читаемых данных? Чтобы уметь принимать с stdin'а НЕСКОЛЬКО блоков данных -- аналогично тому, как бинарный вывод может выдавать несколько блоков; этак получится возможность делать "scatter/gather".

      Напрашивается синтаксис U:OFFSET/COUNT=@FILENAME -- т.е., просто явное указание количества.

      Это будет аналогично поведению cdaclient/pipe2cda -- там значения могут иметь префикс [COUNT].

    • И да, такой синтаксис -- с указанием количества -- вполне осмыслен и для обычного указания значений: U:OFFSET/COUNT=VALUE{,VALUE}.

    Впрочем, покамест это всё больше теоретизирование -- никакой РЕАЛЬНОЙ потребности в таком бинарном В/В нету, это больше из стремления к красоте и полноте реализации.

    ...но как общетеоретические соображения -- вполне полезно, для разработки "общей теории диагностических утилит командной строки" :).

  • 18.07.2021: в парсинг DEVSPEC'а добавлена возможность в ADDRESS_SIZE указывать опциональный префикс 'a' -- чтобы выглядело как "A16", "A24", "A32".

    Основной смысл -- для унификации с создаваемым lab6_vme_firmware_common, где после ширины адреса также сможет указываться ширина данных (там получится "A32D16"). Но и для читаемости будет полезно.

  • 02.10.2021: добавлен ключ "-k" -- игнорировать ошибки обращений VME и НЕ завершать работу, а продолжать.

    Буква "k" выбрана по примеру "make" -- там это "keep going".

    Потребно для целей диагностики -- чтоб можно было производить цепочки VME-команд, часть из которых может и обламываться.

    25.10.2021: для унификации с прочими утилитами, в которых маленькая "-k" уже занята (в первую очередь canmon, где это "print IDs in kozak notation"), переделано на заглавную "-K".

  • 23.01.2022: замечена странность: как минимум конкретно на A3818 vme_test НЕ ругается на спецификацию "@1:32:0x09:0x{i}000000", а просто воспринимает это как какой-то странный адрес.

    (Это было обнаружено при запуске в цикле -- когда по ошибке было запущено

    for i in d5 d6 96 98;do; a3818_test -x @1:32:0x09:0x{i}000000 i:0x104; done
    
    вместо должного
    for i in d5 d6 96 98;do; a3818_test -x @1:32:0x09:0x${i}000000 i:0x104; done
    
    то вместо ругательства на синтаксис оно получало "Bus error", т.к. пыталось лезть в странные адреса.)

    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 местах, но в них так и НЕ сделано исправлений (ибо лень):

    1. В указании usecs в WaitForIRQ().
    2. В парсинге "-i" -- значения irq.n и irq.vector: шибко уж муторно будет тут сделать КОРРЕКТНО, т.к. там весьма развесистый набор допустимых вариантов.

    В других программах:

    • uspci_test.c: добавлено в аналогичные 2 места:
      1. ParseDevSpec() (он там замудрённый, и совсем не похож на VME-related потомков) -- в 2 экземплярах, а 3-й вариант спецификации обеспечивается...
      2. ParseAddrSpec() -- в точности как в vme_test_common.c, т.к. они-то очень похожи.
    • mvme3100_hal.h::mvme3100hal_WinspecPluginParser(): неактуально -- оно там всегда возвращает указатель на точку окончания парсинга вызывальщику, и уж тот должен позаботиться далее.
    • cm5307_test.c: УЖЕ есть проверка на "конец строки или пробел" после "=DATA".
    • canmon_common.c: неидеален парсинг списка байтов для пакета, ну и парсинг задержки/лимита в DoRecv() не имеет проверки (да и пофиг).

    Кстати, есть в модели "ошибкой считается errp==p, а иначе передаём далее" косяк: строка "0x" (например, как фрагмент "0xZZZ") рассматривается как валидная, возвращающая значение 0. И ничегошеньки в такой модели не сделать. А вот МОЖНО сделать, если перед strto*() сбрасывать errno=0, а после вызова проверять на errno!=0 (это вполне официальная рекомендация из man-страницы).

vme_fastadc_common:
  • 15.02.2019: создаётся для будущего модуля vme_fastadc_common.h, в который уйдёт общий кусок из adc4x250_drv.c (он там давно подготовлен -- потому что изначально и делалось в расчёте на такую сегментацию).
  • 27.01.2022: не хватает возможности выполнять операции по ЗАВЕРШЕНИЮ драйвера -- для сброса устройства в заранее определённое состояние, отключение (будущего) прерывания и т.д.
    • Т.е., того, что в cpci_fastadc_common.h делается при помощи определений FASTADC_ON_CLS* (а в camac_fastadc_common.h, кстати, не делается вообще никак).
    • Но конкретно с VME такой фокус не пройдёт из-за шибко уж широкого репертуара операций (там-то было просто 32-битное чтение либо запись BAR0).
    • Напрашивается возможность указывать просто функцию, которую вызвать из 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().

    Конечно, ещё проверить надо будет, но тут косячить особо вроде нечему.

adc4x250:
  • 15.02.2019: раздельчик для работы как над общей инфраструктурой для adc4x250 и adc1000 (а также потенциального ADC500, он же "adc2x500"), так и над конкретными драйверами.

    Для конкретных драйверов -- будут свои level5-разделы.

    21.02.2019: ага, вот сегодня и создаём тот level5-список.

  • 21.02.2019: кстати, давно напрашивается разобраться с названиями: надо б чтобы у "общей инфраструктуры" и 4-канального драйвера были разные имена.

    А что если так: инфраструктура -- adc4x250, а драйвер -- adc250 (и, аналогично, adc1000 и adc500)?

    Надо бы поинтересоваться у батракоцвев/павленковцев/Котова.

    21.02.2019: спросил у Сенченко. Увы, с названиями по-прежнему бардак -- и adc4x250, и чего-то-там-1CH и -4CH...

    Вот не может никак 6-я лаборатория научиться давать внятные названия своим девайсам -- не только до начала разработки, но даже и на этапе внедрения...

    Как бы то ни было -- драйверы назовём именно так: adc250, adc1000 (и вряд-ли-когда-будет-реализован'ный adc500).

    22.02.2019@институтская-сессия-кофе-брейк-до-обеда: спросил и у Фатькина -- да, всё ровно так, нет устаканившихся названий.

    И более того: ADC1000 не особо-то и нужен.

  • 21.01.2022: до сих пор есть старые артефакты, ещё с 2017 года, с "adc4x250" в именах -- adc4x250_drv_i.h, adc4x250.devtype, adc4x250_gui.c, adc4x250_data.c.

    Надо б их поудалять.

    Но они обусловлены наличием drivers/vme/old-src/adc4x250_drv.c -- так что пока он не исчезнет, трогать всё то просто нельзя.

adc4x250_defs.h:
  • 21.02.2019: здесь будет про общую часть для всех АЦП на базе "ADC4X250".

    Скорее всего, ограничится именно файлом adc4x250_defs.h.

  • 20.01.2022: первое изменение с 20-07-2017 -- добавлен регистр ADC_DECIM_RATIO = 0x00010c.

    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.

adc250_drv:
  • 21.02.2019: бывший "adc4x250_drv.c" переименуем в adc250_drv.c -- как раз удобно будет совместить с переездом на унифицированную инфраструктуру с vme_io и с vme_fastadc_common, не трогая имеющиеся исходники.

    01.11.2019: переименовываем заголовок раздела, добавляя "_drv" -- чтоб было как у всех прочих драйверов.

  • 01.11.2019: несколько дней назад Антон Павленко принёс парочку новых блоков взамен старых, поскольку что-то за эти 2 года Котов приподпеределал. И когда вчера я начал проверять, как там работает/нет, то натолкнулся на странноватое поведение. Поэтому выпросил один старый модуль обратно и пытаюсь разобраться, что к чему. Разбираюсь пока на старом драйвере -- не-layer'ном adc4x250_drv, но железки-то те же.

    01.11.2019: итак, что мы имеем:

    • Вчера, когда были 2 новых блока, то возникло 2 проблемы:
      1. НЕ шарится IRQ -- драйвер vmei устроен так, что "получает" уведомление только кто-то один. Каким-то неясным образом -- ПЕРВЫЙ.

        Так что если сначала обратиться, например, к D5'му, то он работает, прерывания ловит. А если потом к D6'му, то он НЕ работает, а драйвер D5'го получает прерывания с вектором от D6'го и, естественно, ничего не делает. Если сначала обратиться к D6'му, а потом к D5'му -- всё зеркально аналогично: D6 работает, а D5 нет, но его прерывания получает D6.

        Позвонил Роговскому, как у него: а очень просто -- у него 4 устройства (карповских?), и у каждого своё IRQ. Т.е., разделение не требуется и всё работает.

      2. Но когда модуль ловит свои прерывания, то они летят ПОСТОЯННО, как будто включен ISTART.

      Первую-то проблему придётся решать переходом на layer'ную архитектуру.

      А вот для разбирательства со сторой и взят старый модуль.

    • Сегодня вставил старый модуль -- ведёт себя аналогично.

      (Разве что числа там читаются в осциллограмму более дикие (из воздуха-то!), но это уже может объясняться более старым железом и/или отсутствием калибровки).

    Возникла мысль: а может, это где-то в драйвере делается принудительный trigger'инг? Но нет -- на вид нету.

    Вариант -- пройтись по "истории модификаций", посмотреть отличия: может, всё-таки добавил какой-то кусок кода в целях отладки, да и забыл.

  • 12.11.2019: создаём уже adc250_drv.c, переделкой из adc4x250_drv.c.

    12.11.2019: процесс был муторный, в силу большого объёма, но говорить особо почти не о чем. Только:

    1. Очень противным было разбирательство, какие "ADC4X250*" надо переименовывать в "ADC250*", а какие нет.

      Каналы-то -- ADC4X250_CHAN_* -- просто контекстной заменой, а вот всё остальное -- практически индивидуально.

    2. В старом драйвере был перехват сигналов -- InterceptSignals(), но обработчик не делал ничего, а лишь печатал номер пойманного сигнала.
      • Зачем это было сделано -- загадка; записей не сохранилось.
      • Если для корректного "отпускания" libvmedirect -- то там надо б было как-то с семафором работать (хотя как? мы ж не знаем, наш он сейчас или чей; только если сбрасывать безусловно, но толку-то...).
      • Возможно, для ловли каких-то ошибок?

    В общем, доведено до собирабельного состояния, но, естественно, пока без поддержки IRQ.

    14.11.2019: и "поддержка IRQ" добавлена, при доводке остальных компонентов.

    18.11.2019: первая проверка. После исправления косяка с SIGSEGV'ом в bivme2hal'е -- похоже, ВСЕ модули ловят IRQ. Но, как и раньше -- оные IRQ происходят безо всяких внешних причин, а просто сразу после программирования устройства на ожидание запуска.

    Предполагается такой сценарий разбирательства:

    • Убрать лишнюю отладочную печать (а то её сейчас невозможно много -- лавина информации мешает что-либо понять).

      Да, убрано -- стало намного читабельнее.

    • Убедиться, что действительно прилетают прерывания ото всех 3 имеющихся модулей.
      • Сложно: от "новых" (6,4,2,3) -- да, прилетают.
      • А от 1 "старого" (версии=4,3,2,1) -- только пока на придёт что-нибудь от новых, потом он замирает.
      • А, нет -- от "нового" первого тоже прилетать перестают как только требуешь от второго.

        19.11.2019: ага, похоже, что в ЛЮБОМ сочетании: как только запускаешь ещё один, предыдущий "отключается". И как я вроде бы умудрялся несколько дней назад получать, что молотят оба "новых" сразу -- загадка; ошибка наблюдения?

      • Плюс, вроде было замечено, что "старый", оставленный молотить в одиночку на несколько часов, через какое-то время завис.

        Может, дело в имеющемся "лишнем" чтении регистра статуса прерываний, так что иногда race condition срабатывает и чтение+сброс_прерывания происходит прямо в обработчике? ...хотя всё равно цепочка не очень ясна.

    • ...и проверить, что приходят они ТОЛЬКО при ожидании (т.е., когда был запрос).

      Да, похоже, что так -- когда попало НЕ летят.

    • Одолжить у Козака VADC16 и проверить, что он работает как положено.
    • Поанализировать содержимое 20191104-adc4x250_drv.c-diffs.txt, дабы попробовать понять, не добавлено ли какой-то лишней команды, вызывающей прерывание программным образом.
    • Ещё разок почитать на эту тему описание.

    19.11.2019: взял у Козака пару VADC16 и немного пообщался с ним на тему "а почему у него нет команды сброса IRQ".

    • (Моя предварительная мысль: а может, сбрасывается по чтению данных из буфера АЦП?)
    • Он при реализации внимательно читал стандарт VME, и если сделал так, как сделал -- значит, стандарт это позволяет.
    • Также он упомянул, что:
      1. С одной стороны, генерация прерывания -- это взведение некоей линии ("точно так же, как это было на PC/AT" (т.е., на шине ISA)).
      2. С другой стороны, есть специальная процедура передачи вектора.

      Тут что-то не совсем сходится: на ISA ведь не было никаких векторов, там разделение IRQ было страшной проблемой.

    • Мои мысли по предыдущему пункту: ну а правда -- раз есть "процедура передачи вектора", по которой контроллер оный вектор должен запомнить и потом отдать наверх драйверу, то на кой вообще тогда может требоваться команда "сбросить прерывание"?

      Ведь по факту получается, что модель -- "message-based" (сообщением является вектор), а не "level-based".

    По результатам мысль: а если попробовать НЕ выполнять команду сброса прерывания -- то оно будет повторяться? Или так и останется 1 раз?

    Если второе, то выходит, что команда "сброс прерывания" по факту нужна не для обработки прерываний, а лишь для того, чтобы устройство получило подтверждение "прерывание обработано" и могло бы работать дальше.

    Список "извращенческих тестов":

    • Поставить печать irq_n,irq_vect прямо в vme_lyr_irq_cb().

      Поставил -- пользительно: позволяет увидеть, что никаких "лишних" прерываний не прилетает.

    • Убрать сброс прерывания из FASTADC_IRQ_P().
    • Повесить VADC16 на тот же N=5 и посмотреть, как они живут вместе с ADC250.

    20.11.2019@утро, дома (было -28, весь день провёл дома за чтением доков по Python): мысли о том, как ещё поразбираться с ADC250@BIVME2:

    1. Попробовать у "зависшего" устройства bivme2test'ом прочитать регистр статуса прерываний -- не будет ли в нём бита "готово"?
    2. А если будет, то не в драйвере vmei ли проблема? Что, если в нём нет буферизации толпы векторов от одного IRQ, и потому остаётся лишь первый или последний, а остальные теряются?
    3. И если так, то не выпросить ли у Павленко исходники модифицированного Игорем Ильиным драйвера vmei.c, чтоб глянуть, в чём отличия от "оригинала" - вдруг удастся использовать его?

      21.11.2019: выпросил, теперь надо анализировать/сравнивать. Попытка сравнить добытый vmei.c от 2010-05-24 "в лоб" с имеющимся у меня vmei-cs.c от 2011-09-13 показывает, что файлы -- родственники, но и различий реально дофига.

      25.11.2019: поанализировал -- всё грустно: там "множественный" API (с указанием и vector) -- для KERNEL-драйверов, каковым является и ихний драйвер vsdc3.o (вот как, ну КАК они додумались до такого бреда?!).

    4. А вообще - что мешает просто взять да попробовать имеющийся в контроллере "тот" драйвер от Игоря? Вдруг заработает? Или, как минимум, проверить, что с ним работает мой bivme2hal.h.

    25.11.2019: единым списком -- результаты разных тестирований:

    • «Попробовать у "зависшего" устройства bivme2test'ом прочитать регистр статуса прерываний -- не будет ли в нём бита "готово"?»
      • ХРЕН... Чё-то ругается -- с 06-м работает, а с d5/d6 -- фиг. И перезагрузка не помогла...
      • Пересобрал bivme2test (после добавления errno=0 перед операцией В/В) -- заработало.
      • А я-то уж думал ввести ключик "-E" -- "ignore VME errors".
      • 26.11.2019: нашёл причину, почему с "новыми" устройствами bivme2_test от 2017-07-11 глючит: в старой версии в 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-битности.

    • «2 штуки VADC16»
    • Пообщался с Сенченко и Котовым. Да, драйвер VSDC3 под BIVME2 действительно был "в ядре", а Котову на BIVME2 вообще не удалось нормально с IRQ работать и он на них там просто забил.
    • Посмотрел исходники мамкинского драйвера -- да, там нет НИКАКОЙ буферизации векторов, так что всегда отдаётся просто последний из приходивших.

    29.01.2020: за прошедшее время и мамкинский драйвер приподпофиксен, и adc250_drv введён в строй и используется аж с 8 устройствами в одном крейте.

    Так что задачу "сделать драйвер, чтоб работал" считаем выполненной, а дальнейшие манипуляции -- уже в последующих разделах.

  • 18.01.2020@дома, суббота, вечер: надо бы переделать 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.

    1. В ValidateParam() добавлено отбрасывание младшего бита (да, аналогично ADC200 -- тыт мы тоже можем только парами пропускать измерения).
    2. А в вычисление оффсета для чтения добавлено (PTSOFS/2)*4.

    01.02.2020@суббота-дома: всё сделано, но ПО ДРУГОЙ модели (по результатам разговора с Котовым 30-01-2020; сама идея записана там же несколькими абзацами выше): вычитываем ОДНОЙ векторной операцией прямо в retdata[] (оно у нас уже как раз 16-битное), а затем меняя местами чётные и нечётные (это необходимо из-за big-endian).

    А ставший ненужным buf[1024] удалён.

    03.02.2020: проверено, после устранения пары косяков в HAL и libvmedirect_vect_io.h -- работает!!!

    И функционирование PTSOFS проверено -- работает как надо, а ЕманоФедя, оказывается, сразу же стал им активно пользоваться.

  • 18.01.2020@дома, суббота, вечер: при обдумывании блочного вычитывания осциллограммы в имеющемся коде обнаружился позорный ляп: оно ВСЕГДА вычитывало данные только с 0-й ячейки памяти -- просто потому, что начальный адрес высчитывало --
    ro = ADC4X250_DATA_ADDR_CHx_base + nl * ADC4X250_DATA_ADDR_CHx_incr;
    -- но далее его инкрементировать при чтении забывало.

    Добавлено "ro++" в цикл по numduplets.

    20.01.2020: да, проверил; результаты проверки:

    • Вариант ДО модификации -- действительно давал все осциллограммы из 2 чередующихся значений (чётные и нечётные, вычитывались из младших и старших полу-uint32 по адресу 0).
    • Исправленный вариант -- выдаёт на вид осмысленный шум.

    Чуть позже: какой, шьорт побьери, "ro++"?! Ведь АДРЕС надо увеличивать на 4, а не на 1!

    Исправил на "ro += 4" -- "шум" очень резко уменьшился, став действительно совсем рядом с 0.

    Кстати, спросил Антона Павленко на тему "как ADC250 реагирует на не-кратный 4 адрес?". Ответ:

    • Оно пытается корректно поддерживать невыровненный доступ.
    • Это (в отличие от исходного M68K, или в противоречие ему) прямо как-то прописано в стандарте VME.
    • Они даже проверяли работу этого на MVME3100; а вот как в этом отношении ведёт себя BIVME2 -- не проверяли.
    • Нюанс там в том, что оно должно как-то корректно работать с порядком байт (big/little-endian).
    • Посоветовал проверить на примере регистра "тип устройства", из которого читаются заранее известные значения с понятной разблюдовкой по байтам -- там должно быть хорошо видно, что к чему.
  • 22.01.2020: кстати, был забыт pzframe-скрин adc250.

    Сделан -- путём копирования adc250_{data,gui}.c из adc4x250_{data,gui}.c с последующей контекстной заменой adc4x250->adc250 и ADC4X250->ADC250.

  • 28.01.2020: надо бы как-то проверять, а есть ли на месте (по указанному адресу) модуль, чтобы хоть как-то было видно -- цветом -- его отсутствие.

    Например, использовать статус (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.

  • 29.01.2020: пара мелких модификаций в коде:
    1. Ещё вчера Котов объяснил, почему не работает TrggrMeasurements(): я там меняю TRIG_SOURCE и делаю R_CTRL:=START ещё раз, но это не сработает, когда оно уже запущено -- надо предварительно сделать "стоп".

      Поэтому перед повторным стартом вставлено ещё R_CTRL:=BREAK_ACK.

    2. А сегодня он сказал, что баг в прошивке, из-за которого требовалась небольшая пауза между R_CTRL:=BREAK_ACK и R_CTRL:=START, исправлен, так что паузу можно убирать.

      Поэтому SleepBySelect(10000) за-#if0'ен.

    Надо будет проверить.

    03.02.2020: да, проверено -- и SHOT работает, и отсутствие паузы проблем не вызывает. Так что "done".

  • 30.01.2020@утро, ~06:00, зарядка: вчера, при обсуждении производительности BIVME2 -- сколько точек успевает вычитываться всеми ADC250 через VME, и сколько успевает отсылаться наверх серверу -- зашёл вопрос о битности. Данные в ADC250 -- 16-битные, наверх же они идут 32-битными. Вот надо ли это?

    А чё б не сделать егошний dtype/datum_t 16-битным -- int16 вместо int32? Раз там и так вычитываются 16-битные с фиксированной точкой -- вот их и использовать.

    Тем более, что тогда можно обойтись безо всяких промежуточных rbuf[1024], вычитывая данные прямо в retdata[], ОДНОЙ векторной операцией. Уже на работе: авотфиг -- ведь мы вычитываем 32-битными словами, а платформа эта -- BIG-endian, так что точки окажутся перепутанными. Надо с Котовым пообщаться...

    Но это именно только в adc250. А в adc1000 и adc500, с их перемешанными каналами -- нет, там придётся читать поштучно. ...хотя, можно вычитывать блоками, а потом раскладывать.

    30.01.2020: уже на работе: ВСЁ ВЕСЕЛЕЕ!!! Там в драйвере и так 16-битные данные --

    typedef int16 ADC250_DATUM_T;
    enum { ADC250_DTYPE = CXDTYPE_INT16};
    
    но в devtype стоит 32-битность --
     
    devtype adc250 r1i3132620,r4i783155,r5i,w30i,r100i
    
    как и в pzframes/adc250_data.c -- ADC250_DTYPE = CXDTYPE_INT32!!!

    Так это что же получается: бедный сервер постоянно занимается преобразованием int16->int32? Во бардак...

    30.01.2020: поговорил с Котовым.

    • Да, он в курсе этой проблемы. И понимает, что big endian -- действительно подкладывает тут свинью.
    • В принципе он обдумывает вариант класть точки в слово в другом порядке -- в младшее полуслово нечётную, в старшее полуслово чётную. Тогда при вычитывании 32-битными словами в 16-битный массив получалочь бы ровно то, что нужно.

      Но препятствием к такой модификации является то, что на ЛИУ-20 уже задеплоено большое количество ADC250, так что пришлось бы модифицировать И модули, И сенченковский софт.

    • А пока -- да, сначала вычитывается вектор, потом в нём переставляются 16-битные элементы (чётные с нечётными).
    • И да, для ADC1000 всё ещё тяжче.

      СЕЙЧАС Котов в своём драйвере вычитывает в отдельный массив по-канально, а потом собирает в один канал. Но этот дополнительный массив для вычитывания -- ОДИН на все экземпляры устройств (поскольку параллельного вычитывания в рамках одного процесса вроде бы не бывает).

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

    • И ещё упоминалась некая "динамическая калибровка", которая может, дополнительно к уже работающей аппаратной, выполняться программно.

      Но внятного описания (что это такое и как это делать) пока нет.

    • Отдельно несколько раз говорилось, что может иметь смысл разделить драйвер на 2:
      1. Первый -- "нижний" -- работает с аппаратурой и просто вычитывает данные из аппаратуры, передавая их наверх.
      2. Второй -- "верхний" -- исполняется на более высокопроизводительном процессоре, он берёт "сырые" данные от нижнего и что-то с ними делает: переставляет ячейки, применяет динамическую калибровку, ...

      Я так и не понял, используется ли у них сейчас такая 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@дома-суббота: да, по-ячеечный вариант тоже оставлен.

  • 03.02.2020@утро-дорога-на-работу, около арки ИЦиГ:: вопрос -- а зачем ВСЕГДА вычитывать ВСЕ каналы?

    Можно же и из устройства вычитывать только те, которые line_rqd[nl] (либо если data_rqd). И наверх отдавать уже только их.

    Вроде бы инфраструктура с PrepareRetBufs() должна такое позволять.

    Понятно, что при натравливании "скрина" на устройство он будет пониторировать все каналы и смысла от такой оптимизации не будет, но вот при чтении данных специализированными программами, мониторирующими только нужные каналы -- очень даже получится экономия, т.к. вычитываться будет ровно столько, сколько нужно.

    03.02.2020: смотрим, как обстоят дела.

    • PrepareRetBufs() УЖЕ устроен именно так -- наверх отдаётся только то, что было запрошено.
    • Конкретно в cPCI'ных ADC200ME и ADC812ME эта оптимизация смысла не имеет: там данные каналов идут вперемежку (200-й -- по точке от чётного/нечётного канала в младших/старших 16 битах одного 32-битного слова; 812-й -- аналогично, но 8 каналов в половинках 4 последовательных 32-битных слов).
    • А вот в CAMAC'овских -- вполне можно б было сделать, конкретно в ADC200, ADC502 и ADC4; в ADC333 уже нет, т.к. там данные кладутся вперемежку от всех каналов (зато там можно ненужные просто отключить аппаратно -- они даже и меряться не будут).
    • Ну и в ADC250 этот подход также вполне применим.

    Насколько такое РЕАЛЬНО требуется -- вопрос отдельный.

  • 10.02.2020: насчёт "отбрасывания первых 18 измерений" --
    В первых 18 точках находятся данные до получения триггера. Таким образом можно считать, что получение данных начинается за 18 отсчетов до получения триггера запуска
    (цитата из описания *ADC4X250_CHx.pdf).
    • По-хорошему, надо ведь начинать чтение с 18-й точки -- у нас это обозвано ADC4X250_TIMER_PRETRIG.
    • Как сказал Котов, для получения, например, 1000 измерений достаточно в регистр TIMER записать именно 1000, а уж 18 модуль прибавит сам

      И, в реальности, ещё 18 он и в конце добавляет -- это всё нужно для ADC1000.

    • Но, в принципе, можно ж выпендриться в стиле f4226 -- позволить "заглядывать в данные ДО запуска", сделав для этого лишний канал PREHIST.

      И технология, как правильно отображать временнЫе метки к таким графикам уже тоже придумана, ещё 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: пора добавлять возможность указывать в auxinfo начальные значения каналов -- нужно для параметров PTSOFS и NUMPTS, а главное -- для TIMING, TRIG_SOURCE и TRIG_INPUT.

    17.02.2020@дома: сделано.

    • Использование Init1Param() там УЖЕ было, только надо было в нём раскомментировать условие "me-^gt;nxt_args[n] < 0".
    • Что любопытно, реализация чуть отличается от оной в прочих драйверах (CAMAC, cPCI): после условного присвоения тут дальше выполняется ValidateParam().
    • Ну и добавлена таблица adc250_params[] (с соответсвующими _lkp[]'ами).

    21.02.2020: проверил -- работает, так что "done".

  • 05.03.2020: усовершенствования в отношении "калибровок": ещё на прошлой неделе в разговоре с Павленко и Котовым было выяснено, что:
    • "Динамическая" калибровка бывает только в ADC1000 и, потенциально, в ADC500 -- т.е., в "обратно-мультиплексирующих" моделях. А в ADC250 её нет.
    • Вся прочая работа с калибровками выполняется исключительно внутри прибора -- снаружи он получает только команду на калибровку.
    • Кроме того, прибор калибруется автоматически сразу при включении питания.
    • Таким образом, у него не бывает состояния "не откалиброван".

      Следовательно, не может быть ни команды "забудь калибровку!", ни статусного бита "откалиброван или нет"; и понятие "visible calibration" также бессмысленно.

      Вывод: надо это наследие ADC200ME/ADC812ME удалять.

    05.03.2020: удаляем каналы FGT_CLB, VISIBLE_CLB и CLB_STATE.

    1. Сами каналы в _drv_i.h и devtype закомментированы (не удалены, чтобы остались "следы" и эти позиции ничем бы по возможности не занимались -- для унификации и "на всякий случай").
    2. И использование их удалено и из драйвера, и из скрина.

      В частности, в скрине в окне "Ctrl..." кнопка "Calibrate" перетащена в ячейку (0,0) калибровочной сетки, а всё остальное (включая clbgrdg) убрано; разве что сама clbform осталась, содержащая сейчас единственный clbgrid.

  • 09.03.2021: надо уметь менять значения PLL-регистров -- PLL1_CTRL и PLL2_CTRL. С ними весьма непросто:
    • они оба 32-битные,
    • в сумме в них аж 8 параметров,
    • а для "актуализации" надо записывать 1 в регистр PLL_UPDATE (причём желательно ОДИН раз, после смены ВСЕХ параметров.

    *Пока что*, чисто для отладки, будем записывать вручную bivme2_test'ом, но вообще-то надо бы какой-то регулярный и консистентный механизм через каналы. Но совсем неясно как; плюс, в группе "w" остались свободны лишь каналы 30...39.

    10.03.2021: в порядке исследования вопроса:

    • В качестве простого хака "шоб прям щас" можно сделать просто 1 канал записи, который бы прописывал предопределённые значения в регистры PLL1/PLL2: например, запись 0 отправляет туда "умолчательные" значения, а запись 1 -- значения для bivme2_rfmeas.

      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
      Умолч.0x0300A00220x0A30x00040F0110x0F A:0 B:15 N:60400
      Треб.0x0200800440x0820x30030F0110x0F A:0 B:15 N:60330

      Смущает, что в описании "регистры ADC4X250_CHx.docx" от Котова сказано, что по умолчанию PLL1_FB_PATH=0x02 (а не =0x03, т.е., PLL2, а не PLL1), а PLL2_M_DIVIDER=3 (а не =0, хотя тут эффект тот же, /3). Антону Павленко отправлено письмо с вопросом.

      10.03.2021@вечер: ответ получен: скорее всего, "косяк в документации, последний раз коррекции в документацию вносились раньше, чем коррекции в встроенный софт". А сами эти странные наборы параметров -- вынесенная в доступ настройка некоторых делителей чипа AD9523.

    11.03.2021@ванна: появилась ясность, как всё-таки делать поддержку смены PLL-регистров.

    • Завести таблицу поддерживаемых пресетов, причём [0]-й элемент не используется.
    • Запись в этот канал значения в диапазоне [1...countof()-1] вызывает прописывание в регистры значений из соответствующей строки таблицы пресетов с завершающим PLL_UPDATE:=1.

      ...а потом сразу же выполняется чтение.

    • Чтение заключается в вычитывании значений регистров и поиске соответствующей им строчки в таблице пресетов. Причём поиск идёт с бОльших номеров в сторону --
      for (val = countof() - 1; val > 0; val--)
      -- т.е., при ненайденности будет автоматически вёрнуто значение 0.
    • Текущие значения PLL1/PLL2 отдавать в ещё паре каналов. Каналы эти должны быть TRUSTED, а отдаваться одновременно с чтением канала-селектора -- таким образом они автоматически будут и прочитаны при инициализации, и обновляться при смене пресета.
    • Для возможности указывать нужный пресет в конфигурации (чтобы оно сразу ставилось в нужный режим) надо завести auxinfo-параметр "pll_preset" -- LOOKUP, в результате ненулевого значения которого будет выполняться запись прямо из _init_d().
    • Вопрос только, что делать, если команда на смену пресета прилетает во время измерения.

      В принципе, можно факт запроса запомнить и отпаботать потом, после окончания измерения (или по STOP'у).

      Но проще пока что не заморачиваться -- пусть это будет ответственностью юзера.

    11.03.2021@вечер: делаем, по вышеприведённому проекту.

    • Заведены каналы PLL_PRESET =39 и CUR_PLL1_CTRL =98 с CUR_PLL2_CTRL =99. Уже начинается дефицит свободных номеров.
    • Запись, чтение и отдача PLL1/PLL2 -- точно по проекту. Таблица названа pll_presets_table[].
    • Само прописывание в железку делает ActivatePLLPreset(). Если что, она и при отложенной активации пригодится.
    • Указание пресета в auxinfo -- прямо через nxt_args[ADC250_CHAN_PLL_PRESET]; благо, в обычной pzframe-деятельности эта ячейка никак не используется.

      И как раз для этого -- ВТОРОЕ использование -- и была сделана ActivatePLLPreset().

    12.03.2021: и в GUI это всё добавлено.

    15.03.2021: только, как выяснилось, алгоритм вычитывания для определения номера текущего пресета СРАЗУ после записи -- не работает: в регистрах ещё сколько-то (>100ms) остаются старые значения.

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

    15.03.2021@~17:30, дворами вдоль Морского по чётной стороне: чуток рассуждений на тему "какого хрена оно так?!":

    1. Может, на эти регистры впрямую отображаются регистры чипа AD9523 и такие "тормоза" -- работа именно этого чипа?

      Тогда тут вообще сложно что-то сделать.

      Разве что ADC250 должен бы тогда иметь внутри БУФЕРНЫЕ регистры, в которые изначально при включении вычитывать из чипа (хотя -- ведь он же сам туда и прописывает значения!), а при чтении по VME отдавать уже из этих буферов.

    2. А если нет -- т.е., это внутренние заскоки ADC250 -- то ситуация идиотская: имеется некий макроскопической длительности процесс, для контроля за которым у софта нет никаких средств: ни IRQ, ни статусных битов.
    3. Использовать в качестве "статуса завершения" поллинг "а когда из регистров начнут читаться записанные значения?" точно нельзя -- смысл-то ведь именно в ПРОВЕРОЧНОМ чтении.

    16.03.2021: немного анализа в попытках найти решение на случай, если Павленко и Котов скажут, что "так и должно быть".

    • Была рассмотрена идея:
      1. "А давайте возвращать текущие значения прямо из _init_d(), чтобы не надо было выполнять чтение из _rw_p()"...
      2. ...и пусть ActivatePLLPreset() сразу же возвращает все 3 канала -- номер пресета и значения регистров.

      Для чего проведён анализа работы cxsd_hw.c с точки зрения инициализации устройств.

      • С одной стороны, устройства рождаются в состоянии OPERATING (а не NOTREADY, как когда-то хотелось), и теперь ясно, почему: чтобы прямо из _init_d() могли бы быть вёрнуты какие-то значения, и они бы считались валидными, а не "полученными до оживления устройства".
      • Так что в принципе -- вернуть прямо из _init_d() можно.
      • НО, с другой стороны -- при старте устройства после успешного завершения метода init_dev() сразу вызывается ReviveDev(), который тут же вызовет ReqRofWrChsOf(), что приведёт к запросу ЧТЕНИЯ каналов записи, в т.ч. и pll_preset, какового чтения мы надеялись избежать (потому, что СРАЗУ после записи читаться будет старое, а не свежезаписанное).

      Так что увы -- "по-простому" сделать не получится...

    • Читать вначале ПРИНУДИТЕЛЬНО, а потом только писать и возвращать текущие значения из pll_presets_table[pll_preset_n].

      Причём "номер текущего пресета" хранить в privrec'е, полем pll_preset_n.

    • Тут будет проблема с неизвестными значениями -- то, что сейчас preset#0: там НЕЛЬЗЯ возвращать значения [n].pll1 и [n].pll2, которые для #0 просто 0,0, а нужно возвращать РЕАЛЬНЫЕ значения.
    • Откуда вывод: нужно иметь ТРИ "кэш"-поля pll_preset_n и cur_pll1_ctrl с cur_pll2_ctrl, последние вычитываются вначале, а при активации пресетов туда копируются значения из таблицы.

    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() добавил, но...

    • Там вся архитектура рассчитана на то, что пресет будет не только писаться, но и читаться. А это -- не работает, увы...
    • Ну ладно ещё принудительное вычитывание+возврат значения CHAN_TIMING в ActivatePLLPreset() добавить можно, да.
    • Плюс, ещё и чтение CLK_SRC в CHAN_TIMING сделано ДО активации указанного в auxinfo пресета -- там явно надо переупорядочить. 19.01.2022: переупорядочено, см. ниже.

    В любом случае, реализовывать это ДО того, как Котов пофиксит чтение-после-записи (чтоб читалось из "теневых регистров"/кэша) -- крайне противно...

    19.01.2022@утро-душ-~06:50: ну так можно сделать "fallback" -- если версия прошивки меньше, например, 999, то просто вернуть указанное для записи значение, а чтения не выполнять.

    19.01.2022@~07:10: сделано. При BASE_SW_VER < 999 выполняется возврат указанного значения и сразу goto NEXT_CHANNEL.

    19.01.2022: делаем далее:

    • Добавлен возврат CLK_SRC в CHAN_TIMING. Причём в ОБЕ ветки:
      1. В "fallback" -- там возвращается только что записанное значение из pll_presets_table[].
      2. В ветку "чтения для поиска текущего пресета по таблице" -- уже прочитанное из регистра значение.

      ЗАМЕЧАНИЕ: возврат делается не ReturnInt32Datum()'ом, а Return1Param()'ом -- чтобы значение попало и в "параметры".

    • И поле clk_src переставлено в НАЧАЛО pll_preset_t, чтоб шло по порядку регистров (вчера было добавлено в конец).

      ...а использование -- запись, чтение, диагностическая печать -- изначально было в порядке регистров.

    • Касательно порядка "чтения CLK_SRC в CHAN_TIMING" относительно активации пресета: оно переупорядочено, и слегка хитрО:
      • Активация пресета переставлена повыше, ПЕРЕД чтением CLK_SRC.
      • А оное чтение сделано условным -- только если пресет НЕ активируется.
      • И, коли так -- то в ветку активации пресета также добавлено Init1Param(me, ADC250_CHAN_TIMING) значением из пресета.
    • В StartMeasurements() закомменчена запись CHAN_TIMING в CLK_SRC -- это ведь и не нужно, и даже вреднО. Да, в отличие от всех предыдущих осциллографических АЦП, которые БЕЗ чипа PLL и потому должны настраиваться при программировании всего прямо перед запуском.

      Кстати, отсюда следует, что такое наличие PLL-чипа (точнее, такое его программирование) сильно ломает саму модель "PZframe": мы не можем просто выполнить "чтение с такими-то параметрами", а настройка режима должна выполняться предварительно. И даже "умное кэширование" (запись параметров пресета только в случае, если включаемые пресет не совпадает с текущим) тут не спасёт: ведь при различии запись придётся-таки делать, а она НЕ может делаться прямо перед измерением -- из-за той задержки активации.

    Вроде всё -- теперь перепроверять работающесть.

    P.S. Общее впечатление -- уже привычное при работе с VME: на выполнение простейших вещей тратится ДИКОЕ время (и неадекватно много усилий), и всё из-за того, что в аппаратуре реализовано криво (она не позволяет прочитать только что записанное). Т.е. -- опять море бездарно потраченного времени.

  • 09.03.2021: надо бы уметь показывать состояние битика PLL_LOCKED из регистра 0x000100/STATUS -- это требуется для проверки работы смены PLL-регистров.

    Возможно, что и не только его -- там битов-то в регистре статуса много.

    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 добавим.

    Посему:

    • Добавлен канал PLL_LOCKED, =48 (остался последний свободный в группе -- 49-й).
    • Зарегистрирован в chinfo[] с типом INDIVIDUAL.
    • И сделано вычитывание в _rw_p() -- очень простое и очевидное.
    • Теперь обучение клиента этому каналу:
      • В _chn_dscrs[] он был добавлен аналогично HWINFO-каналам -- chtype = PZFRAME_CHAN_IMMEDIATE_MASK | PZFRAME_CHAN_ON_CYCLE_MASK.
      • И в панель [Ctrls...] он добавлен, НАД калибровками -- кстати, стало даже красивее.

      Проверено на симуляторе (cxsd -S) -- меняется сразу же при изменении в сервере, не дожидаясь прихода кадра.

      10.03.2021@вечер: а ещё бы проверить, почему в ИСПРАВЛЕННОМ клиенте при натравливании на не-рестартованный сервер, в котором этого канала не было, ручка показывалась просто в состоянии "fail", а не чёрная NOTFOUND.

      07.04.2021: да, разобрался (за почти месяц!!!) и исправил.

    11.03.2021: да, работает, "done".

  • 09.07.2021: Антон Павленко выдал парочку ADC250 с обновлённой прошивкой -- BASE_SW_VER=0x07. Главное отличие в том, что точки в 32-битном слове теперь идут в "правильном" порядке -- младшая/чётная в младшем полуслове, старшая/нечётная в старшем (типа, "настоящий" big endian, а не странная комбинация с little endian'ом). Т.е., надо теперь в драйвере сделать, чтоб при версии>7 попарный свпоппинг чётных/нечётных бы НЕ выполнялся.

    А также Антон сказал, что у них перепрошивка выполняется по VME, и исходники утилитки доступны в GIT'е -- vme_firmware.c (а вот описания, увы, нету 14.07.2021: описание Павленко прислал мылом, файл (переименованный по моим правилам) 20210714-pavlenko-Obnovlenie-PO-po-VME.docx). В принципе, можно слабать свою утилитку командной строки (чтоб не требовалось ихние контроллеры втыкать).

    12.07.2021: приступаем.

    1. Для начала надо этот номер версии где-то хранить, а то сейчас он лишь возвращается наверх в соответствующем канале.

      Для этого введено поле privrec.BASE_SW_VER, заполняемое в ReturnDevInfo() одновременно с возвратом наверх.

    2. И теперь в ReadMeasurements() попарный своппинг точек выполняется только при BASE_SW_VER<7.

      Также модифицирована и старая, не-векторная ветка (которая #else'ная): поскольку там выполняется по-int32'шное чтение и потом складирование 2 штук подряд, то введена вторая ветка, в которой складируются в обратном порядке.

    22.02.2022: всё давно проверено и работает, так что "done".

  • 09.10.2021: надо бы разобраться с AUTOUPDATED-каналами, в частности с XS_DIVISOR и NUM_LINES: они, похоже, просто НИКОГДА не возвращаются.

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

      • Причина, по которой XS_DIVISOR'ов тип у части осциллографов сменен с CHTYPE_AUTOUPDATED на CHTYPE_STATUS, описана 10-07-2016 (искать по ключевому слову "1008").
      • Каналы XS_FACTOR у ВСЕХ осциллографов имеют тип CHTYPE_AUTOUPDATED (и значение -- -9 (наносекунды), возвращаемое одинаково Return1Param()'ом),
      • ...а каналы XS_PER_POINT -- повсеместно CHTYPE_STATUS.
    • OK -- переделываем XS_DIVISOR на режим CHTYPE_STATUS.
    • Проверяем -- да, проблема ожидаемо исчезла.

    Теперь обращаемся к ADC250_CHAN_NUM_LINES:

    • Он почему-то НЕ возвращается -- timestamp числится =1 (NEVER_READ),
    • ...хотя в InitParams() возврат вроде есть.
    • Причём adc333_drv.c, содержащий ровно такой же код -- возвращает (проверено на живом устройстве).
    • Причина оказалась в косячном adc250.devtype, где были указаны неверные номера каналов:
      line<0-3>totalmax 110
      num_lines       114
      
      вместо надлежащих
      line<0-3>totalmax 120
      num_lines       124
      
      (соответствующих числам в adc250_drv_i.h).
    • После исправления косяка проблема исчезла.

    Вот теперь считаем за "done".

  • 10.01.2022: Котов прислал описание новой версии прошивки -- 0x08, с новыми фичами, которые могут быть небесполезны.

    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. Поэтому:

    • Канал FRQDIV работает в "настоящем" диапазоне [0...65535].

      21.01.2022: и даже {R,D}={1,-1} делать не будем -- чтоб не было путаницы между вариантами "сырые данные" и "конвертированные данные".

    • ...для старых прошивок -- форсим значение 0.
    • А уж отдача XS_DIVISOR -- та возвращает значение "FRQDIV+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 (если туда добавить параметр).
    • 21.01.2022: да, параметр "frqdiv" добавлен.

      И для "правильности" вместо констант 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.
    • И нет, для канала FRQDIV наверх НИКАК не отдаётся флаг "CXRF_UNSUPPORTED" при старых версиях.

      Просто инфраструктура с VALIDATE-параметрами в pzframe никак для этого не рассчитана -- поскольку все действия выполняются автоматически (пусть даже и в самом драйвере, а не в pzframe_drv.c), то тупо некуда вставить такие проверки с возвратами.

      Можно, конечно, перевести канал на INDIVIDUAL и добавить проверку и отдачу флага, но зачем? Практической-то выгоды ноль.

    Т.е.:

    1. InitParams() и ValidateParam() поддерживают гарантированно-осмысленное значение cur_args[FRQDIV] безотносительно реальной версии железа (форся =0 при неподдерживаемости фичи).
    2. А уж XS_DIVISOR просто считается равным FRQDIV+1 уже безо всяких проверок BASE_SW_VER.

    ЗАМЕЧАНИЕ: по-хорошему, это ведь не FRQDIV в смысле "делитель частоты", а скорее "число измерений, пропускаемых для складирования 1 измерения в память". Но по-факту во всех осциллографодрайверах это "FRQDIV" использовалось также не как "значение делителя частоты", а как "режим деления частоты".

    21.01.2022: вроде добито (часть мелочей уже сегодня).

    Проверяем -- проде реакция есть: при указании значений существенно больше 0 (10000, 1000) измерения (запрошено 10000 точек) начинают идти существенно медленнее.

    ...а вот ПОДПИСИ на горизонтальной шкале меняются явно не в ту сторону -- числа УМЕНЬШАЮТСЯ, а должны УВЕЛИЧИВАТЬСЯ.

    • Очевидно, формула "XS_DIVISOR=FRQDIV+1" неверна.

      Ну да: раз "XS_DIVISOR", то значение x-секунд на него должно ДЕЛИТЬСЯ (что и происходит в FastadcDataX2XS() -- (x * multiplier) / mes_p->xs_divisor).

      А нам надо УМНОЖАТЬ.

    • Вывод: надо всегда отдавать XS_DIVISOR=1, а XS_PER_POINT=4*(FRQDIV+1).
    • Сделал. Да, нужный результат достигнут.

    Ещё замечание: надо спросить Котова, как эта децимация взаимодействует с "18 первых измерений надо пропустить"? И где теперь это описано, о пропускании? И что насчёт пропускания на ADC1000?

    23.01.2022: спросил, ждём ответа.

    24.01.2022: ответ получен:

    • О децимировании:
      • Для ADC1000 оно "пропускает" и записывает группами по 4 измерения (в точности, как в ADC250). Поэтому можно считать, что пропускание не реализовано, т.к. работает очевидно неправильно.
    • О "18 первых измерений надо пропустить":
      • "Триггер по прежнему приходит на 18 точку. Точки до триггера тоже децимируются."
      • Насчёт отсутствия в описании "18 первых измерений надо пропустить": "Пока осталось. Хочу убрать в будущем.".

        25.01.2022: в ответ отправлена просьба и в будущем не трогать.

      • В ADC1000 пропускать "18*4 (читать с адреса 18*4*2=144(dec))" (формулировка моя).
adc1000_drv:
  • 21.02.2019: раздельчик для специфики с драйвером одноканального ADC1000.

    Таковой драйвер в виде исходника был создан одновременно с 4-канальным, но реально список ListOfVmeDrivers.mk::VMEDRIVERS никогда не добавлялся, поскольку был недоделан (как минимум блок чтения с должным перетасовыванием данных так и не реализован).

    01.11.2019: переименовываем заголовок раздела, добавляя "_drv" -- чтоб было как у всех прочих драйверов.

  • 10.01.2022: "живые" ADC1000 появились, и описание от них Котов прислал, так что уже можно (нужно!) делать драйвер.

    11.01.2022: немного размышлений и обсуждений:

    • Как технически делать исходник драйвера? Напрашиваются 2 варианта:
      1. Полностью отдельный файл adc1000_drv.c, в который изначально скопировать adc250_drv.c, а потом подправить.

        Минус -- дублирование кода, так что при любом следующем изменении нужно будет синхронно исправлять уже ДВА файла.

      2. Сделать ОДИН файл, в котором чуть разные куски для разных моделей. Например, adc4x250_common_drv.h, с #if'ами, который бы уже #include'ился самими adc*_drv.c, #define'ящими соответствующие имена.

        Минус -- громоздкость и усложнённая читаемость.

        С другой стороны, плюс -- различия будут все очевидны, т.к. всё рядышком.

        ...но ещё одна проблема -- что с именами каналов делать.

        1. Или, по аналогии с KOZDEV, сделать "базовый" набор ADC4X250_CHAN_* в adc4x250_common_drv_i.h, который уже отображать в конкретные ADC*_CHAN_*?
        2. ...или, раз набор каналов ADC1000 -- подмножество набора ADC250 (комплект всего из 1 экземпляра вместо 4), то можно именно ADC250_CHAN_* считать за базовые, alias'я на них ADC1000_CHAN_*.

        С точки зрения простоты второй вариант выглядит предпочтительнее.

      Пока склоняюсь ко второму варианту -- всё-таки так отсутствует дублирование, плюс, всё обозримо (плюс, если всё-таки появидся ADC500, то его сюда интегрировать будет уже совсем несложно).

    • Возникает вопрос: раз по факту там 4 раздельных АЦП, то, теоретически, возможно установить им РАЗНЫЕ режимы работы -- в частности разные режимы усиления.

      И как, интересно, с этим обходиться? При записи-то ещё ладно -- писать во все 4 одно и то же; а при чтении кого из них считать за "значение", если они вдруг отличаются?

    • Спросил Котова -- он говорит, что в прошивке форсится одинаковость; по факту вроде как используются настройки 0-го канала (точнее, они форсятся в остальные 3).

      (Надо будет проверить, но если оно так -- то проблемы просто нет, это как бы реально 1 комплект настроек.)

    • Также есть плюс в текущей реализации (base_sw_ver>=0x08): теперь там нет никаких "TRIG_ORDER", а данные просто кладутся последовательно с начала памяти.

      ...правда, надо бы понять, что там с пропуском измерений в начале, которые ADC4X250_TIMER_PRETRIG=18: видимо, нужно пропускать 18*4 слов?

    16.01.2022: наступило понимание того, как именно будем делать:

    • "Мясо" от всех 3 драйверов будет лежать в 1 общем файле, который назовём adc4x250_meat.h.
    • Файлы же adcNNN_drv.c сведутся к #include'нью его с предшествующим определением того, какого устройства это драйвер.
    • "Определения" -- это будут #define-символы DEVTYPE_ADC250, DEVTYPE_ADC500, DEVTYPE_ADC1000.

      Т.е., например, adc250_drv.c превратится в просто

      #define DEVTYPE_ADC250
      #include "adc4x250_meat.h"
      
    • В самое начало 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_drv_i.h, #include'ащий adc250_drv_i.h, но определяющий несколько своих 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,
    • и добавлен первый селектор -- определяющий, какой именно adc*_drv_i.h #include'ить.
    • В него же вставлена и проверка-запрет ADC500: по DEVTYPE_ADC500 делается #error.

    28.01.2022: приступаем плотнее.

    • Дополняем adc1000.devtype современными фичами.
      • Так-то, оказывается, там всё было сделано "как надо" -- все каналы, идущие в adc250.devtype блоками "<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).
    • 31.01.2022: для совместимости добавлены alias-имена с '0', совпадающие с ADC250'шными: line0=data, range0=range и т.д.

    29.01.2022: продолжаем...

    • С adc1000_drv_i.h ситуация такая:
      • Касательно самого факта его существования и наполнения:
        • Во-первых, он также уже был, тоже за 11-08-2017, практически полный.
        • Во-вторых, в pzframes/adc1000_data.c ведь по-хорошему нужно будет использовать ТОЛЬКО adc1000_drv_i.h, БЕЗ adc250_drv_i.h.

        Поэтому принято следующее решение:

        1. Файл будет иметь ПОЛНОЦЕННОЕ наполнение. а не "#include'ащий adc250_drv_i.h, но определяющий несколько своих ADC1000_CHAN_* для одноканальных свойств (чтоб БЕЗ циферки 0-3)", как задумывалось 16-01-2022.
        2. А в adc4x250_meat.h будет лёгкая шизофрения (и так там уже присутствующая и неизбежная при этой модели) -- в основном код будет ссылаться на ADC250_CHAN_*, но для специфичных фич -- на ADC1000_CHAN_*.
      • Добавлены "современные фичи" (run+run_mode плюс всё касательно PLL), закомментированы отсутствующие, касающиеся калибровки.
      • Блоки MIN/MAX/AVG/INT и TOTALMIN/TOTALMAX/NUM_LINES переведены с "каналы идут друг за дружкой" на "идут с дырками по 3 канала", для идентичности с ADC250.
      • ...а вот вместо одиночного OVERFLOW восстановлены каналы OVERFLOW0,OVERFLOW1,OVERFLOW2,OVERFLOW3 -- ведь они (как и нетронутые тогда каналы CLB_FAIL*/CLB_DYN*/CLB_ZERO*/CLB_GAIN*) касаются не "логического канала", который 1, а именно АЦП, которых 4.
      • Тип канала данных сменен с 'i' (int32) на 'h' (int16); для ADC250 это --
        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 АЦП).
      • Константы-перечислители типов PGA переименованы из ADC4X240_PGA_VAR_* в ADC1000_PGA_VAR_* -- так оно не конфликтует с ADC250_PGA_VAR_* (по-хорошему оно вообще должно бы жить в adc4x250_defs.h в виде ADC4X250_PGA_VAR_*, но драйверу-то оно нафиг не нужно, а нужно (возможно!) только клиентам).

    30.01.2022: и далее...

    • Скрин: сделан довольно несложно, копированием соответствующих файлов adc250_*.c и их модификацией.
      • adc1000_data.c делался с подглядыванием на f4226_data.c.
        • Основное -- по-линейные каналы были заменены с DSCR_X4() на явные штучные указания.
        • Включая собственно IS_FRAME-канал.
        • Но конкретно каналы OVERFLOW и CLB* так и остались в DSCR_X4(), т.к. их всё же 4 штуки, по числу АЦП.
      • adc1000_gui.c:
        • Для унификации оставлен тот же базовый вид, что в ADC250 -- cpanel ПОД графиком, а не сверху, как в прочих 1-канальных (F4226, C061621).
        • Лампочки "OVERFLOW", которых всё же 4, пришлось сделать отдельной строчкой-формой, расположенной ниже, уже ПОД строкой TRIG_TYPE/TRIG_INPUT.

    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"...

        • "DEVVAR"? -- неа, плохо: "device variable" (переменная устройства).
        • Во -- "DEVDEP", "device-dependent"!
        • Часом позже: ещё лучшим вариантом было бы "TYPEDEP" -- "type-dependent", но оно всего одной буквой отличается от ключевого слова "typedef", так что никак нельзя.

        Итак -- переименовано: все "DEVSPEC" заменены на "DEVDEP".

      • Ну и за компанию было чуть изменено "ограничение" на значение PTSOFS -- теперь оно "-2" от NUMPTS, а не "-1"; т.к. "-1" получить невозможно из-за алгоритма чтения "парами точек".

        (Можно, конечно, выпендриться и в 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: пытаемся сделать "по-правильному". Вчера весь вечер и сегодня всё утро обдумывал, как же надо сделать. Соображения:

    1. Код должен сохранить читабельность.

      В этом смысле напихивать всякие "__CX_CONCATENATE()" вместо "adcNNN_privrec_t" -- точно не вариант.

    2. Для отлаживабельности через GDB имена должны быть реально разными между разными драйверами.

      Т.е., просто поименовать везде "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_90 и adc1000_a4) поведение PLL-related вещей:
      1. Состояние PLL_LOCKED: ADC250 показывает как "locked", а ADC1000 -- как "fail". Хотя код один и тот же.

        Возможно, что-то недоподключено.

        Либо всё же косяк в коде, но ADC250 уже работали раньше и как-то были доведены до состояния "всё как надо", а ADC1000 впервые потроган драйвером только сегодня утром.

      2. Значения CUR_PLL1_CTRL и CUR_PLL2_CTRL: ADC250 показывает нули, а ADC1000 -- правильные числа из 2-го пресета (в auxinfo у обоих присутствует "pll_preset=vepp5_rfmeas". Причём чтение bivme2_test'ом регистров 0x130,0x134 показывает в них правильные значения.

        Возможно, дело опять в том, что ADC250 уже

        Но возможен также и косяк в драйвере (хотя и в сотрудничестве с "потроганностью" ADC250) -- попытка жульнически обойти проблему "сразу после записи в PLL-регистры из них вычитываются старые значения" как-то аукнулась.

      (Парой часов позже) Что странно, ОБА эти заскока -- ТОЛЬКО на тех 2 устройствах, которые я обсматривал: pll_locked=0 лишь на adc1000_a4, cur_pll1_ctrl,cur_pll2_ctrl=0,0 лишь на adc250_90.

      • Возникла мысль, что причина может быть в том, что конкретно для этих 2 устройств скрины были запущены заранее, ещё ДО запуска сервера и/или до запуска v4bivme2vmeserver'а, что привело к вычитыванию регистров слишком скоро сразу после записи.
      • Но нет: если клиент adc250 действительно был запущен раньше, то adc1000 -- уже ПОСЛЕ запуска всего (проверено по логам -- сравнил времена старта сервера по 4cxsd.log, коннекта драйверов к BIVME2 по 4drivers.log (тут записи отсутствуют -- значит, v4bivme2vmeserver был запущен РАНЕЕ cxsd и потому был готов) и подключения клиентов по 4access.log).
      • Кроме того, это не объясняет pll_locked=0 (оно-то вычитывается постоянно).

    04.02.2022: есть какой-то косяк с отображением значений, причём конкретно в скрине adc1000 -- если натравить на устройство adc1000 скрин adc250 (полу-совместимый за счёт имён-alias'ов), то он показывает другие значения и на реперах, и на тиках вертикальных осей.

    Кстати, заметил, что каналы *RANGEMIN/*RANGEMAX из драйвера не отдаются.

    09.02.2022: разобрался. Косяк состоял из 2 частей, сработавших совместно:

    1. В pzframe_data.c некорректно была организована работа с разными типами -- chan_type -- каналов: для каналов с PZFRAME_CHAN_MARKER_MASK НЕ ловились {R,D}.
    2. В adc1000_data.c значение default_r было 1000000.0, и из-за отсутствия данных о реальном R именно этот миллион и использовался.

    Так-то и в adc250_data.c тоже по ошибке стояло 1000000.0, но, из-за многоканальности, там IS_FRAME каналы НЕ использовались в качестве MARKER, а потому получали правильные данные от сервера (хотя диапазон там всё равно отображался +-8V, вместо правильного +-4V; точнее, ВСЕГДА было +-8V, вне зависимости от реально используемого для данного канала).

    10.02.2022: исправления:

    • Пофиксена работа pzframe_data.c ещё вчера (см. в тамошнем разделе).
    • Значение 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: небольшая экспериментальная работа: попытка смоделировать то, как должно было бы выглядеть вычитывание данных из ADC1000 в "старом" варианте -- при использовании TRIG_ORDER, плюс проверка того, как этот алгоритм будет разложен на регистры (и раскладывабелен ли он).

    06.02.2022: идея алгоритма --

    • Заводим 4 буфера чтения данных adcbuf[4][] (например, по 1024 измерения -- учитывая, что в VME D32 BLT ограничен 256 байтами, как раз будет достаточно и кратно).
    • Перед вычитыванием расставляем 4 "базовых" указателя на эти 4 буфера.
    • В цикле "пока всё пачками по 1024 не вычитали" вычитываем пачки в буфера.
    • Затем присваиваем значения тех 4 "базовых" указателей 4 "рабочим".
    • И в цикле по числу вычитанного делаем следующее:
      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;
      
    • Памятуя, что в PowerPC аж 32 регистра общего назначения, в ARM тоже 31 штука, да и в x86_64 их почти 16, то очевидно все эти w0-w3, a0-a3 и wp прекрасно лягут в регистры, так что производительност будет приемлемой.

    Модель сделана. Детали:

    • Включается этот код #define-символом USE_ADC1000_TRIG_ORDER.
    • adcbuf[4][PER_ADC_BUF_LEN] располагается прямо в privrec'е (ну не в стеке же...).
    • Реализована именно только "модель" -- собственно цикл перетасовки (раскладки из буферов в retdata[]), т.к. интересовал именно этот фрагмент.

    Откомпилировано в ассемблерный код с -O0 (а иначе фиг поймёшь) на x86_64 и PowerPC. На первом -- ну да, используется толпа регистров, хотя сам код и далёк от оптимального (никаких "lodsd" и "stosw", увы); но это, видимо, из-за отсутствия оптимизации. На втором -- вообще нифига не понял (даже не факт, что смог найти точку начала фрагмента).

adc500_drv:
  • 21.02.2019: такового устройства (2 канала по 500МГц), скорее всего не будет -- якобы от него отказались. Но код под него в регистре VERSION (биты 31:30) зарезервирован -- 0x1, так что потенциально девайс возможен, вот и мы раздельчик зарезервируем.

    01.11.2019: переименовываем заголовок раздела, добавляя "_drv" -- чтоб было как у всех прочих драйверов.

:::резерв:::
vdac20_drv:
  • 12.11.2019: создаём раздел (раньше всё об этом драйвере описывалось в разделе "о VME вообще", ещё со времён bigfile-0001.html и v2hw.
  • 12.11.2019: делаем драйвер, путём копирования из старого и несложными модификациями.

    12.11.2019: модификации действительно несложные. А самое "массовое" действие -- замена bivme2_io_ на me->lvmt-> -- вообще обошлось контекстной заменой, да ещё и длина в символах совпадает, так что не пришлось мучиться с подгонкой пробелами.

    Собралось; работоспособность проверим позже.

vadc16_drv:
  • 12.11.2019: создаём раздел (раньше всё об этом драйвере описывалось в разделе "о VME вообще", ещё со времён bigfile-0001.html и v2hw.
  • 12.11.2019: делаем драйвер, путём копирования из старого и несложными модификациями.

    12.11.2019: всё сделано по образу и подобию cdac20_drv.c, только тут ещё работа с IRQ будет, которой пока нет.

dl250_drv:
  • 07.02.2020: просто резервируем место для потенциального драйвера DL250.

    ПОКА в нём надобности вроде нет, но если вдруг появится...

  • 16.03.2020@дома: приступаем к работе над драйвером.

    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, по памяти и глядя на код):

    • Драйвер делаем "по мотивам" DL200'шного -- включая фичу "autoshot" (т.е., автоматическое стреляние раз в N секунд).
    • А вот идеологически устройство отличается от DL200 радикально:
      1. Тут нет разделения на "быстрые" и "медленные" -- устройство одно, оно всегда "быстрое" (квант 4ns), но битность счётчиков такова, что максимальной длительности задержек хватает и для "быстрых" задач.
      2. Фактически, тут 24 независимых устройства в одном: каждому каналу задержки можно независимо указать сигнал-триггер (всего их 11 разных), и по окончанию цикла задержки канал переходит обратно в режим "готов к старту" и будет реагировать на триггер, запускаясь заново.

        В результате хоть канал WAS_SHOT как бы поддерживается (весь код скопирован из dl200_drv.c), но поле last_shot, на основе которого определяется, "был ли выстрел", нигде не заполняется (ибо неясно, где надо), так что канал всегда ==0.

    • Есть у девайса 2 вида регистров, которые влияют на все каналы (1-й вид) или группы по 4 канала (2-й вид):
      1. 24-битовые, в которых каждому каналу соответствует 1 бит.

        Это регистры READY, ENABLE, AUTODISABLE, ... (тут есть как статусные (ro), так и управляющие (rw))ю

      2. "Байтовые", в которых каждому каналу соответствует 1 байт, а всего таких регистров по 6 штук.

        Это регистры всегда управляющие -- BSOURCE и START_SOURCE, причём:

        1. BSOURCE: каждый байт сам не является просто числом-свойством, а содержит некий набор битов, каждый из которых имеет отдельный смысл (0:EXT, 1-4:DZ0-DZ3)
        2. START_SOURCE: аналогично, каждый ниббл является отдельной сущностью (0-2:SOURCE, 4-6:TTL; старшие биты нибблов не используются).

      В карту каналов эти регистры отображаются блоками по 25: 24 -- для каждой линии индивидуально, плюс ещё один (24-й) -- "общий", но только эти общие для разных видов реализованы чуть по-разному:

      1. Для 24-битовых регистров -- это просто канал "все 24 бита", эти каналы так и называются "DL250_CHAN_что_то_24".
      2. A: аналогично однобитовым "булевским" каналам из 24-битных регистров (только процесс чтения и записи этих 24 бит замудрён, вследствие необходимости соотносить 24-битовое значение 6 регистрами по 4 байта).

        Таких каналов 5 групп -- ILK_EXT, ILK_DZ0-ILK_DZ3.

      3. B: это каналы "для доступа ко всем 24 через 1": записываемое в эти каналы значение размножается во все 24. Эти каналы называются "DL250_CHAN_что_то_ALL"

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

      Что им мешало в ПРОШИВКЕ реализовать этот "шаманский танец с бубном" -- я так и не понял.

    • Некоторые регистры -- в т.ч. управляющий регистр DISABLE, при взведённых битиках в AUTODISABLE -- могут менять своё состояние в произвольный момент, о чём никак не сигнализируется.

      Поэтому в функцию _autoshot_hbt(), тут превратившуюся в просто dl250_hbt(), добавлено чтение этих 3 регистров и при несовпадении значений с последними известными -- отдача наверх изменений. Непосредственно чтением-сравнением-отдачей занимается ReportChangedState().

    • А в какой именно момент генерируется IRQ (точнее, по какому сочетанию условий -- учитывая, что по факту в одном блоке 24 независимых линии задержки) -- крайне мутный вопрос. Вроде бы как прерывание генерится по значению регистра READY с учётом того, какие линии разрешены.

      Но каков вообще смысл этого IRQ в случае, если для разных линий используются разные стартовые импульсы с некратными периодами (а это возможно!) -- неясно; и Фатькин это подтверждает.

    Сделано-то оно сделано, и даже скрин сделан, но ничего пока не протестировано -- из-за карантина нет возможности взять девайс из 13-го здания (со стенда клистрона) и вставить в свой крейт.

vsdc4_drv:
  • 07.02.2020: заготовка для драйвера VsDC4, буде таковой потребуется.

    Непосредственной причиной создания раздела (как и заготовки драйвера) послужило то, что устройство несколько нестандартно -- "многовекторно". Это потенциально влияет на всю инфраструктуру vme_lyr.

  • 07.02.2020@дома-вечер-пятницы: почитано описание, и там обнаружилась деталь, сильно отличающая это устройство от прочих: он может присылать не 1 значение вектора прерывания, а 4 -- по одному на каждый канал (каналы-то работают независимо).

    Что сразу ломает стройность имеющейся модели vme_lyr и ставит вопрос, как такое поддерживать.

    07.02.2020@дома-вечер-пятницы: последовательность размышлений:

    • "Нулевая" мысль, показавшаяся бредовой: и чё -- регистрировать для него аж 4 устройства?
    • Первая пришедшая в голову мысль "а не сделать ли как в cankoz_lyr с -- где можно дополнительно указывать НЕСКОЛЬКО идентификаторов устройств.
    • Ведь в принципе у нас есть таблица маппирования -- vect2handle[], где содержатся handle'ы устройств, использующих указанные вектора, и ничто не запрещает из нескольких ячеек ссылаться на один handle.

      Но проблема в том, что процесс подчистки в vme_disconnect() идёт по списку УСТРОЙСТВ, а не по vect2handle[]. И подчистит только ПЕРВУЮ из ячеек.

      10.02.2020: напрашивается, кстати -- ну сделать (аналогично cankoz_lyr'у, где вместо старого devcode теперь есть массив devcodes[10]) вместо поля irq_vect чтоб был массив irq_vects[]. И подчищать ВСЕ вектора из массива.

    • Отдельный вопрос -- а как указывать все эти 4 вектора?
      1. Можно поштучно -- прямо в businfo[], 4 числа вместо 1.
      2. А можно постулировать, что указываем 1 число -- "базу", а остальные идут от неё как +1, +2, +3.

    По некоторому размышлению приняты такие решения:

    1. Регистрируем именно как 4 раздельных устройства.
    2. Номера векторов указываем индивидуально -- 4 числами.

    08.02.2020@дома, суббота, вечер: сделана заготовка, которая пока даже скомпилироваться не сможет. Ключевое, ради чего она делалась СЕЙЧАС:

    • В vsdc4_privrec_t содержатся МАССИВЫ handles[] и irq_vects[].
    • В vsdc4_init_d():
      • Индивидуальное вычитывание 4 векторов из businfo[] со складированием в irq_vects[].
      • Регистрация, в цикле, 4 как бы раздельных устройств, отличающихся только векторами.
    • Замечание: поскольку у VsDC4 адресное пространство 32МБ (25 бит), и jumper'ами указываются только старшие 7 бит (а не 8, как обычнл), то в качестве base_addr используется jumpers << 25 -- 25, а не 24!
    • vsdc4_irq_p() определяет номер "линии" (сработавшего входа) по вектору, просто последовательно сравнивая со всеми 4.

    10.02.2020: спросил Беркаева, понадобятся ли нам на ВЭПП-5 или К500 эти VsDC4.

    Ответ -- скорее всего, нет. По крайней мере, пока для них применения не видно (на K500 работают VsDC2 и их хватает).

    Так что на этом работы замораживаем.

l_timer_drv:
  • 03.11.2020: создаём раздел, поскольку уже надо работать с этим девайсом -- пока что в простейшем варианте, для ВЧ-измерений в клистронке.
  • 03.11.2020: отдельный пункт для "рудиментарного" варианта драйвера -- для синхронизации ADC250 в крейте ВЧ-измерений в клистронке. Обзываем этот вариант "rfmeas_l_timer" (rfmeas_l_timer_drv.c).

    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 (только там адресовалось по номерам регистров, а не по адресам).

    • При записи значения в debug_wr_val выполняется его запись по адресу, указанному в текущем значении debug_rd_addr.
    • При записи значения в debug_rd_addr выполняется чтение по этому адресу и результат вертается в debug_rd_val.

    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 применяется к каналам, которые не просто бессмысленно читать "по запросу", а к тем, которые по факту НЕ обновляются:
      1. Например, версия прошивки устройства.

        Здесь же всякие константы -- фиксированные минимумы/максимумы, CONST32 (=32) etc.

      2. ...ну или обновляются, но крайне редко -- вроде статусных каналов, типа номера текущего состояния vdev-драйверов, когда канал по факту не является "читаемым".
      3. Также *SET_CUR-каналы в плавно-изменяемых ЦАП'оподобных -- это каналы, отражающие текущее записанное/отработанное значение, т.е., "тени" каналов записи.
      4. Во всех _fastadc_common'ах каналам PZFRAME_CHTYPE_AUTOUPDATED и PZFRAME_CHTYPE_STATUS.

        (Почему и _AUTOUPDATED тоже -- вопрос; надо разбираться... В записях ничего не нашлось...)

    02.03.2021: добиваем драйвер в соответствии с полученными разъяснениями:

    1. Оказывается, "специальный запуск" надо делать иногда и однократно, а не по каждому SHOT. По SHOT же нужно только писать лишь EXT_CSR:=1.

      Так что заведён отдельный канал SPECIAL_START и они с SHOT'ом выполняют чуть разные действия (но GATESTAT возвращают оба).

    2. Давно надо было сделать отдачу диагностики из регистра CLK_STAT. Вот с ним сложно:
      1. Часть битов можно читать в произвольный момент, а часть -- "R1c", т.е., обнуляются после чтения; но надо ведь как-то "наверху" показывать их горевшесть в недавнем прошлом!

        Решено завести "кумулятивные" каналы (по аналогии с c_ilk_-каналами в драйверах источников, вроде ist_cdac20).

      2. Когда читать (особенно с учётом п.1) -- тоже вопрос: прерывания-то никакого не делается.

        Решено просто в _hbt().

      Реализация (её можно использовать для всех подобных гадких, мерзких, отвратных и уродских регистров с самостоятельно сбрасывающимися битами):

      • Заведен массив "последних известных" значений битов статуса, last_clks[], и в _init_d() всем элементам делается =-1 -- чтоб следующее вычитанное значение точно не совпало.

        Также заведены -- не массивами, а отдельными int32 -- last_C_PllWasUnlocked и last_C_ExtWasUnlocked, которым также делется инициализация =-1. 03.03.2021: неа, уже не =-1, а =0.

      • Чтение ведётся в _hbt() -- т.е., те же 20Hz.
      • Сначала вычитывается регистр.
      • Потом делается цикл по 8 битам, в котором проверяется, что если текущее значение не совпадает с последним запомненным, то текущее сохраняется в качестве последнего известного в last_clks[] и возвращается наверх.
      • Также конкретно для битов PllWasUnlocked и ExtWasUnlocked -- которые и являются R1c -- дополнительно проверяется, что если текущее значение БОЛЬШЕ последнего известного, то текущее значение туда OR'ится -- для превращения 0 в 1 (а 1, соответственно, остаётся как есть), и отдаётся наверх.
      • Чтобы сэкономить "траффик", все обновления отдаются одной пачкой через ReturnDataSet().

        Для этого заведена пачка массивов и бежит счётчик-указатель count, так что если какой-то канал надо отдать, то его номер и значение записываются в addrs[] и vals[], а затем count++.

        Поскольку всё однотипно, то заполнение также необходимых dtypes[]=INT32,nelems[]=1,val_ps[]=vals+n,rflags[]=0 делается потом в отдельном цикле скопом для всех.

      • Причём значения кналов PHASE и EVENT -- ради которых изначально _hbt() и делалась -- теперь также отдаётся в этой пачке.

    03.03.2021: также доделан скрин rfmeas_l_timer.subsys, так что теперь можно проверять.

    Проверка показала косяки работы "запомненных" состояний: они с самого начала "горели", как будто успели вычитаться в состоянии 1.

    • Причина оказалась в том, что их last-значениям в privrec'е присваивалось =-1, и когда потом в _hbt() делалось me->last_C_xxxWasUnlocked |= v32, то получалось "-1 | 1" и оставалось -1.
    • Так что в _init_d() переделал: вместо присвоения =-1 сделано присвоение =0 и сразу же возврат этого значения.

      Обоснование: это ведь ЗАПОМНЕННОЕ, оно в аппаратуре не существует, а поддерживается только драйвером, так что он имеет полное право отдавать своё значение.

    • @вечер-ванна, уже после окончания тестов: а можно было оставить начальное присваивание как есть, но изменить "запоминание" -- вместо "|=" поставить просто "=": ведь присваивается только "более высокое" состояние, и тогда будет всё работать как надо -- -1 сразу заменится на любое другое, а 0 на 1..
    • Приличия ради бОльшую часть адресов регистров забил в enum -- R_nnn, так что теперь почти везде это вместо констант 0xNNNNNN.

      А для упрощения понимания и сличения с павленковскими инструкциями шестнадцатиричные адреса справа от каждой команды чтения/записи в комментариях.

      Но пока:

      1. Остались 0xAnnnnn в начальном программировании.

        Ибо это часть большого блока-массива, и пришлось бы городить надлежащие _*base и *_incr.

      2. Сами значения всё ещё "магические числа", а не надлежащие константы (для которых пришлось бы много возиться, изготавливая *_shift, *_bits, *_mask).

    Итого: уже вроде юзабельно для нужд ВЭПП-5, но надо б ещё доразобраться с возможностью переключать в режим "пропускать на backplane ВСЕ запуски" (и обратно в "только однократно после команды").

    05.03.2021: приступаем к изготовлению возможности переключать режим работы между "только однократно после команды" и "пропускать на backplane ВСЕ запуски". Благо, после сегодняшних разъяснений Антона Павленко (в ответ на моё сегодняшнее же письмо с вопросом о предполагаемой последовательности команд) стало чуток понятнее, как это сделать правильно.

    • Решено сделать это ОДНИМ каналом "режим", а не троицей "включить одно, включить другое, текущее состояние".
      • При записи 0 включается режим "все", при записи 1 режим "однократно по команде".
      • На чтение отдаётся текущее состояние. Им считается значение бита 5/EnaSingleShoot/0x20 в регистре EXT_SETT/0x400072, который в однократном режиме взведён.
      • Канал получил название "OPERATION_MODE".
    • На данный момент реализовано чтение и начата реализация записи, но стало лень...
    • (А ещё ведь в скрине надо choicebs сделать...)
    • И, очевидно, в adc250_drv.c надо будет добавить что-то при записи в канал "Trigger" вариантов "Back" и "Back+Sync".

      06.03.2021: что, кстати, реализуется неочевидно: там ведь как бы нет никакой "записи в канал", а есть, как в любом pzframe-драйвере, "актуализация" параметров в StartMeasurements(). И как/когда реально выполнять специальную запись? Напрашивается только при "несовпадение текущего с предыдущим и при этом _BP или _BP_SYNC".

      07.03.2021: напрашивается идея сделать то троганье регистров PLL1/PLL2 именно просто отдельным каналом (или каналаМИ), в которые просто отдельно записывать отдельно, для чего ему дать тип INDIVIDUAL.

      08.03.2021: а можно и иначе -- вот пара последовательно пришедших идей:

      1. Можно оставить просто параметрами, но прописывание и актуализацию записью PLL_UPDATE:=1) делать в момент StartMeasurements() только в случае, если текущие значения регистров не совпадают с запланированными.
      2. А можно -- по аналогии с каналом CALIBRATE, который как раз INDIVIDUAL -- при записи в любой из PLL-related параметров взводить ещё флажок, означающий "перед стартом нужно будет обновить регистры PLL1_CTRL/PLL2_CTRL".

    06.03.2021: продолжение:

    • Ну и запись типа сделана -- по 2 команды записи на каждую альтернативу.

      ...но надо ещё раз Антона перечитать и, возможно, что-то добавить.

    • И переключатель в скрин добавлен.

    09.03.2021: был длинный телефонный разговор с Антоном Павленко по обсуждению моих вопросов, заданных в письме от 07-03-2021. По результатам было 2 письма -- 1) Антон прислал значения для записи в PLL-регистры ADC250, 2) я запротоколировал всё обсуждённое. Краткое резюме:

    • Что касается регистров ADC4X250:
      CLK_SRC/0x00012c = 2
      PLL1_CTRL = 0x02008004
      PLL2_CTRL = 0x30030F01
      
      потом PLL_UPDATE.
      
      Процесс переключения источника тактирования занимает ненулевое время.
      Пока считаем, что за 0.1с должно устаканится.
      О состоянии цепи тактирования по битику [0x000100 ? STATUS].PLL_LOCKED.
      Если 1, то всё ок, если 0 - то нет.
      Битик не "интегральный", показывает статус в момент чтения.
      

      Т.е.,

      • Прописывать PLL-регистры в StartMeasurements() категорически нельзя, ни по одному из алгоритмов.

        Нужно как-то выделенной командой. Как именно (учитывая, что регистров 2 штуки по 32 бита, а параметров в них вообще 8 штук) -- пока неясно. Тестировать будем просто ручной записью в регистры bivme2_test'ом.

      • Надо заводить статусный канал для отображения состояния PLL_LOCKED (а может, и прочих).
    • Отработка спец.запуска немгновенна, поэтому
      • Последний шаг цепочки -- EXT_TRG_SRC/0x400204:=0 -- должна исполняться не сразу, а лишь после реального исполнения, которое можен быть определено так же, как и обычный "запуск", по регистру LOG_WR_POSITION/0xC02000.
      • Также возможен облом отработки спец.запуска -- по причине отсутствующести (или ненравящести девайсу) внешнего сигнала синхронизации.

      В связи с этим придуман некоторый алгоритм, по которому исполнение EXT_TRG_SRC/0x400204:=0 будет производиться в _hbt(), причём и таймаут будет детектироваться.

      • Ключевое слово "spec_start_rqd_ticks" -- счётчик (и одновременно флаг "был запущен спец.запуск"), взводимый в =20 при запросе спец.запуска, декрементируемый при >0 в каждом _hbt(), его ==0 после декремента сигнализирует о таймауте, а событие "запуск" при !=0 означает окончание исполнения спец.запуска.
      • Возможен race condition -- что успел произойти предыдущий запуск, но не был определён из-за отсутствия IRQ, а будет "увиден" в следующем _hbt() и будет принят за окончание спец.запуска.

        Поэтому надо прямо в инициировании спец.запуска делать last_LOG_WR_POSITION:=LOG_WR_POSITION.

        И делать это сразу после EXT_CSR/0x400074:=0x02(CloseGate), каковой шаг предполагается в любом случае добавить в начало цепочки команд, для приведения устройства в опреденённое состояние.

        Позже, когда это всё записывал сюда:

        • Кстати, наличие IRQ не спасло бы от race condition: ведь IRQ будут отрабатываться не мгновенно, а "синхронно", получением события из соответствующего файлового дескриптора, так что команда SPECIAL_START=1 вполне может придти уже ПОСЛЕ возникновения IRQ, но ещё ДО его обработки userspace-драйвером.

          И вот как тогда эту проблему решать -- вопрос кабы не более сложный; хотя, скорее всего -- ровно так же.

        • И ещё, кстати, следует битым текстом записать: такое преодоление проблемы -- оно за счёт игнорирования события возможного "последнего" обычного запуска, успевшего придти непосредственно перед инициированием спец.запуска, но ещё не успевшего быть замеченным.
        • А всё потому, что ЖЕЛЕЗКА устроена не очень правильно и сваливает свои внутренние нюансы -- которые, по-хорошему, должны бы решаться в ней самой -- на софт, у которого возможности для корректной работы сильно Уже.

      И, похоже, надо заводить канал "статус отработки последнего спец.запуска" -- удачно/неудачно.

    09.03.2021: делаем!

    • Для сигнализирования "наверх" о результате исполнения спец.запуска заведён канал SPECIAL_START_RESULT=53, с соглашением "0 - таймаут, 1 - успех".

      Также самое событие его прихода является маркером окончания процедуры.

    • Заведено поле счётчика, только названное длиннее -- special_start_rqd_ticks -- с которым проворачиваются предусмотренные алгоритмы.

      (Написано просто, а реально полдня напряжённого кодинга.)

    • В скрин канал "special_start_result" добавлен справа от самой кнопки, ro-селектором.

    Теперь проверять надо -- напряжённая будет проверочка (ещё бы ошибку протестировать -- вынуть кабель).

    10.03.2021: мелкое изменение с каналов SPECIAL_START_RESULT: вчера он был сделан TRUSTED, а это очень нехорошо -- прямо при коннекте приходили бы события UPDATE, что он якобы обновился, хотя реально нет. Поэтому он переделан на AUTOUPDATED_YES плюс ему делается SetChanFreshAge() с et_fresh_age.sec=0x7FFFFFFF, т.е., ~68 лет.

    11.03.2021: проверено -- оно работает.

adcx32:
  • 17.07.2021: раздельчик просто про запас -- на случай, если вдруг занадобится обзавестись драйвером.
lab6_vme_firmware_common:
  • 17.07.2021: раздел для утилитки "менеджер прошивок для VME-устройств Лаб.6 от Павленко, Котова & Co.".

    У них такая утилитка называется vme_firmware, а описание процесса перепрошивки есть в файлике 20210714-pavlenko-Obnovlenie-PO-po-VME.docx, полученном по мылу от Павленко.

    Возможно, придётся обзавестись своей такой же, чтобы не требовалось при любой перепрошивке тасовать блоки (либо свои VME-устройства тащить к ним, либо в свои крейты совать ихний MVME3100).

    ...если сделаю -- то утилитка будет универсальной, под ВСЕ поддерживаемые платформы. Поэтому оно имеет суффикс "_common" (по аналогии с vme_test_common.c). Плюс, чтобы имя было более пристойным, а не бесстыже-декларативно-всеобъемлющим "vme_firmware", добавлен префикс "lab6_".

  • 17.07.2021: некоторые общие соображения.
    1. По аналогии с ихней утилитой, можно эту штуку сделать не просто "прошивальщиком", а именно МЕНЕДЖЕРОМ прошивок -- чтобы он умел и заливать, и выдёргивать, и проверять.

      Поэтому синтаксис имеет смысл сделать таким:

      *_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").

    2. Насчёт того, как указывать устройство -- DEVSPEC: это выглядит совершенно аналогично тому, как указывается в vme_test'е --
      [@BUS_MJR[/BUS_MNR]:]ADDRESS_SIZE:ADDRESS_MODIFIER:BASE_ADDR
      -- но есть пара нюансов:
      1. У разных устройств может отличаться смещение-начала блока регистров EPCS_* от базового адреса устройства: у большинства это +0x000000C0, но у VsDC3/VsDC4 -- +0x01FFFF40.

        Так что надо указывать и это смещение. Напрашивается синтаксис "+EPCS_OFFSET" после BASE_ADDR.

      2. Некоторые устройства работают с 32-битными данными, но часть (s_timer и l_timer?) -- с 16-битными. Поэтому надо как-то указывать битность.

        Напрашивается возможность опционально указывать размер данных после размера адреса -- суффиксом "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, ...
      • ...а открытием файла занимается общий кусок в main()'е.
      • Который пытается открыть файл в нужном режиме, причём позволяет также указывать "-" -- тогда в качестве fd используются STDIN_FILENO и STDOUT_FILENO.

        И помнит в should_close_fd, надо ли делать close(fd), или нет (если stdin/stdout).

    • Вот вся эта вышеописанная логика реализована, с проходом по argv[].
    • Пока что в таблице единственная команда -- devinfo.
    • И доведено до собираемости; в т.ч. проверено, что парсинг "расширенного devinfo" работает.

    Слегка напрягает, что каждую утилиту приходится регистрировать не только в 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').
    • Итого, результат: читает!!! Да, читает все 0x00200000=2097152 байт, увы.

      Но для сравнения с прошивкой в файле от Павленко из прочитанных 2МБ просто вырезается ("dd bs=1c count=СКОЛЬКО_НАДО") нужное количество.

    • Однако тут началась веселуха: при вырезании 707794 байт -- размер даденного firmware.rpd -- MD5-сумма не совпала.

      ...а первого 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). Вкратце (в моём изложении) они таковы:

    1. "Адреса" в EPCS_BUFF_RD_ADDR/EPCS_BUFF_WR_ADDR -- да, указываются в 4-байтных СЛОВАХ.
    2. Почему ихняя vme_firmware.c::reading() в EPCS_BUFF_RD_ADDR пишет ДВАЖДЫ (перед чтением каждого блока и перед чтением слова):
      • Потому, что в D32-устройствах автоинкремент есть, а в конкретно S/L-timer, который D16, автоинкремент закомментирован -- из-за того, что неясно, в какой момент его выполнять (см п.4).
      • 11.01.2022: вот почему чтение прошивки в моём lab6_vme_firmware_common работало, несмотря на отсутствие второй записи (перед чтением слова) -- в ADC250/ADC4X250_CHx автоинкремент есть.
    3. Endianness:
      • Ну да, там всё big-endian.
      • В частности, в их утилите вообще молча предполагается, что она запускается на big-endian-архитектуре (там отсутствует что-либо касательно преобразования порядка байт).
    4. D16 в S/L-timer в какой момент выполняется автоинкремент регистров EPCS_BUFF_RD_ADDR/EPCS_BUFF_WR_ADDR, которые в 4-байтных словах (а чтение/запись-то ведутся 2-байтными операциями!):
      • Ни в какой. Автоинкрементация конкретно в S/L-timer просто закомментирована.

        От меня: видимо, как раз по этой причине.

    5. Как определять размер залитой прошивки и почему файл не кратен 4 байтам:
      • А никак. Просто читать по приведённому алгоритму (в размере EPCS_SIZE), и всё.

        Ибо прошивка -- просто сырой бинарник, скармливаемый FPGA, а та "при старте просто начинает считывать свою прошивку с адреса 0 из флеш и пока не считает всю", и никаких информационных полей там не было предусмотрено.

      • Файл не кратен 4 потому, что "Размер файла прошивки FPGA -- дело производителя FPGA. FPGA считает по SPI столько байт, сколько ей надо. Кратность в 4 байта ? особенность интерфейса vme-epcs (который наш).".

    11.01.2021: за прошедшие несколько дней доделана также и запись -- writeflash_proc().

    • Процедура кодинга была тяжкой: ЧТО конкретно там должно делаться -- великая загадка, ибо в описании от Павленко/Котова сказано по-одному, а в коде их студента сделано несколько иначе, да ещё и код этот несколько индусский.
    • Поэтому сделано так: сначала -- по описанию, потом подсматриваем в ихний код, плюс куски берём из readflash_proc().
    • И сам цикл (реально -- ДВА) сделаны поаккуратнее, чем у ихнего студента (ну да, "похвалил" себя -- а ничего, что сравнивать к.т.н. со студентом как-то не комильфо? :D). В т.ч.
      • Сделано, чтобы при некратности файла 4 -- т.е., если оттуда в конце прочитается <4 байт -- это прочитанное всё же записалось бы во флэш.

        ...а вот если прочиталось <=0 -- т.е., ничего совсем либо ошибка (откуда б?) -- то тогда очередная запись слова уже пропускается.

        Но в обоих случаях взводится end_of_file = 1.

      • И исполнение команды "выполни запись блока" (EPCS_CMD:=EPCS_CMD_WR) производится ТОЛЬКО если в текущий блок было запихнуто !=0 слов.

        (В том исходнике это тоже делается, но условие выглядит столь запутанным, что не сразу его и опознаешь.)

    • Также добавлена УСЛОВНАЯ endian-конверсия.
      • Раньше-то оно делалось безусловно, но то годно для x86, который little-endian, а для BIVME2, где PowerPC, это лишнее.
      • Поэтому вставлен #if, проверяющий BYTE_ORDER, чтоб конверсия выполнялась только на LITTLE_ENDIAN.
      • В readflash_proc() -- аналогично.
      • ...и ради этого в начало добавлено #include "cx_sysdeps.h".
    • 11.01.2022: НИ-ФИ-ГА!!! То добавление "условной конверсии" было позорнейшим косяком!!! В результате получилась следующая фигня:
                  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);
      

      Т.е.,

      • На LITTLE_ENDIAN оно работает, а на BIG_ENDIAN не просто ничего не делает, а даже не копирует из 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...

    • Попробовали прошить ADC250 -- упс, окирпичилось.
    • Котов утащил модуль к себе, там прочитал из него записанное, и обнаружилась странность: ВСЁ состоит из 2 чередующихся слов, причём второе является отзеркаленным первым (этакий "палиндром", (c) Котов).
    • Я посмотрел-посмотрел на свой код и осознал: я endian conversion делаю некорректно!

      То, как оно "делалось" -- страшенный косяк, ибо на little-endian оно работало, а на big-endian -- не делалось даже просто копирования прочитанных добытых из файла значений bytes[] в w (каковая и отправляется в VME).

      Вот оно просто зеркалило туда-обратно мусор, имевшийся в w, потому во флэш и писались чередующиеся значения.

      ...подробнее -- см. comment2 выше, там же, где эта горе-конверсия и описана.

    • Исправил -- вроде зашилось нормально, модуль после этого работает.
    • Но проявился ещё один косяк: не работала программная перезагрузка.
      • ADC250 имеет возможность программного рестарта, путём записи старшего бита в регистр 0x104; т.е., bivme2_test-командой "i:0x104=0x80000000".
      • Но это НЕ работало.
      • Сначала была мысль, что косяк на стороне Котова (честь ему и хвала, что это У НЕГО возникла такая мысль -- не регрессия ли какая-то).
      • Но потом тестами записи в другие регистры оказалось, что пишутся любые числа не больше 0x7FFFFFFF.

        А это уже живо напомнило про косяк strtol() -- что у него числа со старшим битом вызывают "переполнение" и он возвращает значение 0x7FFFFFFF и errno=ERANGE.

      • Проверил -- да, так и есть: в парсинге значений использовалась именно strtol() вместо надлежащей strtoul() (и в куче других мест тоже -- ВЕЗДЕ, кроме парсинга базового адреса в ParseDevSpec().

        КАКОГО ЧЁРТА?!?!?!

      • Исправил -- всё заработало: теперь программная перезагрузка функционирует (при этом, правда, отдаётся VME-ошибка, но это оно так и должно быть).

    Итого:

    • У меня обнаружились 2 косяка:
      1. Некорректное "endian conversion".
      2. Некорректный парсинг чисел в vme_test_common.c
    • А вот у Котова всё работает -- они с Павленко молодцы.

    ...мда, ну я и балбес...

    12.01.2021: добиваем недоделанное вчера:

    • Добавлено нуление массива bytes[] перед чтением из файла -- чтобы при записи неполного слова во флэш туда попадали бы нули, а не мусор.

      ...по-хорошему -- надо бы нулить только если прочитано <4 байт, конкретно непрочитанные. Но лень.

    • Переделана печать в режиме verbose: теперь печатается "\rСООБЩЕНИЕ", а не "СООБЩЕНИЕ\r", как раньше. Смысл -- чтобы если вылезет ошибка, то она напечатается ПОСЛЕ адреса, с которым шла работа, а не затрёт его.

    13.01.2022: новая напасть: при попытке ЧТЕНИЯ прошивки из обновлённых ADC250 (BASE_SW_VER=0x08, в клистронке с BIVME2) получаем облом: сначала чтение отрубается по таймауту на каком-то адресе (причём НЕ по границе сектора!), а следующее чтение срубается уже сразу.

    (Кусок "скриншота" закомменчен тут выше.)

    • Пробовалось на 2 разных устройствах -- 0x90 и 0x92, с примерно одинаковым эффектом (только сами адреса срубания разные -- 0x00014000 и 0x00044400).
    • Причём после этого сам VME-интерфейс остаётся жив -- и devinfo работает, и чтение первых слов адресного пространства тоже.
    • Отправлено письмо Котову+Павленко с вопросом "что бы это было?". Надеюсь, косяк в моей реализации чтения, но кто ж знает...

    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).

    • Причина -- то, что я выполнял чтение на РАБОТАЮЩЕМ осциллографе, который в это время продолжал осциллографировать.
      • Ключ в том, что при запуске ожидания измерения программа в прошивке выходит из основного цикла и начинает просто ждать запуска.
      • А чтение регистров по VME-шине работает потому, что оно производится из обработчика прерывания (VME-обращения в девайсе видятся как прерывания).
    • Следовательно -- можно преспокойно запускать чтение прошивки, если предварительно сделать драйверу этого устройства _devstate=-1.
    • У Котова не получилось воспроизвести это у себя "на столе" с таким же BIVME2, но получилось у нас на крейте bivme2-rfmeas.

      Вероятно, разница в том, что в моём драйвере adc250_drv.c используются прерывания, а у себя на столе он проверял поллингом по регистру статуса.

    • Также как-то по-другому его тест работал на MVME3100 -- "mvme3100 почему то зависает целиком, даже если я читаю разные платы".

      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: проверяем, реально ли ошибка исправлена.

    • Старая версия 0x8 -- да, быстро подвисает.
    • Перепрошил на новую; она, кстати, ощутимо меньше, на процент --
      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
      
    • Обновлённая -- нет; гонялась больше часа, но так и не зависла.
    • 05.02.2022: гоняю уже 6 часов -- по-прежнему не виснет.

    04.02.2022: кстати, был косяк в main()'е в цикле по командам: отсутствовала проверка результата (кода возврата), поэтому ошибка не приводила к завершению работы. Это мешало тестированию "не подвисает ли при чтении прошивки во время осциллографирования" -- там делалось циклом while (приведён в закомментированном виде чуть выше).

    Исправлено -- код берётся, проверяется, и if (r != 0) exit(1);.

:
:
hw4cx/drivers/camac/ -- CAMAC:
  • 13.03.2018: назрело наконец создание раздела "CAMAC".

    Предыдущие драйверы либо копировались из v2cx/, либо (быстрые АЦП/осциллографы) рассматривались в разделе pzframe.

    Но сейчас pzframe уже более-менее дошёл до зрелости, так что там конкретные драйверы обсуждать смысла мало, а надо делать драйвер для F4226, потенциально могущий принести некоторое количество забот и сюрпризов, потому требующий своего раздела.

f4226_drv:
  • 13.03.2018: Фролов наконец-то дал описание F4226 (или оно "Ф4226"?), так что приступаем к созданию драйвера и его раздела.
  • 13.03.2018: судя по описанию, интерфейсно девайс ближе всего к линейке "S" (ADC101, 850, ...): он имеет 1 канал, небольшой разрядности и с выбором частоты и диапазона.

    Вот и будем делать драйвер, взяв за образец c061621_drv.c.

    14.03.2018: начато.

    • f4226.devtype и f4226_drv_i.h скопированы с c061621.devtype и c061621_drv_i.h соответственно, с
      1. Простой контекстной заменой C061621->F4226 и c061621->f4226.
      2. Удалением каналов devtype_id и devtype_str.
      3. Заменой значения _MAX_NUMPTS с 4096 на 1024.
    • f4226_drv.c -- аналогично, только в дополнение к контекстной замене еще удалено немного кода, касающегося этих devtype_*; так он доведён до компилируемости.

    15.03.2018: продолжаем.

    • f4226_drv_i.h:
      • Диапазон значений -- _MIN_VALUE=-10160000, _MAX_VALUE=+10240000, в соответствии с описанием (это параметры для "Вход 2" -- измерительный).
      • Тайминги (F4226_T_*) и диапазоны (F4226_R_*) -- на основе описания регистра статуса (стр.18).
    • f4226_drv.c:
      • Вычищаем всю специфику:
        • Описание A и прочих "свойств" в начале файла.
        • В privrec'е -- devicetype-специфичности memsize, mintick, k_type.
        • А также добыча и использование этой информации.
      • Оставлен только q2xs[], с актуализированным наполнением.
      • Все NAF'ы также удалены.

    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-й.

    • И вроде бы сделана отдача ОТРИЦАТЕЛЬНОГО сдвига по времени (см. 15-03-2018). Тут были сомнения -- КАК это отдавать.
      • Выбран был вариант "вычитать из значения CUR_PTSOFS"
      • Точнее, делается CUR_PTSOFS:=PTSOFS-PREHIST64*64; делается это в самом конце f4226_drv.c::PrepareRetbufs().
      • С одной стороны -- как бы не очень хорошо портить значение CUR_-канала.

        С другой же -- именно mes_p->cur_ptsofs используется в FastadcDataX2XS() для вычисления значения времени для подписи по X-оси графика.

        И более значение CUR_PTSOFS не используется ни для чего.

    • Сделана поддержка в hw4cx/pzframes/ -- f4226_data.c и f4226_gui.c, скопированные с c061621_*.c.

      Из изменений -- вместо удалённой ручки "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.

    Однако:

    1. Остаётся вопрос: а СТОП-то устройству как сделать? Когда/если оно во взведённом в ожидании внешнего запуска состоянии, то как его отменить?
    2. Второй вопрос -- как (и можно ли вообще) выполнить программный старт?

    По списку NAF'ов в описании не ясно ни то, ни другое.

    26.11.2018: изучение документации показывает, что:

    1. В качестве "стоп", похоже, может выступать режим "запись от ЭВМ", в который оно переходит по A(0)F(9).

      Оный NAF добавлен в Init и Abort, ПЕРЕД Mask и Drop.

    2. А вот с программным стартом, похоже, совсем никак...

    28.11.2018: с возможностью использования режима "запись от ЭВМ" в качестве "стоп" -- фигушки!

    • Из него оно по A(0)F(25) в режим "Преобразование" -- НЕ выходит, хоть это и заявляется на стр.36 описания.
    • А выходит после реального выполнения 1024 записей.
    • Похоже, надо на потребность "сделать стоп" забить и "надеяться на лучшее" -- что рано или поздно девайс окажется-таки в работоспособном состоянии.
    • ...хотя -- можно ж РЕАЛЬНО выполнить 1024 записи, да?
    • Чуть позже: ну сделал -- опять облом: проблема в том, что этот глупый девайс зачем-то по окончанию записи и при переходе в режим "Чтение" выставляет LAM.

      Предварительно маскировать LAM?

      Сделано -- вся кухня (замаскировать+сбросить, переход в "Запись", 1024 записи) вытащена в отдельную PerformStop() -- вроде работает как надо, больше LAM'а не возникает.

      ...впрочем, его и так не возникает никогда -- фиг поймёт почему: то ли реально запуски не приходят, то ли я чего не то делаю...

    • Кстати, после включения питания в регистре статуса содержится странноватое число -- 2336=0x920=04440.

      03.12.2018: но то в v5-l-kls3, а в v5-l-cnv (у которого проблемы с Q) -- 2080=0x820=04040.

    А еще некоторые сомнения в реальной рабочести чтения -- на вид, читаются сплошь нули...

    29.11.2018: умаялся я с этой железкой!!! Работает она совсем не в соответствии с описанием.

    • Кодировка режима в регистре статуса -- отличается. Так, постоянно горит 10 -- якобы "Запись", а при переводе A(0)F(9) в "Запись" загорается 11 -- якобы "Монитор".
    • LAM присылается, похоже, не по окончанию измерения или по окончанию записи, а по переходу адресного счётчика через границу: это видно потому, что так же радостно он прилетает и после 1024 чтений.
r0601_drv:
  • 26.03.2018: обнаружилось, что драйвер такой есть, а никаких "сопровождающих файлов" -- _drv_i.h, .devtype к нему нету.

    26.03.2018: попалось случайно, при поиске драйверов для железа управления клистронами.

    Это СДС-16. Использовался, судя по work/pult.20070608/configs/cm5307-9.lst, в "старой", CAMAC'овской, термостабилизации. Точнее, ДОЛЖЕН был использоваться -- реально эти битики Лебедевым никогда заведены не были.

    Также и впредь вряд ли будет использоваться, т.к. в списке описаний помечен как "Не поддерживаем".

    Но раз уж драйвер в дереве есть, то пусть будет "всё как положено".

    • Поэтому изготовлены r0601.devtype и r0601_drv_i.h.
    • А чуть позже обнаружилось, что сам драйвер и не собирался -- "r0601" находилось в ListOfCamacDrivers.mk в за-#'ченной части CAMACDRIVERS.
    • Добавлен в "правильное" место. Собрался.
    • Заодно сам список перетрясён и унифицирован с TYPES_LIST в {types,include/drv_i}/Makefile.
    • Также было обнаружено, что не были включены в сборку драйверы c0601 и i033 -- оба присутствовали в полной комплектации (даже с .devtype'ами!; хотя нет -- i0633_drv_i.h отсутствовал и пришлось сделать), но просто были забыты.

      Также добавлены в список, собрались.

    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-директории, запустить сервер на минутку и потом грохнуть -- сокет закроется, драйвлет корректно завершится с записью статистики).

r0610_drv
  • 27.03.2018: просто резервируем раздельчик сразу после r0601'шного, чтоб были рядом. На случай, если всё же захочется изготовить драйвер, реагирующий на LAM'ы.
  • 27.03.2018: несколько слов о предполагаемом устройстве:
    • Делаться будет, конечно, не абстрактным, а полноценным -- и из-за LAM'ов, и из-за alias'ных каналов (и побитовые, и все биты единым каналом).
    • Да, каналы будут как inprbN, так и inpr16b.
    • Что делать с маской:
      • Можно поставить мониторирование ВСЕХ битов. Вроде "потери" производительности нас волновать не должны.
      • Но место под маску предусмотреть можно -- maskb0...maskb15 и mask16b.
      • В случае реализации каналов для маски --
        1. Надо, чтобы они считывались из устройства (это вроде автоматом должно получиться).
        2. Но нужно также иметь возможность уставить при старте предопределённое значение -- например, "разрешить всё".

          Для этого потребуется auxinfo-параметр mask= (умолчательно =-1, что означает "не записывать").

    • При старте можно также читать "дескриптор", для опознания -- Р0601 это или Р0610. Правда, неясно, что нам это знание даст.
g0401_drv:
  • 02.04.2018@утро-пультовая: делаем драйвер.
    • В качестве "идеологической основы" используем cgvi8 -- по каналам quantN они совместимы.
    • Схему работы -- когда уставки НЕ читабельны из устройства (а хранятся в val_cache[]+rfl_cache[], с начальными CXRF_CAMAC_NO_Q) -- берём из g0603_drv.c

    02.04.2018@утро-пультовая: сделан, компилируется (хотя, естественно, не проверен).

    • Alias-каналы присутствуют, возврат при изменении "тени" сделан не вызовами самой же _rw_p(), а прямо вручную -- дублированным кодом, это оказалось проще и короче.
    • Из регистра управления используются только младшие 3 бита -- код кванта тактирования.

      Остальные биты форсятся в 0 (прямо в _init_d() обнуляются), включая биты "05 - запрет автоматической перезаписи по концу рабочего цикла" и "04 - прерывание цикла работы ГВИ по каналу 7".

    • Заодно обнаружилась некоторая странность с указанием калибровки R среди аналогичных драйверов устройств, работающих с временем:
      • cgvi8_drv.c: указывает R=10.0 (ибо квант 100ns).
      • frolov_d16_drv.c: ничего не указывается.

        Анализ 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.
    • В privrec добавлено поле break_on_ch7 -- де-факто могущее быть 0 или 1, которое:
      • Используется при записи в управляющий регистр.
      • Можно указать в auxinfo -- ради чего добавлена g0401_params[].
    • И логика инициализации этого параметра хитрая: значение берётся из упр.регистра только если текущее значение break_on_ch7<0 -- а в psp-таблице умолчание именно -1.

      Таким образом, "текущее" значение считывается из железа только в случае, если оно в конфиге нет явного задания.

    Кстати, а не сделать ли аналогичную возможность указывать в auxinfo значение и для period_mode?

    03.04.2018: также сделан скрин g0401.subsys (хотя для изменения просто чисел можно пользоваться скрином cgvi8.subsys).

    04.04.2018: мелочи:

    • Добавлена возможность указывать значение кванта времени прямо из auxinfo.
      • Параметр был назван t_quant.
      • Значения указываются в наносекундах -- 100...12800 (это не INT, а LOOKUP).
      • Умолчательное значение -- -1, так что при НЕуказанности читается текущее значение из железки (в точности, как с break_on_ch7).
    • Сам параметр переименован из "period_mode" в "t_quant_mode", т.к. это всё же размер КВАНТА, а не периода.

      Переименование коснулось и поля в privrec'е, и имени канала.

    • Ну и былое поле period_mode_rfl было переименовано в creg_rfl -- так корректнее, поскольку реально это флаги от обращения к регистру управления.
    • Исправлен косяк в формуле пересчёта кодов в 100-наносекундные кванты: там было лишнее умножение на значение номера диапазона --
      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: и еще пара аспектов:

    1. Г0604 на 4-м клистроне НИКОГДА не возвращает Q при записи задержки (хотя должен только "На время перезаписи в ЗУ счетчика..."). Из-за чего каналы задержек там всегда горели бордовым.

      Добавлено принудительное обрезание CXRF_CAMAC_NO_Q при creg_supported==0.

    2. На 2-м клистроне "обычный" Г0401 почему-то стоял в режиме 12.8us/quant (по крайней мере, так читалось из управляющего регистра) и прописывание задержек (порядка 1000ms) вроде бы корректно с учётом этого режима давало кривой результат: модулятор стучал с частотой 1Гц вместо положенных 10.

      В чём конкретно дело -- хбз: возможно, просто глюк.

      Тут и пригодилась возможность указывать t_quant в auxinfo: проставлено t_quant=100 и теперь при запуске сервера выполняется надлежащая инициализация.

    ...а возможно, надо будет всё-таки ещё добавить возможность указания там же "начальных значений" задержек -- чтоб они при старте сервера прописывались.

g0604_drv:
  • 02.04.2018: резервируем раздел сразу после g0401'шного, чтоб были рядышком. На случай, если реально потребуется этот драйвер -- пока есть надежда, что просто заменим единственный ГВИ8 (на 4-м клистроне) на ГВИ8М.

    Основная идея: берём код от g0401_drv.c, но работа с регистром управления удаляется, а функционирование "period_mode" сводится к записи в privrec константы, указанной в auxinfo, и отдаче в канал PERIOD_MODE её же.

    ...хотя одна засада есть: в старом ГВИ8 (Г0604) есть режимы 100нс и 1000нс(=1мкс), а в ГВИ8М второй из них отсутствует...

frolov_d16_drv:
  • 02.04.2018: драйвер есть давно, а раздел создаём только сейчас.
  • 02.04.2018@утро-пультовая: при создании g0401_drv.c обнаружилось, что драйвер Д16 не уставляет никаких R; хотя "квант"-каналы работают в 100-наносекундных единицах.

    Драйвер просто никогда не проверялся на живом железе.

    А в старых, v2'шных -- тоже в скринах почему-то калибровок нету; точнее, в v2hw/chlclients/DB/db_nfrolov_d16.h обнаружены коэффициенты "4"...

    02.08.2018: чуток разбирательств:

    • Коэффициенты "4" в db_nfrolov_d16.h стоят у каналов V.
      • Потому, что эти каналы "выглядят" в наносекундах, а сырые их значения -- типа в 250-пикосекундных единицах, как каналы B, которые в формировании значения участвуют.
      • Но в коде -- как v2hw'шного nfrolov_d16_drv.c, так и нынешнего frolov_d16_drv.c -- стоит собственное масштабирование в 4 раза (и при чтении (/4), и при записи (*4)).

        Т.е., по факту каналы V и работают прямо в наносекундах.

      Так что, похоже, эти коэффициенты там лишние и только всё портят.

    • И более НИГДЕ калибровок нет. А использовалась они у меня только на пучковом датчике и на стенде для ЛИУ (в 14-м здании).

      Т.е., похоже, что функционал "нового" драйвера -- nfrolov_d16 -- толком испытан и не был: этот новый использовался только на стенде ЛИУ, а там всё работало в режиме махарайки.

    18.01.2019: вопрос давно решён: никаких калибровок драйверу уставлять не нужно, т.к.:

    • A и B прямо в кодах.
    • V в наносекундах.

      И каналы V с тех пор уже переведены на вещественные (double), а там калибровки зачастую не нужны вовсе.

    Так что и раздел "done".

  • 02.08.2018: возня с собственно драйвером -- подготовка к его использованию на ИК ВЭПП-5.

    02.08.2018: исчо прикол: Федя желает ловить сигнал окончания от одной конкретной D16 (крейт RFSYN, позиция 20).

    Ловить окончание вроде как положено LAM'ом. Но тут есть противоречие:

    • Обработчик LAM'а ОБЯЗАН оный LAM сбросить -- иначе "так и будет шарашить с максимально возможной скоростью" (сжирая всю производительность процессора).
    • Но в D16 сброс LAM'а одновременно является разрешением на исполнение следующего цикла!

    Т.е., в одном NAF'е (A0F10) совмещено ДВЕ разных функции. Проблемка...

    • С одной стороны, Фролов обещал подумать на тему сделать конкретно для этой Д16 лишний бит статуса (она всё равно внутри отличается от остальных).
    • С другой стороны, похоже, надо выпендриваться поллингом. А именно:
      1. LAM'ом как таковым не пользоваться -- не запрашивать (и вообще маскировать, битом "6" (реально 5) в регистре статуса).
      2. Постоянно выполнять поллинг при помощи "тест LAM по Q" -- A0F8.

        При этом, очевидно, надо иметь какой-то флажок, который по команде DO_SHOT (сброс LAM'а) сбрасывать, а проверку по поллингу выполнять только при сброшенном, и при Q==1 взводить обратно?

        И не вылезет ли тут race condition...

        После обеда: нет, не нужно ни "флажок", ни сбрасывать/взводить его! Надо помнить предыдущее состояние (сначала =-1), и делать Return тогда, когда текущее от предыдущего отличается, сохраняя вёрнутое в предыдущее. Плюс, по команде DO_SHOT надо тут же делать проверку с условным возвратом -- так, возможно, мы минуем race condition. ...и, возможно, ПЕРЕД тоже надо делать проверку с условным возвратом -- на случай, если DO_SHOT дёргается уже после окончания цикла, но ещё ДО очередного поллинга.

      Как утверждается, вроде бы Гусев именно так и делает.

    В любом случае, карта каналов уже чуть подшаманена:

    • Вместо "w30i" она теперь "w29i,r1i", а новый канал получил номер 29.
    • Называется он "finish_flag", а НЕ "lam_sig", как в IE4. Просто на всякий случай; хотя когда попробуем и разберёмся -- возможно, будет стоить унифицировать.
    • Заодно выяснилось, что в _drv_i.h присутствует еще пара каналов -- никак в драйвере не обрабатывающаяся (и в .devtype отсутствующая): AUTO_SHOT=26 и AUTO_SECS=27.

      Появились они еще в v2'шном nfrolov_d16_drv_i.h. Видимо (судя записи в bigfile-0001.html от 24-02-2011), где-то в 2011-м.

    02.08.2018: неа, по словам ЕманоФеди, вариант с поллингом не прокатит: опрашивать имеет смысл с периодом 100мс, а это слишком большое время -- характерное время накопления электронов как раз в районе 100-200мс, и лишняя сотня задержки будет очень большой потерей.

    03.08.2018: поступило предложение-идея от Фролова, как обойтись прямо сейчас, не переделывая ничего в железке.

    • По приходу LAM'а сначала блокировать запуски (взведением бита 1<<4 в регистре статуса), а уже потом делать A0F10.
    • По команде же DO_SHOT вместо сброса LAM'а (который УЖЕ сброшен ранее) сбрасывать тот битик.

    Естетственно, такой режим работы должен включаться отделной опцией в auxinfo.

    Делаем.

    • Канал переименовываем всё-таки в lam_sig.
    • Введён флаг use_lam, и для его установки auxinfo-флаг use_lam (антоним -- ign_lam).
    • LAM_CB():
      1. блокируем запуски;
      2. сбрасываем LAM;
      3. сообщаем возвратом значения канала LAM_SIG.
    • _init_d():
      1. ВСЕГДА указывает, что канал LAM_SIG -- IS_AUTOUPDATED_TRUSTED.

        07.08.2018: нифига НЕ TRUSTED, а надо IS_AUTOUPDATED_YES -- причины см. в разделе о frolov_ie4_drv за сегодня же (вкратце: иначе клиенты получат NEWVAL вместо CURVAL).

      2. При режиме use_lam:
        1. Запрашивает ловлю LAM.
        2. Выполняет сброс.

          04.08.2018: хотя, если так подумать -- а зачем? Ведь если LAM уже горит, то сразу после регистрации он и сработает.

      3. Иначе сразу возвращает канал LAM_SIG с флагом UNSUPPORTED.
    • В _do_rw() отработка DO_SHOT в режиме use_lam сводится к сбросу бита блокировки запусков.
    • Установка/сброс бита блокировки запусков (стартов) делается не вручную, а вызовом свежевведённой SetALLOFF(), которая просто вызывает _do_rw() с DRVA_WRITE, передавая ей нужное значение.

      Таким образом, не просто меняется значение битика, но оно еще и сразу отдаётся наверх в канале ALLOFF.

    06.08.2018: проверил -- такой способ обращения с LAM работает.

    А вот конкретно та специальная Д16 в позиции 20 -- НЕ работает. Точнее, в "непрерывном режиме" она молотит как надо (так и проверили рабочесть), а в однократном -- нет. Просто полный игнор.

    Стали с Фроловым разбираться -- разобрались.

    • Эта конкретная Д16 -- действительно отличается от прочих, у неё есть дополнительные биты статусного регистра:
      • 1<<6 -- стоит в ожидании прихода START (==0) либо уже был старт (==1).

        Имеет смысл только в однократном режиме.

      • 1<<7 -- ==1 у Д16П, так её можно отличить программно.

      ...и дополнительная пара NAF'ов:

      • F25A1 -- старт однократного цикла.
      • F25A2 -- прерывание однократного цикла.

      И называется она "Д16П" (или "D16P").

    • Таким образом:
      1. НЕ нужно махинировать с маскированием стартов: она и так не запустится повторно без F25A1.
      2. По DO_SHOT надо как раз именно F25A1 и давать, вместо сброса LAM или размаскирования стартов.
      3. Битик 1<<6 желательно бы отдавать наверх каналом. Причём максимально оперативно -- как lam_sig.
        • из LAM_CB();
        • но ещё по DO_SHOT (перед ним?), чтоб сначала нолик приходил.

        Но тут может быть засада.

        • Отдавать-то надо именно значение бита из прочитанного регистра статуса.
        • А не вылезет ли где race condition?
    • Федя подтверждает, что и команда прерывания нужна, и бит 1<<6 (он его использует в своём коде).

    Теперь надо сообразить, как лучше это всё встроить в драйвер.

    • То ли добавить auxinfo-параметр "pp".
    • То ли анализировать старший бит регистра статуса -- если он ==1, то автоматом работать иначе.
    • И вообще число вариантов не 4 (2 флага по 2 положения), а всего 3:
      1. Обычная работа с обычной железкой: LAM'ы не ловить.
      2. Обычная железка, но LAM'ы ловить (это нынешнее "use_lam").
      3. Д16П -- эта, в 20-й позиции: LAM'ы ловить, но маскировать запуски не нужно (в однократном режиме, по крайней мере).

      Может, сделать "режим работы", 3-вариантный переключатель?

    06.08.2018@вечер-дорога-домой, около-ИПА: да, надо делать 3-вариантный переключатель и во всех режимо-зависимых точках if()'ы.

    Режимы назвать "NONE", "MSKS" (MaSK Starts) и "D16P".

    07.08.2018: делаем.

    • Каналы:
      • Карта переделана с w29i,r1i на w28i,r2i.
      • Канал, маппирующийся на 1<<6, назван WAS_START.

        Кстати, этот битик -- аналог такого же IE4'шного, где он маппируется на канал BUM_GOING; только там он отдаётся в инвертированном виде.

      • Убран фиг знает когда и зачем затесавшийся канал WAITING=24 (он и был в w-зоне, и возвращал всегда CXRF_UNDUPPORTED; видимо, бывший (когда-то давно, на заре nfrolov_d16) UNUSED1).
      • А в ранее неиспользовавшуюся позицию 25 (ex-UNUSED2) поставлен новый канал, названный "ONES_STOP".
    • Поле "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():
      • Добавлена обработка канала WAS_START.
      • Реакция на DO_SHOT:=1 переделана в 3-вариантную.
      • Добавлена поддержка ONES_STOP -- тут всё просто, но с проверкой на значение mode.
    • _init_d():
      • Добавлено размаскирование LAM'а (сброс бита 1<<5).
      • "Регистрация" канала WAS_START -- перевод в AUTOUPDATED_YES плюс сразу возврат CXRF_UNSUPPORTED в случае, если режим не-D16P.
      • Дополнительная проверка, что при запрошенном режиме d16p в статусном регистре действительно горит старший бит (1<<7).

    Проверил -- вроде работает (только ONES_STOP пока проверить не удалось).

    18.01.2019: драйвер давно отшлифован (в том числе с каналами V (переделанными на double), и с "правильной" конверсией {A,B}<->V), и начинён новыми фичами, и давно уже эксплуатируется на ВЭПП-5.

    Так что первоначальная задача выполнена.

  • 10.08.2018: надо что-то делать с "F_IN_NS": в rfsyn'овских Д16 штатно используется Fclk=Fin, так что для функционирования каналов Vn правильное значение F_IN_NS просто необходимо.

    ...и меняется частота 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-контроллерных драйверов мы вещественную арифметику уже использовали:

    1. В v2'шном vsdc2_drv.c (интеграл -- там float32 домножался на 1e9 и отдавался наверх преобразованный к int32
    2. В dds300_drv.c -- там частоты пересчитываются через вещественное, и в v4 тоже (хотя это всё "внутри" -- каналы только целочисленные).

    Детали идеи реализации:

    • Вещественности там будут в 2 точках:
      1. Параметр -- PSP_P_REAL(), сводящийся к strtod() и вещественным сравнениям.
      2. Собственно пересчёт в frolov_d16_rw_p() -- там арифметика (умножение, деление, остаток от деления) и сравнение с 0.0.
    • Канал, коль он станет f64, надо вытаскивать из общей группы (сейчас =23) в отдельное место -- видимо, между основной группой и каналами чтения. Т.е., =27 (сдвигать неиспользуемые каналы AUTO, плюс -- нечётный, потребует выравнивания в CxsdHwSetDb() -- бе-е-е...).
    • @вечер: а не сделать ли тогда и каналы V тоже вещественными? Чтоб уж четвертинки наносекунд не терять.

    14.08.2018: приступаем к массивной переделке драйвера.

    • Имена FROLOV_D16_CHAN_nnn приведены к современному стандарту: "_BASE" заменено на "_n_base".
    • Раздвигаем и расширяем карту: теперь она стала w40i,w10d,r50i.
      • Т.е., она теперь похожа на IE4'шную.
      • Но с отдельной областью из 10 double'ов для записи.
      • Каналы V перемещены в эту область, base=40; как и F_IN_NS=49.
    • Поле privrec'а F_IN_NS превращено в float64, плюс ему навешен auxinfo-psp-параметр "f_in", с умолчанием 0.0 и разрешенным диапазоном [0.0,2e9] (т.е., до 2e9ns=2s).
    • _rw_p():
      • В верхушке цикла, где проверяется корректность переданного на запись, добавлена отдельная ветка на double-каналы.
      • Изменена обработка об-double'нного канала F_IN_NS:
        • Добавлена инфраструктура для самостоятельного возврата значения (vp, dt_f64, n_1).
        • В конце ветви делается ReturnDataSet() и вместо break исполняется goto NEXT_CHANNEL -- чтобы избежать обычного ReturnInt32Datum()'а в конце цикла.
      • Также переделана работа с каналами V:
        • Расчёт переведён на double: тип сменили v_fclk и quant, вычисление остатка делается через fmod() вместо %.

          ...а наверное, можно б было вычислять c_Ai и v_R одной операцией -- вызовом remquo. Но там в документации какие-то оговорки про то, что в quo попадают "a few bits of the quotient". И нагугленные обсуждения вообще говорят про "3 бита", но без конкретики, и что это так в стандарте. Так что лучше воздержимся.

        • Ну и инфраструктура возврата аналогично F_IN_NS'овой.

    Проверено -- на вид, как ни странно, вроде работает!

    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", чтоб можно было указывать именно частоту?

    Реализация тривиальна:

    1. if(dval<0)dval=0;,
    2. а потом
      if (dval == 0.0) F_IN_NS = 0.0; else F_IN_NS = 1 / dval;
    3. ...и Return'нуть связанные каналы (F_N_NS и V).

    @на-работе: сделано. Только пересчёт не через 1/..., а через 1e9/... -- у нас же НАНОсекунды.

    17.08.2018: а теперь о пересчёте каналов V.

    Часть багов нашлась быстро -- следствия перехода на double:

    1. Пересчёт {A,B}->V: там стояло
      c_Bi / 4
      вместо
      c_Bi / 4.
      -- в результате от 1 до 3 квантов по 0.25нс могло отбрасываться -- этим и объясняется, что "отрезается по 4 единицы" (стало по 1 единице; об этом ниже).
    2. Переменная v_R оставалась int, а ведь ей присваивается результат fmod(), затем
      c_Bi = v_R * 4
      -- т.е., тоже могло теряться от 1 до 3 квантов по 0.25нс.

    Но после этих исправлений "спад" лишь уменьшился до 1 кванта, но не исчез.

    • {3,0}=>137.44; запись 137.44 -- {2,183}=>137.179; запись 137.179 -- {2,182}=>136.929; 136.929 - {2,181}=>136.679, и так далее.
    • Напихивание отладочной печати получаемых в процессе пересчёта значений натолкнуло на мысль, что проблема -- в "погрешности округления при делении": когда остаток от деления на размер кванта A затем умножается на 4 и отбрасывается дробная часть -- c_Bi = v_R * 4.

      И очевидное, казалось бы, решение -- перед этой операцией добавлять к v_R пол-кванта -- 0.125. Что после умножения будет давать 0.5 и тем самым должно б быть эквивалентно обычному "округлению до ближайшего целого".

    • Сделал -- а вот фиг! После этого "спад" заменился "ростом"!
    • Что не так, спрашивается?

      Разные мысли в голову лезут.

      • Как-нибудь аналогично махинировать при вычислении c_Ai:
        1. Добавлять сколько-то перед делением? Неа, в любом случае неясно, сколько (т.к quant может быть разным).
        2. Добавлять 0.5 перед преобразованием в целое? Точно нет -- вся идея {A,B} как раз в том, что на долю B остаётся неуложившееся в A.
    • Проверил указанием значений, кратных 0.25ns -- всё работает как надо, ровнёхонько: 0.25=>{0,1}, 0.5=>{0,2}, ... 45.7=>{0,182}=>45.5.
    • И только после ненулевого результата деления на F_IN_NS -- на 45.7146831 -- вылезают проблемы со "спадом".

    19.08.2018@утро, воскресенье, пешком в 13-е вдоль 4-го: раз у нас теперь есть умение указывать каналом прямо частоту, а auxinfo-параметр называется "f_in" -- нехорошо получается: указываем ведь реально наносекунды, а не частоту!

    А можно сделать так:

    • Чтоб указание f_in=NNN указывало бы именно ЧАСТОТУ, и оно б пересчитывалось в наносекунды.
    • А указание f_in=-NNN считало бы указанное минус-наносекундами.
    • ...естественно, ограничение диапазона из PSP-параметра придётся убрать, и накладывать ограничение уже в _init_d(), после всех пересчётов.

    Сделано. Проверено -- работает.

    19.08.2018: теперь возвращаемся к странностям при записи в V.

    Еще попялившись на результаты записи разных чисел, а главное -- получающегося при этом значения v_R (остатка от деления), понял причину:

    • Да, это результат погрешности округления.
    • Да, прибавление крохотной добавки к изначальному (записываемому) значению V дало бы результат.
    • Но главное в том, ГДЕ возникает "погрешность округления": не при записи, а при ЧТЕНИИ!
    • Точнее, проблема кроется в формате (dpyfmt), который и обрезает дальние-дальние знаки, которые как раз являлись бы той самой "крохотной прибавкой"; точнее, их удаление даёт "убавку", нехватка которой и приводит к спаду.
    • Задействовав при разбирательстве формат %20.10f, смог в деталях понять происходящее:

      В реальности {3,0} -- это вовсе НЕ просто 137.144, а 137.1440493000.

      И именно этого 0.0000493 и не хватало, чтобы деление на 45.7146831 дало целую часть 3, а не 2:

      • 137.1440000/45.7146831 = 2.9999989215718745
      • 137.1440493/45.7146831 = 3.

    Вывод:

    1. Всё у нас в frolov_d16_rw_p() корректно делается, не нужно более никаких махинаций с добавлением 0.125 и т.п.
    2. Вещественные числа -- очень тонкая штука, надо с ними аккуратнее.

    22.08.2018: (@обед, 5-й этаж): мысль -- а ведь можно было выдрепнуться проще: оставить каналы int32, и при смене F_IN_NS просто отдавать наверх R каналов V так, чтобы нужный пересчёт выполнялся на клиенте.

    (@пятнадцатью минутами позже, у себя): неа, нифига:

    1. Такой финт подойдёт только к простым каналам с линейным пересчётом. В Д16 же, с дуплетом {A,B} -- нет.

      ...а вот для ИЕ4, без B -- возможно, что и подошло бы.

    2. Не вполне ясно, как такая "смена R на ходу" отобразится в клиентах: произойдёт ли пересчёт УЖЕ имеющихся у них данных при просто смене R?

      Да, можно сразу после смены форснуть возврат значений; но всё равно некрасиво/сомнительно.

    Но сама идея забавная, хоть Д16 и не прокатит.

  • 10.10.2018: ЕманоФедя требует, чтоб при старте сервера в Д16 проставлялись бы некоторые заранее предопределённые режимы: fclk_sel=Fin, start_sel=START, mode=Cont (кроме xfr_d16_20.mode=1).

    Это можно сделать аналогично пред-программируемым настройкам adc200me/adc812me etc.: в auxinfo указывать желаемые значения, чтоб при специфицированности этих значений в _init_d() вызывалась бы _rw_p() для соответствующих каналов с этими значениями.

    22.10.2018: делаем.

    • Заведены поля в privrec'е, имена у них -- init_val_of_ИМЯ_КАНАЛА (ИМЯ_КАНАЛА -- fclk_sel, start_sel, mode).
    • И соответствующие LOOKUP-строчки в _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.

  • 01.11.2018: отдельно насчёт пересчётов V=>{A,B} и косяков с округлением: с IE4 сегодня вроде удалось найти решение -- при пересчёте V=>A не отбрасывать дробную часть при делении dval/quant, а делать округление.

    Возможно, с 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: поразбирался еще -- стало ясно: проблемы реально НЕТ.

    • Точнее, она в другом: проблема в том, что у нас на экране отображаются микросекунды, с 3 знаками после запятой.
    • Соответственно, значащая информация при выводе на экран просто уничтожается -- это V=4.571 является сильно укороченным вариантом исходного значения.
    • Вот при "нажатии на Enter" и записывается другое число, что закономерно приводит к иным A и B.
    • Но значение на экране при этом остаётся тем же -- A=99,B=180 также выдаёт V=4.571.

    05.11.2018: после изложения вышеприведённых соображений ЕманоФеде пришли к "решению": надо добавить экранным полям 2 знака после запятой.

    • Смысл -- что 1 квант B соответствует 250пс=0.25ns; которые как раз и попадут в те 2 дополнительных знака.
    • Решение, конечно, не совсем идеально, т.к. в зависимости от Fin квант A будет сильно разным и даже при отсутствии B (B=0) реально значащие цифры могут попадать в более дальние позиции и тоже будут отрезаться...

    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.
    • Но после кратких размышлений @Ключи, ~18:10 (Федя позвонил в 18:05, когда я был там и наблюдал, как Наташка морковку убирает) возникли сомнения: ведь каналы dly -- вещественные, так что их то внедрение касаться не должно. Анализ ветвей исполнения (уже @вечер-дома) это подтвердил.
    • Так что, очевидно, косяк был и раньше, но просто не замечался -- ну не зажимал никто Enter, и с таким конкретным значением.

    Пара замечаний:

    1. Чисто теоретически, избавиться от эффекта "зажали Enter и значение полезло вниз" можно при помощи мелкого хака: при записи канала dly сначала вычитывать текущее значение и сравнивать, и если оно отличается от записываемого менее чем на "квант" (равный кванту B плюс чуть-чуть), то считать, что это то же самое значение.

      Но общей проблемы корректности это не решит, да и вообще метод мутноватый.

    2. @утро, поездка на работу, в завороте Пирогова перед ИФП: посмотреть бы всё-таки, как у Гусева сделано -- все утверждают, что "а вот у Гусева всё работало!".

      (Да, но раздельной записи/чтения A и B он не позволял.)

    Разбирательство подетальнее, уже сегодня:

    • То значение "18001. с каким-то хвостом" -- это 18001.87.
      • Там действительно есть длинный хвост: формат %.30f даёт 18001.8704963917.
      • Формат %a -- 0x1.19477b6367f68p+14.
      • A=393 B=144.
    • Изучение frolov_ie4_rw_p() показало такой код:
      c_Ai = round(dval / quant);

      Отсюда радостная мысль -- «о, я тогда забыл воспроизвести это в драйвере Д16! вот сделать (не забыв "правильно" считать значение для B -- как V-A*quant), и настанет счастье!».

      Но фиг --

    • В драйвере же Д16 аналог за-#if0'ен
      #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;
      
      -- т.е., там даже корректно высчитывалось B ("руками" брался "остаток от деления", путём вычитания ЧАСТНОГО*КВАНТ из начального значения).

      Но, ЕМНИП, тот вариант не сказать, чтобы корректно работал, да?

    • Итого: фиг знает, что же делать. Ну изучить ещё конкретные коды и конкретное поведение, а дальше? Разве что действительно код Гусева поанализировать -- вдруг там найдётся гениальная идея...

    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,14418001.8718001.87049639170x1.19477b6367f68p+14  393,143393,144
    393,14318001.6218001.62049639170x1.19467b6367f68p+14  393,142393,143
    393,14218001.3718001.37049639170x1.19457b6367f68p+14  393,141393,142
    393,14118001.1218001.12049639170x1.19447b6367f68p+14  393,140393,141
    393,14018000.8718000.87049639170x1.19437b6367f68p+14  393,139393,140
    393,13918000.6218000.62049639170x1.19427b6367f68p+14  393,138393,139
    393,13818000.3718000.37049639170x1.19417b6367f68p+14  393,137393,138
    393,13718000.1218000.12049639170x1.19407b6367f68p+14  393,136393,137
    393,13617999.8717999.87049639170x1.193f7b6367f68p+14  393,135393,136
    ...
    393,3 17966.6217966.62049639170x1.18ba7b6367f68p+14  393,2 393,3
    393,2 17966.3717966.37049639170x1.18b97b6367f68p+14  393,1 393,2
    393,1 17966.1217966.12049639170x1.18b87b6367f68p+14  393,0 393,1
    393,0 17965.8717965.87049639170x1.18b77b6367f68p+14  392,182392,183
    392,18217965.6617965.65581319480x1.18b69f8d7e7f6p+14  392,182392,182

    ВСЁ!!! Дальше не снижается.

    Т.е., ". . ." -- и так далее, по -0.25. Но:

    • В точке (393,0) остановки не было -- перешло на (392,182).
    • А вот в (392,182) спуск застопорился -- 17965.66 далее не уменьшается.
    • И даже если "вручную" сбросить B, например, до 5 -- то значение 17921.41 стабильно, вниз не ползёт.

    Откуда возникла мысль: а может, и у Гусева тоже было совсем не идеально, просто конкретно на проблемные числа (18001.*) никогда не наступали?

    Увы, попытка добиться от ЕманоФеди ответа "а какие числа бывали раньше?" особого успеха не дала -- он что-то заяснял про разницу в 3-м знаке (из 5 -- это 18Xnn, что ли?), но тут как раз произошла просадка и он убежал исправлять.

    03.10.2019: начал анализировать значения, просто вручную имитируя расчёты, выполняемые драйвером.

    Конкретно для первого же -- 18001.87:

    • Q=45.7146831969
    • 18001.87/Q=393.78748229454516
    • после round() получаем 394 (WTF?!)
    • а 394*Q=18011.5851795786. 18011 -- при том, что исходное число было 18001 -- т.е., 01 выросло до 11!!!

    Ёлки-палки!!! В голову приходит только пугающе знакомое «а как оно с этими "бредовычислениями" вообще могло работать?!».

    04.10.2019: ЧОРД!!! А зачем ROUND()-то делать?! Ведь он в той ветке, которая за-#if0'ена -- и с ней да, значения начинали ползти вверх (теперь стало понятно, почему). Как бы то ни было, ночная идея про "домножать на 4" всё равно валидна.

    03.10.2019@~19:00, выходя из ИЯФа домой, крыльцо и парковка "между клумб": проблема в том, что округлять-то НАДО, но надо округлять не к целым, а "гранулярно", к ДРОБНЫМ числам. А такого функционала стандартная библиотека (libm) вроде не предоставляет.

    04.10.2019@ночью, в районе 03 часов утра: (снилась всякая хрень, в т.ч. про Д16, а проснулся (или перед просыпанием?) -- и озарило:

    • Ну так надо домножать на 4, и уже потом округлять!

      А потом -- делить обратно.

    • "Округлять" хоть вниз -- просто приведением к int, хоть "к ближайшему (кратному 0.25)" -- round().

    04.10.2019: вот написал всё то про "домножать на 4", и даже результаты позавчерашнего тестирования из буллет-листа в табличку переделал (чтобы добавить ещё колонок), а потом -- затык: ЧТО домножать на 4?

    • Если само V -- то как-то без толку: на примере 18001.87 видим, что 18001.87*4/Q=1575.1499291781806, после отбрасывания дробной части и последующего деления получатся те же самые 393. Которые, кстати, и ДОЛЖНЫ получиться.
    • ...завис на пару часов...
    • @~16:50, 3-й этаж главного здания, окно "предбанника" конференц-зала(ждал, пока экспериментальный семинар закончится, чтобы свою презентацию проверить на том проекторе): так махинации с "*4" требуются для компонента B! Это ж на него постоянно не хватает дальних цифр из дробной части, так что он "едет вниз".
    • ...вот же ж блин, ночью было "гениальное озарение", которое сейчас (в обычном состоянии сознания :D) не вполне понимаю, как использовать. А тогда всё показалось таким кристалльно ясным! ("Банан велик, а шкурка ещё больше"?)
    • А сейчас не очень ясно, куда же именно надо воткнуть "домножение на 4" и возможный round().
    • Вероятно, в вычисление компонента B?

      Но там УЖЕ стоит "c_Bi = v_R * 4".

      А в вычислении "v_R = fmod(dval, quant)" -- далее закомментированное "/*+ 0.125*/.

    • Другое дело -- что, возможно, вместо fmod() надо всё-таки использовать бы "v_R = dval - c_Ai * quant" -- почему этого не делается?

      Однако -- а выше 06-02-2019 такой вопрос уже возникал.

    • И надо ОБЯЗАТЕЛЬНО проверить, как же происходит переход (393,0)->(392,182).
    • Проверил:
      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
      

      Т.е., у нас тут ДВЕ разных проблемы:

      1. Первая -- скольжение B вниз.

        И тут действительно замена fmod()'а на вычитание с последующим round() результата -- может дать нужный эффект.

      2. Вторая -- при снижении B до нуля происходит декремент A на 1.

        Причина -- в тех самых недостающих дальних знаках. Как бороться -- неясно...

      3. Не проблема, а вывод: то, что (392,182) не едет дальше вниз -- просто "повезло": там вычисление B даёт небольшой запас в плюс.

    07.10.2019@~09:55, по пути на понедельничную планёрку, в переходе: надо бы посмотреть, какая "дельта" в проблемных случаях из-за отбрасывания дальних знаков; если она окажется существенно меньше "кванта B" -- 250ns, то, вероятно, должен бы существовать способ побороть проблему -- какое-нибудь "хитрое округление".

    (И всё-таки, а может и не такая уж бредовая была ночная идея? Не поможет ли она справитьс с этим результатом "отбрасывания малого"?).

    07.10.2019: делаем программульку work/tests/test_d16_calcs.c, имитирующую вычисления в драйвере, и на ней обкатываем разные варианты и идеи, плюс смотрим промежуточные данные вычислений.

    • Результаты добавляем в ту табличку выше.
    • Для начала -- пробуем считать c_Bi не как
      v_R  = fmod(dval, quant);
      c_Bi = n1_v_R * 4;
      
      а как "остаток от вычитания реально полученного c_Ai" --
      v_R  = dval - c_Ai * quant;
      c_Bi = round(v_R * 4);
      
      (т.е., округляется уже после "*4" -- что и есть по факту "округление до БЛИЖАЙШЕГО кратного 0.250).
    • И стало сильно лучше!!! Первичная проблема -- "езда вниз" -- сразу исчезла.
    • ...но осталась проблема в "переходной точке": (393,0) по-прежнему сваливается "вниз".

      Правда, теперь уже не в (392,182)=17965.66, а в (392,183)=17965.91 =17965.9058131948 =0x1.18b79f8d7e7f6p+14.

    • Кстати, при подсовывании данных со всеми значащими цифрами (которые от вывода %.30f) результаты ОБОИХ вариантов вычислений совпадают.

    08.10.2019: продолжаем разбираться:

    • Добавил печать значения v_R. Однако -- оно в первом и втором случае почти ИДЕНТИЧНО, а разница исключительно из-за round() в c_Bi=..., и при первом варианте было бы то же самое.

      ???...откуда же различия-то берутся???

    • @пляж, после обеда, в районе 16:00: да понятно, откуда: все проблемы -- именно из-за отбрасывания малой дробной части.

      Т.е.,

      • "Титульная" проблема -- решена, и она была скорее из-за глупости и неопытности: для МЛАДШЕГО компонента (в данном случае -- B, а в IE4 это прямо сам A) использовать round() и можно, и нужно.
      • А вот оставшаяся проблема -- именно из-за "неверного" числа.

      Как решать оставшуюся проблему? И имеет ли она какое-то решение в принципе?

      • Возможно, что и не имеет: например, при формате "%.0f", когда отбрасывается уже ОЧЕНЬ много значащей информации.
      • Но может и имеет -- вдруг придумаем ещё какое-нибудь хитрое округление.

      Как смотреть:

      • Надо бы построить табличку с разными значениями A, от 0 до 65535 (вниз?): брать V=A*Q, и смотреть, какие числа получаются в результате отрезания дальних цифр.
      • Смотреть на варианты с форматами .0, .1, .2, .3 -- сколько там отбрасывается.
      • Может, получатся столь небольшие числа, что можно их будет как-нибудь добавлять небольшую дельту "перед делением", или ещё как-нибудь хитро смотреть, что "расстояние" от результата деления до результата деления V+дельта целый квант.
      • Для "смотреть" необязательно делать sprintf("%.N"...) с последующим strtod(), а можно делать "умножение, обрезание, деление": для %.1 -- trunc(V*10)/10, для %.2 -- trunc(V*100)/100, %.3 -- trunc(V*1000)/1000.

    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: ну пока внедряем хотя бы первую часть решения:

    1. Вариант
      v_R = fmod(dval, quant);
      заменен на
      v_R = dval - c_Ai * quant;

      ...хотя это скорее "для вящей корректности".

    2. В общий (для всех вариантов вычисления B) кусок внедрён 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@дома-вечер: колёс вылезти не должно -- потому, что:

    • Дельту надо брать равной 0.1 -- поскольку это верхнее ограничение на ошибку округления при отбрасывании дальних знаков, 1.09->1.0.
      • При отбрасывании более дальних -- например, 1.009->1.00 -- разница меньше.
      • А случай отбрасывания ВСЕХ знаков после запятой -- 1.9->1 -- рассматривать смысла нет, поскольку там отбраывается более, чем квант B (0.25).
    • Исходно число ведь берётся не с потолка, а в результате пересчёта A,B}->V.

      Соответственно, в РЕАЛЬНЫХ случаях ... ВОТ ЧТО ТУТ НАДО НАПИСАТЬ, А? Вроде всё ясно.

    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) ниже общее для всех).
    • Также добавлено такое вычисление в test_d16_calcs.c. На вид -- вроде даёт нужный результат (совпадает с тем, что получается из чисел с не-обрезанными дальними знаками).

    Надо б будет потестировать на реальном железе.

  • 05.11.2018: похоже, нужно уметь как минимум у Д16 в 20-й позиции (управление перепусками) при пересчёте V<->{A,B} часть B игнорировать, а пересчитывать только в A.

    Смысл в том, что там эти части 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: делаем.

    • В privrec добавлено ab_mode.
    • Варианты значений -- AB_MODE_AB=0 и AB_MODE_A_ONLY=1.
    • auxinfo-параметры -- ab и a_only.
    • В _rw_p() теперь по 2 ветки на чтение и запись _CHAN_V_n_base. И да, в варианте A_ONLY используется round().

    18.01.2019: по крайней мере на 20-й Д16П фича "a_only" работает уже больше месяца и вроде без нареканий, так что "done".

  • 18.12.2018: Федя возжелал иметь возможность быстро переключаться между несколькими наборами настроек, которых сейчас видно 4 -- {e-,e+}{ВЭПП-4,ВЭПП-2000}. Список затрагиваемых каналов -- A,B,OFF, каждого по 4 штуки (по числу линий).

    ПО-ХОРОШЕМУ, эта задача должна бы выполняться на "верхнем уровне" -- в клиенте-менеджере_режимов, или хотя бы драйвером в сервере (таковой тоже предполагается, для жонглирования каналами других устройств).

    Но конкретно Д16:

    1. Шибко специализированный девайс, использующийся ТОЛЬКО на ВЭПП-5, поэтому можно отступить от принципа "правильного" разделения обязанностей.
    2. Живёт в CAMAC-крейте, управляемом дохленьким контроллером CM5307/PPC, который если будет принимать из сети 12 штук отдельных команд записи, то это очень долго. А дрыгнуть несколькими NAF'ами -- это быстро.

      20.12.2018: кстати, поскольку на каждый из них будет делаться Return(), то, чтобы сэкономить производительность, можно перед группу "записей в свои каналы" окружить скобками fdio_lock_send()/fdio_unlock_send().

      20.12.2018: (вечером) ага, щас! ЧЕМУ делать lock/unlock? Ведь дескриптор доступен на уровне remcxsd_driver, а эти мозги -- в самом драйвере.

    18.12.2018@дома: сделан минимум: расширена карта каналов.

    • В конце добавлено "w100i". Это с запасом.
    • Пресеты идут блоками по 12: a<0-3>,b<0-3>,off<0-3>, и имеют префикс "pN_". Например, p0_b1 -- это канал b1 0-го пресета.

      21.12.2018: также добавлены DEVTYPE-DOT-HACK-варианты с именами через точку, типа p0.b1 -- чтоб можно было адресоваться "к группам, нацеливая на них макросы" (или хоть обычный скрин frolov_d16.subsys).

    • Пресетов пока 4.
    • Канал для активации назван activate_preset_n и смаппирован на 39 (с момента расширения карты с 20 до 100 каналов 14-08-2018 места стало предостаточно).

    20.12.2018: теперь собственно реализация.

    • В privrec добавлен массив для хранения данных пресетов, presets_buf[].
    • В _rw_p() сделана поддержка записи туда; причём значения проходят ту же проверку в зависимости от типа канала (A, B, OFF), что и при записи в соответствующие обычные каналы.
    • Активация пресета -- простой цикл по 4 линиям, в каждой итерации которого делается 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).

    Делаем:

    • Поскольку V имеют тип double, то их никак нельзя поместить в карту каналов рядом с остальными, которые int32 (хотя там место есть -- по 20 каналов на пресет, из которых заняты 13 (по 4шт A,B,off плюс 1шт alloff)).
    • Поэтому блок 180-199 переведён на double и 16 (4 пресета по 4 канала) штук назначены туда.
    • В frolov_d16_drv_i.h и frolov_d16.devtype всё сделано.
    • Заготовка "case" в коде frolov_d16_rw_p() тоже -- вычисление p_n и p_c.
    • 01.02.2025: вроде и содержимое сделано -- код скопирован с обработки каналов V, но чтения/записи перенаправлены в соответствующие ячейки presets_buf[].
    • 03.02.2025: только было забыто при обновлении пресетных A и B возвращать текущие "значения" пресетных V. Добавлено.

    03.02.2025: в скрин frolov_d16.subsys добавлена вся толпа каналов пресетов -- как минимум, чтоб удобно было проверять.

    08.02.2025: решено также и сюда сделать фичу "вычитать текущие значения из устройства в указанный пресет".

    Несмотря на то, что 29-12-2018 ЕманоФедя утверждал, что это не требуется, сейчас он точку зрения поменял и считает такую возможность нелишьней.

    Сделано -- всё довольно несложно: простым вычитыванием из железа, складированием и вызовами ReturnOne() для всех затронутых каналов.

    ...и да, как и с обычными каналами, канал "alloff" считается как бы "битом 5-й строки" и возвращается в общем цикле с прочими битами запрета.

    10.02.2025: проверяем всё сделанное недавно, на живом железе на ВЭПП-5.

    • При записи пресетных V соответствующие им пресетные A и B обновляются корректно.
    • Вычитывание текущих значений из устройства в указанный пресет тоже работает.
    • ...хотя значения пресетных V при этом не возвращались.

      Причина оказалась в дурацкой ошибке: при вычислении номера канала "пресетный V", передаваемого ReturnOne()'у, номер пресета умножался на FROLOV_D16_CHAN_PRESETS_per_one (как у остальных пресетных каналов) вместо FROLOV_D16_NUMOUTPUTS (специфичного для V, т.к. они идут отдельной группой).

      Поправлено.

frolov_ie4_drv:
  • 02.04.2018: драйвер такой, скорее всего, понадобится -- устройства на впуске/выпуске и в rfsyn есть, а драйвера, оказывается, нет.

    Хотя мне почему-то помнилось, что вроде кабы даже не делал его, где-то то ли для ЭЛС/Семёнова, то ли для ndbp... Или там использовался драйвер от Д16? Да не должен был бы -- вроде устройства несколько различны по функционированию.

    27.07.2018: да, устройство ЧУТЬ-ЧУТЬ похоже на Д16 (по NAF'ам и "адресам" регистров), но отличается настолько, что использовать драйвер одного с другим точно не удастся.

    30.07.2018: кстати, НИКАКИХ следов от когда-либо ранее якобы существовавшего драйвера IE4 обнаружить не удалось, в т.ч. в ARCHIVE/.

  • 27.07.2018: приступаем к изготовлению.

    28.07.2018@дома: за основу берём всё от Д16 -- и _drv.c, и карту каналов.

    Но, поскольку тут предвидится некоторое количество каналов статуса (текущий цикл, исполняется ли сейчас цикл, ...), то вместо "w30i" делаем "w50i,r50i".

    30.07.2018: сделано всё, кроме "импульсности" -- работы с LAM и старта/стопа цикла BUM.

    31.07.2018: делаем недопиленное вчера.

    • Ловля LAM. В LAM_CB() просто вызывается возврат значения канала BUM_GOING, и более ничего -- так я понял рекомендацию Фролова.
    • Каналы BUM_START и BUM_STOP: запись выполняет A1F25 и A2F25 соответственно, плюс далее тут же делается возвращает значения канала BUM_GOING.
    • Канал BUM_GOING: отдаёт инвертированное значение бита 7 (6, если с 0) регистра статуса. В нём вроде как 1, если цикл "завершён" (не идёт?) и 0, если идёт; соответственно, канал загорается "1" во время цикла.
    • Заведены каналы V0...V3. Предполагается, что в них будут отдаваться уже подсчитанные значения в каких-то-секундах. Поскольку подсчёт нетривиален, а главное -- каналы цепочечно-зависимы, то они сделаны readonly.

      Сам подсчёт пока не сделан, возвращаются всегда -1.

    Теперь надо делать скрин и проверять работу на живом железе.

    01.08.2018: начинаем тестирование.

    • Сделан скрин frolov_ie4.subsys: база от d16, но нумерация сразу 1-4, а не 0-3; добавлена панелька BUM, в строке для канала 1 отсутствует поле "/".
    • На вид -- вроде что-то как-то работает. Цикл BUM тИкает.
    • Из результатов наблюдений:
      1. По получении LAM'а выполнять его сброс (A0F10) всё-таки НЕОБХОДИМО, иначе так и будет шарашить с максимально возможной скоростью.
      2. Если во время цикла переключить режим с MODE_BUM на MODE_CONTINUOUS, то цикл просто останавливается, а если потом переключить обратно, то продолжается с того же места (примерно; т.к. там хитрая цепочка счётчиков, то хбз).
      3. Также независимо от режима отрабатываются BUM_START и BUM_STOP: даже в MODE_CONTINUOUS по первому лампочка BUM_GOING зажигается, а по второму гаснет.
    • Добавлен канал LAM_SIG, который AUTOUPDATED_TRUSTED и отдаётся только из LAM_CB() -- чтобы именно отдельно от BUM_GOING сигнализировать о LAM'е. Отдаётся всегда 0.

      07.08.2018: какой, нафиг, TRUSTED?! При этом свежеприконнектившийся клиент сразу получит NEWVAL, а это в корне неверно -- уведомления должны приходить только при РЕАЛЬНЫХ обновлениях! Заменено на IS_AUTOUPDATED_YES.

    02.08.2018: что сделано по результатам вчерашнего:

    1. В LAM_CB() вставлен сброс LAM'а (A0F10).
    2. Временно вчера введённый канал "DROP_LAM" -- занимавший место d16'шного DO_SHOT -- убран. Вместе с кнопкой "Tyk!" в скрине.

      Т.е., в IE4 никакого "программного старта" нет и быть не может (только bum_start, который, собственно, и играет ту же роль в "однократном" режиме).

  • 22.08.2018: надо сделать ту же инфраструктуру с каналами V и F_IN_NS/F_IN_HZ, что и в d16.

    22.08.2018: ну надо -- так делаем.

    1. Адаптируем карту.
      • Теперь она w40i,w10d,r50i.
      • Переносим туда каналы V -- с 40.
      • ...и F_IN_NS=49, плюс вводим F_IN_HZ=48.
    2. "Обвязка".
      • privrec_t'шное поле F_IN_NS стало float64.
      • Добавлена frolov_ie4_params[].
      • В _init_d() соответствующая конверсия (в зависимости от знака) плюс лимитирование.
    3. Собственно работа, в _rw_p().
      • Дополнительный "антураж" для обработки вещественных каналов:
        1. Переменные dval, vp, dtype_f64, n_1.
        2. Предварительная ветвь в "шапке" цикла, где идёт проверка типов.
      • Обработка каналов F_IN_HZ/F_IN_NS просто скопирована.
      • Работа с каналами V -- скопирована "творчески": убрано всё, касающееся B. "Калибровки" при высчитывании кванта (Kclk, Fclk) совпадают с Д16.

    Проверено -- работает (значения совпадают с гусиными (на живом железе запускалось по очереди то одно, то другое)).

  • 22.08.2018: еще модификация: нумерация каналов переделана на 0...3, вместо былой 1...4 (которая использовалась в изначальном варианте (конец июля -- начало августа), по аналогии с документацией у Фролова). Начинать с 0 удобнее; тем более, в v4'шном драйвере d16 изначально было так.
  • 11.10.2018: вылез странный косяк -- почему-то не генерился "сигнал окончания BUM-цикла" отдачей канала LAM_SIG.

    11.10.2018: протокол попытки разобраться:

    • Сообщил о проблеме ЕманоФедя ~14:40 -- что ему сигнал почему-то не прилетает.
    • Я проверил -- ну да, не приходит обновление канала.
    • Напхал в драйвлет отладочной печати, запустил -- и печать в обработчике LAM'а отрабатывается, и через канал уведомление приходит.
    • Думал было, что драйвер был какой-то старый, типа с отсутствующей работой с LAM (ага-ага, ну-ну...) -- вернул старый драйвер... ...а от него теперь тоже через канал уведомление приходит!

    У меня есть гипотеза: возможно, это один из тех косяков Фролова/ИЕ4. Если у него почему-либо залип флажок LAM'а (в Альтере), то он новый LAM и не сгенерит. А при рестарте драйвера делается сброс LAM (путём A(0)F(10), как и в обработчике) -- вот залипшесть могла и отлипнуть.

    Резюме:

    1. Надо немного понаблюдать. Если взбрык повторится, то надо вручную из консоли (telnet'ом) выполнить A(0)F(10), и если поможет -- значит, оно.
    2. Плюс с Фроловым эту гипотезу обсудить -- возможно, он по такой наводке посмотрит у себя прошивку Альтеры и найдёт, где б такой косяк мог случаться.

    12.10.2018: записанное вчера -- скопировано из письма ЕманоФеде. Он ответил:

    видишь ли, глюки были, но Гусев мне поставлял сигнал по тому жепринципу и за все прошлое время срывов цикла из-за чего-то подобного не было ни разу.

    На что мне оставалось ответить только:

    1. Ты уверен, что он именно LAM ловил? Мог ведь просто поллингом смотреть на изменение битика статуса, и при изменении генерить "сигнал".
    2. Наверняка у меня и у него чуть по-разному идёт работа с IE4; в обоих случаях согласно описанию, но, возможно, есть какие-то нюансы, на которые девайс реагирует по-разному.

    12.10.2018: Продолжение марлезонского балета.

    • Сейчас имел общение с раздражённым Фроловым, который никак не мог понять странного поведения битиков маски запретов у ИЕ4: в программе вроде бы разрешаю, а на блоке всё равно будто запрет. И сервер рестартовал -- вычитывает из регистра ИЕ4 то же самое значение, а всё равно девайс нормально не работает.
    • Потом Фролов перегрузил крейт (питанием), и всё очухалось.
    • Оказывается, вчера в районе обеда был скачок питания (о чём благородный дон Еманов забыл упомянуть), вследствие чего разнообразная аппаратура пострадала.
    • В частности, как утверждает Фролов, у ИЕ4 "прокисла программа в -- Альтере" вследствие чего глючили битики в маске, и по той же причине вполне могли и LAM'ы дурить.

    09.02.2023: поскольку на днях опять возникала та же проблема (LAM не приходит), а недавно как раз создавалась диагностика для временно не генерившего LAM блока TIME24K4, то и сюда сготовлена пара каналов чисто для диагностики:

    • LAM_VIA_Q -- возвращает значение из Q по F8A0.

      С именем пытался умничать, в попытке найти официальное название в официальной документации по CAMAC -- была ж где-то табличка. Но всё, что нашёл -- в документе "An Introduction to CAMAC", ныне отсутствующем (host not found) и доступном на web.archive.org, на стр.6 оно в табличке значится как "Test Look-at-Me"; так себе название, потому и взято то, которое взято.

    • FORCE_LAM -- при записи 1 просто дёргает LAM_CB(); этакая эмуляция LAM, чтоб сделался сброс по F10A0 и возвраты всего, чего надо.

    10.02.2023: сегодня был бросок питания, так что мы с Федей воспользовались возможностью провести диагностику; заодно с ИК4 в поз.4 сдурел и Д16П в поз.20 (тоже перестал давать LAM'ы), так что и его протестировали.

    Сценарий был такой:

    • Первым делом проверяем "LAM по Q" посредством F8A0 (спойлер: отсутствуют).
    • Затем рестартуем драйвер посредством записи ._devstate=0 -- при этом в контроллере драйвер перезагружается полностью заново, и NAF'ы инициализации, конечно, тоже делает.
    • Смотрим на поведение плюс опять проверяем по F8A0.

    Результаты тестирования, отправленные письмом Фролову и Еманову (Message-ID: 9c50b054-c8f6-303a-9a9a-49567f35ce78@starnew.inp.nsk.su):

    1. ИЕ4 по F8A0 отдаёт Q=0 -- т.е., блок не просто не генерит LAM, но и на своём "внутреннем" уровне не считает LAM зажжённым.

      При этом бит 7 в статусном регистре -- "цикл <<BUM>>" -- взведён в 1, т.е., блок знает, что цикл завершился.

    2. Д16П по F8A0 также отдаёт Q=0 -- поведение совпадает с ИЕ4.
    3. ИЕ4 после рестарта драйвера (БЕЗ дёрганья питания крейту), т.е., просто после F10A0 -- очухался и начал работать, генеря LAM после окончания цикла BUM.
    4. Д16П же после аналогичной операции -- нет, LAM генерить не начал.

    После дёрганья питания крейта всё пришло в норму.

    10.02.2023: а ещё Федя недоволен, что LAM_SIG приходит не просто после BUM_GOING, но и спустя изрядное время -- в районе 10мс.

    Очевидно, это из-за тормозов CM5307/PPC, с его дорогими системными вызовами.

    И решение проблемы тоже очевидно -- надо отдавать единым ReturnDataSet()'ом вместо 2 отдельных операций.

    Сделано, теперь надо будет проверить.

  • 23.10.2018: в этот драйвер добавлен аналогичный frolov_d16'шному функционал, позволяющий выполнять инициализацию значений нескольких каналов из auxinfo, чтоб при запуске сервера (точнее, драйвера) прописывались предопределённые значения.

    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: вылезла еще проблемка -- с округлением, в чём-то аналогичная d16'шной. Всё ровно так же -- из-за непоказа дальних знаков на квант делится буквально чуть-чуть меньшее число, что приводит к "проскакиванию вниз" на 1 квант.

    01.11.2018: конкретно, на примере канала canhw:19.ic.syn.linBeam.dly.set, он же canhw:19.syn_ie4.v2 с R=1000000.0:

    • Квант (canhw:19.syn_ie4.f_in_ns) -- видится как 3200.0, реально указывается как f_in=0.31249728e6 => f_in_ns=1/0.31249728e6=3.2000278530424325e-06.
    • Коду (canhw:19.syn_ie4.a2) 366 соответствует отображаемое в поле число 1.171; а в реальности (%100.80f) -- 1.1712101942.
    • При попытке нажать Enter -- получается уже 1.168, соответствующее коду 365.
    • Причина -- не только непоказ дальних знаков, но и погрешность округления:
      • Экранное 1.171: (1.171*1000000)/3200.03=365.93406936809964
      • ...но и точное 1.1712101942: (1.1712101942*1000000)/3200.03=365.99975443980213
    • Т.е., проблема именно в погрешности вычисления при обратном преобразовании -- получается целое_на_1_меньше.куча_девяток, что после отбрасывания дробной части даёт как раз на 1 квант меньше.

    В качестве решения: вместо отбрасывания дробной части при делении наносекунд на квант --

    c_Ai = dval / quant;
    сделано предварительное округление:
    c_Ai = round(dval / quant);

    После чего проблема на Инжекционном комплексе вроде исчезла.

  • 24.09.2019: Фролов попросил сделать такую диагностическую фичу: возможность посмотреть значения всех управляющих/статусных регистров, просто напрямую считанных из железки, чтоб чтение выполнялось "по команде".

    Очевидно, надо ввести несколько ro-каналов, возвращаемых по команде в rw-канал.

    24.09.2019: делаем.

    • Места в карте предостаточно, и для rw, и для ro.

      Вводим канал-команду 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[]: цикл просто идёт по ней.
    • Добавлено в скрин rfsyn_eng.subsys -- subwin'ом [?] в панель Starts.

      Пока что только числами; наборами чекбоксов покамест не получилось -- какие-то сложности с fordown(), там образуется бесконечный цикл, вероятно, из-за каких-то конфликтов с использованием $1/$2.

    Надо проверять.

    14.10.2019: сервер (canhw:19) рестартован, на вид работает.

    Правда, скрин был халтурноват: отсутствовала кнопка [Read!] (r:do_diag) и у строки RKm стояло r:diag_RKn (из-за чего она светилась чёрным). Но это быстро поправлено.

frolov_k4x1_drv:
  • 30.07.2018: поскольку драйвер может оказаться чуть менее тривиальным, чем выглядит на первый взгляд, то опишем его тут.
  • 30.07.2018: сделано влёт, за полчаса (после изучения полученного сегодня описания).

    30.07.2018: а именно:

    1. Драйвер frolov_k4x1_drv.c получен из frolov_k4x1_2x2x1_drv.c, минимальной модификацией _rw_p().
    2. О наборе каналов.
      • На первый взгляд, там довольно просто: селектор входов и селектор выходов. Т.е. -- 2 канала; так они и сделаны, in_sel (0-4) и out_sel (0-3).
      • Но если с выбором входа всё просто (0 -- никто, 1-4 -- N-й), то выбор выхода можно рассматривать как просто селектор, так и как 2 включателя -- 1 бит на Out1, 1 бит на OutIn.
      • Поэтому аллокировано не 2 канала, а сразу 10 (w10i), чтобы в случае желания иметь раздельные включатели их можно б было легко добавить.
    3. Также -- больше для себя, чтоб проще потом было разбираться -- сделан скрин frolov_k4x1.subsys.

    Оно пока не проверено, но, вследствие тривиальности, считаем за "done".

frolov_time24k4_drv:
  • 23.01.2023: понадобится драйвер этого девайса -- для временнЫх измерений в кикерной (фактически -- контроль GIN25).

    Вчера Фролов прислал описание (чуток недостаточное) "Измеритель временных интервалов Time24k4.docx", сохранённое как 20230122-frolov-Izmeritel-vremennyh-intervalov-Time24k4.docx, а сегодня после личного обсуждения с ним стало более понятно, что к чему.

    Этот новый подраздельчик впихивается сюда, а не в конец списка (после adc200_drv) для того, чтобы все frolov_*_drv были рядышком.

    24.01.2023: фролов прислал обновлённое описание, сохранено в 20230123-frolov-Izmeritel-vremennyh-intervalov-Time24k4.docx,
  • 23.01.2023: процесс изготовления. Скелет получен копированием frolov_k4x1_2x2x1_drv.c, поскольку там всё довольно просто и имя похоже (только чуток длиннее); а работу с LAM добавим (тем более, что с ней пока не совсем ясно).

    23.01.2023: приступаем. Сначала -- информация и соображения.

    Собственно, что было понято в разговоре с Фроловым:

    • Основная работа -- мерять время, проходящее между общим импульсом Старт и 4 импульсами Стоп.

      Эти 4 времени отдаются 24-битными числами.

    • Также девайс умеет накапливать "статистику": для каждого канала есть кусок в 2048 8-битных слов, которым делается stat[time&2047]++ до тех пор, пока какое-либо значение не достигнет 255.

      Т.е., это "распределение" времён в предположении, что все измерения ложатся примерно в одну область (потому берётся 11 младших бит значения и используется в качестве адреса).

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

    • А вот с LAM'ами пока мутновато -- они генерятся по слишком большому числу причин, и опознать причину конкретного пока проблематично.
    • И есть режимы "однократно" и "постоянно". Их функционирование мне не вполне ясно, особенно их взаимодействие с LAM.

    Наверняка в процессе реализации драйвера возникнут ещё вопросы/непонятки.

    Как будем делать ("ключевые" решения):

    • В основном драйвер будет отдавать эти 4 24-битных числа.
    • А статистику можно будет добывать отдельно, по запросу -- видимо, по каналу-команде "а накопи-ка статистику, и как будет -- сразу отдай".

      24.01.2023: ...возможно, понадобится также канал "прерви и отдай то, что есть сейчас".

    • Для отображения в скрине -- видимо, воспользуемся плагином onei32l, для работы которого нужно будет иметь несколько константных каналов вроде numpts=2048.

      24.01.2023: а не понадобятся ли нам плагины вроде twoi32l и fouri32l?

    • Плюс, естетственно, толпа каналов управления режимами работы.

    24.01.2023: продолжаем.

    • Изучаем новую версию описания. Сильно легче не стало, так что написал очередное письмо Фролову (25.01.2023: отправил только сегодня утром).
    • В основном определялся с картой каналов. Пока определились только векторные STATn и скалярные TIMEn.
    • Поскольку с общими схемой и алгоритмом работы не совсем ясно, то делаем те куски, с которыми ясность есть:
      1. ReadAndReturnTimes() -- возвращает 4 времени, сразу одной пачкой; код возврата скопирован с недавноизготовленной gin25_drv.c::ReturnErrorBits().
      2. ReadAndReturnStats() -- вычитывает и возвращает 4 "осциллограммы" (реально это гистограммы) статистики. Читает посредством DO_NAFB() блоками по 128; это как бы "метод обратного flush", как во всяких adc200/adc333/adc4/adc502, но реально тут всё кратно (128 вместо 100 выбрано как раз потому, что 2048 ему кратно).

    25.01.2023: читал документацию, пытаясь понять, как лучше всё организовать. Так и не понял.

    Замечание прямым текстом насчёт маски

    • Оба ReadAndReturn{Times,Stats}() сделаны с таким расчётом, чтобы можно было производить возврат УСЛОВНО, при не-замаскированности соответствующего канала
    • Например, разрешены каналы 1 и 3, а 2 и 4 замаскированы -- тогда возвращаться будут только данные от 1 и 3.
    • Пока это НЕ сделано, но может быть добавлено тривиально.

    26.01.2023: роем далее.

    • С той маской так и не ясно, как обходиться: можно ли её перепрописывать в произвольный момент -- хбз.

      Выдвигаем предположение, что НЕЛЬЗЯ -- т.е., надо производить запись в момент, когда измерения заведомо остановлены.

      Поскольку полноценного понимания нет, то делаем в соответствии с имеющимся, так, как вроде бы достаточно универсально и должно прокатить в любом варианте:

      • Общая концепция: есть "текущая" маска (записанная в блок) и "желаемая" (какие биты были изменены). В моменты, когда менять маску можно -- видимо, перед запуском измерений -- будет выполняться запись (при несовпадении) и складирование "желаемой" в "текущую".
      • В privrec добавлены поля cur_mask и rqd_mask, плюс mask_rflags для складирования статуса последней операции чтения/записи.
      • Запись и складирование делаются в SetMask(), могущей вызываться из того места, где нужно выполнить "прописывание".
      • _init_d() производит начальное чтение.
      • _rw_p() модифицирует битики в rqd_mask в соответствии с указаниями свыше, но SetMask() НЕ вызывает.

      ЗЫ: вот тот "условный возврат" будет проверять как раз битики прямо в cur_mask, не делая чтения из блока.

    • Зато решено, как будет делаться переключение режима тактирования -- пока что просто сразу по записи соответствующего канала. Это автоматом снимет проблему "а если включили внешнее тактирование, а оно отсутствует -- что делать?": достаточно будет переключить режим и это переключение отработает в любой момент.
      • Канал назван CLK_MODE (аналогично DL250).
      • В _rw_p() он очевидным образом отображается на бит STATUS_EXT_FRQ (номер 0).
    • Для минимизации количества "магических чисел" сделаны enum-определения субадресов для NAF'ов "A_*" (как во многих других frolov_*_drv.c), а также масок битиков для статусного регистра "STATUS_*" и для регистра состояния "STATE_*".
    • Ещё были сделаны фиктивные каналы для onei32l -- NUMPTS, RANGEMIN, RANGEMAX. В _init_d() они помечаются как IS_AUTOUPDATED_TRUSTED и отдаются значения 2048,0,255 соответственно.

    И ещё наконец-то сделана компилируемость и "frolov_time24k4" добавлено в ListOfCamacDrivers.mk (ранее просто пилился текст исходника без каких-либо проверок).

    Что дальше -- как-то не совсем ясно.

    • Напрашивается мысль сделать ПОКА только штучные измерения (БЕЗ статистики).
    • Для чего использовать режим "однократно" -- программировать запуск, ловить LAM, по которому возвращать текущие измерения TIMEn и снова программировать запуск.

    27.01.2023: пилим потихоньку...

    • На перспективу сделана ClearStats() -- с ней хотя бы всё понятно: пара NAF'ов, пауза >1.31ms SleepBySelect()'ом, ещё пара NAF'ов.
    • Добавлена работа с LAM:
      • LAM_CB() -- пока очень простой и прямолинейный: вызывает ReadAndReturnTimes(), затем при несовпадении rqd_mask с cur_mask делает SetMask(), а дальше будет делать старт измерений.

        29.01.2023: да, в конце добавлен вызов StartSingleMeasurement().

      • В _init_d() делается заказ.

      Пока всё -- БЕЗ попыток как-то различать причину LAM'а и работать со статистикой.

    29.01.2023: допиливаем первый вариант -- осталось уже специфическое программирование, для правильной реализации которого пришлось пере-изучать "Приложение 2" от Фролова.

    • _init_d():
      1. вначале выполняет инициализацию, на время которой блокирует старты битом 4 (5 у Фролова) в регистре маски.

        ...но НЕ РАЗблокирует, поскольку эта задача возлагается на старт измерений.

      2. Каковой вызывается в конце.
    • Собственно StartSingleMeasurement() -- там сейчас пара NAF'ов, по рецепту "Пример 3" "Однократные измерения" из описания.

      Есть некоторые сомнения насчёт корректности работы с регистром маски: проблема в том, что собственно маска и бит блокирования стартов расположены в ОДНОМ регистре, который в результате пишется из нескольких разных мест, и насколько эти записи скоординированы -- у меня нет полного понимания.

    Битым текстом оставшиеся непонятки по документации:

    1. Как именно следует различать LAM по причинам "цикл измерений закончился, можно вычитывать данные" и "набор статистики завершён, у кого-то стало 255"?
    2. В чём разница между "триггер лам L" (F25A1) и "лам L запрос" (F10A0)?
    3. И в чём разница между F25A1 и F25A2?

      F25A1: "сброс триггера лам L в ноль. Сброс выходных регистров в ноль. Команда для однократных измерений"

      F25A2: "подготовка. Обнуление (сброс регистра состояний) - бит 2 (лам L), бит 3 (тригг. max), бит 4 (max адрес)."

    01.02.2023: пытаемся проверять, результаты озадачивающие.

    1. Почему-то сразу после включения в регистре масок было 0b1000 -- 4-й канал замаскирован (но надо будет ещё проверить).

      02.02.2023: да нет, вроде после дёрганья Фроловым питания крейта в регистре оказался 0. Может, мой драйвер тогда что-то напортачил?

    2. Что Фролов не возвращает Q -- не новость, так что status2rflags() было везде заменено на x2rflags().
    3. А вот почему каналы, вроде бы помеченные как TRUSTED, да ещё и с лишней защитной проверкой в _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ы не приходят. Будем разбираться...

    • Поперепробовал кучу вариантов того, что делать при старте драйвера для сброса блока в начальное состояние. Фиг.
    • Т.е., чтением регистров посредством cm5307_test'а видно, что сразу после старта драйвера значения времён -- F0A2...F0A5 -- обновляются, но вот LAM как таковой так и не возникает.
    • Поговорил с Фроловым, объяснил ситуацию (а также то, что LAM ловится контроллером аппаратно, а не путём постоянного опроса -- он этого не понимал).

      Он пообещал проверить ("ручным контроллером"), генерится ли LAM на шину.

    07.02.2023: после обеда был сеанс общения с Фроловым, во время которого я ему вживую продемонстрировал поведение, выведя на экран панельки Д16 и T24. В результате он задумался и обещал ещё разок посмотреть и перепроверить (плюс сказал, что какой-то косяк в прошивке нашёл; но работать от этого не начало).

    Вечером я, по давно бродившим в голове мыслям, добавил пару неименованных каналов:

    • 99 -- чтение состояния LAM; два варианта: из битика "LAM" в регистре состояния по F1A4 и из Q по F8A0.
    • 49 -- принудительное исполнение возврата данных; полная копия 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(). Результат -- всё работает:

    • 99 -- успешно читает состояние "1" в обоих вариантах (и по F1A4, и по F8A0).
    • 49 -- исполняет возврат данных.

    14.02.2023: был ещё сеанс переговоров с Виталей Балакиным и Федей Емановым, резюме -- скорее всего, возиться с фичей "статистика" не понадобится, т.к. у нас на машине недостижимы частоты выше ~10Hz, а на таких и программно всё делается (причём гибче и полнее).

g0603_drv:
  • 03.09.2018: для управления клистронами требуется плавное изменение уставок в ПКС8.

    Так что заводим свой раздел и для Г0603.

  • 03.09.2018: собственно задача -- уметь плавно менять уставку, аналогично CAN-ЦАПам.

    Конкретно для клистронов -- плавно нужно уметь только поднимать, но всё же лучше сразу сделать "всё".

    03.09.2018: казалось бы, уже есть "инфраструктура advdac" -- использовать бы её. Авотфиг: она сейчас слишком привязана к kozdev -- там сейчас прямо Return'ы со ссылкой на KOZDEV_CHAN_...

    Похоже, проще будет воспроизвести "вручную", следующим образом:

    • Берём код из драйвера CAC208 годовой давности -- пред-advdac'ной.
    • Но "структуру с данными" для каналов используем уже из advdac.h.
    • ...потом, когда-нибудь, если подпилим advdac_slowmo_kinetic_meat.h до корректно-обобщённого варианта, можно будет перейти на него.

    Делаем.

    • В g0603.devtype карта из простой "w8i" стала "w8i,w8i,r8i"; добавились каналы code_rate<0-7> и code_cur<0-7>. И g0603_drv_i.h дополнен аналогично.

      04.09.2018: переведена на "w24i,r8i" -- добавлены каналы IMM, advdac_slowmo'й поддерживаемые (а раз так, то почему бы и нет?).

    03.09.2018@дома: да нафига так извращаться! Взять и адаптировать advdac_slowmo_kinetic_meat.h прямо СЕЙЧАС. Там ведь сложностей-то -- всего лишь ввести имена вместо KOZDEV_nnn, чтоб драйверы-юзеры их определяли.

    04.09.2018: реализовываем адаптацию.

    • Предварительное действие: имевшийся драйверочек g0603_drv.c -- очень простой и нересурсоёмкий. Вытеснять его монструозным, с heartbeat и прочими блэкджеками -- как-то стремновато.

      Поэтому старая версия отфоркована в g0603_simple_drv.c. К нему сделан g0603_simple.devtype, а вот drv_i/g0603_simple_drv_i.h -- нет, пусть пользуется общим (проверки номеров каналов в _rw_p() есть).

    • Собственно адаптация -- описана в его разделе (заведённом сегодня).
    • И сам g0603_drv.c переделан (ниже вроде протокола того, что требуется для адаптации CAMAC-драйвера под advdac_slowmo):
      • В начале добавлены определения DEVSPEC_CHAN_nnn и прочие HEARTBEAT_* и MIN_VAL/MAX_VAL/THE_QUANT.
      • Пустая (возвращающая параметр) val_to_daccode_to_val().
      • Касательно privrec'а:
        1. Добавлены num_isc и out[].
        2. Сделан typedef g0603_privrec_t privrec_t -- т.к. в CAMAC- и CAN-драйверах используются разные принципы именования (в CAMAC -- с префиксом имени драйвера, в CAN -- без).

          В CAMAC-драйверах это рудимент от времён района 2000г, когда была идея (и возможность) объединять #include'ом код нескольких драйверов в один "над-драйвер" -- позже эту работу взял на себя vdev.

      • Код для для записи и чтения каналов перенесён из _rw() в SendWrRq() и SendRdRq().

        Последний сводится к просто вызову HandleSlowmoREADDAC_in().

      • В самом _rw() остался только разбор по номеру канала и вызовы соответствующих HandleSlowmoOUT*_rw().

        Вследствие чего его структура изменена с CAMAC-style (работа с каждым каналом заполняет значения value и rflags, а в конце стоит общий Return) на CAN-style (последним -- умолчательным -- элементом селектора стоит возврат CXRF_UNSUPPORTED).

      • Добавлен _hbt().
      • И auxinfo-параметры для указания скорости -- тут по-простому, без размножения [0]-го элемента в прочие в init_d().
  • 18.09.2018: при первом испытании на клистронах оказалось, что драйвер НЕработоспособен. Ибо он подразумевал работу с БИполярной моделью устройства, а на клистронах -- ОДНОполярная вариация.

    18.09.2018: после первоначального ужаса ("клистрон зашкаливает! а почему?!") и мысли, что в гусином коде есть какой-то неочевидный пересчёт, попробовал-таки почитать ПКС, а увидев странные числа (будто со смещением на пол-диапазона) догадался заглянуть в собственный код -- и вуаля: там при записи к писуемому коду безусловным образом добавлялось 0x8000.

    Исправление тривиальное и очевидное:

    • В privrec добавлено поле plrty_shift.
    • При записи в SendWrRq() прибавляется его значение.
    • И добавлены 2 auxinfo-параметра-флага, на это поле маппирующихся: unipolar=>0 и bipolar=>0x8000.
    • Замечание: в g0603_simple_drv.c такого НЕ сделано.
v0308_drv:
  • 11.11.2019: создаём раздел.

    Странно, что его не было раньше: драйвер-то есть давно.

    Хотя, возможно, именно в том и причина -- создан был ещё в v2hw, а в hw4cx просто портирован с минимальной модификацией _rw_p().

  • 11.11.2019: ЕманоФедя выдал пару запросов:
    1. Чтоб имена каналов имели вид не FUNCn (n=0,1), а cN.FUNC (N=1,2).
    2. Чтоб было плавное изменение, как в драйверах козачиных ЦАПов.

    Первую-то проблему решить несложно -- добавить alias-имён.

    А со второй муторнее -- придётся ОЧЕНЬ сильно переделывать драйвер, добавляя в него использование advdac. Подсмотрим в g0603_drv.c. ...ну и карту каналов для неё расширить придётся, конечно.

    11.11.2019: поехали.

    1. Alias-имена сделаны тривиально. Только не <1-2>, а <0-1> -- чтоб не было шизофрении, когда одна нумерация идёт с 0, а вторая с 1 (и это в рамках ОДНОГО типа устройства!).
    2. А с плавным изменением всё дольше.
      1. Для начала -- махинации с картой каналов:
        • Расширяем с w10i,r10i до w20i,r20i.
        • Соответственно, все r-каналы переехали с 1n на 2n.
        • Добавлены дуплеты каналов RATE, IMM и CUR.
      2. Собственно поддержка slowmo:
        • Сделана по образу и подобию g0603_drv.c, практически "слизана".
        • За двумя исключениями:
          1. ЧТЕНИЕ: поскольку ВВИ позволяет просто в любой момент прочитать уставку, то инфраструктура val_cache[]/rfl_cache[] нафиг не нужна, а просто выполняется чтение.
          2. Структура кода "покрасивше": теперь forward-declaration делаются для SendWrRq() и SendRdRq(), и сами они не рядом с #include "advdac_slowmo_kinetic_meat.h", а перед _rw_p().

            (В "образце" же оба стояли ПЕРЕД #include, а чтоб SendRdRq() мог вызывать HandleSlowmoREADDAC_in(), последний объявлен через forward-deslaration. Что криво, т.к. нефиг объявлять функцию, определяемую в другом модуле.)

        • "Минусом" по сравнению с без-advdac'ной версией является то, что rflags от ЗАПИСИ не берутся вовсе, а возвращаются только от чтения.

    Ну теперь проверять надо.

    12.11.2019: сделан скрин -- v0308.subsys, чтоб нормально можно было тестировать.

    14.11.2019: проверил. Эх! :D

    • В каналах RATE почему-то показывалось значение -1, хотя инициализируются они значением 500.
    • Причём работает всё "как положено" -- как будто значение стоит именно 500.
    • Напихивание отладочной печати в advdac_slowmo_kinetic_meat.h::HandleSlowmoOUT_RATE_rw() показало, что ТАМ всё в порядке -- 500.
    • А вот "cdaclient -m", натравленный на такой канал, показывал при ре-старте драйвера (записью ._devstate=0) последовательно возвращаемые 500 и затем сразу же -1.
    • По этим всем симптомам и разобрался: за образец брался g0603_drv.c, но его _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 тоже делается по умолчанию.

    • Решения простое: для всех каналов, связанных со slowmo (VOLTS, RATE, IMM, CUR), в конце обработки вместо break теперь делается goto NEXT_CHANNEL.

      Решение не самое красивое, но рабочее и наиболее простое.

    После исправления -- вроде всё вообще как надо. Теперь пусть ЕманоФедя допроверяет.

  • 08.09.2021: есть потребность добавить поддержку девайса под названием Б0307, который технически является как бы 1-канальным вариантов В0308. Сегодня также экспериментально выяснилось, что он не отдаёт бит Q.

    Откуда напрашивается такое решение: добавить драйверу auxinfo-параметр "b0307", булевский, при выставленности которого:

    1. На "нечётные" каналы -- у которых A=1 -- НЕ выполнять case(), а делать
      value = 0; rflags = CXRF_UNSUPPORTED;
    2. "Срезать" битик CXRF_CAMAC_NO_Q -- например, просто
      rflags &=~ CXRF_CAMAC_NO_Q;
      прямо перед возвратом.

    08.09.2021: сделано, ровно по тому проекту.

    10.09.2021: проверяем. Оказалось, что не у всех каналов флаг CXRF_CAMAC_NO_Q срезался -- не хватало в SendRdRq(). Добавлено.

    Засим "done".

adc200_drv:
  • 18.04.2022: занадобилось разобраться с поведением этого драйвера, так что заведём уже раздельчик, чтобы протоколировать происходящее.
  • 18.04.2022: причина -- как-то странно ведёт себя осциллограф обмера кикеров в Зале 2: вроде бы туда подаётся какой-то довольно большой сигнал, в районе 8V, а девайс, рассчитанный на +-2V, не только не показывает перешкал, но вообще показывает какое-то очень маленькое значение, однако ФОРМА сигнала похожа на правильную.

    Тут вспомнилось, что 13-07-2017 были какие-то вопросы к работе то ли пересчёта коды->микровольты, то ли с вычислением диапазонов (что выражалось в странном отображении осциллограмм); как минимум вопросы были к учёту сдвига нуля.

    18.04.2022: немного результатов разбирательства:

    1. Не удалось найти никаких следов старой архитектуры (ещё времён 2005-2006гг, с которой и пошли все fastadc), когда в клиенте были чекбоксы касательно J5 и J6, менявшие селекторы диапазонов с ".256,.512,1.024,2048" на "1,2,4,8" (там просто менялось состояние managedness 2 имевшихся селекторов: в зависимости от состояния чекбокса показывался один из двух).

      Нашлась только запись в bigfile-0001.html за 19-12-2007 с пожеланием этого.

      19.04.2022: а, разобрался: то было не в adc200, а в ndbp_nadc200_elemplugin.c. В мэйнстримовую же поддержку ADC200 оно так и не прошло.

    2. Анализ кода, со сравнением с описанием adc200.doc, показал, что вроде бы пересчёт из кодов в вольты (с учётом сдвига нуля и диапазона) выполняется верно.

      Причём со слвигом нуля отдельно поигрался на живом adc200_kkr1 -- тоже на вид всё работает верно: при изменении сдвига осциллограмма перемещается в осмысленном направлении (вверх при увеличении, вниз при вменьшении) и подписи на осях меняются согласованно.

    3. Анализ экземпляров hw4cx/drivers/camac/src/adc200_drv.c за разное время показал, что с начальной версии 2015 года изменения чисто косметические (добавлены auxifo-параметры и поддержка RUN_MODE), а код пересчёта не менялся вовсе.
  • 18.04.2022: надо бы ввести в драйвере поддержку второго набора диапазонов (который в 4 раза больше) -- параметрами J5 и J6 в auxinfo.

    19.04.2022@~19:00, идя мимо парковки у Ильича 1 и в290мо154: отдельный вопрос с отображением в клиентах: по-хорошему надо бы и на селекторах как-то отражать иной диапазон.

    • Идеально это было реализовано в ndbp_nadc200_elemplugin.c, где просто активировался один из 2 селекторов в зависимости от выбранного режима.
    • Но тут, даже если сварганить 2 селектора (впихнув их внутрь контейнера-формы, которую и размещать в соответствующей клетке сетки),
    • ...будет проблема с ОТОБРАЖЕНИЕМ значений: ведь все поканальные ручки управляются из pzframe_gui.c, а там оно ну никак не рассчитано на подобную "множественность".
    • 20.04.2022: можно выпендриться: жонглировать значениями gui->k_params[RANGE{1,2}] в зависимости от текущих значений J5,J6.

      Но даже такой фокус не проканает -- по причине отсутствия у конкретных knobplugin'ов своих privrec'ов (с чем мы уже недавно столкнулись при работе над onei32l 11-04-2022).

      @Коптюга, тропинка по леску от Терешковой мимо Коптюга-19: а можно извратиться:
      • Хранить указатели на ручки в "незанятых" ячейках -- да хоть в 3,4 и 5,6.
      • Т.е., СОЗДАВАТЬ-то их в ячейках [J5] и [J6], чтобы PzframeGuiMkparknob() регистрировал бы правильные cn'ы, а затем копировать в неиспользуемые.
      • Тут, кстати, автоматом решается и проблема "как помнить, какой вариант сейчас активен" -- да просто сравнивать k_params[J5] с "ячейками хранения".
      • 21.04.2022: только в таких ячейках всё равно надо указывать PZFRAME_CHAN_IMMEDIATE_MASK (несмотря на .name==NULL) -- чтобы PzframeGuiUpdate() не пыталась бы их обновлять.

        А правильнее -- чтоб PzframeGuiUpdate() не трогала бы ячейки с .name==NULL (сейчас там эта проверка отсутствует).

    20.04.2022: делаем.

    • Идеологическое: введём 2 канала, причём это будут каналы ЗАПИСИ -- чтоб можно было при надобности на ходу поменять (для случаев кривой конфигурации).

      Значение "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.

      И далее

      1. ReadMeasurements() -- из кодов в вольты;
      2. 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") -- тоже работает.

dds300_drv:
  • 03.02.2023: заводим раздельчик (ранее его не было ни тут, ни в bigfile-0001.html.

    Занадобилось отразить странности с чтением/записью.

  • 03.02.2023: вылезла странность: каналы ОБОИХ DDS300 на сварке подсвечены бордовым. Я посмотрел -- флаг CAMAC_NO_X (Q там и не проверяется).

    Попробуем исследовать вопрос.

    03.02.2023: "пробуем".

    • Первая мысль была "а если просто блока в той позиции нет?". Увы, не так: и Семёнов говорит, что оба на месте, и я попробовал сделать "cm5307_test 4,0,0 8,8,0" -- в обоих случаях X отдаётся.
    • Но у девайса всё намного сложнее: он не просто NAF'ы выполняет, а там есть целый протокол чтения/записи данных -- доступа ко внутренним регистрам чипа-синтезатора. Чтение -- пишем NAF'ом A1F16 адрес, потом другим NAF'ом A0F0 читаем результат; запись -- единственным NAF'ом A0F16 пишем адрес (старший байт) и данные (младший байт)

      И вот в этой цепочке от каждого NAF'а берутся флаги, которые OR'ом складываются. А для регистров частоты -- "FTW" -- используется по 6 байт, так что при чтении 12, а при записи 6 операций, чьи флаги складываются.

    • Попробовал рестартовать драйверы, посредством ._devstate=0 -- поля записи побелели, хотя числа в них какие-то бредовые.

      Но первая же попытка записи любое поле приводит к его побордовению.

      ...и тут возникла гипотеза -- а не с питанием ли крейта проблемы?

    • А затем полез смотреть в логи --
      • оказывается, ещё давным-давно в драйвере был сделан логгинг (на v4'шном дата 14-07-2015, но оно есть и в v2'шном от 26-07-2013),
      • а в devlist'е обоим экземплярам стоит "log=all",
      • причём ТОЛЬКО для tower:47.

        Раскопки по архивам показали, что оное логгирование было включено ~26-01-2022 (в w20211231-serial-mxupcie.tar.gz его ещё нет и дата 09-09-2021).

    • А собственно в логах: при старте что-то вычитывается (припоминается, что вроде бы и раньше похожие числа были) --
      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
      
      (фиг знает, почему каждый по 2 раза), а вот при записи --
      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: не успетое вчера:

    • В целях диагностики в логгинг был добавлен вывод полученного X -- (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().

    • ЗЫ: кстати, странно, что при обратной записи ранее прочитанного числа-кода FTW оно пишется 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.

    • После этой записи чтение вычитывает те нули, что были записаны.
    • Но если рестартовать после этого драйвер, то опять вычитаются все байты 0x60 (хотя и БЕЗ проблем).
    • После чего попробовал писать не сплошь нули, а меняющиеся байты:
      ...# 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 же и прочиталось.

    Ну вот что тут теперь думать, а? Напрашиваются такие выводы:

    1. Что-то глючит. Поскольку софт там не менялся давно (включая cm5307_dds300.drvlet ДО начала проблем), а глюки начались "вдруг" -- видимо, всё же какие-то приколы с железом. Вероятно, БП крейта?
    2. Почему при ручном тесте не вылазит проблем с X? А хбз.

      Возможно, причина в том, что драйвер молотит NAF'ы на максимальной скорости, а cm5307_test перемежает их парсингом, что вносит задержки.

    Вставить логгинг каждого NAF'а прямо в код драйвера? Это, конечно, тоже внесёт задержки, но зато будем видеть точные данные...

    • Вставил. Выглядело шизануто: ВСЁ прекрасно исполняется, везде есть X. А в результате печатается "-" вместо X.

      Чуть не офигел, пытаясь разобраться, как же так -- все индивидуальные операции дают 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". Так что проверить, увы, не на чем.

dds500_drv:
  • 03.02.2023: на всякий случай резервируем место сразу после сородича. Но вряд ли понадобится -- Шубин и так-то их больше не делал, а 18-02-2022 уволился (инфа от Купчика 14-03-2022).
:::
:::
hw4cx/drivers/serial/:
  • 23.11.2021: понадобилось этот раздел добавить.

    Ранее всё на эту тему обсуждалось в bigfile-0001.html (именно во время CXv2 пройдены основные грабли работы с COM-портами, а в v4 уже практически копировалось готовое, с минимальной переделкой под изменившийся API плюс под наличие настоящих layer'ов).

    Но сейчас появилось уже своё, специфичное.

    Вставляем его "по логике" -- после CAMAC, но ДО "cm5307", который всё равно никогда уже не наполнится (т.к. cm5307_drv.c с момента создания того раздела окончательно уволен в пользу remdrv.c).

  • ZZZZZZZZZZZZZZZZZZZZZZZZZ

    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 -- ОЧЕНЬ неудобная штука в роли "просто шины для подключения устройств".

    1. С одной стороны, при открытии порта нужно выполнить МАССУ "шаманских заклинаний" просто для того, чтобы получить доступ к ПРОСТО ЛИНИИ ПЕРЕДАЧИ ДАННЫХ -- чтоб БЕЗ всей этой фигни типа XON/XOFF, скорости, чётности, битности, стартовых/стоповых битов, неназначения CTTY и прочей хрени, специфичной для терминалов и модемов.
    2. С другой стороны, как раз функциональность для просто линии передачи данных -- там ОТСУТСТВУЕТ. Ни нормального контроля ошибок и уведомления о них, ни стандартизованной реакции на дисконнекты/реконнекты (это для USB-устройств), а опять в основном всякое шаманство.
  • 24.12.2021@обходя Пирогова-34 по дороге домой: учитывая извратность уже сейчас существующих вариантов "программирования", а также то, что, возможно, занадобится поддерживать ещё и тот адаптер AData (27.12.2021: Advantech) (12.12.2022: PCIE-1612C-AE), что уже есть у ЧеблоПаши (12.12.2022: нет, до сих пор нету -- заявка 45257 от 03-09-2021, "Срочно", но фиг...), напрашивается мысль, что нынешний подход "всё-всё сконцентрировано в stdserial_hal.h, управляемое #ifdef'ами" уже становится не вполне адекватен.

    А нужно-таки вытаскивать специфичности настройки serial-портов в отдельные файлы, примерно таким макаром:

    • Если есть что-то специфичное, то Makefile определяет символ SPECIFIC_SERIAL_SETUP_FILE_H, ...
    • ...а в stdserial_hal.h имеется такой кусочек кода:
      #ifdef SPECIFIC_SERIAL_SETUP_FILE_H
        #include SPECIFIC_SERIAL_SETUP_FILE_H
      #endif
      
      -- вместо нынешних начальных #ifdef MOXA....
    • Этот файл
      1. делает, при надобности, нужные #include;
      2. если нужна специфическая настройка линии после открывания, то он также определяет некоторую макро-функцию SPECIFIC_SERIAL_SETUP_CODE.
    • Соответственно, в stdserial_hal.h после открытия дескриптора стоит условный "вызов":
      #ifdef SPECIFIC_SERIAL_SETUP_CODE
        SPECIFIC_SERIAL_SETUP_CODE();
      #endif
      

    ...да, тем самым у нас становится как бы 2-уровневая архитектура HAL-файлов.

    27.12.2021: делаем...

    • Создан mxupcie/mxupcie_serial_setup_file.h с соответствующим содержимым.
    • Ну и moxa/moxa_serial_setup_file.h аналогично.
    • Только имена SETUP_CODE сделаны чуть иные: вместо одного варианта там есть 2: SPECIFIC_SERIAL_SETUP_CODE_1() и SPECIFIC_SERIAL_SETUP_CODE_2() -- для того, чтобы можно было выполнять эту настройку либо сразу после open()'а, либо чуть позже, после tcflush()+tcsetattr().
    • И в {moxa,mxupcie}/Makefile добавлены строчки вида
      LOCAL_DEFINES+= -DSPECIFIC_SERIAL_SETUP_FILE_H='"moxa_serial_setup_file.h"'

      ...вообще-то это ПЛОХОЙ вариант. ХОРОШИЙ -- если б уже src/ShadowRules.mk ставил бы этот -DSPECIFIC_SERIAL_SETUP_FILE_H конкретно в одно правило зависимости, туда же, куда и прочие -DSERIAL....

    • И в stdserial_hal.h всё уже подготовлено.

      НО: оно там пока что закомментировано, а оставлено пока старое.

    Т.е., всё подготовлено, но ПОКА что используется старая архитектура. Почему? А потому, что старая выглядит более понятной и обозримой.

  • 27.12.2021: и ещё -- давно (ещё с 23-12-2021) грызёт идея, что надо бы как-то уменьшать зоопарк *piv485-layer'ов (и оответствующих настроек stdserial_hal.h'а):
    • По сути-то они в основном отличаются лишь префиксом в имени /dev/tty*NNN, и сейчас этот префикс указывается в макросе SERIAL_HAL_PFX.
    • А вот если бы как-то указывать просто оный префикс прямо в конфигурации -- то можно б было использовать ОДИН layer на все варианты ttyS*/ttyUSB*/ttyM*/ttyMUE* (уж на первые 2 -- точно, даже прямо сейчас).
    • Особенно если получится настройку порта делать отдельно, аналогом setserial'а.

    Вопрос лишь в деталях реализации:

    • На уровне HAL'а-то всё просто: добавить serial_hal_opendev()'у параметр dev_tty_name_prefix", который использовать повсеместно.
    • Но вот как УКАЗЫВАТЬ этот префикс "в конфигурации"?
      • Утилитам командной строки -- ладно, ключиком лишним (или параметром); хотя всё равно неудобно.
      • А драйверам?

        В layerinfo -- нельзя: оно привязывается к номеру порта, а на одном узле может быть НЕСКОЛЬКО портов с одним номером, но разными типами -- вроде ttyM0 и ttyMUE0.

        А никаких СТРОКОВЫХ параметров настройки драйвера у нас не предусмотрено.

        ...помнится, аналогичный вопрос уже как-то возникал; в связи не то с serial-драйверами, не то с CAN'овскими...

        Нашёл -- да, в связи с ОБОИМИ: в разделе по cxsd_driver, за 07-09-2014, ключевое слово "businfostr".

    Не, хочется-то хочется (уменьшить зоопарк), но вот только неудобств от этого при использовании видится многовато.

    28.12.2021: немножко информации про потенциальный девайс от Advantech:

    • Модель -- Advantech PCIE-1612C-AE.
    • Драйвер -- там отдаёт всё кривой IIS, так что wget'ом скачивается плохо -- Linux_COM_2020-12-30.tar (бОльшая часть из 41Мбайт занята каким-то бинарником), внутри него есть Exar/adv_17V35X-Ver5.0.7.0.tar.gz, в котором уже adv_17V35X/.

      ...и вот в нём схема именования вроде как "/dev/ttyAP*", но, судя по readme, предусмотрен также и какой-то другой вариант, мне малопонятный, "/dev/ttyBxxPxx" ("For example,B10 means Board ID is 10,P0 means port0.").

    • А вот никакой НАСТРОЙКИ режима работы никакими ioctl()'ами там не предусмотрено, потому как...
    • Режим работы, судя по мануалу, стр.24(=14) устанавливается jumper'ами -- есть варианты 232,485-2,422; режима 485-4 вроде бы нет.
    • Каким драйвером он обслуживается в штатном ядре -- хбз (если чё, в исходниках linux-5.10.88 строки '"ttyAP' не найдено).
      29.12.2021: ЧеблоПаша утверждает, что раз платка сделана на чипе Exar, то должен использоваться драйвер 8250_exar.

      А им, судя по drivers/tty/serial/8250/8250_core.c, используется имя "ttyS".

    Т.е., раз нет никакой настройки, а нужно просто конкретное имя /dev/ttyAP* открыть -- то как раз и пригодился бы вариант "обычный драйвер для COM умеет обращаться к произвольному имени".

    28.12.2021: в качестве идеи решения проблемы "а как строку указывать?!":

    • Если иметь ОДИН layerinfo с именем "compiv485", без номера -- т.е., спрашивать его вызовом GetLayerInfo("compiv485", -1)
    • И в нём указывать "таблицу маппирования" -- т.е., что шина номер такой-то отображается на девайс такой-то; например, "0:ttyAP0,1:ttyUSB0".

    ...правда, это не отвечает на вопрос, как быть с утилитами командной строки. Может, позволить им указывать вместо номера линии прямо имя файла-устройства? Тогда это потребует ещё более полной перетряски API "serial_hal.h".

  • 15.12.2022: а вот ещё: есть такая штука -- Moxa NPort, семейство "конвертеров", или официально "преобразователь RS-422/485 в Ethernet"; например, NPort 5630-16 (такие куплены на СКИФ).

    Так вот: у них, кроме режима работы "виртуальный 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)", где

    • 10. Как работать с устройством NPort напрямую по сети Ethernet, не используя драйвер виртуального COM-порта?

      Все устройства NPort поддерживают режимы работы TCP Server, TCP Client, UDP Server/Client. В этих режимах данные передаются по сети через TCP- или UDP-сокеты. Данные передаются в "сыром" виде: информация, записанная в сокет, будет в неизменном виде выдаваться в последовательный порт устройства NPort. И, наоборот, пришедшие в последовательный порт данные без изменений будут переданы в сокеты TCP и UDP.

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

      Если же Вам в режиме online необходимо менять настройки последовательного порта (скоорсть, четность, ...) или управлять служебными сигналами (RTS/CTS), то такие манипуляции осуществляются через служебный TCP-порт преобразователя. Протокол работы по этому порту публично не распространяется. Пожалуйста, если возникнет указанная задача управления портом, напишите об этом на наш адрес технической поддержки support@moxa.ru

      (bold мой)
    • 11. Мой NPort находится за маршрутизатором / NAT-устройством. Какие TCP/UDP-порты нужно открыть, чтобы обеспечить доступ к NPort?

      Передача информации с каждого последовательного порта NPort идёт по двум TCP-портам: порт для передачи данных и порт для передачи служебных команд.

      В режиме Real COM во всех устройствах NPort для передачи информации используются следующие TCP-порты
      Порт передачи данных (COM1 ... COM16)Командные порты (СОМ1 ... СОМ16)
      TCP: 950 ... 965TCP: 966 ... 981
      Порт передачи данных (COM17 ... COM31)Командные порты (COM17 ... COM31)
      TCP: 982 ... 997TCP: 998 ... 1013

mxupcie/:
  • 23.12.2021: понадобилось сделать поддержку для PCIe-карточки-адаптера MOXA CP132EL, предоставляющей 2 штуки интерфейса RS485.

    (Сама карточка куплена ЕманоФедей для установки "стенд измерений ускоряющих структур" у Лёши Левичева; ранее там использовался USB-адаптер от Каплина, но он постоянно давал USB-дисконнекты.)

    По результатам предварительных разбирательств определено, что требуемый нам драйвер называется mxupcie.ko, а потому директория так и названа.

  • 23.12.2021: подраздельчик для описания РЕЗУЛЬТАТОВ ИССЛЕДОВАНИЙ этого клятого интерфейса.

    23.12.2021: попытки добыть какие-то драйверы и разобраться, ЧТО за драйверы надо брать.

    • Сначала *Я* попробовал взять с moxa.ru со страницы "Плата CP-132EL-I-DB9M" драйвер moxa-linux-kernel-5_x_x-driver-v5_0_tgz.zip (дурацкий, кстати, файл -- там .tgz внутри .zip).
    • Заглянул внутрь -- а там директория mxser/ и внутри всякое, включая driver/kernel5.x/mxser.c.

      И оно не компилируется -- в int-функции стоит просто "return", что приводит к ошибке "'return' with no value, in function returning non-void".

      Плюс -- прямо В ОБЫЧНОМ ЯДРЕ, даже в 3.10 от RHEL7, есть mxser.[ch] (но только в исходниках -- сборка отключена; хотя в Ubuntu -- включена).

    • OK, ЕманоФедя нашёл другое место -- на moxa.com страница "CP-132EL Series" даёт драйвер moxa-linux-kernel-5.x.x-driver-v5.1.tgz, и он уже компилируется.
    • Также ЕманоФедины исследования подсказали, что НАМ (судя по PCI ID?) нужен драйвер не mxser, а mxupcie; которого в ядре как раз нету.
    • И ещё, похоже, чтобы эта фигота работала -- а то по умолчанию это устройство почему-то подхватывается штатным драйвером serial, "выдающим" его порты за ttyS4 и ttyS5 -- надо перед загрузкой драйвера предварительно запускать скрипт moxa_unbind (видимо, он как-то отвязывает устройство от драйвера serial).

    24.12.2021: за вчера-сегодня разбирался в том, КАК эту дрянь программировать. НЕТ НИКАКОГО описания, поэтому пришлось рыться по коду -- как утилит, так и драйверов.

    1. Сначала предположил, что раз это тоже порт RS485 от MOXA, то и программирование его должно бы быть совместимо с оным от наших ARM-коробочек.

      Но нет: это НЕ так, и даже файла moxadevice.h тут нет.

    2. Окей, смотрим содержимое того архива moxa-linux-kernel-5.x.x-driver-v5.1.tgz. Там есть файлы mxser.h и mxpcie.h, которые определяют userspace-API соответствующих драйверов. По идее, это должна быть реализация одного же API для разных вариантов чипа, так что файлы должны быть похожи или и вовсе включать какой-то третий .h.

      Но тоже нет: содержимое файлов mxser.h и mxpcie.h практически не пересекается. Это два совершенно РАЗНЫХ файла.

      После этого встаёт вопрос: а *userspace*-то утилиты как работают -- ведь они, по идее, должны бы одинаково взаимодействовать и с mxser'овыми /dev/ttyM*, и с mxupcie'шными /dev/ttyMUE*? А ответ -- в п.3...

    3. Смотрим в исходники утилит, на примере utility/conf/conftool.c, чтобы понять, какой же .h она #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: ЕманоФедя поразбирался:

      • в мануале (под странным заголовком "PCI Express Board User's Manual) на странице 47(=6-5) в табличке сверху есть указание "0xF : RS-485 2W"; это из описания чипа MUE250, на котором построена эта CP-132EL.
      • В даташите самого чипа -- "PI7C9X7952PCI Express(R) Dual UARTDatasheet", Revision 1.8, August 2017 -- на странице 32 в таблице 6.2.40 тоже сказано "1111b: RS485-2W".
      • Так что то ли важен лишь битик 0x4=0100b (режим -4W -- это 0xB), то ли просто в mxpcie.h ошибка.

      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
        
      • Я смотрел на ВТОРОЙ блок -- ну просто потому, что там названия совпадают с теми, что в conftool.c.
      • А надо было, видимо, на ПЕРВЫЙ -- потому, что именно он совпадает с числами из мануала и даташита.

        ...и что вообще там означает второй блок -- загадка: в коде драйвера ссылок на MX_RS* нет совсем (а вот на MOXA_UIR_RS* есть -- именно так я и понял, что к чему: там эти константы очень активно используются, в нескольких разных местах).

        Возможно, второй блок -- вообще рудимент от какого-то другого файла; иначе говоря -- просто мусор.

      Как именно я (или другою юзер) должен был догадаться, что нужно обращать внимание именно на константы с "UIR" в именах -- хбз. Поскольку ни описания, ни комментариев никаких нет.

      Так что вывод остаётся прежний -- ОТВРАТИТЕЛЬНО написанный софт.

      27.12.2021: ещё пара "деталей":

      1. В драйвере mxser.c всё определение userspace-API -- не в mxser.h, а прямо внутри самого .c!

        Причём это конкретно ТУТ оно так, а в том, что прилагается к RHEL7'шному ядру 3.10 -- всё именно в mxser.h.

      2. И вот эти определения как раз СОВПАДАЮТ с теми, что в moxadevice.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 -- РАЗНЫЕ!!! Даже и не знаю, что про такое можно сказать (остаётся только цитировать Лаврова, Задорнова ("ну тупы-ы-ые!") или Будрайтиса ("сказочный долбо#б...")).

    4. Но, как оказалось, этот файл просто НЕЛЬЗЯ #include'ить в userspace-код, т.к.:
      • В нём используется макрос VERSION_CODE(), определяемый в mxpcie.c (прямо шЫдевр стиля -- .h требует от своего включатора определения чего-то, НЕ являющегося "параметрами"...).
      • Оттуда #include'ится <asm/uaccess.h>, который обычная система иметь совсем не обязана.

      Откуда вопрос: а НАФИГА вообще было делать эти отдельные файлы mxser.h и mxpcie.h, если они в принципе не годны ни для чего, кроме как быть включены в код драйверов?

    5. Так что в конечном итоге оказалось проще ВРУЧНУЮ забить соответствующие определения прямо В СВОЙ исходник:

    Меня вот даже не в универе, а ещё в ШКОЛЕ, на азах программирования, учили, что ТАК делать нельзя -- если у тебя какие-то числа/коды, совместно используемые несколькими разными модулями, или, тем более, определяющие интерфейс между этими модулями, то их нужно выносить в отдельные файлы, которые и должны использоваться обоими модулями.

    Но тут -- вроде бы "уважаемая" контора MOXA, а такой отвратительный код...

    И немножко ещё "деталей":

    • Имеется некоторая шиза с именованием:
      • файл-исходник называется mxpcie.cmxpcie.h),
      • а вот МОДУЛЬ -- уже mxupcie.ko, и имена внутри исходника имеют префиксы mxupcie_.
    • При всей вроде как отличающести реализации от ARM-коробочной, сходство всё же есть и наипрямейшее:
      • Тут в mxpcie.h определения такие:
        #define MOXA                    0x400
        . . .
        #define SMARTIO_PUART_SET_INTERFACE     (MOXA + 79)
        . . .
        #define MX_RS485_2W                     4
        
      • а в moxadevice.h --
        #define MOXA_SET_OP_MODE      (0x400+66)        // to set operating mode
        . . .
        #define RS485_2WIRE_MODE        1
        
      (да, числа-коды режимов работы разные, но это определяется разными чипами).

      Но есть и отличие:

      • на ARM-коробушке команда настройки -- 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: да, я мало того, что потратил ДВА дня на разбирательство с кривейшей поддержкой железки, так ещё и потратил море времени на то, чтобы это всё записать. Зачем?

    Затем, что это очень показательный пример -- тут ПЛОХО ВСЁ. Записано просто чтоб было под рукой, как пример, чьи проблемы можно привести детально. Вкратце:

    1. Документации по программированию НЕТ. Приходится смотреть по исходникам, что огромная проблема, т.к...
    2. Сам драйвер сделан из рук вон плохо.
    3. Отсутствует хоть какая-то общность между разными устройствами -- НЕТ общего API (разные коды ioctl()'ов, разные коды режимов).
  • 23.12.2021: а тут уже о реализации софтинки.

    24.12.2021: за вчера-сегодня КАК БЫ сделано.

    • "3-буквенный префикс" выбран "mxu" -- как сокращённое имя директриии и драйвера ("mxupcie"). ...хотя можно было выбрать и "mue" -- по префиксу имён устройств /dev/ttyMUE*.
    • Собственно "код" в этой директории весь заключён в Makefile, скопированном из serial/com/, в который добавлена строчка
      LOCAL_DEFINES= -DMXUPCIE_KSHD485
      по образцу из moxa/Makefile.
    • Основное же время ушло на поиски того, как надо выполнять специфическую настройку порта после открытия (это красочно описано в предыдущем разделе по "результатам исследований").
    • Код этот запихивался, по введённой в moxa/ традиции, прямо в stdserial_hal.h.
    • СНАЧАЛА пытался брать определения из mxpcie.h, помещённого в свежесозданную given/mxser-linux-kernel-5.x.x/.

      Но это всё не получилось (причины также описаны в предыдущем разделе).

    • Поэтому необходимые 3 штуки определений были запихнуты прямо в stdserial_hal.h, туда, где должно было бы стоять #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() и возникло сомнение: вот там используется такой порядок:

    1. open()
    2. Стандартная настройка -- tcflush(fd, TCIFLUSH) и tcsetattr(fd,TCSANOW,&newtio).
    3. Специфическая настройка -- уставка режима RS485_2W.

    Собственно -- а может, надо СРАЗУ после открытия делать специфическую настройку, а ПОТОМ уже стандартную (где скорость и прочие режимы работы порта)? Ведь эта "стандартная" может по-разному работать в разных режимах.

    @вечер: заглянул в conftool.c -- ДА, там MX_SET_INTERFACE выполняется СРАЗУ после open() (посредством вызова внутренней mx_set_interface()). Правда, там это ЕДИНСТВЕННОЕ выполняемое действие -- т.к. этот conftool работает дополнением к setserial'у и предполагается к использованию совместно с собственно ПО, работающим с линией.

    29.12.2021: заработало!!! Причина первоначальной проблемы оказалась в том, что ЕманоФедя при подключении перепутал провода A и B; он после этого даже получил консультацию Волкольда Волкольдовича, посмотрел на линию осциллографом -- увидел пакеты 10 раз в секунду (это "пинги" KCMD_GET_STATUS=3), затем поменял провода и увидел уже нормальные запрос/ответ.

    На этом можно считать за "done".

serial4:
serial5:
PCI/cPCI:
  • 01.06.2022: заводим раздельчик для всего, связанного с PCI/cPCI.

    Раньше потребности не возникало (сами cpci/-драйверы непринципиально отличаются от v2hw'шных по сути работы с железом, а от прочих CAMAC/VME -- по сути pzframe'овости).

    Сейчас же -- захотелось довести драйвер ядра uspci.c до компилируемости под современные ядра (побудительный мотив -- "а не использовать ли USPCI для взаимодействия с 250МГц и 5ГГц осциллографами от Хильченко, чтобы драйверы были в userspace).

  • 08.06.2022: битым текстом: у нас так и НЕТУ поддержки 64-битных чтения/записи.
uspci:
  • 01.06.2022: заводим подраздел -- ради него ж всё и затевалось.
  • 01.06.2022: надо проапдейтить work/uspci/kernel_module/uspci.c -- он не собирается под linux-3.10 из-за ioctl(): там что-то поменялось.

    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.
    • Видимо, какое-то время начиная с 2.6.11 был "переходный период", когда ядро готово было вызвать и старый вариант, и новый; а потом старый устранили вовсе.
uspci_test:
  • 01.06.2022: тоже заводим подраздельчик. Правда, с не очень ясными перспективами.
  • 04.06.2022: исправлен косяк с парсингом INSTANCE и FUNCTION в ParseDevSpec(), внесённый, видимо, 24-01-2022.
Резерв4:
Резерв5:
Резерв3:
Резерв4:
Резерв5:
cm5307:
cm5307_drv:
common/*cm5307*:
rrund:
remdrv и remdrv_proto:
  • 04.07.2009: уж с год как принято, что интерфейс удалённых драйверов -- "представитель" -- будет называться remdrv, так что переименовываем раздел из "drivelet_drv и drivelet_proto" в "remdrv и remdrv_proto".
  • 16.12.2009: похоже, что собственно реализацию разбора протокола remdrv_proto на стороне удалённых драйверов надо также делать модулем/библиотекой. Причины:
    1. Может возникнуть потребность иметь нечто типа canserver'а не только под CANGW, но и под другие архитектуры.
    2. Стоит унифицировать разборщик и прочие "умности", а не иметь дубли в canserver'е и cm5307_dbody.
    3. Для электроники Карпова в VME (ТНК/Зеленоград) потребуется нечто очень похожее на помесь canserver'а с dbody.

    Так что напрашиваются следующие reusable-компоненты:

    • "Библиотечка" разбора приходящих пакетов с переводом их в cxsd_driver-подобные вызовы, плюс, ...
    • ...в дополнение к ней -- библиотечка отправки пакетов (данных, логов, ...) серверу.
    • (возможно) "Библиотечка" для обработки запросов на "запуск" драйвера -- разделяемая между rrund, canserver'ом и прочими подобными.
    • Собственно rrund.
    • Собственно "remdrv_server" -- чтоб canserver не был чем-то сильно выделенным, а просто собирался бы на основе cxloader'а из готовых компонентов (в число которых также будет входить c4l-based предподготовленный cankoz_lyr, а специфичного будет лишь "glue" этого всего).

    26.04.2010: да, вчера продумал разделение на компоненты более подробно. Итак:

    • Создаём новую библиотеку -- libremdrvlet, живущую в lib/remdrvlet/ с таким наполнением:
      • remdrvlet_listener.c: выполняет "основную работу по слушанью": создаёт сокеты, регистрирует их у fdiolib'а, Daemonize()'ируется и запускается.

        Компоненту/функции передаются port_n, cb1 (что вызывать по получению запроса на запуск драйвлета), cb2 (если !=NULL -- то слушать port_n+1 и вызывать по коннекту на него).

      • remdrvlet_srvmain.c: выполняет стандартные main()-действия: setvbuf(), SIGPIPE=SIG_IGN, SIGCHLD=ripper, ParseCommandLine(), и потом вызывает remdrvlet_listener().
      • remdrvlet_hostio.c: заведует drivelet-side-реализацией протокола:
        1. Расшифровывает пакеты от хоста (ей передаётся набор функций, которые надо вызывать по конкретным типам пакетов, ну и плюс private-pointer, описывающий контекст для ссылки на конкретный драйвер).
        2. Отправляет данные хосту, через fdiolib (эти вызовы подстилают remdrvlet-specific-реализацию API cxsd_driver).
    • rrund:
      • Сделан на remdrvlet_listener+remdrvlet_srvmain.
      • А своего -- что по cb1 делает fork()+exec().
    • cangw/canserver состоит из следующих компонентов:
      • Базируется на remdrvlet_listener+remdrvlet_srvmain+remdrvlet_hostio.
      • canserver_drvtable.c: autogenerated by Makefile.
      • canserver_drvmgr.c:
        1. Поддерживает список активных драйверов (как и в CXv2'шной версии).
        2. На основе предыдущего -- wrapper для remdrvlet_hostio.
      • Собственно CAN обеспечивается связкой cankoz_lyr_common.c+c4lcanhal.h.
    • cm5307_drvlet (ex-cm5307_dbody):
      • Использует только remdrvlet_hostio.
      • Кусок типа cm5307_dbody_driver: реализация API cxsd_driver.
      • CAMAC-specific stuff: NAF'ы, LAM'ы, менеджмент SIGUSR1.
      • Наличие некоего своего варианта DEFINE_DRIVER(), с собственным main()'ом.
    • Поддержка мамкинского BIVME -- пока хотя бы для Карпова:
      • Видимо, аналогично cm5307...

    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 НЕ будет поддерживаться часть, касающаяся внутрисерверного взаимодейтсивя драйверов.

  • 21.06.2015: вся инфраструктура заменена на содержимое из v2 -- для скорости, чтоб быстрей сделать, а имевшиеся файлы отправлены в архив, old-4cx-lib_rem.20130802/.

    Нерадостно, конечно -- v4'шная инфраструктура элегантнее, чем её бледное отражение в v2. Возможно, в будущем всё-таки возьмём оставшиеся неиспользованные интересные решения.

  • 28.06.2015@душ/ванна: насчёт layer'ов: а если дать возможность прямо remote-драйверам (в remsrv) указывать и layer тоже -- в синтаксисе E:HOST[@LAYER]?

    08.11.2019: да, это будет полезно со всех сторон:

    • Позволит делать РАЗНЫЕ layer'ы в remsrv-сборках.

      (Сейчас ВСЕМ принудительно подсовывается 1-й (точнее, с lyrid=-1).)

    • Решит проблему от 05-07-2014 -- "где remdrv возьмёт имя layer'а для добычи layerinfo-информации для передачи её перед пакетом CONFIG?": вот прямо оттуда и возьмёт.
  • 30.06.2015: за прошедшую неделю оная архитектура более-менее доведена до собираемости, так что уже собирается canserver (ныне v4c4lcanserver).

    30.06.2015: всех тонкостей уже не помню (СРАЗУ надо было записывать!), но некоторые:

    • В протоколе приняты несколько решений:
      1. Всё выравниваем по границе 8 байт (64 бита) -- в отличие от CX-протокола, где в v4 выравнивание по 16 байт (128 бит).
        • Это прогресс по сравнению с v2 (где было 4 байта (32 бита)), так что rem* будет нормально работать на 64-битных платформах (например, "CAN-серверы" в новой СУ ВЭПП-5 смогут гонять просто canserver'ы, а не cxsd).
        • С другой стороны, считаем, что поддержка 128-битности просто избыточна.

        Оное выравнивание формализовано в inline-функции REMDRV_PROTO_SIZE_CEIL().

      2. (Полу-следствие из предыдущего): заголовок -- remdrv_pkt_header_t -- теперь имеет размер 4 штуки int32, из которых первая пара фиксированная (pktsize,command), а оставшиеся в поле var, которое в любой своей ветви может содержать не более 2 штук int32.
      3. Когда той пары не хватает, остальное вытаскивается в сегмент данных. Для простейших пакетов -- где объём информации фиксированный -- оно описывается в remdrv_data_NNN_t, которое по возможности так же добивается до кратности 8 байтам.

        (Немного похоже на структуру с chunk'ами, только chunk всегда один)

      4. Конкретно пакеты READ, WRITE и DATA используют следущие принципы:
        • Сначала идут in32-свойства -- номера каналов (addrs), nelems, rflags, опционально timestamps (по 3 int32; факт наличия timestamps указывается в hdr.var.group.first, используемом как булевский флаг).

          Всё это -- БЕЗ добивки до 4 байт.

        • Затем dtypes, являющиеся однобайтовыми.
        • Потом -- добивка, от 0 до 7 байт нулей, ОБЩАЯ для всех предыдущих полей/свойств.
        • Потом -- данные, причём после каждого значения идёт добивка до кратности 8 байт (да, на одиночный int32 по факту уходит по 8 байт).
    • Файл remcxsd_driver_v4.c наполнен специфичным для v4 содержимым -- речь об API драйверов (конкретно ReturnDataSet() получилась самой монструозной) и ProcessPacket().
    • Конкретно в инфраструктуру cankoz_lyr.h & Co. были добавлены 3 дополнительных метода в VMT -- чтоб canserver имел доступ к управлению логгингом, отправке 0xFF и свойствам зарегистрированных устройств.
    • Поддержки layer'ов пока толком нет, а просто хаковатые подвязки в c4l-cangw/Makefile и canserver_common.c, помеченные /*!!! LAYER_HACK */.

      А вот ПО-ХОРОШЕМУ, надо делать просто "полную" инфраструктуру layer'ов прямо в librem*, чтобы их могло быть больше одного.

    • Перераспределение обязанностей/кода между файлами: создан remcxsdP.h (по аналогии с cxsd_hwP.h), и в него ушли:
      1. active_devid и ENTER_DRIVER_S()/LEAVE_DRIVER_S() (из remcxsd.h).
      2. Все 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.h добавлена пара extern-деклараций:
      1. remcxsd_layers -- указатель на массив указателей на метрики layer'ов.
      2. remcxsd_numlyrs -- размер оного массива в штуках (ВКЛЮЧАЯ [0]).
    • Генеримый Makefile'ом canserver_common.c теперь также включает таблицу влинкованных layer'ов lyrtable[] (аналогично таблице драйверов), и заполняет remcxsd_layers=lyrtable remcxsd_numlyrs=countof(lyrtable).
    • ЗАМЕЧАНИЯ:
      1. Массив прямо самих МЕТРИК, а не промежуточных "cxsd_hw_lyr_t".
      2. [0]-й элемент массива должен быть NULL, поскольку lyrid==0 означает "нету".
    • В remsrv_main() всем layer'ам вызываются init_mod() и init_lyr().

      Но НЕ драйверам, увы.

    • При инициализации -- в ReadDriverName() (sic!) делается пред-программирование dev->lyrid=-1.
    • GetLayerVMT() один-в-один взята с серверовой.
    • Команды canserver'а (ff, *_log_frame перебирают ВСЕ имеющиеся layer'ы, чтобы исполнить команду у них у всех.
    • Пока НЕ сделаны как надо все *_CHECK_* -- всё оставлено как есть, и в _uniq_checker()'е вызов 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: разборки:

    • Ethereal показывает, что, похоже, дело в лишних debug-пакетах с DRIVERLOG_C_PKTINFO ("in: RCHAN=%-2d..." по DESC_MULTICHAN).
    • Но! Ведь эти DoDriverLog()'и есть и в v2'шных драйверах.
    • Рытьё дало ответ: v4'шный remdrv НЕ отправляет пакета SETDBG, никогда. Вот remcxsd и лупит наверх ВСЮ отладочную информацию, никак её у себя не фильтруя (кроме PKTDUMP, срезаемых на devid=lyrid по disable_log_frame (log_frame_allowed==0)).
    • Делаем: воспроизводим инфраструктуру адаптации к меняющимся loglevel/drvlogmask из v2.
      • cxsd_logger сохраняет переданное ему verbosity в глобальной cxsd_logger_verbosity.
      • В cxsd_driver.h добавлена GetDevLogPrms() -- аналог v2'шной GetCurrentLogSettings().
      • remdrv_drv.c отправляет пакет SETDBG при старте драйвлета (всегда, после CONFIG) и при изменении (в реакции на DEBUGP, как раньше).

        ...не сказать, что изменения отслеживаются красиво -- по-хорошему, надо б иметь драйверов метод "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()'у.

  • 06.01.2017: в remcxsd_driver.c::SetDevState() был косяк при наличии description:
    • Оно отправляло первый пакет (заголовок) с длиной pkt.pktsize вместо sizeof(pkt) -- видимо, по аналогии из других мест был скопирован вызов fdio_send(...).

      Т.е., в качестве "description" слала мусор из стека,

      ...и потом еще отдельно слало саму строку.

    • Строка же -- никак не являясь корректным заголовком пакета -- вызывала ошибку FDIO_R_INPKT2BIG.

    Вылезло вчера при тестировании не-застреливания в CAN-драйверах по неверному devcode. И почему только никогда не вылезало раньше -- загадка.

remdrv_drv:
  • 27.04.2010: файл был начат еще с неделю назад. Всё идет хоть и не столь гладко, как в воображении, но довольно линейно и несложно.
  • 30.04.2010: а ведь remdrv годится и для ЛОКАЛЬНЫХ драйверов, пускаемых отдельными thread'ами или процессами. Т.е., взаимодействие с такими драйверами будет идти через локальный сокет -- socketpair(AF_UNIX,SOCK_STREAM,...) -- по тому же самому протоколу remdrvlet_proto.

    30.04.2010: несколько замечаний:

    • Т.о., вместо коннекта куда-то оно должно сделать fork()+exec(чего-то).
    • Соответственно, в случае неуспешного запуска или закрытия соединения -- нет никакого смысла пытаться повторить запуск (поскольку драйвер-то локальный, а reconnect был сделан исключительно ради независимости от выключений и перезагрузок удалённых контроллеров).
    • Из минусов -- что никакого zero-copy не будет, т.е. -- скорость взаимодействия получится меньше, чем при каком-то pthread-ориентированном механизме.

      Но зато преспокойно можно так юзать "невежливые" драйверы, желающие монопольности (типа 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: неа, не стоит:

    1. Где брать это нечто-для-отфорковывания? Варианты:
      1. Как-то указывать это remdrv'у:
        1. Либо что надо за-dlopen()'ить такой-то модуль, и после fork()'а вызвать из него такой-то символ.

          Но этот вариант вообще НИЧЕМ не лучше нынешнего с fork()+exec() -- т.к. тот модуль всё равно сильно отдЕлен.

        2. Либо сразу символ -- а модуль должен быть заранее подгружен при помощи load-lib.

          Это еще хуже -- поскольку конфигурация аппаратуры будет не полностью описываться devlist'ом, а еще и вылазить в cxsd.conf.

      2. В КАЖДЫЙ такой драйвер подселять коммуникационные "мозги" от remdrv -- так что он одним концом будет уходить в другой процесс, а другим (коммуникационным) -- "жить" в сервере.
    2. Главная проблема -- в fork-копии будет оставаться живой вся прочая инфраструктура сервера. Прибить её -- хбз как, в то время как exec() это сделал бы автоматом.

    Так что -- сразу же "withdrawn".

    14.05.2010: а вот просто попробовать "мясо" remdrv_drv.c вытащить в нечто библиотекообразное -- может, и разумно; например, для тех же отдельно-thread'овых драйверов. Но это если РЕАЛЬНО понадобится -- сейчас незачем, а усложнение от этого неизбежно.

  • 27.07.2010: случайно обнаружил, что НЕ держится состояние "reconnect_tid=-1" при отсутствии таймаута.

    27.07.2010: видимо, когда писал код, то про это тупо забыл. А ведь могло быть чревато кучей последствий. Фиксим:

    1. В _init_d(), при аллокировании privrec'а.
    2. В начале самого timeout-handler'а -- TryToReconnect().
    3. В CleanupEVRT() -- одновременно с DeregisterDevTout().

    На этом считаем потенциальную проблему исчерпанной. Пара замечаний:

    1. От простейшей проблемы -- что при инициализации reconnect_tid=0 -- нас спасло бы то, что RegisterDevTout() НЕ выдаёт tid=0. Да и потом, при обычной работе, скорее всего проблемы бы не возникло -- поскольку всё время под reconnect_tid выдавалось бы одно и то же число. Другое дело, что могла бы возникнуть путаница с будущим heartbeat_tid'ом.
    2. А обнаружилось оное как раз при внедрении heartbeat'а в v2'шные драйверы.
  • 27.07.2010: кстати, надо бы и "heartbeat" из v2 в каком-то виде сюда внедрить.

    04.05.2016: внедрено-то еще 09-07-2015, но выглядит халтурно:

    1. НЕ сохраняется hbt_tid (в отличие от cxlib'а).
    2. При обломе отправки PING-пакета оно забудет пере-заказать таймаут, т.к. сразу делает return.

      ...а "первоначально" заказывается таймаут вызовом самой HeartBeat() из единственной точки -- remdrv_init_d(), поэтому повторно НИКОГДА таймаут не появится (и, аналогично, второй экземпляр никогда не будет зарегистрирован при еще-не-сработавшем первом (как бывало в начале 2000-х -- кажется, с автоповтором у стрелок DIAL'а)).

    Халтура унаследована еще от v2 (скопировано), но это её не извиняет.

    • На 1-й косяк глаза пока закрываем (с учётом редкости использования и профиля, описанного в п.2, де-факто проблем не будет).
    • А 2-й исправляем -- вместо return ставим goto RE_ENQ.

    ...кстати, смутно вспоминается, что вроде были какие-то странности -- типа не восстанавливалась связь с удалёнными контроллерами после их перезагрузки. Не по этой ли как раз причине? И ведь ДОЛЖНА была НЕ восстанавливаться, после 2-й потери!

  • 05.07.2014: давно замечена идеологическая проблема с layerinfo для удалённых драйверов (хбз, где оно было записано): что в момент, когда, например, cankoz-layer'у надо открыть линию, то неизвестно, какой поставить baud_rate -- поскольку
    1. В отличие от в-серверных драйверов, никакого layerinfo в этот момент нет, и мгновенно добыть никак нельзя -- надо слать запрос и ждать ответа.
    2. Да еще и неясно, КОМУ слать запрос -- ведь один remsrv может быть слугой нескольких господ.

    Решение:

    • Надо, чтоб remdrv ПЕРЕД пакетом CONFIG отправлял бы пакет "LAYERINFO", беря его по шаблону ИМЯ_КОНТРОЛЛЕРА_LAYERNAME_N.
    • А уж remcxsd пусть имеет свою "БД", куда складирует эту информацию и отдаёт по 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: за последние несколько дней драйвер добит содержимым до должного быть рабочим состояния -- параллельно с librem*. Теперь надо проверять.

    02.07.2015: небольшие комментарии по состоянию:

    • Больше всего крови попортила реализация endian conversion. Сейчас сделано очень простодушно -- местами вообще по-элементно; безо всякой оптимизации, но просто чтоб работало.
    • Главной проблемой была конверсия ВЕЩЕСТВЕННЫХ чисел. Порывшись в интернетах, сделано через union --
      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;
      }
      
      (с float32 аналогично). Причина таких извращений, вместо очевидного приведения типов указателей -- "Dereferencing type-punned pointer will break strict-aliasing rules".

      Кстати, эти самые rules делают исключение для char-указателей -- предполагается, что через них могут модифицироваться любые другие типы.

    • Новомодные фичи -- heartbeat, повторные коннекты при BUSY -- пока из v2 не портированы.
    • И поддержки layerinfo пока нет.

    03.07.2015: проверяем.

    • После исправления мелких идиотских косячков первый результат: "базовый" функционал (обмен пакетами) работает, но без РЕАЛЬНОЙ поддержки layer'ов -- вообще никак.

      Поэтому "на той стороне" сделан хак, чтобы проверять.

    06.07.2015: далее:

    • После исправления еще нескольких тоже идиотских косяков проверено, что endian-conversion с вещественными числами работает!!!

    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".

  • 19.07.2015: а хорошо бы мочь указывать и ПОРТ -- вместо стандартного 8000.

    Полезно это для отладки вот прям щас, чтобы иметь одновременно запущенными и старый canserver, и v4c4lcanserver (при запуске серверов по очереди всё будет окей -- неиспользуемую CAN-линию оба canserver'а отпускают).

    20.07.2015: вроде сделано -- указывается очевидным образом, ":PORT" после имени хоста.

    21.07.2015: проверил -- был косяк, что оно сразу складывало число в me->port, а privrec-то еще не аллокирован! В остальном после исправления работает.

  • 02.05.2016@вечер-ванна: то, что remdrv ограничен на приём 100 каналами за раз (SEGLEN_MAX=100) -- проблему можно решить, сравнивая sizeof(int)==sizeof(int32), и если "да", то конвертировать номера каналов прямо в пришедшем пакете и ReturnDataSet() с values[] прямо оттуда.

    03.05.2016: ...неа, проблема-то в векторе указателей на возвращаемые значения (vp2ret[]) -- его в пакете разместить точно никак никогда.

    04.05.2016: кстати, в первоначальной идее было забыто:

    • Про nelems[] -- они, как и номера каналов, при совпадении sizeof(int)==sizeof(int32) вполне беримы из пришедшего пакета.
    • А уж rflags-то по определению (typedef) является uint32.
    • Ну и timestamps: с ними проблем было б больше -- структура всё-таки, с потенциально неопределённой раскладкой в памяти.

    В любом случае, даже вопрос вектора указателей на значения уже ставит крест на этой идее, так что "withdrawn".

  • 08.02.2017: на основе remdrv_drv.c сделан скелет еще одного драйвера "для связи по TCP".
    • Драйвер понадобился для работы с усилителями (УМ) производства Триада-ТВ. У них строковый протокол на основе TCP, строки завершаются CR+LF, порт 10001.

      09.02.2017: проверено, что триада-тв'шный девайс работает и с просто LF (т.е., "\n" вместо "\r\n"), но не суть.

    • Драйвер сделан копированием кусков из remdrv_drv.c, с некоторым упрощением (не требуется ни аллокирование в privrec'е места под auxinfo (и парсинг оного тоже -- только hostname), ни отправка какой-то инициализации, ни, естественно, конверсия данных; не говоря уж о fork-варианте). Поэтому подсматривалось также в slbpm_drv.c.
    • Собственно, что в получившемся скелете драйвера есть: просто коннекченье к указанному хосту:порту, ре-коннекты по мере надобности (с логгингом и сменой состояния -- всё-всё из remdrv), heartbeat (отсылкой CR+LF (09.02.2017: а вот это не айс -- при наличии sendqlib слать что-то в обход неё плохо!)).

      Убрана CleanupEVRT() сотоварищи, поскольку подчистку по завершению драйвера прекрасно выполняет сервер.

    • Живёт оно в hw4cx/drivers/eth/, сам драйвер назван triadatv_um_drv.c, в текущем варианте скопирован в "пример" _tcp_string_drv.c, и в будущем еще будет добавлено использование sendqlib.

    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: странный косяк: при "убиении" CANGW'шного драйвера командой kill (в консольном интерфейсе remcxsd) почему-то:
    1. Сей факт никак не отражается в логе (4drivers.log).
    2. НЕ делается попытки рестартовать драйвер.

    30.03.2017: вылезло при попытке понять, что же происходит с linac1:11 -- там почему-то горели болотным все драйверы от rst1, вот я и попробовал сделать kill 1. А он так и остался навсегда болотным.

    Некоторые подробности:

    • Сбор фактов:
      1. Потом был сделан kill еще одному драйверу -- с тем же эффектом.
      2. Имелось подозрение, что, может быть, придуривается контроллер: CHSTAT присылает, а соединение не закрывает.

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

      3. Кстати, remcxsd'шные драйверы корректно завершали свою работу -- с _term_d(): последующая проверка CAN-монитором показала, что устройства (CANADC40) не сыплют пакетами периодических измерений -- т.к. им были посланы DESC_STOP.
    • Отдельное неудобство -- отсутствие утилиты "CX remote console": никак не посмотреть реального состояния устройств с точки зрения сервера. Точнее, можно -- почитать значения каналов ._devstate и ._devstate_description (они бы помогли, но я слишком поздно о них подумал); но не шибко-то это удобно.
    • Анализ кода:
      1. Сначала (до п.2 из "Фактов") была мысль, что проблема на remsrv'шной стороне -- ведь там есть "отложенное освобождение", по being_processed (с взведением being_destroyed).

        Но нет -- там на вид всё окей.

      2. А потом выяснилась реальная причина:
        • remdrv_drv.c::ProcessInData() по получении CHSTAT просто исполняет указанное в нём -- т.е., делает SetDevState(,DEVSTATE_OFFLINE,...) (так что последующее закрытие соединения -- FDIO_R_CLOSED -- ловить будет уже некому).
        • Исключение только для особой ситуации с rflags==CXRF_CFG_PROBL и проверкой на количество попыток и время.
        • Во всех остальных случах -- оно просто застреливается, МОЛЧА и БЕЗ попыток реконнекта.
    • Выводы:
      1. Всё работало "правильно" -- так, как и было запланировано. Но неудобно, да.
      2. Логгинг должен делаться в обработке команды "kill". Как он делается во всех драйверах -- ПЕРЕД вызовом SetDevState().
      3. Также стоит добавить отдельную команду "close" -- для просто закрытия соединения, БЕЗ SetDevState(), чтоб соединение грохалось индивидуально для драйвера, а не путём убиения всего содержимого remote-сервера.
    • Исправление:
      1. Добавлено логгирование строки "kill command from remsrv console".
      2. Реакция на "kill" расширена поддержкой "close". Кусок кода тот же (парсинг команды и т.д.), но действие отличается -- FreeDevID(), что на стороне remdrv будет видно как просто закрытие соединения.

    Теперь проверять (когда?).

  • 16.10.2018@вечер-дома: возникает желание вытащить из remdrv_drv.c логику коннектов/реконнектов в отдельную библиотеку, доступную для использования и другими драйверами, с иными протоколами. А в нём оставить лишь remdrv-specific код.

    Конкретный побудительный мотив -- предстоящая надобность запинывания драйверов для LXI (см. его раздел в "Знания" за сегодня) и MQTT, где поток текстовый (так что можно использовать ровно то же, лишь в варианте FDIO_STRING).

    Плюс, уже энное время назад делался triadatv_um_drv.c как раз таким вытаскиванием и переделкой на STRING.

    Надо этот текст утащить уже в секцию новой библиотеки, здесь оставив лишь краткое упоминание, что "вытащено в NNN, и сам remdrv_drv.c переведён на неё".

    17.10.2018@утро-дома: некоторые предполагаемые аспекты внутренней реализации:

    • Понадобится какой-нибудь "контекст", содержащий всю внутреннюю информацию библиотеки.
    • Вчера ещё было предположено, что оно должно поддерживать работу как с fdiolib, так и напрямую с cxscheduler (сейчас запросов на оную нет, но мало ли).

      Для чего в контексте иметь поля как handle, так и fdh, присваивая полю неиспользуемого варианта -1.

      А какой вариант использовать -- клиент указывает при инициализации.

    • Выбор названия для библиотеки -- тяжкий вопрос. libremdrv? libremdev? Как-то может участвовать слово "stream", но чтоб не дойти до "streamdevice" :). libtcpdev?
    • Вечером: в раздельчике по triadatv_um_drv.c предлагается также сюда же включить хотя бы рудиментарную поддержку sendqlib'а -- чтоб знал об очереди и необходимости её sq_clear().
  • 26.11.2018: по просьбе юзеров HEARTBEAT_PERIOD сброшен с 300 секунд (5 минут) до 60 секунд (1 минута).

    Так период пинга будет плюс-минус равен времени перезагрузки мамкинских контроллеров после reset или дёрганья питания.

    (Конкретной причиной стало то, что сервер долго не обнаруживал рестарта CM5307 крейта 3-го клистрона (который пришлось дёрнуть из-за странностей после броска питания). Причём Федя умудрился рестартовать сервер уже ПОСЛЕ обнаружения им потери связи с драйверами, но еще ДО её восстановления -- притом, что интервал там всего 10 секунд; но Федя в него попал.)

    Возможно, этот интервал стоило бы сделать настраиваемым per-device -- устройства-то все разные. Но пока ограничимся 1 минутой.

  • 18.12.2022: задолбало, что для remdrv-устройств, которые недоступны по сети (например, крейт выключен) постоянно обновляется ._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 это также добавлено.

remdrv_proto.h:
  • 27.04.2010: файл был начат также с неделю назад. За основу взят старый cm5307_drvlet_proto.h.
  • 27.04.2010: есть мыслишка -- а ведь можно избавиться от необходимости указывать для remdrv в конфиге endianness контроллера: достаточно ввести "договаривание", аналогично cx_proto.

    27.04.2010: например -- чтобы пакет CONFIG слался не сразу, а так: драйвлет при запуске отправляет первый пакет, содержащий лишь команду "ENDIANNESS", endianness-fingerprint и версию протокола (так что бремя проверки совместимости версий будет на remdrv).

    07.05.2010: только одна маленькая проблема: первым ответным пакетом может оказаться пакет RRUNDP, а у него предусмотрено сообщение (т.е. -- данные ПОСЛЕ заголовка); да и вообще, в протоколе remdrv в заголовке указывается ПОЛНЫЙ размер пакета, БЕЗ вычета размера заголовка (хотя это-то можно и поменять, аналогично cx_proto).

  • 22.06.2015: файл переименован в remdrv_proto_v4.h.
  • 24.03.2016: сейчас, во время "переходного периода" с v2 на v4, по факту ВСЕ v4'шные работают на порте 8002 вместо 8000 (см. предыдущий раздел за конец 07-2015 -- для этого и делали возможность указания порта).

    А что б вообще v4'шный не перевести на 8002?

    24.03.2016: да, сделано -- теперь REMDRV_DEFAULT_PORT = 8002.

libremdrvlet:
  • 27.04.2010: поскольку разделение обязанностей между модулями remdrvlet_*.c может меняться, то отдельных секций для них не создаём, а держим всё здесь.
  • 28.04.2010: добавлен новый модуль remdrvlet_utils.c -- для всяких изолированных функций. Сейчас в нём -- remdrvlet_report() (отправка диагностического сообщения хосту) и remdrvlet_debug() (диагностический вывод на stderr).
  • 29.04.2010: для того, чтобы remdrv мог передавать контроллерам просто ИМЯ-ТИПА-УСТРОЙСТВА, надо, чтобы rrund получал из командной строки префикс "./cm5307_" и суффикс ".drvlet". Для этого remdrvlet_srvmain() должен добывать их из командной строки и публиковать в каких-нить переменных (ли отдавать через аксессоры).

    И, соответственно, порт должен указываться в ключике -p.

    29.04.2010: да, сделано, и rrund к этому адаптирован.

  • 30.04.2010: рождаем новый модуль -- remdrvlet_oslike.c, реализующий OS-like часть API cxsd_driver (таймауты, дескрипторы, fdio), аналогично тому, как remdrvlet_hostio.c реализует часть, ответственную за данные.

    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.

  • 21.06.2015: напоминание, что оно (вместе со ВСЕЙ инфраструктурой) заменено на вариант от v2.
libremsrv:
  • 29.04.2010: создаём эту библиотечку. Её задачей будет то, что в CXv2 было работой canserver'а:
    1. libremsrv_drvmgr.c -- менеджмент драйверов (в lib/srv это делает связка cxsd_hw+cxsd_module).
    2. libremsrv_driver.c -- реализация API cxsd_driver в удалённом варианте.
  • 29.04.2010: та часть driver-API, что не касается данных, а является прослойкой/адаптером к cxscheduler+fdiolib, явно должна бы как-то разделяться с не-libremsrv-вещами. Точнее -- вообще браться оттуда, а тут -- только прослоечка (аналогичная прослойке к remdrvlet_hostio).

    01.07.2013: не будет за ненадобностью вследствие uniq.

  • 21.06.2015: напоминание, что оно (вместе со ВСЕЙ инфраструктурой) заменено на вариант от v2.
  • 08.11.2019: позавчера (см. раздел "Общие вопросы" по VME) возникла идея, что на современных платформах remsrv вполне может динамически грузить драйверы, поскольку там доступен dlopen().

    Надо это серьёзно обдумывать, чтобы понять, как/где лучше такое реализовать.

    08.11.2019: смотрим remsrv_drvmgr.c.

    • Само действие по загрузке:
      • Вся работа там выполняется в ReadDriverName().
      • И вот в нём ближе к концу и делается поиск по имени драйвера в таблице remsrv_drivers[].
      • Очевидно, что туда же и надо добавлять попытку загрузки драйвера; точнее --
        1. Попытку поиска драйвера в отдельной таблице ("загруженных").
        2. Если и там не нашлось, то попытку загрузки.
        3. И если успешно загружен -- то повторный поиск в той таблице.
        4. ...и не забыть инкремент reference-count'а.
      • 11.11.2019: а ещё при загрузке надо бы проверять соответствие layer'ов -- указанного в драйвере (при наличии) с тем, что есть в remcxsd_layers[].
      • 12.11.2019: этак мы и до динамической загрузки layer'ов в remsrv допрыгаемся :D
    • Декремент reference-count'а и выгрузка при ==0 -- очевидным образом, во FreeDevID().
    • И, очевидно, в remcxsd_dev_t понадобится добавить какие-то поля для указания на "источник" драйвера -- встроенный или динамически-загруженный, и если последнее, то номер в таблице загруженных.

      ...хотя -- можно эти два поля совместить в одно: если ==0 -- встроенный, >0 -- загруженный, и тогда это индекс в той таблице (в которой в таком случае 0-й элемент никогда не используется).

    • А вот как сохранять и потом использовать prefix/suffix -- надо ещё разбираться...

      09.11.2019@дома-ванна: да как-как -- просто! Ровно так же, как и в rrund.c -- сохранять ссылки на argv[] в argv_params[]. (Парсинг argv[] -- отдельный квест, учитывая возможность указывать порт; но тоже не мега-проблема.)

    27.11.2019: приступаем (с горя, "от нечего делать" -- после обнаружения бесконечных косяков в работе BIVME2, чтобы отдохнуть и сделать хоть что-то успешно работающее):

    • В remcxsd_dev_t добавлено поле dlref.
    • В начало remcxsd_drvmgr.h добавлена проверка определённости 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(){}.

      • Общая схема: работа выполняется в "цикле в 2 стадии, 0 и 1".

        Начинается цикл с поиска по dltable[], а если не найдено и stage==0 -- то выполняется попытка загрузки.

        По сути, такая организация имеет смысл

        ПОИСК_СРЕДИ_ЗАГРУЖЕННЫХ();
        ПОПЫТКА_ЗАГРУЗКИ();
        ПОИСК_СРЕДИ_ЗАГРУЖЕННЫХ();
        
        но всё вместе, БЕЗ вытаскивания поиска в отдельную функцию.
      • Собственно загрузка и поиск символа сделана по образу и подобию cxldr.c.

    Проверено, что файл компилируется и с MAY_USE_DLOPEN=1, и с =0.

    28.11.2019: продолжаем.

    • Для начала -- оказалось, что с MAY_USE_DLOPEN=1 не собирается v4c4lcanserver, т.к. там нету libdl.

      А её и в x-compile/*/ нет...

      Так что --

      1. Файлы libdl* добавлены в ppc860-linux/tools/lib/ и в arm-linux_1.3/lib/.
      2. Для всех *server'ов в их Makefile'ах добавлена строчка 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().

        Причём при ошибке драйвер выгружается.

      • Добавлена куча проверок на совместимость загруженного модуля:
        1. По magicnumber.
        2. По версии с текущим CXSD_DRIVER_MODREC_VERSION.
        3. По layer'у -- наличие, совпадение api_name, соответствие по версии.

        И вот эта толпа проверок -- особенно по layer'у -- имеет просто гигантский объём.

    29.11.2019: первая попытка проверки. Делается путём cd в директорию с v4bivme2vmeserver'ом, куда также скомпилирован тривиальнейший драйвер zzz_drv.so.

    • Временный-халтурный вариант с prefix="" не сработал: dlopen() в таком случае НЕ пытается брать файл из текущей директории, а сразу выполняет поиск по $LD_LIBRARY_PATH -- это прямо в man'е написано.
    • Так что пока захардкожено "./" -- это помогло: драйвер грузится!
    • Следующий тест -- пробуем указать какой-нибудь layer.

      А вот тут уже облом: SIGSEGV. Надо разбираться.

    02.12.2019: продолжаем, разбираемся.

    • Для начала -- неправильно делалось получение layerrec (ссылки на layer'ов modrec): стояло
      lyrrec = remcxsd_layers + -dev->lyrid
      вместо
      lyrrec = remcxsd_layers[-dev->lyrid]

      А поскольку remcxsd_layers -- это

      static CxsdLayerModRec * lyrtable[] = {
          NULL,
          &bivme2vme_layer_modrec,
      };
      CxsdLayerModRec   **remcxsd_layers = lyrtable;
      
      т.е., массив указателей на указатели, то ошибочный вариант давал не указатель на modrec, а указатель на указатель на него, вот и получалась фигня.

      Это исправлено, и SIGSEGV стал появляться дальше :D

    • А вот "дальше" маялся в попытках найти причину очень долго. Вот падает -- и всё тут. Нашёл точку -- DOUBLE_REPORT(). Поставил отладочную печать ДО этого места -- всё окей. После -- падает; НЕПОСРЕДСТВЕННО "до" -- тоже падает...

      В конце концов допёрло: пачать делается ПОСЛЕ dlclose(); т.е., тогда, когда сам модуль драйвера уже отмаппирован из памяти -- вот и падает из-за обращения к уже отсутствующей странице!

      Заковыристый баг, блин...

      Решение очевидно: переставил ВСЕ вызовы DOUBLE_REPORT() в начала if(){}-секций, чтобы были РАНЬШЕ вызовов dlclose().

    • ПОСЛЕ ЭТОГО -- ВРОДЕ ВСЁ РАБОТАЕТ!!!
    • Попробовал ДВА vadc16 на одной линии прерывания: 0,5,50 и 2,5,52. Результат:
      1. Второго прерывания НИКОГДА не видно.
      2. Зато с дикой скоростью летит уведомление о "первом".
      3. А по Ctrl+C cxsd-серверу v4bivme2vmeserver падает по SIGSEGV'у.

    Дальше надо делать парсинг префикса/суффикса из командной строки.

    02.12.2019@сидя-на-лекции-семинаре-Бондаря "О спектроскопии тяжёлого кваркония": надо парсинг делать по-простому -- "независимо": если удалось argv[argn] интерпретировать как число, то считать его указанием порта, а иначе (если pidx<2) -- как указание префикса/суффикса.

    03.12.2019@вечер-дома: делаем -- для начала ИСПОЛЬЗОВАНИЕ параметров. Подсматриваем в rrund.c.

    • Сами argv_params[2]. ТОЛЬКО при MAY_USE_DLOPEN!=0.
    • Локальные prefix, suffix.
    • Умолчательные значения -- если в командной строке не указаны -- ставятся не "", а "./" и "_drv.so" соответственно. Т.к. иначе работать не будет.
    • Формирование имени: используется тот же принцип, что в rrund: если имя начинается с '/, то используется "как есть", и лишь иначе дописываются префикс и суффикс.

    04.12.2019: а теперь собственно парсинг из командной строки.

    • Подсмотрено было опять же в rrund.c, ...
    • ...но с местной спецификой -- потребовалась проверка для определения "ЧТО ЭТО?", порт или префикс/суффикс.
    • Проверка делается с помощью only_digits(): если в argv[argn] только цифры, то пытаемся интерпретировать это как номер порта, а иначе -- как argv_params[].
    • И из-за возможности компиляции с MAY_USE_DLOPEN==0 там получился очень замудрённый многоуровневый блок if()'ов/#if'ов.
    • В частности, ругательство "bad port spec" унесено в конец функции и туда делается 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".

remcxsd:
  • 05.12.2019: создаём раздел для "общей части libremdrvlet и libremcxsd"; сейчас это по факту remcxsd_utils.c и, главное, remcxsd_driver.c -- вот ради последнего и делаем.
  • 05.12.2019: вылезла какая-то странная проблема, вызванная, похоже, поведением remcxsd_driver.c.

    Суть: при испытании пары драйверов vadc16 внутри v4bivme2vmeserver после делания Ctrl+C cxsd-заказчику v4bivme2vmeserver падает по SIGSEGV.

    • Разбирательство в рамках remsrv_drvmgr.c показало, что причина, похоже, -- в ДВОЙНОМ вызове FreeDevID(1).
    • Т.е., сначала делается FreeDevID(), потом повтроно.
    • В нём-то есть защита: что при обнулённом in_use просто ничего не делается. Так что падение -- где-то в другом месте, в вызывальщике.
    • А кто вызывальщик? Только что-то из remcxsd_driver'а.

    05.12.2019: разбираемся:

    • @утро-дома: краткий просмотр текста показал, что вызовов FreeDevID() там дофига.

      Видимо, надо ставить отладочную печать перед ними всеми?

    • @дорога-на-работу-около-НИПС, под аркой (шёл пешком: отвёл машину, но забыл дома пропуск и потому пришлось пешком туда-обратно): а можно воспользоваться фичей C-препроцессора, что изнутри макроса его имя перестаёт быть макросом и передаётся препроцессором компилятору как есть. Т.е.,
      • Определить в начале файла (но после #include "remcxsd.h") макрос с именем FreeDevID().
      • В нём -- вначале диагностическая печать, с привлечением __LINE__, ...
      • ... а затем вызов настоящего FreeDevID().

      Таким образом мы избегаем необходимости расставлять отладочную печать в каждой точке, поскольку каждый "вызов" автоматически дополняется печатью.

      Кстати, даже если бы этой фичи не-макрорасширения-изнутри-тела-макроса и не было, то можно б было добиться того же эффекта при помощи дополнительной функции: сначала определяем локальную helper-функцию с другим именем, вызывающую FreeDevID(), а далее макрос, вызывающий эту helper-функцию. При этом в точке определения той функции имя будет ещё не переопределённым.

    • Да, так и сделал:
      #define FreeDevID(devid) \
          do { \
              fprintf(stderr, "%s::%d %s(): About to FreeDevID(%d)\n", \
                      __FILE__, __LINE__, __FUNCTION__, devid); \
              FreeDevID(devid); \
          } while (0)
      
      (в файле оно в 1 строку, а разбито с бэкслэшеньем тут для наглядности)
    • Результат: оказывается, повторный вызов делается из ReturnDataSet()!!!
    • ...но главная проблема -- в конкретном месте в ней: в конце, за меткой TERMINATE.

      А вот кто вызвал -- как бы понять?

    • В любом случае вопрос: почему этот эффект с SIGSEGV проявляется ТОЛЬКО на vadc16? Да ещё и на паре?

      Не на паре тестовых zzz, не на паре adc250...

    • А дальше, когда стал пытаться найти происхождение -- полный мрак: в конце ReturnDataSet() (куда делается goto) -- сообщение печатается, а вот в начале -- через которое исполнение вроде бы должно было пройти -- нет!

      Как такое вообще может быть?

      • Может, глюк из-за оптимизации?

        Окей -- пересобрал всё с -O0. НЕ помогло.

        Это даже и радостно -- значит, косяк достаточно устойчивый, меньше будет сложностей с его расследованием.

      • Может, какой-то очень хитрый сбой -- что почему-то происходит переход внутрь функции?

        Тут помог бы gdb...

      • Авотхрен! Откопанный в powerpc-sdk-linux-v4.tgz экземпляр -- ppc860/linux/initrd_big/usr/bin/gdb -- при запуске попросту завешивает контроллер намертво.
    • @лыжи-~14:00...14:30, первая 2-ка: некоторые соображения -- что можно попробовать.
      • Экстенсивный путь -- очень трудозатратный: напихать отладочных печатей повсюду -- ведь откуда-то же должно было управление туда придти? Вот рано или поздно и найдём место. Пихать во все "потенциальные точки входа" (правда, вроде бы обе "точки" -- обработчик файлового ввода и обработчик прерываний)
      • Вылет ведь всегда на devid=1, так? А если попробовать не Ctrl+C cxsd-заказчику, а _devstate=-1 индивидуальным устройствам, включая второе?
      • ...а в идеале -- иметь бы экземпляр GDB, собранный под x86, но с поддержкой PowerPC...

        Спросить у Павленко?

        @вечер-ванна: однако - а ведь таковой экземпляр может быть в комплекте powerpc-sdk-linux-v4.tgz! Проверим?
        @вечер-дома: ну проверил -- да, есть, ppc860/linux/tools/bin/ppc-linux-gdb; для запускабельности пришлось доставить в систему ncurses-libs-5.9-13.20130511.el7.i686.rpm.

        Но толку от него чуть: хоть он и пишет, что

        This GDB was configured as "--host=i386-redhat-linux --target=ppc-linux".
        
        но при попытке натравить его на core-файл (для проверки был взят rrund и kill-SEGV'нут после предварительного "ulimit -c 100000") выдал
        GDB can't read core files on this machine.
        

        Ну и нафиг он такой нужен?! :-( (типа для удалённой отладки; вот спасибо!)

        Так что по-прежнему надо спросить у Павленко.

    • Пробуем "хитрые закрытия" при помощи _devstate=-1:
      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  
      
      • Только 1-й -- падает.
      • Только 2-й -- НЕ падает.
      • 2-й, а затем 1-й -- тоже НЕ падает.
      • ...затем, после оживления обоих (_devstate=+1) 1-й -- падает.
      • ...после оживления в обратном порядке: по закрытию 1-го (который теперь devid=2) -- падает, а по закрытию 2-го (который теперь devid=1) -- НЕ падает!

        И что -- делать вывод, что падения как-то привязаны к VME-ресурсам?!

      • Ну да: если поменять в devlist'е устройства местами, то НЕ падает даже по Ctrl+C!

        Зато по a0._devstate=-1 -- падает, хотя у него теперь devid=2!!!

        Ну да, явно VME-ресурсы в деле...

      • О -- а достаточно даже ОДНОГО a0 -- падает!!!
      • И падает ТОЛЬКО при указании IRQ=5.

        Что намекает на возможное участие ADC250, у которых как раз только эта линия.

        ...а у одного VADC16 горел красный светодиод.

      Ну-с, найдено "наименьшее множество", и что теперь делать, как искать проблему?

      Авотхрен!!! Потому что

      • ...малёк потасовал параметры, чтобы поставить IRQ=5 второму девайсу -- не падает; вернул обратно -- опять не падает!!! Блин...

      Ну и что ТЕПЕРЬ делать? Разве что попробовать дёрнуть питание...

    06.12.2019: ан нет -- опять стало падать!

    Чуть позже: точнее, то падает стабильно, то стабильно НЕ падает, то один прогон заказчика-cxsd с Ctrl+C не падает, а следующий прогон (с тем же экземпляром v4bivme2vmeserver'а!) -- падает.

    06.12.2019: проведёна большая серия тестов совместно с Антоном Павленко. Модуль переставлялся на "выноску", плюс Антон смотрел сигналы осциллографом. Результаты:

    • Насчёт VADC16: реализация daisy-chain-передачи сигналов сброса прерываний в нём сделана некорректно.

      Так что до второго (дальнего от контроллера) модуля этот сигнал сброса попросту не доходит.

      Но: если в конкретный момент конкретный VADC16 НЕ запрограммирован на измерения и генерацию прерывания по окончанию цикла, то он эту линию не трогает вовсе, и вот ТОГДА сигнал до дальнего модуля доходит.

      Почему получался SIGSEGV -- т.е., почему постоянно взведённая линия IRQ5 вызывала такой эффект со сдуреванием v4bivme2vmeserver'а -- хбз. Но при заведомо некорректной работе VADC16 задача разбирательства в ситуации становится несколько сомнительной. Весь комплект "потроганных" (загрязнённых лишней отладочной печатью) файлов сохранён в w20191209-1717-vadc16-debug.tar.gz.

    • Насчёт ADC250 и прерываний: в регистре INT_STATUS (0x0014) у "не-сгенерившего" прерывание блока всё-таки есть.

      Так что, похоже, проблема именно в некорректно работающем мамкинском драйвере vmei, который не буферизует вектора и теряет их.

      Возможный рецепт решения -- запинать-таки свой вариант, с буферизацией векторов.

    • Насчёт ADC250 и постоянных запусков: похоже, что при включении стоит режим триггера "Internal" (=1), что означает, что по записи в регистр CTRL бита START -- что и делается в StartMeasurements() -- цикл измерений будет запущен немедленно.

      Надо бы всё-таки проверить, отличается ли в этом смысле поведение "новых" блоков от "старого".

rrund:
  • 28.04.2010: изготовлен, по позавчерашнему проекту, поселён покамест в programs/server/. Получился такой малю-ю-юсенький!
  • 21.06.2015: напоминание, что оно (вместе со ВСЕЙ инфраструктурой) заменено на вариант от v2.
cm5307_drvlet:
canserver:
  • 28.04.2010: также зачат. Отличие от проекта -- в том, что "собственное" содержимое (клиент libremdrvlet плюс "canserver_drvmgr") всё собрано в одном файле -- canserver.c.
:
sw4cx/drivers/:
  • 27.11.2017: давно назрело создание своего раздела для драйверов из sw4cx/ -- у них есть свои проблемы, отдельные от общих vdev/can/..., требующие отдельного обсуждения, чтоб хоть как-то внятно можно было проследить их развитие.
iset_walker_drv:
  • 06.06.2024: драйвер-то начат ещё 09-02-2017, но описание так и велось в разделе по vdev. Давно пора перенести в собственный раздел. Который -- чисто по дате начала работы над драйвером -- становится первым в своём над-разделе.
  • 09.02.2017: потребность общего плана для драйверов источников: для размагничивания (и тому подобных фортелей) нужно уметь последовательно пройти по некоему заданному спику точек, с той скоростью, какая задана для данного источника.
    • Касаться может как магнитов, так и корректоров (у которых драйверов нет). Но в основном пока вроде только источники.
    • Поправка о скорости хождения: полезно уметь это делать не просто линейно, а "плавно", как предусмотрено сегодняшним проектом "плавно разгоняемся, идём, в конце плавно тормозим".

    09.02.2017: предварительные соображения:

    • Идея: сделать эту функциональность отдельным драйвером, который можно натравить на любую сущность, имеющую каналы Iset и Iset_cur.
    • Каналы: собственно список точек, старт, стоп, номер текущей точки.
    • Нюанс: надо будет сделать так, чтобы каналы этого драйвера попадали бы в "то же пространство имён", что и сам источник.

      Но это уже явно работа для макросов в devlist_magx_macros.m4; тем более, что всё равно коэффициенты проставлять.

    Драйверочек iset_walker_drv.c написан, теперь проверять.

    09.02.2017@вечер-дорога-домой-около-ВЦ: а насколько удобно будет такой 2-шаговый подход -- сначала указывать список, потом говорить старт?

    Не будет ли удобнее как с обычной записью значений -- прямо по записи списка значений и начинать его исполнение?

    10.02.2017@вечер: а ведь можно бы всё-таки этот код подселить прямо в драйверы источников.

    • Подселится он туда очень хорошо: там есть вся инфраструктура, просто надо будет добавить еще пару-тройку состояний.
    • Конкретно "STOPPED" не нужно, а надо, чтобы были состояния
      1. "Инициализировать хождение": IsAlwd будет проверять, можно ли (состояние IS_ON), а SwchTo -- собственно инициализировать. Следующим шагом указано "заканчивание".
      2. "Заканчивание": important-channel'ом указан Iset_cur; IsAlwd проверяет, что _cur подошел ближе 305uV к текущей цели, и если да, то переходит к следующему значению, а если значение последнее -- то возвращает 1 ("можно перейти к следующему шагу"). Следующий шаг -- "остановка".
      3. "Остановка": IsAlwd проверяет, что текущим состоянием является одно из двух предыдуших; SwchTo сбрасывает cur_step:=-1; следующий шаг -- IS_ON.
    • В имеющийся код драйверов надо будет добавить упоминаний этих состояний (в тех местах, где проверяется возможность перехода -- в _sodc_cb() и в IsAlwd'ах).
    • И, воизбежание дублирования кода, "мозги" поселить в некий общий .h-файл с макросами, генерящими методы состояний и строки таблицы описания состояний.
    • 14.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: движемся к внедрению.

    • Добавлена поддержка в devlist_magx_macros.m4: макрос MAGX_WALKER_SUBDEV, который можно добавлять (в конец) любого из макросов MAGX_nnn_DEV_LINE().
    • Добавлено во все источниковы (v3h, v1k, ist, mps20/mps25).
    • На linac1 и ring1 подготовлено к использованию.
    • Кстати, конкретно сервер магнитной системы линака (devlist-linac1-11.lst) теперь потребляет 159 файловых дескрипторов; и это без клиентов!

    05.10.2018: в iset_walker_drv.c зашито ОЧЕНЬ много специфики наших KOZAK-driven-источников:

    • И диапазон [-10000305,9999999]uV.
    • И квант 305uV. ...хотя он уже сделан параметризуемым, опциональным параметром в auxinfo.
    • А главное -- работа в целых числах, вследствие чего он НЕпригоден к натравливанию на каналы, работающие в double, вроде основанных на double_iset_drv либо formula_drv.

      29.06.2021: а какого чёрта, собственно, double_iset у нас именно DOUBLE? (Да, понятно, что это типа "максимально общее решение", могущее работать с совсем разными источниками.) Но если источники ОДНОТИПНЫЕ, то у них одинаковые R, и тогда можно работать прямо в int32 -- просто разделять значение на 2 получателя. Вопрос будет только в корректном указании этого R для "двойного" канала, чтоб совпадало с источниковыми. Но, конечно, сейчас вряд ли надо это делать -- уже просто незачем.

    Что делать? Вводить еще какой-нить способ указать в auxinfo, что наша цель -- double-канал, и надо обращаться с ним соответственно? Это-то несложно, но...

    ...а как определять, что значение приблизилось к заказанному? Для double'ов-то квант -- это нечто условное. И raw воспользоваться не удастся -- у изначально-double-каналов оно тоже double.

    29.06.2021: ну как-как -- auxinfo-параметром указывать, "tolerance такой-то": если текущее значение отличается от целевого менее, чем столько-то, то считать, что дошли.

  • 24.06.2021: наблюдена некоторая странность: почему-то иногда walker'ы НЕ считают последовательность завершённой, хотя значение целевого канала дошло уже до значения в последнем шаге.
    • Случается такое крайне редко: вот сейчас, а предыдущий раз чуть ли не полгода назад. Замечено было оператором, что ЕманоФедина DDM вдруг остановилась на 33% и дальше не идёт.

      Помог рестарт драйверов -- запись им ._devstate=0.

      29.06.2021@пешком-от-родителей, ~16:00, уже между Пирогова 22 и 26: а сейчас до меня дошло, что надо было попробовать сделать STOP=1,START=1 -- вдруг бы помогло... Надо будет в следующий "залип" проверить.

    • Обстоятельства:
      • Федина DDM прогоняет walker'ом через размагничивание ТРИ источника: d3m4n5, d5m1t4, d6m1t4.
      • "Зависли" два из них -- d5m1t4 и d6m1t4.
      • И прикол в том, что ОБА они -- реверсивные.
    • Я проверил -- да, шаг числился последний (1-й, из списка в [2]), а значение целевого канала Iset_cur было равным [1]-му элементу списка.

      Но, тем не менее, оно "считалось недошедшим" -- walker_state=2 (WLK_STATE_WALKING).

    • По всему выглядело так, словно последнее значение Iset_cur просто не дошло до walker'а.

    29.06.2021: добавляем отладочности:

    • cdac20_drv собран с DO_RETURN_CUR_PARAMS=1, так что текущие значения CURSPD и CURACC (а у CDAC20 всего ОДИН ЦАП-канал) будут отдаваться в каналах .198 и .199 (KOZDEV_CHAN_ADC_n_base +98 и +99).
    • В сам iset_walker_drv.c добавлен логгинг -- DEFAULT|NOTICE -- всех ключевых точек: START, STOP, окончание шага (когда значение стало достаточно близко к целевому).
    • @~16:30: добавлен возврат последнего известного значения ISET_CUR'а в канале STOP.

      Идея пришла @~14:20 около стадиона НГУ по дороге к родителям:

      • Это такое решение проблемы "как бы всё-таки публиковать в каком-нибудь канале текущие последние полученные значения, но при этом не трогать карту каналов?".
      • Суть идеи в том, что
        1. Значение каналов START и STOP по протоколу CX_VALUE_DISABLED_MASK всё равно никем не используется.
        2. В процессе хождения этот протокол молчит: 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) могут перезагрузиться не все.

    • Просмотр логов определённости не дал, но вызвал подозрения, что кто-то не перезагрузился (или просто не прислал 0xFF) и потому драйвер не в курсе.
    • Анализ кода iset_walker_drv.c показал, что вроде там есть корректная "реакция" на события -- указываются 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).
    • "Проблема" началась в районе 13:10, а раньше вроде как работало; расследование я начал в районе 13:54, осознали причину косяка в районе 14:10.

      "Симптомы" "проблемы" были в том, что некие пару источников ЕманоФедин ddm гонял перемагничиваться, как будто бы "не замечая" окончания, и при каждом следующем переключении режима опять заново, БЕЗ "выстрела" (выпуска?).

      Почему и казалось, что "не замечает".

      Конкретно гонялись d5m1t4_walker и d6m1t4_walker, по траектории в 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
      
      (логи от 2 штук перемешаны, т.к. идут в параллель).
    • НО! ЕманоФедя раскричался, что там вовсе НЕ ТОЛЬКО 2 источника, а в разных случаях их может быть больше (при разных типах переходов ddm'ом задействуются разные наборы источников).
    • После чего Виталя Балакин -- который и был сегодня дежурным и инициировал сегодняшние разбирательства -- осознал, что какой-то из источников работать не может, т.к. он физически заблокирован то ли каким-то рубильником, то ли открытой дверью.

      И после исправления той блокировки -- оно всё заработало!

    • И ещё немного разбирательства: "заработало" в логах выглядело как
      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
      
      -- т.е., только отработка, БЕЗ инициирования "WALK [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: опять просадка и опять проблемы.

    • Как сразу сказал дежурный, просадка произошла во время перемагничивания walker'ом, что и подтвердилось в логах: d6m1t4_walker успел запуститься и отработать 1-й шаг до просадки, а буквально через 5 секунд она случилась.
    • DDM так же замирал "на 60%".
    • Взгляд в логи показал, что на линии IP1IST пакет 0xFF прислали лишь 2 устройства -- 33 и 26 (icd_d3 и icd_sm).
    • Остальные -- нет, но при этом они вроде как считались "работающими": драйверы их не пытались 0xFF-пинговать, значит, измерения АЦП присылались (в т.ч. и от проблемного icd_i14:44); быстрая проверка sktcanmon'ом показала, что да -- летят измерения (но жаль, что не проверял 44-й до того, как послать ему k44/u:0xFF).
    • Собственно проблема была, похоже, именно с 44-м: у него были НУЛИ во входном регистре, но во выходном битик 1:ENABLE горел.
    • Такое впечатление, что в результате просадки как минимум icd_i14 слегка "сошёл с ума".

    03.06.2024: провели тест посредством быстрого дёрганья рубильника питания на морде корзины "ИСТ-14" (это как раз 6m1t4). Фиг -- устройство честно присылает пакет 0xFF,"PowerOn". Похоже, ёмкости платочки "питание собственных нужд" всё-таки не хватает.

    А дёргать рубильник, через который запитан этот ИСТ, при подеваемом от энергоцентра питании -- запрещено, т.к. в этом рубильнике может вспыхнуть дуга.

    Но всё же остаётся вопрос: ЧТО отключает рубильник на морде корзины -- ВЫХОД из корзинного БП (тогда эксперимент нерелевантен, т.к. ёмкость может быть именно в этом БП) или ВХОД (тогда эксперимент показателен и фиг знает, что же произошло 30-05-2024)?

    Чернякина спросить не так просто -- он во временом увольнении ради индексации пенсии.

    04.06.2024: продолжаем расспросы.

    • @после вторничного лабораторного круглого стола: спросил Фролова "рубильник отключает вход или выход?" -- ответ был, что там всё может быть сложнее, и, возможно, разные ИСТы на разных фазах, а просадки по фазам могут по разному быть.

      Вывод: надо бы сравнить, на каких фазах сидят какие ИСТы (в частности, срубившиеся 33 и 26 eSol и 1M:1-4).

    • @~13:00, у буфета на 2-м этаже: также спросил у Беликова. Ответ -- рубильник отключает ВХОД.
kurrez_cac208_drv:
  • 27.11.2017: драйвер начат еще в начале октября (в районе 03-10-2017), запущен на пульту и уже больше месяца работает.
  • 27.11.2017: была проблема, что резонатор отказывался включаться при нажатии на кнопку [On].

    Вело оно себя так прямо на уровне 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. Число со знаком минус -- из-за того, что у Куркина значения отрицательные (по его схемотехническим соображениям).

  • 08.05.2018: с неделю-две назад была наблюдена некая странность с (не)включением: драйвер думал что-то одно, а устройство реально находилось в другом состоянии.

    К сожалению, деталей ситуации не запомнилось, и скриншота, увы, тоже нет. Эта запись делается для обозначения проблемы; когда удастся получить детали -- будем разбираться.

    28.10.2022: опять произошла аналогичная ситуация: позвонил Лебедев, что не отрабатывается уставка.

    • В "ручном" режиме -- якобы работает (хотя тоже что-то неидеально, я недопонял).

      А от компьютера -- фиг.

      31.10.2022: "неидеальность" заключалась в том, что пока на компе не был загружен какой-то режим, в ручном ("BLK") оно тоже отказывалось включаться. Позвали Куркина, и тот разъяснил, что разрешения от CAN-блока (от компьютера) и от панели включены ПОСЛЕДОВАТЕЛЬНО, поэтому пока от компа не пройдёт "инициализация" с разрешением, с морды оно тоже не включится (я не до конца всё понял -- что-то говорилось про "кнопку, которую надо нажать и потом отжать").

    • И что он даже делал reset блоку CAC208, и что-то там у него на экране болотное.

      31.10.2022: не факт, что болотное: возможно, это "бледножёлтый" BG_NORM_YELLOW был так воспринят ("как будто болотное, но не совсем").

    • К сожалению, я не смог посмотреть вживую -- клятый Ростелеком так тормозит, что пользоваться невозможно.

      ...на будущее -- запускать командой

      ringrf is_freezed with_freeze_btn with_once_btn
      и щёлкать "Once".
    • Ручной анализ значений каналов показал, что vdev_state=3, что есть 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? Опять какие-то косяки "ручного" режима (его некорректной реализации и не-отвязанности от компиютерного)?

    • Чуть позже, после обеда: разобрался в причине "DISABLED": таблицей 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 поле пожелтевает.

  • 08.05.2018: присутствуют некие каналы USET_MIN и UEXC_MIN, для которых есть вся обвязка (выставление калибровок и return-type, но их использование несколько туманно.

    Оба они -- каналы чтения, но если значение 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, а сравнения с ним нигде не делается (видимо, Кондаков и компания не выдали этого как обязательного условия при включении).

  • 08.05.2018: энное время назад был введён канал ERRDESCR=98, уставляемый функцией 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: вдогонку:

    1. Добавлено отправление сообщения (если не пустое) также и в лог -- по опыту lebedev_turnmag'а.
    2. Сделана попытка реально заюзать SetErr() -- чтоб сообщало причины проблем.

      Обломиссимо: единственным найденным местом оказалась реакция на изчезновение (обнуление) IS_ON в _sodc_cb(). И более нигде ничего.

  • 08.05.2018: были узнаны Виталей Балакиным у Кондакова калибровки каналов {падающая,отраженная}{1,2} -- теперь уже не в вольтах (как они изначально меряются) а в мощностях (V^2/Ohm). Это формулы вида P=U^2*K, где K в районе 4500 -- в зависимости от конкретного канала.

    И их желательно бы отдавать прямо из драйвера, а не считать в скринах -- чтоб прочие программы (включая всякие журналяторы) могли б пользоваться готовыми данными.

    08.05.2018: учитывая, что исходные величины там -- микровольты, с коэффициентами 1000000 для перевода в вольты, остаться в рамках int32 не удастся, надо сразу переводить в float64.

    Свободное место в карте каналов еще есть -- 6 штук в диапазоне [92,97]. Вот их и заюзаем: 4 штуки сейчас, а еще 2 про запас оставим.

    • Вводим каналы в .devtype- и _drv_i.h-файлы.
    • В kurrez_cac208_sodc_cb() добавлены вычисления и возврат (вот нету "ReturnFloat64Datum()" -- приходится извращаться...).
    • В ringrf.subsys поменяны ссылки на каналы, с U{incd,refl}{1,2} на P{incd,refl}{1,2}.

      Плюс ширина полей увеличена до %7 (характерная величина -- 1500W) у всех строк.

gid25_drv:
  • 16.03.2018@сессия-ИЯФ-Шварц-(ВЭПП2000, упомянул ГИД-160)-Логашенко-КМД3: просто размышления: у нас ведь в софте системы управления ГИД25 сделан не "устройством" (драйвером), а просто набором каналов через cpoint'ы. Причины:
    1. В 2010-м никакого vdev не было и в проекте (потребность появилась в 2012-м, для ИСТ и В1000), и inserver только был создан (для гид'овского trig_read_drv.c).
    2. Функционала у этого "драйвера" нет -- никакой реакции на внешние события (типа блокировок), никаких хитрых процедур включения.

    И эта "недрайверность" -- существенное отличие от прочих "устройств управления источниками".

    • ...а так-то сделать бы можно -- был бы аж с 3 подстилающими устройствами (CGVI8, CEAC124, VSDC2).
    • Но еще "аргумент против" -- на ВЭПП-4 часть каналов в конце, "от несуществующих ГИДов", используется совсем иначе.
v5k5045_drv:
  • 28.03.2018: начинаем, нужно для замены гусиностей.

    28.03.2018: сделан "скелет", уже собирающийся.

    Этот драйвер -- на сегодняшний день чемпион: он имеет в подложке аж 6 устройств (раньше максимум был у vepp4_gimn_drv.c -- 3 штуки).

    18.09.2018: за последние полмесяца-месяц драйвер допилен до рабочего состояния и вполне успешно контролирует клистроны.

    Некоторые замечания/highlights:

    • Возможно, не стоило делать "подчинёнными" все 6 устройств, т.к. в для реальной работы именно с клистроном/модулятором необходимы только 4: ПКС, УР и пара входных регистров.

      ГВИ же и ЦАП -- просто 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:

    1. 20000:
      • В sodc-список добавлен канал PKS_SET_IMM, ссылающийся на pks.code_imm0.
      • В SwchToSW_ON_UP() добавлены "мозги":
        1. Высчитывается "безопасное" значение -- safe_val, как "out_val-DROP_DELTA", но не выше 20000.
        2. И если текущее значение в PKS_SET_CUR меньше, чем safe_val, то в PKS_SET_IMM отправляется safe_val -- это делается ПЕРЕД отправкой значения в PKS_SET.
    2. Номиналы:
      • Добавлен канал hvset_nominal.
      • Добавлено поле privrec_t.out_val_nominal.
      • Заведена табличка text2cfg[], покамест содержащая только параметр nominal.
      • Как сделан парсинг:
        • Синтаксис принят "человеческий" --
          BASE [PARAMETERS]
          вместо извратного ist_cdac20'ского
          [(PARAMETERS)]BASE
        • В _init_d() делается сначала ручное вычленение ссылки на базу и помещение её в base[], а затем оставшееся вручную скармливается psp_parse().
        • "Вычленение" сделано по образцу v3h_a40d16_init_d() (куда потом, украсивленное, с-портировано обратно), только поиск сепаратора другой -- '\0' или isspace() вместо '/'.
        • PSP-парсинг подсмотрен в ist_cdac20_init_d().
      • Значение _CHAN_HVSET_NOMINAL можно менять на ходу, в пределах [MIN_ALWD_VAL,MAX_ALWD_VAL].
      • Само оно, если >0, используется как верхняя граница при записи в _CHAN_HVSET.

    В такой парадигме проверено -- работает.

    Но неясным остаётся вопрос об отношениях указанного номинала с ныне-неработающим вычитыванием текущей уставки: не делать ли в качестве "вычитывания" просто out_val=out_val_nominal?

    23.09.2019: поступила просьба, чтобы при вводе юзером уставки HVSET, более маленькой, чем текущее значение, она бы прописывалась в железо даже в режиме "выкл" (и INTERLOCK тоже -- т.е., во всех, а не только в IS_ON и SW_ON_UP, как сейчас).

    Смысл такой:

    • Вот работали какое-то время, была уставка вроде 45000.
    • Сделали "выкл" (или отрубилось по блокировке) -- сбросилось на 10000 (DROP_DELTA), до 35000.
    • Долго так стоим.
    • После долгой (часы, дни) паузы надо начинать с чего-то малого, и 35000 тут не подходит (оно годится для после КОРОТКИХ простоев).

      Там нужно, условно, 25000.

    • При попытке же юзера ввести в поле HVSET те 25000 -- ничего не происходит, поскольку состояние-то "выкл" (или INTERLOCK).
    • А включение выполняется так: сначала "отрабатывается" стоящее-сейчас значение (те самые 35000), а уж потом снижается до 25000, и это плохо.

      24.09.2019: вот тут странно: перепроверил я код -- всё "включение" сводится к записи значения уставки в PKS_SET; никакого же "бита включения" там нет. Ну и как оно может при этом "сначала забрасывать до 35000"?

      24.09.2019: спросил у Валеры Мусливца -- да, всё верно, но "есть нюанс": уставка действительно "просто стоит на 35000", но модулятор стоит в состоянии "не-работа" из-за наличия блокировки; если же нажать [Reset] в программе или аналогичную кнопку на стойке, то блокировки сбрасываются и НАЧИНАЕТ РАБОТАТЬ -- как будто бы сделано "включение" (да, дурной дизайн).

    • Вот потому и просят юзеры в лице Валеры Мусливца сделать так, чтобы если вводимое в HVSET значение меньше текущего записанного в железку, то оно бы прописывалось туда СРАЗУ, независимо от текущего режима.

    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": это возможность отключать влияние загоревшихся блокировок на работу драйвера.

    03.09.2021: реализовано так:

    • Поле privrec_t.ignore_ilks,
    • на которое отображается свежевведённый канал ignore_ilks=12.
    • Если оно !=0, то в v5k5045_sodc_cb() загорание блокировок НЕ приводит к переходу в состояние INTERLOCK.
    • И это пока всё -- полного игнорирования блокировок нет, ВКЛЮЧИТЬСЯ горящие блокировки НЕ дадут.

    28.10.2021: "неожиданный" побочный эффект (озадаченное письмо от ЕманоФеди):

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

    По коду -- да, ровно так и есть: IsAlwdRST_ILK_SET() позволяет переход (т.е., по факту -- отработку канала V5K5045_CHAN_RST_ILKS) лишь из состояния INTERLOCK (ну и плюс из "промежуточных" -- собственно процесса сброса блокировок. А при игнорировании блокировок и перехода в INTERLOCK не происходит, так что и сбросить никак не удастся.

    28.10.2021: много думал, что же тут можно сделать.

    • Ключевая проблема -- в НЕПОЛНОТЕ того отключения при ignore_ilks: переход в INTERLOCK оно отключает, но более ничего.
    • А можно также разрешить отработку RST_ILKS:=1 не только из того состояния, но и если горит хоть один из битов блокировки.
    • ...для чего определение "горит ли хоть один" вытащить из SwchToDETERMINE() в отдельную функцию.

    28.10.2021: судя по письму ЕманоФеди от сегодня в 16:25, всё это нафиг не нужно, т.к. «Выдумывать же программное лечение проблем в силовой исполнительной электронике мне представляется глупость, поэтому я формулировать "чего хочется" для тебя на такой случай не буду.».

    25.04.2022: всё-таки понадобилось -- проблемы с 3-м модулятором продолжаются. Посему учёт ignore_ilks был добавлен также и в IsAlwdRST_ILK_SET() с IsAlwdSW_ON_UP().

  • 03.09.2021: некоторое время назад из крейтов были выкинуты ГВИ; из devlist'а они тоже были удалены, но в драйвере ссылки туда ещё оставались -- каналы gvi.quant0 (1,2) туннеллируются в свои каналы delay1 (2,3).

    В результате при рестарте сервера все драйверы клистронов не запустились, жалуясь на ненайденные каналы --

    cda_new_chan([4]="gvi.quant0"): unknown channel

    Поэтому сейчас все ссылки на них заключены в #if USE_GVI, а оный символ уставлен в 0.

    Но СВОИ каналы оставлены -- на них даже R'ы ставятся.

ist_cdac20_drv:
  • 28.06.2018: создаём раздел, для документирования всего, касающегося уж много лет как созданного драйвера (ПЕРВОГО из vdev'ных, кстати).
  • 28.06.2018: некоторая засада с -- источниками by Веремеенко (сейчас Васичев-jr.).
    • Они формально совместимы по управлению/контролю с ИСТами, но отличаются по маппированию битов входного регистра на каналы блокировок и входов АЦП на каналы измерения.

      И даже сами наборы каналов различаются; и ладно б просто различаются (можно б было имена-alias'ы сделать) -- но некоторые (блокировки по фазе и температуре) просто на других местах.

    • Сейчас различие учитывается лишь в subsys_magx_macros.m4::MAGX_IST_CDAC20_LINE(), где есть параметр 9, означающий "веремеенковость", когда просто метки ставятся иные.
    • А ЕманоФеде очень желательно иметь доступ к каналам блокировок и измерений по правильным именам.

    Что делать?

    • Городить отдельный драйвер было бы глупо.
    • Делать другой devtype -- тоже проблема: драйвер-то берётся по имени.
    • А можно так: другой devtype, но указывать "/ist_v1k" -- тогда карта возьмётся другая, а сам драйвер для ИСТа.

    28.06.2018: так и делаем.

    • "Другой тип" назван vnn_ceac51; "nn" тут означает "сколько-то" -- мощность у этих источников бывает разная (хотя обычно вроде 500).

      В нём имена каналов отличаются как положено.

    • devlist_magx_macros.m4: добавлен макрос MAGX_VNN_CEAC51_DEV() -- в нём и стоит "/ist_cdac20".

      И, как это ни глупо, у него тоже есть 4-й параметр -- "negated_dcct2". Потому, что у БОЛЬШИНСТВА веремеенковских измерение по этому каналу с обратной полярностью, но вот конкретно у источников spectr1 и spectr2 -- с прямой. :-(

    • subsys_magx_macros.m4: в макрос MAGX_IST_CDAC20_LINE() в ветки "веремеенковостей" внесены новые имена.
    • devlist-canhw-11.lst: сменен макрос у источников: coil1, coil2, rings, spectr1, spectr2, ql15. qltr3, qltr2, qltr1.

    29.06.2018: задеплоил на пульту -- работает.

  • 29.10.2018: надо б будет перевести синтаксис указания PSP-параметров с "[(PARAMETERS)]BASE" (т.е., параметры -- в скобках перед базой) на более естественный "BASE [PARAMETERS]" (т.е., параметры опционально после базы, через пробел), как в v5k5045.
  • 24.06.2021: обнаружился косячок: в 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 вёрнуто положительным.

    24.06.2021: исправляем: там достаточно просто домножать возвращаемое значение на cur_sign, который как раз к этому месту уже определён.

    Так и сделано. Теперь надо будет проверить.

    1. Исправить. 2. Записать про iset_walker (глюки: иногда на реверсивных ИСТах не замечает завершения последнего шага). 3. Записать про поведение a3818+adc250*2 при repcount<1. ...кстати, дескриптор там дорос-таки до 1023...

v3h_a40d16_drv:
  • 29.10.2018: приходится делать раздел.
  • 29.10.2018: парсинг "БАЗА/N" в _init_d() был унифицирован с v5k5045'шным (который, в свою очередь, наполовину основан на тутошнем): вместо hiername[] и len используются переменные base[] и base_len, плюс секционирование и комментарии к секциям.
v5phaseconv_drv:
  • 04.10.2018: драйвер для управления фазовращателями им. тов. Суханова. Ранее контролировалось гусиной программой phr (PHase Rotation), причём преобразование выполнялось прямо в GUI, а в виде каналов фазы НЕ были доступны.
  • 04.10.2018: делаем.

    04.10.2018: процесс:

    • Делаем на vdev-"без состояний", за скелет/основу взят простейший -- lebedev_avg_drv.c, только добавлена многоканальность.
    • Формулы расчёта взяты из гусиного кода -- epics/phr/src/main_cb.c, функции convertVoltToPhase() и convertPhaseToVolt() плюс толпа [4]-массивов констант (там коэффициенты в формулах разные в зависимости от фазовращателя).
    • Отдача значений наверх делается ТОЛЬКО в _sodc_cb(). ...вероятно, это нам еще аукнется (болотным цветом)...

    Итого -- сделано, надо проверять.

    05.10.2018@утро-душ: в коде есть "ручное" преобразование между вольтами и микровольтами, с домножением/делением на 1000000.0.

    • Вообще-то это извращение: ведь у cda уже ЕСТЬ нужная информация о калибровке, которую она может использовать, и дублирование сего в драйвере -- совершенно излишнее действие.
    • Но сие диктуется vdev'ом, который предписывает работу с сырыми данными, принудительно и безусловно проставляя флаг CDA_DATAREF_OPT_NO_RD_CONV всем каналам.
    • Это было (и является) реально необходимым для обычных, массовых INT32-каналов. Но для FLOAT/DOUBLE-каналов -- уже скорее лишнее; а тут как раз случай, когда МОЖНО target-канал декларировать как DOUBLE.
    • Ну так чё -- ввести в vdev флажок "VDEV_DO_RDCONV", чтоб преобразование всё-таки выполнялось? Ведь совсем несложно же.

      ...чуть позже: да, флажок такой сделан, надо его заиспользовывать.

    07.10.2018: (утро) первая проверка драйвера -- вроде работает, но числа почему-то в последнем знаке отличаются от гусиных (проверено поочерёдным запуском того и этого, с неизменным значением в ЦАПе).

    07.10.2018: (вечер, ПОСЛЕ пляжа) заиспользовал VDEV_DO_RDCONV, избавился от всех умножений/делений на 1000000.0 -- вроде всё работает.

    13.10.2018: к вопросу об отличающихся в последнем знаке числах: было подозрение, что, возможно, используемый сейчас гусиный код имеет иные наборы коэффициентов, а мне в лапы попал подустаревший исходник epics/phr/src/main_cb.c.

    Так вот -- нифига, те коэффициенты идентичны приведённым в сухановском описании. Так что:

    1. Надо еще раз провести серию сравнений, с разными числами (типа фазы 0, 10, 20, 90), заскриншотить результаты от гусиного и от моего и затем сравнить внимательнее.

      По результатам можно уже предметно общаться с Федей.

    2. В бумаге Суханова кроме тех 7 коэффициентов приводятся также минимумы и максимумы для каждого канала. Их бы надо тоже учитывать.

    18.10.2018: еще одна проверка совпадений/несовпадений.

    Так вот -- разница есть, даже в нулях:

    • Если ставим во все каналы значение 0 из CX, то в ЦАП попадает {1.333326,0.338994,0.334230,0.337594}, а в EPICS читаются значения {0.02,0.01,0.00,0.00}.
    • Если пишем во все каналы значение 0 из EPICS, то в ЦАП попадает {1.332240,0.338550,0.333975,0.337330}, что в CX читается как {-0.03,-0.01,-0.01,-0.01}.

    Федя выдал гипотезу, что дело может быть в разных значениях PI: у меня оно берётся за M_PI, а у Гусинского используется 3.1415926 (7 знаков после точки вместо 29 в стандартном 3.14159265358979323846264338327).

    Окей, проверил, использовав в драйвере гусиный укороченный вариант -- фиг, вообще никакой разницы.

    Чуть позже:

    • а не в том ли дело, что мой и гусиный драйверы Ц0642 используют чуть разные формулы преобразования вольтов в коды: у меня квант 305uV/bit, а у него линейное отображение [-10,+10]<->[-32768,32767] (кажется)?
    • (где-то раньше я уже с этим встречался, надо б порыться, где...)
    • Как раз в 3-м знаке вольтов после запятой и начинается разница.
    • Хотя всё равно не сходится: гусиные "нули" попадают в ЦАП как значения {1.33318,0.338903,0.334325,0.337682} -- прочитано caget'ом.
    • Впрочем, будучи записаны в ЦАП, эти вольты приводят к появлению нулевых значений в фазах.
    • Аналогично, при записи (caput'ом) вольтов, попадающих в ЦАП в CX, гусиная софтина показывает нулевые фазы.

      Для архива: в CX эти значения читаются из ЦАПа как {1.332240,0.338550,0.333975,0.337330} -- т.е., ровно то, что читается при записи из EPICS нулевых фаз.

    • (А "всё равно не сходится" уже вполне можно списать на погрешности округления.)

    Засим можно считать загадку решённой, а вопрос закрытым.

  • 31.10.2018: как выяснилось, всё не так уж и фиксированно с этими фазовращателями.

    Физически по 2 канала в блоке, и используются 2 блока (чтобы 2*2=4, для 4 клистронов). Однако есть 3 блока -- еще +1 резервный.

    Но Фролов намерял какие-то странности в 1-м, и поставил вместо него 3-й (мне сказав сильно задним числом).

    Сейчас-то я просто руками подшаманил драйвер, вписав в первые 2 колонки коэффициенты от 3-го блока (да, в бумажке от Суханова они есть).

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

    31.10.2018: какие видятся варианты -- а разные, причём разного уровня конфигурябельности:

    • Можно вставить табличку на 6 колонок, каналы адресовать по номерам не напрямую, а через "таблицу трансляции" -- 4 ячейки, в каждой из которых индекс от 1 до 6 (увы, придётся руками +1 делать), указывающий, какую колонку использовать. И:
      • По умолчанию маппирование -- {1,2,3,4}.
      • Но в auxinfo чтоб можно было указывать -- типа "C1=5 C2=6" (это как раз для нынешнего, при использовании 3-го блока).
      • А также сделать эти 4 ячейки каналами, чтоб можно было переконфигурировать вживую, не трогая devlist.
      • ...ну и при записи в индекс, естественно, форсить чтение соответствующей фазы.
    • Можно вместо модели "1 устройство, работающее с 4 каналами", сделать "на каждый канал своё устройство", указывая номер в auxinfo (ну и ссылку на канал ЦАПа?).
    • Для предыдущего можно также указывать коэффициенты в auxinfo.

      ...хотя это можно и для нынешнего многоканального так сделать.

    • А также отобразить коэффициенты на каналы, чтобы дать возможность менять их на лету.

      ...и это тоже можно и в многоканальном сделать.

    Короче: сделать -- МОЖНО, кучей разных способов. Вопрос в ПОТРЕБНОСТИ, которая пока неясна/неопределённа.

    Когда появится ясность -- сделаем.

img878:
  • 14.03.2020: это раздел как бы не совсем о драйвере, а скорее об инфраструктуре. Поэтому назван раздел просто "img878", а не "img878_drv". Но по смыслу он близок к прочим драйверам, потому и тут.
  • 14.03.2020: приступаем к работе над этой штукой.

    Смысл: чтобы можно было видео от TV-камеры, подоткнутой к v5p2 через frame-grabber, смотреть дистанционно -- например, в пультовой ВЭПП-4 (Беркаеву оно нужно для настройки).

    14.03.2020: некоторые базовые идеи:

    • Реального драйвера нет, а есть лишь канал-"почтовый ящик" в сервере, в количестве 4 штук (по числу нужных картинок: 1 текущая и 3 кадра-после-вспышки).
    • Данные в эти каналы кладёт программа tvcapture -- tvcpture.cpp, являющаяся модифицированной Беркаевым демкой от openCV.
    • Кладёт она их туда посредством "pipe2cda -C" (которая полгода назад как раз для этого и была затеяна, хотя в её разделе я сие упомянуть забыл).

      Причина -- openCV основан на Gtk, а для него у нас биндинг cxscheduler'а отсутствует.

    • Для отображения же используется обычная инфраструктура vcamimg, для чего изготовим плагин/клиента как для устройства.

    Название выбираем "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/) и текста писем Беркаеву):

    • 27.03.2020: первая версия, берущая в качестве яркости grayscale-пиксела значение B.

      Получилось не очень красиво, т.к. "отсутствующий сигнал" показывается синим цветом, в результате чего на картинке яркие светлые полосы.

    • 29.03.2020: памятуя, что ~60% яркости -- это зелёный канал, берём значение G вместо B. Стало вроде лучше, но не идеал.

      Поэтому делаем "правильное" преобразование RGB->grayscale: R*0.3+G*0.6+B*0.1.

      Но при сравнении картинки в скрине img878 с исходным изображением вживую наблюдаем странные отличия: какие-то артефакты.

    • 05.04.2020: в продолжение вопроса - точнее, скорее уже окончание.
      1. Я разобрался с тем, почему переданная через CX картинка отличалась от исходной: дело в signed/unsigned. Эти прекрасные люди, что делали openCV, почему-то объявили буфер данных - IplImage.imageData - как просто "char *", а не "unsigned char *"; а на X86 тип char знаковый. В результате пикселы с яркостью выше половины - т.е., со значениями выше 127 - имели значения как бы отрицательные - (например, синий фон - R,G,B:0,0,-64). Вот арифметика перевода из RGB в grayscale и давала диковатые результаты. Я, конечно, сам дурак - на такое надо обращать внимание сразу.

        Сейчас переделал преобразование на явное использование беззнаковых значений, и картинка через CX практически идентична исходной (с точностью до того, что исходная не совсем монохромна).

      2. Насчёт производительности: реальный тест показывает, что всё успевается с огромным запасом - tvcapture с отправкой данных в CX даёт загрузку процессора в районе 10%+/-, и это даже с максимвально нагружающим "правильным" преобразованием Gray=0.3*R+0.6*G+0.1*B.
  • 17.04.2020: во время возни с img878/tvcapture пришла в голову идея: а можно ли сделать библиотечку, создающую отдельный thread для запуска 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-вызовы делать.

    Т.е.,

    • По умолчанию mutex должен быть ВЗВЕДЁН, сигнализируя о ЗАПРЕТЕ любых операций.

      А на время select()'а -- ОПУСКАТЬСЯ, разрешая их.

      И, соответственно, взводиться обратно он будет тоже "условно", по окончанию любых иных действий.

    • Соответственно, все ПРОЧИЕ операции должны быть защищены этим же mutex'ом (это ответ на вопрос "а ВТОРОЙ-то юзер mutex'а кто?").

      ...и вот тут немного неясно, что делать с cda-evproc'ами: ведь они-то будут вызываться как раз когда "ВЗВЕДЁН"; получается, что из них ничего нельзя будет делать, т.к. из-за того, что это тот же самый thread, попытка вызвать любую операцию с защитой приведёт к deadlock'у.

      Чуть позже: да нифига -- раз это тот же самый thread, то из него как раз МОЖНО ВСЁ!

      А защищать надо обращения только из ДРУГИХ thread'ов.

    Пара замечаний:

    1. Однако всё же есть одна проблема: во время "безопасного" нахождения в select()'е программа-клиент может помимо невинных действий (вроде чтения данных из буфера) сделать также то, что повлияет и на "глобальное состояние" -- набор дескрипторов: закрытие соединения (да по любой причине) приведёт и к закрытию дескриптора), который вроде как значится "ждущим" для select()'а.

      Ведь дизайнилось-то всё в расчёте на однопоточность, где (не)явно действует предположение/гарантия, что подобного "выдёргивания из рук" не произойдёт.

      Что делать?

      • Первой мыслью (это во время "пробежки") было, что нужно завести 2-й mutex/флаг, "для УДАЛЕНИЯ дескрипторов": чтобы когда делается sl_del_fd(), то этот флаг взводится и как-то там обрабатывается (в блоке вызова callback'ов по взведённым битам готовности?).

        Сейчас (~20:50), когда записываю те мысли, эта идея представляется странной и непонятной: что за флаг, как именно sl_main_loop() должен реагировать на его поднятость?

        Тогда уж скорее не "флаг", а лишний fd_set, по умолчанию пустой, но чтобы sl_del_fd() взводил бы в нём битик -- чтоб callback'и НЕ вызывались бы для таких "покойных" дескрипторов. (А перед select()'ом чтоб он нулился бы.)

      • Но сейчас припоминается, что, вроде бы, ядро на такие "исчезнувшие" дескрипторы будет реагировать сразу прямо изнутри select()'а -- возвращая ошибку.

        Это, кажется, у меня такой опыт был -- уж не припомню, при каких условиях.

        А в man-странице по select есть такой раздел:

        Multithreaded applications
        If 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 байт?

    2. А не сгодится ли эта же модель для прочих библиотек -- вроде Xlib/Xt, или даже Gtk -- чтоб к ним можно было "подселять" произвольный сторонний код? Тогда это "изобретение" -- довольно глобальная концепция, хоть правда патентуй; как минимум -- статью пиши.

      ...узнать бы, не придумал ли уже кто нибудь её.

    03.06.2020: найдено гуглением (на тему вроде "xlib multithreading"):

    "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: приступаем. Некоторые базовые принципы:

    • Новая библиотека/концепция получает название "mt_cxscheduler".
    • У неё свой include-файл mt_cxscheduler.h, который однако #include'ит обычный cxscheduler.h.
    • Аналогично, исходник в lib/useful/ -- mt_cxscheduler.c, и:
      1. Он #include'ит обычный cxscheduler.c.
      2. В cxscheduler.c добавляется использование нескольких hook'ов с именами вида 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
    • Т.е., это РАЗНЫЕ библиотеки (новая -- libmt_cxscheduler.a но они пересекаются по именам функций, так что являются взаимоисключающими (как и Xh_cxscheduler с Qt*cxscheduler).

    Собственно реализация:

    • Публичный API в mt_cxscheduler.h состоит всего из 2 вызовов -- mt_sl_lock() и mt_sl_unlock().
    • Работа с локингом:
      • Реальную работу с mutex'ом выполняет парочка do_lock() и do_unlock().
      • Публичные mt_sl_lock() и mt_sl_unlock() вызывают их УСЛОВНО, только если текущий threadid отличается от cxscheduler'ова.
      • А вот hook'и 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.c вызов CXSCHEDULER_HOOK_FDSET_CHANGE делается не только при добавлении/удалении/изменении маски дескриптора, но и в sl_enq_tout_at() -- чтобы в случае добавления нового, более близкого таймаута, он бы отработался, а не ждалось бы сначала до истечения предыдущего-ближайшего.

      А вот при УДАЛЕНИИ таймаута -- нет, НЕ делается: ну подумаешь, прервётся select() чуть раньше, чем теперь должен бы, ну и фиг с ним.

      29.11.2024@утро, после мытья посуды: по-хорошему при ДОБАВЛЕНИИ таймаута, могущего быть ранее, чем текущий ближайший, надо не безусловно прерывать select(), а лишь если новый таймаут ДЕЙСТВИТЕЛЬНО ранее, чем текущий ближайший. Сделать это можно, несложно, прямо в sl_enq_tout_at(); но надо ли? Так-то пофиг на небольшую неоптимальность...

    • Инициализация всего выполняется в hook_main_loop():
      1. "Запоминает свою идентичность" --
        mt_sl_threadid = pthread_self();
      2. Создаёт mt_sl_mutex и тут же лочит его.
      3. Создаёт mt_sl_wake_pipe[], тут же переводя его в O_NONBLOCK, и регистрирует читающий конец.

      Оная инициализация делается условно и одноразово, закрытая флагом mt_sl_initialized, так что sl_main_loop() может вызываться многократно.

    • Новоиспечённая библиотека уже зарегистрирована в Makefile'е и билдится.

      ...но в КРОСС-средах -- нет.

      11.06.2020: и в src/TopRules.mk добавлено определение LIBMT_CXSCHEDULER.

    Похоже, пора уже изготавливать вариант tvcapture на этом механизме -- для проверки.

    01.06.2020@вечер: а ещё - надо ж выполнять некую процедуру "запуск cxscheduler'а в отдельный thread", и это ТОЖЕ должно быть в mt_cxscheduler.

    И операция эта должна быть СИНХРОННОЙ -- чтобы пока в порождённом thread'е не произойдёт вся инициализация (mutex'ы, pipe-дескрипторы, ...), в родительском бы ничего не делалось.

    • Как? Напрашивается идея использовать для такой синхронизации pipe: чтобы родитель висел на чтении 1 байта, а потомок когда всё подготовит, то выполнил бы запись этого байта.

      (Примерно так делалось в старой 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().

    • Определение CXSCHEDULER_HOOK_MAIN_LOOP закомментировано.
    • Зато введена mt_sl_start(), должная выполнять инициализацию, включая создание нового thread'а и запуск в нём sl_main_loop().

      Инициализация скопирована из hook_main_loop().

    • ...только "запоминать идентичность" в mt_sl_thread_proc уже не требуется -- это автоматически делается вызовом pthread_create().
    • И никакую "СИНХРОННУЮ" инициализацию-с-ожиданием -- как предполагалось (в каком угаре?!) 01-06-2020 -- делать не нужно: создание mutex'а и pipe'а делается в самом mt_sl_start(), прямо в изначальном thread'е.

      А в качестве "средства синхронизации" будет работать прямо сам mutex, который уже сразу залочен.

      Поэтому:

      1. Никакого ожидания байта из pipe'а не делается.
      2. А mt_sl_thread_proc() состоит лишь из вызова sl_main_loop() в бесконечном цикле.

    Вот теперь -- да, всё, пора уже делать реальное применение, в tvcapture.

    12.06.2020@утро: вот только с работой mutex'а в качестве средства первоначальной синхронизации могут быть проблемы: если реализация mutex'ов предусматриваеть условность -- что заблокированный тем же thread'ом НЕ считается заблокированным -- то получится, что владельцем является как раз стартовый thread, а не "cxscheduler'ный". И тогда:

    1. В самом стартовом thread'е не будет защиты -- раз он будет "владельцем" блокировки, то ему типа всё позволено.
    2. Как раз "cxscheduler'ный", наоборот, возможно, НЕ сможет снять блокировку перед select(), поскольку НЕ он её владелец.

    Так что, возможно, всё же надо вернуться к идее "запускаем всё с синхронизацией через pipe". Благо, способ реализации ясен.

    12.06.2020: да, переделываем.

    • Теперь mt_sl_thread_proc() сначала лочит mutex и отправляет 1 байт в pipe.
    • mt_sl_start() же теперь mutex только создаёт, но НЕ лочит.
    • Зато после создания thread'а он ждёт готовности читающего дескриптора.

      Код ожидания подсмотрен в check_fd_state() -- также игнорирует прерывания; но вместо таймаута указывает NULL -- "бесконечность".

    13.06.2020: и адаптируем tvcapture.c -- тут всё просто.

    • В отдельной директории cdaCV/.
    • Этот вариант сделан в #if'ах по USE_MT_CXSCHEDULER.
    • Изготовлен другой экземпляр CreatePipes(), целиком в #if'е.

      Она делает mt_sl_start(), а потом в "скобках" lock/unlock создаются контекст и 4 канала.

    • А в SendToPipe() просто кусочек отсылки данных ("вычислительная" же часть общая).
    • И Makefile под-адаптирован.

    Сделать-то сделал, да только оно не компилируется: конфликт в определениях типов int64 и uint64, которые в CV'шных .h-файлах также присутствуют.

    Очевидно, нужно утаскивать непосредственное взаимодействие с cda в отдельный файл, чтобы он #include'ил CX'ные header'ы, но НЕ openCV'шные.

    15.06.2020: делаем.

    • Файл cda_adapter.c плюс cda_adapter.h.
    • Весь код инициализации -- в cda_adapter_init().
    • Код отправки -- в cda_adapter_send_one().
    • Что неприятно (но тут пофиг) -- количество кадров/каналов, 4, захардкожено.

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

    1. Диагностика сделана понятнее: везде печатается имя программы (из program_invocation_short_name, при доступности, а иначе "mt_cxscheduler") и PID.
    2. Отключаем диагностику, путём уставки 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" вместо перед печатью.

    Исправлено.

  • 26.05.2020: в продолжение мыслей о "библиотеке, которая бы окружала cxscheduler'ный главный select() mutex'ами" (mt_cxscheduler?): возникли мысли о том, что можно бы CX'ные драйверы использовать в EPICS'е.

    Создаём на эту тему специальный мини-раздельчик; пока тут, в img878, но потом надо бы переселить куда-то.

    26.05.2020: типа протокола, по мере появления идей:

    1. @пробежка-по-квартире: пришла в голову мысль, что чисто идеологически CX и EPICS в отношении работы с устройствами даже похожи (несмотря на отсутствие в EPICS явного понятия "драйвер"): есть "единицы адресации", которые в CX каналы, а в EPICS record'ы, и в CX они адресуются по номерам напрямую, а в EPICS ссылки на "device support" включают некую адресную информацию, в которую вполне можно вставлять что-то вроде "номера канала". И тогда можно для несложных устройств (вроде той же DL250) клепать EPICS'ные "драйверы" чуть ли не копированием CX'ных.
    2. @пробежка-по-квартире-~18:20: а можно пойти ещё дальше -- использовать прямо сами CX'ные драйверы в EPICS'е через какой-нибудь простой код-адаптер! Ведь API cxsd<->драйверы очень простой, и уже сейчас есть 2 его реализации (libcxsd и libremsrv), плюс планировалась 3-я -- адаптер для пановского CompactRIO; так что можно изготовить и 4-ю -- переходничок для связывания с EPICS'ом.
    3. (5 минутами хождения позже) А если использовать ту технологию "cxscheduler в отдельном thread'е, защищённый mutex'ом", то можно вообще полноценный libcxsd подселять к EPICS-IOC'у, и тогда пользоваться ВСЕМ функционалом сервера, включая vdev, remdrv и т.п.

    В любом случае будет вставать вопрос конфига: как сопоставлять CX'ную карту каналов и EPICS'ный DB; причём тут вопрос 2-слойный: и на уровне каналов конкретного экземпляра устройства, и на уровне списка устройств. Но это уж как-нибудь решаемо: можно сделать какой-нибудь препроцессор/конвертер, можно в devtype добавить ещё одно по-канальное поле наподобие dbprops, а можно просто руками изготавливать include-файлы вроде TYPENAME.devtype.db.

    • Замечание 1: а можно в "адресе" record'а указывать insrv-имя, чтобы "адаптер record'ы<->CX-драйверы" работал бы через cda.
    • Замечание 2: а как в EPICS-IOC с заказом дескрипторов и таймаутов? Может, удастся сделать нативный биндинг "ioccxscheduler"?
    • Замечание 3: вопрос к Паше/Лёше насчёт record support'а: а как там с типизацией? Т.е., вот объявляем мы некое поле как "int16" или там "float32" -- и как "драйвер" узнает этот тип (и что класть надо значение именно этого типа)?

      Возможный ответ: dbPutField() одним из параметров принимает dbr-тип, так что она сама выполняет конверсию.

    27.05.2020: в любом случае, можно сделать специальный такой "device support", в качестве адреса принимающий cda-имя канала (хоть insrv::, для доступа к локально-подселённому cxsd, хоть cx:: -- тогда он будет работать как "бридж" к ЛЮБЫМ СУ -- хоть к CX, хоть к Tango).

    01.06.2020: если этот фокус с использованием cxsd'шных драйверов в EPICS'е получится -- значит, выбранная модель драйверов является "правильной": т.е., соответствующей "природе работы с данными".

lhebw_catctl_drv:
  • 17.06.2021: вчера получено от Репкова предварительное описание управления его блоком лазерного подогрева катодом, так что пора приступать.

    Оно работает через CEAC124, но включать это название в название драйвера не будем.

    Имя же выбрано "lhebw_catctl": "lhebw" -- совпадает с именем клиента (Laser-Heated Electron Beam Welding), а "catctl" -- "CATode ConTroL".

  • 17.06.2021: приступаем.

    17.06.2021: пока больше предварительные действия (в т.ч. потому, что алгоритмика работы пока не вполне огрокана):

    • Простейшая карта каналов -- w50i,r50i.
    • Описатели lhebw_catctl.devtype и lhebw_catctl_drv_i.h уже зарегистрированы в своих Makefile'ах.
    • Начат собственно lhebw_catctl_drv.c -- копированием из какого-то из соседних драйверов. Этот пока и близко до компилируемости не доведён.
lebedev_rotmag_drv:(ex-lebedev_turnmag_drv:)
  • 21.09.2021: создаём раздельчик -- назрело (раньше все записи о ВСЁМ лебедином были между делом в других местах).

    05.07.2022: переименовываем из "turnmag" в "rotmag" по настоятельным просьбам ЕманоФеди. Аргументация -- что это "поворотный магнит" не в смысле "магнит, поворачивающий пучок", а "поворачивающийся магнит", что, якобы, на английском именуется 'rotating magnet".

    • Попереименованы сами файлы, которые теперь стали lebedev_rotmag_drv.c, lebedev_rotmag_drv_i.h lebedev_rotmag.devtype.
    • Внутри них все строки "TURNMAG" заменены на "ROTMAG" (во всех регистрах).
    • Скрин стал rotmag.subsys, что отражено в cx-starter-PULT.conf.
    • Изменённое имя типа отражено в devlist-canhw-21.lst, там и устройство переименовано из "turnmag" в "rotmag".
    • Также термин "bury" везде позаменён на "dump".
    • Каналам go_dump_max и go_ring_max даны alias'ы go_dump и go_ring, прямо в lebedev_rotmag.devtype (и более нигде -- это ЕманоФедя попросил такие ииена, но в коде драйвера удобнее иметь ВСЕ каналы начала движения с унифицированными именами одинаковой длины).
  • 21.09.2021: назрело желание иметь ТЕКСТОВЫЙ канал, отображающий состояние/причину останова.

    Конкретно сейчас -- потому, что возникло подозрение, что кто-то сбросил лимит по току с 1.5A, и потому любое шевеление мотора сразу же вызывало останов; но убедиться в этом "задним числом" уже не удастся. А вот если бы имелся текстовый канал "причина останова" -- то всё было бы ясно сразу же.

    И очевидно, что надо добавить параметр "причина" к DoStop() (возможно, с форматом и vararg'ом?).

    05.10.2021@утро-душ: такая-то диагностика -- да, несомненно полезна.

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

    05.10.2021: приступаем.

    • Для начала -- обнаружилось наличие в lebedev_turnmag.devtype канала vdev_state, который тут, в stateless-драйвере, вроде как ну совсем не при делах.

      Поэтому было проведено расследование, чьи результаты описаны в разделе по vdev за сегодня, а канал переименован из vdev_state в zzz (как оно и было в lebedev_turnmag_drv_i.h).

    • Затем добавлен канал STOP_REASON=18.
    • В DoStop() добавлено:
      • Параметры "const char *format, ..."...
      • ...с надлежащим формированием строки.
      • DoDriverLog() результата.
      • Возврат сообщения текстовым каналом реализован отдельной ReportStopReason() -- чтобы она могла вызываться и из другого места для возврата пустой строки.

        "Заклинания" для возврата текстового канала подсмотрены в kurrez_cac208_drv.c::SetErr() (в которой заодно была найдена ошибка -- неприсвоение vp).

    • Ну и во все её вызовы добавлено указание причины.
    • А в DoStart() -- вызов ReportStopReason(me, ""), для очистки сообщения.

    07.10.2021: проверил, на симуляторе: устройству ringsel_adc был поставлен флаг '+' -- суперсимуляция, так что в его каналы чтения можно были писать. Да, работает.

    Только при такой симуляции выявился нюанс (если подумать -- то он должен был быть очевиден изначально): проверка срабатывает ТОЛЬКО в момент реального измерения канала (при симуляции -- "измерения"). Т.е.,

    1. Пока не придёт новое значение -- ничего не проверяется.

      Но это-то понятно -- откуда б иначе получать информацию.

      И бороться с этим бессмысленно: поскольку управление (УРом) идёт через этот же CANADC40, то и пытаться как-то отключать (при, например, посинении) -- попросту нечем будет.

    2. Но также не проверяется и ПЕРЕД СТАРТОМ.

      А вот тут уже спорно:

      • С одной стороны, у полноценных vdev-драйверов (которые с машиной состояния) в IsAlwdSW_ON*() ОБЯЗАТЕЛЬНО проверяется куча условий, в т.ч. и подобные превышения там бы проверялись.
      • С другой же, ну КАК при незапущенном моторе может быть превышение лимита тока? Типа КЗ что ли?

    Короче -- ну да, как бы неидеально всё, но в данной ситуации на эту неидеальность можно просто забить.

    08.10.2021: на пульт скопировано, потом посмотрим после рестарта сервера, что получится.

  • 05.07.2022: надо бы переделать каналы статуса так, чтобы они возвращали значения ТОЛЬКО ПРИ ИЗМЕНЕНИЯХ. Чтобы и траффик лишний уменьшить, и время срабатывания концевиков можно б было видеть.

    Это касается каналов:

    1. GOING_DIR, который полностью искусственный.
    2. CUR_DIRECTION и его "половинок" LIM_SW_DUMP и LIM_SW_RING -- эти являются тоннелями значения C_INPREG8.

    05.07.2022: краткое обсуждение:

    1. С первым должно быть тривиально.

      Хотя настораживает, что там есть закомментированное указание ему режима IS_AUTOUPDATED_TRUSTED -- какового указания, казалось бы, и должно б быть достаточно. Но почему оно закомментировано?!

    2. А вот для вторых надо будет иметь "кэш" -- значения, с которыми сравнивать и возвращать обновления только при несовпадении с предыдущими.
      • И вначале в кэш прописывать значения -1 -- чтоб гарантированно отличалось от любого реального.
      • ...и по-хорошему также прописывать бы -1 и при сбросе подстилающего/subord-устройства.

        Но как ловить то событие -- с учётом stateless-натуры данного драйвера -- хбз.

        ...В конце концов, можно и забить: обновления и так будут идти корректно.

        ...или проблема в том, что при переходе самого ЭТОГО устройства через NOTREADY каналы поболотовеют, а обратно не вернутся?

      • А вот ВНАЧАЛЕ НЕ НУЖНО возвращать какие-либо значения ни в коем случае -- пусть так и висят "never read".

    06.07.2022: делаем, по вчерашнему проекту.

    1. GOING_DIR -- просто раскомментирован перевод в TRUSTED.
    2. Производные от входного регистра:
      • Также переведены на 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[]) и раньше не было.)

    Первая проверка показала, что:

    • Вроде как всё работает должным образом -- обновления однократные, а не постоянные.
    • Рестарт подстилающего устройства -- записью ringsel_adc._devstate=0 -- никакого эффекта не дал, да и на скрине никак на скрине не отразился.

      Почему так -- загадка.

      • Т.к. взгляд на код vdev.c::stat_evproc() показывает, что реакция вроде бы ДОЛЖНА быть.
      • Но почему-то даже включение полного логгинга -- записью rotmag._logmask=-1 -- не показывает в логе НИЧЕГО.
      • ...а ведь "список" _devstate-каналов указывается (в т.ч. devstate_count указан, а вовсе не 0).

      Прям мистика какая-то. Надо бы поразбираться, напихав отладочного вывода.

      Чуть позже: позанимался-таки расследованием -- похоже, причина найдена: дело в комбинации "искусственности" рестарта с нюансом работы "insrv::".

      • Анализ кода is_internal_rw_p() показывает, что там ВЫЗЫВАЕТСЯ и TerminDev(), и InitDevice(); а поскольку оба они делают report_devstate(), то состояние отображаться должно бы.
      • И натравливание cdaclient'а на ringsel_adc._devstate показало, что значение действительно меняется на -1, а потом обратно на +1.
      • И тут дошло: vdev-то по умолчанию слушает через "insrv::", а там через pipe передаются не сами данные, а лишь СОБЫТИЯ; значения же берутся из cxsd_hw_channels[gcid] в момент ПОЛУЧЕНИЯ события.

        И к тому моменту, как поток исполнения дойдёт до vdev-insrv'шного файлового дескриптора, значение в _devstate'овском chn_p->current_val уже давно опять будет +1.

        Потому, что "щёлканье" состоянием -1,+1 делается непрерывным потоком исполнения.

      Так что -- всё нормально, всё как и должно быть. Ну да, вот такой неочевидный нюанс вылез, но запись _devstate=0 -- уж точно не является штатным АВТОМАТИЧЕСКИМ методом работы, а скорее отладочным; так что реальной проблемы нет.

    • Чуть позже: рестарт, сделанный сложнее/длиннее -- _devstate=-1 с последующим _devstate=+1 -- эффект дал: и в логи строчка от stat_evproc() попала, и каналы поболотовели.
    • Так что да -- НАДО искать способ ловить "оживание" устройства (точнее, СВОЙ переход в состояние OPERATING), чтобы инвалидировать кэши записью -1.

    07.07.2022@утро: в голову приходят 3 варианта решения проблемы:

    1. @просыпаясь: а если кроме значения помнить также и rflags, и в lebedev_rotmag_sodc_cb() при несовпадении хотя бы флагов?

      @чуть-позже: да не, не то: ведь болотовеют-то флаги САМОГО устройства, а не подстилающего. Более того -- от подстилающего устройства события с rflags=CXRF_OFFLINE никогда и не прилетит (там они тоже поболотовеют, но вот события "обновление" с ними не будет).

    2. Ловить изменения канала "_devstate" подстилающего устройства самостоятельно, просто добавив его в hw2our_mapping[] отдельным каналом.

      ...возникало сомнение, будет ли это работать -- из-за двойной регистрации этого канала в cda в рамках одного контекста. Но вроде должно: ведь evproc-то будет отличаться, а cda_add_chan() устроена так, что сам канал второй раз создавать не будет, но дополнительный evproc на него навесит.

    3. Ввести-таки состояния в этот драйвер.

      Тогда понадобится сразу 2 штуки:

      • Собственно "DETERMINE", которая реально "RESET" -- для выполнения *_cache=-1.
      • Ещё одно (в которое должен происходить переход автоматически) -- просто для того, чтобы его номер отличался, т.к. иначе vdev_set_state() не будет при сбросе выполнять переход в DETERMINE/RESET (из-за совпадения nxt_state с cur_state).

    Ну-с, и какое решение выберем? Искусственное, но простое 2 или прямолинейное, но более громоздкое 3?

trainer_drv:
  • 18.07.2024: напрашивается драйвер "тренировщик", могущий тренировать как клистроны, так и катоды -- у Старостенко-джуниора по сути тот же алгоритм тренировки.

    Вот и создаём раздельчик.

    18.07.2024@около стадиона НГУ, по дороге из девятиэтажки домой: памятуя, что года полтора назад шло некоторое обсуждение касательно тренировки чего-то на СКИФе, и там Лёша Левичев набросал некое ТЗ+алгоритм, а я этот алгоритм преобразовал в event-driven: вот взять тот алгоритм да реализовать отдельным драйвером, чтоб можно было его "натравливать" на разные устройства через insrv:: -- на вход подавая блокировки.

  • 21.07.2024: отдельный пункт про собственно алгоритм тренировки.

    21.07.2024: информация от того олуторагодичного обсуждения (на СКИФ оно вроде так ни к чему и не привело):

    1. Из письма Левичева 16-02-2023-11:31 Message-ID:1676522729.512887599@f747.i.mail.ru
      Программа тренировки клистрона

      Пользователь должен задавать следующие параметры:

      1. Максимальное напряжение зарядного устройства, до которого программа может его поднимать (или это считывается из уставки программы С. Вощина по управлению модулятором)
      2. Порог вакуума, при котором происходит сброс напряжения
      3. Уровень вакуума на МРН после клистрона, после которого начинается рост напряжения (может быть вшита в программу, пока я бы взял 20 мкА)
      4. Уровень напряжения зарядного устройства, до которого происходит сброс напряжения
      5. Шаг подъема напряжения (в принципе, это может быть вшито в программу, я бы взял 0.1 кВ).
      6. Максимальный уровень напряжения (или битов АЦП) отраженного сигнала, после которого происходит сброс напряжения высокого клистрона.

      Принцип действия программы. Пользователь включает клистрон и поднимается до какого-то напряжения на клистроне и включает программу тренировки. Если произошло превышение п.2 или п.6, то программа сбрасывает напряжения до заданного нижнего порога п.4. После этого она определяет вакуум в насосе (ток насоса). Если он выше порога п.3, то программа ничего не делает, если он ниже, то начинает поднимать напряжение. Поднимает напряжение с шагом п.5, ждет 5 импульсов, после этого снова поднимает, то есть шаг за пять импульсов. На каждом этапе программа определяет максимальное напряжение отраженной волны п.6 и уровень вакуума п.2. Если один из показателей превышен (п.2, п.6), то программа опять сбрасывает напряжение до указанного уровня п.4 и все повторяется. За последний шаг до максимального заданного напряжения программа автоматически уменьшает шаг подъема напряжения на заданный п.5/10. Уже с меньшим шагом поднимается до указанного значения с тем же алгоритмом. Если на стадии подъема ни один показатель не превышен, то программа поднимает до максимального уровня п. 1 и останавливается либо до срабатывания превышения заданных уровней п.2, п.6, либо до изменения пользователем максимального уровня напряжения п.1.

    2. Из моего ответа 16-02-2023-16:56 Message-ID:7541da20-c671-2221-132e-7bfbbb1a55ef@starnew.inp.nsk.su
      • 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 шаг.

    Вот этот мой алгоритм и стоит превратить в драйвер-"тренер".

:
:
Всякости, вроде hw4cx'ных драйверных библиотек:
  • 04.09.2018: непосредственно сейчас создаём ради advdac'а, а дальше мож еще что добавится.
advdac:
  • 04.09.2018: в связи с использованием уже вне CAN-драйверов -- в g0603 (ПКС-8) -- и дальшейшими ради оного модификациями назрело вытаскивание писаний в свой раздел. Который сразу рассекционируем списком level5.
Общее:
  • 04.09.2018: просто заводим.
advdac.h:
  • 04.09.2018: тоже создаём авансом, для структурирования.
advdac_slowmo_kinetic_meat.h:
  • 04.09.2018: базовые записи, в т.ч. о создании advdac вообще -- по ключевому слову "cdac20k_drv", начиная с 02-06-2017.
  • 04.09.2018: понадобилось адаптировать функционал для возможности использования в CAMAC-драйверах -- сейчас для конкретно g0603 (и прочую информацию см. в его разделе за сегодня).

    04.09.2018: процедура:

    1. Все использования KOZDEV_nnn -- {OUT,OUT_RATE,OUT_CUR}_n_base -- были заменены на DEVSPEC_nnn.
    2. А в драйверах-клиентах оные определения сделаны, в том же enum'е, что и прочие DEVSPEC_nnn.
    3. Отдельно о поштучно-возвращаемых значениях CURSPD и CURACC: они были захаркожены KOZDEV_CHAN_ADC_n_base + 98 и +99.

      Поскольку оно нужно только для отладки, то все обращения к ним -- в т.ч. в драйверовых _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: проверено.

    • Передача rflags -- работает (ну еще бы...).
    • Плавное изменение в CAMAC не работало, из-за того, что стояла цепочка SendWrRq();SendRdRq();nxt=0 -- т.е., сброс флага "можно слать следующее" происходил ПОСЛЕ "получения ответа" (которое тут мгновенно) и взведения nxt=1. В CANbus это катило, тут -- нет.

      Сброс переставлен на первое место в цепочке, и всё заработало.

    • Но есть неприятный косяк, что если: а) spd>0 (т.е., "вниз" можно сбрасывать резко), b) плавное изменение УЖЕ идёт, то сброс "вниз" тоже будет идти плавно.

      Надо фиксить. Фиксим, довольно прямолинейно:

      1. Предохранитель
        !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 уже так.

        А теперь общее условие выглядит просто ужасно, с дублированиями. Явно напрашивается на улучшение.

      2. В 2 точках, где выполняется "немедленная запись" (1: если изменение мало или "вниз"; 2: если 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;
        }
        
      3. Заодно все "me->out[l]." были заменены на "ci->" -- так намного короче и читабельнее.

      На вид -- проблема ушла.

      Но теперь надо отдельно смотреть на поведение CAN-устройств, где применение шире, и ждать "сюрпризов".

  • 14.04.2021: добавлена ReportOUT_Ranges(), отдающая наверх диапазоны, известные из по-канальных min/max, а при их неуказанности -- из MIN_ALWD_VAL/MAX_ALWD_VAL.

    14.04.2021: краткие комментарии (собственно работа описана в разделе о диапазонах за сегодня):

    • Главный смысл отдельной функции -- чтобы отдавалось минимальным количеством вызовов (критично для дохленьких контроллеров вроде CM5307 и CANGW); она находит последовательные цепочки каналов с совпадающими диапазонами и отдаёт каждую одним вызовом.

      Поскольку объём получился очень немаленький, то данная функциональность и вытащена в advdac.

    • Странное имя с "_": этот подчерк символизирует ВСЕ каналы "OUT": OUT, OUT_CUR и OUT_IMM.
    • Конкретно для OUT_IMM отправка делается только при определённости этой фичи (т.е., база>0).
advdac_cankoz_table_meat.h:
  • 04.09.2018: и это тож авансом сделано. Процесс создания -- в "о реализации таблиц", начиная с 08-06-2018.
:
:
MQTT:
  • 03.10.2018: заводим раздельчик. Тут будет ВСЁ, касающееся работы с MQTT в CXv4.

    Для начала -- "простой" драйвер, лишь бриджующий данные в MQTT.

    В будущем, возможно, понадобится и что-нибудь поинтеллектуальнее, работающее с MQTT-устройствами "осмысленно" (хотя, скорее всего, это проще будет реализовывать vdev-драйверами, использующими mqtt_mapping-устройства как подстилающие).

  • 05.10.2018: некоторые соображения общего характера о реализации и использовании MQTT в CX.
    • Надо не забывать, что хотелось кроме ДРАЙВЕРА сделать также cda_d_mqtt. И тут есть несколько аспектов:
      • Имена в MQTT -- case-sensitive (как и в EPICS, кстати).

        А 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....", и всё сработает как положено.

    • Клиентская библиотека, реализующая доступ к MQTT-брокеру, пока не найдена (точнее, не выбрана).

      И тут теоретически есть 2 возможности:

      1. Найти "библиотеку вообще", которая бы КАК-ТО умела работать с этим протоколом.

        Как с ней сдруживаться -- будет зависеть от её архитектуры.

      2. Найти библиотеку, умеющую выполнять сериализацию/десериализацию.

        Тогда ввод/вывод будем делать сами, по своему усмотрению.

    • А есть мыслишка попробовать нарисовать такую библиотеку (сериализационный вариант) самостоятельно -- протокол-то, как утверждается, несложный.

      Тогда она гарантированно сгодится и для драйверов (ВСЕХ вариантов), и для cda_d_mqtt, и для ЧеблоПаши.

      30.11.2019@8-ка-в-город ~11:00, едучи по Строителей: ага -- "libmqtt4cx".

    • А на fdiolib, случаем, не удастся реализовать? (Зависит от формата пакетов.)

      Чуть позже: нет, фиг -- не удастся. Дело в том, что длина пакета в MQTT не является фиксированным полем, а начинается со 2-го ([1]-го) байта пакета и имеет переменную длину, с кодировкой a-la UTF8 (младшие 7 бит используются как очередной компонент длины; старший же в значении 0 говорит, что на этом всё, а при =1 -- что дальше будут ещё байт(ы) длины).

    05.10.2018@вечер, дорога домой: а если ввести возможность "расширять" функционал fdiolib'а, как бы "плагинами"?

    • Ведь основную-то рутину -- кусочное вычитывание пакетов, пока не придёт сколько надо -- 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 сделаны.

  • 10.10.2018: кстати, ясно ж, что cda_d_mqtt.c будет делаться на основе cda_d_vcas.c, а mqtt_drv.c -- из remdrv_drv.c.

    Поскольку в обоих используется связь по TCP, с поддержкой реконнектов -- вот и возьмём готовую инфраструктуру, заменив протоколы (содержимое перегоняемых данных) и, вероятно, "мозги" определения адресатов для коннекта (в соответствии с "алгоритмом обнаружения брокера").

mqtt_mapping_drv:
  • 03.10.2018: это "драйверочек", в чём-то аналогичный remdrv: он не обладает никаким своим интеллектом касательно специфичного железа, а является лишь бриджом, пересылающим запросы в MQTT.

    Идея, лежащая в его основе, расписана в подразделе "MQTT" раздела "Знания" -- автоматическое составление карты MQTT-имён, "вытягивая" список имён из devtype'а и заменяя точки на слэши.

    Делаем покамест в sw4cx/drivers/ (хотя потом надо б будет куда-нибудь перенести, но куда (4cx/src/drivers/? hw4cx/drivers/? совсем отдельный проект?) -- неясно совершенно.

  • 03.10.2018: приступаем.

    03.10.2018:

    • Сделан скелет драйвера и базовое наполнение _init_d() -- создание таблицы маппирования на основе devtype.
    • Оно ОЧЕНЬ много лазит во внутренние структуры сервера.
    • И перед реальным внедрением нужно будет всё-таки переделать устройство cxsd_hw_channels[] в сервере -- со статического на аллокируемое. Т.к. с точки зрения синтаксиса C оно всё одинаково, но на уровне кода доступ разный, и когда изменение всё-таки произойдёт, то откомпилированный под старую модель драйвер просто перестанет работать.
    • Пришлось сделать собственную "GetDevInstname()", т.к. нужна, а в драйверном API её пока нету.

      16.02.2020@дома-воскресенье: она переехала в cxsd_driver.c.

    Теперь надо брать да проверять.

    04.10.2018: проверяем.

    • Было несколько дурных косячков -- исправлены.
    • Вытягивание карты работает.
    • В том числе взятие только ПЕРВОГО имени, указывающего на конкретный канал.

    Итого:

    • Исходная идея с автоматическим составлением карты -- работает.
    • А дальше надо уже добывать какую-нибудь MQTT'шную библиотеку и с ней работать.
    • И, возможно, задействуем auxinfo для указания адреса брокера.
4:
Modbus:
  • 21.06.2023: создаём раздел для записей о РЕАЛИЗАЦИИ драйвера для этого протокола.

    Сами записи делались ещё с 07-06-2023, но в другом месте (просто в конце файла), теперь же перенесём их сюда.

    Описания конкретных компонентов (драйверов и утилит) разместятся уже в своих подразделах.

    Замечание: сами компоненты будут жить, вероятно, в hw4cx/drivers/eth/ и, возможно, в hw4cx/srivers/serial/ (размещение внутри serial/ неочевидно), но обсуждение/описание будет тут, в собственном разделе.

Идеи и соображения:
  • 23.06.2023: раздел для записей общего характера о ПРИНЦИПАХ реализации. Сами записи были сделаны раньше, но сегодня сообразил, что их надо бы поделить на разные пункты, а коли так -- то нужен свой подраздел.
  • 07.06.2023@Томск-Беленца-6-23, вечер: насчёт реализации "общего" драйвера Modbus была идеологическая проблема: как реализовывать "сложные" каналы, вроде битовых полей, когда из ОДНОГО аппаратного значения надо вернуть значения во МНОГО каналов (как определять, в КАКИЕ?) и, аналогично, как делать композицию нескольких битовых каналов (КАКИХ?) в одно аппаратное значение.

    Так вот: идея -- в том, чтобы РАЗДЕЛИТЬ эти вещи на разные драйверы (да-да, в стиле EPICS, как и задумывалось 06-03-2014@лыжи (ну ПОЧТИ -- идеологически в ту же сторону)). А именно:

    • Непосредственно общение с Modbus -- один драйвер "modbus_tcp_drv", который работает ТОЛЬКО С ЦЕЛЬНЫМИ (16-битными) значениями.
    • А возню с битами реализовать в отдельных rdreg_drv и wrreg_drv, нацеливаемых на 16/32-битные каналы через cda-insrv.

    Некоторые заметки о планируемых деталях реализации:

    • Делать надо под Modbus-TCP, но стараться компоновать код таким образом, чтобы потенциально совпадающие/разделяемые с Modbus-RTU вещи были отдельными кусками для реюзабельности.
    • Явно надо применять sendqlib, т.к. запросы на чтение/запись каналов будут переть пачками, а их нужно сериализовать и отправлять следующий не ранее получения ответа на предыдущий.
    • Но вот ПОВТОРНЫХ запросов в TCP слать нельзя -- т.к. тут-то, в отличие от RS232/RS485/CAN, ничего потеряться не может (если уж соединение есть, то устройство живо).
    • И что будем делать?
      • Использовать _ONS -- нельзя: он отправится сразу и с концом.
      • ???????????

      18.06.2023: вот тогда вечером на Беленца-6 не записал полный список вариантов, а теперь уже и не вспоминается ничего, кроме того, что _ONS не катит...

    • Важно: в qelem'е должна храниться лишь ИСХОДНАЯ информация о содержимом пакета -- то, на основе чего пакет можно сконструировать (даже не просто {длина,данные} в готовом виде).

      Да, это будет занимать некоторое процессорное время при каждой отправке, но за счёт экономии памяти и упрощения кода: не понадобится ни держать элементы очереди до 256 байт, ни создавать отдельный memory pool для них. А "потери" производительности скорее теоретические: ведь бОльшая часть пакетов будет отправляться лишь единожды (при нормальной работе устройства) и не потребует повторного кодирования.

      ЗЫ: именно такое решение было когда-то в 1990-х принято в Netscape в отношении анимированных GIF'ов: там при каждом показе каждого кадра он заново раскодируется из исходника, а не декодируется всё сразу, Именно из соображений экономии памяти за счёт использования процессора. (Я тогда об этом где-то прочитал, сейчас уже и не вспомню, где именно; то были времена версии 2 или 3)

    • Драйвер-то один, а информация о конкретном типе устройства будет определяться devtype'ом экземпляра -- будет указываться что-то типа "DEVICE_TYPE_NAME/modbus_tcp".
    • Маппирование каналов на регистры -- указывать в drvinfo каждого канала, где как-нибудь указывать тип регистра и его номер.

      Как именно "как-нибудь" -- надо подумать над синтаксисом. Учитывая дурнодоспецифицированность Modbus и плоходокументированность конкретных устройств --

      • то ли указывать раздельно тип (PSP_FLAG'ом) и аппаратный адрес (PSP_INT'ом),
      • то ли позволить просто указание адреса в виде "42002", из которого по первой цифре догадываться о типе, а остальное считать адресом (13.06.2023: но из числа, как узнано сегодня, надо вычитать 1); однако в таком случае нужно всё-таки иметь возможность ТОЧНО указать адрес, например, отрицательным числом (13.06.2023: с другой стороны -- а зачем? раз можно просто указать после "4" аппаратный адрес плюс 1). 16.07.2023: а затем, что в некоторых описаниях -- вроде РПМ-416 -- указываются именно реальные адреса, и лучше прямо их и указывать в конфигах, без дурацких лишних "плюс 1".
    • К вопросу "а как производить обратный маппинг пришедшего ответа с номера регистра на канал?": да очень просто --
      1. Надо в момент ОТСЫЛКИ пакета запоминать в privrec'е исходную информацию (которую также помещать и в егойный NNNqelem_t), в которой указывать номер исходного канала (точнее, его индекс в таблице).
      2. Но по приходу ответа всё же ОБЯЗАТЕЛЬНО надо сравнивать его (код команды и адрес) с ожидаемыми -- т.к. может быть ситуация, когда ответ уже пришёл, но из-за задержек scheduling'а таймаут отработался раньше, пакет из очереди выкинут и отправлен следующий, и тут вдруг будет вычитан ЭТОТ, а он-то не соответствует последнему отправленному.

      13.06.2023: всё-таки фигово получается с таким подходом: если пакет почему-то пропадёт (устройство не отреагировало? или вышеописанный таймаут), то связанный с ним канал НАВЕКИ останется запрошенным-и-неотвеченным...

      18.06.2023: придумано решение для таймаутов -- "ГИБРИДНЫЙ режим", когда при несоответствии пришедшего пакета ожидаемому всё же выполняется поиск канала-получателя.

    • Из этого подхода сразу же напрашивается решение проблемы "а как работать с каналами, отражающимися на НЕСКОЛЬКО аппаратных регистров (int32 - 2шт, float64 - 4шт)?": указывать в drvinfo вместе с адресом ещё и опциональный формат -- просто uint16 (умолчание) или какой вариант кодировки из скольки последовательных регистров (13.06.2023: можно взять из EPICS'а, раздел "Modbus register data types").
    • 13.06.2023: гуглением по "modbus endianness" нашлось "Common Modbus Protocol Misconceptions" с парой важных вещей:
      1. "Регистр 41001" -- это holding register 1001, но АДРЕС регистра -- 1000, т.к. нумерация в документах "one-based", а аппаратная в реальности -- "zero-based".
      2. Modbus -- изначально BIG-endian (номера и значения 16-битных регистров).

    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".
    • Исполнять должна ЦЕПОЧКУ команд; синтаксис вызова --
      modbus_test DEVICE_SPEC COMMAND...
    • Оный DEVICE_SPEC должен мочь поддерживать все возможные спецификации для всех вариантов Modbus'а -- как TCP, так и по RS232/RS485, ...
    • ...и для разных вариантов serial-link'ов можно влинковывать туда "модули" доступа к портам (com, mxupcie, usb), ...
    • ...для чего эти "модули", которые изначально являются симлинками на stdserial_hal.h и потому содержат внутри одинаковые имена вроде serial_hal_opendev(), надо оборачивать в какие-то файлы.
    • А вот функциональность "заворачивания данных в пакет" должна быть общей с драйвером (какой-то общий .h-файл?).
    • Но ДЕШИФРИРОВАТЬ ответные пакеты он должен будет уметь сам, чтобы красиво печатать данные из них.

    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, на аспирантских защитах: а если подумать, то можно обойтись и БЕЗ внешних драйверов: раз уж всё равно будет карта каналов со "свойствами", то ничто не мешает среди свойств, наряду с форматом/кодировкой также указывать опциональный "маппинг на каналы":

    • Один-в-один -- по умолчанию.
    • "Битовый набор" -- входной либо выходной регистр отображается на 1+N последовательных каналов, первый из которых является всеми битами, а следующие N -- побитовое представление.

      При этом указывается такое свойство только ПЕРВОМУ каналу, а остальным ("зависимым") автоматом прописывается номер "базового" и их порядковый номер бита, чтобы при обращении к ним правильно вызвать чтение либо запись (в соответствующий бит) базового.

    Впрочем, это не запрещает изготовить отдельные rdreg_drv и wrreg_drv.

    18.06.2023@Ключи, гуляя в домике и под навесом, ~14:00 : рассуждения на тему "да как же всё-таки отправлять пакеты sendqlib'ом, чтобы и повторную отправку по TCP не делать, и чтоб таймауты обнаруживать?".

    Общий вывод такой:

    • Некоторую паузу "молчания" после отправки пакета выдерживать нужно -- чтоб устройство успело ответить (а не заваливать его толпой пакетов в надежде, что оно разберёт их все последовательно).
    • Но уж если ответа так и не пришло -- тогда всё, ЭТОТ пакет больше не отправлять никогда: ответа может не быть либо
      1. из-за проблем со связью -- и тогда либо он дойдёт и будет отвечен ПОТОМ, либо связь порвётся и всё сначала,
      2. либо потому, что устройство этот пакет воспринимать не хочет (и ошибку на него прислать не соблаговолило, что хоть и против правил, но возможно) -- и тогда на повторные ответа всё равно не будет, так что повторять бессмысленно.

    Так что --

    1. Должен прокатить вариант "_ONS" с принудительным таймаутом, например, в 1 секунду (уж такого точно хватит), а после получения ответа делать 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 :-(.

    2. ...а в "реализаторе" таймаута уж подтирать информацию о последнем отправленном (command=-1?), чтобы считалось "ничего не отправлялось".

    Из написанного выше видно, что всё же крайне желательно, чтобы пакеты ответов, НЕ соответствующие последнему отправленному, всё же обрабатывались, а не просто отбрасывались. Это решит проблему "из-за тормозов 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, рассчитывать-то надо на возможность полного спектра, для чего все части кода стараться делать максимально изолированно/модульно, чтоб реюзабельно).

    • Т.е., похоже, надо "работу с TCP-соединением" делать как бы отдельным независимым модулем внутри драйвера/layer'а, чтобы оно годилось и для Modbus-TCP, и для Modbus-RTU-over-TCP.
    • А вообще получается примерно такой набор вариантов, которые могут существовать (да-да, и Modbus-ASCII тогда уж тоже, гулять так гулять, его добавление на общем фоне даст незаметную прибавку трудоёмкости, а полноты и структурированности может добавить): TCP, RTU-over-COM, ASCII-over-COM, RTU-over-TCP, ASCII-over-TCP.

      (Тут "COM" -- это потенциально любой из ныне поддерживаемых в hw4cx/drivers/serial/ вариантов: com mxupcie usb (Advantech?).)

      23.06.2023@утро, записывая вчерашние идеи про унификауию Modbus-ASCII с -RTU: очевидно же, что надо просто разделить на 2 параметра: 1) формат данных (способ кодирования/декодирования пакета) и 2) способ соединения (коннект через сокет или COM-порт). Ну да, получился заодно бессмысленная комбинация "формат TCP через COM-порт", но и пусть -- вреда от неё нет.

    • Кстати, давно уже ясно, что и при работе с COM-портами надо уметь пере-открывать их в случае "read()==0", что является аналогом реконнекта, так что оная технология будет всеобщей.
    • @вдоль-девятиэтажки, выйдя из Фермер-центра и идя домой с продуктами: а не пригодится ли тут давно поддерживаемый, но пока не реализованный вариант "DEVICE_TYPE/modbus@LAYER"? Чтоб драйвер "modbus_drv.c" вообще был один, а специфику установления соединений и упаковки/распаковки данных держать в layer'ах?
    • @дома, записывая это всё: можно, но неприятно то, что потроха-то у всей этой толпы вариантов по сути очень похожи и НЕ разделяются на "слои", которые можно складывать в нужном порядке "в вертикальные стопки", а скорее это набор техник, комбинируемых более сложно ("горизонтальные комбинации"? "клубки-переплетения"?).

    21.06.2023@утро, мытьё посуды: насчёт векторов.

    Исходные данные:

    • Мы всё равно собираемся в перспективе поддерживать данные размером более 16 бит (32 и 64), для чего запрашивать чтение более 1 регистра (2 и 4 соответственно).
    • Есть ограничение на число читаемых одним пакетом регистров -- чтоб пакет не превышал 255 байт.

    Откуда пара идей:

    1. Разрешить также читать и ВЕКТОРЫ.

      Число элементов можно указывать в drvinfo, а можно и брать от CxsdHwGetChanType().

    2. А можно поддерживать и вектора длиннее, чем влазит в пакет: разбивать запись на несколько пакетов (совершенно очевидно), а чтение выполнять также "слайсами", аналогично тому, как делается в ottcam_drv с получением кадра -- возвращать данные канала тогда, когда получены ВСЕ слайсы.

    Тут, конечно, в обоих случаях возникнут некоторые нетривиальности:

    1. Поскольку даже для "коротких" (влазящих в пакет) векторов фиксированного буферочка uint8[8] в qelem'е не хватит, а держать всегда буфер uint8[252] -- дикий расход, то придётся отдельно аллокировать буфера для таких каналов.

      ...можно аллокировать одном общим блоком, а в свойствах каналов прописывать offset'ы в нём.

    2. Для "больших" векторов (требующих нескольких пакетов) понадобится также место для хранения:
      1. Данных, запрошенных сервером к записи.

        ...хотя тут вопрос о взаимоотношениях этих данных и тех, что хранятся в буферах для отправки: можно же иметь один ЭТОТ буфер, а при формировании пакетов на отправку брать данные прямо из него, с соответствующим для пакета-слайса смещением.

        Но и в таком случае возникает вопрос о том, ЧТО хранить в таких буферах: исходные данные (от сервера) или уже должным образом сконвертированные в байты, готовые к отправке устройству.

      2. Принимаемых данных -- ведь получаемые слайсы нужно куда-то складывать, чтобы потом уже делать ReturnDataSet().

      Напрашиваются правила:

      • при складировании в буферы отправки данные сразу конвертируются в байты для устройства;
      • при получении слайсов от устройства данные в буфер-для-возврата складываются уже конвертированные.

      Так выглядит наиболее "правильно" -- что конверсия происходит в момент получения данных от одной стороны сразу в формат для другой.

    22.06.2023@дорога домой с обеда в Гусях, проходя мимо Алекты по кривым бетонным плитам: если ASCII отличается от RTU только тем, что каждый "байт" в пакете занимает по 2 символа, то

    • Имеет смысл работу с этими двумя подвариантами максимально унифицировать, а конкретно --
      1. Буфер иметь двойного размера -- 512 вместо 256;
      2. Формировать пакет RTU, а потом его "растягивать" -- идя с хвоста (чтобы не портить данные), превращать каждый байт в 2 символа.
      3. И с получаемыми пакетами аналогично -- идя уже с начала, превращать каждые 2 символа в 1 байт.
      4. 23.06.2023: вот только ещё лишние ':' в начале и '\r\n' в конце есть.

      Таким образом, различаться будет только приём/передача, причём минимально, а собственно РАБОТА с данными будет одинаковой

      23.06.2023: а вот контрольные суммы считаются по-разному.

    • 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'ы? Учитывая, что он ОДИН -- может, прямо в него и интегрировать всю логику работы с "портами"?
    • 26.06.2023@пешком домой из ИЯФа после планёрки, около ИПА, ~11:00: неа, лучше -- всё же отдельные layer'ы. Так проще реализовать работу с разными типами COM-портов; называть их, например, "modbus_via_NNN".

      А раз layer'ы становятся обязательны, то для конкретно TCP -- layer-заглушка, с именем вроде "modbus_via_tcp".

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

    25.06.2023@пешком в Ярче после гуляния с Женьками, проходя вдоль Терешковой-24: учитывая характер протокола/трафика Modbus, просто напрашивается делать оптимизации -- если спрашивают какие-то каналы, то слать не раздельные команды чтения, а одну групповую на все и потом дешифрировать ответ.

    Но вот КАК устраивать такое группирование -- вопрос.

    • То ли в auxinfo указывать "периодически шли такие-то команды группового чтения".
    • То ли в drvinfo указывать одним каналам ("лидерам групп"), что читать надо сразу не 1 ячейку, а больше, и последующим каналам прописывать "запрос выполнять через канал-лидер номер такой-то" (и вот это прописывать можно уже автоматически, не требуя ручного указания).
    • То ли программно обнаруживать такие "группы" -- либо при инициализации драйвера, либо динамически по приходящим запросам (последний вариант выглядит диковато и напоминает работу современных CPU со всякими предсказаниями по статистике).

      26.06.2023: и для такого программного обнаружения может понадобиться возможность явного указания "не включая такой-то канал в группу" -- мало ли какие особенности работы устройства, может, что-то надо сильно индивидуально читать.

    • 26.06.2023 ЗЫ: а ПЕРИОДИЧЕСКИЙ опрос чего-нибудь не понадобится ли делать? Ведь многие PLC при отсутствии трафика в течение 10-30 секунд рвут соединение.

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

      26.06.2023@~11:20, дома, вернувшись из ИЯФа с планёрки: либо, как вариант -- у некоторых каналов в drvinfo указывать флажок "опрашивать периодически", тогда они чтоб добавлялись в некий список плюс им чтоб уставлялось IS_AUTOUPDATED_YES.

    26.06.2023@утро, записывая предыдущий вчерашний пункт: кстати, при той оптимизации и процедура дешифрирования пакета ответа ("при несоответствии последнему запрошенному") должна быть весьма нетривиальной: надо не просто сопоставлять пакет каналу, а ПОШТУЧНО регистры анализировать, какой к какому каналу относится -- ведь в одном групповом ответе может быть МНОГО каналов.

    Вообще вся эта "групповуха" требует весьма некислых трудовложений.

    1. Учитывая слабораспространённость У НАС Modbus-устройств (и слабую нагрузку), представляется разумным ЗАБИТЬ на все эти оптимизации, оставив их лишь тут в записях в виде теоретических рассуждений,
    2. а реализовывать драйвер по-простому -- запросы/ответы индивидуально по каналам (если понадобятся вектора -- то ещё и "сегменты" добавить.
  • 11.06.2023: раздельчик для обсуждения проблемы "при использовании rdreg_drv/wrreg_drv затрагиваемые int32-каналы будут опрашиваться ПОСТОЯННО, а не при наличии клиентов".

    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_d_cx

    Оный флаг CDA_DATAREF_OPT_NOMONITOR

    cxlib

    Вызов cx_rq_rd() уже присутствует -- так что тут всё готово.

    cxsd_fe_cx

    Режим 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 отсутствует), ...
    • ...а приходящие запросы на чтение также не приведут к выполнению чтения -- т.к. каналам записи (и AUTOUPDATED!) эти запросы НЕ передаются.

    Т.е., 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(), только тут есть пара нюансов:
      1. Делается проверка с целью предсказать "будет ли реакция сервера на этот запрос чтения, или же данный канал автоматический/internal/rw и запрос будет проигнорирован" -- посредством IsAnUpdate(), скопированной сюда из cxsd_fe_cx.c.

        И в случае игнорирования -- просто СРАЗУ отправляется ответ (аналогично тому, как поступает cxsd_fe_cx.c в ответ на CXC_CH_RQRD).

      2. И таковая "отправка" делается именно отправкой -- т.е., не вызов cda_dat_p_update_dataset() (что могло бы дать бесконечную рекурсию), а "генерится событие обновления", отправкой значения hwr в уведомительный pipe.
    • Дальше занялся cda... там облом -- косяк давнего времени, ещё от 2020-го, с ioctl()'ами.

    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().
    • Давно гуляла в голове мысль, что как раз EPICS-то имеет вариант "прочитай значение по запросу", так что туда эта модель req_read() прекрасно ляжет.
      • Только имелись сомнения -- а не надо ли там сразу указывать место, куда положить результат, как точно сделано у дурацкого ca_array_get()? (Точно НЕ надо указывать -- для подписки, хвала аллаху.)
      • Проверил -- нет, у ca_array_get_callback() такая дурь отсутствует.
      • Более того: у нас этот вызов УЖЕ используется для чтения свойств посредством GetPropsCB().
      • Так что сделано, причём в качестве callback'а указывается ровно тот же NewDataCB().
      • 08.07.2023: только одна проблема: судя по CAref.html, тут НАДО указывать конкретное значение в количестве элементов 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.".

      • 10.07.2023: а ещё ведь тут игнорируется флаг "NOMONITOR". Хотя достаточно просто НЕ заказывать подписку.

        Сделано. Также, поскольку подписка делается не только из cda_d_epics_new_chan(), но и из StateChangeCB(), значение options теперь сохраняется в свежевведённое поле в hwrinfo_t.

        А для "PASSIVEMON" ничего особо делать не надо -- EPICS'ный IOC и так работает по этой модели (опрос он делает САМ, а не при наличии клиентов).

    Итого: инфраструктура "запросить чтение" вроде сделана, теперь надо реализовывать режим PASSIVEMON.

    09.07.2023: приступаем...

    • cx_proto_v4.h -- добавлен код CX_MON_COND_PASSIVE=3, а нереализованная CX_MON_COND_ON_DELTA передвинута на =4.
    • cxlib.h -- синхронизованный CX_UPD_COND_PASSIVE=3
    • cxlib_client.c -- добавлена трансляция из первого во второй в cx_ch_open().
    • cxsd_fe_cx.c -- в нём будет самое сложное, так что сходу сделано лишь самое очевидное: CHNEvproc() по событию _R_UPDATE для PASSIVE-мониторов просто выполняет отправку.

    Пока НЕ сделано:

    1. Основные "мозги" в cxsd_fe_cx.c;
    2. Вообще НИЧЕГО в cda_d_insrv.c -- но этот и NOMONITOR не понимает (не говоря уж о корректной обработке режимов ON_UPDATE/ON_CYCLE -- см. "обсуждение с анализом" за 12-05-2015 и "забавный факт, который надо принять во внимание" за 14-10-2018).
    3. Также пока ничего со стороны cda -- ни _core, ни _d_cx.

    10.07.2023@утро, после планёрки, которая не состоялась: некоторые размышления, начавшиеся ещё вчера:

    • Проект введения нового флага (давно имеющийся в голове, но до сих пор не записанный) состоит в том, чтобы добавить ещё один CDA_DATAREF_OPT_-битик, и вместе с нынешним NOMONITOR они составят 2-битовый код "условие обновления", который по умолчанию -- ==0 -- означает "присылай обновления", поодиночке взведённые битики будут означать два варианта с ограниченным мониторированием, а ещё остаётся один пока незадействованный код (когда оба битика горят).
    • НО: формально-то у нас УЖЕ есть 2 бита "вида обновления" -- ON_UPDATE и NOMONITOR, которые сейчас используются лишь в 3 комбинациях, а вариант ON_UPDATE|NOMONITOR" -- нет, по причине некоторой бессмысленности.

      В принципе, можно как раз эту комбинацию и задействовать.

      (Такая мысль появилась при взгляде на табличку cda_d_cx.c::mode2upd_cond[], где CX_UPD_COND_NEVER присутствует дважды.)

    • Но лучше всё-таки оставить ON_UPDATE независимым.

      Такая мысль появилась уже при взгляде на комментарии к определениям 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():

    • cdaP.h --
      • Вводим тип 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_core.c::cda_req_ref_read() -- теперь прямой вызов метода.
    • cda_d_cx.c, cda_d_insrv.c и cda_d_epics.c подправлены соответственно: в метрику добавлены cda_d_*_req_read() и пара NULL, NULL в конце, а cda_d_*_chan_ioctl() удалены.
    • Прочие dat-plugin'ы -- cda_d_dircn.c, cda_d_local.c, cda_d_v2cx.c, cda_d_vcas.c, cda_d_tango.cpp -- просто получили дополнительные NULL, NULL в конец метрики.

    11.07.2023: возвращаемся к реализации пассивного мониторирования, пока что со стороны cda.

    • cda.h -- добавлена CDA_DATAREF_OPT_PASSIVEMON=1<<19 плюс зарезервировано CDA_DATAREF_OPT_rsrvd18=1<<18 (чтоб было 3 последовательных бита на "режим мониторирования", дающие потенциально 8 вариантов, из которых сейчас задействовано 3).
    • cda_core.c::cda_add_chan() --
      1. Добавлено пропускание нового флага к dat-plugin'овым методам new_chan().
      2. Но ранее модифицирована реакция на CDA_CONTEXT_OPT_IGN_UPDATE: теперь не просто взводится CDA_DATAREF_OPT_NOMONITOR, но ещё и нулятся CDA_DATAREF_OPT_ON_UPDATE | CDA_DATAREF_OPT_PASSIVEMON.
    • cda_d_cx.c --
      • Добавлен битик 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. Учёт добавлен, проблема ушла.

    • cxsd_fe_cx.c -- потенциально самое сложное.
      • @Ключи, бродя взад-вперёд, ~17:00: но ведь по сути всё, где надо внести изменения -- это то, что касается per_cycle_monitors_count, так? Тогда всё становится не так уж сложно!
      • @вечер, ~18:00+: делаем по той идее. Да, в принципе, вариант рабочий, но всё же не совсем.
      • А реально надо было добавить CX_MON_COND_PASSIVE равнозначной "парой" к CX_MON_COND_NEVER. Всего 5 точек, включая проверку на допустимость значения cond в GetChnd(), ...
      • ...но КРОМЕ селектора "что делать по _R_UPDATE в зависимости от 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 давно бродили мысли, но тут наконец в голове склалось определённо:

    • При ДОБАВЛЕНИИ нового hwr'а -- надо его просто добавлять в periodics[] сразу, как уже и делается.
    • А вот УДАЛЯТЬ -- чего сейчас не делается вовсе -- механизмом, аналогичным cxsd_fe_cx.c'шным: завести флажок 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 оно очевидным образом пробралось. Так что есть откуда копировать готовый код, только с УСЛОВНЫМ помещением в список.

    Делаем:

    • Добавлено lcninfo_t.periodics_needs_rebuild.
    • И hwrinfo_t.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, поэтому...

      • в lcninfo_t добавлено поле 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() -- вроде работает как надо.

5:
modbus_tcp_drv:
  • 21.06.2023: подраздел создан, хотя сами работы начались ещё несколько дней назад.

    30.07.2023: A BIG FAT NOTE: ведь надобность в layer'е определяется не типом линка (serial/TCP), а ПРОТОКОЛОМ -- для rtu_over_tcp ровно так же понадобится уметь работать с ТОЛПОЙ адресов на одном сокете.

    Так что потенциальное взаимодействие с потенциальными layer'ами ("modbus_lyr.h?) пока не вполне ясно.

  • 19.06.2023: потихоньку приступаем к созданию общего драйвера, реализующего Modbus-TCP, берущего информацию о каналах из drvinfo.
    • Файл modbus_tcp_drv.c создан копированием из sw4cx/drivers/modbus_tcp_rb_drv.c,
    • ...а помещён он в hw4cx/drivers/eth/.

    22.06.2023: продолжаем.

    • Скелет блока работы с sendqlib'ом берём из triadatv_um_drv.c -- на текущий момент их privrec'и просто идентичны.
    • Но потом добавляем last_sent, и различие появилось :)
    • "Карту" каналов решено аллокировать единым блоком с privrec'ом, "вручную" (как делает remdrv_drv.c для drvletinfo[]), а не отдельным куском, как в подобных, для чего она объявляется в конце privrec'а как map[0].
    • Проход по списку каналов с подсматриванием в drvinfo взят из bridge_drv.c.

    16.07.2023: нашёл руководство по РПМ-416 (это "Анализатор электросети (регистратор) РПМ-416", который надо окучить), и там явным образом присутствуют каналы типа "ULONG" -- т.е., 32-битные (причём используемый там порядок байт не указан).

    Так что конверсия потребуется прямо сейчас, а не "когда-нибудь потом".

    17.07.2023: конверсию решено вытащить в отдельный модуль, сейчас названный "modbus_conv". О нём -- в его собственном разделе ниже.

    21.07.2023: изучал устройство/функционирование "1-битных" сущностей -- coils и discrete inputs, и выводы следующие:

    • Главное -- они действительно позволяют индивидуальную 1-битную адресацию, в т.ч. не только чтение, но и запись одиночного бита.

      Поэтому:

    • Никакая "конверсия" тут смысла не имеет. Поэтому:
      • Надо ВСЕГДА делать преобразование "1 бит Modbus-данных соответствует 1 штуке int32".

        Да, возможно, что в каких-то экзотических случаях понадобится читать 16-битные значения и рассматривать их именно как многобитовые, с какими-то преобразованиями, но это пусть будет делаться на уровне сервера и обработки данных.

      • В утилите командной строки же не требуются никакие DPYFMT -- просто всегда 0 или 1, пачки которых -- через запятую.
    • Функции 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'ов, приводящую к вызовам опять в этот же драйвер для этого же устройства и даже для этого же канала, а там уж не дай бог будет использоваться одно место в памяти для чтения и записи -- гарантированная порча данных).

    • Поэтому надо высчитывать для каждого канала требуемый объём памяти для складирования в формат m_data и в host-side-data, и аллокировать его в privrec'е.
    • А отсюда сразу следует, что надо проход по списку каналов делать в ДВА прохода -- на первом проверять синтаксис и подсчитывать требуемые объёмы, а после него аллокировать privrec и на втором уже заполнять map[].
    • ...и в privrec'е же надо будет хранить список "периодических запросов", ...
    • ...ради чего и парсинг той части auxinfo, что после адреса устройства, также выполнять в два прохода там же.

    24.07.2023: пока начинаем работать над парсингом drvinfo в текущем варианте цикла.

    25.07.2023: код парсинга утащен в отдельную modbus_conv.h::modbus_parse_spec() -- собственно, изначально планировалось именно так.

    27.07.2023:

    • Достигнута первоначальная компилируемость и в hw4cx/drivers/eth/Makefile драйвер добавлен.
    • Начато наполнение ProcessInData().
    • Добавлена инициализация/деинициализация очереди. Формально ничего сложного (хотя изначально почему-то было забыто), но главное -- в параметрах, которые надо было корректно выбрать.

    28.07.2023: пора активно окучивать ProcessInData().

    Соображения/наблюдения:

    • Невозможность из ответного пакета восстановить ключевую информацию пакета запроса -- дикая дичь и проблема.
    • У нас уже был аналогичный случай -- КШД485/ПИВ485; там даже хуже -- не просто "какой-то" информации нет, а и самого кода команды нет в ответе.

      И да, в piv485_lyr_common.c в качестве "кода ответного пакета" используется именно "запомненный код последнего отправленного" -- pivdevinfo_t.last_cmd -- ВСЕГДА, без вариантов.

    • Так что да -- тут без вариантов придётся лазить в last_sent для восстановления отсутствующей в ответе информации.
    • Вот только по задуманной 07-06-2023 в Томске модели ("в qelem'е должна храниться лишь ИСХОДНАЯ информация о содержимом пакета -- то, на основе чего пакет можно сконструировать") у нас в 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 из сравнения убраны.
    • ...и встаёт вопрос -- а реально ли нужно в qelem'е поле 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() переведена на него.
    • ...а в privrec_t добавлено поле 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-битовых каналов. Тезисно:
      • Возвращаются в виде UINT8.
      • Расшифрованные данные помещаются в 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
    
    на
    union {                  // A weird yet portable way to force alignment
        float64    f64;
        uint8      u8[2008]; // For maximum number of coils/discretes to fit
    }          buf;
    
    -- чтобы гарантировать выравнивание даже для 64-битных данных. Из-за этого ссылки/обращения пришлось переделать с просто "buf" на "&buf".

    08.08.2023: первая проверка на "живом" девайсе.

    • Сначала был косяк в DecodeModbusPacket(), где по ошибке стояло требование "nbts должно быть нечётным" (видимо, я при написании той строчки счёл, что сам байт nbts входит в этот объём). Исправлено на "nbts должно быть чётным" -- и данные начали приходить!!!
    • Но обнаружился другой косяк: для TCP отправка делается _ONS с таймаутом 1s, и по получению ответного пакета этот таймаут НЕ снимается, а выдерживается пауза 1 секунда до отправки следующего пакета.

      ...т.к. в 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: продолжаем.

    • По утреннему рецепту перейдено с _ONS+q_sendnext() на _INF+esn_pkt() и таймаут 60 секунд -- проблема ушла, работает резвенько.

      10.08.2023@дорога в ИЯФ, лесок между П28 и НИПС: вот только будет косяк с устройствами, которые не отвечают на не нравящиеся им пакеты -- оно будет зависать на 1 минуту до отправки следующего. Так что надо б что-то другое придумывать -- в идеале всё-таки научиться снимать таймаут перед sendnext'ом.

    • Сделана обработка exception'ов.
      • "Умничанье" делается только при наличии текущего канала-адресата, т.е., last_sent.chan>=0.
      • Первым делом производится ReturnOneRflags() со значением флагов в зависимости от кода exception'а.
      • Затем если это ошибка на команду записи и конкретно ILLEGAL_DATA_VALUE, то ставится в очередь команда чтения с теми же (kind,addr,num1) -- смысл в том, что раз уж запись обломилась из-за "неправильного" значения, то попробовать прочитать реально имеющееся.

        Нюанс 1: ставится с условием SQ_IF_ABSENT, а даже не NONEORFIRST -- ведь нас устроит даже если первое в очереди, т.е., уже только что отправленное (а в основном так оно и будет автоматом, т.к. после записи будет ставиться обратное чтение).

        Нюанс 2: для WRITEONLY-каналов (см. ниже) оно НЕ запрашивается.

      • Флаги в зависимости от кода exception'а: определяются таблицей трансляции ex_r2rflags[], где ILLEGAL_FUNCTION => CXRF_CAMAC_NO_X ("не исполнено"), ILLEGAL_DATA_ADDRESS => CXRF_UNUPPORTED ("нет такого канала"), ILLEGAL_DATA_VALUE => CXRF_INVAL (неверное значение), а всё остальное -- CXRF_CAMAC_NO_Q ("нет подтверждения от устройства"). Подобрано худо-бедно логично.
    • Реализована поддержка write-only-каналов.

      Занадобилась она потому, что даже в РПМ-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 -- при этом просто делается drvinfo++, а до modbus_parse_spec() оно не доходит (ибо там этому делать нечего).

          02.10.2023: такой вариант убран, т.к. уже есть более общий суффиксом "(writeonly)".

      • Использование:
        • По DRVA_READ попытки запросить чтение НЕ делается, а сразу же делается ReturnOneRflags(,,,CXRF_CAMAC_NO_Q), ...
        • ...и по DRVA_WRITE также НЕ запрашивается обратное чтение сразу после записи (которой самой ещё нет :D).
        • Ну и по exception'у (см. выше) ILLEGAL_DATA_VALUE также не запрашивается вычитывание реально-присутствующего-текущего значения.
      • Что ещё НАДО будет сделать, но пока НЕ сделано: по подтверждению записи устройством (в момент получения от него "положительного" ответа на пакет записи) возвращать записанное "сверху" значение.

        Для этого понадобится буфер для хранения этих данных, а таковой буфер можно будет организовать только после перехода на 2-стадийный парсинг, чтобы аллокировать всё одним куском памяти.

    • Дважды упомянутая выше ReturnOneRflags() предназначена для возврвта флагов наиболее "аккуратно".
      • Выбирает нужный тип: для каналов, связанных с 16-битовыми регистрами, берёт из modbus_conv_table[conv].ext_dtype; для 1-битовых -- UINT8.
      • Подбирает nelems: для скаляров вертает 1 элемент, для векторов 0.
      • Собственно значение -- нулевое.
      • Сам возврат -- танцы с бубном вокруг ReturnDataSet() -- слизан с ReturnOneChan().

    ..........

    • exception'ы: умничать только для last_sent.chan>=0; флаги возвращать NO_X,NO_Q,INVAL для разных кодов (ILLEGAL_DATA_ADDRESS:UNSUPPORTED); для скаляров вертать 1 шт, для векторов пустоту (нужного типа!); для ошибок записи слать запрос на чтение, но только для не-WRITE-ONLY-каналов и по условию SQ_IF_ABSENT (т.к. даже если первое в очереди, т.е., уже только что отправленное, то нас устроит).
    • проверить конверсию STRING, не забыв посмотреть EPICS'ную

      Работает. И в EPICS делается ровно то же, проверено сравнением кода на тему "какой байт/байты берётся в каком случае".

    • проверить конверсию FLOAT64, симуляционной записью предусмотренных байтов.
    • Добавить ФЛАГИ к map'у -- чтоб WRITE-ONLY-каналы понимать и не путаться читать: по чтению вертать NO_Q, по записи по ПОДТВЕРЖДЕНИЮ отработки возвращать записанное значение, и НЕ пытаться после записи слать чтение.

      Вопрос только, КАК указывать это write-only в drvinfo. Ведь оно нужно ТОЛЬКО для драйвера, modbus_mon'у нафиг не надо, так что в modbus_parse_spec() этому делать нечего. Символ-префикс ('w'? '/'?), который указывать первым символом в drvinfo?

    • А вообще пора уже приступать к 2-стадийному парсингу auxinfo, чтоб аллокировать все нужные буфера скопом, и мочь исполнять чтения периодически, а также чтения и записи "on_connect".

    11.08.2023: переходим на 2-стадийный парсинг конфигурационной информации.

    • Кстати, в "первенце интроспекции" -- mqtt_mapping_drv.c -- также парсинг в 2 стадии, но там потому, что объём требуемой для списка имён памяти узнать можно лишь после прохода по списку всех каналов.
    • Начальная трансформация:
      • Организуем цикл по stage, ...
      • ...вносим в него по-канальный парсинг (ставя условием заполнения me->map[chn] "stage > 0") и...
      • ...аллокирование privrec'а в КОНЕЦ цикла, "at the end of stage 0".
    • Парсинг auxinfo:
      • Сделано "выкусывание" имени хоста из начала auxinfo, плюс добавлен парсинг опционального :PORT -- то и другое взято из remdrv_drv.c::remdrv_init_d().

        Резолвинг делается только на stage==0, НЕ внутрь me, а в локальную переменную hostname, которая в конце 0-й стадии вместе с прочим сохраняется в свежеаллокированный privrec.

      • А далее -- цикл парсинга оставшегося.
    • О хранении:
      • Для хранения введён простейший тип mreqinfo_t, содержащий kind,op,addr,num1.

        Т.е., в нём НЕ ПРЕДУСМОТРЕНО пока ничего на тему записи (каковая может быть интересна для "on_conn=" -- записью волшебного кода в регистр-пароль разлочивать возможности управления).

      • И в privrec добавлены указатели on_hbt и on_conn (которые будут указывать на соответствующие куски в общем буфере), с количествами on_hbt_count и on_conn_count.
    • Аллокирование (13.08.2023: реально доделано только сегодня):
      • Принято решение решать проблему выравнивания разнородных буферов внутри общего блока "по-простому" -- размер каждого добивать до кратного 16, посредством "(... + 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[] запись пока никак.

    • Настало время заняться утилиткой modbus_mon, и вот на ней можно обкатать запись -- там это проще.

      14.08.2023: с другой стороны, инфраструктура-то для по-канальных буферов для записи уже подготовлена -- можно делать.

    ..........

    • Проверить exception'ы.
    • DATA64
    • trans_id -- доделать
    • Парсинг hbt_period= on_hbt= on_conn=
    • Обобществить вычисление num1 из nelems и его ограничение
    • Сделать скрин.
    • 15.08.2023: надо печатать предупреждения при дубликатах drvinfo. Но вот где...

      (Задача возникла после того, как по ошибке указал в devtype НЕСКОЛЬКИМ именам одинаковые номера каналов -- так возникли дубли, из-за которых каналам указывались не те адреса регистров (приоритет имели те, что позже).)

      Посмотрел я на то, как устроено всё с dcpr'ами (сохранение в них указанного в devlist'е, а затем актуализация в hw) -- а никак! Там, конечно, всё сделано элегантно на грани гениальности:

      • Сами dcpr'ы к каналам не привязаны.
      • А просто заполняются и потом строкам в namespace'ах прописываются ссылки на dcpr_id (группам имён, вроде "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() -- их можно объединить в один цикл, но придётся передавать слишком много параметров.

    • 16.08.2023: замерить производительность на максимальных скоростях -- с hbt_period=1000 заказывать 108 регистров и потом 2 регистра и сравнить герцы -- и записать за 15.08.2023, когда первоначальные измерения и были сделаны.

    14.08.2023: улучшаем парсинг auxinfo-параметров:

    • Теперь после HOST:PORT там идёт не просто список спецификаций для on_hbt[], как было сделано вчера, а список KEY=VALUE, чтоб можно было указывать разные сущности.
    • Собственно разбор пока примитивнейший -- после пропуска isspace()'ов просто делаются сравнения с предопределёнными строками вида "keyword=" (да-да, прямо с '=' внутри).
      • Во-первых, "hbt_period=" -- так указывается период в микросекундах, вместо умолчательного 1*1000*1000.
      • Во-вторых, "on_hbt=" -- это необходимый теперь префикс для периодичностей.
      • А вот "on_conn=" пока отсутствует (записи-то всё равно нет, а без неё особо незачем).
    • Парсинг периодичностей, кстати, дополнен превращением указанного "/COUNT" из "nelems" в num1, посредством надлежащей арифметики с conv'овыми num_cells и ext_per_cell -- актуально в случае указания не просто регистров и количества, а сразу в терминах "параметров" с конверсией; не забыто и ограничение по modbus_max_num1().

      Сама эта арифметика скопирована из парсинга drvinfo, и это не есть гуд -- надо бы обобщить, убрав дублирование.

    15.08.2023@прогулка: насчёт раздербанивания на биты прямо в драйвере, как предположено 15-06-2023:

    • А как "подчинённым" указывать base?
      1. Какой может быть синтаксис в drvinfo? "=BASE:bit_n"?
      2. Как указывать ссылку на базовый: Номера? Или имена? А есть ли API поиска "внутри указанного устройства" (себя) по имени?
    • Как "подчинённым" каналам ловить обновления базовых?

    ВЫВОД: лучше внешними драйверами. Но тогда нужны более векторные, чтоб лебединости из 5 штук uint16 могли переделывать.

    16.08.2023: фигня всё, написано было от непонимания и непомнящести предложенной 15-06-2023 идеи -- "регистр отображается на 1+N последовательных каналов, первый из которых является всеми битами, а следующие N -- побитовое представление"; в том варианте оба вопроса исчезают. Так что вчерашнее в основном "withdrawn".

    Однако пара рациональных зёрен во вчерашнем есть:

    1. Как-то надо мочь указывать в drvinfo, что это такие регистровые композитные каналы. И вот КАК -- пока и близко нет идей.

      @вечер: префиксом, ДО обычной Modbus-спецификации регистра? И префикс должен начинаться с особого символа -- например, с открывающей скобки (и закрываться закрывающейся), вроде "(bitmap)" (точнее, "(bitmap=NUMBITS")?

      17.08.2023@утро, зарядка: тогда уж и "writeonly" можно туда же добавить, вместо '/'. А парсить содержимое скобок -- psp_parse()'ом, с terminators=")".

      20.08.2023@утро, просыпаясь: а чё сразу "префикс"? Можно и СУФФИКС -- "HOLD:1000/8(bitmap=128)". Так оно удобнее с точки зрения проверки: к этому моменту уже известны kind и conv.

    2. "Векторные битовые драйверы" вполне могут быть полезны.

    16.08.2023: насчёт групповых запросов по HeartBeat(): всё-таки неприятно, что если в блоке регистров есть неиспользуемые "дырки" (например, каналы "Тип параметра" у РПМ-416, которые нам нафиг не сдались, но они по 1 регистру на каждые 2 регистра данных), то цикл перебора каналов в ProcessInData() вынужден каждый раз пробегать ВСЕ каналы (т.к. num1_left никогда не обнулится, раз в нём есть ненужное).

    • @обед, Гуси, угловой столик у сцены и у окна, ~13:30: а если прямо в спецификации указывать диапазон каналов для пробегания? И сохранять его как-то в 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 за сегодня.

    • @пешком из Эдема домой, ~14:00: отдельно вопрос синтаксиса; суффикс ":FIRST-LAST" -- "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 -- то это количество следующих за данным "подчинённых" ему каналов.
    • Парсинг:
      • Синтаксис -- суффикс "(ОПЦИИ)" после спецификации регистра, они PSP-парсятся посредством таблицы text2chanopts[].
      • В опциях предусмотрены ключики writeonly (приводит к взведению 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 -- делается в момент аллокирования памяти.)

      • 24.08.2023: подход переделан на запланированный, судя по комментарию "-1 for independent channels, >=0 for subordinate bit-channels", ещё 27-06-2023: base_chan прописывается ТОЛЬКО для подчинённых каналов и в нём лежит номер "базового" (а не ихний собственный, как позавчера). И у самого базового -- .base_chan=-1, так что тут он не отличается от обычных, никак НЕ задействованных в bitmap'ах.
    • В modbus_tcp_rw_p():
      • Для всех связанных с bitmap'ами каналов форсится action=DRVA_READ;
      • а для "подчинённых" каналов делается перенаправление chn на bcase_chan.
    • В ReturnOneChan() для INPR/HOLD-каналов после m2h() и возврата при .bitmap > 0 подготовлен (пока он за-#if 0'ен) несложный цикл (по образу и подобию возврата 1-битных каналов ниже), раздербанивающий данные на биты и возвращающий их в "подчинённых" каналах.

      Возврат делается ПОШТУЧНО, одиночными ReturnInt32Datum(), а НЕ одним ReturnDataSet() на всё.

    Есть неприятное впечатление, что это всё же нечто лишнее в теле драйвера: занимает очень дофига места, а, формально, могло бы выполняться внешним драйвером, подписывающимся на каналы "реального".

    24.08.2023: вчера доделана собираемость, а теперь проверяем "не сломалось ли что" и насколько корректно работает парсинг.

    • С парсингом всё OK.
    • А вот в цикле раздербанивания был забыт сдвиг маски.
    • 26.08.2023: и собственно прочее -- от парсинга до раздербанивания значения на биты -- вроде тоже пашет.

    Надо бы всё-таки менять имена "bitmap" и "base_chan" на что-то другое -- и более адекватное, и покороче (желательно 4 символа для унификации с прочими членами chaninfo_t).

    @холл перед залом круглого стола, ~17:00:

    • "bitmap"->"bitx"?
    • А вот "base_chan" -- идеально тянет на "host", но у этого термина уже иной смысл...

      "srcc"? "truc" (TRUe Channel)?

      25.08.2023@ключи, домик, веранда-предбанник/крыльцо, ~17:00: "hwch" -- HardWare CHannel, т.е., "реальный аппаратный канал"; не совсем "исходный" или "хозяин/владелец", но вполне годно.

    25.08.2023@вечер: да, переименованы: "base_chan"->"hwch", "bitmap"->"bitx" (ВЕЗДЕ -- и в коде, и имя параметра-опции).

    12.09.2023: приступаем к реализации поддержки записи.

    • ОБОБЩЕСТВИТЬ modbus_nelems_of_num1() и modbus_num1_of_nelems().

    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;
    
    используется
    me_size      = (sizeof(*me) + 
                    sizeof(me->map    [0]) * numchans      + 15) &~15UL;
    
    -- заодно исчезла шизофрения (реально недоделанная! :D) "при аллокировании перед map[]'ом есть добивка до кратного 16, а адресация идёт к me->map, который с другим выравниванием".

    21.09.2023: bitx теперь можно указывать не только UINT16-каналам, но и любым INT размером 1, 2 или 4 байта.

    • Занадобилось для того же РПМ-416, где регистры статуса и ошибок -- INT32_BE, а их содержимые являются именно битовыми наборами.
    • Цикл раздербанивания вектора на биты радикально переделан:
      • Работает всегда в 32-битных числах (отсюда и ограничение "не более 4-байтных") -- word32 и mask32.
      • ПЕРЕД циклом в зависимости от размера -- 4,2,1 -- определяется "максимальная маска" maskhi -- 0x80000000,0x8000,0x80 -- и запоминается этот размер.
      • Вычитывание очередной ячейки теперь выполняется не дважды (при старте цикла и при достижении маской максимума), а единожды -- в начале тела цикла, если текущее значение mask32 == 1; тут же делается и инкремент указателя чтения, ...
      • ...кстати, оный указатель чтения -- uint8 *rp -- т.е., просто байтовый.
      • В конце же тела проверка маски при сдвиге сократилась до простого
        if (mask32 == maskhi) mask32 = 1;
        else                  mask32 <<= 1;
        

      Такая организация цикла даже короче и нагляднее -- благодаря ЕДИНСТВЕННОМУ чтению, хотя и за счёт двух if()'ов вместо былого одного (плюс if-селектор перед циклом, но это из-за поддержки данных разного размера).

    • Ну и проверка годности канала в роли bitx-базы была переделана с "только UINT16" на "только REPR_INT и не более 4 байт".
    • Замечание: там на объём проверка несколько некорректная: превышением считается 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: немножко по мелочи:

    • Конверсия для 1-битовых каналов -- компоновка побитово-байтового вектора из вектора INT8/16/32 и раздербанивание байтового вектора в вектор UINT8 -- обобществлены в modbus_bits_h2m() и modbus_bits_m2h() соответственно.

      19.03.2025: в modbus_bits_h2m() был косяк: в case-альтернативах 1,2,4 (байтовый размер исходных данных) отсутствовали "break", что должно было приводить к "проваливанию" в следующие ("fallthrough").

      Чуток анализа:

      • Учитывая, что в modbus_mon.c::PerformIO() парсинг выполняется в буфер UINT8, запись COIL'ов работать не могла в принципе.
      • В драйверах же (в обоих поколениях) при использовании связанными с этими COIL'ами каналами в качестве типов 32-битных -- работать должно, т.к. альтернатива "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).

    • Синтаксис с префиксом '/' для writeonly удалён -- нефиг.

    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.

    • можно его и использовать, просто для is_rw-каналов с bitx>0 аллокировать не по datasize байт, а по datasize*(1+N), где N -- число требуемых дополнительных под-буферов.
    • Правда, по-хорошему -- стоило бы добавить прямо в chaninfo_t и поле "datasize", для упрощения арифметики.
    • Несколькими минутами позже -- неа, не надо: оное "datasize" всегда будет равно num1*2: ведь bitx-каналы отображаются на 16-битные регистры, поэтому (вне зависимости от типа конверсии и типа исходных данных) объём каждого под-буфера будет равен объёму регистрового блока, т.е. -- num1 16-битных слов.

    16.12.2023: во время написания "отчёта по госзаданию" в 20231212-OTCHET-PO-GOSZADANIYU-2023.txt накатал туда фразу

    Спецификация отображения канала на Modbus-регистры указывается в свойстве drvinfo
    -- "в СВОЙСТВЕ"!!!

    А ведь недавно смотрел, как в Tango устроена работа с Modbus, и там тоже такие вещи указываются в PROPERTIES (только устройства или атрибутов -- надо перепроверить; вроде бы устройства, а у нас-то КАНАЛОВ).

    • Т.е., архитектура получилась похожей.
    • И тут у Tango точно приоритет.
    • Только там эти properties формально можно менять по ходу дела, а в CX -- нет.
    • Хотя, возможно, само терминологическое сходство -- "свойства", "properties" -- появилось только сейчас, во время написания отчёта, вследствие знакомости с Tango (с которым прямо перед этим много маялся), а во время создания самой концепции drvinfo в 20?? таких мыслей не было.

    Но сходство в любом случае налицо. Конвергентная эволюция? :D

    01.01.2024: ЗЫ: ещё пара замечаний в ту же степь (сообразил давно, но руки дошли записать только сейчас):

    1. А наше auxinfo -- это аналог "свойств устройства". Только указывается оно всё в формате КЛЮЧ=ЗНАЧЕНИЕ{,КЛЮЧ=ЗНАЧЕНИЕ}.
    2. И ещё: в Tango-то свойства у атрибута могут быть множественными, а у нас -- как бы единственное drvinfo. Но реально:
      1. Стандартизованные служебные свойства указываются отдельно (всякие R, D, units, quant, range ...) -- в этом смысле аналогия полная.
      2. Де-факто тут при надобности тоже могут указываться множественные свойства, опять же в формате КЛЮЧ=ЗНАЧЕНИЕ{,КЛЮЧ=ЗНАЧЕНИЕ}.

    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 является) такого поля нету.
    • Заодно добавлена выдача получаемого значения в сообщения об exception'ах.

    И-и-и -- помогло!!! Теперь обмен идёт инкрементирующимися идентификаторами (проверено wireshark'ом).

    0x.xx.2024:

  • 07.12.2023: вылезла потенциальная диковатая потребность: уметь в Modbus-TCP разные запросы маркировать РАЗНЫМИ unit_id.

    Непосредственная причина -- дурная прошивка ВИП-45, в которой, похоже, за одним IP-соединением "скрывается" несколько устройств. И вообще-то такое поведение возможно и по стандарту ("шлюзы").

    07.12.2023: обсуждение:

    • Возможность указания UNIT_ID per-register -- по сути есть готовая, т.к. modbus_parse_spec() умеет понимать опциональный префикс "+UNIT_ID" и возвращать его (а при неуказанности -- -1), только надо флаг MODBUS_PARSE_FLAG_UNIT_ID взвести.
    • Хранение: ну тупо добавить к chaninfo_t очередное поле -- unit_id.
    • Отправка:
      1. Также добавить поле unit_id и к mbqelem_t.
      2. В q_enqueue() добавить параметр unit_id для складирования туда.
      3. В его вызовах -- в обёртке q_enq_x() и в modbus_tcp_rw_p() с ProcessInData() (в обработке exception'ов) -- передавать в этом параметре значение me->map[chn].unit_id.
      4. А в mb_sender() проверять значение этого qe->unit_id, и если >=0, то использовать вместо me->unit_id.
    • 08.12.2023: "Мимо-канальные" on_hbt/on_conn: да всё то же самое -- добавить поле unit_id и к mreqinfo_t, при парсинге взводить MODBUS_PARSE_FLAG_UNIT_ID и сохранять результат, а в InitializeRemote() и HeartBeat() передавать значение on_NNN[n].unit_id в q_enqueue().
    • И в mb_eq_cmp_func() не забыть добавить в цепочку сравнения -- тут уж просто и безусловно.
    • Вот только с кодом layer'а -- будущего, которого пока нету -- это будет взаимодействовать фиг знает как: ведь там-то именно по UNIT_ID нужно определять драйвер-получатель.

      08.12.2023: с другой стороны -- такие фокусы с per-register-указанием UNIT_ID имеют смысл только для MODBUS_TCP, т.к. для RTU и ASCII адрес является именно адресом отдельного устройства, обслуживаемого отдельным драйвером. Поэтому можно просто запрещать указание "+UNIT_ID" для proto!=MODBUS_TCP -- например, флажок MODBUS_PARSE_FLAG_UNIT_ID не взводить.

    • 08.12.2023: а-а-а, ч-ч-чёрт -- забыл один аспект: ведь и "обратное маппирование" адресов на каналы тоже должно эти адреса учитывать. Но ТОЛЬКО для таких случаев, когда 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'ы:

    • Функции "add()" указывать среду соединения (serial/tcp) и его "адрес" -- /dev/tty* или IP:PORT, и это будет "порт", адресуемый по line_id.
    • Используемые "порты" хранятся в общей таблице, при регистрации устройства делается поиск по ней, при найденности используется имеющийся, при ненайденности создаётся; line_id -- просто индекс в таблице.
    • В качестве "handle" будет работать line_id<<8+unit_id.

      При этом, собственно, и исчезает проблема «нужно будет как-то маппировать "адреса"...» -- в fdio_register_fd() будет передаваться privptr2=ptr2lint(line_id), так что он будет вертаться notifier'у готовеньким.

    • Для TCP-устройств может указываться "захватываем порт целиком".
    • Следствие: serial-линки будут работать как TCP-соединения -- "реконнектиться" (да, как в modbus_mon'е).

    Да, будет более муторно, чем сейчас; да, собственно драйвер чуток усложнится. С другой же стороны, изменения по сути чисто административно-бюрократические, "мясо" работы с Modbus вряд ли сильно затронут.

    05.11.2024@ИЯФ, лестница вниз в пристройке, перед 3-м этажом, ~12:10: очевидно, что "код установления и восстановления соединения" (который можно условно, по аналогии с AsynDriver, назвать "портом") будет потенциально годным вообще для ВСЕХ serial-устройств.

    • ...конечно, при условии такой его компоновки, чтобы connect/reconnect были полностью независимы от протокола передачи данных.
    • 06.11.2024: а именно:
      • "портова ProcessIO()" обрабатывала бы только reason'ы соединения:
      • FDIO_R_CLOSED/FDIO_R_IOERR (хотя в modbus_mon.c по ним rpy_tid трогается),
      • FDIO_R_CONNERR,
      • а FDIO_R_CONNECTED -- неэксклюзивно, т.к. по нему и прикладному протоколу надо действия выполнять,
      • передавая всё остальное обработчику "высокоуровневого порта".
    • И это потенциально позволит гонять КШД485 через TCP-to-SERAIL-конвертеры Moxa.

    И да, в AsynDriver так и сделано -- там это функциональность "порта", вне зависимости от того, что за данные мы через этот порт гоняем; поэтому конкретным драйверам должно быть безралично даже, serial это порт или TCP.

    06.11.2024: дальнейшие соображения по теме:

    1. Вытанцовывается такой ПЯТИслойный стек модулей:
      • Драйвер
      • Layer
      • Менеджер портов (этот можно бы и в layer, но чисто чтобы для РАЗНЫХ layer'ов был одинаковый готовый код)
      • "Драйвер порта" -- тот самый "код установления и восстановления соединения"
      • "Открыватель конкретного типа порта" -- TCP, /dev/ttyS* (локальные), /dev/mxu* (MOXA CP132EL), /dev/ttyM* (MOXA UC-7112)
    2. Насчёт "открывателей":
      • Они будут просто указателями на функции вида "opendev(path_info,params)", ...
      • ...содержащимися в таблице "media_openers[]", индексируемой "типом открывателя".
      • Ещё в этой таблице будут содержаться строки с "сигнатурами" имён -- то, что между /dev/ и NN -- "ttyS", "mxu", "ttyM", ...

        И для MODBUS_MEDIA_SERIAL должен выполняться поиск по сигнатурам, а если не найдено -- то брать "открыватель" от "ttyS", как бы "умолчательный" (он, кстати, будет годиться почти для всего -- ttyUSB*, Advantech PCIE-1612C-AE, ...).

      • Видимо, надо сделать TCP индексом 0, "простой open(/dev/tty*)" -- индексом 1, а прочие "специфичные" -- 2...СКОЛЬКО_ТО.
      • Что, кстати, прямо противоположно нынешним значениям MODBUS_MEDIA_SERIAL=0 и MODBUS_MEDIA_TCP=1 -- очевидно, их надо менять местами первым же шагом.

        07.11.2024: поменяны (и код чуток адаптирован, чтоб сравнение ВСЕГДА делалось с нулевым значением -- теперь это MODBUS_MEDIA_TCP (раньше тоже в основном с ним, но не всегда; теперь -- всегда)).

        04.12.2024: и в modbus_tcp_drv.c переделано аналогично. Его в будущем трогать не будем, но уж это пусть останется унифицированным.

      • В отличие от API serial_hal.h, путь будет передаваться ПОЛНОСТЬЮ, вместе с /dev/ttySOMETHING.
      • Так что этот "API открывателей" будет отдельным, НЕ связанным с serial_hal.h, хоть и копирующим "мясо" оттуда.

      Главный же пока что вопрос -- откуда будут браться сами реализации "открывателей":

      • ну, TCP и /dev/tty* захардкодим, а остальные?
      • Ведь на разных платформах доступны РАЗНЫЕ их наборы: /dev/ttyM* будет только на MOXA UC-7112; а специфичные для "контроллера Торнадо" UART'ы -- возможно, только на нём.
    3. Итого:
      • Драйвер надо будет назвать modbus_drv.c,
      • а layer -- modbus_lyr.c, ...
      • ...так что да -- в devlist'ах будет тот самый «поддерживаемый, но пока не реализованный вариант "DEVICE_TYPE/modbus@LAYER"» в странновато выглядящем варианте "DEVICE_TYPE/modbus@modbus".
      • Текущий modbus_tcp_drv.c надо оставить как есть, а все работы вести уже в новых файлах.
      • Эвристику определения пары {media,proto} -- таблицу conn_types[] -- желательно унифицировать с modbus_mon.c, как, естественно и константы MODBUS_MEDIA_*.

        ...а лучше сразу обобществить всю ParseDevSpec().

      • И обратно -- от реализации "открывателей" выиграет и modbus_mon.c, поскольку научится работать со специфичными типами serial-портов.
      • Остаётся вопрос о том, как указывать параметры COM-порта?

        Для 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".

  • 13.06.2024: обнаружилась забавная штука: конкретно у контроллеров Wago адреса записи и чтения не совпадают: читать надо с адреса+0x200; это сказано, в частности, в "Driver Support for Modbus Protocol under EPICS" от Mark Rivers:
    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: "что будем делать" -- учитывая, что:

    1. Желательно бы указывать как-нибудь ОДИН РАЗ, "опцией" (ключиком в modbus_mon и auxinfo-параметром в modbus_tcp_drv), а не в "адресе" (который TYPE_ADDR).
    2. Адрес для запроса в обоих случаях указывается в вызове modbus_create_req_packet().
    ...напрашивается идея:
    1. Да, сделать ключик и параметр, указывающие "у нас долбанутый Wago!".
    2. И иметь "глобальный" параметр readback_addr_offset, который прибавлять при операциях чтения (op==MODBUS_OP_READ) к адресу, передаваемому в modbus_create_req_packet(), ...

      ...а значение этого параметра взводить в 0x200 при указании того ключика (ну или ключик "read_addr_offs", которому прямо число и указывать).

    14.06.2024@утро, завтрак: с другой стороны -- а МОНИТОРУ-то это зачем? Там всё равно команды чтения и записи указываются ОТДЕЛЬНО, ну так и ставить командам чтения адреса с +0x200.

    15.06.2024: делаем.

    • В privrec добавлено поле read_addr_offs, оно int16 -- знаковое, т.к. смещение может и отличаться.
    • ...после чего вроде бы первейшим делом должно было стать прибавление этого offset'а к адресу в командах чтения, но тут возникли сомнения: а во ВСЕХ ли командах оно так? Для любых ли типов регистров?
    • Пока притормаживаем кодинг и нужно изучить исходники EPICS'ного модуля и, желательно бы, в т.ч.:
      1. Понять, какие ещё странности в этой области могут быть (в описании были какие-то слова про "Koyo" -- что там?).
      2. Найти бы какое-нибудь внятное описание ситуации от самой Wago.

    16.06.2024: изучаем.

    • Смотрим тот же R2-10-1.tar.gz, конкретно файл modbus-R2-10-1/modbusApp/src/drvModbusAsyn.c -- там оное смещение держится в поле pPlc->readbackOffset
    • И используется оно как-то слабопонятно (хотя чё там -- всё в том файле не шибко очевидно :D)
      1. В общем случае при начальном вычитывании -- с не вполне ясными условиями, с комментарием "If this is an output function do a readOnce operation if required.".
      2. Конкретно в функции writeUInt32D() при коде функции MODBUS_WRITE_SINGLE_REGISTER, а читает при этом функцией MODBUS_READ_HOLDING_REGISTERS, и вот именно ЕЙ сбагривается адрес плюс pPlc->readbackOffset.

      И всё, более нигде: ни в readUInt32D, ни в writeInt32(), ни где-либо ещё.

    • Слово "koyo" в коде не встречается, а только в документации и в примерах. И в документации -- тоже исключительно в примерах, а ничего специфического про него нет.
    • А вот по "wago plc 0x200" ничего ВНЯТНОГО найти не удалось.
    • По косвенным признакам возникает впечатление, что у этих долбанатов из Wago есть какие-то странные writeonly-регистры, которые нельзя читать, но можно прочитать записанные в них значения по адресу+0x200.

      "Признаки" -- это вышеописанное поведение 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 сохранять ИСХОДНЫЙ адрес.

    • ...и вообще, не будет ли -- с учётом странностей/нетривиальностей "когда надо прибавлять 0x200, а когда нет" -- правильнее делать это прибавление в другом месте -- в q_enqueue(), т.к. у него больше информации о контексте, в т.ч. есть ссылка на канал.

    Отсюда выводы:

    1. Реализовываем эту фичу ТОЛЬКО в драйвере, монитор и так позволит читать любые указанные адреса.
    2. ...в связи с этим переносим пункт из обще-Modbus'ового "Идеи и соображения" (где он родился 3 дня назад) в драйверов.
    3. Кодинг приостанавливаем до улучшения понимания проблемы.

    16.07.2024@утро-просыпаясь: насчёт проблемы "при обратном маппировании" -- что сравнивать придётся с учётом смещение: неа, НЕ придётся.

    • Прибавлять смещение нужно ТОЛЬКО к адресу в вызове modbus_create_req_packet(), как и запланировано (записано 16-06-2024).
    • А в last_sent надо сохранять ИСХОДНЫЙ адрес.
    • И поскольку в самом ответном пакете-подтверждении адреса нет, то браться он будет из last_sent, так что проблемы нет.
    • Точнее, так: в подтверждениях ЗАПИСИ адреса есть прямо в ответных пакетах (и COIL, и HOLD).

      Но нам-то смещение нужно прибавлять ТОЛЬКО ПРИ ЧТЕНИИ, а в тех ответах адреса нет.

      Чуть позже: но для исполняемого и после ответов на запись 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" -- более дословно

    WAGO-I/O-SYSTEM 750
    Manual
    750-852
    ETHERNET ECO Controller
    PLC - ETHERNET Programmable Fieldbus Controller
    ECO

    Version 1.3.0
    -- с рекомендацией "Начиная с 76 страницы.", которая раздел "7.2 Process Data Architecture".
    • И там на стр.76 в разделе "7.2.3 Example of an Output Data Process Image" нашлась фраза
      In addition, the output data can also be read back with an offset of 200hex (0x0200) added to the MODBUS address.
    • ...при перепроверке она нашлась и в отсмотренном выше "" на стр.47(PDF:53) в разделе "3.1.4.2 Example of a process output image".
    • Но хуже то, что
      1. На стр.77 приводится куча других правил насчёт адресов:

        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.

      2. А на стр.79 есть также малопонятный пассаж
        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.

    Откуда выводы:

    • Возможно, данный маразм касается не только HOLD-, но и COIL-каналов.
    • Что ещё хуже -- возможно, далеко не всегда достаточно "+0x200", а иногда нужно другое смещение.
    • По-хорошему, wago_hold_read_addr_offs надо переименовать из "read" в "readback" -- wago_hold_readback_addr_offs -- для отражения факта, что это касается "обратного вычитывания"
    • Но ОПРЕДЕЛЁННО понять, что к чему, вот так теоретически-дистанционно не удастся, а надо отложить это до появления первого реального девайса.
5:
modbus_lyr: modbus_drv:
  • 19.12.2024: создаём раздел, поскольку приступаем к реальным работам по рефакторингу modbus_tcp_drv.c в раздельные modbus_drv.c и modbus_lyr.c (с описанием интерфейса в modbus_lyr.h).

    Раздел располагаем тут, не в хронологическом порядке, а сразу после modbus_tcp_drv.c'шного.

    Все действия по собственно рефакторингу будем описывать прямо в "корне" подраздела, а порторм -- после первоначального рефакторинга -- можно и в level5-списках собственно драйвера и layer'а.

  • 19.12.2024: соображение общего характера: кроме modbus_drv услугами modbus_lyr могут пользоваться и другие драйверы.

    Если вдруг когда-либо возникнет потребность в какой-либо иной работе с Modbus-устройствами

    21.12.2024@утро, просыпаясь: и вообще, modbus_lyr -- это "драйвер шины", а какие драйверы могут пользоваться его услугами -- вопрос ортогональный.

    Практический вывод из этого глубоко теоретического соображения: разделение между modbus_lyr и modbus_drv нужно реализовывать так, чтобы на стороне layer'а была ТОЛЬКО "работа с шиной", т.е., connect и отправка/получение пакетов. А вся интерпретация данных -- в т.ч. даже и обработка пакетов exception -- на стороне драйвера.

  • 19.12.2024: пристумаем к работам. Проект -- как вообще надо обращаться с портами/соединениями -- был разработам выше за 17-01-2024, 05-11-2024 и 06-11-2024.

    19.12.2024: создаём modbus_lyr.h пока в минимальном варианте, беря за основу cankoz_lyr.h как наиболее похожий.

    • Создано нечто вроде скелета, с пока почти пустой VMT и ещё не дозаполненным списком параметров метода add() -- ModbusAddDevice -- и полностью отсутствующими методами q_enq*().
    • Для понимания "какие потребности в методах работы с очередью" изучаем соответствующие чпсти modbus_tcp_drv.c и...
    • И сходу вылезла проблема: в modbus_tcp_drv.c ведь при постановке запроса в очередь указывается ещё и номер канала -- чтоб... а вот надо б разобраться/вспомнить, чтоб ЧТО. ...и ещё там есть data_ofs -- это "адрес" (смещение в общем wr_buf[]) данных для команд записи.

      Но layer-то вроде не должен бы иметь доступа к драйверовым спискам каналов -- это не просто не его область, а вообще принцип разделения ответственности этому препятствует.

      Как будем решать?

    20.12.2024: результаты исследования вчера:

    • номер канала -- для учёта драйвером и сопоставления пришедшего пакета отправленному;
    • data_ofs -- да, данные для команд записи, ибо может быть много и прямо в qelem пхать нельзя.

    С учётом этого:

    1. ну ладно, канал просто иметь в qelem'е и фиг с ним.
    2. Видимо, надо будет иметь драйверову функцию-метод, вызываемую при КАЖДОЙ отправке пакета (чтоб qelem в last_sent складывать); соответственно, указываться она должна прямо в add().
    3. В add()'е же передавать указатель на драйверов общий буфер для данных, в котором смещением этот по-qelem'ный data_ofs.

    @утро-душ: и сразу отдельный вопрос про менеджмент структур данных "списки драйверов" в layer'е.

    • Самое простое/очевидное -- на каждый "порт" массив [256], в каждой ячейке которого полный описатель информации о драйвере-клиенте.

      (Как в cankoz_lyr_common.c каждый описатель "порта" в массиве из lineinfo_t содержит массив kozdevinfo_t devs[DEVSPERLINE] -- описателей устройств.)

    • Но описатель-то получится немаленький и убивать столько памяти впустую -- для большинства TCP-устройств будет по 1 устройству на "порт" -- как-то неправильно.
    • Может, структуры выделять из одного общего сквозного пула, индекс в котором и будет handle'ом, а на каждый "порт" иметь массив индексов int[256], в котором -1 -- "неиспользовано", а >=0 -- handle?
    • @вечер: ...хотя это несколько противоречит постулату «В качестве "handle" будет работать line_id<<8+unit_id» от 17-01-2024.

      Но, в принципе, можно даже и так оставить -- что лежащее в ячейке [unit_id] значение "индекс в сквозном массиве" будет ВНУТРЕННИМ для layer'а индексом, наружу никак не светящимся. 21.12.2024: да, так и заложено -- в lineinfo_t поле int used[255]; является тем самым массивом индексов.

    • 04.01.2025: по ходу реализации стало ясно, что "а нафига?!". Не видно никакого глубокого смысла делать "handle=line_id<<8+unit_id", проще оставить просто индексом в dev_list[].

      ...хотя будем ещё посматривать -- вдруг какое соображение вылезет.

    21.12.2024: также приступаем к изготовлению modbus_drv.c -- посредством копирования из modbus_tcp_drv.c и модификацией.

    И стало ясно, что:

    • Надо вводить метод драйвера "изменилось состояние готовности" -- чтоб
      1. Передавать состояние is_ready;
      2. и чтоб драйвер мог исполнять свою 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: промежуточные результаты за последние несколько дней.

    • modbus_drv.c доведён до компилируемости.
    • Не получилось удовлетворить хотелку "чтобы драйвер вообще был не в курсе о том, с каким media/protocol он работает":
      • Дело в том, что у Modbus-RTU и Modbus-TCP разные ограничения на максимальное количество регистров в запросе: 125 против 123.
      • Поэтому драйверу нужна информация о протоколе.
      • ...причём СРАЗУ, прямо в момент парсинга auxinfo (конкретно "on_hbt="/"on_conn=") и drvinfo -- в обоих случаях для определения того, сколько Modbus-элементов будет в запросе и ограничения, если оно не влазит.
      • Тип протокола передаётся modbus_num1_of_nelems(), в свою очередь сбагривающей его в modbus_max_num1().

      В качестве временного решения СЕЙЧАС захардкожено значение MODBUS_TCP.

    • Но в будущем понадобится в layer'е иметь возможность узнать тип протокола, причём ЗАРАНЕЕ -- по указанному имени порта.

    12.01.2025: да, в lvmt добавлен метод proto_of_spec(), возвращающий протокол по имени, а в драйвер вставлен его вызов. Реализация метода уже есть -- простейшая modbus_proto_of_spec(), являющаяся переходником к parse_spec_prefix(); и уж последнюю будет вызывать и парсинг имени порта в add()'е.

modbus_lyr:
  • 19.12.2024: подраздел создаём, но пока в нём будет пусто.
  • 23.01.2025: возникла терминологическая проблема: и "канал связи" носит название "port", и TCP'шный номер порта тоже "port". В результате сообщения об ошибках -- в первую очередь, от 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.

    Итого:

    1. В modbus_lyr.c все "port" (относящиеся к соединению, а не к sq_port_t и не к TCP портам), заменены на "line".
    2. А параметр add()'а "port_name" переименован в "line_spec" -- т.к. это не просто имя, а СПЕЦИФИКАЦИЯ, могущая содержать префикс, указывающий тип подключения, а для serial-линков -- ещё и префикс "@SERIAL_PARAMETERS:".
  • 26.01.2025: пора уже начинать записывать технические соображения и нюансы по кодированию layer'а, которое идёт последнюю неделю с лишним.

    26.01.2025: работа с sq_port_t позаимствована из piv485_lyr_common.c (PIV485 было до настоящего момента единственным использованием портов), с адаптацией под местную модель (line,handle).

    27.01.2025: дело дошло до функций взаимодействия с линией -- как отправка/чтение, так и поддержание соединения.

    И вот тут встаёт вопрос о "передаче идентификатора линии" -- как непосредственно функциям, так и fdiolib- и cxscheduler-callback'ам.

    • Как сделано в предшественниках:
      • В modbus_tcp_drv.c в callback'ах очевидным образом используется devid,devptr, а собственным функциям передаётся аналогичный дуплет (devid,me).
      • В modbus_mon.c же вообще всё глобальное, так что там этот вопрос не стоИт.
    • Здесь же
      • Все devid и devptr нерелевантны -- у layer'а первое всегда ==my_lyrid, а второе смысла не имеет.
      • Поскольку используются GROWFIXELEM-slotarray, то указатели на ячейки всегда фиксированны и по памяти не ездят.

        Так что в принципе можно прямо lineinfo_t* в качестве privptr2 и передавать.

    • Но:
      • Для числовых идентификаторов можно выполнять проверку на валидность, а для указателей нельзя.
      • Передача сразу указателей ломает совместимость кода с тем, где GROWING-slotarray'и.
    • Поэтому:
      • Повсеместно передаём параметрами и в качестве privptr2 числовое значение line, из которого потом делается lp=AccessLineSlot(line).
      • ...КРОМЕ нескольких сервисных функций, которым номер линии до фени: CleanupFDIO(), ReportConnfail(), ReportConnect() -- вот им таки сразу передаётся указатель "lineinfo_t *lp".

    28.01.2025: и другой аспект: в драйвере modbus_tcp_drv.c при всяких протокольных ошибках делается CommitSuicide(), т.к. непонятно, как же там продолжать. В layer'е так делать нельзя.

    • И монитор modbus_mon.c тоже так не делает -- он в подобных случаях вызывает ScheduleReconnect(), чтобы "начать с чистого листа".
    • Но если ошибку вызывает какое-то конкретное устройство, то она будет повторяться снова и снова -- ОДНО устройство будет портить работу ВСЕЙ линии.
    • А можно ли как-то деактивировать это одно конкретное устройство?
      • @вечер, мытьё посуды: ведь sq_port-то помнит, кому последнему отправлялся пакет и, соответственно, от кого (скорее всего!) прилетел кривой ответ.

        Так что надо бы из порта эту информацию как-то добыть.

        ...возможно, добавить функцию добычи в 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, а потом для активации надо ещё какую-то запись выполнить -- разные соображения.

    • @прямо во время разговора: Понятно, что на "голом" Modbus-драйвере такое реализовать никак -- ну нету у него функциональности "при записи в такой-то регистр выполни ещё вот это".

      А как МОЖНО добиться требуемого?

      • Вырожденный вариант (как раз на голом драйвере) -- отдавать каналами как двойной регистр уставки, так и регистр активации. Чтобы в скрине была кнопка "активировать", привязанная к нему.
      • Можно сделать эту штуку отдельным драйвером на основе modbus_lyr -- чтоб при записи значения оно бы исполняло запись 2 регистров и потом запись в регистр-активатор.
      • А можно vdev-драйвером, использующим в качестве "подстилающего устройства" некую Modbus-карту регистров на основе стандартного Modbus-драйвера.

      На какой из вариантов оринтироваться -- надо будет выбирать после получения описания.

      02.02.2025: чисто "не сдержался" -- сделал заготовки для всех вариантов (на лыжах ходя обдумывал, вот и решил закрепить): kravets_test.devtype содержит определения пары каналов и должен годиться во всех 3 случаях; kravets_test_modbus_drv.c и kravets_test_vdev_drv.c как варианты драйверов.

      11.02.2025: описание получено, но легче не стало: "активация" выполняется не записью в какой-то другой регистр, а пользовательскими Modbus-командами -- 65 (плавно) и 66 (мгновенно). И такое у нас не поддерживается вовсе...

    • @~14:30, по дороге из девятиэтажки домой, проходя мимо стройки новых корпусов НГУ между Коптюга и стадионом: технически в чём существенное различие между modbus_tcp_drv.c и связкой modbus_drv.c+modbus_lyr.c?

      В том, что первому вообще пофиг на unit_id (значение игнорируется), а во втором случае его значение является ключевым.

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

      Собственно идея:

      • А если ввести в modbus_lyr.c возможность указывать "вот этому драйверу давать ВСЕ прилетающие пакеты"?
      • Например, указанием в 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: по мере тестирования находятся и исправляются разнообразные косяки и упущения.

    • В modbus_drv.c при обломе 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.

    • Была проблема "курицы и яйца" для локальных COM-портов: вызов 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: самый знатный косяк (с которым несколько дней не то, что разобраться -- непонятно было, как к нему подступиться!) заключался в том, что:

    • если какие-то из устройств при регистрации обламывались (например, по дублированию unit_id или по несоответствию протокола),
    • то потом из 2 вполне легитимных устройств на линии "/dev/ttyS3", которая никуда не подключена и потому на ней должны перемежаться посылки от этих 2 устройств, шли посылки лишь от ОДНОГО.

    Причина оказалась тривиальной и тупой:

    • При регистрации устройства в линии делалось
      lp->used[unit_id] = devid;
      вместо
      lp->used[unit_id] = handle;
    • Явно результат путаницы "что мы держим в таблице: devid или внутренний handle".
    • Реально-то понятно было, что держать надо handle.

      Как минимум потому, что именно он уникально привязан к устройству, а одному devid может принадлежать несколько устройств.

      Да и вообще -- какое отношение имеют devid к адресации/учёту устройств ВНУТРИ layer'а? Понятно, что никакого, а использоваться тут должны внутренние handle (как в cda_d_-модулях -- hwr'ы).

    • Но, тем не менее, как-то умудрился накосячить.

    А вот разбирательство было долгим и мучительным.

    • Казалось очевидным, что остаются какие-то "следы" от недозарегистрированных устройств -- то ли где-то что-то недоподчищается, то ли не делается bzero() каких-то ячеек (например, в dev_list[]).
    • Но никак не удавалось ни найти потенциально косячное место, ни даже придумать внятную гипотезу о механизме косяченья.
    • В какой-то момент подозрение пало на sendqlib -- "а что, если работа с портами всё-таки глючит? ведь она проверена на единственном применении с КШД485 (хоть и в вариантах для v2 и v4, но В/В там идентичен)...".
    • Поэтому была добавлена диагностика:
      1. Печать указателей на q и port при их инициализации,
      2. а в mb_sender() "дамп" внутренностей порта -- first_in_port/last_in_port и first_to_send/last_to_send/current_to_send.
    • И вот тут оказалось, что в списке "_to_send" всего ОДИН элемент -- очередь ПЕРВОГО из 2 устройств!
    • Как такое могло произойти -- было совершенно непонятно: анализ кода sendqlib.c (хоть и не самого очевидного) не находил каких-то проблем.
    • Тут уж "была не была" -- добавлена диагностика и в sendqlib.c: add_to_send() и zer_to_send().
    • И тут вылезло неожиданное: после первого же таймаута порта (ответов-то не приходит) вызывался zer_to_send() для очереди ВТОРОГО устройства!
    • А дальше ещё раз внимательно посмотрел на прочую диагностику и заметил ещё одну странность: по облому коннектов предыдущих 2 линий (EHOSTUNREACH) почему-то вызывались ReadyChange() странных наборов устройств: не тех, что принадлежат этим линиям, а "следующих".
    • Ну и дальше механизм "глюка" стал понятен: раз по ошибке вызывался метод ReadyChange(is_ready=0) "не того" устройства, а именно нашего "ВТОРОГО", то его же очереди там же рядышком в ScheduleReconnect() делалось и sq_clear(), поэтому при следующей попытке отправить пакет в линию обнаруживалась пустота очереди и ей легитимно делалось zer_to_send(), а слать было и нечего.
    • Потом уже делом техники было найти причину того, что вызывается метод "не того" устройства, но тут помогло то ли озарение, то ли интуиция.

      ...а может, просто внимательное прочтение цикла, вызывающего stateproc()'ы устройств и сопоставление выданных диагностикой значений handle'ов (должных содержаться в used[]) с реально содержавшимися там числами, очень уж явно совпадавшими с devid'ами обломившихся устройств.

    Выводы:

    1. Работа с портами в sendqlib.c ведётся корректно (по крайней мере сейчас проблем в ней не обнаружено).
    2. Дело было НЕ в обломе регистрации: просто в тестовом devlist'е при НЕобломе получалось 1-в-1 соответствие handle==devid, вот косяк и не проявлялся. А если б были разные устройства, то соответствия бы не было и полезли бы диковатые глюки.
    3. Внимательнее надо быть при работе со сложными структурами данных!

    11.02.2025: итого -- на вид всё работает как предполагалось, но это проверено в тех пределах, в каких можно БЕЗ реального железа.

    Где бы железки-то взять --

    1. С serial-подключением, RTU.
    2. С ethernet-подключением, TCP, но с множественными unit_id.

    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.

    После исправления заработало.

  • 28.02.2025: а ещё у нас нет никакой поддержки broadcast'ов, каковые в Modbus вообще-то возможны -- unit_id=0 (но устройства не имеют права на них отвечать). Например, в Инситек ИПВ4-4.pdf таковые присутствуют.

    ...вообще-то можно "поддерживать" их просто посредством "устройства" с unit_id=0, которое все пакеты отправлял бы с SQ_TRIES_ONS; вот только такого режима в modbus_lyr.c не предусмотрено (но добавить ничто не мешает).

    И специальный драйвер-юзер layer'а чтоб этими отправками заведовал.

    06.03.2025: учитывая немаленькость объёма modbus_drv.c, существенная часть которого приходится на парсинг и сохранение Modbus-спецификаций плюс взаимодействие с layer'ом -- а как бы этак обойтись БЕЗ отдельного драйвера, чтоб обычный "общий"?

    06.03.2025@~18:00, по дороге домой после похода по магазинам за продуктами, идя вдоль подъездов Терешковой-10:

    • Ведь unit_id=0 в принципе годится только для broadcast'ов, так?
    • Ну и пусть обычный modbus_drv.c при unit_id==0 вместо обычной отправки делает с SQ_TRIES_ONS.

      @18:45, дома, заглянувши в modbus_lyr.c: а то и вовсе прямо в modbus_lyr.c::modbus_q_enqueue() -- там самое удобное место и с исторических времён там даже проверка в присвоении item.props.tries осталась.

    • Обстоятельство: broadcast-отсылка, скорее всего, имеет смысл только для команд ЗАПИСИ регистров.

      А для них есть УЖЕ почти готовый механизм, который заставит драйвер НЕ пытаться читать: флаг "writeonly"; вот только надо будет сделать ещё, чтобы даже и ответов не ждало -- чтоб "подтверждения" делались прямо сразу.

      ...хотя в идеале не совсем сразу, а в момент ОТСЫЛКИ.

  • 07.03.2025: а не сделать ли возможность указывать для каждого порта конкретное значение RECONNECT_PERIOD, вместо умолчательных 10с?

    Побудительный мотив -- ВИП-48, у которого прямо в документации "Описание протокола обмена данными Modbus TCP_IP 26 03 2024 (3) для ВИП48и50.doc" сказано "если нет ответа в течение 0,5 секунды, можно закрывать соединение и каждые 1,05 сек пытаться открывать новое соединение".

    09.03.2025: скорее "нет". Т.к.

    1. Указывать ГДЕ? Напрашивается только layerinfo, которая по сути есть спецификация serial "comopts". Туда что ль добавить параметр "reconnect_time=NNN", причём чтоб был доступен и для TCP-линий.
    2. А вообще идея привязываться к специфике устройств выглядит сомнительно.
  • 08.03.2025: а не сделать ли возможность иметь несколько РАЗНЫХ значений hbt_period, чтобы разные спецификации "on_hbt=" можно б было с разной периодичностью отправлять?

    Пока как-то не особо нужно (разве что у метеостанции -- чтобы GPS-координаты вычитывать не только по "on_conn=", но и, например, раз в минуту), но в будущем может потребоваться.

  • 11.03.2025: Карнаев рассказал, что некий производитель каких-то источников (25.03.2025: «Диком», ИПМРН-20) по согласованию с Димой Липовым реализовал Modbus-интерфейс "с нарушением стандарта" -- чтоб можно было запрашивать МНОГО регистров в одной посылке, по 1000 штук вместо 123/125.

    Стандарт-то это нарушает, но нарушение напрашивающееся (ибо ограничение "256 байт" от балды). Поэтому -- а как бы потенциально поддерживать такое расширение?

    И как указывать: modbus_mon'у -- ну ключ "-U" (Unlimited); а layer'у и драйверу? Layer'у -- видимо, тоже рядом с comopts (аналогично RECONNECT_PERIOD), флажком каким-нибудь?

    24.03.2025: подумал-подумал -- ну нафиг с флагом возиться: разрешение "unlimited" влияет не только на числа-ограничения, но и на размеры буферов. Пусть лучше меняется #define-символом, чтоб влиял на условную компиляцию, а уж включить его можно из командной строки make'а.

    Делаем:

    • Выбрано имя UNLIMITED_MODBUS и ...
    • ...в modbus_conv.h с modbus_mon.c вроде сделано (первоначальный вариант). Изменения при включенном режиме "unlimited" заключаются в следующем:
      • Собственно ограничение: в modbus_max_num1() -- данная точка была выбрана после попыток добавить в другие места, вроде modbus_num1_of_nelems(), как наиболее соответствующая архитектуре.
        • Для всех типов регистров и протоколов возвращает 65536
        • Что, конечно, неправильно: как минимум в TCP как минимум 16-битовые регистры в таком количестве возвращены быть не могут, т.к. там в заголовке пакета 2-байтовое поле "размер данных", а в 65536 байт влезет не более ~32768 каналов.

          26.03.2025: а ещё максимум не 65536 байт, а 65535.

        • Потом надо будет более тщательно оценить физические ограничения для разных протоколов и типов регистров.
      • В modbus_mon.c
        • 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]).

      • И появилось понимание, что в драйверах поддержка будет выглядеть аналогично (или даже проще, т.к. там фиксированных буферов нет, а аллокируются динамически по запрашиваемым размерам). 26.03.2025: угу, похоже, что изменения будут ТОЛЬКО в указаниях максимальных размеров пакетов.

        А пока в modbus_tcp_drv.c, modbus_drv.c и modbus_lyr.h вставлено принудительное "#undef UNLIMITED_MODBUS" перед «#include "modbus_conv.h"», от греха подальше.

      Теперь проверять надо.

    • @вечер: и в modbus_mon-Makefile-src.mk надо бы добавить (чтобы при указании "make UNLIMITED_MODBUS=y" оно б делало CPPFLAGS+=-DUNLIMITED_MODBUS).

      25.03.2025: да, реализовано в обоих -- modbus_mon-Makefile-src.mk и Makefile -- делается добавление "-DUNLIMITED_MODBUS" к CPPFLAGS и LOCAL_CPPFLAGS соответственно.

    25.03.2025: вчера взял у Краснова один источник "для тестов на столе", с вынутыми высоковольтными блоками. Тестируем (подключено к b360mc, т.к. до неё патч-корд дотягивается, а до x10sae не достаёт).

    • Первое впечатление -- да, работает.
    • Второе впечатление -- как-то странно: в ответ на запросы больших количеств приходит явно меньшее количество регистров.
    • Чуток поразбиравшись, в т.ч. с привлечением Wireshark, понял: дело в том, что хоть размер пакета в Modbus-TCP и 16-битовый, но в PDU ответов на операции чтения есть поле "число байт данных", и оно 1-байтовое -- 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 */
      
      -- при нестандартно-больших пакетах "число байт данных" берётся из длины пакета.

      После этого стало показываться правильное число регистров.

    • Но вот запрос более 1019 регистров -- 1020 и больше -- заканчивается ничем: ничего не печатается.

      Разбирательство -- с использованием и показаний 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 (тут в тексте ниже закомменчено целиком).

modbus_drv:
  • 19.12.2024: создаём подраздел, но скорее на будущее, а пока в нём будет пусто.
  • 11.02.2025: вроде как код доделан (ещё энное время назад, сейчас только тестирование layer'а завершено в доступных пределах).

    24.03.2025: при сегодняшней проверке с реальным железом диагностическая печать показала, что в q_enqueue() передавались какие-то дикие значения handle.

    Оказывается, при портировании из modbus_tcp_drv.c первым параметром так и осталось "me" вместо "me->handle".

    Исправлено, и, после исправления другого бага в самом layer'е, заработало.

5:
modbus_conv:
  • 22.07.2023: создаём раздел, в который перетаскиваем записи, сделанные по данной теме за последние несколько дней.

    Вот прямо сейчас штука носит название "modbus_conv.h", но, т.к. конверсия теперь является лишь одной из областей ответственности, явно надо переименовать во что-то более адекватное.

  • 17.07.2023: соображения и процесс изготовления, пока несколько в стиле "потока сознания".

    17.07.2023: решено работу по конверсии сосредоточить в отдельном модуле, которому покамест дано название modbus_conv.h, а помещён он в hw4cx/include/; пока что в рамках одного файла живут и определения, и код (возможно, позже разделим; а может и нет).

    • Главное -- это собственно конвертеры:
      • Определён enum-набор "типов конверсии" 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_* -- для того, чтобы модуль должным образом мог формировать (и дешифрировать) пакеты.
    • При надобности можно будет добавить специальные типы для "особых операций", которые serial only -- вроде "Read Exception Status" (код операции 7).
    • ГЛАВНОЕ: предполагается, что взаимодействие клиентской программы с модулем ведётся именно в этих терминах -- тип протокола, тип регистра, ...
    • Получается этакий "стек функциональности", в виде 3 слоёв:
      1. "Верхний" -- в терминах каналов: информация хранится в chaninfo_t по-канально, так что драйвер работает в терминах каналов, а при обращении к среднему уровню передаёт тому свойства из map[chn].

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

      2. "Средний" -- в терминах "логических операций" ("прочитать сущность указанного типа по такому-то адресу и такого-то размера").

        Это КЛЮЧЕВОЕ изобретение, "расшивающее" логику работы с данными от специфики кодирования протокола. И, по сути, это должно б было присутствовать в самом Modbus'е, но нету.

      3. "Нижний" -- уже в терминах Modbus'ных команд; эта протокольная специфика максимально возможно изолируется от остального.
    • В таком виде этот модуль становится вполне пригоден для использования и в утилите командной строки modbus_test (который может оперировать в терминах логических операций).

      Более того: принцип "пригодность для использования в утилите командной строки" отлично подходит как критерий распределения функциональности между драйвером и модулем: если что-то представляется лишним/неподходящим для утилиты -- значит, этой функциональности не место в модуле.

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

    Сходу пара нюансов:

    1. Введём "зеркальную" функцию DecodeModbusPacket(), получающую на вход буфер с полученным пакетом плюс код протокола и раскладывающую в переданные по указателям переменные информацию о типе регистра, адресе, коде операции и складирующую в указанное место данные.
    2. Существенная деталь: этот дешифровщик НЕ МОЖЕТ произвести дешифрирование данных согласно коду типа конверсии, т.к. ему этот код передать невозможно -- код будет определён ПОСЛЕ дешифрирования пакета КЛИЕНТОМ, на основании пары {kind,addr} и маппирования её на канал.

      Поэтому убираем параметр 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.

      ПОКА ЧТО она делает только это, но в будущем планируется добавить к ней

      1. Парсинг опционального %DPYFMT, в варианте "CONV%DPYFMT:" либо просто "%DPYFMT:" (в последнем случае считается UINT16).
      2. Парсинг опционального "/NUM1".

      Возможность присутствия оных (только для modbus_mon'а) будут указываться в flags, аналогично PAS_-флагам в uspci_test.c::ParseAddrSpec() -- которая, кстати, максимально близка по синтаксису.

    26.07.2023: принято важное решение: в CreateModbusPacket() будет передаваться всё-таки не размер данных в байтах, а именно "число единиц В/В" -- num1; мотивация/детали:

    • Оное num1 требуется не только для команд записи, где есть данные (кореллирующего с num1 объёма), но и для команд чтения -- там надо указывать требуемое для чтения количество.
    • Поэтому расположенный в самом начале селектор-if() по типу/команде теперь не только формирует pdu_hdr[], но и устанавливает значение dsiz -- длины данных в байтах.

      А уж идущий далее код формирования пакета в if()'е по типу протокола -- тот просто копирует в пакет нужный объём, не задумываясь о смысле.

    И приступаем к наполнению DecodeModbusPacket()...

    • Начинаем с обнаружения ошибок -- т.н. "exceptions".
    • Сразу определяем enum-коды MODBUS_ERR_*. 10.09.2023: переименованы в MODBUS_EXC_*.
    • Найден "авторитетный источник" "Modicon Modbus Protocol Reference Guide", на стр.103 содержащий список оных; названия взяты прямо из того списка (с заменой пробелов на '_').
    • Но вот как именно возвращать наверх информацию об ошибке -- пока не совсем ясно. Поэтому покамест принят такой (неудовлетворительный) подход:
      • Информация возвращается в 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: ЗАМЕЧАНИЕ В СТОРОНУ:

    • Конкретно сейчас поддерживаются только основные 4 типа сущностей -- 1- и 16-битовые регистры, представляемые кодами MODBUS_KIND_*.
    • Но так-то в Modbus есть ещё команды для диагностике -- как минимум для serial-линков.
    • Так вот: если захочется поддерживать и те диагностические считывания, то для них будут введены дополнительные коды логических сущностей MODBUS_KIND_*, а функции создания/декодирования пакетов будут их понимать.
      • Такой подход позволит автоматом добавить их поддержку и в драйвер, и в modbus_mon.
      • Будет это сделано разными кодами для разных сущностей или одним кодом -- посмотрим при надобности. Но напрашивается разными, чисто для удобства указания текстом (просто имя, вроде "server_id" (функция 17)).

      (придумано ещё давно, но явно записано только сейчас)

    27.07.2023: продолжаем работы с декодированием пакетов.

    • @утро, просыпаясь: всё-таки крайне неудобно сваливать обработку ошибок ЦЕЛИКОМ на программу-клиента. По-хорошему, ему бы отдавать "логическую" информацию -- kind сущности, на которую пришла ошибка, вид операции (read/write) и код ошибки.
      • Идея: поменять идеологию возвращаемого значения: чтобы отрицательные значения являлись бы именно exception-кодами (с минусом), 0 -- "игнорируй пакет", а >0 -- OK.
      • Тогда можно в kind/op возвращать логическую информацию (дешифрированную из кода команды).
    • Да -- так и сделано.
    • После чего DecodeModbusPacket() доведена до некоего минимального состояния -- понимает MODBUS_TCP, конкретно 3 вида функций (чтение/запись holding registers, чтение input registers (идентично holding)).
      • И тут обнаружилось, что Modbus'овские ответы весьма угрёбищны: в ответах на ЗАПИСЬ указываются и номер первого регистра, и их количество, а вот на ЧТЕНИЕ -- указывается только объём данных (в байтах) и потом идут эти самые байты.
      • Поэтому придётся сваливать часть разбора на программу-клиента, которая должна будет помнить, что же именно она просила читать.
      • ...да, конкретно для Modbus-TCP можно использовать присутствующее в нём значение "transaction ID". Поскольку "опоздавшие" пакеты характерны именно для него, то тут это будет некоторым подспорьем.
      • В остальном же принят такой подход:
        • Ту информацию, что ЕСТЬ в пакете -- она клиенту возвращает.

          Сюда всегда входят kind и op -- поскольку они однозначно определяются по коду функции, а он есть всегда.

        • А для того, чего НЕТ -- возвращаются значения -1.

          Сюда входят addr и num1 для пакетов чтения, а также trans_id для не-TCP.

          28.07.2023@душ, ~16:00: а ведь нифига -- конкретно для ответов на чтения 16-битовых сущностей МОЖНО вывести num1 из количества байт. ...а вот для 1-битовых -- увы, увы: можно только проверять "предполагаемое" по last_sent на совместимость с числом байт.

          28.07.2023@вечер, после прогулки, ~18:00: да, добавлено.

      • Соответственно,
        1. Если на что-то клиент будет получать -1, то придётся это "что-то" смотреть в last_sent;
        2. соответственно, придётся как-то поменять парадигму "что делать при «забывании» пакета".
        3. И, что самое неприятное -- "групповые" чтения (вроде заказа 108 штук holding registers для РПМ-416) будут также вынуждены полагаться на информацию из last_sent.
        4. Очевидно, при таких проверках придётся обязательно проверять соответствие полученного количества байт ожидаемому.

    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;
    • чтоб -TCP и -RTU просто ставили бы указатель на нужное место inpkt,
    • а -ASCII дешифрировала бы hex-последовательность в отдельный буфер, и указатель бы ставила на него.

      Дополнительным плюсом будет то, что так можно будет проще считать LRC -- в этом самом буфере, уже бинарном, а не текстовом.

    30.07.2023: делаем.

    • Вводим вышеуказанные pdu и pdu_len.
    • Переделываем имеющийся код:
      1. Ветка MODBUS_TCP выставляет их значения, ...
      2. ...а блок разбора пакета теперь переделан с адресации к pkt[] на pdu[]...
      3. ...и вытащен в отдельный блок ПОСЛЕ селектора по протоколам.
    • Ну и заодно уж сделаны блок дешифрирования для RTU -- он прост, т.к. сводится к нескольким несложным проверкам (но modbus_rtu_crc() пока только прототип) и выставлению pdu,pdu_len, ...
    • ...и ASCII -- вот тут уже больше возни.
      • Вычитывание 1 байта из hex-строки -- 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:

    • @утро, пультовая после несостоявшейся планёрки, ~10:20: доделал 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 "единиц" может не совпадать с количеством единиц клиента.
    • Сделана поддержка конверсий STRING_HIGH и STRING_LOW -- тут всё тривиально: m2h() берёт только нужный байт, а h2m() заполняет используемый байт и нулит неиспользуемый.
    • ...а также STRING_HIGH_LOW и STRING_LOW_HIGH -- вот тут хитрее:
      • 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: за последние несколько дней доделаны остававшиеся конвертеры.

    • INT32_{LE,BE} и FLOAT32_{LE,BE} реализуются одинаковыми конвертерами DATA32_{LE,BE}_{h2m,m2h}(), которые переставляют байты в uint32 в очевидном порядке. ("Очевидный", конечно, весьма условно: для BE просто стандартная перестановка, а вот для LE -- противно, т.к. это по факту "middle endian" (порядок 16-битных слов -- little, а байтов внутри слова -- big).)
    • BCD_UNSIGNED тоже сделан самостоятельно, ибо прост.
    • А вот INT16SM подсмотрен в drvModbusAsyn.c, ...
    • ...как и BCD_SIGNED -- вот тут я даже не уверен в корректности перенимания.
    • А вот FLOAT64 (только он, т.к. INT64 нету) реализуются также "унифицированными" DATA64_{LE,BE}_{h2m,m2h}(), но
      • уже не сдвигами, &'ами и сложениями, а перестановкой байт.
      • И перестановка выполняется по таблицам modbus_data64_{le,be}_{h2m,m2h}_map[8], в которых указывается, какой байт из источника положить в очередной байт приёмника.
      • А эти таблицы определяются по-разному для LITTLE_ENDIAN и BIG_ENDIAN.
      • 12.10.2023: тогда, в августе, было всё-таки не доведено до конца: таблицы 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'ному формату. Надо вдумчиво ещё разок почитать их код.

      • 12.10.2023: другое дело, что содержимое таблиц _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():

    • Добавлены result-параметры station_id_p и dpyfmt_p (этот должен указывать на буфер char[16]), куда будут складываться результаты парсинга "+ID:" и "%DPYFMT:" соответственно, ...
    • ...а num1_p переименован в nels_p, т.к. "/COUNT" будет указываться во "внешних" элементах данных, а не в регистрах.

      ("/COUNT" понадобится и в драйвере -- для указания команд периодического группового чтения.)

    • И для указания "можно парсить такие-то части" (речь о вышеуказанных троих) введены формализованные MODBUS_PARSE_FLAG_*.
    • Но вот РЕАЛИЗАЦИИ пока НЕТ.

    Кстати, немного сопутствующей информации на тему...

    1. О работе drvModbusAsyn.c:
      • C массивом uint16 делается htons() при отправке и ntohs() при получении, так что сами 16-битные значения регистров (а они там именно epicsUInt16) всегда в нативном host-формате.
      • ...и, глядя на код, такое впечатление, что там работа ведётся СИНХРОННО: отправляется запрос и зависается на ожидании ответа -- это делает функция doModbusIO(), вызывающая pasynOctetSyncIO->writeRead().
      • Кстати, проблема "как делается чтение ответного пакета, учитывая, что в RTU нет поля длины -- анализировать код команды и число байт/регистров?" решена по-простому и в лоб: при ОТПРАВКЕ вычисляется ожидаемый размер ответа replySize и столько и читается вышеупомянутым writeRead().
    2. О форматах вообще (результаты гугления по "modbus bcd-unsigned" и "16-bit bcd signed" соответственно):
      • У Schneider Electric есть короткая страничка "Modbus Data Formats", на которой только ПЕРЕЧИСЛЕНЫ несколько десятков широко используемых форматов плюс ещё толпа "Additional supported formats".

        07.11.2024: тот сайт "Server Not Found", но оно же найдено на более правильном сайте -- "Modbus Data Formats".

      • Вариантов формата "BCD" -- МОРЕ!!! См. в английской Wikipedia статью "Binary-coded decimal" и конкретно разделы "Background" (табличка с 39 вариантами) и "Packed BCD" (тут немного про знаковость).

    09.08.2023: протестирована работа STRING-конвертеров.

    • Работают, правильно.
    • И убедился, что в EPICS делается ровно то же -- проверено сравнением кода на тему "какой байт/байты берётся в каком случае".

    11.08.2023: в modbus_parse_spec() добавлен парсинг опциональных префикса "+ID:" и суффикса "/COUNT"; проверка на наличие делается только при взведённости соответствующего MODBUS_PARSE_FLAG_*.

    24.08.2023: добавлен парсинг "%DPYFMT".

    • Делалось по проекту "суффикс после CONV" -- "CONV%DPYFMT".
    • Собственно работа с парсингом формата в основном просто скопирована из console_cda_util.c::ParseDatarefSpec(), включая добавление перед символом формата модификаторов "ll" для INT64/UINT64 (которые нашим Modbus-стеком пока даже не поддерживаются).
    • А вот сам "разборщик" парсера пришлось слегка усовершенствовать.
      • Поскольку все эти компоненты опциональны, то "суффикс" может оказаться и префиксом -- "%x:41001".
      • Но на случай именно суффикса при "парсинге токенов" терминатором теперь кроме isspace() и двоеточия также считается и '%'.
      • ...также раньше не работала спецификация вроде "41001/10", т.к. при первоначальном "парсинге токена" символ '/' не считался разделителем.

        Теперь добавлено, считается.

      А вообще напрашивается заменить это диковатое условие "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().

      26.08.2023@обед, вернувшись из города с RMB M713/1: сделано. Пытаемся проверять --- проблемы, разного рода...
      • Во-первых, какое, к чёрту, "!isalnum(*p)"?!?!?! Безо всякого "not" -- "!" -- ведь надо как раз IS alnum! Т.е.,
        while (*p != '\0'  &&  !isspace(*p)  &&  isalnum(*p)) p++;
      • Во-вторых, а нафига после этого "!isspace()"? Уж если alphanumeric, то точно не пробел.

        Убрано. После чего и проверка "!= '\0'" стала бессмысленна -- это была лишь защита на всякий случай, если NUL считается за isspace().

      • В-третьих, обламывалось с "syntax error in KIND/ADDR spec" на CONV-спецификациях "INT32_BE" -- потому, что есть подчерк, а он буквой не считается...

        Добавлена отдельная проверка.

      Итого, рабочий вариант сейчас выглядит так:

      while (isalnum(*p)  ||  *p == '_') p++;

      Максимально просто и коротко. А возни-то было...

    • 25.08.2023: маленькое управильнивание: теперь парсеру передаётся не только указатель на буфер 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.
    • И собственно парсинг: поддерживаются все варианты формата -- и оба ID вместе "+123.45678:", и поотдельности любой один -- "+123:" либо ".45678".

    ...правда, пока не проверялось. 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.
    • "station_id" заменено на "unit_id".
    • "trans_id" стало "sync_id".
    • Заодно в 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: разное по мелочи:

    • Сюда перенесена конверсия для 1-битовых каналов, ранее делавшаяся прямо в коде modbus_tcp_drv.c:
      • 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 под рукой нет).

    • А можно вместо вещественных (и возни с форматом "%a", о котором думалось) просто добавить в список конверсий варианты INT64_BE и INT64_LE, и проверять на них.
    • OK -- добавлены, и конверсии те же самые 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 для разблокировки записи.)

    • Но надо ж и работу на BIG_ENDIAN-платформе проверить!

      Для этого в hw4cx/drivers/can/c4l-cangw/Makefile была добавлена shadow-сборка modbus_mon'а. Оно собралось, ...

    • ...и, будучи запущено на CM5307-PPC, прочитало из INT64_BE%018x:HOLD:2141 ровно то же значение.

      И записанное с PowerPC читается с x86_64 таким же.

    0x.0x.202x:

  • 06.10.2023: сделана давно задуманная "упаковка в отдельный .tar.gz-файл для публикации" -- складывание вместе со всеми задействуемыми .h и .c-исходниками библиотек.

    06.10.2023: по большому счёту, работа состояла из 2 частей:

    1. В drivers/eth/Makefile добавлен PHONY-target "modbus_mon.tar.gz", сводящийся к
      • Генерации имени "modbus_mon-YYYYMMDD".
      • Созданию директории "/tmp/modbus_mon-YYYYMMDD".
      • Копированию туда всех задействованных файлов (причём конкретно 4cx/src/include/sysdeps/*.h кладутся в поддиректорию sysdeps/).

        ...плюс локальный modbus_mon-Makefile-src.mk туда копируется под именем "Makefile".

      • Генерации из её содержимого файла "/tmp/modbus_mon-YYYYMMDD.tar.gz".
    2. И собственно modbus_mon-Makefile-src.mk, содержащий краткую выжимку из Config.mk (вызов ) и единственное правило сборки

    Ну да, громоздковато -- особенно туча команд копирования исходников.

    Но зато просто работает.

  • 23.11.2023: уже некоторое время понятно, что придётся-таки как-то поддерживать возможность читать/писать одиночные регистры функциями ОДИНОЧНОГО доступа. Причина -- что некоторые устройства тупо не поддерживают функции векторного доступа.

    Надо как-то продумать архитектуру.

    23.11.2023: продумываем.

    • Собственно формирование и декодирование пакетов -- наименьшая сложность:
      • Определить ещё 6 штук кодов MODBUS_FUNC_nnn.
      • В modbus_decode_rpy_packet() понимать их -- ну громоздко, 4 лишних ветки в первом if()/else-if() и во втором по второму коду в каждом условии.
      • ...некоторый вопрос только в возврате данных от одиночных БИТОВЫХ операций: там ведь вместо битовых кодов используются big-endian значения 0xFF00 и 0x0000.
      • В modbus_create_req_packet() тоже несложно -- ну угромозкится главное условие, да и всё.
      • ...единственный нюанс тот же, что при чтении -- для одиночных БИТОВЫХ операций записи (т.е., конкретно для MODBUS_FUNC_WRITE_SINGLE_COIL) нужно будет подменять данные, перебрасывая указатель m_data на предопределённые uint16-цепочки {0xFF,0x00} или {0x00,0x00} в зависимости от значения младшего битика в *m_data (там просто байт, endian-agnostic).
    • Главный же вопрос -- как УКАЗЫВАТЬ необходимость использования функций одиночности.
      1. Собственно указание пользователем.
      2. Передача этого указания в modbus_create_req_packet().

      Как это сделать?

      • Каким-то общим ключиком режима -- не вариант: может быть ситуация, когда в некоем устройстве часть каналов надо читать одиночными операциями, а часть множественными.
      • Т.е., надо как-то указывать прямо в спецификации операции с регистром, парсимой modbus_parse_spec().
      • @вечер: Позволять указывать ":1" вместо "/COUNT"!

        Т.е., после адреса разрешать не только "/nnn", но и ':', после которого обязательно должно

        Вопрос лишь, как возвращать это. "-1" в *nels_p? Нет, категорически криво. Возвращать слово флагов (которое потом можно передавать в формирование пакета)?

      • 24.11.2023@утро: но тут будет конфликт с "половинными" конверсиями: для MODBUS_CONV_STRING_HIGH_LOW и MODBUS_CONV_STRING_LOW_HIGH валидным является сочетание однорегистровой команды и "/2" (а не только "/1").
      • 24.11.2023@утро, мытьё посуды: а если синтаксис "после адреса идёт ':' и нечто" обобщить до "после адреса можно указывать ':' и некие флаги"?

        В таком случае это ":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, буду тянуть до упора, пока не припрёт.

  • 09.12.2023: мелкая модификация в 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"
  • 23.01.2025: добавлена modbus_strproto() -- в интересах modbus_lyr.c, для выдачи в лог ошибок несовпадения протокола.
  • 18.01.2025: уже некоторое время понятно, что для ТНК придётся работать с ВИП-48, который как ВИП-45, соответственно, с тамошним дурным форматом вещественных чисел -- "float16, но без знакового бита и с десятичной экспонентой".

    Так что с начала января приступил к добавлению поддержки этой фиготы прямо сюда -- чтоб можно было даже в modbus_mon'е смотреть значения.

    "Как делать" -- смотрел в описании от авторов из Выборга (отвратительно описано!) и консультировался с Димой Липовым, занимавшимся этим вопросом в EPICS для СКИФ.

    18.01.2025: что сделано к текущему моменту:

    • Имя для конверсии взято "FLOAT16_10E6M10" -- чтоб не только отражало тип+размер, но и со спецификой кодировки ("10-чная экспонента в 6 бит и мантисса в 10 бит").
    • И даже в modbus_mon'овский help добавлено однострочное описание
      (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", ибо математика.

    • Преобразование выполняется между этим "float16" и float32 (как и у Липового, кстати).
      1. Содержимое FLOAT16_10E6M10_m2h() сделал самостоятельно по описанию от авторов из Выборга и поглядывая на код Димы Липового.

        Причём там вставлена проверка на специальные значения 0xFFFF и 0xFFFE, с переводом их в INFINITY и NAN соответственно.

        Правда, насколько она реально нужна -- вопрос; поэтому пока внутри "#if 1".

      2. А вот преобразование из нормального 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]".

    • Сделана тестовая утилитка work/tests/test_float16_10e6m10_conv.c, пробегающая в цикле все числа от 0x0000 до 0xFFFF, выполняющая преобразование каждого числа из "float16" во 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:

    1. Первым вылазит обсуждение для Python "Decompose a float into mantissa and exponent in base 10 without strings" от 26-07-2017. Почитать интересно (позже понял, что там ответ тоже есть, но "не на том языке").
    2. Второй же поинтереснее: "c - Calculating decimal exponent of double quickly and accurately" от 18-08-2023.

      Там для вычисления значения десятичного порядка предлагается формула floor((log10(fabs(Value)))) -- после отдельной проверки значения на 0 (по сути её же предложили и в предыдущем ответе на Python).

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

      Также там говорится, что «Converting floating-point to decimal efficiently is notoriously "hard."» и даётся ссылка на сопутствующую интересную тему -- ...

    3. ... "Algorithm to convert an IEEE 754 double to a string?" от 22-08-2011.

      16.03.2025: стыдно признаться, но до сих пор так и не прочитал. Впрочем, сейчас мельком глянул -- там, по сути, просто толпа ссылок (в основном на бумажные статьи), а не описание алгоритма.

    12.03.2025: пробуем реализовать.

    • Создаём ещё одну тестовую программку -- work/tests/test_FLOAT16_10E6M10_h2m.c ...
    • ...с функцией-конвертером FLOAT16_10E6M10_f2s() (Float to S -- что под "s" подразумевалось, теперь не вспомню...).
    • В начале которой делается куча проверок -- не NAN ли, не бесконечность ли, не отрицательное ли, попадает ли в представимый диапазон. И только если все проверки пройдены, то делается собственно пересчёт.
    • Точнее, пока НЕ делается, а возвращается 0; но главное пока диагностика: в каждой строке выдаётся исходный 16-битный код, результат его пересчёта во float32, потом обратного пересчёта (пока 0), а ещё -- плюс куча дополнительной информации, вроде выделенных из исходного 16-битного кода мантиссы и экспоненты, а также значения взятого от float32 10-чного логарифма и его целой части.
    • Да, floorf((log10f(fabsf(f32_val)))) даёт значение, совпадающее с выдачей "%e", в т.ч. на "границах" (переходах значения экспоненты) переходы совпадают.

    13.03.2025: Ой ли? А не truncf() ли? Учитывая критику во 2-м результате от позавчера...

    14.03.2025: неа, именно floorf()!

    • Вчера сообразил, что при наличии известной экспоненты мантисса вычисляется как "u16_man = f32_val / powf(10, f32_exp)" и сделал это.

      Результат -- ну да, похоже, но не совсем: что-то как-то не совсем то, шибко уж много несовпадений (бОльшая часть!).

    • Сегодня разобрался:
      1. та формула фактически делала ОДИН знак, т.к. ЗНАЧЕНИЕ, делённое на 10^ПОРЯДОК_ЗНАЧЕНИЯ -- это по определению 1 цифра;
      2. но даже после того что-то было не так: результат прогона "test_FLOAT16_10E6M10_h2m | sort -gk4" в конце выдавал какую-то лажу: там было несовпадение порядков, и даже хуже -- переполнение, когда большой положительный порядок превращался в большой отрицательный.
    • Посмотрев, подумав, поанализировав -- разобрался: всё правильно -- представимы-то числа до 1.023e+34, но при этом мантисса должна содержать 4 знака, а порядок должен быть не более +31 (а иначе +34 и превратится в -30).

      Т.е.,

      • надо "сдвигать влево" знаки (эта идея пришла утром, ещё ДО проверки и увидевания лажи): в мантиссу писать не единственный знак (целую часть ЗНАЧЕНИЯ, делённого на 10^ПОРЯДОК_ЗНАЧЕНИЯ), а результат деления, умноженный на 1000 и при этом уменьшать экспоненту на 3.
      • Естественно, с проверками: что число после этого умножения не вылезет за границы мантиссы (10 бит, т.е., 1023), а ещё что экспоненту есть куда уменьшать.
      • Первоначально думал, что надо будет делать цикл, но потом сообразил: знаков-то максимум 4, так что хватит простой цепочки if()elseif()elseif()else.
    • Сделал -- да, ура!!!
    • Но некоторые числа всё же не совпадают: например, 0x8051 (ага, мнемоника -- Intel 8051 :D) даёт 8.100000e-31 при пересчёте обратно превращающееся в 0x8050, соответствующее 8.000000e-31 -- циферка теряется.

      Анализ показал, что причина в неточности вещественной арифметики: там получается f32_man=8.099999e+00, что после умножения на 10 и оцелочисливания даёт 80 вместо желаемого 81.

    • Можно, конечно, пробовать перед делением прибавлять 0.000001, но ну нафиг: такая стопроцентная точность для ВИПов не требуется, а вещественная арифметика -- это такой can of worms, что эти игрища могут дать какие-нибудь непредсказуемые результаты, гораздо худшие, чем лёгкая потеря точности из-за округления.
    • @вечер: всё-таки добавил прибавление перед умножением -- вроде получшело.

    15.01.2025@утро, перед завтраком, ~09:20: провёл кучу тестов на тему "какую минимальную добавку можно прибавлять перед умножением" -- 0.000001, 0.0000001, 0.0000005, 0.0000009. Результаты тестирования показали, что первоначальная 0.000001 оказалась наилучшей -- все остальные недостаточны: сравнение на равенство вещественных значений, полученных из исходного кода и из "вторичного кода", показало, что несовпадений -- 35, 15890, 1409, 104 соответственно.

    Ну-с, засим теоретическую часть задачи можно считать решённой и переходить к практическим действиям по внедрению.

    • В начало test_FLOAT16_10E6M10_h2m.c::main() добавлено, что если в командной строке что-то указано, то (вместо обычного перебора всех кодов) для всех argv[1...argc] печатается результат преобразования FLOAT16_10E6M10_f2s(atof(argv[x])) -- это для возможности легко проверять конверсию произвольных вещественных значений.

      Сделано несколько таких проверок, включая отрицательные и т.п. -- да, всё выглядит как должно быть.

    • Код FLOAT16_10E6M10_f2s() снабжён большим количеством комментариев с пояснением нюансов.
    • Далее собственно код конверсии скопирован один-в-один (со всеми комментариями) в modbus_conv.h::FLOAT16_10E6M10_h2m(), с удалением старого "не очень работающего" кода.
    • Сделано несколько тестов вроде
      modbus_mon -Dd /dev/ttyS0 +1:FLOAT16_10E6M10:HOLD:0x00/2=1.023e34,1.022e34
      -- судя по дампу "отправляемых" пакетов, значения генерятся разумные.
    • modbus_mon-20250315.tar.gz опубликован.
  • 11.02.2025: в полученном сегодня от Кравеца описании фигурируют 6-байтные значения, занимающие 3 16-битных регистра.

    В принципе, не есть великая проблема добавить поддержку такой сущности, но есть нюансы.

    11.02.2025: краткое обсуждение:

    • Делать можно по образу и подобию INT64, только таблички трансляции переделать с 8 на 6 байт.
    • А тип со стороны сервера -- конечно же, INT64.
    • Но есть один нюанс: если для 64-битных значений знаковость/беззнаковость неважна (сервер тэгирует нужным типом, а битовые представления совпадают), то для 48-битового при конверсии в 64-битовое есть два варианта: беззнаковым надо делать старшее слово равным 0, а знаковым производить расширение 47-го бита в биты [48...63].

      Поэтому придётся вводить не 2 типа конверсии -- INT48_LE и INT48_BE, а ещё парочку беззнаковых -- UINT48_LE и UINT48_BE.

      Причём

      1. Конверсия хост->Modbus будет общей, т.к. просто отбрасываем старшие 16 бит. Поэтому просто пара DATA48_LE_h2m() и DATA48_BE_h2m()
      2. А вот конверсия ИЗ Modbus -- увы, придётся иметь раздельную толпу 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 (там числа всегда беззнаковые, так что проблема расширения знака не стоит).

    Но на будущее -- вдруг таки попадётся что-то -- готовый проект есть.

  • 11.02.2025: а ещё в полученном сегодня от Кравеца описании значится, что некие операции "активация уставки" выполняются не записью в регистры, а user-defined-командами 65 (плавно) и 66 (мгновенно).

    Но у нас такое не поддерживается вообще никак (впрочем, и в EPICS'ном Modbus-модуле и в TANGO'вском "Modbus class" тоже).

    Реализация принципиально затруднительна, т.к. непонятно, как наполнять пакет и как дешифрировать. Но имеет смысл порассуждать, как МОЖНО было бы реализовать, хотя бы какие-то аспекты.

    11.02.2025: рассуждения.

    • Во-первых, очевидно, что реализовать общее решение можно ТОЛЬКО для Modbus-TCP и Modbus-ASCII, но НЕ для Modbus-RTU: просто потому, что в RTU отсутствует длина пакета и её приходится "угадывать" fdio-плагину ProcessIO() по частичному пакету, основываясь на коде команды. А для user-defined-команд формула вычисления неизвестна.
    • Во-вторых, как можно было бы "вписать" работу с user-defined-командами в существующий API? Напрашиваются 2 варианта:
      1. Передавать в kind, например, как kind=1000+function_code: поскольку нынешние значения kind лежат в диапазоне 0-3 (а потенциальные всякие "Read Device Identification" тоже заведомо невелики), то гарантированно не перекроется.
      2. Передавать в op, прямо как op=function: опять же, поскольку нынешние MODBUS_OP_READ=0 и MODBUS_OP_WRITE=1, то перекрытия не будет.
  • 27.02.2025: а ведь теоретически существует Modbus-UDP плюс "UDP-транспорт" -- "Modbus-RTU over UDP" и "Modbus-ASCII over UDP". У нас оно не поддерживается вообще никак. Покамест и не надо, но желательно бы держать в уме, чтобы избежать каких-то архитектурных несовместимостей.

    Несколько замечаний:

    1. Видимо, MODBUS_MEDIA_UDP (и, соответственно, считать бинарно "connect() и возможная пауза перед готовностью -- при MEDIA_TCP" уже нельзя, т.к. для UDP надо делать socket()+connect(), но паузы не надо (т.к. "готово").
    2. содержимое RegisterWithFDIO() станет сильно сложнее, т.к. добавится FDIO_DGRAM;
    3. получение -- видимо, конкретно для UDP сильно упростится, т.к. "1 пакет содержится в 1 датаграмме" и не нужно умничать с определением длины при получении (но вот проверить ПОСЛЕ получения надо -- например, что у ASCII в конце перевод строки).
5:
modbus_mon:
  • 22.07.2023: создаём раздел по этой утилите командной строки и перетаскиваем сюда записи, сделанные за последние несколько дней.

    Назовём утилиту -- в соответствии с принципом от 01-09-2012 из bigfile-0001.html -- "modbus_mon".

  • 17.07.2023@пешком из Ярче домой, проходя вдоль бассейна ВЦ, через лесок от НИПСа и вокруг Пирогова-34: с этим "стеком с modbus_conv.h" реализация modbus_test.c (или modbus_util.c или как его назвать? modbus_mon.c?) начинает выглядеть вполне реализовабельно, обозримо и даже почти просто:
    • Эта софтинка будет работать именно на уровне "логических операций", а возня с преобразованием данных и -- главное! -- с конкретными кодами команд ложится на тот модуль.
    • Таким образом, принятие указаний из командной строки сводится к парсингу имено ЛОГИЧЕСКИХ сущностей, каковой является несравненно более простым, чем опознание команд, значений байт и конверсии.
    • В таком случае напрашивается синтаксис, аналогичный uspci_test/*vme_test, где:
      • общий формат команды --
        *modbus* DEVSPEC [COMMAND...]
      • причём в общем случае этот "DEVSPEC" будет включать что-то вроде спецификации протокола и адреса --
        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...}]
        где
        • Опциональный CONV -- одно из названий из modbus_conv_table[*].name, при неуказанности считается за UINT16 (хотя для coils и discrete inputs -- хбз).
        • Парочка "TYPE:ADDR" может указываться либо полностью -- например,
          HOLD:1000
          либо "Modbus-style"
          41001
          либо извратно:
          4:1000
          (все три спецификации эквивалентны, но первая и последняя указывают адрес явно).

          P.S. Кстати, указания для драйвера надо делать аналогично; в идеале -- свалить бы это в общий кусок кода. 21.07.2023: из-за CONV%DPYFMT может не получиться.

          21.07.2023: Wikipedia предлагает ещё варианты ("де-факто"):
          • 401001 -- ВСЕГДА 6 цифр, из которых последние 5 являются адресом плюс 1.
          • 4x03E8 -- шестнадцатиричная нотация.
      • И да -- напрашивается также
        :[microseconds-to-wait]
        (видимо, просто ради задержки перед отправкой следующей команды), ...
      • ...а для serial-вариантов осмысленным выглядит и просто ":" -- это просто режим сниффинга, когда монитор запускается на отдельном ("третьем") порту RS485-линии.

        21.07.2023: вот только сам режим сниффинга -- дико проблемная штука, из-за того, что в пакетах ОТСУТСТВУЕТ указание "направления" (хост устройству или наоборот), а сами пакеты при этом различаются -- причём авансом это понять невозможно; а в случае конкретно RTU, где длина ПАКЕТА в явном виде отсутствует и должна быть угадана из PDU, для кодов 1,2,4,3 в ответном пакете "число байт" расположено в том же месте, что первый байт "адреса первого", а для 15,16 начала ответов те же "адрес первого, количество", но затем просто окончание PDU. Так что даже просто парсинг RTU -- та ещё задачка, для которой придётся использовать FDIO_STREAM_PLUG.

    • Другой вопрос -- а что делать с ПЕРИОДИЧЕСКИМ опросом? Указывать ключиками что-то вроде "-p PERIOD:COMMAND" как-то не хочется: это фиг знает как будет интерферировать с прочим, да и потенциально указывабельные несколько команд с разным периодом -- та ещё радость (sendqlib сюда прикручивать?!).
      • 18.07.2023@совет по автоматизации: лучше дать возможность читать список команд из файла -- "-f FILENAME", как в cdaclient, но только тут читать и исполнять именно по мере поступления; тогда можно просто через pipe от другой программы скармливать.
      • 18.07.2023: однако для каждой ОТПРАВЛЯЕМОЙ команды надо будет иметь "таймаут" -- сколько ждать ответ перед переходом к следующей; для TCP это по умолчанию бесконечность, а для RTU -- хбз. Вопрос в синтаксисе: как указывать (нужно мочь по-командно)?
      • 10.08.2023@круглый стол, встреча с представителями Курчатника по ТНК, ~11:00: а может, всё-таки уметь понимать указание "вот эту команду отправлять с такой-то периодичностью и столько-то раз"? И чтоб можно было и периодичность указывать 0:"максимально быстро" и число раз 0:бесконечно. Тогда в каждый момент будет ровно ОДНА "периодическая" команда, так что никакой sendqlib не нужен. (Это придумалось сидя рядом с Антоном Павленко и раздумывая о том вакуумном контроллере, который иногда подвисает, а его проверяют постоянным поллингом чего-то).
      • 11.08.2023@утро-зарядка: откуда опять встаёт вопрос о синтаксисе. И тут пара соображений:
        1. Каждой команде надо мочь указывать ТРИ параметра: таймаут (=бесконечность?), число раз отправки (=1) и интервал повторений (=0us, максимально быстро?).

          КАК указывать? "[@PARAM=VALUE{,PARAM=VALUE}:]"? Сгодилось бы для psp_parse(...,terminators=":").

        2. Но ведь желательно бы иметь возможность, как у cdaclient и *canmon, чтобы результат вывода можно б было подавать на вход этой же команде для воспроизведения.

          Но там обозначением префикса-времени является как раз '@', так что нельзя.

          (И чтоб в перспективе подходило для сбагривания histplot'у, но там префикса нет, так что задача чуть отдельная.)

        Пока что напрашивается опять точка -- "[.PARAM=VALUE{,PARAM=VALUE}]:"; т.е., например, ".times=0,period=1000,timeout=1000:...".

      • 18.08.2023@пешком около ИПА, по дороге из дома в сторону Морского: в той модели "исполнять 1 команду с опциональными таймаутом, числом и интервалом повторений" всё хорошо (и красиво и несложно реализуемо; и да, на cxscheduler'е, как было спроектировано 11-08-2023 ниже).

        Кроме одной мелочи: а если всё-таки захочется уметь НЕСКОЛЬКО команд выполнять периодически? Совсем-совсем никак и только через pipe, или можно придумать способ указания таких групп?

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

        • Главное -- как указывать (синтаксически).

          20.08.2023: вариант -- использовать подчерк '_', его можно прямо в командной строке указать и он ни с чем не пересекается (25.08.2023: кроме имен конверсий, но там синтаксически всё однозначно разделимо).

          24.08.2023@холл перед залом круглого стола, ~17:00: а ещё дозволять просто пробел -- тогда всё ложится на чтение команд из файла: одна строка -- одна группа. Но можно и точку с запятой ';' -- тогда из shell'а такие группы указывать в кавычках: "CMD1; CMD2".

        • Но и как исполнять -- тоже не вполне ясно. Парсить всю группу сразу и складывать в "цепочку" (при отсутствии скобок состоящую из единственной команды)?
    • 20.07.2023: при дизайне DecodeModbusPacket() осознано, что насчёт конверсии есть нюансик: при ОТПРАВКЕ-то конверсия берётся из спецификации; а при ПОЛУЧЕНИИ?
      • Видимо, надо помнить в каком-то SLOTARRAY'е список конверсий по дуплетам {kind,addr}, и для приходящих пакетов выполнять поиск по той табличке, а не запоминать от предыдущего отправленного.
      • Тогда будет корректно дешифрироваться даже при "некорректном" приходе пакетов (мало ли почему порядок перепутается).
      • А если подряд несколько команд с РАЗНЫМИ типами конверсии -- ну просто складировать в таблицу ПОСЛЕДНИЙ.
      • Вопрос будет только в режиме сниффинга: откуда брать информацию для регистров, к которым из самой командной строки обращений не было?
        • Можно ввести специальный ключик командной строки "-C CONV:TYPE:ADDR", который бы не приводил ни к какому В/В, а лишь складировал бы в табличку код конверсии.
        • Но, возможно, для заранее неизвестных типов данных будет хотеться иметь возможность посмотреть на разные варианты.

          поэтому можно предусмотреть возможность указывать СПИСОК конверсий "по умолчанию", для отсутствующих в таблице каналов -- что-то типа "-U CONV{,CONV...}", и чтоб печатались результаты преобразования во ВСЕ эти варианты.

          ...естественно, с ограничением "подходит по размеру данных" -- т.к. UINT32 нельзя использовать ни для единственного UINT16-значения, ни для вектора, не подходящего по кратности (нечётной длины; а для FLOAT64 -- не кратного 4).

    • 20.07.2023: а ещё отдельный вопрос с ФОРМАТАМИ ПЕЧАТИ: для целых-то почти ясно, хотя есть шестнадцатиричка, а для вещественных?
      • Видимо, взять синтаксис из cdaclient: опциональный префикс "%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:

    1. В КАЖДОЙ команде...

      Напрашивается опциональный префикс "/STATION_ID:".

      (Первоначально думалось о префиксе c точки -- ".STATION_ID:", но этот вариант не подходил бы для указания в DEVSPEC, т.к. конфликтовал бы с указанием IP-адресов или FQDN'ов без порта.)

    2. А ещё в DEVSPEC'е указывать station_id. Пока вытанцовывается "/STATION_ID" после имени хоста; но что с COM-портами делать? Выйдет ведь "/dev/ttyS0/5"...

    01.08.2023@около проходной ОК, по дороге на обед в Гуси, ~13:30: чисто по таблице ASCII остаётся лишь 4 нейтральных (для shell'а) символа: ',', '.', '/', ':' (не считая странно выглядящего и не-пришей-кобыле-хвост '^').

    • Как префикс перед командой годится любой из них.
    • После имени хоста для TCP -- не катят двоеточие (опциональный порт) и точка (разделитель IP и FQDN).
    • После имени COM-порта -- не катят вообще все, хотя двоеточие в меньшей степени (в именах файлов почти не встречается).

    Ну и что -- разрешать ЛЮБОЙ из этих символов, при условии, что он в конкретной точке не может иметь иного значения (например, "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@утро-зарядка:

    • по-хорошему, утилитка должна понимать те же ключики управления форматом вывода, что cdaclient: -Dn, -DT, -Do, -Dq, -De.
    • И собственно код печати можно брать практически тот же, что у -- cda-утилит ведь делать надо то же самое.

    В идеале -- не копировать, а использовать прямо тот же код. Но, увы, не удастся -- там слишком много завязок на cda.

    11.08.2023: некоторые соображения по scheduling'у, вроде тривиальные, но заслуживающие явного проговаривания:

    • Работу с Modbus надо реализовывать на основе fdiolib, а следовательно -- использовать и cxscheduler.
    • Как делать чтение из файла?

      ЛУЧШЕ ВСЕГО -- просто тупым циклом "while (fgets(...) != NULL)".

      А исполнение команды (в т.ч. цикла повторений) должно начинаться с sl_main_loop() и заканчиваться вызовом sl_break().

      ...хотя как именно это должно быть интегрировано в функцию "исполнить команду", в т.ч. как соотноситься с исполнением ОТСЫЛКИ -- пока не вполне ясно.

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

    • Но бывают мысли и "чтение из файла тоже делать через fdiolib, посредством FDIO_STRING".

      В таком случае нужно будет на время работы с линией "отключать" чтение из файла, а на время ожидания команд из файла "отключать" линию. И это при том, что регулированием текущих SL_-масок занимается сам fdiolib и вмешиваться в его работу не стоит, так что только fdio_deregister().

      Откуда очевидным образом следует, что НЕ стоит файл читать fdiolib'ом.

  • 24.08.2023: пора уже приступать к реализации утилитки -- теперь есть ВСЯ требуемая инфраструктура, реализованная в modbus_conv.h, плюс "мозги" коннекчения (и РЕконнекчения) в modbus_tcp_drv.c, да и общее понимание уже давно образовалось.

    24.08.2023@холл перед залом круглого стола, кресло со стороны столовой, лицом к круглому столу ~17:00: общая структура утилиты напрашивается такая:

    • main() работает "общим менеджером" -- парсит командную строку и, при надобности, указанные файлы строка-за-строкой, сбагривая команды (не разбирая их, т.е., это могут быть и ГРУППЫ) функции-исполнителю.
    • Функция исполнитель -- что-то вроде "PerformIO()" -- организует как отправку команд и печать ответов, так и надлежащие повторения и в требуемом количестве и с требуемым периодом.

      25.08.2023: также команда задержки может быть просто среди этих команд в группе, что позволит "прореживать" исполнение группы паузами при необходимости.

    • Вопрос остаётся лишь во взаимодействии этого всего с собственно коннектами и реконнектами.
    • P.S. А вот РЕконнекты надо поддерживать только при указанном "-K" ("keep going after I/O errors (do NOT exit)").

    25.08.2023@ключи, домик, веранда-предбанник/крыльцо, ~16:30: насчёт вопроса о "взаимодействии этого всего с собственно коннектами и реконнектами":

    • Ответ -- делать ВСЮ работу с командами принципиально асинхронной.
    • Это выглядит аналогично модели функционирования seqexecauto и vdev.

    Т.е., в начальной, "синхронной" части "PerformIO()" -- только подготовка, в виде

    1. парсинга всех команд группы в массив;
    2. заказа таймаута через 0 микросекунд для исполнения первой команды из группы и последующего вызова sl_main_loop().

    (@дома, ~18:30: или заказ "через 0 микросекунд" делать по FDIO_R_CONNECTED?)

    @дома, ~18:40: Некоторый вопрос по тому, как реализовывать "периодичность":

    • Просто заказывать таймаут на момент окончания (или на "через период"?) нельзя, т.к. команды могут и не успеть исполниться.
    • Возможно,
      1. надо в обработчике этого таймаута взводить флаг "период прошёл",
      2. а в callback'е по приходу ответа при отработке последней команды -- флаг "все команды выполнены",

        06.09.2023: этот флаг стал ненужен -- логика TryNextCommand() обходится без него.

      3. и из обоих точек проверять оба флага, и если они оба взведены -- то производить те действия, что по окончанию очередного цикла (запуск следующего цикла либо вызов sl_break(), если всё исполнено).
      4. ...а если повторов не требуется, то флаг "период прошёл" можно взводить сразу; как и при неуказанности длины периода (period=0).

    27.08.2023@утро-зарядка: ещё пара замечаний в ту же сторону, насчёт "когда слать":

    1. Конечно же надо иметь флаг "is_ready", обозначающий приконнекченность, и использовать его как обязательное условие для возможности отправки.
    2. Если в какой-то точке считается, что можно делать отправку запроса -- то слать СРАЗУ, безо всякого заказа таймаута в 0 микросекунд.

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

    27.08.2023: начато наполнение скелета. Парсинг ключей командной строки копируется из cdaclient.c (включая -D), с удалением лишнего и небольшими изменениями, с подглядыванием в прочие uspci_test.c и canmon_common.c.

    28.08.2023: продолжаем.

    • Скелет в main() сделан: проверяет обязательное присутствие DEVSPEC, вызывает его парсинг и затем инициирует подключение к устройству вызовом StartConnect(). 08.09.2023: InitiateStart().

      После чего идёт по оставшимся argv[], вызывая либо исполнение команды, либо чтение из указанного файла.

    • ЗАМЕЧАНИЕ: в отличие от других утилит, тут "читай из файла" указывается просто указанием имени этого файла в командной строке вместо просто Modbus-команды:
      • "-" -- читать с stdin'а (берётся просто /dev/stdin).
      • Что угодно, выглядящее как абсолютный путь или простейший относительный -- начинающееся со "/", "./" или "../".
    • За вычетом замены "-" на "/dev/stdin" содержимое ReadFromFile() целиком взято из cdaclient.c (включая rtrim, ltrim и пропуск @TIME в начале строки); а, ну да -- ещё "отработка" строки делается вызовом PerformIO() вместо "RememberChannel().

    Оно компилируется (хотя в бинарник пока не соберётся из-за отсутствия функций-"мяса").

    30.08.2023@Зеленоград: стало очевидно, как именно надо парсить параметры групп (число повторов и период) и индивидуальных команд (таймаут):

    • PSP-таблица будет одна, со всеми 3 параметрами.
    • Но для 1-й команды будет передаваться указатель на таблицу, ...
    • ...а для последующих -- на таблицу+2.
    • И при вызове для первой команды из структуры-результата парсинга будут копироваться в "переменные контекста" два "глобальных" параметра, а таймаут в описатель команды, а для последующих -- только таймаут.

    Таким образом, для первой команды будут распознаваться все параметры, а для последующих -- только таймаут.

    P.S. equals_c='=', separators=",", terminators=":".

    31.08.2023: насчёт возможности указания "trans_id" в описателе команды (и печати его, если попросят):

    • @A320 "Семён Челюскин" из Москвы с ТНК в Нск, ~01:30NSK, перед "ужином", моя руки: указывать его можно после StationID -- чисто по символам получается, что через запятую ',' (ибо точка '.' занята -- она предполагается префиксом параметров команд): "+123,45678" (StationID=123, TransID=45678).
    • @утро, позавтракав, ~10:00: всё-таки неудобно/некрасиво через запятую: лучше б было через точку '.'. 04.09.2023: да, парсинг такого формата в modbus_parse_spec() сделан.

      Но для этого надо освободить её от функции "символ-префикс-интродуктор параметров", т.к. формально возможен синтаксис с указанием ТОЛЬКО TransID (и он даже более осмыслен, т.к. оно поддерживается только в Modbus-TCP, в котором как раз StationID малоосмысленно).

    • @дорога на обед из П28 в Гуси, около Алекты, ~13:00: ну так заменить "символ-префикс-интродуктор параметров" на '/', тогда точка освободится!

      Получится синтаксис вида "/timeout=100000:+123.45678:..." (изначально 02-05-2023 именно так планировалось для указания поканальных параметров в histplot, но 19-05-2023 было осознано, что это пересечётся с PK_FILE).

    • @вечер, дома, записывая предыдущее: а может проще -- вообще БЕЗ символа-префикса для параметров? Т.е., просто смотреть, что если команда начинается с имени какого-то из параметров, за которым идёт '=', то переходить в режим парсинга.

    31.08.2023@дорога домой из Гусей: возможно, полезен также будет ключик "выдавать побайтный дамп Modbus-пакета".

    • Может, "-Dd" (Dump)? 09.09.2023: сделано.
    • Возможно, не помешает также выдача декодированных данных: код команды, контрольная сумма. "-DD"?
    • ...ну и понятно, что для ASCII-версии завершающие CR-LF будут отсутствовать...
    • 01.09.2023@утро-завтрак: а по ключику "-Ds" (print Sends) при указанном "-Dd" надо и дамп отправляемого пакета показывать.

    01.09.2023: сделан ParseDevSpec() -- больше сотни строк в основном всяких проверок и ругательств.

    • Сначала проверяется на наличие префикса "тип соединения" -- всякие там "ascii_over_tcp" по списку conn_types[] (где для каждого указаны media и proto), и если есть, то сохраняется, а если нет, то "предполагает" тип: начинается со '/' -- SERIAL,RTU, иначе -- TCP,TCP.
    • Для проверки "есть ли в начале строки префикс" сделана string_begins_with(), которой кроме ключа-префикса указывается, какой за ним должен идти символ (обычно ':' или '=').

      Потом пригодится и для парсинга параметров команд -- определять факт наличия этих параметров.

      02.09.2023: да, сделана string_begins_with_param(), принимающая вторым аргументом указатель на PSP-таблицу и перебирающая все параметры в ней на тему "а не string_begins_with() ли с именем этого параметра".

    • Парсинг и резолвинг имени скопированы из modbus_tcp_drv.c.
    • Замечание: для media=TCP при proto=TCP по умолчанию берётся стандартный порт, а для остальных протоколов (т.е., "*_over_tcp") указание порта обязательно.
    • Ну и ещё для парсинга параметров команд заведены структура 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().

    • Главное: она работает с ЦЕПОЧКОЙ команд, выпарсиваемой из переданной ей строки.
      1. Разделителями воспринимаются пробел и точка с запятой ';', а также подчерк '_'. В первом случае команду из командной строки надо указывать в кавычках, последний же вариант с подчерком позволяет обходиться без них.
      2. С решётки '#' начинается комментарий; он может быть лишь там же, где разделители -- т.е., МЕЖДУ командами.

        Основное применение -- файлы со "сценариями".

    • На каждом шаге:
      1. Сначала делается проверка string_begins_with_param()'ом на "префикс" с PSP-параметрами (для 0-й команды все 3, для следующих только 1 последний) и если они указаны, то парсятся, ...
      2. затем проверяется, что если далее идёт ':', то это считается указанием паузы ":NNN", ...
      3. иначе парсится Modbus-команда обычным modbus_parse_spec(), причём тут можно указывать ВСЕ "нюансы" -- включая и Station-ID, и Transaction-ID (которые при неуказании считаются за -1 и будут "наследоваться от контекста").
    • Парсинг выполняется в ДВА прохода:
      • на первом подсчитывается количество команд и необходимый объём "сегмента данных" (для записи),
      • в его конце делается аллокирование (точнее рост) буфера для "кода" и "данных",
      • а на втором проходе в подготовленный буфер складываются "код" и "данные".
    • ЗАМЕЧАНИЕ о выравнивании:
      1. Объём "сегмента кода" добивается до кратного 16 (чтобы удовлетворить требованию на выравнивание; а выравнивание на 16 общего буфера гарантируется malloc()'ом).
      2. Для блоков данных будет выполняться выравнивание по их требованиям.

      Ровно как это делается в 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;
              }
          }
      }
      

      Философские комментарии:

      • Получилось весьма высокоинтегрированно и высокооптимизированно -- сварганил менее чем за час, но высоконапряжённо (чуть голова не заболела).
      • Идеологически слегка похоже на основные циклы в cda_f_fla.c::process_commands() и (в меньшей степени) vdev_set_state().

        А вот на seqexecauto.c::ExecuteFrom() похоже не особо.

      Технические комментарии:

      • Это ОДИН общий цикл "пока можно", как бы имитирующий работу некоего виртуального процессора.

        Переход между периодами делается прямо там же, после чего сразу производится попытка выполнить следующее действие.

      • "!is_ready" проверяется перед каждой итерацией попытки исполнения -- на случай, если после очередной команды связь порвалась (мало ли -- при отправке обнаружился облом).
      • Паузы реализуются посредством SleepBySelect() прямо "по месту", т.к. реализация через cxscheduler'ные таймауты была бы шибко проблемной: не говоря об общей громоздкости, пришлось бы ещё как-то блокировать на это время приход пакетов.
      • all_commands_done очевидным образом стала ненужной и убрана.
      • Как и никакого "заказать таймаут через 0" теперь нету -- действия исполняются сразу.
      • Возвращается int, чтобы указывать вызывающему -- только в лице PerformIO() -- надо ли реально запускать цикл, или же все действия цепочки уже выполнены (это возможно в случае, если единственными "командами" были паузы).
    • "Старт исполнения" в конце PerformIO() выглядит так:
          // Reset "virtual machine" context to beginning
          period_idx  = 0;
          StartNewPeriod();
      
          if (TryNextCommand()) sl_main_loop();
      
    • Предполагается, что StartNewPeriod() будет вызываться также
      1. по FDIO_R_CONNECTED;
      2. после получения ответа (включая exception);
      3. по таймауту ожидания ответа.

    Теперь остались 2 больших куска:

    1. Непосредственно работа с линией -- поддержание соединения, отправка и приём пакетов. Тут вопросов нет -- надо брать готовое из modbus_tcp_drv.c.
    2. Печать полученных данных. Тоже ничего принципиально сложного, просто объёмисто, но значительную часть можно позаимствовать из console_cda_util.c.

    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(), вызываемый и при отправке, и при приёмке.
      • Выдаёт он данные с префиксом '#' (шоб игнорировались при скармливании самой утилите).
      • Формат -- "# dump SENT TCP: данные"; для принимаемых будет "RCVD", для RTU и ASCII будут их имена вместо TCP.

        Для TCP и RTU данные выдаются шестнадцатиричкой, по 2 цифры на байт; для ASCII -- строкой, причём не-':' и не-шестнадцатиричные, которых по идее быть не должно, печатаются в формате "\xNN".

      • Дампы выдаются ПЕРЕД отправкой (так что при её ошибке всё равно будут) и ПОСЛЕ декодирования, причём при ошибке вместо дампа будет просто строка "# RCVD MALFORMED packet %zd bytes\n". 10.09.2023: переделано -- теперь выдаётся полноценный дамп, просто с припиской "MALFORMED".
    • Также добавлена печать таймаутов, отправок и подтверждений записи (ключи "-Dt", "-Ds", "-Dr" и флаги print_timeouts, print_sends, print_wr_replies соответственно).

    А потом начинаем проверять.

    • Вылезло несколько ляпов:
      1. "Угадывание" типа протокола по внешнему виду DEVSPEC было перепутано.
      2. В общем цикле по строке в 3-м "параметре for()" имелось лишнее "p++", из-за чего после разбора команд оно перескакивало через '\0' и пыталось парсить environment.
      3. При парсинге ":NNN" было забыто "p++ -- пропускание двоеточия -- перед strtol().
      4. В конце парсинга цепочки было забыто "command_count = idx;".

      Они были оперативно найдены и исправлены, и, ...

    • ...похоже, инфраструктура работает! Ура-ура-ура!!!
    • И алхимия с параметрами period и count тоже работает, хотя тоже вылезла пара косяков:
      1. period_end_proc() вызывал TryNextCommand() безусловно, прямо посередь ожидания ответа (а надо -- только при "command_idx >= command_count").
      2. StartNewPeriod() НЕ делал "period_has_passed = 0" -- из-за чего корректно работал только первый период, а последующие всегда считались завершёнными и длительность игнорировалась.

    Итак, битым текстом: задуманная схема "скриптинга" РАБОТАЕТ В ПОЛНОЙ МЕРЕ! И цепочки команд, и возможность указать число повторов для цепочки, и периодичность (длительность) исполнения одного повтора.

    11.09.2023: сделана печать.

    • "Мясо" этой печати взято из console_cda_util.c и помещено в отдельный файл hw4cx/include/console_mon_util.h -- он может пригодиться и для иных консольных утилит-мониторов.
      • Главное -- PrintDataData(), модифицированная копия PrintDatarefData().
      • Поскольку тут нет cda, то параметры/свойства данных передаются явными параметрами -- data, dtype, nelems, dpyfmt.
      • Для печати передаются 2 параметра-функции:
        1. name_printer -- выдаёт "имя" в нужном формате; вызывается при != NULL.
        2. data_printer -- служит для выдачи нестандартных данных, конкретно -- 1-битовых (COIL и DSCR), которые общая инфраструктура печатать не умеет.

          При указанности -- != NULL -- используется вместо собственного цикла выдачи.

    • Использование этого в собственно modbus_mon.h -- передаются:
      • PrintRegName() -- печатает "имя" в соответствии с print_-флагами.
      • PrintBitData() -- печатает битовую цепочку; указывается в ветке COIL и DSCR.
    • 04.01.2024: только косяк тогда допустил: при печати БИТОВЫХ регистров -- COIL и DSCR -- в PrintDataData() передавался buf, в точности как для 16-битовых. Но у тех-то предварительно делается конверсия из m_data в этот buf[], а тут -- нет.

      В результате печаталась какая-то чухня -- мусор, имевшийся в стеке.

      Исправлено заменой на передачу сразу m_data.

    24.09.2023: продолжаем.

    • Добавлена поддержка "quiet" -- при указанном флаге "-q"/option_quiet просто не делается никакая печать данных.
    • Добавлена/управильнена работа с num1/nelems посредством modbus_num1_of_nelems() и modbus_nelems_of_num1() -- в modbus_conv.h оно было сделано и использовано в modbus_tcp_drv.c ещё 13-09-2023, а сейчас задействовано и тут.
    • Улучшен help; в т.ч. в расширенный (по "-hh") добавлены выдача списка типов конверсии, таблица с описанием форматов адресов регистров, плюс в почти свободной форме "Simple scripting abilities".

    28.09.2023: за вчера-сегодня сделана поддержка Modbus-RTU и работы с serial-link'ом.

    1. RTU:
      • Главное -- в 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 (реально сначала именно там делалось, сюда копировалось).

    2. Serial-link:
      • Добавлена 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: реализована поддержка записи. Поскольку основная "сложность" -- парсинг -- была решена ещё вчера копированием готовенького, то особых трудностей не встретилось.

    • Буквально пара десятков строк кода -- отдельные ветки для COIL и HOLD, содержащие вызов парсинга (COIL -- в 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: пара мелочей насчёт вывода:

    • Добавлен display-флаг "u" -- -Du, отображающийся на print_unit_ids и приводящий к выдаче "+UNIT_ID:".

      Работает только при включенном -Dn -- при отсутствии "имени" регистра и префикс печатать не к чему.

    • В console_mon_util.h::PrintDataData() разделитель, печатаемый при взведённом UTIL_PRINT_NAME после имени, переделан с пробела " " на "=".

      Смысл -- что modbus_mon (в отличие от cdaclient) воспринимает пробелы как разделитель не между именем и значением, а между РАЗНЫМИ командами, поэтому пробел он будет считать окончанием команды, т.е. -- командой чтения, а значение попробует интерпретировать как следующую команду.

      Так же получается цельная команда "РЕГИСТР=ЗНАЧЕНИЕ", так что вывод modbus_mon'а теперь можно скармливать ему же на вход (раньше -- не сработало бы).

    Памятуя существующую функциональность чтения команд из файла, позволяющую в т.ч. работать "в интерактивном командном режиме", пора бы её проверить.

    • Сразу добавлена мелкая декоративная удобность: если и входной поток, и STDOUT по данным isatty() смотрят на терминал, то перед чтением команды выдаётся prompt "> ".
    • Чтение из файла -- работает. Интерактивный режим -- тоже работает.
    • Но вот какой-то баг проявился с аллокированием памяти -- вылазит при чтении команд из файла (хоть из файла/pipe, хоть интерактивно): после завершения чтения на "длинных" командах (с большим объёмом данных, вроде "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. Если указать адрес несуществующего регистра, то вместо ответа exception'ом -- например, ILLEGAL_DATA_ADDRESS -- она просто молчит.
    2. 0-й блок данных -- 0-90, HOLD:0/90 -- читается как целиком, так и индивидуальными регистрами.

      А вот 1-й блок -- 512-527, HOLD:512/$[527-512+1] -- читается ТОЛЬКО ЦЕЛИКОМ. Попытка прочитать любой одиночный адрес, кроме 512 -- также просто нет ответа.

    Насчёт настройки serial-порта:

    • Сейчас-то в код прямо зашито скопированное из stdserial_hal.h::serial_hal_opendev()
      newtio.c_cflag     = B19200 | CRTSCTS*1 | CS8 | CLOCAL | CREAD;
      newtio.c_iflag     = IGNPAR;
      
      (только вместо B19200 в оригинале было параметром baudrate, передававшимся "юзером", и уже в /piv485_lyr_common.c было загито B9600).
    • Но вообще надо бы как-то уметь указывать это из командной строки.
    • @~14:00, спускаясь по лестнице П28 по дороге в ИЯФ: некоторые соображения (возможно, и раньше что-то думывал):
      • Можно указывать PSP-опциями перед именем устройства, в формате "@OPTIONS:/dev/tty*".
      • Где опции -- как скорость (числами), так и всякие even, odd, parenb, ...
      • И отдельный ключик (чтоб он был по умолчанию?) "don't touch" -- т.е., открыть serial-link как есть, НЕ делая никаких tcsetattr().

        06.01.2023: а это решено было НЕ реализовывать. Ибо нефиг.

    • @ИЯФ, ~15:40: а ведь есть некий стандартный/общепринятый синтаксис указания параметров, вроде "19200,n,8,1..." (фиг знает откуда -- кабы не из DOS'овской команды "mode COM1: ..."), и вот в идеале бы сделать, чтоб прямо в том стиле и можно б было указывать.
    • @вечер: да, MODE COMn MODE (Configure Serial Port):
      MODE COMm[:] [b[,p[,d[,s[,r]]]]]
      
      MODE COMm[:] [BAUD=b] [PARITY=p] [DATA=d] [STOP=s] [RETRY=r]
      

    26.10.2023: приступаем; покамест всё в ParseDevSpec():

    • Добавлено распознавание префикса '@' перед именем устройства.
    • ...причём если спецификация начинается с '@', то при неуказанном типе это считается за serial/RTU, как и начинание с '/'.
    • И при наличии оного префикса выполняется PSP-парсинг.
    • Табличка же text2comopts[] пока простая -- содержит лишь список скоростей, от 50 до 115200, PSP_FLAG()'ами.

    29.10.2023: вроде доделано, за вчера и сегодня.

    • Изучение документации показало, что ВСЕ эти 4 параметра -- baudrate, parity, csize, stop bits -- идут в c_cflag.
    • "Названия" значений параметров не имеют пересечений -- так, "8 data bits" обозначается "8", а "2 stop bits" -- "2".

      Поэтому ВСЁ можно делать PSP_FLAG()'ами, позиционно-независимо (это осознано ещё несколько дней назад).

    • В результате принято простое архитектурное решение:
      • 4 параметра парсятся независимо, в отдельные 4 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, дошедшей до конца -- т.е., уже за пределами инициализированной части.

    Поэтому приняты меры для приведения деятельности в правильное русло и устранения проблемы (делалось не совсем в описанном порядке, а доводилось итеративно, но конечный результат именно такой).

    1. Всякие необходимые подготовительные действия:
      • К last_sent было добавлено поле op, и в forget_last_sent() ему делается =-1.
    2. Главное -- в 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 из условия убрано.

    3. Дополнительно:
      • Оказалось, что, опять же, в отличие от modbus_tcp_drv.c, forget_last_sent() НЕ делается ни при получении пакета, ни по таймауту.

        Сделано.

      • Введён display-флажок "печатать уведомления о неожиданных/несоответствующих-запросу пакетах": 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: пара дополнений:

    1. forget_last_sent() в приёме делается только при is_expected (ибо раз "не тот", то и нефиг, а надо дождаться "того");
    2. "переход к следующему шагу" выполняется тоже только при is_expected, а exception вариант условия убран (по той же причине).

    И вот это уже нужно проверить вживую.

    07.02.2024: проверено -- да, работает, не зависает. Диаграмма "событий" отличается -- стала короче -- надо проанализировать и объяснить.

    08.02.2024: разобрался в причине укорочения диаграммы "событий": отсутствует строчка "UNEXPECTED reply R COIL", идущая префиксом к тому неожиданному EXCEPTION'у.

    Почему -- хитрО:

    • В той ситуации "Сеньков путается в пакетах" диаграмма обмена -- req(COIL:3/1) rpy(HOLD:?/3) EXC(COIL:read).
    • И вот РАНЬШЕ после первого же ответа (HOLD:?/3) делалось forget_last_sent(), так что EXCEPTION считался за UNEXPECTED.
    • А теперь забывание выполняется только при соответствии пакета ожиданию, так что после ответа (HOLD:?/3) оно НЕ делается, ...
    • ...так что EXCEPTION(COIL:read) считается ожидаемым.

    08.02.2024: пара мелочей касательно "красоты" диагностики:

    1. Вчера проверил, что парсинг-пропускание @TIMESTAMP-префикса и в modbus_mon, и в cdaclient.c выполняется по-простому -- пропускается всё до первого пробела, а потом пробелы. Так что добавление в выдачу миллисекунд ничего не испортит.

      А миллисекунды тут ОЧЕНЬ нужны -- сильно помогает при разбирательствах.

      Так что теперь console_cda_util.c::PrintDataData() использует для выдачи strcurtime_msc().

    2. Сделано 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?

    • Ключевой аспект тут в том, что пока соединения не произойдёт, то команды не начнут исполняться и даже если в какой-то из них таймаут указан, он не отработается; а будет висеть весь срок соединения (180 секунд или 90?).
    • Единственный осмысленный вариант сделать так, чтобы монитор не зависал сильно надолго -- указывать ключ "-T 10": если за 10 секунд не приконнектилось, то и абзац.
    • И тогда нужно в функции-"завершателе" finish_proc() выдавать какую-то диагностику.

      ...например, "# RUN_TIME_EXPIRED".

    • А указывать на необходимость этого -- ровно так же, как в cdaclient'е: ключом "-D/".

    @21:00: ну делаем.

    • Флаг print_timelim_exp, взводится ключом "-D/".
    • Ну а в finish_proc() при нём взведённом печать.

    Да, работает.

    xx.0x.2024:

5:
4:
5:
3:
4:
5:
runner/:
cx-starter:
  • 13.02.2015: соображение общего плана, давно крутящееся в голове: новая версия cx-starter'а должна быть не только "умной, растартовабельной" (уметь пересчитывать конфигурацию), но еще и БИБЛИОТЕЧНОЙ: основной функционал -- чтение конфигов, запуск/остановка серверов -- надо реализовывать в виде библиотеки, чтоб иметь возможность сделать консольные утилиты вроде "старт сервера".

    04.08.2015: и есть идея делать всё не в programs/runner/, а -- для гибкости -- библиотеку в своём отдельном месте в lib/, а Motif'овский cx-starter.c вести в xmclients/.

    21.08.2015@пляж: еще некоторые соображения по cx-starter'у.

    • Держать-то все компоненты, наверное, стоит всё-таки в одной директории programs/runner/.
    • А вот компонентами этими будут:
      • cx-starter.c -- собственно GUI.
      • run-cxsd.c -- консольная утилита для запуска сервера (как локального, так и удалённого).
      • cx-starter_cfg.c -- все мозги по чтению конфигов и запуску/останову серверов.
      • cx-starter_x11.c -- та часть cx-starter'а, что умеет искать окна на экране, показывать на них стрелочкой и убивать их.

        Это позволит при надобности создавать кроме cx-starter'а другие GUI.

      • Какая-то комбинация из "start-all-servers" (чтоб вызывала run-cxsd для всех ЛОКАЛЬНЫХ серверов из списка) и "etc_init.d_cx-servers".
    • Некоторые общие соображения по внутренней реализации:
      • "Считанный конфиг" стоит сделать "объектом". Чтобы упростить пересчитывание конфигов: по кнопке [Reload] можно будет произвести чтение в "новый" объект, не трогая "текущий", и при успехе (точнее, не-пустоте результата) заменить содержимое окна.
      • Списки сущностей внутри этого "объекта" (это а) описания клиентов и б) srvparams) стоит делать SLOTARRAY'ями.
      • Поскольку в командной строке указывается по несколько конфигов, то должны быть отдельные операции "создать объект конфига" и "дополнить объект конфига информацией из такого-то файла".
    • Хоть работа выглядит в основном немудрёной (чё там -- интеллектуально копировать куски из v2), но уже сейчас просматриваются некоторые сложности:
      1. Идентификация окон программ: это проблема не столько cx-starter'а, сколько pult'а и ему подобных -- тот самый вопрос "курицы и яйца", на тему "где взять app_name+app_class".

        Проект решения в принципе есть, за вчера-сегодня.

      2. "Server LEDs" -- "лампочки" серверов,
        • должные быть отображаемыми cx-starter'ом (и pult-клиентами, кстати, тоже),
        • по которым жмётся правая кнопка мыши,
        • и по списку которых cx-starter должен пройтись, чтобы запустить сервера перед запуском клиента.

        Засада в том, что если в CXv2 список серверов конкретного клиента был заранее известен и фиксирован, то в v4 всё сложнее. В нынешнем варианте без broadcast-резолвинга будет пока так же, но потом-то в самом начале список будет гарантированно пустым (при старте никаких ответов еще не будет получено), а потом "подрастёт".

        В голову лезут разные идеи, среди них:

        • В конфиге cx-starter-SOMETHING.conf для клиентов указывать прямо список серверов (схоже с chaninfo в v2). Вопрос только, как через cda проводить "просто сделай соединение с сервером, без каналов".

          Выглядит кривовато.

        • Отображать LEDs не сразу, а через некоторую паузу -- в районе секунды.

          Не нравится неопределённостью: а почему секунда, а вдруг достаточно меньше или понадобится больше? Кроме того -- а если по ходу дела каналы переедут между серверами, тогда что?

        24.11.2015: идеологическая проблема была решена проще: введено событие CDA_CTX_R_NEWSRV, генерящееся при создании нового sid'а в cda_dat_p_get_server().

        Отдельный вопрос по теме "лампочек" -- нижний уровень реализации, в cda:

        • Как считать состояние FROZEN?

          В v2 был обязательный приход данных с некоей периодичностью. В v4 -- совсем НЕ обязательно; хотя есть событие "CYCLE" -- может, по нему?

          А для других типов протоколов как?

        • А ALMOSTREADY -- имеет ли оно вообще смысл? Пока выглядит что ответ "нет".
        • Как отдавать наверх ТИП протокола? Чтоб starter мог различать своё ("cx::") от другого ("epics::"), и либо исполнять другие команды запуска/останова, либо вообще ничего с "чужими" не делать.

          ...видимо, кроме srvname возвращать также и "typename".

          01.12.2015: да, введена cda_status_srv_scheme().

    • Кроме того, помним, что изначальная архитектура системы запуска и серверов вообще в v2 создавалась как некий хак:
      • Сервера по ПЕРВОНАЧАЛЬНОЙ идее должны были работать под другим uid'ом (а то и вовсе под root). Чтобы юзеры (КЛИЕНТЫ!) не имели возможности пакостить серверам, которые по определению более привилегированные.
      • Может, в дополнение к юзеру oper завести также cxsd, под которым бы работали сервера? А запускаются пусть обычно из стартовых скриптов системы.

      (Воспоминания появились @обед-выехав-из-ИЯФа вниз на Лаврентьева (ЧеблоПаша: как у вас кто-то смог сделать "kill -9" серверам?! (после попытки понять, куда исчезли ВСЕ запущенные cxsd на ring1 (не Роговский ли? Нет, отрицает. Хамло?))) и @обед-дорожка-от-Амиго-к-машине (ЧеблоПаша: а в чём проблема, чтоб сервера запускались из-под root? Как из-под оператора запускаются?! Чё это вдруг?!))

    02.12.2015: за последние пару недель более-менее сделано. Хаковастенько, с признаками быдлокодинга, но зато работает.

    • Общая архитектура -- "объектная":
      1. конфигурация считывается в "объект" (причём метод -- "Merge", чтоб добавлять данные из нескольких файлов), ...
      2. ...и потом на основе объекта "конфигурация" (это если он считался корректно) может быть создан объект "список подсистем", ...
      3. ...которому, в свою очередь, потом делается "realize", ...
      4. ...и уже на основе realized subsys-list создаются экранные кнопки.
    • Разбивка по файлам -- похожая на ту, что планировалась:
      1. Сервисные (формально -- не завязаны на cx-starter вовсе):
        • cx-starter_msg.c -- сервис "reportinfo()".
        • cx-starter_con.c -- сервис консольного уровня, RunCommand() & Co.
        • cx-starter_x11.c -- сервис SearchForWindow().
      2. Основной функционал:
        • cx-starter_cfg.c -- читалка конфигурации.
        • cx-starter_Cdr.c -- менеджмент подсистем (первоначально назвыался cx-starter_cda.c, но делает больше, чем общение с cda).
        • cx-starter_gui.c -- должен бы делать всё высокоуровневое с GUI, включая создание кнопок и всю их обвязку, но пока содержит только PointToTargetWindow().
        • cx-starter.c -- всё оставшееся. В нём же пока живут ChangeClientStatus() и ChangeServerStatus().

          Халтура, да -- потом надо будет по мере надобности (изготовления сопутствующих утилит и/или плагинов) украсивить.

    • Парсинг сделан "вручную" -- как в v2, на основе fgets(), а не через ppf4td.
    • ChangeServerStatus() сервера умеет запускать/убивать как v4'шные, так и v2'шные -- различает их при помощи cda_status_srv_scheme()
    • Пере-считывания конфигурации по команде юзера пока нет. Поскольку не реализованы потроха cda_del_context() (в первую очередь удаление серверов), то оное просто не будет функционировать корректно.

    Что НЕ сделано, и надо будет произвести для "полировки":

    1. Для всех ругательств использовать reportinfo().
    2. Передавать argv0 везде, где он нужен -- а то сейчас на это забито, используется NULL; выезжает только за счёт того, что в CdrLoadSubsystemFileViaPpf4td используется Linux-specific program_invocation_name -- под Форточками это пахать не будет.
    3. Тип "cfgbase_t" надо б переименовать во что-нибудь более приличное.

    02.12.2015: еще некоторые замечания, возникшие в голове по результатам изготовления:

    1. В будущем у серверов теоретически возможен также статус FROZEN. И надо б будет уметь как-то выводить их из этого состояния. Видимо, вместо параметра "on" использовать что-то типа "desired_state"; плюс, переход "frozen->normal" должен проводиться как "restart".
    2. Делать "рестарт" сервера -- совсем несложно: надо просто в одну строку -- для RunCommand() -- впихнуть последовательность "stop; sleep 1; start;".
  • 05.05.2016: идейка: раз cx-starter уже умеет запускать сервера разных типов (v2 и v4), то можно его научить запускать и, например, VCAS-сервер -- тип он от cda добудет, а потом просто иные команды запуска.

    Потребность возникла на пульте уметь запускать и РЕстартовать беркаевский VCAS.

    06.05.2016: вопрос будет стандартный -- как указывать ссылку на "канал". По-простому можно тупо какой-то реальный канал указывать -- ПОКА.

    Но вообще надо бы в cda сделать API "добавь такой-то сервер".

  • 06.03.2017: Федя возмущается (письмо от 15:08:52), что cx-starter жрёт много процессорного времени, и, типа, зря и низачем.

    И, мол, состояние должно считаться server-side.

    06.03.2017: можно ввести per-button-параметр "не выполнять вычисления", чтобы деятельность cx-starter'а ограничивалась слежением за серверами.

    07.03.2017: а можно даже и глобалистичнее -- иерархию параметров:

    • Глобальный ключ командной строки "-noprocess".
    • Config-параметры .noprocess и .doprocess.
    • Per-button-параметры noprocess и doprocess.

    Иерархия приоритетов такая:

    • Ключ командной строки отключает всё железно.
    • Config-параметры уставляют умолчание (видимо, для ПОСЛЕДУЮЩИХ кнопок!).
    • Per-button-параметры позволяют явно указать, что выполнять процессинг (+1), не выполнять его (-1), либо как по умолчанию (0).

    Тут главный вопрос в другом: процессинг-то выполняется не стартером, а в Cdr'ном ProcessContextEvent() -- вот как бы уметь его отключать?

    09.03.2017: заглядывание в ProcessContextEvent() показало, что процессинг НЕ выполняется при взведённом subsys->is_freezed.

    1. Вывод -- взводить is_freezed.
    2. Ну и в cx-starter.c не забывать игнорировать CDA_CTX_R_CYCLE.
    3. Единственное, что неприятно: такое использование is_freezed не аукнется ли как-нибудь?

    10.03.2017: делаем.

    • Добавлена option_noprocess и обработка -noprocess.
    • cx-starter_cfg.h:
      • К cfgbase_t добавлены поля
        1. option_noprocess, ...

          ...заполняемое из CfgBaseCreate(), которому передаётся параметром.

        2. cfg_noprocess, ...

          ...изменяемое в CfgBaseMerge() директивами .noprocess и .doprocess.

      • К cfgline_t -- поле noprocess, ...

        ...процессящееся в CfgBaseMerge() по ранее обсуждённым приоритетам:

        1. не указано -- ставится в текущее значение cfg_noprocess;
        2. при взведённости option_noprocess форсится в 1.

          По идее, надо б было делать просто "cp->noprocess||=cfg->option_noprocess", но оказалось, что в C оператора ||= не существует (как и прочих "boolean {AND,OR,XOR,...} assignment").

    • Использование -- всё в cx-starter_Cdr.c:
      1. SubsysAdder() делает sr->ds->is_freezed = lp->noprocess.
      2. SubsysRealizer() не запрашивает CDA_CTX_EVMASK_CYCLE при noprocess.
    • Заодно была исправлена давняя проблема, что cx-starter'ные группировки НЕ БЫЛИ readonly (в отличие от v2).

      (Да, прямо тест был сделан, с группировкой, где одна из ручек постоянно писала chan=10-chan, и канал щелкался.)

      Исправлено: в SubsysAdder() добавлено CdrSetSubsystemRO(,1).

    А теперь что получилось:

    • Обновления отключаются, да.
    • А вот процессора сильно менше жрать не стало. Буквально -1% из 7-8%.

      Надо профилировать и разбираться?

    • Была гипотеза, что проблема в обновлении server-LED'ов в KeepaliveProc().
      • А вот нифига -- даже после её закомментировывания осталось всё так же.
      • ...но LED'ы обновляются и без неё! Ибо есть реакция на CDA_CTX_R_SRVSTAT (но на CDA_CTX_R_NEWSRV -- нету).

    12.03.2017: отпрофилировал, отдельно в режимах "noprocess" и "doprocess". Прекрасный результат:

    1. Собственно CdrProcessKnobs() занимает не шибко много времени.
    2. А основное -- ForeachRefSlot() и on_cycle_otherop_dropper()!

    Т.е., преизряднейшее время занимает то, что даже и не используется (сброс OTHEROP), а в конкретно cx-starter'е оно и в принципе никогда нужно не будет!

    12.03.2017@дорога-домой_(это вечер воскресенья): напрашиваются мысли:

    1. Насчёт конкретно OTHEROP:
      • Прямо сейчас -- можно ту заготовку для OTHEROP просто отключить.
      • На будущее: ввести бы какой-нибудь флажок типа "CDA_CONTEXT_OPT_NO_OTHEROP", чтобы эта фича игнорировалась.

        13.03.2017: и начать, наконец, использовать параметр options в cda_new_context().

      • И, кстати: ведь аналогично полностью-нафиг-ненужно оно и в vdev-"клиентах", там бы тоже оную фичу отключать. А то там в магнитных системах счёт vdev-драйверов может идти на сотни, контекстов/серверов тоже, а cda-каналов и на тысячи и десятки тысяч.
      • А как вообще будет CXCF_FLAG_OTHEROP вести себя в условиях v4, где флаги свободно распространяются с серверного уровня на клиентский (а не срезаются все, кроме содержащихся в CXRF_SERVER_MASK, как делалось в v2)?

        13.03.2017: всё будет нормально: при сравнении "не взвести ли OTHEROP" -- в cda_dat_p_update_dataset(), сейчас отключеннос -- стоит альтернатива: если CXCF_FLAG_OTHEROP не взводится, то он обязательно сбрасывается.

        14.03.2017: точнее, он там сбрасывался, если НЕ обнаружено отклонение от уставленного. А сейчас сброс вытащен в безусловное действие ПЕРЕД всеми условиями.

    2. Общее, насчёт схемы процессинга ForeachRefSlot()'ом:
      1. Перебор всегда-всех-ref'ов при событии для ОДНОГО сервера выглядит непотребством. В обычных-то GUI-клиентах серверов в основном по штучке-две, но в cx-starter'е и (главное!) в серверах (vdev) -- десятки и даже сотни.

        Не сделать ли:

        1. Объединение ref'ов каждого сервера в список:
          • в refinfo_t поля для объединения всех dataref'ов сервера в список (это всего-то плюс пара int'ов, +8 или +16 байт/ref),
          • соответствующие поля first/last в srvinfo_t...
          • ...и соответствующий несложный процессинг при прописывании ri->sid (удаление из списка "старого" сервера и добавление в "новый").
        2. Отдельный итератор "ForeachSrvsRefSlot()".
      2. А можно ли как-то оптимизировать сами итераторы "Foreach" -- чтобы были варианты-макросы, подставляемые прямо в точку вызова, где бы проверка "condition" вставлялась бы сразу inline, и действие "action" тоже?

        К сожалению, есть 2 проблемы:

        1. Параметры макросов НЕ могут содержать запятых. Так что проверки либо должны быть крайне простыми (без запятых), либо являться вызовами static-inline-функций, которые б компилятор подставлял в точку вызова.
        2. @вечер, гугление: НЕЛЬЗЯ сделать #define изнутри #define.
          • Поэтому прямо внутри GENERIC_SLOTARRAY_DEFINE_nnn() изготовить такой макрос нельзя.
          • Придётся использовать какую-то более хитрую схему -- например, иметь ОТДЕЛЬНЫЙ макрос, который бы генерил "тело" такого макроса, чтоб сам #define делался бы уже клиентским кодом.
          • Но такой вариант не только громоздок, а еще и чреват проблемами -- ведь "описание" правил работы SLOTARRAY'я (in_use_field, in_use_val_unused, in_use_val_used, ...) придётся дублировать.

        Так что, увы, ответ, скорее всего -- "НЕ можно".

    13.03.2017: теперь чуть подробнее, с данными профайлинга.

    Профилировалось на p8b2 (E3-1245v2) с 2 штуками конфигов от linac3 (файл указывался 2 раза), в течение минуты (программа грохалась вручную, т.к. kill не катит (gmon.out не сохраняется), а xkill неспособен найти окно иначе как по числовому id).

    1. В режиме "doprocess" (~16.4-17.1% CPU):
        %   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
      
      (далее обходимся без заголовков и менее времязатратные строчки не приводим)
    2. В режиме "noprocess" (~13.5-13.8% CPU):
       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
      
    3. Без foreach по 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
      
    4. Без foreach по 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'оклиентах:

    • linmag даёт загрузку ~4.3-4.6% CPU; ringmag -- ~2.9-3.6% CPU, ...
    • и это практически не зависит от включенности/отключенности foreach по on_cycle_otherop_dropper();
    • сам оный светится далеко не в начале списка: на 11-м месте в linmag и на 21-м в ringmag, с пренебрежимо малым потреблением.
    • Основной же вклад -- CdrProcessKnobs() (как и предполагалось) и choose_knob_rflags() (вот это неожиданно).

    Итого, как стоит поступить:

    1. Заюзать-таки options, дабы указывать на необходимость "не тратить время на OTHEROP".
    2. В GUI-клиентах же игнорировать overhead от пока-толком-неработающей функциональности, т.к. он пренебрежимо мал.

    14.03.2017: делаем.

    • Вводим новый enum -- CDA_CONTEXT_OPT_xxx, содержащий сейчас CDA_CONTEXT_OPT_NO_OTHEROP и CDA_CONTEXT_OPT_IGN_UPDATE (о ней позже).

      Биты идут с 0 (т.к. он вроде ни с кем не пересекается).

    • Добавлено поле ctxinfo_t.options, в котором сохраняется переданное cda_new_context()'у.
    • Собственно реакция на эти флаги:
      1. Foreach с on_cycle_otherop_dropper() делается только при НЕвыставленном флаге CDA_CONTEXT_OPT_NO_OTHEROP.
      2. Также этот флаг является дополнительным условием (ко всяким max_nelems==1) в cda_dat_p_update_dataset()'ном блоке определения OTHEROP. Т.е., если эта опция взведена, то никакие проверки не делаются, а флаг OTHEROP просто сбрасывается.
      3. Внутренности cda_dat_p_update_dataset() при взведённом CDA_CONTEXT_OPT_IGN_UPDATE не выполняются (просто делается return).
    • Использование флагов в клиентах cda:
      1. В CdrRealizeSubsystem() добавлен параметр cda_ctx_options. Он просто передаётся в cda_new_context() напрямую в качестве options.

        С этим параметром есть некоторые сложности: формально Cdr-клиенты НЕ обязаны знать про cda, и у них просто не будет CDA_-констант в пространстве имён. Посему в этот параметр передаётся следующее:

        • Все pult-подобные (pult, stand, localtest, dircntest), а также histplot -- просто 0.
        • cx-starter_Cdr.c (этот-то cda-aware):
          1. В любом случае -- CDA_CONTEXT_OPT_NO_OTHEROP, ...
          2. ...и при включенном noprocess -- еще дополнительно CDA_CONTEXT_OPT_IGN_UPDATE (ему ж значения всё равно незачем).
      2. vdev_init() всегда указывает CDA_CONTEXT_OPT_NO_OTHEROP.

    Результат:

    • С тем же конфигом cx-starter берёт ~4.3-5.3% CPU (в основном стоит на 4.6%), а профиль выглядит так:
       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
      
    • За 10 минут (т.е., в более стабильном режиме, когда влияние стартовых расходов (парсинг файлов, регистрация каналов) снижается) --
       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 вопроса:

    1. Как бы даже в режиме "doprocess" отключать подписку на векторные каналы (всё равно от неё толку ноль)?

      ...если, конечно, оная подписка делается. Надо проверить, но, весьма вероятно, что она заказывается -- это ведь Cdr делает?

      Проверил: неа, тут беспокоиться не о чем. ВЕКТОРНЫЕ каналы находятся исключительно в ведении своих knobplugin'ов. Cdr же создаёт лишь канал _devstate_ref, смотрящий на "src._devstate". Т.е., тут траффик практически нулевой.

    2. И как всё же в режиме "noprocess" отключать подписку на всё, но чтобы при этом всё же создавались бы все серверные соединения?

      Неясно нифига -- даже не вполне понятно, ГДЕ можно добиться желаемого эффекта.

      Так, навскидку -- видимо, где-то в cda, чтобы реальной регистрации КАНАЛОВ не происходило бы; а указание на это надо б давать через Cdr, путём передачи чего-нибудь в options. ...или прямо в Cdr и не делать регистрации каналов, а делать вместо этого только "cda_add_server_conn()"?

    Чуть позже: а вот если бы таким каналам -- которым передан флаг "не надо мониторировать" -- именно специальный тип мониторинга ставить, "CX_MON_COND_NEVER"! Но это б сгодилось только для CXv4, а для других протоколов -- никак. Да и неправильно это, задачу КЛИЕНТА вешать на сервер.

    06.06.2020@пробежка-по-квартире: последовательные соображения:

    • (вчера) А если при наличии флажка "NOPROCESS/NOMONITOR" именно сам cda_core, в лице cda_add_chan(), будет вместо вызова реального создания канала лишь делать cda_add_server_conn()?

      Ну да, некоторый хак будет, особенно в части определения имени сервера для добавления. Но если получится -- то и фиг с ним.

      Ну и пусть возвращает какой-нибудь фейковый ref (а лучше -- CDA_DATAREF_NONE), в данном случае это некритично.

    • Неа, не получится: у нас выделением имени СЕРВЕРА занимаются именно dat-плагины. Так что если для cx:: общий код в cda_core ещё сможет выкрутиться, то для epics:: -- уж точно нет.
    • И другая проблема: для без-серверных ссылок на каналы (которые с UDP-резолвингом) и сервера проявляться не будут: раз не добавляются реальные каналы, то они и не резолвятся, и информации о серверах взяться неоткуда.

      А это сразу обессмысливает половину идеи CX-starter'а (программы-то он запускать сможет, а сервера -- уже нет).

    • А если "нанести удар в самую нужную точку": пусть всё делается как обычно, но только конкретно ПОДПИСКУ на каналы dat-плагин пусть не делает.

      Т.е., только 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().
    • 09.06.2020@балкон, "прогулка" после обеда: кстати, в принципе через этот chan_ioctl() можно б было и одиночные чтения запрашивать -- в таком "немониторирующем" режиме, a-la салимовцы, оно бы и имело смысл. И эти одиночные чтения транслировались бы в CXC_CH_RQRD -- да-да, вот тут-то он задействуется.

      07.07.2023: забавно -- только наткнулся на эту идею, при том, что идентичная же записана за 11-06-2023.

    09.06.2020: ну-с, что будем делать?

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

      Особенно на фоне идеи про динамическое управление мониторированием.

    Решение на сейчас:

    1. Вот прямо сейчас -- НЕ делаем, а откладываем "на подумать".
    2. Но конкретно в cda_d_cx.c можно:
      1. Ввести дополнительный флажок "не мониторировать" (или на пару с нынешним rq_lock сделать битовое поле "текущее поведение"?).
      2. Набор операций, исполняемый после коннекта и включающий сейчас cx_rd_cur(),cx_setmon(),cx_rq_l_o() -- вытащить в отдельную функцию, а то оно уже 3 раза повторяется.

      (Эти действия понадобятся в любом случае.)

    13.06.2020: кстати, а ведь флажок "NOMONITOR" будет полезен и для софтины tvcapture и прочих подобных бриджевателей -- там совершенно незачем генерить лишний траффик, мониторя свои же записывания.

    16.06.2020: пора переходить к "предварительно-подготовительным" мероприятиям.

    • Была промежуточная мысль в сторону: ведь удобно бы кроме DATAREF_OPT_-флага иметь также и CDA_CONTEXT_OPT_-флаг, чтобы режим включался сразу для ВСЕХ каналов.

      Так вот: у нас УЖЕ есть CDA_CONTEXT_OPT_IGN_UPDATE -- он по смыслу своему прекрасно подходит на эту роль (всё равно события обновления игнорируются, так незачем и подписываться!).

      Да и cx-starter этот флаг УЖЕ выставляет в режиме noprocess, так что его исходники даже и модифицировать не придётся.

    • Но вот для pipe2cda его применять нельзя: там события DATAREF_R_UPDATE используются, так что нужно будет выставлять "NOMONITOR" индивидуально (тут расчёт на то, что даже при НЕмониторировании начальное "текущее значение" всё же придёт).
    • Заведён флаг CDA_DATAREF_OPT_NOMONITOR (да, =1<<20 -- хрен уж с ним).
    • В cda_add_chan():
      1. Этот флаг включен в маску "передаваемых dat-плагину" (вместе с ON_UPDATE и EXCLUSIVE).
      2. Он взводится при наличии у контекста флажка CDA_CONTEXT_OPT_IGN_UPDATE.

        Но только для предачи new_chan()'у, в ri->options он НЕ взводится.

    • Основное -- в cda_d_cx.c:
      • Троица 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).

      • Его использование:
        1. В _new_chan() он выставляется в значение флага CDA_DATAREF_OPT_NOMONITOR.
        2. В do_subscribe() при его взведённости НЕ делается cx_setmon().
    • pipe2cda.c:
      1. При регистрации канала добавлен CDA_DATAREF_OPT_NOMONITOR.
      2. Ловим теперь не только событие DATAREF_R_UPDATE, но и DATAREF_R_CURVAL: для наших целей -- как уведомление об установлении связи -- эти события равнозначны, а при NOMONITOR будут все шансы получения именно только CURVAL.

        ...правда, для каналов ЗАПИСИ, только которые и актуальны для 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 -- приходят и при записи.

  • 30.10.2017: хорошо бы уметь делать МЕТКИ. Чтоб лучше разделять группы программ, а не просто сепараторами, как сейчас.

    Напрашивающийся вариант синтаксиса -- если после '-' стоит еще что-то, то вместо сепаратора делаем метку.

  • 30.10.2017: сейчас, по прошествии 13 лет с появления CX-starter'а, возникает потребность, предвиденная еще тогда, в 2004-м: уметь запускать/останавливать "чужие" сервера. В т.ч., совсем необычные.

    Конкретно: Федя Еманов (и Виталя Балакин по его примеру) навострились делать "демоны" на python, которые как-то обрабатывают данные от обычных CX-серверов и кладут результаты в CX-серверы же. Сами они никаких сокетов не слушают, но от этих "демонов" зависят другие программы, уже вполне графические.

    • С одной стороны, правильным было бы оформлять такие "демоны" серверными драйверами -- тем более, что вроде должно быть можно сварганить "драйвер на Python" (по фединым прикидкам).
    • Но уж что есть, то есть. И желательно бы...
      • мочь делать кнопки "Старт" и "Стоп", там же, где у обычных клиентов лампочки серверов.
      • БЕЗ "слежения", а просто кнопки, нажатие которых вызывает указанное действие.

      ...а то сейчас пришлось делать 2 ОТДЕЛЬНЫХ строчки для запуска и останова балакинского "демона".

  • 17.12.2017: работа с server-led'ами минимально адаптирована к возможности zeroconf-резолвинга.

    19.12.2017: но остаётся ещё проблема, что лампочки серверов, появившиеся ПОСЛЕ начального заполнения окна, не получают привязки меню по правой кнопке мыши.

    • Как привешивать меню на новопоявляющиеся кнопки -- понятно (придумано еще позавчера в секции о MotifKnobs_cda_leds): ловить событие NEWSRV и самостоятельно явным образом вызывать MotifKnobs_leds_grow() -- так избежим проблемы, что cx-starter'овский обработчик вызовется ДО led'овского.
    • А вот как узнавать, на кого еще надо навесить мышь, чтобы не навешивать повторно?

      Очевидно, помнить старое значение cda_status_srvs_count() и сравнивать его с текущим?

      Неа, есть вариант лучше: помнить старое, но сравнивать с текущим (после leds'ного вызова) значением leds.count.

    Делаем.

    1. Добавлено поле subsys_t.oldleds_count.
    2. Первоначальное его заполнение -- из sr->leds.count -- в BtnCreator().
    3. В обработчике NEWSRV сравнение со старым и вызов () для новорожденных.

    Да -- работает! И меню появляется, и по командам даже пытается сделать ssh на IP сервера (от ssh-то в нынешней ситуации толку не будет, но то вопрос следующего раздела).

  • 17.12.2017: в связи с внедрением zeroconf-резолвинга возникает методологическая проблема: ведь стартер при старте серверов указывает им конфиг в виде devlist-HOSTNAME-N.lst, а тут вместо HOSTNAME будет IP-адрес.

    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: насчёт где/когда выполнять резолвинг:

    • Ну ясно, что НЕ принудительно в cxlib'е, HandleSrchReply().
    • В cda_d_cx.c::ProcessSrchEvent()?

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

    • Самое наиочевидное -- прямо в ChangeServerStatus().

      В момент определения имени хоста сервера -- в точке перед "at_host = cf_host = host;".

    • Но неприятно, что резолвинг при этом будет выполняться КАЖДЫЙ РАЗ при запуске/останове сервера.

      Кэшировать бы как-то, но как/куда? Ведь имя добывается прямо в той точке, посредством cda_status_srv_name()...

    • Кстати, утилитка cx-chan-search.c пользуется как раз gethostbyaddr(), и тут за 11-01-2018 объясняется причина.

    Ну -- делаем:

    • Вставлено в точку между добычей имени хоста сервера через SplitSrvspec() и tolower()'изацией.
    • Делается только если не "" и не "localhost".
    • Также проверяется, что host[] содержит только цифры и '.'.
    • И вот в этом случае делается ровно то же, что в cx-chan-search.c::evproc().

    05.12.2022: попробовал проверить, на x10sae -- сходу не получилось: мало того, что путается в IP-адресах (шибко много интерфейсов поднято, и на всех получаем ответы), так ещё если всё же указать один принудительно --

    CX_GURU_LIST=x10sae
    -- то у cx-starter'а хватает мозгов считать его за IsThisHost() и делает не совсем точно то же самое.

    Надо б с другого узла проверить.

    08.12.2022: попробовал ещё раз прямо на x10sae, только поаккуратнее.

    • Вскрылся небольшой косячок: оно пытается использовать ПОЛНОЕ имя, "x10sae.inp.nsk.su".

      Ну OK -- добавил тривиальное обрезание всего начиная с первой '.', если есть.

      После исправления всё делает как надо -- резолвит адрес в имя и правильно указывает на его основе имя devlist'а.

    • Также проверено с другим узлом -- сервер так же на x10sae, а cx-starter на b360mc. Да, тоже всё работает как надо: и имя devlist'а генерится правильно, и ssh делает по короткому имени (хотя вот ЭТО -- уже вопрос, корректно ли).
    • В процессе, кстати, заметил, что cx-chan-search резолвит ВСЕ адреса ВСЕХ локальных интерфейсов в "x10sae.inp.nsk.su", хотя ничего такого ни в каком /etc/hosts нету.

      Думал было, что косяк в реализации в егойном evproc() (например, ПЕРВОЕ имя получает реальное, а остальные -- просто в буфере остаётся).

      Но нет: "traceroute" на эти адреса также показывает "имя локального узла".

      Причины такого поведения мне найти не удалось: в man'е на эту тему ни слова, а гугление по "unnamed ip address traceroute local host name" и "unnamed ip address gethostbyaddr local host name" ничего не дало, и как ещё сформулировать запрос -- хбз.

    А вот тут нужно небольшое "обсуждение":

    1. К вопросу о "cx-starter просто не подходит для работы с zeroconf-клиентами" за 18-12-2017: там ведь есть проблема "курицы и яйца" -- если сервер, на который ссылки каналов без-серверные, НЕ запущен, то "с нуля" его cx-starter запустить и не сможет, поскольку ему просто неоткуда будет взять информацию о том, КАКОЙ сервер является владельцем каналов.

      С другой стороны, как сейчас стало ясно при тестировании, если такой сервер хоть раз появлялся, то далее он уже будет известен.

      Следовательно, cx-starter вполне годится для ПЕРЕзапуска таких серверов.

    2. Также возник -- сегодня -- вопрос о том, насколько корректно укорачивать hostname сервера, отрезая от него домен.
      • Для devlist'а -- да, домен отрезать обязательно (чего, кстати, сейчас НЕ делается).
      • А вот для того, куда делать ssh -- хбз: тут можно определить лишь сравнением доменов target-узла и локального.
      • Но у нас-то сейчас домен отрезается ПРИНУДИТЕЛЬНО.
      • Однако тут ситуацию упрощает тот факт, что оное отрезание выполняется ТОЛЬКО для hostname'ов, полученных резолвингом IP-адресов.

        А IP-адреса будут почти исключительно в результате UDP-резолвинга, который, в свою очередь, в основном годен только для работы в ЛОКАЛЬНОЙ сети.

      • Возможные исключения -- это
        1. Если УДАЛЁННЫЙ сервер будет напрямую указан IP-адресом -- тогда сделается резолвинг и домен будет ЗРЯ отрезан.
        2. Если всё-таки как-то проброшен тоннель/VPN/... в удалённую сеть, так что broadcast'ы туда будут маршрутизироваться (как и ответы на них передаваться обратно) -- тогда ситуация ровно та же, только IP-адрес возьмётся не из явного указания, а из ответа.

        Но оба этих случая, мягко говоря, весьма экзотичны.

        И да, CX-starter создавался всё-таки для работы в ЛОКАЛЬНОЙ пультовой.

    Итого, что осталось как бы "НЕдоделанного":

    • НЕотрезание домена для cf_host (вот его можно было б делать ВСЕГДА).
    • Принудительное всегда-отрезание домена для серверов, чьё имя получено резолвингом IP-адреса; а надо бы принимать решение об отрезании УСЛОВНО, по результату сравнения доменов target-узла и локального.
  • 05.06.2020: текст help'а переведён на английский, чтобы не иметь проблем с Raspberry, Xming и прочими местами, где нет русских шрифтов.

    Старый вариант оставлен, но закрыт #if'ом по MAY_USE_RUSSIAN_KOI8.

    Сделано одновременно и унифицированно с Chl_help'ом.

  • 21.09.2020: небольшое добавление в cstart.sh: теперь cx-starter'у передаётся также $* (в самом конце).

    Смысл -- чтобы можно было прямо команде "start" передать ключик "-noprocess.

  • 04.11.2020: ещё одно усовершенствование: "командный файл настроек per-host" -- cx-starter-settings-HOSTNAME.sh.

    04.11.2020: при наличии этот файл СОРСИТСЯ (командой "."), а не вызывается. Причём ищется он в 4pult/configs/, хоть это и shell-файл -- чтоб был рядом с прочими конфигами.

    Занадобилось для того, чтобы на Raspberry Pi по имени term-kls сделать более крупные цифры. Так что создан конкретно cx-starter-setup-term-kls.sh (он делает "xset fp+ ДИРЕКТОРИЯ" плюс парочку "xrdb -merge").

  • 01.12.2022: возникает желание сделать так, чтобы команда "Stop" серверу отрабатывалась бы ВСЕГДА.

    Хотелка возникла после вчерашней ситуации, когда сервер cxhw:37 завис намертво, так что к нему не коннектилось и starter считал его "неживым", но при попытке сделать ему "Start" новый экземпляр не запускался с предсказуемой диагностикой

    cxsd_fe_cx.c::CreateServSockets: bind(inet_serv_socket): Address already in use

    А если бы можно было "принудительно грохать" -- то рестартовать бы удалось.

    ...вопрос только: всегда грохать принудительно или требовать удержания, например, кнопки Shift?

    01.12.2022: немного исследований вопроса:

    • "Требовать ли удержания Shift" -- нет, не стоит: во-первых, лишняя маета в коде (сейчас в ServMenuCB() информация call_data не используется, а пришлось бы), и во-вторых, нажатие Shift+MB1 либо самим Motif'ом, либо какими-нибудь window manager'ами может восприниматься "некорректно".
    • Главный вопрос -- как указывать на такой "принудительный Stop"?

      При взгляде на ChangeServerStatus() приходит одна идейка, хотя и весьма некрасивая:

      1. В качестве "force stop" передавть значение on=-1.

        При этом условие "if (curstatus == on) return 0;" никогда не сработает.

      2. А затем делать что-то вроде "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".

Автоматический запуск при старте сервера -- run-cx-server и start-all-servers:
  • 15.08.2018: давно стоит задача обеспечить автоматический старт серверов при старте машины-сервера -- чтоб всё функционировало и без cx-starter'а, и чтоб в случае всяких просков питания и прочих катаклизмов всё б поднималось автоматически.

    Это всё обдумывалось ещё со времён v2 -- см. bigfile-0001.html от 05-09-2012. И даже было сделано почти всё, в следующем составе:

    • run-cx-server -- запуск одного сервера с указанным номером.
    • start-all-servers -- запуск всех серверов на данном хосте.
    • Список номеров серверов содержится в $PULT/configs/cx-servers-HOSTNAME.conf.
    • ...отдельно нужен скриптец-"служба" для самой ОС, который бы запускал start-all-servers -- /etc/init.d/cx-servers или аналогичный для systemd.

      Для init'а оный был слабан еще тогда -- src/doc/_etc_init.d_cx-servers; он даже давно реально используется, чтобы грохать все cxsd при shutdown/reboot'е (иначе клиенты были не в курсе, т.к. соединения не закрывались).

    15.08.2018: подготовка -- проект:

    • Главная проблема, почему всё никак не двигалось: существует файл srvparams.conf, из которого cx-starter берёт необходимые вещи по ключу params=, вроде -bNNNNNN.
      • В v2'шной версии было схалтурено и захардкожен параметр -b200000.
      • Была -- весной -- мыслишка сваять отдельный бинарник "выцепи из srvparams.conf параметры указанного сервера", с использованием cx-starter_cfg.c.

        Но это уже несколько overkill -- особенно учитывая, что запуск только локальный, и никакие start=/stop=/user= нафиг не интересны.

      • И еще при создании v4'шного cx-starter'а была мысль -- см. за 21-08-2015 -- иметь отдельную утилитку run-cxsd.c, умеющую стартовать сервера хоть локально, хоть удалённо, но тогда полную модульность так и не сделали и до этой утилитки дело не дошло.
      • Решено сделать по-простому: выцеплять нужную информацию прямо из скрипта, с помощью всяких grep/awk/sed и прочего pattern-matching-and-replacement'а средствами shell'а.
    • Второй вопрос -- как парсить файлы cx-servers-HOSTNAME.conf: сейчас они "читаются" просто обратными апострофами, а хоцца мочь там что-то закомментировывать.

      Идеи:

      1. Отсеивать строки, начинающиеся с '#', используя вместо cat команду
        grep -v '^ *#

        Но это не позволит комментировать с середины строки.

        Вторая серия идеи: вырезать всё от '#' и до конца строки, sed'ом --

        sed -e 's/#.*//'
      2. Игнорировать все номера, начинающиеся НЕ с цифры.

        Это позволить исключать сервера из списка, просто добавляя перед номером, например, '-'.

    • С другой стороны, файлы cx-servers-HOSTNAME.conf можно генерить и автоматически -- в sw4cx/configs/Makefile из значений NNN_SERVERS.

      ...хотя у такого способа есть и минусы -- в хитрых случаях, вроде нынешнего объединения узлов canhw и cxhw на одном (cxhw): запускаться при этом будут только сервера по primary-имени. Но это уже мелочи, легко исправимые вручную.

    Реализация:

    • Решено оба скрипта делать "с исходниками" -- файлами с расширением .sh, которые потом копируются в без расширения и делаются "chmod +x". Как давным-давно с cstart, генеримым из cstart.sh.

      Это было самое простое :)

    • start-all-servers.sh: также сравнительно несложен.

      По сравнению с v2'шным вариантом -- добавлено красивостей (пути засунуты в переменные) и вышеописанная фильтрация (комментарии и только-цифры в номерах серверов).

    • run-cx-server.sh же оказался той ещё задачкой: в shell'е ОЧЕНЬ неудобно делать парсинг и фильтрацию.
      • Конструкция вида
        grep '^.srvparams' | while read A B C
        do
        ...
        done
        
        просто не работает. У меня сходу оно вообще на что-то там ругалось, а еще, как выяснилось чтением нагугленного, в таком варианте "while" будет исполняться в sub-shell'е (т.е., все изменения переменных в нём же и останутся), так что пришлось сбагривать файл через
        while read ...
        done < ${SRVPARAMS_FILE}
        

        А отфильтровывать по ".srvparams" уже внутри -- лишним if'ом.

      • А pattern-matching по значению переменной вообще нормально не работает. Поэтому пришлось использовать хак (тоже нагугленный) -- case, у которого в качестве шаблона МОЖНО писать переменную.
      • В результате внутри while стоит аж 3 отдельных вложенных условия -- что .srvparams, что наш сервер соответствует шаблону, и что 3-е поле начинается с "params=".
      • Отдельным if'ом проверяется, не заключено ли полученное значение в кавычки (например, params="-e 'load-frontend starogate'"). И если "да", то они отрезаются.
    • В sw4cx/configs/Makefile добавлена авто-генерация файлов cx-servers-HOSTNAME.conf.
    • Нарисован прототип определения сервиса для systemd -- src/doc/cx-servers.service.

    09.04.2019: пытаемся двигаться дальше -- припёрло уже.

    • Для начала, в start-all-servers.sh был косяк: оно пыталось в качестве номера экземпляра передавать строку :N -- вот так, с буквой N вместо номера сервера. Потому, что надо было писать :$N.
    • Далее, run-cx-server.sh: там указывалось csxd.conf вместо cxsd.conf -- естественно, cxsd просто отваливался с ошибкой.
    • Собственно "сервис" cx-servers.service:
      • Вариант "forking" точно не катит. Пробуем с "oneshot".
      • При загрузке unit'а systemd ругается, что путь не абсолютен, и потому он игнорирует строку "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: как обойти?

    1. @по-дороге-на-ОК-посмотреть-пашин-компрессор-беркут: создать фейковую службу (Type=simple), которая в реальности ничего не делает, но процесс остаётся висеть; и чтоб при старте вызывался start-all-servers.sh, а после завершения -- kill.
    2. @на-входе-в-пристройку-после-просмотра-пашиного-Беркута: интересно, а можно ли заставить systemd выполнять просто ДЕЙСТВИЯ? Ну как он гибернацию и shutdown запускает -- это ж не служба, а действие.

      И запилить пару таких "действий" -- при старте системы запуск серверов, а при останове -- kill.

    3. @пультовая-четверговое-толковище-по-СУ@15:00-16:00 (выступал Паша, неподготовленный, его козлили 6-я лаборатория): сходу понятия "действие" не найдено.

      Но ещё раз обратил внимание на сервисы с 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.

    Пытаемся сделать. Прямо сплошная "дорога боли"...

    • Выяснилось, что там НЕЛЬЗЯ использовать shell syntax, т.к. синтаксис systemd'шных команд лишь "inspired by shell syntax", но он вовсе не полон, и нельзя использовать почти ничего. В частности, команда
      /usr/bin/kill $(cat /var/tmp/cxsd-*.pid)
      не работает, т.к. не поддерживаются 1) command substitution -- $(cat...); 2) globbing -- cxsd-*.pid. Господи, какие же уроды авторы systemd!!!!!
    • Пришлось добавить скрипт stop-all-servers, содержащий лишь этот kill.
    • Плюс пришлось добавить в start-cx-servers.service параметр KillMode=none, иначе сервера сразу после своего старта прибивались (systemd через cgroups следит).

    Засим -- вроде бы заработало.

    Теперь надо б как-нибудь отвязаться от прописывания путей, чтобы оно ссылалось на home-директорию юзера. $HOME?

    12.04.2019: пытаемся отвязаться от абсолютных путей вроде "/export/ctlhomes/oper/4pult/" или "/home/user/4pult/". По факту -- "дорога боли, день 2".

    • Просто использовать $HOME/4pult/... -- авотхрен!

      Получаем ругательство "Executable path is not absolute, ignoring: ...".

    • Попытка заменить $HOME на %h успехом не увенчалась -- результат тот же.
    • Т.е., эти долбо[CENSORED] авторы systemd просто не догадались поддерживать параметризовабельность путей команд.

      ...это так -- в дополнение к сомнительности идеи требовать именно абсолютного пути с запретом $PATH.

    • Гугление подсказало рецепт (очевидный ведь, блин!) -- использовать синтаксис
      /bin/sh -c КОМАНДА
      (советовали /bin/bash, но не суть).

      Т.е., тут у shell'а путь абсолютный, а дальше выполняется уже стандартный поиск.

      Что по факту просто множит на ноль то требование "везде абсолютные пути!".

    • Окей, делаем, и-и-и... опять фиг! Ругается уже 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@пультовая: тестируем. Аб-зац...

    1. Запуск: обломился, с диагностикой в /var/log/messages:
      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 для своего "активирования" требует нашего сервиса; с другой же, он вроде как должен "полностью активироваться" ДО запуска этого сервиса.

      По-хорошему, должен был бы быть какой-нибудь специальный тэг "запустить в конце, после всего", но такого нету.

      Как бы то ни было -- нынешний вариант работает; хотя и неясно, насколько это корректно и не сломается ли в будущем.

    2. Останов -- всё сложнее и непонятнее.
      • Первый тест -- перезагрузка БЕЗ включения сервиса останова, чтобы проверить, как ведут себя сервера (точнее, сокеты) на виртуалке.

        Всё предсказуемо -- все лампочки остались гореть (сервера убиты не были, вот интерфейс сервера просто и ушёл с "живыми" незакрытыми сокетами).

      • Для следующей перезагрузки было сделано "systemctl enable stop-cx-servers".

        И результат оказался странным: ЧАСТЬ серверов закрылись, а часть -- нет. Т.е., часть лампочек в стартере покраснели, а часть остались зелёными.

      • При следующих же перезагрузках и вовсе ВСЕ оставались зелёными.
      • Сам по себе сервис дело делает -- проверено ручным запуском "systemctl start stop-cx-servers".
      • В чём причина неработы -- неясно, и неясно даже, как бы это понять, т.к. в логах ничего "такого" не видно:
        1. В /var/log/messages просто пусто.
        2. По "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 серверов.

      • Пытаемся думать.
        • Первая идея была -- что, возможно, запускаемый kill отрабатывает как-то асинхронно, чуть позже, и оказывается сам убиваем. Т.е., что надо добавить строчку "KillMode=none".

          Но, судя по логам о получении SIGTERM'а, это не так и сигнал доходит.

        • Тогда вторая гипотеза -- что перезагрузка происходит как-то очень быстро и полная цепочка закрытия сокетов просто не успевает отработаться.

          Если дело в этом, то надо ставить небольшую задержку после убиения.

          Но как-то это сомнительно.

        • Третья гипотеза -- детерменистическая: что ИНТЕРФЕЙС опускается до того, как выполнится убиение (либо параллельно, судя по тому, что в первый раз часть лампочек успела покраснеть).

          Если так, то нужно ставить какую-нибудь хитрую зависимость, чтобы убиение выполнялось в самом начале, либо ПЕРЕД остановом сервиса "network" (?) -- но это вообще неясно, как специфицировать.

        Склоняюсь к последнему варианту. Понять бы, ЧТО там надо указать в зависимостях...

        P.S. Гугление сходу ответа не дало: там лишь обращают внимание, что если указан некий порядок СТАРТА (зависимости After= или Before=), то СТОП будет выполняться в обратном порядке. Но у нас-то РАЗНЫЕ сервисы для старта и стопа, поэтому никаких прямых зависимостей для обращения просто нету.

    Резюме: автоматический старт запинан, а автоматический стоп менее критичен, т.к. через 5 минут cda+cxlib сами обнаружат обрыв.

    15.04.2019@пляж-лыжи: всё-таки НЕОБХОДИМ нормальный останов серверов при перезагрузке. Ибо сегодня проблема проявилась:

    • Управление задающим генератором (устройство slio24_reg) сделано через CANGW (по имени v5-sr-rfr) вместо обычного SocketCAN.
    • И вот после перезагрузки/переезда машины canhw.ic.local из-за того, что у v4c4lcanserver'а остался неприбитый сокет от старой инкарнации сервера, он не позволил новой инкарнации захватить это устройство.
    • ...а из-за косяка со свитчами, когда там почему-то порт начинает пропускать траффик не сразу после активации, проверочная последовательность "пошлём имеющемуся старому владельцу устройства пустой пакет, если узел на том конце перегрузился, то сокет закроется" не срабатывала.

    А если бы stop-cx-servers выполнялся, то сокет закрылся бы сразу и вышеописанных сложностей бы не возникло.

    17.01.2022: а вот ЧеблоПаша нашёл-таки способ сделать ВСЁ в одном файле сервиса. Краткая история:

    • У него на ВЭПП-4 подвисала при перезагрузке виртуалка can2, на которой нынче работает сервер для v4k500.subsys (can2:2). Причём зависала совсем намертво -- вывести ситуацию из ступора могла только перезагрузка всего хоста.
    • В эту виртуалку -- точнее, КОНТЕЙНЕР -- проброшен один CAN-интерфейс -- can1.
    • И выяснилось, что проблема, похоже, в том, что при перезагрузке сервер НЕ убивается, продолжая держать интерфейс, из-за чего виртуалка и не могла завершиться.
    • ...причём если перед загрузкой явно выполнить команду
      systemctl start stop-cx-servers
      то всё работало штатно без проблем, а вот сам SystemD это исполнить был почему-то неспособен.
    • Возникал вопрос -- а какого чёрта оно просто не пришибёт процесс? Ну пусть даже тупым "kill -9"?
    • И при разбирательстве было видно, что "принадлежность"-то по PID'ам правильная: у пары процессов cxsd (:0 и :2, из которых один по ps показывался "Z", а другой "D") PPID'ом значился "systemd-shutdown", но на этом всё и застопоривалось.
    • Как показали ЧеблоПашины разборки, проблема была в том, что располагается cxsd на NFS, но соответствующий раздел отмонтировался ДО исполнения останова серверов -- т.к. stop-cx-servers.service выполнялся уже ПОСЛЕ "nfshome.service".

      А NFS, как известно, в таких случаях завешивает все обращения намертво -- даже никакой "kill -9" не сработает, так и останется процесс в состоянии DEFUNCT.

      Т.е., deadlock из-за как бы "проблемы курицы и яйца".

    • Как объяснить SystemD'е требуемый порядок -- хбз; это ж не классический SysV Init, у которого просто имя стоп-файла (/etc/rc.d/???/KxxName) должно начинаться с достаточно малого числа после "K".
    • Потому ЧеблоПаша напрягся, почитал документацию, погуглил примеры -- и смог-таки сделать старт/стоп ОДНИМ сервисом!

    Вот результат его трудов:

    [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.

    • Непосредственная причина в том, что не находится библиотека libca.so.3.15.6, от которой cxsd_fe_epics.so зависит.
    • И это при том, что строка
      export LD_LIBRARY_PATH=~/epics/base-3.15.6/lib/linux-x86_64
      имелась и в .zshenv (для ZSH) и в .profile (для BASH, работающего /bin/sh'ем).
    • Гугление на тему "systemd environment" и конкретно "systemd LD_LIBRARY_PATH" подсказало, что
      1. Сам systemd ничего "неявного" принципиально не взводит -- для "детерминизма" (а только явные указания Environment= и EnvironmentFile= в .service-файле).

        ...ещё есть некий /etc/environment (в CentOS-7 присутствует, пустой, принесён пакетом "setup-2.8.71-7.el7.noarch", а используется хбз кем). Но пхать в СИСТЕМНЫЙ файл указания на home-директорию конкретного юзера, да ещё и находящуюся на NFS'е -- так себе решение.

      2. Чтобы BASH подхватил свой .profile с настройками, он обязательно должен быть login-shell'ом, для чего нужен ключ "-l".

        (Этот олигофренизм формально умеет парсить "environment-файл", причём при любом запуске, но для этого вместо какого-нибудь фиксированного имени вроде ".bashenv" или ".bash_environment" ему надо указывать имя файла в переменной окружения ENV -- совершенно бесполезное решение, IMHO.)

    • Т.е., нужен ключ "-l".

    Итого, решение -- добавить к /bin/sh ключ "-l", так что теперь "/bin/sh -lc ${HOME}/..." (причём в обоих -- ExecStart= и ExecStop=).

    Проверено -- помогло.

  • 11.02.2023: обнаружилось, что скрипты при NOX=1 не устанавливаются. Причина в том, что они живут в programs/runner/, по NOX=1 исключающейся из сборки.

    Что делать?

    1. Первая мысль -- перенести их в какую-то другую директорию (то ли в programs/server/, то ли отдельную создать).
    2. Чуть позже -- а если исключать из сборки не всю директорию, а только сам cx-starter внутри неё?

    11.02.2023@~17..18, по дороге между лавкой на Морском-16 и Ярче, рядом с Морской-4: собственно, главная проблема с "создать отдельную директорию" -- то, что директория должна была бы назваться "programs/scripts/", но а) первая буква перекроется с "server" и б) может возникнуть путаница между нею и 4cx/src/scrtips.

    Поэтому я полдня пытался придумать варианты названия, которое было бы осмысленным, но не начиналось бы ни на 's', ни на 'r' (run*).

    Так вот: а кто сказал, что слово-название должно быть простым и надо начинать с 's'? Можно использовать словосочетание, где вторым словом будет "scripts", а перед ним ещё что-то.

    Вот и придумал -- "launch scripts", "запускающие скрипты"; совращённо -- "lscripts/".

    12.02.2023: так и делаем.

    • Создана отдельная директория lscripts/, населённая "исходниками" нужных скриптов и Makefile'ом (последний является копией runner/Makefile, из которой убрано всё, касающееся cx-starter* и cstart).
    • Она добавлена в programs/Makefile.
    • Из runner/ .sh-"исходники" этих скриптов убраны, как и ссылки на них из SH_UTILS в тамошнем Makefile.

    Единственный минус -- дублирование Makefile-кода: поскольку в runner/ живёт cstart (привязанный к cx-starter'у), то весь код там остался, просто список его целей SH_UTILS сократился до cstart'а.

    С этого момента содержимое данного раздела располагается в programs/lscripts/, хотя всё описание оставляем тут.

  • 21.08.2023: проявился косяк: при рестарте машины-сервера (реально виртуалки) canhw почему-то не запускается canhw:12.

    Расследование показало, что это из-за наличия в 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[]. Так что "заменять '^' на пробелы" просто негде.
    • @вечер: а если рассматривать какой-нибудь символ -- чисто синтаксически напрашивается '=' -- наравне с пробелами? Тогда для shell'а он не будет разбивать строку, а для парсера конфигурации вполне даже будет должным разделителем.

    22.08.2023: делаем.

    • Анализ кода cxsd_config.c показал, что там повсеместно используется ppf4td_skip_white().
    • Вот и сделана его локальная почти копия skip_white_and_eq(), которая помимо isspace() пропускает также и '='.
    • И в коде также везде заменено; плюс в setenv_parser() удалена отдельная проверка на '=', позволявшая писать в стиле "ENVNAME = STRING", т.к. теперь тот же синтаксис (даже ослабленно-контролируемый) поддерживается автоматом.
    • Строка в srvparams.conf теперь выглядит так:
      .srvparams canhw:12 params="-b200000 -e=load-frontend=epics"

    Проверяем.

    • Через cx-starter -- по-прежнему работает.

      (Как раз занадобилось перепустить 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 SYNTAX

      Although 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 есть упоминание, что он уже работает.

:
Иные programs/:
utils/:
Общее:
drvinfo:
  • 11.07.2009: создаём эту утилитку -- чтобы можно было просматривать описания _drv.so-файлов, вытягивая их прямо из метрик.

    11.07.2009: сразу же вылезла очевидная и дурацкая проблема: dlopen()'ом драйверы просто так не грузятся, почему-то даже с RTLD_LAZY -- требуют функций из cxsd_driver-API.

    Очевидное решение -- подлинковывать библиотеку сервера, чтобы символы были определены.

    (В принципе, есть решение попроще и похалтурнее -- просто иметь такие символы "пустышками". Тогда всё будет грузиться и резолвиться. Но, что неприятно -- будет дублирование.)

    Короче -- сделано. Плюс, пришлось завести и public_funcs_list[].

    Утилита в первоначальном варианте работает, основную информацию выдаёт, включая даже список не-NULL методов. Осталось добавить:

    1. Дамп возможных параметров из params_table.
    2. Дамп таблицы каналов из chan_info.
cxclient:
  • 10.09.2009: создаём консольную утилиту для ручного доступа к CX-серверам, называем -- по-простому -- cxclient. Она должна заменить CXv2'шные cx-rdt, cx-wrt, cx-sub, cx-setchan, cx-bigc.

    Сейчас создан просто скелет -- в первую очередь для отладки cxlib'а.

  • 10.09.2009: некоторые соображения о том, как должен будет выглядеть способ указания параметров через argv[].

    10.09.2009: исходная посылка: может понадобиться посылать НЕСКОЛЬКО пакетов запроса (мало ли -- блокировку там наложить, или еще чего). Однако, в каждом из пакетов может потребоваться по НЕСКОЛЬКО операций чтения/записи. Таким образом, нужен какой-то способ указывать границу. Хотя красиво выглядел бы симвом ';', но из-за shell'а он неприемлем, так что выберем его аналог-братца из BASIC'а -- ':', который совершенно неинтересен shell'у.

cdaclient:
  • 11.09.2009: еще одна консольная утилита -- для ручного доступа через cda. Её аналога в CXv2 не было. Называем опять по-простому -- cdaclient, интерфейс стараемся сделать максимально совместимым с оным в cxclient'е.

    Сейчас создан только скелет -- полезный для отладки cda.

  • 02.04.2014@Снежинск-каземат-11: сделан новый вариант, на cda-с-контекстами. Пока он называется ncdaclient.c.

    Синтаксис вызова --

    cdaclient [-b DEFPFX] {COMMAND}
    т.е., могут указываться совершенно разные ссылки, хоть от разных серверов и даже протоколов.
  • 02.04.2014@Снежинск-каземат-11: фишка в том, ЧТО можно (будет) указывать в COMMAND:
    • CHANNAME -- простейший вариант, имя-ссылка.
    • CHANNAME/TYPE -- типизированная ссылка, где TYPE указывается аналогично devlist'овым: t[COUNT], где t -- [bhiqsdt], а опциональное COUNT служит для векторных каналов.

      Т.е., умолчание -- i1 /*CXDTYPE_INT32, скаляр*/.

    • CHANNAME[/TYPE]=VALUE_TO_SET -- возможность ЗАПИСЫВАТЬ значения. И векторные каналы должны позволять указывать прямо вектор, список значений через запятую.
    • ':' -- пауза, аналогично canmon'у/cm5307_test'у.

      Это может потребовать уже существенно более хитрой работы, с запоминанием параметров в список и последующим его исполнением. И работа выйдет в 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: да, сделано.

    • Парсинг -- можно в произвольном порядке указывать префиксы "@T[count]:" и "%DPYFMT:".
    • Сохраняется инфа (в первую очередь сейчас используется именно dpyfmt) в свежевведённом SLOTARRAY'е.

    19.02.2015: печать чуток под-умнена -- теперь оно смотрит, скаляр ли это или вектор, и в векторе крутит цикл (но ПОКА реально данные не печатает, т.к. надо жонглировать разными типами; да и работа с dpyfmt не рассчитана на целочисленные типы).

    22.04.2015: в дополнение ко вчерашнему добавлению поддержки CDA_DATAREF_OPT_'ов теперь улучшаем печать результатов -- чтоб оно корректно выводило ВСЕ варианты.

    • Сначала отдельная ветка на типы CXDTYPE_TEXT и CXDTYPE_UCTEXT. Оно говорит длину строки, а потом выдаёт саму строку в кавычках.

      Выдача 1 символа делается отдельной PrintChar(), обладающей некоторым интеллектом:

      • 8-битные символы: сначала escape'атся '\\', '\'' и '\"'; затем если isprint(), то выдаётся как есть; отдельно проверяется на '\n' и '\r', а иначе -- печатается \xXX (т.е., всякие \a, \t, etc. отдельно не исследуются). 27.04.2015: да ладно -- сделаны '\a', '\b', '\f', '\t', '\v'.
      • 16-битные -- в формате \uXXXX, а 32-битные -- в формате \UXXXXXXXX. Т.е., как принято в современном Си (C99?).
    • Все же прочие типы:
      • Если канал скалярный -- max_nelems==1 -- то печатается "=значение", иначе "[cur_nelems]={список_через_запятую}".
      • Сама печать делается в соответствии с dpyfmt -- раздельно для вещественных и целочисленных типов, причём для целочисленных отдельные альтернативы для знаковых и беззнаковых вариантов (там, конечно, еще от знаковости в dpyfmt зависит, но хоть расширение будет произведено корректно знаково/беззнаково).
    • Получение dpyfmt от юзера теперь об-умнено:
      • В момент парсинга "%xxxC" в зависимости от известного на текущий момент вида типа -- CXDTYPE_REPR_FLOAT/CXDTYPE_REPR_INT вызывается либо ParseDoubleFormat(), либо свежевведённая ParseIntFormat().

        Т.е., по-хорошему, формат надо указывать ПОСЛЕ типа -- "@T:%nnnC".

      • Но после парсинга флагов @/% стоит дополнительная проверка:
        • Если формат не был указан, то берётся default для нужного вида -- GetTextFormat(NULL) или GetIntFormat(NULL).
        • Если же был, то проверяется соответствие его вида виду типа, и если нет, то выдаётся warning и форсится опять же default.
      • Конкретно для INT64/UINT64 дополнительно сразу после процента вставляется "ll", чтобы формат стал "long long".

        26.04.2015: НЕПРАВИЛЬНО было после процента ("%ll#15d") -- судя по man 3 printf (раздел "Format of the format string"), length modifier должен быть непосредственно перед conversion specifier'ом. Поэтому переделано: теперь символ формата (вместе с '\0') сдвигается на пару символов дальше, и перед ним вставляется "ll".

      • Отдельно для форматов oxX форсится FMT_ALTFORM, чтоб данные выдавались корректно, годными для дальнейшего обратного парсинга.
      • ...для TEXT/UCTEXT понятие dpyfmt смысла не имеет, поэтому на них проверки не распространяются.

    Мозгов там уже столько, что впору вытаскивать их в отдельную бибилиотеку, дабы не дублировать код и иметь единый формат/интерфейс для консольных утилит. Назовём, видимо, console_cda_util.[ch].

    23.04.2015: на будущее -- план, как реализовать функционал записи в cdaclient:

    • Иметь переменную "количество ожидающей записи", изначально =0, которую при парсинге для каждого '='-параметра увеличивать на 1.
    • данные для записи после '=' распарсивать и складывать в malloc()'нутый блок, на который проставлять ссылку в ri.
    • В ri же иметь флаг "есть данные для записи", и для писуемых выставлять его в 1.
    • Когда придёт CDA_REF_R_UPDATE, то можно сбрасывать флаг "есть", отправлять данные, а потом выставлять в 1 флаг "данные отправлены".
    • Когда придёт следующее CDA_REF_R_UPDATE, то можно считать задачу по записи выполненной (это нужно чисто для консольных сценариев -- чтоб реализовывать "синхронную запись").

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

    • Если в конце нет ":", то при достижении "количеством" значения 0 можно делать exit(0).
    • На случай, если захочется реализовывать всякие хитрые сценарии, типа "выполнить такие-то записи, дождаться их отработки, потом дальше делать то-то": для начала, правильно такие вещи делать НЕСКОЛЬКИМИ вызовами cdaclient.

      А если всё же припрёт -- видимо, а) вводить какой-то символ/команду "дождаться окончания исполнения текущего списка записи"; б) запускать запись не всю сразу, а "пачками", до команды "дождаться", и следующую пачку запускать после окончания дожидания предыдущей.

      ...ей-богу, шиза это -- шибко трудоёмко, правильнее делать отдельными вызовами cdaclient.

    27.04.2015: подготавливаем вытаскивание в отдельную библиотеку.

    • Создан тип "свойства ref'а" -- util_refrec_t.
    • Печать вынесена в PrintDatarefData(), причём какие части печатать (время, имя, timestamp) -- указывается битофлаговым параметром parts.
    • Парсинг ушел в ParseDatarefSpec().

      Имя канала она теперь берёт до опционального '=', указатель на который возвращает вызывателю в endptr.

    • И обе они уехали в console_cda_util.c.
    • И das-experiment переведён.

    Ну и мелкие модификации, для более качественного отображения:

    • Символ '=' перед скалярным значением убран (для das-experiment'а -- точно лишнее).
    • Значения выдаются не напрямую printf()'ом, а сначала в буфер, где делается rtrim+ltrim. Так получаем более красивые списки, плюс отсутствие пробелов даст боле корректную выдачу в das-experiment'е, где всё space-separated колонками.

    А насчёт записи -- понятно, что будущие dataset-save и dataset-restore будут по факту упрощенными вариациями cdaclient'а. Так что парсинг значения для записи также надо обобщать.

    29.04.2015: продолжаем -- после вчерашних продвижений в разделе о соседней утилите :):

    • Надо б уметь имена печатать не полные, добытые от cda, а короткие исходные -- чтобы можно было результат вывода сбагрить за отправки в сервер такой же утилите с такими же defpfx+baseref (или как раз наоборот, с другими -- для копирования уставок в другую аналогичную иерархию).

      Для этого

      • Вводим ключ -r (relative).
      • Поле spec переносим уже прямо в util_refrec_t.
      • Добавляем флаг UTIL_PRINT_RELATIVE...
      • ...и его обработку в PrintDatarefData().
    • Поскольку в будущем как минимум dataset-save должен будет уметь писать не на stdout, а в файл (ключ -o FILENAME), то и весь интерфейс печати должен отправлять в указанный FILE*.

      Поэтому PrintDatarefData() добавлен первым аргументом FILE*fp.

      А всех на "-o" переведём позже.

    • Раз данные могут отправляться в файл для последующего восстановления, то надо уметь перед именем выдавать и тип ("@T[nnn]:").
      • Флаг UTIL_PRINT_DTYPE.
      • "Интеллектуальная" выдача в PrintDatarefData().
    • Данные на входе из файла могут быть префиксированы временем. Его нужно уметь обрезать.

      Сделано просто, а для отличения от "@T..." используется факт, что в метке времени после "@" идёт цифра.

    • А еще иметь бы возможность писать множественные спецификации -- как в cxsd-devlist, с "<MIN-MAX>"; а еще чтоб списком через запятую (а еще б множественные диапазоны, в нескольких точках -- как в scenario...). Как для отображения, так и для записи -- чтоб уставлять сразу кучу каналов.

      На нижнем уровне это должно отражаться в аллокации по такому spec'у МНОЖЕСТВА ячеек. А как конкретно реализовывать расширение такого шаблона -- вопрос отдельный; видимо, надо функцию в console_cda_util заводить.

    • У нас сейчас по факту какой-то "cda-mon" -- мониторирует данные и печатает по мере поступления (похож на cx_rdt). Штука, конечно, полезная, но надо бы заиметь режим "прочитай такой-то канал(ы)" и "запиши", с отваливанием после исполнения; и этот режим должен быть основным.
    • Связанная тема: мочь указывать время работы. Для режима мониторирования -- сколько отработать и выйти (аналогично -m у соседа). Для режимов просто чтения и записи -- таймаут, чтоб ждать не более чем столько.

      Кстати, в dataset-* тоже будет полезно.

      • Сначала переделываем ключи в das-experiment'е:
        • его -T теперь стал -p (оно и мнемоничнее -- "period"),
        • а бывший -m теперь -T -- "running Time, Time limit".

          И option_mesdur переименована в option_timelim.

      • Ну а дальше ключик добавлен (кстати, без <math.h> оно считало совсем не то).
    • Начинаем готовиться к записи...
      • В util_refrec_t добавлены поля
        • wr_req -- флаг запрошенности записи.
        • wr_snt -- флаг "значение отправлено, но пока не подтверждено".
        • val2wr (CxAnyVal_t) -- то, что писать (для маленьких значений).
        • *databuf -- буфер для больших значений; если ==NULL, то значение в val2wr.
        • num2wr -- количество элементов для записи.
        • 02.05.2015: num2rd -- запрошенность чтения.
      • Парсинг значения для записи -- ParseDatarefVal(). И пара helper'ов ParseOneChar() и ParseOneDatum().

    30.04.2015: продолжаем.

    • Мысли в сторону, на будущее:
      1. Нынешняя архитектура парсинга -- с fgets() строки и последующей обработкой -- годится для небольших значений. Крупные векторные каналы вроде осфиллографов и прочих waveforms оно отправить в файл сможет, а прочитать -- уже нет.

        Полная же поддержка любой длины -- это на ppf4td на схеме plaintext (и "mem" для параметров командной строки).

        Но сие трудоёмко, так что если в будущем понадобится -- как делать ясно, а пока тратить силы незачем.

      2. Сейчас все ошибки сводятся к "fprintf(stderr,...); exit();". Вообще-то не очень красиво -- лучше б завести архитектуру с возвратом -1 и складированием куда-то текста ошибки.

        А еще б более адресно указывать место ошибки (например, файл:строка, как минимум; а для векторов -- еще и номер элемента).

        Но это тоже трудоёмко, так что отложим.

    • Собственно -- сделан парсинг 1 integer/float-значения в ParseOneDatum().
    • Сделана простейшая обвязка в ParseDatarefVal():
      • Строковые литералы могут быть и "как есть", и окружены одинарными или двойными кавычками (при наличии открывающей закрывающая обязательна).
      • Числовые значения опционально иметь (в таком порядке):
        1. Префикс количества -- [NUMBER]. Причём и скалярам это разрешено -- [1].
        2. Символ '=' (реально он просто пропускается вмеремежку с пробелами -- так что даже "= = =" прокатит).
        3. Скобки (только фигурные) вокруг списка значений (при наличии открывающей закрывающая обязательна).
      • При nelems==1 парсинг делается отдельной веткой, ожидающей именно ровно 1 элемент, и складывающей его в urp->val2wr.

        ...собственно, пока только nelems==1 и сделано, поддержки векторов (с соответствующим аллокированием/ростом!) еще нету.

        04.03.2016: поддержка векторов уже есть, сделано для тестирования CXDTYPE_UNKNOWN.

    • Результат -- проверено на cdaclient, скаляры оно записывает.
    • И парсинг 1 char8/char32-значения сделан в 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. Итак:

      • Ключ -m взводит option_monitor.
      • Команды чтения/записи инкрементируют num2read и num2write соответственно, вместе со взведением в ячейке флага rd_req или
      • ЗАМЕЧАНИЕ: команда записи НЕ считается командой чтения -- НЕ печатает значение и НЕ участвует в подсчёте num2read.

        Если надо мониторить записываемый канал -- следует указать его еще отдельно без '='.

      • Сама запись отрабатывается по проекту от 23-04-2015, в два этапа:
        1. По первому R_UPDATE делается отправка, флаг wr_req сбрасывается, а wr_snt взводится.
        2. По второму R_UPDATE флаг wr_snt сбрасывается и команда считается отработанной.
      • При завершении отработки канала его флаг запрошенности сбрасывается, а соответствующий num2XXX декрементируется.
      • При option_monitor НЕ делается num2read--,rd_req=0. Так что и печатается при каждом обновлении, и завершения не происходит.
      • "Всё ли сделано" проверяется по "num2read!=0 && num2write!=0" -- флаг мониторинга не участвует, так что на команды только записи он не повлияет, произойдёт завершение.
    • Для регулирования, что печатать, введён ключ -Dnnn, где nnn -- список символов, указывающих "классы" печатаемостей. По умолчанию всё выключено.

      (И есть хитрость: '-' в списке классов означает "сбрасывай все следующие", а '-' -- "взводи".)

    12.05.2015: учитывая, что программа в общем-то работает, почитаем раздел за "done", а недоделанности (expand'инг имён каналов, парсинг векторов для записи, ключ -oFILE) допилим в рабочем порядке.

    12.05.2015: учёт -o OUTFILE сделан (в обоих).

    21.07.2015: дополнения в парсинг:

    • Парсинг опционального [COUNT] и опционального же isspace()/'=' сделан общим для всех типов (в самом начале функции).
    • Сделан парсинг строк (т.е. с n_items!=1).

    22.07.2015: дополнения в печать: теперь выдача ВСЕХ компонентов параметризуема. Это включает NELEMS (-DN), PARENS (-Dp), QUOTES (-Dq). Последняя пара в cdaclient включена по умолчанию, а в das-experiment и dataset-save всегда.

    30.03.2016: улучшения в блоке вывода:

    • Добавлена ловля события RDSCHG. Управляется ключом -DR.
    • ...Кстати, еще когда-то давно была сделана ловля STRSCHG, управляемая ключом -DS.
    • Оба этих события печатаются:
      1. "закомменченными" через '#'
      2. и с опциональным префиксом текущего времени (управляемым ключом -DT), но БЕЗ '@';
      3. также при выдаче имени канала учитывается option_relative -- ключ -r.
    • Этим же правилам подчиняется и ругательство при RSLVSTAT.

    02.08.2016: добавлена возможность печатать ТЕКУЩИЕ значения (приходящие по CURVAL).

    • Ключ -Dc
    • Оно выдаётся в "комментарии" -- с префиксом "# CURVAL: ".
    • А с ключом -DC -- вывод идёт в той же ветке, что и UPDATE.

      Т.е., это команда "дай текущее известное значение, и пофиг, если оно старое".

    За компанию также добавлен более подробный help:

    1. На ключ '-D'.
    2. Описаны возможные %- и @-префиксы для имён каналов...
    3. ...включая список понимаемых символов dtype'ов.
    4. Поскольку это всё очень длинно, то для показа этой дополнительной портянки надо указать "-hh".
  • 12.05.2015: насчёт спецификации множественных каналов "шаблонами": идеально сделать комбинированный вариант, И со списками (<FROM-TO>), И с диапазонами ({ITEM,...}) -- как в zsh (вариант {N1..N2}, наверное, не стоит делать).

    12.05.2015: да, так и сделано.

  • 15.05.2015: добавлена фичка -- если после имени канала непусто, но после пробелов идёт '#', то оно НЕ считается значением для записи.

    Это чтоб можно было в списке каналов писать комментарии (а das-experiment и dataset-save и так игнорируют всё после имени канала).

    22.07.2015: вот эта "фичка" даёт проблему -- пустые значения строк (указанные в командной строке как nnn="" -- т.е., кавычки убираются shell'ом) НЕ считаются значениями для записи.

    Строго говоря -- причина в нечетко определённом синтаксисе: наличие/позиция '=' толком не определено (в т.ч. относительно [COUNT]). Доопределить -- и тогда можно сделать более однозначные правила; например, что наличие '=' форсит режим записи, даже при пустоте далее.

    ...пока же пришлось в качестве хака в cdaclient.c::RememberChannel() убрать '=' из списка пропускаемых символов -- чтобы пропуск сделал ParseDatarefVal().

  • 17.12.2015: есть надобность уметь писать данные в РАЗНЫЕ файлы на каждое измерение. Надобность, в первую очередь, для векторных каналов -- и в самую первую очередь для CCD-камер.

    ВОЗМОЖНО, это вопрос не к cdaclient'у, а к какой-то иной утилите, которая бы формировала вывод иным образом. В v2 это делает cx-bigc в режиме gnuplot, и туда такая фича легла бы проще всего.

    17.12.2015: тем не менее, некоторое обсуждение внешних аспектов реализации сделаем здесь.

    • Первейший вопрос -- а КАК указывать из командной строки такую необходимость?

      По осмотру имеющихся ключей (-oOUTFILE у cdaclient и отсутствующего у cx-bigc) разумным выглядит "озаглавливание" -- -O DESTINATION.

    • Второй вопрос -- генерация имён файлов на основе этого "DESTINATION".
      1. Простейший вариант -- считать "DESTINATION" за базу и дописывать к ней некий генеримый суффикс (последовательно увеличивающийся).

        Но тут неудобство в том, что у файлов будет отсутствовать вменяемое расширение.

      2. Поэтому вариант погибче -- если в "DESTINATION" есть символ '*', то считать это шаблоном, а суффикс писать вместо '*' (как это сделано в Xh_fdlg и в v2'шном cdrclient).
    • Отдельный вопрос, конечно, о генерении самого суффикса, но тут проще.
      1. Либо просто последовательно увеличивающееся число (и не забыть энное количество ноликов в начале, для правильной сортировки).
      2. Либо YYYYMMDDhhmmssNNN (NNN -- увеличивающееся число, на случай, если больше 1 записи в секунду).

    02.08.2016: еще раз стало очевидно, что это должна будет делать другая утилита (ибо cdaclient и так перегружен мозгами), которую назовём "pzframe2text". Подробнее -- см. в его собственном разделе ниже.

  • 18.12.2015: кстати, не забываем, что надо в парсере каналов для записи уметь пропускать префикс "(ИМЯ_РУЧКИ)" -- будущий для Cdr-режимов.

    ...видимо, в ParseDatarefSpec() -- туда проще всего ложится, в цикло бработки префиксов while(1).

    18.12.2015: да, сделано, именно там -- очень просто. Но пока не проверено.

  • 13.09.2016: добавлена возможность печати квантов (по событию QUANTCHG) -- ключ -DQ. Нужно для отладки квантов/OTHEROP.

    Заодно добавлена возможность печати fresh_age -- ключ -DF.

  • 14.02.2017: занадобилось уметь выводить в конце -- при завершении по таймауту (-T ...) -- список необслуженных каналов.

    Потребность возникла на ВЭПП-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: да, так и сделано.

  • 22.12.2017@утро, дорога-в-ИЯФ, после ИПА, перед мышью: можно будет сделать пару утилиток -- epicscdaclient и tangocdaclient -- cdaclient со встроенными cda_d_epics и cda_d_tango. Чтоб собирался через "конструктор из кубиков", в сторонней директории.

    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: надо уметь отключать встроенную буферизацию записи (отправка после получения первого UPDATE) -- нужно для тестирования буферизации в cda_core.

    20.02.2018: посмотрел код -- вроде несложно: надо действия, выполняемые по REF_R_UPDATE при взведённом rp->ur.wr_req, исполнять прямо в ActivateChannel().

    20.02.2018@лыжи-после-обеда-2-я-двойка: ключ -w?

    А еще пользительно будет иметь возможность включить "протоколирование" момента записи -- когда выполняется отправка. Через какой-нибудь -Dw.

    21.02.2018: делаем.

    • Введён ключик -w и взводимая им option_w_unbuff.
    • Плюс print-параметр 'w' (-Dw) и взводимый им флаг print_writes.
    • Запись вытащена в отдельную PerformWrite().
    • ...куда также добавлен "логгинг" записи.

      Он простой: кроме слова "WRITE" печатает еще результат (OK или ERR), плюс, при включенности соответствующих ключиков, текущее время и имя канала (в простом варианте -- всегда просто то, что указано в командной строке).

    • И условный вызов оной PerformWrite() добавлен в конец ActivateChannel()

    Вроде работает.

  • 20.04.2018: во всей троице -- cdaclient.c, das-experiment.c, dataset-save.c -- в диагностическом сообщении при ошибке регистрации канала присутствовала одна и та же некорректность: говорилось про ошибку от cda_add_dchan(), в то время как реально давным-давно используется cda_add_chan() (а сокращённая форма не применяется нынче НИГДЕ).

    Исправлено.

  • 29.01.2019: вчера опять возникла дурная проблема: Виталя Балакин натравил cdaclient на текстовый канал, и в ответ получал "nan".

    Вот как бы так сделать, чтоб cdaclient'у не обязательно б было указывать тип канала, а он определял бы сам и как минимум ПЕЧАТАЛ бы значения как надо?

    Ведь у нас уже:

    1. И поддержка вариабельных каналов в cda и cxsd есть (да, как раз на чтение).
    2. И в августе 2018г была добавлена ещё пара фич в сторону полиморфизма:
      • Возможность получать информацию о "нативном" (серверовом) типе канала -- cda_hwinfo_of_ref().
      • Возможность менять тип канала "на ходу" -- cda_set_type().

      Так что со стороны библиотеки поддержка уже имеется, вопрос именно в использовании оной в cdaclient.

    29.01.2019@утро-дорога-на-работу-около-ИПА: как маркировать такие каналы -- записывать "неизвестный тип"?

    А как поступать с dpyfmt? @04.06.2019: придумано -- см. ниже за сегодня.

    30.01.2019@лыжи-после-обеда: а если ещё запись захотим реализовать -- вообще абзац/вертеп/бардак... Мысли на тему:

    • Чтоб смочь осуществить запись "потом" -- надо не парсить указанное "=ЗНАЧЕНИЕ", а просто сохранить его, чтоб распарсить ПОТОМ, после получения информации о типе.
    • А какой ввести символ типа для такого "разберись сама"? @z?
    • ...кстати, насчёт dpyfmt в голове постоянно бродят мысли "а не разрешить ли указывать МНОЖЕСТВЕННЫЕ форматы" -- чтоб сохранять int_dpyfmt и dbl_dpyfmt отдельно, а использовать тот, что подходит.
    • Вообще, для начала хоть бы поддержку @x в cdaclient сделать, на нынешнем уровне "только для чтения"!
    • А как обходиться с разнотиповыми каналами, чьи типы заранее неизвестны -- возможно, идеи появятся после изучения EPICS и TANGO.

    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: ещё немного размышлений на ту же тему: как мне СЕЙЧАС кажется -- похоже, просто нет глубокого смысла напрягаться. Резоны:

    • В конце концов, просто можно считать типизацию "строгой", как в C (с тем же автоматическим преобразованием между числовыми типами).
    • ...вон в TANGO даже и преобразования нет -- обязательно точно указывать тип (включая размер!).
    • А в EPICS caget'у (и caput'у) приходится тратить лишнее время на добычу типа.
    • Хотя в CX -- для cda_d_cx -- это не так, поскольку тип придёт прямо в первом же ответе на CH_OPEN.
    • Но с ещё одной стороны: ведь, формально, тип может от инкарнации к инкарнации канала МЕНЯТЬСЯ. И тогда это "автоопределение" становится слегка сомнительной фичей.

    15.04.2023@Бердск, после обеда-из-Кенгуру на бердской косе, во время прогулки по пляжу: ну да, единственный вариант -- при надобности "выяснить" тип именно ВЫЯСНИТЬ его. Да, потратив лишние усилия и время на состояние "пока неизвестно".

    26.05.2023: вообще-то по результатам реализации epics2cda, где делается cda_set_type(), видно, что ни "выяснять", ни "тратить время и усилия" не нужно: требуемая информация уже готова к моменту CDA_RSLVSTAT_FOUND. А реально останутся лишь проблемы DPYFMT и записи.

    15.11.2024: однако прямо сейчас в качестве временной меры (и, видимо, из-за "нет ничего более постоянного, чем временное", навсегда) сделана поддержка флага "@?".

  • 27.06.2019: потребна возможность уметь указывать cdaclient'у НЕ забэкслэшивать спецсимволы. Чтобы он мог печатать текстовые каналы, содержащие многострочные (через '\n') сообщения (побудительный мотив -- для потенциального-будущего канала "список всех каналов").

    27.06.2019: напрашивается ключик -De -- по аналогии с ключом -e у команды echo.

    Ну что ж -- делаем.

    • Замечание 1: такое умение нужно ТОЛЬКО cdaclient'у; dataset-save'у и тем более das-experiment'у оно категорически противопоказано.
    • Замечание 2: для "вящей корректности" можно бы при указании ключика "не забэкслэшивать" также отключать всякую печать имён и прочей служебной информации -- ибо оно будет смешиваться с собственно данными.

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

    • На стороне console_cda_util:
      • Введён флажок UTIL_PRINT_UNESCAPED.
      • PrintChar() теперь принимает дополнительный булевский параметр unescaped, при взведённости которого просто печатает байт как есть, не проверяя его значение.

        Но только БАЙТ, т.е., если в нём нет битов за границами &0xFF.

      • А PrintDatarefData() туда передаёт True при указанности в parts флага UTIL_PRINT_UNESCAPED.
    • На стороне cdaclient:
      • Флаг print_unescaped, ...
      • ...включаемый при помощи -De, ...
      • ...что отражено в выдаваемом help'е.
      • Замечание: сомнения вызвало то, в какое место списков (флагов и help'а) вставлять нововведение.

        Удовлетворительного решения не нашлось, т.к. сортировано там ни по алфавиту, ни по "значимости", и в разных местах порядок различается.

        Так что вставлено после quotes.

    Всё, работает.

    Замечание: а вот СТРОКИ (которые STRSCHG, -DS) -- те в любом случае печатаются "как есть", просто "%s"'ом, безо всякой интерпретации.

  • 15.10.2019: захотелось сделать режим "выдача в бинарном виде" -- комплементарный к "pipe2cda -B".

    Смысл -- чтоб можно было добывать в исходном виде бинарные данные, хоть записанные pipe2cda (бинарные файлы так копировать :D), хоть просто от драйвера.

    15.10.2019: ну делаем.

    • Флаг -- option_binary (как в pipe2cda), ...
    • ...взводит его ключ -B (тоже как pipe2cda, только без параметров).
    • Отработка флага выполняется непосредственно в ProcessDatarefEvent(): при взведённом обычная печать НЕ вызывается, а делается fwrite(,,,outfile) из буфера, добытого от cda_acc_ref_data() и в объёме, узнанном оттуда же.
  • 03.03.2020: давно хотел сделать ключ "-Da" -- включить ВСЮ по-канальную диагностику.

    Сделал!

    22.06.2021: заметил, что было забыто включение print_strings (ключ "-DS"). Добавлено.

  • 09.02.2021: поднадоело, что timestamp'ы на каналах выдаются в UNIX-формате "секунды.микросекунды".

    Приходится переводить в человекочитаемый формат с помощью "date --date @секунды.микросекунды", что зело неудобно. (Вот сегодня разбирался с осциллографами в bivme2_rfmeas -- когда они последний раз отрабатывали, ключом "-dCt"; пришлось отдельно дешифрировать времена...)

    @дома-ванна, ~16:30: а можно ж добавить ключик -- например, "-D8" ("8" -- iso8601), чтоб оно вместо числа печатало результат stroftime_msc().

    09.02.2021: сделано, минут за 15-20, сразу после ванны.

    • Выдачей вообще-то занимается console_cda_util.c.
    • Поэтому в его API добавлен флажок UTIL_PRINT_TIMES8601=1<<9, а _PRINT_RFLAGS сдвинут дальше.
    • В PrintDatarefData() добавлена проверка на него и...
      1. Он и одиночный является достаточным -- наравне с _PRINT_TIMESTAMP.
      2. При его наличии timestamp "переводится" в struct timeval и выдаётся уже человекочитаемая строка.

        15.10.2021: халтура!!! Там в выдаче между значением и "@YYYY-..." отсутствовал пробел. Добавлен.

    • Учёт: флаг print_times8601, его взведение по "-D8" и использование-конверсия в 2 точках в ProcessDatarefEvent() (по UPDATE и CURVAL).
    • Плюс в выдачу "-hh" добавлено.
    • Более вроде ни в каких утилитах поддержка КЛЮЧИКА не требуется -- только в dataset-save форсится UTIL_PRINT_TIMESTAMP, но уж там человекочитаемость излишня.

    Работает, всё прекрасно.

  • 15.03.2021@утро-бритьё: при ненайденности канала cdaclient вычёркивает его из списка читаемых/мониторируемых и, если стало num2read == 0 -- завершается.

    Но в каких-то обстоятельствах это может оказаться проблемой: если канал почему-то отсутствует лишь временно,

    • Технически такое возможно легко: конфиг временно неправильный и через минуту будет исправлен, или канал переезжает и от старого места уже пришло NOTFOUND а от нового места FOUND ещё не успело.

      Точнее, при переезде ДОЛЖНО бы приходить RSLVSTAT_SEARCHING, а его cdaclient просто игнорирует, так что формально переезд типа безопасен. Но не факт -- не вякнет ли cda_d_cx NOTFOUND'ом при обновлении конфига сервера вместо нужного SEARCHING'а?

    • Конкретный сценарий, когда такое "не обращать внимания на временный NOTFOUND!!!" будет важно при использовании cdaclient в автоматическом режиме -- не вполне ясен.

      Но вот в ручном режиме, когда утилита была запущена для мониторирования, потом канал временно исчезал и появился обратно, а утилита из-за этого перестала его мониторировать, что неприятно (и было обнаружено задним числом!) -- такой сценарий очевиден.

    • ...а ещё подобное возможно с другими dat-плагинами -- да хоть с cda_d_epics!

    Так вот:

    • Может, сделать флажок "-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" ни к чему.

  • 31.03.2021: вылезла неприятная штука: при указании в ключе "-f FILENAME" имени, которое указывает не на файл, а на директорию, никакой ошибки при открытии не происходит, но чтение тут же заканчивается -- очевидно, вследствие того, что читать из директории нельзя и fgets() обламывается. В результате cdaclient выдаёт лишь сообщение "no channels to work with", что несколько сбивает с толку.

    Произошло это на ВЭПП-4, когда Карнаев по ошибке сделал симлинк не на .dat-файл, а на "../", так что утилитка Polarity_switch.sh не могла загрузить позитронный режим.

    Выглядит сие всё странно: в ReadFromFile() проверка есть, но она ошибок не ловит.

    01.04.2021: разбираемся... Да, ситуация диковатая, проблема в самом C/UNIX API, но, похоже, никого оно не волнует.

    • Проверка с использованием давно имеющегося test_fopen показала, что да -- оно счастливо открывает директории "на чтение", а ругается уже при попытке реального чтения.
    • Прогон под "ltrace -S" показывает, что подстилающий open() прекрасно отрабатывает, а вот подстилающий read() уже возвращает -21, т.е. EISDIR.
    • Какого лешего так происходит -- неясно: давным-давно существует O_DIRECTORY, и почему open() на директорию БЕЗ этого флага не обламывается -- загадка.
    • Гугление интернетов по строке "open directory without o_directory should fail" ничего не нашло -- ВООБЩЕ НИЧЕГО. Словно никого эта проблема не колышет.
    • Поскольку совершенно идентичный код чтения списка из файла есть и в das-experiment.c с dataset-save.c, то они тоже подвержены этой проблеме.

    Что делать -- неясно...

    А может, после УСПЕШНОГО открытия пытаться посмотреть тип файла -- что-то типа 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: насчёт этих любых других мест:

    • @~17:40, около Ярче, бродя по паркове Терешковой-21а: а можно бы такую проверку опубликовать в misclib'е, под именем fp_refers_to_dir()
    • @вечер: да и для обычных дескрипторов тоже можно -- "filedes_refers_to_dir()". Чтоб какое-нибудь открытие могло б сразу обламываться.
    • 02.04.2021@утро: но если ЕЩЁ подумать -- то не стоит этого всего делать.

      Резон прост: работает -- не трогай! Мало ли какие косяки где могут вылезти (например, какое-нибудь особое устройство/файл (особая файловая система?) почему-то будут считаться и как директория -- битик там этот будет торчать).

      Конкретно в ЭТОМ случае с утилитами -- решалась КОНКРЕТНАЯ проблема, причём всего лишь WARNING'ом, так что при надобности и на "директорию" натравить всё же можно.

  • 22.05.2023: добавлена возможность печатать события CYCLE -- за это отвечает ключик "-DY", отражающийся на print_cycles. Печатаются "номер сервера" -- nth, передаваемый в info_int -- и имя сервера.

    Неприятным сюрпризом стало то, что "имя сервера" НЕ включает в себя схему. В частности, для "epics::" всегда будет просто пустота "". Поэтому перед именем также печатается и схема, добываемая через cda_status_srv_scheme() (сделанную ещё 01-12-2015).

    ЗЫ: сделано для возможности диагностики циклов, в частности, работы свежевведённых сегодня $CDA_D_EPICS_CYCLE_USECS и $CDA_D_TANGO_CYCLE_USECS.

  • 25.05.2023@после-обеда, идя из Гусей по Николаева домой: а ведь с помощью cdaclient можно б было делать "универсальный frontend": если запускать его, повесив (с помощью хоть netcat, хоть inetd) на некий TCP-порт, то если б он мог на ходу принимать "команды" на мониторинг каналов, то вполне бы мог заменить что-то вроде starogate.

    Но тут вопрос именно в "на ходу":

    1. Чтоб воспринимал приходящее с stdin как команды "мониторируй вот это" или "запиши вот это".
    2. А также отдельная команда "forget ИМЯ_КАНАЛА".
  • 08.07.2024: обнаружилась не мешавшая раньше особенность, которая всё же не совсем корректна: каналы СНАЧАЛА регистрируются посредством cda_add_chan(), БЕЗ evproc'ев, и лишь ПОТОМ отдельно вызывается cda_add_dataref_evproc().

    В результате при попытке отладки каналов-команд схемы "tango::" оная отладка обломилась: там события о готовности присылались прямо из регистрации, а поскольку evproc'а в этот момент ещё не было, то события игнорировались.

    Кроме этих Tango'вских каналов-команд пока что таких "сразу отрабатываемых" вроде нет (т.к. insrv:: в cdaclient бессмысленны). Но в будущем могут появиться -- например, dat-плагин для доступа к каким-нибудь локальным ресурсам.

    Можно попробовать "исправить", но будет это непросто: логика работы с num2read и num2write рассчитана на то, что события начинают приходить ПОСЛЕ регистрации всех каналов, так что:

    1. Если после отработки итератора регистрации число каналов для чтения/записи станет нулевым, то будет выдано ругательство "no channels to work with".

      Чуть позже: неа, это проверяется по nrefs, а оно декрементируется только при ошибках регистрации.

    2. Если ВСЕ каналы окажутся отработаны прямо во время регистрации, то запускаемый далее основной цикл окажется бесконечным.

      Соответственно, надо запускать основной цикл УСЛОВНО, если ещё осталась работа.

    ...и мало ли что ещё там вылезет.

    08.07.2024: краткая попытка понять, почему сделано именно так, раздельно.

    • Вкратце -- а фиг знает.
    • В первых версиях (например, от 15-08-2014) evproc указывался сразу при регистрации.
    • Но затем схема сменилась на нынешнюю (видимо, где-то ??-02-2015).
    • Почему -- непонятно, но в начальных версиях всё делалось прямо в main(), а уже потом процесс был разделён на циклы по RememberChannel() и ActivateChannel(), в промежутке между которыми делается cda_new_context().

      Было ли отделение навешивания evproc'а от регистрации канала обусловлено какой-то рациональной причиной или просто "а почему бы нет?" -- неясно, по записям в начале раздела ничего нет. А есть оно в разделе по das-experiment за 28-04-2015 -- в cdaclient было сделано так же вроде как для унификации.

    11.07.2024: продолжение анализа -- смотрим уже архивные файлы.

    • das-experiment.c изначально, с рождения 19-08-2014, был с навешиванием evproc'а ПОСЛЕ регистрации.
    • А разделение запоминания и активации каналов на ДВА итератора в нём реализовано 28-04-2015, о чём запись в его разделе есть.
    • В cdaclient.c же отдельное навешивание evproc появилось -- судя по w20140815.tar.gz и w20140831-cda_core.tar.gz -- между 15-08-2014 и 22-08-2014.
    • Увы, записей не только об этом конкретном действии, но и вообще о каких-либо значимых действиях в этот интервал времени НЕ СОХРАНИЛОСЬ...

    Однако, судя по дополнительному разбирательству:

    • Но diff между исходниками между 15-08-2014 и 22-08-2014 показал, что -- главное различие введение refrecs_list; и добавление ячейки в него производилось уже ПОСЛЕ регистрации канала, так что индексы (rn) в нём -- только для успешно зарегистрированных каналов
    • Однако с тех пор это изменилось и теперь ячейка аллокируется ДО регистрации, в RememberOneChan(), вызываемом при наполнении списка каналов, происходящем при старте программы (список либо из argv[], либо из файла).
    • А произошло это изменение -- судя по w20150420-snezh.tar.gz и w20150429-morning.tar.gz -- между 19-02-2015 и 29-04-2015 (точнее, после 22-04-2015, судя по резервным файлам во втором архиве).

      Что согласуется с вышенайденной датой 28-04-2015 (где сказано, что "...и cdaclient.c на эту архитектуру переведён").

    Вывод: возврат указания evproc'а прямо в регистрацию канала не выглядит потенциально проблемным.

    11.07.2024: потихоньку...

    • Вызов sl_main_loop() сделан условным -- если хоть одна из num2read или num2write больше нуля. (Сделано ещё 09.07.2024.)
    • Указание evmask,evproc,privptr2 перенесено прямо в вызов cda_add_chan().

    Попробовал проверить -- и тут же нашёл причину того "навешивания позже": ведь вся печать реально делается функциями из console_cda_util.c, а они всю информацию берут из util_refrec_t *urp, включая urp->ref, но он-то ещё незаполнен!

    Решение впопыхах придумано и вроде простое:

    • прямо в начале DatarefEvproc() проверяется, что если в rp->ur.ref ещё не прописано значение, то туда прописывается переданное от cda значение ref (которое уже "правильное"),
    • а при аллокировании ячейки еёйное поле ref явно прописывается значением CDA_DATAREF_ERROR -- чтоб было конкретное "не прописано".
das-experiment:
  • 30.07.2014: Роговский пришел с потребностью в такой тулзе командной строки -- "experiment" (это EPICS'ное название). Оно мониторит некоторое количество каналов и пишет их значения на stdout по неким триггерам, коими могут быть как временной интервал (раз в N секунд), так и приход значения какого-то из этих каналов.

    Вроде сделать такую штучку совсем несложно.

    30.07.2014: конкретности -- как оно может быть устроено:

    1. Набор каналов указывается либо в командной строке, либо в файле, указываемом как @LIST_FILE (аналогично zip/arj/rar/...) (по одному на строку).
    2. Триггеры указываются либо префиксом '+' перед именем канала (такой канал является триггером), либо ключом "-T MILLISECONDS". Может указываться несколько триггеров (в т.ч. одновременно каналы и -T).
    3. Каждому каналу может указываться dpyfmt (используемый вместо некоего умолчательного), через префикс "%DPYFMT:".

    01.08.2014: выдрепнемся -- назовём тулзу "das-experiment".

    19.08.2014: первоначальная -- да, несколько халтурная -- версия сделана.

    • Копированием из cdaclient с минимальными изменениями.
    • Чтение списка из файла не сделано: префикс '@' уже использован для указания типа.

      Идеи? 28.04.2015: -f FILENAME, не?

    • Условия завершения пока тоже нет -- надо поинтересоваться требуемыми вариантами у клиентов.

    22.08.2014: условия завершения сделаны -- 2 варианта:

    1. -t СКОЛЬКО_РАЗ -- в штуках.
    2. -m СКОЛЬКО_ВРЕМЕНИ -- в секундах, дробно (ставится таймаут на это время, исполняющий exit()).

    28.04.2015: улучшаем -- в результате перехода на console_cda_util это стало легче:

    • Уставка дрыгалки периодического триггера перенесена из обработки ключа -T в точку перед sl_main_loop() -- чтобы период начинал считаться с момента реального запуска, а время считывания параметров и прочей инициализации на него б не влияло.

      ...по-хорошему, надо б еще сделать, чтобы он старался реально держать период (как CycleCallback() в сервере), а не просто +ПЕРИОД от момента вызова очередного.

    • "Заказ" канала выносим из main() в отдельную RequestChannel(), чтоб удобно были из -f пользоваться.
    • И обработка -f сделана -- ReadFromFile(), вычитывающая построчно, выкидывающая начальные/конечные пробелы, и игнорирующая пустые строки и начинающиеся с '#'.
    • ...только работать это в таком виде отказалось: ведь оно отрабатывается еще ДО создания контекста, и каналы регистрировать не с чем.
      • А "до" -- потому, что ключи могут идти в произвольном порядке, и есть -b BASEREF и, особенно, -d DEFPFX, влияющие и на создание контекста, и
      • ...в принципе, напрашивается мысль с BASEREF выпендриться в стиле tar, позволяющего смешивать -C с именами файлов.

        Только как-то это выглядит кривым/проблемным в реализации, так что пока не будем.

        (Кривым -- потому, что парсинг getopt()'ом сначала даёт все ключи, и лишь потом парамеры. Поэтому смешивать BASEREF'ы удастся только со списками из файлов.)

      • Видимо, придётся как-то сначала сохранять все ссылки в список, а потом уже их cda_add_chan()'ить.
      • Но делать отдельный slotarray -- как-то странно, откуда напрашивается мысль поменять архитектуру: добавить к util_refrec_t поле "spec", аллокировать ячейки прямо при парсинге, а потом уж проходиться по всему списку скопом, "реализовывая" его (отдельно может возникнуть вопрос -- что делать с ошибками регистрации: Rls(), чтоб дальнейшему эккаунтингу (чтения и записи) не мешали?).

        Тут надо поразмыслить.

        @после-обеда: а зачем сохранять ССЫЛКИ в список, когда можно сохранять туда имена файлов, и потом проходиться по ним итератором?

        Хотя -- нет, интереснее выглядит всё-таки технология "накопить все ссылки, а потом их реализовывать".

    • Замечание: вся эта инфраструктура должна потом пойти также в cdaclient и dataset-save/dataset-restore. Надо сие учитывать при её дизайне.

    28.04.2015: после обеда: да, переделано.

    • В refrec_t добавлено поле spec.
    • Регистрация каналов -- бывший RequestChannel() -- разделена на 2 части:
      1. RememberChannel() парсит "команду", аллокирует ячейку, сохраняет в ней результаты парсинга, в т.ч. в spec strdup()'ится channame[].

        Вызывается как для считанного из файлов, так и для указанного в командной строке.

      2. ActivateChannel() уже регистрирует канал у cda. И если регистрация обломилась, то делает RlsRefRecSlot() ячейке.

        Вызывается после создания контекста, при помощи ForeachRefRecSlot(), поскольку является итератором.

    • Кстати, регистрация укаанного в командной строке перенесена в парсинг командной строки -- т.е., еще ДО создания контекста.
    • ...и cdaclient.c на эту архитектуру переведён.
  • 21.05.2018: есть желание переделать формат вывода das-experiment на совместимый с histplot'ом -- точнее, с тем, что делает 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, по нему выдаётся строка заголовков:
      • Каналы перебираются ForEach'ем, как и в выдаче данных.
      • В качестве имени выдаётся ссылка на канал -- берётся прямо из rp->ur.spec; т.е., как в режиме RELATIVE -- относительно базы.
      • Также принудительно делается префикс %DPYFMT:.

        19.08.2018: теперь только для CXDTYPE_REPR_FLOAT -- для TEXT оно не существует, а для INT всё равно бессмысленно.

    • Перед человеко-читаемым временем добавлены численные СЕКУНДЫ.mss, ...
    • ...а после него -- СЕКУНДЫ.mss с первой строки.

      Именно с ПЕРВОЙ -- время начинает "бежать" не с момента запуска, а с первого события.

      Это сделано по образу и подобию canmon'а.

    Работает (ну ещё бы! :)), "done".

    17.06.2018: надо б добавить, чтобы das-experiment мог бы владеть информацией о disprange, для умения выдавать её в строку заголовков. Иначе в histplot'е фиговато -- диапазон [-100,+100] частенько совсем неподходящ.

    Главный вопрос -- в синтаксисе...

    29.06.2018: результаты размышлений и исследований на эту тему.

    • Информацию надо просто парсить и складывать куда-нибудь в util_refrec_t, чтоб она там просто была/лежала, самой утилитой никак не используясь.
      • И информация желательно чтоб была ЛЮБАЯ, а не только disprange. Т.е., включая label= и units= -- всё, что полезно для histplot'а.
      • ...складировать всё в SLOTARRAY?
    • Что касается синтаксиса.
      • Первой мыслью было "а пусть @/параметр/". Но фиг: "@/" уже занято под OPT_SHY.
      • Так что -- придётся делать использовать запятую ',': "@,параметр:" (либо "@,параметр,параметр:").

        Запятулька и для shell'а индифферентна, и совпадает с синтаксисом histplot'а.

    • Теперь насчёт символа присваивания.
      • Хотелось бы использовать '='.

        Но, на первый взгляд, проблема: оно ж используется в командах записи, для указания записываемого значения.

      • Но анализ исходников показал, что нет: нигде НЕ делается предварительного поиска символа '=' (с разделением спецификации по нему и потом раздельным парсингом), а выполняется просто последовательный просмотр, и на '=' обращает внимание только cdaclient, ПОСЛЕ ParseDatarefSpec().
      • Следовательно -- МОЖНО использовать '=', и будет идентично по синтаксису с histplot'ом.
    • Одно замечание: насчёт аллокирования/освобождения памяти -- надо учитывать, что при неуказанности ключика -1 каждая спецификация из командной строки может размножиться на кучу экземпляров refrec_t.

      И если потом кто-то из них будет признан невалидным и удалён, то НЕЛЬЗЯ в RlsRefRecSlot()'е делать free() параметрам, т.к. они будут разделяться между несколькими экземплярами.

      Да, это халтурка, что -- в случае отсутствия разделения (т.е., единственный канал либо невалидны все) будет оставаться мусор в памяти, но тут на это можно забить (утилита ведь одноразово принимает параметры, и никакой постоянной утечки быть не может).

    ЗЫ: это всё ПРОЕКТ -- годный для реализации, но пока лень.

console_cda_util.c:
  • 12.05.2015: заводим свой раздел, чтобы сюда писать общие для всех утилит вещи.
  • 12.05.2015: о наполнении 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: оказалось, что cdaclient не умеет печатать rflags.

    10.11.2015: сделано:

    • Добавлен флажок UTIL_PRINT_RFLAGS.
    • При его наличии PrintDatarefData() печатает флаги -- cx_strrflag_short()'ом -- после timestamp'а, в угловых скобках.
    • cdaclient'у необходимость выдачи указывается ключом -Df.
  • 07.03.2017: как-то поддостало, что во всех утилитах данные приходят по циклу, а не по обновлению. Указывать всем всегда "@u" -- подзадалбывает.

    07.03.2017: можно подвыпендриться:

    1. С символами префиксов. Например, "@=". Но и не особо красиво, да и жалко символ расходовать (ВЕЗДЕ ведь -- и в Cdr/datatree тоже; см. обсуждение за 265-05-2016 в разделе datatree).
    2. Ключик командной строки, вроде -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'е, работает.

  • 16.10.2017: появилась потребность мочь в консольной утилитке извлекать некоторую конкретную точку из осциллограммы (Валера Мусливец просит уметь писать в "журнал" корреляцию между каким-то напряжением и данными с клистрона (значение на "полочке" в осциллограмме)).

    Первая мысль -- рисовать отдельную утилиту, делающую такое.

    Вторая мысль -- надо бы в cdaclient добавить, чтоб можно было попросить векторный канал, но печатать из него только какой-то конкретный элемент.

    16.10.2017: указывать надо явно как-то в префиксе канала, где-то среди всяких '@'.

    • Вопрос 1: КАК? Уж все символы позадействованы.
    • Вопрос 2: надо б предусмотреть возможность дальнейшего расширения, чтоб кроме шифрограмм спецсимволами можно б было указывать PSP-спецификации широкого профиля -- ОПЦИЯ=ЗНАЧЕНИЕ{,ОПЦИЯ=ЗНАЧЕНИЕ}.
    • Кстати, анализ кода показывает, что в префиксе можно использовать символ '='' -- там так всё устроено, что проверка на его наличие делается уже после всего остального парсинга.
    • @после-обеда а не ввести ли возможность делать так "вырезку" не одной точки, а произвольного "куска" -- от X до Y?

    Короче:

    1. Как делать реализацию -- понятно и просто: расширить util_refrec_t, чтоб там было поле "elem_n" (какой элемент вектора печатать, если не равно -1), и тогда выдавать его как скаляр.

      (Или пару полей, если делать вырезку: "от и до", либо "от и сколько".)

    2. Как синтаксически указывать эту вырезку -- вот вопрос...

    18.10.2017@ВЭПП-5-кухня-~17часов-разговор-с-Валерой: а может, для ДАННОГО случая удобнее будет сваять драйверочек, который отдавал бы 1 скалярный канал, являющийся вырезкой из вектора?

    • Тогда можно было бы сделать вырезаемую точку и не фиксированной, а вариабельной: если формула, то там хоть константа, хоть ссылка на другой канал (куда при смене частоты будет писаться индекс), хоть вычисление.
    • И, естественно, туннелировать и {r,d} (эх!!!).

      Прям хоть вводи вызов SetChanAllRDs(), чтоб он указывал МНОГО {r,d}...

    03.04.2021@вечер, 21:20, после добавления "@~:": перечитал сейчас хотелки этого пункта, и стало очевидно, что

    • надо использовать СТАНДАРТНЫЙ синтаксис -- квадратные скобки; т.е., "ИМЯ_КАНАЛА[ИНДЕКС]".

      Соответственно, если дело дойдёт до "вырезки", то "ИМЯ_КАНАЛА[НАЧАЛО-КОНЕЦ]" или "ИМЯ_КАНАЛА[НАЧАЛО,ДЛИНА]"

    • Как вариант -- менее красивый/привычный, но, возможно, более легкопарсимый -- указывать не суффиксом, а ПРЕФИКСОМ: @[ИНДЕКС].

      А "вырезки" -- аналогично, "@[НАЧАЛО-КОНЕЦ]" или "@[НАЧАЛО,ДЛИНА]".

    • Ну да -- ТАКОЕ уж точно придётся квотить от shell'а, тут уж ничего не попишешь.

    Выглядит сие вполне реализуемо (в обоих вариантах, хотя второй проще), но делать такое стоит лишь в случае возникновения РЕАЛЬНОЙ потребности, а не "просто чтоб было, до кучи" -- ибо штука всё же нетривиальная, влекущая лишнее усложнение кода.

  • 20.04.2018: желательно бы в cdaclient и das-experiment мочь указывать не только каналы, но и формулы.

    20.04.2018: анализ ситуации:

    • Во всех 3 утилитах используется одинаковая схема действий:
      1. Сначала список парсится, поштучно в RememberChannel(), при помощи ParseDatarefSpec(), результаты которого сохраняются в slotarray из refrec_t.
      2. Регистрация делается по списку запомненных, также поштучно в итераторе ActivateChannel(), ...
      3. ...у которого в самом начале стоит cda_add_chan().
    • Напрашивается решение:
      1. Сделать, чтоб ParseDatarefSpec() умела распознавать также и формулы.
      2. В refrec_t добавить поле srctype, куда б прописывался тип.
      3. А ActivateChannel() будет выполнять нужный тип вызова для регистрации -- в точности, как Cdr_treeproc.c::cvt2ref().

        А можно прямо и в console_cda_util'е сразу сделать функцию, выполняющую это (и заодно сообщение об ошибке выводящую в зависимости от типа). Тогда можно будет при желании и varchan'ами расширить.

    • Главный вопрос: КАК указывать тип ссылки (chn/fla/var)?

      В Cdr'е формула указывается префиксом '#', но тут такое не прокатит -- это начало комментария.

      Если только анализом содержимого (что cvt2ref() тоже делает -- проверяя классы символов (уж пробел в формуле наверняка будет, и она автоматом заклассифицируется как IS_FLA). 20.04.2018: неа, нельзя -- это в Cdr просто имена каналов, а в cdaclient'е могут указываться "КАНАЛ=ЗНАЧЕНИЕ" и "КАНАЛ ЗНАЧЕНИЕ" (команды записи). Так что ТОЛЬКО с явным спецификатором типа.

      Вариант: была ж мысль (16-10-2017) иметь возможность указывать "PSP-спецификации широкого профиля -- ОПЦИЯ=ЗНАЧЕНИЕ..."; так вот -- сделать прямо опцию srctype, могущую принимать значения chn,fla,var?

    20.04.2018: кстати:

    • Раз уж речь пошла о "записи" -- а можно ль извратиться так, чтобы позволять указывать "формулы записи" -- т.е., и формула, и значение, которое подать ей на вход?

      Даже если синтаксически как-то и удастся, то это несовместимо с моделью "ждём подтверждения записи путём прихода UPDATE".

  • 03.04.2021: по аналогии с bridge_drv сделана возможность затребовывать обновления не по обновлению, а не чаще раза в цикл. Это как бы контр-мера к введённому 09-03-2017 "ВСЕГДА ON_UPDATE".

    Сделано тем же префиксом @~:; реализация оказалась очень проста -- в ParseDatarefSpec() делается

    options &=~CDA_DATAREF_OPT_ON_UPDATE

    Проверено -- работает.

    И заодно сразу описано в help'е; заодно и @!: описано.

  • 15.11.2024: за вчера-сегодня сделана хаковатая поддержка CXDTYPE_UNKNOWN -- флагом "@?" вместо спецификатора типа "@x:".

    15.11.2024: исходные размышления см. в разделе по cda_d_epics.c за вчера и сегодня (ради тестирования поддержки UNKNOWN в нём и заморочился).

    Собственно реализация -- почти целиком в console_cda_util.[ch]:

    1. Указание флага:
      • Добавлена константа UTIL_FLAG_USE_UNKNOWN, ...
      • ...а в util_refrec_t -- поле util_flags для её хранения.
      • И в ParseDatarefSpec() взведение этого флага по '?'.

      ЗАМЕЧАНИЕ: количество элементов берётся от "основного" типа (и обычно это 1), но для CXDTYPE_UNKNOWN оно означает число байт; поэтому указывать нужно что-то вроде "@?d8:" -- тогда будет "x8", что как раз объём DOUBLE.

    2. Корректная работа с потенциально изменчивыми типами приходящих данных в PrintDatarefData():
      1. dtype:
        • В качестве типа значения теперь используется результат не cda_dtype_of_ref(), cda_current_dtype_of_ref(), ...
        • ...в т.ч. и вместо urp->dtype (при печати префикса типа);
        • для чего получение от cda перетащено в самое начало.
      2. dpyfmt:
        • Вместо urp->dpyfmt при печати значений используется свежевведённая локальная dpyfmt, ...
        • ...которая при совпадении полученного от cda значения dtype уставляется в urp->dpyfmt, ...
        • ...а при НЕсовпадении -- в "%g" для REPR_FLOAT и в "%d" иначе.

          16.11.2024: добавлены доп.условия -- "%u" для беззнаковых и "%lld" и "%llu" для INT64 и UINT64. Кстати, при генерации формата по умолчанию беззнаковость в расчёт не принимается.

    3. В cdaclient.c единственное изменение -- что в ActivateChannel() при взведённом UTIL_FLAG_USE_UNKNOWN (и не-wr_req!) канал регистрируется с типом CXDTYPE_UNKNOWN вместо ur.dtype.
    4. Причём СПЕЦИАЛЬНО он никак не документирован в help'е.
dataset-save: dataset-restore:
  • 13.05.2015: давно (в Снежинске?) задуманные утилиты для сохранения и восстановления режимов, на основе cda-доступа к каналам, а не в рамках Cdr/Chl.

    Создаём.

    06.12.2017: кстати, давно ведь ясно, что никакого dataset-restore не нужно, т.к. с этой ролью прекрасно справляется cdaclient.

  • 13.05.2015: dataset-save.c -- создана клонированием cdaclient, удалением из него "лишнего" и выносом записи (точнее, печати) в отдельную функцию, вызываемую в конце, ПОСЛЕ sl_main_loop()'а.

    На вид пашет.

    Вопрос -- как поступать с недоступными каналами:

    1. С неразрезолвившимися?
    2. С неприконнекченными?
    3. С необновившимися?

    Во всех случаях -- писать в файл с префиксом '#'?

pzframe2text:
  • 02.08.2016: поскольку никакого отдельного cx-bigc теперь нет, а выдавать осциллограммы в пригодном для всяких gnuplot'ов виде по-прежнему надо, то требуется отдельная утилита для подобных задач.

    По ключам нужна будет максимальная совместимость с cdaclient, и пользоваться будет услугами console_cda_util'а.

    Пока делать не будем (не до того; как Роговский попросит -- тогда и сподобимся), но раздел застолбим.

cx-chan-search:
  • 06.12.2017: утилитка для выполнения UDP-запроса "найти канал" из командной строки, через интерфейс cx_srch().

    В простом первоначальном варианте -- просто для тестирования этого самого интерфейса.

    А потом можно будет и добавить функционала:

    • Чтоб указывать несколько имён каналов.
    • Возможность превращать полученный IP-адрес в имя (ключ -N) или не делать этого (ключ -n).
    • Указывабельность таймаута ключом -T SECONDS.
    • Возможность задавать значения param1 и param2 из командной строки.

      Вот тут вопрос об именах ключей: напрашиваются -1 и -2, но первый уже занят в cdaclient под "do NOT expand {} and <> in channel names".

    • А еще отдельно нужен будет ключик для указания "когда получишь первый ответ на имя, то удаляй это имя из списка запрошенных", либо наоборот "печатай всё" (-a?).
  • 06.12.2017: изготавливаем простейший вариант, пока чисто для тестирования функционала в cxlib'е.

    06.12.2017: сделал, взяв основу с cxclient.c.

    • Начинаю проверять -- не работает. Просто ничего и всё тут. И сервер, судя по отладочной печати, пакета не получает.
    • Потом дошло -- файрволл iptables!!! Там не стояло разрешение на 8012/udp. Добавил.
    • Аллилуйя! -- работает! Клиент получил ответ.
    • И, похоже, что при зафильтрованном iptables'ом порте программа Wireshark не очень-то помощник; по крайней мере, если и клиент и сервер оба на одном узле. Т.к., она показывала исходящие пакеты, а вот ответные, когда они уже появились (после правки таблицы) -- почему-то нет, пока не была перезапущена -- тогда стала показывать.

    11.01.2018: добавлен резолвинг IP-адреса в имя.

    1. По умолчанию резолвится, а отключается это интуитивным ключиком '-n' (Numeric, No-resolve).
    2. Собственно резолвинг делается прямо в evproc()'е. С очевидным "если облом (hp==NULL), то печатаем просто IP".

      С резолвингом всё не так очевидно. История получилась длинная -- почти на два дня.

      • Сходу не-зная/не-помня, как это делать, решил подсмотреть в готовом месте -- конкретно в утилите arp.
        • Она находится в пакете net-tools, конкретно для CentOS-7.3 это net-tools-2.0-0.17.20131004git.el7.src.rpm
        • После продирания через тамошнюю специфику (в т.ч. ключевое слово "sprint") искомое -- конкретно для IPv4 -- обнаружилось в lib/inet.c::INET_rresolve().

        Там делается gethostbyaddr().

      • Потом, еще раз просмотрев cxlib_client.c::cx_open(), решил сэкономить и воспользоваться более простым gethostbyname(): всего один параметр -- имя, могущее также быть и IP-адресом.
      • Сделано, пробуем -- фиг!!! Возвращает почему-то тот же IP-адрес.
      • Переделал-таки на gethostbyaddr() (почти по образцу net-tools, но с in_addr_t вместо u_int32_t) -- помогло.
      • Причина такой странности не вполне ясна. Так всё ж понятно!
        • В man'е прямо сказано:
                                           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.
          
        • Да и в первой же нагугленной по "gethostbyname ip-address" ссылке на Stack Overflow -- "How to detect your IP/name using gethostbyname()" -- сказано примерно то же самое и рекомендуется использовать gethostbyaddr() (точнее, вообще getaddrinfo(), традиционно).

    Засим начальный вариант считаем за "done".

  • 11.01.2018@лыжи-после-обеда, начало 2-й 2-ки: полный вариант должен:
    1. Поддерживать асинхронный режим работы (LIBCX_ASYNC вместо LIBCX_SYNC).
    2. Воспринимать НЕСКОЛЬКО имён каналов.
    3. При получении ответа проверять, касается ли ответ кого-то из запрошенных, и если "да", то после печати удалять того из списка.
    4. ...но также замечать получение дополнительных ответов на уже отвеченные (дубли) и сообщать об этом.
    5. Иметь ключ -T DURATION -- в течение какого времени ждать ответа.

    Очевидно, что для реализации пунктов 2-4 нужно поддерживать список запрошенных, аналогично cdaclient'овским refrecs.

    Но также очевидно, что всё это сейчас делать НАФИГ НЕ НАДО! Только когда (если) появится потребность.

    12.01.2018: вдогонку:

    1. Еще, помнится, была мысль иметь ключики для указания значений param1 и param2.

      И конкретно "-1" и "-2" (самые очевидные!) выглядят не очень хорошими претендентами, т.к. в cdaclient ключ -1 используется для "do NOT expand {} and <> in channel names".

    2. А надо ли в дополнение к ключу -T DURATION также ввести возможность затребовывания периодического пере-спрашивания (например, -P PERIOD)? Раз уж утилита готова висеть долго, то смысла это делать, условно, дольше 1 секунды без пере-запрашивания просто нет.
  • 15.05.2021: некоторые мысли и действия по результатам вчерашнего использования, когда понадобилось узнать, в каком сервере живёт конкретный канал (k500.bpm.e2v2.6pic4.c), а оказалось, что такое имя есть в двух сразу.

    Так вот: утилитка-то ПОКАЗАЛА оба ответа, но это чисто по везению -- что второй успел придти за то время, пока печатался первый, а 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.

pipe2cda:
  • 09.09.2019: создаём раздел для утилитки.

    3 пункта ниже изначально были в разделе о cdaclient, в них поясняется смысл утилиты -- мочь принимать данные для отправки с stdin'а.

    Насчёт названия были разные соображения: "cda-pipe-send" (длинно и начало перекрывается с "cdaclient" -- Tab использовать неудобно), "pipe-send" (неконкретно), "pipe-send-chan" (но ведь не один канал, а могут быть разные!), "pipe-send-data" (мутно как-то). В конце концов в голову пришло короткое и однозначное "pipe2cda".

  • 05.09.2019@утро-дома: надо бы как-нибудь сделать cdaclient годным для передачи (серверу) данных ОЧЕНЬ БОЛЬШИХ объёмов.
    • Из командной строки -- не прокатит, вследствие ограничения (128 килобайт, кажется), да и неудобно это для передачи из программ/скриптов.
    • Синтаксиса для чтения из файла -- тоже пока нету, а хотелось бы, в т.ч. чтоб прямо с stdin'а умело считывать.
    • И вариант с "-f FILENAME" не особо годный -- в ReadFromFile() используется line[1000].
    • (уже в ИЯФе) ...отдельная проблема будет в том, что парсинг в console_cda_util.c рассчитан на поток байт не из IO-stream'а, а уже из памяти.

      Использовать fdiolib в режиме FDIO_STRING? Оно как раз и даст готовый поток уже в памяти.

    ...вот если бы что-нибудь вроде принятого со времён pkzip "@file" (включая "@-"), но конкретно такой синтаксис не прокатит, поскольку CHANNAME=@SOMETHING является вполне валидной спецификацией, где передаётся прямо значение "@SOMETHING".

    (Побудительный мотив -- желание уметь передавать cdaclient'ом картинки из беркаевской утилитки tvcapture.)

    15.10.2019: утилита сделана, проверена, "done".

  • 05.09.2019@утро-дома: а ещё бы чтоб cdaclient умел бы работать в "потоковом" режиме (точнее, в режиме трубы-"исполнителя команд"): будучи ЕДИНОЖДЫ запущенным, принимал бы с stdin'а команды на запись.

    Смысл -- что разные экземпляры cdaclient, запущенные через system()/popen(), могут, вследствие разных причин, не соблюдать порядок; один же экземпляр программы этот порядок гарантированно будет поддерживать.

    ...но это, видимо, как раз НЕ cdaclient (поскольку он-то слопывает список СНАЧАЛА, а работает с ним, готовым, ПОТОМ.

    Изготовить отдельную утилиту (cda-send?), которая бы читала команды со stdin, и тут же бы их исполняла?

    05.09.2019@дорога-на-обед, около ГУП УЭВ: не, не так -- cda-pipe-send!

    И понятно, что если понадобится -- слабать эту штуку можно за несколько часов (из готовых кусков от cdaclient'а, дополнив FDIO_STRING-потоком.

    05.09.2019: но будет отдельная тонкость в том, что использоваться в основном будет ОДИН И ТОТ ЖЕ канал, а cdaclient рассчитан на работу с РАЗНЫМИ, и аллокирование там памяти под каждый из них. И если поступить "по-простому", то каждая отправка будет отжирать буфер.

    Надо:

    1. либо делать free() буфера (а тогда и dataref'а бы тоже), ...
    2. либо проверять каждый очередной канал-строчку, не совпадает ли он с предыдущим.

      Но аллокирование выполняет не ParseDatarefSpec(), а уже ParseDatarefVal().

    Однако ситуация чуть хитрее: аллокирование в последнем не просто так по прихоти, а потому, что именно в значении может быть префикс [КОЛИЧЕСТВО]. Отсюда напрашивается решение:

    1. Имя-то проверять, совпадает или нет, и если "нет" (лучше -- что отпарсенное в util_refrec_t по параметрам совпадает или нет), то "выкидывать" старое (либо можно и в SLOTARRAY'е хранить).
    2. А ParseDatarefVal() "научить" проверять "текущее состояние" (значение поля databuf_allocd) на годность для текущей спецификации, и если мало -- то до-аллокировать.

    15.10.2019: и потоковый режим сделан (и надлежащий менеджмент буферов), так что "done".

  • 07.09.2019@дома: а ещё бы уметь слать БИНАРНЫЕ данные -- хоть от программы, хоть <-редиректом из файла.

    Напрашивается синтаксис "-B ИМЯ_КАНАЛА", а на stdin'е -- данные. И операция ОДНОРАЗОВАЯ -- указывается один-единственный канал, производится отправка, и потом утилита отваливает (хотя ключ -T будет полезен).

    ...и это, очевидно, уже в той "отдельной утилите".

    15.10.2019: и бинарный режим сделан (в т.ч. "многократный" с -C), и ограничение времени работы (только через -W).

  • 09.09.2019: приступаем к изготовлению.

    09.09.2019: основу берём от cdaclient.c.

    • Минимальный скелет сделан, компилируется и запускается.
    • Ключиков -- минимум, убрано всё, касающееся вывода (его же нет), оставлены только -d/-b, плюс -w и -T.
    • Причём конкретно -T уже работает.
    • Также добавлен -B -- "binary mode" (см. выше от 07-09-2019.
    • По последнему уже сделан if() выбора режима работы, но пока без наполнения.
    • ...хотя зародыши наполнения уже есть -- stdin_in_cb() (cxscheduler-based) для режима бинарных данных и ProcessFdioEvent() (fdiolib-based) для обычного.

    09.09.2019@вечер-дорога-домой, около ИПА: было бы удобно, если бы режим "-B" позволял бы МНОЖЕСТВЕННУЮ запись -- т.е., один раз установили соединение, а потом сбагриваем бинарные блоки по N байт, а софтина их отправляет.

    Но тут на пути реализации есть несколько проблем:

    1. Никаких средств "синхронизации" и проверки нет, поэтому малейшая ошибка пишущего (хоть на 1 байт) приведёт к необнаружимому и неустранимому сбою в данных.
    2. Сложность в ПОСЛЕДНЕЙ записи: как понять, EOF после последнего "полного" блока данных -- это именно просто EOF, или же это блок данных нулевого размера?

      ...можно, конечно, считать, что:

      1. Если была хоть одна успешная запись, то блок размера 0 ВСЕГДА считать за EOF и не отправлять.
      2. И вообще, отправлять ТОЛЬКО блоки того размера, который указан в спецификации "-B @Tnnn:КАНАЛ", а всё, что меньше -- считать ошибкой и отбрасывать.
    3. Категорически не будет работать отправка блоков данных РАЗНОГО размера -- из-за отсутствия средств "синхронизации" (читай -- возможности по-посылочного указания размера).

    Резюме:

    1. Если нужны какие-то хитрые запросы, то придётся использовать обычную по-строчную отсылку с текстовым форматом спецификации.

      А бинарный формат -- для одноразовых отправок.

    2. В самом-самом крайнем случае, если уж очень сильно припрёт -- можно сделать ограниченный вариант, умеющий слать блоки данных строго ОДНОГО размера, указываемого при старте.

    10.09.2019@утро-дома: а ведь очевидно, что таймаут -- который -T... -- надо считать не с момента запуска, а с момента ПОЛУЧЕНИЯ ПОСЛЕДНЕЙ ПОРЦИИ ДАННЫХ ДЛЯ ОТПРАВКИ. Т.е.,

    • Для обычного текстового потока -- с момента EOF.
    • Для бинарного режима -- с момента окончания вычитывания данных для отправки.

    После чего даже идея: а не переименовать ли в этом случае ключ -T в -W ("Wait time")?

    12.09.2019: ...далее...

    • Да, ключ -T переименован в -W, а его функционал (заказ таймаута) вынесен в отдельную SetWaitLimit(), которую надо вызывать по EOF.
    • Анализ кода парсинга в ParseDatarefSpec() показал, что она по ошибке просто делает exit().

      Это правильное поведение для ранее созданных утилит, где список каналов создаётся в самом начале, и ошибку надо тут же "пресекать", но едва ли годится для долгоиграющей утилиты.

      Надо вводить параметр "флаги", где указывать "не вылетать"?

    • @вечер, дорога домой, около ВЦ: Как всё-таки ввести "множественный" режим? То ли ключ "-M" (Multiple) в дополнение к "-B", то ли просто "-P" (Pipe) ВМЕСТО него.

    13.09.2019@утро-дома, зарядка (только проснулся): некоторые мысли/соображения:

    • А зачем вообще для "-B" использовать чтение через cxscheduler? Можно ж воспользоваться fdiolib'ом -- заказать пакеты фиксированного размера.
    • Собственно, смысл в том, что при "ручном" чтении мы его делаем сразу в буфер, который потом используем и для отправки данных. А в случае с fdiolib придётся делать копию.

      ...хотя никто не мешает проверять, что если в момент FDIO_R_DATA уже можно выполнять запись, то делать её прямо из inpkt, а если ещё нельзя -- тогда делать копию (для которой аллокировать буфер).

    • Вот только надо будет с остатком (когда вычитывается меньше заказанного и EOF) обращаться соответственно.
    • Отдельное соображение: функционирование в режиме "-B" сильно отличается от остального -- и менеджить список не нужно, и парсить имя прямо сразу, считая ошибки фатальными.

      Вот надо прямо сейчас именно его и сделать, а на не-"-B" печатать сообщение с извинениями "упс, пока не реализовано".

    13.09.2019: детально посмотрел схему работы fdiolib -- фиг, "пакеты фиксированного размера" там указываются как len_size=0, и считываются как "заголовок фиксированного размера без данных".

    А при этом "EOF раньше времени" рассматривается как ошибка -- пакет клиенту не доставляется вовсе, просто сразу возникает FDIO_R_CLOSED.

    Так что идея "воспользоваться fdiolib'ом -- заказать пакеты фиксированного размера" обламывается -- придётся всё-таки читать вручную.

    Что ж, тогда продолжаем "пилить" реализацию варианта "-B" в прежнем направлении.

    • Так, подумавши: а ведь для "множественного" варианта нужно будет ДВА комплекта буферов, и для самостоятельного чтения (с посредничеством cxscheduler'а) оный второй комплект придётся аллокировать самостоятельно.

      Смысл -- в том, что при множественной отправке надо разделять буфер, из которого будет выполняться очередная отправка, и тот, в который в настоящий момент ведётся чтение. Иначе получится, что начало буфера содержит кусок "новых" данных, а следующая часть -- кусок "старых".

    16.09.2019: ползём далее.

    • Написан чуть более полный help по вариантам вызова:
      1. Перечислены все 3 варианта вызова с краткими комментариями.
      2. Дана ссылка "(for full CHANNEL syntax see 'cdaclient -hh')".
    • В процессе вышеупомянутого написания утвердился с названием ключика для "множественного бинарного режима" -- -C (Continuous).

    18.09.2019: ещё чуток:

    • Замечание чуть задним числом: все переменные, относящиеся к бинарному режиму, имеют префикс "bin_".
    • В stdin_in_cb() сделан блок чтения, со всеми проверками на тему результата (<0, ==0, else). Логика взята из fdiolib'овского StreamReadyForRead(), и даже имя "bin_reqreadsize" оттуда же.

      В т.ч. с учётом будущего режима "-C" -- отдельный облом по EOF (а для "-B" он считается просто окончанием данных).

      НЕ сделана реакция на всякие EOF и ошибки.

      Также осталось сделать собственно реакцию на "все данные пришли".

    • В main()'е:
      • Перевод STDIN'а в неблокирующийся режим.
      • Аллокирование буфера -- bin_ur.databuf.
    • Код отправки в виде PerformWrite() взят из cdaclient'а, с минимальными убавлениями.

    19.09.2019: ...

    • Добавлена регистрация evproc'а после регистрации канала, маска событий -- только RSLVSTAT и UPDATE.
    • ProcessDatarefEvent() слизывается с cdaclient'овского, но с отличиями:
      • По RSLVSTAT просто печатается сообщение об ошибке (и делается exit() в бинарном режиме).
      • По UPDATE логика чуть изменена, поскольку возможны МНОЖЕСТВЕННЫЕ команды на запись в один канал (пока отправлялась одна, из pipe'а могла быть считана уже следующая).

        А именно -- обработка флагов wr_snt и wr_req поменяна местами, и они теперь не if()/else(), а просто два if() друг за дружкой (ради того порядок и изменён).

    • Отдельный идеологическо-технологический вопрос: надо же как-то помнить, приходил ли уже из канала результат (и, соответственно, можно ли в него слать данные) или же ещё нет; как -- не заводить же лишнее поле?

      Идея проста -- использовать имеющееся rd_req: сначала взводить его в =1, а по первому же приходу данных сбрасывать.

      Так и делаем:

      • В самом начале взводится rd_req=1.
      • По UPDATE сбрасывается rd_req=1.
      • В чтении из STDIN'а по получении пачки для отправки сама отправка вызывается только при rd_req==0.
    • И ещё отдельный вопрос: схема wr_req+wr_snt была создана длля ОДИНОЧНЫХ запросов на запись, поэтому флаги булевские.

      А что, если такой сценарий:

      1. Появляется запрос на запись, делается wr_req=1.
      2. Запрос отправляется, делается wr_req=0 и wr_snt=1.
      3. Появляется ещё один запрос на запись: поскольку rd_req==0, то можно слать сразу же; следовательно, делается wr_req=1, затем тут же wr_req=0 и wr_snt=1 (а оно и так уже было ==1!).

        Итого -- у нас в этот момент отправлено ДВА запроса на запись, но пометка "wr_snt=1" имеется в единственном экземпляре.

      4. Приходит "ответ" на предыдущий запрос (сделанный в п.2.), и делается wr_snt=0.

        Тем самым считается, что ВСЕ отправленные запросы на запись уже отработаны! Хотя в реальности запрос из п.3 мог ещё даже не успеть уйти в сеть.

      Итого:

      • В п.4 получаем ситуацию, когда запрос считается выполненным, хотя физически он мог ещё даже не успеть уйти.
      • При ПРОДОЛЖИТЕЛЬНОЙ работе -- это не проблема: ну отработается запрос чуть позже, да и ладно.
      • Но вот если запрос в п.4 был ПОСЛЕДНИМ, и уже EOF -- то он по факту будет потерян.

      Что делать?

      1. Сходу вроде напрашивается идея переделать булевские "флаги" в считающие: делать не =1 и =0, а ++ и --.

        Но тут вмешивается фактор "неопределённости выполнения запросов на запись": если отправить две штуки подряд (после ещё одного, "нулевого"), то с высокой вероятностью они будут в самом сервере склеены в один, через next_wr_val, и "ответ" придёт всего ОДИН. И программа тогда зависнет очень надолго (навеки?) в ожидании ответа, которого никогда и не будет.

      2. Так что лучше использовать другой вариант -- "локальное упорядочивание": если предыдущий запрос ещё не отработался (т.е., wr_snt!=0), то новый просто не слать, а оставить висеть флаг wr_req=1, и по приходу следующего ответа запись будет выполнена автоматически.

      Так и делаем: по получению данных для записи PerformWrite() вызывается только при wr_snt==0.

    • Введён флаг EOF_rcvd, взводящийся как по реальному EOF, так и при бинарном-однократном режиме по вычитыванию всей ожидаышейся пачки байт.

      А в ProcessDatarefEvent() добавлена проверка на него (что всё исполнено и пора отваливать) -- пока хаковатая, с проверкой на бинарность и отсутствие отправленности запроса.

    • Мелкие доделки на завтра (сегодня просто не успел):
      1. Надо б перевести утилиту с util_refrec_t на refrec_t -- чтоб в текстовом режиме было годно.
      2. Остаётся ещё вставить вызовы SetWaitLimit() в точки после окончания получения данных.

    Проверено -- бинарный режим более-менее работоспособен. Результаты:

    • "-B" работает стабильно.
    • "-C" -- надо добавить второй буфер, чтобы читалось не туда, откуда будет идти отправка.

      И в какой-то момент производить копирование в отправной (видимо, по окончанию чтения очередной порции).

    • Отдельный вопрос по "-C": как относиться к МНОЖЕСТВЕННЫМ пачкам данных, приходящим за время round-trip'а очередного запроса записи?
      • Сейчас -- они все сливаются (ровно как делалось бы и в сервере, если б тот не успевал обрабатывать запись сразу по приходу (в тесте, с noop -- успевает).
      • Но не надо ли сделать так, чтобы РЕАЛЬНО отправлялись бы ВСЕ?
      • И если "да", то как? Не заводить же FIFO запросов -- он махом переполнится.

        Отключать приём данных на время обработки запроса?

        Но тогда может возникнуть крайне нежелательная ситуация, что программа-отправитель (например, телекамерная -- там-то объёмы большие) заблокируется на записи, поскольку программа-читатель pipe2cda не будет вычитывать свой входной буфер.

      • Как вариант -- сделать это поведение настраиваемым при помощи какого-нибудь ключика.

      Ох, мутновато... Пока оставим как есть, а если припрёт ("надо передавать ВСЕ!"), то придётся ключ вводить и махинировать маской (то 0, то SL_RD).

    20.09.2019: последовательность соображений на тему 2-го буфера для текстового режима (почти "поток мыслей" в разных точках пространства и времени):

    • 19.09.2019@верер, дорожка перед парковкой за 20-м зданием: второй буфер понадобится и для каналов в текстовом режиме, а не только для бинарного.

      Ну и что, надо делать функцию "activate second buffer"?

    • 20.09.2019@утро-дома: а вот и нет -- НЕ понадобится: ведь чтение идёт совсем иначе, fdiolib'ом в егойный буфер, а тут мы получаем уже сразу готовую пачку данных, которую НЕ требуется парсить в несколько приёмов.
    • 20.09.2019@утро-лестница, выходя на работу: а вот и ДА -- понадобится! Пусть и из готового текстового буфера, но парсинг идёт постепенно, элемент-за-элементом, и может обломиться из-за какой-нибудь синтаксической ошибки, а тогда в буфере опять окажется "частичное значение" (начало от нового, конец от старого).

    Вывод: надо переходить на refrec_t, в котором завести поле вроде "buf2".

    • 20.09.2019@пляж, ~14:20: а с четвёртой стороны -- зачем КАЖДОМУ-то по буферу?! Достаточно ОДНОГО НА ВСЕХ, включая и бинарный режим.

      Пара соображений по менеджменту этого буфера с точки зрения аллокирования/роста:

      1. В дополнение к самому буферу и его текущему объёму -- иметь также "максимальный обломившийся объём", и если текущего объёма мало и нужно аллокировать больше -- то пытаться, и выдавать ругательство только в случае, если нынешняя хотелка ещё больше "максимального обломившегося".

        @на работе, ~16:00: а почему именно "максимальный" и "если ещё больше"? А не в обратную сторону?

      2. Для бинарного режима ошибка аллокирования является фатальной: если уж в момент необходимости буфер не дали, то всё -- кирдык.

        ...ну разве что перейти в режим "побайтного вычитывания" до остатка текущего объёма, в надежде, что к следующему пакету текущая запись отработается и можно будет читать уже в основной буфер?

    20.09.2019@пляж, ~14:10, дорожка вниз от парковки-с-бочкой к пляжу: зря ломал голову и сокрушался потенциальной сложности -- напрашивается простейший алгоритм, бОльшая часть которого идёт в ProcessFdioEvent():

    • Получив уведомление FDIO_R_DATA, первым делом выпарсиваем оттуда имя канала.
    • Затем ищем по RecSlot'array'ю, есть ли там уже канал с таким именем.

      На всякий случай -- ради EPICS и подобных -- сравнение можно делать case-sensitive.

    • Если уже есть -- то просто идём далее, а если нету -- то заводим новую ячейку.

      Замечание: канал регистрируем с флагом PRIVATE, чтобы он не перекрылся с аналогичным каналом, совпадающим по имени с точностью до регистра (case) -- см. "на всякий случай" к предыдущему пункту.

    • Отдельное замечание: воизбежание постоянной печати одного и того же сообщения об ошибке в случае облома cda_add_chan()'а -- можно такие обломившиеся ячейки не освобождать, а оставлять там ref=CDA_DATAREF_ERROR.

      Тогда они будут считаться "имеющимися", так что повторных попыток регистрации более не будет, но функционировать никак не станут.

      Это актуально для случаев, когда из программы-юзера передаётся ошибочное имя канала.

    • Затем, уже имея ячейку для данного имени канала:
      1. Проверяем, если ref==CDA_DATAREF_ERROR -- просто отваливаем.
      2. Иначе пытаемся выпарсить данные.

      Буфер для парсинга выбираем либо прямо внутренний (если wr_req==0), либо в дополнительный.

    • При недоступности буфера -- просто отваливаем; при ошибке парсинга -- опять же отваливаем (а сообщение должно было быть напечатано ранее, самом кодом парсинга).
    • Если всё ОК -- то:
      1. Если wr_req!=0, то копируем из второго буфера в основной, заменяя имеющееся там (и не забываем также обновить n_items!).
      2. Иначе же взводим wr_req=1 и, если можно отправить сразу (rd_req==0 и wr_snt==0) -- отправляем.

    Всё, достаточно просто и линейно, никакого особого менеджмента/учёта ячеек-каналов не требуется.

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

    20.09.2019: собственно дела:

    • Копируем из cdaclient.c кусок с определением refrec_t и менеджментом их SLOTARRAY'я.
    • Заменяем "util_refrec_t bin_ur" на "refrec_t bin_rr". Плюс повсеместно работа переведена на refrec_t.
    • Также добавлено взведение таймаута при указанном "-W" -- вызовы SetWaitLimit() поставлены после всех 3 штук sl_del_fd().

      ...проверено -- он работает (по крайней мере, в случаях 2 и 3 (т.е., по реальному EOF и по вычитыванию всего ожидавшегося при "-B")).

    24.09.2019: а теперь наконец-то реализуем текстовый режим по описанному 4 дня назад сценарию:

    • Для использования многочисленными глубинными функциями введена global_argv0=argv[0].
    • ProcessFdioEvent() набита функционалом:
      • В самом начале -- анализ кода причины: по любой, кроме R_DATA, чтение STDIN прекращается; плюс, если не-R_CLOSED (который считается нормальной ситуацией), то выдаётся сообщение об ошибке.
      • ...кстати, "прекращение чтения STDIN" выполнено одним общим куском в конце, куда делается goto из всех таких мест. И там также вызывается SetWaitLimit().

    25.09.2019@утро-душ: к вопросу о переделке многих функций console_cda_util, дабы по ошибке не отваливали, а возвращали бы -1:

    • Была мысль понадобавлять дополнительный булевский параметр "не делать exit".
    • А есть вариант проще: ввести глобальную переменную "не делать exit" (уставляемую функцией-аксессором), чтобы конкретно pipe2cda.c её бы взводил.
    • В любом случае, сам код "если делать, то вызвать exit(EC_USR_ERR), а иначе return -1" надо не дублировать в десятках мест, а запхать в макрос, который во все эти места и вставить (эта мысль давняя :-)).

    27.09.2019: эх!

    • Не-отваливание в console_cda_util: да, по идее от 25-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.
      • Затем добавлено аллокирование databuf'а.
      • Весь кусок парсинга значения...
      • ...в котором львиную долю составляет возня с buf2 -- аллокирование (если раньше не было) и использование.

        Фактически, есть ДВЕ ветки кода: короткая -- когда парсинг делается в основной буфер (val2wr или databuf) и длинная -- через buf2.

      • Ну и в конце -- собственно запись (с проверкой на возможность).

    Вот вроде и всё.

    30.09.2019: проверяем, теперь уже всё оптом.

    • Для начала: а чё это в режиме "-C" НЕ происходит завершения по концу файла, если файлом работает pipe (echo ... | pipe2cda)?

      ...из НАСТОЯЩЕГО файла -- происходит. В чём разница?

      Поразбирался -- и из пайпа тоже работает, всё дело в конкретном пайпе.

      • Не работает конкретно
        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
        
      • А без sleep --
        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
        
        -- почему-то всё ОК.
      • Но даже
        (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
        
        -- т.е., с ЯВНЫМ завершением исходной цепочки команд, после чего должен придти EOF, оно НЕ завершается.

      Т.е., ВОЗМОЖНО -- проблема в каких-то аспектах работы shell'а, и конкретно zsh.

      НО:

      1. В варианте запуска не интерактивно, а как "/bin/zsh -c '...'" (т.е., где job control отключен) -- то же самое.
      2. И даже с bash -- "/bin/bash -c '...' -- тоже НЕ завершается.
      3. С другой стороны, если вместо "pipe2cda ..." поставить "cat" -- то всё идёт как надо.

      Окей, вставляем отладочную печать, и... BINGO!!! Проблема действительно в коде: последний read() возвращает 0, но программа почему-то не отваливает.

      Разбираемся:

      • Печать показывает, что при отсутствии "sleep" сразу же выполняется событие "A" ("все данные пачки пришли, можно попробовать записать") и сразу же событие "W" -- PerformWrite().

        А при НАЛИЧИИ sleep -- после r==0 почему-то НЕ всегда отваливает.

        ...ещё после нескольких тестов: неа, иногда и при отсутствии sleep НЕ отваливает, а уж при наличии -- не отваливает практически ВСЕГДА.

      • Догадка-вывод 1: при отсутствии sleep все входные данные прилетают очень быстро, ещё ДО того, как установлено соединение с сервером.

        Поэтому далее всё работает почти как в режиме "-B" -- все данные есть, EOF_rcvd, так что выполняется короткая последовательность "получили первый CURVAL/UPDATE, отправили запись, получили второй CURVAL/UPDATE, и раз всё -- то отвалили".

      • Результат-вывод 2: да, действительно -- проверка 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).

      После чего проблема исчезла.

    • @обед-Гуси: очевидно, что пора уже доделать нормальный эккаунтинг. Правила напрашиваются такие:
      1. Отваливать в ProcessDatarefEvent(), при "EOF_rcvd && num2write == 0".
      2. num2write++ делать при переходе у refrec'а дуплета (wr_req,wr_snt) из состояния (0,0) в любое иное.
      3. num2write-- -- соответственно, при переходе В состояние (0,0) из любого другого.

      Правила выглядят простыми.

    • @дорога после обеда и Добрянки, вдоль чётной стороны Инженерной, около Технопарка: а теоретически можно выпендриться и реализовать даже МНОЖЕСТВЕННЫЕ бинарные каналы:
      • Читать можно из РАЗНЫХ файловых дескрипторов.
      • Для чего потребуется синтаксис вроде "NNN:CHANNEL-TO_WRITE", где NNN -- номер дескриптора.

        Но тут будет некоторая засада с тем, что нужно корректно ДВАЖДЫ указывать номера дескрипторов: для редиректа (NNN<FILENAME и NNN:CHANNEL...). Хотя при использовании из другой программы (для чего и требуется) -- вроде не проблема.

      • @ИЯФ: ещё вариант -- указывать прямо имена файлов, чтобы программа сама бы их открывала. Учитывая возможные символы в именах файлов, единственным осмысленным вариантом видится "CHANNEL-TO-WRITE=FILENAME".

        Такое позволит использовать zsh'ную (а ныне и bash'евскую) фичу "process substitution" -- "<(COMMANDs), когда можно будет указывать CHANNEL-TO-WRITE=<(COMMANDs), и shell сам заменит "<(COMMANDs)" на /proc/self/fd/NNN.

      (Мысль пришла в голову, когда обдумывал перспективы возможного будущего развития ProcessDatarefEvent() -- ведь там refrec от бинарного режима отрабатывается чуть отдельно, НЕ берясь из ptr2lint(privptr2). А если бы понадобились МНОЖЕСТВЕННЫЕ бинарные ссылки -- то тогда надо будет и бинарные тоже менеджить в SLOTARRAY'е.)

      Только практического смысла в таком финте -- примерно ноль.

    • Итак: реализуем эккаунтинг из пред-предыдущего пункта.

      Проверяем -- вроде всё работает. ПОЧТИ...

    • ...текстовый режим на EOF (в виде Ctrl+D с клавы) не реагирует.

      Проблема оказалась идентичной утренней: в ProcessFdioEvent() в блоке STOP_READING_STDIN отсутствовала проверка "если более нет неисполненных запросов на запись, то можно отвалить".

      После добавления проверки -- проблема исчезла.

    • При тестах обнаружилась некоторая неприятность: не совсем "корректно" работает EOF/Ctrl+D в режиме "-B". Конкретно -- если при "@t1:..." сразу нажать Ctrl+D, то оно НЕ отваливает.

      Причина -- технологическая: попытка записать 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".

cx-bridge:
  • 29.01.2020: создаём раздел.
  • 29.01.2020: похоже, назревает потребность в ещё одной консольной утилитке -- для обеспечения бриджинга между несколькими СУ.

    Побудительный мотив: на vepp4-pult6 в очередной раз переполнился /var/tmp/, потому что карнаевский скрипт V3V4_PV2CX с дикой частотой вызывает cdaclient (точнее, он это делает 1 раз в 1 секунду, но с большим количеством каналов). Работа этого скрипта -- перекладывать данные из EPICS в CX. Но ведь такой подход -- раз в секунду читать из одного и писать в другое -- совершенно дик!

    Учитывая наличие в cda поддержки протокола epics::, можно иметь просто бинарную программу, которая бы мониторировала каналы на одной стороне и при обновлении дублировала бы оные на другую.

    29.01.2020@утро, ~12:10, по дороге через студгородок к родителям: некоторые соображения по деталям реализации:

    • Очевидно, каналы должны указываться парами.
    • Пары эти имеет смысл указывать в файле, по одной паре (через пробел -- "откуда куда") на строку, а утилите в командной строке указывать имя файла (или МНОГО имён).
    • Очевидно также, что имеет смысл мочь указывать РАЗНЫЕ defpfx'ы (baseref'ы) для каналов "откуда" и "куда".

      Вечером: соответственно, и контекстов тоже иметь надо будет 2.

    • Для гибкости можно разрешить указывать множественные имена -- с расширителями {A,B,C,...} и <X-Y>.

      И ключ "-1" чтоб традицилнно отключал бы такое расширение.

      Только одна проблема: поскольку указываются ПАРЫ, то надо бы как-то и в правой части уметь делать соответствующую подстановку. Ну и как, спрашивается? 30.01.2020: учитывая, что расширители "{}" и "<>" НЕ бывают вложенными, напрашивается простая схема с backreference'ами $1, $2 и т.д. (причём хватит и односимвольных до $9, а $10 вряд ли потребуется). Но тут, вероятно, понадобится соответствующее сотрудничество от ExpandName()+do_one_stage() -- получать эти backreference'ы; и это будет весьма нетривиально, при нынешней модели работы расширения.

    • Как альтернатива/дополнение: можно читать через ppf4td -- по умолчанию plaintext, но можно указывать схемы m4:: и cpp::.
    • Касательно dtype+nelems каналов: очевидно, нужно использовать их указание от канала-источника для ОБОИХ.

      Но вот прочие флаги (вроде NO_RD_CONV), если они указаны -- должны быть индивидуальны для каждого из пары.

    • Понятно, что для обычного мирроринга нужно ОДНОСТОРОННЕЕ отображение -- копирование данных из источника ("левый" канал) в приёмник ("правый" канал).

      Но стоит упомянуть 2 нюанса:

      1. Может оказаться полезным использовать один и тот же список в РАЗНЫХ направлениях -- и "слева направо", и "справа налево". Ну мало ли -- имена похожи/совпадают в разных случаях, и можно обходиться просто разными baseref'ами.

        Для таких случаев полезно будет предусмотреть ключик -R -- "Reverse the mirroring direction".

        (Замечание: реверсироваться должно именно только направление; а вот правило "использовать dtype+nelems от канала слева" должно оставаться.)

        30.01.2020: да, мутно как-то и непонятно -- когда ж РЕАЛЬНО такое может понадобиться? :-)

      2. ВОЗМОЖНО, захочется всё-таки уметь делать двунаправленный бриджинг -- чтобы не только из канала слева изменения отображались в канал справа, но и наоборот.

        В общем случае это дело проблемное -- получится бесконечный пинг-понг. Но можно попробовать проверять, что если текущее известное значение в канале, куда хотим записать, СОВПАДАЕТ со значением, которое хотим записать -- то запись не делать. Это разорвёт цикл.

        (Хотя одновременно это срежет обновления, когда именно в исходном канале заново прописывается то же самое значение -- при периодических измерениях это совершенно нормально. Тут уж ничего не поделаешь, и единственным решением -- даже чисто по теории информации -- было бы уметь получать "идентификатор того, кто выполнил запись": тогда можно б было проверять, что если это МЫ -- то далее [обратную] запись выполнять не нужно. Но асинхронная модель CX подобного не позволит...)

        Так вот: для такого случая, видимо, будет ключ -2 -- "2-directional mirroring".

    • Полезен будет ключ "-v" -- verbose operation (print data events).
    • В качестве имени утилитки напрашивается cx-bridge.

    30.01.2020@утро, дорога на работу: и ещё пара мыслей:

    • @дом-лестница-вниз: а ведь раз нам надо перекладывать конкретно В cx'ные каналы, то правильнее это делать ПРЯМО ИЗ CX'а -- чтобы некий специальный бесканальный драйвер "смотрел" бы на сторонние каналы, мониторя их, и "перекладывал" бы получаемые значения в локальные каналы.

      При этом автоматически решается проблема "а какой dtype/nelems у канала?" -- он может брать прямо из свойств соответствующего аппаратного канала (да, тот для этого должен быть локальным).

      @уже на улице: cписки каналов ему указывать, очевидно, надо такими же файлами, ссылки на которые давать в auxinfo (да, списком). Ну и baseref'ы как-то там же указывать.

      @за-беркаевским-домом: имя драйверу можно дать "bridge_import" или "bridge_imp".

    • @тропинка в леске к НИПСу: а есть вариант ЕЩЁ ЛУЧШЕ: чтобы вместо noop'а прямо сам драйвер бы "смотрел" на сторонние каналы и ВОЗВРАЩАЛ бы получаемые значения.

      И уж тут драйвер бы просто проходился по списку своих каналов, получая их аппаратные свойства (как это делает 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 пока снижается (до тех пор, пока не потребуется бриджинг в обратную сторону).

:
xmclients:
Общее:
cxplore:
  • 19.07.2009: понадобится такая утилитка -- "Control System Explorer".

    19.07.2009: summary:

    • Смысл -- если наша СУ будет доступна широкой публике для загрузки и использования, то нужна тулза, которая позволила бы просмотреть всё, что подключено к некоему серверу; в том числе, текущие значения, и, возможно, даже покрутить что-нибудь.
    • Как это может быть реализовано: да стандартно, a-la Explorer -- окошко из 2 панелей; в левой -- дерево всего, подключенного к серверу, в правой -- содержимое "текущей ветви".
    • Дополнительная фича -- для каждого устройства можно попросить запустить специфичную-для-него диагностическую программу.

      Или -- реализовывать диагностические программы .subsys-панелями, чтобы эта самая cxplore могла бы сама подгружать их по мере надобности?

    • И -- а не интегрировать ли туда же remote console для управления сервером? Тогда эта программа превратится в "Центр управления СУ".
pult:
  • 03.08.2015: софтина сделана давно, а сейчас переведена из programs/tests/, где создавалась, в xmclients/.
  • 31.05.2016: не хватает поддержки ключа -readonly, имевшейся в v2.

    Hint: CdrSetSubsystemRO().

    09.03.2017: сделано, в рамках перевода pult.c::main() на схему с PK_nnn (для возможности указывать winopt-параметры в командной строке).

  • 02.08.2016: некрасиво, что pzframeclient.c является копией stand.c, к которой добавлена пара строк -- #include "pzframeclient_knobset.h" и RegisterPzframeclientKnobset().

    Надо б чтоб использовался тот же самый исходный файл, чтоб он позволял добавлять те две штучки -D-ключами из командной строки/Makefile.

    02.08.2016: сделано:

    • define-символы:
      • SPECIFIC_KNOBSET_H_FILE -- имя файла; должно быть в двойных кавычках.
      • SPECIFIC_REGISTERKNOBSET_CODE -- код для регистрации. Произвольный код -- если это функция, то должно быть с "()"; символ ';' после него ставится автоматом.
    • В hw4cx/pzframes/Makefile добавлен код для симлинкования и указания пары -D.

    Всё OK.

  • 01.03.2017: желательно бы -- для всяких per-device-скринов -- иметь возможность указывать параметры (options) и в командной строке тоже (в дополнение к тем, что в .subsys-файле в 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: делаем.

    1. datatreeP.h: добавлен тип секции DSTN_ARGVWINOPT (="argv_win_opt").
    2. Chl_app.c:
      • Добавлен флажок appopts_t.forbid_argv_params и PSP-ключ для него.
      • После парсинга win_options добавлен условный -- при forbid_argv_params==0 -- парсинг всех DSTN_ARGVWINOPT-секций, с флагом PSP_MF_NOINIT.

        Ошибки парсинга так же фатальны, как и у основных win_options.

    3. pult.c -- самое сложное: пришлось кардинально менять архитектуру работы с argv[] и "точку" загрузки группировки.
      • Введена толпа "типов параметров" -- PK_SWITCH, PK_SUBSYS, PK_PARAM, PK_BASE, PK_FILE.
      • Парсинг же сделан иначе:
        • У PK_SWITCH теперь абсолютный приоритет: если что-то начинается на '-', то это ключ.
        • Прочее определение сделано не "статично"-независимо, а: если sysname еще не указано, то параметр считается за sysname. Т.е., работают конструкции вроде
          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) с новым стилем парсинга параметров.
    • По-хорошему, эти модификации надо бы и в stand.c отразить.

    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} -- делаем и загрузку указанного в командной строке файла режима.

    • ...не то что б особо требовалось, но уж пусть будет -- для законченности.
    • Сделано тривиально, хотя в реальной работе пока не проверено (а фигли -- чему там не работать, раз собственно загрузка делается в Cdr и оттестирована).
    • Хотя и вылез забавно-странный прикол: если указать вместо файла директорию, то оно не ругается, а просто ничего не делает.

      Как показала проверка простейшей тестовой программкой (она тут ниже закомментарена), таково поведение fopen() -- он позволяет открыть и директорию, хотя потом первый же fgets()==NULL.

stand:
  • 25.09.2014: пришла в голову идея, как сделать универсальную программу "локальный стенд" -- для всяких вещей типа сварки, "tantal" etc.: надо к pult добавить интегрированный сервер, чтоб в командной строке указывать пару "конфиг сервера плюс экран".

    26.09.2014: еще некоторые мысли для "презентации-обоснования":

    Дополнительные бонусы модульности:

    • То, что во многих СУ именуется "display manager'ом", в CX является библиотекой, и на его основе можно делать свои специализированные программы.
    • А добавив в программу еще и CX-сервер -- также являющийся модульной библиотекой" -- мы получаем законченную программу для автоматизации малых стендов, умеющую работать с любой управляющей электроникой.

    Чуть косноязычно, конечно -- надо улучшать, но смысл ясен.

    01.10.2014: ...если хочется иметь в программе CX-сервер, то он должен бы включать в себя функционал не только libcxsd.a, но и специфичности из programs/server/, вроде cxsd_config.[ch].

    Следовательно,

    • Придётся утащить всё содержимое programs/server/ (кроме собственно cxsd.c и cxsd_builtins.c) в еще одну библиотеку.
    • Держать её -- наверное, там же в lib/srv/.
    • Назвать -- видимо, libcxsd_server.a?

    Мда...

    22.10.2014: если хорошенько подумать -- а ЗАЧЕМ тащить сюда всю обвязку сервера? Ведь:

    • Frontend'ы не нужны. Следовательно, и instance тоже.
    • Логгинг не нужен (всё в консоль).

      ...хотя тут понадобится поддержка от cxlogger+cxsd_logger, чтоб позволяли логгинг ТОЛЬКО на консоль, без файлов вовсе.

    • Демонизация не нужна.
    • Перехват сигналов не нужен (раз нет ни франтендов, ни pid-файла).
    • Единственное реально нужное из конфига -- это параметры работы ядра cxsd:
      1. Путь для загрузки драйверов (drivers-path).

        ...возможно, аналогично для libs и exts, когда они появятся (libs-path и exts-path).

      2. Числовые и булевские параметры -- basecyclesize, simulate, verbosity. Но это всё (сейчас) параметры командной строки.

    Парой часов позже: да, сделано, stand собирается и запускается.

    P.S. По факту это сейчас нечто вроде "insrvtest" :)

    22.10.2014@вечер-путь-домой: вообще всё немножко сложнее.

    • Для stand -- да, frontend'ы и прочее не нужны.
    • ...но для каких-то других случаев очень даже понадобятся -- при "подселении сервера к готовой GUI-программе".

      (Кстати, как там должно работать -- пока не совсем ясно. Программа общается с сервером по протоколу вроде local? Или что или как? 23.09.2015: вариант -- см. идею за сегодня в разделе cxsd_fe_cx.)

    • А во многих случаях и пути к загружабельностям (вроде драйверов) не нужны: можно эти загружабельности просто влинковать builtin'ами прямо в программу.
    • ...для чего -- давно такая мысль витает! -- надо б придумать технологию на уровне Makefile (аналогично mkr), чтобы указывались per-executable списки драйверов, протокольных модулей и прочего, что надо влинковать, а оно б само генерило файлец BINARY_builtins.c со всем надлежащим содержимым, включая 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: да, сделано:

    1. Frontend'ы:
      • LIBCXSD_PLUGINS уже всё равно давно влинковывалась, так что надо было только плагин добавить в список builtins_list[].
      • При указании в командной строке параметра ":N" делается CxsdActivateFrontends().

      ...парсинг командной строки, конечно, халтурный. Ничего и близко похожего на "проход по всем, с определением типа параметра".

    2. Ключ -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: ...неа, так просто не получится.

    1. Ведь предполагается внутри себя использовать протокол insrv::, а так -- при "обломе" -- надо будет ставить "cx::localhost:N".

      Но это-то решабельно -- можно ВСЕГДА ставить "cx::..." (потеря в скорости мало важна).

    2. И вторая проблема, похуже: ведь при исчезновении того экземпляра, который сервер, другие НЕ будут пытаться брать на себя его обязанности (в отличие от guru).
    3. ...ну и при обломе инициализации надо мочь грохать все запустившиеся frontend'ы. Но это уже вопрос техники.

      (Хотя возникает race condition при одновременном запуске пары программ -- могут обе обломиться частично, на НЕпересекающихся наборах frontend'ов.)

    07.12.2018: интересно, вот КАК тогда проверял работу ключей -M, -F, -E, -L -- да хотя бы первого? Ведь в вызове getopt() в параметре optstring ОТСУТСТВОВАЛИ "M:", "F:", "E:", "L:"...

    Ну чо -- добавлено. Но качество "тестирования", конечно, внушаить (c)...

  • 03.08.2015: программа переведена из programs/tests/, где она создавалась, в xmclients/.
  • 18.06.2019: ну была тогда сделана "поддержка" возможности указания путей для модулей -- а что толку?! Ведь кроме драйверов, никакие другие модули грузить нет физической возможности -- отсутствуют способы указать это.

    Надо бы всё-таки даже в stand иметь возможность читать config-файл, чтобы stand мог использоваться взаимозаменяемо с cxsd.

    Для чего нужно унифицировать config-читалку -- чтобы один и тот же код использовался в обеих программах.

  • 22.10.2019: как бы не совсем "по stand", но по родственной программе -- которая бы позволяла делать "интерактивно-конфигурируемый стенд для работы с конкретным модулем".

    Например, что-нибудь вроде "sktcanstand" -- софтина, которая бы позволяла сделать "стендик" для работы с любым поддерживаемым CAN-модулем через SocketCAN.

    22.10.2019: как бы предыстория:

    1. Ещё давным-давно -- даже до появления нынешнего stand.c -- была мысль "а вот как бы иметь такую софтинку, чтоб можно было на лету указывать, что мы хотим работать вот с таким-то девайсом, а она бы давала доступ к этому девайсу". Чтоб можно было использовать, как всякие Козаки и Котовы, которые имеют индивидуальные софтинки для диагностики/наладки каждого из типов своих девайсов.

      Очевидно, что для этого, при нашей клиент-серверной архитектуре, нужно, чтобы программа:

      • Включала бы в себя сервер.
      • Плюс скрин-клиента.
      • И "связывала" бы их по протоколу "insrv::".

      Вот из этих хотелок и родился в конечном итоге нынешний stand.c.

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

    2. На семинаре 10-10-2019 Петя Шатунов сказал -- "Дима, у тебя же наверняка есть драйверы для всего ИЯФовского железа! То есть тебе пара пустяков сделать стендик для, например, Попова" (это МОИМИ словами).

      Но тут уже явно напрашивается необходимость иметь такую софтинку, но обязательно с возможностью интерактивного указания адреса.

    Теперь собственно мысли:

    • Очевидно, что программка -- НЕ СОВСЕМ stand, поскольку конфиг надо не читать из файла, а генерить на лету (и "читать" уже из памяти -- но это схема "mem::").
    • В ней нужен интерфейс для указания адреса устройства.
    • ...а "тип устройства" -- ну фиг с ним, пусть из командной строки берёт.

    Так вот, речь об интерфейсе указания адреса. Мысли были всякие (типа просто поля в тулбаре), но наиболее простым и элегантным видится такой:

    • Адрес -- в виде 2 полей, LINE:KID -- указывается действительно в тулбаре.
    • Но во время "обычной работы" он НЕ менябелен (например, делается XmNeditable:=False).
    • А кроме адреса есть ещё кнопки [Run] и [Stop].
    • Так вот: адрес становится редактировабелен в режиме STOP, а нажатие на [Run] делает его опять нередактировабельным.
    • Схема реализации при этом очевидна:
      1. По Run -- поля ввода дисэйблятся, генерится конфиг сервера, по шаблону
        "dev MAIN %s@sktcankoz ~ %d,%d", typename, line_n, kid
        (да, .devtype-файлы нужно будет где-то добыть -- видимо, "слить вместе" в строку-БД ранее, а "конфиг" по шаблону вставить после них).

        И делается старт сервера.

      2. По Stop -- сервер "останавливается" (кстати, как? у нас это пока нигде никак не делалось), и поля становятся доступны для редактирования.
    • Отдельная задача -- как бы сделать, чтобы в тулбаре всегда было место под адрес и кнопки. Ведь, вроде бы, этим управляет скрин (который грузится из файла!).

      Но тут как раз всё просто: достаточно сразу после чтения скрина -- CdrLoadSubsystem() -- подменить значение секции "main",DSTN_WINOPTIONS.

      (Да, отдельный вопрос -- КАК подменять: готового единого API в Cdr нету, поскольку CdrCreateSection() сразу не глядя СОЗДАЁТ НОВУЮ. Но придумаем.)

    Ясно, что аналогичный функционал по идее потребен бы и для других "шин" и даже "подключений CAN", но там пока не очень ясно, как можно всё обобщить, а как конкретно для SocketCAN -- вполне ясно.

  • 01.04.2022: захотелось иметь возможность генерить КАСТОМНЫЕ бинарники-варианты stand'а. Пока ни "сборки из кубиков" нету, ни возможности "загрузить набор knobplugin'ов на лету" -- пусть будет хоть какой-то вариант.

    Потребность -- мочь сгенерить pzframeclient-аналог stand'а (с поддержкой всех осциллографов). Захотелось чисто из спортивного интереса -- смогу ли сделать "диагностический скрин" для работы с "контроллером Tornado", через EPICS (предполагается, что 1) будет использоваться devlist с bridge_drv; 2) сварганим fastadc-плагин "простейший осциллограф", адресующийся к 3 каналам: data, cur_numpts, numpts (этот чисто для удобства -- управление длиной выборки на лету)).

    02.04.2022: ну технически вроде несложно.

    • В stand.c скопирована из pult.c поддержка SPECIFIC_KNOBSET_H_FILE и SPECIFIC_REGISTERKNOBSET_CODE.
    • В hw4cx/pzframes/:
      • Сама софтина получила название "pzframestand".
      • В Makefile:
        • бОльшая часть строк была скопирована из аналогичных для pzframeclient'а;
        • а конкретно pzframeclient_knobset.[ch] использованы готовые -- НЕ делается отдельного "pzframestand_knobset'а".
        • Также для него пришлось добавить указание зависимости от $(LIBCDA_D_INSRV) $(LIBCXSD_PLUGINS) $(LIBCXSD) -- скопировал строчку из 4cx/src/programs/xmclients/Makefile.

    Проверил в простейшем варианте -- вроде бы работает, так что "done".

histplot:
  • 21.05.2018: утилитка-то начата давно, еще 3 года назад (и вся её нехитрая функциональность описывалась и делалась в разделе о MotifKnobs_histplot), но есть желание добавить функциональности, так что создаём ей свой раздел.
  • 21.05.2018: отсутствует сохранение в файл -- точнее, реакция на кнопку [Save]. А ведь несложно же!

    26.05.2018: сделано -- делов-то.

  • 21.05.2018: а еще было б классно, если б она умела ЧИТАТЬ из файлов, как это умеют fastadc'шные скрины. Это позволило бы:
    1. Просматривать сохранённые графики (а то gnuplot зело неудобен).
    2. Возможно, просматривать также файлы, сделанные das-experiment'ом.

    21.05.2018: очевидно, что времена будут подписываться не вполне корректно: сам-то компонент histplot предполагает, что имеет дело с равномерным массивом, а при чтении из файла это не гарантируется:

    • Для файлов от самого histplot'а будет верно только при указании hist_period=, совпадающего с исходным (да и то времена будут "назад от текущего", а не исходные).
    • Для файлов от das-experiment -- вообще никак, т.к. там выдача не периодическая, а по триггеру.

    Но на это проще забить -- особой необходимости тут нет, больше важна просто возможность просмотра. 23.05.2018: читать из файла!!! Подробнее -- ниже за сегодня.

    22.05.2018@утро-дорога-на-работу-около-ИПА: в случае загрузки файла можно (нужно!) вообще никуда не коннектиться.

    Вечером: судя по тексту histplot.c, для этого достаточно в конце main() -- перед XhShowWindow() -- НЕ делать CdrRealize и не заказывать таймаут на сдвиг истории.

    22.05.2018@утро-дорога-на-работу-около-ИПА/ИЦиГ: кстати, а как насчёт самодостаточности файлов? Чтоб прямо В НИХ были и dpyfmt, и hist_period?

    Теоретически, можно:

    • Записывать префикс "%dpyfmt:" перед каналами в строке заголовка.
    • А hist_period как-нить дополнительным комментарием (хотя это уже откровенно нарушит стройность файла).

      В общем, с этим пока просто хбз :)

      26.05.2018: впрочем, с учётом чтения из файла прямо меток времени, hist_period становится просто неактуальным -- так что проблема исчезает.

    • 24.05.2018@утро-дорога-на-работу-около-вивария-ИЦиГ: да, а "механизм" обращения внимания на строку заголовков такой: если первый символ '#' и это самая первая строка, то пытаемся парсить; а если не-первая -- просто пропускаем всё до её конца.

    Вечером: а еще бы чтоб можно было указывать ТОЛЬКО файл, а имена каналов (и, следовательно, их количество) чтоб само брало из файла.

    26.05.2018: а еще диапазоны отображения -- при неуказывании каналов в командной строке им взяться будет совсем неоткуда...

    23.05.2018@утро-дома: насчёт "времена будут подписываться не вполне корректно": а ведь можно и времена тоже вычитывать из файла: 1-я колонка -- прямо в формате timeval'овых tv_sec.tv_usec!

    Чуть подробнее:

    • Прямо в MotifKnobs_histplot_t завести дополнительное поле --
      struct timeval *timestamps_ring;
      int             timestamps_ring_used;
      
      • Реально, конечно, никакое это не кольцо -- т.к. будет бывать только в случае загрузки из файла, когда всё линейно без закольцовки.
      • Поле количества -- предохранитель. В нормальной ситуации при чтении из файла timestamp'ы должны б быть на все строчки, где есть данные, но правила качественного программирования диктуют НЕ полагаться на такую корректность и предохраняться ВСЕГДА.
    • При загрузке из файла этот массив также аллокируется -- длиной histring_len.

      Замечание: и тогда нужно прямо в histplot.c прописывать конкретное число, а не полагаться на Cdr'ное умолчание "0=>86400".

    • И туда сохраняются значения из 1-й колонки файла.
    • А уж MotifKnobs_histplot.c пусть смотрит, что если timestamps_ring!=NULL, то вместо своего вычисления времён для подписей к горизонтальным осям пусть берёт готовые.
    • И, кстати, так автоматом решится проблема, что обычно отображаются относительные времена (минус-расстояние до "сейчас"), которые в случае показа файла бессмысленны совсем.

      Т.к. показываться будут прямо сразу АБСОЛЮТНЫЕ времена!

    С идеологической точки зрения это, конечно, халтура -- как и сама загрузка данных самой программой вместо Cdr'а: нарушается инкапсуляция. Но результат будет достигнут, и весьма занедорого.

    23.05.2018@утро-дома: а еще отдельный вопрос -- КАК указывать файл?

    • Идеальный вариант -- просто именем. Но не всегда можно отличить имя файла от имени канала.

      Видимо, придётся использовать эвристику, как в PzframeMain(). В частности, если первый символ '%' -- то наверняка канал; если присутствует '/' -- вероятно, имя файла.

    • Также можно подстраховаться, сделав ключ -f ИМЯ_ФАЙЛА.
    • И еще ОЧЕНЬ бы желательно мочь брать данные с stdin -- чтоб .gz'нутые файлы можно было читать.
      • Оное указывать именем "-" -- его считать за имя файла и указанным просто, и через "-f-" или "-f -".
      • А открывать -- например, делая вместо fp=fopen() просто fp=stdin.

    25.05.2018: да, но решили ж парсить через ppf4td -- а как ему сбагривать stdin? (Ну, кроме linux-specific /dev/stdin?)

    28.05.2018: с другой стороны, сейчас нигде кроме Linux оно и не надо, так что пусть будет "-"=>/dev/stdin.

    24.05.2018:@утро-дорога-на-работу-около-вивария-ИЦиГ:

    • Да, надо делать так, чтоб можно было указывать ТОЛЬКО файл, не указывая никакие каналы.

      Видимо, надо делать 2 варианта вызова:

      1. Указаны каналы -- работаем в "живом" режиме.
      2. Указан файл -- работаем в режиме "показываем загруженное".

      Вариант "указаны и каналы, и файл" -- выглядит странно.

      И да, надо из строки заголовков брать и имена, и (при указанности) %DPYFMT.

    • А файл бы желательно читать через ppf4td, а не fgets()'ом -- чтобы избежать проблем с длинными строками, не вмещающимися в выделенный буфер (который обычно всего [1000]).

      Это чуть более сложно, но зато позволит читать ЛЮБЫЕ подобные файлы (26.05.2018: хотя кто еще, кроме CX'ных утилит, сгенерит такое -- с 3 timestamp'ами в начале каждой строки... Даже у fastadc формат иной...).

    • И как-то надо будет мочь иметь дело с не-числовыми значениями: если вдруг среди столбцов встретится строковый (das-experiment легко может его выдать), то аккуратненько пропускать такое, сохраняя в ячейку просто NaN.

      В простейшем варианте -- в случае невозможности парсинга (ЛЮБОЙ невозможности!) пропускать до первого whitespace.

      26.05.2018: более продвинутый вариант -- эмулировать ppf4td_get_string(), выкидывая выпарсиваемое "в никуда": понимая парные кавычки (внутри них могут быть пробелы!) и забэкслэшимости.

      26.05.2018: эмулировать -- плохо: дублирование кода, да и рассинхронизоваться можно. А что, если в ppf4td_get_string() добавить флаг "выкидывай результаты, никуда не складывая, и пофиг на длину"? 30.05.2018: сделан, начинаем пользоваться.

    26.05.2018: приступаем.

    • Сделано определение указанности имени файла -- покамест простейшее, еще без PK_nnnn-эвристики.
    • При указанности файла объект создаётся с флагом MOTIFKNOBS_HISTPLOT_FLAG_VIEW_ONLY.
    • "Активация" (Realize) сделана альтернативной: делается лишь при неуказанности файла, а иначе -- нет; альтернативой активации является как раз загрузка.

    28.05.2018: далее.

    • Сделана PK_nnnn-эвристика (определение типа скомбинировано из pult.c и pzframe_main.c).
    • Чтение файла -- LoadPlot(). Скелет взят от cxsd_config.c::ReadConfig().

    28.05.2018@вечер-дорога-домой-около-ИПА: если когда-нибудь захочется сделать в v4'шном Cdr логгинг, то пусть он по формату совпадает. Это позволит показывать такие log-файлы histplot'ом же (неудобно будет, из-за множественных начал, но всё же).

    29.05.2018@утро-подъезд-выходя-на-работу: а еще можно выпендриться и указывать в командной строке, КАКИЕ каналы из файла брать -- из каких колонок.

    • Например, если параметр начинается с ',', то это номер канала/колонки.

      Вопрос -- откуда считать? Колонками, так что 1-й канал будет ",4", или прямо каналами, чтоб ",1" означало "4-я колонка"?

    • Тогда код чтения должег быть устроен совсем иначе: не последовательно заглатывать, а перебирать ВСЕ поля строки, и если номер колонки совпадает с указанным для какого-то канала plot'а, то брать.

    ...хотя это уже 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.
    • Итого -- что-то как-то начало парситься (число точек уже соответствует :D).

    04.06.2018@утро-дорога-на-работу-около-ИПА: насчёт "как указывать метку и диапазоны отображения": а использовать символ ',' в качестве префикса и разделителя.

    Например,

    ,label:abc,disprange:MIN-MAX,...

    Некоторые замечания:

    • Формат по-прежнему будет указываться префиксом %DPYFMT: -- для совместимости с консольными утилитами (хотя и вариант ",dpyfmt:%DPYFMT," тоже бы прокатил).
    • Первая мысль была использовать символ присваивания '=', но ':' совпадает с Cdr'ным.

      К тому же, и с синтаксисом консольных утилит такое почти не конфликтует: '=' там используется для указания, что это запись; а вот двоеточие парсится уже среди флагов, и парсинг не пересечётся никак.

      ...хотя -- всё это небесспорно (т.к. ':' удобно б использовать и как терминатор опции).

    • Символ ',', конечно, выглядит не сильно красиво и очевидно, но он совершенно бесконфликтен -- даже shell'у по барабану.
    • ...а вот в качестве суффикса -- типа ИМЯ_КАНАЛА,label:nnn -- использовать нельзя: в самом имени канала могут быть запятые.
    • Можно постараться сделать так, чтобы толпа ','-префиксов была PSP-парсимой прямо вся скопом (указывать ',' сепаратором? сходу не катит, но надо думать...).
    • Поскольку парсится "спецификация канала" из командной строки и из заголовка файла одинаково -- AddGroupingChan()'ом, то синтаксис будет универсален.

    04.06.2018: расширяем успех.

    • Исправлено несколько мелких косячков и недоделок.
    • После загрузки данных нужно вызвать MotifKnobs_UpdateHistplotGraph() -- чтоб учлось количество точек в историях: изначально-то, при добавлении -- MotifKnobs_AddToHistplot() -- их по 0 штук, поэтому скроллбар "не работал".
    • Направление данных: сохраняются-то они "задом наперёд" -- чтоб в файле следовать хронологически, а читать так же "задом наперёд" мы не можем -- количество точек заранее неизвестно.
      • Можно, конечно, выдрёпываться с histring_start -- при каждом чтении смещая его, тем самым де-факто читая задом наперёд.

        Но это извратно и неудобно; к тому же, "timestamps_ring_start" нету -- там подобная фигня не предусмотрена.

      • Проще зеркалить все буфера после загрузки.

        Сделано.

    • 04.06.2018: Вопрос: а откуда в выводе das-experiment'а сможет взяться disprange? Или всё-таки надо иметь возможность указывать список каналов в командной строке, но данные чтоб грузились из файла? (Ну щас реально так и сделано.)

    06.06.2018:

    • РАССУЖДЕНИЯ О СИНТАКСИСЕ -- вместо ',' использовать просто ОПЦИЯ=ЗНАЧЕНИЕ, чтоб суммарный синтаксис указания каналов был бы {%DPYFMT:|option=value:}NAME. Фиг с ней, с "совместимостью" с cdaclient -- другой тут смысл параметров (нету никакой записи).

      А от histplot-wide параметров как отличать? По наличию в списке оных?

    • PK_PARAM-параметры надо переделывать на PSP.

    14.06.2018: поскольку косяк в MotifKnobs_histplot.c найден и исправлен, можно вернуться сюда.

    о-PSP'шиваем.

    • Еще на прошлой неделе введён тип "конфигурация" -- globopts_t, экземпляр его globopts и соответствующая text2globopts[].
    • Сейчас же всё переведено на использование globopts.
    • ...а потом старый код сменен на psp_parse()'нье.

    16.06.2018: сделана возможность указания параметров канала -- метки и диапазона отображения.

    1. Во-первых, собственно psp_parse()'нье параметров.
      • Разделители -- separators -- указываются ",".
      • Терминаторы -- terminators -- ":".
      • Смысл такого разделения в том, что
        1. ':' может быть валидным началом имени канала (точнее, ссылки на канал), так что указывать двоеточие просто в списке separators нельзя -- сожраны будут все.
        2. Да и завершать же парсенье опций и переходить к собственно имени как-то надо.
      • Соответственно, парсится всё в цикле while(1), индивидуально-пошагово:
        1. Сначала пропускаются все ',' (заодно чтоб можно было с них начинать спецификацию).
        2. Затем проверяется, соответствует ли текущая точка парсинга формату "LETTERS=". Если нет -- на этом парсинг параметров завершается и оставшееся считается уже ссылкой на канал.
        3. А коль соответствует -- то выполняется парсинг.
        4. ...после которого отдельно проверяется, что если текущий символ ':', то он пропускается, ОДНА штука.

        Следствие:

        • Параметры можно разделять как запятыми, так и двоеточиями (одиночными). Т.е., спецификации
          label=123,disprange=-10-+70:chanref
          и
          label=123:disprange=-10-+70:chanref
          эквивалентны.
        • А вот вариант
          label=123,disprange=-10-+70,chanref
          не прокатит: вторая ',' будет опознана и пропущена PSP, он попытается парсить дальше (терминатора-то нету!) и ругнётся "Unrecognized parameter 'chanref'".
      • Замечание: этот while(1) идёт ПОСЛЕ парсинга "%DPYFMT:", так что НЕЛЬЗЯ перемешивать DPYFMT и параметры (в отличие от cdaclient'ного синтаксиса).
    2. Парсинг метки -- тривиален. И если метка указана, то не делается умолчательная генерация из имени канала.
    3. А вот диапазон отображения -- сложнее.
      • Парсинг выполняется PSP-плагином (ещё бы!) RangePluginParser().
      • И если диапазон указан -- т.е., MIN<MAX, то выполняется аллокирование 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: увы, не всё так радужно. Почему-то загрузка из файла работает кривовато.

    1. Иногда, в некоторых строках -- хбз, почему, но видна какая-то кратность -- в качестве первого числа берётся не 4-й столбец (т.е., первый с данными), а 3-й (т.е., количество секунд с начала файла).
    2. Также есть лишняя строка графика: очевидно, она появляется в тех самых строках, где значение первого канала берётся из секунд -- в результате там на конце строки появляется "лишнее" число. (А если список каналов НЕ указан в командной строке, то у строк с данными есть "право" дополнять список, даже если ранее была строка заголовков.)

    18.06.2018: разобрались:

    • Причина косяка была в ppf4td_get_int(): он воспринимает числа вида "068" (миллисекунды) как восьмеричное (начинается с '0'), а в оных никакой '8' быть не может.
    • Вот и оказывалось, что число миллисекунд -- "06", а "8" считалась уже следующим числом (и да, программе пофиг на отсутствие там пробелов).
    • ...и да -- все косячные строки были с начальным ноликом в числе миллисекунд, так и обратил внимание (интуиция какая-то, осенило ж :)).

    Труды праведные:

    • ppf4td_get_int() подпилен (см. в его разделе за сегодня), и в вызовах указывается defbase=10.
    • Сделана возможность указывать параметр "units" -- его теперь сохраняет MotifKnobs_SaveHistplotData().

    19.06.2018: продолжение трудов:

    • Переделано, что метка создаётся не в k->label, а в k->ident.

      Причина в том, что MotifKnobs_SaveHistplotData() сохраняет в файл именно ident, а для показывания в метке в окне plot'а годится не только label, но и (если последний пуст) ident.

    • Добавлена выдача краткого help'а по ключу -h.
    • Еще вчера пришла в голову мысль: а можно ж сделать авто-скейлинг для каналов, которым не указаны диапазоны -- при загрузке данных искать минимумы и максимумы.

      Делаем:

      • Первым делом: в AddGroupingChan() аллокирование параметров переделано из условного (если disprange указан) на безусловное.

        Поскольку вначале делается bzero(), то по умолчанию границы будут [0.0,0.0].

        20.01.2019: а вот и нифига! Подробнее см. ниже раздельчик за сегодня.

      • Есть тонкость: в данных могут быть NAN'ы -- как в конце графиков (если какого-то из каналов записано меньше, чем других), так и в середине (но вот данные такие пришли).

        А у NAN есть свойство, что оно возвращает false при любом "упорядочивающем" сравнении с ним -- например, и "NaN<=0", и "NaN>=0" вернут false.

        Поэтому НЕЛЬЗЯ искать минимум и максимум стандартным алгоритмом

        for (minv = maxv = a[0], x = 1;  x > count; x++)
        {
            if (minval > a[x]) minval = a[x];
            if (maxval < a[x]) maxval = a[x];
        }
        
        -- т.е., беря в качестве начальных значений 0-й элемент, и потом исча значения меньше и больше его. Т.к. даже 0-й может оказаться NaN'ом и отравить весь поиск.
      • Поэтому поиск идёт в 2 этапа:
        • Сначала ищется первый не-NaN.
        • Если оный найден (т.е., индекс поиска меньше длины массива), то далее идёт уже вышеприведённый алгоритм, только не с 0-го элемента, а с того, где найден не-NaN.

          Кроме того, при сравнениях элементы NaN игнорируются. Это как бы не очень обязательно -- устройство сравнений таково, что NaN'ы через них не пройдут (вследствие вышеупомянутого false при сравнениях), но так спокойнее.

      Работает.

    23.06.2018: и снова хренушки! Загруженный график почему-то показывается горизонтально-зеркально.

    Лишнее зеркаленье? Неправильно высчитывается координата в буфере? Или что еще?

    • И как это я не заметил зеркальности отображения, когда тестировал на perl-генеримом графике 0 ... 50 (код тут ниже закомментарен)! Ведь он должен был возрастать слева направо, а не справа налево...
    • Мда, не стоит мне графиками заниматься -- вечно какие-то сложности и о-о-очень долгие разбирательства, явно это не моё.

    24.06.2018: похоже, неправильно понял, как данные должны лежать в памяти. Cdr... ...

    • Как выяснилось анализом кода -- CdrShiftHistory():
      • Значения кладутся в буфер по возрастанию адресов: самое старое будет в [0], следующее в [1], и так далее, пока не завернёт по размеру буфера -- histring_len.
      • «Самая "старая" точка -- это histring_start.» (bigfile-0001.html от 16.12.2004.)
      • Т.е., пока не завернётся, самой старой точкой числится histring_start=0, а потом она потихоньку начинает закольцованно ехать в плюс:
        k->u.k.histring_start = (k->u.k.histring_start + 1) % k->u.k.histring_len;
      • Складируется же на расстоянии "плюс histring_used-1" от начала:
        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 не используют.
    • 25.06.2018: очевидно же, что первопричина проблемы -- в фигово подобранных именах: имя tt_index) используется в 2 разных смыслах:
      1. Расстояние от начала (нулевой точки, *ring_start).
      2. Сдвиг от текущего времени.

      Вот и вышла путаница -- видимо, в обоих случаях используется одинаковая арифметика, а должна зеркальная.

      ...т.е., опять та же проблема -- в некорректном выборе системы координат (только тут их несколько перемешано).

      А решение выглядит очевидным: надо РАЗДЕЛИТЬ имена:

      1. первое останется "временем" (t, t_index), а
      2. второе станет "возрастом" (age, age_index).

    26.06.2018: делаем.

    • Посмотрел внимательнее: так НЕТУ никакого "t_index"! Есть "x_index", что совершенно корректно -- это "расстояние от правого края", технически понятие эквивалентно "age_index".
    • А вот "t" -- да, используется сильно вразнобой. И в основном "не так" -- как "возраст":
      • В make_time_str() -- там это именно "расстояние от правого края", т.е., возраст; она и введена-то была как подмастерье для подписывания времени в координате x_index.
      • В DrawAxis() и DrawGraph() -- вообще всё поназапутано:
        • Часть в каких-то мутно-хитрых единицах: в "ЭКРАННЫХ координатах справа"; это тот же возраст, но в рамках экранного окна.

          Это в DrawGraph() при рисовании вертикальных линий сетки.

        • А прочее -- именно просто ВОЗРАСТ, т.к. расстояние от текущего момента: там сразу идёт счёт с учётом horz_offset.

        И всё это -- в переменной "t".

    • Выглядит так, что по минимуму надо просто укорректить обращения к timestamps_ring[], а потом можно и переименования исполнить.
    • Укоррекчено:
      1. Зеркаленье загруженных данных в histplot.c::LoadPlot() убрано.
      2. Теперь значение timestamp'а берётся с надлежащим "зеркалированием возраста в адрес", из ячейки
        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. Ну вот точно -- НЕ МОЁ это, графики рисовать. А сервер и с железом работать -- моё (даже ЕманоФедя хвалит, что даже конфиг сервера удобно устроен).

  • 22.05.2018@утро-дорога-на-работу-около-ИПА: не хватает возможности указывать формат отображения.

    Можно сделать как в консольных утилитах -- префикс %DPYFMT: (с той разницей, что поддерживать только вещественные).

    В районе обеда: ну да, при проектировании программы 12-10-2016 именно такое и предполагалось.

    26.05.2018@утро-конференц-зал-раб.сов.по-C-tau: а тут будет некоторая засада: ведь формат может быть и int, а не float/double. Но в таком случае надо бы как-нить проверять и форсить double (например, тупо %8.3f).

    26.05.2018: сделано.

    • Пока что прямо внутри парсинга списка argv[] в main(), но потом переедет в отдельную функцию -- когда будем делать парсинг строки заголовков.
    • "Форсение double" сделано чуть иначе: ВНАЧАЛЕ прописывается %8.3f, а потом, если указан "хороший" формат, то заменяется указанным, иначе же просто остаётся.

      И делается проверка, что если указан int-формат, то об этом выводится отдельное ругательство.

    16.06.2018: давно уже убедились, что всё работает -- "done".

  • 17.12.2018: (записано через месяц) при разбирательстве с глюками отображения графиков (сдвиг нуля на 1 пиксел в histplot'ах, подозрение падало на RESCALE_VALUE(), но потом дело оказалось в арифметике геометрии) было замечено, что почему-то одно из значений подсвечивается состоянием RED.

    19.01.2019@дома: да, ситуация ещё раз воспроизведена. Для воспроизведения надо:

    1. Запустить сервер с 3 симулируемыми каналами:
      ./sbin/cxsd -dc configs/cxsd.conf -f <(echo dev a noop w3i -) :1
    2. Натравить на них histplot:
      ~/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().

  • 02.05.2023: надо бы иметь возможность как-то указывать "включи для данной ручки флаг CDA_DATAREF_OPT_ON_UPDATE"; сейчас это просто НЕГДЕ указать, shame on me...

    А вот как бы это указывать -- фиг поймёшь:

    • @-префиксом -- нельзя, их парсит Cdr_treeproc.c::cvt2ref().
    • Возможность указывать префикс "/on_update:" (перед %DPYFMT или наравне с ним)? 19.05.2023: неа, нельзя -- '/' влечёт 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, то это прокатывает) с разными режимами обновления -- и показывался один лесенкой, а второй линейно!).

:
:
lib/:
misc:
Общее:
  • 04.07.2009: была уродская зависимость всех misc/*.o и useful/fdiolib.o от cx.h -- из-за потребности в 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'.)

  • 08.04.2015: замечено, что {n,uintr}_{read,write}() возвращают int, а должны бы ssize_t, т.к. на 64-битных платформах это не одно и то же.

    Потенциальных минусов от перехода на ssize_t не видно (на 32-битных платформах, вроде ppc860, ничего не изменится, т.к. ssize_t==int), а плюс -- в том, что будет более правильно и появится (гипотетическая) поддержка В/В более 2Гб за порцию.

    08.04.2015: сделано.

  • 11.01.2018: обнаружена функция-аналог нашей misc_macros.h'овской 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, тем самым гарантируя, что последний байт тронут не будет.

  • 15.08.2020: существенное изменение: определения для SleepBySelect() и str*time*() вытащены в отдельные файлы misc_sleepbyselect.h и misc_iso8601.h.
    • Сами .c'шники модулей теперь #include'ят только свои индивидуальные файлы, а не общий misclib.h.
    • А в начало misclib.h вставлены #include новорожденной парочки -- чтобы более никуда не пришлось бы вносить изменения.

    Смысл -- сделать эти модулёчки "самодостаточными" и годными для опубликования на GutHub и прочих подобных местах.

misc_multistrings:
  • 21.06.2007: а ведь пора бы уже переходить от сепараторов '\v' и '\f' к CX_US и CX_SS, как планировалось еще в проекте от 07-03-2006--12-03-2006.

    21.06.2007: сделано -- в misclib.h сменил определения MULTISTRING_SEPARATOR и MULTISTRING_OPTION_SEPARATOR на соответствующие ссылки на CX_??_C; заодно переделал их из #define'ов в enum. Что неприятно -- пришлось ради них за-#include'ить "cx.h".

    Заодно пришлось и misc_multistrings.c потрогать -- заменил там в комментарии '\v' на '\t'.

misc_printffmt:
memcasecmp:
timeval_utils:
paramstr_parser:
  • 18.06.2007: этот модуль поселен в misc/ взамен CXv2'шного useful/, поскольку он является "маленьким, самодостаточным и ни от кого более не зависящим модулем" -- точнее, зависит только от memcasecmp, т.е., только от libmisc же.
  • 19.06.2007: поскольку PSP умеет отводить malloc()'ом память для типа MSTRING, по-хорошему надо бы иметь отдельный вызов и для высвобождения результата такого аллокирования. Например, 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'ов при этом ничего не делает.

  • 19.08.2007: кстати, а ведь надо поддерживать соглашение "при rec==NULL производить только парсинг и проверку, но никуда не складывать".

    А пока что, похоже, это НЕ реализовано.

    11.03.2014: сделано 02-01-2014 (см. bigfile-0001.html).

  • 23.02.2017@~10:10-физио: не хватает всё-таки возможности ВПРЯМУЮ узнать, было ли указано значение для некоего параметра, или же это default. И в результате приходится выдрёпываться со всякими проверками -- типа >=0 в pzframe-драйверах или !=0 в CAN-драйверах.

    Непосредственная причина мыслей: у нас в CAN-драйверах очень криво сделана индивидуальная+групповая инициализация скоростей: "общая" указывается как [0], и размножаются в прочие, если те ==0. Но тут куча проблем/невозможностей: например, невозможно поставить все скорости в некое одно значение, а одну конкретную в =0.

    "Правильным" же вариантом была бы иметь возможность указать в paramdescr'е, что при установке надо взвести некий флажок (ОТДЕЛЬНЫЙ от значения!).

    Но теперь такая модификация уже едва ли возможна -- шибко уж велика installed base софта, не напрямую/статически линкующегося с PSP, а динамически; конкретно драйверы.

    23.02.2017: теоретически, есть никак не используемые поля rsrvd1 и rsrvd2.

    Можно было бы в rsrvd1 указывать offset, а в rsrvd2 -- размер в байтах (0 -- нет, 1,2,4 -- sizeof флага).

  • 19.03.2025: в 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().

    Но вот что делать с замусориванием лога сборки -- непонятно...

:
useful:
cxlogger:
cxnetacl:
cxscheduler:
  • 31.05.2007: надо, для легкопортабельности под Форточки, переходить с адресации в sl_???_fd*() по дескрипторам на адресацию по неким внутренним handle'ам. (В соответствии с соображениями из раздела "О работе с памятью" в bigfile-0001.html.)

    08.11.2012: да, и даже больше:

    1. Надо отказываться от client-supplied sl_timeout_t trec'ов в пользу аллокируемых cxscheduler'ом tid'ов.
      • Тогда радикально упростится и об-удобится работа с таймаутами -- в частности, можно будет использовать простой "GROWING", вместо "GROWFIXELEM".
      • И какого ж чёрта я в cxscheduler перенёс старый подход из cxsd_events/[ch], с этими дурацкими trec'ами?! Вследствие этой негибкости мышления и догматичности получена туча неудобств, вытекающих из получившейся кривой архитектуры...
    2. Ну а за компанию и адресацию файловых дескрипторов переделать -- чтоб при регистрации возвращался handle, и вся дальнейшая работа уже с ним.

    По факту это даст как бы "cxscheduler 2.0", не шибко-то совместимый по API. Но надо делать сейчас -- ДО реализации нового сервера (точнее, унифицированной lib/srv/, включающей rem), чтоб не тащить в будущее старые косяки.

    08.11.2012: некоторые соображения по реализации внутренней таблицы таймаутов:

    • Содержать она в себе будет структурки с примерно той информацией, что сейчас в sl_timeout_t: when, cb, privptr.
    • А вот "указатели" prev/next станут ИНДЕКСАМИ в этой таблице. -1 аналогичен NULL.
    • Поскольку операции добавления и удаления из списка таймаутов будут весьма частыми, то работать оно должно несколько иначе:
      1. Неиспользуемые ячейки должны не искаться каждый раз, а держаться в списке "неиспользуемых"...
      2. ...из которого может сразу браться первая же ячейка.
      3. Соответственно, обычный SLOTARRAY_GROWING тут бесполезен, а надо иметь отдельный менеджмент
        • Держим список неиспользуемых, просто однонаправленный. Связывание по тому же самому полю next.
        • "Добыча" -- берётся первая из списка, а вот если нету -- то таблица увеличивается на ALLOC_INC ячеек, и они помещаются в этот список, после чего опять берётся первая.
        • При "освобождении" можно также помещать в начало списка, для простоты.

    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: реализовываем:

    1. cxscheduler2.h -- новый API.
    2. fdiolib2.c -- реализация fdiolib'а под него (.h-файл тот же, ибо API не изменился). Отличие -- добавлено сохранение получаемого fd_handle и указание в дальнейших вызовах его вместо fd.
    3. cxscheduler2.c -- собственно новая реализация.
      • С файловыми дескрипторами -- пока что handle==fd (для простоты реализации), но уже с новым API.
      • Таймауты -- по вышеприведённому проекту от 08-11-2012, с двумя списками (упорядоченным по времени списком занятых плюс однонаправленным списком свободных).

      Вроде сделано -- и как теперь проверять будем? :-)

    4. Xh_cxscheduler2.c --
      • Файловые дескрипторы -- по факту уже была схема с handles, так что всё даже упростилось, за счёт избавления от поиска ячейки по fd.
      • Таймауты -- весь менеджмент скопирован из cxscheduler2.c, включая список свободных (avl). Только вместо собственного менеджмента таймаутов (очереди) -- всё свалено на Xt.

    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".

  • 23.01.2013: наткнулся на "конкурента" cxscheduler'а -- libev, "a high performance full-featured event loop written in C".

    И его предшественник -- libevent, "an event notification library".

    И даже сравнительный benchmark между ними.

  • 02.05.2013: приступаем к созданию "cxscheduler3" -- версии с прямым доступом для драйверов, по проекту из "Об API модулей", с "uniq" и парой privptr'ов вместо одного.

    Делаем всё в дополнительных файлах с суффиксом "3", работы ведутся в 4cx/, дабы не мешать живой системе управления.

    02.05.2013: по параметрам callback'ов получается очень удобно: достаточно добавить в начало списков парочку "uniq, privptr1" -- и выйдет полная совместимость с нынешним драйверным API: uniq<->devid, privptr1<->devptr. (Это следствие "уразумнивания" списков параметров в ноябре-декабре; с fdiolib аналогично).

    • cxscheduler3.[ch]
    • fdiolib3.[ch]
    • Xh_cxscheduler3.c
    • Qcxscheduler3.c
    • 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".

  • 14.09.2013: сходу -- внедрена концепция "uniq_checker", а указание оного делается через sl_set_uniq_checker().

    15.09.2013: в Xh_cxscheduler и Qcxscheduler также скопировано.

  • 18.09.2013: добавлена еще одна проверка на should_break -- сразу после прохода по таймаутам, чтоб они тоже могли б прерывать цикл.

    18.09.2013: непосредственно занадобилось при внедрении clientside handshake-таймаутов в cxlib, иначе в консольных утилитах оно не работало.

    А вообще сие было серьёзной идеологической недодумкой с самого начала.

  • 15.06.2014: надо вводить обнаружение перевода часов назад, чтоб программы при переходе между летним и зимним временем не зависали бы на час.

    В 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, где ВСЕ события доставляются унифицированно.

  • 12.09.2014: желательно бы иметь возможность просматривать список мониторируемых дескрипторов относительно uniq -- чтоб понимать, какой дескриптор принадлежит какому драйверу. Чисто в отладочных целях.

    (Может, и таймауты тоже, но тут не так ясно.)

  • 12.10.2015: (записано 22.10.2015, сидючи на ICALEPCS'е) Федя попросил, чтоб sl_break() отрабатывалось сразу, а не по завершении полных цифлок опроса fd/tout.

    Это легко -- доп.условие в оба цикла (fd/tout).

    16.01.2022: похоже, опять возникает эта же потребность -- у ЕманоФеди что-то там глючит, видимо, без этого условия.

    Ну добавлено -- делов-то...

  • 22.10.2015@Melbourne, ibis, 619, утро-душ: о приоритетах между дескрипторами и таймаутами в cxscheduler: можно ввести флаг SL_PRIO, и ПЕРЕД проходом таймаутов делать поллинг всех таких.

    Вопрос только, КАК это спасёт? Так ли надо делать проверку -- ведь, казалось бы, после этого она всё равно попадёт в опрос дескрипторов?

    А, да, так -- чтоб проверить fd ДО таймаута.

  • 16.01.2022: 01.06.2020 было сделано важное изменение в cxscheduler -- добавление поддержки концепции "mt_cxscheduler", что включает в себя несколько hook'ов, являющихся #define-макросами с именами CXSCHEDULER_HOOK_*, вызываемыми при #ifdef.

    Но описано оно всё было не тут, а в разделе по img878 (т.к. делалось в его интересах).

    И в CXv2 оно не было скопировано -- там так и остался вариант за 16-06-2014.

  • 12.02.2022: некоторое исправление работы с битовыми наборами/масками дескрипторов: теперь sl_set_fd_mask() при занулении битиков удаляет их не только из соответствующих наборов rfds/wfds/efds, но и из мспользуемых с select()'ом sel_rfds/sel_wfds/sel_efds.

    (Подробнее см. в разделе по fdiolib за сегодня.)

    12.02.2022: конкретно:

    1. FD_CLR()'ы добавлены в sl_set_fd_mask(), в ветки с Remove*fds().
    2. А из sl_del_fd() они выкомментированы, за ненадобностью.

    Самое странное в этом то, что сама потребность удалять из sel_*fds сразу же была осознана давным-давно (хбз когда), но сделано это почему-то не было.

  • 03.06.2024@~09:45, дорога в ИЯФ на планёрку, между мышью и аркой ИЦиГ: а не сделать ли вариант на kqueue?

    Но тогда вопрос, как "удалять дескрипторы из рабочего набора" посреди цикла, чтоб они далее не рассматривались.

    @подвал-Кузнецова, ~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).

fdiolib:
  • 29.03.2006: "начальная" версия fdiolib -- за последнюю неделю я ее практически сделал. Пока не испытана, а только создана, но тем не менее...

    29.03.2006: highlights:

    • Во-первых, она основана на идее «адресация не по файловым дескрипторам, а по внутренним "индексам записей", которые наружу никак не видны» за 06-02-2005/02-05-2005. Плюс -- массив "записей" не фиксированный, а растущий по мере надобности, как описано в "О работе с памятью" за 20-12-2004 (менеджмент массива практически копировался из менеджмента cda.c::serverinfo_t.bigcinfo).

      Кстати, циклить по массиву не понадобилось никогда вообще -- поскольку cxscheduler нам всегда передает handle ячейки, связанной с дескриптором.

      И еще: поскольку адресация по файловым дескрипторам теперь отсутствует, то махинации с "clnidx[]" за 08-06-2005 тож нафиг не нужны.

    • Понятие "replybuf" и все с ним связанное из концепции fdiolib удалено -- пусть этим занимается программа-клиент, поскольку у нее и без того будет менеджмент кучи буферов (subscrbuf, lastreqbuf, etc.), а fdiolib следует заниматься выполнением только СВОЕЙ задачи -- I/O

      (К тому же, это в connlib'е библиотека знала о структуре заголовка, а тут-то -- нет.)

    • При регистрации ячейки передается "тип" дескриптора -- FDIO_{CONNECTING,STREAM,LISTEN,DGRAM}.
    • Библиотека сама следит за CONNECTING-дескрипторами, и когда таковой становится ready-for-write, то она проверяет -- если ошибка, то сообщает о ней, а если ОК -- то сама превращает CONNECTING в STREAM.
    • Поддержка дескрипторов DGRAM пока не сделана, но идея о том, как будут запоминаться датаграммы на отправку, следующая: в _sysbuf кладется 32-битовая длина датаграммы, а потом она сама (padded-to-4-byte-boundary), и так далее. В остальном -- аналогично STREAM-дескрипторам (move-on-demand).
    • Как бы то ни было, но из-за потенциальной поддержки РАЗНЫХ типов соединений код В/В имеет двухуровневую/селекторную архитектуру: функции fdio_io_cb() и fdio_send() сами ничего не делают, а вызывают соответствующий "метод" -- {Stream,Dgram}{ReadyForRead,ReadyForWrite,Send}().

      (Напоминает то, как ядро обходится с разными протоколами (IPv*/UNIX/IPX/..., STREAM/DGRAM) и файловыми системами.)

    • Некая верификация корректности параметров (еще и в зависимости от fdtype) в начале fdio_register_fd() присутствует, но пока она слабовата. В частности, пока толком не определено: 1)  как именно описывать STREAM-пакеты фиксированной длины, а также 2)  как бы поддерживать DGRAM-пакеты БЕЗ поля длины -- ведь там длина может браться просто из размера датаграммы.
    • Насчет "пакет всегда будет либо принят к отправке, либо целиком отвергнут" имеются некоторые сомнения.
      1. В месте, где при отправке вызывается GrowBuf() -- там уже полпакета отправлено, а остальное может обломиться из-за нехватки памяти.

        Впрочем, нехватка памяти -- в любом случае столь серьезная проблема, что с чистой совестью можно убивать соединение.

      2. Запланированное для cxsd_fe_cx использование fdiolib для "реассемблирования"/"переупорядочивания" результатов исполнения больших каналов точно создаст разрывность пакетов -- просто потому, что пакет будет отдаваться не циликом, а кусочками.

        Вводить понятия "начало и конец транзакции"? Чтобы и пользоваться _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) можно было перед застреливанием отправить ответ.
    • Стало очевидно, что вместо "fdio_get_req()", потенциально могущего быть вызванным фиг-знает-когда, надо прямо нотификатору передавать req,reqsize. (По аналогии с тем, как Motif/Xt передают не только client_data, но и call_data.)

      Так что -- добавил параметры inpkt,inpktsize и выкинул fdio_get_req().

    • Вроде бы решили забить на "replybuf & Co." -- а буфер такой в fdinfo_t оставался. Выкинут.

    17.01.2007:

    • В соответствии с правилом из bigfile-0001 за 26-01-2006 о выводе отладочной информации перевел диагностику с просто вывода на stderr на функцию reporterror(), выдающую время и прочую информацию.

    18.01.2007:

    • А SIGPIPE-то надо перехватывать -- иначе если с той стороны соединение уже закрыто, а мы пытаемся что-то слать, то получаем SIGPIPE.

      Так что -- вставил уставку в SIG_IGN в cm5307_dbody.c, i_cm5307.c, rrund.c и canserver.c.

    14.04.2007:

    • А еще -- вместо разумного ограничения на максимальный размер _sysbuf'а оставалась проверка-заглушка, которая при ЛЮБОМ newbufsize>0 возвращала EOVERFLOW. Нехорошо!

      Введено дополнительное поле в 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".

  • 15.05.2006: при реализации поддержки UDP необходимо учитывать, что UDP-датаграммы размером 0 байт совершенно легитимны. Это надо принять во внимание как в "send", так и в "recv".

    04.10.2014@Толмачёво-гуляние-по-террасе-гейтов-перед-отлётом: (реально мысли бродили раньше; в связи с предстоящей необходимостью делать "оракула" для UDP-резолвинга имён) с UDP есть и другая проблема -- соединения могут быть не-connected, и надо будет:

    1. Уметь отвечать на тот адрес:порт, с которого пришёл запрос.
    2. Мочь слать на произвольный адрес:порт.

    Собственно:

    • Ясно, что в _sysbuf надо складывать "префиксом" пакета не просто 32-битовую длину датаграммы (как записано в highlights от 29-03-2006), а ЗАГОЛОВОК, включающий также адрес "куда слать", который если указан (о чём иметь флажок), то делать sendto() вместо write().
    • С "ответом" есть два варианта:
      1. Иметь пару вызовов "fdio_lastsrcaddr()" и "fdio_sendto()".
      2. Иметь вызов "fdio_reply()".

      Для реализации нынешней более простой потребности (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*().

    • Заголовок датаграмм для _sysbuf -- dgraminfo_t, содержит размер и информацию об адресе.
    • В fdio_register_fd() в смысле аллокирования приёмного буфера DGRAM-соединения приравнены к STRING -- отводится место сразу по максимуму.

      (Кстати, некрасиво, что буфер отправки аллокируется тем же объёмом.)

    • DgramReadyForRead() вроде сделана, творческим копированием со Stream-варианта.

      Но остаются неопределённости по обработке ошибок: нет ли каких случаев, когда ошибка есть, но нефатальная (например, от прошлой отправки)?

    • DgramSend() просто отфутболивает к DgramSendTo(), to=NULL,tolen=0.

    Некоторые мысли/замечания по ходу дела:

    1. ...неясность с отправкой: что, если сокет вроде готов на запись, но влезет в него ма-а-аленькая датаграммочка, а большая -- нет, вернувши EWOULDBLOCK? И будет оно тогда бесконечно дёргать DgramReadyForWrite(), бесконечно обламывающийся... Или будет вёрнут EMSGSIZE?
    2. И вообще, радикальное отличие: если со stream-дескрипторами любая ошибка фатальна, то с datagram/unconnected всё совсем не так, поскольку каждая операция отправки/приёма независима.
    3. Кстати, поддержка FDIO_DGRAM нужна не только для "оракула", но и в cda_d_cx.c или cxlib_client.c для спрашивающего UDP-сокета.

    09.10.2014@Снежинск-каземат-11: продолжаем:

    • Насчёт "что вернёт": смотрим в 2.6.25.20'ный net/ipv4/udp.c::udp_sendmsg():
      • EMSGSIZE оно вернёт только при размере датаграммы >0xFFFF (и каком-то варианте ICMP_DEST_UNREACH).
      • При невлазящести же, похоже, будет ENOBUFS.
      • Но еще в man-странице по 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() нет НИКАКИХ проверок на размер прилетевшего пакета.

    Делаем.

    1. На верхнюю границу проверять незачем: и так запрашивается вычитывание именно такого объёма, так что больше и не может.
    2. А вот на нижнюю -- проверка добавлена.

      Только облом по ней не считается фатальным, т.к. UDP -- connectionless, и может поймать какой-то мусор, посланный фиг знает кем, так что протокольная ошибка не является поводом застрелиться (иначе это получился бы прекрасный remote DoS).

  • 12.01.2007: нужна в каком-то виде поддержка режима/состояния "когда все забуферизованные данные будут отправлены, то закрыться".

    Смысл -- возможна ситуация, когда отправляется пакет большого объема (не лезущий в системный буфер), но само соединение после этого уже не требуется и программа-клиент хочет про него забыть (как делает Apache). Просто делать "send();deregister();close()" некорректно -- тогда не-успевшие-отправиться данные пропадут.

    Надлежит также иметь и некий "период ожидания" -- если, например, за минуту данные так и не ушли, то просто прибиваем соединение.

    12.01.2007: есть два варианта реализации, оба не вполне удовлетворительные:

    1. Ввести специальный notification-reason -- "все отправлено". Тогда клиент сможет у себя пометить соединение как уже-ненужное, и когда ему прилетит такое уведомление -- сделает deregister(). Таймаут на период ожидания также реализуется клиентом.

      Для этого в fdiolib почти ничего добавлять не требуется -- только вызов с резоном "все отправлено". Но тут есть узкое место -- данные-то могут сразу уйти все, без буферизации -- так что в таком случае также надо как-то уведомлять программу; возможно -- дать ей функцию-проверку "все_отослано()".

      Недостаток же -- все сваливается на клиента.

    2. Ввести специальный подвид закрытия -- "отправь-все-и-тогда-закрой()". А уж fdiolib сама выплюнет все данные по мере возможности, и сама прибъет это соединение.

      Как реализовывать:

      • уставлять fdinfo_t.fdtype в "FDIO_CLOSING", в котором всякие операции будут запрещены (прямо в DECODE_AND_CHECK() -- это корректно, поскольку сама fdiolib никакие свои API-функции сама не вызывает (по крайней мере, сейчас));
      • отправлять по мере возможности, и когда все -- то освобождать соединение и делать close(fr->fd);
      • таймаут поддерживать самостоятельно через cxscheduler.

      Недостатки:

      1. делать больше: по-другому реагировать на ошибки в ReadyForWrite() -- сразу убивать, а не close_because(); не забывать оставлять маску только 2, без 1; ...;
      2. пока-то от cxscheduler'а используется только fd-интерфейс, а так добавится и _timeout. А ведь было желание когда-нибудь научить fdiolib работать с "произвольным" планировщиком.

    Короче -- хотя 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()", а это нехорошо -- надо бы прибивать ТОЛЬКО ПРИЁМ, и оставлять возможность отправки и отложенного-закрытия, чтобы программа могла что-то вякнуть корреспонденту...

  • 14.01.2007: насчет того, что поддержка "replybuf" отсутствует: а что, если ввести полу-поддержку -- с одновременной как бы поддержкой zero-copy? Т.е., чтоб программа могла попросить у fdiolib'а буферок такого-то размера, и та выделила бы местечко в _sysbuf -- естественно, программа далее ОБЯЗАНА закончить набивку пакета сразу же, и затем вызвать что-то типа "можно слать!".
  • 15.01.2007: а где у нас fdio_strreason()?!

    15.01.2007: уже сделан.

  • 15.01.2007: изрядно неудобно, что разрешен handle==0. Стоит зарезервировать нулевое значение, и отдавать начиная с 1 -- чтобы клиенты могли не волнуясь иметь значение 0 как зарезервированное/никакое.

    02.08.2011: угу, явно надо. Причём -- делается это тривиально.

    Протокол делания:

    • Все циклы по watched[] теперь переделаны на с 1 вместо былого 0.
    • Собственно поиск свободной ячейки вытащен в find_free_watched_slot(), ...
    • ...а то раньше оно вместо повторного поиска делало тупо "handle=watched_allocd-WATCHED_ALLOC_INCREMENT".
    • В DECODE_AND_*CHECK() теперь вместо handle<0 стоит handle<=0.

    Осталось проверить.

    07.02.2012@Снежинск-каземат-11: проверено еще примерно тогда же, летом, на sl_dbody -- поведение характЕрно изменилось, и это даже как-то там влияло/помогло с разборками.

  • 20.01.2007: явно нужны макросы, которые бы позволяли получить оффсет некоего поля в структуре и его размер -- для передачи параметров поля "длина пакета".

    20.01.2007: ввел, FDIO_OFFSET_OF() и FDIO_SIZEOF(), просто слизав из paramstr_parser.h.

  • 23.02.2007: иногда ведь воникает потребность в функциональности fdiolib и в Motif/Xt'шных программах, так? А fdiolib привязана к cxscheduler'у.

    Так почему бы не сделать этакий 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".

  • 16.12.2007: выяснилось (из книги Стивенса, Феннера и Рудоффа), что в Solaris'е 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 (хотя вряд ли она когда реально пригодится).

  • 04.07.2009: переименовал несоответствовавший правилам usrptr в privptr.
  • 28.09.2009: добавил еще функции-модификаторы-параметров -- fdio_set_len_endian() и fdio_set_maxpktsize().

    28.09.2009: обе понадобились для cxsd_fe_cx.

    • Первая -- чтобы можно было указать endianness тогда, когда он СТАНЕТ известен (т.е. -- когда клиент его сообщит).
    • Вторая -- чтобы защититься от ситуации, когда клиент попробует прислать что-то еще ДО пакета CXT4_ENDIANID: вначале ставится maxpktsize=sizeof(CxV4Header), и ЛЮБОЙ пакет с DataSize!=0 вызовет FDIO_R_INPKT2BIG.
  • 02.10.2009: есть такая потребность: иметь возможность вызвать отправку так, чтобы она НЕ пыталась сразу отправлять данные, а лишь складировала бы их в буфер.

    Смысл в том, чтобы (в 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'ы очень дорогие, их надо б экономить).

    Делаем:

    • Вводим lock count -- send_locked.
    • Когда он !=0, StreamWrite() не делает ни проверки "можно ль сейчас отправить скопившееся в буфере", ни попытки отправить даденное сразу (если буфер пуст), а только складывает в буфер.
    • "Скобки" -- fdio_lock_send() и fdio_unlock_send(). Они там "плагинизированные" -- сейчас реализации есть только для FDIO_STREAM, а остальные возвращают EINVAL (впрочем, для DGRAM сие всё равно слабоосмысленно).
    • Конкретно StreamLockSend() делает просто send_locked++, ...
    • ...а StreamUnlockSend() содержит мозги: если результат --send_locked ==0, то делает проверку, аналогичную начальной в StreamSend().

    Проверено на canserver'е -- да, вроде бы работает, и CPU занимает меньше.

  • 13.09.2012: некоторые технически-идеологические модификации:
    1. По готовности дескриптора на чтение стоит читать не один пакет, а ВСЁ, сколько есть (bigfile-0001.html за 11-09-2012).
    2. А при этом потребуется более аккуратная работа с handle'ами -- чтоб если внутри нотификатора программа сделает deregister, то повтор из п.1 прекращался.

    16.09.2012: обсуждение:

    1. Здесь не всё так просто -- учитывая, что в fdio_io_cb() стоит проверка и на SL_WR, то ПРЯМОЛИНЕЙНО в нём циклить нельзя (хотя и напрашивается).

      Похоже, надо вместо прямого вызова sl_set_fd_mask() завести свой, который также будет сохранять значение во внутреннее поле, и при цикленьи проверять готовность дескриптора на то (RD, WR), что сейчас в этом поле указано.

      17.06.2013: неа, всё намного проще и элегантнее!

      1. Циклить -- вообще прямо в StreamReadyForWrite(), тогда ничего не требуется от sl*().
      2. И собственно цикленье -- БЕЗ проверки check_fd_state()'ом, а просто сразу пытаться читать read()'ом, и если нечего -- то вернётся -1/EWOULDBLOCK.

        Просто проверять -- результат IsReadError() -- надо аккуратней, при >0 возвращать не -1, а 0.

      И еще: поскольку в разных применениях "разумные" требования на число повторов могут отличаться, то придётся ввести вызов для уставки максимума per-handle.

    2. Сначала (13-09-2012) появилась мысля, что надо 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: да, делаем.

    1. Сначала -- защиту с being_destroyed/being_processed.
      • Скобки "processed:=1,:=0" стоят вокруг только ОДНОГО вызова notifier'а -- в StreamReadyForRead() (для Dgram надо будет тоже), поскольку только в нём может циклить, а все остальные -- включая вызовы close_because() -- сразу же после и отваливают.

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

      • Отсрачивается в fdio_deregister() только "забывание" fr->fd=-1, всё остальное делается сразу.
      • Соответственно, StreamReadyForRead() после вызова нотификатора в случае being_destroyed просто делается fd=-1 и return.
    2. Собственно цикленье:
      • Собственно цикленье: пока текущий счётчик повторов (repcount) не превысит разрешенного максимума, сразу же после вызова нотификатора ему делается инкремент и goto в начало (метка REPEAT).
      • ...максимум -- maxrepcount -- может указываться вызовом fdio_set_maxreadrepcount().
      • Чтение "пока можно" без проверок на готовность дескриптора:
        1. В IsReadError() добавлен отдельно возврат -1 при реальных ошибках, ...
        2. ...а в оба его использования -- соответствующая проверка; причём результат >0 считается ошибкой на первой итерации (при repcount==0) -- но только при чтении заголовка, а при чтении данных уже всегда не-ошибка, т.к. оно может попасть туда и без готовности дескриптора, поскольку...
        3. ...теперь хитрая проверка "а если осталось читать 0 байт, или если дескриптор готов, то провалиться во вторую стадию" сменилась простым goto FALLTHROUGH.

          Кстати, как следствие: две половинки пакета считаются за 1 повтор, а не 2.

      • Кстати, добавлено пере-высчитывание fr после вызова нотификатора, поскольку извнутри него может создаться новая ячейка и массив watched переедет в другое место.

        BTW, это в проекте от 16-09-2012 было забыто (а касается ВСЕХ таких использований с пачечным вычитыванием).

    На вид -- функционирует, а проверять теперь только эксплуатацией.

    11.02.2014: да, вроде работает, проблем не заметно.

    Но насчёт IsReadError() есть некоторое сомнение: а не надо ли по альтернативе r==0 возвращать -1 вместо нынешнего "умолчательного" +1?

  • 21.10.2012@Снежинск-каземат-11: CONNECTING-дескрипторам указывается флаг SL_CE -- чтоб Xh_cxscheduler корректно себя вёл (Xt, мать их...).
  • 12.11.2012: вылез забавный косячок при запуске canserver, собранного с профилированем: он постоянно рвёт соединения с диагностикой
    ProcessPacket(handle=16, dev=7): I/O error
    

    12.11.2012: протокол решения:

    • Анализ кода показал, что FDIO_R_IOERR генерится в 3 местах. Во все была добавлена проверка на SHOULD_RESTART_SYSCALL() -- по аналогии с c4lcanmon'ом, вопившим про EINTR. Но -- не помогло.
    • А добавленная отладочная печать показала, что данная конкретная проблема вылазит в 1-й точке -- IsReadError(), причём errno=11 -- EAGAIN! Прикол в том, что это возникает в StreamReadyForRead(), вызываемом по готовности дескриптора на чтение -- вот такой парадокс!
    • Для решения перед close_because(fr,FDIO_R_IOERR) была также вставлена дополнительная проверка по ERRNO_WOULDBLOCK().
    • ...а для улучшения диагностики -- во все (оба) ProcessPacket()'ы была добавлена выдача также и errno в виде fdio_lasterr().
  • 23.01.2013: а с чего у нас вдруг в StreamSend() используется INC_PTR_BY_BYTES()? Тяжкое наследие connlib'а...

    23.01.2013: решено просто: у самих *Send() параметр buf стал uint8*.

  • 23.01.2013: реализовываем вчерашнюю идею касательно обработки SIGPIPE: что SIG_IGN должно уставляться в fdio_register_fd() при условии, что в этот момент стоит SIG_DFL.

    23.01.2013: сделано. Текущее состояние вычитывается sigaction()'ом, и потом проверяются и sa_handler, и sa_sigaction.

    Вроде работает, почитаем за "done", хотя из программ установку SIGPIPE:=SIG_IGN пока не выкидываем.

    Замечание: потенциальные сложности -- в других средах, типа Win32, где никакого SIGPIPE не существует.

  • 05.06.2013: сходу -- было сомнительное решение, что при попытке повторной регистрации дескриптора оно возвращало имеющийся handle. Это неправильно, поскольку повторная регистрация -- точно баг (а еще может быть с другими параметрами).

    Так что теперь возвращается -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'ом.

  • 14.09.2013: сходу -- внедрена концепция "uniq_checker", а указание оного делается через fdio_set_uniq_checker().
  • 07.05.2014: надо добавить возможность указания, что некий дескриптор -- WRITE-only. Требуется для write-сторон pipe()'ов, чтоб не спрашивать от ядра проверки готовности на чтение O_WRONLY-дескрипторов (а то оно и EBADF вернуть может).

    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.
    • При переходе 0->1 в поле "cur_SL_RD_mask" должно стать =0 и вызваться sl_set_fd_mask() чего-надо.
    • При переходе ->0 должно сделаться cur_SL_RD_mask=SL_RD_mask и опять же вызваться уставка чего-надо.
    • Но: та часть "чего-надо", что отвечает за бит SL_WR, сейчас поддерживается явочным образом -- в соответствующих точках его наличие/отсутствие прописано явно.

      Сейчас же так не прокатит.

      Поэтому надо также ввести

      1. Поле cur_SL_WR_mask, в которое "те" места и писали бы 0/SL_WR.
      2. Функцию "уставить текущую маску" -- SetCurIOMask(), делающую
        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.

  • 07.08.2014@вечер-пляж: возникло желание добавить еще один fdtype -- FDIO_STRING, чтоб оно читало побайтно, до \n, \r или \0, а отправляло бы ровно так же, как FDIO_STREAM. Конкретные соображения в разделе cda_d_vcas за сегодня.

    ...ну и еще надо будет FDIO_STRING_CONN, который вёл бы себя ровно как FDIO_CONNECTING, но превращался бы в FDIO_STRING.

    07.08.2014@вечер-пляж: некоторые замечания о функционировании:

    • Оно может оказаться полезным и для remcxsd -- сейчас там считывание имени ведётся врукопашную.
    • maxpktsize при этом можно считать максимальной разрешенной длиной строки, и сразу делать malloc().
    • Только считывать надо аккуратно, НЕ вылазя за границу \n: после получения строки потребуется перевести дескриптор в другой режим -- видимо, сделав fdio_deregister() с последующим fdio_register_fd() с другими параметрами.

      А ведь хотелось бы для экономии количества syscall'ов делать read() сразу большого блока!

    08.08.2014: сделано, но пока никак не проверено (дождёмся cda_d_vcas).

    • Чтение сделано по 1 байту/символу, вплоть до символа-терминатора (коим считается и EOF при непустой строке) либо до ошибки.
    • Некоторая неясность потенциально связана с пере-регистрацией дескриптора в клиентах вроде remcxsd: ведь когда она производится из notifier'а, то разрегистрация "отложенная".

      Но реально там сразу делается fd=-1, так что последующая регистрация должна пройти нормально.

    21.01.2015: проверено на сделанном позавчера-вчера-сегодня starogate. На вид всё OK.

    09.04.2015: и на cda_d_vcas.c тоже проверено -- работает, так что "done".

  • 05.04.2015: есть некоторая мутность с механизмом асинхронных уведомлений, в смысле вызова этих уведомлений при ошибке ЗАПИСИ в момент не "асинхронной работы", а в момент fdio_send() & Co.

    С одной стороны, синхронные операции -- вроде StreamSend() -- при ошибках просто возвращают -1, и более ничего не делают.

    С другой же стороны, они вначале опционально вызывают свои *ReadyForWrite(), а уж те могут при ошибке дёрнуть close_because(), коий вызовет notifier.

    06.04.2015: обсуждение -- как поступить?

    • Возник вопрос при анализе работы cda_d_vcas.c.
    • Конкретно ТАМ, скорее всего, всё прокатит нормально потому, что в момент облома нотификатор корректно подчистит iohandle и fd -- закроет и сделает =-1, так что "повторное закрытие" ничего не испортит.

      ...но даже и там пришлось вставить лишнюю проверку, чтоб второй раз rcn_tid не заказало.

    • А в других местах может и проблема возникнуть.
    • По анализу имеющихся сейчас "других мест" видно, что правильный вариант -- во время нахождения в "синхронных" вызовах отключать асинхронные уведомления; лучше -- считающим семафором.

      (Да, это махинация в ту же степь, что и being_processed/being_destroyed.)

    • 12.03.2018: а зачем, собственно, каким-то семафором?!

      Ведь та же StreamReadyForWrite() не предполагает НИКАКОЙ рекурсии и вызывается всего из 3 точек: 1) асинхронно; 2) StreamSend(); 3) StreamUnlockSend().

      Решение очевидно: добавить всем *ReadyForWrite() еще один параметр -- "можно ли делать close_because()" (или просто "is_async_use"), и изнутри синхронных вызовов передавать его =0.

    07.04.2015: размышление общего характера:

    • Первая мысль: а ведь если connect() отрабатывается сразу, то и нотификатор по-хорошему надо вызывать сразу -- прямо изнутри СИНХРОННОГО библиотечного вызова!
    • Но: fdiolib'а ведь это не касается -- он сам-то connect() не делает.
    • А делает -- cxlib, имеющий чем-то похожую архитектуру.
    • Но в нём сейчас НЕ стоит проверка/реакция на мгновенную успешность connect()'а, а просто регистрируется событие

      (Исключение -- работа в _sync-режиме, но там всё проще, да и использование этого режима очень ограниченно.)

    • Напрашивается общий принцип: мгновенное завершение connect()'а лучше и не определять, а отдавать на откуп дальнейшей проверке -- типа "ставить в очередь". Так оно всё будет унифицированнее и с минимумом исключений и тонкостей.
  • 07.04.2015: неприлично, что параметр buf у функций отправки имеет тип void*, а не const void*.

    08.04.2015: ага, только ЭТО изменение тянет за собой еще одно: "const" нету и в uintr_write() (который, впрочем, теперь используется уже только в самой fdiolib).

    • Да, "const" добавлено.
    • И в uintr_write() тоже добавлено.

      Результат обоих действий проверен diff'ом по логам от w_mkall.sh -- новых warning'ов не появилось, только старые исчезли.

    • Заодно замечено, что {n,uintr}_{read,write}() возвращают int, а должны бы ssize_t.

      Но это уже в раздел misclib.

  • 07.06.2015: в 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...

  • 10.04.2016: позорище -- в 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".

  • 10.04.2016: забавная особенность, появившаяся с момента введения being_processed+being_destroyed: оно теперь НЕ позволит из обработчика ПЕРЕрегистрировать дескриптор с другими параметрами, т.е., сделать fdio_deregister_fd() с непосредственно следующим fdio_register_fd().

    Потому, что

    1. fdio_register_fd() проверяет, не зарегистрирован ли уже такой дескриптор, сравнением поля .fd всех ячеек, а...
    2. ...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".

  • 03.04.2017: оптимизация менеджмента буфера отправки (_sysbuf*): теперь он "схлопывается" вниз (с нулением _sysbufoffset) не при каждом добавлении данных. А только если "сверху" нет места; если же место есть (между _sysbufoffset+_sysbufused и _sysbufsize), то новые данные просто добавляются в конец.

    03.04.2017: сделано в интересах cxsd_fe_cx (и основные комментарии в его разделе за 31-03-2017 и за сегодня), но полезно для всех -- эта оптимизация очень дешевая (один if), а эффект даёт изрядный.

    Кроме собственно if()'а также чуть изменён код строк fast_memcpy() (там к dst прибавляется _sysbufoffset) и обновления _sysbufused (теперь просто делается +=size).

  • 07.10.2018: возникла потребность в ещё одном расширении: чтоб мочь поддерживать работу с форматами, не укладывающимися в архитектуру "сначала фиксированный заголовок, и в нём длина пакета/данных фиксированным полем". В частности -- для MQTT, где длина начинается со 2-го ([1]-го) байта данных и кодируется в стиле UTF8.
    • Тут соль в том, что в fdiolib уже доведён до совершенства код, делающий кусочное вычитывание пакетов, и было бы крайне обидно тратить усилия на дублирование в другом месте, отличающемся лишь способом дешифровки заголовка -- явно тянет просто расширить fdiolib.
    • Но просто добавить тип "FDIO_MQTT" было бы непотребством.

      Правильно бы сделать как-нибудь более общо -- как в своё время родился сам fdiolib, обобщающий все случаи с длиной фиксированным полем в фиксированном заголовке (и даже без поля длины тоже).

    • Позавчера даже возникла мысля -- плагины что ль начать поддерживать?

    07.10.2018@пляж: так называемый "плагин" для fdiolib'а плагином в полном смысле быть не может, т.к. все интимные детали работы со входным потоком сосредоточены внутри fdiolib.c, в fdinfo_t.

    Что МОЖНО сделать -- так это "методы": API для доступа снаружи, уставляющий значения этих интимных деталей (в основном -- очевидно, просто объёмы "сколько ещё надо прочитать"), плюс возможность "заглядывать" в уже считанные данные.

    Т.е., этот "плагин" -- как бы "внешний": просто программе-юзеру даётся чуть больше возможностей для сования своего носа в процесс приёма.

    Примерно так:

    • Новый "тип" -- FDIO_STREAM_PLUG, плюс дополняющий его FDIO_STREAM_PLUG_CONN (коннектящийся, после успешного коннекта переходящий в FDIO_STREAM_PLUG).
    • Дополнительный тип события "FDIO_R_DATA_PART", присылаемый по приходу части заголовка, считающейся "очередной последней" на текущий момент.
    • Дополнительные методы API чтоб позволяли нососование только при fdtype==FDIO_STREAM_PLUG.
    • 08.10.2018: и да, пример с EPICS/Channel Access подсказывает, что организовывать всю работу стоит так, чтобы для "мяса" прикладной программы (той части, которая работает уже с данными пакета, а не с дешифровкой длины) API оставался бы тем же, что и для обычного FDIO_STREAM.

    08.10.2018@пультовая, в районе 16:30: внимательно поразглядывав код StreamReadyForRead(), пришёл к выводу, что наилучшим местом для "внедрения" плагинности будет точка, где заголовок пакета считается полученным и код собирается добывать длину пакета из заголовка.

    И напрашивается проект реализации со следующими деталями:

    • Вводим в fdinfo_t:
      1. Флажок "is_asking_plugin".
      2. Размер "hdr_size_add" -- сколько, на текущий момент понимания, надо довычитать дополнительно байт заголовка.
      3. Размер "plugin_pktlen" -- вычисленный плагином размер пакета.
    • Вводим пару API-функций для возможности "советования" плагином fdiolib'у:
      1. fdio_advice_hdr_size_add() -- уставляет "hdr_size_add".
      2. fdio_advice_pktlen() -- уставляет "plugin_pktlen".

      Они отрабатывают ТОЛЬКО при взведённом "is_asking_plugin", иначе -- -1/EINVAL.

    • В "той самой точке", где для не-плагинов должно производиться вычитывание поля длины -- первой альтернативой в if()'е (ПЕРЕД len_size==0) вставляется if(fdtype==FDIO_STREAM_PLUG), где:
      • Квазиплагину сообщается о пришедшем известном-на-данный-момент заголовке -- событием "FDIO_R_DATA_PART".
        • Notifier'у передаются принятые на текущий момент данные прямо обычным образом -- через inpkt,inpktsize.

          Так что исчезает потребность заводить отдельный API для "заглядывания" в прочитанные данные.

        • Оно там анализирует длину, и смотрит, принято ли уже ВСЁ поле длины (или весь заголовок) либо нет, и в зависимости от этого:
          1. НЕ всё, надо ещё довычитать: вызывает fdio_advice_hdr_size_add() (в случае MQTT прося +1 к ранее прошенному).
          2. ВСЁ -- можно переходить к данным: вычисляет длину (ВКЛЮЧАЯ уже принятое!) и вызывает fdio_advice_pktlen().
        • 09.10.2018: А ещё надо б, чтоб "оно" имело возможность сигнализировать об ошибке (мало ли -- длина кривая, или слишком большая, или ещё что). Как?
          1. Просто внаглую делать fdio_deregister()?

            Можно (хотя и не очень красиво) -- должно отработаться корректно.

          2. Передавать какому-нибудь advice'ору значение -1?

            Хорошо бы, но не получится -- size_t беззнаково.

            Как вариант -- НЕ вызывать ни того, ни другого: тогда заголовок будет считаться полностью принятым, а "посоветованный" размер пакета (plugin_pktlen) останется нулевым, что сигнализирует невозможную ситуацию (ведь сколько-то УЖЕ принято) -- на это можно реагировать как "PROTOERR"

      • Далее смотрится, что:
        1. Если потребная длина пакета (т.е., hdr_size+hdr_size_add) стала больше текущего reqreadsize -- значит, надо вычитать ещё.

          Просто делаем "goto КОНЕЦ_ЦИКЛА" (в точку, где обрабатывается repcount и принимается решение о повторе).

        2. Иначе считаем заголовок считанным полностью.

          Тут просто переходим далее, к присвоению thisreqpktsize и дальнейшим проверкам, до-аллокированию и вычитыванию остальной части пакета.

        3. 09.10.2018: Ни то, ни другое: и заголовок принят, и plugin_pktlen==0: протокольная ошибка в заголовке, закрываем соединение.
    • Некоторые дополнительные аспекты работы с параметрами-размерами:
      • В начале, при reqreadstate==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_DATA_PART", а 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 -- да, всё получилось, пакеты (с определением длины плагином) принимает.

  • 16.10.2018@вечер-дома: есть (будущая) потребность слегка расширить функциональность FDIO_STRING: чтоб можно было указать, что дальше последует столько-то байт бинарных данных, которые надо просто прочитать, не пытаясь искать \r/\n.

    (Потребность -- для поддержки LXI, в котором, хоть он и текстовый, могут передаваться бинарные данные: например, вэйвформы).

    17.10.2018@утро-дома: мысли на тему о том, как бы это реализовать.

    • Указывать -- каким-нибудь вызовом, работающим ТОЛЬКО с FDIO_STRING-потоками, и разрешённым ТОЛЬКО при reqreadsize==0.

      Назовём его fdio_string_req_bin()?

    • Оно должно просто уставлять значение thisreqpktsize.
    • ...заодно, кстати, можно и взводить reqreadstate=1.
    • Некоторый вопрос в том, как РЕАЛИЗОВЫВАТЬ такое чтение.
      • С одной стороны, не хочется дублировать код из StreamReadyForRead(). Тем более, если пользоваться reqreadstate так, как предложено в предыдущем пункте, то там всё само собой отработается как надо.
      • Но, с другой стороны, там есть счётчик "повторов" (repcount), так что после вычитывания одного такого пакета оно попытается читать следующий как будто STREAM'овый же.

      Хотя проблему с repcount можно обойти, вставив дополнительное условие "повторять только если тип наш (FDIO_STREAM)", но это криво.

      Тем более, что кусок, в StreamReadyForRead() вычитывающий остаток пакета, совсем маленький, так что на дублирование можно закрыть глаза.

    • Ну и отдельный вопрос: как уведомлять клиента о приходе такого "нестандартного пакета" --
      1. Обычным FDIO_R_DATA, и уж клиент пусть помнит, что ждёт бинарных данных, а не строки?
      2. Или всё-таки ввести ещё один код причины -- "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 и комментариемю.

    Насчёт тестирования и потенциального использования:

    • Пока не проверено...
    • ...и не факт, что будет проверено/использовано: как выяснилось, в LXI бинарные данные идут НЕ после '\n', а прямо сразу после текстового указания длины (бардак!).
    • Ну, мож для http пригодится :).
    • Для LXI же -- видимо, придётся весь парсинг выполнять вручную.
    • Единственная альтернатива -- делать режим FDIO_STRING сильно параметризованным, чтоб оно САМО умело распознавать символы-разделители.

      С другой стороны, а зачем? Проще читать вручную, через cxscheduler (или, если хочется использовать fdiolib'овский интеллект буферизации на отправку -- запрашивать пакеты фиксированного размера 1 байт).

      20.10.2018@суббота-дома: а можно воспользоваться предыдущим вариантом -- FDIO_STREAM_PLUG: указать hdr_size=1 и len_size=0, чтобы получать по 1 байту, и дальше уже анализировать эти байты самостоятельно, запрашивая "добавку" обычно по 1 байту, самостоятельно же отлавливая '\n'/'\r', а при переходе в бинарный режим затребовывать как раз нужное количество байт.

    Замечание:

    • Есть одна потенциальная ловушка при использовании с "хорошими" протоколами (где бинарные после перевода строки): ведь если данные идут после '\r', то они могут начинаться с '\n', коий будет слопан в качестве потенциального второго символа из дуплета CR,LF.
    • Однако, как показывает анализ кода StringReadyForRead() -- нет, не будет: символы там воспринимаются раздельно, без попыток анализа разных вариантов (DOS: CR,LF; Unix: LF; MacOS(?): CR), выполняемых в ppf4td_nextc(), дабы воспринимать эти комбинации как единое окончание строки (в противном случае подсчёт номеров строк бы поехал).
    • Что как раз открывает другую проблему: с собеседниками, шлющими именно "\r\n", оный '\n' будет восприниматься как первый байт бинарных данных.
  • 08.02.2022: наблюдена выдача сообщения
    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@утро-душ: не вполне ясно, как тут вести отладку: воспроизводится при вполне неясно каких условиях (слегка рандомно), да и непонятно, на что смотреть.

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

    Можно поступить так:

    • Выводить не на stderr, а на какой-то другой дескриптор -- например, #3.

      Редирект на который должен обеспечиваться запускающим (через синтаксис "cxsd 3>/PATH/TO/FILE").

    • Делать "временную" печать -- то ещё удовольствие, т.к. вставлять придётся в кучу мест; да и сама отладочная печать может быть полезна и в будущем (мало ли что придётся диагностировать).

      Поэтому делать надо на постоянку.

    • Возможно, fdiolib'а будет недостаточно, а понадобится ещё в cxscheduler'е печатать (но не факт).
    • И чтоб включалось какой-то переменной окружения.
    • А чтоб сам номер дескриптора не хардкодить -- ну пусть он в содержимом этой переменной окружения и указывается.

    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().

    Итого:

    1. Косяк оказался в комбинации поведения cxscheduler'а и нюанса поведения fdiolib'а (добавленного, кстати, сильно позже; хбз когда -- записей не видно, надо рыть по архивам).

      В w20090408.tar.gz файл от 03-02-2009 уже с этим. И более того: самый древний из найденных файлов, viper-archive/20060516/work/4cx/src/lib/useful/fdiolib.c за 30-03-2006 (а сами работы по fdiolib'у начались в марте 2006-го, судя по записям от 29-03-2006). Так что -- "нюанс", похоже, был изначально.

    2. Надлежащим исправлением считаем модификацию sl_set_fd_mask().
    3. Ну а логгинг в fdiolib'е оставим, просто сделаем его по умолчанию отключенным.
  • 24.01.2024@утро: а не добавить ли "fdio_reset_read()" -- чтобы сбрасывать состояние чтения (с забыванием всего уже находящегося в reqbuf)?

    Смысл -- чтобы мочь "сбрасывать" состояние чтения для всяких Modbus-serial, если вдруг "что-то не то", например, по таймауту.

    Замечание в сторону:

    • А ведь ещё и надо НЕ закрывать соединение по ошибке при, например, "превышении" пакетом максимального размера -- ведь в реальности такое "превышение" может быть следствием какой-то врЕменной некорректности (вроде мусора в данных).
    • И вообще надо бы как-то уметь восстанавливаться после ошибок -- для serial-линий это особенно важно.

      Но вот "КАК?" -- вопрос, и не столько технический, сколько идеологический. Думать надо...

    24.01.2024@дорога-в-ИЯФ: с другой стороны, нынешний вариант -- "грохать соединение при любой ошибке, в т.ч. протокольной, чтобы потом его открыть заново" -- вполне живуч: как раз пройдёт некоторый интервал времени, так что остаток пакета по serial-link'у успеет придти и будет выкинут при следующем открытии делаемым там tcflush(,TCIFLUSH). Единственное что, modbus_mon'у должен будет быть указан ключик "-K" ("keep going after I/O errors"), но он так и так необходим для не-отвала при ошибках.

    Так что, пожалуй, СЕЙЧАС реальной надобности в "fdio_reset_read()" и нет.

seqexecauto:
smp4td:
  • 09.06.2008: надо будет завести этот модуль -- "Simple Macroprocessor For Text Data". Как "расширение" и обобщение макропроцессора из нынешнего Cdr_fromtext.c.

    1. Название "smp4td" выбрано из соображений уникальности -- и "tfp" (text file parser) и "tdp" (text data parser) имеют некие применения, а эта 6-символьная аббревиатура -- нет.

    2. Поселяем его в useful/, а не в misc/, поскольку он все-таки достаточно крупен -- и скорее в одной весовой категории с прочими файло- и В/В-ориентированными модулями. Да, paramstr_parser, который в некотором роде родственен, живет как раз в misc/, но его можно считать "модулем меньшего масштаба", так что все окей.

    09.06.2008: и еще раз битым текстом "целевая аудитория" этого модуля:

    • Cdr_fromtext
    • Читчик cxsd.conf в сервере.
    • Читчик "blklist.lst" в сервере.
    • Читчик config-файлов в cx-starter'е.

    05.10.2008: STPP -- Simple Text PreProcessor!

    15.10.2008: поразмыслив, нет -- пусть лучше останется SMP4TD: это абсолютно уникальное буквосочетание, в отличие от STPP.

  • 31.10.2008: пора потихоньку приступать. Сегодня создан практически пустой smp4td.h.

    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: продолжение:

    • smp4td_test.c::stdio_lreader() об-умнена: она теперь при fgets()==NULL проверяет feof(), и возвращает EOF или ERR.
    • По образу psp_pluginp_t добавлен параметр char **errstr в smp4td_open_t и smp4td_lread_t -- чтобы они могли возвращать внятное описание ошибки, которое SMP4TD потом сможет интегрировать в своё описание и отдать по smp4td_lasterr(). (Это чтобы вся диагностика делалась единообразно, без надобности лазить в диагностику подстилающей системы чтения.)
    • Соответственно, методы в smp4td_test.c этой технике обучены.
    • Начато наполнение реального чисто конкретного юзера -- Cdr_newf_ldr.c.

    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_PMS_SNGQUOT+SMP4TD_PMS_DBLQUOT. Они указывают 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.

  • 28.02.2009: похоже, надо будет кроме "простых" макросов поддерживать также и макросы с параметрами -- "функции". Синтаксис, видимо, проще всего сделать как в srchtml -- "$(funcname param1,param2,...)". Плюс, по-хорошему надо дозволять вложенные макросы -- т.е., "$(funcname param1,$(MACRO),...)"; видимо, по тому проектику, который был в своё время разработан для srchtml.

    Плюс, кроме .define надо также иметь и .definexline/.endxline.

    В любом случае -- это фичи уже второго порядка, т.к. они значительно усложнят препроцессор.

    01.03.2009: однако -- а решение проблемы реализации "макросов с параметрами" реально довольно просто: надо использовать вложенные вызовы "parse_macro_string()".

    Размышления по теме:

    • Надо будет ввести ограничение на глубину рекурсии -- например, 100.
    • И на глубину вложенных include -- аналогично 100.
    • Могущие появиться в параметрах макросов ')' и ',' можно защищать при помощи '\' -- это получится просто автоматом.

    Потенциальные сложности:

    1. Понадобится "two-stage macro expansion" -- в момент определения и в момент использования. Как?
      1. Use some "magic" and trickery.
      2. Постулировать некие правила -- как в GNU make есть "=" и ":=".
    2. Как будут выживать "защищенные" "\$"?

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

    Более навороченный вариант может оказаться полезен на будущее (аппетиты-то растут во время еды), пока же, для заявленных использований (config-, devlist- и subsys-файлов) хватит и такого SIMPLE.

  • 04.07.2009: вопрос -- а можно ли как-то сделать, чтобы spm4td мог использоваться "параллельно"? Т.е., чтобы каждая его потребность в вызове lread() могла передавать управление вызывающей программе? Тогда можно было б применять его в качестве парсера для всяких commandline-интерфейсов (cx-console, cangw-console), что дало бы в них возможность простенького скриптинга.

    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_stdio.c. В него переехали стандартные opener+closer+lreader из smp4td_test.c.

    15.09.2009: побудительным мотивом послужила надобность реализовать smp4td-читания config- и devlist-файлов в сервере. Поскольку там всё то же самое, то пришлось копировать, что есть изврат. А в будущем явно появятся еще потребители.

    Оставлено соглашение, что reference==NULL возвращает stdin. Это есть хорошо -- тем, что юзер НИКАК не сможет указать ссылку на stdin в обход программы (как это было бы, например, при "" или "-"): первичный файл всегда проходит через программу, а в .include NULL также никак не образуется.

    Возможное будущее усовершенствование -- если заиспользовать privptr в opener'е, например, для суффикса.

  • 15.01.2010: битым текстом: всё-таки smp4td НЕ ОЧЕНЬ подходит для потребностей Cdr. Поскольку не позволяет делать
    • ни макросы с параметрами,
    • ни даже просто макросы, расширяемые в НЕСКОЛЬКО компонентов -- нынешняя модель предполагает, что макрос целиком расширяется в один "токен".
    • Плюс, и shell-подобный синтаксис, где "токен" может быть последовательностью закавыченных и не-закавыченных кусков (БЕЗ сепараторов между ними) понимаемый даже paramstr_parser'ом, он также не воспринимает...

    И, скорее всего, с нынешним подходом "полноценный" макропроцессор сделать не удастся -- там нужна честная "распаковка" входного потока, полностью независимая от вызовов клиента. Т.е. -- модель produce/consume.

    (А в качестве временного решения подошло бы пропускание входного потока через cpp. А тогда понадобится, чтобы smp4td "понимал" выдаваемые оным директивы #file и #line.)

  • 07.10.2013: засим, пожалуй, раздел по smp4td можно считать закрытым, поскольку сам тот препроцессор будет уволен в пользу более гибкой инфраструктуры ppf4td.
    05.12.2013: да, увольняем и удаляем, а раздел ставим в "obsolete".
ppf4td:
  • 07.10.2013: вводим новый раздельчик по архитектуре preprocessor-frontend'а ppf4td. Сами мысли и работы начались полгода назад в разделе по smp4td, но, поскольку модуль явно совсем другой, то вытаскиваем в свой раздел.

    04.12.2013: при создании оно жило в отдельной libppf4td.a, что неудобно. Так что переселяем прямо в libuseful.a.

    06.07.2014: подмодулей стало слишком много, так что содержимое раздела рассортировано по отдельному списку level5.

Общие вопросы:
  • 06.04.2013: еще некоторые соображения на тему "что делать с макропроцессором".

    06.04.2013: по пунктам:

    1. Еще энное время назад (в прошлом году?) появилась мысль: может, вместо изобретения велосипеда использовать готовое -- m4?

      ДОСТОИНСТВА: в нём есть всё, что надо -- и вычисления, и условия внутри макросов (причём исполняемые в момент РАСШИРЕНИЯ макроса!).

      НЕДОСТАТКИ:

      1. Нет такой штуки, как "libm4", его нельзя просто интегрировать в программу, подменив open()/read()/close()..., и "контекста" у него нет -- всё глобально.
      2. И рассчитан он на отправку результата в stdout.

      СЛЕДСТВИЕ: единственный возможный вариант использования -- делать pipe()+fork(), в "основном" процессе читать выход pipe'а, а в порожденном генерить данные на вход. "Подменить" блок чтения в m4 в принципе можно, взяв за основу m4-1.4.1 (1994г.) из RH-7.3 (более свежие распухли и стали шибко навороченны).

    2. 06.04.2013@утро-душ: всё же по smp4td: Вместо всякой алхимии типа "re-entry", оно просто должно мочь отдавать наверх по 1 символу, а уж где оно их будет брать -- его дело. (Спасибо, Олечка, ты для меня как муза.)
      • Для реализации потребуется иметь realloc()'ируемый буфер, к которому добавлять результаты расширения макросов (да, включая \n'ы). Т.е.: обычно пусть черпает из буфера, как буфер заканчивается -- берёт следующий символ из потока, и если тот '$', то делает macro-expansion. А если новая строка -- то считывает её, проверяя на тему .-директив. ...или ввести еще дополнительный уровень, читающий цельные строки, для возможности условной компиляции? -- Да!
      • И еще: оно должно мочь выдавать директивы #line NUM "FILE", а клиенты -- разбирать их. Кстати, GNU m4 такое тоже умеет, при указании ключа -s.

        Выдавать такие директивы надо при .include и его завершении. Что делать при расширении xline-макросов -- некоторая загадка.

      • Короче: так можно почти полностью заменить потенциальный m4; "почти" -- потому что в m4 есть условия прям в макросах... В принципе, должно б хватить, а если что -- можно добавить $(if...) (аналогично GNU make).

        ...хотя, еще правила вложения кавычек в m4 удобные, да и не-только-строкоориентированность, но для наших целей и простого варианта хватит.

    3. В случае принятия такой схемы надо будет к "нижнему" уровню smp4td -- собственно макропроцессору -- добавить еще "средний", для удобства, чтоб в нём содержались стандартные вещи типа SkipWhite(), GetIdent(), и т.п.

      Это просто чтоб не дублировать такой общеиспользуемый функционал во всех клиентах.

    И, конечно, в любом варианте клиентские парсеры надо будет радикально переписать с нуля.

    07.04.2013: ...а если сделать этот "средний уровень" достаточно стандартным, то вообще можно его использовать для ЛЮБЫХ препроцессоров -- хоть smp4td, хоть m4, хоть cpp.

    08.04.2013: решено: делаем 3-уровневую архитектуру парсера:

    • Нижний уровень -- "конкретный" парсер со своим входным синтаксисом (хоть smp4td, хоть на основе m4, хоть на основе cpp), генерящий символьный поток, доступный по простому API из 4 функций: 1) открыть поток; 2) прочитать очередной символ; 3) подсмотреть очередной символ; 4) закрыть поток.

      Могут понадобится еще сервисные функции, типа "дай текущее положение в потоке" (для выдачи ругательства) и "не конец ли файла".

    • Средний уровень -- API парсинга высокоуровневых сущностей: число, идентификатор, строка (это на основе нижнего уровня).
    • Верхний уровень -- парсер конкретного "языка" (subsys, devlist, ...).

    Значительное изменение по сравнению со старой концепцией: теперь входной поток потребляется сразу посимвольно/потокенно, а не построчно (с последующим разбором).

    08.04.2013: начинаем реализовывать. Концепцию называем "gmp4td" (General-purpose Macro-Processor for Text Data).

    10.04.2013: да, многое сделано. По ходу:

    1. Название надо менять на "ppf4td" -- PreProcessor Frontend for Text Data. Ибо по факту это и получается frontend, к которому прикручивается куча back-end'ов.
    2. Эккаунтинг файлов/строк должен жить в его компетенции.

      По факту имеется стандарт на информирование "верхов" --

      #line LINENUM "FILENAME"
      или просто
      #line LINENUM

      Оное есть в cpp по умолчанию, в m4 при -s, ну а в своём препроцессоре добавить будет несложно.

      02.06.2013: а вот и нет -- в cpp не #line, а просто #, т.е.,

      # LINENUM "FILENAME" [FLAGS]

      Так что надо будет иметь ДВА варианта парсенья и возможность плагинам указывать используемый вариант (и, в т.ч., полное его отсутствие).

    3. Для некоторых случаев -- например, корректной обработки "\NEWLINE" -- может понадобиться фича "ungetc", чтоб оно уже подсмотренные символы всё же выпихивало обратно для последующего чтения. Это надо просто иметь в виду, и реализовать такой буферок будет несложно.

    18.04.2013: продолжаем:

    • Сделан общий модуль для всех pipe-препроцессоров -- ppf4td_pipe.c. Он старается всё делать корректно -- ловя ошибки от запущенных препроцессоров (по waitpid()'у).

      26.05.2024: вставлено signal(SIGCHLD, SIG_DFL); непосредственно перед execve(); основное обсуждение в разделе по sim_dir_drv за сегодня.

      30.05.2024: "ловя ошибки" -- ага, ага... А это проверялось хоть раз, что они БУДУТ ловиться? Судя по недавно понятому, при (SIGCHLD,SIG_IGN) -- нифига. Но там вообще результат "ловли" и не проверяется, а возвращается всегда 0.

    • На его основе работают ppf4td_m4.c и ppf4td_cpp.h.

      В m4 под RH-7.3 обнаружился глюк: он НЕ возвращает ошибку при отсутствии файла (exitcode==0). Под RHEL-5.2 уже всё нормально.

    • Для возможности отлова ошибок (и отличения их от просто EOF) был сменен API peekc/nextc. Теперь он отличается от stdio'шного "fgetc()", и возвращает символ не напрямую, а кладёт в указанное место; возвращаемое же значение -- статус: +1 -- ok, 0 -- EOF, -1 -- ошибка.

    01.10.2013: продолжаем, в основном в отношении поддержки "#line...":

    1. Вводим внутренний "буфер подсматривания" ppf4td_ctx_t._ucbuf[], с размером 100 символов. Причём int, а не char.
      • ppf4td_peekc()/ppf4td_nextc() сначала проверяют его.
      • Если там непусто, то просто отдаётся "верхний" символ из того "стека", и более никакие мозги (ppf4td_nextc()'шные) не участвуют.
      • UnGetStr() возвращает указанную строку {buf,len} в буфер (складывает, естественно, задом наперёд). Единственный её юзер -- ppf4td_nextc().
    2. Эккаунтинг:
      • Для указания способа синхронизации (#line, ...) в vmt добавлено поле linesync_type, плюс константы PPF4TD_LINESYNC_nnnn, плюс в DEFINE_PPF4TD_PLUGIN() оно добавлено.
      • Флаг ppf4td_ctx_t._is_at_bol (BeginOfLine), выставляемый при:
        1. отдаче наверх '\r' или '\n' (так достигается независимость от вариантов переноса строки), причём ТОЛЬКО после приёма от плагина, но НЕ из _ucbuf[];
        2. плюс в самом начале, в ppf4td_open();
        ...и сбрасываемый в ppf4td_nextc() при отдаче очередного "реального" символа, если он не-CR/LF (реально там уставка/сброс сделаны присвоением результата сравнения).
      • ppf4td_ctx_t._prev_ch помнит предыдущий РЕАЛЬНО вычитанный+вёрнутый символ, и если текущий -- '\n' при предыдущем '\r', то повторяет чтение. Таким образом пара CR+LF рассматривается как единый "newline".
      • _curline++ делается после вычитывания очередного символа, на основании ПРЕДЫДУЩЕГО значения _is_at_bol. (Да, изначально ставится _curline=0.)
      • Также по этому основанию анализируется начало новой строки на тему "# nnn..." или "#line nnn...". Данный кусок и является "мясом", и он весьма муторный.
        1. Если начало только похоже на указание строки, но не является им, то подсмотренное складывается в _ucbuf[].

          Смотрится аккуратно: очередной символ для проверки не читается (nextc), а подсматривается (peekc), и лишь если подходит -- то вычитывается. Это сделано чтоб не маяться с переводами строк, встречающимися прямо посреди потенциальных #-префиксов (например, "#\n") -- иначе оно некорректно считало номера строк (поскольку '\n' улетало в _ucbuf[] и потом проходило сразу в клиента, мимо "мозгов".

        2. Если же префикс подходит, то сначала вычитывается номер строки, и уставляется _curline=line_no-1 ("-1" потому, что потом сделается +1, а препроцессоры в строке "#..." указывают номер, относящийся к следующей строке).
        3. Потом, опционально, имя файла в кавычках.

          Тут пришлось также ОЧЕНЬ аккуратно делать: поскольку '\n' считается за isspace(), то на строках вида #line NNN\n пропускание whitespace после номера строки заодно сжирало и перевод строки, а последующий пункт успешно скушивал следующую строку.

        4. Затем (в т.ч. прямо после скольки-то символов имени, если оно шибко длинно) просто вычитывается/съедается всё до конца строки.
        5. В конце делается goto обратно на вычитывание из потока. Предполагается, что имеющиеся (еще от прошлого вызова) значения _prev_ch и _is_at_bol сохраняют свой смысл, так что они не трогаются.
      • Кстати, пара наблюдений за поведением cpp и m4:
        • cpp во входном потоке понимает не только "#line nnn", но и просто "#nnn" (без пробела!), и не только передаёт её наверх (дополняя именем файла), а и сам тоже запоминает (обнаружено случайно, наличием в файле строки "#1234").

          Видимо, введено для того, чтоб можно было запускать несколько cpp подряд в конвейере, или ре-использовать результаты "когда-то'шних" прогонов, или для совокупления с какими-то другими препроцессорами.

          Проверено на 2.96@RH-7.3 и на 4.7.2@FC18, поведение в этом смысле одинаковое.

        • Любопытно поведение, если скормить файл с кавычками -- '"' в имени: оба m4 (1.4.1 и 1.4.16) и cpp-2.96 молча копируют их в выдачу, а cpp-4.4.6@SL-6.3 уже квотит бэкслэшами.

    По результатам 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: да, делаем:

    1. Переводим ppf4td_peekc() с вызова vmt->peekc() на вызов ppf4td_nextc(), с последующим возвратом символа в буфер посредством свежевведённой UnGetChr().
    2. А плагинный _peekc() оставлен, поскольку он необходим для корректной обработки "#\n" (см. result-замечание за 01-10-2013).

      Но вообще это кривизна: надо б иметь ОДИН вызов "nextc()", а уж с буферизацией разбираться внутри ppf4td. Второй уровень буферизации? Мутновато...

    Проверено -- вчерашняя проблема исчезла.

    26.11.2013: старый за-#if0'енный код удалён.

    16.10.2013: пилим API:

    1. Пытаемся стандартизовать коды возврата -- PPF4TD_OK=+1, PPF4TD_EOF=0, PPF4TD_ERR=-1,

      ...но фиг: у многих функций ==0 означает "окей". Что очевидно конфликтует с EOF, который вовсе не окей.

      Так что пока забиваем и "withdrawn".

    2. Для тонкой модификации парсинга вводим флаги PPF4TD_FLAG_*, передаваемые функциям "среднего уровня" в дополнительном параметре flags:
      1. PPF4TD_FLAG_DASH: разрешение ppf4td_get_ident()'у считать '-' допустимым для идентификатора символом.
      2. PPF4TD_FLAG_nnnTERM, где nnn -- EOL, HSH, SPC: указание ppf4td_get_string()'у завершить парсинг по этому символу. Т.е.,
        • без HSHTERM оно слопает '#' в строку, а не прервётся;
        • без SPCTERM не будет считать разделённое пробелами разными строками;

          18.12.2019: кстати, там чуднАя проверка: что isspace(), но НЕ '\n' и НЕ '\r'. Ведь из каких-то соображений я это делал (причём стоит это всё ПОСЛЕ проверки по EOLTERM). Интересно, зачем?

        • а EOLTERM по факту указывается всегда.

        Эти флаги работают только вне кавычек, внутри же они неважны (об этом подробнее ниже).

      03.12.2013: добавлен еще один флаг-терминатор -- BRC (BRaCe) -- '}', в интересах парсера .subsys-файлов.

      03.12.2013: но вообще модель выглядит всё более некрасивой. Надо либо указывать терминаторы явно, "группами" (либо строкой, либо 256-битовым набором), либо переходить к постулированию парсинга ТОКЕНАМИ, при которых либо закавыченная строка, либо идентификатор [_a-zA-Z][_a-zA-Z0-9]* (тогда всё остальное автоматом будет его завершать). (Хотя что-то еще для чисел надо предусматривать.)

    3. Собственно парсинг строк -- ppf4td_get_string().
      • Концептуальное -- общий подход:
        • НЕ используется ни freeform/shell-like подход PSP (где кавычки/апострофы являются просто "групповыми заковычивателями" и таки группы могут свободно следовать в толпе не-кавыченностей), ...
        • ...НИ подход SMP4TD с избыточной "гибкостью" на основе хитрых комбинаций PMS_SNGQUOT/PMS_DBLQUOT с PMS_SKIPTERM и PMS_SKIP1TERM, и парсеньем специфичностей через PMS_IDENT и PMS_INT.
        • А используется идея
          1. Если первый символ -- кавычка или апостроф, то парсим всё до появления такого символа; после чего считаем парсинг завершенным (автоматом выкинув закрывающую кавычку/апостроф).
          2. Иначе считаем это просто строкой, завершаемой по указанным во флагах терминаторам (и кавычки далее НЕ являются особыми символами).

          (К чему уже известному это ближе, а?)

        • Соответственно, все специфичности (типа ident, int, double) парсятся своими специализированными функциями. Оно так потому, что, в отличие от SMP4TD, где smp4td_parse_mcstr() выполняла собственно макрорасширения, тут на вход получаем уже отпроцессированный поток символов.

        28.07.2014: плохо -- конкатенация невозможна! Надо всё-таки shell-style. Далее см. отдельную секцию за сегодня.

        04.08.2014: да, переделано.

      • Бэкслэш в любом случае считается особым символом и при указании стандартных C'шных символов генерит соответствующие Control-коды.

        Для других же символов он просто вставляет те символы "как есть" -- так делаются и сам бэкслэш, и кавычки, и пробел.

        28.07.2014: не хватает парсинга цепочки \xNN (или \OOO) -- для возможности указывать апостроф, '\x27', с которым сложности из-за синтаксиса m4. Далее см. отдельную секцию за сегодня.

      • Конкретно бэкслэш+NL (NL -- \n либо \r) пропускает следующий символ, тем самым организуя "перенос строк" с удалением собственно CR/LF.
      • Просто перевод строки считается обычным символом (кроме как не-в-кавычках и при указанном EOLTERM) и попадает в результирующую строку.
    4. Для возможности возвращать специфические ошибки -- константы PPF4TD_Ennn как расширение стандартных Ennn для errno. Аналогично CEnnn (с которыми области не пересекаются), это отрицательные коды.

      Плюс функция ppf4td_strerror().

    24.10.2013: вылезло некоторое "неудобство": как-то молча предполагается, что если некий nextc()/peekc() вернул r=0/ch=EOF, то и следующий сделает то же самое.

    Но при pipe-чтении это НЕ ТАК! Первый peekc() отдаст 0/EOF, а второй уже -1 (не вполне очевидно, почему).

    Вывод: надо б как-нибудь запомниать этот EOF, и если он имел место -- то дальше уже его отдавать, без попыток дополнительного чтения.

  • 08.11.2013: отдельная проблема -- с переносами строк ("\NL"'ями): сейчас оно обрабатывается только в 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" из потока должны будут испариться полностью, не дойдя до более высокого уровня.

    Реальные проблемы видятся иные:

    1. Как это будет интерферировать с эккаунтингом? Он ведь единственный, пользующийся vmt->peekc() напрямую, а тут появится второй. Мож, правда нужен второй уровень буферизации (идея от 16-10-2013), чьё содержимое бы (при наличии) использовалось вместо вызова vmt->{nextc,peekc}().
    2. Петрушка с \r и \n -- удалять-то надо обоих.
    3. Конструкция "\\NL" -- как оно будет обработано? По идее -- это просто бэкслэш в конце строки, а потом перенос строки. Но, поскольку разбор остальных \-последовательностей идёт уровнем выше, то будет проблемиссимо.

      А обрабатывать ТЕ бэкслэши прямо на нижнем уровне вроде никак нельзя -- поскольку "там" могут быть иные требования, например, сохранять все бэкслэшности as-is, для передачи куда-то еще.

    Реализовываться должно ПОЛНОСТЬЮ в ppf4td_nextc(), и чтоб наружу (в т.ч. до ppf4td_peekc(), которая сейчас на ней) это даже б и не попадало.

    26.11.2013: сделано, пользуясь свежевведённым _l0.

    Сам кусочек кода очень простой и очевидный.

    По вышеприведённым трём потенциальным проблемам:

    1. Оно идёт ДО эккаунтинга и никак с ним не интерферирует (вследствие нового буфера).

      Единственное что -- оно само делает _curline++.

      Кстати, а cpp склеивает всё в одну строку, а потом вставляет дополнительные пустые строки -- по количеству удалённых NL'ей.

    2. Удаляет как одиночные \r и \n, так и пару \r\n -- это вышло тривиально.
    3. "\\NL" будет уж как есть -- превратится в одиночный '\' и перенос строки удалится.

      Кстати, cpp (gcc-2.96) делает ровно то же самое.

    Теперь вопрос -- а надо ли оставлять свою обработку "\NL" в ppf4td_get_string() (где теперь они уже "следующего уровня").

  • 26.11.2013: реализуем тот самый "второй уровень буферизации":
    1. В первую очередь в интересах "\NL".
    2. Для избавления от отдельного VMT-вызова peekc().
    3. Ради возможности одноразово вычитывать EOF из реального потока (см. проблему от 24-10-2013), а потом уж работать с ним самостоятельно.

    26.11.2013: делаем первую часть -- основную инфраструктуру, чтоб "\NL" отрабатывалось.

    • собственно второй буфер -- _l0buf[] ("Level0", по аналогии с процессорными кэшами).
    • Все прямые вызовы vmt->nextc в ppf4td_nextc() заменены на вызов ReadNextCh().
    • ReadNextCh() при наличии чего-нибудь в _l0buf[] возвращает оттуда, а иначе вызывает vmt->nextc().
    • Т.е., идея такая: оный буфер используется как локальный prefetch-буфер, ТУПО, безо всяких оптимизаций -- туда просто складируются символы, уже вычитанные из входного потока, но оказавшиеся пока не использованными.

      НИКАКИХ оптимизаций не делается (включая отбрасывание лишних \n после \r), а сбагривается всё as-is.

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

    • Парный вызов -- PeekNextCh(). Что заодно позволяет выкинуть vmt'шный peekc().
    • Выпихивание туда -- UnGetL0s() UnGetL0c(), копии обычных UnGet*().
    • Замечание: для избежания бесконечного зацикливания (вычитано/положено-обратно) предполагается, что при возврате в буфер как минимум 1 первый символ всё же должен быть потреблён (отдан юзеру), так что "указатель чтения" сдвинется.

    Проверено -- пашет.

    06.12.2013: just for note -- what's NOT done yet:

    1. Ditch vmt.peekc().
    2. Cache EOF.
  • 28.11.2013: еще одна хотелка -- чтоб клиент мог делать "ungetstring". Смысл -- при опциональном key:val парсинге, когда вплоть до ':' и непонятно, что сейчас считывается (тот самый key, требуемый более верхнему уровню, или уже val, обрабатываемый конкретными парсерами).

    29.11.2013: реализована ppf4td_ungetchars() просто -- ведь внутри уже есть всё нужное в виде UnGetStr(), к которой новорожденная стала тупым переходником.

    Вопрос в другом -- насколько сие корректно с точки зрения размеров буферов? Сейчас 100 символов, попасть туда могут куски максимум длиной с #line либо с ключевое слово из Cdr_db-парсера (около 10 символов). Т.е., сейчас -- окей, но в общем случае ненадёжненько.

    29.11.2013: кстати, еще раз к вопросу об адекватности используемой сейчас модели.

    1. Насколько корректна такая организация КЛИЕНТА, что конкретные кусочки-распарсиватели читают прямо символы/токены из входного потока, а не делается общий анализ, который бы мог при обнаружении отсутствия ':' использовать уже считанный токен в качестве значения?
    2. Учитывая количество этих уровней (внутренний для \NL'я, внутренний для #line, юзерский) не будет ли правильнее сменить организацию с "один умный frontend, учитывающий и умеющий всё" на несколько мелких (каждый делающий свою мелкую работу и более ничего), имеющих одинаковый интерфейс и могущих соединяться в цепочки?
  • 25.07.2014: разобрался в замеченной еще давно неприятности, когда m4-макросы с "\NL" внутри приводят к странным последствиям -- парсер ругается, будто то, что на следующей строке, подаётся ему не как продолжение предыдущей, а как отдельная строка.

    Оказывается, m4 пытается (в отличие от cpp!) блюсти номера строк, и макросы вида

    define(`abc',
           `def \
            ghi')
    
    abc
    
    расширяет не в простое
    def \
            ghi
    
    а в
    def \
    #line 5
            ghi
    
    -- и эта "#line..." сбивает парсер с толку.

    Надо учиться корректно обрабатывать такие фокусы: если после "\NL" идёт #-LINESYNC-последовательность, то проглатывать и еще один NL, не отдавая его наверх.

    04.08.2014: решение оказалось до идиотизма простым и очевидным: сразу после парсинга \NL делается

    ctx->_is_at_bol=1

    И всё. Дальше желаемый эффект достигается автоматически.

  • 28.07.2014: нынешний вариант синтаксиса парсинга строк в ppf4td_get_string() от 16-10-2013 с "Если первый символ -- кавычка или апостроф, то парсим всё до появления такого символа; после чего считаем парсинг завершенным" неудовлетворителен, т.к. не позволяет делать конкатенацию строк, необходимую при использовании макросов.

    Например, приделать в макросе к строке "abc def" еще "xyz" никак не удастся.

    Так что -- надо всё-таки переходить на shell-like вариант, с возможностью множественных закавыченностей.

    04.08.2014: да, переделано. Конкатенация теперь работает.

  • 28.07.2014: надо в строках уметь понимать \xNN -- для возможности указывать \x27: иначе символ апострофа может представлять проблему, из-за специфического синтаксиса m4.

    21.12.2015: сделано. Воспринимается как полный вариант \xNN, так и короткий \xN. Парсинг одного hex-digit'а в сервисной функции getxdigit().

    28.12.2015: добавлена защита от \x0 -- результирующие NUL'ы просто игнорируются.

  • 24.07.2015: есть еще одна проблема -- m4 не поддерживает вещественную арифметику, егойный eval() работает только с целыми числами. Это крайне неудобно при указании {r,d} -- там частенько требуется арифметика.

    24.07.2015: гугление показывает, что рассчитывать на появление вещественной арифметики в m4 не приходится -- надо делать самим.

    Варианты-размышления:

    1. Первое, что приходит в голову -- вставить прямо в ppf4td поддержку чего-то вроде $[ВЫРАЖЕНИЕ].

      Но тут проблема в том, что '$' вовсе не является каким-то специальным символом. Да и нефиг ему таким являться.

    2. В эту сторону единственная лазейка -- '\'. Например, \(ВЫРАЖЕНИЕ).

      26.12.2015: но и тут вопрос -- в каком формате отдавать "в поток" результирующее число, чтоб не было потери точности. А вариант (3) с возвратом именно double этой проблемы лишен.

    3. Другой вариант -- ВЕЗДЕ, где нужны вещественные, пользоваться 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().
      • Работёнка адская -- учитываются все возможные варианты синтаксиса чисел, описанные в man-странице по strtod().
      • Для упрощения работы -- чтобы не заниматься проверками "не ошибка ли, не закончился ли файл, не конец ли строки, не комментарий ли?" -- использован подход, принятый в Cdr_via_ppf4td.c и cxsd_db_via_ppf4td.c: пара функций-helper'ов -- 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" должно идти хоть что-то.

      • Также принято правило, что наличие HSHTERM влечёт EOLTERM.

    12.04.2020: за прошедшие несколько дней допилено до рабочего состояния. Тут основная работа шла уже над реализацией собственно "алгоритма сортировочной станции".

    • Disclaimer: реализовывался ПРОСТОЙ вариант -- не по английской Википедии (там и приоритеры операций есть, и ассоциативность справа-налево (например, для возведения в степень)), а по русской, Алгоритм сортировочной станции.

      И с ним-то возни было о-го-го, а полноценный со всеми фишками -- сил/терпежу не хватило окучивать.

    • Чего НЕ было сказано в описании алгоритма в Википедии (ни в русской, ни в английской; хотя, возможно, в Таненбауме и есть, но я не помню):
      1. Как делать токенизацию -- выделять токены из входного потока.

        Возможно, подразумевается, что там всё разделяется пробелами. Либо что парсить готовую строку в памяти несложно.

        У нас же -- входной именно ПОТОК, так что нельзя произвольно заглядывать вперёд и при надобности сдавать назад, а нужно корректно распознавать всё сразу по мере поступления.

      2. Что ещё сложнее -- и это уже ВПРЯМУЮ относится к алгоритму, но в описаниях просто опущено -- "проверка синтаксиса": что токены идут в правильном порядке, а не будет, например, двух чисел подряд или двух операторов подряд.

        Для этой проблемы придумано решение: булевский флаг num_exp, своей взведённостью указывающий, что дальше должно быть число или открывающая скобка '(', а сброшенностью -- что, наоборот, должен быть оператор или щакрывающая скобка ')'. При несовпадении увиденного с ожидаемым генерится ошибка PPF4TD_EEXPR (см. ниже).

    • В остальном сделано просто по инструкции -- тупо закодирована последовательность действий из алгоритма.
      1. Парсим входной поток вплоть до закрывающей скобки ')'; она является парной к начальной '(', которая и сигнализирует, что это именно формула, а не просто одиночное число.
      2. Выталкиваем оставшееся в стеке в выходную очередь.
      3. "Исполняем" формулу из выходной очереди.

        Исполнение требует наличия стека -- вот и используется ровно тот же 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_test_double.c, являющаяся копией ppf4td_test.c, но воспринимающей симво '=' как префикс числа и вызывающей ppf4td_get_double().
    • У нас в кросс-среде под ARM нет ни math.h, ни libm.*, поэтому при попытке сборки нововведения там получается ошибка.

      Воизбежание всё окружено проверкой "#if MAY_USE_FLOAT", который при неопределённости ставится по умолчанию в 1, а конкретно в hw4cx/x-build/arm-linux/Makefile форсится в 0.

    14.04.2020: сделано и использование в libCdr и libcxsd. Формально это должно бы описываться в ихних разделах, но тема очень близкая к тутошней, плюс оба случая очень похожи и имеет смысл рассказать вместе.

    Пара общих замечаний:

    1. Работка не слишком сложная (не сравнить с предыдущими днями), но нудная -- просто ремесленный труд.
    2. В обоих случаях, чтобы "не сжигать мосты", использование ppf4td_get_double() сделано условным -- по MAY_USE_PPF4TD_GET_DOUBLE, которая по умолчанию теперь 1.

      Из-за большого количества #if'ов код выглядит страшновато, но в будущем, после их устранения, он станет проще по сравнению с прежним.

      09.04.2021: да, до-за-условливался тогда -- в парсинге диапазонов после проверки, что следующий после 1-го числа символ является '-', было забыто его "потребление" вызовом ppf4td_nextc(), из-за чего вылетала ошибка PPF4TD_EFLOAT. Исправлено, теперь диапазоны опять парсятся нормально.

    Итак:

    • Cdr_via_ppf4td.c: троица DBL_fparser(), RNG_fparser(), PARAM_fparser().
    • cxsd_db_via_ppf4td.c: все изменения сосредоточены в 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)".

    • Кстати, для проверки был восстановлен из архива programs/pults/example.subsys, удалённый когда-то из дерева по ошибке.

      Для тестов числа в разных точках его заменялись на скобочные выражения.

    16.04.2020: замечание по мотивам: теперь, по опыту реализации, появилось чёткое понимание того, как можно реализовать парсинг выражений в cda_f_fla.

    20.04.2020: неа, всё совсем не так просто/очевидно. Реализация разбора синтаксиса выражений -- лишь часть проблемы. А нужно продумать весь синтаксис "языка" (там ещё ссылки на каналы; плюс, как увязывать это всё с парсингом команд).

    Да и нет в этом полноценном формульном языке уже такой острой необходимости, как была во времена CXv2 в начале 2000-х: хоть сколько-то сложный скриптинг теперь продвинутые юзеры (ЕманоФедя и Виталя Балакин) делают у себя на Python, а для простых-средних вещей имеющийся ассемблер "_all_code" уже достаточно приемлем.

    Так что оставляем мысль заняться реализацией полноценного языка формул в cda_f_fla и займёмся лучше локингом каналов -- оно намного полезнее и важнее.

  • 19.12.2015: не всегда надо рассматривать кавычки -- '\"' и '\'' -- как открыватели закавыченной строки. Иногда -- конкретно для парсинга auxinfo -- нужно считать их просто обычными символами и складывать в результат "как есть".

    ...чисто теоретически для конкретно auxinfo вроде бы правильный вариант -- использовать не ppf4td_get_string(), а что-то специализированное, этакое "parse_to_EOL". Но тогда теряются плюшки вроде парсинга \-последовательностей.

    21.12.2015: сделано -- добавлен флаг PPF4TD_FLAG_IGNQUOTES (=1<<1, следующие флаги (*TERM) подвинуты).

  • 12.08.2016: есть какой-то косяк с эккаунтингом при разрыве строк \NL'ем: номера строк съезжают "вниз" (вроде номера 105 в файле из 58 строк), что сильно усложняет поиск ошибок, выдаваемых subsys-парсером.

    Наверное, это было внесено либо при добавлении фичи «после "\NL" сжирать также все последующие пробелы...» (26-12-2015...28-12-2015 в разделе по Cdr_file_via_ppf4td), либо при разборках с #line-после-"\NL" (25-07-2014...04.08.2014).

    В любом случае, нужно разбираться -- тупо отлаживать код (а это адская работёнка -- шибко уж он умный/элегантный/неочевидный).

  • 26.05.2018: желательно иметь возможность сказать функциям-добывателям -- в первую очередь ppf4td_get_string() -- "выкидывай результаты, никуда не складывая, и пофиг на длину".

    Нужно для histplot.c, чтения из файла, чтоб пропускать куски текста, не страдая эмуляцией поведения ppf4td в плане кавычек и бэкслэшей.

    30.05.2018: делаем.

    • Флаг PPF4TD_FLAG_JUST_SKIP=1<<31.
    • Пропускание делается просто, в 2 точках: 1) складирование очередного символа в конце основного for() и 2) запись '\0' после него.
    • Добавлено в ppf4td_get_ident() и ppf4td_get_string().
    • Потенциальная (хотя и очень умозрительная) проблема при таком подходе: НЕ сработает цепочка "проверим, читабельно ли (синтаксически) с флагом JUST_SKIP, а потом уже прочитаем реально". Т.к. может вылезти несоответствие по максимальным длинам: при SKIP они не проверяются, и могут быть любыми (например, 100000), а при реальном чтении результат может не поместиться.
  • 28.05.2018@вечер-дорога-домой-около-ИЦиГ/мыши: а нет ли потенциальной проблемы в схеме mem:: и подобных -- что их можно указать из командной строки? А то укажет юзер чёрт-те что, что окажется неким бредовым адресом в программе -- будет SIGSEGV.

    И не сделать ли, чтоб mem:: и подобные НЕ-внешние схемы нельзя было указывать (из командной строки и прочих user-controlled-мест)?

    Например, ввести в ppf4td_vmt_t поле-флажок, означающий "использовать эту схему только при указании её в ppf4td_open().def_scheme, а при указании в .reference чтоб она (get_plugin_by_scheme()'ом?) запрещалась бы.

    29.05.2018: да нет, не нужно.

    Дело в том, что в схеме mem:: строка указывается прямо после "::", и именно в виде строки. Так что указать "чёрт-те что" просто не удастся.

    А вчера у меня в голове, видимо, был вариант либо "mem::0xADDRESS", либо дуплет scheme,path (где path -- тоже адрес).

    Короче:

    • Для каких-то БУДУЩИХ схем проблема, возможно, будет актуальна. И тогда вариант её решения ясен.
    • А для конкретно mem:: она не стоит, поэтому "withdrawn".
  • 02.06.2018: встретился позорный косяк в ppf4td_get_int(): там парсинг выполнялся сразу через _nextc(), вместо надлежащего "_peekc(), и если символ наш, то _nextc()".

    В результате из цепочки "12345.678" первый же вызов сжирал "12345." -- возвращал-то он 12345, но '.' уже исчезала, что ломало всю схему вызывающему.

    Исправлено на надлежащий вариант.

    P.S. Извиняет лишь то, что до сегодня ppf4td_get_int() не использовался вовсе. А сейчас занадобился в histplot.c для парсинга времён -- SECONDS_SINCE_1970.MILLISECONDS.

  • 18.06.2018: и еще прикол в ppf4td_get_int(): числа, начинающиеся с '0', всегда воспринимаются как восьмеричные, но это некорректно: с нолика могут начинаться zero-padded десятичные -- вроде миллисекунд. На них и накололся: в строке вида "123.068", когда числа парсятся раздельно, "068" воспринималось как восьмеричное "06", а '8' его частью уже не считалась и выглядела для клиента как "отдельный, следующий токен".

    18.06.2018: исправляем.

    • Первой мыслью было "ввести флажок, чтоб считать базу за 10".
    • Но более правильно -- как в strtol(), указывать желаемую базу либо 0, означающий "можешь брать префикс".

      Поскольку использование ограничивается histplot.c, то так и поступим.

    • Добавлен параметр defbase.
    • ...и надлежащие проверки: любые префиксы разрешены при defbase==0, а конкретно "0b" и "0x" -- еще и при 2 и 16 соответственно.
  • 27.12.2019: уже с неделю как напрашивается сделать int64-аналог 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".
    • "long long int" заменен на int64.
    • И сама функция переименована в ppf4td_get_quad().
ppf4td_plaintext:
ppf4td_pipe:
  • 06.07.2014@пляж: хочется иметь что-то типа "memory streams", но при этом препроцессировабельное. Смысл -- чтоб иметь интегрированными в клиенте те самые .subsys-файлы (это можно организовывать простеньким конвертером "subsys2c", генерящим C'шные char[]-строки).

    06.07.2014@пляж: реализовать это можно, сбагривая препроцессору (запущенному с параметром "-", чтоб брал с stdin'а) эти интегрированные строки через pipe.

    06.07.2014: а надо ль так заморачиваться? Ведь можно пропустить экраны через препроцессор ПЕРЕД интегрированием -- тогда хватит и нынешнего mem::.

    Разница, конечно, в МОМЕНТЕ препроцессирования (и что уже никакие параметры at run time не подставишь). Но будет ли это хоть где-то важно?

    06.07.2014: одна тонкость -- при таком "препроцессировании заранее" желательно всё-таки включать в ppf4td_mem.c фичу "воспринимать номера строк". А какой вариант -- m4 или cpp?

    Пока сделано m4 -- PPF4TD_LINESYNC_HLIN.

  • 29.07.2014: вопрос -- а как с pipe()'ами под Win32? Создать-то его вроде можно... ...а точно тамошний m4 сможет в pipe отправлять свой вывод?

    Короче -- скорее всего, всё сделать можно. Но может понадобиться Win32-specific реализация ppf4td_pipe.c.

ppf4td_m4:
  • 27.11.2013: конкретно с m4 есть некоторая засада: он при include() НЕ пытается искать файл в той же директории, что исходный файл (а cpp -- ищет).

    Так что надо б ему дополнительно сбагривать ключик -Idirname.

    09.07.2014: да, сделано. При найденности strrchr()'ом символа '/' в reference оно добавляет в командную строку еще {"-I", dirname(reference)} (только "dirname" оно делает вручную).

    06.09.2015: и еще штука: очень некрасиво, что нынче директория *pult/configs/ забардачена файлами *.devtype.

    Вот если б можно было к путям поиска добавлять еще "-Idirname/types"...

  • 27.07.2016: и еще: очень неудобно, что .devtype-файлы приходится держать там же в configs/, забардачивая её.

    А желательно бы для каждого ТИПА файлов (а не схемы препроцессора!) иметь возможность указать include-директорию, чтоб она тоже добавлялась с префиксом -I.

    Кстати, "-I" и путь можно указывать раздельно.

    Проблем 2:

    1. Главная: как эта доп.директория проделает путь от "определения ТИПА" до ppf4td-плагина?
    2. Учитывая склонность m4 НЕ использовать адресацию относительно исходника, придётся приклеивать к тому пути еще и путь к основному файлу (который в 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@дома:

    @утро-душ:

    • Ещё насчёт дополнительных "-I": очевидно, существенная часть проблемы -- не столько в самом добавлении, а в том, как передать список желаемых добавок с самого верхнего уровня -- точнее, из вызванных им функций вроде CxsdDbLoadDb() и CdrLoadSubsystem() -- до собственно ppf4td_open().
    • Также некоторый вопрос всё же технологический: а КАК указывать список дополнительных путей, передаваемый через ppf4td_open() и далее его плагинам:
      1. Одним параметром?
      2. Или этот один параметр должен быть colon-separated-списком поддиректорий?
      3. Или NULL-terminated списком путей?

      Между (b) и (c) выбор простой -- по опыту findfilein(), надо именно colon-separated-список -- с ним намного удобнее обращаться.

      P.S. Можно также ввести некоторый "язык": например, если путь в path_info начинается со '/', то не прицеплять его к basedir, а считать абсолютным; также можно поддерживать префиксы '!', '$' и '~' - как в findfilein()'е, плюс пусть '/' понимает как абсолютный путь.

      ...а с другой стороны -- да нахрена?!

    • Отдельный вопрос -- как внутри плагиновых open()'ов менеджить массивы argv[]-параметров. В частности, каким должно быть ограничение на количество; плюс, как обходиться с буферами:
      1. На каждый потенциальный путь иметь в стеке по [PATH_MAX]-массиву,
      2. или же завести один большой [PATH_MAX*N], в котором по мере надобности занимать место?
      3. Или и вовсе пользоваться malloc()'ом?

      Впрочем, эта проблема снимается, если обойтись ОДНИМ дополнительным путём -- тогда и менеджить нечего и просто заводится в стеке дополнительный Ipath2[PATH_MAX].

    Далее работы в течение дня:

    • Чуть позже: разбираемся.
      • В первом случае это просто -- там ppf4td_open() вызывается непосредственно из cxsd_db_file_ldr.c::file_ldr().
      • Во втором же есть дополнительная прослойка. Но, как показало рытьё -- непосредственно-вызывальщиком является CdrLoadSubsystemViaPpf4td(); вот в него можно спокойно вставлять "добавить в список путей поддиректорию include" (если надо).
      • Во всех же остальных использованиях ppf4td_open() всё очень просто, плюс там НЕ требуется никаких дополнительных include-поддиректорий.
    • Итого: надо начинать с (1) модификации API -- добавления параметра path_info. Т.е., выбран простейший вариант (a) - с одним параметром; при надобности расширить его до (b) -- colon-separated -- будет просто, и изменения интерфейса не потребуется.

      А дальше уже (2) адаптировать к нему юзеров и, в конце, (3) добавить собственно использование.

    • ~16:00: пункты 1 и 2 выполнены. Конкретно, для devlist'ов поддиректорией назначена "types", а для скринов -- "include".
    • ~17:00: и п.3 тоже - пока в ppf4td_m4.c.
    • И в EXPORTSTREE добавлена configs/types.
    • @около НИПСа, ~19:30: Замечание по реализации/идеологии: фломально, эти дополнительные include-diretory-параметры имеют смысл только для конкретных плагинов. Например, поддиректория "types" имеет смысл только для файлов, а если конфиг будет сорситься из БД, то смысла никакого. А передаваться-то значение path_info будет всегда, независимо от схемы!

      Реально это, конечно, вряд ли станет проблемой -- т.к. де-факто у нас не используется ничего, кроме "m4:".

      Но если вдруг дойдёт до РЕАЛЬНОГО распространения иных схем, и несоответствие параметров станет проблемой -- тогда можно как-то тэгировать: например, префиксом "<m4,cpp>" (что будет означать "этот путь имеет смысл для схем m4 и cpp"; пре передаче плагинам префикс, естественно, должен убираться).

    • Проверил -- да, работает!!!

    Теперь надо все Makefile'ы переделать, чтобы .devtype-файлы клались в configs/types/.

    09.01.2020: доделываем:

    • Во всех 3 **/types/Makefile значение EXPORTSDIR переделано с configs на configs/types.
    • И на пульту перетасовываем файлы.

    Замечание: в ppf4td_cpp.c это НЕ скопировано. И за неиспользуемостью оного, и потому, что у CPP несколько иные правила указания include-файлов -- там обычно именно поддиректорию явно указывают в имени.

ppf4td_cpp:
:
ppf4td_mem
  • 07.10.2013: имеется потребность поддерживать "memory streams" -- чтоб источником была некая NUL-terminated (asciiz) строка.

    Требуется это:

    • как для возможности интегрировать, например, subsys'ы внутрь программ,
    • так и для реализации ключика "-e CONFIG_COMMAND" (иначе его с архитектурой "парсенье config'ов через ppf4td" сделать сложно).

    Никакого препроцессирования/макрорасширения в с такими источниками из памяти и не будет, но в большинстве случаев оно и не особо надо.

    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" -- работает.

:
cxldr:
  • 27.08.2008: новый модуль -- "Module Loader".

    27.08.2008: главный пользователь -- CX-сервер, сие есть преемник CXv2'шного cxsd_loader.c. Одной из целей остается так и не реализованная в CXv2 возможность статической сборки, когда все требуемые модули влинковываются статически, а loader находит их у себя в таблице и отдает сразу. Другая "цель" -- что под Win32 он будет понимать .DLL, прозрачно для остальных частей системы.

    И, поскольку у сервера будет компонент "Channel Manager", линковабельный и в монолитных клиентов, то очевидно, что надо сделать этот module loader отдельной библиотекой, доступной не только серверу. Более того - это должна быть useful/-библиотека, без привязки к CX.

    Отдельный вопрос: а может, вообще как cxscheduler - даже не одна библиотека, а несколько вариантов реализации API, для разных платформ? Типа да, но в рамках одного исходного файла, просто как-нибудь (#include+#define из других файлов?) будет отключабельной кусок, обеспечивающий собственно динамическую загрузку.

    А "внутреннее устройство" будет таким: 3 стадии поиска модулей:

    1. По динамической таблице, в которую программа может что-то добавлять @runtime (воизбежание realloc() -- таблица будет связанным списком, с элементами, добавляемыми программой-"клиентом").
    2. По статической таблице, указываемой в момент компиляции либо сборки.
    3. [опционально] Динамическая загрузка.

    ЗЫ: это покамест только записано сюда, но ничего еще не начато.

    ЗЗЫ: там же надо будет как-то указывать список путей для поиска .so'шек, а сам поиск будет на основе vfindfilein()'а.

    27.01.2010: начато реальное создание модуля. Живет в useful/, а назван cxldr вместо cxloader -- чтоб больше отличался от cxlogger'а. Префиксы -- cxldr_ и CXLDR_.

    03.02.2010: первоначальный вариант сделан. Highlights/features:

    • Поиск модуля ведётся сначала по файлам, а потом по builtin-таблице. Сделано так, а не наоборот, чтобы иметь возможность override'ить встроенные модули.
    • Для статически-собираемых программ, чтобы обходиться совсем без 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: доработано:

    • argv0 передаётся.
    • В качестве "указателя на некую структурку" checker'у теперь передаётся просто указатель на контекст, в котором уж всё есть.
    • Параметр "флаги" добавлен, определен 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: да, ОЧЕНЬ НАДО УМЕТЬ ПЕРЕДАВАТЬ ОПИСАНИЕ ОШИБКИ!!!

    Иначе никакая внятная диагностика невозможна...

  • 30.08.2008: кто-то должен держать reference count'ы, чтобы вызывать методы init() и term() при 0->1 и 1->0. Кто и как?

    30.08.2008: Варианты:

    1. Module Loader отдает флажки, говорящие, что надо вызвать init() или term(). Но с term() тут засада - вызывать-то его надо еще ДО dlclose(), иначе модуль будет уже выгружен и вызвать станет физически нечего.
    2. Для каждого вида модулей (драйверы, layers, frontends...) заранее регистрируется "поддержка" - запись-описание, где указаны все параметры и свойства этого вида (fsuffix, symsuffix), и функции-"методы" (checker), в том числе те, которые надлежит вызывать при 0->1 и 1->0 тоже (а они уж вызовут методы из самого модуля - с нужным набором параметров).

    29.01.2010: да, сделано по 2-му варианту.

  • 30.08.2008: идея: а что если добавить доп.шаг при загрузке -- поиск указанного символа dlsym(RTLD_DEFAULT,...)'ом уже в памяти. Тогда можно просто влинковывать модули, нигде не регистрируя.

    30.08.2008: Стоп!!! А насколько это осмысленно? Ведь заранее-влинкованность нужна в основном в статически-собираемых программах, где никакого dlsym()'а не будет.

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

  • 08.11.2008: а не сделать ли этот "Module Loader" как бы объектом? Т.е., чтобы все таблицы были не фиксированными, в одном экземпляре, а ввести понятие "контекста", в котором и будет храниться вся информация -- список статически влинкованных модулей, а также загруженных-сейчас.

    Это может несколько упростить процедуру "указания списка статически влинкованных модулей для конкретной сборки".

    28.01.2010: да, именно так и реализовано, "объекты" имеют тип cxldr_context_t, а определяются и заполняются в программе макросом CXLDR_DEFINE_CONTEXT. Весьма удобно во всех отношениях -- на каждый "подвид" модулей (драйверы, layer'ы, ...) имеется свой контекст.

  • 07.11.2014: идеологическая проблема: поскольку вызов 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().
    • И при ненахождении модуля findfinein()'ом делается *errstr_p=info.errstr, чтобы (буде и ниже среди builtin'ов модуля не найдётся) клиенту было что вякнуть.
    • ...да, errstr_p может быть NULL -- тогда описание ошибки наверх не передаётся.
    • Клиент же при ошибке -- r!=0 и errno>0 -- логгирует либо errstr, либо (при errstr==NULL) strerror(errno).
      • А при errno<0 -- ничего (типа уже слоггено раньше в CxsdModuleChecker_cm()'е, при логической ошибке)),
      • а при errno==0 -- сообщение "symbol NNN not found".

        ...хотя тут бы теперь можно и через errstr передавать сообщение от dlerror().

    • ...вот эта "логика" что как логгировать выглядит несколько слишком запутанной и некрасивой.
    • Как бы то ни было, в cxsd_modmgr.c код вызова cxldr_get_module() изо всех функций вытащен в общую обёртку DoLoadModule() и уж в неё напичканы все эти условия.

    Засим считаем за "done".

  • 01.09.2019@дома: проблема "иногда фиг поймёшь, в чём проблема при dlopen(), если после проблемного .so-файла по пути поиска пробуются другие, и ошибка (ENOENT) возвращается только на последний" решена радикально.

    01.09.2019@дома: всё очень просто: обычно закомментированная (и раскомментировываемая на время поиска ошибок) в dlopen_checker() выдача на stderr результата dlerror() теперь раскомментирована, но сделана условной по переменной окружения $CXLDR_DEBUG, значение которой должно начинаться на "1", "y" или "Y".

    Таким образом, для печати информации по всем пробуемым .so-файлам нужно запускать программу так:

    CXLDR_DEBUG=1 программа...

    Проверено -- ОЧЕНЬ удобно.

  • 15.11.2023: насчёт cx_module.h (как бы не в этот раздел, но другого более подходящего просто нет).

    При компиляции 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: стал разбираться.

    • Гугление "wnarrowing error c++ hex" дало среди прочих пару любопытных тем-результатов на StackOverflow:
      1. «Narrowing conversion error using newer compiler» за 01-06-2021. Там об инициализации char-массивов константами виде '\x80' -- вылазит ошибка "narrowing conversion" (причём там он инициализирует ещё и unsigned char.

        Полезно чтение обсуждения, включая ссылку на стандарт.

      2. «"narrowing conversion" warning for hex literal with no-suffix?» за за 24-01-2019.
    • И оттуда ссылка на сайт cppreference.com на статью "Integer literal", конкретно раздел "The type of the literal".

    Презанимательнеешее чтиво, показывающее кучу дурацких косяков в C/C++. Особенно насчёт char'ов.

    • Вместо того, чтобы создать РАЗДЕЛЬНЫЕ типы "char" (который осмыслен ТОЛЬКО беззнаковый) и "byte" (который может быть и signed, и unsigned), выёживаются с введением новых правил трактовки констант и приведения/promotion'а.

      А ведь '\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". Воплощаем.

    • Изначальное -- в cx_module.h переделываем из int в unsigned int поля cx_module_rec_t:
      1. magicnumber и
      2. version (это за компанию, да и по смыслу -- отрицательные версии незачем).

      Дата на файле была 14-02-2010...

      За компанию и в v2'шном cxdata.h::subsysdescr_t те же поля аналогично.

    • Рядышком в cx_module.h же те же поля в cx_module_desc_t (это "вторая половинка" -- образец для сличения).
    • "Дальний родственник" -- в main_builtins.h присутствует тип-функция init_builtins_err_notifier_t (смысл его описан в этом файле за 24-11-2016, ключевое слово "err_notifier"), и у неё параметр magicnumber, также об-unsigned'нен.

      Хотя оно вроде реально не использовалось, т.к. единственный вызов соответствующего/такого InitBuiltins() -- в cxsd.c, в виде InitBuiltins(NULL).

      ЗЫ: файлы *_builtins.c генерятся скриптом mkbuiltins.sh, в нём реально и "код".

    • И в cxsd_driver.h об-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.%d.%d.%d" и "%d.%d" переделаны на "%u.%u.%u.%u" и "%u.%u" (и даже "%d" на "%u" -- это когда значения поля версии выдаётся).

    Всё собралось. Сравнение логов сборки до этого изменения и после никаких различий не показывает (помимо ухода той ошибки, из-за которой весь этот раздел и появился).

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

sendqlib:
  • 14.12.2009: наконец-то создаём этот модуль -- в первую очередь, в интересах нового CANKOZ-драйвера.

    Живёт оно, как и предполагалось, в useful/. Файлы именуются sendqlib.[ch], используются префиксы sq_ и SQ_.

  • 21.12.2009: делаем потихоньку.

    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.
    • ...а не ввести ли стандартное действие -- SQ_REPLACE_NOTFIRST?

    Еще -- работа с таймаутами покамест сделана напрямую через 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":

    1. Вообще-то и в старом cankoz_pre_lyr просто вставка в очередь по NONEORFIRST была полезна только против переполнения очереди одинаковыми запросами: ведь там сравнение по elem_is_equal(), так что пакет сравнивался целиком (как, собственно, и требовалось).
    2. Проблему же с "SQ_REPLACE_NOTFIRST" можно решить в 2 этапа:
      1. Добавляем в sq_enq() еще параметр -- "model", и в поиске используем его вместо e (а при model==NULL -- model:=e).
      2. В клиенте-библиотеки (в данном случае -- cankoz_lyr) также добавляем в функцию "поставить в очередь (со всеми вариантами параметров)" еще параметр, в одном из вариантов:
        1. Либо так же передавать и "второй" пакет -- для поиска.
        2. Либо просто передавать параметр "количество байт, по которым искать пакет под замену" (практически всегда будет =1), а уж функция сделает копию вставляемого пакета, указав ему в поле dlc минус-количество.

        Второй вариант выглядит предпочтительнее -- поскольку он элегантнее.

    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".

  • 10.02.2010: есть ведь давняя проблема -- что некоторые блоки иногда НЕ отвечают на некоторые пакеты; пример -- kshd485, в момент шагания реагирующий только на "дай-статус", да и у пановского УБСа вчера такое проявилось.

    В таких случаях программа/очередь начинает бесконечно молотить этот пакет, на который никогда не будет ответа, и засим всё зависает -- поскольку следующие пакеты (теоретически могущие вывести из этого тупика) никогда не будут отправлены.

    Можно ведь ввести в 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():

    • Есть подозрение, что не вполне корректно сделана поддержка ONS-пакетов: у них НИКОГДА не будет SQ_TRY_LAST, поэтому:
      1. Они никогда не удалятся из очереди.
      2. Им может попробоваться уставить ДВА таймаута подряд.

      Может, поменять местами сравнения tries_done==1 и ==epp->tries, чтоб у SQ_TRY_LAST приоритет был выше? Да, тогда у ONS-пакетов всегда единственным "разом" станет LAST вместо FIRST, но что в этом плохого?

    • Выходит, что при SQ_TRY_LAST оно даже не пытается сделать паузу-таймаут, а СРАЗУ же переходит к следующему пакету в очереди (если он есть).

    11.04.2014: да, ровно так и есть -- ONS-пакеты не выкидываются из очереди (вылезло на первом же их юзере -- slbpm_drv).

    В качестве решения вместо "поменять местами сравнения" пока что добавлено альтернативное условие для удаления -- tries==SQ_TRIES_ONS. Проблему это решило.

    А симптомы проявления проблемы выглядели неожиданно. Казалось бы, должно было слать этот пакет до посинения (ведь удаления его из очереди нигде не было); но нет -- через какое-то время бардак прекращался, причём некоторые каналы успевали проморгнуть болотным статусом OFFLINE. Как выяснилось, оно и молотило, но из-за бага у Стюфа эта команда (чтение осциллограммы, SLBCMD_RDOSC=0x01) приводила к отправке самых бредовых пакетов, и в какой-то момент по совпадению это оказывалось якобы чтение регистра 0, хранящего сигнатуру "настроенности"; естественно, значение не совпадало, и перед ре-инициализацией устройства делалась также очистка очереди.

  • 11.06.2010: а еще ведь может потребоваться дополнительный per-item-параметр: минимальное время ПОСЛЕ отправки пакета, в течение которого НИЧЕГО НЕ НАДО пытаться слать (аналогично sea_step_t.chkdelay в seqexecauto).

    11.06.2010: реально оно нужно ТОЛЬКО для _ONS- и _DIR-посылок. Смысл -- если мы знаем, что после этого пакета устройство некоторое время занимается своими делами, и ему просто незачем пытаться слать что-то еще. Так что -- можно поступить следующим образом:

    • Поскольку для _DIR- и _ONS-пакетов обычный параметр .timeout_us не нужен, то для них используем это поле как раз в качестве "задержка до следующей посылки".

      По умолчанию оно как раз ==0 -- т.е., задержка отсутствует.

    • А для таймаута-для-перепосылки при ошибке отправки надо вообще ВСЕГДА использовать именно sq_q_t.timeout_us -- т.к. ошибка относится именно к очереди вообще, а не per-item.
    • Потенциальные сложности:
      1. Надо будет ОЧЕНЬ АККУРАТНО "помнить", что за таймаута мы ждем в некий конкретный момент, и "подчищать" информацию (и сам таймаут) как при его истечении, так и при удалении вызвавшего-таймаут-пункта из головы очереди.
      2. Надо бы как-то различать _ONS- и N-tries-пакеты?

    23.06.2010: да, пункт "для таймаута-для-перепосылки при ошибке отправки надо вообще ВСЕГДА использовать именно sq_q_t.timeout_us" исполнен, в рамках реализации предыдущего раздела.

    06.07.2010: да, и концепция "минимальная задержка до следующей посылки" также реализована. Это оказалось сравнительно просто --

    • При наличии у _ENQ_ONS-пакета положительного timeout_us мы этот таймаут уставляем.
    • Аналогично -- в sq_enq() для _TRIES_DIR-пакетов, при условии, что сейчас таймаута нету (это реально подстраховка -- т.к. _DIR-пакеты, да еще и с принудительной паузой, по-хорошему должны бы использоваться только при пустой очереди).
    • Далее -- цикл в perform_sendnext() теперь прерывается не только при опустошении очереди, но и при наличии таймаута.
    • Аналогично -- в sq_enq() вызов sendnext() делается при условии не только прежде-пустой очереди, но и при отсутствии таймаута (но это также перестраховка -- предыдущего пункта по идее достаточно).
    • И, что важно -- никакого «ОЧЕНЬ АККУРАТНО "помнить"» не требуется: во-первых, ЛЮБОЕ истечение таймаута означает, что можно пытаться делать sendnext(), а во-вторых, даже никакого специального флага ("is_pausing") не понадобилось -- достаточно самого tout_set.

    Итого -- теперь осталось только проверить (а на чём? :-)).

  • 28.10.2011@Снежинск-каземат-11: поскольку все "erase-and-send-next()"'ы теперь должны проверять, что в голове очереди -- именно нужый пакет, то им надо получать указатель на 0-й элемент. Это было сделано "руками".

    А надо иметь 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@дома,вечер: (как бы не совсем про sendqlib, а, скорее, про его "клиента" -- nkshd485) по результатам очередной маеты с КШД485, когда если ему послать пакет, на который он не соизволит ответить, то драйвер так и будет его пытаться молотить, так что очередь встанет (и даже STOP не сделаешь). Идея: а можно ль ПОМНИТЬ последние полученные флаги, и в sender()'е при горящем "едем" заменять потенциальные проблемные на KCMD_GET_STATUS. Но не все, а только КОМАНДЫ -- которые НЕ предполагают ответа.

    10.10.2012: обсуждение:

    • Корень проблемы -- в том, что принятие решения "надо ли ставить команду в очередь" производится в момент получения запроса; принять же решение "можно ли команду отправить" можно только непосредственно в момент перед отправкой -- поскольку лишь тогда будет известно текущее АКТУАЛЬНОЕ значение флагов (а с момента постановки в очередь они могут измениться).
    • Почему именно ЗАМЕНЯТЬ, а не просто выкидывать -- потому, что при реакции на запрос чтения флагов, определяется, надо ли ставить в очередь GET_STATUS именно по наличию в очереди команд, возвращающих статус (KCMD_GET_STATUS-KCMD_PULSE).
    • Просто "все" команды заменять нельзя -- только безответные, поскольку иначе канал будет помечен как запрошенный, и с концом.
    • Что касается "в sender()'е" -- конечно же, нет. Поскольку piv485_sender() относится к компетенции PIV485, а заскоки КШД485 -- к его драйверу.

      Так что надо в layer-API вводить callback, вызываемый из sender()'а для проверки -- в нём и будет махинироваться.

    16.10.2012@Снежинск-каземат-11: вроде сделано.

    • В _open() добавлен параметр "mdproc" -- что вызывать перед отправкой пакета.
    • А _sender() его и вызывает. Ему параметрами передаются dlc_p и data[] (а НЕ указатель на 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: проверено -- работает, кнопки и прочее синеть перестали.

  • 02.02.2013: имевшийся закомментированный прототип sq_ers_n_sn() удалён.
  • 19.11.2014: убрана зависимость от misclib.h, так что теперь sendqlib.[ch] является совершенно автономной штукой, годной для выдачи прочим и заливки на сторонние сайты.
  • 26.12.2016: введена sq_pause(). Она очень простая -- одна строчка, просто вызов enq_tout().

    Резоны -- в bigfile-0001.html за вчера и сегодня в разделе "CAN".

  • 12.06.2017: еще мелкое усовершенствование для SQ_TRIES_DIR: теперь оно при успешной отправке вызывает callback, если тот указан (в качестве try_n передаётся SQ_TRY_LAST).

    Сделано для удобства реализации команд работы с таблицами в cankoz-драйверах (конкретно сейчас -- cdac20k). Там и без этого можно было обойтись, но с этим красивее.

    Ну и из общих соображений так правильнее: нет исключений, всегда поведение одинаковое -- вызываем callback после успешной отправки в первый/последний раз.

  • 15.12.2022: любопытный паттерн (осознанный сегодня после разговора с Лёшей Герасёвым), потребный для RS485-устройств от TDK-Lambda, конкретно связкой AsynDriver+StreamDevice, похоже, неподдерживаемый; но вот и sendqlib'ом тоже (а любопытно б было научиться, если это не слишком сложно):
    • Там адрес устройства-получателя указывается не в каждой команде, а ПРЕДВАРИТЕЛЬНО, отдельной посылкой, после которой идут команды БЕЗ адреса, воспринимаемые уже этим самым предварительно-выбранным.
    • А главный "нюанс" -- что после посылки УСТАНОВКА_АДРЕСА нужно выдержать макроскопическую паузу, порядка 1 секунды.

      (Если бы не это -- то можно б было просто слать ВСЕГДА посылки, состоящие из 2 "пакетов" -- УСТАНОВКА_АДРЕСА,КОМАНДА.)

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

    Собственно: а можно ль такое реализовать?

    15.12.2022: вопрос -- КАК?

    1. Простейший вариант -- "виртуально склеивать" 2 посылки: УСТАНОВКА_АДРЕСА и собственно КОМАНДА.

      Т.е., во-первых, после первой посылки делать паузу, а во-вторых -- что и отличает это от обычной работы -- не переходить к следующей очереди данного порта, а слать следующую посылку из текущей.

      Но тут будет дикая неоптимальность: перед каждой командой станет тратиться 1 секунда на переключение адреса.

    2. Более оптимальным будет группировка: после установки адреса проводить всю пачку обменов для него (т.е., проходить по всей связанной очереди), и лишь затем переходить к следующей очереди данного порта.

    Очевидным "победителем" выглядит 2-й вариант, но как его реализовать, чтобы одна очередь не могла бы навечно захватить порт?

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

    Обсуждение:

    • Таким образом, обычно в очереди будут накапливаться "пачки" на вычитывание всех требуемых данных -- либо начальное при старте, либо по-цикловое.
    • Очевидно, необходима оптимизация: если последним использованным адресом был тот, которому предназначена очередная посылка, то повторять установку адреса и паузу незачем.

      Для чего оный "текущий адрес" нужно будет помнить.

    • Следствие -- ЯВНОЕ ПОНЯТИЕ АДРЕСА. КАК?! Получается, что сам sendqlib должен будет заботиться о формировании и отправке пакетов УСТАНОВКА_АДРЕСА, а не его клиент...

      ...или сделать это "плагином" -- набором "методов" в sq_port_t?

    • ТАЙМАУТ -- ВЫБРАСЫВАТЬ И ПЕРЕХОДИТЬ К СЛЕДУЮЩЕЙ ОЧЕРЕДИ ПОРТА.

    Понятно, что реализовать это можно. НО: выглядит монструозненько, текущую хоть сколько-то простую/стройную логику работы sendqlib'а это сильно нарушит (и явно сильнее, чем 10 лет назад при добавлении "портов"). Поскольку бонусов СЕЙЧАС ноль, а усложнение/об-уродливание очень серьёзное, да и не хочется повторять судьбу омонструозившегося AsynDriver, то просто оставим записанным проект, а делать ничего не будем. (Если припрёт -- подумаем ещё раз.)

[serial-scheduler]:
  • 08.06.2010: нужна еще одна библиотечка -- типа "serial scheduler", для работы в паре с sendqlib'ом на COM-портах.
  • 08.06.2010: конкретика и реализация.

    08.06.2010: смысл -- для serial-линий с МНОЖЕСТВОМ устройств.

    Т.е., например, 32 аскольд-волковских контроллера на RS485; для каждого из них есть своя sendqlib-очередь, но линия-то ОДНА и НЕРАЗДЕЛЯЕМА, так что:

    1. Пока один не отправил посылку целиком -- слать нельзя (оно даже не влезет в output-FIFO).
    2. Более того, пока не получен ответ на чью-то посылку -- слать также нельзя, иначе на линии окажется ДВОЕ передающих.

    Таким образом --

    • Явно напрашивается некий "планировщик", который бы "давал" линию всем желающим по очереди, аналогично тому, как это делает планировщик ОС.
    • А "квантом", после которого можно перейти к следующему "процессу", является отправка+плоучение_ответа. Либо, при неполучении -- таймаут.
    • Для полноты картины можно также ввести и приоритеты -- тогда при наличии запросов от "высокоприоритетных" устройств обслуживаться будут в первую очередь именно они, а низкоприоритетные обождут.

    Непонятностей же на данный момент три:

    1. Как именно должна выглядеть эта библиотека и её API.
    2. Как её требования отразятся на функционировании sendqlib'а. Таймауты-то будут в ведении именно "планировщика", т.к. между вызовом "отправки" sender()'ом (являющейся реально лишь постановкой задания сериализатору) и реальной отправкой пакета в линию может пройти произвольное время, а таймаут надлежит считать именно с момента РЕАЛЬНОЙ отправки. 17.05.2012@Снежинск-каземат-11: не-не-не, какая там «вызовом "отправки" sender()'ом» -- нет! Именно ОТПРАВКА sender()'ом и будет настоящей, от которой надо считать время. Но до неё будет доходить дело не сразу после попадания пакета в голову локальной очереди, а когда еще и планировщик допустит до реальной отправки в порт.
    3. И как вообще назвать библиотечку, чтоб и отражало суть, и минимизировать пересечение по начальным буквам с sendqlib'ом и с seqexecauto?

    15.09.2011: по размышлению напрашивается 2 глобальных подхода:

    1. Если хорошенько подумать, то очевидно, что этот функционал необходимо реализовывать непосредственно в sendqlib'е.

      Простое объяснение -- потому что это "натуральное" место для него.

      Более сложное: ключевым является п.2 из вышеприведённого списка -- таймауты. ТУТ НАДО НАПИСАТЬ КАКОЕ-ТО ВНЯТНОЕ ОБЪЯСНЕНИЕ (типа что функционал sendqlib и serial-scheduler слишком взаимосвязан, и попытка его разделить по двум библиотекам приведёт к слишком бардачному и плохоизолированному коду).

      Возможная реализация:

      • Завести в sendqlib'е вторую сущность -- "очередь в порт", и чтоб каждая "обычная" очередь могла связываться с таким портом.
      • В случае с RS232/485 будет очевидное соответствие -- каждому физическому порту сопоставляется "очередь в порт".
      • Соответственно, при необходимости что-то отправить -- отправка делается не сразу же, а эта queue добавляется в очередь своего порта.
      • Более точно:
        • Добавление в очередь порта происходит при добавлении пакета в пустую queue (т.е., переход 0->не-0, от "нам ничего не надо слать" к "нам надо!").
        • Удаление из очереди порта -- при удалении последнего пакета (т.е., переход от "нам есть что слать" к "нам ничего не надо").
      • "Очередь в порт" же пусть циклит по всем своим текущим элементам. Например, так:
        • все "хочущие" помещаются в зацикленный (как? (*)) список,
        • плюс есть указатель на "текущего", кто и получит право послать свой пакет при первой возможности (т.е., по истечению таймаута);
        • после "реализации" права (т.е., после отправки) указатель переходит на следующего.

        (*) Насчет "как" организовывать кольцевой список:

        1. Т.к. операции удаления и добавления выходят очень частыми, то список должен быть двунаправленным.
        2. Поскольку нужно понятие "конец очереди" (куда добавлять очередного желающего), то надо иметь указатели на начало и конец (просто же зациклить желающих между собой -- не удастся). Соответственно, получается ОБЫЧНЫЙ двунаправленный список, с first->prev==NULL и last->next==NULL.
        3. Соответственно, операция "переход на следующего" будет с выделенным случаем:
          next_to_send = next_to_send->next;
          if (next_to_send == NULL) next_to_send = first;
          
        4. Единственное что -- надо будет уделить особое внимание вопросу удаления queue'й: при гроханьи корректно удалять из очереди порта, при этом надлежащим образом переводя next_to_send.

        Замечание: приведённый алгоритм -- это для простой очереди, БЕЗ приоритетов.

      • Очевидно, что такое поведение -- кроме кольцевого цикленья -- очень похоже на работу основного функционала sendqlib'а.
    2. А с другой стороны: может, реально сделать некий "планировщик", по очереди передающий некий "ресурс" желающим? Вопрос тут в основном в разработке адекватного API. возможно, он сам выкристаллизуется по результатам реализации варианта I (поскольку реализация планировщика там на вид совершенно ортогональна основному функционалу).

    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 добавлены поля:
      1. port -- ссылка на оный.
      2. prev_in_port/next_in_port -- менеджмент списка.
      3. 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() -- она делает то же, что и раньше -- но только "результат" возвращает.

      Результат -- это "чем дело закончилось" ("дело" -- отправка пакетов из данной очереди):

      1. ERROR=-1 -- ошибка отправки, запущен таймаут.
      2. PAUSE=0 -- активирован таймаут, больше ничего слать не следует.
      3. EMPTY=+1 -- очередь опустошена,
    • Нынешняя же новая perform_sendnext() принимает дополнительно параметр from_enq -- означающий, что она вызвана из sq_enq().
      • И сей параметр является ключевым:
        1. Если он ==0 -- значит, "можно переходить к следующему по очереди" (т.е., либо ответ получен (erase and send next), либо истёк таймаут (хоть ожидания ответа, хоть паузы после ons'а)).
        2. Если же !=0 -- значит, оно вызвано из sq_enq(), и можно попробовать отправить пакет ТОЛЬКО если сейчас ничего другого на отправку нет (и не ждётся ничего, и в очереди на отправку нет никого -- реально проверка сводится к current_to_send==NULL).
      • Если from_enq==0, то делается еще одна проверка -- что вызывальщик действительно является "держателем порта" (current_to_send==q) -- иначе просто ничего не делается.
      • Затем молотится цикл по очередям -- 1) переход к следующей; 2) отсылка; до тех пор, пока current_to_send!=NULL и не-пауза.
    • ЗАМЕЧАНИЕ: все махинации с current_to_send лежат исключительно на ВЫЗЫВАЛЬЩИКАХ функций add_to_send()/zer_to_send(), но НЕ на них самих.

      Также и "wrap-around" (зацикливание) при переходе к следующему элементу/очереди -- аналогично.

    Мысли по общему состоянию:

    1. Без-портовая реализация была довольно красивой и элегантной (хоть и не самой очевидной). С нынешними же дополнениями оно стало монструозненьким. По факту нынешнее надо б назвать "sendqlib2".

      (Чем-то ситуация напоминает мне душераздирающие гусиные рассказы про обрюзгновение asyn-driver'а.)

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

    2. Причём, нынешняя схема работы еще и не вполне оптимальна: например, может быть ситуация, что блоку X послана некая команда, требующая затем паузы NNNms, а со следующим блоком Y в это время уже вполне можно общаться. Но СЕЙЧАС оно будет честно выжидать эти NNNms, и лишь потом перейдёт к блоку Y! Главное следствие -- ОДИН отсутствующий блок будет дико тормозить весь порт своими ожиданиями ответов, которые никогда не придут.

      Так что, в принципе, можно как-то сильнее химичить с очередью -- например, на время "сна" уводить из очереди вовсе.

    3. Еще проблема/race-condition: если очередь удаляется из порта в тот момент (мало ли -- sato), когда из неё только что было что-то отправлено, то получается гадость: ответ придёт кому-то другому. По-хорошему, надо бы таймаутец выждать.

      А как? При q->tout_set заказать таймаут "следующему" в голове? с одной стороны, он из-за этого пропустит свой ход; с другой -- если заказывать "хвосту", то perform_sendnext() проигнорирует его, ибо не голова. А если никого не осталось, то кому заказывать (ведь может добавиться кто-то позже, и тогда тому, свежепоявившемуся, прилетит "подарочек из прошлого")? Совсем правильно -- надо б уметь заказывать таймаут САМОМУ ПОРТУ. И port_p->tout_set должно быть доп.барьером для отправки. Отдельный вопрос -- СКОЛЬКО выжидать. Ведь сейчас не помнится ни момент заказа таймаута, ни его размер.

      А при удалении мы ставим в качестве текущего не следующий, а ПРЕДЫДУЩИЙ -- тогда хоть сразу, хоть по таймауту очередным станет как раз следующий.

    Но потенциальный улучшайзинг -- потом, может быть.

    24.12.2012@по-пути-домой-по-Лаврентьева-и-мимо-НИПСа: а ведь "таймаут порту" -- это и есть естественное решение проблемы! Только помнить ничего не надо, а просто СРАЗУ заказывать таймаут ПОРТУ. В этом случае дальнейшая судьба очереди-заказчика значения уже иметь не будет.

    "Следствия":

    1. Таким образом можно решать проблему "чтоб блок, заказавший просто паузу, не тормозил весь порт": в подобных случаях заказывать таймаут именно очереди, а не порту. Указывать же на такую принудительную паузу можно отрицательным размером таймаута.

      Но тут будет техническая сложность реализации -- как-то надо эту очередь временно уводить из порта... Ладно, это уж точно "на потом".

    2. К сожалению, такая штука НЕ РЕШИТ проблему торможения порта отсутствующим устройством -- ведь держать паузу в ожидании ответа всё равно придётся.

      Потенциальное решение лежит в иной плоскости: надо при НЕполучении ответа не пытаться сразу же спрашивать снова, а выдерживать паузу -- НЕ равную времени ожидания; например, 10 секунд (как у remdrv).

      И реализация этого решения имеет проблему, в точности аналогичную предыдущему пункту: надо уметь временно уводить очередь из порта.

    25.12.2012: угу -- продолжение деятельности по результатам размышлений:

    • Переименовываем perform_sendnext_no_port() в perform_sendnext_in_queue().
    • Убираем нафиг возврат ею результата -- всё равно он определяем по наличию таймаута (ERROR/PAUSE -- пофиг) или по пустоте очереди (ring_used==0).
    • Добавляем поддержку "таймаутов к портам":
      • В 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-го элемента очереди -- в противном случае таймаут порта также не трогается (именно чтоб остальные таймауты (по удалению устройства и по очистке очереди) оставались в силе).

      • Проверка "а можно ли слать дальше, или выставлет таймаут?" (q->tout_set) дополнена проверкой на уставленность таймаута порта (кроме SQ_TRIES_DIR, использование которой с портами вообще бессмысленно).

    Итого -- предварительно сделано. Теперь надо добавлять поддержку таймаутов портов в nkshd485 и проверять работу.

    27.12.2012: хех -- а ведь таймаутов для layer'ов (коим является порт) у нас в CXv2 не предусмотрено! Придётся делать...

    В nkshd485 поддержка добавлена.

    30.12.2012: поддержка таймаутов добавлена, в обе среды, так что проводим тесты.

    • С одним 485 работает вообще как раньше.
    • С несколькими -- странновато (проверяется на Moxa, с тремя: 1:245, 2:164, 3:246). Вроде всё пашет, но через раз серийники глючат: вместо нужных показывает у второго тоже 245, а у третьего вообще 0. Смахивает на заскоки с диспетчеризацией пакетов...
    • Также по рестарту сервера (или по sato всех сразу) оно иногда замерзает намертво.

    03.01.2013: вроде разобрался с проблемами (с первой -- вчера, со второй, более противной -- сегодня).

    1. Почему путались пакеты. Надо было "получателя" выбирать не по lp->last_kid, а по data[0]. Как следствие -- и last_cmd должно быть в dp, а не в lp (что они оба в lp -- следствие не очень удачного рефакторинга при переходе от старого ISTC'шного kshd485_drv.c).

      Причина всего -- что таймаут может возникнуть не только из-за тормозов устройства, но еще и потому, что сам ДРАЙВЕР/сервер чем-то занят. Вот оно так и выходило -- при запуске 3 драйверов оно просто не укладывалось в 100мс, и сначала отрабатывал таймаут, а потом уж проверялись дескрипторы (где обнаруживался ожидаемый пакет, но отдававшийся уже не тому экземпляру драйвера, который его ждал).

      Это некоторый удар по всей концепции sendqlib'а -- следствие конкретного ТЕКУЩЕГО устройства cxscheduler'а. А как по-другому-то можно сделать?

    2. Почему замерзало. В piv485_disconnect() по закрытию линии делался сброс её таймаута (вот порт и ждал таймаута, который никогда не придёт).

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

    Теперь оно молотит на вид стабильно.

    02.02.2013: за прошедший месяц вроде всё окей, камеры работают. Так что ставим "done".

  • 02.02.2013: поскольку цели раздела выполнены, то, надеюсь, более здесь ничего не добавится, а вся работа пойдёт в "родном" разделе sendqlib'а.
:
Qcxscheduler:
  • 11.01.2015: создана; точнее, скопирована из CXv2.
  • 29.03.2017: Федя очень жаждет иметь сборку отдельно-одновременно для Qt4 и Qt5. Т.е., очевидно, что нужно убирать проверку "а есть ли нужный qmake?" из lib/Makefile в её собственный Makefile, в котором проверять на оба варианта уже отдельно. И как-то разводить сборку ДВУХ target'ов с практически одинаковыми именами из одного исходника.

    30.03.2017: там делать сборку обоих внутри одной директории можно, но мутновато. Поэтому пока сделано в отдельной директории lib/Qt5cxscheduler/, собирается libQt5cxscheduler.a.

    Потом, кстати, надо будет и обычный 4-шный Qcxscheduler складывать в libQt4cxscheduler.a.

    31.03.2017: сделано.

    • Живёт теперь всё в lib/Qcxscheduler/.
    • Qcxscheduler/Makefile стал крупноват --
      • в нём теперь проверки "что собирать, а что нет"; причём куски на тему Qt4 и Qt5 идентичны, отличаются только циферкой; ...
      • зато lib/Makefile от проверок на тему Qt освобождён.
    • Библиотеки теперь называются унифицированно -- libQt4cxscheduler.a и libQt5cxscheduler.a
    • Как сделана сборка двух библиотек из одного исходника:
      • Исходник остался Qcxscheduler.cpp, но он симлинкуется в Qt[45]cxscheduler.cpp.
      • А вот файлы Qt[45]cxscheduler.pro реально разные, хотя отличие внутри только циферкой '4' либо '5' (в двух местах).

    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: сделано.

    • Исходники Qcxscheduler.{h,cpp,pro} переименованы в Qcxscheduler_src.{h,cpp,pro}.
    • Вместо циферки "4" или "5" в них строка _ZZZ_VERSION_ZZZ_,
    • Для генерации Qt[45]cxscheduler.{h,cpp,pro} исходники пропускаются через
      sed -e 's/_ZZZ_VERSION_ZZZ_/$*/g'

    Монструозноватый Makefile получился.

    07.04.2017: ЕманоФедя проверил -- вроде всё теперь собирается и работает хорошо. Засим, наконец-то, можно делать "done".

    22.11.2018: еще модификация: эта, с позволения, "система сборки" переведена на использование софтины qtchooser.

    • Побудительный мотив -- желание Феди.
    • Смысл -- что эта штука как-то умеет работать и в случае "нестандартной" установки Qt, в т.ч. в нестандартную директорию, и оно само умеет узнавать, по какому пути надо вызывать qmake для указанной версии.
    • Там выдаётся хорошо заметный WARNING с объяснением ситуации.
    • Плюс, в случае проблем можно явно отключить сборку Qt'шностей полностью, для чего указать make'у в командной строке NOQT=Y.

      Этот ключ отрабатывается независимо как в lib/Makefile, так и в lib/Qcxscheduler/Makefile.

    • При отсутствии /usr/bin/qtchooser используется старая схема с индивидуальным тестированием на наличие /usr/bin/qmake-qt4 и /usr/bin/qmake-qt5.
4PyQt:
  • 11.01.2015: создана, творческим копированием из CXv2.
    • Сей момент состоит только из Makefile, а cdr_wrapper.py пока нету.
    • Генеримая библиотека называется libcda4PyQt.so (именно cda, НЕ Cdr (как в v2) -- поскольку в v4 доступ к каналам по именам уже нативный в cda).
  • 30.03.2017: уже энное время как не нужна, т.к. используется еманофедина python'овская обёртка.

    Изводим.

vdev:
  • 17.07.2015: приступаем к портированию vdev и vdev-драйверов под v4.

    17.07.2015: соображения и ход работ:

    • Сложности и решения:
      • Базовый постулат -- в v4 ссылки на каналы делаются по именам, вместо номеров. Что и позволяет очень легко и элегантно реализовать всё на cda.
      • НО! Эти самые номера каналов подчинённых устройств (sodc) использовались в v2 для адресации в каналы at-run-time, и на них было многое завязано (в т.ч. importancy и прочие табличности).

        Что делать?

      • Решение пришло само: использовать в качестве этих номеров просто номера соответствующих строк в таблице mapping'а (тип vdev_sodc_dsc_t[]) -- она же "описание свойств каналов"
      • Такое решение очень органично ложится в общую модель: вследствие адресации по именам напрочь отсутствуют "множественные observer'ы", а все subordinate-каналы унифицированы и располагаются в общем адресном пространстве и при желании могут быть даже хоть перемешаны.
      • Кстати, в таблицу маппинга -- первой ячейкой -- добавлена та самая именованая ссылка на target-канал.
    • Работы:
      • Собственно, приступаем. Начаты vdev.h и vdev.c, параллельно делается (скорее даже "копируется/портируется из старого") ist_cdac20_drv.c.
      • ...работа идёт очень легко -- ну и неудивительно, поскольку vdev изначально разрабатывался с прицелом на v4 (из которого и термин inserver появился).
    • Кстати, битым текстом: модель -- с использованием cda и конкатенирующихся имён -- такова, что можно натравливать vdev-драйверы вместо соседних устройств (по insrv::) на устройства в других серверах: достаточно в "базе" указать что-нибудь вида "cx::server:N.device".

    18.07.2015: далее:

    • Несмотря на всю лёгкость, несколько техническо-идеологических проблем всё же просматриваются. Но сразу видны и решения.
      1. Как всё-таки быть драйверам с МНОЖЕСТВЕННЫМИ target-устройствами (v3h_a40d16)? Ведь там вроде недостаточно указания ОДНОЙ базы (читай -- имени устройства) для обоих target'ов.

        РЕШЕНИЕ: возложить эту задачу на devlist, чтобы там устройства alias'ами объединялись в одну "поддиректорию" с фиксированными именами-ссылками на target'ы. Примерно так:

        dev zzz_adc canadc40 ...
        dev zzz_dac candac16 ...
        cpoint v3h_a.adc zzz_adc
        cpoint v3h_a.dac zzz_dac
        
        (фиксированные имена-ссылки -- "adc" и "dac").

        Имена target-каналов будут указываться в виде "adc.mes" и "dac.out".

      2. Для корректной работы желательно всё же знать состояние (devstate) target-устройств. Но, поскольку теперь всё унифицировано без разделения по устройствам, то vdev не имеет никакой возможности понять, какие _devstate спрашивать (и даже определить, сколько вообще штук этих target-устройств).

        РЕШЕНИЕ: а просто явным образом пусть драйвер-клиент указывает список имён каналов (с количеством), которые надо считать за статусы. Для однодевайсных драйверов это всегда будет [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() его каналам); а надо было наоборот. Вчера последствия были осознаны и порядок изменён.

      3. Рано или поздно (нет, НЕ сейчас, НЕ для ist, v1k и v3h) понадобятся и не-int32-каналы -- точнее, а) не только int32, б) не только скалярные, но и векторные.

        ПРОЕКТ РЕШЕНИЯ: состоит из 2 частей:

        1. Дополнительные описатели в vdev_sodc_dsc_t -- в конце, поэтому для обычных каналов их можно просто не заполнять, так что вид исходников не загромождается.

          Указывать там пару полей -- dtype и nelems, которые по умолчанию (по ==0) считаются за CXDTYPE_INT32 и 1.

          10.08.2016: ДА! В pzframe_data именно так и сделано -- dtype==0 считается за CXDTYPE_INT32, max_nelems==0 за 1.

        2. Содержимое vdev_sodc_cur_t: поле val превращается из int32 в CxAnyVal_t, для хранения значений и других типов.

          Несколько замечаний:

          • ПЛЮС: добыча значения от cda будет тривиальной -- просто тупо просим положить по адресу &(ctx->cur[sodc].val) нужное число байт.
          • МИНУС: в драйверах клиентах доступ к текущему станет длиннее -- cur[sodc].val.i32 вместо просто cur[sodc].val.i32.

            Переживём.

            22.08.2016: да аксессоры надо сделать, как в pzframe: vdev_get_int().

          • МИНУС: векторные каналы в такую ячейку всё равно не засунешь. Но для этой проблемы решение будем придумывать по мере надобности (ёлки -- да хоть аллокировать в драйвере/privrec'е буфер, и отвести в vdev_sodc_cur_t под указатель и размер в байтах).

            17.08.2016: собственно -- в pzframe_data оная проблема решена: там аллокируется один общий буфер, объёмом равным сумме всех требуемых размеров, и на его последовательные части расставляются указатели current_val в нуждающихся ячейках.

        16.08.2016: конкретный пример, где оно понадобится -- потенциальный драйвер «процесс "Электронно-лучевая сварка"»: там надо иметь дело с энным количеством каналов-таблиц для козачиных ЦАПов (причём, вероятно, именно цапОВ, а не просто одного девайса).

        05.07.2018: да-да, именно там! Только еще сомневался -- "а не делать ли эту штуку knobplugin'ом?". Нет -- надо именно драйвером, и для него сделать поддержку векторностей.

    • Собственно работы:
      • ist_cdac20_drv.c доведён до компилируемости.
      • vdev.c тоже.

    Теперь проверять, отлаживать, и потом делать остальные два драйвера.

    19.07.2015: v1k_cdac20_drv.c.

    20.07.2015: v3h_a40d16_drv.c.

    • С одной стороны, оно стало проще, чем в v2 -- не нужно никаких трансляций через C3H_nnn(), а вся адресация по фиксированным номерам "виртуальных каналов", берущимся из одной из 8 карт.
    • С другой стороны, эти самые карты пришлось делать вручную все 8 штук -- т.к. в Си нет возможности сгенерить имена, заканчивающиеся на строку-число b+(n)*5.

    14.08.2015: в v2'шном linmagx какие-то странности с отдаваемыми значениями каналов SWITCH_ON/SWITCH_OFF у V3H: почему-то почти повсеместно значения 2 (DISABLED); см. ~/20150814-linmagx-strange-sw_on_off_values.gif. Единственный 0 -- у канала SWITCH_OFF у lens20, которой только что руками было сделано "Вкл".

    • Видимо, придуривается логика отдачи state_related_channels.
    • Рытьё показало, что, похоже, присутствует логический ляп: значения этих каналов ЗАПИСИ отдаются наверх только по прямому запросу -- либо при записи (т.е., команде смены состояния), либо при вычитывании (которое происходит ТОЛЬКО при старте драйвера (или при переходе в OPERATING)). А при просто смене состояния -- по какой-либо внутренней причине -- НЕТ, вот и остаётся старое значение.

      Вообще-то это странно, т.к. в vdev_set_state() (в обоих!) в самом конце есть цикл прохода по state_related_channels и дёрганья do_rw(DRVA_READ) для каждого.

    • У тутошнего v4'шного логика функционирования такая же, так что проблема тоже должна присутствовать.

      01.09.2015: (вечер после пляжа) да, присутствует.

    31.08.2015: глобальное переименование: out,out_rate,out_cur в iset,iset_rate,iset_cur.

    01.09.2015: (совсем вечер, после фиксенья v3h_a40d16) разобрался в проблеме "почему-то почти повсеместно значения 2 (DISABLED)".

    • Причина в том, что ВСЕ vdev-драйверы в своих _rw_p() (вызываемых из vdev_set_state()) при определении "можно(0)/нельзя(2)" вызывали
      sw_alwd(,sdp->state)
      вместо надлежащего
      sw_alwd(,me->ctx.cur_state)

      Т.е., передавали target-state'ову проверяльщику "можно ли перейти в это состояние" в качестве текущего состояния его же само, вместо реально текущего (прикол в том, что проверка "можно ли" при DRVA_WRITE (несколькими строками выше) делалась правильно).

    • Пытаться искать в получавшихся значениях какую-то логику -- вряд ли есть смысл.
    • Исправлено, в т.ч. в старых в qult/drivers/.

    09.09.2015: vdev.c переехала из sw4cx/drivers/ в 4cx/src/lib/vdev/.

    08.12.2015: некрасивость обнаружилась:

    • Если какие-то каналы статуса, получаемые из битов входного регистра, при отдаче наверх должны инвертироваться, то несколько некрасиво, что этим каналам стоит флаг "VDEV_TUBE" -- этак они будут отдаваться два раза подряд (с минимальной паузой), с прямым и инверсным значением.

      (Осознано по результатам анализа кода.)

    • Сейчас таковыми каналами являются биты блокировок у v1k_cdac20 и биты OPR_Sn у mps20_ceac124.
    • Не то чтобы прямо супер-критично (СЕЙЧАС!), но некрасиво.
    • По-хорошему -- таким каналам флаг VDEV_TUBE ставить бы не надо (как и вообще любым, проходящим через обработку в драйвере, а не просто туннелируемым).

    09.12.2015: проверено при помощи "cdaclient -m ИМЯ_КАНАЛА@u" -- да, так и происходит.

    09.12.2015: за вчера-сегодня сделан еще один драйвер -- mps20_ceac124_drv.c (для беликовского MPS-20). Сегодня отлажен и работает.

    Засим можно считать, что модуль vdev.c вполне доведён, так что ставим "done".

    20.03.2016: сделана первая попытка использовать в качестве базового устройства не локальное (insrv::), а удалённое, по протоколу cx::.

    • "Скелет" вроде работает, на вид прилично.
    • Но что-то подглючивает: например, часть каналов вообще не обновляется, пока не нажать что-то, вызывающее смену статуса, да и ist_state стоит =0 (UNKNOWN) -- видимо, потому, что почему-то часть "IMPR"-каналов до него так и не доходит.

      Косяки явно связаны с общим поведением связки 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-драйверах значения используются сразу, а не складываются куда-то, то функция сразу и возвращает значение, а не статус (0:ok,-1:err). В случае ошибки возвращается 0.
      • Вроде бы пора переводить все драйвера на её использование, но...

        Вариант с ней --

        vdev_get_int(&(me->ctx), CN)
        -- на 13 символов длиннее, чем ранее:
        me->cur[CN].val
        в то время как прямая адресация к прямому доступу через val.i32 -- всего на 4:
        me->cur[CN].val.i32

        А если переименовать "val" в "v", то и вовсе на 2.

      • Поэтому в нынешних драйверах оставляем прямую адресацию, хоть это и чревато для не-INT32-каналов (но иных сейчас всё равно нет). После обеда: да, она на v.i32 и переведена.
    • Ну и собственно поле в vdev_sodc_cur_t переделываем: вместо
      int32 val
      теперь
      CxAnyVal_t v

      И vdev_get_int() адаптирована -- там куча условий и длинный switch().

      07.10.2018: только в switch()'е были перепутаны SINGLE и DOUBLE: для первого бралось из v.f64, а для второго из v.f32. Исправил.

    • В самом vdev.c доступ к значениям _devstate-каналов всё же идёт напрямую через 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).

    1. Аллокирование в vdev_init().
      • Делалось по образу и подобию pzframe_data.c::PzframeDataRealize().
      • В 2 прохода:
        1. Просматриваются все каналы на тему "кому не хватает места в vdev_sodc_cur_t.v" и объёмы этих суммируются.

          Если результат ненулевой -- аллокируется vdev_context_t.buf.

        2. В цикле регистрации каналов те, "кому не хватает", заселяются в буфер с прописыванием указателя в ихние vdev_sodc_cur_t.current_val.
      • Ну и собственно регистрируются каналы теперь не с фиксированными "CXDTYPE_INT32,1", а "dtype,max_nelems".
    2. Также туда вставлено корректное освобождение ресурсов -- safe_free(ctx->buf) -- при возврате -CXRF_DRV_PROBL.
    3. Плюс, сделано наполнение vdev_fini() -- освобождение cid'а и safe_free(ctx->buf).
    4. А вот дальше -- в использовании в sodc_evproc() возникла некоторая неопределённость: ЧТО/КАК именно там делать?
      • Как именно спрашивать данные у cda?
      • Надо ли для не-скаляров передавать в sodc_cb() int-значение?
      • И еще: если клиентам vdev'а пользоваться текущими значениями в буферах, то надо бы помнить и текущее значение nelems от этого буфера.

        А поля для этого в vdev_sodc_cur_t не предусмотрено!

      Кстати, конкретно для weldproc_drv.c это всё вообще не требуется (он только пишет в каналы векторов, но не читает их), так что на этом этапе можно просто забить.

      Но так поступать не хочется -- лучше сразу сделать "правильно".

    05.07.2018: доделываем.

    1. Добыча данных.
      • Добавлено поле vdev_sodc_cur_t.current_nelems.
      • Начальная, добывающая-данные, часть sodc_evproc() изрядно усложнилась, с использованием в качестве основы pzframe_data.c::ProcessOneChanUpdate().
        • Собственно добыча dtype,max_nelems (с учётом 0 как INT32 и 1), выбор места для размещения значения, в зависимости от объёма.
        • Вычитывание теперь не фиксированное, а в соответствии с вышеуказанным.
        • Добыча current_nelems, с наложением ограничения "не больше max_nelems".
        • Для TUBE-каналов отдача наверх изменена соответственно.
      • Теперь это обязательно нужно проверять -- в т.ч. чтобы убедиться, что TUBE'инг по-прежнему работает правильно.

    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: кстати, проверил работу не-скалярности:

    1. ЗАПИСЬ -- работает (собственно, на этом weldproc_drv и живёт).
    2. ЧТЕНИЕ -- похоже, тоже. Проверено было полу-косвенно: sodc_cb печатает current_nelems всех сбагриваемых ему каналов, и у козако-табличных выдавались правильные значения: сначала 0, после старта таблицы (т.е., записи чего-то) -- 2.
  • 14.09.2015: в юзерах vdev'а -- конкретно в ist_cdac20 -- обнаружился кривой эффектик: при нажатии на [Сброс блокировок] сами блокировки сбрасываются, а кумулятивные значения остаются гореть.

    Причина очевидна: зануление кумулятивных -- Drop_c_ilks() -- делается ПЕРЕД сбросом самих блокировок, в SwchToRST_ILK_SET(), а после этого успевают придти "новые" значения битов блокировок, еще не сбросившихся, и кумулятивные опять взводятся (да, взводятся только для тех, которые горели, а для погасших (которых не должно бывать, и ради определения которых кумулятивные и делались) так и остаются сброшенными).

    Очевидное решение -- поставить зануление кумулятивных ПОЗЖЕ, через какую-то паузу от команды на сброс блокировок. Такое место -- в точке сброса бита сброса, SwchToRST_ILK_DRP(), поскольку там задержка в 0.5с между состояниями.

    15.09.2015: да, так и сделано.

    21.09.2015: вроде помогло, кумулятивные тоже сбрасываются (с хорошо заметной задержкой).

  • 09.12.2015: общая неудобность у vdev-драйверов: канал "state" у всех них генерит обновления постоянно. Неудобно оно оказалось при отладке, при попытке понять, через какие состояния проходит машина -- между переходами печаталась также толпа дублей.

    А всё потому, что этим каналам никто НЕ ставит IS_AUTOUPDATED_YES (как и многим другим).

    Но, собственно -- для конкретно канала "state" можно ж это делать прямо в vdev_init().

    Сделано.

    21.03.2016: ага, но только теперь во всех графических клиентах каналы "state" горят гусиным цветом -- ведь у них fresh_age-то по-прежнему 5.0с, а обновлений никаких не происходит!

    Так что IS_AUTOUPDATED_YES заменено на IS_AUTOUPDATED_TRUSTED. Помогло.

  • 23.03.2016: является НЕ очень хорошей идеей использовать 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@утро-мытьё-посуды: решение-то очевидно:

    • Надо forget'ить каналы только того устройства, от которого прилетело "OFFLINE". И прямо в самом vdev.c::stat_evproc().
    • А чтобы знать, какой канал к какому подчинённому устройству принадлежит, нужно каждый канал маркировать "номером устройства", соответствующим позиции его "._devstate" в devstate_names[].
      • Поле "номер подчинённого устройства" надо сделать опциональным, после обязательных name/mode/ourc -- туда же, где dtype+max_nelems, но перед ними (всё равно пока не используются).
      • Таким образом, для обычных vdev-драйверов, у которых по 1 подстилающему устройству, там по умолчанию будет 0, что и соответствует единственному их устройству.
      • ...а если надо фичу отключить совсем, то можно указать -1 -- оно не подойдёт ни к какому устройству.

    (Задумался в очередной раз об этой проблеме из-за vepp4_gimn_drv.c, где подчинённых устройств ТРИ -- cpks, cgvi и vsdc2.)

    20.10.2016: делаем.

    • Добавлено поле vdev_sodc_dsc_t.subdev_n.
    • Добавлено специфицирование его в vepp4_gimn_drv.c и v3h_a40d16_drv.c. Чтобы не напутать, техника такая:
      1. Объявляется enum с константами вида SUBDEV_nnn (по штучке на каждое под-устройство).
      2. В hw2our_mapping[] в .subdev_n указываются эти константы.
      3. Они же используются в описании devstate_names[], который теперь не просто {СПИСОК}, а с позиционной инициализацией вида
        [SUBDEV_nnn] = "mmm._devstate"
    • В stat_evproc() при свежеполученном состоянии target'а ==DEVSTATE_OFFLINE сбрасывает "полученность" всем его каналам (т.е., с subdev_n==tdev).

      Вот это -- небесспорное решение, т.к. не до конца ясно, как правильнее поступать:

      1. При ==DEVSTATE_OFFLINE ли, или при !=DEVSTATE_OPERATING?
      2. Или, может, наоборот -- при ==DEVSTATE_OPERATING, чтобы сбрасывалось непосредственно перед очухиванием, как в самом сервере это делается именно в ReviveDev()?

        После анализа кода cxsd_hw.c -- неа, нельзя. Там рассылка нотификации об изменении _devstate -- путём вызова report_devstate() -- выполняется в последнюю очередь, уже ПОСЛЕ ReRequestDevData()+ReqRofWrChsOf(), а драйвер данные может вернуть сразу же, т.е. после изменения _devstate их уже не поступит.

      ...короче -- переделано на !=DEVSTATE_OPERATING.

    • Далее надо бы заиспользовать преимущества нового подхода:
      • Удалить vdev_forget_all().
      • Всехние SwchToUNKNOWN() устранить -- их единственный смысл был в вызове забывания.

      06.06.2024: сделано и то, и другое. Спустя много-много лет...

    • Также вылезли некоторые непонятки при анализе кода vdev.c:
      1. В 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.

        Главная досада -- совершенно непонятна логика этого куска кода.

        13.09.2018: сегодня сие себя проявило -- вылез косяк: у драйвера v5k5045 каналы кумулятивных блокировок (c_ilkN) остались болотными после того, как remote-драйверы в контроллеры были перезапущены.
        • Причина -- что по уходу подчинённого устройства в NOTREADY (по обрыву соединения с контроллером) оно скакнуло в DETERMINE вместо UNKNOWN.
        • А по оживанию всех подчинённых и по приходу последнего IMPR-значения оно в sodc_evproc() уже НЕ шло в DETERMINE, т.к. это происходит лишь при нахождении в UNKNOWN.
        • И, как следствие, НЕ вызывалась отдача текущих значений c_ilks_vals[], выполняемая в SwchToDETERMINE().

        Так что считаем тот "странный пассаж" (унаследованный от 2012 года) косяком и исправляем, добавив альтернативу NOTREADY к предыдущей ветке по OFFLINE:

        if      (subord_state == DEVSTATE_OFFLINE  ||
                 subord_state == DEVSTATE_NOTREADY)
        {
            vdev_set_state(ctx, ctx->state_unknown_val);
        }
        

        Проблема с не-разболотовением кумулятивных блокировок ушла.

        Пара замечаний:

        1. Не этим ли объяснялись некоторые странности (какие?) в поведении разных vdev-драйверов (каких?)?
        2. Надо внимательно смотреть, не аукнется ль данное изменение -- вдруг вылезут какие колёса.
      2. Также не очень понятно использование поля chan_state_n:
        1. Оно вроде должно указывать номер канала, в котором возвращается номер текущего состояния машины состояний. И в vdev_set_state() именно это и делается.
        2. Но в stat_evproc() почему-то в этом канале возвращается свежевысчитанный devstate!

          Чего в v2, кстати, НЕ было!

          Когда и зачем сие появилось?

        "Наверху" эта проблема не должна быть заметна -- обычно сразу после такого "счастья" должен был идти vdev_set_state(), возвращающий правильное значение.

        Может, это когда-то было сделано для целей отладки -- смотреть так внутренний параметр, при помощи "cdaclient ....ist_state@u"?

        После обеда: раскопки по архивам показывают, что этот пассаж появился в интервале 27-03-2016...29-03-2016. Одновременно с внедрением поддержки timestamp'ов.

        Считаем, что оно было для отладки, и закомментировываем.

    20.10.2016@по-пути-на-обед-около-столовой-АСТС-N6: а ведь переход в _STATE_UNKNOWN никогда не происходил!!!

    • Причина -- подчинённые драйверы никогда не могли уходить в OFFLINE, а только максимум в NOTREADY. Причём и в v2 тоже.
    • Единственное исключение -- фатальная ошибка, вроде WRONG_DEV, но после неё восстановление всё равно невозможно, так что этот сценарий можно игнорировать.
    • Следовательно -- SwchToUNKNOWN() никогда никем не вызывалось, и vdev_forget_all() тоже.
    • Вот почему в v3h_xa40d16/v3h_a40d16 никогда никаких глюков и не было -- а они неизбежно должны были бы быть, ведь при включении питания "OPERATING" от под-устройств приходит по очереди, и каждый OPERATING предваряется NOTREADY (см. cankoz_lyr_common.c::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".

  • 23.03.2016: надо бы в vdev_sodc_cur_t добавить поле timestamp (ts), чтоб каналы как минимум TUBE'ились бы более корректно (в sodc_evproc() под это уже давно заточка есть).

    29.03.2016: делаем.

    • Поле ts добавлено.
    • Заполнение его в sodc_evproc().
    • ...и инициализация в NEVER_READ в vdev_init().

      (Плюс туда же в vdev_forget_all(), но эта функция сама по себе сомнительна -- см. предыдущий пункт за 23-03.2016.)

    • И собственно туннелирование в sodc_evproc(), с использованием свежевведённого в этих целях ReturnInt32DatumTimed().
    • И в драйверах код "ручного tube'ing'а" переведён на него.

    30.03.2016: вроде работает как положено, "done".

    15.09.2018: оно РАБОТАЛО как положено, пока было поверх remdrv'шных. А при переходе на локальные @sktcankoz -- вчера вылез косяк:

    • Некоторые каналы в интерфейсе навечно выглядели посиневшими (DEFUNCT); у них были timestamp'ы NEVER_READ -- хотя значения верные.
    • Проблема вылезла в первую очередь на ist_cdac20, а каналы -- iset_cur, do_calb_dac, digcorr_mode. Т.е., TUBE'имые каналы ЗАПИСИ. Т.е., те, которые, будучи один раз когда-то прочитаны, далее не запрашиваются (а вот запись, кстати, от посинелости избавляла).

    Нудное разбирательство показало, что проблема --

    • в vdev_init(), в том, ГДЕ делалась инициализация ts=NEVER_READ: она делалась ПОСЛЕ cda_add_chan().
    • А из-за локальности подстилающего драйвера -- и того, что он в devlist'е раньше -- к моменту регистрации insrv::-sodc-канала он уже жив и "текущее значение" отдаётся
    • ...причём "готовы" ВСЕ каналы, в т.ч. и те, которые реально еще не прочитаны из железа -- потому, что CAN-драйверы при успешной инициализации сразу возвращают DEVSTATE_OPERATING, несмотря на не полученный ещё ответ на 0xFF.

      Причём включая DIGCORR_MODE, который должен бы вначале вычитаться из железки. Это из-за перемудрённости/ляпа в общей архитектуре CAN-драйверов: DIGCORR_MODE относится к CONFIG-каналам (и в SUPPORTED_chans в его позиции стоит 1) и потому ВСЕГДА отдаётся из кэша.

    Исправление тривиально -- инициализация ts'а переставлена в точку ПЕРЕД регистрацией канала.

  • 08.04.2016: сегодня при разгребании последствий ночной просадки электричества -- когда сдурел один CAN-блок в Беликове (и намертво забил линию) -- почему-то драйверы v1k_cdac20 НЕ восстановили работу при очухивании CDAC20 (которым жались кнопки Reset). Пришлось перезагружать CANGW, только тогда всё пришло в норму.

    Кто виновник -- неясно (не было возможности разобраться). Потенциальные претенденты --

    1. Сам vdev. Как бы? Неправильно ловит изменение статуса?
    2. CANGW'шный cankoz_lyr либо remsrv?
    3. Драйверы (cdac20_drv)? А они как могут добиться такого эффекта?

    10.04.2016: сегодня опять была просадка.

    • И конкретно cangw-magsys (видимо, включенный в UPS) НЕ перегрузился, но что-либо на CAN-линии видеть перестал (точнее, какие-то изначальные пакеты 0xFF в логах в районе 16:04 видны, а потом -- пустота).
    • Запущенный на 1-й линии c4lcanmon показывал бегающие по линии пакеты (от успевших быть запрограммированными на периодические измерения CANADC40 с id 54 и 56.
    • Но сам v4c4lcanserver НЕ видел ничего -- ни ответов на даваемую ему по telnet'у команду "ff"; ни ответов на k0/b:0xff, отправленную монитором с can1, ни впрямую посланного через can1 "поддельного" пакета /r:0xff.

    Таким образом, с vdev обвинение в данном случае снимается. Возможные версии:

    1. Сдурел драйвер can4linux в CANGW. Возможно, не выполнил какую-то ре-инициализацию чипа, пока не переоткрыли /dev/can0 (что произошло после рестарта v4c4lcanserver).

      Но раньше-то, с v2'шным, таких косяков вроде не замечалось. Поэтому еще вариант --

    2. Глюк в cankoz_lyr_common.c, как-то вкравшийся при переходе с v2 на v4. Но о-о-очень сомнительно.

    Если дело в требуемом reset'е чипу SJA1000 -- то, теоретически, в can4linux.h есть некая CMD_RESET, выполняемая через ioctl(), которую можно б подавать через консольный интерфейс -- для чего придётся расширить сам API canhal.h.

  • 10.04.2016: еще насчёт vdev-драйверов, по результатам той просадки: вылезла пара сложностей с v3h_a40d16:
    1. Как-то оказался в установленном состоянии бит A40_SW_OFF (бит УРа в АЦП, в состоянии "0" говорящий "Выкл", а в "1" -- не-выкл). И драйвер никак не позволял его сбросить.
    2. Как-то оказалось, что драйвер считает девайс включенным (видимо, бит OPR горел), но реально в ЦАПе прописан 0...

    10.04.2016: главное, конечно -- неясно, как вообще такая ситуация могла возникнуть:

    1. Откуда после броска питания в CANADC40 оказался горящим бит 1?
    2. Откуда взялся горящий бит OPR? ...хотя как раз это вполне можно объяснить косяками ВЧ300.

    09.01.2017: еще косячок с тем же v3h_a40d16 (2!):

    1. После очередного рестарта сервера оказалось, что у троицы линз -- QL27,QL28,QL31 --
      1. в ЦАПе значение выставлено, а в драйвере источника считается 0,
      2. и сам источник считается выключенным (и это верно -- оба бита (SW_ON и SW_OFF) в положении 0).
    2. И драйвер НИЧЕГО сделать с источником не позволяет!

    Обсуждение:

    • С пунктом 1 всё ясно -- это явно следствие некорректного вычитывания при старте подчинённых каналов записи (баг, найденный 05-12-2016 и исправленный 07-12-2016).
    • А вот с п.2 сложнее:
      1. Неясно, как вообще такая комбинация в ЦАП попала -- выставленное напряжение, но сброшенный бит SW_ON.

        Видимо, [после некорректного вычитывания каналов записи] на какой-то стадии щелканья командами драйвера v3h он почему-то сбросил бит SW_ON, а потом уже не смог перенести присутствующей в D16_OUT_CUR ненулевой уставки.

        ...хотя анализ кода не дал ни единой точки, где б такое могло случиться: все записи в D16_SW_ON -- только в процессе включения/выключения, до которых вроде дойти не должно было (а переход в INTERLOCK -- SW_ON сбрасывается одновременно с записью 0 в уставку (как и в SW_OFF_DOWN)...).

      2. Хуже то, что вылезти из этой ситуации драйвер не позволяет НИКАК.

    "Идея":

    • А что, если в DETERMINE считать ненулевое значение D16_OUT_CUR за состояние включенности?
    • Неа, не прокатит: бит OPR-то ==0, а при этом _sodc_cb() сразу же перейдёт в SW_OFF_DOWN).
    • ...Оно, конечно, даст нужный результат -- при выключении в ЦАП будет записан 0, но как-то это кривовато. Да и некрасиво, что прямо при запуске драйвера попрёт запись всякого.

    10.01.2017: еще идея:

    • В лоб -- добавить специальное состояние "INITIALIZE" (с соответствующим каналом-командой и кнопкой в окне управления)?
    • Можно, конечно, но тоже ж криво...

    Часом позже, ~10:00, после анализа кода драйвера: а можно лучше --

    • В IsAlwdSW_OFF_DOWN() "разрешать" также и при ненулевом значении D16_OUT_CUR!
    • Вот ЭТОТ вариант выглядит максимально красиво: и в общую архитектуру укладывается (там, кстати, уже разрешается при просто не-нулевом значении D16_OPR), и никаких состояний/каналов добавлять не надо -- т.е., изменения только в РЕАЛИЗАЦИИ драйвера, а интерфейс не трогается.
    • Неприятно же то, что повторное нажатие [Off] в процессе снижения значения в ЦАПе будет снова и снова переводить в SW_OFF_DOWN и потом в SW_OFF_CHK_I, но это практически "no-op", так что на лёгкую некрасивость можно и забить.

    Так и сделано, во всех 4 драйверах. Теперь проверять.

    10.01.2017: к вопросу об "откуда взялся бит регистра в не том состоянии": поговорил с Козаком. Результаты:

    1. По кнопке Reset читаемые из регистра значения нулятся, а реально отрабатываемые -- нет, остаются прежними.

      Причина -- регистр отрабатывается отдельной (от процессора) Альтерой, и в неё по включению/reset'у запись не делается. Да, баг, но сейчас его исправлять смысла нет -- ибо CANADC40/CANDAC16 очень много в разных местах, и уж пусть лучше везде будет одинаковое косячное поведение, чем везде разное.

    2. Также чисто ТЕОРЕТИЧЕСКИ в регистрах может оказываться всякий бред в случаях плохого питания.

    05.10.2017: позавчера опять был такой косяк после просадки, а сегодня снова, причём сразу на нескольких устройствах.

    • Симптомы: драйвер ЦАПа считает, что там в OUT0 стоит некое значение (0.9V, 6.75V, ...), но из канала контроля ЦАПа (ADC5) читается примерно нулевое значение, и все биты входного и выходного регистров нули.
    • Предположение: возможно, при просадке питания CDAC20 сбросился, но пакет 0xFF до контроллера не дошёл.

      Получасом позже: обсудил с Козаком, посмотрел на логи -- не-а, не было никакого 0xFF'а. Ведь АЦП-то продолжил работать, обмерять и присылать данные, а при перевключении блока он бы отрубился и драйвер определил бы это через минуту и отправил 0xFF.

      Видимо, всё же просто взглюк из-за просадки: АЦП остался жив, а ЦАП сбросился, но драйвер этого не узнал и продолжал думать, что выставлен не 0.

    • Идея 1 (аналогичная "INITIALIZE" от 10-01-2017): сделать еще один канал "SET_ZERO", в который писать по специальной кнопке [Z], чтоб оно переходило в соответствующее состояние и принудительно писало бы C20C_OUT:=0.

      Плохое решение: забардачивает интерфейс, требует неочевидных действий от оператора. Withdrawn.

    • Идея 2 (придумана совместно с Федей): прямо в SwchToDETERMINE() пытаться отлавливать такие "неправильные" комбинации значений и стараться сразу же исправлять ситуацию -- например, писать в ЦАП 0.

      Поскольку переход в DETERMINE происходит и по кнопке [R] (RESET_STATE), и при старте драйвера, и при очухивании подчинённого устройства, то лечение получится максимально всеобъемлющим и удобным.

    05.03.2017: в продолжение ситуации от 09-01-2017 "в ЦАПе значение выставлено, а в драйвере источника считается 0", или "выставленное напряжение, но сброшенный бит SW_ON": сегодня наблюдал её возникновение воочию.

    • Ситуация:
      • Велись разборки в ИП2 с глючащими линзами QL24-QL31.
      • Сначала там виновником оказался CANDAC16, но потом его заменили на исправный (хотя и не сразу -- первый резервный с полки оказался тоже дефектным: у него почему-то не работали часть битов УРа и каналов ЦАПа -- как будто наружу выходили нули (контакта нету?)).
      • А потом разбирались с дуреющей QL25. В конечном итоге причиной оказался неисправный БП, но по ходу вылазло собственно "оно".
      • При подъёме тока срабатывала блокировка.
    • И в какой-то момент, имеючи открытое окошко [...] ("QL25: Extended controls"), я в нём увидел, что драйвер ВЧ300 вроде как перешел в состояние Off/Interlock, но уставка в ЦАПе (поле "Set, V" в под-панельке "CANADC40+CANDAC16) осталась ненулевой!
    • Т.е., как будто вина ДРАЙВЕРА v3h_a40d16: словно он в какой-то момент переходов между состояниями "забыл" отправить в ЦАП значение 0 -- сделать
      SndCVal(me, D16_OUT, 0)
    • Как такое может происходить -- загадка. В коде вроде везде оное нуление стоит.
    • Гипотеза (выстрел наугад): ведь не очень ясно, как сами "мозги" ВЧ300 себя ведут в такой ситуации: они могут
      1. Зажигать бит ILK.
      2. Гасить бит OPR.
      Причём -- в любой последовательности.

      Нет ли тут какой-то тонкости/засады?

      Сходу при просмотре кода -- нет, на вид всё корректно: нуление есть в реакции на оба события.

    • Как будем разбираться -- похоже, надо изготавливать симулятор: devlist с 3 устройствами (ADC40,DAC16,V3H), где вместо реальных "железок" будет использоваться драйвер noop; только каналы out_cur* и битов СДСа нужно будет смаппировать на каналы записи (4-го девайса, еще одного noop'а?), чтоб в них иметь возможность писать.

      И нарисовать скриптик, пишущий cdaclient'ом команды v3h'у, а потом имитирующий "отработку" записью в симулируемые каналы обратной связи.

    13.03.2018: на эту тему есть отдельный раздельчик, начатый 12-02-2018.

    И возникло подозрение, что дело в использовании _forget_all(), могущего вызвать проблемы на многодевайсных vdev-драйверах -- коим (единственным!) и является v3h_a40d16.

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

    Дальнешие записи, очевидно, уже в том раздельчике.

  • 25.05.2016: добавлено указание флага CDA_DATAREF_OPT_ON_UPDATE при регистрации каналов.

    Не с его ли отсутствием (т.е., обновлениями всего скопом "в конце цикла") были связаны проблемы за 20-03-2016 ("попытка использовать ... не локальное (insrv::), а удалённое, по протоколу cx::")?

  • 27.06.2016: "обнаружился фееричный баг" (в который раз уже, а?) -- время от времени взмаргивали красным все скопом каналы блокировок всех В1000 (v1k_cdac20). Замечено это было после просадки по воде, так что внимательно всё рассматривалось.

    Причём кумулятивные блокировки -- НЕ подсвечивались. Чего, по идее, быть вообще ну никак не может.

    Часок посидел, покумекал -- догадался, в чём дело: у В1000 блокировки "инверсные" (0:проблема, 1:OK), а они всё равно TUBE'атся, что криво (еще с v2 это было ясно!); вот и получался race condition.

    27.06.2016: что, видимо, происходило:

    • vdev.c::sodc_evproc() по R_UPDATE делает следующее:
      1. Складывает значение в cur[sodc].val.
      2. Для TUBE-каналов -- также сразу Return этого значения.
      3. Вызывает sodc_cb().
    • v1k_cdac20_sodc_cb():
      1. Инвертирует значение.
      2. Return'ит это значение.
      3. НЕ складывает его в cur[sodc].val (да это бы и не помогло -- всё равно race бы было, см. ниже).
    • v1k_cdac20_rw_p() для TUBE-каналов просто молча возвращает текущее значение из cur[sodc].val.
    • Сценарий возникновения косяка, очевидно, был такой:
      • Приходящее значение хоть и возвращалось вначале инверсным, но тут же заменялось "правильным", и в Chl-клиентах это замечено быть не могло никак (т.к. два возврата шли неразрывно, а не-@u-каналы отдаются клиенту в конце цикла).

        ...а вот cdaclient'ом, с ключом "-m" и суффиксом "@u", эти "1, потом сразу 0" отлично видны.

      • Проходил целый цикл, делался новый запрос -- и, поскольку каналы блокировок НЕ были помечены как "IS_AUTOUPDATED", то им делался DRVA_READ запрос, на который тут же отдавалось "1".
      • Конечно же, где-то рядом с этим (то ли до, то ли после, но "вместе с" -- в рамках BeginOfCycle() -- слался запрос на чтение регистров.
      • Однако, очевидно, он так и не успевал вернуть результат в течение 200мс цикла, и клиенту отправлялось неверное значение "1".
    • Конечно, неясно, почему такой эффект не замечался раньше и стал очень часто проявляться сегодня. Возможно, какие-то неполадки с CAN-линией, из-за чего ответы прибегали поздно.

      Но, в любом случае, лазейка для ошибки присутствовала.

    Вывод: флаг TUBE можно ставить только тем каналам, значения которых действительно отображаются между аппаратным и vdev-каналом 1-в-1. И ни в коем случае НЕЛЬЗЯ помечать так каналы, проходящие какую-либо трансформацию.

    Что сделано:

    • У каналов _ILK_* убран флаг TUBE.

      И номера соответствующих им V1K-каналов убраны, заменены на -1 (хотя это при отсутствии TUBE уже неважно).

    • Они добавлены v1k_cdac20_rw_p() с реакцией "не делать ничего, будут вёрнуты из sodc_cb()".
    • ...а вот каналов вроде того же ISET_CUR и измеряемых там, в отличие от ist_cdac20_drv.c (ISET_CUR, DCCT1/DCCT2), так не игнорируется.

      Потому, что в В1000 они (в отличие от ИСТа) никакой спецификой не обладают и просто TUBE'атся.

    • Также все блокировки -- и обычные, и кумулятивные -- помечены как IS_AUTOUPDATED_YES.

      И в соседних драйверах тоже.

      По-хорошему, это бы надо сделать и со всеми OPR-каналами, но лень.

    • ...кстати, ровно такой же косяк был в mps20_ceac124_drv.c с каналами OPR_Sn, они тоже инвертированные.

      Сделано то же самое.

  • 09.08.2016: попытка использовать vdev для простых драйверов, БЕЗ состояний -- просто как упрощатель работы с cda/insrv, в т.ч. умеющий корректно обращаться с _devstate подчинённых устройств (примерно эти функции в v2 выполнял подмодуль "observer" (собственно, и в v4'шном vdev.h первая часть ровно так и помечена)).

    Нужно это в интересах свежевводимых драйверов для тех систем, что в v2 делались на клиентском уровне -- lebedev_subharmonic, lincamsel, linsenctl, turnmag (вот в этом состояния могут и пригодиться), ringcamsel.

    09.08.2016: типа протокола:

    • Тренируемся на lebedev_subharmonic, под именем zebedev_subharmonic_drv.c.
    • Первая замеченная особенность -- что для не-state-драйверов совершенно ненужно функционирование vdev_work_hbt().

      Соответственно, теперь первоначальный его вызов делается только при work_hbt_period>0.

    • Более серьёзно то, что обращение к таблице состояний -- state_descr -- делается безусловно, без проверки, что state_count>0.

      Оная проверка добавлена в sodc_evproc() и к условию из предыдущего пункта (для защиты содержимого vdev_work_hbt(), лазящего в state_descr без проверок -- ибо это и есть его работа).

      Еще таблица используется в vdev_set_state(), но там своя проверка в самом начале.

    • (10.08.2016) еще добавлена дополнительная проверка на значение 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: далее:

    • Связанная потребность: нужно уметь указывать -- в auxinfo -- имя ЕДИНСТВЕННОГО target-канала.
      • Требуется для наипростейших драйверов, которые, тем не менее, могут быть не просто тривиальными преобразователями (хотя даже для тех нужно "туннелирование" _devstate), а добавлять какой-то свой функционал

        Конкретно -- lebedev_linsenctl, который в дополнение к инвертированию единственного бита УРа также поддерживает канал "осталось времени до окончания езды".

      • Собственно, проблема в том, что в vdev_sodc_dsc_t указывается ИМЯ КАНАЛА ОТНОСИТЕЛЬНО БАЗЫ (...БАЗА.КАНАЛ). Но в данном случае мы заранее имя канала не знаем (оно может быть ЛЮБЫМ битом УРа (outrbN), а хотим, чтобы прямо оно и указывалось в auxinfo.

        Технически решение несложно: в vdev_init() при регистрации канала в качестве (base,spec) вместо обычного (base,map[sodc].name) указывать ("",base) -- это даст ровно нужный эффект.

      • Вопрос в том, КАК УКАЗЫВАТЬ vdev'у на необходимость такой перестановки.
        1. Очевидная идея -- добавить какой-нибудь mode-флажок, вроде "VDEV_SINGLE" или "VDEV_DIRECT".
        2. Но на прогулке пришла мысль поэлегантнее: достаточно просто указывать пустой name -- "".

        Вот по второму варианту и сделано.

      Проверено -- работает (на симуляционном target-драйвере).

    Собственно, основная тема тоже проверена и тоже работает.

  • 25.09.2016: вылезла неприятность с каналами C_ILK на всех ist-подобных драйверах: они с недавних пор стали после рестарта контроллеров делаться SFERR'ного цвета и оставаться такими практически навечно.
    • Почему такое происходит -- понятно: по рестарту контроллеров драйвер вслед за sobord-драйвером уходит в NOTREADY и сервер его каналы о-CXRF_OFFLINE'ивает.

      А _C_ILK-каналам стоит IS_AUTOUPDATED_TRUSTED, поэтому они обновляются очень изредко, и так и останутся SFERR'нутыми до загорания настоящей блокировки.

    • Почему раньше проблемы не было -- тоже ясно: раньше им ReturnType не делалось и каналы просто ежецикленно опрашивались, честно возвращаясь из _rw_p(). Лишь ~05-07-2016 появилась уставка IS_AUTOUPDATED_YES, в промежутке 12-09-2016...19-09-2016 смененная на _TRUSTED (видимо, как раз из-за каких-то таких проблем (или ради корректности); увы, не записывал...).

    Решение -- вертать их значения по "оживанию". Оным оживанием проще всего считать вход в состояние DETERMINE. Вот в SwchToDETERMINE() и вставлен возврат текущих значений, после чего всё и заработало.

    ...а изначально был сделан глупый поступок: в DETERMINE вставлено уставление c_ilk*:=-1, чтобы типа точно не равно следующему приходящему значению блокировок. Но забыто, что обновление _C_ILK-каналов -- КУМУЛЯТИВНЫХ! -- делается только по val!=0, вот то :=-1 никакого эффекта и не давало :)

  • 26.10.2016: очередной "феерический" (реально -- идиотский) баг.

    Обнаружилось, что vdev-драйверы почему-то постоянно отдают свой статус (*_state -- номер состояния state-машины). Несмотря на то, что вроде бы в vdev_init() делается SetChanReturnType(,chan_state_n,IS_AUTOUPDATED_TRUSTED) (делалось специально и отдельно -- см. за 09-12-2016).

    Разборка показала, что:

    • Проблема в guard'е при этом TRUSTED: проверяется, что chan_state_n -- валиден, т.е., >=0 и <"количества каналов". Но только вместо "количества каналов" (которого НЕТУ!) используется state_count -- количество состояний, НИКАКОГО отношения к количеству каналов не имеющее!
    • И:
      1. А добавлен этот доп.guard был только в v4 -- в v2 проверялось лишь на >=0.

        И добавлен конкретно 10-08-2016 (об этом даже запись есть выше!), при допиливании vdev для возможности использования в режиме "observer only", без машины состояний.

      2. Также это дополнение есть и в возврате текущего состояния в vdev_set_state(), т.е., и оно тоже не работало!

      Т.е., значение возвращалось исключительно из драйверовых _rw_p().

    26.10.2016: ну чо -- исправляем.

    • Добавляем поле vdev_context_t.our_numchans (более, кроме как в этих guard'ах, нигде пока что нафиг не нужное). В самое начало блока "b. State machine".
      • Сравнения теперь идут с ним.
      • Т.о., для observer-only-драйверов достаточно будет не заполнять это поле -- т.е., для них вообще ничего не меняется.
    • Полномасштабные же драйверы пишут туда свои _NUMCHANS.

    Проверено на полу-симуляторе -- работает, "done".

    05.10.2021: в процессе работы над драйвером lebedev_turnmag появились сомнения в корректности работы с chan_state_n, поэтому было проведено расследование со следующими результатами:

    • Подозрение было в том, что
      1. из-за того, что в этом драйвере (как и в туче других, которые stateless, а пользуются только "observer'ом") значение chan_state_n НЕ устанавливается, и потому ==0,
      2. а в vdev.c проверка всегда "ctx->chan_state_n >= 0",
      3. то по ошибке будет оказываться "влияние" на 0-й канал -- в него будет возвращаться значение, а как минимум -- из vdev_init() -- ещё и уставляться свойства.
    • Но расследование показало, что везде стоят проверки вида "ctx->chan_state_n < ctx->our_numchans" -- и поле "our_numchans" было введено 26-10-2016 ровно ради этого: чтобы у stateless-драйверов оно было бы ==0 и, следовательно, работало бы преградой ("guard") от такой дури.

      И отсутствие проблемы подтвердилось при прогоне теста в режиме симуляции.

    • Но чисто на будущее -- если и у stateless-, observer-only драйверов всё же когда-либо это поле будет заиспользовано (ну мало ли зачем...), всё же добавлено дополнительное условие "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: исправил на ">=". Теперь будем ждать, не аукнется ли где (хотя не должно -- прямо по сути, ведь не должно быть состояний с номером, равным количеству состояний).

  • 05.12.2016: а вот vepp4_gimn_drv почему-то наоборот -- не отдаёт состояние, оно так и горит гусиным цветом, пока первый раз не обновится.

    Возможно, как-то связано со вторым сегодняшним багом -- невовремя-переходом-в-DETERMINE?

    06.12.2016: кажется, догадался: да, проблема СВЯЗАНА с тем багом.

    • Как это происходит:
      • "Полная готовность" считается прямо изнутри init_dev()/vdev_init(), в прямо при регистрации последнего из IMPR-каналов, при сразу-же-вызове для него sodc_cb(). И, соответственно, прямо оттуда делается vdev_set_state(,DETERMINE), который и приводит к возврату значения канала _GIMN_STATE.
      • Оттуда же, но чуть позже -- при регистрации _devstate-каналов -- определяется и статус-devstate.
        • Не поэтому ли и множественные вызовы переходов в DETERMINE (см. вторую проблему за вчера)?

          Должно быть, именно поэтому.

        • Но почему всего 2 раза?! Подчинённых-то трое -- должно 3 раза дёргаться...
      • Но в конце vdev_init()'а делается возврат DEVSTATE-кода, на основании текущих состояний подчинённых. А они-то все еще NOTREADY, вот и остаётся канал синим.

        Замечания:

        1. Возврат devstate'а делается "умно" -- вычислением на основании состояний подчинённых.
        2. ...НО ПОЧЕМУ СИНЕЕТ?!?!?! Непонятно: ведь timestamp-же НЕ должен сбрасываться!

      Попытка "починить" тем же способом, что вторую проблему -- добавлением фейковых read-каналов. Помогло.

    • Отдельный вопрос в сторону: а корректно ли, что в InitDevice() изначально проставляется dev->state=DEVSTATE_OPERATING?
      • Первая мысль -- "ведь если устройство сразу из init()'а вернёт OPERATING, то ничего не произойдёт -- из-за равенства состояний!!!".

        Но это не проблема, т.к.:

        1. InitDevice() не использует SetDevState(), а вызывает ReviveDev() напрямую.

          И, соответственно, никаких проверок на равенство не делается.

        2. Да и в самом 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);
        
      • А причина сразу-OPERATING, видимо, в том, что если драйвер начинает прямо из init_dev()'а что-то возвращать, то из-за NOTREADY оно считается "не очень актуальным".

        Сейчас-то эта проблема неактуальна -- никаких сбросов rflags/timestamps уже нет. Но всё же проще оставить как есть.

        Главный вопрос -- а когда было сделано, что изначально ставится OPERATING?

  • 05.12.2016: обнаружилась неприятность: vdev совершенно не приспособлен для работы с исключительно write-каналами, без единого канала чтения.

    05.12.2016: выявилось на vepp4_gimn_drv: он вместо подхватывания текущих значений из аппаратуры всегда берёт нули.

    Анализ показал, что:

    • У драйвера vepp4_gimn_drv.c все IMPR-каналы -- только каналы записи.
    • А они, в отличие от каналов чтения, отдаются СРАЗУ же при запросе.
    • Вот и оказывается, что прямо сразу при старте -- точнее, прямо сразу не выходя из vdev_init() -- оно как бы получает "всё", но это "всё" -- совсем неправильное.
    • И еще: переход в DETERMINE делается в vdev.c в 2 точках:
      1. В sodc_evproc(), при состоянии UNKNOWN.

        (Тут и происходит косяк с переходом при реально НЕ полученных реальных данных.)

      2. В stat_evproc(), при subord_state==DEVSTATE_NOTREADY и нашем состоянии !=UNKNOWN.

      И вот это второе -- слабо понятно. Или "что-то странное".

      • Оно и переходит не один раз (по отладочной печати видно).
      • И еще 20-10-2016 к этому пассажу были вопросы -- см. по фразе "во ВТОРОЙ рабочей версии".

    Но проблему как-то решать надо? Надо! Ведь vdev-драйвер, работающий только с управляющими каналами -- вполне легитимная вещь.

    (Хотя идеологически и небезупречная: отсутствие каналов чтения -- это отсутствие обратной связи. А драйвер "законченной железки" без обратной связи -- штука странная.)

    Ну и какими могут быть решения?

    1. Простое: иметь ненужный канал чтения на каждое устройство.
    2. Сложное: например, переходить в DETERMINE только при state устройства этого канала равным OPERATING.

    ...в любом случае, желательно бы разобраться в логике того второго -- в stat_evproc() -- перехода в DETERMINE.

    06.12.2016: пытаемся поразбираться.

    • Во-первых, добавлено 3 фейковых канала "чтения", по штуке на каждый из подчинённых девайсов.
      • НЕ помогло!
      • Возможно, потому, что все они -- каналы hw_ver, имеющие режим IS_AUTOUPDATED_TRUSTED.
      • Да, точно: при AUTOUPDATED_TRUSTED делается fresh_age={0,0}, а в проверке "считать ли CURVAL за UPDATE" стоит условие, что при нулевом fresh_age "считать".
      • Да нет, не потому. А потому, что каналы HW_VER отдаются сразу по приходу пакета 0xFF, т.е., еще задолго ДО получения текущего значения канала записи.
      • Короче -- НЕ катят.

        (Хотя для не-синести канала _STATE -- прокатило.)

    • Во-вторых, вообще-то ВЕЗДЕ -- и в cxsd_fe_cx.c, и в cda_d_insrv.c -- rw-каналы возвращаются как "update" только в случае, если их timestamp.sec!=CX_TIME_SEC_NEVER_READ; следовательно, проблемы-неприятности, обозначенной в заголовке, быть не должно вообще.

      Так почему она есть?!

    07.12.2016: кажется, разобрался "почему" -- CXSD_HW_TIMESTAMP_OF_CHAN()!

    1. Расследование:
      • Да -- определение "update" при возврате в обоих вышеупомянутых файлах было сделано верно. Проблема была в другом...
      • ...в cda_d_insrv.c при отдаче данных в cda в качестве timestamp'а вместо реального значения использовалось CXSD_HW_TIMESTAMP_OF_CHAN().
      • А оно для rw-каналов отдаёт CX_TIME_SEC_TRUSTED (там с оговорками, но не суть).
      • Но cda_dat_p_update_dataset() при TRUSTED считает, что ВСЕГДА "update", игнорируя указанное!
      • Вот и получалось, что совершенно никакое не "авторитетное" значение хваталось в качестве текущего, и отсюда вытекали все проблемы.
    2. Разбор:
      • Эта 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 уволена -- подробнее см. в профильном разделе за сегодня.

    3. Решение:
      • Элементарно: НЕ использовать CXSD_HW_TIMESTAMP_OF_CHAN(), а использовать chn_p->timestamp.
      • Так и сделано, и вуаля -- оно заработало как надо!!!

        ...и безо всяких фейковых каналов, введённых вчера.

      • Так что вовсе не "vdev совершенно не приспособлен для работы с исключительно write-каналами" -- очень даже приспособлен!

    04.01.2017: поскольку со всем разобрались -- изначальная проблема решена, и даже сделаны оргвыводы -- то "done".

  • 03.10.2017@по-переходу-из-13-го-в-1-е, после обеда: некузяво, что у каждого типа vdev-драйвера СВОЁ имя канала "текущий номер state'а машины состояния": ist_state, v1k_state, v3h_state, mps20_state, gimn_state, а сейчас добавляется rez_state (для kurrez_cac208). И только в lebedev_turnmag -- где машина состояний вообще не используется! -- в devtype задекларирован "vdev_state".

    А ведь стоит это безобразие унифицировать, ВСЕХНИЕ каналы назвав просто "vdev_state".

    03.10.2017: да, в создаваемом сейчас kurrez_cac208_drv.c сделано именно KURREZ_CAC208_CHAN_VDEV_STATE и vdev_state.

    04.10.2017: еще в сторону унификации и, заодно, документирования и упрощения работы при создании будущих драйверов:

    • @утро-дома начато краткое описание "Общий порядок создания vdev-драйвера", в будущем добавится раздельчиком к doc/lib/vdev.ru.html.

      10.10.2017: дописано, добавлено в doc/lib/vdev.ru.html.

    • Текущий скелет kurrez_cac208_drv.c, содержащий все нужные компоненты, но совсем НЕ содержащий "мяса", скопирован в TEMPLATE_VDEV_drv.c.
    • Индексы каналов в карте теперь имеют типо-агностичный префикс 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
  • 06.10.2017: заметил, что vdev_set_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; т.е., всего через год), просто так и не было сделано.

    Формально-то сделать не особо сложно -- просто надо аккуратно.

    1. Вложенный вызов -- не стоит. Лучше уж модифицировать nxt_state (с предварительным prev_state=cur_state!) и делать goto на начало.
    2. Второй вопрос -- ГДЕ делать проверку. Понятно, что после выполнения swch_to(). Но
      1. ДО цикла по state_related_channels[], так что значения изменятся только один раз, в конце, или...
      2. ПОСЛЕ него, так что значения state-related-каналов будут отдаваться наверх после каждого перещёлкивания состояния?

      Более симпатичен второй вариант -- он обеспечивает совместимость поведения с поштучными переходами (без цикленья которые, как сейчас). Считаем, что тут никаких побочных эффектов не будет, что вызов do_rw(,,DRVA_READ,...) никаких плохих последствий не повлечёт.

    3. Но также перед переходом на следующее состояние нужна дополнительная проверка, что машина всё ещё в том состоянии, в которое ЭТОТ экземпляр вызова её должен был перевести -- т.е., ctx->cur_state==nxt_state -- а то в swch_to() может быть сделано что угодно, там-то побочные эффекты, включая вызовы vdev_set_state(), разрешены.

    Вот только лень как-то: по-хорошему, при таком радикальном изменении проверять бы надо, анализировать таблицы состояний всех vdev-драйверов. А пока и вариант с delay_us=1 работает.

    10.07.2018: да, сделано.

    • Реализация -- по проекту от 10-10-2017 с модификацией nxt_state и goto на начало:
      • В конце, после цикла по state_related_channels, делается проверка: если состояние еще то же (а то мало ли -- актуализатор мог вызвать переход в другое состояние, как это делают все SwchToDETERMINE(); да и возврат state_related-каналов мог как-то косвенно привести к смене), ...
      • ...и следующее состояние указано, а задержка отсутствует, ...
      • ...то при разрешенности перехода (is_alwd==NULL или вернёт true)...
      • ...делается замена значения nxt_state и goto на начало.
    • Была мысль (@утро-пляж) более "правильной" реализации: цикл вроде do{...}while(...) или for(...){...}.

      Но там никак не вытанцовывалась элегантная конструкция: в первом случае нельзя обойтись просто условием (нужно выполнить присваивание nxt_state), а во втором надо как-то форсить выполнение первой итерации (например, вводить флаг is_first, сбрасываемый в "сдвиге цикла").

      Так что лучшим вариантом остался goto.

    • Присвоение prev_state теперь в коде, а не в объявлении.

    На weldproc_drv.c проверено -- работает. Прочие драйверы надо просмативать отдельно.

  • 06.10.2017: и давно уже пора унести 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()'е).
  • 10.10.2017@по-переходу-в-13-е-здание: как-то неправильно выглядит, что в action'ах может вызываться vdev_set_state() для перехода в другое состояние. Это хоть и даёт максимальную гибкость, но некрасиво с точки зрения "теории машин состояния".

    А красивым было бы, чтоб и проверяльщики (check, sw_alwd), и действия (action, swch_to) могли бы возвращать номер состояния, в которое надо перейти.

    10.10.2017: это в ДОПОЛНЕНИЕ к текущей возможности sw_alwd'ов говорить "да, готово" и "нет, не готово"; т.е., чтобы:

    • 0 - не готово.
    • +1 -- готово, переходить можно.
    • -N -- перейти в состояние N.

    Из чего следует, что состояния с номером 0 быть не может (у всех нынешних это DETERMINE, так что проблемы нет).

    Однако, сей красивой идее скорее надо сказать "НЕТ!", и вот почему.

    1. Необходимость возвращать МИНУС-номер-состояния -- плохо. А возвращать просто номер состояния -- нельзя, т.к. тогда ломается изначальная логика с возвратом +1 (т.е., C'шного "true") в качестве "можно".
    2. Добавление кода возврата к swch_to -- очень существенное изменение интерфейса.

      И неудобств это доставит намного больше, чем даст плюсов:

      • Чревато ошибками -- где-то будет забыто добавить.
      • Увеличит объём кода: добавить лишних return 1 (а где-то и return 0, и надо везде аккуратно выбрать!) придётся больше, чем сэкономится нововведением.

      Это при том, что номер состояния, в которое автоматически перейти далее, уже есть в vdev_state_dsc_t.next_state.

      Т.е., введение кода возврата в swch_to -- не вариант.

    3. А sw_alwd() может вызываться и не только из vdev'овского кода перехода по состояниям, но и при работе со state-related-каналами. И получится нонсенс: проверка вызывается лишь чтобы понять, что вернуть в канале (0 или 2), а вдруг она возвращает номер состояния, и-и-и... это указание просто игнорируется.

    В общем, слишком много проблем принесёт такое нововведение. Увы, "withdrawn".

  • 12.02.2018: мелкая мелочь, обнаружившаяся сегодня. В какой-то момент вылезло (linmag, линзы QL24...QL31):
    • Стоит некая ненулевая уставка, состояние "Вкл", но отработки нет -- измеряется 0.
    • Но в реальности в каналах ЦАП прописаны именно "0".

    И да, там были какие-то проблемы с железом. Но как случилась такая ситуация -- в ЦАПе записан 0, а драйвер ВЧ300 об этом не знает?

    12.02.2018: расследование показало:

    • Драйверы (ВСЕ -- ist_cdac20, v1k_cdac20, v3h_a40d16, mps20_ceac124, ...) НЕ следят за каналом _OUT. И если в него просто руками (cdaclient'ом) записать 0, то так и будет -- драйвер считает "всё в норме", а в реальности устройство отрабатывает нулевую уставку.
    • Почему vdev-драйверы не отслеживают изменения каналов уставки посторонними -- ясно:
      1. Считается, что они -- драйверы -- сами авторитетны, и никто не будет лазить мимо.
      2. А если бы отслеживать, то там опухнешь -- будут ведь прилетать уведомления и об изменении по инициативе самого этого драйвера, и надо как-то отделять "правильные" (которые сам запросил) от "неправильных".

        И по "неправильным" -- делать ЧТО? (С неожиданным нулём-то ещё понятно, а с остальными значениями что делать?)

    • Главный вопрос -- КАК произошло то, что произошло? После припоминания особенности работы выходного регистра у Козака и опроса Витали Балакина ситуация прояснилась.
      • В какой-то момент Виталя нажал на CANDAC16 и CANADC40 кнопки Reset.
      • Значения при этом обнулились.
      • ...а вот выходные регистры, хоть и отдались наверх нулями, в реальности продолжали держать "1" -- это особенность у Козака такая (известный баг, который исправлять он не будет, из-за широкой распространённости девайсов).

        (И да, помнится, когда я заглядывал в панель [...], никто из битов выходного регистра не горел, а только входной "On")

      • В результате ВЧ300 продолжили работать -- отдавать наверх статус OPR==1.
      • ...а драйвер v3h_a40d16 реагирует (переходом в состояние _IS_OFF) именно на _OPR, а за управляющими битами (ни за _SW_ON (который и так обычно должен быть ==0), ни за _SW_OFF (де-факто это "_ENABLE", должный при работе быть ==1)) он не следит.

        Вот драйвер и считал, что всё включено!

      • И лишь после дрыганья Выкл/Вкл одной из 8 линз, когда в выходной регистр прописалось значение с 0 в остальных битах, и оно было отработано, произошло "согласование" (синхронизация) -- остальные 7 штук перешли в состояние "Выкл" (биты "ENABLE" пропали -- источники вырубились; биты OPR обнулились; драйверы это заметили и перешли в _STATE_IS_OFF).

    Теперь вопрос -- что делать? А ничего, видимо. Ключевая причина -- косяк в CAN-железе. Человек глазом видит проблему, а пытаться взвалить её определение на драйвер -- куча сложностей (почти "искусственный интеллект"), и может привести к ложным срабатываниям.

    13.03.2018: см. также записи о разборках с той же группой QL'ей от 05-03-2018. Совсем не факт, что именно "косяк в CAN-железе" -- возможно, всё ж какой-то ляп в софте; вероятно, конкретно в v3h_a40d16_drv.c (т.к. на всяких ist_cdac20 и v1k_cdac20 такого вроде не бывало).

    О-о-о -- а не в двухдевайсности ли (ADC40+DAC16) дело?

    • Ведь v3h -- ЕДИНСТВЕННЫЙ vdev-драйвер, работающий поверх ДВУХ устройств.
    • И "подложка" из-за этого неатомарна: один девайс может быть в состоянии OPERATING, в то время как второй проходит через ->NOTREADY->OPERATING.
    • А во всехних SwchToUNKNOWN() делается vdev_forget_all(), у многодевайсных драйверов потенциально могущий привести к проблемам.

      Это было осознано 23-03-2016, и к 20-10-2016 реализована модификация архитектуры, на основе поля .subdev_n, позволившая избавиться от vdev_forget_all() (vdev.c::stat_evproc() сама переводит в состояние "неполучены" все каналы исключительно того подстилающего устройства, которое прислало сигнал о своём !=DEVSTATE_OPERATING).

    • Но вызовы vdev_forget_all() НЕ БЫЛИ убраны.
    • Хоть в конце того пункта и сделано заключение "переход в _STATE_UNKNOWN никогда не происходил!!!", но, похоже, это не так -- чисто по наблюдённым симптомам.

      И надо проводить тесты -- причём на ЖИВЫХ устройствах, а не только на симуляторе, как задумано 05-03-2018.

  • 05.10.2018: всем subordinate-каналам при регистрации принудительно ставится 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: проверил (в воскресенье, не дожидаясь понедельника) -- работает, вроде в обе стороны данные ходят как надо.

  • 23.10.2018: касательно не vdev, а драйверов источников, но зато всех сразу -- поэтому записываем здесь, как общий принцип: надо уметь различать отключение оператором и отключение по причине исчезновения бита "OPR".

    Сейчас и то, и другое сводится к запуску выключения -- _sodc_cb() делает vdev_set_state(,_STATE_SW_OFF_DOWN), так что в результате устройство/драйвер оказывается в состоянии _STATE_IS_OFF, без возможности узнать причину.

    А есть желание мочь узнавать, ПОЧЕМУ источник в состоянии "OFF". Нужно для высокоуровневой логики -- чтоб оно могло принять решение, надо ли пытаться включить источник повторно, или считать его "охреневшим" и звать оператора.

    23.10.2018: (реально всё было продумано ещё на прошлой неделе, но руки дошли только сейчас): соображения:

    1. Явно надо вводить дополнительное состояние -- "_STATE_CUT_OFF" ("отрубился"), по смыслу аналогичное _STATE_INTERLOCK'у.
    2. Поскольку источник отрубается САМ, то нет смысла миндальничать и исполнять ритуал выключения (плавно опускать уставку, снимать биты включенности) -- надо просто сразу всё занулить; в точности, как в SwchToINTERLOCK().
    3. К состоянию _CUT_OFF надо относиться так же, как к _IS_OFF: оно должно позволять включение.
    4. Для информирования клиентов нужно вводить ещё один канал -- "condition".

      В нём отдавать:

      • -1 при нахождении в _STATE_INTERLOCK.
      • 0 при нахождении в _STATE_CUT_OFF.
      • +1 при _STATE_IS_OFF
      • +2 при _STATE_IS_ON и аналогичных -- т.е., когда отдаётся _CHAN_IS_ON==1.

      Канал этот надо включить в список state_related_channels[], аналогично _CHAN_IS_ON'у.

    24.10.2018: делаем.

    • Для общности числовые значения "состояний" вынесены в отдельный файл drv_i/vdev_ps_conditions.h, они имеют префикс VDEV_PS_CONDITION_.
    • Сами значения сдвинуты на -1 по сравнению со вчерашним проектом: INTERLOCK=-2, CUT_OFF=-1, IS_OFF=0, IS_ON=+1.

      Смысл в том, чтобы знак определял "хорошесть" (оба нехороших состояния имеют отрицательные коды), а уж по конкретному значению можно понять конкретную ситуацию.

    • Плюс добавлено значение OFFLINE=-99 -- для отдачи в состоянии UNKNOWN.
    • Сам канал назван "vdev_condition" (сходно с vdev_state), в *_drv_i.h -- nnn_CHAN_VDEV_CONDITION.
    • 25.10.2018: он сделан IS_AUTOUPDATED_YES, плюс прямо из _init() отдаётся значение OFFLINE. Чтоб не генерить лишнего траффика, а отдавать наверх значения только при изменении vdev-состояний (для чего он должен быть в state_related_channels[], только без привязанного состояния -- с полем .state=-1).
    • Затронуты драйверы: ist_cdac20, v1k_cdac20, v3h_a40d16, mps20_ceac124; плюс не имеющие своих драйверов vnn_ceac51.devtype и sim_ist_cdac20.devtype.
    • Для VDEV_PS_CONDITION_IS_ON проверка "когда отдаётся _CHAN_IS_ON==1" не делается -- вместо этого он отдаётся по else.

      На первое время покатит, но вообще решение не очень хорошее, так что надо повсеместно относиться к этому пределено аккуратно.

    • Новое состояние nnn_STATE_CUT_OFF:
      • Вставлено перед _IS_OFF.
      • SwchToCUT_OFF() скопирована из SwchToINTERLOCK().
      • Доп.условие, разрешающее включение из него, вставлено в IsAlwdSW_ON_ENABLE().
      • Теперь _sodc_cb() именно в него переходит при исчезновении бита OPR.

    29.10.2018: наконец-то проверил. После правки пары косяков (вроде забытого добавления в state_related_channels[]) работает как надо.

    Только собственно переход в CUT_OFF пока не проверен -- для этого надо какой-нибудь источник руками с его локальной панели выключить.

  • 11.04.2021: есть потребность драйверам уметь добывать кванты от "подстилающих" subord-каналов -- как минимум для того, чтобы отдавать их наверх в качестве "своих" (для соответствующих ourc).

    11.04.2021: вылез этот вопрос потому, что ЕманоФедя затребовал получение квантов для каналов источников -- Iset.

    • Очевидно, что в идеале нужно сотрудничество от vdev, чтобы он подписывался на событие QUANTCHG.
    • Подписаться-то несложно, но вот драйверам-клиентам как отдавать? Ведь есть только vdev_sodc_cb_t(), у которого отсутствует "reason" (и это правильно!).

      Т.е., если делать -- то вводить ещё один обратный вызов, уже с "reason", для всякого информирования. И да, это сломает совместимость.

      15.04.2021: а вот и нет -- совместимость не сломается! Т.к. нет заполнение содержимого контекста выполняется не передачей параметров в vdev_init(), а ПЕРЕД этим вызовом, самим драйвером по-польно. Вот и будет по умолчанию NULL. И сама $(LIBVDEV) сейчас влинковывается не в сервер, а в драйверы индивидуально, поэтому расширение vdev_context_t бинарную совместимость не затронет.

    • Прямо сейчас во всех драйверах просто захардкожено число -- "305" оно используется при определении, что текущая уставка (в *_OUT_CUR) дошла практически до заданного значения.

    В общем, СЕЙЧАС в затронутые 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 микровольтов.

  • 05.06.2024: обнаружена проблема: v3h_a40d16 не работает в режиме симуляции -- некоторые каналы (конкретно каналы записи) навеки остаются болотными.

    Похоже, это происходит в случае, когда подчинённые устройства локальны (а не удалённые), т.е., данные доступны прямо в момент регистрации, а не потом, уже после завершения vdev_init().

    Проблема обнаружена при работе над sim_dir_drv.c, но быстро осознано, что причина где-то в vdev, так что дальнейшие разборки записываем тут.

    05.06.2024: начало в разделе по sim_dir_drv за вчера и сегодня, а тут описываем уже специфичные разбирательства.

    • Видимо, надо напихивать отладочных печатей в vdev.c, чтобы понимать, в какие моменты приходят какие данные и какие состояния "полученности" каналов и готовности для переключения в DETERMINE.
    • @душ, после завтрака: возможно, ключевое слово -- AllImportantAreReady(): именно оно определяет готовность всех подчинённостей, но вызывается оно ТОЛЬКО из sodc_evproc().

      Проверять его прямо в конце vdev_init(), и если "да", то производить переход в DETERMINE (со всеми теми же проверками)?

    • Но возникает вопрос: а почему, коли так, этот переход не происходит прямо в sodc_evproc(), вызывающемся во время регистрации каналов, в момент регистрации ПОСЛЕДНЕГО IMPR-канала?
    • ...а потому, что когда каналы "обновляются" прямо во время vdev_init(), то devstate-каналы ещё не зарегистрированы (т.к. они регистрируются ПОСЛЕ sodc-каналов), поэтому состояния под-устройств ещё "не готовы" (нули -- DEVSTATE_NOTREADY, да и ).
    • И чё -- получается, для решения проблемы достаточно поменять местами блоки "регистрация sodc-каналов" и "регистрация devstate-каналов" (поставив последнюю вперёд)?
    • Не, чё-то тут не то: в sodc_evproc() в проверке "Check if we are ready to operate" состояния подчинённых устройств вообще никак не участвуют -- предполагается, что если уж каналы все готовы, то устройства, наверное, тоже все OPERATING.
    • Пара соображений:
      1. @~12:40, дорога с П28 в Элегант-Люкс, лесок между двором и общагой 8-кой: а почему это дурит только ДВУХустройственный v3h_a40d16, в то время как ОДНОустройственный ist_cdac20 подобных проблем не выказал?
      2. @~14:00, дорога из Ярче домой, по Терешковой вдоль шк.25: вот выше была идея при симуляции рестартовать подстилающие "CAN"-драйверы, чтоб случились обновления; а если на РЕАЛЬНОМ железе рестартовать vdev-драйвер? Ведь он окажется в той же ситуации, что при симуляции -- все данные уже готовы; дурканёт или нет?
    • OK, напихиваем отладочной печати.
      • В sodc_evproc() (только для IMPR-каналов), stat_evproc(), плюс в конце vdev_init() выдача AllImportantAreReady().
      • Результат престраннейший: вроде бы все 4 важных канала "приходят" ДО конца инициализации, но готовность -- 0!
      • Предполагая, что нечто может происходить между регистрацией каналов и концом -- т.е., при регистрации devstate-каналов -- добаляем выдачу AllImportantAreReady() между циклами регистрации sodc- и devstate-каналов.
      • BINGO!!!
        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'ов) -- уже опять нет.

      • Засунул ту же выдачу уже внутрь цикла по регистрации devstate-каналов -- зануляется после ПЕРВОЙ же итерации, несмотря на то, что выдача в stat_evproc(), как видно в вышеприведённом дампе, говорит, что состояние подчинённого [0]=1 (1 -- это DEVSTATE_OPERATING).

        WTF?!

      • Смотрим, анализируем -- и понимаем: vdev_forget_all(), блин!!!
        • При первой итерации -- когда известно состояние только первого подчинённого, а у второго состояние ещё ==0 (NOTREADY) -- ...
        • ...stat_evproc() вычисляет "кумулятивное" состояние подчинённых как группы посредством CalcSubordDevState() (получая NOTREADY) и с помощью SelectOurDevState() выбирает "своё" состояние, ...
        • ...и потом, если оно OFFLINE или NOTREADY (а оно NOTREADY!), делает
          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, из которого так и не выйдет, по причине отсутствия "стимулов".

    • @душ перед ужином: ну так ТУТ-то как раз не будет ли достаточно перетащить регистрацию devstate перед sodc?
    • Попробовал -- да!!!
    • Теперь надо провести тесты: вариант "с переставленностью" и с не-деактивированным vdev_forget_all(); попробовать ._devstate=0 "CAN"-устройству.
    • @засыпая: ну ладно, пусть "перестановка" -- не самый красивый вариант, и стоило бы сделать как-то получше; но КАК? Как было бы правильнее? Какой-то флаг заводить в структуре-контексте -- "недоинициализировано", поэтому не производить переходов?
      06.06.2024@ИЯФ, ~12:30, попытавшись сходить на обед, но увидев масштаб потопа и вернувшись обратно:
      • Как вариант -- можно было бы вместо "флага" прописать cur_state=-1, и чтоб stat_evproc() при cur_state<0 не пытался бы вычислять devstate и производить переход в UNKNOWN.
      • Но это, конечно, тоже так себе вариант -- он имеет неприятное последствие, что запретит легитимные переходы в UNKNOWN на основании реально полученных статусов (в т.ч. у ОДНОустройственных драйверов). Не то, чтоб как-то было критично на этапе инициализации -- и так ведь изначально cur_state==0, но общенекрасиво.
      • Опять же, и с этой проблемой можно побороться: на каждой итерации регистрации devstate-каналов прописывать cur_state=-1-tdev -- чтоб CalcSubordDevState() знала, по какому количеству уже реально готовых статусов (что, кстати, формально тоже не совсем так: а вдруг статусы начнут приходить не по порядку?).
      • Но, чёрт возьми -- да ну нафиг! Уж лучше обойдёмся текущей реализацией, когда просто сам порядок выбран так, что он обеспечивает отсутствие проблем. Это, может, и не мега-правильно, зато просто, элегантно и работает.

    06.06.2024: завершаем.

    Во-первых, проводим тесты.

    1. "._devstate=0" подстилающему "CAN"-устройству: да, помогает.

      Только делать это надо ОБОИМ устройствам.

    2. Теперь вариант "с переставленностью" и с не-деактивированным vdev_forget_all(): да, тоже работает.

    Во-вторых, собственно исправление кода.

    • Убираем vdev_forget_all().
    • Также убираем в sw4cx/drivers/ всехние 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".

pzframe:
  • 09.09.2015: начаты работы по pzframe_drv и драйверам были в начале 2014-го и описаны в раздельчике "О базовых и настроечных каналах".
  • 17.04.2016: начинаем работы над v4'шной реализацией clientside-стека pzframe*.

    Работы ведём в hw4cx/pzframes/, а потом общие файлы переедут в 4cx/src/ -- в include/ и lib/pzframe/.

    17.04.2016: реально думки начались еще в начале недели, а сейчас уже конкретно делаем файлы. Конкретно начато с pzframe_data в лице pzframe_data.h.

    • На вид вся реализация будет проще --
      1. и благодаря отсутствию разделения на CHAN и BIGC,
      2. и потому, что dtype в v4 уже интегрирован изначально,
      3. и за счёт поддержки {r,d} и в векторных каналах, и в "параметрах" (которые теперь просто обычные каналы),
      4. и благодаря множественным векторным каналам на соединении,
      5. да и вообще в v4 многое из архитектуры было сделано с учётом потребностей осциллографов вообще и реализации их в pzframe в частности.
    • Лёгкая неясность возникла с 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[] (да, индексируясь по тем разрезолвленным номерам).
    • К вопросу о функционировании pzframe_data в паре как с knobplugin'ом, так и с main'ом: ему будет передаваться cid, и если он ==CDA_CONTEXT_ERROR, то pzframe_data сама сделает cda_new_context().
    • Отдельный вопрос -- как поступать с регистрацией самих каналов.
      • Передавать-то при инициализации можно "базу" (которая прямиком в cda_add_chan() и уйдёт) -- для main' это будет argv[1], а для knobplugin'а -- ... ну придумаем что.
      • НО! Ведь datatree/Cdr сами могут (должны!) регистрировать что-то.
        • И чё -- передавать pzframe_data'овой инициализации также и dataref основного (векторного) канала, чтобы она использовала его? И evproc тоже дергать тогда "снаружи", из метода "NewData" bigc-knobplugin'а?
        • Неа -- криво это. И, кстати, еще получится, что "свойства" канала -- dtype+max_nelems -- будут указываться в ДВУХ разных местах (NNN_data.c и описателе узла в .subsys-файле).
        • Пусть уж вся информация берётся из NNN_data.c, а в .subsys-файле указывается какая-нибудь мелкая фиговинка-заглушка (для функционирования knobplugin'а и pzframe-стека оно не нужно вовсе, но cx-starter'у требуется, чтоб была какая-нибудь ссылка на сервер).
        • Одно мелкое замечание: желательно всё же иметь возможность в описателе ручки уметь ограничивать объём (max_nelems) данных векторного канала -- нефиг тратить лишнюю память, если заранее известно, что на некоем графике не понадобится больше скольки-то точек.

          ...хотя тут может быть побочный эффект: ну ограничим, а сторонняя программа в fast-ADC запишет больше; и как тогда из большого вектора извлекать данные "дальних" каналов, если их там физически не будет?

    18.04.2016@вечер-душ: по здравому размышлению --

    • Вот нефиг тащить в v4 внутренности от v2, базируя всё на них.
    • Надо делать собственно МОЗГИ почти с нуля:
      • Начинать строить с pzframe_data, подгоняя её под нужды конкретных использователей. Начать для примера можно с ADC200ME.

        Чтобы adc200me_data.c содержал именно описание СПЕЦИФИКИ блока, максимально избавленное от особенностей, продиктованных ограничениями реализации.

      • От v2'шной же реализации pzframe надо брать только "технологию" наследования/ссылания между уровнями (то, что хоть неприятно, но свои функции выполняет; раздел в bigfile-0001.html, заметка конкретно от 04-09-2012).

    19.04.2016@утро-душ: насчёт "резолвинга" -- о чём думалось вчера:

    • Вчера-то думалось (но не было записано) завести enum-константы, и набивать массив описания параметров позиционно, по ним -- инициализациями вида «[НОМЕР]={"ИМЯ",...}», как делается в vdev-драйверах.

      Тогда надо б было ОБЯЗАТЕЛЬНО эти enum'ы заводить в NNN_data.h, чтоб "наследующие" (вроде manyadcs) имели доступ.

      Но ведь проще использовать в качестве номеров drv_i/-номера! Там уже заведено всё нужное, да и доступны они без дополнительных действий всем желающим уже.

    • И более того: надо перейти от концепции "описание параметров" (paramdscr) к "описанию каналов": чтобы там в одной таблице описывались и "параметры", и векторные каналы (реально -- должен быть ОДИН канал).
      • Тогда pzframe_data.c будет регистрировать каналы, идя по этой таблице.
      • ...а с evproc'ем -- только "основной" (векторный) канал; чтобы все действия по обработке производить по его обновлению -- когда (по идеологии функционирования параметризованных каналов) в "параметрах" данные уже обновятся.
      • Очевидно, в описании каналов понадобятся -- кроме имён -- также dtype и max_nelems.

        (Эта мысль пришла в голову еще до идеи с общей таблицей -- так параметры смогут быть не только int32.)

      • СЛЕДСТВИЕ:
      • Ну и поле "param_type" как бы вернётся -- уже в форме "chan_type", могущем быть либо "IS_PARAM" (=0), либо "IS_MAIN" (=1).
      • В векторе "текущих данных" -- где будут храниться значения (видимо, этот вектор будет всё же отделён от вектора, хранящего cda_dataref'ы) -- надо будет иметь поля с типом 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.

        Замечание: добавлены они в конец, именно в таком порядке, поэтому:

        1. для скалярных каналов max_nelems может не указываться (возьмётся =1 -- прямо cda_add_chan() это фиксит),
        2. а для int32-каналов -- еще и dtype (его pzframe_data.c сделает =CXDTYPE_INT32).

        ...а учитывая, что "обычные" значения для change_important и chan_type -- тоже нули, то по факту для большиства строк в таблице достаточно писать только имя.

    • pzframe_cfg_t теперь содержит только фиксированные "поведенческие" параметры.
    • Всё, связанное с "param_info" изведено полностью (оно использовалось в основном для {r,d}-пересчёта, что теперь задача cda, а прочие применения пойдут напрямую через 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: далее:

    • Начинаем начинять pzframe_data.c.
    • Поскольку без реального применения это всё лишь теории, то начинаем работу над fastadc_data.[ch] (чей юзер -- adc200me_data.c -- начат несколько дней назад).
    • По ходу пиления вылез идеологический вопрос касательно пересчёта -- {r,d}:
      • В v2 калибровки указывались в 2 раздельных шага:
        1. Из сырых единиц в вольты -- *adc*_data.c::*adc*_raw2pvl().
        2. В операторские величины (амперы, ...) -- параметрами coeff и zero, указываемыми в параметрах узла/ручки -- т.е., clientside.
      • В v4 же предполагается, что все такие свойства должны указываться server-side -- в конфиге, описывающем аппаратуру.
      • НО! А как указывать разные коэффициенты для разных частей одного векторного канала?
        • Теоретически -- можно б перейти в fastadc* на по-входовые каналы, которые LINEn.
          • Фиг бы с ним, что в fastadc* -- главное, что в pzframe понадобится работать с не-единичным векторным каналом. И даже более того -- понятие "основного (кадрового) канала" там станет лишним.
          • С другой стороны, уже сейчас архитектура на это рассчитана: тем, что каналы вроде как "параметров" могут быть не только int32.
        • Отдельное замечание: тогда понадобится некоторая "точка синхронизации", чтобы знать, что в этот момент пришли уже все данные+параметры.
          1. Простейшее -- в драйверах АЦПов (или АЦПей?) чуть сменить порядок отдачи векторных каналов. Вместо нынешнего DATA;LINE0,LINE1,... сделать LINE0,LINE1,...;DATA -- тогда маркером будет тот же самый основной канал.

            Но! Он ведь в этой по-входовой модели нам уже нафиг не нужен. Да и объёи у него о-го-го.

          2. Хитрее (хотя и чреватее) -- завести в драйверах специальный dummy-канал, который отдавать в самом конце, и тогда pzframe_data сможет привязываться к его приходу.
            • В pzframe_drv.h для него ввести свой тип PZFRAME_CHTYPE_TRIGG (больше для красоты, т.к. канал такой будет один, и каждый драйвер просто явно будет ставить его в конец списка возврата; ну еще SetChanReturnType(, IS_AUTOUPDATED_YES) делать).
            • В pzframe_data.h -- аналогично тип PZFRAME_CHAN_IS_TRIGGER, на канал с которым и вешать evproc для обработки пришедшего кадра.

              20.04.2016@вечер: только это должно быть не типом, а ФЛАГОМ -- т.к. у одноканальных устройств (vcamimg, u0632, одноканальные осциллографы) триггером будет работать прямо сам канал кадра.

              20.04.2016@вечер: а чем станет сам "тип" -- вообще вопрос. Предполагалось, что он будет указывать, параметр ли это и тогда надо копировать в "previous" (для сравнения), или же "кадровый" канал и копировать не надо. Но по факту можно просто копировать поле valbuf ВСЕГДА, не заморачиваясь.

      • НО 2! У нас вроде бы не предусмотрена возможность указывания {r,d} аппаратному устройству (в директиве channels) -- а только для "виртуальных устройств", через cpoint (тому были идеологические обоснования -- где-то за лето 2015г. надо искать).

        Коий cpoint в данном случае описывать даже не очень понятно как.

    20.04.2016@вечер: да, если хорошенько подумать -- то ведь к этому мы и стремились, делая прямо в драйверах раздельные каналы для разных "входов" и по-входовые каналы свойств, включая всякие LINE1ON: чтобы знания о специфике устройств максимально переехали в драйверы.

    • Поэтому -- ДА, НАДО переходить на по-входовые каналы!

      ...и даже некоторый вопрос -- а зачем вообще нужны станут "общие" каналы DATA?

    • Что же до указания {r,d} -- то это, кажется, должно б быть доступно через "хитрые" cpoint'ы:
      dev DEV_NAME ADC_TYPE ~ ...
      cpoint DEV_NAME.line1 R D
      

      Такие фокусы вроде бы работают.

      ...с чем-то подобным косяки были (у MPS20 или где?), но там всё равно надо разбираться.

    21.04.2016: реализуем:

    • Только это не "TRIGGER" (мутный термин), а "MARKER"!
    • Со стороны драйверов:
      • Введено PZFRAME_CHTYPE_MARKER.
      • Добавлено в список типов, которым указывается автообновляемость, во все 5 (сейчас) драйверов. 23.05.2016: это было лишним: канал отдавался клиенту СРАЗУ при подсоединении (ибо is_autoupdated). Убрано.

        Плюс игнорирование этого канала в _rw_p().

      • Многоканальным драйверам -- сейчас это 3 штуки (adc200me, adc812me, adc4, но НЕ c061621) добавлен канал "MARKER". Варианты были либо сделать его самым последним (но этот у некоторых может оказаться занят (c061621, хоть ему сейчас и не нужно)), либо последним в секции "data" -- т.е., у всех это номер 9, он как раз повсеместно неиспользуется и имеет вид "r" и тип "i". А именно:
        • В _drv_i.h.
        • В .devtype (кроме adc4.devtype, которого пока нет).
        • Собственно "возврат" в 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 -- но там совсем другое дело).

      • Предполагается, что туда будут помещаться получаемые от cda калибровки. Пока это не делается -- неясно еще, в какой точке и как ловить событие RDSCHG.
      • Всё касательно "raw2pvl" удаляется в пользу общего пересчёта.

        Замечание: пересчёт этот нужен будет ТОЛЬКО для отображения подписей/реперов и при сохранении данных в файл -- т.е., редко, и без требований к производительности. Отрисовка же вся, как и раньше, в сырых величинах.

      28.06.2016: а какого чёрта всё с {r,d} запланировано в fastadc?! Ведь работа с данными -- прерогатива pzframe_data!!! Наверное, по привычке было сделано, не думая. Сейчас перетаскиваем всё в pzframe.

    • Кстати, а не научиться ли использовать предоставляемые сервером units, label etc.? Для случаев, когда они есть, конечно -- это позволит полностью переложить информацию "о физике" с клиента на серверов конфиг.

      ...а с dpyfmt как быть? :)

    21.04.2016@вечер-дома: и чего, спрашивается, понесло сразу на fastadc_data? Сделал бы сначала vcamimg_data -- он намного проще, т.к. там ни пересчёта никакого нет, ни множественных линий :).

    С другой стороны, тогда бы наткнулись на проблему "как делать множественность" позже, и её решение потребовало бы бОльших усилий.

    22.04.2016: продолжаем:

    • К вопросу о "mes":
      • В собственно pzframe он не нужен. Вообще никак.
        • То, что раньше было им (databuf, rflags, tag, info[]) теперь в основном содержится в cur_data[].
        • Что-то осмысленное появляется только в fastadc_data -- exttim, plots[].
        • А в vcamimg оно и раньше-то не использовалось СОВСЕМ.
      • Вывод: "mes" уходит на уровень "наследников pzframe", где понадобится -- конкретно пока только в fastadc.
      • В роли "info" будет выступать массив pzframe_chan_data_t[] -- данные "параметров" складируются теперь в него.
      • Былая "info2mes" всё-таки будет нужна -- для заполнения mes по полученным данным.
        • Имя ей оставим старое? Не брать же "chan_data2mes".
        • Её задачей будет прописать значения в структуру *mes_t (в любой экземпляр!) информацию на основе данных из массива pzframe_chan_data_t[] (также из любого экземпляра!).

          Требование поддержки "любости" позволит при надобности реализовать svd.

        • Но ссылку на сам экземпляр fastadc_data тоже передавать надо -- вдруг понадобится какая-то инфа либо из dscr'а, либо из dcnv.
    • Замечание битым текстом: сейчас сохраняться в "prv" будут ТОЛЬКО данные, содержащиеся в pzframe_chan_data_t.valbuf.

      Хоть буферы для данных бОльшего объёма в cur_data и будут аллокироваться, но в prv_data -- нет.

    • adc200me_data.c вроде сделан, до компилируемости. Всё довольно просто и компактно (основное место занимает таблица описания каналов); правда, там пока нет вообще ничего на тему устанавливаемых из командной строки параметров (поскольку эта часть отсутствует на всех уровнях).
    • Далее -- набиваем кодом fastadc_data.c.

    23.04.2016@дома: кстати, насчёт панели управления в *gui:

    1. Учитывая, что теперь все параметры являются просто отдельными каналами, есть 2 варианта реализации ручек:
      1. Как раньше в v2 -- делать simple-knob, на который вешать общий evproc и из него руками слать значение в канал.
      2. Создавать просто обычную ручку, добавляя её в дерево, и ставя ей соответствующий rd_ref.

        Этот вариант хоть и выглядит красивее, но имеет проблемы:

        • Биндинг ручек с каналами производится в Cdr, который для pzframe как бы отсутствует.

          Но это-то невеликая сложность -- биндить можно и вручную (благо, тут ТОЛЬКО ссылки на каналы).

        • Архитектура с деревом предполагает, что обновлением занимается тоже Cdr (в сотрудничестве с Chl!). Которому эти не-им-созданные ручки надо еще как-то подкинуть.
        • ...но главное -- у автономных клиентов (которые на основе _main, а не _knobplugin) никакого Cdr и не будет.

      Посему -- останемся всё-таки со старой моделью "всё делаем сами, безо всяких Cdr".

    2. Как создавать панель управления (в свете решения по первому пункту этот вопрос уже как бы неактуален, но всё же заслуживает упоминания) -- опять же, по-старому или по-новому:
      1. Всё вручную, толпой индивидуальных вызовов -- как раньше.
      2. Делать текстовое описание (на языке subsys, но без m4) этого деревца ручек.

        Тут, конечно, есть некоторые сложности:

        • Как быть в readonly-режиме? При "вручную" помеченные как PZFRAME_CHAN_RW_ONLY_MASK ручки просто заменяются на Core размером 1*1 пиксел. А тут -- иметь ДВА дерева?
        • Как быть с внутренними (не-маппирующимися на каналы) ручками, в v2 делающимися через mkstdctl()?
        • Дерево realize'ится Cdr'ом, которого опять же может не быть.

        Еще даже до осознания отрицательного ответа по п.1 было ясно, что СЕЙЧАС в любом случае надо сделать по-старому, а в будущем, возможно, перейти на текстовое описание дерева.

    3. Отдельный вопрос -- "О программной управляемости cont-плагинов": он предполагал вариант именно с добавлением всего в общее дерево, чтобы "внутренние" параметры самих pzframe-gui-панелей были доступны как обычные ручки соседним ручкам.

    Итого -- в нынешней парадигме (в первую очередь с отделённым от datatree/Knobs слоем Cdr) удастся всё сделать исключительно по-старому.

    Возможно, надо менять саму парадигму, делать всё как-то СОВСЕМ ПО-ДРУГОМУ?

    25.04.2016: пилим fastadc_data:

    • fastadc_data.c тоже сделан до компилируемости, в обрезанном варианте -- нет всего,
      1. связанного с сохранением/загрузкой режимов (ничего сложного, но неохота тратить время на покаместо ненужный функционал);
      2. касающегося юзером параметров -- FastadcDataCreateText2DcnvTable() (тут просто неясно, как делать, а главное -- ЧТО делать, и надо ли вообще).
    • Напрашивается мысль, что раз уж теперь используются отдельные по-входовые каналы, то надо иметь формализованное описание их функционирования на уровне fastadc_data:
      • Имея в виду, что формально каналы одного АЦП могут быть и неодинаковыми --
        1. разных типов;
        2. с разной длиной (в v2/liu таков manyadcs, пусть он и искусственный).
      • Напрашивается табличка, состоящая из num_lines ячеек, содержащих:
        • Номер ячейки канала, отвечающего за эту линию. Тогда fastadc_data сможет сама добывать оттуда данные, не требуя этого от конкретных *adc*_data. Если -1, то ничего не делать, а полагаться на info2mes().
        • Номер ячейки канала, отвечающей за NUMPTS этой линии. Если -1, то брать от общего param_cur_numpts; а если и он -1 -- то не делать ничего, а полагаться на *adc*_data.
        • Максимально-допустимое количество точек. Это можно брать из max_nelems исходного канала.

          И dtype линии тоже можно брать оттуда же.

        • Свой индивидуальный dpyfmt; если пуст -- брать общий.
        • ...и диапазон тоже.
    • Тип создан -- fastadc_line_dscr_t, и ссылка на таблицу -- fastadc_type_dscr_t.line_dscrs.

    27.04.2016: в продолжение по fastadc_line_dscr_t:

    • Надо бы и ссылку на канал "is_on" тоже.

      А по-хорошему -- и на прочие "свойственные" каналы. Чтоб свести количество КОДА в *adc*_data.c к минимуму, переместив всё по максимуму в "описания".

    • И имена каналов, и units лучше указывать там же, заменив этим v2'шные line_name_prefix, line_name_list, line_unit_list.

      ...некоторый вопрос с именем/идентификатором остаётся -- нужно ж как-то формировать метки для параметров вроде "ch1on" в PSP-таблице.

    • "Общие параметры" -- которые должны использоваться при неуказанности в индивидуальных описателях -- стоит передавать также такой же готовой структурой line_dscr, а не раздельными параметрами.
    • Замечание: для ссылок на каналы -- по номерам -- выберем буквосочетание "cn" (Channel Number). Это аналог v2'шного "pn" (Parameter Number).
    • @вечер: конечно, не совсем это красиво.
      • Изначально ведь задумывалось, что у каждого fasadc будет стандартный набор имён каналов, так что "general fastadcclient" сможет последовательно
        1. Узнать количество входов, прочитав канал "num_lines".
        2. Брать данные по всем входам, читая их каналы lineN, lineNon, lineNrange*, ...
      • Правда, в той схеме есть пара тонкостей, делающих её реализацию проблемной:
        1. "Знание" о количестве входов:
          • Каналы стать реально доступны уже ПОСЛЕ запуска клиента (при удалённых драйверах -- как оно в том же CAMAC).

            Поэтому нельзя сразу просто "вот прочитать текущее значение num_lines, и сделать под него интерфейс".

          • И даже больше -- прямо при жизни клиента может произойти рестарт сервера с заменой типа АЦП. И по-хорошему клиент должен смочь адаптироваться.

            Вывод: нужно мочь переделывать интерфейс "на лету", по изменению значения канала num_lines.

        2. Откуда браться панели управления со специфичными для каждого типа устройства ручками?

          ...когда-то, кажется, была мысль иметь текстовый канал с описанием панели управления на языке subsys. Но это выглядит [сейчас] совсем фантастично.

      • Замечание в сторону: а нехило бы иметь канал, отдающий имя типа устройства.

        Или вообще делать это заботой не драйвера, а прямо сервера -- ввести 3-й специальный _dev-канал, "_devtype". Благо, вся инфраструктура уже есть -- надо только эту строку "возвращать" не при смене состояния, а единожды при старте; можно даже просто запонять всё прямо в CxsdHwSetDb().

      Вывод:

      1. Учитывая глобалистичность задачи создания такого "общего fastadc-клиента", проще для каждого типа осциллографов клепать свой стек DEVTYPE_data+DEVTYPE_gui.
      2. ...А вот файлы DEVTYPE.c и DEVTYPE_knobplugin.[ch] все абсолютно одинаковы и вполне могут генериться (это касается не только fastadc, а вообще ВСЕХ pzframe-девайсов).
      3. И да -- у нас будет "некрасивая" архитектура, где внутри всего клиентского стека параметры адресуются не по именам, а по номерам, назначаемым в DEVTYPE_data.[ch] (де-факто -- всегда номера каналов из DEVTYPE_drv_i.h.

    28.04.2016: для разнообразия позанимаемся уровнем gui -- благо, он-то от v2'шного отличается незначительно.

    • В основном -- копируем.
      • pzframe_gui.[ch] -- куцый пока вариант: нет ничего ни с leds (фиг бы с ними), ни с работой с параметрами. Но уже доведён до компилируемости.
      • ...заодно доведён до компилируемости pzframe_data.c.
      • fastadc_gui.h -- вообще копия v2'шного, отличия только в параметрах FastadcGuiRealize() (present_cid+base).
    • hw4cx/pzframes/Makefile поднапичкан требуемым для сборки библиотек (оно потом уйдёт в lib/pzframe/).
    • Кстати, за компанию -- явно напрашивается, что нужно-таки сделать xmclients/DirRules.mk, с поддержкой отдельно Chl- и не-Chl-клиентов. И, видимо, с LOCAL_LIBDEPS.
    • ЗАМЕЧАНИЕ: в отличие от v2, пытаемся сделать, чтобы PzframeGuiRealize() САМ вызывал PzframeDataRealize() -- как и должно быть для "правильной" реализации наследования.
      29.04.2016: видимо, обломимся:
      • Причина "кривости" -- множественное наследование: у узла "pzframe_data" ДВА как-бы-наследника (по "вертикали" (см. bigfile-0001.html за 04-09-2012) -- pzframe_gui, а по горизонтали -- fastadc_data.
      • И вызывать realize-предка (и init!) должен кто-то один.

        Вот в v2 и сделано, что методы "предка" вызываются наследником-по-горизонтали.

        И это правильно: т.к. у нас по горизонтали реальное наследование, а по вертикали -- ссылочное; и метод предка вызывает РЕАЛЬНЫЙ НАСЛЕДНИК (в смысле ООП) -- т.е., горизонтальный.

      • Посему убираем у PzframeGuiRealize() все параметры, кроме собственно gui.
    • И еще замечание чуть в сторону: в pzframe_gui.c пришлось за-#include'ить Chl.h ради CHL_DATA_PATTERN_FMT.

      Но это плохо!

      Вывод: нужно вытащить эти #define'ы с паттернами для имён файлов в какой-то отдельный .h.

      18.05.2016: и с CHL_STDCMD_nnn бы тоже.

    29.04.2016: продолжаем разнообразить:

    • fastadc_gui.c -- копия старого файла (cp) с последующим редактированием. Модификации --
      1. адаптация к MotifKnobs;
      2. временное закомментировывание всего, касающегося dsp/pvl.
    • pzframe_main.h -- минимальные изменения, из-за строковости команд (вместо целочисленности).
    • fastadc_main.h -- вообще без изменений.

      fastadc_main.c -- временно закомментированы оба вызова *CreateText2*Table().

    02.05.2016@дома: покуда еще нету "привязывания в сервере {r,d} одного канала к другому": надо уметь делать это прямо на стороне клиента в pzframe_data. Для чего ввести:

    1. Флаг PZFRAME_CHAN_NO_RD_CONV_MASK -- для указания не-конвертировать в cda.

      23.05.2016: добавлен. CDA_DATAREF_OPT_NO_RD_CONV выставляется при нём либо при PZFRAME_CHAN_IS_FRAME.

    2. Номер "базового" входа для данного канала-параметра -- от кого брать цепочку {r,d}.

      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: ползём дальше:

    • pzframe_main.c:
      • CommandProc() переведена на строки, но бОльшая её часть пока закомментирована.
      • Логика парсинга параметров модифицирована:
        1. Убрана специальная роль '!' -- p_xc.
        2. Удалено PK_CHANBASE.
        3. PK_BIGC переименовано в PK_BASE.
    • Также сделан первый полный пример -- adc200me:
      • adc200me_gui.[ch] -- копия из v2hw/ с почти автоматической коррекцией.

        19.05.2016: ага, щас!!! Надо было почти ВСЕ spec'ы для SimpleKnob'ов адаптировать к v4.

      • adc200me.c -- вообще без коррекции (будущий автогенеримый файл, как и adc200me_gui.h, кстати).
    • pzframe_cpanel_decor.h -- небольшая адаптация, под DataKnob.

      19.07.2017: дополнение: сильно надоело (при создании под-окошка [Ctrl...] в ADC4X250) повсеместно при создании grid'ов потом делать их настройку -- Grid, Spacing, Padding. Поэтому добавлена MakeFreeGrid(), комбинирующая это всё ("Free" -- что, в отличие от "A", parent'ом не обязательно должен быть другой grid -- и, соответственно, позиционирование в нём не делается).

    • После чего бинарник adc200me начал собираться, для чего понадобилось...
      • Напихать в местный Makefile правила, должные быть в xmclients/DirRules.mk.
      • Добавить EXPORTSFILES=/EXPORTSDIR= во многие 4cx/src/lib/*/Makefile (в половине отсутствовало).
      • pzframe_data.c::PzframeDataSetRunMode() -- скопирована из старого, но "мясо" закомментировано (т.к. там cda_run_server()/cda_hlt_server(), которое более не существует -- надо менять парадигму работы); она последнее, без чего не линковалось.

      Пока, естественно, не работает (SIGSEGV) -- нет огромных кусков, в первую очередь в PzframeDataRealize(). Но собралось, да!!!

    19.05.2016: отдельный вопрос насчёт работы будущего pzframe_knobplugin.c: ведь ему надо бы при регистрации каналов как-то учитывать текущее "baseref".

    • Если бы делалось "правильно" -- с регистрацией каналов Cdr'ом, а не pzframe_data'й, то вопроса бы не было.
    • А сейчас-то где эту "дополнительную базу" добывать?
    • ...и, кстати, даже добывши, её надо будет как-то подсовывать, а как? Т.к. имеющийся в 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).
        • ...а вот потенциально-требуемые буфера для prv'шных данных не аллокируются НИКОГДА -- что де-факто влияет на векторные каналы (т.к. скаляры прямо в ячейках, т.е. есть ВСЕГДА).

      В конечном итоге функция получилась монструозноватая и не слишком-то красивая: работа по подсчёту требуемого для current_val'ов места и его прописывание являются разными блоками кода (дублирование) и несколько разнесены.

    • Также в PzframeGuiRealize():
      • добавлено malloc()'ирование k_params[] и Param_prm[] (раздельное!),
      • ради чего функцию пришлось превратить из void в int, т.к. аллокирование может и обломиться.
      • Правда, вызывальщик -- в лице FastadcGuiRealize() -- на результат всё равно не смотрит. А если б и смотрел -- то "PzframeDataDestroy()" у нас всё равно нету, а понадобилась бы, т.к. PzframeDataInit() перед этим уже вызвана.
    • PzframeGuiMkparknob(): сделана (пока была пустой, всё SIGSEGV'илось), стала намного проще старой.
    • Обнаружилась странная штука: glibc в SL-6.3 почему-то падает от вызова вида
      fprintf(stderr, "%s", NULL);
      вместо того, чтобы напечатать "(nil)" (как делал это в других системах).

      Проявилось на KnobsCore_knobset.c::fprintf_stderr_knobpath -- пришлось там ставить проверку на !=NULL, а при ==NULL передавать "(NULL-ident)".

    • Изрядное отличие v4'шного SimpleKnobs от v2: ручки по умолчанию ro, а для rw'шности нужно указывать флаг rw явно (а раньше явно указывалось ro).

      Решение, в принципе, разумное, и было принято еще в 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() раскомменчено, ессно.
    • Теперь вопрос с диапазонами (min/max) -- почему-то 123.450V.

      ...ага, ясно: это как раз из-за "всего, касающегося dsp/pvl" -- gui_pvl2dsp() сейчас состоит из "return 123.45". Т.е., ни диапазонов это реально не касается, ни на отображении графиков оно не скажется.

    23.05.2016: ...

    • PzframeDataEvproc() наполнен функционалом.
    • Далее попробована работа с реальным железом. Конечно, ничего пока не отображается (т.к. на уровнях pzframe_gui и fastadc_gui пока ничего нет), но кнопки Shot и Stop отрабатываются.

      При этом поправлен косяк с излишне помечавшимся как is_autoupdated MARKER'ом.

    • Пара технических вопросов:
      • Кто и как будет обновлять ручки-параметры? Тот же ELAPSED надо обновлять хотя бы раз в цикл, но никак не только по общему приходу данных.
      • Чтобы вся схема работала, каналы должны приходить по мере реального обновления. Т.е., как бы с суффиксом "@u". Но писать это "@u" в таблицах-описателях -- совсем не комильфо.

        Видимо, нужно ввести отдельную CDA_DATAREF_OPT_ON_UPDATE плюс в cda_dat_p_new_chan_f() параметр "options" для передачи этого флажка.

    24.05.2016: м-м-м:

    • решаем вчерашнюю проблему #2 (с on_update):
      • CDA_DATAREF_OPT_ON_UPDATE введена.
      • Каналы PzframeDataRealize() теперь регистрирует с флагами ON_UPDATE и PRIVATE (это требование cda -- иначе может смешать в один dataref каналы с одним названием, но разными опциями).
    • PzframeGuiEventProc() наполнена кодом. И получены первые рисующиеся графики (шум около нуля; но и на CALIBRATE реагирует) -- ура!
    • Из пока недоделанного, скорее в епархии PzframeDataEvproc() и ProcessAdcInfo():
      1. НЕ собираются rflags от индивидуальных IS_FRAME-каналов, и НЕ OR'ятся в общие pzframe_data_t.rflags. Поэтому график пока НЕ расцвечивается. 25.05.2016: собираются, расцвечивается.

        P.S. А вот эти rflags вполне тянули бы на включение в pzframe_mes_t, если б таковой был. Но нету.

      2. Прописывание данных в plots[] делается в конкретных info2mes(). А в идеале бы -- автоматически, по описателю в fastadc_line_dscr_t.

        ...но пока оставим как есть -- оно так проще.

    • Кстати, похоже, есть идеологическая проблема, приводящая к сниженной до 1/цикл частоте обновления.
      • Ведь запросы на чтение мониторируемых каналов генерит cxsd_fe_cx, РАЗ В ЦИКЛ.
      • А в v2 оно делалось клиентом, прямо по получении данных.

      И что --

      • делать, чтобы для каналов, мониторируемых с условием CX_MON_COND_ON_UPDATE, запрос на чтение слался бы сразу же по получению данных?
      • В таком чистом виде это даст проблему бесконечной рекурсии на тут-же-возвращаемых каналах.
      • Добавить per-monitor флаг-семафор, что "сейчас монитор запрашивается", дабы при его взведённости запрос не происходил (а отрабатывался бы уже со всеми раз в цикл)?

        В принципе -- это терпимое решение, хотя и весьма некрасивое.

    • Есть еще одна странная проблема: ручка ISTART при первом нажатии почему-то генерит значение 0, хотя отображает "нажато".

      Видимо, какой-то косяк OpenMotif; ну или, по крайней мере, на уровне MotifKnobs.

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

      25.05.2016: да, откровенно косяк в OpenMotif. Есть и в v2 тоже, и в разных версиях RedHat.

    25.05.2016: сегодня Самойлов высказал пожелания, чего бы он хотел мочь делать в программе для ADC812ME (к которому ему подключат кучу сигналов): чтоб можно было смотреть разницу показаний между любыми двумя точками ЛЮБОЙ ПАРЫ каналов (т.е., наше "разница значений между реперами (в двух точках ОДНОГО канала)" -- не катит).

    Вроде запросы не бог весть какие, но вот "запросто" ж не сделать. А как? Что приходит в голову:

    • Специальная тулза "fastadc_tool", ...
    • ...с возможностью свободного указания нескольких источников данных, хоть из разных АЦП.
    • Выглядит сильно похоже на manyadcs_knobplugin.
    • Вот тут-то и понадобились бы "устройства-каналы" -- чтоб имена всех атрибутов канала имели бы общий префикс: LINE.data, LINE.on, LINE.cur_numpts, LINE.rangemin, ...
    • А может, всё-таки "по-простому" -- добавить специальных классов stdctl'ов, чтоб можно было сделать табличку, где бы селекторами выбирались каналы, между которыми считать разности. И, возможно, еще какой-то сервис/арифметика.

      Возможно, чтоб табличка жила в отдельном popup-окне (как "Stats...", или даже вместе с ним).

    25.05.2016: необходимые модификации внутренней организации:

    • Глобальная переделка системы evproc'ев:
      • Параметр 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.

      • Они заполняются одновременно с чтением данных, и даже в prv_data[] копируются.
      • Добавлено pzframe_data_t.rflags, заполняемая OR'ом флагов ото всех IS_FRAME-каналов.
      • Соответственно, они теперь используются при определении "state" для расцвечивания в pzframe_gui.
      • Чего НЕ сделано, хотя идеологически бы напрашивается:
        1. Перенос поля pzframe_gui_t.curstate в pzframe_data_t -- рядышком к rflags, из которых оно и получается.

          Но даже тип knobstate_t там недоступен -- стек слоёв так устроен (ну типа что *_data -- уровень cda, а уровень Knobs начинается только в *_gui).

        2. А в идеале -- и сама обработка тоже чтоб была в PzframeDataEvproc(), а не в pzframe_gui.

          Что потребовало бы вытаскивания метода "newstate" из _gui в _data, что сей момент не выглядит хорошей идеей.

    • 18.06.2016: (задумано еще тогда, а сделано только сегодня) добавлены аксессоры PzframeDataGetChanInt() и PzframeDataGetChanDbl()
      • чтоб код в куче мест не лазил руками в ...valbuf.i32 (т.к. там может быть вовсе не INT32), а просто говорил бы "дай мне значение канала".
      • Они возвращают 0(ok)/-1(ошибка), а запрошенное значение складывается по указателю.
        • Там делаются проверки на:
          1. номер параметра в допустимом диапазоне;
          2. параметр -- скаляр;
          3. тип является числовым (это встроено в конверсию -- допустимое конвертируется, а "default:" -- ERANGE.
        • Соответственно, после такой "добычи" в случае успеха код может (при надобности) уже напрямую лезть в cur_data[cn] за rflags и timestamp.
        • ...а если лезть не надо, то можно и результат не проверять -- значение возвращается всегда, просто при ошибке оно будет 0/NAN.
      • Сами аксессоры -- практически близнецы, отличие только в типе возвращаемого значения.
      • Ну и заиспользованы -- в pzframe_gui, fastadc_data; а в fastadc_gui.c::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().
        • Код "заказа канала" перепахан. Теперь он подготавливает разные триплеты {evmask,evproc,privptr2} в зависимости от типа и модификаторов канала.
        • Поскольку обработчику индивидуальных каналов -- 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).
      • Замечания:
        1. Для IS_FRAME-каналов флаг "немедленности" игнорируется и обработка всегда ведётся в группе по маркеру.
        2. Каналы с индивидуальным обновлением НЕ участвуют в вычислении info_changed НИКОГДА (независимо от наличия у них флага change_important), ибо обновляются "out of band", несинхронно с прочими и их участие было бы бессмысленно.
      • Хохма в том, что СЕЙЧАС параметр ELAPSED не выводится НИ ОДНИМ из имеющихся _gui-экранов.
    • Собственно обновление параметров на экране:
      • Конкретная ручка обновляется функцией UpdateParamKnob().

        Причём колоризация делается на основе rflags, а не просто KNOBSTATE_NONE (так было отловлено, что adc200me_drv.c

        Почему-то канал SERIAL показывается гусино-defunct'ным. 27.06.2016: причина понята еще 21-06-2016, а сегодня всё исправлено -- канал стал показываться нормально, без посинелости.

      • Она вызывается:
        1. Из EventProc по PZFRAME_REASON_PARAM.
        2. Из PzframeGuiUpdate() (а НЕ прямо из реакции на PZFRAME_REASON_DATA) -- чтобы при загрузке из файла тоже бы всё обновилось.

      Итак -- всё зажило!!!

    • Что НЕ сделано -- так это пересчёт параметров по {r,d} в 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'ы. Но как?

    • Проблема в том, что добычу данных pzframe_data организует целиком самостоятельно, НИКАК не сотрудничая с Cdr (ну дык -- у standalone-клиентов никакого Cdr и нет).
    • А все идеи, как интегрировать bigc/user/... в общую инфраструктуру строились на том, что описатели данных будут содержаться в .subsys-исходниках, чтоб Cdr могла организовывать "соединения с каналами".

      Но в нынешней ситуации всё очевидно не так.

      29.09.2016: однако см. комментарий (отдельным пунктом-разделом) ниже за сегодня.

    • Раз уж
      1. всё так грустно,
      2. но pzframe_knobplugin и потомков реализовывать надо, для чего от инфраструктуры требуется возможность указывать "базовое имя устройства",

        то, может, стоит реализовать их "впрямую", как некоторый хак: ввести специальный отдельный тип ручки, "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).
    • Cdr_via_ppf4td.c воспринимает тип ручки "pzframe".

    30.05.2016: а теперь собственно *_knobplugin.[ch]:

    1. Пофайлово:
      • pzframe_knobplugin.h: модификации косметические -- Knob/knobinfo_t->DataKnob, аналогично colalarm_t->knobstate_t.
      • pzframe_knobplugin.c -- переделка уже архитектурная:
        • Совсем другая схема добычи "ссылки на источник данных".
        • В v4 иное соглашение о том, что возвращается конструктором ручки: не виджет, а статус (виджет же помещается в k->w).
      • fastadc_knobplugin.h -- отличие только в прототипе FastadcKnobpluginDoCreate() -- int(DataKnob) вместо CxWidget(knobinfo_t*).
      • fastadc_knobplugin.c -- реализационное отражение тех различий.
    2. Общие идеологические изменения:
      1. Отныне DoCreate()'ам НЕ передаётся kpn, а постулируется, что он расположен в самом начале k->privrec -- как, де-факто, и раньше было.
      2. Генерация (и подсовывание в k->w) RedRectWidget'а переложена на PzframeKnobpluginDoCreate() (с его клиентов).

        ...конечно, весь этот RedRect -- лишь робкая попытка делать всё корректно, т.к. destroy'енье у нас всё равно отсутствует почти повсеместно; но всё же хоть что-то.

    31.05.2016: добиваем knobplugin'овость:

    • Конкретнодевайсовое:
      • Сделаны adc200me_knobplugin.h (вообще одна строчка с extern) и adc200me_knobplugin.c (тоже сильно упростился -- 21 строка против 43 раньше).

        ...сначала -- вручную.

      • Затем файлы adc200me{.c,_data.h,_gui.h,_knobplugin.[ch]} переведены на генерацию из обобщённых исходников, имеющих в имени на месте типа устройства ("adc200me") SRC_FASTADC.

        Работа мутная, состоявшая из 2 частей:

        1. Собственно генерация -- несложна: sed'ом паттерн DEVTYPE_LCASE заменяется на тип устройство маленькими буквами (adc200me), а DEVTYPE_UCASE -- заглавными (ADC200ME; заглавность делается tr'ом).
        2. Противнее оказалось объяснить make'у, что перед обычными действиями, начинающимися с генерации зависимостей, нужно произвести генерацию _{data,gui,knobplugin}.h -- т.к. они #include'атся файлами, из которых делаются .d.

          Т.е., просто напихано 6 штук (по количеству пар X.dep:G.h) зависимостей для .dep-файлов. Плюс отдельная для pzframeclient_knobset.d.

          ...при добавлении VCAMIMGS количество этих строк-зависимостей приподувеличится.

        Работа, конечно, была противной, зато впредь НЕ надо будет бесконечно дублировать/копировать код при добавлении новых типов устройств.

      • Чуть позже: поскольку -- благодаря выбранной парадигме -- в SRC_*.h НЕ содержится ничего, специфичного для fastadc, а только касающееся pzframe, то они были переименованы в SRC_ANYKIND_*.h, а использующие их правила обобщены с $(FASTADCS) на $(DEVS).
    • pzframeclient:
      • pzframeclient_knobset.c генерится из Makefile'а на основе списка девайсов.
      • pzframeclient.c скопирован из pult.c с минимальным добавлением регистрации knobset'а.
      • ...а вообще-то надо бы добавлять в синтаксис subsys'ов возможность указывать библиотеки knobplugin'ов для загрузки, и чтоб обычный pult.c умел такие библиотеки грузить (как и stand.c).

        Тогда не потребуется никакой pzframeclient и дублирования кода не будет.

        02.08.2016: а покамест просто исходник pult.c подшаманен так, чтобы позволять "внешние дополнения", и теперь pzframeclient.c делается симлинком туда.

      Проверено на простейшем тесте -- работает!!!!!!!!!

    01.06.2016: развиваем успех:

    • Добита поддержка второго устройства -- ADC812ME. Пока, правда, не проверена.

      Поскольку заполнять adc812me_data.c::adc812me_chan_dscrs[] 8-ками одинаковых каналов было лень, то сделана пара макросов -- DSCR_X8() и DSCR_X8RDS(), создающие сразу по 8 строк (второй отличается тем, что проставляет поле rd_source_cn). Надо будет в ADC4 аналогично сделать.

      Дальше надо слабать CAMAC'овский ADC200 (уже в нормальной микровольтной кодировке вместо байтовых кодов) и сделать vcamimg для ottcam'а.

    • ...вместо этого сделан C061621. Который не проверить никак вообще...

    03.06.2016: за вчера и сегодня:

    • Сделана оставшаяся четвёрка -- ADC200, ADC502, ADC333, ADC4.

      В последней паре (4-канальных) для *_chan_dscrs[] используются макросы DSCR_X4() и DSCR_X4RDS(), аналогичные 812'шным.

    03.06.2016:

    • Пилим vcamimg:
      • vcamimg_data.[ch]: сильно упростились, поскольку выкинуты части, так и не потребовавшиеся:
        • mes: отсутствует, он в v2-то содержал только pzframe_mes_t (коий в v4 рассосался), а специфичного ничего не было. Соответственно, и info2mes тоже отсутствует.
        • dcnv: аналогично отсутствует, т.к.:
          • никаких "преобразований, вариабельных в зависимости от типа устройства" -- нет;
          • пересчёта данных -- {r,d}, dpyfmt -- нет;
          • да и множественных линий тоже нет.
        • Следственно, и генерация PSP-таблицы для dcnv тоже отсутствует.

        Если что-то из этой толпы вдруг занадобится (хотя идеологически неясно, зачем бы), то вернуть будет несложно.

      • Но изрядное отличие -- поскольку в pzframe_data теперь нет канала "данные" (умолчательного источника всего), то добавился параметр data_cn, указывающий на ячейку с данными.
      • vcamimg_gui.[ch]: тоже в основном копирование, с модификацией касательно источника данных -- теперь используется data_cn и описатель канала, на который он показывает.
      • vcamimg_{main,knobplugin}.[ch] -- тривиальные копии-с-адаптацией соответствующих fastadc_*.
    • Кстати, проверено функционирование кнопок [>] и [|>]: в mini-toolbar'е работают как надо. А в оконном тулбаре -- будто их и нету вовсе.

      23.09.2016: ну естественно -- реакции-то в pzframe_main.c::CommandProc() не было. Добавлена -- работает. Вот только "отражения" состояния в кнопках нету, т.к. v4'шный Xh не поддерживает XhSetCommand*(). Часом позже: в Xh прототипы добавлены (реализация пока "пустышками"), посему и в pzframe_main.c вызовы раскомментированы.

    • Кстати 2: почему-то на ADC200ME кнопка [Stop] на экране никакого эффекта не даёт. Хотя до драйвера её нажатие явно доходит.

      18.06.2016: разобрались, дело в лишнем вызове read_measurements() в pzframe_drv (см. в его разделе).

    04.06.2016@дома-ближе-к-полуночи (закончено в 23:40): решил по-быстрому спортировать ottcam -- благо, там всё проще, чем у fastadc'ов:

    • Да, сделано. И действительно несложно.

      Вопрос только -- где именно окажутся косяки или вообще будет падать.

    • 05.06.2016@дома: проверено.
      • В ottcam_drv.c допилена-таки симуляция (летающий квадратик с градиентом по диагонали, включающийся по "-" в auxinfo).
      • И -- запахало сразу же!!!

        (Ну да -- параметры настроек синие, т.к. игнорируются, но это совсем неважно.)

      Ура, работает!!!

    05.06.2016: сборка hw4cx/pzframes/ включена в hw4cx/Makefile на общих основаниях.

    06.06.2016: неудобно, что если имя канала указано неверно, то никакой диагностики нет -- ни почернения, ни на stderr. Фиг поймёшь ошибку.

    07.06.2016: кстати, ручки ПАРАМЕТРОВ уже сейчас чернеют при ненайденности канала.

    10.03.2021: а вот и не всегда и не совсем! (возможно, в т.ч. из-за необходимости "маркера", которого не будет) Подробнее см. ниже в специально созданном разделе от сегодня.

    Но чтоб это произошло, нужно чтобы произошло событие "маркер" (которого при ненайденности не будет).

    Нужно:

    1. У маркера ловить событие RSLVSTAT, и по нему проставлять NOTFOUND вообще всем каналам (в cur_data[*].rflags), после чего как-то передавать это событие на уровень gui, чтоб он всем param-ручкам сделал SetSimpleKnobState(,KNOBSTATE_NOTFOUND).
    2. Поскольку у кадровых каналов -- LINEn -- своих ручек нет (а чернить весь график малополезно), то в fastadc_gui.c надо чернить соответствующие FASTADC_GUI_CTL_LINE_LABEL (потом, в случае появления канала -- т.е., при исчезновении флага NOTFOUND -- не забывая вернуть нормальное состояние).

      Это всё для хитрых случаев, когда часть линий по какой-то причине будет отсутствовать, а потом вдруг появится. Т.е., для хитрых комбинаторных устройств (вроде manyadcs) либо для композитных, составляемых cpoint'ами (в т.ч. из разных серверов -- актуально будет после запинывания UDP-резолвинга).

    После обеда: сделано.

    1. По ручкам:
      • RSLVSTAT у маркера ловится, и флаг ненайденности прописывается. После чего...
      • ...введён PZFRAME_REASON_RSLVSTAT=5 (как cda'шный (25.08.2016: вместе с ним переведён на =10.)), это событие генерится PzframeDataEvproc()'ем и ловится PzframeGuiEventProc()'ем, уставляющим чёрное состояние всем param-ручкам.
    2. По линиям fastadc'ов:
      • После получасового рытья (и разбирательства в последовательности производимых действий и областях ответственности) было принято решение поселить эти "мозги" в FastadcGuiRenewPlot().
      • Раньше ссылки на simpleknob'ы меток-выключателей нигде не хранились, а теперь для них введён массив label_ks[].
      • И тут вылез косячок: оно-то почерневало, но при нажатии на чекбокс вдруг становилось обычным (до следующего обновления данных).

        08.06.2016: следующее сделано уже сегодня, но запишем тут же, для целостности.

        • (@вчера-вечер-дома-душ) Возникла мысль, что set_knobstate() делается кем-то при отработке "действия" ручки (кем-то вдоль длинной цепочки, через которую этой действие проходит, чтобы быть -- обычно -- в конечном итоге отправлено серверу).

          ...как выяснилось позже, это было верное предположение, но дальше разборки пошли не в ту степь.

        • Почему-то подумалось, что ("хрен знает, где как и почему" (позор!!!)) KNOBSTATE_NONE ставится при любом юзер-обновлении ручки, и потому надо почернение вытащить в отдельную функцию, чтоб её дёргать как из FastadcGuiRenewPlot() (для всех), так и из ChnSwchKCB().
        • Эта функция была сделана -- SetChnSwchState(), а потом...
        • ...на основании чего-то (вспомнить бы, чего... гениальное озарение?) было решено, что как-то влияют currflags и надо бы заодно и в них записывать CXCF_NOTFOUND.

          А потом -- в порядке оптимизации/управильнивания --

          1. Записывать не конкретный флаг, а просто копировать флаги от соответствующего data_cn.
          2. И состояние ставить не "явно NOTFOUND" по наличию флага, а делать choose_knobstate(k,k->currflags).
        • И выяснилось, что прописывания currflags при обновлении достаточно -- в ChnSwchKCB() ничего делать и не надо!
        • Нашлось и объяснение: set_knobstate(), на основе currflags, делается в set_knob_controlvalue() при fromlocal.

          Коий прямо вызывается из MotifKnobs_SetControlValue(), дёргаемого CB()'ами всех типов ручек (включая onoff) для передачи "действия".

    23.06.2016: в сторону избавления от info2mes() -- делаем в ProcessAdcInfo():

    • В цикле по линиям на основании line_dscrs[] добываются
      1. numpts: может быть указан константой (через '-');
      2. on: при неуказанности источника считается за =1;
      3. x_buf, x_buf_dtype: в зависимости от on (при on==0 -- NULL,CXDTYPE_INT32,numpts:=0).
      4. cur_range: оказалось самым хлопотным, ибо:
        • К fastadc_line_dscr_t добавились поля range_min_cn и range_max_cn.
        • Во всех TYPE_data.c их пришлось заполнить (и .range тоже!), для чего -- воизбежание повторов и нечитаемости -- заполнение переведено на макросы LINE_DSCR() везде, кроме c061621.
        • Собственно заполнение -- пара (min,max) 2-уровневых if()'ов (1: есть ли; 2: INT/не-INT).

        По умолчанию прописывается значение константного 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, требовавшимся лишь для него же).
    • Всеобщий dpyfmt переименован в common_dpyfmt, поскольку он теперь "умолчательный" -- если по-линейные не указаны.
    • Более ненужный line_units_list[] также убран.
    • Как и line_names_list[].
    • ЗАМЕЧАНИЕ: наличие line_dscrs[] теперь ОБЯЗАТЕЛЬНО, размером в num_lines.

    28.06.2016: делаем пересчёт по {r,d}.

    • Во-первых, складирование {r,d} перетащено из fastadc_data (где оно было по недоразумению) в pzframe_data.

      В pzframe_chan_data_t добавлен буфер rds_buf[PZFRAME_DATA_RDS_MAX_COUNT*2] (да, 20*2double=320байт; но у нас максимум 100-150 каналов в АЦП, 32кБ -- ну переживём) и количество -- rds_count.

    • Заполняются они в ProcessOneChanUpdate() КАЖДЫЙ РАЗ -- халтура!!!.
    • Насчёт связки с XhPlot: там есть понятия "pvl" и "dsp" -- raw2pvl и pvl2dsp.

      Так вот -- raw2pvl нужна только для REPR_INT, и она может не указываться (=NULL), тогда будет использоваться дефолтная, просто преобразующая int в double.

    • НАДО также в line_dscr_t указывать НАЧАЛЬНЫЙ "r" -- чтоб подписи делать.

    28.06.2016@вечер-уходя-с-работы: та архитектура выглядит кривовато:

    • хранение 20 пар {r,d} на канал;
    • самостийная конверсия;
    • да еще и добываются они как попало.

    А можно бы следать лучше:

    • Конверсией пусть занимается cda -- ввести для этого специальный вызов, принимающий ссылку на канал и double, который надо сконвертить.
    • Соответственно, gui_pvl2dsp() пусть его и вызывает.
    • ...но также нужно ловить факт "полученности" калибровок, чтобы при отсутствии использовать тот "начальный r" (это нужно будет при старте, ДО коннеекта).

    29.06.2016: реализуем:

    • Навороченное вчера с rds_count/rds_buf[]/_RDS_MAX_COUNT убрано.
    • Введена 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).
      • При регистрации у cda "обычных" (не-MARKER и не-IMMEDIATE) каналов теперь вешается обработчик PzframeRDsCEvproc() с маской RDSCHG, который и генерит событие из предыдущего пункта.
      • А оное событие обрабатывается в fastadc_data.c::DoEvproc(), который тупо идёт по списку линий и смотрит, что если указанный для этой линии data_cn совпадает с об-RD'шившимся, то взводит в 1 nl'ную ячейку в ...
      • ...свежевведённом fastadc_data_t.lines_rds_rcvd[], ...
      • ...коий и проверяется в gui_pvl2dsp().
      • Дополнительно: cda-событие RDSCHG трактуется как принудительный "info_changed", для чего введено поле rds_had_changed, взводящееся по событию и используемое в качестве старта для info_changed и затем сбрасываемое.

        10.04.2022: "взводящееся", ага! ПОДГОТОВЛЕНО тогда было, а вот взведение =1 было забыто. Сегодня добавлено; надо посматривать, не вылезет ли где чего...

    • ЗАМЕЧАНИЕ 1: эта архитектура с обращением gui_pvl2dsp() напрямую к cda плохо подойдёт для manyadcs. Там придётся как-то изворачиваться. 01.07.2016@утро-душ: очевидно, надо подтасовывать свойства (в atd->line_dscrs[] плюс в refs[]) -- подсовывать ref от реального source-канала. И, кстати -- такой "составной" осциллограф надо будет попытаться обобщить, он может быть полезен и в других местах (тот же two812ch -- не его ли подвид?). @вечер-613-я: и да, и нет: нет, two812ch -- НЕ "подвид" manyadcs; но да -- two812ch является как раз тем самым общим "над-осциллографом".

      P.S. И отдельно -- а с сохранёнными файлами как, при оффлайн-загрузке их?

    • ЗАМЕЧАНИЕ 2: несколько мутна перспектива использования поля rd_source_nc -- раз конверсия переложена на cda (а не выполняется самостоятельно), то поле так и не задействовано.

      Хотя в принципе и можно бы "руками":

      1. При регистрации каналов с rd_source_nc>=0 выставлять NO_RD_CONV.
      2. При дОбыче значения вместо просто "cda_get_ref_data()" делать:
        1. cda_get_ref_dval()
        2. cda_rd_convert(...rd_source_nc,...)
        3. Складировать в зависимости от dtype.

      Конечно, это прокатит только со скалярами, но иное нам пока и не требуется.

    14.07.2016: тогда, полмесяца назад при разбирательстве с pvl/dsp, забыто было доделать оное "для собственных нужд fastadc_gui" -- для реперов.

    • А анализ v2'шного кода -- всяких Save -- показывает, что всё-таки нужен "GetDsp", причём на уровне _data, а не _gui.

      Соответственно, и "gui_pvl2dsp()" должен не сам всю конверсию делать, а пользоваться _data'шным методом.

    • Делаем:
      • Введена FastadcDataPvl2Dsp(), содержащая код конверсии, сделанный 29-06-2016 в gui_pvl2dsp().

        В отличие от v2, тут НЕТ параметра "mes".

      • gui_pvl2dsp() переведена на неё.
      • Введена FastadcDataGetDsp() -- калька с v2'шной.

        Для её удобства внутри есть static inline функция FastadcDataGetPvl() -- также почти калька с v2'шной и Xh_plot'овского куска кода, но только безо всяких raw2pvl.

    • Ну и использование в реперах:
      1. При отрисовке -- просто раскомментирован "старый" код.
      2. При создании (в вычислении ширины) -- чуть более развесистый код, сильно отличающийся от старого вследствие разницы в описании линий.

    12.09.2016: перетаскиваем библиотеки из hw4cx/pzframes/ в 4cx/src/ -- include/ и lib/pzframe/.

    • Собственно библиотеки и их .h-ки перенесены.
    • hw4cx/pzframes/Makefile адаптирован (удалено то, что касалось их сборки).
    • sw4cx/xmclients/Makefile тоже.

    27.09.2016: добавлена ("доделана") поддержка leds.

    • В отличие от v2, где об индикации на тулбаре в standalone-варианте также заботился pzframe_gui, тут LEDы на тулбаре и в cpanel'е полностью разделены -- это 2 независимых куска, просто cpanel'ный при наличии тулбара отключается (хотя мог бы и присутствовать).
    • pzframe_main.c содержит код, аналогичный Chl_app.c'шному (туда на прошлой неделе было сделано), самостоятельно заботящийся о тулбаре.
    • Соответственно, во всех {fastadc,vcamimg,wirebpms}_gui.c::PopulateCpanel() действия сократились до простого вызова PzframeGuiMkLeds().
    • PzframeGuiMkLeds() же проверяет, что local_leds_form!=NULL, и считает это сигналом о необходимости сделать "маленький" LEDs.

      Ему теперь не передаётся ни parent, ни in_toolbar -- всё фиксированно и просто.

    • Только одна проблема: в развесистых программах -- например, liu.subsys -- осциллографичности теперь снабжены длиннющей линейкой лампочек.

      По логике, в программах с тулбаром спокойно можно делать look.noleds=1.

      Да, так и сделано -- мозги на эту тему добавлены в PzframeKnobpluginDoCreate().

    10.01.2017: странный косяк вылез -- тогда незамеченный, но именно тогда появившийся (проверено тестированием версий за разные даты): конкретно v5h1adc200s.subsys почему-то стал пустым -- только statusLine, а workSpace пуст. Точнее, в нём лежит lrtbForm размером 1*4 пиксела, и даже её растягивание (включением resizable) не помогает -- пустота.

    Включение тулбара (убирание notoolbar) ситуацию спасает: содержимое появляется. Очевидно, за счёт того, что leds появляются на тулбаре, а в pzframe_knobplugin'ах при этом отключаются.

    11.01.2017: в продолжение расследования:

    • Найден также другой вариант, в котором всё работает: используем в качестве корневого виджета-ручки scroll, в котором и располагаются осциллографы -- вместо lrtb с grid'ом внутри.

      Заодно оно и resizable сделано.

      Там есть свои косяки -- после схлопывания cpanel'а по [-] он обратно по [+] не разворачивается, пока не дрыгнешь размер (хоть ресайзом окна, хоть сдвигом "шторки" split'тера).

    • Последовательным отключением кусочков кода найдено место-источник проблемы: виджет-сетка, в которую вносятся изменения в MotifKnobs_leds_grow().

      Если этот блок кода отключать -- т.е., не создавать в сетке child-виджетов -- то всё окей. (Можно ей размер (в клетках) увеличивать -- не влияет; а вот создание child'ов -- ёк...)

      Видимо, опять косяк с XmSepGridLayout, в обсчёте геометрии -- оные уже встречались (например, при hfill'е всех ячеек в колонке эффект аналогичный).

    • Пока в качестве паллиативного решения будем использовать вариант v5h1adc200s.subsys на основе scroll'а.
  • 09.06.2016: чуть в сторону -- об "xs". В некоторых случаях девайсового CHAN_XS_PER_POINT в виде просто множителя недостаточно.

    09.06.2016: Конкретно:

    1. ADC333: у него xs="us", а квант времени в самом быстром режиме -- 0.5us (в v2 внутренние расчёты шли в наносекундах).
    2. ADC200 (CAMAC): xs="ns", но есть режим 195MHz, в котором вес кванта 1e9/195e6=5.128.

    Напрашивается идея сменить один параметр "МНОЖИТЕЛЬ" на пару "*МНОЖИТЕЛЬ/ДЕЛИТЕЛЬ". У кого всё просто -- указывают только множитель, а делитель=0, и он считается =1.

    10.06.2016@утро-дома: и еще:

    • ЕманоФедя давно для работы по внешнему таймеру (когда квант реально неизвестен) требовал возможности указывать некий внешний канал, откуда считывать размер этого кванта.
    • Так вот -- можно:
      1. Ввести -- на уровне fastadc_data -- возможность указывать имя канала "вес кванта при внешнем таймировании" (в options у knobplugin'ов и в argv[] у main).
      2. Факт "внешнести" указывать отдачей канала "МНОЖИТЕЛЬ" XS_PER_POINT=0. Чтоб при наличии канала "внешний вес" бралось его значение, а при отсутствии -- просто 1.

    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").

    Обсуждение:

    • Тогда от параметра-понятия "xs" можно будет избавиться, заменив его на "xs_factor_chan_n" и "def_xs_factor" (последний будет использоваться
      1. ДО получения значения канала;
      2. для "artificial" устройств, вроде manyadcs.
    • само использование, конечно, не совсем очевидно/тривиально: как именно переводить просто числа в масштабируемые (с кратностью 3 порядка)?

      Видимо, написать в столбец несколько примеров.

    • ГДЕ это должно быть -- тоже некоторый вопрос. То ли в fastadc (тогда -- в fastadc_data), то ли в Xh_plot.
    • И еще: для случая "ext_timing без внешнего размера кванта" ядиницы времени должны подписываться как просто "x".

    11.06.2016@утро-пляж: насчёт "ext_tim_point_size": вопрос, в чём этот канал должен быть.

    • Видимо, в тех единицах, которые специфичны для данного типа АЦП. И если в системе несколько типов -- то канал "внешняя длина кванта" может адаптироваться для них разными cpoint'ами.
    • И, очевидно -- из предыдущего -- должны поддерживаться разные типы/представления: как INT, так и FLOAT.
    • 12.06.2016@ключи-вечер: А вот и нифига!!! Этот канал должен при exttim ПОЛНОСТЬЮ заменять канал XS_PER_POINT, т.е., быть в INT'ах (точнее, fastadc_data будет его регистрировать как INT32).
    • 20.06.2016@около-гаража-кондиционерщиков-~17:00: а можно в случае "exttim" считать этот канал сразу за CXDTYPE_DOUBLE и просто домножать на него x, а XS_DIVISOR не использовать.

      @там-же, пятью минутами позже: Но вот тоже нифига! Т.к. в этом случае модель "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 ссылки на параметры, которые каналы, некоторые можно сделать "хитрыми":

    • >=0 -- номер параметра;
    • ==-1 -- нету;
    • <1 -- сразу "immediate"-значение:
      • для канала XS_FACTOR -- просто оно (-3, -6, -9, ...);
      • для XS_PER_POINT и XS_DIVISOR -- значения с обратным знаком от реальных (-250=>250).

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

    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: делаем.

    • Введён "dcnv"-параметр ext_xs_per_point. Получаемое значение складывается в adc->ext_xs_per_point_val.
    • Общее положение: все данные для работы с временами должны быть прямо в fastadc_mes_t. Чтобы
      1. Структура mes была бы самодостаточной.
      2. Кроме mes могла б быть отдельная svd (хотя при отображении на экране всё равно будет использоваться только mes -- т.к. подписи к горизонтальным осям в одном экземпляре).
      3. Достаточно было бы 1 указателя -- mes_p, и не требовалось бы к нему еще и указатель на adc-содержателя передавать.
    • Соответственно, в ProcessAdcInfo() выполяется:
      • Копирование в adc->mes текущих значений.
      • Копирование ext_xs_per_point из adc->ext_xs_per_point_val.

        Т.е., значение "внешней частоты" фиксируется в момент обновления кадра (когда она и актуальна), а при последующих её обновлениях -- на текущую картинку никакого влияния не оказывается.

      • Определение "exttim = (xs_per_point == 0)".
    • Замечание: в будущем, при добавлении загрузки из файла:
      1. значение ext_xs_per_point будет браться из файла;
      2. затем всё будет так же актуализироваться при вызове ProcessAdcInfo().
    • Введена FastadcDataX2XS() (параметр -- mes_p), реализующая логику "как пересчитать по имеющимся данным x в xs".

    07.07.2016: вводим в драйверах каналы XS_DIVISOR и XS_FACTOR.

    • В c061621, adc4, adc812me всё было просто -- тупо вставить 2 канала после XS_PER_POINT, сдвинув на 2 вверх следующий блок.

      Кстати, в adc502.devtype был косяк -- часть каналов от adc200 например, (frqdiv).

    • adc200, adc502, adc200me: карты раздвинуты на 10 каналов, с 70 до 80.
    • adc333: всё, начиная с LINE0ON, сдвинуто вверх на 10 (с 40 на 50) -- место там в конце было. А уже потом вставлена пара каналов с раздвижкой группы.

      И, за компанию, adc4 раздвинут аналогично -- тоже ж 4-канальный, всё похоже.

    • Неприятный прикол: а если стоит "ext" и используется "frqdiv" (возможно в ADC200, ADC812ME; плюс ADC333 -- у него по числу каналов) -- что тогда?
      • С одной стороны, "xs" должно браться с внешнего канала.
      • С другой же, оно должно бы учитывать делитель (умножать на коэффициент), но как бы оно отразилось во внешнем канале?
      • ...разве что в параметр "XS_DIVISOR" это вносить...

    08.07.2016: продолжаем.

    • Новые параметры были добавлены во все драйверы.
      • Пока "вчерновую" --
        1. в таблицы chinfo[] (как AUTOUPDATED -- чтоб маркировались как TRUSTED);
        2. возврат их значений при инициализации.
      • Значение FACTOR сейчас повсеместно =-9 -- в т.ч. у 333-го, с его микросекундами, т.к. минимальное время -- 0.5us/канал.
      • DIVISOR же у всех разный. Там, где всё просто -- 0 (т.е. 1); где хитрее, как у adc200 и adc333 -- 1000.

        В будущем же, по-хорошему, надо бы у всех, допускающих внешнее таймирование, делать 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, так что вместе с измерением назад не вертаются.

    @вечер-дома:

    • Решение очевидно: нужно вместо "ручного" подправления в cur_args[] исправлять также и в 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 в драйверах:

    • У всех затронутых драйверов -- 200, 333, 812 -- тип этого параметра сменен с AUTOUPDATED на STATUS, и значение теперь отдаётся не при старте, а вместе с каждым измерением.
    • "Базовое" значение у всех 1000, и только у 333-го -- 1008, чтоб нацело делилось на 2, 3, 4 (ключевое тут -- 3); традиционно (в ReadMeasurements() аналогично 108).

      03.11.2018: почему тогда НЕ было сделано xs_divisor=1000 у ВСЕХ?! Подробнее на эту тему ниже, за сегодня.

    29.07.2016: одна недоработочка: для вычисления FastadcDataX2XS() нужно также знать значение PTSOFS. Во всех индивидуальных *_x2xs() оно просто прибавлялось к x, но в fastadc_data.c его взять негде -- забыли!

    Добавляем:

    1. Поле fastadc_type_dscr_t.common_cur_ptsofs_cn.
    2. Параметр FastadcDataFillStdDscr()::common_cur_ptsofs_cn.
    3. Поле fastadc_mes_t.cur_ptsofs.
    4. ProcessAdcInfo(): добыча+сохранение значения в оное поле.
    5. FastadcDataX2XS(): x+=mes_p->cur_ptsofs.
    6. Всехние *_data.c::*_get_type_dscr(): передача номера CUR_PTSOFS'а Fill()'у.

    03.08.2016: и еще кое-чего в модели не хватает для полноты: СДВИГА. Т.е., из "seconds=offset+X*xs/divisor" множитель -- есть, а offset -- отсутствует.

    • Точнее, "сдвиг" присутствует в виде PTSOFS'а, но он прибавляется к "x", а к готовому значению -- нет.
    • Потенциально занадобилось для VSDC2 -- там вначале есть некоторое "мёртвое время" (32 такта).
    • Что делать?
      1. Сейчас можно забить -- пока что при надобности можно обойтись PTSOFS'ом, форся его значение минимум в эти 32.

        Хотя для VSDC2 вообще можно забить на всё-про-всё -- там осциллограммы являются побочным средством, а "качество" их вычитывания (кривой протокол) в принципе не позволяет рассматривать их серьёзно, поэтому от них нужна только форма сигнала, а на времена плевать совсем.

      2. Если же припрёт -- то придётся вводить параметр/канал "XS_OFFSET", в не очень ясно, каких величинах (наверное, в конечных XS'ах, т.к. в исходных X'ах можно играться PTSOFS'ом).

    30.09.2016: раскомментирован "блок вычислений касательно XS" в конце ProcessAdcInfo() -- иначе не работала FastadcDataX2XS() (уже используемая для сохранения).

    Резон -- уже во всех текущих драйверах есть отдача нужных параметров.

    На вид -- всё работает корректно.

    ...этак скоро можно будет от info2mes() избавляться -- всё уже есть.

    19.07.2017: завершение перехода на FastadcDataX2XS() -- в fastadc_gui.c оставалось 3 точки.

    1. UpdateRepers()
    2. fastadc_gui_mkstdctl(), ветка FASTADC_GUI_CTL_REPER_TIME
    3. gui_x2xs()

    Довольно несложно, и вроде всё работает.

    03.11.2018: вот понадобилось наконец-то пользоваться фичей "ext_xs_per_point", чтоб осциллографичности показывали осмысленные времена, а не просто номера отсчётов. И оказалось, что фиг-фиг-фиг -- не особо-то оно готово к использованию.

    • Тогда, в 2016-м, НЕ была сделана отдача xs_divisor у всех, а лишь у adc333, adc200 и adc812me (через год добавились adc4x250 и adc1000).

      Т.е., у тех, где либо есть параметр/канал FRQDIV, либо используется мультиплексируемый АЦП (adc333).

    • Что порождает вопрос: а как сейчас вообще функционируют fastadc_gui у всех прочих типов -- учитывая, что вычисление делается в FastadcDataX2XS(), БЕЗ использования типоспецифичных _x2xs(), а все "вычисления" выполняются в fastadc_data.c::ProcessAdcInfo()?

      Или секрет в еще не убранном вызове типоспецифичных _info2mes()?

      Или у прочих отдаются НЕ скалированные на 1000 значения в XS_PER_POINT? Судя по adc502_drv.c -- именно так.

    • Отдельная проблема -- точность: если divisor=1000, и канал ext_xs_per_point считается целочисленным, то выходит, что точности хватит лишь на 1000 точек.

      Например, при периоде 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: начинаем реализацию "wirebpm" -- для поддержки ИПП.

    16.07.2016: копированием из vcamimg'а, с последующей контекстной заменой vcamimg->wirebpm, Vcamimg->Wirebpm, VCAMIMG->WIREBPM.

    Стандартные имена переменных/полей такие:

    • adc/vci -- bpm (wirebpm_data_t, сам объект).
    • atd/vtd -- wtd (wirebpm_type_dscr_t, описатель типа).
    • a/v -- b (wirebpm_data_t-поле внутри wirebpm_gui_t).

    21.07.2016: продолжаем.

    • Выкидываем vcamimg-специфичности:
      • поля в data и соответствующие параметры в ...Init();
      • поля в gui;
      • CTL_nnn и их реализацию.
    • Добавляем:
      • Поле-виджет DrawingArea называем hgrm (как в старом ippclient'е).
      • Единственный специфичный тип stdctl'а -- CTL_MAGN.

    21.08.2016: на этой неделе после почти месячной паузы продолжаем.

    Краткий обзор принятых рхитектурных решений:

    1. Единственный конкретный тип назван прямо u0632, безо всяких _hist или _spectr. Конкретный вид отображения указывается wirebpm_gui через параметр kind -- как раз "hist" или "spectr".

      Это всё ПОКА.

    2. Насчёт внутренностей реализации wirebpm_gui.c:
      • Никаких "старых" (prev, shadow) пока не поддерживается. Ибо это в первую очередь работа pzframe_data, где оное пока отсутствует.
      • И, естественно, никакого "набора фона" теперь нет -- ибо оно переехало в драйвер. Следствие -- изрядное упрощение.
      • Состояния (knobstates) теперь отображаются цветом не столбцов, а фона -- в точности как в fastadc+Xh_plot+Xh_viewport (реализация слизана оттуда).

        Да, OVERFLOW конкретных столбиков оно теперь показывать не будет (а реально оно никогда не происходило -- из-за 14-битного формата данных). Зато DrawHist() сильно упростится.

      • Почему в v2'шном ippclient.c данные имели тип double -- загадка, т.к. по факту обращение с ними было как с целочисленными. Видимо, исключительно потому, что cda_getphyschanval() отдаёт double; но что мешало использовать cda_getphyschanraw() -- неясно, она ж появилась еще в 01-2004.

        Или всё из-за набора фона, который мог быть дробным (ибо 5 циклов)?

    3. Поскольку конкретно u0632 является очень-многоканальным устройством, то для адресации pzframe-каналов используются не U0632_CHAN_..., а локальные C_..., вводимые в u0632_internals.h.
    4. В linipp.subsys управление введённостью/выведенностью датчиков вынесено в заголовок (nattl=1), поэтому с ручками на cpanel'е оно теперь вообще никак не интерферирует (а раньше была замутная архитектура -- те ручки (которые теперь parknob'ы и даже magn!) определялись в группировке, а ipp-knobplugin'ы с ними хитроумно коннектились.

    22.08.2016: продолжаем.

    • Адаптация DrawHist():
      • С одной стороны, она изрядно упростилась.
      • С другой -- увы, полностью ушла универсальность/отвязанность от специфики ipp/wirebpm.
        • Явно передаётся параметро wirebpm_gui_t *gui.
        • Никуда не делись "магические числа" из enum'а, вроде PIXELS_PER_ROW и "/ (1 << 13)" (последнее -- диапазон).
        • И явно прошита int32'шность, вместо использования dtype.
      • По-хорошему, надо бы:
        1. В дополнение к PzframeDataGetChanInt() и PzframeDataGetChanDbl() создать аналогичные аксессоры к элементам ВЕКТОРОВ: указывать номер ячейки, и чтоб оно корректно работало и со скалярами.
        2. А еще лучше -- как-нибудь формализовать "работу с гистограммами" аналогично тому, что сделано с графиками.

          Может, через тот же plotdata? Там ведь уже есть аксессоры.

    • Вводим "виртуализацию отрисовщиков".
      • Как бы "4-й уровень иерархии" (после _data и _gui, но перед _knobplugin/_main) -- подвид отрисовки.

        Сейчас оно живёт полностью внутри wirebpm_gui.[ch], но при крайней надобности теоретически может быть и доформализовано и выделено в отдельные файлы/модули.

      • Указатель на таблицу виртуальных методов -- wirebpm_gui_t.wiregui_vmt_p. Да, оно полностью отделено и от pzframe_data_vmt, и от pzframe_gui_vmt.
      • В таблице сейчас 2 метода:
        1. do_tune -- "произвести донастройку". Вызывается из WirebpmGuiRealize().

          Сейчас аллокирует GC, а в перспективе может и размеры задавать.

        2. do_draw -- отрисовка.
      • Прописывает значение в wiregui_vmt_p -- WirebpmGuiRealize(), на основе значения look.kind.

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

    23.08.2016: добиваем -- собственно сами отрисовщики изготовлены.

    • Проверено на симуляции данных (тоже взятой из старого ippclient'а -- синусом) -- рисует.
    • ESPECTR пока с обкорнанным функционалом, без реперов и вычислений.
    • Для полного функционала нужно будет всё-таки вытащить создание hgrm'а в do_tune() -- чтоб EventHandler вешать.
    • Короче: кодочек получился тот еще, похлеще fastadc. Но работать будет.

    13.10.2016: добавляем к ESPECTR'у реперы и вычисления (благо, гибкая настройка формул уже сделана).

    • Неудобство: метод do_tune() вызывается ДО создания виджета hgrm, а нам надо навешивать event-handler, т.е., уже ПОСЛЕ.

      Явно нужно ДВА метода tune -- пред-настройка и пост-настройка. Так и сделано:

      • Пред-настройка -- теперь do_prep() (у HIST'а есть только он).
      • Пост-настройка -- do_tune().
    • Собственно поддержка "реперов"-ворот сделана.
    • Для простоты решено "формулы вычисления по столбцам" не использовать.
      • Причины:
        1. Во-первых -- лень.
        2. Во-вторых, там мутновато, какая именно должна быть формула.

          Заранее думал, что параметр -- число частиц. А вот нифига: число частиц на результат вычисления просто умножается, а параметром выступает номер столбца.

      • Посему -- вычисления прямо в коде, как и в v2'шном ippclient'е.
      • Но где-то ж надо добывать внешний параметр -- ток. А он как раз формулой считается (т.к. это сумма 2 токов -- магнит-спектрометр нынче управляется 2 источниками).

        Детали -- в следующем пункте.

    • Как сделана добыча тока через формулу:
      • Оно попадает в категорию "dcnv".
        • Поэтому добавлено поле wirebpm_data_t.dcnv, имеющее тип wirebpm_dcnv_t, содержащий единственное поле ext_i_fla (тип -- char[200]; стыдоба, но вроде хватит).
        • Соответственно, парсинг и использование -- в компетенции wirebpm_data (аналогично fastadc'шному ext_xs_per_point).
          • Парсинг: добавлена WirebpmDataGetDcnvTable(), возвращающая указатель на dcnv-таблицу.

            Это именно "Get", а не "Create" (как в fastadc).

            • Поскольку нет заранее неизвестного списка линий, то и генерить никакой таблицы не нужно, она просто фиксированная.
            • Соответственно, free()'ить её не нужно.
          • Использование: да полностью аналогично fastadc'шному ext_xs_per_point:
            • Обработчик ExtIEvproc(), добывающий значение и складирующий в ext_i_val.
            • По обновлению данных -- в DoEvproc -- оно перекладывается в mes.ext_i.

              ...да, ради этого шага пришлось добавить и поле mes, и метод .evproc.

          Забавно, что тут dcnv есть, а в vcamimg -- нет; а понадобится ли?

        • Имя параметра -- ext_i_fla, и это "fla" (а не "src"!) подчёркивает, что оно является именно формулой. Краткое обсуждение:
          • Да, в идеале надо было б сделать детерминирование, к какому виду относится указанное, и в зависимости от этого регистрировать разное -- в точности как в Cdr_treeproc'ном cvt2ref().
          • ...прямо хоть публикуй интерфейс cvt2ref()!
            • Впрямую это делать нельзя ну никак: ведь Cdr, а использование требуется в _data.

              ...ну да, идеологически оно как бы выполняет функции уровня Cdr, но всё же.

              (И вообще, в lib/pzframe/ зависимости от Cdr отсутствуют.)

            • Перетаскивать для опубликования в datatree? Тоже криво -- т.к. входит-то эта работа в прямую компетенцию именно Cdr (Data Representation).
            • Если так подумать, то текущая потребность -- "иметь подготовленными для использования knobplugin'ом некоторого количества ссылок на каналы" -- чётко пересекается с продуманной давным-давно концепцией DATAKNOB_USER. Тогда всю "чёрную работу" выполнял бы как раз Cdr.

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

            Очевидно, что при повторении подобных потребностей нужно будет двигаться именно в сторону "USER"-узлов. Просто не очень понятно, как же это бы реализовать поудобнее, чтоб knobplugin'ам сподручнее получать доступ к этим данным -- вопрос биндинга:

            • Толпы параметров по номерам?
            • Или по именам?
            • А имена откуда брать?
            • Видимо, нужна архитектура, похожая на "параметры".

              У них ведь тоже парсится имя-идентификатор (и метка!) плюс всякие дополнительности.

    • A big, FAT note: не очень красиво сделана {R,D}-конверсия.
      • Данные-то хранятся сырые, а для показа количества частиц нужны пересчитанные по текущей калибровке.
      • Пересчёт делает cda_rd_convert(), но делается пересчёт в момент отображения и по ТЕКУЩЕМУ коэффициенту, а в момент получения данных он мог быть иным.

        А возможен race condition, когда в момент отрисовки от сервера будет получена уже новая калибровка, а сами данные еще не дойдут.

      • Следовательно, по-хорошему -- надо бы сохранять пересчитанные значения (в mes?) в момент получения данных.
      • ...или, например, иметь ДВА экземпляра векторных каналов: второй -- без пометки "IS_FRAME", чтоб пересчитывался в частицы сразу.
      • Но для простоты решено забить.
      • Кстати, с fastadc ровно та же картина: при отрисовке коэффициенты берутся непосредственно от cda, а это может оказаться момент "между" -- после прихода коэффициента, но до прихода данных.

        Снижает (нулит) остроту проблемы то, что у нынешних fastadc-драйверов у ВСЕХ коэффициенты фиксированные и отдаются один раз при старте.

    Засим раздел по wirebpm можно (на пока?) считать за "done".

  • 20.07.2016@утро-после-душа: к вопросу о типе узлов _PZFR: некрасиво, что приходится в cx-starter*.conf'ах для pzframeclient-программ указывать chaninfo=... (а как следствие -- и app_name=...). В противном случае никаких server-led'ов нет.

    И это притом, что вся нужная исходная информация у Cdr присутствует.

    Идея: а что, если прямо в Cdr регистрировать каналы "_devstate" с указанным в узле src и текущим baseref?

    20.07.2016: некоторые соображения:

    • Это проканает даже для "синтетических" устройств, скомпонованных cpoint'ами -- благодаря тому, что суффикс "._devstate" можно прилепить к любому валидному имени канала и он разрезолвится.
    • Проблема может быть при иных протоколах, вроде vcas:: и epics:: -- там-то подобных имён нету.
      • В будущем проблему можно решить, введя behaviour-флаг "не пытаться коннектиться к каналу _devstate".
      • В любом случае, максимум проблем -- чёрный квадратик сигнализации об отсутствующем канале.

      Да и вероятность 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 оного нет вовсе.

    Работает, ура!!!

  • 23.07.2016: желательно бы в fastadc_data добавить правило "если rangemin==rangemax, то найти min/max по данным".

    Актуально, например, прямо сейчас для VSDC2 -- там хрен бы знал какой диапазон float32-чисел от осциллограммы.

    29.07.2016: да, сделано.

    • Собственно функции прохода по векторам для всех dtype'ов генерятся одним #define'ом DEFINE_MINMAX_FINDER() и имеют имена вида FindMinMax_DTYPE().
    • Проверки на min==max делаются перед вызовом FastAdcSymmMinMax*(), и, при необходимости, дёргается нужный вариант поиска в зависимости от 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'ев.
      • Evproc'ам же передаётся значение (под именем info_int), но без какой-либо возможности менять его значение (каковая возможность есть у Xt'шных event-processer'ов в виде continue_to_dispatch).
      • Вовлечённые же в процесс взведения и использования флага функции вызываются в цепочке/дереве обработки события PZFRAME_REASON_DATA:
        • fastadc_data.c::ProcessAdcInfo(), в котором есть потребность дополнительно взвести флаг.

          Она вызывается в самом начале цепочки, из DoEvproc(), являющейся реализацией метода pzframe_data_vmt_t.evproc.

        • fastadc_gui.c::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():

    • Добавлена проверка, что если границы диапазона равны, то они чуток "раздвигаются".
    • Причём "раздвижка" сделана хитро -- так, чтобы гарантированно избегать целочисленного переполнения:
      • если значение границ диапазона меньше 1, то ВЕРХНЯЯ граница ИНкрементируется (и гарантированно влезет в диапазон представления)
      • иначе же НИЖНЯЯ граница ДЕкрементируется (и, соответственно, гарантированно не опустится ниже 0, что безопасно даже с беззнаковыми типами).

    Падать перестало.

    И В 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.

      Поэтому инициализация перенесена НИЖЕ этой проверки, а из объявления она убрана.

    • Теперь с осями -- там есть 2 части:
      1. DrawPlotAxis() -- собственно отрисовка;
      2. XhPlotCalcMargins() -- вычисления размеров полей.

      Ранее в них были просто прямые обращения к one->data->cur_range и one->data->all_range соответственно, теперь же они заменены на обращение к локальной переменной, в которую предварительно копируется и о-CheckDispRange()'ивается.

      ...что странно -- в XhPlotCalcMargins() именно all_range; видимо, чтобы зарезервировать в поля МАКСИМАЛЬНО возможное место?

    • А из ProcessAdcInfo() принудительное расширение убрано.

    Проверено -- вроде пашет.

    Замечание: такой вариант с "расширением" прямо в Xh_plot отличается от первоначального (в fastadc_data) тем, что теперь НЕ делается симметризация (а раньше бы делалась, т.к. расширение производилось до неё). Но это вряд ли важно.

  • 03.09.2016@Снежинск-каземат-11: надо бы сделать прямо в fastadc_gui возможность синхронизовывать реперы с другим экземпляром fastadc -- то, что делается в liuclient'е с парами BPM:adc200_0/adc200_1 и BPM:adc200_2/adc200_3.
  • 13.09.2016: Старостенко-мл (aka Джуниор=Детокос) внёс предложение: чтоб масштаб на графиках менялся колесом мыши.

    13.09.2016: в принципе, можно -- по "увеличению" включать следующий коэффициент, по уменьшению -- предыдущий.

    • Просто колесо (без Shift/Ctrl) на графике -- не очень хорошая идея: вообще-то это должен быть скроллинг.

      XmScrollBar, кстати, полесо понимает. Так что, по-хорошему, надо бы ButtonPress/ButtonRelease скроллинга пересылать ему.

    • Второй вопрос -- КАК делать зум: по горизонтали/вертикали вместе, либо раздельно (например, +Shift -- вертикаль, +Ctrl -- горизонталь)?
    • Ну и не забывать обновлять экранные селекторы масштаба.

    29.09.2016@семинар-NI-в-технопарке: насчёт прошенного вчера Самойловым более удобного масштабирования графиков: а может, сделать на графике кнопочку "разрешить автомасштабирование"? При включенности ВСЕГДА использовать самопоиск min/max по данным (из пред-предыдущего пункта).

  • 29.09.2016: всё-таки неудобно, что у осциллографоподобностей "обвязка" создаётся вручную. В результате там вроде бы как на вид обычные ручки, но только, например, окно "Knob properties" для них не вызвать, никакие свойства не посмотреть.

    Надо, НАДО думать о том, как бы это -- т.е., всехние _gui.c -- перевести с ручного создания "обвязки" на указывание строкового описателя (в subsys-виде), чтоб обвязка генерилась обычным образом и "приклеивалась" бы в качестве обычной DATATREE_CONT-подветки.

    29.09.2016: очевидные сложности:

    1. Поскольку препроцессор (M4) не встроен, а внешняя программа, то либо будет делаться лишний "popen()" при создании каждого pzframe_knobplugin'а, либо надо включать в _gui.c уже отпрепроцессированный (на этапе компиляции) текст.
    2. Это создание как-то должно будет учитывать содержимое _chan_dscrs[]. Вот конкретно сейчас 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).

  • 30.09.2016@утро-мытьё-посуды: насчёт хранения предыдущих измерений (речь о векторных каналах, не помещающихся в valbuf): если (когда!) это будет делаться, то надо не буферы копировать, а перебрасывать указатели current_val: менять их местами (так что prv_data[cn] будет указывать на предыдущие данные).
  • 30.09.2016: делаем сохранение и чтение данных (пока в основном только в fastadc).

    30.09.2016: процесс:

    • В pzframe_gui.c использование (реакция на кнопку [Save]) уже было.
    • В pzframe_main.c обработка команд скопирована Chl_app.c'овская, а PzframeMainLoadData() и PzframeMainSaveData() уже были.
    • ...необходимый pzframe_data.c::>PzframeDataFdlgFilter() скопирован из v2.
    • Собственно работа, в fastadc_data.c:
      • Общие определения взяты старые, но param_lp_s теперь не "#.PARAM", а "#=PARAM" -- чтоб, не дай бог, старые файлы не подошли.
      • FastadcDataFilter() идентична старой.
      • FastadcDataSave() отличается в первую очередь из-за разнообразных dtype у параметров, ну и ещё чуток по мелочи.
      • FastadcDataLoad() уже посложнее:
        1. Параметры-то по именам, а не по номерам, поэтому хитрее и парсинг, и поиск (которого раньше не было вовсе).
        2. И они бывают разного типа -- посему там не только более ветвистый код, но и пришлось ввести -- пока локальные -- PzframeDataPutChanInt() и PzframeDataPutChanDbl(), скопированные со своих Get-дуалов.

          Тут надо корректно понимать смысл слова "Put": значение кладётся в текущее, но НИКУДА НЕ ОТСЫЛАЕТСЯ.

        3. "Нуление" всего по ошибке теперь не так тривиально, поэтому оно было вытащено в отдельную ZeroAllData(), идущую по всем каналам и параметрам делающая bzero() содержимого. Логика по определению указателя на "содержимое" и объёма позаимствована из pzframe_data.c::ProcessOneChanUpdate().

    03.10.2016: проверяем сделанное:

    • За компанию с param_lp_s поменяны также прочие _lp_s (т.к. иначе сигнатуры от v2 и v4 "подхватывались" друг-друговым кодом -- проверено).

      Теперь они ВСЕ (включая сигнатуру "Data:") начинаются с "#=".

    • Собственно -- загрузка работает!!! И по кнопке [Open] из тулбара, и при указании файла из командной строки (только в текущей директории обязателен префикс "./").
    • Но поле графика остаётся цвета "JUSTCREATED".

      Анализ:

      • Причина -- при загрузке никто и никогда состояние (gui->curstate) не трогает, и vmt.newstate() не вызывает.
      • При обычном же обновлении эту работу выполняет PzframeGuiEventProc() перед вызовом PzframeGuiUpdate().

      Решение: дык пусть это делает сама PzframeGuiUpdate()!

      • В ZeroAllData() добавлено pfr->rflags=0.
      • Обновление curstate перенесено в PzframeGuiUpdate().

      Ура -- всё пашет!!!

    Раз на вид всё окей, считаем за "done", а дальше будет пилить по необходимости.

    P.S. Для liu и подобных понадобится сделать FastadcDataSave() публичной.

  • 31.05.2017: возникли некоторые мысли насчёт реализации фич "страничный режим" и "самописец".

    Причина возникновения -- позавчера-вчера делал для Роговского модификацию v2'шного драйвера nadc502 (она названа wadc502), в которой:

    1. Данные передаются НЕмасштабированными на квант 16-битовыми кодами (но С учётом сдвига нуля).
    2. Сделаны ручки индивидуального отключения линий -- так что ненужная линия не будет ни пересылаться драйвером серверу, ни даже вычитываться NAF'ами.

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

    Но:

    • Опрос о целевом измеряемом сигнале показал, что, оказывается, сам сигнал является периодическим, с небольшой длиной импульса (около полутора тысяч точек) на фоне длинной "паузы", и в идеале для него подошел бы страничный режим -- чтобы на каждый паттерн сигнала АЦП бы запускался, измерял и засыпал бы до следующего.

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

      1. Организация сигналов такова, что никакого стартового импульса перед интересующим сигналом нет -- просто этот сигнал появляется в примерно предсказуемые моменты.
      2. Конкретно ADC502 страничного режима не имеет...
    • Однако мысли о том, как можно б было организовать страничного режима в голове уже зароились (именно зароились, а не зародились :)).
    • И тогда за компанию уж и о "время назад".

    31.05.2017: собственно:

    • В самом примитивном варианте эта функциональность просто реализуется в драйвере, но никак не видна в клиенте.

      Т.е.:

      • Заводим каналы "размер страницы" (в отсчётах) и "включить страничный режим" (флаг), плюс "режим самописца" (тоже флаг).
      • Наверх по-прежнему отдаются массивы данных, которые клиент отрисовывает, просто подписи к шкале времени будут некорректными.
    • Если же делать clientside-поддержку (точнее, больше GUI-side), то надо отражать в fastadc_mes_t эти 3 параметра (более от уровня fastadc_data вроде ничего не требуется).
    • А уж fastadc_gui может:
      1. В страничном режиме:
        1. В точке старта каждой страницы ставить вертикальный маркер (аналогично реперу), с номером страницы. Напрашивается либо бледно-серый, либо бледно-розовый цвет.
        2. Сами подписи времени пусть выглядят как "#N+XXXxs", где N -- номер страницы.

          (В файле с сохранёнными данными, конечно, не очень красиво (с точки зрения всяких excel/gnuplot), но уж ладно.)

        02.06.2017: пообщался с ЕманоФедей на тему, как оно сделано у "взрослых" осциллографов (вроде Acquiris/Agilent/KeySight). Ответ: все сегменты измерения рисуются поверх друг друга (как в manyadcs на ЛИУ).

      2. В режиме "время назад" делать подписи по числам не увеличивающимся слева направо от 0 (точнее, PTSOFS) вверх, а уменьшающимися слева направо от максимума до 0.

      Некоторый вопрос в том, как реагировать на одновременную включенность этих режимов (а такое вообще технологически возможно?)?

    • Чуть более продвинутый вариант -- пытаться откуда-то добывать период внешних запусков (аналогично "внешнему" каналу ext_xs_per_point) и формировать подписи соответственно (т.е., просто в абсолютном времени).

    На вид ничего сверхсложного. Пока не требуется, но если вдруг -- то есть понимание, что именно делать.

  • 11.07.2017: в граф.клиенте ADC812ME обнаружился косячок: канал FRQDIV в GUI использовался, но в adc812me_chan_dscrs[] он отсутствовал. В результате он просто не работал и всегда отображался с ==0.

    12.07.2017: фиксим: в pzframe_gui.c::PzframeGuiMkparknob() добавлена проверка, что запрашиваемый канал реально регистрируется (его .name!=NULL), и если нет, то на консоль валится WARNING. Для отлова таких ошибок хватит.

    Заодно, кстати, добавлена проверка, что номер запрашиваемого канала не вылазит за размер таблицы используемых.

  • 26.01.2018: насчёт отображения в fastadc_gui:
    • На этой неделе ЕманоФедя возмущался, что у него на некоем графике половина места пропадает зря -- там сигнал всегда положительный, а в минусовую область никогда не заходит. И вот как бы этак сделать, чтобы можно было пустоту убрать.

      Тогда я смог только посокрушаться "ну нет пока поддержки вертикальных scrollbar'ов!...".

    • А сегодня с аналогичной просьбой обратился Роговский: у них на картинке с ADC502 такой "колокольчик", всегда положительный.
    • В идеале, конечно, надо бы делать некое всеобъемлющее решение -- чтоб и сразу могло б в параметрах ручки указываться, и чтоб юзер мог как-нибудь регулировать.
    • Но в качестве паллиативного решения можно ограничиться именно параметрами gui-уровня: чтоб юзер мог указывать "непересекаемые границы" -- минимум и максимум, ниже и выше которых заходить не надо.
      • Т.е., если предоставленная драйвером граница не доходит до "непересекаемой", то использовать её, а иначе -- указанное в конфиге значение.

        Например, если указан диапазон [-1,+4], то при даденном драйвером [-0.5,+0.5] будет как отдано, [-4,+4] превратится в [-1,+4], а [-8,+8] в [-1,+4].

      • Тонкость #1: в качестве значений по умолчанию надо указывать минимальное и максимальное возможные значения для данного типа (да, с вещественными это сложно; да и с целочисленными не легче из-за разных размеров).

        Это избавит от необходимости писать PSP-плагин для парсинга (кроме собственно значения нужен ещё факт, что парсинг был).

        Хотя,

        • Хак #1: для вещественных можно использовать NaN в качестве "значение не было указано".
        • Хак #2: в качестве умолчательных границ можно брать не диапазоны представления чисел данного типа, а указанные в atd значения range.

          ...хотя -- а если оне не указаны?

      • Тонкость #2: при указанности хотя бы одной границе (точнее, при УПИРАНИИ хотя бы в одну границу!) надо отключать симметризирование (SymmMinMax*).
    • Соответственно, для Роговского и Феди можно указывать "min=0".

    28.08.2020: близкая тема -- на днях ЕманоФедя опять возмутился, что при использовании в ADC200 фичи "сдвиг нуля" отображение всё равно симметризует диапазоны (которые тут становятся уже принципиально НЕсимметричными); т.е., Федя пытается использовать площадь осциллограммы по максимуму, убирая ту часть, где сигнала никогда не бывает, а программа мешает ему это сделать.

    Ну что ж -- очевидно, что надо делать это симметризование отключабельным. Процесс размышлений и реализации:

    • Первейший вопрос -- КАК/ГДЕ указывать на необходимость НЕсимметризации?
      • Первая мысль -- в каких-нибудь флагах adc-type-description'а.

        (Именно adc-type, т.е., fastadc_type_dscr_t, а не fastadc_gui_dscr_t -- потому, что симметризацией занимается уровень fastadc_data.)

        Но флагов таких нет.

        Да и неправильно бы так было: в одном устройстве могут, в принципе, быть каналы с разными требованиями к симметризовабельности -- речь в первую очередь об искусственных/композитных, вроде manyadcs.

      • ...вторая мысль -- а если указывать в свойствах канала, битиком конкретно в pzframe_chan_dscr_t.chan_type.

        Но и это тоже неправильно, потому что...

      • Надо указывать в свойствах ЛИНИИ -- в fastadc_line_dscr_t.

      Вот последний вариант и выберем.

    • Но указывать там физически негде -- не было никакого поля "флаги/поведение".
    • Посему -- добавляем!
      • Поле options
      • Плюс про запас ещё троицу rsrvd1,rsrvd2,rsrvd3 (на случай, если knobplugin'ы станут загружаемыми -- чтоб не париться о совместимости ABI).
      • И собственно флажок FASTADC_LINE_OPTION_NO_SYMM.
    • Ну и собственно работа -- в fastadc_data.c теперь все вызовы FastadcSymmMinMax*() окружены проверками, что флаг не взведён.
    • Выставление флажка добавлено в adc200_data.c::LINE_DSCR().
    • Ну -- проверяем! А фиг -- всё равно симметризует.

      Умаялся искать причину -- и отладочных печатей понаделал, которые показали, что всё вроде правильно, и значения каналов диапазонов от драйвера проверил...

      Оказалось, что дело в info2mes: оно всё-таки вызывается в самом конце ProcessAdcInfo(), и конкретно в adc200_info2mes() имелась принудительная симметризация.

      После закомментировывания проблема ушла и теперь вроде всё работает как надо -- НЕ симметризуется.

    • Кстати, возможно, флаги "NO_SYMM" надо выставить также и в adc502_data.c -- поскольку у ADC502 тоже есть по-канальный "сдвиг нуля на +-1/4".
  • 05.12.2018: поступила просьба от Беркаева сделать реперы более видимыми -- например, фиолетовыми.

    Потребность в АЦП клистронов на ВЭПП-5: там сигнал иногда гуляет, и желательно бы легче видеть отклонение "горбов" от реперных точек (устанавливаемых в нужные места).

    09.12.2018: после некоторых размышлений и обмысливания разных вариантов (включая «не сдалать ли ключик "contrast_repers", в дополнение к ключу black») было принято решение максимально простое: в качестве цвета реперов использовать цвет осей.

    Реализация свелась к модификации одной строки в Xh_plot.c::XhCreatePlot().

    17.12.2018: всё-таки переделано на "фиолетовые" -- точнее, magenta (#ff00ff).

    1. Модифицированы 2 строчки в Xh_colors.c.
    2. В pzframe_main.c закомментирована строка, делавшая реперы чисто зелёными --
      XhSetColorBinding("GRAPH_REPERS", "#00FF00");

    Вроде неплохо.

    Кстати, как говорит Беркаев, цвет "magenta" используется в стеклянных кокпитах то ли Boeing, то ли Airbus в похожих целях -- для показа стрелки "куда лететь" (или "куда рулить"?); там он и взял идею о фиолетовом.

  • 05.12.2018: Беркаеву понадобилась возможность "указать осциллограмму как хорошую, чтобы она показывалась фоном к текущей, и чтоб было видно отклонение".

    Я-то сказал, что такая фича есть, надо просто кнопки на морды окон вывести. Но есть ли?

    07.12.2018: посмотрел -- неа, в v4 точно нету. Но, может, можно быстро скопировать из v2?

    10.12.2018: стал смотреть детальнее. Неа, и в v2 нету :)

    Результаты анализа текущей ситуации:

    • ЕСТЬ только пиктограммы, и то только крупные 32x32, btn_setgood.xpm и btn_rstgood.xpm.
    • Сама концепция называется "svd".
    • Где/когда:
      1. В v2 оно было реализовано только в ПЕРВОНАЧАЛЬНОМ варианте который ещё до введения термина pzframe (а возможно, ещё и до fastadc -- там речь про adc200).

        В нём это было реализовано 19-12-2007.

      2. При переходе на слоёный стек {fastadc,pzframe}_{knobplugin,gui,data} в районе 18-12-2013 было решено убрать понятие "svd" с уровня pzframe и отдать на уровень конкретности (fastadc).

        Но в той версии -- она является текущей в v2 -- сама реализация "svd" отсутствует, а есть лишь:

        • некоторая подготовка в виде нескольких полей в fastadc_data.h и vcamimg_data.h (в этой-то зачем?!)
        • плюс поддержка отрисовки "старых" в fastadc_gui.c::gui_draw().
        • Ну и собственно Xh_plot оную поддержку имеет -- егошний DrawPlotView() "знает" о возможности наличия предыдущих в количестве num_saved, а количество оное уставляется через XhPlotSetNumSaved().

        Видимо, так и не потребовалось.

      3. При реализации в v4 в мае-июне-2016 также ничего не было сделано.

        И в gui_draw() код, реагирующий на age, просто закомментирован.

    • Собственно, реализации ничего не противоречит, и:
      • На этапе (2) даже была сделана хорошая архитектурная подготовка в виде концепции "mes", когда вся информация об измерении сконцентрирована в одной структуре.
      • А в v4 -- mes остался только на уровне fastadc/wirebpm (из pzframe исчез) и стал содержать уже "обработанную" информацию вместо просто info[].

    11.12.2018: делаем.

    1. Аллокирование памяти.

      Тут были некоторые сомнения на тему "где это реализовывать -- в fastadc или всё-таки в pzframe?".

      • В пользу pzframe_data -- то, что там всё равно аллокируется общий буфер, в который можно и сохраняемые данные поселить. А также то, что решение на уровне pzframe будет общим и для fastadc, и для wirebpm, и для возможных прочих вариантов.

        Но минусом тут то, КАК PzframeDataRealize() должен узнавать, для каких каналов нужно заготавливать место?

      • В пользу fastadc_data -- то, что оно там уже имеет всю требуемую информацию: она есть в atc, уже на уровне ЛИНИЙ, а не каналов.

      Поэтому решено аллокирование поручить 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 делается).
    2. Сохранение. Названия функций взяты из v2 образца конца 2012 года (т.е. из той самой "первоначальной" реализации).
      1. Уровень _data:
        • FastadcDataCopy2Svd() -- интеллектуальное копирование всех mes.plots[] в svd.plots[]: поля копируются поштучно, а содержимое буферов данных -- отдельно, ровно по нужному объёму; причём только при взведённом on, а в противном случае ставится x_buf=NULL, x_buf_dtype=INT32, numpts=0 -- как в ProcessAdcInfo().
        • FastadcDataResetSvd() -- просто сброс use_svd=0.
      2. Уровень _gui: FastadcGuiCopy2Svd() и FastadcGuiResetSvd() скопированы с версии 2012 года без изменений -- да-да, API XhPlot остался тем же.
    3. Отображение: это самое простое -- в gui_draw() раскомментирован выбор plots из mes либо svd в зависимости от age.
    4. Пользовательский интерфейс... А вот тут проблема: тулбары создаёт и реагирует на их кнопки уровень pzframe, а понятие "svd" существует только на уровне fastadc.

      В реализации 2012 года проблема отсутствовала потому, что там не было разделения на уровни.

    12.12.2018: продолжаем:

    1. Насчёт реализации в пользовательском интерфейсе весь вчерашний вечер и сегодняшнее утро усиленно размышлял, и вот что получается.

      Если не устраивать радикального повышения модульности в виде указания содержимого тулбаров из уровней-юзеров pzframe (т.е., fastadc/wirebpm/vcamimg), то создаваться тулбары будут всё равно в pzframe.

      И тогда просматриваются 2 варианта того, как устроить реакцию на нажатия кнопок (т.е., передачу информации о нажатии от pzframe своим юзерам):

      1. "A-la наследование": ввести в pzframe_gui_vmt_t метод "управление сохранённостью" (которому передаётся 1 для сохранения и 0 для сброса). А pzframe_main.c::CommandProc() и pzframe_gui.c::*BtnCB() станут вызывать его.
      2. Передавать просто СОБЫТИЯ (точнее, Xh-КОМАНДЫ), обычным образом -- с тулбара они передадутся сами, а мини-тулбар может их сгенерить.

        Тогда придётся добавить ещё инфраструктуру обработки команд в {fastadc,wirebpm,vcamimg}_gui.

      Первый вариант (a) выглядит проще и беспроблемнее -- всё делается в рамках инфраструктуры pzframe, и не требуется никак завязываться на специфику работы Xh/Motif/...

      Делаем.

      • Добавлен метод pzframe_gui_vmt_t.svd_ctl.
      • В fastadc_gui_std_pzframe_vmt добавлен указатель на SvdCtl() (чей функционал тривиален).

        (А в wirebpm'ную и vcamimg'ную добавлены NULL'ы.)

      • Нарисованы пиктограммы btn_mini_setgood.xpm и btn_mini_rstgood.xpm (без них совсем никак). Сделаны по образу обычных, но поглядывая на курсор "рука" (из шрифта cursor).
      • Основной тулбар: в pzframe_main.c кнопки раскомментированы и в CommandProc() добавлена реакция на них -- вызов метода svd_ctl().
      • Мини-тулбар: в pzframe_gui.c после кнопок [Start] и [Once] прилепдены ещё пара, чьи *BtnCB() так же вызывают метод svd_ctl().

        В отличие от прочих 3, ссылки на виджеты НЕ сохраняются ни в каком *_gui_t -- просто надобности к ним потом обращаться нету (а к loop и once -- есть).

    Собственно, проверено -- РАБОТАЕТ!!! Прямо с первого раза, что удивительно и подозрительно.

    Дальнейшие напрашивающиеся усовершенствования:

    1. Надо поддерживать флаг NO_SVD (а в manyadcs_knobplugin.c и two812ch_knobplugin его добавить).

      А NO_ALLOC?

    2. Нужна управляемость наличием/отсутствием этих кнопок. Как и предыдущей пары, кстати.

      Ибо сейчас их получилось многовато и эта четвёрка уже распирает cpanel; при том, что кнопка [Once] пультовым программам уж точно не нужна.

      Тут некоторая двоякость: с одной стороны, надо бы уметь управлять этим на уровне экземпляров осциллографов, а с другой, у некоторых типов кнопки не нужны никогда (wirebpm). В голову приходит мысл сделать ДВА набора флагов: один указывается в atd или вообще рядом с vmt, а второй -- уже в options. И кнопки появляются только при взведённости ОБОИХ флагов.

    3. Почему кнопка [Start] на основном тулбаре НЕ отражает состояние (и по молчанию она ненажата), хотя на мини-тулбаре при устройстве всё OK?

    13.12.2018: продолжаем.

    1. По флагам.
      • Для начала -- о смысле/значении конкретных флагов: результаты изучения кода.
        • ARTIFICIAL:
          1. Он отключает регистрацию контекста и каналов -- PzframeDataRealize() при нём взведённом просто делает return сразу после аллокирования буферов (а при взведённом ещё и NO_ALLOC -- не делает вообще ничего).
          2. Виджету-графику сразу ставится состояние KNOBSTATE_NONE вместо обычного изначального KNOBSTATE_JUSTCREATED.
        • NO_SVD: отключение фичи "умение сохранять и отображать старое <хорошее> измерение".

          В v4 сводится к не-аллокированию svd_buf'а.

        • NO_ALLOC: отключает аллокирование буферов.
      • Никакого "manyadcs_knobplugin.c" для v4 пока нету.
      • В имеющийся (но пока реально НЕ используемый) two812ch_data.c добавлено (скопировано из v2'шного в liu/xmclients/)
        PZFRAME_B_ARTIFICIAL | PZFRAME_B_NO_SVD | PZFRAME_B_NO_ALLOC
        -- оно так должно будет быть во всех искусственных.
      • PZFRAME_B_NO_ALLOC поддерживается ещё с 2016-го.
      • Поддержка PZFRAME_B_NO_SVD добавлена сейчас:
        1. FastadcDataRealize() выполняет аллокирование лишь при НЕвзведённости, причём невзведён должен быть и NO_ALLOC.
        2. Кнопки на тулбарах:
          • PzframeGuiMkCommons() теперь НЕ делает ту пару кнопок при взведённом NO_SVD.
          • Неделанье кнопок на оконном тулбаре обсуждается ниже, в разделе о тулбаре.
    2. О наличии/отсутствии кнопок на оконном тулбаре.
      1. Оконный тулбар: какая-нибудь управляемость наличием/отсутствием на нём кнопок в pzframe_main.c отсутствовала, в отличие от Chl_app.c. Вот с последнего и надо брать пример.
        • Вот пример и взят -- добавлен цикл, проходящийся по всему тулбару и об-NOP'ивающий все лишние кнопки.
        • Правда, сейчас по факту под условие "выкинуть!" не подходит никто.

          Но инфраструктура-то сделана.

          15.12.2018: впрочем, удаление кнопок [SetGood][RstGood] при наличии NO_SVD всё же сделано.

      2. Мини-тулбар...

        Сделал по-простому: в мини-тулбаре кнопка [Once] НЕ делается при том же условии, когда делаются SetGood+RstGood.

        Таким образом, он продолжает оставаться компактным -- текущая проблема решена.

    3. На остальное пока решено забить.

    15.12.2018: на пульт новый pzframeclient скопирован, он там будет потихоньку вводиться в строй по мере перезапуска программ.

    Итак, титульная задача решена -- возможность указывать "хорошую" осциллограмму сделана, так что "done".

    А для аспектов функционирования тулбаров создан свой "тикет" за сегодня, см. ниже.

    15.12.2018: чуток допилено:

    • В VcamimgDataFillStdDscr() и WirebpmDataFillStdDscr() вставлено форсение PZFRAME_B_NO_SVD.
    • И в цикл "убирания лишних кнопок из оконного тулбара" добавлено понимание взведённого NO_SVD.

    А то по-дурацки смотрелись утилиты ottcam и u0632, никакого "сохранения хорошего" не поддерживающие.

  • 05.12.2018: а не изготовить ли клиента (_data плюс _gui-"морду") "неспецифичного клиента" -- для отображения просто 1 линии графика, БЕЗ cpanel (точнее, с пустой панелью, содержащей только fps)?

    Смысл -- чтоб в v5cnv не запихивать полноценный adc333, от которого показывается только 4-я линия (канал3), а составить cpoint'ами "виртуальный АЦП", состоящий из line3,numpts,ptsofs и прочих "реально необходимых" каналов, и уже на него натравливать тот компонент в v5cnv.

    10.12.2018: надо только будет название выбрать.

    Например, "adc1"? Не, пхохо -- начало пересекается с adc1000.

  • 06.12.2018@вечер-дорога-домой-вдоль-Лаврентьева: а можно и положение реперов маппировать на каналы -- чтоб они могли б сохраняться, да и менялись бы все скопом.

    Как конкретно это делать -- не вполне ясно, но:

    1. API FastadcGuiSetReper() уже есть, так что программно реперы управлябельны.
    2. Можно, наверное, ввести options-параметры вроде "reper1" и "reper2", указываемые аналогично ext_xs_per_point, только на уровне fastadc_gui -- чтоб оно a) за этими каналами следило и ставило б реперы в эти позиции; b) при изменении своих реперов отправляло бы их позиции в каналы.

    Некоторая неприятность лишь в том, что у реперов есть не только позиция, но и состояние вкл/выкл -- которое, впрочем, управляется указанием позиции <0.

    Ну и понятны и ограничения этой фичи:

    1. Синхронизировать имеет смысл реперы лишь идентичных АЦП.
    2. Поскольку позиции реперов -- это просто номера отсчётов (а НЕ времена!), то и настройки синхронизируемых АЦП должны быть идентичны -- таймирование, frqdiv, PTSOFS.

    16.12.2018: а есть идея и пострашнее: "как бы это сделать так, чтобы «сохранённые хорошие» графики тоже маппировались бы на каналы и были б общими для всех экземпляров осциллографов (и переживали бы рестарт программы, и тоже могли бы сохраняться в режим)".

    Сия жуткая мысль пришла в голову пока что только мне. Вопрос, когда она придёт в головы юзеров.

    Как такое делать -- и думать не хочется (понятно ж, что можно -- просто возни много): на стороне сервера надо будет заводить для каждого осциллографа набор каналов, миррорящий его набор данных; каждый экземпляр fastadc-морды будет пытаться приконнектиться также к этим дополнительным каналам, ...

    Пока моя позиция проста: нынешнее "сохранённое хорошее" -- это вопрос локального удобства; морды осциллографов -- это НЕ программы для анализа данных, если нужен такой функционал, то его следует реализовывать в специализированных программах.

  • 15.12.2018: требуются некоторые усовершенствования в интерфейсе, в плане функционирования кнопок тулбаров (как на оконном тулбаре, так и на мини-тулбаре (и вообще стоит внятно определиться, ЧТО же это такое)).

    15.12.2018: итак, список хотелок:

    1. Управляемость наличием/отсутствием кнопок.
      1. На основном тулбаре: какие когда должны быть, а когда отсутствовать.
      2. На мини-тулбаре: сейчас отдельно управляется только [Save].

        А в остальном его появление управляется флагом "noleds", передаваемым от хозяина (main'а или knobplugin'а). И это как-то совсем неправильно.

      3. В частности, если fastadc-knobplugin принадлежит окну, ИМЕЮЩЕМУ тулбар с leds'ами, то он автоматом лишается мини-тулбара; в т.ч. кнопок [SetGood][RstGood].
    2. Декоративности -- нажатость и дисэйбленность:
      1. Основной тулбар:
        • Почему кнопка [Loop] на основном тулбаре НЕ отражает состояние (и по молчанию она ненажата), хотя на мини-тулбаре всё OK?

          ...вроде были какие-то проблемы с этим, судя по закомментированности средней строки в pzframe_main.c::ShowRunMode(). Надо порыться -- где-то это должно быть записано.

        • И, аналогично, почему кнопка [Once] НЕ дисэйблится при нажатой [Loop]?

          Хотя XhSetCommandEnabled() делается.

          А, нашёл: ВСЁ содержимое XhSetCommandEnabled() вообще за-#if0'ено.

          Что ж там было-то такое...

          22.02.2020: да, функционал нажатия/отжатия и дисэйбленья на основном тулбаре в v4 с самого начала просто не работал (точнее, не был сделан). Сегодня реализован и работает.

      2. Мини-тулбар:
        • Можно бы сделать, чтобы при наличии "хорошего" кнопка [SetGood] становилась бы нажатой, а при сбросе -- отжималась бы.
        • И, аналогично, чтоб при отсутствующей сохранённости кнопка [RstGood]

        ...хотя вот это уже больше похоже на блажь

    Это всё задачи не особо срочные (кроме разве что пункта 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] становилась бы нажатой, а при сбросе -- отжималась бы»:

    • переход из пристройки в 13-е, ~16:00: поступить по аналогии с кнопками Loop и Once: ввести функцию ShowSvdMode(), которая бы ставила кнопку SetGood в состояние pressed=svd_present, а RstGood в состояние enabled=!svd_present.

      И да, оная функциональность должна быть на обоих уровнях -- и pzframe_gui, и pzframe_main; совершенно аналогично ShowRunMode().

      Кстати, надо бы под-изучить, как работает последняя.

    • пультовая, ~16:20: посмотрел на код -- да, модель работоспособная, но есть заковыка: ведь это всё делается на уровне pzframe, а понятие "svd" (вместе с флагом use_svd) живёт уже на уровне fastadc.
    • Похоже, придётся добавить ещё один метод в pzframe_gui_svd_ctl_t -- svd_state()?

    26.02.2020@пультовая: делаем.

    • Добавлено поле pzframe_gui_vmt_t.svd_state (добавленого типа pzframe_gui_svd_state_t).
    • В fastadc_gui.c добавлен "метод" SvdState() (в остальных двух -- vcamimg_gui.c и wirebpm_gui.c -- прописаны NULL'ы).
    • В pzframe_main.c сделана ShorSvdMode(), ...
    • ...и её вызовы вставлены в обработку команд SET_GOOD и RST_GOOD, а также в PzframeMain() сразу после SetRunMode() -- это начальная инициализация.
    • Также кнопка PZFRAME_MAIN_CMD_SET_GOOD переделана с XhXXX_TOOLCMD() на XhXXX_TOOLCHK() -- в противном случае OnOff не работает.

    Проверяем -- нифига не работает: постоянно считается svd_state=0.

    • Оказалось, что поле use_svd только СБРАСЫВАЕТСЯ в =0, а =1 нигде не было.
    • Поскольку для XhPlot'а оно было нафиг не нужно -- для него нужно 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() -- но "инициализации" не происходило.
      • Причина нашлась быстро: оная "Realize()" вызывается из FastadcGuiRealize(), и ещё ДО PopulateCpanel() и, соответственно, mkctls(), которая и вызывает, по коду FASTADC_GUI_CTL_COMMONS, создание мини-туобара.
      • Поэтому вызов был перетащен в место после создания кнопок.
      • А заодно, кстати, туда же (в место после создания loop_button и once_button) переехал и "начальный" вызов ShowRunMode() -- он раньше просто не работал, а спасал ситуацию вызов в PzframeGuiEventProc()'е.

    Да, теперь работает.

    Но -- НЕ работает "параллельность": чтобы нажимаешь кнопку на основном тулбаре, и это же состояние отражается на мини-тулбаре.

    Сначала была идея "так можно же сделать соответствующий вызов в pzframe_gui, чтобы его дёргал pzframe_main для синхронизации!" (а при желании -- как-нибудь организовать и обратное сообщение).

    Но потом пришло осознание, что это нафиг не нужно, а надо поработать над НЕсозданием мини-тулбара при наличии основного (и, наоборот, чтобы он принудительно создавался при отсутствии основного). Т.е., нужен какой-то ещё параметр, помимо noleds.

    29.02.2020@дома-суббота: делаем "исключительность" -- чтобы при наличии основного тулбара с кнопками для осциллографа мини-тулбар бы НЕ создавался.

    1. @утро: собственно "не создавание при ненадобности":
      • Добавлено поле pzframe_gui_look_t.embed_tbar.
      • В PzframeGuiMkCommons() теперь РАЗНЫЕ условия: панель leds упарвляется флагом noleds==0, а "куски мини-тулбара" (он фактически состоит из 4 кнопок) управляются флагом embed_tbar.
      • В ТОМ ЧИСЛЕ, этот флаг добавлен как дополнительное условие при создании кнопок setg/rstg.

        Именно это и определяет их НЕсоздание при наличии основного тулбара.

      • В PzframeKnobpluginDoCreate() делается принудительное embed_tbar = 1.
      • А в PzframeMain() -- embed_tbar = opts.notoolbar.

      Ура!

    2. @около полуночи, приехавши с вечеринки от Шваки, заканчивал в полпервого: усимметричиваем/украсивливаем -- избавляемся от мутного термина "noleds":
      • Поле pzframe_gui_look_t.noleds переименовано в embed_leds.
      • Соответственно, его "смысл" инвертировался -- так что
      • В PzframeGuiMkCommons() условие инвертировано (точнее, убрано "== 0").
      • В PzframeKnobpluginDoCreate() и 0PzframeMain() -- присвоения также инвертированы; стало даже красивше.

    Ну что, на этом (наконец-то!) можно считать "апгрейд красивости и корректности" клиентской инфраструктуры pzframe завершённым?

    03.03.2020: а, да -- "done".

  • 09.10.2019: добавляем в fastadc возможность рисовать "толстыми" линиями -- как это умеет histplot.

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

    09.10.2019: действия:

    1. Xh_plot (для него в bigfile-0002.html даже раздела нет -- всё в bigfile-0001.html, но раз делаем в интересах тутошних использований, то здесь и описываем):
      • Введён новый 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 в них совпадает!).

    2. Собственно fastadc
      • Добавлено поле fastadc_gui_look_t.wide.
      • В fastadc_gui_text2look[] для него аж 4 строчки: "thin" -- для обычного варианта, а "thick", "wide" и "bold" -- для широкого.
      • И при взведённости флага делается options |= XH_PLOT_NO_SCROLLBAR_MASK.

    Тут надо отметить, что этот режим переключается только изначально, в отличие от histplot'а, где оно работает динамически, по селектору "Mode".

  • 28.01.2020: возникло желание, чтобы "остановившиеся" АЦП были видны издалека -- например, чтобы "крутилка" (roller) становилась бы цвета DEFUNCT через, например, 5 секунд (точнее -- через fresh_age) после последнего обновления.

    (Это по результатам двухчасового "гипнотизирования" 8 штук ADC250, которые то работали, то нет, и приходилось вглядываться, крутится ли оно там и/или разглядывать время с момента последнего запуска.)

    Обоснование, кстати, простое: когда оно стоит, то разглядывать конкретное положение крутилки незачем -- оно смысла не имеет, факт "замёрзшести" важнее. Число же FPS и секунд с последнего запуска посиневать нельзя -- последнее имеет смысл и спустя час после замерзания.

    Есть, правда, сомнения -- а можно ли крутилку расцвечивать отдельно? Она для этого должна быть отдельным виджетом.

    29.01.2020: посмотрел -- да, крутилка сделана отдельным виджетом, pzframe_gui_t.roller. ...как, кстати, и отображалка FPS -- fps_show! Можно и её посинять.

    Очевидно, нужно будет добавить туда же:

    1. Поле для сохранения текущего состояния (посинено/нет), чтобы не дёргать смену цветов почём зря.
    2. Поля вроде deffg/defbg, для хранения нормального цвета.

    А вот время последнего обновления УЖЕ есть в pzframe_data_t.timeupd.

    Расцветка:

    • Онормалевание (РАЗсиневание) -- PzframeGuiEventProc(), событие PZFRAME_REASON_DATA.
    • Посиневание -- ShowStats().

      Тем более, что там УЖЕ вычисляется время с последнего обновления (для отображения в time_show) -- вот его и использовать лоя сверки с fresh_age.

    • Единственная пока неясность: а сам 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: делаем.

    Сначала -- отображение посинения:

    • К pzframe_gui_t добавлены:
      1. roller_curstate, она инициализируется в KNOBSTATE_NONE.
      2. roller_deffg и roller_deffg, они заполняются при создании виджета.
    • Собственно расцвечивание выполняется в нововведённой SetRollerState().
    • Решения о смене цвета принимаются в соответствии с проектом от 29-01-2020 -- в PzframeGuiEventProc() и ShowStats().
    • Кстати, воизбежание путаницы roller_states[] переименовано в roller_chars[].

    Затем дОбыча fresh_age'я:

    • К pzframe_data_t добавлено поле fresh_age_timeval.

      Оно struct timeval -- т.к. в ShowStats() вычисления ведутся в этих единицах -- и оттого имеет такой суффикс.

    • Возраст считается указанным, если он >0.
    • Добыча -- с переводом из cx_time -- делается в PzframeDataEvproc(), который теперь и FRESHCHG ловит.
    • ...а ловит он его от канала(ов) MARKER.

    12.02.2020: далее переходим к колоризации основной панели. Некоторые мысли по теме (часть вечером ~19:00 опять @8-ка, поворот со Строителей на Бердское):

    • Цветомодификацию надо выполнять НЕ всегда. А, например, если предыдущее состояние -- JUSTCREATED.
    • И вообще только между некоторыми состояниями.
    • Важный аспект: нужно как можно меньше портить картинку.

    13.02.2020@дома: приступаем. Вся работа пока идёт в pzframe_gui.c.

    • Само действие "перерасцветить панель, со сравнением curstate==newstate" вынесено в отдельную SetMainState().

      Были некие смонения, как назвать -- SetЧЕГОИМЕННОState(), поскольку эта сущность "основная отображалка" никакого штатного названия в рамках pzframe_gui не имеет. Вот и выбрано "Main".

    • Далее сделано об-NOTFOUND'ливание: в PzframeGuiEventProc() в дополнение к окучиванию всех индивидуальных каналов добавлен также вызов SetMainState(gui, KNOBSTATE_NOTFOUND) -- тут всё максимально просто.

      Здесь проигнорировано вчерашнее соображение "...выполнять НЕ всегда", поскольку ситуация с почерневанием -- довольно неординарная и тут будет к место сигнализировать о ней сразу.

      Хотя при надобности добавить защитное условие "только если curstate==KNOBSTATE_JUSTCREATED" проблемой не будет.

    • Также сделано и РАЗ-NOTFOUND'ливание при получении положительного результата резолвинга. Оно выполняется только в случае, если предыдущим состоянием было NOTFOUND, и заключается в SetMainState(gui, KNOBSTATE_JUSTCREATED).

      Это некоторый компромисс -- оно не факт, что ВСЕГДА будет именно свежесозданным. Но тут важно хоть как-то отобразить тот факт, что устройство "ожило".

    Начальная проверка показывает, что оно чернеет.

    А вот первоначальная задача -- показывать нерабочесть устройств -- так и не решена.

    Чуть позже @вечер, ванна: размышления по теме:

    • Мысль: да, события NEWVAL/UPDATE на каналы отпавших устройств не доходят.

      Но ведь скрины-то в аналогичной ситуации показывают каналы в нужной расцветке! Что б так же и не сделать?

    • Скрины работают благодаря Cdr, где по событию CDA_CTX_R_CYCLE вызывается процессинг всей группировки и вычитываются ТЕКУЩИЕ rflags, а уж они-то у cda всегда актуальные (при коннекте считываются, а при помирании устройства доставляются от сервера по событию STATCHG (через cxsd_fe_cx до клиента доходящему как CURVAL).
    • НО: если делать так же -- подсматривать за флагами канала MARKER -- то будет неприятно: график будет портиться при любой проблеме с устройством, а для осциллографов хотелось бы, чтоб последний график оставался "читаем" максимально долго -- пока не обновится.
    • Тут надо битым текстом сказать: в отличие от обычных каналов, pzframe-каналы остаются осмысленными долго после обновления, так что надо стараться максимально долго их сохранять, а не портить.

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

      14.02.2020: а вот и фиг -- НЕ видна. Даже при явном делании устройству _devstate=-1 -- скрин просто замирает, но обвязка-то тоже не обновляется. ...и при запуске скрина на УЖЕ нерабочее устройство -- аналогично, всё светло-бирюзовенькое.

    • Похоже, все эти махинации "подсматривать чьи-то флаги" -- от лукавого. Надо следить именно за _devstate.
    • А его не передавать отдельным параметром, а маркировать флажком -- PZFRAME_CHAN__DEVSTATE_MASK.

    14.02.2020: проверил вчерашние изменения в полноте (включая отбирание существовавшего устройства "на лету" при рестарте сервера и затем такое же возвращение) -- работает.

    15.02.2020@дома-суббота: делаем слежение за каналом _devstate, чтобы отлавливать нерабочие устройства.

    • В pzframe_data.h добавлены:
      • PZFRAME_CHAN__DEVSTATE_MASK -- этот флажок надлежит указывать в поле chan_type для таковых каналов.
      • PZFRAME_REASON_DEVSTATE -- код события "получено новое значение канала _devstate".
    • Далее в pzframe_data.c сделано:
      • Каналы с флажком PZFRAME_CHAN__DEVSTATE_MASK регистрируются (с единственным битом UPDATE в evmask'е) на другой обработчик событий -- ...
      • ...PzframeDevSEvproc(): получает текущее значение канала и отдаёт его выше в качестве info_int к событию DEVSTATE.
    • А уж pzframe_gui.c ловит это событие и действует в зависимости от полученного состояния:
      1. <=0 -- если текущим ("предшествующим") состоянием является JUSTCREATED, то переводим основную панель в SFERR.
      2. >0 -- если текущим является SFERR, то переводим в JUSTCREATED.

    Некоторые комментарии:

    1. Состояние SFERR ("болотное") выбрано специально, чтобы издалека хорошо отличалось от HWERR ("бордового"), показывающего аппаратные ошибки вроде перешкала, таймаута и т.д.
    2. Поспольку переход делается ТОЛЬКО из JUSTCREATED, то работает он лишь со свежезапущенными скринами.

      Те же, что ранее уже получали данные -- так и продолжат эти данные показывать. А "поплохелость", по идее, должна бы как-нибудь стать видна через прочие, "обвязочные" ручки каналов. Хотя покамест -- нет, оно не так, поскольку...

    3. ...пока расцвечивание выполняется ТОЛЬКО с основной панелью, а ручки-параметры не трогаются (в отличие от расцветки по RSLVSTAT).

    Проверено -- работает как задумано.

    03.03.2020: ставим "done".

  • 10.03.2021: наблюдена странность: когда при добавлении к драйверу ADC250 канала PLL_LOCKED уже обновлённый клиент был натравлен на ещё необновлённый сервер -- в котором свежедобавленного канала не было -- то его ручка НЕ почернела, а просто отображала нулевое значение (несмотря на наличие флагов IMMEDIATE|ON_CYCLE).

    12.03.2021: туда же были добавлены ещё 3 канала касательно PLL, и тоже IMMEDIATE|ON_CYCLE.

    Пытался понять логику, почему/как происходит то, что происходит -- не особо успешно. Странно оно как-то...

    13.03.2021: но напрашивается мысль, что причина -- в не-ловле RSLVSTAT каналами, как параметрами (у тех-то ладно), так и IMMEDIATE-ручками.

    Я экспериментировал с наличием/отсутствием IMMEDIATE|ON_CYCLE и даже добавлял IS_PARAM (он, как видно по коду, вообще о другом) -- один чёрт, поведение одинаковое.

    ...к сожалению, всё это на симуляторе -- "cxsd -S" (и "-s" тоже), т.к. реальное железо в клистронке стоит без запусков.

    14.03.2021: ещё результаты:

    • А вот после рестарта сервера каналы почернели!

      Надо бы поиграться ещё -- ведь режим суперсимуляции позволяет имитировать приход "маркера".

    • А при "отсутствии" данных -- как при отсутствии соединения, так и при непришедшем ещё "маркере" -- они светлоголубые JUSTCREATED.

      Причём таковы как не-IMMEDIATE, так и отсутствующие каналы.

    • А при наличии соединения, но отсутствии маркера присутствующие IMMEDIATE-каналы (блок HWINFO) обновляются, в отличие от отсутствующих.
    • Отсутствующие же IMMEDIATE -- ВИДИМО -- как-то умудряются обновляться по маркеру. Чего быть не должно.
    • 15.03.2021: ещё прикол: RW-ручка отсутствующего RW-канала -- конкретно PLL_PRESET -- НЕ почернела, хотя соседние RO-disp'ы readonly-каналов (причём AUTOUPDATED) CUR_PLL1_CTRL/CUR_PLL2_CTRL почернели.

    15.03.2021@утро: т.е., суть проблемы битым текстом: в момент обновления ручек -- как общего по "маркеру", так и группового (а вот с чего оно, для IMMEDIATE|ON_CYCLE?) -- уже должен иметься CXCF_FLAG_NOTFOUND (и, видимо, статус NOTFOUND), но почернения не происходит.

    15.03.2021: пытаемся разбираться дальше: вылезло ещё какое-то посинение...

    17.03.2021: с посинением разобрался: оно было из-за тормозов отображения X-окна по сети с v5p3 (к тому же изрядно нагруженного!) на домашний комп через duct, x10sae, star.

    • При малых FPS -- в районе 1 -- всё работало нормально и не синело.
    • А вот при бОльших, вроде хотя бы 2, уже потихоньку тормозило и через некоторое время работало уже с данными, полученными давнее, чем 5с назад -- вот и синело.

      22.03.2021: очевидно, что при таком постепенном отставании (из-за неуспевания отображения) в буфере отправки сервера должны потихоньку накапливаться данные и в конце концов он должен переполняться. Так вот -- да, переполняется: судя по 4access.log'e примерно раз в 2 часа происходит "send-buffer overflow, disconnecting ...", а ещё через примерно 2 минуты клиент реконнектится (видимо, эти 2 минуты уходят на вычерпывание локального буфера).

    Также было извлечено 2 важных урока:

    1. НЕЛЬЗЯ в одном fprintf(stderr, ...) использовать любую stroftime*()/strcurtime*() более 1 раза: оно возвращает указатель на статический buf[], так что последующие вызовы перепрописывают результаты предыдущих -- в итоге везде печатается результат самого "первого", наилевейшего вызова (потому, что при cdecl-вызовах -- а *printf() таковы -- параметры кладутся в стек справа налево).
    2. При печати дуплетов СЕКУНДЫ,ДОЛИ_СЕКУНДЫ надо доли выдавать с ФИКСИРОВАННЫМ числом цифр, с добивкой слева нулями -- %09ld для наносекунд (cx_time_t) и %06ld для микросекунд (struct timeval).

      В противном случае при переводе в "вещественные" для ручных разбирательств -- СЕКУНДЫ.ДОЛИ -- дробная часть получится в десятки/сотни/... раз больше реальной, если в начале были отброшенные нули.

      Более того: и печатать дуплеты лучше не через запятую ',', а сразу через точку ., чтобы иметь готовые как бы вещественные числа.

      В закомментированной отладочной печати в cda_dat_p_update_dataset() теперь сделано и то, и другое.

    Плюс, при отладочной выдаче обнаружилась дурость:

    • IMMEDIATE|ON_CYCLE-ручки обновляются не однократно, а ДВАЖДЫ. Очевидно, потому, что они обновляются и сами по себе -- по PZFRAME_REASON_PARAM, и со всей толпой в цикле в PzframeGuiUpdate().
    • А вот второе-то почему? Ведь оттуда их надо бы исключать, разве нет?
    • Возможно, они оттуда НЕ исключены ради обновления при загрузке файла.

    OK -- добавлено исключение при групповом обновлении, с условием прямо обратным условию для индивидуального обновления.

    Проверил -- вроде всё работает, деградации не видно.

    05.04.2021: возвращаясь к титульной проблеме: проведён ещё один тест, при котором вроде бы нарыт "минимальный test-case", с которым и нужно разбираться. Сценарий его таков:

    1. Сервер должен отсутствовать.
    2. Запускаем клиента, в котором канал одной ручки точно имеет "нерезолвируемое" имя.

      Всё отображается светлоголубым.

    3. Запускаем сервер, так что клиент к нему может приконнектиться.

      И вот тут в клиенте все каналы колоризуются в NORMAL, включая и проблемный.

    4. А по приходу маркера -- проблемный уже чернеет.
    5. ТАКЖЕ чернеет после реконнекта к серверу (после рестарта оного).
    6. ...с другой стороны, если указать несуществующее устройство, то всё почерневает сразу.

      Но эта-то реакция обеспечивается отдельно PzframeDataEvproc()'ем: там по RSLVSTAT_NOTFOUND маркера принудительно проставляется CXCF_FLAG_NOTFOUND всем-всем каналам и генерится событие PZFRAME_REASON_RSLVSTAT, и уже ОНО принудительно всем-всем ручкам ставит KNOBSTATE_NOTFOUND, независимо от rflag'ов.

    Т.е.,

    1. Есть сомнения -- используются ли rflags СРАЗУ. И есть ли они при первоначальной отрисовке вообще.
    2. Получше бы понять, в какой момент вызывается первая отрисовка -- случаем, не ДО реального получения флагов ли?

    @вечер: ответы -- "нет, Не используются" и "да, ДО реального получения флагов".

    05.04.2021@вечер: налаживаем диагностику:

    • В adc250_data.c канал [ADC250_CHAN_CUR_PLL2_CTRL] (его номер 99) специально "испорчен" -- ему сделано заведомо нерезолвящееся имя.
    • А в UpdateParamKnob() вставлена отладочная печать: при cn=99 выдаётся значение rflags и получаемое на их основе knobstate.
    • После удивительного результата -- rflags=0, что ведёт к knobstate=2 (NORMAL) -- к печати также добавлено raise(11), чтобы натравить на это gdb и с помощью bt посмотреть, откуда ж оно такое вызвано.

    Результат оказался немного постыдным:

    • Backtrace показал, что UpdateParamKnob() вызывается из PzframeGuiUpdate(), в общем цикле со всеми остальными ручками -- т.е., по приходу маркера.
    • После чего была добавлена отладочная печать по cn=99 уже в него -- чтобы понять, какого же лешего в общем цикле вызывается обновление IMMEDIATE-ручки.

      Оно показало, что значение .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.
    • И вот теперь, после исправления, ручка уже не обновляется СОВСЕМ -- ни при первом соединении с сервером, ни при дальнейших реконнектах; так и стоит светлоголубая JUSTCREATED.

    Т.е., конкретно ЭТА -- "отлаживаемая" -- проблема вызвана моим же косяком. Но титульная проблема от этого никуда не девалась -- ненайденные ручки НЕ ЧЕРНЕЮТ. Причём ни обычные, ни IMMEDIATE-варианты.

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

    05.04.2021@ложась-баиньки: да, теперь всё выглядит логичным:

    1. Поскольку RSLVSTAT на IMMEDIATE-каналы не заказывается, а собылие UPDATE на NOTFOUND-канал никогда не придёт, то нет вообще никаких источников/причин обновления, вот ручка так и остаётся светлоголубой.
    2. Странно скорее другое: а откуда на не-IMMEDIATE-каналы добываются rflags, что ручки в момент ОБЩЕГО обновления всё же чернеют? Или там вычитывание данных у cda по приходу маркера выполняется?

    Выводы (нумерация унифицирована с предыдущим списком, а порядок -- с порядком действий):

    1. Надо посмотреть, когда/где вообще производится добыча rflags.
    2. Очевидно, что нужно ловить RSLVSTAT на ВСЕ каналы:
      • И на IMMEDIATE.
      • И на не-IMMEDIATE.

        ...хотя с этими вопрос дискуссионный: может, всё же по "маркеру"?

    06.04.2021: итак, "по обратному порядку":

    1. Добыча rflags -- да, делается
      • ТОЛЬКО в ProcessOneChanUpdate(), вызываемом лишь по приходу маркера (для не-IMMEDIATE-каналов) и по приходу индивидуальных данных для IMMEDIATE-каналов (которого для ненайденных никогда не произойдёт).
      • Плюс по RSLVSTAT со статусом !=CDA_RSLVSTAT_FOUND от маркера принудительно прописывается rflags=CXCF_FLAG_NOTFOUND всем-всем каналам.
    2. Ловля RSLVSTAT на все индивидуальные каналы:
      1. IMMEDIATE: в PzframeChanEvproc() добавлен анализ кода в reason; и конкретно по RSLVSTAT_NOTFOUND делается добыча у cda текущих флагов и "пинание" обновления ручки генерацией PZFRAME_REASON_PARAM.
      2. не-IMMEDIATE: в PzframeRDsCEvproc() всё ровно то же самое.

        Включая "пинание" обновления -- без него почерневало не сразу, а лишь по следующему приходу маркера.

        ...переименовать ба её ещё -- а это "RDsC" теперь некорректно отражает её суть...

      И при регистрации обоих долавлено CDA_REF_EVMASK_RSLVSTAT.

    Также было проведено исследование поведения при обращении к "без-серверным" устройствам -- через unknown:0.adc250_9a:

    • При натравливании на такое устройство вроде бы сразу же должно быть CDA_RSLVSTAT_SEARCHING, т.е., != CDA_RSLVSTAT_FOUND, из-за чего вроде бы должно сразу почернеть. А не чернеет!

      После часа возни и смотрения "как баран на новые ворота" наконец дошло: в этом случае событие RSLVSTAT прилетает сразу при старте -- т.е., во время нахождения в *DataRealize() и ещё ДО "оживления" GUI, выполняемого в *GuiRealize()позже. Поэтому, хотя событие и генерится, но ловить его некому (и даже чернеть ещё физически нечему).

    • Когда сервер найден -- это проще всего тестируется запуском сервера ПОСЛЕ скрина -- то ручки чернеют. ХБЗ ПОЧЕМУ...

      @после-обеда: радобрался:

      • Дело было в симулируемом сервере! Конкретно в режиме суперсимуляции "-S".
      • Там очень быстро проскакивало состояние JUSTCREATED, устанавливаемое при RSLVSTAT_FOUND маркера, ...
      • ...но потом оно тут же сменялось состоянием NOTFOUND, получаемым от rflag'ов (которые так и оставались =CXCF_FLAG_NOTFOUND, проставленным при RSLVSTAT_SEARCHING маркера) в момент общего обновления по приходу маркера.

        Маркер приходил сразу же после коннекта -- в режиме "-S" это канал записи, на который UPDATE/NEWVAL присылается сразу.

      • Казалось бы -- но и остальные каналы тоже должны б были обновиться, сбросив rflags=0.

        Но нет: "маркер"-то в таблице каналов стоит раньше, поэтому он и регистрируется раньше и значение на него приходит раньше.

        А если вручную из другого терминала сделать cdaclient'ом marker=0, то всё обновляется уже в нормальное состояние -- именно потому, что к тому моменту каналы ручек также успели получить обновления.

      Мда, вот такие неочевидные следствия работы с индивидуальными каналами с "маркером" в качестве сигнала готовности, вместо полноценной передачи всей информации о кадре в едином канале структурного типа.

      Т.е., я ПОЛДНЯ убил на поиск "ошибки", в реальности ошибкой не являющейся, а обусловленной отличиями работы симулятора от реального железа. Прямо "вскрытие показало, что пациент умер от вскрытия"...

    • А вот после убиения сервера всё предсказуемо чернеет -- это и правильно.

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

    07.04.2021: PzframeRDsCEvproc() переименована в более соответствующее PzframePrpCEvproc(); "Prp" -- "Properties".

    Засим задачу можно считать решённой. Отладочные печати убраны, порча имени канала тоже. Разделу ставим "done".

    07.04.2021: кстати, а теперь, когда есть индивидуальная ловля RSLVSTAT с надлежащей реакцией у обычных каналов, зачем оставлять групповое почернение/о-JUSTCREATED'нье? Может, убрать его (оно делается в pzframe_data и pzframe_gui рядом с ключевым словом PZFRAME_REASON_RSLVSTAT)?

  • 09.02.2022: обнаружился ляпчик: у ОДНОканальных осциллографов, у которых один канал работает и PZFRAME_CHAN_IS_FRAME, и PZFRAME_CHAN_MARKER_MASK, калибровки {R,D} на такие каналы НЕ приходили.

    09.02.2022: обнаружилось при расследовании замеченной 07-02-2022 странности у скрина adc1000: в нём почему-то показывались явно неправильные значения как на реперах, так и на осях.

    Вот и стал разбираться, в т.ч. напихал чуток отладочной печати касательно диапазонов, а потом уже осенило "а получаем ли мы {R,D}?!".

    Механизм проявления проблемы:

    • Ляп был заложен изначально, в 2016-м, и заключался в том, что каналы MARKER обрабатываются совершенно отдельно, в PzframeDataEvproc(), а не в PzframePrpCEvproc().
    • И, поскольку в первом, в отличие от второго, просто отсутствовала реакция на CDA_REF_R_RDSCHG, то и событие PZFRAME_REASON_RDSCHG не генерилось.
    • Соответственно, и fastadc_data.c::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 вроде тоже сделал.

    Но вылезли ещё косяки:

    1. Почему-то при старте сервера диапазоны у тех АЦП, где они в auxinfo не указаны, переключаются с =3 (читается из железки после сброса по 0x104=0x80000000) на =2;

      -- разобрался, накосячил сегодня с ADC4X250_PGA_RANGE_bits.

    2. В скринах adc250 значения каналов выбора диапазонов НЕ обновляются при изменении их из других экземпляров скринов (т.е., при изменении значений каналов в сервере).

      -- похоже, всё там обновляется (вот сейчас вечером проверил -- да!), а просто подтормаживает из-за того, что скрины запущены по сети на p320t, и чем дольше скрин провисел, тем дольше задержка (буфера отправки потихоньку растут (но всё же ме-е-едленно, до "send buffer overflow" дело не доходит), задержка тоже растёт).

  • 09.04.2022@утро-просыпаясь: При неуказанных cur_numpts_cn и common_cur_numpts_cn брать cda_current_nelems_of_ref() от канала данных.

    Смысл: чтобы для "искусственных" или "неродных для CX" осциллографов упростить жизнь -- чтоб можно было обходиться одним-единственным каналом "осциллограмма", не требуя наличия канала CUR_NUMPTS.

    09.04.2022: сделал -- надо-то было добавить несколько альтернатив к if()'у в ProcessAdcInfo().

    Проверить бы ещё...

    10.04.2022: проверил, с помощью onei32l_data.c, меняя значения параметра common_cur_numpts_cn в егойном FastadcDataFillStdDscr().

    1. Фиксированное значение, указанное с минусом -- работает.
    2. Указание FASTADC_DATA_CN_MISSING, означающее "брать текущее значение nelems" -- ТЕПЕРЬ тоже работает.

      Там добыча dataref'а канала данных линии --

      pfr->refs[atd->line_dscrs[nl].data_cn]
      а вчера накосячил и сделал
                atd->line_dscrs[nl].data_cn
      -- т.е., брался внутренний индекс канала, а не его dataref, так что cda возвращала -1.
    3. ...кстати, заодно проверил, что и со значением -1 оно тоже справляется (превращая в 0) -- когда ошибка возвращалась.

    Засим "done".

pzframe_drv:
  • 06.06.2016: создаём раздел.

    (Основная работа описана в "О базовых и настроечных каналах", но пора уже и свой завести.)

  • 06.06.2016: надо уметь отключать функционирование pzframe-каналов. Для камер нужно -- чтоб ВООБЩЕ НЕ гнали никакого потока, дабы не занимали полосу сети никто, кроме единственной.

    06.06.2016: первые мысли по теме:

    1. Напрашивается ввести специальный канал-"запрещатор", по умолчанию =0, в который когда записано !=0, то обычная работа подавляется.
    2. Спрашивается -- а КАК добиться нужного эффекта? Ведь надо не просто канал наверх не возвращать, а даже и запрос кадра не делать -- чтобы ethernet не нагружать.
    3. И при всём том надо, чтобы канал "не завис" -- когда запрос на него отправлен, но ответ никогда не придёт (из-за "запрета").

    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".

  • 18.06.2016: есть косяк с функционированием [Stop] -- канала _STOP, вызывающего PerformTimeoutActions() и, в т.ч., метод abort_measurements(), который у всех реализуется функциями AbortMeasurements().

    Суть проблемы в том, что по [Stop]'у данные возвращаются не сплошь нулевые, а ПРЕДЫДУЩИЕ.

    18.06.2016: анализ:

    • Разборки показали причину: при возврате данных -- в ReturnMeasurements() -- вызывается метод read_measurements(). Который у fastadc-драйверов радостно производит вычитывание из железки (где и лежат предыдущие значения), перепрописывая при этом нули.

      (Кстати, очевидно, статистика (CALC_STATS) -- которая также нулится в Abort'е -- тоже, наверное, должна считаться по этим предыдущим.)

    • Оно ведёт себя точно так же и в v2 (проверено на v5h1adc200s) -- там архитектура идентична.
    • Поиск по записям нашёл "причину-подозреваемого": в 201403-SNEZHINSK-ACTIVITY.txt за 27-05-2014. Там были проблемы с калибровками.

      ...хотя не факт, что в этот момент: предшествующий той дате 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: с пару недель назад имел разговор с Сашей Сенченко на тему, как у него ведётся программирование ADC4X250 -- по следам разборок моих с Котовым и Павленко, в результате которых выяснилось, что там между ADC_BREAK_ACK и START должна быть пауза.

    27.11.2017: так вот: у Сенченко юзкейс сильно отличается.

    • У него запуск делается не в один приём, а как бы в два: сначала все девайсы программируются, а лишь потом даётся отдельная команда ("команда" в смысле TANGO) "можно ждать старта".
    • Обусловлен такой подход иными потребностями машины: ЛИУ-20 -- штука импульсная, и там есть отдельная фаза подготовки.
    • ...соответственно, у него проблема малого времени между теми двумя записями в девайс не стоИт -- всё в принципе разнесено по времени.

    И тогда же была какая-то мысль, что при надобности и у нас можно б было с vdev реализовать аналогичную модель работы -- не по запросу измерения канала (осциллограммы), а именно по команде.

    Парой минут позже:

    • Да очевидно ж, как! Достаточно вызывать pzframe_drv_req_mes() не по запросу измерения канала, а именно по той команде "можно мерять!".
    • Понятно, что избавления от "проблемы малого времени между записями в девайс" это не будет -- но и не надо.
    • Вопрос скорее в том, надо ли такое делать общей фичей всех pzframe/fastadc-драйверов -- "импульсно-ориентированность"?

      А для реализации понадобилось бы дополнительно 2 канала:

      1. Режим работы -- запуск по чтению канала осциллограммы или по записи 1 в канал "можно".
      2. Сам канал "можно!".

    Короче -- ничего сложного, и даже вообще тривиально. Но как-то пока не требуется.

    15.10.2021: поскольку задача решена посредством PZFRAME_RUN_MODE_ON_RUN, то "done".

  • 21.10.2019: и ещё небольшое соображение на тему стартов: у нас ведь запуск процедуры измерений (программирование на измерения) выполняется по чтению любого из PZFRAME_CHTYPE_BIGC-каналов; всё-таки, а не будет ли проблемы, что клиент не успевает после получения предыдущего результата прислать запрос на следующий?

    Может, что-нибудь придумать?

    21.10.2019@~14:20, дорога к родителям мимо Коптюга 7 и 11: как вариант -- ввести стандартный для pzframe_drv параметр, управляемый каналом вроде "AUTOSTART" (не возникло бы путаницы с ISTART...).

    21.10.2019@вечер, ИЯФ: кстати,

    • "клиент не успевает" -- невозможно, поскольку этим занимается cxsd_fe_cx.c::MonEvproc(), который для ON_UPDATE-каналов делает следующий запрос СРАЗУ же при событии _CHAN_R_UPDATE.
    • Другое дело, что при использовании драйвлетов в задачу входит ещё и линия связи Мамкин<->сервер, так что при каких-либо тормозах там или при быстром приходе двух стартов подряд -- вполне может образоваться пропуск.

    И типа пояснения: параметр-то является каналом, но вся реальная отработка -- внутри pzframe_drv.c, в точности, как у ISTART.

    27.02.2021: вчера возникло подозрение, что такая "неуспевающесть" вследствие задержек сети может оказаться причиной пропуска запусков и навечного зависания у еманофединой софтины-менеджера/дирижёра крейта rfmeas:

    • Она дожидается прихода MARKER'а от ADC250 и после этого делает SHOT=1 L_TIMER'у.

      Смысл -- чтобы разрешать прохождение следующего запуска только ПОСЛЕ того, как все ADC250 вычитались. (Конечно, так нужно дожидаться маркеров от ВСЕХ осциллографов, но пока, для тренировки, ловится только один.)

    • Так вот: иногда как будто SHOT не отрабатывается -- следующего маркера от осциллографа более не прилетает, никогда.
    • И возникло предположение, что происходит следующее:
      • Осциллограф отрабатывает, вычитывается и отправляет данные наверх.

        После этого он переходит в режим ничегонеделания -- следующего запроса на измерение пока не было, поэтому pzframe_drv_req_mes() ещё никем не вызван.

      • Сервер (через remdrv) получает данные, отдаёт их далее, и в т.ч. cxsd_fe_cx'ный CHNEvproc() тут же запрашивает чтение этого же канала снова.
      • ...но запрос этот по сети ещё не успевает дойти до контроллера и драйвлета...
      • Тем временем менеджер/дирижёр получает маркер и тут же требует SHOT=1.
      • Теперь самое фантастическое предположение: этот запрос успевает обогнать по сети запрос на чтение/измерение осциллографических каналов.
      • ...и L_TIMER выполняет запуск (точнее, разрешение на его прохождение) ещё ДО ТОГО, как осциллограф запрограммирован и готов к ловле этого запуска.

        Вуаля -- запуск осциллографом пропущен!!!

      (Этой гипотезе противоречит то, что, как будто бы, именно реально не отрабатывается пуск -- по нему должно вернуться значение канала GATESTAT, а этого не происходит; как это вообще возможно -- загадка...)

    Размышления и анализ кода в cxsd_fe_cx.c наводят на мысль, что проблема вроде бы не в этом, но мало ли; а готовый проект решения, если что -- уже вот он тут есть.

    После того, как это всё записал, осенило --

    • а ведь ЕСТЬ вариант, как такому произойти!
      • Ведь в BIVME2 работает remsrv, и команды от сервера там обрабатываются не просто по мере поступления, но путём последовательного вычитывания из сокетов.
      • И если после отправки данных осциллографом...
      • ...будет что-то ещё делаться другими осциллографами -- а процессор (плюс VME-шина) там успевает с трудом -- то...
      • ...запрос SHOT=1 успеет дойти до контроллера...
      • ...и будет обработан в порядке перебора сокетов РАНЕЕ, чем также уже пришедший запрос на запуск осциллографирования.

      ЭТОЙ гипотезе противоречит то, что в 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 рестартовать.

  • 02.02.2020@дома, воскресенье: как обобщение 2 предыдущих пунктов: а не ввести ли канал "operation_model", описывающий, когда надо запускать измерения -- исполнять действия pzframe_drv_req_mes(), который бы мог принимать одно из 3 значений:
    • 0 -- по запросу чтения векторных каналов (по умолчанию, как сейчас);
    • 1 -- по специальной команде (придётся добавить канал);
    • 2 -- сразу же после возврата предыдущих данных.

    Смысл варианта 1 -- эмуляция работы как у Сенченко на ЛИУ-20. В этом случае драйверов StartMeasurements() будет вызываться только по команде.

    Смысл варианта 2 -- максимально быстрая готовность к следующему измерению, чтоб не ждало, пока измерения отдадутся наверх и оттуда придёт запрос на следующее измерение.

    03.02.2020: немножко анализа: для реализации оного понадобится 2 вещи:

    1. Поддержка в pzframe_drv -- в т.ч. некоторая модификация всей семантики pzframe_drv_req_mes().
    2. Пара каналов в карте -- "operation mode" (3-позиционный) и "enable start" (для реального разрешения).

    У большинства драйверов свободны каналы 18 и 19.

    27.03.2021: да, придётся. Конкретно на bivme2_rfmeas проявилась проблема: до драйвера ADC250 слишком долго идёт команда на организацию следующего измерения, в результате чего устройство умудряется пропустить следующий запуск (команда на который успевает придти "соседнему" драйверу L_TIMER'а раньше).

    Ключевым моментом является, видимо, то, что дохленький 50MHz MPC852 категорически не справляется с нагрузкой из 10 штук ADC250, так что, видимо, пока дойдёт дело до запроса на измерение line0 последнему осциллографу, уже успевает приилететь команд и произойти запуск.

    Конкретно ТАМ проблема была решена добавлением pzframe_drv_req_mes() сразу после pzframe_drv_drdy(), но вообще это плохая практика и нужно ОБЩЕЕ решение.

    Некоторые соображения:

    • Как делать на уровне API pzframe_drv -- понятно:
      • Замечание по терминологии: используем термин "run" -- т.е., "run mode" (а не "operation model" и не "start mode").
      • Добавить в 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
        
    • Реализация режима AUTO_RE_RUN тривиальна -- ровно так, как это сделано сейчас для ADC250: вызывать _req_mes() сразу после _drdy().
    • А вот с отложенным запуском при RUN:=1 будет сложнее -- придётся в куче мест учитывать.

      И само "учитываемое" надо будет добавить -- поле, сигнализирующее, что уже был _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-драйверах предполагает указывание только

    1. Параметров измерения -- которые прописываются в устройство при каждом StartMeasurements().
    2. Параметров обработки -- вроде CALCSTATS -- используемых самим драйвером в 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_NOT=0, MEASURING_RQD=1, MEASURING_RUN=2 (последнее аналогично былому !=0).
    • Был проведён внимательный просмотр кода на тему "как применяется measuring_now", с анализом, чтобы понять -- в какой ситуации как должно будет измениться условие.
    • По результатам сформировался постулат: операции вроде "shot" и "stop" (и тем более istart), а также elapsed касаются ТОЛЬКО состояния _RUN.

      Состояние же _RQD -- просто прописывается, запрещая тем самым дальнейшие переходы (кроме как по RUN:=1), но и всё.

    • Предыдущее значительно упрощает набор условий на тему "в каком состоянии measuring_now что можно делать".
    • "Мясо" pzframe_drv_req_mes() надо будет вынести в отдельную функцию, чтобы непосредственно запуск мог бы производиться и из других точек == вроде _RUN:=1.

    19.04.2021: реализация:

    1. Добавление новых определений:
      • PZFRAME_RUN_MODE_*
      • PZFRAME_MEASURING_*
      • Каналы RUN_MODE=18 и RUN=19 в картах каналов (_drv_i.h и .devtype) драйверов adc200 adc200me adc250 adc333 adc4 adc502 adc812me c061621 f4226 (а конкретно у ottcam -- каналы 28 и 29).
      • ...плюс эти каналы добавлены во всехние chinfo[].
      • ...и во всехние *_data.c::*_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.
    2. Модификация API, с нарушением совместимости:
      • К pzframe_drv_init() добавлены 6 параметров, отражающих свежедобавленные поля pzframe_drv_t, и их сохранение.
      • Соответствующее изменение всех драйверов.
    3. Собственно "мозги":
      • Перевод с 0/1 на MEASURING_NOT/MEASURING_RUN.
      • "Мясо" pzframe_drv_req_mes() -- собственно запуск измерения, с обвязкой -- вытащено в RunMeasurements().
      • В драйверах сбагривание указанных в auxinfo PZFRAME_CHTYPE_PZFRAME_STD-параметров pzframe_drv_rw_p().

    Некоторые соображения/сомнения насчёт "нештатного" завершения -- по STOP и по таймауту:

    1. Не сделать ли, чтобы по STOP прерывалось также и состояние MEASURING_RQD? Вот просто гложут сомнения, не надо ль такое...

      Главный вопрос -- ЗАЧЕМ? Что это даст и в какой ситуации? Ведь реально-то ничего не происходит и с точки зрения ДРАЙВЕРА никаких отличий между _NOT и _RQD нету -- параметры точно так же можно менять, пока не будет реально перейдено в _RUN.

      Сейчас представляется, что лучше просто подождать -- если возникнет РЕАЛЬНЫЙ запрос, тогда и посмотрим.

    2. В режиме AUTO_RE_RUN по STOP и по таймауту происходит переход в MEASURING_NOT, а не обратно в MEASURING_RUN.

      Насколько это правильно, что потребуется дополнительный запрос "сверху" для перезапуска? Но запрос-то будет -- в обоих случаях будет вёрнут канал с флагом TIMEOUT и сервер заново пришлёт запрос (либо, при STOP(VALUE_DISABLED_MASK), измерения будут автоматически перезапущены).

    20.04.2021: и ещё некоторые соображения/сомнения:

    1. Неприятно, что в режимах, отличных от RUN_MODE_ON_REQUEST и RUN_MODE_AUTO_RE_RUN, может возникнуть ситуация "осциллограф висит и на запуски не реагирует, и с ним ничего не сделать!!!11".

      Откуда грызёт мысль -- а может, всё-таки как-нибудь разрешить-таки реакцию на STOP из состояния MEASURING_RQD?

    2. "Добавляет" и тот факт, что текущее состояние -- значение measuring_now -- никак не увидеть.

      Может, нужен ещё один канал для отдачи наверх этого значения?

    25.04.2021: мыслишка -- а может, в режиме ON_RUN выполнять запуск, даже если ранее и не было запросов (на чтение), т.е., даже не из состояния _RQD, а прямо из _NOT?

    Сделать это, очевидно, несложно -- просто лишнее условие. Вопрос лишь в наличии реальной потребности: если никто не мониторирует -- то зачем?

    Кроме того, если запросов сверху не приходило, то и ничего из флагов *_rqd взведено не будет и при срабатывании осциллографа наверх ничего из векторных данных отправлено тоже не будет, а только статистика; ну и зачем тогда вообще стартовать?

    08.10.2021: надо бы всё же доделать. Битым текстом -- чего не хватает:

    1. Поддержки указания RUN_MODE в auxinfo.
    2. Убрать "лишний" вызов pzframe_drv_req_mes() из adc250_drv.c::FASTADC_IRQ_P().
    3. Протестировать работу на четвёрке ADC250.

    ДЕЛАЕМ. Пока что -- для проверки -- на adc250_drv.c.

    1. Поддержка указания RUN_MODE в auxinfo -- самое длинное:
      • Добавление строчки в adc250_params[] -- самое простое.

        Lookup-табличка покамест сделана локально -- adc250_run_mode_lkp[].

        Но потом надо её переделать в глобальную pzframe_drv_run_mode_lkp[], чтоб была доступна сразу всем драйверам.

        15.10.2021: да, углобализована.

      • Самый любопытный вопрос -- 1) КУДА добавить "инициализацию" и 2) КАК её выполнять.
        1. КУДА -- после анализа кода конкретно adc250_drv.c стало очевидно, что вставлять надо в InitParams(), в самый конец, т.к. это точка уже непосредственно перед возвратом из _init_d(), и к этому моменту:
          1. pzframe_drv_init() уже вызвана, так что контекст создан/инициализирован;
          2. все параметры уже также инициализированы (или прочитаны из железа).

          (Единственное сомнение -- "а что, если что-то пойдёт не так и драйвер решит застрелиться ещё ДО возвращения из _init_d(), а мы тут вернём DEVSTATE_OPERATING? Ведь в cxsd_hw.c::InitDevice() нет проверки "а не застрелилось ли уже устройство к моменту возврата из метода init_dev()?" А надо бы добавить! 09.10.2021: да, добавлено; подробнее -- в разделе по cxsd_hw.)

        2. КАК -- вроде очевидно: просто передать значение в pzframe_drv_rw_p().

          Вот оная передача и сделана, причём вообще безусловная.

        А потом, порывшись, нашёл заметки на эту тему от 16-04-2021:

        1. "Куда" там не сказано, но предложен вариант "организовать цикл по chinfo[] ... PZFRAME_STD" -- это сработает для ВСЕХ pzframe_drv-параметров, включая ISTART и WAITTIME.
        2. КАК -- предложено также сбагривать в pzframe_drv_rw_p(), но только условно (при >=0).

        Посему -- да, так и переделано.

        14.05.2022: ага-ага, только "переделано" тогда (09-10-2021?) оно было ТОЛЬКО в adc250_drv.c. Сегодня же обобщено -- подробнее см. в отдельной секции ниже за сегодня.

    2. Убрать "лишний" вызов было наименьшей проблемой.

    09.10.2021: проверяем...

    1. Проверено -- да, на вид всё работает.
      • Протестированы ВСЕ режимы.
      • ...хотя, надо признать, и НЕ вся функциональность: пока что не проверено, как срабатывают всякие STOP'ы в разных режимах и на разных "стадиях" (в разных состояниях PZFRAME_MEASURING_*).
      • ...и, кстати, НЕ протестирована работа auxinfo-параметров (не говоря уж о том, что в другие драйверы оно пока НЕ сделано.)

    15.10.2021: добиваем:

    • Табличка adc250_run_mode_lkp[] углобализована в pzframe_drv_run_mode_lkp[].

      Проверена -- работает.

    • Также проведён небольшой тест на тему "как себя ведёт measuring_now в разных режимах", для чего в adc250_rw_p() добавлена отдача этого значения по запросу канала 51 (это отсутствующий ADC250_CHAN_CLB_STATE).

      На вид -- вроде бы всё как задумано работает. (Хотя, надо признать, полное тестирование -- всей матрицы вариантов -- проводить было просто лень.)

    • И во все pzframe_drv-драйверы поддержка указания в auxinfo скопирована (кроме ottcam_drv.c, у которого вообще PSP-таблица отсутствует, ибо парсинг auxinfo ручной -- там адрес камеры).

    Засим проект вроде можно считать доделанным, так что "done".

  • 14.05.2022: о возхможности указывать в auxinfo "бихевиористские" параметры -- которые обрабатываются pzframe_drv_rw_p() -- istart, run_mode, wait_time. Принципиальный кусок был сделан в октябре 2021, а сегодня оно "обобщено".

    14.05.2022: тогда (09-10-2021?) оно было сделано ТОЛЬКО в adc250_drv.c, причём прямо в конце InitParams(); в camac/ и cpci/ это всё бы не работало, хотя всякие "istart"/"noistart там давно есть.

    Теперь же "обобщаем":

    • Тот цикл перенесён в "будущий vme_fastadc_common.h" -- в 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.

      Кроме того,

      • А вдруг в списке auxinfo-параметров окажется не просто бихевиористский параметр, а прямо сразу КОМАНДА -- например, "shot=1"? Тогда ведь эта команда начнёт исполняться в ещё как бы недоинициализированном драйвере (15.05.2022: хотя, вообще-то, в ДОинициализированном -- ведь этот цикл делается в самом конце, ПОСЛЕ всей прочей инициализации).

        ...но так-то и работающая уже полгода в 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, в которое "машина состояний" без явного запроса не попадёт.

    • Но было решено сделать так, как сделано, потому, что
      1. Сделанная ещё в октябре полгода отработала безо всяких проблем.
      2. Предполагается, что авторы драйверов не идиоты и не станут разрешать помещать в auxinfo команды.
    • ИДЕАЛЬНЫМ вариантом было бы свалить эту "инициализацию бихевиористских параметров" на pzframe_drv_init(), чтобы она брала оттуда только несколько интересующих её значений -- конкретно ISTART, RUN_MODE и WAITTIME.

      Но для этого пришлось бы добавлять к ней аж 3 параметра: chinfo[], chinfo_count, nxt_args[]. Так что -- пусть будет так, как сделано сейчас.

oscilloscope:
  • 03.03.2020: заводим раздел по программе "осциллограф вообще".
    • Рабочее название -- "oscilloscope"
    • Когда-то (в 2000-х?) обдумывалось также название "cxoscope".
    • Идеологическим предшественником этого раздела является одноимённый раздел в bigfile-0001.html.

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

  • 26.02.2020@лыжи, начало 2-й 2-ки: некоторый поток мыслей насчёт "неспецифичного АЦП":
    • Можно изготовить такой хитрый ТИП осциллографа (_main+_knobplugin+_gui+_data) чтобы он парсил из параметров командной строки (main) или options (knobplugin) свою "конфигурацию" -- количество и список каналов, названия каналов маркеров, ..., и на лету создавал бы свои _chan_dscrs[] и _line_dscrs[].
    • Замечание 0: "конфигурацию" стоит указывать отдельным ОДНИМ psp-параметром (вложенным PSP?), в котором перечислять всё-всё, например, через запятую.
    • Замечание 0.1: при надобности указывать диапазоны значений каналов (хотя и автоскейлинга хватит), label и units -- можно использовать формат от histplot'а.
    • Замечание 1: число каналов-"линий" стоит ограничить 16 -- это и на графике будет видно приемлемо, и таблицу можно будет сделать простым массивом, а не заниматься предварительным подсчётом количества.
    • Замечание 2: пусть так указываются только каналы, необходимые для графика (всякие marker, numpts+ptsofs, xs*), а обвязка (управление диапазонами, таймингом, ...) -- не его компетенция, её можно делать в скрине отдельно.
    • Замечание 3: как поддерживать МНОЖЕСТВЕННЫЕ маркеры (ведь при компоновке нескольких АЦП с внешним запуском маркеры будут прилетать пачкой)?

      Идея: на 1-й реагировать сразу, а следующие -- не ранее, чем, например, через 100мс (а если в течение этих 100мс были ещё, то по прошествию надо обновиться принудительно); хотя модель из manyadcs_knobplugin, где ждало "всех" выглядит красивше, но она не обломится ли при отказе части устройств (и неприсылании ими маркеров)?.

    • 04.03.2020: а ещё, возможно, понадобятся "множественные NUMPTS'ы": если данные от разных осциллографов, то надо брать максимальный из.

      Как это реализовывать -- хбз.

      • То ли правда как-то указывать несколько каналов и из них генерить один.
      • Тогда надо будет делать какой-то FastadcData-API для указания "сейчас NUMPTS равно вот столько-то".
      • То ли сделать возможность "авто-подбор по максимуму из числа точек каналов".

      Поизучал код в fastadc_data.c: ни-че-го не надо делать!!! УЖЕ всё есть:

      1. В fastadc_line_dscr_t есть поле cur_numpts_cn.

        У всех нынешних осциллографов в этой позиции стоит FASTADC_DATA_CN_MISSING.

      2. А в ProcessAdcInfo() выбирается максимум среди всех по-канальных cur_numpts и общего cur_numpts.

      Сделано это было, видимо, для manyadcs (которого пока так и нет), и для "осциллографа вообще" подойдёт идеально.

    • Таким образом можно будет как делать "вырезки" из многоканальных осциллографов, так и составлять "композитные осциллографы" из каналов нескольких устройств.

    Тут надо дополнительно отметить пару процедурных аспектов:

    1. Оно должно быть И программой, И knobplugin'ом.

      Чтобы можно было и натравить из командной строки на некоторый набор каналов (программа "oscilloscope"), и использовать в скринах (библиотеку "liboscilloscope_knobplugin.a").

    2. Жить исходники должны, вероятно, в lib/pzframe/.

      Да, кривовато, но уж как есть; а если пихать в xmclients/, то там библиотека knobplugin'а будет не к месту.

    28.02.2020: поговорил с ЕманоФедей -- неа, сейчас оно не понадобится: мои скрины им не нужны, справятся с рисованием интерфейса сами. Так что оставляем проект просто проектом; если понадобится в будущем -- тогда и разморозим.

  • 20.10.2021: некоторые общие размышления на ту же тему, но с точки зрения "всё ли делать только на стороне клиента".

    Появились они по результатам обмозговывания реальной задачи -- как сделать инженерный скрин для системы наблюдения за кикерами, где в cxhw:18 вместо 2 штук ADC200 стало 4, причём по паре составляют по сути "один 4-канальный осциллограф", так что желательно бы и отображать их на экране одной 4-канальной панелькой, а не плодить толпы 2-канальных (которые ещё и в экран плохо лезут).

    • @перед переходом через Ильича, после спуска по тропинке справа от Ильича 3, ~14:20: может, как минимум, начать в самом devlist-cxhw-18.lst с простейших действий -- наплодить cpoint'ов, образующих набор каналов для линий этих "псевдо-4-канальных осциллографов"?
    • @около детсадиков Детский проезд 6 и 8 и домов Морской 8 и Терешковой 26, ~15:40: а правильно ли решать эту задачу "создания композитного осциллографа" лишь на уровне КЛИЕНТА?

      Может, стоит часть работы выполнять на стороне СЕРВЕРА?

      Т.е., прямо ДРАЙВЕР oscilloscope_drv.c, который бы по указанной в auxinfo информации собирал бы каналы от других устройств, представлясь наверх как единый осциллограф?

Одноканальные осциллографы: onei32l: onef64l:
  • 07.04.2022: ещё в прошлую пятницу 01-04-2022 возникло желание иметь плагин-панель "простейший осциллограф на 1 канал", чтобы без каких-либо ручек настроек, а только сама осциллограмма (ну и плюс канал NUMPTS и, возможно, всякие реперы). Смысл -- чтоб слабать панель для "контроллера Торнадо", где через cda_d_epics получать данные.

    Вот сегодня приступаем. Штука обозвана "oneline".

    08.04.2022: а ведь по-хорошему надо кроме int32-панели иметь и аналогичную для float-каналов.

    Тогда "oneline" переименовать в "onei32l" ("One int32 line"), а вещественную назвать "onef64l" ("One float64 line").

    09.04.2022: переименовываем.
  • 07.04.2022: начинаем. За образец взят c061621; точнее -- c061621_data.c и c061621_gui.c.

    08.04.2022: делаем.

    • Поскольку _drv_i.h-файл всё же нужен, для определений индексов каналов и прочих всякостей, а у "фиктивного" осциллографа его быть как бы не может, то делаем локальный pzframes/oneline_drv_i.h.
    • Далее повыкинуто всё "лишнее"
      • Из oneline_data.c -- вообще практически всё:
        • _info2mes() стала пустой (потом в неё, видимо, обсчёт min/max/avg попадёт).
        • _x2xs() устранена полностью.
        • В описании dscr'а почти все каналы превратились в FASTADC_DATA_CN_MISSING.
      • Из oneline_gui.c -- оставлено только COMMONS и NUMPTS.

        ...но часть просто закомменчена (чтоб как минимум реперы потом вернуть, в одну линию с прочим).

    • Должным образом регистрируем/описываем в pzframes/Makefile, а затем добиваемся компилируемости.

      ...и запускаемости.

    Теперь надо:

    1. Проверить это на каком-нибудь настоящем осциллографе (например, на одном из ADC250 в клистронке).
    2. Добавить/вернуть реперы.

    08.04.2022@совет-по-СКИФ-~15:00+: давно бродит соображение, что нужно уметь насчитывать минимум, максимум и среднее по полученной осциллограмме. Причём:

    1. Как-то отдавать результаты "в общее пользование" -- чтобы прочие компоненты скрина могли бы эти значения, например, отправлять на самописец.
    2. Как считать СРЕДНЕЕ для ЦЕЛОЧИСЛЕННОГО? Ведь переполнение может быть.

      Ну, видимо, через int64.

      09.04.2022: сделано -- весь подсчёт min/max/avg (последний через int64). Пока холостое -- результаты никуда не деваются.

      ...а потом ещё и {R,D}-конвертировать.

    10.04.2022: проверено -- панелька, в своём минимальном варианте, работает.

    • "Минимальность" -- ни реперов нет, ни X_CMPR, ни LINE_MAGN (особенно последнего сильно не хватает, т.к. осциллограмма почти нулевая).
    • Поскольку в 4-м здании на выходных была профилактика электропитания, то ADC250 в клистронке использовать не удалось, а был взят один из тех, что молотят на b360mc.
    • Чтобы значения (работающие в МИКРОвольтах!) отображались корректно, у бриджуемого канала надо указывать "r:1000000".

    11.04.2022@утро: после того допиливания собственно отображения весь день вчерашнего воскресенья обдумывал задачу "а как бы всё-таки отдавать в скрин насчитанные значения min/max/avg?". Там довольно грустно:

    • Идея-то изначально (когда только задумывал всю архитектуру oneline) была в том, чтобы насчитанные значения сбагривать в "локальные переменные" -- %КАНАЛы.

      Но с нынешней архитектурой pzframe/fastadc это так просто не получится, т.к.

      • Нету никакого "privrec'а" у конкретного "типа" (а есть только у "широких классов" -- fastadc, vcamimg, wirebpm).

        Т.е., dataref'ы от %КАНАЛов даже хранить просто негде.

      • Более того, даже и никакого метода "init" или "realize" на уровне "типа устройства" тоже нету (а только опять у "широких классов").

        Соответственно, даже если бы и было куда складировать, то НЕКОМУ было бы регистрировать каналы.

      Ну да, вот така "квазиобъектная" архитектура получилась. Просто не было надобности конкретным 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: работаем!

    • Проверяем работу onei32l: бриджуемая осциллограмма отображается, но как-то странно: сплошной 0.
    • Драйвер vector_minmaxavg_drv.c сделан, но почему-то данные не отдаёт: постоянно NEVER_READ.

    12.04.2022: разбираемся.

    • График "сплошной 0" был оттого, что в bridge_drv отсутствовал флаг NO_RD_CONV, так что сырые значения (в микровольтах) сначала приводились в диапазон [-10.0,+10.0] с сохранением в int16 (фактически превращаясь в 0), а потом ещё раз делились на 1000000, что уже гарантированно делало постоянный 0.

      Добавлена поддержка флага "@.", включающего NO_RD_CONV, после чего сырые значения стали осмысленными -- совпадающими с сырыми данными исходной осциллограммы.

      ...но на экране по-прежнему плоская горизонтальная линия в 0... Что-то с параметрами/калибровками?

    • Проверяем, кто же виновник:
      1. Сначала пытаемся натравить скрин adc250 на бриджуемый канал. Фигушки: ему не хватает какой-то толпы служебных каналов...
      2. Тогда натравливаем скрин onei32l на живой adc250: и вот тут видим опять горизонтальную линию в 0, даже при x100, притом, что реальный скрин там же показывает "пилу шума".

      Вывод: что-то накосячено в oneo32l_data.c.

    • Разобрался: да, "накосячено". Только не в самом драйвере, а в его ИСПОЛЬЗОВАНИИ. Точнее, конкретно в диапазонах и калибровке R.

      Дело в том, что планировался драйвер под работу с "контроллером Tornado", где диапазон [-10V,+10V] и данные в микровольтах (R=1000000), а тестируется с ADC250 -- где и диапазон другой (по максимуму [-32768,+32767], а обычно и того меньше), и калибровка 4096 вместо 1000000.

      Вот и получалась горизонтальная линия при отображении под намного более широкий диапазон.

      При замене в onei32l_drv_i.h диапазона на [-32767,+32767] картинка отобразилась соответствующая оной от скрина adc250 (вдвое ниже, но то из-за конкретного текущего диапазона).

    • С неотдачей данных драйвером vector_minmaxavg проблема оказалась в невнимательности:
      1. В конфиге ссылка на канал-bridge была указана как adc1 вместо insrv::adc1.data, в результате искался глобальный канал "adc1", а на RSLVSTAT_SEARCHING драйвер никак не реагирует.
      2. И в самом драйвере в cda_new_context() параметр defpfx был NULL вместо "insrv::".

        ...да, наследие от mirror_drv.c -- в том так же, ЕДИНСТВЕННОМ из "предназначенных для внутрисерверной работы".

      После исправления обоих косяков проблема исчезла.

    • В скрин добавлены недостававшие детали -- X_CMPR, LINE_MAGN и реперы (именно LINE_MAGN и помог доразобраться с "горизонтальной линией").

    Вывод: концептуально-то вроде работает, но нужно бы сделать как-то понадёжнее и постабильнее.

    @вечер, ~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 избавляться.

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

:
remote-serial-connection:
  • 06.08.2024: создаём раздел.
  • 06.08.2024: описание соображений.

    06.08.2024: цепочка:

    • Предпосылка: потенциально наклёвыватся несколько драйверов (leybold, pfeiffer, phytron, прочие КШД) и уже есть несколько штук разной степени "продвинутости по полноте (где есть TCP, а где только serial, и в последних отсутствует reconnect)" (kshd485, liu_key, triadatv_um, modbus_tcp, да тот же remdrv), реализующие плюс-минус одну и ту же функциональность:
      • Связь с устройством посредством взаимодействия:
        1. с serial-link'ом, желательно с возможностью пере-открытия (критично для USB-адаптеров, которые /dev/ttyUSB*), либо...
        2. ...с TCP-соединением к некоему порту, и тут уж ОБЯЗАТЕЛЬНО нужна поддержка реконнектов, либо...
        3. ...с serial-link'ом, доступным через TCP-соединение к указанному порту -- это требуется для всех serial-устройств, т.к. сейчас многое работает через MOXA nPort.
      • Почти всегда -- кроме работы с заведомо одиночным устройством (remdrv либо в случае RS232 с единичным девайсом) -- требуется layer'ность: за одним "портом" может скрываться несколько устройств, и "портов" этих может быть более одного.
      • И реконнекты по факту необходимы ВЕЗДЕ, но реализованы они НЕ везде (в kshd485_drv.c нету), а где реализованы -- повсеместно по одной схеме, скопированной изначально с remdrv_drv.c (а туда -- с CXv2'шного cda.c), включающей ScheduleReconnect() и флаги is_suffering/is_reconnecting, is_ready, was_success (список/имена флагов от варианта к варианту разнятся).
      • Нюанс: в основном для отправки/приёма используется fdiolib, но местами -- голый cxscheduler напрямую (kshd485_drv.c).
    • И поскольку таких случаев уже ОЧЕНЬ не один, то хочется вместо дублирования кода с подпиливанием под каждый конкретный случай заиметь некое ОБЩЕЕ РЕШЕНИЕ.
    • В виде библиотеки-"кубика", наподобие vdev, чтоб можно было сочетать с другими кубиками -- sendqlib, pzframe_drv, тот же vdev.

      Т.е., видимо, чтоб функциональность инкапсулировалась в некоем объекте (который бы и содержал поля-флаги и всякие rcn_tid), а непосредственно "действия" по взаимодействию -- те же чтение/запись, да даже взаимодействие с fdiolib -- выполнялись бы "виртуальными методами наследника", т.е., кодом из "юзера"; ровно аналогично vdev и pzframe_drv.

      По минимуму -- примерно то, что сделано в modbus_mon.c: там как раз и работа как с TCP, так и с локальными устройствами есть, и реконнекты -- причём с ОБОИМИ видами "подключений".

    • Но вот как конкретно ЭТО должно выглядеть, не очень ясно -- с учётом затребованной реализации layer'ности, требуется нечто весьма развесистое (если не сказать монструозное).

      ...да, в EPICS что-то из этого делается AsynDriver'ом, но он-то как раз монстр.

    • @вечер, ванна с 10см воды (горячая закончилась): а ведь НЕ НАДО реализовывать никакую "полную инфраструктуру": по факту, надо реализовать функционирование ОДИНОЧНОГО ПОРТА -- именно то, что делает modbus_mon.c, а уж потом пусть код-юзер (да хоть тот же piv485_lyr.c) при надобности объединяет это в layer.
    • Но вопросы с названием всё равно остаются: это что-то вроде "remote serial device" -- "remdev", "serdev", "remserdev"?

      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: ещё соображения:

    • Желательно унифицировать так, чтобы после инициализации работа с локальным и TCP-устройствами не отличалась бы вовсе.
    • Менеджмент TCP-соединений лучше бы возложить полностью на библиотеку/объект -- чтобы в т.ч. в перспективе была бы поддержка IPv6.
    • ...а вот с /dev/tty* сложнее: тут ведь может требоваться нетривиальная настройка...

    08.08.2024: продолжаем размышлять.

    • Ну, во-первых, правильнее будет говорить именно о СОЕДИНЕНИИ, а не об устройстве -- т.е., CONNECTION, а не device.

      Тогда название получается либо "pconn", либо "lrpconn" -- "Local/Remote Persistent CONNection".

    • @душ ~14:00: коль уж обобщать концепцию: вот сейчас у нас предполагается 2 вида соединений:
      1. локальное -- как результат open("/dev/...") или прочего открывания, дающего файловый дескриптор;
      2. удалённое -- то, куда надо делать connect().

      А какие ЕЩЁ могут быть варианты?

      Может, стоит как-то обобщить, сделав "центральной" (core) функциональностью менеджмент переподключений, а собственно открывание/коннекченье -- вариантами/"плагинами"?

      И да, тогда точно надо постараться понять, какие ещё могут быть варианты.

    • Кстати, попытки подключения в cda_d_tango.cpp (в виде создавания DeviceProxy) -- вполне так пример такого "плагина", хотя и весьма хренового (из-за блокирующести).

    09.08.2024: продолжение размышлений/соображений:

    • Спросил ЧеблоПашу -- неа, у него иных идей (кроме очевидных TCP и USB/файл) нету.
    • Мою идею "PCIe-адаптеры могут быть Plug-and-Play" он отрицать не стал.
    • Но, кстати, в некотором роде может быть что-то вроде CAN: там
      • формально "подключения" нету, да и "арбитраж доступа" не требуется (в отличие от serial-линков и serial-via-TCP),
      • но USB-CAN-адаптер ведь может быть вынут и вставлен, так что нечто вроде "реконнекта" (в т.ч. при первоначальном обломе) было бы нелишним.
    • ЗЫ: оказалось, что в can/socketcan/sktcan_hal.h НИГДЕ НЕ делается 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 уровня ПЯТИслойного стека модулей -- "Менеджер портов", "Драйвер порта", "Открыватель конкретного типа порта".

  • 22.02.2025: здесь будем записывать соображения по реализации нижних слоёв "ПЯТИслойного стека модулей" (с расчётом на использование в modbus_lyr.c).

    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": он ведь сразу "готов" после открытия.

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

    • Вариант генерации .mkr-файлов был слегка "лопухнутый" -- они создавались бы для ВСЕХ target'ов, в т.ч. и для простых/монолитных, типа .h-файлов.

      Посему теперь 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) -- дабы лишние пробелы не плодились.
    • Проверил изготовляемость .so-файлов, на примере lib/Xh/bigredcur.so -- работает.

      Только при этом обнаружилось, что шибко уж многое не "подключается" при отсутствующем 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: продолжение пикника:

    • Вчера сделал поддержку "debuggability" -- в зависимости от $(DEBUGGABLE) уставляются флажки "-g" для компилятора и линкера, а также оно управляет флагом "-s" линкера.
    • Вчера вылезли две вещи. Во-первых, поддержка указывания библиотек не была сделана никак. Во-вторых, конкретно для .so-файла bigredcur.so захотелось иметь при линковке флажок -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_nnnPer-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: сделано:

    1. Rules.mk->TopRules.mk
    2. SRCDIR->TOPDIR
    3. PRJ*->TOP*
    4. SPJ*->PRJ*

    23.01.2012: имелась лёгкая кривость: UNCFILES входили в список MONOLITHIC_TARGETS, и для них пытались (через MONOLITHIC_DEPENDS) генериться .d-зависимости. Естественно, для всяких скриптов и тому подобных НЕ-.c-файлов сие обломится. Решение таково:

    • UNCFILES вынесена из MONOLITHIC_TARGETS напрямую в TARGET_FILES. Так что проблема зависимостей решена.
    • Кривоватое название "LOCAL_MONOLITHIC_TARGETS" переделано в MONO_C_FILES. При надобности C++ можно будет добавить туда и MONO_CXX_FILES. Благодаря тому, что список зависимостей формируется не как ".c:=.d", а "$(addsuffix .d, $(basename ...))", это будет работать -- вообще ЛЮБОЕ расширение покатит.

    23.01.2012: добавлена еще одна категория -- ARCH_, для специфичностей [кросс-]компиляции под конкретную архитектуру.

    • Смысл -- для установки в файлах типа ppc860_rules.mk, которые как бы ортогональны всему остальному (так что, к примеру, в категорию DIR_ оно не попадает -- в DirRules.mk может быть что-то своё).
    • Расположено между DIR_ и PRJ_.

    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 изведена.
    • И *.log *.pid, бывшие раньше содержимым WORKFILES, теперь переехали в серверовы директории в LOCAL_WORKFILES.
    • Замечание: ShadowRules.mk должна использовать категорию 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.
    • В ней можно указывать файлы, которые важны, но "даны свыше" (given).
    • По сути своей эта вещь выглядит локальной, так что едва ли потребует "категорий".

    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).

    • Смысл -- чтоб мочь помещать в .a-библиотеки также некие сторонние ("given") файлы, не пытаясь организовывать их сборку (в т.ч. махинации с .d), а просто беря готовое.
    • Понадобилось для влинковывания в libcda.a v2-библиотек, собираемых в соседней ../v2cx/: просто засунуть в наш .a другой .a никак -- ar этого не умеет (кроме как в экзотичном варианте "ar -M", со скрипт-командой "addlib"), а вот его составные части -- проще.

    25.02.2016: в TopRules добавлена поддержка "install" и "exports" -- скопирована из v2 (а в GeneralRules.mk всё уже было, т.к. он идентичен).

    • Также заполнена EXPORTSFILES в include/Makefile -- без этого ничего не работало.
    • Замечение: DirRules.mk сейчас есть только в programs/drivers/. Понадобятся ли еще где-нибудь -- неясно (т.к. вся архитектура GeneralRules сделана так, чтобы минимизировать специализированность директорий).

    Если где чего будет не хватать -- добавим в рабочем порядке.

  • 14.02.2006: еще требование: в режиме "вне дерева" и библиотеки должны браться из exports/, и include-файлы!

    (А то в CXv2 include-файлы ВСЕГДА берутся из src/include/.)

    14.07.2008: в CXv2 это уже исправлено -- 23-05-2008.

    20.04.2012: а здесь-то тогда еще нет! А "done" уже поставил!

    Но сейчас действительно пофиксил.

  • 15.05.2006: еще с давних-давних времен, когда я пользовался программой deco, и она оставляла backup-файлы .b, в Rules.mk в определении BCKPFILES упоминалось "*.b .*.b". Это явно устарело -- разумные редакторы всегда делают backup-суффикс ~, а расширение ".b" может где-то и использоваться.

    15.05.2006: удалены.

  • 14.07.2008: надо СРАЗУ делать все отдельными под-проектами -- отдельно "базу", отдельно драйверы, и т.д. Как именно побъем на компоненты, и какая для этого понадобится дополнительная инфраструктура?

    (По опыту запинывания сегодня связки CX+cm5307_драйверы+клиенты на RH-7.3@beast в ЭЛС-2.)

    15.07.2008: собственно, а проблема-то в чем? Видимо, в том, что хочется:

    1. иметь как бы "раздельные" директории/проекты для в-принципе-независимых компонентов;
    2. между ними все-таки будут иметься зависимости (еще бы -- драйверы-то точно зависят от API сервера; клиенты -- от библиотек...);
    3. а инсталлировать всю шоблу на некую машину -- в один прием, максимально просто.
    Мда... Немного напоминает ситуацию с RPM-пакетами, но именно немного...

    13.05.2012@Снежинск-каземат-11: с тех пор всё именно по такой схеме и делается -- 4cx/+drivers/, cx/+v2hw/, так что этот раздел можно считать "done".

  • 30.08.2008: кстати, в линуксовом ядре подсмотрел -- у листинг зависимостей для файла, например, FILENAME.o будет иметь имя не FILENAME.d, а FILENAME.o.d. Т.е., в принципе схема более гибкая -- как минимум тем, что позволяет иметь "генеримые" зависимости НЕСКОЛЬКИМ звеньям в цепочке исходник->конечный_файл.

    Пожалуй, стоит перейти на такую схему.

    26.01.2012: да, воистину стоит -- тем более, что .mkr именно так (просто добавлением) и делается. Вообще, нынешняя схема -- это скорее "хак", т.к. в нём недостаточно общности (хотя оно и чуток элегантнее).

  • 21.02.2009: пришла в голову идея, как можно сделать поддержку сборки не только "снаружи" дерева, но и при инсталлированной системе.

    Тривиально -- в exports/ надо иметь еще одну директорию, которая содержала бы отражение обычного дерева src/, но населением там были бы только *.mk-файлы. И выделить для них даже отдельную "категорию" -- EXPORTSFILESM.

    13.05.2012@Снежинск-каземат-11: да, ЭТО -- надо сделать, только с учётом желания перейти со статичной схемы EXPORTS{FILES,DIR}{,2,3} на конфигурируемый (см. за 25-04-2012).

  • 20.08.2009: портировал сюда из CXv2::Rules.mk фенечку с отключением "set -e" при наличии ключа "-k".
  • 10.01.2009: плохо, что правило для %.so выглядит как "%.so: %.o": из-за этого файл с тем же basename, что и у самого .so, становится необходимым -- что неправильно.

    По-хорошему, надо б оттуда вообще %.o убрать, а исходники/компоненты пусть определяет по зависимостям. Единственный вопрос -- для однофайловых, чтоб оно как-то догадывалось об исходниках БЕЗ target_COMPONENTS...

  • 02.09.2010: очевидная мысль:

    Все куски/директории, допускающие "стороннее" использование (хоть симлинкованием, хоть напрямую) должны САМИ предоставлять make-код, обеспечивающий это.

    А вовсе НЕ места-юзеры должны сим заниматься, как сейчас.

    02.09.2010: Преимущества также очевидны:

    1. Вся алхимия находится в одном месте, а не разбросана по количеству "юзеров".

      Соответственно, любые исправления делаются легко и просто, причём в одном-единственном экземпляре.

      Собственно, это вообще очевидно -- так бы и надо было делать с самого начала (но история была другой -- "симлинкуемости" были приёмышами).

    2. В любой момент можно будет поменять модель с "симлинкования" на "прямое использование оттуда".

    Примерно так уже сейчас сделано в work/drivers/can/, но:

    • Там оно так потому, что директория common_sources/ изначально рассчитана ТОЛЬКО на стороннее использование.
    • А надо бы такой же подход ввести и во всех lib/, которые будут полезны и для "внешнего" применения (реально -- все, кроме Motif-related).

      И даже programs/ (типа server/, drivers/ и utils/.

    • И надо бы файлы, ответственные за это, называть не "LocalRules.mk" (в этих определяются именно правила, локальные для директории, которые применяются и к содержимому внешних директорий).

      А, например, ShadowRules.mk.

    • Ежу понятно, что именно в эти файлы уйдёт бОльшая часть содержимого Makefile'ов -- поскольку в теневых директориях эта информация будет требоваться наравне с собственно именами файлов и правилами линкования.

      Впрочем, в нынешней GeneralRules.mk-based системе сборки бОльшая часть содержимого Makefile'ов -- это и есть собственно списки компонентов. Всё же прочее -- это зависимости да определение специфичных флагов.

    25.12.2011: да, надо сделать вот ТАК:

    • Файлы, ответственные за специфичные для некоей директории правила сборки (которые сейчас -- LocalRules.mk), должны называться DirRules.mk. Это, кстати, кореллирует с соглашением о переменных -- соответствующие называются DIR_nnn.

      Другое дело, что с более красивой и стройной v4'шной системой сборки (переменные CLASS_nnn, TARGET_COMPONENTS, авто-генерация через *.mkr) надобность в таких файлах сильно падает -- фактически на них останутся лишь определения доп. библиотечных зависимостей.

    • Для каждого TARGET-файла (включая как исполняемые, так и .a) должны быть файлы TARGET_Rules.mk. Например -- libuseful.a_Rules.mk, долженствующий, в частности, определять переменную libuseful.a_COMPONENTS.
    • Надобность в ShadowRules.mk не до конца определённа, но, возможно, именно в них надо складывать правила для конкретно КРОСС-сборки. И как раз они будут include'ить файлы TARGET_Rules.mk.
  • 17.04.2012: к вопросу о поддержке директорий с драйвлетами (которые DRIVELETS=...).
    • Некоторое время назад, в районе 26-01-2012, в интересах CXv2'шной programs/server/drivers/bivme2/DirRules.mk была введена DIR_DEPENDS.
    • Так вот -- её мало. Смысл был в том, чтобы можно было работать не только со стандартными TARGET-определениями.
    • А вот обломилось -- зависимости оно делает, зато готовые бинарники не удаляет. ибо не знает про них.
    • Конкретно сейчас это решается просто-халтурно: введением еще и DIR_TARGETS.
    • Но вообще надо думать шире -- что-то тут с моделью не так.

      Придумывать какую-нибудь замуть на тему преобразования имён между исходниками и бинарниками?

      Неа, это надо возлагать на конкретные "DirRules.mk", в них максимальная гибкость.

    17.04.2012: да, введена DIR_TARGETS.

    18.04.2012: а как можно в DirRules.mk выполнять "произвольные преобразования" -- да очень просто:

    1. Указывать только список TARGET'ов и ПРАВИЛА, как, например, из v0628_drv.c сгенерить cm5307-ppc-v0628.drvlet (тут -- что-то типа cm5307-ppc-%.drvlet:%_drv.o).
    2. Если просто шаблон не прокатит, то можно генерить per-target-файлы с правилами, по аналогии с .mkr.

    13.08.2013: введена также и LOCAL_DEPENDS -- чтоб Makefile'ы могли указывать на необходимость генерации .d-файлов для локально-используемых "библиотек". Это оно на замену старым SUBFLSSOURCES и UTILSSOURCES (имевшихся только в server/drivers/*.mk), для тех ленивых директорий, что не хотят использовать более общий механизм *_COMPONENTS.

  • 17.04.2012: вообще очень неправильно, что у нас чохом удаляются все *.o: кроме местно-генеримых могут быть и сторонние файлы.

    Сейчас решение простое -- помещать их в отдельную директорию, БЕЗ Makefile, и брать для линковки оттуда. В принципе -- это даже красивее.

    А возможно ли сделать так, чтобы удалялись лишь те .o-файлы, которые оно генерит само?

    17.04.2012: неа, скорее НЕВОЗМОЖНО. Поскольку главный принцип данной системы сборки -- указываются компоненты, из которых собираются конечные файлы, а уж каким путём make их сделает -- вопрос свободный. Поэтому взять список промежуточных .o (или каких прочих подобных) -- решительно неоткуда.

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

    Так что -- "withdrawn".

  • 23.04.2012: при внедрении новой схемы ("все правила описываются в GeneralRules.mk, для модульности, а файлы-использователи лишь определяют список целей и их состав"), покамест в v2, вылезло неудобство:
    • Что иногда надо кроме списков файлов/целей описывать также и специфические правила/зависимости -- в основном, всякие симлинки.
    • И иногда эти специфичности бывают в отдельных .mk-файлах -- для той же модульности.
    • А первое встреченное правило make считает за основную цель (поэтому в старой схеме с Rules.mk почти все файлы имели в начале ".PHONY:firsttarget" и "firsttarget:all" -- избавление от чего и значится одной из целей в списке за 08-11-2005).
    • В результате пришлось такие описания (определение+правила) бить пополам, а для внешних .mk-файлов вводить двухстадийную обработку: сначала zzz_STAGE=1, include zzz.mk, потом include *Rules.mk, а далее zzz_STAGE=2, include zzz.mk. Что исключительно криво и неудобно.

    В общем -- конфликт, требующий разрешения :-)

    23.04.2012: по здравому размышлению решено было не делать избавление от ".PHONY:firsttarget" фетишем -- пусть оно будет в тех сравнительно редких makefile'ах, где требуется включение внешних хитрых правил. Это намного меньшая проблема, чем та монструозная двухстадийная схема.

    28.04.2012: собственно -- а почему в makefile'ах? Пусть уж оно тогда и будет в тех .mk-файлах, ради которых нужно!!!

  • 25.04.2012: по опыту внедрения поддержки exports/install при переводе v2 на эту систему сборки: надо менять механизм описания со статичного -- {EXPORTSDIR,EXPORTSFILES}{,2,3} -- на гибкий-модульный: чтоб был "список имён пар" ACT_EXPORTSPAIRS (равный {TOP,PRJ,DIR,ARCH,LOCAL,...}EXPORTSPAIRS), содержащий просто список слов-названий (NNN), а система пусть использует набор пар переменных NNN_EXPORTSDIR/NNN_EXPORTSFILES.

    25.04.2012: причина -- в разных местах требуются разные наборы пар ДИРЕКТОРИЯ/ФАЙЛЫ, и:

    • Надёжно "стандартно" смаппировать их на куцый список {,2,3} затруднительно.
    • Ну и просто стандартизовать вместо нумерованного фиксированный список в стиле {DIR,ARCH,LOCAL,...}EXPORTS{DIR,FILES} тоже никак, ибо в каждой из категорий может быть более одной пары ДИРЕКТОРИЯ/ФАЙЛЫ.
    • Теоретически, можно сделать очень большой набор -- {1,2,3}{DIR,ARCH,LOCAL,...}..., но это будет уж шибко монструозно (хотя если припрёт -- то как паллиатив годится).

    Как сие может быть реализовано:

    1. Для внутридеревного exports: это почти ничего не стоит -- сменить три строчки на
      $(foreach NNN, $(strip $(ACT_EXPORTSPAIRS)), ...)
      
    2. А для внедеревного install:, с алхимией _OI_..., будет посложнее:
      1. Возможно, придётся генерить include'имый make-код -- типа exports-support.mkr, имеющий зависимость аналогично .mkr-файлам.

        Но это кривовато и неприятно, т.к. тут надобность в генерении не списка зависимостей (которые синтаксически должны быть на разных строчках, что внутри-make'ным циклом не сделаешь), а просто последовательности shell-команд через ';'.

      2. А можно -- попробовать заменить статичный список _OI_{...}{1,2,3} при помощи $(call).
  • 13.05.2012@Снежинск-каземат-11: есть непонятки: судя по описанию, команда "cp -Rp" вовсе НЕ должна копировать симлинки корректно. Для симлинков официально нужен ключ "-d" (или "-P"?).

    Нынешнее поведение с "-R" появилось 30-01-2004 (см. bigfile-0001.html) ради симлинков в chlclients/, и оно выполняет задачу. Вопрос -- почему? И не переделать ли всё-таки как надо ("cp -dpR"?)?

  • 19.03.2015: в преддверии добавления поддержки кросс-компиляции все библиотеки переведены с указания 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-го.

  • 25.07.2015@пляж-утро: давно грызут мысли, что НАДО вводить "конструктор из кубиков" -- чтоб для конкретной программы указывать список модулей, должных быть влинкованными, а "конструктор" сым бы генерил файлы NNN_builtins.c.

    Собственно, мысли КАК это реализовать:

    • Списки -- в файлах PROGNAME.modlist.
    • Скрипт mkbuiltins.sh, делающий 1 проход по modlist, во время прохода собирающий в одну переменную кусок "DECLARE_type(name)", а в другую -- таблицу "&(type_MODREC_NAME(name).mr)".
    • В TopRules.mk правило:
      %_builtins.c: %.modlist
      	$(TOPDIR)/scripts/mkbuiltins.sh <$< >$@
      
    • И чтоб туда же вгенерировывались функции InitBuiltins() и TermBuiltins(), для вызова их из PROGNAME.c.

    22.11.2016: делаем, по тому проекту. Детали:

    1. Технические аспекты:
      • Лёгкой проблемой было "как организовать в shell'е цикл while(!eof)?". Решается просто: read возвращает exitcode, так что цикл свёлся к while read ....
      • Проблема посложнее -- как вставить в строку '\n'?

        Ответ оказался прост: конструкция $'СТРОКА', при этом СТРОКА может содержать \-коды.

      • Однако: в дебильном dash'е эта фича традиционно не работает.

        Так что надо будет либо

        1. указывать вместо #/bin/sh прямо сразу #/bin/bash, что есть криво, либо
        2. в *.mk вместо просто вызова ...mkbuiltins.sh указывать ${SHELL} ...mkbuiltins.sh -- чтобы учитывалось указываебельное в командной строке SHELL=...

        Лучше, наверное, второй вариант.

    2. Идеологические аспекты:
      • Синтаксис файлов *.modlist:
        • Каждая строка входного файла состоит из пары полей:
          ТИП_МОДУЛЯ ИМЯ_МОДУЛЯ
        • ТИП_МОДУЛЯ: cxsd_frontend, cxsd_dbldr, dat_plugin.
        • Пустые строки пропускаются; строки, начинающиеся с '#' -- тоже.
      • Для каждого типа модулей есть переменная с именем WAS_тип, которая изначально пустая, а при встрече такого типа выставляемая в =Y.

        И потом строки #include ... выдаются только при уставленности соответствующей WAS_тип -- так в .c-файл попадает лишь то, что реально используется.

    23.11.2016@утро-дома: по-хорошему, кроме *builtins.c надо бы еще генерить списочек файлов (компонентов для влинковывания) для make, чтобы не надо было вручную поддерживать когерентность.

    ...и еще -- надо бы и *_builtins.h тоже уметь генерить.

    23.11.2016: продолжаем:

    • Как поступим: сделаем 3 разных файла (mkbuiltins_c.sh, mkbuiltins_h.sh, mkbuiltins_m.sh), или оставим один, которому обязательным параметром указывается "что делать"?

      Из соображений инкапсуляции и недублирования явно лучше второй вариант -- ОДИН скрипт.

      Так и сделано.

      • В начале проверка наличия $1, с последующим case на валидность указаного -- -Mc, -Mh, -Mm.
      • Конкретно -Mm требует указания имени переменной, в которую делается присвоение списка файлов.
      • И в самом конце, после обработки входного файла -- 2-й case, с разным выводом в зависимости от запрошенного.
      • Что приятно: here-documents -- <<__END__ -- корректно взаимодействуют с условными конструкциями (if, case). А то были сомнения.
    • Насчёт генерации .h было некоторое сомнение: там ведь нужно иметь #define-символ для не-включения-повторно. И этот символ обычно делается из имени файла -- где его брать?

      Но тут этот файл на програму всегда один, так что выбрано статичное имя __MAIN_BUILTINS_H.

    • И более серьёзное сомнение насчёт генерации данных для Make:
      1. Указываются разные виды объектников, а БРАТЬСЯ-то они -- файлы -- откуда будут?

        Это должен будет как-то решать Makefile. Использовать VPATH?

      2. СЕЙЧАС ведь используемые cxsd модули вообще влинковываются в него не из .o-файлов, а из библиотек!

        Ну на СЕЙЧАС решение тривиально -- не использовать фичу -Mm.

        А в будущем -- возможно, надо будет повсеместно оставлять модули ТОЛЬКО в объектниках.

        ...хотя есть еще простое решение: 3-е поле в строке -- "параметры". И, например, "-" -- не добавлять в список линковки.

    24.11.2016: далее:

    • В InitBuiltins() добавлен параметр-функция "err_notifier". Это замена имевшегося cxsd'шного logline() -- он вызывается при init_mod()<0:
      • Ему передаются модулевы magicnumber и name -- чтоб можно было их выдать.
      • Он возвращает результат: ==0 -- идти дальше, !=0 -- закончить и вернуть ошибку.
      • ...кстати, отличие InitBuiltins() от старого cxsd'шного -- тут есть int-код возврата.
      • При отсутствии -- err_notifier==NULL -- просто выдаётся стандартное сообщение на stderr.
    • ...для -Mc тоже нужен параметр -- имя .h.
    • после обеда@лыжня-2км-последние-100м: если хорошенько подумать, то *builtins.h вообще ВСЕГДА ОДИНАКОВЫЙ. Следовательно -- его можно иметь вообще один стандартный,
      • прямо в src/include/;
      • генерацию его из mkbuiltins.sh убрать;
      • параметр для -Mc становится не нужен.
    • @после-лыж: сказано -- сделано.
      • Сделан include/main_builtins.h.
      • Его генерация убрана.
      • Параметр от -Mc убран.

    25.11.2016: внедряем:

    • Сначала локально в programs/server/:
      • В Makefile добавлено правило "%_builtins.c:...", ...
      • ...и сам файл объявлен как LOCAL_GNTD.
      • cxsd_builtins.h (11-02-2010!) -- уволен.
      • А вызову InitBuiltins() добавлен параметр NULL.

      Вроде работает как надо.

    • Правило "%_builtins.c:..." перенесено в TopRules.mk.

    Расширение и углубление:

    • Добавлено распознавание "категорий" cxsd_driver, cxsd_layer, cxsd_ext, cxsd_lib. Имена файлов строятся по тем же правилам, что в lib/srv/cxsd_modmgr.c.

      Не проверено пока, конечно.

    • А еще как-то надо будет "motif_knobset" сделать.
  • 06.09.2016@Снежинск-каземат-11: криво, что цель "install" никак не зависит от EXPORTSFILES*. В результате в некоторых местах если просто сказать "make install", то оно обломится по ошибке копирования.

    06.09.2016@Снежинск-каземат-11: подразобрался.

    • Проблема возникала конкретно в liu/screens/, с правилами из programs/pults/DirRules.mk.
    • Кривость была в том, что пара файлов -- генеримых копированием из .../hw4cx/screens/ -- не создавалась по "all".
    • А вот правило "exports: all" присутствует.

    Так что считаем, что всё окей, а пункт "withdrawn".

    А в том проблемном случае проставлена зависимость

    liu: panov_ubs.m4 kshd485.m4
    т.е. "бинарного" клиента (реально симлинка) от include-файлов.
  • 03.11.2016: еще энное время назад была мысль: как плохо, что в PrjRules.mk указывается абсолютная ссылка
    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
    -- позор! Исправлено.
  • 08.06.2019: правило:
    никогда, Никогда, НИКОГДА нельзя указывать имена библиотек вида "-lLIBNAME" в ЗАВИСИМОСТЯХ; а можно только в *LIBS, лишь передаваемых линкеру.

    Ниже история.

    08.06.2019: история вышла позорноватая, поучительная, но разрешилась быстро.

    • Во время очередной пересборки всего (w_mkall.sh) вдруг отказался собираться programs/runner/cx-starter.
      • Ругался так:
        /lib/libdl.so: error adding symbols: File in wrong format
        
      • И в командной строке линковки почему-то явно указывался "/lib/libdl.so".
    • Т.е.: сборка велась на x86_64 (CentOS-7, x10sae), а библиотека в командную строку как-то попала 32-битная. ...немудрено, что вылезала ошибка.
    • Почему ошибка не появлялась раньше -- ясно: эта 32-битная библиотека была инсталлирована совсем недавно, пакетом glibc-devel-2.17-157.el7.i686.rpm, поставленным 27-05-2019 для сборки чего-то 32-битного.

      (Это НЕ xfte; а что же тогда? А-а-а, это я пытался gcc-7 собрать (для ЧеблоПаши и Славы проверял простоту сборки из исходников), чтоб он и под x86 тоже был, но не вышло, так что ограничился "--disable-multilib", но RPM'ки остались.)

    • МЕХАНИЗМ подпихивания "/lib/libdl.so" был найден штудированием info по GNU make: как сказано в разделе "4.5.6 Directory Search for Link Libraries",
      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).
      
    • ПРИЧИНА после этого нашлась махом: в programs/runner/Makefile в списке ЗАВИСИМОСТЕЙ фигурировало $(LIBDL).
    • Т.е., встретив в списке зависимостей "-ldl", шибко-умный-GNU-make занялся самодеятельностью и САМ нашёл (по СВОИМ соображениям) "подходящий" .so-файл (который, конечно, был совсем НЕподходящим -- требовался /lib64/libdl.so.2, находимый линкером) и подставил его в командную строку на место "-ldl".
    • Некий вопрос в том, почему раньше (ДО появления /lib/libdl.so) всё работало?

      Видимо, не находя подходящего в обычных путях поиска, make просто оставлял указание "-ldl" нетронутым, так что оно доходило до линкера и исполнялось должным образом.

    После перетаскивания $(LIBDL) из зависимостей в SPECIFIC_LIBS всё починилось.

    Это было единственное такое косячное место (созданное, очевидно, впопыхах).

  • 10.06.2019: в системе сборки присутствует бардак с указанием "X11R6".
    • Где-то присутствует тупое "-L/usr/X11R6/lib" -- это 4cx/src/TopRules.mk::MOTIF_LIBS (и много в старом cx/src/TopRules.mk).
    • Где-то условные конструкции с выбором между "/usr/X11R6/lib64" и "/usr/X11R6/lib" -- это 4cx/src/programs/xmclients/Makefile.

      Причины этого "вудуизма" можно найти в bigfile-0001.html за 22-06-2006...26-06-2006 (там какая-то гадость в FC4) плюс за 26-01-2009.

    Выглядит это всё фиговато; надо бы оный бардак устранить -- скорее всего, в СОВРЕМЕННЫХ системах большая часть уже нафиг не нужна (хотя поддержку старых систем желательно бы сохранить -- хотя бы RH-73).

  • 23.07.2019: нужна поддержка компиляции C++'ного кода -- файлов .cpp.

    23.07.2019: делаем.

    • Основной посыл: у GCC хватает мозгов для вызова "нужного" компилятора (cc1 или cc1plus) в зависимости от расширения исходного файла. Поэтому используем для .cpp ровно такие же правила, что и для .c, а НЕ создаём отдельных "$(_CPP_O_LINE)" и "$(G++)".
    • Сначала изготавливаем поддержку "%.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), ...
      • ...а далее пары из {TOP,DIR,SHD,ARCH,PRJ,LOCAL,SPECIFIC} _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: попробовал. Пока не очень вытанцовывается.

    1. Если просто указать бинарнику, то работает; например, такое:
      tango_cdaclient: LD=$(GXXCMD)
    2. Указывать PHONY-цель в качестве ЗАВИСИМОСТИ БИНАРНИКА -- нельзя: её имя подставляется в строку линковки, и там уж линкер ругается на отсутствие такого файла.
    3. А если указывать БИНАРНИК зависимостью PHONY-цели --
      .PHONY: GXX-LINKED
      GXX-LINKED:	LD=$(GXXCMD)
      GXX-LINKED:	tango_cdaclient
      
      -- то эффект отсутствует. Очевидно, по причине отсутствия нужного "направления" зависимости, так что значение переменной, определяемой для GXX-LINKED, не пропагируется на её зависимости.

    Может, не выпендриваться, а просто пользоваться вариантом 1, работающим? Недостаток лишь в том, что во ВСЕХ использованиях придётся указывать конкретную специфику, вместо просто символьного заклинания "вот это линкуем g++".

  • 20.09.2020: ЕманоФедя очень хочет уметь получать от Makefile'а список генеримых файлов, чтобы запихивать его в .gitignore.

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

    • Переводим с просто печати на непосредственно запись в .gitignore.
    • И добавляем рекуррентный вызов.
    • Цель, конечно же, переименована в create-gitignore.
    • -Ну и сам .gitignore был добавлен к RQD_GNTDFILES*.d *.mkr).

    Пара замечаний:

    1. Что в этом всём неприятно -- тем самым система сборки берёт на себя ПОЛНУЮ ответственность за файлы .gitignore, поскольку в оных нет фичи "include другого файла" (гугля на тему "gitignore include other file" я обнаружит только крайне странный рецепт в ответе на вопрос "Can I include other .gitignore file in a .gitignore file? (like #include in c-like languages)" -- там что-то с подменой текста в шаблоне).
    2. При взгляде на пару целей "distclean" и "maintainer-clean", включая их описание в info-документации на Make, закрадывается сомнение: а правильно ЛИ я их использую?

      Ведь подготовка директории к упаковке в дистрибутив -- это именно "distclean". А я почему-то в этих целях применяю "maintainer-clean" (ещё с конца 1990-х, когда стал использовать авто-генерацию зависимостей? тогда в ucam оно было "mostlyclean"), "distclean" же по факту не используется вовсе.

      Что с этим делать -- неясно. Впрочем, и НАДО ли что-то делать -- хбз.

  • 15.04.2021: в очередной раз у очередного юзера вылезли проблемы со сборкой под Debian (точнее, Ubuntu), будь он неладен (конкретно сейчас -- Виталя Балакин на ноуте у студента/студентки).

    В принципе, способы решения проблемы описаны в 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'".

    • Но озвученная вчера Виталей проблема слышалась по-другому: что-то в файле cxsd_builtins.sh. А при его генерации никаких "echo -e" не используется.

    Так что рыть дальше.

    16.04.2021: пытаюсь рыть дальше. Для симуляции Debi[li]an поставил RPM'ку dash-0.5.10.2-1.el7.x86_64.rpm, скопировал дерево в /tmp и натравил на него "make SHELL=/bin/dash" -- бинго!!! Но легче от этого результата не стало, ибо:

    • Проблема на cxsd_builtins.sh действительно вылезла: там на концах строк стоят символы '$'.
    • А происходят они из конструкции $'\n', используемой в mkbuiltins.sh для помещения внутрь переменной символа '\n'.
    • Данный вывод, кстати, был прямым текстом записан ещё 22-11-2016 при реализации mkbuiltins.sh.
    • И вот это уже НИКАК не исправить: документированных и портабельных способов не видно.
      • В документации (man'е) данного глупо-shell'а этот вопрос обходится стороной.
      • Даже если что-то и найдётся, то совсем не факт, что оно будет совместимо с bash/zsh, и уж тем более с какими-то ещё shell'ами (хотя с ними и сейчас-то не факт, что совместимо).
    • Т.е., можно, конечно, отказаться от переводов строк в генеримых файлах -- gcc-то и так всё съест -- но тогда получится совершенно неудобочитаемый текст.
    • P.S.: кстати, возник вопрос -- а как вообще рецепт с "make SHELL=/bin/bash" работает, если в начале скрипта стоит явное "#!/bin/sh" и /bin/sh указывает на быдло-shell? Почему скрипт обрабатывается именно bash'ем, а не тем, куда симлинк указывает?

      Ответ, скорее всего, в том, что Make исполняет команды при помощи "${SHELL} -c КОМАНДА", и когда указанному shell'у попадается такой скрипт, то на заголовок "!#..." уже не смотрится, а просто делается исполнение скрипта как "своего".

    Итого, резюме: нет, фиг -- горбатого могила исправит.

  • 30.07.2022: оказывается, в Makefile'ах работает "else ifeq ..." -- работает как просто "elif". Осознано чтением info.

    (Осознано при изготовлении gw/Makefile, где конструкция "if...elif...else" понадобилась -- там нужно проверить наличие И EPICS, И TANGO, ругаясь при отсутствии, а если есть оба -- то включить сборку в epics2tango/.)

  • 23.02.2023: мелкая модификация в w_tar.sh: в командную строку tar добавлены ключики
    --owner=0 --group=0 --mode=og-w
    -- чтобы отвязаться от конкретного юзера и его настроек.
  • 04.06.2023@Томск-Беленца-6-23: к вопросу «при компиляции .c-файлов в tango/... им указывается "-std=c++11" -- как бы это обойти?» -- да элементарно:
    • изготовить ОТДЕЛЬНЫЕ "ветки" для .cpp, чтоб можно было использовать ОТДЕЛЬНЫЕ переменные, не затрагивая C'шные (но чтоб и C'шные *_CPPFLAGS туда бы тоже включались).
    • Как назвать (долго думал) -- CXX_CPPFLAGS.

    13.06.2023: делаем, все работы в GeneralRules.mk:

    1. Определена ACT_CXX_CPPFLAGS -- копированием определения ACT_CPPFLAGS и "раздваиванием" каждого компонента на "$(nnn_CPPFLAGS) $(nnn_CXX_CPPFLAGS)", аналогично тому, как в определении ACT_CXXFLAGS.
    2. В определении _CXX_O_LINE заменено $(ACT_CPPFLAGS) на $(ACT_CXX_CPPFLAGS), ...
    3. ...и в определениях _CXX_DEP_LINE тоже.

    Ну и использование -- ключ "-std=c++11" теперь указывается не в LOCAL_CPPFLAGS, а в LOCAL_CXX_CPPFLAGS -- в файлах frgn4cx/tango/cda/Makefile и frgn4cx/gw/epics2smth/Makefile (во втором расширенные копии определений из первого).

    Вот во втором результат и получен -- теперь всё красиво.

    ...столько возни ради вроде бы мелочи, и из-за идиотизма клятого Tango...

  • 05.03.2025: вылезла неожиданная проблема -- драйвер 4cx/src/programs/drivers/test_rem_rd_rw_drv.c (зачем он -- см. за 15-01-2018) содержит такой фрагмент:
    #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'ить именно его.

    Итак:

    1. Фрагмент в драйвере заменён на
      #define TRUE_DEFINE_DRIVER_H_FILE "empty_DEFINE_DRIVER.h"
      #include "cxsd_driver.h"
      
    2. В programs/drivers/Makefile добавлен кусок
      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: делаем раздел, конкретно для того, чтобы было куда разместить подраздел про Elbrus.

    Для стройности структуры ставим его первым, ПЕРЕД разделом "Кросс-компиляция".

Общие вопросы:
  • 21.02.2023: подраздел создан чисто для порядка, как резерв на будущее. Пока что в него писать нечего.
MacOS-X/Darwin:
  • 21.02.2023: собственно изначальные действия были проведены 19-03-2014 (см. bigfile-0001.html), а сейчас создаём тут формальный подраздел, чтоб в случае продолжения работ было куда записывать.
Elbrus/E2K:
  • 21.02.2023: из академического интереса появилось желание попробовать собирабельность под Эльбрус/Elbrus, который сейчас как архитектура именуется "E2K". Ради этого и создаём подраздельчик.
    29.03.2023: учитывая, как всё разрослось, делаем также level5-список, с подразделами для CX, EPICS и Tango.
CX-сборка:
  • 21.02.2023: приступаем. Собственно, начальная малая часть информации была получена ещё в субботу 11-02-2023, когда потрогал вживую парочку Эльбрусов.

    21.02.2023: предварительное -- сбор информации.

    • Ещё тогда посмотрел, как утилита uname показывает архитектуру/процессор: "uname -p" говорит "e2k".

      ...тогда ключ "-p" нашёл чтением man'а, т.к. на память не помнил.

    • Сейчас же стал смотреть насчёт правильного ключика и:
      • В Sysdepflags.sh используется "-m" для всех ОС, кроме SOLARIS (но НЕ SUNOS!) и IRIX -- для тех "-p".
      • Гуглением на тему «uname "-p" or "-m"» нашёл на Reddit обсуждение "How are "uname -m" and "uname -p" different?" от июня 2022, где утверждается, что

        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 output unknown. 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 of uname -m from the uname syscall. For uname -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" второй же ссылкой дало страницу "Идентификаторы процессоров -- База знаний сообщества разработчиков Эльбрус" с таблицей, утверждающей, что практически повсеместно "uname -m" скажет "e2k".
    • Рытьём по коду "диагностирования и настройки" в системе сборки понял, что если нет никаких специфичных для типа процессора вещей (а их в CX нет нигде, кроме проверки на не-CPU_MCF5200 для dlopen() и vsnprintf() и CPU_X86 для ассемблерных вставок ndbp_image_elemplugin.c), то
      • Нигде ничего делать не нужно, кроме как...
      • ...в самом Sysdepflags.sh, где надо выставить 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.

    • С первым ещё буду возиться, ...
    • ...а вот tango-9.2.5a -- сначала "./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
      
    • (откуда взялась дата 2008-01-23 -- загадка: реально стоит 2017 год)

      12.03.2023: нашёл, откуда: это в config/config.guess есть строка timestamp='2008-01-23'.

    • После найденного гуглением по "configure unable to guess system type" обсуждения "How to resolve configure guessing build type failure? / configure error: cannot guess build type you must specify one" указал "./configure --build=e2k-unknown-linux-gnu" и получил уже
      configure: error: Package requirements (omniORB4 >= 4.1.2) were not met:
      
      No package 'omniORB4' found
      
      (имеющийся какой-то другой "orbit-2.0" её не устроил). До проверки на ZMQ/0mq дело так и не дошло, хотя как раз это-то вроде присутствует.

    05.03.2023: дальнейшие обсуждения сборки EPICS и Tango пусть будут в их собственных пунктах ниже.

    P.S. И да -- это пусть всё будет ЗДЕСЬ, в одном общем разделе по Эльбрусу (а не в их собственных подразделах), чтобы вся информация была собрана в одном месте.

    29.03.2023: только "пункты" преобразованы в подразделы свежевведённого level5-списка.
  • 17.03.2023: раз уж собрались EPICS и Tango, то чё б не попробовать собрать и frgn4cx/?

    17.03.2023: пробуем:

    • epics/ оно пропустило с диагностикой
      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 с гарантией облома.

    • А вот с tango/ получилось прямо "катастрофичнее" :D -- при генерации cda_d_tango.dep выдало
      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.
      
      -- ну тоже понятно: этот клятый omniORB был собран локально, а оно его пытается искать в стандартных системных директориях.

    20.03.2023: возвращаемся к этой задаче.

    1. Tango -- начинаем с него, как с более простого.
      • Тут ясно, что надо в каких-то параметрах подсунуть ссылки на локальные include/ и lib/.
      • Список возможных *_{CPPFLAGS,CFLAGS,CXXFLAGS,...} в GeneralRules.mk весьма велик, поэтому главным вопросом было -- какую "категорию" выбрать?

        Логически была выбрана ARCH_ -- она как раз для такого и была предусмотрена 23-01-2012.

      • Сработала команда (0m27.784s на единственный cda_d_tango.cpp!!! 22.03.2023: перепроверил: 0m30,321s на чисто компиляцию (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 не упоминается вовсе)).

    2. EPICS -- поскольку ещё 17-03-2023 попытка твикнуть frgn4cx/epics/FrgnRules.mk обломилась (см. в его разделе за ту дату), то пробуем также обойтись уставкой параметров make'у из командной строки.

      И уж тут-то всё тривиально -- просто подменяем пару определений из FrgnRules.mk:

      make EPICS_BASE_DIR=${HOME}/compile/base-3.15.9 EPICS_OS_LIB_DIR='$(EPICS_BASE_DIR)/lib/linux-e2k'
EPICS-сборка:
  • 05.03.2023: насчёт сборки EPICS.

    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: разбираемся дальше.

    • Взгляд на текст EpicsHostArch.pl показывает, что он получает от PERL'а строку в формате "ARCH-OS" ("e2k-linux"), а наружу выдаёт её в обратном порядке "OS-ARCH" (EPICS требует "linux-e2k").
    • Есть ДВА скрипта с набором слов "linux", "x86_64" и "Common" в именах:
      1. CONFIG.linux-x86_64.Common -- "Definitions for linux-x86_64 host builds".

        include'ит CONFIG.UnixCommon.Common и более не содержит ничего.

      2. CONFIG.Common.linux-x86_64 -- "Definitions for linux-x86_64 target builds"

        include'ит CONFIG.Common.linuxCommon и добавляет "-m64" к ...CFLAGS и ...LDFLAGS.

    Пользуемся полученным знанием:

    • Создаём в configure/os файл CONFIG.linux-e2k.Common (хостовый!) просто копией из CONFIG.linux-x86_64.Common.
    • Первая попытка холостой сборки обламывается:
      $ 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
      
    • Но оно аналогично обламывается и под просто linux-x86_64 -- это косяк самой системы сборки: она предполагает, что делает директорию O.linux-x86_64 и потом входит в неё, но из-за "-n" директория не создаётся и входить некуда.
    • Поэтому запускаем просто "make EPICS_HOST_ARCH=linux-e2k", безо всяких "-n".
    • Вторая попытка сборки обломилась по совсем непонятной причине: оно в какой-то момент пыталось исполнить ??????????????????, а того не было.
    • После чего был создан также и CONFIG.Common.linux-e2k также просто копией из CONFIG.Common.linux-x86_64.
    • И оно собралось!!!

    Итак, "рецепт" --

    1. Создать в configure/os файлы:
      • CONFIG.linux-e2k.linux-e2k копией из CONFIG.linux-x86.linux-x86
      • CONFIG.linux-e2k.Common копией из CONFIG.linux-x86_64.Common
      • CONFIG.Common.linux-e2k копией из CONFIG.Common.linux-x86_64
    2. Собирать командой "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: решил за компанию попробовать сборку и EPICS7 -- того же 7.0.2.2, что и в 2019-м.

    11.03.2023: итак:

    • Сравнил задействованные компоненты системы сборки (те файлы *x86_64*, что копировал в *e2k*) -- они совпадают с милипусечным отличием в CONFIG.Common.linux-x86_64, которое вряд ли роляет.
    • Поэтому просто воспроизвёл действия от 3.15.9 -- фиг, облом, причём первое замеченное отклонение от нормального поведение -- на вызове perl-скрипта.
    • Возникло подозрение, что дело может быть в версии perl -- там v5.30.3, а на x10sae -- v5.16.3.
    • Попробовал также более свежий 7.0.7 (в надежде, что там адаптировались) -- тоже фиг.
    • Собственно проблема проявляется в modules/libcom/src/O.linux-e2k при обращении к скрипту bldEnvData.pl -- он вызывается как
      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
      
      • Поиск строки "bldEnvData.pl" по всем исходникам показывает, что командная строка его вызова определяется в modules/libcom/src/env/RULES, и там фрагмент выглядит как "-c $(CMPLR_CLASS) -s $(OS_CLASS)" -- т.е., пустой оказывается $(CMPLR_CLASS).
      • А поиск ЕЁ определений показывает кучу файлов. И в configure/os/ к x86/x86_64 ничего не относится, зато в configure/CONFIG.gnuCommon есть строка "CMPLR_CLASS = gcc".

        ...или он что -- НЕ включается в сборку на e2k?

      Откуда возникают вопросы:

      1. Почему в 3.15.9 это проблемой НЕ является.
      2. Почему под "настоящим" x86_64 тоже всё работает, а в "скопированном" в e2k -- сбоит.
    • Затем пришла в голову идея, как решить данную проблему БЕЗ разбирательства с её причинами -- просто исправить симптомы: указать CMPLR_CLASS=gcc в командной строке.
    • Указал -- та ошибка исчезла (хотя она и так игнорировалась -- хбз почему).
    • Но фатальной стали косяки уже следующих (причём, сами эти ошибки почему-то игнорируются) -- при сборке фиготы под названием "antelope" происходит вызов пары каких-то обкорнанных-с-начала команд, вроде
      D_GNU_SOURCE -D_DEFAULT_SOURCE           -D_X86_64_  -DUNIX  -Dlinux .........
      
      и
      o antelope  -L/export/home/bolkhov/compile/base-7.0.2.2/lib/linux-e2k .........
      
      -- их отсутствие почему-то не вызывает останова! -- и потом оно пытается использовать несуществующий файл antelope, но тут уж make отваливает по отсутствию зависимости.

    15.03.2023: полез разбираться поплотнее:

    • В поддиректории base-7.0.2.2/modules/libcom/src/O.linux-e2k/
    • делаем "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)
      
    • ...и ищем 243 и 207 в configure/RULES_BUILD -- соответственно
      %$(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) в базе НИГДЕ НЕ определена.
    • Реально вопросов 4:
      1. Почему при неопределённом $(CCC) это неопределённое также съедает и СЛЕДУЮЩИЙ СИМВОЛ из строки?

        Так что "$(CCC) -o ..." дает "o ..."?

        И аналогично от "-DGNU_SOURCE" остаётся "DGNU_SOURCE".

      2. Почему ошибка исполнения команды игнорируется.
      3. В т.ч. от "perl -CSD .../bldEnvData.pl"?
      4. Почему $(CCC) не определена?

    Пытаемся разобраться:

    1. Может, это "order-only rules"?

      (Раз в правиле линковки есть "|" (в "info make" искать по "`\|'")).

    2. "grep -w CCC" показывает, что НЕзакомментированное определение есть в configure/CONFIG.gnuCommon:
      CCC = $(GNU_BIN)/$(CMPLR_PREFIX)g++$(CMPLR_SUFFIX)
      

      ...да вот только он, судя по "make -p --debug=vj", в списке включаемых файлов ОТСУТСТВУЕТ! Почему?

    Чуть позже: сообразил насчёт пп. 1 и 2 -- это не разные, это ОДНА И ТА ЖЕ ПРОБЛЕМА!!!

    • Ведь если первый символ в командной строке -- '-', то make считает это за префикс "игнорировать ошибку команды". ...Естественно, сам префикс из команды удаляется.
    • Вот и получается, что
      1. "-o ..." и "-DGNU_SOURCE ..." превращаются в "o ..." и "DGNU_SOURCE ..." соответственно,
      2. а ошибки исполнения этих "команд" игнорируются
      -- такой весёлый эффект два-в-одном-флаконе при неопределённости первого макроса в командной строке.
    • Так что пытаться гуглить на тему "make eats first character after undefined macro" было абсолютно бесполезно и бессмысленно :)

    Таким образом:

    • Корень проблем -- именно не-определённость $(CCC).
    • Но это всё равно не объясняет п.3 -- там-то вроде бы никакого префикса "дефис" нету.

      ...хотя и является причиной ошибки -- там ведь тоже дело в определении из того же configure/CONFIG.gnuCommon и его невключением.

    Явно надо разбираться со списком include'имых файлов --

    • благо, получить его от "make --debug=v" можно.
    • Кстати, этот список надо сравнить с аналогичным от base-3.15 -- там это "antelope" тоже есть, а сборка всего подряд идёт также в одной директории (только там это src/libCom/O.linux-*, а не modules/libcom/src/O.linux-* -- перекомпоновали и об-lowercase'или).

    Да, сравнивал -- и в конце концов обнаружил, что косяк МОЙ: конкретно в 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. Теперь надо будет как-то проверить работу.

  • 24.03.2023: попробовал также собрать EPICS под "e2k_128", сделав в configure/os/ файлы *e2k_128* (конкретно в CONFIG.Common.linux-e2k_128 сделано -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
      
      • Сравнением с логами от успешно собравшейся 64-битной версии (путём пачки последовательных sed'ов к каждому логу отличия были устранены и логи приведены к годному для diff'а виду) было понято, что проблема в почему-то не сработавшей командной строке
        [--путь-убран--]/base-3.15.9.BACKUP/bin/linux-e2k_128/genApps ../O.Common/gddApps.h
        
        -- вроде запускается, но файла ../O.Common/gddApps.h почему-то не генерит.
      • Запуском "вручную" было увидено, что программа SIGSEGV'ится, $?=139 (139=128+11).

        Почему make при этом не вылетает по ошибке -- загадка (ведь что-то такое уже было).

      • Как бы то ни было: после прогона под GDB вылезло ровно то, ради чего и заморачивались с "защищённым режимом":
        Thread 1 "genApps" received signal SIGSEGV, Segmentation fault
        
        exc_array_bounds at 0x5017e0e8 ALS2 | ALS5
        
        0x000000005017e0e8 in gddBounds1D::operator new(unsigned long) ()
        
      • ...однако не шибко-то много диагностики получается -- т.к. флаг "-g" при сборке отсутствует.
      • Выяснилось, что за "отлаживаемость" отвечает make-параметр/переменная $(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: кажется, нашёл:

        • в configure/CONFIG_COMMON есть определение 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))
          
        • ну а в конкретных CONFIG.* определения OPT_nnn_YES и OPT_nnn_NO -- например, в configure/CONFIG.gnuCommon присутствует парочка
          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(). Мраки...

      • Потом захотел проверить: насколько проблемный код -- содержимое src/ca/legacy/gdd/ -- изменился между 3.15.9 и 7.0.7; попытался сделать diff -Naurb -- и осознал, что в EPICS7 этого просто НЕТ: оттуда весь libCAS выселен в отдельный "модуль".
      • Из чего следует очевидная мысль: надо попробовать собрать именно 7.0.7 -- в нём сборка пройдёт дальше, поскольку этой точки просто нет.

    26.03.2023: OK, пробуем 7.0.7, с твиком osdElfFindAddr.c и configure/os/-файлами.

    • Прособиралось аж 15 минут и срубилось на
      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'
      
      -- ну это косяк явно уже самой системы, в которой нету 128-битной версии этого "libncursesw".
      • Ну пробуем собрать ncurses-5.9.tar.gz из ncurses-5.9-13.20130511.el7.src.rpm (игнорируя патчи) от CentOS-7.3.
      • Просто "./configure" там традиционно не сработало -- "error: cannot guess build type; you must specify one" (не сюрприз).
      • Так что делаем
        ./configure --build=e2k-unknown-linux-gnu --prefix=${HOME}/usr128 CPPFLAGS=-m128 LDFLAGS=-m128
        
      • ...но занадобился ещё GPM.
        • Берём gpm-1.20.7.tar.xz из gpm-1.20.7-5.el7.src.rpm оттуда же, также без патчей.
        • Сначала там надо запустить "./autogen.sh", он сгенерит (11 секунд!!!) всё для configure'а, а потом уже обычным образом.
        • ...но опять фиг: при линковке не нашлось major(). А это явно что-то касательно ядра.
        • Поэтому патчи таки применил -- там как раз был gpm-1.20.7-sysmacros.patch для этого.
        • Ту-то проблему решил, но дальше полезло ещё.
        • Так что взял gpm-1.20.7-17.el8.src.rpm от CentOS-8, с бОльшим количеством патчей.
        • Вот с ними уже собралось (аж за 1 минуту ровно :D).
      • Но также, поскольку нужно именно "ncursesw", а не "ncurses", занадобился ключик "--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-библиотеки.
      • Собиралось от 7m28s до ~10m, в зависимости от ключей.
    • Но всё это не помогло (несмотря даже на указание флажков "-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: кхм...

    • Вчера, засыпая, обдумывал ситуацию и пришёл к выводу, что учитывая отсутствие каких-либо упоминаний ncursesw в логе сборки -- на /usr/lib ссылается какой-то из файлов в /usr/lib128/.
    • Сегодня нашёл (проверил ldd 64-битную версию этого makeBpt) -- readline:
      ##$ 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: хоцца всё-таки довести до конца, поэтому продолжаем.

    • Поскольку Багзилла МЦСТ'шная посторонних не пускает даже посмотреть, то "отрепортил" баг в телеграм-канал; возможно, передадут ответственным.
    • А чтоб не ждать -- решил просто собрать свой экземпляр readline.
    • В процессе чего обнаружил в readline-6.2-9.el7.src.rpm специальный патч readline-6.2-shlib.patch, убирающий ключик "-rpath" при сборке .so'шки. Вероятно, под Эльбрус собиралось без этого патча, вот и результат.
    • Собственно попытка сборки обломилась: оно жаждет "-ltinfo", но не находит его.

      Как показал поиск под CentOS-7.3, libtinfo -- часть ncurses.

    30.03.2023: продолжаем.

    • Как показал поиск "find /lib* /usr/lib* -iname '*tinfo*'" -- там вообще нигде ни в каком виде нет libtinfo.so*; как так?! Отключено при сборке?
    • И в логе от configure есть вот такое:
      checking for tgetent... no
      checking for tgetent in -ltermcap... no
      checking for tgetent in -ltinfo... no
      checking for tgetent in -lcurses... yes
      
      -- так нафига оно пытается сделать "-ltinfo"?!
    • grep "по всему" показал, что это как раз readline-6.2-shlib.patch добавляет -- в нём есть строчка
      + SHLIB_LIBS='-ltinfo'
      применяемая к файлу support/shobj-conf -- видимо, RedHat'овские игрища...
    • Закомментировал эту строчку -- собралось!!! (общее время сборки -- 1m26.526s)
    • Итого, алгоритм сборки readline следующий:
      1. Развернуть .tar.gz;
      2. наложить все патчи (первый из которых -- readline62-001 -- делается с -p0 вместо обычного -p1);
      3. закомментить строку "SHLIBS='-ltinfo'" в support/shobj-conf; 03.04.2023: неа, сделать так:

        в support/shobj-conf заменить "SHLIBS='-ltinfo'" на "SHLIBS='-lncursesw'";

      4. ./configure --build=e2k-unknown-linux-gnu --prefix=${HOME}/usr128 \
            CPPFLAGS="-m128 -I${HOME}/usr128/include" LDFLAGS="-m128 -L${HOME}/usr128/lib"
        
      5. make; make install

    Но легче от этого не стало -- срубается на линковке по пачке "undefined reference"...

    Что делать? Напрашиваются 2 варианта:

    1. Попробовать пересобрать "свой" ncurses ещё разок более аккуратно -- возможно, тогда в цейтноте где-то накосячил.

      (Хотя ещё отдельный вопрос: а не подхватывается ли всё же что-то системное, что несовместимо по режимам сборки?)

    2. Попробовать таки исключить использование readline -- то, что не удалось в 2019-м.

    Начинаем со второго.

    1. Рытьё в configure/os/ (я ж теперь опытнее!) подсказало, что надо указать 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

    2. 01.04.2023: пересобрал, во всём разобрался -- проблема была не столько в сборке, сколько в кривых исходниках связки readline+ncurses: readline плохо указывал, что ему нужно (слабоопределённо с "-lncurses", а строки "ncursesw" нет вообще нигде), а у ncurses бардак с ncurses/ncursesw/tinfo.

    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"
      

      Что вызвало подозрения, т.к.:

      1. В одном из указаний "/usr/include" вместо "/usr128/include".
      2. Для СИСТЕМНЫХ директорий вроде надо не "-I", а "-isystem" (упомянуто в bigfile-0001.html за 28-06-2004 и 29-06-2004).
    • "-I${HOME}/usr128/include" вместо "-I${HOME}/usr/include" не помогло.
    • Для сборки с включенным readline -- тоже нет.
    • "-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: некоторые мысли, пришедшие в голову чуть позже на основе полученной ранее информации:

    1. @вечер, засыпая: теперь, с пониманием надобности ключиков "-lreadline -lncursesw", вспомнилось виденное grep'еньем по configure/os/ --

      у параметра COMMANDLINE_LIBRARY есть ведь вариант "READLINE_NCURSES", дающий именно нужный набор ключей!!! (В тех редких местах, где используется)

    2. 02.04.2023@утро, просыпаясь: насчёт ситуации "отсутствует в libreadline.so указание на зависимость от libncursesw.so" --

      а не в том ли дело, что из CentOS'ной спецификации было убрано "-ltinfo"? Ведь надо было не убирать её, а заменить на "-lncursesw"!

    Если эти соображения верны, то заработать должно с ЛЮБЫМ из этих двух вариантов: хоть с "правильно" собранным libreadline, хоть с указанием

    COMMANDLINE_LIBRARY=READLINE_NCURSESW LDLIBS_READLINE_NCURSESW="-lreadline -lncursesw"
    (да-да, вариант "LDLIBS_READLINE_NCURSESW" есть штатно в CONFIG.Common.cygwin-x86)

    03.04.2023: проверяем: да, катят оба варианта!

    1. Принудительное указание _NCURSESW в COMMANDLINE_LIBRARY -- да, спокойно доходит до "makeBpt: protected mode runtime error".
    2. Указание "SHLIBS='-lncursesw'" в support/shobj-conf: сработало! Тоже спокойно дошло до косяка makeBpt.

      (Правда, по "ldd libreadline.so.6.2" вылазит ошибка "libncursesw.so.5 => not found", но это из-за отсутствия "-rpath", что нестрашно -- в EPICS он делается.)

    Остановимся, пожалуй, на 2-м варианте -- поскольку он максимально похож на обычную сборку.

    04.04.2023: возвращаемся к разбору собственно облома makeBpt.

    • То, что показывает ссылки на исходник:строчку не всех точек в backtrace -- видимо, особенность Эльбруса: такое впечатление, что часть является какими-то "точками перехода", генерируемыми компилятором, но к исходному тексту впрямую не относящимися, а потому показывается, что адрес в libCom.so.3.22.0 (в который влинкован и результат osdMutex.c).
    • В конце концов добрался до "первопричины" -- modules/libcom/src/osi/os/posix/osdMutex.c::globalAttrInit(), конкретно строчка
      status = pthread_mutex_init(&temp, &globalAttrRecursive);
      (и да -- файл существенно отличается даже между 7.0.7 и 7.0.2.2, не говоря уж о 3.15.*, где потроха категорически другие.)
    • Поскольку выглядит всё вроде бы прилично, то я предположил, что где-то что-то недоинициализировано.

      Добавил зануление посредством memset(,0,) и temp, и globalAttrRecursive -- не помогло.

    • После чего накидал простенький тест test_pthread_mutex.c:
      #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)
    • После этого makeBpt отработал без падений!
    • ...и далее всё собралось.

    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
      
      -- работает.
    • Обычным 64-битным:
      ./compile/base-3.15.9/bin/linux-e2k/camonitor ovch300_a40_0.adc0
      
      -- работает.
    • 128-битным 3.15.9 (оказывается, базовые тулзы там собрались -- ca/client/tools делается прямо перед ca/legacy/gdd):
      ./ptr128/base-3.15.9.BACKUP/bin/linux-e2k_128/camonitor ovch300_a40_0.adc0
      
      -- тоже работает!
    • 128-битным 7.0.7:
      ./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
      
      -- ничего не падает.
    • ...а *quad и uc* просто игнорируются на уровне cxsd_fe_epics: quad/INT64 в cxsd_fe_epics.c::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
      
      мда....
    • ...правда, реальных МАССИВОВ там нет -- симуляция же. Надо запускать сервер с суперсимуляцией и писать конкретные значения.
    • OK -- делаем, запускаем сервер с "-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
      
      -- тоже ничего не падает.
    • (Ну да, в uchar и uctext ничего не пишется из-за несовместимости по типам, а натравленный на сервер с УЖЕ записанным "ABCD" camonitor показывает в скалярах нули (а векторы и вовсе не кажет) и только при записи во время него запущенного обновления показывают верные данные, но то какой-то давний косяк cxsd_fe_epics+libcas. ...а cainfo не кажет вообще ничего; вот поштучно -- пожалуйста, а пачкой -- чё-то у него слетает...)

      23.04.2023: не, про "в скалярах нули" -- это было неверным наблюдением, из-за свежезапущенного сервера, которому в скаляры ничего не было записано: ведь запись векторов в скаляры не срабатывает, она отсеивается в cxsd_hw.c::IsCompatible(). Если же сначала записать скаляры (а не только векторы) и потом запустить camonitor, то показываются записанные значения.

    16.04.2023@утро-душ: что дальше можно делать:

    1. Проверить бы -- а вдруг при сборке 3.15.9, обломившейся на GDD'шном genApps, всё-таки что-то успело собраться (конкретно libcas+libgdd), как успели собраться libca+caget/caput/camonitor? Тогда можно б было проверить 128-битный cxsd_fe_epics & Co.

      Посмотрел -- неа, фиг: обламывается прямо ПЕРЕД генерацией libgdd.a и libgdd.so.3.15.9, увы...

    2. Чуть в сторону: раз в EPICS7 есть cadef.h и libca.* -- может, попробовать собрать cda_d_epics с ними хотя бы на x86_64?

      Задача выглядит несложной -- подсунуть другие параметры make'у, вроде

      EPICS_BASE_DIR=${HOME}/compile/base-7.0.2.2

      @после-обеда: да, сработало, собралось. Проверять надо, но пока особо незачем.

    3. И если получится, то тогда уже и 128-битный вариант попробовать, параметрами вроде
      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
      
      -- ну и тоже работает, во всех проверенных вариантах типов.
    4. ОТДЕЛЬНО -- надо б понять, какого чёрта команды, срубающиеся по ошибке защищённого режима, НЕ вызывают остановки make (т.е., НЕ считаются ошибкой).

      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'ами.

    5. И ещё надо б разобраться, почему получение командой SIGSEGV'а НЕ приводит к удалению созданного/тронутого ею файла, как вроде бы должно.

      @вечером: "разобрался" -- см. 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 -- фиг, результат тот же.
    • Более того, логи от сборки с параметром =1 и =0 -- СОВПАДАЮТ (с точностью до имён директорий).
    • А не попробовать ли конкретно ЭТОТ шаг -- "genApps ../O.Common/gddApps.h" -- выполнить с помощью обычной 64-битной утилиты (ровно как для Tango был использован приём кросс-компиляции)?

      22.04.2023: даже проверять не стал, т.к. ТОЧНО получится: эти файлы всегда одинаковые, с md5sum=56bde7d82fdc9e2e05614b5baac3d5c7, так что можно вообще просто скопировать из 64-битной директории.

    19.04.2023:

    • Напихал отладочных печатей указателей посредством нашего print_ptr() внутрь newdel_setNext(), определяемой в макросе gdd_NEWDEL_FUNC() (см. ниже его обсуждение).

      Видно, что оно,

      • аллокировав некий кусок, от которого первым виден дескриптор x=0x000000a00000000018000000518f23d0 (т.е. размер 0x000000a0),
      • потихоньку прёт по нему всё вверх и вверх,
      • и в конце концов пытается записать по адресу-дескриптору pfld=0x000000a00000009818000000518f23d0 указатель (т.е., 16 байт).
      • Но смещение-то уже 0x98, а 0xa0-0x98=8, т.е., 16-байтный указатель не влезет, вот и получаем ошибку переполнения.
    • Откуда это всё берётся: похоже, какой-то свой "шибко умный" менеджмент памяти, сделанный криворуко. Конкретно:
      • src/ca/legacy/gdd/gddUtils.h -- определение класса 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;
        };
        
        
      • src/ca/legacy/gdd/aitTypes.h содержит определение
        typedef aitUint32           aitIndex;
        
        т.е., размер gddBounds -- 8 байт.
      • src/ca/legacy/gdd/gddNewDel.h -- определения макросов 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; }
        
      • src/ca/legacy/gdd/gddNewDel.h же определяет и реализацию этих 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));" -- явным образом кастится ОТРИЦАТЕЛЬНОЕ целое к указателю! Фантазёр без тормозов, блин...

      • Итого:
        1. Этот г-н Jim Kowalkowski догадался использовать под хранение УКАЗАТЕЛЯ места в некоей структуре, которая НЕ ИМЕЕТ НИКАКИХ гарантий того, что её размер будет достаточным.

          Нет бы использовать какой-нибудь union с полем вроде void * -- неа, просто надеется, что "ну влезет же". (Ага, оно и влазило вплоть до 64-битных указателей включительно, но о гарантиях со стороны языка автор не позаботился, хотя это было несложно.)

        2. Отдельно доставляет то, что всё-всё сделано МАКРОСАМИ -- в результате оно совершенно неудобоотлаживабельно, поскольку GDB не может дать ссылки на "реальную строку" исходного кода (как бы?), а даёт указание на строку, в которой стоит макрос gdd_NEWDEL_FUNC().

          И, замечу, это в якобы C++, где к тому времени было и множественное наследование, и template'ы.

          Т.е., по факту тут даже не то что не плюсовый код, а такой, что и в обычном C был бы крив (из-за внахаловку записи указателя на место пары int32).

    • Что делать?
      • Добавлять лишнее поле, заведомо достаточного размера?
      • Вручную добивать структуру до размера указателя -- вроде "char padding[sizeof(void*) - ]"
      • А может, какой-нибудь структуре сделать __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'е?

    • А-а-а, нет: там НЕ было сделано "make exports", так что в compile/work/4cx/exports оставался древний бинарник (ровно 1 месяц назад -- от 23 марта 08:12). После "make exports" проблема ушла.
    • Собственно тесты -- тот же набор, что 15-04-2023: сначала с devlist-canhw-11.lst и потом с devlist-test-alltypes.lst (сначала просто так, потом с сервером в режиме "-S" и записью из третьей консоли).

      С аналогичным эффектом.

    • Но сервер таки словил SIGILL по "exc_diag_ct_cond" (кстати, его же и 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. И да -- на обычных архитектурах тот косяк приводит к мусору, а в защищённом режиме ещё и к вылету по причине неинициализированных данных.

Tango-сборка:
  • 12.03.2023: насчёт сборки Tango.

    12.03.2023: давно напрашивалось -- раз фиг знает, где взять содержимое пакета omniORB для yukari, то просто собрать его из исходников.

    • Берём omniORB-4.2.0.tar.bz2 из omniORB-4.2.0-3.el7.src.rpm (штатный от CentOS-7.3?).
    • Пытаемся его сконфигурить как
      ./configure --prefix=${HOME}/usr --disable-static
      -- фиг, вылазит ошибка, аналогичная TANGO'вской, только дата уже
      config.guess timestamp = 2001-08-21
      
    • OK -- пробуем рецепт
      ./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.

    • Троица zeromq-4.1.4.tar.gz с libpgm-5.2.122~dfsg.tar.gz libsodium-1.0.18.tar.gz взяты из zeromq-4.1.4-6.el7.src.rpm с openpgm-5.2.122-2.el7.src.rpm и libsodium-1.0.18-1.el7.src.rpm соответственно.
    • Конфигурирование:
      • libsodium собрался просто так. 06.04.2023: ага, потому, что у него config.sub и config.guess упоминается "e2k" (и ещё у gpm-1.20.7).
      • libsodium ещё были указаны ключи "--disable-silent-rules --disable-opt" (подсмотрено в его .spec-файле).
      • Для openpgm (который собирается аж в openpgm/pgm/) таки понадобилось "--build=e2k-unknown-linux-gnu" (там дата config.guess значилась за 2012-02-10), как и для zeromq (2013-06-10).
      • zeromq -- у него нет ключей для указания "pgm и sodium искать там-то", а либо через переменную окружения $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'
        
        • Попробовал было указать ещё и make'у эти "sodium_CFLAGS=...", но не помогло. И не зря -- как потом показал поиск, эти определения есть в Makefile, сгенерённом конфигурацией.
        • Засада в том, что из-за прятанья командной строки не видно конкретных обстоятельств проблемы.
        • Поиском по Makefile (искал строку "verb") предположил, что этим аспектом управляет переменная 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'у параметры никак не сказались на командной строке!!!

        • А так -- да, всё "undefined" расположено как раз в libsodium.so.
        • И вручную явное указание той командной строке -L/export/home/bolkhov/usr/lib -lsodium проблему решает.
        • OK, попробовал другой вариант конфигурации, с указанием не только "-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" присутствует совсем в других местах.

        • Отдельно попробовал вариант с ОТКЛЮЧЕНИЕМ этих sodium и pgm --
          ./configure --prefix=${HOME}/usr2 --build=e2k-unknown-linux-gnu --with-libsodium=no --with-pgm=no
          
          -- и тоже вроде прокатило. Собралось за 8m49.190s. И TANGO с ним тоже собралось, за 148m10.734s.
      • ...а ещё ZMQ использует libtool -- та ещё дрянь... См. "Autotools Considered Harmful".

        Реально-то именно с ним и были связаны наши проблемы: autoconf, automake, libtool и связанный с ними pkg-config -- это части "GNU Autotools", изначально именовавшейся "GNU build system".

    • Собралось -- libsodium 13m14.668s, libpgm -- 2m21.673s, zeromq -- 9m13.233s.
    • И само tango-9.2.5a тоже собралось, 150m17.506s.

    Теперь надо будет проверять работоспособность. Та ещё задачка...

  • 05.04.2023: пробуем собрать в 128-битном "защищённом" режиме.

    05.04.2023: начинаем сразу с omniORB (и сразу облом):

    • Конфигурируем посредством
      ./configure --prefix=${HOME}/usr128 --disable-static --build=e2k-unknown-linux-gnu CPPFLAGS=-m128 LDFLAGS=-m128
    • Срубилось:
      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"
         ^
      
      -- ошибка сразу понятная и даже вполне предсказуемая (чисто из размеров: при 128-битном указателе нет ни одного штатного/обычного целочесленного типа того же размера).
    • Там вот такой кусочек кода:
      //
      // 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 именно для арифметики указателей и предназначен (и какого лешего эта библиотека что-то своё изобретает?!).
    • Но дальше вылезла другая проблема:
      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')
      
      -- оно собирает какую-то .so'шку и пытается её загрузить из python'ского кода, но python-то в системе 64-битный.

      (Сама библиотека в порядке, ссылается на /lib128/ и /usr/lib128/ -- ldd подтвердил.)

    • Шо, собрать python под защищённый режим и подсунуть его? (А чё -- ЕманоФедя таки локальные сборки+установки делал тучу раз.)
    • Ха -- погуглив "tango controls cross-compilation", первой же ссылкой получил "How to cross compile omniORB" с рецептом, весьма отвратным.

      ...мож правда проще Python собрать? :D

    Пробуем вырулить ХОТЬ КАК-ТО...

    • Попробовал попробовать "рецепт" про кросс-компиляцию.

      Его смысл в том, чтобы ПЕРЕД общей сборкой конкретно 3 директории собрать ОТДЕЛЬНО, под ХОСТ-платформу; тогда общая сборка их собирать уже не будет (ибо по timestamp'ам всё свежее), а .so-модуль будет совместим с python'ом.

      Сделать я это попробовал чуть иначе: если в "рецепте" предлагается указывать make'у другой компилятор (CC=...), то я попробовал указывать другие CPPFLAGS и LDFLAGS --

      CPPFLAGS=-m64 LDFLAGS=-m64

      Так вот -- фиг: если "./configure"'у указываются именно специфичные флаги, то на этапе make к ним добавлена уже туча всякого, включая "-I...", так что с моим указанием оно резко обломилось по ненайденности своих include-файлов.

    • Подумал-подумал я, и изобрёл такой вариант (вдохновлённый "рецептом"):
      1. Заводим в своём ~/bin/ скрипты g++64.sh, g++128.sh, gcc64.sh, gcc128.sh которые могут использоваться именно как "компиляторы под указанную архитектуру", БЕЗ надобности указания ключей. Например, gcc128.sh:
        #!/bin/sh
        gcc -m128 $*
        
      2. И указываем "./configure"'у 128-битные версии, а make'у при сборке тех 3 директорий -- 64-битные.
    • Итого, цепочка такая (после подправления файла):
      1. Конфигурирование:
        ./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
        
      2. Сборка тех 3 директорий:
        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/
        
      3. Сборка всего остального: просто 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.

    • ...и ещё то же самое в wstringtypes.h -- третий...
    • ...и cdrMemoryStream.cc (тут уже внутри функции, а не определение типа).

    После этого всего -- omniORB собрался!

    Далее:

    • libsodium:
      ./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.

    • libpgm:
      cd openpgm/pgm
      ./configure --prefix=${HOME}/usr128 --build=e2k_128-unknown-linux-gnu CPPFLAGS=-m128 LDFLAGS=-m128
      make AM_DEFAULT_VERBOSITY=1
      
    • zeromq:
      ./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];
        
      • При этом определение zmq_msg_t в include/zmq.h
        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.

    Сначала пробуем собрать свежую версию -- вдруг там что-то изменилось (такой-то кривой код могли и исправить) и проблема решена?

    • Это 4.3.4, взятая из zeromq-4.3.4-2.el9.src.rpm, внутри там файл теперь libzmq-4.3.4.tar.gz ("lib"!) и патчей нет вообще никаких.
    • Попробовал: сначала там теперь нужно "./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'ом свою документацию, а потом...

    • ...облом более странный:
      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
      
      (да-да, номера строк в тамошних Makefile'ах особенно доставляют -- клятый libtool...)

    Так что забиваем на новую версию и возвращаемся к 4.1.4.

    • Внимательное вчитывание в код показало следующую картину:
      • Используются ДВЕ структуры данных, которые обе должны занимать 64 байта:
        1. zmq_msg_t -- "заглушка" для внешнего кода;
        2. zmq::msg_t -- реальная структура с данными.

          Они для одного и того же куска памяти, но снаружи видна "заглушка", а внутри -- реальное содержимое.

          (~16:00, дорога из Эдема домой, пересекая Николаева: чё не использовали собственную аллокацию с отдачей наружу handle'ов (как в cxscheduler) -- загадка.)

      • Проблема -- с "реальной" структурой, которая вручную подогнана так, чтобы занимать те же 64 байта, а на e2k_128 эта подгонка почему-то ломается.

        (~16:00, дорога из Эдема домой, пересекая Николаева: почему не использовали средства языка -- тоже загадка.)

      • "Суть" подгонки в том, что:
        1. первым ВСЕГДА вне union'а идёт поле file_desc, сделанное 64-битным для того, чтобы следующий далее во всех union-вариантах
        2. указатель * metadata не требовал бы padding'а от компилятора;
        3. за ним -- "вариабельное" поле unused, чей размер и вычисляется диковатым образом, дающим сбой при 128-битности;
        4. а в конце -- 8-битные поля type и flags.
        5. (Мелкое отличие только в union-варианте vsm: перед последней парой есть ещё 8-битное size -- для того случая размер max_vsm_size.)

        Т.е., они пытаются сделать так, чтобы:

        • у всех вариантов было фиксированное начало (file_desc,metadata)...
        • ...и фиксированное ОКОНЧАНИЕ type,flags,
        • с возможностью включать доп.поля вроде size,
        • а серединка unused "растягивалась" бы.
      • Корень проблемы --
        • что они сделали file_desc 64-битным в предположении, что указатели не более 8 байт,
        • а 128-битные-то -- 16 байт и требуют соответствующего выравнивания и компилятор вставляет 8 байт padding'а, которые в их расчётах никак не учитываются,
      • вследствие чего размер zmq::msg_t становится 64+8=72 байта, что вызывает несовпадения в "проверке" посредством zmq_msg_size_check[].
    • Напрашивающийся фикс -- в 128-битном режиме вставлять эти 8 байт padding'а "вручную" и учитывать их в вычислениях.
    • И да, анализ требуемого содержимого показал, что и в этом случае оно всё влезет в 64 байта.
    • Сделано -- код модифицирован следующим образом (добавленное -- bold):
      #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'
      
    • Ну это-то выглядит уже просто косяком системы сборки (дурацкий libtool, да), который предположительно проще победить.

    08.04.2023: пытаемся побороть "-nostdlib"...

    • Рытьё показало, что и "-nostdlib", и все те пути просто ЗАХАРДКОЖЕНЫ в /usr/bin/libtool -- оно этом монструозном скрипте (>10000 строк!) зашито в определениях archive_cmds с archive_expsym_cmds и predep_objects с postdep_objects; причём там захардкожена и ссылка на директорию конкретной версии компилятора -- кстати, в CentOS-7.3 аналогично, зашито 4.8.5.

      Как эта хрень должна использоваться в случае кросс-компиляции или хотя бы наличии нескольких вариантов локальной архитектуры (32- и 64-битного) -- загадка.

    • Читал-читал всякое, в т.ч. на тему "libtool 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.

    • Итого, последовательность сборки (после распаковки):
      1. Подправить src/msg.hpp;
      2. Конфигурирование (выше эта же команда уже есть):
        ./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"
        
      3. Подправление:
        sed -i -e 's|lib64|lib128|g' ./libtool
      4. Сборка:
        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;

      ...а-а-а, нашёл: на Эльбрусе этот же кусок имеет вид

      /* 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__  */
      
      -- т.е., именно pointer...
    • Порылся поглубже: в tango'вской инфраструктуре (у них собственный "слой" работы с thread'ами) везде предполагается, что идентификатор нити -- целочисленный; и даже более того, они повсеместно нарушают правило "не применяйте операции == и != для сравнения идентификаторов, а пользуйтесь pthread_equal()" -- используют именно операторы числового сравнения.

      ...с одной стороны, это лишний камень в огород multithreading'а: наверняка этот "слой" был заведён для возможности работы с разными реализациями.

      С другой -- это опять характеризует "качество" Tango-кода: для них же, козлов, был введён специальный тип pthread_t, но нет, вместо его использования наколхозили long'ов.

      И что-то мне подсказывает, что репортить им это как баг бессмысленно -- просто не поймут...

    01.08.2024: подумавши "а не заслать ли всё же в Tango-forum вопрос насчёт какого чёрта они считают pthread_t целочисленным?", решил поискать, какими стандартами определяется, каким должен быть pthread_t:

  • 14.04.2023: НЕ СЮДА -- про frgn4cx/python3

    Ещё некоторое время назад начал делать специальный драйвер для возможности исполнять в CX-сервере python'ский код. Вот это -- о нём.

    14.04.2023: первоначальные потребности и соображения:

    • Главная хотелка была у ЕманоФеди: чтоб те "сервисы", что сейчас занимаются вычислениями на основе данных аппаратуры (например, с пикапов) и публикуют их в noop-каналы для всеобщего доступа, можно б было интегрировать в сервер -- дабы результаты вычислений публиковались бы в правильных readonly-каналах.
    • Ориентация строго на 3-й Python.
    • Предполагается, что будет грузиться указанный .py-модуль, из которого будут вызываться некие функции с заранее предопределёнными именами.
    • Первоначальная идея драйвера -- чтобы он мониторил указанные (в auxinfo) каналы и при их обновлении дёргал бы метод "произведи вычисления"; и да -- дёргать при обновлении КАЖДОГО из каналов, так что если исходных каналов 10 штук, то обновление дёрнется 8 раз.
    • Чуть позже стало очевидно, что можно реализовать полноценную поддержку ВСЕГО ДРАЙВЕРА на python -- т.е., чтоб все драйверовы методы были бы переходниками, вызывающими функции из модулей, а возврат данных делался бы каким-нибудь вызовом, передающим информацию в ReturnDataSet().
    • Передача данных, когда будет реализована полноценно -- на механизме "MemoryView".

    "Процесс реализации":

    • Расположение:
      • Первоначальный вариант был сделан в sw4cx/python3_drivers/, а потом оно переехало в frgn4cx/python3/, конкретно в поддиректорию drivers/;
      • ...одной из причин переезда явилось то, что в прочих поддиректориях frgn4cx/'а уже есть условная сборка -- при наличии требуемых компонентов;
      • вот тут тоже проверяется наличие команды python3-config, и проверка реализована способом, подсмотренным в "info make" по строке поиска "$(PATH".
    • Указание "какой модуль грузить" сначала думалось делать в auxinfo; затем пришло осознание "а пусть будет как в remdrv -- указывать ИМЯ_PYTHON_МОДУЛЯ/python3"; такой вариант и элегантнее (и имя парсить не надо :D), и позволяет легко менять C-драйверы на PY.
    • Сделана функция ParseRefSpec() -- точнее, взят соответствующий кусок кода из trig_read_drv.c::trig_read_init_d() и обобщён (а так-то, кстати, он же есть и в mirror_drv.c).

      (Поскольку предполагалось, что надо будет регистрировать cda-каналы.)

    Некоторые дополнительные соображения:

    • (сегодня) а ведь "возможность писать CX-драйверы на python" -- это весьма громкая штука; можно даже статью на эту тему сделать.
    • А есть ли что-то подобное в EPICS? (В Tango-то есть возможность ВЕСЬ device server сделать питонским, а в EPICS точно есть какие-то приблуды ("pvapy"?), но спектр их мне неясен.

      ЧеблоПаша не знает.

      Случайно в 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.

      Так что -- ДА, есть, именно в том виде, о котором думается: возможность использовать ОТДЕЛЬНЫЕ змеиные драйверы (а не весь сервер).

Сборка под -m128:
  • 29.03.2023: тут будет про нюансы сборки под "128-битный режим" на примере CX.

    А по EPICS и Tango -- пункты в их подразделах.

    Проблемы-то похожие, но у EPICS и Tango из-за их зависимостей куча своих нюансов, а главное -- баги, которые предполагается найти посредством "защищённого режима", уже их собственные специфичные.

  • 17.03.2023: насчёт ключика "-m128", он же "-mptr128", который как бы не 128-битный режим, а просто "128-битные указатели", но при этом реально адрес там 64-битный, а остальные 2 по 32 бита используются для хранения размера блока памяти и смещения в нём -- это т.н. "защищённый режим".

    Так вот: захотелось с этой штукой поиграться -- чисто из интереса. Для описания этих игрищ и раздел.

    Источники информации:

    17.03.2023: "формат" указателя описан в документации так:

    • указатель на начало выделенной области памяти/объекта (64 бита);
    • размер выделенной памяти/объекта (32 бита);
    • смещение относительно начала (32 бита).

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

    • Для этого была написана простенькая программа, печатающая числовые значения несколько указателей: на int-массив, на 2-й элемент массива, на malloc()'нутый кусок памяти, на функцию.
    • Запустил её -- фиг: выдаются крохотные значения, явно не более чем 8-байтные.
    • Что наводило на 2 предположения: либо всё описанное не работает, либо служебная часть указателей просто не печатается.
    • Так что был написан расширенный вариант программы test_m128.c, печатающий числовое значение указателей "вручную":
      #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, только она не младшей части указателя, а в средней. Т.е., арифметика указателей работает весьма неортодоксально.
      • Сравнение чисел с /proc/`pidof test_m128/maps (закомменчено тут выше) показало, что код на указанных местах, как и статические данные (массив a[]), а вот malloc()'нутого -- не видно.
      • Попытка аллокировать даже не >4GB, а хотя бы 3 -- 3*(size_t)0x40000000 -- привела к печати (на STDOUT!) сообщения
        ERROR: trying to malloc 3221225472 (0xc0000000) bytes
        malloc cannot allocate over 2**31 bytes (2Gb) in protected mode
        
        (да, безграмотновато -- "Gb" вместо "GB")

        Сообщение, кстати, некорректное: реально не "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).
    • И-и-и... Сразу же ругательство
      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
      
      -- и отвал через SIGSEGV!!!

      Попытка "перепрыгнуть" через эту ошибку далее и собрать, для простоты (нет никаких .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:" (см. в соседнем разделе за сегодня).

    • Логика подсказывала, что явно надо ещё как-то указать ЛИНКЕРУ на "128-битность".

      Хотя в "man ld" про такое ничего нет (только какое-то "-m emulation"), но, коль скоро в качестве линкера используется сам gcc, то уж он-то должен знать, что скормить реальному линкеру.

    • Поэтому командная строка дополнена указанием "TOP_LDFLAGS=-m128" -- и вот это помогло.
    • ...но с libXm.so вышел облом: она имеется только в /usr/lib64/ (линкер хватает /usr/lib/libXm.so, а /usr/lib->lib64), и в результате
      /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.
      • Пробуем под GDB -- там чуть информативнее,
        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 ()
        
        но деталей не показывает, ибо "No symbol table info available." (?!?!?!).
      • Окей -- проверяем посредством "ppf4td_test m4::/dev/null" -- тоже падает. Ну это по крайней мере уже изучабельно путём напихивания отладочных печатей в ppf4td_pipe_open().
      • Путём перемещения отладочной диагностики буквально по 1 строчке обнаружил точку падения: строка "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 байтам -- это "за пределами" просто попадало в неиспользуемую область и потому всё молча работало.

      Что ж -- действительно помогает этот "защищённый режим"!!!

    • Пробуем дальше -- запускаем cxsd с конфигом devlist-canhw-11.lst; тоже падает, на этот раз в 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() из программки выше,

      • проблемное имя -- QLB3n4.Iset_cur
      • передаваемое CxsdDbResolveName()'у значение name=0x000001f4000000001800c2deffdc2e80; т.е., длина 0x1f4 -- это targetbuf[500] (0x1f4=500) у ParseOneCpoint() (вызывальщик);
      • непосредственно перед SIGILL'ом dot_p=0x000001f40000000f1800c2deffdc2e80 (смещение 0xF -- это 15, длина строки "QLB3n4.Iset_cur"),
      • а dot_p+1=0x000001f4000000101800c2deffdc2e80 -- предсказуемо 0x10=16,
      • и почему следующее прямо за этой диагностикой
        fprintf(stderr, "dot_p[1]=0x%02x ", dot_p[1]);
        вызывает SIGILL -- неясно: вроде бы 0x10 находится вполне в пределах размера 0x1f4...

      Как будто "защищённый режим" как-то знает, что после терминирующего '\0', т.е. по смещению 16, уже ничего не было записано и потому запрещает обращения туда.

      Чуть позже: проверил, поставив перед чтением (на котором SIGILL'илось) запись туда же -- и да, падать перестало!!!

    На этом (пока?) вроде падения закончились.

  • 24.03.2023: захотелось получше разобраться с тем, что дигностируются обращения к неинициализированной памяти (даже когда в размер сегмента укладывается).

    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;
    }
    

    Результат тестирования:

    • При указании числа 12 -- SIGILL; а 11 -- почему-то нет; печатает 10.
    • Если в буфер писАть больше данных, то сваливается позже; и, такое впечатление, что есть "кратность 4": когда в буфере 13 байт, то 14-й и 15-й проблем не вызовут, а вот обращение к 16-му -- да.
      • Может, реально есть у памяти тэги, по одному на 32 бита, где может значиться "неинициализировано"?
      • (Это могло б быть ответом на то, почему вчерашний баг в CxsdDbResolveName() проявлялся не всегда, а лишь на конкретном имени: если длина имени плюс NUL кратна 4, то тогда обращение "после" даст ошибку, а иначе нет.)
    • Что любопытно, из GDB читается без проблем -- просто нули.
    • Ещё прикол с GDB: в программку было добавлено
      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-битная часть указателя с прибавленным значением смещения.

    • ...и /proc/PID/maps показывают диапазоны аналогично -- БЕЗ "1800", например у этой программки (обращаем внимание на [stack]) --
      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" -- падает.

    С новополученным пониманием перечитал документ "Режим Безопасных Вычислений" -- и там есть кое-что, ставшее яснее:

    • В 11.1: "контролируется использование неинициализированных переменных" -- было неясно, как такое возможно.
    • В 11.2: "Дескриптор в ТБВ имеет размер 128 бит, выровнен по размеру и подтверждён аппаратными тэгами".
    • В 11.3: "Аппаратные тэги - добавочные биты информации, которые хранятся отдельно от основных данных и используются для разметки дополнительных свойств ТБВ. Для размещения тэгов «рядом» с данными в оперативной памяти используется часть битов ECC.".

    24.03.2023: пробуем также собрать EPICS под "e2k_128" -- записываем это в его подразделе раздела по E2K.

  • 28.04.2023: оказывается, E2K-128 -- не единственная такая архитектура с "аппаратной защитой от переполнения буфера за счёт «широких указателей»".

    Погуглив "128 bit pointer", наткнулся страничку "We may end up with 128-bit pointers even without a "true" 128-bit architecture. ..." с упоминанием 3 вещей:

    • Capability Hardware Enhanced RISC Instructions (CHERI) -- модель очень похожая на эльбрусовскую, но разработанная под РАЗНЫЕ архитектуры: судя по документу "Capability Hardware Enhanced RISC Instructions: CHERI Instruction-Set Architecture (Version 8)" от October 2020, там присутствуют "64-bit MIPS" плюс "CHERI-RISC-V, CHERI-x86-64, and Arm Morello".

      (Комментарий с исходной страницы -- "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++".)

    • "AS/400 architecture has been using 128bit pointers for several decades by now, even if underneath it involves 32bit or 64bit cpus - except for a tiny bit of code provided by IBM, you're not allowed to target them directly."
    • "And ARM is working with the CHERI team to make a hardware prototype SOC (not FPGA-based like the MIPS CHERI research devices)" -- со ссылкой на "Morello Program one year on: A step closer to securing our digital future" от October 29, 2020.
aarch64:
  • 17.03.2023: поскольку известно, что по размерам типов данных ARM64, именуемый AArch64, идентичен x86_64, то возникло желание слабать его поддержку хотя бы просто в системе сборки -- да, в основном опять из академического интереса, но раз уж у нас для СКИФа есть SOM'ы Variscite, то почему бы и нет.

    Создаём раздел.

    Название выбрано именно "aarch64", а не "arm64", по результатам чтения "источников" --

    • "Differences between arm64 and aarch64" на StackOverflow от 06-08-2015 (там присутствуют ссылки на происхождение обоих вариантов и описание, почему остался один и не самый интуитивный).
    -- откуда следует, что оно повсеместно, в т.ч. в uname, именуется "aarch64".
  • 17.03.2023: собственно действия -- тривиальны: в Sysdepflags.sh добавил в конец альтернативы определения процессора вариант
    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: создаём инфраструктуру для кросс-компиляции -- в work/x-compile/.

    06.05.2011: базовая идея --

    Средства кросс-компиляции должны быть полностью отделены от какой-то конкретной подсистемы, и даже от самого "CX" вообще.

    Они могут только быть в курсе инфраструктуры "GeneralRules.mk".

    Так что --

    • Делаем директорию work/x-compile/, населяемую поддиректориями с кросс-средствами для конкретных платформ.
    • В каждой из тех директорий будут:
      1. Собственно кросс-средства (компиляторы/линкеры/..., библиотеки, include).
      2. Файлик X-Rules.mk, предназначенный для include'нья соответствующим Makefile'ом или *.mk, и содержащий кастомизации make-среды для конкретной cross-target.
    • Учитывая, что теоретически target-платформы могут быть разными и множественными, в именовании директорий будем придерживаться схемы "ПРОЦЕССОР-ОС". Сейчас ОС всегда будет "linux".
    • Сейчас там заведены поддиректории:
      • mcf5200-linux -- только для CM5307 с mcf5200/m68k/uClinux (в основном -- legacy).
      • ppc860-linux -- для CM5307/PPC, CANGW, и, видимо, для BIVME2.
    • Вскорости добавится еще что-то для Moxa.
    • Покамест всё -- БЕЗ X-Rules.mk, их разработка еще впереди.
  • 18.10.2019: надо бы приподпеределать систему кросс-сборки в hw4cx/ так, чтобы при недоступности x-compile/ соответствующие директории бы просто ОТКЛЮЧАЛИСЬ (как это сейчас сделано с drivers/daqmx/ и в frgn4cx/{epics,tango}/), а не происходила бы ошибка компиляции.

    22.10.2019: в frgn4cx/ спасает то, что директории epics/ и tango/ ДВУХуровневые: описания "хитрых правил" лежат в самих директориях и решения принимаются там же, а собираемое содержимое лежит в поддиректориях */cda/, которые при надобности выключаются из сборки.

    Впрочем, drivers/daqmx/ вполне себе одноуровневая.

    Собственно, какие директории требуют "особого обращения" -- условного отключения:

    • x-build/ -- внутри парочка ppc860-linux/ и arm-linux/.

      И в x-build/Makefile УЖЕ есть проверки: на тему ":$(OS):$(CPU_X86_COMPAT):" и ":$(OS):$(CPU_X86_COMPAT):$(OS_GE_2_6_18):" соответственно.

      Ну так и добавить ВТОРЫЕ стадии проверок: что если MAY_BUILD_nnn, то проверять также наличие файлов-"канареек", и если их нету -- то выдавать warning и делать MAY_BUILD_nnn=NO

    • А остальное уже в drivers/:
      • camac/cm5307_ppc_drvlets/ -- проверка в camac/Makefile.
      • vme/bivme2/
      • can/c4l-cangw/
      • serial/moxa -- проверка в serial/Makefile.
      • 23.10.2019: еще cpci/ -- они зависят от наличия uspci.h.
      ...и там можно использовать тот же подход.

    Весь вопрос лишь в том, как бы добывать имя файла-"канарейки", чтобы без дублирования. Т.к. местами будет по ДВА условия, а имена директорий в разных *.mk с определениями могут перекрываться.

    23.10.2019: начато, пока С ДУБЛИРОВАНИЕМ, на x-build/ppc-860.

    Проверка делается в 2 стадии: сначала первая (старая), на "теоретическую" возможность -- что платформа+ОС "та", а затем вторая (новая), на "практическую" возможность -- что факл компилятора физически доступен.

    Работает.

    24.10.2019: продолжаем:

    • Добавляем аналогичное для x-build/arm-linux. Тоже работает.
    • А потом смотрим на получившееся безобразие -- и переделываем: с 2 последовательных стадий (во второй из которых лишний тест на тему ":$(MAY_BUILD_nnn):"=":YES:") на двухуровневые вложенные условия.

      И да -- это тоже работает, а места занимает меньше и выглядит читабельнее.

    • После чего и остальная толпа тоже сделана, в основном копированием из x-build/Makefile с минимальной адаптацией имён целевых директорий и текстов $(warning)'ов.
    • ...только с cpci/ пришлось отдельно повозиться -- перетащить PRJDIR=.. в самое начало файла.

    Итого -- вроде готово.

    Хотя на тему не-дублирования всех определений *_CANARY надо ещё подумать, так что пока "done" не ставим.

mcf5200-linux:
  • 06.05.2011: создана, населена копией содержимого cx/tools/cm5307/.
ppc860-linux:
  • 06.05.2011: создана, населена копией содержимого cx/tools/cm5307-ppc/.
  • 29.11.2019: в tools/lib/ добавлен libdl.so* -- libdl.so->libdl.so.2->libdl-2.3.1.so (без первого, не-версионированного, линковка не работала).
  • 05.06.2023@Томск-Беленца-6-23, вечер: надо под PPC860 собрать utils, server и запилить их работу под big-endian (плюс локально драйверы c4l, camac, bivme2).

    06.06.2023@Томск-Беленца-6-23, утро: неа, CAMAC низзя -- там API работы с LAM такой, что годится только для одно-LAM'ных программ (присылается сигнал и номер LAM'а подразумевается), а для нескольких LAM в одном процессе оно вообще никак не годится.

arm-linux_1.3:
  • 23.05.2012: создана, уже с нуля, путём творческого вытаскивания файлов из стандартного SDK.

    23.05.2012: детали:

    1. Основной способ выискивания нужных файлов стандартный -- прогон с обычным SDK, а потом взятие тех, чей access-time изменился.
    2. Плюс -- пара симлинков на файлы в lib/: ld-linux.so.2->ld-2.2.5.so и libc.so.6->libc-2.2.5.so.

      Это было сделано после первого прогона, по ругательствам: дополнение к п.1 -- ls -ltuL **/*(@).

    3. И еще -- три симлинка на директории в arm-linux/sys-include/: asm->asm-arm, asm-arm/arch->arch-moxacpu, asm-arm/proc->proc-armv.

      Это по access-time выяснить уже невозможно (т.к., не конечные файлы, а компоненты путей), потому было найдено явочным порядком -- по ругательствам, с последующим find . -name ИМЯ-ПРОБЛЕМЫ -ls по исходному SDK.

    4. Не обошлось без традиционных для кросс-сред ложек дёгтя:
      • Вся КОМПИЛЯЦИЯ прекрасно работает даже под RH-7.3.

        А сборка -- нет, поскольку конкретно lib/gcc-lib/arm-linux/3.3.2/collect2 (единственный!) требует от /lib/i686/libc.so.6 символа/version GLIBC_2.3, которого там нет (максимум 2.2.4).

        На RHEL5 всё пашет нормально.

      • После вытаскивания из SDK уже arm-linux/bin/ld стал придуриваться: в упор не желал находить ld-linux.so.2, с такой диагностикой:
        ...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)
        
        (все переносы строк мои).

        И вот тут началась традиционная ж:

        1. Ключ -B -- нифига не помогал.
        2. Попытка понять, что же ему надо, при помощи strace -f -s200, с фильтрацией по open|stat|access -- нифига не дала.

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

        • Проблему решил им же посоветованный ключ -rpath-link, уже знакомый по PPC/CANGW (см. bigfile-0001.html за 19-01-2007).

          Но с чего он вообще занадобился, когда в полном SDK всё и так работало -- загадка.

        • Возможно, именно какой-то файл не был скопирован из SDK, но чё-то он даже через strace не нашёлся.
  • 29.11.2019: в lib/ добавлен libdl.so.2, плюс симлинк на него libdl.so.

    Поскольку SDK посеян, то файл был взят просто с живого контроллера из его /lib/.

???:
Поддержка Android NDK:
  • 05.02.2020: создаём раздел немного задним числом.

    Располагаем его сразу после кросс-компиляции (ибо это по сути вариант оной) и ПЕРЕД более ранним "pults".

  • 31.01.2020: делаем некоторую поддержку для сборки при помощи Android NDK -- ЕманоФедя попросил.

    31.01.2020: мини-протокол:

    • Работа -- прямо в основном дереве в директории jni/ (так NDK требует).
    • Там должны быть файлы Application.mk и Android.mk. Первый определяет некие настройки target-системы и не меняется, а вот второй -- содержит списки для сборки, на языке Make с некоторыми специализированными соглашениями о синтаксисе, чтобы прямо в одном файле могла указываться сборка кучи библиотек одинаковым образом.
    • Вот автоматизация генерации Android.mk и сделана.
      • В файле Android.mk.source лежит набор дуплетов
        ИМЯ_ДИРЕКТОРИИ ИМЯ_БИБЛИОТЕКИ
      • а скрипт mk-Android.mk.sh из них уже генерит готовый файл.

      Скрипт "тупой" -- читает с STDIN и пишет на STDOUT.

      ...по-хорошему, туда бы ещё Makefile для автоматического запуска генерации, но неясно, как NDK отнесётся к наличию такого файла.

    • Ну и для работы этого хозяйства в lib/{cda,cxlib}/ списки компонентов библиотек были вытащены в отдельные файлы libNNN.a_COMPONENTS.mk
    • Плюс, в корневой Makefile добавлено
    LOCAL_BCKPFILES=jni/*~
pults/:
  • 15.06.2016: создаём раздельчик. Вряд ли он будет большим -- задача несложная, но отдельная.
  • 15.06.2016@душ-после-пляжа: надо делать симлинки скринов, примерно как оно делалось в v2'шном programs/chlclients/DirRules.mk. Т.к. надоело уже и у себя запускать клиентов вручную "~/work/4cx/.../pult ПУТЬ_К_.subsys", и на пультовых машинах симлинки делать руками.

    Главный вопрос -- откуда брать информацию о target'е симлинка?

    1. Ввести опциональный файл-таблицу с дуплетами "ПАТТЕРН|target", который парсить awk'ом?
    2. Или прямо в .subsys-файлах позволить директиву "clientprog NNN", которую Cdr бы игнорировал, а DirRules вытаскивал бы этот NNN и использовал как symlink target?
    3. А может, вообще делать .subsys-файлам "chmod +x", а у них в начале писать "#!~/4pult/bin/NNN"?

    30.06.2016@пляж: в конце концов, сделать по-простому (вариант (а)): файл-таблица LIST_OF_PULTS.lst, содержащий дуплеты вида

    ШАБЛОН_ИМЕНИ:БИНАРНЫЙ_КЛИЕНТ

    Чтоб шаблон мог содержать "*", для указания клиентов группам экранов; а если подходящей строчки не нашлось (или вообще LIST_OF_PULTS.lst отсутствует) -- то используется умолчание "pult".

    Как парсить -- да, вопрос: получается как бы "инверсный grep", т.к. паттерн в файле, а фиксированный как раз образец, с которым матчинг. Наверное, действительно awk'ом.

    01.07.2016: приступаем потихоньку.

    • Чтобы не было пересечений по первой букве с "server", сама директория названа pults/, и раздельчик тоже переименован.
    • Начат programs/pults/DirRules.mk.

    05.07.2016: да!!! Получилось!!!

    Основное сделано вчера, а добито сегодня.

    Главная проблема была в том, как сделать "инверсный grep". Делалось на awk'е.

    • В отличие от проекта за 01-07-2016, разделитель не ':', а пробел -- т.к. в awk'е он является особым случаем, соответствуя произвольному количеству whitespace'а.
    • Обвязка сделана быстро. Идея такова:
      1. сначала (в BEGIN) переменной CLIENT присваивается умолчательное значение -- "pult";
      2. затем идётся по файлу, и если строчка подходит, то CLIENT'у переприсваивается значение $2;
      3. в конце (END) печатается текущее значение CLIENT.

      Таким образом убиваем всех зайцев сразу:

      • С отсутствием совпадений всё ОК -- берётся умолчание.
      • И с множественными совпадениями проблем нет -- печатается всегда 1 вариант.
      • И даже более того -- берётся ПОСЛЕДНИЙ вариант. Тем самым, можно в файле сначала поместить общий дефолт -- с ключом "*", а потом уточнения для разных вариантов.
    • А дальше начались проблемы; точнее, тернистый путь, в конце концов приведший к успеху.
      • Матчинг вида
        ИМЯ_КЛИЕНТА ~ $2
        почему-то не желал работать.

        Сейчас-то, задним числом, ясно, что где-то что-то указывалось "не так" (а "как" надо -- видно через пункт ниже).

      • Была идея, что не работает указание $2 справа от ~, и потому -- надо сначала присвоить $2 переменной, а потом справа указать её.

        Началась эпопея с попыткой сделать compound statement. Просто ";" не катит -- это разделитель РАЗНЫХ правил, последовательно применяемых к строке. Обычная запятая "," в C'шном стиле тоже не работает (возможно, потому, что это в awk'е разделитель частей "диапазона", которые работают с группами строк и потому к построчному неприменимы).

        Поэтому в конце концов удалось с вариантом

        (P=$$1) && ("$(strip $1)" ~ P)
        -- сначала присвоение (оно при !=0 даёт истину), а потом сравнение; ($strip) -- чтоб обрезать начальный пробел от аргумента функции (иначе не матчится; оно нужно в любом случае).
      • Но потом, увидев где-то пример, было сделано так:
        "$(strip $1)" ~ $$1
        -- и это работает!!!

        Т.е., видимо:

        1. Строка слева должна быть в кавычках (иначе это слово воспринимается как имя переменной, которая -- за отсутствием -- пуста).
        2. Паттерн справа должен быть просто $1 -- БЕЗ кавычек и БЕЗ слэшей (НЕ /$1/).
    • Дальше был сделан единственный пока почти пустой клиент -- knobtest.subsys, изготовлен свой Makefile и директория добавлена в список сборки.

    Засим почитаем за "done".

???:
Система сборки в sw4cx:
  • 19.02.2021: пора создавать раздел -- назрело уже и перезрело.
  • 19.02.2021: позавчера ЕманоФедя возмутился (в очередной раз?), что инсталляция сваливает ВСЁ ВЕЗДЕ -- в частности, что на ВЭПП-4 попадают ВЭПП-5'шные скрины и конфиги. И попросил как-нибудь сделать, чтобы была сегментация -- чтоб можно было ставить не всё, а лишь нужную часть (а уж в исходниках пусть всё лежит хоть вместе).

    Подумал я подумал -- напрашивается решение:

    • Сделать переменную SW4CX_PARTS, содержащую space-separated-список "подпроектов", по умолчанию -- vepp5 vepp4 liu weld.
    • При надобности поставить не всё -- в командной строке указывается нужная часть, например, SW4CX_PARTS=vepp4, что по правилам Make'а имеет приоритет перед внутрифайловым значением.

      ...а при надобности "ничего" -- вообще SW4CX_PARTS=none (это сработает по причине указанного далее механизма функционирования).

    • В местах же, где составляются списки файлов для инсталляции -- сейчас это configs/ и screens/ -- эти списки будут определяться 2-уровнево: по-подпроектные списки типа vepp4_CLIENTS, собираемые вместе конструкциями вида
      CLIENTS=$(sort $(foreach P, $(PARTS), $($(P)_CLIENTS)) )
    • Поскольку где-то могут встречаться дубли, то $(sort ...) необходима.

    19.02.2021: сделал первую итерацию --

    • В PrjRules.mk добавлено определение SW4CX_PARTS.
    • В screens/ и configs/ в Makefile сделана "инфраструктура" с раздельным указанием списков *_CLIENTS и *_SRVHOSTS/*_CLNHOSTS соответственно.
    • НО: главной проблемой оказалось РАЗДЕЛИТЬ -- что должно отойти каким под-проектам. Если в configs/ ещё более-менее ясно (просто по именам хостов), то в screens/ -- не особо: как разделить ВЭПП-4 и ВЭПП-5?

      (Вот со сваркой и ЛИУ -- weld и liu -- проблем не возникло.)

    • Ну и в configs/ реально "есть нюансы" -- похоже, там надо вводить ещё категории списков, т.к. некоторое количество файлов для ВЭПП-5 добавляются явочным порядком поштучно: вся $(ACCESSGROUPS) и cx-starter-setup-term-kls.sh, да и canhw_CAN_LINES_LIST.m4 тоже.
    • ЗАМЕЧАНИЕ: всё пока что только в тех 2 директориях; а, например,
      1. в xmclients/ -- нет, хотя там-то уж точно weldclient и liuclient подпроекто-специфичны.
      2. И в drivers/ тоже нет, поскольку исходные vdev-драйверы источников всё же общие.

        Хотя уж там-то при желании можно разделить без особых проблем -- чисто по принадлежности к специфическим установкам (кстати, даже проще, чем в конфигах).

  • 19.02.2021: обнаружилось, что V4HDIR=$(PRJDIR)/../hw4cx указывается в куче мест раздельно -- в {drivers,screens,xmclients}/Makefile. А должно бы быть единое прямо в PrjRules.mk (как это, кстати, в v2'шных подпроектах было).

    Сделано -- перенёс в PrjRules.mk.

???:
???:
???:
Документация:
  • 16.11.2006: директория src/doc/ добавлена. И туда скопирован style.css из CX.
  • 19.07.2009: зарегистрировал сайт http://cxs.sf.net/. "CXS" -- потому, что у них там "unix name" должно быть 3-15 символов; а хотел-то вообще cx.sf.net :-).

    Наполнение пока отсутствует, только сделал Trove-категорирование.

  • 24.08.2009: создал файл cxsd-devlist.hml с описанием формата конфиг-файлов сервера. Источником послужила последняя версия соответствующего манускрипта из e-mail переписки с "519".

    17.05.2015: еще энное время назад было закомментировано упоминание о SMP4TD, а сегодня добавлено упоминание m4.

    Кроме того, из описаний channels и devtype убраны ":ХОЗ.АПП.КАН" и всё связанное с ними.

    19.05.2015: и еще убран параметр "ТИП" из раздела cpoint (ума не приложу -- о чём я думал, когда его туда вводил?).

  • 01.11.2016: потихоньку наполняем 4cx/src/doc/.

    01.11.2016: часть файлов touch'нута еще 12-10-2016 (но пока пусты), а сегодня делаем реальное наполнение.

    • SOURCES.ru.html -- описания директорий с исходниками (4cx/, hw4cx/, sw4cx/). Дописан до худо-бедно полного варианта; очень кратко по самому 4cx/src/, но там лучше файлы сделать.
    • BUILD.ru.html -- очень краткая заготовка про систему сборки.

    02.11.2016: пишем дальше:

    • Наполнен DIRECTORIES.ru.html. Очень кратко и тезисно -- просто структура дерева с комментариями, но, вроде, должно быть достаточно.
    • Донаполнен BUILD.ru.html, остались неописанными только некоторые аспекты.

      И правила создания своих Makefile'ов там отсутствуют как класс.

    03.11.2016: продолжаем:

    • BUILD.ru.html дописан, теперь там и "аспекты" освещены, и про OPTIMIZATION/DEBUGGABLE/PROFILABLE написано, и восхваление фич присутствует.
    • Начат lib/vdev.ru.html.

    07.11.2016: чуть-чуть:

    • Начат lib/cxsd_driver.ru.html; пока только введение.

    08.11.2016: далее:

    • uniq.ru.html -- кратко.
    • _devstate.ru.html -- тоже кратко (хотя куда там подробнее?).
    • Начат rflags.ru.html. Текст описания и обоснования концепции взят из диссертации с минимальными изменениями.

    09.11.2016: и еще:

    • В BUILD.ru.html отсутствовало описание PREFIX=! Сделано.
    • Добит rflags.ru.html. Подписи к флагам -- в основном из Chl_knobprops, чуток из диссертации.

      НЕ сделано -- подробное описание флагов. При надобности можно будет добавить раздельчик.

    • Сделан cxdtype.ru.html. Состоит из "основной" части (введение и список типов) и приложения с описанием внутреннего формата и функций для работы с dtype.

    10.11.2016: и:

    • cx_time_t.ru.html -- очень кратко и исчерпывающе.
    • Продолжаем писать lib/cxsd_driver.ru.html.

    30.11.2016: мелочи:

    • Переименование с добавлением .ru: cx_proto_v4.html стал cx_proto_v4.ru.html, а misc_notes.html -- misc_notes.ru.html.
    • Начато писание разъясняющих дополнений по devlist'ам -- cxsd-devlist-rationale.ru.html и cpoints.ru.html.
    • Написано programs/das-experiment.ru.html.

    01.12.2016: эх!

    • Начато programs/cdaclient.ru.html.
doc/etc/:
  • 06.02.2017: директория создана. В неё будут класться исходники всяких скриптов и конфигов, должных устанавливаться в хост-системы -- вроде скрипта старта серверов при загрузке ОС и описателя для logrotate.

    Собственного Makefile в ней нет, а только строка LOCAL_BCKPFILES=etc/*~ уровнем выше.

  • 06.02.2017: сделан rhel_etc_logrotate.d_cxsd, должный класться в /etc/logrotate.d/cxsd в RHEL-совместимых системах (версии 5, 6, 7 вроде все совместимы)(09.02.2017: И RH-7.3 тоже).

    В нём правила ротации для /var/tmp/*log.

    Задеплоен на vepp4-pult6, посмотрим, как ротейтнется в выходные.

    09.02.2017: модифицируем файл.

    • Шаблон /var/tmp/*log заменён на чёткое перечисление 3 файлов --
      4access.log 4cxsd.log 4drivers.log

      Смысл в том, чтобы не затрагивать:

      1. v2'шные сервера (всё ещё живы на ring1 -- роговские).
      2. cx-starter.log.
    • Вот с cx-starter.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) детальное сообщение тоже есть. Но кто ж догадался бы, что туда надо заглянуть...

    Пытаемся разобраться в причине и найти пути решения.

    • В CentOS-6.3 была версия logrotate-3.7.8-15.el6.x86_64.rpm; в CentOS-7.3 уже logrotate-3.8.6-12.el7.x86_64.
    • Как показал анализ changelog'а (/usr/share/doc/logrotate-*/CHANGES), изменение произошло в версии 3.8.0:
      - 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 же на директории есть -- какие вопросы?!

    • Вторая мысль: возможно, причина в том, что даже при наличии sticky просто "переименования" из-под root могут что-то испортить -- например, этак можно сделать файт, который после переименования получит имя, совпадающее с именем существующего чужого файла, и тем самым сотрёт его.

      ...с другой стороны -- таким вектором атаки воспользоваться проблематично: чтобы что-то сказать logrotate'у, надо иметь возможность положить файлы в /etc/logrotate.d/, а это уже права root. Разве что рассчитывать на уже ИМЕЮЩИЕСЯ там файлы с шаблонами, под которые подогнать своё. Хотя всё равно маловероятная экзотика.

    • Забегая вперёд: хотя в сети встречаются аналогичные ругательные сообщения от logrotate с упоминанием слова "sticky", в самих исходниках logrotate-3.8.6 этого слова НЕТ.

      Так что пред-предыдущее соображение на тему "Ведь sticky bit же на директории есть" авторами вообще никак не рассматривалось.

    • Итого: всё-таки ПРИДЁТСЯ возиться с добавлением ключевого слова "su". Эх!
    • Ну что ж -- надо лезть в исходники и смотреть, что и как там указывать. И вот почему:
      1. На разных инсталляциях CX могут использоваться РАЗНЫЕ username'ы. И подстраивать под эти различия содержимое /etc/logrotate.d/cxsd -- как-то противно.
      2. А вот если бы оно там позволяло указывать, что в качестве uid/gid использовать uid/gid файла -- получилась бы автоматическая адаптация!
    • OK, лезем и изучаем.
    • Использование -- в logrotate.c, а парсинг в config.c.

      И увы -- не видно там ничего типа "используй uid/gid от файла".

    • Ладно, роем дальше. И оказывается, что:
      1. В logrotate.c::findNeedRotating(), где выполняется проверка на тему "какие права у parent directory", оная проверка НЕ делается при взведённом флаге LOG_FLAG_SU.
      2. А оный флаг выставляется при парсинге директивы "su" в ЛЮБОМ варианте.
    • Т.е., достаточно просто указать "su", и проверка исполняться не будет!!!
    • А парсинг делается через sscanf(), в котором присвоение полям suUid и suGid делается ТОЛЬКО при указанности этих значений.

      Т.е., можно написать сокращённо -- "su root" и даже просто "su", о чём в man'е не сказано.

    • Пробуем просто "su" -- фиг. Ошибка при попытке выполнить set*uid(-1,-1).
    • Окей, делаем "su 0 0" -- опять фиг. Числовые ID'ы оно не понимает, т.к. всегда использует getpwnam() и getgrnam().
    • Ну-с, "su root root" -- ура! Сработало!

      Такая строчка де-факто просто отменяет то нововведение, возвращая поведение logrotate к версии 3.7 (точнее, до-3.8.0).

    • Погуглив по фразе «logrotate "su root root"» обнаружил, что не я один такой "умный" -- этот же рецепт в сети уже встречается.
    • Модифицированный файл теперь лежит в дистрибутиве в 4cx/src/doc/etc/rhel7_etc_logrotate.d_cxsd; и да, очень жаль, что более невозможно использовать один файл.

    Итак, в режиме отладки (с ключом "-d") работает, в "боевом" режиме с добавленной спецификацией "daily" -- тоже, теперь осталось подождать до утра следующего воскресенья, чтобы убедиться, что работает как надо везде.

    26.03.2019: да, на пульту тоже работает -- на canhw; а на cxhw умудрился указать "su 0 0" вместо "su root root" -- оно и ругается, но уже только в мыло, а в /var/log/messages никакого "ALERT" нету.

    Исправил.

    27.03.2019: а теперь и на cxhw тоже работает.

???:
Pult:
Экс-гусиности -- клистроны, впуск-выпуск, rfsyn:
  • 26.03.2018: работа предстоит немалая, так что оно заслуживает своего раздела, с обсуждением потребностей, выбираемых решений и деталей реализации.

    (Да, реально работы начаты еще пару недель назад, 13-03-2018, с f4226, но осознание "глобальности" пришло сегодня :).)

  • 26.03.2018: анализ задачи -- какие есть подсистемы, какие в них устройства, как распределить по серверам, ...

    26.03.2018: поехали:

    1. Подсистемы, после обсуждения с ЕманоФедей предполагается поделить по серверам так:
      1. Rfsyn, впуск/выпуск, задающий Куркин.

        Сюда понадобится новый сервер. Следующий свободный -- :25; очевидно, canhw:25 -- хоть железо комбинированное, CAMAC и CAN, но коль скоро CAN есть, то обращаться нужно из canhw.

      2. Клистроны, резонатор.

        Это в сервер cxnhw:19 -- резонатор в нём уже живёт.

        Тут та же "странность": сейчас ВСЯ клистронная электроника в CAMAC, но поселено будет в canhw:19 из-за того, что резонатор на CAN.

      Есть один нюанс: содержимое гусиного окошка "Phase and Frequency Control" надо будет разделить: 4 строчки верхней таблицы ("Фазовращатели") разнести по окнам клистронов, а "Частота генератора [кГц]" -- очевидно, в окно RFSYN.

    2. В CAMAC-крейтах клистронов набивка такая:
      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, соответственно).

    3. Итак, что бы имеем по драйверам (adc200 не обсуждаем вследствие наличия):
      • g0603 есть.
      • g0401: когда-то был, авторства Андрея Чугуева, от 24-05-2004, в ARCHIVE/work/pult.20070608/drivers/abstract_camac/a_g0401.h, использовался не вполне ясно где -- ARCHIVE/work/pult.20070608/configs/cm5307-20.lst.
      • g0604 -- нету и никогда не было. Но если девайс совместим с g0401 (автор-то один и тот же Цуканов), то можно обойтись егойным драйвером.

        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. Подробности -- в егойном разделе.

      • v0612 -- хбз; устроит ли v06xx? 27.03.2018: да, это он и есть (хбз почему до 24 каналов).
      • c0642 есть.
      • r0610 -- нету никак. Вечером: похоже, пойдёт драйвер r0601. 27.03.2018: должен.
      • f4226 -- в работе.

    27.03.2018: ясно, что можно делать клистронокасательную набивку devlist-canhw-19.lst. Идеи/принципы:

    • На вариабельность по количеству Ф4226 можно забить, а можно "учитывать" -- просто не создавать строчек для них.
    • Собственно всё "обустройство" для каждого клистрона следует делать макросом, который:
      1. Объявляет физические девайсы.
      2. Делает для них cpoint-группу (чтоб её сбагривать непосредственно "драйверу клистрона").
      3. Объявляет девайс-клистрон.
      4. Ему же передаётся параметром количество Ф4226, чтоб отсутствующие не объявлялись (но они пока объявляются).
    • Имена:
      • Физических девайсов -- klsN_DEVNAME.
      • Префикс cpoint-группы -- klsN_devs, так что её содержимое имеет имена knsN_devs.DEVNAME.
      • Девайс-клистрон -- klsN.
      • Его драйвер назовём v5k5045 -- v5k5045_drv.c. Префикс "v5" -- просто на всякий случай, если вдруг вылезет потребность в автоматизации оного клистрона через какую-то иную электронику.

    27.03.2018: теперь касательно rfsyn, впуска/выпуска, задающего Куркина:

    1. Набивка подсистем:
      • Rfsyn: сплошные D16 в разных вариациях (включая ИЕ4) -- вроде всё знакомо (плюс несколько K4X1 и Ф20).

        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, и на него пока ни драйвера, ни толкового описания.

      • Впуск/выпуск:
        1. CAMAC-крейт, 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" -- просто формирователи, они НЕуправляемые.

        2. CAN: 1 штука CAC208 и 1 штука CURVV, который не подключен.
      • Задающий Куркин: CAN, такого состава:
        • CANDAC16
        • CANADC40
        • ???
        • ???

        Кстати, не будет ли целесообразным в эту же CAN-линию посадить и управление резонатором? Стойка-то просто соседняя, а зацеплена она аж за контроллер cangw-rst1, на котором пол-магнитной системы висит.

    2. По драйверам: вроде большинство есть, кроме:
      • D16 "со счётчиком"
      • D16M1 -- ???
      • Ф20 (и не пассивный ли он?) 30.03.2018: да, пассивный.
      • TTL -- хбз, что это, и не пассивное ли оно. 30.03.2018: да, пассивное.
      • Пара куркинских -- возможно, на основе стандартного SLIO24?

    28.03.2018: приступаем к созданию v5k5045_drv.c. Пока делаем "скелет", чтоб собирался.

    02.04.2018@утро-пультовая: сделан g0401_drv.c.

    07.08.2018: чуть задним числом, но уж пусть хоть так будет:

    1. RFSYN и впуск-выпуск (кикеры) подселены в cxhw:19 (devlist-cxhw-19.lst) -- к уже имеющемуся там резонатору (ringrf).
    2. Все драйверы сделаны и сегодня доиспытан последний -- D16 в варианте D16P (в 20-й позиции, перепускная).

    Теперь надо деплоить конфиг целиком и делать некоего клиента, чтоб управлять всем сходу.

    • Как он должен выглядеть -- пока не вполне ясно.
    • В идеале -- "деревом", показывающим цепочку запусков.
    • Но соединения таковы, что там есть "петля" -- из одного блока 2 выхода идут в 2 других, а потом опять сходятся в один.

      Правда, это разные режимы работы (впуск и выпуск кикерами), так что вместе/одновременно они работать не будут. Но как изобразить связи древовидно -- всё равно вопрос.

    • Плюс, контейнер-"дерево" (outline) в v4 отсутствует.

      ...впрочем, неясно, насколько он доделан и в v2 -- там шибко много стенаний от 29-08-2013, хотя в апреле-мае 2006 много наработано.

    • А пока можно сделать просто набором панелек (по одной на каждый блок), либо вообще табличку (по строке на каждый выход, плюс справа пристраивать device-wide элементы (возможно, в subwin'ах)).
  • 08.08.2018: для клистронов отводим cxhw:25 -- devlist-cxhw-25.lst.

    21.08.2018: его содержимое сделано. Там всё просто -- макрос ONE_KLS(), объявляющий все устройства (в т.ч. f4226 -- условно, в указанном количестве), сводящий их в cpoint-иерархию и объявляющий устройство klsN (на эту иерархию нацеленное).

  • 13.10.2018: (реально уже с неделю-полторы как) и еще одно -- управление конверсионной системой.
    • Селить это безобразие (висящее на крейте v5-l-cnv) надо в тот же devlist-canhw-19.lst -- к rfsyn'у и резонатору.
    • А вот скрин нужен отдельный от прочих; v5cnv?

    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: далее:

    • Посмотрено, какие именно каналы в устройствах что делают:
      • Уставка напряжения магниту-концентратору -- Ц0642.канал0, прямо в его вольтах, диапазон [0-9].
      • Выкл/вкл -- В0612.бит0; 0 -- выключено, 1 -- включено.

        (У раздолбая Гусева включенное(красное) положение слева, выключенное(зелёное) справа -- людей путать...).

      • Бит готовности: Р0610.бит0; 0 -- неготово, 1 -- готово.
      • Биты блокировок: Р0610.бит1...бит3; 0 -- блокировка, 1 -- отсутствие (нормально-замкнутые).
    • В devlist добавлены и надлежащие cpoint'ы (для блокировок, кстати, еманофедя-ALIAS'ов в не было).
    • Скрин нарисован (пока без Ф4226).

    Осталось проверить функционирование на живом оборудовании.

    В частности, узнать: надо ли при загорании блокировок делать выключение (ic.linCNV.swc:=0)?

Сварка:
  • 25.04.2018: уже с месяц идут работы над переводом сварки на CXv4, но раздел создаём сейчас.
  • 25.04.2018: тут описание процесса.

    25.04.2018:

    • За прошедшее время подготовлены devlist'ы на все 3 сварки: devlist-bike-47.lst, devlist-romeo-48.lst, devlist-ep1-berezin2-49.lst. Пока в минимальном варианте -- include'ы, список устройств, блок cpoint'ов для эмуляции старой карты каналов для доступа через starogate.
    • Также сделаны baachi.subsys и (вчера) verona.subsys.
    • Тормозящим фактором было отсутствие поддержки LAPPROX в формулах, но на прошлой неделе оно было реализовано.
    • И вчера проведён тест на симуляторе: запущены сервера :48 и :49 с -s и на них натравлены скрины. Найдено и исправлено несколько косяков.

    26.04.2018:

    • Добиваем недоделанности скрина verona.subsys: панели DDS300 и "Алармы и блокировки" -- со вторым было много маеты, в т.ч. сделано 4 именованных точки контроля с калибровками.
    • Зато очень простой оказалась реализация кнопки [Сброс] -- там надо выставить битик =1, подождать секунду, сбросить битик =0. В v2 это два десятка строк, а в v4 -- тривиальная w-формула со "sleep 1.0" посерединке.
    • 10.05.2018: убрано "уст." из меток gunctl, убраны units в измерениях magsys.
    • 11.05.2018: сделана панель bfp16.
    • 28.04.2018: разобраться с iptables: почему при отсутствии "-p -udp --sport 8012" клиенты НЕ получают ответов на резолвинг. Кроме того, почему "systemctl restart iptables" приводит к долгой паузе в получении пакетов: неужто недостаточно первой строчки про --state RELATED,ESTABLISHED? 04.04.2025: да, "при отсутвии..." не работает потому, что отправляется на один адрес (broadcast), а ответ приходит с другого (unicast). Соображено это было ещё 11-06-2021 ниже, а сегодня сообразил ещё раз. Что касается долгой паузы -- видимо, потому, что таблицы соответствия (куда были какие пакеты отправлены) сбрасываются при загрузке правил-таблиц файрволлинга.
      11.06.2021: некоторое соображение, пришедшее в голову по результатам разборки ЧеблоПаши со странным поведением EPICS'а на ВЭПП-4 (caget с машины, находящейся не в одной сети с сервером, а глядящей туда через gateway, почему-то не коннектился, несмотря на должным образом уставленные $EPICS_CA_*; ко мне обращались за консультацией):
      • Возможно, дело в том, что ОТПРАВКА от клиента идёт на broadcast-адрес, а ОТВЕТ прилетает с ДРУГОГО адреса. Вот и не догадывается ядро, что тот пакет -- вроде как не относящийся к "виртуальному соединению" -- нужно принять, а не отбрасывать.

        04.04.2025: да, именно в этом и дело -- сегодня проверено на CXv4, где без "--sport 8012" не работало, а с его добавлением заработало.

      • Кстати, самого "--sport 8012" я сейчас нигде в конфигах (всякие notes/*Activity*) не нашёл.

        Так что вопрос -- где же именно я те эксперименты делал и добавлял лишнюю строчку в настройки файрволлинга.

        Возможно, на ep1-berezin2, но он сей момент не пингуется, как и tower.

        А на пультовых машинах вообще список фильтрации пуст.

      ЗЫ: а у ЧеблоПаши проблема, похоже, в специфике работы EPICS'а:

      • IOC пользуется хаком, основанным на поведении Linux'ового ядра: что если некий порт за-bind'ен НЕСКОЛЬКИМИ процессами (с флажком SO_REUSEPORT), то
        • UNICAST-пакеты ловятся каким-то ОДНИМ процессом (не то последним, не то по кругу).
        • BROADCAST-пакеты же передаются ВСЕМ процессам.
      • Поэтому если на некоем IP-адресе сидит несколько IOC'ов, то надо ОБЯЗАТЕЛЬНО указывать в EPICS_CA_ADDR_LIST именно broadcast-адрес.
      • Через IP-gateway broadcast'ы НЕ проходят, поэтому приходится пользоваться unicast-адресами.
      • ...а они приводят к тому, что пакет ловится, вероятно, НЕ тем IOC-сервером, который владеет интересующим каналом.

        Вот и всё -- "связь не работает!!!111".

    11.05.2018: начата weldcc.subsys -- для начала копированием из Вероны.

    12.05.2018: продолжаем weldcc.subsys.

    • Разблюдовка вакуума отличается от вероновской:
      1. плюс 4 лампочки блокировки, ...
      2. вместо переключателя TМН, оборотов оного, кнопки сброса, показаний вторичноэмиссионного чего-то;
      3. подписи на лампочках блокировок чуть иные.
    • Блок DDS тут двойной -- DDS500 плюс DDS300, в закладочнике.

      Ради чего пришлось изготовить tabber_cont.

    14.05.2018: продолжаем -- помимо панели "ЭЛС" (с которой будет отдельная долгая возня) остались 2 штуки магнитных системы.

    • "Магнитная система 2" оказалась идентична обычной магнитной системе от Вероны (ну еще бы -- это физически она и есть, сначала прямо CEAC124 (и крейт?) таскали).
      • cpoint'ы в devlist'е просто скопированы.
      • А подписи на панельке укорочены, по образу db_weldcc.h.
    • "Магнитная система 1" -- старая, САМАЯ старая, на аппаратном уровне отличается отсутствием измерений напряжений, и каналы измерения токов там расположены довольно хаотично.
      • В скрине сделано в основном копированием из "новой" магнитной системы, с убиранием колонки Uизм.

        Плюс добавлена ручка, глядящая на rate канала LD (Линза Динамическая).

      • cpoint'ы в devlist'е сделаны копированием с изменениями -- подправлены имена и ссылки на каналы, а калибровки совпадают с МагнСист2.

    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 -- всё заработало.

    • 2 нижние строчки в панели "Алармы и блокировки" были удалены, т.к. они, как утверждается, давно неактуальны.
    • И нашлась пара ляпов, внесённых при переносе с v2 на v4:
      1. Отсутствовали калибровки на каналах DDS300 и DDS500.

        Сейчас не вполне ясно, к чему они относятся: к DDS'ам как устройствам, или же к конкретной установке.

        Поэтому внесены в devlist-xweld-50.lst, в виде "замещающих cpoint'ов" -- в виде

        cpoint dds.ftw0 dds.ftw0 143
      2. Блокировки там инверсные ("нормально-замкнутые"?) -- горят в состоянии бита "0", а не горят при "1".

        Поэтому скрин теперь ссылается на каналы не vac.inprbN, а vac.ilkN, имеющие вид

        cpoint vac.ilk0 vac.inprb0 -1 -1
        (да, дуплет R=-1,D=-1 даёт инверсию -- 0 превращается в 1, а 1 в 0).
    • 30.03.2019: отдельный квест -- как запинать "иконку на десктоп" для запуска CX-starter'а.
      • В отупевшем RHEL7'шном Gnome3 штатных средств нету НЕЛЬЗЯ просто -- "создать новый лончер".
      • Поэтому после гугления и чтения мутных рецептов был взят за образец xv.desktop и модифицирован "как надо".
      • Тонкости:
        1. Просто так оно запускать отказывается, и первый раз при double-click'е спрашивает что-то на тему, что это не-trusted, и можно ли запускать -- надо нажать кнопку "Trust and execute".
        2. Путь к пиктограмме приходится указывать полный, т.к. неизвестно, какой там список путей поиска.
        3. А вот бинарник можно как с путём, так и без, если он в $PATH -- из ~/bin/ оно прекрасно запустилось.

      Файл с результатом этих расследований положен в 4cx/src/doc/etc/rhel7_Desktop_CX-starter.desktop.

      Но самое обидное: как выяснилось, УЖЕ имевшийся аналогичный файл от RHEL6 с romeo -- сейчас он помещён в 4cx/src/doc/etc/rhel6_Desktop_CX-starter.desktop -- тоже подошёл бы после правки путей.

  • 02.07.2018: та самая панель "ЭЛС" -- weldclient_process_noop.c (сначала предполагался weldclient_process_knobplugin.c).
    • Делать будем унифицированный для CAC208 и CEAC124, просто указывать ему параметром, сколько каналов.
    • Клиентская программа получит название "weldclient", собираться будет стандартным образом (как liuclient etc.) с использованием исходника pult.c.

    02.07.2018: приступаем.

    • Первейший вопрос -- ТИП какой?
      • Формально это как раз тот самый DATAKNOB_USER: какое-то количество каналов, должных указываться в описании и резолвиться в dataref'ы.

        Но никакой поддержки такого поведения нет:

        • ни в парсинге в Cdr_via_ppf4td.c,
        • ни в Cdr_treeproc.c (в этом только в CdrProcessKnobs() -- но там и несложно, просто пройтись по всем),
        • ни интерфейс USER-ручек с деревом никак не продуман (он просто пуст).

        Так что увы, не прокатит.

      • Поэтому использовать бум DATAKNOB_NOOP.

        ...он же потом подойдёт и для liu'шного beamprof_knobplugin'а, который сделаем "по образу и подобию".

    • Сделана "минимальная собирабельность" -- обвязка в Makefile и скелет самого weldclient_process_noop.c. Собирается, запускается.

    03.07.2018@утро-дома: творческий кризис: никак не могу толком решить, как именно организовывать работу/внутренности этого knobplugin'а. Чтоб он работал с разными вариантами, да и вообще просто -- КАК?

    03.07.2018@дорога-на-работу-мимо-НИПС-и-ИПА: а может, дело в том, что это стоит делать не knobplugin'ом, а драйвером?

    • Драйвер был бы на vdev'е -- как раз и проверить/доделать поддержку векторных каналов.
    • Назвать его как-нибудь общо, хотя для начала можно и "weldproc".
    • И тут, конечно, тоже вопрос: а как делать маппинги, когда нужно и номера номера каналов (для формирования таблицы), и индивидуальные каналы/метки -- для чтения начальных значений и отображения текущих?

      Указывать в формате вроде lineN=TABLE_IDX:SCALAR_NAME?

      На работе: или у нас сейчас таблицы формируются из индивидуальных по-канальных векторов, а не одним массивом?

      Вечером: точно -- судя по advdac_cankoz_table_meat.h, KOZDEV_CHAN_OUT_TAB_ALL сейчас совсем не работает, а используются исключительно по-канальные KOZDEV_CHAN_OUT_TAB_n_base+n. Так что никакие номера не нужны, а просто поштучные имена.

    • И еще ведь кроме таблицы нужно предпринимать иные действия -- включение тока при старте и отключение при стопе.

      Позволить указывать формулы?

    • ...и да, ПОТРЕБУЕТСЯ возможность драйверу отдавать множественные {R,D} -- для туннеллирования калибровок, которых будет как минимум 2 (козачиные 1000000.0 и коэффициент вольты->миллиметры).

      @вечер-дома: хотя конкретно тут можно схалтурить: перемножать все коэффициенты и отдавать наверх 1 пару -- воспользовавшись тем, что сдвиг нуля отсутствует.

      04.07.2018@обед: а можно и еще проще -- считать, что этот драйвер всегда работает в микровольтах, а калибровки навешивать в devlist'е (совпадающие с оными для скаляров). Ведь всякие драйверы ist/v1k/v3h/... работают именно так.

    04.07.2018: к вопросу о синтаксисе. Безотносительно того, ЧТО использовать -- knobplugin или драйвер -- разумным выглядит выбрать указание "маппингов" каналов из 2 вариантов:

    1. lineN=SCALAR_NAME,VECTOR_NAME,LABEL (пункт "LABEL" имеет смысл только для knobplugin'а).
    2. lineN=BASE,LABEL где BASE указывает на имя cpoint-иерархии из 2 элементов, такого вида:
      cpoint BASENAME {
          vector DEVICENAME.out_tabN
          scalar DEVICENAME.outN
      }
      

      Замечание:

      • В "scalar" также может быть ссылка на cpoint-канал -- калибровка ж еще нужна.
      • Либо, как вариант, именно ЭТА иерархия и становится основной, а изначальные скалярные cpoint'ы ссылаются на неё, уже без эоэффициентов.

        ...хотя, глядя на нынешние devlist'ы, где все магнитные системы идут едиными списками с калибровками -- неа, такой подход менее удобен.

      После обеда: а впрочем, если использовать драйвер, то нафиг тут калибровки -- решили ж уже, что драйвер weldproc работает в микровольтах, а калибровки навешиваются на его каналы одинаковые вместе с каналами реального ЦАПа.

    Таким образом, в любом из (1) или (2) количество "строк" получается автоматически -- просто смотрится номер последней указанной.

    Хотя формально есть и вариант #3:

    1. Указывать количество и базу, а уж указанная "базой" cpoint-иерархия должна содержать N дуплетов имён vectorN,scalarN.

    Конкретно сейчас наиболее симпатичным представляется вариант 2.

    04.07.2018@вечер-дома: всё же наиболее симпатичным представляется вариант с драйвером. Так что быстренько вделываем в vdev.c поддержку векторных каналов.

    06.07.2018: приступаем к изготовлению weldproc_drv.c.

    • За основу взят TEMPLATE_VDEV_drv.c -- скелет от kurrez_cac208.c.
    • Парадигма конфигурирования предполагается такой: в devlist'е создаётся cpoint-иерархия из всех вовлечённых каналов (табличные, скалярные, командные (или эти делать ссылкой на устройство?)), а auxinfo указывается в формате "HIERARCHY_BASE/N", где N -- количество строк (по этому количеству будут браться каналы -- cur1,tab1...curN,tabN).
    • Парсинг такого формата взят из v3h_a40d16_drv.cstrtol() вместо вычитания '0').
    • Парадигма доступа к subord-каналам нетривиальна.
      • Поскольку количество каналов может быть разным, с конфигурированием at-run-time, то фиксированной картой пользоваться нельзя: отсутствующие каналы интерфейсом insrv:: будут вёрнуты как -1 (CDA_DAT_P_ERROR), что немедленно приведёт к ошибке и DEVSTATE_OFFLINE.
      • Поэтому штатная 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: драйвер сделан было в основном еще с неделю назад, а потом происходила отладка, в первую очередь обусловленная сложностями с реализацией таблиц в драйверах козачиных устройств.

    • Помимо прочего, были косяки с отслеживанием "состояния таблицы в устройстве" по 0xFD (GET_DAC_STAT). Подробнее описано в тамошнем разделе за 11-07-2018...13-07-2018.
    • Отдельный нюанс -- что команда "unicast FILE_STOP" (0xFB) поддерживается далеко не с первых версий прошивки (например, в CAC208 -- начиная с sw_ver=4, а у меня на столе =3), и реально СТОП не работает.
    • Следствие предыдущего: если сделать weldproc'ем Stop, а потом быстро (ДО завершения реального исполнения таблицы) заново Start, то зрайвер зависает в состоянии EBW_STATE_ACTIVATE.
      • Потому, что разрешатор идти дальше -- IsAlwdGO(), проверяющий путём проверки значения ЦАПова канала DO_TAB_START (==0 -- значит, можно) -- НЕ вернёт true, вследствие рассинхронизации с advdac'овым состоянием.
      • А рассинхронизация происходит оттого, что команда FILE_STOP реально не сработала, и advdac_cankoz_table_meat.h, перейдя в состояние TMODE_ACTV и начав поллить устройство периодическими 0xFD, обнаруживает в статусе флаг DAC_STAT_EXECING и, считая это за "detected START", переходит в TMODE_RUNN. А в этом состоянии делать запуск уже низя, вот значение DO_TAB_START и равняется 2.
    • Кстати, для записи дуплетов (в векторы времён и вольтов) сделана простенькая SndDupl().

    16.07.2018: допиливание:

    • Не очень удобно, что время -- time_ms в миллисекундах (т.к. далее в таблицу надо писать МИКРОсекунды).

      Поэтому переделано на микросекунды, сам канал переназван из "time_ms" в просто "time", и на него отдаётся коэффициент 1000000 -- т.е., на клиентском уровне вообще секунды (как и у козакотаблиц, кстати).

    • Упрощение подсовывания карты хотел сделать, по идее от 07-07-2018 -- фиг, не сработало (ну я и барашко!).
      • Чтоб сработало, нужно б было номера sodc'ов для curN,tabN пустить вместе, чередуя.
      • Не то, чтоб это на что-то влияло -- диапазоны реально не используются, т.к. трансляция идёт по таблице.
      • Но просто лень. Хотя на будущее -- надо иметь в виду.

    17.07.2018: финализация.

    • В weldproc.subsys добавлено отображение лампочки is_welding.

      weldcc.subsys такой роскоши пока нет -- мож, потом подпеределаем.)

    • И панели управления ОБОИМИ "процессами" в weldcc.subsys добавлены.
    • Также в драйвер добавлен парсинг параметров at_beg и at_end (изначально забытый).

      Проверено -- работает.

      Одно только странно: время между исполнением at_beg- и at_end-формул на 1 секунду больше, чем время исполнения таблицы. Проверено по print_str, печатающей время (хотя и всего лишь по секундам).

    Теперь всё -- дальше проверять на живой машине.

    20.05.2019: да проверено уже с месяц-два как (на xweld) -- работает.

    Так что можно считать раздел за "done".

linthermcan:
  • 08.06.2018: вчера ЕманоФедя развыступался, что никак нельзя "перманентно" отключить сигнализацию о большом несоответствии температуры.
    • Причина, мол, в том, что железо термостабилизации в принципе не справляется, и исправить это нет никакой возможности.
    • А из-за того, что температура постоянно то выходит за границы допустимого, то возвращается обратно, просто ткнуть в горящую лампочку для отключения звука не получается -- оно постоянно включается обратно.
    • И из-за этого дежурных бибиканье просто бесит, смысла якобы оно не имеет, и некоторые особо нервные просто "выкусывают" бибикалку с матери.
    • Но нажать [Out->Good] им почему-то тоже низзя.

    08.06.2018: добавлен им отключатель -- переключателем в правом верхнем углу окна. По умолчанию (=0) алармы разрешены, но можно выключить (=1). Делается это через канал %silent, потому локально для программы. Собственно обращение внимания на отключенность -- в макросе THERMLINE(), в определении _alm.

???:
C13:
Сам bigfile-0002:
Знания:
TANGO:
  • 22.06.2017@утро-~11:00-занятие-по-TANGO-в-ТСАНИ:
    • В TANGO у каждого девайса есть обязательные атрибуты State и Status, являющиеся очень точными аналогами наших _devstate и _devstate_description: это числовой код текущего состояния (enum) и строковое описание (freeform).

      Отличия в том, что:

      1. Оба этих атрибута являются "обычными каналами" -- т.е., драйвер сам меняет их состояние по своему усмотрению, а вся их "специальность" исключительно по соглашению.
      2. State может иметь кучу разных состояний: кроме ON, OFF, FAIL туда намешана ещё толпа всего (включая MOVING -- для сервоприводов).
        • Т.е., в нём смешаны 2 разные сущности: состояние девайса в смысле работоспособности (осмысленное для любого девайса) и всякие специфичности вроде состояния "едем".
        • Следствие -- для многоканальных устройств (например, ОДИН device-type, управляющий ДВУМЯ подвижками) это State в своей расширенной части не особо осмысленно.
    • Как утверждает Фатькин, TANGO изначально создавалось для управления всякими beamline'ами, где измерения в основном периодические (всякие температуры, токи) плюс управление подвижками. Поэтому там всё по поллингу (опционально -- с некоторой периодичностью), а event'овость была прикручена позже.
    • Соответственно, чтобы просто "вернуть значение изменившегося канала", надо сгенерить event, по получению которого "ядро СУ" произведёт вычитывание (да, кривизна напоминает EPICS'ную).
  • 25.09.2018@~13:00, холл 3-го этажа около круглого стола, после вторничного совета по автоматизации: поговорил с Сашей Сенченко на тему "как в TANGO устроена модель обмена данными между сервером и драйверами -- что делать с долгочитаемыми каналами (выходит, пока один канал не прочитался (а он может интегрироваться 100мс), драйвер висит, не выходя из своего кода, и следующие каналы тоже зависнут в очереди, хотя могли бы обработаться сразу?), а также может ли драйвер передавать "наверх" значения (каналов записи) по собственной инициативе". Нифига толком не помню...
    • ...как-то можно и multithreading использовать.
    • ...в т.ч. можно, кажется, разные каналы по разным thread'ам распихать.
    • ...есть какое-то поле состояния, в котором можно указывать, что, хоть код драйвера сейчас чем-то и занят ("reading"), но допустимо делать следующий запрос (в другом thread'е?).
    • ...при синхронных запросах есть таймауты, до 3 секунд.
  • 25.10.2018: еще поговорил с Сенченко, на тему "кто в TANGO является инициатором запуска чтения/измерения из каналов чтения".
    • В основном -- просто клиенты присылают запросы, эти запросы передаются драйверам, теми исполняются, и клиентам отдаются ответы.
    • Причём эти запросы СЕРИАЛИЗУЮТСЯ -- они все ставятся в очередь и передаются драйверу последовательно.
    • ...хотя можно указать девайс (или атрибут?) как "thread-safe" (там свой, другой термин), и тогда эти чтения смогут исполняться в параллель.
    • И каждый клиент получает ответ именно на СВОЙ запрос, никакого демультиплексирования там НЕТ.
    • (Вывод -- от меня: TANGO ориентирована не столько на "данные", сколько на RPC. Это ж реально именно тупо-процедурный подход, безо всякого понимания "смысла" данных.)
    • Но также можно сказать, что канал надо подвергать поллингу -- само ядро TANGO будет вызывать чтение с какой-то частотой.
    • Клиенты же могут по мере своих потребностей производить вычитывание "последнего известного" (причём хранится аж 10 штук последних известных).
    • Но также клиент может подписаться на "event" -- когда устройство (само?) обновило значение канала, то оное значение будет прислано прямо в событии.

      Как всё-таки происходит генерация событий (на любой канал можно подписаться? или драйвер специально генерит event'ы?) -- я не понял.

      В event'ах могут присылаться значения не только скаляров, а практически любого размера каналов (хотя какое-то ограничение сверху есть, определяется оно ZeroMQ).

    • И да -- event'ы, в отличие от обычной работы, ходят по ZeroMQ, а не через CORBA.

    Еще вывод от меня: похоже, там фреймворк многое никак не специфицирует и не обеспечивает, отдавая на откуп конкретного программиста то, что в EPICS и CX является функционалом самого фреймворка (та же отдача данных по подписке -- тут тупо вызовы процедуры "читай атрибут").

    25.07.2019: и ещё пообщался с Сенченко на темы устройства TANGO вообще и в частности доступа из клиентов.

    1. Судя по описаниям, всё общение клиента с сервером идёт через DeviceProxy, так? И что тогда -- на 2 канала разных устройств нужно заводить 2 штуки DeviceProxy?
      • Ещё можно использовать некий AttributeProxy.
      • А строго говоря, понятия "канал" в TANGO нет.
      • Там есть "атрибуты" (attributes), properties, команды.

        Единых правил что для чего использовать -- нету (это я вроде и в мануале встречал), но в общем случае примерно так:

        • Атрибуты -- это собственно наши "каналы": то, что маппируется на аппаратуру -- ЦАП, АЦП, ...
        • Properties -- условно, конфигурационная информация: IP-адрес Ethernet-контроллера, позиция в CAMAC-крейте, диапазон измерения, ...
        • Команды -- служат для СИНХРОННОГО выполнения некоего АТОМАРНОГО действия, продолжительного по времени. И непрерывного: пока выполняется команда от одного клиента, другой команду запустить не может.

          Поэтому модель с делением команды на 2 канала -- инициатор (куда писать) и результат (который мониторировать) -- не вполне подходит: клиент не сможет быть уверен, что получил ответ на СВОЮ команду, а не на какую-то другую.

      • И, кстати, в TANGO НЕТУ структурных типов (произвольных структур) -- это я с EPICS4 перепутал.

        А из нетривиальных типов есть только массивы, одно- и двумерные. Причём одномерные являются частным случаем двумерных.

        Для всего же иного там используется понятие "encoded": это дуплет из строки и цепочки байт. Строка содержит "схему кодирования", а далее байтами передаётся информация. И предполагается, что оба участника эту схему должны понимать.

    2. Аналоги caget/caput - ?
      • Нет, нету. Просто потому, что и "каналов вообще" там нету.
      • А условные аналоги "наших каналов" существуют только в TAURUS.
      • Так что если писать какую-то софтину вроде фединого DDM'а, то на TAURUS'е, тем более, что тот тоже на Python.
    3. Полное имя канала: HOST:PORT/DOMAIN/FAMILY/MEMBER?CHANNEL - ?
      • Последний разделитель, между MEMBER и "CHANNEL" -- тоже '/'.
      • Хотя где-то для чего-то (в taurus-что-то) используется и '.'.
      • ...отдельное сомнение (от меня): раз "каналов вообще" нету, то ГДЕ используется '/'? Вероятно, тоже только в TAURUS'е?
    4. Alias'ы - существуют ли?
      • Да, где-то есть, но Сенченко сам этим не пользовался и даже не помнит, где их искать.
      • Кроме того, как-то там можно делать прямо "alias'ные device'ы".
      • А также device'ы, у которых ВСЕ атрибуты форвардятся на атрибуты других устройств.
    5. Как они обходятся с многоканальными устройствами, вроде CAC208, ADC4X250 и т.д.?
      • Ключевое слово -- какие-то "динамические атрибуты".

        Их вроде как можно создавать в конце init'а, и потом некий(е) метод(ы) устройства будет получать указание на запись и чтение таковых.

        И, видимо, методу передаётся ИМЯ атрибута, так что он может сопоставить ему номер канала (ЦАП 0-7). Для чего можно использовать хоть "словарь" (хэш-таблицу), хоть тупо брать последний символ имени.

    На вопрос "Саша, а можешь предположить, почему в TANGO всё сделано настолько иначе, чем у прочих (EPICS, NI, ...), что нет парадигмы каналов?" ответ был такой:

    • Вероятно, это следствие использования CORBA: они постарались построить СУ вокруг неё, отразив в парадигме СУ corba'овские понятия.

    От меня: ужас!!! Как это поделие, реализующее несколько странную модель (на мой взгляд -- категорически неадекватную, там же нет кучи необходимого функционала!), умудрилось завоевать такой огромный кусок "рынка" СУ физических установок?!?!?!

    28.07.2019@вечер-дома: судя по документу tango_92.pdf, ПОЛНЫЙ синтаксис имени устройства --

    [//FACILITY/]DOMAIN/CLASS/MEMBER

    В идеале, этот синтаксис бы тоже надо поддерживать (хотя что именно означает это опциональное "//FACILITY" -- хбз).

    02.08.2019: UPD: Сенченко говорит, что он вариант с "FACILITY" раньше не встречал и он считает, что "На самом деле это host:port.".

    02.08.2019: ещё кусочек информации от Сенченко:

    • А вообще есть TangoTest в котором есть все типы атрибутов, команды. Если ставишь из репозитория - то обычно он там есть.

    Это в ответ на мой вопрос "Есть ли где-нибудь пример как быстро запустить минимальный TANGO-сервер, с какими-нибудь "синтетическим" устройством, чисто для освоения/тестов?".

    Кстати, я посмотрел -- в дистрибутиве tango-9.2.5a есть директория cppserver/tangotest, и там действительно что-то собрано.

    18.10.2019: ещё немного пообщался с Сенченко, на этот раз по поводу pipe'ов (про которые он на семинаре 10-10-2019 говорил, что они (введённые в TANGO-9) и есть некий аналог структурных типов.

    Далее -- заметки в свободной форме, НЕ цитаты, но как я его понял (и что ещё раньше прочитал сам).

    • Судя по описанию, операция "читать pipe" (или "писать pipe", или "писать, а потом читать pipe") действительно "атомарна" -- она возвращает набор данных достаточно произвольных типов, тем самым имитируя "произвольную структуру".
      • "Набор" в данном случае -- некоторая цепочка типов, передаваемых друг за дружкой.
      • Хотя раз от раза -- точнее, "пачка от пачки", "обновление от обновления" -- можно передавать разные наборы, но если драйвер сгенерил один набор, то этот набор и будет прочитан всеми "запросившими чтение" клиентами.
      • Т.е., это НЕ pipe в смысле "соединение точка-точка" между клиентом и драйвером.

        22.10.2019: точнее, "не совсем" -- см. следующие пункты.

      22.10.2019: но типы-то -- только стандартные танговские "атомарные", т.е., скаляры и 1D и 2D-массивы. Следовательно -- вложенных структур тут сделать нельзя, в отличие от EPICS!

    • Но вот когда пытался обсудить с Сашей вопрос "атомарности" и "что будет, если НЕСКОЛЬКО клиентов одновременно попросят чтение", то выяснилось грустное.
      1. Во-первых, похоже, что TANGO позволяет работать с pipe'ами ТОЛЬКО СИНХРОННО (вроде это даже в доке написано).

        Как говорит Саша, они вроде собираются/пытаются сделать возможность асинхронной работы, но что-то там это как-то плохо ложится на архитектуру (из-за объектности, что ли...).

      2. Во-вторых, операция "чтения" -- она СЕРИАЛИЗУЕТСЯ, а не мультиплексируется.

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

        • метод драйвера будет последовательно вызван несколько раз -- и пока не завершится первый вызов, следующие будут просто висеть в очереди;
        • а НЕ будет сделано одно чтение и всем клиентам розданы результаты.

      Тут не очень понятно: как всё-таки устроено чтение?

      • То ли именно каждому клиенту оно будет выполнено индивидуально и отправлены индивидуальные результаты.
      • То ли всё-таки клиент может ОТДЕЛЬНО ждать уведомления о "событии" и потом отдельно тут же произвести чтение "значения" (набора) интересующего pipe'а, и если за время между уведомлением и чтением больше ничего не произошло (не было обновлений), то такой клиент получит тот же самый набор значений.
    • "Неприятный для TANGO вывод номер 1": похоже, что с атомарностью там всё-таки не очень.

      Это следует из того, что там можно отдельно ловить уведомления, а потом отдельно запрашивать собственно "чтение данных".

      В частности, моё наблюдение при попытке изготовить cda_d_tango.c: в tango'вской клиентской библиотеке НЕТУ локального кэша значений, совсем. Вот что пришло из сети, в пакете -- то клиенту и отдают, и после этого "отдавания" (когда пакет выкидывается) -- данные библиотекой забываются.

      А вот в СЕРВЕРЕ -- похоже, последние значения хранятся.

    • "Неприятный для TANGO вывод номер 2": также похоже, что TANGO всё-таки сделано в парадигме "стараемся имитировать по сети то, как всё бы работало локально" -- выглядит так, что там НЕТУ никакого мультиплексирования.

      "Мультиплексирование" -- важный аспект функционала EPICS и CX, когда результат выполнения одной операции (чтение, обновление) раздаётся ВСЕМ заинтересованным клиентам.

    Замечание: бОльшая часть вышенаписанного -- со слов Сенченко, но сам он pipe'ами не пользуется и, возможно, что-то понимает не вполне верно.

  • 24.07.2019: собрал TANGO-9.2.5a.

    24.07.2019: процесс несложный, но для этой штуки нужны "пререквизиты" (зависимости) omniORB и zeromq, доступные в виде RPM.

    • tango-9.2.5a.tar.gz (wget'ом скачивается файл под именем "download" -- косяк wget'а, надо просто переименовывать).

      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!!!

    • Зависимости:
      • omniORB-devel-4.2.0-3.el7.x86_64.rpm
        • omniORB-4.2.0-3.el7.x86_64.rpm
      • zeromq-devel-4.1.4-6.el7.x86_64.rpm
        • zeromq-4.1.4-6.el7.x86_64.rpm
          • openpgm-5.2.122-2.el7.x86_64.rpm
          • libsodium-1.0.18-1.el7.x86_64.rpm

      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.

    • На реальную работу это не влияет -- поскольку /etc/ld.so.conf.d/mariadb-x86_64.conf указывает директорию /usr/lib64/mysql.
    • Почему её не находит configure'вский "ld" -- вопрос, из содержимого config.log это неясно (там никаких -L не видно).
  • 09.08.2019: о "минимальном TANGO-Device-server'е", в т.ч. без БД -- как этой хренью пользоваться.

    09.08.2019: просто поток информации, по мере разбирательства.

    • Оно лежит в cppserver/tangotest/, но конкретно бинарники в поддиректории .libs/ (libtool, блин...).
    • Конкретно TangoTest требует LD_LIBRARY_PATH, а в lt-TangoTest пути уже как-то встроены.
    • Свою командную строку он вроде бы объясняет, при запуске без параметров.
    • ...но для режима "-nodb" там забыли указать, что необходимо указывать ключ
      -ORBendPoint giop:tcp::10001

      Тут "10001" -- это порт, а всё остальное -- предопределено: "-ORB*" -- опции, передаваемые ORB'у, в частности, "endPoint" -- что слушать; "giop" -- название протокола то ли CORBA вообще, то ли конкретно omniORB'а.

      Нарыл я этот синтаксис случайно, каким-то гуглением, но он описан в документации по omniORB, раздел "8.6 Transports and endpoints".

    • Итого, полная командная строка (без директорий и LD_LIBRARY_PATH) --
      TangoTest instname -ORBendPoint giop:tcp::10001 -nodb -dlist a/b/c,x/y/z

      Тут "instname" -- имя экземпляра (хотя нафиг оно при "-nodb" -- загадка), а a/b/c и x/y/z -- имена устройств, которые надо создать.

    • Замечание 1: при запуске с ключом "-v5" оно вываливает кучу информации, включая имена и типы атрибутов в создаваемых устройствах.
    • 11.08.2019@дома: Замечание 2: просто так натравить клиента на такой сервер не получится. Необходимо ещё указывать в именах устройств "#dbase=no", в противном случае поимеваем 'Tango::ConnectionFailed'.
  • 22.08.2019: еще давно (полмесяца?) придумана идея, как делать "gateway" для доступа из TANGO в другие СУ: девайс с динамическими атрибутами. Все приходящие к нему имена -- он пытается создать каналы (хоть CX, хоть EPICS, хоть вообще через cda -- "protocol::name").

    Вопрос только в типизации: откуда возьмётся информация о типах? Зависит от конкретного API динамических атрибутов. Если возможна "динамическая типизация" -- то на лету можно выяснять тип исходного канала. Если НЕвозможна -- то только конфиг заранее создавать (список бриджуемых каналов, тогда там и маппирование сразу делать (такое-то TANGO-имя маппируется на такой-то foreign-канал)).

    01.09.2024: уже несколько дней обмозговывал саму ту мысль и испытывал сомнения: чисто технологически -- возможно ли?

    • Ведь если эти "динамические атрибуты" должны создаваться ЗАРАНЕЕ (как рассказал Сенченко 25-07-2019 "в конце init'а"), то УВЫ --
    • -- ведь запросы будут приходить из сети и, не находя атрибута, просто отражаться, ...
    • ...т.к. НЕ ВИДНО способа, как бы эти запросы на некие имена могли бы доходить до кода драйвера, чтобы он создавал новые имена.
    • Тут ключевой проблемой -- которая и выглядит непреодолимым препятствием -- является то, что в TANGO список имён (устройства+атрибуты+команды) прописан в БД; и именно БД является "авторитетным источником" (в т.ч. сам сервер может быть и не запущен), и протокол не предусматривает возможности какого-либо динамического резолвинга в ОБХОД этой БД.

      И вообще клиент имеет право сделать DEVICE_PROXY->attribute_list_query() (и подобные) для получения ИСЧЕРПЫВАЮЩЕГО СПИСКА АТРИБУТОВ, после чего, не найдя в этом списке искомого, посчитать "нету такого". Конкретно epics2tango_gw.cpp именно по такой идеологии строится.

    • Ну и отдельный нюанс -- имена: учитывая довольно жёсткий синтаксис "DOMAIN/FAMILY/DEVICE/ATTRIBUTE", весьма проблематично поддерживать EPICS'ные имена, имеющие мало того, что иной синтаксис (':' и '.' -- ну ещё ладно, можно трансляцию из '/' делать), но главное -- ПРОИЗВОЛЬНУЮ глубину "иерархии".

      Разве что считать именем исключительно компонент "ATTRIBUTE", в котором уже могут быть ':' и '.': например, "GATEWAY/TO/EPICS/epics:name:of:record.val".

    • Попробовал гуглить "tango-controls dynamic attribute".
      • "How to add dynamic attributes to a device class" -- что-то типа Mini-HOWTO по данной теме.

        И оттуда как-то не видно способа создавать атрибуты по ходу работы, а только при старте --

        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:
  • 03.11.2020: на zoom-вебинаре "1st Tango Kernel Webinar" задал пару вопросов авторам (ключевым разработчикам) и получил ответы:
    1. A genetal questions: are there still plans to get rid of CORBA completely, as was planned since at least 2015?

      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.

    2. Are there any plans yo add automatic data--type conversion? For example, is some attrubite is 'int', it still could be requested to be read into a 'double' variable without raising an exception. This could increase flexibility.

      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.

  • 20.01.2021: был ещё один zoom-вебинар, "3rd Tango Kernel Webinar", "Tango events implementation in cppTango", где основным докладчиком выступал "one of the original Tango C++ library developers, Emmanuel Taurel".

    Так вот: он рассказал (слайд 11 презентации), что для ZMQ-реализации событий всё равно используется код "представления форматов данных" -- маршаллинга/демаршаллинга -- от CORBA. Причём именно прямо КОД из CORBA-библиотеки, а не просто формат. Тут уж я не удержался и задал вопрос:

    • QUESTION (to page 11): if ZMQ event distribution re-uses CORBA code for data marshalling, then how do you plan to proceed after prospective removal of CORBA from TANGO? Wouldn't this code reuse make de-CORBA'ing impossible? Or is a byte-to-byte-compatible reimplementation of CORBA marshalling planned?
    • Andy Gotz:
      @boikhov this depends on if we want binary compatible on the wire or not. If we do then we would need to only keep the CDR marshalling routines and not the whole library. If we replace CORBA then we will replace the coding on the wire and then we would replace CDR for the events too.

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

  • 20.01.2021: как бы не совсем про TANGO, но всё же...

    (С неделю назад пришло в голову) Я очень не люблю TANGO, однако именно оно -- точнее, какая-то презентация, видимо, Andy Gotz'а, то ли на ICALEPCS-2001, то ли на PCaPAC-2002, натолкнула меня на мысль уметь использовать CORBA, что, в конечном итоге, и привело к идее plugin-frontend'ов.

  • 25.01.2021@утро, мытьё посуды: пришла в голову аналогия насчёт TANGO.
    • Допустим, надо вам построить садовый домик. Чтобы и летом там бывать (и иногда жить/ночевать), и зимой чтоб можно было туда на лыжах придти, отдохнуть, чаю попить, а то и вовсе на выходные.
    • Как поступит обычный человек?

      Оценит свои потребности, прикинет бюджет и построит домик деревянный или кирпичный. И будет им пользоваться.

    • А как поступит человек увлекающийся?

      "Есть такой офигенный материал -- бамбук! Лёгкий, красивый, экологичный, а при постукивании звучит приятно!" И сделает домик из бамбука.

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

      Но домик-то нужен -- и начнёт увлекающийся человек его апгрейдить: обошьёт снаружи вагонкой, понапихает пакли или пенопласта для утепления, воткнёт внутрь реек, чтоб можно было что-то крепить...

      ...однако, хоть бамбука уже почти и не видно, это по-прежнему останется тот же бамбуковый домик, просто чуток пропатченный.

    • Вот TANGO -- как тот бамбуковый домик: CORBA была выбрана не потому, что наилучшим образом подходит для решения задачи, а потому, что это "технология 21-го века"; как раньше и TACO был основан на RPC, потому что оно "leading technologies of the 80's". Да, вот прямо так и написано в "Overview of Tango Controls".

      В результате оказалось, что CORBA для СУ подходит очень фигово, и начали приделывать к TANGO разные костыли, да только уши CORBA до сих пор торчат там отовсюду.

  • 28.02.2021@дорога-к-родителям, около девятиэтажки Ильича-17: ещё некоторая "аналогия" между CX и TANGO -- в передаче не-скалярных данных.
    • В CX ещё с древних (до-UCAM'овских?) времён для передачи не-скаляров предполагалось использовать т.н. "прямые каналы" (англ. "direct channels", DIRC).
      • В т.ч. для больших объёмов -- в первую очередь, осциллограмм.
      • И их же предполагалось применять для всяких сложностандартизуемых операций -- например, для выполнения NAF'ов.

        И даже что-то на эту тему было сделано: имеется файл work/ucam/utest.c от 29-05-1998, содержащий в т.ч. вызов ucam_naf(). Насколько там оно было сделано далее в сервере и транспьютере -- надо отдельно рыть/вспоминать.

      • Об этой вещи -- "трубы", "прямые каналы" -- я высказывался ещё на КМУ (за который получил 2-е место и поездку на CSC-96) и защите магистерского диплома весной 1996г.
      • Как-то оно там вроде работало, хотя и являлось долгое время самой масштабной проблемой.
      • Потом эта идея "прямых" каналов де-факто трансформировалась в "большие" каналы, про которые в bigfile.html про "по минимуму" есть запись от 05-12-2002, что "Сделано -- работают".
      • И в конце концов было решено переименовать "прямые каналы" (Dirc) в "большие каналы" (Bigc), о чём в bigfile-0001.html даже есть запись от 10-01-2004.
    • В TANGO для передачи "данных произвольной структуры" (в точности, как в UCAM с NAF'ами) используются именно pipes, и это считается "рекомендованным вариантом" (в отличие от EPICS4/EPICS7, где есть поддержка прямо произвольных структур).

      01.09.2024: СТОП!!! "pipes" или всё-таки DevEncoded ("тэг(строка)+данные(байтовые)")?

    Т.е.: исходная идея была аналогичной, причём в CX/UCAM оно появилось ещё сильно ранее появления TANGO как такового, но в CX было решено от такого подхода отказаться, в пользу полноценной модели каналов не-скалярного содержания (хотя и огрвниченных моделью "вектор элементов любой байтности плюс массив int32-мараметров), а в TANGO так и оставили этот RPC-образный механизм.

    (Недавно читал статью Бак, Батраков, et al "КОМПЛЕКС ЦИФРОВОЙ ОСЦИЛЛОГРАФИИ УСКОРИТЕЛЯ ЛИУ-20" в ПТЭ-2021-N2, и там на стр.12 tango'вские "pipe" обозваны "каналами" -- вот тут у меня и щёлкнуло: "каналы" <-> "прямые каналы".)

EPICS:
  • 22.12.2017: просто создан раздел, чтоб был раньше Vsystem'ного. Писать пока нечего.
  • 05.06.2019: давно меня мучили воспоминания про то, что в EPICS есть какой-то функционал "interpose layer", и что это может быть мегакруто и более общо, чем CX'ные layer'ы.

    Так вот: понятие "interpose interface" там существует только конкретно в asynDriver (о чём можно почитать в описании "asynDriver: Asynchronous Driver Support), а вовсе не как общая концепция в EPICS. Что подтверждает и гугление -- словосочетания вроде "interpose layer" и "interpose interface" в применении к EPICS встречаются только совместно с asynDriver.

    Так что -- бобёр, выдыхай! :) И механизм layer'ов у нас хорош и адекватен, и sendqlib наш (с его концепцией "портов") обеспечивает достаточную для наших целей функциональность.

  • 26.08.2019: насчёт "создания драйверов под EPICS" (точнее, "driver support"?).

    26.08.2019: немного информации от Лёши Герасёва:

    • Ссылка на презентацию о том, как писать EPICS DeviceSupport: Writing EPICS Drivers (and Device Support).
    • Его мнение:
      По поводу драйвера как сущности в EPICS'е - кажется, её нет. Насколько я понимаю, EPICS оперирует только рекордами, а отображать рекорды в экземпляры драйвера и обратно - это задача DeviceSupport'а, и как его напишешь - так и будет отображаться.

      Весело, да? А ЧеблоПаша настаивал, что "на уровне КЛИЕНТОВ в EPICS драйверов нет". Но их, похоже, в обычном смысле там просто нет вовсе (как я и раньше считал ;-)).

    27.08.2019: вот читаю я эту презенташку, и удивляют 2 вещи (которые надо делать в "configuration function"):

    1. «Create "card" structure.»

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

      Ну это ладно -- вопрос гибкости, да и в CX тоже изначально так было. Да и во времена DOS (когда EPICS создавался) иных вариантов не было.

    2. «Put into linked list.»

      Вот это непонятно совершенно. Схрена ли драйверу (который даже НЕ PnP, как в ядре Linux!) одного устройства нужно знать сразу обо ВСЕХ экземплярах такого устройства?

      Чуть позже, по мере чтения: причина, похоже, в том, что само "ядро IOC'а" менеджментом ресурсов НЕ ЗАНИМАЕТСЯ!

      • Функции-методу "Configure" передаётся некий "адрес" (номер или строка -- имеет смысл только в рамках этого типа), ...
      • ...а она должна проверить наличие этого устройства, аллокировать приватную структуру, прописать там pCard->cardnumber=cardnumber, и поместить структуру в свой список.
      • Затем функция-метод "Open" должна выполнять трансляцию cardnumber->"handle", где "handle" -- это и есть указатель на приватную структуру.
      • И затем всем прочим "методам" передаётся уже оный "handle".

      Какой радикальный контраст с CX'ной моделью, где:

      • "Приватная структура" может аллокироваться сразу ядром сервера, ...
      • ...а также есть назначаемый сервером же devid.
      • И всем методам драйвера передаются оба -- и devid, и privrec.
      • И эта пара выполняет роль "идентификаторов": devid -- "server-side ID", privrec -- "driver-side ID" (такая технология "идентификаторы для каждой стороны", кстати, вполне формализована в EPICS'ном же протоколе Channel Access!).
      • И, тем самым, когда драйвер хочет что-то отдать серверу по своей инициативе, то просто сразу указывает devid, и более никакой магии/трансляции не требуется.

    31.12.2020: что нагуглилось на тему "EPICS driver writer's guide":

    • Writing EPICS Drivers (and Device Support) -- PPT-презентация от Dirk Zimoch за 2007 год.

      Подробная, 73 слайда (плюс "шаги" внутри слайдов). На примере VME.

      (Чуть позже: и да, это на неё давал ссылку Лёша Герасёв полтора года назад.)

    • Epics Driver/Device Support Tutorial от Till Straumann на Epics Collaboration Meeting, Spring 2012.

      Кратенькая, на 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" привело к статейке

    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.

    • 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.
    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".
    (текст скопирован прямо из HTML'ного исходника, с тэгами и плэйнтекстовой "разметкой")
  • 05.09.2019: провёл эксперимент на тему "а сколько UDP-пакетов генерит Channel Access, когда просишь у него несколько каналов: по пакету на каждый канал, или же группирует запросы в 1 пакет?". Ответ: группирует -- wireshark явно показывает имена ВСЕХ каналов (xxx, yyy, zzz) в блоке данных одного пакета (который, в свою очередь, шлётся через постепенно увеличивающиеся интервалы).
  • 20.02.2021: в какой-то момент придётся поглубже разобраться в вопросе "AsynDriver и StreamDevice -- что это такое и каковы их взаимоотношения?".

    20.02.2021: особо углубляться в собственно ЧТЕНИЕ и ИЗУЧЕНИЕ -- почему-то лень, но хоть ссылки на информацию пусть тут будут.

    Гуглилось по 3 фразам: "epics asyndriver tutorial", "epics streamdevice tutorial", "epics asyndriver streamdevice relationship". Сцылки:

    20.02.2021: кстати, МОИ мысли насчёт всего этого безобразия:

    • EPICS был сделан конкретно под VME, что и определило его архитектуру:
      • Всё ЛОКАЛЬНОЕ, с минимальным временем доступа и, видимо, мгновенным исполнением.
      • Потому там использована абстракция "база данных" вместо просто каналов.
      • А надлежащий драйверный API отсутствует.

      Asyn же был добавлен задним числом, в попытке исправить ситуацию, и получился монструознейшей штукой.

      Т.е., все эти навороты -- из-за дикого разбаланса, происходящего от неадекватной обслуживаемому железу модели ПО: работа драйверов (с железом) перемешана с более высокоуровневой ("БД").

    • TANGO же, наоборот, изначально делалась в основном для вряд ли локального железа (подвижки), поэтому драйверный API там как бы есть (хотя и несколько сомнительный). Но в TANGO проблемы с другой стороны -- что там по факту RPC, а не каналы.
    • А CX изначально делался под РАЗНУЮ аппаратуру, потому в нём есть структура с разделением обязанностей: БД каналов -- отдельно, работа с железом -- отдельно. И для разных видов железа возможны разные поддерживающие технологии -- включая sendqlib и fdiolib (поддерживающий и строки), которые к CX вообще никак не привязаны, но вместе позволяют очень просто организовывать взаимодействие с удалёнными, асинхронными и в т.ч. текстовыми устройствами/протоколами.
  • 07.03.2021: еще немного по "истории EPICS". Наткнулся на это всё, гугля на тему "epics gtacs download" -- пытался найти исходники старых версий, а в идеале и GTACS.
    • С исходниками глухо: самое старое, что есть на странице "EPICS Base Downloads" -- это R3.12.1 от Mon, 10th Jul 1995.
    • 30.04.2023: а вот на странице "EPICS Base" на https://epics-controls.org/ есть ссылка на "Base Release 3.11", но там исходников нет, а только Release Notes. .

    Но также нашлись тексты:

    • "Status and design of the Advanced Photon Source control system", W. McDowell, M. Knott, F. Lenkszus, M. Kraimer. N. Arnold, R. Daly, PAC 1993.
    • Оттуда ссылка на |||J. O. Hill, "Channel Access: A Software Bus for the LAACS," in Accelerator and Large Experimental Physics Control Systems, D. P. Gurd and M. Crowley-Milling, Eds., ICLAEPCS, Vancouver, B. C., Canada, pp. 288-291,1989.|||
    • а из Vsystem'овской статьи MOPHA042 на ICALEPCS-2019 нашлась ссылка на |||A. J. Kozubal et al., "Run-time Environment and Applica-tion Tools for the Ground Test Accelerator Control Sys-tem," in Proc. Int. Conf. on Accelerator and Large Experi-mental Physics Control Systems, Vancouver, British Co-lumbia, Canada, 1989, pp. 288-291.|||
    • ICALEPCS-2019 MOCPR02 "The EPICS Collaboration Turns 30", L.R. Dalesio, A.N. Johnson, K.-U. Kasemir: статья, слайды презентации.

    А ещё по ходу встретились пара статей, оказавших в своё время большое влияние на осознание мною того, как бы должна быть устроена СУ в условиях со-существования разных СУ и необходимости взаимодействия между ними. Это не столько про EPICS, а скорее "по СУ вообще", но, поскольку история, то оставим здесь:

    • PCaPAC 2002, "The Babylonization of Control Systems" -- совместная презентация P.Duval, M.Plesko.

      Это там есть картинка с "покрытием" разных уровней СУ в разных фреймворках.

      (На неё я даже в кандидатской диссертации ссылался.)

    • ICALEPCS 2003, TH513 "The Babylonization of Control Systems Part II - The Rise of the Fallen Tower" -- те же авторы.

      Оно было на ICALEPCS в Южной Корее, куда я не смог поехать (и сайт конференции издох ещё в середине-конце 2000-х).

    Кстати, есть некая публикация

    01.04.2022: и ещё чуток условно исторического -- найдено в процессе разбирательства на тему "что есть GDD и что с ним стало?" и "что такое Data Access и как оно соотносится с pvAccess?":

    • "Using Data Access, First Impressions" Kay Kasemir, October 2005.

      Попалось при гуглении про «"epics" "data access"» (да-да, без кавычек -- никак: всё фигню разную подсовывает).

      Рассказ о первых впечатлениях про т.н. "Data Access" -- то, что потом превратилось в pvAccess. Описываются некоторые базовые принципы и подходы. Похоже, оно всё вусмерть объектное -- там прямо сам принцип реализации "полиморфные методы assign(), reveal(), ...; плюс функция-обходчик PropertyCatalog::traverse()". Ключевое слово для описания концепции было бы "introspection", хотя в тексте оно и отсутствует.

    • 04.04.2022: "Data Access Update" Jeff Hill, 10-12-2004 (более ранний, чем предыдущий документ), 30 слайдов плотного текста.

      Нагуглилось по «site:epics.anl.gov "data access"».

      • Первые слайды ценны тем, что приводится аргументация "зачем нужна такая передача СТРУКТУРИРОВАННЫХ данных": "Intelligent devices require message passing". (Ну да, очень похоже на наши соображения о больших каналах ещё во времена CXv2.)
      • В разделе "Common Misconceptions" на стр.30 указано, что "This interface isn't compatible with C" является заблуждением, т.к. "All of the interfaces described here couldhave C, java, python have C, java, python ... wrappers".

      Но как именно всё же работает маршаллинг -- мне так и не стало ясно (т.е., как все эти вызовы преобразуются в байтовый поток в сокет).

    • "Introduction to EPICS 7" Andrew Johnson, 2020-02-20.

      Попалось при гуглении про «"epics" "data access"».

      • Describe EPICS V4 in six words: "V4 added structured data to EPICS".
      • Ну и много-много про то, ЗАЧЕМ это всё сделано и, "на эмоциональном уровне", примерно понятно ЧТО оно умеет делать: 1) чтение/запись произвольного набора полей (даже разных рЕкордов); 2) при таких чтении/записи на время операции лочатся все вовлечённые рЕкорды.
      • И ещё там есть такая фраза (слайд 7):
        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-х.

    Ну и я не удержался и написал ему письмо с вопросом

    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:4a3da69-1799-84dc-43ad-146aaea06f@starnew.inp.nsk.su) не попало, поскольку, похоже, было отфильтровано ANL'ем).

    И Боб ответил (Message-ID:CAKc=vhn_VyH5cEPp7oYyfBE4t+9koATCkCO5o6v8wmdktpfxiQ@mail.gmail.com),

    1. Текстом -- про публикации (bold мой):
      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":
      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.
      

      Т.е. (выводы от меня):

      1. Хотя и приведено определение "a value of some data type along with the time stamp and alarm status of that data" -- IMHO, абсолютно корректное (я пришёл к тем же выводам!), но реально в Channel Access timestamp и status вовсе НЕ обязательны, а в толпе разных DBR_-вариантов данные могут присылаться и без этих значений, плюс там группа исключений из общей матрицы есть (dbr_ctrl_string отсутствует и нет никакого DBR_xxx_STRING с полем "units").

        Т.е., IMHO, косяк в НЕотделении "свойств" (всякие units, range, ...) и "состояния"; и явного признания, что это мисдизайн, я никогда не встречал.

      2. А вот насчёт "заточенности под VME" -- видимо, я был неправ.

      Ну и слово "Foxboro" для меня новое. Погуглил --

    2. плюс приаттачив PDF статьи 1989 года
    3. и приведя длинный список публикаций по теме (в т.ч. ранних лет).

    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 может всё объяснять.

    08.03.2025: обобщить бы RegisterWithFDIO() -- он уже сейчас в 3 местах, и из них как минимум в 2 (_lyr.c и _mon.c) по определению содержит идентичный код (в _tcp_drv.c большая часть избыточна, но и фиг с ним) -- т.к. оба на основе modbus_conv.h. Вопрос только в том, КУДА поселить это обобщение: вроде бы напрашивается именно modbus_conv.h, но он не должен иметь зависимости от fdiolib, а так она появится... 11.03.2025: учитывая появившееся желание "уметь отступать от ограничений Modbus, позволяя больше значений и большие пакеты", обобщение становится ещё более актуальным.

    27.03.2025: проверка Modbus-ASCII показала, что в ИСХОДЯЩИХ пакетах LRC считалась некорректно -- без учёта UNIT_ID. Вследствие того, как в modbus_create_req_packet() устроено складирование данных в исходящий пакет: UNIT_ID не входит в PDU, а кладётся отдельно; сумма же считается из 2 кусков. Добавление параметра sum0 (в который передаётся UNIT_ID отправляемого пакета и которым инициализируется сумма) ситуацию исправило, коммуникация заработала. Но выглядит это всё криво -- надо бы как-то архитектуру улучшить.

    27.03.2025: записать в раздел "не сделать ли период реконнектов настраиваемым", что для ВИП-45/ВИП-48 это может быть актуально, т.к. там при отсутствии запросов интерфейсу делается down и через ~1-1.5sec обратно up, и он остаётся включенным ~5sec; так что если вдруг период реконнектов совпадёт с периодом up/down и первая попытка подключения попадёт в фазу "down", то установление соединения может растянуться на длительное время. 29.03.2025: при подключении точка-точка патч-кордом проблема не возникает, видимо, потому, что у ядра Linux хватает ума не пытаться слать пакеты в отключенный интерфейс и они отправляются сразу после включения; а вот при подключении через коммутатор информации об отключенности интерфейса ВИПа не будет и пакеты будут отправляться и теряться.

    29.03.2025: давно напрашивается мысль так модифицировать определение IST'ов -- MAGX_IST_CDAC20_DEV() -- и прочих в devlist_magx_macros.m4, чтоб там сразу делался cpoint на подстилающее устройство, вроде "cpoint $1.hw_dev $2", чтоб можно было в инженерных скринах вроде ist_cdac20.subsys и исходное устройство показывать, как в subsys_magx_macros.m4'шных панелях.

    31.03.2025@по дороге в ИЯФ, по лестнице у ИЦиГ после мыши: давно напрашивается сделать, чтобы по завершении modbus_mon'а возвращался бы осмысленный exitcode, а не всегда 0. Например, статус исполнения последней операции: 0: OK, 1: exception, 2: timeout. 01.04.2025: сделал. Предварительное действие -- в console_mon_util.h добавлен код EC_TIMEOUT. Введена last_known_ec, в которую записывается EC_-код последнего "результата" исполнения, а финальный return теперь её и возвращает. Плюс, finish_proc() -- которая отрабатывает ключ "-T" -- тоже теперь генерит EC_TIMEOUT.

    31.03.2025: а не добавить ли конверсию "BOOL" -- чтоб любой не-0 от хоста превращался бы в 1? У Диком'овского ИП-МРН20 куча регистров булевских. ...хотя, чуток поразмыслив -- неа, наверное, не стоит: пусть это будет заботой источника значения; поди уж справятся.

    31.03.2025: modbus_mon: сделал ключ "-Dy" -- для Modbus-TCP выдавать ещё SYNC_ID. Инфраструктура была подготовлена заранее -- все нужные if()'ы при печати были, просто в них везде стояло 0 вместо закомментированного print_sync_ids, каковой сейчас и сделан и раскомменчен.

    31.03.2025: проверил работу записи конверсии FLOAT16_10E6M10 -- фигвам: не принимает ВИП-48 эти числа, ругается "EXCEPTION 3:ILLEGAL_DATA_VALUE". Если же записывать просто те же коды, что читаются из HOLD:0/2, то принимает. Похоже, дело в том, что это недоразумение требует конкретный вариант кодировки -- с конкретной экспонентой. Прямо https://www.youtube.com/watch?v=IYtVFNhDdVo в чистом виде...

    07.04.2025: после наблюдения той дичи напрашивалась мысль поменять алгоритм в 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].

    08.04.2025: ещё разок взглянул на код и на добавленную диагностическую выдачу. Что имеем: при формуле f32_man = f32_val / powf(10, f32_exp); результирующее значение ВСЕГДА будет в диапазоне от 1.0 до 9.9(...), просто по определению. Значит, для получения желаемого девайсом диапазона [10..99] надо делать просто ВСЕГДА домножение мантиссы на 10 (с декрементом экспоненты). @вечер, уже лягши в постель, но не удержался: сделал -- от всех 3 сдвигов оставлен только последний вариант, который как раз сдвигает на 1 десятичный разряд.

    09.04.2025: проверил на живом ВИП-48 -- да, работает.

    31.03.2025: в modbus_mon.c "поддержка" UNLIMITED_MODBUS была реализована крайне сыро: 1) в SendNextCommand() оставался buf[256] -- который при попытке записи будучи передан в modbus_create_req_packet() переполнился бы; 2) в ProcessInData() аналогично m_data[256] -- который при сбагривании в modbus_decode_rpy_packet() наверняка переполнялся.

    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 или номер сервера). Чтоб хоть диагностика была. Посмотрел в коде 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 уже сообразил.

    17.04.2025: надо разбираться с тем, как устанавливать COM-порту скорости, отличающиеся от стандартных. Нужно в интересах драйвера bondarenko_ubs_drv.c -- там требуется скорость 125000. Некоторое время назад при поиске чего-то другого встречал ключевое слово "termios2" с комментарием, что оно не сочетается с обычным "termios", поскольку 2 разных .h-файла, пересекающиеся по именам и потому их нельзя #include'ить вместе.

    Гуглим по 2 вариантам:

    1. "termios2" https://sources.debian.org/src/picocom/3.1-4/termios2.txt/ https://www.linux.org.ru/forum/development/8982485 https://stackoverflow.com/questions/37710525/including-termios-h-and-asm-termios-h-in-the-same-project https://stackoverflow.com/questions/12646324/how-can-i-set-a-custom-baud-rate-on-linux https://github.com/npat-efault/picocom/blob/master/termios2.c
    2. "linux non-standard serial baud rate" https://groups.google.com/g/atlanta-linux-enthusiasts/c/w6M9ukRqmgs?pli=1 https://support.connecttech.com/hc/en-us/articles/4416820936347-Setting-a-Custom-Baud-Rate-in-Linux-2-6-x-Kernel https://raspberrypi.stackexchange.com/questions/47461/how-to-set-non-standard-serial-port-speed https://stackoverflow.com/questions/19440268/how-to-set-a-non-standard-baudrate-on-a-serial-port-device-on-linux https://github.com/nix-rust/nix/issues/1376
    drivers/usb/serial/ftdi_sio.c

    Вот только "grep -rw B38400" по исходникам kernel-3.10.0-514.2.2-1.el7 (это от CentOS-7.3) не находит внятного использования оного B38400, а касательно "alt_speed" ещё грустнее -- есть ПРИСВОЕНИЯ ЕМУ, а вот ИСПОЛЬЗОВАНИЯ его значения не видно совсем.

    21.04.2025: grep по /usr/include/ показывает, что там слова "alt_speed" нигде нет (CentOS-7.3).

    22.04.2025: и возник вопрос -- а откуда я вообще откопал это "alt_speed"?

    Рытьё/поиск по ссылкам показало, что в одном-единственном месте, где оно как раз и выглядело крайне привлекательно -- "How to set a non-standard baudrate on a serial port device on Linux?" на StackOverflow 13/18-10-2013, где речь о том, как это делается в ДРАЙВЕРАХ.

    Таким образом, для userspace слово "alt_speed" вообще иррелевантно.

    И надо смотреть на termios2, а то с B38400 всё очень уж через одно место -- там надо руками делитель указывать, вместо желаемой скорости.

    21.04.2025: для проверки набросана work/tests/ttycat.c -- она пересылает байты между stdin/stdout и указанным /dev/tty* и предполагается запускать её в количестве 2 штук на соединённых друг с дружкой адаптерах. Пока установка скорости не сделана, но предполагается именно туда запихивать разные способы, для тестирования: на паре соединённых адаптеров будет видно, работает ли.

    22.04.2025: со стандартными уставками утилитка проверена, на пере соединённых адаптеров -- да, работает. Показателен эксперимент с B50 -- прям видно, как символы передаются.

    24.04.2025: добавлена также и поддержка нестандартных скоростей -- отдельным файлом set_tty_speed_via_termios2.c, содержащим set_tty_speed_via_termios2(). Проверено -- да, разные скорости работают. Странность в том, что если указывать скорости ниже 50, то реально работает что-то более высокое (но медленное, т.к. заметно). Но что такая установка скорости действительно работает, видно по тому, что если поставить на обоих сторонах скорость 76, то символы пересылаются верно, а если на одном сделать 77, то идёт мусор.

    25.04.2025: set_tty_speed_via_termios2.[ch] скопированы в hw4cx/drivers/eth/ и поддержка нестандартных скоростей добавлена в modbus_mon.c и modbus_lyr.c, с надлежащими проверками на доступность этой функциональности (код в них практически одинаковый). Для указания служит параметр "speed=BAUDS", парсится он в поле comopts_t.speed, а используется его значение, если оно >0.

    21.04.2025: идея о том, как можно было бы реализовать парсинг скорости линии в предположении, что устанавливается именно через alt_speed: сделать оное alt_speed отдельным параметром в PSP-таблице, чтоб можно было указывать в стиле "speed=125000,8,n,1". Тогда перед выставлением скорости после открытия линии будет проверяться в стиле

    if (modbus_comopts.alt_speed != 0) modbus_comopts.bauds = B38400;

    22.04.2025@утро, зарядка: а если скорость по умолчанию сделать 38400, то и отдельных действий никаких выполнять не надо будет. @по дороге в ИЯФ, проходя мимо мыши, ~09:50: но лучше всё-таки проверять и при не-нулевом значении speed выдавать warning и форсить B38400; кстати, для "красивых" скоростей вроде 125000, 250000 и 500000 можно было бы сделать короткие ключи -- прямо в виде этих чисел, чтобы соответствующие числа в поле alt_speed прописывали.

    18.04.2025: посетила идея, как можно Tango'вские 2-мерные данные -- Tango::IMAGE -- хотя бы читать (касается и cda_d_tango.cpp, и epics2tango_gw.cpp): возвращать их 1-мерными массивами, обычным/очевидным/стандартным образом, просто складывая строки друг за дружкой (20.04.2025: а если способ хранения в памяти позволит -- то вообще просто беря данные из памяти как есть; правда, на то надежды мало: боюсь, внутренним типом окажется vector<vector<T>>, который вряд ли хранит содержимое линейно/последовательно). Ну и размер отдавая как "dim_x*dim_y".

  • 26.04.2021: у EPICS'а есть своя "боль", как есть они и у CX (RD-конверсия client-side, перепутанное "направление" у R и D) и у TANGO (CORBA, отсутствие авто-конверсии типов, ...). Называется эта боль -- ограниченный набор типов данных вскладчину с системой рЕкордов:
    • Цифровой/двоичный В/В там лишь 16-битный -- вот такой подстилающий тип у mbbi_direct и mbbo_direct; а никаких "MBBI_DIRECT32" и "MBBO_DIRECT32" не существует (не говоря уж о 64-битных).
    • И в EPICS7 ситуация, похоже, НЕ улучшилась (по "mbbi_direct32" нагугливается только одна-единственная страница Bug #1777985 "Release notes for 32-bit mbbi/oDirect are wrong", а по "mbbo_direct32" вообще ничего). 27.04.2021: ясно почему -- подчерк лишний.

    ...так что поддерживать CURVV с его 24-битным входным регистром, как и DL250 с толпой 24-битных регистров управления режимами и блокировок в EPICS просто не удастся (сложно -- можно :D).

    Это в дополнение к тому, что в EPICS3 нет не только int64, но также нет беззнаковых типов.

    27.04.2021: продолжение о бинарном В/В:

    • С другой стороны -- в "Multi-Bit Binary Input Direct Record (mbbiDirect)" от R7 сказано
      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.

      Но, если верить документации, это ТОЛЬКО для входных -- "Input"; выходные же -- "Output" -- по-прежнему 16-битные: в "Multi-Bit Binary Output Direct Record (mbboDirect)" сказано

      The mbboDirect record performs the opposite function to that of the mbbiDirect record. It accumulates bits (in the fields B0 - BF) as unsigned characters

      "Во тупы-ы-ые!!!".

    • Но это тоже не факт, т.к. нагугливается (по "mbbi_direct 32-bit") толпа ссылок:
    • Кстати, правильные названия -- "mbbiDirect" и "mbboDirect", безо всяких подчерков. Так гуглить вроде правильнее.

    Кстати, а корень этой проблемы -- именно в самой системе рЕкордов. Переделать из 16-битности в 32-битность так сложно потому, что вместо простого драйверного API с передачей данных между драйверами и "ядром сервера" имеются развесистейшие потроха (в виде этих разнородных рЕкордов). Если б была просто передача данных, то и просто целочисленный ввод (и вывод) был бы совместим с "бинарным"/регистровым; и при надобности уж внутри сервера можно б было раскладывать результат хоть в 8, хоть в 16, хоть в 32 бита, и при надобности делать преобразование данных (как в CX и сделано).

  • 26.10.2023@утро, ~08:20: (пытаясь разобраться, умеет ли caRepeater "размножать" UDP-пакеты не только по клиентам, но и по серверам; в документации -- не видно): встретилась чёткая формулировка того, что такое "old" и "new" сервера: в сообщении "Re: Another Dual NICs question" за 21-03-2007 от Ralph Lange:
    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).
    

    А встретилась ссылка туда в сообщении "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. Искать в архиве.

Vsystem:
  • 22.12.2017@защита-докторской-Карнаева: А что сейчас с V-System?

    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 за сегодня (чтоб все ответы из той переписки были рядом).

  • 07.03.2021: в bigfile-0001.html была ссылка на статью "A Comparison of Vsystem and EPICS", весьма небезынтересную. Именно там мне встретилась фраза "Vsystem has a unified API for both local and remote access", послужившая толчком к созданию cda_d_insrv.c.

    Но ссылка вела на anl.gov и с тех пор протухла. Гугление эту статью найти позволяет -- например,

    Также по ходу дела встретилось:

    • ICALEPCS-2019 MOPHA042 "Evaluating VISTA and EPICS With Regard to Future Control Systems Development at ISIS" by I. Finch, ISIS, Rutherford Appleton Laboratory, United Kingdom.

      У них СУ работала под Vista с середины 1980-х (~35 лет), пережила пару смен серверного железа и ОС, а сейчас они сделали бридж Vista<->EPICS через MQTT на Python, но подумывают о полном переезде на EPICS.

MQTT:
  • 02.10.2018: хоть MQTT/MQTT-SN и не фреймворк СУ, а скорее просто протокол обмена данными, но он имеет функционала достаточно для построения простой СУ. Так что заслуживает описания тут.
  • 02.10.2018: некоторые мысли.

    02.10.2018: ЧеблоПаша сейчас мучается с поиском решений для максимально простой реализации поддержки MQTT в EPICS.

    Некоторая поддержка есть в CSS, но там она своя, а "в EPICS вообще" -- нету.

    02.10.2018@после обеда, дорога из дому в ИЯФ, около НИПСа: очень удобно б было иметь "шлюз" в виде псевдодрайвера, который бы просто маппировал MQTT'шные имена/каналы на CX'ные. Чтоб можно было его использовать как-нибудь вроде

    dev some_ps senkov_ps/mqtt_gw ~ -

    Вопрос только: а как МАППИРОВАНИЕ-то делать? Нужна ведь некая карта соответствия между MQTT'шными именами и номерами каналов.

    И тут идейка:

    • А пусть этот mqtt_gw_drv умеет самостоятельно лазить в "конфиг сервера" -- через CxsdDbFindNsp() находит нужный devtype-namespace (через GetDevTypename() узнав имя типа), и потом делает таблицу соответствия.

      Естественно, таблицу эту составлять надо в init_d(), к тому же там надо будет заготавливать имена (у нас имена с разделителями '.', в MQTT -- '/', плюс там еще префикс имени устройства).

    • Также из "конфига сервера" (точнее, БД/hw) драйвер может узнавать свойства каналов -- скалярность/векторность, целочисленность/вещественность, r/w.

    03.10.2018: ага, начато -- скелет сделан, потихоньку наполняем _init_d().

    Только названо оно mqtt_mapping_drv.c, а не "mqtt_gw".

    ...к вечеру: "мясо" создания таблицы маппирования в _init_d() сделано, теперь надо проверять. Сделаем этому драйверу свой раздел и далнейшее будем описывать там.

  • 29.11.2019: позвонил Роговский, и в разговоре выяснилось, что у них в принципе есть потребность работы с MQTT.

    Причём -- чтоб оно было доступно из cda, через какой-нибудь "cda_d_mqtt".

    Смысл:

    • У них есть некоторое количество железа (BPM'ы?), генерящего изрядные объёмы бинарных данных (~32КБ).
    • У них много разного народу, пишущего свой софт для СУ.
    • И конкретно СЕЙЧАС они пользуются CAS для обмена данными, но тот-то рассчитан на передачу строк.

      Поэтому для передачи многокилобайтных бинарных данных их придётся гонять через строковое представление (например, BASE64). А это изрядная потеря производительности.

    • А вот если бы железки можно было перевести на MQTT-SN (т.е., по UDP) и пользоваться какой-нибудь стандартной библиотечкой и брокером (Mosquito?), то вся эта толпа народу спокойно работала бы с такими железками. Потому как есть прямо готовый биндинг для python.

      ...а то cx'ом этому народу пользоваться сложно (или западло?).

    • А Роговский мог бы к таким железкам доступаться напрямую через cda.

    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.

LXI:
  • 16.10.2018: тоже не framework, а всего лишь протокол, но также потенциально нуждается в поддержке в CX, а следовательно -- в знаниях о нём.

    Раздельчик создаём после сегодняшнего техсовета на ВЭПП-4, где Лёша Герасёв рассказывал об удачной попытке запинать поддержку в EPICS.

  • 16.10.2018: даже и не знаю, что написать тут в заголовке; опять "некоторые мысли"?

    16.10.2018: итак:

    • Разговоры об LXI касательно железа для ВЭПП-4 шли давно.
    • LXI -- текстовый протокол, так что с устройством можно общаться просто посредством telnet.
    • Какая-то поддержка вроде как в EPICS есть. // Переспросить у ЧеблоПаши!!!

      17.10.2018: Переспросил. Некая liblxi, как утверждается, "...is based on the VXI-11 RPC protocol implementation which is part of the asynDriver EPICS module...".

    • Сейчас в EPICS предполагается работа через стек/связку AsynDriver+StreamDevice.

      Но проблема вроде как в том, что в некоторых случаях девайс отвечает не текстом, а байтовым потоком -- в частности, при передаче вэйвформы.

      • А Asyn+StreamDevice вроде как были рассчитаны только на текстовый обмен (там как-то табличку можно задать, что по такому-то ответу (regexp'ом указывается, что ли) сделать то-то), и бинарность представляла проблему.
      • Но Герасёв всё же разобрался, что как-то там можно и с бинарными данными работать -- прямо всё уже готово.

      17.10.2018: ещё чуток поспрашивал Герасёва -- вот результаты:

      • Коннектами/реконнектами заведует именно Asyn. А StreamDevice -- это просто штука, умеющая дешифрировать текстовый поток.
      • Резолвинг имя->IP, похоже, выполняется непосредственно перед коннектом (выяснено по результатам просмотра исходников).
      • Собственно резолвинг выполняется собственной функцией, с комментарием, что "нельзя использовать gethostbyname(), т.к. она не-thread-safe".

        Но внутри той функции используется именно gethostbyname() (т.е., НЕ неблокирующийся вариант), просто окружённый блокировкой (семафорами?).

    • Там же на техсовете ВЭПП-4 был Беркаевым задан вопрос: а как в прочих СУ -- TANGO и CX?

      Насчёт TANGO никто из нас не знает (ЧеблоПаша вообще заявил, что его танго танцевать не учили -- тут я его уел, что учили, в июне 2017-го несколько занятий).

      А насчёт CX -- мой ответ что "проблем не будет".

    • Очевидно, что раз эта штука текстовая -- ну так драйвер делается на основе remdrv_drv.c, только с заменой FDIO_STREAM на FDIO_STRING.

    16.10.2018@вечер-дома: некоторые мысли:

    1. Похоже, этот EPICS::StreamDevice содержит функционал, аналогичный remdrv'шной логике коннектов/реконнектов/...

      Аналогичные remdrv'шным "мозги" могут потребоваться для mqtt_mapping_drv.c, LXI'ного драйвера и т.д.

      Вывод: а мож как-нибудь ту часть -- с коннектами/реконнектами -- из remdrv вытащить и "обобщить"?

      17.10.2018: ага -- еще ведь hw4cx/drivers/eth/triadatv_um_drv.c (именно на базе remdrv_drv.c), при создании которого была сделана попытка обобщить код на будущее, создав "шаблон" _tcp_string_drv.c.

      • Но часть драйвером может работать на fdiolib, а другой части может потребоваться cxscheduler напрямую.

        Делать поддержку и того, и другого (с отдельными полями для handle и fdh), чтоб клиент указывал желаемый механизм?

      • Плюс, как минимум с LXI будет иная проблема: там бывает передача части данных в бинарном виде, хотя основной обмен идёт текстовым потоком.

        Отсюда следующая идея (п.2):

    2. Дать возможность указывать FDIO_STRING-дескрипторам "а теперь прочитай столько-то байт бинарных данных".

      Дальнейшее обсуждение этого вопроса будем вести уже в разделе fdiolib'а.

    19.10.2018: FDIO_STRING-то фокусу с бинарными данными обучен, но пользы от того не будет.

    • Протокол LXI сделан бестолково: там бинарные данные прут прямо после текста (точнее, сразу после текстовой спецификации длины), БЕЗ перевода строки. Примерно так:
      #800001234<...тут 1234 байта данных>\n

      Здесь '8' -- число цифр в спецификаторе длины, "00001234" -- та самая длина, а потом, без каких-либо разделителей, прут эти данные. Что интересно -- после данных всё-таки идёт перевод строки, зачем-то.

      (Это проверено на осциллографе Agilent (МОДЕЛЬ?!).)

    • Есть некоторая непонятка: указывается, что для поддержки LXI необходимо поддерживать протокол VXI-11. НО: "VXI" -- это "VMEbus eXtention for Instrumentation".

      Спрашивается: как протокол от крейтовой системы с 32-битной шиной может быть частью протокола, основанного на байтовом потоке (идущем по TCP, а сам протокол унаследован от GPIB, с 8-битной шины со строковыми данными)?

  • 30.11.2019@8-ка-в-город ~11:00, едучи по Строителей, уже перед самым поворотом на Бердское: если всё-таки делать свою собственную реализацию библиотеки для работы с LXI, то, по аналогии с libmqtt4cx, надо её делать на основе fdiolib+cxscheduler'а, а называть -- liblxi4cx.
VME:
  • 05.06.2020: создаём раздельчик -- давно пора, для складирования попадающейся информации.
  • 05.06.2020: попалась презентация "Introduction to VME" от INFN'а (плохо показывается Firefox'ом -- белый текст на белом фоне; evince OK). Там же, кстати, в конце и про CAMAC дофига написано.

    Дата последней модификации -- 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) не динамическое маппирование, а СТАТИЧЕСКАЯ конфигурация, с такой аргументацией:

    • Using a static set-up has the advantage that one can not run out of map decoders in the middle of an application
    • This policy enforces some discipline and is therefore not liked by everybody. We are, however, convinced that it helps to reduce problem

    Кстати, некое описание егойного 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 ядра часто меняется, а для экзотического процессора никто не рвётся всё обновлять.

  • 06.06.2020: ещё ссылка, на официальный документ от VITA -- там и рядышком что-то ещё должно быть: "VMEbus Address Modifier" ("VMEbus Address Modifier Codes Under VME64x").

    Кстати, из того текста следует, что в 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: и ещё немножко разрозненной информации.

    06.06.2020: пообщался с Павленко, спросив его пару вещей и получив ответы:

    1. Насчёт "что за смысл в разноразрядных циклах IACK -- 8, 16, 32 бита?".

      Ответ: а хбз -- всегда в железе вроде именно 8 бит.

      Но, памятуя, что "interrupt vector" в M68K -- это просто НОМЕР прерывания, который умножался (на 4?) для получения адреса ячейки, где содержался адрес процедуры-обработчика, идея о 16- и 32-битных векторах выглядит бредово (возможно, просто некое уже VME-специфичное расширение для передачи дополнительной информации?).

    2. Насчёт "а что за адресация D08(O) и D08(EO)" (Odd и Even/Odd)?

      Ответ: причина в том, что на адресной шине 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").

  • 05.02.2021: предпринята попытка разобраться с географической адреcацией.

    Побудительным мотивом стал абзац

    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":

    • Презентация "An introduction to VMEbus" содержит...
      • На слайде 15 краткое описание:
        • The base address of a slave can be set:
          • Mechanically: on-board Jumpers, DIP switches
          • By S/W: VME64x geographical addressing, CR/CSR
      • И целый слайд 25 "25CR/CSR space access and geographical addressing":
        • "Classic" VMEbus slaves use on-board jumpers or switches for the initialization of the base address and the interrupt vectors
        • The VME64(x) standard proposes a S/W based mechanism (plug-and-play) The basic principles are:
          • Each slave has a special window of 512 kB consisting of a Configuration ROM (CR) and a Control and Status Register (CSR) section
          • Access to this window is in A24 mode with AM=0x2f only
          • The address of that window (BAR) is either set by jumpers (VME64) or derived from the slot number (geographical addressing, VME64x) with the formula: address = slot# * 0x80000
          • The CR/CSR space contains many (mostly optional) features to specify and control the functions of a slave board
          • Slave boards are identified by a manufacturer + board ID stored in the CR. Board IDs have to be unique (http://cern.ch/boardid)
          • The most important CSR space registers are the eight ADER registers. They are used to define the base address(es) of the main function(s) of the slave.
    • Дата самой презентации неясна, но, судя по упоминанию как "недавних" событий конца 1990-х, а также контроллеру Concurrent Technologies VP110 на Pentium III -- где-то начало 2000-х.

      07.02.2021: Page Info говорит дату модификации "April 24, 2008", а так -- это явно более ранняя версия презентации Markus Joos, нарытой 11-06-2020.

    • И там, в частности, на слайде 36 упоминается "The Tundra Universe chip is used on most current SBCs but has a number of limitations:"
      • There is no H/W byte swapping (required on little endian CPUs). The VP110 has extra logic for that purpose
  • 06.02.2021: немножко информации про бридж, используемый в Kontron'овских контроллерах -- ALMA.

    06.02.2021: так вот:

    • Он называется "ALMA2f", и вроде везде утверждается, что это собственная разработка Kontron.

      Но по нему документации найти не удалось, совсем.

    • Только чуток в описании PowerPC'шного VM6250 -- "6U VME PowerPC SBCUser's Guide" -- там на стр.26 есть раздел "2.3 ALMA2f PCI to VME Bridge" с описанием фич бриджа (но без деталей).
    • Также при этом случайно нарылось на сайте PowerBridge описание на нечто под названием ALMA2e -- "ALMA2ePCI/VME 2eSST Bridge" User Manual от IBM, Preliminary Version 0.3 Dec 10, 2003
    • Так вот -- оно ОЧЕНЬ похоже (блок-схема почти идентична оной в VM6250), и в них обоих упоминается "ALMA_V64" как предшественник. Складывается впечатление, что история этой троицы такова:
      • ALMA_V64 -- PCI (?), когда-то сделан, видимо, IBM'ом.
      • ALMA2e -- тоже PCI, тоже от IBM.
      • ALMA2f -- уже PCIe-вариант, похоже, к этому времени дизайн перешёл к Kontron.
    • 07.02.2021: из чтения документации на ALMA2e как будто следует, что там аж 1k элементов в таблице маппинга -- "PCI to VME access (1K-Entry Mapping Table)".

      Т.е., аж 1024 "окна"? Ну так это бы полностью решало проблему окон -- не нужно никакого динамического ремаппинга! Собственно, обычно хватило бы, например, даже 32 окон -- чтобы заведомо превышало максимально возможное количество модулей в крейте. А 8 -- это какой-то стыд...

    08.02.2021: ещё чуток нарытой информации:

    • Да, ALMA_V64 -- продукт IBM, судя по упоминаемому в других Kontron'овских описаниях "ALMA_V64 . PCI to VME Bridge Data Sheet, IBM, March 1998" (к сожалению, этот документ нарыть не удалось).
    • Статья "Solving Vendor Lock-in in VME Single Board Computers through Open-sourcing of the PCIe-VME64x Bridge" -- TUAPL03 на ICALEPCS-2017.

      Там упомянут MEN A25 (видимо, это название чипа), а Тундры -- старая Tundra Universe II (PCI?) и "новая", discontinued, TSI148.

  • 28.06.2021: кстати, насчёт MEN A25 -- пора бы для него свой раздельчик завести для записывания находимой информации.

    28.06.2021: так вот:

    • "Status and plans for the MEN-A25 VME CPU" -- презентация на ICALEPCS-2017 (явно связанная с той статьёй про "Solving Vendor Lock-in...").
    • PCI-Express to VME bridge -- страница с "исходниками" бриджа (VHDL для FPGA?).
    • A25 - Embedded Single Board Computer with Intel Xeon D -- страница продукта на сайте конторы Duagon.

      Там же теоретически есть возможность скачать драйвер и документацию, но мало того, что для этого надо регистрироваться, так даже и после регистрации просто так не дают -- нужно заполнить форму "А вам зачем?" и потом, *возможно*, позволят.

      27.11.2021: ещё тогда, летом, зарегистрировался и попросил -- фиг, ничем не закончилось: запрос перекинули в российский офис их партнёра, оттуда какой-то чел что-то мне писал, но в какой-то кривой кодировке (никак не дешифрируемой), я ему пытался об этом сказать, но без толку; так это и заглохло.

    27.11.2021: пара аспектов, незамеченных мною тогда при чтении TUALP03 "Solving...":

    1. Там реализована ТОЛЬКО базовая функциональность VME плюс блочные пересылки (раздел "FPGA IMPLEMENTATION") и CR/CSR:
      • "VME single cycles: A16, A24, A32 with any of the D8, D16, D32 data widths" и "VME block transfers: A16, A24, A32 with any of theD 8, D16, D32 plus the A32D64 multiplexed blocktransfer (MBLT)".
      • a special type of A24 access to read and write the CR/CSR
      • However, none of the fast transfer modes (2eVME,2eSST) is currently implemented.

      Т.е., ни 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 не реализован...

    2. И ещё там есть вот такая табличка (стр.5=135):
      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
      
      из которой вроде бы следует, что там для A32 вообще ОДНО-единственное окно -- BAR3?! Или оно сразу размером 32 бита (т.е., предназначено для использования в 64-битных системах)?

      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: нужен свой раздельчик и про Тундру -- Universe/Universe II/TSI148.

    Чтоб был рядом с прочими бриджами, вставляем его сюда, не по хронологии добавления.

    17.12.2021: немного о зоопарке Тундр (чтоб лучше понимать "что есть что"):

    • То ли "SCV64", то ли "SCV64TM", она же CA91C078A. Про него нагуглить особо ничего не удалось, в т.ч. неясно наличие поддержки под Linux.
    • Universe II -- 1997, "supported PCI-to-VME64".

      Судя по linux-*/drivers/vme/bridges/ -- она же CA91C142.

    • ?Universe IID? -- 1998
    • TSI148 -- 2004, "supported PCI-X-to-VME64/VME320".

    Источники информации:

    17.12.2021: мои впечатления от TSI148 по опыту реализации поддержки для MVME3100:

    • Вкратце -- УЖАС!!!
    • Оно ПЫТАЕТСЯ имитировать M68K, в т.ч. тем, что устанавливаются не сами AM'ы, а "режимы доступа" -- program/data, privileged/unprivileged.
    • Но получается это у него плохо -- например, приходится прямо в конфигурации окна устанавливать ещё и "ширину данных" (8/16/32/64), что напрочь рушит саму модель "просто обращаемся по нужному адресу за данными".

    11.01.2022: по результатам общения с Котовым (по ходу разбирательства с работой прошивальщика):

    • Я-то считал, что это указание "ширины данных" необходимо для того, чтобы чип бриджа мог бы корректно выполнять endian conversion. И продолжаю так считать.

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

      31.01.2022: пытался найти ответ на этот вопрос, несколько раз.

      • Антон Павленко, делавший cPCI'ный DL200 -- не помнит.
      • В распиновке "PCI bus распиновка и описание @ pinouts.ru" -- тоже не видно таких пинов. И как же там указывается ширина доступа? Ну не может же оно ВСЕГДА работать по 64 бита -- ведь как минимум первые реализации были 32-битными...
      • 31.01.2022: заинтересовался из распиновки, что -- такое линии "C/BE0"..."C/BE3" "Command, Byte Enable". И-и-и... Нашёл: "How the PCI Bus Works"
        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".

    • Но Котов утверждает, что так устанавливается МАКСИМАЛЬНАЯ ширина (как-то оно, похоже, необходимо для DMA), а обращения МЕНЬШЕЙ ширины разрешаются.
    • С другой стороны, они-то работали с Тундрой только на PowerPC, т.е., на BIG-endian-платформе, где конверсии нет; так что этого аспекта в принципе видеть не могли.
  • 08.07.2021: решил немного разобраться в вопросе "а как в VME устроены прерывания?", который изначально-то проистекает из "как в M68K устроены прерывания?".

    Побудительным мотивом явилось то, что в котовском драйвере vmei.c используется подход "уровни прерываний -- ничто, вектора -- всё!".

    • Обоснование описывается так: ну ведь уровень -- это всего лишь приоритет, а смысл прерывания -- в его векторе.
    • Обоснование это я слышал от Фатькина, а Котов, видимо, по этой идеологии и реализовывал.
    • Работает оно так:
      • Создаются 256 файлов /dev/vmei0.../dev/vmei255 -- по одному на каждый возможный вектор.

        И интересующаяся этим вектором софтина должна открыть соответствующий файл, мониторить его select()'ом и читать read()'ом.

      • А обработчик VME-прерываний -- vmei_isr() -- при возникновении события делает "level_mask |= 1<<level", каковой потом читается read()'ом -- т.е., возвращается БИТОВАЯ МАСКА УРОВНЕЙ, на которых данный вектор возникал с последнего чтения.

    Смысл такого подхода "битовая маска уровней" лично мне неясен: в какой практической ситуации оно может потребоваться? Тем более, если уж декларируется "уровни -- ничто!": тогда уровень вообще никого не интересует, а важен лишь факт наличия вектора.

    Косяки данного подхода:

    1. Оно категорически несовместимо ни с одним из других API доступа к прерываниям: ни с CAEN'овским, ни с мамкинским, ни даже с IFC1210 (где используется "полный набор" /dev/toscavmeeventL.V -- 256*7 файлов).
    2. Если софтина хочет мониторить ВСЕ вектора, то ей придётся открыть и мониторить 256 файлов (против всего 7).
    3. А ещё оно позволяет читать в ЛЮБОЙ момент -- даже если никаких прерываний не было, то просто возвращает 0.

      Тем самым нарушается модель "читать можно только после готовности на чтение по select()'у", и по факту оно работает как ioctl() "прочитай текущую известную маску".

    08.07.2021: так вот: решил копнуть чуть глубже и разобраться, КАК "правильно" и ПОЧЕМУ. Результаты:

    • В имеющихся реализациях от Мамкина и CAEN уровень прерывания -- ПЕРВИЧЕН: у Мамкина вектор читается из регистра по адресу/смещению 0x100 + 4 * irql, а CAENVME_IACKCycle()'у надо просто явно указывать УРОВЕНЬ, для которого выполняем цикл IACK.
    • Т.е., по факту уровни первичны, а вектора "подчинённы" -- т.е., у каждого уровня может быть своё, независимое пространство векторов.
    • Дальше погуглил на тему "m68k irq handling", и наткнулся на некую "Lecture 17" (под заголовком "Lecture 2") от Andreas Moshovos за непонятную дату, которая, судя по индексной странице курса ECE243, SPRING 2006.

      Так вот, там сказано:

      There are seven interrupt acknowledgement lines, IACK1 through IACK7 (active low).

      Я на эту информацию должен был натыкаться раньше при чтении описания шины VME.

    • Вот и объяснение: что в аппаратуре исполнение цикла IACK просто жёстко ПРИВЯЗАНО к уровню.
    • Соответственно, фатькинская идея "уровни -- ничто!", хоть и верна по изначальному посылу применительно к M68K (адрес обработчика берётся как 0x00000000+ВЕКТОР*4, и уровень тут не участвует никак), но в отношении VME де-факто используется другой подход: каждый уровень может иметь свой набор векторов.
  • 16.12.2021: а -- да, давно хотел сформулировать своё мнение относительно VME, теперь на основе ещё более широкого и глубокого опыта:
    • VME must die!
    • Эта шина, разработанная в начале 1980-х, сейчас, с точки зрения программиста, категорически не соответствует современным подходам к программированию и требует просто адских усилий при реализации вроде бы простейших вещей.
      • То, что она big-endian -- в мире, где почти все процессоры little-endian -- это ещё мелочь.
      • Главная проблема -- в отвратительной организации: там отсутствует надлежащее структурирование с разделением на слои, которое позволило бы и ПО должным образом фрагментировать на независимые части.
        • Вместо этого интимнейшие детали шины "торчат" прямо в высокоуровневое ПО, и избежать этого никак невозможно (например, даже теоретическая возможность RORA сразу рушит возможность переноса операции снятия прерываний на нижний уровень).
        • Как следствие -- отсутствует хоть сколько-то стандартный API работы с VME. У ВСЕХ чипов-бриджей API РАЗНЫЙ (не говоря уж о CAEN, с его оптическим линком).

          (Что-то как-то стандартизовано только под RTEMS, с закосом под имитацию старых контроллеров на M68K под VxWorks. Но там по факту одна-единственная платформа -- MVME3100 на TSI148.)

        • Почему так сделано -- понятно: всё строилось вокруг M68K (просто взята его шина), и предполагалась работа "напрямую" -- из единого пространства, без разделения на kernel/userspace, так что и обращения к памяти делаются тривиально, и прерывания тут же обрабатываются очень легко.

          Никаких вариантов, очевидно, и не предполагалось.

          Но с исчезновением M68K простота обращения через память превратилась в адские пляски с обращениями через бридж, а со сменой парадигмы от "всё делаем прямо в ядре RTOS" в сторону многослойного структурного программирования и сама модель работы VME стала категорически неадекватна.

        • Конкретно TSI148 -- вообще шизофрения: с одной стороны, она пытается имитировать M68K -- там вместо AM'а указываются индивидуально битики SUPER/USER и PROG/DATA (причём все 4 -- это ОТДЕЛЬНЫЕ биты, а не 2 бита 0/1); с другой стороны, приходится в конфигурации окна заранее указывать то, что так шло бы контекстом самой операции -- даже размер данных (8/16/32); плюс, из-за этого "просто указать AM нельзя" и приходится выпендриваться с этими битиками (а вот у CAEN'а -- можно, и это сильно проще).
          22.12.2021: на всякий случай проверил -- да, это не API ядра, а именно в самой TSI148 так:
          • см. мануал, раздел 2.3.1 на странице 61 --
            The address and Address Modifier (AM) codes that are generated by the Tsi148 are functions
            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).
            (это ЕДИНСТВЕННОЕ место в документе, где встречается словосочетание "Address Modifier").
          • Только сами коды отличаются ("Table 1: VMEbus Address Mode Codes" там же) -- то ли коды от какого-то более старого чипа, то ли авторы драйвера постарались на КАЖДУЮ фичу выделить свой исключительный бит.
    • Для контраста: PCI организована совсем иначе, правильно структурированной. Там софту совершенно пофиг, какой вариант шины используется -- PCI, PCIe, в каком подвиде (да хоть TCA), и даже на каком процессоре работает программа -- Intel, AMD, да хоть ARM: всё реализуется через стандартные API, а тонкости работы с конкретным подвидом шины -- забота драйвера чипсета, ПО верхнего уровня и даже драйверов в ядре это не касается.
  • 06.06.2023@Томск-Беленца-6-23, утро: ещё давно МНЕ стало очевидно, что наилучший способ реализации доступа к VME в 32-битные адреса из 64-битной системы -- это аллокировать ПЛОСКОЕ окно размером 2^(32+6) байт -- чтобы напрямую смещением в этом окне кодировать и адрес (безо всяких окон, войдёт ВСЁ адресное пространство), и Address Modifier.

    Прямо напрашивается написать статью "Regarding 32-bit VME access from 64-bit host", где бы сия идея излагалась.

    Но способ настолько очевидный, что вознкиает вопрос -- а не запатентовал ли уже кто-нибудь эту идею? Поискать бы патенты...

    09.06.2023: ага, попробовал на patents.google.com -- ладно, что синтаксис поиска так себе, но по запросу "(vme 32-bit 64-bit bus)" ещё и результатов толпа (и бОльшая часть -- вовсе не про VME)...

Modbus:
  • 19.08.2020: создаём раздел. Возился-то с этим вопросом раньше, но догадался записать только сейчас.
  • 19.08.2020: побудительным мотивом была потребность связаться с ПЛК, обрабатывающим радиационные блокировки. Он доступен снаружи по Modbus TCP.

    С ним в конечном итоге разрулилось проще -- сделал очень простой специализированный драйверок modbus_tcp_rb_drv.c ("rb" - Радиационные Блокировки), который по факту ничего не знает про Modbus, а просто отправляет и принимает пакеты фиксированного бинарного формата.

    Но в процессе -- ДО того, как сделать драйверок, поскольку там было малопонятно, что к чему -- почитал про Modbus и даже сформулировал некие идеи/подходы на перспективу.

    19.08.2020: источники информации:

    Краткое резюме того, что я узнал в процессе чтения тех источников о Modbus и что я о нём думаю:

    • В Modbus используется идеология/парадигма "каналов". Которые бывают 4 видов: 1-битные чтения (discrete inputs) и чтения/записи (coils), 16-битные чтения (input registers) и чтения/записи (holding registers).

      1-битовые, насколько я понял, читаются группами (пишу по памяти через пару месяцев, могу и путать).

      Более сложные типы -- 32-битные, вещественные, строки, массивы -- в стандарте не специфицированы и в каждой реализации, где они требуются, делаются по-своему.

      • Есть отдельно команды индивидуального чтения каналов/регистров и группового (диапазонов-блоков). И, что любопытно -- обязательными для реализации являются именно ГРУППОВЫЕ команды 1,2,3,4,
      • Нумерация каналов/регистров толком не специфицирована, как и то, пересекаются ли пространства разных типов или нет. Есть только сомнительное/неоднозначное соглашение, что в документации каналы разного типа обозначаются разными условными "диапазонами": 00001-09999, 10001-19999, 30001-39999, 40001-49999 для coils, discrete inputs, input registers, holding registers, соответственно.

        Но в реальности эти "адреса/номера" не имеют никакого гарантированного отношения к протокольным адресам соответствующих регистров, и даже само это "соглашение" не является частью стандарта. Это просто наследие от первой реализации Modicon (автора стандарта) от 1979 года.

        По МОЕМУ опыту разбирательства с нашим ПЛК -- от этого соглашения только вред; лучше бы просто писали номера/адреса.

    • И вот конкретно в Modbus TCP вполне МОГЛИ бы сделать "активное устройство", но вроде бы нету.

      Так что -- ТОЛЬКО периодический поллинг.

    • Форматы пакетов в Modbus RTU (Remote Terminal Unit; бинарный протокол) и Modbus ASCII (текстовый протокол) -- эта пара используется по RS232/422/485 -- и Modbus TCP различаются между собой все трое.

      У них есть идеологически как бы "общая часть", под названием PDU (Protocol Data Unit), состоящая из кода функции и данных пакета, но с точки зрения кодинга я не увидел глубокого смысла в унификации на уровен именно кода.

    • *Я* лично не вижу никакого смысла в C++-подобной реализации Modbus с корневым виртуальным классом "Modbus вообще" и отнаследованными от него "Modbus RTU", "Modbus TCP" и т.д.

      Дело в том, что между этими протоколами по факту нет вообще почти ничего общего: у них даже форматы пакетов разные, так что обобщать там особо нечего -- разве что операции "отправить пакет" и "принять пакет".

      Но эти вещи намного удобнее было бы делать в парадигме "интерфейса" -- в стиле наших layer'ов.

    Теперь что касается появившихся в процессе оного изучения (но ещё ДО решения задачи с рад.блокировками путём примитивного драйвера) идей:

    • Напрашивается сделать "драйвер-бридж для Modbus-TCP вообще" -- modbus_tcp_gw_drv.c (примитивный "rb" был назван по мотивам этого имени).

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

    • Вопрос в том, как производить маппирование между номерами каналов и типами регистров и конкретными их номерами (внутри этих типов).
      1. По типу данных? Например, 16-битные r-каналы -- input registers, w-каналы -- holding registers, байтовые r- и w-каналы -- discrete inputs и coils.

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

        Но это решает лишь проблему определения типа, но не соотнесения номеров драйверных каналов и modbus'овских.

      2. Группами - а базы и размеры групп указывать в auxinfo.
      3. В drvinfo.

        Но тут опять же остаётся вопрос "обратного маппирования" -- из modbus'овского пространства {ТИП,НОМЕР} в CX'ное.

      На СЕЙЧАС наиболее простым и надёжным видится вариант (b) -- маппирование через auxinfo: никакого "творческого" поиска, всё линейно (только в init_d() проверить на неперекрытие).

    Впрочем, если/когда встанет вопрос о РЕАЛЬНОЙ потребности в реализации такого "универсального" драйвера -- тогда и посмотрим, на примере конкретной железки.

    19.12.2022@утро, мытьё посуды: так надо drvinfo пользоваться! В нём указывать каждому каналу, на какой ТИП и НОМЕР modbus-канала он маппируется.

    (@при записывании заметил) Однако, ровно эта же идея приходила в голову и тогда, летом 2020-го -- вариант (c)...

  • 10.08.2023: вчера на обеде ЧеблоПаша высказал удивление на тему почему я, дурак, писал свою реализацию Modbus вместо того, чтобы воспользоваться готовой libmodbus.

    А я ответил, что подозреваю её в непригодности для использования во фреймворках систем управления, т.к. там наверняка всё сделано по модели "отправили запрос и зависаем в ожидании ответа".

    (Косвенным свидетельством в пользу этого было то, что EPICS'ный Modbus-support всё делает сам, не пользуясь этой библиотекой. Но мало ли -- вдруг она просто появилась позже.)

    10.08.2023: но подозрения подозрениями, а лучше бы знать точно. Поэтому я взял исходники libmodbus-3.0.8 (из EPEL'а от RHEL7) и заглянул внутрь.

    • Увы, подозрения оправдались: оно действительно работает СИНХРОННО, без возможности разделять отправку и приём (путём мониторирования дескрипторов). Это хорошо видно в функции read_registers() в src/modbus.c (которая и осуществляет обмен) -- там сначала send_msg(), а потом сразу же receive_msg().
    • В результате: если тебе надо работать с одним-единственным Modbus-устройством -- OK, всё легко и просто; если же нужно с НЕСКОЛЬКИМИ -- то придётся на каждое заводить по отдельному thread'у (а если устройств 100 штук?). Но ещё хуже то, что такое размножение годится только для Modbus-TCP, где по отдельному TCP-сокету на каждый девайс; для RTU же -- фиг, т.к. там COM-порт общий для всех, и в него нельзя лазить нескольким thread'ам одновременно, а никакого "планировщика доступа к линии" в libmodbus не видно (по крайней мере, в той версии 3.0.8).
    • Ну и до кучи -- поддерживаются только TCP и RTU, а Modbus-ASCII -- нет. Хотя отличия в реализации минимальны (ибо Modbus, хоть в описании и говорится про "соответствие модели OSI" и на каких её уровнях он якобы работает, в реальности нифига не структурирован и на уровни не делится, а RTU/ASCII/TCP скорее являются модификациями поведения некоторых нюансов реализации).
    • Но так, с точки зрения качества кода -- написана libmodbus очень пристойно, хорошо структурирована и вроде не содержит очевидных идиотизмов, имеющихся, например, в ZeroMQ. Так что для простых действий вроде домашней автоматизации -- почему бы и нет.
    • P.S. А ещё там в src/modbus.h есть такой фрагмент:
      /* 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" нагуглен, ссылка добавлена к источникам информации выше.

Git:
  • 19.08.2020: создаём раздел.

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

  • 19.08.2020: итак, результаты изучения.

    19.08.2020: ну-с, начнём -- с ругани.

    • Эти дятлы (увы, начиная с Торвальдса) повторили косяк CVS и SVN: они НЕ сохраняют timestamp'ы файлов, а всегда ставят текущее время.
    • Обоснование очевидное и тупое: ради Make'а -- ведь он же, бедненький, пользуется timestamp'ами для определения необходимости перекомпиляции, и если НЕ будет ставиться текущее время (в момент checkout'а), то сборка может оказаться кривой/неполной.
    • Это прямым текстом сказано в Git FAQ на kernel.org -- ?Why isn't Git preserving modification time on files?".
    • И дополнительно есть "аргумент", что "Git - это система контроля версий, а не создания резервных копий".

      (Хотя задачи-то очевидно крайне близкие и связанные!)

    • То, что можно б было сделать специальный ключик -- как "-p" у команды cp (и тогда юзер берёт на себя заботы о корректной пересблоке) -- этим прекрасным людям, похоже, в головы не пришло.

    20.08.2020: осенило -- есть ведь ещё одна причина, уже вполне техническая и рациональная: в случае ПАРАЛЛЕЛЬНОЙ разработки, когда в один файл (в РАЗНЫЕ его части) коммитятся изменения из нескольких параллельных веток, не очень ясно, какой timestamp нужно ставить.

    Но такие параллельные изменения, хоть и являются "основным фокусом" для разработчиков git'а -- это всё же отдельный частный случай, далеко НЕ всеобщий.

    А среди индивидуальных кодеров и небольших команд более частая ситуация "одним файлом в конкретный момент занимается только ОДИН человек", и для никакой проблемы выбора нет.

MicroTCA:
  • 31.12.2020: создаём раздел, ибо назрело.
  • 31.12.2020: давно стояла задача просветиться о MicroTCA, поскольку его предстоит использовать на СКИФе, а я в нём нифига не понимаю (там ещё и толпа терминов -- MCH, RTM, ...).

    Некоторое время назад (в октябре?) был найден текст-tutorial

    • "Introduction to MicroTCA" от Ray Larsen, SLAC MicroTCA Review, June 4-5, 2012.

      Солидная презентация на 61 слайд.

    Из Википедии удалось узнать, что конкретно MicroTCA.4 -- это под-стандарт, ориентированный на использование в High Energy Physics. 02.01.2021: только в .4 есть rear I/O (MicroRTM).

    31.12.2020: некоторое количество статей, описаний и стандартов по MicroTCA:

    • Научпоп на русском:
    • На сайте PICMG: ...а по MTCA.4, увы, раздел пуст -- документов нет.
    • "Who Makes What: ATCA, AMC & MicroTCA" -- о производителях железа (ктейты, контроллеры, ...). К сожалению, информация подустарела -- 2009 год.
    • "Development of MicroTCA-based Image Processing System at SPring-8" -- статья на ICALEPCS-2013; увы, поток сознания (японцам сложно структурировать?).
    • "MicroTCA 2019: The 8th MicroTCA workshop"
    • На "9th MicroTCA Workshop for Industry & Research, 1-3 December, DESY, Hamburg" (виртуалный workshop) было всякое интересное. В частности, tutorial'ы:
      1. MTCA.4 Tutorial Basics Introduction in xTCA -- много картинок, пояснения и расшифровки аббревиатур.

        Слайд 15 о взаимоотношениях ATCA и MicroTCA:

        • The basic idea of MTCA is to have a shelf that contains just AMC modules
        • Backplane directly accepts AMC modules
        • AMCs are interchangeable between ATCA and MTCA
      2. MCH and power unit (system basics) -- малополезен, кроме слайда 9.
      3. MTCA.4 Tutorial MicroTCA Management -- детально прописано, что происходит при hot-plug-втыкании/вытыкании модулей.
    • 04.01.2021: "RT2010 & xTCA workshop a (personal) summary" Markus Joos, CERN-PH/ESE, 2010.

      Чувак делится своими впечатлениями с воркшопа. И, в частности:

      • Слайд 6 "xTCA users & products": про ATCA сказано "ITER not yet fully convinced, will also evaluate PXIe".
      • Слайд 8 "IPMI":
        • Nobody likes it; everybody uses it
        • IPMI spec (600 pages) very ambiguous. This often leads to interoperability problems as manufacturers interpret the spec in different ways
      • Слайд 11 "Other Information":
        • Exhibitors (only ~5): CAEN:
          • still not convinced by xTCA
          • also hesitating to go for VXS
          • VMEbus 2eSST in the pipeline
        • Other conference highlights
          • As many new VMEbus designs as xTCA modules

      (Наткнулся, пытаясь нагуглить, что же используется в ITER'е -- памятуя рассказанное Денисом Степановым на институтском семинаре в районе года назад, когда он говорил, что у них в основном PXI-express и *TCA, а на вопрос "не рассматривался ли VME?" сильно удивился и ответил, что, может, только в шутку.)

    • 15.01.2021: "A comparative discussion of bus/crate standards and their use at CERN" от того же Markus Joos, с DAQ/FEE/Trigger for COMPASS beyond 2020 workshop - Prague - 10.11.2017.

      Там есть краткая история модульных стандартов -- NIM, VMEbus, VXS, CompactPCI, PCIe с вариантами, TCA, AMC, MTCA (увы, без CAMAC); для каждого краткая сводка причин успеха/неуспеха и история применения в ЦЕРН.

      Наиболее ценен слайд 18 "The CERN accelerator controls systems - is MTCA the future?". И там в секции "What we don't like" квинтэссенция:

      • MTCA is an overkill in many respects and very complex

      (Самое странное, что наткнулся я на него случайно, гуглением по "huawei microtca"...)

      P.S. Там упоминается некий "FELIX": это разработанная для ATLAS программно-аппаратная платформа (Front End LInk eXchange),

      • есть презентация по нему -- "FELIX: the New Detector Readout System for the ATLAS Experiment" с TWEPP-2017,
      • ("FELIX is a router between custom serial links and a commodity network, which separates data transport from data processing.")
      • и там на слайде 12 "Wupper: PCIe engine for FELIX" рассказано про "PCIe Engine with DMA interface to the Xilinx Virtex-7 (Kintex Ultrascale) PCIe Gen3 Integrated Block for PCI Express",
      • который "Published as Open Source (LGPL) on OpenCores http://opencores.org/project,virtex7_pcie_dma

    (На часть этого есть ссылки из статьи MicroTCA в Википедии, часть нагуглил.)

  • 13.04.2021: помнится, когда я ещё совсем ничего не знал про TCA, слышал от Фатькина утверждение, что интерконнект там именно по 1GbE является обязательным, а PCIe -- опционален; потом из общения с Тиленом узнал, что это не так, а изучение стандартов это подтвердило.

    Так вот: судя по описанию CompactPCI Serial в Википедии, именно в НЁМ очень развит Ethernet-интерконнект -- "Ethernet Full Mesh Architecture", "8 x Ethernet 10GBASE-T".

    Может, Фатькин тогда просто спутал?

Всякие разные слабоспецифицированные знания:
  • 31.12.2020: создаём раздельчик, чтобы записывать сюда всякую разрозненную информацию, ни к чему иному конкретному не относящуюся.
  • 31.12.2020: насчёт TMDS (DVI, HDMI) и LVDS (DisplayPort, а заодно и SATA).

    31.12.2020: некоторое время назад захотелось разобраться, в чём отличие TMDS от LVDS -- просто для собственного понимания, т.к. на СКИФе предполагается использовать в системе синхронизации какой-то тип сигнала, по смыслу являющийся LVDS'ом.

    Так вот -- внятного объяснения не нагуглилось, а только некое описание того, как кодируется сигнал для мониторов и LCD-панелей:

  • 09.02.2021: насчёт CAN-адаптера Tews TPMC810: захотелось вспомнить поточнее, что же там было не так с его BSP, что им оказалось невозможно пользоваться настолько, что вот совсем никак, пока ЧеблоПаша не сделал plx_pci.

    Так вот -- выяснил: там в API драйвера отсутствует возможность следить за появлением пакетов по select()/poll(), а только либо зависать на ожидании, либо периодически поллить.

    • BSP (Board Support Package) сходу найти не удалось, но...
    • На оф.странице "TPMC810 / Two Independent Channel Extended CAN Bus" если щёлкнуть слева на "All Manuals (PDF)", то откроется список всего доступного, где есть...
    • Ссылка на "TDRV010-SW-82 Linux Device Driver" (Issue 2.0.3 November 2017).
    • И там есть только 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 :) Гы-гы-гы!!!

  • 14.02.2021: так, если понадобится, то чтоб было под рукой -- ссылки на статьи Лаб.6 (Батраков, Павленко, Фатькин, etc,) по автоматизации ЛИУ-20.

    На ICALEPCS-2017:

  • 15.02.2021: Precision Time Protocol (PTP), IEEE 1588: всплыл при обсуждении с Хильченко контроллера Чеблакова-Торнадо, как способ синхронизации множества сетевых устройств.

    Отдельный раздельчик в "Знаниях" создавать смысла нет, а вот небольших заметок оно заслуживает.

    15.02.2021: итак:

    • Стартовой точкой традиционно послужила статья "Precision Time Protocol" из английской Википедии
      • Общее:
        • "IEEE 1588 is designed to fill a niche not well served by either of the two dominant protocols, NTP and GPS. IEEE 1588 is designed for local systems requiring accuracies beyond those attainable using NTP. It is also designed for applications that cannot bear the cost of a GPS receiver at each node, or for which GPS signals are inaccessible."

          Оно даёт суб-микросекундную точность.

        • Есть 2 версии: v1 2002 года и обратно-НЕсовместимая с ней v2 2008 года (плюс совместимая с последней v2.1 2019 года "IEEE 1588-2019").
      • Оно работает по обычному Ethernet (в отличие от EtherCAT'а).
      • При этом сосуществует с обычным траффиком (опять же в отличие от).
      • Использует UDP.
      • Причём multicast'ы, с заранее предопределёнными адресами, в т.ч. 802.3 Ethernet-адресами.
    • Также первой в списке нагуглилась статья на Хабре "IEEE 1588 Precision Time Protocol (PTP)" за 20-12-2012.
      • Там первым делом упомянуто, что для совсем хорошей точности необходима АППАРАТНАЯ поддержка от Ethernet-чипа -- чтоб маркировал полученные пакеты timestamp'ами момента получения.

        Отдельно нарыто, что как минимум некоторые Intel'овские адаптеры поддерживают оное -- например, I340 и I350, причём ТОЛЬКО для пакетов IEEE 1588; источник -- "Which Intel NICs support hardware timestamps (hardstamps)" 2015 года.

      • Также там описываются конкретные алгоритмы синхронизации с картинками и формулами, но вчитываться внимательно было уже лень.
    • Отдельно сразу нагуглилось наличие поддержки в Linux -- PTPd.

    25.02.2021: пообщался с Чеблопашей -- он после того разговора полторы недели назад занялся изучением PTP касательно возможности использовать его с SOM'ами.

    • В том NXP'шном ("i.MX7"?), что у них, есть аппаратная поддержка.
    • Причём не просто timestamp'ов, а такое впечатление, что прямо на уровне чипа (хбз, какой его части) реализован практически полный протокол.
    • И неясно становится, что же делает демон (ptp4l?).

      26.02.2021: Паша разобрался чуть больше: "Демон обеспечивает бОльшую часть функционирования протокола -- работает «дирижёром», отправляя пакеты, а чип занимается лишь вычислением поправок".

    • Отдельный вопрос -- как с поддержкой на уровне свитчей.

      Так вот: у Aruba есть "младшая" линейка с поддержкой 1588, а в старшей (которую мы предварительно выбрали для СКИФ) оной поддержки нет.

      Эта аппаратная поддержка заключается в том, что свитчи являются "прозрачными" для протокола, не внося искажений. Как сие возможно -- МНЕ пока не вполне ясно; может, свитчи интеллектуально корректируют содержимое PTP-пакетов для отражения там задержки на прохождение между портами этого свитча?

EtherCAT:
  • 22.01.2021: создаём раздел.

    В силу необходимости начать хоть что-то понимать в этом вопросе.

  • 22.01.2021: крохи знаний...

    22.01.2021: по результатам обсмотра огрызков презентации контроллера источников от 6-й лаборатории (у Карнаева нашлась):

    • Ключевое:
      • EtherCAT -- это совсем НЕ Ethernet.
      • Там используется -- в варианте "100Mbit TX" -- цепочечное закольцованное подключение, где из "мастера" идёт линк точка-точка к первому ведомому, от того другой линк к следующему, и так далее; а от последнего линк возвращается к "мастеру".

      Т.е., это похоже на CAEN'овское подключение контроллеров оптикой, а также и на CAN-bus.

      Но больше всего это похоже на Token Ring.

      ...и есть неясность, бегают ли там кадры только в одном направлении кольца, или в обоих возможных (дуплекс ведь, так что можно -- тем самым удваивая пропускную способность до 200Mbps).

    • Чипы в EtherCAT'е используются свои, специализированные, это НЕ всякие Intel/Realtek/...
    • 24.01.2021: писание "What Is EtherCAT Protocol and How Does It Work?" от 10 Jun 2020.

      Найдено при гуглении на тему "ethercat criticism".

      Кстати, очень сильно смущает отсутствие какой-либо критики EtherCAT -- мне найти не удалось, везде сплошные восхваления (в т.ч. в Википедии).

    24.01.2021: спросил мылом у Дуброва (как человека, детально разбирающегося в обычном Ethernet), "может, есть какие-нибудь знания/соображения насчёт EtherCAT, какие в нём могут быть подводные камни?". Его ответ оказался очень полезен:

    • Из собственно ответа:
      • У EtherCAT slave устройства принципиально должно быть не менее двух интерфейсов - в один он принимает поток, и, после анализа заголовков, начинает, не дожидаясь полного приёма, двигать пакет во второй интерфейс, вписывая/считывая в/из нужные/-ых поля/-ей предназначенные для него данные. За счёт того, что пакет продвигается без полного store-and-forward, он быстро, с минимальными задержками, пробегает по всему кольцу, обеспечивает realtime на уровне микросекунд.
      • Из минусов - классичесский vendor lock (Beckhoff). Хотя членство в их ассоциации бесплатно... пока :-)
    • Также Дубров посоветовал ещё один текст, на Хабре -- "Ethercat для начинающих", 26-Aug-2020.
      • ...EtherCAT работает так -- устройство принимает пакет, и как только оно приняло заголовок (и поняло что это EtherCAT-пакет) оно начинает этот пакет отсылать дальше, в процессе отсылки заменяя те биты пакета в которых оно должно отослать информацию на нужные.
      • Насчёт портов:
        • оказывается, их у каждого устройства не по 2, а всегда по 4, просто остальные 2 обычно "заглушены" (нераспаяны);
        • также есть правила, по которым пакеты пересылаются между портами: по кольцу, "по часовой стрелке" с 0 на 1, с 1 на 2, с 2 на 3, с 3 на 0, отключенные порты пропускаются;
        • причём "обработка" производится после получения из 0-го порта (там стоит "ethercat processing unit").

        Отсюда следствия:

        1. Топология соединений устройств может быть не только кольцевой, но и всякой древовидной и звездообразной (при 3 и 4 портах).
        2. Также можно использовать не закольцованность, а просто линию, с "оконечным" устройством со всего 1 портом. По тому правилу пакет, принятый из порта 0, будет отправлен обратно в него же (поскольку порты 1,2,3 не работают, то по кольцу вернётся в 0).
        3. Гуляние пакетов по кольцу в ОБОИХ направлениях (как мы думали) -- очень сомнительно, поскольку обработка ТОЛЬКО после получения из 0-го порта, а обратное направление -- это будет из 1-го.
        4. ...хотя для повышения НАДЁЖНОСТИ второе направление использоваться вроде как может -- говорится, что мастер при обнаружении разрыва может отправлять пакеты через второй порт.

          ...и, видимо, обрабатываться они будут устройствами при получении не в "исходном" направлении (по противокольцу), а уже при возврате -- поскольку будут приходить с 0-го на 1-й (а у последнего в цепочке -- пройдут по кругу "1,(2),(3),(0),EPU,1").

        5. 01.02.2021: как утверждает Павленко & Co., пакеты мастером ВСЕГДА посылаются в оба порта, а получаются обратно, соответственно, по умолчанию из "другого" порта.

          И таким образом можно определить разрыв (без каких-то дополнительных действий?) -- что ответные пакеты начинают приходить из того же порта, а отличать их от обычно приходящих в этот порт пакетов изначально от второго порта можно по WKC (который при обычном прохождении обратной петли должен остаться ==0), плюс, по самому значению WKC можно (опять же без доп.действий?) сразу же понять, на каком именно хопе разрыв связи.

      • Также там описаны формат EtherCAT-пакета (можно работать через UDP!), включая формат размещаемых в нём EtherCAT-датаграмм, и механизмы взаимодействия под названиями SyncManager и FMMU (Fieldbus Memory Management Unit).
CANopen:
  • 03.02.2021@дорога от родителей, около НГУшного стадиона: учитывая использование в EtherCAT'е протокола CANopen (там он идёт как "CoE" -- "CANopen over EtherCAT"), применяемого там для перечисления/обнаружения объектов -- явно надо бы его всё ж таки изучить. И сколлекционировать тут ссылки на информацию и самые важные находки.

    03.02.2021: создаём раздел.

  • 03.02.2021: собираем.

    03.02.2021: в качестве исходной точки берём

    • Статью "CANopen" из английской Википедии.
      • 11-битный CAN-идентификатор используется как 2 поля: 4-bit "Function code" и 7-bit "Node ID", что даёт 127 устройств (адрес 0 -- broadcast).

        Он называется "COB-ID" (communication object identifier).

      • Пока неясно, как передаётся "адрес отправителя" -- если он вообще нужен (в SDO есть отдельные команды "transmit" и "receive").
      • Для передачи больших объёмов есть протокол "SDO" (Service Data Object), весьма замудрённый. В т.ч. там есть сегментирование -- для передачи больших объёмов (т.к. на данные в SDO-пакете остаётся лишь 4 байта, а остальное -- protocol overhead, включая 2 байта index и 1 байт subindex).
      • ...а также есть протокол "PDO" (Process Data Object), где передаётся до 8 байт, но конкретный их состав -- заранее никак не известно (там что-то сказано о каком-то "J1939").
    • Неплохо дополняет её статья "CANopen Explained - A Simple Intro (2020)" -- там и язык временами понятнее, и даже "иллюстрации" есть (правда для многих надо щелкнуть "+"), плюс присутствует "CANopen COB-ID converter".

    Ну уже сейчас вполне ясно, почему Козак предпочёл сделать свой протокол, а не использовать CANopen: шибко уж он сложен, по сравнению с CAN-Kozak.

Электроника MRF:
  • 04.02.2021: давно хотел почитать документацию от MRF -- как программировать их железки по VME, PCIe и Ethernet -- более детально. А раз так -- то надо результаты/впечатления от прочтения куда-то записывать.

    Вот и создаём раздел.

  • 04.02.2021: итак,

    05.02.2021: конкретные источники информации:

    13.02.2021: эх,

    • всё-таки есть у них CSR: Receive Data Buffer Control and Status Register (0x22, 0x23, EVR-MRM-007.pdf, стр.51)... И в нём как readonly-биты, так и read-write (а вот есть ли write-only -- хбз).
    • И в mTCA-варианте оно тоже осталось -- тот же Data Buffer CSR, судя по DCManual-191127.pdf, стр.38 -- отличие лишь в бите 16, который теперь просто "1", а не "MODE".

    03.03.2021: зато у них, похоже, НЕТУ любимого батраковцами извращения "R1c" (битик сбрасывается сразу после чтения регистра).

    • Я делал контекстный поиск в общем (VME, PCIe, mTCA) описании EVM/EVR DCManual-191127.pdf по словам "clear", "reset" и "zero", но ни одно из их появлений не было связано с автоочисткой -- только явные сбросы.

      (Всё, что нашлось "авто" -- всякие авто-остановы/авто-сбросы счётчиков после переполнения и т.п.)

    04.03.2021: но документация и система команд всё же далеки от идеала:

    • С адресами регистров бывают некоторые странности.

      Например, в "Table 17: Event Generator Register Map" на стр.30 указан 0x060 EvanControl "Event Analyser Control Register", но в дальнейшей детализации этого адреса вообще нет, а есть только байт 0x063 под тем же именем.

      ...видимо, это следствие VME'шной big-endianness'ности, но всё равно дичь -- ведь адрес в детализации попросут не соответствует таблице (не говоря уж о том, что в PCI эта endianness может и отличаться).

    • Местами встречается указание "то-то сбрасывается при записи 1 в такой-то бит", но рекомендуется писать в бит, помеченный RO:
      • На стр.82 в описании "Receive Data Buffer Control and Status Register"/0x022 про DBCS/bit13, сказано, что "Flag is cleared by writing '1' to DBRX or DBRDY...", а DBRX/bit15 -- "(read-only)".
      • "Фокус" в том, что bit15 -- это DBRX/DBENA: т.е., на ЧТЕНИЕ это DBRX ("Data Buffer Receiving (read-only)"), а на ЗАПИСЬ -- DBENA ("Set-up for Single Reception (write '1' to set-up)").

      Т.е., тот же уродский дуализм, что и у батраковцев (например, у L_TIMER'а регистр EXT_CSR/0x400074, который на чтение "возвращает состояние «ворот»", а на запись позволяет разрешить или запретить внешний запуск).

  • 10.10.2022: чуть в сторону --
    • "MRF Timing System Overview" -- презентация от Jukka Pietarinen на Timing Workshop EPICS Meeting Spring 2019.
      • Там в т.ч. рассказывается история/background фирмы MRF.
      • Стр.6 -- "What do you need a timing system for?" -- ОЧЕНЬ лаконичное, ёмкое и понятное перечисление.
UIO:
  • 10.10.2022: поскольку занадобилось изучить "Linux UIO", то создаём раздельчик для записи результатов изучения.

    (Кажется, ещё с полгода назад слегка изучал эту тему, но вот записей, оказывается, не оставил -- вплоть до сегодняшнего дня ни единого экземпляра буквосочетания "UIO" в этом файле не было.)

  • 10.10.2022: итак...

    12.10.2022@М-46: ключевое слово при конфигурации UIO -- "device tree".

    24.10.2022: Лёша Герасёв прислал пару ссылок на описание/документацию по "device tree" от профильных контор.

  • 10.10.2022@пешком мимо 4-го здания (из пультовой ВЭПП-5 в пультовую СКИФ на сборище по MRF), ~10:50: а можно ли через UIO реализовать интерфейс вроде pci4624?

    Т.е., некий "uio4624", чтобы можно было делать "adc200me@uio4624"; это, конечно, малоосмысленно, но вот какие-нибудь другие PCI/PCIe-устройства -- вполне.

Leybold:/USS:
  • 18.07.2024: изучал эту тему давно, но пришло время записать в файл, чтоб информация не потерялась. Создаём раздел.
  • 18.07.2024: изначальная потребность --
    • Старостенко-Джуниору на стенде тренировки катодов в подвале-блоке Кузнецова уметь получать в компьютере измерения вакуума, чтобы при его ухудшении сбрасывать накал катода (уставку в программе senkov_ebc; там до сих пор CXv2, но заменить не проблема) на некоторое время, а потом поднимать обратно.
    • Т.е., нужно для обратной связи в процессе тренировки.
    • Непосредственно "как человек" он общается с "пультом" Leybold TURBO CONTROL i 800100V0004, а тот подключен (через USB!!!) к стоящему на турбике контроллеру TURBOVAC 350 iX 830061V3300.

    18.07.2024: изучалась тема в несколько приёмов, с ясностью и документированностью тут большие проблемы -- описание крайне невнятное, фиг поймёшь что да как, так что часть информации ниже является результатом моих разбирательств.

    Результаты первоначального изучения вопроса с привлечением помощи других:

    • На "пульте"-то присутствуют вышеупомянутый USB с подписью "USB TMP", DB9 "RS485 TMP" и RJ45 "ETH".
    • 18-06-2024 был задан мылом вопрос Александру Краснову на тему "а у нас в Институте кто-нибудь эти железки с компьютером дружил?", на что получен ответ

      Если нужно читать только давление, то проще всего впаятся прямо в кабель (один из разъемов) от датчика давления к контроллеру. Там есть аналоговый сигнал (логарифм измеряемого тока). Перевод в давление и распайку разъема можно найти в паспорте на датчик давления. Кажется, других способов читать данные с этих контроллеров мы еще не применяли.

      Если паспорта на датчик давления нет, то можно будет спросить Сашу Жукова.

    • ...тем временем велось начатое ещё ранее вялое изучение вопроса самостоятельно...
    • 01-07-2024 удалось пересечься с Жуковым и получить информацию, что
      • Нет, к СУ эти штуки у нас никто не подключает.
      • Если надо смотреть с компьютера -- то есть софтина LeyAssist, позволяющая из-под Windows получать данные и ещё что-то там.
      • Также в пульте есть HTTP-сервер, к которому можно из браузера подключаться.
    • Сначала я слабо понимал взаимоотношения этих 2 устройств (а кто б мне объяснил? ну могли бы вакуумщики, но мне и в голову не приходило задать такой вопрос -- вот оно следствие бессистемного получения знаний) и считал, что "пульт" и есть контроллер, с которым надо общаться, и лишь несколько потом понял, что общаться правильнее с собственно контроллером на турбике.

    Результаты рытья вопроса лично мною (начато ещё в середине июня, потом шло перемежаясь с опросом вакуумщиков):

    • Мануал от "пульта" -- "TURBOVACTURBO.CONTROL i24 VDC Display Unit Brief Instructions 300680364_002_C2 Part No. 800100V0004" -- даже слова "protocol" не содержит.

      ЗЫ: но это СЕЙЧАС стало понятно, что и неудивительно -- ведь "пульт"-то и сам является клиентом.

    • Ну хорошо, погуглил "leybold site:anl.gov" -- вдруг в EPICS что-то есть готовое.

      Первый же результат -- «EPICS driver / support for "USS" Device: Leybold Turbovac iX» за 6 Oct 2022, это первое увиденное мною упоминание "USS protocol", и оттуда ссылка на...

    • ..."Serial Interfaces for TURBOVAC i/iXRS 232, RS 485, Profibus, Profinet, USB Operating Instructions 300450826_002_C2" (этот же документ потом выдал и Жуков)

      Там видно некоторое количество информации насчёт работы через RS485, Ethernet, а также упоминается USS.

    • Далее было погуглено по «leybold turbo control "protocol"» -- толку ноль.
    • Так и оставалось неясным -- а что же у этой штуки на каком порту работает.
    • 18-07-2024 подключение "пульта" кросс-кабелем по Ethernet показало, что:
      • По дампу Wireshark видно: сначала он пытается получить IP-адрес по DHCP, а через некоторое время берёт себе самоприсвоенный из диапазона 169.* (это было видно по запросам "а есть тут кто-нибудь с адресом 169.254.157.199?").

        Далее адаптеру компа был назначен 169.254.157.1 и проведён...

      • ...скан nmap'ом: в режиме "-sX -p 1-31000" открытых портов не обнаружилось.

        А вот тривиальный "-sT -p 1-31000" (connecT scan) показал единственный порт 80, т.е. http.

        ...а я-то надеялся, что найдётся какой-нибудь "USS TCP"...

    • Сегодня нарыт каталог продуктов Leybold -- "High Vacuum Pumps" -- в котором табличка фич разных моделей.

      Но описания девайса там нет.

    • 19.07.2024: а вот общий мануал "TURBOVAC i(X) TURBOVAC 90 i(X), 250 i(X), (T) 350 i(X), (T) 450 i(X) Turbomolecular Pumps with Integrated Frequency Converter (and Control Unit) Operating Instructions 300554863_002_C0" рекомендует
      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.
      -- т.е., ровно тот документ, что я ранее и нашёл. Т.е., он применим и к непосредственно контроллеру турбика.
    • И, кстати, в этих общих мануалах постоянно упоминается некий интерфейс "Anybus", который теперь ставят вместо RS485.
      • По чтению доков возникло впечатление, что это просто некий "интерфейс", могущий быть сконфигурированным для работы в разных вариантах, в т.ч. и как RS485.
      • Это торговая марка германской конторы HMS Networks, ...
      • "Anybus | Get connected and Stay connected" -- оф.страница содержит в основном всякие маркетинговые слова.

      Короче -- проще считать, что имеем дело с просто 485-м, хотя, возможно, на стороне устройства может требоваться конфигурирование.

    Ну и общий вывод:

    • Если всё-таки реализовывать поддержку USS, то, скорее всего, проще всего будет скопировать архитектуру и даже сами исходники с таковых от Modbus. Главное только понять, ЧТО является адресуемыми сущностями в USS, т.к. от этого архитектура софта сильно зависит.

      20.07.2024@вечер, засыпая: кстати, а для утилиты командной строки есть готовая console_mon_util.h -- она как раз примерно для такого готовилась почти сразу, чтоб подходить для произвольных подобных утилиток.

    • ЗЫ: а в ПРОСТЕЙШЕМ варианте -- вычитывание одного-единственного параметра "давление" (или кто он там), фиксированным пакетом (только контрольную сумму считать надо) -- можно сделать драйвер на основе modbus_tcp_rb_drv.c, только заменив 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: изучаем.

    • В описании USS от Siemens -- uss_24178253_spec_76.pdf -- в первую очередь схема адресации и формат пакета.
      • Стр.A-3(PDF:9): Telegram format:
        |STX=0x02|LGE=len| ADR | ...data... | BCC|
        -- начинается всегда с ASCII-кода STX, Ctrl+B.
      • Длина (LanGE?) -- длина пакета минус 2 (без STX и LGE, но С BCC), 1<=LGE<=254.

        ЗАМЕЧАНИЕ: Судя по Serial_Interfaces_for_TURBOVAC_300450826.pdf стр.22, у Leybold всегда LGE=22.

      • Стр.PDF:10: ADR состоит из 5-битового адреса в битах 0-4 и 3 битов

        Биты 5,6,7 -- это Broadcast, Mirror и Special соответственно. Mirror -- вернуть пакет как есть (ping-pong).

        Обычные посылки -- адрес всегда 1-31 (0 -- хбз... Master?).

        ЗАМЕЧАНИЕ: У Leybold'ов адрес устанавливается параметром #37, и Serial_Interfaces_for_TURBOVAC_300450826.pdf стр.29 утверждает, что по умолчанию значение 0.

      • Контрольная сумма, BCC ("Block Check Character") -- просто XOR всех байтов, предшествующих самому байту контрольной суммы.

        ЗАМЕЧАНИЕ: Вот только 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-го байта". ...но формулировка действительно кривая.

      • Данные ВСЕГДА BIG-ENDIAN:
        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.
      • Отдельный прикол в том, что на стр.A-9/PDF:15 имеется раздел "5.2.1.1. Response delay time":
        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мс даже либеральнее этих требований.

      • 03.08.2024: Стр.C-1/PDF:30: и делался USS явно с оглядкой на Profibus:
        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.
    • А вот в "Serial interfaces for TURBOVAC..." уже поболее деталей:
      • Стр.7: настройки интерфейса -- 19200,e,8,1, flowcontrol:none
      • Стр.22: формат пакета ("телеграммы") -- "Telegram for RS 232 and RS 485" перечисляет и специфичные поля внутри пакета.

        И, похоже, там ВСЕГДА 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, не повлияв на работу.

      • Стр.23: формат пакета для Profibus -- "Telegram for Profibus", он похож, но отличается, в т.ч. переставленными байтами.
      • Стр.24: "Description of PKE, IND, Control and Status Bits"
        • "PKE: Parameter Number and Type of Access" -- байты 3,4 "PKE": биты0-10: номер параметра, бит11=0, биты12-15: тип запроса (читать значение, писать значение, читать "поле", писать "поле") или тип параметра в ответе.

          Причём прямо в типе запроса или ответа указывается размер -- 16 или 32 бита (правда, коды весьма бардачны -- нету бита "размер", просто у разных кодов разная битность значения).

          И прямо там же "ответы-ошибки": 0b0111 -- "The frequency converter can not run the command", 0b1000 -- "During a write access: no permission to write".

        • "IND" -- номер поля в "векторных" параметрах. ОДИН байт, байт 6.

          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 это слово рассмотрено побитово и там у старшего байта кодировка замороченная.

      • Стр.27-40: список параметров. Нужное нам "давление" -- возможно, один из 616, 617, 618 "Gauge pressure value" in mbar, Torr, Pa.
      • Список типов там -- U16, I16, U32, I32, R32 (float?). И это ВСЁ -- никаких LE/BE, всё ТОЛЬКО big-endian.

        01.08.2024: нифига -- есть ещё строки: параметры 312-316, 350, 355, 395, 396, которые U16[0..NN] "One ASCII char per index"; очень похоже на MODBUS_CONV_STRING_LOW.

      • Но бывают "массивовые" параметры: возвращается-то ОДНО слово (16 или 32 бита), но которое из -- указывается отдельным параметром "индекс" (IDX, байт номер 6) в запросе.

        Стр.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[].

        Другое дело, что зачастую это хоть связанные (однотипные настройки для разных аксессуаров), но различные значения, просто скрывающиеся под разными индексами одного номера параметра, и адресоваться к ним стоит индивидуально.

      • 02.08.2024: Стр.60-65: примеры "телеграмм", расписанные побайтно (даже побитно :D), с комментариями и описанием полей/байтов.

        ...правда, в начале там подзаголовок "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?

    • OK, подоткнулся -- а там в web-интерфейсе НЕТ никаких настроек.
    • Но есть какой-то режим "LCD", который показывает отражение LCD-экранчика "пульта". Однако там нажать тоже ничего нельзя.
    • Но есть возможность залогиниться -- надо ввести login/password.

      КАКИЕ?

    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:

    • Порывшись в меню на бледном дисплейчике (включил яркий lightbar над столом, спасибо Диме Старостенко за удобный стол), нашёл пункт Menu(F3)|"Control H" (это "Control Hierarchy"), гда по умолчанию стоит "Display", а надо было поставить "ETH Remote" и сохранить -- после этого стал впускать, именно по user/user (пробовал другие -- "Incorrect username or password").
    • Там слева появляется более расширенное меню, но толку от него нет: в пункте "Configuration" подпункты "Accessory", "I/O X1", "TMP" и "TMP-Service" просто ничего не делают -- никакой реакции, как и на пункт "Parameters".

      Пробовано разными версиями Firefox: и x10sae'шным 45, и p320t'шным 78, и свежим 115 -- результат одинаков.

      Попытка просмотра исходников (Ctrl+U, Page Source) показала, что какая-то реакция в HTML вроде есть. Но не работает. Или этим дятлам Internet Explorer подавай?

    • Хотя в "TURBOVAC i(X) TURBO.CONTROL i Software Description Operating Instructions 300702826_002_C0" на страницах 14-18 рассказывается, что эти подпункты должны делать, со скриншотами; и конкретно "Configuration|TMP-Address", как показано на стр.18 в Fig.16 -- как раз позволяет указать "RS485 address" в диапазоне 0-31.
    • Кстати, тыканье по меню (F3) самого пульта тоже ничего не дало -- там ещё скуднее.

    Так что, увы -- идея включить работу с 485 провалилась.

    Спросить совета у вакуумщиков?

Pfeiffer:
  • 21.07.2024: по ходу разбирательств с Leybold'ом решил из любопытства исследовать аналогичный вопрос про Pfeiffer (т.к. на ЭЛС именно ихние откачные посты) и нашёл результаты, поэтому создаём раздел для их записи.
  • 21.07.2024: собственно...

    21.07.2024: гугление "pfeiffer rs485 protocol" дало:

    Резюме: там СВОЙ протокол, чисто текстовый/ASCII, строки разделяются символом 0x0D (Ctrl+M). (А я-то думал -- вдруг там Modbus, тогда можно будет использовать готовую поддержку...)

Phytron:
  • 03.08.2024: давно стоял вопрос "а что за протокол у Phytron?", т.к. ихние контроллеры шаговиков рассматривались в качестве одного из кандидатов для ВЭПП-5.

    Поиски сначала в январе 2023, а потом в январе 2024 ничего не дали -- такое впечатление, что внятного описания протокола вообще нет, а конкретно для EPICS просто есть реализация "поддержки" от самого Phytron. И было решено "пусть козилабовцы сделают всё для СКИФ, а потом посмотрим -- да хоть сниффером сети глянем, что там шлётся в serial-link через MOXA".

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

  • 03.08.2024: собственно информация.

    03.08.2024: просто что-то вроде протокола действий и находок.

    • Гуглим "phytron rs485 protocol" -- находятся
      • "ZMX+ Stepper motor power stage with ServiceBus" -- PDF на 4 страницы, и там упоминается некий "ServiceBus".
      • "phyMOTION™ Freely programmable motion controller (PMC) for multi axes stepper motor applications" -- страничка с упоминанием "phyLOGIC".

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

        Оттуда ссылка на "Manual (PDF) phyLOGIC instruction set" --

      • "phyLOGIC™ Command Reference for thephyMOTION™ Controller"

        Там встречаются слова "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>
        
    • OK, гуглим «"minilog" 485 protocol» (именно с кавычками!) и находим:
    • Ладно, теперь гуглим по «"servicebus" protocol phytron» (обязательно с кавычками и со словом "phytron", иначе находится сплошь что-то насчёт Micrsoft Azure):
      • "ServiceBus" -- пункт из глоссария:

        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.

      • "Instruction Set for Stepper Motor Power Stages with ServiceBus" -- PDF на 32 странички.

        Там опять есть слова "485", "STX" и "telegram", плюс фрагмент

        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> 
        
        и дальше табличка с расшифровкой/описанием полей. Числа, однобайтные -- адрес и checksum -- передаются парами 16-ричных символов.

        Ну и есть таблицы с описанием разных инструкций.

    Итого: теперь когда будем сниффером смотреть на трафик на СКИФе, то (надеюсь!) хоть будет лучшее понимание того, на что мы смотрим.

    06.08.2024: ага, "смотреть" -- щас!!!

    Как выяснилось, связь с поставленными Козилабом девайсами не через RS485, а по Ethernet -- прямо в корзине (на пару десятков устройств) стоит контроллер.

    • Сканирование nmap'ом (с ключом -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) --
        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
        
        (возможно, этот "filtered" -- что-то врЕменное).
      • "nmap -O -sT -p 3400-3500,20000-24000 phytron-kg" --
        PORT      STATE SERVICE
        22222/tcp open  easyengine
        23232/tcp open  unknown
        
      • UDP -- "nmap -O -sU -p 1-65000 phytron-kg" -- пустота.
    • Гугление:
      1. по "WIZnet embedded" нашло "WIZnet W5300 Embedded Ethernet Controller"
        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."
      2. По "WIZnet W5100" находятся сплошь шилды для Arduino по ценам 700-1500р плюс сам чип за $2.86-$3.90 и за 1440р в Чип и Дип.
      3. А просто замена циферки в URL'е из п.1 даёт "WIZnet W5100 Hardwired TCP/IP Embedded Controller"

        W5100 Datasheet -- PDF на 72 страницы, где можно понять дату выхода по строке "Ver. 1.0.0 | Dec. 21, 2006 | Released with W5100 Launching", плюс там же есть фраза "0.18 μm CMOS technology".

    • По портам:
      • "22222/tcp easyengine" -- это какая-то хрень "EasyEngine", "Admin Tools on Port 22222".
      • А вот по «23232 phytron» и «"phytron" 23232 port» не находится НИЧЕГО полезного.
    • Ладно, стал гуглить "phytron ethernet protocol".
    • Вторым результатом -- "IPCOMM Communication Software for Stepper Motors Control Units IPP, GSP, GCD and GLD" -- PDF на 60 страниц.

      Слова "Ethernet" там нету, но что есть:

      • Стр.23/PDF:25 -- странный пассаж касательно настроек софтины под Windows (IPCOMM RS monitor):
        The following Protocol settings are possible:
        Only STX/ETX
        STX/ETX and checksum
        STX/ETX and address
        Full protocol
      • Стр.29/PDF:31 -- раздел "IPCOMM 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), а то будем записывать в отдельном месте.

  • 09.08.2024: сюда заскладируем информацию касательно "ADS Twincat".
PLC Beckhoff:
C++:
  • 09.11.2024: создаём.
  • 09.11.2024: пытаясь разобраться в деталях работы libCAS, пытался в GDB поставить breakpoint на пару методов класса e2s_PV, этим libCAS'ом вызываемых. И фиг -- «Cannot reference virtual member function "interestRegister"».

    Стал гуглить по "Cannot reference virtual member function", и первым же результатом -- "Dissassembling virtual methods in multiple inheritance. How is the vtable working?" на StackOverflow за 07-05-2014.

    Там обсуждаются вопросы реализации множественного наследования и содержимого "vtable" (VMT).

SPI:
USB-Serial:
  • 20.03.2025: создаём раздел для записи процесса разбирательства с функционированием USB-serial-адаптера на FTDI FT232 от Сергея Вощина, чьё поведение отличается от поведения адаптера GearMo на таком же FT232; а также в надежде, что доразберёмся и запишем сюда рецепт решения.
  • 20.03.2025: суть проблемы -- что modbus_mon с адаптером от Вощина НЕ работает, а вот modpoll -- работает.

    Причём "не работает" -- это:

    1. Не получает ответов от устройства.
    2. При окончании работы -- когда надо сделать close() подвисает на 30 секунд.

    А modpoll ни одной из этих проблем не имеет.

    20.03.2025: собственно, что происходило:

    • Ещё вчера попробовал связаться с девайсом -- был облом.

      Думал всякое -- что адаптер может быть неисправен, кабель, само устройство; что МОЯ софтина может что-то не так делать.

    • Дабы убедиться, решил таки воспользоваться другим софтом -- этим самым modpoll.

      Попробовал -- а он, зараза, работает!!! Данные получает...

      -- 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
    • Проблема в том, что "конкурент" исключительно в бинарном виде, исходники не посмотреть.
    • P.S. Просто запись вышеуказанных данных (8 байт) в порт ("cat /tmp.file.dat >/dev/ttyUSB0") и слежка за ним же в другом окне ("cat </dev/ttyUSB0") также результатов не дали.

    Стал разбираться, с привлечением "strace -vttfs200" для обоих.

    Итак, что там с modpoll:

    1. Порт он открывает не open(), а openat().

      Неожиданно, но вряд ли это принципиально (и хорошо, что я знаю, что это вообще такое -- спасибо покойному BugTraq).

    2. Показалось, что делает close() после каждого обмена.

      Но когда перепроверил, то оказалось, что нет.

    3. НЕ зависает с Вощиным на 30sec на close().

      Я пытался разобраться:

      • Пробовал вставлять tcflush(the_fd, TCIOFLUSH) перед close() -- не помогло.
      • Изначально в modbus_mon не было close(), и тогда зависало на exit_group().
      • ЗЫ: в "man setserial" сказано, что 3000 (в 1/100sec) -- это умолчательное значение параметра closing_wait,
      • но "setserial -ga" значения не показывает, а говорит "infinite", и никакие попытки изменить не дают эффекта, несмотря на root.

        А с обычным /dev/ttyS0 -- изменяется.

    4. Настройку порта он делает как-то чуток иначе; как -- не совсем ясно, т.к. strace показывает дешифровку бестолково (БЕЗ "-v" -- только начало, а С -- малопонятные числа, без расшифровки; но у совпадающих ioctl()'ов они вроде совпадают), а ltrace с его бинарником не работает.
    5. Он после отправки:
      • делает ioctl(3, TCSBRK, 0x1) = 0
      • и потом переходит в режим выполнения в цикле ioctl(3, TIOCSERGETLSR, ...), перемежающийся паузами посредством nanosleep({0, 100000}, NULL) -- по 100mksec?, из которых последний (7-й) явно что-то возвращает,
      • т.к. безо всяких select()'ов он переходит к чтению (сразу 5 байт).
      • При физически отключенной линии он делает это 14 раз, потом ещё раз TIOCSERGETLSR,
      • а потом просто ("наугад?") read(), возвращающий 0,
      • после чего входит в select() на ~1s, после истечения которого говорит "Reply time-out!".

    Выводы, результаты, что ещё делать:

    • Такое поведение с зависанием вроде бы встречалось и раньше, но не могу сходу вспомнить, на каком адаптере и при каких условиях.
    • Поискать среди tcsetattr/ioctl'ов параметр "время зависания"?
    • Попробовать mbusd?
    • Надо обзавестись обычным кабелем DB9-DB9, чтобы работать с "АрсТерм ИП МРН" со своим адаптером.

    21.03.2025: пробуем mbusd.

    • Лень возиться с git'ом, поэтому погуглил "mbusd tgz" и нашёл https://mirror.yandex.ru/macports/distfiles/mbusd/ с mbusd-0.5.2.tar.gz и скачал последний.
    • Распаковал -- а там опять cmake... Причём в ответ на "cmake ." эта тварь говорит
      CMake 3.2 or higher is required. You are running version 2.8.12.2
    • Учитывая, что поциент состоит из 11 .c-файлов, попробовал "в лоб" -- просто gcc их всех.

      Получилось, только в варианте

      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
    • Прогоняем mbusd под "strace -vfs200" -- видим понятную картину: он пытается отправить команду 4 раза с интервалом в 0.5sec, после чего отваливает.
    • ...но вот если не забыть указать UNIT_ID -- например,
      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.
    • Что любопытно, после этого -- после трогания GearMo=ttyUSB1 modpoll'ом -- и от него cat начинает получать то же самое!

      После выдёргивания обоих USB-RS485-адаптеров GearMo возвращается к нормальному состоянию, а Вощин -- так и остаётся "сразу EOF".

    • Но после ещё одного выдёргивания "сразу EOF" стало на GearMo, зато комбинация "cat </dev/ttyUSB0" с "echo Abcd1234 >/dev/ttyUSB1" передала строку с одного на другой...
    • По какому принципу это "сразу EOF" где есть, а где нет -- я так и не понял: после нескольких перевтыканий (при НЕзапущенных на них программах!) оно стало на обоих...
    • @позже, гуляя по Морскому: а не может ли дело быть в том, что этот "EOF" отдавался не на свежевоткнутое устройство, а на /dev/-файл, оставшийся от устройства, но удерживаемый какой-то софтиной (ну забыл Ctrl+C нажать, бывает)? И свежевоткнутое тогда должно было получить следующее имя -- ttyUSB2 -- и просто не участвовать в деле. Вряд ли, конечно -- программы-то обмен вели...

      @ещё позже, дома: неа -- есть только ttyUSB0 и ttyUSB1

    • @ещё чуть позже: написал даже простенький тест work/tests/test_select_file.c -- проверяем указанный файл 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" даёт результат "висим на ожидании, а не отваливаем".
    • Теперь надо будет опять соединить 2 адаптера и попробовать писать и читать в разных направлениях.

    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). Что не помогло (но вряд ли мешает).
    • Немного анализа:
      • В "man termios" сказано
        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".

???
Идеи - что надо обдумать на будущее:
Общая схема работы -- как именно должны данные доставляться из сервера клиентам и как обновляться:
  • 09.11.2006: мысли по теме (в основном -- из зала заседаний PCaPAC-2006), в порядке эволюции:
    • 24.10.2006@PCaPAC-2006: Итого: у нас выходят посылки синхронные и асинхронные.

      Но: пока не отработана одна синхронная посылка, другие -- запрещены. Плохо!

      А ведь -- e.g., для чтения разных каналов с разной частотой -- надо меть много запросов активными одновременно.

      Списки запросов, тэгируемые номерами (Seq)?

    • 24.10.2006@PCaPAC-2006: А чтоб не перерисовывать (и не делать history-shift) всего и всегда, можно-таки перейти на "callback'и", отрисовывая (обновляя!) по приходу данных от каждого сервера knob'ы, присутствующие в его dependants-списке.

      Техническая проблема -- как поступать с ручками, завязанными на НЕСКОЛЬКО каналов:

      1. Несколько каналов от ОДНОГО сервера, а главное -- приходящие одновременно. Явно надо обрабатывать единожды. Вводить какой-то еще "last-processed-sequence-number", и производить обновление только при current_seq!=last_processed_seq?
      2. Каналы от РАЗНЫХ серверов. ХЗ...

      Отдельный вопрос -- ручки, связанные лишь через local-regs (спец. "sid" -- "все"?).

    • 24.10.2006@PCaPAC-2006: (слегка в сторону) СтоИт-таки вопрос, как бы так покрасивее поддерживать всякие искусственные/множественные каналы, типа radio-buttons, disableing'а, etc. -- как сейчас, с торможением на 1 шаг -- криво; "хак через регистры" -- кривовато. Нужем общий, автоматический, красивый механизм!
    • 24.10.2006@PCaPAC-2006: а еще надо будет вводить по-канальную доставку данных, и по-канальные "callback'и" для SetValue().
    • 26.10.2006@PCaPAC-2006: Откладывание обновлений в knob'ах ("wasjustset") -- может, не флагом, а как с OTHEROP -- при свежести?
    • 26.10.2006@PCaPAC-2006: А может, главная проблема со всякими "wasjustset", сдвигами-истории-невовремя из-за обновления всего экрана сразу, etc. (что еще входит в этот список проблем?) -- в неадекватной модели -- циклического группового обновления?

      Да, это экономит много "ресурсов", это проще, но...

      Ведь этот способ обусловлен не сутью, "природой" решаемой задачи -- производства В/В и "связыванием" каналов аппаратуры с экранными ручками (как должно бы быть), а внутренней спецификой устройства транспорта (CX-протокол) и работы сервера -- которые обусловлены спецификой работы с давно помершими транспьютерами.

      Так что -- надо переосмысливать и схему работы сервера, и протокол, и модель функционирования UI (Cdr+cda).

    13.11.2006: вот и сегодня со Шпомером пообщался -- ему нужно просто измерить N раз один канал, для некоей статистики, а все эти длины циклов ему нафиг не сдались. Простая такая человеческая потребность.

    Так что -- железно надо уходить от принудительной цикличности к возможности индивидуальной работы, которая бы эмулировала обычные, локальные-в-программе, измерения. Именно так можно будет максимально приблизиться ко всяким тулзам под dos/windows. А главное -- это ПО СМЫСЛУ задачи.

Еще на тему "как бы порезвее исполнять чтения, и как избегать задержек/неопределенностей, вносимых сетью":
  • 14.07.2007@пляж (опять :): идейка -- а что, если ввести механизм исполнения последовательности команд в сервере?

    14.07.2007: недостаток систем клиент-сервер вообще, и 3-уровневой архитектуры в частности -- в том, что сеть вносит мало того, что просто задержку, так еще и "дребезг" по времени, и хоть сколько-то мелкие времена (примерно <0.1с) уже сложно гарантировать.

    Воспоминание: вроде где-то (в DPS, или еще в каком-то конкуренте/предшественнике X11) была возможность "загрузить" кусочек кода прямо в сервер дисплея, и он исполнялся бы прямо там.

    Идея: а можно ведь ввести и в наш сервер возможность "заказывать" не просто чтение/запись, а исполнение некоторой последовательности команд, между которыми могут быть и программируемые паузы. (Подобные идеи выдвигались для "умного" исполнителя NAF'ов.)

    Как именно это реализовывать -- не очень понятно. Некоторые общие мысли:

    • По какому подпротоколу это заказывать -- вопрос: то ли по обычному (это предпочтительнее, чтоб уж наравне с обычными данными), то ли специальный вводить...
    • Как возвращать результаты -- тоже вопрос. И как синхронизировать (т.е., чтобы, к примеру, "команда" чтения считалась отработанной ПОСЛЕ того, как она чтение реально произошло). Видимо, будет что-то похожее на будущие fork'и, и "когда" возвращать -- тоже чтоб могло указываться в каждой команде индивидуально.

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

    • Для корректного исполнения может понадобиться "запускать" каждый набор в отдельном thread'е... Или обойдемся таймаутами cxscheduler'а?
    • Кстати, а в EPICS-то ихняя State Machine имеет похожие свойства -- исполняется в сервере, и каждый экземпляр в своем thread'е.

      Но зато -- SNL@CA-server едва ли позволит динамическую загрузку!

    А вообще, такая фича, конечно, позволит снизить остроту проблемы недо-реактивности, и позволит не прибегать к дурацким способам типа прямого коннекченья между 1-м и 3-м уровнями (клиент-драйвер), как жто сделали несчастные FESA'маны. И уж наверняка расширит возможности CX -- всякая алхимия типа "подъема энергии" станет проще и надежнее.

К вопросу об обновлении ручек, зависящих от более чем одного канала:
  • 30.07.2007: проблема-то это очень давняя, как минимум с 1998г. есть непонятки, как же это ПРАВИЛЬНО стоило бы делать.

    30.07.2007: пообщался с Гусевым, чтобы понять, как это делает он, и как это принято делать в EPICS. Ага, щас!!!

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

    Во-вторых, EPICS подобную проблему особо решать и не пытается. Там есть специальные типы record'ов, которые могут последовательно либо параллельно опрашивать несколько других record'ов, и сигнализировать о завершении операции -- но и только. А такая "поддержка" немногого стоит. (Ага, а если исходные каналы -- в разных IOC'ах? Тогда сие и вовсе мимо кассы.)

  • 01.08.2007: кажется, наклевывается проект решения этой давней идеологической проблемы...

    01.08.2007: собственно постановка-то проблемы такова:

    • Некоторые ручки зависят от нескольких каналов, и хорошо еще, если это каналы одного сервера.
    • Разные ручки в иерархии могут зависеть от разных серверов. И обновлять по приходу данных одного сервера ручки, относящиеся к другому -- бред.

      Крайний пример -- программа linmag, с сотнями каналов, в которую хотелось добавить несколько канальчиков от другого сервера, который чешет с частотой 10Гц. В результате все те сотни начинали обновляться 10 раз в секунду, нафиг убивая процессор.

    • А хочется -- чтобы ручки обновлялись ровно по приходу ИХ данных, и не трогались бы по приходу "чужих" данных.

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

    • Куча лишней работы -- индивидуальные вызовы callback'ов.
    • Очень громоздкая схема, технически сложная в реализации и подверженная ошибкам.
    • А главное -- она ВООБЩЕ ИГНОРИРУЕТ проблему синхронизации, так что ручки, зависящие от нескольких каналов ОДНОГО сервера (99% случаев мульти-ручек) будут дергаться по N раз подряд. Что есть полная фигня.

    А теперь идея, как объединить плюсы обоих подходов и решить при этом присущие им проблемы:

    • У нас ведь в 90% случаев все каналы относятся к одному серверу, и данные на них приходят скопом, так что по большей части групповая обработка как раз удобна, а разруливать надо как раз исключения.
    • То есть -- надо идти по дереву, обрабатывая ручки, относящиеся к основному серверу, и пропуская те, которые относятся к другим.
    • И, аналогично, по приходу данных от доп. серверов -- надо проходиться только по зависящим от них ручкам (точнее -- подветкам).
    • Т.е. -- надо при realize иерархии найти в ней "точки изменений", и callback'и навешивать уже на них. Первой такой точкой будет вершина дерева -- она привяжется к callback'у основного соединения.
    • Как можно производить обработку -- например, ввести у каждого узла, являющегося "точкой изменения", массив-"список" булевских/битовых флагов, с числом элементов равным числу соединений окна (подсистемы), и при встрече такого узла нырять внутрь него только если вызвавшее-эту-обработку соединение фигурирует в этом списке как используемое.

      Так решится проблема ручек-зависящих-от-нескольких-соединений.

    • Реально остается нерешенным вопрос "как же находить такие точки изменений?". Видимо, просто тупо обходить дерево, имея "текущий" список соединений, и для каждой ручки сравнивая "ее" список с этим "текущим".
    • Кстати, оптимизация: если подсистема использует ОДНО соединение, то ни у одного узла не будет отводиться этот массив-список -- поскольку ни один узел и не будет "точкой изменения".
    • Отдельный вопрос -- синтетические каналы, привязанные только к регистрам: у них ведь вообще зависимость от соединений будет отсутствовать.

      Видимо, у них список просто оставлять NULL, так что они будут обновляться одновременно со своим "содержателем".

    Ручки-BIGC, возможно, все равно будут работать по своим индивидуальным callback'ам. По крайней мере, можно ввести выбор -- либо оптом, со всеми, либо индивидуально. Это и будет значением флага "когда", указываемым при заказе такого большого канала -- "immediately" или "at end of cycle". С ручками-USER -- вообще пока некоторые мраки, их обдумаем отдельно.

    Недостатки такого решения -- точнее, проблемы, им пока не адресуемые:

    • У всех epics'ных каналов будет одно-единственное "соединение", так что они в этой модели будут обрабатываться ВСЕ по приходу данных одного.

      Тут дело в отсутствии в epics'ном CA самого понятия "цикла", да и вообще средств "синхронизации" для экранных ручек.

      Возможно, проблему можно как-то решить на уровне cda_d_epics, введя там симуляцию цикла.

    • Как и раньше, отсутствует оптимизация "отображать только при изменениях".

      Но она в любом случае сильно путает карты всяким histplot'ам.

    • Ну и некая неясность, как обходиться со схемой доступа "local::" -- но это, скорее, вопрос к cda.

    Отдельно стоит упомянуть запись истории: давно ясно, что там надо сделать ДВА варианта.

    1. При histring_frqdiv>=0 -- складывать в историю каждое N-е ОБНОВЛНИЕ.
    2. При histring_frqdiv<0 -- писать в историю значение каждые -histring_frqdiv секунд.

      Для этого введем в Cdr "heartbeat"-функцию, которая вызывается раз в секунду и проходится по дереву и делает сдвиг истории тем, кому надо.

    09.08.2010: в CXv2 технология "точки изменения" вставлена, и успешно работает. Так что -- идея абсолютно правильная, именно так тут и сделаем.

    01.04.2014@Снежинск-каземат-11: насчёт "histring_frqdiv" -- ясно, что надо делать совсем по-другому, БЕЗ неё, а надо обновлять историю просто периодически -- см. bigfile-0001.html за 13-07-2011. Так что то про histring_frqdiv за 01-08-2007 ставим "withdrawn".

Хитрость для разных парсеров в структуры:
  • 19.08.2007: иногда в архитектурах с формализованными парсерами-для-одного-поля, вызываемыми "главным циклом парсинга" (paramstr_parser, Cdr_fromtext.c::ParseKnobDescr()), требуется более хитрая работа -- например, парная работа с полями (something+something_count). А архитектура-то для такого не очень заточена -- она рассчитана на атомарные поля.

    19.08.2007: решения этой проблемы есть, и даже два разных.

    1. "Правильно" -- сгруппировывать все причастные поля в один struct, и уж ЕГО делать полем структуры-в-которую-парсится. И оное struct-поле будет вполне атомарным.

      Но иногда содержимое структуры предопределено "свыше", как данность, и надо парсить туда. В таком случае работает второй, "обходной" способ:

    2. Если парсер-поля точно знает, что за поле он обрабатывает -- т.е., оно уникально -- то можно просто внахаловку получить из указателя на поле указатель на всю структуру, и тогда уж работать с любыми ее полями напрямую. Делается это примерно так:
      rec_t *rec = (rec_t *)(((int8 *)field_p) - offsetof(rec_t, field));

      Именно такой подход и используется в PARAM_fparser()'е.

О синхронизации и продолжительных операциях:
  • 28.08.2007: с нынешней архитектурой -- куча независимых каналов и представление ЦАПИ как больших каналов -- возникает пара вопросов:
    1. Как синхронизовывать работу МНОГИХ устройств -- в частности, как делать одновременный запуск всех ЦАПИ? А ведь запусков может требоваться несколько -- как для нескольких разных устройств, так и с разными "метками" для одного устройства, а еще -- несколько АНАЛОГИЧНЫХ запусков подряд.
    2. Как получать ТЕКУЩЕЕ состояние исполнения некоего файла?

    Вопросы эти наиявнейше вылезли при обдумывании системы управления карботроном -- да и с любым кольцевым бустером будет то же самое.

    28.08.2007: в принципе-то, все это можно делать и сейчас, без добавления какой-либо еще инфраструктуры, просто при помощи "каналов" и "атрибутов больших каналов":

    1. Про запуски в любом случае вопрос -- КАК их вообще делать? И ответ-то очевиден -- при помощи командного канала. Вот и всякие хитрые запуски -- с метками, бродкастные, и т.д. -- также командными каналами.

      Например, если устройство поддерживает 8 таблиц -- то делаем 8 больших каналов, плюс 1 обычный командный канал (номер таблицы и флаг "бродкаст"), плюс 2 статусных канала (номер исполняемой таблицы и позиция в ней).

    2. Для текущего состояния можно завести либо атрибут, либо обычный канал, который и будет отдавать либо номер шага, либо "сколько осталось". А уж драйвер будет обновлять этот канал либо сам, раз в N миллисекунд, либо по пакету 0xFE (точнее -- в любом случае по пакету 0xFE, вопрос только в причине его появления).

    Конечно, это не шибко-то красиво, но работать будет.

    P.S. Кстати, очевидно, что драйвер "CANDAC16-ЦАПИ" проще будет сделать ОТДЕЛЬНЫМ от обычного candac16 -- поскольку сочетать "в одном флаконе" работу в обоих этих режимах довольно некрасиво (точнее, -ЦАПИ-вариант может и позволять отдельные уставки, являясь как бы расширением обычного candac16, но использовать его ВСЕГДА -- overkill). 26.02.2013: а вот и неа, давно уже НЕ очевидно! И более того -- давно разработана и реализована модель функционирования табличных каналов в козачиных устройствах, хорошо комбинирующая ЦАПИ с одновременной работой не-задействованных в таблице отдельных каналов ЦАП. Так что считаем этот P.S.-абзац за "obsolete" (а предыдущие соображения -- вполне релевантны, и даже в основном реализованы).

О сохранении/восстановлении режимов:
  • 28.08.2007: Малютин развыступался, что его не устраивает сохранение шага каналов вместе с режимом: мол, режим просто так грузить нельзя -- там могут быть не-должные-скачком-меняться каналы.

    Поэтому он жаждет, чтобы "параметры, не относящиеся к крейту, сохранять в другом конфигурационном файле, загружаемом автоматически" (при запуске программы -- грузим, при изменении -- автоматом сохраняем).

    28.08.2007: ну и -- как, а?

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

In-server channel API:
  • 05.07.2008: еще давно стало ясно, что надобно уметь из драйверов доступаться к каналам других драйверов этого же сервера, и, в идеале, с тем же API, что из клиентов. (По давности -- см. NOTES.html за 13-03-2005/16-03-2005 ("inter-driver channel API") и bigfile-0001.html за 31-10-2005 ("Vsystem").)

    05.07.2008: учитывая, что мы собираемся дать драйверам возможность доступаться к другим серверам, а для этого им будет дадена cda, вечерком (уже лег в постель!) в голову пришла мысль:

    Чтоб API доступа к каналам других драйверов изнутри сервера был таким же, как извне -- надо его сделать ТЕМ ЖЕ. Т.е. -- реально через обязательно имеющуюся (если драйверам вообще доступ к каналам нужен) cda. А такие "внутрисерверные каналы" -- просто еще один протокол доступа (аналогично "local:").

    Тогда задача решается автоматом -- точнее, сама проблема отсутствует как класс.

    Вопрос лишь в производительности/оптимальности.

    15.07.2008: и, кстати, учитывая, что в сервере для возможности "frontend"'ам получать данные сразу в момент из прихода от драйверов -- ОБЯЗАТЕЛЬНО будет механизм "повесить callback на канал", то при желании будет возможно реализовать именно "быстрый" внутрисерверный channel API для драйверов.

Насчет БД конфигурации:
  • 07.07.2008: надо дать программам возможность узнавать, например, "тип устройства" такого-то блока/канала. Это чтоб программа-"oscilloscope" могла определить, с каким устройством она имеет дело, и вела б себя соответственно.

    И вообще надо б уметь кучу всякой информации из этого "дерева-БД" высасывать -- типа набора атрибутов большого канала, с диапазонами etc.

    Вопрос -- КАКУЮ информацию стоит отдавать (чтоб именно НУЖНУЮ, а не тупо давать полный доступ ко всему, что знает сервер), и как, "с какой парадигмой"?

Насчет типов данных:
  • 26.07.2008: Надо все-таки определиться с поддерживаемыми типами сейчас, хватит уж откладывать.

    26.07.2008: итак:

    1. Делаем только скалярные типы, массивы и строки. Структур -- пока не надо.
    2. Массивы -- видимо, как в EPICS'е, указывая количество (скаляры=1).
    3. Строки -- в начале LE-int16-длина, потом данные, БЕЗ '\0'.
    4. Тип скаляров -- указывается размером, int8: младшие 5 бит -- размер в байтах, старшие 3 -- вид (0:int, 1:real). Пока поддерживать int:1,2,4, real:4,8.

      Или, может, лучше символом? Буквы b,s,i,l,f,d (byte, short, int, long-long, float, double); целые -- фиксированной битовости (8,16,32,64), а вещественные -- в зависимости от платформы.

    5. На основании этого типа и выполняется конверсия (сервером? Или клиентом?).
    6. Отдельный вопрос -- как, если вообще возможно, выполнять конверсию между скалярами и массивами? Использовать 0-й элемент массива, или вообще запретить её?
    7. Еще вопрос -- как всё-таки работать с большими каналами, какие атрибуты у них иметь?
    8. Как устроить API сервер<->драйверы? Сейчас-то {int count, int32 *values}, а сделать как? По общему смыслу выходит - {int count, int datatype, int dataunits, void *data}. Но это шибко сложно -- особенно при передаче ДРАЙВЕРАМ . Или драйверам, как и клиентам, сам сервер будет давать данные в уже запрошенном виде?
    9. Сериализацию вещественных типов -- можно подсмотреть в той библиотечке tpl (это и есть ключевой пункт, дающий отмашку на переход с int32 к более хитрым типам).

    Реализация remote_drv.c станет весьма нетривиальной.

    P.S. Если ЭТОТ вопрос решить/проработать, то из сложностей останутся только временнАя характеристика работы -- "когда" отдавать данные. Ну и, плюс, способы работы с большими каналами -- но это уже вытекает почти автоматом из типов данных и временной характеристики.

    17.12.2009: маленькое дополнение насчет строк: длину надо передавать как LE-int32 (а не le-int16) -- во-первых, для унификации (раз уж всё равно все числа 32-битные), а во-вторых, чтоб уж были допустимы строки и длиннее 65535 символов.

    Чуть позже: а вообще -- актуально ль это? Или всё равно рассматриваем строки как массивы типа 't'?

    02.10.2018: а теперь краткое описание РЕАЛЬНО выбранных решений.

    1. Да, скаляры, массивы и строки (которые являются частным случаем векторов -- с типом "символ"). Структур нет и нет даже идеи, как бы можно было их реализовать.
    2. Массивы -- да, указывается количество. Скаляры -- длина=1.

      Важное отличие от EPICS и записанного в тогдашнем проекте: в конфиге и при создании каналов указывается МАКСИМАЛЬНАЯ длина, а значение в векторном канале может содержаться длиной от 0 до максимальной.

    3. Строки -- просто массивы символов, БЕЗ всякой специфичности с "LE-int16-длиной".

      Зато на стороне клиента (в cda) для TEXT-данных всё же резервируется одна дополнительная ячейка в буфере "текущее значение", и туда кладётся NUL -- чтоб удобнее було работать из C/C++.

    4. Тип скаляров:
      • Хранится в типе cxdtype_t, который является 8-битовым целым, и там размер указывается в младших 3 битах (записывается показатель степени двойки: 0:1байт, 1:2байта, 2:4байта, 3:8байт, 7:error), тип -- в следующих 4 (0:unknown, 1:int, 2:float, 3:text), а старший бит отдан под unsignedness.
      • В конфигах (и в командной строке) указывается символом b,h,i,q,s,d,t,u (h:int16, s:single).
      • Для стандартных типов есть enum-константы CXDTYPE_nnn.
      • Также дополнительно тип CXDTYPE_UNKNOWN может функционировать в роли "вариант": резервируется буфер указанного размера для данных, а при появлении данных также запоминается и "текущий" тип.
    5. Конверсия выполняется И сервером, И клиентом: кому прислали, тот и выполняет конверсию в нужный ему формат данных.

      И реально конверсия выполняется аж в 4 точках:

      1. В сервере -- при получении данных от драйвера.
      2. В сервере -- при отправке драйверу полученных от клиента данных.
      3. В клиенте -- при получении данных от сервера.
      4. В клиенте -- при получении данных от программы.

      Конверсия:

      • Выполняется между совместимыми типами: между int и float, между типами разного размера, плюс учитывается (без)знаковость целых.
      • НЕ делается между числовыми и символьными (операция считается неопределённой).
      • НЕ делается между символьными разного размера (8-битовым TEXT и 32-битовым UCTEXT). Просто не сделано, т.к. преобразование кодировок символов -- занятие более сложное, чем просто преобразование размера (да и не используются у нас пока UCTEXT).
    6. Конверсия между скалярами и массивами НЕ выполняется. Исключение следует из того, что массив в 1 элемент эквивалентен скаляру:
      1. Скаляр всегда можно записать в вектор, который получает размер 1.
      2. Вектор размером 1 элемент ложится в скаляр.
    7. "Большие каналы" пока сделаны просто в виде разрозненного набора каналов, объединяющихся в "кортежи" лишь логически. Идеология (если позволительно это так именовать) pzframe (годная только для измеряемых каналов), один из каналов "кортежа" назначается триггером, сигнализирующим, что все данные готовы.

      Полноценной атомарности пока нету, и КАК её сделать -- самая великая на текущий момент задача.

    8. API сервер<->драйверы сделан "по максимуму" -- передаётся кортеж count,addrs[],dtypes[],nelems[],*values[] (от драйверов серверу отдаются также rflags[],timestamps[], причём последние опциональны).
    9. "Сериализация" вещественных типов оказалась тривиальной: просто преобразуется порядок байтов, в зависимости от endianness платформы, а само содержимое байтов везде одинаковое, благодаря повсеместному использованию представления по стандарту IEEE 754.
  • 24.08.2008: отдельный, технический, но важный вопрос: как именно указывать способ преобразования инженерных единиц (померянные с АЦП вольты) в "логические"?

    24.08.2008: исходная посылка: в преобразовании должны иметься коэффициент и сдвиг нуля. Формул, в принципе, несколько штук/вариантов, математически эквивалентных.

    Сейчас -- в CXv2::cda -- используется преобразование v=c/r-d, где v -- осмысленное (операторское) значение, c -- инженерное значение, считанное из устройства, r -- коэффициент, d -- сдвиг нуля.

    Но с такой схемой вечные проблемы:

    1. Я постоянно забываю, что делается с коэффициентом -- то ли делится на него, то ли что... Приходится лезть в cda.c::DecodeData().

      Плюс, даваемые юзерами коэффициенты еще и интерферируют с коэффициентом 1000000 (микровольты->вольты) -- надо вспоминать/разбираться, этот 1000000 умножать на тот коэфф, или делить. Но это-то следствие того, что преобразование device-int->phys-double совмещено с преобразованием инженерное->операторское.

    2. Юзеры почему-то имеют привычку давать коэффициент для УМНОЖЕНИЯ, а не деления.
    3. Они временами дают формулы, где сначала сдвигается ноль, а потом работает коэффициент. Да и сдвиг нуля имеют привычку указывать в другую сторону -- в смысле, с другим знаком :-)

    Так что, в принципе можно рассмотреть еще несколько вариантов, тех самых математически эквивалентных:

    • v=c*r-d
    • v=(c-d)/r
    • v=(c-d)*r

    Короче -- реально вопросов 2:

    1. Коэффициент -- делить на него или умножать?
    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.

    • Вопрос 1: почему тут была выбрана именно такая модель (и когда?)?
    • Вопрос 2: и не переделать ли на "как везде"?

    02.10.2018: в порядке информации: а вот в EPICS сделано чуть иначе:

    • Code=(Value-EOFF)/ESLO и, соответственно, Value=Code*ESLO+EOFF.

      (EOFF и ESLO могут указываться в свойствах рЕкорда.)

    • Т.е., переход от {ESLO,EOFF} к {R,D} -- R=1/ESLO, D=-EOFF.
    • Т.о., там иное понимание сдвига нуля.

      Возможно, тамошнее "более естественное" -- указывается просто точка, в которой ноль (а то у нас в термостабилизации D=-40, т.к. нулевому измерению соответствует 40 градусов).

    • А "коэффициент" -- тоже как бы наоборот, он определяется прямо самим термином "slope" (наклон, градиент).

      Хбз, какой из вариантов "правильнее", хотя математически они эквивалентны, а разница сводится к разнице между тангенсом и котангенсом.

    Резюме: да, возможно (особенно с учётом замечаний 2,3 от 24-08-2008), выбрана была не самая удобная для взаимодействия с юзерами модель. Причина сего проста -- объективных резонов для выбора какого-либо из вариантов в тот момент не было, увы. На функциональность выбор не влияет, а лишь на удобство, так что теперь уж с этим жить :).

  • 23.10.2012@Снежинск-каземат-11: о конверсии типов между числами и строками.

    23.10.2012@автобус-из-каземата: бывает желание с некоторыми каналами обходиться в терминах не 0/1, а сразу "On"/"Off", "Yes"/"No", etc. Т.е., мочь иногда выполнять простые преобразования между числом и строкой; в основном -- в пределах простых lookup'ов.

    Мысли о том, как бы это можно было сделать.

    • Производиться оно должно в сервере -- у него есть все необходимые для преобразования исходные данные.
    • Тупое преобразование (числовой канал, а клиент строковый) сделать просто: чтение -- sprintf(), запись -- strtol()/strtod().
    • А для lookup'ов -- драйвер должен предоставлять этот самый lookup. Видимо, в описании свойств каналов -- рядом с (phys_r,phys_d,phys_q), boss (в правильном его варианте).
    • Вопрос в сторону: а как утилиты командной строки (cx-setchan) понимать тип указанного им значения? Догадываться (не дали ли ошибки strtol(), strtod()), или лучше ввести для строк/массивов обязательный префикс типа?

    06.03.2020: всплыл один нюанс (при обсуждении возможных способов всё-таки реализовать эту конверсию между int-enum'ами и строками, записанном вчера, 05-03-2020 в раздельчике о конверсии между целочисленными и символьными):

    • Как организовать преобразование из приходящих (серверу) строк в целочисленные значения -- примерно понятно: "догадаться" о надобности такого преобразования (хоть по флагу USGN_MASK, хоть по эвристике на основании nelems участвующих), а потом поискать по таблице.
    • А вот ОБРАТНОЕ-то преобразование -- из целого числа в строку -- как делать?
      • ТУТ откуда возьмётся флаг USGN_MASK? Должен быть на целевом строковом канале?
      • А эвристику на основании nelems как? Тоже предполагаем, что всегда есть max_nelems>1 на целевом канале?

      А всегда ли эта требуемая для определения необходимости конкретно такого преобразования информация будет доступна?

    Впрочем, тут опять главная проблема та же -- то, что у нас на уровне cxsd и cda нету никаких enum/lookup. Т.е., разговор аналогично беспредметен.

    06.03.2020@дома: есть "простое" решение -- ввести отдельный CXDTYPE_REPR_ENUM, который везде рассматривать как числовой и считать совместимым с INT/FLOAT, и только при необходимости конверсии в/из TEXT работать с ним иначе.

    Правда, тут шибко много минусов:

    • Лишнее усложнение кода.
    • Векторные ENUM'ы -- как-то лишены смысла (только как некая "последовательная таблица состояний", а состояний чего?).
    • И конверсия между TEXT и векторными ENUM'ами -- тоже хбз, как бы могла быть реализована (по факту -- там нужен ВЕКТОР строк, по одной на каждый элемент; но такового у нас нет).
    • Как указывать в конфиге -- тоже вопрос:
      1. Какими символами? Их же, по идее, аж 3 штуки надо -- для 8-, 16- и 32-битового.

        Ну ладно, можно ограничиться одним 32-битным -- для которых заюзать символ 'e'.

        А в EPICS, кстати, это 16 бит -- в db_access.h есть строка "typedef epicsUInt16 dbr_enum_t;".

      2. Как указывать сами наборы строк? В EPICS-то это элементарно ложится на их схему с индивидуальным указанием всего-всего для каждого экземпляра record'а.

        А у нас архитектура другая: разделяются ОПРЕДЕЛЕНИЯ "типов" и ИНСТАНЦИИРОВАНИЕ, где все свойства берутся из типов.

        Пока единственное, что приходит в голову -- отдельно определять "типы" ENUM'ов, вроде

        enumdef ENUM_NAME {
            STRING_A  VALUE_A
            STRING_B  VALUE_B
            . . .
            STRING_N  VALUE_N
        }
        
        а потом в enum-каналах ссылаться на эти "типы". Хотя как ссылаться -- тоже вопрос.
    • И как клиенту передавать список возможных вариантов -- вопрос отдельный. Технически-то проблема невеликая: ну добавить в протокол ещё один тип информационного Fork'а, для передачи списка дуплетов СТРОКА:ЧИСЛО. (А передавать, кстати, лучше так: обязательно количество N, потом N чисел, а за ними N штук asciiz-строк).

      Но главный вопрос -- а клиенту-то с ними что делать?

    А плюсов по-прежнему почти нет -- по причине малонадобности ENUM'ов в CX'е вообще.

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

  • 03.07.2014: о текущем насущном...

    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".

  • 30.04.2015: надо б научиться делать конверсию между int-типами и символьными скалярами. Или лучше вообще между числовыми и символьными: чтоб пишешь в канал 'c' значение 65, а в нём появляется символ 'A', и аналогично с массивами.

    02.05.2015: пара замечаний:

    1. Такое точно будет конфликтовать с пред-предыдущим пунктом о конверсии между СКАЛЯРНЫМ числом и строкой.
    2. Оно выглядит интересно, но реального практического применения не имеет -- больше для игрищ да отладки.

    Так что, записать -- записали, а реализовывать спешить незачем.

    05.03.2020: а вот сделано! Спокойно за полдня после обеда, и в максимальном объёме -- между любыми целочисленными и символьными типами одинакового размера.

ВременнАя схема работы:
  • 28.07.2008: надо наконец все же решиться и определить ТОЧНО, как именно система будет позволять работать с данными.

    28.07.2008: исходные посылки:

    • Механизм CXv2, когда вся шобла от одного сервера приходит оптом в конце цикла -- конечно, удобна, для >90% случаев.
    • Но зачастую она неадекватна. В частности, когда в рамках ОДНОГО сервера работает несколько железок/драйверов с РАЗНЫМИ характерными временами. Пример -- МНТЦ2257/C13, где термопары -- медленные, а вот пирометр -- вплоть до 50Гц, из-за чего его пришлось представлять "большим" каналом.
    • Старая идея -- сделать 4 варианта, указываемых в заказе:
      1. в конце цикла,
      2. с указанной частотой,
      3. по готовности,
      4. при отклонении значения более чем на D.
      (варианты 2 и 4 имеют смысл только для подписки).

      Главный "прикол" тут -- что данные будут приходить не заранее определенным пакетом, а индивидуально, так что надо будет разбираться, что именно пришло (как -- давно решено: по номеру-позиции в заказе), и "дергать нужный callback". Но это касается, видимо, подписки -- поскольку для просто запроса давно было решено в качестве "способа" для всего пакета выбирать из всех fork'ов тот, что самый медленный.

    • И вообще -- чтобы "способы" заказа обычных и больших каналов были одинаковыми.

    Кстати, связанная тема -- это обработка в cda/Cdr с учетом "выборочного обновления ручек по приходу данных" (conns_u).

    30.04.2014: общий принцип API каналов: наверное, API разных уровней должны быть максимально схожими -- что серверный, что cda<->dat_p, тогда работой cda/cxlib/cx_proto будет просто туннелирование.

    У нас же исторически (еще с транспьютеров) привязка к "циклам" -- вот и маемся.

    (Осознано по результатам обдумывания "как бы должен выглядеть API cda-плагинов доступа к данным (dat_p)".)

    Но это небесспорно, конечно:

    1. "Просто туннелирование" не учитывает множественности клиентов и разрешения конфликтов (да и прочей хитрой логики, типа "цветов").
    2. Циклы (точнее, групповой возврат данных) не только "экономят ресурсы", но еще и упрощают работу с толпой каналов сразу.
  • 20.08.2008: отдельный прикол с каналами записи -- там тоже может требоваться синхронизация!

    20.08.2008: по результатам обсуждения с Карнаевым СУ для карботрона HITS: там могут быть некоторые каналы, которые можно записывать не в произвольный момент времени, а лишь по некоему внешнему импульсу.

    Исходные данные: в некоем канале пролетает пучок, пролетает он быстро, раз в 100ms, занимая мгновение этого периода (а остальное время 100ms-мгновение вроде как можно писАть), но надо быть точно уверенным, что ЭТОТ пучок пролетел при ЭТИХ токах в магнитах. Поэтому -- надо обязательно производить запись в момент, когда пучка нету.

    Т.е.: при получении от клиента команды записи надо не производить запись, а складывать значение в некий буфер (это может быть и c_next_wr_val), выставлять флажок, что таковое значение имеется, а потом по получении внешнего запуска (отдельный вопрос -- как и откуда) производить собственно запись в аппаратуру.

    Возможное решение на сейчас: если сие будет делаться на козачиных CANDAC16 или подобных, имеющих режим ЦАПИ, то можно работать через одношаговые таблицы. Т.е., при получении запроса на запись драйвер будет отправлять не команду уставки непосредственно в ЦАП, а команду записи в таблицу -- так, чтобы модифицировать конкретно нужный канал (благо, команда 0xF2 как раз пишет ровно 4 байта). При получении же команды внешнего запуска "можно!" драйвер будет отправлять команду F7 -- "исполняй!". Таблиц можно иметь ДВЕ -- "текущая", используемая в данном интервале-100ms, и "следующая", программирование которой выполняется.

    Главная проблема в том, что у Козака программируются не значения, а инкременты, что делает все эти махинации с таблицами очень муторными. Т.е., надо в начале иметь заполненную-нулями таблицу, в которую при надобности производить запись. В идеале -- одновременно с каждой командой F7 сразу же отправлять кучу команд, приводящих к созданию "нулевой" таблицы. Радует, что изменения-то скорее всего будут происходить редко, так что драйвер таблицу заготовит, и она будет ждать момента, когда понадобится реально поменять некое значение. Так что траффик получится небольшой.

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

    Ой-е...

    24.08.2008: обсудил эту идею с Козаком (это в воскресенье, блин!). Резюме: да, это будет работать.

    А второй его совет -- надо все-таки хорошенько трясти клиентов, чтобы представляли обоснования таких требований. А пока не обоснуют (и это должен быть НЕ ТОЛЬКО Карнаев, но и иные посвященные/компетентные) -- посылать подальше.

  • 22.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: И вообще, насчет внешних запусков...

    23.08.2008: раз уж это вещь такая для карботрона (и Снежинска!) глобальная, то:

    Не сделать ли эти запуски "событиями", и не под-вернуть ли двухшаговую структуру цикла, просто дать возможность начало (а не конец? :-() цикла определять не таймером, а внешним сигналом. И, тогда, начало второй стадии либо уже через N микросекунд, либо тоже внешним импульсом. И, поскольку даже в тактируемых-внешне серверах могут быть обычные постоянно-измеряемые каналы, то надо под-обобщить эту архитектуру, чтобы, в частности, отсутствие внешних запусков не мешало таким каналам продолжать измеряться. Ну и что, мы приходим к множественным параллельным циклам внутри сервера, аналогично epics'ным scan periods(?)?

    Короче:

    1. Надо хорошенько это обдумать, составив временнУю диаграмму цикла карботрона HITS и Снежинска - что когда делать.
    2. Видимо, из основного "ядра" сервера надо переложить управление циклами на отдельный модуль, или - плагин-место, чтоб была гибкость.
    3. Надо драйверам мочь легко ловить все эти стробы - так что вместо метода NewCycle() ввести какой-то более общий способ ловли "временнЫх меток" ("событий") от того модуля.

    08.05.2009: да, по разговорам про СУ уже ТНК (Зеленоград) стала ясна необходимость подобного механизма. Точнее -- более общего его варианта:

    • Первоначальная потребность -- чтобы некоторые каналы не трогались бы в определенные периоды времени.
    • Идея: вводим понятие "фаз" цикла, плюс "цветов" каналов. Каждый канал может быть "покрашен", а в каждой фазе трогание некоторых цветов может быть запрещено. Так что при обращении к каналу сервер проверяет, что если "chancol_status[c_color[chan]]==nowrite", то запись либо обламывается, либо ставится в режим ожидания (и будет отработана при разрешении трогать этот цвет.
    • Фазы и цвета вводятся в начале конфигурации сервера -- строками, а для внутренней адресации им сопоставляются числа (последовательные номера). Плюс -- фаза по умолчанию (==0) и цвет по умолчанию (==0).
    • API фаз содержит функции типа
      • "GetPhaseByName()" и "GetChancolByName()".
      • "SetSrvPhase()" -- может вызываться хоть драйвером по сигналу от блока, хоть модулем по сигналу от другой программы, etc... Это даёт максимальную гибкость.
      • "SetChancolStatus(chancol, status)" -- уставляет статус плюс инициирует отработку отложенных запросов. "()" "()" "()"
    • Само же изменение разрешенности записи можно делать как "вручную" -- вызывая "SetChancolStatus()", так и задав серверу матрицу соответствия, каждой фазе по строке, в ячейках которой указываются статусы-разрешения цветов для этой фазы.
    • При запрещенности записи можно либо ставить запрос в ожидание, либо обламывать (как при блокировке).
    • Можно также распространить этот механизм и на чтение. Плюс, если мы знаем, что измерения некоторых каналов в некоторые моменты времени бессмысленны, то можно игнорировать присылаемые драйвером данные.
    • Объединение этих вариантов -- сделать статусы наборами битовых флагов, типа "write disabled", "deny write", "read disabled", "ignore results".
    • Реализация отложенной записи: у каждого цвета имеется односвязный список каналов, в которые имеются отложенные запросы. Когда на канал приходит запрос, который должен быть отложен, то канал добавляется к этому списку (через поле c_nextinlist[] -- индекс следующего в списке, !=0). И при переходе статуса в состояние "разрешено" сервер пройдет по списку и отправит все ожидающие запросы.

    14.05.2009: ну и какой-то API для циклов также нужен -- поскольку требуется понятие "номер цикла работы установки".

    14.10.2013: добавлено "свойство" cxsd_hw_chan_t.chancol. А прочие поля, для функционирования (типа заблокированности) -- пока еще нет.

  • 24.08.2008: Поскольку понадобится ЧАСТО знать ТОЧНОЕ время (например, запрос-то на измерение будет отправлен в нужный момент, а вот когда реально будет вернут результат?), то: надо посмотреть, как реализована (в ядре?) 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.

    Кстати, по ходу дела натолкнулся на ещё любопытные вещи:

    1. "Проблемы со скоростью выполнения системных вызовов «gettimeofday» и «clock_gettime» в AWS EC2 / Блог компании Southbridge" на Хабре.
    2. "How does strace work?" на pacagecloud.
  • 18.06.2013: пара соображений в эту же степь -- о временнЫх аспектах работы с каналами.

    18.06.2013:

    1. Атомарное ЧТЕНИЕ групп: чтоб ответ приходил, когда освежились ВСЕ каналы из запроса.

      Это концепция зеркальная к структурированным запросам ("прочитать такой-то базовый канал при вот этих значениях настроечных") -- чтоб fastadc-клиент получил осциллограмму вместе с её параметрами.

      Реализация: счётчик "еще неполученного" и индивидуальные булевские флаги "не получено" на каждый канал-участник группы. При переходе каждого флага из 1 в 0 делается декремент счётчика, а при переходе его в 0 считается всё готовым и запрос отправляется заказчику.

      Замечание:

      • В общем случае решение корректным не будет -- если каналы несвязанные, то может быть ситуация, когда часть каналов успеет обновиться неединожды, пока другие еще не готовы (потому и нужны булевские флаги, а не просто счётчик).
      • Но для атомарного получения значений связанных каналов -- когда надо выдержать небольшое буферное время перед приходом данных соседних каналов -- оно самое то.
    2. Не ввести ли еще ТРЕТИЙ тип каналов, в дополнение к r и w -- автообновляемый a (т.е., это тоже канал чтения, но запросы на него драйверу передавать незачем, т.к. драйвер присылает данные сам по мере готовности). Единственный смысл -- экономия траффика/производительности, чтоб не слать толпы запросов впустую (как для ИПП/u0632). И какой тогда код-типа вводить -- "-1"?

      Вообще, скорее это просто блажь (в ИПП проблема вызвана неадекватным использованием скаляров), так что ставим "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" уволено вообще, посему и недостаток исчез.

    • С одной стороны, можно в cxsd_hw завести лишнее поле, куда в ReturnDataSet() принудительно складировать реальный timestamp (он там всё равно есть).
    • С другой же, а если подумать -- зачем вообще нужны TRUSTED? Когда и в каких целях оно вводилось (инфа разбросана по файлу)?

    01.09.2015: надо для autoupdated-каналов ставить fresh_age={0,0}. Иначе они в клиентах синеют. (И не забывать событие послать.)

    ...часом позже -- сделано; отправка события скопирована из SetChanFreshAge(). Вроде помогло.

    Тонкости:

    • fresh_age={0,0} делается ТОЛЬКО при is_autoupdated!=0.
    • Соответственно, и уведомления рассылаются тоже только в этом случае.

    Идея в том, что если вдруг какой-то драйвер решает сделать некий(ие) канал(ы) НЕ-autoupdated, то пусть потом после этого вызова отдельно делает SetChanFreshAge(), и уж по нему клиенты будут уведомлены о новом возрасте свежести.

    16.09.2015: что-то предыдущая запись совсем странная: неясно, в КАКИХ клиентах autoupdated-каналы синели и почему?

    Сегодня выяснилось как раз другое -- то ={0,0} наоборот мешало механизму легитимного посиневания при долгом необновлении (сделанному вчера).

    Пока что нуление отключено, и всё прекрасно!

    Чуть позже:

    • Какая-то проблема есть с ist_cdac20. Их каналы Iset_cur почему-то не обновляются (но не у всех!), а dcct2 обновляются, но синеют (и тоже не у всех).
    • Еще чуть позже: проблема реально в исходном канале cdac20.out_cur.

      Он "автоматический", и отдаётся только по вычитыванию ЦАПа, коее происходит лишь при старте и при изменении значения. А 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-устройств/экземпляров так...

      Хотя вот есть непонятки:

      • Теоретически то -- вроде должно быть всегда "правильно": ЛОКАЛЬНЫЙ драйвер ist_cdac20 должен полностью инициализироваться и всё заказать ДО того, как успеют придти все данные от УДАЛЁННОГО cdac20/remdrv.

        Т.к. от удалённого всё придёт только уже внутри sl_main_loop()'а, после инициализации.

      • Но фокус в том, что insrv сам работает через pipe посредством cxscheduler'а.
      • ...Или всё-таки нет, и как только поступят данные от cdac20/remdrv, они должны доставиться заказчику, который уже всё соединение cda<->сервер установил?
      • ...хотя в случае ре-старта (sato) драйвера-клиента ситуация в любом случае окажется кривой -- обновления уже точно НЕ будет.

    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_driver.h (поскольку реально не использовалось, то ABI де-факто не ломается).
    • CXSD_HW_TIMESTAMP_OF_CHAN() не использовался еще с декабря, а сейчас и из cxsd_hwP.h удалён.
    • ...ну и собственно из cxsd_fe_cx.c затенённое использование убрано, и из cda_d_insrv.c вы-#if'ленное тоже.
    • В cda_dat_p_update_dataset() проверка "TRUSTED" убрана:
      1. В проверке на посинение по fresh_age.
      2. В проверке, что считать за update.
    • В Chl_knobprops.c распознавание (и печать "Trusted") -- тоже.
    • Ну и само CX_TIME_SEC_TRUSTED переименовано в __UNUSED__CX_TIME_SEC_TRUSTED.
  • 24.09.2013: близкая тема: возрасты/timestamp'ы.

    24.09.2013: давно ясно, что надо отходить от "возрастов" в сторону "timestamp'ов". Сегодня опять пообщался с Пашей Чеблаковым на тему "как в EPICS вообще и у них на NSLS-II в частности работают с timestamp'ами", и вот результаты общения и мои мысли на тему:

    1. Результаты:
      • Там у них время сихнронизуется между машинами либо по ntp, либо при помощи специальной железной системы. Соответственно, время "едино" на всех машинах и можно сравнивать метки с разных IOC'ов.
      • Оно указывается в наносекундах (хбз как).
      • Есть возможность указывать, что в качестве timestamp'а одного рекорда использовать оный от другого. Это полезно в ситуации, когда точно известно, что некие измерения произведены одновременно (по внешнему запуску), но просто вычитываются и отдаются не сразу; а так у них у всех будет одинаковая пометка.
    2. Рассуждение общего характера: у "возрастов" и "timestamp'ов" несколько разные области применения:
      • Возрасты нужны для GUI -- например, для посинения (как оно исторически в CX и вводилось).
      • А timestamp'ы для "мозгов", чтоб можно было с данными какой-то процессинг и сравнение производить.
    3. Как и что стоит делать:
      • Иметь и возвращать надо именно timestamp'ы, а уж пусть клиенты при надобности делают с ними что хотят, хоть в возрасты конвертируют.
      • Форматы:
        • Брать проще всего их прямо от gettimeofday() (только еще с таймзонами б разобраться...)
        • Отдавать в виде {секунды-01.01.1970,наносекунды}.
          1. Наносекунды -- overkill, конечно, но для простоты/общности; для них по определению отлично подойдёт int32 (~=4*10^9, нано=1*10^(-9)).
          2. Секунды -- можно для принципиального невнесения "проблемы 2038" отдавать в int64, из которого при желании старшие 32бита пока просто игнорировать.

          Итого -- получится {int32,int64}, а с учётом rflags -- {int32,int32,int64} служебных данных на каждое значение канала.

      • Как отдавать "от другого канала":
        1. либо в конфиге указывать, в числе свойств,
        2. либо позволять драйверам в Return*() явно указывать и timestamp'ы.

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

    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 не устраивает:

    1. Она слишком привязывает к *nix, свой же тип является платформо-независимым.
    2. Когда-нибудь вполне могут зачем-то потребоваться НАНОсекунды; или, так: МИКРОсекунд может оказаться недостаточно. Незачем вводить потенциальное ограничение -- без него лучше.
    3. Как ни смешно, но это даст почти совместимость с EPICS (Channel Access): там TIMESTAMP содержит именно наносекунды; хотя секунды считаются с 1990 (против обычного в *nix 1970 и мелкософтного 1980).

    22.08.2014: к ReturnDataSet() добавлен еще параметр timestamps; если

    1. он !=NULL и
    2. значение ячейки sec>0 и
    3. ссылка timestamp_cn -- "брать от" не указана),
    то оно используется, а иначе используется либо текущее время, либо "откуда".

    Т.о., обычные драйверы спокойно указывают timestamps=NULL, и всё пашет как надо, а уж всякие remdrv могут отдавать своё значение.

  • 18.11.2013@Беркаев/1-214: еще в ту же степь: у vsdc2 есть "особенность", что вычитывание осциллограммы длится очень долго и на это время все устройства с меньшим приоритетом (бОльшими KID) ничего не смогут слать.

    Поэтому надо бы вычитывать осциллограммы не сразу после интегралов, а чуть позже, когда и все остальные блоки тоже ответят.

    Вопрос -- КАК?

    18.11.2013@Беркаев/1-214: соображения возникли при обсуждении функционирования БИИПов на канале (К500) вечером у Беркаева. Он в своих "интеллектуальных серверах" это просто вручную явным образом отсрачивает.

    • А в CX как поступать -- что-то делать в драйверах? И как бы?
    • Или как раз тут достаточно будет "фаз"/"цветов"?
    • С одной стороны вроде бы, казалось бы, да, но...

      Ведь запросы на осциллограммы будут приходить ЗАРАНЕЕ, еще ДО "строба запуска", и к моменту реального запуска уже все будут запрошены.

    • Можно, конечно, организовать временнУю диаграмму хитрее:
      • не 2 фазы (1. сбор интегралов; 2. сбор осциллограмм), ...
      • а 3: дополнительная фаза "подготовка" (0-я), во время которой каналы осциллограмм уже заблокированы для исполнения, а только запоминаются;
      • приходящие же во время 2-й фазы запросы должны исполняться драйверами немедленно, чтоб не перетекать в 1-ю фазу.

      Но тут есть узкое место: в самый начальный момент после включения, когда блок еще НИЧЕГО не измерил и показывать нечего. Возвращать пустоту?

    • А ведь насколько проще б было прямо в драйвере иметь некую паузу N миллисекунд (после прихода интеграла), ранее которой он бы НЕ начинал вычитывать осциллограмму... Хотя и ограничение "сверху" тоже нужно -- чтоб вычитывание не залезло б на "фазу 1" вышеприведённой диаграммы.

      Т.е., по факту -- реализация тех же фаз, но в каждом экземпляре устройства отдельно.

  • 17.11.2013: а всё-таки как бы вернуть/добавить некую НЕ-временнУю пометку "когда" -- в какой фазе произошло измерение данного канала?

    17.11.2013: реализовать такое было б несложно:

    • Все возможные "маркеры состояний" нумеруются, каждому назначается номер.
    • Маркер по умолчанию (==0) -- номер цикла.
    • При отдаче значения кроме флагов и timestamp'а также возвращать значение markers[CHANREC.marker]; тип -- int32.

    Есть пара принципиальных вопросов:

    1. Насколько такой механизм реально нужен, и в каких именно применениях?
    2. Не надо ли как-то скрестить/связать "маркеры" с "фазами"?

      В частности, не сделать ли маркером по умолчанию (==0) как раз фазу?

  • 17.10.2016: идеологическая проблема со значениями fresh_age:
    • У нас сейчас предполагается, что fresh_age указывается драйвером, на основании известной ему информации, например, о возможной скорости опроса устройства (пример -- козачиные АЦП, где зависит от количества каналов и времени интегрирования).
    • Однако такой подход адекватен для циклических машин/измерений; для импульсных же измерений это не катит.
    • У импульсных интервал между измерениями может меняться, зависит НЕ от аппаратуры и может быть известен уж точно НЕ драйверу железки.

    17.10.2016: конкретно сейчас потребность возникла на ВЭПП-4, с БИИПами (vsdc2) на ГИД25.

    • Карнаев утверждает, что просто отключать этот механизм (ставить fresh_age=0) не стоит, т.к. посинение "по делу" им нравится. А, мол, надо просто вдвое увеличить границу (до 10 секунд?).

      Т.е. -- вставить в vsdc2_drv.c указание 10 секунд?

    • Но есть еще проблема там же -- триггерируемые каналы (Uuvh): ведь они отдаются по триггеру от VsDC2, но trig_read_drv.c возраст свежести "туннелирует" от канала данных, а не от канала-триггера.
    • Вывод: надо б как-то мочь указывать fresh_age ОТДЕЛЬНО от драйвера. А как?
    • ...и, с учётом trig_read_drv, желательно б мочь указывать "авторитетно" -- чтобы драйвер это указание перебить не мог.

    18.10.2016@утро-еще-в-постели: а если дать возможность указывать значение default'ного возраста (вместо фиксированных 5с) в конфиге -- например, в спецификации ИНФО_ПО_КАНАЛАМ, для каждой группы индивидуально.

    ...кстати, ИНФО_ПО_КАНАЛАМ парсится в тип CxsdChanInfoRec, используемый также для указания групп каналов в CxsdDriverModRec (точнее, МОГУЩИЙ быть использован -- сейчас нигде). Соответственно, драйверы могли бы указывать свои умолчания. Хотя, конечно, только в серверных, а не в удалённых (но и remcxsd_driver'у никто не мешает присылать эту информацию в случае наличия, еще ДО инициализации драйвера).

    19.10.2016: в конфиге-то можно -- например, через двоеточие: r5i100:3.5 (три с половиной секунды).

    Но в идеале нужна возможность указывать возраста не группам каналов, а ИНДИВИДУАЛЬНЫМ каналам -- т.к. даже у одного VsDC2 первый и второй каналы могут работать на разные подсистемы и иметь разные "разумные" возрасты свежести.

    ...не говоря уж о том, что ИНФО_ПО_КАНАЛАМ в основном у всех устройств стоит "~", т.е., используется то, что указано в devtype -- а та инфа столь же фиксированна, как и возвращаемая драйвером.

  • 12.10.2018@вечер-дома: вылезла (вчера) проблемка, после расследования оказавшаяся весьма серьёзной, в чё-то даже академической.

    Симптомы -- что cdaclient, будучи натравлен на readonly-канал, не являющийся IS_AUTOUPDATED, начинает получать измерения оттуда с бешеной частотой.

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

    13.10.2018@утро-дома: (и потом уже на работе тоже): итак, в чём проблема: отсутствует чётко прописанная "конституция" на тему того, кто-когда-как инициирует чтение (кто является "актором"). Для rw и ro-autoupdated-каналов -- такой инициатор есть, а вот для просто readonly -- нету.

    Если попробовать классифицировать нынешние виды каналов в CXv4, то получается следующий список:

    • rw-каналы: изменение производится фактом записи.
    • ro-IS_AUTOUPDATED-каналы: обновление инициируется либо устройством, либо драйвером (по ему известными правилам).
    • Обычные ro-каналы, которые можно читать в любой момент: а вот тут НЕТ адекватных правил, по факту "когда попросят, тогда и читаемся".

      Причём реально есть ДВА варианта таких каналов:

      1. Обычные readonly-каналы, могущие быть прочитанными в любой момент.
      2. Pzframe-каналы (точнее, каналы с "векторными данными", DRVA_READ которых и инициируют запуск измерений).

        ...а также прочие каналы, которые хочется мочь читать как можно чаще, без ориентировки на циклы (хотя это уже сомнительно).

      И получается, что для этих 2 вариантов нужно работать совершенно по-разному: для (2) прочитанность в текущем цикле нужно игнорировать, а для (1) -- нет, даже при режиме ON_UPDATE.

      Но модификации при введении флага CXSD_HW_DRVA_IGNORE_UPD_CYCLE_FLAG была сделана исключительно под нужды (2), так что каналы варианта (1) стали работать неадекватно.

    Кстати, "о конкурентах":

    • Теперь стало кристалльно ясно, зачем в EPICS'е "polling period'ы" (несколько штук),
    • и НЕясно, как это устроено в TANGO. 25.10.2018: поговорил с Сенченко -- см. в профильном разделе за сегодня.
    • 02.06.2019: при изучении DAQmx наткнулся в "Using NI-DAQmx in Text Based Programming Environments" на пассаж (bold мой):
      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.

    Что делать?

    1. Очевидно -- думать, крепко думать.
    2. И чётко осознать, что корень проблемы -- в том, что тогда, в 2008-м, так и не была как следует продумана та самая "временнАя схема работы с каналами".

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

    3. В качестве "оправдания" можно сказать, что я тогда просто и не знал, КАК надо делать (да, малограмотен/малоопытен был -- косяк с выбором варианта для калибровок (v=c/r-d вместо более стандартного v=c*r+d) оттого же).
    4. Также возможно, что и не существует никаких внятных работ на эту тему.

      Я -- не встречал; то ли это означает, что никто не знает или никто не озабачивался классификацией, то ли лишь то, что я малограмотно пытаюсь изобрести велосипед.

    5. 14.10.2018: и надо осознать, что пока не вгрокаюсь хорошенько в данный вопрос -- вряд ли получится решить проблему "атомарных измерений в v4".

    14.10.2018: к вопросу о том, "что же делать":

    • Первоначальная мысль (вчера) была -- а пусть драйверы указывают на те каналы, что требуют специального обращения (игнорирования upd_cycle). Например, у pzframe-драйверов это все data-каналы.

      ...но такая схема не кажется правильной: реально лишь КЛИЕНТ может определить, какой из вариантов (игнорировать/уважать) надо использовать. Например, каналы АЦП -- вроде Ц0601/Ц0612: иногда будет требоваться читать их раз-в-сколько-то, а иногда -- как можно чаще.

      Поэтому напрашивается другой вариант: ...

    • ...разделить режим CDA_DATAREF_OPT_ON_UPDATE и CX_MON_COND_ON_CYCLE на 2:
      1. "Классический" ON_UPDATE -- просто присылать обновления немедленно, не дожидаясь конца цикла.
      2. "Быстрый" вариант (как нынешний, для pzframe) -- дополнительно ещё и игнорировать upd_cycle.

        Название ON_IMMED, суффикс "@i"?

    Как бы то ни было, надо не кидаться "в бой с шашкой наголо", а поразмыслить, чтобы понимание созрело (да-да, "ждание преполняет", блин...).

    И, кстати, забавный факт, который надо принять во внимание:

    • cda_d_insrv.c вообще игнорирует концепцию "on_update/on_cycle", а использует СВОЁ понимание, комбинирующее оба варианта:
      • Данные он отправляет немедленно по обновлению, не дожидаясь конца цикла -- как cxsd_fe_cx в режиме ON_UPDATE.
      • Но затребывает чтение лишь раз в цикл; НЕ повторяя требование по самому обновлению.

        (Что, кстати, было бы весьма чревато для локальных каналов дало бы -- бесконечную вложенность, от которой в cxsd_fe_cx.c::MonEvproc() сделана защита в виде being_reqd.)

      • Плюс, НЕ запрашивает чтение в момент добавления канала ("монитора").

      Так что лозунг "чтоб протоколы CX:: и INSRV:: работали одинаково" пока исполняется несколько сомнительно/посредственно.

    14.10.2018@вечер-18:00-пристройка-лестница-вниз: у нас, возможно (возможно!) смешаны 2 разных сущности:

    1. когда отдавать результаты (on_cycle/on_update);
    2. как запрашивать данные (раз в цикл или в ответ на обновление; а некоторые каналы и вовсе не надо запрашивать).

    Понятно, что эти сущности взаимозависимы: для "раз в цикл" железная взаимосвязь; но вот для "по обновлению" возможны варианты.

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

    15.10.2018@@утро-дома-лестница-вниз-по-пути-на-работу: вот тут видна разница в подходах в EPICS и CX: в первом каждый канал-рЕкорд заводится отдельно и ему "присваивается конкретный цикл поллинга", а в CX при заведении устройства каналы создаются все оптом, и часть из них может не опрашиваться вовсе, а читаются лишь те, на которые приходят запросы от клиентов -- и это явно подчёркивается в отличиях CX.

    24.10.2018@вечер: де-факто свойство "autoupdated" -- аналог EPICS'ного SCAN. Только указывается драйвером, а не в конфиге.

    25.10.2018@утро-дорога-на-работу-между-ИПА-и-ИЦИГ: ведь "ignore_upd_cycle" должно определяться не запросом, а каналом: для pzframe'овых -- да, для обычных -- нет.

    • Точнее, так: игнорировать надо, только если это указано И в свойстве канала, И в запросе.
    • Тогда всё получится как надо: не-pzframe'овые каналы будут запрашиваться не чаще раза в цикл, даже в режиме ON_UPDATE, а pzframe'овые -- сколько влезет.
    • Место для проверки "свойства" -- очевидным образом в ConsiderRequest(), в том же if()'е, где и нынешнее ignore_upd_cycle проверяется.
    • Да, это выглядит очень похоже на решение, так жаждемое с 13-10-2018...14-10-2018.

    25.10.2018@обед-по-пути-в-Гуси-перед-входом-в-здание: уметь бы всё-таки свойства типа "autoupdated" указывать в конфиге (видимо, в channels/devtype). Чтоб:

    1. Свойства можно б было указывать БЕЗ драйвера.

      Это важно для драйвера mqtt_mapping, который может работать для кучи устройств, и ему просто негде взять информацию об autoupdated'нутости.

    2. Свойства бывали доступны еще до загрузки драйвера.

      После обеда: хотя какие именно "свойства" есть СЕЙЧАС, которые бы роляли ДО загрузки драйвера? Не autoupdated -- тот только после имеет смысл. Но в будущем что-то может и проявиться.

    Синтаксис, правда, не вполне ясен: опять в namespace указывать? Не очень это хорошо...

    26.11.2018: реализовано решение в виде «"ignore_upd_cycle" должно определяться не запросом, а каналом». Теперь обновлённость-в-текущем-цикле игнорируется, только если это указано "И в свойстве канала, И в запросе".

    Свойство канала указывается драйвером, с помощью свежевведённого режима DO_IGNORE_UPD_CYCLE.

    Подробности в разделе по cxsd_hw за сегодня.

2038:
  • 05.09.2008: а ведь нам надо держать в уме "проблему 2038 года". По большей-то части к тому времени "либо осел помрет, либо падишах, либо я", да и платформы в основном станут как минимум 64-битными, но: карботрон-то как будет сделан, так софт останется на десятилетия; да и со Снежинском аналогично...

    14.10.2018: с момента написания предыдущей фразы прошло 10 лет.

    Момент "2038" приблизился на 10 лет -- осталось 20 лет вместо былых 30...

    14.10.2018@вечер-дома-ванна: однако надо отметить, что:

    • В протоколе CXv4 timestamp'ы передаются с 64-битными секундами -- просто старший int32 (timestamp_sec_hi32) на стороне сервера всегда форсится =0, а на стороне клиента пока игнорируется.
    • Аналогично в протоколе remdrv/v4 -- timestamp'ы передаются 3 штуками int32, из которых секунды 2 штуками, только старший int32 на стороне драйвлетов всегда форсится =0, а на стороне remdrv пока игнорируется.

    Так что в ПРОТОКОЛЫ корректная поддержка уже заложена, просто пока недозаиспользована.

Статусы запросов:
  • 22.10.2008: в результате обсуждения файла carbotron-cs-channels-20080820 с Карнаевым стало ясно, что надо вводить еще одну "сущность"/"поле" -- "результат выполнения запроса".

    22.10.2008: надо на каждый канал в запросе (а не на весь запрос!) присылать некий "статус" исполнения -- чтобы, например, на попытки писАть в каналы чтения отдавалось бы "недопустимая операция". Т.е., на флаги канала ("is readonly" флагом состояния не является), а именно статус исполнения запроса. Таким образом, пока что наклевываются следующие использования:

    1. Запись в каналы чтения.
    2. Попытка записи в заблокированный канал.
    3. А также -- можно сообщать "второму пишущему" (т.е., при c_wr_req[]), что он создает толкотню.

    Как назвать эту фичу? Видимо, rqstat (request status). Это именно статус, а не флаги.

    Как реализовывать? Некоторые соображения:

    • В сервере rqstat -- отдельное "поле", существующее в буфере ответа (в CXv4, вообще-то, ХБЗ где -- т.к. тут Channel-Manager отвязан от реализации протокола), так что -- по своему отдельному rqstat на каждый канал в запросе.

      В момент получения/обработки запроса это поле инициализируется в 0, и потом, по ходу разбирательства, туда могут добавляться битики RQST_RDONLY, RQST_WRITE_LCKD, RQST_MANY_WRITERS. И, при отправке ответа, этот статус уйдет клиенту.

    • В клиентах же удобнее это отображать уже на rflags -- как уже делается с OTHEROP. И, возможно (?), как-то отражать в knobstate.

      Битов в старшей половине rflags пока навалом -- свободны с 21 по 27.

    • В принципе-то можно было б сэкономить на объеме ответного пакета:
      • Не добавлять в протокол лишнее поле, а передавать rqstat в старшей половине rflags, а уж cxlib при разборе пакета переложит это в rqstat и обнулит использованное место.
      • Хотя это неудобно в реализации в сервере -- надо кувыркаться с упаковкой двух разных сущностей в одно слово.
      • Но, с другой стороны -- а что, если просто СРАЗУ поместить cda-флаги RDONLY, WRITE_LCKD и MANY_WRITERS на те же места, которые используются для транспортировки? Тогда вообще все становится очень просто -- даже концепция "rqstat" как бы полу-излишня, и всю работу делает сервер.
      • И отдача флага NOTFOUND (а это -- именно флаг!) становится тривиальна.
      • Проблема же -- идеологическая: некрасиво, что мы смешиваем в сущности от разных уровней.
      • Похоже -- надо еще раз обдумать наполнение rflags!
Об API модулей:
  • 18.02.2009: некоторые размышления

    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 драйверов/модулей.

    • Постановка проблемы:
      1. Есть множество разных API (cxscheduler, fdiolib, cda, ...), чьи услуги требуются драйверам и прочим модулям.
      2. Но модуль в любой момент может быть деактивирован, и нужна возможность без его участия всё за ним подчистить.
      3. Сейчас решение -- для КАЖДОГО такого API вводится wrapper, которым и пользуются модули, и уж этот wrapper, регистрируя все свои ресурсы, использованные модулем, позволяет сделать подчистку.

        Это весьма слабоудобно.

    • А если бы все затронутые API включали такую парадигму: при запросе ресурса модуль указывет некий свой уникальный ключ/идентификатор, чтобы ресурс им "тэгировался".

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

    • На роль "ключа" отлично подходит нынешний devid (в таком случае трансформирующийся в "modid", или даже обратно в "magicid", как было первоначально около 2000-го).

      Встаёт вопрос об отведении этих идентификаторов (сейчас >0 -- driver, <0 -- layer), но тут можно разбить int32 на два поля: старшие N бит -- "класс" (driver, layer, ...), младшие 32-N -- идентификатор внутри "класса".

    Такой подход к API был бы всеобъемлющ (кстати, он похож на API ядра *nix -- там ключом работает pid). Но, надеюсь, в ближайшее время к нему прибегать не придётся -- ибо это очередное очень радикальное изменение всего и вся. Может, в CXv5? :-)

    Парой часов позже:

    • А вот и нет, по-хорошему надо делать это ПРЯМО СЕЙЧАС, пока всё-равно-изменённый API еще не расползся, "потом" будет сложнее. А дополнительным бонусом станет то, что никакого "cxsd_oslike.c" не потребуется вовсе -- драйверы смогут НАПРЯМУЮ работать со всеми API.
    • С другой стороны, вылезет и пара серьёзных минусов:
      1. Жесткая привязка драйверов к API cxscheduler'а и прочим.
      2. В сами API придётся вставить передачу ДВУХ privptr'ов -- первый в качестве devptr (ссылка на объект), а второй -- именно privptr (специфичная для callback'а информация).

    13.01.2013: еще:

    • Что до "жесткой привязки драйверов" -- ну так остальные компоненты всё равно жестко привязаны к cxscheduler+fdiolib.
    • Еще из неудобств -- ВСЕМ драйверам надо будет обязательно хранить свой devid в privrec'е, т.к. уж библиотекам его всем callback'ам передавать незачем.

      ...или всё ж передавать? А нах?

    • И еще проблема -- драйверный-то API делает close всех зарегистрированных дескрипторов. Значит, и sl_cleanup() тоже должен?
    • Сам уникальный идентификатор надо назвать 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 библиотеки можно зарегистрировать. У всех библиотек вид оного одинаков (так что можно всем регистрировать одного):

    • Принимает пару параметров (func_name,uniq); func_name -- чтоб в лог можно было ругнуться, чего именно нелегитимно хотели.
    • Возвращает 0 если всё окей, и !=0 если нельзя.
    • Неуставленный (==NULL) считается за "всегда окей").

    Нынешние 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: во время экскурсионной командировки в Зеленоград на ТНК посмотрел на работу Козака+Тарарышкина, и вспомнились бродившие в голове мысли на эту тему. Тут описаны идеи/результаты.

    23.04.2009:

    • для диагностических программ надо иметь возможность читать/писать не уже-пересчитанные значения операторских каналов ("амперы"), а СЫРЫЕ числа ("вольты").

      Сие замечание актуально, если местом преобразования между аппаратными и операторскими каналами будет избран сервер, а не cda -- тогда в протоколе (и в cda) надо будет иметь возможность доступаться как к операторским каналам, так и к аппаратным.

      Если же конверсия будет на совести cda -- то проблемы нет, просто надо уметь игнорировать присылаемые сервером (r,d), уставляя принудительно (1,0); хотя и тут отдельный вопрос -- КАК? В том самом [ПРЕФИКС]?

    • На будущее захочется иметь для-блочные утилиты -- типа candac16, cac208, etc. -- для диагностики, работающие через cda. И для всех козачиных блоков лучше иметь ОДНУ программу, которая бы создавала разные экраны в зависимости от типа блока. А для этого надо вычитывать этот тип at-runtime.
    • Следствие: из-за cangw -- надо ведь мочь получать не только имя драйвера, но и имя блока. А как?

      25.04.2009: а так: указывать первым параметром в конфиге ИМЯ БЛОКА (как было в .uds) -- именно оно важно, а драйвер -- это лишь часть адресной информации. Т.е., строчка в конфиге будет иметь вид

      DEVNAME DRIVER BUSINFO AUXINFO {CONTENT}
    • И еще -- такие программы должны поддерживать ВСЕ возможности блока -- и ЦАПы, и АЦП, и регистры, и осциллограммы, и файлы.

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

      А альтернативы АЦП/осциллограммы и ЦАПы/файлы -- видимо, делать tabber'ами.

  • 23.04.2009: "сопутствующий вопрос" -- об информации по аппаратуре.

    23.04.2009: идея родилась глядючи на курчатниковскую программу, которая по мере старта/опроса аппаратуры выдавала в окошко табличку со списком устройств, где живые были строчками на зеленом фоне, неживые -- на розовом, а с мертвыми контроллерами -- на сером (или наоборот).

    1. Первоначальный вариант: иметь бы возможность добыть карту устройств из layer'а -- чтобы смочь отобразить их в программе диагностики.
    2. Второй вариант: неа, надо мочь добыть просто КАРТУ УСТРОЙСТВ из сервера (scan), а layer/драйверы чтоб могли отдавать и строку описания состояния (как и было задумано в давнем проекте).

    28.04.2009: и еще чуть подумавши -- а ведь варианты 1 и 2 не взаимоисключающи, а ДОПОЛНЯЮЩИ:

    1. Для программ диагностики надо иметь именно карту устройств, чтоб знать, на что натравливать эти программы.
    2. А для более низкоуровневых разборок -- надо б всё-таки уметь вытрясать и карту устройств из layer'а. Ибо могут быть и неописанные устройства, а возможна и ситуация разделения одной CAN-ветки между двумя серверами (и вызываемые этим проблемы с лшибками в описаниях -- кому какое устройство принадлежит).
  • 23.03.2010: еще: у всех устройств, которые хоть в принципе поддерживают серийники, надо обязательно иметь канал ".SERIAL", чтоб диагностическая программа могла отображать это число.

    23.03.2010: к таким устройствам относятся батраковские ADC200ME/ADC812 и DL200, PISO-Encoder600. Ну и пановский HWADDR -- тоже можно рассматривать как этот serial.

    Это позволит ВСЕГДА иметь на экране максимально полную информацию об устройстве, вне зависимости от того, как оно было сконфигурено в БД -- по адресу или по серийнику.

    Хотя и указанный в БД адрес также хорошо бы мочь получать.

  • 25.03.2010: еще: у fastadc должны быть каналы, возвращающие ТЕКУЩИЕ минимально- и максимально-возможные значения для КАЖДОЙ линии, а накже "наличествующесть" данных этой линии (касается ADC333) -- чтоб клиентам не приходилось умничать и вычислять эти вещи на основе прочих атрибутов. В таком случае ВСЁ интимное знание о блоке будет сосредоточено только в драйвере, а "описания морд" таких осциллографов можно будет сделать текстовым.

    26.03.2010: следствие: даже adc200 надо будет перевести с uint8-данных на нормальные int16 или int32 -- чтоб оно отдавало сразу готовое в милли- или микровольтах.

    29.03.2010: в ту же степь -- надо иметь также канал "множитель времени" -- для перевода номера отсчета в что-то-секунды (что сейчас делает n2xs).

  • 28.03.2010: в каждом многоканальном fastadc должны быть дополнительные по-линейные каналы, возвращающие уже выделенные данные для конкретной линии.

    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@Снежинск-утро-полигон-около-раздевалки: с этими индивидуальными каналами вылазит проблемка: как реализовывать АТОМАРНЫЕ получения самого канала и его "настроечных" (тайминги, диапазоны, ...)? Ведь настроечные каналы могут быть настроечными для ОДНОГО базового, не для нескольких.

    • То ли в данном случае забить на отношения базовый<->настроечный, и опираться на timestamp'ы (тогда как-то должно будет указываться, что timestamp'ы всех индивидуальных каналов (да и настроечных тоже) брать от основного (всевключающего).
    • ...то ли вообще надо выкидывать понятия "базовый/настроечный" из конфигурации, оставляя их только в запросах/ответах (т.е., в протоколе)?

    16.08.2014@пляж: насчёт параметризованных запросов вообще:

    • Со стороны API драйверов: ввести ЕЩЕ один метод, "do_par_rdwr()", а если он NULL, то эмулировать его вызовами do_rw() сначала с записью настроечных, а потом записью или чтением базового. Альтернативный вариант - driverrec.flags, где указание на поддерживаемость параметризованных запросов (DRVA_PZREAD, DRVA_PZWRITE).
    • В БД же надо понятия базового и настроечных извести, оставив только условие "все каналы в параметризованном запросе относятся к одному устройству".

      (Теоретически можно оставить в чисто информационных целях.)

  • 25.10.2021: о ключе "keep going" в утилитах командной строки: некоторое время назад стало ясно, что таковой нужен, чтобы утилитки игнорировали ошибки В/В -- это бывает необходимо при отладке/диагностике (например, сканирование шины по разным адресам, когда часть операций (по "пустым" адресам) может давать ошибки).

    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: итак:

    • vme_test_common.c: было сделано 02-10-2021, "-k".

      Но сейчас переделано на заглавное "-K" -- для унификации.

    • cdaclient.c: было сделано 18-03-2021, "-N".

      Но сейчас добавлено понимание и "-K" -- тоже для унификации.

    • canmon_common.c: пришлось изрядно повозиться (подробности в своём разделе за сегодня), но добавлено.
    • uspci_test.c: тоже добавлено, несложно. Но не проверено (просто не на чем).
    • А вот куда НЕ сделано:
      • cm5307_test.c -- там просто нет такой потребности, т.к. в CAMAC нет "фатальных ошибок В/В" ("ошибки" отдаются отсутствием X и Q, что является штатной работой и штатно же отображается).
      • Всякие утилитки вроде dumpserial_common.c setupkshd485_common.c и прочих lab6_vme_firmware_common.c -- ибо нефиг.

    ЗАМЕЧАНИЕ: запрет отваливать касается ТОЛЬКО операций В/В, но НЕ влияет на:

    1. Парсинг синтаксиса -- уж если в командной строке ошибка, то это фатально.
    2. Операции открытия шины -- тут уж ничего не попишешь.
    3. "Процедурные" ошибки -- например, возврат <0 из select().

      Вот здесь уже вопрос -- откуда вообще такие ошибки могут возникать и можно ли их как-нибудь игнорировать?

      Но в любом случае это уже за рамками изначальной задачи -- мочь игнорировать ошибки ШИНЫ.

О текстовых файлах и кодировках:
  • 19.03.2010: очевидно, что почти ВО ВСЕХ текстовых файлах (кроме гарантированно-чисто-числовых/конфигурационных) должна указываться кодировка. Иначе гарантированно начнется бардак.

    19.03.2010: в частности, в первую очередь речь про .subsys-файлы. Там можно иметь директиву encoding.

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

    Также это актуально и для "файлов", получаемых из СУБД -- т.к. они будут ровно так же просасываться через smp4td, а для собственно программ выглядеть ровно как текстовые файлы.

О механизме поиска ручек:
  • 10.01.2011: при реализации разнообразного функционала для liuclient'а в Снежинске всплыла тема поиска ручек в группировке/поддереве по именам.

    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 за сегодня.

  • 20.03.2018@лыжи-конец-2-й-2-ки: энное время назад возникали мысли на тему функционала, требующего обхода деревьев:
    1. (когда-то-в-Снежинск-каземат-11) Поиск узла по имени в графическом клиенте -- чтоб этот узел подсвечивался на экране.
    2. Возможность применить некую операцию ко всем узлам, чьи имена соответствуют некоему шаблону (кончаются на указанную цепочку ".x.y.z", либо шаблон с "*") -- это описано в первом пункте раздела за 10-01-2011.

    Так вот: способом добиться результата выглядит создание функции "datatree_foreach_node()", которой бы передавались: 1) узел-контейнер -- в котором искать; 2) функция-компаратор и privptr для неё.

    20.03.2018@лыжи-конец-2-й-2-ки: некоторые подробности и дополнительные соображения:

    • Функция может возвращать количество найденных (а "найденный узел" возвращать в параметре-по-указателю): 0 -- нет, 1 -- единственный, >1 -- сколько. Это полезно для случаев, когда реально нужно найти единственный конкретный узел.
    • Можно иметь "стандартную реализацию" функции-компаратора, рассматривающую privptr как строку-шаблон; хоть в варианте ".x.y.z" (т.е., надо убедиться, что "текущий" узел имеет имя "z", его предок "y", пред-предок "x"), хоть с '*' и '?' ("любое количество узлов" и "узел с любым именем").

      Такую "стандартную реализацию" можно запрашивать, указывая компаратор=NULL (как в findfilein() указание checker=NULL означает "используй fopen(), в privptr имя файла").

      ...при надобности можно таких "стандартных" сделать несколько, используя константы ((void*)1), ((void*)2), ...

      22.03.2018: вариант с "любое количество узлов" через '*' выглядит пугающе -- хбз, как такое делать (и точно рекуррентно). Так что с этим стоит повременить, по крайней мере, пока не припрёт.

    • Конкретно для поиска на экране желательно бы иметь возможность "продолжить поиск с текущей точки". Для этого надо иметь какой-то "контекст", чтоб в нём записывалась текущая найденная.

      Проблема: а как записывать? Ведь уровней-то туча, поиск может быть рекуррентным, и надо б хранить список всех "текущих проверяемых точек на каждом уровне иерархии"?

      22.03.2018: а вот и нет! Ведь каждую эту точку можно "вычислить", идя вверх по дереву. Так что вопрос будет не идеологическим ("ой, рекурсия ж..."), исключительно техническо-изобретательским: как организовать реализацию поиска, чтоб она дозволяла такое продолжение.

О программной управляемости cont-плагинов:
  • 30.03.2011@Снежинск-каземат-11: а если б можно было как-то ПРОГРАММНО (т.е. -- из client-программы/формул/скриптов) рулить работой контейнеров?

    30.03.2011@Снежинск-каземат-11: идея появилась глядя на поведение саровской SCADA: а можно ли как-то сделать возможность ПРОГРАММНОГО указания ELEM_TABBER'у, что ему надо выбрать такую-то закладку? Чтобы, например, если есть несколько экранов-табов, то чтоб блок "сценариев/техпроцесса" конкретной программы мог бы активировать в некий момент нужный из этих экранов.

    Хотя, конечно -- вопрос, а как к ним адресоваться? Логично -- по номерам, но это неудобно. А если б по ident'ам?

    И -- tabber не единственный такой элемент. Вырисовывается такая картинка:

    • tabber: указывается номер таба, требующий активации;
    • subwin: 0 -- убрать, 1 -- появить, -1 -- сменить состояние;
    • прочие контейнеры: 0 -- свернуть, 1 -- развернуть.

    17.11.2013: see also KnobsCore/11-08-2012 (SetVis).

    24.09.2011@Снежинск-каземат-11: последние несколько дней опять задумывался об этом вопросе, и вот некоторые соображения:

    • Во-первых, дабы это всё было возможно, требуется всё-таки чтоб "элемент заодно должен также быть и каналом" (bigfile-0001.html, 20-04-2006). Просто потому, что необходима сущность, к которой формулы/скрипты могли бы адресоваться.
    • Соответственно:
      1. cont-ручки должны иметь как минимум метод SetValue -- которому и будет передаваться желаемое значение (типа номера закладки), в конечном итоге от set_knob_controlvalue().
      2. Следствие -- у cont-ручек должны быть все свойства knob-ручек.
      3. ...включая data-binding.
      4. Просто по умолчанию он отсутствует, и возможность программной управляемости тогда тоже отсутствует.
    • Следствие -- управлять можно будет не только локально, но и "глобально": привязывая такой cont к физическому каналу.
    • Замечание: а что до "как адресоваться? ... по номерам ... неудобно" -- то в v4 предусмотрены и другие типы данных, кроме int32/double, в частности строки. Просто в datatree & Co. это пока никак не присутствует.

    25.09.2011@Снежинск-каземат-11: вообще, конечно, вылазит просто классическая цепочка наследования: unif,knob,cont,grpg. Только делается всё на НЕобъектном языке.

  • 08.09.2013: связанная тема -- как бы дать возможность "user"-knobplugin'ам добавлять к себе поддеревья ручек, которые б обрабатывались как просто ручки (дерева, взятые прямо из обычной группировки).

    08.09.2013: мысли такие витали давно (найти б еще, где (если?) записывались). По результатам реализации плагинчика управления сваркой мысли выкристаллизовались поконкретнее.

    1. Это РЕАЛЬНО потребно -- как для weldclient_process_knobplugin'а было б удобней!
    2. Просто поставить такую задачу и думать, как её решать.
    3. Идея: оставить у userknob'а поле "subtree", которое он бы мог заполнять из встроенного text-описания вызовом обычной Cdr'ной функции типа нынешней "CdrCvtElemnet2Eleminfo()". И чтоб такие "искусственные" подветки обрабатывались наравне с обычными подветками, и destroy'ились бы так же, и чтоб теми же методами пользовались (чтоб можно было правую кнопку мыши жать).

    12.02.2015@вечер-беговая-дорожка: в очередной раз обдумывал этот вопрос; очередная пачка мыслей (в значительной степени пересекающаяся с написанным выше и в "общих вопросах" по Cdr за 24-10-2012, 25-10-2012, 26-02-2013):

    • Доп.деревья у плагинов pzframe & Co. - да просто: делать эти плагины контейнерами, и чтоб они при создании создавали б нужные подветки (скорее - набор ручек).
    • Ну или - что-то вроде USER (но с knoblist-содержимым) - чтоб биндинги сделала сама Cdr.
    • ...а еще лучше - именно CONT'ами, но чтоб ручки для экрана генерились бы условно: если в подчинённых найдена ручка с таким-то ident.
    • Хотя последние 2 варианта плохи тем, что придётся иметь определения прямо в subsys'е (в КАЖДОЙ точке использования), вместо создания из самого плагина.

    Короче -- пока наиболее осмысленным выглядит "гибридный вариант" описанный 26-02-2013.

О мегаклиентах:
  • 31.03.2011@Снежинск-Снежинка-303-душ-утро: мега-клиент kozclient -- интегрирующий:
    • libcxsd и cda_d_direct;
    • се драйверы для козачиных блоков, sktcankoz_lyr, remdrv, и умеющий сам генерить конфиг по указанию в командной строке;
    • "экраны" для всех блоков, и "морда" берется либо по argv[0], либо указывается явно ключиком.

    31.03.2011@Снежинск-каземат-11: Поведение:

    • На какой тип блока он работает -- берётся либо из argv[0], либо указывается явно ключиком.
    • Смотрит по указываемой в командной строке ссылке на "источник данных", и:
      • либо коннектится к указанному серверу.блоку;
      • либо, при указании протокола "direct", сам генерит конфиг для sktcankoz и запускает внутренний сервер;
      • либо, при указании протокола "rem", сам генерит конфиг для /rem и запускает внутренний сервер.
    • Выбирает по типу блока нужный экран -- который потом берётся из внутренней таблицы по URI-типу "string".

    Такой мега-клиент во-первых будет очень сильно полезен (для всякой отладки, разборок и т.п.), а во-вторых -- будет отличным ПРОСТЫМ И УДОБНЫМ полигоном для отработки всех технологий и компонентов.

    Понадобится собственно сам 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?

О междрайверных связях:
  • 06.05.2011: с наличием протокола/API "inserver" потенциально будут те же возможности, что даёт EPICS с меж-record'ными связями. Т.е. -- построение довольно больших "сетей", включая всякие хитрые булевские/арифметические/условностные/etc.
    1. Но с одним крупным отличием:
      • ТАМ это всё описывается прямо на уровне конфигурации, и связывается конфигуратором/ядром системы.
      • ТУТ же -- оно на совести самого драйвера, который должен зарегистрировать свои "источники" данных.
    2. Схемы неравнозначны:
      • EPICS'ная даёт более удобное/стандартизованное конфигурирование (в т.ч. с возможностью стандартизованной проверки корректности).
      • Наша же даёт бОльшую гибкость.
    3. А можно ли иметь "best of all worlds" --
      • Т.е., чтобы в дополнение к "ручному" доступу к inserver (и прочим cda-протоколам вообще) был бы некий "механизм" общего указания этих самых "линков" прямо из конфига?
      • Тогда автоматом получился бы супер-мощный механизм -- просто за счёт возможности указывать кроме локальных также и "внешние" ссылки.
      • И как, спрашивается? Внедрять это прямо в ядро/конфиг -- мутноватенько. Вариантов видится 2:
        1. Делать какую-то пародию на EPICS'ный "хбз-что support" -- специальный тип драйвера, которому отдельно указывается название "драйвера 2-го уровня" (или вместо типа драйвера такой layer сделать?), а уж он окудахчивает для того всё требуемое.
        2. Ввести в driverrec какое-то описание "требуемых входных каналов" -- каковые, при указанности в driverrec'е, должны будут присутствовать в конфиге, и тогда уж ядро озаботится их привязыванием.

        Более симпатичен 2-й вариант -- он хоть и менее гибок, но более удобен и практичен.

    06.05.2011: а ведь вырисовывается картина, что у нас есть энное количество "сущностей" -- устройств, и у них имеется некоторое количество возможных "properties" (тип, опции, списки каналов, линки, auxinfo, ...).

    Вопрос: не генерализовать ли вообще эту идею -- чтобы вместо нынешней позиционной нотации был psp-style набор пар "СВОЙСТВО=ЗНАЧЕНИЕ"?

    Тогда всё стало бы очень легко расширяемо (кстати, в EPICS'е кабы не так всё и устроено...).

    Неа, не хотелось бы. Читабельность и элегантность упали бы ниже плинтуса, плюс, возможно, auxinfo тогда б пришлось указывать в кавычках. И вообще -- не нравится.

    06.05.2011: а собственно реализовать заготовку "ядром" "линков" для драйвера/девайса -- совсем несложно:

    • В метрике иметь таблицу с полями {callback, cb_privptr}.
    • Заполучив список ссылок на каналы, "ядро" регистрирует их в cda и САМО регистрирует для них dev-callback'и на указанные в таблице обработчики.

    Проблема в другом -- всё-таки в собственно ПОЛУЧЕНИИ списка каналов:

    1. Как указывать каналы в devlist'ах? В опциях?
    2. Как сделать, чтоб можно было указывать не ВСЕ каналы, а только часть? Видимо, как с businfo -- в метрике определять границы [min,max].
    3. А если всё-таки постараться сделать элегантнее, чтобы для драйверов типа ist_cdac20 вместо тупого длинного списка каналов можно было бы указать только "базу", относительно которой всё бы резолвилось? Как?

      Видимо, добавить в таблицу еще одно поле -- "смещение от базы".

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

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

    • Отдельный вопрос -- что надо бы как-то указывать и СПОСОБ опроса: по изменению, с какой-то частотой, ... (всё, что позволяет и обычная cda/CX).

      Еще одним полем в той "таблице"?

    23.01.2014: в v2 изрядная часть "ядра линков" реализована в vdev.

    06.05.2011: и есть еще одна проблема, более высокого и общего порядка: а что делать, если разные компоненты требуют один и тот же канал (т.е., с совпадающей полной ссылкой "СЕРВЕР.КАНАЛ"), но с РАЗНЫМИ способами опроса?

    По умолчанию получится, что сделается так, как попросил ПЕРВЫЙ, а остальным вернут тот же handle, и с теми же "правилами" обновления.

    06.03.2014@лыжи: по впечатлениям от общения с Пашей Чеблаковым получается, что в EPICS поперемешаны (разделение есть, но плохое) две разных сущности/концепции:

    1. работа с железом (драйверы, в том странном наборе уровней *Support и типах рЕкордов) и
    2. "поддержка “processing nodes”" (через линки)".

    Собственно, можно и в v4'шном CX-сервере тоже иметь в дополнение к обычной работе с железом -- "EPICS-style links layer", обеспечивающий похожий функционал для “processing nodes”; работать ему лучше через cda, а грузиться как "библиотека" (директивой load-lib).

  • 09.05.2011: более глобально -- а в конечном счёте не идёт ли всё к тому, что имеется просто некий набор "кубиков", из которых можно собирать разные конструкции?

    И в этом смысле -- и драйверы, и всё прочее становятся примерно одного порядка, и имеющиеся сейчас различия (типа всяких хитростей в протоколе) будут потихоньку нивелироваться, в пользу неких более общих "связей".

  • 23.01.2014: по опыту запинывания драйверов пикапов накопителя на ВЭПП-5 -- надо, надо стремиться делать возможность одним драйверам управлять другими при помощи "линков", чтоб можно было всё "из кубиков" строить. В т.ч. и в контроллерах.

    Из-за необходимости реализовать функционал "макроустройства" ADC4M, состоящего из связки {ADC4, линия задержки, усилитель} -- и управляемого атомарно, в рамках одного большого канала -- пришлось городить завёрнутую структуру, с:

    • драйвером-надстройкой над nadc4,
    • #include'ащим исходник nadc4_drv.c и
    • вынесенные в отдельные .h-файлы "мозги" драйверов для линии задержки и усилителя,
    • махинирующий этой троицей.

    И по ходу реализации в голову лезли всякие дикие мысли -- типа не использовать ли "объектность наследование"; для чего пришлось бы полностью публиковать все потроха наследуемых драйверов, включая privrec и методы, да еще и какие-то особые правила/инфраструктуру для этого вводить.

    А при наличии возможности одному драйверу дирижировать другими (возможно, эксклюзивно, через наложение блокировок) -- всё бы делалось легко и стандартно, без извратов.

Глобально -- об удобствах и сценариях:
  • 25.09.2011@Снежинск-каземат-11: (часов 11 воскресенья, глядя, как Логачёв шажками, меняя количество шагов КШД485 и потыкивая в [GO] пытается подвести нужное место колеса мишени к камере): при попытке прикинуть, а как бы это можно было сделать по-человечески, автоматизировать, пришли следующие мысли.

    25.09.2011@Снежинск-каземат-11: крупной проблемой в v2 является то, что возможности программы -- однонаправленные: она может дать некую команду (типа смены числа шагов d КШД), но просто "спокойно" исполнить некоторую последовательность действий с обратной связью -- оч-чень нетривиально. Причин 2:

    1. ФИЗИЧЕСКИ команды на запись передаются драйверам не сразу, а с гранулярностью цикла сервера.

      Результат -- нельзя сразу выполнить несколько запросов на запись, если они должны произойти в определённом порядке, приходится их секвенсировать, по 1 за цикл.

    2. Программа может только отправить запрос, но узнать, что он РЕАЛЬНО был исполнен -- не может.

    Поэтому (в основном) и был придуман seqexecauto, чтоб позволить следить за "результатом" как бы "исполнения" команды, и уж потом передавать упарвление следующему шагу.

    • В v4 вроде бы как предполагается иметь решение 1-й проблемы.
    • А вот со 2-й -- чуть сложнее. Вроде бы как и были мысли сделать какой-то callback-API, но...

    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: некоторые вещи общего плана, которые где-то следует отразить, поскольку они влияют на всю идеологию/архитектуру, и потому заслуживают глубоких размышлений.

    20.02.2013: по порядку:

    1. О точках контроля и коэффициентах:
      1. Очень желательно бы иметь возможность для точек контроля (которые есть alias'ы на обычные каналы) мочь указывать СВОИ, отдельные {r,d} в дополнение к оным аппаратного канала. Например, некий канал АЦП даёт {r=1000000,d=0}, а повешенная на него точка контроля "ток магнита" -- еще {1/250,0}.
      2. Однако по изначальной задумке (нигде не записанной, но держимой в уме) alias'ы при резолвинге должны отдавать просто номер target-канала (как бы inode), а уж по этому номеру и выспрашивается набор {r,d,q,fresh-age}. И в случае смены сих параметров at-runtime программе присылается уведомление, и она просто повторно спрашивает "свойства".

        И все обращения на чтение/запись/подписку реально должны идти к "inode".

      Но иметь раздельные коэффициенты было б ЧРЕЗВЫЧАЙНО удобно: тогда можно будет в одной и той же софтине обращаться и к аппаратному каналу (с вольтами), и к точке контроля (с амперами). 03.04.2013: да, ОЧЕНЬ нужно. Сегодняшний пример -- GID25x4: там надо видеть и пересчитанные значения от CEAC124, и (для понимания ситуации) исходные, для чего просто иметь subwin-панель "CEAC124". Но в v2 панель вынужденно пользуется коэффициентами для амперов...

      Что ж делать, а?

      • Отдавать "цепочку пар {r,d}" -- в принципе, можно (именно цепочку, а не пару, поскольку alias'ы могут сами ссылаться на alias'ы);

        И отдавать "inode" самого alias'а тоже можно (для чего придётся заводить их отдельным массивом каналов) -- чтоб в клиенте каналы считались разными, и cda не пыталась бы возвращать уже имеющийся handle.

      • Но все обращения read/write-то должны идти к реальному, АППАРАТНОМУ каналу.

      Пока вменяемого решения не видно (кроме делания на каждый alias inserver-callback'ов, что совершенно неоптимально и затратно).

    2. Для возможности корректной "удалённой" реализации драйверов типа ist_xcdac20 им надо иметь доступ не только к каналам, но и к статусу устройства.
      • Такому каналу можно давать специальное имя "devname._state" (генерить, естественно, самим cxsd_db/cxsd_hw, автоматом).
      • А что до номера -- то можно его ставить ПЕРЕД реальными каналами устройства. И можно даже целую сотенку каналов резервировать (хотя зачем? только что для ровности счёта...).
      • Но вопрос -- а для точек контроля такое имеет ли смысл, и если "да", то как оно должно формироваться? У реальных каналов -- собственно имя канала заменяется на _state (т.е., dirname(chan)+"._state"); а тут могут быть многоуровневые иерархии, так что операция непонятно как должна производиться.
    3. Ограничение диапазонов значений. Сейчас оно производится на двух уровнях:
      1. На самом верхнем, у клиента -- логические.
      2. На нижнем, в драйвере -- тут накладываются уже ограничения исполнительной электроники, которая значения вне этих диапазонов всё равно не поймёт.

      Напрашивается ограничение еще на одном уровне -- на среднем, в сервере. Это как бы расширение первого пункта -- ограничение по точкам контроля, т.е., как бы по логическим значениям, но централизованное, не зависящее от клиентов.

      Главный вопрос -- как должны указываться эти диапазоны: в конфиге, или же как-то на живой системе, с возможностью изменения на лету, или как? И, с учётом пункта (I) -- не делать ли привязку ограничений не к аппаратным каналам, а к точкам контроля?

    03.03.2013@вечер-21:00, пешком по Лаврентьева с парковки: идея -- сделать "идентификатор аппаратного канала" таким же свойством именованного канала, как и коэффициенты. Тогда:

    • При резолвинге возвращаются коэффт-ы и идент "точки контроля",
    • а по получению свойств (адресуемых по иденту "точки") -- еще и идент аппаратного канала.
    • Уникальность ссылок cda пусть проверяет по идентификаторам точек (или вообще по именам),
    • а в запросах (getvset или что там) указывает идентификаторы аппаратных каналов.

    Возможные детали реализации:

    • Точкам контроля получают свои идентификаторы, сквозным образом с аппаратными каналами. Просто при получении "свойств" для точек контроля указываются идентификаторы их базовых аппаратных каналов (для ссылок на ссылки -- уже разрезолвленные до упора), а для аппаратных -- их же собственные.

      Формально по протоколу будут возможны запросы на чтение/запись/... с точками контроля, но сервер может на них отвечать "ошибка" (а правильный клиент такого запроса и не пришлёт).

    • Если захочется оптимизации -- то можно в принципе для "простых" точек контроля, т.е., которые являются просто именованными ссылками, но не добавляют никаких свойств ({r,d}) сразу при резолвинге отдавать идентификатор подстилающего аппаратного канала.

      В случае переконфигурации клиенты всё равно повторят резолвинг с нуля.

    • Для ускорения/упрощения процесса сервер может в ответ на запросы резолвинга прямо в этом же пакете по собственной инициативе присылать отдельными fork'ами и свойства запрошенных к резолвингу каналов.

      Тогда клиент их тоже примет к сведению (если надо), а дополнительный запрос пришлёт на отсутствующую информацию (если таковая останется).

    04.03.2013: Замечание: вообще-то, если cda будет проверять "уникальность" по именам, то вся проблема почти исчезает: у аппаратных каналов и alias'ов имена будут в любом случае разными.

    Хотя, нет: хоть cda такие каналы и не смешает, но не будет возможности различать их на уровне сервера в целях получения разных {r,d}. Посему -- withdrawn.

    20.03.2013: еще несколько соображений в ту же степь:

    • @утро-дом, после семёновского заскока с реально отрубившимся weld02: надо б иметь возможность на экране смотреть живость не только каналов, а и драйверов (статусы).

      А если б еще просто CAN-блоков (откликаются ль на FF/еще-что-то?).

      Это сильно похоже на концепцию "IOC health monitor".

    • @по пути на работу, около ИХБФМ: "как узнавать статусный канал устройства для alias'ных": да просто -- пусть это делается не по имени, а по ID канала, спец.вызовом (или вообще отдаётся свойством, как и "реальный ID канала для заказа".

      Как вариант: пусть имена вида "что.то.там._devstate" резолвит так -- т.е., чтоб "_devstate" можно ббыло приписать к любому имени канала.

    • @вечер-по-пути-домой: про "сотенку каналов резерва на статусы": с учётом наличия triggered/multwrite/... -- многовато перерасхода будет.

      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 явно не соответствует задаче.

    • Видимо, надо переходить на "стиль .subsys" -- последовательное указание через пробел, с возможностью в любой момент переключиться на формат КЛЮЧ:ЗНАЧЕНИЕ (например, units:"kV").
    • ...а еще желательна бы возможность ГРУППОВОГО указания: чтоб одна строка конфигурила сразу группу однотипных каналов, например, ЦАПа.

      Тут можно принять какое-нибудь упрощение: например, что указание {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".

  • 18.11.2013: близкий аспект -- надо вместе с прочими атрибутами канала ({r,d}, hw_id) присылать и ТИП. Как минимум -- r/w, а можно также и полный тип (cxdtype плюс количество аллокированных ячеек).

    Это может быть полезно для "изготовления скринов на лету", по информации прям от сервера. Да и вообще добавляет системе полноты.

  • 17.03.2014: еще близкий аспект: есть необходимость уметь делать "zeroconf-резолвинг" -- т.е., без предварительного знания имени сервера.

    17.03.2014: мысли такие грызли давно, просто сейчас наконец сподобился записать.

    1. Предпосылки: если для небольшой системы достаточно иметь всё в одном сервере (а для программы всё-в-одном -- просто необходимо!), то в крупных системах необходимость как-то зашивать ссылки на конкретные сервера может быть крайне неудобной и болезненной.

      Сама задача разбивается на две крупных части: ПОТРЕБНОСТИ и РЕАЛИЗАЦИЯ.

    2. Потребности: в EPICS любое имя может быть в любом IOC'е. Например, sys.subsys.a в одном, а sys.subsys.b в другом.

      У нас же подразумевается, что если "корень" (sys) в некоем сервере, то и всё его содержимое тоже в том же сервере. Точнее, даже не "sys", а server:N.sys.

      Обсуждение:

      • Наш подход, с одной стороны, структурнее и проще, а с другой -- менее гибок. Даже если забыть бурчание "вечно эти физики/инженеры наворотят кривую структуру...", весьма желательной выглядит возможность делать "cross-site links".
      • Чтоб сервер умел давать клиенту redirect "ветка с этой точки живёт в том-то сервере"?

        Кривовато. Кроме того, это оставит "дырки" в массиве каналов у sid'а, "смотрящего" на этот сервер.

      • И вообще, тут главный вопрос -- в ИДЕОЛОГИИ: у нас предполагается (для конкретно "cx::", что каналы, запрашиваемые у некоего sid'а, физически будут в этом же сервере. И, следствие: каналы (CxDataRef_t), попавшие в некий sid, навсегда останутся в нём же; максимум -- при переконфигурации самого сервера сменится их маппирование на конкретные аппаратные каналы.
      • Эта идеология проста и элегантна, но негибка.

        Видимо, её надо менять.

    3. Реализация: то, как реализовать собственно процесс zeroconf'а с минимальными напрягами и минимальной кривизной -- вопрос технический. Содержит 2 аспекта:
      1. Реализация в рамках сети: тут вариантов ровно 1 -- broadcast'ы. Выбрать какой-нибудь порт, "оракул" вешается на него и отвечает на вопросы -- типа, что "да, на этом хосте есть такой канал, обслуживается сервером N".
      2. Реализация в рамках одного хоста: состоит из 2 под-аспектов:
        1. Занятие порта: "кто первый встал, того и тапки". Смог занять порт -- значит, становится оракулом.
        2. Передача ответственности: чтоб не заниматься пингованием, остальные сервера на том же хосте -- НЕ смогшие занять порт -- устанавливают stream-соединение с занявшим. Соединение неактивное, по нему ничего не гоняется, но если оракул сдохнет, то сокет станет готов на чтение и EOF -- в этот момент можно пытаться заново занять порт.

          06.10.2014@Снежинск-автобус-из-каземата-на-обед: вопрос только -- а КАКОЕ stream-соединение? КУДА? /tmp/cxv4-resolver-socket?

        Сами обязанности "оракула" не должны никак интерферировать с обычной работой сервера. Это как бы "задача внутри процесса": смог занять порт -- становишься оракулом; не смог -- коннектишься к оракулу и следишь за его сдыханием; не смог занять и не смог приконнектиться -- нештатная ситуация, глюк какой-то.

        И -- получивший лычки оракула остаётся таковым пожизненно. Хотя, в принципе, сложить обязанности несложно -- сделать close() udp-сокета и всех приконнекченных собратьев; вопрос лишь "зачем?".

      16.10.2014: о терминологии: в топку термин "оракул" (Oracle? Ню-ню!), пусть будет "гуру"!.

    4. Актуальность -- или такова ли реально проблема: а возможно, это всё как-то обходибельно на более высоком уровне -- клиентских скринов.

      Например, каналы "с неизвестным корнем" могут иметь названия с каким-нибудь хитрым префиксом, вроде "???" или "UNKNOWN" на месте сервера. А подставляться такой префикс может как-нибудь там при генерации скрина.

      Или еще вариант: в таких не-тривиальных случаях вначале добавляется дополнительный шаг -- спрашивание у БД им.Макеева. Правда, это не решает проблемы"навсегдашности" -- что каналы никак не смогут переехать в другой sid.

    5. Злой проект решения: что, если отвязать dataref'ы от srvconn'ов? Т.е.,
      • Значение dataref'ов -- НЕ "сколько-то старших битов на sid, а младшие на ячейку внутри sid'а".
      • А общий, сквозной SLOTARRAY на ВСЕ dataref'ы. И уж в каждом слоте указывается ссылка на конкретный физический sid и номер физической ячейки в нём (т.е., ТРОЙНАЯ косвенность: dataref, ячейка, номер физического канала).
      • Изрядное неудобство -- что делать при гроханьи sid'а? Надо ведь инвалидировать все глядящие на него dataref'ы.

        А КАК? Тут ведь есть варианты:

        1. Считать, что они "принадлежат" ему, и полностью их грохать -- "cda_del_chan()".
        2. Считать, что ссылка dataref'ом на sid -- штука временная, и просто "отвязывать" dataref'ы, переводя их в "режим поиска цели".

        Возможно, правильным будет средний вариант: у каждого 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-резолвинга":

    • Приятное следствие схемы:
      1. Нужна возможность явно указать "найди сервер" (т.е. "выполни резолвинг"). Например, имя сервера "_UNKNOWN_" -- оно содержит подчерк, так что не может быть реальным именем хоста (да и вряд ли вообще такое имя кто заюзает).
      2. И такое имя сервера должно считаться по умолчанию.

        А протокол по умолчанию будет "CX::".

        Таким образом, просто имена каналов (без протокола/сервера) будут автоматом резолвиться поиском.

    • Единственная шероховатость (весьма общего характера): как различать сервер-без-номера -- "HOSTNAME.CHANNEL..." и просто FQCN (см. за 18-09-2007).

      И ведь даже факт указания ЭТОГО именно в префиксе (defserver) использовать никак нельзя -- имена должны быть полностью определимы/расшифровываемы сами по себе. Поскольку многие каналы могут указываться FQCN'ами прямо в середине группировки; собственно, вопрос -- "а как теперь определять FQCN?".

    • И еще: надо, чтоб такие каналы работали в режиме "plug and play" -- при исчезновении сервера (или когда он после перепуска более не владеет каналом с таким именем) должен производиться повторный резолвинг.

    07.06.2014@8-ка-в-город ~11:00, поворот со Строителей на Бердское: нюанс -- а КОГДА и КАК слать UDP-бродкаст резолвинга?

    • Ясно, что по пакету для каждого канала -- сильно жирно, оно создаст шторм в сети.
    • Значит, надо группировать -- слать, например, пачками, чтоб не более ~10kb на UDP-пакет.
    • Но возникает вопрос -- а КОГДА считать пакет "достаточным" и готовым к отправке? Просматриваются следующие варианты:
      1. По мере заполнения. Т.е., когда наберётся 10kb. Но это маловероятный сценарий.
      2. Отправлять первый сразу же, а остальные копить и отправлять либо
        1. по получению ответа на первый, либо
        2. по таймауту в 100мс.

        В любом из подвидов к моменту наступления "события" как раз все запросы и накопятся -- потому, что событие придёт от cxscheduler'а, который получит управление ПОСЛЕ всей инициализации.

      3. По явному "пинку" от более высокого уровня (в лице Cdr), что "всё подготовлено, фас!".

      Разумно выглядят варианты (2) и (3) вместе: (2) для "глупых" консольных утилит, а (3) для Cdr-based клиентов.

    16.10.2014: идеологический вопрос -- ЧТО должен отвечать резолвер, какой АДРЕС? Номер сервера (он же порт) -- понятно, а адрес-то как указывать?

    Конкретно на сейчас покатит очень простой ответ:

    1. Отвечается именно просто номер/порт.
    2. (Причём даже без ссылки на номер канала -- это уж пусть клиент спрашивает у конкретного сервера после коннекта, воизбежание race conditions.)
    3. А адрес клиент берёт из ответного пакета -- там, в случае multihome-хостов, уж система позаботится об ответе с правильного IP.
    4. И в вызове cda_dat_p_get_server() в качестве srvrspec указывает просто AAA.BBB.CCC.DDD:N.

    Да, некрасиво. Но для начала пойдёт, поскольку работать будет.

    17.10.2014: еще идеологический вопрос: КАК должен быть сдизайнен резолвер со стороны клиента?

    • Явно сетевая часть жить должна в cxlib'е, поскольку именно его прерогатива "знать" протокол.
    • А cda пусть лазит туда по некоему API.

      ...по асинхронному устройству должному быть похожим на обычный API работы с данными.

    • Но этот резолвер должен быть один (множественные сокеты вряд ли имеют смысл), и как-то диспетчировать ответы разным "запрашивающим".

      И, видимо, держать в себе некий готовый "пул" ответов.

    • А тут и вопрос -- КАК обрабатывать ситуацию, когда cda-клиент что-то спросил, и потом ДО появления ответа отвалился? Надо ведь записать, что ответ уже никому не нужен -- нельзя просто запоминать cd клиента, поскольку оно будет reuse'нуто.
    • И еще важный аспект: что при обрыве соединения с сервером (ЛЮБОГО из многочисленных с этим сервером!) надо вычёркивать из пула готовых ответов все, касающиеся этого сервера.

    26.05.2015: давно терзали мысли -- как может функционировать резолвинг? Проблема в том, что предполагалось

    1. Использовать резолвинг "глобально", т.е. непривязанно к соединению, а просто указывать, что вызвать.

      И при этом проблема, что если 1. заказ; 2. заказавший (напр. драйвер) отваливается; 3 приходит ответ -- то что делать? Вызывать то, чего уже нет?

    2. Как распознавать ответ -- в смысле, понять, что вот этот ответ на вот тот-то висящий неисполненным запрос, и не перепутать (особенно по прошествии некоторого времени)?

    Сегодня пришло некое понимание/осознание, КАК правильно поступить.

    1. Для резолвинга всё-таки НАДО делать "соединения", но только не те, а специальные "объекты", ни к каким серверам не приконнекченные. Чтобы запрос шел через них, регистрировался (в общем буфере?) на них, и при сдыхании запросившего этот "объект" удалялся бы, с инвалидацией всех привязанных к нему запросов.

      UDP-сокет можно иметь как один общий на всё, так и по-"объект"но. Это технологически маловажно, поскольку при удалении одного клиента и создании другого он может получить сокет с тем же source-port, и на него прекрасно придут "чужие" ответы (на отправленное раньше), так что распознавание всё равно надо делать вручную, не полагаясь на механизмы ОС (ибо их подходящих НЕТ).

    2. Распознавание: каждое соединение помечать идентификатором, постоянно растущим, и чтоб с ответом тоже присылался этот идентификатор; не совпадает -- несчитово.

      Идентификатор -- 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. Какая-то "магия" напрашивается.

    Мысль: можно из помещать в специальный "сервер", который не нормальное соединение, а именно для резолвинга. Тогда всё симметризуется: и каналы всегда при каком-то сервере (а не в "нигде"), и подчистка объекта-резолвера выполнится автоматически при удалении контекста.

    Есть, конечно, и неприятности:

    • Этот резолвер будет торчать в списке sid'ов, хотя клиенту его видеть нафиг не нужно.
    • Он будет получать privrec обычного сервера, хотя его потребности совсем иные.

      ...ну некоторое сходство есть, да -- список каналов (frs_hwr,lst_hwr) и cd.

    Вообще напрашивается мысль: сейчас забить на фичу резолвинга и заняться другим (rem*). А потом, как появится более красивое понимание -- вернуться и добить.

    22.07.2016@пляж-~10:00...11:00: вроде появилось понимание всех идеологических тонкостей функционирования резолвинга на уровне cda, cxlib и даже CX-протокола (все неподписанные "comment2" -- от после-обеда за сегодня же):

    1. В ответных UDP-пакетах должна быть и исходная информация -- имя канала, чтоб клиент мог проверить, что это ответ на его запрос (а то по просто hwid'ам можно и ошибиться).

      Это расходится с идеей от 16-10-2014 об ответе ("просто номер/порт").

    2. По ОТДЕЛЬНОМУ фиктивному серверу (sid'у) на каждый контекст (с неким бредовым именем).

      Проблема (да, замеченная 19-06-2015): будет светиться в LED'ах. Решение: ввести cda_dat_p_get_server()'у флаг "не добавлять в список"?

    3. В cxlib'е -- также явное создание resolve-объекта. Так решаем проблему неявности -- того, кому доставлять ответ и как "забывать" про запросы забытых контекстов.

      Это расходится с идеей от 26-05-2015 о безразличности ("маловажно").

    4. В resolve-sid'е канал может быть в одном из 3 состояний (реально 2, но 0-е -- имеет смысл с точки зрения удобства мышления):
      • 0: отсутствует (отвечено).
      • 1: добавлено, но еще не спрошено.
      • 2: спрошено.

      Правила поведения:

      • При добавлении если суммарный объём каналов типа "1" превышает разрешенный размер пакета, то делается отправка.
      • Также при добавлении включается таймер (0.1s?), по истечении которого выполняется принудительная отправка.

        А лучше -- бесконечно малый, вроде 1ms. Смысл -- как замечено 17-06-2014, событие "таймаут" придёт от cxscheduler'а, который активизируется уже после того, как весь список каналов будет заказан.

      • При отправке каналы переводятся из "1" в "2".
      • Также в конце отправки должен включаться другой таймер (10s?), по истечению которого каналы "2" переводятся в "1" и повторяется отправка всех каналов типа "1".
      • В отличие от того, что было задумано 17-06-2014, все пакеты запросов надо слать сразу, НЕ дожидаясь ответа на предыдущие запросы.

        Смысл -- отсутствие смысла в том правиле.

        А защита от "переполнения буфера" -- сама отправка: когда попытка послать вернёт ошибку, тогда и тормозим отправку.

        ...вообще, отправка сейчас идёт через fdiolib -- это как-то может и проконфликтовать с таймаутами. И чо -- нужно уведомление "была сделана отправка"? Или слать самостоятельно, напрямую sendto(), а возобновлять отправку по таймауту?

    5. Вопрос: надо ли помечать канал как "NOTFOUND" при неответе в течение периода таймаута (как предполагалось 10-04-2015 "при отсутствии ответа")?

      Пожалуй, стоит.

    24.07.2016@утро-душ: пара замечаний насчёт формирования пакетов:

    • Всё-таки НЕкрасиво делать пакеты вида "сначала все chunk'и, потом все строки". Принцип фрагментации нарушается.
    • В любом случае, надо будет в cxlib'е иметь пару функций:
      1. Получить размер, сколько займёт resolve-chunk для канала с указанным именем (да, включая данные, как бы они не упаковывались).
      2. Получить максимально разрешенный размер resolve-пакета -- chunk'и с каким суммарным размером можно отправлять одним пакетом.

    @после-обеда: кар-р-рамба!!! Ведь 12-01-2015 речь шла о ПРОСТО НАБОРЕ СТРОК, совсем БЕЗО ВСЯКИХ chunk'ов!!! Так имеем максимальную экономию объёма в пакете, хотя и за счёт времени на поиск.

    26.07.2016: а вот и нет -- НЕЛЬЗЯ отвечать просто строками! Надо ж еще номер сервера как-то указывать (2 байта).

    Поэтому промежуточное решение:

    • Отвечается (и спрашивается) не просто набором строк, а с 2/4-байтными префиксами, содержащими номер порта (а в запросах -- нули).

      01.12.2017: а вот и нет!!! Вовсе НЕ с "префиксами", а с обычными CxV4Chunk было сделано при реализации 09-06-2015, и это правильно!

    • Хоть "chunk'и" и другие, но их количество также будет указываться в NumChunks.
    • Пакет ОБЯЗАТЕЛЬНО должен иметь код CXT4_RESOLVE.
    • При проходе по "chunk'ам" надо аккуратно искать терминирующие NUL'ы и проверять, что не вышли за границы принятого пакета. На ОБЕИХ сторонах, где ведётся приём.

    26.07.2016: (реально -- вчера) и еще: надо всё-таки иметь в cxlib'е общий пул закэшированных ответов, чтобы не плодить одинаковые запросы пачками из всяких cx-starter'ов.

    1. Пул -- простейший, растущий realloc()'ом/GrowBuf()'ом. Помнятся текущий аллокированный размер и текущий занятый.
    2. Каждый элемент пула -- 16-байтный дескриптор (содержащий результат резолвинга) плюс NUL-terminated строка запроса. Элементы добиваются до кратности 16 байтам.
    3. При АВАРИЙНОМ закрытии соединения из пула должны удаляться все элементы, касающиеся этого сервера (IP).
    4. "Удаление" -- прямо со сдвигом всего последующего блока данных.
    5. Использование:
      • API -- "поискать канал с таким-то именем в пуле"; если нашелся -- то дальнейшие шаги стадии резолвинга пропускаем.
      • Следствие из п.3: в принципе возможна ситуация, когда соединение закрыто, потом -- позже -- будет запрос к каналу этого же сервера, а сервер за это время перепустился, что осталось необнаруженным и

        Посему:

        1. cda_d_cx должен быть готов к тому, что какой-то сервер при прямом запросе RESOLVE канала ответит "NOTFOUND", и тогда для не-w_srv-канала надо отправить его обратно на резолвинг.

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

        2. Может, как-то бы надо по-другому чистить пул -- возможно, при ЛЮБОМ закрытии соединения?

          Или, хотя бы, при закрытии ПОСЛЕДНЕГО соединения с сервером с этим IP:N.

          ...а если ответы были получены, добавлены в пул, но соединения с сервером никогда не произошло? Ситуация бредовая, но пункт (a) должен в случае чего решить проблему.

    30.11.2017: через почти полтора года после написания...

    • Во-первых, есть сомнения насчёт корректности такого изготовления "просто растущего пула", который даже НЕ на SLOTARRAY'е.
      • Ибо сдвиг блока данных -- операция всё ж недешёвая.
      • Плюс, поскольку cda_d_cx должен будет как-то соотносить все ответы в resolve-сокеты с содержимым пула (как минимум для того, чтобы их туда поместить), то он будет подвержен прекраснейшему легко организуемому DDoS'у.

        Хоть оный DDoS вряд ли случится в сети пультовой, но наличие самой возможности, заложенной в идеологии, выглядит крайне неприятно.

      • И неясно, что тогда имелось в виду под "плюс NUL-terminated строка запроса" -- очевидно (судя по "добиваются до кратности 16 байтам"), что эта строка будет идти прямо за 16-байтным дескриптором (а не аллокироваться strdup()'ом)?

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

        Этакий объект для нечастых lookup'ов, желательно с минимумом модификаций.

    • Во-вторых, написанное в п.5 о следствии из п.3 -- вообще нифига не ясно.

    Может -- ну его нафиг, этот пул, и для начала сделаем всё по-простому, с запросами и ответами, без дополнительного кэширования (коее выглядит плохо продуманным и могущим принести больше вреда, чем пользы)?

    17.12.2017: в основном всё сделано.

    22.12.2017: идеологически-методологический вопрос -- а насколько вообще хорошо, что в роли "гуру" работает процесс обычного сервера? Ведь тем самым на него создаётся дополнительная нагрузка.

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

    Но проблема в том, как прибить все остальные ресурсы. Дескрипторы-то файловые еще можно все поприкрывать (циклом), но прочие ресурсы (fdiolib, cxscheduler, stdio, ...) -- фиг. Проще всего, конечно, было бы запускать отдельный процесс -- exec()'ом, при этом всё прикроется автоматом. Но это сведёт на нет достоинство "гуру запускается автоматически".

  • 01.04.2014@Снежинск-Снежинка-504-вечер: а вот ПЕРЕСЕЧЕНИЕ (объединение?) двух предыдущих пунктов: получение типов каналов "на лету" и zeroconf-резолвинг (+plug-and-play):
    1. По-хорошему, прямо cda должна позволять регистрировать канал без знания его типа, чтоб этот тип он обрёл в момент получения информации о канале (т.е., по завершению резолвинга).
    2. Следствие: раз при отпадении/переконфигурировании сервера, содержащего канал, этот канал может вернуться в режим резолвинга (plug-and-play за 24-03-2014), после которого его тип имеет шанс смениться.

    02.04.2014@Снежинск-каземат-11: "жертв" сходу видно двое -- стек Knobs*/Cdr/cda () и cdaclient.

    • Конкретно Knobs*/Cdr/datatree сделать ничего не сможет: если уж там указан тип ручки "скаляр", то вектором она ну никак не станет. Наверное, сможет только деактивироваться либо как-то пользоваться авто-конверсией, если оная появится.
    • А cdaclient -- тот может как-то пытаться дрыгаться при неуказанном типе.
    • Для удобства стоит даже ввести специальный cxdtype -- CXDTYPE_REPR_UNKNOWN, и на его роль отлично ложится значение 0 (оно как раз не-занято).
    • Следствие/технология: надо постараться "помнить поменьше" (в клиентах типа cdaclient'а), и при надобности спрашивать нужное (касательно типов) у cda прямо перед использованием.

    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'е.

  • 19.07.2014@пляж: еще в ту же степь: желательно, чтоб экранные метки брались из описаний каналов сервера. Чтобы редактировать метки в одной "авторитетной" точке, а не в куче описаний экранов.

    Например, в магнитной системе/коррекции когда определят, какой канал ЦАП/АЦП за что отвечает, поименуют -- и сразу в клиентах всё появится.

    20.07.2014: для реализации этой возможности:

    1. Идеологическое -- на уровнях сервера, протокола и cxlib+cda:
      • НАДО рассчитывать на возможность давать каналам метки (а не только идентификаторы). И чтоб эти метки передавались "наверх", и при неуказанности метки в группировке использовались бы там.
      • "Неуказанность" должна считаться по тем же самым правилам, что и у колонок/столбцов -- т.е., пустая строка.
      • И, соответственно, в заголовки колонок/столбцов тоже должно пролазить.
      • И аналогично надо тогда добавить каналам и tip, и comment, и можно даже geoinfo.

        ...так можно и до units с dpyfmt доиграться -- а с ними неприятнее...

        28.07.2014: а до ident? Чтоб и имена тоже обновлялись по мере надобности. (Конечно, чревато -- при этом взаимоссылки нифига работать не будут. Но зато какая гибкость! А цена реализации нулевая -- всё вместе с прочими строками.)

      • Т.е., аппаратные каналы и точки контроля как бы сблизятся с "логическими каналами" -- ручками.

        Заодно станут ненужными извращения вроде usertext.

      • Но, естественно, это всё коснётся только каналов, маппирующихся на ручки 1-к-1 (которых, впрочем, большинство).
      • Передавать наверх -- вместе с прочими свойствами, phys_id, {r,d}, ...

        Разница лишь в том, что если {r,d} могут меняться драйверами на лету, то текстовые строки -- нет, они указываются только в конфигурации.

    2. Конфигурирование сервера: реализация хранения несложна, в основном вопросы касаются синтаксиса конфиг-файлов.
      • В каких местах имеют смысл нововводимые свойства:
        1. label, tip и comment -- на всех 3 уровнях: devtype, channels, cpoint.

          Причём должно производиться должное "наследование с возможностью замены" -- если не указано в cpoint, то брать из channels; если не указано в channels, то брать из devtype.

        2. geoinfo -- имеет смысл только в channels и cpoint; т.е., лишь у ЭКЗЕМПЛЯРА.
      • Главный вопрос -- СИНТАКСИС: как это указывать?

        Сейчас всё просто -- несколько свойств через пробел (а у cpoint еще и опциональные

        [R [D [MIN MAX]]]
        (коие стоило б распространить и на аппаратные каналы, умолчательно считая "1 0 0 0").

        Здесь же добавится 4 СТРОКОВЫХ свойства, могущих требовать закавычивания с несколько забардачивающих вид.

      @засыпая-на-ночь:

      • можно сделать ровно как в парсинге параметров ручек -- ParseKnobDescr(): нынешние свойства идут как есть, а если надо указать дополнительные строковые -- то тэгированно, вида КЛЮЧ:ЗНАЧЕНИЕ.
      • И синтаксис диапазона сменить с [MIN MAX] на [MIN-MAX].
      • Недостаток: порядок указания сильно отличается от ручечного, где вначале строки; тут вначале будет только ID, а остальное сильно в конце.
      • Собственно, идея даёт ясность, как должен быть устроен парсер конкретных строк в devlist-файлах: аналогично Cdr_via_ppf4td'шностям.

      22.07.2014: см. записи от 04-06-2014 и 09-06-2014 выше. Итого -- те же мысли по второму разу :)

    3. Клиентско-интерфейсное -- как это всё может функционировать в *Knobs*.
      • Методу knob.PropsChg() стоит передавать слово флагов, указывающих на изменения -- так проще, чем тупо сравнивать всё между текущим k и old_k.
      • Также нужен элементам метод "ChanPropsChg(chan_n,flags)" -- чтоб они могли менять метки колонок/столбцов.
      • В grid'е возникает вопрос с XmLabel'ами колонок/столбцов -- как менять им XmNlabel, плюс давняя связанная проблема с разворачиванием collabel'а из "!", вызванная "сдвигом" из-за nattl. Ответ прост:

        Вместо тупо номера source-knob'а указывать им в XmNuserData комбинированное значение: ((type<<24)+n), где type={0:chan_n, 1:colnames[n], 2:rownames[n]}.

        Вопрос сдвига на nattl при этом снимается -- виджету УЖЕ передаётся сдвинутый на сколько надо номер model_k.

        (А какую колонку разворачивать -- collabel и так решает по своей grid_x.)

      • (22.07.2014) отдельные вопросы --
        1. как/где ХРАНИТЬ обновляемые строки,
        2. и как РЕШАТЬ, что они нуждаются в обновлении (после первого же обновления они уже НЕ будут ""/NULL).

        Идеи ответов:

        1. Хранить -- да так же, в malloc()'нутом/strdup()'нутом буфере. Т.е., обнаружив необходимость подменить в ручке строку:
          1. Делаем p=strdup() с новым значением. Если облом -- йок, иначе...
          2. safe_free() старого.
          3. k->field=p
        2. Решать --
          1. либо вводить рядом с такими строками поле битовых флагов "поведение строк" (1:"неуказана"),
          2. либо добавлять в начало строки какой-нибудь хитрый префикс, вроде '\x01'.

          ...приятнее кажется первый вариант: и меньше умничать, и на остальное использование этих строк (при собственно сбагривании тулкитам для отображения) не повлияет.

    30.07.2014@утро-перед-уходом-на-работу: тогда был забыт еще один аспект --

    • 2.5. Взаимодействие cda->Cdr: как уведомлять Cdr об изменениях?
      • Видимо, через evproc: там же есть параметр reason -- ну вот ввести при регистрации evproc'а (и канала) еще параметр "reason_mask", где указывать набор интересующих событий.
      • И конкретно Cdr будет указывать не-нулевую маску только для rd_ref и только для ручек, где есть хоть одна "неопределённая" строка (в маске "поведение строк").

    15.08.2014: а чё б вместе со строками не отдавать наверх также и диапазоны -- все 4? Чтоб при их неуказанности в клиенте они бы использовались из конфига.

    Естественно, при этом придётся добавить в CxKnobParam_t поле "specified".

  • 12.08.2014@пляж: так у нас же СЕЙЧАС есть всё для обновления строк! Т.к. cda_d_v2cx владеет нужной информацией. Можно сразу брать и делать.

    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 либо "".
      • Но вообще-то "" -- легитимная указанная строка.
      • Единственная проблема сей момент -- что в label нет возможности указать NULL (остальным полям -- можно, символ минуса/дефиса без кавычек).
    • При не-пустости для rd_ref указывается обработчик на CDA_REF_EVMASK_PROPSCHG, ...
    • RefPropsChgEvproc(),
      1. Добывающий строки от cda.
      2. Обновляющий их в ручке (в т.ч. dpyfmt -- повторным парсеньем, как в DPYFMT_fparser()).
      3. Дергающий метод PropsChg() и...
      4. ...ChildPropsChg() у uplink'а.
    • Собственно ChildPropsChg() был введён. Для удобства ему передаётся не указатель на изменившийся knob, а его номер -- k - k->uplink->u.c.content.
    • Пара тонкостей, из-за чего изначально не работало:
      1. В cda_add_chan() вызов cda_add_dataref_evproc() переставлен в точку ДО вызова new_chan() -- в противном случае в момент "обновления" свойств evproc был незарегистрирован и ничего не ловил.
      2. В RefPropsChgEvproc() строки вычитываются из переданного ему ref, а не из k->u.k.rd_ref -- поскольку он вызывается еще до возврата из cda_add_chan(), когда rd_ref=0.

    Итого:

    1. Сейчас на v2cx:: работает -- чисто потому, что там поля обновляются "из протокола" еще ДО создания ручек.

      24.04.2015: ага, только был косяк: не делалось bzero(&strs), так что для direct-каналов -- SERVER:N.CHAN_NUMBER -- в strs оставался мусор и SIGSEGV'илось. Исправлено.

    2. Собственно отработки *PropsChg() нету
      1. Ни в knob-плагинах -- придётся муторно умничать со всеми свойствами; отдельные вопросы вызывают tip и dpyfmt.
      2. Ни в cont-плагинах -- а там придётся как-то хранить описания массивов заголовков строк,столбцов и закладок (вид -- откуда взята, источник -- model_k), на тему надо ли поменять где-то метку.

    10.01.2015: "PROPSCHG" повсеместно переименовано в "STRSCHG", а RefPropsChgEvproc() в RefStrsChgEvproc().

    04.07.2015: в cda_dat_p_set_strings() был косяк в работе с памятью:

    • Она ВСЕГДА делала safe_realloc(), в т.ч. при size==0, и ВСЕГДА при результате ==NULL делала молчаливый return, НЕ записывая ничего в ri->strings_buf.
    • Но по стандарту realloc(,0) может вернуть и NULL, сделав free()! Вот и получалось, что блок освобождался, а указатель на него продолжал висеть, и через пару ре-стартов сервера (и пару "возвратов" строк) делался double-free.
    • ...единственная странность -- что в самый первый раз realloc(NULL,0) возвращал НЕ NULL. Имеет право, конечно, но смысл неясен.

    Исправлено -- сейчас при size==0 делается free() и new_strings_buf=NULL, а проверка if(safe_realloc()==NULL)return -- только в случае реального реаллокирования.

    ...по-хорошему -- надо б там вообще на GrowBuf() переходить.

  • 01.08.2015@пляж-утро: есть технологически-идеологическая проблема с vdev-драйверами -- ist_cdac20, v1k_cdac20, v3h_a40d16 -- с указанием коэффициентов.
    • С "подстилающих" CAN-драйверов они получают всё в сырых микровольтах, и своим каналам тоже ставят коэффициенты r=1000000.0.
    • НО! Ведь есть второй коэффициент -- для перевода этих вольтов в амперы источников.
    • И что делать? Указывать эти коэффициенты драйверам, чтоб они отдавали 1000000.0, умноженные на доп.коэффициент?

      Но это идеологически криво -- информация-то КОНФИГУРАЦИОННАЯ (у обычных каналов (типа коррекций), работающих без доп.драйверов, указываемая в точках контроля).

    • Корнем проблемы представляется то, что у нас нет никакой возможности указывать в devlist'ах свойства аппаратных каналов.

      Потому, что "создание" индивидуальных аппаратных каналов происходит в cxsd_hw -- CxsdHwSetDb(), а не в cxsd_db.

    • А оные свойства очень желательны -- кроме r,d к ним относятся еще min/max и "цвета".
    • Напрашивающееся решение --
      1. Прямо при актуализации проходить по обоим namespace'ам каждого девайса (сначала общий type_nsp_id, потом персональный chan_nsp_id) и прописывать их props'ы в каналовы.
      2. ...для чего дать возможность эти props'ы в devlist'ах указывать. Для чего завести тип-структуру cxsd_db_ЧЕГО_НИБУДЬ_t и включить её как в cxsd_hw_chan_t (юзуемую), так и в CxsdDbDcLine_t (куда складывать результат парсинга).
      3. А использовать (отдавать клиентам), только если r!=0.0&&d!=0.0 (с константой 0.0 сравнивать на равенство корректно).

        Это, кстати, могло бы касаться и обычных драйверовых phys_r,phys_d, но там уже есть флаг phys_rd_specified.

        ЗЫ: отдавать ПЕРЕД драйверовыми -- как от cpoint'ов (коевыми эти r,d, по сути, и являются (от некоего "фантомного" cpoint'а)).

    02.08.2015: строго говоря, это -- "вторые {r,d}" -- нехорошо.

    1. Для начала, даже способ указания тупо больной: получается, что перемешаны "структура" (ссылки ИМЯ:НОМЕР; т.е., суть namespace'ов) и "свойства".

      Вот если б можно было отдельно делать namespace, и отдельно указывать "КАНАЛ:СВОЙСТВА" (например, "out0 min:-5.0 max:+5.0"; при том, что сам маппинг канала "out0" определён ранее), то было бы приемлемо, а так шибко денормализовано.

    2. Этак получится, что мы убиваем фичу v4 -- возможность "смотреть на устройства как на просто устройства (без обвеса их точками контроля), и видеть значения в голых вольтах, не конвертированными в токи". А делаем прямо какой-то v2, где коэффициенты вольты<->амперы вешались прямо на аппаратные каналы.
    3. А "корень" проблемы в желании сэкономить -- не городить "виртуальные устройства" из наборов точек контроля (которым и давать коэффициенты), а использовать прямо сразу vdev-устройства (ведь они типа и так уже "трансформируют" голые устройства (вроде CDAC20) в виртуальные).

    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'ом), но пока не придумаем что-нибудь получше -- покатит.

  • 02.08.2015@после-обеда, воскресенье, пешком по безлюдному ИЯФу из 13-го мимо 4-го, с 5-литровкой воды в руках: у нас предполагается, что {r,d} при точках контроля (и возможные "вторые {r,d} каналов") будут меняться в конфигах и актуализироваться при рестарте сервера (или пере-чтении БД).

    Но можно ж сделать возможность крутить эти настройки на лету, в живом сервере -- хоть через протокол CX, хоть через консоль. Суть сведётся к модификации чисел (да, в readoonly-БД...:-() и потом "раскрутить точку контроля до target-аппаратного-канала и вызвать ему событие RDSCHG.

    (Побудительный мотив -- когда Беркаевым обнаружилось, что в v2'шном скрине ringcor45 на пульте для корректоров в RST5 стоят неправильные коэффициенты (старые; исправлено было прямо в ИП-1 при Довженко, но программы на linac3/worker1 обновить забыл). Ведь такое же может возникнуть и в v4 -- когда выясняется, что коэффициенты "не те", но зачем рестартовать сервер, если несложно подкрутить всё прямо на лету? Заодно удобный инструмент настройки и тестирования.)

Multithreading:
  • 06.10.2013@душ: рано или поздно придётся делать поддержку, так что лучше сразу продумывать при реализации соответствующих кусков кода.

    06.10.2013@душ: конкретные соображения касательно покамест КЛИЕНТСКОЙ части.

    • Если cd/sid используются только в ОДНОМ thread'е -- то это safe.

      Хотя отдельный вопрос, какому thread'у будут приходить уведомления от fdiolib, т.е., в каком именно исполняется cxscheduler-совместимый цикл.

      Так что -- лучше б на "в каком" не закладываться.

    • Создание новых соединений затрагивает все thread'ы (из-за потенциального realloc'а), так что нужен global lock.
    • Ну и тогда можно сделать per-connection lock'и.

    Кстати, locking нужен:

    • как на уровне cxlib (при модификации списка соединений и для защиты внутренних структур каждого соединения; может, тогда иметь один глобальный для всего cxlib?), ...
    • так и на уровне cda -- ровно с теми же соображениями.

    А вот по СЕРВЕРНОЙ части всё выглядит намного непонятнее. Хотя тупой глобальный lock тоже для начала покатит.

    24.01.2015@Сессия-ИЯФ-Сухарев-12:20: мысли в продолжение вопроса (независимо от ранее написанного):

    ...кстати, а КАК параллелить конкретно клиентов? Варианты:

    1. в cxscheduler делать привязку дескрипторов к thread'ам;
    2. глобальные ресурсы (slotarray'и) окружать бы mutex'ами/семафорами, чтоб
      1. на запись (модификация) они бы делались эксклюзивными - т.к. растить массивы только по очереди;
      2. на чтение можно и параллельно, но запрещать модификацию (п."а"), поскольку закэшированные указатели при realloc-growing'е инвалидируются.

    ...отдельный вопрос - что на один dataref могут быть навешены ссылки от разных thread'ов; и как распределять callback'и по thread'ам? Видимо, правильный ответ - каждому thread'у заводить СВОЙ контекст. (Либо ОДИН thread работает с cda, а дальше уж сам распределяет по "клиентским" thread'ам - тогда вообще ничего делать не надо, всё и так будет работать.)

    26.08.2019: некоторые соображения на тему:

    • Идеальный вариант -- разделение работы: чтоб GUI занимался один thread, данными (взаимодействием с cda) другой и т.д.

      Но это вряд ли достижимо.

      И думать надо ещё и про поддержку в СЕРВЕРЕ.

    • Видимо, в cda придётся делать mutex'ы per-object:
      1. На каждый dataref.
      2. На каждый sid; и/или глобальный (на refs_list -- оно ж общее на ВСЕ sid'ы/контексты).
Множественные polling periods:
  • 16.09.2019: записываем мысли от вчера; раздел создаём в этой точке, сразу после "Multithreading" -- идеологически близкие вещи.
  • 15.09.2019@утро-зарядка: некоторые соображения насчёт того, как можно было бы организовать МНОЖЕСТВЕННЫЕ "polling periods", они же "polling threads" -- так эти вещи называются в прочих СУ.

    (Подумалось при обмышлении, как же работают 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

    Соответственно:

    1. Поле "pollid" к cxsd_hw_chan_t надо будет добавить.

      Как/где/откуда оно будет заполняться -- хбз.

      По умолчанию оно будет =0, и будет являться индексом в...

    2. 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: пара замечаний задним числом:

    1. Главный-то вопрос -- КАК могли бы заполняться эти "номера poller'ов" в каждом канале.

      В EPICS -- оно явно прописывается в КАЖДОМ record'е (поскольку авто-создания каналов при инстанциировании ("экземпляризовании"?) устройств там нет, а описывается каждый record индивидуально, то и проблемы заполнить ещё одно поле не возникает.

      В TANGO -- похоже, как-то в БД конфигурации указывается (как?).

    2. Собылие CYCLE надо отправлять по сети всё-таки только для ГЛАВНОГО цикла -- от прочих смысла нет.
Статическая БД:
  • 05.09.2014: пришла пора хорошенько задуматься о "статической БД" -- всех аспектах её устройства и реализации:
    • Вынимание инфы по каналам из таблицы в драйвере (или отдача драйвером).
    • Дополнение информацией, указанной в channels/devtype.
    • Плюс cpoint в разнообразнейших вариантах.
    • каналы _devstate -- и добавление, и поддержка (как организовать чтение!).
    • ...и API для поиска канала+свойств по имени (как в проекте от 03-03-2013 ({r,d}*) и 04-06-2014 (строки)).

    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@утро-душ: устройство собственно базы имён -- да простое:

    • cpoint'ы имеют номера выше номеров каналов, пишутся в отдельный массив, в котором индексируются номерами (id-cxsd_hw_numchans).
    • В каждой строчке записано имя, ссылка на следующую точку (для составления цепочки {r,d}, поэтому НЕ на конечную точку (аппаратный канал)), сами эти {r,d} (опционально) и строки (также опционально).
    • Поиск должен вестись с конца к началу -- чтобы более поздние записи имели бы приоритет.
    • Как именно хранить строки -- некоторый вопрос; видимо, в CxsdHwSetDb() на 1-м проходе подсчитывать объёмы, а на 2-м уже складировать и расставлять указатели/смещения (как аналогично уже делается).
    • Еще вопрос -- что делать с "динамичностью" статической БД: по прикидкам ДРАЙВЕРЫ могут давать имена каналов (например, remdrv -- очень далеко не сразу).

      Как-то зачеркивать строки, либо переставлять ссылки на имена...

      Для начала на это просто можно забить.

    • Но ГЛАВНЫЙ ВОПРОС -- как правильно поддерживать вложенные структуры: складировать прямо fully-qualified names вроде КОРЕНЬ.ВЕТКА.ПОДВЕТКА.КАНАЛ, или же как-то отражать структуру? Ведь если внутри сервера резолвинг аппаратных имён УСТРОЙСТВО.КАНАЛ можно делать 2-стадийным (найти устройство, потом в нём канал), то список для гуру надо бы формировать по-имённый -- в т.ч. с аппаратными именами.

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

    07.10.2014@беговая-дорожка: а зачем передавать гуру прямо полный (по-имённый) список каналов? Пусть бы он имеет в своём распоряжении ровно ту же структуру со списком устройств и каналов в них, что сам сервер, и выполняет такой же поиск.

    09.10.2014@8-ка-из-города: передавать эту структуру по UNIX-сокету довольно просто, там ведь все размеры идентичны на разных концах (платформа одна), так что сериализация/десериализация тривиальна.

    09.10.2014: еще некоторое количество размышлений в течение дня:

    • @душ насчёт поиска: хорошо было б в гуру (который внутри cxsd_fe_cx.c) иметь не свою реализацию поиска, а ИМЕННО ТУ ЖЕ, что в cxsd_hw.c: чтоб той указывалась "БД, в которой искать" -- вместо "текущей" ту, что получена от N-го сервера. Чтоб как-нибудь выделить из CxsdHwResolveChan() собственно поиск (оставив в ней только заполнение свойств с переходом по цепочке), например, в CxsdHwFindNodeIn().

      Но это не прокатит, поскольку у нас нет единой "текущей" БД, а она раскидана по куче глобальных переменных 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. (Только если для сохранения режимов -- там имя канала нужно.)

    • @вечер-перед-сном-дома ...и devtype'ы реализуются тривиально -- просто offset'ы из него раскладываются по каналам у каждого экземпляра его типа.

      24.03.2015: либо вообще реализовать devtype как этакий "namespace", и у экземпляра устройства проставлять ссылку на этот "namespace". А для без-devtype'ных (у которых личные channels) просто делаются отдельные namespace'ы, куда и проставляется namespace_ref.

      24.03.2015@вечер-Технопарк-Zoomer-занятие-по-Lego: еще немного мыслей о namespace'ах:

      1. У каждого есть номер ("id"; 0 -- нет/пустой), и в девайсах записываются прямо просто номера (и не нужно devtype'ы никуда копировать/раскладывать).
      2. Внутренности:
        • одно поле -- количество элементов;
        • плюс "аллокированное количество" (>= количества-использованных);
        • в конце -- массив [0] из дуплетов {оффсет_имени,номер_канала}.

      25.03.2015: во всём этом есть еще один нюанс: devtype -- это больше, чем просто namespace; у него еще есть имя-типа и ИНФО_ПО_КАНАЛАМ. Так что и их надо будет добавить.

    24.03.2015@утро-душ: а отдельные "точки контроля" ("символьные линки") так и делать отдельно, просто дополнительной таблицей, по которой и вести тупой последовательный поиск, только в ДВА этапа:

    1. Просто по совпадению имён: ищем "a.b.c.d", а оно записано в полном имени точки.
    2. Иерархически: ищем "a.b.c.d", а точка-симлинк имеет имя "a.b" -- тогда там, куда она указывает, ищем узел с именем "c.d".

    Возможно, при реализации этапы как-нибудь удобно самообъединятся.

    24.03.2015: насчёт общих/по-устройственных dev_namespace'ов есть идеологический вопрос: надо ли давать возможность "наследования"/импорта информации в channels из devtype'а?

    Для ответа надо понять -- а КОГДА вообще понадобится использовать индивидуальные channels? Сейчас в голову приходят только софтовые каналы -- "почтовые ящики". 31.03.2015: не "когда", а "ЗАЧЕМ"! Для указания свойств индивидуальных каналов -- диапазоны, geoinfo и т.п.

    28.03.2015@утро-душ: (или даже раньше) а всякие label/units/dpyfmt -- очень удобно хранить в том же strbuf, а в инфах записывать их оффсеты.

    28.03.2015@Аригато: насчёт проблемы "а как бы в гуру использовать тот же код резолвинга, что в самом сервере?":

    • Поиск -- "Resolve()" -- должен идти не по текущим cxsd_hw'шным переменным, а по указанной БД!
    • В таком случае у гуру будет просто копия БД каждого из подведомственных серверов, по которой он спокойно поищет.

      ...кстати, внутренности Resolve() должны переехать в cxsd_db.

    • Боковое следствие: количество каналов должно быть записано прямо в 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 нужны ясно когда/зачем -- для указания свойств индивидуальных экземпляров каналов.

      СЕЙ момент нет (поскольку вообще никаких свойств нету), но в близком будущем -- выглядит вполне актуально.

    • Есть некоторая идеологическая кривизна/нестыковка в текущей концепции:
      • С одной стороны, мы указываем просто NAMESPACE, который должен быть просто списком имён, ссылающихся на номера.
      • С другой же -- прямо там же указываем и СВОЙСТВА каналов.

      Для точек контроля это еще приемлемо (т.к. они только так и существуют, будучи указанными).

      А для настоящих каналов -- лёгкий бред: ведь каналы существуют независимо от наличия у них имён (в EPICS всё проще-яснее -- там прямо свойства конкретного канала и прописываешь в описании рЕкорда, от имени и до всяких пределов; там ведь никакого наследования нет -- вот и проще).

    • Касательно "наследования" можно сделать просто: ввести ДВА поля-ссылки на namespace: на devtype и на индивидуальный channels.

      Искать пытаться по обоим, но сначала по channels -- это позволит переопределять стандартные свойства (но только ВСЕ сразу, частично не удастся...).

    • Cpoints по-хорошему надо делать именно СИМВОЛЬНЫМИ ссылками: т.е., не резолвить прямо в момент считывания, а оставлять имена, чтоб резолвить уже в момент запроса. Это и даст максимальную гибкость -- в точности как желалось в проекте.

      ...а сами target-имена записывать так же, как и просто имена -- оффсетами в strbuf[].

    • И тут уже некий вопрос, как же бы такую БД (формально полным списком имён не обладающую, а только позволяющую проверить наличие и разрезолвить) совокуплять с EPICS'ным "portable CA server".
  • 05.03.2015: насчёт упомянутой в начале раздела "Вынимание инфы по каналам из таблицы в драйвере (или отдача драйвером)" -- давно назревало, и вот:

    Авторитетным источником информации по именам каналов должна быть КОНФИГУРАЦИЯ, драйверы же -- чисто консультативны.

    Причина очевидна: в момент запуска драйверов может еще не быть (при remdrv), или может быть режим симуляции, когда они необязательны.

    Так что надобность в наличии в драйвере таблицы имён (или еще как-либо вытягивания их из какой-то таблицы с целью отдачи серверу) -- сомнительна.

  • 12.03.2015: некоторые мысли насчёт назначения имён каналов. Появились по результатам запинывания adc812me_drv.c, в _drv_i.h для которого был допущен ляп в номере одного канала; а в EPICS'е-то при этом нет никаких номеров -- он же всё сам делает!

    12.03.2015: итак, мысль:

    Что можно бы сделать как-то АВТОназначение номеров -- чтоб у драйвера были только имена, а уж сервер назначал бы каждому имени номер, единожды при загрузке драйвера.

    Тогда программеру б не пришлось заниматься назначением этих чисел (что чревато ошибками).

    Но:

    1. Техническая проблема: драйверу придётся складывать эти назначения в некие переменные, и пользоваться переменными вместо нынешних констант nnn_CHAN_mmm.

      (Да даже если сервер САМ будет складывать в предподготовленную таблицу -- один фиг.)

      Плюс исчезает сортировка каналов -- "диапазоны"

    2. Учитывая наличие понятия "удалённый драйвер", эта архитектура становится еще более трудноисполнимой: "назначать номера при загрузке драйвера" -- никак, т.к. драйвер для всех удалённых один; вытаскивать таблицу имён из драйвера -- аналогично никак.

      Ну и т.д.

    Так что -- идея вряд ли жизнеспособна. По крайней мере, в НАШЕЙ архитектуре, рассчитанной, в отличие от EPICS'а, на многоканальные устройства, и в которой номера каналов играют некоторую роль -- как минимум, они доступны драйверам, а не просто гуляют по сетевому протоколу (как CA'шный SID).

  • 10.04.2015@утро-завтрак: еще со стародавних времён, с конца 1990-х, когда всё проектировалось с Ю.Эйдельманом в 521-й, была мысль, что и ЭКРАНЫ тоже должны храниться в БД на сервере, и добываться оттуда клиентом при старте.

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

  • 10.04.2015@утро-завтрак: зато другая идея, имеющая косвенное касательство к БД, вполне жизненна: если завести GUI-тулзу, могущую вычитывать из сервера список усройств, то можно, чтобы она умела запускать инженерный экран для этого устройства -- вычитав его devtype. Пара замечаний:
    • Этой тулзе нужна будет функциональность cx-starter'а -- по поиску окон. Посему потроха cx-starter'а надо делать как библиотеку.
    • Такие экраны надо будет как-то на дисплее отличать. Для чего ставить им то ли title, то ли app_name в виде
      devtype: SERVER:N.devname
Clientside-режимы:
  • 16.04.2015@Снежинск-каземат-11:
  • 16.04.2015@Снежинск-Икарус-на-полигон (уже полигон, после внешнего КПП и перед внутренним; после очередной перепалки с Акимовым на тему организации "мозгов"):
    • Исходные акимовские утверждения: "не надо, чтоб программа портила режим своими вычислениями, а пусть работает с сохранённым в режиме, сдвигая тамошние уставки в каналы DL200 на сколько-то".
    • Моя реакция: режим сейчас -- это адская смесь разнородных параметров/сущностей, с которым хрен что сделаешь.
    • Размышление: ведь "режимы" в нынешнем понимании -- это временный хак, сделанный году в 2003-м на скорую руку, чтоб было хоть что-то.

      И они таковы потому, что не было адресации по именам. Но сейчас-то адресация по именам ЕСТЬ!

    • Идея: реализовать "режим" как набор данных, сохранённых из некоего списка имён каналов (а не ручек!). Список -- да хоть просто плоский (текстовым файлом).

      И назвать эту сущность-список "dataset".

    • Замечание: сохранять надо не просто пары (имя,значение), а с кучей атрибутов -- как минимум timestamp и rflags.
    • Мысль: такие dataset'ы, при правильной разбивке каналов по ним, можно комбинировать разнообразными способами. Например, на ЛИУ-2: настройки накалов тиратронов -- один; по-канальные тайминги стоечных DL200 -- другой; настройки центральной DL200 (в виде сдвига нуля и межимпульсного времени) -- третий.

    Утилита сохранения режима, конечно, слегка нетривиальна -- надо после запуска еще дождаться, пока придут все запрошенные значения.

    @обед-после-столовой-автобус-в-каземат: еще пара замечаний:

    1. Утилиты эти -- что-нибудь вроде dataset-save и dataset-restore -- держать прямо в programs/utils/, рядом с cdaclient и das-experiment, которые тоже должны уметь "правильно" выдавать текстовое представление значений.
    2. К вопросу о том, КАК понимать пришедшесть всех значений: с одной стороны, надо бы именно ДОЖДАТЬСЯ, пока все придут (с неким таймаутом). Но, с другой -- ведь для rw-каналов события CDA_REF_R_UPDATE никогда и не придут, т.к. они когда-то вычитаны, и теперь "TRUSTED".
      • Первая мысль -- в cda_dat_p_update_dataset() для не-call_update (опкод CXC_CURVAL, ответ на CXC_PEEK) генерить события с другим reason, например, CDA_REF_R_CURVAL).
      • Вторая -- зачем плодить лишние сущности: при TRUSTED можно генерить CDA_REF_R_UPDATE ВСЕГДА, независимо от call_update -- ведь по факту это то же самое, что настоящее обновление.

        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-каналы работает, и даёт нужный эффект.

  • 31.08.2015: а ведь всё-таки стендовым клиентам нужно простое ЛОКАЛЬНОЕ сохранение/чтение режимов, желательно -- интегрированное в них, с теми кнопками Save/Load.

    Формат файлов -- можно тот же, что у 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: и еще вопрос: ВОССТАНОВЛЕНИЕ как делать?

    • Если по имени канала, как предполагается -- то не будет работать ограничение по диапазонам.
    • Как-то сохранять всё-таки и имя/путь ручки, чтобы делать ей setcontrolvalue()?

      Видимо, надо как-то сохранять и то, и другое -- чтоб cdaclient/dataset-restore использовали имена каналов, а Cdr -- имена ручек. Подумать о синтаксисе?

      Тогда, кстати, и проблема регистров/формул решится автоматом, и относительности тоже.

    10.11.2015: о синтаксисе подумано, ответ прост и ясен:

    • Да, формат должен быть совместим с cdaclient/dataset-restore.
    • Для этого ПЕРЕД именем канала может быть -- опционально -- имя ручки в круглых скобках -- т.е.,
      (ИМЯ_РУЧКИ)ССЫЛКА_НА_КАНАЛ

      Соответственно,

      1. cdaclient.c::ReadFromFile() должен будет при обнаружении '(' (после опционального "@TIME") пропустить всё до ')' включительно.
      2. Cdr же нехай смотрит: если есть (ИМЯ_РУЧКИ) -- то ищет её и делает ей 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: "бла-бла-бла..." -- в смысле, много наговорено, но сделано всё по-другому и сильно проще. А именно:

    • Еще в феврале-ноябре 2016-го.
    • Сохраняется в формате, совместимом с cdaclient'овским.

      Так что можно и Cdr'ные режимы восстанавливать cdaclient'ом, и, теоретически, cdaclient'овские подсовывать Cdr'у.

    • При чтении используется никакой не ppf4td, а просто fopen() со стандартным цикром while(fgets(...)!=NULL.
    • ...и да, вместо "сохранябельности" можно указывать НЕсохранябельность, а по умолчанию сохраняется всё.
Фишки:
  • 29.09.2014: это сборный раздел для обсуждения разных "фишек" CXv4, в основном реализуемых в сервере.
  • 29.09.2014: собственно, что относится к этим штучкам:
    • Блокировки (locking).
    • "Цвета" каналов и "фазы" работы (colors and phases) сервера.
    • Автосохранение/автозагрузка режимов. Плюс -- явное сохранение и загрузка. // Порядок/приоритет, складывание значений в next_wr_val
    • Базовые и настроечные каналы -- атомарные/"параметризованные" чтение/запись.
    • Строки (ident, label, ...), коэффициенты.
    • (01.03.2015) "Библиотечная" структура реализации разных классов каналов вместо типов record'ов.
Блокировки:
  • 30.09.2014: предыдущие заметки на эту тему были в списке фич за 06-07-2007 и в cxsd за 08-09-2014.
  • 08.09.2014: со стороны cda...

    08.09.2014@вечер-лесок-от-Коптюга-у-НГУшного-стадиона: чуток в сторону, касательно блокировок cda:

    1. Ясно, что надо будет иметь какой-то вызов "добудь блокировку на каналы" (и соответствующие методы у dat-плагинов). Вопрос в формате:
      1. Блокировать каналы поштучно?
      2. Или указывать сразу пачку, и чтоб могли заблокироваться все сразу?

      Симпатичнее второй вариант: он позволит атомарно блокировать группы каналов (что необходимо для избежания race condition'ов -- по крайней мере, в рамках каждого сервера). Придётся, конечно, приложить мозги: разбивать запрос на по-sid'ные группы, сбагривая каждую кому надо.

      Поскольку операция продолжительная, то надо будет асинхронно уведомлять клиентов, чему посвящен следующий пункт.

    2. При разрыве соединения с сервером блокировка считается утерянной, о чём надо уведомить клиента -- специальным 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'ов.
      • Поскольку в передаваемом наборе могут быть поперемешаны каналы от разных sid'ов, то этот массив она МОДИФИЦИРУЕТ -- сортируя каналы по sid'ам.

        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.

      • Лочить разрешается ТОЛЬКО обычные каналы (REF_TYPE_CHN).
      • Если хоть у одного вовлечённого sid'а метод do_lock==NULL, то обламывается вся операция целиком, не лочится ничего.
      • Буфер, в который складируются hwr'ы ref'ов конкретного sid'а, добавлен к 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).

    • Во-первых, для обычных (с указанием сервера в имени) каналов надо будет повторять блокировку ТОЙ ЖЕ ПАЧКОЙ.
    • Во-вторых, для broadcast-резолвлемых каналов надо будет иметь процедуру "переноса блокировок" в случае переезда их в другой sid. И, по-хорошему, тоже лочить пачкой.
    • Частичный ответ: для каждой пачки запоминать уникальный идентификатор (глобальный, для всех sid'ов), и при миграции делать лочку всех новоприбывших с одинаковым идентификатором одновременно.

      ...или идентификатор иметь глобальный вообще, в рамках cda? Тогда и у разных sid'ов изначальные наборы, полученные от одного вызова, будут иметь этот идентификатор совпадающим.

      Встаёт вопрос о поддержании набора уникальных идентификаторов:

      • Тупо ++'имый счёчик не катит, поскольку через 2^32 обращений переполнится.
      • Вариант решения: иметь SLOTARRAY, где единственным полем будет reference count, при лочке ставить ему число использующих каналов, а при разлочке -- уменьшать на количество разлочиваемых, и при ==0 освобождать.

        Да, в refinfo_t придётся добавить поле "lock_id".

      • Встанет вопрос недублирующести/неповторности: чтоб уже запрошенные на лоченье ref'ы исключались из списка "на блокировку", а и так не залоченные исключались из "на разблокировку".

        Проще всего это реализовать при сортировке: такие ref'ы считать "очень большими", чтоб загонялись в конец списка.

    Результат на сейчас: к cda_dat_p_lock_op_f() добавлен параметр lock_id (пока передаётся -1).

  • 30.10.2014: со стороны cxsd_hw...

    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: (по результатам обсуждения "как бы наладить взаимодействие между установками ВЭПП-5/ВЭПП-4/ВЭПП-2000, и чтоб соседние установки имели доступ ко многим [проксируемым] каналам на чтение, а к некоторым -- еще и на запись, но чтоб разные установки имели разные права"): понятие блокировок тесно перекрывается с понятием "access control". В т.ч. даже право накладывать блокировку также должно регулироваться access control'ом.

    И еще это как-то может взаимодействовать с "цветами" каналов и фазами...

    18.03.2016: обсуждать тут пока не очень понятно, что. Ниже просто сборище мыслей на тему.

    • По большому счёту всё это -- блокировки, "цвета", access control -- часть общей системы "дозволения записи" (а по-хорошему -- и чтения тоже).
    • Сюда же косвенно относится консольный интерфейс -- каким клиентам что можно.
    • Регулироваться оно (например, кому можно накладывать блокировки, а кому неи) должно бы всё мочь с участием access control.
    • А access control, в том числе, должен бы быть и "динамическим" -- чтобы некоторые права можно было давать/отбирать во время работы. Например, какому-то клиенту (или адресу?) дать временно право на запись.
    • Можно также решения принимать и на основании progname/username. Хоть это по факту и порицаемое "clientside security", но для наших целей приемлемо и достаточно удобно.
    • С access control есть одна засада. Точнее, даже две.
      1. Как осуществлять фильтрацию на основе ИМЁН каналов? Ведь команды чтения/записи адресуются не по именам, а уже по идентификаторам (числовым адресам каналов).
        • Единственное, что приходит в голову -- при резолвинге производить заодно проверку по access control, и результат сохранять в какой-то внутренней структуре (коих иметь slotarray; возможно, тот же, который по мониторируемым каналам).
          • А доступ (чтение/запись/мониторинг) к каналам позволять только если они фигурируют в этом списке "спрошенных ранее".
          • ...или вообще в протоколе ввести правило, что при резолвинге возвращается не gcid канала, а номер ячейки в списке разрезолвленных.

            Тогда "resolve" де-факто превращается в "open()", а возвращает он как бы "дескриптор в таблице дескрипторов процесса" (тот самый список юзаемых каналов), а уж в ячейках этой таблицы хранятся сами gcid -- тем самым получается некоторая аналогия с работой файлов в *nix, где 2 уровня таблиц дескрипторов.

        • Соответственно, прямой доступ по номерам (gcid) придётся запретить. Но не только по этой причине, а еще и вследствие пункта (II).
      2. Собственно "имена" -- не каноничны; точнее, не однозначны.
        • Есть каноничные имена -- УСТРОЙСТВО.КАНАЛ (хотя и там бывают alias'ы), ...
        • а есть "симлинки"-cpoint'ы, причём оных может быть толпа разных и понятие "иерархии" там, вследствие возможности симлинков на симлинки и симлинков на устройства и на "директории" симлинков, весьма расплывчато.
        • ...А еще и числовые ссылки -- через gcid...
  • 20.04.2020: приступаем к работам по реализации возможности блокировки каналов (локинга).

    09.05.2020: поскольку делалось всё совместно, да и переплетено сильно, то здесь тоже всё запротоколировано совместно, без разделения по cda, cxsd_hw и размышлениям.

    20.04.2020: размышления на тему локинга. И мысль: а не нужно ли при попытке залочить уже залоченный канал попробовать "пнуть" владельца лока? По аналогии с remdrv - если то соединение уже отвалилось (например, по питанию), а новолочащий - как раз реинкарнация "того". Соответственно, тот, кто накладывает лок, должен пытаться делать это 2-3 раза с паузой в несколько секунд.

    21.04.2020: продолжение размышлений:

    • А насколько у нас вообще адекватная модель, с ОТДЕЛЬНЫМ локингом? Может, не так уж и плохо было придумано в VCAS'е, что именно прямо РЕГИСТРАЦИЯ канала делается в режиме "эксклюзивно"?
    • В принципе, подобное можно б было имитировать, введя флажок вроде "CDA_DATAREF_OPT_EXCLUSIVE".
    • Прикол только в том, что тогда и для cdaclient'а нужно будет какой-то @-символ предусматривать, а к "excl" (EXCLusive) идеально подошел бы '!' (EXCLamation mark). Но этот символ уже занят в bridge_drv для "trusted" - придётся там переделывать.

    После размышления начал пилить локинг.

    • Для начала сделаем в cxsd_hw - CxsdHwLockChannels() - плюс в cda_d_insrv: там всё локально, без необходимости гонять по сети и следить за дисконнектами/реконнектами. Так что можно на простом варианте обкатать/потестировать.
    • Начинаем с CxsdHwLockChannels() - он простой, работает в 2 прохода:
      1. Проверить все каналы в списке, что они годны и операция lock/unlock с ними ВСЕМИ возможна.
      2. Блокировка накладывается либо снимается.
    • Приняты правила касательно operation:
      1. Режим RD "не существует" и игнорируется.
      2. Наличие бита WR означает "наложить блокировку", а отсутствие - "снять блокировку".
      3. Бит ALL_OR_NOTHING считается обязательным, без него - ошибка.
    • И ещё особенность: CxsdHwLockChannels() при ошибке возвращает в errno "объяснение" для неё. Это единственный такой вызов в cxsd_hw (кроме сервисных, вроде добавления/удаления evproc'ев).

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

    • Результат:
      • Ему локинг нужен вообще исключительно для имитации драйверов клиентами -- т.е., чтобы некий канал мог бы себя вести как статусный readonly-канал драйвера, чтоб один-единственный клиент-драйвер в него бы отдавал статусы, а более никому это б было недоступно.
      • Про эксклюзивную же запись в реально аппаратные каналы речи вроде не идёт.
      • Кроме того, его бы вполне устроил вариант "прямо при коннекте каналы лочатся", а динамически снимать/накладывать блокировки вроде не нужно.
    • Ну -- понятно, что именно "полноценный" локинг его потребности бы решил.
    • Кстати, попробовал погуглить на тему локинга в EPICS И TANGO. Результат -- нулевой.
      • Точнее, в EPICS есть некий "Database Locking" -- чтобы разные thread'ы могли бы корректно параллельно доступаться к БД, не вызывая её повреждения; но и только.
      • А по TANGO вообще ноль.

        22.04.2020: а, нет -- есть! Надо было не гуглить, а смотреть в документации. Так вот, в tango_92.pdf есть раздел "4.9 Device locking". Итого: там лочится прямо всё устройство целиком, через методы DeviceProxy -- lock() и unlock(). А вот снятие блокировки crash'нутого клиента делается не по дисконнекту, а через указанный интервал времени.

    22.04.20@утро, ~09:00: насчёт lockset'ов: а зачем делать какой-то менеджмент, SLOTARRAY etc?

    • Пусть КЛИЕНТ указывает этот lockset_id. Уж он-то знает, какие каналы у него "взаимосвязаны".
    • А для тех каналов, которым локинг указывается флагом прямо при регистрации -- пусть назначается lockset_id=0.

    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(). И тут становится ясно, что не всё так просто -- есть подводные камни, о которых раньше не думалось:

    1. При отваливании клиента надо будет выполнять подчистку. Это можно делать в двух разных местах:
      1. В клиентском интерфейсе -- cxsd_fe_cx и cda_d_insrv.

        Тогда нужно там хранить массив-список всех лоченных в данный момент hwr'ов, плюс как-то его поддерживать в актуальном состоянии (держать всегда сортированным?).

        @душ-после-завтрака: ещё соображения:

        • А ещё надо не забывать снимать локи при разрегистрации отдельных hwr'ов.
        • БИНГО!!! Так после этого никакой "массив-список всех лоченных" и не нужен! Поскольку можно просто в момент разрегистрации hwr'а проверять, не залочен ли он данным uniq, и если "да", то лок снимать.

          Пара замечаний:

          1. Хотя это даст проблему с "локингом всего девайса через _devstate": если явно лочилось устройство целиком, а разрегистрируется один канал из серединки...

            Впрочем, это сценарий хоть и возможный, но крайне экзотичный -- можно забить.

          2. Эта идея касается именно cda_d_insrv и cxsd_fe_cx -- они-то локальны к cxsd_hw и имеют прямой доступ туда. В cda_d_cx же список лоченных будет нужен -- как минимум для того, чтобы восстанавливать локи после реконнектов.

          Ура-ура -- идея вполне рабочая, надо реализовывать.

      2. В самом cxsd_hw -- в cxsd_hw_do_cleanup().

        Причём, с учётом сделанного вчера "лоченья устройств целиком посредством _devstate" -- лучше цикл гнать сверху вниз.

    2. В самом cda_d_insrv надо будет выполнять трансляцию hwrs[]->gcids[]. Т.е., опять в функции нужен доступ к переменной-массиву заранее неясного размера; а не используется ли у нас уже где-нибудь подобное? Только тут вариант "разбивать на пачки по 100 штук" не катит -- операция должна быть атомарной.

      23.04.2020@утро-душ: теоретически можно б было воспользоваться прямо переданным массивом hwrs[] -- поподменяв в нём hwr'ы на gcid'ы, ведь всё равно это всё int'ы. Но это очень плохая идея -- радикально нарушаются правила инкапсуляции/изоляции. Так что не будем.

    ...кстати,

    • в cda_d_insrv'шной RlsLcnSlot() НЕ вызывается cxsd_hw_do_cleanup(), а модуль полагается на то, что cda_core при удалении соединения вызывает удаление всех каналов поштучно, так что в UnRegisterInsrvHwr() просто делается индивидуальный CxsdHwDelChanEvproc().
    • В принципе, оно и понятно -- никакого cleanup'а делать нельзя, поскольку прибивается лишь соединение, а драйвер-то ещё жив, и именно он является первичным владельцем идентификатора uniq (==devid), а не модуль доступа (в отличие от cxsd_fe_cx).
    • Ну да, в основном соединения будут грохаться именно в момент TerminDev()'а, но нем не менее (кстати, посмотрел -- да, там очень тщательно выбран порядок вызова cleanuper'ов, так что cda'шный будет вызван первым).

    Чуть позже, по результатам идеи про "в момент разрегистрации hwr'а проверять, не залочен ли он данным uniq":

    1. Чтоб кто попало не лазил в cxsd_hw_channels[] напрямую, лучше б сделать CxsdHw-функцию "кто локер данного gcid'а".

      ...впрочем, анализ кода показал, что cda_d_insrv и cxsd_fe_* и так невозбранно лазят в cxsd_hw_channels[] как к себе домой. Так что глубокого смысла и нет.

    2. Следствие: поскольку оная функция должна будет уметь возвращать и ошибку, то lyrid==-1 будет проблемой: он сейчас вполне валидный (и самый используемый!), но именно -1 было бы идеальным выделенным кодом для ошибки.

      А вот это будет в любом случае небесполезно. Несрочно, но лучше б сделать.

    Итого -- делаем в cda_d_insrv.c:

    • Перво-наперво -- массив для трансляции hwrs[]->gcids[]: из cda_core.c взят менеджмент hwr_arr_buf[], который переименован в gcn_arr_buf[]/
    • Использование его в cda_d_insrv_lock_op(): рост до нужного объёма, затем наполнение (совмещённое с предварительной проверкой валидности hwr'а) и в конце сбагривание в CxsdHwLockChannels().
    • И "простодушное" снятие блокировки в UnRegisterInsrvHwr().
    • Чего пока НЕ сделано, совсем-совсем -- так это использования lockset_id.

      ПОЗЖЕ.

    И снятие блокировок в cxsd_hw_do_cleanup() тоже сделано -- циклом сверху вниз.

    А вот дальше надо заниматься уже веткой cda_d_cx<-cxlib<-протокол->cxsd_fe_cx; но там пока слабопонятно, как именно делать -- там ведь "отложенный" локинг (когда разрезолвится), плюс надо повторять при реконнектах (а как? как с группами быть?). Поэтому временно отвлечёмся и чуток переделаем диаграмму работы резолвинга (см. от 29-10-2019) -- это как раз близкая тема; заодно и освежим в памяти функционирование cda_d_cx -- глядишь, снизойдёт озарение.

    25.04.2020@вечер: возвращаемся к реализации локинга, после пары дней возни с поддержкой "модификации БД на лету" (отвлекался туда, чтобы лучше погрузиться в функционирование cda_d_cx; погрузился).

    Консенсус внутри меня таков: раз нет чёткого понимания, как можно всё сделать, то будем делать ВСЁ и как получится ("если не знаете, что делать, то делайте хоть что-нибудь"), а потом посмотрим, что выйдет. Это "ВСЁ" включает в себя:

    • Флажок CDA_DATAREF_OPT_EXCLUSIVE.
    • Запросы на блокировку слать "уж как получится":
      1. для OPT_EXCLUSIVE-каналов -- вместе с запросом мониторинга;
      2. для явного вызова -- видимо, сразу при состоянии OPERATING и "запоминать на потом" в противном случае.
    • Как обращаться с lockset_id -- пока неясно.
    • Как хранить список каналов, пришедший от cda_lock_chans() (для восстановления локов после реконнектов) -- также не вполне ясно. И ведь РАЗблокировка должна из него удалять.

      Видимо, достаточным (на сейчас?) решением будет просто помечать каналы так же, как и по OPT_EXCLUSIVE -- тогда всё получится автоматом.

    • Ответы от сервера "успешная/неуспешная блокировка" просто передавать cda_core как есть, а уж тот пусть транслирует их клиенту в событие CDA_REF_R_LOCKSTAT.
      • По обрыву соединения можно (нужно!) сразу генерить сообщение "потеря блокировки".
      • И dat-plugin НЕ должен пытаться давать события успех/потеря сбалансированными "парами" -- пусть просто даёт, и всё.
      • ...а вот cda_core -- та уже может фильтровать их, отдавая клиентам сообщение только при СМЕНЕ состояния.

        Для чего ей придётся хранить у себя текущее состояние.

    • Также нужен вызов вроде "cda_lock_stat_of_ref()". Он будет просто возвращать то самое запомненное в refinfo_t состояние.

      27.08.2020: ага, вот только этот вызов как-то был забыт -- прототип в cda.h имелся, а реализация отсутствовала, на что Федя натолкнулся. Добавлен.

    26.04.2020: приступаем.

    • Флажок CDA_DATAREF_OPT_EXCLUSIVE создан.
    • В console_cda_util.c поддержка его взведения добавлена -- префикс "@!:" (да, типа перекрывается с bridge_drv'шным -- будем думать; как и о том, надо ли его добавлять в cvt2ref()), а в cdaclient.c -- реакция на события LOCKSTAT по ключу -DL.
    • В cda_core.c:
      • В 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()'а.

    • В cda_d_insrv():
      1. В _new_chan() добавлена обработка CDA_DATAREF_OPT_EXCLUSIVE: сразу же делает локинг и затем отдаёт состояние.
      2. И lock_op() после локинга отдаёт lockstat затронутых каналов.

      Пара замечаний по реализации:

      • В обоих случаях отдаваемое наверх состояние "залочен/нет" определяется как "cxsd_hw_channels[gcid].locker == uniq", т.е., отдаётся РЕАЛЬНОЕ состояние, а не предполагаемое по успешности 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.

    • cx_proto_v4.h:
      1. Код CXC_LOCK_OP = CXC_REQ_CMD('C', 'l', 'o').

        (А на будущее, с "через handle" -- скорее всего, возьмём 'lk' вместо 'lo'.)

      2. CxV4LockOpChunk, для использования И в запросе, И в отклике: "туда" уходит поле operation, "оттуда" возвращается lockstat_result.
    • cxlib:
      • Добавлен код CAR_LOCKSTAT...
      • ...плюс его генерация по приходу ответа на CXC_LOCK_OP.
      • Ну и собственно клиентский вызов -- cx_rq_l_o() ("l_o" -- Lock Op).

        В отличие от прочих (мониторы, чтение/запись), он ОДНОКАНАЛЬНЫЙ, а не многоканальный. Резон простой: как показал опыт, для единственного юзера -- cda_d_cx -- требуются именно одноканальные операции; многоканальные же использовались в v2 и сделаны лишь по старой памяти.

    • cda_d_cx.c (29.04.2020):
      • В hwrinfo_t добавлено поле rq_lock.
      • И повсеместно, где делается cx_setmon(), также добавлен условный вызов cx_rq_l_o().
      • Сделана ловля события CAR_LOCKSTAT, по которому присланное состояние отдаётся наверх.
      • Также по потере соединения -- точнее, в 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'а,
        18.06.2020: хал-ту-ра!!!

        Тогда реализация была сделана с кучей косяков:

        • Отсутствовал вызов cx_begin().
        • Были смешаны 2 разных действия:
          1. запоминание в hwrinfo_t запрошенного режима залоченности;
          2. отправка запроса на локинг.

          Последнее-то делается только при (a) готовности соединения -- CDA_DAT_P_OPERATING, и (b) готовности канала -- RSLV_STATE_DONE.

          В результате, если хоть одно из этих условий не выполнялось, то и желаемый режим "эксклюзивности" не запоминался -- при не-(a) для ВСЕХ каналов в списке, а при не-(b) для конкретного.

        Т.е., оно в принципе работать не могло, да и не тестировалось (тестировался лишь вариант CDA_DATAREF_OPT_EXCLUSIVE).

        Исправляем:

        • Добавлен вызов cx_begin().
        • Сделано ДВА цикла:
          1. сначала только сохраняем запрошенный режим лоченности -- безусловно,
          2. затем готовим и отправляем запрос серверу -- тут уже условно, при наличии готовностей.

        Протестировать бы теперь именно ДИНАМИЧЕСКИЙ локинг...

        21.06.2020: сделан тест -- work/tests/test_cda_lock_chans.c.

        22.06.2020: потестировал -- похоже, работает.

      • Концепция "lockset_id" пока напрочь игнорируется, а в уведомлениях отдаётся 0.

        Но в порядке проекта -- если захочется сделать, то можно заменить поле rq_lock на lockset_id, в которое при отсутствии запроса блокировки записывать -1 ("не надо"), по OPT_EXCLUSIVE -- 0, а при явном указании -- что указано.

    • cxsd_fe_cx.c (30.04.2020):
      • ВСЁ СЛОЖНО... Тут оказалось неприятнее всего, и "по полной программе" сделать не удалось.
      • В ServeIORequest() просто вычитывается информация запроса, вызывается локинг, а потом клиенту отправляется текущее состояние этой блокировки.
      • Отправка клиенту состояния делается в PutLkStChunkReply()
      • Чего НЕ сделано:
        • Автоматическое снятие блокировки при удалении монитора так и не сделано.

          Потому, что чтобы найти монитор, которому нужно прописать это состояние -- надо знать его "cond". А у нас нет никакой возможности угадать значение этого поля.

          Это, кстати, и есть одна из причин, почему НАДО переходить в протоколе с "прямой адресации по gcid" на "через handle".

        • Ну и, собственно, "прописывание" состояния блокировки в монитор отсутствует -- по вышеописанной причине. Хотя определение значения do_lock имеется (из CxsdHwLockChannels() скопировано).

        Будем полагаться на то, что блокировку снимает cda_d_cx_del_chan() (ну либо закрытие соединения).

      • Заодно, кстати, исправлен случайно замеченный косяк в CXC_???MON: там бралось просто поле из пришедшего по сети куска данных --
        cpid = monr->cpid;
        -- вместо надлежащего конвертирования:
        cpid = host_i32(cp, monr->cpid);

        Когда всё с одинаковым endianness -- проблем не будет, но в общем случае это косяк.

    Осмысление содеянного:

    • Итак, идеологическая проблема заключалась в том, что локинг, в отличие от чтения и записи -- это СИНХРОННАЯ операция, обязательно требующая результата/подтверждения.

      (Чтение -- вешается на сервер, мониторирующий каналы и присылающий данные по мере получения; запись -- вместо синхронной модели используется асинхронная, "отправляем ЗАПРОС на запись", а там уж как получится (обычно доходит, но может и потеряться).)

    • Так вот: что было сделано с точки зрения "идеологии" -- эта синхронная операция декомпозирована на набор асинхронных, которые в сумме позволяют получить тот же результат.
    • ...хотя если смотреть глубже, то и чтение с записью -- тоже синхронные операции, которые также были декомпозированы/трансформированы в асинхронную модель.

    А теперь пара мыслей, пришедших по результатам реализации (точнее, даже ДО -- ещё утром, при визуальном анализе кода:

    1. Замечена потенциальная проблема:
      • Если cx_run() почему-либо обломится (например, соединение закрыто с той стороны и отправка даст ошибку), то прямо изнутри cx_run()'а будет вызван MarkAsClosed() и далее ProcessCxlibEvent(), в котором cxlib-соединение будет закрыто.
      • Соответственно, и состояние sid'а также изменится.
      • А у нас в нескольких местах результат cx_run()'а НЕ проверяется.
      • Если его вызов стоит последним и далее возврат -- то никаких проблем (особенно если это _del_chan() -- всё равно подчистка, и если соединение порвалось, то серверу уже более ничего говорить не надо, у себя он подчистит сам).
      • А вот если вызов НЕ последний и далее вызываются какие-то dat_p-действия (или return какого-нибудь "success") -- то проблема: к моменту этих действий реальное состояние уже изменится и они только всё испортят.
      • Таковых у нас 2 точки: SuccesProc() и ProcessCxlibEvent().

        18.05.2020: поставлены проверки результата cx_run() в этих 2 точках.

    2. И ещё наблюдение: нехорошо, что по CX-протоколу ходят команды, адресующиеся прямо к каналам сервера по номерам (gcid). Это сильно усложняет для cxsd_fe_cx подчистку: он же должен снять лок, а лок по протоколу ставится на gcid, а не на "монитор" -- так что, формально, у самого cxsd_fe_cx этот лок нигде не запоминается. Придётся при лок-операциях отдельно искать, есть ли ассоциированный с этим gcid монитор (а если их несколько?).
      • Вот если бы ЛЮБЫЕ операции с каналами адресовались не по gcid, а только по monitor-id, который надо предварительно получить, то проблемы бы и не было.
      • Так что да -- EPICS-Channel-Access'ный подход с принудительным использованием SID (Server-side ID) более правильный.
      • А в протоколе CXv4 сделано НЕ так чисто по наследству/старой памяти от предыдущих версий. В cda_d_cx-то всё реально везде было бы достаточно monitor-id -- она уже устроена "правильно".
      • И как я мог так лохануться?! Ведь к тому времени давным-давно знал принципы "правильного" устройства -- ещё со времён DOS с доступом к файлам через handle'ы, и затем по опыту cxscheduler'а, в котором первоначальная неудобная модель с прямой адресацией файловых дескрипторов и clientside-timeout-structs была переделана на "при регистрации создаётся дескриптор, через который и идут все дальнейшие действия".
      • И с MUSTER and Co проблем бы не было -- по причине отсутствия торчащести gcid в протокол.

        ...надо выше в comment2 написать, что не требовалось бы никакой "гарантии неповторяющести hwid".

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

        Хотя можно сделать проще и быстрее: ведь этот аспект касается ТОЛЬКО протокола и почти не торчит, так что можно параллельно добавить второй вариант реализации "мониторы как каналы", а потом перейти на это как на основной способ работы. Протокол при этом можно проапгрейдить до 4.1; только надо явно смотреть, как где сравнивается на тему VERSION_IS_COMPATIBLE.

        Мелкая идея по будущей реализации: надо будет в сетевом вызове "создай монитор для канала с таким-то именем" сделать параметр "создай, даже если канал не найден". Смысл -- чтобы cda могла так указывать каналы с RSLV_TYPE_NAME, про которые точно известно, что они именно к этому серверу относятся; а каналам RSLV_TYPE_GLBL будет указываться 0, чтобы отсутствие такого имени в сервере возвращало бы канал в состояние RSLV_STATE_UNKNOWN.

      • Замечание в сторону: информирование клиента о hwid всё-таки полезно -- чисто в целях отладки. Так что можно будет оставить отправку ИНФОРМАЦИОННОГО сообщения "вот у этого канала сейчас hwid такой-то".
      • И ещё будет приятный бонус: исчезнет имеющаяся сейчас чехарда с cpid/hwid -- где что использовать (см. обсуждение за 28-07-2015 в разделе о cxsd_fe_cx). Вся эта специфика "аппаратный ли это канал или cpoint" будет инкапсулирована внутри frontend'а, а до клиента не дойдёт (кроме как в виде отладочной информации).

    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.

    • (вечер) решил доперепроверить -- убедиться, что и сообщение LOCKSTAT корректно доходит. Оказалось -- нет, не доходит...

      Разобрался: в cda_dat_p_report_dataset_lockstat() вместо R_LOCKSTAT рассылалось R_STATCHG (копировал из cda_dat_p_defunct_dataset() и забыл сменить код).

      Исправил, проверяем: да, сообщение доходит.

    • (заполночь) ...но вылезла новая напасть: БЛОКИРОВКА не работает! Т.е., лочишь одним cdaclient'ом -- прилетает сообщение, что лок получен; параллельно запускаешь другой на тот же канал, и тоже с лочкой -- и ему тоже прилетает сообщение, что лок получен, и запись от него проходит!

      Сходу причина не найдена, да и поздно уже -- завтра...

    • 02.05.2020: разобрался. Косяк оказался в том, что условие "выставлен ли флаг ALLORNOTHING" было незавершённым: отсутствовала часть "then" (вернуть ошибку), и в качестве оной Си-компилятор воспринимал цикл "проверки возможности блокировки".

      После исправления-то оно заработало, но есть неясности:

      1. Когда/как оказалось, что этой «резолютивной части "then"» нету?

        Остаётся только предполагать, что удалил её случайно, при каком-нибудь копировании куска текста (сделал Ctrl+K,Ctrl+M вместо Ctrl+K,Ctrl+C) -- всё-таки дистанционная работа из дома на x10sae имеет изрядную латентность и работаешь как в густом желе; возможно, косякнул.

      2. КАК при этом первая проверка оказалась успешной?

        Я проверил "backup'ы" несколькодневной давности -- уже тогда косяк был. Т.е., проверка проводилась с ним.

        А каким образом при этом могло всё работать корректно -- неясно.

        А-а-а, дошло!!! Та проверка проводилась просто попыткой записи в залоченный канал -- эта запись и обламывалась. А вечерняя проверка доставки событий LOCKSTAT проводилась уже с префиксом "@!:" у ОБОИХ cdaclient'ов -- и у тестирующего-пишущего тоже, вот он и "крал" лок у первого, поэтому и запись от него отрабатывалась.

        У-ф-ф, отлегло... :)

    Итого -- получается, что вроде как работает? Квест почти двухнедельной длительности завершён?

    Ну-у-у, почти:

    1. Нет автоматического сброса лока в cxsd_fe_cx -- но это уж после перехода на протокол с handle'ами.
    2. Также пока нет ни "пинга" текущего владельца при попытке залочки уже лоченого канала, ни повторов попыток лочки со стороны клиента (см. за 20-04-2020).

      Ни то, ни другое в текущей модели реализовать невозможно: ни у cxsd_hw нет механизма вызова владельца, ни клиент не делает повторных попыток локинга (и последнее уже вообще неясно как исправить: это РАНЬШЕ (при задумывании с групповыми локами) предполагались какие-то и локи многих сразу, и повторы, а теперь -- осталась простая асинхронная модель).

    01.05.2020: некоторые размышления на будущее: нам ведь понадобится иметь возможность запроса информации для отладки/разбирательства -- например, кто залочил такой-то канал? Так вот:

    • Как выяснять локера? Вот тут и помощет chan_ioctl() -- переправлять запрос серверу и получать оттуда ответ АСИНХРОННО.

      А уж при переходе на модель "через handle" реализовать это станет крайне просто.

      И подобных запросов в протокол можно насовать сколько угодно -- точнее, один запрос "IOCTL", в котором куча кодов-подзапросов.

    • А как из командной строки такое получать? Лишнюю утилиту городить -- кривовато.

      Можно добавить функционал прямо в cdaclient:

      • Запрашивать ключом -QОПЕРАЦИЯ:КАНАЛ -- так можно будет запрашивать толпу каналов сразу, синтаксисом -QОПЕРАЦИЯ:КАНАЛ_С_{ПЕРЕ,ЧИС,ЛЕН,ИЕМ} c расширение хоть средствами shell'а, хоть console_cda_util'а.
      • А как обрабатывать -- да аналогично num2read и num2write, ввести 3-й счётчик -- num2ioctl (плюс всю сопутствующую "инфраструктуру", с ожиданием какой-нибудь готовности канала, чтоб можно было вызвать "cda_ioctl_chan()".
    • Одно замечание: надо будет в access.log выдавать также реальный ID клиента, а не только его 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: есть всё-таки некоторое неудобство/недоработанность в том, что запрошенная клиентом блокировка НЕ является "настойчивой" и не пытается при возможности (отпускании предыдущим локером) установиться сама, на стороне сервера, а требуется КЛИЕНТУ периодически долбиться.

    Сегодня во время очередной переписки с ЕманоФедей появилась мысль, как сие можно исправить.

    Будем обсуждать в отдельном подразделе.

    01.09.2020: собственно та переписка -- мой ответ на Федино письмо:

    On Mon, 31 Aug 2020, Fedor Emanov wrote:
    
    > Пересобрал, проверил, теперь если до запуска основного цикла запросить
    > блокировку, то она срабатывает.
    > Далее запускаю второй экземпляр программы, он разумеется так же до
    > старта основного цикла запрашивает
    > блокировку. Теперь убиваем первый экземпляр. Казалось бы второй уже
    > запросил блокировку и должен бы её
    > наконец-то получить? но в событиях мне этого не приходит. Это опять
    > баг? просто у тебя в описании сказано
    
            Это дизайн такой.
    
    > "запросил блокировку.... дождался сообщения о том что она получена", а
    > тут выходит что мои запросы выкидываются
    
            Они не выкидываются, а исполняются лишь в момент собственно
    запроса либо при коннекте.  А вот постоянных повторов -- НЕ делается,
    поскольку совершенно неясно, по какому принципу их делать (периодиечски --
    плохо, а события "владелец блокировки её снял" на уровне сервера нету, да и
    непонятно, КОМУ их отправлять).
    
    > и я периодически должен запрашивать заново?
    
            Да, периодически спрашивать заново.
    

    Собственно, из написанного -- второго моего абзаца, самого большого -- можно "от противного" вывести проект реализации.

    • Главная идея -- ну так ввести в cxsd_hw событие "смена состояния залоченности канала", и пусть cxsd_fe_cx это событие ловит и как раз и пытается в этот момент перехватить лок.
    • Добавить рассылку события в 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_fe_cx.c и cda_d_insrv.c.
    • @~10:30, спускаясь по лестнице дома, идя к родителям: событие "захват/потеря лока" -- крайне редкое, так что тут на производительность можно внимания не обращать (да и количество мониторящих конкретный ОДИН канал обычно всего несколько штук).
    • @~10:30, спускаясь по лестнице дома, идя к родителям: с точки зрения того, КТО выполняет захват блокировки после отпускания её предыдущим владельцем: хорошо ли возлагать это на "клиентов" cxsd_hw?

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

      Тут плюс будет в том, что обычно-то "достойным" можно считать первого в списке (т.е., просто следующего за прошлым локером), но можно и ввести некие "приоритеты".

      @через час, вернувшись домой: yу и дополнительный бонус -- как-то можно будет учитывать lockset'ы и "ALLORNOTHING", которые сейчас вообще никак. А всё потому, что при перекладывании работы с "клиентов" на cxsd_hw у него можно будет иметь более полную картину (в отличие от клиентов, у которых в моменте evproc'а знания ограничены одним этим каналом).

    • Только есть одна тонкость (точнее -- ТОЛСТОСТЬ): большинству-то каналов событие LOCKSTAT нафиг не нужно -- оно требуется только тем, у кого запрошен локинг, а после отмены клиентом запроса оно опять становится не нужно.

      Получается -- потребуется API для смены маски в уже зарегистрированном evproc'е (чтобыменять наличие битика LOCKSTAT в маске событий по мере надобности)? Ну-у-у, сделать то это можно -- технически проблема невеликая. Но как-то мне это не нравится...

      @~10:40, пешком к родителям, Коптюга после выхода из леса: но тут будет проблема с самим поиском мониторов в CxsdHwAddChanEvproc()/CxsdHwDelChanEvproc(): ведь они ищут пункты с ТОЧНО ТАКОЙ ЖЕ evmask, как указана. А при "модификации по мере надобности битика в маске запрошенных событий" сам cxsd_fe_cx.c будет путаться в том, что он должен указывать. (@через час, уже опять дома: можно, конечно, учитывать значение бита MONMODE_RQ_LOCK, но это всё равно хренововато...)

      @~11:00, пешком от родителей домой, вдоль Терешковой после "девятиэтажки": а может -- ну его нафиг, это дрыганье-битика-на-ходу? Просто ВСЕГДА заказывать событие LOCKSTAT, а уж в обработчике смотреть, нужно ли оно нам: если в мониторе битик MONMODE_RQ_LOCK взведён, то пытаться перехватить лок, а если нет -- то ничего не делать. Тем более, что «событие "захват/потеря лока" -- крайне редкое, так что тут на производительность можно внимания не обращать».

    • @~11:20, пешком вверх по лестнице в подьезде дома: есть один косяк с этой всей придумкой: она напрочь ломает концепцию -- так и НЕ реализованную -- "ALLORNOTHING".

      С придуманным подходом -- попытка захватить локи прямо в evproc'ах по отпусканию -- ничего подобного не получится, поскольку в момент evproc'а не будет никакой информации об АТОМАРНОЙ операции "блокировка ГРУППЫ каналов".

    В принципе -- вполне понятно, как делать; но вот ДЕЛАТЬ ли -- некий вопрос. Лучше взять паузу, пусть это всё в голове получше устаканится -- глядишь, ещё что придумается.

"Цвета" каналов и фазы:
  • 30.09.2014: предыдущее обсуждение было в разделе "временнАя схема работы", относительно ТНК за 08-05-2009.
  • 18.10.2015@ICALEPCS-2015, Melbourne, CS CyberSecurity, Masetti, "Centralized configuration of Role-Based dquote> authentication in JCOP Framework"А что, если скомбинировать с правами доступа, чтоб даже "запрещённо-покрашенные каналы" можно было менять НЕКОТОРЫМ юзерам/ролям?

    Ох, чревато это -- хрен отладишь.

    И вообще, надо иметь 2 типа запрета изменений:

    1. Откладываемые (до разрешения -- в next_wr).
    2. Отбрасываемые -- запись игнорируется полностью.

    18.03.2016: а ведь "цвета"+фазы должны бы, видимо, как-то взаимодействовать с "access control". Как?...

[Авто]сохранение и загрузка значений/режимов:
  • 30.09.2014: это обсуждалось чёрт-те сколько раз, "образец" -- автоматически сохраняемые уставки в TANGO, плюс какое-то подобие механизма есть в EPICS.

    Конкретно см. bigfile-0001.html за 09-02-2011 и 08-02-2014.

  • 30.09.2014: мысли/конкретика.

    30.09.2014: мыслей, собственно, пока 2:

    • Вопрос "в каком порядке записывать значения в каналы, принадлежащие одному устройству" (иногда это бывает актуально) имеет, похоже, один очевидный ответ -- у каждого канала сделать "приоритет" (просто число), указываемый в devtype.
    • Устройства НЕ СРАЗУ могут быть активны, так что сразу писать нельзя. Тут, возможно, может быть использован next_wr_val.

    17.04.2015@Снежинка-утро-зарядка: (мысли, навеянные вчерашними размышлениями о clientside-режимах, в частности, вопросом, в каком порядке что записывать):

    • "как загружать/записывать" -- маркировать все каналы полем "stage" (аналогично passno в fstab), и каждый драйвер сообщает свой текущий stage, после чего производится запись в соответствующие каналы.
    • @после-душа: либо маркировать каналы как зависимые, и производить запись в зависящие после подтверждения отработки записи в зависимости. (1. Да, строить деревья. 2. Да, вешаться на evproc'ы каналов; или в ReturnDataSet() проверять, рядом с TryWrNext()?)

    16.01.2016@утро-душ: пара мыслей:

    1. "Автоматическое" сохранение значений с нынешней инфраструктурой evproc'ев делалось бы просто -- вешаемся, и в момент прихода события пишем. Но не нужно, ибо незачем для snapshot'ов.
    2. Формат же файлов надо делать совпадающим с clientside -- т.е., "dataset": текстовый, с набором строк вида (space-separated)
      УСТРОЙСТВО.КАНАЛ ЗНАЧЕНИЕ TIMESTAMP RFLAGS
      (последняя пара полей -- чисто для информации, ровно как и в cdaclient/dataset-save).

    17.01.2016: две тонкости:

    1. Кто/как будет заниматься сохранением/загрузкой?

      Правильнее это делать extension'ом.

    2. А где этот extension будет брать имя для восстановления режима при старте?

      Видимо, надо ввести в config-файле возможность указывать ext'у параметры -- строку, как auxinfo для драйверов.

    15.06.2020@пробежка по квартире, ~14:20: некоторые идеи на тему автосохранения -- КОГДА его выполнять.

    • Очень желательно бы выполнять автосохранение в момент завершения сервера.

      Но проблема в том, что завершение сервера выполняется по СИГНАЛУ -- т.е., "асинхронно", и уж из обработчика сигнала точно НЕЛЬЗЯ обращаться к данным, т.к. они могут быть в произвольном "некогерентном" состоянии.

    • Так вот, идея:
      • В обработчике SIGTERM не выполнять выход, а лишь взводить флажок "завершаемся!" и вызывать sl_break().
      • А непосредственно выход с сохранением выполнять в точке ПОСЛЕ sl_main_loop()'а.
      • Для защиты от зависаний -- мало ли, где какой драйвер сдуреет -- нужно сделать, что если обработчик SIGTERM видит, что флажок "завершаемся!" УЖЕ взведён, то тогда выполняется просто выход.
    • Ну и в качестве защиты при шибко долгом сохранении: если основной цикл корректно завершился, а сам процесс сохранения немгновенный, чтобы второй сигнал не вызвал несвоевременного прерывания с выходом: в момент начала сохранения нужно взводить ещё один флажок -- "да, основной цикл завершён, сохраняем!", и тогда чтобы обработчик SIGTERM игнорировал бы уже взведённость флага "завершаемся!".

      Но, на случай, если именно сохранение зависнет -- нужно, чтобы ТРЕТИЙ сигнал завершения прерывал бы и сохранение тоже.

      ...а можно просто при начале сохранения сбрасывать флаг "завершаемся!", и тогда всё получится само собой: второй сигнал взведёт флаг обратно, а третий уже приведёт к завершению (поскольку флаг взведён).

О базовых и настроечных каналах:
  • 30.09.2014: подраздельчик создан сегодня, перемещением сюда содержимого раздела более высокого уровня.
  • 08.09.2014: пора начать продумывать то, как реализовать работу атомарных запросов -- например, "померяй такой-то канал при таких-то уставках" (fastadc и подобные) либо "запиши такой-то канал при таких-то уставках" (табличные каналы в козачиных ЦАПах).

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

    08.09.2014: краткая история вопроса:

    • Первоначально предполагалось, что
      1. Некоторым каналам -- "настроечным" будет в конфигурации указываться -- хозяин -- "базовый".
      2. У драйверов будет либо специальный метод -- "do_par_rw", либо в дополнение DRVA_READ и DRVA_WRITE добавится еще пара кодов операций (DRVA_PZREAD и DRVA_PZWRITE).

        При неподдержке драйвером эмулировать -- производить запись настроечных каналов, а потом чтение (или тоже запись) базовых.

      3. В протоколе будет специальный вид запроса.
    • В любом случае открытым оставался вопрос, а как же понять, что драйвер дал ответ на тот параметризованный запрос (да и при каких условиях вообще этот запрос можно слать). Некоторые соображения по теме есть за 18-06-2013.
    • Но вылезла проблемка: в схеме "базовый/настроечный" настроечный может иметь одного хозяина, а по смыслу задачи возможны более хитрые конфигурации -- один настроечный для НЕСКОЛЬКИХ базовых (например, у многоканальных fastadc в дополнение к общему массиву будут каналы-представления, по штучке на каждый вход).

      Поэтому назрела мысль вообще отказаться от указания отношений базовый/настроечный в КОНФИГУРАЦИИ, а оставить их только в протоколе (в широком смысле -- на всех этапах путешествия запроса между клиентом и драйвером), см. за 12-04-2014.

    • Дополнительная пара взаимопротиворечащих мыслей:
      1. Вдруг понадобится указывать в запросе несколько БАЗОВЫХ каналов -- "померяй то-то и то-то при таких-то уставках".
      2. С другой стороны, не указывать ли каналам ПРЕДСТАВЛЕНИЯ, от какого канала они образуются?

        Тогда

        1. Сервер сам поймёт, что при указании канала-представления реально запросить "исходный" канал (это, кстати, снимет проблему путаницы -- чтоб разные каналы-отражения не мешались друг дружке).
        2. Можно оставить понятие "настроечный" в конфигурации -- поскольку множественность базовых исчезает.

        Эта схема выглядит красивой, но надо понимать, что она подходит именно для текущих аппаратных задач, вроде fastadc. В каком-то более хитром случае может потребоваться более общее решение, вроде упомянутого в (1).

    Вот такова ситуация на текущий момент. И надо придумывать идеальное решение.

    17.09.2014: собственно, наиболее правильным выглядит решение (2) -- каналам представления указывать их реальный источник, а настроечным -- его же как базовый.

    Теоретическое возражение: а что делать, если настроечный канал реально настроечный для МНОГИХ АППАРАТНЫХ?

    Ответ: оное у нас встречается (пока?) только в АЦП, и тут два случая:

    1. CAMAC: c0609/c0642: тут надо на каждый АЦП'шный канал создавать по отдельной паре настроечных T и D (при старте драйвера все их заполняя умолчательными значениями).

      Проблема "один настроечный для многих" исчезает.

    2. CAN: козачиные АЦП всё равно не позволяют оперативно менять TIMECODE и GAIN, так что эта парочка живёт как отдельные каналы, слабо касающиеся остальных.

      Отдельный случай -- осциллографические измерения. Но там вполне можно поступить как в предыдущем пункте -- делать раздельные настроечные (хотя хбз... чисто идеологически неясно, как вообще лучше обращаться с этим режимом у этих АЦП....).

    28.01.2015: больше часа проговорили с Роговским на тему "как вообще идеологически организовывать работу с «большими каналами» -- для решения НЫНЕШНИХ задач, текуще в v2 и перспективно в v4". Результаты обсуждения, применимые к v4:

    1. Разделить параметры УПРАВЛЯЮЩИЕ (которые крутит юзер в любой момент) и ОПИСЫВАЮЩИЕ ТЕКУЩЕЕ измерение (например, сколько в нём точек). И чтоб "текуще-описывающие" были readonly.

      Т.е., в терминах v2'шного pzframe управляющие параметры связаны с nxt_args[], а описывающие-текущее с cur_args[].

      Это как бы немножко "некрасиво" -- смахивает на гусино-epics'ное разделение на {value,readback_value}, но в данном случае технологически необходимо, поскольку "readback_value" в используется не само по себе, а совместно с другим полем (массив), и должно быть с ним жестко скоординировано.

    2. Пытаться делать унифицированную карту каналов для всех быстрых АЦП -- бесполезно. Поскольку придётся унифицировать 1-каналыные АЦП (101-й) с 16-канальными (есть у фрицев), то получится какой-то дикий overhead.

      А главное -- такая унификация БЕССМЫСЛЕННА: ведь в v4 адресация по именам и есть резолвинг, так что незачем иметь в 1-канальном АЦП имена вроде "line3data", отдающие CXRF_UNSUPPORTED -- достаточно просто не иметь таких имён.

      Вывод для "бихевиористских" параметров, унифицированных у всех pzframe-подобных устройств, и необходимых для v4'шного варианта pzframe -- SHOT, ISTART, WAITTIME, STOP, ELAPSED: их делать

      1. либо так же, как в v2 (раздельным набором, с указанием номера каждого в pzframe_drv_t и -1 при отсутствии);
      2. либо фиксированным блоком, с указанием его смещения от 0.

      Первый вариант мне нравится больше (и v2'шный код подойдёт один-в-один).

    3. Нынешняя v2'шная модель, заточенная в первую очередь под гарантирование атомарности измерений, была сделана под влиянием соображений от Эйдельмана и Карнаева. Но:
      1. Вживую таких задач так и не встретилось.
      2. Сама идея, что некий канал будет постоянно разными программами запрашиваться с разными параметрами -- несколько бредова: ведь канал-то ОДИН, и источник сигнала, обмеряемый им, тоже один; следовательно, требуемые параметры обмера также должны быть одни (определяемые смыслом сигнала).

        ...а в тех случаях, когда физически требуется мерять один сигнал разными способами (например, 220V -- отдельно смотреть синусоиду и пульсации), то надо ставить разветвитель и ДВА измерителя.

      Следовательно, надо б еще раз поговорить с Карнаевым для прояснения ситуации.

    29.01.2015: собственно, а что не так с моделью в v2, что для v4 хочется сделать другую модель:

    • Во-первых, модель должна быть такова, чтобы всё получалось удобно и "само собой", не требуя от клиентов (да и драйверов тоже) хитрого кода, направленного на обход и преодоление заложенных в модели неудобств и ограничений.
    • Главное неудобство в v2 -- невозможность крутить параметры независимо и индивидуально: ни независимо от собственно запроса большого канала, ни более дальние (с бОльшими номерами) не затрагивая ближние (с меньшими номерами).

      Сия проблема в pzframe-девайсах обходится aliasing'ом параметров на скалярные каналы, но уже это само является извращением и реализуется как раз в клиентах и драйверах, вместо сервера.

    • Отсутствие возможности делать по-линейные (по-канальные) вырезки из общего массива данных в осциллографах -- чтоб программа могла получать сразу осциллограмму только с конкретного входа, а не заниматься вытаскиванием этих данных из общего массива.

      Эта проблема идеологическая, и определяется, во-первых, неудобной схемой работы с параметрами (см. предыдущий пункт), а во-вторых, ориентированностью на "отдельность" большого канала -- он существует как бы сам, изолированно, отдельно от прочих больших каналов (это даже в протокол пролезло -- 1 bigc на соединение); а вырезки потребовали бы общих параметров для нескольких связанных bigc.

    30.01.2015@утро: следствие: при такой модели общая "libpzframe_drv" должна будет совершенно иначе обращаться с параметрами. Ибо:

    • Параметров будет не-фиксированное количество (уж точно без верхнего ограничения в 100 штук).
    • Параметры желательно допускать разного типа (а не только int32).
    • Параметры относятся одновременно к НЕСКОЛЬКИМ большим каналам (линии, представления, ...).
    • ...да и каналов-то, действительно, больше одного.

    Следовательно -- и обработка параметров в "pzframe_drv_rw_p()" становится нетривиальной, и копирование nxt->cur; да и возврат данных в "ReturnMeasurements()" тоже (оное, похоже, вообще надо сделать pzframe_drv-методом драйвера).

    03.02.2015: потихоньку запиливаем, на примере adc200me_drv.c плюс cpci_fastadc_common.h (почти пустой) и пока локального pzframe_drv.c. Результаты и размышления на текущий момент:

    • Работа с параметрами из pzframe_drv вообще убрана.
      • Оставленная в нём pzframe_drv_rw_p() обрабатывает лишь стандартные бихевиористские параметры (причём значения -- у ISTART и WAITTIME) складирует в свои переменные, а не в массив параметров.
      • У драйвера же есть своя собственная _rw_p(), которая
        1. Понимает запросы на базовые каналы -- выставляет у себя флаги запрошенности, после чего должна дёрнуть pzframe_drv_req_mes() (стартующий измерения).

          Этими флагами достигается адаптивность -- наверх венрутся только те базовые/отражения/вырезки, что были запрошены.

        2. Обрабатывает СВОИ параметры (вроде того же SERIAL и переключения таймингов/диапазонов/...).
        3. Вызывает pzframe_drv_rw_p() для всего остального.
    • ReturnMeasurements() пока вытащена тоже прямо в драйвер -- для реализации там:
      1. всей той алхимии с возвратом текуще-описывающих каналов (скопированно из управляющих, использовавшихся при измерении);
      2. возврата только запрошенных базовых/отражений.

        Это она делает "интеллектуально", составляя списки для ReturnDataSet()'а, при надобности подсовывая в начало сами базовые, а потом безусловно параметры по списку.

    • И уже сейчас видно, что
      1. "Алхимия" должна выполняться прямо в ReadMeasurements(), подготавливая все данные в готовом для Return* виде.
      2. Драйвер должен предоставлять для pzframe_drv некоторое описание специфики своих каналов (для обеспечения правильного возврата наверх), содержащее:
        • Список параметров, подлежащих отдаче одновременно с базовыми -- номера, dtype, указатели на значения (лежащие в privrec'е).
        • ОПИСАНИЯ БАЗОВЫХ КАНАЛОВ -- где флаги запрошенности, расположения в памяти для возврата и т.д.
        • КУСКИ ПАМЯТИ ДЛЯ МАССИВОВ

        Да, монструозненько выглядит параметризация (уже похоже на pzframe_data)

      Сейчас отладимся локально, а потом формализуем это в pzframe_drv.

    26.02.2015: за вчера-сегодня очень предварительная версия adc200me_drv+pzframe_drv сваяна. Собирается, но работать пока не будет: и с параметрами работа недопилена, и архитектура ReadMeasurements() пока никак.

    27.02.2015@утро: а теперь допилена вроде должная работать версия.

    • Чтобы не заморачиваться с навороченными описаниями (см. нижний п.2 от 03-02-2015), сделано проще: конкретный драйвер предоставляет функцию, заполняющую массивы для ReturnDataSet()'а.

      Функция указывается init'у в параметре prepare_retbufs.

    • А сами буфера -- поле pzframe_drv_t.retbufs, имеющее специальный тип pzframe_retbufs_t.

    Надо проверять.

    03.03.2015: за позвачера-вчера-сегодня потроха adc200me_drv.c сделаны.

    • Первый вариант был "_rw_p() содержит кучу if()'ов", и получился не только громоздким, но и идеологически невнятным.
    • Поэтому был взят подход, чья предтеча внедрена энное время назад в panov_ubs_drv: есть таблица, индексируемая по номерам каналов, описывающая их "свойства".
    • В данном случае (пока) свойство -- это "тип" канала:
      1. UNSUPPORTED=0 -- отсутствует (=0, так что значение по умолчанию)
      2. BIGC -- векторный канал, запомнить спрошенность и дёрнуть pzframe_drv_req_mes().
      3. PZFRAME_STD -- передать pzframe_drv_rw_p().
      4. INDIVIDUAL -- реальное действие; но чтение -- из nxt_args[].
      5. VALIDATE -- запись пропустить через ValidateParam() и складировать в nxt_args[].
      6. STATUS -- отдать из cur_args[].
    • В принципе, выглядит достаточно параметризовабельным для возможности переноса этого всего -- кроме INDIVIDUAL -- в pzframe_drv.

      Но такая параметризация будет настолько громоздкой, что выглядит не стоящей выделки. Пока будем тестировать на adc200me, а в прочие драйверы просто копировать код -- благо, модификаций по "индивидуальности" там немного.

    • Плюс, есть некоторые идеологические неясности с тем, когда обращаться к cur_args[], а когда к nxt_args[]. Надо допроверять и дораспробовать.
    • P.S. И пока НЕ сделан подсчёт статистики, имеющийся в v2'шных CAMAC'овских драйверах. Вопрос не столько в самих подсчётах, сколько в идеологии -- ГДЕ, КОГДА и КАК его выполнять.

    На первый взгляд -- вроде работает.

    06.03.2015: имевшиеся баги пофиксены, и подход продвинут и расширен:

    • "Свойства" сделаны глобальными, и теперь в этой глобальной таблице содержится СТРУКТУРА из 2 полей:
      1. собственно тип (chtype) -- имеет тот же смысл, что раньше;
      2. "исходный канал" (refchn) -- чтобы при возврате в PrepareRetbufs() знать, с какого управляющего канала копировать значение CUR-канала (то, что раньше указывалось во внутренней PrepareRetbufs()'овской таблице).

        Это поле используется ТОЛЬКО для STATUS-каналов.

    • Добавлен еще один "тип" -- AUTOUPDATED. Так помечаются каналы, отдаваемые драйвером по собственной инициативе, обычно однократно; похожи на STATUS-каналы, но возвращаются не по окончанию измерения, а в некий иной момент. Примеры:
      • SERIAL -- возвращается один-единственный раз, прямо при старте драйвера, после вычитывания серийника из устройства.
      • Состояние и статистика калибровки -- возвращаются после завершения калибровки (а состояние -- также при сбросе калибровки).
    • Некоторые принципы, по которым каналам назначаются типы и как каналы отдаются:
      • При старте вычитываются из памяти устройства ТОЛЬКО параметры VALIDATE. Остальным делается =0.
      • Следствие -- ValidateParam() обязана обрабатывать только VALIDATE-параметры, остальные ей теперь никогда не попадут.
      • По окончанию измерения все STATUS-каналы ОБЯЗАНЫ быть вёрнуты (точнее, указаны в retbufs для возврата).
    • Какие действия выполняются "стандартной инфраструктурой", управляемой этой таблицей:
      • Для каналов STATUS и AUTOUPDATED делается SetChanReturnType(,,,1).
      • VALIDATE-каналы вычитываются из сохранённого в памяти устройства массива.
      • STATUS-каналы помещаются в retbufs в PrepareRetbufs(). Причём:
        1. При refchn>=0 значение будет взято из указанного канала.
        2. Иначе "индивидуальная часть" (в начале) этой функции должна заполнить значения.

        ...в обоих случаях делается копирование nxt_args[x]=cur_args[x] -- для полной корректности (чтобы даже при отсутствии флага is_autoupdated всё бы работало как надо; может пригодиться для удалённых драйверов, когда инициализация асинхронна и запросы могут быть уже "в полёте" еще до её завершения).

    10.03.2015: конкретно adc200me_drv.c вроде допилен, добавлены каналы:

    1. "Свойств" -- по-линейные абсолютные минимумы/максимумы (LINE[12]TOTAL{MIN,MAX}) и NUM_LINES (отдающий 2).

      Уставляются при инициализации.

    2. Вычисления статистики -- по-линейные MIN, MAX, AVG, INT (по образу и подобию nadc200).

      Считаются в ReadMeasurements(), нулятся в AbortMeasurements(), возвращаются (при запрошенности!) в PrepareRetbufs().

      ...тут, конечно, есть некоторая сложность с интегралами: в 12-битном осциллографе с 2^20 сэмплами сумма не влезла бы в 32 бита даже в исходных "сырых" кодах, а уж в микровольтах и подавно. Поэтому она считается в 64-битной переменной (12бит+10бит"uV"+20бит_адрес -- 42 бита, явно должно влезть). Хотя при сохранении в 32-битный канал INTn оно и обрежется...

    11.03.2015: замечание общего характера насчёт памяти и размеров:

    • Сейчас прямо в privrec'е отводится массив для считываемых данных размером "по максимуму".
    • Но в некоторых случаях оно может быть лишним.
    • Тогда можно будет извернуться: НЕ указывать в DriverModRec'е privrecsize, а парсить auxinfo руками, узнавать оттуда желаемый максимальный размер (точнее, NUMPTS), и аллокировать privrec руками.
    • Другое дело, что
      1. Это усложнение и онеочевидливание кода.
      2. Когда РЕАЛЬНО может понадобиться -- неясно: для cPCI-осциллографов обычно будет хватать ОЗУ Kontron'ов (что 512М CP6000, что 8G CP6003), а для CAMAC'овских должно быть достаточно 16M CM5307/PPC.

      Но если вдруг понадобится -- то алгоритм действий вот он тут.

    12.03.2015: сделан второй драйвер -- adc812me_drv.c, по образу и подобию 200-го, и, видимо, самый монструозный из всех (за вычетом 333-го с возможностью отключения линий).

    Резюме:

    • Да, дублированного между драйверами кода много. В конкретно ДАННОМ случае можно будет попробовать часть вытащить в cpci_fastadc_common.h, коль для pzframe_drv это кажется монструозноватым.
    • Но лучше подождать до CAMAC-драйверов, чтоб сразу учесть опыт/потребности того же 333-го.

    13.03.2015: мелкое дополнение: теперь pzframe_drv значение ELAPSED при НЕзапущенности измерений отдаёт -1.

    26.06.2015: pzframe_drv перетащена в основное дерево 4cx/, и в hw4cx/x-build/ она теперь тоже собирается.

    15.07.2015: продолжаем работы, теперь уже с CAMAC'овскими.

    • Портирован camac_fastadc_common.h -- в основном отсечением лишнего.
    • Сделан первый драйвер -- adc4_drv.c; он выбран за наименьший размер. Путём переделки v2'шного исходника добавлением кода от adc200me_drv.c.
    • Константы CHTYPE_* и тип chinfo_t перенесены в pzframe_drv.h под именами PZFRAME_CHTYPE_* и pzframe_chinfo_t соответственно. Ибо нефиг дублировать совсем идентичные определения.

    16.07.2015: вроде сделан (компилируется) следующий драйвер -- c061621_drv.c. А нафига, спрашивается?

    • Общее впечатление: проверить его сейчас всё равно не удастся, а времени и сил на эти монструозные работы уходит много.
    • Общее решение: пока забить/отложить (лучше vdev'ом заняться), а следующим делать драйвер adc200, поскольку он реально необходим и быстро проверяем.
    • ...и, кстати, надо будет этот драйвер делать с ADC200_DATUM_T=int32, а не экономить с uint8 (вызывающим море неудобств).

    02.06.2016: возобновляем работу над драйверами.

    • Сделаны _drv_i.h и .devtype для оставшейся троицы CAMAC'овских -- adc200, adc502, adc333. В интересах pzframe-клиентов.
    • Что занятно, карта каналов ADC502 полностью совпадает с оной от ADC200, только на месте 200'шного FRQDIV стоит TMODE, а каналы сдвига нуля называются не ZERO, а SHIFT (они не плавные, а на +-1/4 диапазона).

    10.06.2016: работа над собственно драйверами:

    • adc333_drv.c.
    • Глядя на почти совпадающие adc4_rw_p() и adc333_rw_p(), хочется всё-таки унести этот код в camac_fastadc_common.h. Только надо будет как-то параметризовать кусок на тему PZFRAME_CHTYPE_BIGC, чтобы:
      1. Отличало каналы "данные вообще" (CHAN_DATA) и по-линейные (CHAN_LINEn).
      2. Работало бы для случаев одно-канальных утройств, вроде c061621_drv.c.

      Похоже -- тупо ввести 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:

    • adc502_drv.c.
    • adc200_drv.c. Этот надо интенсивно и экстенсивно тестировать -- т.к. в нём, в отличие от v2'шного, выполняется пересчёт в микровольты, а оный у этого девайса довольно замудрён.

      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: немножко анализа:

    • Там же не так всё просто: поскольку умолчания в PSP-таблице будут проставляться -1, что не является приемлемыми значениями, то нужно что-то чуть хитрее -- как-то эти умолчания должны убираться.
    • В cPCI'ных это делается при помощи Init1Param(), уставляющей указанное ей в вызове значение, только если текущее в nxt_args[n]<0.

      Таким образом, для КАЖДОЙ строчки в PSP-таблице _params[] должна б быть строчка в коде, "заботящаяся" о замене -1 на надлежащее.

    • Но! Ведь реально достаточно прямо в _params[] и указать умолчательные значения, а более нигде в коде ничего присваивать и не нужно!

      Сначала была мысля, что в adc200me/adc812me оно так потому, что там параметры могут считываться из памяти устройства сохранённые. Но потом стало ясно, что это просто лёгкий изврат -- поскольку указываемые в auxinfo значения НЕ имеют приоритета перед сохранёнными, то такая хитрость и незачем. Почему именно так сделано -- хбз, а запись об "Init1Param" есть в 201403-SNEZHINSK-ACTIVITY.txt 31-03-2014.

    • @лыжи: есть ТРЕТИЙ тип pzframe-драйверов -- которые могут считывать текущие уставки из железа. И adc4x250 с f4226 -- именно такие.

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

      • Во-первых, потому, что в разных случаях могут быть разные приоритеты: в одном -- восстанавливать "последнее известное", а в другом -- гарантированно выставить "safe defaults".
      • Во-вторых, не всегда можно понять: считанные из железа -- это реально последние ВЫСТАВЛЕННЫЕ, или же это умолчательные значения после включения?

        Это второе иногда можно пытаться разрулить чтением из железа параметров, которые осмысленно не могут быть нулями -- например, число точек для измерения; но не всегда такие параметры есть (у Ф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: далее:

    • Остальные в camac/:
      • adc333_drv.c: осциллографовы были все, поведенческих -- только "calcstats".
      • adc4_drv.c: осциллографовы имелись все, кроме почему-то PTSOFS, поведенческих -- опять только "calcstats".
      • adc502_drv.c: вообще один-единственный "calcstats".
      • c061621_drv.c: был только "calcstats".

        Вообще тут всё несколько по-своему, т.к. драйвер для НЕСКОЛЬКИХ типов сразу. Поэтому сделана только четвёрка поведенческих параметров, а прочее нетронуто.

      Итого: во всю шоблу (с оговоркой о c061621) добавлены ВСЕ отсутствовавшие параметры, а ручная инициализация в InitParams() убрана.

    • Делаемый f4226_drv.c:
    • cpci/:
      • adc200me_drv.c: были все параметры осциллографа, а из поведенческих -- лишь "calcs"/"nocalcs" -- сделана вся четвёрка istart/noistart/calcstats/nocalcstats.
      • adc812me_drv.c: не было ни одного из 4, добавлены.
    • vme/: там всё просто, но пока не тратим время; как пойдёт в дело (или удастся допилить) -- тогда и параметры пришьём.
Свойства -- строки, коэффициенты, диапазоны:
  • 30.09.2014: подраздельчик об оснащении каналов и точек контроля:
    • Строками (ident, label, tip, comment, geoinfo, units, dpyfmt) -- 19-07-2014-...;
    • множественными коэффициентами пересчёта ({r,d}) -- 20-02-2013;
    • диапазонами [MIN MAX] -- 09-06-2014.

    Собственно, фокус в том, что {r,d} могут браться "от другого" канала (29-05-2014).

Диапазоны ограничения записи в сервере:
  • 24.11.2014: на тему о том, как ограничивать неким диапазоном записываемые значения.
  • 24.11.2014: размышления -- а надо ли оно, и если "да", то где и как?

    24.11.2014: набор соображений:

    • Мысль: а надо ли вообще давать СЕРВЕРУ такой функционал? Ведь:
      1. Аппаратные каналы: драйверы сами проверяют, и ограничивают значение разрешенными устройством рамками.

        ...так что сбагривать ДРАЙВЕРУ границы наверх серверу -- малоосмысленно, только что для передачи дальше наверх в клиента, чтоб юзера ограничивали на экране.

      2. Точки контроля: их границы отдаются (будут отдаваться) только наверх клиентам, вместе с номером аппаратного канала, а дальше никак ни в чём не участвуют.

      Так может -- вовсе освободить сервер от функции проверки на попадание в диапазон, оставив её на драйверах и клиентах?

    • С другой стороны, иногда вроде б как-то хочется мочь ЖЕЛЕЗНО ограничивать задаваемое значение -- чтобы, например, выше +5V в конкретный 10V-канал записать невозможно было в принципе.

      А это может делать только сервер, где-то в StoreForSending().

    • Отдельный вопрос -- векторные каналы? Насколько имеет смысл? Соображения:
      1. Скалярные ограничения применять -- в принципе, можно. Например, чтобы некий канал 10V-ЦАПа не превышал 5V.

        Но не лучше ли это как-нибудь свалить на драйвер?

        ...проще, конечно, свалить, но шибко уж это необщо. Тут-то EPICS'ные record-support'ы гибчее -- можно разным каналам назначать разный "функционал".

        А если ввести понятие "дополнительная обработка/support" (загружаемыми модулями!), маркировать некий канал таким образом, и тогда будет вызываться некий дополнительный функционал?

      2. Векторные ограничения -- хбз. Где, когда, зачем?
      3. Конкретно на NSLS-II ограничивались 1-я и 2-я производные waveform'ов -- чтобы источники не напрягались слишком резкими изменениями. Но это уже на уровне конкретного драйвера.

    25.11.2014@утро, пешком из ЦНМТ домой:

    • Ограничивать ведь надо одинаково скалярные каналы ЦАПа и их векторные представления, так?

      Ну и пусть тогда в драйверах будут доп.каналы "верхний лимит" и "нижний лимит", в который диапазон и вгонять значения ВСЕГДА.

      Для этого придётся карту каналов kozdev раздвинуть еще на 100 -- чтоб влезли 32*2=64 канала лимитов, и в резерве останется еще 40, а общее число станет удобным 500.

    25.11.2014@вечер: насчёт общих ограничений есть еще один вариант: сменить концепцию

    "при резолвинге точки контроля отдавать chanid реального аппаратного канала"
    на
    "разрешены запросы записи/чтения непосредственно в точки контроля".

    Обсуждение:

    • Ограничение/недостаток: обращение-то к точкам контроля будет разрешено, но evproc'ы на них навесить не удастся, поскольку данные приходят из драйвера не в них, а в реальные аппаратные каналы.

      Но это в принципе можно отразить в frontend'ах/cda_d_insrv.

    • Внутренности/мозги Consider/Store/Send останутся теми же, но перевод "globalchan->dev_chan" делать не вычитанием dev_p->first, а ВСЕГДА иметь в cxsd_hw_chan_t "devchan" -- в таком случае и с аппаратными каналами, и с точками контроля обращение будет одинаковое.
    • Главный вопрос -- как именно при записи производить "ограничивание": ведь возможна ситуация, когда у точки контроля диапазон будет шире, чем у подстилающего аппаратного канала. И вообще диапазонов может быть ЦЕПОЧКА -- при ссылке на ссылку.

      Бежать при каждой записи по всей цепочке -- плохой вариант.

      Хотя в EPICS'е "многоуровневые иерархии PV" именно так и работают (там это именно полноценные иерархии, где каждая PV самостоятельна, просто "смотрит" на другую, и callback'и на неё навешивать можно).

      Собирать "самый узкий диапазон" по цепочке? Ох, муторно...

    09.12.2015: в продолжение темы -- начинают надобиться ограничения.

    Сначала некоторые комментарии о текущей ситуации:

    • По факту сейчас запись приходит именно на точку контроля -- и вообще всё взаимодействие клиент/сервер (cda_d_cx+cxlib<->cxsd_fe_cx) идет в идентификаторах точек контроля (cpid), а идентификаторы аппаратных каналов (hwid) игнорируются. За это отвечает реакция на CXC_CVT_TO_RPY(CXC_RESOLVE) в async_CXT4_DATA_IO():
      rsi.hwid = prps->hwid*0 + prps->cpid*1;
    • Парсинг свойств cpoint'ов сделан, никаких лимитов там нет, но ввести их несложно -- поскольку парсинг {r,d} есть в общей форме (в ParseCpointProps() посредством cptfielddescr_t), только с phys_rd_specified микрохалтура (но это несложно исправить).

    О хотелках:

    • Ограничения хочется именно железные, на стороне сервера -- чтоб в магниты ненароком фатальные токи не записали.
    • А СЕЙЧАС ограничение не просто в клиенте, а еще и на уровне datatree -- вписывается в параметры ручки.
    • В результате ничто не мешает записать -- например, через cdaclient -- любое число.

    Теперь идеи "что можно сделать":

    • Поскольку запись идёт по адресу cpoint'а, то в момент "раскрутки" cpid->hwid можно и лимиты применить. ...или заиметь?
    • Но есть и неприятности:
      1. Перво-наперво -- что "раскрутку" выполняет cxsd_fe_cx.c, так что данное ограничение будет только при доступе через протокол CX, а все прочие (insrv, будущий EPICS) -- неа.

        Но с этим можно и смириться -- для достижения ТЕКУЩИХ целей.

      2. Второе неудобство -- т.к. "раскрутчик" cpid2gcid() используется еще в куче точек, то начинять его мозгами на тему диапазонов будет нехорошо. Придётся сделать отдельную функцию, копирующую код раскрутки, но с дополнительным функционалом обработки диапазонов.
      3. Собственно обработка диапазонов ("вгоняние"): ведь на каждом уровне cpoint'а есть СВОИ {r,d}, и как при "опускании до аппаратного канала" с этим обходиться ({r,d}-преобразовывать лимиты "с верхних уровней"?) -- неясно.
      4. ...а еще и типы могут быть разными. Лимиты же всегда в double. Тоже та еще радость с выполнением преобразований.

    Короче -- всё это выглядит пугающе стремновато, так что пока забиваем (но проект вот он тут есть).

    P.S. Да, вот в этот момент архитектура "client performs {r,d} conversion" начинает выглядет кривовато, по сравнению с "всё делается в одних единицах/типах, а конверсия и вгоняние выполняется сервером на границе с драйвером".

  • 28.08.2018: родственный аспект -- ЕманоФедя просит, чтоб можно было на верхнем уровне узнавать "аппаратные" диапазоны каналов записи (ему это для визуального билдера интерфейсов надо).

    В принципе, сделать-то несложно: добавить цепочку передачи, аналогичную передаче кванта, только чтоб передавалась ПАРА значений.

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

        • Каналы указываются группой -- first,count.
        • Значения -- поштучно, minv и maxv. Вроде так должно быть удобнее для всяких cac208_drv.
        • Содержимое почти целиком скопировано с SetChanQuant() -- там различие лишь в складировании переданного.
      • В cxsd_hw_chan_t добавлены поля range[2] и range_dtype.
      • Код события -- CXSD_HW_CHAN_R_RANGECHG=6.
    • Ветка сервер->клиенты (cxsd_fe_cx->cxlib->...):
      • cx_proto_v4.h: новый chunk-code CXC_RANGE='Prn'; структурка -- CxV4RangeChunk.
      • cxsd_fe_cx.c::PutRangeChunkReply(): сделана на основе PutQuantChunkReply().
      • Также добавлены вызовы её в тех же местах, и к MONITOR_EVMASK добавлен битик RANGECHG.
      • cxlib_client.c: 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-плагинов -- 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.
      • cda_d_cx.c -- всё просто, плюс альтернатива в ProcessCxlibEvent().
    • cdaclient:
      • Добавлен ключик -DM (Min/Max).
      • Печать -- при неуказанности "=UNSPECIFIED", а так в виде "=[MIN,MAX]".

        Сама печать взята из выдачи кванта, так что перед обоими компонентами печатается префикс "@T:".

    30.08.2018: далее.

    • Ветка remcxsd->remdrv:
      • Код -- REMDRV_C_RANGE="Rnge", структурка -- remdrv_data_set_range_t.
      • remcxsd_driver_v4.c::SetChanRange() традиционно слизана с соседней SetChanQuant().
      • remdrv_drv.c::ProcessInData() -- обработка традиционно скопирована с соседней от QUANT.
    • Драйверы:
      • 10.12.2019: ни-фи-га тогда не было сделано -- вызовов SetChanRange() не встречалось нигде, кроме remdrv.c.

    06.12.2019: ЕманоФедя недавно опять попросил диапазоны, так что в качестве первого шага добавлена отдача диапазонов в v5k5045_drv.c.

    • Возвращается как из init_d(), так и при смене (через запись канала NOMINAL).
    • ...для чего собственно возврат вытащен в функцию ReturnHVSetRange().

    Теперь надо проверять.

    09.12.2019: пытаемся проверить. Фиг -- не работает.

    • Первая стадия -- ЕманоФедя перепустил клистронный сервер cxhw:25, и "всё сломалось!!1" (на скрине v5kls всё стало болотным).
      • Я думал было, что проблема может быть в драйвере (и даже стал продумывать процедуру тестов.

        И драйвер вернул на место старый.

      • ...но ничего не исправилось -- всё осталось болотным!
      • Оказалось, что проблема была совсем в другом месте -- у Феди+Славы каким-то образом умудрились загрузить в DNS старый конфиг, где контроллеры имели адреса 192.168.131.4n, хотя они ещё в сентябре были переведены на 192.168.130.4n.

        Вот и вышло, что контроллеры имеют адреса в 130-й, а remdrv пытается коннектиться к 131-й -- с предсказуемо отрицательным результатом.

      • После обнаружения и исправления проблемы был вёрнут на место драйвер с отдачей диапазонов.
    • Но дальше оказалось, что до клиентов никакие диапазоны не доходят -- проверено при помощи cdaclient.

    10.12.2019: разбираемся.

    • Напихиванием отладочной печати причина найдена:
      • В отличие от всех прочих Chunk'ов, конкретно CxV4RangeChunk имеет размер, НЕ кратный 16 -- всего лишь 56.
      • А при отправке в сеть в cxsd_fe_cx -- точнее, в PutRangeChunkReply() -- делается округление размера вверх до кратного 16, т.е., до 64.
      • ...но в async_CXT4_DATA_IO() конкретно в ветке по CXC_RANGE делается проверка, что размер принятого chunk'а должен быть РАВЕН ожидаемому, а если нет -- то отваливаем и ничего не делаем.
    • Раз уж так было накосячено с размером CxV4RangeChunk (не добавлена пара int32 для заполнения), то остаётся только проверку ослабить -- отваливать не при неравенстве, а если принятый объём МЕНЬШЕ ожидаемого.
    • Сделано -- ситуация исправилась, теперь до cdaclient'а диапазоны доходят и он их даже печатает.
    • Также в конец 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'ова фича "диапазон каналов":

    1. Смотрим при запуске сервера в консоли, сколько у него каналов -- чтобы узнать номер последнего. Конкретно у cxnhw:11 -- 16122 канала.
    2. Вычитываем данные по ВСЕМ каналам, указывая их диапазоном "<0-16121>" --
      cdaclient -DM -T10 localhost:12.'<0-16121>' 2>/tmp/Elog >/tmp/Mlog

      Смысл:

      • "-T10" -- чтобы чеерез некоторое разумное время утилита завершилась, а то на кучу каналов ответа может так и не придти.
      • В Elog попадут ошибки -- в т.ч. о несуществующем канале номер 0 и в результате от вычитывания текстовых каналов (типа _devstate_description) как чисел.
      • А вот в Mlog -- уже полезные результаты, по которым можно grep'ить.
    3. "Здоровые" каналы, без диапазона, имеют значение UNSPECIFIED (таковых при косяке не было вовсе :D), а у прочих наблюдались разнообразные значения вроде "@i:nnn" и даже "@t:???" (это как раз из-за "наследования" dtype'ов от квантов, которые форсятся в каналовы).

    25.03.2021: проверил -- да, по крайней мере на "тестовом" devlist-canhw-11.lst косяка более не видно.

  • 09.04.2021: немного о практическом применении -- об отдаче диапазонов из драйверов.

    Итак:

    • Вот сегодня была сделала отдача диапазонов из драйверов CAN-ЦАПов. Но сделана она была сразу БЛОКАМИ -- для всех каналов ЦАПа уставляется диапазон [MIN_ALWD_VAL,MAX_ALWD_VAL].

      (Да, код во всех 4 драйверах одинаковый -- 3 идентичные строчки (для OUT, OUT_CUR и OUT_IMM), поскольку есть сделанная ради advdac'а унификация имён.

    • Но это некорректно, т.к. в auxinfo могут быть назначены индивидуальные минимумы/максимумы для каждого канала,
    • А просто циклом поштучно для каждого -- стрёмно: хочется ведь рптимизировать отсылку для дохленьких контроллеров...

    Уродство... Как поступить-то?

    11.04.2021: @мытьё-посуды, 13:40: ну сделать код, идущий по массиву и вычленяющий последовательности одинаковых. В >99% случаев и будет одним сегментом.

    Получасом позже: ну сделал (для примера в candac16_drv.c). Монструознейше!

    Ещё 20 минутами позже: а может, свалить эту работу на advdac?

    12.04.2021@утро, ~7:30: проверяем -- запускаем сервер с 1 драйвером CANDAC16 (устройства он не найдёт, но диапазоны отдаст) с указанными в auxinfo несколькими разрозненными min/max.

    Чё-то фигня какая-то...

    • Вариант "отдаём диапазоны одним блоком" -- работает, конечно.
    • А вот варианты "циклом поштучно для каждого" и "с нахождением последовательностей одинаковых" -- для указанных min/max дают их, а для остальных -- [0,0].

    Разобрался:

    • Причина в том, что по умолчанию в out[*].{min,max} записываются 0, а не MIN_ALWD_VAL,MAX_ALWD_VAL.
    • И что делать? Очевидное решение -- изменить умолчания. Но как-то стрёмно...

    Вот вроде смешная проблема, а красивого решения в голову не приходит, блин...

    14.04.2021: а может -- правда вытащить этот монструозный код в advdac, и там уж развлекаться на полную (прочее содержимое advdac_slowmo_kinetic_meat.h тоже не особо кратко).

    Это "на полную" предполагает отсутствие всяких "оптимизаций"/сокращений с адресацией к out[first].{min,max}, а:

    • "Держание" последних известных min/max в отдельных переменных.
    • И "вычитывание" min/max от [l]-го канала в другую пару отдельных переменных, которые и сравниваются с предыдущими.
    • Причём "вычитывание" заключается в том, что если min>=max (т.е., диапазон не указан), то явным образом берутся MIN_ALWD_VAL,MAX_ALWD_VAL.

    Чуть позже -- да, сделано! Работает. И получившаяся ReportOUT_Ranges() уже переселена в advdac_slowmo_kinetic_meat.h.

"Библиотечная" структура реализации разных классов каналов вместо типов record'ов:
  • 01.03.2015@лыжи: (реально несколькими днями (а первые мысли -- месяцами) раньше) у нас радикально отличается подход к реализации разных КЛАССОВ каналов/устройств.
    • EPICS: каждый "класс" -- свой тип record'а.
    • CXv4: ВСЕ каналы на уровне API совершенно одинаковы, только readonly/readwrite.

      А специфика реализации -- в используемых конкретным драйвером библиотеках, которые никак не навязываются сервером, а драйвер может их комбинировать любым удобным ему способом.

      • Самый крупный на сейчас пример -- pzframe_drv: оно имеет как бы сильно отличающуюся семантику, но это личное дело самого драйвера.
      • И всякие sendqlib'ы драйверы могут использовать по своему усмотрению, комбнируя с прочими библиотеками.

    Т.е., в EPICS -- стиль "классического ООП с типизацией по классам и наследованием", а в CX -- "подмешивание" (mixin).

  • 01.03.2015@лыжи: (опять же реально несколькими днями раньше, но на той же 5-ке) сейчас библиотеки (libraries -- в смысле модули, загружаемые сервером с CXLDR_FLAG_GLOBAL/RTLD_GROBAL) сделаны "явочным порядком", "по договоренности".
    • А если как-нибудь в описании драйвера указывать, что ему нужны, кроме layer'а, еще такие-то библиотеки?
    • Либо вообще концепцию layer'ов расширить -- чтоб вместо ЕДИНСТВЕННОГО они были бы МНОЖЕСТВЕННЫМИ. И все требуемые данному устройству указывались бы в конфиге (либо через дополнительные '@', либо через ',').

      Тогда драйвер будет не просто адресоваться к каким-то символам, а сначала запрашивать VMT интересующего его layer'а-библиотеки. Бонус в том, что разным экземплярам устройств можно будет подсовывать РАЗНЫЕ библиотеки, отвечающие одному и тому же API.

      Проблема в том, что реализация получится уж больно громоздкой -- с единственным layer'ом намного проще.

Адресация по timestamp'ам в архивных данных:
  • 30.07.2021: раздел пока даже не столько для обсуждения, сколько просто для записи пришедшей в голову идеи "можно доступаться к архивным данным прямо по протоколу CX, используя timestamp в качестве префикса".
  • 30.07.2021: позавчера было обсуждение со словенцами из Cosylab (и ЧеблоПашей и Карнаевым) архивирования данных и системы их просмотра для СКИФа -- там всё под EPICS и через Channel Access и/или PVaccess. И после него мне пришла в голову "идея на грани гениальности": дать доступ к архиву прямо по Channel Access (или PVaccess). Вот цитата из моего сегодняшнего письма Карнаеву/Чеблакову с изложением этих мыслей:

    Чтобы можно было обращаться к каналам со специальными именами вида @TIMESTAMP:ИМЯ_PV или [TIMESTAMP]ИМЯ_PV, и оно бы автоматически отдавалось бы из архива (с надлежащим timestamp'ом и прочими атрибутами вроде alarm etc.). Это дало бы возможности, в частности:

    1. ссылаться на архивные данные в разных программах -- например, как на "точку для сравнения";
    2. использовать для просмотра не-тривиальных данных те же программы, что и при обычной работе с системой управления; (не-тривиальные данные -- это, например, осциллограммы, картинки, ... -- т.е., все, состоящие не из 1 канала, а из некоей более сложной структуры (осциллограмма -- waveform, диапазоны и прочие настройки)

    ...да мало ли ещё плюсов от такой возможности -- это сравнимо по степени унификации с Unix'ным "любой объект -- это файл!".

    Идея эта родилась, конечно, в первую очередь с CX в уме: вследствие принятой в CX иерархической системы имён всё очень хорошо сходится --

    • родственные каналы будут обычно в одной иерархии, имея общий префикс, и добавление дополнительного префикса timestamp'а на их родственность никак не повлияет -- они останутся "соседними по именам";
    • утилитам, работающим с "не-тривиальными данными" -- как раз обычно и указывается общий префикс, так что они автоматом будут годиться для просмотра таких архивных данных.

    11.08.2021: письмо-то тогда отправил -- с изложением идеи и вопросом, не возникала ли такая идея раньше -- но ответа так ни от кого из них не дождался.

    То ли идея совсем бредовая, то ли, наоборот, слишком гениальная и потому просто никому не понятная...

    По-хорошему, всё же надо будет в EPICS'ном maillist'е "Tech-talk" спросить, но, видимо, попозже -- когда буду лучше петрить: ведь наверняка будут вопросы "а вам это зачем?" (прикидка -- для просмотра вэйвформ и связанных с ними данных -- т.е., "сложных структур" -- из архива), и вряд ли кто-то прямо загорится "о, да-да, отличная идея, давайте сделаем!!!111".

    11.08.2021: неприятное соображение касательно реализации такого под CX: если делать путём изготовления другого "серверного окружения" (cxsd_hw+cxsd_db "обращающиеся в архив" вместо БД железа), то будет проблема, заключающаяся в том, что

    • предполагается, что CxsdHwResolveChan() должна дать "мгновенный" ответ, ...
    • ...а замена её внутренностей с поиска по своей БД -- CxsdDbResolveName() -- на СЕТЕВОЙ запрос к архиву будет принципиально долгим.

      ...и к тому же ещё и асинхронным -- на что юзеры API "ResolveChan" никак не рассчитывают.

    И как это можно расшить? Напрашиваются такие варианты:

    1. Делать "другую реализацию" не cxsd_hw+cxsd_db, а cxsd_fe_cx.

      Но это крайне неприятно по ряду причин:

      1. Придётся поддерживать МНОЖЕСТВЕННЫЕ реализации серверной части протокола CXv4 -- а это ОЧЕНЬ ПЛОХАЯ идея.
      2. Такой доступ "в архив по обычному протоколу данных" будет работать только через CX, а остальные плагин-протоколы нет.
    2. Как-то перетрясти внутреннее устройство cxsd_fe_cx.c для поддержки асинхронности запросов "есть ли такой канал?". Как именно -- неясно: там рассчитано на мгновенный последовательный ответ на все chunk'и запроса.
    3. ВСЕГДА отдавать ответ "да, канал найден!", но считать его "неготовым", и тут же организовывать реальный запрос к архиву, а по получению ответа "оживлять" канал.

      ...правда, тоже попахивает асинхронностью.

      Возможно, ввести на уровне API cxsd_hw дополнительный параметр "канал ещё не готов", чтобы в таком случае cxsd_fe_cx бы НЕ пытался отдавать клиенту свойства такого канала, а отсылал бы их потом, после получения ответа от архива.

    Посмотрел внимательно на код обработки CXC_CH_OPEN -- по сути, вариант (3) легко доводится до варианта (2): при наличии параметра/состояния "канал ещё не готов" (точнее, "недо-найден") ВООБЩЕ НИЧЕГО клиенту не отвечать (в нём канал просто будет оставаться недо-открытым и нефункционирующим).

    Но, естественно, придётся вводить дополнительный callback-API "уведомление о завершении поиска канала".

    Как бы то ни было -- такая модификация выглядит не шибко сложной, а рассуждения пока всё равно лишь теоретические.

  • 18.07.2022: ещё некоторое время назад при рытье CAS_Tutorial.pdf и сопутствующих презентаций обнаружил в какой-то из тех презентаций, что им приходила в голову иная идея для обеспечения доступа к архиву (да, записываем только сейчас, месяцы спустя...):
    • ОТДЕЛЬНЫМ КАНАЛОМ, который client-specific -- т.е., у разных клиентов будут разные "инкарнации" этого канала, и данные из прочих каналов им будут отдаваться тоже разными в зависимости от их персонального "времени".
    • А совсем НЕ префиксом для всех каналов.

    (Да, там предполагалось, что такую функциональность может реализовывать какой-то бридж доступа к архиву, сделанный на основе libcas.)

    У этого подхода есть плюсы и минусы по сравнению с вариантом "[TIMESTAMP]ИМЯ_PV":

    • ПЛЮС -- что не требуется как-то уметь указывать готовому клиенту, что всем именам надо добавлять некий префикс.

      ...тут дело в том, что в EPICS, в отличие от CX, вроде нет такой штуки как "иерархическая адресация" и тем более "baseref".

    • ПЛЮС -- это позволяет, на лету крутя значение канала-указателя-времени, прямо внутри окна приложения просматривать изменения во времени.
    • МИНУС -- в готовых-то программах/скринах нет никакой ручки "время", так что В НИХ крутить вроде как нечего; ...

      ...а сделать эту ручку "внешней" по отношению к скринам -- отдельной софтиной или просто caput'ом -- нельзя, т.к. все эти каналы-указатели-времени -- per-client, так что указатель времени от другой программы не сможет влиять на отдачу данных того скрина.

???:
Проблемы:
  • 28.11.2014: раздел для обсуждения (и хотя бы перечисления) имеющихся и потенциальных проблем реализации. В первую очередь идеологических (например, реализация БД выглядит чисто техническим челленджем, идейных проблем там нет).
  • 28.11.2014: очевидные на текущий момент
    • Timestamp'ы: нынешняя схема работать будет только при точной синхронизации часов на клиенте и сервере.
    • Параметризованные запросы: КАК?
    • (02.12.2014) конверсия между числами и строками.
Timestamp'ы:
  • 28.11.2014: как оно заточено сейчас, предполагается, что работать будет только при точной синхронизации часов на клиенте и сервере, иначе каналы будут посиневать либо всегда (сервер отстаёт от клиента), либо никогда (клиент отстаёт от сервера).

    28.11.2014: идея: а что, если в каждом пакете с данными присылать timestamp (результат gettimeofday) от сервера, в клиенте в момент получения делать то же самое, и разницу (клиентово-серверово) прибавлять к timestamp'ам каналов? С одной стороны, локальную проблему решит; с другой стороны, не обесценивает ли это вообще всю идею глобальных timestamp'ов?

    P.S. А поле под timestamp надо в пакете с данными предусмотреть.

    P.P.S. Главная проблема с этой "1 секундой" -- что пакет мог задержаться из-за активности системы/сети, и даже при точно синхронизованных временах получится расхождение...

  • 04.08.2015: близкий вопрос -- возрасты свежести. Идеология была разработана под v2'шную модель, а в v4, вследствие другой схемы обновлений (не "всё всегда" приходит от сервера, а только реально обновившиеся) схема не очень-то работает.

    04.08.2015: список замечаний:

    • Сейчас сделано, что CxsdHwSetDb() при создании каналов (заполнении cxsd_hw_channels[]) rw-каналам возраст свежести по умолчанию ставится {0,0} (в отличие от ro, у которых {5,0}). Не сказать, что что-то при этом сильно изменится.
    • Указание timestamp'а канала -- 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);
      т.е., игнорирование.
    • НО!
      1. Неприятно, что реальных timestamp'ов rw-каналов никогда не видно. (И с учётом умолчательного для них fresh_age={0,0} такое поведение просто лишено смысла.)
      2. Но главное -- что этот механизм не даст эффекта: ведь клиентам значения присылаются при обновлении, так что взведения при записи флага wr_req клиент не увидит.

        Единственное исключение -- свежезапущенный клиент, который получит текущее значение в ответ на PEEK.

Параметризованные запросы:
  • 28.11.2014: собственно, КАК? Вот всё упоминается, что "надо будет сделать", но как конкретно (или хотя бы примерно) должна выглядеть их реализация (в первую очередь в cxsd_hw (в куче точек!), но и в cda тоже -- всё в точках передачи данных (рядом с конверсией)) -- совершенно неясно.

    28.01.2015: пока что всё больше и больше выглядит (см. обсуждение про "базовые и настроечные" за сегодня), что по крайней мере для fastadc/pzframe ничего этого не потребуется вовсе, поскольку должный эффект достижим просто "правильной" организацией каналов-параметров:

    1. Кручение параметров обеспечивается разделением параметров на "настраивающие" ("NUMPTS") и "описывающие текущее" ("NUMPTS_CUR").
    2. Возврат массива вместе с параметрами -- просто само собой и выходит, для того и была сделана ReturnDataSet() взамен v2'шной ReturnChanGroup().

    ...т.е., всё, что потребуется -- это

    1. Возвращать в качестве timestamp'ов параметрических каналов оные от основного. Что уже давно в cxsd_hw и сделано (осталось только уметь в devlist'е указывать).
    2. По возврату/передаче вызывать клиента после того, как ВСЕ данные уже сложены и готовы к вычитыванию.

      В сервере-то это сделано, а у клиента -- некий вопрос: значения-то придут по протоколу CX независимой пачкой, и описывающие параметры придут ПОЗЖЕ описываемого массива. Но тут решение -- драйверам в ReturnDataSet()'е указывать массив после параметров; работать оно будет, хотя это кривизна -- использование побочного эффекта вместо корректной гарантии совместности.

    Вот хорошо бы, чтоб всё так получилось, поскольку вопрос "Как понять, что данный ответ от драйвера является ответом на вот этот запрос с этими параметрами" -- то, что в v2 делалось хитрыми проверками в may_return_data() -- так и не решен, и даже идей его решения нету.

    ...и никакие PZREAD/PZWRITE не потребуются (которые тоже так толком и не продуманы) -- достаточно нынешнего API, а вопрос "группового возврата (п."b") решается уже в рамках сервера/протокола, не затрагивая драйверы.

    01.02.2015@на-ночь-засыпая: да,

    • Можно ввести соглашение, что "базовый" канал возвращается драйвером после всех настроечных (в конце ReturnDataSet()'ного списка).

      03.02.2015: а почему ПОСЛЕ?! Ведь, во-первых, все evproc'ы вызываются уже в самом конце, после обработки ВСЕХ каналов (так что тут порядок пофиг), а во-вторых, копирование timestamp'ов производится как раз в этом месте, и там порядок уже НЕ пофиг (хотя при возврате одним блоком всё равно используется ОДИН timestamp, взятый в начале вызова). Так что -- ставим базовые как раз в НАЧАЛО списка.

      25.02.2015: а ПОТОМУ ПОСЛЕ, что evproc'ы вызываются всё-таки в порядке списка (хоть и после обновления всего), и при "после" параметры прилетят по TCP уже ДО базового, и в момент дрыганья они будут готовы. При "базовый в НАЧАЛЕ" же параметры в момент обновления базового будут еще СТАРЫЕ. Жопка...

      05.03.2015: короче -- СЕЙЧАС надо возвращать СНАЧАЛА СКАЛЯРНЫЕ параметры, а уж в конце списка -- вектора.

    • В таком случае задача "атомарного возврата базового и параметров" локализуется в реализации CX-протокола (и, вероятно, cda):
      1. evproc достаточно вешать на базовый, и по дерганью оного возвращать уже всё сразу, ...
      2. ...для чего ("всё сразу") и надо будет добавить в CX-протокол/cda возможность хитрого запроса "мониторь вот этот канал, а при его отсылке заодно отсылай и значения вот тех".

        Хотя по факту -- учитывая, что связь по TCP, т.е., упорядоченная, с гарантией последовательности -- достаточно и нынешней модели: параметры и так придут в клиента (и cda) ДО базового (просто отдельными посылками, а не одним chunk'ом/пакетом) и в момент его вычитывания клиентом значения уже будут актуальными.

    02.02.2015: но стоит заметить, что все эти рассуждения касаются ЧИТАЮЩИХ каналов. С каналами ЗАПИСИ всё не так просто -- там нужна гарантированная атомарность, а не "удобно получилось по факту".

    С другой стороны, СЕЙЧАС таких каналов записи ровно 1 вид -- таблицы в козачиных ЦАП. А там параметры в общем-то не особо нужны: вместо нынешнего [0] можно использовать прямо nelems.

    03.08.2016: по результатам реализации всего стека pzframe, где решением оказался отдельный канал "маркер", видно, что в случае реализации параметризованных запросов на уровне протокола (а не API драйверов) нужно будет делать ровно так же: объявлять один из каналов в запросе "маркером", чтоб сервер отсылал всю пачку именно по обновлению этого канала.

  • 20.05.2015: тут будем записывать результаты разговоров и всякие возможные резоны для "старой" схемы, как в v2 -- с атомарностью.

    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, да?), то вот что вылазит:

    • Пусть даже пофиг, как сервер это получит (допустим, как-то придумали способ передать такой запрос через cda->cxlib->cxsd_fe_cx).
    • Учитывая, что запросов таких может быть не один и не от одного клиента (иначе бы и огород с "как добиться атомарности" городить не надо было) -- очевидно, надо запрос где-то сохранять; причём сохранять целиком, единым куском (все записываемые параметры и запрашиваемый на измерение (или запись) канал).
    • Как передавать такое драйверу? Требовать от драйверов поддержки именно _PZREAD/_PZWRITE? Возможно (именно так оно издавна задумывалось, с эмуляцией путём "сначала куча записей, потом чтение/запись").

      Если драйвер не умеет -- то полная Ж: невозможно гарантировать корректную передачу параметров на запись, а главное -- их отработку (учитывая, что на эти каналы могли приходить ИНДИВИДУАЛЬНЫЕ запросы записи).

    • Множественные запросы, очевидно, надо ставить в очередь. И, возможно, как-то детектировать совпадающие -- чтоб не дублировать, а давать драйверу ОДИН запрос и раздавать ответы всем.

      @лыжи-конец-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@вечер-лыжи: Некоторые мысли насчёт "как реализовать параметризованные запросы", пришедшие в голову совершенно неожиданно в процессе обдумывания реализации драйвера "прописыватель пресетов".

    19.12.2018@вечер-лыжи: хронология мыслей:

    1. @1-я 2-ка, начало: Размышлял, как бы реализовать драйвер "preset_selector": как там выполнять парсинг параметров, как аллокировать место под сами пресеты, как регистрировать каналы, как выполнять групповую запись при активации пресета.

      Детали "размышлений":

      • Первые 10 каналов зарезервировать для "служебных" целей: в частности, канал 0 -- собственно активатор пресетов.
      • В начале auxinfo указывать количество пресетов; каналы перечислять списком через пробел, но дать возможность указывать параметры (defpfx и base) -- если внутри есть '=', то это параметр. Естественно, defpfx можно указывать лишь ДО первого канала.
      • Получается, что и регистрация контекста должна выполняться не перед циклом парсинга, а перед регистрацией первого канала.
      • И некоторые неясности с аллокированием памяти: если заранее не знать количества каналов (а знать лишь количество пресетов) -- то как аллокировать?

        Или сначала понарегистрировать каналы (их, если сделать ограничение, например, 100, можно и в сиксированный массив в стеке), а потом, ПОСЛЕ парсинга, уже аллокировать?

      • ...и еще: поскольку нужна поддержка каналов разного типа, то в качестве значений пресетов использовать CxAnyVal_t; и хранить переданный при последней записи тип, и его же и отправлять при активации пресета.
      • ...но ведь cda сама будет выполнять конверсию (для буферизации для -- отправки) значит, нужно будет при регистрации канала указывать его тип...

        25.12.2018: да, это уже делается в mirror_drv.c, можно оттуда код взять. А trig_read_drv.c идёт дальше: при отсутствии @t-спецификации пытается разрезолвить канал при помощи CxsdHwResolveChan() и взять его свойства (dtype ПЛЮС n_items!) из cxsd_hw_channels[].

      • ...откуда явно следует, что надо бы научиться и ПИСАТЬ в каналы CXDTYPE_UNKNOWN. Чтоб полиморфизм был уж полным, и чтобы прямо сами драйверы могли решать, устраивает ли их полученный в записи для канала тип или нет.
    2. 2. @1-я 2-ка, между 0.5 и 1: И придумалось: во -- надо иметь готовые массивы номеров каналов, а массив указателей values нацеливать на соответствующий блок значений (в зависимости от номера выбранного пресета).
    3. 3. @2-я 2-ка, начало: и тут вдруг сообразил: мы ж через cda работаем, в которой НЕТУ никаких групповых операций!
    4. 4. @2-я 2-ка, между 0.5 и 1: нуачо -- давай сделаем ДВА варианта драйвера: один на cda (с поштучной записью), а второй прямо на cxsd_hwP (через CxsdHwDoIO() -- так будет не только побыстрее, но ещё и сгруппирует записи каналов, принадлежащих одному устройству, в один запрос).
    5. 5. @2-я 2-ка, между 1 и 1.5 (горочка вниз в лощинку): а ведь так гордился отличием CX от прочих, что в нём есть групповые операции! Ну да, есть, но только на уровне протокола, сервера и драйверов. А на уровне клиентов -- нет... (А так-то да: CxsdHwDoIO() и ReturnDataSet() -- симметричные операции, с похожими наборами параметров. Понятно, что у первого нет rflags[] и timestamps[], а у второго нет action; но requester и devid в них по смыслу аналогичны.)
    6. 6. @2-я 2-ка, последние 0.5км: осенило!
      • Какая у нас проблема: что в cda напрочь отсутствуют групповые операции, а лишь всё индивидуально, так?
      • Ну так сделать возможность ПРЯМО НА УРОВНЕ cda ОБЪЯВЛЯТЬ, что некоторый набор каналов составляет одну группу! Тут можно воспользоваться принципом, придуманным (но не реализованным) для локинга: каналы сначала регистрируются индивидуально, а потом одним вызовом набор ref'ов объявляется "группой".

        И чтоб запись таких каналов могла бы делаться скопом, и чтоб команда уходила и через сетевой протокол тоже прямо единым блоком, чтоб и на стороне сервера оно б тоже расшифровывалось как один запрос и приводило бы к ОДНОМУ вызову CxsdHwDoIO().

      • Реализация общей инфраструктуры должна быть на уровне cda_core. В refinfo_t добавить поле group_id, которое изначально =-1 (либо =0, если реальные group_id могут быть только с 1). И группировать можно будет только ref'ы с одинаковым sid (а тут надо проверять, что это РАБОЧИЙ sid, а не резолвер).
      • Понятно, что тут будет туча сложностей и ограничений. А именно:
        • Реализовывать такую групповую операцию сможет только cda_d_cx, но НЕ cda_d_epics; возможно, cda_d_tango, для "команд".
        • Загруппировывать можно будет ТОЛЬКО каналы, принадлежащие одному sid'у; а в какой момент определять, что они таковы? А если изначально они безсерверные, с расчётом на UDP-резолвинг?
        • RSLV_TYPE_GLBL-канал могут менять свою принадлежность по ходу жизни, перенацеливаясь на другие сервера. Группа будет разваливаться? Но если ВСЕ каналы переехали в другой сервер, то всё окей.
        • А ещё каналы должны быть уже в состоянии "ready".
        • Похоже, из двух предыдущих пунктов следует вывод: проверять "годны ли каналы для групповой операции" надо не в момент создания группы, а именно в момент самой операции. Тогда операция будет считаться валидной, если все каналы "ready" и все принадлежат одному sid'у.
        • Отдельная головная боль: раз группы могут "разваливаться" (становиться временно негодными), то программе надо б мочь получать какие-то уведомления о наступлении готовности. Очевидно, всё нужное есть в инфраструктуре уже СЕЙЧАС; надо только понять, ЧТО это.
        • Не делать ли так: если риходит изменение статуса некоего ref'а, и этот ref входит в группу, то выполнять для всей группы проверку "все ли каналы готовы" -- т.е., де-факто именно "проверку на годность". И тогда присылать клиенту event уже уровня группы (а не одного канала).
        • А какие операции вообще считать определёнными для группы? Ну запись -- окей, да.

          А чтение? Чтоб какой-то из каналов назначался бы триггером? А только ли операция PZWRITE (которая реально является просто единой операцией записи, с должным образом упорядоченными каналами, чтобы "параметры" шли ДО параметризуемого основного канала)? А как насчёт PZREAD?

    Как бы то ни было, главная идея-прорыв -- о том, что "групповость" добавляется и в cda.

Конверсия числа<->строки:
  • 02.12.2014: давным-давно предполагалось, что таковая конверсия будет возможна -- прямо прозрачно, наряду с автоконверсией между вещественными и целыми.

    Но пока в эту сторону не сделано ничего, совсем.

    Неясно даже, КАКОГО рода конверсия предполагается: диапазонная,

    [0,LAST]<=>["String0\tString1\t...StringN"]
    -- явно '\t'-separated multistring, или же поштучная
    (Val1<=>"String1"), (Val2<=>"String2"), ..., (ValN<=>"StringN")
    -- хбз как.
Неплоская структура devtype'ов:
  • 11.01.2016: Федя еще энное время назад (месяц-два-три) начал требовать возможности более сложной структуры имён каналов внутри devtype, в идеале -- "вложенных devtype".
    • Ясно, что это как бы не совсем возможно и совсем не в дугу: devtype'ы создавались как просто маппинги имя->номер, и не более того.
    • Федины же резоны такие: раз идеология имён в CXv4 выглядит как C'шная, то хочется и C'шными же приёмами пользоваться, с вложением типов.
    • Но потребность в не-плоской структуре есть.
    • Пока она исполняется при помощи cpoint'ов, но они функционируют на другом уровне.

    В общем, надо на эту тему думать -- может, удастся как-то непротиворечиво и "в струю" расширить систему типов и схему именования.

    11.01.2016: замечание-наблюдение: "подветки" в devtype'ах можно делать, если просто разрешить имена с '.' внутри в парсинге -- ParseChanList() (две точки ParseAName() -- имя определяемого канала и ссылка на target), в остальных же местах всё и так будет работать.

    Конкретно -- в CxsdDbResolveName() поиск внутри устройства никак не интересуется наличием '.' после первой (отделяющей имя канала от имени устройства).

    29.07.2016: и еще более это нужно для pzframe-устройств:

    • Там есть несколько блоков одинаковых каналов -- именуемых somechanN, которые тем не менее имеют смысл именно в межканальном наборе (chanabcN, chandefN и changhiN логически связаны), так что желательно бы иметь возможность адресоваться к ним через единый префикс и потом фиксированное имя через точку -- как к groupN.{abc,def,ghi}.
    • К примеру, в VSDC2 таких наборов 2, в быстрых осциллографах -- по числу линий, а в U0632 -- вообще 30.
    • Но устройство clientside-части pzframe таково, что оно хочет иметь некий "префикс", к которому через точку (это делает cda!) дописываются имена каналов из набора.

    24.10.2016: пробуем "хак" по идее от 11-01-2016.

    • Поскольку используется ParseAName(), маппирующаяся на ppf4td_get_ident(), и никак не позволяющая «просто разрешить имена с '.' внутри» то:
      1. Введена ParseMName() ("M" -- Multi), использующая...
      2. ...локальная "копия" стандартной -- local_ppf4td_get_ident(), дозволяющая '.' при наличии флага...
      3. ...локально введён PPF4TD_FLAG_DOT=PPF4TD_FLAG_IGNQUOTES.
    • Дополнительно к указанным "две точки" понадобилось также модифицировать проверку "парсить ли дальше после '>'" -- там было лишь isalnum(), кстати, считавший нелегитимными подчерки '_'; вот оно дополнено и подчерком, и '.'.
    • После этого всего -- да, работает как задумано!!!

      (Проверено на симуляторе с u0632.)

    25.10.2016: реализуем хак "полномасштабно".

    • ppf4td:
      1. Вводим поддержку возможности считать точку частью идентификатора:
        1. Флаг PPF4TD_FLAG_DOT запараллелен с PPF4TD_FLAG_IGNQUOTES.
        2. Собственно использование -- тривиально.
      2. За компанию -- для будущих дополнений -- раздвигаем флаги: теперь все PPF4TD_FLAG_*TERM идут с 10, а не с 2.

      Поскольку никакими загружаемостями ppf4td не используется, то проблем от смены API нет и ничьи версии продвигать не требуется.

    • Локальности, соответственно, убираем.
    • Начиняем "многолинейные" devtype'ы соответствующими строчками.

      В их число вошли u0632.devtype и vsdc2.devtype.

      А вот fastadc пока без надобности, но если припрёт -- то легко (кроме одноканального c061621, которому это не нужно).

    • И в s_u0632.subsys возможность заюзана: там уже была предподготовка в виде под-оконца для каждой строчки, а теперь в него вставлен pzframe.
    • Поскольку это всё же хак, то повсюду расставлены комментарии-предупреждения "DEVTYPE-DOT-HACK":
      1. В коде.
      2. В devtype'ах.

    Пара замечаний:

    1. В таком халтурно-хаковатом варианте реализация даже гибче, чем если б с вложенными devtype'ами: она позволяет то же самое, что и cpoint'ы (хотя и менее удобно) --
      1. набирать "блоки" из разрозненных каналов;
      2. делать разнородные, где часть содержимого -- индивидуальные каналы (например, по-линейные настройки), а часть -- "глобальные" (всякие ptsofs и const32).
    2. Можно выпендриваться даже хуже, чем с cpoint'ами: поскольку '.' не является внутри namespace'а устройства чем-то особым, то можно делать совершенно независимые имена вроде "ИМЯ" и "ИМЯ.ПОД_ИМЯ" (например, line2 и line2.numpts).

      Но это чревато проблемами, и лучше такого не допускать.

  • 18.05.2018: интересно, а можно ль делать "симлинки"-cpoint'ы на середину таких "неплоских" (с точкой внутри) имён? Например,
    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) это никак не сделать.

    Можно только пофантазировать, не доработать ли схему как-нибудь, чтобы она допускала такой частичный матчинг:

    • в случае, если последний кусок target'а cpoint'а нашёлся в каком-то имени, и сразу после него в этом имени идёт '.', как-то запомнить сей факт;
    • для чего придётся добавлять новый targ_type, вроде "CXSD_DB_RESOLVE_MTCHBEG", и сохранять в item_data в дополнение к ref_n еще и то самое "смещение".

    Однако, по здравому размышлению -- нет! Нельзя так делать, ибо это криво.

    1. ЧЕЙ ref_n запоминать в item_data? Ведь потенциальных "подходящих" для указанного префикса целая толпа, и при использовании данного cpoint'а как промежуточной точки (с дальнейшими компонентами в имени) пришлось бы проверять ВСЕХ потенциально подходящих.
    2. Идеологический косяк: разрешение подобного частичного матчинга сломало бы текущую парадигму работы, при которой ищется только ТОЧНОЕ соответствие и НИКАКИХ "префиксов", что позволяет иметь похожие и даже перекрывающиеся имена, вроде "abc.def" и "abc.def.ghi", и резолвятся они оба.

    Вывод: если и захочется сделать возможность ссылаться на серединку имени "неплоских" devtype'ов, то придётся реализовывать ПОЛНОЦЕННЫЕ не-плоские типы, чтобы такая «ссылка на серединку» ссылалась на некий узел, являющийся содержателем вложенной группы имён/узлов.

    А этот раздельчик метим "withdrawn".

  • 27.09.2018@утро, только пришёл на работу, лифт на 5-й этаж: с другой стороны -- а кто мешает ввести такой особый тип оконечного узла CXSD_DB_CLVL_ITEM_TYPE_nnn, указывающий на "частичное" имя (да, при резолвинге цели cpoint'а придётся сильно хитрее сравнивать, с учётом возможных '.'), и в CxsdDbClvlInfo_t также хранить оффсет точки, на которую в "target-имени" указывает этот cpoint-узел.

    ...но, подозреваю, мутновато всё это будет -- фигня на фигне, хак на хаке. Просто идея такая пришла, возможность теоретически есть -- вот и записываем.

Табличные каналы в козачиных ЦАПах, в v4:
  • 26.09.2016: давняя идея была в том, что на табличные каналы натягиваются те же самые {R,D}, что на соответствующие им скалярные, ну и при надобности делаются аналогичные скалярностным cpoint'ы.

    НО! Ведь в текущей v2'шной реализации (которую хотелось с минимальными напрягами перенести в v4) каналы НЕОДНОРОДНЫ! Они состоят из дуплетов {ВРЕМЯ(cs),ЗНАЧЕНИЕ(uV)}, т.е., никакие прямолинейные {R,D}-преобразования применять нельзя!

    @После обсуждения на пультовой (после понедельничной планёрки) с участием Беркаева гусевской идеи подобавлять в CEAC51A/CEAC51 команду от CPS10 "иди от текущего значения вот к этому, за такое-то время". Там же обсуждались и вообще разные применения табличной отработки -- вроде плавного подъёма, но не линейного, а "по кривой как у гистерезиса -- например, из двух сшитых парабол".

    26.09.2016: ну и что делать?

    • Первая паническая мысль была «надо ввести специальный модификатор в dtype, что канал "комплексный"».

      Но этак никаких битов в dtype не хватит на такие выдрепоны.

    • Понятно, что нужно как-то менять модель данных. Как?
    • И еще понятно, что решения из v2 в v4 просто так не переносятся.

      Примеров достаточно: quant/OTHEROP (из-за разных типов); conns_u (из-за возможности миграции каналов между серверами), теперь вот это...

      (...а еще {R,D}, которые раньше были зашиты, а теперь приходят от сервера, и пока не придут -- никакую запись делать нельзя...)

    28.09.2016: вчера поговорил с ЧеблоПашей.

    • Да, видимо, отдельный массив для времён, хоть это и криво и неатомарно.
    • ..."а вот в EPICS4 есть двумерные массивы!"

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

    ...кстати, и драйвера ИСТа у него нет вовсе.

    • Предложение то же -- отдельный массив на времена.
    • Или "а делай конверсию тоже массивом, с разными коэффициентами в чётных и нечётных позициях".

    29.09.2016@утро-мытьё-посуды: да, разными каналами, а синхронизация -- "агрегирование каналов" в параметризованных запросах.

    15.07.2018: собственно -- сделано еще год назад, с раздельными массивами; и даже больше -- ОБЩИЙ массив (матрица на все каналы) не используется и даже реально не поддерживается вовсе.

    С чистой совестью "done".

  • 14.11.2016@вторничный-круглый-стол-~10:00: уметь бы "стартовать с текущего значения" -- например, указанием значения -20V. ("вдогонку" по мотивам письма Беркаева от 12-11-2016.)

    Потребно для перестройки магнитов -- всех скопом за указанное время.

    15.07.2018: эта-то фича сделана, еще в июне-2017, при реализации поддержки таблиц. Только значения там не -20, а >+20.

    Но:

    • В таком виде она годится ТОЛЬКО для вышеуказанной цели "всех скопом за указанное время".

      А правило "скорость не выше такой-то" -- увы, тут игнорируется.

    • Так что для просто "привести всех в нужную точку" это использование текущих не подходит.

    С другой стороны: а зачем тогда вообще таблицы? Просто записываем режим -- и все топают к нужным значениям с разрешённым им скоростями.

    Так что зря я вскинулся, и преспокойно ставим "done".

Недоделанное:
  • 29.04.2016: краткий список глобальных вещей, которые в v4 еще не сделаны, но необходимы.
    1. UDP-резолвинг -- cda'шная часть.
    2. cda: не-конвертация данных до реальной отправки (чтобы при вызове команды записи сразу после открытия соединения, но еще ДО ответа (с калибровками) от сервера данные не "портились").
    3. Client-side стек pzframe/fastadc/vcamimg.
    4. Удалябельность ("гроханье") cda-объектов -- cda_del_context() & Co.
    5. (10.05.2016) Допилить тулбар -- сохранение/восстановление режимов и server-leds.
    6. (03.06.2016) Оранжевение -- OTHEROP.
    7. (03.06.2016) "Стили" в MotifKnobs -- "important", VIC.
    8. (30.07.2016) Сохранение графиков в histplot'е.
    9. (30.07.2016) Сохранение и чтение графиков в fastadc.
    10. (09.08.2016) Разобраться, почему SIGSTOP'нутой и потом SIGCONT'нутой программе накопившиеся у сервера обновления присылаются не почти-мгновенно (по объёму входного буфера), а очень медленно и долго.
    11. (10.10.2016) Скрипты для интеграции в ОС: в init/systemd для запуска/останова при загрузке/останове ОС, плюс для logrotate.
Нереентрантность SLOTARRAY:
  • 01.12.2017: сегодня осознана проблема, что SLOTARRAY-GROWING очень плохо подходит для организации структур данных, имеющих "внешний интерфейс" -- т.е., когда вовсе НЕ вся работа с ними (с точки зрения алгоритмов и прочего control flow, а не инкапсуляции) находится под контролем и может быть проанализирована.

    04.12.2017: (огрокано еще 01-12-2017, но записывается сегодня) собственно, первая часть проблемы была осознана еще с месяц назад, при реализации "гроханья" cda-ресурсов, а вторая -- уже сегодня, при анализе поведения cxlib и cda.

    1. Первая проблема выглядит общей для ООП: что, если нужно удалить некий объект прямо изнутри его callback'а?

      У нас эта проблема решается путём "отложенного удаления": если объект находится в процессе обработки клиентом (т.е., в данный момент исполняется callback), то удаления не производится, а объект лишь помечается как требующий удаления, и уже после завершения callback'а тот код (принадлежащий объекту, а не его клиенту), что вызвал callback, проверяет этот флаг и выполняет реальное удаление.

      • Обеспечивается это парой флажков:
        1. being_processed: ++'ится перед вызовом callback'а и --'ится после, удаление считается разрешенным при ==0;
        2. being_destroyed: взводится в функции удаления ("деструкторе") при being_processed, и проверяется в точке после вызова callback'а, что если !=0 при ==0'м being_processed, то производится удаление.
      • Эта архитектура еще несколько лет назад была внедрена в fdiolib и затем в libremsrv.
      • А в ноябре она была реализована в cda для dat-плагинов. Из-за 2-уровневости -- объекты "существуют" в рамках cda_core, но код живёт в cda_d_* -- реализация получилась развесистее:
        1. Методы del_srv() не просто процедуры, выполняющие удаление беспрекословно, а ФУНКЦИИ, возвращающие результат "да, я всё удалил" или "нет, я пока занят, сообщу позже".
        2. С дополнительной функцией, предоставляемой cda_core'ом для dat-плагинов "я закончил свои действия и всё удалил, можешь освобождать ресурсы на своей стороне" -- cda_dat_p_del_server_finish().
    2. Сегодня же осознана вторая часть: что при использовании SLOTARRAY-GROWING дело не только в удалении вызвавшего callback объекта, а вообще в ЛЮБЫХ действиях, могущих модифицировать вектор.

      Точнее, ДОБАВИТЬ в него новые ячейки -- в этот момент вектор может реаллокироваться, и имеющийся в находящемся в исполнении коде указатель станет невалиден.

      Причём этот код --

      • не только код конкретной работы с объектами (как в cda), куда при желании можно было бы добавить пере-кэширование указателей на объекты после каждого вызова callback'а,
      • а еще и код самого SLOTARRAY'я в misc_macros.h: там -- в Foreach'ах -- никак не предусматривается возможность такой модификации "на лету", и просто идётся по списку объектов последовательно.

        И, при проходе итератора по вектору, расположенному в другом объекте (как список evproc'ев в контексте или сервере), еще и сам этот объект-вместитель может переехать, из-за того, что создан новый объект его типа.

    Выводы, по пунктам:

    1. В ООП никак толком не рассматривается такой паттерн поведения, когда объект удаляется прямо из его же callback'а.
      • ...хотя вот для файловых дескрипторов это совершенно нормальное поведение -- закрыть оный при получении EOF, и ядро нормально такое обрабатывает (вероятно, потому, что модель работы иная -- "события" вызываются вовсе не из кода обработки В/В, а сообщаются клиентской программе "синхронно", в моменты, когда и она готова к их приёму, и ядро находится вне обработчиков).
      • Но в ООП-сообществе (судя по гуглению) в лучшем случае советуют использовать smart-pointer'ы (де-факто имеет значение что-то вроде reference cointing'а?), а чаще -- "не стоит так делать".

        (Хотя и совет "пусть callback возвращает вызывателю, что тот больше не нужен и может уничтожиться" тоже встречался.)

    2. Теперь прочувствовано, на собственно опыте, почему структура данных "держим все объекты в реаллокируемом массиве" столь мало популярна, по сравнению с обычной "аллокируем каждый объект отдельно/независимо", несмотря на вроде бы простоту и эффективность первой.

    04.12.2017: выводы "на сейчас", в отношении cda и cxlib (более развёрнуто в разделе по cxlib_client за сегодня): пока с этим косяком проще смириться, т.к. проблемы будут крайне редко. А если припрёт, то переходить на SLOTARRAY-GROWFIXELEM (придуманный как-то в душе (утром?) в крохотной сидячей ванне в "Снежинке" :-)).

    04.12.2017: а не касается ли эта проблема совсем всех-всех? Включая cxscheduler и fdiolib?

    Через несколько минут, по результатам анализа их кода: нет, этой пары не касается.

    • В cxscheduler собственный менеджмент ресурсов/массивов, и множественных callback'ов там нет.
    • И с fdiolib аналогично. Да, и там есть пере-кэширование после вызова notifier'а (либо сразу после close_because() делается return).

    У-ф-ф-ф, ну хоть тут всё хорошо :)

    Вывод: одиночные callback'и ("хуки"?) радикально проще в использовании, чем evproc-/callback-list'ы, позволяющие навесить море обработчиков на событие.

    30.01.2018: ну вот, сегодня реальное проявление проблемы -- в cxlib, HandleSrchReply() начал segfault'иться, если в пакете более 1 ответа, на втором ответе. Кратко:

    • В cxlib'е V4_ALLOC_INC=2, а начинается счёт с 1. В результате cd=1 уходил на seeker'а, а вновь создаваемый (для свежеузнанного сервера) требовал роста массива, так что HandleSrchReply::cp вдруг становился невалидным.
    • Раньше-то проблемы не было потому, что ответы приходили поштучно, а теперь сразу пачкой.
    • В ДАННОМ случае проблема решена просто -- пере-высчитывание cp перед каждым вызовом нотификатора.

      Но это удалось лишь потому, что тут нет никакого ForEach...(), а просто ручной ОДИНОЧНЫЙ вызов нотификатора.

      Т.е., аналогично cxscheduler и fdiolib -- cxlib иммунен к проблеме; точнее, иммунизировабелен.

Бесканальные устройства:
  • 01.08.2018@утро-дома, потом пляж: бывает ведь, что от драйвера требуется только поддерживать некоторое фиксированное состояние устройства, НИЧЕГО НЕ ПРИНИМАЯ сверху (либо алгоритм/действия прошит железно, либо значения указываются в auxinfo).

    (Мысля пришла при обдумывании функционирования системы синхронизации ВЭПП-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, т.к. отсутствуют возможности регулирования).).

3???:
Потенциальная будущая версия v5:
  • 26.08.2019: тут будем вести просто список фич, без детального обсуждения (оно -- в разделах ниже).

    26.08.2019: первые 2 пункта давние, 3-й свежий.

    • Протокол. В т.ч. с поддержкой 64 битов.
    • "Сложные" типы данных.
    • Multithreading support (на эту тему есть свой раздел от 06-10-2013).
  • 20.04.2016@утро: протокол CXv4 -- 32-битный, включая размеры пакетов.

    И если вдруг понадобится передавать что-то свыше 4ГБ -- точнее, 2^32-1, а скорее даже 2^31-1, или 2ГБ -- то текущий протокол уже не прокатит, и придётся создавать другой, 64-битный. Который и можно назвать "v5".

    20.04.2016: с другой стороны, это вопрос именно протокола -- который в нынешней архитектуре вполне заменябелен на уровне модулей, связки cda_d_cx/cxlib+cxsd_fe_cx. Прочая же система вполне 64bit-safe.

  • 09.05.2017@утро-душ: чего еще не хватает в v4 прямо сейчас -- так это более "сложных" типов данных.

    Вот надо делать поддержку таблиц в козачиных ЦАП'ах (для перевода сварки на v4), а выйдет криво: вместо массива дуплетов {вольты,время_перехода} придётся делать ОТДЕЛЬНЫЙ массив с временами. Причина -- калибровки: ведь уставки надо указывать в вольтах или даже с еще коэффициентами, а времена -- в что-то-секундах. И свести это в единый массив никак не получится.

    09.05.2017@утро-душ: вот в EPICSv4 есть более сложные типы -- и матрицы всякие, и структуры. И даже стандартизация -- т.н. "normative types".

    Что же нам делать?

    • Очевидно, что если б были матрицы в роли "базовых"/поддерживаемых типов, то проблема б стала решаемой.

      В частности, тогда и калибровки {R,D} для таких типов тоже были бы "структурами".

    • Безотносительно глубоких деталей реализации -- как эти вещи могли бы функционировать на уровне клиентского API?
      • Очевидно, как-то придётся вовлекать dtype.

        Сейчас это просто байт (8 бит), но де-факто он всегда занимает 32-битное слово, даже в протоколе (кроме единственного случая CxV4MonitorChunk, где пакуется вместе с cond).

      • Напрашивается идея: сделать его де-юре 32-битным, чтобы при (dtype&0xFFFFFF00)==0 всё было бы как сейчас, но в старших 3 байтах как-то кодировать дополнительную информацию.
      • Главный вопрос -- КАК кодировать.
      • Никакого универсального способа в голову не приходит (кроме разве что постулированного worldwide-набора "нормативных типов", но это будет нерасширяемо).
        • Поэтому, видимо, надо между узлами (клиент и сервер, сервер и драйвлеты) как-то передавать описания типов, а уж в рамках каждого конкретного узла (клиент, сервер, драйвлет) чтобы конкретное значение dtype однозначно бы указывало на тип.
        • Причём эта однозначность должна в рамках клиента распространяться между ВСЕМИ серверами, с которыми он имеет дело -- чтобы равенство dtype'ов означало бы идентичность типов данных, без разницы, от взаимодействия с каким сервером они получены.
        • Т.е., при получении информации от сервера клиент будет составлять у себя некий реестр ("словарь"), в котором будет регистрировать типы.

          И в "расширенном dtype" будет храниться индекс описателя в этом реестре.

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

          Кстати, это немного напоминает систему типов C (и C++ ли тоже?), где эквивалентность типов определяется не по их именам (они в результате typedef'ов могут быть разными), а по их определениям.

        • Описанное выше -- это, в терминах того же EPICS'а, аналогично "CID" (Client ID, а точнее -- Client-Side ID), а в наших -- "hwr" (HardWare Ref: идентификатор, уникальный в рамках клиента).

          Но также существует и "SID" (Server ID, реально -- Server-Side ID), в наших "hwid": идентификатор, уникальный в рамках сервера.

          Так вот: чисто для некоторой экономии можно при резолвинге сделать еще один уровень словаря: чтобы сервер не с описанием каждого канала передавал бы описание его типа, а передавал бы индекс в рамках своего "реестра типов", а клиент бы держал свой кэш интересующих описаний типов для данного серверного соединения, состоящий из дуплетов {серверов_идент,описание}.

          С другой стороны, это преизрядное усложнение -- ради небольшой экономии траффика придётся лишиться "гарантированного наличия информации сразу после резолвинга", т.к. в части случаев будет требоваться еще один запрос/ответ; а лишний перезапрос -- это лишняя латентность, никак не уменьшаемая (ибо оно не векторизуется), а лишняя латентность -- всегда хуже лишнего траффика.

          Так что оставим эту идею, прямо сразу же "withdrawn".

    • А ведь изначально CX разрабатывался как ПРОСТАЯ система, с простыми компонентами, которые просто использовать/сочетать, без замудрённости EPICS'а с его множественными record type'ами.

      А теперь -- вот, получается, что этот простой подход просто не позволяет удовлетворить несложную вроде бы потребность в "таблицах переходов".

      Или как-то можно выкрутиться? Элегантнее, чем отдельным массивом?

    • Близкое по смыслу: надо б как-то поддерживать "атомарность": то, что в v2 было "большими каналами", в v4 реализовано хаком с каналами-"триггерами", каковой подход имеет массу недостатков.

      Постоянно в последние 2-3 года (да и с самого начала, года с 2005-го) мыслилось, что нужно придумать какой-то механизм.

      • Сначала были мысли про "базовые" и "настроечные" каналы.
      • Потом -- что надо от этой статичной схемы отказываться (в первую очередь из-за множественности "базовых" каналов).
      • Текущая идея-фаворит -- что надо это как-то реализовывать на уровне протокола клиент-сервер, не вовлекая драйверы.

        ...но и cda придётся поучаствовать, конечно.

    Вот эта пара -- поддержка нетривиальных типов и атомарности -- вполне тянет на v5.

    09.05.2017@вечер-ванна: и еще:

    1. Адресация: в языках-то для лазанья внутрь сложных типов есть средства -- '.' и '->' для полей структур, '[]' для элементов массива.

      А тут как? Вот есть какой-то блок данных -- массив байтов; и чо?

      Задача, собственно, из двух частей:

      1. Правильно закодировать/декодировать при отправке/приёме по сети; в т.ч., сделать {R,D}-конверсию.
      2. Доступаться к данным в клиенте.

      Чуть пространнее:

      • По первому пункту -- вот точно начинает напрашиваться вопрос "а как информация о типах хранится в C/C++?". И когда-то давно была мысль -- передавать "строковый описатель": некая строка, содержащая закодированное описание типа.
        • Например, просто буквами (как ИНФО_ПО_КАНАЛАМ в devlist'ах);
        • структуры -- в фигурных скобках {...},
        • массивы (структур) -- квадратными скобками [N] (хотя это и интерферирует с указанием просто числа после типа в ИНФО_ПО_КАНАЛАМ).

        Такой синтаксис позволил бы описывать структуры данных произвольной глубины вложенности.

      • Второй пункт -- вот вообще фиг знает.

        Методы-аксессоры? Которым передаётся "ссылка" на этот блок полученных данных и некий (строковый?) спецификатор того, что мы хотим получить?

        А у коллег-конкурентов как (TANGO, EPICS4)?

      И еще:

      • ...а еще ведь массивы могут быть переменной длины (а матрицы -- переменного размера по обоим (всем) измерениям).
      • ...а еще ведь может возникнуть желание делать "вложенные" эти "нормативные типы".
      • И как это всё сочетается с ЗАПИСЬЮ?

        Или подобное только для чтения? Не айс, т.к. вот прямо сейчас видно, что клиентам по возможности надо давать возможностей максимально близко к драйверам -- чтоб изготавливать "clientside-драйверы" (чем ЕманоФедя активно злоупотребляет пользуется).

      И, если еще как-то можно представить интерфейс к этому кошмару в cxlib (ибо низкоуровневый), то как оно должно выглядеть в cda -- неясно.

    2. (Полубредовая полуоформившаяся идея): не сделать ли возможность давать "имена" таким группам каналов -- которые вместе представляют собой единую логическую сущность? Как v4'шная группа, составляющая pzframe, или epics'ный набор, составляющий нормативный тип?

      Чтобы с такими "группами" работал бы broadcast-resolving, чтоб софтине можно б было указать "имя группы", оно бы куда-то разрезолвилось, а уже из того сервера бы вычитался бы список составляющих группу каналов.

      10.05.2017: идея действительно совсем никак не оформившаяся (от приятного лежания в тёплой ванне посетила). Как оное может помочь -- неясно, и как оно вообще может быть применено -- тоже. В текущей ситуации резолвинг "группы" прекрасно делается по раздельным именам (которые заранее известны), префиксируемым указываемым общим именем "элемента". Полезно может оказаться только в ситуации, когда состав группы заранее никак не известен; но как тогда с её содержимым обращаться -- тоже не особо понятно.

    10.05.2017: в качестве промежуточного резюме: попытка продумать поддержку сложных типов показывает, что это офигеть как проблемно и выглядит просто бесконечно трудоёмко и НЕкрасиво.

    ...но что же делать с таблицами в козачиных ЦАПах? Ведь такой вроде простой пример -- ну надо ж что-то придумать...

    Итого на сейчас:

    1. Стараться выкрутиться имеющимися средствами. Да -- пока, похоже, отдельный массив на времена.
    2. Вот если придумается что-то элегантное -- тогда можно попробовать.

    10.05.2017@дома-вечер-после-японского: пораздумавши, напрашиваются некоторые выводы:

    • Если б была "протокольная" поддержка pzframe (ex-"больших каналов"), то без хитрой типизации в большинстве случаев можно и обойтись.

      И то, что эти составляющие группу каналы являются именно раздельными каналами, просто адресуемыми -- автоматом убирает проблему "как доступаться к данным [сложных структур] в клиенте".

    • И в реальной жизни никакая особая вложенность и не нужна -- в подавляющем большинстве случаев хватит одного "уровня" (каналы в группе).
    • 11.05.2017@утро-по-пути-на-работу-светофор-ИЯФ: единственное что -- в текущем понимании модели "как реализовать групповую работу" (возвращать группу каналов по каналу-триггеру) не хватает поддержки записи.

      Т.е., ранее обдумывавшихся "параметризованных запросов" (PZREAD/PZWRITE).

    13.05.2017@дома-00:15: а ведь TANGO'вские RPC можно делать "атомарными большими каналами" -- как было в v2; только собственно атомарность надо сделать (тут это будет PZREAD).

    Кстати

    1. В v2 результат возвращался (2) при совпадении параметров ЛИБО (1) в случае, когда "проверяемый (можно ли ему вернуть)" является первым в списке (т.е., это ЕГО запрос обрабатывался и, видимо, вызвал ответ (с оговоркой асинхронных возвратов по инициативе драйвера)).
    2. В EPICS же есть некий "IOID" (Request ID) -- примерный аналог CX'ного Seq; но он вроде никак НЕ используется в целях такой идентификации (ибо самой атомарности в EPICS3 нету).

      ЗЫ: а в v4'шном cxlib_client.c поля cp->Seq и cp->syncSeq мэинтэйнятся, но никогда никак не проверяются (в отличие от CheckSyncSeq() в v2).

    15.05.2017: вот нифига, НЕ НАДО пытаться реализовать EPICS'ную замудрённость для решения имеющейся проблемы.

    • CX изначально делался по другой парадигме -- как ПРОСТАЯ система ("CISC vs. RISC" -- см. NOTES.html за 18-10-2007).
    • Вот и надо придерживаться этих принципов простоты, и стараться всё реализовывать простыми средствами.
    • 27.07.2019@утро-воскресенья, по пути в ИЯФ: строго говоря, УСТРОЙСТВА не выдают никаких данных "структурных" типов -- а только целые и вещественные, плюс массивы оных.

      Всякая же структурность -- это уже способ передачи более высокоуровневой информации, НЕ от устройств. [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'ы по сетевому протоколу.

    • Почти везде dtype реально передаётся 32-битным полем.
    • Единственное исключение -- CxV4MonitorChunk, где упаковывается вместе с "условием" в 32-битное поле dtype_and_cond.

      Но:

      1. Далее идёт 8-байтовое поле moninfo[], которое НЕ ИСПОЛЬЗУЕТСЯ.
      2. В качестве dtype ВСЕГДА передаётся 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, то:

    • В сетевом ПРОТОКОЛЕ ничего менять не потребуется.
    • А все проблемы будут исключительно в коде, который может оказаться недостаточно просто перекомпилировать, т.к. что-то может линковаться динамически.
    • 08.07.2019: во всех тех поисках забыл о протоколе remdrv. Там-то тоже cxdtype_t-данные передаются, и местами захардкожено, что это 1 байт.

      А так надо будет учитывать и изменившуюся байтовость, и что нужно выполнять endian-conversion. Конкретно:

      • remdrv_drv.c:
        1. ProcessInData(), код REMDRV_C_DATA.
        2. remdrv_rw_p().
      • remcxsd_driver_v4.c:
        1. ReturnDataSet().
        2. ProcessPacket(), код REMDRV_C_WRITE.
      • В остальных же пакетах от драйвлета (REMDRV_C_QUANT, REMDRV_C_RANGE) УЖЕ передаётся 32-битовым и конверсится.

        Только надо переделать поля с int32 на uint32.

    • 09.07.2019: кстати, а ведь и в cxsd_fe_cx.c тоже надо будет выполнять endian conversion.

      Несколькими минутами позже: неа, уже БОЛЕЕ -- не надо: там и так из-за того, что оно передаётся 32-битным, УЖЕ делается конверсия.

    • 09.07.2019: очевидно, что нужно будет сдвинуть номера версий ВСЕХ динамических компонентов -- драйверов/layer'ов, REMDRV_PROTO, cda-плагинов.

    Получасом позже:

    • Пообщался с ЕманоФедей. Похоже, у него никакой специфики использования cxdtype_t (вроде упаковки с чем-то ещё) нету. И в случае изменений размера достаточно будет просто всё перекомпилировать.
    • Проблему же с динамической линковкой можно будет решить простым стандартным способом -- сменой версии.

    17.07.2019@пляж-после-обеда: а толку-то от 32-битного cxdtype_t? Ведь ПОЛНАЯ информация о типе туда всё равно не вместится. Так что можно пока притормозить переход на 32-битность -- пока детально не огрокаем, как всё устроено в EPICS4 и TANGO, чтобы достичь лучшего понимания.

    07.07.2019@вечер-дома: в презентации "EPICS 7" от Kay Kasemir есть фраза, что EPICS 7 "Breaks your device support".

    Так вот, почему-то (от противного? :-)) эта фраза натолкнула на мысль:

    • Ведь "параметризованное чтение" -- это запись значений параметров, а потом (08.07.2019: но ПОСЛЕ их записи!) запрос чтения базового канала.

      А "параметризованная запись" -- аналогично, запись значений параметров, а затем запись в базовый канал.

    • Что, если НЕ трогать драйверный API, а реализовывать отработку PZREAD и PZWRITE прямо на уровне cxsd_hw?
    08.07.2019: еще вчера мысль про "реализовать PZREAD/PZWRITE на уровне cxsd_hw, не трогая API драйверов" показалась скорее безумноватой.

    А по здравому размышлению -- оно просто нереализовабельно.

    • Хотя бы потому, что возможны длительные отсрочки в выполнении записи параметров (и не забываем про remdrv).
    • Да и если в момент получения PZ-запроса часть каналов-параметров ещё находится в состоянии "в процессе записи", то тут же отправить следующий запрос на запись никак нельзя.

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

    После обеда: кстати, можно 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" -- цепочка вложенных полей.
      • Такое -- вполне реализовабельно: оный "getfield()" обладает всей информацией о структуре композитного типа и может провести все проверки. Правда, операция будет дико медленная -- это по факту режим работы, как у интерпретируемого языка.
      • Но это всё же очень неудобно. Удобнее было бы всё-таки прямое маппирование на нативные композитные типы языка.
      • Но при прямом маппировании будут заморочки с определением выравнивания -- у разных языков (и даже с разными ключами компиляции) оно может различаться.
      • @презентация-Левичева-о-клистроне: а можно выпендриться: не угадывать из библиотек, как именно расположит компилятор поля, а генерить и "свои" devlist-описания типов, и языковые (для C -- в виде .h-файлов) из неких специальных исходников. Примерно как при работе с транспьютерами определения констант для C и OCCAM генерились скриптом const2hdr.pl из .const-файлов.

        12.07.2019: как вариант -- в качестве "исходников" использовать именно devlist-определения, а генерить только для языков.

    • @утро, мытьё посуды: а не заглянуть ли в ASN.1 -- оно же как раз о маршаллинге.

      12.07.2019: кстати, в bigfile-0001.html ASN.1 упоминался 15-04-2007 именно в контексте "посмотреть бы, в связи с тем, как передавать данные больших каналов".

    • @утро, душ: неплохо бы композитные типы не описывать в каждой точке использования целиком-во-всех-деталях, а иметь что-то типа "typedef'а", чтоб определять "тип с таким-то именем -- это вот такая-то структура", и затем использовать уже это имя. В частности, "r1{TYPENAME}".

      И какое ключевое слово выберем?

      • Ведь "typedef" -- нельзя, оно слишком похоже на "devtype", возникнет путаница.
      • Что-нибудь с "class"? Тоже не айс -- "классы" обычно подразумевают наличие "методов", а тут это не пришей кобыле хвост.
      • Может, "composite"? Типа такого:
        composite TYPENAME {CONTENT}
      • 01.08.2019@дорога-на-работу с обеда, около ИПА: а может "datatype?

        Оно и достаточно "уникально" и не может быть ни с чем спутано, и слово "type" в нём присутствует, так что файлы с определениями типов также можно будет сваливать в types/.

    • Собственно "передача" (от драйверов серверу, по сети, от клиентов в cda/cxlib): вроде бы просто цепочка байтов, только надо гарантировать выравнивание (чтоб каждое поле выравнивалось по своей натуральной границе).

      @презентация-Левичева-о-клистроне: а вот и нет! Надо ведь производить endian conversion, так что рассматривать сложные типы как просто последовательность байтов -- никак нельзя.

    • А как указывать в devlist'ах?

      Ведь сейчас используются просто буквы, обозначающие конкретный скалярный тип -- это очень коротко; например, r1i -- 1 скалярный int32. А тут как указывать -- r1WHAT?

      Напрашивается идея, что "расширение синтаксиса" -- указывать что-то в фигурных скобках (по аналогии с описанием структур в C): r1{SPEC}.

      Неприятно то, что фигурные скобки будут плохо сочетаться с синтаксисом devtype: там ведь '{' является окончанием списка групп каналов и открывает список пространства имён. Так-то всё проходит, но очень плохо перенесёт синтаксические ошибки.

    • А ещё есть такой постулат, что для одинаковых типов желательно бы, чтоб коды cxdtype во всех клиентах были ОДИНАКОВЫМИ.
    • @презентация-Левичева-о-клистроне: кстати, теперь я, кажется, чуть лучше понимаю смысл "normative types" в EPICS4: "нормативизация" позволяет автоматически иметь на всех узлах одинаковый код типа, плюс вместо "распарсивания описания типа at-run-time" можно иметь предопределённые описания.
    • @презентация-Левичева-о-клистроне: правильно бы считать типы совпадающими, если совпадают их описания (в "нормализованном", "внутреннем" виде -- где говорится что-то типа "{int32 x; byte data[40];}", вот эти цепочки и сравнивать).

      ЕМНИП (из книги Страуструпа, что ли), C (но НЕ C++!) именно так и сравнивает типы на эквивалентность.

    • 12.07.2019: BTW, а ведь композитные типы могут иметь смысл в не-идентичных, а лишь "похожих" вариантах: например, условный тип "картинка" должен обязательно содержать поля ШИРИНА и ВЫСОТА, плюс собственно "ДВУМЕРНЫЙ МАССИВ ПИКСЕЛОВ", но конкретно РАЗМЕР этого массива может быть в разных случаях разный, и вполне осмысленна конверсия между разными размерами (например, обрезание невмещающегося, с опциональными сдвигами сверху и снизу).

      Это оно так ПО СМЫСЛУ; а насколько реализовабельно, и насколько оправданно по трудозатратам -- вопрос.

      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 также и "информацию о поле внутри канала".

    Что будет представлять из себя эта "информация о поле внутри канала" -- отдельный вопрос: вероятно, некое смещение плюс ссылка на определение типа. При указании на "целый" канал -- какое-нибудь специальное значение, говорящее "всё целиком".

    Соответственно, как это "будет функционировать":

    1. CxsdDbResolveName()/CxsdHwResolveChan() получат дополнительный return-параметр -- эта самая "информация". И их "клиенты" должны будут хранить её в дополнение к идентификатору канала.
    2. Мониторинг же организуется так: подписка на обновления делается всегда на ВЕСЬ канал целиком, а уж frontend'ы (и прочие insrv) пусть берут ту часть значения (по "информации"), которая их интересует.

      ЗЫ: в трактате Kay Kasemir'а утверждается, что присылаются только ИЗМЕНЕНИЯ (а не, в т.ч., структуры целиком). Уж не знаю, как это сделано (если реально сделано) -- помнится предыдущее и сравнивается? А не проще ль в СЕРВЕРЕ ловить именно событие "обновление значения", а совпадает ли значение с предыдущим или отличается -- вопрос более верхних уровней.

    17.07.2019@вечер-пультовая: Об "обращении клиента к частям композитных каналов" (которое COMPOSITE_CHANNEL.SUBFIELD.SUBSUBFIELD): в рамках cxsd_db/cxsd_hw-то поиск сделать ясно как. А вот как КЛИЕНТУ отдавать результат поиска, чтобы он под подписываться на такой "частичный канал"? Сейчас -- просто cpnt_id; а там что -- еще второй id ("subfield_id") делать?

    (EPICS решает проблему ЯВНЫМ "созданием «канала» -- аллокирует Server-Side-ID (SSID), который уже содержит любую нужную информацию.)

    Чуть позже: а ведь можно этот "2-й ID" аллокировать не cxsd_hw-wide, а лишь CONNECTION-wide -- чтобы у соединения был реестр таких "ссылок на внутрь", и клиенту отдавать номер строки в реестре. Но, раз НЕТУ никакого явного "создать канал" и "удалить канал", а есть отдельно резолвинг и отдельно использование, то такой подход чреват/сомнителен.

    18.07.2019@утро-дома: последовательные соображения на эту тему:

    • @во-время-зарядки: а можно при ЛЮБОМ УСПЕШНОМ резолвинге складировать результат в "per-connection реестре", так что последующие запросы на чтение/запись/мониторинг будут уже осмысленны.

      @ИЯФ: уже задним числом: отдельный вопрос в "составе" такого реестра -- как он должен был бы выглядеть? Ведь "номера строк" в нём осмысленны только в сочетании с hwid/cpid того канала, для которого они были получены. Поэтому если б пришлось его делать, то каждая строка должна б была содержать и hwid/cpid того канала, чтобы когда клиент пришлёт дуплет {cpid,subfield_registry_id}, то можно б было убедиться в осмысленности второго id'а.

    • @чуть-позже: а можно сделать так (этот вариант совершенно stateless и потому выглядит наиболее элегантным и безопасным): собственно "SUBFIELD_ID" чтоб был прямо индексом в том самом "линейном списке описателей", относящемся к целевому каналу.

      Краткое обсуждение такого варианта:

      • Плюсом тут является то, что этот "subfield_id" (в сочетании с hwid канала) даёт совершенно однозначную ссылку на конкретное внутреннее поле, и не требует заведения никаких дополнительных "реестров".
      • Проверка его валидности крайне проста: что его значение НЕ превышает количества элементов в списке описателей минус 1.
      • С другой стороны, такой подход накладывает дополнительное ограничение на формат/структуру "линейного списка описателей": в том
        1. ДОЛЖНЫ быть строки не только на каждое "простое" ("атомарное"?) поле -- скаляр или массив, но также и на каждое композитное поле.

          Цикл "конверсии/добычи" такие строки будет просто пропускать.

          Пара замечаний:

          1. Вопрос, ЧТО будет в таких строках?

            Ну ясно, что какой-то спец.флажок, показывающий циклу, что при конверсии эту строку надо пропустить.

            А ещё? Очевидно, необходимо количество под-элементов. Но также, видимо, потребуется и общая длина последующего "линейного списка описателей", относящегося к этому полю (не считая его собственной строки). Поскольку, например, для композитного типа такого вида:

            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) соответственно.
          2. Отдельный бонус: поскольку в каждом "линейном списке описателей" композитного типа в 0-й позиции будет стоять "описатель всей структуры", то правило "subfield_id==0 означает всю структуру" из просто условной договорённости превращается в автоматически работающее.
        2. НЕ ДОЛЖНО быть никаких лишних/незначащих/декоративных строк. Чтобы КАЖДЫЙ subfield_id в диапазоне от 0 (весь канал) до num_subfield_ids-1 являлся бы ссылкой на какое-нибудь поле.
      • П.1 в предыдущем списке автоматом решает и проблему "что, если какой-то клиент захочет, подписавшись на ЦЕЛЫЙ композитный канал, взять из него КУСОЧЕК, ТОЖЕ КОМПОЗИТНЫЙ?" (см. от 17-07-2019 ниже).

    Второй вариант ("чуть-позже") выглядит очень красивым и решает все возможные проблемы. Если станем делать -- то стоит придерживаться именно его.

    15.07.2019@вечер-выходя-из-ИЯФа: к вопросу о том, как "описывать" сложные композитные структуры и как производить сериализацию/десериализацию ("маршаллинг") -- всё обдумывал, особенно после прочтения в описании протокола pvAccess, что они не используют padding вовсе. И осенило:

    • ЛЮБЫЕ вложенные структуры любой сложности (кроме, конечно, переменного размера (вроме как в конце)) в конечном итоге могут быть сведены к ЛИНЕЙНОМУ списку из описателей {dtype,nelems,offset_in_data,offset_in_record}.
    • В случае с отсутствием padding'а (как в pvAccess) становится ненужным offset_in_data, поскольку всё идёт линейно/последовательно.
    • Такие списки (таблицы) могут использоваться также прямо для проведения конверсии: нужно кроме вышеперечисленных полей (которые описание) иметь также поле-указатель-на-функцию, куда прописывать указатель на "выполнятеля конверсии".

      Это для КОНВЕРСИИ -- если надо переделывать порядок байт.

      А если просто нужно распотрошить содержимое пришедшего пакета в некий тип-структуру, то достаточно и того описателя: просто в цикле линейно пройтись по той таблице, делая memcpy() указанных количеств байт по указанным смещениям.

      ...да, есть некоторый вопрос "а как быть с variable-length полями", но тут что-нибудь придумываемо (проблема скорее техническая, чем идеологическая).

    • Немного в сторону, насчёт union'ов:
      • У нас в CX union'ы никак не предусмотрены. И я вообще в принципе не вижу в них никакого смысла: ведь в ЯВУ они используются преимущественно для "преобразования данных" -- когда нужно, например, доступиться к double как к массиву байтов; тем самым как бы обходится несовершенство языка. На уровне СУ же -- таких потребностей вроде нет.
      • В качестве же "регулярных структур данных" (т.е., живущих дольше внутренностей одной функции, где делается какое-нибудь преобразование данных) union'ы как таковые -- зло: они представляют собой неопределённые данные, чей тип неизвестен.
      • Случай, когда можно смириться с чем-то типа union -- это "запись с вариантами" в Pascal: там поле-вариант ("переключатель") сразу определяет тип данных, содержащихся в вариативном поле.
      • Конкретно "записи с вариантами" используют для экономии места: чтобы держать в одной записи разнородные данные, которые в любом случае осмысленны только по раздельности. Хотя это применение сомнительное.

        16.07.2019: и я лично таким пользуюсь нечасто: в 4cx/src/*.h слово "union" встречается всего 7 раз. Но все эти разы -- тип содержимого НЕ меняется после первоначальной инициализации и постоянен на всё время существования объекта.

      • Насколько я понимаю, применяемый в "EPICS4" union -- в обязательном порядке имеет поле-переключатель, так что это скорее именно "запись с вариантами". Но вот ЗАЧЕМ он вообще нужен -- для меня загадка.
    • Кстати, по реализации: можно иметь в рамках экземпляра программы "реестр" используемых типов, содержащий дуплеты {строковое описание типа, указатель на такую таблицу}, и при регистрации нового канала с композитным типом производить поиск по реестру, и если такая строка уже найдена, то заносить в refinfo_t индекс этой строки.

      И строки ТОЛЬКО ДОБАВЛЯТЬ, никогда не удаляя.

    • Ещё мысль о "регистрации композитных типов".
      • Суть проблемы: есть
        1. C'шный тип -- как-то разложенный на память, на что могут влиять правила выравнивания как конкретной архитектуры, так и ключи/умолчания конкретного компилятора.
        2. "Сетевой" тип -- то, что передаётся (в как-то сериализованном виде) системой управления по сети.

        И эту пару нужно как-то "сдружить" -- чтоб чтение канала такого типа раскладывало бы полученные данные по полям C'шной переменной, а запись в такой канал готовила бы сериализованное представление, годное для отправки серверу.

        (Кстати, на стороне сервера -- в связке сервер<->драйверы -- стоит аналогичная проблема.)

      • Так вот, идея решения: программа сначала должна выполнять "регистрацию" всех используемых типов.
        • А регистрация заключается в передаче таблицы, содержащей список строк вида {ИмяПоля,dtype,nelems}, где ИмяПоля -- цепочка "путь к полю" из имён, разделённых точками ('.').
        • Тогда cda сможет составить свою внутреннюю таблицу для преобразования (тот самый "линейный список описателей").

        Т.е., "связывание" полей C'шных типов с сетевыми будет производиться по именам полей.

        16.07.2019: явно в этой схеме не хватает ещё имён самих типов. Как-то бы и их пристегнуть.

      Идея пока не особо детально продуманна, главный смысл -- именно в самом факте "требования предварительной регистрации".

    • 16.07.2019: кстати, вот эти "линейные списки описателей" и могут использоваться как те самые «описания (в "нормализованном", "внутреннем" виде...)» (выше от 11-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.

    • Сдвигаем номера версий:
      • cdaP.h::CDA_DAT_P_MODREC_VERSION_MAJOR: 1->2.
      • cdaP.h::CDA_FLA_P_MODREC_VERSION_MAJOR: 1->2.
      • cxsd_driver.h::CXSD_DRIVER_MODREC_VERSION_MAJOR: 10->11.
      • cxsd_driver.h::CXSD_LAYER_MODREC_VERSION_MAJOR: 2->3.
      • cxsd_frontend.h::CXSD_FRONTEND_MODREC_VERSION_MAJOR: 2->3.
      • remdrv_proto_v4.h::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 также трогать не требуется, поскольку бинарное содержимое передаваемых данных не меняется.

    • remdrv_drv.c: как указано 08-07-2019, изменения в ProcessInData() (ветвь REMDRV_C_DATA) и remdrv_rw_p() (в частности, поскольку теперь требуется endian conversion, введён dt2snd[]).
    • remcxsd_driver.c: как указано 08-07-2019, изменения в ReturnDataSet() (добавлен dt2snd[]; тут не ради конверсии, а для гарантированно-32битности) и ProcessPacket() (ветвь REMDRV_C_WRITE).
    • cx.h: собственно сменен тип (предыдущая дата файла была 04-01-2017).

    Проверять теперь надо!

    23.09.2019: ну проверил. Фиг -- не работает, нули кажутся. Разбираемся --

    • Оказалось, что в remdrv_drv.c::ProcessInData() было забыто делать endian-conversion приходящих dtype'ов.

      Добавлено.

    • Заодно было замечено, что, в отличие от всех прочих получаемых данных, dtype'ы возвращаются прямо из dt_ptr.

      Исправлено -- введён массив cxdtype_t dt2ret[], куда и "копируются" dtype'ы, и уже он передаётся ReturnDataSet()'у.

    Теперь -- да, работает. Ура!

  • 28.08.2018: а что вообще дальше в глобальном плане?

    Ну доведён CXv4 до очень юзабельного состояния, а дальше-то что, какое направление развития?

    То, чем я сейчас занимаюсь -- rfsyn, клистроны, сварка; hwinfo -- это мелочи. Нужно думать глобально (2-е правило Шварценеггера -- "never think small, think BIG!").

    А вот фиг знает, нет определённой ясности.

    Всё, что приходит в голову -- "расширение", в виде поддержки других платформ и протоколов: порт под Windows, плагины для EPICS и Tango.

    28.08.2018: чуть более формально:

    1. Ближайшее:
      • Возможность смены типов (dtype,nelems) каналов в cda.
      • Возможность отдачи драйвером диапазона значений канала -- SetChanRange().
      • Перевести cxsd_hw_channels[] со статического массива на malloc()/realloc()'ируемый буфер.
      • (30.08.2018@пляж) поддержка выражений -- (...) -- в ppf4td_get_double().
    2. Глобальное:
      • cda_d_epics
      • cda_d_tango
      • Параметризуемые чтения и записи; чтоб по эффекту получился аналог EPICS'ных композитных типов ("положа руку на сердце", эта фича -- реально крупный плюс, у нас отсутствующий).
      • (19.10.2018) "Как правильно устраивать опрос не-autoupdated каналов чтения?". Точно понадобится с этим разобраться для параметризуемых запросов.
      • (14.09.2018) Порт под Windows?

    27.08.2018@утро-дорога-на-работу, по Лаврентьева: ну очевидно же, что надо подизучить EPICS и Tango, с троякой целью:

    1. Разобраться в них.
    2. Сделать плагины cda_d_epics, cda_d_tango; по возможности -- cxsd_fe_epics, cxsd_fe_tango.
    3. Составить сравнительную табличку фич разных СУ.

      Например, строительные блоки: CX и Tango -- устройства; EPICS -- каналы; имена: CX и Tango -- экземпляры устройств заводятся целиком, со всеми каналами, EPICS -- каналы объявляются поштучно, ссылаясь на устройства косвенно (и могут быть каналы, в устройствах существующие, но никак не адресуемые); CX и EPICS -- есть alias'ы, Tango -- вроде нету (?); модель обмена: CX -- асинхронная, EPICS -- рекорды в основном пассивны, процессятся только по запросу "свыше", Tango -- синхронная (хотя и с исключениями, вроде нотификаций/event'ов и особенностями, вроде таймаутов).

    И понятно, что по результатам разработки cda_d_{epics,tango} вполне можно написать статью на ICALEPCS-2019.

    ...возможно, также и обзорную статью по той сравнительной таблице.

  • 30.01.2025: CXv5 еще некоторые соображения -- о множественных/групповых/векторных запросах.

    30.01.2025@вечер, лыжи: цепочка событий/мыслей была такой:

    • @в районе "апекса" (где отвод на 10-ку) 1-й 5-ки: исходные предпосылки:
      • @перед апексом: при обдумывании того, как бы модифицировать драйвер frolov_d16_drv.c так, чтобы в сохранённых пресетах можно было менять не только значения A и B, но и V, возникла идея "оптимизации": при изменении любого из троицы (A,B,V) не выполнять возврат сразу, а взводить флаг "было модифицировано", чтобы в конце frolov_d16_rw_p() скопом вернуть те значения, что изменились -- тем самым избегая множественных возвратов V при изменении A и B скопом.
      • @сразу после апекса: но потом сообразил, что это бессмысленно, т.к. в реальности НИКОГДА драйверам не приходят "групповые" запросы на запись.

        Потому, что:

        • внутренний API сервера -- cxsd_driver -- групповой (в обоих направлениях);
        • _старый_ CX-протокол и API cxlib -- тоже;
        • а вот API cda и _новый_ CX-протокол и API cxlib (на handle'ах) -- НЕТ!
    • "Синтез":
      • @1-я 5-ка, то ли 1-я, то ли 2-я горка вниз после "апекса": решение этой проблемы -- и возможность выполнять групповые операции на ВСЕЙ цепочке от клиента до драйвера -- может стать годным шагом в сторону CXv5.

        Только как это сделать?

      • @2-я 3-ка, где-то за 1км до конца, в районе последней крупной горки вниз: ну "как как" -- да прямо групповой вызов и сделать!

        Тем более, что локинг как раз с группами и работает.

        ...хотя, кажется, там до сих пор адресация по hwid, а не по handle.

        31.01.2025: неа, именно по handle, т.к. локинга есть 2 варианта: "старый" cx_rq_l_o() по hwid и "новый" cx_ch_rq_l_o() по chnd. Но главный прикол в другом: обе эти операции работают с ОДИНОЧНЫМ каналом, а НЕ с группой!

  • 06.03.2025: уже с полмесяца бродит в голове идея (а она ещё раньше не записана? 09.03.2025: 23-08-2008) "иметь НЕСКОЛЬКО периодических событий с разными периодами, как EPICS'ные scan period'ы, чтоб не плодить по таймауту на каждый драйвер, а привязывать их к этим общесерверным периодам" (60s, 10s, 1s, 0.1s).

    И такое, возможно, не даст никаких плюсов, т.к. накладные расходы cxscheduler'а, возможно, не так уж велики, но вот то, что аналогичные действия ото всех драйверов будут совсем одновременно, а не распределены хоть сколько-то -- точно минус.

    06.03.2025: реализовывать-то вряд ли будем, но чисто аргументация:

    1. При большом количестве АНАЛОГИЧНЫХ устройств (CAN, Modbus, ...) каждое из них регистрирует некий реально периодический таймаут -- hbt, alv и т.п.

      В результате получается изрядное количество по сути дублей.

      А если ввести понятие "периодического события", на которое каждый драйвер устройства может подписаться, то дубли исчезнут, что, возможно, даст выигрыш.

    2. Смысл конкретных значений таких периодов:
      • 60s -- ALIVE_SECONDS у CAN-устройств.
      • 10s -- RECONNECT_DELAY у всех TCP-драйверов (remdrv, Modbus, ...).

        ...хотя это НЕ периодическое событие!

        Но и ALIVE_USECS у UDP-устройств (ottcam, slbpm, будущий Карпов).

      • 1s -- да мало ли что занадобится, но очевидная вещь.
      • 0.1s -- HEARTBEAT_FREQ=10 у CAN-устройств и прочих advdac-based.

    09.03.2025: а как в EPICS -- по информации из "EPICS Process Database Concepts: Scanning Specification":

    • Стандартный список там -- 10s, 5s, 2s, 1s, .5s, .2s, .1s.
    • И там оно в принципе реконфигурябельно -- можно заменить содержимое menuScan.dbd своим списком (только отсортированным).

    Некоторые соображения на случай гипотетической реализации:

    • Можно было бы иметь фиксированный список (0.1, ... 60).
    • А можно как-то конфигурябельный; только тогда вопрос будет в том, чтобы используемый сервером список периодов удовлетворял бы потребностям используемых драйверов.
    • Но можно и вовсе "автоматический": по каждому запросу от драйвера ищется соответствие, а при его отсутствии -- заводится новый период в "пуле".

      При отключении последнего юзера такого периода -- надо период из пула удалять.

    • Какой может быть "API периодов" -- сходу не совсем ясно, но вряд ли это мега-проблема.

    Нашел, обсуждалась аналогичная вещь 23-08-2008 в разделе "ВременнАя схема работы".

Доступ издалека:
  • 27.06.2019: ключевые слова -- "CX-gateway" и "name server": возможны ли такие вещи в CX?

    27.06.2019: вернулся к этим 2 концепциям после разговора с ЕманоФедей на тему о том, как доступаться к СУ клиентам, работающим в ДРУГОЙ сети, а не в локальной сети СУ.

    Оная "другая сеть" может доступаться к сети СУ через VPN, коий бывает 2 видов:

    1. На клиентском узле интерфейс, имеющий адрес в сети СУ (т.н. "tap"): тут проблем ноль -- те же broadcast'ы будут ходить как при пребывании в сети СУ "вживую".
    2. На клиентском узле есть МАРШРУТИЗАЦИЯ в сеть СУ (т.н. "tun"): тут никакие broadcast'ы ходить уже не будут, т.к. они не должны выходить за пределы broadcast-домена.

    Собственно, какие могут быть глобально-философские средства решения проблемы второго варианта:

    1. А возможен ли "CX-gateway"? Ведь оная возможность -- сродни виртуализовабельности компьютерных архитектур, и является признаком зрелости, ортогональности и прочей положительности системы.
    2. Можно ль всё-таки сделать некий "nameserver", который бы принимал запросы по unicast-адресу, и отдавал бы ответы, в которых сам адрес отвечающего не использовался бы, а передавался бы адрес (IP? имя?) в протоколе?

      Тогда для клиентов из другой сети, но с маршрутизацией (но НЕ NAT!) всё бы работало.

2???:
1???:

11.07.2019: что надо прочитать побыстрее: