Перейти к содержанию
Материалы раздела

OSDP

OSDP в Lua RTOS

Модуль поддерживает обе роли OSDP: контроллер доступа Control Panel (CP) и периферийное устройство Peripheral Device (PD). В штатной прошивке ODNFC-RS485 доступны OSDP, Modbus и UART; для других устройств набор модулей может отличаться.

Наличие обоих модулей в прошивке не означает, что они могут одновременно использовать один физический UART/RS-485 интерфейс. Приложение должно выбрать один протокол, освободить UART перед переключением и не запускать параллельно циклы обмена Modbus и OSDP на одной шине.

Версию модуля можно проверить во время выполнения:

local version, source = osdp.version()
print(version, source) -- 3.2.5, v3.2.5 (v3.2.5)

Lua API использует индексы PD с единицы. Например, первый элемент списка, переданного в osdp.cp({...}), имеет индекс 1 в остальных методах CP.

Канал связи

recv и необязательный flush должны быть неблокирующими. recv возвращает доступные байты строкой или ""/nil, когда данных нет. Нельзя ждать полный пакет внутри recv: модуль сам собирает его из фрагментов.

send должен принять весь переданный буфер за один вызов и вернуть #buf (либо nil после успешной отправки); частичная запись не поддерживается. uart.write() в режиме RS-485 ждёт окончания передачи перед снятием DE, поэтому не добавляйте в обработчик дополнительные ожидания или циклы повторной отправки.

channel_id должен быть положительным. Для всех PD на одной многоточечной шине укажите одинаковый ID, а для независимых шин — разные ID. Это не адрес PD, а идентификатор общего канала связи.

local ch = {
  channel_id = 1,
  send = function(_, buf)
    uart.write(2, buf)
    return #buf
  end,
  recv = function(_, max_len)
    return uart.read(2, "*nl", 0, max_len) or ""
  end,
  flush = function()
    uart.flush(2)
  end,
}

Вызывайте refresh() обеих ролей не реже одного раза в 50 мс. В примерах использован период 10 мс. Обработчик Lua выполняется синхронно внутри refresh(), поэтому он не должен надолго задерживать выполнение программы.

Общая конфигурация PD

Каждая конфигурация содержит:

  • send, recv, необязательный flush и data для канала;
  • channel_id > 0;
  • address от 0 до 126; 127 зарезервирован для широковещательных команд;
  • baud_rate: 9600, 19200, 38400, 57600, 115200 или 230400;
  • необязательные name, flags и scbk.

scbk — бинарная строка ровно из 16 байт. PD без scbk создаётся только при явном OSDP_FLAG_INSTALL_MODE, чтобы случайно не оставить устройство на стандартном SCBK-D. Для обычной защищённой работы обе стороны должны получить один и тот же SCBK и флаг OSDP_FLAG_ENFORCE_SECURE. OSDP_FLAG_INSTALL_MODE предназначен только для контролируемого ввода в эксплуатацию через SCBK-D; его нельзя сочетать с OSDP_FLAG_ENFORCE_SECURE. На стороне CP режим установки требует новый scbk: после сеанса с SCBK-D модуль передаст этот ключ в PD и заново проверит защищённое соединение.

Захват пакетов в PCAP не поддерживается, поэтому OSDP_FLAG_CAPTURE_PACKETS нельзя передавать при создании контекста. Флаг OSDP_FLAG_ALLOW_EMPTY_ENCRYPTED_DATA_BLOCK нужен только для совместимости с ошибочными PD и не должен использоваться без необходимости.

CP

local osdp = osdp or require("osdp")

local scbk = "0123456789ABCDEF"
local cp = osdp.cp({
  {
    send = ch.send,
    recv = ch.recv,
    flush = ch.flush,
    data = ch,
    channel_id = 1,
    address = 101,
    baud_rate = 9600,
    flags = osdp.OSDP_FLAG_ENFORCE_SECURE,
    scbk = scbk,
    name = "reader-1",
  },
})

cp:on_event(function(pd_idx, ev)
  if ev.type == osdp.OSDP_EVENT_CARDREAD then
    print("CARD", pd_idx, ev.reader, ev.format, ev.length, ev.data)
  elseif ev.type == osdp.OSDP_EVENT_STATUS then
    print("STATUS", ev.status_type, ev.count, ev.report)
  end
end)

while true do
  cp:refresh()
  tmr.delayms(10)
end

Ключ в примере предназначен только для демонстрации. Для рабочего устройства используйте уникальный случайный ключ из 16 байт и храните его как секрет.

Методы CP:

  • cp:status() — таблица признаков доступности PD;
  • cp:sc_status() — таблица состояний Secure Channel;
  • cp:pd_id(index) — полученный идентификатор PD или nil;
  • cp:capability(index, function_code) — заявленная возможность PD или nil;
  • cp:send_command(index, command) — команда поставлена в очередь;
  • cp:flush_commands(index) — число удалённых ожидающих команд;
  • cp:set_enabled(index, boolean) и cp:is_enabled(index) — управление PD;
  • cp:modify_flag(index, flags, set) — изменение поддерживаемых флагов во время работы;
  • cp:on_event(callback) — обработчик событий;
  • cp:file_ops(index, callbacks) и cp:file_status(index) — передача файлов;
  • cp:teardown() — закрытие контекста.

