Documentation

Python Getting Started

The libosdp Python package wraps the LibOSDP C library so you can build and test OSDP devices without writing any C. Commands and events are typed dataclasses, so your editor completes the fields, a type checker catches a wrong one before you run, and an out-of-range value raises where you wrote it instead of being silently truncated into a packet.

Install

Pre-built wheels are published on PyPI for Linux, macOS, and Windows:

pip install libosdp

That is all most users need. If no wheel matches your platform, pip builds from source, which needs a C compiler — see Build and Install for the per-OS toolchain requirements and other install methods.

A channel to talk over

LibOSDP does not own the transport. You implement a Channel that moves bytes over your wire (serial, TCP, etc.) and hand it to the device. A pyserial-backed channel looks like this:

import serial
from osdp import Channel

class SerialChannel(Channel):
    def __init__(self, device: str, speed: int = 115200):
        self.dev = serial.Serial(device, speed, timeout=0)

    def read(self, max_bytes: int) -> bytes:
        return self.dev.read(max_bytes)

    def write(self, data: bytes) -> int:
        return self.dev.write(data)

    def flush(self) -> None:
        self.dev.flush()

    def __del__(self):
        self.dev.close()

Both snippets below use this SerialChannel (pip install pyserial).

Control Panel mode

A Control Panel (CP) drives one or more Peripheral Devices (PDs). Each PD is described by a PDInfo. start() spawns a background thread that services the bus for you, so the application never has to poll on a timer.

from osdp import ControlPanel, PDInfo, KeyStore, LogLevel, LEDColor, commands, events

channel = SerialChannel("/dev/ttyUSB0")

# KeyStore.gen_key() provisions a random Secure Channel Base Key (SCBK).
pd_info = [
    PDInfo(101, channel, scbk=KeyStore.gen_key()),
]

cp = ControlPanel(pd_info, log_level=LogLevel.Debug)
cp.start()
cp.sc_wait_all()   # block until the secure channel is up

# An LED command carries a temporary block, a permanent block, or both.
# Counts are in units of 100ms; on_count and off_count cannot both be zero.
led_cmd = commands.LED(
    reader=1,
    led_number=0,
    permanent=commands.PermanentLEDParams(
        on_color=LEDColor.Red,
        off_color=LEDColor.Black,
        on_count=10,
        off_count=10,
    ),
)

while True:
    # Each event is its own type, so match gives you the right fields for free.
    match cp.get_event(pd_info[0].address):
        case events.CardRead(data=data):
            print(f"CP: card {data.hex()}")
        case events.KeyPress(data=keys):
            print(f"CP: keypad {keys!r}")

    cp.submit_command(pd_info[0].address, led_cmd)

The full CP surface — every method, the event queue, and completion callbacks — is on the Control Panel page. The commands you submit are documented in Commands; the events you read in Events.

Peripheral Device mode

A Peripheral Device (PD) answers a CP. Describe it with PDInfo, declare its capabilities, and hand LibOSDP a command handler. Return None to accept a command, return a command to answer it inline, or raise NakError to decline it.

from osdp import (
    Capability, Command, NakCode, NakError, PDCapabilities, PDInfo,
    PeripheralDevice, StatusReportType, commands, events, CardFormat,
)

channel = SerialChannel("/dev/ttyUSB0")

# scbk=None puts the PD in install mode (it accepts a key from the CP).
pd_info = PDInfo(101, channel, scbk=None)

pd_cap = PDCapabilities([
    (Capability.OutputControl, 1, 1),
    (Capability.LEDControl, 2, 1),
])

def command_handler(cmd: Command) -> Command | None:
    match cmd:
        case commands.Status(type=report_type):
            return commands.Status(type=report_type, report=read_inputs())
        case commands.BioRead():
            raise NakError(NakCode.BioType)
    return None

pd = PeripheralDevice(pd_info, pd_cap, command_handler=command_handler)
pd.start()
pd.sc_wait()

# For the raw card formats the length is in BITS; this one is ASCII, so bytes.
card_event = events.CardRead(
    reader_no=1,
    direction=1,
    format=CardFormat.ASCII,
    data=bytes([9, 1, 9, 2, 6, 3, 1, 7, 7, 0]),
)

while True:
    pd.submit_event(card_event)

The full PD surface and the command-handler contract are on the Peripheral Device page.

Full examples

Runnable command-line versions of both apps live in the LibOSDP repo:

Where to next