Skip to content

Troubleshooting

Common issues and their solutions.


Build & Source Generation Issues

Problem: Generated files not appearing

Symptoms:

  • Build succeeds but no .GeneratedFiles/ directory
  • "Type or namespace not found" errors for Thing classes

Solutions:

  1. Verify YAML is marked as AdditionalFiles:

    <ItemGroup>
        <AdditionalFiles Include="adapter-registration.yaml" />
    </ItemGroup>
    

  2. Check YAML syntax:

    # Install YAML validator
    dotnet tool install -g yamllint
    yamllint adapter-registration.yaml
    

  3. Enable generator diagnostics:

    <PropertyGroup>
        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
        <CompilerGeneratedFilesOutputPath>.\.GeneratedFiles</CompilerGeneratedFilesOutputPath>
    </PropertyGroup>
    

  4. Clean and rebuild:

    dotnet clean
    rm -rf obj bin .GeneratedFiles
    dotnet build
    

  5. Check generator errors:

  6. Look for AdapterSourceGenerator warnings in build output
  7. Check MSBuild binlog: dotnet build /bl
  8. Open msbuild.binlog in MSBuild Structured Log Viewer

Problem: "Partial class has no definition"

Cause: Generated partial class doesn't exist.

Solution: Device type in YAML must match namespace convention:

# YAML
device_types:
  - type_id: "ControllerDevice"  # ← Must match class name

# C#
namespace Pq.Adapter.Vendor.Product.Devices;
internal sealed partial class ControllerDevice { }  # ← Must match type_id

Problem: Property validation errors at runtime

Cause: YAML property constraints violated.

Example:

properties:
  port:
    type: int
    range: [1, 65535]

# Runtime error if: port = 99999

Solution: Validate configuration before adapter starts:

protected override Task OnStart(CancellationToken cancellationToken)
{
    if (Port < 1 || Port > 65535)
        throw new ArgumentOutOfRangeException(nameof(Port), "Port must be 1-65535");
    // ...
}


Connection Issues

Problem: Device never connects

Symptoms:

  • Connect() called repeatedly
  • Logs show "Connection failed, retrying..."
  • Framework retry loop active

Debug steps:

  1. Check network connectivity:

    ping <device-ip>
    telnet <device-ip> <port>
    

  2. Verify credentials:

  3. Password correct?
  4. Device locked out?
  5. SDK license valid?

  6. Check return value:

    public async Task<bool> Connect(...)
    {
        try
        {
            // ... connection logic
            return true;  // ← MUST return true when connected
        }
        catch
        {
            return false; // ← Return false to trigger retry
        }
    }
    

  7. Enable verbose SDK logging:

    // In appsettings.json
    {
      "Logging": {
        "LogLevel": {
          "Pq.Adapter": "Trace"  // ← See all adapter activity
        }
      }
    }
    

Problem: ConnectionBase function not updating in legacy/custom lifecycle code

Symptoms:

  • Device connects but status stays "Disconnected"
  • NATS doesn't receive status updates

Cause: Legacy/custom lifecycle code bypasses the normal IDeviceConnection return-value lifecycle.

Solution:

public async Task<bool> Connect(ConnectionContext context, CancellationToken ct)
{
    return await _protocol.ConnectAndInitialize(ct);
}

For current adapters, prefer returning true/false from IDeviceConnection.Connect; raw signal calls are legacy/custom-only.

Problem: "Connection lost" spam

Symptoms:

  • Rapid connect/disconnect cycles
  • Connection metrics show < 1 second uptime

Causes:

  1. Receive loop crashing:
    private async Task ReceiveLoop(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            var data = await _stream.ReadAsync(buffer, ct);
    
             // ❌ Exception here causes disconnect/retry handling
            ParseAndDispatch(data);
        }
    }
    

Fix: Add try/catch:

