cue: add SQLite persistence — sessions, measurements, auto-save
This commit is contained in:
parent
bbfb008d1a
commit
ae35b1248f
|
|
@ -765,6 +765,9 @@ dependencies = [
|
|||
"iced",
|
||||
"midir",
|
||||
"muda",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"winres",
|
||||
]
|
||||
|
|
@ -1023,6 +1026,18 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fast-srgb8"
|
||||
version = "1.0.0"
|
||||
|
|
@ -1433,6 +1448,15 @@ version = "0.16.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hassle-rs"
|
||||
version = "0.11.0"
|
||||
|
|
@ -1816,6 +1840,17 @@ dependencies = [
|
|||
"redox_syscall 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
|
|
@ -2892,6 +2927,20 @@ version = "0.20.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.18.0"
|
||||
|
|
@ -3626,6 +3675,12 @@ version = "0.2.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ midir = "0.10"
|
|||
tokio = { version = "1", features = ["full"] }
|
||||
futures = "0.3"
|
||||
muda = { version = "0.17", default-features = false }
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winres = "0.1"
|
||||
|
|
|
|||
227
cue/src/app.rs
227
cue/src/app.rs
|
|
@ -14,6 +14,7 @@ use crate::protocol::{
|
|||
self, AmpPoint, ClPoint, ClResult, Electrode, EisMessage, EisPoint, LpRtia, LsvPoint,
|
||||
PhResult, Rcal, Rtia,
|
||||
};
|
||||
use crate::storage::{Session, Storage};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Tab {
|
||||
|
|
@ -30,6 +31,21 @@ enum PaneId {
|
|||
Data,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum SessionItem {
|
||||
None,
|
||||
Some(i64, String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SessionItem {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SessionItem::None => f.write_str("(none)"),
|
||||
SessionItem::Some(_, name) => f.write_str(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
BleReady(mpsc::UnboundedSender<Vec<u8>>),
|
||||
|
|
@ -86,6 +102,11 @@ pub enum Message {
|
|||
CleanVChanged(String),
|
||||
CleanDurChanged(String),
|
||||
StartClean,
|
||||
/* Sessions */
|
||||
CreateSession,
|
||||
SelectSession(Option<i64>),
|
||||
DeleteSession,
|
||||
SessionNameInput(String),
|
||||
/* Misc */
|
||||
OpenMidiSetup,
|
||||
RefreshMidi,
|
||||
|
|
@ -100,6 +121,13 @@ pub struct App {
|
|||
native_menu: NativeMenu,
|
||||
show_sysinfo: bool,
|
||||
|
||||
/* Storage */
|
||||
storage: Storage,
|
||||
current_session: Option<i64>,
|
||||
sessions: Vec<Session>,
|
||||
session_name_input: String,
|
||||
creating_session: bool,
|
||||
|
||||
/* EIS */
|
||||
eis_points: Vec<EisPoint>,
|
||||
sweep_total: u16,
|
||||
|
|
@ -274,6 +302,8 @@ fn style_neutral() -> impl Fn(&Theme, button::Status) -> ButtonStyle {
|
|||
|
||||
impl App {
|
||||
pub fn new() -> (Self, Task<Message>) {
|
||||
let storage = Storage::open().expect("failed to open database");
|
||||
let sessions = storage.list_sessions().unwrap_or_default();
|
||||
(Self {
|
||||
tab: Tab::Eis,
|
||||
status: "Starting...".into(),
|
||||
|
|
@ -288,6 +318,12 @@ impl App {
|
|||
native_menu: NativeMenu::init(),
|
||||
show_sysinfo: false,
|
||||
|
||||
storage,
|
||||
current_session: None,
|
||||
sessions,
|
||||
session_name_input: String::new(),
|
||||
creating_session: false,
|
||||
|
||||
eis_points: Vec::new(),
|
||||
sweep_total: 0,
|
||||
freq_start: "1000".into(),
|
||||
|
|
@ -362,6 +398,87 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
fn save_eis(&self, session_id: i64) {
|
||||
let params = serde_json::json!({
|
||||
"freq_start": self.freq_start,
|
||||
"freq_stop": self.freq_stop,
|
||||
"ppd": self.ppd,
|
||||
"rtia": format!("{}", self.rtia),
|
||||
"rcal": format!("{}", self.rcal),
|
||||
"electrode": format!("{}", self.electrode),
|
||||
});
|
||||
if let Ok(mid) = self.storage.create_measurement(session_id, "eis", ¶ms.to_string()) {
|
||||
let pts: Vec<(i32, String)> = self.eis_points.iter().enumerate()
|
||||
.filter_map(|(i, p)| serde_json::to_string(p).ok().map(|j| (i as i32, j)))
|
||||
.collect();
|
||||
let _ = self.storage.add_data_points_batch(mid, &pts);
|
||||
}
|
||||
}
|
||||
|
||||
fn save_lsv(&self, session_id: i64) {
|
||||
let params = serde_json::json!({
|
||||
"v_start": self.lsv_start_v,
|
||||
"v_stop": self.lsv_stop_v,
|
||||
"scan_rate": self.lsv_scan_rate,
|
||||
"rtia": format!("{}", self.lsv_rtia),
|
||||
});
|
||||
if let Ok(mid) = self.storage.create_measurement(session_id, "lsv", ¶ms.to_string()) {
|
||||
let pts: Vec<(i32, String)> = self.lsv_points.iter().enumerate()
|
||||
.filter_map(|(i, p)| serde_json::to_string(p).ok().map(|j| (i as i32, j)))
|
||||
.collect();
|
||||
let _ = self.storage.add_data_points_batch(mid, &pts);
|
||||
}
|
||||
}
|
||||
|
||||
fn save_amp(&self, session_id: i64) {
|
||||
let params = serde_json::json!({
|
||||
"v_hold": self.amp_v_hold,
|
||||
"interval_ms": self.amp_interval,
|
||||
"duration_s": self.amp_duration,
|
||||
"rtia": format!("{}", self.amp_rtia),
|
||||
});
|
||||
if let Ok(mid) = self.storage.create_measurement(session_id, "amp", ¶ms.to_string()) {
|
||||
let pts: Vec<(i32, String)> = self.amp_points.iter().enumerate()
|
||||
.filter_map(|(i, p)| serde_json::to_string(p).ok().map(|j| (i as i32, j)))
|
||||
.collect();
|
||||
let _ = self.storage.add_data_points_batch(mid, &pts);
|
||||
}
|
||||
}
|
||||
|
||||
fn save_cl(&self, session_id: i64) {
|
||||
let params = serde_json::json!({
|
||||
"cond_v": self.cl_cond_v,
|
||||
"cond_t": self.cl_cond_t,
|
||||
"free_v": self.cl_free_v,
|
||||
"total_v": self.cl_total_v,
|
||||
"dep_t": self.cl_dep_t,
|
||||
"meas_t": self.cl_meas_t,
|
||||
"rtia": format!("{}", self.cl_rtia),
|
||||
});
|
||||
if let Ok(mid) = self.storage.create_measurement(session_id, "chlorine", ¶ms.to_string()) {
|
||||
let mut pts: Vec<(i32, String)> = self.cl_points.iter().enumerate()
|
||||
.filter_map(|(i, p)| serde_json::to_string(p).ok().map(|j| (i as i32, j)))
|
||||
.collect();
|
||||
if let Some(r) = &self.cl_result {
|
||||
if let Ok(j) = serde_json::to_string(r) {
|
||||
pts.push((pts.len() as i32, format!("{{\"result\":{}}}", j)));
|
||||
}
|
||||
}
|
||||
let _ = self.storage.add_data_points_batch(mid, &pts);
|
||||
}
|
||||
}
|
||||
|
||||
fn save_ph(&self, session_id: i64, result: &PhResult) {
|
||||
let params = serde_json::json!({
|
||||
"stabilize_s": self.ph_stabilize,
|
||||
});
|
||||
if let Ok(mid) = self.storage.create_measurement(session_id, "ph", ¶ms.to_string()) {
|
||||
if let Ok(j) = serde_json::to_string(result) {
|
||||
let _ = self.storage.add_data_point(mid, 0, &j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::BleReady(tx) => {
|
||||
|
|
@ -407,6 +524,9 @@ impl App {
|
|||
}
|
||||
self.eis_points.clear();
|
||||
} else {
|
||||
if let Some(sid) = self.current_session {
|
||||
self.save_eis(sid);
|
||||
}
|
||||
self.status = format!("Sweep complete: {} points", self.eis_points.len());
|
||||
}
|
||||
}
|
||||
|
|
@ -431,6 +551,9 @@ impl App {
|
|||
self.status = format!("LSV: {}/{}", self.lsv_points.len(), self.lsv_total);
|
||||
}
|
||||
EisMessage::LsvEnd => {
|
||||
if let Some(sid) = self.current_session {
|
||||
self.save_lsv(sid);
|
||||
}
|
||||
self.status = format!("LSV complete: {} points", self.lsv_points.len());
|
||||
}
|
||||
EisMessage::AmpStart { v_hold } => {
|
||||
|
|
@ -447,6 +570,9 @@ impl App {
|
|||
}
|
||||
EisMessage::AmpEnd => {
|
||||
self.amp_running = false;
|
||||
if let Some(sid) = self.current_session {
|
||||
self.save_amp(sid);
|
||||
}
|
||||
self.status = format!("Amp complete: {} points", self.amp_points.len());
|
||||
}
|
||||
EisMessage::ClStart { num_points } => {
|
||||
|
|
@ -468,12 +594,18 @@ impl App {
|
|||
self.cl_result.as_ref().unwrap().i_total_ua);
|
||||
}
|
||||
EisMessage::ClEnd => {
|
||||
if let Some(sid) = self.current_session {
|
||||
self.save_cl(sid);
|
||||
}
|
||||
self.status = format!("Chlorine complete: {} points", self.cl_points.len());
|
||||
}
|
||||
EisMessage::PhResult(r) => {
|
||||
if self.collecting_refs {
|
||||
self.ph_ref = Some(r);
|
||||
} else {
|
||||
if let Some(sid) = self.current_session {
|
||||
self.save_ph(sid, &r);
|
||||
}
|
||||
self.status = format!("pH: {:.2} (OCP={:.1} mV, T={:.1}C)",
|
||||
r.ph, r.v_ocp_mv, r.temp_c);
|
||||
self.ph_result = Some(r);
|
||||
|
|
@ -712,6 +844,37 @@ impl App {
|
|||
self.send_cmd(&protocol::build_sysex_start_clean(v, d));
|
||||
self.status = format!("Cleaning: {:.0} mV for {:.0}s", v, d);
|
||||
}
|
||||
/* Sessions */
|
||||
Message::CreateSession => {
|
||||
if self.creating_session {
|
||||
let name = self.session_name_input.trim();
|
||||
if !name.is_empty() {
|
||||
if let Ok(id) = self.storage.create_session(name, "") {
|
||||
self.current_session = Some(id);
|
||||
self.sessions = self.storage.list_sessions().unwrap_or_default();
|
||||
self.status = format!("Session: {}", name);
|
||||
}
|
||||
}
|
||||
self.session_name_input.clear();
|
||||
self.creating_session = false;
|
||||
} else {
|
||||
self.creating_session = true;
|
||||
}
|
||||
}
|
||||
Message::SelectSession(id) => {
|
||||
self.current_session = id;
|
||||
}
|
||||
Message::DeleteSession => {
|
||||
if let Some(id) = self.current_session {
|
||||
let _ = self.storage.delete_session(id);
|
||||
self.current_session = None;
|
||||
self.sessions = self.storage.list_sessions().unwrap_or_default();
|
||||
self.status = "Session deleted".into();
|
||||
}
|
||||
}
|
||||
Message::SessionNameInput(s) => {
|
||||
self.session_name_input = s;
|
||||
}
|
||||
Message::OpenMidiSetup => {
|
||||
let _ = std::process::Command::new("open")
|
||||
.arg("-a")
|
||||
|
|
@ -882,6 +1045,8 @@ impl App {
|
|||
.push(ref_row)
|
||||
.push(text(format!("{:.1} C", self.temp_c)).size(14));
|
||||
|
||||
let session_row = self.view_session_row();
|
||||
|
||||
let controls = self.view_controls();
|
||||
|
||||
let body: Element<'_, Message> = if self.show_sysinfo {
|
||||
|
|
@ -903,7 +1068,7 @@ impl App {
|
|||
};
|
||||
|
||||
container(
|
||||
column![tabs, status_row, controls, body]
|
||||
column![tabs, session_row, status_row, controls, body]
|
||||
.spacing(10)
|
||||
.padding(20)
|
||||
.width(Length::Fill)
|
||||
|
|
@ -914,6 +1079,66 @@ impl App {
|
|||
.into()
|
||||
}
|
||||
|
||||
fn view_session_row(&self) -> Element<'_, Message> {
|
||||
let mut r = row![].spacing(6).align_y(iced::Alignment::Center);
|
||||
|
||||
let selected = self.current_session.and_then(|id| {
|
||||
self.sessions.iter().find(|s| s.id == id)
|
||||
});
|
||||
let options: Vec<SessionItem> = std::iter::once(SessionItem::None)
|
||||
.chain(self.sessions.iter().map(|s| SessionItem::Some(s.id, s.name.clone())))
|
||||
.collect();
|
||||
let current = match selected {
|
||||
Some(s) => SessionItem::Some(s.id, s.name.clone()),
|
||||
None => SessionItem::None,
|
||||
};
|
||||
r = r.push(text("Session:").size(12));
|
||||
r = r.push(
|
||||
pick_list(options, Some(current), |item| match item {
|
||||
SessionItem::None => Message::SelectSession(None),
|
||||
SessionItem::Some(id, _) => Message::SelectSession(Some(id)),
|
||||
}).width(180).text_size(12),
|
||||
);
|
||||
|
||||
if self.creating_session {
|
||||
r = r.push(
|
||||
text_input("Session name", &self.session_name_input)
|
||||
.on_input(Message::SessionNameInput)
|
||||
.on_submit(Message::CreateSession)
|
||||
.width(150)
|
||||
.size(12),
|
||||
);
|
||||
r = r.push(
|
||||
button(text("Save").size(11))
|
||||
.style(style_action())
|
||||
.padding([4, 10])
|
||||
.on_press(Message::CreateSession),
|
||||
);
|
||||
} else {
|
||||
r = r.push(
|
||||
button(text("New").size(11))
|
||||
.style(style_apply())
|
||||
.padding([4, 10])
|
||||
.on_press(Message::CreateSession),
|
||||
);
|
||||
}
|
||||
|
||||
if self.current_session.is_some() {
|
||||
r = r.push(
|
||||
button(text("Delete").size(11))
|
||||
.style(style_danger())
|
||||
.padding([4, 10])
|
||||
.on_press(Message::DeleteSession),
|
||||
);
|
||||
} else {
|
||||
r = r.push(
|
||||
text("No session -- data will not be saved").size(11),
|
||||
);
|
||||
}
|
||||
|
||||
r.into()
|
||||
}
|
||||
|
||||
fn view_controls(&self) -> Element<'_, Message> {
|
||||
match self.tab {
|
||||
Tab::Eis => row![
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ mod ble;
|
|||
mod native_menu;
|
||||
mod plot;
|
||||
mod protocol;
|
||||
mod storage;
|
||||
|
||||
fn main() -> iced::Result {
|
||||
iced::application(app::App::title, app::App::update, app::App::view)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
/// EIS4 SysEx Protocol — manufacturer ID 0x7D (non-commercial)
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
pub const SYSEX_MFR: u8 = 0x7D;
|
||||
|
||||
/* ESP32 → Cue */
|
||||
|
|
@ -174,7 +176,7 @@ impl std::fmt::Display for LpRtia {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EisPoint {
|
||||
pub freq_hz: f32,
|
||||
pub mag_ohms: f32,
|
||||
|
|
@ -188,32 +190,32 @@ pub struct EisPoint {
|
|||
pub pct_err: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LsvPoint {
|
||||
pub v_mv: f32,
|
||||
pub i_ua: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AmpPoint {
|
||||
pub t_ms: f32,
|
||||
pub i_ua: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClPoint {
|
||||
pub t_ms: f32,
|
||||
pub i_ua: f32,
|
||||
pub phase: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClResult {
|
||||
pub i_free_ua: f32,
|
||||
pub i_total_ua: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PhResult {
|
||||
pub v_ocp_mv: f32,
|
||||
pub ph: f32,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
use rusqlite::{Connection, params};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Session {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub notes: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Measurement {
|
||||
pub id: i64,
|
||||
pub session_id: i64,
|
||||
pub mtype: String,
|
||||
pub params_json: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataPoint {
|
||||
pub id: i64,
|
||||
pub measurement_id: i64,
|
||||
pub idx: i32,
|
||||
pub data_json: String,
|
||||
}
|
||||
|
||||
pub struct Storage {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
pub fn open() -> Result<Self, rusqlite::Error> {
|
||||
let dir = dirs();
|
||||
std::fs::create_dir_all(&dir).ok();
|
||||
let path = dir.join("measurements.db");
|
||||
let conn = Connection::open(path)?;
|
||||
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?;
|
||||
conn.execute_batch(SCHEMA)?;
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn create_session(&self, name: &str, notes: &str) -> Result<i64, rusqlite::Error> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO sessions (name, notes) VALUES (?1, ?2)",
|
||||
params![name, notes],
|
||||
)?;
|
||||
Ok(self.conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
pub fn list_sessions(&self) -> Result<Vec<Session>, rusqlite::Error> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, name, notes, created_at FROM sessions ORDER BY created_at DESC",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(Session {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
notes: row.get(2)?,
|
||||
created_at: row.get(3)?,
|
||||
})
|
||||
})?;
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
pub fn delete_session(&self, id: i64) -> Result<(), rusqlite::Error> {
|
||||
self.conn.execute("DELETE FROM sessions WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_measurement(
|
||||
&self, session_id: i64, mtype: &str, params_json: &str,
|
||||
) -> Result<i64, rusqlite::Error> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO measurements (session_id, type, params_json) VALUES (?1, ?2, ?3)",
|
||||
params![session_id, mtype, params_json],
|
||||
)?;
|
||||
Ok(self.conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
pub fn add_data_point(
|
||||
&self, measurement_id: i64, idx: i32, data_json: &str,
|
||||
) -> Result<(), rusqlite::Error> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO data_points (measurement_id, idx, data_json) VALUES (?1, ?2, ?3)",
|
||||
params![measurement_id, idx, data_json],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_data_points_batch(
|
||||
&self, measurement_id: i64, points: &[(i32, String)],
|
||||
) -> Result<(), rusqlite::Error> {
|
||||
let tx = self.conn.unchecked_transaction()?;
|
||||
{
|
||||
let mut stmt = tx.prepare(
|
||||
"INSERT INTO data_points (measurement_id, idx, data_json) VALUES (?1, ?2, ?3)",
|
||||
)?;
|
||||
for (idx, json) in points {
|
||||
stmt.execute(params![measurement_id, idx, json])?;
|
||||
}
|
||||
}
|
||||
tx.commit()
|
||||
}
|
||||
|
||||
pub fn get_measurements(&self, session_id: i64) -> Result<Vec<Measurement>, rusqlite::Error> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, session_id, type, params_json, created_at \
|
||||
FROM measurements WHERE session_id = ?1 ORDER BY created_at DESC",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![session_id], |row| {
|
||||
Ok(Measurement {
|
||||
id: row.get(0)?,
|
||||
session_id: row.get(1)?,
|
||||
mtype: row.get(2)?,
|
||||
params_json: row.get(3)?,
|
||||
created_at: row.get(4)?,
|
||||
})
|
||||
})?;
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
pub fn get_data_points(&self, measurement_id: i64) -> Result<Vec<DataPoint>, rusqlite::Error> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, measurement_id, idx, data_json \
|
||||
FROM data_points WHERE measurement_id = ?1 ORDER BY idx",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![measurement_id], |row| {
|
||||
Ok(DataPoint {
|
||||
id: row.get(0)?,
|
||||
measurement_id: row.get(1)?,
|
||||
idx: row.get(2)?,
|
||||
data_json: row.get(3)?,
|
||||
})
|
||||
})?;
|
||||
rows.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn dirs() -> std::path::PathBuf {
|
||||
dirs_home().join(".eis4")
|
||||
}
|
||||
|
||||
fn dirs_home() -> std::path::PathBuf {
|
||||
std::env::var("HOME")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("."))
|
||||
}
|
||||
|
||||
const SCHEMA: &str = "
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS measurements (
|
||||
id INTEGER PRIMARY KEY,
|
||||
session_id INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL,
|
||||
params_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS data_points (
|
||||
id INTEGER PRIMARY KEY,
|
||||
measurement_id INTEGER NOT NULL REFERENCES measurements(id) ON DELETE CASCADE,
|
||||
idx INTEGER NOT NULL,
|
||||
data_json TEXT NOT NULL
|
||||
);
|
||||
";
|
||||
Loading…
Reference in New Issue