Skip to content

Overview

  • Audience: Developers implementing new device adapters
  • Framework Version: 2.0
  • Last Updated: 2026-02-07

What is an Adapter?

An adapter is a self-contained executable that bridges physical security devices with the PQ platform. It:

  • Translates ALL device events into PQ domain events (critical - no event left behind)
  • Manages device connections and lifecycle
  • Synchronizes access control data (credentials, schedules, permissions)
  • Routes commands from PQ server to devices
  • Reports device status and health metrics

Event logging requirement: Adapters MUST translate and publish ALL relevant events from the device. Missing events break audit trails, compliance reporting, and ABAC authorization rules. Only exclude internal protocol messages (acks, keepalives, heartbeats).

Architecture

┌─────────────────────────────────────────────┐
│ PQ Server (ABAC, API, Blazor UI)            │
└────────────┬────────────────────────────────┘
             │ NATS (events, commands, status)
┌────────────┴────────────────────────────────┐
│ Adapter Process                             │
│  ├─ DeviceAdapterBase (root Thing)          │
│  ├─ Transport (shared SDK resources)        │
│  ├─ Protocol (per-device communication)     │
│  ├─ Device Things (hierarchy)               │
│  └─ Functions (Door, Reader, Access, etc.)  │
└────────────┬────────────────────────────────┘
             │ Vendor Protocol (TCP/UDP/gRPC)
┌────────────┴────────────────────────────────┐
│ Physical Device(s)                          │
└─────────────────────────────────────────────┘

Core Concepts

Thing Hierarchy

Adapters model physical hardware as a tree of Things:

DeviceAdapterBase (root)
 └─ Controller Thing
     ├─ Door Thing (access point)
     ├─ Reader Thing
     ├─ Input Thing
     └─ Output Thing

Each Thing:

  • Has properties (IP address, serial number, etc.)
  • Exposes functions (Door, Reader, Power, etc.)
  • Publishes events and status updates
  • Manages child Things

Functions

Functions represent device capabilities:

  • Door - lock/unlock, open, door contact state
  • Reader - card reads, biometric scans
  • Power - online/offline, AC fail
  • Tamper - tamper detection
  • AccessSynchronization - credential upload/download
  • ConnectionBase - connection state management

Functions are state machines with:

  • States (locked/unlocked, online/offline, etc.)
  • Actions (events that trigger state transitions)
  • Status publishing (real-time state to PQ server)

Commands

Commands are operations triggered by PQ server:

Framework generates typed command classes from pq-commands.yaml:

  • "pq.command.access.open"Access.Open class
  • "pq.command.access.lock"Access.Lock class
  • "pq.command.output.activate"Output.Activate class

You implement handler methods with specific signatures:

public static async Task<DeviceCommandResult> Open(
    DoorDevice door,        // Thing instance
    Access.Open command,    // Typed command
    Protocol protocol)      // DI services
{
    // Execute on device
    return DeviceCommandResult.Succeeded();
}

Framework auto-discovers handlers by signature - no interface needed.

Events

Events report device activity to PQ server:

Framework generates typed event classes from pq-events.yaml:

  • "pq.event.access.granted"PqEvent.Access.Granted
  • "pq.event.access.door.opened"PqEvent.Access.Door.Opened
  • "pq.event.intrusion.alarm"PqEvent.Intrusion.Alarm

Preferred: Use function methods when available:

// Function methods handle state + event + status
await door.Opened(DeviceTimestamp.UtcNow);

Custom events: Use PqEvent classes:

await PublishEvent(
    PqEvent.Access.Granted
        .At(timestamp, reader)
        .WithIdentity(personId, credentialId)
        .Build()
);

Address Resolution

Problem: Device events contain protocol-specific addresses (device ID, door number), but you need Thing instances.

Solution: Framework provides address-based lookups:

// Event arrives with protocol address
var door = FindByAddress(evt.DoorID) as DoorDevice;

// Now you have typed Thing instance
await door.Opened(DeviceTimestamp.UtcNow);
  • Static addressing: Things with IProtocolAddressable<T> (address in YAML config)
  • Dynamic addressing: Things with IDynamicProtocolAddressable<T> (runtime assignment)
  • Hierarchy queries: Inject IThingQuery.IDescendants<T>, IDirect<T>, ISiblings<T>, IAll<T>

