forked from HiroyukiSakoh/EchoDotNetLite
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
import Smdn.Net.EchonetLite.RouteB.SkStackIP
- Loading branch information
Showing
10 changed files
with
639 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
src/Smdn.Net.EchonetLite.RouteB.SkStackIP/Smdn.Net.EchonetLite.RouteB.SkStackIP.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<!-- | ||
SPDX-FileCopyrightText: 2024 smdn <smdn@smdn.jp> | ||
SPDX-License-Identifier: MIT | ||
--> | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<PropertyGroup> | ||
<TargetFrameworks>net8.0;net6.0;netstandard2.1</TargetFrameworks> | ||
<VersionPrefix>2.0.0</VersionPrefix> | ||
<VersionSuffix>preview1</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>CS1591;$(NoWarn)</NoWarn> <!-- CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' --> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup Label="metadata"> | ||
<Description> | ||
<![CDATA[Skyley Networksの[SKSTACK IP](https://www.skyley.com/wiki/?SKSTACK+IP+for+HAN)を使用して、 スマート電力量メータとの情報伝達手段である「Bルート」を介したECHONET Lite規格の通信を扱うためのAPIを提供します。 | ||
ECHONET Lite規格における下位通信層に相当する実装である`SkStackRouteBUdpEchonetLiteHandler`クラスをはじめとするAPIを提供します。 | ||
]]> | ||
</Description> | ||
<CopyrightYear>2024</CopyrightYear> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup Label="package properties"> | ||
<PackageTags>SKSTACK;SKSTACK-IP;Route-B;B-Route;smart-meter;smart-energy-meter;$(PackageTags)</PackageTags> | ||
<GenerateNupkgReadmeFileDependsOnTargets>$(GenerateNupkgReadmeFileDependsOnTargets);GenerateReadmeFileContent</GenerateNupkgReadmeFileDependsOnTargets> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> | ||
<PackageReference Include="Smdn.Net.SkStackIP" /> | ||
<ProjectOrPackageReference Include="$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)..\Smdn.Net.EchonetLite.RouteB\Smdn.Net.EchonetLite.RouteB.csproj'))" /> | ||
</ItemGroup> | ||
|
||
<Target Name="GenerateReadmeFileContent"> | ||
<PropertyGroup> | ||
<PackageReadmeFileContent><![CDATA[# $(PackageId) $(PackageVersion) | ||
$(Description) | ||
## 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)). | ||
]]></PackageReadmeFileContent> | ||
</PropertyGroup> | ||
</Target> | ||
</Project> |
9 changes: 9 additions & 0 deletions
9
...kStackIP/Smdn.Net.EchonetLite.RouteB.SkStackIP/ISkStackRouteBEchonetLiteHandlerFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp> | ||
// SPDX-License-Identifier: MIT | ||
using System; | ||
|
||
namespace Smdn.Net.EchonetLite.RouteB.Transport.SkStackIP; | ||
|
||
public interface ISkStackRouteBEchonetLiteHandlerFactory : IRouteBEchonetLiteHandlerFactory { | ||
Action<SkStackRouteBSessionConfiguration>? ConfigureRouteBSessionConfiguration { get; set; } | ||
} |
312 changes: 312 additions & 0 deletions
312
...RouteB.SkStackIP/Smdn.Net.EchonetLite.RouteB.SkStackIP/SkStackRouteBEchonetLiteHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,312 @@ | ||
// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp> | ||
// SPDX-License-Identifier: MIT | ||
|
||
using System; | ||
using System.Buffers; | ||
using System.ComponentModel; | ||
#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE | ||
using System.Diagnostics.CodeAnalysis; | ||
#endif | ||
using System.Net; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
|
||
using Polly; | ||
using Polly.Registry; | ||
|
||
using Smdn.Net.EchonetLite.RouteB.Credentials; | ||
using Smdn.Net.SkStackIP; | ||
|
||
namespace Smdn.Net.EchonetLite.RouteB.Transport.SkStackIP; | ||
|
||
public abstract class SkStackRouteBEchonetLiteHandler : RouteBEchonetLiteHandler { | ||
public static readonly string ResiliencePipelineKeyForSend = nameof(SkStackRouteBEchonetLiteHandler) + "." + nameof(resiliencePipelineSend); | ||
|
||
private SkStackClient? client; | ||
private readonly bool shouldDisposeClient; | ||
private readonly SkStackRouteBSessionConfiguration sessionConfiguration; | ||
private readonly ResiliencePipeline? resiliencePipelineSend; | ||
private SkStackPanaSessionInfo? panaSessionInfo; | ||
private SemaphoreSlim semaphore = new(initialCount: 1, maxCount: 1); | ||
|
||
/// <inheritdoc/> | ||
public override ISynchronizeInvoke? SynchronizingObject { | ||
#if !SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE | ||
#pragma warning disable CS8602 | ||
#endif | ||
get { ThrowIfDisposed(); return client.SynchronizingObject; } | ||
set { ThrowIfDisposed(); client.SynchronizingObject = value; } | ||
#pragma warning restore CS8602 | ||
} | ||
|
||
/// <inheritdoc/> | ||
public override IPAddress? LocalAddress => panaSessionInfo?.LocalAddress; | ||
|
||
/// <inheritdoc/> | ||
public override IPAddress? PeerAddress => panaSessionInfo?.PeerAddress; | ||
|
||
private protected SkStackClient Client { | ||
get { | ||
ThrowIfDisposed(); | ||
|
||
#if !SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE | ||
#pragma warning disable CS8603 | ||
#endif | ||
return client; | ||
#pragma warning restore CS8603 | ||
} | ||
} | ||
|
||
private protected SkStackRouteBEchonetLiteHandler( | ||
SkStackClient client, | ||
SkStackRouteBSessionConfiguration sessionConfiguration, | ||
bool shouldDisposeClient = false, | ||
IServiceProvider? serviceProvider = null // TODO: logger | ||
) | ||
{ | ||
this.client = client ?? throw new ArgumentNullException(nameof(client)); | ||
this.sessionConfiguration = (sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration))).Clone(); // holds the clone to avoid being affected from the changes to the original | ||
this.shouldDisposeClient = shouldDisposeClient; | ||
|
||
_ = serviceProvider?.GetService<ILoggerFactory>(); // TODO | ||
|
||
var resiliencePipelineProvider = serviceProvider?.GetService<ResiliencePipelineProvider<string>>(); | ||
|
||
_ = resiliencePipelineProvider?.TryGetPipeline(ResiliencePipelineKeyForSend, out resiliencePipelineSend); | ||
} | ||
|
||
/// <inheritdoc/> | ||
protected override void Dispose(bool disposing) | ||
{ | ||
base.Dispose(disposing); | ||
|
||
DisposeCore(); | ||
} | ||
|
||
/// <inheritdoc/> | ||
protected override async ValueTask DisposeAsyncCore() | ||
{ | ||
await base.DisposeAsyncCore().ConfigureAwait(false); | ||
|
||
DisposeCore(); | ||
} | ||
|
||
private void DisposeCore() | ||
{ | ||
if (shouldDisposeClient) | ||
client?.Dispose(); | ||
|
||
client = null; | ||
|
||
panaSessionInfo = null; | ||
|
||
semaphore?.Dispose(); | ||
semaphore = null!; | ||
} | ||
|
||
#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE | ||
[MemberNotNull(nameof(client))] | ||
#endif | ||
protected override void ThrowIfDisposed() | ||
{ | ||
#pragma warning disable CA1513 | ||
if (client is null) | ||
throw new ObjectDisposedException(GetType().FullName); | ||
#pragma warning restore CA1513 | ||
|
||
base.ThrowIfDisposed(); | ||
} | ||
|
||
protected override ValueTask ConnectAsyncCore( | ||
IRouteBCredential credential, | ||
CancellationToken cancellationToken | ||
) | ||
{ | ||
#pragma warning disable CA1510 | ||
if (credential is null) | ||
throw new ArgumentNullException(nameof(credential)); | ||
#pragma warning restore CA1510 | ||
|
||
ThrowIfDisposed(); | ||
ThrowIfReceiving(); | ||
|
||
var shouldPerformActiveScan = | ||
!sessionConfiguration.Channel.HasValue || | ||
!sessionConfiguration.PanId.HasValue || | ||
( | ||
sessionConfiguration.PaaMacAddress is null && | ||
sessionConfiguration.PaaAddress is null | ||
); | ||
|
||
// TODO: reduce allocation | ||
var rbidBufferWriter = new ArrayBufferWriter<byte>(initialCapacity: RouteBCredentials.AuthenticationIdLength); | ||
var passwordBufferWriter = new ArrayBufferWriter<byte>(initialCapacity: RouteBCredentials.PasswordLength); | ||
|
||
credential.WriteIdTo(rbidBufferWriter); | ||
credential.WritePasswordTo(passwordBufferWriter); | ||
|
||
if (shouldPerformActiveScan) { | ||
// obtain PAN information by active scan prior to initialization | ||
return Core( | ||
authenticateAsPanaClientAsync: (device, ct) => device.AuthenticateAsPanaClientAsync( | ||
rbid: rbidBufferWriter.WrittenMemory, | ||
password: passwordBufferWriter.WrittenMemory, | ||
scanOptions: sessionConfiguration.ActiveScanOptions, | ||
cancellationToken: ct | ||
) | ||
); | ||
} | ||
else { | ||
var shouldResolvePaaAddress = sessionConfiguration.PaaAddress is null; | ||
|
||
if (shouldResolvePaaAddress) { | ||
return Core( | ||
authenticateAsPanaClientAsync: (device, ct) => device.AuthenticateAsPanaClientAsync( | ||
rbid: rbidBufferWriter.WrittenMemory, | ||
password: passwordBufferWriter.WrittenMemory, | ||
paaMacAddress: sessionConfiguration.PaaMacAddress!, | ||
channel: sessionConfiguration.Channel!.Value, | ||
panId: sessionConfiguration.PanId!.Value, | ||
cancellationToken: ct | ||
) | ||
); | ||
} | ||
else { | ||
return Core( | ||
authenticateAsPanaClientAsync: (device, ct) => device.AuthenticateAsPanaClientAsync( | ||
rbid: rbidBufferWriter.WrittenMemory, | ||
password: passwordBufferWriter.WrittenMemory, | ||
paaAddress: sessionConfiguration.PaaAddress!, | ||
channel: sessionConfiguration.Channel!.Value, | ||
panId: sessionConfiguration.PanId!.Value, | ||
cancellationToken: ct | ||
) | ||
); | ||
} | ||
} | ||
|
||
#if !SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE | ||
#pragma warning disable CS8604 | ||
#endif | ||
async ValueTask Core( | ||
Func<SkStackClient, CancellationToken, ValueTask<SkStackPanaSessionInfo>> authenticateAsPanaClientAsync | ||
) | ||
{ | ||
await PrepareConnectionAsync(cancellationToken).ConfigureAwait(false); | ||
|
||
panaSessionInfo = await authenticateAsPanaClientAsync( | ||
client, | ||
cancellationToken | ||
).ConfigureAwait(false); | ||
} | ||
#pragma warning restore CS8604 | ||
} | ||
|
||
private protected abstract ValueTask PrepareConnectionAsync(CancellationToken cancellationToken); | ||
|
||
protected override async ValueTask DisconnectAsyncCore( | ||
CancellationToken cancellationToken | ||
) | ||
{ | ||
if (client is null) | ||
return; | ||
if (!client.IsPanaSessionAlive) | ||
return; | ||
|
||
_ = await client.TerminatePanaSessionAsync(cancellationToken: default).ConfigureAwait(false); | ||
} | ||
|
||
/// <inheritdoc/> | ||
protected override ValueTask<IPAddress> ReceiveAsyncCore( | ||
IBufferWriter<byte> buffer, | ||
CancellationToken cancellationToken | ||
) | ||
{ | ||
ThrowIfDisposed(); | ||
|
||
return ReceiveEchonetLiteAsync( | ||
buffer: buffer, | ||
cancellationToken: cancellationToken | ||
); | ||
} | ||
|
||
private protected abstract ValueTask<IPAddress> ReceiveEchonetLiteAsync( | ||
IBufferWriter<byte> buffer, | ||
CancellationToken cancellationToken | ||
); | ||
|
||
/// <inheritdoc/> | ||
protected override ValueTask SendAsyncCore( | ||
ReadOnlyMemory<byte> buffer, | ||
CancellationToken cancellationToken | ||
) | ||
{ | ||
ThrowIfDisposed(); | ||
|
||
#if !SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE | ||
#pragma warning disable CS8602, CS8604 | ||
#endif | ||
if (!client.IsPanaSessionAlive) | ||
throw new InvalidOperationException("pana session terminated or expired"); | ||
|
||
return SendToAsyncCore( | ||
remoteAddress: client.PanaSessionPeerAddress, // TODO: multicast | ||
buffer: buffer, | ||
cancellationToken: cancellationToken | ||
); | ||
#pragma warning restore CS8602, CS8604 | ||
} | ||
|
||
/// <inheritdoc/> | ||
protected override ValueTask SendToAsyncCore( | ||
IPAddress remoteAddress, | ||
ReadOnlyMemory<byte> buffer, | ||
CancellationToken cancellationToken | ||
) | ||
{ | ||
#pragma warning disable CA1510 | ||
if (remoteAddress is null) | ||
throw new ArgumentNullException(nameof(remoteAddress)); | ||
#pragma warning restore CA1510 | ||
|
||
ThrowIfDisposed(); | ||
|
||
#if !SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE | ||
#pragma warning disable CS8602 | ||
#endif | ||
if (!client.IsPanaSessionAlive) | ||
throw new InvalidOperationException("pana session terminated or expired"); | ||
if (!client.PanaSessionPeerAddress.Equals(remoteAddress)) | ||
throw new NotSupportedException($"Sending to a specified remote address {remoteAddress} is not supported."); | ||
|
||
return Core(); | ||
|
||
async ValueTask Core() | ||
{ | ||
await semaphore.WaitAsync( | ||
cancellationToken: cancellationToken | ||
).ConfigureAwait(false); | ||
|
||
try { | ||
await SendEchonetLiteAsync( | ||
buffer: buffer, | ||
resiliencePipeline: resiliencePipelineSend, | ||
cancellationToken: cancellationToken | ||
).ConfigureAwait(false); | ||
} | ||
finally { | ||
semaphore.Release(); | ||
} | ||
} | ||
#pragma warning restore CS8602 | ||
} | ||
|
||
private protected abstract ValueTask SendEchonetLiteAsync( | ||
ReadOnlyMemory<byte> buffer, | ||
ResiliencePipeline? resiliencePipeline, | ||
CancellationToken cancellationToken | ||
); | ||
} |
Oops, something went wrong.