First iteration

This commit is contained in:
Martino Ferrari
2026-05-07 16:47:32 +02:00
commit e3d621c425
12 changed files with 5839 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
# Generated by Cargo
# will have compiled files and executables
debug
target
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Generated by cargo mutants
# Contains mutation testing data
**/mutants.out*/
# rustc will dump stack traces when hitting an internal compiler error to PWD
rustc-ice-*.txt
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
Generated
+5127
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "remote_scope"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.102"
arrow = "58.2.0"
crossbeam-channel = "0.5.15"
eframe = { version = "0.30", features = ["glow", "persistence"] }
egui_plot = "0.30"
parquet = "58.2.0"
parquet_derive = "58.2.0"
tokio = { version = "1.52.2", features = ["full"] }
[build-dependencies]
embed-resource = "3.0.9"
+28
View File
@@ -0,0 +1,28 @@
# Makefile for RTM3004 Remote Scope
APP_NAME = remote_scope
VERSION = 1.0.0
AUTHOR = "RemoteScope Author"
TARGET_WIN = x86_64-pc-windows-gnu
.PHONY: all build-linux build-windows clean help
help:
@echo "Usage:"
@echo " make build-linux - Build for the current Linux host"
@echo " make build-windows - Cross-compile for Windows (requires $(TARGET_WIN) target)"
@echo " make all - Build for both Linux and Windows"
all: build-linux build-windows
build-linux:
cargo build --release
@echo "Linux build complete: target/release/$(APP_NAME)"
build-windows:
@rustup target add $(TARGET_WIN) > /dev/null 2>&1 || true
cargo build --release --target $(TARGET_WIN)
@echo "Windows build complete: target/$(TARGET_WIN)/release/$(APP_NAME).exe"
clean:
cargo clean
+35
View File
@@ -0,0 +1,35 @@
# RTM3004 Remote Scope
A Rust-based cross-platform UI for controlling and acquiring data from a Rohde & Schwarz RTM3004 oscilloscope.
## Features
- **Ethernet Connectivity**: Connect via Raw TCP (Port 5025).
- **Fast Segmentation**: High-speed internal acquisition with bulk binary download.
- **Real-time Visualization**: Downsampled plots using `egui_plot`.
- **Disk Streaming**: Save full-resolution data to Apache Parquet files for easy analysis in Python/Pandas.
- **Cross-platform**: Works on Windows and Linux.
## Tech Stack
- **UI**: [egui](https://github.com/emilk/egui) / [eframe](https://github.com/emilk/egui/tree/master/crates/eframe)
- **Plotting**: [egui_plot](https://github.com/emilk/egui_plot)
- **Data Storage**: [Parquet](https://crates.io/crates/parquet)
- **Communication**: SCPI over TCP
## Usage
1. Connect the RTM3004 to your network via Ethernet.
2. Ensure the scope has an IP address (check under Setup -> Network).
3. Run the application:
```bash
cargo run --release
```
4. Enter the IP address and click **Connect**.
5. Configure your segments and click **Start Acquisition**.
## Dependencies
Linux users may need the following libraries:
- `libxcb`
- `libxkbcommon`
- `libwayland` (if using Wayland)
- `libfontconfig`
Refer to the [egui documentation](https://github.com/emilk/egui/blob/master/README.md) for detailed platform-specific requirements.
+5
View File
@@ -0,0 +1,5 @@
fn main() {
if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows" {
embed_resource::compile("resources.rc", embed_resource::NONE);
}
}
+173
View File
@@ -0,0 +1,173 @@
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<title>
- Rust Programming Language
</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="description" content="A language empowering everyone to build reliable and efficient software.">
<!-- Twitter card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@rustlang">
<meta name="twitter:creator" content="@rustlang">
<meta name="twitter:title" content="">
<meta name="twitter:description" content="A language empowering everyone to build reliable and efficient software.">
<meta name="twitter:image" content="https://www.rust-lang.org/static/images/rust-social.jpg">
<!-- Facebook OpenGraph -->
<meta property="og:title" content="" />
<meta property="og:description" content="A language empowering everyone to build reliable and efficient software.">
<meta property="og:image" content="https://www.rust-lang.org/static/images/rust-social-wide.jpg" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<!-- styles -->
<link rel="stylesheet" href="/static/styles/a11y-dark.css"/>
<link rel="stylesheet" href="/static/styles/vendor_10880690442070639967.css"/>
<link rel="stylesheet" href="/static/styles/fonts_8049871103083011125.css"/>
<link rel="stylesheet" href="/static/styles/app_17774143076088586802.css"/>
<!-- favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/apple-touch-icon.png?v=ngJW8jGAmR">
<link rel="icon" sizes="16x16" type="image/png" href="/static/images/favicon-16x16.png">
<link rel="icon" sizes="32x32" type="image/png" href="/static/images/favicon-32x32.png">
<link rel="icon" type="image/svg+xml" href="/static/images/favicon.svg">
<link rel="manifest" href="/static/images/site.webmanifest?v=ngJW8jGAmR">
<link rel="mask-icon" href="/static/images/safari-pinned-tab.svg?v=ngJW8jGAmR" color="#000000">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-config" content="/static/images/browserconfig.xml?v=ngJW8jGAmR">
<meta name="theme-color" content="#ffffff">
<script type="text/javascript">
window.RUST_BASE_URL = "";
</script>
<!-- Custom Highlight pack with: Rust, Markdown, TOML, Bash, JSON, YAML,
and plaintext. -->
<script src="/static/scripts/highlight.pack.js" defer></script>
<script src="/static/scripts/init.js" defer></script>
</head>
<body>
<nav class="flex flex-row justify-center justify-end-l items-center flex-wrap ph2 pl3-ns pr3-ns pb3">
<div class="brand flex-auto w-100 w-auto-l self-start tc tl-l">
<a href="/" class="brand">
<img class="v-mid ml0-l" alt="Rust Logo" src="/static/images/rust-logo-blk.svg">
<span class="dib ml1 ml0-l">Rust</span>
</a>
</div>
<ul class="nav list w-100 w-auto-l flex flex-none flex-row flex-wrap justify-center justify-end-l items-center pv2 ph0 ph4-ns">
<li class="tc pv2 ph2 ph4-ns flex-20-s"><a href="/tools/install">Install</a></li>
<li class="tc pv2 ph2 ph4-ns flex-20-s"><a href="/learn">Learn</a></li>
<li class="tc pv2 ph2 ph4-ns flex-20-s"><a href="https://play.rust-lang.org/">Playground</a></li>
<li class="tc pv2 ph2 ph4-ns flex-20-s"><a href="/tools">Tools</a></li>
<li class="tc pv2 ph2 ph4-ns flex-20-s"><a href="/governance">Governance</a></li>
<li class="tc pv2 ph2 ph4-ns flex-20-s"><a href="/community">Community</a></li>
<li class="tc pv2 ph2 ph4-ns flex-20-s"><a href="https://blog.rust-lang.org/">Blog</a></li>
</ul>
<div class=" w-100 w-auto-l flex-none flex justify-center pv4 pv-0-l languages">
<div class="select">
<label for="language-nav" class="hidden">Language</label>
<select id="language-nav" data-current-lang="en-US">
<option title="English (en-US)" value="en-US">English (en-US)</option>
<option title="Español (es)" value="es">Español (es)</option>
<option title="Français (fr)" value="fr">Français (fr)</option>
<option title="Italiano (it)" value="it">Italiano (it)</option>
<option title="日本語 (ja)" value="ja">日本語 (ja)</option>
<option title="Português (pt-BR)" value="pt-BR">Português (pt-BR)</option>
<option title="Русский (ru)" value="ru">Русский (ru)</option>
<option title="Türkçe (tr)" value="tr">Türkçe (tr)</option>
<option title="简体中文 (zh-CN)" value="zh-CN">简体中文 (zh-CN)</option>
<option title="正體中文 (zh-TW)" value="zh-TW">正體中文 (zh-TW)</option>
</select>
</div>
</div>
</nav>
<main><header>
<div class="w-100 mw-none ph3 mw8-m mw9-l center f3">
<div class="flex-none flex-l mt5 mb5 tc tl-l">
<div class="w-70-l w-100">
<h1>404</h1>
<h2 class="subtitle">Whoops, this page doesnt exist :-(</h2>
</div>
<div class="w-30-l w-100 mt5 mt0-l">
<img id="ferris-error" src="/static/images/ferris-error.png" alt='404 not found image'>
</div>
</div>
</div>
</header>
</main>
<footer>
<div class="w-100 mw-none ph3 mw8-m mw9-l center f3">
<div class="flex flex-column flex-row-l pv0-l">
<div class="flex flex-column mw8 w-100 measure-wide-l pv2 pv5-m pv2-ns ph4-m ph4-l" id="get-help">
<h4>Get help!</h4>
<ul>
<li><a href="/learn">Documentation</a></li>
<li><a href="http://forge.rust-lang.org">Rust Forge (Contributor Documentation)</a></li>
<li><a href="https://users.rust-lang.org">Ask a Question on the Users Forum</a></li>
</ul>
<div class="languages">
<div class="select">
<label for="language-footer" class="hidden">Language</label>
<select id="language-footer">
<option title="English (en-US)" value="en-US">English (en-US)</option>
<option title="Español (es)" value="es">Español (es)</option>
<option title="Français (fr)" value="fr">Français (fr)</option>
<option title="Italiano (it)" value="it">Italiano (it)</option>
<option title="日本語 (ja)" value="ja">日本語 (ja)</option>
<option title="Português (pt-BR)" value="pt-BR">Português (pt-BR)</option>
<option title="Русский (ru)" value="ru">Русский (ru)</option>
<option title="Türkçe (tr)" value="tr">Türkçe (tr)</option>
<option title="简体中文 (zh-CN)" value="zh-CN">简体中文 (zh-CN)</option>
<option title="正體中文 (zh-TW)" value="zh-TW">正體中文 (zh-TW)</option>
</select>
</div>
</div>
</div>
<div class="flex flex-column mw8 w-100 measure-wide-l pv2 pv5-m pv2-ns ph4-m ph4-l">
<h4>Terms and policies</h4>
<ul>
<li><a href="/policies/code-of-conduct">Code of Conduct</a></li>
<li><a href="/policies/licenses">Licenses</a></li>
<li><a href="https://foundation.rust-lang.org/policies/logo-policy-and-media-guide/">Logo Policy and Media Guide</a></li>
<li><a href="/policies/security">Security Disclosures</a></li>
<li><a href="https://foundation.rust-lang.org/policies/privacy-policy/">Privacy Notice</a>
</li>
<li><a href="/policies">All Policies</a></li>
</ul>
</div>
<div class="flex flex-column mw8 w-100 measure-wide-l pv2 pv5-m pv2-ns ph4-m ph4-l">
<h4>Social</h4>
<div class="flex flex-row flex-wrap items-center">
<a rel="me" href="https://social.rust-lang.org/@rust" target="_blank"><img src="/static/images/mastodon.svg"
alt="Mastodon" title="Mastodon" /></a>
<a rel="me" href="https://bsky.app/profile/rust-lang.org" target="_blank"><img
src="/static/images/bluesky.svg" alt="Bluesky" title="Bluesky" /></a>
<a href="https://www.youtube.com/channel/UCaYhcUwRBNscFNUKTjgPFiA" target="_blank"><img class="pv2"
src="/static/images/youtube.svg" alt="youtube logo" title="YouTube" /></a>
<a href="https://github.com/rust-lang" target="_blank"><img src="/static/images/github.svg" alt="github logo"
title="GitHub" /></a>
</div>
</div>
</div>
<div class="attribution">
<p>
Maintained by the Rust Team. See a bug?
<a target="_blank" href="https://github.com/rust-lang/www.rust-lang.org/issues/new/choose">File an issue!</a>
</p>
<p>Web site built every day at 22:00 UTC</p>
<p>Looking for the <a href="https://prev.rust-lang.org">previous website</a>?</p>
</div>
</div>
</footer>
<script src="/static/scripts/languages.js"></script>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
id ICON "icon.ico"
1 VERSIONINFO
FILEVERSION 1,0,0,0
PRODUCTVERSION 1,0,0,0
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "CompanyName", "RemoteScope Author"
VALUE "FileDescription", "RTM3004 Oscilloscope UI"
VALUE "FileVersion", "1.0.0"
VALUE "InternalName", "remote_scope"
VALUE "LegalCopyright", "Copyright (c) 2026"
VALUE "OriginalFilename", "remote_scope.exe"
VALUE "ProductName", "RTM3004 Remote Scope"
VALUE "ProductVersion", "1.0.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
+242
View File
@@ -0,0 +1,242 @@
mod scpi;
mod processor;
mod writer;
use eframe::egui;
use egui_plot::{Line, Plot, PlotPoints};
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use anyhow::{Result, anyhow};
use scpi::ScpiClient;
use processor::{decimate, ScopeHeader};
use writer::{ParquetWriter, ScopeRecord};
enum Message {
Connected(bool),
Data(Vec<[f64; 2]>),
Error(String),
}
enum Command {
Connect(String),
StartAcquisition { segments: usize, stream: bool },
StopAcquisition,
}
struct RemoteScopeApp {
ip_address: String,
connected: bool,
is_acquiring: bool,
stream_to_disk: bool,
segment_count: usize,
ch1_scale: f32,
ch1_offset: f32,
ch1_enabled: bool,
plot_data: Vec<[f64; 2]>,
status_msg: String,
cmd_tx: Sender<Command>,
msg_rx: Receiver<Message>,
}
impl RemoteScopeApp {
fn new(cc: &eframe::CreationContext<'_>) -> Self {
let (cmd_tx, cmd_rx) = mpsc::channel();
let (msg_tx, msg_rx) = mpsc::channel();
let ctx = cc.egui_ctx.clone();
thread::spawn(move || {
let mut client: Option<ScpiClient> = None;
let mut acquiring = false;
while let Ok(cmd) = cmd_rx.recv() {
match cmd {
Command::Connect(ip) => {
match ScpiClient::connect(format!("{}:5025", ip)) {
Ok(c) => {
client = Some(c);
msg_tx.send(Message::Connected(true)).unwrap();
}
Err(e) => {
msg_tx.send(Message::Error(e.to_string())).unwrap();
}
}
}
Command::StartAcquisition { segments, stream } => {
if let Some(ref mut c) = client {
acquiring = true;
let msg_tx = msg_tx.clone();
let ip = ip_address_logic(c); // Placeholder for complex state
// In a real app, this would be a loop in a separate thread
// For brevity in this skeleton, we'll do one shot
if let Err(e) = run_acquisition(c, segments, stream, &msg_tx, &ctx) {
msg_tx.send(Message::Error(e.to_string())).unwrap();
}
acquiring = false;
}
}
Command::StopAcquisition => {
acquiring = false;
}
}
}
});
Self {
ip_address: "192.168.1.100".to_owned(),
connected: false,
is_acquiring: false,
stream_to_disk: false,
segment_count: 10,
ch1_scale: 1.0,
ch1_offset: 0.0,
ch1_enabled: true,
plot_data: Vec::new(),
status_msg: "Offline".to_string(),
cmd_tx,
msg_rx,
}
}
}
fn ip_address_logic(_c: &mut ScpiClient) {}
fn run_acquisition(
client: &mut ScpiClient,
segments: usize,
stream: bool,
msg_tx: &Sender<Message>,
ctx: &egui::Context
) -> Result<()> {
// 1. Configure
client.send_command("FORM REAL,32")?;
client.send_command("ACQ:MODE SEGM")?;
client.send_command(format!("ACQ:NSIN:COUN {}", segments).as_str())?;
// 2. Acquire
client.send_command("SING")?;
client.wait_for_opc()?;
// 3. Fetch Header
let header_str = client.query_ascii("CHAN1:DATA:HEAD?")?;
let header = ScopeHeader::from_string(&header_str).ok_or(anyhow!("Failed to parse header"))?;
// 4. Fetch Data
let raw_data = client.query_binary("CHAN1:DATA?")?;
let f32_data: Vec<f32> = raw_data.chunks_exact(4)
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
.collect();
// 5. Process
let decimated = decimate(&f32_data, 2000);
msg_tx.send(Message::Data(decimated)).unwrap();
// 6. Write if needed
if stream {
let mut writer = ParquetWriter::new("capture.parquet")?;
let records: Vec<ScopeRecord> = f32_data.iter().enumerate().map(|(i, &v)| {
ScopeRecord {
timestamp: header.x_start + (i as f64 * (header.x_stop - header.x_start) / header.num_samples as f64),
channel_1: v,
}
}).collect();
writer.write_batch(&records)?;
writer.close()?;
}
ctx.request_repaint();
Ok(())
}
impl eframe::App for RemoteScopeApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Receive messages
while let Ok(msg) = self.msg_rx.try_recv() {
match msg {
Message::Connected(c) => {
self.connected = c;
self.status_msg = if c { "Connected".to_string() } else { "Offline".to_string() };
}
Message::Data(data) => {
self.plot_data = data;
}
Message::Error(e) => {
self.status_msg = format!("Error: {}", e);
}
}
}
egui::SidePanel::left("control_panel").show(ctx, |ui| {
ui.heading("RTM3004 Control");
ui.group(|ui| {
ui.label("Connection");
ui.text_edit_singleline(&mut self.ip_address);
if ui.button(if self.connected { "Disconnect" } else { "Connect" }).clicked() {
if !self.connected {
self.cmd_tx.send(Command::Connect(self.ip_address.clone())).unwrap();
} else {
self.connected = false;
}
}
});
ui.add_enabled_ui(self.connected, |ui| {
ui.separator();
ui.heading("Channels");
ui.checkbox(&mut self.ch1_enabled, "Channel 1");
ui.add(egui::Slider::new(&mut self.ch1_scale, 0.001..=10.0).text("Scale (V/div)"));
ui.separator();
ui.heading("Acquisition");
ui.add(egui::Slider::new(&mut self.segment_count, 1..=1000).text("Segments"));
if ui.button(if self.is_acquiring { "Stop" } else { "Start Acquisition" }).clicked() {
self.is_acquiring = !self.is_acquiring;
if self.is_acquiring {
self.cmd_tx.send(Command::StartAcquisition {
segments: self.segment_count,
stream: self.stream_to_disk
}).unwrap();
} else {
self.cmd_tx.send(Command::StopAcquisition).unwrap();
}
}
ui.checkbox(&mut self.stream_to_disk, "Stream to Disk (.parquet)");
});
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
ui.label(&self.status_msg);
});
});
egui::CentralPanel::default().show(ctx, |ui| {
let plot = Plot::new("scope_plot")
.allow_zoom(true)
.allow_drag(true)
.legend(egui_plot::Legend::default());
plot.show(ui, |plot_ui| {
if !self.plot_data.is_empty() {
plot_ui.line(Line::new(PlotPoints::from(self.plot_data.clone())).name("CH1"));
}
});
});
}
}
fn main() -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([1024.0, 768.0]),
..Default::default()
};
eframe::run_native(
"RTM3004 Remote Scope",
options,
Box::new(|cc| Ok(Box::new(RemoteScopeApp::new(cc)))),
)
}
+46
View File
@@ -0,0 +1,46 @@
pub struct ScopeHeader {
pub x_start: f64,
pub x_stop: f64,
pub num_samples: usize,
pub values_per_sample: usize,
}
impl ScopeHeader {
pub fn from_string(s: &str) -> Option<Self> {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() < 4 { return None; }
Some(Self {
x_start: parts[0].parse().ok()?,
x_stop: parts[1].parse().ok()?,
num_samples: parts[2].parse().ok()?,
values_per_sample: parts[3].parse().ok()?,
})
}
}
pub fn decimate(data: &[f32], target_points: usize) -> Vec<[f64; 2]> {
if data.is_empty() { return Vec::new(); }
if data.len() <= target_points {
return data.iter().enumerate()
.map(|(i, &v)| [i as f64, v as f64])
.collect();
}
let chunk_size = data.len() / target_points;
let mut decimated = Vec::with_capacity(target_points * 2);
for (i, chunk) in data.chunks(chunk_size).enumerate() {
let mut min = chunk[0];
let mut max = chunk[0];
for &v in chunk {
if v < min { min = v; }
if v > max { max = v; }
}
let x = (i * chunk_size) as f64;
decimated.push([x, min as f64]);
decimated.push([x, max as f64]);
}
decimated
}
+73
View File
@@ -0,0 +1,73 @@
use anyhow::{anyhow, Result};
use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
pub struct ScpiClient {
stream: TcpStream,
}
impl ScpiClient {
pub fn connect<A: ToSocketAddrs>(addr: A) -> Result<Self> {
let stream = TcpStream::connect_timeout(
&addr.to_socket_addrs()?.next().ok_or(anyhow!("Invalid address"))?,
Duration::from_secs(5),
)?;
stream.set_read_timeout(Some(Duration::from_secs(10)))?;
Ok(Self { stream })
}
pub fn send_command(&mut self, cmd: &str) -> Result<()> {
let formatted = format!("{}\n", cmd);
self.stream.write_all(formatted.as_bytes())?;
Ok(())
}
pub fn query_ascii(&mut self, query: &str) -> Result<String> {
self.send_command(query)?;
let mut response = String::new();
let mut reader = std::io::BufReader::new(&self.stream);
std::io::BufRead::read_line(&mut reader, &mut response)?;
Ok(response.trim().to_string())
}
/// Reads a "Definite Length Block" as per IEEE 488.2
/// Format: #<num_digits><length><data>
pub fn query_binary(&mut self, query: &str) -> Result<Vec<u8>> {
self.send_command(query)?;
let mut header_start = [0u8; 2];
self.stream.read_exact(&mut header_start)?;
if header_start[0] != b'#' {
return Err(anyhow!("Invalid binary block header start: {:?}", header_start));
}
let num_digits = (header_start[1] as char)
.to_digit(10)
.ok_or(anyhow!("Invalid num_digits in header"))? as usize;
let mut length_bytes = vec![0u8; num_digits];
self.stream.read_exact(&mut length_bytes)?;
let length_str = std::str::from_utf8(&length_bytes)?;
let data_length: usize = length_str.parse()?;
let mut buffer = vec![0u8; data_length];
self.stream.read_exact(&mut buffer)?;
// R&S typically ends blocks with a newline, let's consume it if present
let mut trailing = [0u8; 1];
let _ = self.stream.read(&mut trailing);
Ok(buffer)
}
pub fn wait_for_opc(&mut self) -> Result<()> {
let res = self.query_ascii("*OPC?")?;
if res == "1" {
Ok(())
} else {
Err(anyhow!("Unexpected *OPC? response: {}", res))
}
}
}
+44
View File
@@ -0,0 +1,44 @@
use anyhow::Result;
use parquet::file::properties::WriterProperties;
use parquet::schema::parser::parse_message_type;
use parquet::record::RecordWriter;
use std::fs::File;
use std::sync::Arc;
#[derive(parquet_derive::ParquetRecordWriter)]
pub struct ScopeRecord {
pub timestamp: f64,
pub channel_1: f32,
}
pub struct ParquetWriter {
writer: parquet::file::writer::SerializedFileWriter<File>,
}
impl ParquetWriter {
pub fn new(path: &str) -> Result<Self> {
let file = File::create(path)?;
let schema = "
message schema {
REQUIRED DOUBLE timestamp;
REQUIRED FLOAT channel_1;
}
";
let schema = Arc::new(parse_message_type(schema)?);
let props = Arc::new(WriterProperties::builder().build());
let writer = parquet::file::writer::SerializedFileWriter::new(file, schema, props)?;
Ok(Self { writer })
}
pub fn write_batch(&mut self, records: &[ScopeRecord]) -> Result<()> {
let mut row_group_writer = self.writer.next_row_group()?;
records.write_to_row_group(&mut row_group_writer)?;
row_group_writer.close()?;
Ok(())
}
pub fn close(self) -> Result<()> {
self.writer.close()?;
Ok(())
}
}