1/* DCC_RADIO_SETUP_WIZARD */ 2(() => { 3 "use strict"; 4 5 const stepNames = ["Hardware", "Radio", "CAT / PTT", "Audio", "Check"]; 6 const state = { 7 step: 0, 8 catalog: null, 9 detection: null, 10 selectedModelId: null, 11 connectionType: "serial", 12 rigPort: "", 13 baud: "auto", 14 pttType: "RIG", 15 pttPort: "", 16 playbackDevice: "", 17 backendSettings: {}, 18 details: null, 19 modelSearch: "", 20 manufacturerFilter: "", 21 detailsRequest: 0, 22 civAddress: "", 23 audioDevice: "", 24 guidance: null, 25 probe: null, 26 message: "", 27 messageKind: "", 28 busy: false 29 }; 30 31 function escapeHtml(value) { 32 return String(value ?? "") 33 .replaceAll("&", "&") 34 .replaceAll("<", "<") 35 .replaceAll(">", ">") 36 .replaceAll('"', """) 37 .replaceAll("'", "'"); 38 } 39 40 async function fetchJson(url, options = {}) { 41 if (window.dccDemoMode) { 42 if (!window.dccDemoRadioApi) throw new Error("Radio setup is unavailable in this demo"); 43 return window.dccDemoRadioApi(url, options); 44 } 45 const response = await fetch(url, {cache: "no-store", ...options}); 46 let payload = {}; 47 try { 48 payload = await response.json(); 49 } catch { 50 payload = {}; 51 } 52 if (!response.ok) { 53 const detail = Array.isArray(payload.detail) ? payload.detail.map(item => `${item.loc?.at(-1) || "Setting"}: ${item.msg}`).join("; ") : payload.detail; 54 throw new Error(detail || payload.message || `Request failed (${response.status})`); 55 } 56 return payload; 57 } 58 59 function modelById(modelId = state.selectedModelId) { 60 return state.catalog?.models?.find( 61 model => Number(model.model_id) === Number(modelId) 62 ) || null; 63 } 64 65 function selectedAudio() { 66 return state.catalog?.audio_candidates?.find( 67 item => item.capture_device === state.audioDevice 68 ) || null; 69 } 70 71 function requestPayload() { 72 const model = modelById(); 73 if (!model) throw new Error("Select the exact radio model"); 74 return { 75 model_id: Number(model.model_id), 76 manufacturer: model.manufacturer, 77 model_name: model.model, 78 connection_type: state.connectionType, 79 rig_port: state.rigPort, 80 baud: state.baud, 81 ptt_type: state.pttType, 82 ptt_port: state.pttPort, 83 hamlib_settings: state.backendSettings, 84 civ_address: state.civAddress || null, 85 audio_device: state.audioDevice, 86 playback_device: state.playbackDevice || state.audioDevice 87 }; 88 } 89 90 function ensureOverlay() { 91 let overlay = document.querySelector(".dcc-wizard-overlay"); 92 if (overlay) return overlay; 93 94 overlay = document.createElement("div"); 95 overlay.className = "dcc-wizard-overlay"; 96 overlay.hidden = true; 97 overlay.innerHTML = ` 98 <section class="dcc-wizard" role="dialog" aria-modal="true" aria-labelledby="dcc-wizard-title"> 99 <header class="dcc-wizard-header"> 100 <div> 101 <small>${window.dccDemoMode ? "DEMO RADIO SETUP · SIMULATED CONNECTION" : "RECEIVE-ONLY RADIO SETUP"}</small> 102 <h2 id="dcc-wizard-title">Get my radio connected</h2> 103 <p>${window.dccDemoMode ? "Explore Hamlib settings. Saved settings are remembered in this browser." : "Guided Hamlib CAT and audio setup. This wizard never sends a PTT-on command."}</p> 104 </div> 105 <button type="button" class="dcc-wizard-close" aria-label="Close radio setup">Ã</button> 106 </header> 107 <nav class="dcc-wizard-steps" aria-label="Radio setup steps"></nav> 108 <main class="dcc-wizard-body"></main> 109 <footer class="dcc-wizard-footer"></footer> 110 </section>`; 111 112 document.body.appendChild(overlay); 113 overlay.querySelector(".dcc-wizard-close")?.addEventListener("click", closeWizard); 114 overlay.addEventListener("click", event => { 115 if (event.target === overlay && !state.busy) closeWizard(); 116 }); 117 return overlay; 118 } 119 120 function setMessage(message = "", kind = "") { 121 state.message = message; 122 state.messageKind = kind; 123 } 124
125 function renderSteps() { 126 const steps = ensureOverlay().querySelector(".dcc-wizard-steps"); 127 steps.innerHTML = stepNames.map((name, index) => { 128 const className = index < state.step 129 ? "complete" 130 : index === state.step ? "active" : ""; 131 return `<div class="dcc-wizard-step ${className}"><i>${index < state.step ? "â" : index + 1}</i><span>${name}</span></div>`; 132 }).join(""); 133 } 134 135 function renderMessage() { 136 if (!state.message) return ""; 137 return `<div class="dcc-wizard-alert ${escapeHtml(state.messageKind)}">${escapeHtml(state.message)}</div>`; 138 } 139 140 function deviceRows(items, type) { 141 if (!items?.length) { 142 return `<div class="dcc-wizard-alert">${type === "serial" ? "No serial device was detected. You can enter a device path or select network CAT in the next steps." : "No audio device was detected. Connect the radio's USB audio or an external sound interface to the radio host, then reopen setup."}</div>`; 143 } 144 return `<div class="dcc-device-list">${items.map((item, index) => { 145 const value = type === "serial" ? item.path : item.capture_device; 146 const checked = value === (type === "serial" ? state.rigPort : state.audioDevice); 147 const title = type === "serial" 148 ? item.likely_model 149 : `${item.device_id} â ${item.label}`; 150 const detail = type === "serial" 151 ? `${item.path}${item.stable_path ? " ⢠stable path" : " ⢠temporary path"}` 152 : item.capture_device; 153 return `<label class="dcc-device"> 154 <input type="radio" name="dcc-${type}-device" value="${escapeHtml(value)}" ${checked ? "checked" : ""}> 155 <span><strong>${escapeHtml(title || `Device ${index + 1}`)}</strong><small>${escapeHtml(detail)}</small></span> 156 </label>`; 157 }).join("")}</div>`; 158 } 159 160 function renderHardware() { 161 const suggestions = state.catalog.suggestions || []; 162 const suggestion = suggestions[0]; 163 const summary = suggestion 164 ? `Best match: ${suggestion.display_name} (${suggestion.confidence} confidence). You will confirm it on the next page.` 165 : "No exact model name was available from USB. Select the radio from the complete Hamlib list on the next page."; 166 return ` 167 <h3>${window.dccDemoMode ? "Try a sample radio connection" : "What did the Pi find?"}</h3> 168 <p>${window.dccDemoMode ? "These are sample CAT and audio devices. Browse the full offline Hamlib catalog in the next step." : "The wizard checks stable USB serial names and ALSA audio cards without opening transmit audio or keying the radio."}</p> 169 <div class="dcc-wizard-alert success">${escapeHtml(summary)}</div> 170 <div class="dcc-wizard-grid"> 171 <section class="dcc-wizard-panel"> 172 <h4>CAT / serial devices</h4> 173 ${deviceRows(state.catalog.serial_candidates, "serial")} 174 </section> 175 <section class="dcc-wizard-panel"> 176 <h4>Receive / transmit audio devices</h4> 177 ${deviceRows(state.catalog.audio_candidates, "audio")} 178 </section> 179 </div> 180 ${renderMessage()}`; 181 } 182 183 function manufacturerOptions() { 184 const names = [...new Set( 185 (state.catalog.models || []).map(model => model.manufacturer).filter(Boolean) 186 )].sort((a, b) => a.localeCompare(b)); 187 return [`<option value="">All manufacturers</option>`] 188 .concat(names.map(name => `<option value="${escapeHtml(name)}" ${state.manufacturerFilter === name ? "selected" : ""}>${escapeHtml(name)}</option>`)) 189 .join(""); 190 } 191 192 function filteredModels(search = "", manufacturer = "") { 193 const normalize = value => String(value).toUpperCase().replace(/[^A-Z0-9]/g, ""); 194 const needle = normalize(search); 195 return (state.catalog.models || []).filter(model => { 196 if (manufacturer && model.manufacturer !== manufacturer) return false; 197 if (!needle) return true; 198 return normalize(`${model.manufacturer} ${model.model} ${model.model_id}`).includes(needle); 199 }); 200 } 201 202 function modelOptions(models) { 203 return models.map(model => { 204 const selected = Number(model.model_id) === Number(state.selectedModelId); 205 return `<option value="${model.model_id}" ${selected ? "selected" : ""}>${escapeHtml(model.display_name)} â #${model.model_id} · ${escapeHtml(model.status || "Status not reported")}</option>`; 206 }).join(""); 207 } 208 209 function guidanceHtml() { 210 if (!state.guidance) return ""; 211 return `<section class="dcc-guidance"> 212 <h4>Radio-side instructions: ${escapeHtml(state.guidance.family)}</h4> 213 <ol>${(state.guidance.steps || []).map(step => `<li>${escapeHtml(step)}</li>`).join("")}</ol> 214 </section>`; 215 } 216 217 function renderRadio() { 218 const models = filteredModels(state.modelSearch, state.manufacturerFilter); 219 return ` 220 <h3>Select the exact radio</h3> 221 <p>All radio backends from the ${window.dccDemoMode ? "offline demo" : "installed"} Hamlib catalog, including receivers and experimental drivers. Available controls depen
221d on the selected backend.</p> 222 <div class="dcc-wizard-form"> 223 <label>Search radios 224 <input type="search" id="dcc-model-search" value="${escapeHtml(state.modelSearch)}" placeholder="Model, manufacturer, or Hamlib ID"> 225 </label> 226 <label>Manufacturer 227 <select id="dcc-manufacturer-filter">${manufacturerOptions()}</select> 228 </label> 229 <label class="wide">Radio model 230 <select id="dcc-model-select" size="10">${modelOptions(models)}</select> 231 </label> 232 </div> 233 <p class="dcc-radio-count" id="dcc-model-count">${models.length} of ${state.catalog.model_count} Hamlib backends · ${escapeHtml(state.catalog.hamlib_version || "Version unavailable")}</p> 234 <div id="dcc-model-details">${modelDetailsHtml()}</div> 235 ${guidanceHtml()} 236 ${renderMessage()}`; 237 } 238 239 function serialOptions() { 240 const paths = state.catalog.serial_candidates || []; 241 return paths.map(item => { 242 const selected = item.path === state.rigPort; 243 return `<option value="${escapeHtml(item.path)}" ${selected ? "selected" : ""}>${escapeHtml(item.likely_model)}
243 â ${escapeHtml(item.path)}</option>`; 244 }).join(""); 245 } 246 247 function modelDetailsHtml() { 248 if (!modelById()) return `<p>No matching radios. Change the search or manufacturer filter.</p>`; 249 if (!state.details) return `<p>Loading this radio's settingsâ¦</p>`; 250 return `<p class="dcc-wizard-copy">${escapeHtml(state.details.note)}</p> 251 ${state.details.capabilities?.text ? `<details class="dcc-backend-details"><summary>Backend capabilities, modes, ranges, and meters</summary><pre>${escapeHtml(state.details.capabilities.text)}</pre></details>` : ""}`; 252 } 253 254 function backendSettingsHtml() { 255 const options = state.details?.settings || []; 256 const editable = options.filter(item => item.editable); 257 return `<details class="dcc-backend-details"><summary>Hamlib settings (${editable.length} configurable)</summary> 258 <p>Leave a field at âBackend defaultâ to use this radio's own defaults. Radio menu settings must match.</p> 259 <div class="dcc-wizard-form">${editable.map(item => { 260 const value = state.backendSettings[item.name] ?? ""; 261 const defaultLabel = `Backend default${item.default ? ` â ${item.default}` : ""}`; 262 const control = item.type === "combo" 263 ? `<select data-hamlib-setting="${escapeHtml(item.name)}"><option value="">${escapeHtml(defaultLabel)}</option>${item.choices.map(choice => `<option value="${escapeHtml(choice)}" ${value === choice ? "selected" : ""}>${escapeHtml(choice)}</option>`).join("")}</select>` 264 : `<input type="${item.type === "number" ? "number" : "text"}" data-hamlib-setting="${escapeHtml(item.name)}" value="${escapeHtml(value)}" placeholder="${escapeHtml(defaultLabel)}" ${item.type === "number" ? `min="${escapeHtml(item.minimum)}" max="${escapeHtml(item.maximum)}" step="${Number(item.step) > 0 ? escapeHtml(item.step) : "any"}"` : ""}>`; 265 return `<label>${escapeHtml(item.name.replaceAll("_", " "))}${control}<small>${escapeHtml(item.description)}${item.type === "number" ? ` (${escapeHtml(item.minimum)}â${escapeHtml(item.maximum)})` : ""}</small></label>`; 266 }).join("")}</div> 267 ${!editable.length ? `<p>${escapeHtml(state.details?.note || "No backend settings were returned.")}</p>` : ""} 268 <p>Port, baud, CI-V and PTT use the fields above. DTR/RTS force-on controls and action buttons are excluded from receive-only setup.</p> 269 </details>`; 270 } 271
272 function invalidateProbe() { 273 state.probe = null; 274 } 275 276 async function selectModel(modelId) { 277 if (Number(state.selectedModelId) === Number(modelId)) return; 278 state.selectedModelId = modelId; 279 state.details = null; 280 state.guidance = null; 281 state.backendSettings = {}; 282 state.baud = "auto"; 283 state.pttType = "RIG"; 284 state.pttPort = ""; 285 state.civAddress = ""; 286 if (Number(modelId) === 2) { state.connectionType = "rigctld"; state.rigPort = ""; } 287 invalidateProbe(); 288 renderFooter(); 289 await loadGuidance(); 290 // Preserve search focus and scroll while asynchronous metadata arrives. 291 const details = ensureOverlay().querySelector("#dcc-model-details"); 292 if (details) details.innerHTML = modelDetailsHtml(); 293 const guidance = ensureOverlay().querySelector(".dcc-guidance"); 294 if (guidance) guidance.outerHTML = guidanceHtml(); 295 } 296 297 function renderCat() { 298 const isNetwork = ["network", "rigctld"].includes(state.connectionType); 299 const isRemote = state.connectionType === "rigctld" || Number(state.selectedModelId) === 2; 300 const isIcom = `${modelById()?.manufacturer || ""} ${modelById()?.model || ""}`.toUpperCase().includes("ICOM"); 301 return ` 302 <h3>CAT connection and PTT method</h3> 303 <p>${window.dccDemoMode ? "Explore connection options and each radio's Hamlib settings. No device will be opened." : "Digital Command Center will c
303onfigure a local rigctld connection, but the main application remains transmit-locked."}</p> 304 <div class="dcc-wizard-form"> 305 <label>Connection type 306 <select id="dcc-connection-type"> 307 <option value="serial" ${state.connectionType === "serial" ? "selected" : ""}>USB or serial cable</option> 308 <option value="network" ${state.connectionType === "network" ? "selected" : ""}>Direct network CAT (selected radio backend)</option> 309 <option value="rigctld" ${state.connectionType === "rigctld" ? "selected" : ""}>Remote rigctld server (NET rigctl)</option> 310 <option value="device" ${state.connectionType === "device" ? "selected" : ""}>Other Hamlib device path</option> 311 </select> 312 </label> 313 ${isNetwork ? ` 314 <label>Network CAT address 315 <input type="text" id="dcc-rig-port" value="${escapeHtml(state.rigPort)}" placeholder="192.168.1.50:4532"> 316 </label>` : ` 317 <label>CAT device path 318 <input type="text" id="dcc-rig-port" list="dcc-serial-paths" value="${escapeHtml(state.rigPort)}" placeholder="/dev/ttyUSB0"> 319 <datalist id="dcc-serial-paths">${serialOptions()}</datalist> 320 </label>`} 321 <label>CAT baud rate 322 <select id="dcc-baud" ${state.connectionType !== "serial" ? "disabled" : ""}>${(state.catalog.baud_rates || []).map(value => `<option value="${value}" ${value === state.baud ? "selected" : ""}>${value === "auto" ? "Auto â Hamlib backend default" : `${value} baud`}</option>`).join("")}</select> 323 </label> 324 <label>PTT method 325 <select id="dcc-ptt-type">${(isRemote ? ["RIG", "NONE"] : state.catalog.ptt_types).map(value => `<option value="${value}" ${state.pttType === value ? "selected" : ""}>${escapeHtml(({RIG: "CAT command", NONE: "None â receive only", GPION: "GPIO (inverted)"})[value] || value)}</option>`).join("")}</select> 326 </label> 327 ${!isRemote && !["RIG", "NONE"].includes(state.pttType) ? `<label>PTT device / GPIO pin 328 <input type="text" id="dcc-ptt-port" value="${escapeHtml(state.pttPort)}" placeholder="Blank uses CAT port for DTR / RTS"> 329 </label>` : ""} 330 ${isIcom && !isRemote ? `<label>CI-V address (optional) 331 <input type="text" id="dcc-civ-address" value="${escapeHtml(state.civAddress)}" placeholder="Example: 0xA4"> 332 </label>` : ""} 333 </div> 334 ${isRemote ? `<p class="dcc-wizard-copy">The remote rigctld server owns the radio's serial and PTT settings. Audio must be routed separately to an audio interface on this host.</p>` : backendSettingsHtml()} 335 <div class="dcc-wizard-alert">${window.dccDemoMode ? "Transmit and tune are simulated on the dashboard with a matched antenna. These settings never connect to physical hardware." : "The PTT method is saved for future transmit setup. The receive-only service runs with PTT disabled; this wizard never keys the radio."}</div> 336 ${guidanceHtml()} 337 ${renderMessage()}`; 338 } 339 340 function audioOptions(playback = false) { 341 return (state.catalog.audio_candidates || []).map(item => { 342 const device = playback ? item.playback_device : item.capture_device; 343 const selected = device === (playback ? state.playbackDevice : state.audioDevice); 344 return `<option value="${escapeHtml(device)}" ${selected ? "selected" : ""}>${escapeHtml(item.device_id)} â ${escapeHtml(item.label)}</option>`; 345 }).join(""); 346 } 347 348 function renderAudio() { 349 const audio = selectedAudio(); 350 return ` 351 <h3>Select the radio audio interface</h3> 352 <p>The same USB codec is normally used for receive and transmit audio. Only receive audio is opened during setup.</p> 353 <div class="dcc-wizard-form"> 354 <label class="wide">Receive audio device 355 <select id="dcc-audio-device">${audioOptions()}</select> 356 </label> 357 <label class="wide">Playback audio device (saved for transmit setup) 358 <select id="dcc-playback-device">${audioOptions(true)}</select> 359 </label> 360 </div> 361 <section class="dcc-guidance"> 362 <h4>Selected ALSA paths</h4> 363 <ol> 364 <li>Receive: ${escapeHtml(audio?.capture_device || "No device selected")}</li> 365 <li>Transmit: ${escapeHtml(audio?.playback_device || "No device selected")} â not opened by this wizard</li> 366 <li>Set the radio DATA input to USB when it has a built-in USB codec.</li> 367 </ol> 368 </section> 369 <h3 style="margin-top:18px">Common setup questions</h3> 370 <div class="dcc-help-list"> 371 <details><summary>Which serial port should I choose?</summary><p>Prefer a path under /dev/serial/by-id. It follows the same radio after a reboot or USB reconnect. ttyUSB and ttyACM names can change.</p></details> 372 <details><summary>What baud rate should I use?</summary><p>The radio menu and Hamlib must agree. Auto uses the backend default; modern USB radios often work best at 38400 or 115200.</p></details> 373 <details><summary>Should I use CAT or VOX for PTT?</summary><p>Use CAT whenever the radio supports it. VOX is intentionally not configured because background computer audio could key the transmitter.</p></details> 374 <details><summary>${window.dccDemoMode ? "Can I try transmitting?" : "Why is transmit still locked?"}</summary><p>${window.dccDemoMode ? "Yes. Enable TX or Tune on the demo dashboard to try the simulated radio and matched antenna." : "Radio setup proves CAT and receive audio first. Transmit requires separate antenna, power, waveform, and safety approval."}</p></details> 375 <details><summary>What if no USB audio appears?</summary><p>Confirm the cable carries data, try another USB port, power-cycle the radio, and check that the radio exposes its USB audio codec.</p></details> 376 </div> 377 ${renderMessage()}`; 378 } 379 380 function checkRow(label, value, detail = "") { 381 const stateClass = value ? "ok" : "bad"; 382 return `<div class="dcc-check"><span>${escapeHtml(label)}${detail ? `<small style="display:block;color:#78909a;margin-top:3px">${escapeHtml(detail)}</small>` : ""}</span><strong class="${stateClass}">${value ? "PASS" : "CHECK"}</strong></div>`; 383 } 384 385 function renderCheck() { 386 const model = modelById(); 387 const probe = state.probe; 388 return ` 389 <h3>${window.dccDemoMode ? "Review demo radio settings" : "Receive-only connection check"}</h3> 390 <p>${window.dccDemoMode ? "Validate and save these settings for your next visit. No hardware connection or transmission is tested." : "Review the selection, run the checks, then save it. Applying the setup briefly restarts radio control and the dashboard."}</p> 391 <div class="dcc-wizard-grid">
392 <section class="dcc-wizard-panel"> 393 <h4>Selected configuration</h4> 394 <div class="dcc-check-list"> 395 <div class="dcc-check"><span>Radio</span><strong>${escapeHtml(model?.display_name || "Not selected")}</strong></div> 396 <div class="dcc-check"><span>CAT</span><strong>${escapeHtml(state.rigPort || "Not selected")}</strong></div> 397 <div class="dcc-check"><span>Baud / PTT</span><strong>${escapeHtml(`${state.baud} / ${state.pttType}`)}</strong></div> 398 <div class="dcc-check"><span>Audio</span><strong>${escapeHtml(state.audioDevice || "Not selected")}</strong></div> 399 <div class="dcc-check"><span>Playback</span><strong>${escapeHtml(state.playbackDevice || state.audioDevice)}</strong></div> 400 ${state.pttPort ? `<div class="dcc-check"><span>PTT device (saved)</span><strong>${escapeHtml(state.pttPort)}</strong></div>` : ""} 401 ${Object.entries(state.backendSettings).map(([key, value]) => `<div class="dcc-check"><span>${escapeHtml(key)}</span><strong>${escapeHtml(value)}</strong></div>`).join("")} 402 <div class="dcc-check"><span>Transmit state</span><strong class="ok">LOCKED</strong></div> 403 </div> 404 </section> 405 <section class="dcc-wizard-panel"> 406 <h4>${window.dccDemoMode ? "Demo validation" : "Hardware checks"}</h4> 407 ${probe ? `<div class="dcc-check-list"> 408 ${checkRow("Hamlib model installed", probe.checks.model_available)} 409 ${checkRow("Hamlib settings valid", probe.checks.settings_valid !== false)} 410 ${checkRow("Playback device detected", probe.checks.playback_device_detected !== false)} 411 ${checkRow("CAT connection present", probe.checks.connection_present, probe.connection_detail)} 412 ${checkRow("Audio device detected", probe.checks.audio_device_detected)} 413 ${window.dccDemoMode ? "" : checkRow("Current CAT responding", probe.checks.current_cat_connected, probe.current_frequency_hz ? `${(probe.current_frequency_hz / 1000000).toFixed(6)} MHz ${probe.current_mode || ""}` : "A new configuration may need to be applied first")} 414 </div>` : `<div class="dcc-wizard-alert">Run the checks to confirm the selected hardware before saving.</div>`} 415 </section> 416 </div> 417 <div class="dcc-wizard-actions" style="margin-top:15px"> 418 <button type="button" id="dcc-run-probe" ${state.busy ? "disabled" : ""}>${state.busy ? "Checkingâ¦" : window.dccDemoMode ? "Validate demo settings" : "Run receive-only checks"}</button> 419 <button type="button" class="primary" id="dcc-apply-radio" ${!probe?.ready_to_apply || state.busy ? "disabled" : ""}>${window.dccDemoMode ? "Save in demo" : "Save and connect receive-only"}</button> 420 </div> 421 ${renderMessage()}`; 422 } 423 424 function renderBody() { 425 const body = ensureOverlay().querySelector(".dcc-wizard-body"); 426 if (!state.catalog) { 427 body.innerHTML = `<h3>Scanning the Piâ¦</h3><p>Reading Hamlib models, USB serial devices, and audio cards.</p>${renderMessage()}`; 428 return; 429 } 430 body.innerHTML = [renderHardware, renderRadio, renderCat, renderAudio, renderCheck][state.step](); 431 bindStepControls(); 432 } 433 434 function canContinue() { 435 if (!state.catalog) return false; 436 if (state.step === 1) return Boolean(modelById()) && Boolean(state.details); 437 if (state.step === 2) return Boolean(state.rigPort); 438 if (state.step === 3) return Boolean(state.audioDevice); 439 return true; 440 } 441 442 function renderFooter() { 443 const footer = ensureOverlay().querySelector(".dcc-wizard-footer"); 444 footer.innerHTML = ` 445 <span>${window.dccDemoMode ? "SIMULATED TX / RX · MATCHED ANTENNA" : "TX LOCKED ⢠NO PTT TEST"}</span> 446 <div class="dcc-wizard-actions"> 447 <button type="button" id="dcc-wizard-back" ${state.step === 0 || state.busy ? "disabled" : ""}>Back</button> 448 ${state.step < stepNames.length - 1 449 ? `<button type="button" class="primary" id="dcc-wizard-next" ${!canContinue() || state.busy ? "disabled" : ""}>Next</button>` 450 : `<button type="button" id="dcc-wizard-finish" ${state.busy ? "disabled" : ""}>Close</button>`} 451 </div>`; 452 footer.querySelector("#dcc-wizard-back")?.addEventListener("click", () => {
453 state.step = Math.max(0, state.step - 1); 454 setMessage(); 455 renderAll(); 456 }); 457 footer.querySelector("#dcc-wizard-next")?.addEventListener("click", async () => { 458 state.step = Math.min(stepNames.length - 1, state.step + 1); 459 setMessage(); 460 if (state.step === 2) await loadGuidance(); 461 renderAll(); 462 }); 463 footer.querySelector("#dcc-wizard-finish")?.addEventListener("click", closeWizard); 464 } 465 466 function renderAll() { 467 renderSteps(); 468 renderBody(); 469 renderFooter(); 470 } 471 472 async function loadGuidance() { 473 const model = modelById(); 474 if (!model) return; 475 if (state.details?.model_id === Number(model.model_id)) return; 476 const revision = ++state.detailsRequest; 477 try { 478 const details = await fetchJson( 479 `/api/v1/radio/setup/models/${Number(model.model_id)}` 480 ); 481 if (revision !== state.detailsRequest || Number(model.model_id) !== Number(state.selectedModelId)) return; 482 state.details = details; 483 state.guidance = details.guidance; 484 } catch (error) { 485 if (revision === state.detailsRequest) setMessage(error.message, "error"); 486 } 487 renderFooter(); 488 } 489 490 function bindStepControls() { 491 const body = ensureOverlay().querySelector(".dcc-wizard-body"); 492 body.addEventListener("input", invalidateProbe, {once: true}); 493 body.addEventListener("change", invalidateProbe, {once: true}); 494 body.querySelectorAll('input[name="dcc-serial-device"]').forEach(input => { 495 input.addEventListener("change", () => { state.rigPort = input.value; renderFooter(); }); 496 }); 497 body.querySelectorAll('input[name="dcc-audio-device"]').forEach(input => { 498 input.addEventListener("change", () => { state.audioDevice = input.value; renderFooter(); }); 499 }); 500 501 const search = body.querySelector("#dcc-model-search"); 502 const manufacturer = body.querySelector("#dcc-manufacturer-filter"); 503 const select = body.querySelector("#dcc-model-select"); 504 function refreshModelOptions() { 505 state.modelSearch = search?.value || ""; 506 state.manufacturerFilter = manufacturer?.value || ""; 507 const models = filteredModels(search?.value || "", manufacturer?.value || ""); 508 select.innerHTML = modelOptions(models); 509 if (!models.some(item => Number(item.model_id) === Number(state.selectedModelId))) { 510 void selectModel(models[0]?.model_id || null); 511 if (select && state.selectedModelId) select.value = String(state.selectedModelId); 512 } 513 body.querySelector("#dcc-model-count").textContent = `${models.length} of ${state.catalog.model_count} Hamlib backends`; 514 body.querySelector("#dcc-model-details").innerHTML = modelDetailsHtml(); 515 renderFooter(); 516 } 517 search?.addEventListener("input", refreshModelOptions); 518 manufacturer?.addEventListener("change", refreshModelOptions); 519 select?.addEventListener("change", async () => { 520 await selectModel(Number(select.value)); 521 }); 522 523 body.querySelector("#dcc-connection-type")?.addEventListener("change", event => { 524 state.connectionType = event.target.value; 525 if (["network", "rigctld"].includes(state.connectionType) && state.rigPort.startsWith("/dev/")) { 526 state.rigPort = ""; 527 } 528 if (state.connectionType !== "serial") state.baud = "auto"; 529 if (state.connectionType === "rigctld") { 530 state.backendSettings = {}; state.pttType = "RIG"; state.pttPort = ""; state.civAddress = ""; 531 } 532 if (state.connectionType === "serial" && !state.rigPort.startsWith("/dev/")) { 533 state.rigPort = state.catalog.serial_candidates?.[0]?.path || ""; 534 } 535 renderAll(); 536 }); 537 body.querySelector("#dcc-rig-port")?.addEventListener("change", event => { 538 state.rigPort = event.target.value.trim(); 539 renderFooter(); 540 }); 541 body.querySelector("#dcc-rig-port")?.addEventListener("input", event => { 542 state.rigPort = event.target.value.trim(); 543 renderFooter(); 544 }); 545 body.querySelector("#dcc-baud")?.addEventListener("change", event => { state.baud = event.target.value; }); 546 body.querySelector("#dcc-ptt-type")?.addEventListener("change", event => { state.pttType = event.target.value; state.pttPort = ""; renderAll(); }); 547 body.querySelector("#dcc-ptt-port")?.addEventListener("input", event => { state.pttPort = event.target.value.trim(); }); 548 body.querySelectorAll("[data-hamlib-setting]").forEach(input => { 549 input.addEventListener("input", () => { 550 if (input.value === "") delete state.backendSettings[input.dataset.hamlibSetting]; 551 else state.backendSettings[input.dataset.hamlibSetting] = input.value; 552 invalidateProbe(); 553 }); 554 }); 555 body.querySelector("#dcc-playback-device")?.addEventListener("change", event => { state.playbackDevice = event.target.value; }); 556 body.querySelector("#dcc-civ-address")?.addEventListener("input", event => { state.civAddress = event.target.value.trim(); }); 557 body.querySelector("#dcc-audio-device")?.addEventListener("change", event => { 558 state.audioDevice = event.target.value; 559 renderAll(); 560 });
561 body.querySelector("#dcc-run-probe")?.addEventListener("click", runProbe); 562 body.querySelector("#dcc-apply-radio")?.addEventListener("click", applyRadio); 563 } 564 565 async function runProbe() { 566 state.busy = true; 567 setMessage("Running receive-only hardware checksâ¦"); 568 renderAll(); 569 try { 570 state.probe = await fetchJson("/api/v1/radio/setup/probe", { 571 method: "POST", 572 headers: {"Content-Type": "application/json"}, 573 body: JSON.stringify(requestPayload()) 574 }); 575 setMessage( 576 state.probe.ready_to_apply 577 ? window.dccDemoMode ? "Demo settings are valid. No hardware was checked." : "Hardware checks passed. The configuration is ready to save." 578 : state.probe.settings_error || "One or more hardware checks need attention before saving.", 579 state.probe.ready_to_apply ? "success" : "error" 580 ); 581 } catch (error) { 582 state.probe = null; 583 setMessage(error.message, "error"); 584 } finally { 585 state.busy = false; 586 renderAll(); 587 } 588 } 589 590 async function waitForApply(requestedAt) { 591 for (let attempt = 0; attempt < 45; attempt += 1) { 592 await new Promise(resolve => setTimeout(resolve, 2000)); 593 let status; 594 try { 595 status = await fetchJson("/api/v1/radio/setup/status"); 596 } catch (error) { 597 if (attempt > 35) throw error; 598 continue; 599 } 600 if (requestedAt && status.requested_at_utc !== requestedAt) continue; 601 if (status.state === "success") { 602 if (status.schema_version !== 2) throw new Error("Update the radio setup worker to support the new Hamlib settings. This worker did not confirm that they were applied."); 603 return status; 604 } 605 if (status.state === "failed") { 606 throw new Error(status.message || "Radio setup failed and was rolled back"); 607 } 608 } 609 throw new Error("The dashboard did not confirm the new radio connection in time"); 610 } 611 612 async function applyRadio() { 613 state.busy = true; 614 setMessage("Saving the receive-only configuration. The dashboard may reconnect briefly."); 615 renderAll(); 616 try { 617 const queued = await fetchJson("/api/v1/radio/setup/apply", { 618 method: "POST", 619 headers: {"Content-Type": "application/json"}, 620 body: JSON.stringify(requestPayload()) 621 }); 622 const status = await waitForApply(queued.requested_at_utc); 623 const mhz = Number(status.frequency_hz) / 1000000; 624 setMessage( 625 status.simulated ? `${status.radio_name} connected in the simulator. Transmit and tune are ready to try.` : `${status.radio_name || "Radio"} connected receive-only${Number.isFinite(mhz) ? ` at ${mhz.toFixed(6)} MHz` : ""}. Transmit remains locked.`, 626 "success" 627 ); 628 state.probe = { 629 ready_to_apply: true, 630 connection_detail: status.simulated ? "Saved in this browser's demo settings" : "New CAT configuration is responding", 631 current_frequency_hz: status.frequency_hz, 632 current_mode: status.mode, 633 checks: { 634 model_available: true, 635 connection_present: true, 636 audio_device_detected: true, 637 current_cat_connected: true 638 } 639 }; 640 window.dispatchEvent(new CustomEvent("dcc-radio-setup-complete", {detail: status})); 641 } catch (error) { 642 setMessage(error.message, "error"); 643 } finally { 644 state.busy = false; 645 renderAll(); 646 } 647 } 648 649 async function loadCatalog() { 650 const [catalog, detection] = await Promise.all([ 651 fetchJson("/api/v1/radio/setup/catalog"), 652 fetchJson("/api/v1/radio/detect") 653 ]); 654 if (!catalog.models?.length) { 655 throw new Error("Hamlib did not return any radio models. Confirm rigctl is installed."); 656 } 657 state.catalog = catalog; 658 state.detection = detection; 659 660 const configured = detection.configured || {}; 661 const suggestion = catalog.suggestions?.[0]; 662 const configuredExists = catalog.models.some( 663 model => Number(model.model_id) === Number(configured.model_id) 664 ); 665 state.selectedModelId = configuredExists ? configured.model_id : suggestion?.model_id || catalog.models[0].model_id; 666 state.connectionType = configured.connection_type || "serial"; 667 state.rigPort = configured.rig_port || suggestion?.serial_path || catalog.serial_candidates?.[0]?.path || ""; 668 state.audioDevice = ( 669 catalog.audio_candidates?.find(item => item.capture_device === configured.audio_device)?.capture_device || 670 catalog.audio_candidates?.find(item => item.likely_radio_audio)?.capture_device || 671 catalog.audio_candidates?.[0]?.capture_device || "" 672 ); 673 state.baud = configured.baud || "auto"; 674 state.pttType = configured.ptt || "RIG"; 675 state.civAddress = configured.civ_address || ""; 676 state.backendSettings = {...(configured.hamlib_settings || {})}; 677 state.pttPort = configured.ptt_port || ""; 678 state.playbackDevice = configured.playback_device || state.audioDevice; 679 state.details = null; 680 state.modelSearch = ""; 681 state.manufacturerFilter = ""; 682 await loadGuidance(); 683 } 684 685 async function openWizard() {
686 const overlay = ensureOverlay(); 687 const settingsDialog = document.querySelector(".settings-dialog"); 688 const settingsDone = Array.from( 689 settingsDialog?.querySelectorAll("button") || [] 690 ).find(button => button.textContent.trim().toUpperCase() === "DONE"); 691 if (settingsDone) { 692 settingsDone.click(); 693 await new Promise(resolve => setTimeout(resolve, 120)); 694 } 695 overlay.hidden = false; 696 state.step = 0; 697 state.catalog = null; 698 state.probe = null; 699 state.busy = true; 700 setMessage(); 701 renderAll(); 702 try { 703 await loadCatalog(); 704 } catch (error) { 705 setMessage(error.message, "error"); 706 } finally { 707 state.busy = false; 708 renderAll(); 709 overlay.querySelector(".dcc-wizard-close")?.focus(); 710 } 711 } 712 713 function closeWizard() { 714 if (state.busy) return; 715 ensureOverlay().hidden = true; 716 } 717 718 function addSettingsCard() { 719 const dialog = document.querySelector(".settings-dialog"); 720 if (!dialog) return; 721 const activeTab = dialog.querySelector('[role="tab"][aria-selected="true"]'); 722 if (!activeTab || activeTab.textContent.trim().toUpperCase() !== "RADIO") return; 723 const panel = dialog.querySelector('[role="tabpanel"][data-state="active"]'); 724 if (!panel) return; 725 726 const section = panel.querySelector(".settings-section"); 727 const heading = section?.querySelector("h3"); 728 if (heading) heading.textContent = "Hamlib radio setup"; 729 const description = section?.querySelector("p"); 730 if (description) description.textContent = "Choose any installed Hamlib radio backend and configure its CAT connection, audio interface, and model-specific settings."; 731 const oldForm = panel.querySelector(".settings-form"); 732 const oldTest = panel.querySelector(".test-strip"); 733 if (oldForm) oldForm.style.display = "none"; 734 if (oldTest) oldTest.style.display = "none"; 735 section?.querySelectorAll("button").forEach(button => { 736 if (button.textContent.toUpperCase().includes("AUTO DETECT")) button.style.display = "none"; 737 }); 738 739 if (panel.querySelector(".dcc-radio-setup-card")) return; 740 const card = document.createElement("div"); 741 card.className = "dcc-radio-setup-card"; 742 card.innerHTML = `<div><strong>Connect any Hamlib-supported radio</strong><span>Automatic hardware clues, the complete WSJT-X/Hamlib radio list, model-specific help, CAT/PTT questions, audio selection, and a receive-only connection test.</span></div><button type="button">Get my radio connected</button>`; 743 card.querySelector("button")?.addEventListener("click", openWizard); 744 section?.insertAdjacentElement("afterend", card); 745 } 746 747 window.dccRadioSetupOpen = openWizard; 748 ensureOverlay(); 749 addSettingsCard(); 750 setInterval(addSettingsCard, 400); 751})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.