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.

The scanning subsystem is the primary data-entry mechanism on Dragon Guard Handheld. It abstracts two fundamentally different capture technologies — 2D barcode via keyboard-wedge injection and UHF RFID via a proprietary vendor SDK — behind a single IScanCaptureService interface. View-models subscribe to events on the interface and never interact with hardware adapters directly.
IScanCaptureService (and its concrete implementation ScanCaptureService) is registered as a singleton in the DI container. All view-models share the same service instance; starting a new session from one screen automatically stops any prior session state.

IScanCaptureService

namespace Handheld.Services.Scanning;

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;

    // ── Policy ─────────────────────────────────────────────────────────────────
    void ApplyRfidPolicy(bool canUseRfid, bool canOperateWithoutRfid, string rfidHardwareStatus);

    // ── Lifecycle ──────────────────────────────────────────────────────────────
    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);
}

State properties

PropertyTypeDescription
CurrentModeScanModeThe mode that is currently active or was most recently set
IsSessionActivebooltrue while a scan session is open and accepting reads
CanUseRfidboolSet by ApplyRfidPolicy; false when device has no RFID hardware or policy disables it
CanOperateWithoutRfidboolWhen true, RFID failures silently fall back to barcode mode
RfidHardwareStatusstringOpaque status string from the policy context (e.g. NO_RFID_PROFILE)

Session lifecycle

InitializeAsync()
    └─► registers broadcast receivers, enables UHF module

StartSessionAsync(ScanSessionRequest)
    ├── Mode = Barcode  →  IsSessionActive = true, fires SessionStarted
    ├── Mode = Rfid*, CanUseRfid = false, CanOperateWithoutRfid = true
    │       →  fires ErrorOccurred, falls back to Barcode
    ├── Mode = Rfid*, CanUseRfid = false, CanOperateWithoutRfid = false
    │       →  fires ErrorOccurred, session NOT started
    └── Mode = Rfid*  →  delegates to VendorRfidScanAdapter (Android only)

StopSessionAsync()
    ├── Mode = Barcode  →  IsSessionActive = false, fires SessionStopped
    └── Mode = Rfid*   →  delegates to VendorRfidScanAdapter, fires SessionStopped

HandleKeyboardInputAsync(string?)
    └── Only active when Mode = Barcode or Hybrid
        fires CodeScanned with ScanType = "BARCODE"

ScanSessionRequest

public sealed record ScanSessionRequest(ScanMode Mode, string? Reason = null);
ScanSessionRequest is a positional record passed to StartSessionAsync. Reason is an optional human-readable label that can be used for logging or UI display (e.g. "Receipt scan", "Shipment inventory").

ScanMode

public enum ScanMode
{
    Barcode      = 0,   // 2D / 1D barcode via keyboard-wedge
    RfidSingle   = 1,   // UHF RFID, stops after the first unique EPC
    RfidInventory = 2,  // UHF RFID, continuous reads until StopSessionAsync
    Hybrid       = 3    // Barcode via keyboard-wedge, RFID reads forwarded as tags
}
ValueCapture channelSession ends automatically
BarcodeHandleKeyboardInputAsyncCodeScannedNo — must call StopSessionAsync
RfidSingleVendor broadcast / keyboard-wedge EPCYes — after the first unique EPC
RfidInventoryVendor broadcast / keyboard-wedge EPCNo — must call StopSessionAsync or cap is reached
HybridBoth channels simultaneouslyNo

ScanResult

public sealed class ScanResult
{
    public Guid     Id              { get; init; }   // Unique per read; defaults to Guid.NewGuid()
    public string   ScanType        { get; init; }   // "BARCODE" | "RFID"
    public ScanMode Mode            { get; init; }   // Mode that produced this result
    public string   RawValue        { get; init; }   // Unmodified data from the hardware
    public string   NormalizedValue { get; init; }   // Trimmed / uppercased value
    public string?  Epc             { get; init; }   // Set for RFID reads; null for barcode
    public string?  Barcode         { get; init; }   // Set for barcode reads; null for RFID
    public string?  Symbology       { get; init; }   // e.g. "KEYBOARD_WEDGE", "com.rscja.scanner.uhf"
    public int?     Rssi            { get; init; }   // Signal strength (RFID only)
    public string?  Antenna         { get; init; }   // Antenna identifier (RFID only)
    public int      SeenCount       { get; init; }   // Observation count; defaults to 1
    public string?  DeviceId        { get; init; }   // Source device identifier
    public DateTime TimestampUtc    { get; init; }   // UTC capture time; defaults to DateTime.UtcNow
    public string?  MetadataJson    { get; init; }   // Arbitrary adapter-specific metadata

