@sensebox/flash-tool 1.1.0 → 1.1.1
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/index.cjs +156 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +156 -4
- package/dist/index.js.map +1 -1
- 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/useFlashTool.js","../src/react/useFlashDevice.js","../src/core/esptool.js","../src/core/boards.js","../src/core/sambaTransport.js","../src/core/samd.js","../src/core/flash.js","../src/firmware/index.js","../src/react/flashToolConfig.js","../src/react/Dropdown.jsx"],"names":[],"mappings":"AAAA;ACAA,4EAAyC;AACzC,qCAA6B;AAC7B,6FAAsB;ADEtB;AACA;AELA;AFOA;AACA;AGRA;AHUA;AACA;AIXA,IAAM,MAAA,EAAQ,CAAC,EAAA,EAAA,GAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAA,GAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AAK/D,IAAM,sBAAA,EAAwB,KAAA;AACrC,IAAM,gCAAA,EAAkC,CAAA;AAMxC,IAAM,sBAAA,EAAwB,UAAA;AAC9B,IAAM,oBAAA,EAAsB,UAAA;AAQrB,SAAS,mBAAA,CAAoB,IAAA,EAAM,QAAA,EAAU;AAClD,EAAA,IAAI;AACF,IAAA,MAAM,EAAE,WAAA,EAAa,aAAa,EAAA,EAAI,IAAA,CAAK,OAAA,CAAQ,CAAA;AACnD,IAAA,OACE,YAAA,IAAgB,SAAA,GAChB,aAAA,IAAiB,+BAAA;AAAA,EAErB,EAAA,UAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAaA,MAAA,SAAe,eAAA,CAAgB,SAAA,EAAW;AACxC,EAAA,MAAM,SAAA,CAAU,MAAA,CAAO,KAAK,CAAA;AAC5B,EAAA,MAAM,SAAA,CAAU,MAAA,CAAO,IAAI,CAAA;AAC3B,EAAA,MAAM,KAAA,CAAM,GAAG,CAAA;AACf,EAAA,MAAM,SAAA,CAAU,MAAA,CAAO,KAAK,CAAA;AAC5B,EAAA,MAAM,KAAA,CAAM,GAAG,CAAA;AACjB;AAUA,MAAA,SAAe,oBAAA,CAAqB,MAAA,EAAQ;AAC1C,EAAA,MAAM,IAAA,EAAM,IAAI,UAAA,CAAW,EAAE,CAAA;AAC7B,EAAA,MAAM,KAAA,EAAO,IAAI,QAAA,CAAS,GAAA,CAAI,MAAM,CAAA;AACpC,EAAA,IAAA,CAAK,SAAA,CAAU,CAAA,EAAG,qBAAA,EAAuB,IAAI,CAAA;AAC7C,EAAA,IAAA,CAAK,SAAA,CAAU,CAAA,EAAG,mBAAA,EAAqB,IAAI,CAAA;AAC3C,EAAA,IAAA,CAAK,SAAA,CAAU,CAAA,EAAG,mBAAA,EAAqB,IAAI,CAAA;AAC3C,EAAA,IAAA,CAAK,SAAA,CAAU,EAAA,EAAI,CAAA,EAAG,IAAI,CAAA;AAE1B,EAAA,MAAM,MAAA,CAAO,OAAA,CAAQ,MAAA,CAAO,aAAA,EAAe,GAAA,EAAK,KAAA,CAAA,EAAW,KAAK,CAAA;AAClE;AAWA,MAAA,SAAsB,YAAA,CAAa,MAAA,EAAQ;AACzC,EAAA,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,UAAA,IAAc,UAAA,EAAY;AACxC,IAAA,MAAM,IAAI,KAAA,CAAM,gDAAgD,CAAA;AAAA,EAClE;AAEA,EAAA,MAAM,MAAA,CAAO,QAAA,CAAS,UAAA,EAAY,CAAA,EAAG,CAAC,CAAA;AACtC,EAAA,MAAM,MAAA,CAAO,QAAA,CAAS,UAAA,EAAY,UAAU,CAAA;AAC5C,EAAA,MAAM,MAAA,CAAO,QAAA,CAAS,UAAA,EAAY,GAAI,CAAA;AACtC,EAAA,MAAM,MAAA,CAAO,QAAA,CAAS,UAAA,EAAY,UAAU,CAAA;AAC5C,EAAA,MAAM,MAAA,CAAO,QAAA,CAAS,UAAA,EAAY,CAAC,CAAA;AACnC,EAAA,MAAM,KAAA,CAAM,GAAG,CAAA;AACjB;AAYA,MAAA,SAAsB,cAAA,CAAe,IAAA,EAAM;AACzC,EAAA,IAAI;AACF,IAAA,MAAM,IAAA,CAAK,KAAA,CAAM,CAAA;AAAA,EACnB,EAAA,WAAQ;AAAA,EAER;AAEA,EAAA,MAAM,KAAA,CAAM,GAAG,CAAA;AAOf,EAAA,IAAI;AACF,IAAA,MAAM,IAAA,CAAK,IAAA,CAAK,EAAE,QAAA,EAAU,OAAO,CAAC,CAAA;AACpC,IAAA,MAAM,IAAA,CAAK,KAAA,CAAM,CAAA;AACjB,IAAA,MAAM,KAAA,CAAM,GAAG,CAAA;AAAA,EACjB,EAAA,WAAQ;AAAA,EAER;AAEA,EAAA,IAAI,SAAA;AACJ,EAAA,IAAA,CAAA,IAAS,QAAA,EAAU,CAAA,EAAG,QAAA,EAAU,CAAA,EAAG,OAAA,EAAA,EAAW;AAC5C,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,CAAK,IAAA,CAAK,EAAE,QAAA,EAAU,KAAK,CAAC,CAAA;AAElC,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,CAAK,UAAA,CAAW;AAAA,UACpB,iBAAA,EAAmB,KAAA;AAAA,UACnB,aAAA,EAAe;AAAA,QACjB,CAAC,CAAA;AAAA,MACH,EAAA,WAAQ;AAAA,MAER;AAEA,MAAA,MAAM,KAAA,CAAM,GAAG,CAAA;AACf,MAAA,MAAA;AAAA,IACF,EAAA,MAAA,CAAS,GAAA,EAAK;AACZ,MAAA,UAAA,EAAY,GAAA;AAKZ,MAAA,MAAM,KAAA,CAAM,GAAG,CAAA;AACf,MAAA,GAAA,CAAI,IAAA,CAAK,UAAA,IAAc,KAAA,EAAO;AAC5B,QAAA,MAAA;AAAA,MACF;AAEA,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,CAAK,KAAA,CAAM,CAAA;AAAA,MACnB,EAAA,WAAQ;AAAA,MAER;AACA,MAAA,GAAA,CAAI,QAAA,EAAU,CAAA,EAAG;AACf,QAAA,MAAM,KAAA,CAAM,GAAG,CAAA;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAEA,EAAA,GAAA,CAAI,IAAA,CAAK,UAAA,IAAc,KAAA,EAAO;AAC5B,IAAA,MAAA;AAAA,EACF;AAEA,EAAA,MAAM,IAAI,KAAA;AAAA,IACR,CAAA,iDAAA,EAAoD,UAAA,GAAA,KAAA,EAAA,KAAA,EAAA,EAAA,SAAA,CAAW,OAAO,CAAA;AAAA,EAAA;AAE1E;AAqBO;AAKL,EAAA;AACA,EAAA;AAIA,EAAA;AACE,IAAA;AACA,IAAA;AAEA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AAAoB,IAAA;AAEtB,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAAc,IAAA;AAGhB,IAAA;AACE,MAAA;AACA,MAAA;AACE,QAAA;AAAgB,MAAA;AAClB,IAAA;AAEF,IAAA;AAEA,IAAA;AACE,MAAA;AACA,MAAA;AAAyB,QAAA;AAE8B,MAAA;AAEvD,MAAA;AACE,QAAA;AACA,QAAA;AAAA,MAAA;AAEF,MAAA;AAAoB,QAAA;AAIQ,MAAA;AAE5B,MAAA;AAAuB,IAAA;AAGzB,IAAA;AAAwD,EAAA;AAE5D;AAeA;AAKE,EAAA;AACE,IAAA;AACA,IAAA;AAAyB,EAAA;AAEzB,IAAA;AACE,MAAA;AAA+B,IAAA;AACzB,IAAA;AAMR,IAAA;AACE,MAAA;AACE,QAAA;AAAiC,MAAA;AAEjC,QAAA;AAAqC,MAAA;AACvC,IAAA;AACM,IAAA;AAER,EAAA;AAEA,IAAA;AAA+B,EAAA;AAEnC;AAqBA;AAAkC,EAAA;AAChC,EAAA;AACA,EAAA;AACW,EAAA;AACC,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACG,EAAA;AACd,EAAA;AACA,EAAA;AAEF;AAGE,EAAA;AACA,EAAA;AAAiB,IAAA;AACF,IAAA;AAAC,IAAA;AACW,IAAA;AACW;AAAA,EAAA;AAGtC,EAAA;AACA,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AAAU,QAAA;AAC2D,MAAA;AACrE,IAAA;AAGF,IAAA;AAAwB,MAAA;AACtB,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACU,MAAA;AAER,QAAA;AACE,UAAA;AAA+C,QAAA;AACjD,MAAA;AACF,IAAA;AAGF,IAAA;AACA,IAAA;AAAO,EAAA;AAEP,IAAA;AACE,MAAA;AAA2B,IAAA;AACrB,IAAA;AAER,EAAA;AAEJ;AJ1JA;AACA;AK/MA;AAEA;AAAY,EAAA;AACA,EAAA;AACW,EAAA;AACZ,EAAA;AACE,EAAA;AACA,EAAA;AAEb;AAQO;AAAuB,EAAA;AACvB,IAAA;AACC,IAAA;AACW,IAAA;AACH,IAAA;AACI,IAAA;AACT,IAAA;AACU,IAAA;AACP,IAAA;AACiB,IAAA;AACN;AAAA,IAAA;AAEZ,IAAA;AACA,IAAA;AACQ,EAAA;AACnB,EAAA;AACU,IAAA;AACL,IAAA;AACC,IAAA;AACW,IAAA;AACH,IAAA;AACI,IAAA;AACT,IAAA;AACU,IAAA;AAC0B,IAAA;AACrC,IAAA;AACK,IAAA;AACM,EAAA;AACnB,EAAA;AACW,IAAA;AACN,IAAA;AACC,IAAA;AACW,IAAA;AACH,IAAA;AACI,IAAA;AACT,IAAA;AACU,IAAA;AACkC,IAAA;AACd,IAAA;AAC/B,IAAA;AACK,IAAA;AACM,EAAA;AAErB;AAEO;AAOA;AAGP;AAQO;AACL,EAAA;AAGA,EAAA;AACA,EAAA;AAAyC,IAAA;AAClB,IAAA;AACrB,EAAA;AAEJ;AAQO;AAxGP,EAAA;AAyGE,EAAA;AACA,EAAA;AACE,IAAA;AAA4B,EAAA;AAE5B,IAAA;AAAO,EAAA;AAET,EAAA;AAAwC,IAAA;AAIL,EAAA;AAErC;AAaO;AACL,EAAA;AAAiC,IAAA;AACD,EAAA;AAEhC,EAAA;AAKF;AAQO;AACL,EAAA;AAAO,IAAA;AACqB,IAAA;AACJ,IAAA;AACtB,IAAA;AACY,IAAA;AACiB,IAAA;AACvB,EAAA;AAEV;AAOO;AAnKP,EAAA;AAoKE,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AAA+C,EAAA;AAEjD,EAAA;AAEA,EAAA;AACF;ALiJA;AACA;AM3TO;AACL,EAAA;AAA2B,IAAA;AAIvB,MAAA;AAA2B,QAAA;AACf,QAAA;AACA,QAAA;AACA,QAAA;AACF,QAAA;AACK,QAAA;AACD,MAAA;AAEd,MAAA;AAAiC,QAAA;AACZ,QAAA;AACJ,MAAA;AAEjB,MAAA;AACA,MAAA;AAAoB,IAAA;AACtB,IAAA;AAME,MAAA;AACA,MAAA;AAAyB,IAAA;AAC3B,IAAA;AAGE,MAAA;AACA,MAAA;AAAY,IAAA;AACd,IAAA;AAnCJ,MAAA;AAsCM,MAAA;AACA,MAAA;AACE,QAAA;AAAyB,MAAA;AAEzB,QAAA;AAAW,MAAA;AACb,IAAA;AACF,IAAA;AAGE,MAAA;AACE,QAAA;AAAsC,MAAA;AAEtC,QAAA;AAAsE,MAAA;AACxE,IAAA;AACF,IAAA;AAME,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AACE,UAAA;AACA,UAAA;AACE,YAAA;AACE,cAAA;AAAsD,UAAA;AAExD,YAAA;AAAe,UAAA;AACjB,QAAA;AAEA,UAAA;AAAsD,QAAA;AACxD,MAAA;AAEF,MAAA;AAAU,QAAA;AACR,MAAA;AAEF,IAAA;AACF,IAAA;AAKE,MAAA;AACE,QAAA;AACA,QAAA;AAAwB,UAAA;AAC4B,UAAA;AAClD,QAAA;AAEF,QAAA;AACA,QAAA;AAAU,MAAA;AACZ,IAAA;AACF,IAAA;AAGE,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACE,YAAA;AAA4B,UAAA;AAC9B,QAAA;AAEA,UAAA;AAAsD,QAAA;AACxD,MAAA;AAEF,MAAA;AACE,QAAA;AAAU,UAAA;AAC4E,QAAA;AACtF,MAAA;AAEF,MAAA;AAA+C,IAAA;AACjD,EAAA;AAEJ;AN4SA;AACA;AO9ZA;AAaA;AACE,EAAA;AACA,EAAA;AAAe,IAAA;AACA,IAAA;AAAC,IAAA;AAC6B;AAAA,IAAA;AACE;AAAA,EAAA;AAE/C,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AAAoE,IAAA;AAEtE,IAAA;AAIE,MAAA;AAAqE,IAAA;AAEvE,IAAA;AAA0C,MAAA;AACT,MAAA;AAE7B,QAAA;AAA0D,MAAA;AAC5D,IAAA;AAEF,IAAA;AACA,IAAA;AAEA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AAAmE,MAAA;AACrE,IAAA;AAEF,IAAA;AAAmB,EAAA;AAEnB,IAAA;AACE,MAAA;AAAuB,IAAA;AACjB,IAAA;AAGR,IAAA;AACE,MAAA;AAAiB,IAAA;AACX,IAAA;AAER,EAAA;AAEJ;APiZA;AACA;AQxcO;AAIL,EAAA;AACE,IAAA;AAAyE,EAAA;AAE3E,EAAA;AAAwB,IAAA;AACtB,IAAA;AACyE,IAAA;AACvD,IAAA;AACC,IAAA;AACA,IAAA;AACA,IAAA;AACD,IAAA;AACG,IAAA;AACC,IAAA;AACtB,IAAA;AACA,EAAA;AAEJ;ARucA;AACA;AG/dA;AAA8B,EAAA;AAClB,EAAA;AACA,EAAA;AAEZ;AAEA;AACE,EAAA;AAGA,EAAA;AACE,IAAA;AAAsC,EAAA;AAExC,EAAA;AACA,EAAA;AACE,IAAA;AAAU,MAAA;AAC4B,IAAA;AACtC,EAAA;AAEF,EAAA;AACF;AAuBO;AAAwB,EAAA;AAC7B,EAAA;AACS,EAAA;AACT,EAAA;AACgB,EAAA;AAElB;AACE,EAAA;AAAgB,IAAA;AACqD,IAAA;AACjC,EAAA;AAOpC,EAAA;AACA,EAAA;AACE,IAAA;AAAA,MAAA;AAC2D,IAAA;AAC3D,EAAA;AAGF,EAAA;AACA,EAAA;AAIA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AAAkB,EAAA;AAEpB,EAAA;AACE,IAAA;AACA,IAAA;AAA4B,EAAA;AAE9B,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AACA,EAAA;AAA2B,IAAA;AACtB,IAAA;AACc,IAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AAChB,EAAA;AAGL,EAAA;AACE,IAAA;AAAuC,EAAA;AAOzC,EAAA;AAGA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAAa,EAAA;AAGf,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AAAwD,QAAA;AAC1B,MAAA;AAE9B,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAQA,MAAA;AACE,QAAA;AACA,QAAA;AAAA,UAAA;AACE,QAAA;AACF,MAAA;AAEA,QAAA;AAAsB,MAAA;AAGxB,MAAA;AAAO,IAAA;AAEP,MAAA;AACE,QAAA;AAAoC,MAAA;AAEtC,MAAA;AAAO,IAAA;AACT,EAAA;AAOF,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAAO,IAAA;AAEP,MAAA;AACA,MAAA;AAAO,IAAA;AACT,EAAA;AAGF,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAAa,EAAA;AAIf,EAAA;AAA0B,IAAA;AAEtB,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AAEA,QAAA;AAEA,QAAA;AAAsB,UAAA;AACpB,UAAA;AACA,UAAA;AACmD,QAAA;AAGrD,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AAAO,QAAA;AAGT,QAAA;AAAA,UAAA;AACE,QAAA;AAEF,QAAA;AACA,QAAA;AAAO,MAAA;AAEP,QAAA;AACA,QAAA;AAAmD;AACnD,QAAA;AACA,QAAA;AAAM,MAAA;AAEN,QAAA;AAAkB,MAAA;AACpB,IAAA;AACF,IAAA;AACsC,EAAA;AAKxC,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AAA0D,QAAA;AACtB,MAAA;AAEpC,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAAO,IAAA;AAEP,MAAA;AACE,QAAA;AACA,QAAA;AAAO,MAAA;AAET,MAAA;AACA,MAAA;AAA0C;AAC1C,MAAA;AACA,MAAA;AAAM,IAAA;AACR,EAAA;AAGF,EAAA;AAAc,IAAA;AAEV,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AAAA,MAAA;AAEF,MAAA;AAEA,MAAA;AAEA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAEA,MAAA;AACE,QAAA;AACE,UAAA;AACA,UAAA;AACE,YAAA;AAAqC,UAAA;AAKvC,UAAA;AACE,YAAA;AAAiB,cAAA;AACiB,cAAA;AAChC,YAAA;AAEF,YAAA;AACE,cAAA;AAAU,gBAAA;AACR,cAAA;AACF,YAAA;AAEF,YAAA;AACA,YAAA;AAAgB,UAAA;AAElB,UAAA;AAEE,YAAA;AACA,YAAA;AAA4B,UAAA;AAC9B,QAAA;AAEF,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AAGE,YAAA;AACA,YAAA;AAAA,UAAA;AACF,QAAA;AAGF,QAAA;AACA,QAAA;AAAuB,UAAA;AACrB,UAAA;AACM,UAAA;AACN,UAAA;AACS,UAAA;AACF,UAAA;AACK,QAAA;AAGd,QAAA;AACA,QAAA;AACA,QAAA;AAMA,QAAA;AACA,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AAAiB,QAAA;AACnB,MAAA;AAEA,QAAA;AACA,QAAA;AAAiB,MAAA;AAEjB,QAAA;AAAkB,MAAA;AACpB,IAAA;AACF,IAAA;AACA,MAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,IAAA;AACF,EAAA;AAGF,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAAkC,EAAA;AAGpC,EAAA;AAAO,IAAA;AACL,IAAA;AACA,IAAA;AACuB,IAAA;AACgB,IAAA;AACvC,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,EAAA;AAEJ;AH4YA;AACA;ASnxBA;AAAgB,EAAA;AACoC,EAAA;AACZ,EAAA;AACI,EAAA;AAE5C;AAEO;AAEP;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AAA8B,EAAA;AAEhC,EAAA;AACF;AAOA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AT6wBA;AACA;AU7yBO;AAKA;AAAyB,EAAA;AAEhC;AAEO;AACL,EAAA;AACA,EAAA;AACF;AAYO;AAAyC,EAAA;AACJ,EAAA;AACN,EAAA;AACE,EAAA;AAExC;AAMO;AAAsC,EAAA;AAChC,IAAA;AACH,IAAA;AACI,IAAA;AAER,EAAA;AACJ,EAAA;AACK,IAAA;AACG,EAAA;AACR,EAAA;AACO,IAAA;AACC,EAAA;AACR,EAAA;AACU,IAAA;AACF,EAAA;AAEV;AAIO;AAAyC,EAAA;AACnC,IAAA;AACH,IAAA;AACI,IAAA;AAER,EAAA;AACJ,EAAA;AACK,IAAA;AACG,EAAA;AACR,EAAA;AACO,IAAA;AACC,EAAA;AACR,EAAA;AACU,IAAA;AACF,EAAA;AAEV;AAEO;AAAwB,EAAA;AAChB,EAAA;AACJ,EAAA;AACG,EAAA;AACD,EAAA;AACF,EAAA;AACE,EAAA;AACM,EAAA;AACC,EAAA;AACD,EAAA;AACV,EAAA;AACG,EAAA;AACJ,EAAA;AACK,EAAA;AACJ,EAAA;AACA,EAAA;AACK,EAAA;AACH,EAAA;AACA,EAAA;AACO,EAAA;AACH,EAAA;AACD,EAAA;AACD,EAAA;AAET,EAAA;AACU,EAAA;AACD,EAAA;AACU,EAAA;AAEnB,EAAA;AACS,EAAA;AACC,EAAA;AACD,EAAA;AACQ,EAAA;AACN,EAAA;AACE,EAAA;AACG,EAAA;AACZ,EAAA;AACK,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAEb;AAMO;AAA2B,EAAA;AACnB,EAAA;AACJ,EAAA;AACG,EAAA;AACD,EAAA;AACF,EAAA;AACE,EAAA;AACM,EAAA;AACC,EAAA;AACD,EAAA;AACV,EAAA;AACG,EAAA;AACJ,EAAA;AACK,EAAA;AACJ,EAAA;AACA,EAAA;AACK,EAAA;AACH,EAAA;AACA,EAAA;AAEP,EAAA;AACW,EAAA;AACD,EAAA;AAEV,EAAA;AAEA,EAAA;AACU,EAAA;AACD,EAAA;AAET,EAAA;AAEA,EAAA;AACS,EAAA;AACC,EAAA;AACD,EAAA;AACQ,EAAA;AACN,EAAA;AACE,EAAA;AACG,EAAA;AACZ,EAAA;AACK,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAEb;AAWO;AACL,EAAA;AAAe,IAAA;AACgC,IAAA;AAC1C,EAAA;AAEL,EAAA;AAGA,EAAA;AAAO,IAAA;AACF,IAAA;AACgD,IAAA;AAC9B,MAAA;AACnB,MAAA;AACQ,IAAA;AACV,EAAA;AAEJ;AAGO;AACL,EAAA;AACF;AASO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAcO;AACL,EAAA;AACE,IAAA;AAAqD,EAAA;AAEvD,EAAA;AAAO,IAAA;AACuB,IAAA;AAC+B,IAAA;AAKrD,EAAA;AAEV;AViuBA;AACA;AEv8BA;AAgBO;AAAsB,EAAA;AAC3B,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACY,EAAA;AACD,EAAA;AACF,EAAA;AACT,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAEF;AACE,EAAA;AAA8B,IAAA;AAC5B,IAAA;AACA,IAAA;AACA,IAAA;AACA,EAAA;AAEF,EAAA;AAAM,IAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,EAAA;AAGF,EAAA;AAEA,EAAA;AAIA,EAAA;AAKA,EAAA;AACA,EAAA;AAEA,EAAA;AAA6C,IAAA;AACkB,EAAA;AAG/D,EAAA;AACE,IAAA;AAA+D,EAAA;AAQjE,EAAA;AAKA,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAMA,EAAA;AACE,IAAA;AAEA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AAAA,IAAA;AAGF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAEA,IAAA;AAEI,MAAA;AACA,MAAA;AACA,MAAA;AAAsB,IAAA;AAGtB,MAAA;AACA,MAAA;AACA,MAAA;AAAQ,QAAA;AAC6C,QAAA;AACnD,MAAA;AAEF,MAAA;AACA,MAAA;AAAwD,IAAA;AAGxD,MAAA;AAAuC,IAAA;AAG3C,IAAA;AACE,MAAA;AAAY,IAAA;AACd,EAAA;AACC,IAAA;AACD,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,EAAA;AAKF,EAAA;AAOA,EAAA;AAAuB,IAAA;AAGf,MAAA;AAEiD,MAAA;AACrB,IAAA;AAE3B,IAAA;AACkE,EAAA;AAG3E,EAAA;AAEA,EAAA;AACI,IAAA;AAIkD,IAAA;AACtB,EAAA;AAIhC,EAAA;AAAsD,IAAA;AAC7C,IAAA;AAC2B,EAAA;AAGpC,EAAA;AAAwB,IAAA;AAEpB,MAAA;AACA,MAAA;AACA,MAAA;AAAuB,IAAA;AACzB,IAAA;AACqC,EAAA;AAGvC,EAAA;AACE,IAAA;AAAyC,EAAA;AAG3C,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AAAwB,EAAA;AAG1B,EAAA;AACE,IAAA;AACA,IAAA;AAA0D,EAAA;AAI5D,EAAA;AACE,IAAA;AAQE,MAAA;AAAoC,IAAA;AACtC,EAAA;AACC,IAAA;AACD,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,EAAA;AAIF,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AAA0B,IAAA;AAE1B,MAAA;AAAY,IAAA;AACd,EAAA;AAIF,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AAAO,IAAA;AAEP,MAAA;AAAY,IAAA;AACd,EAAA;AAIF,EAAA;AACE,IAAA;AAA8D,EAAA;AAIhE,EAAA;AACE,IAAA;AACA,IAAA;AAA8D,EAAA;AAIhE,EAAA;AAEA,EAAA;AAAO,IAAA;AACF;AAAA,IAAA;AAEY,IAAA;AACf,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACQ,IAAA;AACS,IAAA;AACF,IAAA;AACf;AAAA,IAAA;AAE6D,IAAA;AACnC,IAAA;AAC1B,IAAA;AACA,IAAA;AACA;AAAA,IAAA;AAEA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,EAAA;AAEJ;AF22BA;AACA;AWxqCA;AACA;AAaO;AAAkB,EAAA;AACvB,EAAA;AACA,EAAA;AACA,EAAA;AACW,EAAA;AACX,EAAA;AACA,EAAA;AAEF;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AACA,EAAA;AAEA,EAAA;AACE,IAAA;AAAmC,EAAA;AAGrC,EAAA;AACE,IAAA;AAEA,IAAA;AACE,MAAA;AACE,QAAA;AAAa,MAAA;AACf,IAAA;AAEF,IAAA;AACA,IAAA;AAAyE,EAAA;AAG3E,EAAA;AACE,IAAA;AACA,IAAA;AAAqD,EAAA;AAGvD,EAAA;AArDF,IAAA;AAsDI,IAAA;AACA,IAAA;AAAiC,EAAA;AAGnC,EAAA;AA1DF,IAAA;AA2DI,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAAoB,EAAA;AAGtB,EAAA;AACE,IAAA;AACA,IAAA;AAME,MAAA;AACA,MAAA;AAAY,IAAA;AACd,EAAA;AAGF,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AAAoC,MAAA;AAEtC,MAAA;AAAO,IAAA;AACR,EAAA;AAGH,EAAA;AA3FF,IAAA;AA4FI,IAAA;AAAmB,MAAA;AAEf,QAAA;AACA,QAAA;AACA,QAAA;AAAA,MAAA;AAEA,QAAA;AACA,QAAA;AACA,QAAA;AAAA,MAAA;AAEA,QAAA;AACA,QAAA;AACA,QAAA;AAAA,MAAA;AAEA,QAAA;AACA,QAAA;AACA,QAAA;AAAA,MAAA;AACG,MAAA;AAEH,QAAA;AACA,QAAA;AACA,QAAA;AAAA,MAAA;AAEA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AAAA,MAAA;AAEA,QAAA;AACA,QAAA;AAAA,MAAA;AAEA,QAAA;AAAA,IAAA;AACJ,EAAA;AAGF,EAAA;AACE,IAAA;AAAC,IAAA;AAAA,MAAA;AACM,MAAA;AAG8B,IAAA;AAAA,oBAAA;AAEnC,MAAA;AAAC,MAAA;AAAA,QAAA;AACM,QAAA;AACA,QAAA;AACK,QAAA;AACI,QAAA;AACC,QAAA;AACH,QAAA;AACZ,QAAA;AACgC,QAAA;AACrB,MAAA;AAAA,sBAAA;AAE6C,sBAAA;AAGxD,sBAAA;AAC6C,IAAA;AAC/C,oBAAA;AAEA,MAAA;AAAC,MAAA;AAAA,QAAA;AACW,QAAA;AACL,QAAA;AACO,QAAA;AACD,MAAA;AAAA,MAAA;AAIP,QAAA;AAAC,QAAA;AAAA,UAAA;AAEG,YAAA;AAA4B,UAAA;AAC9B,UAAA;AACK,UAAA;AACA,UAAA;AAC2B,UAAA;AACtB,UAAA;AAGmD,UAAA;AAC5C,UAAA;AACU,UAAA;AACa,QAAA;AAAA,wBAAA;AAEa,wBAAA;AACI,QAAA;AAEP,MAAA;AAIvD,IAAA;AACH,EAAA;AAGN;AAEA;AACE,EAAA;AACE,IAAA;AAAC,IAAA;AAAA,MAAA;AACC,MAAA;AACQ,MAAA;AACF,MAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACK,MAAA;AACE,MAAA;AACC,MAAA;AACH,IAAA;AAAA,oBAAA;AAEW,EAAA;AAG7B;AAEA;AACE,EAAA;AACE,IAAA;AAAC,IAAA;AAAA,MAAA;AACC,MAAA;AACQ,MAAA;AACF,MAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACK,MAAA;AACE,MAAA;AACC,MAAA;AACH,IAAA;AAAA,oBAAA;AAEmB,EAAA;AAGrC;AAEA;AAAqB,EAAA;AACA,IAAA;AACD,MAAA;AACU,MAAA;AACF,MAAA;AACN,MAAA;AACI,IAAA;AACrB,EAAA;AACD,EAAA;AACe,EAAA;AACG,EAAA;AACA,EAAA;AACG,EAAA;AACF,EAAA;AAEvB;AXuoCA;AACA;ACt2CO;AACL,EAAA;AACA,EAAA;AAEA,EAAA;AAAM,IAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACS,IAAA;AACQ,IAAA;AACjB,IAAA;AACiB,IAAA;AACjB,IAAA;AACA,IAAA;AACqB,IAAA;AACb,IAAA;AACS,IAAA;AACF,IAAA;AACI,IAAA;AACnB,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACS,IAAA;AACF,IAAA;AACK,IAAA;AACL,IAAA;AACG,EAAA;AAGZ,EAAA;AACA,EAAA;AAEA,EAAA;AAA6D,IAAA;AACxD,IAAA;AAC8B,EAAA;AAGnC,EAAA;AACE,IAAA;AAA2B,EAAA;AAG7B,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AAA6C,IAAA;AAE/C,IAAA;AACA,IAAA;AAA8D,EAAA;AAGhE,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AAA0C,EAAA;AAG5C,EAAA;AACE,IAAA;AAGE,EAAA;AAIJ,EAAA;AAMU,IAAA;AAAC,IAAA;AAAA,MAAA;AACM,MAAA;AAGL,MAAA;AAC8C,MAAA;AACpC,MAAA;AACI,MAAA;AACgC,MAAA;AACL,IAAA;AAAA,oBAAA;AAE3B,EAAA;AAGd,IAAA;AAAC,IAAA;AAAA,MAAA;AACM,MAAA;AACK,MAAA;AACD,MAAA;AACU,MAAA;AACL,IAAA;AAAA,oBAAA;AAEE,EAAA;AAGpB,IAAA;AAAC,IAAA;AAAA,MAAA;AACM,MAAA;AACK,MAAA;AACqB,MAAA;AACZ,MAAA;AACL,IAAA;AAAA,oBAAA;AAEJ,EAAA;AAKd,IAAA;AAAC,IAAA;AAAA,MAAA;AACU,MAAA;AACF,MAAA;AACG,MAAA;AAC6B,MAAA;AACpB,IAAA;AAAA,EAAA;AAMnB,IAAA;AAAC,IAAA;AAAA,MAAA;AACM,MAAA;AACK,MAAA;AACD,IAAA;AAAA,IAAA;AAED,EAAA;AAUN,IAAA;AAAC,IAAA;AAAA,MAAA;AACiC,MAAA;AACzB,MAAA;AACH,IAAA;AAAA,IAAA;AAE6D,EAAA;AAS3E,oBAAA;AACE,MAAA;AAAC,MAAA;AAAA,QAAA;AACW,QAAA;AACsB,MAAA;AAAA,sBAAA;AAEhC,QAAA;AAAC,QAAA;AAAA,UAAA;AACW,UAAA;AACL,UAAA;AACM,UAAA;AACQ,UAAA;AACuB,QAAA;AAAA,wBAAA;AAMxC,UAAA;AAAC,UAAA;AAAA,YAAA;AACM,YAAA;AACK,YAAA;AACsB,YAAA;AACb,UAAA;AAAA,UAAA;AACpB,QAAA;AAGH,wBAAA;AAEG,UAAA;AACQ,UAAA;AACA,UAAA;AACA,UAAA;AACA,QAAA;AASX,MAAA;AACF,IAAA;AACF,IAAA;AACS,EAAA;AASP,IAAA;AAAC,IAAA;AAAA,MAAA;AAGC,IAAA;AAAA,EAAA;AAQE,IAAA;AAAC,IAAA;AAAA,MAAA;AACM,MAAA;AACK,MAAA;AACD,MAAA;AACU,IAAA;AAAA,IAAA;AAE8B,EAAA;AAgBvD,IAAA;AAAC,IAAA;AAAA,MAAA;AAGC,IAAA;AAAA,EAAA;AAUE,IAAA;AAAC,IAAA;AAAA,MAAA;AACM,MAAA;AACK,MAAA;AACD,MAAA;AACU,IAAA;AAAA,IAAA;AAEX,EAAA;AAwCZ,IAAA;AAAC,IAAA;AAAA,MAAA;AACM,MAAA;AACK,MAAA;AACD,MAAA;AACE,IAAA;AAAA,IAAA;AAME,EAAA;AASP,IAAA;AAAC,IAAA;AAAA,MAAA;AACW,MAAA;AACoC,IAAA;AAAA,EAAA;AA4BtD,IAAA;AAAC,IAAA;AAAA,MAAA;AACM,MAAA;AACK,MAAA;AACD,IAAA;AAAA,IAAA;AAED,EAAA;AAQV,IAAA;AAAC,IAAA;AAAA,MAAA;AACM,MAAA;AACK,MAAA;AACD,IAAA;AAAA,IAAA;AAED,EAAA;AAe1B;AAEA;AACE,EAAA;AACE,IAAA;AAAC,IAAA;AAAA,MAAA;AACS,MAAA;AACF,MAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACK,MAAA;AACE,MAAA;AACC,MAAA;AACH,IAAA;AAAA,oBAAA;AAEwC,oBAAA;AACZ,oBAAA;AACpB,EAAA;AAG1B;AAEA;AACE,EAAA;AACE,IAAA;AAAC,IAAA;AAAA,MAAA;AACS,MAAA;AACF,MAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACK,MAAA;AACE,MAAA;AACC,MAAA;AACH,IAAA;AAAA,oBAAA;AAEmB,EAAA;AAGrC;AAEA;AACE,EAAA;AACE,IAAA;AAAC,IAAA;AAAA,MAAA;AACS,MAAA;AACF,MAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACK,MAAA;AACE,MAAA;AACC,MAAA;AACH,IAAA;AAAA,oBAAA;AAEU,oBAAA;AACW,EAAA;AAGvC;AAEA;AACE,EAAA;AACE,IAAA;AAAC,IAAA;AAAA,MAAA;AACS,MAAA;AACF,MAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACK,MAAA;AACE,MAAA;AACC,MAAA;AACH,IAAA;AAAA,oBAAA;AAEkB,oBAAA;AACiC,oBAAA;AACI,EAAA;AAGzE;AAMA;AACE,EAAA;AACE,IAAA;AAAC,IAAA;AAAA,MAAA;AACS,MAAA;AACF,MAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACK,MAAA;AACE,MAAA;AACC,MAAA;AACH,IAAA;AAAA,oBAAA;AAEoC,oBAAA;AACmF,EAAA;AAGzI;AAEA;AACE,EAAA;AACE,IAAA;AAAC,IAAA;AAAA,MAAA;AACS,MAAA;AACF,MAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACK,MAAA;AACE,MAAA;AACC,MAAA;AACH,IAAA;AAAA,oBAAA;AAEsE,oBAAA;AAC5D,oBAAA;AACyB,EAAA;AAGrD;AAEA;AACE,EAAA;AACE,IAAA;AAAC,IAAA;AAAA,MAAA;AACS,MAAA;AACF,MAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACK,MAAA;AACE,MAAA;AACC,MAAA;AACH,IAAA;AAAA,oBAAA;AAEqC,EAAA;AAGvD;AAEA;AACE,EAAA;AACE,IAAA;AAAC,IAAA;AAAA,MAAA;AACS,MAAA;AACF,MAAA;AACC,MAAA;AACF,MAAA;AACE,MAAA;AACK,MAAA;AACE,MAAA;AACC,MAAA;AACH,IAAA;AAAA,oBAAA;AAEuB,oBAAA;AACP,oBAAA;AACE,EAAA;AAGpC;AAEA;AAAuB,EAAA;AACA,EAAA;AACL,EAAA;AACC,EAAA;AAEnB;AAEA;AAAsB;AAAA,EAAA;AAEmB;AAAA,EAAA;AAEZ,IAAA;AACiB,IAAA;AACN,IAAA;AACE,IAAA;AACG,EAAA;AAC1C;AAAA,EAAA;AAEyB,IAAA;AACrB,IAAA;AAC4B,EAAA;AAChC;AAAA,EAAA;AAE2C;AAAA,EAAA;AAEO;AAAA,EAAA;AAEM;AAAA,EAAA;AAEzB,IAAA;AACd,MAAA;AACE,MAAA;AACI,MAAA;AACD,IAAA;AACpB,EAAA;AACH;AAAA,EAAA;AAEiB;AAAA,EAAA;AAEM;AAAA,EAAA;AAEC;AAAA,EAAA;AAEH;AAAA,EAAA;AAEH;AAAA,EAAA;AAEoB;AAAA,EAAA;AAEnB,EAAA;AACE,EAAA;AACE,EAAA;AACG,EAAA;AACL,EAAA;AACF;AAAA,EAAA;AAGrB;ADmrCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","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 { useFlashTool } from \"./useFlashTool.js\";\nimport { Dropdown } from \"./Dropdown.jsx\";\nimport { FIRMWARE_ALIASES, FIRMWARE_TYPES } from \"./flashToolConfig.js\";\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\n/**\n * Self-contained widget for connecting to and flashing a senseBox board\n * (MCU-S2 by default, also MCU Eye and MCU) over the Web Serial API. Renders its own\n * compact status panel - drop it anywhere in a React tree.\n *\n * All behaviour lives in the headless useFlashTool() hook; this component\n * only renders it. Use the hook directly to build a custom UI.\n */\nexport function FlashTool({ expand = false, className, ...options }) {\n const [logOpen, setLogOpen] = React.useState(expand);\n const [helpOpen, setHelpOpen] = React.useState(false);\n\n const {\n supported,\n connected,\n deviceLabel,\n status,\n progress,\n log,\n needsBootloaderPermission,\n disconnect,\n strings: merged,\n firmwareOptions: baseFirmwareOptions,\n selectedType,\n setSelectedType: handleFirmwareChange,\n firmwareLocked,\n showFirmwarePicker,\n firmwareDescription: activeFirmwareDescription,\n binary: resolvedBinary,\n firmwareLoading: builtinLoading,\n firmwareError: builtinError,\n retryFirmwareLoad: handleRetryFirmwareLoad,\n steps,\n flashComplete,\n latestLogLine,\n busy,\n connect: handleConnect,\n grant: handleGrant,\n startFlash: handleFlash,\n retry: handleRetry,\n startNew: handleStartNew,\n } = useFlashTool(options);\n\n const { step1Hint, step2HintPermission } = merged;\n const { step1: step1Status, step2: step2Status, step3: step3Status } = steps;\n\n const firmwareOptions = baseFirmwareOptions.map((option) => ({\n ...option,\n icon: FIRMWARE_ICONS[option.value],\n }));\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 logRef = useRef(null);\n useEffect(() => {\n if (!logOpen || !logRef.current) return;\n logRef.current.scrollTop = logRef.current.scrollHeight;\n }, [log, logOpen]);\n\n if (!supported) {\n return (\n <div className={`sbflash sbflash--unsupported ${className || \"\"}`}>\n {merged.unsupported}\n </div>\n );\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\">{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 {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 /** Target board: \"MCU-S2\" (default), \"MCU-EYE\" or \"MCU\" (case-insensitive). The bundled sketches exist for MCU-S2 only; other boards need `binary` or `firmwares`. */\n board: PropTypes.string,\n /** Deprecated: filters the device picker by vendor id only, as in 1.0. Ignored when `board` is set. */\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, useMemo, useRef, useState } from \"react\";\nimport { useFlashDevice } from \"./useFlashDevice.js\";\nimport { loadBuiltinFirmware } from \"../firmware/index.js\";\nimport {\n BUILTIN_FIRMWARE_FLASH_OVERRIDES,\n DEFAULT_FIRMWARE_DESCRIPTIONS,\n DEFAULT_FIRMWARE_DESCRIPTIONS_DE,\n FIRMWARE_TYPES,\n firmwareLabel,\n getLatestLogLine,\n getStepStatuses,\n getStrings,\n resolveFirmwareType,\n} from \"./flashToolConfig.js\";\n\n// Loaded builtin firmware bytes are cached across hook instances/re-renders\n// (keyed by type) so switching the sketch back and forth, or mounting more\n// than one flash UI, doesn't re-decode the same image.\nconst builtinFirmwareCache = new Map();\n\n/**\n * Headless version of <FlashTool />: all of its behaviour (sketch selection,\n * loading the bundled firmware images, per-sketch flash options, auto-flash,\n * the bootloader permission flow, retry, step states, callbacks) without any\n * markup. Render your own UI from the returned state and wire the returned\n * actions to your buttons.\n *\n * `connect()` and `grant()` open the browser's serial port picker and must be\n * called directly from a user gesture (e.g. a click handler) - Web Serial\n * rejects requestPort() without transient user activation.\n *\n * Takes the same options as <FlashTool />'s props (minus the purely visual\n * ones like `className` and `expand`).\n */\nexport function useFlashTool({\n binary,\n firmwares,\n firmware,\n firmwareType,\n defaultFirmwareType,\n firmwareFlashOptions,\n firmwareDescriptions,\n board,\n usbVendorId,\n flashOptions,\n keepConnection,\n autoFlash = true,\n language = \"en\",\n strings: stringOverrides,\n onConnected,\n onDisconnected,\n onFlashed,\n onError,\n onFirmwareTypeChange,\n} = {}) {\n const device = useFlashDevice({\n board,\n usbVendorId,\n flashOptions,\n keepConnection,\n });\n const {\n supported,\n profile,\n connected,\n bootloaderReady,\n deviceLabel,\n status,\n log,\n error,\n needsBootloaderPermission,\n selectDevice,\n grantBootloaderPort,\n flash,\n resetFlashState,\n } = device;\n\n const [busy, setBusy] = useState(null); // \"connect\" | \"grant\" | null\n\n const strings = getStrings(language, stringOverrides, profile);\n\n // The bundled firmware images are MCU-S2 images. Other boards only flash\n // what the caller passes via `binary` / `firmwares`.\n const builtinAvailable = Boolean(profile.builtinFirmware);\n\n // `firmware` is the friendlier alias-accepting option (e.g. \"circuitpython\"\n // resolves to \"circuitpy\"); `firmwareType` is the original, exact-key\n // option. Either one locks the selection to that sketch.\n const lockedFirmwareType = resolveFirmwareType(firmwareType || firmware);\n const firmwareLocked = Boolean(lockedFirmwareType);\n\n const [selectedType, setSelectedTypeState] = useState(\n lockedFirmwareType || defaultFirmwareType || FIRMWARE_TYPES[0],\n );\n\n useEffect(() => {\n if (lockedFirmwareType) setSelectedTypeState(lockedFirmwareType);\n }, [lockedFirmwareType]);\n\n // The sketch selection 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 (selection drives which bundled firmware\n // is loaded). With a single fixed `binary` and no locked `firmwareType`, a\n // picker would silently do nothing - so don't offer one.\n const showFirmwarePicker =\n Boolean(firmwares) || firmwareLocked || (!binary && builtinAvailable);\n\n // Either pick the binary out of a `{ circuitpy, ota, basic, standard }`\n // map keyed by the selection, or fall back to a single `binary`.\n const explicitBinary = firmwares ? firmwares[selectedType] : binary;\n\n const [builtinBinary, setBuiltinBinary] = useState(null);\n const [builtinLoading, setBuiltinLoading] = useState(false);\n const [builtinError, setBuiltinError] = useState(null);\n const [builtinRetryToken, setBuiltinRetryToken] = 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 available.\n useEffect(() => {\n if (!supported || explicitBinary || !builtinAvailable) 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 }, [\n supported,\n explicitBinary,\n builtinAvailable,\n selectedType,\n onError,\n builtinRetryToken,\n ]);\n\n // Gate on builtinAvailable too: after switching away from the MCU-S2 the\n // last loaded S2 image is still in state and must never reach another board.\n const resolvedBinary =\n explicitBinary || (builtinAvailable ? builtinBinary : null) || undefined;\n\n // The flash-address/erase override depends on which kind of image is\n // selected. Only applied when the selection is meaningful (see\n // showFirmwarePicker) - otherwise `selectedType` doesn't correspond to\n // anything real and its overrides must not hit an unrelated `binary`.\n const flashOverrides = useMemo(\n () =>\n showFirmwarePicker\n ? {\n ...(builtinAvailable &&\n BUILTIN_FIRMWARE_FLASH_OVERRIDES[selectedType]),\n ...firmwareFlashOptions?.[selectedType],\n }\n : {},\n [showFirmwarePicker, builtinAvailable, firmwareFlashOptions, selectedType],\n );\n\n const latestLogLine = useMemo(() => getLatestLogLine(log), [log]);\n\n const firmwareDescription = showFirmwarePicker\n ? {\n ...(builtinAvailable &&\n (language === \"de\"\n ? DEFAULT_FIRMWARE_DESCRIPTIONS_DE[selectedType]\n : DEFAULT_FIRMWARE_DESCRIPTIONS[selectedType])),\n ...firmwareDescriptions?.[selectedType],\n }\n : null;\n\n const firmwareOptions = FIRMWARE_TYPES.map((type) => ({\n value: type,\n label: firmwareLabel(type, strings),\n }));\n\n const setSelectedType = useCallback(\n (value) => {\n if (firmwareLocked) return;\n setSelectedTypeState(value);\n onFirmwareTypeChange?.(value);\n },\n [firmwareLocked, onFirmwareTypeChange],\n );\n\n const retryFirmwareLoad = useCallback(() => {\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 /** Opens the port picker. Call from a click handler. */\n const connect = useCallback(async () => {\n setBusy(\"connect\");\n try {\n return await selectDevice();\n } finally {\n setBusy(null);\n }\n }, [selectDevice]);\n\n /** Grants the bootloader port, then flashes. Call from a click handler. */\n const grant = useCallback(async () => {\n setBusy(\"grant\");\n try {\n const granted = await grantBootloaderPort();\n if (granted && resolvedBinary) await flash(resolvedBinary, flashOverrides);\n return granted;\n } finally {\n setBusy(null);\n }\n }, [grantBootloaderPort, resolvedBinary, flash, flashOverrides]);\n\n /** Flashes the currently selected firmware. */\n const startFlash = useCallback(async () => {\n if (resolvedBinary) await flash(resolvedBinary, flashOverrides);\n }, [resolvedBinary, flash, flashOverrides]);\n\n /** Clears the error and flashes again. */\n const retry = useCallback(async () => {\n resetFlashState();\n if (resolvedBinary) await flash(resolvedBinary, flashOverrides);\n }, [resetFlashState, resolvedBinary, flash, flashOverrides]);\n\n /** Back to the initial state after a finished upload. */\n const startNew = resetFlashState;\n\n return {\n ...device,\n // firmware\n firmwareTypes: FIRMWARE_TYPES,\n firmwareOptions,\n selectedType,\n setSelectedType,\n firmwareLocked,\n showFirmwarePicker,\n firmwareDescription,\n binary: resolvedBinary,\n firmwareLoading: builtinLoading,\n firmwareError: builtinError,\n retryFirmwareLoad,\n // derived\n steps: getStepStatuses({ status, connected, bootloaderReady }),\n flashComplete: status === \"done\",\n latestLogLine,\n busy,\n strings,\n // actions\n connect,\n grant,\n startFlash,\n retry,\n startNew,\n };\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { touchTo1200bps, waitForBootloaderPort } from \"../core/esptool.js\";\nimport {\n DEFAULT_BOARD,\n getBoardProfile,\n labelForPort,\n legacyVendorProfile,\n matchesPort,\n pickLivePort,\n portFilters,\n} from \"../core/boards.js\";\nimport { flashWithProfile } from \"../core/flash.js\";\n\nconst DEFAULT_FLASH_OPTIONS = {\n baudrate: 115200,\n eraseAll: false,\n usingUsbOtg: true,\n};\n\nfunction resolveProfile({ board, profile, usbVendorId }) {\n if (profile) return profile;\n // 1.0 compatibility: a custom vendor id without a board keeps the old\n // vendor-id-only port filtering.\n if (board === undefined && usbVendorId !== undefined) {\n return legacyVendorProfile(usbVendorId);\n }\n const resolved = getBoardProfile(board ?? DEFAULT_BOARD);\n if (!resolved) {\n throw new Error(\n `[flash-tool] Unknown board \"${board}\". Use \"MCU\", \"MCU-S2\" or \"MCU-EYE\".`,\n );\n }\n return resolved;\n}\n\n/**\n * Framework-agnostic-ish (but React) state machine for connecting to a\n * senseBox board over Web Serial, putting it into bootloader mode and\n * flashing a firmware binary supplied by the caller. ESP32 boards (MCU-S2,\n * MCU Eye) are flashed with esptool-js, the SAMD21 MCU with bossa-web.\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 {\"MCU\"|\"MCU-S2\"|\"MCU-EYE\"} [options.board] Target board. Defaults to \"MCU-S2\".\n * @param {object} [options.profile] Custom board profile (see BOARD_PROFILES); wins over `board`.\n * @param {number} [options.usbVendorId] Deprecated: vendor-id-only filtering as in 1.0. Ignored when `board` or `profile` is set.\n * @param {object} [options.flashOptions] Overrides for esptool flashBinary() (ESP boards only).\n * @param {boolean} [options.keepConnection] Keep the selected device after a\n * successful upload (default false: drop it, the UI goes back to \"not\n * connected\"). With true, the next flash() re-uses it - if the handle went\n * stale while the board reset, a live, already-permitted port of the same\n * board is picked up via getPorts() instead, so no picker is needed.\n */\nexport function useFlashDevice({\n board,\n profile: customProfile,\n usbVendorId,\n flashOptions = {},\n keepConnection = false,\n} = {}) {\n const profile = useMemo(\n () => resolveProfile({ board, profile: customProfile, usbVendorId }),\n [board, customProfile, usbVendorId],\n );\n\n // Starts false so the first client render matches the server-rendered\n // markup (navigator isn't defined during SSR); flips after mount once we\n // can safely check for Web Serial support without causing a hydration\n // mismatch.\n const [supported, setSupported] = useState(false);\n useEffect(() => {\n setSupported(\n typeof navigator !== \"undefined\" && navigator.serial !== undefined,\n );\n }, []);\n\n const [port, setPortState] = useState(null);\n const [bootloaderPort, setBootloaderPortState] = useState(null);\n // Mirrors of the port state. Actions read these instead of the closed-over\n // state, so e.g. flash() called right after grantBootloaderPort() in the\n // same handler already sees the freshly granted port.\n const portRef = useRef(null);\n const bootloaderPortRef = useRef(null);\n const setPort = useCallback((value) => {\n portRef.current = value;\n setPortState(value);\n }, []);\n const setBootloaderPort = useCallback((value) => {\n bootloaderPortRef.current = value;\n setBootloaderPortState(value);\n }, []);\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 = {\n ...DEFAULT_FLASH_OPTIONS,\n address: profile.address,\n flashMode: profile.flashMode,\n flashFreq: profile.flashFreq,\n flashSize: profile.flashSize,\n ...flashOptions,\n };\n\n const appendLog = useCallback((message) => {\n setLog((previous) => previous + message);\n }, []);\n\n // Switching boards drops the selected ports, so a port picked for one board\n // never receives another board's firmware.\n // Keyed by value, not object identity, so an inline `profile` object\n // doesn't count as a board switch on every render.\n const profileKey =\n profile.id ??\n JSON.stringify([portFilters(profile), portFilters(profile, true)]);\n const previousProfileKey = useRef(profileKey);\n useEffect(() => {\n if (previousProfileKey.current === profileKey) return;\n previousProfileKey.current = profileKey;\n setPort(null);\n setBootloaderPort(null);\n setNeedsBootloaderPermission(false);\n setDeviceLabel(\"\");\n setStatus(\"idle\");\n setProgress(0);\n setError(null);\n }, [profileKey, setPort, setBootloaderPort]);\n\n const selectDevice = useCallback(async () => {\n setError(null);\n try {\n const selectedPort = await navigator.serial.requestPort({\n filters: portFilters(profile),\n });\n setPort(selectedPort);\n setNeedsBootloaderPermission(false);\n const label = labelForPort(selectedPort, profile);\n setDeviceLabel(label);\n\n // The board may already be sitting in its bootloader (e.g. an ESP32-S2\n // enumerates as a generic \"ESP32-S2\" because no application firmware\n // is running, or an MCU after a reset double-press). In that case the\n // selected port *is* the bootloader port already - skip the 1200bps\n // touch/reset dance entirely instead of trying to \"prepare\" a board\n // that's already ready.\n if (matchesPort(selectedPort, profile, true)) {\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 }, [profile, appendLog, setPort, setBootloaderPort]);\n\n // Adopts a live port of this board the page already has permission for\n // (navigator.serial.getPorts()), without a picker - e.g. to restore a\n // connection after a page reload. Returns the port, or null if none is\n // plugged in / permitted.\n const reconnect = useCallback(async () => {\n setError(null);\n try {\n const livePort = pickLivePort(await navigator.serial.getPorts(), profile);\n if (!livePort) return null;\n setPort(livePort);\n setNeedsBootloaderPermission(false);\n setDeviceLabel(labelForPort(livePort, profile));\n setBootloaderPort(matchesPort(livePort, profile, true) ? livePort : null);\n return livePort;\n } catch (err) {\n setError(err?.message || String(err));\n return null;\n }\n }, [profile, setPort, setBootloaderPort]);\n\n const disconnect = useCallback(() => {\n setPort(null);\n setBootloaderPort(null);\n setNeedsBootloaderPermission(false);\n setDeviceLabel(\"\");\n setStatus(\"idle\");\n setError(null);\n }, [setPort, setBootloaderPort]);\n\n // Phase 1: 1200bps touch + look for an already-permitted bootloader port.\n const prepareBootloader = useCallback(\n async (explicitPort) => {\n const activePort = explicitPort || portRef.current;\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 (candidate) => matchesPort(candidate, profile, true),\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 [profile, appendLog, setBootloaderPort],\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: portFilters(profile, true),\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 }, [profile, appendLog, setBootloaderPort]);\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 = bootloaderPortRef.current;\n lockRef.current = true;\n setStatus(\"flashing\");\n setProgress(0);\n setLog(\"\");\n setError(null);\n\n try {\n if (!flashPort) {\n let activePort = portRef.current;\n if (!activePort) {\n throw new Error(\"No device selected.\");\n }\n // The board reset since the port was picked (previous upload with\n // keepConnection, replugged cable) - its handle is stale. Use the\n // live, already-permitted port of the same board instead.\n if (activePort.connected === false) {\n const livePort = pickLivePort(\n await navigator.serial.getPorts(),\n profile,\n );\n if (!livePort) {\n throw new Error(\n \"The device is no longer connected. Plug it in and connect it again.\",\n );\n }\n activePort = livePort;\n setPort(livePort);\n }\n if (matchesPort(activePort, profile, true)) {\n // Already sitting in its bootloader - no reset needed.\n flashPort = activePort;\n setBootloaderPort(activePort);\n }\n }\n if (!flashPort) {\n lockRef.current = false; // prepareBootloader takes its own lock\n flashPort = await prepareBootloader(portRef.current);\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 flashWithProfile({\n profile,\n port: flashPort,\n binary,\n options: activeFlashOptions,\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 the bootloader handle is\n // stale. Without keepConnection drop the device too, so the UI\n // returns to its initial \"not connected\" state, ready for the next\n // board; with it, the next flash() re-resolves a live port.\n setBootloaderPort(null);\n setNeedsBootloaderPermission(false);\n if (!keepConnection) {\n setPort(null);\n setDeviceLabel(\"\");\n }\n } catch (err) {\n setError(err?.message || String(err));\n setStatus(\"error\");\n } finally {\n lockRef.current = false;\n }\n },\n [\n prepareBootloader,\n appendLog,\n mergedFlashOptions,\n profile,\n keepConnection,\n setPort,\n setBootloaderPort,\n ],\n );\n\n const resetFlashState = useCallback(() => {\n setStatus(\"idle\");\n setProgress(0);\n setLog(\"\");\n setError(null);\n setNeedsBootloaderPermission(false);\n }, []);\n\n return {\n supported,\n profile,\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 reconnect,\n disconnect,\n prepareBootloader,\n grantBootloaderPort,\n flash,\n resetFlashState,\n };\n}\n","const 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 * Leaves USB download mode on an ESP32-S3 (senseBox MCU Eye) and boots the\n * flashed application. Register addresses and the watchdog sequence follow\n * Espressif's ESP32S3ROM hard_reset/watchdog_reset in\n * esptool/targets/esp32s3.py. A watchdog reset also works when the native USB\n * connection cannot pulse the physical EN pin.\n *\n * @param {ESPLoader} loader A connected esptool-js loader.\n */\nexport async function resetEsp32S3(loader) {\n if (loader.chip.CHIP_NAME !== \"ESP32-S3\") {\n throw new Error(\"ESP32-S3 reset requested for a different chip.\");\n }\n // Clear only FORCE_DOWNLOAD_BOOT, preserving the other RTC option bits.\n await loader.writeReg(0x6000812c, 0, 1);\n await loader.writeReg(0x600080b0, 0x50d83aa1); // unlock RTC watchdog\n await loader.writeReg(0x6000809c, 2000); // allow commands to finish before reset\n await loader.writeReg(0x60008098, 0xd0000102); // enable RTC system reset\n await loader.writeReg(0x600080b0, 0); // lock watchdog configuration\n await sleep(500);\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 * `matcher` is either a USB vendor id (1.0 behaviour: the ESP32 ROM\n * bootloader PID 0x0002 of that vendor, or any newly appearing port) or a\n * function `(port) => boolean`. With a function only matching ports are\n * accepted, so unrelated devices plugged in meanwhile are ignored - e.g.\n * `(port) => matchesPort(port, profile, true)`.\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 | ((port: SerialPort) => boolean)} [matcher]\n * @returns {Promise<SerialPort|null>}\n */\nexport function waitForBootloaderPort(\n knownPorts,\n timeoutMs = 8000,\n matcher = DEFAULT_USB_VENDOR_ID,\n) {\n const strict = typeof matcher === \"function\";\n const isBootloader = strict\n ? matcher\n : (port) => isRomBootloaderPort(port, matcher);\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 (isBootloader(candidate) || (!strict && !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 isBootloader(candidate) && candidate.connected === true,\n );\n if (bootloader) {\n finish(bootloader);\n return;\n }\n const fresh = ports.find(\n (candidate) =>\n (!strict || isBootloader(candidate)) &&\n !known.has(candidate) &&\n candidate.connected !== false,\n );\n if (fresh) finish(fresh);\n }, 400);\n\n const timerId = setTimeout(() => finish(null), timeoutMs);\n });\n}\n\n/**\n * Reboots the chip into the freshly flashed application. Exported for tests.\n *\n * - ESP32-S3 (MCU Eye): RTC watchdog reset (resetEsp32S3).\n * - ESP32-S2 (MCU-S2): EN pulse via RTS, then the S2 RTC software reset as a\n * fallback for boards without an RTS->EN circuit.\n * - Other chips over USB-OTG: EN pulse, then esptool-js' USB-OTG hard reset.\n * The S2 register is never written to a non-S2 chip.\n *\n * @param {ESPLoader} loader\n * @param {Transport} transport\n * @param {{ usingUsbOtg?: boolean, onLog?: (msg: string) => void }} [options]\n */\nexport async function resetAfterFlash(\n loader,\n transport,\n { usingUsbOtg = false, onLog } = {},\n) {\n if (usingUsbOtg && loader.chip.CHIP_NAME === \"ESP32-S3\") {\n onLog?.(\"Restarting into the uploaded program...\\n\");\n await resetEsp32S3(loader);\n } else 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 // The RTC register address is ESP32-S2 specific; other chips get\n // esptool-js' own USB-OTG reset instead.\n try {\n if (loader.chip.CHIP_NAME === \"ESP32-S2\") {\n await softResetViaRegister(loader);\n } else {\n await loader.after(\"hard_reset\", true);\n }\n } catch {\n // Ignore - the chip is already rebooting.\n }\n } else {\n await loader.after(\"hard_reset\");\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 {string} [params.expectedChip] Abort before writing if the chip is\n * not this one (esptool-js CHIP_NAME, e.g. \"ESP32-S2\", \"ESP32-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 expectedChip,\n onLog,\n onProgress,\n}) {\n // Loaded on demand: keeps esptool-js out of SAMD-only flashes and lets\n // this module load in Node (esptool-js' ESM build isn't Node-resolvable).\n const { ESPLoader, Transport } = await import(\"esptool-js\");\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 if (expectedChip && loader.chip.CHIP_NAME !== expectedChip) {\n throw new Error(\n `Expected ${expectedChip}, but connected to ${loader.chip.CHIP_NAME}. Check the selected board.`,\n );\n }\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 await resetAfterFlash(loader, transport, { usingUsbOtg, onLog });\n return chip;\n } finally {\n try {\n await transport.disconnect();\n } catch {\n // Ignore errors while releasing the port.\n }\n }\n}\n","// USB identities and flash settings for the supported senseBox boards.\n//\n// Note: the ESP32-S2 and ESP32-S3 ROM bootloaders both enumerate as\n// 303a:0002, so the bootloader port alone cannot tell an MCU-S2 from an\n// MCU Eye. The selected profile decides which board is expected, and\n// `chip` is checked by flashBinary() after connecting (expectedChip).\n\nconst ESPRESSIF_VENDOR_ID = 0x303a;\n\nconst esp = {\n vendorId: ESPRESSIF_VENDOR_ID,\n bootloaderProductId: 0x0002,\n address: 0x10000,\n flashMode: \"dio\",\n flashFreq: \"80m\",\n backend: \"esp\",\n};\n\n/**\n * Keys are upper-case board ids. `deviceName` / `bootloaderName` are the\n * names the browser's port picker shows for the application and bootloader\n * port, so they can be quoted in UI hints. `builtinFirmware` marks boards the\n * bundled firmware images in src/firmware/ are built for.\n */\nexport const BOARD_PROFILES = {\n MCU: {\n id: \"MCU\",\n compilerBoard: \"sensebox-mcu\",\n deviceName: \"senseBox MCU\",\n bootloaderName: \"senseBox MCU\",\n label: \"senseBox MCU (SAMD21)\",\n bootloaderLabel: \"senseBox MCU (SAMD21 bootloader)\",\n vendorId: 0x04d8,\n productIds: [0xef66, 0xef67],\n bootloaderProductId: 0xef66,\n // The SAM-BA bootloader occupies the first 8 KiB; fixed in samd.js.\n address: 0x2000,\n backend: \"samd\",\n builtinFirmware: false,\n },\n \"MCU-S2\": {\n ...esp,\n id: \"MCU-S2\",\n compilerBoard: \"sensebox-esp32s2\",\n deviceName: \"senseBox MCU-S2 ESP32-S2\",\n bootloaderName: \"ESP32-S2\",\n label: \"senseBox MCU-S2 (ESP32-S2)\",\n bootloaderLabel: \"senseBox MCU-S2 (ESP32-S2 bootloader)\",\n productIds: [0x81b8, 0x81b9, 0x81ba, 0x0002],\n chip: \"ESP32-S2\",\n flashSize: \"4MB\",\n builtinFirmware: true,\n },\n \"MCU-EYE\": {\n ...esp,\n id: \"MCU-EYE\",\n compilerBoard: \"sensebox_mcu_eye\",\n deviceName: \"senseBox MCU Eye\",\n bootloaderName: \"ESP32-S3\",\n label: \"senseBox MCU Eye (ESP32-S3)\",\n bootloaderLabel: \"senseBox MCU Eye (ESP32-S3 bootloader)\",\n productIds: [0x82d1, 0x82d2, 0x82d3, 0x0002, 0x1001],\n bootloaderProductIds: [0x0002, 0x1001],\n chip: \"ESP32-S3\",\n flashSize: \"16MB\",\n builtinFirmware: false,\n },\n};\n\nexport const DEFAULT_BOARD = \"MCU-S2\";\n\n/**\n * Looks up a board profile by id, case-insensitively (\"MCU-Eye\" works).\n * @param {string} board\n * @returns {object|null}\n */\nexport const getBoardProfile = (board) =>\n (typeof board === \"string\" && BOARD_PROFILES[board.toUpperCase()]) || null;\n\nconst bootloaderProductIds = (profile) =>\n profile.bootloaderProductIds ?? [profile.bootloaderProductId];\n\n/**\n * Web Serial `requestPort()` filters for a profile's application ports, or\n * its bootloader ports when `bootloader` is true. A profile without\n * `productIds` filters application ports by vendor id only.\n */\nexport function portFilters(profile, bootloader = false) {\n const productIds = bootloader\n ? bootloaderProductIds(profile)\n : profile.productIds;\n if (!productIds) return [{ usbVendorId: profile.vendorId }];\n return productIds.map((usbProductId) => ({\n usbVendorId: profile.vendorId,\n usbProductId,\n }));\n}\n\n/**\n * Whether a port belongs to the profile (or its bootloader).\n * @param {SerialPort} port\n * @param {object} profile\n * @param {boolean} [bootloader]\n */\nexport function matchesPort(port, profile, bootloader = false) {\n let info;\n try {\n info = port.getInfo?.() ?? {};\n } catch {\n return false;\n }\n return portFilters(profile, bootloader).some(\n (filter) =>\n filter.usbVendorId === info.usbVendorId &&\n (filter.usbProductId === undefined ||\n filter.usbProductId === info.usbProductId),\n );\n}\n\n/**\n * Picks a currently connected port of the profile out of `ports` (usually\n * `navigator.serial.getPorts()`, i.e. ports the page already has permission\n * for). After a reset the board re-enumerates and an earlier port handle can\n * go stale (`connected === false`); this finds the live one without a picker.\n * Ports whose `connected` flag is unknown (older browsers) count as live.\n *\n * @param {SerialPort[]} ports\n * @param {object} profile\n * @returns {SerialPort|null}\n */\nexport function pickLivePort(ports, profile) {\n const candidates = (ports ?? []).filter((candidate) =>\n matchesPort(candidate, profile),\n );\n return (\n candidates.find((candidate) => candidate.connected === true) ??\n candidates.find((candidate) => candidate.connected === undefined) ??\n null\n );\n}\n\n/**\n * Profile emulating flash-tool 1.0 for callers that only pass a custom\n * `usbVendorId`: picker filtered by vendor id only, ROM bootloader on PID\n * 0x0002, MCU-S2 flash settings, no chip check.\n * @param {number} vendorId\n */\nexport function legacyVendorProfile(vendorId) {\n return {\n ...BOARD_PROFILES[\"MCU-S2\"],\n id: `vendor:${vendorId}`,\n vendorId,\n productIds: undefined,\n bootloaderProductIds: [0x0002],\n chip: undefined,\n };\n}\n\n/**\n * Human-readable port label for logs/UI.\n * @param {SerialPort} port\n * @param {object} [profile]\n */\nexport function labelForPort(port, profile) {\n const info = port.getInfo?.() ?? {};\n if (profile?.productIds) {\n if (matchesPort(port, profile, true)) return profile.bootloaderLabel;\n if (matchesPort(port, profile)) return profile.label;\n }\n const hex = (value) =>\n value !== undefined ? value.toString(16).padStart(4, \"0\") : \"????\";\n return `USB device (VID 0x${hex(info.usbVendorId)} / PID 0x${hex(info.usbProductId)})`;\n}\n","// bossa-web 0.9.7 terminates binary replies at NUL and doesn't await its read\n// loop on disconnect. Keep exact-length replies intact and release the reader\n// before closing the Web Serial port (including when a reset disconnects USB).\nexport function createSamBATransport(SamBA) {\n return class extends SamBA {\n async connect() {\n // Native USB CDC does not use CTS/RTS hardware flow control. In\n // particular, don't inherit bossa-web's flowControl: \"hardware\".\n await this.serialPort.open({\n baudRate: 115200,\n dataBits: 8,\n stopBits: 1,\n parity: \"none\",\n flowControl: \"none\",\n bufferSize: 4096,\n });\n await this.serialPort.setSignals({\n dataTerminalReady: true,\n requestToSend: true,\n });\n await new Promise((resolve) => setTimeout(resolve, 100));\n await this._connect();\n }\n\n async setBinaryMode() {\n // BOSSA's Samba::init() drains the optional N# acknowledgement without\n // requiring two bytes. A valid subsequent V# reply establishes that the\n // bootloader is responding; memory reads must still have exact lengths.\n await this.sendCommand(\"N\", 0);\n await this.readBuffer(100);\n }\n\n readLoop() {\n this.activeReadLoop = super.readLoop();\n return this.activeReadLoop;\n }\n\n async disconnect() {\n this.closed = true;\n try {\n await this.serialReader?.cancel();\n } finally {\n await this.activeReadLoop;\n }\n }\n\n async sendCommand(...args) {\n try {\n return await super.sendCommand(...args);\n } catch (error) {\n throw new Error(`SAM-BA command ${args[0]}: ${error.message ?? error}`);\n }\n }\n\n async readVersion() {\n // In BOSSA, 256 is a maximum text buffer size, not an exact wire length.\n // Read the version through its line/NUL terminator, while retaining exact\n // lengths for memory reads, where zero and newline bytes are valid data.\n await this.sendCommand(\"V\", 0);\n const reply = [];\n const deadline = Date.now() + 3000;\n while (Date.now() < deadline && reply.length < 256) {\n if (this.inputBuffer.length) {\n const byte = this.inputBuffer.shift();\n if (byte === 0 || byte === 10 || byte === 13) {\n if (reply.length)\n return new TextDecoder().decode(Uint8Array.from(reply));\n } else {\n reply.push(byte);\n }\n } else {\n await new Promise((resolve) => setTimeout(resolve, 10));\n }\n }\n throw new Error(\n \"SAM-BA handshake failed: no complete version reply to V#. \" +\n \"Double-press the MCU reset button, reconnect its bootloader port, and retry.\",\n );\n }\n\n async read(address, buffer, size) {\n // SAMD bootloaders require USB reads below 64 bytes. The dependency's\n // chunked read advances the destination twice and leaves gaps in readback.\n for (let offset = 0; offset < size;) {\n const length = Math.min(63, size - offset);\n const data = await this.sendCommand(\n `R${this.hex(address + offset)},${this.hex(length)}`,\n length,\n );\n buffer.set(data, offset);\n offset += length;\n }\n }\n\n async readBuffer(timeout = 3000, responseSize) {\n const reply = [];\n const deadline = Date.now() + timeout;\n while (Date.now() < deadline) {\n if (this.inputBuffer.length) {\n const byte = this.inputBuffer.shift();\n reply.push(byte);\n if (responseSize ? reply.length === responseSize : byte === 0) {\n return Uint8Array.from(reply);\n }\n } else {\n await new Promise((resolve) => setTimeout(resolve, 10));\n }\n }\n if (responseSize) {\n throw new Error(\n `Incomplete SAM-BA response: expected ${responseSize} bytes, received ${reply.length}.`,\n );\n }\n return reply.length ? Uint8Array.from(reply) : null;\n }\n };\n}\n","import { createSamBATransport } from \"./sambaTransport.js\";\n\n// The MCU linker script reserves the first 8 KiB for its SAM-BA bootloader.\nconst APPLICATION_OFFSET = 0x2000;\n\n/**\n * Flashes an application image to a senseBox MCU (SAMD21) through its SAM-BA\n * bootloader, using bossa-web. bossa-web is an optional dependency and only\n * loaded when this runs.\n *\n * @param {object} params\n * @param {SerialPort} params.port The SAM-BA bootloader port.\n * @param {Uint8Array} params.firmware Application image (written at 0x2000).\n * @param {(msg: string) => void} [params.onLog]\n * @param {(percent: number) => void} [params.onProgress]\n */\nexport async function flashSamdBinary({ port, firmware, onLog, onProgress }) {\n const { SamBA, Device, Family, Flasher } = await import(\"bossa-web\");\n const logger = {\n debug: () => {},\n log: (...args) => onLog?.(`${args.join(\" \")}\\n`),\n error: (...args) => onLog?.(`${args.join(\" \")}\\n`),\n };\n const Transport = createSamBATransport(SamBA);\n const samba = new Transport(port, { logger });\n try {\n await samba.connect();\n const device = new Device(samba);\n await device.create();\n const flash = device.flash;\n if (device.family !== Family.FAMILY_SAMD21 || !flash) {\n throw new Error(\"The connected device is not a SAMD21 senseBox MCU.\");\n }\n if (\n !firmware.length ||\n firmware.length > flash.totalSize - APPLICATION_OFFSET\n ) {\n throw new Error(\"Firmware does not fit in the MCU application flash.\");\n }\n const flasher = new Flasher(samba, flash, {\n onStatus: (message) => onLog?.(message),\n onProgress: (current, total) => {\n if (total) onProgress?.(Math.round((current / total) * 100));\n },\n });\n await flasher.erase(APPLICATION_OFFSET);\n await flasher.write(firmware, APPLICATION_OFFSET);\n // bossa-web's verify() is unimplemented, so compare flash readback directly.\n onLog?.(\"Verify firmware\\n\");\n for (let offset = 0; offset < firmware.length; offset += 256) {\n const expected = firmware.subarray(offset, offset + 256);\n const actual = new Uint8Array(expected.length);\n await samba.read(APPLICATION_OFFSET + offset, actual, actual.length);\n if (actual.some((byte, index) => byte !== expected[index])) {\n throw new Error(`Firmware verification failed at offset ${offset}.`);\n }\n }\n await device.reset();\n } finally {\n try {\n await samba.disconnect();\n } catch {\n // Reset can already have disconnected the device.\n }\n try {\n await port.close();\n } catch {\n // Release partially opened ports as well as successful connections.\n }\n }\n}\n","import { flashBinary } from \"./esptool.js\";\nimport { flashSamdBinary } from \"./samd.js\";\n\n/**\n * Flashes `binary` to `port` using the backend the board profile needs:\n * bossa-web for the SAMD21 MCU, esptool-js (with a chip check) otherwise.\n *\n * @param {object} params\n * @param {object} params.profile Board profile (see boards.js).\n * @param {SerialPort} params.port Bootloader port.\n * @param {Uint8Array} params.binary Firmware image.\n * @param {object} [params.options] Merged esptool options (ESP boards only).\n * @param {(msg: string) => void} [params.onLog]\n * @param {(percent: number) => void} [params.onProgress]\n * @param {object} [deps] Injected flash functions (for tests).\n */\nexport function flashWithProfile(\n { profile, port, binary, options = {}, onLog, onProgress },\n deps = { flashBinary, flashSamdBinary },\n) {\n if (profile.backend === \"samd\") {\n return deps.flashSamdBinary({ port, firmware: binary, onLog, onProgress });\n }\n return deps.flashBinary({\n port,\n fileArray: [{ data: binary, address: options.address ?? profile.address }],\n baudrate: options.baudrate,\n flashMode: options.flashMode,\n flashFreq: options.flashFreq,\n flashSize: options.flashSize,\n eraseAll: options.eraseAll,\n usingUsbOtg: options.usingUsbOtg,\n expectedChip: profile.chip,\n onLog,\n onProgress,\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","// Shared constants and pure helpers behind <FlashTool /> and the headless\n// useFlashTool() hook. No React in here, so everything is unit-testable and\n// reusable by custom UIs (labels, descriptions, step logic).\n\nexport const 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\".\nexport const FIRMWARE_ALIASES = {\n circuitpython: \"circuitpy\",\n};\n\nexport function 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. Only applied on\n// boards the bundled images are built for (MCU-S2) - for the MCU (SAMD21)\n// the application offset is fixed at 0x2000 anyway.\nexport const 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`.\nexport const 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\".\nexport const 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\nexport const 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 step1HintBoard:\n \"Plug in your senseBox, then select “%1” in 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 step2HintPermissionBoard:\n \"The board now shows up as a new USB device. Select “%1” once to grant permission.\",\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.\nexport const 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 step1HintBoard:\n \"Schließe deine senseBox an und wähle in der Geräteliste des Browsers „%1“ 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 step2HintPermissionBoard:\n \"Das Board meldet sich jetzt als neues USB-Gerät. Wähle einmalig „%1“ aus, um die Berechtigung zu erteilen.\",\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 * Merges the localized default strings with caller overrides and fills in\n * the board-specific hints (names the browser's port picker shows). A\n * caller-supplied `strings.step1Hint` / `step2HintPermission` always wins.\n *\n * @param {\"en\"|\"de\"} [language]\n * @param {Record<string,string>} [strings]\n * @param {{deviceName?: string, bootloaderName?: string}} [profile]\n */\nexport function getStrings(language, strings, profile = {}) {\n const merged = {\n ...(language === \"de\" ? DEFAULT_STRINGS_DE : DEFAULT_STRINGS),\n ...strings,\n };\n const withName = (key, name) =>\n strings?.[key] ??\n (name ? merged[`${key}Board`].replace(\"%1\", name) : merged[key]);\n return {\n ...merged,\n step1Hint: withName(\"step1Hint\", profile.deviceName),\n step2HintPermission: withName(\n \"step2HintPermission\",\n profile.bootloaderName,\n ),\n };\n}\n\n/** Display label for a firmware type, e.g. \"circuitpy\" -> strings.firmwareCircuitpy. */\nexport function firmwareLabel(type, strings) {\n return strings[`firmware${type.charAt(0).toUpperCase()}${type.slice(1)}`];\n}\n\n/**\n * Last non-empty log line, without esptool-js's \"(NN%)\" progress suffix\n * (the same percentage is already exposed as `progress`). Useful to show\n * that a long full-chip erase is still working while progress sits at 0%.\n *\n * @param {string} log\n */\nexport function getLatestLogLine(log) {\n if (!log) return \"\";\n const lines = log.split(\"\\n\").filter((line) => line.trim().length > 0);\n const lastLine = lines[lines.length - 1] || \"\";\n return lastLine.replace(/\\s*\\(\\d+ ?%\\)\\s*$/, \"\");\n}\n\n/**\n * Status of the 3 user-facing steps (connect, bootloader, upload). Exactly\n * one step is \"active\" at a time, so a UI always knows which action to show.\n *\n * A successful flash resets `connected`/`bootloaderReady` (see\n * useFlashDevice) so the hook is ready for the next board - but all 3 steps\n * stay \"done\" until the user explicitly starts a new upload.\n *\n * @param {{status: string, connected: boolean, bootloaderReady: boolean}} state\n * @returns {{step1: StepStatus, step2: StepStatus, step3: StepStatus}}\n * @typedef {\"done\"|\"active\"|\"pending\"|\"error\"} StepStatus\n */\nexport function getStepStatuses({ status, connected, bootloaderReady }) {\n if (status === \"done\") {\n return { step1: \"done\", step2: \"done\", step3: \"done\" };\n }\n return {\n step1: connected ? \"done\" : \"active\",\n step2: !connected ? \"pending\" : bootloaderReady ? \"done\" : \"active\",\n step3: !bootloaderReady\n ? \"pending\"\n : status === \"error\"\n ? \"error\"\n : \"active\",\n };\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"]}
|
|
1
|
+
{"version":3,"sources":["/Users/eric/Documents/arbeit/sensebox-flash-tool/dist/index.cjs","../src/react/FlashTool.jsx","../src/react/useFlashTool.js","../src/react/useFlashDevice.js","../src/core/md5.js","../src/core/esptool.js","../src/core/boards.js","../src/core/sambaTransport.js","../src/core/samd.js","../src/core/flash.js","../src/firmware/index.js","../src/react/flashToolConfig.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 { useFlashTool } from \"./useFlashTool.js\";\nimport { Dropdown } from \"./Dropdown.jsx\";\nimport { FIRMWARE_ALIASES, FIRMWARE_TYPES } from \"./flashToolConfig.js\";\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\n/**\n * Self-contained widget for connecting to and flashing a senseBox board\n * (MCU-S2 by default, also MCU Eye and MCU) over the Web Serial API. Renders its own\n * compact status panel - drop it anywhere in a React tree.\n *\n * All behaviour lives in the headless useFlashTool() hook; this component\n * only renders it. Use the hook directly to build a custom UI.\n */\nexport function FlashTool({ expand = false, className, ...options }) {\n const [logOpen, setLogOpen] = React.useState(expand);\n const [helpOpen, setHelpOpen] = React.useState(false);\n\n const {\n supported,\n connected,\n deviceLabel,\n status,\n progress,\n log,\n needsBootloaderPermission,\n disconnect,\n strings: merged,\n firmwareOptions: baseFirmwareOptions,\n selectedType,\n setSelectedType: handleFirmwareChange,\n firmwareLocked,\n showFirmwarePicker,\n firmwareDescription: activeFirmwareDescription,\n binary: resolvedBinary,\n firmwareLoading: builtinLoading,\n firmwareError: builtinError,\n retryFirmwareLoad: handleRetryFirmwareLoad,\n steps,\n flashComplete,\n latestLogLine,\n busy,\n connect: handleConnect,\n grant: handleGrant,\n startFlash: handleFlash,\n retry: handleRetry,\n startNew: handleStartNew,\n } = useFlashTool(options);\n\n const { step1Hint, step2HintPermission } = merged;\n const { step1: step1Status, step2: step2Status, step3: step3Status } = steps;\n\n const firmwareOptions = baseFirmwareOptions.map((option) => ({\n ...option,\n icon: FIRMWARE_ICONS[option.value],\n }));\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 logRef = useRef(null);\n useEffect(() => {\n if (!logOpen || !logRef.current) return;\n logRef.current.scrollTop = logRef.current.scrollHeight;\n }, [log, logOpen]);\n\n if (!supported) {\n return (\n <div className={`sbflash sbflash--unsupported ${className || \"\"}`}>\n {merged.unsupported}\n </div>\n );\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\">{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 {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 /** Target board: \"MCU-S2\" (default), \"MCU-EYE\" or \"MCU\" (case-insensitive). The bundled sketches exist for MCU-S2 only; other boards need `binary` or `firmwares`. */\n board: PropTypes.string,\n /** Deprecated: filters the device picker by vendor id only, as in 1.0. Ignored when `board` is set. */\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, useMemo, useRef, useState } from \"react\";\nimport { useFlashDevice } from \"./useFlashDevice.js\";\nimport { loadBuiltinFirmware } from \"../firmware/index.js\";\nimport {\n BUILTIN_FIRMWARE_FLASH_OVERRIDES,\n DEFAULT_FIRMWARE_DESCRIPTIONS,\n DEFAULT_FIRMWARE_DESCRIPTIONS_DE,\n FIRMWARE_TYPES,\n firmwareLabel,\n getLatestLogLine,\n getStepStatuses,\n getStrings,\n resolveFirmwareType,\n} from \"./flashToolConfig.js\";\n\n// Loaded builtin firmware bytes are cached across hook instances/re-renders\n// (keyed by type) so switching the sketch back and forth, or mounting more\n// than one flash UI, doesn't re-decode the same image.\nconst builtinFirmwareCache = new Map();\n\n/**\n * Headless version of <FlashTool />: all of its behaviour (sketch selection,\n * loading the bundled firmware images, per-sketch flash options, auto-flash,\n * the bootloader permission flow, retry, step states, callbacks) without any\n * markup. Render your own UI from the returned state and wire the returned\n * actions to your buttons.\n *\n * `connect()` and `grant()` open the browser's serial port picker and must be\n * called directly from a user gesture (e.g. a click handler) - Web Serial\n * rejects requestPort() without transient user activation.\n *\n * Takes the same options as <FlashTool />'s props (minus the purely visual\n * ones like `className` and `expand`).\n */\nexport function useFlashTool({\n binary,\n firmwares,\n firmware,\n firmwareType,\n defaultFirmwareType,\n firmwareFlashOptions,\n firmwareDescriptions,\n board,\n usbVendorId,\n flashOptions,\n keepConnection,\n autoFlash = true,\n language = \"en\",\n strings: stringOverrides,\n onConnected,\n onDisconnected,\n onFlashed,\n onError,\n onFirmwareTypeChange,\n} = {}) {\n const device = useFlashDevice({\n board,\n usbVendorId,\n flashOptions,\n keepConnection,\n });\n const {\n supported,\n profile,\n connected,\n bootloaderReady,\n deviceLabel,\n status,\n log,\n error,\n needsBootloaderPermission,\n selectDevice,\n grantBootloaderPort,\n flash,\n resetFlashState,\n } = device;\n\n const [busy, setBusy] = useState(null); // \"connect\" | \"grant\" | null\n\n const strings = getStrings(language, stringOverrides, profile);\n\n // The bundled firmware images are MCU-S2 images. Other boards only flash\n // what the caller passes via `binary` / `firmwares`.\n const builtinAvailable = Boolean(profile.builtinFirmware);\n\n // `firmware` is the friendlier alias-accepting option (e.g. \"circuitpython\"\n // resolves to \"circuitpy\"); `firmwareType` is the original, exact-key\n // option. Either one locks the selection to that sketch.\n const lockedFirmwareType = resolveFirmwareType(firmwareType || firmware);\n const firmwareLocked = Boolean(lockedFirmwareType);\n\n const [selectedType, setSelectedTypeState] = useState(\n lockedFirmwareType || defaultFirmwareType || FIRMWARE_TYPES[0],\n );\n\n useEffect(() => {\n if (lockedFirmwareType) setSelectedTypeState(lockedFirmwareType);\n }, [lockedFirmwareType]);\n\n // The sketch selection 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 (selection drives which bundled firmware\n // is loaded). With a single fixed `binary` and no locked `firmwareType`, a\n // picker would silently do nothing - so don't offer one.\n const showFirmwarePicker =\n Boolean(firmwares) || firmwareLocked || (!binary && builtinAvailable);\n\n // Either pick the binary out of a `{ circuitpy, ota, basic, standard }`\n // map keyed by the selection, or fall back to a single `binary`.\n const explicitBinary = firmwares ? firmwares[selectedType] : binary;\n\n const [builtinBinary, setBuiltinBinary] = useState(null);\n const [builtinLoading, setBuiltinLoading] = useState(false);\n const [builtinError, setBuiltinError] = useState(null);\n const [builtinRetryToken, setBuiltinRetryToken] = 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 available.\n useEffect(() => {\n if (!supported || explicitBinary || !builtinAvailable) 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 }, [\n supported,\n explicitBinary,\n builtinAvailable,\n selectedType,\n onError,\n builtinRetryToken,\n ]);\n\n // Gate on builtinAvailable too: after switching away from the MCU-S2 the\n // last loaded S2 image is still in state and must never reach another board.\n const resolvedBinary =\n explicitBinary || (builtinAvailable ? builtinBinary : null) || undefined;\n\n // The flash-address/erase override depends on which kind of image is\n // selected. Only applied when the selection is meaningful (see\n // showFirmwarePicker) - otherwise `selectedType` doesn't correspond to\n // anything real and its overrides must not hit an unrelated `binary`.\n const flashOverrides = useMemo(\n () =>\n showFirmwarePicker\n ? {\n ...(builtinAvailable &&\n BUILTIN_FIRMWARE_FLASH_OVERRIDES[selectedType]),\n ...firmwareFlashOptions?.[selectedType],\n }\n : {},\n [showFirmwarePicker, builtinAvailable, firmwareFlashOptions, selectedType],\n );\n\n const latestLogLine = useMemo(() => getLatestLogLine(log), [log]);\n\n const firmwareDescription = showFirmwarePicker\n ? {\n ...(builtinAvailable &&\n (language === \"de\"\n ? DEFAULT_FIRMWARE_DESCRIPTIONS_DE[selectedType]\n : DEFAULT_FIRMWARE_DESCRIPTIONS[selectedType])),\n ...firmwareDescriptions?.[selectedType],\n }\n : null;\n\n const firmwareOptions = FIRMWARE_TYPES.map((type) => ({\n value: type,\n label: firmwareLabel(type, strings),\n }));\n\n const setSelectedType = useCallback(\n (value) => {\n if (firmwareLocked) return;\n setSelectedTypeState(value);\n onFirmwareTypeChange?.(value);\n },\n [firmwareLocked, onFirmwareTypeChange],\n );\n\n const retryFirmwareLoad = useCallback(() => {\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 /** Opens the port picker. Call from a click handler. */\n const connect = useCallback(async () => {\n setBusy(\"connect\");\n try {\n return await selectDevice();\n } finally {\n setBusy(null);\n }\n }, [selectDevice]);\n\n /** Grants the bootloader port, then flashes. Call from a click handler. */\n const grant = useCallback(async () => {\n setBusy(\"grant\");\n try {\n const granted = await grantBootloaderPort();\n if (granted && resolvedBinary) await flash(resolvedBinary, flashOverrides);\n return granted;\n } finally {\n setBusy(null);\n }\n }, [grantBootloaderPort, resolvedBinary, flash, flashOverrides]);\n\n /** Flashes the currently selected firmware. */\n const startFlash = useCallback(async () => {\n if (resolvedBinary) await flash(resolvedBinary, flashOverrides);\n }, [resolvedBinary, flash, flashOverrides]);\n\n /** Clears the error and flashes again. */\n const retry = useCallback(async () => {\n resetFlashState();\n if (resolvedBinary) await flash(resolvedBinary, flashOverrides);\n }, [resetFlashState, resolvedBinary, flash, flashOverrides]);\n\n /** Back to the initial state after a finished upload. */\n const startNew = resetFlashState;\n\n return {\n ...device,\n // firmware\n firmwareTypes: FIRMWARE_TYPES,\n firmwareOptions,\n selectedType,\n setSelectedType,\n firmwareLocked,\n showFirmwarePicker,\n firmwareDescription,\n binary: resolvedBinary,\n firmwareLoading: builtinLoading,\n firmwareError: builtinError,\n retryFirmwareLoad,\n // derived\n steps: getStepStatuses({ status, connected, bootloaderReady }),\n flashComplete: status === \"done\",\n latestLogLine,\n busy,\n strings,\n // actions\n connect,\n grant,\n startFlash,\n retry,\n startNew,\n };\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { touchTo1200bps, waitForBootloaderPort } from \"../core/esptool.js\";\nimport {\n DEFAULT_BOARD,\n getBoardProfile,\n labelForPort,\n legacyVendorProfile,\n matchesPort,\n pickLivePort,\n portFilters,\n} from \"../core/boards.js\";\nimport { flashWithProfile } from \"../core/flash.js\";\n\nconst DEFAULT_FLASH_OPTIONS = {\n baudrate: 115200,\n eraseAll: false,\n usingUsbOtg: true,\n};\n\nfunction resolveProfile({ board, profile, usbVendorId }) {\n if (profile) return profile;\n // 1.0 compatibility: a custom vendor id without a board keeps the old\n // vendor-id-only port filtering.\n if (board === undefined && usbVendorId !== undefined) {\n return legacyVendorProfile(usbVendorId);\n }\n const resolved = getBoardProfile(board ?? DEFAULT_BOARD);\n if (!resolved) {\n throw new Error(\n `[flash-tool] Unknown board \"${board}\". Use \"MCU\", \"MCU-S2\" or \"MCU-EYE\".`,\n );\n }\n return resolved;\n}\n\n/**\n * Framework-agnostic-ish (but React) state machine for connecting to a\n * senseBox board over Web Serial, putting it into bootloader mode and\n * flashing a firmware binary supplied by the caller. ESP32 boards (MCU-S2,\n * MCU Eye) are flashed with esptool-js, the SAMD21 MCU with bossa-web.\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 {\"MCU\"|\"MCU-S2\"|\"MCU-EYE\"} [options.board] Target board. Defaults to \"MCU-S2\".\n * @param {object} [options.profile] Custom board profile (see BOARD_PROFILES); wins over `board`.\n * @param {number} [options.usbVendorId] Deprecated: vendor-id-only filtering as in 1.0. Ignored when `board` or `profile` is set.\n * @param {object} [options.flashOptions] Overrides for esptool flashBinary() (ESP boards only).\n * @param {boolean} [options.keepConnection] Keep the selected device after a\n * successful upload (default false: drop it, the UI goes back to \"not\n * connected\"). With true, the next flash() re-uses it - if the handle went\n * stale while the board reset, a live, already-permitted port of the same\n * board is picked up via getPorts() instead, so no picker is needed.\n */\nexport function useFlashDevice({\n board,\n profile: customProfile,\n usbVendorId,\n flashOptions = {},\n keepConnection = false,\n} = {}) {\n const profile = useMemo(\n () => resolveProfile({ board, profile: customProfile, usbVendorId }),\n [board, customProfile, usbVendorId],\n );\n\n // Starts false so the first client render matches the server-rendered\n // markup (navigator isn't defined during SSR); flips after mount once we\n // can safely check for Web Serial support without causing a hydration\n // mismatch.\n const [supported, setSupported] = useState(false);\n useEffect(() => {\n setSupported(\n typeof navigator !== \"undefined\" && navigator.serial !== undefined,\n );\n }, []);\n\n const [port, setPortState] = useState(null);\n const [bootloaderPort, setBootloaderPortState] = useState(null);\n // Mirrors of the port state. Actions read these instead of the closed-over\n // state, so e.g. flash() called right after grantBootloaderPort() in the\n // same handler already sees the freshly granted port.\n const portRef = useRef(null);\n const bootloaderPortRef = useRef(null);\n const setPort = useCallback((value) => {\n portRef.current = value;\n setPortState(value);\n }, []);\n const setBootloaderPort = useCallback((value) => {\n bootloaderPortRef.current = value;\n setBootloaderPortState(value);\n }, []);\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 = {\n ...DEFAULT_FLASH_OPTIONS,\n address: profile.address,\n flashMode: profile.flashMode,\n flashFreq: profile.flashFreq,\n flashSize: profile.flashSize,\n ...flashOptions,\n };\n\n const appendLog = useCallback((message) => {\n setLog((previous) => previous + message);\n }, []);\n\n // Switching boards drops the selected ports, so a port picked for one board\n // never receives another board's firmware.\n // Keyed by value, not object identity, so an inline `profile` object\n // doesn't count as a board switch on every render.\n const profileKey =\n profile.id ??\n JSON.stringify([portFilters(profile), portFilters(profile, true)]);\n const previousProfileKey = useRef(profileKey);\n useEffect(() => {\n if (previousProfileKey.current === profileKey) return;\n previousProfileKey.current = profileKey;\n setPort(null);\n setBootloaderPort(null);\n setNeedsBootloaderPermission(false);\n setDeviceLabel(\"\");\n setStatus(\"idle\");\n setProgress(0);\n setError(null);\n }, [profileKey, setPort, setBootloaderPort]);\n\n const selectDevice = useCallback(async () => {\n setError(null);\n try {\n const selectedPort = await navigator.serial.requestPort({\n filters: portFilters(profile),\n });\n setPort(selectedPort);\n setNeedsBootloaderPermission(false);\n const label = labelForPort(selectedPort, profile);\n setDeviceLabel(label);\n\n // The board may already be sitting in its bootloader (e.g. an ESP32-S2\n // enumerates as a generic \"ESP32-S2\" because no application firmware\n // is running, or an MCU after a reset double-press). In that case the\n // selected port *is* the bootloader port already - skip the 1200bps\n // touch/reset dance entirely instead of trying to \"prepare\" a board\n // that's already ready.\n if (matchesPort(selectedPort, profile, true)) {\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 }, [profile, appendLog, setPort, setBootloaderPort]);\n\n // Adopts a live port of this board the page already has permission for\n // (navigator.serial.getPorts()), without a picker - e.g. to restore a\n // connection after a page reload. Returns the port, or null if none is\n // plugged in / permitted.\n const reconnect = useCallback(async () => {\n setError(null);\n try {\n const livePort = pickLivePort(await navigator.serial.getPorts(), profile);\n if (!livePort) return null;\n setPort(livePort);\n setNeedsBootloaderPermission(false);\n setDeviceLabel(labelForPort(livePort, profile));\n setBootloaderPort(matchesPort(livePort, profile, true) ? livePort : null);\n return livePort;\n } catch (err) {\n setError(err?.message || String(err));\n return null;\n }\n }, [profile, setPort, setBootloaderPort]);\n\n const disconnect = useCallback(() => {\n setPort(null);\n setBootloaderPort(null);\n setNeedsBootloaderPermission(false);\n setDeviceLabel(\"\");\n setStatus(\"idle\");\n setError(null);\n }, [setPort, setBootloaderPort]);\n\n // Phase 1: 1200bps touch + look for an already-permitted bootloader port.\n const prepareBootloader = useCallback(\n async (explicitPort) => {\n const activePort = explicitPort || portRef.current;\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 (candidate) => matchesPort(candidate, profile, true),\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 [profile, appendLog, setBootloaderPort],\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: portFilters(profile, true),\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 }, [profile, appendLog, setBootloaderPort]);\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 = bootloaderPortRef.current;\n lockRef.current = true;\n setStatus(\"flashing\");\n setProgress(0);\n setLog(\"\");\n setError(null);\n\n try {\n if (!flashPort) {\n let activePort = portRef.current;\n if (!activePort) {\n throw new Error(\"No device selected.\");\n }\n // The board reset since the port was picked (previous upload with\n // keepConnection, replugged cable) - its handle is stale. Use the\n // live, already-permitted port of the same board instead.\n if (activePort.connected === false) {\n const livePort = pickLivePort(\n await navigator.serial.getPorts(),\n profile,\n );\n if (!livePort) {\n throw new Error(\n \"The device is no longer connected. Plug it in and connect it again.\",\n );\n }\n activePort = livePort;\n setPort(livePort);\n }\n if (matchesPort(activePort, profile, true)) {\n // Already sitting in its bootloader - no reset needed.\n flashPort = activePort;\n setBootloaderPort(activePort);\n }\n }\n if (!flashPort) {\n lockRef.current = false; // prepareBootloader takes its own lock\n flashPort = await prepareBootloader(portRef.current);\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 flashWithProfile({\n profile,\n port: flashPort,\n binary,\n options: activeFlashOptions,\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 the bootloader handle is\n // stale. Without keepConnection drop the device too, so the UI\n // returns to its initial \"not connected\" state, ready for the next\n // board; with it, the next flash() re-resolves a live port.\n setBootloaderPort(null);\n setNeedsBootloaderPermission(false);\n if (!keepConnection) {\n setPort(null);\n setDeviceLabel(\"\");\n }\n } catch (err) {\n setError(err?.message || String(err));\n setStatus(\"error\");\n } finally {\n lockRef.current = false;\n }\n },\n [\n prepareBootloader,\n appendLog,\n mergedFlashOptions,\n profile,\n keepConnection,\n setPort,\n setBootloaderPort,\n ],\n );\n\n const resetFlashState = useCallback(() => {\n setStatus(\"idle\");\n setProgress(0);\n setLog(\"\");\n setError(null);\n setNeedsBootloaderPermission(false);\n }, []);\n\n return {\n supported,\n profile,\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 reconnect,\n disconnect,\n prepareBootloader,\n grantBootloaderPort,\n flash,\n resetFlashState,\n };\n}\n","// Minimal MD5 (RFC 1321) for Uint8Array input, returning lowercase hex.\n// Used to let esptool-js verify flashed data against the chip's own MD5\n// (esptool-js expects the caller to provide the hash function).\n\nconst S = [\n 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5,\n 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11,\n 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15,\n 21,\n];\nconst K = Array.from(\n { length: 64 },\n (_, i) => Math.floor(Math.abs(Math.sin(i + 1)) * 2 ** 32) >>> 0,\n);\n\n/**\n * @param {Uint8Array} data\n * @returns {string} 32-char lowercase hex digest\n */\nexport function md5Hex(data) {\n const length = data.length;\n const paddedLength = (((length + 8) >> 6) + 1) << 6;\n const buffer = new Uint8Array(paddedLength);\n buffer.set(data);\n buffer[length] = 0x80;\n const view = new DataView(buffer.buffer);\n view.setUint32(paddedLength - 8, (length << 3) >>> 0, true);\n view.setUint32(paddedLength - 4, Math.floor(length / 0x20000000), true);\n\n let a0 = 0x67452301;\n let b0 = 0xefcdab89;\n let c0 = 0x98badcfe;\n let d0 = 0x10325476;\n const M = new Uint32Array(16);\n\n for (let offset = 0; offset < paddedLength; offset += 64) {\n for (let j = 0; j < 16; j++) M[j] = view.getUint32(offset + j * 4, true);\n let a = a0;\n let b = b0;\n let c = c0;\n let d = d0;\n for (let i = 0; i < 64; i++) {\n let f;\n let g;\n if (i < 16) {\n f = (b & c) | (~b & d);\n g = i;\n } else if (i < 32) {\n f = (d & b) | (~d & c);\n g = (5 * i + 1) & 15;\n } else if (i < 48) {\n f = b ^ c ^ d;\n g = (3 * i + 5) & 15;\n } else {\n f = c ^ (b | ~d);\n g = (7 * i) & 15;\n }\n const sum = (a + f + K[i] + M[g]) >>> 0;\n a = d;\n d = c;\n c = b;\n b = (b + ((sum << S[i]) | (sum >>> (32 - S[i])))) >>> 0;\n }\n a0 = (a0 + a) >>> 0;\n b0 = (b0 + b) >>> 0;\n c0 = (c0 + c) >>> 0;\n d0 = (d0 + d) >>> 0;\n }\n\n const out = new DataView(new ArrayBuffer(16));\n [a0, b0, c0, d0].forEach((word, i) => out.setUint32(i * 4, word, true));\n return Array.from(new Uint8Array(out.buffer), (byte) =>\n byte.toString(16).padStart(2, \"0\"),\n ).join(\"\");\n}\n","import { md5Hex } from \"./md5.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 * Leaves USB download mode on an ESP32-S3 (senseBox MCU Eye) and boots the\n * flashed application. Register addresses and the watchdog sequence follow\n * Espressif's ESP32S3ROM hard_reset/watchdog_reset in\n * esptool/targets/esp32s3.py. A watchdog reset also works when the native USB\n * connection cannot pulse the physical EN pin.\n *\n * @param {ESPLoader} loader A connected esptool-js loader.\n */\nexport async function resetEsp32S3(loader) {\n if (loader.chip.CHIP_NAME !== \"ESP32-S3\") {\n throw new Error(\"ESP32-S3 reset requested for a different chip.\");\n }\n // Clear only FORCE_DOWNLOAD_BOOT, preserving the other RTC option bits.\n await loader.writeReg(0x6000812c, 0, 1);\n await loader.writeReg(0x600080b0, 0x50d83aa1); // unlock RTC watchdog\n await loader.writeReg(0x6000809c, 2000); // allow commands to finish before reset\n await loader.writeReg(0x60008098, 0xd0000102); // enable RTC system reset\n await loader.writeReg(0x600080b0, 0); // lock watchdog configuration\n await sleep(500);\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 * `matcher` is either a USB vendor id (1.0 behaviour: the ESP32 ROM\n * bootloader PID 0x0002 of that vendor, or any newly appearing port) or a\n * function `(port) => boolean`. With a function only matching ports are\n * accepted, so unrelated devices plugged in meanwhile are ignored - e.g.\n * `(port) => matchesPort(port, profile, true)`.\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 | ((port: SerialPort) => boolean)} [matcher]\n * @returns {Promise<SerialPort|null>}\n */\nexport function waitForBootloaderPort(\n knownPorts,\n timeoutMs = 8000,\n matcher = DEFAULT_USB_VENDOR_ID,\n) {\n const strict = typeof matcher === \"function\";\n const isBootloader = strict\n ? matcher\n : (port) => isRomBootloaderPort(port, matcher);\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 (isBootloader(candidate) || (!strict && !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 isBootloader(candidate) && candidate.connected === true,\n );\n if (bootloader) {\n finish(bootloader);\n return;\n }\n const fresh = ports.find(\n (candidate) =>\n (!strict || isBootloader(candidate)) &&\n !known.has(candidate) &&\n candidate.connected !== false,\n );\n if (fresh) finish(fresh);\n }, 400);\n\n const timerId = setTimeout(() => finish(null), timeoutMs);\n });\n}\n\n/**\n * Reboots the chip into the freshly flashed application. Exported for tests.\n *\n * - ESP32-S3 (MCU Eye): RTC watchdog reset (resetEsp32S3).\n * - ESP32-S2 (MCU-S2): EN pulse via RTS, then the S2 RTC software reset as a\n * fallback for boards without an RTS->EN circuit.\n * - Other chips over USB-OTG: EN pulse, then esptool-js' USB-OTG hard reset.\n * The S2 register is never written to a non-S2 chip.\n *\n * @param {ESPLoader} loader\n * @param {Transport} transport\n * @param {{ usingUsbOtg?: boolean, onLog?: (msg: string) => void }} [options]\n */\nexport async function resetAfterFlash(\n loader,\n transport,\n { usingUsbOtg = false, onLog } = {},\n) {\n if (usingUsbOtg && loader.chip.CHIP_NAME === \"ESP32-S3\") {\n onLog?.(\"Restarting into the uploaded program...\\n\");\n await resetEsp32S3(loader);\n } else 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 // The RTC register address is ESP32-S2 specific; other chips get\n // esptool-js' own USB-OTG reset instead.\n try {\n if (loader.chip.CHIP_NAME === \"ESP32-S2\") {\n await softResetViaRegister(loader);\n } else {\n await loader.after(\"hard_reset\", true);\n }\n } catch {\n // Ignore - the chip is already rebooting.\n }\n } else {\n await loader.after(\"hard_reset\");\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 {string} [params.expectedChip] Abort before writing if the chip is\n * not this one (esptool-js CHIP_NAME, e.g. \"ESP32-S2\", \"ESP32-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 expectedChip,\n onLog,\n onProgress,\n}) {\n // Loaded on demand: keeps esptool-js out of SAMD-only flashes and lets\n // this module load in Node (esptool-js' ESM build isn't Node-resolvable).\n const { ESPLoader, Transport } = await import(\"esptool-js\");\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 if (expectedChip && loader.chip.CHIP_NAME !== expectedChip) {\n throw new Error(\n `Expected ${expectedChip}, but connected to ${loader.chip.CHIP_NAME}. Check the selected board.`,\n );\n }\n\n // Overall progress across all files, weighted by their uncompressed size\n // (esptool-js reports per file, so a small trailing file such as otadata\n // would otherwise make the bar jump back to 0%).\n const sizes = fileArray.map((file) => file.data.length);\n const totalSize = sizes.reduce((sum, size) => sum + size, 0);\n const doneBefore = (index) =>\n sizes.slice(0, index).reduce((sum, size) => sum + size, 0);\n\n await loader.writeFlash({\n fileArray,\n flashSize,\n flashMode,\n flashFreq,\n eraseAll,\n compress: true,\n // Read the MD5 back from the chip after each file, so a write that\n // did not stick fails the upload instead of reporting success. Only\n // with the stub: esptool-js parses the ROM's MD5 reply differently.\n calculateMD5Hash: loader.IS_STUB ? md5Hex : undefined,\n reportProgress: (fileIndex, written, total) => {\n if (total > 0 && totalSize > 0) {\n const done =\n doneBefore(fileIndex) + (written / total) * sizes[fileIndex];\n onProgress?.(Math.round((done / totalSize) * 100));\n }\n },\n });\n\n await resetAfterFlash(loader, transport, { usingUsbOtg, onLog });\n return chip;\n } finally {\n try {\n await transport.disconnect();\n } catch {\n // Ignore errors while releasing the port.\n }\n }\n}\n","// USB identities and flash settings for the supported senseBox boards.\n//\n// Note: the ESP32-S2 and ESP32-S3 ROM bootloaders both enumerate as\n// 303a:0002, so the bootloader port alone cannot tell an MCU-S2 from an\n// MCU Eye. The selected profile decides which board is expected, and\n// `chip` is checked by flashBinary() after connecting (expectedChip).\n\nconst ESPRESSIF_VENDOR_ID = 0x303a;\n\nconst esp = {\n vendorId: ESPRESSIF_VENDOR_ID,\n bootloaderProductId: 0x0002,\n address: 0x10000,\n // otadata partition in the senseBox TinyUF2 partition tables (MCU-S2 and\n // MCU Eye). Rewritten on every sketch upload to select ota_0, see flash.js.\n otadataAddress: 0xe000,\n flashMode: \"dio\",\n flashFreq: \"80m\",\n backend: \"esp\",\n};\n\n/**\n * Keys are upper-case board ids. `deviceName` / `bootloaderName` are the\n * names the browser's port picker shows for the application and bootloader\n * port, so they can be quoted in UI hints. `builtinFirmware` marks boards the\n * bundled firmware images in src/firmware/ are built for.\n */\nexport const BOARD_PROFILES = {\n MCU: {\n id: \"MCU\",\n compilerBoard: \"sensebox-mcu\",\n deviceName: \"senseBox MCU\",\n bootloaderName: \"senseBox MCU\",\n label: \"senseBox MCU (SAMD21)\",\n bootloaderLabel: \"senseBox MCU (SAMD21 bootloader)\",\n vendorId: 0x04d8,\n productIds: [0xef66, 0xef67],\n bootloaderProductId: 0xef66,\n // The SAM-BA bootloader occupies the first 8 KiB; fixed in samd.js.\n address: 0x2000,\n backend: \"samd\",\n builtinFirmware: false,\n },\n \"MCU-S2\": {\n ...esp,\n id: \"MCU-S2\",\n compilerBoard: \"sensebox-esp32s2\",\n deviceName: \"senseBox MCU-S2 ESP32-S2\",\n bootloaderName: \"ESP32-S2\",\n label: \"senseBox MCU-S2 (ESP32-S2)\",\n bootloaderLabel: \"senseBox MCU-S2 (ESP32-S2 bootloader)\",\n productIds: [0x81b8, 0x81b9, 0x81ba, 0x0002],\n chip: \"ESP32-S2\",\n flashSize: \"4MB\",\n builtinFirmware: true,\n },\n \"MCU-EYE\": {\n ...esp,\n id: \"MCU-EYE\",\n compilerBoard: \"sensebox_mcu_eye\",\n deviceName: \"senseBox MCU Eye\",\n bootloaderName: \"ESP32-S3\",\n label: \"senseBox MCU Eye (ESP32-S3)\",\n bootloaderLabel: \"senseBox MCU Eye (ESP32-S3 bootloader)\",\n productIds: [0x82d1, 0x82d2, 0x82d3, 0x0002, 0x1001],\n bootloaderProductIds: [0x0002, 0x1001],\n chip: \"ESP32-S3\",\n flashSize: \"16MB\",\n builtinFirmware: false,\n },\n};\n\nexport const DEFAULT_BOARD = \"MCU-S2\";\n\n/**\n * Looks up a board profile by id, case-insensitively (\"MCU-Eye\" works).\n * @param {string} board\n * @returns {object|null}\n */\nexport const getBoardProfile = (board) =>\n (typeof board === \"string\" && BOARD_PROFILES[board.toUpperCase()]) || null;\n\nconst bootloaderProductIds = (profile) =>\n profile.bootloaderProductIds ?? [profile.bootloaderProductId];\n\n/**\n * Web Serial `requestPort()` filters for a profile's application ports, or\n * its bootloader ports when `bootloader` is true. A profile without\n * `productIds` filters application ports by vendor id only.\n */\nexport function portFilters(profile, bootloader = false) {\n const productIds = bootloader\n ? bootloaderProductIds(profile)\n : profile.productIds;\n if (!productIds) return [{ usbVendorId: profile.vendorId }];\n return productIds.map((usbProductId) => ({\n usbVendorId: profile.vendorId,\n usbProductId,\n }));\n}\n\n/**\n * Whether a port belongs to the profile (or its bootloader).\n * @param {SerialPort} port\n * @param {object} profile\n * @param {boolean} [bootloader]\n */\nexport function matchesPort(port, profile, bootloader = false) {\n let info;\n try {\n info = port.getInfo?.() ?? {};\n } catch {\n return false;\n }\n return portFilters(profile, bootloader).some(\n (filter) =>\n filter.usbVendorId === info.usbVendorId &&\n (filter.usbProductId === undefined ||\n filter.usbProductId === info.usbProductId),\n );\n}\n\n/**\n * Picks a currently connected port of the profile out of `ports` (usually\n * `navigator.serial.getPorts()`, i.e. ports the page already has permission\n * for). After a reset the board re-enumerates and an earlier port handle can\n * go stale (`connected === false`); this finds the live one without a picker.\n * Ports whose `connected` flag is unknown (older browsers) count as live.\n *\n * @param {SerialPort[]} ports\n * @param {object} profile\n * @returns {SerialPort|null}\n */\nexport function pickLivePort(ports, profile) {\n const candidates = (ports ?? []).filter((candidate) =>\n matchesPort(candidate, profile),\n );\n return (\n candidates.find((candidate) => candidate.connected === true) ??\n candidates.find((candidate) => candidate.connected === undefined) ??\n null\n );\n}\n\n/**\n * Profile emulating flash-tool 1.0 for callers that only pass a custom\n * `usbVendorId`: picker filtered by vendor id only, ROM bootloader on PID\n * 0x0002, MCU-S2 flash settings, no chip check.\n * @param {number} vendorId\n */\nexport function legacyVendorProfile(vendorId) {\n return {\n ...BOARD_PROFILES[\"MCU-S2\"],\n id: `vendor:${vendorId}`,\n vendorId,\n productIds: undefined,\n bootloaderProductIds: [0x0002],\n chip: undefined,\n };\n}\n\n/**\n * Human-readable port label for logs/UI.\n * @param {SerialPort} port\n * @param {object} [profile]\n */\nexport function labelForPort(port, profile) {\n const info = port.getInfo?.() ?? {};\n if (profile?.productIds) {\n if (matchesPort(port, profile, true)) return profile.bootloaderLabel;\n if (matchesPort(port, profile)) return profile.label;\n }\n const hex = (value) =>\n value !== undefined ? value.toString(16).padStart(4, \"0\") : \"????\";\n return `USB device (VID 0x${hex(info.usbVendorId)} / PID 0x${hex(info.usbProductId)})`;\n}\n","// bossa-web 0.9.7 terminates binary replies at NUL and doesn't await its read\n// loop on disconnect. Keep exact-length replies intact and release the reader\n// before closing the Web Serial port (including when a reset disconnects USB).\nexport function createSamBATransport(SamBA) {\n return class extends SamBA {\n async connect() {\n // Native USB CDC does not use CTS/RTS hardware flow control. In\n // particular, don't inherit bossa-web's flowControl: \"hardware\".\n await this.serialPort.open({\n baudRate: 115200,\n dataBits: 8,\n stopBits: 1,\n parity: \"none\",\n flowControl: \"none\",\n bufferSize: 4096,\n });\n await this.serialPort.setSignals({\n dataTerminalReady: true,\n requestToSend: true,\n });\n await new Promise((resolve) => setTimeout(resolve, 100));\n await this._connect();\n }\n\n async setBinaryMode() {\n // BOSSA's Samba::init() drains the optional N# acknowledgement without\n // requiring two bytes. A valid subsequent V# reply establishes that the\n // bootloader is responding; memory reads must still have exact lengths.\n await this.sendCommand(\"N\", 0);\n await this.readBuffer(100);\n }\n\n readLoop() {\n this.activeReadLoop = super.readLoop();\n return this.activeReadLoop;\n }\n\n async disconnect() {\n this.closed = true;\n try {\n await this.serialReader?.cancel();\n } finally {\n await this.activeReadLoop;\n }\n }\n\n async sendCommand(...args) {\n try {\n return await super.sendCommand(...args);\n } catch (error) {\n throw new Error(`SAM-BA command ${args[0]}: ${error.message ?? error}`);\n }\n }\n\n async readVersion() {\n // In BOSSA, 256 is a maximum text buffer size, not an exact wire length.\n // Read the version through its line/NUL terminator, while retaining exact\n // lengths for memory reads, where zero and newline bytes are valid data.\n await this.sendCommand(\"V\", 0);\n const reply = [];\n const deadline = Date.now() + 3000;\n while (Date.now() < deadline && reply.length < 256) {\n if (this.inputBuffer.length) {\n const byte = this.inputBuffer.shift();\n if (byte === 0 || byte === 10 || byte === 13) {\n if (reply.length)\n return new TextDecoder().decode(Uint8Array.from(reply));\n } else {\n reply.push(byte);\n }\n } else {\n await new Promise((resolve) => setTimeout(resolve, 10));\n }\n }\n throw new Error(\n \"SAM-BA handshake failed: no complete version reply to V#. \" +\n \"Double-press the MCU reset button, reconnect its bootloader port, and retry.\",\n );\n }\n\n async read(address, buffer, size) {\n // SAMD bootloaders require USB reads below 64 bytes. The dependency's\n // chunked read advances the destination twice and leaves gaps in readback.\n for (let offset = 0; offset < size;) {\n const length = Math.min(63, size - offset);\n const data = await this.sendCommand(\n `R${this.hex(address + offset)},${this.hex(length)}`,\n length,\n );\n buffer.set(data, offset);\n offset += length;\n }\n }\n\n async readBuffer(timeout = 3000, responseSize) {\n const reply = [];\n const deadline = Date.now() + timeout;\n while (Date.now() < deadline) {\n if (this.inputBuffer.length) {\n const byte = this.inputBuffer.shift();\n reply.push(byte);\n if (responseSize ? reply.length === responseSize : byte === 0) {\n return Uint8Array.from(reply);\n }\n } else {\n await new Promise((resolve) => setTimeout(resolve, 10));\n }\n }\n if (responseSize) {\n throw new Error(\n `Incomplete SAM-BA response: expected ${responseSize} bytes, received ${reply.length}.`,\n );\n }\n return reply.length ? Uint8Array.from(reply) : null;\n }\n };\n}\n","import { createSamBATransport } from \"./sambaTransport.js\";\n\n// The MCU linker script reserves the first 8 KiB for its SAM-BA bootloader.\nconst APPLICATION_OFFSET = 0x2000;\n\n/**\n * Flashes an application image to a senseBox MCU (SAMD21) through its SAM-BA\n * bootloader, using bossa-web. bossa-web is an optional dependency and only\n * loaded when this runs.\n *\n * @param {object} params\n * @param {SerialPort} params.port The SAM-BA bootloader port.\n * @param {Uint8Array} params.firmware Application image (written at 0x2000).\n * @param {(msg: string) => void} [params.onLog]\n * @param {(percent: number) => void} [params.onProgress]\n */\nexport async function flashSamdBinary({ port, firmware, onLog, onProgress }) {\n const { SamBA, Device, Family, Flasher } = await import(\"bossa-web\");\n const logger = {\n debug: () => {},\n log: (...args) => onLog?.(`${args.join(\" \")}\\n`),\n error: (...args) => onLog?.(`${args.join(\" \")}\\n`),\n };\n const Transport = createSamBATransport(SamBA);\n const samba = new Transport(port, { logger });\n try {\n await samba.connect();\n const device = new Device(samba);\n await device.create();\n const flash = device.flash;\n if (device.family !== Family.FAMILY_SAMD21 || !flash) {\n throw new Error(\"The connected device is not a SAMD21 senseBox MCU.\");\n }\n if (\n !firmware.length ||\n firmware.length > flash.totalSize - APPLICATION_OFFSET\n ) {\n throw new Error(\"Firmware does not fit in the MCU application flash.\");\n }\n const flasher = new Flasher(samba, flash, {\n onStatus: (message) => onLog?.(message),\n onProgress: (current, total) => {\n if (total) onProgress?.(Math.round((current / total) * 100));\n },\n });\n await flasher.erase(APPLICATION_OFFSET);\n await flasher.write(firmware, APPLICATION_OFFSET);\n // bossa-web's verify() is unimplemented, so compare flash readback directly.\n onLog?.(\"Verify firmware\\n\");\n for (let offset = 0; offset < firmware.length; offset += 256) {\n const expected = firmware.subarray(offset, offset + 256);\n const actual = new Uint8Array(expected.length);\n await samba.read(APPLICATION_OFFSET + offset, actual, actual.length);\n if (actual.some((byte, index) => byte !== expected[index])) {\n throw new Error(`Firmware verification failed at offset ${offset}.`);\n }\n }\n await device.reset();\n } finally {\n try {\n await samba.disconnect();\n } catch {\n // Reset can already have disconnected the device.\n }\n try {\n await port.close();\n } catch {\n // Release partially opened ports as well as successful connections.\n }\n }\n}\n","import { flashBinary } from \"./esptool.js\";\nimport { flashSamdBinary } from \"./samd.js\";\n\nconst OTADATA_SIZE = 0x2000;\n\n/**\n * otadata image (two 4 KiB sectors) that makes the ESP-IDF bootloader boot\n * ota_0 - the same content as Arduino's boot_app0.bin, which the Arduino IDE\n * writes on every upload. Sector 0 holds one valid esp_ota_select_entry_t\n * (ota_seq = 1, CRC as computed by ESP-IDF), sector 1 stays erased.\n *\n * Writing only the sketch to ota_0 is not enough: whatever selected another\n * app partition before (TinyUF2 drag & drop, an OTA update, APOTA) would\n * still win at the next boot and the old sketch keeps running.\n */\nexport function createOtadataBootOta0() {\n const otadata = new Uint8Array(OTADATA_SIZE).fill(0xff);\n otadata.set([0x01, 0x00, 0x00, 0x00], 0); // ota_seq = 1 -> ota_0\n otadata.set([0x9a, 0x98, 0x43, 0x47], 28); // crc32 of ota_seq\n return otadata;\n}\n\n/**\n * Flashes `binary` to `port` using the backend the board profile needs:\n * bossa-web for the SAMD21 MCU, esptool-js (with a chip check) otherwise.\n *\n * For ESP boards a sketch written to the profile's app address (ota_0) is\n * accompanied by an otadata image selecting ota_0, so the new sketch is the\n * one that boots. Full images written elsewhere (e.g. merged images at 0x0)\n * bring their own otadata and are left alone. Disable with\n * `options.writeOtadata = false`.\n *\n * @param {object} params\n * @param {object} params.profile Board profile (see boards.js).\n * @param {SerialPort} params.port Bootloader port.\n * @param {Uint8Array} params.binary Firmware image.\n * @param {object} [params.options] Merged esptool options (ESP boards only).\n * @param {(msg: string) => void} [params.onLog]\n * @param {(percent: number) => void} [params.onProgress]\n * @param {object} [deps] Injected flash functions (for tests).\n */\nexport function flashWithProfile(\n { profile, port, binary, options = {}, onLog, onProgress },\n deps = { flashBinary, flashSamdBinary },\n) {\n if (profile.backend === \"samd\") {\n return deps.flashSamdBinary({ port, firmware: binary, onLog, onProgress });\n }\n const address = options.address ?? profile.address;\n const fileArray = [{ data: binary, address }];\n if (\n options.writeOtadata !== false &&\n profile.otadataAddress !== undefined &&\n address === profile.address\n ) {\n // Written after the app, so a failed app write leaves the previous boot\n // selection untouched.\n fileArray.push({\n data: createOtadataBootOta0(),\n address: profile.otadataAddress,\n });\n }\n return deps.flashBinary({\n port,\n fileArray,\n baudrate: options.baudrate,\n flashMode: options.flashMode,\n flashFreq: options.flashFreq,\n flashSize: options.flashSize,\n eraseAll: options.eraseAll,\n usingUsbOtg: options.usingUsbOtg,\n expectedChip: profile.chip,\n onLog,\n onProgress,\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","// Shared constants and pure helpers behind <FlashTool /> and the headless\n// useFlashTool() hook. No React in here, so everything is unit-testable and\n// reusable by custom UIs (labels, descriptions, step logic).\n\nexport const 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\".\nexport const FIRMWARE_ALIASES = {\n circuitpython: \"circuitpy\",\n};\n\nexport function 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. Only applied on\n// boards the bundled images are built for (MCU-S2) - for the MCU (SAMD21)\n// the application offset is fixed at 0x2000 anyway.\nexport const 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`.\nexport const 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\".\nexport const 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\nexport const 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 step1HintBoard:\n \"Plug in your senseBox, then select “%1” in 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 step2HintPermissionBoard:\n \"The board now shows up as a new USB device. Select “%1” once to grant permission.\",\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.\nexport const 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 step1HintBoard:\n \"Schließe deine senseBox an und wähle in der Geräteliste des Browsers „%1“ 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 step2HintPermissionBoard:\n \"Das Board meldet sich jetzt als neues USB-Gerät. Wähle einmalig „%1“ aus, um die Berechtigung zu erteilen.\",\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 * Merges the localized default strings with caller overrides and fills in\n * the board-specific hints (names the browser's port picker shows). A\n * caller-supplied `strings.step1Hint` / `step2HintPermission` always wins.\n *\n * @param {\"en\"|\"de\"} [language]\n * @param {Record<string,string>} [strings]\n * @param {{deviceName?: string, bootloaderName?: string}} [profile]\n */\nexport function getStrings(language, strings, profile = {}) {\n const merged = {\n ...(language === \"de\" ? DEFAULT_STRINGS_DE : DEFAULT_STRINGS),\n ...strings,\n };\n const withName = (key, name) =>\n strings?.[key] ??\n (name ? merged[`${key}Board`].replace(\"%1\", name) : merged[key]);\n return {\n ...merged,\n step1Hint: withName(\"step1Hint\", profile.deviceName),\n step2HintPermission: withName(\n \"step2HintPermission\",\n profile.bootloaderName,\n ),\n };\n}\n\n/** Display label for a firmware type, e.g. \"circuitpy\" -> strings.firmwareCircuitpy. */\nexport function firmwareLabel(type, strings) {\n return strings[`firmware${type.charAt(0).toUpperCase()}${type.slice(1)}`];\n}\n\n/**\n * Last non-empty log line, without esptool-js's \"(NN%)\" progress suffix\n * (the same percentage is already exposed as `progress`). Useful to show\n * that a long full-chip erase is still working while progress sits at 0%.\n *\n * @param {string} log\n */\nexport function getLatestLogLine(log) {\n if (!log) return \"\";\n const lines = log.split(\"\\n\").filter((line) => line.trim().length > 0);\n const lastLine = lines[lines.length - 1] || \"\";\n return lastLine.replace(/\\s*\\(\\d+ ?%\\)\\s*$/, \"\");\n}\n\n/**\n * Status of the 3 user-facing steps (connect, bootloader, upload). Exactly\n * one step is \"active\" at a time, so a UI always knows which action to show.\n *\n * A successful flash resets `connected`/`bootloaderReady` (see\n * useFlashDevice) so the hook is ready for the next board - but all 3 steps\n * stay \"done\" until the user explicitly starts a new upload.\n *\n * @param {{status: string, connected: boolean, bootloaderReady: boolean}} state\n * @returns {{step1: StepStatus, step2: StepStatus, step3: StepStatus}}\n * @typedef {\"done\"|\"active\"|\"pending\"|\"error\"} StepStatus\n */\nexport function getStepStatuses({ status, connected, bootloaderReady }) {\n if (status === \"done\") {\n return { step1: \"done\", step2: \"done\", step3: \"done\" };\n }\n return {\n step1: connected ? \"done\" : \"active\",\n step2: !connected ? \"pending\" : bootloaderReady ? \"done\" : \"active\",\n step3: !bootloaderReady\n ? \"pending\"\n : status === \"error\"\n ? \"error\"\n : \"active\",\n };\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"]}
|