diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX.csproj b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX.csproj new file mode 100644 index 0000000..eb0260b --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX.csproj @@ -0,0 +1,29 @@ + + + + net8.0;net6.0;netstandard2.1 + 1.0.0 + + + enable + + CS1591;$(NoWarn) + + + + Provides APIs to operate ROHM BP35A1 and other ROHM Wi-SUN modules using the SKSTACK-IP command. + 2021 + + + + SKSTACK,SKSTACK-IP,BP35A1,ROHM-BP35A1,Wi-SUN + + + + + + + diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35A1.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35A1.cs new file mode 100644 index 0000000..29ceb35 --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35A1.cs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT + +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Smdn.Devices.BP35XX; + +public class BP35A1 : BP35Base { + /// + /// Refer to the initial value of baud rate for UART setting in the BP35A1. + /// + /// + /// See 'BP35A1コマンドリファレンス 3.32. WUART (プロダクト設定コマンド)' for detailed specifications. + /// + internal const BP35UartBaudRate DefaultValueForBP35UartBaudRate = BP35UartBaudRate.Baud115200; + + public static ValueTask CreateAsync( + string? serialPortName, + IServiceProvider? serviceProvider = null, + CancellationToken cancellationToken = default + ) + => CreateAsync( + configurations: new BP35A1Configurations() { + SerialPortName = serialPortName, + }, + serviceProvider: serviceProvider, + cancellationToken: cancellationToken + ); + + public static ValueTask CreateAsync( + BP35A1Configurations configurations, + IServiceProvider? serviceProvider = null, + CancellationToken cancellationToken = default + ) + => InitializeAsync( +#pragma warning disable CA2000 + device: new BP35A1( + configurations: configurations ?? throw new ArgumentNullException(nameof(configurations)), + serviceProvider: serviceProvider + ), +#pragma warning restore CA2000 + tryLoadFlashMemory: configurations.TryLoadFlashMemory, + serviceProvider: serviceProvider, + cancellationToken: cancellationToken + ); + + private BP35A1( + IBP35Configurations configurations, + IServiceProvider? serviceProvider = null + ) + : base( + configurations: configurations, + serialPortStreamFactory: serviceProvider?.GetService(), + logger: serviceProvider?.GetService()?.CreateLogger() + ) + { + } +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35A1Configurations.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35A1Configurations.cs new file mode 100644 index 0000000..eeffe6a --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35A1Configurations.cs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 smdn +// SPDX-License-Identifier: MIT +using Smdn.Net.SkStackIP; + +namespace Smdn.Devices.BP35XX; + +public sealed class BP35A1Configurations : IBP35Configurations { + /// + public string? SerialPortName { get; set; } + + /// + public BP35UartBaudRate BaudRate { get; set; } = BP35A1.DefaultValueForBP35UartBaudRate; + + /// + public bool TryLoadFlashMemory { get; set; } = BP35Base.DefaultValueForTryLoadFlashMemory; + + SkStackERXUDPDataFormat IBP35Configurations.ERXUDPDataFormat => SkStackERXUDPDataFormat.Binary; +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35Base.DefaultSerialPortStreamFactory.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35Base.DefaultSerialPortStreamFactory.cs new file mode 100644 index 0000000..2b62f11 --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35Base.DefaultSerialPortStreamFactory.cs @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT + +using System; +using System.IO; +using System.IO.Ports; + +namespace Smdn.Devices.BP35XX; + +#pragma warning disable IDE0040 +partial class BP35Base { +#pragma warning restore IDE0040 + private class DefaultSerialPortStreamFactory : IBP35SerialPortStreamFactory { + public static DefaultSerialPortStreamFactory Instance { get; } = new(); + + public Stream CreateSerialPortStream(IBP35Configurations configurations) + { + if (string.IsNullOrEmpty(configurations.SerialPortName)) { + throw new ArgumentException( + message: $"The {nameof(configurations.SerialPortName)} is not set for the {configurations.GetType().Name}", + paramName: nameof(configurations) + ); + } + + const string CRLF = "\r\n"; + +#pragma warning disable CA2000 + var port = new SerialPort( + portName: configurations.SerialPortName, + baudRate: configurations.BaudRate switch { + BP35UartBaudRate.Baud2400 => 2_400, + BP35UartBaudRate.Baud4800 => 4_800, + BP35UartBaudRate.Baud9600 => 9_600, + BP35UartBaudRate.Baud19200 => 19_200, + BP35UartBaudRate.Baud38400 => 38_400, + BP35UartBaudRate.Baud57600 => 57_600, + BP35UartBaudRate.Baud115200 => 115_200, + _ => throw new ArgumentException( + message: $"A valid {nameof(BP35UartBaudRate)} value is not set for the {configurations.GetType().Name}", + paramName: nameof(configurations) + ), + }, + parity: Parity.None, + dataBits: 8, + stopBits: StopBits.One + ) { + Handshake = Handshake.None, // TODO: RequestToSend + DtrEnable = false, + RtsEnable = false, + NewLine = CRLF, + }; +#pragma warning restore CA2000 + + port.Open(); + + // discard input buffer to avoid reading previously received data + port.DiscardInBuffer(); + + return port.BaseStream; + } + } +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35Base.Functions.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35Base.Functions.cs new file mode 100644 index 0000000..a04999c --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35Base.Functions.cs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Smdn.Devices.BP35XX; + +#pragma warning disable IDE0040 +partial class BP35Base { +#pragma warning restore IDE0040 + public ValueTask SetUdpDataFormatAsync( + BP35UdpReceiveDataFormat format, + CancellationToken cancellationToken = default + ) + { + return SendWOPTAsync( + mode: format switch { + BP35UdpReceiveDataFormat.Binary => BP35ERXUDPFormatBinary, + BP35UdpReceiveDataFormat.HexAscii => BP35ERXUDPFormatHexAscii, + _ => throw new ArgumentException($"undefined value of {nameof(BP35UdpReceiveDataFormat)}", nameof(format)), + }, + cancellationToken: cancellationToken + ); + } + + public async ValueTask GetUdpDataFormatAsync( + CancellationToken cancellationToken = default + ) + { + var mode = await SendROPTAsync( + cancellationToken: cancellationToken + ).ConfigureAwait(false); + + return (mode & BP35ERXUDPFormatMask) switch { + BP35ERXUDPFormatBinary => BP35UdpReceiveDataFormat.Binary, + BP35ERXUDPFormatHexAscii => BP35UdpReceiveDataFormat.HexAscii, + _ => BP35UdpReceiveDataFormat.Binary, // XXX + }; + } + + public ValueTask SetUartOptionsAsync( + BP35UartBaudRate baudRate, + BP35UartCharacterInterval characterInterval = default, + BP35UartFlowControl flowControl = default, + CancellationToken cancellationToken = default + ) + => SetUartOptionsAsync( + uartConfigurations: new(baudRate, characterInterval, flowControl), + cancellationToken: cancellationToken + ); + + public ValueTask SetUartOptionsAsync( + BP35UartConfigurations uartConfigurations, + CancellationToken cancellationToken = default + ) + => SendWUARTAsync( + mode: uartConfigurations.Mode, + cancellationToken: cancellationToken + ); + + public async ValueTask GetUartOptionsAsync( + CancellationToken cancellationToken = default + ) + { + var mode = await SendRUARTAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + + return new(mode); + } +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35Base.SkStackIP.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35Base.SkStackIP.cs new file mode 100644 index 0000000..ad77ea4 --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35Base.SkStackIP.cs @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT + +using System; +using System.Buffers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Smdn.Formats; +using Smdn.Net.SkStackIP.Protocol; + +namespace Smdn.Devices.BP35XX; + +#pragma warning disable IDE0040 +partial class BP35Base { +#pragma warning restore IDE0040 + private static readonly SkStackProtocolSyntax RMCommandSyntax = new BP35CommandSyntax(); + + /// + /// See 'BP35A1コマンドリファレンス 3.30. WOPT (プロダクト設定コマンド)' for detailed specifications. + /// + private const byte BP35ERXUDPFormatMask = 0b_0000000_1; + + /// + /// See 'BP35A1コマンドリファレンス 3.30. WOPT (プロダクト設定コマンド)' for detailed specifications. + /// + private const byte BP35ERXUDPFormatBinary = 0b_0000000_0; + + /// + /// See 'BP35A1コマンドリファレンス 3.30. WOPT (プロダクト設定コマンド)' for detailed specifications. + /// + private const byte BP35ERXUDPFormatHexAscii = 0b_0000000_1; + + /// + /// Sends a command WOPT. + /// + /// + /// See 'BP35A1コマンドリファレンス 3.30. WOPT (プロダクト設定コマンド)' for detailed specifications. + /// + private protected async ValueTask SendWOPTAsync( + byte mode, + CancellationToken cancellationToken = default + ) + { + byte[]? modeBytes = null; + + try { + modeBytes = ArrayPool.Shared.Rent(2); + + _ = Hexadecimal.TryEncodeUpperCase(mode, modeBytes.AsSpan(), out var lengthOfMODE); + + _ = await SendCommandAsync( + command: BP35CommandNames.WOPT, + writeArguments: writer => writer.WriteToken(modeBytes.AsSpan(0, lengthOfMODE)), + syntax: RMCommandSyntax, + cancellationToken: cancellationToken, + throwIfErrorStatus: true + ).ConfigureAwait(false); + } + finally { + if (modeBytes is not null) + ArrayPool.Shared.Return(modeBytes); + } + } + + /// + /// Sends a command ROPT. + /// + /// + /// See 'BP35A1コマンドリファレンス 3.31. ROPT (プロダクト設定コマンド)' for detailed specifications. + /// + private protected async ValueTask SendROPTAsync( + CancellationToken cancellationToken = default + ) + { + var resp = await SendCommandAsync( + command: BP35CommandNames.ROPT, + writeArguments: null, + syntax: RMCommandSyntax, + cancellationToken: cancellationToken, + throwIfErrorStatus: true + ).ConfigureAwait(false); + + return Convert.ToByte(Encoding.ASCII.GetString(resp.StatusText.Span), 16); + } + + /// + /// Sends a command WUART. + /// + /// + /// See 'BP35A1コマンドリファレンス 3.32. WUART (プロダクト設定コマンド)' for detailed specifications. + /// + private protected async ValueTask SendWUARTAsync( + byte mode, + CancellationToken cancellationToken = default + ) + { + byte[]? modeBytes = null; + + try { + modeBytes = ArrayPool.Shared.Rent(2); + + _ = Hexadecimal.TryEncodeUpperCase(mode, modeBytes.AsSpan(), out var lengthOfMODE); + + _ = await SendCommandAsync( + command: BP35CommandNames.WUART, + writeArguments: writer => writer.WriteToken(modeBytes.AsSpan(0, lengthOfMODE)), + syntax: RMCommandSyntax, + cancellationToken: cancellationToken, + throwIfErrorStatus: true + ).ConfigureAwait(false); + } + finally { + if (modeBytes is not null) + ArrayPool.Shared.Return(modeBytes); + } + } + + /// + /// Sends a command RUART. + /// + /// + /// See 'BP35A1コマンドリファレンス 3.33. RUART (プロダクト設定コマンド)' for detailed specifications. + /// + private protected async ValueTask SendRUARTAsync( + CancellationToken cancellationToken = default + ) + { + var resp = await SendCommandAsync( + command: BP35CommandNames.RUART, + writeArguments: null, + syntax: RMCommandSyntax, + cancellationToken: cancellationToken, + throwIfErrorStatus: true + ).ConfigureAwait(false); + + return Convert.ToByte(Encoding.ASCII.GetString(resp.StatusText.Span), 16); + } +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35Base.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35Base.cs new file mode 100644 index 0000000..a498b91 --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35Base.cs @@ -0,0 +1,225 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT +#pragma warning disable CA1848 + +using System; +#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE +using System.Diagnostics.CodeAnalysis; +#endif +#if !SYSTEM_CONVERT_TOHEXSTRING +using System.Buffers; // ArrayPool +#endif +using System.Net; +using System.Net.NetworkInformation; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Smdn.Net.SkStackIP; + +namespace Smdn.Devices.BP35XX; + +public abstract partial class BP35Base : SkStackClient { + internal const bool DefaultValueForTryLoadFlashMemory = true; + + private protected static async ValueTask InitializeAsync( + TBP35XX device, + bool tryLoadFlashMemory = DefaultValueForTryLoadFlashMemory, + IServiceProvider? serviceProvider = null, + CancellationToken cancellationToken = default + ) + where TBP35XX : BP35Base + { +#pragma warning disable CA1510 + if (device is null) + throw new ArgumentNullException(nameof(device)); +#pragma warning disable CA1510 + + try { + await device.InitializeAsync( + tryLoadFlashMemory, + serviceProvider, + cancellationToken + ).ConfigureAwait(false); + + return device; + } + catch { + device.Dispose(); + + throw; + } + } + + private protected static InvalidOperationException CreateNotInitializedException() + => new(message: "not initialized"); + + /* + * instance members + */ +#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE + [MemberNotNullWhen(true, nameof(skstackVersion))] + [MemberNotNullWhen(true, nameof(skstackAppVersion))] + [MemberNotNullWhen(true, nameof(linkLocalAddress))] + [MemberNotNullWhen(true, nameof(macAddress))] + [MemberNotNullWhen(true, nameof(rohmUserId))] + [MemberNotNullWhen(true, nameof(rohmPassword))] +#endif + private protected bool IsInitialized { get; private set; } + +#if !SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE +#pragma warning disable CS8603 +#endif + private Version? skstackVersion; + public Version SkStackVersion => IsInitialized ? skstackVersion : throw CreateNotInitializedException(); + + private string? skstackAppVersion; + public string SkStackAppVersion => IsInitialized ? skstackAppVersion : throw CreateNotInitializedException(); + + private IPAddress? linkLocalAddress; + public IPAddress LinkLocalAddress => IsInitialized ? linkLocalAddress : throw CreateNotInitializedException(); + + private PhysicalAddress? macAddress; + public PhysicalAddress MacAddress => IsInitialized ? macAddress : throw CreateNotInitializedException(); + + private string? rohmUserId; + public string RohmUserId => IsInitialized ? rohmUserId : throw CreateNotInitializedException(); + + private string? rohmPassword; + public string RohmPassword => IsInitialized ? rohmPassword : throw CreateNotInitializedException(); +#pragma warning restore CS8603 + + /// + /// Initializes a new instance of the class with specifying the serial port name. + /// + /// + /// A that holds the configurations to the instance. + /// + /// + /// A that provides the function to create the serial port stream according to the . + /// + /// The to report the situation. +#pragma warning disable IDE0290 + private protected BP35Base( + IBP35Configurations configurations, + IBP35SerialPortStreamFactory? serialPortStreamFactory, + ILogger? logger + ) +#pragma warning restore IDE0290 + : base( + stream: (serialPortStreamFactory ?? DefaultSerialPortStreamFactory.Instance).CreateSerialPortStream( + configurations ?? throw new ArgumentNullException(nameof(configurations)) + ), + leaveStreamOpen: false, // should close the opened stream + erxudpDataFormat: configurations.ERXUDPDataFormat, + logger: logger + ) + { + } + + private async ValueTask InitializeAsync( + bool tryLoadFlashMemory, + IServiceProvider? serviceProvider, + CancellationToken cancellationToken + ) + { + // retrieve firmware version + skstackVersion = (await SendSKVERAsync(cancellationToken).ConfigureAwait(false)).Payload; + + Logger?.LogInformation("{Name}: {Value}", nameof(SkStackVersion), skstackVersion); + + skstackAppVersion = (await SendSKAPPVERAsync(cancellationToken).ConfigureAwait(false)).Payload; + + Logger?.LogInformation("{Name}: {Value}", nameof(SkStackAppVersion), skstackAppVersion); + + // retrieve EINFO + var respInfo = await SendSKINFOAsync(cancellationToken).ConfigureAwait(false); + var einfo = respInfo.Payload!; + + linkLocalAddress = einfo.LinkLocalAddress; + macAddress = einfo.MacAddress; + + Logger?.LogInformation("{Name}: {Value}", nameof(LinkLocalAddress), linkLocalAddress); + Logger?.LogInformation("{Name}: {Value}", nameof(MacAddress), macAddress); + + Logger?.LogInformation("{Name}: {Value}", nameof(einfo.Channel), einfo.Channel); + Logger?.LogInformation("{Name}: {Value} (0x{ValueToBeDisplayedInHex:X4})", nameof(einfo.PanId), einfo.PanId, einfo.PanId); + + // parse ROHM user ID and password + (rohmUserId, rohmPassword) = ParseRohmUserIdAndPassword(linkLocalAddress); + + // try load configuration from flash memory + if (tryLoadFlashMemory) { + try { + await SendSKLOADAsync(cancellationToken).ConfigureAwait(false); + } + catch (SkStackFlashMemoryIOException) { + Logger?.LogWarning("Could not load configuration from flash memory."); + } + } + + // disable echoback (override loaded configuration) + await SendSKSREGAsync( + register: SkStackRegister.EnableEchoback, + value: false, + cancellationToken: cancellationToken + ).ConfigureAwait(false); + + // set ERXUDP data format + var udpDataFormat = await GetUdpDataFormatAsync(cancellationToken).ConfigureAwait(false); + +#pragma warning disable IDE0055, IDE0072 + ERXUDPDataFormat = udpDataFormat switch { + BP35UdpReceiveDataFormat.HexAscii => SkStackERXUDPDataFormat.HexAsciiText, + /*BP35UdpReceiveDataFormat.Binary,*/ _ => SkStackERXUDPDataFormat.Binary, + }; +#pragma warning restore IDE0055, IDE0072 + + await InitializeAsyncCore(serviceProvider, cancellationToken).ConfigureAwait(false); + + IsInitialized = true; + + static (string, string) ParseRohmUserIdAndPassword(IPAddress linkLocalAddress) + { +#if SYSTEM_CONVERT_TOHEXSTRING + Span addressBytes = stackalloc byte[16]; + + if (linkLocalAddress.TryWriteBytes(addressBytes, out var bytesWritten) && (8 + 2) <= bytesWritten) { + return ( + Convert.ToHexString(addressBytes.Slice(0, 2)), + Convert.ToHexString(addressBytes.Slice(8, 2)) + ); + } +#else + byte[]? addressBytes = null; + + try { + addressBytes = ArrayPool.Shared.Rent(16); + + if (linkLocalAddress.TryWriteBytes(addressBytes, out var bytesWritten) && (8 + 2) <= bytesWritten) { + return ( + $"{addressBytes[0]:X2}{addressBytes[1]:X2}", + $"{addressBytes[8]:X2}{addressBytes[9]:X2}" + ); + } + } + finally { + if (addressBytes is not null) + ArrayPool.Shared.Return(addressBytes); + } +#endif + + return default; // or throw exception? + } + } + + private protected virtual ValueTask InitializeAsyncCore( + IServiceProvider? serviceProvider, + CancellationToken cancellationToken + ) + { + // nothing to do in this class + return default; + } +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35CommandNames.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35CommandNames.cs new file mode 100644 index 0000000..e940474 --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35CommandNames.cs @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT + +using System; +using System.Text; + +namespace Smdn.Devices.BP35XX; + +/// +/// See 'BP35A1コマンドリファレンス 3. コマンドリファレンス' for detailed specifications. +/// +internal class BP35CommandNames { + /// + /// See 'BP35A1コマンドリファレンス 3.30. WOPT (プロダクト設定コマンド)' for detailed specifications. + /// + public static ReadOnlyMemory WOPT { get; } = Encoding.ASCII.GetBytes(nameof(WOPT)); + + /// + /// See 'BP35A1コマンドリファレンス 3.31. ROPT (プロダクト設定コマンド)' for detailed specifications. + /// + public static ReadOnlyMemory ROPT { get; } = Encoding.ASCII.GetBytes(nameof(ROPT)); + + /// + /// See 'BP35A1コマンドリファレンス 3.32. WUART (プロダクト設定コマンド)' for detailed specifications. + /// + public static ReadOnlyMemory WUART { get; } = Encoding.ASCII.GetBytes(nameof(WUART)); + + /// + /// See 'BP35A1コマンドリファレンス 3.33. RUART (プロダクト設定コマンド)' for detailed specifications. + /// + public static ReadOnlyMemory RUART { get; } = Encoding.ASCII.GetBytes(nameof(RUART)); +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35CommandSyntax.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35CommandSyntax.cs new file mode 100644 index 0000000..b522260 --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35CommandSyntax.cs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT + +using System; + +using Smdn.Net.SkStackIP.Protocol; + +namespace Smdn.Devices.BP35XX; + +/// +/// See below for detailed specifications. +/// +/// 'BP35A1コマンドリファレンス 3.30. WOPT (プロダクト設定コマンド)' +/// 'BP35A1コマンドリファレンス 3.31. ROPT (プロダクト設定コマンド)' +/// 'BP35A1コマンドリファレンス 3.32. WUART (プロダクト設定コマンド)' +/// 'BP35A1コマンドリファレンス 3.33. RUART (プロダクト設定コマンド)' +/// +/// +/// +internal sealed class BP35CommandSyntax : SkStackProtocolSyntax { + /// + /// Gets the newline character used in the product configuration commands (プロダクト設定コマンド). + /// Only CR is used as a newline character in the product configuration command and its response. + /// + public override ReadOnlySpan EndOfCommandLine => "\r"u8; + public override bool ExpectStatusLine => true; + public override ReadOnlySpan EndOfStatusLine => "\r"u8; +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UartBaudRate.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UartBaudRate.cs new file mode 100644 index 0000000..6b3e9d7 --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UartBaudRate.cs @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT + +namespace Smdn.Devices.BP35XX; + +/// +/// An enumeration type representing the configuration values for the UART baud rate, to be set or get by the WUART and RUART commands. +/// +/// +/// See below for detailed specifications. +/// +/// 'BP35A1コマンドリファレンス 3.32. WUART (プロダクト設定コマンド)' +/// 'BP35A1コマンドリファレンス 3.33. RUART (プロダクト設定コマンド)' +/// +/// +#pragma warning disable CA1027 +public enum BP35UartBaudRate : byte { +#pragma warning restore CA1027 + Baud115200 = 0b_0_000_0_000, // default(BP35UartBaudRate) + Baud2400 = 0b_0_000_0_001, + Baud4800 = 0b_0_000_0_010, + Baud9600 = 0b_0_000_0_011, + Baud19200 = 0b_0_000_0_100, + Baud38400 = 0b_0_000_0_101, + Baud57600 = 0b_0_000_0_110, +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UartCharacterInterval.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UartCharacterInterval.cs new file mode 100644 index 0000000..7be4179 --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UartCharacterInterval.cs @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT + +namespace Smdn.Devices.BP35XX; + +/// +/// An enumeration type representing the configuration values for the inter-character intervals in UART, to be set or get by the WUART and RUART commands. +/// +/// +/// See below for detailed specifications. +/// +/// 'BP35A1コマンドリファレンス 3.32. WUART (プロダクト設定コマンド)' +/// 'BP35A1コマンドリファレンス 3.33. RUART (プロダクト設定コマンド)' +/// +/// +#pragma warning disable CA1027 +public enum BP35UartCharacterInterval : byte { +#pragma warning restore CA1027 + None = 0b_0_000_0_000, // default(BP35UartCharacterInterval) + Microseconds100 = 0b_0_001_0_000, + Microseconds200 = 0b_0_010_0_000, + Microseconds300 = 0b_0_011_0_000, + Microseconds400 = 0b_0_100_0_000, + Microseconds50 = 0b_0_101_0_000, // or may be 500μsecs? +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UartConfigurations.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UartConfigurations.cs new file mode 100644 index 0000000..8211e61 --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UartConfigurations.cs @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2023 smdn +// SPDX-License-Identifier: MIT +using System; + +namespace Smdn.Devices.BP35XX; + +/// +/// A read-only structure that represents the configuration values relevant to UART, set and get by the WUART and RUART commands. +/// +/// +/// See below for detailed specifications. +/// +/// 'BP35A1コマンドリファレンス 3.32. WUART (プロダクト設定コマンド)' +/// 'BP35A1コマンドリファレンス 3.33. RUART (プロダクト設定コマンド)' +/// +/// +public readonly struct BP35UartConfigurations { + private const byte BaudRateMask = 0b_0_000_0_111; + // private const byte ReservedBitMask = 0b_0_000_1_000; + private const byte CharacterIntervalMask = 0b_0_111_0_000; + private const byte FlowControlMask = 0b_1_000_0_000; + + internal byte Mode { get; } + + public BP35UartBaudRate BaudRate => (BP35UartBaudRate)(Mode & BaudRateMask); + public BP35UartCharacterInterval CharacterInterval => (BP35UartCharacterInterval)(Mode & CharacterIntervalMask); + public BP35UartFlowControl FlowControl => (BP35UartFlowControl)(Mode & FlowControlMask); + + internal BP35UartConfigurations( + byte mode + ) + { + Mode = mode; + } + + public BP35UartConfigurations( + BP35UartBaudRate baudRate, + BP35UartCharacterInterval characterInterval, + BP35UartFlowControl flowControl + ) + { +#if SYSTEM_ENUM_ISDEFINED_OF_TENUM + if (!Enum.IsDefined(baudRate)) +#else + if (!Enum.IsDefined(typeof(BP35UartBaudRate), baudRate)) +#endif + throw new ArgumentException($"undefined value of {nameof(BP35UartBaudRate)}", nameof(baudRate)); + +#if SYSTEM_ENUM_ISDEFINED_OF_TENUM + if (!Enum.IsDefined(flowControl)) +#else + if (!Enum.IsDefined(typeof(BP35UartFlowControl), flowControl)) +#endif + throw new ArgumentException($"undefined value of {nameof(BP35UartFlowControl)}", nameof(flowControl)); + +#if SYSTEM_ENUM_ISDEFINED_OF_TENUM + if (!Enum.IsDefined(characterInterval)) +#else + if (!Enum.IsDefined(typeof(BP35UartCharacterInterval), characterInterval)) +#endif + throw new ArgumentException($"undefined value of {nameof(BP35UartCharacterInterval)}", nameof(characterInterval)); + + Mode = (byte)((byte)baudRate | (byte)characterInterval | (byte)flowControl); + } + + public void Deconstruct( + out BP35UartBaudRate baudRate, + out BP35UartCharacterInterval characterInterval, + out BP35UartFlowControl flowControl + ) + { + baudRate = BaudRate; + characterInterval = CharacterInterval; + flowControl = FlowControl; + } +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UartFlowControl.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UartFlowControl.cs new file mode 100644 index 0000000..b69bc38 --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UartFlowControl.cs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT + +namespace Smdn.Devices.BP35XX; + +/// +/// An enumeration type representing the configuration values for the flow control in UART, to be set or get by the WUART and RUART commands. +/// +/// +/// See below for detailed specifications. +/// +/// 'BP35A1コマンドリファレンス 3.32. WUART (プロダクト設定コマンド)' +/// 'BP35A1コマンドリファレンス 3.33. RUART (プロダクト設定コマンド)' +/// +/// +#pragma warning disable CA1027 +public enum BP35UartFlowControl : byte { +#pragma warning restore CA1027 + Disabled = 0b_0_000_0_000, // default(BP35UartFlowControl) + Enabled = 0b_1_000_0_000, +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UdpReceiveDataFormat.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UdpReceiveDataFormat.cs new file mode 100644 index 0000000..cd4e642 --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35UdpReceiveDataFormat.cs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT + +namespace Smdn.Devices.BP35XX; + +/// +/// An enumeration type representing the configuration values for the display format of the data part in ERXUDP event, to be set or get by the WOPT and ROPT commands. +/// +/// +/// See below for detailed specifications. +/// +/// 'BP35A1コマンドリファレンス 3.30. WOPT (プロダクト設定コマンド)' +/// 'BP35A1コマンドリファレンス 3.31. ROPT (プロダクト設定コマンド)' +/// +/// +#pragma warning disable CA1027 +public enum BP35UdpReceiveDataFormat : byte { +#pragma warning restore CA1027 + Binary = 0b_0000_0000, + HexAscii = 0b_0000_0001, +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/IBP35Configurations.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/IBP35Configurations.cs new file mode 100644 index 0000000..457e941 --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/IBP35Configurations.cs @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2023 smdn +// SPDX-License-Identifier: MIT +using Smdn.Net.SkStackIP; + +namespace Smdn.Devices.BP35XX; + +public interface IBP35Configurations { + /// + /// Gets the value that holds the serial port name for communicating with the device that implements the SKSTACK-IP protocol. + /// + string? SerialPortName { get; } + + /// + /// Gets the value that specifies the baud rate of the serial port for communicating with the device. + /// + BP35UartBaudRate BaudRate { get; } + + /// + /// Gets a value indicating whether or not to attempt to load the configuration from flash memory during initialization. + /// + bool TryLoadFlashMemory { get; } + + /// + /// Gets the value that specifies the format of the data part received in the event ERXUDP. See . + /// + /// + /// + SkStackERXUDPDataFormat ERXUDPDataFormat { get; } +} diff --git a/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/IBP35SerialPortStreamFactory.cs b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/IBP35SerialPortStreamFactory.cs new file mode 100644 index 0000000..b6a192f --- /dev/null +++ b/src/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/IBP35SerialPortStreamFactory.cs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 smdn +// SPDX-License-Identifier: MIT +using System.IO; + +namespace Smdn.Devices.BP35XX; + +public interface IBP35SerialPortStreamFactory { + Stream CreateSerialPortStream(IBP35Configurations configurations); +} diff --git a/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX.Tests.csproj b/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX.Tests.csproj new file mode 100644 index 0000000..1a028a7 --- /dev/null +++ b/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX.Tests.csproj @@ -0,0 +1,15 @@ + + + + net8.0;net6.0 + $(TargetFrameworks) + + + + + + + diff --git a/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35A1.Commands.cs b/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35A1.Commands.cs new file mode 100644 index 0000000..19bc20b --- /dev/null +++ b/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35A1.Commands.cs @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT + +using System; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +using NUnit.Framework; + +namespace Smdn.Devices.BP35XX; + +[TestFixture] +public class BP35A1CommandsTests { + private async Task<(BP35A1, PseudoSkStackStream)> CreateDeviceAsync() + { + var factory = new PseudoSerialPortStreamFactory(); + var services = new ServiceCollection(); + + services.Add( + ServiceDescriptor.Singleton( + typeof(IBP35SerialPortStreamFactory), + factory + ) + ); + + // SKVER + factory.Stream.ResponseWriter.WriteLine("EVER 1.2.10"); + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKAPPVER + factory.Stream.ResponseWriter.WriteLine("EAPPVER pseudo-BP35A1"); + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKINFO + factory.Stream.ResponseWriter.WriteLine("EINFO FE80:0000:0000:0000:021D:1290:1234:5678 001D129012345678 21 8888 FFFE"); + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKLOAD + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKSREG SFE 0 + factory.Stream.ResponseWriter.WriteLine("OK"); + + // ROPT + factory.Stream.ResponseWriter.Write("OK 00\r"); + + var bp35a1 = await BP35A1.CreateAsync( + new BP35A1Configurations() { + SerialPortName = "/dev/pseudo-serial-port", + TryLoadFlashMemory = true, + }, + services.BuildServiceProvider() + ); + + factory.Stream.ReadSentData(); // clear sent data + + return (bp35a1, factory.Stream); + } + + [TestCase(BP35UdpReceiveDataFormat.Binary, 0b_0000000_0)] + [TestCase(BP35UdpReceiveDataFormat.HexAscii, 0b_0000000_1)] + public async Task SetUdpDataFormatAsync( + BP35UdpReceiveDataFormat format, + byte expectedWOPTArg + ) + { + var (dev, s) = await CreateDeviceAsync(); + + using var bp35a1 = dev; + using var stream = s; + + // WOPT + stream.ResponseWriter.Write($"OK\r"); + + Assert.DoesNotThrowAsync(async () => + await bp35a1.SetUdpDataFormatAsync( + format: format + ) + ); + + var commands = Encoding.ASCII.GetString(stream.ReadSentData()); + + Assert.That(commands, Is.EqualTo($"WOPT {expectedWOPTArg:X2}\r")); + } + + [TestCase(0b_0000000_0, BP35UdpReceiveDataFormat.Binary)] + [TestCase(0b_0000000_1, BP35UdpReceiveDataFormat.HexAscii)] + public async Task GetUdpDataFormatAsync( + byte statusText, + BP35UdpReceiveDataFormat expectedFormat + ) + { + var (dev, s) = await CreateDeviceAsync(); + + using var bp35a1 = dev; + using var stream = s; + + // ROPT + stream.ResponseWriter.Write($"OK {statusText:X2}\r"); + + BP35UdpReceiveDataFormat actualFormat = default; + + Assert.DoesNotThrowAsync(async () => + actualFormat = await bp35a1.GetUdpDataFormatAsync() + ); + + var commands = Encoding.ASCII.GetString(stream.ReadSentData()); + + Assert.That(commands, Is.EqualTo("ROPT\r")); + + Assert.That(actualFormat, Is.EqualTo(expectedFormat)); + } + + [TestCase(BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled, 0b_0_000_0_000)] + [TestCase(BP35UartBaudRate.Baud9600, BP35UartCharacterInterval.None, BP35UartFlowControl.Enabled, 0b_1_000_0_011)] + [TestCase(BP35UartBaudRate.Baud2400, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled, 0b_0_000_0_001)] + [TestCase(BP35UartBaudRate.Baud4800, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled, 0b_0_000_0_010)] + [TestCase(BP35UartBaudRate.Baud9600, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled, 0b_0_000_0_011)] + [TestCase(BP35UartBaudRate.Baud19200, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled, 0b_0_000_0_100)] + [TestCase(BP35UartBaudRate.Baud38400, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled, 0b_0_000_0_101)] + [TestCase(BP35UartBaudRate.Baud57600, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled, 0b_0_000_0_110)] + [TestCase(BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.Microseconds100, BP35UartFlowControl.Disabled, 0b_0_001_0_000)] + [TestCase(BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.Microseconds200, BP35UartFlowControl.Disabled, 0b_0_010_0_000)] + [TestCase(BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.Microseconds300, BP35UartFlowControl.Disabled, 0b_0_011_0_000)] + [TestCase(BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.Microseconds400, BP35UartFlowControl.Disabled, 0b_0_100_0_000)] + [TestCase(BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.Microseconds50, BP35UartFlowControl.Disabled, 0b_0_101_0_000)] + [TestCase(BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.None, BP35UartFlowControl.Enabled, 0b_1_000_0_000)] + public async Task SetUartOptionsAsync( + BP35UartBaudRate baudRate, + BP35UartCharacterInterval characterInterval, + BP35UartFlowControl flowControl, + byte expectedWUARTArg + ) + { + var (dev, s) = await CreateDeviceAsync(); + + using var bp35a1 = dev; + using var stream = s; + + // WUART + stream.ResponseWriter.Write($"OK\r"); + + Assert.DoesNotThrowAsync(async () => + await bp35a1.SetUartOptionsAsync( + baudRate: baudRate, + flowControl: flowControl, + characterInterval: characterInterval + ) + ); + + var commands = Encoding.ASCII.GetString(stream.ReadSentData()); + + Assert.That(commands, Is.EqualTo($"WUART {expectedWUARTArg:X2}\r")); + } + + [TestCase(0b_0_000_0_000, BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled)] + [TestCase(0b_1_000_0_011, BP35UartBaudRate.Baud9600, BP35UartCharacterInterval.None, BP35UartFlowControl.Enabled)] + [TestCase(0b_0_000_0_001, BP35UartBaudRate.Baud2400, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled)] + [TestCase(0b_0_000_0_010, BP35UartBaudRate.Baud4800, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled)] + [TestCase(0b_0_000_0_011, BP35UartBaudRate.Baud9600, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled)] + [TestCase(0b_0_000_0_100, BP35UartBaudRate.Baud19200, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled)] + [TestCase(0b_0_000_0_101, BP35UartBaudRate.Baud38400, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled)] + [TestCase(0b_0_000_0_110, BP35UartBaudRate.Baud57600, BP35UartCharacterInterval.None, BP35UartFlowControl.Disabled)] + [TestCase(0b_0_001_0_000, BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.Microseconds100, BP35UartFlowControl.Disabled)] + [TestCase(0b_0_010_0_000, BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.Microseconds200, BP35UartFlowControl.Disabled)] + [TestCase(0b_0_011_0_000, BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.Microseconds300, BP35UartFlowControl.Disabled)] + [TestCase(0b_0_100_0_000, BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.Microseconds400, BP35UartFlowControl.Disabled)] + [TestCase(0b_0_101_0_000, BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.Microseconds50, BP35UartFlowControl.Disabled)] + [TestCase(0b_1_000_0_000, BP35UartBaudRate.Baud115200, BP35UartCharacterInterval.None, BP35UartFlowControl.Enabled)] + public async Task GetUartOptionsAsync( + byte statusText, + BP35UartBaudRate expectedBaudRate, + BP35UartCharacterInterval expectedCharacterInterval, + BP35UartFlowControl expectedFlowControl + ) + { + var (dev, s) = await CreateDeviceAsync(); + + using var bp35a1 = dev; + using var stream = s; + + // RUART + stream.ResponseWriter.Write($"OK {statusText:X2}\r"); + + BP35UartBaudRate actualBaudRate = default; + BP35UartFlowControl actualFlowControl = default; + BP35UartCharacterInterval actualCharacterInterval = default; + + Assert.DoesNotThrowAsync(async () => { + (actualBaudRate, actualCharacterInterval, actualFlowControl) = await bp35a1.GetUartOptionsAsync(); + }); + + var commands = Encoding.ASCII.GetString(stream.ReadSentData()); + + Assert.That(commands, Is.EqualTo("RUART\r")); + + Assert.That(actualBaudRate, Is.EqualTo(expectedBaudRate)); + Assert.That(actualCharacterInterval, Is.EqualTo(expectedCharacterInterval)); + Assert.That(actualFlowControl, Is.EqualTo(expectedFlowControl)); + } +} diff --git a/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35A1.cs b/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35A1.cs new file mode 100644 index 0000000..0d965b0 --- /dev/null +++ b/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/BP35A1.cs @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT + +using System; +using System.IO; +using System.Net; +using System.Net.NetworkInformation; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +using NUnit.Framework; + +using Smdn.Net.SkStackIP; + +namespace Smdn.Devices.BP35XX; + +[TestFixture] +public class BP35A1Tests { + [TestCase(true)] + [TestCase(false)] + public void CreateAsync(bool tryLoadFlashMemory) + { + var factory = new PseudoSerialPortStreamFactory(); + var services = new ServiceCollection(); + + services.Add( + ServiceDescriptor.Singleton( + typeof(IBP35SerialPortStreamFactory), + factory + ) + ); + + // SKVER + factory.Stream.ResponseWriter.WriteLine("EVER 1.2.10"); + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKAPPVER + factory.Stream.ResponseWriter.WriteLine("EAPPVER pseudo-BP35A1"); + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKINFO + factory.Stream.ResponseWriter.WriteLine("EINFO FE80:0000:0000:0000:021D:1290:1234:5678 001D129012345678 21 8888 FFFE"); + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKLOAD + if (tryLoadFlashMemory) + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKSREG SFE 0 + factory.Stream.ResponseWriter.WriteLine("OK"); + + // ROPT + factory.Stream.ResponseWriter.Write("OK 00\r"); + + Assert.DoesNotThrowAsync( + async () => { + using var bp35a1 = await BP35A1.CreateAsync( + new BP35A1Configurations() { + SerialPortName = "/dev/pseudo-serial-port", + TryLoadFlashMemory = tryLoadFlashMemory, + }, + services.BuildServiceProvider() + ); + } + ); + } + + public async Task Properties() + { + const string version = "1.2.10"; + const string appVersion = "pseudo-BP35A1"; + const string userId = "FE80"; + const string password = "021D"; + const string linkLocalAddress = $"{userId}:0000:0000:0000:{password}:1290:1234:5678"; + const string macAddress = "001D129012345678"; + + var factory = new PseudoSerialPortStreamFactory(); + var services = new ServiceCollection(); + + services.Add( + ServiceDescriptor.Singleton( + typeof(IBP35SerialPortStreamFactory), + factory + ) + ); + + // SKVER + factory.Stream.ResponseWriter.WriteLine($"EVER {version}"); + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKAPPVER + factory.Stream.ResponseWriter.WriteLine($"EAPPVER {appVersion}"); + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKINFO + factory.Stream.ResponseWriter.WriteLine($"EINFO {linkLocalAddress} {macAddress} 21 8888 FFFE"); + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKLOAD + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKSREG SFE 0 + factory.Stream.ResponseWriter.WriteLine("OK"); + + // ROPT + factory.Stream.ResponseWriter.Write("OK 00\r"); + + using var bp35a1 = await BP35A1.CreateAsync( + new BP35A1Configurations() { + SerialPortName = "/dev/pseudo-serial-port", + }, + services.BuildServiceProvider() + ); + + Assert.That(bp35a1.SkStackVersion, Is.EqualTo(Version.Parse(version))); + Assert.That(bp35a1.SkStackAppVersion, Is.EqualTo(appVersion)); + Assert.That(bp35a1.LinkLocalAddress, Is.EqualTo(IPAddress.Parse(linkLocalAddress))); + Assert.That(bp35a1.MacAddress, Is.EqualTo(PhysicalAddress.Parse(macAddress))); + Assert.That(bp35a1.RohmUserId, Is.EqualTo(userId)); + Assert.That(bp35a1.RohmPassword, Is.EqualTo(password)); + } + + [Test] + public async Task Dispose() + { + var factory = new PseudoSerialPortStreamFactory(); + var services = new ServiceCollection(); + + services.Add( + ServiceDescriptor.Singleton( + typeof(IBP35SerialPortStreamFactory), + factory + ) + ); + + // SKVER + factory.Stream.ResponseWriter.WriteLine("EVER 1.2.10"); + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKAPPVER + factory.Stream.ResponseWriter.WriteLine("EAPPVER pseudo-BP35A1"); + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKINFO + factory.Stream.ResponseWriter.WriteLine("EINFO FE80:0000:0000:0000:021D:1290:1234:5678 001D129012345678 21 8888 FFFE"); + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKLOAD + factory.Stream.ResponseWriter.WriteLine("OK"); + + // SKSREG SFE 0 + factory.Stream.ResponseWriter.WriteLine("OK"); + + // ROPT + factory.Stream.ResponseWriter.Write("OK 00\r"); + + using var bp35a1 = await BP35A1.CreateAsync( + new BP35A1Configurations() { + SerialPortName = "/dev/pseudo-serial-port", + TryLoadFlashMemory = true, + }, + services.BuildServiceProvider() + ); + + Assert.DoesNotThrow(() => bp35a1.Dispose(), "Dispose #0"); + + Assert.DoesNotThrow(() => Assert.That(bp35a1.SkStackVersion, Is.EqualTo(Version.Parse("1.2.10")))); + Assert.DoesNotThrow(() => Assert.That(bp35a1.RohmUserId, Is.EqualTo("FE80"))); + + Assert.ThrowsAsync( + async () => await bp35a1.AuthenticateAsPanaClientAsync( + Encoding.ASCII.GetBytes("01234567890123456789012345678901"), + Encoding.ASCII.GetBytes("pass"), + SkStackActiveScanOptions.Default + ) + ); + Assert.ThrowsAsync( + async () => await bp35a1.ActiveScanAsync( + Encoding.ASCII.GetBytes("01234567890123456789012345678901"), + Encoding.ASCII.GetBytes("pass"), + SkStackActiveScanOptions.Default + ) + ); + + Assert.DoesNotThrow(() => bp35a1.Dispose(), "Dispose #1"); + } +} diff --git a/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/PseudoSerialPortStreamFactory.cs b/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/PseudoSerialPortStreamFactory.cs new file mode 100644 index 0000000..d8d4782 --- /dev/null +++ b/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/PseudoSerialPortStreamFactory.cs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT +using System.IO; + +namespace Smdn.Devices.BP35XX; + +internal class PseudoSerialPortStreamFactory : IBP35SerialPortStreamFactory { + public PseudoSkStackStream Stream { get; } = new(); + + public PseudoSerialPortStreamFactory() + { + } + + public Stream CreateSerialPortStream(IBP35Configurations configurations) + => Stream; +} diff --git a/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/SkStackPseudoStream.cs b/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/SkStackPseudoStream.cs new file mode 100644 index 0000000..f012ea4 --- /dev/null +++ b/tests/Smdn.Devices.BP35XX/Smdn.Devices.BP35XX/SkStackPseudoStream.cs @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2021 smdn +// SPDX-License-Identifier: MIT + +using System; +using System.IO; +using System.IO.Pipelines; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Smdn.Devices.BP35XX; + +internal class PseudoSkStackStream : Stream { + public override bool CanWrite => true; + public override bool CanRead => true; + public override bool CanSeek => false; + public override long Length => throw new NotSupportedException(); + public override long Position { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + private Pipe readStreamPipe; + private Stream readStreamReaderStream; + private Stream readStreamWriterStream; + private TextWriter readStreamWriter; + private Stream writeStream; + + public PseudoSkStackStream() + { + this.readStreamPipe = new Pipe( + new PipeOptions( + useSynchronizationContext: false + ) + ); + this.writeStream = new MemoryStream(); + this.readStreamReaderStream = readStreamPipe.Reader.AsStream(); + this.readStreamWriterStream = readStreamPipe.Writer.AsStream(); + this.readStreamWriter = new StreamWriter(readStreamWriterStream, Encoding.ASCII) { + NewLine = "\r\n", + AutoFlush = true, + }; + } + + public Stream ResponseStream => readStreamWriterStream; + public TextWriter ResponseWriter => readStreamWriter; + + public byte[] ReadSentData() + { + try { + writeStream.Position = 0L; + + using (var stream = new MemoryStream()) { + writeStream.CopyTo(stream); + + return stream.ToArray(); + } + } + finally { + writeStream.SetLength(0L); + } + } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); + public override void SetLength(long value) => throw new NotImplementedException(); + public override void Flush() => writeStream.Flush(); + + public override void Write(byte[] buffer, int offset, int count) + => writeStream.Write(buffer, offset, count); + + public override int Read(byte[] buffer, int offset, int count) + => readStreamReaderStream.Read(buffer, offset, count); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => readStreamReaderStream.ReadAsync(buffer, offset, count, cancellationToken); + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + => readStreamReaderStream.ReadAsync(buffer, cancellationToken); +}