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 @@ + + + + + + + + + + + + +