Skip to content

Instantly share code, notes, and snippets.

@smdn
Created February 12, 2024 12:09
Show Gist options
  • Save smdn/aed8fcf57a9ae4295bd8d83c10dafbcd to your computer and use it in GitHub Desktop.
Save smdn/aed8fcf57a9ae4295bd8d83c10dafbcd to your computer and use it in GitHub Desktop.
Smdn.Net.SkStackIP 1.0.0 Release Notes
diff --git a/doc/api-list/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP-net6.0.apilist.cs b/doc/api-list/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP-net6.0.apilist.cs
new file mode 100644
index 0000000..2c1c882
--- /dev/null
+++ b/doc/api-list/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP-net6.0.apilist.cs
@@ -0,0 +1,466 @@
+// Smdn.Net.SkStackIP.dll (Smdn.Net.SkStackIP-1.0.0)
+// Name: Smdn.Net.SkStackIP
+// AssemblyVersion: 1.0.0.0
+// InformationalVersion: 1.0.0+655c155832ea35fece55fe3cd2467b473922319c
+// TargetFramework: .NETCoreApp,Version=v6.0
+// Configuration: Release
+// Referenced assemblies:
+// Microsoft.Extensions.Logging.Abstractions, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
+// Polly.Core, Version=8.0.0.0, Culture=neutral, PublicKeyToken=c8a3ffc3f8f825cc
+// Smdn.Fundamental.ControlPicture, Version=3.0.0.1, Culture=neutral
+// Smdn.Fundamental.PrintableEncoding.Hexadecimal, Version=3.0.0.0, Culture=neutral
+// System.Collections, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.ComponentModel.Primitives, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.IO.Pipelines, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
+// System.Linq, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.Memory, Version=6.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
+// System.Net.NetworkInformation, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.Net.Primitives, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.ObjectModel, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.Runtime, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.Runtime.InteropServices, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.Threading, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+#nullable enable annotations
+
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.IO.Pipelines;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Polly;
+using Smdn.Net.SkStackIP;
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP {
+ public enum SkStackERXUDPDataFormat : int {
+ Binary = 0,
+ HexAsciiText = 1,
+ }
+
+ public enum SkStackErrorCode : int {
+ ER01 = 1,
+ ER02 = 2,
+ ER03 = 3,
+ ER04 = 4,
+ ER05 = 5,
+ ER06 = 6,
+ ER07 = 7,
+ ER08 = 8,
+ ER09 = 9,
+ ER10 = 10,
+ Undefined = 0,
+ }
+
+ public enum SkStackEventNumber : byte {
+ ActiveScanCompleted = 34,
+ BeaconReceived = 32,
+ EchoRequestReceived = 5,
+ EnergyDetectScanCompleted = 31,
+ NeighborAdvertisementReceived = 2,
+ NeighborSolicitationReceived = 1,
+ PanaSessionEstablishmentCompleted = 37,
+ PanaSessionEstablishmentError = 36,
+ PanaSessionExpired = 41,
+ PanaSessionTerminationCompleted = 39,
+ PanaSessionTerminationRequestReceived = 38,
+ PanaSessionTerminationTimedOut = 40,
+ TransmissionTimeControlLimitationActivated = 50,
+ TransmissionTimeControlLimitationDeactivated = 51,
+ UdpSendCompleted = 33,
+ Undefined = 0,
+ WakeupSignalReceived = 192,
+ }
+
+ public enum SkStackResponseStatus : int {
+ Fail = -1,
+ Ok = 1,
+ Undetermined = 0,
+ }
+
+ public enum SkStackUdpEncryption : byte {
+ EncryptIfAble = 2,
+ ForceEncrypt = 1,
+ ForcePlainText = 0,
+ }
+
+ public enum SkStackUdpPortHandle : byte {
+ Handle1 = 1,
+ Handle2 = 2,
+ Handle3 = 3,
+ Handle4 = 4,
+ Handle5 = 5,
+ Handle6 = 6,
+ None = 0,
+ }
+
+ public abstract class SkStackActiveScanOptions : ICloneable {
+ public static SkStackActiveScanOptions Default { get; }
+ public static SkStackActiveScanOptions Null { get; }
+ public static SkStackActiveScanOptions ScanUntilFind { get; }
+
+ public static SkStackActiveScanOptions Create(IEnumerable<int> scanDurationGenerator, PhysicalAddress paaMacAddress) {}
+ public static SkStackActiveScanOptions Create(IEnumerable<int> scanDurationGenerator, Predicate<SkStackPanDescription>? paaSelector = null) {}
+
+ protected SkStackActiveScanOptions() {}
+
+ public abstract SkStackActiveScanOptions Clone();
+ object ICloneable.Clone() {}
+ }
+
+ public class SkStackClient : IDisposable {
+ public static readonly TimeSpan SKSCANDefaultDuration; // = "00:00:00.0480000"
+ public static readonly TimeSpan SKSCANMaxDuration; // = "00:02:37.2960000"
+ public static readonly TimeSpan SKSCANMinDuration; // = "00:00:00.0192000"
+
+ public event EventHandler<SkStackPanaSessionEventArgs>? PanaSessionEstablished;
+ public event EventHandler<SkStackPanaSessionEventArgs>? PanaSessionExpired;
+ public event EventHandler<SkStackPanaSessionEventArgs>? PanaSessionTerminated;
+ public event EventHandler<SkStackEventArgs>? Slept;
+ public event EventHandler<SkStackEventArgs>? WokeUp;
+
+ public SkStackClient(PipeWriter sender, PipeReader receiver, SkStackERXUDPDataFormat erxudpDataFormat = SkStackERXUDPDataFormat.Binary, ILogger? logger = null) {}
+ public SkStackClient(Stream stream, bool leaveStreamOpen = true, SkStackERXUDPDataFormat erxudpDataFormat = SkStackERXUDPDataFormat.Binary, ILogger? logger = null) {}
+
+ public SkStackERXUDPDataFormat ERXUDPDataFormat { get; protected set; }
+ [MemberNotNullWhen(true, "PanaSessionPeerAddress")]
+ public bool IsPanaSessionAlive { [MemberNotNullWhen(true, "PanaSessionPeerAddress")] get; }
+ protected ILogger? Logger { get; }
+ public IPAddress? PanaSessionPeerAddress { get; }
+ public TimeSpan ReceiveResponseDelay { get; set; }
+ public TimeSpan ReceiveUdpPollingInterval { get; set; }
+ public ISynchronizeInvoke? SynchronizingObject { get; set; }
+
+ public ValueTask<IReadOnlyList<SkStackPanDescription>> ActiveScanAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, SkStackActiveScanOptions? scanOptions = null, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, IPAddress paaAddress, SkStackChannel channel, int panId, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, IPAddress paaAddress, int channelNumber, int panId, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, PhysicalAddress paaMacAddress, SkStackChannel channel, int panId, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, PhysicalAddress paaMacAddress, int channelNumber, int panId, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, SkStackActiveScanOptions? scanOptions = null, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, SkStackPanDescription pan, CancellationToken cancellationToken = default) {}
+ public async ValueTask<IPAddress> ConvertToIPv6LinkLocalAddressAsync(PhysicalAddress macAddress, CancellationToken cancellationToken = default) {}
+ public ValueTask DisableFlashMemoryAutoLoadAsync(CancellationToken cancellationToken = default) {}
+ protected virtual void Dispose(bool disposing) {}
+ public void Dispose() {}
+ public ValueTask EnableFlashMemoryAutoLoadAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<IReadOnlyList<IPAddress>> GetAvailableAddressListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<IReadOnlyList<SkStackUdpPort>> GetListeningUdpPortListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<IReadOnlyDictionary<IPAddress, PhysicalAddress>> GetNeighborCacheListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<IReadOnlyList<SkStackUdpPortHandle>> GetUnusedUdpPortHandleListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask LoadFlashMemoryAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<SkStackUdpPort> PrepareUdpPortAsync(int port, CancellationToken cancellationToken = default) {}
+ public ValueTask<IPAddress> ReceiveUdpAsync(int port, IBufferWriter<byte> buffer, CancellationToken cancellationToken = default) {}
+ public ValueTask<IPAddress> ReceiveUdpEchonetLiteAsync(IBufferWriter<byte> buffer, CancellationToken cancellationToken = default) {}
+ public ValueTask SaveFlashMemoryAsync(SkStackFlashMemoryWriteRestriction restriction, CancellationToken cancellationToken = default) {}
+ internal protected ValueTask<SkStackResponse<TPayload>> SendCommandAsync<TPayload>(ReadOnlyMemory<byte> command, Action<ISkStackCommandLineWriter>? writeArguments, SkStackSequenceParser<TPayload> parseResponsePayload, SkStackProtocolSyntax? syntax = null, bool throwIfErrorStatus = true, CancellationToken cancellationToken = default) {}
+ internal protected async ValueTask<SkStackResponse> SendCommandAsync(ReadOnlyMemory<byte> command, Action<ISkStackCommandLineWriter>? writeArguments = null, SkStackProtocolSyntax? syntax = null, bool throwIfErrorStatus = true, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKADDNBRAsync(IPAddress ipv6Address, PhysicalAddress macAddress, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<string>> SendSKAPPVERAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKDSLEEPAsync(bool waitUntilWakeUp = false, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKERASEAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<(IPAddress LinkLocalAddress, PhysicalAddress MacAddress, SkStackChannel Channel, int PanId, int Addr16)>> SendSKINFOAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKJOINAsync(IPAddress ipv6address, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<IPAddress>> SendSKLL64Async(PhysicalAddress macAddress, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKLOADAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IPAddress Address)> SendSKREJOINAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKRESETAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSAVEAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyList<SkStackPanDescription> PanDescriptions)> SendSKSCANActiveScanAsync(TimeSpan duration = default, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyList<SkStackPanDescription> PanDescriptions)> SendSKSCANActiveScanAsync(int durationFactor, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyList<SkStackPanDescription> PanDescriptions)> SendSKSCANActiveScanPairAsync(TimeSpan duration = default, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyList<SkStackPanDescription> PanDescriptions)> SendSKSCANActiveScanPairAsync(int durationFactor, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyDictionary<SkStackChannel, decimal> ScanResult)> SendSKSCANEnergyDetectScanAsync(TimeSpan duration = default, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyDictionary<SkStackChannel, decimal> ScanResult)> SendSKSCANEnergyDetectScanAsync(int durationFactor, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKSENDTOAsync(SkStackUdpPort port, IPAddress destinationAddress, int destinationPort, ReadOnlyMemory<byte> data, SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKSENDTOAsync(SkStackUdpPort port, IPEndPoint destination, ReadOnlyMemory<byte> data, SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKSENDTOAsync(SkStackUdpPortHandle handle, IPAddress destinationAddress, int destinationPort, ReadOnlyMemory<byte> data, SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKSENDTOAsync(SkStackUdpPortHandle handle, IPEndPoint destination, ReadOnlyMemory<byte> data, SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSETPWDAsync(ReadOnlyMemory<byte> password, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSETPWDAsync(ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSETRBIDAsync(ReadOnlyMemory<byte> id, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSETRBIDAsync(ReadOnlyMemory<char> id, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<TValue>> SendSKSREGAsync<TValue>(SkStackRegister.RegisterEntry<TValue> register, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSREGAsync<TValue>(SkStackRegister.RegisterEntry<TValue> register, TValue @value, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<IReadOnlyList<IPAddress>>> SendSKTABLEAvailableAddressListAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<IReadOnlyList<SkStackUdpPort>>> SendSKTABLEListeningPortListAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<IReadOnlyDictionary<IPAddress, PhysicalAddress>>> SendSKTABLENeighborCacheListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKTERMAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, SkStackUdpPort UdpPort)> SendSKUDPPORTAsync(SkStackUdpPortHandle handle, int port, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKUDPPORTUnsetAsync(SkStackUdpPortHandle handle, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<Version>> SendSKVERAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask SendUdpEchonetLiteAsync(ReadOnlyMemory<byte> buffer, ResiliencePipeline? resiliencePipeline = null, CancellationToken cancellationToken = default) {}
+ protected async ValueTask SetFlashMemoryAutoLoadAsync(bool trueIfEnable, CancellationToken cancellationToken = default) {}
+ public void StartCapturingUdpReceiveEvents(int port) {}
+ public void StopCapturingUdpReceiveEvents(int port) {}
+ public ValueTask<bool> TerminatePanaSessionAsync(CancellationToken cancellationToken = default) {}
+ protected void ThrowIfDisposed() {}
+ internal protected void ThrowIfPanaSessionAlreadyEstablished() {}
+ [MemberNotNull("PanaSessionPeerAddress")]
+ internal protected void ThrowIfPanaSessionIsNotEstablished() {}
+ }
+
+ public class SkStackCommandNotSupportedException : SkStackErrorResponseException {
+ }
+
+ public class SkStackErrorResponseException : SkStackResponseException {
+ public SkStackErrorCode ErrorCode { get; }
+ public string ErrorText { get; }
+ public SkStackResponse Response { get; }
+ }
+
+ public class SkStackEventArgs : EventArgs {
+ public SkStackEventNumber EventNumber { get; }
+ }
+
+ public class SkStackFlashMemoryIOException : SkStackErrorResponseException {
+ }
+
+ public abstract class SkStackFlashMemoryWriteRestriction {
+ public static SkStackFlashMemoryWriteRestriction CreateGrantIfElapsed(TimeSpan interval) {}
+ public static SkStackFlashMemoryWriteRestriction DangerousCreateAlwaysGrant() {}
+
+ protected SkStackFlashMemoryWriteRestriction() {}
+
+ internal protected abstract bool IsRestricted();
+ }
+
+ public static class SkStackKnownPortNumbers {
+ public const int EchonetLite = 3610;
+ public const int Pana = 716;
+ }
+
+ public class SkStackPanaSessionEstablishmentException : SkStackPanaSessionException {
+ }
+
+ public sealed class SkStackPanaSessionEventArgs : SkStackEventArgs {
+ public IPAddress PanaSessionPeerAddress { get; }
+ }
+
+ public abstract class SkStackPanaSessionException : InvalidOperationException {
+ public IPAddress Address { get; }
+ public SkStackEventNumber EventNumber { get; }
+ }
+
+ public sealed class SkStackPanaSessionInfo {
+ public SkStackChannel Channel { get; }
+ public IPAddress LocalAddress { get; }
+ public PhysicalAddress LocalMacAddress { get; }
+ public int PanId { get; }
+ public IPAddress PeerAddress { get; }
+ public PhysicalAddress PeerMacAddress { get; }
+ }
+
+ public static class SkStackRegister {
+ public abstract class RegisterEntry<TValue> {
+ private protected delegate bool ExpectValueFunc(ref SequenceReader<byte> reader, out TValue @value);
+
+ public bool IsReadable { get; }
+ public bool IsWritable { get; }
+ public TValue MaxValue { get; }
+ public TValue MinValue { get; }
+ public string Name { get; }
+ }
+
+ public static SkStackRegister.RegisterEntry<bool> AcceptIcmpEcho { get; }
+ public static SkStackRegister.RegisterEntry<ulong> AccumulatedSendTimeInMilliseconds { get; }
+ public static SkStackRegister.RegisterEntry<SkStackChannel> Channel { get; }
+ public static SkStackRegister.RegisterEntry<bool> EnableAutoLoad { get; }
+ public static SkStackRegister.RegisterEntry<bool> EnableAutoReauthentication { get; }
+ public static SkStackRegister.RegisterEntry<bool> EnableEchoback { get; }
+ public static SkStackRegister.RegisterEntry<bool> EncryptIPMulticast { get; }
+ public static SkStackRegister.RegisterEntry<uint> FrameCounter { get; }
+ public static SkStackRegister.RegisterEntry<bool> IsSendingRestricted { get; }
+ public static SkStackRegister.RegisterEntry<ReadOnlyMemory<byte>> PairingId { get; }
+ public static SkStackRegister.RegisterEntry<ushort> PanId { get; }
+ public static SkStackRegister.RegisterEntry<TimeSpan> PanaSessionLifetimeInSeconds { get; }
+ public static SkStackRegister.RegisterEntry<bool> RespondBeaconRequest { get; }
+ public static SkStackRegister.RegisterEntry<SkStackChannel> S02 { get; }
+ public static SkStackRegister.RegisterEntry<ushort> S03 { get; }
+ public static SkStackRegister.RegisterEntry<uint> S07 { get; }
+ public static SkStackRegister.RegisterEntry<ReadOnlyMemory<byte>> S0A { get; }
+ public static SkStackRegister.RegisterEntry<bool> S15 { get; }
+ public static SkStackRegister.RegisterEntry<TimeSpan> S16 { get; }
+ public static SkStackRegister.RegisterEntry<bool> S17 { get; }
+ public static SkStackRegister.RegisterEntry<bool> SA0 { get; }
+ public static SkStackRegister.RegisterEntry<bool> SA1 { get; }
+ public static SkStackRegister.RegisterEntry<bool> SFB { get; }
+ public static SkStackRegister.RegisterEntry<ulong> SFD { get; }
+ public static SkStackRegister.RegisterEntry<bool> SFE { get; }
+ public static SkStackRegister.RegisterEntry<bool> SFF { get; }
+ }
+
+ public class SkStackResponse {
+ public SkStackResponseStatus Status { get; }
+ public ReadOnlyMemory<byte> StatusText { get; }
+ public bool Success { get; }
+ }
+
+ public class SkStackResponseException : InvalidOperationException {
+ public SkStackResponseException() {}
+ public SkStackResponseException(string message) {}
+ public SkStackResponseException(string message, Exception? innerException = null) {}
+ }
+
+ public class SkStackResponse<TPayload> : SkStackResponse {
+ public TPayload Payload { get; }
+ }
+
+ public class SkStackUartIOException : SkStackErrorResponseException {
+ }
+
+ public class SkStackUdpSendFailedException : InvalidOperationException {
+ public SkStackUdpSendFailedException() {}
+ public SkStackUdpSendFailedException(string message) {}
+ public SkStackUdpSendFailedException(string message, Exception? innerException = null) {}
+ public SkStackUdpSendFailedException(string message, SkStackUdpPortHandle portHandle, IPAddress peerAddress, Exception? innerException = null) {}
+
+ public IPAddress? PeerAddress { get; }
+ public SkStackUdpPortHandle PortHandle { get; }
+ }
+
+ public class SkStackUdpSendResultIndeterminateException : InvalidOperationException {
+ public SkStackUdpSendResultIndeterminateException() {}
+ public SkStackUdpSendResultIndeterminateException(string message) {}
+ public SkStackUdpSendResultIndeterminateException(string message, Exception? innerException = null) {}
+ }
+
+ public readonly struct SkStackChannel :
+ IComparable<SkStackChannel>,
+ IEquatable<SkStackChannel>
+ {
+ public static readonly IReadOnlyDictionary<int, SkStackChannel> Channels; // = "System.Collections.Generic.Dictionary`2[System.Int32,Smdn.Net.SkStackIP.SkStackChannel]"
+ public static readonly SkStackChannel Empty; // = "0ch (S02=0x00, 0 MHz)"
+
+ public static SkStackChannel Channel33 { get; }
+ public static SkStackChannel Channel34 { get; }
+ public static SkStackChannel Channel35 { get; }
+ public static SkStackChannel Channel36 { get; }
+ public static SkStackChannel Channel37 { get; }
+ public static SkStackChannel Channel38 { get; }
+ public static SkStackChannel Channel39 { get; }
+ public static SkStackChannel Channel40 { get; }
+ public static SkStackChannel Channel41 { get; }
+ public static SkStackChannel Channel42 { get; }
+ public static SkStackChannel Channel43 { get; }
+ public static SkStackChannel Channel44 { get; }
+ public static SkStackChannel Channel45 { get; }
+ public static SkStackChannel Channel46 { get; }
+ public static SkStackChannel Channel47 { get; }
+ public static SkStackChannel Channel48 { get; }
+ public static SkStackChannel Channel49 { get; }
+ public static SkStackChannel Channel50 { get; }
+ public static SkStackChannel Channel51 { get; }
+ public static SkStackChannel Channel52 { get; }
+ public static SkStackChannel Channel53 { get; }
+ public static SkStackChannel Channel54 { get; }
+ public static SkStackChannel Channel55 { get; }
+ public static SkStackChannel Channel56 { get; }
+ public static SkStackChannel Channel57 { get; }
+ public static SkStackChannel Channel58 { get; }
+ public static SkStackChannel Channel59 { get; }
+ public static SkStackChannel Channel60 { get; }
+
+ public static bool operator == (SkStackChannel x, SkStackChannel y) {}
+ public static bool operator != (SkStackChannel x, SkStackChannel y) {}
+
+ public int ChannelNumber { get; }
+ public decimal FrequencyMHz { get; }
+ public bool IsEmpty { get; }
+
+ public bool Equals(SkStackChannel other) {}
+ public override bool Equals(object? obj) {}
+ public override int GetHashCode() {}
+ int IComparable<SkStackChannel>.CompareTo(SkStackChannel other) {}
+ public override string ToString() {}
+ }
+
+ public readonly struct SkStackPanDescription {
+ public SkStackChannel Channel { get; }
+ public int ChannelPage { get; }
+ public int Id { get; }
+ public PhysicalAddress MacAddress { get; }
+ public uint PairingId { get; }
+ public decimal Rssi { get; }
+
+ public override string ToString() {}
+ }
+
+ public readonly struct SkStackUdpPort {
+ public static readonly SkStackUdpPort Null; // = "0 (#0)"
+
+ public SkStackUdpPortHandle Handle { get; }
+ public bool IsNull { get; }
+ public bool IsUnused { get; }
+ public int Port { get; }
+
+ public override string ToString() {}
+ }
+}
+
+namespace Smdn.Net.SkStackIP.Protocol {
+ public delegate TResult SkStackSequenceParser<TResult>(ISkStackSequenceParserContext context);
+
+ public interface ISkStackCommandLineWriter {
+ void WriteMaskedToken(ReadOnlySpan<byte> token);
+ void WriteToken(ReadOnlySpan<byte> token);
+ }
+
+ public interface ISkStackSequenceParserContext {
+ ReadOnlySequence<byte> UnparsedSequence { get; }
+
+ void Complete();
+ void Complete(SequenceReader<byte> consumedReader);
+ void Continue();
+ ISkStackSequenceParserContext CreateCopy();
+ virtual SequenceReader<byte> CreateReader() {}
+ void Ignore();
+ void SetAsIncomplete();
+ void SetAsIncomplete(SequenceReader<byte> incompleteReader);
+ }
+
+ public abstract class SkStackProtocolSyntax {
+ public static SkStackProtocolSyntax Default { get; }
+
+ protected SkStackProtocolSyntax() {}
+
+ public abstract ReadOnlySpan<byte> EndOfCommandLine { get; }
+ public virtual ReadOnlySpan<byte> EndOfEchobackLine { get; }
+ public abstract ReadOnlySpan<byte> EndOfStatusLine { get; }
+ public abstract bool ExpectStatusLine { get; }
+ }
+
+ public static class SkStackTokenParser {
+ public static bool Expect<TValue>(ref SequenceReader<byte> reader, int length, Converter<ReadOnlySequence<byte>, TValue> converter, [NotNullWhen(true)] out TValue @value) {}
+ public static bool ExpectADDR16(ref SequenceReader<byte> reader, out ushort @value) {}
+ public static bool ExpectADDR64(ref SequenceReader<byte> reader, [NotNullWhen(true)] out PhysicalAddress? @value) {}
+ public static bool ExpectBinary(ref SequenceReader<byte> reader, out bool @value) {}
+ public static bool ExpectCHANNEL(ref SequenceReader<byte> reader, out SkStackChannel @value) {}
+ public static bool ExpectCharArray(ref SequenceReader<byte> reader, [NotNullWhen(true)] out string? @value) {}
+ public static bool ExpectCharArray(ref SequenceReader<byte> reader, out ReadOnlyMemory<byte> @value) {}
+ public static bool ExpectDecimalNumber(ref SequenceReader<byte> reader, int length, out uint @value) {}
+ public static bool ExpectDecimalNumber(ref SequenceReader<byte> reader, out uint @value) {}
+ public static bool ExpectEndOfLine(ref SequenceReader<byte> reader) {}
+ public static bool ExpectIPADDR(ref SequenceReader<byte> reader, [NotNullWhen(true)] out IPAddress? @value) {}
+ public static bool ExpectSequence(ref SequenceReader<byte> reader, ReadOnlySpan<byte> expectedSequence) {}
+ public static bool ExpectToken(ref SequenceReader<byte> reader, ReadOnlySpan<byte> expectedToken) {}
+ public static bool ExpectUINT16(ref SequenceReader<byte> reader, out ushort @value) {}
+ public static bool ExpectUINT32(ref SequenceReader<byte> reader, out uint @value) {}
+ public static bool ExpectUINT64(ref SequenceReader<byte> reader, out ulong @value) {}
+ public static bool ExpectUINT8(ref SequenceReader<byte> reader, out byte @value) {}
+ public static void ToByteSequence(ReadOnlySequence<byte> hexTextSequence, int byteSequenceLength, Span<byte> destination) {}
+ public static bool TryExpectStatusLine(ref SequenceReader<byte> reader, out SkStackResponseStatus status) {}
+ public static OperationStatus TryExpectToken(ref SequenceReader<byte> reader, ReadOnlySpan<byte> expectedToken) {}
+ }
+
+ public class SkStackUnexpectedResponseException : SkStackResponseException {
+ public string? CausedText { get; }
+ }
+}
+// API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.4.0.0.
+// Smdn.Reflection.ReverseGenerating.ListApi.Core v1.3.0.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)
diff --git a/doc/api-list/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP-net8.0.apilist.cs b/doc/api-list/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP-net8.0.apilist.cs
new file mode 100644
index 0000000..d28e48b
--- /dev/null
+++ b/doc/api-list/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP-net8.0.apilist.cs
@@ -0,0 +1,464 @@
+// Smdn.Net.SkStackIP.dll (Smdn.Net.SkStackIP-1.0.0)
+// Name: Smdn.Net.SkStackIP
+// AssemblyVersion: 1.0.0.0
+// InformationalVersion: 1.0.0+655c155832ea35fece55fe3cd2467b473922319c
+// TargetFramework: .NETCoreApp,Version=v8.0
+// Configuration: Release
+// Referenced assemblies:
+// Microsoft.Extensions.Logging.Abstractions, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
+// Polly.Core, Version=8.0.0.0, Culture=neutral, PublicKeyToken=c8a3ffc3f8f825cc
+// Smdn.Fundamental.ControlPicture, Version=3.0.0.1, Culture=neutral
+// Smdn.Fundamental.PrintableEncoding.Hexadecimal, Version=3.0.0.0, Culture=neutral
+// System.Collections, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.ComponentModel.Primitives, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.IO.Pipelines, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
+// System.Linq, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.Memory, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
+// System.Net.NetworkInformation, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.Net.Primitives, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// System.Threading, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+#nullable enable annotations
+
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.IO.Pipelines;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Polly;
+using Smdn.Net.SkStackIP;
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP {
+ public enum SkStackERXUDPDataFormat : int {
+ Binary = 0,
+ HexAsciiText = 1,
+ }
+
+ public enum SkStackErrorCode : int {
+ ER01 = 1,
+ ER02 = 2,
+ ER03 = 3,
+ ER04 = 4,
+ ER05 = 5,
+ ER06 = 6,
+ ER07 = 7,
+ ER08 = 8,
+ ER09 = 9,
+ ER10 = 10,
+ Undefined = 0,
+ }
+
+ public enum SkStackEventNumber : byte {
+ ActiveScanCompleted = 34,
+ BeaconReceived = 32,
+ EchoRequestReceived = 5,
+ EnergyDetectScanCompleted = 31,
+ NeighborAdvertisementReceived = 2,
+ NeighborSolicitationReceived = 1,
+ PanaSessionEstablishmentCompleted = 37,
+ PanaSessionEstablishmentError = 36,
+ PanaSessionExpired = 41,
+ PanaSessionTerminationCompleted = 39,
+ PanaSessionTerminationRequestReceived = 38,
+ PanaSessionTerminationTimedOut = 40,
+ TransmissionTimeControlLimitationActivated = 50,
+ TransmissionTimeControlLimitationDeactivated = 51,
+ UdpSendCompleted = 33,
+ Undefined = 0,
+ WakeupSignalReceived = 192,
+ }
+
+ public enum SkStackResponseStatus : int {
+ Fail = -1,
+ Ok = 1,
+ Undetermined = 0,
+ }
+
+ public enum SkStackUdpEncryption : byte {
+ EncryptIfAble = 2,
+ ForceEncrypt = 1,
+ ForcePlainText = 0,
+ }
+
+ public enum SkStackUdpPortHandle : byte {
+ Handle1 = 1,
+ Handle2 = 2,
+ Handle3 = 3,
+ Handle4 = 4,
+ Handle5 = 5,
+ Handle6 = 6,
+ None = 0,
+ }
+
+ public abstract class SkStackActiveScanOptions : ICloneable {
+ public static SkStackActiveScanOptions Default { get; }
+ public static SkStackActiveScanOptions Null { get; }
+ public static SkStackActiveScanOptions ScanUntilFind { get; }
+
+ public static SkStackActiveScanOptions Create(IEnumerable<int> scanDurationGenerator, PhysicalAddress paaMacAddress) {}
+ public static SkStackActiveScanOptions Create(IEnumerable<int> scanDurationGenerator, Predicate<SkStackPanDescription>? paaSelector = null) {}
+
+ protected SkStackActiveScanOptions() {}
+
+ public abstract SkStackActiveScanOptions Clone();
+ object ICloneable.Clone() {}
+ }
+
+ public class SkStackClient : IDisposable {
+ public static readonly TimeSpan SKSCANDefaultDuration; // = "00:00:00.0480000"
+ public static readonly TimeSpan SKSCANMaxDuration; // = "00:02:37.2960000"
+ public static readonly TimeSpan SKSCANMinDuration; // = "00:00:00.0192000"
+
+ public event EventHandler<SkStackPanaSessionEventArgs>? PanaSessionEstablished;
+ public event EventHandler<SkStackPanaSessionEventArgs>? PanaSessionExpired;
+ public event EventHandler<SkStackPanaSessionEventArgs>? PanaSessionTerminated;
+ public event EventHandler<SkStackEventArgs>? Slept;
+ public event EventHandler<SkStackEventArgs>? WokeUp;
+
+ public SkStackClient(PipeWriter sender, PipeReader receiver, SkStackERXUDPDataFormat erxudpDataFormat = SkStackERXUDPDataFormat.Binary, ILogger? logger = null) {}
+ public SkStackClient(Stream stream, bool leaveStreamOpen = true, SkStackERXUDPDataFormat erxudpDataFormat = SkStackERXUDPDataFormat.Binary, ILogger? logger = null) {}
+
+ public SkStackERXUDPDataFormat ERXUDPDataFormat { get; protected set; }
+ [MemberNotNullWhen(true, "PanaSessionPeerAddress")]
+ public bool IsPanaSessionAlive { [MemberNotNullWhen(true, "PanaSessionPeerAddress")] get; }
+ protected ILogger? Logger { get; }
+ public IPAddress? PanaSessionPeerAddress { get; }
+ public TimeSpan ReceiveResponseDelay { get; set; }
+ public TimeSpan ReceiveUdpPollingInterval { get; set; }
+ public ISynchronizeInvoke? SynchronizingObject { get; set; }
+
+ public ValueTask<IReadOnlyList<SkStackPanDescription>> ActiveScanAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, SkStackActiveScanOptions? scanOptions = null, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, IPAddress paaAddress, SkStackChannel channel, int panId, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, IPAddress paaAddress, int channelNumber, int panId, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, PhysicalAddress paaMacAddress, SkStackChannel channel, int panId, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, PhysicalAddress paaMacAddress, int channelNumber, int panId, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, SkStackActiveScanOptions? scanOptions = null, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, SkStackPanDescription pan, CancellationToken cancellationToken = default) {}
+ public async ValueTask<IPAddress> ConvertToIPv6LinkLocalAddressAsync(PhysicalAddress macAddress, CancellationToken cancellationToken = default) {}
+ public ValueTask DisableFlashMemoryAutoLoadAsync(CancellationToken cancellationToken = default) {}
+ protected virtual void Dispose(bool disposing) {}
+ public void Dispose() {}
+ public ValueTask EnableFlashMemoryAutoLoadAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<IReadOnlyList<IPAddress>> GetAvailableAddressListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<IReadOnlyList<SkStackUdpPort>> GetListeningUdpPortListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<IReadOnlyDictionary<IPAddress, PhysicalAddress>> GetNeighborCacheListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<IReadOnlyList<SkStackUdpPortHandle>> GetUnusedUdpPortHandleListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask LoadFlashMemoryAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<SkStackUdpPort> PrepareUdpPortAsync(int port, CancellationToken cancellationToken = default) {}
+ public ValueTask<IPAddress> ReceiveUdpAsync(int port, IBufferWriter<byte> buffer, CancellationToken cancellationToken = default) {}
+ public ValueTask<IPAddress> ReceiveUdpEchonetLiteAsync(IBufferWriter<byte> buffer, CancellationToken cancellationToken = default) {}
+ public ValueTask SaveFlashMemoryAsync(SkStackFlashMemoryWriteRestriction restriction, CancellationToken cancellationToken = default) {}
+ internal protected ValueTask<SkStackResponse<TPayload>> SendCommandAsync<TPayload>(ReadOnlyMemory<byte> command, Action<ISkStackCommandLineWriter>? writeArguments, SkStackSequenceParser<TPayload> parseResponsePayload, SkStackProtocolSyntax? syntax = null, bool throwIfErrorStatus = true, CancellationToken cancellationToken = default) {}
+ internal protected async ValueTask<SkStackResponse> SendCommandAsync(ReadOnlyMemory<byte> command, Action<ISkStackCommandLineWriter>? writeArguments = null, SkStackProtocolSyntax? syntax = null, bool throwIfErrorStatus = true, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKADDNBRAsync(IPAddress ipv6Address, PhysicalAddress macAddress, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<string>> SendSKAPPVERAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKDSLEEPAsync(bool waitUntilWakeUp = false, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKERASEAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<(IPAddress LinkLocalAddress, PhysicalAddress MacAddress, SkStackChannel Channel, int PanId, int Addr16)>> SendSKINFOAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKJOINAsync(IPAddress ipv6address, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<IPAddress>> SendSKLL64Async(PhysicalAddress macAddress, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKLOADAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IPAddress Address)> SendSKREJOINAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKRESETAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSAVEAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyList<SkStackPanDescription> PanDescriptions)> SendSKSCANActiveScanAsync(TimeSpan duration = default, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyList<SkStackPanDescription> PanDescriptions)> SendSKSCANActiveScanAsync(int durationFactor, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyList<SkStackPanDescription> PanDescriptions)> SendSKSCANActiveScanPairAsync(TimeSpan duration = default, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyList<SkStackPanDescription> PanDescriptions)> SendSKSCANActiveScanPairAsync(int durationFactor, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyDictionary<SkStackChannel, decimal> ScanResult)> SendSKSCANEnergyDetectScanAsync(TimeSpan duration = default, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyDictionary<SkStackChannel, decimal> ScanResult)> SendSKSCANEnergyDetectScanAsync(int durationFactor, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKSENDTOAsync(SkStackUdpPort port, IPAddress destinationAddress, int destinationPort, ReadOnlyMemory<byte> data, SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKSENDTOAsync(SkStackUdpPort port, IPEndPoint destination, ReadOnlyMemory<byte> data, SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKSENDTOAsync(SkStackUdpPortHandle handle, IPAddress destinationAddress, int destinationPort, ReadOnlyMemory<byte> data, SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKSENDTOAsync(SkStackUdpPortHandle handle, IPEndPoint destination, ReadOnlyMemory<byte> data, SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSETPWDAsync(ReadOnlyMemory<byte> password, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSETPWDAsync(ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSETRBIDAsync(ReadOnlyMemory<byte> id, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSETRBIDAsync(ReadOnlyMemory<char> id, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<TValue>> SendSKSREGAsync<TValue>(SkStackRegister.RegisterEntry<TValue> register, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSREGAsync<TValue>(SkStackRegister.RegisterEntry<TValue> register, TValue @value, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<IReadOnlyList<IPAddress>>> SendSKTABLEAvailableAddressListAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<IReadOnlyList<SkStackUdpPort>>> SendSKTABLEListeningPortListAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<IReadOnlyDictionary<IPAddress, PhysicalAddress>>> SendSKTABLENeighborCacheListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKTERMAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, SkStackUdpPort UdpPort)> SendSKUDPPORTAsync(SkStackUdpPortHandle handle, int port, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKUDPPORTUnsetAsync(SkStackUdpPortHandle handle, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<Version>> SendSKVERAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask SendUdpEchonetLiteAsync(ReadOnlyMemory<byte> buffer, ResiliencePipeline? resiliencePipeline = null, CancellationToken cancellationToken = default) {}
+ protected async ValueTask SetFlashMemoryAutoLoadAsync(bool trueIfEnable, CancellationToken cancellationToken = default) {}
+ public void StartCapturingUdpReceiveEvents(int port) {}
+ public void StopCapturingUdpReceiveEvents(int port) {}
+ public ValueTask<bool> TerminatePanaSessionAsync(CancellationToken cancellationToken = default) {}
+ protected void ThrowIfDisposed() {}
+ internal protected void ThrowIfPanaSessionAlreadyEstablished() {}
+ [MemberNotNull("PanaSessionPeerAddress")]
+ internal protected void ThrowIfPanaSessionIsNotEstablished() {}
+ }
+
+ public class SkStackCommandNotSupportedException : SkStackErrorResponseException {
+ }
+
+ public class SkStackErrorResponseException : SkStackResponseException {
+ public SkStackErrorCode ErrorCode { get; }
+ public string ErrorText { get; }
+ public SkStackResponse Response { get; }
+ }
+
+ public class SkStackEventArgs : EventArgs {
+ public SkStackEventNumber EventNumber { get; }
+ }
+
+ public class SkStackFlashMemoryIOException : SkStackErrorResponseException {
+ }
+
+ public abstract class SkStackFlashMemoryWriteRestriction {
+ public static SkStackFlashMemoryWriteRestriction CreateGrantIfElapsed(TimeSpan interval) {}
+ public static SkStackFlashMemoryWriteRestriction DangerousCreateAlwaysGrant() {}
+
+ protected SkStackFlashMemoryWriteRestriction() {}
+
+ internal protected abstract bool IsRestricted();
+ }
+
+ public static class SkStackKnownPortNumbers {
+ public const int EchonetLite = 3610;
+ public const int Pana = 716;
+ }
+
+ public class SkStackPanaSessionEstablishmentException : SkStackPanaSessionException {
+ }
+
+ public sealed class SkStackPanaSessionEventArgs : SkStackEventArgs {
+ public IPAddress PanaSessionPeerAddress { get; }
+ }
+
+ public abstract class SkStackPanaSessionException : InvalidOperationException {
+ public IPAddress Address { get; }
+ public SkStackEventNumber EventNumber { get; }
+ }
+
+ public sealed class SkStackPanaSessionInfo {
+ public SkStackChannel Channel { get; }
+ public IPAddress LocalAddress { get; }
+ public PhysicalAddress LocalMacAddress { get; }
+ public int PanId { get; }
+ public IPAddress PeerAddress { get; }
+ public PhysicalAddress PeerMacAddress { get; }
+ }
+
+ public static class SkStackRegister {
+ public abstract class RegisterEntry<TValue> {
+ private protected delegate bool ExpectValueFunc(ref SequenceReader<byte> reader, out TValue @value);
+
+ public bool IsReadable { get; }
+ public bool IsWritable { get; }
+ public TValue MaxValue { get; }
+ public TValue MinValue { get; }
+ public string Name { get; }
+ }
+
+ public static SkStackRegister.RegisterEntry<bool> AcceptIcmpEcho { get; }
+ public static SkStackRegister.RegisterEntry<ulong> AccumulatedSendTimeInMilliseconds { get; }
+ public static SkStackRegister.RegisterEntry<SkStackChannel> Channel { get; }
+ public static SkStackRegister.RegisterEntry<bool> EnableAutoLoad { get; }
+ public static SkStackRegister.RegisterEntry<bool> EnableAutoReauthentication { get; }
+ public static SkStackRegister.RegisterEntry<bool> EnableEchoback { get; }
+ public static SkStackRegister.RegisterEntry<bool> EncryptIPMulticast { get; }
+ public static SkStackRegister.RegisterEntry<uint> FrameCounter { get; }
+ public static SkStackRegister.RegisterEntry<bool> IsSendingRestricted { get; }
+ public static SkStackRegister.RegisterEntry<ReadOnlyMemory<byte>> PairingId { get; }
+ public static SkStackRegister.RegisterEntry<ushort> PanId { get; }
+ public static SkStackRegister.RegisterEntry<TimeSpan> PanaSessionLifetimeInSeconds { get; }
+ public static SkStackRegister.RegisterEntry<bool> RespondBeaconRequest { get; }
+ public static SkStackRegister.RegisterEntry<SkStackChannel> S02 { get; }
+ public static SkStackRegister.RegisterEntry<ushort> S03 { get; }
+ public static SkStackRegister.RegisterEntry<uint> S07 { get; }
+ public static SkStackRegister.RegisterEntry<ReadOnlyMemory<byte>> S0A { get; }
+ public static SkStackRegister.RegisterEntry<bool> S15 { get; }
+ public static SkStackRegister.RegisterEntry<TimeSpan> S16 { get; }
+ public static SkStackRegister.RegisterEntry<bool> S17 { get; }
+ public static SkStackRegister.RegisterEntry<bool> SA0 { get; }
+ public static SkStackRegister.RegisterEntry<bool> SA1 { get; }
+ public static SkStackRegister.RegisterEntry<bool> SFB { get; }
+ public static SkStackRegister.RegisterEntry<ulong> SFD { get; }
+ public static SkStackRegister.RegisterEntry<bool> SFE { get; }
+ public static SkStackRegister.RegisterEntry<bool> SFF { get; }
+ }
+
+ public class SkStackResponse {
+ public SkStackResponseStatus Status { get; }
+ public ReadOnlyMemory<byte> StatusText { get; }
+ public bool Success { get; }
+ }
+
+ public class SkStackResponseException : InvalidOperationException {
+ public SkStackResponseException() {}
+ public SkStackResponseException(string message) {}
+ public SkStackResponseException(string message, Exception? innerException = null) {}
+ }
+
+ public class SkStackResponse<TPayload> : SkStackResponse {
+ public TPayload Payload { get; }
+ }
+
+ public class SkStackUartIOException : SkStackErrorResponseException {
+ }
+
+ public class SkStackUdpSendFailedException : InvalidOperationException {
+ public SkStackUdpSendFailedException() {}
+ public SkStackUdpSendFailedException(string message) {}
+ public SkStackUdpSendFailedException(string message, Exception? innerException = null) {}
+ public SkStackUdpSendFailedException(string message, SkStackUdpPortHandle portHandle, IPAddress peerAddress, Exception? innerException = null) {}
+
+ public IPAddress? PeerAddress { get; }
+ public SkStackUdpPortHandle PortHandle { get; }
+ }
+
+ public class SkStackUdpSendResultIndeterminateException : InvalidOperationException {
+ public SkStackUdpSendResultIndeterminateException() {}
+ public SkStackUdpSendResultIndeterminateException(string message) {}
+ public SkStackUdpSendResultIndeterminateException(string message, Exception? innerException = null) {}
+ }
+
+ public readonly struct SkStackChannel :
+ IComparable<SkStackChannel>,
+ IEquatable<SkStackChannel>
+ {
+ public static readonly IReadOnlyDictionary<int, SkStackChannel> Channels; // = "System.Collections.Generic.Dictionary`2[System.Int32,Smdn.Net.SkStackIP.SkStackChannel]"
+ public static readonly SkStackChannel Empty; // = "0ch (S02=0x00, 0 MHz)"
+
+ public static SkStackChannel Channel33 { get; }
+ public static SkStackChannel Channel34 { get; }
+ public static SkStackChannel Channel35 { get; }
+ public static SkStackChannel Channel36 { get; }
+ public static SkStackChannel Channel37 { get; }
+ public static SkStackChannel Channel38 { get; }
+ public static SkStackChannel Channel39 { get; }
+ public static SkStackChannel Channel40 { get; }
+ public static SkStackChannel Channel41 { get; }
+ public static SkStackChannel Channel42 { get; }
+ public static SkStackChannel Channel43 { get; }
+ public static SkStackChannel Channel44 { get; }
+ public static SkStackChannel Channel45 { get; }
+ public static SkStackChannel Channel46 { get; }
+ public static SkStackChannel Channel47 { get; }
+ public static SkStackChannel Channel48 { get; }
+ public static SkStackChannel Channel49 { get; }
+ public static SkStackChannel Channel50 { get; }
+ public static SkStackChannel Channel51 { get; }
+ public static SkStackChannel Channel52 { get; }
+ public static SkStackChannel Channel53 { get; }
+ public static SkStackChannel Channel54 { get; }
+ public static SkStackChannel Channel55 { get; }
+ public static SkStackChannel Channel56 { get; }
+ public static SkStackChannel Channel57 { get; }
+ public static SkStackChannel Channel58 { get; }
+ public static SkStackChannel Channel59 { get; }
+ public static SkStackChannel Channel60 { get; }
+
+ public static bool operator == (SkStackChannel x, SkStackChannel y) {}
+ public static bool operator != (SkStackChannel x, SkStackChannel y) {}
+
+ public int ChannelNumber { get; }
+ public decimal FrequencyMHz { get; }
+ public bool IsEmpty { get; }
+
+ public bool Equals(SkStackChannel other) {}
+ public override bool Equals(object? obj) {}
+ public override int GetHashCode() {}
+ int IComparable<SkStackChannel>.CompareTo(SkStackChannel other) {}
+ public override string ToString() {}
+ }
+
+ public readonly struct SkStackPanDescription {
+ public SkStackChannel Channel { get; }
+ public int ChannelPage { get; }
+ public int Id { get; }
+ public PhysicalAddress MacAddress { get; }
+ public uint PairingId { get; }
+ public decimal Rssi { get; }
+
+ public override string ToString() {}
+ }
+
+ public readonly struct SkStackUdpPort {
+ public static readonly SkStackUdpPort Null; // = "0 (#0)"
+
+ public SkStackUdpPortHandle Handle { get; }
+ public bool IsNull { get; }
+ public bool IsUnused { get; }
+ public int Port { get; }
+
+ public override string ToString() {}
+ }
+}
+
+namespace Smdn.Net.SkStackIP.Protocol {
+ public delegate TResult SkStackSequenceParser<TResult>(ISkStackSequenceParserContext context);
+
+ public interface ISkStackCommandLineWriter {
+ void WriteMaskedToken(ReadOnlySpan<byte> token);
+ void WriteToken(ReadOnlySpan<byte> token);
+ }
+
+ public interface ISkStackSequenceParserContext {
+ ReadOnlySequence<byte> UnparsedSequence { get; }
+
+ void Complete();
+ void Complete(SequenceReader<byte> consumedReader);
+ void Continue();
+ ISkStackSequenceParserContext CreateCopy();
+ virtual SequenceReader<byte> CreateReader() {}
+ void Ignore();
+ void SetAsIncomplete();
+ void SetAsIncomplete(SequenceReader<byte> incompleteReader);
+ }
+
+ public abstract class SkStackProtocolSyntax {
+ public static SkStackProtocolSyntax Default { get; }
+
+ protected SkStackProtocolSyntax() {}
+
+ public abstract ReadOnlySpan<byte> EndOfCommandLine { get; }
+ public virtual ReadOnlySpan<byte> EndOfEchobackLine { get; }
+ public abstract ReadOnlySpan<byte> EndOfStatusLine { get; }
+ public abstract bool ExpectStatusLine { get; }
+ }
+
+ public static class SkStackTokenParser {
+ public static bool Expect<TValue>(ref SequenceReader<byte> reader, int length, Converter<ReadOnlySequence<byte>, TValue> converter, [NotNullWhen(true)] out TValue @value) {}
+ public static bool ExpectADDR16(ref SequenceReader<byte> reader, out ushort @value) {}
+ public static bool ExpectADDR64(ref SequenceReader<byte> reader, [NotNullWhen(true)] out PhysicalAddress? @value) {}
+ public static bool ExpectBinary(ref SequenceReader<byte> reader, out bool @value) {}
+ public static bool ExpectCHANNEL(ref SequenceReader<byte> reader, out SkStackChannel @value) {}
+ public static bool ExpectCharArray(ref SequenceReader<byte> reader, [NotNullWhen(true)] out string? @value) {}
+ public static bool ExpectCharArray(ref SequenceReader<byte> reader, out ReadOnlyMemory<byte> @value) {}
+ public static bool ExpectDecimalNumber(ref SequenceReader<byte> reader, int length, out uint @value) {}
+ public static bool ExpectDecimalNumber(ref SequenceReader<byte> reader, out uint @value) {}
+ public static bool ExpectEndOfLine(ref SequenceReader<byte> reader) {}
+ public static bool ExpectIPADDR(ref SequenceReader<byte> reader, [NotNullWhen(true)] out IPAddress? @value) {}
+ public static bool ExpectSequence(ref SequenceReader<byte> reader, ReadOnlySpan<byte> expectedSequence) {}
+ public static bool ExpectToken(ref SequenceReader<byte> reader, ReadOnlySpan<byte> expectedToken) {}
+ public static bool ExpectUINT16(ref SequenceReader<byte> reader, out ushort @value) {}
+ public static bool ExpectUINT32(ref SequenceReader<byte> reader, out uint @value) {}
+ public static bool ExpectUINT64(ref SequenceReader<byte> reader, out ulong @value) {}
+ public static bool ExpectUINT8(ref SequenceReader<byte> reader, out byte @value) {}
+ public static void ToByteSequence(ReadOnlySequence<byte> hexTextSequence, int byteSequenceLength, Span<byte> destination) {}
+ public static bool TryExpectStatusLine(ref SequenceReader<byte> reader, out SkStackResponseStatus status) {}
+ public static OperationStatus TryExpectToken(ref SequenceReader<byte> reader, ReadOnlySpan<byte> expectedToken) {}
+ }
+
+ public class SkStackUnexpectedResponseException : SkStackResponseException {
+ public string? CausedText { get; }
+ }
+}
+// API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.4.0.0.
+// Smdn.Reflection.ReverseGenerating.ListApi.Core v1.3.0.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)
diff --git a/doc/api-list/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP-netstandard2.1.apilist.cs b/doc/api-list/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP-netstandard2.1.apilist.cs
new file mode 100644
index 0000000..39e4fdd
--- /dev/null
+++ b/doc/api-list/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP-netstandard2.1.apilist.cs
@@ -0,0 +1,456 @@
+// Smdn.Net.SkStackIP.dll (Smdn.Net.SkStackIP-1.0.0)
+// Name: Smdn.Net.SkStackIP
+// AssemblyVersion: 1.0.0.0
+// InformationalVersion: 1.0.0+655c155832ea35fece55fe3cd2467b473922319c
+// TargetFramework: .NETStandard,Version=v2.1
+// Configuration: Release
+// Referenced assemblies:
+// Microsoft.Extensions.Logging.Abstractions, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
+// Polly.Core, Version=8.0.0.0, Culture=neutral, PublicKeyToken=c8a3ffc3f8f825cc
+// Smdn.Fundamental.ControlPicture, Version=3.0.0.1, Culture=neutral
+// Smdn.Fundamental.Encoding.Buffer, Version=3.0.0.0, Culture=neutral
+// Smdn.Fundamental.PrintableEncoding.Hexadecimal, Version=3.0.0.0, Culture=neutral
+// System.IO.Pipelines, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
+// netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
+#nullable enable annotations
+
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.IO.Pipelines;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Polly;
+using Smdn.Net.SkStackIP;
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP {
+ public enum SkStackERXUDPDataFormat : int {
+ Binary = 0,
+ HexAsciiText = 1,
+ }
+
+ public enum SkStackErrorCode : int {
+ ER01 = 1,
+ ER02 = 2,
+ ER03 = 3,
+ ER04 = 4,
+ ER05 = 5,
+ ER06 = 6,
+ ER07 = 7,
+ ER08 = 8,
+ ER09 = 9,
+ ER10 = 10,
+ Undefined = 0,
+ }
+
+ public enum SkStackEventNumber : byte {
+ ActiveScanCompleted = 34,
+ BeaconReceived = 32,
+ EchoRequestReceived = 5,
+ EnergyDetectScanCompleted = 31,
+ NeighborAdvertisementReceived = 2,
+ NeighborSolicitationReceived = 1,
+ PanaSessionEstablishmentCompleted = 37,
+ PanaSessionEstablishmentError = 36,
+ PanaSessionExpired = 41,
+ PanaSessionTerminationCompleted = 39,
+ PanaSessionTerminationRequestReceived = 38,
+ PanaSessionTerminationTimedOut = 40,
+ TransmissionTimeControlLimitationActivated = 50,
+ TransmissionTimeControlLimitationDeactivated = 51,
+ UdpSendCompleted = 33,
+ Undefined = 0,
+ WakeupSignalReceived = 192,
+ }
+
+ public enum SkStackResponseStatus : int {
+ Fail = -1,
+ Ok = 1,
+ Undetermined = 0,
+ }
+
+ public enum SkStackUdpEncryption : byte {
+ EncryptIfAble = 2,
+ ForceEncrypt = 1,
+ ForcePlainText = 0,
+ }
+
+ public enum SkStackUdpPortHandle : byte {
+ Handle1 = 1,
+ Handle2 = 2,
+ Handle3 = 3,
+ Handle4 = 4,
+ Handle5 = 5,
+ Handle6 = 6,
+ None = 0,
+ }
+
+ public abstract class SkStackActiveScanOptions : ICloneable {
+ public static SkStackActiveScanOptions Default { get; }
+ public static SkStackActiveScanOptions Null { get; }
+ public static SkStackActiveScanOptions ScanUntilFind { get; }
+
+ public static SkStackActiveScanOptions Create(IEnumerable<int> scanDurationGenerator, PhysicalAddress paaMacAddress) {}
+ public static SkStackActiveScanOptions Create(IEnumerable<int> scanDurationGenerator, Predicate<SkStackPanDescription>? paaSelector = null) {}
+
+ protected SkStackActiveScanOptions() {}
+
+ public abstract SkStackActiveScanOptions Clone();
+ object ICloneable.Clone() {}
+ }
+
+ public class SkStackClient : IDisposable {
+ public static readonly TimeSpan SKSCANDefaultDuration; // = "00:00:00.0480000"
+ public static readonly TimeSpan SKSCANMaxDuration; // = "00:02:37.2960000"
+ public static readonly TimeSpan SKSCANMinDuration; // = "00:00:00.0192000"
+
+ public event EventHandler<SkStackPanaSessionEventArgs>? PanaSessionEstablished;
+ public event EventHandler<SkStackPanaSessionEventArgs>? PanaSessionExpired;
+ public event EventHandler<SkStackPanaSessionEventArgs>? PanaSessionTerminated;
+ public event EventHandler<SkStackEventArgs>? Slept;
+ public event EventHandler<SkStackEventArgs>? WokeUp;
+
+ public SkStackClient(PipeWriter sender, PipeReader receiver, SkStackERXUDPDataFormat erxudpDataFormat = SkStackERXUDPDataFormat.Binary, ILogger? logger = null) {}
+ public SkStackClient(Stream stream, bool leaveStreamOpen = true, SkStackERXUDPDataFormat erxudpDataFormat = SkStackERXUDPDataFormat.Binary, ILogger? logger = null) {}
+
+ public SkStackERXUDPDataFormat ERXUDPDataFormat { get; protected set; }
+ public bool IsPanaSessionAlive { get; }
+ protected ILogger? Logger { get; }
+ public IPAddress? PanaSessionPeerAddress { get; }
+ public TimeSpan ReceiveResponseDelay { get; set; }
+ public TimeSpan ReceiveUdpPollingInterval { get; set; }
+ public ISynchronizeInvoke? SynchronizingObject { get; set; }
+
+ public ValueTask<IReadOnlyList<SkStackPanDescription>> ActiveScanAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, SkStackActiveScanOptions? scanOptions = null, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, IPAddress paaAddress, SkStackChannel channel, int panId, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, IPAddress paaAddress, int channelNumber, int panId, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, PhysicalAddress paaMacAddress, SkStackChannel channel, int panId, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, PhysicalAddress paaMacAddress, int channelNumber, int panId, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, SkStackActiveScanOptions? scanOptions = null, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(ReadOnlyMemory<byte> rbid, ReadOnlyMemory<byte> password, SkStackPanDescription pan, CancellationToken cancellationToken = default) {}
+ public async ValueTask<IPAddress> ConvertToIPv6LinkLocalAddressAsync(PhysicalAddress macAddress, CancellationToken cancellationToken = default) {}
+ public ValueTask DisableFlashMemoryAutoLoadAsync(CancellationToken cancellationToken = default) {}
+ protected virtual void Dispose(bool disposing) {}
+ public void Dispose() {}
+ public ValueTask EnableFlashMemoryAutoLoadAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<IReadOnlyList<IPAddress>> GetAvailableAddressListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<IReadOnlyList<SkStackUdpPort>> GetListeningUdpPortListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<IReadOnlyDictionary<IPAddress, PhysicalAddress>> GetNeighborCacheListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<IReadOnlyList<SkStackUdpPortHandle>> GetUnusedUdpPortHandleListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask LoadFlashMemoryAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<SkStackUdpPort> PrepareUdpPortAsync(int port, CancellationToken cancellationToken = default) {}
+ public ValueTask<IPAddress> ReceiveUdpAsync(int port, IBufferWriter<byte> buffer, CancellationToken cancellationToken = default) {}
+ public ValueTask<IPAddress> ReceiveUdpEchonetLiteAsync(IBufferWriter<byte> buffer, CancellationToken cancellationToken = default) {}
+ public ValueTask SaveFlashMemoryAsync(SkStackFlashMemoryWriteRestriction restriction, CancellationToken cancellationToken = default) {}
+ internal protected ValueTask<SkStackResponse<TPayload>> SendCommandAsync<TPayload>(ReadOnlyMemory<byte> command, Action<ISkStackCommandLineWriter>? writeArguments, SkStackSequenceParser<TPayload> parseResponsePayload, SkStackProtocolSyntax? syntax = null, bool throwIfErrorStatus = true, CancellationToken cancellationToken = default) {}
+ internal protected async ValueTask<SkStackResponse> SendCommandAsync(ReadOnlyMemory<byte> command, Action<ISkStackCommandLineWriter>? writeArguments = null, SkStackProtocolSyntax? syntax = null, bool throwIfErrorStatus = true, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKADDNBRAsync(IPAddress ipv6Address, PhysicalAddress macAddress, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<string>> SendSKAPPVERAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKDSLEEPAsync(bool waitUntilWakeUp = false, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKERASEAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<(IPAddress LinkLocalAddress, PhysicalAddress MacAddress, SkStackChannel Channel, int PanId, int Addr16)>> SendSKINFOAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKJOINAsync(IPAddress ipv6address, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<IPAddress>> SendSKLL64Async(PhysicalAddress macAddress, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKLOADAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IPAddress Address)> SendSKREJOINAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKRESETAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSAVEAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyList<SkStackPanDescription> PanDescriptions)> SendSKSCANActiveScanAsync(TimeSpan duration = default, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyList<SkStackPanDescription> PanDescriptions)> SendSKSCANActiveScanAsync(int durationFactor, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyList<SkStackPanDescription> PanDescriptions)> SendSKSCANActiveScanPairAsync(TimeSpan duration = default, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyList<SkStackPanDescription> PanDescriptions)> SendSKSCANActiveScanPairAsync(int durationFactor, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyDictionary<SkStackChannel, decimal> ScanResult)> SendSKSCANEnergyDetectScanAsync(TimeSpan duration = default, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, IReadOnlyDictionary<SkStackChannel, decimal> ScanResult)> SendSKSCANEnergyDetectScanAsync(int durationFactor, uint channelMask = uint.MaxValue, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKSENDTOAsync(SkStackUdpPort port, IPAddress destinationAddress, int destinationPort, ReadOnlyMemory<byte> data, SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKSENDTOAsync(SkStackUdpPort port, IPEndPoint destination, ReadOnlyMemory<byte> data, SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKSENDTOAsync(SkStackUdpPortHandle handle, IPAddress destinationAddress, int destinationPort, ReadOnlyMemory<byte> data, SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble, CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKSENDTOAsync(SkStackUdpPortHandle handle, IPEndPoint destination, ReadOnlyMemory<byte> data, SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSETPWDAsync(ReadOnlyMemory<byte> password, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSETPWDAsync(ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSETRBIDAsync(ReadOnlyMemory<byte> id, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSETRBIDAsync(ReadOnlyMemory<char> id, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<TValue>> SendSKSREGAsync<TValue>(SkStackRegister.RegisterEntry<TValue> register, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKSREGAsync<TValue>(SkStackRegister.RegisterEntry<TValue> register, TValue @value, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<IReadOnlyList<IPAddress>>> SendSKTABLEAvailableAddressListAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<IReadOnlyList<SkStackUdpPort>>> SendSKTABLEListeningPortListAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<IReadOnlyDictionary<IPAddress, PhysicalAddress>>> SendSKTABLENeighborCacheListAsync(CancellationToken cancellationToken = default) {}
+ public async ValueTask<(SkStackResponse Response, bool IsCompletedSuccessfully)> SendSKTERMAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask<(SkStackResponse Response, SkStackUdpPort UdpPort)> SendSKUDPPORTAsync(SkStackUdpPortHandle handle, int port, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse> SendSKUDPPORTUnsetAsync(SkStackUdpPortHandle handle, CancellationToken cancellationToken = default) {}
+ public ValueTask<SkStackResponse<Version>> SendSKVERAsync(CancellationToken cancellationToken = default) {}
+ public ValueTask SendUdpEchonetLiteAsync(ReadOnlyMemory<byte> buffer, ResiliencePipeline? resiliencePipeline = null, CancellationToken cancellationToken = default) {}
+ protected async ValueTask SetFlashMemoryAutoLoadAsync(bool trueIfEnable, CancellationToken cancellationToken = default) {}
+ public void StartCapturingUdpReceiveEvents(int port) {}
+ public void StopCapturingUdpReceiveEvents(int port) {}
+ public ValueTask<bool> TerminatePanaSessionAsync(CancellationToken cancellationToken = default) {}
+ protected void ThrowIfDisposed() {}
+ internal protected void ThrowIfPanaSessionAlreadyEstablished() {}
+ internal protected void ThrowIfPanaSessionIsNotEstablished() {}
+ }
+
+ public class SkStackCommandNotSupportedException : SkStackErrorResponseException {
+ }
+
+ public class SkStackErrorResponseException : SkStackResponseException {
+ public SkStackErrorCode ErrorCode { get; }
+ public string ErrorText { get; }
+ public SkStackResponse Response { get; }
+ }
+
+ public class SkStackEventArgs : EventArgs {
+ public SkStackEventNumber EventNumber { get; }
+ }
+
+ public class SkStackFlashMemoryIOException : SkStackErrorResponseException {
+ }
+
+ public abstract class SkStackFlashMemoryWriteRestriction {
+ public static SkStackFlashMemoryWriteRestriction CreateGrantIfElapsed(TimeSpan interval) {}
+ public static SkStackFlashMemoryWriteRestriction DangerousCreateAlwaysGrant() {}
+
+ protected SkStackFlashMemoryWriteRestriction() {}
+
+ internal protected abstract bool IsRestricted();
+ }
+
+ public static class SkStackKnownPortNumbers {
+ public const int EchonetLite = 3610;
+ public const int Pana = 716;
+ }
+
+ public class SkStackPanaSessionEstablishmentException : SkStackPanaSessionException {
+ }
+
+ public sealed class SkStackPanaSessionEventArgs : SkStackEventArgs {
+ public IPAddress PanaSessionPeerAddress { get; }
+ }
+
+ public abstract class SkStackPanaSessionException : InvalidOperationException {
+ public IPAddress Address { get; }
+ public SkStackEventNumber EventNumber { get; }
+ }
+
+ public sealed class SkStackPanaSessionInfo {
+ public SkStackChannel Channel { get; }
+ public IPAddress LocalAddress { get; }
+ public PhysicalAddress LocalMacAddress { get; }
+ public int PanId { get; }
+ public IPAddress PeerAddress { get; }
+ public PhysicalAddress PeerMacAddress { get; }
+ }
+
+ public static class SkStackRegister {
+ public abstract class RegisterEntry<TValue> {
+ private protected delegate bool ExpectValueFunc(ref SequenceReader<byte> reader, out TValue @value);
+
+ public bool IsReadable { get; }
+ public bool IsWritable { get; }
+ public TValue MaxValue { get; }
+ public TValue MinValue { get; }
+ public string Name { get; }
+ }
+
+ public static SkStackRegister.RegisterEntry<bool> AcceptIcmpEcho { get; }
+ public static SkStackRegister.RegisterEntry<ulong> AccumulatedSendTimeInMilliseconds { get; }
+ public static SkStackRegister.RegisterEntry<SkStackChannel> Channel { get; }
+ public static SkStackRegister.RegisterEntry<bool> EnableAutoLoad { get; }
+ public static SkStackRegister.RegisterEntry<bool> EnableAutoReauthentication { get; }
+ public static SkStackRegister.RegisterEntry<bool> EnableEchoback { get; }
+ public static SkStackRegister.RegisterEntry<bool> EncryptIPMulticast { get; }
+ public static SkStackRegister.RegisterEntry<uint> FrameCounter { get; }
+ public static SkStackRegister.RegisterEntry<bool> IsSendingRestricted { get; }
+ public static SkStackRegister.RegisterEntry<ReadOnlyMemory<byte>> PairingId { get; }
+ public static SkStackRegister.RegisterEntry<ushort> PanId { get; }
+ public static SkStackRegister.RegisterEntry<TimeSpan> PanaSessionLifetimeInSeconds { get; }
+ public static SkStackRegister.RegisterEntry<bool> RespondBeaconRequest { get; }
+ public static SkStackRegister.RegisterEntry<SkStackChannel> S02 { get; }
+ public static SkStackRegister.RegisterEntry<ushort> S03 { get; }
+ public static SkStackRegister.RegisterEntry<uint> S07 { get; }
+ public static SkStackRegister.RegisterEntry<ReadOnlyMemory<byte>> S0A { get; }
+ public static SkStackRegister.RegisterEntry<bool> S15 { get; }
+ public static SkStackRegister.RegisterEntry<TimeSpan> S16 { get; }
+ public static SkStackRegister.RegisterEntry<bool> S17 { get; }
+ public static SkStackRegister.RegisterEntry<bool> SA0 { get; }
+ public static SkStackRegister.RegisterEntry<bool> SA1 { get; }
+ public static SkStackRegister.RegisterEntry<bool> SFB { get; }
+ public static SkStackRegister.RegisterEntry<ulong> SFD { get; }
+ public static SkStackRegister.RegisterEntry<bool> SFE { get; }
+ public static SkStackRegister.RegisterEntry<bool> SFF { get; }
+ }
+
+ public class SkStackResponse {
+ public SkStackResponseStatus Status { get; }
+ public ReadOnlyMemory<byte> StatusText { get; }
+ public bool Success { get; }
+ }
+
+ public class SkStackResponseException : InvalidOperationException {
+ public SkStackResponseException() {}
+ public SkStackResponseException(string message) {}
+ public SkStackResponseException(string message, Exception? innerException = null) {}
+ }
+
+ public class SkStackResponse<TPayload> : SkStackResponse {
+ public TPayload Payload { get; }
+ }
+
+ public class SkStackUartIOException : SkStackErrorResponseException {
+ }
+
+ public class SkStackUdpSendFailedException : InvalidOperationException {
+ public SkStackUdpSendFailedException() {}
+ public SkStackUdpSendFailedException(string message) {}
+ public SkStackUdpSendFailedException(string message, Exception? innerException = null) {}
+ public SkStackUdpSendFailedException(string message, SkStackUdpPortHandle portHandle, IPAddress peerAddress, Exception? innerException = null) {}
+
+ public IPAddress? PeerAddress { get; }
+ public SkStackUdpPortHandle PortHandle { get; }
+ }
+
+ public class SkStackUdpSendResultIndeterminateException : InvalidOperationException {
+ public SkStackUdpSendResultIndeterminateException() {}
+ public SkStackUdpSendResultIndeterminateException(string message) {}
+ public SkStackUdpSendResultIndeterminateException(string message, Exception? innerException = null) {}
+ }
+
+ public readonly struct SkStackChannel :
+ IComparable<SkStackChannel>,
+ IEquatable<SkStackChannel>
+ {
+ public static readonly IReadOnlyDictionary<int, SkStackChannel> Channels; // = "System.Collections.Generic.Dictionary`2[System.Int32,Smdn.Net.SkStackIP.SkStackChannel]"
+ public static readonly SkStackChannel Empty; // = "0ch (S02=0x00, 0 MHz)"
+
+ public static SkStackChannel Channel33 { get; }
+ public static SkStackChannel Channel34 { get; }
+ public static SkStackChannel Channel35 { get; }
+ public static SkStackChannel Channel36 { get; }
+ public static SkStackChannel Channel37 { get; }
+ public static SkStackChannel Channel38 { get; }
+ public static SkStackChannel Channel39 { get; }
+ public static SkStackChannel Channel40 { get; }
+ public static SkStackChannel Channel41 { get; }
+ public static SkStackChannel Channel42 { get; }
+ public static SkStackChannel Channel43 { get; }
+ public static SkStackChannel Channel44 { get; }
+ public static SkStackChannel Channel45 { get; }
+ public static SkStackChannel Channel46 { get; }
+ public static SkStackChannel Channel47 { get; }
+ public static SkStackChannel Channel48 { get; }
+ public static SkStackChannel Channel49 { get; }
+ public static SkStackChannel Channel50 { get; }
+ public static SkStackChannel Channel51 { get; }
+ public static SkStackChannel Channel52 { get; }
+ public static SkStackChannel Channel53 { get; }
+ public static SkStackChannel Channel54 { get; }
+ public static SkStackChannel Channel55 { get; }
+ public static SkStackChannel Channel56 { get; }
+ public static SkStackChannel Channel57 { get; }
+ public static SkStackChannel Channel58 { get; }
+ public static SkStackChannel Channel59 { get; }
+ public static SkStackChannel Channel60 { get; }
+
+ public static bool operator == (SkStackChannel x, SkStackChannel y) {}
+ public static bool operator != (SkStackChannel x, SkStackChannel y) {}
+
+ public int ChannelNumber { get; }
+ public decimal FrequencyMHz { get; }
+ public bool IsEmpty { get; }
+
+ public bool Equals(SkStackChannel other) {}
+ public override bool Equals(object? obj) {}
+ public override int GetHashCode() {}
+ int IComparable<SkStackChannel>.CompareTo(SkStackChannel other) {}
+ public override string ToString() {}
+ }
+
+ public readonly struct SkStackPanDescription {
+ public SkStackChannel Channel { get; }
+ public int ChannelPage { get; }
+ public int Id { get; }
+ public PhysicalAddress MacAddress { get; }
+ public uint PairingId { get; }
+ public decimal Rssi { get; }
+
+ public override string ToString() {}
+ }
+
+ public readonly struct SkStackUdpPort {
+ public static readonly SkStackUdpPort Null; // = "0 (#0)"
+
+ public SkStackUdpPortHandle Handle { get; }
+ public bool IsNull { get; }
+ public bool IsUnused { get; }
+ public int Port { get; }
+
+ public override string ToString() {}
+ }
+}
+
+namespace Smdn.Net.SkStackIP.Protocol {
+ public delegate TResult SkStackSequenceParser<TResult>(ISkStackSequenceParserContext context);
+
+ public interface ISkStackCommandLineWriter {
+ void WriteMaskedToken(ReadOnlySpan<byte> token);
+ void WriteToken(ReadOnlySpan<byte> token);
+ }
+
+ public interface ISkStackSequenceParserContext {
+ ReadOnlySequence<byte> UnparsedSequence { get; }
+
+ void Complete();
+ void Complete(SequenceReader<byte> consumedReader);
+ void Continue();
+ ISkStackSequenceParserContext CreateCopy();
+ virtual SequenceReader<byte> CreateReader() {}
+ void Ignore();
+ void SetAsIncomplete();
+ void SetAsIncomplete(SequenceReader<byte> incompleteReader);
+ }
+
+ public abstract class SkStackProtocolSyntax {
+ public static SkStackProtocolSyntax Default { get; }
+
+ protected SkStackProtocolSyntax() {}
+
+ public abstract ReadOnlySpan<byte> EndOfCommandLine { get; }
+ public virtual ReadOnlySpan<byte> EndOfEchobackLine { get; }
+ public abstract ReadOnlySpan<byte> EndOfStatusLine { get; }
+ public abstract bool ExpectStatusLine { get; }
+ }
+
+ public static class SkStackTokenParser {
+ public static bool Expect<TValue>(ref SequenceReader<byte> reader, int length, Converter<ReadOnlySequence<byte>, TValue> converter, [NotNullWhen(true)] out TValue @value) {}
+ public static bool ExpectADDR16(ref SequenceReader<byte> reader, out ushort @value) {}
+ public static bool ExpectADDR64(ref SequenceReader<byte> reader, [NotNullWhen(true)] out PhysicalAddress? @value) {}
+ public static bool ExpectBinary(ref SequenceReader<byte> reader, out bool @value) {}
+ public static bool ExpectCHANNEL(ref SequenceReader<byte> reader, out SkStackChannel @value) {}
+ public static bool ExpectCharArray(ref SequenceReader<byte> reader, [NotNullWhen(true)] out string? @value) {}
+ public static bool ExpectCharArray(ref SequenceReader<byte> reader, out ReadOnlyMemory<byte> @value) {}
+ public static bool ExpectDecimalNumber(ref SequenceReader<byte> reader, int length, out uint @value) {}
+ public static bool ExpectDecimalNumber(ref SequenceReader<byte> reader, out uint @value) {}
+ public static bool ExpectEndOfLine(ref SequenceReader<byte> reader) {}
+ public static bool ExpectIPADDR(ref SequenceReader<byte> reader, [NotNullWhen(true)] out IPAddress? @value) {}
+ public static bool ExpectSequence(ref SequenceReader<byte> reader, ReadOnlySpan<byte> expectedSequence) {}
+ public static bool ExpectToken(ref SequenceReader<byte> reader, ReadOnlySpan<byte> expectedToken) {}
+ public static bool ExpectUINT16(ref SequenceReader<byte> reader, out ushort @value) {}
+ public static bool ExpectUINT32(ref SequenceReader<byte> reader, out uint @value) {}
+ public static bool ExpectUINT64(ref SequenceReader<byte> reader, out ulong @value) {}
+ public static bool ExpectUINT8(ref SequenceReader<byte> reader, out byte @value) {}
+ public static void ToByteSequence(ReadOnlySequence<byte> hexTextSequence, int byteSequenceLength, Span<byte> destination) {}
+ public static bool TryExpectStatusLine(ref SequenceReader<byte> reader, out SkStackResponseStatus status) {}
+ public static OperationStatus TryExpectToken(ref SequenceReader<byte> reader, ReadOnlySpan<byte> expectedToken) {}
+ }
+
+ public class SkStackUnexpectedResponseException : SkStackResponseException {
+ public string? CausedText { get; }
+ }
+}
+// API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.4.0.0.
+// Smdn.Reflection.ReverseGenerating.ListApi.Core v1.3.0.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/ISkStackCommandLineWriter.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/ISkStackCommandLineWriter.cs
new file mode 100644
index 0000000..5091a0e
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/ISkStackCommandLineWriter.cs
@@ -0,0 +1,10 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+public interface ISkStackCommandLineWriter {
+ void WriteToken(ReadOnlySpan<byte> token);
+ void WriteMaskedToken(ReadOnlySpan<byte> token);
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/ISkStackCommandLineWriterExtensions.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/ISkStackCommandLineWriterExtensions.cs
new file mode 100644
index 0000000..bf5600c
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/ISkStackCommandLineWriterExtensions.cs
@@ -0,0 +1,166 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Buffers;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+#if SYSTEM_TEXT_ASCII
+using System.Text;
+#endif
+
+using Smdn.Formats;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+internal static class ISkStackCommandLineWriterExtensions {
+ private const int LengthOfADDR64 = 16; // "0123456789ABCDEF".Length
+ private const int LengthOfIPADDR = 39; // "XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX".Length
+
+ public static void WriteTokenHex(this ISkStackCommandLineWriter writer, byte value)
+ => writer.WriteToken("0123456789ABCDEF"u8.Slice(value, 1));
+
+ public static void WriteTokenBinary(this ISkStackCommandLineWriter writer, bool value)
+ => WriteTokenUINT8(writer, value ? (byte)1 : (byte)0, zeroPadding: false);
+
+ public static void WriteTokenUINT8(this ISkStackCommandLineWriter writer, byte value, bool zeroPadding = false)
+ => WriteTokenUnsignedNumber(writer, value, length: zeroPadding ? 2 : 0);
+
+ public static void WriteTokenUINT16(this ISkStackCommandLineWriter writer, ushort value, bool zeroPadding = false)
+ => WriteTokenUnsignedNumber(writer, value, length: zeroPadding ? 4 : 0);
+
+ public static void WriteTokenUINT32(this ISkStackCommandLineWriter writer, uint value, bool zeroPadding = false)
+ => WriteTokenUnsignedNumber(writer, value, length: zeroPadding ? 8 : 0);
+
+ public static void WriteTokenUINT64(this ISkStackCommandLineWriter writer, ulong value, bool zeroPadding = false)
+ => WriteTokenUnsignedNumber(writer, value, length: zeroPadding ? 16 : 0);
+
+ private static void WriteTokenUnsignedNumber(ISkStackCommandLineWriter writer, ulong value, int length)
+ {
+ if (16 < length)
+ throw new NotSupportedException("length too long");
+
+ var formattedNumberBufferLength = length == 0
+ ? 16 // ulong.MaxValue.ToString("X").Length
+ : length;
+
+#if SYSTEM_IUTF8SPANFORMATTABLE
+ Span<byte> bytesSpan = stackalloc byte[formattedNumberBufferLength];
+
+ if (!value.TryFormat(
+ bytesSpan,
+ out var bytesWritten,
+ length == 0 ? "X" : stackalloc char[2] { 'X', (char)('0' + length) },
+ provider: null
+ )) {
+ throw new InvalidOperationException("unexpected error in conversion");
+ }
+#else
+ Span<char> charsSpan = stackalloc char[formattedNumberBufferLength];
+
+ if (!value.TryFormat(
+ charsSpan,
+ out var charsWritten,
+ length == 0 ? "X" : stackalloc char[2] { 'X', (char)('0' + length) },
+ provider: null
+ )) {
+ throw new InvalidOperationException("unexpected error in conversion");
+ }
+
+ Span<byte> bytesSpan = stackalloc byte[charsWritten];
+
+ var bytesWritten = SkStack.ToByteSequence(charsSpan.Slice(0, charsWritten), bytesSpan);
+#endif
+
+ writer.WriteToken(bytesSpan.Slice(0, bytesWritten));
+ }
+
+ public static void WriteTokenADDR64(this ISkStackCommandLineWriter writer, PhysicalAddress macAddress)
+ {
+ if (macAddress is null)
+ throw new ArgumentNullException(nameof(macAddress));
+
+ var macAddressBytes = macAddress.GetAddressBytes();
+
+ if (macAddressBytes.Length != 8)
+ throw new ArgumentException("address length must be exactly 64 bits", nameof(macAddress));
+
+ Span<byte> addr64 = stackalloc byte[LengthOfADDR64];
+
+ _ = Hexadecimal.TryEncodeUpperCase(macAddressBytes.AsSpan(), addr64, out _);
+
+ writer.WriteToken(addr64);
+ }
+
+ public static void WriteTokenIPADDR(this ISkStackCommandLineWriter writer, IPAddress ipv6address)
+ {
+ if (ipv6address is null)
+ throw new ArgumentNullException(nameof(ipv6address));
+ if (ipv6address.AddressFamily != AddressFamily.InterNetworkV6)
+ throw new ArgumentException($"`{nameof(ipv6address)}.{nameof(IPAddress.AddressFamily)}` must be {nameof(AddressFamily.InterNetworkV6)}");
+
+ const int LengthOfIPv6Address = 16;
+
+ Span<byte> addressBytes = stackalloc byte[LengthOfIPv6Address];
+
+ if (!ipv6address.TryWriteBytes(addressBytes, out _))
+ throw new InvalidOperationException($"{nameof(IPAddress)}.{nameof(IPAddress.TryWriteBytes)} failed unexpectedly");
+
+ Span<byte> ipaddr = stackalloc byte[LengthOfIPADDR];
+ var bytesWritten = 0;
+
+ for (var i = 0; i < LengthOfIPv6Address; i += 2) {
+ if (0 < i)
+ ipaddr[bytesWritten++] = (byte)':';
+
+ Hexadecimal.TryEncodeUpperCase(addressBytes.Slice(i, 2), ipaddr.Slice(bytesWritten), out var bytesEncoded);
+
+ bytesWritten += bytesEncoded;
+ }
+
+ writer.WriteToken(ipaddr);
+ }
+
+ public static void WriteToken(this ISkStackCommandLineWriter writer, ReadOnlySpan<char> token)
+ => WriteDefaultEncodingToken(
+ writer,
+ token,
+ write: static (t, w) => w.WriteToken(t)
+ );
+
+ public static void WriteMaskedToken(this ISkStackCommandLineWriter writer, ReadOnlySpan<char> token)
+ => WriteDefaultEncodingToken(
+ writer,
+ token,
+ write: static (t, w) => w.WriteMaskedToken(t)
+ );
+
+ private static void WriteDefaultEncodingToken(
+ ISkStackCommandLineWriter writer,
+ ReadOnlySpan<char> token,
+ SpanAction<byte, ISkStackCommandLineWriter> write
+ )
+ {
+ if (token.IsEmpty)
+ throw new ArgumentException("cannot be empty", paramName: nameof(token));
+
+ byte[]? tokenBytes = null;
+
+ try {
+ tokenBytes = ArrayPool<byte>.Shared.Rent(token.Length);
+
+#if SYSTEM_TEXT_ASCII
+ if (Ascii.FromUtf16(token, tokenBytes.AsSpan(), out var lengthOfToken) != OperationStatus.Done)
+ throw new ArgumentException("token contains non ASCII characters", paramName: nameof(token));
+#else
+ var lengthOfToken = SkStack.ToByteSequence(token, tokenBytes.AsSpan());
+#endif
+
+ write(tokenBytes.AsSpan(0, lengthOfToken), writer);
+ }
+ finally {
+ if (tokenBytes is not null)
+ ArrayPool<byte>.Shared.Return(tokenBytes, clearArray: true);
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/ISkStackSequenceParserContext.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/ISkStackSequenceParserContext.cs
new file mode 100644
index 0000000..5ec3756
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/ISkStackSequenceParserContext.cs
@@ -0,0 +1,21 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+#pragma warning disable CA1716
+
+using System.Buffers;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+public interface ISkStackSequenceParserContext {
+ ReadOnlySequence<byte> UnparsedSequence { get; }
+
+ SequenceReader<byte> CreateReader() => new(UnparsedSequence);
+ ISkStackSequenceParserContext CreateCopy();
+
+ void Continue();
+ void Complete();
+ void Complete(SequenceReader<byte> consumedReader);
+ void Ignore();
+ void SetAsIncomplete();
+ void SetAsIncomplete(SequenceReader<byte> incompleteReader);
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStack.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStack.cs
new file mode 100644
index 0000000..261f9f5
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStack.cs
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.Buffers;
+using System.Text;
+
+#if !SYSTEM_TEXT_ENCODINGEXTENSIONS
+using Smdn.Text.Encodings; // EncodingReadOnlySequenceExtensions
+#endif
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+internal static class SkStack {
+ private static readonly Encoding DefaultEncoding = Encoding.ASCII;
+
+ public static byte[] ToByteSequence(string text)
+ => DefaultEncoding.GetBytes(text);
+
+#if !SYSTEM_TEXT_ASCII
+ public static int ToByteSequence(ReadOnlySpan<char> source, Span<byte> destination)
+ => DefaultEncoding.GetBytes(source, destination);
+#endif
+
+ public static string GetString(ReadOnlySpan<byte> sequence)
+ => DefaultEncoding.GetString(sequence);
+
+ public static string GetString(ReadOnlySequence<byte> sequence)
+ => DefaultEncoding.GetString(sequence);
+
+ public static ReadOnlySpan<byte> CRLFSpan => "\r\n"u8;
+
+ public const byte SP = 0x20;
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackCommandLineWriter.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackCommandLineWriter.cs
new file mode 100644
index 0000000..3ae3366
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackCommandLineWriter.cs
@@ -0,0 +1,70 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Buffers;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+internal sealed class SkStackCommandLineWriter : ISkStackCommandLineWriter {
+ private readonly IBufferWriter<byte> writer;
+ private readonly IBufferWriter<byte>? writerForLog;
+
+ public SkStackCommandLineWriter(
+ IBufferWriter<byte> writer,
+ IBufferWriter<byte>? writerForLog
+ )
+ {
+ this.writer = writer;
+ this.writerForLog = writerForLog;
+ }
+
+ public void Write(ReadOnlySpan<byte> sequence)
+ {
+ if (sequence.IsEmpty)
+ throw new ArgumentException("cannot be empty", paramName: nameof(sequence));
+
+ writer.Write(sequence);
+
+ writerForLog?.Write(sequence);
+ }
+
+ public void WriteToken(ReadOnlySpan<byte> token)
+ {
+ if (token.IsEmpty)
+ throw new ArgumentException("cannot be empty", paramName: nameof(token));
+
+ writer.GetSpan(1)[0] = SkStack.SP;
+ writer.Advance(1);
+
+ writer.Write(token);
+
+ if (writerForLog is not null) {
+ writerForLog.GetSpan(1)[0] = SkStack.SP;
+ writerForLog.Advance(1);
+
+ writerForLog.Write(token);
+ }
+ }
+
+ public void WriteMaskedToken(ReadOnlySpan<byte> token)
+ {
+ if (token.IsEmpty)
+ throw new ArgumentException("cannot be empty", paramName: nameof(token));
+
+ writer.GetSpan(1)[0] = SkStack.SP;
+ writer.Advance(1);
+
+ writer.Write(token);
+
+ if (writerForLog is not null) {
+ writerForLog.GetSpan(1)[0] = SkStack.SP;
+ writerForLog.Advance(1);
+
+ const int MaskLength = 4;
+ const byte MaskByte = (byte)'*';
+
+ writerForLog.GetSpan(MaskLength).Slice(0, MaskLength).Fill(MaskByte);
+ writerForLog.Advance(MaskLength);
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackCommandNames.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackCommandNames.cs
new file mode 100644
index 0000000..22f096a
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackCommandNames.cs
@@ -0,0 +1,161 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 3. コマンドリファレンス' for detailed specifications.</para>
+/// </remarks>
+internal class SkStackCommandNames {
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.1. SKSREG' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKSREG { get; } = SkStack.ToByteSequence(nameof(SKSREG));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.2. SKINFO' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKINFO { get; } = SkStack.ToByteSequence(nameof(SKINFO));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.3. SKSTART' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKSTART => throw new NotImplementedException();
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.4. SKJOIN' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKJOIN { get; } = SkStack.ToByteSequence(nameof(SKJOIN));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.5. SKREJOIN' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKREJOIN { get; } = SkStack.ToByteSequence(nameof(SKREJOIN));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.6. SKTERM' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKTERM { get; } = SkStack.ToByteSequence(nameof(SKTERM));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.7. SKSENDTO' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKSENDTO { get; } = SkStack.ToByteSequence(nameof(SKSENDTO));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.8. SKPING' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKPING => throw new NotImplementedException();
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.9. SKSCAN' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKSCAN { get; } = SkStack.ToByteSequence(nameof(SKSCAN));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.10. SKREGDEV' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKREGDEV => throw new NotImplementedException();
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.11. SKRMDEV' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKRMDEV => throw new NotImplementedException();
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.12. SKSETKEY' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKSETKEY => throw new NotImplementedException();
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.13. SKRMKEY' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKRMKEY => throw new NotImplementedException();
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.14. SKSECENABLE' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKSECENABLE => throw new NotImplementedException();
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.15. SKSETPSK' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKSETPSK => throw new NotImplementedException();
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.16. SKSETPWD' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKSETPWD { get; } = SkStack.ToByteSequence(nameof(SKSETPWD));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.17. SKSETRBID' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKSETRBID { get; } = SkStack.ToByteSequence(nameof(SKSETRBID));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.18. SKADDNBR' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKADDNBR { get; } = SkStack.ToByteSequence(nameof(SKADDNBR));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.19. SKUDPPORT' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKUDPPORT { get; } = SkStack.ToByteSequence(nameof(SKUDPPORT));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.20. SKSAVE' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKSAVE { get; } = SkStack.ToByteSequence(nameof(SKSAVE));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.21. SKLOAD' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKLOAD { get; } = SkStack.ToByteSequence(nameof(SKLOAD));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.22. SKERASE' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKERASE { get; } = SkStack.ToByteSequence(nameof(SKERASE));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.23. SKVER' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKVER { get; } = SkStack.ToByteSequence(nameof(SKVER));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.24. SKAPPVER' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKAPPVER { get; } = SkStack.ToByteSequence(nameof(SKAPPVER));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.25. SKRESET' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKRESET { get; } = SkStack.ToByteSequence(nameof(SKRESET));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.26. SKTABLE' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKTABLE { get; } = SkStack.ToByteSequence(nameof(SKTABLE));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.27. SKDSLEEP' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKDSLEEP { get; } = SkStack.ToByteSequence(nameof(SKDSLEEP));
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.28. SKRFLO' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKRFLO => throw new NotImplementedException();
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.29. SKLL64' for detailed specifications.</para>
+ /// </remarks>
+ public static ReadOnlyMemory<byte> SKLL64 { get; } = SkStack.ToByteSequence(nameof(SKLL64));
+
+#if false
+ /// <summary>`SKSLEEP` undocumented command.</summary>
+ public static ReadOnlyMemory<byte> SKSLEEP => throw new NotImplementedException();
+#endif
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackErrorCodeNames.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackErrorCodeNames.cs
new file mode 100644
index 0000000..489b0c2
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackErrorCodeNames.cs
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 7. エラーコード' for detailed specifications.</para>
+/// </remarks>
+internal static class SkStackErrorCodeNames {
+ public static SkStackErrorCode ParseErrorCode(ReadOnlySpan<byte> errorCodeName)
+ {
+ if (errorCodeName.Length != 4)
+ return SkStackErrorCode.Undefined;
+
+ if (errorCodeName[0] != (byte)'E')
+ return SkStackErrorCode.Undefined;
+
+ if (errorCodeName[1] != (byte)'R')
+ return SkStackErrorCode.Undefined;
+
+ if (errorCodeName[2] == (byte)'0') {
+ // ER01-ER09
+ return errorCodeName[3] switch {
+ (byte)'1' => SkStackErrorCode.ER01,
+ (byte)'2' => SkStackErrorCode.ER02,
+ (byte)'3' => SkStackErrorCode.ER03,
+ (byte)'4' => SkStackErrorCode.ER04,
+ (byte)'5' => SkStackErrorCode.ER05,
+ (byte)'6' => SkStackErrorCode.ER06,
+ (byte)'7' => SkStackErrorCode.ER07,
+ (byte)'8' => SkStackErrorCode.ER08,
+ (byte)'9' => SkStackErrorCode.ER09,
+ _ => SkStackErrorCode.Undefined,
+ };
+ }
+ else if (errorCodeName[2] == (byte)'1' && errorCodeName[3] == (byte)'0') {
+ // ER10
+ return SkStackErrorCode.ER10;
+ }
+
+ return SkStackErrorCode.Undefined;
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEvent.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEvent.cs
new file mode 100644
index 0000000..96ef86b
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEvent.cs
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE
+using System.Diagnostics.CodeAnalysis;
+#endif
+using System.Net;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 4.8. EVENT' for detailed specifications.</para>
+/// </remarks>
+internal readonly struct SkStackEvent {
+ public static SkStackEvent Create(
+ SkStackEventNumber number,
+ IPAddress senderAddress,
+ int parameter,
+ SkStackEventCode expectedSubsequentEventCode
+ )
+ => new(
+ number,
+ senderAddress ?? throw new ArgumentNullException(nameof(senderAddress)),
+ parameter,
+ expectedSubsequentEventCode
+ );
+
+ public static SkStackEvent CreateWakeupSignalReceived()
+ => new(
+ SkStackEventNumber.WakeupSignalReceived,
+ default,
+ default,
+ default
+ );
+
+ public bool HasSenderAddress => Number != SkStackEventNumber.WakeupSignalReceived;
+
+ public SkStackEventNumber Number { get; }
+
+#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE
+ [MemberNotNullWhen(true, nameof(HasSenderAddress))]
+#endif
+ public IPAddress? SenderAddress { get; }
+
+ public int Parameter { get; }
+
+ public SkStackEventCode ExpectedSubsequentEventCode { get; }
+
+ private SkStackEvent(
+ SkStackEventNumber number,
+ IPAddress? senderAddress,
+ int parameter,
+ SkStackEventCode expectedSubsequentEventCode
+ )
+ {
+ Number = number;
+ SenderAddress = senderAddress;
+ Parameter = parameter;
+ ExpectedSubsequentEventCode = expectedSubsequentEventCode;
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEventCode.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEventCode.cs
new file mode 100644
index 0000000..5cab3b1
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEventCode.cs
@@ -0,0 +1,51 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 4. イベント' for detailed specifications.</para>
+/// </remarks>
+internal enum SkStackEventCode {
+ Undefined = 0,
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.1. ERXUDP' for detailed specifications.</para>
+ /// </remarks>
+ ERXUDP = 0x41,
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.2. EPONG' for detailed specifications.</para>
+ /// </remarks>
+ EPONG = 0x42,
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.3. EADDR' for detailed specifications.</para>
+ /// </remarks>
+ EADDR = 0x43,
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.4. ENEIGHBOR' for detailed specifications.</para>
+ /// </remarks>
+ ENEIGHBOR = 0x44,
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.5. EPANDESC' for detailed specifications.</para>
+ /// </remarks>
+ EPANDESC = 0x45,
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.6. EEDSCAN' for detailed specifications.</para>
+ /// </remarks>
+ EEDSCAN = 0x46,
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.7. EPORT' for detailed specifications.</para>
+ /// </remarks>
+ EPORT = 0x47,
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.8. EVENT' for detailed specifications.</para>
+ /// </remarks>
+ EVENT = 0x48,
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEventCodeNames.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEventCodeNames.cs
new file mode 100644
index 0000000..7e36d5f
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEventCodeNames.cs
@@ -0,0 +1,17 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+internal static class SkStackEventCodeNames {
+ public static ReadOnlySpan<byte> ERXUDP => "ERXUDP"u8;
+ public static ReadOnlySpan<byte> EPONG => "EPONG"u8;
+ public static ReadOnlySpan<byte> EADDR => "EADDR"u8;
+ public static ReadOnlySpan<byte> ENEIGHBOR => "ENEIGHBOR"u8;
+ public static ReadOnlySpan<byte> EPANDESC => "EPANDESC"u8;
+ public static ReadOnlySpan<byte> EEDSCAN => "EEDSCAN"u8;
+ public static ReadOnlySpan<byte> EPORT => "EPORT"u8;
+ public static ReadOnlySpan<byte> EVENT => "EVENT"u8;
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEventHandlerBase.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEventHandlerBase.cs
new file mode 100644
index 0000000..a51f3f5
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEventHandlerBase.cs
@@ -0,0 +1,9 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+namespace Smdn.Net.SkStackIP.Protocol;
+
+internal abstract class SkStackEventHandlerBase {
+ public virtual bool DoContinueHandlingEvents(SkStackResponseStatus status) => status != SkStackResponseStatus.Fail;
+ public abstract bool TryProcessEvent(SkStackEvent ev);
+ public virtual void ProcessSubsequentEvent(ISkStackSequenceParserContext context) { /*do nothing*/ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEventParser.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEventParser.cs
new file mode 100644
index 0000000..1c281dc
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackEventParser.cs
@@ -0,0 +1,428 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Collections.ObjectModel; // ReadOnlyDictionary
+#if NULL_STATE_STATIC_ANALYSIS_ATTRIBUTES
+using System.Diagnostics.CodeAnalysis;
+#endif
+using System.Net;
+using System.Net.NetworkInformation;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+internal static class SkStackEventParser {
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.1. ERXUDP' for detailed specifications.</para>
+ /// </remarks>
+ public static OperationStatus TryExpectERXUDP(
+ ISkStackSequenceParserContext context,
+ SkStackERXUDPDataFormat erxudpDataFormat,
+ out SkStackUdpReceiveEvent erxudp,
+ out ReadOnlySequence<byte> erxudpData,
+ out int erxudpDataLength
+ )
+ {
+ erxudp = default;
+ erxudpData = default;
+ erxudpDataLength = default;
+
+ var reader = context.CreateReader();
+ var status = SkStackTokenParser.TryExpectToken(ref reader, SkStackEventCodeNames.ERXUDP);
+
+ if (status is OperationStatus.NeedMoreData or OperationStatus.InvalidData)
+ return status;
+
+ if (
+ SkStackTokenParser.ExpectIPADDR(ref reader, out var sender) &&
+ SkStackTokenParser.ExpectIPADDR(ref reader, out var dest) &&
+ SkStackTokenParser.ExpectUINT16(ref reader, out var rport) &&
+ SkStackTokenParser.ExpectUINT16(ref reader, out var lport) &&
+ SkStackTokenParser.ExpectADDR64(ref reader, out var senderlla) &&
+ SkStackTokenParser.ExpectBinary(ref reader, out var secured) &&
+ SkStackTokenParser.ExpectUINT16(ref reader, out var datalen)
+ ) {
+ erxudpDataLength = datalen;
+
+ var lengthOfDataSequence = erxudpDataFormat switch {
+ SkStackERXUDPDataFormat.HexAsciiText => erxudpDataLength * 2,
+ _ => erxudpDataLength,
+ };
+
+ if (reader.Remaining < lengthOfDataSequence + 2 /*CRLF*/)
+ return OperationStatus.NeedMoreData;
+
+ var erxudpDataStart = reader.Position;
+
+ reader.Advance(lengthOfDataSequence);
+
+ var erxudpDataEnd = reader.Position;
+
+ if (!SkStackTokenParser.ExpectEndOfLine(ref reader))
+ return OperationStatus.NeedMoreData;
+
+ erxudp = new(
+ sender: sender,
+ dest: dest,
+ rport: rport,
+ lport: lport,
+ senderlla: senderlla,
+ secured: secured
+ );
+
+ erxudpData = reader.Sequence.Slice(erxudpDataStart, erxudpDataEnd);
+
+ context.Complete(reader);
+ return OperationStatus.Done;
+ }
+
+ return OperationStatus.NeedMoreData;
+ }
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.2. EPONG' for detailed specifications.</para>
+ /// </remarks>
+ public static bool ExpectEPONG(
+ ISkStackSequenceParserContext context
+ )
+ => throw new NotImplementedException();
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.3. EADDR' for detailed specifications.</para>
+ /// </remarks>
+ public static IReadOnlyList<IPAddress>? ExpectEADDR(
+ ISkStackSequenceParserContext context
+ )
+ {
+ var reader = context.CreateReader();
+
+ if (SkStackTokenParser.TryExpectStatusLine(ref reader, out var status)) {
+ // do not consume here
+ context.Ignore();
+ return status == SkStackResponseStatus.Ok ? Array.Empty<IPAddress>() : null;
+ }
+ else if (
+ SkStackTokenParser.ExpectToken(ref reader, SkStackEventCodeNames.EADDR) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ var list = new List<IPAddress>(capacity: 2);
+
+ for (; ; ) {
+ var statusLineReader = reader;
+
+ if (SkStackTokenParser.TryExpectStatusLine(ref statusLineReader, out var st)) {
+ context.Complete(reader); // do not consume status line here
+ return st == SkStackResponseStatus.Ok ? list : null;
+ }
+ else if (
+ SkStackTokenParser.ExpectIPADDR(ref reader, out var address) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ list.Add(address);
+ }
+ else {
+ context.SetAsIncomplete();
+ return default;
+ }
+ }
+ }
+
+ context.SetAsIncomplete();
+ return default;
+ }
+
+ private static readonly IReadOnlyDictionary<IPAddress, PhysicalAddress> EmptyNeighborCacheList = new ReadOnlyDictionary<IPAddress, PhysicalAddress>(
+ new Dictionary<IPAddress, PhysicalAddress>(capacity: 0)
+ );
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.4. ENEIGHBOR' for detailed specifications.</para>
+ /// </remarks>
+ public static IReadOnlyDictionary<IPAddress, PhysicalAddress>? ExpectENEIGHBOR(
+ ISkStackSequenceParserContext context
+ )
+ {
+ var statusLineReader = context.CreateReader();
+ var reader = context.CreateReader();
+
+ if (SkStackTokenParser.TryExpectStatusLine(ref statusLineReader, out var status)) {
+ // do not consume status line here
+ context.Ignore();
+ return status == SkStackResponseStatus.Ok ? EmptyNeighborCacheList : null;
+ }
+ else if (
+ SkStackTokenParser.ExpectToken(ref reader, SkStackEventCodeNames.ENEIGHBOR) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ const int NumberOfNeighborCacheEntry = 8; // 3.18. SKADDNBR
+ var neighborCache = new Dictionary<IPAddress, PhysicalAddress>(capacity: NumberOfNeighborCacheEntry);
+
+ for (; ; ) {
+ statusLineReader = reader;
+
+ if (SkStackTokenParser.TryExpectStatusLine(ref statusLineReader, out var st)) {
+ context.Complete(reader); // do not consume status line here
+ return st == SkStackResponseStatus.Ok ? neighborCache : null;
+ }
+ else if (
+ SkStackTokenParser.ExpectIPADDR(ref reader, out var ipaddr) &&
+ SkStackTokenParser.ExpectADDR64(ref reader, out var addr64) &&
+ SkStackTokenParser.ExpectADDR16(ref reader, out _) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ neighborCache[ipaddr] = addr64;
+ }
+ else {
+ break;
+ }
+ }
+ }
+
+ context.SetAsIncomplete();
+ return default;
+ }
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.5. EPANDESC' for detailed specifications.</para>
+ /// </remarks>
+ public static bool ExpectEPANDESC(
+ ISkStackSequenceParserContext context,
+ bool expectPairingId,
+ out SkStackPanDescription pandesc
+ )
+ {
+ pandesc = default;
+
+ var reader = context.CreateReader();
+
+ if (
+ SkStackTokenParser.ExpectToken(ref reader, SkStackEventCodeNames.EPANDESC) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ if (
+ SkStackTokenParser.ExpectSequence(ref reader, EPANDESCPrefixChannel) &&
+ SkStackTokenParser.ExpectUINT8(ref reader, out var channel) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader) &&
+
+ SkStackTokenParser.ExpectSequence(ref reader, EPANDESCPrefixChannelPage) &&
+ SkStackTokenParser.ExpectUINT8(ref reader, out var channelPage) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader) &&
+
+ SkStackTokenParser.ExpectSequence(ref reader, EPANDESCPrefixPanId) &&
+ SkStackTokenParser.ExpectUINT16(ref reader, out var panId) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader) &&
+
+ SkStackTokenParser.ExpectSequence(ref reader, EPANDESCPrefixAddress) &&
+ SkStackTokenParser.ExpectADDR64(ref reader, out var macAddress) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader) &&
+
+ SkStackTokenParser.ExpectSequence(ref reader, EPANDESCPrefixLQI) &&
+ SkStackTokenParser.ExpectUINT8(ref reader, out var lqi) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ uint pairingId = default;
+
+ if (expectPairingId) {
+ if (!(
+ SkStackTokenParser.ExpectSequence(ref reader, EPANDESCPrefixPairId) &&
+ SkStackTokenParser.ExpectUINT32(ref reader, out pairingId) && // instead of CHAR[8]
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ )) {
+ context.SetAsIncomplete();
+ return false;
+ }
+ }
+
+ pandesc = new SkStackPanDescription(
+ channel: SkStackChannel.FindByChannelNumber(channel, nameof(channel)),
+ channelPage: channelPage,
+ id: panId,
+ macAddress: macAddress,
+ rssi: SkStackLQI.ToRSSI(lqi),
+ pairingId: expectPairingId ? pairingId : default
+ );
+
+ context.Complete(reader);
+ return true;
+ }
+ }
+
+ context.SetAsIncomplete();
+ return false;
+ }
+
+#pragma warning disable IDE0055
+ private static ReadOnlySpan<byte> EPANDESCPrefixChannel => " Channel:"u8;
+ private static ReadOnlySpan<byte> EPANDESCPrefixChannelPage => " Channel Page:"u8;
+ private static ReadOnlySpan<byte> EPANDESCPrefixPanId => " Pan ID:"u8;
+ private static ReadOnlySpan<byte> EPANDESCPrefixAddress => " Addr:"u8;
+ private static ReadOnlySpan<byte> EPANDESCPrefixLQI => " LQI:"u8;
+ private static ReadOnlySpan<byte> EPANDESCPrefixPairId => " PairID:"u8;
+#pragma warning restore IDE0055
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.6. EEDSCAN' for detailed specifications.</para>
+ /// </remarks>
+ public static bool ExpectEEDSCAN(
+ ISkStackSequenceParserContext context,
+#if NULL_STATE_STATIC_ANALYSIS_ATTRIBUTES
+ [NotNullWhen(true)]
+#endif
+ out IReadOnlyDictionary<SkStackChannel, decimal>? result
+ )
+ {
+ result = default;
+
+ var reader = context.CreateReader();
+
+ if (
+ SkStackTokenParser.ExpectToken(ref reader, SkStackEventCodeNames.EEDSCAN) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ var ret = new Dictionary<SkStackChannel, decimal>(SkStackChannel.Channels.Count);
+
+ result = ret;
+
+ for (var i = 0; i < SkStackChannel.Channels.Count; i++) {
+ if (
+ SkStackTokenParser.ExpectUINT8(ref reader, out var channel) &&
+ SkStackTokenParser.ExpectUINT8(ref reader, out var lqi)
+ ) {
+ ret[SkStackChannel.FindByChannelNumber(channel, nameof(channel))] = SkStackLQI.ToRSSI(lqi);
+ }
+ else {
+ context.SetAsIncomplete();
+ return false;
+ }
+ }
+
+ if (SkStackTokenParser.ExpectEndOfLine(ref reader)) {
+ // [VER 1.2.10, APPVER rev26e] EEDSCAN responds extra CRLF
+ if (SkStackTokenParser.ExpectSequence(ref reader, SkStack.CRLFSpan))
+ SkStackTokenParser.ExpectEndOfLine(ref reader);
+
+ context.Complete(reader);
+ return true;
+ }
+ }
+
+ context.SetAsIncomplete();
+ return false;
+ }
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.7. EPORT' for detailed specifications.</para>
+ /// </remarks>
+ public static IReadOnlyList<SkStackUdpPort>? ExpectEPORT(
+ ISkStackSequenceParserContext context
+ )
+ {
+ var statusLineReader = context.CreateReader();
+ var reader = context.CreateReader();
+
+ if (SkStackTokenParser.TryExpectStatusLine(ref statusLineReader, out var status)) {
+ // do not consume status line here
+ context.Ignore();
+ return status == SkStackResponseStatus.Ok ? Array.Empty<SkStackUdpPort>() : null;
+ }
+ else if (
+ SkStackTokenParser.ExpectToken(ref reader, SkStackEventCodeNames.EPORT) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ var ports = new SkStackUdpPort[SkStackUdpPort.NumberOfPorts];
+
+ for (var i = 0; i < SkStackUdpPort.NumberOfPorts; i++) {
+ if (
+ SkStackTokenParser.ExpectDecimalNumber(ref reader, out var port) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ ports[i] = new SkStackUdpPort(
+ handle: (SkStackUdpPortHandle)((int)SkStackUdpPort.HandleMin + i),
+ port: (int)port
+ );
+ }
+ else {
+ break; // incomplete
+ }
+ }
+
+ for (; ; ) {
+ statusLineReader = reader;
+
+ if (SkStackTokenParser.TryExpectStatusLine(ref statusLineReader, out var st)) {
+ context.Complete(reader); // do not consume status line here
+ return st == SkStackResponseStatus.Ok ? ports : null;
+ }
+
+ // [VER 1.2.10, APPVER rev26e] EPORT responds extra CRLF and PORT_UDPs?
+ // "EPORT␍␊3610␍␊716␍␊0␍␊0␍␊0␍␊0␍␊␍␊3610␍␊0␍␊0␍␊0␍␊OK␍␊"
+
+ if (reader.TryReadTo(out ReadOnlySequence<byte> _, SkStack.CRLFSpan, advancePastDelimiter: true))
+ continue; // ignore extra lines
+ else
+ break; // incomplete
+ }
+ }
+
+ context.SetAsIncomplete();
+ return default;
+ }
+
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.8. EVENT' for detailed specifications.</para>
+ /// </remarks>
+ public static OperationStatus TryExpectEVENT(
+ ISkStackSequenceParserContext context,
+ out SkStackEvent ev
+ )
+ {
+ ev = default;
+
+ var reader = context.CreateReader();
+ var status = SkStackTokenParser.TryExpectToken(ref reader, SkStackEventCodeNames.EVENT);
+
+ if (status is OperationStatus.NeedMoreData or OperationStatus.InvalidData)
+ return status;
+
+ if (SkStackTokenParser.ExpectUINT8(ref reader, out var num)) {
+ var number = (SkStackEventNumber)num;
+
+ IPAddress? sender = default;
+ int parameter = default;
+ SkStackEventCode expectedSubsequentEventCode = default;
+
+ // C0 does not define <IPADDR> and <PARAM>
+ if (number != SkStackEventNumber.WakeupSignalReceived) {
+ if (!SkStackTokenParser.ExpectIPADDR(ref reader, out sender))
+ return OperationStatus.NeedMoreData;
+
+ switch (number) {
+ case SkStackEventNumber.EnergyDetectScanCompleted:
+ expectedSubsequentEventCode = SkStackEventCode.EEDSCAN;
+ break;
+ case SkStackEventNumber.BeaconReceived:
+ expectedSubsequentEventCode = SkStackEventCode.EPANDESC;
+ break;
+ case SkStackEventNumber.UdpSendCompleted:
+ if (!SkStackTokenParser.ExpectUINT8(ref reader, out var param))
+ return OperationStatus.NeedMoreData;
+
+ parameter = param;
+ break;
+ }
+ }
+
+ if (SkStackTokenParser.ExpectEndOfLine(ref reader)) {
+ ev = number == SkStackEventNumber.WakeupSignalReceived
+ ? SkStackEvent.CreateWakeupSignalReceived()
+ : SkStackEvent.Create(number, sender!, parameter, expectedSubsequentEventCode);
+
+ context.Complete(reader);
+ return OperationStatus.Done;
+ }
+ }
+
+ return OperationStatus.NeedMoreData;
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackLQI.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackLQI.cs
new file mode 100644
index 0000000..bd8be43
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackLQI.cs
@@ -0,0 +1,11 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+internal static class SkStackLQI {
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 4.6. EEDSCAN' for detailed specifications.</para>
+ /// </remarks>
+ public static decimal ToRSSI(int lqi) => (0.275m * lqi) - 104.27m;
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackProtocolSyntax.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackProtocolSyntax.cs
new file mode 100644
index 0000000..77558ba
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackProtocolSyntax.cs
@@ -0,0 +1,42 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+public abstract class SkStackProtocolSyntax {
+ public static SkStackProtocolSyntax Default { get; } = new DefaultSyntax();
+
+ private sealed class DefaultSyntax : SkStackProtocolSyntax {
+ public override ReadOnlySpan<byte> EndOfCommandLine => SkStack.CRLFSpan;
+ public override bool ExpectStatusLine => true;
+ public override ReadOnlySpan<byte> EndOfStatusLine => SkStack.CRLFSpan;
+ }
+
+ internal static SkStackProtocolSyntax SKSENDTO { get; } = new SKSENDTOSyntax();
+
+ private sealed class SKSENDTOSyntax : SkStackProtocolSyntax {
+ public override ReadOnlySpan<byte> EndOfCommandLine => ReadOnlySpan<byte>.Empty;
+ public override ReadOnlySpan<byte> EndOfEchobackLine => SkStack.CRLFSpan;
+ public override bool ExpectStatusLine => true;
+ public override ReadOnlySpan<byte> EndOfStatusLine => SkStack.CRLFSpan;
+ }
+
+ internal static SkStackProtocolSyntax SKLL64 { get; } = new SKLL64Syntax();
+
+ private sealed class SKLL64Syntax : SkStackProtocolSyntax {
+ public override ReadOnlySpan<byte> EndOfCommandLine => SkStack.CRLFSpan;
+ public override bool ExpectStatusLine => false;
+ public override ReadOnlySpan<byte> EndOfStatusLine => SkStack.CRLFSpan;
+ }
+
+ protected SkStackProtocolSyntax()
+ {
+ }
+
+ public abstract ReadOnlySpan<byte> EndOfCommandLine { get; }
+ public virtual ReadOnlySpan<byte> EndOfEchobackLine => EndOfCommandLine;
+ public abstract bool ExpectStatusLine { get; }
+ public abstract ReadOnlySpan<byte> EndOfStatusLine { get; }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackResponseStatusCodes.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackResponseStatusCodes.cs
new file mode 100644
index 0000000..7f357ce
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackResponseStatusCodes.cs
@@ -0,0 +1,12 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+internal static class SkStackResponseStatusCodes {
+ public static ReadOnlySpan<byte> OK => "OK"u8;
+
+ public static ReadOnlySpan<byte> FAIL => "FAIL"u8;
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackSequenceParser.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackSequenceParser.cs
new file mode 100644
index 0000000..7ebd729
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackSequenceParser.cs
@@ -0,0 +1,8 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+public delegate TResult SkStackSequenceParser<TResult>(
+ ISkStackSequenceParserContext context
+);
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackTokenParser.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackTokenParser.cs
new file mode 100644
index 0000000..bb9a540
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackTokenParser.cs
@@ -0,0 +1,580 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.Buffers;
+using System.Buffers.Binary;
+#if NULL_STATE_STATIC_ANALYSIS_ATTRIBUTES
+using System.Diagnostics.CodeAnalysis;
+#endif
+using System.Net;
+using System.Net.NetworkInformation;
+
+using Smdn.Formats;
+using Smdn.Text.Unicode.ControlPictures;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+public static class SkStackTokenParser {
+ private delegate (bool Success, TResult Result) TryConvertTokenFunc<TArg, TResult>(ReadOnlySequence<byte> token, TArg arg);
+
+ private readonly struct None { }
+
+ private static OperationStatus TryExpectToken(
+ ref SequenceReader<byte> reader,
+ ReadOnlySpan<byte> expectedToken,
+ bool throwIfUnexpected
+ )
+ => TryExpectOrConvertToken<None, None>(
+ reader: ref reader,
+ length:
+#if DEBUG
+ expectedToken.Length == 0 ? throw new InvalidOperationException("expected token must be non-empty string") : expectedToken.Length,
+#else
+ expectedToken.Length,
+#endif
+ throwIfUnexpected: throwIfUnexpected,
+ expectedToken: expectedToken,
+ arg: default, // no argument
+ tryConvert: default, // no argument
+ out _ // discard
+ );
+
+ private static OperationStatus TryConvertToken<TArg, TResult>(
+ ref SequenceReader<byte> reader,
+ int length,
+ bool throwIfUnexpected,
+ TArg arg,
+ TryConvertTokenFunc<TArg, TResult>? tryConvert,
+ out TResult? result
+ )
+ => TryExpectOrConvertToken(
+ reader: ref reader,
+ length: length,
+ throwIfUnexpected: throwIfUnexpected,
+ expectedToken: default, // no argument
+ arg: arg,
+ tryConvert: tryConvert,
+ out result
+ );
+
+ private static OperationStatus TryExpectOrConvertToken<TArg, TResult>(
+ ref SequenceReader<byte> reader,
+ int length,
+ bool throwIfUnexpected,
+ ReadOnlySpan<byte> expectedToken,
+ TArg arg,
+ TryConvertTokenFunc<TArg, TResult>? tryConvert,
+ out TResult? result
+ )
+ {
+ result = default;
+
+ // if fixed length
+ if (0 < length && reader.Remaining < length)
+ return OperationStatus.NeedMoreData;
+
+ var readerEOL = reader;
+ var readerSP = reader;
+ var foundTokenDelimitByEOL = readerEOL.TryReadTo(out ReadOnlySequence<byte> tokenDelimitByEOL, SkStack.CRLFSpan);
+ var foundTokenDelimitBySP = readerSP.TryReadTo(out ReadOnlySequence<byte> tokenDelimitBySP, SkStack.SP);
+
+ if (foundTokenDelimitByEOL && foundTokenDelimitBySP) {
+ // select shorter one
+ if (tokenDelimitByEOL.Length < tokenDelimitBySP.Length) {
+ foundTokenDelimitByEOL = true;
+ foundTokenDelimitBySP = false;
+ }
+ else {
+ foundTokenDelimitBySP = true;
+ foundTokenDelimitByEOL = false;
+ }
+ }
+
+ if (!(foundTokenDelimitByEOL || foundTokenDelimitBySP))
+ return OperationStatus.NeedMoreData;
+
+ ReadOnlySequence<byte> token;
+ var consumedReader = reader;
+
+ if (foundTokenDelimitByEOL) {
+ token = tokenDelimitByEOL;
+ consumedReader.Advance(tokenDelimitByEOL.Length); // keep EOL
+ }
+ else {
+ token = tokenDelimitBySP;
+ consumedReader.Advance(tokenDelimitBySP.Length + 1); // consume SP
+ }
+
+ if (0 < length && token.Length != length) {
+ return throwIfUnexpected
+ ? throw SkStackUnexpectedResponseException.CreateInvalidToken(
+ token,
+ $"invalid length of token (expected {length} but was {token.Length})"
+ )
+ : OperationStatus.InvalidData;
+ }
+
+ if (!expectedToken.IsEmpty) {
+ if (IsExpectedToken(token, expectedToken, throwIfUnexpected)) {
+ reader = consumedReader;
+
+ return OperationStatus.Done;
+ }
+
+ return throwIfUnexpected
+ ? throw SkStackUnexpectedResponseException.CreateInvalidToken(
+ token: token,
+ extraMessage: $"expected: '{expectedToken.ToControlCharsPicturizedString()}'"
+ )
+ : OperationStatus.InvalidData;
+ }
+
+ if (tryConvert is null)
+ throw new InvalidOperationException($"It is necessary to specify a valid value for either the parameter {nameof(expectedToken)} or {nameof(tryConvert)}.");
+
+ bool converted;
+
+ try {
+ (converted, result) = tryConvert(token, arg);
+ }
+ catch (SkStackUnexpectedResponseException) {
+ throw; // rethrow
+ }
+ catch (Exception ex) {
+ result = default;
+
+ throw SkStackUnexpectedResponseException.CreateInvalidFormat(
+ token: token,
+ innerException: ex
+ );
+ }
+
+ if (!converted)
+ return OperationStatus.InvalidData;
+
+ reader = consumedReader;
+
+ return OperationStatus.Done;
+
+ static bool IsExpectedToken(ReadOnlySequence<byte> tokenSequence, ReadOnlySpan<byte> expectedToken, bool throwIfUnexpected)
+ {
+ var reader = new SequenceReader<byte>(tokenSequence);
+
+ for (var i = 0; i < expectedToken.Length; i++) {
+ if (reader.TryRead(out var b) && b != expectedToken[i]) {
+ return throwIfUnexpected
+ ? throw SkStackUnexpectedResponseException.CreateInvalidToken(tokenSequence, "unexpected token")
+ : false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ public static bool Expect<TValue>(
+ ref SequenceReader<byte> reader,
+ int length,
+ Converter<ReadOnlySequence<byte>, TValue> converter,
+#if NULL_STATE_STATIC_ANALYSIS_ATTRIBUTES
+ [NotNullWhen(true)]
+#endif
+ out TValue? value
+ )
+ => OperationStatus.Done == TryConvertToken(
+ reader: ref reader,
+ length: length,
+ throwIfUnexpected: true,
+ arg: converter,
+ tryConvert: static (token, conv) => (true, conv(token)),
+ result: out value
+ );
+
+ public static OperationStatus TryExpectToken(
+ ref SequenceReader<byte> reader,
+ ReadOnlySpan<byte> expectedToken
+ )
+ => TryExpectToken(
+ reader: ref reader,
+ expectedToken: expectedToken,
+ throwIfUnexpected: false
+ );
+
+ public static bool ExpectToken(
+ ref SequenceReader<byte> reader,
+ ReadOnlySpan<byte> expectedToken
+ )
+ => OperationStatus.Done == TryExpectToken(
+ reader: ref reader,
+ expectedToken: expectedToken,
+ throwIfUnexpected: true
+ );
+
+ public static bool ExpectEndOfLine(
+ ref SequenceReader<byte> reader
+ )
+ => ExpectSequenceCore(
+ reader: ref reader,
+ expectedSequence: SkStack.CRLFSpan,
+ throwIfUnexpected: true,
+ createUnexpectedExceptionMessage: static _ => $"expected EOL, but not"
+ );
+
+ public static bool ExpectSequence(
+ ref SequenceReader<byte> reader,
+ ReadOnlySpan<byte> expectedSequence
+ )
+ => ExpectSequenceCore(
+ reader: ref reader,
+ expectedSequence: expectedSequence,
+ throwIfUnexpected: true,
+ createUnexpectedExceptionMessage: static seq => $"expected sequence '{seq.ToControlCharsPicturizedString()}', but not"
+ );
+
+ private delegate string CreateUnexpectedSequenceExceptionMessageFunc(ReadOnlySpan<byte> expectedSequence);
+
+ private static bool ExpectSequenceCore(
+ ref SequenceReader<byte> reader,
+ ReadOnlySpan<byte> expectedSequence,
+ bool throwIfUnexpected,
+ CreateUnexpectedSequenceExceptionMessageFunc createUnexpectedExceptionMessage
+ )
+ {
+ if (reader.Remaining < expectedSequence.Length)
+ return false; // incomplete
+
+ var consumedReader = reader;
+
+ if (!consumedReader.IsNext(expectedSequence, advancePast: true)) {
+ return throwIfUnexpected
+ ? throw SkStackUnexpectedResponseException.CreateInvalidToken(
+ consumedReader.GetUnreadSequence().Slice(0, expectedSequence.Length),
+ createUnexpectedExceptionMessage(expectedSequence)
+ )
+ : false;
+ }
+
+ reader = consumedReader;
+
+ return true;
+ }
+
+ public static bool ExpectCharArray(
+ ref SequenceReader<byte> reader,
+ out ReadOnlyMemory<byte> value
+ )
+ => Expect(
+ reader: ref reader,
+ length: 0,
+ converter: static seq => seq.ToArray().AsMemory(),
+ value: out value
+ );
+
+ public static bool ExpectCharArray(
+ ref SequenceReader<byte> reader,
+#if NULL_STATE_STATIC_ANALYSIS_ATTRIBUTES
+ [NotNullWhen(true)]
+#endif
+ out string? value
+ )
+ => Expect(
+ reader: ref reader,
+ length: 0,
+ converter: SkStack.GetString,
+ value: out value
+ );
+
+ [CLSCompliant(false)]
+ public static bool ExpectDecimalNumber(
+ ref SequenceReader<byte> reader,
+ out uint value
+ )
+ => Expect(ref reader, 0, ToDecimalNumber, out value);
+
+ [CLSCompliant(false)]
+ public static bool ExpectDecimalNumber(
+ ref SequenceReader<byte> reader,
+ int length,
+ out uint value
+ )
+ => Expect(ref reader, length, ToDecimalNumber, out value);
+
+ public static bool ExpectUINT8(
+ ref SequenceReader<byte> reader,
+ out byte value
+ )
+ => Expect(ref reader, length: 1 * 2, ToUINT8, out value);
+
+ [CLSCompliant(false)]
+ public static bool ExpectUINT16(
+ ref SequenceReader<byte> reader,
+ out ushort value
+ )
+ => Expect(ref reader, length: 2 * 2, ToUINT16, out value);
+
+ [CLSCompliant(false)]
+ public static bool ExpectUINT32(
+ ref SequenceReader<byte> reader,
+ out uint value
+ )
+ => Expect(ref reader, length: 4 * 2, ToUINT32, out value);
+
+ [CLSCompliant(false)]
+ public static bool ExpectUINT64(
+ ref SequenceReader<byte> reader,
+ out ulong value
+ )
+ => Expect(ref reader, length: 8 * 2, ToUINT64, out value);
+
+ public static bool ExpectBinary(
+ ref SequenceReader<byte> reader,
+ out bool value
+ )
+ => Expect(ref reader, length: 1, ToBinary, out value);
+
+ private static bool ExpectUINT8Array<TValue>(
+ ref SequenceReader<byte> reader,
+ int length,
+ Converter<Memory<byte>, TValue> converter,
+#if NULL_STATE_STATIC_ANALYSIS_ATTRIBUTES
+ [NotNullWhen(true)]
+#endif
+ out TValue? value
+ )
+ {
+ value = default;
+
+ byte[]? buffer = null;
+
+ try {
+ var lengthOfUINT8Array = length * 2;
+
+ buffer = ArrayPool<byte>.Shared.Rent(lengthOfUINT8Array);
+
+ return OperationStatus.Done == TryConvertToken(
+ reader: ref reader,
+ length: lengthOfUINT8Array,
+ throwIfUnexpected: true,
+ arg: (converter, memory: buffer.AsMemory(0, length)),
+ tryConvert: static (token, arg) => {
+ ToByteSequence(token, arg.memory.Length, arg.memory.Span);
+
+ return (true, arg.converter(arg.memory));
+ },
+ result: out value
+ );
+ }
+ finally {
+ if (buffer is not null)
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
+ }
+
+ public static bool ExpectIPADDR(
+ ref SequenceReader<byte> reader,
+#if NULL_STATE_STATIC_ANALYSIS_ATTRIBUTES
+ [NotNullWhen(true)]
+#endif
+ out IPAddress? value
+ )
+ => Expect(
+ reader: ref reader,
+ length: 39, // "XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX".Length
+ converter: static seq => IPAddress.Parse(SkStack.GetString(seq)),
+ value: out value
+ );
+
+ public static bool ExpectADDR64(
+ ref SequenceReader<byte> reader,
+#if NULL_STATE_STATIC_ANALYSIS_ATTRIBUTES
+ [NotNullWhen(true)]
+#endif
+ out PhysicalAddress? value
+ )
+ => ExpectUINT8Array(
+ reader: ref reader,
+ length: 8,
+ converter: static array => new PhysicalAddress(array.ToArray()), // XXX: cannot pass ReadOnlySpan<byte>
+ value: out value
+ );
+
+ [CLSCompliant(false)]
+ public static bool ExpectADDR16(
+ ref SequenceReader<byte> reader,
+ out ushort value
+ )
+ => ExpectUINT16(
+ reader: ref reader,
+ value: out value
+ );
+
+ public static bool ExpectCHANNEL(
+ ref SequenceReader<byte> reader,
+ out SkStackChannel value
+ )
+ {
+ value = default;
+
+ if (ExpectUINT8(ref reader, out var ch)) {
+ value = SkStackChannel.FindByChannelNumber(ch, nameof(ch));
+ return true;
+ }
+
+ return false;
+ }
+
+ public static void ToByteSequence(ReadOnlySequence<byte> hexTextSequence, int byteSequenceLength, Span<byte> destination)
+ {
+ if ((hexTextSequence.Length & 0x1L) != 0L)
+ throw SkStackUnexpectedResponseException.CreateInvalidToken(hexTextSequence, "HEX ASCII");
+ if (destination.Length < byteSequenceLength)
+ throw new ArgumentException($"buffer too short. expected at least {byteSequenceLength} but was {destination.Length}.", nameof(destination));
+
+ var reader = new SequenceReader<byte>(hexTextSequence);
+
+ Span<byte> hexTextOneByte = stackalloc byte[2];
+
+ for (var index = 0; index < byteSequenceLength; index++) {
+ reader.TryCopyTo(hexTextOneByte);
+
+ if (!Hexadecimal.TryDecode(hexTextOneByte, out var decodedbyte))
+ throw SkStackUnexpectedResponseException.CreateInvalidToken(hexTextOneByte.Slice(0, 1), "HEX ASCII");
+
+ destination[index] = decodedbyte;
+
+ reader.Advance(2);
+ }
+ }
+
+ private static byte ToUINT8(ReadOnlySequence<byte> token)
+ {
+ try {
+ Span<byte> uint8 = stackalloc byte[1];
+
+ ToByteSequence(token, 1, uint8);
+
+ return uint8[0];
+ }
+ catch (SkStackUnexpectedResponseException ex) {
+ throw SkStackUnexpectedResponseException.CreateInvalidToken(token, "UINT8", ex);
+ }
+ }
+
+ private static ushort ToUINT16(ReadOnlySequence<byte> token)
+ {
+ try {
+ Span<byte> uint16 = stackalloc byte[2];
+
+ ToByteSequence(token, 2, uint16);
+
+ return BinaryPrimitives.ReadUInt16BigEndian(uint16);
+ }
+ catch (Exception ex) {
+ throw SkStackUnexpectedResponseException.CreateInvalidToken(token, "UINT16", ex);
+ }
+ }
+
+ private static uint ToUINT32(ReadOnlySequence<byte> token)
+ {
+ try {
+ Span<byte> uint32 = stackalloc byte[4];
+
+ ToByteSequence(token, 4, uint32);
+
+ return BinaryPrimitives.ReadUInt32BigEndian(uint32);
+ }
+ catch (Exception ex) {
+ throw SkStackUnexpectedResponseException.CreateInvalidToken(token, "UINT32", ex);
+ }
+ }
+
+ private static ulong ToUINT64(ReadOnlySequence<byte> token)
+ {
+ try {
+ Span<byte> uint64 = stackalloc byte[8];
+
+ ToByteSequence(token, 8, uint64);
+
+ return BinaryPrimitives.ReadUInt64BigEndian(uint64);
+ }
+ catch (Exception ex) {
+ throw SkStackUnexpectedResponseException.CreateInvalidToken(token, "UINT32", ex);
+ }
+ }
+
+ private static uint ToDecimalNumber(ReadOnlySequence<byte> token)
+ {
+ const int MaxLength = 10; // uint.MaxValue.ToString("D").Length
+
+ if (MaxLength < token.Length)
+ throw SkStackUnexpectedResponseException.CreateInvalidToken(token, "decimal number with max 10 digit");
+
+ var reader = new SequenceReader<byte>(token);
+
+#if SYSTEM_IUTF8SPANPARSABLE
+ Span<byte> str = stackalloc byte[(int)reader.Length];
+
+ _ = reader.TryCopyTo(str);
+#else
+ Span<char> str = stackalloc char[(int)reader.Length];
+
+ for (var i = 0; i < token.Length; i++) {
+ reader.TryRead(out var d);
+ str[i] = (char)d;
+ }
+#endif
+
+ try {
+ return uint.Parse(str, provider: null);
+ }
+ catch (Exception ex) {
+ throw SkStackUnexpectedResponseException.CreateInvalidToken(token, "decimal number", ex);
+ }
+ }
+
+ private static bool ToBinary(ReadOnlySequence<byte> token)
+ {
+ try {
+ return token.FirstSpan[0] switch {
+ (byte)'0' => false,
+ (byte)'1' => true,
+ _ => throw SkStackUnexpectedResponseException.CreateInvalidToken(token, "0 or 1"),
+ };
+ }
+ catch (SkStackUnexpectedResponseException ex) {
+ throw SkStackUnexpectedResponseException.CreateInvalidToken(token, "Binary", ex);
+ }
+ }
+
+ public static bool TryExpectStatusLine(
+ ref SequenceReader<byte> reader,
+ out SkStackResponseStatus status
+ )
+ {
+ status = default;
+
+ var readerOk = reader;
+ var readerFail = reader;
+
+ if (
+ readerOk.IsNext(SkStackResponseStatusCodes.OK, advancePast: true) &&
+ (readerOk.IsNext(SkStack.SP, advancePast: true) || readerOk.IsNext(SkStack.CRLFSpan, advancePast: true))
+ ) {
+ reader = readerOk;
+ status = SkStackResponseStatus.Ok;
+ return true;
+ }
+ else if (
+ readerFail.IsNext(SkStackResponseStatusCodes.FAIL, advancePast: true) &&
+ (readerFail.IsNext(SkStack.SP, advancePast: true) || readerFail.IsNext(SkStack.CRLFSpan, advancePast: true))
+ ) {
+ reader = readerFail;
+ status = SkStackResponseStatus.Fail;
+ return true;
+ }
+ else {
+ return false; // is incomplete or is not status line
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackUdpReceiveEvent.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackUdpReceiveEvent.cs
new file mode 100644
index 0000000..ac760d1
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackUdpReceiveEvent.cs
@@ -0,0 +1,32 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System.Net;
+using System.Net.NetworkInformation;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 4.1. ERXUDP' for detailed specifications.</para>
+/// </remarks>
+internal readonly struct SkStackUdpReceiveEvent {
+ public IPEndPoint RemoteEndPoint { get; }
+ public IPEndPoint LocalEndPoint { get; }
+ public PhysicalAddress RemoteLinkLocalAddress { get; }
+ public bool IsSecured { get; }
+
+ internal SkStackUdpReceiveEvent(
+ IPAddress sender,
+ IPAddress dest,
+ uint rport,
+ uint lport,
+ PhysicalAddress senderlla,
+ bool secured
+ )
+ {
+ RemoteEndPoint = new(sender, (int)rport);
+ LocalEndPoint = new(dest, (int)lport);
+ RemoteLinkLocalAddress = senderlla;
+ IsSecured = secured;
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackUnexpectedResponseException.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackUnexpectedResponseException.cs
new file mode 100644
index 0000000..6a3a8b6
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.Protocol/SkStackUnexpectedResponseException.cs
@@ -0,0 +1,80 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+#pragma warning disable CA1032
+
+using System;
+using System.Buffers;
+
+using Smdn.Text.Unicode.ControlPictures;
+
+namespace Smdn.Net.SkStackIP.Protocol;
+
+/// <summary>
+/// The exception that is thrown when the <see cref="SkStackClient"/> received unexpected response.
+/// </summary>
+/// <seealso cref="SkStackTokenParser"/>
+public class SkStackUnexpectedResponseException : SkStackResponseException {
+ /// <summary>
+ /// Gets the token or text of the response that caused the exception.
+ /// </summary>
+ public string? CausedText { get; }
+
+ private SkStackUnexpectedResponseException(string? causedText, string message, Exception? innerException = null)
+ : base(message, innerException)
+ {
+ CausedText = causedText;
+ }
+
+ internal static SkStackUnexpectedResponseException CreateLackOfExpectedResponseText(Exception? innerException = null)
+ => new(
+ causedText: null,
+ message: "lack of expected response text",
+ innerException: innerException
+ );
+
+ internal static SkStackUnexpectedResponseException CreateInvalidFormat(
+ ReadOnlySequence<byte> token,
+ Exception? innerException = null
+ )
+ => new(
+ causedText: token.ToControlCharsPicturizedString(),
+ message: $"unexpected response format: '{token.ToControlCharsPicturizedString()}'",
+ innerException: innerException
+ );
+
+ internal static SkStackUnexpectedResponseException CreateInvalidToken(
+ ReadOnlySpan<byte> token,
+ string extraMessage,
+ Exception? innerException = null
+ )
+ => new(
+ causedText: token.ToControlCharsPicturizedString(),
+ message: $"unexpected response token: '{token.ToControlCharsPicturizedString()}' ({extraMessage})",
+ innerException: innerException
+ );
+
+ internal static SkStackUnexpectedResponseException CreateInvalidToken(
+ ReadOnlySequence<byte> token,
+ string extraMessage,
+ Exception? innerException = null
+ )
+ => new(
+ causedText: token.ToControlCharsPicturizedString(),
+ message: $"unexpected response token: '{token.ToControlCharsPicturizedString()}' ({extraMessage})",
+ innerException: innerException
+ );
+
+ internal static void ThrowIfUnexpectedSubsequentEventCode(
+ SkStackEventCode subsequentEventCode,
+ SkStackEventCode expectedEventCode
+ )
+ {
+ if (subsequentEventCode != expectedEventCode) {
+ throw new SkStackUnexpectedResponseException(
+ causedText: null,
+ message: $"expected subsequent event code is {expectedEventCode}, but was {subsequentEventCode}",
+ innerException: null
+ );
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.csproj b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.csproj
new file mode 100644
index 0000000..c583c86
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP.csproj
@@ -0,0 +1,80 @@
+<!--
+SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+SPDX-License-Identifier: MIT
+-->
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFrameworks>net8.0;net6.0;netstandard2.1</TargetFrameworks>
+ <VersionPrefix>1.0.0</VersionPrefix>
+ <VersionSuffix></VersionSuffix>
+ <!-- <PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion> -->
+ <Nullable>enable</Nullable>
+ <RootNamespace/> <!-- empty the root namespace so that the namespace is determined only by the directory name, for code style rule IDE0030 -->
+ <NoWarn>CA1848;$(NoWarn)</NoWarn> <!-- CA1848: For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogXxxxx(...)' -->
+ <NoWarn>CS1591;$(NoWarn)</NoWarn> <!-- CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' -->
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ </PropertyGroup>
+
+ <PropertyGroup Label="metadata">
+ <Description>Provides APIs for operating devices that implement Skyley Networks' SKSTACK IP.</Description>
+ <CopyrightYear>2021</CopyrightYear>
+ </PropertyGroup>
+
+ <PropertyGroup Label="package properties">
+ <PackageTags>SKSTACK,SKSTACK-IP,PANA,Route-B,ECHONET,ECHONET-Lite</PackageTags>
+ <GenerateNupkgReadmeFileDependsOnTargets>$(GenerateNupkgReadmeFileDependsOnTargets);GenerateReadmeFileContent</GenerateNupkgReadmeFileDependsOnTargets>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="System.IO.Pipelines" Version="8.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
+ <PackageReference Include="Polly.Core" Version="8.0.0" />
+ <PackageReference Include="Smdn.Fundamental.ControlPicture" Version="[3.0.0.1,4.0.0)" />
+ <PackageReference Include="Smdn.Fundamental.Encoding.Buffer" Version="[3.0.0,4.0.0)" Condition="$(TargetFramework.StartsWith('netstandard'))" />
+ <PackageReference Include="Smdn.Fundamental.PrintableEncoding.Hexadecimal" Version="[3.0.0,4.0.0)" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <!-- Third party notice -->
+ <None
+ Include="$(MSBuildThisFileDirectory)..\..\ThirdPartyNotices.md"
+ Pack="true"
+ PackagePath="ThirdPartyNotices.md"
+ CopyToOutputDirectory="None"
+ />
+ </ItemGroup>
+
+ <Target Name="GenerateReadmeFileContent" DependsOnTargets="ReadReadmeFileNoticeSectionContent">
+ <PropertyGroup>
+ <PackageReadmeFileContent><![CDATA[# $(PackageId) $(PackageVersion)
+`$(PackageId)` is a library that provides APIs for operating devices that implement Skyley Networks' SKSTACK IP.
+
+This library supports to use any `Stream` or `PipeReader`/`PipeWriter` as the communication channel for the SKSTACK IP protocol, so it has the ability to communicate with devices that use other than serial ports, e.g., pseudo devices.
+
+## Getting started
+First, add package [$(PackageId)](https://www.nuget.org/packages/$(PackageId)) and [System.IO.Ports](https://www.nuget.org/packages/System.IO.Ports) to the project file.
+
+```
+dotnet add package $(PackageId)
+dotnet add package System.IO.Ports
+```
+
+Next, open the serial port to which the SKSTACK-IP device is connected using with the `SerialPort` class.
+
+Then, create a `SkStackClient` instance from the `SerialPort.BaseStream` and call the `SkStackClient`'s method to send the command.
+
+```cs
+$([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)..\..\examples\getting-started\Program.cs').TrimEnd())
+```
+
+More examples can be found on the [GitHub repository]($(RepositoryUrl)/tree/main/examples/), including examples of using library features.
+
+## Contributing
+This project welcomes contributions, feedbacks and suggestions. You can contribute to this project by submitting [Issues]($(RepositoryUrl)/issues/new/choose) or [Pull Requests]($(RepositoryUrl)/pulls/) on the [GitHub repository]($(RepositoryUrl)).
+
+## Notice
+$(ReadmeFileNoticeSectionContent)
+]]></PackageReadmeFileContent>
+ </PropertyGroup>
+ </Target>
+</Project>
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackActiveScanOptions.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackActiveScanOptions.cs
new file mode 100644
index 0000000..8ffbb8a
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackActiveScanOptions.cs
@@ -0,0 +1,148 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.NetworkInformation;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>
+/// The type for defining the scan intervals (scan duration factors) and the method that selects discovered PANA Authentication Agents (PAA) in the scan for PAA, by the <c>SKSCAN</c> command.
+/// </summary>
+/// <seealso cref="SkStackClient.ActiveScanAsync" />
+public abstract class SkStackActiveScanOptions : ICloneable {
+ /// <summary>
+ /// Gets the <see cref="SkStackActiveScanOptions"/> which selects the PAA which found at first during the scan.
+ /// The scan starts with a duration factor <c>3</c>. If not found, continue scanning with the following durations factors: <c>4</c>, <c>5</c>, <c>6</c>, <c>6</c>, <c>6</c>.
+ /// If any PAA is not found until the last scan, it will stop the scanning.
+ /// </summary>
+ public static SkStackActiveScanOptions Default { get; } = new DefaultActiveScanOptions();
+
+ private sealed class DefaultActiveScanOptions : SkStackActiveScanOptions {
+ public override SkStackActiveScanOptions Clone() => new DefaultActiveScanOptions();
+
+ internal override bool SelectPanaAuthenticationAgent(SkStackPanDescription desc) => true; // select first one
+
+ internal override IEnumerable<int> YieldScanDurationFactors()
+ {
+ yield return 3;
+ yield return 4;
+ yield return 5;
+ yield return 6;
+ yield return 6;
+ yield return 6;
+ }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="SkStackActiveScanOptions"/> which does not select any PAA and does not perform any scanning.
+ /// </summary>
+ public static SkStackActiveScanOptions Null { get; } = new NullActiveScanOptions();
+
+ private sealed class NullActiveScanOptions : SkStackActiveScanOptions {
+ public override SkStackActiveScanOptions Clone() => new NullActiveScanOptions();
+
+ internal override bool SelectPanaAuthenticationAgent(SkStackPanDescription desc) => false; // select nothing
+
+ internal override IEnumerable<int> YieldScanDurationFactors()
+ => Enumerable.Empty<int>();
+ }
+
+ /// <summary>
+ /// Gets the <see cref="SkStackActiveScanOptions"/> selects the PAA which found at first during the scan.
+ /// The scan starts with a duration factor <c>5</c> and continues scanning infinitely until it finds any PAA.
+ /// </summary>
+ public static SkStackActiveScanOptions ScanUntilFind { get; } = new ScanUntilFindActiveScanOptions();
+
+ private sealed class ScanUntilFindActiveScanOptions : SkStackActiveScanOptions {
+ public override SkStackActiveScanOptions Clone() => new ScanUntilFindActiveScanOptions();
+
+ internal override bool SelectPanaAuthenticationAgent(SkStackPanDescription desc) => true; // select first one
+
+ internal override IEnumerable<int> YieldScanDurationFactors()
+ {
+ for (; ; )
+ yield return 5;
+ }
+ }
+
+ /// <summary>
+ /// Creates the <see cref="SkStackActiveScanOptions"/> with the custom selection method and duration factors.
+ /// </summary>
+ /// <param name="scanDurationGenerator">A collection or iterator that defines the scan durations.</param>
+ /// <param name="paaSelector">
+ /// A callback to select the target PAA from the PAAs found during the scan.
+ /// If <see langword="null"/>, selects the PAA which found at first during the scan.
+ /// </param>
+ public static SkStackActiveScanOptions Create(
+ IEnumerable<int> scanDurationGenerator,
+ Predicate<SkStackPanDescription>? paaSelector = null
+ )
+ => new UserDefinedActiveScanOptions(
+ paaSelector: paaSelector,
+ scanDurationGenerator: scanDurationGenerator
+ );
+
+ private sealed class UserDefinedActiveScanOptions : SkStackActiveScanOptions {
+ private readonly Predicate<SkStackPanDescription>? paaSelector;
+ private readonly IEnumerable<int> scanDurationGenerator;
+
+ public UserDefinedActiveScanOptions(
+ Predicate<SkStackPanDescription>? paaSelector,
+ IEnumerable<int> scanDurationGenerator
+ )
+ {
+ this.paaSelector = paaSelector;
+ this.scanDurationGenerator = scanDurationGenerator ?? throw new ArgumentNullException(nameof(scanDurationGenerator));
+ }
+
+ public override SkStackActiveScanOptions Clone() => new UserDefinedActiveScanOptions(paaSelector, scanDurationGenerator.ToArray());
+ internal override bool SelectPanaAuthenticationAgent(SkStackPanDescription desc) => paaSelector?.Invoke(desc) ?? true;
+ internal override IEnumerable<int> YieldScanDurationFactors() => scanDurationGenerator;
+ }
+
+ /// <summary>
+ /// Creates the <see cref="SkStackActiveScanOptions"/> with the custom selection method and duration factors.
+ /// </summary>
+ /// <param name="scanDurationGenerator">A collection or iterator that defines the scan durations.</param>
+ /// <param name="paaMacAddress">
+ /// A <see cref="PhysicalAddress"/> of the target PAA. This method selects the first PAA found during the scan that matches this <see cref="PhysicalAddress"/>.
+ /// </param>
+ public static SkStackActiveScanOptions Create(
+ IEnumerable<int> scanDurationGenerator,
+ PhysicalAddress paaMacAddress
+ )
+ => new FindByMacAddressActiveScanOptions(
+ paaMacAddress: paaMacAddress,
+ scanDurationGenerator: scanDurationGenerator
+ );
+
+ private sealed class FindByMacAddressActiveScanOptions : SkStackActiveScanOptions {
+ private readonly PhysicalAddress paaMacAddress;
+ private readonly IEnumerable<int> scanDurationGenerator;
+
+ public FindByMacAddressActiveScanOptions(
+ PhysicalAddress paaMacAddress,
+ IEnumerable<int> scanDurationGenerator
+ )
+ {
+ this.paaMacAddress = paaMacAddress;
+ this.scanDurationGenerator = scanDurationGenerator ?? throw new ArgumentNullException(nameof(scanDurationGenerator));
+ }
+
+ public override SkStackActiveScanOptions Clone() => new FindByMacAddressActiveScanOptions(paaMacAddress, scanDurationGenerator.ToArray());
+ internal override bool SelectPanaAuthenticationAgent(SkStackPanDescription desc) => desc.MacAddress.Equals(paaMacAddress);
+ internal override IEnumerable<int> YieldScanDurationFactors() => scanDurationGenerator;
+ }
+
+ /*
+ * instance members
+ */
+ // TODO: public IProgress<int> Progress { get; set; }
+ public abstract SkStackActiveScanOptions Clone();
+ object ICloneable.Clone() => Clone();
+ internal abstract bool SelectPanaAuthenticationAgent(SkStackPanDescription desc);
+ internal abstract IEnumerable<int> YieldScanDurationFactors();
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackChannel.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackChannel.cs
new file mode 100644
index 0000000..a870f04
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackChannel.cs
@@ -0,0 +1,127 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+#pragma warning disable CA1036
+
+using System;
+using System.Collections.Generic;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 6. 周波数とチャネル番号' for detailed specifications.</para>
+/// </remarks>
+public readonly struct SkStackChannel : IEquatable<SkStackChannel>, IComparable<SkStackChannel> {
+ public static readonly IReadOnlyDictionary<int, SkStackChannel> Channels = new Dictionary<int, SkStackChannel> {
+ { 33, new(channelNumber: 33, frequencyMHz: 922.5m) },
+ { 34, new(channelNumber: 34, frequencyMHz: 922.7m) },
+ { 35, new(channelNumber: 35, frequencyMHz: 922.9m) },
+ { 36, new(channelNumber: 36, frequencyMHz: 923.1m) },
+ { 37, new(channelNumber: 37, frequencyMHz: 923.3m) },
+ { 38, new(channelNumber: 38, frequencyMHz: 923.5m) },
+ { 39, new(channelNumber: 39, frequencyMHz: 923.7m) },
+ { 40, new(channelNumber: 40, frequencyMHz: 923.9m) },
+ { 41, new(channelNumber: 41, frequencyMHz: 924.1m) },
+ { 42, new(channelNumber: 42, frequencyMHz: 924.3m) },
+ { 43, new(channelNumber: 43, frequencyMHz: 924.5m) },
+ { 44, new(channelNumber: 44, frequencyMHz: 924.7m) },
+ { 45, new(channelNumber: 45, frequencyMHz: 924.9m) },
+ { 46, new(channelNumber: 46, frequencyMHz: 925.1m) },
+ { 47, new(channelNumber: 47, frequencyMHz: 925.3m) },
+ { 48, new(channelNumber: 48, frequencyMHz: 925.5m) },
+ { 49, new(channelNumber: 49, frequencyMHz: 925.7m) },
+ { 50, new(channelNumber: 50, frequencyMHz: 925.9m) },
+ { 51, new(channelNumber: 51, frequencyMHz: 926.1m) },
+ { 52, new(channelNumber: 52, frequencyMHz: 926.3m) },
+ { 53, new(channelNumber: 53, frequencyMHz: 926.5m) },
+ { 54, new(channelNumber: 54, frequencyMHz: 926.7m) },
+ { 55, new(channelNumber: 55, frequencyMHz: 926.9m) },
+ { 56, new(channelNumber: 56, frequencyMHz: 927.1m) },
+ { 57, new(channelNumber: 57, frequencyMHz: 927.3m) },
+ { 58, new(channelNumber: 58, frequencyMHz: 927.5m) },
+ { 59, new(channelNumber: 59, frequencyMHz: 927.7m) },
+ { 60, new(channelNumber: 60, frequencyMHz: 927.9m) },
+ };
+
+ public static readonly SkStackChannel Empty;
+
+ public static SkStackChannel Channel33 => Channels[33];
+ public static SkStackChannel Channel34 => Channels[34];
+ public static SkStackChannel Channel35 => Channels[35];
+ public static SkStackChannel Channel36 => Channels[36];
+ public static SkStackChannel Channel37 => Channels[37];
+ public static SkStackChannel Channel38 => Channels[38];
+ public static SkStackChannel Channel39 => Channels[39];
+ public static SkStackChannel Channel40 => Channels[40];
+ public static SkStackChannel Channel41 => Channels[41];
+ public static SkStackChannel Channel42 => Channels[42];
+ public static SkStackChannel Channel43 => Channels[43];
+ public static SkStackChannel Channel44 => Channels[44];
+ public static SkStackChannel Channel45 => Channels[45];
+ public static SkStackChannel Channel46 => Channels[46];
+ public static SkStackChannel Channel47 => Channels[47];
+ public static SkStackChannel Channel48 => Channels[48];
+ public static SkStackChannel Channel49 => Channels[49];
+ public static SkStackChannel Channel50 => Channels[50];
+ public static SkStackChannel Channel51 => Channels[51];
+ public static SkStackChannel Channel52 => Channels[52];
+ public static SkStackChannel Channel53 => Channels[53];
+ public static SkStackChannel Channel54 => Channels[54];
+ public static SkStackChannel Channel55 => Channels[55];
+ public static SkStackChannel Channel56 => Channels[56];
+ public static SkStackChannel Channel57 => Channels[57];
+ public static SkStackChannel Channel58 => Channels[58];
+ public static SkStackChannel Channel59 => Channels[59];
+ public static SkStackChannel Channel60 => Channels[60];
+
+ internal static SkStackChannel FindByChannelNumber(int channelNumber, string? paramNameOfChannelNumber = null)
+ => Channels.TryGetValue(channelNumber, out var channel)
+ ? channel
+ : throw new ArgumentOutOfRangeException(
+ paramName: paramNameOfChannelNumber ?? nameof(channelNumber),
+ actualValue: channelNumber,
+ message: "undefined channel"
+ );
+
+ /*
+ * instance members
+ */
+ public int ChannelNumber { get; }
+ public decimal FrequencyMHz { get; }
+ internal byte RegisterS02Value => (byte)ChannelNumber;
+
+ public bool IsEmpty => Equals(Empty);
+
+ private SkStackChannel(int channelNumber, decimal frequencyMHz)
+ {
+ ChannelNumber = channelNumber;
+ FrequencyMHz = frequencyMHz;
+ }
+
+ public override bool Equals(object? obj)
+ => obj switch {
+ SkStackChannel channel => Equals(channel),
+ _ => false,
+ };
+
+ public bool Equals(SkStackChannel other)
+ => ChannelNumber == other.ChannelNumber;
+
+ public static bool operator ==(SkStackChannel x, SkStackChannel y) => x.Equals(y);
+ public static bool operator !=(SkStackChannel x, SkStackChannel y) => !x.Equals(y);
+
+ int IComparable<SkStackChannel>.CompareTo(SkStackChannel other)
+ => ChannelNumber.CompareTo(other.ChannelNumber);
+
+#if false
+ public static bool operator < (SkStackChannel x, SkStackChannel y) => x.ChannelNumber < y.ChannelNumber;
+ public static bool operator <= (SkStackChannel x, SkStackChannel y) => x.ChannelNumber <= y.ChannelNumber;
+ public static bool operator > (SkStackChannel x, SkStackChannel y) => x.ChannelNumber > y.ChannelNumber;
+ public static bool operator >= (SkStackChannel x, SkStackChannel y) => x.ChannelNumber >= y.ChannelNumber;
+#endif
+
+ public override int GetHashCode()
+ => ChannelNumber.GetHashCode();
+
+ public override string ToString()
+ => $"{ChannelNumber}ch ({nameof(SkStackRegister.S02)}=0x{ChannelNumber:X2}, {FrequencyMHz} MHz)";
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKADDNBR.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKADDNBR.cs
new file mode 100644
index 0000000..8086537
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKADDNBR.cs
@@ -0,0 +1,50 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ /// <summary>
+ /// <para>Sends a command <c>SKADDNBR</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.18. SKADDNBR' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse> SendSKADDNBRAsync(
+ IPAddress ipv6Address,
+ PhysicalAddress macAddress,
+ CancellationToken cancellationToken = default
+ )
+ {
+ const int LengthOfAddr64 = 8;
+
+ if (ipv6Address is null)
+ throw new ArgumentNullException(nameof(ipv6Address));
+ if (ipv6Address.AddressFamily != AddressFamily.InterNetworkV6)
+ throw new ArgumentException($"`{nameof(ipv6Address)}.{nameof(IPAddress.AddressFamily)}` must be {nameof(AddressFamily.InterNetworkV6)}");
+ if (macAddress is null)
+ throw new ArgumentNullException(nameof(macAddress));
+ if (macAddress.GetAddressBytes().Length != LengthOfAddr64)
+ throw new ArgumentException($"`{nameof(macAddress)}` must be address that is {LengthOfAddr64} bytes length");
+
+ return SendCommandAsync(
+ command: SkStackCommandNames.SKADDNBR,
+ writeArguments: writer => {
+ writer.WriteTokenIPADDR(ipv6Address);
+ writer.WriteTokenADDR64(macAddress);
+ },
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKDSLEEP.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKDSLEEP.cs
new file mode 100644
index 0000000..804706c
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKDSLEEP.cs
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ /// <summary>
+ /// <para>Sends a command <c>SKDSLEEP</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.27. SKDSLEEP' for detailed specifications.</para>
+ /// </remarks>
+ /// <seealso cref="Slept"/>
+ /// <seealso cref="WokeUp"/>
+ public ValueTask<SkStackResponse> SendSKDSLEEPAsync(
+ bool waitUntilWakeUp = false,
+ CancellationToken cancellationToken = default
+ )
+ => SendCommandAsync(
+ command: SkStackCommandNames.SKDSLEEP,
+ writeArguments: null,
+ commandEventHandler: new SKDSLEEPEventHandler(this, waitUntilWakeUp),
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+
+ private class SKDSLEEPEventHandler : SkStackEventHandlerBase {
+ private readonly SkStackClient owner;
+ private readonly bool waitUntilWakeUp;
+
+ public SKDSLEEPEventHandler(SkStackClient owner, bool waitUntilWakeUp)
+ {
+ this.owner = owner;
+ this.waitUntilWakeUp = waitUntilWakeUp;
+ }
+
+ public override bool DoContinueHandlingEvents(SkStackResponseStatus status)
+ {
+ var sleepStartedSuccessfully = status == SkStackResponseStatus.Ok;
+
+ if (sleepStartedSuccessfully)
+ owner.RaiseEventSlept();
+
+ if (waitUntilWakeUp)
+ return sleepStartedSuccessfully; // do continue handling events if sleep started (wait until `EVENT CO`)
+ else
+ return false; // do not continue handling events
+ }
+
+ public override bool TryProcessEvent(SkStackEvent ev)
+ => ev.Number == SkStackEventNumber.WakeupSignalReceived;
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKINFO.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKINFO.cs
new file mode 100644
index 0000000..9ca167d
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKINFO.cs
@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ /// <summary>
+ /// <para>Sends a command <c>SKINFO</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.2. SKINFO' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse<(
+ IPAddress LinkLocalAddress,
+ PhysicalAddress MacAddress,
+ SkStackChannel Channel,
+ int PanId,
+ int Addr16
+ )>>
+ SendSKINFOAsync(
+ CancellationToken cancellationToken = default
+ )
+ => SendCommandAsync(
+ command: SkStackCommandNames.SKINFO,
+ writeArguments: null,
+ parseResponsePayload: static context => {
+ var reader = context.CreateReader();
+
+ if (
+ SkStackTokenParser.ExpectToken(ref reader, "EINFO"u8) &&
+ SkStackTokenParser.ExpectIPADDR(ref reader, out var linkLocalAddress) &&
+ SkStackTokenParser.ExpectADDR64(ref reader, out var macAddress) &&
+ SkStackTokenParser.ExpectCHANNEL(ref reader, out var channel) &&
+ SkStackTokenParser.ExpectUINT16(ref reader, out var panId) &&
+ SkStackTokenParser.ExpectADDR16(ref reader, out var addr16) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ context.Complete(reader);
+ return (
+ linkLocalAddress,
+ macAddress,
+ channel,
+ (int)panId,
+ (int)addr16
+ );
+ }
+
+ context.SetAsIncomplete();
+ return default;
+ },
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKJOIN_SKREJOIN.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKJOIN_SKREJOIN.cs
new file mode 100644
index 0000000..b9fea30
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKJOIN_SKREJOIN.cs
@@ -0,0 +1,118 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ /// <summary>
+ /// <para>Sends a command <c>SKJOIN</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.4. SKJOIN' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse> SendSKJOINAsync(
+ IPAddress ipv6address,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (ipv6address is null)
+ throw new ArgumentNullException(nameof(ipv6address));
+ if (ipv6address.AddressFamily != AddressFamily.InterNetworkV6)
+ throw new ArgumentException($"`{nameof(ipv6address)}.{nameof(IPAddress.AddressFamily)}` must be {nameof(AddressFamily.InterNetworkV6)}");
+
+ return SKJOIN(ipv6address, cancellationToken);
+
+ async ValueTask<SkStackResponse> SKJOIN(IPAddress addr, CancellationToken ct)
+ {
+ var (response, _) = await SKJOIN_SKREJOIN(SkStackCommandNames.SKJOIN, addr, ct).ConfigureAwait(false);
+
+ return response;
+ }
+ }
+
+ /// <summary>
+ /// <para>Sends a command <c>SKREJOIN</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.5. SKREJOIN' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<(
+ SkStackResponse Response,
+ IPAddress Address
+ )> SendSKREJOINAsync(
+ CancellationToken cancellationToken = default
+ )
+ => SKJOIN_SKREJOIN(SkStackCommandNames.SKREJOIN, ipv6address: null, cancellationToken);
+
+ private async ValueTask<(
+ SkStackResponse Response,
+ IPAddress Address
+ )>
+ SKJOIN_SKREJOIN(
+ ReadOnlyMemory<byte> command,
+ IPAddress? ipv6address,
+ CancellationToken cancellationToken
+ )
+ {
+ var eventHandler = new SKJOINEventHandler();
+ var resp = await SendCommandAsync(
+ command: command,
+ writeArguments: writer => {
+ if (ipv6address is not null)
+ writer.WriteTokenIPADDR(ipv6address);
+ },
+ commandEventHandler: eventHandler,
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ eventHandler.ThrowIfEstablishmentError();
+
+#if DEBUG
+ if (eventHandler.Address is null)
+ throw new InvalidOperationException($"{eventHandler.Address} has not been set");
+#endif
+
+ return (resp, eventHandler.Address!);
+ }
+
+ private class SKJOINEventHandler : SkStackEventHandlerBase {
+ public bool HasAddressSet { get; private set; }
+ public IPAddress? Address { get; private set; }
+
+ private SkStackEventNumber eventNumber;
+
+ public void ThrowIfEstablishmentError()
+ {
+ if (eventNumber != SkStackEventNumber.PanaSessionEstablishmentCompleted)
+ throw new SkStackPanaSessionEstablishmentException($"PANA session establishment failed. (0x{eventNumber:X})", Address!, eventNumber);
+ }
+
+ public override bool TryProcessEvent(SkStackEvent ev)
+ {
+ switch (ev.Number) {
+ case SkStackEventNumber.PanaSessionEstablishmentCompleted:
+ case SkStackEventNumber.PanaSessionEstablishmentError:
+ eventNumber = ev.Number;
+#if DEBUG
+ if (!ev.HasSenderAddress)
+ throw new InvalidOperationException($"{nameof(ev.SenderAddress)} must not be null");
+#endif
+ Address = ev.SenderAddress!;
+ return true;
+
+ default:
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKSCAN.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKSCAN.cs
new file mode 100644
index 0000000..fe72208
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKSCAN.cs
@@ -0,0 +1,340 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.Collections.Generic;
+#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE
+using System.Diagnostics.CodeAnalysis;
+#endif
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ private abstract class SKSCANEventHandler<TScanResult> : SkStackEventHandlerBase {
+ public bool HasScanResultSet { get; private set; }
+
+#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE
+ [MemberNotNullWhen(true, nameof(HasScanResultSet))]
+#endif
+ public TScanResult? ScanResult { get; private set; }
+
+ public abstract override bool TryProcessEvent(SkStackEvent ev);
+ public abstract override void ProcessSubsequentEvent(ISkStackSequenceParserContext context);
+
+ public void SetScanResult(TScanResult scanResult)
+ {
+ HasScanResultSet = true;
+ ScanResult = scanResult;
+ }
+ }
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSCAN 0</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.9. SKSCAN' for detailed specifications.</para>
+ /// </remarks>
+ [CLSCompliant(false)]
+ public ValueTask<(
+ SkStackResponse Response,
+ IReadOnlyDictionary<SkStackChannel, decimal> ScanResult
+ )> SendSKSCANEnergyDetectScanAsync(
+ TimeSpan duration = default,
+ uint channelMask = SKSCANDefaultChannelMask,
+ CancellationToken cancellationToken = default
+ )
+ => SendSKSCANAsyncCore(
+ mode: SKSCANMode.EnergyDetectScan,
+ channelMask: channelMask,
+ durationFactor: TranslateToSKSCANDurationFactorOrThrowIfOutOfRange(duration == default ? SKSCANDefaultDuration : duration, nameof(duration)),
+ commandEventHandler: new SKSCANEnergyDetectScanEventHandler(),
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSCAN 0</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.9. SKSCAN' for detailed specifications.</para>
+ /// </remarks>
+ [CLSCompliant(false)]
+ public ValueTask<(
+ SkStackResponse Response,
+ IReadOnlyDictionary<SkStackChannel, decimal> ScanResult
+ )> SendSKSCANEnergyDetectScanAsync(
+ int durationFactor,
+ uint channelMask = SKSCANDefaultChannelMask,
+ CancellationToken cancellationToken = default
+ )
+ => SendSKSCANAsyncCore(
+ mode: SKSCANMode.EnergyDetectScan,
+ channelMask: channelMask,
+ durationFactor: ThrowIfDurationFactorOutOfRange(durationFactor, nameof(durationFactor)),
+ commandEventHandler: new SKSCANEnergyDetectScanEventHandler(),
+ cancellationToken: cancellationToken
+ );
+
+ private class SKSCANEnergyDetectScanEventHandler : SKSCANEventHandler<IReadOnlyDictionary<SkStackChannel, decimal>> {
+ public override bool TryProcessEvent(SkStackEvent ev)
+ {
+ if (ev.Number == SkStackEventNumber.EnergyDetectScanCompleted)
+ return false; // process subsequent event
+
+ return false;
+ }
+
+ public override void ProcessSubsequentEvent(ISkStackSequenceParserContext context)
+ {
+ var reader = context.CreateReader(); // retain current buffer
+
+ if (SkStackEventParser.ExpectEEDSCAN(context, out var result)) {
+ SetScanResult(result);
+ context.Complete();
+ }
+ else {
+ context.SetAsIncomplete(reader); // revert buffer
+ }
+ }
+ }
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSCAN 2</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.9. SKSCAN' for detailed specifications.</para>
+ /// </remarks>
+ [CLSCompliant(false)]
+ public ValueTask<(
+ SkStackResponse Response,
+ IReadOnlyList<SkStackPanDescription> PanDescriptions
+ )> SendSKSCANActiveScanPairAsync(
+ TimeSpan duration = default,
+ uint channelMask = SKSCANDefaultChannelMask,
+ CancellationToken cancellationToken = default
+ )
+ => SendSKSCANAsyncCore(
+ mode: SKSCANMode.ActiveScanPair,
+ channelMask: channelMask,
+ durationFactor: TranslateToSKSCANDurationFactorOrThrowIfOutOfRange(duration == default ? SKSCANDefaultDuration : duration, nameof(duration)),
+ commandEventHandler: new SKSCANActiveScanEventHandler(expectPairingId: true),
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSCAN 2</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.9. SKSCAN' for detailed specifications.</para>
+ /// </remarks>
+ [CLSCompliant(false)]
+ public ValueTask<(
+ SkStackResponse Response,
+ IReadOnlyList<SkStackPanDescription> PanDescriptions
+ )> SendSKSCANActiveScanPairAsync(
+ int durationFactor,
+ uint channelMask = SKSCANDefaultChannelMask,
+ CancellationToken cancellationToken = default
+ )
+ => SendSKSCANAsyncCore(
+ mode: SKSCANMode.ActiveScanPair,
+ channelMask: channelMask,
+ durationFactor: ThrowIfDurationFactorOutOfRange(durationFactor, nameof(durationFactor)),
+ commandEventHandler: new SKSCANActiveScanEventHandler(expectPairingId: true),
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSCAN 3</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.9. SKSCAN' for detailed specifications.</para>
+ /// </remarks>
+ [CLSCompliant(false)]
+ public ValueTask<(
+ SkStackResponse Response,
+ IReadOnlyList<SkStackPanDescription> PanDescriptions
+ )>
+ SendSKSCANActiveScanAsync(
+ TimeSpan duration = default,
+ uint channelMask = SKSCANDefaultChannelMask,
+ CancellationToken cancellationToken = default
+ )
+ => SendSKSCANAsyncCore(
+ mode: SKSCANMode.ActiveScan,
+ channelMask: channelMask,
+ durationFactor: TranslateToSKSCANDurationFactorOrThrowIfOutOfRange(duration == default ? SKSCANDefaultDuration : duration, nameof(duration)),
+ commandEventHandler: new SKSCANActiveScanEventHandler(expectPairingId: false),
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSCAN 3</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.9. SKSCAN' for detailed specifications.</para>
+ /// </remarks>
+ [CLSCompliant(false)]
+ public ValueTask<(
+ SkStackResponse Response,
+ IReadOnlyList<SkStackPanDescription> PanDescriptions
+ )>
+ SendSKSCANActiveScanAsync(
+ int durationFactor,
+ uint channelMask = SKSCANDefaultChannelMask,
+ CancellationToken cancellationToken = default
+ )
+ => SendSKSCANAsyncCore(
+ mode: SKSCANMode.ActiveScan,
+ channelMask: channelMask,
+ durationFactor: ThrowIfDurationFactorOutOfRange(durationFactor, nameof(durationFactor)),
+ commandEventHandler: new SKSCANActiveScanEventHandler(expectPairingId: false),
+ cancellationToken: cancellationToken
+ );
+
+ private class SKSCANActiveScanEventHandler : SKSCANEventHandler<IReadOnlyList<SkStackPanDescription>> {
+ private readonly bool expectPairingId;
+
+ private const int ExpectedMaxPanDescriptionCount = 1;
+ private List<SkStackPanDescription>? scanResult = null;
+
+ public SKSCANActiveScanEventHandler(bool expectPairingId)
+ {
+ this.expectPairingId = expectPairingId;
+ }
+
+ public override bool TryProcessEvent(SkStackEvent ev)
+ {
+ switch (ev.Number) {
+ case SkStackEventNumber.BeaconReceived:
+ return false; // process subsequent event
+
+ case SkStackEventNumber.ActiveScanCompleted:
+ SetScanResult(
+ (IReadOnlyList<SkStackPanDescription>?)scanResult ?? Array.Empty<SkStackPanDescription>()
+ );
+ return true; // completed
+
+ default:
+ return false;
+ }
+ }
+
+ public override void ProcessSubsequentEvent(ISkStackSequenceParserContext context)
+ {
+ var reader = context.CreateReader(); // retain current buffer
+
+ if (SkStackEventParser.ExpectEPANDESC(context, expectPairingId, out var pandesc)) {
+ scanResult ??= new(capacity: ExpectedMaxPanDescriptionCount);
+ scanResult.Add(pandesc);
+ context.Continue();
+ }
+ else {
+ context.SetAsIncomplete(reader); // revert buffer
+ }
+ }
+ }
+
+ private enum SKSCANMode : byte {
+ EnergyDetectScan = 0,
+ ActiveScanPair = 2,
+ ActiveScan = 3,
+ }
+
+ private const uint SKSCANDefaultChannelMask = 0xFFFFFFFF;
+
+ private const byte SKSCANMinDurationFactor = 0x0; // 0
+ private const byte SKSCANMaxDurationFactor = 0xE; // 14
+ private const byte SKSCANDefaultDurationFactor = 0x2; // 2
+
+ private static byte ThrowIfDurationFactorOutOfRange(int durationFactor, string paramName)
+ => durationFactor is >= SKSCANMinDurationFactor and <= SKSCANMaxDurationFactor
+ ? (byte)durationFactor
+ : throw new ArgumentOutOfRangeException(paramName, durationFactor, $"must be in range of {SKSCANMinDurationFactor}~{SKSCANMaxDurationFactor}");
+
+ private static TimeSpan ToSKSCANDuration(int factor)
+ => TimeSpan.FromMilliseconds(9.6) * (Math.Pow(2.0, factor) + 1);
+
+ /// <summary>
+ /// The minimum scan duration for each channel in <c>SKSCAN</c> command.
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.9. SKSCAN' for detailed specifications.</para>
+ /// </remarks>
+ /// <seealso cref="SendSKSCANActiveScanAsync(TimeSpan, uint, CancellationToken)"/>
+ /// <seealso cref="SendSKSCANActiveScanPairAsync(TimeSpan, uint, CancellationToken)"/>
+ /// <seealso cref="SendSKSCANEnergyDetectScanAsync(TimeSpan, uint, CancellationToken)"/>
+ public static readonly TimeSpan SKSCANMinDuration = ToSKSCANDuration(SKSCANMinDurationFactor);
+
+ /// <summary>
+ /// The maximum scan duration for each channel in <c>SKSCAN</c> command.
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.9. SKSCAN' for detailed specifications.</para>
+ /// </remarks>
+ /// <seealso cref="SendSKSCANActiveScanAsync(TimeSpan, uint, CancellationToken)"/>
+ /// <seealso cref="SendSKSCANActiveScanPairAsync(TimeSpan, uint, CancellationToken)"/>
+ /// <seealso cref="SendSKSCANEnergyDetectScanAsync(TimeSpan, uint, CancellationToken)"/>
+ public static readonly TimeSpan SKSCANMaxDuration = ToSKSCANDuration(SKSCANMaxDurationFactor);
+
+ /// <summary>
+ /// The default scan duration for each channel in <c>SKSCAN</c> command.
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.9. SKSCAN' for detailed specifications.</para>
+ /// </remarks>
+ /// <seealso cref="SendSKSCANActiveScanAsync(TimeSpan, uint, CancellationToken)"/>
+ /// <seealso cref="SendSKSCANActiveScanPairAsync(TimeSpan, uint, CancellationToken)"/>
+ /// <seealso cref="SendSKSCANEnergyDetectScanAsync(TimeSpan, uint, CancellationToken)"/>
+ public static readonly TimeSpan SKSCANDefaultDuration = ToSKSCANDuration(SKSCANDefaultDurationFactor);
+
+ private static byte TranslateToSKSCANDurationFactorOrThrowIfOutOfRange(TimeSpan duration, string paramName)
+ {
+ if (SKSCANMinDuration <= duration && duration <= SKSCANMaxDuration) {
+ for (byte durationFactor = SKSCANMinDurationFactor + 1; durationFactor <= SKSCANMaxDurationFactor; durationFactor++) {
+ if (duration < ToSKSCANDuration(durationFactor))
+ return (byte)(durationFactor - 1);
+ }
+
+ return SKSCANMaxDurationFactor;
+ }
+
+ throw new ArgumentOutOfRangeException(paramName, duration, $"must be in range of {SKSCANMinDuration}~{SKSCANMaxDuration}");
+ }
+
+ private async ValueTask<(
+ SkStackResponse Response,
+ TScanResult ScanResult
+ )> SendSKSCANAsyncCore<TScanResult>(
+ SKSCANMode mode,
+ uint channelMask,
+ byte durationFactor,
+ SKSCANEventHandler<TScanResult> commandEventHandler,
+ CancellationToken cancellationToken
+ )
+ {
+ var resp = await SendCommandAsync(
+ command: SkStackCommandNames.SKSCAN,
+ writeArguments: writer => {
+ writer.WriteTokenHex((byte)mode);
+ writer.WriteTokenUINT32(channelMask, zeroPadding: true);
+ writer.WriteTokenHex(durationFactor);
+ },
+ commandEventHandler: commandEventHandler,
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+#if DEBUG
+ if (!commandEventHandler.HasScanResultSet)
+ throw new InvalidOperationException($"{commandEventHandler.ScanResult} has not been set");
+#endif
+
+ return (resp, commandEventHandler.ScanResult!);
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKSENDTO.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKSENDTO.cs
new file mode 100644
index 0000000..776d720
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKSENDTO.cs
@@ -0,0 +1,164 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ /// <summary>
+ /// <para>Sends a command <c>SKSENDTO</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.7. SKSENDTO' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<(
+ SkStackResponse Response,
+ bool IsCompletedSuccessfully
+ )> SendSKSENDTOAsync(
+ SkStackUdpPort port,
+ IPEndPoint destination,
+ ReadOnlyMemory<byte> data,
+ SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble,
+ CancellationToken cancellationToken = default
+ )
+ => SendSKSENDTOAsync(
+ handle: port.Handle,
+ destinationAddress: (destination ?? throw new ArgumentNullException(nameof(destination))).Address,
+ destinationPort: destination.Port,
+ data: data,
+ encryption: encryption,
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSENDTO</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.7. SKSENDTO' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<(
+ SkStackResponse Response,
+ bool IsCompletedSuccessfully
+ )> SendSKSENDTOAsync(
+ SkStackUdpPort port,
+ IPAddress destinationAddress,
+ int destinationPort,
+ ReadOnlyMemory<byte> data,
+ SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble,
+ CancellationToken cancellationToken = default
+ )
+ => SendSKSENDTOAsync(
+ handle: port.Handle,
+ destinationAddress: destinationAddress,
+ destinationPort: destinationPort,
+ data: data,
+ encryption: encryption,
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSENDTO</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.7. SKSENDTO' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<(
+ SkStackResponse Response,
+ bool IsCompletedSuccessfully
+ )> SendSKSENDTOAsync(
+ SkStackUdpPortHandle handle,
+ IPEndPoint destination,
+ ReadOnlyMemory<byte> data,
+ SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble,
+ CancellationToken cancellationToken = default
+ )
+ => SendSKSENDTOAsync(
+ handle: handle,
+ destinationAddress: (destination ?? throw new ArgumentNullException(nameof(destination))).Address,
+ destinationPort: destination.Port,
+ data: data,
+ encryption: encryption,
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSENDTO</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.7. SKSENDTO' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<(
+ SkStackResponse Response,
+ bool IsCompletedSuccessfully
+ )> SendSKSENDTOAsync(
+ SkStackUdpPortHandle handle,
+ IPAddress destinationAddress,
+ int destinationPort,
+ ReadOnlyMemory<byte> data,
+ SkStackUdpEncryption encryption = SkStackUdpEncryption.EncryptIfAble,
+ CancellationToken cancellationToken = default
+ )
+ {
+ const int MinDataLength = 0x0001;
+ const int MaxDataLength = 0x04D0;
+
+ SkStackUdpPort.ThrowIfPortHandleIsOutOfRange(handle, nameof(handle));
+#if SYSTEM_ENUM_ISDEFINED_OF_TENUM
+ if (!Enum.IsDefined(encryption))
+#else
+ if (!Enum.IsDefined(typeof(SkStackUdpEncryption), encryption))
+#endif
+ throw new ArgumentException($"undefined value of {nameof(SkStackUdpEncryption)}", nameof(encryption));
+ if (destinationAddress is null)
+ throw new ArgumentNullException(nameof(destinationAddress));
+ SkStackUdpPort.ThrowIfPortNumberIsOutOfRange(destinationPort, nameof(destinationPort));
+ if (data.IsEmpty)
+ throw new ArgumentException("must be non-empty sequence", nameof(data));
+ if (data.Length is not (>= MinDataLength and <= MaxDataLength))
+ throw new ArgumentException($"length of {nameof(data)} must be in range of {MinDataLength}~{MaxDataLength}", nameof(data));
+
+ return SKSENDTO();
+
+ async ValueTask<(SkStackResponse, bool)> SKSENDTO()
+ {
+ SkStackResponse response;
+ bool hasUdpSendResultStored;
+ bool isCompletedSuccessfully;
+
+ try {
+ response = await SendCommandAsync(
+ command: SkStackCommandNames.SKSENDTO,
+ writeArguments: writer => {
+ writer.WriteTokenHex((byte)handle);
+ writer.WriteTokenIPADDR(destinationAddress);
+ writer.WriteTokenUINT16((ushort)destinationPort, zeroPadding: true);
+ writer.WriteTokenHex((byte)encryption);
+ writer.WriteTokenUINT16((ushort)data.Length, zeroPadding: true);
+ writer.WriteToken(data.Span);
+ },
+ syntax: SkStackProtocolSyntax.SKSENDTO, // SKSENDTO must terminate the command line without CRLF
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+ }
+ finally {
+ hasUdpSendResultStored = lastUdpSendResult.Remove(
+ destinationAddress,
+ out isCompletedSuccessfully
+ );
+ }
+
+ if (!hasUdpSendResultStored) // in case when the 'EVENT 21' was not raised after SKSENDTO
+ throw new SkStackUdpSendResultIndeterminateException();
+
+ return (response, isCompletedSuccessfully);
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKTABLE.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKTABLE.cs
new file mode 100644
index 0000000..6c73dca
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKTABLE.cs
@@ -0,0 +1,81 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ /// <summary>
+ /// <para>Sends a command <c>SKTABLE 1</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.26. SKTABLE' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse<IReadOnlyList<IPAddress>>> SendSKTABLEAvailableAddressListAsync(
+ CancellationToken cancellationToken = default
+ )
+ => SendCommandAsync(
+ command: SkStackCommandNames.SKTABLE,
+ writeArguments: static writer => writer.WriteTokenHex(0x1),
+ parseResponsePayload: SkStackEventParser.ExpectEADDR,
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKTABLE 2</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.26. SKTABLE' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse<IReadOnlyDictionary<IPAddress, PhysicalAddress>>> SendSKTABLENeighborCacheListAsync(
+ CancellationToken cancellationToken = default
+ )
+ => SendCommandAsync(
+ command: SkStackCommandNames.SKTABLE,
+ writeArguments: static writer => writer.WriteTokenHex(0x2),
+ parseResponsePayload: SkStackEventParser.ExpectENEIGHBOR,
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKTABLE E</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.26. SKTABLE' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse<IReadOnlyList<SkStackUdpPort>>> SendSKTABLEListeningPortListAsync(
+ CancellationToken cancellationToken = default
+ )
+ {
+ return SendSKTABLEListeningPortListAsyncCore();
+
+ async ValueTask<SkStackResponse<IReadOnlyList<SkStackUdpPort>>> SendSKTABLEListeningPortListAsyncCore()
+ {
+ var resp = await SendCommandAsync(
+ command: SkStackCommandNames.SKTABLE,
+ writeArguments: static writer => writer.WriteTokenHex(0xE),
+ parseResponsePayload: SkStackEventParser.ExpectEPORT,
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ var portList = resp.Payload!;
+
+ // store or update the port handle for ECHONET Lite each time the EPORT is received
+ udpPortHandleForEchonetLite = portList.FirstOrDefault(static p => p.Port == SkStackKnownPortNumbers.EchonetLite).Handle;
+
+ return resp;
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKTERM.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKTERM.cs
new file mode 100644
index 0000000..7bda759
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKTERM.cs
@@ -0,0 +1,59 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ /// <summary>
+ /// <para>Sends a command <c>SKTERM</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.6. SKTERM' for detailed specifications.</para>
+ /// </remarks>
+ public async ValueTask<(
+ SkStackResponse Response,
+ bool IsCompletedSuccessfully
+ )> SendSKTERMAsync(
+ CancellationToken cancellationToken = default
+ )
+ {
+ var eventHandler = new SKTERMEventHandler();
+
+ var resp = await SendCommandAsync(
+ command: SkStackCommandNames.SKTERM,
+ writeArguments: null,
+ commandEventHandler: eventHandler,
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ return (resp, eventHandler.IsCompletedSuccessfully);
+ }
+
+ private class SKTERMEventHandler : SkStackEventHandlerBase {
+ public bool IsCompletedSuccessfully { get; private set; }
+
+ public override bool TryProcessEvent(SkStackEvent ev)
+ {
+ switch (ev.Number) {
+ case SkStackEventNumber.PanaSessionTerminationCompleted:
+ IsCompletedSuccessfully = true;
+ return true;
+
+ case SkStackEventNumber.PanaSessionTerminationTimedOut:
+ IsCompletedSuccessfully = false;
+ return true;
+
+ default:
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKUDPPORT.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKUDPPORT.cs
new file mode 100644
index 0000000..83e7098
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.SKUDPPORT.cs
@@ -0,0 +1,69 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ /// <summary>
+ /// <para>Sends a command <c>SKUDPPORT</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.19. SKUDPPORT' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<(SkStackResponse Response, SkStackUdpPort UdpPort)> SendSKUDPPORTAsync(
+ SkStackUdpPortHandle handle,
+ int port,
+ CancellationToken cancellationToken = default
+ )
+ {
+ SkStackUdpPort.ThrowIfPortHandleIsOutOfRange(handle, nameof(handle));
+ SkStackUdpPort.ThrowIfPortNumberIsOutOfRangeOrUnused(port, nameof(port));
+
+ return SKUDPPORT();
+
+ async ValueTask<(SkStackResponse Response, SkStackUdpPort UdpPort)> SKUDPPORT()
+ {
+ var resp = await SendCommandAsync(
+ command: SkStackCommandNames.SKUDPPORT,
+ writeArguments: writer => {
+ writer.WriteTokenHex((byte)handle);
+ writer.WriteTokenUINT16((ushort)port, zeroPadding: true);
+ },
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ return (resp, new SkStackUdpPort(handle, port));
+ }
+ }
+
+ /// <summary>
+ /// <para>Sends a command <c>SKUDPPORT</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.19. SKUDPPORT' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse> SendSKUDPPORTUnsetAsync(
+ SkStackUdpPortHandle handle,
+ CancellationToken cancellationToken = default
+ )
+ {
+ SkStackUdpPort.ThrowIfPortHandleIsOutOfRange(handle, nameof(handle));
+
+ return SendCommandAsync(
+ command: SkStackCommandNames.SKUDPPORT,
+ writeArguments: writer => {
+ writer.WriteTokenHex((byte)handle);
+ writer.WriteTokenUINT16(SkStackKnownPortNumbers.SetUnused, zeroPadding: true);
+ },
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.cs
new file mode 100644
index 0000000..4510743
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Commands.cs
@@ -0,0 +1,374 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+#pragma warning disable CA1506 // TODO: refactor
+
+using System;
+using System.Net;
+using System.Net.NetworkInformation;
+#if SYSTEM_TEXT_ASCII
+using System.Text;
+#endif
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ /// <summary>
+ /// <para>Sends a command <c>SKSREG</c>.</para>
+ /// <para>Sets the value of the register specified by <paramref name="register"/> to <paramref name="value"/>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.1. SKSREG' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse> SendSKSREGAsync<TValue>(
+ SkStackRegister.RegisterEntry<TValue> register,
+ TValue value,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (register is null)
+ throw new ArgumentNullException(nameof(register));
+ if (!register.IsWritable)
+ throw new InvalidOperationException($"register {register.Name} is not writable");
+
+ register.ThrowIfValueIsNotInRange(value, nameof(value));
+
+ return SendCommandAsync(
+ command: SkStackCommandNames.SKSREG,
+ writeArguments: writer => {
+ writer.WriteToken(register.SREG.Span);
+ register.WriteValueTo(writer, value);
+ },
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+ }
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSREG</c>.</para>
+ /// <para>Gets the value of the register specified by <paramref name="register"/>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.1. SKSREG' for detailed specifications.</para>
+ /// </remarks>
+ /// <seealso cref="SkStackRegister"/>
+ public ValueTask<SkStackResponse<TValue>> SendSKSREGAsync<TValue>(
+ SkStackRegister.RegisterEntry<TValue> register,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (register is null)
+ throw new ArgumentNullException(nameof(register));
+ if (!register.IsReadable)
+ throw new InvalidOperationException($"register {register.Name} is not readable");
+
+ return SendCommandAsync(
+ command: SkStackCommandNames.SKSREG,
+ writeArguments: writer => writer.WriteToken(register.SREG.Span),
+ parseResponsePayload: register.ParseESREG,
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+ }
+
+#if false
+ /// <summary>
+ /// <para>Sends a command <c>SKPING</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.8. SKPING' for detailed specifications.</para>
+ /// </remarks>
+#endif
+
+ private const int SKSETPWDMinLength = 1;
+ private const int SKSETPWDMaxLength = 32;
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSETPWD</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.16. SKSETPWD' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse> SendSKSETPWDAsync(
+ ReadOnlyMemory<char> password,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (password.Length is not (>= SKSETPWDMinLength and <= SKSETPWDMaxLength))
+ throw new ArgumentException($"length of `{nameof(password)}` must be in range of {SKSETPWDMinLength}~{SKSETPWDMaxLength}", nameof(password));
+#if SYSTEM_TEXT_ASCII
+ if (!Ascii.IsValid(password.Span))
+ throw new ArgumentException($"`{nameof(password)}` contains invalid characters for ASCII sequence", paramName: nameof(password));
+#endif
+
+ return SendCommandAsync(
+ command: SkStackCommandNames.SKSETPWD,
+ writeArguments: writer => {
+ writer.WriteTokenUINT8((byte)password.Length, zeroPadding: false);
+ writer.WriteMaskedToken(password.Span);
+ },
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+ }
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSETPWD</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.16. SKSETPWD' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse> SendSKSETPWDAsync(
+ ReadOnlyMemory<byte> password,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (password.Length is not (>= SKSETPWDMinLength and <= SKSETPWDMaxLength))
+ throw new ArgumentException($"length of `{nameof(password)}` must be in range of {SKSETPWDMinLength}~{SKSETPWDMaxLength}", nameof(password));
+
+ return SendCommandAsync(
+ command: SkStackCommandNames.SKSETPWD,
+ writeArguments: writer => {
+ writer.WriteTokenUINT8((byte)password.Length, zeroPadding: false);
+ writer.WriteMaskedToken(password.Span);
+ },
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+ }
+
+ private const int SKSETRBIDLengthOfId = 32;
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSETRBID</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.17. SKSETRBID' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse> SendSKSETRBIDAsync(
+ ReadOnlyMemory<char> id,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (id.Length != SKSETRBIDLengthOfId)
+ throw new ArgumentException($"length of `{nameof(id)}` must be exact {SKSETRBIDLengthOfId}", nameof(id));
+#if SYSTEM_TEXT_ASCII
+ if (!Ascii.IsValid(id.Span))
+ throw new ArgumentException($"`{nameof(id)}` contains invalid characters for ASCII sequence", paramName: nameof(id));
+#endif
+
+ return SendCommandAsync(
+ command: SkStackCommandNames.SKSETRBID,
+ writeArguments: writer => writer.WriteToken(id.Span),
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+ }
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSETRBID</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.17. SKSETRBID' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse> SendSKSETRBIDAsync(
+ ReadOnlyMemory<byte> id,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (id.Length != SKSETRBIDLengthOfId)
+ throw new ArgumentException($"length of `{nameof(id)}` must be exact {SKSETRBIDLengthOfId}", nameof(id));
+
+ return SendCommandAsync(
+ command: SkStackCommandNames.SKSETRBID,
+ writeArguments: writer => writer.WriteToken(id.Span),
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+ }
+
+ /// <summary>
+ /// <para>Sends a command <c>SKSAVE</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.20. SKSAVE' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse> SendSKSAVEAsync(
+ CancellationToken cancellationToken = default
+ )
+ => SendFlashMemoryCommand(
+ command: SkStackCommandNames.SKSAVE,
+ messageForFlashMemoryIOException: "Failed to save the register values to the flash memory.",
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKLOAD</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.21. SKLOAD' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse> SendSKLOADAsync(
+ CancellationToken cancellationToken = default
+ )
+ => SendFlashMemoryCommand(
+ command: SkStackCommandNames.SKLOAD,
+ messageForFlashMemoryIOException: "Failed to load the register values from the flash memory or the register values have not been saved in the flash memory.",
+ cancellationToken: cancellationToken
+ );
+
+ private async ValueTask<SkStackResponse> SendFlashMemoryCommand(
+ ReadOnlyMemory<byte> command,
+ string messageForFlashMemoryIOException,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var resp = await SendCommandAsync(
+ command: command,
+ throwIfErrorStatus: false,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ resp.ThrowIfErrorStatus(
+ (r, code, text) => code == SkStackErrorCode.ER10
+ ? new SkStackFlashMemoryIOException(r, code, text.Span, messageForFlashMemoryIOException)
+ : null
+ );
+
+ return resp;
+ }
+
+ /// <summary>
+ /// <para>Sends a command <c>SKERASE</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.22. SKERASE' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse> SendSKERASEAsync(
+ CancellationToken cancellationToken = default
+ )
+ => SendCommandAsync(
+ command: SkStackCommandNames.SKERASE,
+ throwIfErrorStatus: false,
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKVER</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.23. SKVER' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse<Version>> SendSKVERAsync(
+ CancellationToken cancellationToken = default
+ )
+ => SendCommandAsync(
+ command: SkStackCommandNames.SKVER,
+ writeArguments: null,
+ parseResponsePayload: static context => {
+ var reader = context.CreateReader();
+
+ if (
+ SkStackTokenParser.ExpectToken(ref reader, "EVER"u8) &&
+ SkStackTokenParser.ExpectCharArray(ref reader, out string? version) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ context.Complete(reader);
+ return Version.Parse(version);
+ }
+
+ context.SetAsIncomplete();
+ return default;
+ },
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKAPPVER</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.24. SKAPPVER' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse<string>> SendSKAPPVERAsync(
+ CancellationToken cancellationToken = default
+ )
+ => SendCommandAsync(
+ command: SkStackCommandNames.SKAPPVER,
+ writeArguments: null,
+ parseResponsePayload: static context => {
+ var reader = context.CreateReader();
+
+ if (
+ SkStackTokenParser.ExpectToken(ref reader, "EAPPVER"u8) &&
+ SkStackTokenParser.ExpectCharArray(ref reader, out string? appver) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ context.Complete(reader);
+ return appver;
+ }
+
+ context.SetAsIncomplete();
+ return default;
+ },
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKRESET</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.25. SKRESET' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse> SendSKRESETAsync(
+ CancellationToken cancellationToken = default
+ )
+ => SendCommandAsync(
+ command: SkStackCommandNames.SKRESET,
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>
+ /// <para>Sends a command <c>SKLL64</c>.</para>
+ /// </summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.29. SKLL64' for detailed specifications.</para>
+ /// </remarks>
+ public ValueTask<SkStackResponse<IPAddress>> SendSKLL64Async(
+ PhysicalAddress macAddress,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (macAddress is null)
+ throw new ArgumentNullException(nameof(macAddress));
+
+ return SendCommandAsync(
+ command: SkStackCommandNames.SKLL64,
+ writeArguments: writer => writer.WriteTokenADDR64(macAddress),
+ parseResponsePayload: static context => {
+ var reader = context.CreateReader();
+
+ if (
+ SkStackTokenParser.ExpectIPADDR(ref reader, out var linkLocalAddress) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ context.Complete(reader);
+ return linkLocalAddress;
+ }
+
+ context.SetAsIncomplete();
+ return default;
+ },
+ syntax: SkStackProtocolSyntax.SKLL64, // SKLL64 does not define its status
+ throwIfErrorStatus: true,
+ cancellationToken: cancellationToken
+ );
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Events.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Events.cs
new file mode 100644
index 0000000..ab0ef18
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Events.cs
@@ -0,0 +1,289 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+
+#pragma warning disable CA2012
+ private static readonly ValueTask<bool> TrueResultValueTask =
+#if SYSTEM_THREADING_TASKS_VALUETASK_FROMRESULT
+ ValueTask.FromResult(true);
+#else
+ new(result: true);
+#endif
+
+ private static readonly ValueTask<bool> FalseResultValueTask =
+#if SYSTEM_THREADING_TASKS_VALUETASK_FROMRESULT
+ ValueTask.FromResult(false);
+#else
+ new(result: false);
+#endif
+#pragma warning restore CA2012
+
+ private readonly Dictionary<IPAddress, bool> lastUdpSendResult = new(capacity: 2);
+
+#pragma warning disable CA1502 // TODO: refactor
+ /// <returns><see langword="true"/> if the first event processed and consumed, otherwise <see langword="false"/>.</returns>
+ private ValueTask<bool> ProcessEventsAsync(
+ ISkStackSequenceParserContext context,
+ SkStackEventHandlerBase? eventHandler, // handles events that are triggered by commands
+ CancellationToken cancellationToken
+ )
+ {
+ var reader = context.CreateReader();
+
+ if (reader.TryRead(out var firstByte)) {
+ const byte FirstByteOfEVENTOrERXUDP = (byte)'E';
+
+ if (firstByte != FirstByteOfEVENTOrERXUDP) {
+ context.Ignore();
+ return FalseResultValueTask;
+ }
+ }
+ else {
+ context.SetAsIncomplete();
+ return FalseResultValueTask;
+ }
+
+ var statusEVENT = SkStackEventParser.TryExpectEVENT(context, out var ev);
+
+ if (statusEVENT == OperationStatus.NeedMoreData) {
+ context.SetAsIncomplete();
+ return FalseResultValueTask;
+ }
+ else if (statusEVENT == OperationStatus.Done) {
+ var eventHandlerStatesCompleted = eventHandler is not null && eventHandler.TryProcessEvent(ev);
+
+ // log event
+ switch (ev.Number) {
+ case SkStackEventNumber.NeighborSolicitationReceived:
+ case SkStackEventNumber.NeighborAdvertisementReceived:
+ case SkStackEventNumber.EchoRequestReceived:
+ case SkStackEventNumber.UdpSendCompleted:
+ Logger?.LogInfoIPEventReceived(ev);
+ break;
+
+ case SkStackEventNumber.PanaSessionEstablishmentError:
+ case SkStackEventNumber.PanaSessionEstablishmentCompleted:
+ case SkStackEventNumber.PanaSessionTerminationRequestReceived:
+ case SkStackEventNumber.PanaSessionTerminationCompleted:
+ case SkStackEventNumber.PanaSessionTerminationTimedOut:
+ case SkStackEventNumber.PanaSessionExpired:
+ Logger?.LogInfoPanaEventReceived(ev);
+ break;
+
+ case SkStackEventNumber.TransmissionTimeControlLimitationActivated:
+ case SkStackEventNumber.TransmissionTimeControlLimitationDeactivated:
+ Logger?.LogInfoAribStdT108EventReceived(ev);
+ break;
+
+ case SkStackEventNumber.WakeupSignalReceived:
+ // TODO: log event
+ break;
+ }
+
+ // raise event
+ switch (ev.Number) {
+ case SkStackEventNumber.PanaSessionEstablishmentCompleted:
+ PanaSessionPeerAddress = ev.SenderAddress;
+ RaiseEventPanaSessionEstablished(ev);
+ break;
+
+ case SkStackEventNumber.PanaSessionTerminationRequestReceived:
+ case SkStackEventNumber.PanaSessionTerminationCompleted:
+ case SkStackEventNumber.PanaSessionTerminationTimedOut:
+ PanaSessionPeerAddress = null;
+ RaiseEventPanaSessionTerminated(ev);
+ break;
+
+ case SkStackEventNumber.PanaSessionExpired:
+ PanaSessionPeerAddress = null;
+ RaiseEventPanaSessionExpired(ev);
+ break;
+
+ case SkStackEventNumber.TransmissionTimeControlLimitationActivated:
+ case SkStackEventNumber.TransmissionTimeControlLimitationDeactivated:
+ // TODO: raise event
+ break;
+
+ case SkStackEventNumber.WakeupSignalReceived:
+ RaiseEventWokeUp(ev);
+ break;
+
+ case SkStackEventNumber.BeaconReceived:
+ SkStackUnexpectedResponseException.ThrowIfUnexpectedSubsequentEventCode(
+ subsequentEventCode: ev.ExpectedSubsequentEventCode,
+ expectedEventCode: SkStackEventCode.EPANDESC
+ );
+ break;
+
+ case SkStackEventNumber.UdpSendCompleted:
+#if DEBUG
+ if (!ev.HasSenderAddress)
+ throw new InvalidOperationException($"{nameof(ev.SenderAddress)} must not be null");
+#endif
+ switch (ev.Parameter) {
+ case 0: // success
+ lastUdpSendResult[ev.SenderAddress!] = true;
+ break;
+
+ case 1: // failed
+ lastUdpSendResult[ev.SenderAddress!] = false;
+ break;
+
+ case 2: // performed Neighbor Solicitation
+ default:
+ break; // nothing to do
+ }
+
+ break;
+
+ case SkStackEventNumber.EnergyDetectScanCompleted:
+ SkStackUnexpectedResponseException.ThrowIfUnexpectedSubsequentEventCode(
+ subsequentEventCode: ev.ExpectedSubsequentEventCode,
+ expectedEventCode: SkStackEventCode.EEDSCAN
+ );
+ break;
+ }
+
+ if (eventHandlerStatesCompleted)
+ context.Complete();
+ else
+ context.Continue();
+
+ return TrueResultValueTask;
+ }
+
+ var statusERXUDP = SkStackEventParser.TryExpectERXUDP(
+ context,
+ erxudpDataFormat,
+ out var erxudp,
+ out var erxudpData,
+ out var erxudpDataLength
+ );
+
+ if (statusERXUDP == OperationStatus.NeedMoreData) {
+ context.SetAsIncomplete();
+ return FalseResultValueTask;
+ }
+ else if (statusERXUDP == OperationStatus.Done) {
+ Logger?.LogInfoIPEventReceived(erxudp, erxudpData);
+
+ return ProcessERXUDPAsync();
+
+ async ValueTask<bool> ProcessERXUDPAsync()
+ {
+ await OnERXUDPAsync(
+ localPort: erxudp.LocalEndPoint.Port,
+ remoteAddress: erxudp.RemoteEndPoint.Address,
+ data: erxudpData,
+ dataLength: erxudpDataLength,
+ dataFormat: erxudpDataFormat,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ context.Continue();
+ return true;
+ }
+ }
+
+ // if (status == OperationStatus.InvalidData)
+ context.Ignore();
+ return FalseResultValueTask;
+ }
+#pragma warning restore CA1502
+
+ /// <summary>
+ /// Gets or sets the object used to marshal the event handler calls that are issued when an event received.
+ /// </summary>
+ public ISynchronizeInvoke? SynchronizingObject { get; set; }
+
+ /// <summary>
+ /// Occurs when a PANA session is established.
+ /// </summary>
+ public event EventHandler<SkStackPanaSessionEventArgs>? PanaSessionEstablished;
+
+ /// <summary>
+ /// Occurs when a PANA session is terminated.
+ /// </summary>
+ public event EventHandler<SkStackPanaSessionEventArgs>? PanaSessionTerminated;
+
+ /// <summary>
+ /// Occurs when a PANA session is expired.
+ /// </summary>
+ public event EventHandler<SkStackPanaSessionEventArgs>? PanaSessionExpired;
+
+ internal void RaiseEventPanaSessionEstablished(SkStackEvent baseEvent) => RaiseEventPanaSession(PanaSessionEstablished, baseEvent);
+ internal void RaiseEventPanaSessionTerminated(SkStackEvent baseEvent) => RaiseEventPanaSession(PanaSessionTerminated, baseEvent);
+ internal void RaiseEventPanaSessionExpired(SkStackEvent baseEvent) => RaiseEventPanaSession(PanaSessionExpired, baseEvent);
+
+ private void RaiseEventPanaSession(EventHandler<SkStackPanaSessionEventArgs>? ev, SkStackEvent baseEvent)
+ {
+ if (ev is null)
+ return; // return without creating event args if event hanlder is null
+
+ InvokeEvent(SynchronizingObject, ev, this, new SkStackPanaSessionEventArgs(baseEvent));
+ }
+
+ /// <summary>
+ /// Occurs when a device enters sleep mode.
+ /// </summary>
+ /// <seealso cref="SendSKDSLEEPAsync"/>
+ public event EventHandler<SkStackEventArgs>? Slept;
+
+ /// <summary>
+ /// Occurs when a device returns from sleep mode.
+ /// </summary>
+ /// <seealso cref="SendSKDSLEEPAsync"/>
+ public event EventHandler<SkStackEventArgs>? WokeUp;
+
+ internal void RaiseEventSlept() => RaiseEvent(Slept, default);
+ private void RaiseEventWokeUp(SkStackEvent baseEvent) => RaiseEvent(WokeUp, baseEvent);
+
+ private void RaiseEvent(EventHandler<SkStackEventArgs>? ev, SkStackEvent baseEvent)
+ {
+ if (ev is null)
+ return; // return without creating event args if event hanlder is null
+
+ InvokeEvent(SynchronizingObject, ev, this, new SkStackEventArgs(baseEvent));
+ }
+
+ private static void InvokeEvent<TEventArgs>(
+ ISynchronizeInvoke? synchronizingObject,
+ EventHandler<TEventArgs> ev,
+ object sender,
+ TEventArgs args
+ )
+ where TEventArgs : SkStackEventArgs
+ {
+ if (synchronizingObject is null || !synchronizingObject.InvokeRequired) {
+ try {
+ ev(sender, args);
+ }
+#pragma warning disable CA1031
+ catch {
+ // ignore exceptions
+ }
+#pragma warning restore CA1031
+ }
+ else {
+ synchronizingObject.BeginInvoke(
+ method: ev,
+ args: new object[] { sender, args }
+ );
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.EchonetLite.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.EchonetLite.cs
new file mode 100644
index 0000000..fa81047
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.EchonetLite.cs
@@ -0,0 +1,100 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.Buffers;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Polly;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ /// <summary>
+ /// An UDP port handle currently assigned to the port for ECHONET Lite.
+ /// </summary>
+ /// <remarks>
+ /// This value will be updated each time an <c>EPORT</c> is received. See implementation of <see cref="SendSKTABLEListeningPortListAsync"/>.
+ /// </remarks>
+ /// <seealso cref="SkStackKnownPortNumbers.EchonetLite"/>
+ /// <seealso cref="SendUdpEchonetLiteAsync"/>
+ private SkStackUdpPortHandle udpPortHandleForEchonetLite;
+
+ public ValueTask<IPAddress> ReceiveUdpEchonetLiteAsync(
+ IBufferWriter<byte> buffer,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (buffer is null)
+ throw new ArgumentNullException(nameof(buffer));
+
+ ThrowIfDisposed();
+
+ return ReceiveUdpAsync(
+ port: SkStackKnownPortNumbers.EchonetLite,
+ buffer: buffer,
+ cancellationToken: cancellationToken
+ );
+ }
+
+ [CLSCompliant(false)] // ResiliencePipeline is not CLS compliant
+ public ValueTask SendUdpEchonetLiteAsync(
+ ReadOnlyMemory<byte> buffer,
+ ResiliencePipeline? resiliencePipeline = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (SkStackUdpPort.IsPortHandleIsOutOfRange(udpPortHandleForEchonetLite))
+ throw new InvalidOperationException($"UDP port {SkStackKnownPortNumbers.EchonetLite} is not listening. Call {nameof(PrepareUdpPortAsync)} or {nameof(SendSKUDPPORTAsync)} in advance to listen the port.");
+
+ ThrowIfDisposed();
+ ThrowIfPanaSessionIsNotEstablished();
+
+ resiliencePipeline ??= ResiliencePipeline.Empty;
+
+ return resiliencePipeline.ExecuteAsync(
+ ct => SendUdpEchonetLiteAsyncCore(
+ thisClient: this,
+ udpPortHandleForEchonetLite: udpPortHandleForEchonetLite,
+#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE
+ peerAddress: PanaSessionPeerAddress,
+#else
+ peerAddress: PanaSessionPeerAddress!,
+#endif
+ buffer: buffer,
+ cancellationToken: ct
+ ),
+ cancellationToken: cancellationToken
+ );
+
+ static async ValueTask SendUdpEchonetLiteAsyncCore(
+ SkStackClient thisClient,
+ SkStackUdpPortHandle udpPortHandleForEchonetLite,
+ IPAddress peerAddress,
+ ReadOnlyMemory<byte> buffer,
+ CancellationToken cancellationToken
+ )
+ {
+ var (_, isCompletedSuccessfully) = await thisClient.SendSKSENDTOAsync(
+ handle: udpPortHandleForEchonetLite,
+ destinationAddress: peerAddress,
+ destinationPort: SkStackKnownPortNumbers.EchonetLite,
+ data: buffer,
+ encryption: SkStackUdpEncryption.ForceEncrypt,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ if (!isCompletedSuccessfully) {
+ throw new SkStackUdpSendFailedException(
+ message: $"Failed to send ECHONET Lite frame. (Handle: {udpPortHandleForEchonetLite}, Peer: {peerAddress})",
+ portHandle: udpPortHandleForEchonetLite,
+ peerAddress: peerAddress
+ );
+ }
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.FlashMemory.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.FlashMemory.cs
new file mode 100644
index 0000000..baf9dcf
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.FlashMemory.cs
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ public ValueTask SaveFlashMemoryAsync(
+ SkStackFlashMemoryWriteRestriction restriction,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (restriction is null)
+ throw new ArgumentNullException(nameof(restriction));
+ if (restriction.IsRestricted())
+ throw new InvalidOperationException($"Writing to flash memory is restricted by the {nameof(SkStackFlashMemoryWriteRestriction)}.");
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return SaveFlashMemoryAsyncCore();
+
+ async ValueTask SaveFlashMemoryAsyncCore()
+ => await SendSKSAVEAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ public async ValueTask LoadFlashMemoryAsync(
+ CancellationToken cancellationToken = default
+ )
+ => await SendSKLOADAsync(cancellationToken).ConfigureAwait(false);
+
+ public ValueTask EnableFlashMemoryAutoLoadAsync(
+ CancellationToken cancellationToken = default
+ )
+ => SetFlashMemoryAutoLoadAsync(
+ trueIfEnable: true,
+ cancellationToken: cancellationToken
+ );
+
+ public ValueTask DisableFlashMemoryAutoLoadAsync(
+ CancellationToken cancellationToken = default
+ )
+ => SetFlashMemoryAutoLoadAsync(
+ trueIfEnable: false,
+ cancellationToken: cancellationToken
+ );
+
+ protected async ValueTask SetFlashMemoryAutoLoadAsync(
+ bool trueIfEnable,
+ CancellationToken cancellationToken = default
+ )
+ => await SendSKSREGAsync(
+ register: SkStackRegister.EnableAutoLoad,
+ value: trueIfEnable,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.IP.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.IP.cs
new file mode 100644
index 0000000..0216a78
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.IP.cs
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System.Collections.Generic;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ public async ValueTask<IReadOnlyList<IPAddress>> GetAvailableAddressListAsync(
+ CancellationToken cancellationToken = default
+ )
+ {
+ var resp = await SendSKTABLEAvailableAddressListAsync(
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ return resp.Payload!;
+ }
+
+ public async ValueTask<IPAddress> ConvertToIPv6LinkLocalAddressAsync(
+ PhysicalAddress macAddress,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var resp = await SendSKLL64Async(
+ macAddress: macAddress,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ return resp.Payload!;
+ }
+
+ public async ValueTask<IReadOnlyDictionary<IPAddress, PhysicalAddress>> GetNeighborCacheListAsync(
+ CancellationToken cancellationToken = default
+ )
+ {
+ var resp = await SendSKTABLENeighborCacheListAsync(
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ return resp.Payload!;
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.PANA.ActiveScan.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.PANA.ActiveScan.cs
new file mode 100644
index 0000000..9d75be4
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.PANA.ActiveScan.cs
@@ -0,0 +1,87 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ public ValueTask<IReadOnlyList<SkStackPanDescription>> ActiveScanAsync(
+ ReadOnlyMemory<byte> rbid,
+ ReadOnlyMemory<byte> password,
+ SkStackActiveScanOptions? scanOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ => ActiveScanAsyncCore(
+ rbid: rbid,
+ password: password,
+ scanDurationFactorGenerator: (scanOptions ?? SkStackActiveScanOptions.Default).YieldScanDurationFactors(),
+ cancellationToken: cancellationToken
+ );
+
+ private async ValueTask<IReadOnlyList<SkStackPanDescription>> ActiveScanAsyncCore(
+ ReadOnlyMemory<byte>? rbid,
+ ReadOnlyMemory<byte>? password,
+ IEnumerable<int> scanDurationFactorGenerator,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (scanDurationFactorGenerator is null)
+ throw new ArgumentNullException(nameof(scanDurationFactorGenerator));
+
+ if (rbid is not null || password is not null) {
+ // If RBID or password is supplied, set them before scanning.
+ await SetRouteBCredentialAsync(
+ rbid: rbid,
+ rbidParamName: nameof(rbid),
+ password: password,
+ passwordParamName: nameof(password),
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+ }
+
+ foreach (var durationFactor in scanDurationFactorGenerator) {
+ var (_, result) = await SendSKSCANActiveScanPairAsync(
+ durationFactor: durationFactor,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ if (0 < result.Count)
+ return result;
+
+ // no pairs found, try again with next scan duration factor
+ }
+
+ return Array.Empty<SkStackPanDescription>();
+ }
+
+ private async ValueTask<SkStackPanDescription> ActiveScanPanaAuthenticationAgentAsync<TArg>(
+ SkStackActiveScanOptions baseScanOptions,
+ Func<TArg, SkStackPanDescription, CancellationToken, ValueTask<bool>> selectPanaAuthenticationAgentAsync,
+ TArg arg,
+ CancellationToken cancellationToken
+ )
+ {
+ var activeScanResult = await ActiveScanAsyncCore(
+ rbid: null,
+ password: null,
+ scanDurationFactorGenerator: baseScanOptions.YieldScanDurationFactors(),
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ SkStackPanDescription? ret = null;
+
+ foreach (var pan in activeScanResult) {
+ if (await selectPanaAuthenticationAgentAsync(arg, pan, cancellationToken).ConfigureAwait(false)) {
+ ret = pan;
+ break;
+ }
+ }
+
+ return ret ?? throw new InvalidOperationException("No appropriate PAA was found or selected in active scan result.");
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.PANA.AuthenticateAsPaC.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.PANA.AuthenticateAsPaC.cs
new file mode 100644
index 0000000..523cc2f
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.PANA.AuthenticateAsPaC.cs
@@ -0,0 +1,396 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.Buffers;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ /// <inheritdoc cref="AuthenticateAsPanaClientAsyncCore"/>
+ /// <param name="rbid">A Route-B ID used for PANA authentication.</param>
+ /// <param name="password">A password ID used for PANA authentication.</param>
+ /// <param name="scanOptions">Options such as scanning behavior when performing active scanning.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(
+ ReadOnlyMemory<byte> rbid,
+ ReadOnlyMemory<byte> password,
+ SkStackActiveScanOptions? scanOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ ThrowIfPanaSessionAlreadyEstablished();
+
+ return AuthenticateAsPanaClientAsyncCore(
+ rbid: rbid,
+ password: password,
+ getPaaAddressTask: default,
+ channel: null,
+ panId: null,
+ scanOptions: scanOptions ?? SkStackActiveScanOptions.Default,
+ cancellationToken: cancellationToken
+ );
+ }
+
+ /// <inheritdoc cref="AuthenticateAsPanaClientAsyncCore"/>
+ /// <param name="rbid">A Route-B ID used for PANA authentication.</param>
+ /// <param name="password">A password ID used for PANA authentication.</param>
+ /// <param name="paaAddress">An <see cref="IPAddress"/> representing the IP address of the PANA Authentication Agent (PAA).</param>
+ /// <param name="channelNumber">A channel number to be used for PANA session.</param>
+ /// <param name="panId">A Personal Area Network (PAN) ID to be used for PANA session.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(
+ ReadOnlyMemory<byte> rbid,
+ ReadOnlyMemory<byte> password,
+ IPAddress paaAddress,
+ int channelNumber,
+ int panId,
+ CancellationToken cancellationToken = default
+ )
+ => AuthenticateAsPanaClientAsync(
+ rbid: rbid,
+ password: password,
+ paaAddress: paaAddress,
+ channel: SkStackChannel.FindByChannelNumber(channelNumber, nameof(channelNumber)),
+ panId: panId,
+ cancellationToken: cancellationToken
+ );
+
+ /// <inheritdoc cref="AuthenticateAsPanaClientAsyncCore"/>
+ /// <param name="rbid">A Route-B ID used for PANA authentication.</param>
+ /// <param name="password">A password ID used for PANA authentication.</param>
+ /// <param name="paaAddress">An <see cref="IPAddress"/> representing the IP address of the PANA Authentication Agent (PAA).</param>
+ /// <param name="channel">A <see cref="SkStackChannel"/> representing the channel to be used for PANA session.</param>
+ /// <param name="panId">A Personal Area Network (PAN) ID to be used for PANA session.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(
+ ReadOnlyMemory<byte> rbid,
+ ReadOnlyMemory<byte> password,
+ IPAddress paaAddress,
+ SkStackChannel channel,
+ int panId,
+ CancellationToken cancellationToken = default
+ )
+ {
+ ThrowIfPanaSessionAlreadyEstablished();
+
+ if (channel.IsEmpty)
+ throw new ArgumentException(message: "invalid channel (empty channel)", paramName: nameof(channel));
+
+ return AuthenticateAsPanaClientAsyncCore(
+ rbid: rbid,
+ password: password,
+ getPaaAddressTask: new(paaAddress ?? throw new ArgumentNullException(nameof(paaAddress))),
+ channel: channel,
+ panId: ValidatePanIdAndThrowIfInvalid(panId, nameof(panId)),
+ scanOptions: SkStackActiveScanOptions.Null, // scanning will not be performed and therefore this will not be referenced
+ cancellationToken: cancellationToken
+ );
+ }
+
+ /// <inheritdoc cref="AuthenticateAsPanaClientAsyncCore"/>
+ /// <param name="rbid">A Route-B ID used for PANA authentication.</param>
+ /// <param name="password">A password ID used for PANA authentication.</param>
+ /// <param name="pan">A <see cref="SkStackPanDescription"/> representing the address of the PANA Authentication Agent (PAA), PAN ID, and channel used for PANA session.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(
+ ReadOnlyMemory<byte> rbid,
+ ReadOnlyMemory<byte> password,
+ SkStackPanDescription pan,
+ CancellationToken cancellationToken = default
+ )
+ => AuthenticateAsPanaClientAsync(
+ rbid: rbid,
+ password: password,
+ paaMacAddress: pan.MacAddress,
+ channel: pan.Channel,
+ panId: pan.Id,
+ cancellationToken: cancellationToken
+ );
+
+ /// <inheritdoc cref="AuthenticateAsPanaClientAsyncCore"/>
+ /// <param name="rbid">A Route-B ID used for PANA authentication.</param>
+ /// <param name="password">A password ID used for PANA authentication.</param>
+ /// <param name="paaMacAddress">A <see cref="PhysicalAddress"/> representing the MAC address of the PANA Authentication Agent (PAA).</param>
+ /// <param name="channelNumber">A channel number to be used for PANA session.</param>
+ /// <param name="panId">A Personal Area Network (PAN) ID to be used for PANA session.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(
+ ReadOnlyMemory<byte> rbid,
+ ReadOnlyMemory<byte> password,
+ PhysicalAddress paaMacAddress,
+ int channelNumber,
+ int panId,
+ CancellationToken cancellationToken = default
+ )
+ => AuthenticateAsPanaClientAsync(
+ rbid: rbid,
+ password: password,
+ paaMacAddress: paaMacAddress,
+ channel: SkStackChannel.FindByChannelNumber(channelNumber, nameof(channelNumber)),
+ panId: panId,
+ cancellationToken: cancellationToken
+ );
+
+ /// <inheritdoc cref="AuthenticateAsPanaClientAsyncCore"/>
+ /// <param name="rbid">A Route-B ID used for PANA authentication.</param>
+ /// <param name="password">A password ID used for PANA authentication.</param>
+ /// <param name="paaMacAddress">A <see cref="PhysicalAddress"/> representing the MAC address of the PANA Authentication Agent (PAA).</param>
+ /// <param name="channel">A <see cref="SkStackChannel"/> representing the channel to be used for PANA session.</param>
+ /// <param name="panId">A Personal Area Network (PAN) ID to be used for PANA session.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+ public ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsync(
+ ReadOnlyMemory<byte> rbid,
+ ReadOnlyMemory<byte> password,
+ PhysicalAddress paaMacAddress,
+ SkStackChannel channel,
+ int panId,
+ CancellationToken cancellationToken = default
+ )
+ {
+ ThrowIfPanaSessionAlreadyEstablished();
+
+ return AuthenticateAsPanaClientAsyncCore(
+ rbid: rbid,
+ password: password,
+#pragma warning disable CA2012, CS8620
+ getPaaAddressTask: ConvertToIPv6LinkLocalAddressAsync(
+ paaMacAddress ?? throw new ArgumentNullException(nameof(paaMacAddress)),
+ cancellationToken
+ ),
+#pragma warning restore CA2012, CS8620
+ channel: channel,
+ panId: ValidatePanIdAndThrowIfInvalid(panId, nameof(panId)),
+ scanOptions: SkStackActiveScanOptions.Null, // scanning will not be performed and therefore this will not be referenced
+ cancellationToken: cancellationToken
+ );
+ }
+
+ private static int ValidatePanIdAndThrowIfInvalid(int panId, string paramName)
+ {
+ if (SkStackRegister.PanId.MinValue <= panId && panId <= SkStackRegister.PanId.MaxValue)
+ return panId;
+
+ throw new ArgumentOutOfRangeException(paramName, panId, $"must be in range of {SkStackRegister.PanId.MinValue}~{SkStackRegister.PanId.MaxValue}");
+ }
+
+ /// <summary>
+ /// Starts the PANA authentication sequence with the current instance as the PaC.
+ /// </summary>
+ /// <param name="rbid">A Route-B ID used for PANA authentication.</param>
+ /// <param name="password">A password ID used for PANA authentication.</param>
+ /// <param name="getPaaAddressTask">
+ /// An <see cref="ValueTask{IPAddress}"/> that returns IP address of the PANA Authentication Agent (PAA).
+ /// If returns <see langword="null"/>, an active scan will be performed to discover the PAAs.
+ /// </param>
+ /// <param name="channel">A <see cref="SkStackChannel"/> representing the channel to be used for PANA session.</param>
+ /// <param name="panId">A Personal Area Network (PAN) ID to be used for PANA session.</param>
+ /// <param name="scanOptions">Options such as scanning behavior when performing active scanning.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+ /// <returns>
+ /// A <see cref="ValueTask{SkStackPanaSessionInfo}"/> representing the established PANA session information.
+ /// </returns>
+ /// <seealso cref="SkStackPanaSessionInfo"/>
+ /// <seealso cref="SkStackRegister.Channel"/>
+ /// <seealso cref="SkStackRegister.PanId"/>
+ /// <seealso cref="PanaSessionPeerAddress"/>
+ /// <seealso cref="IsPanaSessionAlive"/>
+ /// <seealso cref="SendSKJOINAsync"/>
+ /// <seealso cref="SendSKSETRBIDAsync(ReadOnlyMemory{byte}, CancellationToken)"/>
+ /// <seealso cref="SendSKSETPWDAsync(ReadOnlyMemory{byte}, CancellationToken)"/>
+ private async ValueTask<SkStackPanaSessionInfo> AuthenticateAsPanaClientAsyncCore(
+ ReadOnlyMemory<byte> rbid,
+ ReadOnlyMemory<byte> password,
+ ValueTask<IPAddress?> getPaaAddressTask,
+ SkStackChannel? channel,
+ int? panId,
+ SkStackActiveScanOptions scanOptions,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var paaAddress = await getPaaAddressTask.ConfigureAwait(false);
+
+ if (paaAddress is not null && !paaAddress.IsIPv6LinkLocal)
+ throw new NotSupportedException($"Supplied IP address is not an IPv6 link local address. PAA Address: {paaAddress}");
+
+ await SetRouteBCredentialAsync(
+ rbid: rbid,
+ rbidParamName: nameof(rbid),
+ password: password,
+ passwordParamName: nameof(password),
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ PhysicalAddress? paaMacAddress = null;
+ var needToFindPanaAuthenticationAgent = true;
+
+ // If PAA address is IPv6 link local, construct MAC address from it and add to the neighbor address table.
+ if (paaAddress is not null) {
+ byte[]? linkLocalAddressBytes = null;
+
+ try {
+ linkLocalAddressBytes = ArrayPool<byte>.Shared.Rent(16);
+
+ if (paaAddress.TryWriteBytes(linkLocalAddressBytes, out var bytesWritten) && 8 <= bytesWritten) {
+ var linkLocalAddressMemory = linkLocalAddressBytes.AsMemory(0, bytesWritten);
+ var macAddressMemory = linkLocalAddressMemory.Slice(linkLocalAddressMemory.Length - 8, 8);
+
+ macAddressMemory.Span[0] &= 0b_1111_1101;
+
+ paaMacAddress = new PhysicalAddress(macAddressMemory.ToArray());
+
+ await SendSKADDNBRAsync(
+ ipv6Address: paaAddress,
+ macAddress: paaMacAddress,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ needToFindPanaAuthenticationAgent = false;
+ }
+ }
+ finally {
+ if (linkLocalAddressBytes is not null)
+ ArrayPool<byte>.Shared.Return(linkLocalAddressBytes);
+ }
+ }
+
+ // If channel or PAN ID is not supplied, have to scan PAN and retrieve them.
+ needToFindPanaAuthenticationAgent |= !channel.HasValue;
+ needToFindPanaAuthenticationAgent |= !panId.HasValue;
+
+ IPAddress paaAddressNotNull;
+ PhysicalAddress paaMacAddressNotNull;
+ SkStackChannel channelNotNull;
+ int panIdNotNull;
+
+ if (needToFindPanaAuthenticationAgent) {
+ var peer = await FindPanaAuthenticationAgentAsync(
+ paaAddress: paaAddress,
+ scanOptions: scanOptions,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ channelNotNull = peer.Channel;
+ panIdNotNull = peer.Id;
+ paaMacAddressNotNull = peer.MacAddress;
+
+ if (paaAddress is null) {
+ var respSKLL64 = await SendSKLL64Async(
+ macAddress: paaMacAddressNotNull,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ paaAddressNotNull = respSKLL64.Payload!;
+
+ await SendSKADDNBRAsync(
+ ipv6Address: paaAddressNotNull,
+ macAddress: paaMacAddressNotNull,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+ }
+ else {
+ paaAddressNotNull = paaAddress;
+ }
+ }
+ else {
+#if DEBUG
+ if (paaAddress is null)
+ throw new InvalidOperationException($"{nameof(paaAddress)} is null");
+ if (paaMacAddress is null)
+ throw new InvalidOperationException($"{nameof(paaMacAddress)} is null");
+ if (!channel.HasValue)
+ throw new InvalidOperationException($"{nameof(channel)} is null");
+ if (!panId.HasValue)
+ throw new InvalidOperationException($"{nameof(panId)} is null");
+#endif
+
+ paaAddressNotNull = paaAddress!;
+ paaMacAddressNotNull = paaMacAddress!;
+
+#if !DEBUG
+#pragma warning disable CS8629
+#endif
+ channelNotNull = channel.Value!;
+ panIdNotNull = panId.Value!;
+#pragma warning restore CS8629
+ }
+
+ // Set channel and PAN ID if needed.
+ var resp = await SendSKINFOAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ var (localAddress, localMacAddress, currentChannel, currentPanId, _) = resp.Payload;
+
+ if (!currentChannel.Equals(channelNotNull)) {
+ await SendSKSREGAsync(
+ SkStackRegister.Channel,
+ channelNotNull,
+ cancellationToken
+ ).ConfigureAwait(false);
+ }
+
+ if (currentPanId != panIdNotNull) {
+ await SendSKSREGAsync(
+ SkStackRegister.PanId,
+ (ushort)panIdNotNull,
+ cancellationToken
+ ).ConfigureAwait(false);
+ }
+
+ // Then attempt to establish the PANA session.
+ await SendSKJOINAsync(
+ ipv6address: paaAddressNotNull,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ return new(
+ localAddress: localAddress,
+ localMacAddress: localMacAddress,
+ peerAddress: paaAddressNotNull,
+ peerMacAddress: paaMacAddressNotNull,
+ channel: channelNotNull,
+ panId: panIdNotNull
+ );
+ }
+
+ private ValueTask<SkStackPanDescription> FindPanaAuthenticationAgentAsync(
+ IPAddress? paaAddress,
+ SkStackActiveScanOptions scanOptions,
+ CancellationToken cancellationToken
+ )
+ {
+ if (paaAddress is null) {
+#if DEBUG
+ if (scanOptions is null)
+ throw new ArgumentNullException(nameof(scanOptions));
+#endif
+
+ // If PAA address is not supplied, scan and select PAN with the specified selector.
+ return ActiveScanPanaAuthenticationAgentAsync(
+ baseScanOptions: scanOptions,
+ selectPanaAuthenticationAgentAsync: static (options, desc, _) => new(options.SelectPanaAuthenticationAgent(desc)),
+ arg: scanOptions,
+ cancellationToken: cancellationToken
+ );
+ }
+ else {
+ // If PAA address is supplied, scan and resolve MAC address to collate with the supplied one.
+ return ActiveScanPanaAuthenticationAgentAsync(
+ baseScanOptions: scanOptions,
+ selectPanaAuthenticationAgentAsync:
+ async (address, desc, token) => address.Equals(
+ await ConvertToIPv6LinkLocalAddressAsync(
+ desc.MacAddress,
+ token
+ ).ConfigureAwait(false)
+ ),
+ arg: paaAddress,
+ cancellationToken: cancellationToken
+ );
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.PANA.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.PANA.cs
new file mode 100644
index 0000000..3fedde1
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.PANA.cs
@@ -0,0 +1,94 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE || SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE
+using System.Diagnostics.CodeAnalysis;
+#endif
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ /// <summary>Gets the <see cref="IPAddress"/> of current PANA session peer.</summary>
+ /// <value><see langword="null"/> if PANA session has been terminated, expired, or not been established.</value>
+ public IPAddress? PanaSessionPeerAddress { get; private set; }
+
+ /// <summary>Gets a value indicating whether or not the PANA session is alive.</summary>
+ /// <value><see langword="true"/> if PANA session is established and alive, <see langword="false"/> if PANA session has been terminated, expired, or not been established.</value>
+#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE
+ [MemberNotNullWhen(true, nameof(PanaSessionPeerAddress))]
+#endif
+ public bool IsPanaSessionAlive => PanaSessionPeerAddress is not null;
+
+#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE
+ [MemberNotNull(nameof(PanaSessionPeerAddress))]
+#endif
+ protected internal void ThrowIfPanaSessionIsNotEstablished()
+ {
+ if (PanaSessionPeerAddress is null)
+ throw new InvalidOperationException("PANA session has expired or has not been established yet.");
+ }
+
+ protected internal void ThrowIfPanaSessionAlreadyEstablished()
+ {
+ if (PanaSessionPeerAddress is not null)
+ throw new InvalidOperationException("PANA session has been already established.");
+ }
+
+ private async ValueTask SetRouteBCredentialAsync(
+ ReadOnlyMemory<byte>? rbid,
+ string rbidParamName,
+ ReadOnlyMemory<byte>? password,
+ string passwordParamName,
+ CancellationToken cancellationToken
+ )
+ {
+ if (rbid is not null) {
+ if (rbid.Value.IsEmpty)
+ throw new ArgumentException("must be non-empty string", rbidParamName ?? nameof(rbid));
+
+ _ = await SendSKSETRBIDAsync(
+ id: rbid.Value,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+ }
+
+ if (password is not null) {
+ if (password.Value.IsEmpty)
+ throw new ArgumentException("must be non-empty string", passwordParamName ?? nameof(password));
+
+ _ = await SendSKSETPWDAsync(
+ password: password.Value,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+ }
+ }
+
+ /// <summary>
+ /// Terminates the currently established PANA session by sending <c>SKTERM</c> command.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">PANA session has already expired or has not been established yet.</exception>
+ /// <returns><see langword="true"/> if terminated successfully, otherwise <see langword="false"/> (timed out).</returns>
+ public ValueTask<bool> TerminatePanaSessionAsync(
+ CancellationToken cancellationToken = default
+ )
+ {
+ ThrowIfDisposed();
+ ThrowIfPanaSessionIsNotEstablished();
+
+ return TerminatePanaSessionAsyncCore();
+
+ async ValueTask<bool> TerminatePanaSessionAsyncCore()
+ {
+ var (_, isCompletedSuccessfully) = await SendSKTERMAsync(
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ return isCompletedSuccessfully;
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.UDP.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.UDP.cs
new file mode 100644
index 0000000..33c2817
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Functions.UDP.cs
@@ -0,0 +1,358 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+#if NULL_STATE_STATIC_ANALYSIS_ATTRIBUTES
+using System.Diagnostics.CodeAnalysis;
+#endif
+using System.IO.Pipelines;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ private static readonly TimeSpan ReceiveUdpPollingIntervalDefault = TimeSpan.FromMilliseconds(10);
+
+ private TimeSpan receiveUdpPollingInterval = ReceiveUdpPollingIntervalDefault;
+
+ public TimeSpan ReceiveUdpPollingInterval {
+ get => receiveUdpPollingInterval;
+ set {
+ if (value <= TimeSpan.Zero)
+ throw new ArgumentOutOfRangeException(message: "must be non-zero positive value", actualValue: value, paramName: nameof(ReceiveUdpPollingInterval));
+
+ receiveUdpPollingInterval = value;
+ }
+ }
+
+ private SkStackERXUDPDataFormat erxudpDataFormat;
+
+ /// <summary>
+ /// Gets or sets the format of the data part in event <c>ERXUDP</c>.
+ /// </summary>
+ /// <remarks>
+ /// <para>See below for detailed specifications.</para>
+ /// <list type="bullet">
+ /// <item><description>'BP35A1コマンドリファレンス 3.30. WOPT (プロダクト設定コマンド)'</description></item>
+ /// <item><description>'BP35A1コマンドリファレンス 4.1. ERXUDP'</description></item>
+ /// </list>
+ /// </remarks>
+ /// <seealso cref="SkStackERXUDPDataFormat"/>
+ public SkStackERXUDPDataFormat ERXUDPDataFormat {
+ get => erxudpDataFormat;
+ protected set => erxudpDataFormat = ValidateERXUDPDataFormat(value, nameof(ERXUDPDataFormat));
+ }
+
+ private static SkStackERXUDPDataFormat ValidateERXUDPDataFormat(
+ SkStackERXUDPDataFormat value,
+ string paramNameForValue
+ )
+ {
+#if SYSTEM_ENUM_ISDEFINED_OF_TENUM
+ if (!Enum.IsDefined(value))
+#else
+ if (!Enum.IsDefined(typeof(SkStackERXUDPDataFormat), value))
+#endif
+ throw new ArgumentException(message: $"undefined value of {nameof(SkStackERXUDPDataFormat)}", paramName: paramNameForValue);
+
+ return value;
+ }
+
+ private readonly Dictionary<int/*port*/, Pipe> udpReceiveEventPipes = new(
+ capacity: SkStackUdpPort.NumberOfPorts
+ );
+
+ public async ValueTask<IReadOnlyList<SkStackUdpPort>> GetListeningUdpPortListAsync(
+ CancellationToken cancellationToken = default
+ )
+ {
+ var resp = await SendSKTABLEListeningPortListAsync(
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ var portList = resp.Payload!;
+
+ return portList.Where(static p => !p.IsUnused).ToArray();
+ }
+
+ public async ValueTask<IReadOnlyList<SkStackUdpPortHandle>> GetUnusedUdpPortHandleListAsync(
+ CancellationToken cancellationToken = default
+ )
+ {
+ var resp = await SendSKTABLEListeningPortListAsync(
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ var portList = resp.Payload!;
+
+ return portList.Where(static p => p.IsUnused).Select(static p => p.Handle).ToArray();
+ }
+
+ public async ValueTask<SkStackUdpPort> PrepareUdpPortAsync(
+ int port,
+ CancellationToken cancellationToken = default
+ )
+ {
+ static bool TryFindPort(
+ IReadOnlyList<SkStackUdpPort> ports,
+ Predicate<SkStackUdpPort> predicate,
+ out SkStackUdpPort port)
+ {
+ port = default;
+
+ foreach (var p in ports) {
+ if (predicate(p)) {
+ port = p;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ var respSKTABLE = await SendSKTABLEListeningPortListAsync(
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+ var listeningPortList = respSKTABLE.Payload!;
+
+ if (TryFindPort(listeningPortList, p => p.Port == port, out var requestedListeningPort))
+ return requestedListeningPort;
+
+ if (TryFindPort(listeningPortList, static p => p.IsUnused, out var unusedPort)) {
+ var (resp, newlyListeningPort) = await SendSKUDPPORTAsync(
+ handle: unusedPort.Handle,
+ port: port,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ return newlyListeningPort;
+ }
+
+ throw new InvalidOperationException("there are no unused port");
+ }
+
+ /// <summary>
+ /// Starts capturing <c>ERXUDP</c> events for the specified port number and
+ /// allocates buffer for reading and writing the received data.
+ /// </summary>
+ /// <param name="port">The port number to start capturing <c>ERXUDP</c> events.</param>
+ /// <seealso cref="ReceiveUdpAsync"/>
+ /// <seealso cref="StopCapturingUdpReceiveEvents"/>
+ public void StartCapturingUdpReceiveEvents(int port)
+ {
+ ThrowIfDisposed();
+
+ SkStackUdpPort.ThrowIfPortNumberIsOutOfRangeOrUnused(port, nameof(port));
+
+ lock (udpReceiveEventPipes) {
+ udpReceiveEventPipes[port] = new Pipe(new PipeOptions()); // TODO: options
+ }
+ }
+
+ /// <summary>
+ /// Stops capturing <c>ERXUDP</c> events for the specified port number.
+ /// </summary>
+ /// <param name="port">The port number to stop capturing <c>ERXUDP</c> events.</param>
+ /// <seealso cref="ReceiveUdpAsync"/>
+ public void StopCapturingUdpReceiveEvents(int port)
+ {
+ ThrowIfDisposed();
+
+ SkStackUdpPort.ThrowIfPortNumberIsOutOfRangeOrUnused(port, nameof(port));
+
+ lock (udpReceiveEventPipes) {
+ udpReceiveEventPipes.Remove(port);
+ }
+ }
+
+ private const int UdpReceiveEventLengthOfRemoteAddress = 16;
+ private const int UdpReceiveEventLengthOfDataLength = sizeof(ushort);
+
+ private ValueTask OnERXUDPAsync(
+ int localPort,
+ IPAddress remoteAddress,
+ ReadOnlySequence<byte> data,
+ int dataLength,
+ SkStackERXUDPDataFormat dataFormat,
+ CancellationToken cancellationToken
+ )
+ {
+ if (!udpReceiveEventPipes.TryGetValue(localPort, out var pipe))
+ // not capturing
+#if SYSTEM_THREADING_TASKS_VALUETASK_COMPLETEDTASK
+ return ValueTask.CompletedTask;
+#else
+ return default;
+#endif
+
+ return OnERXUDPAsyncCore(pipe.Writer, remoteAddress, data, dataLength, dataFormat, cancellationToken);
+
+ static async ValueTask OnERXUDPAsyncCore(
+ PipeWriter writer,
+ IPAddress remoteAddress,
+ ReadOnlySequence<byte> data,
+ int dataLength,
+ SkStackERXUDPDataFormat dataFormat,
+ CancellationToken cancellationToken
+ )
+ {
+ var packetLength = UdpReceiveEventLengthOfRemoteAddress + UdpReceiveEventLengthOfDataLength + dataLength;
+ var memory = writer.GetMemory(dataLength);
+
+ // BYTE[16]: remote address
+ if (!remoteAddress.TryWriteBytes(memory.Span, out var bytesWritten) && bytesWritten != UdpReceiveEventLengthOfRemoteAddress)
+ throw new InvalidOperationException("unexpected format of remote address");
+
+ // UINT16: length of data
+ BinaryPrimitives.WriteUInt16LittleEndian(memory.Span.Slice(UdpReceiveEventLengthOfRemoteAddress), (ushort)dataLength);
+
+ // BYTE[n]: data
+ if (dataFormat == SkStackERXUDPDataFormat.Binary) {
+ data.CopyTo(memory.Span.Slice(UdpReceiveEventLengthOfRemoteAddress + UdpReceiveEventLengthOfDataLength));
+ }
+ else {
+ SkStackTokenParser.ToByteSequence(
+ data,
+ dataLength,
+ memory.Span.Slice(UdpReceiveEventLengthOfRemoteAddress + UdpReceiveEventLengthOfDataLength)
+ );
+ }
+
+ writer.Advance(packetLength);
+
+ var result = await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
+
+ if (result.IsCompleted)
+ return;
+ // throw new InvalidOperationException("writer is completed");
+ }
+ }
+
+ /// <summary>
+ /// Receives UDP data for the port number that has started capturing <c>ERXUDP</c> events.
+ /// </summary>
+ /// <param name="port">The port number to receive UDP data.</param>
+ /// <param name="buffer">The <see cref="IBufferWriter{Byte}"/> that the received UDP data is written to.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+ /// <returns>
+ /// A <see cref="ValueTask{IPAddress}"/> representing the source address of the received UDP data.
+ /// </returns>
+ /// <seealso cref="StartCapturingUdpReceiveEvents"/>
+ /// <seealso cref="StopCapturingUdpReceiveEvents"/>
+ public ValueTask<IPAddress> ReceiveUdpAsync(
+ int port,
+ IBufferWriter<byte> buffer,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (buffer is null)
+ throw new ArgumentNullException(nameof(buffer));
+
+ ThrowIfDisposed();
+
+ SkStackUdpPort.ThrowIfPortNumberIsOutOfRangeOrUnused(port, nameof(port));
+
+ if (!udpReceiveEventPipes.TryGetValue(port, out var pipe))
+ throw new InvalidOperationException($"The port number {port} is not configured to capture receiving events. Call the method `{nameof(StartCapturingUdpReceiveEvents)}` first.");
+
+ return ReceiveUdpAsyncCore(
+ thisClient: this,
+ pipeReader: pipe.Reader,
+ bufferWriter: buffer,
+ eventPollingInterval: receiveUdpPollingInterval,
+ cancellationToken: cancellationToken
+ );
+
+ static async ValueTask<IPAddress> ReceiveUdpAsyncCore(
+ SkStackClient thisClient,
+ PipeReader pipeReader,
+ IBufferWriter<byte> bufferWriter,
+ TimeSpan eventPollingInterval,
+ CancellationToken cancellationToken
+ )
+ {
+ for (; ; ) {
+ if (!pipeReader.TryRead(out var readResult)) {
+ var receiveNotificationalEventResult = await thisClient.ReceiveNotificationalEventAsync(cancellationToken).ConfigureAwait(false);
+
+ if (!receiveNotificationalEventResult.Received)
+ await Task.Delay(eventPollingInterval, cancellationToken).ConfigureAwait(false);
+
+ continue;
+ }
+
+ if (readResult.IsCanceled)
+ throw new InvalidOperationException("pending read was cancelled");
+
+ var receivedDataSequence = readResult.Buffer;
+
+ if (TryReadReceiveResult(ref receivedDataSequence, bufferWriter, out var remoteAddress)) {
+ // advance the buffer to the position where the reading finished
+ pipeReader.AdvanceTo(consumed: receivedDataSequence.Start);
+
+ return remoteAddress;
+ }
+ else {
+ // mark entire buffer as examined to receive the subsequent data
+ pipeReader.AdvanceTo(consumed: readResult.Buffer.Start, examined: readResult.Buffer.End);
+
+ // continue;
+ }
+ }
+ }
+
+ static bool TryReadReceiveResult(
+ ref ReadOnlySequence<byte> receivedDataSequence,
+ IBufferWriter<byte> buffer,
+#if NULL_STATE_STATIC_ANALYSIS_ATTRIBUTES
+ [NotNullWhen(true)]
+#endif
+ out IPAddress? remoteAddress
+ )
+ {
+ remoteAddress = default;
+
+ var reader = new SequenceReader<byte>(receivedDataSequence);
+
+ if (reader.Remaining < UdpReceiveEventLengthOfRemoteAddress + UdpReceiveEventLengthOfDataLength)
+ return false; // need more
+
+ // BYTE[16]: remote address
+ Span<byte> remoteAddressBytes = stackalloc byte[UdpReceiveEventLengthOfRemoteAddress];
+
+ reader.TryCopyTo(remoteAddressBytes);
+ reader.Advance(UdpReceiveEventLengthOfRemoteAddress);
+
+ remoteAddress = new(remoteAddressBytes);
+
+ // UINT16: length of data
+ reader.TryReadLittleEndian(out short signedLengthOfData);
+
+ var dataLength = unchecked((ushort)signedLengthOfData);
+
+ // BYTE[n]: data
+ if (reader.Remaining < dataLength)
+ return false; // need more
+
+ reader.TryCopyTo(buffer.GetSpan(sizeHint: dataLength).Slice(0, dataLength));
+
+ buffer.Advance(dataLength);
+
+ reader.Advance(dataLength);
+
+ receivedDataSequence = reader.GetUnreadSequence();
+
+ return true;
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Logging.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Logging.cs
new file mode 100644
index 0000000..4a4e67d
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Logging.cs
@@ -0,0 +1,18 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using Microsoft.Extensions.Logging;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ internal static readonly EventId EventIdReceivingStatus = new(1, "receiving status");
+ internal static readonly EventId EventIdCommandSequence = new(2, "sent command sequence");
+ internal static readonly EventId EventIdResponseSequence = new(3, "received response sequence");
+
+ internal static readonly EventId EventIdIPEventReceived = new(6, "IP event received");
+ internal static readonly EventId EventIdPanaEventReceived = new(7, "PANA event received");
+ internal static readonly EventId EventIdAribStdT108EventReceived = new(8, "ARIB STD-T108 event received");
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Receiving.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Receiving.cs
new file mode 100644
index 0000000..d409687
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Receiving.cs
@@ -0,0 +1,398 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ private enum ParseSequenceStatus {
+ Initial,
+ Undetermined, // the parsing finished without declaring its state (invalid state)
+ Ignored, // the content of current buffer is a sequence which is not a target for this parser (ex. echoback line)
+ Incomplete, // the content of current buffer is a incomplete sequence to complete parsing
+ Continueing, // the content of current buffer is a complete sequence but needs more data sequence to return final result
+ Completed, // the parsing completed to return final result
+ }
+
+ private class ParseSequenceContext : ISkStackSequenceParserContext {
+ public ReadOnlySequence<byte> UnparsedSequence { get; internal set; }
+ public ParseSequenceStatus Status { get; private set; } = ParseSequenceStatus.Initial;
+
+ public ParseSequenceContext()
+ {
+ }
+
+ public void Update(ReadOnlySequence<byte> unparsedSequence)
+ {
+ UnparsedSequence = unparsedSequence;
+ Status = ParseSequenceStatus.Undetermined;
+ }
+
+ public bool IsConsumed(ReadOnlySequence<byte> sequence)
+ => !sequence.Start.Equals(UnparsedSequence.Start);
+
+ /*
+ * ISkStackSequenceParserContext
+ */
+ ISkStackSequenceParserContext ISkStackSequenceParserContext.CreateCopy()
+ => (ISkStackSequenceParserContext)MemberwiseClone();
+
+ void ISkStackSequenceParserContext.Continue()
+ => Status = ParseSequenceStatus.Continueing;
+
+ void ISkStackSequenceParserContext.Complete()
+ => Status = ParseSequenceStatus.Completed;
+
+ void ISkStackSequenceParserContext.Complete(SequenceReader<byte> consumedReader)
+ {
+ Status = ParseSequenceStatus.Completed;
+ UnparsedSequence = consumedReader.GetUnreadSequence();
+ }
+
+ void ISkStackSequenceParserContext.Ignore()
+ => Status = ParseSequenceStatus.Ignored;
+
+ void ISkStackSequenceParserContext.SetAsIncomplete()
+ => Status = ParseSequenceStatus.Incomplete;
+
+ void ISkStackSequenceParserContext.SetAsIncomplete(SequenceReader<byte> incompleteReader)
+ {
+ Status = ParseSequenceStatus.Incomplete;
+ UnparsedSequence = incompleteReader.GetUnreadSequence();
+ }
+ }
+
+ private static readonly TimeSpan ReceiveResponseDelayDefault = TimeSpan.FromMilliseconds(10);
+
+ private TimeSpan receiveResponseDelay = ReceiveResponseDelayDefault;
+
+ /// <summary>
+ /// Gets or sets the interval to delay before attempting to receive a subsequent sequence
+ /// if the response sequence currently received is incomplete.
+ /// </summary>
+ public TimeSpan ReceiveResponseDelay {
+ get => receiveResponseDelay;
+ set {
+ if (value <= TimeSpan.Zero)
+ throw new ArgumentOutOfRangeException(message: "must be non-zero positive value", actualValue: value, paramName: nameof(ReceiveResponseDelay));
+
+ receiveResponseDelay = value;
+ }
+ }
+
+ private readonly ParseSequenceContext parseSequenceContext;
+ private SemaphoreSlim streamReaderSemaphore;
+
+#pragma warning disable CA1502 // TODO: refactor
+ private async ValueTask<TResult?> ReadAsync<TArg, TResult>(
+ Func<ISkStackSequenceParserContext, TArg, TResult> parseSequence,
+ TArg arg,
+ SkStackEventHandlerBase? eventHandler,
+ bool processOnlyERXUDP = false,
+ CancellationToken cancellationToken = default,
+ [CallerMemberName] string? callerMemberName = default
+ )
+ {
+#if DEBUG
+ if (parseSequence is null)
+ throw new ArgumentNullException(nameof(parseSequence));
+#endif
+
+ Logger?.LogReceivingStatus($"{callerMemberName} waiting");
+
+ await streamReaderSemaphore.WaitAsync().ConfigureAwait(false);
+
+ Logger?.LogReceivingStatus($"{callerMemberName} entered");
+
+ try {
+ Logger?.LogReceivingStatus($"{callerMemberName} reading");
+
+ for (; ; ) {
+ var reparse = parseSequenceContext.Status switch {
+ ParseSequenceStatus.Ignored or ParseSequenceStatus.Continueing => !parseSequenceContext.UnparsedSequence.IsEmpty,
+ _ => false,
+ };
+
+ ReadOnlySequence<byte> buffer;
+ TResult? result = default;
+ IDisposable? scopeReadAndParse = null;
+
+ try {
+ if (reparse) {
+ scopeReadAndParse = Logger?.BeginScope($"{callerMemberName} reparse buffered sequence");
+
+ // reparse previous data sequence
+ buffer = parseSequenceContext.UnparsedSequence;
+ }
+ else {
+ scopeReadAndParse = Logger?.BeginScope($"{callerMemberName} read sequence from stream");
+
+ Logger?.LogReceivingStatus("buffered: ", parseSequenceContext.UnparsedSequence);
+
+ // receive data sequence and parse it
+ var readResult = await streamReader.ReadAsync(cancellationToken).ConfigureAwait(false);
+
+ if (readResult.IsCanceled)
+ throw new OperationCanceledException("canceled");
+
+ buffer = readResult.Buffer;
+ }
+
+ Logger?.LogReceivingStatus("sequence: ", buffer);
+
+ parseSequenceContext.Update(buffer);
+
+ try {
+ // process events which is received until this point
+ var eventProcessed = await ProcessEventsAsync(
+ parseSequenceContext,
+ eventHandler,
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ Logger?.LogReceivingStatus($"status: {parseSequenceContext.Status}");
+
+ if (eventProcessed) {
+ if (processOnlyERXUDP && parseSequenceContext.Status == ParseSequenceStatus.Continueing)
+ (parseSequenceContext as ISkStackSequenceParserContext).Complete(); // reset status as Completed to stop reading
+ }
+ else if (parseSequenceContext.Status != ParseSequenceStatus.Incomplete) {
+ // if buffered data sequence does not contain any events, parse it with the specified parser
+ Logger?.LogReceivingStatus($"parser: {parseSequence.Method.Name} -- {parseSequence.Method}");
+
+ result = parseSequence(parseSequenceContext, arg);
+
+ Logger?.LogReceivingStatus($"parse status: {parseSequenceContext.Status}");
+ }
+ }
+ catch (SkStackUnexpectedResponseException ex) {
+ Logger?.LogReceivingStatus("unexpected response: ", buffer, ex);
+
+ throw;
+ }
+ }
+ finally {
+ scopeReadAndParse?.Dispose();
+ }
+
+ var (markAsExamined, advanceIfConsumed, returnResult, delay) = parseSequenceContext.Status switch {
+ ParseSequenceStatus.Completed => (markAsExamined: true, advanceIfConsumed: true, returnResult: true, delay: default),
+ ParseSequenceStatus.Ignored => (markAsExamined: false, advanceIfConsumed: false, returnResult: true, delay: default),
+ ParseSequenceStatus.Incomplete => (markAsExamined: true, advanceIfConsumed: false, returnResult: false, delay: true),
+ ParseSequenceStatus.Continueing => (markAsExamined: true, advanceIfConsumed: true, returnResult: false, delay: false),
+ ParseSequenceStatus.Undetermined or _ => throw new InvalidOperationException("final status is invalid or remains undetermined"),
+ };
+
+ if (advanceIfConsumed && parseSequenceContext.IsConsumed(buffer)) {
+ // advance the buffer to the position where parsing finished
+ Logger?.LogDebugResponse(buffer.Slice(0, parseSequenceContext.UnparsedSequence.Start), result);
+ streamReader.AdvanceTo(consumed: parseSequenceContext.UnparsedSequence.Start);
+ }
+ else if (markAsExamined) {
+ // mark entire buffer as examined to receive the subsequent data
+ streamReader.AdvanceTo(consumed: buffer.Start, examined: buffer.End);
+ }
+
+ if (returnResult)
+ return result;
+
+ if (delay)
+ await Task.Delay(receiveResponseDelay).ConfigureAwait(false);
+
+ Logger?.LogReceivingStatus($"{callerMemberName} continue reading");
+ } // for infinite
+ }
+ finally {
+ Logger?.LogReceivingStatus($"{callerMemberName} exited");
+ streamReaderSemaphore.Release();
+ }
+ }
+#pragma warning restore CA1502
+
+ private async ValueTask<SkStackResponse<TPayload>> ReceiveResponseAsync<TPayload>(
+ ReadOnlyMemory<byte> command,
+ SkStackSequenceParser<TPayload?>? parseResponsePayload,
+ SkStackEventHandlerBase? commandEventHandler,
+ SkStackProtocolSyntax syntax,
+ CancellationToken cancellationToken
+ )
+ {
+ Logger?.LogReceivingStatus($"{nameof(ReceiveResponseAsync)} ", command);
+
+ // try read and parse echoback
+ await ReadAsync(
+ parseSequence: ParseEchobackLine,
+ arg: (command, syntax),
+ eventHandler: null,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ var response = new SkStackResponse<TPayload>();
+
+ // read and parse response payload
+ if (parseResponsePayload is not null) {
+ response.Payload = await ReadAsync(
+ parseSequence: static (context, parser) => parser(context),
+ arg: parseResponsePayload, // TODO: syntax
+ eventHandler: null,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+ }
+
+ // read and parse response status line
+ if (syntax.ExpectStatusLine) {
+ (response.Status, response.StatusText) = await ReadAsync(
+ parseSequence: ParseStatusLine,
+ arg: (command, syntax),
+ eventHandler: null,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+ }
+ else {
+ // (SKLL64) if the command which is not defined its status, always treat it as success
+ response.Status = SkStackResponseStatus.Ok;
+ }
+
+ if (commandEventHandler is not null && commandEventHandler.DoContinueHandlingEvents(response.Status)) {
+ const int ParseSequenceEmptyResult = default;
+
+ Logger?.LogReceivingStatus($"{nameof(ReceiveResponseAsync)} {commandEventHandler.GetType().Name}");
+
+ await ReadAsync(
+ parseSequence: static (context, handler) => { handler.ProcessSubsequentEvent(context); return ParseSequenceEmptyResult; },
+ arg: commandEventHandler,
+ eventHandler: commandEventHandler,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+ }
+
+ return response;
+ }
+
+ internal readonly struct ReceiveNotificationalEventResult {
+ public static readonly ReceiveNotificationalEventResult NotReceivedResult = new(~default(int));
+ public static readonly ReceiveNotificationalEventResult ReceivedResult = default;
+
+ public bool Received => value == ReceivedResult.value;
+
+ private readonly int value;
+
+ private ReceiveNotificationalEventResult(int value)
+ {
+ this.value = value;
+ }
+ }
+
+ internal ValueTask<ReceiveNotificationalEventResult> ReceiveNotificationalEventAsync(
+ CancellationToken cancellationToken
+ )
+ => ReadAsync(
+ parseSequence: static (context, _) => ReceiveNotificationalEventResult.NotReceivedResult,
+ arg: default(int),
+ eventHandler: null,
+ processOnlyERXUDP: true,
+ cancellationToken: cancellationToken
+ );
+
+ private static object? ParseEchobackLine(
+ ISkStackSequenceParserContext context,
+ (ReadOnlyMemory<byte> Command, SkStackProtocolSyntax Syntax) args
+ )
+ {
+ var (command, syntax) = args;
+
+ // SKSENDTO occasionally echoes back the line with only CRLF even if the register SFE is set to 0 (???)
+ if (syntax == SkStackProtocolSyntax.SKSENDTO) {
+ var sksendtoEchobackLineReader = context.CreateReader();
+
+ if (sksendtoEchobackLineReader.IsNext(syntax.EndOfEchobackLine, advancePast: true)) {
+ context.Complete(sksendtoEchobackLineReader);
+ return SkStackClientLoggerExtensions.EchobackLineMarker;
+ }
+ }
+
+ var comm = command.Span;
+ var reader = context.CreateReader();
+
+ if (comm.Length <= reader.Length && !reader.IsNext(comm, advancePast: false)) {
+ context.Ignore(); // echoback line does not start with the command
+ return null;
+ }
+
+ var echobackLineReader = reader;
+
+ if (!reader.TryReadTo(out ReadOnlySequence<byte> echobackLine, delimiter: syntax.EndOfEchobackLine)) {
+ context.SetAsIncomplete(); // end of echoback line is not found
+ return default;
+ }
+
+ if (echobackLine.Length < comm.Length) {
+ context.Ignore();
+ return null;
+ }
+
+ echobackLineReader.Advance(comm.Length); // advance to position right after the command
+
+ if (echobackLineReader.IsNext(SkStack.SP) || echobackLineReader.IsNext(syntax.EndOfEchobackLine)) {
+ context.Complete(reader);
+ return SkStackClientLoggerExtensions.EchobackLineMarker;
+ }
+ else {
+ context.Ignore();
+ return null;
+ }
+ }
+
+ private static (
+ SkStackResponseStatus Status,
+ ReadOnlyMemory<byte> StatusText
+ )
+ ParseStatusLine(
+ ISkStackSequenceParserContext context,
+ (ReadOnlyMemory<byte> Command, SkStackProtocolSyntax Syntax) args
+ )
+ {
+ SkStackResponseStatus status = default;
+ ReadOnlyMemory<byte> statusText = default;
+
+ var (_, syntax) = args;
+
+ var reader = context.CreateReader();
+
+ if (!reader.TryReadTo(out ReadOnlySequence<byte> statusLine, delimiter: syntax.EndOfStatusLine, advancePastDelimiter: true)) {
+ context.SetAsIncomplete();
+ return default;
+ }
+
+ var statusLineReader = new SequenceReader<byte>(statusLine);
+
+ if (statusLineReader.IsNext(SkStackResponseStatusCodes.OK, advancePast: true))
+ status = SkStackResponseStatus.Ok;
+ else if (statusLineReader.IsNext(SkStackResponseStatusCodes.FAIL, advancePast: true))
+ status = SkStackResponseStatus.Fail;
+
+ if (status == default) {
+ // if the line starts with unknown status code, mark entire buffer as consumed to discard buffer
+ // context.Ignore();
+ context.Complete(reader);
+ return default;
+ }
+ else {
+ if (statusLineReader.IsNext(SkStack.SP, advancePast: true))
+ statusText = statusLineReader.GetUnreadSequence().ToArray();
+
+ context.Complete(reader);
+
+ return (status, statusText);
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Sending.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Sending.cs
new file mode 100644
index 0000000..9415be3
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.Sending.cs
@@ -0,0 +1,194 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackClient {
+#pragma warning restore IDE0040
+ /// <summary>Sends a command.</summary>
+ /// <param name="command">The command to be sent.</param>
+ /// <param name="writeArguments">The <see cref="Action{ISkStackCommandLineWriter}"/> for write command arguments to the buffer. Can be <see langword="null"/> if command has no arguments.</param>
+ /// <param name="syntax">The <see cref="SkStackProtocolSyntax"/> that describes the command syntax.</param>
+ /// <param name="throwIfErrorStatus">The <see langword="bool"/> value that specifies whether throw exception if the response status is error.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+ protected internal async ValueTask<SkStackResponse> SendCommandAsync(
+ ReadOnlyMemory<byte> command,
+ Action<ISkStackCommandLineWriter>? writeArguments = null,
+ SkStackProtocolSyntax? syntax = null,
+ bool throwIfErrorStatus = true,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var resp = await SendCommandAsyncCore<SkStackResponse.NullPayload>(
+ command: command,
+ writeArguments: writeArguments,
+ parseResponsePayload: null,
+ commandEventHandler: null,
+ syntax: syntax ?? SkStackProtocolSyntax.Default,
+ throwIfErrorStatus: throwIfErrorStatus,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ return resp;
+ }
+
+ /// <summary>Sends a command.</summary>
+ /// <typeparam name="TPayload">The type of response payload. See <paramref name="parseResponsePayload"/>.</typeparam>
+ /// <param name="command">The command to be sent.</param>
+ /// <param name="writeArguments">The <see cref="Action{ISkStackCommandLineWriter}"/> for write command arguments to the buffer. Can be <see langword="null"/> if command has no arguments.</param>
+ /// <param name="parseResponsePayload">The delegate for parsing the response payload. If <see langword="null"/>, parsing response payload will not be attempted.</param>
+ /// <param name="syntax">The <see cref="SkStackProtocolSyntax"/> that describes the command syntax.</param>
+ /// <param name="throwIfErrorStatus">The <see langword="bool"/> value that specifies whether throw exception if the response status is error.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+ protected internal ValueTask<SkStackResponse<TPayload>> SendCommandAsync<TPayload>(
+ ReadOnlyMemory<byte> command,
+ Action<ISkStackCommandLineWriter>? writeArguments,
+ SkStackSequenceParser<TPayload?> parseResponsePayload,
+ SkStackProtocolSyntax? syntax = null,
+ bool throwIfErrorStatus = true,
+ CancellationToken cancellationToken = default
+ )
+ => SendCommandAsyncCore(
+ command: command,
+ writeArguments: writeArguments,
+ parseResponsePayload: parseResponsePayload ?? throw new ArgumentNullException(nameof(parseResponsePayload)),
+ commandEventHandler: null,
+ syntax: syntax ?? SkStackProtocolSyntax.Default,
+ throwIfErrorStatus: throwIfErrorStatus,
+ cancellationToken: cancellationToken
+ );
+
+ /// <summary>Sends a command.</summary>
+ /// <param name="command">The command to be sent.</param>
+ /// <param name="writeArguments">The <see cref="Action{ISkStackCommandLineWriter}"/> for write command arguments to the buffer. Can be <see langword="null"/> if command has no arguments.</param>
+ /// <param name="commandEventHandler">The <see cref="SkStackEventHandlerBase" /> that handles the events that will occur until the response is received.</param>
+ /// <param name="syntax">The <see cref="SkStackProtocolSyntax"/> that describes the command syntax.</param>
+ /// <param name="throwIfErrorStatus">The <see langword="bool"/> value that specifies whether throw exception if the response status is error.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+ internal async ValueTask<SkStackResponse> SendCommandAsync(
+ ReadOnlyMemory<byte> command,
+ Action<ISkStackCommandLineWriter>? writeArguments,
+ SkStackEventHandlerBase? commandEventHandler,
+ SkStackProtocolSyntax? syntax = null,
+ bool throwIfErrorStatus = true,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var resp = await SendCommandAsyncCore<SkStackResponse.NullPayload>(
+ command: command,
+ writeArguments: writeArguments,
+ parseResponsePayload: null,
+ commandEventHandler: commandEventHandler,
+ syntax: syntax ?? SkStackProtocolSyntax.Default,
+ throwIfErrorStatus: throwIfErrorStatus,
+ cancellationToken: cancellationToken
+ ).ConfigureAwait(false);
+
+ return resp;
+ }
+
+ /// <summary>Sends a command.</summary>
+ /// <typeparam name="TPayload">The type of response payload. See <paramref name="parseResponsePayload"/>.</typeparam>
+ /// <param name="command">The command to be sent.</param>
+ /// <param name="writeArguments">The <see cref="Action{ISkStackCommandLineWriter}"/> for write command arguments to the buffer. Can be <see langword="null"/> if command has no arguments.</param>
+ /// <param name="parseResponsePayload">The delegate for parsing the response payload. If <see langword="null"/>, parsing response payload will not be attempted.</param>
+ /// <param name="commandEventHandler">The <see cref="SkStackEventHandlerBase" /> that handles the events that will occur until the response is received.</param>
+ /// <param name="syntax">The <see cref="SkStackProtocolSyntax"/> that describes the command syntax.</param>
+ /// <param name="throwIfErrorStatus">The <see langword="bool"/> value that specifies whether throw exception if the response status is error.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+ private ValueTask<SkStackResponse<TPayload>> SendCommandAsyncCore<TPayload>(
+ ReadOnlyMemory<byte> command,
+ Action<ISkStackCommandLineWriter>? writeArguments,
+ SkStackSequenceParser<TPayload?>? parseResponsePayload,
+ SkStackEventHandlerBase? commandEventHandler,
+ SkStackProtocolSyntax syntax,
+ bool throwIfErrorStatus,
+ CancellationToken cancellationToken
+ )
+ {
+ ThrowIfDisposed();
+
+ syntax ??= SkStackProtocolSyntax.Default;
+
+ // write command line
+ WriteCommandLine(
+ command,
+ writeArguments,
+ syntax
+ );
+
+ // flush command and receive response
+ return FlushAndReceive(
+ command,
+ parseResponsePayload,
+ commandEventHandler,
+ syntax,
+ throwIfErrorStatus,
+ cancellationToken
+ );
+ }
+
+ private void WriteCommandLine(
+ ReadOnlyMemory<byte> command,
+ Action<ISkStackCommandLineWriter>? writeArguments,
+ SkStackProtocolSyntax syntax
+ )
+ {
+ if (command.IsEmpty)
+ throw new ArgumentException("must be non-empty byte sequence", nameof(command));
+
+ // write command
+ commandLineWriter.Write(command.Span);
+
+ // write arguments
+ if (writeArguments is not null)
+ writeArguments(commandLineWriter);
+
+ // write end of command line
+ if (!syntax.EndOfCommandLine.IsEmpty)
+ // must terminate the SKSENDTO command line without CRLF
+ // ROHM product setting commands line must be terminated with CR instead of CRLF
+ commandLineWriter.Write(syntax.EndOfCommandLine);
+
+ // write command to logger
+ if (Logger is not null && logWriter is not null) {
+ Logger.LogDebugCommand(logWriter.WrittenMemory);
+ logWriter.Clear();
+ }
+ }
+
+ private async ValueTask<SkStackResponse<TPayload>> FlushAndReceive<TPayload>(
+ ReadOnlyMemory<byte> command,
+ SkStackSequenceParser<TPayload?>? parseResponsePayload,
+ SkStackEventHandlerBase? commandEventHandler,
+ SkStackProtocolSyntax syntax,
+ bool throwIfErrorStatus,
+ CancellationToken cancellationToken
+ )
+ {
+ var writerResult = await streamWriter.FlushAsync(cancellationToken).ConfigureAwait(false);
+
+ if (writerResult.IsCompleted)
+ throw new InvalidOperationException("writer is completed");
+
+ var response = await ReceiveResponseAsync(
+ command,
+ parseResponsePayload,
+ commandEventHandler,
+ syntax,
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ if (throwIfErrorStatus)
+ response.ThrowIfErrorStatus(translateException: null);
+
+ return response;
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.cs
new file mode 100644
index 0000000..ad42e56
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClient.cs
@@ -0,0 +1,137 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Buffers;
+using System.IO;
+using System.IO.Pipelines;
+
+using Microsoft.Extensions.Logging;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>
+/// Provides a client implementation that sends SKSTACK-IP commands and receives responses and handles events.
+/// </summary>
+public partial class SkStackClient : IDisposable {
+ private PipeWriter streamWriter;
+ private readonly SkStackCommandLineWriter commandLineWriter;
+ private PipeReader streamReader;
+
+ protected ILogger? Logger { get; }
+
+ private readonly ArrayBufferWriter<byte>? logWriter;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SkStackClient"/> class with specifying the <see cref="Stream"/> for transmitting SKSTACK-IP protocol.
+ /// </summary>
+ /// <param name="stream">
+ /// The data stream for transmitting SKSTACK-IP protocol.
+ /// </param>
+ /// <param name="leaveStreamOpen">
+ /// A <see langworkd="bool"/> value specifying whether the <paramref name="stream"/> should be left open or not when disposing instance.
+ /// </param>
+ /// <param name="erxudpDataFormat">
+ /// A value that specifies the format of the data part received in the event <c>ERXUDP</c>. See <see cref="SkStackERXUDPDataFormat"/>.
+ /// </param>
+ /// <param name="logger">The <see cref="ILogger"/> to report the situation.</param>
+ public SkStackClient(
+ Stream stream,
+ bool leaveStreamOpen = true,
+ SkStackERXUDPDataFormat erxudpDataFormat = default,
+ ILogger? logger = null
+ )
+ : this(
+ sender: PipeWriter.Create(
+ ValidateStream(stream, nameof(stream)),
+ new(leaveOpen: leaveStreamOpen, minimumBufferSize: 64)
+ ),
+ receiver: PipeReader.Create(
+ stream,
+ new(leaveOpen: leaveStreamOpen, bufferSize: 1024, minimumReadSize: 256)
+ ),
+ erxudpDataFormat: erxudpDataFormat,
+ logger: logger
+ )
+ {
+ }
+
+ private static Stream ValidateStream(Stream stream, string paramNameOfStream)
+ {
+ if (stream is null)
+ throw new ArgumentNullException(paramName: paramNameOfStream);
+ if (!stream.CanRead)
+ throw new ArgumentException(message: $"{nameof(stream)} must be readable stream", paramName: paramNameOfStream);
+ if (!stream.CanWrite)
+ throw new ArgumentException(message: $"{nameof(stream)} must be writable stream", paramName: paramNameOfStream);
+
+ return stream;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SkStackClient"/> class with specifying the <see cref="PipeWriter"/> and <see cref="PipeReader"/>.
+ /// </summary>
+ /// <param name="sender">
+ /// A <see cref="PipeWriter"/> for sending SKSTACK-IP protocol commands.
+ /// </param>
+ /// <param name="receiver">
+ /// A <see cref="PipeReader"/> for receiving SKSTACK-IP protocol responses.
+ /// </param>
+ /// <param name="erxudpDataFormat">
+ /// A value that specifies the format of the data part received in the event <c>ERXUDP</c>. See <see cref="SkStackERXUDPDataFormat"/>.
+ /// </param>
+ /// <param name="logger">The <see cref="ILogger"/> to report the situation.</param>
+ public SkStackClient(
+ PipeWriter sender,
+ PipeReader receiver,
+ SkStackERXUDPDataFormat erxudpDataFormat = default,
+ ILogger? logger = null
+ )
+ {
+ streamReader = receiver ?? throw new ArgumentNullException(nameof(receiver));
+ streamWriter = sender ?? throw new ArgumentNullException(nameof(sender));
+ this.erxudpDataFormat = ValidateERXUDPDataFormat(erxudpDataFormat, nameof(erxudpDataFormat));
+ Logger = logger;
+
+ if (Logger is not null && Logger.IsCommandLoggingEnabled()) {
+ logWriter = new ArrayBufferWriter<byte>(initialCapacity: 64);
+
+ commandLineWriter = new(streamWriter, logWriter);
+ }
+ else {
+ commandLineWriter = new(streamWriter, null);
+ }
+
+ parseSequenceContext = new ParseSequenceContext();
+ streamReaderSemaphore = new(initialCount: 1, maxCount: 1);
+
+ StartCapturingUdpReceiveEvents(SkStackKnownPortNumbers.EchonetLite);
+ }
+
+ protected void ThrowIfDisposed()
+ {
+ if (streamWriter is null)
+ throw new ObjectDisposedException(GetType().FullName);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing) {
+ streamWriter?.Complete();
+ streamWriter = null!;
+
+ streamReader?.Complete();
+ streamReader = null!;
+
+ streamReaderSemaphore?.Dispose();
+ streamReaderSemaphore = null!;
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClientLoggerExtensions.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClientLoggerExtensions.cs
new file mode 100644
index 0000000..33dfb30
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackClientLoggerExtensions.cs
@@ -0,0 +1,200 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Buffers;
+
+using Microsoft.Extensions.Logging;
+
+using Smdn.Net.SkStackIP.Protocol;
+using Smdn.Text.Unicode.ControlPictures;
+
+namespace Smdn.Net.SkStackIP;
+
+internal static class SkStackClientLoggerExtensions {
+ private const string PrefixCommand = "↦ ";
+ private const string PrefixResponse = "↤ ";
+ private const string PrefixEchoback = "↩ ";
+
+ private const LogLevel LogLevelReceivingStatusDefault = LogLevel.Trace;
+
+ public static void LogReceivingStatus(this ILogger logger, string prefix, ReadOnlyMemory<byte> command, Exception? exception = null)
+ {
+ var level = exception is null ? LogLevelReceivingStatusDefault : LogLevel.Error;
+
+ if (!logger.IsEnabled(level))
+ return;
+
+ logger.Log(
+ level,
+ SkStackClient.EventIdReceivingStatus,
+ exception,
+ "{Prefix}{Command}",
+ prefix,
+ command.Span.ToControlCharsPicturizedString()
+ );
+ }
+
+ public static void LogReceivingStatus(this ILogger logger, string prefix, ReadOnlySequence<byte> sequence, Exception? exception = null)
+ {
+ var level = exception is null ? LogLevelReceivingStatusDefault : LogLevel.Error;
+
+ if (!logger.IsEnabled(level))
+ return;
+
+ logger.Log(
+ level,
+ SkStackClient.EventIdReceivingStatus,
+ exception,
+ "{Prefix}{Sequence}",
+ prefix,
+ sequence.ToControlCharsPicturizedString()
+ );
+ }
+
+ public static void LogReceivingStatus(this ILogger logger, string message, Exception? exception = null)
+ {
+ var level = exception is null ? LogLevelReceivingStatusDefault : LogLevel.Error;
+
+ if (!logger.IsEnabled(level))
+ return;
+
+ logger.Log(
+ level,
+ SkStackClient.EventIdReceivingStatus,
+ exception,
+ "{Message}",
+ message
+ );
+ }
+
+ private const LogLevel LogLevelCommand = LogLevel.Debug;
+
+ public static bool IsCommandLoggingEnabled(this ILogger logger)
+ => logger.IsEnabled(LogLevelCommand);
+
+ public static void LogDebugCommand(this ILogger logger, ReadOnlyMemory<byte> sequence)
+ {
+ if (!logger.IsEnabled(LogLevelCommand))
+ return;
+
+ logger.Log(
+ LogLevelCommand,
+ SkStackClient.EventIdCommandSequence,
+ "{Prefix}{Sequence}",
+ PrefixCommand,
+ sequence.Span.ToControlCharsPicturizedString()
+ );
+ }
+
+ public static readonly object EchobackLineMarker = new();
+
+ private const LogLevel LogLevelResponse = LogLevel.Debug;
+
+ public static void LogDebugResponse(this ILogger logger, ReadOnlySequence<byte> sequence, object? marker)
+ {
+ if (!logger.IsEnabled(LogLevelResponse))
+ return;
+
+ logger.Log(
+ LogLevelResponse,
+ SkStackClient.EventIdResponseSequence,
+ "{Prefix}{Sequence}",
+ ReferenceEquals(marker, EchobackLineMarker) ? PrefixEchoback : PrefixResponse,
+ sequence.ToControlCharsPicturizedString()
+ );
+ }
+
+ public static void LogInfoIPEventReceived(this ILogger logger, SkStackEvent ev)
+ {
+ const LogLevel Level = LogLevel.Information;
+
+ if (!logger.IsEnabled(Level))
+ return;
+
+ if (ev.Number == SkStackEventNumber.UdpSendCompleted) {
+ logger.Log(
+ Level,
+ SkStackClient.EventIdIPEventReceived,
+ "IPv6: {Number} - {Parameter} (EVENT {NumberInHex:X2}, PARAM {Parameter}, {SenderAddress})",
+ ev.Number,
+ ev.Parameter switch {
+ 0 => "Successful",
+ 1 => "Failed",
+ 2 => "Neighbor Solicitation",
+ _ => "Unknown",
+ },
+ (byte)ev.Number,
+ ev.Parameter,
+ ev.SenderAddress
+ );
+ }
+ else {
+ logger.Log(
+ Level,
+ SkStackClient.EventIdIPEventReceived,
+ "IPv6: {Number} (EVENT {NumberInHex:X2}, {SenderAddress})",
+ ev.Number,
+ (byte)ev.Number,
+ ev.SenderAddress
+ );
+ }
+ }
+
+ public static void LogInfoIPEventReceived(this ILogger logger, SkStackUdpReceiveEvent erxudp, ReadOnlySequence<byte> erxudpData)
+ {
+ const LogLevel Level = LogLevel.Information;
+
+ if (!logger.IsEnabled(Level))
+ return;
+
+ logger.Log(
+ Level,
+ SkStackClient.EventIdIPEventReceived,
+ "{Prefix}: {LocalEndPoint}←{RemoteEndPoint} {RemoteLinkLocalAddress} (secured: {IsSecured}, length: {Length})",
+ erxudp.LocalEndPoint.Port switch {
+ SkStackKnownPortNumbers.EchonetLite => "ECHONET Lite/IPv6",
+ SkStackKnownPortNumbers.Pana => "PANA/IPv6",
+ _ => "IPv6",
+ },
+ erxudp.LocalEndPoint,
+ erxudp.RemoteEndPoint,
+ erxudp.RemoteLinkLocalAddress,
+ erxudp.IsSecured,
+ erxudpData.Length
+ );
+ }
+
+ public static void LogInfoPanaEventReceived(this ILogger logger, SkStackEvent ev)
+ {
+ const LogLevel Level = LogLevel.Information;
+
+ if (!logger.IsEnabled(Level))
+ return;
+
+ logger.Log(
+ Level,
+ SkStackClient.EventIdPanaEventReceived,
+ "PANA: {Number} (EVENT {NumberInHex:X2}, {SenderAddress})",
+ ev.Number,
+ (byte)ev.Number,
+ ev.SenderAddress
+ );
+ }
+
+ public static void LogInfoAribStdT108EventReceived(this ILogger logger, SkStackEvent ev)
+ {
+ const LogLevel Level = LogLevel.Information;
+
+ if (!logger.IsEnabled(Level))
+ return;
+
+ logger.Log(
+ Level,
+ SkStackClient.EventIdAribStdT108EventReceived,
+ "ARIB STD-T108: {Number} (EVENT {NumberInHex:X2}, {SenderAddress})",
+ ev.Number,
+ (byte)ev.Number,
+ ev.SenderAddress
+ );
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackCommandNotSupportedException.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackCommandNotSupportedException.cs
new file mode 100644
index 0000000..2d2cc02
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackCommandNotSupportedException.cs
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+#pragma warning disable CA1032
+
+using System;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>Describes the error code <c>ER04</c>.</summary>
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 7. エラーコード' for detailed specifications.</para>
+/// </remarks>
+public class SkStackCommandNotSupportedException : SkStackErrorResponseException {
+ internal SkStackCommandNotSupportedException(
+ SkStackResponse response,
+ SkStackErrorCode errorCode,
+ ReadOnlySpan<byte> errorText,
+ string message
+ )
+ : base(
+ response: response,
+ errorCode: errorCode,
+ errorText: errorText,
+ message: message
+ )
+ {
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackERXUDPDataFormat.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackERXUDPDataFormat.cs
new file mode 100644
index 0000000..d2cd140
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackERXUDPDataFormat.cs
@@ -0,0 +1,15 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+namespace Smdn.Net.SkStackIP;
+
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 3.30. WOPT (プロダクト設定コマンド)' for detailed specifications.</para>
+/// </remarks>
+public enum SkStackERXUDPDataFormat {
+ /// <summary>The data part of <c>ERXUDP</c> is displayed in binary format.</summary>
+ Binary = 0,
+
+ /// <summary>The data part of <c>ERXUDP</c> is displayed in hex ASCII format.</summary>
+ HexAsciiText = 1,
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackErrorCode.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackErrorCode.cs
new file mode 100644
index 0000000..7384310
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackErrorCode.cs
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+namespace Smdn.Net.SkStackIP;
+
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 7. エラーコード' for detailed specifications.</para>
+/// </remarks>
+public enum SkStackErrorCode {
+ Undefined = 0,
+
+ ER01 = 1,
+ ER02 = 2,
+ ER03 = 3,
+ ER04 = 4,
+ ER05 = 5,
+ ER06 = 6,
+ ER07 = 7,
+ ER08 = 8,
+ ER09 = 9,
+ ER10 = 10,
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackErrorResponseException.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackErrorResponseException.cs
new file mode 100644
index 0000000..08ebfe6
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackErrorResponseException.cs
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+#pragma warning disable CA1032
+
+using System;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>
+/// The exception that is thrown when the <see cref="SkStackClient"/> received an error response.
+/// </summary>
+public class SkStackErrorResponseException : SkStackResponseException {
+ /// <summary>
+ /// Gets the <see cref="SkStackResponse"/> that caused the exception.
+ /// </summary>
+ public SkStackResponse Response { get; }
+
+ /// <summary>
+ /// Gets the <see cref="SkStackErrorCode"/> that caused the exception.
+ /// </summary>
+ public SkStackErrorCode ErrorCode { get; }
+
+ /// <summary>
+ /// Gets the <see langword="string"/> that describes the reason of the error.
+ /// </summary>
+ public string ErrorText { get; }
+
+ internal SkStackErrorResponseException(
+ SkStackResponse response,
+ SkStackErrorCode errorCode,
+ ReadOnlySpan<byte> errorText,
+ string message,
+ Exception? innerException = null
+ )
+ : base(
+ message: errorText.IsEmpty
+ ? $"{message} [{errorCode}]"
+ : $"{message} [{errorCode}] \"{SkStack.GetString(errorText)}\"",
+ innerException: innerException
+ )
+ {
+ Response = response;
+ ErrorCode = errorCode;
+ ErrorText = SkStack.GetString(errorText);
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackEventArgs.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackEventArgs.cs
new file mode 100644
index 0000000..78686fe
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackEventArgs.cs
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.Net;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>
+/// Provides data for the events <see cref="SkStackClient.WokeUp"/> and <see cref="SkStackClient.Slept"/>.
+/// </summary>
+public class SkStackEventArgs : EventArgs {
+ private protected IPAddress? SenderAddress { get; }
+
+ /// <summary>
+ /// Gets the <see cref="SkStackEventNumber"/> that represents the event that occurred.
+ /// </summary>
+ public SkStackEventNumber EventNumber { get; }
+
+ internal SkStackEventArgs(SkStackEvent baseEvent)
+ {
+ EventNumber = baseEvent.Number;
+ SenderAddress = baseEvent.Number switch {
+ SkStackEventNumber.Undefined => null,
+ SkStackEventNumber.WakeupSignalReceived => null,
+ _ => baseEvent.SenderAddress ?? throw new InvalidOperationException($"{nameof(baseEvent.SenderAddress)} must not be null"),
+ };
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackEventNumber.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackEventNumber.cs
new file mode 100644
index 0000000..6a90170
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackEventNumber.cs
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+namespace Smdn.Net.SkStackIP;
+
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 4.8. EVENT' for detailed specifications.</para>
+/// </remarks>
+public enum SkStackEventNumber : byte {
+ Undefined = 0x00,
+
+ NeighborSolicitationReceived = 0x01,
+ NeighborAdvertisementReceived = 0x02,
+ EchoRequestReceived = 0x05,
+ EnergyDetectScanCompleted = 0x1F,
+ BeaconReceived = 0x20,
+ UdpSendCompleted = 0x21,
+ ActiveScanCompleted = 0x22,
+
+ PanaSessionEstablishmentError = 0x24,
+ PanaSessionEstablishmentCompleted = 0x25,
+ PanaSessionTerminationRequestReceived = 0x26,
+ PanaSessionTerminationCompleted = 0x27,
+ PanaSessionTerminationTimedOut = 0x28,
+ PanaSessionExpired = 0x29,
+
+ /// <seealso href="https://www.arib.or.jp/kikaku/kikaku_tushin/desc/std-t108.html">[ARIB STD-T108] 920MHz帯テレメータ用、テレコントロール用及びデータ伝送用無線設備</seealso>
+ /// <seealso href="http://www.arib.or.jp/english/html/overview/doc/5-STD-T108v1_3-E1.pdf">[ARIB STD-T108] 920MHz帯テレメータ用、テレコントロール用及びデータ伝送用無線設備 (PDF)</seealso>
+ TransmissionTimeControlLimitationActivated = 0x32,
+
+ /// <seealso href="https://www.arib.or.jp/kikaku/kikaku_tushin/desc/std-t108.html">[ARIB STD-T108] 920MHz帯テレメータ用、テレコントロール用及びデータ伝送用無線設備</seealso>
+ /// <seealso href="http://www.arib.or.jp/english/html/overview/doc/5-STD-T108v1_3-E1.pdf">[ARIB STD-T108] 920MHz帯テレメータ用、テレコントロール用及びデータ伝送用無線設備 (PDF)</seealso>
+ TransmissionTimeControlLimitationDeactivated = 0x33,
+
+ /// <summary><c>SKDSLEEP</c>; Wake-up signal received.</summary>
+ /// <remarks>This event is not clearly documented.</remarks>
+ WakeupSignalReceived = 0xC0,
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackFlashMemoryIOException.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackFlashMemoryIOException.cs
new file mode 100644
index 0000000..46ea975
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackFlashMemoryIOException.cs
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+#pragma warning disable CA1032
+
+using System;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>Describes the error code <c>ER10</c> of <c>SKSAVE</c> or <c>SKLOAD</c> response.</summary>
+/// <remarks>
+/// <para>See below for detailed specifications.</para>
+/// <list type="bullet">
+/// <item><description>'BP35A1コマンドリファレンス 3.20. SKSAVE'</description></item>
+/// <item><description>'BP35A1コマンドリファレンス 3.21. SKLOAD'</description></item>
+/// <item><description>'BP35A1コマンドリファレンス 7. エラーコード'</description></item>
+/// </list>
+/// </remarks>
+/// <seealso cref="SkStackClient.SendSKSAVEAsync"/>
+/// <seealso cref="SkStackClient.SendSKLOADAsync"/>
+public class SkStackFlashMemoryIOException : SkStackErrorResponseException {
+ internal SkStackFlashMemoryIOException(
+ SkStackResponse response,
+ SkStackErrorCode errorCode,
+ ReadOnlySpan<byte> errorText,
+ string message
+ )
+ : base(
+ response: response,
+ errorCode: errorCode,
+ errorText: errorText,
+ message: message
+ )
+ {
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackFlashMemoryWriteRestriction.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackFlashMemoryWriteRestriction.cs
new file mode 100644
index 0000000..7ba3ec6
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackFlashMemoryWriteRestriction.cs
@@ -0,0 +1,75 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Diagnostics;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>
+/// Provides a restriction to write to device's flash memory.
+/// </summary>
+/// <remarks>
+/// For devices such as the <see href="https://www.rohm.co.jp/products/wireless-communication/specified-low-power-radio-modules/bp35a1-product">ROHM BP35A1</see>,
+/// the number of writes to flash memory is limited up to approximately 10,000.
+/// Be careful not to write unnecessarily to prevent damage to the flash memory.
+/// </remarks>
+/// <see cref="SkStackClient.SaveFlashMemoryAsync"/>
+public abstract class SkStackFlashMemoryWriteRestriction {
+ /// <summary>
+ /// Create an <see cref="SkStackFlashMemoryWriteRestriction"/> instance that always grants write permission.
+ /// </summary>
+ /// <remarks>
+ /// Be careful not to exceed the write limit, as the instance returned by this method will grant all write requests.
+ /// </remarks>
+ public static SkStackFlashMemoryWriteRestriction DangerousCreateAlwaysGrant()
+ => new AlwaysGrantSkStackFlashMemoryWriteRestriction();
+
+ private sealed class AlwaysGrantSkStackFlashMemoryWriteRestriction : SkStackFlashMemoryWriteRestriction {
+ protected internal override bool IsRestricted() => false;
+ }
+
+ /// <summary>
+ /// Create an <see cref="SkStackFlashMemoryWriteRestriction"/> instance that grants write permission only if a certain amount of time has elapsed.
+ /// </summary>
+ public static SkStackFlashMemoryWriteRestriction CreateGrantIfElapsed(TimeSpan interval)
+ {
+ if (interval <= TimeSpan.Zero)
+ throw new ArgumentOutOfRangeException(message: "must be non zero positive value", paramName: nameof(interval), actualValue: interval);
+
+ return new GrantIfElapsedSkStackFlashMemoryWriteRestriction(interval);
+ }
+
+ private sealed class GrantIfElapsedSkStackFlashMemoryWriteRestriction : SkStackFlashMemoryWriteRestriction {
+ private readonly TimeSpan interval;
+ private Stopwatch? stopwatch;
+
+ public GrantIfElapsedSkStackFlashMemoryWriteRestriction(TimeSpan interval)
+ {
+ this.interval = interval;
+ }
+
+ protected internal override bool IsRestricted()
+ {
+ const bool Permit = false;
+
+ if (stopwatch is null) {
+ stopwatch = Stopwatch.StartNew();
+
+ return Permit; // permit the initial write
+ }
+
+ if (interval <= stopwatch.Elapsed) {
+ stopwatch.Restart();
+
+ return Permit; // permit if specific interval has elapsed
+ }
+
+ return !Permit;
+ }
+ }
+
+ /*
+ * instance members
+ */
+ protected internal abstract bool IsRestricted();
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackKnownPortNumbers.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackKnownPortNumbers.cs
new file mode 100644
index 0000000..9adf0b7
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackKnownPortNumbers.cs
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+namespace Smdn.Net.SkStackIP;
+
+public static class SkStackKnownPortNumbers {
+ /// <summary>Represents the port number <c>3610</c>, assigned to ECHONET Lite.</summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 5.1. UDP ポート' for detailed specifications.</para>
+ /// </remarks>
+ public const int EchonetLite = 3610;
+
+ /// <summary>Represents the port number <c>716</c>, assigned to PANA.</summary>
+ /// <remarks>
+ /// <para>See below for detailed specifications.</para>
+ /// <list type="bullet">
+ /// <item><description>'BP35A1コマンドリファレンス 5.1. UDP ポート'</description></item>
+ /// <item><description><see href="https://datatracker.ietf.org/doc/html/rfc5191">[RFC5191] Protocol for Carrying Authentication for Network Access (PANA) 6.1. IP and UDP Headers</see></description></item>
+ /// </list>
+ /// </remarks>
+ public const int Pana = 716;
+
+ /// <summary>Represents the port number <c>0</c>, to set to be unused port.</summary>
+ /// <remarks>
+ /// <para>See 'BP35A1コマンドリファレンス 3.19. SKUDPPORT' for detailed specifications.</para>
+ /// </remarks>
+ internal const int SetUnused = 0;
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanDescription.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanDescription.cs
new file mode 100644
index 0000000..3e6b3d5
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanDescription.cs
@@ -0,0 +1,36 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.Net.NetworkInformation;
+
+namespace Smdn.Net.SkStackIP;
+
+public readonly struct SkStackPanDescription {
+ public SkStackChannel Channel { get; }
+ public int ChannelPage { get; }
+ public int Id { get; }
+ public PhysicalAddress MacAddress { get; }
+ public decimal Rssi { get; }
+ [CLSCompliant(false)] public uint PairingId { get; }
+
+ internal SkStackPanDescription(
+ SkStackChannel channel,
+ int channelPage,
+ int id,
+ PhysicalAddress macAddress,
+ decimal rssi,
+ uint pairingId
+ )
+ {
+ Channel = channel;
+ ChannelPage = channelPage;
+ Id = id;
+ MacAddress = macAddress;
+ Rssi = rssi;
+ PairingId = pairingId;
+ }
+
+ public override string ToString()
+ => $"{Channel}, Channel page: {ChannelPage}, PAN ID: 0x{Id:X4}, MAC address: {MacAddress}, Pairing ID: 0x{PairingId:X8}, RSSI: {Rssi:N2} dB";
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanaSessionEstablishmentException.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanaSessionEstablishmentException.cs
new file mode 100644
index 0000000..30a95f0
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanaSessionEstablishmentException.cs
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+#pragma warning disable CA1032
+
+using System;
+using System.Net;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>
+/// The exception that represents an error on the establishment of a PANA session.
+/// </summary>
+/// <seealso cref="SkStackClient.SendSKJOINAsync"/>
+public class SkStackPanaSessionEstablishmentException : SkStackPanaSessionException {
+ internal SkStackPanaSessionEstablishmentException(
+ string message,
+ IPAddress address,
+ SkStackEventNumber eventNumber,
+ Exception? innerException = null
+ )
+ : base(
+ message: message,
+ address: address,
+ eventNumber: eventNumber,
+ innerException: innerException
+ )
+ {
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanaSessionEventArgs.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanaSessionEventArgs.cs
new file mode 100644
index 0000000..b8792b8
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanaSessionEventArgs.cs
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System.Net;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>
+/// <para>Provides data for the following events.</para>
+/// <list type="bullet">
+/// <item><description><see cref="SkStackClient.PanaSessionEstablished"/></description></item>
+/// <item><description><see cref="SkStackClient.PanaSessionTerminated"/></description></item>
+/// <item><description><see cref="SkStackClient.PanaSessionExpired"/></description></item>
+/// </list>
+/// </summary>
+public sealed class SkStackPanaSessionEventArgs : SkStackEventArgs {
+ /// <summary>
+ /// Gets the peer address of the PANA session to which the event occurred.
+ /// </summary>
+ public IPAddress PanaSessionPeerAddress => SenderAddress!;
+
+ internal SkStackPanaSessionEventArgs(SkStackEvent baseEvent)
+ : base(baseEvent)
+ {
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanaSessionException.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanaSessionException.cs
new file mode 100644
index 0000000..1c92420
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanaSessionException.cs
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+#pragma warning disable CA1032
+
+using System;
+using System.Net;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>
+/// The exception that represents an error whitin a PANA session.
+/// </summary>
+public abstract class SkStackPanaSessionException : InvalidOperationException {
+ public IPAddress Address { get; }
+ public SkStackEventNumber EventNumber { get; }
+
+ private protected SkStackPanaSessionException(
+ string message,
+ IPAddress address,
+ SkStackEventNumber eventNumber,
+ Exception? innerException = null
+ )
+ : base(
+ message: message,
+ innerException: innerException
+ )
+ {
+ Address = address;
+ EventNumber = eventNumber;
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanaSessionInfo.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanaSessionInfo.cs
new file mode 100644
index 0000000..fbfec0a
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackPanaSessionInfo.cs
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System.Net;
+using System.Net.NetworkInformation;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>
+/// A class representing information about an established PANA session.
+/// </summary>
+public sealed class SkStackPanaSessionInfo {
+ /// <summary>
+ /// Gets the <see cref="IPAddress"/> representing the local IP address of the PANA session.
+ /// </summary>
+ public IPAddress LocalAddress { get; }
+
+ /// <summary>
+ /// Gets the <see cref="PhysicalAddress"/> representing the local MAC address of the PANA session.
+ /// </summary>
+ public PhysicalAddress LocalMacAddress { get; }
+
+ /// <summary>
+ /// Gets the <see cref="IPAddress"/> representing the peer IP address of the PANA session.
+ /// </summary>
+ /// <seealso cref="SkStackClient.PanaSessionPeerAddress"/>
+ public IPAddress PeerAddress { get; }
+
+ /// <summary>
+ /// Gets the <see cref="PhysicalAddress"/> representing the peer MAC address of the PANA session.
+ /// </summary>
+ public PhysicalAddress PeerMacAddress { get; }
+
+ /// <summary>
+ /// Gets the <see cref="SkStackChannel"/> representing the logical channel number used in the PANA session.
+ /// </summary>
+ public SkStackChannel Channel { get; }
+
+ /// <summary>
+ /// Gets the value representing the ID for the Personal Area Network (PAN) used in the PANA session.
+ /// </summary>
+ public int PanId { get; }
+
+ internal SkStackPanaSessionInfo(
+ IPAddress localAddress,
+ PhysicalAddress localMacAddress,
+ IPAddress peerAddress,
+ PhysicalAddress peerMacAddress,
+ SkStackChannel channel,
+ int panId
+ )
+ {
+ LocalAddress = localAddress;
+ LocalMacAddress = localMacAddress;
+ PeerAddress = peerAddress;
+ PeerMacAddress = peerMacAddress;
+ Channel = channel;
+ PanId = panId;
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackRegister.RegisterEntry.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackRegister.RegisterEntry.cs
new file mode 100644
index 0000000..29c1128
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackRegister.RegisterEntry.cs
@@ -0,0 +1,279 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+#pragma warning disable SA1316
+
+using System;
+using System.Buffers;
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable IDE0040
+partial class SkStackRegister {
+#pragma warning restore IDE0040
+ public abstract class RegisterEntry<TValue> {
+ public string Name { get; }
+ internal ReadOnlyMemory<byte> SREG { get; }
+ public bool IsReadable { get; }
+ public bool IsWritable { get; }
+ public TValue MinValue { get; }
+ public TValue MaxValue { get; }
+
+ internal delegate void WriteSKSREGArgumentFunc(ISkStackCommandLineWriter writer, TValue value);
+ private WriteSKSREGArgumentFunc WriteSKSREGArgument { get; }
+
+ private protected delegate bool ExpectValueFunc(ref SequenceReader<byte> reader, out TValue value);
+ private ExpectValueFunc ExpectValue { get; }
+
+ private protected RegisterEntry(
+ string name,
+ (bool isReadable, bool isWritable) readWrite,
+ (TValue minValue, TValue maxValue) valueRange,
+ WriteSKSREGArgumentFunc writeSKSREGArgument,
+ ExpectValueFunc expectValue
+ )
+ {
+ if (readWrite.isWritable && writeSKSREGArgument is null)
+ throw new ArgumentNullException(nameof(writeSKSREGArgument));
+ if (readWrite.isReadable && expectValue is null)
+ throw new ArgumentNullException(nameof(expectValue));
+
+ Name = name;
+ SREG = SkStack.ToByteSequence(name);
+ IsReadable = readWrite.isReadable;
+ IsWritable = readWrite.isWritable;
+ MinValue = valueRange.minValue;
+ MaxValue = valueRange.maxValue;
+ WriteSKSREGArgument = writeSKSREGArgument;
+ ExpectValue = expectValue;
+ }
+
+ internal virtual void ThrowIfValueIsNotInRange(TValue value, string paramName)
+ {
+ if (!IsInRange(value))
+ throw new ArgumentOutOfRangeException(paramName, value, $"must be in range of {MinValue}~{MaxValue}");
+ }
+
+ private protected abstract bool IsInRange(TValue value);
+
+ internal TValue? ParseESREG(
+ ISkStackSequenceParserContext context
+ )
+ {
+ var reader = context.CreateReader();
+
+ if (
+ SkStackTokenParser.ExpectToken(ref reader, "ESREG"u8) &&
+ ExpectValue(ref reader, out var result) &&
+ SkStackTokenParser.ExpectEndOfLine(ref reader)
+ ) {
+ context.Complete(reader);
+ return result;
+ }
+
+ context.SetAsIncomplete();
+ return default;
+ }
+
+ internal void WriteValueTo(ISkStackCommandLineWriter writer, TValue value)
+ => WriteSKSREGArgument(writer, value);
+ }
+
+ private abstract class ComparableValueRegisterEntry<TValue> :
+ RegisterEntry<TValue>
+ where TValue : IComparable<TValue> {
+ private protected ComparableValueRegisterEntry(
+ string name,
+ (bool isReadable, bool isWritable) readWrite,
+ (TValue minValue, TValue maxValue) valueRange,
+ WriteSKSREGArgumentFunc writeSKSREGArgument,
+ ExpectValueFunc expectValue
+ )
+ : base(
+ name: name,
+ readWrite: readWrite,
+ valueRange: valueRange,
+ writeSKSREGArgument: writeSKSREGArgument,
+ expectValue: expectValue
+ )
+ {
+ }
+
+ private protected override bool IsInRange(TValue value)
+ => MinValue.CompareTo(value) <= 0 && 0 <= MaxValue.CompareTo(value);
+ }
+
+ private sealed class RegisterBinaryEntry : ComparableValueRegisterEntry<bool> {
+ public RegisterBinaryEntry(
+ string name,
+ (bool isReadable, bool isWritable) readWrite
+ )
+ : base(
+ name: name,
+ readWrite: readWrite,
+ valueRange: (minValue: false, maxValue: true),
+ writeSKSREGArgument: static (writer, value) => writer.WriteTokenBinary(value),
+ expectValue: SkStackTokenParser.ExpectBinary
+ )
+ {
+ }
+
+ private protected override bool IsInRange(bool value) => true;
+ }
+
+#if false // unused
+ private sealed class RegisterUINT8Entry : ComparableValueRegisterEntry<byte> {
+ public RegisterUINT8Entry(
+ string name,
+ (bool isReadable, bool isWritable) readWrite,
+ (byte minValue, byte maxValue) valueRange
+ )
+ : base(
+ name: name,
+ readWrite: readWrite,
+ valueRange: valueRange,
+ writeSKSREGArgument: static (writer, value) => writer.WriteTokenUINT8(value),
+ expectValue: SkStackTokenParser.ExpectUINT8
+ )
+ {
+ }
+ }
+#endif
+
+ private sealed class RegisterChannelEntry : ComparableValueRegisterEntry<SkStackChannel> {
+ public RegisterChannelEntry(
+ string name,
+ (bool isReadable, bool isWritable) readWrite,
+ (SkStackChannel minValue, SkStackChannel maxValue) valueRange
+ )
+ : base(
+ name: name,
+ readWrite: readWrite,
+ valueRange: valueRange,
+ writeSKSREGArgument: static (writer, value) => writer.WriteTokenUINT8(value.RegisterS02Value),
+ expectValue: SkStackTokenParser.ExpectCHANNEL
+ )
+ {
+ }
+ }
+
+ private sealed class RegisterUINT16Entry : ComparableValueRegisterEntry<ushort> {
+ public RegisterUINT16Entry(
+ string name,
+ (bool isReadable, bool isWritable) readWrite,
+ (ushort minValue, ushort maxValue) valueRange
+ )
+ : base(
+ name: name,
+ readWrite: readWrite,
+ valueRange: valueRange,
+ writeSKSREGArgument: static (writer, value) => writer.WriteTokenUINT16(value),
+ expectValue: SkStackTokenParser.ExpectUINT16
+ )
+ {
+ }
+ }
+
+ private sealed class RegisterUINT32Entry : ComparableValueRegisterEntry<uint> {
+ public RegisterUINT32Entry(
+ string name,
+ (bool isReadable, bool isWritable) readWrite,
+ (uint minValue, uint maxValue) valueRange
+ )
+ : base(
+ name: name,
+ readWrite: readWrite,
+ valueRange: valueRange,
+ writeSKSREGArgument: static (writer, value) => writer.WriteTokenUINT32(value),
+ expectValue: SkStackTokenParser.ExpectUINT32
+ )
+ {
+ }
+ }
+
+ private sealed class RegisterUINT32SecondsTimeSpanEntry : ComparableValueRegisterEntry<TimeSpan> {
+ public RegisterUINT32SecondsTimeSpanEntry(
+ string name,
+ (bool isReadable, bool isWritable) readWrite,
+ (uint minValue, uint maxValue) valueRange
+ )
+ : base(
+ name: name,
+ readWrite: readWrite,
+ valueRange: (
+ minValue: TimeSpan.FromSeconds(valueRange.minValue),
+ maxValue: TimeSpan.FromSeconds(valueRange.maxValue)
+ ),
+ writeSKSREGArgument: static (writer, value) => writer.WriteTokenUINT32((uint)value.TotalSeconds),
+ expectValue: ExpectValue
+ )
+ {
+ }
+
+ private static bool ExpectValue(
+ ref SequenceReader<byte> reader,
+ out TimeSpan value
+ )
+ {
+ value = default;
+
+ if (SkStackTokenParser.ExpectUINT32(ref reader, out var seconds)) {
+ value = TimeSpan.FromSeconds(seconds);
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ private sealed class RegisterUINT64Entry : ComparableValueRegisterEntry<ulong> {
+ public RegisterUINT64Entry(
+ string name,
+ (bool isReadable, bool isWritable) readWrite,
+ (uint minValue, uint maxValue) valueRange
+ )
+ : base(
+ name: name,
+ readWrite: readWrite,
+ valueRange: valueRange,
+ writeSKSREGArgument: static (writer, value) => writer.WriteTokenUINT64(value),
+ expectValue: SkStackTokenParser.ExpectUINT64
+ )
+ {
+ }
+ }
+
+ private sealed class RegisterCHARArrayEntry : RegisterEntry<ReadOnlyMemory<byte>> {
+ private readonly int minLength;
+ private readonly int maxLength;
+
+ public RegisterCHARArrayEntry(
+ string name,
+ (bool isReadable, bool isWritable) readWrite,
+ int minLength,
+ int maxLength
+ )
+ : base(
+ name: name,
+ readWrite: readWrite,
+ valueRange: default,
+ writeSKSREGArgument: static (writer, value) => writer.WriteToken(value.Span),
+ expectValue: SkStackTokenParser.ExpectCharArray
+ )
+ {
+ this.minLength = minLength;
+ this.maxLength = maxLength;
+ }
+
+ private protected override bool IsInRange(ReadOnlyMemory<byte> value) => throw new NotImplementedException();
+
+ internal override void ThrowIfValueIsNotInRange(ReadOnlyMemory<byte> value, string paramName)
+ {
+ if (value.IsEmpty)
+ throw new ArgumentException("must be non-empty value", paramName);
+ if (!(minLength <= value.Length && value.Length <= maxLength))
+ throw new ArgumentOutOfRangeException(paramName, value, $"length of {paramName} must be in range of {minLength}~{maxLength}");
+ }
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackRegister.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackRegister.cs
new file mode 100644
index 0000000..efe3d73
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackRegister.cs
@@ -0,0 +1,75 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 3.1. SKSREG' for detailed specifications.</para>
+/// </remarks>
+public static partial class SkStackRegister {
+ private static readonly (bool IsReadable, bool IsWritable) RW = (IsReadable: true, IsWritable: true);
+ private static readonly (bool IsReadable, bool IsWritable) R = (IsReadable: true, IsWritable: false);
+
+ public static RegisterEntry<SkStackChannel> S02 { get; } = new RegisterChannelEntry(name: nameof(S02), readWrite: RW, valueRange: (minValue: SkStackChannel.Channel33, maxValue: SkStackChannel.Channel60));
+ [CLSCompliant(false)]
+ public static RegisterEntry<ushort> S03 { get; } = new RegisterUINT16Entry(name: nameof(S03), readWrite: RW, valueRange: (minValue: 0x0000, maxValue: 0xFFFF));
+ [CLSCompliant(false)]
+ public static RegisterEntry<uint> S07 { get; } = new RegisterUINT32Entry(name: nameof(S07), readWrite: R, valueRange: default);
+ public static RegisterEntry<ReadOnlyMemory<byte>> S0A { get; } = new RegisterCHARArrayEntry(name: nameof(S0A), readWrite: RW, minLength: 8, maxLength: 8);
+ public static RegisterEntry<bool> S15 { get; } = new RegisterBinaryEntry(name: nameof(S15), readWrite: RW);
+ [CLSCompliant(false)]
+ public static RegisterEntry<TimeSpan> S16 { get; } = new RegisterUINT32SecondsTimeSpanEntry(name: nameof(S16), readWrite: RW, valueRange: (minValue: 0x_0000_003C, maxValue: 0x_FFFF_FFFF));
+ public static RegisterEntry<bool> S17 { get; } = new RegisterBinaryEntry(name: nameof(S17), readWrite: RW);
+ public static RegisterEntry<bool> SA0 { get; } = new RegisterBinaryEntry(name: nameof(SA0), readWrite: RW);
+ public static RegisterEntry<bool> SA1 { get; } = new RegisterBinaryEntry(name: nameof(SA1), readWrite: RW);
+ public static RegisterEntry<bool> SFB { get; } = new RegisterBinaryEntry(name: nameof(SFB), readWrite: R);
+ [CLSCompliant(false)]
+ public static RegisterEntry<ulong> SFD { get; } = new RegisterUINT64Entry(name: nameof(SFD), readWrite: R, valueRange: default);
+ public static RegisterEntry<bool> SFE { get; } = new RegisterBinaryEntry(name: nameof(SFE), readWrite: RW);
+ public static RegisterEntry<bool> SFF { get; } = new RegisterBinaryEntry(name: nameof(SFF), readWrite: RW);
+
+ /*
+ * alias of SXX
+ */
+
+ /// <remarks>This property is an alias for the register number <see cref="S02"/>.</remarks>
+ public static RegisterEntry<SkStackChannel> Channel => S02;
+
+ /// <remarks>This property is an alias for the register number <see cref="S03"/>.</remarks>
+ [CLSCompliant(false)] public static RegisterEntry<ushort> PanId => S03;
+
+ /// <remarks>This property is an alias for the register number <see cref="S07"/>.</remarks>
+ [CLSCompliant(false)] public static RegisterEntry<uint> FrameCounter => S07;
+
+ /// <remarks>This property is an alias for the register number <see cref="S0A"/>.</remarks>
+ public static RegisterEntry<ReadOnlyMemory<byte>> PairingId => S0A;
+
+ /// <remarks>This property is an alias for the register number <see cref="S15"/>.</remarks>
+ public static RegisterEntry<bool> RespondBeaconRequest => S15;
+
+ /// <remarks>This property is an alias for the register number <see cref="S16"/>.</remarks>
+ [CLSCompliant(false)] public static RegisterEntry<TimeSpan> PanaSessionLifetimeInSeconds => S16;
+
+ /// <remarks>This property is an alias for the register number <see cref="S17"/>.</remarks>
+ public static RegisterEntry<bool> EnableAutoReauthentication => S17;
+
+ /// <remarks>This property is an alias for the register number <see cref="SA0"/>.</remarks>
+ public static RegisterEntry<bool> EncryptIPMulticast => SA0;
+
+ /// <remarks>This property is an alias for the register number <see cref="SA1"/>.</remarks>
+ public static RegisterEntry<bool> AcceptIcmpEcho => SA1;
+
+ /// <remarks>This property is an alias for the register number <see cref="SFB"/>.</remarks>
+ public static RegisterEntry<bool> IsSendingRestricted => SFB;
+
+ /// <remarks>This property is an alias for the register number <see cref="SFD"/>.</remarks>
+ [CLSCompliant(false)] public static RegisterEntry<ulong> AccumulatedSendTimeInMilliseconds => SFD;
+
+ /// <remarks>This property is an alias for the register number <see cref="SFE"/>.</remarks>
+ public static RegisterEntry<bool> EnableEchoback => SFE;
+
+ /// <remarks>This property is an alias for the register number <see cref="SFF"/>.</remarks>
+ public static RegisterEntry<bool> EnableAutoLoad => SFF;
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackResponse.OfTPayload.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackResponse.OfTPayload.cs
new file mode 100644
index 0000000..7b4fff0
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackResponse.OfTPayload.cs
@@ -0,0 +1,12 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+namespace Smdn.Net.SkStackIP;
+
+public class SkStackResponse<TPayload> : SkStackResponse {
+ public TPayload? Payload { get; internal set; }
+
+ internal SkStackResponse()
+ : base()
+ {
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackResponse.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackResponse.cs
new file mode 100644
index 0000000..3e667fe
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackResponse.cs
@@ -0,0 +1,104 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+#if NULL_STATE_STATIC_ANALYSIS_ATTRIBUTES
+using System.Diagnostics.CodeAnalysis;
+#endif
+
+using Smdn.Net.SkStackIP.Protocol;
+
+namespace Smdn.Net.SkStackIP;
+
+public class SkStackResponse {
+ internal readonly struct NullPayload { }
+
+ public bool Success => Status == SkStackResponseStatus.Ok;
+ public SkStackResponseStatus Status { get; internal set; } = SkStackResponseStatus.Undetermined;
+ public ReadOnlyMemory<byte> StatusText { get; internal set; }
+
+ internal SkStackResponse()
+ {
+ }
+
+ private bool TryParseErrorStatus(
+ out SkStackErrorCode errorCode,
+ out ReadOnlyMemory<byte> errorText,
+#if NULL_STATE_STATIC_ANALYSIS_ATTRIBUTES
+ [NotNullWhen(true)]
+#endif
+ out string? errorMessage
+ )
+ {
+ errorCode = default;
+ errorText = default;
+ errorMessage = default;
+
+ if (Status is SkStackResponseStatus.Ok or SkStackResponseStatus.Undetermined)
+ return false; // not error status
+
+ ReadOnlySpan<byte> errorCodeName;
+
+ if (5 <= StatusText.Length && StatusText.Span[4] == SkStack.SP) {
+ errorCodeName = StatusText.Span.Slice(0, 4);
+ errorText = StatusText.Slice(5);
+ }
+ else {
+ errorCodeName = StatusText.Span;
+ errorText = default;
+ }
+
+ errorCode = SkStackErrorCodeNames.ParseErrorCode(errorCodeName);
+
+ errorMessage = errorCode switch {
+ SkStackErrorCode.ER01 => "Reserved error code",
+ SkStackErrorCode.ER02 => "Reserved error code",
+ SkStackErrorCode.ER03 => "Reserved error code",
+ SkStackErrorCode.ER04 => "Unsupported command",
+ SkStackErrorCode.ER05 => "Invalid number of arguments",
+ SkStackErrorCode.ER06 => "Argument out-of-range or invalid format",
+ SkStackErrorCode.ER07 => "Reserved error code",
+ SkStackErrorCode.ER08 => "Reserved error code",
+ SkStackErrorCode.ER09 => "UART input error",
+ SkStackErrorCode.ER10 => "Command completed unsuccessfully",
+ _ => "unknown or undefined error code",
+ };
+
+ return true;
+ }
+
+ internal void ThrowIfErrorStatus(
+ Func<SkStackResponse, SkStackErrorCode, ReadOnlyMemory<byte>, Exception?>? translateException
+ )
+ {
+ if (!TryParseErrorStatus(out var errorCode, out var errorText, out var errorMessage))
+ return;
+
+ var translatedException =
+ translateException?.Invoke(this, errorCode, errorText)
+ ?? errorCode switch {
+ SkStackErrorCode.ER04 => new SkStackCommandNotSupportedException(
+ response: this,
+ errorCode: errorCode,
+ errorText: errorText.Span,
+ message: errorMessage
+ ),
+
+ SkStackErrorCode.ER09 => new SkStackUartIOException(
+ response: this,
+ errorCode: errorCode,
+ errorText: errorText.Span,
+ message: errorMessage
+ ),
+
+ _ => new SkStackErrorResponseException(
+ response: this,
+ errorCode: errorCode,
+ errorText: errorText.Span,
+ message: errorMessage
+ ),
+ };
+
+ throw translatedException;
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackResponseException.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackResponseException.cs
new file mode 100644
index 0000000..5766d77
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackResponseException.cs
@@ -0,0 +1,25 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>
+/// The exception that is thrown when the <see cref="SkStackClient"/> received an invalid or an unexpected response.
+/// </summary>
+public class SkStackResponseException : InvalidOperationException {
+ public SkStackResponseException()
+ : base()
+ {
+ }
+
+ public SkStackResponseException(string message)
+ : base(message: message)
+ {
+ }
+
+ public SkStackResponseException(string message, Exception? innerException = null)
+ : base(message: message, innerException: innerException)
+ {
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackResponseStatus.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackResponseStatus.cs
new file mode 100644
index 0000000..25da5a7
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackResponseStatus.cs
@@ -0,0 +1,10 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+namespace Smdn.Net.SkStackIP;
+
+public enum SkStackResponseStatus {
+ Undetermined = 0, // used as default(SkStackResponseStatus)
+ Ok = +1,
+ Fail = -1,
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUartIOException.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUartIOException.cs
new file mode 100644
index 0000000..4049caa
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUartIOException.cs
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+#pragma warning disable CA1032
+
+using System;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>Describes the error code <c>ER09</c>.</summary>
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 7. エラーコード' for detailed specifications.</para>
+/// </remarks>
+public class SkStackUartIOException : SkStackErrorResponseException {
+ internal SkStackUartIOException(
+ SkStackResponse response,
+ SkStackErrorCode errorCode,
+ ReadOnlySpan<byte> errorText,
+ string message
+ )
+ : base(
+ response: response,
+ errorCode: errorCode,
+ errorText: errorText,
+ message: message
+ )
+ {
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpEncryption.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpEncryption.cs
new file mode 100644
index 0000000..e949cf1
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpEncryption.cs
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+namespace Smdn.Net.SkStackIP;
+
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 3.7. SKSENDTO' for detailed specifications.</para>
+/// </remarks>
+public enum SkStackUdpEncryption : byte {
+ ForcePlainText = 0x00,
+ ForceEncrypt = 0x01,
+ EncryptIfAble = 0x02,
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpPort.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpPort.cs
new file mode 100644
index 0000000..a66a6d5
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpPort.cs
@@ -0,0 +1,55 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <remarks>
+/// <para>See 'BP35A1コマンドリファレンス 5. 待ち受けポート番号' for detailed specifications.</para>
+/// </remarks>
+public readonly struct SkStackUdpPort {
+ internal const int NumberOfPorts = 6;
+ internal static readonly SkStackUdpPortHandle HandleMin = SkStackUdpPortHandle.Handle1;
+ internal static readonly SkStackUdpPortHandle HandleMax = SkStackUdpPortHandle.Handle6;
+
+ public static readonly SkStackUdpPort Null = default; // Null.Handle will be invalid handle
+
+ public SkStackUdpPortHandle Handle { get; }
+ public int Port { get; }
+
+ public bool IsNull => Handle == Null.Handle;
+ public bool IsUnused => Port == 0;
+
+ internal SkStackUdpPort(SkStackUdpPortHandle handle, int port)
+ {
+ Handle = handle;
+ Port = port;
+ }
+
+ public override string ToString()
+ => $"{Port} (#{(byte)Handle})";
+
+ internal static bool IsPortHandleIsOutOfRange(SkStackUdpPortHandle handle)
+ => handle is < SkStackUdpPortHandle.Handle1 or > SkStackUdpPortHandle.Handle6;
+
+ internal static void ThrowIfPortHandleIsOutOfRange(SkStackUdpPortHandle handle, string paramName)
+ {
+ if (IsPortHandleIsOutOfRange(handle))
+ throw new ArgumentOutOfRangeException(paramName: paramName, actualValue: handle, message: $"invalid value of {nameof(SkStackUdpPortHandle)}");
+ }
+
+ internal static void ThrowIfPortNumberIsOutOfRange(int portNumber, string paramName)
+ {
+ if (portNumber is not (>= ushort.MinValue and <= ushort.MaxValue)) // UINT16
+ throw new ArgumentOutOfRangeException(paramName, portNumber, $"must be in range of {ushort.MinValue}~{ushort.MaxValue}");
+ }
+
+ internal static void ThrowIfPortNumberIsOutOfRangeOrUnused(int portNumber, string paramName)
+ {
+ if (portNumber == SkStackKnownPortNumbers.SetUnused)
+ throw new ArgumentOutOfRangeException(paramName, portNumber, $"can not use port number {SkStackKnownPortNumbers.SetUnused}");
+
+ ThrowIfPortNumberIsOutOfRange(portNumber, paramName);
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpPortHandle.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpPortHandle.cs
new file mode 100644
index 0000000..56f9d56
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpPortHandle.cs
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+namespace Smdn.Net.SkStackIP;
+
+/// <remarks>
+/// <para>See below for detailed specifications.</para>
+/// <list type="bullet">
+/// <item><description>'BP35A1コマンドリファレンス 3.7. SKSENDTO'</description></item>
+/// <item><description>'BP35A1コマンドリファレンス 3.19. SKUDPPORT'</description></item>
+/// <item><description>'BP35A1コマンドリファレンス 4.7. EPORT'</description></item>
+/// <item><description>'BP35A1コマンドリファレンス 5. 待ち受けポート番号'</description></item>
+/// </list>
+/// </remarks>
+public enum SkStackUdpPortHandle : byte {
+ None = 0,
+ Handle1 = 1,
+ Handle2 = 2,
+ Handle3 = 3,
+ Handle4 = 4,
+ Handle5 = 5,
+ Handle6 = 6,
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpSendFailedException.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpSendFailedException.cs
new file mode 100644
index 0000000..ce5bfa8
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpSendFailedException.cs
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Net;
+
+namespace Smdn.Net.SkStackIP;
+
+/// <summary>
+/// The exception that is thrown when the <see cref="SkStackClient"/> attempted to perform <c>SKSENDTO</c> and raised <c>EVENT 21</c> with <c>PARAM 1</c> ('Failed to send UDP').
+/// </summary>
+/// <seealso cref="SkStackEventNumber.UdpSendCompleted"/>
+/// <seealso cref="SkStackClient.SendUdpEchonetLiteAsync"/>
+public class SkStackUdpSendFailedException : InvalidOperationException {
+ public SkStackUdpPortHandle PortHandle { get; }
+ public IPAddress? PeerAddress { get; }
+
+ public SkStackUdpSendFailedException()
+ : base()
+ {
+ }
+
+ public SkStackUdpSendFailedException(string message)
+ : base(message: message)
+ {
+ }
+
+ public SkStackUdpSendFailedException(string message, Exception? innerException = null)
+ : base(message: message, innerException: innerException)
+ {
+ }
+
+ public SkStackUdpSendFailedException(
+ string message,
+ SkStackUdpPortHandle portHandle,
+ IPAddress peerAddress,
+ Exception? innerException = null
+ )
+ : base(message: message, innerException: innerException)
+ {
+ PortHandle = portHandle;
+ PeerAddress = peerAddress;
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpSendResultIndeterminateException.cs b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpSendResultIndeterminateException.cs
new file mode 100644
index 0000000..0657e9c
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/Smdn.Net.SkStackIP/SkStackUdpSendResultIndeterminateException.cs
@@ -0,0 +1,30 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+
+namespace Smdn.Net.SkStackIP;
+
+#pragma warning disable CS0419
+/// <summary>
+/// The exception that is thrown when the <c>EVENT 21</c> was not raised or received after performing <c>SKSENDTO</c>.
+/// </summary>
+/// <seealso cref="SkStackClient.SendSKSENDTOAsync"/>
+#pragma warning restore CS0419
+public class SkStackUdpSendResultIndeterminateException : InvalidOperationException {
+ private const string DefaultMessage = "Unable to confirm the send results since the EVENT 21 was not raised or received after performing SKSENDTO.";
+
+ public SkStackUdpSendResultIndeterminateException()
+ : base(message: DefaultMessage)
+ {
+ }
+
+ public SkStackUdpSendResultIndeterminateException(string message)
+ : base(message: message)
+ {
+ }
+
+ public SkStackUdpSendResultIndeterminateException(string message, Exception? innerException = null)
+ : base(message: message, innerException: innerException)
+ {
+ }
+}
diff --git a/src/Smdn.Net.SkStackIP/System.Buffers/SequenceReaderExtensions.cs b/src/Smdn.Net.SkStackIP/System.Buffers/SequenceReaderExtensions.cs
new file mode 100644
index 0000000..0cb12f2
--- /dev/null
+++ b/src/Smdn.Net.SkStackIP/System.Buffers/SequenceReaderExtensions.cs
@@ -0,0 +1,16 @@
+// SPDX-FileCopyrightText: 2021 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+
+using System.Runtime.CompilerServices;
+
+namespace System.Buffers;
+
+internal static class SequenceReaderExtensions {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ReadOnlySequence<T> GetUnreadSequence<T>(this SequenceReader<T> sequenceReader) where T : unmanaged, IEquatable<T>
+#if SYSTEM_BUFFERS_SEQUENCEREADER_UNREADSEQUENCE
+ => sequenceReader.UnreadSequence;
+#else
+ => sequenceReader.Sequence.Slice(sequenceReader.Position);
+#endif
+}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment