diff --git a/Makefile b/Makefile index 90ddf0f..c58a0ba 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ export IDF_PYTHON_ENV_PATH := $(HOME)/.espressif/python_env/idf6.0_py3.12_env IDF = . $(IDF_PATH)/export.sh > /dev/null 2>&1 && idf.py -.PHONY: all flash monitor clean menuconfig size erase select +.PHONY: all flash monitor clean menuconfig size erase select fcf all: $(IDF) build @@ -28,6 +28,10 @@ size: erase: $(IDF) -p $(PORT) erase-flash +fcf: + rm -rf build sdkconfig + $(IDF) -p $(PORT) flash monitor + select: @devs=($$(ls /dev/cu.usb* 2>/dev/null)); \ if [ $${#devs[@]} -eq 0 ]; then \ diff --git a/cue/Cargo.lock b/cue/Cargo.lock index 14d1cde..d04ac29 100644 --- a/cue/Cargo.lock +++ b/cue/Cargo.lock @@ -761,6 +761,7 @@ checksum = "e162d0c2e2068eb736b71e5597eff0b9944e6b973cd9f37b6a288ab9bf20e300" name = "cue" version = "0.1.0" dependencies = [ + "dirs-next", "futures", "iced", "midir", @@ -836,6 +837,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -847,6 +858,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dispatch" version = "0.2.0" diff --git a/cue/Cargo.toml b/cue/Cargo.toml index 70ec142..97c0faa 100644 --- a/cue/Cargo.toml +++ b/cue/Cargo.toml @@ -12,6 +12,7 @@ muda = { version = "0.17", default-features = false } rusqlite = { version = "0.31", features = ["bundled"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +dirs-next = "2" [target.'cfg(windows)'.build-dependencies] winres = "0.1" diff --git a/cue/src/app.rs b/cue/src/app.rs index e9735a9..1d71c4a 100644 --- a/cue/src/app.rs +++ b/cue/src/app.rs @@ -127,6 +127,7 @@ pub enum Message { OpenMidiSetup, RefreshMidi, ToggleTransport, + UdpAddrChanged(String), } pub struct App { @@ -224,6 +225,7 @@ pub struct App { temp_c: f32, midi_gen: u64, transport: TransportMode, + udp_addr: String, } /* ---- data table formatting ---- */ @@ -418,6 +420,7 @@ impl App { temp_c: 25.0, midi_gen: 0, transport: TransportMode::Midi, + udp_addr: crate::udp::load_addr(), }, Task::none()) } @@ -1023,26 +1026,32 @@ impl App { self.ble_connected = false; self.status = match self.transport { TransportMode::Midi => "Looking for MIDI device...".into(), - TransportMode::Udp => "Connecting UDP...".into(), + TransportMode::Udp => format!("Connecting UDP to {}...", self.udp_addr), }; } + Message::UdpAddrChanged(s) => { + self.udp_addr = s; + } } Task::none() } pub fn subscription(&self) -> Subscription { let use_udp = self.transport == TransportMode::Udp; + let udp_addr = self.udp_addr.clone(); let transport = Subscription::run_with_id( self.midi_gen, iced::stream::channel(100, move |mut output| async move { if use_udp { + let addr = udp_addr.clone(); loop { let (udp_tx, mut udp_rx) = mpsc::unbounded_channel::(); let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::>(); let tx = udp_tx.clone(); + let a = addr.clone(); tokio::spawn(async move { - if let Err(e) = crate::udp::connect_and_run(tx, cmd_rx).await { + if let Err(e) = crate::udp::connect_and_run(tx, cmd_rx, a).await { eprintln!("UDP: {e}"); } }); @@ -1223,6 +1232,15 @@ impl App { .padding([4, 10]) .on_press(Message::ToggleTransport), ); + if self.transport == TransportMode::Udp { + status_row = status_row.push( + text_input("IP:port", &self.udp_addr) + .size(12) + .width(160) + .on_input(Message::UdpAddrChanged) + .on_submit(Message::ToggleTransport), // reconnect on enter + ); + } if !connected && self.transport == TransportMode::Midi { status_row = status_row.push( button(text("Refresh MIDI").size(11)) diff --git a/cue/src/storage.rs b/cue/src/storage.rs index 852037d..dc33e37 100644 --- a/cue/src/storage.rs +++ b/cue/src/storage.rs @@ -1,4 +1,5 @@ use rusqlite::{Connection, params}; +use serde::{Serialize, Deserialize}; #[derive(Debug, Clone)] #[allow(dead_code)] @@ -159,6 +160,68 @@ impl Storage { })?; rows.collect() } + + pub fn export_session(&self, session_id: i64) -> Result> { + let sess = self.conn.query_row( + "SELECT id, name, notes, created_at FROM sessions WHERE id = ?1", + params![session_id], + |row| Ok(Session { + id: row.get(0)?, name: row.get(1)?, + notes: row.get(2)?, created_at: row.get(3)?, + }), + )?; + let measurements = self.get_measurements(session_id)?; + let mut export_measurements = Vec::new(); + for m in &measurements { + let points = self.get_data_points(m.id)?; + let data: Vec = points.iter() + .map(|p| serde_json::from_str(&p.data_json).unwrap_or(serde_json::Value::Null)) + .collect(); + export_measurements.push(ExportMeasurement { + mtype: m.mtype.clone(), + params: serde_json::from_str(&m.params_json).unwrap_or_default(), + created_at: m.created_at.clone(), + data, + }); + } + let export = ExportSession { + name: sess.name, + notes: sess.notes, + created_at: sess.created_at, + measurements: export_measurements, + }; + Ok(serde_json::to_string_pretty(&export)?) + } + + pub fn import_session(&self, json: &str) -> Result> { + let export: ExportSession = serde_json::from_str(json)?; + let session_id = self.create_session(&export.name, &export.notes)?; + for m in &export.measurements { + let params_json = serde_json::to_string(&m.params)?; + let mid = self.create_measurement(session_id, &m.mtype, ¶ms_json)?; + let points: Vec<(i32, String)> = m.data.iter().enumerate() + .map(|(i, v)| (i as i32, serde_json::to_string(v).unwrap_or_default())) + .collect(); + self.add_data_points_batch(mid, &points)?; + } + Ok(session_id) + } +} + +#[derive(Serialize, Deserialize)] +struct ExportSession { + name: String, + notes: String, + created_at: String, + measurements: Vec, +} + +#[derive(Serialize, Deserialize)] +struct ExportMeasurement { + mtype: String, + params: serde_json::Value, + created_at: String, + data: Vec, } fn dirs() -> std::path::PathBuf { diff --git a/cue/src/udp.rs b/cue/src/udp.rs index 0ec9ee3..c32be3a 100644 --- a/cue/src/udp.rs +++ b/cue/src/udp.rs @@ -4,7 +4,25 @@ use std::time::{Duration, Instant}; use crate::protocol::{self, EisMessage}; -const ESP_ADDR: &str = "192.168.4.1:5941"; +const DEFAULT_ADDR: &str = "192.168.4.1:5941"; +const SETTINGS_FILE: &str = ".eis4_udp_addr"; + +pub fn load_addr() -> String { + let path = dirs_next::home_dir() + .map(|h| h.join(SETTINGS_FILE)) + .unwrap_or_default(); + std::fs::read_to_string(path) + .ok() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| DEFAULT_ADDR.to_string()) +} + +pub fn save_addr(addr: &str) { + if let Some(path) = dirs_next::home_dir().map(|h| h.join(SETTINGS_FILE)) { + let _ = std::fs::write(path, addr); + } +} const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(5); const TIMEOUT: Duration = Duration::from_secs(10); @@ -33,9 +51,13 @@ fn extract_sysex_frames(buf: &[u8]) -> Vec> { pub async fn connect_and_run( tx: mpsc::UnboundedSender, mut cmd_rx: mpsc::UnboundedReceiver>, + addr: String, ) -> Result<(), Box> { + let esp_addr = if addr.contains(':') { addr.clone() } else { format!("{addr}:5941") }; + save_addr(&esp_addr); + loop { - let _ = tx.send(UdpEvent::Status("Connecting UDP...".into())); + let _ = tx.send(UdpEvent::Status(format!("Connecting UDP to {esp_addr}..."))); let sock = match UdpSocket::bind("0.0.0.0:0").await { Ok(s) => s, @@ -46,7 +68,7 @@ pub async fn connect_and_run( } }; - if let Err(e) = sock.connect(ESP_ADDR).await { + if let Err(e) = sock.connect(&esp_addr).await { let _ = tx.send(UdpEvent::Status(format!("Connect failed: {e}"))); tokio::time::sleep(Duration::from_secs(2)).await; continue; diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 6f06b68..09834e8 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,3 +1,9 @@ idf_component_register(SRCS "eis4.c" "eis.c" "echem.c" "ble.c" "wifi_transport.c" "temp.c" "refs.c" INCLUDE_DIRS "." REQUIRES ad5941 ad5941_port bt nvs_flash esp_wifi esp_netif esp_event) + +if(DEFINED ENV{WIFI_SSID}) + target_compile_definitions(${COMPONENT_LIB} PRIVATE + STA_SSID="$ENV{WIFI_SSID}" + STA_PASS="$ENV{WIFI_PASS}") +endif() diff --git a/main/wifi_transport.c b/main/wifi_transport.c index 99823d4..57eced7 100644 --- a/main/wifi_transport.c +++ b/main/wifi_transport.c @@ -197,9 +197,30 @@ static void wifi_event_handler(void *arg, esp_event_base_t base, } } +#ifndef STA_SSID +#define STA_SSID "" +#endif +#ifndef STA_PASS +#define STA_PASS "" +#endif + +static void sta_event_handler(void *arg, esp_event_base_t base, + int32_t id, void *data) +{ + (void)arg; + if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) { + printf("WiFi: STA disconnected, reconnecting...\n"); + esp_wifi_connect(); + } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t *evt = (ip_event_got_ip_t *)data; + printf("WiFi: STA connected, IP " IPSTR "\n", IP2STR(&evt->ip_info.ip)); + } +} + static int wifi_ap_init(void) { esp_netif_create_default_wifi_ap(); + esp_netif_create_default_wifi_sta(); wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT(); esp_err_t err = esp_wifi_init(&wifi_cfg); @@ -208,6 +229,10 @@ static int wifi_ap_init(void) esp_event_handler_instance_t inst; esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, &inst); + esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, + sta_event_handler, NULL, &inst); + esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, + sta_event_handler, NULL, &inst); wifi_config_t ap_cfg = { .ap = { @@ -222,9 +247,23 @@ static int wifi_ap_init(void) esp_wifi_set_mode(WIFI_MODE_APSTA); esp_wifi_set_config(WIFI_IF_AP, &ap_cfg); + + if (strlen(STA_SSID) > 0) { + wifi_config_t sta_cfg = {0}; + strncpy((char *)sta_cfg.sta.ssid, STA_SSID, sizeof(sta_cfg.sta.ssid) - 1); + strncpy((char *)sta_cfg.sta.password, STA_PASS, sizeof(sta_cfg.sta.password) - 1); + sta_cfg.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; + esp_wifi_set_config(WIFI_IF_STA, &sta_cfg); + } + err = esp_wifi_start(); if (err) return err; + if (strlen(STA_SSID) > 0) { + esp_wifi_connect(); + printf("WiFi: STA connecting to \"%s\"\n", STA_SSID); + } + printf("WiFi: AP \"%s\" on channel %d\n", WIFI_SSID, WIFI_CHANNEL); return 0; }