@sensebox/flash-tool 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/basic-OBQD77DM.js +6 -0
- package/dist/basic-OBQD77DM.js.map +1 -0
- package/dist/basic-PDQS3NQS.cjs +6 -0
- package/dist/basic-PDQS3NQS.cjs.map +1 -0
- package/dist/index.cjs +514 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +574 -112
- package/dist/index.js.map +1 -1
- package/dist/style.css +288 -11
- package/package.json +1 -1
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/eric/Documents/arbeit/sensebox-flash-tool/dist/index.cjs","../src/react/FlashTool.jsx","../src/react/useFlashDevice.js","../src/core/esptool.js","../src/firmware/index.js"],"names":[],"mappings":"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","file":"/Users/eric/Documents/arbeit/sensebox-flash-tool/dist/index.cjs","sourcesContent":[null,"import React, { useEffect, useRef } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport PropTypes from \"prop-types\";\nimport { useFlashDevice } from \"./useFlashDevice.js\";\nimport { loadBuiltinFirmware } from \"../firmware/index.js\";\n\n// Loaded builtin firmware bytes are cached across component instances/\n// re-renders (keyed by type) so switching the dropdown back and forth, or\n// mounting more than one <FlashTool />, doesn't re-decode the same image.\nconst builtinFirmwareCache = new Map();\n\n// Styles are shipped separately (see package.json \"./style.css\" export) so\n// consumers explicitly opt in with `import \"@sensebox/flash-tool/style.css\"`\n// instead of this component silently injecting global CSS.\n\nconst FIRMWARE_TYPES = [\"circuitpy\", \"ota\", \"basic\", \"standard\"];\n\n// circuitpy/ota/basic/standard are all pre-merged images (bootloader +\n// partition table + otadata + app[+ filesystem] already combined into one\n// file via `esptool merge_bin`), so they must be written starting at 0x0\n// with the whole chip erased first. Otherwise a previous partition table,\n// a stale otadata selection, and/or a stale filesystem partition survive -\n// e.g. CircuitPython never mounts as CIRCUITPY, or `basic` boots whatever\n// was left behind by a previously flashed image instead of the new sketch.\n// Overridable per type via the `firmwareFlashOptions` prop.\nconst BUILTIN_FIRMWARE_FLASH_OVERRIDES = {\n circuitpy: { address: 0x0, eraseAll: true },\n ota: { address: 0x0, eraseAll: true },\n basic: { address: 0x0, eraseAll: true },\n standard: { address: 0x0, eraseAll: true },\n};\n\nconst DEFAULT_STRINGS = {\n unsupported: \"Web Serial is not supported in this browser.\",\n connect: \"Connect board\",\n connecting: \"Connecting...\",\n connected: \"senseBox connected\",\n prepare: \"Prepare upload\",\n preparing: \"Preparing...\",\n grantPermission: \"Select bootloader port\",\n waitingForBinary: \"Waiting for firmware...\",\n loadingFirmware: \"Loading firmware...\",\n flash: \"Upload\",\n flashing: \"Uploading...\",\n done: \"Upload complete\",\n error: \"Upload failed\",\n retry: \"Retry\",\n disconnect: \"Disconnect\",\n showLog: \"Show log\",\n hideLog: \"Hide log\",\n sketchLabel: \"Sketch\",\n firmwareCircuitpy: \"CircuitPython\",\n firmwareOta: \"senseBox OTA\",\n firmwareBasic: \"Basic\",\n firmwareStandard: \"Standard senseBox Sketch\",\n help: \"Help\",\n helpTitle: \"How it works\",\n helpStep1: \"Connect your senseBox\",\n helpStep2: \"Select a sketch\",\n helpStep3: \"Give your browser permission\",\n helpStep4: \"Start the upload\",\n helpClose: \"Close\",\n};\n\n/**\n * Self-contained widget for connecting to and flashing an ESP32-family board\n * (senseBox MCU-S2 by default) over the Web Serial API. Renders its own\n * compact status panel - drop it anywhere in a React tree.\n *\n * This component is flash-only: it expects an already-compiled firmware\n * image via the `binary` prop. Pair it with whatever compiler/build step\n * your app uses.\n */\nexport function FlashTool({\n binary,\n firmwares,\n firmwareType,\n defaultFirmwareType,\n firmwareFlashOptions,\n usbVendorId,\n flashOptions,\n autoFlash = true,\n strings,\n className,\n onConnected,\n onDisconnected,\n onFlashed,\n onError,\n onFirmwareTypeChange,\n}) {\n const merged = { ...DEFAULT_STRINGS, ...strings };\n const [logOpen, setLogOpen] = React.useState(false);\n const [helpOpen, setHelpOpen] = React.useState(false);\n const [busy, setBusy] = React.useState(null); // \"connect\" | \"prepare\" | \"grant\" | null\n const [selectedType, setSelectedType] = React.useState(\n firmwareType || defaultFirmwareType || FIRMWARE_TYPES[0],\n );\n\n const firmwareLocked = Boolean(firmwareType);\n\n // The sketch-type dropdown only actually changes what gets flashed when\n // there's more than one binary to choose from: a `firmwares` map, or no\n // caller-supplied `binary` at all (dropdown drives which bundled firmware\n // is loaded). A caller who passes a single fixed `binary` without also\n // locking `firmwareType` would otherwise see an interactive dropdown that\n // silently does nothing - `explicitBinary` below always resolves to that\n // same fixed `binary` regardless of the selected type, so it looks like\n // choosing e.g. \"basic\" should flash the basic sketch when it actually\n // keeps flashing whatever `binary` was passed in.\n const showFirmwarePicker = Boolean(firmwares) || !binary || firmwareLocked;\n\n useEffect(() => {\n if (firmwareType) setSelectedType(firmwareType);\n }, [firmwareType]);\n\n useEffect(() => {\n if (!helpOpen) return;\n const onKeyDown = (event) => {\n if (event.key === \"Escape\") setHelpOpen(false);\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [helpOpen]);\n\n const {\n supported,\n connected,\n bootloaderReady,\n deviceLabel,\n status,\n progress,\n log,\n error,\n needsBootloaderPermission,\n selectDevice,\n disconnect,\n prepareBootloader,\n grantBootloaderPort,\n flash,\n resetFlashState,\n } = useFlashDevice({ usbVendorId, flashOptions });\n\n // Either pick the binary out of a `{ circuitpy, ota, basic, standard }`\n // map keyed by the dropdown selection, or fall back to a single `binary`\n // prop for callers that don't need the firmware picker at all.\n const explicitBinary = firmwares ? firmwares[selectedType] : binary;\n\n const [builtinBinary, setBuiltinBinary] = React.useState(null);\n const [builtinLoading, setBuiltinLoading] = React.useState(false);\n const [builtinError, setBuiltinError] = React.useState(null);\n const [builtinRetryToken, setBuiltinRetryToken] = React.useState(0);\n\n // No binary supplied by the caller for this sketch type - fall back to the\n // firmware image bundled with the package, loaded on demand via a plain\n // dynamic import() (see src/firmware/index.js) and cached. Skipped\n // entirely when Web Serial isn't even available, so an unsupported browser\n // never does the (currently pointless) decode work the instant the widget\n // mounts.\n useEffect(() => {\n if (!supported || explicitBinary) return;\n\n const cached = builtinFirmwareCache.get(selectedType);\n if (cached) {\n setBuiltinBinary(cached);\n setBuiltinError(null);\n return;\n }\n\n let cancelled = false;\n setBuiltinBinary(null);\n setBuiltinError(null);\n setBuiltinLoading(true);\n\n loadBuiltinFirmware(selectedType)\n .then((bytes) => {\n if (cancelled || !bytes) return;\n builtinFirmwareCache.set(selectedType, bytes);\n setBuiltinBinary(bytes);\n })\n .catch((err) => {\n if (cancelled) return;\n const message = err?.message || String(err);\n console.error(\n `[FlashTool] Failed to load builtin \"${selectedType}\" firmware`,\n err,\n );\n setBuiltinError(message);\n onError?.(err instanceof Error ? err : new Error(message));\n })\n .finally(() => {\n if (!cancelled) setBuiltinLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n }, [supported, explicitBinary, selectedType, onError, builtinRetryToken]);\n\n const resolvedBinary = explicitBinary || builtinBinary || undefined;\n\n // The flash-address/erase override depends on which kind of image is\n // selected, not on whether a `firmwares` map was supplied - a single\n // `binary` prop locked via `firmwareType=\"circuitpy\"` needs it too. Only\n // applied when the picker is actually meaningful (see showFirmwarePicker)\n // - otherwise `selectedType` doesn't correspond to anything real and\n // applying its overrides to an unrelated fixed `binary` would be wrong.\n const flashOverrides = React.useMemo(\n () =>\n showFirmwarePicker\n ? {\n ...BUILTIN_FIRMWARE_FLASH_OVERRIDES[selectedType],\n ...firmwareFlashOptions?.[selectedType],\n }\n : {},\n [showFirmwarePicker, firmwareFlashOptions, selectedType],\n );\n\n // The board reports progress only while it's actually writing data - the\n // upfront full-chip erase (needed for circuitpy/ota/standard) can take\n // 10-20s with the bar pinned at 0%, so surface the latest esptool log line\n // (\"Erasing flash...\") to show the widget is still working, not stuck.\n const latestLogLine = React.useMemo(() => {\n if (!log) return \"\";\n const lines = log.split(\"\\n\").filter((line) => line.trim().length > 0);\n return lines[lines.length - 1] || \"\";\n }, [log]);\n\n const handleFirmwareChange = (event) => {\n const value = event.target.value;\n setSelectedType(value);\n onFirmwareTypeChange?.(value);\n };\n\n const handleRetryFirmwareLoad = () => {\n setBuiltinRetryToken((token) => token + 1);\n };\n\n const prevConnected = useRef(connected);\n useEffect(() => {\n if (connected && !prevConnected.current) onConnected?.(deviceLabel);\n if (!connected && prevConnected.current) onDisconnected?.();\n prevConnected.current = connected;\n }, [connected, deviceLabel, onConnected, onDisconnected]);\n\n useEffect(() => {\n if (status === \"done\") onFlashed?.();\n if (status === \"error\" && error) onError?.(new Error(error));\n }, [status, error, onFlashed, onError]);\n\n // Auto-flash as soon as a binary is ready and the device is connected.\n useEffect(() => {\n if (\n autoFlash &&\n resolvedBinary &&\n connected &&\n !bootloaderReady &&\n status === \"idle\" &&\n !needsBootloaderPermission\n ) {\n flash(resolvedBinary, flashOverrides);\n }\n }, [\n autoFlash,\n resolvedBinary,\n flashOverrides,\n connected,\n bootloaderReady,\n status,\n needsBootloaderPermission,\n flash,\n ]);\n\n if (!supported) {\n return (\n <div className={`sbflash sbflash--unsupported ${className || \"\"}`}>\n {merged.unsupported}\n </div>\n );\n }\n\n const handleConnect = async () => {\n setBusy(\"connect\");\n try {\n await selectDevice();\n } finally {\n setBusy(null);\n }\n };\n\n const handlePrepare = async () => {\n setBusy(\"prepare\");\n try {\n await prepareBootloader();\n } finally {\n setBusy(null);\n }\n };\n\n const handleGrant = async () => {\n setBusy(\"grant\");\n try {\n await grantBootloaderPort();\n if (resolvedBinary) await flash(resolvedBinary, flashOverrides);\n } finally {\n setBusy(null);\n }\n };\n\n const handleFlash = async () => {\n if (resolvedBinary) await flash(resolvedBinary, flashOverrides);\n };\n\n const handleRetry = () => {\n resetFlashState();\n if (resolvedBinary) flash(resolvedBinary, flashOverrides);\n };\n\n return (\n <div className={`sbflash ${className || \"\"}`}>\n <div className=\"sbflash__row\">\n <span\n className={`sbflash__dot sbflash__dot--${\n status === \"error\"\n ? \"error\"\n : status === \"flashing\"\n ? \"busy\"\n : connected\n ? \"ok\"\n : \"off\"\n }`}\n />\n <span\n className=\"sbflash__label\"\n title={connected ? deviceLabel : undefined}\n >\n {connected ? merged.connected : merged.connect}\n </span>\n </div>\n\n <div className=\"sbflash__field\">\n <div className=\"sbflash__field-head\">\n <span className=\"sbflash__field-label\">{merged.sketchLabel}</span>\n <div className=\"sbflash__icon-row\">\n {log && (\n <button\n type=\"button\"\n className={`sbflash__icon-button${\n logOpen ? \" sbflash__icon-button--active\" : \"\"\n }`}\n onClick={() => setLogOpen((v) => !v)}\n aria-pressed={logOpen}\n aria-label={logOpen ? merged.hideLog : merged.showLog}\n title={logOpen ? merged.hideLog : merged.showLog}\n >\n <IconTerminal />\n </button>\n )}\n {connected && (\n <button\n type=\"button\"\n className=\"sbflash__icon-button\"\n onClick={disconnect}\n aria-label={merged.disconnect}\n title={merged.disconnect}\n >\n <IconDisconnect />\n </button>\n )}\n <button\n type=\"button\"\n className=\"sbflash__icon-button\"\n onClick={() => setHelpOpen(true)}\n aria-label={merged.help}\n title={merged.help}\n >\n <IconHelp />\n </button>\n </div>\n </div>\n {showFirmwarePicker && (\n <div className=\"sbflash__select-wrap\">\n <select\n className=\"sbflash__select\"\n value={selectedType}\n onChange={handleFirmwareChange}\n disabled={firmwareLocked || status === \"flashing\"}\n >\n {FIRMWARE_TYPES.map((type) => (\n <option key={type} value={type}>\n {merged[\n `firmware${type.charAt(0).toUpperCase()}${type.slice(1)}`\n ]}\n </option>\n ))}\n </select>\n </div>\n )}\n {builtinError && (\n <div className=\"sbflash__field-error-row\">\n <span className=\"sbflash__field-error\">{builtinError}</span>\n <button\n type=\"button\"\n className=\"sbflash__link\"\n onClick={handleRetryFirmwareLoad}\n >\n {merged.retry}\n </button>\n </div>\n )}\n </div>\n\n {helpOpen &&\n createPortal(\n <div\n className=\"sbflash__modal-overlay\"\n onClick={() => setHelpOpen(false)}\n >\n <div\n className=\"sbflash__modal\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={merged.helpTitle}\n onClick={(event) => event.stopPropagation()}\n >\n <div className=\"sbflash__modal-head\">\n <span className=\"sbflash__modal-title\">\n {merged.helpTitle}\n </span>\n <button\n type=\"button\"\n className=\"sbflash__modal-close\"\n onClick={() => setHelpOpen(false)}\n aria-label={merged.helpClose}\n >\n ×\n </button>\n </div>\n <ol className=\"sbflash__modal-steps\">\n {[\n merged.helpStep1,\n merged.helpStep2,\n merged.helpStep3,\n merged.helpStep4,\n ].map((step, index) => (\n <li key={index} className=\"sbflash__modal-step\">\n <span className=\"sbflash__modal-step-index\">\n {index + 1}\n </span>\n <span>{step}</span>\n </li>\n ))}\n </ol>\n </div>\n </div>,\n document.body,\n )}\n\n {!connected && (\n <button\n type=\"button\"\n className=\"sbflash__button sbflash__button--primary\"\n onClick={handleConnect}\n disabled={busy === \"connect\"}\n >\n {busy === \"connect\" ? merged.connecting : merged.connect}\n </button>\n )}\n\n {connected && needsBootloaderPermission && (\n <button\n type=\"button\"\n className=\"sbflash__button sbflash__button--primary\"\n onClick={handleGrant}\n disabled={busy === \"grant\"}\n >\n {merged.grantPermission}\n </button>\n )}\n\n {connected && !needsBootloaderPermission && !bootloaderReady && (\n <button\n type=\"button\"\n className=\"sbflash__button sbflash__button--secondary\"\n onClick={handlePrepare}\n disabled={busy === \"prepare\" || status === \"flashing\"}\n >\n {busy === \"prepare\" ? merged.preparing : merged.prepare}\n </button>\n )}\n\n {connected &&\n bootloaderReady &&\n status !== \"flashing\" &&\n status !== \"error\" && (\n <button\n type=\"button\"\n className=\"sbflash__button sbflash__button--primary\"\n onClick={handleFlash}\n disabled={!resolvedBinary}\n >\n {resolvedBinary\n ? merged.flash\n : builtinLoading\n ? merged.loadingFirmware\n : merged.waitingForBinary}\n </button>\n )}\n\n {status === \"flashing\" &&\n (progress > 0 ? (\n <div className=\"sbflash__progress-wrap\">\n <div className=\"sbflash__progress\">\n <div className=\"sbflash__progress-track\">\n <div\n className=\"sbflash__progress-fill\"\n style={{ width: `${Math.min(100, progress)}%` }}\n />\n </div>\n <span className=\"sbflash__progress-value\">{progress}%</span>\n </div>\n {latestLogLine && (\n <span className=\"sbflash__progress-detail\">\n {latestLogLine}\n </span>\n )}\n </div>\n ) : (\n <div className=\"sbflash__task\">\n <span className=\"sbflash__task-spinner\" />\n <span className=\"sbflash__task-label\">\n {latestLogLine || merged.flashing}\n </span>\n </div>\n ))}\n\n {status === \"done\" && (\n <div className=\"sbflash__status sbflash__status--ok\">\n {merged.done}\n </div>\n )}\n\n {status === \"error\" && (\n <div className=\"sbflash__status sbflash__status--error\">\n {merged.error}\n <button\n type=\"button\"\n className=\"sbflash__button sbflash__button--secondary\"\n onClick={handleRetry}\n >\n {merged.retry}\n </button>\n </div>\n )}\n\n {log && logOpen && <pre className=\"sbflash__log\">{log}</pre>}\n </div>\n );\n}\n\nfunction IconTerminal() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n width=\"12\"\n height=\"12\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <rect x=\"1.5\" y=\"2.5\" width=\"13\" height=\"11\" rx=\"2\" />\n <path d=\"M4.25 6.25 6.75 8.5l-2.5 2.25\" />\n <path d=\"M8.5 11h3\" />\n </svg>\n );\n}\n\nfunction IconDisconnect() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n width=\"12\"\n height=\"12\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M8 1.5v4.25\" />\n <path d=\"M4.5 5.5a5 5 0 1 0 7 0\" />\n </svg>\n );\n}\n\nfunction IconHelp() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n width=\"12\"\n height=\"12\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"8\" cy=\"8\" r=\"6.5\" />\n <path d=\"M6 6.25a2 2 0 1 1 2.75 1.85c-.6.25-.95.7-.95 1.4v.25\" />\n <circle cx=\"8\" cy=\"11.5\" r=\"0.9\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n );\n}\n\nFlashTool.propTypes = {\n /** Already-compiled firmware image to flash. Ignored if `firmwares` is set. */\n binary: PropTypes.instanceOf(Uint8Array),\n /** Map of sketch options to their compiled firmware image: { circuitpy, ota, basic, standard }. */\n firmwares: PropTypes.shape({\n circuitpy: PropTypes.instanceOf(Uint8Array),\n ota: PropTypes.instanceOf(Uint8Array),\n basic: PropTypes.instanceOf(Uint8Array),\n standard: PropTypes.instanceOf(Uint8Array),\n }),\n /** Locks the sketch dropdown to this value and disables it. One of \"circuitpy\" | \"ota\" | \"basic\" | \"standard\". */\n firmwareType: PropTypes.oneOf(FIRMWARE_TYPES),\n /** Initial dropdown selection when `firmwareType` isn't set (dropdown stays interactive). */\n defaultFirmwareType: PropTypes.oneOf(FIRMWARE_TYPES),\n /** Per-sketch overrides for the esptool flash step, keyed by firmware type. Merges over (and can override) the built-in CircuitPython defaults. */\n firmwareFlashOptions: PropTypes.objectOf(PropTypes.object),\n /** USB vendor id used to filter the device picker. Defaults to senseBox (0x303a). */\n usbVendorId: PropTypes.number,\n /** Overrides for the esptool flash step (address, baudrate, flashMode, flashFreq, flashSize, eraseAll, usingUsbOtg). */\n flashOptions: PropTypes.object,\n /** Start flashing automatically once the selected firmware and a connected device are both ready. */\n autoFlash: PropTypes.bool,\n /** Overrides for the built-in (English) UI strings. */\n strings: PropTypes.object,\n className: PropTypes.string,\n onConnected: PropTypes.func,\n onDisconnected: PropTypes.func,\n onFlashed: PropTypes.func,\n onError: PropTypes.func,\n /** Called with the newly selected sketch type when the dropdown changes. */\n onFirmwareTypeChange: PropTypes.func,\n};\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n DEFAULT_USB_VENDOR_ID,\n flashBinary,\n touchTo1200bps,\n waitForBootloaderPort,\n} from \"../core/esptool.js\";\n\nconst DEFAULT_FLASH_OPTIONS = {\n address: 0x10000,\n baudrate: 115200,\n flashMode: \"dio\",\n flashFreq: \"80m\",\n flashSize: \"4MB\",\n eraseAll: false,\n usingUsbOtg: true,\n};\n\nfunction labelForPort(port) {\n const info = port.getInfo?.() ?? {};\n const vendor = info.usbVendorId\n ? info.usbVendorId.toString(16).padStart(4, \"0\")\n : \"????\";\n const product = info.usbProductId\n ? info.usbProductId.toString(16).padStart(4, \"0\")\n : \"????\";\n return `USB device (VID 0x${vendor} / PID 0x${product})`;\n}\n\n/**\n * Framework-agnostic-ish (but React) state machine for connecting to an\n * ESP32-family board over Web Serial, putting it into bootloader mode and\n * flashing a firmware binary supplied by the caller.\n *\n * This hook does NOT compile anything - `binary` must already be a\n * Uint8Array of the firmware image. Pair it with your own compile step, or\n * use <FlashTool /> which wraps this hook with a ready-made UI.\n *\n * @param {object} [options]\n * @param {number} [options.usbVendorId] USB vendor id to filter the device picker by.\n * @param {object} [options.flashOptions] Overrides for esptool flashBinary().\n */\nexport function useFlashDevice({\n usbVendorId = DEFAULT_USB_VENDOR_ID,\n flashOptions = {},\n} = {}) {\n const supported =\n typeof navigator !== \"undefined\" && navigator.serial !== undefined;\n\n const [port, setPort] = useState(null);\n const [bootloaderPort, setBootloaderPort] = useState(null);\n const [needsBootloaderPermission, setNeedsBootloaderPermission] =\n useState(false);\n const [deviceLabel, setDeviceLabel] = useState(\"\");\n const [status, setStatus] = useState(\"idle\"); // idle | flashing | done | error\n const [progress, setProgress] = useState(0);\n const [log, setLog] = useState(\"\");\n const [error, setError] = useState(null);\n\n const lockRef = useRef(false);\n const mergedFlashOptions = { ...DEFAULT_FLASH_OPTIONS, ...flashOptions };\n\n const appendLog = useCallback((message) => {\n setLog((previous) => previous + message);\n }, []);\n\n const selectDevice = useCallback(async () => {\n setError(null);\n try {\n const selectedPort = await navigator.serial.requestPort({\n filters: [{ usbVendorId }],\n });\n setPort(selectedPort);\n setBootloaderPort(null);\n setNeedsBootloaderPermission(false);\n const label = labelForPort(selectedPort);\n setDeviceLabel(label);\n return selectedPort;\n } catch (err) {\n if (err?.name !== \"NotFoundError\") {\n setError(err?.message || String(err));\n }\n return null;\n }\n }, [usbVendorId]);\n\n const disconnect = useCallback(() => {\n setPort(null);\n setBootloaderPort(null);\n setNeedsBootloaderPermission(false);\n setDeviceLabel(\"\");\n setStatus(\"idle\");\n setError(null);\n }, []);\n\n // Phase 1: 1200bps touch + look for an already-permitted bootloader port.\n const prepareBootloader = useCallback(\n async (explicitPort) => {\n const activePort = explicitPort || port;\n if (!activePort || lockRef.current) return;\n lockRef.current = true;\n setError(null);\n setNeedsBootloaderPermission(false);\n try {\n const knownPorts = await navigator.serial.getPorts();\n appendLog(\"Putting board into download mode (1200bps touch)...\\n\");\n await touchTo1200bps(activePort);\n\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n const newPort = await waitForBootloaderPort(\n knownPorts,\n 3000,\n usbVendorId,\n );\n\n if (newPort) {\n setBootloaderPort(newPort);\n appendLog(\"Bootloader port found and already authorized.\\n\");\n return newPort;\n }\n\n appendLog(\n \"Bootloader port needs permission - a picker must be shown from a user gesture.\\n\",\n );\n setNeedsBootloaderPermission(true);\n return null;\n } catch (err) {\n const message = err?.message || String(err);\n appendLog(`Bootloader preparation failed: ${message}\\n`);\n setError(message);\n throw err;\n } finally {\n lockRef.current = false;\n }\n },\n [port, usbVendorId, appendLog],\n );\n\n // Phase 2: must run directly from a user gesture (click handler) so\n // requestPort() keeps fresh transient user activation.\n const grantBootloaderPort = useCallback(async () => {\n setError(null);\n try {\n appendLog(\"Requesting permission for the bootloader port...\\n\");\n const authorizedPort = await navigator.serial.requestPort({\n filters: [{ usbVendorId }],\n });\n setBootloaderPort(authorizedPort);\n setNeedsBootloaderPermission(false);\n appendLog(\"Bootloader port authorized.\\n\");\n return authorizedPort;\n } catch (err) {\n if (err?.name === \"NotFoundError\") {\n appendLog(\"Selection cancelled.\\n\");\n return null;\n }\n const message = err?.message || String(err);\n appendLog(`Authorization failed: ${message}\\n`);\n setError(message);\n throw err;\n }\n }, [usbVendorId, appendLog]);\n\n const flash = useCallback(\n async (binary, overrideOptions) => {\n if (!binary || binary.length === 0) {\n setError(\"No firmware binary supplied.\");\n setStatus(\"error\");\n return;\n }\n if (lockRef.current) return;\n\n const activeFlashOptions = { ...mergedFlashOptions, ...overrideOptions };\n\n let flashPort = bootloaderPort;\n lockRef.current = true;\n setStatus(\"flashing\");\n setProgress(0);\n setLog(\"\");\n setError(null);\n\n try {\n if (!flashPort) {\n if (!port) {\n throw new Error(\"No device selected.\");\n }\n lockRef.current = false; // prepareBootloader takes its own lock\n flashPort = await prepareBootloader(port);\n lockRef.current = true;\n if (!flashPort) {\n // needsBootloaderPermission is now set; caller must call\n // grantBootloaderPort() from a user gesture and retry flash().\n setStatus(\"idle\");\n return;\n }\n }\n\n appendLog(\"Starting upload to the microcontroller...\\n\");\n await flashBinary({\n port: flashPort,\n fileArray: [{ data: binary, address: activeFlashOptions.address }],\n baudrate: activeFlashOptions.baudrate,\n flashMode: activeFlashOptions.flashMode,\n flashFreq: activeFlashOptions.flashFreq,\n flashSize: activeFlashOptions.flashSize,\n eraseAll: activeFlashOptions.eraseAll,\n usingUsbOtg: activeFlashOptions.usingUsbOtg,\n onLog: appendLog,\n onProgress: setProgress,\n });\n\n setProgress(100);\n setStatus(\"done\");\n appendLog(\"\\nUpload finished successfully.\\n\");\n // flashBinary() already closed the port on its way out and the board\n // reset back into application mode, so every handle we held is\n // stale - drop them so the UI returns to its initial \"not\n // connected\" state, ready for the next board.\n setPort(null);\n setBootloaderPort(null);\n setNeedsBootloaderPermission(false);\n setDeviceLabel(\"\");\n } catch (err) {\n setError(err?.message || String(err));\n setStatus(\"error\");\n } finally {\n lockRef.current = false;\n }\n },\n [port, bootloaderPort, prepareBootloader, appendLog, mergedFlashOptions],\n );\n\n const resetFlashState = useCallback(() => {\n setStatus(\"idle\");\n setProgress(0);\n setLog(\"\");\n setError(null);\n }, []);\n\n return {\n supported,\n connected: Boolean(port),\n bootloaderReady: Boolean(bootloaderPort),\n port,\n deviceLabel,\n status,\n progress,\n log,\n error,\n needsBootloaderPermission,\n selectDevice,\n disconnect,\n prepareBootloader,\n grantBootloaderPort,\n flash,\n resetFlashState,\n };\n}\n","import { ESPLoader, Transport } from \"esptool-js\";\n\nconst sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n\n// senseBox MCU-S2 (ESP32-S2) USB vendor id. After the 1200bps touch the board\n// re-enumerates as the ROM serial bootloader, which always uses product id\n// 0x0002 regardless of the application product id.\nexport const DEFAULT_USB_VENDOR_ID = 0x303a;\nconst ESP32_ROM_BOOTLOADER_PRODUCT_ID = 0x0002;\n\n// ESP32-S2 RTC control registers used for an on-chip software reset. Writing\n// the SW_SYS_RST bit performs a full digital system reset - functionally the\n// same as a power cycle - which works over native USB (no RTS/DTR lines\n// required).\nconst RTC_CNTL_OPTIONS0_REG = 0x3f408000; // DR_REG_RTCCNTL_BASE + 0x0\nconst RTC_CNTL_SW_SYS_RST = 0x80000000; // BIT(31)\n\n/**\n * Checks whether a serial port is the ESP32 ROM serial bootloader.\n * @param {SerialPort} port\n * @param {number} vendorId\n * @returns {boolean}\n */\nfunction isRomBootloaderPort(port, vendorId) {\n try {\n const { usbVendorId, usbProductId } = port.getInfo();\n return (\n usbVendorId === vendorId &&\n usbProductId === ESP32_ROM_BOOTLOADER_PRODUCT_ID\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Performs a real hardware reset by pulsing the EN/RST pin via the RTS line,\n * exactly like the Arduino IDE / esptool.py do. esptool-js' built-in\n * \"hard_reset\" only ever releases RTS (`setRTS(false)`) and never asserts it,\n * so it produces no EN pulse - we do the full pulse here.\n *\n * IO0 is kept HIGH (`setDTR(false)`) so the chip boots the application instead\n * of dropping back into the ROM download mode.\n *\n * @param {Transport} transport A connected esptool-js transport.\n */\nasync function hardResetViaRts(transport) {\n await transport.setDTR(false); // IO0 HIGH -> normal boot (not download mode)\n await transport.setRTS(true); // EN LOW -> hold chip in reset\n await sleep(150);\n await transport.setRTS(false); // EN HIGH -> release; chip boots the new app\n await sleep(150);\n}\n\n/**\n * Triggers a full on-chip software reset on an ESP32-S2 by setting the\n * RTC_CNTL_SW_SYS_RST bit. Used as a fallback for boards without an RTS->EN\n * auto-reset circuit. Fired without waiting for an acknowledgement: the chip\n * resets the instant the register is written, so it never sends a response.\n *\n * @param {ESPLoader} loader A connected esptool-js loader.\n */\nasync function softResetViaRegister(loader) {\n const pkt = new Uint8Array(16);\n const view = new DataView(pkt.buffer);\n view.setUint32(0, RTC_CNTL_OPTIONS0_REG, true); // address\n view.setUint32(4, RTC_CNTL_SW_SYS_RST, true); // value\n view.setUint32(8, RTC_CNTL_SW_SYS_RST, true); // mask (only touch the reset bit)\n view.setUint32(12, 0, true); // delay (us)\n\n await loader.command(loader.ESP_WRITE_REG, pkt, undefined, false);\n}\n\n/**\n * Puts an ESP32 with native USB into ROM download mode without the user\n * having to press BOOT + RESET, mirroring the Arduino IDE behaviour\n * (`use_1200bps_touch=true`). Opening the port at 1200 baud is detected by the\n * running firmware, which then reboots into the serial bootloader. The device\n * re-enumerates on the USB bus (usually with a different product id), so the\n * original port handle becomes unusable afterwards.\n *\n * @param {SerialPort} port The currently selected (application) serial port.\n */\nexport async function touchTo1200bps(port) {\n try {\n await port.close();\n } catch {\n // Port was not open - ignore.\n }\n\n await sleep(300);\n\n // Warm-up open at a standard baud rate. On a freshly enumerated native-USB\n // CDC port the very first open at the unusual 1200 baud rate is frequently\n // rejected by the OS driver, whereas a standard rate opens fine. Opening and\n // closing at 115200 first initialises the CDC line coding so the following\n // 1200-baud open succeeds on the first try.\n try {\n await port.open({ baudRate: 115200 });\n await port.close();\n await sleep(200);\n } catch {\n // If the warm-up could not open the port, the retry loop below still tries.\n }\n\n let lastError;\n for (let attempt = 0; attempt < 3; attempt++) {\n try {\n await port.open({ baudRate: 1200 });\n\n try {\n await port.setSignals({\n dataTerminalReady: false,\n requestToSend: false,\n });\n } catch {\n // setSignals is optional - the 1200 baud open alone is usually enough.\n }\n\n await sleep(250);\n return; // Success\n } catch (err) {\n lastError = err;\n\n // Chrome-specific race: the 1200-baud open makes the firmware reset and\n // the USB device re-enumerate as the ROM bootloader *during* the open()\n // call, so Chrome rejects even though the touch physically succeeded.\n await sleep(150);\n if (port.connected === false) {\n return;\n }\n\n try {\n await port.close();\n } catch {\n // Nothing to close - ignore.\n }\n if (attempt < 2) {\n await sleep(300);\n }\n }\n }\n\n if (port.connected === false) {\n return;\n }\n\n throw new Error(\n `Failed to open port at 1200bps after 3 attempts: ${lastError?.message}`,\n );\n}\n\n/**\n * Waits for the serial bootloader port to become available after a reset into\n * the bootloader (mirrors the Arduino IDE `wait_for_upload_port=true`).\n *\n * Resolves with the bootloader port, or `null` if none shows up within the\n * timeout (e.g. the board was already in the bootloader, or the browser has\n * no permission for the re-enumerated device).\n *\n * @param {SerialPort[]} knownPorts Ports that already existed before the reset.\n * @param {number} [timeoutMs] How long to wait for the new port.\n * @param {number} [vendorId] USB vendor id to match against.\n * @returns {Promise<SerialPort|null>}\n */\nexport function waitForBootloaderPort(\n knownPorts,\n timeoutMs = 8000,\n vendorId = DEFAULT_USB_VENDOR_ID,\n) {\n return new Promise((resolve) => {\n const known = new Set(knownPorts);\n let settled = false;\n\n const cleanup = () => {\n navigator.serial.removeEventListener(\"connect\", onConnect);\n clearInterval(pollId);\n clearTimeout(timerId);\n };\n const finish = (result) => {\n if (settled) return;\n settled = true;\n cleanup();\n resolve(result);\n };\n\n const onConnect = (event) => {\n const candidate = event.target;\n if (isRomBootloaderPort(candidate, vendorId) || !known.has(candidate)) {\n finish(candidate);\n }\n };\n navigator.serial.addEventListener(\"connect\", onConnect);\n\n const pollId = setInterval(async () => {\n const ports = await navigator.serial.getPorts();\n const bootloader = ports.find(\n (candidate) =>\n isRomBootloaderPort(candidate, vendorId) &&\n candidate.connected === true,\n );\n if (bootloader) {\n finish(bootloader);\n return;\n }\n const fresh = ports.find(\n (candidate) => !known.has(candidate) && candidate.connected !== false,\n );\n if (fresh) finish(fresh);\n }, 400);\n\n const timerId = setTimeout(() => finish(null), timeoutMs);\n });\n}\n\n/**\n * Flashes one or more binary images to an ESP32-family chip over the Web\n * Serial API, using Espressif's esptool-js.\n *\n * @param {object} params\n * @param {SerialPort} params.port A Web Serial port chosen by the user.\n * @param {Array<{ data: Uint8Array, address: number }>} params.fileArray\n * @param {number} [params.baudrate] Baud rate for the flashing session.\n * @param {string} [params.flashMode] SPI flash mode (e.g. \"dio\").\n * @param {string} [params.flashFreq] SPI flash frequency (e.g. \"80m\").\n * @param {string} [params.flashSize] Flash size (e.g. \"4MB\").\n * @param {boolean} [params.eraseAll] Erase the whole flash before writing.\n * @param {boolean} [params.usingUsbOtg] Reset via the USB-OTG sequence (ESP32-S2/S3).\n * @param {(msg: string) => void} [params.onLog] Receives console output.\n * @param {(percent: number) => void} [params.onProgress] 0-100 progress.\n * @returns {Promise<string>} The detected chip description.\n */\nexport async function flashBinary({\n port,\n fileArray,\n baudrate = 115200,\n flashMode = \"keep\",\n flashFreq = \"keep\",\n flashSize = \"keep\",\n eraseAll = false,\n usingUsbOtg = false,\n onLog,\n onProgress,\n}) {\n const terminal = {\n clean: () => {},\n write: (data) => onLog?.(data),\n writeLine: (data) => onLog?.(`${data}\\n`),\n };\n\n const transport = new Transport(port, false);\n const loader = new ESPLoader({ transport, baudrate, terminal });\n\n try {\n const chip = await loader.main();\n\n await loader.writeFlash({\n fileArray,\n flashSize,\n flashMode,\n flashFreq,\n eraseAll,\n compress: true,\n reportProgress: (_fileIndex, written, total) => {\n if (total > 0) {\n onProgress?.(Math.round((written / total) * 100));\n }\n },\n });\n\n if (usingUsbOtg) {\n try {\n await hardResetViaRts(transport);\n } catch {\n // The chip drops off the USB bus the instant it resets, so the\n // trailing RTS write may fail - that is expected.\n }\n try {\n await softResetViaRegister(loader);\n } catch {\n // Ignore - the chip is already rebooting.\n }\n } else {\n await loader.after(\"hard_reset\");\n }\n return chip;\n } finally {\n try {\n await transport.disconnect();\n } catch {\n // Ignore errors while releasing the port.\n }\n }\n}\n","// Firmware is shipped through the JS module graph (dynamic import of a\n// base64-encoded module, generated by scripts/generate-firmware-modules.mjs\n// from the .bin files in this folder) rather than fetched by URL. A plain\n// import() is resolved correctly by every bundler regardless of how it\n// packages this library, unlike `new URL(path, import.meta.url)` + fetch,\n// which breaks the moment a consumer's bundler re-hosts the code somewhere\n// that doesn't also serve the sibling .bin file.\nconst LOADERS = {\n circuitpy: () => import(\"./generated/circuitpy.js\"),\n ota: () => import(\"./generated/ota.js\"),\n basic: () => import(\"./generated/basic.js\"),\n standard: () => import(\"./generated/standard.js\"),\n};\n\nexport const BUILTIN_FIRMWARE_TYPES = Object.keys(LOADERS);\n\nfunction base64ToBytes(base64) {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\n/**\n * Loads one of the firmware images bundled with the package.\n * @param {\"circuitpy\"|\"ota\"|\"basic\"|\"standard\"} type\n * @returns {Promise<Uint8Array|null>}\n */\nexport async function loadBuiltinFirmware(type) {\n const loader = LOADERS[type];\n if (!loader) return null;\n const module = await loader();\n return base64ToBytes(module.default);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/Users/eric/Documents/arbeit/sensebox-flash-tool/dist/index.cjs","../src/react/FlashTool.jsx","../src/react/useFlashDevice.js","../src/core/esptool.js","../src/firmware/index.js","../src/react/Dropdown.jsx"],"names":[],"mappings":"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","file":"/Users/eric/Documents/arbeit/sensebox-flash-tool/dist/index.cjs","sourcesContent":[null,"import React, { useEffect, useRef } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport PropTypes from \"prop-types\";\nimport { useFlashDevice } from \"./useFlashDevice.js\";\nimport { loadBuiltinFirmware } from \"../firmware/index.js\";\nimport { Dropdown } from \"./Dropdown.jsx\";\n\n// Loaded builtin firmware bytes are cached across component instances/\n// re-renders (keyed by type) so switching the dropdown back and forth, or\n// mounting more than one <FlashTool />, doesn't re-decode the same image.\nconst builtinFirmwareCache = new Map();\n\n// Styles are shipped separately (see package.json \"./style.css\" export) so\n// consumers explicitly opt in with `import \"@sensebox/flash-tool/style.css\"`\n// instead of this component silently injecting global CSS.\n\nconst FIRMWARE_TYPES = [\"circuitpy\", \"ota\", \"basic\", \"standard\"];\n\n// Friendlier/alternate spellings accepted by the `firmware` prop, resolved\n// down to the internal FIRMWARE_TYPES key. \"circuitpython\" is the obvious\n// one people reach for instead of \"circuitpy\".\nconst FIRMWARE_ALIASES = {\n circuitpython: \"circuitpy\",\n};\n\nfunction resolveFirmwareType(value) {\n if (!value) return undefined;\n return FIRMWARE_ALIASES[value] || value;\n}\n\n// circuitpy/ota/basic/standard are all pre-merged images (bootloader +\n// partition table + otadata + app[+ filesystem] already combined into one\n// file via `esptool merge_bin`), so they must be written starting at 0x0\n// with the whole chip erased first. Otherwise a previous partition table,\n// a stale otadata selection, and/or a stale filesystem partition survive -\n// e.g. CircuitPython never mounts as CIRCUITPY, or `basic` boots whatever\n// was left behind by a previously flashed image instead of the new sketch.\n// Overridable per type via the `firmwareFlashOptions` prop.\nconst BUILTIN_FIRMWARE_FLASH_OVERRIDES = {\n circuitpy: { address: 0x0, eraseAll: true },\n ota: { address: 0x0, eraseAll: true },\n basic: { address: 0x0, eraseAll: true },\n standard: { address: 0x0, eraseAll: true },\n};\n\n// Short blurb shown under the sketch picker so people know what they're\n// about to flash before they commit to it. Overridable (whole map or per\n// type) via the `firmwareDescriptions` prop, same merge pattern as\n// `firmwareFlashOptions`.\nconst DEFAULT_FIRMWARE_DESCRIPTIONS = {\n circuitpy: {\n text: \"Install CircuitPython on your senseBox now! For a more detailed tutorial, check out our\",\n linkText: \"docs\",\n linkUrl:\n \"https://docs.sensebox.de/docs/editors/circuitpython/circuitpython_esp32/?board=edus2\",\n },\n ota: {\n text: \"Flashes the senseBox OTA firmware, which lets you update your senseBox wirelessly afterwards.\",\n },\n basic: {\n text: \"A minimal starter sketch - a good starting point for your own senseBox projects.\",\n },\n standard: {\n text: \"The TinyUF2 bootloader, ready to read out and upload data from your connected sensors.\",\n },\n};\n\n// German translation of DEFAULT_FIRMWARE_DESCRIPTIONS, used when `language`\n// is \"de\".\nconst DEFAULT_FIRMWARE_DESCRIPTIONS_DE = {\n circuitpy: {\n text: \"Installiere jetzt CircuitPython auf deiner senseBox! Ein ausführliches Tutorial findest du in unserer\",\n linkText: \"Dokumentation\",\n linkUrl:\n \"https://docs.sensebox.de/docs/editors/circuitpython/circuitpython_esp32/?board=edus2\",\n },\n ota: {\n text: \"Spielt die senseBox-OTA-Firmware auf, mit der du deine senseBox anschließend drahtlos aktualisieren kannst.\",\n },\n basic: {\n text: \"Ein minimales Starter-Sketch - ein guter Ausgangspunkt für eigene senseBox-Projekte.\",\n },\n standard: {\n text: \"Das Standard-senseBox-Sketch, bereit zum Auslesen und Hochladen von Daten deiner angeschlossenen Sensoren.\",\n },\n};\n\nconst DEFAULT_STRINGS = {\n unsupported: \"Web Serial is not supported in this browser.\",\n connect: \"Connect board\",\n connecting: \"Connecting...\",\n connected: \"senseBox connected\",\n prepare: \"Prepare upload\",\n preparing: \"Preparing...\",\n grantPermission: \"Select bootloader port\",\n waitingForBinary: \"Waiting for firmware...\",\n loadingFirmware: \"Loading firmware...\",\n flash: \"Upload\",\n flashing: \"Uploading...\",\n done: \"Upload complete\",\n newUpload: \"Start new upload\",\n error: \"Upload failed\",\n retry: \"Retry\",\n disconnect: \"Disconnect\",\n showLog: \"Show log\",\n hideLog: \"Hide log\",\n logPlaceholder: \"Click \\\"Connect board\\\" to see the log here.\",\n sketchLabel: \"Sketch\",\n step1Title: \"Connect your board\",\n step1Hint: \"Plug in your senseBox, then pick it from the browser's device list.\",\n step2Title: \"Enter bootloader mode\",\n step2Hint: \"Put the board into upload mode.\",\n step2HintPermission: \"Select the bootloader port the browser is asking for.\",\n step2Done: \"Bootloader ready\",\n step3Title: \"Upload firmware\",\n step3Hint: \"Waiting for the previous steps to finish.\",\n firmwareCircuitpy: \"CircuitPython\",\n firmwareOta: \"senseBox OTA\",\n firmwareBasic: \"senseBox:basic\",\n firmwareStandard: \"TinyUF2\",\n help: \"Help\",\n helpTitle: \"How it works\",\n helpStep1: \"Connect your senseBox\",\n helpStep2: \"Select a sketch\",\n helpStep3: \"Give your browser permission\",\n helpStep4: \"Start the upload\",\n helpClose: \"Close\",\n};\n\n// German translation of DEFAULT_STRINGS, used when the `language` prop is\n// \"de\". Keep this in sync key-for-key with DEFAULT_STRINGS above - it's\n// merged as a base layer, so a missing key here silently falls back to the\n// English default rather than erroring.\nconst DEFAULT_STRINGS_DE = {\n unsupported: \"Web Serial wird von diesem Browser nicht unterstützt.\",\n connect: \"Board verbinden\",\n connecting: \"Verbinde...\",\n connected: \"senseBox verbunden\",\n prepare: \"Upload vorbereiten\",\n preparing: \"Bereite vor...\",\n grantPermission: \"Bootloader-Port auswählen\",\n waitingForBinary: \"Warte auf Firmware...\",\n loadingFirmware: \"Firmware wird geladen...\",\n flash: \"Hochladen\",\n flashing: \"Wird hochgeladen...\",\n done: \"Upload abgeschlossen\",\n newUpload: \"Neuen Upload starten\",\n error: \"Upload fehlgeschlagen\",\n retry: \"Erneut versuchen\",\n disconnect: \"Trennen\",\n showLog: \"Protokoll anzeigen\",\n hideLog: \"Protokoll ausblenden\",\n logPlaceholder:\n 'Klicke auf \"Board verbinden\", um das Protokoll hier zu sehen.',\n sketchLabel: \"Sketch\",\n step1Title: \"Board verbinden\",\n step1Hint:\n \"Schließe deine senseBox an und wähle sie aus der Geräteliste des Browsers aus.\",\n step2Title: \"Bootloader-Modus aktivieren\",\n step2Hint: \"Versetze das Board in den Upload-Modus.\",\n step2HintPermission:\n \"Wähle den Bootloader-Port aus, nach dem der Browser fragt.\",\n step2Done: \"Bootloader bereit\",\n step3Title: \"Firmware hochladen\",\n step3Hint: \"Warte auf die vorherigen Schritte.\",\n firmwareCircuitpy: \"CircuitPython\",\n firmwareOta: \"senseBox OTA\",\n firmwareBasic: \"senseBox:basic\",\n firmwareStandard: \"TinyUF2\",\n help: \"Hilfe\",\n helpTitle: \"So funktioniert's\",\n helpStep1: \"Verbinde deine senseBox\",\n helpStep2: \"Wähle einen Sketch aus\",\n helpStep3: \"Erteile deinem Browser die Berechtigung\",\n helpStep4: \"Starte den Upload\",\n helpClose: \"Schließen\",\n};\n\n/**\n * Self-contained widget for connecting to and flashing an ESP32-family board\n * (senseBox MCU-S2 by default) over the Web Serial API. Renders its own\n * compact status panel - drop it anywhere in a React tree.\n *\n * This component is flash-only: it expects an already-compiled firmware\n * image via the `binary` prop. Pair it with whatever compiler/build step\n * your app uses.\n */\nexport function FlashTool({\n binary,\n firmwares,\n firmware,\n firmwareType,\n defaultFirmwareType,\n firmwareFlashOptions,\n firmwareDescriptions,\n usbVendorId,\n flashOptions,\n autoFlash = true,\n expand = false,\n language = \"en\",\n strings,\n className,\n onConnected,\n onDisconnected,\n onFlashed,\n onError,\n onFirmwareTypeChange,\n}) {\n // `strings` always wins over the localized defaults, so callers can still\n // override individual labels on top of the German translation.\n const merged = {\n ...(language === \"de\" ? DEFAULT_STRINGS_DE : DEFAULT_STRINGS),\n ...strings,\n };\n const [logOpen, setLogOpen] = React.useState(expand);\n const [helpOpen, setHelpOpen] = React.useState(false);\n const [busy, setBusy] = React.useState(null); // \"connect\" | \"prepare\" | \"grant\" | null\n\n // `firmware` is the friendlier alias-accepting prop (e.g. \"circuitpython\"\n // resolves to \"circuitpy\"); `firmwareType` is the original, exact-key\n // prop. Either one locks the picker to that sketch.\n const lockedFirmwareType = resolveFirmwareType(firmwareType || firmware);\n\n const [selectedType, setSelectedType] = React.useState(\n lockedFirmwareType || defaultFirmwareType || FIRMWARE_TYPES[0],\n );\n\n const firmwareLocked = Boolean(lockedFirmwareType);\n\n // The sketch-type dropdown only actually changes what gets flashed when\n // there's more than one binary to choose from: a `firmwares` map, or no\n // caller-supplied `binary` at all (dropdown drives which bundled firmware\n // is loaded). A caller who passes a single fixed `binary` without also\n // locking `firmwareType` would otherwise see an interactive dropdown that\n // silently does nothing - `explicitBinary` below always resolves to that\n // same fixed `binary` regardless of the selected type, so it looks like\n // choosing e.g. \"basic\" should flash the basic sketch when it actually\n // keeps flashing whatever `binary` was passed in.\n const showFirmwarePicker = Boolean(firmwares) || !binary || firmwareLocked;\n\n useEffect(() => {\n if (lockedFirmwareType) setSelectedType(lockedFirmwareType);\n }, [lockedFirmwareType]);\n\n useEffect(() => {\n if (expand) setLogOpen(true);\n }, [expand]);\n\n useEffect(() => {\n if (!helpOpen) return;\n const onKeyDown = (event) => {\n if (event.key === \"Escape\") setHelpOpen(false);\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [helpOpen]);\n\n const {\n supported,\n connected,\n bootloaderReady,\n deviceLabel,\n status,\n progress,\n log,\n error,\n needsBootloaderPermission,\n selectDevice,\n disconnect,\n prepareBootloader,\n grantBootloaderPort,\n flash,\n resetFlashState,\n } = useFlashDevice({ usbVendorId, flashOptions });\n\n // Either pick the binary out of a `{ circuitpy, ota, basic, standard }`\n // map keyed by the dropdown selection, or fall back to a single `binary`\n // prop for callers that don't need the firmware picker at all.\n const explicitBinary = firmwares ? firmwares[selectedType] : binary;\n\n const [builtinBinary, setBuiltinBinary] = React.useState(null);\n const [builtinLoading, setBuiltinLoading] = React.useState(false);\n const [builtinError, setBuiltinError] = React.useState(null);\n const [builtinRetryToken, setBuiltinRetryToken] = React.useState(0);\n\n // No binary supplied by the caller for this sketch type - fall back to the\n // firmware image bundled with the package, loaded on demand via a plain\n // dynamic import() (see src/firmware/index.js) and cached. Skipped\n // entirely when Web Serial isn't even available, so an unsupported browser\n // never does the (currently pointless) decode work the instant the widget\n // mounts.\n useEffect(() => {\n if (!supported || explicitBinary) return;\n\n const cached = builtinFirmwareCache.get(selectedType);\n if (cached) {\n setBuiltinBinary(cached);\n setBuiltinError(null);\n return;\n }\n\n let cancelled = false;\n setBuiltinBinary(null);\n setBuiltinError(null);\n setBuiltinLoading(true);\n\n loadBuiltinFirmware(selectedType)\n .then((bytes) => {\n if (cancelled || !bytes) return;\n builtinFirmwareCache.set(selectedType, bytes);\n setBuiltinBinary(bytes);\n })\n .catch((err) => {\n if (cancelled) return;\n const message = err?.message || String(err);\n console.error(\n `[FlashTool] Failed to load builtin \"${selectedType}\" firmware`,\n err,\n );\n setBuiltinError(message);\n onError?.(err instanceof Error ? err : new Error(message));\n })\n .finally(() => {\n if (!cancelled) setBuiltinLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n }, [supported, explicitBinary, selectedType, onError, builtinRetryToken]);\n\n const resolvedBinary = explicitBinary || builtinBinary || undefined;\n\n // The flash-address/erase override depends on which kind of image is\n // selected, not on whether a `firmwares` map was supplied - a single\n // `binary` prop locked via `firmwareType=\"circuitpy\"` needs it too. Only\n // applied when the picker is actually meaningful (see showFirmwarePicker)\n // - otherwise `selectedType` doesn't correspond to anything real and\n // applying its overrides to an unrelated fixed `binary` would be wrong.\n const flashOverrides = React.useMemo(\n () =>\n showFirmwarePicker\n ? {\n ...BUILTIN_FIRMWARE_FLASH_OVERRIDES[selectedType],\n ...firmwareFlashOptions?.[selectedType],\n }\n : {},\n [showFirmwarePicker, firmwareFlashOptions, selectedType],\n );\n\n // The board reports progress only while it's actually writing data - the\n // upfront full-chip erase (needed for circuitpy/ota/standard) can take\n // 10-20s with the bar pinned at 0%, so surface the latest esptool log line\n // (\"Erasing flash...\") to show the widget is still working, not stuck.\n const latestLogLine = React.useMemo(() => {\n if (!log) return \"\";\n const lines = log.split(\"\\n\").filter((line) => line.trim().length > 0);\n const lastLine = lines[lines.length - 1] || \"\";\n // esptool-js appends a \"(NN%)\" progress suffix to its \"Writing at\n // 0x...\" lines - drop it since the same percentage is already shown by\n // the progress bar/value right next to this line.\n return lastLine.replace(/\\s*\\(\\d+ ?%\\)\\s*$/, \"\");\n }, [log]);\n\n const activeFirmwareDescription = showFirmwarePicker\n ? {\n ...(language === \"de\"\n ? DEFAULT_FIRMWARE_DESCRIPTIONS_DE[selectedType]\n : DEFAULT_FIRMWARE_DESCRIPTIONS[selectedType]),\n ...firmwareDescriptions?.[selectedType],\n }\n : null;\n\n const handleFirmwareChange = (value) => {\n setSelectedType(value);\n onFirmwareTypeChange?.(value);\n };\n\n const firmwareOptions = FIRMWARE_TYPES.map((type) => ({\n value: type,\n label:\n merged[`firmware${type.charAt(0).toUpperCase()}${type.slice(1)}`],\n icon: FIRMWARE_ICONS[type],\n }));\n\n const handleRetryFirmwareLoad = () => {\n setBuiltinRetryToken((token) => token + 1);\n };\n\n const logRef = useRef(null);\n useEffect(() => {\n if (!logOpen || !logRef.current) return;\n logRef.current.scrollTop = logRef.current.scrollHeight;\n }, [log, logOpen]);\n\n const prevConnected = useRef(connected);\n useEffect(() => {\n if (connected && !prevConnected.current) onConnected?.(deviceLabel);\n if (!connected && prevConnected.current) onDisconnected?.();\n prevConnected.current = connected;\n }, [connected, deviceLabel, onConnected, onDisconnected]);\n\n useEffect(() => {\n if (status === \"done\") onFlashed?.();\n if (status === \"error\" && error) onError?.(new Error(error));\n }, [status, error, onFlashed, onError]);\n\n // Auto-flash as soon as a binary is ready and the device is connected.\n useEffect(() => {\n if (\n autoFlash &&\n resolvedBinary &&\n connected &&\n !bootloaderReady &&\n status === \"idle\" &&\n !needsBootloaderPermission\n ) {\n flash(resolvedBinary, flashOverrides);\n }\n }, [\n autoFlash,\n resolvedBinary,\n flashOverrides,\n connected,\n bootloaderReady,\n status,\n needsBootloaderPermission,\n flash,\n ]);\n\n if (!supported) {\n return (\n <div className={`sbflash sbflash--unsupported ${className || \"\"}`}>\n {merged.unsupported}\n </div>\n );\n }\n\n const handleConnect = async () => {\n setBusy(\"connect\");\n try {\n await selectDevice();\n } finally {\n setBusy(null);\n }\n };\n\n const handlePrepare = async () => {\n setBusy(\"prepare\");\n try {\n await prepareBootloader();\n } finally {\n setBusy(null);\n }\n };\n\n const handleGrant = async () => {\n setBusy(\"grant\");\n try {\n await grantBootloaderPort();\n if (resolvedBinary) await flash(resolvedBinary, flashOverrides);\n } finally {\n setBusy(null);\n }\n };\n\n const handleFlash = async () => {\n if (resolvedBinary) await flash(resolvedBinary, flashOverrides);\n };\n\n const handleRetry = () => {\n resetFlashState();\n if (resolvedBinary) flash(resolvedBinary, flashOverrides);\n };\n\n // Which of the 3 user-facing steps is currently the one waiting on an\n // interaction - drives both the stepper's visual state (done/active/\n // pending/error) and which step's action is actually rendered, so it's\n // always obvious that exactly one step needs the user's attention next.\n //\n // A successful flash resets `connected`/`bootloaderReady` back to their\n // initial falsy values (see useFlashDevice) so the hook is ready for the\n // next board - but the stepper should keep showing all 3 steps as\n // complete until the user explicitly starts a new upload, rather than\n // snapping step 1 back to \"active\" the instant the flash finishes.\n const flashComplete = status === \"done\";\n const step1Status = flashComplete ? \"done\" : connected ? \"done\" : \"active\";\n const step2Status = flashComplete\n ? \"done\"\n : !connected\n ? \"pending\"\n : bootloaderReady\n ? \"done\"\n : \"active\";\n const step3Status = flashComplete\n ? \"done\"\n : !bootloaderReady\n ? \"pending\"\n : status === \"error\"\n ? \"error\"\n : \"active\";\n\n const handleStartNew = () => {\n resetFlashState();\n };\n\n return (\n <div className={`sbflash ${className || \"\"}`}>\n <div className=\"sbflash__field\">\n <div className=\"sbflash__field-head\">\n <span className=\"sbflash__field-label\">{merged.sketchLabel}</span>\n <div className=\"sbflash__icon-row\">\n <button\n type=\"button\"\n className={`sbflash__icon-button${\n logOpen ? \" sbflash__icon-button--active\" : \"\"\n }`}\n onClick={() => !expand && setLogOpen((v) => !v)}\n disabled={expand}\n aria-pressed={logOpen}\n aria-label={logOpen ? merged.hideLog : merged.showLog}\n title={logOpen ? merged.hideLog : merged.showLog}\n >\n <IconTerminal />\n </button>\n {connected && (\n <button\n type=\"button\"\n className=\"sbflash__icon-button\"\n onClick={disconnect}\n aria-label={merged.disconnect}\n title={merged.disconnect}\n >\n <IconDisconnect />\n </button>\n )}\n <button\n type=\"button\"\n className=\"sbflash__icon-button\"\n onClick={() => setHelpOpen(true)}\n aria-label={merged.help}\n title={merged.help}\n >\n <IconHelp />\n </button>\n </div>\n </div>\n {showFirmwarePicker && (\n <Dropdown\n options={firmwareOptions}\n value={selectedType}\n onChange={handleFirmwareChange}\n disabled={firmwareLocked || status === \"flashing\"}\n aria-label={merged.sketchLabel}\n />\n )}\n {builtinError && (\n <div className=\"sbflash__field-error-row\">\n <span className=\"sbflash__field-error\">{builtinError}</span>\n <button\n type=\"button\"\n className=\"sbflash__link\"\n onClick={handleRetryFirmwareLoad}\n >\n {merged.retry}\n </button>\n </div>\n )}\n {activeFirmwareDescription?.text && (\n <p className=\"sbflash__field-description\">\n {activeFirmwareDescription.text}\n {activeFirmwareDescription.linkUrl && (\n <>\n {\" \"}\n <a\n href={activeFirmwareDescription.linkUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {activeFirmwareDescription.linkText || activeFirmwareDescription.linkUrl}\n </a>\n </>\n )}\n </p>\n )}\n </div>\n\n {helpOpen &&\n createPortal(\n <div\n className=\"sbflash__modal-overlay\"\n onClick={() => setHelpOpen(false)}\n >\n <div\n className=\"sbflash__modal\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={merged.helpTitle}\n onClick={(event) => event.stopPropagation()}\n >\n <div className=\"sbflash__modal-head\">\n <span className=\"sbflash__modal-title\">\n {merged.helpTitle}\n </span>\n <button\n type=\"button\"\n className=\"sbflash__modal-close\"\n onClick={() => setHelpOpen(false)}\n aria-label={merged.helpClose}\n >\n ×\n </button>\n </div>\n <ol className=\"sbflash__modal-steps\">\n {[\n merged.helpStep1,\n merged.helpStep2,\n merged.helpStep3,\n merged.helpStep4,\n ].map((step, index) => (\n <li key={index} className=\"sbflash__modal-step\">\n <span className=\"sbflash__modal-step-index\">\n {index + 1}\n </span>\n <span>{step}</span>\n </li>\n ))}\n </ol>\n </div>\n </div>,\n document.body,\n )}\n\n <ol className=\"sbflash__stepper\">\n <li className={`sbflash__step sbflash__step--${step1Status}`}>\n <span className=\"sbflash__step-marker\">\n <span className=\"sbflash__step-index\">\n {step1Status === \"done\" ? <IconCheck /> : 1}\n </span>\n <span\n className={`sbflash__step-line${\n step1Status === \"done\" ? \" sbflash__step-line--done\" : \"\"\n }`}\n />\n </span>\n <div className=\"sbflash__step-body\">\n <span className=\"sbflash__step-title\">{merged.step1Title}</span>\n {!connected && !flashComplete ? (\n <>\n <span className=\"sbflash__step-hint\">{merged.step1Hint}</span>\n <button\n type=\"button\"\n className=\"sbflash__button sbflash__button--primary\"\n onClick={handleConnect}\n disabled={busy === \"connect\"}\n >\n {busy === \"connect\" ? merged.connecting : merged.connect}\n </button>\n </>\n ) : (\n <span className=\"sbflash__step-detail\" title={deviceLabel}>\n {deviceLabel || merged.connected}\n </span>\n )}\n </div>\n </li>\n\n <li className={`sbflash__step sbflash__step--${step2Status}`}>\n <span className=\"sbflash__step-marker\">\n <span className=\"sbflash__step-index\">\n {step2Status === \"done\" ? <IconCheck /> : 2}\n </span>\n <span\n className={`sbflash__step-line${\n step2Status === \"done\" ? \" sbflash__step-line--done\" : \"\"\n }`}\n />\n </span>\n <div className=\"sbflash__step-body\">\n <span className=\"sbflash__step-title\">{merged.step2Title}</span>\n {step2Status === \"active\" && needsBootloaderPermission && (\n <>\n <span className=\"sbflash__step-hint\">\n {merged.step2HintPermission}\n </span>\n <button\n type=\"button\"\n className=\"sbflash__button sbflash__button--primary\"\n onClick={handleGrant}\n disabled={busy === \"grant\"}\n >\n {merged.grantPermission}\n </button>\n </>\n )}\n {step2Status === \"active\" && !needsBootloaderPermission && (\n <>\n <span className=\"sbflash__step-hint\">{merged.step2Hint}</span>\n {/* <button\n type=\"button\"\n className=\"sbflash__button sbflash__button--secondary\"\n onClick={handlePrepare}\n disabled={busy === \"prepare\"}\n >\n {busy === \"prepare\" ? merged.preparing : merged.prepare}\n </button> */}\n </>\n )}\n {step2Status === \"done\" && (\n <span className=\"sbflash__step-detail\">{merged.step2Done}</span>\n )}\n {step2Status === \"pending\" && (\n <span className=\"sbflash__step-hint\">{merged.step2Hint}</span>\n )}\n </div>\n </li>\n\n <li className={`sbflash__step sbflash__step--${step3Status}`}>\n <span className=\"sbflash__step-marker\">\n <span className=\"sbflash__step-index\">\n {step3Status === \"done\" ? <IconCheck /> : 3}\n </span>\n </span>\n <div className=\"sbflash__step-body\">\n <span className=\"sbflash__step-title\">{merged.step3Title}</span>\n\n {step3Status === \"pending\" && (\n <span className=\"sbflash__step-hint\">{merged.step3Hint}</span>\n )}\n\n {step3Status === \"active\" && status !== \"flashing\" && (\n <button\n type=\"button\"\n className=\"sbflash__button sbflash__button--primary\"\n onClick={handleFlash}\n disabled={!resolvedBinary}\n >\n {resolvedBinary\n ? merged.flash\n : builtinLoading\n ? merged.loadingFirmware\n : merged.waitingForBinary}\n </button>\n )}\n\n {status === \"flashing\" &&\n (progress > 0 ? (\n <div className=\"sbflash__progress-wrap\">\n <div className=\"sbflash__progress\">\n <div className=\"sbflash__progress-track\">\n <div\n className=\"sbflash__progress-fill\"\n style={{ width: `${Math.min(100, progress)}%` }}\n />\n </div>\n <span className=\"sbflash__progress-value\">\n {progress}%\n </span>\n </div>\n {latestLogLine && (\n <span className=\"sbflash__progress-detail\">\n {latestLogLine}\n </span>\n )}\n </div>\n ) : (\n <div className=\"sbflash__task\">\n <span className=\"sbflash__task-spinner\" />\n <span className=\"sbflash__task-label\">\n {latestLogLine || merged.flashing}\n </span>\n </div>\n ))}\n\n {status === \"done\" && (\n <div className=\"sbflash__done-banner\" role=\"status\">\n <span className=\"sbflash__done-icon\">\n <IconCheck />\n </span>\n <span className=\"sbflash__done-text\">{merged.done}</span>\n <button\n type=\"button\"\n className=\"sbflash__button sbflash__button--secondary\"\n onClick={handleStartNew}\n >\n {merged.newUpload}\n </button>\n </div>\n )}\n\n {status === \"error\" && (\n <div className=\"sbflash__status sbflash__status--error\">\n {merged.error}\n <button\n type=\"button\"\n className=\"sbflash__button sbflash__button--secondary\"\n onClick={handleRetry}\n >\n {merged.retry}\n </button>\n </div>\n )}\n </div>\n </li>\n </ol>\n\n {logOpen && (\n <pre className=\"sbflash__log\" ref={logRef}>\n {log || merged.logPlaceholder}\n </pre>\n )}\n </div>\n );\n}\n\nfunction IconTerminal() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n width=\"12\"\n height=\"12\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <rect x=\"1.5\" y=\"2.5\" width=\"13\" height=\"11\" rx=\"2\" />\n <path d=\"M4.25 6.25 6.75 8.5l-2.5 2.25\" />\n <path d=\"M8.5 11h3\" />\n </svg>\n );\n}\n\nfunction IconCheck() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n width=\"11\"\n height=\"11\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M3 8.5 6.5 12 13 4.5\" />\n </svg>\n );\n}\n\nfunction IconDisconnect() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n width=\"12\"\n height=\"12\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M8 1.5v4.25\" />\n <path d=\"M4.5 5.5a5 5 0 1 0 7 0\" />\n </svg>\n );\n}\n\nfunction IconHelp() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n width=\"12\"\n height=\"12\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"8\" cy=\"8\" r=\"6.5\" />\n <path d=\"M6 6.25a2 2 0 1 1 2.75 1.85c-.6.25-.95.7-.95 1.4v.25\" />\n <circle cx=\"8\" cy=\"11.5\" r=\"0.9\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n );\n}\n\n// Small per-sketch icons shown in the sketch Dropdown (trigger + each menu\n// item). Purely decorative visual shorthand for the 4 built-in firmware\n// types - not meant to be swappable via props, the sketch labels already\n// are (see `strings`).\nfunction IconChip() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n width=\"14\"\n height=\"14\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.4\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <rect x=\"4\" y=\"4\" width=\"8\" height=\"8\" rx=\"1.2\" />\n <path d=\"M6 1.5v2M10 1.5v2M6 12.5v2M10 12.5v2M1.5 6v-.01M1.5 10v-.01M14.5 6v-.01M14.5 10v-.01M1.5 6h2M1.5 10h2M12.5 6h2M12.5 10h2\" />\n </svg>\n );\n}\n\nfunction IconCloud() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n width=\"14\"\n height=\"14\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.4\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M4.5 12.5A3 3 0 0 1 4 6.53 3.5 3.5 0 0 1 11 5.5a2.75 2.75 0 0 1 .5 5.46\" />\n <path d=\"M6.5 12.5h5\" />\n <path d=\"M8 9.5v5M6.25 12.25 8 14.5l1.75-2.25\" />\n </svg>\n );\n}\n\nfunction IconBolt() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n width=\"14\"\n height=\"14\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.4\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M8.5 1.5 3 9h4l-1 5.5L13 7H9l-0.5-5.5Z\" />\n </svg>\n );\n}\n\nfunction IconLayers() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n width=\"14\"\n height=\"14\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.4\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M8 1.75 14 5 8 8.25 2 5Z\" />\n <path d=\"M2 8 8 11.25 14 8\" />\n <path d=\"M2 11 8 14.25 14 11\" />\n </svg>\n );\n}\n\nconst FIRMWARE_ICONS = {\n circuitpy: <IconChip />,\n ota: <IconCloud />,\n basic: <IconBolt />,\n standard: <IconLayers />,\n};\n\nFlashTool.propTypes = {\n /** Already-compiled firmware image to flash. Ignored if `firmwares` is set. */\n binary: PropTypes.instanceOf(Uint8Array),\n /** Map of sketch options to their compiled firmware image: { circuitpy, ota, basic, standard }. */\n firmwares: PropTypes.shape({\n circuitpy: PropTypes.instanceOf(Uint8Array),\n ota: PropTypes.instanceOf(Uint8Array),\n basic: PropTypes.instanceOf(Uint8Array),\n standard: PropTypes.instanceOf(Uint8Array),\n }),\n /** Same as `firmwareType`, but also accepts friendlier aliases (e.g. \"circuitpython\" for \"circuitpy\"). */\n firmware: PropTypes.oneOf([\n ...FIRMWARE_TYPES,\n ...Object.keys(FIRMWARE_ALIASES),\n ]),\n /** Locks the sketch dropdown to this value and disables it. One of \"circuitpy\" | \"ota\" | \"basic\" | \"standard\". */\n firmwareType: PropTypes.oneOf(FIRMWARE_TYPES),\n /** Initial dropdown selection when `firmwareType` isn't set (dropdown stays interactive). */\n defaultFirmwareType: PropTypes.oneOf(FIRMWARE_TYPES),\n /** Per-sketch overrides for the esptool flash step, keyed by firmware type. Merges over (and can override) the built-in CircuitPython defaults. */\n firmwareFlashOptions: PropTypes.objectOf(PropTypes.object),\n /** Per-sketch description shown under the sketch picker, keyed by firmware type: { text, linkText, linkUrl }. Merges over (and can override) the built-in defaults. */\n firmwareDescriptions: PropTypes.objectOf(\n PropTypes.shape({\n text: PropTypes.string,\n linkText: PropTypes.string,\n linkUrl: PropTypes.string,\n }),\n ),\n /** USB vendor id used to filter the device picker. Defaults to senseBox (0x303a). */\n usbVendorId: PropTypes.number,\n /** Overrides for the esptool flash step (address, baudrate, flashMode, flashFreq, flashSize, eraseAll, usingUsbOtg). */\n flashOptions: PropTypes.object,\n /** Start flashing automatically once the selected firmware and a connected device are both ready. */\n autoFlash: PropTypes.bool,\n /** Keep the log/terminal view expanded by default and disable the button to collapse it. */\n expand: PropTypes.bool,\n /** UI language for the built-in strings. \"de\" uses the built-in German translation. */\n language: PropTypes.oneOf([\"en\", \"de\"]),\n /** Overrides for the built-in UI strings (applied on top of `language`). */\n strings: PropTypes.object,\n className: PropTypes.string,\n onConnected: PropTypes.func,\n onDisconnected: PropTypes.func,\n onFlashed: PropTypes.func,\n onError: PropTypes.func,\n /** Called with the newly selected sketch type when the dropdown changes. */\n onFirmwareTypeChange: PropTypes.func,\n};\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n DEFAULT_USB_VENDOR_ID,\n flashBinary,\n isRomBootloaderPort,\n touchTo1200bps,\n waitForBootloaderPort,\n} from \"../core/esptool.js\";\n\nconst DEFAULT_FLASH_OPTIONS = {\n address: 0x10000,\n baudrate: 115200,\n flashMode: \"dio\",\n flashFreq: \"80m\",\n flashSize: \"4MB\",\n eraseAll: false,\n usingUsbOtg: true,\n};\n\n// Web Serial's getInfo() only ever exposes numeric VID/PID, never the\n// descriptor's product string - so friendly names for known boards have to\n// be looked up by hand. Keyed by \"vendorId:productId\" (both lowercase hex,\n// no padding/prefix).\nconst KNOWN_USB_DEVICES = {\n \"303a:81b8\": \"senseBox MCU-S2 (ESP32-S2)\",\n \"303a:2\": \"senseBox MCU-S2 (ESP32-S2 bootloader)\",\n};\n\nfunction labelForPort(port) {\n const info = port.getInfo?.() ?? {};\n const vendor = info.usbVendorId\n ? info.usbVendorId.toString(16).padStart(4, \"0\")\n : \"????\";\n const product = info.usbProductId\n ? info.usbProductId.toString(16).padStart(4, \"0\")\n : \"????\";\n\n const known =\n info.usbVendorId !== undefined && info.usbProductId !== undefined\n ? KNOWN_USB_DEVICES[\n `${info.usbVendorId.toString(16)}:${info.usbProductId.toString(16)}`\n ]\n : undefined;\n\n return known || `USB device (VID 0x${vendor} / PID 0x${product})`;\n}\n\n/**\n * Framework-agnostic-ish (but React) state machine for connecting to an\n * ESP32-family board over Web Serial, putting it into bootloader mode and\n * flashing a firmware binary supplied by the caller.\n *\n * This hook does NOT compile anything - `binary` must already be a\n * Uint8Array of the firmware image. Pair it with your own compile step, or\n * use <FlashTool /> which wraps this hook with a ready-made UI.\n *\n * @param {object} [options]\n * @param {number} [options.usbVendorId] USB vendor id to filter the device picker by.\n * @param {object} [options.flashOptions] Overrides for esptool flashBinary().\n */\nexport function useFlashDevice({\n usbVendorId = DEFAULT_USB_VENDOR_ID,\n flashOptions = {},\n} = {}) {\n const supported =\n typeof navigator !== \"undefined\" && navigator.serial !== undefined;\n\n const [port, setPort] = useState(null);\n const [bootloaderPort, setBootloaderPort] = useState(null);\n const [needsBootloaderPermission, setNeedsBootloaderPermission] =\n useState(false);\n const [deviceLabel, setDeviceLabel] = useState(\"\");\n const [status, setStatus] = useState(\"idle\"); // idle | flashing | done | error\n const [progress, setProgress] = useState(0);\n const [log, setLog] = useState(\"\");\n const [error, setError] = useState(null);\n\n const lockRef = useRef(false);\n const mergedFlashOptions = { ...DEFAULT_FLASH_OPTIONS, ...flashOptions };\n\n const appendLog = useCallback((message) => {\n setLog((previous) => previous + message);\n }, []);\n\n const selectDevice = useCallback(async () => {\n setError(null);\n try {\n const selectedPort = await navigator.serial.requestPort({\n filters: [{ usbVendorId }],\n });\n setPort(selectedPort);\n setNeedsBootloaderPermission(false);\n const label = labelForPort(selectedPort);\n setDeviceLabel(label);\n\n // The board may already be sitting in the ROM serial bootloader (e.g.\n // it enumerates as a generic \"ESP32-S2\" instead of \"senseBox MCU-S2\"\n // because no application firmware is running to rename it). In that\n // case the selected port *is* the bootloader port already - skip the\n // 1200bps touch/reset dance entirely instead of trying to \"prepare\"\n // a board that's already ready.\n if (isRomBootloaderPort(selectedPort, usbVendorId)) {\n setBootloaderPort(selectedPort);\n appendLog(\n \"Board is already in bootloader mode - skipping reset.\\n\",\n );\n } else {\n setBootloaderPort(null);\n }\n\n return selectedPort;\n } catch (err) {\n if (err?.name !== \"NotFoundError\") {\n setError(err?.message || String(err));\n }\n return null;\n }\n }, [usbVendorId, appendLog]);\n\n const disconnect = useCallback(() => {\n setPort(null);\n setBootloaderPort(null);\n setNeedsBootloaderPermission(false);\n setDeviceLabel(\"\");\n setStatus(\"idle\");\n setError(null);\n }, []);\n\n // Phase 1: 1200bps touch + look for an already-permitted bootloader port.\n const prepareBootloader = useCallback(\n async (explicitPort) => {\n const activePort = explicitPort || port;\n if (!activePort || lockRef.current) return;\n lockRef.current = true;\n setError(null);\n setNeedsBootloaderPermission(false);\n try {\n const knownPorts = await navigator.serial.getPorts();\n appendLog(\"Putting board into download mode (1200bps touch)...\\n\");\n await touchTo1200bps(activePort);\n\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n const newPort = await waitForBootloaderPort(\n knownPorts,\n 3000,\n usbVendorId,\n );\n\n if (newPort) {\n setBootloaderPort(newPort);\n appendLog(\"Bootloader port found and already authorized.\\n\");\n return newPort;\n }\n\n appendLog(\n \"Bootloader port needs permission - a picker must be shown from a user gesture.\\n\",\n );\n setNeedsBootloaderPermission(true);\n return null;\n } catch (err) {\n const message = err?.message || String(err);\n appendLog(`Bootloader preparation failed: ${message}\\n`);\n setError(message);\n throw err;\n } finally {\n lockRef.current = false;\n }\n },\n [port, usbVendorId, appendLog],\n );\n\n // Phase 2: must run directly from a user gesture (click handler) so\n // requestPort() keeps fresh transient user activation.\n const grantBootloaderPort = useCallback(async () => {\n setError(null);\n try {\n appendLog(\"Requesting permission for the bootloader port...\\n\");\n const authorizedPort = await navigator.serial.requestPort({\n filters: [{ usbVendorId }],\n });\n setBootloaderPort(authorizedPort);\n setNeedsBootloaderPermission(false);\n appendLog(\"Bootloader port authorized.\\n\");\n return authorizedPort;\n } catch (err) {\n if (err?.name === \"NotFoundError\") {\n appendLog(\"Selection cancelled.\\n\");\n return null;\n }\n const message = err?.message || String(err);\n appendLog(`Authorization failed: ${message}\\n`);\n setError(message);\n throw err;\n }\n }, [usbVendorId, appendLog]);\n\n const flash = useCallback(\n async (binary, overrideOptions) => {\n if (!binary || binary.length === 0) {\n setError(\"No firmware binary supplied.\");\n setStatus(\"error\");\n return;\n }\n if (lockRef.current) return;\n\n const activeFlashOptions = { ...mergedFlashOptions, ...overrideOptions };\n\n let flashPort = bootloaderPort;\n lockRef.current = true;\n setStatus(\"flashing\");\n setProgress(0);\n setLog(\"\");\n setError(null);\n\n try {\n if (!flashPort) {\n if (!port) {\n throw new Error(\"No device selected.\");\n }\n lockRef.current = false; // prepareBootloader takes its own lock\n flashPort = await prepareBootloader(port);\n lockRef.current = true;\n if (!flashPort) {\n // needsBootloaderPermission is now set; caller must call\n // grantBootloaderPort() from a user gesture and retry flash().\n setStatus(\"idle\");\n return;\n }\n }\n\n appendLog(\"Starting upload to the microcontroller...\\n\");\n await flashBinary({\n port: flashPort,\n fileArray: [{ data: binary, address: activeFlashOptions.address }],\n baudrate: activeFlashOptions.baudrate,\n flashMode: activeFlashOptions.flashMode,\n flashFreq: activeFlashOptions.flashFreq,\n flashSize: activeFlashOptions.flashSize,\n eraseAll: activeFlashOptions.eraseAll,\n usingUsbOtg: activeFlashOptions.usingUsbOtg,\n onLog: appendLog,\n onProgress: setProgress,\n });\n\n setProgress(100);\n setStatus(\"done\");\n appendLog(\"\\nUpload finished successfully.\\n\");\n // flashBinary() already closed the port on its way out and the board\n // reset back into application mode, so every handle we held is\n // stale - drop them so the UI returns to its initial \"not\n // connected\" state, ready for the next board.\n setPort(null);\n setBootloaderPort(null);\n setNeedsBootloaderPermission(false);\n setDeviceLabel(\"\");\n } catch (err) {\n setError(err?.message || String(err));\n setStatus(\"error\");\n } finally {\n lockRef.current = false;\n }\n },\n [port, bootloaderPort, prepareBootloader, appendLog, mergedFlashOptions],\n );\n\n const resetFlashState = useCallback(() => {\n setStatus(\"idle\");\n setProgress(0);\n setLog(\"\");\n setError(null);\n }, []);\n\n return {\n supported,\n connected: Boolean(port),\n bootloaderReady: Boolean(bootloaderPort),\n port,\n deviceLabel,\n status,\n progress,\n log,\n error,\n needsBootloaderPermission,\n selectDevice,\n disconnect,\n prepareBootloader,\n grantBootloaderPort,\n flash,\n resetFlashState,\n };\n}\n","import { ESPLoader, Transport } from \"esptool-js\";\n\nconst sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n\n// senseBox MCU-S2 (ESP32-S2) USB vendor id. After the 1200bps touch the board\n// re-enumerates as the ROM serial bootloader, which always uses product id\n// 0x0002 regardless of the application product id.\nexport const DEFAULT_USB_VENDOR_ID = 0x303a;\nconst ESP32_ROM_BOOTLOADER_PRODUCT_ID = 0x0002;\n\n// ESP32-S2 RTC control registers used for an on-chip software reset. Writing\n// the SW_SYS_RST bit performs a full digital system reset - functionally the\n// same as a power cycle - which works over native USB (no RTS/DTR lines\n// required).\nconst RTC_CNTL_OPTIONS0_REG = 0x3f408000; // DR_REG_RTCCNTL_BASE + 0x0\nconst RTC_CNTL_SW_SYS_RST = 0x80000000; // BIT(31)\n\n/**\n * Checks whether a serial port is the ESP32 ROM serial bootloader.\n * @param {SerialPort} port\n * @param {number} vendorId\n * @returns {boolean}\n */\nexport function isRomBootloaderPort(port, vendorId) {\n try {\n const { usbVendorId, usbProductId } = port.getInfo();\n return (\n usbVendorId === vendorId &&\n usbProductId === ESP32_ROM_BOOTLOADER_PRODUCT_ID\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Performs a real hardware reset by pulsing the EN/RST pin via the RTS line,\n * exactly like the Arduino IDE / esptool.py do. esptool-js' built-in\n * \"hard_reset\" only ever releases RTS (`setRTS(false)`) and never asserts it,\n * so it produces no EN pulse - we do the full pulse here.\n *\n * IO0 is kept HIGH (`setDTR(false)`) so the chip boots the application instead\n * of dropping back into the ROM download mode.\n *\n * @param {Transport} transport A connected esptool-js transport.\n */\nasync function hardResetViaRts(transport) {\n await transport.setDTR(false); // IO0 HIGH -> normal boot (not download mode)\n await transport.setRTS(true); // EN LOW -> hold chip in reset\n await sleep(150);\n await transport.setRTS(false); // EN HIGH -> release; chip boots the new app\n await sleep(150);\n}\n\n/**\n * Triggers a full on-chip software reset on an ESP32-S2 by setting the\n * RTC_CNTL_SW_SYS_RST bit. Used as a fallback for boards without an RTS->EN\n * auto-reset circuit. Fired without waiting for an acknowledgement: the chip\n * resets the instant the register is written, so it never sends a response.\n *\n * @param {ESPLoader} loader A connected esptool-js loader.\n */\nasync function softResetViaRegister(loader) {\n const pkt = new Uint8Array(16);\n const view = new DataView(pkt.buffer);\n view.setUint32(0, RTC_CNTL_OPTIONS0_REG, true); // address\n view.setUint32(4, RTC_CNTL_SW_SYS_RST, true); // value\n view.setUint32(8, RTC_CNTL_SW_SYS_RST, true); // mask (only touch the reset bit)\n view.setUint32(12, 0, true); // delay (us)\n\n await loader.command(loader.ESP_WRITE_REG, pkt, undefined, false);\n}\n\n/**\n * Puts an ESP32 with native USB into ROM download mode without the user\n * having to press BOOT + RESET, mirroring the Arduino IDE behaviour\n * (`use_1200bps_touch=true`). Opening the port at 1200 baud is detected by the\n * running firmware, which then reboots into the serial bootloader. The device\n * re-enumerates on the USB bus (usually with a different product id), so the\n * original port handle becomes unusable afterwards.\n *\n * @param {SerialPort} port The currently selected (application) serial port.\n */\nexport async function touchTo1200bps(port) {\n try {\n await port.close();\n } catch {\n // Port was not open - ignore.\n }\n\n await sleep(300);\n\n // Warm-up open at a standard baud rate. On a freshly enumerated native-USB\n // CDC port the very first open at the unusual 1200 baud rate is frequently\n // rejected by the OS driver, whereas a standard rate opens fine. Opening and\n // closing at 115200 first initialises the CDC line coding so the following\n // 1200-baud open succeeds on the first try.\n try {\n await port.open({ baudRate: 115200 });\n await port.close();\n await sleep(200);\n } catch {\n // If the warm-up could not open the port, the retry loop below still tries.\n }\n\n let lastError;\n for (let attempt = 0; attempt < 3; attempt++) {\n try {\n await port.open({ baudRate: 1200 });\n\n try {\n await port.setSignals({\n dataTerminalReady: false,\n requestToSend: false,\n });\n } catch {\n // setSignals is optional - the 1200 baud open alone is usually enough.\n }\n\n await sleep(250);\n return; // Success\n } catch (err) {\n lastError = err;\n\n // Chrome-specific race: the 1200-baud open makes the firmware reset and\n // the USB device re-enumerate as the ROM bootloader *during* the open()\n // call, so Chrome rejects even though the touch physically succeeded.\n await sleep(150);\n if (port.connected === false) {\n return;\n }\n\n try {\n await port.close();\n } catch {\n // Nothing to close - ignore.\n }\n if (attempt < 2) {\n await sleep(300);\n }\n }\n }\n\n if (port.connected === false) {\n return;\n }\n\n throw new Error(\n `Failed to open port at 1200bps after 3 attempts: ${lastError?.message}`,\n );\n}\n\n/**\n * Waits for the serial bootloader port to become available after a reset into\n * the bootloader (mirrors the Arduino IDE `wait_for_upload_port=true`).\n *\n * Resolves with the bootloader port, or `null` if none shows up within the\n * timeout (e.g. the board was already in the bootloader, or the browser has\n * no permission for the re-enumerated device).\n *\n * @param {SerialPort[]} knownPorts Ports that already existed before the reset.\n * @param {number} [timeoutMs] How long to wait for the new port.\n * @param {number} [vendorId] USB vendor id to match against.\n * @returns {Promise<SerialPort|null>}\n */\nexport function waitForBootloaderPort(\n knownPorts,\n timeoutMs = 8000,\n vendorId = DEFAULT_USB_VENDOR_ID,\n) {\n return new Promise((resolve) => {\n const known = new Set(knownPorts);\n let settled = false;\n\n const cleanup = () => {\n navigator.serial.removeEventListener(\"connect\", onConnect);\n clearInterval(pollId);\n clearTimeout(timerId);\n };\n const finish = (result) => {\n if (settled) return;\n settled = true;\n cleanup();\n resolve(result);\n };\n\n const onConnect = (event) => {\n const candidate = event.target;\n if (isRomBootloaderPort(candidate, vendorId) || !known.has(candidate)) {\n finish(candidate);\n }\n };\n navigator.serial.addEventListener(\"connect\", onConnect);\n\n const pollId = setInterval(async () => {\n const ports = await navigator.serial.getPorts();\n const bootloader = ports.find(\n (candidate) =>\n isRomBootloaderPort(candidate, vendorId) &&\n candidate.connected === true,\n );\n if (bootloader) {\n finish(bootloader);\n return;\n }\n const fresh = ports.find(\n (candidate) => !known.has(candidate) && candidate.connected !== false,\n );\n if (fresh) finish(fresh);\n }, 400);\n\n const timerId = setTimeout(() => finish(null), timeoutMs);\n });\n}\n\n/**\n * Flashes one or more binary images to an ESP32-family chip over the Web\n * Serial API, using Espressif's esptool-js.\n *\n * @param {object} params\n * @param {SerialPort} params.port A Web Serial port chosen by the user.\n * @param {Array<{ data: Uint8Array, address: number }>} params.fileArray\n * @param {number} [params.baudrate] Baud rate for the flashing session.\n * @param {string} [params.flashMode] SPI flash mode (e.g. \"dio\").\n * @param {string} [params.flashFreq] SPI flash frequency (e.g. \"80m\").\n * @param {string} [params.flashSize] Flash size (e.g. \"4MB\").\n * @param {boolean} [params.eraseAll] Erase the whole flash before writing.\n * @param {boolean} [params.usingUsbOtg] Reset via the USB-OTG sequence (ESP32-S2/S3).\n * @param {(msg: string) => void} [params.onLog] Receives console output.\n * @param {(percent: number) => void} [params.onProgress] 0-100 progress.\n * @returns {Promise<string>} The detected chip description.\n */\nexport async function flashBinary({\n port,\n fileArray,\n baudrate = 115200,\n flashMode = \"keep\",\n flashFreq = \"keep\",\n flashSize = \"keep\",\n eraseAll = false,\n usingUsbOtg = false,\n onLog,\n onProgress,\n}) {\n const terminal = {\n clean: () => {},\n write: (data) => onLog?.(data),\n writeLine: (data) => onLog?.(`${data}\\n`),\n };\n\n const transport = new Transport(port, false);\n const loader = new ESPLoader({ transport, baudrate, terminal });\n\n try {\n const chip = await loader.main();\n\n await loader.writeFlash({\n fileArray,\n flashSize,\n flashMode,\n flashFreq,\n eraseAll,\n compress: true,\n reportProgress: (_fileIndex, written, total) => {\n if (total > 0) {\n onProgress?.(Math.round((written / total) * 100));\n }\n },\n });\n\n if (usingUsbOtg) {\n try {\n await hardResetViaRts(transport);\n } catch {\n // The chip drops off the USB bus the instant it resets, so the\n // trailing RTS write may fail - that is expected.\n }\n try {\n await softResetViaRegister(loader);\n } catch {\n // Ignore - the chip is already rebooting.\n }\n } else {\n await loader.after(\"hard_reset\");\n }\n return chip;\n } finally {\n try {\n await transport.disconnect();\n } catch {\n // Ignore errors while releasing the port.\n }\n }\n}\n","// Firmware is shipped through the JS module graph (dynamic import of a\n// base64-encoded module, generated by scripts/generate-firmware-modules.mjs\n// from the .bin files in this folder) rather than fetched by URL. A plain\n// import() is resolved correctly by every bundler regardless of how it\n// packages this library, unlike `new URL(path, import.meta.url)` + fetch,\n// which breaks the moment a consumer's bundler re-hosts the code somewhere\n// that doesn't also serve the sibling .bin file.\nconst LOADERS = {\n circuitpy: () => import(\"./generated/circuitpy.js\"),\n ota: () => import(\"./generated/ota.js\"),\n basic: () => import(\"./generated/basic.js\"),\n standard: () => import(\"./generated/standard.js\"),\n};\n\nexport const BUILTIN_FIRMWARE_TYPES = Object.keys(LOADERS);\n\nfunction base64ToBytes(base64) {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\n/**\n * Loads one of the firmware images bundled with the package.\n * @param {\"circuitpy\"|\"ota\"|\"basic\"|\"standard\"} type\n * @returns {Promise<Uint8Array|null>}\n */\nexport async function loadBuiltinFirmware(type) {\n const loader = LOADERS[type];\n if (!loader) return null;\n const module = await loader();\n return base64ToBytes(module.default);\n}\n","import React, { useEffect, useRef, useState } from \"react\";\nimport PropTypes from \"prop-types\";\n\n/**\n * A custom, animated single-select dropdown - a nicer-looking replacement\n * for a bare `<select>` that can show an icon next to each option (and next\n * to the current selection in the trigger). Every option row reserves the\n * same icon slot width regardless of whether it actually has an icon, so a\n * mix of icon/no-icon options still lines up.\n *\n * Deliberately not built on the native <select>: rendering custom option\n * icons and a CSS-driven open/close transition isn't possible with a real\n * <select>'s browser-native popup.\n */\nexport function Dropdown({\n options,\n value,\n onChange,\n disabled = false,\n placeholder,\n className,\n \"aria-label\": ariaLabel,\n}) {\n const [open, setOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState(-1);\n const rootRef = useRef(null);\n const triggerRef = useRef(null);\n const optionRefs = useRef([]);\n\n const selectedIndex = options.findIndex((option) => option.value === value);\n const selected = selectedIndex >= 0 ? options[selectedIndex] : null;\n\n useEffect(() => {\n if (disabled && open) setOpen(false);\n }, [disabled, open]);\n\n useEffect(() => {\n if (!open) return;\n\n const onDocPointerDown = (event) => {\n if (rootRef.current && !rootRef.current.contains(event.target)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"pointerdown\", onDocPointerDown);\n return () => document.removeEventListener(\"pointerdown\", onDocPointerDown);\n }, [open]);\n\n useEffect(() => {\n if (!open) return;\n setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);\n }, [open, selectedIndex]);\n\n useEffect(() => {\n if (!open || activeIndex < 0) return;\n optionRefs.current[activeIndex]?.focus();\n }, [open, activeIndex]);\n\n const commit = (index) => {\n const option = options[index];\n if (!option || option.disabled) return;\n onChange?.(option.value);\n setOpen(false);\n triggerRef.current?.focus();\n };\n\n const handleTriggerKeyDown = (event) => {\n if (disabled) return;\n if (\n event.key === \"Enter\" ||\n event.key === \" \" ||\n event.key === \"ArrowDown\" ||\n event.key === \"ArrowUp\"\n ) {\n event.preventDefault();\n setOpen(true);\n }\n };\n\n const moveActive = (delta) => {\n setActiveIndex((current) => {\n const count = options.length;\n let next = current;\n for (let i = 0; i < count; i++) {\n next = (next + delta + count) % count;\n if (!options[next].disabled) return next;\n }\n return current;\n });\n };\n\n const handleListKeyDown = (event) => {\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n moveActive(1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n moveActive(-1);\n break;\n case \"Home\":\n event.preventDefault();\n setActiveIndex(0);\n break;\n case \"End\":\n event.preventDefault();\n setActiveIndex(options.length - 1);\n break;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n commit(activeIndex);\n break;\n case \"Escape\":\n event.preventDefault();\n setOpen(false);\n triggerRef.current?.focus();\n break;\n case \"Tab\":\n setOpen(false);\n break;\n default:\n break;\n }\n };\n\n return (\n <div\n ref={rootRef}\n className={`sbdropdown${open ? \" sbdropdown--open\" : \"\"}${\n disabled ? \" sbdropdown--disabled\" : \"\"\n }${className ? ` ${className}` : \"\"}`}\n >\n <button\n ref={triggerRef}\n type=\"button\"\n className=\"sbdropdown__trigger\"\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-label={ariaLabel}\n disabled={disabled}\n onClick={() => setOpen((v) => !v)}\n onKeyDown={handleTriggerKeyDown}\n >\n <span className=\"sbdropdown__icon-slot\">{selected?.icon}</span>\n <span className=\"sbdropdown__trigger-label\">\n {selected?.label || placeholder}\n </span>\n <IconChevron className=\"sbdropdown__chevron\" />\n </button>\n\n <ul\n className=\"sbdropdown__menu\"\n role=\"listbox\"\n aria-label={ariaLabel}\n onKeyDown={handleListKeyDown}\n >\n {options.map((option, index) => (\n <li key={option.value} role=\"presentation\">\n <button\n ref={(el) => {\n optionRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"option\"\n aria-selected={option.value === value}\n tabIndex={-1}\n className={`sbdropdown__option${\n option.value === value ? \" sbdropdown__option--selected\" : \"\"\n }${index === activeIndex ? \" sbdropdown__option--active\" : \"\"}`}\n disabled={option.disabled}\n onClick={() => commit(index)}\n onMouseEnter={() => setActiveIndex(index)}\n >\n <span className=\"sbdropdown__icon-slot\">{option.icon}</span>\n <span className=\"sbdropdown__option-label\">{option.label}</span>\n {option.value === value && (\n <IconCheck className=\"sbdropdown__option-check\" />\n )}\n </button>\n </li>\n ))}\n </ul>\n </div>\n );\n}\n\nfunction IconChevron({ className }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 10 6\"\n width=\"10\"\n height=\"6\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M1 1l4 4 4-4\" />\n </svg>\n );\n}\n\nfunction IconCheck({ className }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 16 16\"\n width=\"12\"\n height=\"12\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M3 8.5 6.5 12 13 4.5\" />\n </svg>\n );\n}\n\nDropdown.propTypes = {\n options: PropTypes.arrayOf(\n PropTypes.shape({\n value: PropTypes.string.isRequired,\n label: PropTypes.node.isRequired,\n icon: PropTypes.node,\n disabled: PropTypes.bool,\n }),\n ).isRequired,\n value: PropTypes.string,\n onChange: PropTypes.func,\n disabled: PropTypes.bool,\n placeholder: PropTypes.string,\n className: PropTypes.string,\n \"aria-label\": PropTypes.string,\n};\n"]}
|