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 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 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( 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(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(); } 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; } } }