Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CodexaCP/DG_APK/llms.txt

Use this file to discover all available pages before exploring further.

Dragon Guard Handheld unifies barcode scanning and UHF RFID reading under a single, mode-aware service contract. Every view model that needs scan data subscribes to one interface — IScanCaptureService — and never touches hardware directly. Switching between a plain barcode gun, a single-tag RFID read, a continuous inventory sweep, or a combined Hybrid session is a one-line async call. The two concrete adapters that sit beneath the service (KeyboardWedgeScanAdapter and VendorRfidScanAdapter) handle the physical layer in isolation, keeping all Android-specific broadcast wiring away from shared business logic.

Scan Modes

The ScanMode enum controls which physical channel is active and how results are routed.
namespace Handheld.Services.Scanning;

public enum ScanMode
{
    Barcode       = 0,  // keyboard-wedge only
    RfidSingle    = 1,  // one EPC, then session stops automatically
    RfidInventory = 2,  // continuous multi-tag sweep until StopSessionAsync
    Hybrid        = 3,  // keyboard-wedge + RFID simultaneously
}
ValueIntDescription
Barcode0Keyboard-wedge barcode only. RFID hardware is not activated.
RfidSingle1Android RFID hardware reads one tag, then the session self-terminates.
RfidInventory2Continuous UHF sweep. Session runs until StopSessionAsync is called or the policy MaxReadsPerOperation cap is hit.
Hybrid3Both channels active. Barcode input and RFID tags both surface through their respective events.

IScanCaptureService Interface

IScanCaptureService is the sole scanning contract consumed by view models and other services. It is registered as a singleton in the DI container.
Because IScanCaptureService is a singleton, event subscriptions persist across navigation. Always unsubscribe handlers in OnDisappearing (or equivalent) to avoid duplicate callbacks and memory leaks.
namespace Handheld.Services.Scanning;

public sealed record ScanSessionRequest(ScanMode Mode, string? Reason = null);

public interface IScanCaptureService
{
    // ── State ────────────────────────────────────────────────────────────
    ScanMode CurrentMode          { get; }
    bool     IsSessionActive      { get; }
    bool     CanUseRfid           { get; }
    bool     CanOperateWithoutRfid { get; }
    string   RfidHardwareStatus   { get; }

    // ── Events ───────────────────────────────────────────────────────────
    event EventHandler<CodeScannedEventArgs>?      CodeScanned;
    event EventHandler<TagObservedEventArgs>?      TagObserved;
    event EventHandler<ScanSessionEventArgs>?      SessionStarted;
    event EventHandler<ScanSessionEventArgs>?      SessionStopped;
    event EventHandler<TriggerStateChangedEventArgs>? TriggerStateChanged;
    event EventHandler<ScanErrorEventArgs>?        ErrorOccurred;

    // ── Methods ──────────────────────────────────────────────────────────
    void  ApplyRfidPolicy(bool canUseRfid, bool canOperateWithoutRfid, string rfidHardwareStatus);
    Task  InitializeAsync(CancellationToken ct = default);
    Task  SetModeAsync(ScanMode mode, CancellationToken ct = default);
    Task  StartSessionAsync(ScanSessionRequest request, CancellationToken ct = default);
    Task  StopSessionAsync(CancellationToken ct = default);
    Task  HandleKeyboardInputAsync(string? value, CancellationToken ct = default);
}

Properties

PropertyTypeDescription
CurrentModeScanModeThe active scan mode set by the most recent SetModeAsync or StartSessionAsync call.
IsSessionActivebooltrue while a scan session is running. Updated by adapter events.
CanUseRfidboolSet by ApplyRfidPolicy. Gates all RFID session starts.
CanOperateWithoutRfidboolWhen true and RFID is unavailable, StartSessionAsync downgrades to Barcode mode instead of blocking. Default: true.
RfidHardwareStatusstringHuman-readable hardware status token. Initial value: "NO_RFID_PROFILE". Other values: READY, RFID_NOT_SUPPORTED, RFID_DISABLED_BY_POLICY, DEVICE_NOT_AUTHORIZED.

Methods

InitializeAsync(CancellationToken)

Initializes the underlying hardware adapter. On Android, registers the ScanBroadcastReceiver for RFID broadcast intents, configures the RFID output mode to EPC bank (mode 0), suppresses the trailing Enter/Tab keystroke the wedge otherwise injects, and subscribes to AndroidKeyboardWedge trigger events. Calling this more than once is safe — the adapter short-circuits if already initialized.
await scanCaptureService.InitializeAsync(cancellationToken);

SetModeAsync(ScanMode, CancellationToken)

