Este artigo descreve uma arquitetura para simular a aquisição de dados de ultrassom com alta taxa (10 MB/s) em um sistema supervisório (SCADA) baseado em Windows, onde a velocidade de processamento é muito menor que a de coleta. A solução cobre desde a captura, armazenamento, aálise espectral, geração de imagens até a análise visual, utliizando ConcurrentQueue + snapshots periódicos, com foco em desempenho e confiabilidade no .NET 8.
Contexto do Problema
- Características do ultrassom: geração de ~1000 quadros/s, cada quadro com ~10 kB de forma de onda (amostras em double), totalizando ~10 MB/s.
- Disparidade de velocidades: captura (10 MB/s) >> processamento (1 MB/s). Atraso aceitável para exibição é de segundos.
- Requisitos: persistência em SQLite, imagem em tempo real, recuperação após falha, baixo uso de memória.
- Plataforma: Windows 10/11, .NET 8 WinForms, 4 cores, 16 GB RAM, SSD.
Arquitetura Proposta
Baseada em fila concorrente + snapshot periódico, com as seguintes responsabilidades:
- Coleta assíncrona → enfileira quadros em
ConcurrentQueue<QuadroUltrassom>(~microssegundos). - Persistência em lote → SQLite modo WAL, transações de 1000 quadros (~50 ms).
- Análise → tarefa separada consome da fila (1 MB/s) e executa transformada de Fourier simplificada.
- Geração de imagem → SkiaSharp converte espectro em bitmap em tons de cinza.
- Análise de imagem → detecção de bordas simulada.
- Sanpshot → a cada 10 segundos serializa a fila com MessagePack (arquivo .snap).
Implementação (.NET 8, WinForms)
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Data.SQLite;
using System.Drawing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using MessagePack;
using SkiaSharp;
namespace UltraSim
{
public partial class MainForm : Form
{
private CancellationTokenSource _cts;
private ConcurrentQueue<FrameData> _queue = new();
private const int BatchSize = 1000;
private const int SnapshotIntervalMs = 10_000;
private const int MaxQueueItems = 50_000;
private static readonly string DbPath = "ultrasound.db";
private static readonly string SnapPath = "snapshot.bin";
private SQLiteConnection _db;
private PictureBox _imgBox;
[MessagePackObject]
public struct FrameData
{
[Key(0)] public string Timestamp;
[Key(1)] public double[] Signal; // 2560 doubles ≈ 10 KB
public FrameData(DateTime when, Random rng)
{
Timestamp = when.ToString("o");
Signal = ArrayPool<double>.Shared.Rent(2560);
for (int i = 0; i < 2560; i++)
Signal[i] = Math.Sin(i * 0.01) + rng.NextDouble() * 0.1;
}
public void Release() => ArrayPool<double>.Shared.Return(Signal);
}
public MainForm()
{
InitializeComponent();
BuildUI();
OpenDb();
}
private void BuildUI()
{
Text = "Ultrassom Simulator - .NET 8";
ClientSize = new Size(800, 600);
var btnStart = new Button { Text = "Iniciar", Location = new Point(20, 20), Width = 100 };
btnStart.Click += (_, _) => Start();
var btnStop = new Button { Text = "Parar", Location = new Point(130, 20), Width = 100 };
btnStop.Click += (_, _) => Stop();
var log = new TextBox
{
Location = new Point(20, 60), Width = 760, Height = 200,
Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Vertical
};
_imgBox = new PictureBox
{
Location = new Point(20, 270), Width = 760, Height = 300,
BorderStyle = BorderStyle.FixedSingle
};
Controls.AddRange(new Control[] { btnStart, btnStop, log, _imgBox });
}
private void OpenDb()
{
_db = new SQLiteConnection($"Data Source={DbPath};Pooling=True;");
_db.Open();
using var cmd = _db.CreateCommand();
cmd.CommandText = @"
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA cache_size=10000;
CREATE TABLE IF NOT EXISTS Frames (Timestamp TEXT, Waveform BLOB)";
cmd.ExecuteNonQuery();
}
private async void Start()
{
if (_cts != null) return;
_cts = new CancellationTokenSource();
var token = _cts.Token;
await RestoreSnapshotAsync(token);
_ = Task.Run(() => Acquire(token));
_ = Task.Run(() => Process(token));
_ = Task.Run(() => SnapshotLoop(token));
}
private void Stop()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
// Aguarda descarga dos dados acumulados (simplificado)
Task.Run(async () =>
{
await FlushAsync();
_db?.Close();
}).Wait();
}
private async Task Acquire(CancellationToken ct)
{
var rng = new Random();
int counter = 0;
while (!ct.IsCancellationRequested)
{
if (_queue.Count >= MaxQueueItems)
{
await Task.Delay(10, ct);
continue;
}
var frame = new FrameData(DateTime.UtcNow, rng);
_queue.Enqueue(frame);
counter++;
if (counter % 200 == 0)
{
BeginInvoke(() =>
{
var log = Controls[2] as TextBox;
log.AppendText($"Quadro {counter} coletado. Fila: {_queue.Count}\n");
});
}
if (_queue.Count >= BatchSize)
await FlushAsync();
await Task.Delay(1, ct); // ~1000 quadros/s
}
}
private async Task FlushAsync()
{
var batch = new List<FrameData>(BatchSize);
while (_queue.TryDequeue(out var f) && batch.Count < BatchSize)
batch.Add(f);
if (batch.Count == 0) return;
try
{
using var tx = _db.BeginTransaction();
using var cmd = _db.CreateCommand();
cmd.CommandText = "INSERT INTO Frames (Timestamp, Waveform) VALUES (@ts, @wf)";
var pTs = cmd.Parameters.Add("@ts", System.Data.DbType.String);
var pWf = cmd.Parameters.Add("@wf", System.Data.DbType.Binary);
foreach (var f in batch)
{
pTs.Value = f.Timestamp;
pWf.Value = MessagePackSerializer.Serialize(f.Signal);
cmd.ExecuteNonQuery();
f.Release();
}
tx.Commit();
}
catch (Exception ex)
{
BeginInvoke(() =>
{
var log = Controls[2] as TextBox;
log.AppendText($"Erro ao inserir lote: {ex.Message}\n");
});
}
}
private async Task Process(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
if (_queue.TryDequeue(out var f))
{
var espectro = Analisar(f.Signal);
using var bmp = GerarImagem(espectro);
var analise = AnalisarImagem(bmp);
BeginInvoke(() =>
{
_imgBox.Image?.Dispose();
_imgBox.Image = bmp.ToBitmap();
var log = Controls[2] as TextBox;
log.AppendText($"Processado {f.Timestamp}: {analise}\n");
});
f.Release();
}
await Task.Delay(10, ct); // ~100 quadros/s
}
}
private double[] Analisar(double[] sinal)
{
var r = new double[sinal.Length];
for (int i = 0; i < sinal.Length; i++)
r[i] = sinal[i] * 0.5; // placeholder
return r;
}
private SKBitmap GerarImagem(double[] dados)
{
var bmp = new SKBitmap(256, 256);
using var canvas = new SKCanvas(bmp);
canvas.Clear(SKColors.Black);
for (int x = 0; x < 256 && x < dados.Length; x++)
{
int val = (int)(Math.Abs(dados[x]) * 255);
val = Math.Clamp(val, 0, 255);
var cor = new SKColor((byte)val, (byte)val, (byte)val);
for (int y = 0; y < 256; y++)
bmp.SetPixel(x, y, cor);
}
return bmp;
}
private string AnalisarImagem(SKBitmap bmp) => "Bordas detectadas (simulação)";
private async Task SnapshotLoop(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(SnapshotIntervalMs, ct);
if (_queue.Count == 0) continue;
var itens = new List<FrameData>();
while (_queue.TryDequeue(out var f))
itens.Add(f);
try
{
using var fs = new FileStream(SnapPath, FileMode.Create, FileAccess.Write,
FileShare.None, 8192, true);
await MessagePackSerializer.SerializeAsync(fs, itens, cancellationToken: ct);
// Reenfileira para não perder dados em uso
foreach (var f in itens)
_queue.Enqueue(f);
BeginInvoke(() =>
{
var log = Controls[2] as TextBox;
log.AppendText($"Snapshot salvo: {itens.Count} quadros\n");
});
}
catch (Exception ex)
{
BeginInvoke(() =>
{
var log = Controls[2] as TextBox;
log.AppendText($"Falha no snapshot: {ex.Message}\n");
});
}
}
}
private async Task RestoreSnapshotAsync(CancellationToken ct)
{
if (!File.Exists(SnapPath)) return;
try
{
using var fs = new FileStream(SnapPath, FileMode.Open, FileAccess.Read, FileShare.Read);
var lista = await MessagePackSerializer.DeserializeAsync<List<FrameData>>(fs, cancellationToken: ct);
foreach (var f in lista)
_queue.Enqueue(f);
File.Delete(SnapPath);
BeginInvoke(() =>
{
var log = Controls[2] as TextBox;
log.AppendText($"Restaurados {lista.Count} quadros do snapshot\n");
});
}
catch (Exception ex)
{
BeginInvoke(() =>
{
var log = Controls[2] as TextBox;
log.AppendText($"Restauração falhou: {ex.Message}\n");
});
}
}
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
}
Configuração do Projeto
dotnet new winforms -n UltraSim
cd UltraSim
dotnet add package System.Data.SQLite.Core --version 1.0.118.0
dotnet add package MessagePack --version 2.5.168
dotnet add package SkiaSharp --version 2.88.8
Arquivo .csproj (relevante):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.118.0" />
<PackageReference Include="MessagePack" Version="2.5.168" />
<PackageReference Include="SkiaSharp" Version="2.88.8" />
</ItemGroup>
</Project>
Resultados Esperados
- Vazão: coleta de 10 MB/s (1000 quadros/s), processamento de 1 MB/s (100 quadros/s).
- Persistência: cada lote de 1000 quadros é inserido no SQLite em ~50 ms.
- Snapshot: a cada 10 s, a fila (até 100 MB) é serializada em ~20 ms.
- Recuperação: até 10 s de dados perdidos máxima (100 MB).
- Imagem: atualização a cada ~1 s, sem congelamento da UI.
Este projeto pode ser adaptado para taxas mais altas (aumentando o batch ou usando buffers de arquivo).