SSPC-Tester/SSPCTester.Logic/Storage/WaveformStore.cs
2026-06-30 12:10:29 +08:00

179 lines
7.0 KiB
C#

using System.Buffers.Binary;
using System.Text.Json;
using System.Text.Json.Serialization;
using SSPCTester.Devices.Interfaces;
using SSPCTester.Logic.Models;
namespace SSPCTester.Logic.Storage;
public static class WaveformStore
{
public static async Task<string?> SaveAsync(
Project project, int channel, string phase, CurveData curve, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(project.FolderPath)) return null;
string folder = Path.Combine(project.FolderPath, "curves");
Directory.CreateDirectory(folder);
string stem = $"CH{channel:00}_{phase}_{DateTime.Now:yyyyMMdd_HHmmssfff}";
string binPath = Path.Combine(folder, stem + ".bin");
string jsonPath = Path.Combine(folder, stem + ".json");
var channels = curve.EngineeringChannels.Count > 0
? curve.EngineeringChannels
: new[] { new CurveChannelData(0, DaqChannelRole.OutputVoltage, "输出电压", "V", 1, 0, curve.Voltage) };
int samples = channels.Min(x => x.Samples.Length);
byte[] buffer = new byte[channels.Count * sizeof(float) * Math.Min(samples, 4096)];
await using (var stream = File.Create(binPath))
{
for (int offset = 0; offset < samples;)
{
int take = Math.Min(4096, samples - offset);
int byteOffset = 0;
for (int sample = 0; sample < take; sample++)
foreach (var item in channels)
{
BinaryPrimitives.WriteSingleLittleEndian(
buffer.AsSpan(byteOffset, sizeof(float)),
(float)item.Samples[offset + sample]);
byteOffset += sizeof(float);
}
await stream.WriteAsync(buffer.AsMemory(0, byteOffset), ct);
offset += take;
}
}
await File.WriteAllTextAsync(jsonPath, JsonSerializer.Serialize(new
{
format = "float32-little-endian-interleaved",
sample_rate_hz = curve.SampleRateHz,
trigger_sample_index = curve.TriggerSampleIndex,
trigger_time_sec = curve.TriggerTimeSec,
samples_per_channel = samples,
channels = channels.Select(x => new
{
physical_channel = x.PhysicalChannel,
role = x.Role.ToString(),
name = x.Name,
unit = x.Unit,
scale = x.Scale,
offset = x.Offset
})
}, new JsonSerializerOptions { WriteIndented = true }), ct);
return Path.GetRelativePath(project.FolderPath, jsonPath);
}
public static async Task<CurveData?> LoadAsync(
Project project, string? relativeJsonPath, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(project.FolderPath) ||
string.IsNullOrWhiteSpace(relativeJsonPath))
{
return null;
}
string jsonPath = Path.IsPathRooted(relativeJsonPath)
? relativeJsonPath
: Path.Combine(project.FolderPath, relativeJsonPath);
if (!File.Exists(jsonPath)) return null;
await using var jsonStream = File.OpenRead(jsonPath);
var metadata = await JsonSerializer.DeserializeAsync<WaveformMetadata>(
jsonStream, cancellationToken: ct);
if (metadata == null) return null;
string binPath = Path.ChangeExtension(jsonPath, ".bin");
if (!File.Exists(binPath)) return null;
int channelCount = metadata.Channels.Length;
int samples = metadata.SamplesPerChannel;
if (channelCount <= 0 || samples <= 0) return null;
var values = Enumerable.Range(0, channelCount)
.Select(_ => new double[samples]).ToArray();
byte[] buffer = new byte[channelCount * sizeof(float) * Math.Min(samples, 4096)];
await using (var stream = File.OpenRead(binPath))
{
for (int offset = 0; offset < samples;)
{
int take = Math.Min(4096, samples - offset);
int expectedBytes = channelCount * sizeof(float) * take;
int read = 0;
while (read < expectedBytes)
{
int n = await stream.ReadAsync(buffer.AsMemory(read, expectedBytes - read), ct);
if (n == 0)
throw new InvalidDataException($"Waveform data ended early: {binPath}");
read += n;
}
int byteOffset = 0;
for (int sample = 0; sample < take; sample++)
for (int channel = 0; channel < channelCount; channel++)
{
values[channel][offset + sample] =
BinaryPrimitives.ReadSingleLittleEndian(buffer.AsSpan(byteOffset, sizeof(float)));
byteOffset += sizeof(float);
}
offset += take;
}
}
var channels = metadata.Channels.Select((x, index) => new CurveChannelData(
x.PhysicalChannel,
Enum.TryParse<DaqChannelRole>(x.Role, true, out var role) ? role : DaqChannelRole.Unconfigured,
x.Name,
x.Unit,
x.Scale,
x.Offset,
values[index])).ToArray();
var voltage = channels.FirstOrDefault(x => x.Role == DaqChannelRole.OutputVoltage)?.Samples
?? channels.First().Samples;
var current = channels.FirstOrDefault(x => x.Role == DaqChannelRole.OutputCurrent)?.Samples;
return new CurveData
{
SampleRateHz = metadata.SampleRateHz,
TriggerSampleIndex = metadata.TriggerSampleIndex,
TriggerTimeSec = metadata.TriggerTimeSec,
Voltage = voltage,
Current = current,
EngineeringChannels = channels
};
}
private sealed class WaveformMetadata
{
[JsonPropertyName("sample_rate_hz")]
public int SampleRateHz { get; set; }
[JsonPropertyName("trigger_sample_index")]
public int TriggerSampleIndex { get; set; }
[JsonPropertyName("trigger_time_sec")]
public double TriggerTimeSec { get; set; }
[JsonPropertyName("samples_per_channel")]
public int SamplesPerChannel { get; set; }
[JsonPropertyName("channels")]
public WaveformChannelMetadata[] Channels { get; set; } = Array.Empty<WaveformChannelMetadata>();
}
private sealed class WaveformChannelMetadata
{
[JsonPropertyName("physical_channel")]
public int PhysicalChannel { get; set; }
[JsonPropertyName("role")]
public string Role { get; set; } = "";
[JsonPropertyName("name")]
public string Name { get; set; } = "";
[JsonPropertyName("unit")]
public string Unit { get; set; } = "";
[JsonPropertyName("scale")]
public double Scale { get; set; }
[JsonPropertyName("offset")]
public double Offset { get; set; }
}
}