Updates CurrentMode and propagates the new mode down to the vendor adapter. Does not start or stop a session.
await scanCaptureService.SetModeAsync(ScanMode.RfidInventory, cancellationToken);

StartSessionAsync(ScanSessionRequest, CancellationToken)

Starts a capture session for the mode specified in ScanSessionRequest. Contains RFID policy enforcement:
  • Barcode mode — marks the session active immediately and fires SessionStarted without involving the RFID hardware.
  • RFID/Hybrid mode, CanUseRfid = false, CanOperateWithoutRfid = true — fires ErrorOccurred with a degradation message, then starts a barcode-only session automatically.
  • RFID/Hybrid mode, CanUseRfid = false, CanOperateWithoutRfid = false — fires ErrorOccurred and does not start a session.
  • RFID/Hybrid mode, CanUseRfid = true — delegates to VendorRfidScanAdapter.StartSessionAsync on Android.
await scanCaptureService.StartSessionAsync(
    new ScanSessionRequest(ScanMode.RfidSingle, "Item Inquiry"),
    cancellationToken);

StopSessionAsync(CancellationToken)

Stops the active session. For Barcode mode this is purely in-memory. For RFID/Hybrid modes on Android it sends STOP_BARCODE_RFID to the system ScanService and restores the physical trigger binding to the 2D barcode decoder.
await scanCaptureService.StopSessionAsync(cancellationToken);

HandleKeyboardInputAsync(string?, CancellationToken)

Feeds a string from a keyboard-wedge scanner into the KeyboardWedgeScanAdapter. Only processes input when CurrentMode is Barcode or Hybrid. On a valid non-whitespace value it creates a ScanResult with ScanType = "BARCODE" and Symbology = "KEYBOARD_WEDGE", then raises CodeScanned.
await scanCaptureService.HandleKeyboardInputAsync(scannedText, cancellationToken);

ApplyRfidPolicy(bool, bool, string)

Stores the three RFID capability flags derived from RfidHandheldContextDto. Call this once after fetching the handheld context before starting any RFID session.
scanCaptureService.ApplyRfidPolicy(
    canUseRfid: context.CanUseRfid,
    canOperateWithoutRfid: context.CanOperateWithoutRfid,
    rfidHardwareStatus: context.RfidHardwareStatus);

Events

EventArgs typeRaised by
CodeScannedCodeScannedEventArgsKeyboard-wedge barcode reads (Barcode or Hybrid mode). Carries Result (ScanResult).
TagObservedTagObservedEventArgsEach distinct EPC emitted by the RFID adapter. Carries Result (ScanResult).
SessionStartedScanSessionEventArgsWhen a session becomes active. IsActive = true. Also carries Mode (ScanMode) and Message (human-readable status string).
SessionStoppedScanSessionEventArgsWhen a session ends (manual stop, single-read auto-stop, or policy cap). IsActive = false. Also carries Mode and Message.
TriggerStateChangedTriggerStateChangedEventArgsPhysical trigger key pressed (IsPressed = true) or released (IsPressed = false). Also carries Mode (ScanMode) showing which mode was active when the trigger fired. Android only.
ErrorOccurredScanErrorEventArgsRFID unavailability, adapter failures, or policy blocking. Carries Message (string) and optional Exception (Exception?).
RFID resolve failures must not block barcode or manual inquiry. Any code path that handles ErrorOccurred or a failed API call should degrade to barcode/manual mode rather than disabling the inquiry screen entirely. CanOperateWithoutRfid = true is the system default for this reason.

ScanResult Record

Every scan event delivers a ScanResult instance via its EventArgs.
namespace Handheld.Services.Scanning;

public sealed class ScanResult
{
    public Guid     Id              { get; init; } = Guid.NewGuid();
    public string   ScanType        { get; init; } = string.Empty; // "BARCODE" | "RFID"
    public ScanMode Mode            { get; init; }
    public string   RawValue        { get; init; } = string.Empty;
    public string   NormalizedValue { get; init; } = string.Empty;
    public string?  Epc             { get; init; }
    public string?  Barcode         { get; init; }
    public string?  Symbology       { get; init; }
    public int?     Rssi            { get; init; }
    public string?  Antenna         { get; init; }
    public int      SeenCount       { get; init; } = 1;
    public string?  DeviceId        { get; init; }
    public DateTime TimestampUtc    { get; init; } = DateTime.UtcNow;
    public string?  MetadataJson    { get; init; }

