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
| Property | Type | Description |
|---|
CurrentMode | ScanMode | The mode that is currently active or was most recently set |
IsSessionActive | bool | true while a scan session is open and accepting reads |
CanUseRfid | bool | Set by ApplyRfidPolicy; false when device has no RFID hardware or policy disables it |
CanOperateWithoutRfid | bool | When true, RFID failures silently fall back to barcode mode |
RfidHardwareStatus | string | Opaque 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
}
| Value | Capture channel | Session ends automatically |
|---|
Barcode | HandleKeyboardInputAsync → CodeScanned | No — must call StopSessionAsync |
RfidSingle | Vendor broadcast / keyboard-wedge EPC | Yes — after the first unique EPC |
RfidInventory | Vendor broadcast / keyboard-wedge EPC | No — must call StopSessionAsync or cap is reached |
Hybrid | Both channels simultaneously | No |
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 event | Forwarded to |
|---|
VendorRfidScanAdapter.TagObserved | IScanCaptureService.TagObserved |
VendorRfidScanAdapter.SessionStarted | IScanCaptureService.SessionStarted + updates IsSessionActive |
VendorRfidScanAdapter.SessionStopped | IScanCaptureService.SessionStopped + updates IsSessionActive |
VendorRfidScanAdapter.TriggerStateChanged | IScanCaptureService.TriggerStateChanged |
VendorRfidScanAdapter.ErrorOccurred | IScanCaptureService.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 action | Direction | Purpose |
|---|
com.rscja.scanner.action.ENABLE_FUNCTION_BARCODE_RFID | Send | Enables the UHF module (function = 11) |
com.rscja.scanner.action.START_BARCODE_RFID | Send | Starts UHF inventory |
com.rscja.scanner.action.STOP_BARCODE_RFID | Send | Stops UHF inventory |
com.rscja.scanner.action.SCAN_RESULT_BROADCAST_RFID | Send | Routes EPC results to com.rscja.scanner.action.scanner.RFID |
com.rscja.scanner.action.UHF_MODE | Send | Sets EPC bank read mode (mode = 0) |
com.rscja.scanner.action.SCAN_KEY_CONFIG_BARCODE_RFID | Send | Binds/unbinds physical trigger keys to UHF function |
com.rscja.scanner.action.CONTINUOUS_SCAN_RFID | Send | Toggles single-shot vs continuous inventory |
com.rscja.scanner.action.CONTINUOUS_SCAN_INTERVAL_TIME_RFID | Send | Sets inventory interval (300 ms default) |
com.rscja.scanner.action.ENABLE_ENTER_BARCODE_RFID | Send | Suppresses trailing Enter keystroke after each EPC (set to false) |
com.rscja.android.DATA_RESULT | Receive | InfoWedge primary data broadcast (com.infowedge.data key) |
com.rscja.scanner.action.scanner.RFID | Receive | RFID broadcast fallback (data key) |
com.scanner.broadcast | Receive | Barcode broadcast fallback |
com.rscja.android.OVER_RESULT | Receive | End-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
| Mode | Continuous flag | Session auto-stop |
|---|
RfidSingle | isContinuous = false | Yes — after the first unique EPC processed by ProcessEpc |
RfidInventory | isContinuous = true, interval = 300 ms | No — 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.