├── doc ├── _static │ ├── css │ │ └── style.css │ ├── js │ │ └── script.js │ └── img │ │ ├── icon.png │ │ ├── logo.png │ │ └── wireshark.png ├── requirements.txt ├── changelog.rst ├── license.rst ├── libosdp │ ├── index.rst │ ├── compatibility.rst │ ├── cross-compiling.rst │ └── debugging.rst ├── protocol │ ├── index.rst │ ├── faq.rst │ └── introduction.rst ├── api │ ├── index.rst │ ├── miscellaneous.rst │ ├── channel.rst │ ├── pd-info.rst │ ├── event-structure.rst │ ├── peripheral-device.rst │ ├── command-structure.rst │ └── control-panel.rst ├── osdpctl │ ├── index.rst │ └── introduction.rst ├── README.md ├── conf.py.in ├── CMakeLists.txt └── index.rst ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── publish-platformio.yml │ ├── cross-plaform-build.yml │ ├── create-release.yml │ ├── build-ci.yml │ └── publish-pypi.yml ├── zephyr ├── module.yml ├── src │ └── osdp.c ├── Kconfig └── CMakeLists.txt ├── tests ├── pytest │ ├── requirements.txt │ ├── README.md │ ├── run.sh │ ├── _test_connection_topologies.py │ ├── test_sc_keys.py │ ├── test_data.py │ ├── test_events.py │ ├── test_hotplug.py │ ├── test_status.py │ └── test_file_tx.py └── unit-tests │ ├── README.md │ ├── CMakeLists.txt │ ├── test.h │ └── test-cp-fsm.c ├── python ├── MANIFEST.in ├── .gitignore ├── pyproject.toml ├── osdp │ ├── __init__.py │ ├── channel.py │ ├── key_store.py │ ├── helpers.py │ ├── constants.py │ └── peripheral_device.py ├── README.md └── osdp_sys │ └── module.h ├── examples ├── rust │ └── README.md ├── c │ ├── Makefile │ ├── README.md │ ├── CMakeLists.txt │ ├── cp_app.c │ └── pd_app.c ├── cpp │ ├── Makefile │ ├── CMakeLists.txt │ ├── cp_app.cpp │ └── pd_app.cpp ├── platformio │ ├── README.md │ ├── cp.ino │ └── pd.ino └── python │ ├── README.md │ ├── cp_app.py │ └── pd_app.py ├── .gitmodules ├── scripts ├── gen-compile-commands.sh ├── make-html-docs.sh ├── clang-format-check.sh ├── run_tests.sh ├── install-git-hooks.sh ├── run_pytests.sh ├── make-release.sh └── clang-format-diff.py ├── platformio ├── platformio.cpp ├── osdp_export.h └── osdp_config.h ├── cmake ├── UseLibOSDP.cmake ├── LibOSDPConfig.cmake.in ├── FindSphinx.cmake ├── _FindMbedTLS.cmake ├── BuildType.cmake ├── GitSubmodules.cmake ├── GitInfo.cmake ├── AddCCompilerFlag.cmake └── CreatePackages.cmake ├── misc └── libosdp.pc.in ├── .editorconfig ├── shell.nix ├── .vscode ├── c_cpp_properties.json ├── settings.json ├── tasks.json └── launch.json ├── src ├── osdp_diag.h ├── crypto │ ├── tinyaes.c │ ├── mbedtls.c │ ├── openssl.c │ └── tinyaes_src.h ├── osdp_diag.c ├── osdp_config.h.in └── osdp_file.h ├── .gitignore ├── library.json ├── SECURITY.md ├── CONTRIBUTING.md ├── Makefile ├── include ├── osdp_export.h └── osdp.hpp ├── CMakeLists.txt ├── .clang-format └── CODE_OF_CONDUCT.md /doc/_static/css/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/_static/js/script.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: sidcha 2 | -------------------------------------------------------------------------------- /zephyr/module.yml: -------------------------------------------------------------------------------- 1 | name: libosdp 2 | -------------------------------------------------------------------------------- /tests/pytest/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | -------------------------------------------------------------------------------- /python/MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include vendor * 2 | -------------------------------------------------------------------------------- /python/.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | build/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-rtd-theme 3 | breathe 4 | -------------------------------------------------------------------------------- /doc/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | .. include:: ../CHANGELOG 5 | -------------------------------------------------------------------------------- /doc/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | .. include:: ../LICENSE 5 | :literal: 6 | -------------------------------------------------------------------------------- /tests/unit-tests/README.md: -------------------------------------------------------------------------------- 1 | # Libosdp Tests 2 | 3 | Really poor man's unit testing harness. 4 | -------------------------------------------------------------------------------- /doc/_static/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goToMain/libosdp/HEAD/doc/_static/img/icon.png -------------------------------------------------------------------------------- /doc/_static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goToMain/libosdp/HEAD/doc/_static/img/logo.png -------------------------------------------------------------------------------- /examples/rust/README.md: -------------------------------------------------------------------------------- 1 | See: https://github.com/goToMain/libosdp-rs/tree/master/libosdp/examples 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "utils"] 2 | path = utils 3 | url = https://github.com/goToMain/c-utils.git 4 | -------------------------------------------------------------------------------- /doc/_static/img/wireshark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goToMain/libosdp/HEAD/doc/_static/img/wireshark.png -------------------------------------------------------------------------------- /python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 57.2.0"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /scripts/gen-compile-commands.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p build 4 | pushd build 5 | cmake .. 6 | cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 .. 7 | mv compile_commands.json .. 8 | popd 9 | -------------------------------------------------------------------------------- /doc/libosdp/index.rst: -------------------------------------------------------------------------------- 1 | LibOSDP 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | build-and-install 8 | cross-compiling 9 | secure-channel 10 | debugging 11 | compatibility 12 | -------------------------------------------------------------------------------- /doc/protocol/index.rst: -------------------------------------------------------------------------------- 1 | Protocol Description 2 | ==================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | introduction 8 | commands-and-replies 9 | packet-structure 10 | pd-capabilities 11 | faq 12 | -------------------------------------------------------------------------------- /doc/api/index.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | control-panel 8 | peripheral-device 9 | pd-info 10 | miscellaneous 11 | command-structure 12 | event-structure 13 | channel 14 | -------------------------------------------------------------------------------- /platformio/platformio.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | 9 | int64_t osdp_millis_now() 10 | { 11 | return (int64_t)millis(); 12 | } 13 | -------------------------------------------------------------------------------- /doc/osdpctl/index.rst: -------------------------------------------------------------------------------- 1 | osdpctl - Tool 2 | ============== 3 | 4 | .. warning:: 5 | This tool has been deprecated. The future plan is to rewrite it in Rust, but 6 | there is no ETA for it. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | introduction 12 | configuration 13 | -------------------------------------------------------------------------------- /cmake/UseLibOSDP.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | add_definitions(${LIBOSDP_DEFINITIONS}) 8 | include_directories(${LIBOSDP_INCLUDE_DIR}) 9 | link_directories(${LIBOSDP_LIBRARY_DIR}) 10 | -------------------------------------------------------------------------------- /tests/pytest/README.md: -------------------------------------------------------------------------------- 1 | # Run Tests 2 | 3 | `make check` on cmake builds will invoke pytest correctly. During development, 4 | it might be useful to run an individual test (instead of everything). To do so, 5 | 6 | ``` 7 | PYTHONPATH=../../build/python/ python3 -m pytest -vv -s test_events.py::test_event_input 8 | ``` 9 | -------------------------------------------------------------------------------- /zephyr/src/osdp.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | int64_t osdp_millis_now(void) 11 | { 12 | return (int64_t) k_uptime_get(); 13 | } 14 | -------------------------------------------------------------------------------- /misc/libosdp.pc.in: -------------------------------------------------------------------------------- 1 | prefix=@CMAKE_INSTALL_PREFIX@ 2 | exec_prefix=${prefix} 3 | includedir=${prefix}/include 4 | libdir=${exec_prefix}/lib 5 | 6 | Name: @PROJECT_NAME@ 7 | Description: @PROJECT_DESCRIPTION@ 8 | URL: @PROJECT_URL@ 9 | Version: @PROJECT_VERSION@ 10 | Libs: -L${libdir} -l@LIB_TARGET@ 11 | Cflags: -I${includedir} 12 | -------------------------------------------------------------------------------- /platformio/osdp_export.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef OSDP_EXPORT_H_ 8 | #define OSDP_EXPORT_H_ 9 | 10 | #define OSDP_EXPORT 11 | #define OSDP_NO_EXPORT 12 | #define OSDP_DEPRECATED_EXPORT 13 | 14 | #endif /* OSDP_EXPORT_H_ */ 15 | -------------------------------------------------------------------------------- /examples/c/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | ROOT_DIR ?= ../.. 8 | BUILD_DIR ?= $(ROOT_DIR)/build 9 | 10 | all: 11 | gcc -I$(ROOT_DIR)/include cp_app.c -o cp_sample -L$(BUILD_DIR)/lib -losdp 12 | gcc -I$(ROOT_DIR)/include pd_app.c -o pd_sample -L$(BUILD_DIR)/lib -losdp 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | indent_style = tab 8 | indent_size = 8 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # 4 space indentation 15 | [*.{py.in,py,md,cmake,rs,lua,json}] 16 | indent_style = space 17 | indent_size = 4 18 | -------------------------------------------------------------------------------- /examples/cpp/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | ROOT_DIR ?= ../.. 8 | BUILD_DIR ?= $(ROOT_DIR)/build 9 | 10 | all: 11 | g++ -std=c++0x -I$(ROOT_DIR)/include cp_app.cpp -o cp_sample -L$(BUILD_DIR)/lib -losdp 12 | g++ -std=c++0x -I$(ROOT_DIR)/include pd_app.cpp -o pd_sample -L$(BUILD_DIR)/lib -losdp 13 | -------------------------------------------------------------------------------- /scripts/make-html-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPTS_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 4 | ROOT_DIR="${SCRIPTS_DIR}/../" 5 | 6 | pushd "${ROOT_DIR}/doc" 7 | rm -rf .venv __pycache__ 8 | python3 -m venv .venv 9 | source ./.venv/bin/activate 10 | pip install -r requirements.txt 11 | 12 | cmake -B build .. 13 | cmake --build build -t html_docs 14 | mv build/doc/sphinx/ . 15 | rm -rf build 16 | mv sphinx build 17 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | pkgs.mkShell { 4 | buildInputs = with pkgs; [ 5 | # lib 6 | cmake 7 | gcc 8 | gnumake 9 | python3 10 | 11 | # secure channel 12 | openssl 13 | 14 | # doc 15 | doxygen 16 | python3Packages.breathe 17 | python3Packages.sphinx 18 | python3Packages.sphinx_rtd_theme 19 | 20 | # examples 21 | python3Packages.pyserial 22 | 23 | # others 24 | git 25 | ]; 26 | } 27 | -------------------------------------------------------------------------------- /cmake/LibOSDPConfig.cmake.in: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | @PACKAGE_INIT@ 8 | 9 | include("${CMAKE_CURRENT_LIST_DIR}/LibOSDPTargets.cmake") 10 | 11 | if(@OpenSSL_FOUND@) 12 | include(CMakeFindDependencyMacro) 13 | find_dependency(OpenSSL) 14 | endif() 15 | 16 | if(@MbedTLS_FOUND@) 17 | include(CMakeFindDependencyMacro) 18 | find_dependency(MbedTLS) 19 | endif() 20 | -------------------------------------------------------------------------------- /cmake/FindSphinx.cmake: -------------------------------------------------------------------------------- 1 | #Look for an executable called sphinx-build 2 | find_program(SPHINX_EXECUTABLE 3 | NAMES sphinx-build 4 | DOC "Path to sphinx-build executable") 5 | 6 | include(FindPackageHandleStandardArgs) 7 | 8 | #Handle standard arguments to find_package like REQUIRED and QUIET 9 | find_package_handle_standard_args(Sphinx 10 | "Failed to find sphinx-build executable" 11 | SPHINX_EXECUTABLE) 12 | -------------------------------------------------------------------------------- /examples/platformio/README.md: -------------------------------------------------------------------------------- 1 | # PlatformIO Arduino Examples 2 | 3 | LibOSDP provides a native port for [PlatformIO][1]. If you have it already 4 | setup, you can set `lib_deps=https://github.com/goToMain/libosdp` in your 5 | `platformio.ini` and build your CP/PD application as you would build any 6 | PlatformIO project. 7 | 8 | Note: These examples are provided only for demonstration purposes. A real world 9 | CP/PD device would have to do a lot more. 10 | 11 | 1: https://docs.platformio.org/en/latest/what-is-platformio.html 12 | -------------------------------------------------------------------------------- /examples/c/README.md: -------------------------------------------------------------------------------- 1 | # LibOSDP Usage Samples 2 | 3 | These samples are meant to act as a reference for the API calls of LibOSDP. They 4 | demonstrate the right initialization and refresh workflows the CP/PD to work 5 | properly but are not working examples. 6 | 7 | Assuming you have already built LibOSDP, you can run the following commands to 8 | to compile `cp_sample` and `pd_sample`. 9 | 10 | ```sh 11 | gcc cp_app.c -o cp_sample -l osdp -I ../../include/ -L ../../build/lib 12 | gcc pd_app.c -o pd_sample -l osdp -I ../../include/ -L ../../build/lib 13 | ``` 14 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # LibOSDP Documentation 2 | 3 | These files here are source files for sphinx documentation. Individual files and 4 | links (rendered by github) will not work as you might expect. 5 | 6 | Please visit [https://libosdp.sidcha.dev/][1] for html documentation. 7 | 8 | ## Build the html doc locally 9 | 10 | From the repo root, 11 | 12 | ```sh 13 | # install dependencies 14 | sudo apt install python3-sphinx 15 | pip3 install -r doc/requirements.txt 16 | 17 | # build 18 | mkdir build && cd build && cmake .. 19 | make html_docs 20 | ``` 21 | 22 | [1]: https://libosdp.sidcha.dev/ 23 | -------------------------------------------------------------------------------- /doc/api/miscellaneous.rst: -------------------------------------------------------------------------------- 1 | Miscellaneous 2 | ============= 3 | 4 | Debugging and Diagnostics 5 | ------------------------- 6 | 7 | .. doxygenfunction:: osdp_logger_init 8 | 9 | .. doxygenfunction:: osdp_get_version 10 | 11 | .. doxygenfunction:: osdp_get_source_info 12 | 13 | Status 14 | ------ 15 | 16 | .. doxygenfunction:: osdp_get_status_mask 17 | 18 | .. doxygenfunction:: osdp_get_sc_status_mask 19 | 20 | 21 | File Operations 22 | --------------- 23 | 24 | .. doxygenstruct:: osdp_file_ops 25 | :members: 26 | 27 | .. doxygenfunction:: osdp_file_register_ops 28 | 29 | .. doxygenfunction:: osdp_get_file_tx_status 30 | -------------------------------------------------------------------------------- /doc/api/channel.rst: -------------------------------------------------------------------------------- 1 | Communication Channel 2 | ===================== 3 | 4 | LibOSDP uses a _channel_ communicate with the devices. It is upto the 5 | users to define a channel and pass it to `osdp_{cp,pd}_setup()` method. 6 | A cahnnel is defined as: 7 | 8 | .. code:: c 9 | 10 | struct osdp_channel { 11 | void *data; 12 | int id; 13 | read_fn_t recv; 14 | write_fn_t send; 15 | flush_fn_t flush; 16 | }; 17 | 18 | .. doxygenstruct:: osdp_channel 19 | :members: 20 | 21 | .. doxygentypedef:: osdp_write_fn_t 22 | 23 | .. doxygentypedef:: osdp_read_fn_t 24 | 25 | .. doxygentypedef:: osdp_flush_fn_t 26 | -------------------------------------------------------------------------------- /cmake/_FindMbedTLS.cmake: -------------------------------------------------------------------------------- 1 | find_path(MBEDTLS_INCLUDE_DIRS mbedtls/ssl.h) 2 | 3 | find_library(MBEDTLS_LIBRARY mbedtls) 4 | find_library(MBEDX509_LIBRARY mbedx509) 5 | find_library(MBEDCRYPTO_LIBRARY mbedcrypto) 6 | 7 | set(MBEDTLS_LIBRARIES "${MBEDTLS_LIBRARY}" "${MBEDX509_LIBRARY}" "${MBEDCRYPTO_LIBRARY}") 8 | 9 | include(FindPackageHandleStandardArgs) 10 | 11 | find_package_handle_standard_args( 12 | MbedTLS 13 | "Failed to find MbedTLS executable" 14 | MBEDTLS_INCLUDE_DIRS 15 | MBEDTLS_LIBRARY 16 | MBEDX509_LIBRARY 17 | MBEDCRYPTO_LIBRARY 18 | ) 19 | 20 | mark_as_advanced( 21 | MBEDTLS_INCLUDE_DIRS 22 | MBEDTLS_LIBRARY 23 | MBEDX509_LIBRARY 24 | MBEDCRYPTO_LIBRARY 25 | ) 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /cmake/BuildType.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | set(BUILD_TYPE "Release") 8 | if(EXISTS "${CMAKE_SOURCE_DIR}/.git") 9 | set(BUILD_TYPE "Debug") 10 | endif() 11 | 12 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 13 | message(STATUS "Build type unspecified, setting to '${BUILD_TYPE}'") 14 | set(CMAKE_BUILD_TYPE "${BUILD_TYPE}" CACHE STRING 15 | "Choose the type of build" FORCE) 16 | 17 | # Set the possible values of build type for cmake-gui 18 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" 19 | "MinSizeRel" "RelWithDebInfo") 20 | endif() 21 | -------------------------------------------------------------------------------- /python/osdp/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | from .control_panel import ControlPanel 8 | from .peripheral_device import PeripheralDevice 9 | from .key_store import KeyStore 10 | from .constants import ( 11 | LibFlag, Command, CommandLEDColor, CommandFileTxFlags, Event, EventNotification, 12 | CardFormat, Capability, LogLevel, StatusReportType 13 | ) 14 | from .helpers import PdId, PDInfo, PDCapabilities 15 | from .channel import Channel 16 | 17 | __author__ = 'Siddharth Chandrasekaran ' 18 | __copyright__ = 'Copyright 2021-2024 Siddharth Chandrasekaran' 19 | __license__ = 'Apache License, Version 2.0 (Apache-2.0)' 20 | -------------------------------------------------------------------------------- /examples/c/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | cmake_minimum_required(VERSION 3.14) 8 | project(osdp_c_sample) 9 | 10 | if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) 11 | # This sample is built individually so try to locate an installed 12 | # version of libosdp. 13 | find_package(libosdp NO_MODULE REQUIRED) 14 | endif() 15 | 16 | set(CP_SAMPLE c_cp_sample) 17 | set(PD_SAMPLE c_pd_sample) 18 | 19 | add_executable(${CP_SAMPLE} cp_app.c) 20 | add_executable(${PD_SAMPLE} pd_app.c) 21 | 22 | target_link_libraries(${CP_SAMPLE} PRIVATE $,osdp,osdpstatic>) 23 | target_link_libraries(${PD_SAMPLE} PRIVATE $,osdp,osdpstatic>) 24 | -------------------------------------------------------------------------------- /tests/pytest/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PYTEST_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | pushd ${PYTEST_DIR} 5 | 6 | echo "[-] Creating an isolated environment.." 7 | rm -rf __pycache__/ 8 | rm -rf .venv ../../python/{build,dist,libosdp.egg-info,vendor} 9 | python3 -m venv .venv 10 | source ./.venv/bin/activate 11 | pip install --upgrade pip 12 | 13 | echo "[-] Installing dependencies.." 14 | pip install -r requirements.txt 15 | 16 | echo "[-] Installing libosdp.." 17 | pip install ../../python 18 | 19 | if [[ "$1" == "-n" ]]; then 20 | echo "To run tests do:" 21 | echo 22 | echo "cd ${PYTEST_DIR}" 23 | echo "source .venv/bin/activate" 24 | echo "pytest -vv --show-capture=all [test_.py]" 25 | exit 26 | fi 27 | 28 | echo "[-] Running tests capturing all output.." 29 | pytest -vv --show-capture=all 30 | -------------------------------------------------------------------------------- /.github/workflows/publish-platformio.yml: -------------------------------------------------------------------------------- 1 | name: Publish PlatformIO 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_run: 6 | workflows: ["Create Release"] 7 | types: [completed] 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout the repository 15 | uses: actions/checkout@v4 16 | with: 17 | submodules: recursive 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: '3.x' 23 | 24 | - name: Install PlatformIO CLI 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install platformio 28 | 29 | - name: Publish to PlatformIO Registry 30 | env: 31 | PLATFORMIO_AUTH_TOKEN: ${{ secrets.PLATFORMIO_AUTH_TOKEN }} 32 | run: | 33 | pio package publish --type library --no-interactive --owner sidcha 34 | -------------------------------------------------------------------------------- /python/osdp/channel.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | class Channel(abc.ABC): 4 | def __init__(self) -> None: 5 | self.id = 0 # TODO 6 | 7 | @abc.abstractmethod 8 | def read(self, max_bytes: int) -> bytes: 9 | """ 10 | Read at most `max_bytes` bytes from the stream and return the data as 11 | a byte array. 12 | """ 13 | return [] 14 | 15 | @abc.abstractmethod 16 | def write(self, buf: bytes) -> int: 17 | """ 18 | Send buf (as much as possible) over the raw stream and return the 19 | number of bytes that were actually sent. This has be to <= len(buf). 20 | """ 21 | return len(buf) 22 | 23 | @abc.abstractmethod 24 | def flush(self) -> None: 25 | """ 26 | If the underlying stream supports it, perform a flush here. If not, 27 | can return without doing anything. 28 | """ 29 | pass 30 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/include", 7 | "${workspaceFolder}/src", 8 | "${workspaceFolder}/utils/include", 9 | "${workspaceFolder}/utils/src", 10 | "${workspaceFolder}/tests/unit-tests", 11 | "${workspaceFolder}/build/include", 12 | "/usr/include/python3.13" 13 | ], 14 | "defines": [], 15 | "compilerPath": "/usr/bin/gcc", 16 | "cStandard": "c17", 17 | "cppStandard": "gnu++17", 18 | "intelliSenseMode": "linux-gcc-x64", 19 | "compileCommands": "${workspaceFolder}/build/compile_commands.json", 20 | "configurationProvider": "ms-vscode.cmake-tools" 21 | } 22 | ], 23 | "version": 4 24 | } -------------------------------------------------------------------------------- /doc/api/pd-info.rst: -------------------------------------------------------------------------------- 1 | PD Info 2 | ======= 3 | 4 | ``osdp_pd_info_t`` is a user provided structure which describes how the PD has to 5 | be setup. In CP mode, the app described the PDs it would like to communicate 6 | with using an array of ``osdp_pd_info_t`` structures. 7 | 8 | .. doxygenstruct:: osdp_pd_info_t 9 | :members: 10 | 11 | Setup Flags 12 | ----------- 13 | 14 | OSDP setup in CP or PD mode can be infulenced by the following flags (set in 15 | ``osdp_pd_info_t::flags``). Some of them are effective only in CP or PD mode; see 16 | individual flag documentation below. 17 | 18 | .. doxygendefine:: OSDP_FLAG_ENFORCE_SECURE 19 | 20 | .. doxygendefine:: OSDP_FLAG_INSTALL_MODE 21 | 22 | .. doxygendefine:: OSDP_FLAG_IGN_UNSOLICITED 23 | 24 | .. doxygendefine:: OSDP_FLAG_ENABLE_NOTIFICATION 25 | 26 | .. doxygendefine:: OSDP_FLAG_CAPTURE_PACKETS 27 | 28 | .. doxygendefine:: OSDP_FLAG_ALLOW_EMPTY_ENCRYPTED_DATA_BLOCK 29 | -------------------------------------------------------------------------------- /src/osdp_diag.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef _OSDP_PCAP_H_ 8 | #define _OSDP_PCAP_H_ 9 | 10 | #include "osdp_common.h" 11 | 12 | #if defined(OPT_OSDP_PACKET_TRACE) || defined(OPT_OSDP_DATA_TRACE) 13 | 14 | void osdp_packet_capture_init(struct osdp_pd *pd); 15 | void osdp_packet_capture_finish(struct osdp_pd *pd); 16 | void osdp_capture_packet(struct osdp_pd *pd, uint8_t *buf, int len); 17 | 18 | #else 19 | 20 | static inline void osdp_packet_capture_init(struct osdp_pd *pd) 21 | { 22 | ARG_UNUSED(pd); 23 | } 24 | 25 | static inline void osdp_packet_capture_finish(struct osdp_pd *pd) 26 | { 27 | ARG_UNUSED(pd); 28 | } 29 | 30 | static inline void osdp_capture_packet(struct osdp_pd *pd, 31 | uint8_t *buf, int len) 32 | { 33 | ARG_UNUSED(pd); 34 | ARG_UNUSED(buf); 35 | ARG_UNUSED(len); 36 | } 37 | 38 | #endif 39 | 40 | #endif /* _OSDP_PCAP_H_ */ 41 | 42 | -------------------------------------------------------------------------------- /scripts/clang-format-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) 2021-2025 Siddharth Chandrasekaran 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | 8 | refs_str="$(git rev-list --parents -n1 HEAD)"; refs=($refs_str) 9 | 10 | merge_head=${refs[0]} 11 | base_head=${refs[1]} 12 | branch_head=${refs[2]} 13 | 14 | files=$(git diff --name-only ${base_head} | grep -E ".*\.(cpp|cc|c\+\+|cxx|c|h|hpp)$") 15 | if [ -z "${files}" ]; then 16 | echo "No source code to check for formatting." 17 | exit 0 18 | fi 19 | 20 | diff=$(git diff -U0 ${base_head} -- ${files} | ./scripts/clang-format-diff.py -p1) 21 | 22 | if [[ -z "${diff}" ]]; then 23 | echo "All source code in PR properly formatted." 24 | exit 0 25 | fi 26 | 27 | echo -e "\nFound formatting errors!\n\n" 28 | 29 | echo "${diff}" 30 | 31 | echo "\n\nWarning: found some clang format issues!" 32 | echo "You can run 'clang-format --style=file -i FILE_YOU_MODIFED' fix these issues!" 33 | exit 0 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | THIS IS A TEMPELTE 11 | ------------------ 12 | Please **remove** sections that you are not filling data into (such as this one) 13 | 14 | **Describe the bug** 15 | A description of what the bug is. 16 | 17 | **Expected behavior** 18 | A description of what you expected to happen. 19 | 20 | **Observed behavior** 21 | A description of what happened. 22 | 23 | **Additional context** 24 | Add any other context about the problem such as a non standard setup, local 25 | modifications you've done, etc., please mention them here. 26 | 27 | **A Comprehensive Log file** 28 | In some cases, it might help to provide a comprehensive log file (see how you 29 | can generate this [here][1]). This is an optional requirement, so use your best 30 | judgment and decide if it is needed. 31 | 32 | [1]: https://libosdp.sidcha.dev/libosdp/debugging.html 33 | -------------------------------------------------------------------------------- /cmake/GitSubmodules.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | find_package(Git QUIET) 8 | 9 | if(GIT_FOUND AND 10 | EXISTS "${PROJECT_SOURCE_DIR}/.git" AND 11 | NOT EXISTS "${PROJECT_SOURCE_DIR}/utils/CMakeLists.txt" 12 | ) 13 | message(STATUS "Submodule checkout") 14 | execute_process( 15 | COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive 16 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 17 | RESULT_VARIABLE GIT_SUBMOD_RESULT 18 | ) 19 | if(NOT GIT_SUBMOD_RESULT EQUAL "0") 20 | message(FATAL_ERROR "git submodule update --init --recursive failed with ${GIT_SUBMOD_RESULT}, please checkout submodules") 21 | endif() 22 | endif() 23 | 24 | if(NOT EXISTS "${PROJECT_SOURCE_DIR}/utils/CMakeLists.txt") 25 | message(FATAL_ERROR "The submodules were not downloaded! GIT_SUBMODULE was turned off or failed. Please update submodules and try again.") 26 | endif() 27 | -------------------------------------------------------------------------------- /examples/python/README.md: -------------------------------------------------------------------------------- 1 | # Python Examples 2 | 3 | To run the samples, you have to install the following python packages: 4 | 5 | ```sh 6 | python3 -m pip install pyserial libosdp 7 | ``` 8 | 9 | Then you can run start the CP/PD service as, 10 | 11 | ```sh 12 | ./examples/python/cp_app.py /dev/ttyUSB0 --baudrate 115200 13 | # (or) 14 | ./examples/python/pd_app.py /dev/ttyUSB0 --baudrate 115200 15 | ``` 16 | 17 | ## Note: 18 | 19 | To test how the CP and PD would potentially interact with each other, you can 20 | ask socat to create a pair of psudo terminal devices that are connected to each 21 | other and use that as a serial channel for libosdp communications. 22 | 23 | To do this run: 24 | 25 | ```sh 26 | socat pty,raw,echo=0,nonblock,link=/tmp/ttyS0 pty,raw,echo=0,nonblock,link=/tmp/ttyS1 27 | ``` 28 | 29 | While the above command is running, you can use `/tmp/ttyS0` and `/tmp/ttyS1` to 30 | start your CP and PD app as, 31 | 32 | ``` 33 | ./examples/python/cp_app.py /tmp/ttyS0 34 | ./examples/python/pd_app.py /tmp/ttyS1 35 | ``` 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | .DS_Store 55 | .vscode/ 56 | build/ 57 | build-*/ 58 | xcode/ 59 | tags 60 | 61 | ## Lean make related stuffs 62 | osdp_config.h 63 | osdpctl/osdpctl 64 | config.make 65 | 66 | ## python 67 | /python/vendor 68 | 69 | ## pytests 70 | .pytest_cache/ 71 | __pycache__/ 72 | .cache/ 73 | *.pyc 74 | .venv/ 75 | 76 | ## Rust 77 | Cargo.lock 78 | target/ 79 | 80 | ## clangd 81 | compile_commands.json 82 | 83 | ## patch 84 | *.orig 85 | *.rej 86 | *.patch 87 | -------------------------------------------------------------------------------- /examples/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | cmake_minimum_required(VERSION 3.14) 8 | project(osdp_cpp_sample) 9 | 10 | if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) 11 | # This sample is built individually so try to locate an installed 12 | # version of libosdp. 13 | find_package(libosdp NO_MODULE REQUIRED) 14 | endif() 15 | 16 | set(CMAKE_CXX_STANDARD 11) 17 | set(CP_SAMPLE cpp_cp_sample) 18 | set(PD_SAMPLE cpp_pd_sample) 19 | 20 | add_executable(${CP_SAMPLE} cp_app.cpp) 21 | add_executable(${PD_SAMPLE} pd_app.cpp) 22 | 23 | # CPP sample does not build in some old compilers and causes relase checks to 24 | # fail. So let's exclude this from all for now; CI will still test this target. 25 | set_target_properties(${CP_SAMPLE} PROPERTIES EXCLUDE_FROM_ALL TRUE) 26 | set_target_properties(${PD_SAMPLE} PROPERTIES EXCLUDE_FROM_ALL TRUE) 27 | 28 | target_link_libraries(${CP_SAMPLE} PRIVATE $,osdp,osdpstatic>) 29 | target_link_libraries(${PD_SAMPLE} PRIVATE $,osdp,osdpstatic>) -------------------------------------------------------------------------------- /.github/workflows/cross-plaform-build.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | name: Cross Platform Build 8 | 9 | on: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | cross_platform_build: 14 | name: Build on ${{ matrix.os }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | # macos-13 is an intel runner, macos-14 is apple silicon 19 | os: [ubuntu-latest, macos-13, macos-14, windows-latest] 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | submodules: recursive 24 | - name: Configure 25 | run: cmake -B build -DCMAKE_BUILD_TYPE=Debug . 26 | - name: Build 27 | run: cmake --build build --parallel 8 28 | - name: Run unit-tests 29 | run: cmake --build build --parallel 8 --target check-ut 30 | - name: Pack built binaries 31 | run: cmake --build build --target package 32 | - name: Upload artifacts 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: libosdp-${{ matrix.os }}-binaries 36 | path: build/artifacts/ 37 | -------------------------------------------------------------------------------- /doc/libosdp/compatibility.rst: -------------------------------------------------------------------------------- 1 | compatibility 2 | ------------- 3 | 4 | This page lists commercial PD/CP devices with which the LibOSDP counterpart was 5 | tested to work (or not work). 6 | 7 | The data found here is mostly sourced from the users and should be treated with 8 | a grain of salt as both sides of this comparison may evolve to have different 9 | versions and hence varying behaviour. Nevertheless, it's a good starting point. 10 | 11 | +----------------------------+----------------------------+----------+---------------------------------------------+ 12 | | CP Device | PD Device | Status | Comments | 13 | +============================+============================+==========+=============================================+ 14 | | LibOSDP CP | LibOSDP PD | Working | Native support; CI ensure that this works | 15 | +----------------------------+----------------------------+----------+---------------------------------------------+ 16 | 17 | If want to report your own findings here, send an email to sidcha.dev@gmail.com 18 | or send a PR updating this doc on github. 19 | -------------------------------------------------------------------------------- /cmake/GitInfo.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | execute_process( 8 | COMMAND git -C ${CMAKE_CURRENT_LIST_DIR} log --pretty=format:'%h' -n 1 9 | OUTPUT_VARIABLE GIT_REV 10 | ERROR_QUIET 11 | OUTPUT_STRIP_TRAILING_WHITESPACE 12 | ) 13 | 14 | if ("${GIT_REV}" STREQUAL "") 15 | set(GIT_REV "") 16 | set(GIT_DIFF "") 17 | set(GIT_TAG "") 18 | set(GIT_BRANCH "None") 19 | else() 20 | execute_process( 21 | COMMAND git -C ${CMAKE_CURRENT_LIST_DIR} diff --quiet --exit-code 22 | RESULT_VARIABLE RETURN_CODE 23 | OUTPUT_STRIP_TRAILING_WHITESPACE 24 | ERROR_QUIET 25 | ) 26 | if(RETURN_CODE AND NOT RETURN_CODE EQUAL 0) 27 | set(GIT_DIFF "+") 28 | endif() 29 | execute_process( 30 | COMMAND git -C ${CMAKE_CURRENT_LIST_DIR} describe --exact-match --tags 31 | OUTPUT_VARIABLE GIT_TAG ERROR_QUIET 32 | OUTPUT_STRIP_TRAILING_WHITESPACE 33 | ) 34 | execute_process( 35 | COMMAND git -C ${CMAKE_CURRENT_LIST_DIR} rev-parse --abbrev-ref HEAD 36 | OUTPUT_VARIABLE GIT_BRANCH 37 | OUTPUT_STRIP_TRAILING_WHITESPACE 38 | ) 39 | string(SUBSTRING "${GIT_REV}" 1 7 GIT_REV) 40 | endif() 41 | -------------------------------------------------------------------------------- /scripts/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | SCRIPTS_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 6 | ROOT_DIR="${SCRIPTS_DIR}/../" 7 | 8 | function run_make_check() { 9 | echo "[-] Running make check" 10 | pushd ${ROOT_DIR} 11 | rm -rf build 12 | ./configure.sh -f 13 | make check 14 | popd 15 | } 16 | 17 | function run_cmake_unit_test() { 18 | echo "[-] Running cmake unit-tests" 19 | rm -rf build 20 | cmake -B build . 21 | cmake --build build -t check-ut 22 | } 23 | 24 | function run_pytest() { 25 | echo "[-] Creating an isolated environment.." 26 | pushd ${SCRIPTS_DIR} 27 | rm -rf __pycache__/ 28 | rm -rf .venv ${ROOT_DIR}/python/{build,dist,libosdp.egg-info,vendor} 29 | python3 -m venv .venv 30 | source ./.venv/bin/activate 31 | pip install --upgrade pip 32 | 33 | echo "[-] Installing dependencies.." 34 | pushd "${ROOT_DIR}/tests/pytest" 35 | pip install -r requirements.txt 36 | 37 | echo "[-] Installing libosdp.." 38 | pip install "${ROOT_DIR}/python" 39 | 40 | echo "[-] Running tests capturing all output.." 41 | pytest -vv --show-capture=all 42 | popd 43 | } 44 | 45 | run_make_check 46 | run_cmake_unit_test 47 | run_pytest 48 | -------------------------------------------------------------------------------- /zephyr/Kconfig: -------------------------------------------------------------------------------- 1 | config LIBOSDP 2 | bool "Open Supervised Device Protocol (OSDP) driver" 3 | select RING_BUFFER 4 | select CRC 5 | select RING_BUFFER 6 | select CRYPTO 7 | select CRYPTO_MBEDTLS_SHIM 8 | select MBEDTLS 9 | select MBEDTLS_CIPHER_AES_ENABLED 10 | select MBEDTLS_CIPHER_CCM_ENABLED 11 | imply SERIAL_SUPPORT_INTERRUPT 12 | imply UART_INTERRUPT_DRIVEN 13 | imply UART_USE_RUNTIME_CONFIGURE 14 | select CRC 15 | help 16 | Add support for Open Supervised Device Protocol (OSDP) 17 | 18 | config OSDP_UART_BAUD_RATE 19 | int "OSDP UART baud rate" 20 | default 115200 21 | help 22 | OSDP defines that baud rate can be either 9600 or 38400 or 23 | 115200. 24 | 25 | config OSDP_LOG_LEVEL 26 | int "OSDP Logging Level" 27 | default 1 28 | help 29 | Set the logging level for the OSDP driver 30 | 31 | config OSDP_UART_BUFFER_LENGTH 32 | int "OSDP UART buffer length" 33 | default 256 34 | help 35 | OSDP RX and TX buffer FIFO length. 36 | 37 | config OSDP_THREAD_STACK_SIZE 38 | int "OSDP Thread stack size" 39 | default 1024 40 | help 41 | Thread stack size for osdp refresh thread 42 | 43 | config APP_LINK_WITH_OSDP 44 | bool "Make libsample header file available to application" 45 | default y 46 | depends on LIBOSDP 47 | 48 | -------------------------------------------------------------------------------- /scripts/install-git-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat > .git/hooks/pre-commit << --- 4 | #!/bin/bash 5 | 6 | changes=\$(git diff --cached --name-status) 7 | m="pre-commit error" 8 | 9 | echo "\${changes}" | while read status file; do 10 | # Release checks 11 | if [[ "\$file" == "CHANGELOG" ]] ; then 12 | git diff --cached CHANGELOG | grep -q '## TODO' && \\ 13 | echo "\$m release-check: CHANGELOG has TODOs" && exit 1 14 | echo "\${changes}" | ( ! grep -q -e '^M\\sCMakeLists.txt' ) && \\ 15 | echo "\$m release-check: Version in CMakeLists.txt not modified!" && exit 1 16 | clv="\$(git diff --cached CHANGELOG | perl -ne 'if (s/^\+v(\d+\.\d+\.\d+)/\$1/) {print;last}')" 17 | cmv="\$(git diff --cached CMakeLists.txt | perl -ne 'if (s/^\+project\(libosdp VERSION (\d+\.\d+\.\d+)\)/\$1/) {print;last}')" 18 | if [[ \$clv != "\$cmv" ]]; then 19 | echo "\$m release-check: Version mismatch! (\$clv/\$cmv)" && exit 1 20 | fi 21 | fi 22 | done 23 | 24 | files=\$(git diff --cached --name-only | grep -E ".*\.(cpp|cc|c\+\+|cxx|c|h|hpp)$") 25 | if [[ ! -z "\${files}" ]]; then 26 | git diff -U0 --cached -- \${files} | ./scripts/clang-format-diff.py -p1 27 | fi 28 | 29 | --- 30 | chmod a+x .git/hooks/pre-commit && echo "Installed hook: .git/hooks/pre-commit" 31 | -------------------------------------------------------------------------------- /doc/api/event-structure.rst: -------------------------------------------------------------------------------- 1 | Application Events 2 | ================== 3 | 4 | LibOSDP exposes the following structures thought ``osdp.h``. This document 5 | attempts to document each of its members. The following structure is used as a 6 | wrapper for all the events for convenience. 7 | 8 | .. code:: c 9 | 10 | struct osdp_event { 11 | enum osdp_event_type type; // Used to select specific event in union 12 | union { 13 | struct osdp_event_keypress keypress; 14 | struct osdp_event_cardread cardread; 15 | struct osdp_event_mfgrep mfgrep; 16 | struct osdp_status_report status; 17 | }; 18 | }; 19 | 20 | Below are the structure of each of the event structures. 21 | 22 | 23 | Key press Event 24 | --------------- 25 | 26 | .. doxygenstruct:: osdp_event_keypress 27 | :members: 28 | 29 | Card read Event 30 | --------------- 31 | 32 | .. doxygenstruct:: osdp_event_cardread 33 | :members: 34 | 35 | Manufacture specific reply Event 36 | -------------------------------- 37 | 38 | .. doxygenstruct:: osdp_event_mfgrep 39 | :members: 40 | 41 | Status report request Event 42 | --------------------------- 43 | 44 | .. doxygenstruct:: osdp_status_report 45 | :members: 46 | 47 | .. doxygenenum:: osdp_status_report_type 48 | -------------------------------------------------------------------------------- /cmake/AddCCompilerFlag.cmake: -------------------------------------------------------------------------------- 1 | # - Adds a compiler flag if it is supported by the compiler 2 | # 3 | # This function checks that the supplied compiler flag is supported and then 4 | # adds it to the corresponding compiler flags 5 | # 6 | # add_c_compiler_flag( []) 7 | # 8 | # - Example 9 | # 10 | # include(AddCCompilerFlag) 11 | # add_c_compiler_flag(-Wall) 12 | # add_c_compiler_flag(-no-strict-aliasing RELEASE) 13 | # Requires CMake 2.6+ 14 | 15 | if(__add_c_compiler_flag) 16 | return() 17 | endif() 18 | 19 | set(__add_c_compiler_flag INCLUDED) 20 | 21 | include(CheckCCompilerFlag) 22 | 23 | function(add_c_compiler_flag FLAG) 24 | string(TOUPPER "HAVE_C_FLAG_${FLAG}" SANITIZED_FLAG) 25 | string(REPLACE "+" "X" SANITIZED_FLAG ${SANITIZED_FLAG}) 26 | string(REGEX REPLACE "[^A-Za-z_0-9]" "_" SANITIZED_FLAG ${SANITIZED_FLAG}) 27 | string(REGEX REPLACE "_+" "_" SANITIZED_FLAG ${SANITIZED_FLAG}) 28 | set(CMAKE_REQUIRED_FLAGS "${FLAG}") 29 | check_c_compiler_flag("" ${SANITIZED_FLAG}) 30 | if(${SANITIZED_FLAG}) 31 | set(VARIANT ${ARGV1}) 32 | if(ARGV1) 33 | string(REGEX REPLACE "[^A-Za-z_0-9]" "_" VARIANT "${VARIANT}") 34 | string(TOUPPER "_${VARIANT}" VARIANT) 35 | endif() 36 | set(CMAKE_C_FLAGS${VARIANT} "${CMAKE_C_FLAGS${VARIANT}} ${FLAG}" PARENT_SCOPE) 37 | endif() 38 | endfunction() 39 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LibOSDP", 3 | "version": "3.1.0", 4 | "description": "A cross-platform open source implementation of IEC 60839-11-5 Open Supervised Device Protocol (OSDP). The protocol is intended to improve interoperability among access control and security products. It supports Secure Channel (SC) for encrypted and authenticated communication between configured devices.", 5 | "authors": [{ 6 | "name": "Siddharth Chandrasekaran", 7 | "email": "sidcha.dev@gmail.com" 8 | }], 9 | "license": "Apache-2.0", 10 | "keywords": ["OSDP", "LibOSDP", "CP", "PD", "ACS"], 11 | "frameworks": ["arduino"], 12 | "dependencies": [], 13 | "build": { 14 | "srcFilter": [ 15 | "+<**/*.c>", 16 | "-", 17 | "-", 18 | "-", 19 | "+<../utils/src/disjoint_set.c>", 20 | "+<../utils/src/list.c>", 21 | "+<../utils/src/logger.c>", 22 | "+<../utils/src/queue.c>", 23 | "+<../utils/src/slab.c>", 24 | "+<../utils/src/utils.c>", 25 | "+<../utils/src/crc16.c>", 26 | "+<../platformio/platformio.cpp>" 27 | ], 28 | "includeDir": ".", 29 | "flags": [ 30 | "-I include", 31 | "-I utils/include", 32 | "-I platformio", 33 | "-D __BARE_METAL__" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /doc/conf.py.in: -------------------------------------------------------------------------------- 1 | import sphinx_rtd_theme 2 | 3 | project = '@PROJECT_NAME@' 4 | copyright = '@PROJECT_YEAR@, @PROJECT_ORG@' 5 | author = '@PROJECT_AUTHOR@' 6 | release = '@PROJECT_VERSION@' 7 | 8 | master_doc = 'index' 9 | extensions = ['sphinx_rtd_theme', 'breathe'] 10 | templates_path = ['_templates'] 11 | exclude_patterns = [ 12 | '_build', 13 | 'Thumbs.db', 14 | '.DS_Store', 15 | 'conf.py.in', 16 | '*.txt' 17 | ] 18 | breathe_default_project = 'LibOSDP' # see CMakeLists.txt 19 | 20 | # -- Options for HTML output ------------------------------------------------- 21 | 22 | static_path = '@CMAKE_CURRENT_SOURCE_DIR@/_static/' 23 | 24 | html_title = '@PROJECT_NAME@ - @PROJECT_DESCRIPTION@' 25 | html_short_title = '@PROJECT_NAME@ - @PROJECT_VERSION@' 26 | html_baseurl = '@PROJECT_HOMEPAGE@' 27 | html_static_path = [ static_path ] 28 | html_logo = static_path + 'img/logo.png' 29 | html_favicon = static_path + 'img/icon.png' 30 | html_css_files = ['_static/css/style.css'] 31 | html_js_files = ['_static/js/script.js'] 32 | html_theme = 'sphinx_rtd_theme' 33 | html_copy_source = False 34 | html_theme_options = { 35 | 'logo_only': False, 36 | 'display_version': True, 37 | 'prev_next_buttons_location': 'bottom', 38 | 'style_external_links': False, 39 | # Toc options 40 | 'collapse_navigation': True, 41 | 'sticky_navigation': False, 42 | 'navigation_depth': 4, 43 | 'includehidden': True, 44 | 'titles_only': False 45 | } 46 | -------------------------------------------------------------------------------- /tests/pytest/_test_connection_topologies.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | import time 8 | from osdp import * 9 | 10 | def test_daisy_chained_pds(utils): 11 | # Created single CP-PD pair 12 | pd0_info = PDInfo(101, utils.ks.gen_key(), name='daisy_chained_pd', channel_type='unix_bus') 13 | pd1_info = PDInfo(102, utils.ks.gen_key(), name='daisy_chained_pd', channel_type='unix_bus') 14 | pd0 = utils.create_pd(pd0_info) 15 | pd1 = utils.create_pd(pd1_info) 16 | cp = utils.create_cp([ pd0_info, pd1_info ], sc_wait=True) 17 | 18 | # Exercise both the PDs while checking whether they are alive and well 19 | for _ in range(5): 20 | test_cmd = { 21 | 'command': Command.Comset, 22 | 'address': 101, 23 | 'baud_rate': 9600 24 | } 25 | assert cp.is_sc_active(101) 26 | assert cp.send_command(101, test_cmd) 27 | assert pd0.get_command() == test_cmd 28 | 29 | test_cmd = { 30 | 'command': Command.Comset, 31 | 'address': 102, 32 | 'baud_rate': 9600 33 | } 34 | assert cp.is_sc_active(102) 35 | assert cp.send_command(102, test_cmd) 36 | assert pd1.get_command() == test_cmd 37 | 38 | time.sleep(1) 39 | 40 | # Cleanup 41 | cp.teardown() 42 | pd1.teardown() 43 | pd0.teardown() 44 | -------------------------------------------------------------------------------- /examples/c/cp_app.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | /** 12 | * This method overrides the one provided by libosdp. It should return 13 | * a millisecond reference point from some tick source. 14 | */ 15 | int64_t osdp_millis_now() 16 | { 17 | return 0; 18 | } 19 | 20 | int sample_cp_send_func(void *data, uint8_t *buf, int len) 21 | { 22 | (void)(data); 23 | (void)(buf); 24 | 25 | // TODO (user): send buf of len bytes, over the UART channel. 26 | 27 | return len; 28 | } 29 | 30 | int sample_cp_recv_func(void *data, uint8_t *buf, int len) 31 | { 32 | (void)(data); 33 | (void)(buf); 34 | (void)(len); 35 | 36 | // TODO (user): read from UART channel into buf, for upto len bytes. 37 | 38 | return 0; 39 | } 40 | 41 | osdp_pd_info_t pd_info[] = { 42 | { 43 | .address = 101, 44 | .baud_rate = 115200, 45 | .flags = 0, 46 | .channel.send = sample_cp_send_func, 47 | .channel.recv = sample_cp_recv_func, 48 | .scbk = NULL, 49 | }, 50 | }; 51 | 52 | int main() 53 | { 54 | osdp_t *ctx; 55 | 56 | osdp_logger_init("osdp::cp", OSDP_LOG_DEBUG, NULL); 57 | 58 | ctx = osdp_cp_setup(1, pd_info); 59 | if (ctx == NULL) { 60 | printf("cp init failed!\n"); 61 | return -1; 62 | } 63 | 64 | while (1) { 65 | // your application code. 66 | 67 | osdp_cp_refresh(ctx); 68 | // delay(); 69 | } 70 | return 0; 71 | } 72 | -------------------------------------------------------------------------------- /src/crypto/tinyaes.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "tinyaes_src.h" 12 | 13 | void osdp_crypt_setup() 14 | { 15 | } 16 | 17 | void osdp_encrypt(uint8_t *key, uint8_t *iv, uint8_t *data, int len) 18 | { 19 | struct AES_ctx aes_ctx; 20 | 21 | if (iv != NULL) { 22 | /* encrypt multiple block with AES in CBC mode */ 23 | AES_init_ctx_iv(&aes_ctx, key, iv); 24 | AES_CBC_encrypt_buffer(&aes_ctx, data, len); 25 | } else { 26 | /* encrypt one block with AES in ECB mode */ 27 | assert(len <= 16); 28 | AES_init_ctx(&aes_ctx, key); 29 | AES_ECB_encrypt(&aes_ctx, data); 30 | } 31 | } 32 | 33 | void osdp_decrypt(uint8_t *key, uint8_t *iv, uint8_t *data, int len) 34 | { 35 | struct AES_ctx aes_ctx; 36 | 37 | if (iv != NULL) { 38 | /* decrypt multiple block with AES in CBC mode */ 39 | AES_init_ctx_iv(&aes_ctx, key, iv); 40 | AES_CBC_decrypt_buffer(&aes_ctx, data, len); 41 | } else { 42 | /* decrypt one block with AES in ECB mode */ 43 | assert(len <= 16); 44 | AES_init_ctx(&aes_ctx, key); 45 | AES_ECB_decrypt(&aes_ctx, data); 46 | } 47 | } 48 | 49 | void osdp_fill_random(uint8_t *buf, int len) 50 | { 51 | int i, rnd; 52 | 53 | for (i = 0; i < len; i++) { 54 | rnd = rand(); 55 | buf[i] = (uint8_t)(((float)rnd) / (float)RAND_MAX * 256); 56 | } 57 | } 58 | 59 | void osdp_crypt_teardown() 60 | { 61 | } -------------------------------------------------------------------------------- /tests/unit-tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | set(OSDP_UNIT_TEST osdp_unit_test) 8 | 9 | # tests can include private headers 10 | include_directories(${LIB_OSDP_INCLUDE_DIRS}) 11 | include_directories(${LIB_OSDP_PRIVATE_INCLUDE_DIRS}) 12 | 13 | # rebuild libosdp with test flag 14 | set(LIB_OSDP_TEST osdptest) 15 | add_definitions(-DUNIT_TESTING ${LIB_OSDP_DEFINITIONS}) 16 | add_library(${LIB_OSDP_TEST} STATIC EXCLUDE_FROM_ALL 17 | ${LIB_OSDP_SOURCES} ${PROJECT_SOURCE_DIR}/src/osdp_file.c) 18 | 19 | list(APPEND OSDP_UNIT_TEST_SRC 20 | test.c 21 | test-cp-phy.c 22 | test-cp-fsm.c 23 | test-file.c 24 | test-commands.c 25 | test-events.c 26 | test-hotplug.c 27 | test-async-fuzz.c 28 | ) 29 | 30 | add_executable(${OSDP_UNIT_TEST} EXCLUDE_FROM_ALL ${OSDP_UNIT_TEST_SRC}) 31 | 32 | target_link_libraries(${OSDP_UNIT_TEST} ${LIB_OSDP_TEST} osdp utils pthread) 33 | 34 | if (OPT_BUILD_SANITIZER) 35 | target_compile_options(${OSDP_UNIT_TEST} PRIVATE 36 | -fsanitize=address,undefined,leak 37 | ) 38 | target_link_options(${OSDP_UNIT_TEST} PRIVATE 39 | -fsanitize=address,undefined,leak 40 | ) 41 | endif() 42 | 43 | include_directories( 44 | ${PROJECT_SOURCE_DIR}/include 45 | ${PROJECT_SOURCE_DIR}/utils/include 46 | ) 47 | 48 | add_custom_target(check-ut 49 | COMMAND ${CMAKE_BINARY_DIR}/bin/${OSDP_UNIT_TEST} 50 | COMMAND rm ${CMAKE_BINARY_DIR}/bin/${OSDP_UNIT_TEST} 51 | DEPENDS ${OSDP_UNIT_TEST} 52 | ) 53 | -------------------------------------------------------------------------------- /doc/api/peripheral-device.rst: -------------------------------------------------------------------------------- 1 | Peripheral Device 2 | ================= 3 | 4 | The following functions are used when OSDP is to be used in pd mode. The library 5 | returns a single opaque pointer of type ``osdp_t`` where it maintains all it's 6 | internal data. All applications consuming this library must pass this context 7 | pointer all API calls. 8 | 9 | Device lifecycle management 10 | --------------------------- 11 | 12 | .. doxygentypedef:: osdp_t 13 | 14 | .. doxygenfunction:: osdp_pd_setup 15 | 16 | .. doxygenfunction:: osdp_pd_refresh 17 | 18 | .. doxygenfunction:: osdp_pd_teardown 19 | 20 | 21 | PD Capabilities 22 | --------------- 23 | 24 | .. doxygenstruct:: osdp_pd_cap 25 | :members: 26 | 27 | .. doxygenfunction:: osdp_pd_set_capabilities 28 | 29 | Commands 30 | -------- 31 | 32 | .. doxygentypedef:: pd_command_callback_t 33 | 34 | .. doxygenfunction:: osdp_pd_set_command_callback 35 | 36 | Refer to the `command structure`_ document for more information on how the 37 | ``cmd`` structure is framed. 38 | 39 | .. _command structure: command-structure.html 40 | 41 | Events 42 | ------ 43 | 44 | When a PD app has some event (card read, key press, etc.,) to be reported to the 45 | CP, it creates the corresponding event structure and calls 46 | ``osdp_pd_submit_event`` to deliver it to the CP on the next osdp_POLL command. 47 | 48 | .. doxygenfunction:: osdp_pd_submit_event 49 | 50 | .. doxygenfunction:: osdp_pd_flush_events 51 | 52 | Refer to the `event structure`_ document for more information on how to 53 | populate the ``event`` structure for these function. 54 | 55 | .. _event structure: event-structure.html -------------------------------------------------------------------------------- /examples/cpp/cp_app.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | int sample_cp_send_func(void *data, uint8_t *buf, int len) 13 | { 14 | (void)(data); 15 | (void)(buf); 16 | 17 | // TODO (user): send buf of len bytes, over the UART channel. 18 | 19 | return len; 20 | } 21 | 22 | int sample_cp_recv_func(void *data, uint8_t *buf, int len) 23 | { 24 | (void)(data); 25 | (void)(buf); 26 | (void)(len); 27 | 28 | // TODO (user): read from UART channel into buf, for upto len bytes. 29 | 30 | return 0; 31 | } 32 | 33 | osdp_pd_info_t pd_info[] = { 34 | { 35 | .name = "pd[101]", 36 | .baud_rate = 115200, 37 | .address = 101, 38 | .flags = 0, 39 | .id = {}, 40 | .cap = nullptr, 41 | .channel = { 42 | .data = nullptr, 43 | .id = 0, 44 | .recv = sample_cp_recv_func, 45 | .send = sample_cp_send_func, 46 | .flush = nullptr, 47 | .close = nullptr, 48 | }, 49 | .scbk = nullptr, 50 | } 51 | }; 52 | 53 | int event_handler(void *data, int pd, struct osdp_event *event) { 54 | (void)(data); 55 | 56 | std::cout << "PD" << pd << " EVENT: " << event->type << std::endl; 57 | return 0; 58 | } 59 | 60 | int main() 61 | { 62 | OSDP::ControlPanel cp; 63 | 64 | cp.logger_init("osdp::cp", OSDP_LOG_DEBUG, NULL); 65 | 66 | cp.setup(1, pd_info); 67 | 68 | cp.set_event_callback(event_handler, nullptr); 69 | 70 | while (1) { 71 | // your application code. 72 | 73 | cp.refresh(); 74 | std::this_thread::sleep_for(std::chrono::microseconds(10 * 1000)); 75 | } 76 | 77 | return 0; 78 | } 79 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | There are many users using LibOSDP in some capacity in production. If you think 4 | you found a bug that may have security implications, please follow the usual 5 | responsible disclosure protocols. Any issue reported in this channel will be 6 | acknowledged withing 3 business days. 7 | 8 | If an issue has been confirmed by a maintainer, we request the reporter to 9 | respect a 90 day embargo period before making the issue public. 10 | 11 | ## Supported Versions 12 | 13 | LibOSDP will support the last 2 [releases][1] for security and bug fixes. 14 | 15 | | Version | Branch | Supported | 16 | | ------- | -------|------------------- | 17 | | <= 1.5 | N/A | :x: | 18 | | 2.4.x | 2.4.x | :white_check_mark: | 19 | | latest | master | :white_check_mark: | 20 | 21 | ## Reporting a Vulnerability 22 | 23 | Please send an email to sidcha.dev@gmail.com ([GPG]([2])). 24 | 25 | ## Security Mailing List 26 | 27 | If you are a vendor using LibOSDP in a product (or any production capacity), 28 | please send an email to sidcha.dev@gmail.com to get added to a private mailing 29 | list which will be used to notify about critical incidents such as 30 | vulnerabilities and potential fixes or workarounds before the issue has been made 31 | public. 32 | 33 | You can also follow the [security advisories][3] page but this will be updated 34 | only after the issue has been made public. 35 | 36 | Note: For very obvious reasons, not everyone can be added to this list. You 37 | should be able to prove that you are indeed using LibOSDP in production. 38 | 39 | [1]: https://github.com/goToMain/libosdp/releases 40 | [2]: https://github.com/sidcha.gpg 41 | [3]: https://github.com/goToMain/libosdp/security/advisories 42 | -------------------------------------------------------------------------------- /src/osdp_diag.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | 9 | #include "osdp_common.h" 10 | 11 | static void pcap_file_name(struct osdp_pd *pd, char *buf, size_t size) 12 | { 13 | int n; 14 | char *p; 15 | 16 | n = snprintf(buf, size, "osdp-trace-%spd-%d-", 17 | is_pd_mode(pd) ? "" : "cp-", pd->address); 18 | n += add_iso8601_utc_datetime(buf + n, size - n); 19 | strcpy(buf + n, ".pcap"); 20 | 21 | while ((p = strchr(buf, ':')) != NULL) { 22 | *p = '_'; 23 | } 24 | } 25 | 26 | void osdp_packet_capture_init(struct osdp_pd *pd) 27 | { 28 | pcap_t *cap; 29 | char path[128]; 30 | 31 | pcap_file_name(pd, path, sizeof(path)); 32 | cap = pcap_start(path, OSDP_PACKET_BUF_SIZE, OSDP_PCAP_LINK_TYPE); 33 | if (cap) { 34 | LOG_WRN("Capturing packets to '%s'", path); 35 | LOG_WRN("A graceful teardown of libosdp ctx is required" 36 | " for a complete trace file to be produced."); 37 | } else { 38 | LOG_ERR("Packet capture init failed; check if path '%s'" 39 | " is accessible", path); 40 | } 41 | pd->packet_capture_ctx = (void *)cap; 42 | } 43 | 44 | void osdp_packet_capture_finish(struct osdp_pd *pd) 45 | { 46 | pcap_t *cap = pd->packet_capture_ctx; 47 | size_t num_packets; 48 | 49 | assert(cap); 50 | num_packets = cap->num_packets; 51 | if (pcap_stop(cap)) { 52 | LOG_ERR("Unable to stop capture (flush/close failed)"); 53 | return; 54 | } 55 | LOG_INF("Captured %d packets", num_packets); 56 | } 57 | 58 | void osdp_capture_packet(struct osdp_pd *pd, uint8_t *buf, int len) 59 | { 60 | pcap_t *cap = pd->packet_capture_ctx; 61 | 62 | assert(cap); 63 | assert(len <= OSDP_PACKET_BUF_SIZE); 64 | pcap_add(cap, buf, len); 65 | } 66 | -------------------------------------------------------------------------------- /tests/pytest/test_sc_keys.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | from osdp import * 8 | from conftest import make_fifo_pair, cleanup_fifo_pair 9 | 10 | def test_set_new_scbk(utils): 11 | # Create single CP-PD pair 12 | f1, f2 = make_fifo_pair("sc_keys") 13 | pd_addr = 101 14 | key = utils.ks.new_key('sc-keys-pd') 15 | pd = utils.create_pd(PDInfo(pd_addr, f1, scbk=key)) 16 | cp = utils.create_cp([ PDInfo(pd_addr, f2, scbk=key) ], sc_wait=True) 17 | 18 | # Set a new SCBK from the CP side and verify whether it was received 19 | # by the PD as we intended. 20 | new_key = utils.ks.gen_key() 21 | keyset_cmd = { 22 | 'command': Command.Keyset, 23 | 'type': 1, 24 | 'data': new_key 25 | } 26 | assert cp.submit_command(pd_addr, keyset_cmd) 27 | cmd = pd.get_command() 28 | assert cmd == keyset_cmd 29 | utils.ks.update_key('sc-keys-pd', new_key) 30 | 31 | # Stop CP and restart SC with new SCBK. PD should accept it 32 | cp.teardown() 33 | cp = utils.create_cp([ PDInfo(pd_addr, f2, scbk=utils.ks.get_key('sc-keys-pd')) ]) 34 | assert cp.sc_wait(pd_addr) 35 | 36 | # Cleanup 37 | cp.teardown() 38 | pd.teardown() 39 | cleanup_fifo_pair("sc_keys") 40 | 41 | def test_install_mode_set_scbk(utils): 42 | f1, f2 = make_fifo_pair("install_mode") 43 | pd_addr = 101 44 | pd = utils.create_pd(PDInfo(pd_addr, f1, flags=[ LibFlag.InstallMode ])) 45 | cp = utils.create_cp([ 46 | PDInfo(pd_addr, f2, scbk=utils.ks.new_key('install-mode-pd')) 47 | ]) 48 | assert cp.sc_wait_all() 49 | 50 | # Cleanup 51 | cp.teardown() 52 | pd.teardown() 53 | cleanup_fifo_pair("install_mode") -------------------------------------------------------------------------------- /platformio/osdp_config.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef _OSDP_CONFIG_H_ 8 | #define _OSDP_CONFIG_H_ 9 | 10 | /** 11 | * @brief The following macros are defined defined from the variable in cmake 12 | * files. All @XXX@ are replaced by the value of XXX as resolved by cmake. 13 | */ 14 | #define PROJECT_VERSION "3.1.0" 15 | #define PROJECT_NAME "libosdp" 16 | #define GIT_BRANCH "platformio" 17 | #define GIT_REV "" 18 | #define GIT_TAG "" 19 | #define GIT_DIFF "" 20 | #define REPO_ROOT "" 21 | 22 | /** 23 | * @brief Other OSDP constants 24 | */ 25 | #define OSDP_PD_SC_RETRY_MS (600 * 1000u) 26 | #define OSDP_PD_POLL_TIMEOUT_MS (50) 27 | #define OSDP_PD_SC_TIMEOUT_MS (8 * 1000u) 28 | #define OSDP_PD_ONLINE_TOUT_MS (8 * 1000u) 29 | #define OSDP_RESP_TOUT_MS (200) 30 | #define OSDP_CMD_MAX_RETRIES (8) 31 | #define OSDP_ONLINE_RETRY_WAIT_MAX_MS (300 * 1000u) 32 | #define OSDP_CMD_RETRY_WAIT_MS (800) 33 | #define OSDP_PACKET_BUF_SIZE (256) 34 | #define OSDP_RX_RB_SIZE (512) 35 | #define OSDP_CP_CMD_POOL_SIZE (4) 36 | #define OSDP_FILE_ERROR_RETRY_MAX (10) 37 | #define OSDP_PD_MAX (126) 38 | #define OSDP_CMD_ID_OFFSET (5) 39 | #define OSDP_PCAP_LINK_TYPE (162) 40 | #define OSDP_PD_NAME_MAXLEN (16) 41 | #define OSDP_MINIMUM_PACKET_SIZE (128) 42 | 43 | #endif /* _OSDP_CONFIG_H_ */ 44 | -------------------------------------------------------------------------------- /examples/platformio/cp.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | OSDP::ControlPanel cp; 11 | 12 | int serial1_send_func(void *data, uint8_t *buf, int len) 13 | { 14 | (void)(data); 15 | 16 | int sent = 0; 17 | for (int i = 0; i < len; i++) { 18 | if (Serial1.write(buf[i])) { 19 | sent++; 20 | } else { 21 | break; 22 | } 23 | } 24 | return sent; 25 | } 26 | 27 | int serial1_recv_func(void *data, uint8_t *buf, int len) 28 | { 29 | (void)(data); 30 | 31 | int read = 0; 32 | while (Serial1.available() && read < len) { 33 | buf[read] = Serial1.read(); 34 | read++; 35 | } 36 | return read; 37 | } 38 | 39 | osdp_pd_info_t pd_info[] = { 40 | { 41 | .name = "pd[101]", 42 | .baud_rate = (int)115200, 43 | .address = 101, 44 | .flags = 0, 45 | .id = {}, 46 | .cap = nullptr, 47 | .channel = { 48 | .data = nullptr, 49 | .id = 0, 50 | .recv = serial1_recv_func, 51 | .send = serial1_send_func, 52 | .flush = nullptr 53 | }, 54 | .scbk = nullptr, 55 | }, 56 | }; 57 | 58 | int event_handler(void *data, int pd, struct osdp_event *event) 59 | { 60 | (void)(data); 61 | 62 | Serial.println("Received an event!"); 63 | return 0; 64 | } 65 | 66 | void setup() 67 | { 68 | Serial.begin(115200); 69 | Serial1.begin(115200); 70 | 71 | cp.logger_init("osdp::cp", OSDP_LOG_DEBUG, NULL); 72 | 73 | cp.setup(1, pd_info); 74 | cp.set_event_callback(event_handler, nullptr); 75 | } 76 | 77 | void loop() 78 | { 79 | cp.refresh(); 80 | delay(1000); 81 | } 82 | -------------------------------------------------------------------------------- /src/osdp_config.h.in: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef _OSDP_CONFIG_H_ 8 | #define _OSDP_CONFIG_H_ 9 | 10 | /** 11 | * @brief The following macros are defined defined from the variable in cmake 12 | * files. All @XXX@ are replaced by the value of XXX as resolved by cmake. 13 | */ 14 | #define PROJECT_VERSION "@PROJECT_VERSION@" 15 | #define PROJECT_NAME "@PROJECT_NAME@" 16 | #define GIT_BRANCH "@GIT_BRANCH@" 17 | #define GIT_REV "@GIT_REV@" 18 | #define GIT_TAG "@GIT_TAG@" 19 | #define GIT_DIFF "@GIT_DIFF@" 20 | #define REPO_ROOT "@REPO_ROOT@" 21 | 22 | /** 23 | * @brief Other OSDP constants 24 | */ 25 | #define OSDP_PD_SC_RETRY_MS (600 * 1000u) 26 | #define OSDP_PD_POLL_TIMEOUT_MS (50) 27 | #define OSDP_PD_SC_TIMEOUT_MS (8 * 1000u) 28 | #define OSDP_PD_ONLINE_TOUT_MS (8 * 1000u) 29 | #define OSDP_RESP_TOUT_MS (200) 30 | #define OSDP_CMD_MAX_RETRIES (8) 31 | #define OSDP_ONLINE_RETRY_WAIT_MAX_MS (300 * 1000u) 32 | #define OSDP_CMD_RETRY_WAIT_MS (800) 33 | #define OSDP_PACKET_BUF_SIZE (256) 34 | #define OSDP_RX_RB_SIZE (512) 35 | #define OSDP_CP_CMD_POOL_SIZE (4) 36 | #define OSDP_FILE_ERROR_RETRY_MAX (10) 37 | #define OSDP_PD_MAX (126) 38 | #define OSDP_CMD_ID_OFFSET (5) 39 | #define OSDP_PCAP_LINK_TYPE (162) 40 | #define OSDP_PD_NAME_MAXLEN (16) 41 | #define OSDP_MINIMUM_PACKET_SIZE (128) 42 | 43 | #endif /* _OSDP_CONFIG_H_ */ 44 | -------------------------------------------------------------------------------- /zephyr/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if (CONFIG_LIBOSDP) 2 | set(OSDP_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..) 3 | set(REPO_ROOT ${OSDP_ROOT}) 4 | 5 | list(APPEND CMAKE_MODULE_PATH "${OSDP_ROOT}/cmake") 6 | include(GitInfo) 7 | 8 | ## TODO: Convert these to Kconfig entries 9 | configure_file(${OSDP_ROOT}/src/osdp_config.h.in ${CMAKE_CURRENT_BINARY_DIR}/include/osdp_config.h @ONLY) 10 | 11 | file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/include/osdp_export.h " 12 | #ifndef OSDP_EXPORT_H_ 13 | #define OSDP_EXPORT_H_ 14 | 15 | #define OSDP_EXPORT 16 | #define OSDP_NO_EXPORT 17 | #define OSDP_DEPRECATED_EXPORT 18 | 19 | #endif /* OSDP_EXPORT_H_ */" 20 | ) 21 | 22 | zephyr_interface_library_named(osdp) 23 | target_link_libraries(zephyr_interface INTERFACE osdp) 24 | target_include_directories(osdp INTERFACE ${OSDP_ROOT}/include) 25 | target_include_directories(osdp INTERFACE ${CMAKE_CURRENT_BINARY_DIR}/include) 26 | 27 | zephyr_library_named(modules_osdp) 28 | zephyr_library_compile_definitions(__BARE_METAL__) 29 | zephyr_library_link_libraries(osdp) 30 | target_include_directories(modules_osdp PRIVATE ${OSDP_ROOT}/utils/include) 31 | 32 | zephyr_library_sources( 33 | ## LibOSDP 34 | ${OSDP_ROOT}/src/osdp_cp.c 35 | ${OSDP_ROOT}/src/osdp_pd.c 36 | ${OSDP_ROOT}/src/osdp_common.c 37 | ${OSDP_ROOT}/src/osdp_phy.c 38 | ${OSDP_ROOT}/src/osdp_sc.c 39 | ${OSDP_ROOT}/src/osdp_file.c 40 | ${OSDP_ROOT}/src/crypto/mbedtls.c 41 | 42 | ## Utils 43 | ## TODO: Migrate these to use (or wrap around) zephyr API 44 | ${OSDP_ROOT}/utils/src/list.c 45 | ${OSDP_ROOT}/utils/src/queue.c 46 | ${OSDP_ROOT}/utils/src/slab.c 47 | ${OSDP_ROOT}/utils/src/utils.c 48 | ${OSDP_ROOT}/utils/src/logger.c 49 | ${OSDP_ROOT}/utils/src/disjoint_set.c 50 | 51 | ## Zephyr glue code 52 | src/osdp.c 53 | ) 54 | 55 | target_include_directories(modules_osdp PRIVATE ${ZEPHYR_MBEDTLS_MODULE_DIR}/include) 56 | zephyr_link_libraries(mbedTLS) 57 | endif() 58 | -------------------------------------------------------------------------------- /cmake/CreatePackages.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | # Package Info 8 | set(CPACK_PACKAGE_NAME ${PROJECT_NAME}) 9 | set(CPACK_URL ${PROJECT_URL}) 10 | set(CPACK_PACKAGE_DESCRIPTION_SUMMARY ${PROJECT_DESCRIPTION}) 11 | set(CPACK_RESOURCE_FILE_README "${PROJECT_SOURCE_DIR}/README.md") 12 | set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") 13 | set(CPACK_PACKAGE_DESCRIPTION_FILE ${CPACK_RESOURCE_FILE_README}) 14 | set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}/packages") 15 | SET(CPACK_OUTPUT_FILE_PREFIX "artifacts") 16 | 17 | # Version 18 | set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) 19 | set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) 20 | set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) 21 | 22 | set(PACKAGE_NAME ${PROJECT_NAME}-${PROJECT_VERSION}) 23 | set(SYSTEM_NAME ${CMAKE_SYSTEM_NAME}-${CMAKE_HOST_SYSTEM_PROCESSOR}) 24 | 25 | # Source tarballs 26 | set(CPACK_SOURCE_PACKAGE_FILE_NAME "${PACKAGE_NAME}.src") 27 | set(CPACK_SOURCE_GENERATOR "TGZ") 28 | set(CPACK_SOURCE_IGNORE_FILES 29 | .git/ 30 | .github/ 31 | .vscode/ 32 | .venv/ 33 | .pytest_cache 34 | build/ 35 | tags 36 | __pycache__/ 37 | /_config.yml 38 | /python/dist/ 39 | /python/libosdp.egg-info/ 40 | /python/vendor/ 41 | ) 42 | 43 | # Binaries 44 | set(CPACK_GENERATOR "TGZ") 45 | set(CPACK_PACKAGE_FILE_NAME "${PACKAGE_NAME}-${SYSTEM_NAME}") 46 | include(CPackComponent) 47 | cpack_add_component(distributables 48 | DISPLAY_NAME ${PROJECT_NAME} 49 | DESCRIPTION "Distributables (shared/static libararies, binaries, etc.,)" 50 | ) 51 | cpack_add_component(distributables 52 | DISPLAY_NAME ${PROJECT_NAME} 53 | DESCRIPTION "Development headers" 54 | ) 55 | cpack_add_component(config_files 56 | DISPLAY_NAME ${PROJECT_NAME} 57 | DESCRIPTION "Package Configuration Files" 58 | ) 59 | 60 | include(CPack) 61 | -------------------------------------------------------------------------------- /doc/protocol/faq.rst: -------------------------------------------------------------------------------- 1 | Frequently Asked Questions 2 | ========================== 3 | 4 | The following FAQ is being put together by the community regarding OSDP. In 5 | addition to the primary authors, the following people have helped generously to 6 | obtain these information. 7 | 8 | - David Eriksson 9 | 10 | Is OSDP standardized? 11 | --------------------- 12 | 13 | Yes. OSDP has become an IEC standard – IEC 60839-11-5 14 | 15 | Who is the owner of this protocol? 16 | ---------------------------------- 17 | 18 | OSDP is wholly owned by SIA. 19 | 20 | Can I use OSDP/libosdp in my product? 21 | ------------------------------------- 22 | 23 | Yes, you are free to use OSDP and/or libosdp in your product. You do not have to 24 | pay any royalties to SIA or goToMain. 25 | 26 | Where can I find the specification document? 27 | -------------------------------------------- 28 | 29 | Though the protocol is free, the specification is developed maintained and by 30 | Security Industry Association and must be purchased from their website. 31 | 32 | Is OSDP free to consume? 33 | ------------------------ 34 | 35 | Yes. OSDP is open to consumption. You can procure the specification document 36 | and implement it yourself. 37 | 38 | Although, there is a very specific OSDP use case named Transparent Mode for 39 | smart cards that MAY have a license requirement from HID/Assa. There is an 40 | alternate method of transporting APDUs from a smart card called Extended Packet 41 | Mode that was developed by the OSDP WG and included in the standard that has no 42 | encumbrance. 43 | 44 | I'm contributing to libosdp project should I buy the specification? 45 | ------------------------------------------------------------------- 46 | 47 | Please send an email to sidcha.dev@gmail.com. Contributors of this project do 48 | not need to purchase the specification. 49 | 50 | Update (29.06.2021): 51 | 52 | This needs to be confirmed with SIA due to the new terms under which the 53 | specification is shared to me. 54 | -------------------------------------------------------------------------------- /doc/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(Sphinx) 2 | find_package(Doxygen) 3 | 4 | if(SPHINX_FOUND AND DOXYGEN_FOUND) 5 | 6 | # Find all the public headers 7 | get_target_property(LIBOSDP_PUBLIC_HEADER_DIR osdp INTERFACE_INCLUDE_DIRECTORIES) 8 | file(GLOB_RECURSE LIBOSDP_PUBLIC_HEADERS ${LIBOSDP_PUBLIC_HEADER_DIR}/*.h) 9 | 10 | # Copy public header and patch it for doxygen run 11 | configure_file( 12 | ${PROJECT_SOURCE_DIR}/include/osdp.h 13 | ${CMAKE_CURRENT_BINARY_DIR}/include/osdp.h 14 | COPYONLY 15 | ) 16 | execute_process( 17 | COMMAND sed -ie "/^#include $/d" ${CMAKE_CURRENT_BINARY_DIR}/include/osdp.h 18 | COMMAND sed -ie "/^OSDP_EXPORT$/d" ${CMAKE_CURRENT_BINARY_DIR}/include/osdp.h 19 | ) 20 | 21 | set(DOXYGEN_INPUT_DIRS ${CMAKE_CURRENT_BINARY_DIR}/include/) 22 | set(DOXYGEN_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/doxygen) 23 | set(DOXYGEN_INDEX_FILE ${DOXYGEN_OUTPUT_DIR}/html/index.html) 24 | 25 | # Generate Doxyfile in build dir. 26 | configure_file(Doxyfile.in Doxyfile @ONLY) 27 | 28 | add_custom_command(OUTPUT ${DOXYGEN_INDEX_FILE} 29 | DEPENDS ${LIBOSDP_PUBLIC_HEADERS} 30 | COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYFILE_OUT} 31 | MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile 32 | COMMENT "Generating doxygen docs") 33 | 34 | add_custom_target(Doxygen DEPENDS ${DOXYGEN_INDEX_FILE}) 35 | 36 | # Generate conf.py in build dir. 37 | configure_file(conf.py.in conf.py @ONLY) 38 | 39 | set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}) 40 | set(SPHINX_BUILD ${CMAKE_CURRENT_BINARY_DIR}/sphinx) 41 | 42 | add_custom_target(html_docs 43 | COMMAND ${SPHINX_EXECUTABLE} -qn -b html 44 | -c ${CMAKE_CURRENT_BINARY_DIR} 45 | # Tell Breathe where to find the Doxygen output 46 | -Dbreathe_projects.LibOSDP=${DOXYGEN_OUTPUT_DIR}/xml 47 | ${SPHINX_SOURCE} ${SPHINX_BUILD} 48 | WORKING_DIRECTORY ${SPHINX_SOURCE} 49 | DEPENDS Doxygen 50 | COMMENT "Generating documentation with Sphinx" 51 | ) 52 | 53 | endif() # SPHINX_FOUND 54 | -------------------------------------------------------------------------------- /doc/api/command-structure.rst: -------------------------------------------------------------------------------- 1 | Application Commands 2 | ==================== 3 | 4 | LibOSDP exposes the following structures thought ``osdp.h``. This document 5 | attempts to document each of its members. The following structure is used as a 6 | wrapper for all the commands for convenience. 7 | 8 | .. code:: c 9 | 10 | struct osdp_cmd { 11 | enum osdp_cmd_e id; // Command ID. Used to select specific commands in union 12 | union { 13 | struct osdp_cmd_led led; 14 | struct osdp_cmd_buzzer buzzer; 15 | struct osdp_cmd_text text; 16 | struct osdp_cmd_output output; 17 | struct osdp_cmd_comset comset; 18 | struct osdp_cmd_keyset keyset; 19 | struct osdp_cmd_mfg mfg; 20 | struct osdp_cmd_file_tx file_tx; 21 | struct osdp_status_report status; 22 | }; 23 | }; 24 | 25 | Below are the structure of each of the command structures. 26 | 27 | LED command 28 | ----------- 29 | 30 | .. doxygenstruct:: osdp_cmd_led_params 31 | :members: 32 | 33 | .. doxygenstruct:: osdp_cmd_led 34 | :members: 35 | 36 | Buzzer command 37 | -------------- 38 | 39 | .. doxygenstruct:: osdp_cmd_buzzer 40 | :members: 41 | 42 | Text command 43 | ------------ 44 | 45 | .. doxygenstruct:: osdp_cmd_text 46 | :members: 47 | 48 | Output command 49 | -------------- 50 | 51 | .. doxygenstruct:: osdp_cmd_output 52 | :members: 53 | 54 | Comset command 55 | -------------- 56 | 57 | .. doxygenstruct:: osdp_cmd_comset 58 | :members: 59 | 60 | Keyset command 61 | -------------- 62 | 63 | .. doxygenstruct:: osdp_cmd_keyset 64 | :members: 65 | 66 | Manufacture specific command 67 | ---------------------------- 68 | 69 | .. doxygenstruct:: osdp_cmd_mfg 70 | :members: 71 | 72 | File transfer command 73 | --------------------- 74 | 75 | .. doxygenstruct:: osdp_cmd_file_tx 76 | :members: 77 | 78 | Status report command 79 | --------------------- 80 | 81 | .. doxygenstruct:: osdp_status_report 82 | :members: 83 | 84 | .. doxygenenum:: osdp_status_report_type 85 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | name: Create Release 8 | 9 | on: 10 | push: 11 | # Sequence of patterns matched against refs/tags 12 | tags: 13 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 14 | 15 | jobs: 16 | cross_platform_build: 17 | name: Build on ${{ matrix.os }} 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | # macos-13 is an intel runner, macos-14 is apple silicon 22 | os: [ubuntu-latest, macos-13, macos-14, windows-latest] 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | submodules: recursive 27 | - name: Configure 28 | run: cmake -B build -DCMAKE_BUILD_TYPE=Debug -DOPT_OSDP_LIB_ONLY=on . 29 | - name: Build 30 | run: cmake --build build --parallel 8 31 | - name: Pack built binaries 32 | run: cmake --build build --target package 33 | - name: Upload artifacts 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: LibOSDP-${{ matrix.os }}-binaries 37 | path: build/artifacts/ 38 | 39 | build: 40 | name: Create Release 41 | needs: cross_platform_build 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Checkout code 45 | uses: actions/checkout@v4 46 | with: 47 | submodules: recursive 48 | - name: Produce Release Artifacts 49 | run: perl -e 'local $/; $_=<>; print $1 if (/-{4,}\n+\d+.*?\n+(.*?)\n+v\d+\./sg)' CHANGELOG > RELEASE.txt 50 | - name: Create Release 51 | id: create_release 52 | uses: actions/create-release@v1 53 | env: 54 | # This token is provided by Actions, you do not need to create your own token 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | with: 57 | tag_name: ${{ github.ref }} 58 | release_name: Release ${{ github.ref }} 59 | body_path: ./RELEASE.txt 60 | draft: false 61 | prerelease: false 62 | -------------------------------------------------------------------------------- /doc/api/control-panel.rst: -------------------------------------------------------------------------------- 1 | Control Panel 2 | ============= 3 | 4 | The following functions are used when OSDP is to be used in CP mode. The library 5 | returns a single opaque pointer of type ``osdp_t`` where it maintains all it's 6 | internal data. All applications consuming this library must pass this context 7 | pointer all API calls. 8 | 9 | For the CP application, it's connected PDs are referenced by the offset number 10 | (0-indexed). This offset corresponds to the order in which the 11 | ``osdp_pd_info_t`` was populated when passed to ``osdp_cp_setup``. 12 | 13 | Device lifecycle management 14 | --------------------------- 15 | 16 | .. doxygentypedef:: osdp_t 17 | 18 | .. doxygenfunction:: osdp_cp_setup 19 | 20 | .. doxygenfunction:: osdp_cp_refresh 21 | 22 | .. doxygenfunction:: osdp_cp_teardown 23 | 24 | Events 25 | ------ 26 | 27 | Events are generated by the PD and sent to the CP. The CP app can register a 28 | callback using ``osdp_cp_set_event_callback`` to get notified of events. 29 | 30 | .. doxygentypedef:: cp_event_callback_t 31 | 32 | .. doxygenfunction:: osdp_cp_set_event_callback 33 | 34 | Refer to the `event structure`_ document for more information on how the 35 | ``event`` structure is framed. 36 | 37 | .. _event structure: event-structure.html 38 | 39 | Commands 40 | -------- 41 | 42 | Commands are sent from the CP to the PD to perform various actions. The CP app 43 | has to create a command struct and then call ``osdp_cp_submit_command`` to enqueue 44 | the command to a particular PD. 45 | 46 | .. doxygenfunction:: osdp_cp_submit_command 47 | 48 | .. doxygenfunction:: osdp_cp_flush_commands 49 | 50 | Refer to the `command structure`_ document for more information on how to 51 | populate the ``cmd`` structure for these function. 52 | 53 | .. _command structure: command-structure.html 54 | 55 | Get PD capability 56 | ----------------- 57 | 58 | .. doxygenstruct:: osdp_pd_cap 59 | :members: 60 | 61 | .. doxygenfunction:: osdp_cp_get_capability 62 | 63 | Others 64 | ------ 65 | 66 | .. doxygenfunction:: osdp_cp_get_pd_id 67 | 68 | .. doxygenfunction:: osdp_cp_modify_flag 69 | 70 | -------------------------------------------------------------------------------- /examples/cpp/pd_app.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | int sample_pd_send_func(void *data, uint8_t *buf, int len) 13 | { 14 | (void)(data); 15 | (void)(buf); 16 | 17 | // TODO (user): send buf of len bytes, over the UART channel. 18 | 19 | return len; 20 | } 21 | 22 | int sample_pd_recv_func(void *data, uint8_t *buf, int len) 23 | { 24 | (void)(data); 25 | (void)(buf); 26 | (void)(len); 27 | 28 | // TODO (user): read from UART channel into buf, for upto len bytes. 29 | 30 | return 0; 31 | } 32 | 33 | osdp_pd_info_t info_pd = { 34 | .name = "pd[101]", 35 | .baud_rate = 9600, 36 | .address = 101, 37 | .flags = 0, 38 | .id = { 39 | .version = 1, 40 | .model = 153, 41 | .vendor_code = 31337, 42 | .serial_number = 0x01020304, 43 | .firmware_version = 0x0A0B0C0D, 44 | }, 45 | .cap = (struct osdp_pd_cap []) { 46 | { 47 | .function_code = OSDP_PD_CAP_READER_LED_CONTROL, 48 | .compliance_level = 1, 49 | .num_items = 1 50 | }, 51 | { 52 | .function_code = OSDP_PD_CAP_READER_AUDIBLE_OUTPUT, 53 | .compliance_level = 1, 54 | .num_items = 1 55 | }, 56 | { static_cast(-1), 0, 0 } /* Sentinel */ 57 | }, 58 | .channel = { 59 | .data = nullptr, 60 | .id = 0, 61 | .recv = sample_pd_recv_func, 62 | .send = sample_pd_send_func, 63 | .flush = nullptr, 64 | .close = nullptr, 65 | }, 66 | .scbk = nullptr, 67 | }; 68 | 69 | int pd_command_handler(void *data, struct osdp_cmd *cmd) 70 | { 71 | (void)(data); 72 | 73 | std::cout << "PD: CMD: " << cmd->id << std::endl; 74 | return 0; 75 | } 76 | 77 | int main() 78 | { 79 | OSDP::PeripheralDevice pd; 80 | 81 | pd.logger_init("osdp::pd", OSDP_LOG_DEBUG, NULL); 82 | 83 | pd.setup(&info_pd); 84 | 85 | pd.set_command_callback(pd_command_handler, nullptr); 86 | 87 | while (1) { 88 | pd.refresh(); 89 | 90 | // your application code. 91 | std::this_thread::sleep_for(std::chrono::microseconds(10 * 1000)); 92 | } 93 | 94 | return 0; 95 | } 96 | -------------------------------------------------------------------------------- /python/osdp/key_store.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | import os 8 | import tempfile 9 | import random 10 | 11 | class KeyStore(): 12 | def __init__(self, dir=None): 13 | self.temp_dir = None 14 | self.keys = {} 15 | if not dir: 16 | self.temp_dir = tempfile.TemporaryDirectory() 17 | self.key_dir = self.temp_dir.name 18 | else: 19 | self.key_dir = dir 20 | 21 | def key_file(self, name): 22 | return os.path.join(self.key_dir, 'key_' + name + '.bin') 23 | 24 | @staticmethod 25 | def gen_key(key_len=16): 26 | key = [] 27 | for i in range(key_len): 28 | key.append(random.randint(0, 255)) 29 | return bytes(key) 30 | 31 | def _store_key(self, key): 32 | with open(self.key_file(key), "w") as f: 33 | f.write(key.hex()) 34 | 35 | def get_key(self, name): 36 | if name not in self.keys: 37 | raise RuntimeError 38 | return self.keys[name] 39 | 40 | def new_key(self, name, key_len=16, force=True): 41 | if not force and name in self.keys: 42 | raise RuntimeError 43 | self.keys[name] = self.gen_key(key_len) 44 | return self.keys[name] 45 | 46 | def commit_key(self, name): 47 | if name not in self.keys: 48 | raise RuntimeError 49 | self._store_key(self.keys[name]) 50 | 51 | def update_key(self, name, key): 52 | if name not in self.keys: 53 | raise RuntimeError 54 | self.keys[name] = key 55 | 56 | def load_key(self, name, key_len=16): 57 | if not os.path.exists(self.key_file(name)): 58 | raise RuntimeError 59 | with open(self.key_file(name), "r") as f: 60 | key = bytes.fromhex(f.read()) 61 | if not key or not isinstance(key, bytes) or len(key) != key_len: 62 | raise RuntimeError 63 | self.keys[name] = key 64 | return key 65 | 66 | def __del__(self): 67 | if self.temp_dir: 68 | self.temp_dir.cleanup() 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love your input! We want to make contributing to this project as easy and 4 | transparent as possible, whether it's: 5 | 6 | - Reporting a bug 7 | - Discussing the current state of the code 8 | - Submitting a fix 9 | - Proposing new features 10 | 11 | ## We Develop with GitHub 12 | 13 | We use GitHub to host code, to track issues and feature requests, as well as 14 | accept contributions in the from of pull requests. 15 | 16 | You start off by creating a fork of goToMain/libosdp to /libosdp 17 | and then clone from your fork (or add another remote to existing clone). After 18 | that, you can create a branch from master and make your code changes there and 19 | push your changed to origin (if you added a remote earlier, then to that 20 | remote). Now when you visit the GitHub page for /libosdp, you 21 | should see an option to raise a pull request to goToMain/libosdp. 22 | 23 | You can read more about GitHub development work flow [here][1] and 24 | 25 | ## Any contributions you make will be under the project's license 26 | 27 | In short, when you submit code changes, your submissions are understood to be 28 | under the same license that covers the project. Feel free to contact the 29 | maintainers if that's a concern. 30 | 31 | ## Write bug reports with detail, background, and sample code 32 | 33 | **Great Bug Reports** tend to have: 34 | 35 | - A quick summary and/or background 36 | - Steps to reproduce 37 | * Be specific! 38 | * Give sample code if you can 39 | - What you expected would happen 40 | - What actually happens 41 | - Notes (possibly including why you think this might be happening, or stuff 42 | you tried that didn't work) 43 | 44 | ## Use a Consistent Coding Style 45 | 46 | Look around, make your code fit in. We mostly follow the [Linux kernel coding 47 | style][2] with minor variations. To keep things some what sane, install 48 | [editorconfig][3] plugin in your favorite editor -- this should handle a lot of 49 | unintended white-space issues. 50 | 51 | [1]: https://guides.github.com/introduction/flow/index.html 52 | [2]: https://www.kernel.org/doc/html/latest/process/coding-style.html 53 | [3]: https://editorconfig.org/ 54 | -------------------------------------------------------------------------------- /tests/pytest/test_data.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | import pytest 8 | 9 | from osdp import * 10 | from conftest import make_fifo_pair, cleanup_fifo_pair 11 | 12 | pd_cap = PDCapabilities([ 13 | (Capability.OutputControl, 1, 1), 14 | (Capability.LEDControl, 1, 2), 15 | (Capability.AudibleControl, 1, 3), 16 | (Capability.TextOutput, 1, 4), 17 | ]) 18 | 19 | key = KeyStore.gen_key() 20 | f1, f2 = make_fifo_pair("data") 21 | 22 | pd_id = PdId(1, 1, 314, 512, 443) 23 | 24 | # TODO remove this. 25 | pd_addr = 101 26 | pd = PeripheralDevice(PDInfo(pd_addr, f1, scbk=key, id=pd_id), pd_cap, log_level=LogLevel.Debug) 27 | cp = ControlPanel([PDInfo(pd_addr, f2, scbk=key, id=pd_id)]) 28 | 29 | @pytest.fixture(scope='module', autouse=True) 30 | def setup_test(): 31 | pd.start() 32 | cp.start() 33 | cp.sc_wait_all() 34 | yield 35 | teardown_test() 36 | 37 | def teardown_test(): 38 | cp.teardown() 39 | pd.teardown() 40 | cleanup_fifo_pair("data") 41 | 42 | def test_cp_pd_id(): 43 | assert cp.online_wait(pd.address) 44 | pd_id_recv = cp.get_pd_id(pd.address) 45 | assert pd_id_recv.version == pd_id.version 46 | assert pd_id_recv.model == pd_id.model 47 | assert pd_id_recv.vendor_code == pd_id.vendor_code 48 | assert pd_id_recv.serial_number == pd_id.serial_number 49 | assert pd_id_recv.firmware_version == pd_id.firmware_version 50 | 51 | def test_cp_check_capability(): 52 | assert cp.online_wait(pd.address) 53 | # check libosdp default capabilities 54 | assert cp.check_capability(pd.address, Capability.CheckCharacter) == (1, 0) 55 | assert cp.check_capability(pd.address, Capability.CommunicationSecurity) == (1, 0) 56 | assert cp.check_capability(pd.address, Capability.ReceiveBufferSize) == (0, 1) 57 | 58 | # check local capabilities 59 | assert cp.check_capability(pd.address, Capability.OutputControl) == (1, 1) 60 | assert cp.check_capability(pd.address, Capability.LEDControl) == (1, 2) 61 | assert cp.check_capability(pd.address, Capability.AudibleControl) == (1, 3) 62 | assert cp.check_capability(pd.address, Capability.TextOutput) == (1, 4) 63 | -------------------------------------------------------------------------------- /examples/python/cp_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | 8 | import argparse 9 | import serial 10 | from osdp import * 11 | 12 | class SerialChannel(Channel): 13 | def __init__(self, device: str, speed: int): 14 | self.dev = serial.Serial(device, speed, timeout=0) 15 | 16 | def read(self, max_read: int): 17 | return self.dev.read(max_read) 18 | 19 | def write(self, data: bytes): 20 | return self.dev.write(data) 21 | 22 | def flush(self): 23 | self.dev.flush() 24 | 25 | def __del__(self): 26 | self.dev.close() 27 | 28 | parser = argparse.ArgumentParser(prog = 'cp_app', description = "LibOSDP CP APP Example") 29 | parser.add_argument("device", type = str, metavar = "PATH", help = "Path to serial device") 30 | parser.add_argument("--baudrate", type = int, metavar = "N", default = 115200, help = "Serial port's baud rate (default: 115200)") 31 | parser.add_argument("--log-level", type = int, metavar = "LEVEL", default = 6, help = "LibOSDP log level; can be 0-7 (default: 6)") 32 | args = parser.parse_args() 33 | 34 | ## Describe the PD (setting scbk=None puts the PD in install mode) 35 | channel = SerialChannel(args.device, args.baudrate) 36 | pd_info = [ 37 | PDInfo(101, channel, scbk=KeyStore.gen_key()), 38 | ] 39 | 40 | ## Create a CP device and kick-off the handler thread 41 | cp = ControlPanel(pd_info, log_level=args.log_level) 42 | cp.start() 43 | cp.sc_wait_all() 44 | 45 | ## create a LED Command to be used later 46 | led_cmd = { 47 | 'command': Command.LED, 48 | 'reader': 1, 49 | 'led_number': 0, 50 | 'control_code': 1, 51 | 'on_count': 10, 52 | 'off_count': 10, 53 | 'on_color': CommandLEDColor.Red, 54 | 'off_color': CommandLEDColor.Black, 55 | 'timer_count': 10, 56 | 'temporary': True 57 | } 58 | 59 | count = 0 # loop counter 60 | while True: 61 | ## Send LED command to PD-0 62 | cp.submit_command(pd_info[0].address, led_cmd) 63 | 64 | ## Check if we have an event from PD 65 | event = cp.get_event(pd_info[0].address, timeout=2) 66 | if event: 67 | print(f"PD-0 Sent Event {event}") 68 | 69 | if count >= 5: 70 | break 71 | count += 1 72 | 73 | cp.teardown() 74 | -------------------------------------------------------------------------------- /doc/libosdp/cross-compiling.rst: -------------------------------------------------------------------------------- 1 | Cross Compiling 2 | =============== 3 | 4 | LibOSDP is written in C and does not depend on any other libraries. You can 5 | compile it to pretty much any platform (even Windows). Follow the cross 6 | compilation best practice for your platform. This document gives you some ideas 7 | on how this can be done but is in no way conclusive. 8 | 9 | Using Cmake 10 | ----------- 11 | 12 | LibOSDP can be compiled with your cross compiler by passing a toolchain file to 13 | cmake. This can be done by invoking cmake with the command line argument 14 | ``-DCMAKE_TOOLCHAIN_FILE=/path/to/toolchain-file.cmake``. 15 | 16 | If your toolchain is installed in ``/opt/toolchain/armv8l-linux-gnueabihf/`` and 17 | the sysroot is present in ``/opt/toolchain/armv8l-linux-gnueabihf/sysroot``, the 18 | ``toolchain-file.cmake`` file should look like this: 19 | 20 | .. code:: cmake 21 | 22 | set(CMAKE_SYSTEM_NAME Linux) 23 | set(CMAKE_SYSTEM_PROCESSOR arm) 24 | 25 | # specify the cross compiler and sysroot 26 | set(TOOLCHAIN_INST_PATH /opt/toolchain/armv8l-linux-gnueabihf) 27 | set(CMAKE_C_COMPILER ${TOOLCHAIN_INST_PATH}/bin/armv8l-linux-gnueabihf-gcc) 28 | set(CMAKE_CXX_COMPILER ${TOOLCHAIN_INST_PATH}/bin/armv8l-linux-gnueabihf-g++) 29 | set(CMAKE_SYSROOT ${TOOLCHAIN_INST_PATH}/sysroot) 30 | 31 | # don't search for programs in the build host directories 32 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 33 | 34 | # search for libraries and headers in the target directories only 35 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 36 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 37 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 38 | 39 | For convenience, the ``toolchain-file.cmake`` file can be placed in a common path 40 | (probably where the toolchain is installed) and referenced from our build directory. 41 | 42 | .. code:: sh 43 | 44 | mkdir build && cd build 45 | cmake -DCMAKE_TOOLCHAIN_FILE=/opt/toolchain/armv8l-linux-gnueabihf/toolchain-file.cmake .. 46 | make 47 | 48 | Using make build 49 | ---------------- 50 | 51 | You could use the ``--cross-compile`` flag in configure.sh and then invoke make 52 | to build the library. 53 | 54 | .. code:: sh 55 | 56 | ./configure.sh --cross-compile arm-none- 57 | make 58 | make DESTDIR=/opt/arm-none-sysroot/ install 59 | -------------------------------------------------------------------------------- /examples/platformio/pd.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | OSDP::PeripheralDevice pd; 11 | 12 | int serial1_send_func(void *data, uint8_t *buf, int len) 13 | { 14 | (void)(data); 15 | 16 | int sent = 0; 17 | for (int i = 0; i < len; i++) { 18 | if (Serial1.write(buf[i])) { 19 | sent++; 20 | } else { 21 | break; 22 | } 23 | } 24 | return sent; 25 | } 26 | 27 | int serial1_recv_func(void *data, uint8_t *buf, int len) 28 | { 29 | (void)(data); 30 | 31 | int read = 0; 32 | while (Serial1.available() && read < len) { 33 | buf[read] = Serial1.read(); 34 | read++; 35 | } 36 | return read; 37 | } 38 | 39 | osdp_pd_info_t info_pd = { 40 | .name = "pd[101]", 41 | .baud_rate = 9600, 42 | .address = 101, 43 | .flags = 0, 44 | .id = { 45 | .version = 1, 46 | .model = 153, 47 | .vendor_code = 31337, 48 | .serial_number = 0x01020304, 49 | .firmware_version = 0x0A0B0C0D, 50 | }, 51 | .cap = (struct osdp_pd_cap []) { 52 | { 53 | .function_code = OSDP_PD_CAP_READER_LED_CONTROL, 54 | .compliance_level = 1, 55 | .num_items = 1 56 | }, 57 | { 58 | .function_code = OSDP_PD_CAP_READER_AUDIBLE_OUTPUT, 59 | .compliance_level = 1, 60 | .num_items = 1 61 | }, 62 | { static_cast(-1), 0, 0 } /* Sentinel */ 63 | }, 64 | .channel = { 65 | .data = nullptr, 66 | .id = 0, 67 | .recv = serial1_recv_func, 68 | .send = serial1_send_func, 69 | .flush = nullptr 70 | }, 71 | .scbk = nullptr, 72 | }; 73 | 74 | int pd_command_handler(void *data, struct osdp_cmd *cmd) 75 | { 76 | (void)(data); 77 | 78 | Serial.println("Received a command!"); 79 | return 0; 80 | } 81 | 82 | void setup() 83 | { 84 | Serial.begin(115200); 85 | Serial1.begin(115200); 86 | 87 | pd.logger_init("osdp::pd", OSDP_LOG_DEBUG, NULL); 88 | 89 | pd.setup(&info_pd); 90 | 91 | pd.set_command_callback(pd_command_handler, nullptr); 92 | } 93 | 94 | void loop() 95 | { 96 | pd.refresh(); 97 | delay(1000); 98 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | ifeq ($(wildcard config.make),) 8 | $(error run ./configure.sh first. See ./configure.sh -h) 9 | endif 10 | 11 | include config.make 12 | 13 | O ?= $(BUILD_DIR) 14 | OBJ_LIBOSDP := $(SRC_LIBOSDP:%.c=$(O)/%.o) 15 | OBJ_TEST := $(SRC_TEST:%.c=$(O)/%.o) 16 | CCFLAGS += -Wall -Wextra -O3 17 | 18 | ifeq ($(V),) 19 | MAKE := make -s 20 | Q := @ 21 | else 22 | Q := 23 | MAKE := make 24 | endif 25 | 26 | .PHONY: all 27 | all: libosdp $(TARGETS) 28 | 29 | .PHONY: libosdp 30 | libosdp: $(O)/libosdp.a $(O)/libosdp.pc 31 | 32 | .PHONY: pd_app 33 | pd_app: $(O)/pd_app.elf 34 | 35 | .PHONY: cp_app 36 | cp_app: $(O)/cp_app.elf 37 | 38 | $(O)/%.o: %.c 39 | @echo " CC $<" 40 | @mkdir -p $(@D) 41 | $(Q)$(CC) -c $< $(CCFLAGS) $(CCFLAGS_EXTRA) -o $@ 42 | 43 | $(O)/libosdp.a: CCFLAGS_EXTRA=-Iutils/include -Iinclude -Isrc -I$(O) 44 | $(O)/libosdp.a: $(OBJ_LIBOSDP) 45 | @echo " AR $(@F)" 46 | $(Q)$(AR) qc $@ $^ 47 | 48 | ## Samples 49 | 50 | $(O)/cp_app.elf: $(O)/libosdp.a 51 | @echo "LINK $(@F)" 52 | $(Q)$(CC) $(CCFLAGS) examples/c/cp_app.c -o $@ -Iinclude -L$(O) -losdp 53 | 54 | $(O)/pd_app.elf: $(O)/libosdp.a 55 | @echo "LINK $(@F)" 56 | $(Q)$(CC) $(CCFLAGS) examples/c/pd_app.c -o $@ -Iinclude -L$(O) -losdp 57 | 58 | ## Tests 59 | 60 | .PHONY: check 61 | check: CCFLAGS_EXTRA=-DUNIT_TESTING -Iutils/include -Iinclude -Isrc -I$(O) 62 | check: clean $(OBJ_TEST) 63 | @echo "LINK $@" 64 | $(Q)$(CC) $(CCFLAGS) $(OBJ_TEST) -o $(O)/unit-test 65 | $(Q)$(O)/unit-test 66 | 67 | ## Clean 68 | 69 | .PHONY: clean 70 | clean: 71 | $(Q)rm -f $(O)/src/*.o $(O)/src/crypto/*.o $(OBJ_TEST) 72 | $(Q)rm -f $(O)/*.a $(O)/*.elf 73 | 74 | .PHONY: distclean 75 | distclean: clean 76 | $(Q)rm config.make 77 | $(Q)rm -rf $(O) 78 | 79 | ## Install 80 | 81 | .PHONY: install 82 | install: libosdp 83 | install -d $(DESTDIR)$(PREFIX)/lib/ 84 | install -m 644 $(O)/libosdp.a $(DESTDIR)$(PREFIX)/lib/ 85 | install -d $(DESTDIR)$(PREFIX)/lib/pkgconfig 86 | install -m 644 $(O)/libosdp.pc $(DESTDIR)$(PREFIX)/lib/pkgconfig/ 87 | install -d $(DESTDIR)$(PREFIX)/include/ 88 | install -m 644 include/osdp.h $(DESTDIR)$(PREFIX)/include/ 89 | install -m 644 include/osdp.hpp $(DESTDIR)$(PREFIX)/include/ 90 | install -m 644 $(O)/include/osdp_export.h $(DESTDIR)$(PREFIX)/include/ 91 | 92 | -------------------------------------------------------------------------------- /examples/c/pd_app.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | /** 12 | * This method overrides the one provided by libosdp. It should return 13 | * a millisecond reference point from some tick source. 14 | */ 15 | int64_t osdp_millis_now() 16 | { 17 | return 0; 18 | } 19 | 20 | enum osdp_pd_e { 21 | OSDP_PD_1, 22 | OSDP_PD_2, 23 | OSDP_PD_SENTINEL, 24 | }; 25 | 26 | int sample_pd_send_func(void *data, uint8_t *buf, int len) 27 | { 28 | (void)(data); 29 | (void)(buf); 30 | 31 | // TODO (user): send buf of len bytes, over the UART channel. 32 | 33 | return len; 34 | } 35 | 36 | int sample_pd_recv_func(void *data, uint8_t *buf, int len) 37 | { 38 | (void)(data); 39 | (void)(buf); 40 | (void)(len); 41 | 42 | // TODO (user): read from UART channel into buf, for upto len bytes. 43 | 44 | return 0; 45 | } 46 | 47 | int pd_command_handler(void *arg, struct osdp_cmd *cmd) 48 | { 49 | (void)(arg); 50 | 51 | printf("PD: CMD: %d\n", cmd->id); 52 | return 0; 53 | } 54 | 55 | osdp_pd_info_t info_pd = { 56 | .address = 101, 57 | .baud_rate = 9600, 58 | .flags = 0, 59 | .channel.send = sample_pd_send_func, 60 | .channel.recv = sample_pd_recv_func, 61 | .id = { 62 | .version = 1, 63 | .model = 153, 64 | .vendor_code = 31337, 65 | .serial_number = 0x01020304, 66 | .firmware_version = 0x0A0B0C0D, 67 | }, 68 | .cap = (struct osdp_pd_cap []) { 69 | { 70 | .function_code = OSDP_PD_CAP_READER_LED_CONTROL, 71 | .compliance_level = 1, 72 | .num_items = 1 73 | }, 74 | { 75 | .function_code = OSDP_PD_CAP_READER_AUDIBLE_OUTPUT, 76 | .compliance_level = 1, 77 | .num_items = 1 78 | }, 79 | { (uint8_t)-1, 0, 0 } /* Sentinel */ 80 | }, 81 | .scbk = NULL, 82 | }; 83 | 84 | int main() 85 | { 86 | osdp_t *ctx; 87 | 88 | osdp_logger_init("osdp::pd", OSDP_LOG_DEBUG, NULL); 89 | 90 | ctx = osdp_pd_setup(&info_pd); 91 | if (ctx == NULL) { 92 | printf("pd init failed!\n"); 93 | return -1; 94 | } 95 | 96 | osdp_pd_set_command_callback(ctx, pd_command_handler, NULL); 97 | 98 | while (1) { 99 | osdp_pd_refresh(ctx); 100 | 101 | // your application code. 102 | // delay(); 103 | } 104 | 105 | return 0; 106 | } 107 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "C_Cpp.files.exclude": { 3 | "python/vendor/**": true 4 | }, 5 | "C_Cpp.default.includePath": [ 6 | "${workspaceFolder}/include", 7 | "${workspaceFolder}/src", 8 | "${workspaceFolder}/utils/include", 9 | "${workspaceFolder}/utils/src", 10 | "${workspaceFolder}/tests/unit-tests", 11 | "${workspaceFolder}/build/include" 12 | ], 13 | "files.associations": { 14 | "**/*.h": "c", 15 | "*.in": "c" 16 | }, 17 | "files.watcherExclude": { 18 | "python/vendor/**": true 19 | }, 20 | "files.exclude": { 21 | "python/vendor/**": true, 22 | "python/build/**": true, 23 | "**/*.egg-info/**": true 24 | }, 25 | "python.analysis.extraPaths": [ 26 | "./python" 27 | ], 28 | "search.exclude": { 29 | "python/vendor/**": true 30 | }, 31 | "cSpell.words": [ 32 | "ACURXSIZE", 33 | "BIOMATCH", 34 | "BIOMATCHR", 35 | "BIOREAD", 36 | "BIOREADR", 37 | "CAPDET", 38 | "CARDREAD", 39 | "CHLNG", 40 | "CRAUTH", 41 | "CRAUTHR", 42 | "FTSTAT", 43 | "ISTATR", 44 | "libosdp", 45 | "LSTATR", 46 | "MFGERRR", 47 | "MFGREP", 48 | "MFGSTATR", 49 | "osdp", 50 | "OSTATR", 51 | "PDCAP", 52 | "PDID", 53 | "PIVDATA", 54 | "PIVDATAR", 55 | "RMAC", 56 | "RMODE", 57 | "RSTATR", 58 | "scbk", 59 | "SCBKD", 60 | "TDSET", 61 | "UNSUP", 62 | "Wiegand" 63 | ], 64 | "editor.rulers": [ 65 | 80 66 | ], 67 | "python.testing.pytestArgs": [ 68 | "tests/pytest" 69 | ], 70 | "python.testing.unittestEnabled": false, 71 | "python.testing.pytestEnabled": true, 72 | "C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json", 73 | "C_Cpp.intelliSenseEngine": "default", 74 | "C_Cpp.errorSquiggles": "enabled", 75 | "C_Cpp.autocomplete": "default", 76 | "C_Cpp.configurationWarnings": "enabled", 77 | "cmake.buildDirectory": "${workspaceFolder}/build", 78 | "cmake.sourceDirectory": "${workspaceFolder}", 79 | "cmake.buildTask": true, 80 | "cmake.configureOnOpen": false, 81 | "debug.console.fontSize": 12, 82 | "debug.console.fontFamily": "monospace", 83 | "terminal.integrated.fontSize": 12, 84 | "terminal.integrated.fontFamily": "monospace" 85 | } -------------------------------------------------------------------------------- /examples/python/pd_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | 8 | import signal 9 | import argparse 10 | import serial 11 | from osdp import * 12 | 13 | exit_event = 0 14 | def signal_handler(sig, frame): 15 | global exit_event 16 | print('Received SIGINT, quitting...') 17 | exit_event = 1 18 | 19 | signal.signal(signal.SIGINT, signal_handler) 20 | 21 | class SerialChannel(Channel): 22 | def __init__(self, device: str, speed: int): 23 | self.dev = serial.Serial(device, speed, timeout=0) 24 | 25 | def read(self, max: int): 26 | return self.dev.read(max) 27 | 28 | def write(self, data: bytes): 29 | return self.dev.write(data) 30 | 31 | def flush(self): 32 | self.dev.flush() 33 | 34 | def __del__(self): 35 | self.dev.close() 36 | 37 | parser = argparse.ArgumentParser(prog = 'pd_app', description = "LibOSDP PD APP Example") 38 | parser.add_argument("device", type = str, metavar = "PATH", help = "Path to serial device") 39 | parser.add_argument("--baudrate", type = int, metavar = "N", default = 115200, help = "Serial port's baud rate (default: 115200)") 40 | parser.add_argument("--log-level", type = int, metavar = "N", default = 6, help = "LibOSDP log level; can be 0-7 (default: 6)") 41 | args = parser.parse_args() 42 | 43 | ## Describe the PD (setting scbk=None puts the PD in install mode) 44 | channel = SerialChannel(args.device, args.baudrate) 45 | pd_info = PDInfo(101, channel, scbk=None) 46 | 47 | ## Indicate the PD's capabilities to LibOSDP. 48 | pd_cap = PDCapabilities([ 49 | (Capability.OutputControl, 1, 1), 50 | (Capability.LEDControl, 2, 1), 51 | (Capability.AudibleControl, 1, 1), 52 | (Capability.TextOutput, 1, 1), 53 | ]) 54 | 55 | ## Create a PD device and kick-off the handler thread 56 | pd = PeripheralDevice(pd_info, pd_cap, log_level=args.log_level) 57 | pd.start() 58 | pd.sc_wait(timeout=-1) 59 | 60 | ## create a card read event to be used later 61 | card_event = { 62 | 'event': Event.CardRead, 63 | 'reader_no': 1, 64 | 'direction': 1, 65 | 'format': CardFormat.ASCII, 66 | 'data': bytes([9,1,9,2,6,3,1,7,7,0]), 67 | } 68 | 69 | while not exit_event: 70 | ## Check if we have any commands from the CP 71 | cmd = pd.get_command(timeout=5) 72 | if cmd: 73 | print(f"PD: Received command: {cmd}") 74 | 75 | ## Send a card read event to CP 76 | pd.submit_event(card_event) 77 | 78 | pd.teardown() 79 | -------------------------------------------------------------------------------- /src/crypto/mbedtls.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | 17 | mbedtls_aes_context aes_ctx; 18 | mbedtls_entropy_context entropy_ctx; 19 | mbedtls_ctr_drbg_context ctr_drbg_ctx; 20 | 21 | void osdp_crypt_setup() 22 | { 23 | int rc; 24 | const char *version; 25 | 26 | version = osdp_get_version(); 27 | mbedtls_aes_init(&aes_ctx); 28 | mbedtls_entropy_init(&entropy_ctx); 29 | mbedtls_ctr_drbg_init(&ctr_drbg_ctx); 30 | 31 | rc = mbedtls_ctr_drbg_seed(&ctr_drbg_ctx, 32 | mbedtls_entropy_func, 33 | &entropy_ctx, 34 | (const unsigned char *)version, 35 | strlen(version)); 36 | assert(rc == 0); 37 | } 38 | 39 | void osdp_encrypt(uint8_t *key, uint8_t *iv, uint8_t *data, int len) 40 | { 41 | int rc; 42 | 43 | if (iv != NULL) { 44 | /* encrypt multiple block with AES in CBC mode */ 45 | rc = mbedtls_aes_setkey_enc(&aes_ctx, key, 128); 46 | assert(rc == 0); 47 | rc = mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_ENCRYPT, 48 | len, iv, data, data); 49 | assert(rc == 0); 50 | } else { 51 | /* encrypt one block with AES in ECB mode */ 52 | assert(len <= 16); 53 | rc = mbedtls_aes_setkey_enc(&aes_ctx, key, 128); 54 | assert(rc == 0); 55 | rc = mbedtls_aes_crypt_ecb(&aes_ctx, MBEDTLS_AES_ENCRYPT, 56 | data, data); 57 | assert(rc == 0); 58 | } 59 | } 60 | 61 | void osdp_decrypt(uint8_t *key, uint8_t *iv, uint8_t *data, int len) 62 | { 63 | int rc; 64 | 65 | if (iv != NULL) { 66 | /* decrypt multiple block with AES in CBC mode */ 67 | rc = mbedtls_aes_setkey_dec(&aes_ctx, key, 128); 68 | assert(rc == 0); 69 | rc = mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_DECRYPT, 70 | len, iv, data, data); 71 | assert(rc == 0); 72 | } else { 73 | /* decrypt one block with AES in ECB mode */ 74 | assert(len <= 16); 75 | rc = mbedtls_aes_setkey_dec(&aes_ctx, key, 128); 76 | assert(rc == 0); 77 | rc = mbedtls_aes_crypt_ecb(&aes_ctx, MBEDTLS_AES_DECRYPT, 78 | data, data); 79 | assert(rc == 0); 80 | } 81 | } 82 | 83 | void osdp_fill_random(uint8_t *buf, int len) 84 | { 85 | int rc; 86 | 87 | rc = mbedtls_ctr_drbg_random(&ctr_drbg_ctx, buf, len); 88 | assert(rc == 0); 89 | } 90 | 91 | void osdp_crypt_teardown() 92 | { 93 | mbedtls_ctr_drbg_free(&ctr_drbg_ctx); 94 | mbedtls_entropy_free(&entropy_ctx); 95 | mbedtls_aes_free(&aes_ctx); 96 | } 97 | -------------------------------------------------------------------------------- /src/crypto/openssl.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | void osdp_crypt_setup() 17 | { 18 | } 19 | 20 | void __noreturn osdp_openssl_fatal(void) 21 | { 22 | /** 23 | * ERR_print_errors_fp(stderr) is not available when build as a shared 24 | * library in some platforms. Maybe we should call ERR_print_errors_cb() 25 | * in future but for now, we will just fprintf. 26 | */ 27 | fprintf(stderr, "Openssl fatal error\n"); 28 | abort(); 29 | } 30 | 31 | void osdp_encrypt(uint8_t *key, uint8_t *iv, uint8_t *data, int data_len) 32 | { 33 | int len; 34 | EVP_CIPHER_CTX *ctx; 35 | const EVP_CIPHER *type; 36 | 37 | ctx = EVP_CIPHER_CTX_new(); 38 | if (ctx == NULL) { 39 | osdp_openssl_fatal(); 40 | } 41 | 42 | if (iv != NULL) { 43 | type = EVP_aes_128_cbc(); 44 | } else { 45 | type = EVP_aes_128_ecb(); 46 | } 47 | 48 | if (!EVP_EncryptInit_ex(ctx, type, NULL, key, iv)) { 49 | osdp_openssl_fatal(); 50 | } 51 | 52 | if (!EVP_CIPHER_CTX_set_padding(ctx, 0)) { 53 | osdp_openssl_fatal(); 54 | } 55 | 56 | if (!EVP_EncryptUpdate(ctx, data, &len, data, data_len)) { 57 | osdp_openssl_fatal(); 58 | } 59 | 60 | if (!EVP_EncryptFinal_ex(ctx, data + len, &len)) { 61 | osdp_openssl_fatal(); 62 | } 63 | 64 | EVP_CIPHER_CTX_free(ctx); 65 | } 66 | 67 | void osdp_decrypt(uint8_t *key, uint8_t *iv, uint8_t *data, int data_len) 68 | { 69 | int len; 70 | EVP_CIPHER_CTX *ctx; 71 | const EVP_CIPHER *type; 72 | 73 | ctx = EVP_CIPHER_CTX_new(); 74 | if (ctx == NULL) { 75 | osdp_openssl_fatal(); 76 | } 77 | 78 | if (iv != NULL) { 79 | type = EVP_aes_128_cbc(); 80 | } else { 81 | type = EVP_aes_128_ecb(); 82 | } 83 | 84 | if (!EVP_DecryptInit_ex(ctx, type, NULL, key, iv)) { 85 | osdp_openssl_fatal(); 86 | } 87 | 88 | if (!EVP_CIPHER_CTX_set_padding(ctx, 0)) { 89 | osdp_openssl_fatal(); 90 | } 91 | 92 | if (!EVP_DecryptUpdate(ctx, data, &len, data, data_len)) { 93 | osdp_openssl_fatal(); 94 | } 95 | 96 | if (!EVP_DecryptFinal_ex(ctx, data + len, &len)) { 97 | osdp_openssl_fatal(); 98 | } 99 | 100 | EVP_CIPHER_CTX_free(ctx); 101 | } 102 | 103 | void osdp_fill_random(uint8_t *buf, int len) 104 | { 105 | if (RAND_bytes(buf, len) != 1) { 106 | osdp_openssl_fatal(); 107 | } 108 | } 109 | 110 | void osdp_crypt_teardown() 111 | { 112 | } 113 | -------------------------------------------------------------------------------- /include/osdp_export.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef _OSDP_EXPORT_H_ 8 | #define _OSDP_EXPORT_H_ 9 | 10 | /* ---------- feature detection helpers ---------- */ 11 | #if defined(__has_cpp_attribute) 12 | #define API_HAS_CPP_ATTR(x) __has_cpp_attribute(x) 13 | #else 14 | #define API_HAS_CPP_ATTR(x) 0 15 | #endif 16 | 17 | #if defined(__has_attribute) 18 | #define API_HAS_ATTR(x) __has_attribute(x) 19 | #else 20 | #define API_HAS_ATTR(x) 0 21 | #endif 22 | 23 | /* ---------- Export / Import / Visibility ---------- */ 24 | #if defined(_WIN32) || defined(__CYGWIN__) 25 | #if defined(BUILDING_API) 26 | #if defined(__GNUC__) 27 | #define API_EXPORT __attribute__ ((dllexport)) 28 | #else 29 | #define API_EXPORT __declspec(dllexport) 30 | #endif 31 | #else 32 | #if defined(__GNUC__) 33 | #define API_EXPORT __attribute__ ((dllimport)) 34 | #else 35 | #define API_EXPORT __declspec(dllimport) 36 | #endif 37 | #endif 38 | #define API_NO_EXPORT 39 | #else 40 | #if defined(__GNUC__) && (__GNUC__ >= 4) 41 | #define API_EXPORT __attribute__ ((visibility ("default"))) 42 | #define API_NO_EXPORT __attribute__ ((visibility ("hidden"))) 43 | #else 44 | #define API_EXPORT 45 | #define API_NO_EXPORT 46 | #endif 47 | #endif 48 | 49 | /* ---------- Deprecation (with message) ---------- */ 50 | 51 | /* Prefer C++ [[deprecated("msg")]] if available, otherwise compiler specifics. */ 52 | #if defined(__cplusplus) && API_HAS_CPP_ATTR(deprecated) 53 | /* [[deprecated("msg")]] supported */ 54 | #define API_DEPRECATED(msg) [[deprecated(msg)]] 55 | #elif defined(_MSC_VER) 56 | /* MSVC supports __declspec(deprecated("msg")) */ 57 | #define API_DEPRECATED(msg) __declspec(deprecated(msg)) 58 | #elif (defined(__clang__) && API_HAS_ATTR(deprecated)) \ 59 | || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5))) 60 | /* Clang and modern GCC support attribute deprecated("msg") */ 61 | #define API_DEPRECATED(msg) __attribute__((deprecated(msg))) 62 | #elif (defined(__GNUC__) || defined(__clang__)) 63 | /* Older GCC/Clang: attribute deprecated without message */ 64 | #define API_DEPRECATED(msg) __attribute__((deprecated)) 65 | #else 66 | /* Unknown compiler: no-op */ 67 | #define API_DEPRECATED(msg) 68 | #endif 69 | 70 | /* ---------- helpers ---------- */ 71 | #define OSDP_EXPORT API_EXPORT 72 | #define OSDP_NO_EXPORT API_NO_EXPORT 73 | #define OSDP_DEPRECATED_EXPORT(msg) API_DEPRECATED(msg) API_EXPORT 74 | #define OSDP_DEPRECATED_NO_EXPORT(msg) API_DEPRECATED(msg) API_NO_EXPORT 75 | 76 | #endif /* _OSDP_EXPORT_H_ */ 77 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2019-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | cmake_minimum_required(VERSION 3.14 FATAL_ERROR) 8 | cmake_policy(SET CMP0063 NEW) 9 | 10 | # Generate compile_commands.json for IDE support 11 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 12 | 13 | project(libosdp VERSION 3.1.0) 14 | 15 | set(PROJECT_AUTHOR "Siddharth Chandrasekaran") 16 | set(PROJECT_AUTHOR_EMAIL "sidcha.dev@gmail.com") 17 | set(PROJECT_YEAR "2019") 18 | set(PROJECT_ORG "goToMain") 19 | set(PROJECT_URL "https://github.com/goToMain/libosdp/") 20 | set(PROJECT_HOMEPAGE "https://libosdp.sidcha.dev/") 21 | set(PROJECT_DESCRIPTION "Open Supervised Device Protocol (OSDP) Library") 22 | set(PROJECT_LICENSE "Apache License, Version 2.0 (Apache-2.0)") 23 | 24 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 25 | 26 | ## Options 27 | option(OPT_OSDP_PACKET_TRACE "Enable raw packet trace for diagnostics" OFF) 28 | option(OPT_OSDP_DATA_TRACE "Enable command/reply data buffer tracing" OFF) 29 | option(OPT_OSDP_SKIP_MARK_BYTE "Don't send the leading mark byte (0xFF)" OFF) 30 | option(OPT_DISABLE_PRETTY_LOGGING "Don't colorize log ouputs" OFF) 31 | option(OPT_BUILD_SANITIZER "Enable different sanitizers during build" OFF) 32 | option(OPT_BUILD_STATIC "Build static library" ON) 33 | option(OPT_BUILD_SHARED "Build shared library" ON) 34 | option(OPT_OSDP_STATIC_PD "Setup PD single statically" OFF) 35 | option(OPT_OSDP_LIB_ONLY "Only build the library" OFF) 36 | option(OPT_BUILD_BARE_METAL "Build library for bare metal targets" OFF) 37 | 38 | ## Includes 39 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") 40 | include(AddCCompilerFlag) 41 | include(GitInfo) 42 | include(BuildType) 43 | 44 | ## Global settings 45 | if(NOT MSVC) 46 | add_compile_options(-Wall -Wextra) 47 | endif() 48 | 49 | if (OPT_BUILD_BARE_METAL) 50 | add_compile_options(-D__BARE_METAL__) 51 | else() 52 | include(GNUInstallDirs) 53 | endif() 54 | 55 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 56 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 57 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 58 | set(CMAKE_WARN_DEPRECATED ON) 59 | 60 | # Each subdirectory has it's own CMakeLists.txt 61 | include(GitSubmodules) 62 | add_subdirectory(src) 63 | if (NOT OPT_OSDP_STATIC_PD AND NOT OPT_OSDP_LIB_ONLY AND NOT MSVC) 64 | add_subdirectory(utils) 65 | add_subdirectory(tests/unit-tests) 66 | add_subdirectory(examples/c) 67 | add_subdirectory(examples/cpp) 68 | add_subdirectory(doc) 69 | endif() 70 | 71 | ## uninstall target 72 | add_custom_target(uninstall 73 | COMMAND xargs rm < ${CMAKE_BINARY_DIR}/install_manifest.txt 74 | ) 75 | 76 | ## include package rules at last 77 | include(CreatePackages) 78 | -------------------------------------------------------------------------------- /python/osdp/helpers.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | from .constants import Capability, LibFlag 8 | from .channel import Channel 9 | 10 | class PdId: 11 | def __init__(self, version: int, model: int, vendor_code: int, 12 | serial_number: int, firmware_version: int): 13 | self.version = version 14 | self.model = model 15 | self.vendor_code = vendor_code 16 | self.serial_number = serial_number 17 | self.firmware_version = firmware_version 18 | 19 | class PDInfo: 20 | def __init__(self, address: int, channel: Channel, scbk: bytes=None, 21 | name: str=None, flags=[], id: PdId=None): 22 | self.address = address 23 | self.flags = flags 24 | self.scbk = scbk 25 | if name: 26 | self.name = name 27 | else: 28 | self.name = "PD-" + str(address) 29 | self.channel = channel 30 | if id: 31 | self.id = id 32 | else: 33 | self.id = PdId(1, 1, 0xcafebabe, 0xdeadbeaf, 0xdeaddead) 34 | 35 | def get_flags(self) -> int: 36 | ret = 0 37 | for flag in self.flags: 38 | ret |= flag 39 | return ret 40 | 41 | def get(self) -> dict: 42 | return { 43 | 'name': self.name, 44 | 'address': self.address, 45 | 'flags': self.get_flags(), 46 | 'scbk': self.scbk, 47 | 'channel': self.channel, 48 | 49 | # Following are needed only for PD. For CP these are don't cares 50 | 'version': self.id.version, 51 | 'model': self.id.model, 52 | 'vendor_code': self.id.vendor_code, 53 | 'serial_number': self.id.serial_number, 54 | 'firmware_version': self.id.firmware_version 55 | } 56 | 57 | class _PDCapEntity: 58 | def __init__(self, function_code: Capability, 59 | compliance_level: int=1, num_items: int=1): 60 | self.function_code = function_code 61 | self.compliance_level = compliance_level 62 | self.num_items = num_items 63 | 64 | def get(self) -> dict: 65 | return { 66 | 'function_code': self.function_code, 67 | 'compliance_level': self.compliance_level, 68 | 'num_items': self.num_items 69 | } 70 | 71 | class PDCapabilities: 72 | def __init__(self, cap_list=[]): 73 | self.capabilities = {} 74 | for cap in cap_list: 75 | (fc, cl, ni) = cap 76 | self.capabilities[fc] = _PDCapEntity(fc, cl, ni) 77 | 78 | def get(self): 79 | ret = [] 80 | for fc in self.capabilities.keys(): 81 | ret.append(self.capabilities[fc].get()) 82 | return ret 83 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AccessModifierOffset: -4 3 | AlignAfterOpenBracket: Align 4 | AlignConsecutiveAssignments: false 5 | AlignConsecutiveDeclarations: false 6 | AlignOperands: true 7 | AlignTrailingComments: false 8 | AllowAllParametersOfDeclarationOnNextLine: false 9 | AllowShortBlocksOnASingleLine: false 10 | AllowShortCaseLabelsOnASingleLine: false 11 | AllowShortFunctionsOnASingleLine: None 12 | AllowShortIfStatementsOnASingleLine: false 13 | AllowShortLoopsOnASingleLine: false 14 | AlwaysBreakAfterDefinitionReturnType: None 15 | AlwaysBreakAfterReturnType: None 16 | AlwaysBreakBeforeMultilineStrings: false 17 | AlwaysBreakTemplateDeclarations: false 18 | BinPackArguments: true 19 | BinPackParameters: true 20 | BraceWrapping: 21 | AfterClass: false 22 | AfterControlStatement: false 23 | AfterEnum: false 24 | AfterFunction: true 25 | AfterNamespace: true 26 | AfterObjCDeclaration: false 27 | AfterStruct: false 28 | AfterUnion: false 29 | BeforeCatch: false 30 | BeforeElse: false 31 | IndentBraces: false 32 | BreakBeforeBinaryOperators: None 33 | BreakBeforeBraces: Custom 34 | BreakBeforeTernaryOperators: false 35 | BreakConstructorInitializersBeforeComma: false 36 | BreakAfterJavaFieldAnnotations: false 37 | BreakStringLiterals: false 38 | ColumnLimit: 80 39 | CommentPragmas: '^ IWYU pragma:' 40 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 41 | ConstructorInitializerIndentWidth: 8 42 | ContinuationIndentWidth: 8 43 | Cpp11BracedListStyle: false 44 | DerivePointerAlignment: false 45 | DisableFormat: false 46 | ExperimentalAutoDetectBinPacking: false 47 | 48 | IncludeCategories: 49 | - Regex: '.*' 50 | Priority: 1 51 | IncludeIsMainRegex: '(Test)?$' 52 | IndentCaseLabels: false 53 | IndentWidth: 8 54 | IndentWrappedFunctionNames: false 55 | JavaScriptQuotes: Leave 56 | JavaScriptWrapImports: true 57 | KeepEmptyLinesAtTheStartOfBlocks: false 58 | MacroBlockBegin: '' 59 | MacroBlockEnd: '' 60 | AlignConsecutiveMacros: true 61 | MaxEmptyLinesToKeep: 1 62 | NamespaceIndentation: None 63 | ObjCBlockIndentWidth: 8 64 | ObjCSpaceAfterProperty: true 65 | ObjCSpaceBeforeProtocolList: true 66 | 67 | # Taken from git's rules 68 | PenaltyBreakBeforeFirstCallParameter: 30 69 | PenaltyBreakComment: 10 70 | PenaltyBreakFirstLessLess: 0 71 | PenaltyBreakString: 10 72 | PenaltyExcessCharacter: 100 73 | PenaltyReturnTypeOnItsOwnLine: 60 74 | 75 | PointerAlignment: Right 76 | ReflowComments: false 77 | SortIncludes: false 78 | SpaceAfterCStyleCast: false 79 | SpaceAfterTemplateKeyword: true 80 | SpaceBeforeAssignmentOperators: true 81 | SpaceBeforeParens: ControlStatements 82 | SpaceInEmptyParentheses: false 83 | SpacesBeforeTrailingComments: 1 84 | SpacesInAngles: false 85 | SpacesInContainerLiterals: false 86 | SpacesInCStyleCastParentheses: false 87 | SpacesInParentheses: false 88 | SpacesInSquareBrackets: false 89 | Standard: Cpp03 90 | TabWidth: 8 91 | UseTab: Always 92 | ... 93 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # LibOSDP for Python 2 | 3 | This package exposes the C/C++ library for OSDP devices to python to enable rapid 4 | prototyping of these devices. There are two modules exposed by this package: 5 | 6 | - `osdp_sys`: A thin wrapper around the C/C++ API; this is a low level API and 7 | is no longer recommended to use this directly. 8 | 9 | - `osdp`: A wrapper over the `osdp_sys` to provide python friendly API; this 10 | implementation which is now powering the integration testing suit used to test 11 | all changes made to this project. 12 | 13 | ## Install 14 | 15 | You can install LibOSDP from PyPI using, 16 | 17 | ```sh 18 | pip install libosdp 19 | ``` 20 | 21 | Or, from github, 22 | 23 | ```sh 24 | pip install -e "git+https://github.com/goToMain/libosdp#egg=libosdp&subdirectory=python" 25 | ``` 26 | 27 | Or, from source using, 28 | 29 | ```sh 30 | git clone https://github.com/goToMain/libosdp --recurse-submodules 31 | cd libosdp/python 32 | python3 setup.py install 33 | ``` 34 | 35 | ## Quick Start 36 | 37 | ### Control Panel Mode 38 | 39 | ```python 40 | # Create a communication channel 41 | channel = SerialChannel("/dev/ttyUSB0") 42 | 43 | # populate osdp_pd_info_t from python 44 | pd_info = [ 45 | PDInfo(101, channel, scbk=KeyStore.gen_key()), 46 | ] 47 | 48 | # Create a CP device and kick-off the handler thread and wait till a secure 49 | # channel is established. 50 | cp = ControlPanel(pd_info, log_level=LogLevel.Debug) 51 | cp.start() 52 | cp.sc_wait_all() 53 | 54 | while True: 55 | ## Check if we have an event from PD 56 | led_cmd = { ... } 57 | event = cp.get_event(pd_info[0].address) 58 | if event: 59 | print(f"CP: Received event {event}") 60 | 61 | # Send LED command to PD-0 62 | cp.send_command(pd_info[0].address, led_cmd) 63 | ``` 64 | 65 | see [examples/cp_app.py][2] for more details. 66 | 67 | ### Peripheral Device mode: 68 | 69 | ```python 70 | # Create a communication channel 71 | channel = SerialChannel("/dev/ttyUSB0") 72 | 73 | # Describe the PD (setting scbk=None puts the PD in install mode) 74 | pd_info = PDInfo(101, channel, scbk=None) 75 | 76 | # Indicate the PD's capabilities to LibOSDP. 77 | pd_cap = PDCapabilities() 78 | 79 | # Create a PD device and kick-off the handler thread and wait till a secure 80 | # channel is established. 81 | pd = PeripheralDevice(pd_info, pd_cap) 82 | pd.start() 83 | pd.sc_wait() 84 | 85 | while True: 86 | # Send a card read event to CP 87 | card_event = { ... } 88 | pd.notify_event(card_event) 89 | 90 | # Check if we have any commands from the CP 91 | cmd = pd.get_command() 92 | if cmd: 93 | print(f"PD: Received command: {cmd}") 94 | ``` 95 | 96 | see [examples/pd_app.py][3] for more details. 97 | 98 | [1]: https://libosdp.sidcha.dev/api/ 99 | [2]: https://github.com/goToMain/libosdp/blob/master/examples/python/cp_app.py 100 | [3]: https://github.com/goToMain/libosdp/blob/master/examples/python/pd_app.py 101 | -------------------------------------------------------------------------------- /tests/pytest/test_events.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | import pytest 8 | 9 | from osdp import * 10 | from conftest import make_fifo_pair, cleanup_fifo_pair, wait_for_non_notification_event 11 | 12 | pd_cap = PDCapabilities([ 13 | (Capability.OutputControl, 1, 8), 14 | (Capability.ContactStatusMonitoring, 1, 8), 15 | (Capability.LEDControl, 1, 1), 16 | (Capability.AudibleControl, 1, 1), 17 | (Capability.TextOutput, 1, 1), 18 | ]) 19 | 20 | key = KeyStore.gen_key() 21 | f1, f2 = make_fifo_pair("events") 22 | 23 | secure_pd = PeripheralDevice( 24 | PDInfo(101, f1, scbk=key, flags=[ LibFlag.EnforceSecure ]), 25 | pd_cap, 26 | log_level=LogLevel.Debug 27 | ) 28 | 29 | pd_list = [ 30 | secure_pd, 31 | ] 32 | 33 | cp = ControlPanel([ 34 | PDInfo(101, f2, scbk=key, flags=[ LibFlag.EnforceSecure, LibFlag.EnableNotification ]) 35 | ], 36 | log_level=LogLevel.Debug 37 | ) 38 | 39 | @pytest.fixture(scope='module', autouse=True) 40 | def setup_test(): 41 | for pd in pd_list: 42 | pd.start() 43 | cp.start() 44 | if not cp.online_wait_all(timeout=10): 45 | teardown_test() 46 | pytest.fail("Failed to bring all PDs online within timeout") 47 | yield 48 | teardown_test() 49 | 50 | def teardown_test(): 51 | cp.teardown() 52 | for pd in pd_list: 53 | pd.teardown() 54 | cleanup_fifo_pair("events") 55 | 56 | def check_event(event): 57 | wait_for_non_notification_event(cp, secure_pd.address, event) 58 | 59 | def test_event_keypad(): 60 | event = { 61 | 'event': Event.KeyPress, 62 | 'reader_no': 1, 63 | 'data': bytes([9,1,9,2,6,3,1,7,7,0]), 64 | } 65 | secure_pd.submit_event(event) 66 | check_event(event) 67 | 68 | def test_event_mfg_reply(): 69 | event = { 70 | 'event': Event.ManufacturerReply, 71 | 'vendor_code': 0x153, 72 | 'data': bytes([0x10,9,1,9,2,6,3,1,7,7,0]), 73 | } 74 | secure_pd.submit_event(event) 75 | check_event(event) 76 | 77 | def test_event_cardread_wiegand(): 78 | event = { 79 | 'event': Event.CardRead, 80 | 'reader_no': 1, 81 | 'direction': 0, # has to be zero 82 | 'length': 16, 83 | 'format': CardFormat.Wiegand, 84 | 'data': bytes([0x55, 0xAA]), 85 | } 86 | secure_pd.submit_event(event) 87 | check_event(event) 88 | 89 | def test_event_input(): 90 | event = { 91 | 'event': Event.Status, 92 | 'type': StatusReportType.Input, 93 | 'report': bytes([1, 0, 1, 0, 1, 0, 1, 0]) 94 | } 95 | secure_pd.submit_event(event) 96 | check_event(event) 97 | 98 | def test_event_output(): 99 | event = { 100 | 'event': Event.Status, 101 | 'type': StatusReportType.Output, 102 | 'report': bytes([0, 1, 0, 1, 0, 1, 0, 1]) 103 | } 104 | secure_pd.submit_event(event) 105 | check_event(event) 106 | -------------------------------------------------------------------------------- /python/osdp_sys/module.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef _PYOSDP_H_ 8 | #define _PYOSDP_H_ 9 | 10 | #define PY_SSIZE_T_CLEAN 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | 21 | typedef struct { 22 | PyObject_HEAD 23 | bool is_cp; 24 | 25 | int file_id; 26 | struct { 27 | PyObject *open_cb; 28 | PyObject *read_cb; 29 | PyObject *write_cb; 30 | PyObject *close_cb; 31 | } fops; 32 | } pyosdp_base_t; 33 | 34 | typedef struct { 35 | pyosdp_base_t base; 36 | PyObject *event_cb; 37 | int num_pd; 38 | osdp_t *ctx; 39 | char *name; 40 | } pyosdp_cp_t; 41 | 42 | typedef struct { 43 | pyosdp_base_t base; 44 | PyObject *command_cb; 45 | osdp_t *ctx; 46 | char *name; 47 | } pyosdp_pd_t; 48 | 49 | #define DBG_PRINT_Py_REFCNT(x) \ 50 | fprintf(stderr, "%s: %s:%d Py_REFCNT(%s): %ld (%p)\n", TAG, __FUNCTION__, __LINE__, STR(X) , Py_REFCNT(x), x); 51 | 52 | /* from pyosdp_utils.c */ 53 | 54 | int pyosdp_module_add_type(PyObject *module, const char *name, 55 | PyTypeObject *type); 56 | 57 | int pyosdp_parse_int(PyObject *obj, int *res); 58 | int pyosdp_parse_str(PyObject *obj, char **str); 59 | int pyosdp_parse_bytes(PyObject *obj, uint8_t **data, int *length, bool allow_empty); 60 | 61 | int pyosdp_dict_get_bool(PyObject *dict, const char *key, bool *res); 62 | int pyosdp_dict_get_int(PyObject *dict, const char *key, int *res); 63 | int pyosdp_dict_get_str(PyObject *dict, const char *key, char **str); 64 | int pyosdp_dict_get_bytes(PyObject *dict, const char *key, uint8_t **buf, 65 | int *len); 66 | int pyosdp_dict_get_bytes_allow_empty(PyObject *dict, const char *key, uint8_t **data, 67 | int *length); 68 | int pyosdp_dict_get_object(PyObject *dict, const char *key, PyObject **obj); 69 | 70 | int pyosdp_dict_add_bool(PyObject *dict, const char *key, bool val); 71 | int pyosdp_dict_add_int(PyObject *dict, const char *key, int val); 72 | int pyosdp_dict_add_str(PyObject *dict, const char *key, const char *val); 73 | int pyosdp_dict_add_bytes(PyObject *dict, const char *key, const uint8_t *data, 74 | int len); 75 | void pyosdp_get_channel(PyObject *channel, struct osdp_channel *ops); 76 | 77 | /* from pyosdp_base.c */ 78 | 79 | extern PyTypeObject OSDPBaseType; 80 | int pyosdp_add_type_osdp_base(PyObject *module); 81 | 82 | /* from pyosdp_cp.c */ 83 | 84 | int pyosdp_add_type_cp(PyObject *module); 85 | 86 | /* from pyosdp_pd.c */ 87 | 88 | int pyosdp_add_type_pd(PyObject *module); 89 | 90 | /* from pyosdp_cmd.c */ 91 | 92 | int pyosdp_make_struct_cmd(struct osdp_cmd *cmd, PyObject *dict); 93 | int pyosdp_make_dict_cmd(PyObject **dict, struct osdp_cmd *cmd); 94 | int pyosdp_make_dict_event(PyObject **dict, struct osdp_event *event); 95 | int pyosdp_make_struct_event(struct osdp_event *event, PyObject *dict); 96 | PyObject *pyosdp_make_dict_pd_id(struct osdp_pd_id *pd_id); 97 | 98 | #endif /* _PYOSDP_H_ */ 99 | -------------------------------------------------------------------------------- /src/crypto/tinyaes_src.h: -------------------------------------------------------------------------------- 1 | #ifndef _AES_H_ 2 | #define _AES_H_ 3 | 4 | #include 5 | #include 6 | 7 | // #define the macros below to 1/0 to enable/disable the mode of operation. 8 | // 9 | // CBC enables AES encryption in CBC-mode of operation. 10 | // CTR enables encryption in counter-mode. 11 | // ECB enables the basic ECB 16-byte block algorithm. All can be enabled simultaneously. 12 | 13 | // The #ifndef-guard allows it to be configured before #include'ing or at compile time. 14 | #ifndef CBC 15 | #define CBC 1 16 | #endif 17 | 18 | #ifndef ECB 19 | #define ECB 1 20 | #endif 21 | 22 | #ifndef CTR 23 | #define CTR 1 24 | #endif 25 | 26 | 27 | #define AES128 1 28 | //#define AES192 1 29 | //#define AES256 1 30 | 31 | #define AES_BLOCKLEN 16 // Block length in bytes - AES is 128b block only 32 | 33 | #if defined(AES256) && (AES256 == 1) 34 | #define AES_KEYLEN 32 35 | #define AES_keyExpSize 240 36 | #elif defined(AES192) && (AES192 == 1) 37 | #define AES_KEYLEN 24 38 | #define AES_keyExpSize 208 39 | #else 40 | #define AES_KEYLEN 16 // Key length in bytes 41 | #define AES_keyExpSize 176 42 | #endif 43 | 44 | struct AES_ctx 45 | { 46 | uint8_t RoundKey[AES_keyExpSize]; 47 | #if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) 48 | uint8_t Iv[AES_BLOCKLEN]; 49 | #endif 50 | }; 51 | 52 | void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key); 53 | #if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) 54 | void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv); 55 | void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv); 56 | #endif 57 | 58 | #if defined(ECB) && (ECB == 1) 59 | // buffer size is exactly AES_BLOCKLEN bytes; 60 | // you need only AES_init_ctx as IV is not used in ECB 61 | // NB: ECB is considered insecure for most uses 62 | void AES_ECB_encrypt(const struct AES_ctx* ctx, uint8_t* buf); 63 | void AES_ECB_decrypt(const struct AES_ctx* ctx, uint8_t* buf); 64 | 65 | #endif // #if defined(ECB) && (ECB == !) 66 | 67 | 68 | #if defined(CBC) && (CBC == 1) 69 | // buffer size MUST be mutile of AES_BLOCKLEN; 70 | // Suggest https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme 71 | // NOTES: you need to set IV in ctx via AES_init_ctx_iv() or AES_ctx_set_iv() 72 | // no IV should ever be reused with the same key 73 | void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); 74 | void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); 75 | 76 | #endif // #if defined(CBC) && (CBC == 1) 77 | 78 | 79 | #if defined(CTR) && (CTR == 1) 80 | 81 | // Same function for encrypting as for decrypting. 82 | // IV is incremented for every block, and used after encryption as XOR-compliment for output 83 | // Suggesting https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme 84 | // NOTES: you need to set IV in ctx with AES_init_ctx_iv() or AES_ctx_set_iv() 85 | // no IV should ever be reused with the same key 86 | void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); 87 | 88 | #endif // #if defined(CTR) && (CTR == 1) 89 | 90 | 91 | #endif // _AES_H_ 92 | -------------------------------------------------------------------------------- /tests/unit-tests/test.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef _OSDP_TEST_H_ 8 | #define _OSDP_TEST_H_ 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "osdp_common.h" 15 | 16 | #define SUB_1 " -- " 17 | #define SUB_2 " -- " 18 | 19 | #define DO_TEST(t, m) \ 20 | do { \ 21 | t->tests++; \ 22 | if (m(t->mock_data)) { \ 23 | t->failure++; \ 24 | } else { \ 25 | t->success++; \ 26 | } \ 27 | } while (0) 28 | 29 | #define TEST_REPORT(t, s) \ 30 | do { \ 31 | t->tests++; \ 32 | if (s == true) \ 33 | t->success++; \ 34 | else \ 35 | t->failure++; \ 36 | } while (0) 37 | 38 | #define CHECK_ARRAY(a, l, e) \ 39 | do { \ 40 | if (l < 0) \ 41 | printf("error! invalid length %d\n", len); \ 42 | else if (l != sizeof(e) || memcmp(a, e, sizeof(e))) { \ 43 | printf("error! comparison failed!\n"); \ 44 | hexdump(e, sizeof(e), SUB_1 "Expected"); \ 45 | hexdump(a, l, SUB_1 "Found"); \ 46 | return -1; \ 47 | } \ 48 | } while (0) 49 | 50 | struct test { 51 | int loglevel; 52 | int success; 53 | int failure; 54 | int tests; 55 | void *mock_data; 56 | }; 57 | 58 | /* Helpers */ 59 | int test_setup_devices(struct test *t, osdp_t **cp, osdp_t **pd); 60 | int async_runner_start(osdp_t *ctx, void (*fn)(osdp_t *)); 61 | int async_runner_stop(int runner); 62 | int async_cp_runner_start(osdp_t *cp_ctx); 63 | int async_pd_runner_start(osdp_t *pd_ctx); 64 | int async_cp_runner_stop(int work_id); 65 | int async_pd_runner_stop(int work_id); 66 | void enable_line_noise(); 67 | void disable_line_noise(); 68 | void print_line_noise_stats(); 69 | 70 | void run_cp_fsm_tests(struct test *t); 71 | void run_cp_phy_fsm_tests(struct test *t); 72 | void run_cp_phy_tests(struct test *t); 73 | void run_file_tx_tests(struct test *t, bool line_noise); 74 | void run_command_tests(struct test *t); 75 | void run_event_tests(struct test *t); 76 | void run_hotplug_tests(struct test *t); 77 | void run_async_fuzz_tests(struct test *t); 78 | 79 | #endif 80 | -------------------------------------------------------------------------------- /python/osdp/constants.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | import osdp_sys 8 | 9 | class LibFlag: 10 | EnforceSecure = osdp_sys.FLAG_ENFORCE_SECURE 11 | InstallMode = osdp_sys.FLAG_INSTALL_MODE 12 | IgnoreUnsolicited = osdp_sys.FLAG_IGN_UNSOLICITED 13 | EnableNotification = osdp_sys.FLAG_ENABLE_NOTIFICATION 14 | CapturePackets = osdp_sys.FLAG_CAPTURE_PACKETS 15 | AllowEmptyEncryptedDataBlock = osdp_sys.FLAG_ALLOW_EMPTY_ENCRYPTED_DATA_BLOCK 16 | 17 | class LogLevel: 18 | Emergency = osdp_sys.LOG_EMERG 19 | Alert = osdp_sys.LOG_ALERT 20 | Critical = osdp_sys.LOG_CRIT 21 | Error = osdp_sys.LOG_ERROR 22 | Warning = osdp_sys.LOG_WARNING 23 | Notice = osdp_sys.LOG_NOTICE 24 | Info = osdp_sys.LOG_INFO 25 | Debug = osdp_sys.LOG_DEBUG 26 | 27 | class StatusReportType: 28 | Local = osdp_sys.STATUS_REPORT_LOCAL 29 | Input = osdp_sys.STATUS_REPORT_INPUT 30 | Output = osdp_sys.STATUS_REPORT_OUTPUT 31 | Remote = osdp_sys.STATUS_REPORT_REMOTE 32 | 33 | class Command: 34 | Output = osdp_sys.CMD_OUTPUT 35 | Buzzer = osdp_sys.CMD_BUZZER 36 | LED = osdp_sys.CMD_LED 37 | Comset = osdp_sys.CMD_COMSET 38 | ComsetDone = osdp_sys.CMD_COMSET_DONE 39 | Text = osdp_sys.CMD_TEXT 40 | Manufacturer = osdp_sys.CMD_MFG 41 | Keyset = osdp_sys.CMD_KEYSET 42 | FileTransfer = osdp_sys.CMD_FILE_TX 43 | Status = osdp_sys.CMD_STATUS 44 | 45 | class CommandLEDColor: 46 | Black = osdp_sys.LED_COLOR_NONE 47 | Red = osdp_sys.LED_COLOR_RED 48 | Green = osdp_sys.LED_COLOR_GREEN 49 | Amber = osdp_sys.LED_COLOR_AMBER 50 | Blue = osdp_sys.LED_COLOR_BLUE 51 | Magenta = osdp_sys.LED_COLOR_MAGENTA 52 | Cyan = osdp_sys.LED_COLOR_CYAN 53 | White = osdp_sys.LED_COLOR_WHITE 54 | 55 | class CommandFileTxFlags: 56 | Cancel = osdp_sys.CMD_FILE_TX_FLAG_CANCEL 57 | 58 | class EventNotification: 59 | Command = osdp_sys.EVENT_NOTIFICATION_COMMAND 60 | SecureChannelStatus = osdp_sys.EVENT_NOTIFICATION_SC_STATUS 61 | PeripheralDeviceStatus = osdp_sys.EVENT_NOTIFICATION_PD_STATUS 62 | 63 | class Event: 64 | CardRead = osdp_sys.EVENT_CARDREAD 65 | KeyPress = osdp_sys.EVENT_KEYPRESS 66 | ManufacturerReply = osdp_sys.EVENT_MFGREP 67 | Status = osdp_sys.EVENT_STATUS 68 | Notification = osdp_sys.EVENT_NOTIFICATION 69 | 70 | class CardFormat: 71 | Unspecified = osdp_sys.CARD_FMT_RAW_UNSPECIFIED 72 | Wiegand = osdp_sys.CARD_FMT_RAW_WIEGAND 73 | ASCII = osdp_sys.CARD_FMT_ASCII 74 | 75 | class Capability: 76 | Unused = osdp_sys.CAP_UNUSED 77 | ContactStatusMonitoring = osdp_sys.CAP_CONTACT_STATUS_MONITORING 78 | OutputControl = osdp_sys.CAP_OUTPUT_CONTROL 79 | CardDataFormat = osdp_sys.CAP_CARD_DATA_FORMAT 80 | LEDControl = osdp_sys.CAP_READER_LED_CONTROL 81 | AudibleControl = osdp_sys.CAP_READER_AUDIBLE_OUTPUT 82 | TextOutput = osdp_sys.CAP_READER_TEXT_OUTPUT 83 | TimeKeeping = osdp_sys.CAP_TIME_KEEPING 84 | CheckCharacter = osdp_sys.CAP_CHECK_CHARACTER_SUPPORT 85 | CommunicationSecurity = osdp_sys.CAP_COMMUNICATION_SECURITY 86 | ReceiveBufferSize = osdp_sys.CAP_RECEIVE_BUFFERSIZE 87 | CombinedMessageSize = osdp_sys.CAP_LARGEST_COMBINED_MESSAGE_SIZE 88 | SmartCard = osdp_sys.CAP_SMART_CARD_SUPPORT 89 | Readers = osdp_sys.CAP_READERS 90 | Biometrics = osdp_sys.CAP_BIOMETRICS 91 | -------------------------------------------------------------------------------- /src/osdp_file.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef _OSDP_FILE_H_ 8 | #define _OSDP_FILE_H_ 9 | 10 | #include "osdp_common.h" 11 | 12 | #define TO_FILE(pd) (pd)->file 13 | 14 | #define OSDP_FILE_TX_STATE_IDLE 0 15 | #define OSDP_FILE_TX_STATE_PENDING 1 16 | #define OSDP_FILE_TX_STATE_ERROR -1 17 | #define OSDP_FILE_TX_STATE_WAIT -2 18 | 19 | /** 20 | * @brief OSDP specified command: File Transfer: 21 | * 22 | * @param type File transfer type 23 | * - 1: opaque file contents recognizable by this specific PD 24 | * - 2..127: Reserved for future use 25 | * - 128..255: Reserved for private use 26 | * @param size File size (4 bytes,) little-endian format. 27 | * @param offset Offset in file of current message. 28 | * @param length Length of data section in this command. 29 | * @param data File contents. Variable length 30 | */ 31 | PACK(struct osdp_cmd_file_xfer { 32 | uint8_t type; 33 | uint32_t size; 34 | uint32_t offset; 35 | uint16_t length; 36 | uint8_t data[]; 37 | }); 38 | 39 | /** 40 | * @brief OSDP specified command: File Transfer Stat: 41 | * 42 | * @param control Control flags. 43 | * - bit-0: 1 = OK to interleave; 0 = dedicate for filetransfer 44 | * - bit-1: 1 = shall leave secure channel for file transfer; 0 = stay in 45 | * secure channel if SC is active 46 | * - bit-2: 1 = separate poll response is available; 0=no other activity 47 | * @param delay Request CP for a time delay in milliseconds before next 48 | * CMD_FILETRANSFER message 49 | * @param status File transfer status. This is a signed little- endian number 50 | * - 0: ok to proceed 51 | * - 1: file contents processed 52 | * - 2: rebooting now, expect full communications reset 53 | * - 3: PD is finishing file transfer. PD should send CMD_FILETRANSFER 54 | * with data length set to 0 (idle) until this status changes 55 | * - -1: abort file transfer 56 | * - -2: unrecognized file contents 57 | * - -3: file data unacceptable (malformed) 58 | * @param rx_size Alternate maximum message size for CMD_FILETRANSFER. If set to 59 | * 0 then no change requested, otherwise use this value 60 | */ 61 | PACK(struct osdp_cmd_file_stat { 62 | uint8_t control; 63 | uint16_t delay; 64 | int16_t status; 65 | uint16_t rx_size; 66 | }); 67 | 68 | enum file_tx_state_e { 69 | OSDP_FILE_IDLE, 70 | OSDP_FILE_INPROG, 71 | OSDP_FILE_DONE, 72 | OSDP_FILE_KEEP_ALIVE, 73 | }; 74 | 75 | struct osdp_file { 76 | uint32_t flags; 77 | int file_id; 78 | enum file_tx_state_e state; 79 | int length; 80 | uint32_t size; 81 | uint32_t offset; 82 | int errors; 83 | bool cancel_req; 84 | int64_t tstamp; 85 | uint32_t wait_time_ms; 86 | struct osdp_file_ops ops; 87 | }; 88 | 89 | int osdp_file_cmd_tx_build(struct osdp_pd *pd, uint8_t *buf, int max_len); 90 | int osdp_file_cmd_tx_decode(struct osdp_pd *pd, uint8_t *buf, int len); 91 | int osdp_file_cmd_stat_decode(struct osdp_pd *pd, uint8_t *buf, int len); 92 | int osdp_file_cmd_stat_build(struct osdp_pd *pd, uint8_t *buf, int max_len); 93 | int osdp_file_tx_command(struct osdp_pd *pd, int file_id, uint32_t flags); 94 | int osdp_file_tx_get_command(struct osdp_pd *pd); 95 | void osdp_file_tx_abort(struct osdp_pd *pd); 96 | 97 | #endif /* _OSDP_FILE_H_ */ 98 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at sidcha.dev@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /tests/unit-tests/test-cp-fsm.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | 9 | #include 10 | #include "test.h" 11 | 12 | extern int (*test_state_update)(struct osdp_pd *); 13 | 14 | int test_fsm_resp = 0; 15 | 16 | int test_cp_fsm_send(void *data, uint8_t *buf, int len) 17 | { 18 | ARG_UNUSED(data); 19 | 20 | #ifndef OPT_OSDP_SKIP_MARK_BYTE 21 | int cmd_id_offset = OSDP_CMD_ID_OFFSET + 1; 22 | #else 23 | int cmd_id_offset = OSDP_CMD_ID_OFFSET; 24 | #endif 25 | 26 | switch (buf[cmd_id_offset]) { 27 | case 0x60: 28 | test_fsm_resp = 1; 29 | break; 30 | case 0x61: 31 | test_fsm_resp = 2; 32 | break; 33 | case 0x62: 34 | test_fsm_resp = 3; 35 | break; 36 | default: 37 | printf(SUB_1 "invalid ID:0x%02x\n", buf[cmd_id_offset 38 | 39 | ]); 40 | } 41 | return len; 42 | } 43 | 44 | int test_cp_fsm_receive(void *data, uint8_t *buf, int len) 45 | { 46 | ARG_UNUSED(data); 47 | 48 | uint8_t resp_id[] = { 49 | #ifndef OPT_OSDP_SKIP_MARK_BYTE 50 | 0xff, 51 | #endif 52 | 0x53, 0xe5, 0x14, 0x00, 0x04, 0x45, 0xa1, 0xa2, 0xa3, 0xb1, 53 | 0xc1, 0xd1, 0xd2, 0xd3, 0xd4, 0xe1, 0xe2, 0xe3, 0xf8, 0xd9 54 | }; 55 | uint8_t resp_cap[] = { 56 | #ifndef OPT_OSDP_SKIP_MARK_BYTE 57 | 0xff, 58 | #endif 59 | 0x53, 0xe5, 0x0b, 0x00, 0x05, 0x46, 0x04, 0x04, 0x01, 0xb3, 0xec 60 | }; 61 | uint8_t resp_ack[] = { 62 | #ifndef OPT_OSDP_SKIP_MARK_BYTE 63 | 0xff, 64 | #endif 65 | 0x53, 0xe5, 0x08, 0x00, 0x06, 0x40, 0xb0, 0xf0 66 | }; 67 | 68 | ARG_UNUSED(len); 69 | 70 | switch (test_fsm_resp) { 71 | case 1: 72 | memcpy(buf, resp_ack, sizeof(resp_ack)); 73 | return sizeof(resp_ack); 74 | case 2: 75 | memcpy(buf, resp_id, sizeof(resp_id)); 76 | return sizeof(resp_id); 77 | case 3: 78 | memcpy(buf, resp_cap, sizeof(resp_cap)); 79 | return sizeof(resp_cap); 80 | } 81 | return -1; 82 | } 83 | 84 | int test_cp_fsm_setup(struct test *t) 85 | { 86 | /* mock application data */ 87 | osdp_pd_info_t info = { 88 | .address = 101, 89 | .baud_rate = 9600, 90 | .flags = 0, 91 | .channel.data = NULL, 92 | .channel.send = test_cp_fsm_send, 93 | .channel.recv = test_cp_fsm_receive, 94 | .channel.flush = NULL, 95 | .scbk = NULL, 96 | }; 97 | osdp_logger_init("osdp::cp", t->loglevel, NULL); 98 | struct osdp *ctx = (struct osdp *)osdp_cp_setup(1, &info); 99 | if (ctx == NULL) { 100 | printf(" init failed!\n"); 101 | return -1; 102 | } 103 | SET_CURRENT_PD(ctx, 0); 104 | SET_FLAG(GET_CURRENT_PD(ctx), PD_FLAG_SKIP_SEQ_CHECK); 105 | t->mock_data = (void *)ctx; 106 | return 0; 107 | } 108 | 109 | void test_cp_fsm_teardown(struct test *t) 110 | { 111 | osdp_cp_teardown(t->mock_data); 112 | } 113 | 114 | void run_cp_fsm_tests(struct test *t) 115 | { 116 | int result = true; 117 | uint32_t count = 0; 118 | struct osdp *ctx; 119 | 120 | printf("\nStarting CP Phy state tests\n"); 121 | 122 | if (test_cp_fsm_setup(t)) 123 | return; 124 | 125 | ctx = t->mock_data; 126 | 127 | printf(SUB_1 "executing state_update()\n"); 128 | while (1) { 129 | test_state_update(GET_CURRENT_PD(ctx)); 130 | 131 | if (GET_CURRENT_PD(ctx)->state == OSDP_CP_STATE_OFFLINE) { 132 | printf(SUB_2 "state_update() CP went offline\n"); 133 | result = false; 134 | break; 135 | } 136 | if (count++ > 300) 137 | break; 138 | usleep(1000); 139 | } 140 | printf(SUB_1 "state_update test %s\n", result ? "succeeded" : "failed"); 141 | 142 | TEST_REPORT(t, result); 143 | 144 | test_cp_fsm_teardown(t); 145 | } 146 | 147 | // unnecessary 148 | -------------------------------------------------------------------------------- /include/osdp.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef LIBOSDP_OSDP_HPP_ 8 | #define LIBOSDP_OSDP_HPP_ 9 | 10 | #include 11 | 12 | /** 13 | * @file: LibOSDP classical wrapper. See osdp.h for documentation. 14 | */ 15 | 16 | namespace OSDP { 17 | 18 | class OSDP_EXPORT Common { 19 | public: 20 | Common() : _ctx(nullptr) {} 21 | 22 | void logger_init(const char *name, int log_level, 23 | osdp_log_puts_fn_t puts_fn) 24 | { 25 | osdp_logger_init(name, log_level, puts_fn); 26 | } 27 | 28 | const char *get_version() 29 | { 30 | return osdp_get_version(); 31 | } 32 | 33 | const char *get_source_info() 34 | { 35 | return osdp_get_source_info(); 36 | } 37 | 38 | void get_status_mask(uint8_t *bitmask) 39 | { 40 | osdp_get_status_mask(_ctx, bitmask); 41 | } 42 | 43 | void get_sc_status_mask(uint8_t *bitmask) 44 | { 45 | osdp_get_sc_status_mask(_ctx, bitmask); 46 | } 47 | 48 | int file_register_ops(int pd, struct osdp_file_ops *ops) 49 | { 50 | return osdp_file_register_ops(_ctx, pd, ops); 51 | } 52 | 53 | int file_tx_get_status(int pd, int *size, int *offset) 54 | { 55 | return osdp_get_file_tx_status(_ctx, pd, size, offset); 56 | } 57 | 58 | protected: 59 | osdp_t *_ctx; 60 | }; 61 | 62 | class OSDP_EXPORT ControlPanel : public Common { 63 | public: 64 | ControlPanel() {} 65 | 66 | ~ControlPanel() 67 | { 68 | if (_ctx) { 69 | osdp_cp_teardown(_ctx); 70 | } 71 | } 72 | 73 | bool setup(int num_pd, osdp_pd_info_t *info) 74 | { 75 | _ctx = osdp_cp_setup(num_pd, info); 76 | return _ctx != nullptr; 77 | } 78 | 79 | bool setup() 80 | { 81 | return setup(0, nullptr); 82 | } 83 | 84 | int add_pd(int num_pd, osdp_pd_info_t *info) 85 | { 86 | return osdp_cp_add_pd(_ctx, num_pd, info); 87 | } 88 | 89 | void refresh() 90 | { 91 | osdp_cp_refresh(_ctx); 92 | } 93 | 94 | [[deprecated]] 95 | int send_command(int pd, struct osdp_cmd *cmd) 96 | { 97 | return osdp_cp_submit_command(_ctx, pd, cmd); 98 | } 99 | 100 | int submit_command(int pd, struct osdp_cmd *cmd) 101 | { 102 | return osdp_cp_submit_command(_ctx, pd, cmd); 103 | } 104 | 105 | void set_event_callback(cp_event_callback_t cb, void *arg) 106 | { 107 | osdp_cp_set_event_callback(_ctx, cb, arg); 108 | } 109 | 110 | int get_pd_id(int pd, struct osdp_pd_id *id) 111 | { 112 | return osdp_cp_get_pd_id(_ctx, pd, id); 113 | } 114 | 115 | int get_capability(int pd, struct osdp_pd_cap *cap) 116 | { 117 | return osdp_cp_get_capability(_ctx, pd, cap); 118 | } 119 | 120 | }; 121 | 122 | class OSDP_EXPORT PeripheralDevice : public Common { 123 | public: 124 | PeripheralDevice() {} 125 | 126 | ~PeripheralDevice() 127 | { 128 | if (_ctx) { 129 | osdp_pd_teardown(_ctx); 130 | } 131 | } 132 | 133 | bool setup(osdp_pd_info_t *info) 134 | { 135 | _ctx = osdp_pd_setup(info); 136 | return _ctx != nullptr; 137 | } 138 | 139 | void refresh() 140 | { 141 | osdp_pd_refresh(_ctx); 142 | } 143 | 144 | void set_command_callback(pd_command_callback_t cb, void* args) 145 | { 146 | osdp_pd_set_command_callback(_ctx, cb, args); 147 | } 148 | 149 | [[deprecated]] 150 | int notify_event(struct osdp_event *event) 151 | { 152 | return osdp_pd_submit_event(_ctx, event); 153 | } 154 | 155 | int submit_event(struct osdp_event *event) 156 | { 157 | return osdp_pd_submit_event(_ctx, event); 158 | } 159 | 160 | int flush_events() 161 | { 162 | return osdp_pd_flush_events(_ctx); 163 | } 164 | }; 165 | 166 | }; /* namespace OSDP */ 167 | 168 | #endif // LIBOSDP_OSDP_HPP_ 169 | -------------------------------------------------------------------------------- /.github/workflows/build-ci.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | name: Build CI 8 | 9 | on: 10 | push: 11 | branches: [ master ] 12 | pull_request: 13 | branches: [ master ] 14 | 15 | jobs: 16 | CmakeBuild: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | submodules: recursive 22 | - name: Configure 23 | run: cmake -DCMAKE_BUILD_TYPE=Debug . 24 | - name: Build 25 | run: cmake --build . --parallel 7 --target all --target cpp_cp_sample --target cpp_pd_sample 26 | - name: Package 27 | run: cmake --build . --target package --target package_source 28 | - uses: actions/upload-artifact@v4 29 | with: 30 | name: libosdp-ubuntu-latest-binaries.zip 31 | path: artifacts/ 32 | 33 | MakeBuild: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | with: 38 | submodules: recursive 39 | - name: configure 40 | run: | 41 | ./configure.sh 42 | - name: make 43 | run: make V=1 44 | 45 | DocBuild: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v4 49 | with: 50 | submodules: recursive 51 | - name: Install dependencies 52 | run: | 53 | sudo apt update 54 | sudo apt install -y doxygen 55 | pip3 install -r doc/requirements.txt 56 | - name: Configure 57 | run: cmake -DCMAKE_BUILD_TYPE=Debug . 58 | - name: Build docs 59 | run: cmake --build . --parallel 7 --target html_docs 60 | 61 | Test: 62 | runs-on: ubuntu-latest 63 | steps: 64 | - uses: actions/checkout@v4 65 | with: 66 | submodules: recursive 67 | - name: Configure 68 | run: cmake -DCMAKE_BUILD_TYPE=Debug . 69 | - name: Run unit-tests 70 | run: cmake --build . --parallel 7 --target check-ut 71 | - name: Run pytest 72 | run: tests/pytest/run.sh 73 | 74 | CheckPatch: 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v4 78 | with: 79 | submodules: recursive 80 | - name: ClangFormatCheck 81 | run: | 82 | ./scripts/clang-format-check.sh 83 | 84 | Python: 85 | name: Python 86 | runs-on: ubuntu-latest 87 | steps: 88 | - name: Checkout sources 89 | uses: actions/checkout@v4 90 | with: 91 | submodules: recursive 92 | - name: Install pypa/build 93 | run: python3 -m pip install build --user 94 | - name: Build a binary wheel and a source tarball 95 | run: python3 -m build python 96 | - name: Store the distribution packages 97 | uses: actions/upload-artifact@v4 98 | with: 99 | name: python-package-distributions 100 | path: python/dist/ 101 | 102 | PlatformIO: 103 | runs-on: ubuntu-latest 104 | strategy: 105 | matrix: 106 | examples: 107 | - examples/platformio/cp.ino 108 | - examples/platformio/pd.ino 109 | steps: 110 | - uses: actions/checkout@v4 111 | with: 112 | submodules: recursive 113 | - uses: actions/cache@v4 114 | with: 115 | path: | 116 | ~/.cache/pip 117 | ~/.platformio/.cache 118 | key: ${{ runner.os }}-pio 119 | - uses: actions/setup-python@v5 120 | with: 121 | python-version: '3.11' 122 | - name: Install PlatformIO Core 123 | run: pip install --upgrade platformio 124 | 125 | - name: Build PlatformIO examples 126 | run: pio ci --verbose --lib . --board=megaatmega2560 ${{ matrix.examples }} 127 | -------------------------------------------------------------------------------- /doc/protocol/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | OSDP describes the communication protocol for interfacing one or more Peripheral 5 | Devices (PD) to a Control Panel (CP). The OSDP specification describes the 6 | protocol implementation over a two-wire RS-485 multi-drop serial communication 7 | channel Nevertheless, this protocol can be used to transfer secure data over any 8 | physical channel. 9 | 10 | LibOSDP complies with v2.2 of the OSDP specification. This page pulls excepts 11 | from the specification that are crucial points when trying to understand the 12 | protocol. 13 | 14 | Physical Interface 15 | ------------------ 16 | 17 | Half-duplex RS-485 - one twisted pair, shield/signal ground 18 | 19 | Signaling 20 | --------- 21 | 22 | Half duplex asynchronous serial 8 data bits, 1 stop bit, no parity bits, with 23 | either one of 9600, 19200, 38400, 57600, 115200 or 230400 baud rates. 24 | 25 | Character Encoding 26 | ------------------ 27 | 28 | The complete 8-bit character is used. All possible bit patterns may appear 29 | within a message. 30 | 31 | Channel Access 32 | -------------- 33 | 34 | The communication channel is used in the “interrogation/reply” mode. Only the CP 35 | may spontaneously send a message. Each message sent by the CP is addressed to 36 | one and only one PD. 37 | 38 | Timing 39 | ------ 40 | 41 | The transmitting device shall guarantee a gap of a minimum of two character 42 | times before it may access the communication channel. This idle line delay is 43 | required to allow for signal converters and/or multiplexers to sense that the 44 | line has become idle. 45 | 46 | The PD shall send a single reply message to each message addressed to it within 47 | 200 ms. If the PD is unable to accept the command for processing due to temporary 48 | unavailability of a resource required to process the command, then the PD shall 49 | send the osdp_BUSY reply. When the CP receives the osdp_BUSY reply, it may, at 50 | its discretion, choose to re-send the same command as it would if the command 51 | delivery timed out. 52 | 53 | The typical REPLY_DELAY should be less than 3 milliseconds. If a device is 54 | overwhelmed it can send a BUSY message. 55 | 56 | Message Synchronization 57 | ----------------------- 58 | 59 | The general procedure for a peripheral device (PD) to obtain message 60 | synchronization is to wait for an inter-character timeout then look for a 61 | Start-Of-Message (SOM) code. The device should then receive and store at least 62 | the header fields while computing the checksum/CRC on the rest of the message. 63 | If the checksum is good, only the PD that matches the address field processes 64 | the message. All other PDs, however, should monitor the packet by counting the 65 | remaining portion of packet to be able to anticipate the start of the next 66 | packet. 67 | 68 | If there is an inter-character timeout while receiving the message the PD shall 69 | abort the receive sequence. Once aborted, the PD should re-sync using the method 70 | described above. 71 | 72 | The nominal value of the inter-character timeout shall be 20 milliseconds. This 73 | parameter may need to be adjusted for special channel timing considerations. 74 | 75 | Packet Structure 76 | ---------------- 77 | 78 | See `packet structure documentation`_. 79 | 80 | .. _packet structure documentation: packet-structure.html 81 | 82 | Peripheral Device Capabilities 83 | ------------------------------ 84 | 85 | OSDP PDs must advertise a list of predefined capabilities to the CP in response 86 | the osdp_PDCAP command. See a `comprehensive list PD capabilities`_. 87 | 88 | .. _comprehensive list PD capabilities: pd-capabilities.html 89 | 90 | for more details. 91 | 92 | Commands 93 | -------- 94 | 95 | See list of `commands supported by LibOSDP`_. 96 | 97 | .. _commands supported by LibOSDP: commands-and-replies.html 98 | -------------------------------------------------------------------------------- /scripts/run_pytests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # LibOSDP PyTest Runner - Enhanced script for running Python tests 4 | # 5 | # USAGE EXAMPLES: 6 | # 7 | # Show help: 8 | # ./scripts/run_pytests.sh --help 9 | # 10 | # Run all tests (full setup): 11 | # ./scripts/run_pytests.sh 12 | # 13 | # Run specific test file: 14 | # ./scripts/run_pytests.sh test_commands.py 15 | # 16 | # Run specific test function: 17 | # ./scripts/run_pytests.sh test_commands.py::test_command_mfg_with_reply 18 | # 19 | # Run tests matching pattern: 20 | # ./scripts/run_pytests.sh -k "mfg" 21 | # 22 | # Skip setup and run specific test (faster for development): 23 | # ./scripts/run_pytests.sh -s test_events.py::test_event_mfg_reply 24 | # 25 | # Fast fail with quiet output: 26 | # ./scripts/run_pytests.sh -q -f 27 | # 28 | # Custom pytest arguments: 29 | # ./scripts/run_pytests.sh --tb=short -x 30 | # 31 | 32 | set -e 33 | 34 | SCRIPTS_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 35 | ROOT_DIR="${SCRIPTS_DIR}/../" 36 | 37 | # Help function 38 | show_help() { 39 | cat << EOF 40 | LibOSDP PyTest Runner 41 | 42 | USAGE: 43 | $0 [OPTIONS] [PYTEST_ARGS] 44 | 45 | DESCRIPTION: 46 | Sets up an isolated Python environment and runs LibOSDP Python tests. 47 | 48 | OPTIONS: 49 | -h, --help Show this help message 50 | -s, --skip-setup Skip environment setup (use existing .venv) 51 | -q, --quiet Run tests with minimal output 52 | -v, --verbose Run tests with extra verbose output (default) 53 | -f, --fast-fail Stop on first test failure 54 | 55 | EXAMPLES: 56 | # Run all tests 57 | $0 58 | 59 | # Run specific test file 60 | $0 test_commands.py 61 | 62 | # Run specific test function 63 | $0 test_commands.py::test_command_mfg_with_reply 64 | 65 | # Run tests matching pattern 66 | $0 -k "mfg" 67 | 68 | # Run with custom pytest args 69 | $0 --tb=short -x 70 | 71 | # Skip setup and run specific test 72 | $0 -s test_events.py::test_event_mfg_reply 73 | 74 | EOF 75 | } 76 | 77 | # Parse arguments 78 | SKIP_SETUP=false 79 | PYTEST_ARGS=() 80 | VERBOSE_LEVEL="-vv" 81 | 82 | while [[ $# -gt 0 ]]; do 83 | case $1 in 84 | -h|--help) 85 | show_help 86 | exit 0 87 | ;; 88 | -s|--skip-setup) 89 | SKIP_SETUP=true 90 | shift 91 | ;; 92 | -q|--quiet) 93 | VERBOSE_LEVEL="-v" 94 | shift 95 | ;; 96 | -v|--verbose) 97 | VERBOSE_LEVEL="-vv" 98 | shift 99 | ;; 100 | -f|--fast-fail) 101 | PYTEST_ARGS+=("-x") 102 | shift 103 | ;; 104 | *) 105 | # All other arguments are passed to pytest 106 | PYTEST_ARGS+=("$1") 107 | shift 108 | ;; 109 | esac 110 | done 111 | 112 | pushd ${ROOT_DIR}/tests/pytest/ 113 | 114 | if [ "$SKIP_SETUP" = false ]; then 115 | echo "[-] Creating an isolated environment.." 116 | rm -rf __pycache__/ 117 | rm -rf .venv ${ROOT_DIR}/python/{build,dist,libosdp.egg-info,vendor} 118 | python3 -m venv .venv 119 | source ./.venv/bin/activate 120 | pip install --upgrade pip 121 | 122 | echo "[-] Installing dependencies.." 123 | pip install -r requirements.txt 124 | 125 | echo "[-] Installing libosdp.." 126 | pip install "${ROOT_DIR}/python" 127 | else 128 | echo "[-] Using existing environment.." 129 | if [ ! -d .venv ]; then 130 | echo "ERROR: .venv directory not found. Run without -s/--skip-setup first." 131 | exit 1 132 | fi 133 | source ./.venv/bin/activate 134 | fi 135 | 136 | echo "[-] Running tests capturing all output.." 137 | pytest ${VERBOSE_LEVEL} --show-capture=all "${PYTEST_ARGS[@]}" 138 | -------------------------------------------------------------------------------- /tests/pytest/test_hotplug.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | import time 8 | import pytest 9 | 10 | from osdp import * 11 | from conftest import make_fifo_pair, cleanup_fifo_pair, assert_command_received, wait_for_notification_event, wait_for_non_notification_event 12 | 13 | secure_pd_addr = 101 14 | insecure_pd_addr = 102 15 | 16 | f1_1, f1_2 = make_fifo_pair("secure_pd") 17 | f2_1, f2_2 = make_fifo_pair("insecure_pd") 18 | 19 | key = KeyStore.gen_key() 20 | 21 | pd_cap = PDCapabilities([ 22 | (Capability.OutputControl, 1, 1), 23 | (Capability.LEDControl, 1, 1), 24 | (Capability.AudibleControl, 1, 1), 25 | (Capability.TextOutput, 1, 1), 26 | ]) 27 | 28 | pd_info_list = [ 29 | PDInfo(secure_pd_addr, f1_1, scbk=key, flags=[ LibFlag.EnforceSecure, LibFlag.EnableNotification ]), 30 | PDInfo(insecure_pd_addr, f2_1, flags=[ LibFlag.EnableNotification ]) 31 | ] 32 | 33 | secure_pd = PeripheralDevice( 34 | PDInfo(secure_pd_addr, f1_2, scbk=key, flags=[ LibFlag.EnforceSecure ]), 35 | pd_cap, 36 | log_level=LogLevel.Debug 37 | ) 38 | 39 | insecure_pd = PeripheralDevice( 40 | PDInfo(insecure_pd_addr, f2_2), 41 | pd_cap, 42 | log_level=LogLevel.Debug 43 | ) 44 | 45 | pd_list = [ 46 | secure_pd, 47 | insecure_pd, 48 | ] 49 | 50 | cp = ControlPanel(pd_info_list, log_level=LogLevel.Debug) 51 | 52 | @pytest.fixture(scope='module', autouse=True) 53 | def setup_test(): 54 | for pd in pd_list: 55 | pd.start() 56 | cp.start() 57 | if not cp.online_wait_all(timeout=10): 58 | teardown_test() 59 | pytest.fail("Failed to bring all PDs online within timeout") 60 | yield 61 | teardown_test() 62 | 63 | def teardown_test(): 64 | cp.teardown() 65 | for pd in pd_list: 66 | pd.teardown() 67 | cleanup_fifo_pair("secure_pd") 68 | cleanup_fifo_pair("insecure_pd") 69 | 70 | def test_hotplug_enable_already_enabled(): 71 | assert cp.is_online(secure_pd_addr) 72 | assert cp.is_pd_enabled(secure_pd_addr) == True 73 | 74 | result = cp.enable_pd(secure_pd_addr) 75 | assert result == False, "enable_pd should return False when PD is already enabled" 76 | 77 | def test_hotplug_disable_functionality(): 78 | assert cp.is_online(secure_pd_addr) 79 | assert cp.is_pd_enabled(secure_pd_addr) == True 80 | 81 | result = cp.disable_pd(secure_pd_addr) 82 | assert result == True, "disable_pd should return True on success" 83 | 84 | # Allow time for state change processing 85 | time.sleep(0.5) 86 | 87 | assert cp.is_pd_enabled(secure_pd_addr) == False, "PD should be disabled after disable_pd" 88 | 89 | def test_hotplug_enable_after_disable(): 90 | cp.disable_pd(secure_pd_addr) 91 | time.sleep(0.5) 92 | assert cp.is_pd_enabled(secure_pd_addr) == False 93 | 94 | result = cp.enable_pd(secure_pd_addr) 95 | assert result == True, "enable_pd should return True on success" 96 | 97 | # Allow time for state change processing and re-initialization 98 | time.sleep(1) 99 | 100 | assert cp.is_pd_enabled(secure_pd_addr) == True, "PD should be enabled after enable_pd" 101 | 102 | def test_hotplug_command_blocking(): 103 | assert cp.is_pd_enabled(secure_pd_addr) == True 104 | 105 | cmd = { 106 | 'command': Command.Buzzer, 107 | 'reader': 0, 108 | 'control_code': 1, 109 | 'on_count': 1, 110 | 'off_count': 1, 111 | 'rep_count': 1 112 | } 113 | 114 | initial_result = cp.submit_command(secure_pd_addr, cmd) 115 | assert initial_result == True, "Commands should be accepted for enabled PDs" 116 | 117 | cp.disable_pd(secure_pd_addr) 118 | 119 | # Allow time for state change processing 120 | time.sleep(0.5) 121 | 122 | assert cp.is_pd_enabled(secure_pd_addr) == False 123 | 124 | disabled_result = cp.submit_command(secure_pd_addr, cmd) 125 | assert disabled_result == False, "Commands should be blocked for disabled PDs" 126 | -------------------------------------------------------------------------------- /scripts/make-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | usage() { 4 | cat >&2<<---- 5 | LibOSDP release helper 6 | 7 | OPTIONS: 8 | --patch Release version bump type: patch (default) 9 | --major Release version bump type: major 10 | --minor Release version bump type: minor 11 | -h, --help Print this help 12 | --- 13 | } 14 | 15 | function setup_py_inc_version() { 16 | dir=$1 17 | inc=$2 18 | perl -pi -se ' 19 | if (/^project_version = "(\d+)\.(\d+)\.(\d+)"$/) { 20 | $maj=$1; $min=$2; $pat=$3; 21 | if ($major) { $maj+=1; $min=0; $pat=0; } 22 | if ($minor) { $min+=1; $pat=0; } 23 | $pat+=1 if $patch; 24 | $_="project_version = \"$maj.$min.$pat\"\n" 25 | }' -- -$inc $dir/setup.py 26 | } 27 | 28 | function cmake_inc_version() { 29 | dir=$1 30 | inc=$2 31 | perl -pi -se ' 32 | if (/^project\((\w+) VERSION (\d+)\.(\d+)\.(\d+)\)$/) { 33 | $maj=$2; $min=$3; $pat=$4; 34 | if ($major) { $maj+=1; $min=0; $pat=0; } 35 | if ($minor) { $min+=1; $pat=0; } 36 | $pat+=1 if $patch; 37 | $_="project($1 VERSION $maj.$min.$pat)\n" 38 | }' -- -$inc $dir/CMakeLists.txt 39 | } 40 | 41 | function platformio_inc_version() { 42 | inc=$1 43 | perl -pi -se ' 44 | if (/^#define PROJECT_VERSION (\s+) "(\d+)\.(\d+)\.(\d+)"$/) { 45 | $maj=$2; $min=$3; $pat=$4; 46 | if ($major) { $maj+=1; $min=0; $pat=0; } 47 | if ($minor) { $min+=1; $pat=0; } 48 | $pat+=1 if $patch; 49 | $_="#define PROJECT_VERSION $1 \"$maj.$min.$pat\"\n" 50 | }' -- -$inc platformio/osdp_config.h 51 | 52 | perl -pi -se ' 53 | if (/^ "version": "(\d+)\.(\d+)\.(\d+)",$/) { 54 | $maj=$1; $min=$2; $pat=$3; 55 | if ($major) { $maj+=1; $min=0; $pat=0; } 56 | if ($minor) { $min+=1; $pat=0; } 57 | $pat+=1 if $patch; 58 | $_=" \"version\": \"$maj.$min.$pat\",\n" 59 | }' -- -$inc library.json 60 | } 61 | 62 | function generate_change_log() { 63 | last_rel=$(git tag --list 'v*' --sort=v:refname | tail -1) 64 | version=$(perl -ne 'print $1 if (/ VERSION (\d+.\d+.\d)\)$/)' CMakeLists.txt) 65 | 66 | cat <<-EOF 67 | v${version} ## TODO 68 | ------ 69 | 70 | $(date "+%d %B %Y") 71 | 72 | Release subject ## TODO" 73 | 74 | Enhancements: ## TODO" 75 | 76 | Fixes: ## TODO" 77 | EOF 78 | 79 | # Changes since last release 80 | git log ${last_rel}..HEAD --oneline --no-merges --decorate=no | perl -pe 's/^\w+ / - /' 81 | } 82 | 83 | function prepare_libosdp_release() { 84 | cmake_inc_version "." $1 85 | setup_py_inc_version "python" $1 86 | platformio_inc_version $1 87 | generate_change_log > /tmp/rel.txt 88 | printf '%s\n\n\n%s\n' "$(cat /tmp/rel.txt)" "$(cat CHANGELOG)" > CHANGELOG 89 | } 90 | 91 | function do_libosdp_release() { 92 | if [[ "$(git diff --stat)" == "" ]]; then 93 | prepare_libosdp_release $1 94 | exit 0 95 | fi 96 | git diff --cached --name-status | while read status file; do 97 | if [[ "$file" != "CHANGELOG" ]] && \ 98 | [[ "$file" != "CMakeLists.txt" ]] && \ 99 | [[ "$file" != "python/setup.py" ]] && \ 100 | [[ "$file" != "library.json" ]] && \ 101 | [[ "$file" != "platformio/osdp_config.h" ]] 102 | then 103 | echo "ERROR:" 104 | echo " Only CHANGELOG CMakeLists.txt and few other files must be modified" 105 | echo " to make a release commit. To prepare a new release, run this" 106 | echo " script on a clean git tree." 107 | exit 1 108 | fi 109 | done 110 | version=$(perl -ne 'print $1 if (/ VERSION (\d+\.\d+\.\d+)\)$/)' CMakeLists.txt) 111 | if grep -q -E "^v$version ## TODO$" CHANGELOG; then 112 | echo "CHANGELOG needs to be updated manually" 113 | exit 1 114 | fi 115 | git add CHANGELOG CMakeLists.txt python/setup.py library.json platformio/osdp_config.h && 116 | git commit -s -m "Release v$version" && 117 | git tag "v$version" -s -a -m "Release v$version" 118 | } 119 | 120 | INC="patch" 121 | COMPONENT="libosdp" 122 | while [ $# -gt 0 ]; do 123 | case $1 in 124 | --patch) INC="patch";; 125 | --major) INC="major";; 126 | --minor) INC="minor";; 127 | -h|--help) usage; exit 0;; 128 | *) echo -e "Unknown option $1\n"; usage; exit 1;; 129 | esac 130 | shift 131 | done 132 | 133 | do_libosdp_release $INC 134 | 135 | -------------------------------------------------------------------------------- /tests/pytest/test_status.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2023-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | import time 8 | import pytest 9 | 10 | from osdp import * 11 | from conftest import make_fifo_pair, cleanup_fifo_pair 12 | 13 | pd_cap = PDCapabilities([ 14 | (Capability.OutputControl, 1, 1), 15 | (Capability.LEDControl, 1, 1), 16 | (Capability.AudibleControl, 1, 1), 17 | (Capability.TextOutput, 1, 1), 18 | ]) 19 | 20 | key = KeyStore.gen_key() 21 | f1, f2 = make_fifo_pair("status") 22 | 23 | pd_addr = 101 24 | pd_info_list = [PDInfo(pd_addr, f2, scbk=key, flags=[LibFlag.EnforceSecure, LibFlag.EnableNotification])] 25 | 26 | pd = PeripheralDevice( 27 | PDInfo(pd_addr, f1, scbk=key, flags=[LibFlag.EnforceSecure]), 28 | pd_cap, 29 | log_level=LogLevel.Debug 30 | ) 31 | cp = ControlPanel(pd_info_list, log_level=LogLevel.Debug) 32 | 33 | @pytest.fixture(scope='module', autouse=True) 34 | def setup_test(): 35 | pd.start() 36 | cp.start() 37 | if not cp.sc_wait_all(timeout=10): 38 | teardown_test() 39 | pytest.fail("Failed to establish secure channel within timeout") 40 | yield 41 | teardown_test() 42 | 43 | def teardown_test(): 44 | try: 45 | if cp.thread: 46 | cp.teardown() 47 | except RuntimeError: 48 | pass # Already stopped 49 | try: 50 | if pd.thread: 51 | pd.teardown() 52 | except RuntimeError: 53 | pass # Already stopped 54 | cleanup_fifo_pair("status") 55 | 56 | def test_cp_online_status(): 57 | """Test CP's ability to detect PD online status""" 58 | # Verify PD is online and CP can detect it 59 | assert cp.is_online(pd_addr), "PD should be online" 60 | 61 | # Test online wait functionality 62 | assert cp.online_wait(pd_addr, timeout=1), "online_wait should return True for already online PD" 63 | 64 | # Test status bitmask includes this PD 65 | online_mask = cp.status() 66 | assert online_mask & (1 << 0), "PD should be set in online bitmask" 67 | 68 | def test_cp_sc_status(): 69 | """Test CP's ability to detect PD secure channel status""" 70 | # Verify SC is active and CP can detect it 71 | assert cp.is_sc_active(pd_addr), "Secure channel should be active" 72 | 73 | # Test SC wait functionality 74 | assert cp.sc_wait(pd_addr, timeout=1), "sc_wait should return True for already active SC" 75 | 76 | # Test SC status bitmask includes this PD 77 | sc_mask = cp.sc_status() 78 | assert sc_mask & (1 << 0), "PD should be set in SC bitmask" 79 | 80 | def test_pd_online_status(): 81 | """Test PD's ability to detect CP online status""" 82 | # Verify PD can detect that CP is online 83 | assert pd.is_online(), "PD should be online" 84 | 85 | def test_pd_sc_status(): 86 | """Test PD's ability to detect secure channel status""" 87 | # Verify PD can detect that SC is active 88 | assert pd.is_sc_active(), "Secure channel should be active" 89 | 90 | def test_status_bitmasks(): 91 | """Test status bitmask methods and counting functions""" 92 | # Test online status bitmask 93 | online_mask = cp.status() 94 | assert online_mask & (1 << 0), "PD should be set in online bitmask" 95 | assert cp.get_num_online() == 1, "Should have 1 PD online" 96 | 97 | # Test SC status bitmask 98 | sc_mask = cp.sc_status() 99 | assert sc_mask & (1 << 0), "PD should be set in SC bitmask" 100 | assert cp.get_num_sc_active() == 1, "Should have 1 PD with active SC" 101 | 102 | def test_status_wait_methods(): 103 | """Test various wait methods for status monitoring""" 104 | # Test waiting for already online PDs 105 | assert cp.online_wait_all(timeout=1), "online_wait_all should return True for already online PDs" 106 | assert cp.sc_wait_all(timeout=1), "sc_wait_all should return True for already active SCs" 107 | 108 | # Test individual wait methods 109 | assert cp.online_wait(pd_addr, timeout=1), "online_wait should return True for already online PD" 110 | assert cp.sc_wait(pd_addr, timeout=1), "sc_wait should return True for already active SC" 111 | -------------------------------------------------------------------------------- /.github/workflows/publish-pypi.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | name: Publish PyPI 8 | 9 | on: 10 | workflow_dispatch: 11 | workflow_run: 12 | workflows: ["Create Release"] 13 | types: [completed] 14 | 15 | jobs: 16 | build_wheels: 17 | name: "Build wheels on ${{ matrix.os }} (${{ matrix.image }}; Archs: ${{ matrix.archs }})" 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | include: 23 | # Linux manylinux 24 | - os: ubuntu-22.04 25 | platform: linux 26 | image: manylinux 27 | archs: x86_64 28 | - os: ubuntu-22.04 29 | platform: linux 30 | image: manylinux 31 | archs: aarch64 32 | 33 | # Linux musllinux 34 | - os: ubuntu-22.04 35 | platform: linux 36 | image: musllinux 37 | archs: x86_64 38 | - os: ubuntu-22.04 39 | platform: linux 40 | image: musllinux 41 | archs: aarch64 42 | 43 | # macOS (multi-arch) 44 | - os: macos-13 45 | platform: macos 46 | image: macosx 47 | archs: "x86_64 arm64 universal2" 48 | 49 | # Windows (multi-arch) 50 | - os: windows-2022 51 | platform: windows 52 | image: win 53 | archs: "AMD64 ARM64" 54 | 55 | env: 56 | CIBW_OUTPUT_DIR: wheelhouse 57 | PIP_DISABLE_PIP_VERSION_CHECK: "1" 58 | 59 | steps: 60 | - name: Checkout repository 61 | uses: actions/checkout@v4 62 | with: 63 | fetch-depth: 0 64 | submodules: recursive 65 | 66 | - name: Set up QEMU (Linux cross-arch userspace for aarch64) 67 | if: matrix.platform == 'linux' 68 | uses: docker/setup-qemu-action@v2 69 | with: 70 | platforms: all 71 | 72 | - name: Set up Docker Buildx 73 | if: matrix.platform == 'linux' 74 | uses: docker/setup-buildx-action@v2 75 | 76 | - name: Set up Python (for macOS/Windows runners cibuildwheel uses local Pythons) 77 | if: matrix.platform != 'linux' 78 | uses: actions/setup-python@v4 79 | with: 80 | python-version: "3.11" 81 | 82 | - name: Build wheels with cibuildwheel 83 | uses: pypa/cibuildwheel@v3.0.0 84 | with: 85 | output-dir: ${{ env.CIBW_OUTPUT_DIR }} 86 | package-dir: python/ 87 | env: 88 | CIBW_ARCHS_LINUX: ${{ matrix.archs }} 89 | CIBW_ARCHS_MACOS: ${{ matrix.archs }} 90 | CIBW_ARCHS_WINDOWS: ${{ matrix.archs }} 91 | # Only build for the selected image type 92 | CIBW_BUILD: "*-${{ matrix.image }}_*" 93 | 94 | - name: List produced wheels 95 | shell: bash 96 | run: | 97 | echo "Wheel files produced:" 98 | ls -la ${{ env.CIBW_OUTPUT_DIR }} || true 99 | 100 | - name: Upload wheelhouse artifact 101 | uses: actions/upload-artifact@v4 102 | with: 103 | name: cibw-wheelhouse-${{ matrix.os }}-${{ matrix.image }}-${{ matrix.archs }} 104 | path: ${{ env.CIBW_OUTPUT_DIR }} 105 | 106 | build_sdist: 107 | name: Build source distribution 108 | runs-on: ubuntu-latest 109 | steps: 110 | - uses: actions/checkout@v4 111 | with: 112 | submodules: recursive 113 | - name: Build sdist 114 | run: | 115 | python -m pip install --upgrade pip setuptools wheel build 116 | pushd python 117 | python -m build --sdist 118 | 119 | - uses: actions/upload-artifact@v4 120 | with: 121 | name: cibw-sdist 122 | path: python/dist/*.tar.gz 123 | 124 | upload_pypi: 125 | name: Upload to PyPI 126 | needs: [build_wheels, build_sdist] 127 | runs-on: ubuntu-latest 128 | environment: pypi 129 | permissions: 130 | id-token: write 131 | steps: 132 | - uses: actions/download-artifact@v4 133 | with: 134 | pattern: cibw-* 135 | path: python/dist 136 | merge-multiple: true 137 | 138 | - name: Publish package distributions to PyPI 139 | uses: pypa/gh-action-pypi-publish@release/v1 140 | with: 141 | packages-dir: python/dist/ 142 | -------------------------------------------------------------------------------- /python/osdp/peripheral_device.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | import osdp_sys 8 | import time 9 | import queue 10 | import threading 11 | from typing import Callable, Tuple 12 | 13 | from .helpers import PDInfo, PDCapabilities 14 | from .constants import LogLevel 15 | 16 | class PeripheralDevice(): 17 | def __init__(self, pd_info: PDInfo, pd_cap: PDCapabilities, 18 | log_level: LogLevel=LogLevel.Info, 19 | command_handler: Callable[[dict], Tuple[int, dict]]=None): 20 | self.command_queue = queue.Queue() 21 | self.address = pd_info.address 22 | self.user_command_handler = None 23 | osdp_sys.set_loglevel(log_level) 24 | self.ctx = osdp_sys.PeripheralDevice(pd_info.get(), capabilities=pd_cap.get()) 25 | # Always use our internal handler to ensure queue functionality 26 | self.ctx.set_command_callback(self._internal_command_handler) 27 | self.set_command_handler(command_handler) 28 | self.event = None 29 | self.lock = None 30 | self.thread = None 31 | 32 | @staticmethod 33 | def refresh(event, lock, ctx): 34 | while not event.is_set(): 35 | lock.acquire() 36 | ctx.refresh() 37 | lock.release() 38 | time.sleep(0.020) #sleep for 20ms 39 | 40 | def _internal_command_handler(self, command) -> Tuple[int, dict]: 41 | """Internal handler that manages both queue and user callback""" 42 | # Always put command in queue for get_command() compatibility 43 | self.command_queue.put(command) 44 | 45 | # If user has set a custom handler, call it too 46 | if self.user_command_handler: 47 | try: 48 | return self.user_command_handler(command) 49 | except Exception as e: 50 | print(f"Error in user command handler: {e}") 51 | return -1, None 52 | 53 | return 0, None 54 | 55 | def set_command_handler(self, handler: Callable[[dict], Tuple[int, dict]]): 56 | """Set user command handler while maintaining queue functionality""" 57 | self.user_command_handler = handler 58 | 59 | def get_command(self, timeout: int=5): 60 | block = timeout >= 0 61 | try: 62 | cmd = self.command_queue.get(block, timeout=timeout) 63 | except queue.Empty: 64 | return None 65 | return cmd 66 | 67 | def submit_event(self, event): 68 | self.lock.acquire() 69 | ret = self.ctx.submit_event(event) 70 | self.lock.release() 71 | return ret 72 | 73 | def notify_event(self, event): 74 | from warnings import warn 75 | warn("This method has been renamed to submit_event", DeprecationWarning, 2) 76 | return self.submit_event(event) 77 | 78 | def register_file_ops(self, fops): 79 | self.lock.acquire() 80 | ret = self.ctx.register_file_ops(0, fops) 81 | self.lock.release() 82 | return ret 83 | 84 | def is_sc_active(self): 85 | return self.ctx.is_sc_active() 86 | 87 | def is_online(self): 88 | return self.ctx.is_online() 89 | 90 | def sc_wait(self, timeout=8): 91 | count = 0 92 | res = False 93 | while count < timeout * 2: 94 | time.sleep(0.5) 95 | if self.is_sc_active(): 96 | res = True 97 | break 98 | count += 1 99 | return res 100 | 101 | def start(self): 102 | if self.thread: 103 | raise RuntimeError("Thread already running!") 104 | self.event = threading.Event() 105 | self.lock = threading.Lock() 106 | args = (self.event, self.lock, self.ctx,) 107 | self.thread = threading.Thread(name='pd', target=self.refresh, args=args) 108 | self.thread.start() 109 | 110 | def get_file_tx_status(self): 111 | self.lock.acquire() 112 | ret = self.ctx.get_file_tx_status(0) 113 | self.lock.release() 114 | return ret 115 | 116 | def stop(self): 117 | if not self.thread: 118 | raise RuntimeError("Thread not running!") 119 | while self.thread.is_alive(): 120 | self.event.set() 121 | self.thread.join(2) 122 | if not self.thread.is_alive(): 123 | self.thread = None 124 | break 125 | 126 | def teardown(self): 127 | self.stop() 128 | self.ctx = None 129 | -------------------------------------------------------------------------------- /tests/pytest/test_file_tx.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021-2025 Siddharth Chandrasekaran 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | import time 8 | import random 9 | import pytest 10 | from osdp import * 11 | from conftest import make_fifo_pair, cleanup_fifo_pair 12 | 13 | sender_data = [ random.randint(0, 255) for _ in range(4096) ] 14 | 15 | def sender_open(file_id: int, file_size: int) -> int: 16 | assert file_id == 13 17 | assert file_size == 0 # sender has to return file_size so this must be 0 18 | return 4096 # we'll just send 4k of random data 19 | 20 | def sender_read(size: int, offset: int) -> bytes: 21 | assert offset < 4096 22 | if offset + size > 4096: 23 | size = 4096 - offset 24 | return bytes(sender_data[offset:offset+size]) 25 | 26 | def sender_write(data: bytes, offset: int) -> int: 27 | # sender should not try to write anything! 28 | assert False 29 | 30 | def sender_close(file_id: int): 31 | assert file_id == 13 32 | 33 | sender_fops = { 34 | 'open': sender_open, 35 | 'read': sender_read, 36 | 'write': sender_write, 37 | 'close': sender_close 38 | } 39 | 40 | receiver_data = [0] * 4096 41 | 42 | def receiver_open(file_id: int, file_size: int) -> int: 43 | assert file_id == 13 44 | assert file_size == 4096 45 | return 0 # indicates success. Both CP and PD have the file_size now. 46 | 47 | def receiver_read(size: int, offset: int) -> bytes: 48 | # receiver should not read anything 49 | assert False 50 | 51 | def receiver_write(data: bytes, offset: int) -> int: 52 | global receiver_data 53 | assert offset + len(data) <= 4096 54 | receiver_data[offset:offset + len(data)] = list(data) 55 | return len(data) 56 | 57 | def receiver_close(file_id: int): 58 | assert file_id == 13 59 | 60 | receiver_fops = { 61 | 'open': receiver_open, 62 | 'read': receiver_read, 63 | 'write': receiver_write, 64 | 'close': receiver_close 65 | } 66 | 67 | pd_cap = PDCapabilities([]) 68 | 69 | f1, f2 = make_fifo_pair("file") 70 | key = KeyStore.gen_key() 71 | 72 | pd = PeripheralDevice( 73 | PDInfo(101, f1, scbk=key, flags=[ LibFlag.EnforceSecure ]), 74 | pd_cap, 75 | log_level=LogLevel.Debug 76 | ) 77 | cp = ControlPanel([ 78 | PDInfo(101, f2, scbk=key, flags=[ LibFlag.EnforceSecure ]), 79 | ], 80 | log_level=LogLevel.Debug 81 | ) 82 | 83 | @pytest.fixture(scope='module', autouse=True) 84 | def setup_test(): 85 | pd.start() 86 | cp.start() 87 | cp.sc_wait_all() 88 | yield 89 | teardown_test() 90 | 91 | def teardown_test(): 92 | cp.teardown() 93 | pd.teardown() 94 | cleanup_fifo_pair("file") 95 | 96 | def test_file_transfer(utils): 97 | # Register file OPs and kick off a transfer 98 | assert cp.register_file_ops(101, sender_fops) 99 | assert pd.register_file_ops(receiver_fops) 100 | file_tx_cmd = { 101 | 'command': Command.FileTransfer, 102 | 'id': 13, 103 | 'flags': 0 104 | } 105 | assert cp.submit_command(101, file_tx_cmd) 106 | assert pd.get_command() == file_tx_cmd 107 | 108 | # Monitor transfer status 109 | file_tx_status = False 110 | tries = 0 111 | while tries < 10: 112 | time.sleep(0.5) 113 | status = cp.get_file_tx_status(101) 114 | if not status or 'size' not in status or 'offset' not in status: 115 | break 116 | if status['size'] <= 0: 117 | break 118 | if status['size'] == status['offset']: 119 | file_tx_status = True 120 | break 121 | tries += 1 122 | assert file_tx_status 123 | 124 | # Check if the data was sent properly 125 | assert sender_data == receiver_data 126 | 127 | def test_file_tx_abort(utils): 128 | # Register file OPs and kick off a transfer 129 | assert cp.register_file_ops(101, sender_fops) 130 | assert pd.register_file_ops(receiver_fops) 131 | file_tx_cmd = { 132 | 'command': Command.FileTransfer, 133 | 'id': 13, 134 | 'flags': 0 135 | } 136 | assert cp.submit_command(101, file_tx_cmd) 137 | assert pd.get_command() == file_tx_cmd 138 | 139 | # Allow some number of transfers to go through 140 | time.sleep(0.5) 141 | 142 | file_tx_abort = { 143 | 'command': Command.FileTransfer, 144 | 'id': 13, 145 | 'flags': CommandFileTxFlags.Cancel 146 | } 147 | assert cp.submit_command(101, file_tx_abort) 148 | 149 | # Allow some time for CP to send the abort to PD 150 | time.sleep(0.2) 151 | 152 | assert cp.get_file_tx_status(101) == None 153 | assert pd.get_file_tx_status() == None 154 | -------------------------------------------------------------------------------- /doc/libosdp/debugging.rst: -------------------------------------------------------------------------------- 1 | Debugging 2 | ========= 3 | 4 | LibOSDP has a lot of debugging/diagnostics built into it to help narrow down the 5 | issue without having to exchange a bunch of emails/comments with each user who 6 | encounters an issue. This save developer time at both ends. That said, the 7 | default logging methods are very sane which makes this unnecessary for every 8 | issue. So don't trouble yourself to generate such logs proactively. 9 | 10 | A note on the log file 11 | ---------------------- 12 | 13 | It is preferable to attach the full logs (from osdp_cp_setup() to point of 14 | failure). If you feel that you know what the issue is or that the entire log 15 | wont help, please feel free to snip them as you see fit, but please try to 16 | retain the very first few log lines as it has some info on which version of 17 | LibOSDP you are running and you PD connection topology. 18 | 19 | When creating an issue in GitHub, it is not very elegant to post the entire log 20 | file in the issue description so it is better to attached the log file to the 21 | issue or post it in some log sharing websites such as https://pastebin.com/ and 22 | include the link in the issue. 23 | 24 | Log Level 25 | --------- 26 | 27 | LibOSDP supports different logging levels with ``LOG_DEBUG`` being the most 28 | verbose mode. When asking for help, please set the log level to ``LOG_DEBUG``. 29 | 30 | This can be done by calling osdp_logger_init() BEFORE calling osdp_cp/pd_setup() 31 | as, 32 | 33 | .. code:: c 34 | 35 | osdp_logger_init("osdp::cp", LOG_DEBUG, uart_puts); 36 | 37 | Any log messages emitted by LibOSDP other than `DEBUG` and `INFO` should be 38 | treated out-of-ordinary (cues for action). 39 | 40 | Packet Trace Builds 41 | ------------------- 42 | 43 | This is the most verbose form of debugging where all bytes on the wire are 44 | captured and stored to a .pcap file which can then be inspected with WireShark. 45 | This can come in handy when trying to debug low level issues. 46 | 47 | To enable packet trace builds, follow these steps: 48 | 49 | .. code:: sh 50 | 51 | mkdir build-pt && cd build-pt 52 | cmake -DOPT_OSDP_PACKET_TRACE=on .. 53 | make 54 | 55 | During CP/PD setup, you must set PD info flag `OSDP_FLAG_CAPTURE_PACKETS`. 56 | After this when you run your application, libosdp will produce a `.pcap` file 57 | int the current directory which contains all the packets it decoded from the 58 | communication channel. 59 | 60 | Data Trace Builds 61 | ----------------- 62 | 63 | When secure channel is working fine and you are encountering a command level 64 | failure, it can be helpful to see the decrypted messages instead of the junk 65 | that would get dumped when secure channel is enabled. This option dumps the 66 | packet just after it was built/decrypted. 67 | 68 | To enable data trace builds, follow these steps: 69 | 70 | .. code:: sh 71 | 72 | mkdir build-dt && cd build-dt 73 | cmake -DOPT_OSDP_DATA_TRACE=on .. 74 | make 75 | 76 | During CP/PD setup, you must set PD info flag `OSDP_FLAG_CAPTURE_PACKETS`. 77 | After this when you run your application, libosdp will produce a `.pcap` file 78 | in the current directory which contains all the packets it decoded from the 79 | communication channel. 80 | 81 | Note: It is seldom useful to run on both packet trace AND data trace (in fact it 82 | makes it harder to locate relevant information) so please never do it. 83 | 84 | WireShark Payload Dissector 85 | --------------------------- 86 | 87 | To view the captured packets (see above), as an one time setup, we must first 88 | setup WireShark with a custom protocol dissector. 89 | 90 | .. code:: sh 91 | 92 | mkdir -p $HOME/.local/lib/wireshark/plugins 93 | cp path/to/libosdp/misc/osdp_dissector.lua $HOME/.local/lib/wireshark/plugins/ 94 | 95 | Note: For Windows, osdp_disector.lua needs to be copied to 96 | `%APPDATA%\Wireshark\plugins` 97 | 98 | For the dissector to be loaded, you should restart Wireshark it's running. Then 99 | from the GUI, goto, 100 | 101 | .. code:: text 102 | 103 | Preference -> Protocols -> DLT_USER -> Encapsulations Table -> Edit 104 | 105 | In the new window that popped-up, click the "+" button to add a new row and 106 | then chose the following: 107 | 108 | .. code:: text 109 | 110 | DLT: User 15 (DLT=162) 111 | Payload Dissector: osdp 112 | Header size: 0 113 | Trailer size: 0 114 | 115 | After that, you can do `File -> Open` and choose the the `.pcap` files that were 116 | produced by LibOSDP build when PACKET_TRACE enabled. Here is a screenshot of 117 | what you can expect to see: 118 | 119 | .. image:: /_static/img/wireshark.png 120 | :width: 400 121 | :alt: Wireshark OSDP protocol screenshot 122 | 123 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "CMake: Configure", 6 | "type": "shell", 7 | "command": "cmake", 8 | "args": ["-B", "build", "."], 9 | "group": "build", 10 | "presentation": { 11 | "echo": true, 12 | "reveal": "always", 13 | "focus": false, 14 | "panel": "shared" 15 | }, 16 | "problemMatcher": [] 17 | }, 18 | { 19 | "label": "CMake: Build", 20 | "type": "shell", 21 | "command": "cmake", 22 | "args": ["--build", "build", "--parallel"], 23 | "group": { 24 | "kind": "build", 25 | "isDefault": true 26 | }, 27 | "presentation": { 28 | "echo": true, 29 | "reveal": "always", 30 | "focus": false, 31 | "panel": "shared" 32 | }, 33 | "problemMatcher": "$gcc", 34 | "dependsOn": "CMake: Configure" 35 | }, 36 | { 37 | "label": "CMake: Clean", 38 | "type": "shell", 39 | "command": "cmake", 40 | "args": ["--build", "build", "--target", "clean"], 41 | "group": "build", 42 | "presentation": { 43 | "echo": true, 44 | "reveal": "always", 45 | "focus": false, 46 | "panel": "shared" 47 | }, 48 | "problemMatcher": [] 49 | }, 50 | { 51 | "label": "Make: Build", 52 | "type": "shell", 53 | "command": "make", 54 | "args": [], 55 | "group": "build", 56 | "presentation": { 57 | "echo": true, 58 | "reveal": "always", 59 | "focus": false, 60 | "panel": "shared" 61 | }, 62 | "problemMatcher": "$gcc" 63 | }, 64 | { 65 | "label": "Make: Clean", 66 | "type": "shell", 67 | "command": "make", 68 | "args": ["clean"], 69 | "group": "build", 70 | "presentation": { 71 | "echo": true, 72 | "reveal": "always", 73 | "focus": false, 74 | "panel": "shared" 75 | }, 76 | "problemMatcher": [] 77 | }, 78 | { 79 | "label": "Make: Unit Tests", 80 | "type": "shell", 81 | "command": "make", 82 | "args": ["check"], 83 | "group": "test", 84 | "presentation": { 85 | "echo": true, 86 | "reveal": "always", 87 | "focus": false, 88 | "panel": "shared" 89 | }, 90 | "problemMatcher": "$gcc" 91 | }, 92 | { 93 | "label": "Configure: Lean Build", 94 | "type": "shell", 95 | "command": "./configure.sh", 96 | "args": ["--debug"], 97 | "group": "build", 98 | "presentation": { 99 | "echo": true, 100 | "reveal": "always", 101 | "focus": false, 102 | "panel": "shared" 103 | }, 104 | "problemMatcher": [] 105 | }, 106 | { 107 | "label": "Configure: With OpenSSL", 108 | "type": "shell", 109 | "command": "./configure.sh", 110 | "args": ["--debug", "--crypto", "openssl"], 111 | "group": "build", 112 | "presentation": { 113 | "echo": true, 114 | "reveal": "always", 115 | "focus": false, 116 | "panel": "shared" 117 | }, 118 | "problemMatcher": [] 119 | }, 120 | { 121 | "label": "Python: Run Tests", 122 | "type": "shell", 123 | "command": "python", 124 | "args": ["-m", "pytest", "tests/pytest", "-v"], 125 | "group": "test", 126 | "presentation": { 127 | "echo": true, 128 | "reveal": "always", 129 | "focus": false, 130 | "panel": "shared" 131 | }, 132 | "problemMatcher": [] 133 | }, 134 | { 135 | "label": "Format: Check Code Style", 136 | "type": "shell", 137 | "command": "./scripts/clang-format-check.sh", 138 | "args": [], 139 | "group": "build", 140 | "presentation": { 141 | "echo": true, 142 | "reveal": "always", 143 | "focus": false, 144 | "panel": "shared" 145 | }, 146 | "problemMatcher": [] 147 | } 148 | ] 149 | } -------------------------------------------------------------------------------- /scripts/clang-format-diff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | #===- clang-format-diff.py - ClangFormat Diff Reformatter ----*- python -*--===# 4 | # 5 | # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 6 | # See https://llvm.org/LICENSE.txt for license information. 7 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 8 | # 9 | #===------------------------------------------------------------------------===# 10 | 11 | """ 12 | This script reads input from a unified diff and reformats all the changed 13 | lines. This is useful to reformat all the lines touched by a specific patch. 14 | Example usage for git/svn users: 15 | 16 | git diff -U0 --no-color HEAD^ | clang-format-diff.py -p1 -i 17 | svn diff --diff-cmd=diff -x-U0 | clang-format-diff.py -i 18 | 19 | """ 20 | from __future__ import absolute_import, division, print_function 21 | 22 | import argparse 23 | import difflib 24 | import re 25 | import subprocess 26 | import sys 27 | 28 | if sys.version_info.major >= 3: 29 | from io import StringIO 30 | else: 31 | from io import BytesIO as StringIO 32 | 33 | 34 | def main(): 35 | parser = argparse.ArgumentParser(description=__doc__, 36 | formatter_class= 37 | argparse.RawDescriptionHelpFormatter) 38 | parser.add_argument('-i', action='store_true', default=False, 39 | help='apply edits to files instead of displaying a diff') 40 | parser.add_argument('-p', metavar='NUM', default=0, 41 | help='strip the smallest prefix containing P slashes') 42 | parser.add_argument('-regex', metavar='PATTERN', default=None, 43 | help='custom pattern selecting file paths to reformat ' 44 | '(case sensitive, overrides -iregex)') 45 | parser.add_argument('-iregex', metavar='PATTERN', default= 46 | r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hh|hpp|m|mm|inc|js|ts|proto' 47 | r'|protodevel|java|cs)', 48 | help='custom pattern selecting file paths to reformat ' 49 | '(case insensitive, overridden by -regex)') 50 | parser.add_argument('-sort-includes', action='store_true', default=False, 51 | help='let clang-format sort include blocks') 52 | parser.add_argument('-v', '--verbose', action='store_true', 53 | help='be more verbose, ineffective without -i') 54 | parser.add_argument('-style', 55 | help='formatting style to apply (LLVM, Google, Chromium, ' 56 | 'Mozilla, WebKit)') 57 | parser.add_argument('-binary', default='clang-format', 58 | help='location of binary to use for clang-format') 59 | args = parser.parse_args() 60 | 61 | # Extract changed lines for each file. 62 | filename = None 63 | lines_by_file = {} 64 | for line in sys.stdin: 65 | match = re.search(r'^\+\+\+\ (.*?/){%s}(\S*)' % args.p, line) 66 | if match: 67 | filename = match.group(2) 68 | if filename == None: 69 | continue 70 | 71 | if args.regex is not None: 72 | if not re.match('^%s$' % args.regex, filename): 73 | continue 74 | else: 75 | if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): 76 | continue 77 | 78 | match = re.search(r'^@@.*\+(\d+)(,(\d+))?', line) 79 | if match: 80 | start_line = int(match.group(1)) 81 | line_count = 1 82 | if match.group(3): 83 | line_count = int(match.group(3)) 84 | if line_count == 0: 85 | continue 86 | end_line = start_line + line_count - 1 87 | lines_by_file.setdefault(filename, []).extend( 88 | ['-lines', str(start_line) + ':' + str(end_line)]) 89 | 90 | # Reformat files containing changes in place. 91 | for filename, lines in lines_by_file.items(): 92 | if args.i and args.verbose: 93 | print('Formatting {}'.format(filename)) 94 | command = [args.binary, filename] 95 | if args.i: 96 | command.append('-i') 97 | if args.sort_includes: 98 | command.append('-sort-includes') 99 | command.extend(lines) 100 | if args.style: 101 | command.extend(['-style', args.style]) 102 | p = subprocess.Popen(command, 103 | stdout=subprocess.PIPE, 104 | stderr=None, 105 | stdin=subprocess.PIPE, 106 | universal_newlines=True) 107 | stdout, stderr = p.communicate() 108 | if p.returncode != 0: 109 | sys.exit(p.returncode) 110 | 111 | if not args.i: 112 | with open(filename) as f: 113 | code = f.readlines() 114 | formatted_code = StringIO(stdout).readlines() 115 | diff = difflib.unified_diff(code, formatted_code, 116 | filename, filename, 117 | '(before formatting)', '(after formatting)') 118 | diff_string = ''.join(diff) 119 | if len(diff_string) > 0: 120 | sys.stdout.write(diff_string) 121 | 122 | if __name__ == '__main__': 123 | main() 124 | -------------------------------------------------------------------------------- /doc/osdpctl/introduction.rst: -------------------------------------------------------------------------------- 1 | Create/Manage/Control OSDP Devices 2 | ================================== 3 | 4 | osdpctl is a tool that uses libosdp to setup/manage/control osdp devices. It 5 | also serves as a starting point for those who intend to consume this library. 6 | It cannot be used directly in applications as most of the time a lot more 7 | product specific customizations are needed. 8 | 9 | This tool brings in a concept called **channel** to describe the communication 10 | between a CP and PD. Although OSDP defines this protocol to run on RS458 11 | (serial), it is possible to run this over other mediums too. From a testing 12 | perspective, running on a unix IPC channel is very convenient. To know 13 | more about how this is achieved, look at ``channel_*.c`` files in this 14 | directory. 15 | 16 | Configuration files 17 | ------------------- 18 | 19 | Since OSDP requires a lot of configuration information to setup a PD/CP, its 20 | not practical to pass all of them from the command line (I tried). So the tool 21 | uses a configuration file (ini format) to get the settings needed to configure 22 | the OSDP library (libosdp). This file determines if the service is to run as a 23 | CP or PD. You can read more about the configuration file and the various keys 24 | it can contain in the `documentation section`_. 25 | 26 | Some sample configuration files can be found inside the ``config/`` `directory`_. 27 | 28 | .. _directory: https://github.com/cbsiddharth/libosdp/tree/master/osdpctl/config 29 | .. _documentation section: configuration.html 30 | 31 | Start / Stop a service 32 | ---------------------- 33 | 34 | An OSDP service can be started by passing a suitable config file to ``osdpctl`` 35 | tool. You can pass the ``-f`` flag to fork the process to background and the ``-l`` 36 | flag to log output to a file or ``-q`` to disable logging. So to start a OSDP 37 | process as a daemon, you can do the following: 38 | 39 | .. code:: bash 40 | 41 | osdpctl pd-0.cfg start -f -l /tmp/pd-0.log 42 | 43 | To stop a running service, you must pass the same configuration file that was 44 | used to start it. 45 | 46 | .. code:: bash 47 | 48 | osdpctl pd-0.cfg stop 49 | 50 | Send control commands to an OSDP service 51 | ---------------------------------------- 52 | 53 | A command to be sent to a running CP/PD service must be of the following format. 54 | Some of these commands will in-turn be sent by the CP/PD device to its connected 55 | counterpart over OSDP as a command/event. 56 | 57 | For instance, if you send a LED command to a CP instance specifying a correct PD 58 | offset, then that command is sent over to the relevant PD. 59 | 60 | :: 61 | 62 | osdpctl send [ARG1 ARG2 ...] 63 | 64 | Here, ``PD-OFFSET`` is the offset number (starting with 0) of the PD in the config 65 | file ``CONFIG``. In PD mode, since there is only one PD, this is always set as 0. 66 | 67 | This section will only document the ``COMMANDS`` and their arguments. You must 68 | prefix ``osdpctl send `` to each of these commands for 69 | them to actually get through. 70 | 71 | CP commands 72 | ~~~~~~~~~~~ 73 | 74 | The following commands can be passed to a OSDP device that is setup as a CP. 75 | The PD to which these commands are being sent must have the capability of 76 | executing them. Refer to the `PD capabilities document`_. 77 | for more details. 78 | 79 | .. _PD capabilities document: ../protocol/pd-capabilities.html 80 | 81 | LED 82 | ^^^ 83 | 84 | This command is used to control LEDs in a PD. It is of the format: 85 | ``led ``. 86 | 87 | Examples: 88 | 89 | :: 90 | 91 | led 0 red blink 5 # blink LED number 0 in red color for 5 times 92 | led 1 amber blink 0 # blink LED number 1 in amber color forever 93 | led 2 green static 1 # Turn on LED number 2 green color 94 | led 1 blue static 0 # Turn off LED number 1 blue color 95 | 96 | Buzzer 97 | ^^^^^^ 98 | 99 | This command is used to control Buzzers in a PD. It is of the format: 100 | ``buzzer ``. 101 | 102 | Examples: 103 | 104 | :: 105 | 106 | buzzer blink 0 # beep the buzzer forever 107 | buzzer blink 5 # beep the buzzer 5 times 108 | buzzer static 1 # turn off the buzzer 109 | buzzer static 0 # turn on the buzzer 110 | 111 | Output 112 | ^^^^^^ 113 | 114 | This command is used to control LEDs in a PD. It is of the format: 115 | ``output ``. 116 | 117 | Examples: 118 | 119 | :: 120 | 121 | output 0 1 # Set output number 0 high 122 | output 2 0 # Set output number 2 low 123 | 124 | Text 125 | ^^^^ 126 | 127 | This command is used to control the text that is displayed on the PD. It is of 128 | the format: ``text ``. 129 | 130 | Examples: 131 | 132 | :: 133 | 134 | text 'Hello World' # Set text "hello world" in display 135 | 136 | Communication Params set 137 | ^^^^^^^^^^^^^^^^^^^^^^^^ 138 | 139 | This command is used to set the communication parameters of a connected PD. It 140 | is of the format: ``comset
``. 141 | 142 | Examples: 143 | 144 | :: 145 | 146 | comset 12 115200 # Set PD address to 12 and baud rate to 115200 147 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug: Unit Tests", 6 | "type": "cppdbg", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/build/check", 9 | "args": [], 10 | "stopAtEntry": false, 11 | "cwd": "${workspaceFolder}", 12 | "environment": [], 13 | "externalConsole": false, 14 | "MIMode": "gdb", 15 | "setupCommands": [ 16 | { 17 | "description": "Enable pretty-printing for gdb", 18 | "text": "-enable-pretty-printing", 19 | "ignoreFailures": true 20 | } 21 | ], 22 | "preLaunchTask": "CMake: Build", 23 | "miDebuggerPath": "/usr/bin/gdb" 24 | }, 25 | { 26 | "name": "Debug: Make Unit Tests", 27 | "type": "cppdbg", 28 | "request": "launch", 29 | "program": "${workspaceFolder}/build/check", 30 | "args": [], 31 | "stopAtEntry": false, 32 | "cwd": "${workspaceFolder}", 33 | "environment": [], 34 | "externalConsole": false, 35 | "MIMode": "gdb", 36 | "setupCommands": [ 37 | { 38 | "description": "Enable pretty-printing for gdb", 39 | "text": "-enable-pretty-printing", 40 | "ignoreFailures": true 41 | } 42 | ], 43 | "preLaunchTask": "Make: Unit Tests", 44 | "miDebuggerPath": "/usr/bin/gdb" 45 | }, 46 | { 47 | "name": "Debug: CP Sample", 48 | "type": "cppdbg", 49 | "request": "launch", 50 | "program": "${workspaceFolder}/bin/c_cp_sample", 51 | "args": [], 52 | "stopAtEntry": false, 53 | "cwd": "${workspaceFolder}", 54 | "environment": [], 55 | "externalConsole": false, 56 | "MIMode": "gdb", 57 | "setupCommands": [ 58 | { 59 | "description": "Enable pretty-printing for gdb", 60 | "text": "-enable-pretty-printing", 61 | "ignoreFailures": true 62 | } 63 | ], 64 | "preLaunchTask": "CMake: Build", 65 | "miDebuggerPath": "/usr/bin/gdb" 66 | }, 67 | { 68 | "name": "Debug: PD Sample", 69 | "type": "cppdbg", 70 | "request": "launch", 71 | "program": "${workspaceFolder}/bin/c_pd_sample", 72 | "args": [], 73 | "stopAtEntry": false, 74 | "cwd": "${workspaceFolder}", 75 | "environment": [], 76 | "externalConsole": false, 77 | "MIMode": "gdb", 78 | "setupCommands": [ 79 | { 80 | "description": "Enable pretty-printing for gdb", 81 | "text": "-enable-pretty-printing", 82 | "ignoreFailures": true 83 | } 84 | ], 85 | "preLaunchTask": "CMake: Build", 86 | "miDebuggerPath": "/usr/bin/gdb" 87 | }, 88 | { 89 | "name": "Debug: Custom Binary", 90 | "type": "cppdbg", 91 | "request": "launch", 92 | "program": "${input:binaryPath}", 93 | "args": [], 94 | "stopAtEntry": false, 95 | "cwd": "${workspaceFolder}", 96 | "environment": [], 97 | "externalConsole": false, 98 | "MIMode": "gdb", 99 | "setupCommands": [ 100 | { 101 | "description": "Enable pretty-printing for gdb", 102 | "text": "-enable-pretty-printing", 103 | "ignoreFailures": true 104 | } 105 | ], 106 | "miDebuggerPath": "/usr/bin/gdb" 107 | }, 108 | { 109 | "name": "Attach to Process", 110 | "type": "cppdbg", 111 | "request": "attach", 112 | "program": "${input:attachProgram}", 113 | "processId": "${input:processId}", 114 | "MIMode": "gdb", 115 | "setupCommands": [ 116 | { 117 | "description": "Enable pretty-printing for gdb", 118 | "text": "-enable-pretty-printing", 119 | "ignoreFailures": true 120 | } 121 | ], 122 | "miDebuggerPath": "/usr/bin/gdb" 123 | } 124 | ], 125 | "inputs": [ 126 | { 127 | "id": "binaryPath", 128 | "description": "Path to the binary to debug", 129 | "default": "${workspaceFolder}/build/", 130 | "type": "promptString" 131 | }, 132 | { 133 | "id": "attachProgram", 134 | "description": "Path to program to attach to", 135 | "default": "", 136 | "type": "promptString" 137 | }, 138 | { 139 | "id": "processId", 140 | "description": "Process ID to attach to", 141 | "default": "", 142 | "type": "promptString" 143 | } 144 | ] 145 | } -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | OSDP - Open Supervised Device Protocol 2 | ====================================== 3 | 4 | .. image:: https://img.shields.io/github/v/release/goToMain/libosdp 5 | :target: https://github.com/goToMain/libosdp/releases 6 | :alt: libosdp Version 7 | .. image:: https://img.shields.io/github/license/goToMain/libosdp 8 | :target: https://github.com/goToMain/libosdp/ 9 | :alt: License 10 | .. image:: https://github.com/goToMain/libosdp/workflows/Build%20CI/badge.svg 11 | :target: https://github.com/goToMain/libosdp/actions?query=workflow%3A%22Build+CI%22 12 | :alt: Build status 13 | 14 | This is a cross-platform open source implementation of IEC 60839-11-5 Open 15 | Supervised Device Protocol (OSDP). The protocol is intended to improve 16 | interoperability among access control and security products. It supports Secure 17 | Channel (SC) for encrypted and authenticated communication between configured 18 | devices. 19 | 20 | OSDP describes the communication protocol for interfacing one or more Peripheral 21 | Devices (PD) to a Control Panel (CP) over a two-wire RS-485 multi-drop serial 22 | communication channel. Nevertheless, this protocol can be used to transfer 23 | secure data over any stream based physical channel. Have a look at the 24 | `protocol design documents`_ to get a better idea. 25 | 26 | This protocol is developed and maintained by `Security Industry Association`_ 27 | (SIA). 28 | 29 | .. _Security Industry Association: https://www.securityindustry.org/industry-standards/open-supervised-device-protocol/ 30 | .. _protocol design documents: protocol/index.html 31 | 32 | Salient Features of LibOSDP 33 | --------------------------- 34 | 35 | - Supports secure channel communication (AES-128) by default and provides a 36 | custom init-time flag to enforce a higher level of security not mandated by 37 | the specification 38 | - Can be used to setup a PD or CP mode of operation 39 | - Exposes a well defined contract though a single header file 40 | - Cross-platform; runs on bare-metal, Linux, Mac, and even Windows 41 | - No run-time memory allocation. All memory is allocated at init-time 42 | - No external dependencies (for ease of cross compilation) 43 | - Fully non-blocking, asynchronous design 44 | - Provides Rust, Python3, and C++ bindings for the C library for faster 45 | integration into various development phases. 46 | - Includes dozens of integration and unit tests which are incorporated in CI 47 | to ensure higher quality of releases. 48 | - Built-in, sophisticated, debugging infrastructure and tools ([see][14]). 49 | 50 | Usage Overview 51 | -------------- 52 | 53 | A device complying with OSDP can either be a CP or a PD. There can be only one 54 | CP on a bus which can talk to multiple PDs. LibOSDP allows your application to 55 | work either as a CP or a PD so depending on what you want to do you have to do 56 | some things differently. 57 | 58 | LibOSDP creates the following constructs which allow interactions between 59 | devices on the OSDP bus. These should not be confused with the protocol 60 | specified terminologies that may use the same names. They are: 61 | 62 | - Channel - Something that allows two OSDP devices to talk to each other 63 | - Commands - A call for action from a CP to one of its PDs 64 | - Events - A call for action from a PD to its CP 65 | 66 | You start by implementing the `osdp_channel` interface; this allows LibOSDP to 67 | communicate with other osdp devices on the bus. Then you describe the PD you 68 | are 69 | 70 | - talking to on the bus (in case of CP mode of operation) or, 71 | - going to behave as on the bus (in case of PD mode of operation) 72 | 73 | by using the `osdp_pd_info_t` struct. 74 | 75 | You can use `osdp_pd_info_t` struct (or an array of it in case of CP) to create 76 | a `osdp_t` context. Then your app needs to call the `osdp_cp/pd_refresh()` as 77 | frequently as possible. To meet the OSDP specified timing requirements, your 78 | app must call this method at least once every 50ms. 79 | 80 | After this point, the CP context can, 81 | - send commands to any one of the PDs (to control LEDs, Buzzers, etc.,) 82 | - register a callback for events that are sent from a PD 83 | 84 | and the PD context can, 85 | - notify it's controlling CP about an event (card read, key press, etc.,) 86 | - register a callback for commands issued by the CP 87 | 88 | Supported Commands and Replies 89 | ------------------------------ 90 | 91 | OSDP has certain command and reply IDs pre-registered. This implementation 92 | of the protocol support only the most common among them. You can see a 93 | `list of commands and replies`_ and their support status in LibOSDP here. 94 | 95 | .. _list of commands and replies: protocol/index.html 96 | 97 | .. toctree:: 98 | :caption: LibOSDP 99 | :hidden: 100 | :maxdepth: 1 101 | 102 | libosdp/build-and-install 103 | libosdp/cross-compiling 104 | libosdp/secure-channel 105 | libosdp/debugging 106 | libosdp/compatibility 107 | libosdp/production-usage 108 | 109 | .. toctree:: 110 | :caption: Protocol 111 | :maxdepth: 2 112 | :hidden: 113 | 114 | protocol/introduction 115 | protocol/commands-and-replies 116 | protocol/packet-structure 117 | protocol/pd-capabilities 118 | protocol/faq 119 | 120 | .. toctree:: 121 | :caption: API 122 | :maxdepth: 2 123 | :hidden: 124 | 125 | api/control-panel 126 | api/peripheral-device 127 | api/pd-info 128 | api/miscellaneous 129 | api/command-structure 130 | api/event-structure 131 | api/channel 132 | 133 | .. toctree:: 134 | :caption: osdpctl 135 | :maxdepth: 2 136 | :hidden: 137 | 138 | osdpctl/introduction 139 | osdpctl/configuration 140 | 141 | .. toctree:: 142 | :caption: Appendix 143 | :hidden: 144 | 145 | changelog 146 | license 147 | --------------------------------------------------------------------------------