    // ── Computed display helpers ──────────────────────────────────────────────
    public string DisplayValue { get; }   // Epc if set, otherwise NormalizedValue
    public string DisplayRssi  { get; }   // Rssi.ToString() or "-" when null
}

Event Args

CodeScannedEventArgs

Raised by IScanCaptureService.CodeScanned when a barcode read completes.
public sealed class CodeScannedEventArgs : EventArgs
{
    public CodeScannedEventArgs(ScanResult result);
    public ScanResult Result { get; }
}

TagObservedEventArgs

Raised by IScanCaptureService.TagObserved when an RFID EPC is received (after deduplication).
public sealed class TagObservedEventArgs : EventArgs
{
    public TagObservedEventArgs(ScanResult result);
    public ScanResult Result { get; }
}

ScanSessionEventArgs

Raised by SessionStarted and SessionStopped.
public sealed class ScanSessionEventArgs : EventArgs
{
    public ScanSessionEventArgs(ScanMode mode, string message, bool isActive);
    public ScanMode Mode     { get; }   // Mode that started or stopped
    public string   Message  { get; }   // Human-readable status message
    public bool     IsActive { get; }   // true for SessionStarted, false for SessionStopped
}

TriggerStateChangedEventArgs

Raised by TriggerStateChanged when the physical scan trigger is pressed or released.
public sealed class TriggerStateChangedEventArgs : EventArgs
{
    public TriggerStateChangedEventArgs(bool isPressed, ScanMode mode);
    public bool     IsPressed { get; }   // true = trigger pressed, false = released
    public ScanMode Mode      { get; }
}

ScanErrorEventArgs

Raised by ErrorOccurred for hardware errors, policy violations, or RFID fallback notifications.
public sealed class ScanErrorEventArgs : EventArgs
{
    public ScanErrorEventArgs(string message, Exception? exception = null);
    public string     Message   { get; }   // Human-readable description (Spanish on handheld)
    public Exception? Exception { get; }   // Inner exception if available; null for policy errors
}

ScanCaptureService (concrete implementation)

