From d2b46822652f9406f18fbbd5ecea778b99e16f2d Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Fri, 15 Nov 2024 13:04:20 -0800 Subject: [PATCH] Add automatic fallback to DFU updates if no serial port is available; clean up code and UX during the flashing process --- src/api/hardware/index.js | 2 +- src/renderer/App.js | 18 ++++++ src/renderer/screens/FirmwareUpdate.js | 40 +++++++----- .../FirmwareUpdate/BootloaderConnectDialog.js | 2 +- .../FirmwareUpdate/FlashConfirmDialog.js | 25 ++++---- .../FirmwareUpdate/FocusConnectDialog.js | 8 +-- src/renderer/screens/KeyboardSelect.js | 39 +++++++++--- src/renderer/utils/URLParameters.js | 48 +++++++++++++++ src/renderer/utils/connectToDfuUsbPort.js | 47 ++++++++++---- src/renderer/utils/connectToSerialport.js | 61 +++++++++++-------- 10 files changed, 210 insertions(+), 80 deletions(-) create mode 100644 src/renderer/utils/URLParameters.js diff --git a/src/api/hardware/index.js b/src/api/hardware/index.js index 4294407ce..0c1b19a42 100644 --- a/src/api/hardware/index.js +++ b/src/api/hardware/index.js @@ -35,7 +35,7 @@ export const Hardware = { nonSerial: [Model100], }; -function getDeviceProtocol(vid, pid) { +export function getDeviceProtocol(vid, pid) { for (const device of Object.values(Hardware.devices)) { if (device.usb) { // Check main USB vid/pid diff --git a/src/renderer/App.js b/src/renderer/App.js index 94c1fa3e2..9b52da5e3 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -46,6 +46,7 @@ import SystemInfo from "./screens/SystemInfo"; import HelpConnection from "./screens/Help/Connection"; import { Store } from "@renderer/localStore"; import logger from "@renderer/utils/Logger"; +import urlParameters from "@renderer/utils/URLParameters"; const settings = new Store(); const App = (props) => { @@ -114,6 +115,23 @@ const App = (props) => { setupLayout(); }, []); + useEffect(() => { + // Parse URL parameters if they exist + const searchParams = window.location.search; + if (searchParams) { + urlParameters.parseFromURL(searchParams); + + // Remove parameters from URL without triggering a reload + const newUrl = window.location.pathname + window.location.hash; + window.history.replaceState({}, "", newUrl); + + // If we have device identifiers, navigate to keyboard select + if (urlParameters.hasDeviceIdentifiers()) { + navigate("/keyboard-select"); + } + } + }, []); + useEffect(() => { // Only navigate to /keyboard-select if the LocationProvider says we're not already on another page if (!connected && history?.location?.pathname === "/sanity-check") { diff --git a/src/renderer/screens/FirmwareUpdate.js b/src/renderer/screens/FirmwareUpdate.js index f45c5542e..f94d1f35e 100644 --- a/src/renderer/screens/FirmwareUpdate.js +++ b/src/renderer/screens/FirmwareUpdate.js @@ -84,6 +84,15 @@ const FirmwareUpdate = (props) => { const NOTIFICATION_THRESHOLD = 5; + useEffect(() => { + if (activeDevice?.focus?.in_bootloader) { + logger.log("Using existing bootloader Focus instance"); + setIsBootloader(true); + setFactoryReset(true); + setbootloaderProtocol(activeDevice.focus.focusDeviceDescriptor?.usb?.bootloader?.protocol); + } + }, [activeDevice]); + const loadDefaultFirmwareContent = async () => { const { vendor, product } = focusDeviceDescriptor.info; const firmwareType = focusDeviceDescriptor.info.firmwareType || "hex"; @@ -140,19 +149,17 @@ const FirmwareUpdate = (props) => { const flasher = activeDevice.getFlasher(); await onStepChange("flash"); let firmwareToSend = firmwareContent; - logger.log(focus); if (selectedFirmwareType == "default") { firmwareToSend = await loadDefaultFirmwareContent(); } - logger.log("about to flash"); - logger.log(" the usb device is ", port); - logger.log(" firmware content is ", firmwareToSend); - if (bootloaderProtocol == "avr109") { - await flasher.flash(focus._port, firmwareToSend); - } else if (bootloaderProtocol == "dfu") { - await flasher.flash(port, firmwareToSend); - } + logger.log("Flashing with protocol:", bootloaderProtocol); + logger.log("Active device:", activeDevice); + logger.log("Port:", port); + + const portToUse = activeDevice.focus?.in_bootloader ? activeDevice.focus._port : port._port; + + await flasher.flash(portToUse, firmwareToSend); }; const updateDeviceFirmware = async () => { @@ -255,9 +262,14 @@ const FirmwareUpdate = (props) => { const focus = await connectToSerialport(); return focus; } else if (bootloaderProtocol == "dfu") { - const usb = await connectToDfuUsbPort(); - return usb; + const focus = await connectToDfuUsbPort(); + if (focus) { + // Store the USB device reference for flashing + setUsbDevice(focus._port); + return focus; + } } + return null; }; const connectToBootloader = async (callback) => { @@ -437,12 +449,10 @@ const FirmwareUpdate = (props) => { onConfirm={upload} onCancel={() => setConfirmationOpen(false)} isFactoryReset={factoryReset} + activeDevice={activeDevice} /> - + ); }; diff --git a/src/renderer/screens/FirmwareUpdate/BootloaderConnectDialog.js b/src/renderer/screens/FirmwareUpdate/BootloaderConnectDialog.js index f44a19746..64f256122 100644 --- a/src/renderer/screens/FirmwareUpdate/BootloaderConnectDialog.js +++ b/src/renderer/screens/FirmwareUpdate/BootloaderConnectDialog.js @@ -25,4 +25,4 @@ const BootloaderConnectDialog = ({ open, onConnect, bootloaderProtocol, connectT ); }; -export default BootloaderConnectDialog; \ No newline at end of file +export default BootloaderConnectDialog; diff --git a/src/renderer/screens/FirmwareUpdate/FlashConfirmDialog.js b/src/renderer/screens/FirmwareUpdate/FlashConfirmDialog.js index 3ea2eed46..e9c44c626 100644 --- a/src/renderer/screens/FirmwareUpdate/FlashConfirmDialog.js +++ b/src/renderer/screens/FirmwareUpdate/FlashConfirmDialog.js @@ -5,16 +5,13 @@ import Alert from "@mui/material/Alert"; import AlertTitle from "@mui/material/AlertTitle"; import ConfirmationDialog from "@renderer/components/ConfirmationDialog"; -const FlashConfirmDialog = ({ open, onConfirm, onCancel, isFactoryReset }) => { +const FlashConfirmDialog = ({ open, onConfirm, onCancel, isFactoryReset, activeDevice }) => { const { t } = useTranslation(); + console.log("activeDevice", activeDevice); return ( { ? t("firmwareUpdate.factoryConfirmDialog.contents") : t("firmwareUpdate.confirmDialog.description")} - - {t("firmwareUpdate.calloutTitle")} - - {t("hardware.updateInstructions")} - - + {activeDevice?.focus.in_bootloader || ( + + {t("firmwareUpdate.calloutTitle")} + + {t("hardware.updateInstructions")} + + + )} ); }; -export default FlashConfirmDialog; \ No newline at end of file +export default FlashConfirmDialog; diff --git a/src/renderer/screens/FirmwareUpdate/FocusConnectDialog.js b/src/renderer/screens/FirmwareUpdate/FocusConnectDialog.js index 34ea2376f..e6db66bee 100644 --- a/src/renderer/screens/FirmwareUpdate/FocusConnectDialog.js +++ b/src/renderer/screens/FirmwareUpdate/FocusConnectDialog.js @@ -19,11 +19,7 @@ const FocusConnectDialog = ({ open, onConnect }) => { }; return ( - + {t("firmwareUpdate.reconnectDialog.contents")} @@ -31,4 +27,4 @@ const FocusConnectDialog = ({ open, onConnect }) => { ); }; -export default FocusConnectDialog; \ No newline at end of file +export default FocusConnectDialog; diff --git a/src/renderer/screens/KeyboardSelect.js b/src/renderer/screens/KeyboardSelect.js index 00e48a9cc..3e4196a52 100644 --- a/src/renderer/screens/KeyboardSelect.js +++ b/src/renderer/screens/KeyboardSelect.js @@ -31,11 +31,15 @@ import logo from "@renderer/logo-small.png"; import { navigate } from "@renderer/routerHistory"; import logger from "@renderer/utils/Logger"; import { connectToSerialport } from "@renderer/utils/connectToSerialport"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { ConnectionButton } from "./KeyboardSelect/ConnectionButton"; import { DeviceImage } from "./KeyboardSelect/DeviceImage"; import { ProductStatus } from "./KeyboardSelect/ProductStatus"; +import { connectToDfuUsbPort } from "../utils/connectToDfuUsbPort"; +import { getDeviceProtocol } from "@api/hardware"; +import urlParameters from "@renderer/utils/URLParameters"; +import Button from "@mui/material/Button"; const KeyboardSelect = (props) => { const [opening, setOpening] = useState(false); @@ -56,25 +60,44 @@ const KeyboardSelect = (props) => { try { setLoading(true); - logger.log("in connectToKeyboard"); + + // Check if we have URL parameters to determine the connection method + if (urlParameters.hasDeviceIdentifiers()) { + const protocol = getDeviceProtocol(urlParameters.vid, urlParameters.pid); + logger.log("Detected protocol for device:", protocol, { vid: urlParameters.vid, pid: urlParameters.pid }); + + if (protocol === "dfu") { + const bootloaderFocus = await connectToDfuUsbPort(urlParameters.vid, urlParameters.pid); + if (bootloaderFocus) { + props.onConnect(bootloaderFocus); + urlParameters.clear(); + await navigate("/firmware-update"); + return; + } + } + } + + // Fall back to regular connection attempt const focus = await connectToSerialport(); if (focus) { - logger.log("Calling props.onConnect with the focus object"); - logger.log("focus", focus); props.onConnect(focus); - logger.log("Got a device"); } else { - logger.log("looks like the user aborted"); - setOpening(false); + const bootloaderFocus = await connectToDfuUsbPort(); + if (bootloaderFocus) { + props.onConnect(bootloaderFocus); + await navigate("/firmware-update"); + } } } catch (err) { logger.error("error while trying to connect", { error: err, device: activeDevice, }); - setOpening(false); await navigate("/help/connection-failed"); toast.error(t("keyboardSelect.connectionFailed", { error: err.toString() })); + } finally { + setOpening(false); + setLoading(false); } }; diff --git a/src/renderer/utils/URLParameters.js b/src/renderer/utils/URLParameters.js new file mode 100644 index 000000000..5161ba933 --- /dev/null +++ b/src/renderer/utils/URLParameters.js @@ -0,0 +1,48 @@ +class URLParameters { + constructor() { + this.vid = null; + this.pid = null; + this.version = null; + } + + parseFromURL(searchString) { + // Handle both ? and ; separated parameters + const params = searchString + .replace("?", "") + .split(";") + .reduce((acc, param) => { + const [key, value] = param.split("="); + if (key && value) acc[key] = value; + return acc; + }, {}); + + // Parse VID (handle both "0x3496" and "3496" formats) + if (params.vid) { + this.vid = parseInt(params.vid.replace("0x", ""), 16); + } + + // Parse PID (handle both "0x0005" and "0005" formats) + if (params.pid) { + this.pid = parseInt(params.pid.replace("0x", ""), 16); + } + + // Parse version + this.version = params.version || null; + + return this; + } + + clear() { + this.vid = null; + this.pid = null; + this.version = null; + } + + hasDeviceIdentifiers() { + return this.vid !== null && this.pid !== null; + } +} + +// Create a singleton instance +const urlParameters = new URLParameters(); +export default urlParameters; diff --git a/src/renderer/utils/connectToDfuUsbPort.js b/src/renderer/utils/connectToDfuUsbPort.js index b77149d05..b9656bb84 100644 --- a/src/renderer/utils/connectToDfuUsbPort.js +++ b/src/renderer/utils/connectToDfuUsbPort.js @@ -1,19 +1,44 @@ -import { getDfuDevices } from "@api/hardware"; +import { getDfuDevices, Hardware } from "@api/hardware"; import logger from "@renderer/utils/Logger"; +import Focus from "@api/focus"; -const connectToDfuUsbPort = async () => { - let usb; +export const connectToDfuUsbPort = async (targetVid, targetPid) => { try { - const devices = await navigator.usb.getDevices(); - logger.log("devices", devices); - usb = await navigator.usb.requestDevice({ - filters: getDfuDevices(), - }); + let filters = getDfuDevices(); + + // If we have target VID/PID, only look for that specific device + if (targetVid && targetPid) { + filters = [ + { + vendorId: targetVid, + productId: targetPid, + }, + ]; + } + + const usb = await navigator.usb.requestDevice({ filters }); + + if (usb) { + // Create and configure a new Focus instance + const focus = new Focus(); + focus.in_bootloader = true; + focus._port = usb; + + // Find the matching device from Hardware.devices + const matchingDevice = Hardware.devices.find( + (device) => + device.usb?.bootloader?.vendorId === usb.vendorId && device.usb?.bootloader?.productId === usb.productId + ); + + if (matchingDevice) { + // Set the full device descriptor + focus.focusDeviceDescriptor = matchingDevice; + return focus; + } + } } catch (e) { logger.error("Failed to open usb port", e); } - return usb; + return null; }; - -export { connectToDfuUsbPort }; diff --git a/src/renderer/utils/connectToSerialport.js b/src/renderer/utils/connectToSerialport.js index f3911cfbe..379362bdf 100644 --- a/src/renderer/utils/connectToSerialport.js +++ b/src/renderer/utils/connectToSerialport.js @@ -19,7 +19,7 @@ import { Hardware, supportedDeviceVIDPIDs } from "@api/hardware"; import logger from "@renderer/utils/Logger"; // returns a promise that resolves to a Focus object -export const connectToSerialport = async () => { +export const connectToSerialport = async (targetVid, targetPid) => { const focus = new Focus(); let serialPort; @@ -76,33 +76,44 @@ export const connectToSerialport = async () => { await serialPort.open({ baudRate: 9600 }); }; - await openPort(); + try { + await openPort(); - if (!serialPort) { - logger.log("The user didn't select a serialport"); - return; - } - const info = serialPort.getInfo(); + if (!serialPort) { + logger.log("No serialport selected"); + return null; + } - const dVid = info.usbVendorId; - const dPid = info.usbProductId; - logger.log("The connected device:", info); - for (const hw of Hardware.devices) { - let found = false; - let bootloader = false; - if (dVid == hw.usb.vendorId && dPid == hw.usb.productId) { - found = true; - logger.log("Found a keyboard", hw); - focus.open(serialPort, hw); - } else if (dVid == hw.usb.bootloader?.vendorId && dPid == hw.usb.bootloader?.productId) { - found = true; - bootloader = true; - logger.log("Found a keyboard bootloader", hw); + const info = serialPort.getInfo(); + const dVid = info.usbVendorId; + const dPid = info.usbProductId; - focus.open(serialPort, hw); + // If we have target VID/PID, check if this device matches + if (targetVid && targetPid && (dVid !== targetVid || dPid !== targetPid)) { + return null; } - if (!found) continue; - } - return focus; + logger.log("The connected device:", info); + for (const hw of Hardware.devices) { + let found = false; + let bootloader = false; + if (dVid == hw.usb.vendorId && dPid == hw.usb.productId) { + found = true; + logger.log("Found a keyboard", hw); + focus.open(serialPort, hw); + } else if (dVid == hw.usb.bootloader?.vendorId && dPid == hw.usb.bootloader?.productId) { + found = true; + bootloader = true; + logger.log("Found a keyboard bootloader", hw); + + focus.open(serialPort, hw); + } + if (!found) continue; + } + + return focus; + } catch (e) { + logger.error("Failed to open serial port", e); + return null; + } };