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);
+}