WiFi STA to home network, configurable UDP address in Cue, JSON session export/import

This commit is contained in:
jess 2026-03-31 20:23:55 -07:00
parent f36989e3f9
commit 4beb9f4408
8 changed files with 181 additions and 6 deletions

View File

@ -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 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: all:
$(IDF) build $(IDF) build
@ -28,6 +28,10 @@ size:
erase: erase:
$(IDF) -p $(PORT) erase-flash $(IDF) -p $(PORT) erase-flash
fcf:
rm -rf build sdkconfig
$(IDF) -p $(PORT) flash monitor
select: select:
@devs=($$(ls /dev/cu.usb* 2>/dev/null)); \ @devs=($$(ls /dev/cu.usb* 2>/dev/null)); \
if [ $${#devs[@]} -eq 0 ]; then \ if [ $${#devs[@]} -eq 0 ]; then \

22
cue/Cargo.lock generated
View File

@ -761,6 +761,7 @@ checksum = "e162d0c2e2068eb736b71e5597eff0b9944e6b973cd9f37b6a288ab9bf20e300"
name = "cue" name = "cue"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"dirs-next",
"futures", "futures",
"iced", "iced",
"midir", "midir",
@ -836,6 +837,16 @@ dependencies = [
"dirs-sys", "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]] [[package]]
name = "dirs-sys" name = "dirs-sys"
version = "0.3.7" version = "0.3.7"
@ -847,6 +858,17 @@ dependencies = [
"winapi", "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]] [[package]]
name = "dispatch" name = "dispatch"
version = "0.2.0" version = "0.2.0"

View File

@ -12,6 +12,7 @@ muda = { version = "0.17", default-features = false }
rusqlite = { version = "0.31", features = ["bundled"] } rusqlite = { version = "0.31", features = ["bundled"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
dirs-next = "2"
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]
winres = "0.1" winres = "0.1"

View File

@ -127,6 +127,7 @@ pub enum Message {
OpenMidiSetup, OpenMidiSetup,
RefreshMidi, RefreshMidi,
ToggleTransport, ToggleTransport,
UdpAddrChanged(String),
} }
pub struct App { pub struct App {
@ -224,6 +225,7 @@ pub struct App {
temp_c: f32, temp_c: f32,
midi_gen: u64, midi_gen: u64,
transport: TransportMode, transport: TransportMode,
udp_addr: String,
} }
/* ---- data table formatting ---- */ /* ---- data table formatting ---- */
@ -418,6 +420,7 @@ impl App {
temp_c: 25.0, temp_c: 25.0,
midi_gen: 0, midi_gen: 0,
transport: TransportMode::Midi, transport: TransportMode::Midi,
udp_addr: crate::udp::load_addr(),
}, Task::none()) }, Task::none())
} }
@ -1023,26 +1026,32 @@ impl App {
self.ble_connected = false; self.ble_connected = false;
self.status = match self.transport { self.status = match self.transport {
TransportMode::Midi => "Looking for MIDI device...".into(), 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() Task::none()
} }
pub fn subscription(&self) -> Subscription<Message> { pub fn subscription(&self) -> Subscription<Message> {
let use_udp = self.transport == TransportMode::Udp; let use_udp = self.transport == TransportMode::Udp;
let udp_addr = self.udp_addr.clone();
let transport = Subscription::run_with_id( let transport = Subscription::run_with_id(
self.midi_gen, self.midi_gen,
iced::stream::channel(100, move |mut output| async move { iced::stream::channel(100, move |mut output| async move {
if use_udp { if use_udp {
let addr = udp_addr.clone();
loop { loop {
let (udp_tx, mut udp_rx) = mpsc::unbounded_channel::<UdpEvent>(); let (udp_tx, mut udp_rx) = mpsc::unbounded_channel::<UdpEvent>();
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<Vec<u8>>(); let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<Vec<u8>>();
let tx = udp_tx.clone(); let tx = udp_tx.clone();
let a = addr.clone();
tokio::spawn(async move { 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}"); eprintln!("UDP: {e}");
} }
}); });
@ -1223,6 +1232,15 @@ impl App {
.padding([4, 10]) .padding([4, 10])
.on_press(Message::ToggleTransport), .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 { if !connected && self.transport == TransportMode::Midi {
status_row = status_row.push( status_row = status_row.push(
button(text("Refresh MIDI").size(11)) button(text("Refresh MIDI").size(11))

View File

@ -1,4 +1,5 @@
use rusqlite::{Connection, params}; use rusqlite::{Connection, params};
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)] #[allow(dead_code)]
@ -159,6 +160,68 @@ impl Storage {
})?; })?;
rows.collect() rows.collect()
} }
pub fn export_session(&self, session_id: i64) -> Result<String, Box<dyn std::error::Error>> {
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<serde_json::Value> = 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<i64, Box<dyn std::error::Error>> {
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, &params_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<ExportMeasurement>,
}
#[derive(Serialize, Deserialize)]
struct ExportMeasurement {
mtype: String,
params: serde_json::Value,
created_at: String,
data: Vec<serde_json::Value>,
} }
fn dirs() -> std::path::PathBuf { fn dirs() -> std::path::PathBuf {

View File

@ -4,7 +4,25 @@ use std::time::{Duration, Instant};
use crate::protocol::{self, EisMessage}; 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 KEEPALIVE_INTERVAL: Duration = Duration::from_secs(5);
const TIMEOUT: Duration = Duration::from_secs(10); const TIMEOUT: Duration = Duration::from_secs(10);
@ -33,9 +51,13 @@ fn extract_sysex_frames(buf: &[u8]) -> Vec<Vec<u8>> {
pub async fn connect_and_run( pub async fn connect_and_run(
tx: mpsc::UnboundedSender<UdpEvent>, tx: mpsc::UnboundedSender<UdpEvent>,
mut cmd_rx: mpsc::UnboundedReceiver<Vec<u8>>, mut cmd_rx: mpsc::UnboundedReceiver<Vec<u8>>,
addr: String,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let esp_addr = if addr.contains(':') { addr.clone() } else { format!("{addr}:5941") };
save_addr(&esp_addr);
loop { 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 { let sock = match UdpSocket::bind("0.0.0.0:0").await {
Ok(s) => s, 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}"))); let _ = tx.send(UdpEvent::Status(format!("Connect failed: {e}")));
tokio::time::sleep(Duration::from_secs(2)).await; tokio::time::sleep(Duration::from_secs(2)).await;
continue; continue;

View File

@ -1,3 +1,9 @@
idf_component_register(SRCS "eis4.c" "eis.c" "echem.c" "ble.c" "wifi_transport.c" "temp.c" "refs.c" idf_component_register(SRCS "eis4.c" "eis.c" "echem.c" "ble.c" "wifi_transport.c" "temp.c" "refs.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES ad5941 ad5941_port bt nvs_flash esp_wifi esp_netif esp_event) 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()

View File

@ -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) static int wifi_ap_init(void)
{ {
esp_netif_create_default_wifi_ap(); esp_netif_create_default_wifi_ap();
esp_netif_create_default_wifi_sta();
wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT(); wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_err_t err = esp_wifi_init(&wifi_cfg); 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_t inst;
esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
wifi_event_handler, NULL, &inst); 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 = { wifi_config_t ap_cfg = {
.ap = { .ap = {
@ -222,9 +247,23 @@ static int wifi_ap_init(void)
esp_wifi_set_mode(WIFI_MODE_APSTA); esp_wifi_set_mode(WIFI_MODE_APSTA);
esp_wifi_set_config(WIFI_IF_AP, &ap_cfg); 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(); err = esp_wifi_start();
if (err) return err; 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); printf("WiFi: AP \"%s\" on channel %d\n", WIFI_SSID, WIFI_CHANNEL);
return 0; return 0;
} }