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 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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user