ScanCaptureService is the concrete singleton that backs IScanCaptureService. It owns one KeyboardWedgeScanAdapter instance (all platforms) and one VendorRfidScanAdapter instance (Android only, guarded by #if ANDROID).
public sealed class ScanCaptureService : IScanCaptureService, IDisposable
{
    public ScanCaptureService();

    // Additional methods not on the interface (called by the RFID context service)
    public void ConfigureDeviceProfile(RfidEffectiveDeviceProfileContextDto? profile);
    public void ConfigurePolicy(RfidEffectivePolicyContextDto? policy);

    public void Dispose();  // Unregisters broadcast receivers and keyboard-wedge events
}

Event forwarding

VendorRfidScanAdapter events are forwarded to the interface-level events inside the constructor:
Adapter eventForwarded to
VendorRfidScanAdapter.TagObservedIScanCaptureService.TagObserved
VendorRfidScanAdapter.SessionStartedIScanCaptureService.SessionStarted + updates IsSessionActive
VendorRfidScanAdapter.SessionStoppedIScanCaptureService.SessionStopped + updates IsSessionActive
VendorRfidScanAdapter.TriggerStateChangedIScanCaptureService.TriggerStateChanged
VendorRfidScanAdapter.ErrorOccurredIScanCaptureService.ErrorOccurred
Barcode events are generated inline in HandleKeyboardInputAsync via KeyboardWedgeScanAdapter.CreateResult and forwarded directly as CodeScanned.

KeyboardWedgeScanAdapter

KeyboardWedgeScanAdapter converts keyboard-wedge text input (the Android Entry field value after a scanner injects characters and a terminating keystroke) into a ScanResult.
public sealed class KeyboardWedgeScanAdapter
{
    // Returns null if value is null or whitespace
    public ScanResult? CreateResult(string? value, ScanMode mode);
}
The produced ScanResult always has:
  • ScanType = "BARCODE"
  • Symbology = "KEYBOARD_WEDGE"
  • Barcode = trimmed input value
  • NormalizedValue = trimmed input value
  • RawValue = original (untrimmed) input
The keyboard-wedge adapter is the correct path for all handheld scanners that operate in “HID keyboard” mode — including standard 1D/2D barcode guns and any scanner configured for output mode 0 (keyboard emulation). ViewModels call HandleKeyboardInputAsync from their scan-entry TextChanged handler.

VendorRfidScanAdapter (Android)

VendorRfidScanAdapter integrates the CHAINWAY C72 (and compatible RSCJA-family) UHF RFID module via the vendor’s Scanner broadcast API. The C72 UHF hardware is owned by the system ScanService (com.rscja.scanner); direct DeviceAPI / JNI initialization always fails because ScanService already holds /dev/ttyS1. The correct integration path is the broadcast API.
// Android-only: Platforms/Android/Scanning/VendorRfidScanAdapter.cs
public sealed class VendorRfidScanAdapter : IDisposable
{
    public bool     IsSessionActive { get; }
    public ScanMode CurrentMode     { get; }
    public string?  LastError       { get; }

    public event EventHandler<TagObservedEventArgs>?         TagObserved;
    public event EventHandler<ScanSessionEventArgs>?         SessionStarted;
    public event EventHandler<ScanSessionEventArgs>?         SessionStopped;
    public event EventHandler<TriggerStateChangedEventArgs>? TriggerStateChanged;
    public event EventHandler<ScanErrorEventArgs>?           ErrorOccurred;

    public void ConfigureProfile(RfidEffectiveDeviceProfileContextDto? profile);
    public void ConfigurePolicy(RfidEffectivePolicyContextDto? policy);

    public Task InitializeAsync(CancellationToken ct = default);
    public Task SetModeAsync(ScanMode mode, CancellationToken ct = default);
    public async Task StartSessionAsync(ScanSessionRequest request, CancellationToken ct = default);
    public Task StopSessionAsync(CancellationToken ct = default);

    public void Dispose();
}

Broadcast intents used

Intent actionDirectionPurpose
com.rscja.scanner.action.ENABLE_FUNCTION_BARCODE_RFIDSendEnables the UHF module (function = 11)
com.rscja.scanner.action.START_BARCODE_RFIDSendStarts UHF inventory
com.rscja.scanner.action.STOP_BARCODE_RFIDSendStops UHF inventory
com.rscja.scanner.action.SCAN_RESULT_BROADCAST_RFIDSendRoutes EPC results to com.rscja.scanner.action.scanner.RFID
com.rscja.scanner.action.UHF_MODESendSets EPC bank read mode (mode = 0)
com.rscja.scanner.action.SCAN_KEY_CONFIG_BARCODE_RFIDSendBinds/unbinds physical trigger keys to UHF function
com.rscja.scanner.action.CONTINUOUS_SCAN_RFIDSendToggles single-shot vs continuous inventory
com.rscja.scanner.action.CONTINUOUS_SCAN_INTERVAL_TIME_RFIDSendSets inventory interval (300 ms default)
com.rscja.scanner.action.ENABLE_ENTER_BARCODE_RFIDSendSuppresses trailing Enter keystroke after each EPC (set to false)
com.rscja.android.DATA_RESULTReceiveInfoWedge primary data broadcast (com.infowedge.data key)
com.rscja.scanner.action.scanner.RFIDReceiveRFID broadcast fallback (data key)
com.scanner.broadcastReceiveBarcode broadcast fallback
com.rscja.android.OVER_RESULTReceiveEnd-of-scan signal (ignored, not data)

Policy enforcement

ConfigurePolicy(RfidEffectivePolicyContextDto?) stores a policy context that is applied during EPC processing:
  • Duplicate suppression — if DuplicateReadWindowSeconds > 0, EPCs seen within the window are silently dropped.
  • Max reads cap — if MaxReadsPerOperation is set, StopSessionAsync is called automatically once the cap is reached during inventory mode.

Single vs inventory modes

ModeContinuous flagSession auto-stop
RfidSingleisContinuous = falseYes — after the first unique EPC processed by ProcessEpc
RfidInventoryisContinuous = true, interval = 300 msNo — runs until StopSessionAsync or cap

Physical trigger behavior

TriggerKeys = { 139, 293, 294 } are bound to function 11 (UHF) while a session is active. On trigger press, START_BARCODE_RFID is sent; on release, STOP_BARCODE_RFID is sent for inventory mode. When the session stops, trigger keys are rebound to FunctionBarcode2DSoft = 2 (standard 2D barcode).
The Enter keystroke suppression (ENABLE_ENTER_BARCODE_RFID = false) is critical. Without it, the RSCJA ScanService injects a newline after every EPC, which opens the focused MAUI Picker on the scan screen. This is configured during InitializeAsync and persists for the application lifetime.

Build docs developers (and LLMs) love