diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index 90cdf84..0000000
--- a/.editorconfig
+++ /dev/null
@@ -1,111 +0,0 @@
-[*]
-charset=utf-8
-end_of_line=crlf
-trim_trailing_whitespace=false
-insert_final_newline=false
-indent_style=space
-indent_size=4
-# Microsoft .NET properties
-csharp_new_line_before_members_in_object_initializers=false
-csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
-csharp_prefer_braces=true:warning
-csharp_style_var_elsewhere=true:suggestion
-csharp_style_var_for_built_in_types=true:suggestion
-csharp_style_var_when_type_is_apparent=true:suggestion
-csharp_using_directive_placement=inside_namespace:silent
-dotnet_naming_rule.event_rule.severity=warning
-dotnet_naming_rule.event_rule.style=on_upper_camel_case_style
-dotnet_naming_rule.event_rule.symbols=event_symbols
-dotnet_naming_rule.private_constants_rule.severity=none
-dotnet_naming_rule.private_constants_rule.style=upper_camel_case_style
-dotnet_naming_rule.private_constants_rule.symbols=private_constants_symbols
-dotnet_naming_rule.private_instance_fields_rule.severity=warning
-dotnet_naming_rule.private_instance_fields_rule.style=lower_camel_case_style
-dotnet_naming_rule.private_instance_fields_rule.symbols=private_instance_fields_symbols
-dotnet_naming_rule.private_static_fields_rule.severity=warning
-dotnet_naming_rule.private_static_fields_rule.style=s_lower_camel_case_style
-dotnet_naming_rule.private_static_fields_rule.symbols=private_static_fields_symbols
-dotnet_naming_rule.private_static_readonly_rule.severity=warning
-dotnet_naming_rule.private_static_readonly_rule.style=upper_camel_case_style
-dotnet_naming_rule.private_static_readonly_rule.symbols=private_static_readonly_symbols
-dotnet_naming_rule.type_parameters_rule.severity=warning
-dotnet_naming_rule.type_parameters_rule.style=upper_camel_case_style
-dotnet_naming_rule.type_parameters_rule.symbols=type_parameters_symbols
-dotnet_naming_style.lower_camel_case_style.capitalization=camel_case
-dotnet_naming_style.lower_camel_case_style.required_prefix=_
-dotnet_naming_style.on_upper_camel_case_style.capitalization=pascal_case
-dotnet_naming_style.on_upper_camel_case_style.required_prefix=On
-dotnet_naming_style.s_lower_camel_case_style.capitalization=camel_case
-dotnet_naming_style.s_lower_camel_case_style.required_prefix=s_
-dotnet_naming_style.upper_camel_case_style.capitalization=pascal_case
-dotnet_naming_symbols.event_symbols.applicable_accessibilities=*
-dotnet_naming_symbols.event_symbols.applicable_kinds=event
-dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities=private
-dotnet_naming_symbols.private_constants_symbols.applicable_kinds=field
-dotnet_naming_symbols.private_constants_symbols.required_modifiers=const
-dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities=private
-dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds=field
-dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities=private
-dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds=field
-dotnet_naming_symbols.private_static_fields_symbols.required_modifiers=static
-dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities=private
-dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds=field
-dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers=static,readonly
-dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities=*
-dotnet_naming_symbols.type_parameters_symbols.applicable_kinds=type_parameter
-dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:none
-dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:none
-dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:none
-dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion
-dotnet_style_predefined_type_for_member_access=true:suggestion
-dotnet_style_qualification_for_event=false:suggestion
-dotnet_style_qualification_for_field=false:suggestion
-dotnet_style_qualification_for_method=false:suggestion
-dotnet_style_qualification_for_property=false:suggestion
-dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion
-# ReSharper properties
-resharper_autodetect_indent_settings=true
-resharper_braces_for_for=required
-resharper_braces_for_foreach=required
-resharper_braces_for_ifelse=required
-resharper_braces_for_while=required
-resharper_braces_redundant=true
-resharper_csharp_naming_rule.private_constants=AaBb:do_not_check
-resharper_csharp_naming_rule.private_instance_fields=_ + aaBb
-resharper_csharp_naming_rule.private_static_fields=s_ + aaBb
-resharper_csharp_naming_rule.private_static_readonly=AaBb
-resharper_enforce_line_ending_style=true
-resharper_place_field_attribute_on_same_line=false
-resharper_use_indent_from_vs=false
-# ReSharper inspection severities
-resharper_arrange_redundant_parentheses_highlighting=hint
-resharper_arrange_this_qualifier_highlighting=hint
-resharper_arrange_type_member_modifiers_highlighting=hint
-resharper_arrange_type_modifiers_highlighting=hint
-resharper_built_in_type_reference_style_for_member_access_highlighting=hint
-resharper_built_in_type_reference_style_highlighting=hint
-resharper_enforce_do_while_statement_braces_highlighting=warning
-resharper_enforce_fixed_statement_braces_highlighting=warning
-resharper_enforce_foreach_statement_braces_highlighting=warning
-resharper_enforce_for_statement_braces_highlighting=warning
-resharper_enforce_if_statement_braces_highlighting=warning
-resharper_enforce_lock_statement_braces_highlighting=warning
-resharper_enforce_while_statement_braces_highlighting=warning
-resharper_redundant_base_qualifier_highlighting=warning
-resharper_suggest_var_or_type_built_in_types_highlighting=hint
-resharper_suggest_var_or_type_elsewhere_highlighting=hint
-resharper_suggest_var_or_type_simple_types_highlighting=hint
-resharper_web_config_module_not_resolved_highlighting=warning
-resharper_web_config_type_not_resolved_highlighting=warning
-resharper_web_config_wrong_module_highlighting=warning
-[{.eslintrc,.stylelintrc,jest.config,.babelrc,bowerrc,*.jsb3,*.jsb2,*.json}]
-indent_style=space
-indent_size=2
-[*.{appxmanifest,asax,ascx,aspx,build,cs,cshtml,dtd,fs,fsi,fsscript,fsx,master,ml,mli,nuspec,razor,resw,resx,skin,vb,xaml,xamlx,xoml,xsd}]
-indent_style=space
-indent_size=4
-tab_width=4
-
-[{*.yml,*.yaml}]
-indent_style=space
-indent_size=2
diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
index 65dc0ee..1e442b4 100644
--- a/.github/workflows/run_tests.yml
+++ b/.github/workflows/run_tests.yml
@@ -6,24 +6,25 @@ on:
jobs:
build:
- name: Build check on ${{ matrix.os }} - ${{ matrix.configuration }} (SDK ${{ matrix.sdk }})
+ name: Build check on ${{ matrix.os }} - ${{ matrix.configuration }} (SDK ${{ matrix.sdk }})
strategy:
matrix:
- os: [windows-latest]
- sdk: [ 3.1.x ]
- configuration: [Release, Debug]
+ os: [ windows-latest ]
+ sdk: [ 8.0 ]
+ configuration: [ Debug, Release ]
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v2
-
- - name: Setup dotnet
- uses: actions/setup-dotnet@v1
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ matrix.sdk }}
-
- - name: Build Core
- run: dotnet build --configuration ${{ matrix.configuration }} PTI.Rs232Validator/PTI.Rs232Validator.csproj
-
- - name: Build Emulator
- run: dotnet build --configuration ${{ matrix.configuration }} PTI.Rs232Validator.Emulator/PTI.Rs232Validator.Emulator.csproj
+
+ - name: Build
+ run: dotnet build --configuration ${{ matrix.configuration }} PTI.Rs232Validator/PTI.Rs232Validator.csproj
+
+ - name: Test
+ run: dotnet test --configuration ${{ matrix.configuration }} PTI.Rs232Validator.Test/PTI.Rs232Validator.Test.csproj
diff --git a/PTI.Rs232Validator.CLI/BaseLogger.cs b/PTI.Rs232Validator.CLI/BaseLogger.cs
deleted file mode 100644
index a3f6d0f..0000000
--- a/PTI.Rs232Validator.CLI/BaseLogger.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-namespace PTI.Rs232Validator.CLI
-{
- using System;
-
- public abstract class BaseLogger : ILogger
- {
- ///
- /// Relative timestamp
- ///
- protected readonly DateTime Epoch = DateTime.Now;
-
- ///
- /// Logging level
- /// 0: None
- /// 1: Error (Red)
- /// 2: Error, Info (White)
- /// 3: Error, Info, Debug (Gray)
- /// 4: Error, Info, Debug, Trace (DarkGray)
- ///
- public int Level { get; set; }
-
- ///
- public abstract void Trace(string format, params object[] args);
-
- ///
- public abstract void Debug(string format, params object[] args);
-
- ///
- public abstract void Info(string format, params object[] args);
-
- ///
- public abstract void Error(string format, params object[] args);
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.CLI/Commands/SendExtendedRequestCommand.cs b/PTI.Rs232Validator.CLI/Commands/SendExtendedRequestCommand.cs
new file mode 100644
index 0000000..efad40d
--- /dev/null
+++ b/PTI.Rs232Validator.CLI/Commands/SendExtendedRequestCommand.cs
@@ -0,0 +1,61 @@
+using PTI.Rs232Validator.Cli.Utility;
+using PTI.Rs232Validator.Messages.Commands;
+using Spectre.Console.Cli;
+using System.ComponentModel;
+
+namespace PTI.Rs232Validator.Cli.Commands;
+
+///
+/// A command to send an extended request to a bill acceptor.
+///
+public class SendExtendedRequestCommand : Command
+{
+ ///
+ /// The settings for .
+ ///
+ public class Settings : CommandSettings
+ {
+ [CommandArgument(0, "")]
+ public string PortName { get; init; } = string.Empty;
+
+ [CommandArgument(1, "")]
+ [TypeConverter(typeof(ByteConverter))]
+ public ExtendedCommand ExtendedCommand { get; init; }
+
+ [CommandArgument(2, "[arguments]")]
+ public byte[] Arguments { get; init; } = [];
+ }
+
+ ///
+ public override int Execute(CommandContext context, Settings settings)
+ {
+ var commandLogger = Factory.CreateMultiLogger();
+ using var billValidator = Factory.CreateBillValidator(settings.PortName);
+
+ switch (settings.ExtendedCommand)
+ {
+ case ExtendedCommand.BarcodeDetected:
+ var responseMessage = billValidator.GetDetectedBarcode().Result;
+ if (responseMessage is { IsValid: true, Barcode.Length: > 0 })
+ {
+ commandLogger.LogInfo($"The barcode is: {responseMessage.Barcode}");
+ }
+ else if (responseMessage is { IsValid: true, Barcode.Length: 0 })
+ {
+ commandLogger.LogInfo("No barcode was detected since the last power cycle.");
+ }
+ else
+ {
+ commandLogger.LogError("Failed to get the barcode.");
+ }
+
+ break;
+
+ default:
+ commandLogger.LogError("The specified command is not supported.");
+ return 1;
+ }
+
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.CLI/ConsoleInterrupt.cs b/PTI.Rs232Validator.CLI/ConsoleInterrupt.cs
deleted file mode 100644
index a1861ef..0000000
--- a/PTI.Rs232Validator.CLI/ConsoleInterrupt.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-namespace PTI.Rs232Validator.CLI
-{
- using System.Runtime.InteropServices;
-
- ///
- /// CTRL+C console escape helper
- ///
- public static class ConsoleInterrupt
- {
- ///
- /// Called when control event is received
- ///
- /// Type of control event
- public delegate bool HandlerRoutine(CtrlTypes ctrlType);
-
- ///
- /// Native control types
- ///
- public enum CtrlTypes
- {
- CtrlCEvent = 0,
- CtrlBreakEvent,
- CtrlCloseEvent,
- CtrlLogoffEvent = 5,
- CtrlShutdownEvent
- }
-
- ///
- /// Add or remove a console control event handler
- ///
- /// Callback
- /// True to add, false to remove
- /// true on success
- [DllImport("Kernel32")]
- public static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add);
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.CLI/ConsoleLogger.cs b/PTI.Rs232Validator.CLI/ConsoleLogger.cs
deleted file mode 100644
index 5f2ed44..0000000
--- a/PTI.Rs232Validator.CLI/ConsoleLogger.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-namespace PTI.Rs232Validator.CLI
-{
- using System;
-
- ///
- /// Color console logger
- ///
- internal class ConsoleLogger : BaseLogger
- {
- ///
- public override void Trace(string format, params object[] args)
- {
- if (Level < 4)
- {
- return;
- }
-
- Log("TRACE", ConsoleColor.DarkGray, format, args);
- }
-
- ///
- public override void Debug(string format, params object[] args)
- {
- if (Level < 3)
- {
- return;
- }
-
- Log("DEBUG", ConsoleColor.Gray, format, args);
- }
-
- ///
- public override void Info(string format, params object[] args)
- {
- if (Level < 2)
- {
- return;
- }
-
- Log("INFOR", ConsoleColor.White, format, args);
- }
-
- ///
- public override void Error(string format, params object[] args)
- {
- if (Level < 1)
- {
- return;
- }
-
- Log("ERROR", ConsoleColor.Red, format, args);
- }
-
- private void Log(string level, ConsoleColor color, string format, params object[] args)
- {
- Console.ForegroundColor = color;
- Console.Write($"[{level}] {DateTime.Now - Epoch}::");
- Console.WriteLine(format, args);
- Console.ForegroundColor = ConsoleColor.White;
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.CLI/FileLogger.cs b/PTI.Rs232Validator.CLI/FileLogger.cs
deleted file mode 100644
index f10df2d..0000000
--- a/PTI.Rs232Validator.CLI/FileLogger.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-namespace PTI.Rs232Validator.CLI
-{
- using System;
- using System.IO;
-
- public class FileLogger : BaseLogger, IDisposable
- {
- private readonly Stream _stream;
- private readonly StreamWriter _logWriter;
-
- ///
- /// Create a new file logger that write to this log path.
- /// If the file does not exist, it will be created. If the
- /// file exists, it will be appended to.
- ///
- /// File to write to
- ///
- public FileLogger(string logPath)
- {
- _stream = new FileStream(logPath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
- _logWriter = new StreamWriter(_stream)
- {
- AutoFlush = true
- };
- }
-
- ///
- public override void Trace(string format, params object[] args)
- {
- if (Level < 4)
- {
- return;
- }
-
- Log("TRACE", format, args);
- }
-
-
- ///
- public override void Debug(string format, params object[] args)
- {
- if (Level < 3)
- {
- return;
- }
-
- Log("DEBUG", format, args);
- }
-
- ///
- public override void Info(string format, params object[] args)
- {
- if (Level < 2)
- {
- return;
- }
-
- Log("INFOR", format, args);
- }
-
- ///
- public override void Error(string format, params object[] args)
- {
- if (Level < 1)
- {
- return;
- }
-
- Log("ERROR", format, args);
- }
-
- private void Log(string level, string format, params object[] args)
- {
- var line = $"[{level}] {DateTime.Now - Epoch}::{string.Format(format, args)}";
- _logWriter.WriteLine(line);
- }
-
- ///
- public void Dispose()
- {
- _logWriter?.Flush();
- _logWriter?.Dispose();
-
- _stream?.Flush();
- _stream?.Dispose();
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.CLI/MultiLogger.cs b/PTI.Rs232Validator.CLI/MultiLogger.cs
deleted file mode 100644
index f50c56f..0000000
--- a/PTI.Rs232Validator.CLI/MultiLogger.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-namespace PTI.Rs232Validator.CLI
-{
- using System.Collections.Generic;
-
- ///
- /// Logs to multiple ILoggers at once
- ///
- public class MultiLogger : ILogger
- {
- private readonly IEnumerable _loggers;
-
- public MultiLogger(IEnumerable loggers)
- {
- _loggers = loggers;
- }
-
- ///
- public void Trace(string format, params object[] args)
- {
- foreach (var logger in _loggers)
- {
- logger.Trace(format, args);
- }
- }
-
- ///
- public void Debug(string format, params object[] args)
- {
- foreach (var logger in _loggers)
- {
- logger.Debug(format, args);
- }
- }
-
- ///
- public void Info(string format, params object[] args)
- {
- foreach (var logger in _loggers)
- {
- logger.Info(format, args);
- }
- }
-
- ///
- public void Error(string format, params object[] args)
- {
- foreach (var logger in _loggers)
- {
- logger.Error(format, args);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.CLI/Program.cs b/PTI.Rs232Validator.CLI/Program.cs
deleted file mode 100644
index 4e6b57f..0000000
--- a/PTI.Rs232Validator.CLI/Program.cs
+++ /dev/null
@@ -1,155 +0,0 @@
-namespace PTI.Rs232Validator.CLI
-{
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading;
- using Emulator;
-
- internal static class Program
- {
- private static readonly string[] BillValues = {"Unknown", "$1", "$2", "$5", "$10", "$20", "$50", "$100"};
-
- private static CancellationTokenSource s_cancellationTokenSource;
-
- private static void Main(string[] args)
- {
- // Capture ctrl+c to stop process
- ConsoleInterrupt.SetConsoleCtrlHandler(ConsoleHandler, true);
-
- s_cancellationTokenSource = new CancellationTokenSource();
-
- var loggers = new List
- {
- new FileLogger("trace.log") {Level = 4},
- new FileLogger("debug.log") {Level = 3},
- new FileLogger("info.log") {Level = 2},
- new FileLogger("error.log") {Level = 1},
- new ConsoleLogger {Level = 4}
- };
-
- var logger = new MultiLogger(loggers);
-
- // RunEmulator(logger);
-
- RunValidator(logger, args);
- }
-
- ///
- /// Runs a virtual bill validator, forever
- ///
- /// Logger attaches to emulator
- private static void RunEmulator(ILogger logger)
- {
- var runner = new EmulationRunner(TimeSpan.FromMilliseconds(1),
- s_cancellationTokenSource.Token, logger);
- runner.CreditEveryNLoops(10, -1, 1, 2, 3, 4, 5, 6, 7);
- }
-
- ///
- /// Run a real bill validator
- ///
- /// Logger attaches to validator
- /// Program arguments
- private static void RunValidator(ILogger logger, string[] args)
- {
- var portName = args.FirstOrDefault();
- if (string.IsNullOrEmpty(portName))
- {
- Console.WriteLine("Usage: rs232validator.cli.exe portName");
- return;
- }
-
-
- var config = Rs232Config.UsbRs232Config(portName, logger);
-
- var validator = new ApexValidator(config);
-
- validator.OnLostConnection += (sender, eventArgs) =>
- {
- config.Logger?.Error($"[APP] Lost connection to acceptor");
- };
-
- validator.OnBillInEscrow += (sender, i) =>
- {
- // For USA this index represent $20. This example will always return a $20
- // Alternatively you could set the Rs232Config mask to 0x5F to disable a 20.
- if (i == 5)
- {
- config.Logger.Info($"[APP] Issuing a return command for this {BillValues[i]}");
-
- validator.Return();
- }
- else
- {
- config.Logger.Info($"[APP] Issuing stack command for this {BillValues[i]}");
-
- validator.Stack();
- }
- };
-
- validator.OnCreditIndexReported += (sender, i) =>
- {
- config.Logger.Info($"[APP] Credit issued: {BillValues[i]}");
- };
-
- validator.OnStateChanged += (sender, state) =>
- {
- config.Logger.Info($"[APP] State changed from {state.OldState} to {state.NewState}");
- };
-
- validator.OnEventReported += (sender, evt) => { config.Logger.Info($"[APP] Event(s) reported: {evt}"); };
-
- validator.OnCashBoxRemoved += (sender, eventArgs) => { config.Logger.Info("[APP] Cash box removed"); };
-
- validator.OnCashBoxAttached += (sender, eventArgs) => { config.Logger.Info("[APP] Cash box attached"); };
-
- if (!validator.StartPollingLoop())
- {
- config.Logger.Error("[APP] Failed to start RS232 main loop");
- return;
- }
-
- config.Logger.Info("[APP] Validator is now running. CTRL+C to Exit");
- while (true)
- {
- Thread.Sleep(TimeSpan.FromMilliseconds(100));
-
- if (!validator.IsUnresponsive)
- {
- continue;
- }
-
- config.Logger?.Error("[APP] validator failed to start. Quitting now");
-
- validator.StopPollingLoop();
-
- break;
- }
- }
-
- ///
- /// Console interrupt handler
- ///
- /// Console control code
- /// true if interrupt was handled, false otherwise
- private static bool ConsoleHandler(ConsoleInterrupt.CtrlTypes ctrl)
- {
- // Only handle ctrl+c
- if (ctrl != ConsoleInterrupt.CtrlTypes.CtrlCEvent)
- {
- return false;
- }
-
- // Detach this handler
- ConsoleInterrupt.SetConsoleCtrlHandler(ConsoleHandler, false);
-
- // Cancel running tasks
- s_cancellationTokenSource.Cancel();
-
- // Yes, we handled the interrupt
- return true;
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Cli/Commands/PollCommand.cs b/PTI.Rs232Validator.Cli/Commands/PollCommand.cs
new file mode 100644
index 0000000..1ccbed1
--- /dev/null
+++ b/PTI.Rs232Validator.Cli/Commands/PollCommand.cs
@@ -0,0 +1,101 @@
+using PTI.Rs232Validator.Cli.Utility;
+using Spectre.Console.Cli;
+using System;
+using System.Threading;
+
+namespace PTI.Rs232Validator.Cli.Commands;
+
+///
+/// A command to poll a bill acceptor.
+///
+public class PollCommand : Command
+{
+ ///
+ /// The settings for .
+ ///
+ public class Settings : CommandSettings
+ {
+ [CommandArgument(0, "")]
+ public string PortName { get; init; } = string.Empty;
+
+ [CommandArgument(1, "")]
+ public byte BillTypeToReturn { get; init; }
+
+ [CommandOption("--detect-barcodes")]
+ public bool ShouldDetectBarcodes { get; init; }
+ }
+
+ ///
+ public override int Execute(CommandContext context, Settings settings)
+ {
+ var commandLogger = Factory.CreateMultiLogger();
+ using var billValidator = Factory.CreateBillValidator(settings.PortName);
+
+ billValidator.OnStateChanged += (_, state) =>
+ {
+ commandLogger.LogInfo($"The state has changed from {state.OldState} to {state.NewState}");
+ };
+
+ billValidator.OnEventReported += (_, evt) =>
+ {
+ commandLogger.LogInfo($"Received the following event(s): {evt}");
+ };
+
+ billValidator.OnCashboxAttached += (_, _) => { commandLogger.LogInfo("The cashbox was attached."); };
+
+ billValidator.OnCashboxRemoved += (_, _) => { commandLogger.LogInfo("The cashbox was removed."); };
+
+ billValidator.OnBillStacked += (_, billType) =>
+ {
+ commandLogger.LogInfo($"A bill of type {billType} was stacked.");
+ };
+
+ billValidator.OnBillEscrowed += (_, billType) =>
+ {
+ if (billType == settings.BillTypeToReturn)
+ {
+ commandLogger.LogInfo($"Sent a request to return of a bill of type {billType}.");
+ billValidator.ReturnBill();
+ return;
+ }
+
+ commandLogger.LogInfo($"Sent a request to stack a bill of type {billType}.");
+ billValidator.StackBill();
+ };
+
+ billValidator.OnBarcodeDetected += (_, barcode) =>
+ {
+ commandLogger.LogInfo($"Detected a barcode: {barcode}");
+ };
+
+ billValidator.OnConnectionLost += (_, _) => { commandLogger.LogError("Lost connection to the acceptor."); };
+
+ billValidator.Configuration.ShouldEscrow = true;
+ billValidator.Configuration.ShouldDetectBarcodes = settings.ShouldDetectBarcodes;
+
+ if (!billValidator.StartPollingLoop())
+ {
+ commandLogger.LogError("Failed to start the polling loop.");
+ return 1;
+ }
+
+ var cancelRequested = false;
+ Console.CancelKeyPress += (_, _) => { cancelRequested = true; };
+
+ commandLogger.LogInfo("Now polling the acceptor. Press CTRL+C to exit.");
+ while (true)
+ {
+ if (billValidator.IsConnectionPresent && !cancelRequested)
+ {
+ Thread.Sleep(billValidator.Configuration.PollingPeriod);
+ continue;
+ }
+
+ commandLogger.LogInfo("The acceptor is no longer connected. Exiting now.");
+ billValidator.StopPollingLoop();
+ break;
+ }
+
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Cli/Commands/SendTelemetryRequestCommand.cs b/PTI.Rs232Validator.Cli/Commands/SendTelemetryRequestCommand.cs
new file mode 100644
index 0000000..d714626
--- /dev/null
+++ b/PTI.Rs232Validator.Cli/Commands/SendTelemetryRequestCommand.cs
@@ -0,0 +1,207 @@
+using PTI.Rs232Validator.Cli.Utility;
+using PTI.Rs232Validator.Messages.Commands;
+using Spectre.Console.Cli;
+using System.ComponentModel;
+using System.Linq;
+
+namespace PTI.Rs232Validator.Cli.Commands;
+
+///
+/// A command to send a telemetry request to a bill acceptor.
+///
+public class SendTelemetryRequestCommand : Command
+{
+ ///
+ /// The settings for .
+ ///
+ public class Settings : CommandSettings
+ {
+ [CommandArgument(0, "")]
+ public string PortName { get; init; } = string.Empty;
+
+ [CommandArgument(1, "")]
+ [TypeConverter(typeof(ByteConverter))]
+ public TelemetryCommand TelemetryCommand { get; init; }
+
+ [CommandArgument(2, "[arguments]")]
+ public string[] Arguments { get; init; } = [];
+ }
+
+ ///
+ public override int Execute(CommandContext context, Settings settings)
+ {
+ var commandLogger = Factory.CreateMultiLogger();
+ using var billValidator = Factory.CreateBillValidator(settings.PortName);
+
+ switch (settings.TelemetryCommand)
+ {
+ case TelemetryCommand.Ping:
+ {
+ var responseMessage = billValidator.PingAsync().Result;
+ if (responseMessage.IsValid)
+ {
+ commandLogger.LogInfo("Successfully pinged the acceptor.");
+ }
+ else
+ {
+ commandLogger.LogError("Failed to ping the acceptor.");
+ }
+
+ break;
+ }
+
+ case TelemetryCommand.GetSerialNumber:
+ {
+ var responseMessage = billValidator.GetSerialNumberAsync().Result;
+ if (responseMessage is { IsValid: true, SerialNumber.Length: > 0 })
+ {
+ commandLogger.LogInfo($"The serial number is: {responseMessage.SerialNumber}");
+ }
+ else if (responseMessage is { IsValid: true, SerialNumber.Length: 0 })
+ {
+ commandLogger.LogInfo("The was not assigned a serial number.");
+ }
+ else
+ {
+ commandLogger.LogError("Failed to get the serial number.");
+ }
+
+ break;
+ }
+
+ case TelemetryCommand.GetCashboxMetrics:
+ {
+ var responseMessage = billValidator.GetCashboxMetrics().Result;
+ if (responseMessage.IsValid)
+ {
+ commandLogger.LogInfo($"The cashbox metrics are as follows — {responseMessage}");
+ }
+ else
+ {
+ commandLogger.LogError("Failed to get the cashbox metrics.");
+ }
+
+ break;
+ }
+
+ case TelemetryCommand.ClearCashboxCount:
+ {
+ var responseMessage = billValidator.ClearCashboxCount().Result;
+ if (responseMessage.IsValid)
+ {
+ commandLogger.LogInfo("Successfully cleared the cashbox count.");
+ }
+ else
+ {
+ commandLogger.LogError("Failed to clear the cashbox count.");
+ }
+
+ break;
+ }
+
+ case TelemetryCommand.GetUnitMetrics:
+ {
+ var responseMessage = billValidator.GetUnitMetrics().Result;
+ if (responseMessage.IsValid)
+ {
+ commandLogger.LogInfo($"The unit metrics are as follows — {responseMessage}");
+ }
+ else
+ {
+ commandLogger.LogError("Failed to get the unit metrics.");
+ }
+
+ break;
+ }
+
+ case TelemetryCommand.GetServiceUsageCounters:
+ {
+ var responseMessage = billValidator.GetServiceUsageCounters().Result;
+ if (responseMessage.IsValid)
+ {
+ commandLogger.LogInfo($"The service usage counters are as follows — {responseMessage}");
+ }
+ else
+ {
+ commandLogger.LogError("Failed to get the service usage counters.");
+ }
+
+ break;
+ }
+
+ case TelemetryCommand.GetServiceFlags:
+ {
+ var responseMessage = billValidator.GetServiceFlags().Result;
+ if (responseMessage.IsValid)
+ {
+ commandLogger.LogInfo($"The service flags are as follows — {responseMessage}");
+ }
+ else
+ {
+ commandLogger.LogError("Failed to get the service flags.");
+ }
+
+ break;
+ }
+
+ case TelemetryCommand.ClearServiceFlags:
+ {
+ var indexString = settings.Arguments.FirstOrDefault();
+ if (indexString is null || !byte.TryParse(indexString, out var index))
+ {
+ commandLogger.LogError("The index of the service flag to clear is required.");
+ return 1;
+ }
+
+ var correctableComponent = (CorrectableComponent)index;
+ var responseMessage = billValidator.ClearServiceFlags(correctableComponent).Result;
+ if (responseMessage.IsValid)
+ {
+ commandLogger.LogInfo($"Successfully cleared the service flag for index {index}.");
+ }
+ else
+ {
+ commandLogger.LogError($"Failed to clear the service flag for index {index}.");
+ }
+
+ break;
+ }
+
+ case TelemetryCommand.GetServiceInfo:
+ {
+ var responseMessage = billValidator.GetServiceInfo().Result;
+ if (responseMessage.IsValid)
+ {
+ commandLogger.LogInfo($"The service info is as follows — {responseMessage}");
+ }
+ else
+ {
+ commandLogger.LogError("Failed to get the service info.");
+ }
+
+ break;
+ }
+
+ case TelemetryCommand.GetFirmwareMetrics:
+ {
+ var responseMessage = billValidator.GetFirmwareMetrics().Result;
+ if (responseMessage.IsValid)
+ {
+ commandLogger.LogInfo($"The firmware metrics are as follows — {responseMessage}");
+ }
+ else
+ {
+ commandLogger.LogError("Failed to get the firmware metrics.");
+ }
+
+ break;
+ }
+
+ default:
+ commandLogger.LogError("The telemetry command is not supported.");
+ return 1;
+ }
+
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Cli/Loggers/ConsoleLogger.cs b/PTI.Rs232Validator.Cli/Loggers/ConsoleLogger.cs
new file mode 100644
index 0000000..53b2529
--- /dev/null
+++ b/PTI.Rs232Validator.Cli/Loggers/ConsoleLogger.cs
@@ -0,0 +1,34 @@
+using PTI.Rs232Validator.Loggers;
+using System;
+
+namespace PTI.Rs232Validator.Cli.Loggers;
+
+///
+/// An implementation of that logs colored messages to the console.
+///
+public class ConsoleLogger : NamedLogger where T : class
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// .
+ public ConsoleLogger(LogLevel minLogLevel) : base(minLogLevel)
+ {
+ }
+
+ ///
+ protected override void Log(string name, LogLevel logLevel, string format, params object[] args)
+ {
+ Console.ForegroundColor = logLevel switch
+ {
+ LogLevel.Trace => ConsoleColor.DarkGray,
+ LogLevel.Debug => ConsoleColor.Gray,
+ LogLevel.Info => ConsoleColor.White,
+ LogLevel.Error => ConsoleColor.Red,
+ _ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null)
+ };
+
+ Console.WriteLine($"[{name}] [{logLevel}] {string.Format(format, args)}");
+ Console.ForegroundColor = ConsoleColor.White;
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Cli/Loggers/FileLogger.cs b/PTI.Rs232Validator.Cli/Loggers/FileLogger.cs
new file mode 100644
index 0000000..690a79e
--- /dev/null
+++ b/PTI.Rs232Validator.Cli/Loggers/FileLogger.cs
@@ -0,0 +1,84 @@
+using PTI.Rs232Validator.Loggers;
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace PTI.Rs232Validator.Cli.Loggers;
+
+internal static class FileLogger
+{
+ public static readonly Dictionary> LogFilePathMap = new();
+}
+
+///
+/// An implementation of that logs messages to a file.
+///
+public class FileLogger : NamedLogger, IDisposable where T : class
+{
+ private readonly string _logFilePath;
+ private readonly StreamWriter _streamWriter;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The file to write log messages to.
+ /// .
+ ///
+ /// The access requested is not permitted by the operating system for the specified path,
+ /// such as when access is Write or ReadWrite and the file or directory is set for read-only access.
+ ///
+ ///
+ /// If the specified file does not exist, it will be created.
+ /// If the specified file exists, it will be appended to.
+ ///
+ public FileLogger(string logFilePath, LogLevel minLogLevel) : base(minLogLevel)
+ {
+ _logFilePath = logFilePath;
+ if (FileLogger.LogFilePathMap.TryGetValue(_logFilePath, out var value))
+ {
+ FileLogger.LogFilePathMap[_logFilePath] =
+ new Tuple((byte)(value.Item1 + 1), value.Item2);
+ _streamWriter = FileLogger.LogFilePathMap[_logFilePath].Item2;
+ }
+ else
+ {
+ var stream = new FileStream(_logFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
+ _streamWriter = new StreamWriter(stream)
+ {
+ AutoFlush = true
+ };
+ FileLogger.LogFilePathMap.Add(_logFilePath, new Tuple(1, _streamWriter));
+ }
+ }
+
+ ///
+ protected override void Log(string name, LogLevel logLevel, string format, params object[] args)
+ {
+ var line = $"[{name}] [{logLevel}] {string.Format(format, args)}";
+ _streamWriter.WriteLine(line);
+ }
+
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+
+ if (!FileLogger.LogFilePathMap.TryGetValue(_logFilePath, out var value))
+ {
+ return;
+ }
+
+ if (value.Item1 == 1)
+ {
+ FileLogger.LogFilePathMap.Remove(_logFilePath);
+ _streamWriter.Flush();
+ ;
+ _streamWriter.Dispose();
+ }
+ else
+ {
+ FileLogger.LogFilePathMap[_logFilePath] =
+ new Tuple((byte)(value.Item1 - 1), value.Item2);
+ }
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Cli/Loggers/MultiLogger.cs b/PTI.Rs232Validator.Cli/Loggers/MultiLogger.cs
new file mode 100644
index 0000000..d6095b9
--- /dev/null
+++ b/PTI.Rs232Validator.Cli/Loggers/MultiLogger.cs
@@ -0,0 +1,53 @@
+using PTI.Rs232Validator.Loggers;
+using System.Collections.Generic;
+
+namespace PTI.Rs232Validator.Cli.Loggers;
+
+///
+/// An implementation of that logs messages to multiple loggers.
+///
+public class MultiLogger : ILogger
+{
+ private readonly IEnumerable _loggers;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The loggers to log messages to.
+ public MultiLogger(IEnumerable loggers)
+ {
+ _loggers = loggers;
+ }
+
+ public void LogTrace(string format, params object[] args)
+ {
+ foreach (var logger in _loggers)
+ {
+ logger.LogTrace(format, args);
+ }
+ }
+
+ public void LogDebug(string format, params object[] args)
+ {
+ foreach (var logger in _loggers)
+ {
+ logger.LogDebug(format, args);
+ }
+ }
+
+ public void LogInfo(string format, params object[] args)
+ {
+ foreach (var logger in _loggers)
+ {
+ logger.LogInfo(format, args);
+ }
+ }
+
+ public void LogError(string format, params object[] args)
+ {
+ foreach (var logger in _loggers)
+ {
+ logger.LogError(format, args);
+ }
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.CLI/PTI.Rs232Validator.CLI.csproj b/PTI.Rs232Validator.Cli/PTI.Rs232Validator.Cli.csproj
similarity index 59%
rename from PTI.Rs232Validator.CLI/PTI.Rs232Validator.CLI.csproj
rename to PTI.Rs232Validator.Cli/PTI.Rs232Validator.Cli.csproj
index a5f07d2..932ac18 100644
--- a/PTI.Rs232Validator.CLI/PTI.Rs232Validator.CLI.csproj
+++ b/PTI.Rs232Validator.Cli/PTI.Rs232Validator.Cli.csproj
@@ -2,13 +2,17 @@
Exe
- net461;net472;netcoreapp3.1
latest
+ net8.0
+ enable
-
+
+
+
+
diff --git a/PTI.Rs232Validator.Cli/Program.cs b/PTI.Rs232Validator.Cli/Program.cs
new file mode 100644
index 0000000..f1edb56
--- /dev/null
+++ b/PTI.Rs232Validator.Cli/Program.cs
@@ -0,0 +1,25 @@
+using PTI.Rs232Validator.Cli.Commands;
+using PTI.Rs232Validator.Messages.Commands;
+using Spectre.Console.Cli;
+
+var app = new CommandApp();
+app.Configure(configuration =>
+{
+ configuration.Settings.TrimTrailingPeriod = false;
+ configuration.Settings.ValidateExamples = true;
+
+ configuration.AddCommand("poll")
+ .WithDescription("Polls a bill acceptor.")
+ .WithExample(["poll", "COM1"]);
+
+ configuration.AddCommand("send-telemetry")
+ .WithDescription("Sends a telemetry request to a bill acceptor.")
+ .WithExample(["send-telemetry", "COM1", ((byte)TelemetryCommand.Ping).ToString()])
+ .WithExample(["send-telemetry", "COM1", ((byte)TelemetryCommand.ClearServiceFlags).ToString(), "1"]);
+
+ configuration.AddCommand("send-extended")
+ .WithDescription("Sends an extended request to a bill acceptor.")
+ .WithExample(["send-extended", "COM1", ((byte)ExtendedCommand.BarcodeDetected).ToString()]);
+});
+
+return app.Run(args);
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Cli/Utility/Factory.cs b/PTI.Rs232Validator.Cli/Utility/Factory.cs
new file mode 100644
index 0000000..6b75b67
--- /dev/null
+++ b/PTI.Rs232Validator.Cli/Utility/Factory.cs
@@ -0,0 +1,52 @@
+using PTI.Rs232Validator.Cli.Loggers;
+using PTI.Rs232Validator.Loggers;
+using PTI.Rs232Validator.SerialProviders;
+using System;
+using System.IO;
+using BillValidator = PTI.Rs232Validator.BillValidators.BillValidator;
+
+namespace PTI.Rs232Validator.Cli.Utility;
+
+///
+/// A factory for creating objects.
+///
+public static class Factory
+{
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The class to log for.
+ /// A new instance of .
+ public static MultiLogger CreateMultiLogger() where T : class
+ {
+ var currentDirectory = Environment.CurrentDirectory;
+ var traceLogFilePath = Path.Combine(currentDirectory, "trace.log");
+ var debugLogFilePath = Path.Combine(currentDirectory, "debug.log");
+ var infoLogFilePath = Path.Combine(currentDirectory, "info.log");
+ var errorLogFilePath = Path.Combine(currentDirectory, "error.log");
+
+ return new MultiLogger([
+ new FileLogger(traceLogFilePath, LogLevel.Trace),
+ new FileLogger(debugLogFilePath, LogLevel.Debug),
+ new FileLogger(infoLogFilePath, LogLevel.Info),
+ new FileLogger(errorLogFilePath, LogLevel.Error),
+
+ new ConsoleLogger(LogLevel.Info)
+ ]);
+ }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The name of the serial port to use.
+ /// A new instance of .
+ public static BillValidator CreateBillValidator(string serialPortName)
+ {
+ var serialPortProviderLogger = CreateMultiLogger();
+ var serialPortProvider = SerialProvider.CreateUsbSerialProvider(serialPortProviderLogger, serialPortName);
+
+ var billValidatorLogger = CreateMultiLogger();
+ var configuration = new Rs232Configuration();
+ return new BillValidator(billValidatorLogger, serialPortProvider, configuration);
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Emulator/ApexDeviceMessage.cs b/PTI.Rs232Validator.Emulator/ApexDeviceMessage.cs
deleted file mode 100644
index b93cb91..0000000
--- a/PTI.Rs232Validator.Emulator/ApexDeviceMessage.cs
+++ /dev/null
@@ -1,163 +0,0 @@
-namespace PTI.Rs232Validator.Emulator
-{
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using Messages;
-
- ///
- /// Apex RS-232 poll response message
- ///
- internal class ApexDeviceMessage : Rs232BaseMessage
- {
- private const int AckByte = 2;
- private const int CashBoxByte = 4;
- private const int CashBoxBit = 4;
- private const int CreditByte = 5;
-
- ///
- /// Standard device message
- ///
- private static readonly byte[] BaseMessage =
- {
- 0x02, 0x0B, 0x20, 0x00, 0x00, 0x00, 0x00, 0x12, 0x13, 0x03, 0x3B
- };
-
- ///
- /// State map keys by (byte, bit) into the payload.
- /// e.g.
- /// (1,2) => byte 1 bit 2 of payload
- ///
- private static readonly Dictionary StateMap =
- new Dictionary
- {
- {Rs232State.Idling, (3, 0)},
- {Rs232State.Accepting, (3, 1)},
- {Rs232State.Escrowed, (3, 2)},
- {Rs232State.Stacking, (3, 3)},
- {Rs232State.Returning, (3, 5)},
- {Rs232State.BillJammed, (4, 2)},
- {Rs232State.StackerFull, (4, 3)},
- {Rs232State.Failure, (5, 2)}
- };
-
- ///
- /// Event map keys by (byte, bit) into the payload.
- /// e.g.
- /// (1,2) => byte 1 bit 2 of payload
- ///
- private static readonly Dictionary EventMap =
- new Dictionary
- {
- {Rs232Event.Stacked, (3, 4)},
- {Rs232Event.Returned, (3, 6)},
- {Rs232Event.Cheated, (4, 0)},
- {Rs232Event.BillRejected, (4, 1)},
- {Rs232Event.PowerUp, (5, 0)},
- {Rs232Event.InvalidCommand, (5, 1)}
- };
-
- ///
- public ApexDeviceMessage() : base(BaseMessage)
- {
- RawMessage[RawMessage.Length - 1] = CalculateChecksum();
- }
-
- ///
- /// Sets ack bit and recalculates checksum
- ///
- /// True to set ack bit
- /// this
- public ApexDeviceMessage SetAck(bool ack)
- {
- RawMessage[AckByte] = (byte) (ack ? 0x21 : 0x20);
- RawMessage[RawMessage.Length - 1] = CalculateChecksum();
- return this;
- }
-
- ///
- /// Sets state bit and recalculates checksum
- ///
- /// Which state to set
- /// this
- public ApexDeviceMessage SetState(Rs232State state)
- {
- if (state == Rs232State.None)
- {
- return this;
- }
-
- var (index, bit) = StateMap[state];
-
- // Clear current state
- RawMessage[index] = 0;
- RawMessage[index] = SetBit(bit, RawMessage[index]);
- RawMessage[RawMessage.Length - 1] = CalculateChecksum();
-
- return this;
- }
-
- ///
- /// Sets event bit(s) and recalculates checksum
- ///
- /// Event(s) to set
- /// this
- public ApexDeviceMessage SetEvents(Rs232Event events)
- {
- foreach (var evt in Enum.GetValues(typeof(Rs232Event)).Cast())
- {
- if (evt == Rs232Event.None)
- {
- continue;
- }
-
- var (index, bit) = EventMap[evt];
-
- RawMessage[index] = events.HasFlag(evt)
- ? SetBit(bit, RawMessage[index])
- : ClearBit(bit, RawMessage[index]);
- }
-
- RawMessage[RawMessage.Length - 1] = CalculateChecksum();
-
- return this;
- }
-
- ///
- /// Sets cash box present state recalculates checksum
- ///
- /// True to set cash box present
- /// this
- public ApexDeviceMessage SetCashBoxState(bool present)
- {
- RawMessage[CashBoxByte] = present
- ? SetBit(CashBoxBit, RawMessage[CashBoxByte])
- : ClearBit(CashBoxBit, RawMessage[CashBoxByte]);
-
- RawMessage[RawMessage.Length - 1] = CalculateChecksum();
-
- return this;
- }
-
- ///
- /// Sets credit index to report. Null for no credit to report.
- ///
- /// Credit index in range (0,7)
- /// this
- /// Thrown if credit is greater than 7
- public ApexDeviceMessage SetCredit(int credit)
- {
- if (credit < 0 || credit > 7)
- {
- throw new ArgumentException($"Invalid credit value: {nameof(credit)}. Must in range (0,7).");
- }
-
- credit = (credit << 3) & 0b00111000;
- RawMessage[CreditByte] = (byte) credit;
-
- RawMessage[RawMessage.Length - 1] = CalculateChecksum();
-
- return this;
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Emulator/ApexEmulator.cs b/PTI.Rs232Validator.Emulator/ApexEmulator.cs
deleted file mode 100644
index 1ea91c2..0000000
--- a/PTI.Rs232Validator.Emulator/ApexEmulator.cs
+++ /dev/null
@@ -1,172 +0,0 @@
-namespace PTI.Rs232Validator.Emulator
-{
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using Providers;
-
- ///
- /// Apex RS-232 mode emulator
- /// This creates a serial provider that directly attaches to a
- /// RS-232 state machine. The state of this instance can be directly modified
- /// and the polling loop will automatically build the poll response based on
- /// the configured state.
- ///
- public class ApexEmulator : ISerialProvider, IEmulator
- {
- private byte? _credit;
- private ApexDeviceMessage _nextResponse;
- private readonly IList _issuedCredits;
-
- ///
- /// Create a new emulator in the PowerUp state
- ///
- public ApexEmulator()
- {
- CurrentState = Rs232State.None;
- CurrentEvents = Rs232Event.PowerUp;
-
- CashBoxPresent = true;
- _issuedCredits = new List();
- }
-
- ///
- public event EventHandler OnPollResponseSent;
-
- ///
- public int TotalPollCount { get; private set; }
-
- ///
- public bool CashBoxPresent { get; set; }
-
- ///
- public Rs232State CurrentState { get; set; }
-
- ///
- public Rs232Event CurrentEvents { get; set; }
-
- ///
- /// List of credits issues from this emulator
- ///
- public IEnumerable IssueCredits => _issuedCredits;
-
- ///
- public byte? Credit
- {
- get => _credit;
- set
- {
- if (value.HasValue)
- {
- var v = value.Value;
- if (v > 7)
- {
- throw new ArgumentException("Credit index must be in range (0,7)");
- }
- }
-
- _credit = value;
- }
- }
-
- ///
- ///
- /// Fake port is always open
- ///
- public bool IsOpen => true;
-
- ///
- public ILogger Logger { get; set; }
-
- ///
- /// Fake port can always be opened
- ///
- public bool TryOpen()
- {
- return true;
- }
-
- ///
- public void Close()
- {
- // Nothing to close
- }
-
- ///
- public byte[] Read(int count)
- {
- // Response is build when a host message is received via Write
- if (_nextResponse is null)
- {
- return null;
- }
-
- var payload = _nextResponse.Serialize();
-
- // Return no more than what's available or requested
- var readLen = Math.Min(count, payload.Length);
-
- var slice = new byte[readLen];
- Array.Copy(payload, slice, slice.Length);
-
- return slice;
- }
-
- ///
- public void Write(byte[] data)
- {
- // Handle polling request
- _nextResponse = PrepareNextResponse(data);
-
- // Handle post-processing
- ++TotalPollCount;
- OnPollResponseSent?.Invoke(this, EventArgs.Empty);
- }
-
- ///
- public void Dispose()
- {
- // Nothing to do
- }
-
- ///
- /// Parse host data to build a polling response
- ///
- /// Message data received from host
- private ApexDeviceMessage PrepareNextResponse(byte[] dataFromHost)
- {
- var response = new ApexDeviceMessage();
-
- var hostMessage = new ApexHostMessage(dataFromHost);
-
- // Malformed host message
- if (!hostMessage.IsHostMessage)
- {
- _nextResponse.SetEvents(Rs232Event.InvalidCommand);
- return response;
- }
-
- // Update the ACK state to match the host
- response.SetAck(hostMessage.Ack);
-
- // Report the current state
- response.SetState(CurrentState);
-
- // Report any events then clear them
- response.SetEvents(CurrentEvents);
-
- // Set the cash box presence
- response.SetCashBoxState(CashBoxPresent);
-
- // Set credit bits if specified
- if (Credit.HasValue)
- {
- _issuedCredits.Add(Credit.Value);
- response.SetCredit(Credit.Value);
- Credit = null;
- }
-
- return response;
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Emulator/ApexHostMessage.cs b/PTI.Rs232Validator.Emulator/ApexHostMessage.cs
deleted file mode 100644
index 57fcd58..0000000
--- a/PTI.Rs232Validator.Emulator/ApexHostMessage.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace PTI.Rs232Validator.Emulator
-{
- using Messages;
-
- ///
- /// Wraps raw host message data
- ///
- internal class ApexHostMessage : Rs232BaseMessage
- {
- ///
- public ApexHostMessage(byte[] data) : base(data)
- {
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Emulator/EmulationRunner.cs b/PTI.Rs232Validator.Emulator/EmulationRunner.cs
deleted file mode 100644
index aa3961f..0000000
--- a/PTI.Rs232Validator.Emulator/EmulationRunner.cs
+++ /dev/null
@@ -1,203 +0,0 @@
-namespace PTI.Rs232Validator.Emulator
-{
- using System;
- using System.Threading;
- using Providers;
-
- ///
- /// Helpers for running an emulator in deterministic sequences
- ///
- public class EmulationRunner where T : class, IEmulator, ISerialProvider, new()
- {
- private readonly T _emulator;
- private readonly CancellationToken _token;
-
- ///
- /// Create a new emulator with the specified polling period
- ///
- /// Main loop period
- /// Optional logger
- public EmulationRunner(TimeSpan pollingPeriod, ILogger logger = null) : this(pollingPeriod,
- CancellationToken.None, logger)
- {
- }
-
- ///
- /// Create a new emulator with the specified polling period
- ///
- /// Main loop period
- /// Cancellation token
- /// Optional logger
- public EmulationRunner(TimeSpan pollingPeriod, CancellationToken token, ILogger logger = null)
- {
- _emulator = new T();
- _token = token;
- Config = new Rs232Config(_emulator)
- {
- Logger = logger,
- PollingPeriod = pollingPeriod
- };
- }
-
- ///
- /// Device configuration
- ///
- public Rs232Config Config { get; }
-
- ///
- /// Runs polling loop this many times at a minimum.
- /// Due to timing limitations, this is a minimum run count
- /// and their may be 1 or more extra loops executed.
- ///
- /// Loops to run
- ///
- public T RunIdleFor(int loops)
- {
- // Setup a semaphore to wait for this many polling loops
- var sem = new EmulationLoopSemaphore(_token)
- {
- SignalAt = loops
- };
- _emulator.OnPollResponseSent += sem.LoopCallback;
- sem.OnLoopCalled += (sender, args) =>
- {
- // Always be idle
- _emulator.CurrentState = Rs232State.Idling;
- };
-
- // Create a new validator so we have perfect control of the state
- var validator = new ApexValidator(Config);
- validator.StartPollingLoop();
-
- // Wait for signal
- sem.Gate.WaitOne();
-
- // Cleanup
- validator.StopPollingLoop();
- _emulator.OnPollResponseSent -= sem.LoopCallback;
-
- return _emulator;
- }
-
- ///
- /// Every n loops, issue the specified credit value
- ///
- /// Count of loops between credits
- /// Run this many times. -1 to loop forever.
- /// Credits to issue
- public T CreditEveryNLoops(int loops, int count, params byte[] creditIndices)
- {
- // Create a new validator so we have perfect control of the state
- var validator = new ApexValidator(Config);
-
- // Setup a semaphore to wait for this many polling loops
- var sem = new EmulationLoopSemaphore(_token)
- {
- SignalAt = loops * count
- };
-
- // Start in idling state
- _emulator.CurrentState = Rs232State.Idling;
- _emulator.CurrentEvents = Rs232Event.None;
-
- var next = 0;
- _emulator.OnPollResponseSent += sem.LoopCallback;
- sem.OnLoopCalled += (sender, args) =>
- {
- switch (_emulator.CurrentState)
- {
- case Rs232State.None:
- _emulator.CurrentState = Rs232State.Idling;
- _emulator.CurrentEvents = Rs232Event.None;
- break;
-
- case Rs232State.Idling:
- if (sem.Iterations % loops == 0)
- {
- _emulator.CurrentState = Rs232State.Accepting;
- _emulator.CurrentEvents = Rs232Event.None;
- }
-
- break;
-
- case Rs232State.Accepting:
- _emulator.CurrentState = Rs232State.Stacking;
- _emulator.CurrentEvents = Rs232Event.None;
- break;
-
- case Rs232State.Stacking:
- _emulator.CurrentState = Rs232State.Idling;
- _emulator.CurrentEvents = Rs232Event.Stacked;
- _emulator.Credit = creditIndices[next++ % creditIndices.Length];
- break;
- }
- };
-
- validator.StartPollingLoop();
-
- // Wait for signal
- sem.Gate.WaitOne();
-
- // Cleanup
- validator.StopPollingLoop();
- _emulator.OnPollResponseSent -= sem.LoopCallback;
-
- return _emulator;
- }
- }
-
- ///
- /// Acts as a semaphore by signalling an event once
- /// the specified count of iterations have been completed.
- ///
- internal class EmulationLoopSemaphore
- {
- private readonly CancellationToken _token;
-
- public EmulationLoopSemaphore(CancellationToken token)
- {
- _token = token;
- }
-
- ///
- /// Signal after this many iterations
- ///
- public int SignalAt { get; set; }
-
- ///
- /// Current loop iteration count
- /// A negative value will loop forever
- ///
- public int Iterations { get; private set; }
-
- ///
- /// Semaphore signalled upon completion
- ///
- public AutoResetEvent Gate { get; } = new AutoResetEvent(false);
-
- ///
- /// Raised when is called
- ///
- public event EventHandler OnLoopCalled;
-
- ///
- /// Handles emulator loop-complete callback
- ///
- public void LoopCallback(object sender, EventArgs e)
- {
- OnLoopCalled?.Invoke(this, EventArgs.Empty);
-
- ++Iterations;
-
- if (SignalAt > 0 && Iterations >= SignalAt)
- {
- Gate.Set();
- }
-
- if (_token.IsCancellationRequested)
- {
- Gate.Set();
- }
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Emulator/IEmulator.cs b/PTI.Rs232Validator.Emulator/IEmulator.cs
deleted file mode 100644
index 2b53725..0000000
--- a/PTI.Rs232Validator.Emulator/IEmulator.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-namespace PTI.Rs232Validator.Emulator
-{
- using System;
-
- public interface IEmulator
- {
- ///
- /// Total count of complete polling transactions
- /// One transaction is host->device
- ///
- int TotalPollCount { get; }
-
- ///
- /// When true, the cash box is reported as present
- ///
- ///
- /// The cash box state is always reported. For stackerless devices
- /// this property is always true.
- ///
- bool CashBoxPresent { get; set; }
-
- ///
- /// Current acceptor state
- /// Setting this property has no effect until the next polling message
- /// is received.
- ///
- /// State persists across polling messages
- Rs232State CurrentState { get; set; }
-
- ///
- /// Current acceptor events
- /// Setting this property has no effect until the next polling message
- /// is received.
- ///
- /// Events are one-shot meaning that they are automatically cleared once sent
- Rs232Event CurrentEvents { get; set; }
-
- ///
- /// Next credit to report
- /// Value must be in range (0,7)
- ///
- /// Thrown when value is greater than 7
- byte? Credit { get; set; }
-
- ///
- /// Raised when this instance completes a poll response
- ///
- event EventHandler OnPollResponseSent;
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Emulator/PTI.Rs232Validator.Emulator.csproj b/PTI.Rs232Validator.Emulator/PTI.Rs232Validator.Emulator.csproj
deleted file mode 100644
index 020b3c6..0000000
--- a/PTI.Rs232Validator.Emulator/PTI.Rs232Validator.Emulator.csproj
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
- net461;net472;netcoreapp3.1
- latest
-
-
-
-
-
-
-
-
-
-
-
diff --git a/PTI.Rs232Validator.Test/GlobalNamespaces.cs b/PTI.Rs232Validator.Test/GlobalNamespaces.cs
new file mode 100644
index 0000000..9a28bd8
--- /dev/null
+++ b/PTI.Rs232Validator.Test/GlobalNamespaces.cs
@@ -0,0 +1 @@
+global using NUnit.Framework;
diff --git a/PTI.Rs232Validator.Tests/PTI.Rs232Validator.Tests.csproj b/PTI.Rs232Validator.Test/PTI.Rs232Validator.Test.csproj
similarity index 53%
rename from PTI.Rs232Validator.Tests/PTI.Rs232Validator.Tests.csproj
rename to PTI.Rs232Validator.Test/PTI.Rs232Validator.Test.csproj
index 6f2de35..ff652cf 100644
--- a/PTI.Rs232Validator.Tests/PTI.Rs232Validator.Tests.csproj
+++ b/PTI.Rs232Validator.Test/PTI.Rs232Validator.Test.csproj
@@ -2,18 +2,18 @@
false
- net461;net472;netcoreapp3.1
latest
+ net8.0
-
-
-
+
+
+
+
-
diff --git a/PTI.Rs232Validator.Test/Tests/BillValidatorTests.cs b/PTI.Rs232Validator.Test/Tests/BillValidatorTests.cs
new file mode 100644
index 0000000..2e7db50
--- /dev/null
+++ b/PTI.Rs232Validator.Test/Tests/BillValidatorTests.cs
@@ -0,0 +1,422 @@
+using Moq;
+using PTI.Rs232Validator.BillValidators;
+using PTI.Rs232Validator.Loggers;
+using PTI.Rs232Validator.Messages.Commands;
+using PTI.Rs232Validator.Messages.Requests;
+using PTI.Rs232Validator.Messages.Responses.Extended;
+using PTI.Rs232Validator.SerialProviders;
+using PTI.Rs232Validator.Test.Utility;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace PTI.Rs232Validator.Test.Tests;
+
+public class BillValidatorTests
+{
+ [Test]
+ public async Task AllCommunicationAttemptsAreReported()
+ {
+ var validResponsePayload = new byte[]
+ { 0x02, 0x0B, 0x21, 0b00000001, 0b00010000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x38 };
+ var invalidResponsePayload = new byte[]
+ { 0x02, 0x0B, 0x20, 0b00000000, 0b00010000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x38 };
+
+ var mockSerialProvider = new Mock();
+ mockSerialProvider
+ .Setup(sp => sp.TryOpen())
+ .Returns(true);
+ mockSerialProvider
+ .SetupSequence(sp => sp.Read(It.IsAny()))
+ .ReturnsValidPollResponses()
+ .ReturnsResponse(validResponsePayload)
+ .ReturnsResponse(invalidResponsePayload)
+ .ReturnsEmptyResponses();
+
+ var rs232Configuration = new Rs232Configuration
+ {
+ PollingPeriod = TimeSpan.Zero
+ };
+ using var billValidator = new BillValidator(new NullLogger(), mockSerialProvider.Object, rs232Configuration);
+
+ var communicationAttempts = 0;
+ var wasValidResponseReported = false;
+ var wasInvalidResponseReported = false;
+ billValidator.OnCommunicationAttempted += (_, args) =>
+ {
+ communicationAttempts++;
+ if (args.ResponseMessage.Payload.SequenceEqual(validResponsePayload))
+ {
+ wasValidResponseReported = true;
+ }
+ else if (args.ResponseMessage.Payload.SequenceEqual(invalidResponsePayload))
+ {
+ wasInvalidResponseReported = true;
+ }
+ };
+
+ var didPollingLoopStart = billValidator.StartPollingLoop();
+ Assert.That(didPollingLoopStart, Is.True);
+
+ while (billValidator.IsConnectionPresent)
+ {
+ await Task.Delay(rs232Configuration.PollingPeriod);
+ }
+
+ Assert.That(communicationAttempts,
+ Is.EqualTo(BillValidator.SuccessfulPollsRequiredToStartPollingLoop + 2 + 1));
+ Assert.That(wasValidResponseReported, Is.True);
+ Assert.That(wasInvalidResponseReported, Is.True);
+ }
+
+ [Test, TestCaseSource(typeof(Rs232Payloads), nameof(Rs232Payloads.PollResponsePayloadAndStatePairs))]
+ public async Task ANewStateEncodedInAPollResponseIsReported(byte[] responsePayload, Rs232State expectedState)
+ {
+ var mockSerialProvider = new Mock();
+ mockSerialProvider
+ .Setup(sp => sp.TryOpen())
+ .Returns(true);
+ mockSerialProvider
+ .SetupSequence(sp => sp.Read(It.IsAny()))
+ .ReturnsValidPollResponses()
+ .ReturnsResponse(responsePayload)
+ .ReturnsEmptyResponses();
+
+ var rs232Configuration = new Rs232Configuration
+ {
+ PollingPeriod = TimeSpan.Zero
+ };
+ using var billValidator = new BillValidator(new NullLogger(), mockSerialProvider.Object, rs232Configuration);
+
+ var actualState = Rs232State.None;
+ billValidator.OnStateChanged += (_, args) => { actualState = args.NewState; };
+
+ var didPollingLoopStart = billValidator.StartPollingLoop();
+ Assert.That(didPollingLoopStart, Is.True);
+
+ while (billValidator.IsConnectionPresent)
+ {
+ await Task.Delay(rs232Configuration.PollingPeriod);
+ }
+
+ Assert.That(actualState, Is.EqualTo(expectedState));
+ }
+
+ [Test, TestCaseSource(typeof(Rs232Payloads), nameof(Rs232Payloads.PollResponsePayloadAndEventPairs))]
+ public async Task ASingleEventEncodedInAPollResponseMessageIsReported(byte[] responsePayload,
+ Rs232Event expectedEvent)
+ {
+ var mockSerialProvider = new Mock();
+ mockSerialProvider
+ .Setup(sp => sp.TryOpen())
+ .Returns(true);
+ mockSerialProvider
+ .SetupSequence(sp => sp.Read(It.IsAny()))
+ .ReturnsValidPollResponses()
+ .ReturnsResponse(responsePayload)
+ .ReturnsEmptyResponses();
+
+ var rs232Configuration = new Rs232Configuration
+ {
+ PollingPeriod = TimeSpan.Zero
+ };
+ using var billValidator = new BillValidator(new NullLogger(), mockSerialProvider.Object, rs232Configuration);
+
+ var actualEvent = Rs232Event.None;
+ billValidator.OnEventReported += (_, e) => { actualEvent = e; };
+
+ var didPollingLoopStart = billValidator.StartPollingLoop();
+ Assert.That(didPollingLoopStart, Is.True);
+
+ while (billValidator.IsConnectionPresent)
+ {
+ await Task.Delay(rs232Configuration.PollingPeriod);
+ }
+
+ Assert.That(actualEvent, Is.EqualTo(expectedEvent));
+ }
+
+ [Test]
+ public async Task MultipleEventsEncodedInAPollResponseMessageAreReported()
+ {
+ var expectedEvent = Rs232Event.None;
+ foreach (var value in Enum.GetValues(typeof(Rs232Event)))
+ {
+ expectedEvent |= (Rs232Event)value;
+ }
+
+ var mockSerialProvider = new Mock();
+ mockSerialProvider
+ .Setup(sp => sp.TryOpen())
+ .Returns(true);
+ mockSerialProvider
+ .SetupSequence(sp => sp.Read(It.IsAny()))
+ .ReturnsValidPollResponses()
+ .ReturnsResponse(Rs232Payloads.PollResponsePayloadWithEveryEvent)
+ .ReturnsEmptyResponses();
+
+ var rs232Configuration = new Rs232Configuration
+ {
+ PollingPeriod = TimeSpan.Zero
+ };
+ using var billValidator = new BillValidator(new NullLogger(), mockSerialProvider.Object, rs232Configuration);
+
+ var actualEvent = Rs232Event.None;
+ billValidator.OnEventReported += (_, e) => { actualEvent |= e; };
+
+ var didPollingLoopStart = billValidator.StartPollingLoop();
+ Assert.That(didPollingLoopStart, Is.True);
+
+ while (billValidator.IsConnectionPresent)
+ {
+ await Task.Delay(rs232Configuration.PollingPeriod);
+ }
+
+ Assert.That(actualEvent, Is.EqualTo(expectedEvent));
+ }
+
+ [Test, TestCaseSource(typeof(Rs232Payloads), nameof(Rs232Payloads.PollResponsePayloadAndCashboxPresencePairs))]
+ public async Task TheCashboxPresenceEncodedInAPollResponseMessageIsReported(byte[] responsePayload,
+ bool expectedIsCashboxPresent)
+ {
+ var mockSerialProvider = new Mock();
+ mockSerialProvider
+ .Setup(sp => sp.TryOpen())
+ .Returns(true);
+ mockSerialProvider
+ .SetupSequence(sp => sp.Read(It.IsAny()))
+ .ReturnsValidPollResponses()
+ .ReturnsResponse(responsePayload)
+ .ReturnsEmptyResponses();
+
+ var rs232Configuration = new Rs232Configuration
+ {
+ PollingPeriod = TimeSpan.Zero
+ };
+ using var billValidator = new BillValidator(new NullLogger(), mockSerialProvider.Object, rs232Configuration);
+
+ bool? wasCashboxAttached = null;
+ billValidator.OnCashboxAttached += (_, _) => { wasCashboxAttached = true; };
+ billValidator.OnCashboxRemoved += (_, _) => { wasCashboxAttached = false; };
+
+ var didPollingLoopStart = billValidator.StartPollingLoop();
+ Assert.That(didPollingLoopStart, Is.True);
+
+ while (billValidator.IsConnectionPresent)
+ {
+ await Task.Delay(rs232Configuration.PollingPeriod);
+ }
+
+ Assert.That(wasCashboxAttached, Is.EqualTo(expectedIsCashboxPresent));
+ }
+
+ [Test, TestCaseSource(typeof(Rs232Payloads), nameof(Rs232Payloads.PollResponsePayloadAndStackedBillPairs))]
+ public async Task AStackedBillEncodedInAPollResponseMessageIsReported(byte[] responsePayload, byte expectedBillType)
+ {
+ var mockSerialProvider = new Mock();
+ mockSerialProvider
+ .Setup(sp => sp.TryOpen())
+ .Returns(true);
+ mockSerialProvider
+ .SetupSequence(sp => sp.Read(It.IsAny()))
+ .ReturnsValidPollResponses()
+ .ReturnsResponse(responsePayload)
+ .ReturnsEmptyResponses();
+
+ var rs232Configuration = new Rs232Configuration
+ {
+ PollingPeriod = TimeSpan.Zero
+ };
+ using var billValidator = new BillValidator(new NullLogger(), mockSerialProvider.Object, rs232Configuration);
+
+ var actualBillType = 0;
+ billValidator.OnBillStacked += (_, billType) => { actualBillType = billType; };
+
+ var didPollingLoopStart = billValidator.StartPollingLoop();
+ Assert.That(didPollingLoopStart, Is.True);
+
+ while (billValidator.IsConnectionPresent)
+ {
+ await Task.Delay(rs232Configuration.PollingPeriod);
+ }
+
+ Assert.That(actualBillType, Is.EqualTo(expectedBillType));
+ }
+
+ [Test, TestCaseSource(typeof(Rs232Payloads), nameof(Rs232Payloads.PollResponsePayloadAndEscrowedBillPairs))]
+ public async Task AnEscrowedBillEncodedInAPollResponseMessageIsReported(byte[] responsePayload,
+ byte expectedBillType)
+ {
+ var mockSerialProvider = new Mock();
+ mockSerialProvider
+ .Setup(sp => sp.TryOpen())
+ .Returns(true);
+ mockSerialProvider
+ .SetupSequence(sp => sp.Read(It.IsAny()))
+ .ReturnsValidPollResponses()
+ .ReturnsResponse(responsePayload)
+ .ReturnsEmptyResponses();
+
+ var rs232Configuration = new Rs232Configuration
+ {
+ PollingPeriod = TimeSpan.Zero
+ };
+ using var billValidator = new BillValidator(new NullLogger(), mockSerialProvider.Object, rs232Configuration);
+
+ var actualBillType = 0;
+ billValidator.OnBillEscrowed += (_, billType) => { actualBillType = billType; };
+
+ var didPollingLoopStart = billValidator.StartPollingLoop();
+ Assert.That(didPollingLoopStart, Is.True);
+
+ while (billValidator.IsConnectionPresent)
+ {
+ await Task.Delay(rs232Configuration.PollingPeriod);
+ }
+
+ Assert.That(actualBillType, Is.EqualTo(expectedBillType));
+ }
+
+ [Test]
+ public async Task ADetectedBarcodeEncodedInAnExtendedResponseMessageIsReported()
+ {
+ var responsePayload = (byte[])Rs232Payloads.BarcodeDetectedResponsePayloadAndBarcodePair[0];
+ var expectedBarcode = (string)Rs232Payloads.BarcodeDetectedResponsePayloadAndBarcodePair[1];
+
+ var mockSerialProvider = new Mock();
+ mockSerialProvider
+ .Setup(sp => sp.TryOpen())
+ .Returns(true);
+ mockSerialProvider
+ .SetupSequence(sp => sp.Read(It.IsAny()))
+ .ReturnsValidPollResponses()
+ .ReturnsResponse(responsePayload)
+ .ReturnsEmptyResponses();
+
+ var rs232Configuration = new Rs232Configuration
+ {
+ PollingPeriod = TimeSpan.Zero
+ };
+ using var billValidator = new BillValidator(new NullLogger(), mockSerialProvider.Object, rs232Configuration);
+
+ var actualBarcode = string.Empty;
+ billValidator.OnBarcodeDetected += (_, barcode) => { actualBarcode = barcode; };
+
+ var didPollingLoopStart = billValidator.StartPollingLoop();
+ Assert.That(didPollingLoopStart, Is.True);
+
+ while (billValidator.IsConnectionPresent)
+ {
+ await Task.Delay(rs232Configuration.PollingPeriod);
+ }
+
+ Assert.That(actualBarcode, Is.EqualTo(expectedBarcode));
+ }
+
+ [Test]
+ public async Task ALostConnectionIsReported()
+ {
+ var mockSerialProvider = new Mock();
+ mockSerialProvider
+ .Setup(sp => sp.TryOpen())
+ .Returns(true);
+ mockSerialProvider
+ .SetupSequence(sp => sp.Read(It.IsAny()))
+ .ReturnsValidPollResponses()
+ .ReturnsEmptyResponses();
+
+ var rs232Configuration = new Rs232Configuration
+ {
+ PollingPeriod = TimeSpan.Zero
+ };
+ using var billValidator = new BillValidator(new NullLogger(), mockSerialProvider.Object, rs232Configuration);
+
+ var wasConnectionLost = false;
+ billValidator.OnConnectionLost += (_, _) => { wasConnectionLost = true; };
+
+ var didPollingLoopStart = billValidator.StartPollingLoop();
+ Assert.That(didPollingLoopStart, Is.True);
+
+ while (billValidator.IsConnectionPresent)
+ {
+ await Task.Delay(rs232Configuration.PollingPeriod);
+ }
+
+ Assert.That(wasConnectionLost, Is.True);
+ }
+
+ [Test]
+ public async Task ANonPollMessageCanBeSentOutsideAndDuringPolling()
+ {
+ var barcodeDetectedResponsePayload = (byte[])Rs232Payloads.BarcodeDetectedResponsePayloadAndBarcodePair[0];
+
+ var mockSerialProvider = new Mock();
+ mockSerialProvider
+ .Setup(sp => sp.TryOpen())
+ .Returns(true);
+ mockSerialProvider
+ .SetupSequence(sp => sp.Read(It.IsAny()))
+ .ReturnsValidPollResponses()
+ .ReturnsResponse(barcodeDetectedResponsePayload);
+
+ var rs232Configuration = new Rs232Configuration
+ {
+ PollingPeriod = TimeSpan.Zero
+ };
+ using var billValidator = new BillValidator(new NullLogger(), mockSerialProvider.Object, rs232Configuration);
+
+ var responseMessage1 = await billValidator.SendNonPollMessageAsync(
+ ack => new ExtendedRequestMessage(ack, ExtendedCommand.BarcodeDetected, []),
+ payload => new BarcodeDetectedResponseMessage(payload));
+
+ var wasBarcodeRequested = false;
+ var requestAck = false;
+ mockSerialProvider
+ .Setup(sp => sp.Write(It.IsAny()))
+ .Callback((byte[] payload) =>
+ {
+ if (payload.Length < 2)
+ {
+ return;
+ }
+
+ wasBarcodeRequested = (payload[2] & 0b11110000) == 0x70;
+ requestAck = (payload[2] & 0x01) == 1;
+ });
+
+ var responsePayloadRemainder = Array.Empty();
+ mockSerialProvider
+ .Setup(sp => sp.Read(It.Is(count => count == 2)))
+ .Returns(() =>
+ {
+ byte[] responsePayload;
+ if (wasBarcodeRequested && requestAck)
+ {
+ responsePayload = barcodeDetectedResponsePayload;
+ }
+ else
+ {
+ responsePayload = requestAck
+ ? Rs232Payloads.OneAckValidPollResponsePayload
+ : Rs232Payloads.ZeroAckValidPollResponsePayload;
+ }
+
+ responsePayloadRemainder = responsePayload[2..];
+ return responsePayload[..2];
+ });
+
+ mockSerialProvider
+ .Setup(sp => sp.Read(It.Is(count => count != 2)))
+ .Returns(() => responsePayloadRemainder);
+
+ var didPollingLoopStart = billValidator.StartPollingLoop();
+ Assert.That(didPollingLoopStart, Is.True);
+
+ var responseMessage2 = await billValidator.SendNonPollMessageAsync(
+ ack => new ExtendedRequestMessage(ack, ExtendedCommand.BarcodeDetected, []),
+ payload => new BarcodeDetectedResponseMessage(payload));
+
+ Assert.That(responseMessage1.IsValid, Is.True);
+ Assert.That(responseMessage2.IsValid, Is.True);
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Test/Tests/ExtendedResponseMessageTests.cs b/PTI.Rs232Validator.Test/Tests/ExtendedResponseMessageTests.cs
new file mode 100644
index 0000000..34028bd
--- /dev/null
+++ b/PTI.Rs232Validator.Test/Tests/ExtendedResponseMessageTests.cs
@@ -0,0 +1,47 @@
+using PTI.Rs232Validator.Messages.Commands;
+using PTI.Rs232Validator.Messages.Responses.Extended;
+using PTI.Rs232Validator.Test.Utility;
+
+namespace PTI.Rs232Validator.Test.Tests;
+
+public class ExtendedResponseMessageTests
+{
+ [Test]
+ public void ExtendedResponseMessage_DeserializesCommandAndStatusAndData()
+ {
+ var responsePayload = new byte[]
+ {
+ 0x02, 0x28, 0x71, 0x01, 0x01, 0x10, 0x00, 0x00, 0x01, 0x02, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
+ 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50,
+ 0x51, 0x52, 0x03, 0x58
+ };
+
+ const ExtendedCommand expectedCommand = ExtendedCommand.BarcodeDetected;
+ var expectedStatus = new byte[] { 0x01, 0x10, 0x00, 0x00, 0x01, 0x02 };
+ var expectedData = new byte[]
+ {
+ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
+ 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50,
+ 0x51, 0x52
+ };
+
+ var extendedResponseMessage = new ExtendedResponseMessage(responsePayload);
+
+ Assert.That(extendedResponseMessage.IsValid, Is.True);
+ Assert.That(extendedResponseMessage.Command, Is.EqualTo(expectedCommand));
+ Assert.That(extendedResponseMessage.Status, Is.EqualTo(expectedStatus));
+ Assert.That(extendedResponseMessage.Data, Is.EqualTo(expectedData));
+ }
+
+ [Test]
+ public void BarcodeDetectedResponseMessage_DeserializesBarcode()
+ {
+ var responsePayload = (byte[])Rs232Payloads.BarcodeDetectedResponsePayloadAndBarcodePair[0];
+ var barcode = (string)Rs232Payloads.BarcodeDetectedResponsePayloadAndBarcodePair[1];
+
+ var barcodeDetectedResponseMessage = new BarcodeDetectedResponseMessage(responsePayload);
+
+ Assert.That(barcodeDetectedResponseMessage.IsValid, Is.True);
+ Assert.That(barcodeDetectedResponseMessage.Barcode, Is.EqualTo(barcode));
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Test/Tests/PollResponseMessageTests.cs b/PTI.Rs232Validator.Test/Tests/PollResponseMessageTests.cs
new file mode 100644
index 0000000..50aca46
--- /dev/null
+++ b/PTI.Rs232Validator.Test/Tests/PollResponseMessageTests.cs
@@ -0,0 +1,73 @@
+using PTI.Rs232Validator.Messages.Responses;
+using PTI.Rs232Validator.Test.Utility;
+using System;
+
+namespace PTI.Rs232Validator.Test.Tests;
+
+public class PollResponseMessageTests
+{
+ [Test, TestCaseSource(typeof(Rs232Payloads), nameof(Rs232Payloads.PollResponsePayloadAndStatePairs))]
+ public void PollResponseMessage_DeserializesSingleStates(byte[] responsePayload, Rs232State expectedState)
+ {
+ var pollResponseMessage = new PollResponseMessage(responsePayload);
+
+ Assert.That(pollResponseMessage.IsValid, Is.True);
+ Assert.That(pollResponseMessage.State, Is.EqualTo(expectedState));
+ }
+
+ [Test, TestCaseSource(typeof(Rs232Payloads), nameof(Rs232Payloads.PollResponsePayloadAndEventPairs))]
+ public void PollResponseMessage_DeserializesSingleEvents(byte[] responsePayload, Rs232Event expectedEvent)
+ {
+ var pollResponseMessage = new PollResponseMessage(responsePayload);
+
+ Assert.That(pollResponseMessage.IsValid, Is.True);
+ Assert.That(pollResponseMessage.Event, Is.EqualTo(expectedEvent));
+ }
+
+ [Test]
+ public void PollResponseMessage_DeserializesMultipleEvents()
+ {
+ var pollResponseMessage = new PollResponseMessage(Rs232Payloads.PollResponsePayloadWithEveryEvent);
+
+ var expectedEvent = Rs232Event.None;
+ foreach (var value in Enum.GetValues(typeof(Rs232Event)))
+ {
+ expectedEvent |= (Rs232Event)value;
+ }
+
+ Assert.That(pollResponseMessage.IsValid, Is.True);
+ Assert.That(pollResponseMessage.Event, Is.EqualTo(expectedEvent));
+ }
+
+ [Test, TestCaseSource(typeof(Rs232Payloads), nameof(Rs232Payloads.PollResponsePayloadAndCashboxPresencePairs))]
+ public void PollResponseMessage_DeserializesCashboxPresence(byte[] responsePayload, bool expectedIsCashboxPresent)
+ {
+ var pollResponseMessage = new PollResponseMessage(responsePayload);
+
+ Assert.That(pollResponseMessage.IsValid, Is.True);
+ Assert.That(pollResponseMessage.IsCashboxPresent, Is.EqualTo(expectedIsCashboxPresent));
+ }
+
+ [Test, TestCaseSource(typeof(Rs232Payloads), nameof(Rs232Payloads.PollResponsePayloadAndStackedBillPairs))]
+ public void PollResponseMessage_DeserializesBillType(byte[] responsePayload, byte expectedBillType)
+ {
+ var pollResponseMessage = new PollResponseMessage(responsePayload);
+
+ Assert.That(pollResponseMessage.IsValid, Is.True);
+ Assert.That(pollResponseMessage.BillType, Is.EqualTo(expectedBillType));
+ }
+
+ [Test]
+ public void PollResponseMessage_DeserializesModelNumberAndFirmwareRevision()
+ {
+ var responsePayload = new byte[] { 0x02, 0x0B, 0x20, 0b00000001, 0b00010000, 0b00000000, 0, 1, 2, 0x03, 0x39 };
+ const byte expectedModelNumber = 1;
+ const byte expectedFirmwareRevision = 2;
+
+ var pollResponseMessage = new PollResponseMessage(responsePayload);
+
+ Assert.That(pollResponseMessage.IsValid, Is.True);
+ Assert.That(pollResponseMessage.ModelNumber, Is.EqualTo(expectedModelNumber));
+ Assert.That(pollResponseMessage.FirmwareRevision, Is.EqualTo(expectedFirmwareRevision));
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Test/Tests/TelemetryResponseMessageTests.cs b/PTI.Rs232Validator.Test/Tests/TelemetryResponseMessageTests.cs
new file mode 100644
index 0000000..09113e7
--- /dev/null
+++ b/PTI.Rs232Validator.Test/Tests/TelemetryResponseMessageTests.cs
@@ -0,0 +1,265 @@
+using PTI.Rs232Validator.Messages.Responses.Telemetry;
+
+namespace PTI.Rs232Validator.Test.Tests;
+
+public class TelemetryResponseMessageTests
+{
+ [Test]
+ public void TelemetryResponseMessage_DeserializesData()
+ {
+ var responsePayload = new byte[]
+ { 0x02, 0x0E, 0x61, 0x32, 0x31, 0x30, 0x32, 0x36, 0x30, 0x30, 0x31, 0x30, 0x03, 0x59 };
+
+ var expectedData = new byte[] { 0x32, 0x31, 0x30, 0x32, 0x36, 0x30, 0x30, 0x31, 0x30 };
+
+ var telemetryResponseMessage = new TelemetryResponseMessage(responsePayload);
+
+ Assert.That(telemetryResponseMessage.IsValid, Is.True);
+ Assert.That(telemetryResponseMessage.Data, Is.EqualTo(expectedData));
+ }
+
+ [Test]
+ public void GetSerialNumberResponseMessage_DeserializesSerialNumber()
+ {
+ var responsePayload = new byte[]
+ { 0x02, 0x0E, 0x61, 0x32, 0x31, 0x30, 0x32, 0x36, 0x30, 0x30, 0x31, 0x30, 0x03, 0x59 };
+
+ const string expectedSerialNumber = "210260010";
+
+ var getSerialNumberResponseMessage = new GetSerialNumberResponseMessage(responsePayload);
+
+ Assert.That(getSerialNumberResponseMessage.IsValid, Is.True);
+ Assert.That(getSerialNumberResponseMessage.SerialNumber, Is.EqualTo(expectedSerialNumber));
+ }
+
+ [Test]
+ public void GetCashboxMetricsResponseMessage_DeserializesCashboxMetrics()
+ {
+ var responsePayload = new byte[]
+ {
+ 0x02, 0x35, 0x61,
+ 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E,
+ 0x0E, 0x0D, 0x0E, 0x0D, 0x0E, 0x0D, 0x0E, 0x0D,
+ 0x0D, 0x0C, 0x0D, 0x0C, 0x0D, 0x0C, 0x0D, 0x0C,
+ 0x0C, 0x0B, 0x0C, 0x0B, 0x0C, 0x0B, 0x0C, 0x0B,
+ 0x0B, 0x0A, 0x0B, 0x0A, 0x0B, 0x0A, 0x0B, 0x0A,
+ 0x0A, 0x09, 0x0A, 0x09, 0x0A, 0x09, 0x0A, 0x09,
+ 0x03, 0x54
+ };
+
+ const uint expectedCashboxRemovedCount = 0xFEFEFEFE;
+ const uint expectedCashboxFullCount = 0xEDEDEDED;
+ const uint expectedBillsStackedSinceCashboxRemoved = 0xDCDCDCDC;
+ const uint expectedBillsStackedSincePowerUp = 0xCBCBCBCB;
+ const uint expectedAverageTimeToStack = 0xBABABABA;
+ const uint expectedTotalBillsStacked = 0xA9A9A9A9;
+
+ var getCashboxMetricsResponseMessage = new GetCashboxMetricsResponseMessage(responsePayload);
+
+ Assert.That(getCashboxMetricsResponseMessage.IsValid, Is.True);
+ Assert.That(getCashboxMetricsResponseMessage.CashboxRemovedCount, Is.EqualTo(expectedCashboxRemovedCount));
+ Assert.That(getCashboxMetricsResponseMessage.CashboxFullCount, Is.EqualTo(expectedCashboxFullCount));
+ Assert.That(getCashboxMetricsResponseMessage.BillsStackedSinceCashboxRemoved,
+ Is.EqualTo(expectedBillsStackedSinceCashboxRemoved));
+ Assert.That(getCashboxMetricsResponseMessage.BillsStackedSincePowerUp,
+ Is.EqualTo(expectedBillsStackedSincePowerUp));
+ Assert.That(getCashboxMetricsResponseMessage.AverageTimeToStack, Is.EqualTo(expectedAverageTimeToStack));
+ Assert.That(getCashboxMetricsResponseMessage.TotalBillsStacked, Is.EqualTo(expectedTotalBillsStacked));
+ }
+
+ [Test]
+ public void GetUnitMetricsResponseMessage_DeserializesUnitMetrics()
+ {
+ var responsePayload = new byte[]
+ {
+ 0x02, 0x45, 0x61,
+ 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E,
+ 0x0E, 0x0D, 0x0E, 0x0D, 0x0E, 0x0D, 0x0E, 0x0D,
+ 0x0D, 0x0C, 0x0D, 0x0C, 0x0D, 0x0C, 0x0D, 0x0C,
+ 0x0C, 0x0B, 0x0C, 0x0B, 0x0C, 0x0B, 0x0C, 0x0B,
+ 0x0B, 0x0A, 0x0B, 0x0A, 0x0B, 0x0A, 0x0B, 0x0A,
+ 0x0A, 0x09, 0x0A, 0x09, 0x0A, 0x09, 0x0A, 0x09,
+ 0x09, 0x08, 0x09, 0x08, 0x09, 0x08, 0x09, 0x08,
+ 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07,
+ 0x03, 0x24
+ };
+
+ const uint expectedTotalValueStacked = 0xFEFEFEFE;
+ const uint expectedTotalDistanceMoved = 0xEDEDEDED;
+ const uint expectedPowerUpCount = 0xDCDCDCDC;
+ const uint expectedPushButtonCount = 0xCBCBCBCB;
+ const uint expectedConfigurationCount = 0xBABABABA;
+ const uint expectedUsbEnumerationsCount = 0xA9A9A9A9;
+ const uint expectedTotalCheatAttemptsDetected = 0x98989898;
+ const uint expectedTotalSecurityLockupCount = 0x87878787;
+
+ var getUnitMetricsResponseMessage = new GetUnitMetricsResponseMessage(responsePayload);
+
+ Assert.That(getUnitMetricsResponseMessage.IsValid, Is.True);
+ Assert.That(getUnitMetricsResponseMessage.TotalValueStacked, Is.EqualTo(expectedTotalValueStacked));
+ Assert.That(getUnitMetricsResponseMessage.TotalDistanceMoved, Is.EqualTo(expectedTotalDistanceMoved));
+ Assert.That(getUnitMetricsResponseMessage.PowerUpCount, Is.EqualTo(expectedPowerUpCount));
+ Assert.That(getUnitMetricsResponseMessage.PushButtonCount, Is.EqualTo(expectedPushButtonCount));
+ Assert.That(getUnitMetricsResponseMessage.ConfigurationCount, Is.EqualTo(expectedConfigurationCount));
+ Assert.That(getUnitMetricsResponseMessage.UsbEnumerationsCount, Is.EqualTo(expectedUsbEnumerationsCount));
+ Assert.That(getUnitMetricsResponseMessage.TotalCheatAttemptsDetected,
+ Is.EqualTo(expectedTotalCheatAttemptsDetected));
+ Assert.That(getUnitMetricsResponseMessage.TotalSecurityLockupCount,
+ Is.EqualTo(expectedTotalSecurityLockupCount));
+ }
+
+ [Test]
+ public void GetServiceUsageCountersResponseMessage_DeserializesServiceUsageCounters()
+ {
+ var responsePayload = new byte[]
+ {
+ 0x02, 0x35, 0x61,
+ 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E,
+ 0x0E, 0x0D, 0x0E, 0x0D, 0x0E, 0x0D, 0x0E, 0x0D,
+ 0x0D, 0x0C, 0x0D, 0x0C, 0x0D, 0x0C, 0x0D, 0x0C,
+ 0x0C, 0x0B, 0x0C, 0x0B, 0x0C, 0x0B, 0x0C, 0x0B,
+ 0x0B, 0x0A, 0x0B, 0x0A, 0x0B, 0x0A, 0x0B, 0x0A,
+ 0x0A, 0x09, 0x0A, 0x09, 0x0A, 0x09, 0x0A, 0x09,
+ 0x03, 0x54
+ };
+
+ const uint expectedDistancedMovedSinceLastTachSensorService = 0xFEFEFEFE;
+ const uint expectedDistanceMovedSinceLastBillPathService = 0xEDEDEDED;
+ const uint expectedDistancedMoveSinceLastBeltService = 0xDCDCDCDC;
+ const uint expectedBillsStackedSinceLastCashboxService = 0xCBCBCBCB;
+ const uint expectedDistanceMovedSinceLastMasService = 0xBABABABA;
+ const uint expectedDistanceMovedSinceLastSpringRollerService = 0xA9A9A9A9;
+
+ var getServiceUsageCountersResponseMessage = new GetServiceUsageCountersResponseMessage(responsePayload);
+
+ Assert.That(getServiceUsageCountersResponseMessage.IsValid, Is.True);
+ Assert.That(getServiceUsageCountersResponseMessage.DistancedMovedSinceLastTachSensorService,
+ Is.EqualTo(expectedDistancedMovedSinceLastTachSensorService));
+ Assert.That(getServiceUsageCountersResponseMessage.DistanceMovedSinceLastBillPathService,
+ Is.EqualTo(expectedDistanceMovedSinceLastBillPathService));
+ Assert.That(getServiceUsageCountersResponseMessage.DistancedMoveSinceLastBeltService,
+ Is.EqualTo(expectedDistancedMoveSinceLastBeltService));
+ Assert.That(getServiceUsageCountersResponseMessage.BillsStackedSinceLastCashboxService,
+ Is.EqualTo(expectedBillsStackedSinceLastCashboxService));
+ Assert.That(getServiceUsageCountersResponseMessage.DistanceMovedSinceLastMasService,
+ Is.EqualTo(expectedDistanceMovedSinceLastMasService));
+ Assert.That(getServiceUsageCountersResponseMessage.DistanceMovedSinceLastSpringRollerService,
+ Is.EqualTo(expectedDistanceMovedSinceLastSpringRollerService));
+ }
+
+ [Test]
+ public void GetServiceFlagsResponseMessage_DeserializesServiceFlags()
+ {
+ var responsePayload = new byte[]
+ {
+ 0x02, 0x0B, 0x61,
+ 0b00000000, 0b00000001, 0b00000001, 0b00000010, 0b00000010, 0b00000011,
+ 0x03, 0x69
+ };
+
+ const GetServiceFlagsResponseMessage.ServiceSuggestor expectedTachSensorServiceSuggestor =
+ GetServiceFlagsResponseMessage.ServiceSuggestor.None;
+ const GetServiceFlagsResponseMessage.ServiceSuggestor expectedBillPathServiceSuggestor =
+ GetServiceFlagsResponseMessage.ServiceSuggestor.UsageMetrics;
+ const GetServiceFlagsResponseMessage.ServiceSuggestor expectedCashboxBeltServiceSuggestor =
+ GetServiceFlagsResponseMessage.ServiceSuggestor.UsageMetrics;
+ const GetServiceFlagsResponseMessage.ServiceSuggestor expectedCashboxMechanismServiceSuggestor =
+ GetServiceFlagsResponseMessage.ServiceSuggestor.DiagnosticsAndError;
+ const GetServiceFlagsResponseMessage.ServiceSuggestor expectedMasServiceSuggestor =
+ GetServiceFlagsResponseMessage.ServiceSuggestor.DiagnosticsAndError;
+ const GetServiceFlagsResponseMessage.ServiceSuggestor expectedSpringRollersServiceSuggestor =
+ GetServiceFlagsResponseMessage.ServiceSuggestor.UsageMetrics
+ | GetServiceFlagsResponseMessage.ServiceSuggestor.DiagnosticsAndError;
+
+ var getServiceFlagsResponseMessage = new GetServiceFlagsResponseMessage(responsePayload);
+
+ Assert.That(getServiceFlagsResponseMessage.IsValid, Is.True);
+ Assert.That(getServiceFlagsResponseMessage.TachSensorServiceSuggestor,
+ Is.EqualTo(expectedTachSensorServiceSuggestor));
+ Assert.That(getServiceFlagsResponseMessage.BillPathServiceSuggestor,
+ Is.EqualTo(expectedBillPathServiceSuggestor));
+ Assert.That(getServiceFlagsResponseMessage.CashboxBeltServiceSuggestor,
+ Is.EqualTo(expectedCashboxBeltServiceSuggestor));
+ Assert.That(getServiceFlagsResponseMessage.CashboxMechanismServiceSuggestor,
+ Is.EqualTo(expectedCashboxMechanismServiceSuggestor));
+ Assert.That(getServiceFlagsResponseMessage.MasServiceSuggestor, Is.EqualTo(expectedMasServiceSuggestor));
+ Assert.That(getServiceFlagsResponseMessage.SpringRollersServiceSuggestor,
+ Is.EqualTo(expectedSpringRollersServiceSuggestor));
+ }
+
+ [Test]
+ public void GetServiceInfoResponseMessage_DeserializesServiceInfo()
+ {
+ var responsePayload = new byte[]
+ {
+ 0x02, 0x11, 0x61,
+ 0b11111111, 0b11111111, 0b11111111, 0b11111111,
+ 0b10111111, 0b10111111, 0b10111111, 0b10111111,
+ 0b10011111, 0b10011111, 0b10011111, 0b10011111,
+ 0x03, 0x70
+ };
+
+ byte[] expectedLastCustomerService = [0b01111111, 0b01111111, 0b01111111, 0b01111111];
+ byte[] expectedLastServiceCenterService = [0b00111111, 0b00111111, 0b00111111, 0b00111111];
+ byte[] expectedLastOemService = [0b00011111, 0b00011111, 0b00011111, 0b00011111];
+
+ var getServiceInfoResponseMessage = new GetServiceInfoResponseMessage(responsePayload);
+
+ Assert.That(getServiceInfoResponseMessage.IsValid, Is.True);
+ Assert.That(getServiceInfoResponseMessage.LastCustomerService, Is.EqualTo(expectedLastCustomerService));
+ Assert.That(getServiceInfoResponseMessage.LastServiceCenterService,
+ Is.EqualTo(expectedLastServiceCenterService));
+ Assert.That(getServiceInfoResponseMessage.LastOemService, Is.EqualTo(expectedLastOemService));
+ }
+
+ [Test]
+ public void GetFirmwareMetricsResponseMessage_DeserializesFirmwareMetrics()
+ {
+ var responsePayload = new byte[]
+ {
+ 0x02, 0x45, 0x61,
+ 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E,
+ 0x0E, 0x0D, 0x0E, 0x0D, 0x0E, 0x0D, 0x0E, 0x0D,
+ 0x0D, 0x0C, 0x0D, 0x0C, 0x0D, 0x0C, 0x0D, 0x0C,
+ 0x0C, 0x0B, 0x0C, 0x0B,
+ 0x0B, 0x0A, 0x0B, 0x0A,
+ 0x0A, 0x09, 0x0A, 0x09, 0x0A, 0x09, 0x0A, 0x09,
+ 0x09, 0x08, 0x09, 0x08, 0x09, 0x08, 0x09, 0x08,
+ 0x08, 0x07, 0x08, 0x07,
+ 0x07, 0x06, 0x07, 0x06,
+ 0x06, 0x05, 0x06, 0x05, 0x06, 0x05, 0x06, 0x05,
+ 0x03, 0x24
+ };
+
+ const uint expectedFlashUpdateCount = 0xFEFEFEFE;
+ const uint expectedUsbFlashDriveFirmwareUpdateCount = 0xEDEDEDED;
+ const uint expectedTotalFlashDriveInsertCount = 0xDCDCDCDC;
+ const ushort expectedFirmwareCountryRevision = 0xCBCB;
+ const ushort expectedFirmwareCoreRevision = 0xBABA;
+ const uint expectedFirmwareBuildRevision = 0xA9A9A9A9;
+ const uint expectedFirmwareCrc = 0x98989898;
+ const ushort expectedBootloaderMajorRevision = 0x8787;
+ const ushort expectedBootloaderMinorRevision = 0x7676;
+ const uint expectedBootloaderBuildRevision = 0x65656565;
+
+ var getFirmwareMetricsResponseMessage = new GetFirmwareMetricsResponseMessage(responsePayload);
+
+ Assert.That(getFirmwareMetricsResponseMessage.IsValid, Is.True);
+ Assert.That(getFirmwareMetricsResponseMessage.FlashUpdateCount, Is.EqualTo(expectedFlashUpdateCount));
+ Assert.That(getFirmwareMetricsResponseMessage.UsbFlashDriveFirmwareUpdateCount,
+ Is.EqualTo(expectedUsbFlashDriveFirmwareUpdateCount));
+ Assert.That(getFirmwareMetricsResponseMessage.TotalFlashDriveInsertCount,
+ Is.EqualTo(expectedTotalFlashDriveInsertCount));
+ Assert.That(getFirmwareMetricsResponseMessage.FirmwareCountryRevision,
+ Is.EqualTo(expectedFirmwareCountryRevision));
+ Assert.That(getFirmwareMetricsResponseMessage.FirmwareCoreRevision, Is.EqualTo(expectedFirmwareCoreRevision));
+ Assert.That(getFirmwareMetricsResponseMessage.FirmwareBuildRevision, Is.EqualTo(expectedFirmwareBuildRevision));
+ Assert.That(getFirmwareMetricsResponseMessage.FirmwareCrc, Is.EqualTo(expectedFirmwareCrc));
+ Assert.That(getFirmwareMetricsResponseMessage.BootloaderMajorRevision,
+ Is.EqualTo(expectedBootloaderMajorRevision));
+ Assert.That(getFirmwareMetricsResponseMessage.BootloaderMinorRevision,
+ Is.EqualTo(expectedBootloaderMinorRevision));
+ Assert.That(getFirmwareMetricsResponseMessage.BootloaderBuildRevision,
+ Is.EqualTo(expectedBootloaderBuildRevision));
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Test/Utility/MoqExtensions.cs b/PTI.Rs232Validator.Test/Utility/MoqExtensions.cs
new file mode 100644
index 0000000..28bddb0
--- /dev/null
+++ b/PTI.Rs232Validator.Test/Utility/MoqExtensions.cs
@@ -0,0 +1,38 @@
+using Moq.Language;
+using PTI.Rs232Validator.BillValidators;
+
+namespace PTI.Rs232Validator.Test.Utility;
+
+public static class MoqExtensions
+{
+ public static ISetupSequentialResult ReturnsResponse(this ISetupSequentialResult result,
+ byte[] responsePayload)
+ {
+ return result
+ .Returns(responsePayload[..2])
+ .Returns(responsePayload[2..]);
+ }
+
+ public static ISetupSequentialResult ReturnsValidPollResponses(this ISetupSequentialResult result)
+ {
+ for (var i = 0; i < BillValidator.SuccessfulPollsRequiredToStartPollingLoop; i++)
+ {
+ var payload = i % 2 == 0
+ ? Rs232Payloads.OneAckValidPollResponsePayload
+ : Rs232Payloads.ZeroAckValidPollResponsePayload;
+ result = result.ReturnsResponse(payload);
+ }
+
+ return result;
+ }
+
+ public static ISetupSequentialResult ReturnsEmptyResponses(this ISetupSequentialResult result)
+ {
+ for (var i = 0; i < BillValidator.MaxReadAttempts; i++)
+ {
+ result = result.Returns([]);
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Test/Utility/Rs232Payloads.cs b/PTI.Rs232Validator.Test/Utility/Rs232Payloads.cs
new file mode 100644
index 0000000..a5a73c0
--- /dev/null
+++ b/PTI.Rs232Validator.Test/Utility/Rs232Payloads.cs
@@ -0,0 +1,223 @@
+namespace PTI.Rs232Validator.Test.Utility;
+
+public static class Rs232Payloads
+{
+ ///
+ /// This payload contains the enumerator
+ /// and no enumerators.
+ ///
+ public static byte[] ZeroAckValidPollResponsePayload =>
+ [0x02, 0x0B, 0x20, 0b00000001, 0b00010000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x39];
+
+ ///
+ /// This payload contains the enumerator
+ /// and no enumerators.
+ ///
+ public static byte[] OneAckValidPollResponsePayload =>
+ [0x02, 0x0B, 0x21, 0b00000001, 0b00010000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x38];
+
+ ///
+ /// These payloads contain no enumerators.
+ ///
+ public static object[] PollResponsePayloadAndStatePairs =>
+ [
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000001, 0b00010000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x38 },
+ Rs232State.Idling
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000010, 0b00010000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x3B },
+ Rs232State.Accepting
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000100, 0b00010000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x3D },
+ Rs232State.Escrowed
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00001000, 0b00010000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x31 },
+ Rs232State.Stacking
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00100000, 0b00010000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x19 },
+ Rs232State.Returning
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000000, 0b00010100, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x3D },
+ Rs232State.BillJammed
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000000, 0b00011000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x31 },
+ Rs232State.StackerFull
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000000, 0b00010000, 0b00000100, 0x00, 0x01, 0x02, 0x03, 0x3D },
+ Rs232State.Failure
+ }
+ ];
+
+ ///
+ /// These payloads contain the enumerator.
+ ///
+ public static object[] PollResponsePayloadAndEventPairs =>
+ [
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00010001, 0b00010000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x28 },
+ Rs232Event.Stacked
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b01000001, 0b00010000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x78 },
+ Rs232Event.Returned
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000001, 0b00010001, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x39 },
+ Rs232Event.Cheated
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000001, 0b00010010, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x3A },
+ Rs232Event.BillRejected
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000001, 0b00010000, 0b00000010, 0x00, 0x01, 0x02, 0x03, 0x3A },
+ Rs232Event.InvalidCommand
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000001, 0b00010000, 0b00000001, 0x00, 0x01, 0x02, 0x03, 0x39 },
+ Rs232Event.PowerUp
+ }
+ ];
+
+ ///
+ /// This payload contains the enumerator.
+ ///
+ public static byte[] PollResponsePayloadWithEveryEvent =>
+ [0x02, 0x0B, 0x21, 0b01010001, 0b00010011, 0b00000011, 0x00, 0x01, 0x02, 0x03, 0x68];
+
+ ///
+ /// These payloads contain the enumerator
+ /// and no enumerators.
+ ///
+ public static object[] PollResponsePayloadAndCashboxPresencePairs { get; } =
+ [
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000001, 0b00000000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x28 },
+ false
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000001, 0b00010000, 0b00000000, 0x00, 0x01, 0x02, 0x03, 0x38 },
+ true
+ }
+ ];
+
+ ///
+ /// These payloads contain the enumerator
+ /// and the enumerator.
+ ///
+ public static object[] PollResponsePayloadAndStackedBillPairs =>
+ [
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00010001, 0b00010000, 0b00001000, 0x00, 0x01, 0x02, 0x03, 0x20 },
+ (byte)1
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00010001, 0b00010000, 0b00010000, 0x00, 0x01, 0x02, 0x03, 0x38 },
+ (byte)2
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00010001, 0b00010000, 0b00011000, 0x00, 0x01, 0x02, 0x03, 0x30 },
+ (byte)3
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00010001, 0b00010000, 0b00100000, 0x00, 0x01, 0x02, 0x03, 0x08 },
+ (byte)4
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00010001, 0b00010000, 0b00101000, 0x00, 0x01, 0x02, 0x03, 0x00 },
+ (byte)5
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00010001, 0b00010000, 0b00110000, 0x00, 0x01, 0x02, 0x03, 0x18 },
+ (byte)6
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00010001, 0b00010000, 0b00111000, 0x00, 0x01, 0x02, 0x03, 0x10 },
+ (byte)7
+ }
+ ];
+
+ ///
+ /// These payloads contain the enumerator
+ /// and no enumerators.
+ ///
+ public static object[] PollResponsePayloadAndEscrowedBillPairs =>
+ [
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000100, 0b00010000, 0b00001000, 0x00, 0x01, 0x02, 0x03, 0x35 },
+ (byte)1
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000100, 0b00010000, 0b00010000, 0x00, 0x01, 0x02, 0x03, 0x2D },
+ (byte)2
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000100, 0b00010000, 0b00011000, 0x00, 0x01, 0x02, 0x03, 0x25 },
+ (byte)3
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000100, 0b00010000, 0b00100000, 0x00, 0x01, 0x02, 0x03, 0x1D },
+ (byte)4
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000100, 0b00010000, 0b00101000, 0x00, 0x01, 0x02, 0x03, 0x15 },
+ (byte)5
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000100, 0b00010000, 0b00110000, 0x00, 0x01, 0x02, 0x03, 0x0D },
+ (byte)6
+ },
+ new object[]
+ {
+ new byte[] { 0x02, 0x0B, 0x21, 0b00000100, 0b00010000, 0b00111000, 0x00, 0x01, 0x02, 0x03, 0x05 },
+ (byte)7
+ }
+ ];
+
+ public static object[] BarcodeDetectedResponsePayloadAndBarcodePair =>
+ [
+ new byte[]
+ {
+ 0x02, 0x28, 0x71, 0x01, 0x01, 0x10, 0x00, 0x00, 0x01, 0x02, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
+ 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50,
+ 0x51, 0x52, 0x03, 0x58
+ },
+ "0123456789ABCDEFGHIJKLMNOPQR"
+ ];
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Tests/IntegrationTest.cs b/PTI.Rs232Validator.Tests/IntegrationTest.cs
deleted file mode 100644
index c9309e2..0000000
--- a/PTI.Rs232Validator.Tests/IntegrationTest.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-namespace PTI.Rs232Validator.Tests
-{
- using System;
- using System.Linq;
- using Emulator;
- using NUnit.Framework;
-
- public class Integration
- {
- [Test]
- public void SinglePollTest()
- {
- // Setup
- var runner = new EmulationRunner(TimeSpan.FromMilliseconds(1));
- var loopCount = 5;
-
- // Execute
- var emulator = runner.RunIdleFor(loopCount);
-
- // Assert
- Assert.GreaterOrEqual(emulator.TotalPollCount, loopCount);
- }
-
- [Test]
- public void CreditSequenceTest()
- {
- // Setup
- const int pollsBetween = 5;
- const int creditCount = 10;
- const byte creditIndex = 1;
- var runner = new EmulationRunner(TimeSpan.FromMilliseconds(1));
- var expected = Enumerable.Repeat(creditIndex, creditCount).ToList();
-
- // Execute
- var emulator = runner.CreditEveryNLoops(pollsBetween, creditCount, creditIndex);
-
- // Assert
- Assert.AreEqual(expected, emulator.IssueCredits.ToList());
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator.Tests/MessageTest.cs b/PTI.Rs232Validator.Tests/MessageTest.cs
deleted file mode 100644
index 997f58e..0000000
--- a/PTI.Rs232Validator.Tests/MessageTest.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-namespace PTI.Rs232Validator.Tests
-{
- using Messages;
- using NUnit.Framework;
-
- public class MessageTest
- {
- ///
- /// Ensure that host messages are detected
- ///
- [Test]
- public void HostParseTest()
- {
- // Setup
- var raw = new byte[] {0x02, 0x08, 0x10, 0x7F, 0x00, 0x00, 0x03, 0x67};
-
- // Execute
- var message = new TestMessage(raw);
-
- // Assert
- Assert.True(message.IsHostMessage);
- }
-
- ///
- /// Ensure that host messages are detected
- ///
- [Test]
- public void NotHostParseTest()
- {
- // Setup
- var raw = new byte[]
- {
- 0x02, 0x0B, 0x20, 0x01, 0x10, 0x00, 0x00, 0x12, 0x13, 0x03, 0x3B
- };
-
- // Execute
- var message = new TestMessage(raw);
-
- // Assert
- Assert.False(message.IsHostMessage);
- }
-
- ///
- /// Detect ACK message
- ///
- [Test]
- public void IsAckTest()
- {
- // Setup
- var raw = new byte[] {0x02, 0x08, 0x11, 0x7F, 0x00, 0x00, 0x03, 0x68};
-
- // Execute
- var message = new TestMessage(raw);
-
- // Assert
- Assert.True(message.Ack);
- }
-
- ///
- /// Detect ACK message
- ///
- [Test]
- public void IsNotAckTest()
- {
- // Setup
- var raw = new byte[] {0x02, 0x08, 0x10, 0x7F, 0x00, 0x00, 0x03, 0x67};
-
- // Execute
- var message = new TestMessage(raw);
-
- // Assert
- Assert.False(message.Ack);
- }
-
- ///
- /// Test that serialization works
- ///
- [Test]
- public void SerializeTest()
- {
- // Setup
- var raw = new byte[] {0x02, 0x08, 0x10, 0x7F, 0x00, 0x00, 0x03, 0x67};
-
- // Execute
- var message = new TestMessage(raw);
-
- // Assert
- Assert.AreEqual(raw, message.Serialize());
- }
- }
-
- ///
- /// Concrete wrapper to test base message members
- ///
- internal class TestMessage : Rs232BaseMessage
- {
- public TestMessage(byte[] messageData) : base(messageData)
- {
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/ApexValidator.cs b/PTI.Rs232Validator/ApexValidator.cs
deleted file mode 100644
index 720bf6f..0000000
--- a/PTI.Rs232Validator/ApexValidator.cs
+++ /dev/null
@@ -1,443 +0,0 @@
-namespace PTI.Rs232Validator
-{
- using System;
- using System.Threading;
- using Messages;
-
- ///
- /// Apex series RS232 implementation
- ///
- public class ApexValidator : BaseValidator
- {
- ///
- /// When Apex is busy stacking it will not respond.
- /// This sets the maximum threshold of messages to
- /// treat as busy before we consider the acceptor offline.
- ///
- private const int MaxBusyMessages = 6;
-
- ///
- /// Tracks Apex state between polling messages
- ///
- private ApexState _apexState;
-
- ///
- /// Create a new Apex RS232 validator.
- /// You must be using the RS232 Tx and Tx on the 18 pin IO.
- /// You must have the acceptor configured for RS232 mode.
- ///
- /// Configuration options
- public ApexValidator(Rs232Config config) : base(config)
- {
- _apexState = new ApexState();
- }
-
- ///
- public override void PauseAcceptance()
- {
- if (IsPaused)
- {
- Logger?.Error("{0} acceptance is already paused", GetType().Name);
- return;
- }
-
- Logger?.Info("{0} pausing acceptance", GetType().Name);
-
- // Track what the enable mask currently is
- _apexState.PreviousEnableMask = Config.EnableMask;
-
- // This cause the bill acceptor to not accept any bills
- Config.EnableMask = 0;
- }
-
- ///
- public override void ResumeAcceptance()
- {
- if (!IsPaused || !_apexState.PreviousEnableMask.HasValue)
- {
- Logger?.Error("{0} acceptance is not paused", GetType().Name);
- return;
- }
-
- Logger?.Info("{0} resuming acceptance", GetType().Name);
-
- Config.EnableMask = _apexState.PreviousEnableMask.Value;
- _apexState.PreviousEnableMask = null;
- }
-
- ///
- public override bool IsPaused => _apexState.PreviousEnableMask.HasValue;
-
- ///
- public override bool IsUnresponsive => _apexState.NonResponsiveCount > MaxBusyMessages;
-
- ///
- protected override bool PollDevice()
- {
- if (!SerialProvider.IsOpen)
- {
- Logger?.Error("{0} Serial provider is not open", GetType().Name);
- _apexState.NonResponsiveCount++;
- return false;
- }
-
- var pollResponse = SendPollMessage();
-
- if (pollResponse is null)
- {
- return false;
- }
-
-
- HandleState(pollResponse);
-
- HandleEvents(pollResponse);
-
- HandleCashBox(pollResponse);
-
- HandleCredit(pollResponse);
-
- // Handle escrow events last so logging and other events have a logical order
- HandleEscrow(pollResponse);
-
- return true;
- }
-
- ///
- protected override void DoStack()
- {
- Logger?.Trace("{0} Issuing do-stack request", GetType().Name);
- _apexState.StackNext = true;
- }
-
- ///
- protected override void DoReturn()
- {
- Logger?.Trace("{0} Issuing do-return request", GetType().Name);
- _apexState.ReturnNext = true;
- }
-
- ///
- /// Send poll message to device and return a validated response.
- /// If the response is malformed, null will be returned.
- ///
- /// Device response
- private ApexResponseMessage SendPollMessage()
- {
- // Replay the last message or build a new message
- var nextMessage = _apexState.LastMessage ?? GetNextHostMessage();
- var payload = nextMessage.Serialize();
-
- SerialProvider.Write(payload);
-
- // Device always responds with 11 bytes
- var deviceData = TryPortRead();
- var pollResponse = new ApexResponseMessage(deviceData);
-
- // Log the parsed command and response together for easier analysis
- Logger?.Trace("{0} poll message: {1}", GetType().Name, nextMessage);
- Logger?.Trace("{0} poll response: {1}", GetType().Name, pollResponse);
-
- // The response was invalid or incomplete
- if (!pollResponse.IsValid)
- {
- // Request retransmission (by not modifying the ACK and payload)
- _apexState.LastMessage = nextMessage;
-
- // Update counter for responsive check
- if (pollResponse.IsEmptyResponse)
- {
- _apexState.NonResponsiveCount++;
- }
-
- // If there is a protocol violation and strict mode is enabled,
- // report the problem and request a retransmit
- if (pollResponse.HasProtocolViolation && Config.StrictMode)
- {
- Logger?.Error("{0} Invalid message: {1}", GetType().Name, deviceData.ToHexString());
- Logger?.Error("{0} Problems: {1}", GetType().Name,
- string.Join(Environment.NewLine, pollResponse.AllPacketIssues));
- return null;
- }
-
- // Target is responsive, this is just a bad message
- if (!IsUnresponsive)
- {
- return null;
- }
-
- // Target is unresponsive, check if client needs to be notified
- if (!_apexState.NotifiedLostConnection)
- {
- LostConnection();
-
- _apexState.NotifiedLostConnection = true;
- }
-
- Logger?.Trace("{0} Device is not responding", GetType().Name);
-
- return null;
- }
-
- // If ACK does not match, then device is requesting a retransmit
- if (nextMessage.Ack != pollResponse.Ack)
- {
- // Request retransmission
- _apexState.LastMessage = nextMessage;
-
- return null;
- }
-
- // Update state with successful polling notification
- _apexState.RegisterSuccessfulPoll();
-
- return pollResponse;
- }
-
- ///
- /// Perform escrow actions
- ///
- /// Poll response from device
- /// If not in escrow mode no actions will be performed
- private void HandleEscrow(Rs232ResponseMessage pollResponse)
- {
- if (!Config.IsEscrowMode || !pollResponse.State.HasFlag(Rs232State.Escrowed))
- {
- return;
- }
-
- // Handle escrow state
- if (!pollResponse.CreditIndex.HasValue)
- {
- Logger?.Error("{0} Escrow state entered without a credit message", GetType().Name);
- }
- else
- {
- BillInEscrow(pollResponse.CreditIndex.Value);
- }
- }
-
- ///
- /// Handle state change actions
- ///
- /// Poll response from device
- private void HandleState(Rs232ResponseMessage pollResponse)
- {
- // Report on any state change
- if (_apexState.LastState == pollResponse.State)
- {
- return;
- }
-
- var args = new StateChangeArgs(_apexState.LastState, pollResponse.State);
- StateChanged(args);
-
- Logger?.Info("{0} Entering state: {1}", GetType().Name, pollResponse.State);
-
- _apexState.LastState = pollResponse.State;
- }
-
- ///
- /// Handle event notifications
- ///
- /// Poll response from device
- private void HandleEvents(Rs232ResponseMessage pollResponse)
- {
- // Report on any active events
- if (pollResponse.Event == Rs232Event.None)
- {
- return;
- }
-
- Logger?.Info("{0} Setting events(s): {1}", GetType().Name, pollResponse.Event);
-
- EventReported(pollResponse.Event);
- }
-
- ///
- /// Handle cash box notifications
- ///
- /// Poll response from device
- private void HandleCashBox(Rs232ResponseMessage pollResponse)
- {
- IsCashBoxPresent = pollResponse.IsCashBoxPresent;
-
- if (pollResponse.IsCashBoxPresent)
- {
- // Only report an attached cash box if we've reported it missing
- if (!_apexState.CashBoxAttachedReported && _apexState.CashBoxRemovalReported)
- {
- Logger?.Info("{0} Reporting cash box attached", GetType().Name);
-
- CashBoxAttached();
-
- _apexState.CashBoxAttachedReported = true;
- }
-
- // Clear the cash box removal flag so the next removal can raise and event
- _apexState.CashBoxRemovalReported = false;
- }
- else
- {
- if (!_apexState.CashBoxRemovalReported)
- {
- Logger?.Info("{0} Reporting cash box removed", GetType().Name);
-
- CashBoxRemoved();
-
- _apexState.CashBoxRemovalReported = true;
- }
-
- // Clear the cash box attached flag so the next attachment can raise and event
- _apexState.CashBoxAttachedReported = false;
- }
- }
-
- ///
- /// Handle credit notifications
- ///
- /// Poll response from device
- private void HandleCredit(Rs232ResponseMessage pollResponse)
- {
- // Report any available credit
- if (!pollResponse.Event.HasFlag(Rs232Event.Stacked))
- {
- return;
- }
-
- if (!pollResponse.CreditIndex.HasValue)
- {
- Logger?.Error("{0} Stack event issued without a credit message", GetType().Name);
- }
- else
- {
- Logger?.Info("{0} Reporting credit index: {1}", GetType().Name, pollResponse.CreditIndex);
- CreditIndexReported(pollResponse.CreditIndex.Value);
- }
- }
-
- ///
- /// Build the next message to send based on our current state
- ///
- ///
- private Rs232BaseMessage GetNextHostMessage()
- {
- var nextMessage = new Rs232PollMessage(_apexState.Ack)
- .SetEnableMask(Config.EnableMask)
- .SetStack(_apexState.StackNext)
- .SetReturn(_apexState.ReturnNext)
- .SetEscrowMode(Config.IsEscrowMode);
-
- // Clear the stack and return bits if set
- _apexState.StackNext = false;
- _apexState.ReturnNext = false;
-
- return nextMessage;
- }
-
- ///
- /// Read from serial provider with retry
- ///
- /// Data from port or null on error
- private byte[] TryPortRead()
- {
- var backoff = TimeSpan.Zero;
- var retry = 3;
- byte[] deviceData;
- do
- {
- // Device always responds with 11 bytes
- deviceData = SerialProvider.Read(11);
-
- if (backoff != TimeSpan.Zero)
- {
- Thread.Sleep(backoff);
- }
-
- backoff += TimeSpan.FromMilliseconds(50);
- } while (deviceData is null && retry-- > 0);
-
- return deviceData;
- }
- }
-
- ///
- /// Holds all the state used between polling messages
- ///
- internal struct ApexState
- {
- ///
- /// Bit is toggled on every successful message.
- /// When the bit is *not* toggled that signals that
- /// a retransmission is being requested by either
- /// the host or the device.
- ///
- public bool Ack;
-
- ///
- /// Last state reported by device
- ///
- public Rs232State LastState;
-
- ///
- /// Don't spam the cash box removal event
- /// When set, the event has already been raised
- /// and the device has not reported the cash box
- /// to have returned.
- ///
- public bool CashBoxRemovalReported;
-
- ///
- /// Don't spam the cash box attached event
- /// When set, the event has already been raised
- /// and the device is currently reporting the
- /// cash box as present.
- ///
- public bool CashBoxAttachedReported;
-
- ///
- /// When set, the stack flag will be set in the next polling message
- ///
- public bool StackNext;
-
- ///
- /// When set, the return flag will be set in the next polling message
- ///
- public bool ReturnNext;
-
- ///
- /// Last polling message sent to device
- /// Used for retransmission
- ///
- public Rs232BaseMessage LastMessage;
-
- ///
- /// Count of consecutive busy/non-response messages
- ///
- public int NonResponsiveCount;
-
- ///
- /// When true, the client has been notified of the lost connection
- ///
- public bool NotifiedLostConnection;
-
- ///
- /// Stores the enable mask for pause/resume API
- /// If null, the acceptor is not not paused
- ///
- public byte? PreviousEnableMask;
-
- ///
- /// Toggles the ack state and clears responsive trackers
- ///
- public void RegisterSuccessfulPoll()
- {
- Ack = !Ack;
- LastMessage = null;
-
- // Reset the responsiveness trackers
- NonResponsiveCount = 0;
- NotifiedLostConnection = false;
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/BaseValidator.cs b/PTI.Rs232Validator/BaseValidator.cs
deleted file mode 100644
index 336297b..0000000
--- a/PTI.Rs232Validator/BaseValidator.cs
+++ /dev/null
@@ -1,376 +0,0 @@
-namespace PTI.Rs232Validator
-{
- using System;
- using System.Threading;
- using Providers;
-
- ///
- /// Base implementation common to all validators
- ///
- public abstract class BaseValidator : IDisposable
- {
- private readonly object _mutex = new object();
-
- ///
- /// Serial provider instance
- ///
- protected readonly ISerialProvider SerialProvider;
-
- ///
- /// Event is triggered after a number of polling
- /// cycles to assert that the device is operating
- /// normally.
- ///
- private readonly CounterEvent _deviceIsReady;
-
- private bool _isRunning;
- private Thread _rs232Worker;
-
- ///
- /// Create a new base validator
- ///
- /// Configuration to use
- protected BaseValidator(Rs232Config config)
- {
- Config = config ?? throw new ArgumentNullException(nameof(config));
- SerialProvider = config.SerialProvider;
-
- // All services will use this logger instance
- Config.Logger ??= new NullLogger();
- SerialProvider.Logger = Config.Logger;
- Logger = Config.Logger;
-
- // Wait for this many polls before saying the acceptor is online
- _deviceIsReady = new CounterEvent(2);
-
- Logger?.Info("{0} Created new validator: {1}", GetType().Name, config);
- }
-
- ///
- /// Gets the active RS-232 configuration
- /// You cannot change the configuration of a running
- /// validator.
- ///
- public Rs232Config Config { get; }
-
- ///
- /// Optional logger to attach to this acceptor
- ///
- protected ILogger Logger { get; }
-
- ///
- public void Dispose()
- {
- SerialProvider?.Dispose();
-
- Logger?.Trace("{0} Validator disposed", GetType().Name);
- }
-
- ///
- /// Raised when the state of the bill acceptor changes
- ///
- public event EventHandler OnStateChanged;
-
- ///
- /// Raised when one ore more events are reported by the device
- ///
- public event EventHandler OnEventReported;
-
- ///
- /// Raised when credit is reported. The reported
- /// value is the RS232 credit index.
- ///
- public event EventHandler OnCreditIndexReported;
-
- ///
- /// Raised when a bill is being help in escrow.
- /// The reported value is the RS232 credit index.
- /// This event is raised while the device is holding
- /// the bill in escrow. In other words, this event
- /// may be raised multiple times. Use the event
- /// event to obtain
- /// the final credit-issue notification.
- ///
- /// Only raised in escrow mode
- public event EventHandler OnBillInEscrow;
-
- ///
- /// Raised when the cash box is removed from validator.
- /// You may also poll .
- ///
- public event EventHandler OnCashBoxRemoved;
-
- ///
- /// Raised when the cash box is attached to the validator.
- /// You may also poll .
- /// This event will only be raised once the cash box
- /// has been reported as missing for the first time.
- /// Otherwise, you would see this event at every startup.
- ///
- public event EventHandler OnCashBoxAttached;
-
- ///
- /// Raised when the API suspects the device connection has
- /// been lost. You can use this to be notified of a lost
- /// connection or you can poll .
- ///
- public event EventHandler OnLostConnection;
-
- ///
- /// Attempt to start the RS232 polling loop
- ///
- /// True when loop starts
- public bool StartPollingLoop()
- {
- lock (_mutex)
- {
- if (_isRunning)
- {
- Logger?.Error("{0} Already polling, ignoring start request", GetType().Name);
- return false;
- }
-
- if (!SerialProvider.TryOpen())
- {
- Logger?.Error("{0} Failed to open serial provider", GetType().Name);
- return false;
- }
-
- _isRunning = true;
- }
-
- _rs232Worker = new Thread(MainLoop)
- {
- // Terminate if our parent thread dies
- IsBackground = true
- };
-
- _rs232Worker.Start();
-
- if (Config.DisableLivenessCheck)
- {
- Logger?.Info("{0} Polling thread started (no liveness check): {1}", GetType().Name,
- _rs232Worker.ManagedThreadId);
- }
- else
- {
- // RS-232 does not have a "ping" concept so instead we wait for a
- // number of healthy messages before telling the caller that the
- // message loop has "started successfully".
- if (!_deviceIsReady.WaitOne(Config.PollingPeriod._Multiply(5)))
- {
- Logger?.Info("{0} timed out waiting for a valid polling response", GetType().Name);
- return false;
- }
-
- Logger?.Info("{0} Polling thread started: {1}", GetType().Name, _rs232Worker.ManagedThreadId);
- }
-
- return true;
- }
-
- ///
- /// Stop the RS232 polling loop
- ///
- public void StopPollingLoop()
- {
- lock (_mutex)
- {
- if (!_isRunning)
- {
- Logger?.Error("{0} Polling loop is not running, ignoring stop command", GetType().Name);
- return;
- }
-
- _isRunning = false;
- }
-
- Logger?.Debug("{0} Stopping polling loop...", GetType().Name);
-
- if (!_rs232Worker.Join(TimeSpan.FromSeconds(10)))
- {
- Logger?.Error("{0} Failed to stop polling loop", GetType().Name);
- }
- else
- {
- Logger?.Info("{0} Polling loop stopped", GetType().Name);
- }
-
- try
- {
- SerialProvider.Close();
- }
- catch (Exception ex)
- {
- Logger?.Error("{0} Unable to close serial provider: {1}", GetType().Name, ex.Message);
- }
- }
-
- ///
- /// Perform escrow stack
- ///
- /// Configuration must specify Escrow Mode for this to work
- public void Stack()
- {
- if (!Config.IsEscrowMode)
- {
- Logger.Error("{0} Cannot manually issue stack command in non-escrow mode", GetType().Name);
- return;
- }
-
- DoStack();
- }
-
- ///
- /// Perform escrow return
- ///
- /// Configuration must specify Escrow Mode for this to work
- public void Return()
- {
- if (!Config.IsEscrowMode)
- {
- Logger.Error("{0} Cannot manually issue return command in non-escrow mode", GetType().Name);
- return;
- }
-
- DoReturn();
- }
-
- ///
- /// Disables the bill acceptor within the time period defined by the poll rate.
- /// The poll rate is the maximum time between poll packets from host to device.
- /// This tells the acceptor to stop accepting bills but keep reporting status.
- /// The acceptor's lights will turn off after this call takes effect.
- ///
- ///
- /// The command will take up to to take effect.
- public abstract void PauseAcceptance();
-
- ///
- /// Returns the acceptor to bill accepting mode.
- /// This command has no effect if the acceptor is already running and accepting.
- /// The acceptor's lights will turn on after this command takes effect.
- ///
- ///
- /// The command will take up to to take effect.
- public abstract void ResumeAcceptance();
-
- ///
- /// Returns true if acceptance is current paused
- ///
- ///
- public abstract bool IsPaused { get; }
-
- ///
- /// Returns true if the API thinks the device has stopped responding
- ///
- public abstract bool IsUnresponsive { get; }
-
- ///
- /// Returns true if cash box is present.
- /// will notify you
- /// if the cash box is removed.
- ///
- public bool IsCashBoxPresent { get; protected set; }
-
- ///
- /// Main loop thread
- ///
- private void MainLoop()
- {
- while (true)
- {
- lock (_mutex)
- {
- if (!_isRunning)
- {
- Logger?.Debug("{0} MainLoop received stop signal", GetType().Name);
- break;
- }
- }
-
- if (PollDevice())
- {
- _deviceIsReady.Set();
- }
-
- Thread.Sleep(Config.PollingPeriod);
- }
- }
-
- ///
- /// Send next polling message and parse the response
- /// This will trigger events for State, Event, and Credit messages
- ///
- /// true if polling loop transmits and receives without fault
- protected abstract bool PollDevice();
-
- ///
- /// Perform escrow stack function
- ///
- protected abstract void DoStack();
-
- ///
- /// Perform escrow return function
- ///
- protected abstract void DoReturn();
-
- ///
- /// Raise the state change event
- ///
- protected void StateChanged(StateChangeArgs args)
- {
- OnStateChanged?.Invoke(this, args);
- }
-
- ///
- /// Raise the event reported event
- ///
- protected void EventReported(Rs232Event evt)
- {
- OnEventReported?.Invoke(this, evt);
- }
-
- ///
- /// Raise the credit event
- ///
- /// Bill index
- protected void CreditIndexReported(int index)
- {
- OnCreditIndexReported?.Invoke(this, index);
- }
-
- ///
- /// Raise the bill in escrow event
- ///
- /// Bill index
- protected void BillInEscrow(int index)
- {
- OnBillInEscrow?.Invoke(this, index);
- }
-
- ///
- /// Raise cash box removed event
- ///
- protected void CashBoxRemoved()
- {
- OnCashBoxRemoved?.Invoke(this, EventArgs.Empty);
- }
-
- ///
- /// Raise cash box attached event
- ///
- protected void CashBoxAttached()
- {
- OnCashBoxAttached?.Invoke(this, EventArgs.Empty);
- }
-
- ///
- /// Raise the lost connection event
- ///
- protected void LostConnection()
- {
- OnLostConnection?.Invoke(this, EventArgs.Empty);
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/BillValidators/BillValidator.Extended.cs b/PTI.Rs232Validator/BillValidators/BillValidator.Extended.cs
new file mode 100644
index 0000000..f6e3d4e
--- /dev/null
+++ b/PTI.Rs232Validator/BillValidators/BillValidator.Extended.cs
@@ -0,0 +1,33 @@
+using PTI.Rs232Validator.Messages.Commands;
+using PTI.Rs232Validator.Messages.Requests;
+using PTI.Rs232Validator.Messages.Responses.Extended;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace PTI.Rs232Validator.BillValidators;
+
+public partial class BillValidator
+{
+ ///
+ /// Gets the last detected barcode after a power cycle.
+ ///
+ ///
+ /// An instance of with
+ /// set to true if successful.
+ ///
+ /// The work is queued on the thread pool.
+ public async Task GetDetectedBarcode()
+ {
+ return await SendExtendedMessageAsync(ExtendedCommand.BarcodeDetected, [],
+ payload => new BarcodeDetectedResponseMessage(payload));
+ }
+
+ private async Task SendExtendedMessageAsync(ExtendedCommand command,
+ IReadOnlyList requestData, Func, TResponseMessage> createResponseMessage)
+ where TResponseMessage : ExtendedResponseMessage
+ {
+ return await SendNonPollMessageAsync(
+ ack => new ExtendedRequestMessage(ack, command, requestData), createResponseMessage);
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/BillValidators/BillValidator.Telemetry.cs b/PTI.Rs232Validator/BillValidators/BillValidator.Telemetry.cs
new file mode 100644
index 0000000..18b1076
--- /dev/null
+++ b/PTI.Rs232Validator/BillValidators/BillValidator.Telemetry.cs
@@ -0,0 +1,160 @@
+using PTI.Rs232Validator.Messages.Commands;
+using PTI.Rs232Validator.Messages.Requests;
+using PTI.Rs232Validator.Messages.Responses.Telemetry;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace PTI.Rs232Validator.BillValidators;
+
+public partial class BillValidator
+{
+ ///
+ /// Pings the acceptor.
+ ///
+ ///
+ /// An instance of with
+ /// set to true if successful.
+ ///
+ /// The work is queued on the thread pool.
+ public async Task PingAsync()
+ {
+ return await SendTelemetryMessageAsync(TelemetryCommand.Ping, [],
+ payload => new TelemetryResponseMessage(payload));
+ }
+
+ ///
+ /// Gets the serial number assigned to the acceptor.
+ ///
+ ///
+ /// An instance of with
+ /// set to true if successful.
+ ///
+ /// The work is queued on the thread pool.
+ public async Task GetSerialNumberAsync()
+ {
+ return await SendTelemetryMessageAsync(TelemetryCommand.GetSerialNumber, [],
+ payload => new GetSerialNumberResponseMessage(payload));
+ }
+
+ ///
+ /// Gets the telemetry metrics about the cashbox.
+ ///
+ ///
+ /// An instance of with
+ /// set to true if successful.
+ ///
+ /// The work is queued on the thread pool.
+ public async Task GetCashboxMetrics()
+ {
+ return await SendTelemetryMessageAsync(TelemetryCommand.GetCashboxMetrics, [],
+ payload => new GetCashboxMetricsResponseMessage(payload));
+ }
+
+ ///
+ /// Clears the count of bills in the cashbox.
+ ///
+ ///
+ /// An instance of with
+ /// set to true if successful.
+ ///
+ /// The work is queued on the thread pool.
+ public async Task ClearCashboxCount()
+ {
+ return await SendTelemetryMessageAsync(TelemetryCommand.ClearCashboxCount, [],
+ payload => new TelemetryResponseMessage(payload));
+ }
+
+ ///
+ /// Gets the general telemetry metrics for an acceptor.
+ ///
+ ///
+ /// An instance of with
+ /// set to true if successful.
+ ///
+ /// The work is queued on the thread pool.
+ public async Task GetUnitMetrics()
+ {
+ return await SendTelemetryMessageAsync(TelemetryCommand.GetUnitMetrics, [],
+ payload => new GetUnitMetricsResponseMessage(payload));
+ }
+
+ ///
+ /// Gets the telemetry metrics since the last time an acceptor was serviced.
+ ///
+ ///
+ /// An instance of with
+ /// set to true if successful.
+ ///
+ /// The work is queued on the thread pool.
+ public async Task GetServiceUsageCounters()
+ {
+ return await SendTelemetryMessageAsync(TelemetryCommand.GetServiceUsageCounters, [],
+ payload => new GetServiceUsageCountersResponseMessage(payload));
+ }
+
+ ///
+ /// Gets the flags about what needs to be serviced.
+ ///
+ ///
+ /// An instance of with
+ /// set to true if successful.
+ ///
+ /// The work is queued on the thread pool.
+ public async Task GetServiceFlags()
+ {
+ return await SendTelemetryMessageAsync(TelemetryCommand.GetServiceFlags, [],
+ payload => new GetServiceFlagsResponseMessage(payload));
+ }
+
+ ///
+ /// Clears 1 or more service flags.
+ ///
+ /// The component to clear the service flag for.
+ ///
+ /// An instance of with
+ /// set to true if successful.
+ ///
+ /// The work is queued on the thread pool.
+ public async Task ClearServiceFlags(CorrectableComponent correctableComponent)
+ {
+ return await SendTelemetryMessageAsync(TelemetryCommand.ClearServiceFlags,
+ [(byte)correctableComponent], payload => new TelemetryResponseMessage(payload));
+ }
+
+ ///
+ /// Gets the info that was attached to the last service.
+ ///
+ ///
+ /// An instance of with
+ /// set to true if successful.
+ ///
+ /// The work is queued on the thread pool.
+ public async Task GetServiceInfo()
+ {
+ return await SendTelemetryMessageAsync(TelemetryCommand.GetServiceInfo, [],
+ payload => new GetServiceInfoResponseMessage(payload));
+ }
+
+ ///
+ /// Gets the telemetry metrics that pertain to an acceptor's firmware.
+ ///
+ ///
+ /// An instance of with
+ /// set to true if successful.
+ ///
+ /// The work is queued on the thread pool.
+ public async Task GetFirmwareMetrics()
+ {
+ return await SendTelemetryMessageAsync(TelemetryCommand.GetFirmwareMetrics, [],
+ payload => new GetFirmwareMetricsResponseMessage(payload));
+ }
+
+ private async Task SendTelemetryMessageAsync(TelemetryCommand command,
+ IReadOnlyList requestData, Func, TResponseMessage> createResponseMessage)
+ where TResponseMessage : TelemetryResponseMessage
+ {
+ return await SendNonPollMessageAsync(
+ ack => new TelemetryRequestMessage(ack, command, requestData), createResponseMessage);
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/BillValidators/BillValidator.cs b/PTI.Rs232Validator/BillValidators/BillValidator.cs
new file mode 100644
index 0000000..3991be2
--- /dev/null
+++ b/PTI.Rs232Validator/BillValidators/BillValidator.cs
@@ -0,0 +1,662 @@
+using PTI.Rs232Validator.Loggers;
+using PTI.Rs232Validator.Messages;
+using PTI.Rs232Validator.Messages.Commands;
+using PTI.Rs232Validator.Messages.Requests;
+using PTI.Rs232Validator.Messages.Responses;
+using PTI.Rs232Validator.Messages.Responses.Extended;
+using PTI.Rs232Validator.SerialProviders;
+using PTI.Rs232Validator.Utility;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace PTI.Rs232Validator.BillValidators;
+
+///
+/// A hardware connection to a bill acceptor.
+///
+public partial class BillValidator : IDisposable
+{
+ internal const byte SuccessfulPollsRequiredToStartPollingLoop = 2;
+ internal const byte MaxReadAttempts = 4;
+ private const byte MaxIncorrectPayloadPardons = 2;
+ private static readonly TimeSpan BackoffIncrement = TimeSpan.FromMilliseconds(50);
+ private static readonly TimeSpan StopLoopTimeout = TimeSpan.FromSeconds(3);
+
+ private readonly ILogger _logger;
+ private readonly ISerialProvider _serialProvider;
+ private readonly object _mutex = new();
+
+ // A message callback sends a message to the acceptor and returns true if the message can be discarded
+ // (i.e. the message should NOT be sent again).
+ private readonly Queue> _messageCallbacks = new();
+ private Func? _lastMessageCallback;
+
+ private Task _worker = Task.CompletedTask;
+ private bool _isPolling;
+ private bool _lastAck;
+ private Rs232State _state;
+ private bool _shouldRequestBillStack;
+ private bool _shouldRequestBillReturn;
+ private bool _wasCashboxAttachmentReported;
+ private bool _wasCashboxRemovalReported;
+ private bool _wasEscrowedBillReported;
+ private bool _wasBarcodeDetectionReported;
+ private bool _wasConnectionLostReported;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public BillValidator(ILogger logger, ISerialProvider serialProvider, Rs232Configuration configuration)
+ {
+ _logger = logger;
+ _serialProvider = serialProvider;
+ Configuration = configuration;
+ }
+
+ ///
+ /// An event that is raised when an attempt to communicate with the acceptor is carried out.
+ ///
+ public event EventHandler? OnCommunicationAttempted;
+
+ ///
+ /// An event that is raised when the state of the acceptor changes.
+ ///
+ public event EventHandler? OnStateChanged;
+
+ ///
+ /// An event that is raised when 1 or more events are reported by the acceptor.
+ ///
+ public event EventHandler? OnEventReported;
+
+ ///
+ /// An event that is raised when the cashbox is attached.
+ ///
+ public event EventHandler? OnCashboxAttached;
+
+ ///
+ /// An event that is raised when the cashbox is removed.
+ ///
+ public event EventHandler? OnCashboxRemoved;
+
+ ///
+ /// An event that is raised when a bill is stacked.
+ ///
+ public event EventHandler? OnBillStacked;
+
+ ///
+ /// An event that is raised when a bill is escrowed.
+ ///
+ public event EventHandler? OnBillEscrowed;
+
+ ///
+ /// An event that is raised when a barcode is detected.
+ ///
+ public event EventHandler? OnBarcodeDetected;
+
+ ///
+ /// An event that is raised when the connection to the acceptor seems to be lost.
+ ///
+ public event EventHandler? OnConnectionLost;
+
+ ///
+ public Rs232Configuration Configuration { get; }
+
+ ///
+ /// Is the connection to the acceptor present?
+ ///
+ public bool IsConnectionPresent { get; private set; }
+
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ _serialProvider.Dispose();
+ }
+
+ ///
+ /// Starts the RS-232 polling loop.
+ ///
+ /// True if the polling loop starts; otherwise, false.
+ public bool StartPollingLoop()
+ {
+ lock (_mutex)
+ {
+ if (_isPolling)
+ {
+ _logger.LogDebug("The polling loop is running, so ignoring the start request.");
+ return false;
+ }
+ }
+
+ if (!TryOpenPort())
+ {
+ return false;
+ }
+
+ if (!CheckForDevice())
+ {
+ ClosePort();
+ return false;
+ }
+
+ lock (_mutex)
+ {
+ _isPolling = true;
+ }
+
+ _worker = Task.Factory.StartNew(LoopPollMessages, TaskCreationOptions.LongRunning);
+ IsConnectionPresent = true;
+ return true;
+ }
+
+ ///
+ /// Stops the RS-232 polling loop.
+ ///
+ public void StopPollingLoop()
+ {
+ lock (_mutex)
+ {
+ if (!_isPolling)
+ {
+ _logger.LogDebug("The polling loop is not running, so ignoring the stop request.");
+ return;
+ }
+
+ _isPolling = false;
+ }
+
+ _logger.LogDebug("Stopping the polling loop...");
+
+ if (_worker.Wait(StopLoopTimeout))
+ {
+ _logger.LogDebug("Stopped the polling loop.");
+ }
+ else
+ {
+ _logger.LogError("Failed to stop the polling loop.");
+ }
+
+ _messageCallbacks.Clear();
+ _lastMessageCallback = null;
+ _shouldRequestBillStack = false;
+ _shouldRequestBillReturn = false;
+ _wasCashboxAttachmentReported = false;
+ _wasCashboxRemovalReported = false;
+ _wasConnectionLostReported = false;
+ IsConnectionPresent = false;
+
+ ClosePort();
+ }
+
+ ///
+ /// Stacks a bill in escrow.
+ ///
+ public void StackBill()
+ {
+ lock (_mutex)
+ {
+ if (_state != Rs232State.Escrowed)
+ {
+ _logger.LogDebug("Cannot stack a bill that is not in escrow.");
+ return;
+ }
+ }
+
+ lock (_mutex)
+ {
+ _shouldRequestBillStack = true;
+ }
+ }
+
+ ///
+ /// Returns a bill in escrow.
+ ///
+ public void ReturnBill()
+ {
+ lock (_mutex)
+ {
+ if (_state != Rs232State.Escrowed)
+ {
+ _logger.LogDebug("Cannot return a bill that is not in escrow.");
+ return;
+ }
+ }
+
+ lock (_mutex)
+ {
+ _shouldRequestBillReturn = true;
+ }
+ }
+
+ ///
+ /// Sends an instance of , which should not be an instance of
+ /// , to the acceptor and returns an instance of
+ /// created from the response payload.
+ ///
+ internal async Task SendNonPollMessageAsync(
+ Func createRequestMessage,
+ Func, TResponseMessage> createResponseMessage)
+ where TResponseMessage : Rs232ResponseMessage
+ {
+ var eventWaitHandle = new ManualResetEvent(false);
+ var incorrectPayloadCount = 0;
+ TResponseMessage responseMessage = createResponseMessage([]);
+ var messageCallback = new Func(() =>
+ {
+ var messageRetrievalResult =
+ TrySendMessage(createRequestMessage, createResponseMessage, out responseMessage);
+ switch (messageRetrievalResult)
+ {
+ case MessageRetrievalResult.IncorrectPayload when ++incorrectPayloadCount <= MaxIncorrectPayloadPardons:
+ case MessageRetrievalResult.IncorrectAck:
+ return false;
+ }
+
+ if (messageRetrievalResult == MessageRetrievalResult.IncorrectPayload)
+ {
+ LogPayloadIssues(responseMessage);
+ }
+
+ eventWaitHandle.Set();
+ return true;
+ });
+
+ bool isPolling;
+ lock (_mutex)
+ {
+ isPolling = _isPolling;
+ }
+
+ if (isPolling)
+ {
+ EnqueueMessageCallback(messageCallback);
+ return await Task.Run(() =>
+ {
+ eventWaitHandle.WaitOne();
+ return responseMessage;
+ });
+ }
+
+ return await Task.Run(() =>
+ {
+ if (!TryOpenPort())
+ {
+ return responseMessage;
+ }
+
+ if (!CheckForDevice())
+ {
+ ClosePort();
+ return responseMessage;
+ }
+
+ while (!messageCallback.Invoke())
+ {
+ Thread.Sleep(Configuration.PollingPeriod);
+ }
+
+ ClosePort();
+ return responseMessage;
+ });
+ }
+
+ private void EnqueueMessageCallback(Func messageCallback)
+ {
+ lock (_mutex)
+ {
+ _messageCallbacks.Enqueue(messageCallback);
+ }
+ }
+
+ private Func? DequeueMessageCallback()
+ {
+ lock (_mutex)
+ {
+ if (_messageCallbacks.Count > 0)
+ {
+ return _messageCallbacks.Dequeue();
+ }
+ }
+
+ return null;
+ }
+
+ private bool TryOpenPort()
+ {
+ if (_serialProvider.TryOpen())
+ {
+ return true;
+ }
+
+ _logger.LogDebug("Failed to open the serial provider.");
+ return false;
+ }
+
+ private void ClosePort()
+ {
+ try
+ {
+ _serialProvider.Close();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError("Failed to close the serial provider: {0}", ex.Message);
+ }
+ }
+
+ private void LogPayloadIssues(Rs232ResponseMessage responseMessage)
+ where TResponseMessage : Rs232ResponseMessage
+ {
+ var payloadIssues = responseMessage.GetPayloadIssues();
+ if (!payloadIssues.Any())
+ {
+ return;
+ }
+
+ var errorMessage = "Received an invalid response for a {0}:";
+ var errorArgs = new object[payloadIssues.Count + 1];
+ errorArgs[0] = typeof(TResponseMessage).Name.AddSpacesToCamelCase();
+ for (var i = 0; i < payloadIssues.Count; i++)
+ {
+ errorMessage += $"\n\t{{{i + 1}}}";
+ errorArgs[i + 1] = payloadIssues[i];
+ }
+
+ _logger.LogError(errorMessage, errorArgs);
+ }
+
+ private MessageRetrievalResult TrySendMessage(
+ Func createRequestMessage,
+ Func, TResponseMessage> createResponseMessage, out TResponseMessage responseMessage)
+ where TResponseMessage : Rs232ResponseMessage
+ {
+ var requestMessage = createRequestMessage(!_lastAck);
+ var requestPayload = requestMessage.Payload.ToArray();
+
+ IReadOnlyList responsePayload = Array.Empty();
+ var backoffTime = Configuration.PollingPeriod;
+ for (var i = 0; i < MaxReadAttempts; i++)
+ {
+ _serialProvider.Write(requestPayload);
+
+ responsePayload = _serialProvider.Read(2);
+ if (responsePayload.Count == 2)
+ {
+ var remainingByteCount = (uint)(responsePayload[1] - 2);
+ responsePayload = responsePayload.Concat(_serialProvider.Read(remainingByteCount)).ToArray();
+ break;
+ }
+
+ Thread.Sleep(backoffTime);
+ backoffTime += BackoffIncrement;
+ }
+
+ responseMessage = createResponseMessage(responsePayload);
+ _logger.LogTrace("Sent data to acceptor: {0}", requestMessage.Payload.ConvertToHexString(true, false));
+ _logger.LogTrace("Received data from acceptor: {0}", responseMessage.Payload.ConvertToHexString(true, false));
+ OnCommunicationAttempted?.Invoke(this, new CommunicationAttemptedEventArgs(requestMessage, responseMessage));
+
+ if (responsePayload.Count == 0)
+ {
+ _logger.LogDebug("Experienced a communication timeout.");
+ if (!_wasConnectionLostReported)
+ {
+ OnConnectionLost?.Invoke(this, EventArgs.Empty);
+ _wasConnectionLostReported = true;
+ }
+
+ IsConnectionPresent = false;
+ return MessageRetrievalResult.Timeout;
+ }
+
+ _wasConnectionLostReported = false;
+ IsConnectionPresent = true;
+
+ if (!responseMessage.IsValid)
+ {
+ return MessageRetrievalResult.IncorrectPayload;
+ }
+
+ if (requestMessage.Ack != responseMessage.Ack)
+ {
+ return MessageRetrievalResult.IncorrectAck;
+ }
+
+ _lastAck = responseMessage.Ack;
+ return MessageRetrievalResult.Success;
+ }
+
+ private bool TrySendPollMessage(Func createPollRequestMessage)
+ {
+ var messageRetrievalResult = TrySendMessage(createPollRequestMessage,
+ payload =>
+ {
+ var pollResponseMessage = new PollResponseMessage(payload);
+ if (pollResponseMessage.GetPayloadIssues().Count == 0)
+ {
+ return pollResponseMessage;
+ }
+
+ var extendedResponseMessage = new ExtendedResponseMessage(payload);
+ if (extendedResponseMessage.GetPayloadIssues().Count == 0)
+ {
+ return extendedResponseMessage;
+ }
+
+ return pollResponseMessage;
+ }, out var responseMessage);
+ if (messageRetrievalResult != MessageRetrievalResult.Success)
+ {
+ if (messageRetrievalResult == MessageRetrievalResult.IncorrectPayload)
+ {
+ LogPayloadIssues(responseMessage);
+ }
+
+ return false;
+ }
+
+ if (responseMessage.State != _state)
+ {
+ _logger.LogDebug("The state changed from {0} to {1}.", _state, responseMessage.State);
+ OnStateChanged?.Invoke(this, new StateChangedEventArgs(_state, responseMessage.State));
+
+ lock (_mutex)
+ {
+ _state = responseMessage.State;
+ }
+ }
+
+ if (responseMessage.Event != Rs232Event.None)
+ {
+ _logger.LogDebug("Received event(s): {0}.", responseMessage.Event);
+ OnEventReported?.Invoke(this, responseMessage.Event);
+ }
+
+ if (responseMessage.IsCashboxPresent && !_wasCashboxAttachmentReported)
+ {
+ _logger.LogDebug("The cashbox was attached.");
+ OnCashboxAttached?.Invoke(this, EventArgs.Empty);
+ _wasCashboxAttachmentReported = true;
+ _wasCashboxRemovalReported = false;
+ }
+
+ if (!responseMessage.IsCashboxPresent && !_wasCashboxRemovalReported)
+ {
+ _logger.LogDebug("The cashbox was removed.");
+ OnCashboxRemoved?.Invoke(this, EventArgs.Empty);
+ _wasCashboxRemovalReported = true;
+ _wasCashboxAttachmentReported = false;
+ }
+
+ if (responseMessage.Event.HasFlag(Rs232Event.Stacked))
+ {
+ if (responseMessage.BillType == 0)
+ {
+ _logger.LogError("Stacked an unknown bill.");
+ }
+ else
+ {
+ _logger.LogDebug("Stacked a bill of type {0}.", responseMessage.BillType);
+ }
+
+ OnBillStacked?.Invoke(this, responseMessage.BillType);
+ }
+
+ if (responseMessage.State == Rs232State.Escrowed && !_wasEscrowedBillReported)
+ {
+ if (responseMessage.BillType == 0)
+ {
+ _logger.LogError("Escrowed an unknown bill.");
+ }
+ else
+ {
+ _logger.LogDebug("Escrowed a bill of type {0}.", responseMessage.BillType);
+ }
+
+ OnBillEscrowed?.Invoke(this, responseMessage.BillType);
+ _wasEscrowedBillReported = true;
+ }
+
+ if (responseMessage.State != Rs232State.Escrowed)
+ {
+ lock (_mutex)
+ {
+ _shouldRequestBillStack = false;
+ _shouldRequestBillReturn = false;
+ }
+
+ _wasEscrowedBillReported = false;
+ _wasBarcodeDetectionReported = false;
+ }
+
+ if (responseMessage.MessageType == Rs232MessageType.ExtendedCommand)
+ {
+ _logger.LogDebug("Received an extended response message.");
+ var extendedResponseMessage = (ExtendedResponseMessage)responseMessage;
+ switch (extendedResponseMessage.Command)
+ {
+ case ExtendedCommand.BarcodeDetected:
+ var barcodeDetectedResponseMessage =
+ new BarcodeDetectedResponseMessage(extendedResponseMessage.Payload);
+ if (!barcodeDetectedResponseMessage.IsValid)
+ {
+ LogPayloadIssues(barcodeDetectedResponseMessage);
+ return false;
+ }
+
+ if (!_wasBarcodeDetectionReported)
+ {
+ _logger.LogDebug("Detected a barcode: {0}", barcodeDetectedResponseMessage.Barcode);
+ OnBarcodeDetected?.Invoke(this, barcodeDetectedResponseMessage.Barcode);
+ _wasBarcodeDetectionReported = true;
+ }
+
+ break;
+ default:
+ _logger.LogError("Received an unknown extended command: {0}.", extendedResponseMessage.Command);
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ private bool CheckForDevice()
+ {
+ var successfulPolls = 0;
+ var wasAckFlipped = false;
+ while (successfulPolls < SuccessfulPollsRequiredToStartPollingLoop)
+ {
+ var messageRetrievalResult = TrySendMessage(
+ ack => new PollRequestMessage(ack),
+ payload => new PollResponseMessage(payload),
+ out var pollResponseMessage);
+
+ if (messageRetrievalResult != MessageRetrievalResult.Success)
+ {
+ if (messageRetrievalResult != MessageRetrievalResult.IncorrectPayload)
+ {
+ return false;
+ }
+
+ if (wasAckFlipped)
+ {
+ LogPayloadIssues(pollResponseMessage);
+ return false;
+ }
+
+ wasAckFlipped = true;
+ _lastAck = !_lastAck;
+ continue;
+ }
+
+ successfulPolls++;
+ Thread.Sleep(Configuration.PollingPeriod);
+ }
+
+ return true;
+ }
+
+ private void LoopPollMessages()
+ {
+ while (true)
+ {
+ lock (_mutex)
+ {
+ if (!_isPolling)
+ {
+ _logger.LogDebug("Received the stop signal.");
+ return;
+ }
+ }
+
+ if (_lastMessageCallback is not null)
+ {
+ if (_lastMessageCallback.Invoke())
+ {
+ _lastMessageCallback = null;
+ }
+ }
+ else
+ {
+ var messageCallback = DequeueMessageCallback();
+ if (messageCallback is not null)
+ {
+ if (!messageCallback.Invoke())
+ {
+ _lastMessageCallback = messageCallback;
+ }
+ }
+ else
+ {
+ messageCallback = () => TrySendPollMessage(ack =>
+ new PollRequestMessage(ack)
+ .SetEnableMask(Configuration.EnableMask)
+ .SetEscrowRequested(Configuration.ShouldEscrow
+ || _shouldRequestBillStack
+ || _shouldRequestBillReturn)
+ .SetStackRequested(_shouldRequestBillStack)
+ .SetReturnRequested(_shouldRequestBillReturn)
+ .SetBarcodeDetectionRequested(Configuration.ShouldDetectBarcodes));
+ if (!messageCallback())
+ {
+ _lastMessageCallback = messageCallback;
+ }
+ }
+ }
+
+ Thread.Sleep(Configuration.PollingPeriod);
+ }
+ }
+
+ private enum MessageRetrievalResult : byte
+ {
+ Success,
+ Timeout,
+ IncorrectAck,
+ IncorrectPayload
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/CommunicationAttemptedEventArgs.cs b/PTI.Rs232Validator/CommunicationAttemptedEventArgs.cs
new file mode 100644
index 0000000..71f6123
--- /dev/null
+++ b/PTI.Rs232Validator/CommunicationAttemptedEventArgs.cs
@@ -0,0 +1,35 @@
+using PTI.Rs232Validator.Messages.Requests;
+using PTI.Rs232Validator.Messages.Responses;
+using System;
+
+namespace PTI.Rs232Validator;
+
+///
+/// An implementation of that contains info about an attempt to communicate with an acceptor.
+///
+public class CommunicationAttemptedEventArgs : EventArgs
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// .
+ /// .
+ public CommunicationAttemptedEventArgs(Rs232RequestMessage requestMessage, Rs232ResponseMessage responseMessage)
+ {
+ RequestMessage = requestMessage;
+ ResponseMessage = responseMessage;
+ }
+
+ ///
+ /// An instance of , the of which was
+ /// sent to the acceptor.
+ ///
+ public Rs232RequestMessage RequestMessage { get; }
+
+ ///
+ /// An instance of , the of which was
+ /// either received from the acceptor or created as an empty collection due to a timeout.
+ ///
+ /// Consider checking of the instance.
+ public Rs232ResponseMessage ResponseMessage { get; }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/CorrectableComponent.cs b/PTI.Rs232Validator/CorrectableComponent.cs
new file mode 100644
index 0000000..efa0e39
--- /dev/null
+++ b/PTI.Rs232Validator/CorrectableComponent.cs
@@ -0,0 +1,42 @@
+namespace PTI.Rs232Validator;
+
+///
+/// The components of an acceptor that may require service.
+///
+public enum CorrectableComponent : byte
+{
+ ///
+ /// The tach sensor.
+ ///
+ TachSensor = 0,
+
+ ///
+ /// The bill path.
+ ///
+ BillPath = 1,
+
+ ///
+ /// The cashbox belt.
+ ///
+ CashboxBelt = 2,
+
+ ///
+ /// The cashbox stacking mechanism.
+ ///
+ CashboxMechanism = 3,
+
+ ///
+ /// The mechanical anti-stringing lever (MAS).
+ ///
+ MAS = 4,
+
+ ///
+ /// The spring rollers.
+ ///
+ SpringRollers = 5,
+
+ ///
+ /// All components.
+ ///
+ All = 0x7F
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/CounterEvent.cs b/PTI.Rs232Validator/CounterEvent.cs
deleted file mode 100644
index 3bd62e8..0000000
--- a/PTI.Rs232Validator/CounterEvent.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-namespace PTI.Rs232Validator
-{
- using System;
- using System.Threading;
-
- ///
- /// A single-use event that signals after the specified number of signals
- ///
- internal class CounterEvent
- {
- private readonly AutoResetEvent _event;
- private readonly int _signalAt;
- private int _counter;
- private bool _executed;
-
- ///
- /// Create a new counter event to signal after
- /// calls to
- ///
- /// Signal event after this many calls to Set
- public CounterEvent(int signalAt)
- {
- _signalAt = signalAt;
- _event = new AutoResetEvent(false);
- }
-
- ///
- /// Signal the event
- ///
- public void Set()
- {
- if (_executed)
- {
- return;
- }
-
- ++_counter;
-
- if (_counter != _signalAt)
- {
- return;
- }
-
- _event.Set();
- _executed = true;
- }
-
- ///
- /// Wait for event to be signalled
- ///
- /// Timeout
- /// true if event is signalled before timeout
- public bool WaitOne(TimeSpan timeSpan)
- {
- return _event.WaitOne(timeSpan);
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Extensions.cs b/PTI.Rs232Validator/Extensions.cs
deleted file mode 100644
index 1c09f39..0000000
--- a/PTI.Rs232Validator/Extensions.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-namespace PTI.Rs232Validator
-{
- using System;
- using System.Text;
-
- internal static class Extensions
- {
- ///
- /// Returns the data formatted as specified by the format string.
- ///
- /// byte[]
- /// delimiter such as command or tab
- /// Set true to prefix each byte with 0x
- ///
- public static string ToHexString(this byte[] arr, string delimiter = ", ", bool hexPrefix = false)
- {
- if (arr is null || arr.Length == 0)
- {
- return string.Empty;
- }
-
- var hex = new StringBuilder(arr.Length * 2);
-
- var prefix = string.Empty;
- if (hexPrefix)
- {
- prefix = "0x";
- }
-
- foreach (var b in arr)
- {
- hex.AppendFormat("{0}{1:X2}{2}", prefix, b, delimiter);
- }
-
- var result = hex.ToString().Trim().TrimEnd(delimiter.ToCharArray());
- return result;
- }
-
- ///
- /// Format byte value as binary
- /// 8 => 0b00001000
- ///
- /// Value to represent in binary
- /// Value as binary string
- public static string ToBinary(this byte b)
- {
- return Convert.ToString(b, 2).PadLeft(8, '0');
- }
-
- ///
- /// Multiple timespan by a constant value as timeSpan * factor
- ///
- /// This is a backwards compatibility feature for NET Framework
- /// Time span
- /// Factor
- public static TimeSpan _Multiply(this TimeSpan timeSpan, int factor)
- {
- if (factor <= 0)
- {
- throw new ArgumentException($"{nameof(factor)} must be > 0");
- }
- var result = timeSpan;
- for (var i = 0; i < factor; ++i)
- {
- result = result.Add(timeSpan);
- }
-
- return result;
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/ILogger.cs b/PTI.Rs232Validator/ILogger.cs
deleted file mode 100644
index 48b7864..0000000
--- a/PTI.Rs232Validator/ILogger.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-namespace PTI.Rs232Validator
-{
- ///
- /// Generic logging interface
- ///
- public interface ILogger
- {
- ///
- /// Log at trace level
- ///
- void Trace(string format, params object[] args);
-
- ///
- /// Log at trace level
- ///
- void Debug(string format, params object[] args);
-
- ///
- /// Log at trace level
- ///
- void Info(string format, params object[] args);
-
- ///
- /// Log at trace level
- ///
- void Error(string format, params object[] args);
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Loggers/ILogger.cs b/PTI.Rs232Validator/Loggers/ILogger.cs
new file mode 100644
index 0000000..0c44ec0
--- /dev/null
+++ b/PTI.Rs232Validator/Loggers/ILogger.cs
@@ -0,0 +1,58 @@
+namespace PTI.Rs232Validator.Loggers;
+
+///
+/// The levels of log messages.
+///
+public enum LogLevel
+{
+ ///
+ /// A trace message.
+ ///
+ Trace,
+
+ ///
+ /// A debug message.
+ ///
+ Debug,
+
+ ///
+ /// An info message.
+ ///
+ Info,
+
+ ///
+ /// An error message.
+ ///
+ Error
+}
+
+///
+/// A generic logging interface.
+///
+public interface ILogger
+{
+ ///
+ /// Logs a message at the trace level.
+ ///
+ /// The format of the message.
+ /// An array of objects to format.
+ public void LogTrace(string format, params object[] args);
+
+ ///
+ /// Logs a message at the debug level.
+ ///
+ ///
+ public void LogDebug(string format, params object[] args);
+
+ ///
+ /// Logs a message at the info level.
+ ///
+ ///
+ public void LogInfo(string format, params object[] args);
+
+ ///
+ /// Logs a message at the error level.
+ ///
+ ///
+ public void LogError(string format, params object[] args);
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Loggers/NamedLogger.cs b/PTI.Rs232Validator/Loggers/NamedLogger.cs
new file mode 100644
index 0000000..d7d53a6
--- /dev/null
+++ b/PTI.Rs232Validator/Loggers/NamedLogger.cs
@@ -0,0 +1,96 @@
+namespace PTI.Rs232Validator.Loggers;
+
+///
+/// An implementation of that has a name and logs certain messages.
+///
+public abstract class NamedLogger : ILogger
+{
+ ///
+ /// Creates a new instance of .
+ ///
+ /// .
+ /// .
+ protected NamedLogger(string name, LogLevel minLogLevel)
+ {
+ Name = name;
+ MinLogLevel = minLogLevel;
+ }
+
+ ///
+ /// The name of this instance.
+ ///
+ public string Name { get; }
+
+ ///
+ /// The minimum log level a message must be to be logged.
+ ///
+ public LogLevel MinLogLevel { get; }
+
+ ///
+ public void LogTrace(string format, params object[] args)
+ {
+ if (MinLogLevel > LogLevel.Trace)
+ {
+ return;
+ }
+
+ Log(Name, LogLevel.Trace, format, args);
+ }
+
+ ///
+ public void LogDebug(string format, params object[] args)
+ {
+ if (MinLogLevel > LogLevel.Debug)
+ {
+ return;
+ }
+
+ Log(Name, LogLevel.Debug, format, args);
+ }
+
+ ///
+ public void LogInfo(string format, params object[] args)
+ {
+ if (MinLogLevel > LogLevel.Info)
+ {
+ return;
+ }
+
+ Log(Name, LogLevel.Info, format, args);
+ }
+
+ ///
+ public void LogError(string format, params object[] args)
+ {
+ if (MinLogLevel > LogLevel.Error)
+ {
+ return;
+ }
+
+ Log(Name, LogLevel.Error, format, args);
+ }
+
+ ///
+ /// Logs a specified message at the specified log level.
+ ///
+ /// .
+ /// The log level of the message.
+ /// The format of the message.
+ /// An array of objects to format.
+ protected abstract void Log(string name, LogLevel logLevel, string format, params object[] args);
+}
+
+///
+/// An implementation of that uses the name of the generic type as the name of the logger.
+///
+/// The class to log for.
+public abstract class NamedLogger : NamedLogger where T : class
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// .
+ protected NamedLogger(LogLevel minLogLevel) : base(typeof(T).Name, minLogLevel)
+ {
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Loggers/NullLogger.cs b/PTI.Rs232Validator/Loggers/NullLogger.cs
new file mode 100644
index 0000000..17ce1a3
--- /dev/null
+++ b/PTI.Rs232Validator/Loggers/NullLogger.cs
@@ -0,0 +1,27 @@
+namespace PTI.Rs232Validator.Loggers;
+
+///
+/// An implementation of that does nothing.
+///
+public class NullLogger : ILogger
+{
+ ///
+ public void LogTrace(string format, params object[] args)
+ {
+ }
+
+ ///
+ public void LogDebug(string format, params object[] args)
+ {
+ }
+
+ ///
+ public void LogInfo(string format, params object[] args)
+ {
+ }
+
+ ///
+ public void LogError(string format, params object[] args)
+ {
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/ApexResponseMessage.cs b/PTI.Rs232Validator/Messages/ApexResponseMessage.cs
deleted file mode 100644
index 27ffe6b..0000000
--- a/PTI.Rs232Validator/Messages/ApexResponseMessage.cs
+++ /dev/null
@@ -1,196 +0,0 @@
-namespace PTI.Rs232Validator.Messages
-{
- using System.Collections.Generic;
- using System.Linq;
-
- ///
- /// Message from Apex in response to a host poll message
- ///
- internal class ApexResponseMessage : Rs232ResponseMessage
- {
- private const int CashBoxBit = 4;
- private static readonly int[] CreditBits = {3, 4, 5};
-
- ///
- /// State map keys by (byte, bit) into the payload.
- /// e.g.
- /// (1,2) => byte 1 bit 2 of payload
- ///
- private static readonly Dictionary<(byte, byte), Rs232State> StateMap =
- new Dictionary<(byte, byte), Rs232State>
- {
- {(0, 0), Rs232State.Idling},
- {(0, 1), Rs232State.Accepting},
- {(0, 2), Rs232State.Escrowed},
- {(0, 3), Rs232State.Stacking},
- {(0, 5), Rs232State.Returning},
- {(1, 2), Rs232State.BillJammed},
- {(1, 3), Rs232State.StackerFull},
- {(2, 2), Rs232State.Failure}
- };
-
- ///
- /// Event map keys by (byte, bit) into the payload.
- /// e.g.
- /// (1,2) => byte 1 bit 2 of payload
- ///
- private static readonly Dictionary<(byte, byte), Rs232Event> EventMap =
- new Dictionary<(byte, byte), Rs232Event>
- {
- {(0, 4), Rs232Event.Stacked},
- {(0, 6), Rs232Event.Returned},
- {(1, 0), Rs232Event.Cheated},
- {(1, 1), Rs232Event.BillRejected},
- {(2, 0), Rs232Event.PowerUp},
- {(2, 1), Rs232Event.InvalidCommand}
- };
-
- ///
- /// Map of reserved bits for each byte index of payload
- ///
- private static readonly Dictionary ReservedBits = new Dictionary
- {
- {0, new[] {7}},
- {1, new[] {5, 6, 7}},
- {2, new[] {6, 7}},
- {3, new[] {0, 1, 2, 3, 4, 5, 6, 7}},
- {4, new[] {7}},
- {5, new[] {7}}
- };
-
- ///
- /// Holds a copy of the just data portion of the device's response
- ///
- private readonly byte[] _payload;
-
- ///
- /// Create and parse a new response message
- ///
- /// Device response data
- public ApexResponseMessage(byte[] data) : base(data)
- {
- if (data is null)
- {
- IsEmptyResponse = true;
- }
- else if (data.Length == 11)
- {
- _payload = data.Skip(3).Take(6).ToArray();
-
- IsValid = Parse();
- }
- }
-
- ///
- public override int Model { get; protected internal set; }
-
- ///
- public override int Revision { get; protected internal set; }
-
- ///
- /// Fully parse message data
- ///
- /// True if message was fully parsed
- private bool Parse()
- {
- if (RawMessage is null)
- {
- PacketIssues.Add("Empty packet");
-
- IsEmptyResponse = true;
-
- // Return early, nothing to parse
- return false;
- }
-
- if (RawMessage.Length != 11)
- {
- PacketIssues.Add($"Packet length is {RawMessage.Length}, expected 11");
-
- // Return early, not enough to parse
- return false;
- }
-
- var actualChecksum = CalculateChecksum();
- var expectedChecksum = RawMessage[RawMessage.Length - 1];
- if (actualChecksum != expectedChecksum)
- {
- PacketIssues.Add($"Packet checksum is {actualChecksum}, expected {expectedChecksum}");
-
- // Return early, can't trust the data
- return false;
- }
-
- // Extract state
- var states = new List(8);
- foreach (var kv in StateMap)
- {
- var index = kv.Key.Item1;
- var bit = kv.Key.Item2;
-
- if (!IsBitSet(bit, _payload[index]))
- {
- continue;
- }
-
- State = kv.Value;
- states.Add(kv.Value);
- }
-
- // Extract events
- foreach (var kv in EventMap)
- {
- var index = kv.Key.Item1;
- var bit = kv.Key.Item2;
- if (IsBitSet(bit, _payload[index]))
- {
- Event |= kv.Value;
- }
- }
-
- // Check cash box presence
- IsCashBoxPresent = IsBitSet(CashBoxBit, _payload[1]);
-
- // Extract model
- Model = _payload[4];
-
- // Extract revision
- Revision = _payload[5];
-
- // Check all bytes for reserved bits
- foreach (var kv in ReservedBits)
- {
- var index = kv.Key;
- var bits = kv.Value;
-
- if (!AreAnyBitsSet(bits, _payload[index]))
- {
- continue;
- }
-
- PacketIssues.Add($"Byte {index} has one more reserved bits set ({string.Join(",", bits)}");
- HasProtocolViolation = true;
- }
-
- // Having not state is a violation
- if (states.Count == 0)
- {
- PacketIssues.Add("No state bit set");
- HasProtocolViolation = true;
- }
- // Have more than one state is a violation
- else if (states.Count > 1)
- {
- PacketIssues.Add($"More than one state set: {string.Join(",", states.Select(x => x.ToString()))}");
- HasProtocolViolation = true;
- }
-
- if (!HasProtocolViolation)
- {
- CreditIndex = AreAnyBitsSet(CreditBits, _payload[2]) ? _payload[2] >> 3 : (int?) null;
- }
-
- return !HasProtocolViolation;
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Commands/ExtendedCommand.cs b/PTI.Rs232Validator/Messages/Commands/ExtendedCommand.cs
new file mode 100644
index 0000000..746b220
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Commands/ExtendedCommand.cs
@@ -0,0 +1,12 @@
+namespace PTI.Rs232Validator.Messages.Commands;
+
+///
+/// The RS-232 extended commands.
+///
+public enum ExtendedCommand : byte
+{
+ ///
+ /// A command to get the last barcode string.
+ ///
+ BarcodeDetected = 0x01
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Commands/TelemetryCommand.cs b/PTI.Rs232Validator/Messages/Commands/TelemetryCommand.cs
new file mode 100644
index 0000000..214de2c
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Commands/TelemetryCommand.cs
@@ -0,0 +1,57 @@
+namespace PTI.Rs232Validator.Messages.Commands;
+
+///
+/// The RS-232 telemetry commands.
+///
+public enum TelemetryCommand : byte
+{
+ ///
+ /// A command to verify communications are working.
+ ///
+ Ping = 0x00,
+
+ ///
+ /// A command to get the serial number assigned to an acceptor.
+ ///
+ GetSerialNumber = 0x01,
+
+ ///
+ /// A command to get the telemetry metrics about the cashbox.
+ ///
+ GetCashboxMetrics = 0x02,
+
+ ///
+ /// A command to clear the count of bills in the cashbox.
+ ///
+ ClearCashboxCount = 0x03,
+
+ ///
+ /// A command to get the general telemetry metrics for an acceptor.
+ ///
+ GetUnitMetrics = 0x04,
+
+ ///
+ /// A command to get the telemetry metrics since the last time an acceptor was serviced.
+ ///
+ GetServiceUsageCounters = 0x05,
+
+ ///
+ /// A command to get the flags about what needs to be serviced.
+ ///
+ GetServiceFlags = 0x06,
+
+ ///
+ /// A command to clear 1 or more service flags.
+ ///
+ ClearServiceFlags = 0x07,
+
+ ///
+ /// A command to get the info that was attached to the last service.
+ ///
+ GetServiceInfo = 0x08,
+
+ ///
+ /// A command to get the telemetry metrics that pertain to an acceptor's firmware.
+ ///
+ GetFirmwareMetrics = 0x09
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Requests/ExtendedRequestMessage.cs b/PTI.Rs232Validator/Messages/Requests/ExtendedRequestMessage.cs
new file mode 100644
index 0000000..4543d42
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Requests/ExtendedRequestMessage.cs
@@ -0,0 +1,53 @@
+using PTI.Rs232Validator.Messages.Commands;
+using PTI.Rs232Validator.Utility;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace PTI.Rs232Validator.Messages.Requests;
+
+///
+/// An implementation of for .
+///
+public class ExtendedRequestMessage : Rs232RequestMessage
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ /// An enumerator of .
+ /// The data.
+ public ExtendedRequestMessage(bool ack, ExtendedCommand command, IReadOnlyList data) : base(BuildPayload(ack,
+ command, data))
+ {
+ Command = command;
+ }
+
+ private ExtendedCommand Command { get; }
+
+ ///
+ public override string ToString()
+ {
+ return base.ToString() + $" | {nameof(Command).AddSpacesToCamelCase()}: {Command}";
+ }
+
+ private static ReadOnlyCollection BuildPayload(
+ bool ack,
+ ExtendedCommand command,
+ IReadOnlyList data)
+ {
+ var payload = new List
+ {
+ Stx,
+ 0,
+ (byte)((byte)Rs232MessageType.ExtendedCommand | (ack ? 1 : 0)),
+ (byte)command
+ };
+
+ payload.AddRange(data);
+ payload.Add(Etx);
+ payload.Add(0);
+ payload[1] = (byte)payload.Count;
+
+ return payload.AsReadOnly();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Requests/PollRequestMessage.cs b/PTI.Rs232Validator/Messages/Requests/PollRequestMessage.cs
new file mode 100644
index 0000000..58165fd
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Requests/PollRequestMessage.cs
@@ -0,0 +1,123 @@
+using PTI.Rs232Validator.Utility;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace PTI.Rs232Validator.Messages.Requests;
+
+///
+/// An implementation of for polling an acceptor.
+///
+public class PollRequestMessage : Rs232RequestMessage
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ public PollRequestMessage(bool ack) : base(BuildPayload(ack))
+ {
+ }
+
+ private byte AcceptanceMask { get; set; }
+ private bool IsEscrowRequested { get; set; }
+ private bool IsStackRequested { get; set; }
+ private bool IsReturnRequested { get; set; }
+ private bool IsBarcodeDetectionRequested { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ return base.ToString() + " | " +
+ $"{nameof(Ack).AddSpacesToCamelCase()}: {Ack} | " +
+ $"{nameof(AcceptanceMask).AddSpacesToCamelCase()}: {AcceptanceMask.ConvertToBinaryString(true)} | " +
+ $"{nameof(IsEscrowRequested).AddSpacesToCamelCase()}: {IsEscrowRequested} | " +
+ $"{nameof(IsStackRequested).AddSpacesToCamelCase()}: {IsStackRequested} | " +
+ $"{nameof(IsReturnRequested).AddSpacesToCamelCase()}: {IsReturnRequested} | " +
+ $"{nameof(IsBarcodeDetectionRequested).AddSpacesToCamelCase()}: {IsBarcodeDetectionRequested}";
+ }
+
+ ///
+ /// Sets the enable mask, which represents types of bills to accept.
+ ///
+ /// The new acceptance mask.
+ /// This instance.
+ ///
+ /// 0b00000001: only accept the 1st bill type (e.g. $1).
+ /// 0b00000010: only accept the 2nd bill type (e.g. $2).
+ /// 0b00000100: only accept the 3rd bill type (e.g. $5).
+ /// 0b00001000: only accept the 4th bill type (e.g. $10).
+ /// 0b00010000: only accept the 5th bill type (e.g. $20).
+ /// 0b00100000: only accept the 6th bill type (e.g. $50).
+ /// 0b01000000: only accept the 7th bill type (e.g. $100).
+ ///
+ public PollRequestMessage SetEnableMask(byte acceptanceMask)
+ {
+ AcceptanceMask = acceptanceMask;
+ MutatePayload(3, (byte)(acceptanceMask & 0x7F));
+ return this;
+ }
+
+ ///
+ /// Sets whether to request a bill to be escrowed.
+ ///
+ /// True to request a bill escrow.
+ /// This instance.
+ public PollRequestMessage SetEscrowRequested(bool isEscrowRequested)
+ {
+ IsEscrowRequested = isEscrowRequested;
+ MutatePayload(4, isEscrowRequested ? Payload[4].SetBit(4) : Payload[4].ClearBit(4));
+ return this;
+ }
+
+ ///
+ /// Sets whether to request a bill to be stacked.
+ ///
+ /// True to request a bill stack.
+ /// This instance.
+ /// This method is only relevant if a bill is in escrow.
+ public PollRequestMessage SetStackRequested(bool isStackRequested)
+ {
+ IsStackRequested = isStackRequested;
+ MutatePayload(4, isStackRequested ? Payload[4].SetBit(5) : Payload[4].ClearBit(5));
+ return this;
+ }
+
+ ///
+ /// Sets whether to request a bill to be returned.
+ ///
+ /// True to request a bill return.
+ /// This instance.
+ /// This method is only relevant if a bill is in escrow.
+ public PollRequestMessage SetReturnRequested(bool isReturnRequested)
+ {
+ IsReturnRequested = isReturnRequested;
+ MutatePayload(4, isReturnRequested ? Payload[4].SetBit(6) : Payload[4].ClearBit(6));
+ return this;
+ }
+
+ ///
+ /// Sets whether to request barcode detection.
+ ///
+ /// True to request barcode detection.
+ /// This instance.
+ public PollRequestMessage SetBarcodeDetectionRequested(bool isBarcodeDetectionRequested)
+ {
+ IsBarcodeDetectionRequested = isBarcodeDetectionRequested;
+ MutatePayload(5, isBarcodeDetectionRequested ? Payload[5].SetBit(1) : Payload[5].ClearBit(1));
+ return this;
+ }
+
+ private static ReadOnlyCollection BuildPayload(bool ack)
+ {
+ return new List
+ {
+ Stx,
+ 8,
+ (byte)((byte)Rs232MessageType.HostToAcceptor | (ack ? 1 : 0)),
+ 0,
+ 0,
+ 0,
+ Etx,
+ 0
+ }.AsReadOnly();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Requests/Rs232RequestMessage.cs b/PTI.Rs232Validator/Messages/Requests/Rs232RequestMessage.cs
new file mode 100644
index 0000000..62f3386
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Requests/Rs232RequestMessage.cs
@@ -0,0 +1,49 @@
+using PTI.Rs232Validator.Utility;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PTI.Rs232Validator.Messages.Requests;
+
+///
+/// An RS-232 message from a host to an acceptor.
+///
+public abstract class Rs232RequestMessage : Rs232Message
+{
+ private readonly byte[] _payloadSource;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// .
+ protected Rs232RequestMessage(IReadOnlyList payload)
+ {
+ _payloadSource = payload.ToArray();
+ _payloadSource[^1] = CalculateChecksum(_payloadSource);
+ }
+
+ ///
+ public override IReadOnlyList Payload => _payloadSource.AsReadOnly();
+
+ ///
+ public override string ToString()
+ {
+ return $"{nameof(Ack).AddSpacesToCamelCase()}: {Ack} | " +
+ $"{nameof(MessageType).AddSpacesToCamelCase()}: {MessageType}";
+ }
+
+ ///
+ /// Mutates at the specified index and calculates the checksum for the last byte.
+ ///
+ /// The index to mutate.
+ /// The value to set at the specified index.
+ protected void MutatePayload(byte index, byte value)
+ {
+ if (index >= _payloadSource.Length)
+ {
+ return;
+ }
+
+ _payloadSource[index] = value;
+ _payloadSource[^1] = CalculateChecksum(_payloadSource);
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Requests/TelemetryRequestMessage.cs b/PTI.Rs232Validator/Messages/Requests/TelemetryRequestMessage.cs
new file mode 100644
index 0000000..0dfa5dc
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Requests/TelemetryRequestMessage.cs
@@ -0,0 +1,53 @@
+using PTI.Rs232Validator.Messages.Commands;
+using PTI.Rs232Validator.Utility;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace PTI.Rs232Validator.Messages.Requests;
+
+///
+/// An implementation of for .
+///
+public class TelemetryRequestMessage : Rs232RequestMessage
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ /// An enumerator of .
+ /// The data.
+ public TelemetryRequestMessage(bool ack, TelemetryCommand command, IReadOnlyList data)
+ : base(BuildPayload(ack, command, data))
+ {
+ Command = command;
+ }
+
+ private TelemetryCommand Command { get; }
+
+ ///
+ public override string ToString()
+ {
+ return base.ToString() + $" | {nameof(Command).AddSpacesToCamelCase()}: {Command}";
+ }
+
+ private static ReadOnlyCollection BuildPayload(
+ bool ack,
+ TelemetryCommand command,
+ IReadOnlyList data)
+ {
+ var payload = new List
+ {
+ Stx,
+ 0,
+ (byte)((byte)Rs232MessageType.TelemetryCommand | (ack ? 1 : 0)),
+ (byte)command
+ };
+
+ payload.AddRange(data);
+ payload.Add(Etx);
+ payload.Add(0);
+ payload[1] = (byte)payload.Count;
+
+ return payload.AsReadOnly();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Responses/Extended/BarcodeDetectedResponseMessage.cs b/PTI.Rs232Validator/Messages/Responses/Extended/BarcodeDetectedResponseMessage.cs
new file mode 100644
index 0000000..1622795
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Responses/Extended/BarcodeDetectedResponseMessage.cs
@@ -0,0 +1,49 @@
+using PTI.Rs232Validator.Messages.Commands;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace PTI.Rs232Validator.Messages.Responses.Extended;
+
+///
+/// An RS-232 message from an acceptor to a host for .
+///
+public class BarcodeDetectedResponseMessage : ExtendedResponseMessage
+{
+ ///
+ /// The expected payload size in bytes.
+ ///
+ private const byte PayloadByteSize = 40;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ public BarcodeDetectedResponseMessage(IReadOnlyList payload) : base(payload)
+ {
+ if (!IsValid)
+ {
+ return;
+ }
+
+ if (payload.Count < PayloadByteSize)
+ {
+ PayloadIssues.Add(
+ $"The payload size is {payload.Count} bytes, but {PayloadByteSize} bytes are expected.");
+ }
+
+ Barcode = Encoding.ASCII.GetString(Data.ToArray()).Trim('\0');
+ }
+
+ ///
+ /// The last barcode string.
+ ///
+ /// If the string is empty, no barcode was detected.
+ public string Barcode { get; } = string.Empty;
+
+ ///
+ public override string ToString()
+ {
+ return IsValid ? $"Barcode: {Barcode}" : base.ToString();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Responses/Extended/ExtendedResponseMessage.cs b/PTI.Rs232Validator/Messages/Responses/Extended/ExtendedResponseMessage.cs
new file mode 100644
index 0000000..0877ba3
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Responses/Extended/ExtendedResponseMessage.cs
@@ -0,0 +1,75 @@
+using PTI.Rs232Validator.Messages.Commands;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PTI.Rs232Validator.Messages.Responses.Extended;
+
+///
+/// An RS-232 extended message from an acceptor to a host.
+///
+public class ExtendedResponseMessage : PollResponseMessage
+{
+ ///
+ /// The minimum payload size in bytes.
+ ///
+ protected new const byte MinPayloadByteSize = 12;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ public ExtendedResponseMessage(IReadOnlyList payload) : base(payload, GetStatus(payload))
+ {
+ if (PayloadIssues.Count > 0)
+ {
+ return;
+ }
+
+ if (payload.Count < MinPayloadByteSize)
+ {
+ PayloadIssues.Add(
+ $"The payload size is {payload.Count} bytes, but at least {MinPayloadByteSize} bytes are expected.");
+ return;
+ }
+
+ if (MessageType != Rs232MessageType.ExtendedCommand)
+ {
+ PayloadIssues.Add(
+ $"The message type is {MessageType}, but {Rs232MessageType.ExtendedCommand} is expected.");
+ return;
+ }
+
+ Command = (ExtendedCommand)payload[3];
+
+ Data = payload
+ .Skip(10)
+ .Take(payload.Count - MinPayloadByteSize)
+ .ToList()
+ .AsReadOnly();
+ }
+
+ ///
+ /// An enumerator of .
+ ///
+ public ExtendedCommand Command { get; }
+
+ ///
+ /// The data.
+ ///
+ internal IReadOnlyList Data { get; } = [];
+
+ private static IReadOnlyList GetStatus(IReadOnlyList payload)
+ {
+ if (payload.Count < MinPayloadByteSize)
+ {
+ return Array.Empty();
+ }
+
+ return payload
+ .Skip(4)
+ .Take(StatusByteSize)
+ .ToList()
+ .AsReadOnly();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Responses/PollResponseMessage.cs b/PTI.Rs232Validator/Messages/Responses/PollResponseMessage.cs
new file mode 100644
index 0000000..f2ca27f
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Responses/PollResponseMessage.cs
@@ -0,0 +1,219 @@
+using PTI.Rs232Validator.Utility;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PTI.Rs232Validator.Messages.Responses;
+
+///
+/// An RS-232 poll message from an acceptor to a host.
+///
+public class PollResponseMessage : Rs232ResponseMessage
+{
+ private const byte PayloadByteSize = 11;
+
+ ///
+ /// The expected size of .
+ ///
+ protected const byte StatusByteSize = 6;
+
+ ///
+ /// A map of (byteIndex, bitIndex) pairs in to .
+ ///
+ private static readonly Dictionary<(byte, byte), Rs232State> StateMap = new()
+ {
+ { (0, 0), Rs232State.Idling },
+ { (0, 1), Rs232State.Accepting },
+ { (0, 2), Rs232State.Escrowed },
+ { (0, 3), Rs232State.Stacking },
+ { (0, 5), Rs232State.Returning },
+ { (1, 2), Rs232State.BillJammed },
+ { (1, 3), Rs232State.StackerFull },
+ { (2, 2), Rs232State.Failure }
+ };
+
+ ///
+ /// A map of (byteIndex, bitIndex) pairs in to .
+ ///
+ private static readonly Dictionary<(byte, byte), Rs232Event> EventMap = new()
+ {
+ { (0, 4), Rs232Event.Stacked },
+ { (0, 6), Rs232Event.Returned },
+ { (1, 0), Rs232Event.Cheated },
+ { (1, 1), Rs232Event.BillRejected },
+ { (2, 0), Rs232Event.PowerUp },
+ { (2, 1), Rs232Event.InvalidCommand }
+ };
+
+ ///
+ /// A map of byte indices in d to reserved bit indices.
+ ///
+ private static readonly Dictionary ReservedBitIndices = new()
+ {
+ { 0, [7] },
+ { 1, [5, 6, 7] },
+ { 2, [6, 7] },
+ { 3, [0, 1, 2, 3, 4, 5, 6, 7] },
+ { 4, [7] },
+ { 5, [7] }
+ };
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ public PollResponseMessage(IReadOnlyList payload) : base(payload)
+ {
+ if (!IsValid)
+ {
+ return;
+ }
+
+ if (payload.Count != PayloadByteSize)
+ {
+ PayloadIssues.Add(
+ $"The payload size is {payload.Count} bytes, but {PayloadByteSize} bytes are expected.");
+ return;
+ }
+
+ if (MessageType != Rs232MessageType.AcceptorToHost)
+ {
+ PayloadIssues.Add($"The message type is {MessageType}, but {Rs232MessageType.AcceptorToHost} is expected.");
+ }
+
+ Status = payload
+ .Skip(3)
+ .Take(StatusByteSize)
+ .ToList()
+ .AsReadOnly();
+ DeserializeStatus();
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// .
+ /// .
+ protected PollResponseMessage(IReadOnlyList payload, IReadOnlyList status) : base(payload)
+ {
+ if (PayloadIssues.Count > 0 || payload.Count < PayloadByteSize || status.Count != 6)
+ {
+ return;
+ }
+
+ Status = status;
+ DeserializeStatus();
+ }
+
+ ///
+ /// An enumerator of .
+ ///
+ public Rs232State State { get; private set; }
+
+ ///
+ /// A collection of enumerators.
+ ///
+ public Rs232Event Event { get; private set; }
+
+ ///
+ /// Is the cashbox present?
+ ///
+ ///
+ /// For stackerless models, this will always be true.
+ ///
+ public bool IsCashboxPresent { get; private set; }
+
+ ///
+ /// The bill type in escrow.
+ ///
+ public byte BillType { get; private set; }
+
+ ///
+ /// Model number.
+ ///
+ public byte ModelNumber { get; private set; }
+
+ ///
+ /// Firmware revision.
+ ///
+ ///
+ /// 1.17 returns 17.
+ ///
+ public byte FirmwareRevision { get; private set; }
+
+ ///
+ /// A 6-byte collection representing the status of the acceptor.
+ ///
+ internal IReadOnlyList Status { get; } = [];
+
+ ///
+ public override string ToString()
+ {
+ return IsValid
+ ? $"{nameof(State).AddSpacesToCamelCase()}: {State} | " +
+ $"{nameof(Event).AddSpacesToCamelCase()}(s): {Event} | " +
+ $"{nameof(BillType).AddSpacesToCamelCase()}: {BillType} | " +
+ $"{nameof(ModelNumber).AddSpacesToCamelCase()}: {ModelNumber} | " +
+ $"{nameof(FirmwareRevision).AddSpacesToCamelCase()}: {FirmwareRevision} | " +
+ $"{nameof(IsCashboxPresent).AddSpacesToCamelCase()}: {IsCashboxPresent}"
+ : base.ToString();
+ }
+
+ private void DeserializeStatus()
+ {
+ var states = new List();
+ foreach (var pair in StateMap)
+ {
+ var byteIndex = pair.Key.Item1;
+ var bitIndex = pair.Key.Item2;
+ if (Status[byteIndex].IsBitSet(bitIndex))
+ {
+ states.Add(pair.Value);
+ }
+ }
+
+ foreach (var pair in EventMap)
+ {
+ var byteIndex = pair.Key.Item1;
+ var bitIndex = pair.Key.Item2;
+ if (Status[byteIndex].IsBitSet(bitIndex))
+ {
+ Event |= pair.Value;
+ }
+ }
+
+ IsCashboxPresent = Status[1].IsBitSet(4);
+ ModelNumber = Status[4];
+ FirmwareRevision = Status[5];
+
+ foreach (var pair in ReservedBitIndices)
+ {
+ var byteIndex = pair.Key;
+ var bitIndices = pair.Value;
+
+ var setBitIndices = bitIndices.Where(bitIndex => Status[byteIndex].IsBitSet(bitIndex)).ToArray();
+ if (setBitIndices.Length == 0)
+ {
+ continue;
+ }
+
+ PayloadIssues.Add(
+ $"The status byte {byteIndex} has 1 or more reserved bits set: {string.Join(",", setBitIndices)}.");
+ }
+
+ switch (states.Count)
+ {
+ case 0:
+ PayloadIssues.Add("The status has no state set.");
+ break;
+ case > 1:
+ PayloadIssues.Add(
+ $"The status has more than 1 state set: {string.Join(",", states.Select(s => s.ToString()))}.");
+ break;
+ default:
+ State = states[0];
+ break;
+ }
+
+ BillType = (byte)(Status[2] >> 3);
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Responses/Rs232ResponseMessage.cs b/PTI.Rs232Validator/Messages/Responses/Rs232ResponseMessage.cs
new file mode 100644
index 0000000..99bfe47
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Responses/Rs232ResponseMessage.cs
@@ -0,0 +1,90 @@
+using PTI.Rs232Validator.Utility;
+using System.Collections.Generic;
+
+namespace PTI.Rs232Validator.Messages.Responses;
+
+///
+/// An RS-232 message from an acceptor to a host.
+///
+public abstract class Rs232ResponseMessage : Rs232Message
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ protected Rs232ResponseMessage(IReadOnlyList payload)
+ {
+ Payload = payload;
+
+ if (payload.Count == 0)
+ {
+ PayloadIssues.Add("The payload is empty.");
+ return;
+ }
+
+ if (payload.Count < MinPayloadByteSize)
+ {
+ PayloadIssues.Add(
+ $"The payload size is {payload.Count} bytes, but at least {MinPayloadByteSize} bytes are expected.");
+ return;
+ }
+
+ if (payload[0] != Stx)
+ {
+ PayloadIssues.Add($"The payload starts with 0x{payload[0]:X2}, but 0x{Stx:X2} is expected.");
+ }
+
+ if (payload[1] != payload.Count)
+ {
+ PayloadIssues.Add(
+ $"The payload size is {payload.Count} bytes, but the payload reported a size of {payload[1]} bytes.");
+ }
+
+ if (MessageType == Rs232MessageType.HostToAcceptor)
+ {
+ PayloadIssues.Add($"The message type is {MessageType}, which should never occur.");
+ }
+
+ if (payload[^2] != Etx)
+ {
+ PayloadIssues.Add($"The payload ends with 0x{payload[^2]:X2}, but 0x{Etx:X2} is expected.");
+ }
+
+ var actualChecksum = payload[^1];
+ var expectedChecksum = CalculateChecksum(payload);
+ if (actualChecksum != expectedChecksum)
+ {
+ PayloadIssues.Add(
+ $"The payload has a checksum of 0x{actualChecksum:X2}, but 0x{expectedChecksum:X2} is expected.");
+ }
+ }
+
+ ///
+ public override IReadOnlyList Payload { get; }
+
+ ///
+ /// Is this instance valid (i.e. are there no issues with )?
+ ///
+ public bool IsValid => PayloadIssues.Count == 0;
+
+ ///
+ /// A collection of issues with .
+ ///
+ protected List PayloadIssues { get; } = [];
+
+ ///
+ public override string ToString()
+ {
+ return IsValid
+ ? $"Valid {GetType().Name.AddSpacesToCamelCase()}"
+ : $"Invalid {GetType().Name.AddSpacesToCamelCase()}";
+ }
+
+ ///
+ /// Gets the issues with .
+ ///
+ public IReadOnlyList GetPayloadIssues()
+ {
+ return PayloadIssues.AsReadOnly();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Responses/Telemetry/GetCashboxMetricsResponseMessage.cs b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetCashboxMetricsResponseMessage.cs
new file mode 100644
index 0000000..3224778
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetCashboxMetricsResponseMessage.cs
@@ -0,0 +1,83 @@
+using PTI.Rs232Validator.Messages.Commands;
+using PTI.Rs232Validator.Utility;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PTI.Rs232Validator.Messages.Responses.Telemetry;
+
+///
+/// An RS-232 message from an acceptor to a host for .
+///
+public class GetCashboxMetricsResponseMessage : TelemetryResponseMessage
+{
+ private const byte PayloadByteSize = 53;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ public GetCashboxMetricsResponseMessage(IReadOnlyList payload) : base(payload)
+ {
+ if (PayloadIssues.Count > 0)
+ {
+ return;
+ }
+
+ if (payload.Count != PayloadByteSize)
+ {
+ PayloadIssues.Add($"The payload size is {payload.Count} bytes, but {PayloadByteSize} bytes are expected.");
+ return;
+ }
+
+ var data = Data.ToArray();
+ CashboxRemovedCount = data[..8].ConvertToUint32Via4BitEncoding();
+ CashboxFullCount = data[8..16].ConvertToUint32Via4BitEncoding();
+ BillsStackedSinceCashboxRemoved = data[16..24].ConvertToUint32Via4BitEncoding();
+ BillsStackedSincePowerUp = data[24..32].ConvertToUint32Via4BitEncoding();
+ AverageTimeToStack = data[32..40].ConvertToUint32Via4BitEncoding();
+ TotalBillsStacked = data[40..48].ConvertToUint32Via4BitEncoding();
+ }
+
+ ///
+ /// The number of times the cashbox has been removed.
+ ///
+ public uint CashboxRemovedCount { get; init; }
+
+ ///
+ /// The number of times the cashbox has been full.
+ ///
+ public uint CashboxFullCount { get; init; }
+
+ ///
+ /// The count of bills stacked since the cashbox was last removed.
+ ///
+ public uint BillsStackedSinceCashboxRemoved { get; init; }
+
+ ///
+ /// The count of bills stacked since the unit has been powered.
+ ///
+ public uint BillsStackedSincePowerUp { get; init; }
+
+ ///
+ /// The average time, in milliseconds, it takes to stack a bill.
+ ///
+ public uint AverageTimeToStack { get; init; }
+
+ ///
+ /// The total number of bills put in the cashbox for the lifetime of the unit.
+ ///
+ public uint TotalBillsStacked { get; init; }
+
+ ///
+ public override string ToString()
+ {
+ return IsValid
+ ? $"{nameof(CashboxRemovedCount).AddSpacesToCamelCase()}: {CashboxRemovedCount} | " +
+ $"{nameof(CashboxFullCount).AddSpacesToCamelCase()}: {CashboxFullCount} | " +
+ $"{nameof(BillsStackedSinceCashboxRemoved).AddSpacesToCamelCase()}: {BillsStackedSinceCashboxRemoved} | " +
+ $"{nameof(BillsStackedSincePowerUp).AddSpacesToCamelCase()}: {BillsStackedSincePowerUp} | " +
+ $"{nameof(AverageTimeToStack).AddSpacesToCamelCase()}: {AverageTimeToStack} | " +
+ $"{nameof(TotalBillsStacked).AddSpacesToCamelCase()}: {TotalBillsStacked}"
+ : base.ToString();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Responses/Telemetry/GetFirmwareMetricsResponseMessage.cs b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetFirmwareMetricsResponseMessage.cs
new file mode 100644
index 0000000..03a2606
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetFirmwareMetricsResponseMessage.cs
@@ -0,0 +1,111 @@
+using PTI.Rs232Validator.Messages.Commands;
+using PTI.Rs232Validator.Utility;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PTI.Rs232Validator.Messages.Responses.Telemetry;
+
+///
+/// An RS-232 message from an acceptor to a host for .
+///
+public class GetFirmwareMetricsResponseMessage : TelemetryResponseMessage
+{
+ private const byte PayloadByteSize = 69;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ public GetFirmwareMetricsResponseMessage(IReadOnlyList payload) : base(payload)
+ {
+ if (PayloadIssues.Count > 0)
+ {
+ return;
+ }
+
+ if (payload.Count != PayloadByteSize)
+ {
+ PayloadIssues.Add($"The payload size is {payload.Count} bytes, but {PayloadByteSize} bytes are expected.");
+ return;
+ }
+
+ var data = Data.ToArray();
+ FlashUpdateCount = data[..8].ConvertToUint32Via4BitEncoding();
+ UsbFlashDriveFirmwareUpdateCount = data[8..16].ConvertToUint32Via4BitEncoding();
+ TotalFlashDriveInsertCount = data[16..24].ConvertToUint32Via4BitEncoding();
+ FirmwareCountryRevision = data[24..28].ConvertToUint16Via4BitEncoding();
+ FirmwareCoreRevision = data[28..32].ConvertToUint16Via4BitEncoding();
+ FirmwareBuildRevision = data[32..40].ConvertToUint32Via4BitEncoding();
+ FirmwareCrc = data[40..48].ConvertToUint32Via4BitEncoding();
+ BootloaderMajorRevision = data[48..52].ConvertToUint16Via4BitEncoding();
+ BootloaderMinorRevision = data[52..56].ConvertToUint16Via4BitEncoding();
+ BootloaderBuildRevision = data[56..64].ConvertToUint32Via4BitEncoding();
+ }
+
+ ///
+ /// The total times an acceptor has had a firmware update.
+ ///
+ public uint FlashUpdateCount { get; init; }
+
+ ///
+ /// The total times an acceptor has had a firmware update via a flash drive.
+ ///
+ public uint UsbFlashDriveFirmwareUpdateCount { get; init; }
+
+ ///
+ /// The total times an acceptor has detected a flash drive insert.
+ ///
+ public uint TotalFlashDriveInsertCount { get; init; }
+
+ ///
+ /// The country revision of the firmware.
+ ///
+ public ushort FirmwareCountryRevision { get; init; }
+
+ ///
+ /// The core revision of the firmware.
+ ///
+ public ushort FirmwareCoreRevision { get; init; }
+
+ ///
+ /// The build revision of the firmware.
+ ///
+ public uint FirmwareBuildRevision { get; init; }
+
+ ///
+ /// The CRC of the firmware.
+ ///
+ public uint FirmwareCrc { get; init; }
+
+ ///
+ /// The major revision of the bootloader.
+ ///
+ public ushort BootloaderMajorRevision { get; init; }
+
+ ///
+ /// The minor revision of the bootloader.
+ ///
+ public ushort BootloaderMinorRevision { get; init; }
+
+ ///
+ /// The build revision of the bootloader.
+ ///
+ public uint BootloaderBuildRevision { get; init; }
+
+ ///
+ public override string ToString()
+ {
+ return IsValid
+ ? $"{nameof(FlashUpdateCount).AddSpacesToCamelCase()}: {FlashUpdateCount} | " +
+ $"{nameof(UsbFlashDriveFirmwareUpdateCount).AddSpacesToCamelCase()}: {UsbFlashDriveFirmwareUpdateCount} | " +
+ $"{nameof(TotalFlashDriveInsertCount).AddSpacesToCamelCase()}: {TotalFlashDriveInsertCount} | " +
+ $"{nameof(FirmwareCountryRevision).AddSpacesToCamelCase()}: {FirmwareCountryRevision} | " +
+ $"{nameof(FirmwareCoreRevision).AddSpacesToCamelCase()}: {FirmwareCoreRevision} | " +
+ $"{nameof(FirmwareBuildRevision).AddSpacesToCamelCase()}: {FirmwareBuildRevision} | " +
+ $"{nameof(FirmwareCrc).AddSpacesToCamelCase()}: {FirmwareCrc} | " +
+ $"{nameof(BootloaderMajorRevision).AddSpacesToCamelCase()}: {BootloaderMajorRevision} | " +
+ $"{nameof(BootloaderMinorRevision).AddSpacesToCamelCase()}: {BootloaderMinorRevision} | " +
+ $"{nameof(BootloaderBuildRevision).AddSpacesToCamelCase()}: {BootloaderBuildRevision}"
+ : base.ToString();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Responses/Telemetry/GetSerialNumberResponseMessage.cs b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetSerialNumberResponseMessage.cs
new file mode 100644
index 0000000..fc8f206
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetSerialNumberResponseMessage.cs
@@ -0,0 +1,56 @@
+using PTI.Rs232Validator.Messages.Commands;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace PTI.Rs232Validator.Messages.Responses.Telemetry;
+
+///
+/// An RS-232 message from an acceptor to a host for .
+///
+public class GetSerialNumberResponseMessage : TelemetryResponseMessage
+{
+ private const byte PayloadByteSize = 14;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ internal GetSerialNumberResponseMessage(IReadOnlyList payload) : base(payload)
+ {
+ if (PayloadIssues.Count > 0)
+ {
+ return;
+ }
+
+ if (payload.Count != PayloadByteSize)
+ {
+ PayloadIssues.Add($"The payload size is {payload.Count} bytes, but {PayloadByteSize} bytes are expected.");
+ return;
+ }
+
+ var serialNumber = Encoding.ASCII.GetString(Data.ToArray()).Trim('\0');
+ foreach (var c in serialNumber)
+ {
+ if (!char.IsDigit(c))
+ {
+ PayloadIssues.Add($"The data contains a non-digit character: {serialNumber}.");
+ return;
+ }
+ }
+
+ SerialNumber = serialNumber;
+ }
+
+ ///
+ /// The serial number of an acceptor.
+ ///
+ /// If the string is empty, then the acceptor was not assigned a serial number.
+ public string SerialNumber { get; } = "";
+
+ ///
+ public override string ToString()
+ {
+ return IsValid ? $"Serial Number: {SerialNumber}" : base.ToString();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Responses/Telemetry/GetServiceFlagsResponseMessage.cs b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetServiceFlagsResponseMessage.cs
new file mode 100644
index 0000000..b360df6
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetServiceFlagsResponseMessage.cs
@@ -0,0 +1,106 @@
+using PTI.Rs232Validator.Messages.Commands;
+using PTI.Rs232Validator.Utility;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PTI.Rs232Validator.Messages.Responses.Telemetry;
+
+///
+/// An RS-232 message from an acceptor to a host for .
+///
+public class GetServiceFlagsResponseMessage : TelemetryResponseMessage
+{
+ private const byte PayloadByteSize = 11;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ public GetServiceFlagsResponseMessage(IReadOnlyList payload) : base(payload)
+ {
+ if (PayloadIssues.Count > 0)
+ {
+ return;
+ }
+
+ if (payload.Count != PayloadByteSize)
+ {
+ PayloadIssues.Add($"The payload size is {payload.Count} bytes, but {PayloadByteSize} bytes are expected.");
+ return;
+ }
+
+ var data = Data.ToArray();
+ TachSensorServiceSuggestor = (ServiceSuggestor)data[0];
+ BillPathServiceSuggestor = (ServiceSuggestor)data[1];
+ CashboxBeltServiceSuggestor = (ServiceSuggestor)data[2];
+ CashboxMechanismServiceSuggestor = (ServiceSuggestor)data[3];
+ MasServiceSuggestor = (ServiceSuggestor)data[4];
+ SpringRollersServiceSuggestor = (ServiceSuggestor)data[5];
+ }
+
+ ///
+ /// An enumerator of for the tach sensor.
+ ///
+ public ServiceSuggestor TachSensorServiceSuggestor { get; init; }
+
+ ///
+ /// An enumerator of for the bill path.
+ ///
+ public ServiceSuggestor BillPathServiceSuggestor { get; init; }
+
+ ///
+ /// An enumerator of for the cashbox belt.
+ ///
+ public ServiceSuggestor CashboxBeltServiceSuggestor { get; init; }
+
+ ///
+ /// An enumerator of for the cashbox stacking mechanism.
+ ///
+ public ServiceSuggestor CashboxMechanismServiceSuggestor { get; init; }
+
+ ///
+ /// An enumerator of for the mechanical anti-stringing lever (MAS).
+ ///
+ public ServiceSuggestor MasServiceSuggestor { get; init; }
+
+ ///
+ /// An enumerator of for the spring rollers.
+ ///
+ public ServiceSuggestor SpringRollersServiceSuggestor { get; init; }
+
+ ///
+ public override string ToString()
+ {
+ return IsValid
+ ? $"{nameof(TachSensorServiceSuggestor).AddSpacesToCamelCase()}: {TachSensorServiceSuggestor} | " +
+ $"{nameof(BillPathServiceSuggestor).AddSpacesToCamelCase()}: {BillPathServiceSuggestor} | " +
+ $"{nameof(CashboxBeltServiceSuggestor).AddSpacesToCamelCase()}: {CashboxBeltServiceSuggestor} | " +
+ $"{nameof(CashboxMechanismServiceSuggestor).AddSpacesToCamelCase()}: {CashboxMechanismServiceSuggestor} | " +
+ $"{nameof(MasServiceSuggestor).AddSpacesToCamelCase()}: {MasServiceSuggestor} | " +
+ $"{nameof(SpringRollersServiceSuggestor).AddSpacesToCamelCase()}: {SpringRollersServiceSuggestor}"
+ : base.ToString();
+ }
+
+ ///
+ /// The entities that suggest a component requires service.
+ ///
+ [Flags]
+ public enum ServiceSuggestor : byte
+ {
+ ///
+ /// No entity suggests that a component requires service.
+ ///
+ None = 0,
+
+ ///
+ /// The usage metrics suggest that a component requires service.
+ ///
+ UsageMetrics = 1 << 0,
+
+ ///
+ /// The diagnostics and errors of the system suggest that a component requires service.
+ ///
+ DiagnosticsAndError = 1 << 1
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Responses/Telemetry/GetServiceInfoResponseMessage.cs b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetServiceInfoResponseMessage.cs
new file mode 100644
index 0000000..011c6db
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetServiceInfoResponseMessage.cs
@@ -0,0 +1,62 @@
+using PTI.Rs232Validator.Messages.Commands;
+using PTI.Rs232Validator.Utility;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PTI.Rs232Validator.Messages.Responses.Telemetry;
+
+///
+/// An RS-232 message from an acceptor to a host for .
+///
+public class GetServiceInfoResponseMessage : TelemetryResponseMessage
+{
+ private const byte PayloadByteSize = 17;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ internal GetServiceInfoResponseMessage(IReadOnlyList payload) : base(payload)
+ {
+ if (PayloadIssues.Count > 0)
+ {
+ return;
+ }
+
+ if (payload.Count != PayloadByteSize)
+ {
+ PayloadIssues.Add($"The payload size is {payload.Count} bytes, but {PayloadByteSize} bytes are expected.");
+ return;
+ }
+
+ var data = Data.ToArray();
+ LastCustomerService = data[..4].ClearEighthBits();
+ LastServiceCenterService = data[4..8].ClearEighthBits();
+ LastOemService = data[8..12].ClearEighthBits();
+ }
+
+ ///
+ /// The 4 bytes of custom data that a customer wrote to an acceptor on the last service.
+ ///
+ public byte[] LastCustomerService { get; } = new byte[4];
+
+ ///
+ /// The 4 bytes of custom data that a service center wrote to an acceptor on the last service.
+ ///
+ public byte[] LastServiceCenterService { get; } = new byte[4];
+
+ ///
+ /// The 4 bytes of custom data that an OEM wrote to an acceptor on the last service.
+ ///
+ public byte[] LastOemService { get; } = new byte[4];
+
+ ///
+ public override string ToString()
+ {
+ return IsValid
+ ? $"{nameof(LastCustomerService).AddSpacesToCamelCase()}: {LastCustomerService.ConvertToHexString(true, false)} | " +
+ $"{nameof(LastServiceCenterService).AddSpacesToCamelCase()}: {LastServiceCenterService.ConvertToHexString(true, false)} | " +
+ $"{nameof(LastOemService).AddSpacesToCamelCase()}: {LastOemService.ConvertToHexString(true, false)}"
+ : base.ToString();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Responses/Telemetry/GetServiceUsageCountersResponseMessage.cs b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetServiceUsageCountersResponseMessage.cs
new file mode 100644
index 0000000..f8757b9
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetServiceUsageCountersResponseMessage.cs
@@ -0,0 +1,83 @@
+using PTI.Rs232Validator.Messages.Commands;
+using PTI.Rs232Validator.Utility;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PTI.Rs232Validator.Messages.Responses.Telemetry;
+
+///
+/// An RS-232 message from an acceptor to a host for .
+///
+public class GetServiceUsageCountersResponseMessage : TelemetryResponseMessage
+{
+ private const byte PayloadByteSize = 53;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ public GetServiceUsageCountersResponseMessage(IReadOnlyList payload) : base(payload)
+ {
+ if (PayloadIssues.Count > 0)
+ {
+ return;
+ }
+
+ if (payload.Count != PayloadByteSize)
+ {
+ PayloadIssues.Add($"The payload size is {payload.Count} bytes, but {PayloadByteSize} bytes are expected.");
+ return;
+ }
+
+ var data = Data.ToArray();
+ DistancedMovedSinceLastTachSensorService = data[..8].ConvertToUint32Via4BitEncoding();
+ DistanceMovedSinceLastBillPathService = data[8..16].ConvertToUint32Via4BitEncoding();
+ DistancedMoveSinceLastBeltService = data[16..24].ConvertToUint32Via4BitEncoding();
+ BillsStackedSinceLastCashboxService = data[24..32].ConvertToUint32Via4BitEncoding();
+ DistanceMovedSinceLastMasService = data[32..40].ConvertToUint32Via4BitEncoding();
+ DistanceMovedSinceLastSpringRollerService = data[40..48].ConvertToUint32Via4BitEncoding();
+ }
+
+ ///
+ /// The total amount of movement since the last tach sensor service in mm.
+ ///
+ public uint DistancedMovedSinceLastTachSensorService { get; init; }
+
+ ///
+ /// The total amount of movement since the last bill path service in mm.
+ ///
+ public uint DistanceMovedSinceLastBillPathService { get; init; }
+
+ ///
+ /// The total amount of movement since the last belt service in mm.
+ ///
+ public uint DistancedMoveSinceLastBeltService { get; init; }
+
+ ///
+ /// The total amount of bills stacked since the last cashbox mechanism service in mm.
+ ///
+ public uint BillsStackedSinceLastCashboxService { get; init; }
+
+ ///
+ /// The total amount of movement since the last mechanical anti-stringing lever (MAS) service in mm.
+ ///
+ public uint DistanceMovedSinceLastMasService { get; init; }
+
+ ///
+ /// The total amount of movement since the last spring roller service in mm.
+ ///
+ public uint DistanceMovedSinceLastSpringRollerService { get; init; }
+
+ ///
+ public override string ToString()
+ {
+ return IsValid
+ ? $"{nameof(DistancedMovedSinceLastTachSensorService).AddSpacesToCamelCase()}: {DistancedMovedSinceLastTachSensorService} | " +
+ $"{nameof(DistanceMovedSinceLastBillPathService).AddSpacesToCamelCase()}: {DistanceMovedSinceLastBillPathService} | " +
+ $"{nameof(DistancedMoveSinceLastBeltService).AddSpacesToCamelCase()}: {DistancedMoveSinceLastBeltService} | " +
+ $"{nameof(BillsStackedSinceLastCashboxService).AddSpacesToCamelCase()}: {BillsStackedSinceLastCashboxService} | " +
+ $"{nameof(DistanceMovedSinceLastMasService).AddSpacesToCamelCase()}: {DistanceMovedSinceLastMasService} | " +
+ $"{nameof(DistanceMovedSinceLastSpringRollerService).AddSpacesToCamelCase()}: {DistanceMovedSinceLastSpringRollerService}"
+ : base.ToString();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Responses/Telemetry/GetUnitMetricsResponseMessage.cs b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetUnitMetricsResponseMessage.cs
new file mode 100644
index 0000000..1704df0
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Responses/Telemetry/GetUnitMetricsResponseMessage.cs
@@ -0,0 +1,97 @@
+using PTI.Rs232Validator.Messages.Commands;
+using PTI.Rs232Validator.Utility;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PTI.Rs232Validator.Messages.Responses.Telemetry;
+
+///
+/// An RS-232 message from an acceptor to a host for .
+///
+public class GetUnitMetricsResponseMessage : TelemetryResponseMessage
+{
+ private const byte PayloadByteSize = 69;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ public GetUnitMetricsResponseMessage(IReadOnlyList payload) : base(payload)
+ {
+ if (PayloadIssues.Count > 0)
+ {
+ return;
+ }
+
+ if (payload.Count != PayloadByteSize)
+ {
+ PayloadIssues.Add($"The payload size is {payload.Count} bytes, but {PayloadByteSize} bytes are expected.");
+ return;
+ }
+
+ var data = Data.ToArray();
+ TotalValueStacked = data[..8].ConvertToUint32Via4BitEncoding();
+ TotalDistanceMoved = data[8..16].ConvertToUint32Via4BitEncoding();
+ PowerUpCount = data[16..24].ConvertToUint32Via4BitEncoding();
+ PushButtonCount = data[24..32].ConvertToUint32Via4BitEncoding();
+ ConfigurationCount = data[32..40].ConvertToUint32Via4BitEncoding();
+ UsbEnumerationsCount = data[40..48].ConvertToUint32Via4BitEncoding();
+ TotalCheatAttemptsDetected = data[48..56].ConvertToUint32Via4BitEncoding();
+ TotalSecurityLockupCount = data[56..64].ConvertToUint32Via4BitEncoding();
+ }
+
+ ///
+ /// The total value of currency accepted for the lifetime of the acceptor.
+ ///
+ public uint TotalValueStacked { get; init; }
+
+ ///
+ /// The total distance the acceptance motor has moved in mm.
+ ///
+ public uint TotalDistanceMoved { get; init; }
+
+ ///
+ /// The total times the acceptor has powered on.
+ ///
+ public uint PowerUpCount { get; init; }
+
+ ///
+ /// The total times the diagnostics push button has been pressed.
+ ///
+ public uint PushButtonCount { get; init; }
+
+ ///
+ /// The total times the acceptor has been re-configured.
+ ///
+ public uint ConfigurationCount { get; init; }
+
+ ///
+ /// The total times the acceptor has had the USB device port plugged in.
+ ///
+ public uint UsbEnumerationsCount { get; init; }
+
+ ///
+ /// The total times the acceptor has detected a cheat attempt.
+ ///
+ public uint TotalCheatAttemptsDetected { get; init; }
+
+ ///
+ /// The total times the acceptor went into a security lockup due to cheat attempts.
+ ///
+ public uint TotalSecurityLockupCount { get; init; }
+
+ ///
+ public override string ToString()
+ {
+ return IsValid
+ ? $"{nameof(TotalValueStacked).AddSpacesToCamelCase()}: {TotalValueStacked} | " +
+ $"{nameof(TotalDistanceMoved).AddSpacesToCamelCase()}: {TotalDistanceMoved} | " +
+ $"{nameof(PowerUpCount).AddSpacesToCamelCase()}: {PowerUpCount} | " +
+ $"{nameof(PushButtonCount).AddSpacesToCamelCase()}: {PushButtonCount} | " +
+ $"{nameof(ConfigurationCount).AddSpacesToCamelCase()}: {ConfigurationCount} | " +
+ $"{nameof(UsbEnumerationsCount).AddSpacesToCamelCase()}: {UsbEnumerationsCount} | " +
+ $"{nameof(TotalCheatAttemptsDetected).AddSpacesToCamelCase()}: {TotalCheatAttemptsDetected} | " +
+ $"{nameof(TotalSecurityLockupCount).AddSpacesToCamelCase()}: {TotalSecurityLockupCount}"
+ : base.ToString();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Responses/Telemetry/TelemetryResponseMessage.cs b/PTI.Rs232Validator/Messages/Responses/Telemetry/TelemetryResponseMessage.cs
new file mode 100644
index 0000000..b09555c
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Responses/Telemetry/TelemetryResponseMessage.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PTI.Rs232Validator.Messages.Responses.Telemetry;
+
+///
+/// An RS-232 telemetry message from an acceptor to a host.
+///
+public class TelemetryResponseMessage : Rs232ResponseMessage
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ public TelemetryResponseMessage(IReadOnlyList payload) : base(payload)
+ {
+ if (PayloadIssues.Count > 0)
+ {
+ return;
+ }
+
+ if (payload.Count < MinPayloadByteSize)
+ {
+ return;
+ }
+
+ if (MessageType != Rs232MessageType.TelemetryCommand)
+ {
+ PayloadIssues.Add(
+ $"The message type is {MessageType}, but {Rs232MessageType.TelemetryCommand} is expected.");
+ return;
+ }
+
+ Data = payload
+ .Skip(3)
+ .Take(payload.Count - MinPayloadByteSize)
+ .ToList()
+ .AsReadOnly();
+ }
+
+ ///
+ /// The data.
+ ///
+ internal IReadOnlyList Data { get; } = [];
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Rs232BaseMessage.cs b/PTI.Rs232Validator/Messages/Rs232BaseMessage.cs
deleted file mode 100644
index 057dfc5..0000000
--- a/PTI.Rs232Validator/Messages/Rs232BaseMessage.cs
+++ /dev/null
@@ -1,134 +0,0 @@
-namespace PTI.Rs232Validator.Messages
-{
- using System.Collections.Generic;
- using System.Linq;
-
- ///
- /// Base RS-232 message
- /// All message have an ACK and host bit
- ///
- public abstract class Rs232BaseMessage
- {
- ///
- /// Create a new message from raw data
- ///
- /// raw message data
- protected Rs232BaseMessage(byte[] messageData)
- {
- RawMessage = messageData;
-
- // If there is data, the 3rd byte is the msg type and ack byte
- if (messageData is null || messageData.Length <= 2)
- {
- return;
- }
-
- // Host: 0x10, Device: 0x20
- IsHostMessage = messageData[2] >> 4 == 1;
-
- // ACK toggles with each successfully message
- Ack = (messageData[2] & 1) == 1;
- }
-
- ///
- /// True if this is a host message
- ///
- public bool IsHostMessage { get; }
-
- ///
- /// Toggle bit state
- ///
- public bool Ack { get; }
-
- ///
- /// Original raw message
- ///
- protected byte[] RawMessage { get; }
-
- ///
- /// Returns a copy of the original message data
- ///
- ///
- public byte[] Serialize()
- {
- return (byte[]) RawMessage.Clone();
- }
-
- ///
- /// Calculate and return the RS-232 checksum for this message
- ///
- /// XOR 1-byt checksum
- protected byte CalculateChecksum()
- {
- // No packet can have less than this many bytes
- if (RawMessage.Length < 5)
- {
- return 0;
- }
-
- byte checksum = 0;
- for (var i = 1; i < RawMessage.Length - 2; ++i)
- {
- checksum ^= RawMessage[i];
- }
-
- return checksum;
- }
-
- ///
- /// Returns true if bit is set in value
- ///
- /// 0-based bit to test
- /// value to test
- /// true if bit is set
- protected static bool IsBitSet(int bit, byte value)
- {
- return (value & (1 << bit)) == 1 << bit;
- }
-
- ///
- /// Returns true if any bits are set in value
- ///
- /// 0-based bits to test
- /// value to test
- /// true if any bit from bits are set
- protected static bool AreAnyBitsSet(IEnumerable bits, byte value)
- {
- return bits.Any(b => IsBitSet(b, value));
- }
-
-
- ///
- /// Returns true if all bits are set in value
- ///
- /// 0-based bits to test
- /// value to test
- /// true if all bits from bits are set
- protected static bool AreAllBitsSet(IEnumerable bits, byte value)
- {
- return bits.All(b => IsBitSet(b, value));
- }
-
- ///
- /// Return value with bit set
- ///
- /// 0-based bit to set
- /// Value to set
- /// Value with bit set
- protected static byte SetBit(int bit, byte value)
- {
- return (byte) (value | (1 << bit));
- }
-
- ///
- /// Return value with bit cleared
- ///
- /// bit to clear
- /// Value to clear
- /// Value with bit cleared
- protected static byte ClearBit(int bit, byte value)
- {
- return (byte) (value & ~(1 << bit));
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Rs232Message.cs b/PTI.Rs232Validator/Messages/Rs232Message.cs
new file mode 100644
index 0000000..4327f98
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Rs232Message.cs
@@ -0,0 +1,64 @@
+using PTI.Rs232Validator.Utility;
+using System.Collections.Generic;
+
+namespace PTI.Rs232Validator.Messages;
+
+///
+/// An RS-232 message.
+/// Each message contains a message type and an ACK number.
+///
+public abstract class Rs232Message
+{
+ ///
+ /// The minimum payload size in bytes.
+ ///
+ protected const byte MinPayloadByteSize = 5;
+
+ ///
+ /// The start of a message payload.
+ ///
+ protected const byte Stx = 0x02;
+
+ ///
+ /// The end of a message payload.
+ ///
+ protected const byte Etx = 0x03;
+
+ ///
+ /// The byte collection representing this instance.
+ ///
+ public abstract IReadOnlyList Payload { get; }
+
+ ///
+ /// The ACK number.
+ ///
+ /// False = 0; True = 1.
+ public bool Ack => Payload.Count >= 3 && Payload[2].IsBitSet(0);
+
+ ///
+ /// An enumerator of .
+ ///
+ public Rs232MessageType MessageType =>
+ Payload.Count >= 3 ? (Rs232MessageType)(Payload[2] & 0b11110000) : Rs232MessageType.Unknown;
+
+ ///
+ /// Calculates the 1-byte XOR checksum of the specified payload.
+ ///
+ /// The payload to calculate the checksum of.
+ /// The checksum.
+ internal static byte CalculateChecksum(IReadOnlyList payload)
+ {
+ if (payload.Count < MinPayloadByteSize)
+ {
+ return 0;
+ }
+
+ byte checksum = 0;
+ for (var i = 1; i < payload.Count - 2; i++)
+ {
+ checksum ^= payload[i];
+ }
+
+ return checksum;
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Rs232MessageType.cs b/PTI.Rs232Validator/Messages/Rs232MessageType.cs
new file mode 100644
index 0000000..04e2c73
--- /dev/null
+++ b/PTI.Rs232Validator/Messages/Rs232MessageType.cs
@@ -0,0 +1,32 @@
+namespace PTI.Rs232Validator.Messages;
+
+///
+/// The RS-232 message types.
+///
+public enum Rs232MessageType : byte
+{
+ ///
+ /// An unknown message type.
+ ///
+ Unknown = 0x00,
+
+ ///
+ /// A poll message from a host to an acceptor.
+ ///
+ HostToAcceptor = 0x10,
+
+ ///
+ /// A poll message from an acceptor to a host.
+ ///
+ AcceptorToHost = 0x20,
+
+ ///
+ /// A telemetry command message.
+ ///
+ TelemetryCommand = 0x60,
+
+ ///
+ /// An extended command message.
+ ///
+ ExtendedCommand = 0x70
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Rs232PollMessage.cs b/PTI.Rs232Validator/Messages/Rs232PollMessage.cs
deleted file mode 100644
index e7ee214..0000000
--- a/PTI.Rs232Validator/Messages/Rs232PollMessage.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-namespace PTI.Rs232Validator.Messages
-{
- ///
- /// Message from host to device
- ///
- internal class Rs232PollMessage : Rs232BaseMessage
- {
- ///
- /// Base !ACK message
- ///
- private static readonly byte[] BaseMessageNoAck =
- {
- 0x02, 0x08, 0x10, 0x00, 0x00, 0x00, 0x03, 0x00
- };
-
- ///
- /// Base ACK message
- ///
- private static readonly byte[] BaseMessageAck =
- {
- 0x02, 0x08, 0x11, 0x00, 0x00, 0x00, 0x03, 0x00
- };
-
- private bool _stack;
- private bool _return;
- private bool _escrow;
- private byte _enableMask;
-
- ///
- /// Create a new polling message in the specified ACK state
- ///
- /// True to set ACK bit
- public Rs232PollMessage(bool ack) : base(ack ? BaseMessageAck : BaseMessageNoAck)
- {
- }
-
- ///
- /// Set enable flags
- /// A bit mask representing which bills to accept
- /// 0b00000001: $1 or first note
- /// 0b00000010: $2 or second note
- /// 0b00000100: $5 or third note
- /// 0b00001000: $10 or fourth note
- /// 0b00010000: $20 or fifth note
- /// 0b00100000: $50 or sixth note
- /// 0b01000000: $100 of seventh note
- ///
- /// Enable mask
- /// this
- public Rs232PollMessage SetEnableMask(byte mask)
- {
- _enableMask = mask;
- RawMessage[3] = (byte) (mask & 0x7F);
- RawMessage[RawMessage.Length - 1] = CalculateChecksum();
- return this;
- }
-
- ///
- /// Set escrow mode bit
- ///
- /// True to enable escrow mode
- /// this
- public Rs232PollMessage SetEscrowMode(bool enabled)
- {
- _escrow = enabled;
- var v = RawMessage[4];
- RawMessage[4] = (byte) (enabled ? v | 0x10 : v & ~0x10);
- RawMessage[RawMessage.Length - 1] = CalculateChecksum();
- return this;
- }
-
- ///
- /// Set stack bit
- ///
- /// true to perform stack
- /// Only used in escrow mode
- /// this
- public Rs232PollMessage SetStack(bool doStack)
- {
- _stack = doStack;
- var v = RawMessage[4];
-
- // Set or clear the stack bit
- RawMessage[4] = (byte) (doStack ? v | 0x20 : v & ~0x20);
- RawMessage[RawMessage.Length - 1] = CalculateChecksum();
-
- return this;
- }
-
- ///
- /// Set return bit
- ///
- /// true to perform bill return
- /// Only used in escrow mode
- /// this
- public Rs232PollMessage SetReturn(bool doReturn)
- {
- _return = doReturn;
- var v = RawMessage[4];
-
- // Set or clear the return bit
- RawMessage[4] = (byte) (doReturn ? v | 0x40 : v & ~0x40);
- RawMessage[RawMessage.Length - 1] = CalculateChecksum();
-
- return this;
- }
-
- ///
- /// Return poll message details as parsed bits
- ///
- ///
- public override string ToString()
- {
- // Fixed width log entry
- return
- $"Ack: {Ack,5}, Enabled: 0b{_enableMask.ToBinary()}, Escrow: {_escrow,5}, Stack: {_stack,5}, Return: {_return,5}";
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Messages/Rs232ResponseMessage.cs b/PTI.Rs232Validator/Messages/Rs232ResponseMessage.cs
deleted file mode 100644
index 4ee3549..0000000
--- a/PTI.Rs232Validator/Messages/Rs232ResponseMessage.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-namespace PTI.Rs232Validator.Messages
-{
- using System.Collections.Generic;
-
- internal abstract class Rs232ResponseMessage : Rs232BaseMessage
- {
- protected readonly IList PacketIssues = new List();
-
- protected Rs232ResponseMessage(byte[] messageData) : base(messageData)
- {
- }
-
- ///
- /// True if packet is well-formed
- ///
- public bool IsValid { get; protected set; }
-
- ///
- /// True when...
- /// * Packet is correct length with correct checksum AND
- /// * one or more reserved bits set OR
- /// * more than one state is set OR
- /// * a state is missing an accompanying but (e.g. stack+credit)
- ///
- public bool HasProtocolViolation { get; protected set; }
-
- ///
- /// Credit index, if any, from this message
- /// Will be null if is false
- ///
- public int? CreditIndex { get; protected set; }
-
- ///
- /// State reported by acceptor
- ///
- public Rs232State State { get; protected set; }
-
- ///
- /// Events reported in this message
- ///
- public Rs232Event Event { get; protected set; }
-
- ///
- /// If true, cash box is attached
- ///
- /// For stackerless models, this will always be true
- public bool IsCashBoxPresent { get; protected set; }
-
- ///
- /// If true, the device might be busy and unable to respond
- ///
- public bool IsEmptyResponse { get; protected set; }
-
- ///
- /// Acceptor model
- ///
- public abstract int Model { get; protected internal set; }
-
- ///
- /// Firmware revision
- /// 1.17 return 17
- ///
- public abstract int Revision { get; protected internal set; }
-
-
- ///
- /// List of packet issues
- ///
- public IEnumerable AllPacketIssues => PacketIssues;
-
- ///
- /// Formats the decoded bits from the data
- ///
- ///
- public override string ToString()
- {
- // Fixed width log entry
- return !IsValid
- ? "Invalid Poll Response"
- : $"State: {State,12}, Event(s): {Event,-24}, Credit: {CreditIndex,4}, Model: 0x{Model:X2}, Rev.: 0x{Revision:X2}, CB Present: {IsCashBoxPresent,5}";
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/NullLogger.cs b/PTI.Rs232Validator/NullLogger.cs
deleted file mode 100644
index 73b8eee..0000000
--- a/PTI.Rs232Validator/NullLogger.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-namespace PTI.Rs232Validator
-{
- ///
- /// A nop logger
- ///
- internal class NullLogger : ILogger
- {
- ///
- public void Trace(string format, params object[] args)
- {
- }
-
- ///
- public void Debug(string format, params object[] args)
- {
- }
-
- ///
- public void Info(string format, params object[] args)
- {
- }
-
- ///
- public void Error(string format, params object[] args)
- {
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/PTI.Rs232Validator.csproj b/PTI.Rs232Validator/PTI.Rs232Validator.csproj
index bb13db4..5f98bf6 100644
--- a/PTI.Rs232Validator/PTI.Rs232Validator.csproj
+++ b/PTI.Rs232Validator/PTI.Rs232Validator.csproj
@@ -1,14 +1,15 @@
-
+
1.1.1.0
2020
+ enable
+ net8.0
-
+
PTI.Rs232Validator
- net461;net472;netcoreapp3.1
latest
AnyCPU
true
@@ -36,19 +37,20 @@
TRACE;DEBUG
portable
-
+
RELEASE
true
-
-
-
-
+
+
+
+
+ <_Parameter1>PTI.Rs232Validator.Test
+
-
-
-
+
+
-
+
diff --git a/PTI.Rs232Validator/Providers/BaseSerialPortProvider.cs b/PTI.Rs232Validator/Providers/BaseSerialPortProvider.cs
deleted file mode 100644
index e735a2c..0000000
--- a/PTI.Rs232Validator/Providers/BaseSerialPortProvider.cs
+++ /dev/null
@@ -1,158 +0,0 @@
-namespace PTI.Rs232Validator.Providers
-{
- using System;
- using System.IO.Ports;
-
- ///
- /// Default RS232 serial port configuration
- /// At 9600 baud with 10 bits per transmit, we have a max data rate
- /// of 89.6 KB/second.
- ///
- public abstract class BaseSerialPortProvider : ISerialProvider
- {
- ///
- /// Original port name
- ///
- private readonly string _portName;
-
- ///
- /// Create a new base port provider on this serial port
- ///
- /// OS port name
- protected BaseSerialPortProvider(string portName)
- {
- _portName = portName;
- }
-
- ///
- /// Native serial port handle
- ///
- protected abstract SerialPort Port { get; }
-
- ///
- public bool IsOpen => Port?.IsOpen ?? false;
-
- ///
- public bool TryOpen()
- {
- try
- {
- if (IsOpen)
- {
- Logger?.Info("{0} Port {1} is already open", GetType().Name, _portName);
- return true;
- }
-
- Port?.Open();
- if (Port is null || !Port.IsOpen)
- {
- return false;
- }
-
- // On open, clear any pending reads or writes
- Port.DiscardInBuffer();
- Port.DiscardOutBuffer();
-
- return true;
- }
- catch (UnauthorizedAccessException)
- {
- Logger?.Error("{0} Failed to open port {1} because it some other process or instance has it open",
- GetType().Name, _portName);
- return false;
- }
- catch (Exception ex)
- {
- Logger?.Error("{0} Failed to open port {1}: {2}", ex.Message, GetType().Name, _portName);
- return false;
- }
- }
-
- ///
- public void Close()
- {
- Port.Close();
- }
-
- ///
- public byte[] Read(int count)
- {
- if (count <= 0)
- {
- throw new ArgumentException($"{count} must be greater than zero");
- }
-
- if (!IsOpen)
- {
- Logger?.Error("{0} Cannot read from port that is not open. Try opening it.", GetType().Name);
- return default;
- }
-
- try
- {
- // Read one byte at a time to avoid timeout issues
- var receive = new byte[count];
- for (var i = 0; i < count; ++i)
- {
- receive[i] = (byte) Port.ReadByte();
- }
-
- Logger?.Trace("{0}<< {1}", GetType().Name, receive.ToHexString());
-
- return receive;
- }
- catch (TimeoutException)
- {
- Logger?.Trace(
- "{0} A read operation timed out. This is expected behavior while the device is feeding or stacking a bill",
- GetType().Name);
- return default;
- }
- catch (Exception ex)
- {
- Logger?.Error("{0} Failed to read port: {1}{2}{3}", GetType().Name, ex.Message, Environment.NewLine,
- ex.StackTrace);
- return default;
- }
- }
-
- ///
- public void Write(byte[] data)
- {
- if (data is null)
- {
- throw new ArgumentNullException(nameof(data));
- }
-
- try
- {
- Logger?.Trace("{0}>> {1}", GetType().Name, data.ToHexString());
-
- Port.Write(data, 0, data.Length);
- }
- catch (TimeoutException)
- {
- Logger?.Trace(
- "{0} A write operation timed out. This is expected behavior while the device is feeding or stacking a bill",
- GetType().Name);
- }
- catch (Exception ex)
- {
- Logger?.Error("{0} Failed to write port: {1}{2}{3}", GetType().Name, ex.Message, Environment.NewLine,
- ex.StackTrace);
- }
- }
-
- ///
- public ILogger Logger { get; set; }
-
- ///
- public void Dispose()
- {
- Port?.Close();
- Port?.Dispose();
-
- Logger?.Trace("{0} disposed", GetType().Name);
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Providers/ISerialProvider.cs b/PTI.Rs232Validator/Providers/ISerialProvider.cs
deleted file mode 100644
index f5417ba..0000000
--- a/PTI.Rs232Validator/Providers/ISerialProvider.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-namespace PTI.Rs232Validator.Providers
-{
- using System;
-
- ///
- /// Serial data provider contract.
- /// You can use this interface to provide your own serial connection or mock interface.
- ///
- public interface ISerialProvider : IDisposable
- {
- ///
- /// Returns true if provider is in a state
- /// that allows for reading and writing of data
- ///
- bool IsOpen { get; }
-
- ///
- /// Optional logger
- ///
- public ILogger Logger { get; set; }
-
- ///
- /// Try to enter the open state or return false
- ///
- /// True on success, otherwise false
- bool TryOpen();
-
- ///
- /// Close the data provider
- ///
- void Close();
-
- ///
- /// Read and return count bytes from provider.
- /// If there is a problem reading from the port, for example
- /// a timeout or IO exception, null will be returned.
- ///
- /// Count of bytes to read
- /// Data from provider
- byte[] Read(int count);
-
- ///
- /// Write data to provider
- ///
- /// Data to write
- void Write(byte[] data);
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Providers/TtlSerialProvider.cs b/PTI.Rs232Validator/Providers/TtlSerialProvider.cs
deleted file mode 100644
index 8c7a46f..0000000
--- a/PTI.Rs232Validator/Providers/TtlSerialProvider.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-namespace PTI.Rs232Validator.Providers
-{
- using System;
- using System.IO.Ports;
-
- ///
- /// Traditional hardware RS-232 using DB9 with full
- /// RTS and DTR support.
- ///
- public sealed class TtlSerialProvider : BaseSerialPortProvider
- {
- ///
- /// Create a new serial port connection
- /// This is for true DB9 serial ports.
- ///
- /// OS name of port
- public TtlSerialProvider(string portName) : base(portName)
- {
- try
- {
- Port = new SerialPort
- {
- BaudRate = 9600,
- Parity = Parity.Even,
- DataBits = 7,
- StopBits = StopBits.One,
- Handshake = Handshake.None,
- ReadTimeout = 250,
- WriteTimeout = 250,
- WriteBufferSize = 1024,
- ReadBufferSize = 1024,
- DtrEnable = true,
- RtsEnable = true,
- DiscardNull = false,
- PortName = portName
- };
- }
- catch (Exception ex)
- {
- Logger?.Error("{0} Failed to create port: {1}{2}{3}", GetType().Name, ex.Message, Environment.NewLine,
- ex.StackTrace);
- }
- }
-
- ///
- protected override SerialPort Port { get; }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Providers/UsbSerialProvider.cs b/PTI.Rs232Validator/Providers/UsbSerialProvider.cs
deleted file mode 100644
index 0b7f149..0000000
--- a/PTI.Rs232Validator/Providers/UsbSerialProvider.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-namespace PTI.Rs232Validator.Providers
-{
- using System;
- using System.IO.Ports;
-
- ///
- /// A serial port provider for USB serial emulators
- /// This provider does not use an RTS or DTR and has a longer
- /// read timeout.
- ///
- public sealed class UsbSerialProvider : BaseSerialPortProvider
- {
- ///
- /// Create a new serial port connection
- /// This is for USB serial port emulators
- ///
- /// OS name of port
- public UsbSerialProvider(string portName) : base(portName)
- {
- try
- {
- Port = new SerialPort
- {
- BaudRate = 9600,
- Parity = Parity.Even,
- DataBits = 7,
- StopBits = StopBits.One,
- Handshake = Handshake.None,
- ReadTimeout = 100,
- WriteTimeout = 100,
- WriteBufferSize = 1024,
- ReadBufferSize = 1024,
- DtrEnable = false,
- RtsEnable = false,
- DiscardNull = false,
- PortName = portName
- };
- }
- catch (Exception ex)
- {
- Logger?.Error("{0} Failed to create port: {1}{2}{3}", GetType().Name, ex.Message, Environment.NewLine,
- ex.StackTrace);
- }
- }
-
- ///
- protected override SerialPort Port { get; }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Rs232Config.cs b/PTI.Rs232Validator/Rs232Config.cs
deleted file mode 100644
index 8a0a4b1..0000000
--- a/PTI.Rs232Validator/Rs232Config.cs
+++ /dev/null
@@ -1,117 +0,0 @@
-namespace PTI.Rs232Validator
-{
- using System;
- using Providers;
-
- ///
- /// RS-232 bill validator options
- ///
- public class Rs232Config
- {
- ///
- /// Default to accepting all notes
- ///
- public const byte DefaultEnableMask = 0x7F;
-
- ///
- /// Default period between polling message sent from host to device
- ///
- public static readonly TimeSpan DefaultPollingPeriod = TimeSpan.FromMilliseconds(100);
-
- ///
- /// Create a new configuration using a custom serial provider
- ///
- /// serial provider implementation
- /// Optional system logger
- public Rs232Config(ISerialProvider provider, ILogger logger = null)
- {
- SerialProvider = provider;
- Logger = logger ?? new NullLogger();
- }
-
- ///
- /// A bit mask representing which bills to accept
- /// 0b00000001: $1 or first note
- /// 0b00000010: $2 or second note
- /// 0b00000100: $5 or third note
- /// 0b00001000: $10 or fourth note
- /// 0b00010000: $20 or fifth note
- /// 0b00100000: $50 or sixth note
- /// 0b01000000: $100 of seventh note
- ///
- public byte EnableMask { get; set; } = DefaultEnableMask;
-
- ///
- /// Escrow mode allows you to manually stack or return a node
- /// based on the credit value reported by the device.
- /// When this mode is enabled, you must manually call
- /// the Stack and Return functions. Otherwise, the device
- /// will perform the stacking and returning automatically
- /// based on the validation of the bill.
- ///
- public bool IsEscrowMode { get; set; }
-
- ///
- /// This protocol reports the cash box state for every polling message.
- /// This may overwhelm your logs so we will reports the event only
- /// once by default. To receive notifications for all cash box
- /// removal messages, set this flag to true.
- ///
- public bool ReportAllCashBoxRemovalEvents { get; set; }
-
- ///
- /// Optionally provide your own serial port or mock implementation
- ///
- public ISerialProvider SerialProvider { get; }
-
- ///
- /// Time period between messages sent from host to device
- ///
- public TimeSpan PollingPeriod { get; set; } = DefaultPollingPeriod;
-
- ///
- /// Automatic logger
- ///
- public ILogger Logger { get; set; }
-
- ///
- /// In strict mode, the acceptor device will be
- /// held the exact communication specification.
- /// Violations will be reported via the logging interface.
- ///
- public bool StrictMode { get; set; }
-
- ///
- /// Do not wait for liveness check when starting the polling loop.
- ///
- public bool DisableLivenessCheck { get; set; }
-
- ///
- /// Create a new config using a USB serial port configuration
- ///
- /// OS port name to use for bill validator connection
- /// Optional system logger
- public static Rs232Config UsbRs232Config(string portName, ILogger logger = null)
- {
- return new Rs232Config(new UsbSerialProvider(portName), logger);
- }
-
- ///
- /// Create a new config using a TTL (DB9) serial port configuration
- ///
- /// OS port name to use for bill validator connection
- /// Optional system logger
- public static Rs232Config TtlRs232Config(string portName, ILogger logger = null)
- {
- return new Rs232Config(new TtlSerialProvider(portName), logger);
- }
-
- ///
- public override string ToString()
- {
- return
- $"EnableMask: {EnableMask:X8}, PollingPeriod: {PollingPeriod}, EscrowMode: {IsEscrowMode}, " +
- $"ReportAllCashBoxRemovalEvents: {ReportAllCashBoxRemovalEvents}, StrictMode: {StrictMode}";
- }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Rs232Configuration.cs b/PTI.Rs232Validator/Rs232Configuration.cs
new file mode 100644
index 0000000..63e1803
--- /dev/null
+++ b/PTI.Rs232Validator/Rs232Configuration.cs
@@ -0,0 +1,42 @@
+using System;
+
+namespace PTI.Rs232Validator;
+
+///
+/// The configuration for communicating with an RS-232 bill acceptor.
+///
+public class Rs232Configuration
+{
+ ///
+ /// The enable mask, which represents types of bills to accept.
+ ///
+ ///
+ /// 0b00000001: only accept the 1st bill type (e.g. $1).
+ /// 0b00000010: only accept the 2nd bill type (e.g. $2).
+ /// 0b00000100: only accept the 3rd bill type (e.g. $5).
+ /// 0b00001000: only accept the 4th bill type (e.g. $10).
+ /// 0b00010000: only accept the 5th bill type (e.g. $20).
+ /// 0b00100000: only accept the 6th bill type (e.g. $50).
+ /// 0b01000000: only accept the 7th bill type (e.g. $100).
+ ///
+ public byte EnableMask { get; set; } = 0x7F;
+
+ ///
+ /// Should the acceptor escrow each bill?
+ ///
+ ///
+ /// Setting this to true will cause the acceptor to place each bill in escrow and wait for the host to stack or return it.
+ /// Setting this to false will cause the acceptor to automatically stack or return each bill.
+ ///
+ public bool ShouldEscrow { get; set; }
+
+ ///
+ /// Should the acceptor detect barcodes?
+ ///
+ public bool ShouldDetectBarcodes { get; set; }
+
+ ///
+ /// The time period between messages sent from the host to the acceptor.
+ ///
+ public TimeSpan PollingPeriod { get; set; } = TimeSpan.FromMilliseconds(100);
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Rs232Event.cs b/PTI.Rs232Validator/Rs232Event.cs
index 23004a2..cf8859f 100644
--- a/PTI.Rs232Validator/Rs232Event.cs
+++ b/PTI.Rs232Validator/Rs232Event.cs
@@ -8,7 +8,7 @@ namespace PTI.Rs232Validator
/// once per occurence.
///
[Flags]
- public enum Rs232Event
+ public enum Rs232Event : byte
{
///
/// No event flags are set
diff --git a/PTI.Rs232Validator/Rs232State.cs b/PTI.Rs232Validator/Rs232State.cs
index b8b918c..a83174e 100644
--- a/PTI.Rs232Validator/Rs232State.cs
+++ b/PTI.Rs232Validator/Rs232State.cs
@@ -29,7 +29,7 @@ public enum Rs232State
Escrowed,
///
- /// A bill is being stacked in the cash box
+ /// A bill is being stacked in the cashbox
///
Stacking,
@@ -44,7 +44,7 @@ public enum Rs232State
BillJammed,
///
- /// The cash box is full
+ /// The cashbox is full
///
StackerFull,
diff --git a/PTI.Rs232Validator/SerialProviders/ISerialProvider.cs b/PTI.Rs232Validator/SerialProviders/ISerialProvider.cs
new file mode 100644
index 0000000..0cf090a
--- /dev/null
+++ b/PTI.Rs232Validator/SerialProviders/ISerialProvider.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace PTI.Rs232Validator.SerialProviders;
+
+///
+/// A provider of serial communication to an external device.
+///
+public interface ISerialProvider : IDisposable
+{
+ ///
+ /// Is there an open connection to the external device?
+ ///
+ bool IsOpen { get; }
+
+ ///
+ /// Tries to open a connection to the external device.
+ ///
+ /// True if successful; otherwise, false.
+ bool TryOpen();
+
+ ///
+ /// Closes the connection to the external device.
+ ///
+ void Close();
+
+ ///
+ /// Reads data from the external device.
+ ///
+ /// The count of bytes to read.
+ ///
+ /// If successful, an array with the requested of bytes;
+ /// otherwise, an array with less than the requested of bytes.
+ ///
+ byte[] Read(uint count);
+
+ ///
+ /// Writes data to the external device.
+ ///
+ /// The data to write.
+ void Write(byte[] data);
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/SerialProviders/SerialProvider.cs b/PTI.Rs232Validator/SerialProviders/SerialProvider.cs
new file mode 100644
index 0000000..fd67acf
--- /dev/null
+++ b/PTI.Rs232Validator/SerialProviders/SerialProvider.cs
@@ -0,0 +1,209 @@
+using PTI.Rs232Validator.Loggers;
+using PTI.Rs232Validator.Utility;
+using System;
+using System.Collections.Generic;
+using System.IO.Ports;
+
+namespace PTI.Rs232Validator.SerialProviders;
+
+///
+/// A runtime implementation of .
+///
+public class SerialProvider : ISerialProvider
+{
+ private bool _isDisposed;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// An instance of .
+ /// .
+ protected SerialProvider(ILogger logger, SerialPort serialPort)
+ {
+ Logger = logger;
+ Port = serialPort;
+ }
+
+ ///
+ public bool IsOpen => Port.IsOpen;
+
+ ///
+ /// An instance of .
+ ///
+ protected ILogger Logger { get; }
+
+ ///
+ /// An instance of .
+ ///
+ protected SerialPort Port { get; }
+
+ ///
+ /// Creates a new instance of for USB serial emulators.
+ ///
+ /// .
+ /// The name of the serial port to use.
+ /// A new instance of .
+ public static ISerialProvider CreateUsbSerialProvider(ILogger logger, string serialPortName)
+ {
+ var serialPort = new SerialPort
+ {
+ BaudRate = 9600,
+ Parity = Parity.Even,
+ DataBits = 7,
+ StopBits = StopBits.One,
+ Handshake = Handshake.None,
+ ReadTimeout = 100,
+ WriteTimeout = 100,
+ WriteBufferSize = 1024,
+ ReadBufferSize = 1024,
+ DtrEnable = false,
+ RtsEnable = false,
+ DiscardNull = false,
+ PortName = serialPortName
+ };
+
+ return new SerialProvider(logger, serialPort);
+ }
+
+ ///
+ /// Creates a new instance of for traditional RS-232 hardware using DB9 with full
+ /// RTS and DTR support.
+ ///
+ ///
+ public static ISerialProvider CreateTtlSerialProvider(ILogger logger, string serialPortName)
+ {
+ var serialPort = new SerialPort
+ {
+ BaudRate = 9600,
+ Parity = Parity.Even,
+ DataBits = 7,
+ StopBits = StopBits.One,
+ Handshake = Handshake.None,
+ ReadTimeout = 250,
+ WriteTimeout = 250,
+ WriteBufferSize = 1024,
+ ReadBufferSize = 1024,
+ DtrEnable = true,
+ RtsEnable = true,
+ DiscardNull = false,
+ PortName = serialPortName
+ };
+
+ return new SerialProvider(logger, serialPort);
+ }
+
+ ///
+ public bool TryOpen()
+ {
+ try
+ {
+ if (IsOpen)
+ {
+ Logger.LogDebug("Tried to open serial port {0}, but it is already open.", Port.PortName);
+ return true;
+ }
+
+ Port.Open();
+ if (!Port.IsOpen)
+ {
+ return false;
+ }
+
+ Port.DiscardInBuffer();
+ Port.DiscardOutBuffer();
+
+ return true;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ Logger.LogDebug("Failed to open serial port {0} because it is already in use.", Port.PortName);
+ return false;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to open serial port {0}: {1}", Port.PortName, ex.Message);
+ return false;
+ }
+ }
+
+ ///
+ public void Close()
+ {
+ Port.Close();
+ }
+
+ ///
+ public byte[] Read(uint count)
+ {
+ if (count == 0)
+ {
+ throw new ArgumentException("Cannot read 0 bytes.", nameof(count));
+ }
+
+ if (!IsOpen)
+ {
+ Logger.LogError("Cannot read data while closed.");
+ return [];
+ }
+
+ var payload = new List();
+ uint index = 0;
+ try
+ {
+ for (index = 0; index < count; index++)
+ {
+ payload.Add((byte)Port.ReadByte());
+ }
+ }
+ catch (TimeoutException)
+ {
+ Logger.LogTrace("A read operation timed out at index {0}.", index);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to read from serial port {0}: {1}", Port.PortName, ex.Message);
+ return [];
+ }
+
+ Logger.LogTrace("Received serial data: {0}", payload.ToArray().ConvertToHexString(true, false));
+ return payload.ToArray();
+ }
+
+ ///
+ public void Write(byte[] data)
+ {
+ if (!IsOpen)
+ {
+ Logger.LogError("Cannot write data while closed.");
+ return;
+ }
+
+ try
+ {
+ Logger.LogTrace("Sent data to serial port: {0}", data.ConvertToHexString(true, false));
+ Port.Write(data, 0, data.Length);
+ }
+ catch (TimeoutException)
+ {
+ Logger.LogTrace("A write operation timed out.");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to write to serial port {0}: {1}", Port.PortName, ex.Message);
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ Port.Close();
+ Port.Dispose();
+ _isDisposed = true;
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/StateChangeArgs.cs b/PTI.Rs232Validator/StateChangeArgs.cs
deleted file mode 100644
index 4a3375f..0000000
--- a/PTI.Rs232Validator/StateChangeArgs.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-namespace PTI.Rs232Validator
-{
- using System;
-
- ///
- /// State change information reports prior state and new state
- ///
- public class StateChangeArgs : EventArgs
- {
- ///
- /// Create a new start transition event argument
- ///
- /// Device's most recent state
- /// Device current state
- public StateChangeArgs(Rs232State oldState, Rs232State newState)
- {
- OldState = oldState;
- NewState = newState;
- }
-
- ///
- /// The most recent state of the device
- ///
- public Rs232State OldState { get; }
-
- ///
- /// The new state of the device
- ///
- public Rs232State NewState { get; }
- }
-}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/StateChangedEventArgs.cs b/PTI.Rs232Validator/StateChangedEventArgs.cs
new file mode 100644
index 0000000..757e062
--- /dev/null
+++ b/PTI.Rs232Validator/StateChangedEventArgs.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace PTI.Rs232Validator;
+
+///
+/// An implementation of that contains information about a state change of an acceptor.
+///
+public class StateChangedEventArgs : EventArgs
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// .
+ /// .
+ public StateChangedEventArgs(Rs232State oldState, Rs232State newState)
+ {
+ OldState = oldState;
+ NewState = newState;
+ }
+
+ ///
+ /// The previous state of the device.
+ ///
+ public Rs232State OldState { get; }
+
+ ///
+ /// The new state of the device.
+ ///
+ public Rs232State NewState { get; }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Utility/ByteExtensions.cs b/PTI.Rs232Validator/Utility/ByteExtensions.cs
new file mode 100644
index 0000000..cd327a8
--- /dev/null
+++ b/PTI.Rs232Validator/Utility/ByteExtensions.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace PTI.Rs232Validator.Utility;
+
+///
+/// A container of extension methods for .
+///
+public static class ByteExtensions
+{
+ ///
+ /// Indicates whether the specified bit is set (i.e. 1).
+ ///
+ /// The byte to observe.
+ /// The 0-based index of the bit (e.g. 0 => 2^0).
+ ///
+ public static bool IsBitSet(this byte b, byte bitIndex)
+ {
+ return (b & (1 << bitIndex)) != 0;
+ }
+
+ ///
+ /// Sets the specified bit.
+ ///
+ /// The byte to mutate.
+ /// The 0-based index of the bit to set (e.g. 0 => 2^0).
+ /// The mutated byte.
+ public static byte SetBit(this byte b, byte bitIndex)
+ {
+ return (byte)(b | (1 << bitIndex));
+ }
+
+ ///
+ /// Clears the specified bit.
+ ///
+ /// The byte to mutate.
+ /// The 0-based index of the bit to clear (e.g. 0 => 2^0).
+ /// The mutated byte.
+ public static byte ClearBit(this byte b, byte bitIndex)
+ {
+ return (byte)(b & ~(1 << bitIndex));
+ }
+
+ ///
+ /// Converts the specified byte to a string representation of its binary value.
+ ///
+ /// The byte to convert.
+ /// True to include the binary prefix "0b"; otherwise, false.
+ /// The binary string.
+ ///
+ ///
+ /// byte b = 0b00000001;
+ /// Console.WriteLine(b.ConvertToBinary(true)); // Output: 0b00000001
+ /// Console.WriteLine(b.ConvertToBinary(false)); // Output: 00000001
+ ///
+ ///
+ public static string ConvertToBinaryString(this byte b, bool shouldIncludePrefix)
+ {
+ var prefix = shouldIncludePrefix ? "0b" : string.Empty;
+ return prefix + Convert.ToString(b, 2).PadLeft(8, '0');
+ }
+
+ ///
+ /// Converts the specified 4-byte collection to a 16-bit unsigned integer via 4-bit encoding under big-endian order.
+ ///
+ /// The 4-byte collection to convert.
+ /// The 16-bit unsigned integer.
+ public static ushort ConvertToUint16Via4BitEncoding(this IReadOnlyList bytes)
+ {
+ const byte expectedByteSize = 4;
+ if (bytes.Count != expectedByteSize)
+ {
+ throw new ArgumentException($"The byte collection size is {bytes.Count}, but 4 is expected.", nameof(bytes));
+ }
+
+ ushort result = 0;
+ byte j = 0;
+ for (var i = 0; i < expectedByteSize; i += 2)
+ {
+ result |= (ushort)((bytes[i] << 4 | bytes[i + 1]) << (8 - 8 * j));
+ j++;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Converts the specified 8-byte collection to a 32-bit unsigned integer via 4-bit encoding under big-endian order.
+ ///
+ /// The 8-byte collection to convert.
+ /// The 32-bit unsigned integer.
+ public static uint ConvertToUint32Via4BitEncoding(this IReadOnlyList bytes)
+ {
+ const byte expectedByteSize = 8;
+ if (bytes.Count != expectedByteSize)
+ {
+ throw new ArgumentException($"The byte collection size is {bytes.Count}, but 8 is expected.", nameof(bytes));
+ }
+
+ uint result = 0;
+ byte j = 0;
+ for (var i = 0; i < expectedByteSize; i += 2)
+ {
+ result |= (uint)((bytes[i] << 4 | bytes[i + 1]) << (24 - 8 * j));
+ j++;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Clears the 8th bit of each byte in the specified collection.
+ ///
+ /// The byte collection to mutate.
+ /// The mutated byte collection.
+ public static byte[] ClearEighthBits(this byte[] bytes)
+ {
+ for (var i = 0; i < bytes.Length; i++)
+ {
+ bytes[i] = bytes[i].ClearBit(7);
+ }
+
+ return bytes;
+ }
+
+ ///
+ /// Converts the specified byte collection to a hexadecimal string.
+ ///
+ /// The byte collection to convert.
+ /// True to include the hex prefix "0x"; otherwise, false.
+ /// True to include spaces between each byte; otherwise, false.
+ /// The hexadecimal string.
+ ///
+ ///
+ /// var bytes = new byte[] { 0x01, 0x02, 0x03, 0x04 };
+ /// Console.WriteLine(bytes.ConvertToHexString(true, true)); // Output: 0x01 0x02 0x03 0x04
+ /// Console.WriteLine(bytes.ConvertToHexString(true, false)); // Output: 0x01020304
+ /// Console.WriteLine(bytes.ConvertToHexString(false, true)); // Output: 01 02 03 04
+ /// Console.WriteLine(bytes.ConvertToHexString(false, false)); // Output: 01020304
+ ///
+ ///
+ public static string ConvertToHexString(this IReadOnlyList bytes, bool shouldIncludeHexPrefix, bool shouldIncludeSpaces)
+ {
+ if (bytes.Count == 0)
+ {
+ return string.Empty;
+ }
+
+ var hexString = new StringBuilder(bytes.Count * 2);
+ for (var i = 0; i < bytes.Count; i++)
+ {
+ if (shouldIncludeHexPrefix && (shouldIncludeSpaces || i == 0))
+ {
+ hexString.Append("0x");
+ }
+
+ hexString.Append(bytes[i].ToString("X2"));
+
+ if (shouldIncludeSpaces && i < bytes.Count - 1)
+ {
+ hexString.Append(' ');
+ }
+ }
+
+ return hexString.ToString();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validator/Utility/StringExtensions.cs b/PTI.Rs232Validator/Utility/StringExtensions.cs
new file mode 100644
index 0000000..a9afd7a
--- /dev/null
+++ b/PTI.Rs232Validator/Utility/StringExtensions.cs
@@ -0,0 +1,22 @@
+using System.Text.RegularExpressions;
+
+namespace PTI.Rs232Validator.Utility;
+
+///
+/// A container of extension methods for .
+///
+public static partial class StringExtensions
+{
+ ///
+ /// Adds spaces between words in the specified camelCase or PascalCase string.
+ ///
+ /// The string to mutate.
+ /// The mutated string.
+ public static string AddSpacesToCamelCase(this string input)
+ {
+ return BetweenCamelCaseWordsRegex().Replace(input, " ");
+ }
+
+ [GeneratedRegex("(?<=[a-z])(?=[A-Z0-9])|(?<=[A-Z])(?=[A-Z][a-z])")]
+ private static partial Regex BetweenCamelCaseWordsRegex();
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/Controls/ActionResultDisplay.xaml b/PTI.Rs232Validtor.Desktop/Controls/ActionResultDisplay.xaml
new file mode 100644
index 0000000..f4251fe
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/Controls/ActionResultDisplay.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/Controls/ActionResultDisplay.xaml.cs b/PTI.Rs232Validtor.Desktop/Controls/ActionResultDisplay.xaml.cs
new file mode 100644
index 0000000..1cd73d0
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/Controls/ActionResultDisplay.xaml.cs
@@ -0,0 +1,48 @@
+using System.Windows;
+
+namespace PTI.Rs232Validator.Desktop.Controls;
+
+///
+/// A control containing a button to perform some action and a label to display the result.
+///
+public partial class ActionResultDisplay
+{
+ public static readonly DependencyProperty ActionButtonContentProperty = DependencyProperty.Register(
+ nameof(ActionButtonContent), typeof(object), typeof(ActionResultDisplay), new PropertyMetadata(default(object)));
+
+ public static readonly DependencyProperty ResultDescriptionProperty = DependencyProperty.Register(
+ nameof(ResultDescription), typeof(string), typeof(ActionResultDisplay), new PropertyMetadata(default(string)));
+
+ public static readonly DependencyProperty ResultValueProperty = DependencyProperty.Register(
+ nameof(ResultValue), typeof(object), typeof(ActionResultDisplay), new PropertyMetadata(default(object)));
+
+ public ActionResultDisplay()
+ {
+ InitializeComponent();
+ }
+
+ public event RoutedEventHandler? OnButtonClick;
+
+ public object ActionButtonContent
+ {
+ get => GetValue(ActionButtonContentProperty);
+ set => SetValue(ActionButtonContentProperty, value);
+ }
+
+ public string ResultDescription
+ {
+ get => (string) GetValue(ResultDescriptionProperty);
+ set => SetValue(ResultDescriptionProperty, value);
+ }
+
+ public object ResultValue
+ {
+ get => GetValue(ResultValueProperty);
+ set => SetValue(ResultValueProperty, value);
+ }
+
+ private void Button_OnClick(object sender, RoutedEventArgs e)
+ {
+ OnButtonClick?.Invoke(sender, e);
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/PTI.Rs232Validator.Desktop.csproj b/PTI.Rs232Validtor.Desktop/PTI.Rs232Validator.Desktop.csproj
new file mode 100644
index 0000000..44aa8b2
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/PTI.Rs232Validator.Desktop.csproj
@@ -0,0 +1,57 @@
+
+
+ net8.0-windows
+ WinExe
+ false
+ true
+ true
+ enable
+ latest
+
+
+ icon.ico
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Wpf
+ Designer
+
+
+
+
+ MSBuild:Compile
+ Wpf
+ Designer
+
+
+
+
+ MainWindow.xaml
+
+
+ MainWindow.xaml
+
+
+ MainWindow.xaml
+
+
+ MainWindow.xaml
+
+
+ MainWindow.xaml
+
+
+ MainWindow.xaml
+
+
+ MainWindow.xaml
+
+
+
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/Themes/purple.xaml b/PTI.Rs232Validtor.Desktop/Themes/purple.xaml
new file mode 100644
index 0000000..3f6e856
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/Themes/purple.xaml
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/Utility/BoolNegationConverter.cs b/PTI.Rs232Validtor.Desktop/Utility/BoolNegationConverter.cs
new file mode 100644
index 0000000..279e634
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/Utility/BoolNegationConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace PTI.Rs232Validator.Desktop.Utility;
+
+///
+/// An instance of that negates a boolean value.
+///
+public class BoolNegationConverter : IValueConverter
+{
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is not bool boolValue)
+ {
+ return false;
+ }
+
+ return !boolValue;
+ }
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is not bool boolValue)
+ {
+ return false;
+ }
+
+ return !boolValue;
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/Utility/EnumBindingSourceExtension.cs b/PTI.Rs232Validtor.Desktop/Utility/EnumBindingSourceExtension.cs
new file mode 100644
index 0000000..09362f6
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/Utility/EnumBindingSourceExtension.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Windows.Markup;
+
+namespace PTI.Rs232Validator.Desktop.Utility;
+
+///
+/// An implementation of that allows enumerations to be used as a binding source.
+///
+public class EnumBindingSourceExtension : MarkupExtension
+{
+ private readonly Type _enumType;
+
+ public EnumBindingSourceExtension(Type enumType)
+ {
+ if (!enumType.IsEnum)
+ {
+ throw new ArgumentException("The provided type is not an enumeration.", nameof(enumType));
+ }
+
+ _enumType = enumType;
+ }
+
+ public override object ProvideValue(IServiceProvider serviceProvider)
+ {
+ return Enum.GetValues(_enumType);
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/Views/App.xaml b/PTI.Rs232Validtor.Desktop/Views/App.xaml
new file mode 100644
index 0000000..4ed4a04
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/Views/App.xaml
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/Views/App.xaml.cs b/PTI.Rs232Validtor.Desktop/Views/App.xaml.cs
new file mode 100644
index 0000000..5cc0db3
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/Views/App.xaml.cs
@@ -0,0 +1,3 @@
+namespace PTI.Rs232Validator.Desktop.Views;
+
+public partial class App;
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/Views/MainWindow.Bank.cs b/PTI.Rs232Validtor.Desktop/Views/MainWindow.Bank.cs
new file mode 100644
index 0000000..3bd6b5e
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/Views/MainWindow.Bank.cs
@@ -0,0 +1,178 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace PTI.Rs232Validator.Desktop.Views;
+
+// This portion defines USD bill counts and their cumulative total.
+partial class MainWindow
+{
+ #region Fields
+
+ private static readonly ReadOnlyDictionary UsdBillValues = new Dictionary
+ {
+ { 1, 1 },
+ { 2, 2 },
+ { 3, 5 },
+ { 4, 10 },
+ { 5, 20 },
+ { 6, 50 },
+ { 7, 100 }
+ }.AsReadOnly();
+
+ private int _bill1Count;
+ private int _bill2Count;
+ private int _bill3Count;
+ private int _bill4Count;
+ private int _bill5Count;
+ private int _bill6Count;
+ private int _bill7Count;
+ private int _total;
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Count of bill type 1.
+ ///
+ public int Bill1Count
+ {
+ get => _bill1Count;
+ set
+ {
+ _bill1Count = value;
+ NotifyPropertyChanged(nameof(Bill1Count));
+ }
+ }
+
+ ///
+ /// Count of bill type 2.
+ ///
+ public int Bill2Count
+ {
+ get => _bill2Count;
+ set
+ {
+ _bill2Count = value;
+ NotifyPropertyChanged(nameof(Bill2Count));
+ }
+ }
+
+ ///
+ /// Count of bill type 3.
+ ///
+ public int Bill3Count
+ {
+ get => _bill3Count;
+ set
+ {
+ _bill3Count = value;
+ NotifyPropertyChanged(nameof(Bill3Count));
+ }
+ }
+
+ ///
+ /// Count of bill type 4.
+ ///
+ public int Bill4Count
+ {
+ get => _bill4Count;
+ set
+ {
+ _bill4Count = value;
+ NotifyPropertyChanged(nameof(Bill4Count));
+ }
+ }
+
+ ///
+ /// Count of bill type 5.
+ ///
+ public int Bill5Count
+ {
+ get => _bill5Count;
+ set
+ {
+ _bill5Count = value;
+ NotifyPropertyChanged(nameof(Bill5Count));
+ }
+ }
+
+ ///
+ /// Count of bill type 6.
+ ///
+ public int Bill6Count
+ {
+ get => _bill6Count;
+ set
+ {
+ _bill6Count = value;
+ NotifyPropertyChanged(nameof(Bill6Count));
+ }
+ }
+
+ ///
+ /// Count of bill type 7.
+ ///
+ public int Bill7Count
+ {
+ get => _bill7Count;
+ set
+ {
+ _bill7Count = value;
+ NotifyPropertyChanged(nameof(Bill7Count));
+ }
+ }
+
+ ///
+ /// Cumulative total of all bills in US dollars.
+ ///
+ public int Total
+ {
+ get => _total;
+ set
+ {
+ _total = value;
+ NotifyPropertyChanged(nameof(Total));
+ }
+ }
+
+ #endregion
+
+ ///
+ /// Increments the bill count and total when a bill is stacked.
+ ///
+ private void BillValidator_OnBillStacked(object? sender, byte billType)
+ {
+ switch (billType)
+ {
+ case 1:
+ Bill1Count++;
+ break;
+ case 2:
+ Bill2Count++;
+ break;
+ case 3:
+ Bill3Count++;
+ break;
+ case 4:
+ Bill4Count++;
+ break;
+ case 5:
+ Bill5Count++;
+ break;
+ case 6:
+ Bill6Count++;
+ break;
+ case 7:
+ Bill7Count++;
+ break;
+ default:
+ LogInfo("Stacked an unknown bill type: {0}.", billType);
+ return;
+ }
+
+ var value = UsdBillValues[billType];
+ Total += value;
+ LogInfo("Stacked a bill of type {0} and added ${1} to total.", billType, value);
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/Views/MainWindow.EnableMask.cs b/PTI.Rs232Validtor.Desktop/Views/MainWindow.EnableMask.cs
new file mode 100644
index 0000000..4f4f5ab
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/Views/MainWindow.EnableMask.cs
@@ -0,0 +1,33 @@
+using System.Windows;
+
+namespace PTI.Rs232Validator.Desktop.Views;
+
+// This portion mutates the enable mask of the RS-232 configuration.
+public partial class MainWindow
+{
+ private byte GetEnableMask()
+ {
+ var enableMask = 0;
+ enableMask |= EnableMaskCheckBox1?.IsChecked is not null && EnableMaskCheckBox1.IsChecked.Value ? 1 << 0 : 0;
+ enableMask |= EnableMaskCheckBox2?.IsChecked is not null && EnableMaskCheckBox2.IsChecked.Value ? 1 << 1 : 0;
+ enableMask |= EnableMaskCheckBox3?.IsChecked is not null && EnableMaskCheckBox3.IsChecked.Value ? 1 << 2 : 0;
+ enableMask |= EnableMaskCheckBox4?.IsChecked is not null && EnableMaskCheckBox4.IsChecked.Value ? 1 << 3 : 0;
+ enableMask |= EnableMaskCheckBox5?.IsChecked is not null && EnableMaskCheckBox5.IsChecked.Value ? 1 << 4 : 0;
+ enableMask |= EnableMaskCheckBox6?.IsChecked is not null && EnableMaskCheckBox6.IsChecked.Value ? 1 << 5 : 0;
+ enableMask |= EnableMaskCheckBox7?.IsChecked is not null && EnableMaskCheckBox7.IsChecked.Value ? 1 << 6 : 0;
+ return (byte)enableMask;
+ }
+
+ ///
+ /// Mutates for .
+ ///
+ private void EnableMaskCheckbox_Changed(object sender, RoutedEventArgs e)
+ {
+ if (_rs232Configuration is null)
+ {
+ return;
+ }
+
+ _rs232Configuration.EnableMask = GetEnableMask();
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/Views/MainWindow.Escrow.cs b/PTI.Rs232Validtor.Desktop/Views/MainWindow.Escrow.cs
new file mode 100644
index 0000000..11c5f3d
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/Views/MainWindow.Escrow.cs
@@ -0,0 +1,101 @@
+using System.Windows;
+
+namespace PTI.Rs232Validator.Desktop.Views;
+
+// This portion processes bills in escrow.
+partial class MainWindow
+{
+ private static readonly object ManualLock = new();
+ private bool _isInEscrowMode;
+ private bool _isBillInEscrow;
+
+ ///
+ /// .
+ ///
+ public bool IsInEscrowMode
+ {
+ get => _isInEscrowMode;
+ set
+ {
+ if (_rs232Configuration is not null)
+ {
+ _rs232Configuration.ShouldEscrow = value;
+ }
+
+ _isInEscrowMode = value;
+ NotifyPropertyChanged(nameof(IsInEscrowMode));
+ }
+ }
+
+ ///
+ /// Is a bill in escrow?
+ ///
+ public bool IsBillInEscrow
+ {
+ get => _isBillInEscrow;
+ set
+ {
+ _isBillInEscrow = value;
+ NotifyPropertyChanged(nameof(IsBillInEscrow));
+ }
+ }
+
+ private void BillValidator_OnBillEscrowed(object? sender, byte billType)
+ {
+ LogInfo("Escrowed a bill of type {0}.", billType);
+
+ DoOnUiThread(() =>
+ {
+ // Rejects are triggered by:
+ // 1) invalid bills
+ // 2) cheat attempts
+
+ // Returns are triggered by:
+ // 1) bills disabled by the enable mask
+ // 2) manual delivery of a poll message requesting that the bill be returned
+
+ lock (ManualLock)
+ {
+ IsBillInEscrow = true;
+ }
+ });
+ }
+
+ ///
+ /// Notifies to stack the bill in escrow.
+ ///
+ private void StackButton_Click(object sender, RoutedEventArgs e)
+ {
+ var billValidator = GetBillValidatorOrShowMessage();
+ if (billValidator is null)
+ {
+ return;
+ }
+
+ billValidator.StackBill();
+
+ lock (ManualLock)
+ {
+ IsBillInEscrow = false;
+ }
+ }
+
+ ///
+ /// Notifies to return the bill in escrow.
+ ///
+ private void ReturnButton_Click(object sender, RoutedEventArgs e)
+ {
+ var billValidator = GetBillValidatorOrShowMessage();
+ if (billValidator is null)
+ {
+ return;
+ }
+
+ billValidator.ReturnBill();
+
+ lock (ManualLock)
+ {
+ IsBillInEscrow = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/Views/MainWindow.Extended.cs b/PTI.Rs232Validtor.Desktop/Views/MainWindow.Extended.cs
new file mode 100644
index 0000000..3d0a1ef
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/Views/MainWindow.Extended.cs
@@ -0,0 +1,59 @@
+using System.Windows;
+
+namespace PTI.Rs232Validator.Desktop.Views;
+
+// This portion communicates with an acceptor via extended commands.
+public partial class MainWindow
+{
+ private bool _isBarcodeDetectionEnabled;
+
+ ///
+ /// .
+ ///
+ public bool IsBarcodeDetectionEnabled
+ {
+ get => _isBarcodeDetectionEnabled;
+ set
+ {
+ if (_rs232Configuration is not null)
+ {
+ _rs232Configuration.ShouldDetectBarcodes = value;
+ }
+
+ _isBarcodeDetectionEnabled = value;
+ NotifyPropertyChanged(nameof(IsBarcodeDetectionEnabled));
+ }
+ }
+
+ private void BillValidator_OnBarcodeDetected(object? sender, string barcode)
+ {
+ LogInfo("Detected barcode: {0}.", barcode);
+ DoOnUiThread(() => GetDetectedBarcodeDisplay.ResultValue = barcode);
+ }
+
+ private async void GetDetectedBarcodeDisplay_OnClickAsync(object sender, RoutedEventArgs e)
+ {
+ var billValidator = GetBillValidatorOrShowMessage();
+ if (billValidator is null)
+ {
+ return;
+ }
+
+ var responseMessage = await billValidator.GetDetectedBarcode();
+ string resultValue;
+ if (responseMessage is { IsValid: true, Barcode.Length: > 0 })
+ {
+ resultValue = responseMessage.Barcode;
+ }
+ else if (responseMessage is { IsValid: true, Barcode.Length: 0 })
+ {
+ resultValue = "No barcode was detected since the last power cycle.";
+ }
+ else
+ {
+ resultValue = ErrorMessage;
+ }
+
+ DoOnUiThread(() => GetDetectedBarcodeDisplay.ResultValue = resultValue);
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/Views/MainWindow.Logger.cs b/PTI.Rs232Validtor.Desktop/Views/MainWindow.Logger.cs
new file mode 100644
index 0000000..be4c2c0
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/Views/MainWindow.Logger.cs
@@ -0,0 +1,153 @@
+using PTI.Rs232Validator.Loggers;
+using PTI.Rs232Validator.Utility;
+using System;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Windows.Controls;
+
+namespace PTI.Rs232Validator.Desktop.Views;
+
+///
+/// A log entry.
+///
+public record LogEntry(LogLevel Level, string Timestamp, string Message);
+
+///
+/// A payload exchange between a host and an acceptor.
+///
+public record PayloadExchange(
+ string Timestamp,
+ string RequestPayload,
+ string RequestDecodedInfo,
+ string ResponsePayload,
+ string ResponseDecodedInfo);
+
+// This portion provides logging.
+public partial class MainWindow : ILogger
+{
+ private const string TimestampFormat = "MM/dd/yyyy hh:mm:ss tt";
+
+ private ScrollViewer? _logScrollViewer;
+ private ScrollViewer? _payloadScrollViewer;
+ private bool _isAutoScrollEnabledForLogs = true;
+ private bool _isAutoScrollEnabledForPayloads = true;
+
+ ///
+ /// A collection of instances.
+ ///
+ public ObservableCollection LogEntries { get; } = [];
+
+ ///
+ /// A collection of instances.
+ ///
+ public ObservableCollection PayloadExchanges { get; } = [];
+
+ ///
+ public void LogTrace(string format, params object[] args)
+ {
+ // Do nothing.
+ }
+
+ ///
+ public void LogDebug(string format, params object[] args)
+ {
+ // Do nothing.
+ }
+
+ ///
+ public void LogInfo(string format, params object[] args)
+ {
+ Log(LogLevel.Info, format, args);
+ }
+
+ ///
+ public void LogError(string format, params object[] args)
+ {
+ Log(LogLevel.Error, format, args);
+ }
+
+ private void Log(LogLevel level, string format, params object[] args)
+ {
+ DoOnUiThread(() =>
+ {
+ LogEntries.Add(new LogEntry(level, DateTimeOffset.Now.ToString(TimestampFormat),
+ string.Format(format, args)));
+
+ foreach (var column in LogGridView.Columns)
+ {
+ column.Width = column.ActualWidth;
+ column.Width = double.NaN;
+ }
+ });
+ }
+
+ private void BillValidator_OnCommunicationAttempted(object? sender, CommunicationAttemptedEventArgs e)
+ {
+ DoOnUiThread(() =>
+ {
+ PayloadExchanges.Add(new PayloadExchange(
+ DateTimeOffset.Now.ToString(TimestampFormat),
+ e.RequestMessage.Payload.ConvertToHexString(false, true),
+ e.RequestMessage.ToString(),
+ e.ResponseMessage.Payload.ConvertToHexString(false, true),
+ e.ResponseMessage.ToString()));
+
+ foreach (var column in PayloadGridView.Columns)
+ {
+ column.Width = column.ActualWidth;
+ column.Width = double.NaN;
+ }
+ });
+ }
+
+ private void SetUpLogAutoScroll()
+ {
+ LogEntries.CollectionChanged += (_, e) =>
+ {
+ if (e.Action != NotifyCollectionChangedAction.Add)
+ {
+ return;
+ }
+
+ DoOnUiThread(() =>
+ {
+ if (_isAutoScrollEnabledForLogs)
+ {
+ _logScrollViewer?.ScrollToBottom();
+ }
+ });
+ };
+
+ PayloadExchanges.CollectionChanged += (_, e) =>
+ {
+ if (e.Action != NotifyCollectionChangedAction.Add)
+ {
+ return;
+ }
+
+ DoOnUiThread(() =>
+ {
+ if (_isAutoScrollEnabledForPayloads)
+ {
+ _payloadScrollViewer?.ScrollToBottom();
+ }
+ });
+ };
+ }
+
+ private void LogListView_ScrollChanged(object sender, ScrollChangedEventArgs e)
+ {
+ _logScrollViewer ??= (ScrollViewer)e.OriginalSource;
+
+ var lastExtentHeight = e.ExtentHeight - e.ExtentHeightChange;
+ _isAutoScrollEnabledForLogs = e.VerticalOffset + e.ViewportHeight >= lastExtentHeight;
+ }
+
+ private void PayloadListView_ScrollChanged(object sender, ScrollChangedEventArgs e)
+ {
+ _payloadScrollViewer ??= (ScrollViewer)e.OriginalSource;
+
+ var lastExtentHeight = e.ExtentHeight - e.ExtentHeightChange;
+ _isAutoScrollEnabledForPayloads = e.VerticalOffset + e.ViewportHeight >= lastExtentHeight;
+ }
+}
\ No newline at end of file
diff --git a/PTI.Rs232Validtor.Desktop/Views/MainWindow.StatesAndEvents.cs b/PTI.Rs232Validtor.Desktop/Views/MainWindow.StatesAndEvents.cs
new file mode 100644
index 0000000..a170a54
--- /dev/null
+++ b/PTI.Rs232Validtor.Desktop/Views/MainWindow.StatesAndEvents.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Linq;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace PTI.Rs232Validator.Desktop.Views;
+
+// This portion displays the current state and latest events of an acceptor.
+partial class MainWindow
+{
+ private static readonly SolidColorBrush InactiveBrush = new(Colors.LightGray);
+ private static readonly SolidColorBrush CashBoxAttachedBrush = new(Colors.LightYellow);
+ private static readonly SolidColorBrush ActiveEventBrush = new(Colors.LightGreen);
+ private static readonly SolidColorBrush ActiveStateBrush = new(Colors.LightBlue);
+
+ private Rs232State _state = Rs232State.None;
+ private Rs232Event _event = Rs232Event.None;
+
+ ///
+ /// The enumerator for .
+ ///
+ public Rs232State State
+ {
+ get => _state;
+ set
+ {
+ DoOnUiThread(() =>
+ {
+ DeactivateButtonsWithTag(_stateTagText);
+ switch (value)
+ {
+ case Rs232State.Idling:
+ IdlingButton.Background = ActiveStateBrush;
+ break;
+ case Rs232State.Accepting:
+ AcceptingButton.Background = ActiveStateBrush;
+ break;
+ case Rs232State.Escrowed:
+ EscrowedButton.Background = ActiveStateBrush;
+ break;
+ case Rs232State.Stacking:
+ StackingButton.Background = ActiveStateBrush;
+ break;
+ case Rs232State.Returning:
+ ReturningButton.Background = ActiveStateBrush;
+ break;
+ case Rs232State.BillJammed:
+ BillJammedButton.Background = ActiveStateBrush;
+ break;
+ case Rs232State.StackerFull:
+ StackerFullButton.Background = ActiveStateBrush;
+ break;
+ case Rs232State.Failure:
+ FailureButton.Background = ActiveStateBrush;
+ break;
+ }
+ });
+
+ _state = value;
+ NotifyPropertyChanged(nameof(State));
+ }
+ }
+
+ ///
+ /// The enumerators reported by .
+ ///
+ public Rs232Event Event
+ {
+ get => _event;
+ set
+ {
+ DoOnUiThread(() =>
+ {
+ DeactivateButtonsWithTag(_eventTagText);
+
+ if (value.HasFlag(Rs232Event.Stacked))
+ {
+ StackedButton.Background = ActiveEventBrush;
+ }
+ if (value.HasFlag(Rs232Event.Returned))
+ {
+ ReturnedButton.Background = ActiveEventBrush;
+ }
+ if (value.HasFlag(Rs232Event.Cheated))
+ {
+ CheatedButton.Background = ActiveEventBrush;
+ }
+ if (value.HasFlag(Rs232Event.BillRejected))
+ {
+ RejectedButton.Background = ActiveEventBrush;
+ }
+ if (value.HasFlag(Rs232Event.PowerUp))
+ {
+ LogInfo("The bill acceptor was powered up.");
+ }
+ });
+
+ _event = value;
+ NotifyPropertyChanged(nameof(Event));
+ }
+ }
+
+ private void BillValidator_OnStateChanged(object? sender, StateChangedEventArgs eventArgs)
+ {
+ LogInfo("The state changed from {0} to {1}.", eventArgs.OldState, eventArgs.NewState);
+ State = eventArgs.NewState;
+ }
+
+ private void BillValidator_OnEventReported(object? sender, Rs232Event rs232Event)
+ {
+ LogInfo("Received event(s): {0}.", rs232Event);
+ Event = rs232Event;
+ }
+
+ private void BillValidator_CashboxAttached(object? sender, EventArgs e)
+ {
+ LogInfo("The cashbox was attached.");
+ DoOnUiThread(() => CashboxButton.Background = CashBoxAttachedBrush);
+ }
+
+ private void BillValidator_CashboxRemoved(object? sender, EventArgs e)
+ {
+ LogInfo("The cashbox was removed.");
+ DoOnUiThread(() => CashboxButton.Background = InactiveBrush);
+ }
+
+ private void DeactivateButtonsWithTag(string tagText)
+ {
+ var buttons = StateMachine.Children.OfType