WiFi STA to home network, configurable UDP address in Cue, JSON session export/import
This commit is contained in:
parent
f36989e3f9
commit
4beb9f4408
6
Makefile
6
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
|
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 \
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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, ¶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<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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue