1. ハードウェアドライバの実装(NI DAQを例に)
ここでは、NI DAQmx APIを使用したIDeviceControllerとIDataCollectorServiceの実装を示します。前提として、NI DAQmxドライバのインストール(NationalInstruments.DAQmx NuGetパッケージ)とデバイスの接続・設定が必要です。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NationalInstruments.DAQmx;
using Microsoft.Extensions.Logging;
using PowerCycling.Domain;
namespace PowerCycling.Infrastructure
{
public class NiDaqDeviceController : IDeviceController
{
private readonly ILogger<NiDaqDeviceController> _logger;
private Task _controlTask;
private bool _isRunning;
public NiDaqDeviceController(ILogger<NiDaqDeviceController> logger) => _logger = logger;
public async Task ConfigureAsync(ParamSetupModel parameters, CancellationToken ct)
{
try
{
_logger.LogInformation("NI DAQデバイス設定開始...");
using var aoTask = new NationalInstruments.DAQmx.Task();
aoTask.AOChannels.CreateVoltageChannel("Dev1/ao0", "Control", 0, 5, AOVoltageUnits.Volts);
aoTask.Control(TaskAction.Commit);
_logger.LogInformation($"出力チャンネル設定: 電流 {parameters.ProjectCurrent:F2} A");
using var tempTask = new NationalInstruments.DAQmx.Task();
tempTask.AOChannels.CreateVoltageChannel("Dev1/ao1", "Temp", 0, 10, AOVoltageUnits.Volts);
tempTask.Control(TaskAction.Commit);
_logger.LogInformation($"コールドプレート温度設定: {parameters.ProjectColdplatTemp:F2} ℃");
await Task.CompletedTask;
}
catch (DaqException ex)
{
_logger.LogError(ex, "NI DAQ設定失敗");
throw;
}
}
public async Task StartAsync(CancellationToken ct)
{
_isRunning = true;
_controlTask = Task.Run(async () =>
{
while (_isRunning && !ct.IsCancellationRequested)
{
await Task.Delay(1000, ct);
_logger.LogInformation("NI DAQデバイス動作中...");
}
}, ct);
await Task.CompletedTask;
}
public async Task PauseAsync(CancellationToken ct)
{
_isRunning = false;
_logger.LogInformation("NI DAQ一時停止");
await Task.CompletedTask;
}
public async Task StopAsync(CancellationToken ct)
{
_isRunning = false;
_logger.LogInformation("NI DAQ停止");
await Task.CompletedTask;
}
public async Task ResumeAsync(CancellationToken ct)
{
if (!_isRunning) await StartAsync(ct);
}
public async Task<bool> IsDeviceConnectedAsync(CancellationToken ct)
{
try
{
var connected = DaqSystem.Local.Devices.Any(d => d.Contains("Dev1"));
_logger.LogInformation($"NI DAQ接続状態: {(connected ? "接続済み" : "未接続")}");
return await Task.FromResult(connected);
}
catch (DaqException ex)
{
_logger.LogError(ex, "接続確認失敗");
return false;
}
}
public async Task<string> GetDeviceStatusAsync(CancellationToken ct) =>
await Task.FromResult(_isRunning ? "Running" : "Stopped");
}
public class NiDaqDataCollectorService : IDataCollectorService
{
private readonly ILogger<NiDaqDataCollectorService> _logger;
private NationalInstruments.DAQmx.Task _aiTask;
public NiDaqDataCollectorService(ILogger<NiDaqDataCollectorService> logger) => _logger = logger;
public async Task InitializeAsync(CancellationToken ct)
{
try
{
_logger.LogInformation("NI DAQデータ収集初期化...");
_aiTask = new NationalInstruments.DAQmx.Task();
_aiTask.AIChannels.CreateVoltageChannel("Dev1/ai0", "VCE", AIVoltageUnits.Volts, 0, 5, AIVoltageUnits.Volts);
_aiTask.AIChannels.CreateThermocoupleChannel("Dev1/ai1", "TVJ", 0, 100, ThermocoupleType.K, AITemperatureUnits.DegreesC);
_aiTask.Timing.ConfigureSampleClock("", 1000, SampleClockActiveEdge.Rising, SampleQuantityMode.ContinuousSamples);
_aiTask.Control(TaskAction.Commit);
_logger.LogInformation("初期化完了");
}
catch (DaqException ex)
{
_logger.LogError(ex, "初期化失敗");
throw;
}
await Task.CompletedTask;
}
public async Task<Dictionary<string, List<double>>> CollectDataAsync(CancellationToken ct)
{
var data = new Dictionary<string, List<double>>
{
{ "VCE", new List<double>() }, { "VCETime", new List<double>() },
{ "TVJ", new List<double>() }, { "TVJTime", new List<double>() }, { "RthJC", new List<double>() }
};
try
{
_logger.LogInformation("データ収集開始...");
using var reader = new AnalogMultiChannelReader(_aiTask.Stream);
double time = 0;
for (int i = 0; i < 100 && !ct.IsCancellationRequested; i++)
{
var samples = await Task.Run(() => reader.ReadSingleSample(), ct);
data["VCE"].Add(samples[0]);
data["VCETime"].Add(time);
data["TVJ"].Add(samples[1]);
data["TVJTime"].Add(time);
data["RthJC"].Add(CalcRth(samples[0], samples[1]));
time += 0.001;
await Task.Delay(1, ct);
}
_logger.LogInformation("データ収集完了");
}
catch (DaqException ex)
{
_logger.LogError(ex, "データ収集失敗");
throw;
}
return data;
}
public async Task<double> GetCurrentValueAsync(string channel, CancellationToken ct)
{
try
{
using var task = new NationalInstruments.DAQmx.Task();
if (channel == "VCE") task.AIChannels.CreateVoltageChannel("Dev1/ai0", channel, AIVoltageUnits.Volts, 0, 5, AIVoltageUnits.Volts);
else if (channel == "TVJ") task.AIChannels.CreateThermocoupleChannel("Dev1/ai1", channel, 0, 100, ThermocoupleType.K, AITemperatureUnits.DegreesC);
else return 0;
using var reader = new AnalogSingleChannelReader(task.Stream);
return await Task.Run(() => reader.ReadSingleSampleDouble(), ct);
}
catch (DaqException ex)
{
_logger.LogError(ex, $"{channel}読み取り失敗");
return 0;
}
}
private double CalcRth(double vce, double tvj) => tvj > 0 ? vce / tvj : 0;
}
}
説明:
ConfigureAsyncで出力チャンネル(電流、温度制御)を設定CollectDataAsyncで1ms間隔でアナログ入力(電圧、温度)を収集GetCurrentValueAsyncで単一チャンネルのリアルタイム値を取得
2. フィッティングアルゴリズムの拡張
指数フィッティングとスプライン補間を追加し、UIから選択可能にします。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MathNet.Numerics;
using MathNet.Numerics.Interpolation;
using Microsoft.Extensions.Logging;
using PowerCycling.Domain;
namespace PowerCycling.Infrastructure
{
public enum FitMethod { Polynomial, Exponential, Spline }
public interface ICurveFittingService
{
Task<List<ChartValue>> ApplyFitAsync(List<ChartValue> data, FitMethod method, int degree, CancellationToken ct);
Task<List<ChartValue>> PreprocessAsync(List<ChartValue> data, CancellationToken ct);
}
public class CurveFittingService : ICurveFittingService
{
private readonly ILogger<CurveFittingService> _logger;
public CurveFittingService(ILogger<CurveFittingService> logger) => _logger = logger;
public async Task<List<ChartValue>> ApplyFitAsync(List<ChartValue> data, FitMethod method, int degree, CancellationToken ct)
{
if (data == null || data.Count < 2) { _logger.LogWarning("データ不足"); return data; }
return await Task.Run(() =>
{
try
{
var xs = data.Select(v => v.X).ToArray();
var ys = data.Select(v => v.Y).ToArray();
var station = data.First().Station;
List<ChartValue> result;
switch (method)
{
case FitMethod.Polynomial:
var coeff = Fit.Polynomial(xs, ys, degree);
result = GenPoints(xs, coeff, station, "Poly");
_logger.LogInformation($"多項式フィット完了: {station}, 次数 {degree}");
break;
case FitMethod.Exponential:
var expCoeff = FitExponential(xs, ys);
result = GenPoints(xs, expCoeff, station, "Exp");
_logger.LogInformation($"指数フィット完了: {station}");
break;
case FitMethod.Spline:
var spline = CubicSpline.InterpolateNatural(xs, ys);
result = GenSplinePoints(xs, spline, station);
_logger.LogInformation($"スプライン補間完了: {station}");
break;
default: result = data; break;
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "フィッティング失敗");
return data;
}
}, ct);
}
public async Task<List<ChartValue>> PreprocessAsync(List<ChartValue> data, CancellationToken ct)
{
if (data == null || data.Count == 0) { _logger.LogWarning("空データ"); return data; }
return await Task.Run(() =>
{
int winSize = Math.Max(3, data.Count / 20);
var xs = data.Select(v => v.X).ToArray();
var ys = data.Select(v => v.Y).ToArray();
var station = data.First().Station;
var smoothed = new double[ys.Length];
for (int i = 0; i < ys.Length; i++)
{
int start = Math.Max(0, i - winSize / 2);
int end = Math.Min(ys.Length - 1, i + winSize / 2);
smoothed[i] = Enumerable.Range(start, end - start + 1).Average(j => ys[j]);
}
var res = new List<ChartValue>();
for (int i = 0; i < xs.Length; i++)
res.Add(new ChartValue { X = xs[i], Y = smoothed[i], Station = station + "_Pre" });
_logger.LogInformation($"前処理完了: {station}");
return res;
}, ct);
}
private List<ChartValue> GenPoints(double[] xs, double[] coeff, string station, string tag)
{
var points = new List<ChartValue>();
double min = xs.Min(), max = xs.Max(), step = (max - min) / 99;
for (int i = 0; i < 100; i++)
{
double x = min + i * step;
double y = tag == "Exp" ? coeff[0] * Math.Exp(coeff[1] * x) : EvalPoly(x, coeff);
points.Add(new ChartValue { X = x, Y = y, Station = $"{station}_{tag}" });
}
return points;
}
private List<ChartValue> GenSplinePoints(double[] xs, IInterpolation spline, string station)
{
var points = new List<ChartValue>();
double min = xs.Min(), max = xs.Max(), step = (max - min) / 99;
for (int i = 0; i < 100; i++)
{
double x = min + i * step;
points.Add(new ChartValue { X = x, Y = spline.Interpolate(x), Station = $"{station}_Spline" });
}
return points;
}
private double[] FitExponential(double[] x, double[] y)
{
var logY = y.Select(v => Math.Log(v)).ToArray();
var coeff = Fit.Polynomial(x, logY, 1);
return new[] { Math.Exp(coeff[0]), coeff[1] };
}
private double EvalPoly(double x, double[] coeff) => coeff.Select((c, i) => c * Math.Pow(x, i)).Sum();
}
}
ViewModel側の更新例:
// TestViewModel内
private FitMethod _selectedFit = FitMethod.Polynomial;
private int _polyDegree = 3;
public FitMethod SelectedFit { get => _selectedFit; set => SetProperty(ref _selectedFit, value); }
public int PolyDegree { get => _polyDegree; set => SetProperty(ref _polyDegree, value); }
private async Task FitCurveCmd(string station)
{
var raw = SelectedProjectModel.ChartValueList.Where(v => v.Station == station).ToList();
var fitted = await _fittingService.ApplyFitAsync(raw, SelectedFit, PolyDegree, _cts.Token);
foreach (var v in fitted) SelectedProjectModel.ChartValueList.Add(v);
Global.LogAndPop($"フィット完了: {station} - {SelectedFit}", Level.INFO);
}
XAML例:
<StackPanel Orientation="Horizontal">
<TextBlock Text="フィット方式" />
<ComboBox SelectedItem="{Binding SelectedFit}">
<ComboBoxItem Content="多項式" />
<ComboBoxItem Content="指数" />
<ComboBoxItem Content="スプライン" />
</ComboBox>
<TextBlock Text="次数" Visibility="{Binding SelectedFit, Converter={StaticResource FitToVis}, ConverterParameter=Polynomial}" />
<TextBox Text="{Binding PolyDegree}" Visibility="{Binding SelectedFit, Converter={StaticResource FitToVis}, ConverterParameter=Polynomial}" />
</StackPanel>
3. UI最適化(LiveCharts2)
LiveChartsCore.SkiaSharpView.WPF NuGetパッケージを導入し、既存のCanvas描画を置き換えます。
<UserControl ...>
<Grid>
<lvc:CartesianChart Series="{Binding Series}" ZoomMode="Both" TooltipPosition="Top"
XAxes="{Binding XAxis}" YAxes="{Binding YAxis}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseMove">
<i:InvokeCommandAction Command="{Binding ChartMoveCmd}" PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</lvc:CartesianChart>
</Grid>
</UserControl>
ViewModel側:
using LiveChartsCore; using LiveChartsCore.Defaults; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using SkiaSharp;
public class TestViewModel : BindableBase
{
public ObservableCollection<ISeries> Series { get; set; } = new();
public Axis[] XAxis { get; set; } = { new Axis { Name = "時間 (s)" } };
public Axis[] YAxis { get; set; } = { new Axis { Name = "値" } };
public DelegateCommand<MouseEventArgs> ChartMoveCmd { get; }
public TestViewModel()
{
ChartMoveCmd = new DelegateCommand<MouseEventArgs>(OnChartMove);
// データ更新時の処理
_curveGen.CurveUpdated += chartValues =>
{
Series.Clear();
foreach (var group in chartValues.GroupBy(v => v.Station))
{
Series.Add(new LineSeries<ObservablePoint>
{
Name = group.Key,
Values = group.Select(v => new ObservablePoint(v.X, v.Y)).ToList(),
Stroke = new SolidColorPaint(GetColor(group.Key)) { StrokeThickness = 2 },
Fill = null
});
}
};
}
private void OnChartMove(MouseEventArgs e)
{
var chart = e.Source as CartesianChart;
if (chart == null) return;
var pos = e.GetPosition(chart);
var nearest = Series.SelectMany(s => s.Values.Cast<ObservablePoint>().Select(p => new { p, s.Name }))
.OrderBy(p => Math.Abs(p.p.X - pos.X)).FirstOrDefault();
if (nearest != null) Global.LogAndPop($"[{nearest.Name}] X:{nearest.p.X:F2}, Y:{nearest.p.Y:F2}", Level.INFO);
}
private SKColor GetColor(string name) => name switch
{
string n when n.Contains("VCE") => SKColors.Blue,
string n when n.Contains("TVJ") => SKColors.Red,
_ => SKColors.Black
};
}
4. パフォーマンス監視
System.Diagnosticsを使用してCPU使用率とメモリを監視します。
public interface IPerfMonitor
{
Task StartAsync(CancellationToken ct);
event Action<double, double> OnUpdate; // CPU%, メモリMB
}
public class PerfMonitor : IPerfMonitor
{
private readonly ILogger<PerfMonitor> _logger;
private readonly PerformanceCounter _cpu = new("Processor", "% Processor Time", "_Total");
private readonly PerformanceCounter _mem = new("Memory", "Available MBytes");
public event Action<double, double> OnUpdate;
public PerfMonitor(ILogger<PerfMonitor> logger) => _logger = logger;
public async Task StartAsync(CancellationToken ct)
{
await Task.Run(async () =>
{
while (!ct.IsCancellationRequested)
{
try
{
var cpu = _cpu.NextValue();
var mem = _mem.NextValue();
OnUpdate?.Invoke(cpu, mem);
_logger.LogInformation($"CPU: {cpu:F2}%, 空きメモリ: {mem:F2} MB");
await Task.Delay(1000, ct);
}
catch (Exception ex) { _logger.LogError(ex, "監視エラー"); }
}
}, ct);
}
}
ViewModelとの統合:
// TestViewModel内
private double _cpuUsage, _availMem;
public double CpuUsage { get => _cpuUsage; set => SetProperty(ref _cpuUsage, value); }
public double AvailMem { get => _availMem; set => SetProperty(ref _availMem, value); }
// コンストラクタで
var perf = serviceProvider.GetRequiredService<IPerfMonitor>();
perf.OnUpdate += (cpu, mem) => { CpuUsage = cpu; AvailMem = mem; };
_ = perf.StartAsync(_cts.Token);
XAML表示例:
<StackPanel Orientation="Horizontal">
<TextBlock Text="CPU:" />
<TextBlock Text="{Binding CpuUsage, StringFormat={}{0:F2}%}" />
<TextBlock Text="空きメモリ:" />
<TextBlock Text="{Binding AvailMem, StringFormat={}{0:F2} MB}" />
</StackPanel>
5. DIコンテナ設定
services.AddSingleton<IDeviceController, NiDaqDeviceController>();
services.AddSingleton<IDataCollectorService, NiDaqDataCollectorService>();
services.AddSingleton<ICurveFittingService, CurveFittingService>();
services.AddSingleton<IPerfMonitor, PerfMonitor>();
6. 利用上の注意
- NI DAQデバイス(Dev1)を使用。チャンネル設定は環境に合わせて変更してください。
- フィッティング方式は多項式(次数指定可)、指数、スプラインから選択可能。
- LiveCharts2チャートは拡大・縮小・ドラッグに対応。マウスホバーで近傍点の座標を表示。
- パフォーマンス監視はCPU使用率と空きメモリを1秒間隔で取得。閾値超過時に警告ログを出力。