try
{
    ParseAndDispatch(data);
}
catch (Exception ex)
{
    _logger.LogError(ex, "Parse failed");
    // don't disconnect on parse errors
}

  1. Heartbeat/keepalive missing:
  2. Some SDKs require periodic ping
  3. Add timer to send keepalive packets

  4. SDK timeout too aggressive:

  5. Increase SDK socket timeout
  6. Add connection health check

Event Issues

Problem: Missing events in audit trail

Symptoms:

  • Some device events don't appear in PQ UI
  • Audit trail has gaps
  • ABAC rules not triggering

CRITICAL REQUIREMENT: Adapters MUST translate ALL relevant device events to PQ events.

Common mistake: Only translating access events (granted/denied) and ignoring:

  • Door state changes (opened, closed, forced)
  • Alarms (tamper, duress, fire)
  • System events (offline, online, battery low)
  • Reader events (card read failure, biometric timeout)
  • Configuration changes (schedule updated, user added)

What to exclude:

  • Protocol acks and confirmations
  • Keepalive/heartbeat messages
  • Low-level transport messages
  • Internal SDK state changes

Solution: Review device event documentation, map EVERY user-visible event to PQ taxonomy.

For unmapped events, publish an unknown/unresolved event instead of dropping them. Prefer the generated router Unhandled hook:

router.Unhandled(async (owner, evt, reason, ct) =>
{
    await owner.Unknown(
        evt.Timestamp,
        $"vendor:0x{evt.Code:X4}",
        reason,
        rawData: Convert.ToBase64String(evt.RawData),
        cancellationToken: ct);

    _logger.LogWarning("Unmapped event {Code} published as unknown", evt.Code);
});

Known events should call generated function methods or generated PqEvent builders from the matched Thing:

router.Route(
    e => e.Code == 0x1000,
    e => e.DeviceId,
    async (door, evt, ct) =>
    {
        await door.Opened(evt.Timestamp);
    });

Manual dispatch should use the same rule:

switch (evt.Code)
{
    case 0x1000:
        await door.Opened(evt.Timestamp);
        break;

    default:
        await door.Unknown(evt.Timestamp, $"0x{evt.Code:X4}", UnhandledReason.Unmapped,
            rawData: Convert.ToBase64String(evt.RawData), cancellationToken: ct);
        break;
}

Why this matters:

  • Preserves complete audit trail
  • PQ can detect missing mappings
  • Debugging is easier with raw data
  • Compliance requirements met

Problem: Events not appearing in PQ UI

Debug steps:

  1. Verify event reception:

    protected override Task OnEventReceived(DeviceEvent evt)
    {
        _logger.LogInformation("Received event: {@Event}", evt);  // ← Log every event
        // ...
    }
    

  2. Check event taxonomy:

  3. Event name must exist in pq-events.yaml
  4. Use exact string: "pq.event.access.granted" not "access.granted"

  5. Verify NATS publishing:

    # Subscribe to all adapter events
    nats sub "pq.event.>"
    
    # Should see:
    # [#1] Received on "pq.event.access.<adapter-id>.<thing-id>"
    # {"eventName":"pq.event.access.granted",...}
    

  6. Check device ID:

  7. Every event needs valid DeviceId
  8. Must match Thing.DeviceId in device tree

  9. Validate timestamp:

  10. Use device timestamp, not DateTime.UtcNow
  11. Format: ISO 8601 or Unix epoch

Problem: Events duplicated

Causes:

  1. Multiple event subscriptions:
  2. SDK event handler registered multiple times
  3. Unsubscribe in OnDisconnected()

  4. Event not consumed in WaitForEvent:

    var evt = await WaitForEvent(
        predicate: e => e.Code == 0x1000,
        timeout: TimeSpan.FromSeconds(5),
        consumeEvent: true  // ← Set true to prevent re-dispatch
    );
    

Problem: Wrong identity in events

Symptoms:

  • Access event shows wrong person
  • ABAC rules don't trigger

Cause: Protocol address not mapped to PQ ID.

Solution: Resolve protocol IDs to PQ IDs via AccessSynchronizationFunction, then call the typed function method (not EventBuilder — see RULE-003):

