feat(stage3): Instrument autodetection and hardware port selector (fixes #86) #98
@@ -386,14 +386,24 @@ pub async fn read_tiff_preview_png(path: String) -> Result<String, String> {
|
||||
pub struct ChartreadConfig {
|
||||
pub basename: String,
|
||||
pub cwd: String,
|
||||
pub port: Option<String>,
|
||||
}
|
||||
|
||||
pub fn build_chartread_args(config: &ChartreadConfig) -> Vec<String> {
|
||||
vec![
|
||||
let mut args = vec![
|
||||
"-v".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]
|
||||
@@ -699,11 +709,23 @@ mod tests {
|
||||
let config = ChartreadConfig {
|
||||
basename: "my_profile".to_string(),
|
||||
cwd: "/home/user".to_string(),
|
||||
port: None,
|
||||
};
|
||||
let args = build_chartread_args(&config);
|
||||
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]
|
||||
fn test_build_colprof_args() {
|
||||
let config = ColprofConfig {
|
||||
|
||||
@@ -249,6 +249,19 @@
|
||||
<h2>Measure Target</h2>
|
||||
<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 -->
|
||||
<div class="chartread-status">
|
||||
<div class="status-label">State: <span id="chartreadState">IDLE</span></div>
|
||||
|
||||
@@ -36,11 +36,70 @@ export function initChartread() {
|
||||
const btnRetry = document.getElementById("btnRetry");
|
||||
const btnSkip = document.getElementById("btnSkip");
|
||||
const btnCancel = document.getElementById("btnCancel");
|
||||
const btnDetectInstruments = document.getElementById("btnDetectInstruments");
|
||||
const instrumentSelect = document.getElementById("chartreadInstrumentSelect");
|
||||
const promptText = document.getElementById("chartreadPrompt");
|
||||
const stateLabel = document.getElementById("chartreadState");
|
||||
const logContainer = document.getElementById("chartreadLogContainer");
|
||||
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) {
|
||||
currentState = newState;
|
||||
if (stateLabel) stateLabel.textContent = newState;
|
||||
@@ -103,9 +162,12 @@ export function initChartread() {
|
||||
setState(STATE.CALIBRATING);
|
||||
setPrompt("Starting chartread... waiting for instrument calibration prompt.");
|
||||
|
||||
const selectedPort = instrumentSelect && instrumentSelect.value ? instrumentSelect.value : null;
|
||||
|
||||
const config = {
|
||||
basename: basename,
|
||||
cwd: cwd,
|
||||
port: selectedPort,
|
||||
};
|
||||
|
||||
currentProcessId = `chartread_${basename}`;
|
||||
|
||||
Reference in New Issue
Block a user