See Pattern 7: Address Resolution for details.

Configuration-Driven Generation

Adapters use YAML files to declaratively define:

  1. adapter-registration.yaml - Device types, properties, commands, functions
  2. access-model.yaml - Access synchronization entities (optional)

The framework source generator creates:

  • Thing classes with property validation
  • Function state machines with typed methods
  • Command classes (typed, hierarchical)
  • Event classes (typed, hierarchical)
  • Access model entities with CRUD operations

You implement partial classes to add protocol logic.

Development Workflow

1. Create adapter-registration.yaml (device types, properties, commands)
2. Build → Framework generates:
   - DeviceAdapterBase (entry point)
   - Thing base classes
   - Function state machines
   - Command/Event classes
   - Routing and DI
3. Implement Protocol (per-device SDK communication)
4. Implement command handlers (static methods, auto-discovered)
5. Implement event translation (device → PqEvent classes)
6. Test basic flow (connect → command → event)
7. (Optional) Implement Transport for custom protocols
8. (Optional) Implement access synchronization
9. Test with real hardware
10. Deploy as standalone executable

What You Implement

Must implement:

  • ProtocolBase<TEvent> subclass (per-device communication)
  • Thing partial classes (extend generated code)
  • Command handler methods (typed commands, signature-based)
  • Event translation logic (device events → PQ events)

Conditionally implement:

  • TransportBase subclass - only for custom protocols (gRPC, serial, SDK processes)
  • TCP/UDP: framework provides ready-made transport
  • IProtocolCommand implementations - only for access upload (offline access control)
  • Access model entities - only if device supports offline access

Optional:

  • Custom functions (device-specific capabilities beyond standard)
  • Enrollment services (for biometrics)
  • Shared resources (external SDK processes, connection pools)

Framework generates automatically:

  • DeviceAdapterBase (root adapter entry point - rarely needs customization)
  • Thing base classes from YAML
  • Function state machines
  • Command and Event classes
  • Command dispatcher and routing

What the Framework Provides

Lifecycle management:

  • Connection retry with exponential backoff
  • Graceful shutdown and reconnect
  • Connection metrics (MTBF, MTTR, availability)

Event/Status publishing:

  • Automatic NATS message bus integration
  • Event routing to ABAC engine
  • Status aggregation and publishing

Access synchronization:

  • Differential sync (add/update/delete detection)
  • Address allocation and mapping
  • Dependency ordering (schedules → access levels → users)
  • Hash-based change detection

Command routing:

  • Commands from server routed to correct Thing
  • Request/reply pattern via NATS
  • Timeout and error handling

Storage:

  • LiteDB for address mappings and state
  • Persistent settings per device
  • Event index tracking

Technology Stack

  • .NET 10 - Runtime and SDK
  • C# 13 - Language features (primary constructors, collection expressions)
  • Source Generators - Code generation from YAML (Things, Functions, Commands)
  • NATS JetStream - Message bus (abstracted by framework)
  • LiteDB - Embedded database for address mappings and state
  • TCP/UDP - Framework-provided transports
  • gRPC/Serial/Custom - Implement custom transport if needed

Project Structure

Pq.Adapter.Vendor.Product/
├─ adapter-registration.yaml    # Device types, functions, commands
├─ access-model.yaml            # Access entities (optional)
├─ Pq.Adapter.Vendor.Product.csproj
├─ Communication/
│   ├─ Transport.cs             # Custom transport (gRPC/serial only!)
│   ├─ Protocol.cs              # Per-device protocol (always)
│   └─ DeviceEvent.cs           # Device-specific events
├─ Devices/
│   ├─ ControllerDevice.cs      # Thing implementations
│   └─ DoorDevice.cs
├─ Commands/
│   └─ DoorCommands.cs          # Command handlers (typed methods)
├─ Capabilities/
│   └─ AccessSynchronization.cs # Access upload (optional)

Next Steps

  1. local-development-setup.md — Configure PQ for local adapter debugging
  2. Follow tutorials/00-hello-world.md for quick-start
  3. Read 01-step-by-step.md for detailed implementation guide
  4. Read 02-patterns.md for common implementation patterns
  5. Consult 03-troubleshooting.md when stuck
  6. Use definition-of-done.md to track completion

References