var personId = _accessSync.ResolvePerson(evt.ProtocolPersonId);
var credentialId = _accessSync.ResolveCredential(evt.ProtocolCardNumber);

await reader.AccessGranted(evt.Timestamp, credentialId, personId: personId);

Problem: Person not in access-sync storage (ResolvePerson returns null)

Symptoms:

  • A specific, known denial/grant event is logged as the generic pq.event.access.denied instead of its precise sub-type (e.g. …antipassback)
  • The event shows no person even though the device sent a user ID

Cause: The vendor user has no mapping in access-sync storage, so ResolvePerson returns null — and the handler downgraded the event type to compensate. That loses information you already had (RULE-027 rung 2).

Solution: Keep the precise event type; degrade only the identity detail. Pass the vendor reference as externalPersonRef when personId is null:

var personId = _accessSync.ResolvePerson(evt.UserId.ToString());

await reader.AccessDeniedAntipassback(
    evt.Timestamp,
    externalPersonRef: personId.HasValue ? null : evt.UserId.ToString(),
    personId: personId);
// renders: "Access denied to unknown person (120) at Door 5: antipassback violation"
personId is optional on these events; never fall back to a generic event just because it did not resolve.


Access Synchronization Issues

Problem: "Address space exhausted"

Symptoms:

  • Sync fails with AddressSpaceExhaustedException
  • Only first N entities upload successfully

Cause: Range in access-model.yaml too small.

Solution:

model:
  - type_id: Cardholder
    range: [1, 1000]  # ← Increase max address

Check device limits:

  • Consult SDK documentation for max entities
  • Some devices have slot limits (e.g., 32K users max)

Problem: "Address already assigned"

Symptoms:

  • First access sync works, next sync fails.
  • Exception contains InvalidOperationException: Address already assigned.

Cause: Transform() mutates address state (SetAddress() or address-prefilled objects), then differ sets address again for unchanged entities.

Do not do this:

  • Do not call SetAddress() in Transform().
  • Do not read entity.Address in Transform() to create references.

Do this instead:

  • Keep entity addresses null in Transform() and return object references (EntityName / EntityName[]).
  • Set address only when device truly assigns it in command response.

Verification: Run identical sync twice. Second run must not throw.

Problem: Sync never completes

Symptoms:

  • StartSynchronization() called but no completion
  • PQ UI shows "Syncing..." indefinitely

Cause: Not calling completion methods.

Solution:

public async Task PerformSync(CancellationToken cancellationToken)
{
    try
    {
        await _accessSync.StartSynchronization(cancellationToken);

        // ... upload logic

        await _accessSync.CompleteSynchronization(cancellationToken);  // ← Must call on success
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "Access synchronization failed");
        await _accessSync.FailSynchronization(cancellationToken);  // ← Must call on error
        throw;
    }
}

Problem: Changes not detected

Symptoms:

  • Person modified in PQ but device not updated
  • Expected UPDATE but got no operation

Cause: Hash computation doesn't match.

Debug:

// In access model entity
public override int GetHashCode()
{
    var hash = new HashCode();
    hash.Add(CardNumber);
    hash.Add(Name);
    hash.Add(ValidFrom);
    // ❌ Missing ValidUntil - change won't be detected!
    return hash.ToHashCode();
}

Solution: Include ALL properties in hash:

hash.Add(ValidUntil);  // ← Don't forget any property

Problem: "Missing dependency" error

Cause: Access model entities in wrong order.

Solution: Define in dependency order (leaves first):

model:
  # Layer 0 - no dependencies
  - type_id: Schedule

  # Layer 1 - references Schedule
  - type_id: AccessLevel
    properties:
      schedule_address: uint  # ← points to Schedule

  # Layer 2 - references AccessLevel
  - type_id: Cardholder
    properties:
      access_levels: uint[]  # ← points to AccessLevel


Command Handling Issues

Problem: Commands not found / timeout

Symptoms:

  • PQ UI shows "Command not found" or "Command timed out"
  • Command reaches adapter but no response

