Skip to content

Commit

Permalink
Add automatic fallback to DFU updates if no serial port is available;…
Browse files Browse the repository at this point in the history
… clean up code and UX during the flashing process
  • Loading branch information
obra committed Nov 15, 2024
1 parent 85ed06c commit d2b4682
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 80 deletions.
2 changes: 1 addition & 1 deletion src/api/hardware/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions src/renderer/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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") {
Expand Down
40 changes: 25 additions & 15 deletions src/renderer/screens/FirmwareUpdate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -437,12 +449,10 @@ const FirmwareUpdate = (props) => {
onConfirm={upload}
onCancel={() => setConfirmationOpen(false)}
isFactoryReset={factoryReset}
activeDevice={activeDevice}
/>

<FlashNotification
open={flashNotificationMsg !== RebootMessage.clear}
message={flashNotificationMsg}
/>
<FlashNotification open={flashNotificationMsg !== RebootMessage.clear} message={flashNotificationMsg} />
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ const BootloaderConnectDialog = ({ open, onConnect, bootloaderProtocol, connectT
);
};

export default BootloaderConnectDialog;
export default BootloaderConnectDialog;
25 changes: 12 additions & 13 deletions src/renderer/screens/FirmwareUpdate/FlashConfirmDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<ConfirmationDialog
title={
isFactoryReset
? t("firmwareUpdate.factoryConfirmDialog.title")
: t("firmwareUpdate.confirmDialog.title")
}
title={isFactoryReset ? t("firmwareUpdate.factoryConfirmDialog.title") : t("firmwareUpdate.confirmDialog.title")}
open={open}
onConfirm={onConfirm}
onCancel={onCancel}
Expand All @@ -25,14 +22,16 @@ const FlashConfirmDialog = ({ open, onConfirm, onCancel, isFactoryReset }) => {
? t("firmwareUpdate.factoryConfirmDialog.contents")
: t("firmwareUpdate.confirmDialog.description")}
</Typography>
<Alert severity="info">
<AlertTitle>{t("firmwareUpdate.calloutTitle")}</AlertTitle>
<Typography component="p" gutterBottom>
{t("hardware.updateInstructions")}
</Typography>
</Alert>
{activeDevice?.focus.in_bootloader || (
<Alert severity="info">
<AlertTitle>{t("firmwareUpdate.calloutTitle")}</AlertTitle>
<Typography component="p" gutterBottom>
{t("hardware.updateInstructions")}
</Typography>
</Alert>
)}
</ConfirmationDialog>
);
};

export default FlashConfirmDialog;
export default FlashConfirmDialog;
8 changes: 2 additions & 6 deletions src/renderer/screens/FirmwareUpdate/FocusConnectDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,12 @@ const FocusConnectDialog = ({ open, onConnect }) => {
};

return (
<ConfirmationDialog
open={open}
title={t("firmwareUpdate.reconnectDialog.title")}
onConfirm={handleConfirm}
>
<ConfirmationDialog open={open} title={t("firmwareUpdate.reconnectDialog.title")} onConfirm={handleConfirm}>
<Typography component="p" sx={{ mb: 2 }}>
{t("firmwareUpdate.reconnectDialog.contents")}
</Typography>
</ConfirmationDialog>
);
};

export default FocusConnectDialog;
export default FocusConnectDialog;
39 changes: 31 additions & 8 deletions src/renderer/screens/KeyboardSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
};

Expand Down
48 changes: 48 additions & 0 deletions src/renderer/utils/URLParameters.js
Original file line number Diff line number Diff line change
@@ -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;
47 changes: 36 additions & 11 deletions src/renderer/utils/connectToDfuUsbPort.js
Original file line number Diff line number Diff line change
@@ -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 };
Loading

0 comments on commit d2b4682

Please sign in to comment.