179 lines
7.0 KiB
C#
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; }
|
||
|
|
}
|
||
|
|
}
|