Causes:

  1. Command not declared in YAML:

    device_types:
      - type_id: "Door"
        commands:
          - pq.command.access.open  # ← Must declare here
          - pq.command.access.lock
    

  2. Handler signature mismatch:

    // ❌ Wrong - missing DeviceCommandResult return type
    public async Task Open(DoorDevice door, Access.Open cmd) { }
    
    // ✅ Correct
    public static async Task<DeviceCommandResult> Open(
        DoorDevice door,
        Access.Open cmd,
        Protocol protocol,
        CancellationToken ct)
    {
        await protocol.OpenDoor(door.Address, ct);
        return DeviceCommandResult.Succeeded();
    }
    

  3. Missing public accessibility:

    // ❌ Wrong - private/internal not detected
    private static async Task<DeviceCommandResult> Open(...) { }
    
    // ✅ Correct - must be public
    public static async Task<DeviceCommandResult> Open(...) { }
    

  4. Missing Thing or command parameter:

    // ❌ Wrong - external handler needs both Thing and command parameters
    public static async Task<DeviceCommandResult> Open(
        Access.Open cmd,
        Protocol protocol)
    
    // ✅ Recommended external handler shape
    public static async Task<DeviceCommandResult> Open(
        DoorDevice door,      // ← Thing first
        Access.Open cmd,      // ← Command second
        Protocol protocol)
    

Problem: "Command handler not found" warning

Symptoms:

  • Build warning: PQ0001: Command handler not found
  • Command declared in YAML but not handled

Cause: No method matching command signature.

Solution:

// In Commands/DoorCommands.cs
public static class DoorCommands
{
    // Framework looks for method accepting Access.Open command
    public static async Task<DeviceCommandResult> Open(
        DoorDevice door,
        Access.Open command,    // ← Must match command type
        Protocol protocol,
        CancellationToken ct)
    {
        // implementation
        await protocol.OpenDoor(door.Address, ct);
        return DeviceCommandResult.Succeeded();
    }
}

Problem: "Command handler without YAML declaration" warning

Symptoms:

  • Build warning: PQ0002: Command handler without YAML declaration
  • Handler exists but command not in device type's commands list

Cause: Handler method exists but command not declared in YAML.

Solution: Add command to device type:

device_types:
  - type_id: "Door"
    commands:
      - pq.command.access.open    # ← Add missing command
      - pq.command.access.lock
      - pq.command.access.unlock

Problem: Handler called but execution fails

Symptoms:

  • No build warnings
  • Command reaches handler
  • Exception or wrong behavior

Debug:

public static async Task<DeviceCommandResult> Open(
    DoorDevice door,
    Access.Open command,
    Protocol protocol,
    ILogger<DoorCommands> logger,  // ← Add logger for debugging
    CancellationToken ct)
{
    logger.LogInformation("Opening door {Address}", door.Address);

    try
    {
        await protocol.OpenDoor(door.Address, ct);
        logger.LogInformation("Door opened successfully");
        return DeviceCommandResult.Succeeded();
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "Failed to open door");
        return DeviceCommandResult.Failed();
    }
}


Performance Issues

Problem: Slow event processing

Symptoms:

  • Events delayed by seconds
  • Event queue building up

Causes:

  1. Blocking in OnEventReceived:
    protected override Task OnEventReceived(DeviceEvent evt)
    {
        Thread.Sleep(1000);  // ❌ NEVER block event loop
        return Task.CompletedTask;
    }
    

Fix:

protected override async Task OnEventReceived(DeviceEvent evt)
{
    await Task.Delay(1000);  // ✅ Use async
}

  1. Synchronous SDK calls:
    var result = _sdk.GetData();  // ❌ Synchronous I/O
    

Fix:

var result = await _sdk.GetDataAsync();  // ✅ Async I/O

Problem: High memory usage

Causes:

  1. Event buffer not cleared:
  2. Some SDKs buffer events in memory
  3. Call acknowledge/commit after processing

  4. LiteDB not disposed:

    protected override async Task OnStop(CancellationToken ct)
    {
        _database?.Dispose();  // ← Dispose resources
    }
    

  5. Large event payloads:

  6. Don't include raw bytes in logs
  7. Trim unnecessary event data

