Repository navigation
Creating First Plugin
ここでは、画面を持たず生IQを受け取る最小プラグインを作ります。完成版はdocs/samples/SRdeckPlugin.Exampleにあります。
プロジェクト名と出力アセンブリ名をSRdeckPlugin.<Name>にします。
dotnet new classlib -n SRdeckPlugin.Example -f net10.0リポジトリ内ではContractsとSDKをProjectReferenceで追加します。外部リポジトリでは、対応するGitHub Releaseから取得した同一バージョンのNuGetパッケージを使います。
using SRdeckPlugin.Contracts;
using SRdeckPlugin.Sdk;
namespace SRdeckPlugin.Example;
public sealed class ExamplePlugin : PluginModuleBase, IIqBlockConsumer
{
private readonly IqStreamContinuityTracker continuity = new();
private long processedSamples;
public ExamplePlugin() => RegisterStreamReset(continuity.Reset);
public override PluginDescriptor Descriptor { get; } = new(
Id: "example.decoder",
DisplayName: "Example decoder",
Description: "Minimal headless raw-IQ plugin",
PluginVersion: new Version(1, 0),
MinimumHostApiVersion: new Version(1, 0),
MaximumHostApiVersion: new Version(1, 0),
Capabilities: PluginCapabilities.IqConsumer | PluginCapabilities.Headless,
Provider: "Example provider",
License: "License name");
public PluginIqPreferences IqPreferences { get; } = new(4);
public long ProcessedSamples => Interlocked.Read(ref processedSamples);
protected override ValueTask OnStartStreamAsync(
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Interlocked.Exchange(ref processedSamples, 0);
return ValueTask.CompletedTask;
}
public ValueTask ConsumeAsync(
IIqBlockLease block,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (State != PluginLifecycleState.Streaming)
return ValueTask.CompletedTask;
IqStreamTransition transition = continuity.Observe(block.Metadata);
if (transition.RequiresReset)
{
// フィルター、同期器、遅延処理などをリセットする。
}
Interlocked.Add(ref processedSamples, block.Samples.Length);
return ValueTask.CompletedTask;
}
}PluginModuleBaseが公開の非同期ライフサイクルを実装します。方式固有の初期化や終了処理はOnInitializeAsync、OnActivateAsync、OnStartStreamAsync、OnStopStreamAsync、OnDeactivateAsyncなどをoverrideします。
サンプルの有効期間はConsumeAsyncが戻るまでです。別スレッドや後続タスクで使う場合は、その場でプラグイン所有のバッファへコピーします。
-
Idは永続化される安定IDです。^[a-z0-9]+(?:[.-][a-z0-9]+)*$に一致させ、後から再利用やローカライズをしません。 - Host API 1.0を対象にする場合は、最小/最大APIバージョンの範囲に
1.0を含めます。 -
Capabilitiesは、実際に実装した任意インターフェースと一致させます。 - エントリクラスはpublicな引数なしコンストラクターを持つ必要があります。
dotnet build SRdeckPlugin.Example\SRdeckPlugin.Example.csproj -c Release生成されたSRdeckPlugin.Example.dllとプラグイン固有の依存DLLをSRdeck.exeと同じフォルダーへコピーし、SRdeckを再起動します。pluginsサブフォルダーへは置きません。
生IQを使うヘッドレスプラグインの最小プロジェクトは、次の形にできます。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>SRdeckPlugin.Example</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SRdeckPlugin.Contracts\SRdeckPlugin.Contracts.csproj" />
<ProjectReference Include="..\SRdeckPlugin.Sdk\SRdeckPlugin.Sdk.csproj" />
</ItemGroup>
</Project>外部リポジトリではProjectReferenceを、対象ホストと同じ版のPackageReferenceへ置き換えます。UIを持つ場合はnet10.0-windows、UseWPF=true、SRdeckPlugin.Wpf参照を追加します。
この例はIIqBlockConsumerで生IQを受け取りますが、既知周波数の狭帯域方式ではIPluginChannelBlockConsumerを推奨します。
| 選択 | 向いている処理 | 自分で所有するもの |
|---|---|---|
| 生IQ | 広帯域探索、独自チャネル化 | 周波数変換、フィルター、リサンプル |
| 標準チャネルIQ | 既知中心/帯域の復調 | 方式固有の同期、復調、解析 |
標準チャネル例はdocs/samples/SRdeckPlugin.ChannelExampleにあります。同調要求、チャネルID、中心周波数、帯域、出力サンプルレートを一つの定義から作り、周波数オーバーレイとも一致させてください。
ConsumeAsyncでは、次の順序を保つとテストしやすくなります。
- キャンセル、Dispose、Streaming状態を確認する。
- IQメタデータの継続性を確認する。
- 不連続時にフィルター、同期器、フレーム蓄積をリセットする。
- leaseが有効な間に同期処理を行う。
- 後続処理が必要なら所有バッファへコピーする。
- メトリクスを更新し、例外をホストへ漏らさない。
リアルタイムコールバックでファイルI/O、WPF更新、無制限キューへの書き込みを行わないでください。処理が追いつかない場合に古いブロックをどう扱うかを、有界キューとメトリクスで明示します。
設定は不変recordにし、読み込み後に範囲と互換性を正規化します。
public sealed record ExampleSettings(
long FrequencyHz = 145_000_000,
int MaximumHistory = 10_000)
{
public ExampleSettings Normalize() => this with
{
FrequencyHz = Math.Clamp(FrequencyHz, 100_000L, 2_500_000_000L),
MaximumHistory = Math.Clamp(MaximumHistory, 100, 100_000)
};
}設定ストアはプラグインIDで分離され、データディレクトリは通常次になります。
%LOCALAPPDATA%\SRdeck\plugins\example.decoder
旧版にないフィールド、壊れた値、範囲外入力を想定します。受信中の設定変更は、適用、拒否、再同調、DSPリセットのどれになるかを明確にしてください。
WPF UIを追加する場合はIPluginViewProviderを実装し、DescriptorへMainView/SettingsViewを追加します。結果や診断も同じ規則です。
| 機能 | インターフェース | 能力フラグ |
|---|---|---|
| メイン/設定ビュー | IPluginViewProvider |
MainView/SettingsView
|
| プロファイル | IPluginProfileProvider |
対応するProfile能力 |
| ライブ変更 | ILivePluginProfileProvider |
対応するLive能力 |
| 周波数マーカー | IFrequencyOverlayProvider |
FrequencyOverlay |
| 結果通知 | IPluginResultProvider |
ResultPublisher |
| エクスポート | IPluginExportProvider |
Export |
| 診断 | IPluginProcessingDiagnosticsProvider |
対応するDiagnostics能力 |
正確な能力名は現在のContractsを正本とします。UIは幅700 pxへ適応し、DSPからDispatcher経由で一定間隔に更新します。詳細は右ペインUI設計を参照してください。
最低限、次を自動テストします。
- DescriptorのID、API範囲、Capabilities
- publicな引数なしコンストラクター
- Initialize、Activate、Start、Consume、Stop、Deactivate、Dispose
- Stop、Deactivate、Disposeの二重呼び出し
- キャンセルされたStart/Consume
- RATE、中心周波数、ストリーム世代、不連続でリセット
- leaseをコールバック後に保持しない
- 設定の既定値と範囲外正規化
- 例外時の音声、Timer、Task、ファイル、ネイティブ資源解放
既知IQを使う場合は、サンプルレート、中心周波数、信号振幅、期待フレームをテストデータと一緒に記録します。
クリーンなホストフォルダーで次を確認します。
- DLLを置く前はMODEに存在しない。
- DLLと専用依存を置き、再起動すると表示される。
- Initialize/Activateが1回だけ成功する。
- Start/Stopを繰り返せる。
- MODEを別プラグインへ切り替えられる。
- SDR再検出とRATE変更後に再Startできる。
- 終了時にプロセスが残らない。
デバッグビルドだけでなく、最終的なRelease配布物でも繰り返します。
- DLLを
pluginsサブフォルダーへ置く - Descriptor IDを表示名やローカライズ文字列から生成する
- Capabilitiesと実装インターフェースが一致しない
-
ConsumeAsync後もleaseのMemoryを保持する - 周波数変更でDSP状態をリセットしない
- IQコールバックからObservableCollectionを更新する
- StopでバックグラウンドTaskを待たない
- ホスト共有DLLの別版をプラグインへ同梱する
- 開発出力フォルダーの偶然の依存でしか起動確認しない
- 標準チャネルIQ:
docs/samples/SRdeckPlugin.ChannelExample - UI: 右ペインUI設計
- テストとZIP: テストと配布
- API全体: プラグインAPI仕様