    // Computed display helpers
    public string DisplayValue => !string.IsNullOrWhiteSpace(Epc) ? Epc : NormalizedValue;
    public string DisplayRssi  => Rssi.HasValue ? Rssi.Value.ToString() : "-";
}
Field reference:
FieldNotes
IdUnique per read. Use for deduplication in UI lists.
ScanType"BARCODE" for wedge reads, "RFID" for UHF reads.
ModeThe ScanMode active when the read occurred.
RawValueUnmodified data from the hardware.
NormalizedValueTrimmed; RFID values are additionally upper-cased.
EpcSet for RFID results. Not the same as ItemNo — use GET /api/rfid/tags/resolve/{epc} to map it.
BarcodeSet for keyboard-wedge results.
Symbology"KEYBOARD_WEDGE" for wedge scans; "com.rscja.scanner.uhf" for UHF.
RssiSignal strength in dBm. Null for barcode reads. Use DisplayRssi for safe rendering.
AntennaAntenna identifier from the RFID hardware. May be null.
SeenCountNumber of times the same tag has been observed in the current session (deduplication counter).
DeviceIdHandheld device identifier when provided by the adapter.
MetadataJsonReserved for vendor-specific extra fields.
DisplayValueReturns Epc if set, otherwise NormalizedValue. Safe for all display bindings.
DisplayRssiReturns the RSSI integer as a string, or "-" when unavailable.

ScanSessionRequest Record

public sealed record ScanSessionRequest(ScanMode Mode, string? Reason = null);
Reason is a free-text label that appears in session event messages and aids log correlation. Pass the module or screen name (e.g. "Item Inquiry", "Sandbox").

Scan Adapters

The service composes two adapters. Both are instantiated inside ScanCaptureService — they are not registered in DI directly.

KeyboardWedgeScanAdapter

Handles barcode input delivered as keyboard characters by USB/Bluetooth scanner guns. Wraps raw string input in a ScanResult with ScanType = "BARCODE" and Symbology = "KEYBOARD_WEDGE". Active in Barcode and Hybrid modes. Platform-agnostic — works on all targets.

VendorRfidScanAdapter