NATS / Messaging Issues

Problem: "NATS connection refused"

Cause: NATS server not running.

Solution:

# Start NATS with JetStream
nats-server -js

# Or use Docker
docker run -p 4222:4222 nats:latest -js

Problem: Events published but not received by server

Symptoms:

  • Adapter logs show PublishEvent calls
  • PQ server doesn't receive events

Debug:

# Monitor all adapter events
nats sub "pq.event.>"

# Check if events are actually published

Causes:

  1. Wrong subject format:
  2. Framework publishes events to pq.event.{category}.{adapterId}.{thingId}
  3. Framework publishes status to pq.status.{adapterId}
  4. Framework receives commands on pq.command.{adapterId}
  5. Don't manually publish to NATS

  6. Event validation failed:

  7. Check event builder includes required fields
  8. Verify DeviceId is valid GUID

Debugging Techniques

Enable maximum logging

{
  "Logging": {
    "LogLevel": {
      "Default": "Trace",
      "Pq.Adapter": "Trace",
      "Pq.Adapters.Framework": "Trace"
    }
  }
}

Inspect device tree

// In DeviceAdapterBase
protected override Task OnStart(CancellationToken cancellationToken)
{
    _logger.LogInformation("Device tree: {@Tree}", DumpTree());
    return base.OnStart(cancellationToken);
}

private object DumpTree(Thing? thing = null, int depth = 0)
{
    thing ??= this;
    return new
    {
        Type = thing.GetType().Name,
        DeviceId = thing.DeviceId,
        Children = thing.Children.Select(c => DumpTree(c, depth + 1))
    };
}

Monitor NATS traffic

# All messages
nats sub ">"

# Device events only
nats sub "pq.event.>"

# Adapter status only
nats sub "pq.status.<adapter-id>"

Use breakpoints effectively

  1. OnEventReceived - see all incoming events
  2. IDeviceConnection.Connect/Disconnect - verify connection flow
  3. DispatchCommand - debug command handling
  4. CreateCommands - inspect access upload operations

Capture network traffic

# For TCP protocols
tcpdump -i any -s 65535 -w capture.pcap port <device-port>

# Analyze in Wireshark
wireshark capture.pcap

Common Mistakes Summary

  1. ❌ Bypassing IDeviceConnection lifecycle or returning true before initialization completes
  2. ❌ Blocking in async methods (Thread.Sleep, sync I/O)
  3. ❌ Command handler not public or wrong return type
  4. ❌ Command declared in YAML but no matching handler signature
  5. ❌ Forgetting ClearPendingCommands() on disconnect
  6. ❌ Wrong event taxonomy strings (typos, wrong namespace)
  7. ❌ Using protocol IDs instead of PQ IDs in events
  8. ❌ Not disposing resources in OnStop()
  9. ❌ Missing properties in hash computation
  10. ❌ Wrong access model dependency order
  11. ❌ Partial class name mismatch with YAML type_id
  12. ❌ Implementing custom transport for TCP when framework TCP transport fits; use custom for serial, UDP, vendor SDKs, or protocol-specific transports

Getting Help

  1. Check framework reference:
  2. Functions Registry - complete function reference
  3. Generated Code API - PqEvent, commands, functions
  4. Event Taxonomy - all event types and usage

  5. Review this documentation set:

  6. Check the closest archetype under archetypes/
  7. Look for related patterns and examples in this folder
  8. Prefer generalized guidance over ad hoc inference

  9. Enable diagnostics:

  10. Trace logging
  11. NATS monitoring
  12. Network captures

  13. Create minimal repro:

  14. Isolate issue to smallest code snippet
  15. Remove vendor-specific details
  16. Share logs and stack traces

  17. Document findings:

  18. Note any documentation gap you find so it can be addressed
  19. Record device-specific quirks alongside your adapter
  20. Help future developers