send_command поддерживает output, led, buzzer (buzz), text, comset, keyset, file_tx, mfg и status. Команды разрешены после перехода PD в доступное состояние. Для keyset поле типа ключа называется key_type, потому что type содержит имя команды:

assert(cp:send_command(1, {
  type = "keyset",
  key_type = 1,
  data = new_scbk16,
}))

Необязательное поле command_flags принимает только OSDP_CMD_FLAG_BROADCAST. Широковещательная команда предназначена для контролируемой шины с единственным PD и игнорируется при OSDP_FLAG_ENFORCE_SECURE. В обработчике PD исходные флаги доступны в cmd.command_flags.

Ротация ключа через keyset выполняется внутри уже активного Secure Channel. Числовые поля проверяются при вызове: байтовые значения не могут быть отрицательными или больше 255, timer не может превышать 65535, адрес COMSET лежит в диапазоне 0..126, а его скорость — одна из 9600, 19200, 38400, 115200 или 230400.

Поля таблиц команд:

typeПоля
outputoutput (0..255), control_code (0..6), timer (0..65535, единица 100 мс)
ledreader, led; таблицы temporary и permanent с control_code, on_count, off_count, on_color, off_color; у temporary также есть timer
buzzer / buzzreader, control_code, on_count, off_count, repeat, все 0..255
textreader, control_code, temp_time, row, col, text до 32 байт
comsetaddress 0..126 и поддержанная baud_rate без 57600
keysetkey_type = 1, data ровно 16 байт; нужен активный Secure Channel
file_txцелый file_id, необязательный flags; для отмены — OSDP_CMD_FILE_TX_FLAG_CANCEL
mfg24-битный vendor_code (или vendor), байтовый command, data до 63 байт
statusstatus_type: input, output, local, remote либо соответствующая константа

В led временный control_code лежит в 0..2, постоянный — в 0..1, цвета — от OSDP_LED_COLOR_NONE до OSDP_LED_COLOR_WHITE. Для текста стандартные control codes — 1..4; нулевое значение оставлено для совместимости.

Если задан OSDP_FLAG_ENABLE_NOTIFICATION, on_event также получает OSDP_EVENT_NOTIFICATION. В нём доступны notification_type, arg0 и arg1. Это локальные диагностические события модуля, а не сообщения от PD.

PD

local pd = osdp.pd({
  send = ch.send,
  recv = ch.recv,
  flush = ch.flush,
  data = ch,
  channel_id = 1,
  address = 101,
  baud_rate = 9600,
  flags = osdp.OSDP_FLAG_ENFORCE_SECURE,
  scbk = "0123456789ABCDEF",
  name = "lua-pd",
  id = {
    vendor_code = 0x00A1B2,
    model = 1,
    version = 1,
    serial_number = 1,
    firmware_version = 0x000100,
  },
  capabilities = {
    {function_code = osdp.OSDP_PD_CAP_OUTPUT_CONTROL,
     compliance_level = 1, num_items = 2},
    {function_code = osdp.OSDP_PD_CAP_READER_LED_CONTROL,
     compliance_level = 1, num_items = 1},
  },
})

pd:on_command(function(cmd)
  if cmd.id == osdp.OSDP_CMD_STATUS then
    return {report = string.char(0, 0)}
  end
  return true -- ACK; false/nil создаёт NAK
end)

pd:notify({
  type = "cardread",
  reader = 0,
  format = osdp.OSDP_CARD_FMT_RAW_WIEGAND,
  length = 26,
  data = string.char(0x12, 0x34, 0x56, 0x70),
})

Методы PD:

  • pd:refresh() — выполнить один неблокирующий шаг протокола;
  • pd:status() — признак доступности, pd:sc_status() — состояние Secure Channel;
  • pd:on_command(callback) — принять команды CP; true или таблица означает ACK, false/nil — NAK, целое значение используется как явный код ответа;
  • pd:notify(event) — поставить событие в очередь;
  • pd:flush_events() — удалить ещё не отправленные события и вернуть их число;
  • pd:file_ops(1, callbacks) и pd:file_status(1) — передача файлов;
  • pd:teardown() — освободить контекст; после этого остальные методы выдают ошибку OSDP context is closed.

pd:notify поддерживает cardread, keypress, status и mfgrep. pd:flush_events() удаляет события, которые ещё не отправлены. В обработчик команды COMSET после успешного подтверждения приходит дополнительный OSDP_CMD_COMSET_DONE: только после него приложение должно сохранить новые параметры связи в энергонезависимой памяти.

