Skip to content

Commit

Permalink
import Smdn.Net.EchonetLite.RouteB.SkStackIP
Browse files Browse the repository at this point in the history
  • Loading branch information
smdn committed Apr 5, 2024
1 parent a9b73df commit 9fd1891
Show file tree
Hide file tree
Showing 10 changed files with 639 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/PackageVersion.targets
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ SPDX-License-Identifier: MIT
so use PackageReference@Update instead.
-->
<ItemGroup>
<PackageReference Update="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Update="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Update="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
<PackageReference Update="System.Text.Json" Version="6.0.0" />
<PackageReference Update="Smdn.Net.SkStackIP" Version="[1.0.0,2.0.0)" />
</ItemGroup>

<ItemGroup>
Expand Down
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>
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; }
}
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
);
}
Loading

0 comments on commit 9fd1891

Please sign in to comment.