feat(stage3): Instrument autodetection and hardware port selector (fixes #86) #98

Merged
gronod merged 3 commits from feat/86-instrument-autodetection into development 2026-08-25 17:27:27 +01:00
3 changed files with 101 additions and 4 deletions
+25 -3
View File
@@ -386,14 +386,24 @@ pub async fn read_tiff_preview_png(path: String) -> Result<String, String> {
pub struct ChartreadConfig { pub struct ChartreadConfig {
pub basename: String, pub basename: String,
pub cwd: String, pub cwd: String,
pub port: Option<String>,
} }
pub fn build_chartread_args(config: &ChartreadConfig) -> Vec<String> { pub fn build_chartread_args(config: &ChartreadConfig) -> Vec<String> {
vec![ let mut args = vec![
"-v".to_string(), "-v".to_string(),
"-u".to_string(), "-u".to_string(),
config.basename.clone(), ];
]
if let Some(ref port) = config.port {
if !port.trim().is_empty() {
args.push("-c".to_string());
args.push(port.trim().to_string());
}
}
args.push(config.basename.clone());
args
} }
#[tauri::command] #[tauri::command]
@@ -699,11 +709,23 @@ mod tests {
let config = ChartreadConfig { let config = ChartreadConfig {
basename: "my_profile".to_string(), basename: "my_profile".to_string(),
cwd: "/home/user".to_string(), cwd: "/home/user".to_string(),
port: None,
}; };
let args = build_chartread_args(&config); let args = build_chartread_args(&config);
assert_eq!(args, vec!["-v", "-u", "my_profile"]); assert_eq!(args, vec!["-v", "-u", "my_profile"]);
} }
#[test]
fn test_build_chartread_args_with_port() {
let config = ChartreadConfig {
basename: "my_profile".to_string(),
cwd: "/home/user".to_string(),
port: Some("1".to_string()),
};
let args = build_chartread_args(&config);
assert_eq!(args, vec!["-v", "-u", "-c", "1", "my_profile"]);
}
#[test] #[test]
fn test_build_colprof_args() { fn test_build_colprof_args() {
let config = ColprofConfig { let config = ColprofConfig {
+13
View File
@@ -249,6 +249,19 @@
<h2>Measure Target</h2> <h2>Measure Target</h2>
<p>Read the printed target patches with your spectrophotometer.</p> <p>Read the printed target patches with your spectrophotometer.</p>
<!-- Instrument & Hardware Port Selector -->
<div class="form-container" style="margin-bottom: 16px;">
<div class="form-group">
<label for="chartreadInstrumentSelect">Instrument / Hardware Port</label>
<div class="input-row" style="display: flex; gap: 8px; align-items: center;">
<select id="chartreadInstrumentSelect" style="flex: 1;">
<option value="">Auto-Detect (First Available Instrument)</option>
</select>
<button type="button" class="secondary" id="btnDetectInstruments" title="Detect connected instruments via instlist">↻ Detect</button>
</div>
</div>
</div>
<!-- State & Prompt Area --> <!-- State & Prompt Area -->
<div class="chartread-status"> <div class="chartread-status">
<div class="status-label">State: <span id="chartreadState">IDLE</span></div> <div class="status-label">State: <span id="chartreadState">IDLE</span></div>
+62
View File
@@ -36,11 +36,70 @@ export function initChartread() {
const btnRetry = document.getElementById("btnRetry"); const btnRetry = document.getElementById("btnRetry");
const btnSkip = document.getElementById("btnSkip"); const btnSkip = document.getElementById("btnSkip");
const btnCancel = document.getElementById("btnCancel"); const btnCancel = document.getElementById("btnCancel");
const btnDetectInstruments = document.getElementById("btnDetectInstruments");
const instrumentSelect = document.getElementById("chartreadInstrumentSelect");
const promptText = document.getElementById("chartreadPrompt"); const promptText = document.getElementById("chartreadPrompt");
const stateLabel = document.getElementById("chartreadState"); const stateLabel = document.getElementById("chartreadState");
const logContainer = document.getElementById("chartreadLogContainer"); const logContainer = document.getElementById("chartreadLogContainer");
const logPre = document.getElementById("chartreadLog"); const logPre = document.getElementById("chartreadLog");
// Instrument detection logic
if (btnDetectInstruments && instrumentSelect) {
btnDetectInstruments.addEventListener("click", async () => {
btnDetectInstruments.disabled = true;
btnDetectInstruments.textContent = "Detecting...";
setPrompt("Querying connected spectrophotometers and colorimeters via instlist...");
const detected = [];
try {
const unlistenStdout = await listen("process:stdout", (event) => {
if (event.payload.id !== "instlist" || !event.payload.line) return;
const line = event.payload.line.trim();
// Matches Argyll instlist output e.g. "1: 'i1Pro' on 'USB'" or "1: 'ColorMunki'" or "1 = 'i1Display'"
const match = line.match(/^(\d+)[\s:=]+'?([^'\n]+)'?(?:\s+on\s+'?([^'\n]+)'?)?/i);
if (match) {
const index = match[1];
const name = match[2].trim();
const port = match[3] ? match[3].trim() : "";
detected.push({ index, name, port });
}
});
const unlistenExit = await listen("process:exit", (event) => {
if (event.payload.id !== "instlist") return;
unlistenStdout();
unlistenExit();
btnDetectInstruments.disabled = false;
btnDetectInstruments.textContent = "↻ Detect";
instrumentSelect.innerHTML = `<option value="">Auto-Detect (First Available Instrument)</option>`;
if (detected.length > 0) {
detected.forEach((inst) => {
const opt = document.createElement("option");
opt.value = inst.index;
opt.textContent = `${inst.index}: ${inst.name} ${inst.port ? `(${inst.port})` : ""}`;
instrumentSelect.appendChild(opt);
});
instrumentSelect.value = detected[0].index;
setPrompt(`Found ${detected.length} instrument(s): ${detected.map(d => d.name).join(", ")}`);
} else {
setPrompt("No instruments found via instlist. Ensure USB cable is plugged in.");
}
});
await invoke("detect_instruments");
} catch (err) {
console.error("detect_instruments error:", err);
btnDetectInstruments.disabled = false;
btnDetectInstruments.textContent = "↻ Detect";
setPrompt(`Instrument detection error: ${err}`);
}
});
}
function setState(newState) { function setState(newState) {
currentState = newState; currentState = newState;
if (stateLabel) stateLabel.textContent = newState; if (stateLabel) stateLabel.textContent = newState;
@@ -103,9 +162,12 @@ export function initChartread() {
setState(STATE.CALIBRATING); setState(STATE.CALIBRATING);
setPrompt("Starting chartread... waiting for instrument calibration prompt."); setPrompt("Starting chartread... waiting for instrument calibration prompt.");
const selectedPort = instrumentSelect && instrumentSelect.value ? instrumentSelect.value : null;
const config = { const config = {
basename: basename, basename: basename,
cwd: cwd, cwd: cwd,
port: selectedPort,
}; };
currentProcessId = `chartread_${basename}`; currentProcessId = `chartread_${basename}`;