Для cardread поддерживаются форматы необработанных данных OSDP_CARD_FMT_RAW_UNSPECIFIED и OSDP_CARD_FMT_RAW_WIEGAND. Устаревший ASCII-формат не поддерживается. length задаётся в битах, по умолчанию равен #data * 8 и не может описывать больше бит, чем передано в data.

Поля событий pd:notify:

typeПоля и ограничения
cardreadreader 0..255, формат необработанных данных format, необязательные length в битах и direction 0/1, data 1..64 байт
keypressreader 0..255, data 1..64 байт
statusstatus_type, report или совместимые count и mask
mfgrep24-битный vendor_code/vendor, command 0..255, data до 127 байт

Поле direction сохранено для совместимости, но ответ OSDP с необработанными данными его не передаёт, поэтому принимающая сторона получает 0.

Отчёты о состоянии

Для каждого входа или выхода используется один байт состояния. Полученное событие и команда status содержат:

  • status_type;
  • count;
  • report — бинарную строку длиной count;
  • mask — совместимое старое представление первых 32 элементов: ненулевой байт превращается в установленный бит.

При отправке предпочтительно задавать report. Старые {count, mask} также принимаются, но не могут выразить значения, отличные от 0 и 1, и ограничены 32 элементами.

Для input и output длина отчёта должна совпадать с num_items заявленной возможности PD. Состояние local всегда содержит два байта (tamper, power), а remote — один байт power.

Первичное назначение SCBK

В контролируемом окружении создайте PD без ключа с OSDP_FLAG_INSTALL_MODE. На CP задайте желаемый новый 16-байтный scbk и тот же флаг. Модуль установит Secure Channel на стандартном SCBK-D, автоматически передаст новый SCBK командой KEYSET и повторно установит канал уже с ним. Успех подтверждается cp:sc_status()[index] == true; это состояние не считает сеанс на SCBK-D защищённым. После ввода в эксплуатацию уберите install mode и перезапустите обе стороны с новым ключом и OSDP_FLAG_ENFORCE_SECURE.

Не оставляйте install mode включённым в штатной эксплуатации: SCBK-D является общеизвестным ключом и годится только для первичной настройки в физически контролируемой среде.

Данные производителя

Поле command сохранено для совместимости. В OSDP 2.2 это первый байт данных производителя: при отправке он добавляется перед data, а при приёме отделяется от него. Поэтому для самого поля data доступно на один байт меньше, чем для всего блока данных производителя.

Передача файлов

До команды file_tx зарегистрируйте обработчики файла на нужной стороне:

local payload = "file contents"

cp:file_ops(1, {
  open = function(file_id, size) return 0, #payload end,
  read = function(max_len, offset)
    return string.sub(payload, offset + 1, offset + max_len)
  end,
  write = function(chunk, offset) return #chunk end,
  close = function() return 0 end,
})

assert(cp:send_command(1, {type = "file_tx", file_id = 1, flags = 0}))
local state = cp:file_status(1) -- {size=..., offset=...} или nil

file_status возвращает nil, если обработчики ещё не зарегистрированы или передача не активна.

После каждого успешного вызова open модуль вызывает close ровно один раз: при штатном завершении, ошибке/отмене передачи, замене обработчиков через file_ops или закрытии OSDP-контекста через teardown/GC.

Экспортируемые константы

Модуль экспортирует:

  • флаги OSDP_FLAG_ENFORCE_SECURE, OSDP_FLAG_INSTALL_MODE, OSDP_FLAG_IGN_UNSOLICITED, OSDP_FLAG_ENABLE_NOTIFICATION и OSDP_FLAG_ALLOW_EMPTY_ENCRYPTED_DATA_BLOCK;
  • все OSDP_CMD_*, включая OSDP_CMD_COMSET_DONE, и события OSDP_EVENT_CARDREAD, OSDP_EVENT_KEYPRESS, OSDP_EVENT_STATUS, OSDP_EVENT_MFGREP, OSDP_EVENT_NOTIFICATION;
  • типы уведомлений OSDP_EVENT_NOTIFICATION_COMMAND, SC_STATUS и PD_STATUS;
  • OSDP_STATUS_REPORT_INPUT, OUTPUT, LOCAL, REMOTE;
  • форматы необработанных данных карты, восемь OSDP_LED_COLOR_* от NONE до WHITE и OSDP_CMD_FILE_TX_FLAG_CANCEL, OSDP_CMD_FLAG_BROADCAST;
  • capability codes OSDP_PD_CAP_CONTACT_STATUS_MONITORING, OUTPUT_CONTROL, CARD_DATA_FORMAT, READER_LED_CONTROL, READER_AUDIBLE_OUTPUT, READER_TEXT_OUTPUT, TIME_KEEPING, CHECK_CHARACTER_SUPPORT, COMMUNICATION_SECURITY, RECEIVE_BUFFERSIZE, LARGEST_COMBINED_MESSAGE_SIZE, SMART_CARD_SUPPORT, READERS, BIOMETRICS, SECURE_PIN_ENTRY и OSDP_VERSION.