Android-only (#if ANDROID). Integrates the CHAINWAY C72 (RSCJA) UHF module via the system ScanService broadcast API. Binds physical trigger keys (keycodes 139, 293, 294) for pistol-style operation. Reads EPC bank (mode 0). Supports single-shot and continuous inventory with configurable deduplication windows and per-session read caps from RfidEffectivePolicyContextDto.

Vendor RFID SDK

The RFID adapter is backed by the vendor AAR binding at:
Platforms/Android/Bindings/DeviceAPI_ver20250209_release.aar
The C72 UHF module is owned by the system ScanService (com.rscja.scanner). Direct DeviceAPI / JNI init() calls return -1 at runtime because ScanService already holds the serial port (/dev/ttyS1). The integration therefore uses the ScanService broadcast API exclusively:
Intent ActionPurpose
ENABLE_FUNCTION_BARCODE_RFID (function=11)Enables the UHF thread (RFID_UHF=true) in ScanService. Also re-sent at the start of every StartSessionAsync call to re-arm the UHF module if InfoWedge has flipped RFID_UHF back to false since InitializeAsync.
START_BARCODE_RFID (function=11)Begins a UHF inventory read. Also sent on every physical trigger-press while a session is active.
STOP_BARCODE_RFID (function=11)Stops the UHF inventory. Also sent on trigger-release in RfidInventory mode (pauses continuous reads without ending the session).
SCAN_RESULT_BROADCAST_RFIDRoutes RFID results to the app’s broadcast receiver. Configured once in InitializeAsync with resultAction = com.rscja.scanner.action.scanner.RFID and data = "data".
UHF_MODE (mode=0)Selects EPC bank output. Mode 0 = EPC (default); mode 1 = TID. Set once in InitializeAsync.
CONTINUOUS_SCAN_RFID (isContinuous)Sets single-shot (isContinuous=false) vs. continuous (true) mode. Sent on each StartSessionAsync.
CONTINUOUS_SCAN_INTERVAL_TIME_RFID (intervalTime=300)Sets the inter-read interval to 300 ms for continuous inventory sessions. Not sent for RfidSingle.
SCAN_KEY_CONFIG_BARCODE_RFIDBinds or unbinds physical trigger keys (keycodes 139, 293, 294) to a function module. Set to function=11 (UHF) on session start, restored to function=2 (2D barcode soft-decode) on session stop.
ENABLE_ENTER_BARCODE_RFID (enter=false)Suppresses the trailing Enter keystroke the wedge injects after each read. Set once in InitializeAsync to prevent the Enter from activating the focused mode picker in the MAUI UI.
ENABLE_TAB_BARCODE_RFID (tab=false)Suppresses the trailing Tab keystroke. Set once in InitializeAsync alongside ENABLE_ENTER_BARCODE_RFID.
Results arrive on two channels simultaneously — the com.rscja.scanner.action.scanner.RFID broadcast and the keyboard wedge (keycode 293 with buffered EPC characters). VendorRfidScanAdapter listens on both and deduplicates via the policy window.

AndroidKeyboardWedge

AndroidKeyboardWedge is a static helper that MainActivity.DispatchKeyEvent calls for every key event. While an RFID session is active (IsSessionActive = true), it buffers the hex characters the wedge emits and fires EpcScanned when keycode 293 arrives, and fires TriggerPressed/TriggerReleased for the physical scan button. All events are logged to logcat tag RFID_WEDGE for ADB diagnostics.

Scan Event Args

The ScanEvents.cs types are shared by both adapters and all consumers of IScanCaptureService.
namespace Handheld.Services.Scanning;

public sealed class CodeScannedEventArgs : EventArgs
{
    public CodeScannedEventArgs(ScanResult result) => Result = result;
    public ScanResult Result { get; }
}

public sealed class TagObservedEventArgs : EventArgs
{
    public TagObservedEventArgs(ScanResult result) => Result = result;
    public ScanResult Result { get; }
}

public sealed class ScanSessionEventArgs : EventArgs
{
    public ScanSessionEventArgs(ScanMode mode, string message, bool isActive)
    {
        Mode = mode;
        Message = message;
        IsActive = isActive;
    }
    public ScanMode Mode    { get; }
    public string   Message { get; }
    public bool     IsActive { get; }
}

public sealed class TriggerStateChangedEventArgs : EventArgs
{
    public TriggerStateChangedEventArgs(bool isPressed, ScanMode mode)
    {
        IsPressed = isPressed;
        Mode      = mode;
    }
    public bool     IsPressed { get; }
    public ScanMode Mode      { get; }
}

public sealed class ScanErrorEventArgs : EventArgs
{
    public ScanErrorEventArgs(string message, Exception? exception = null)
    {
        Message   = message;
        Exception = exception;
    }
    public string     Message   { get; }
    public Exception? Exception { get; }
}

RfidHandheldContextService

RfidHandheldContextService is a singleton that caches the per-module RFID context returned by GET /api/rfid/handheld/context across navigations. Once a module’s context is fetched it is held in memory until ClearCache() is called explicitly.
RfidHandheldContextService cannot be registered via AddHttpClient<T> because that helper always registers the client class as Transient, which would lose the in-memory module cache on every injection. Instead, MauiProgram registers a named HttpClient ("rfid-handheld-context") and resolves it through IHttpClientFactory to construct the singleton manually:
builder.Services.AddHttpClient("rfid-handheld-context", ConfigureHttpClient)
    .AddHttpMessageHandler<DynamicBaseUrlMessageHandler>()
    .AddHttpMessageHandler<AuthenticatedHttpMessageHandler>();

builder.Services.AddSingleton<RfidHandheldContextService>(sp =>
{
    var http = sp.GetRequiredService<IHttpClientFactory>()
                 .CreateClient("rfid-handheld-context");
    return new RfidHandheldContextService(http);
});
// Fetch (cached after first call)
var context = await rfidHandheldContextService.GetContextAsync("item-inquiry", ct);

// Apply to the scan service before starting a session
scanCaptureService.ApplyRfidPolicy(
    context.CanUseRfid,
    context.CanOperateWithoutRfid,
    context.RfidHardwareStatus);

// Invalidate when the operator logs out or settings change
rfidHandheldContextService.ClearCache();
If the backend is unreachable or returns an error, GetContextAsync returns RfidHandheldContextDto.Fallback(module) — a safe context with CanUseRfid = false, CanOperateWithoutRfid = true, and both barcode and manual fallbacks allowed. This ensures the handheld never blocks the operator due to a context fetch failure.

Debug Sandbox

RfidSandboxPage (backed by RfidSandboxViewModel) is a debug-only screen for testing SDK initialization and EPC reads in isolation. It:
  • Exposes all four ScanMode values for selection.
  • Calls InitializeAsync, StartSessionAsync, and StopSessionAsync through the production IScanCaptureService.
  • Displays resolved item results (calls ItemService.ResolveRfidTagAsync for each EPC) and a live read count.
  • Forces ApplyRfidPolicy(canUseRfid: true, canOperateWithoutRfid: false, "SANDBOX") so RFID errors surface instead of silently falling back.
  • Exposes LastError and SessionStatus bindings for on-screen diagnostics.
The sandbox page is not part of the production navigation graph and does not affect any warehouse transaction flow.

Build docs developers (and LLMs) love