diff --git a/index-jquery.html b/index-jquery.html new file mode 100644 index 0000000..803165e --- /dev/null +++ b/index-jquery.html @@ -0,0 +1,153 @@ + + + + + + + + flash.comma.ai + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ comma +

flash.comma.ai

+

This tool allows you to flash AGNOS onto your comma device.

+

AGNOS is the Ubuntu-based operating system for your comma 3/3X.

+
+
+ +
+

Requirements

+
    +
  • A web browser which supports WebUSB (such as Google Chrome, Microsoft Edge, Opera), running on Windows, macOS, Linux, or Android.
  • +
  • A USB-C cable to power your device outside the car.
  • +
  • Another USB-C cable to connect the device to your computer.
  • +
+

USB Driver

+

You need additional driver software for Windows before you connect your device.

+
    +
  1. Download and install Zadig.
  2. +
  3. + Under Device in the menu bar, select Create New Device. + Zadig Create New Device +
  4. +
  5. + Fill in three fields. The first field is just a description and you can fill in anything. + The next two fields are very important. Fill them in with 05C6 and 9008 respectively. + Press "Install Driver" and give it a few minutes to install. + Zadig Form +
  6. +
+

No additional software is required for macOS or Linux.

+
+
+ +
+

QDL Mode

+

Follow these steps to put your device into QDL mode:

+
    +
  1. Power off the device and wait for the LEDs to switch off.
  2. +
  3. Connect the device to your computer using the USB-C port (port 2).
  4. +
  5. Connect power to the OBD-C port (port 1).
  6. +
  7. The device then should be visible as an option when choosing the device to flash
  8. +
+ image showing comma three and two ports +
+
+ +
+

Flashing

+

After your device is in QDL mode, you can click the button to start flashing. A prompt may appear to select a device; choose the device starts with QUSB_BULK.

+

The process can take 30+ minutes depending on your internet connection and system performance. Do not unplug the device until all steps are complete.

+
+
+ +
+

Troubleshooting

+

Too slow

+

It is recommended that you use a USB 3.0 cable when flashing since it will speed up the flashing time by a lot.

+ +

Cannot enter QDL

+

Try using a different USB cable or USB port. Sometimes USB 2.0 ports work better than USB 3.0 (blue) ports. If you're using a USB hub, try connecting the device directly to your computer, or alternatively use a USB hub between your computer and the device.

+ +

My device's screen is blank

+

The device screen will be blank in QDL mode, but you can verify that it is in QDL if the device shows up when you press the Flash icon.

+ +

After flashing, device says unable to mount data partition

+

This is expected after the filesystem is erased. Press confirm to finish resetting your device.

+ +

General Tips

+
    +
  • Try another computer or OS
  • +
  • Try different USB ports on your computer
  • +
  • Try different USB-C cables, including the OBD-C cable that came with the device
  • +
+ +

Other questions

+

If you need help, join our Discord server and go to the #hw-three-3x channel.

+
+
+ +
+
+
+ status +
+
+
+
+
+
+ Initializing... + + + + +
+
+
+ + + + + diff --git a/package.json b/package.json index 7968bfd..e35addb 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "build": "vite build", "start": "vite preview", "lint": "eslint . --ext js,jsx --report-unused-disable-directives", - "test": "vitest" + "test": "vitest", + "build:jquery-css": "tailwindcss -i src/index-jquery.css -o public/index-jquery.css", + "watch:jquery-css": "tailwindcss -i src/index-jquery.css -o public/index-jquery.css --watch" }, "engines": { "node": ">=20.11.0" diff --git a/public/index-jquery.css b/public/index-jquery.css new file mode 100644 index 0000000..867d393 --- /dev/null +++ b/public/index-jquery.css @@ -0,0 +1,1601 @@ +/* +! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: Inter Variable, sans-serif; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + letter-spacing: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +input:where([type='button']), +input:where([type='reset']), +input:where([type='submit']) { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +.prose { + color: var(--tw-prose-body); + max-width: 65ch; +} + +.prose :where(p):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 1.25em; + margin-bottom: 1.25em; +} + +.prose :where([class~="lead"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-lead); + font-size: 1.25em; + line-height: 1.6; + margin-top: 1.2em; + margin-bottom: 1.2em; +} + +.prose :where(a):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-links); + text-decoration: underline; + font-weight: 500; +} + +.prose :where(strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-bold); + font-weight: 600; +} + +.prose :where(a strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(blockquote strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(thead th strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: decimal; + margin-top: 1.25em; + margin-bottom: 1.25em; + padding-inline-start: 1.625em; +} + +.prose :where(ol[type="A"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: upper-alpha; +} + +.prose :where(ol[type="a"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: lower-alpha; +} + +.prose :where(ol[type="A" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: upper-alpha; +} + +.prose :where(ol[type="a" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: lower-alpha; +} + +.prose :where(ol[type="I"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: upper-roman; +} + +.prose :where(ol[type="i"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: lower-roman; +} + +.prose :where(ol[type="I" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: upper-roman; +} + +.prose :where(ol[type="i" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: lower-roman; +} + +.prose :where(ol[type="1"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: decimal; +} + +.prose :where(ul):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: disc; + margin-top: 1.25em; + margin-bottom: 1.25em; + padding-inline-start: 1.625em; +} + +.prose :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *))::marker { + font-weight: 400; + color: var(--tw-prose-counters); +} + +.prose :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *))::marker { + color: var(--tw-prose-bullets); +} + +.prose :where(dt):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + margin-top: 1.25em; +} + +.prose :where(hr):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-color: var(--tw-prose-hr); + border-top-width: 1px; + margin-top: 3em; + margin-bottom: 3em; +} + +.prose :where(blockquote):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 500; + font-style: italic; + color: var(--tw-prose-quotes); + border-inline-start-width: 0.25rem; + border-inline-start-color: var(--tw-prose-quote-borders); + quotes: "\201C""\201D""\2018""\2019"; + margin-top: 1.6em; + margin-bottom: 1.6em; + padding-inline-start: 1em; +} + +.prose :where(blockquote p:first-of-type):not(:where([class~="not-prose"],[class~="not-prose"] *))::before { + content: open-quote; +} + +.prose :where(blockquote p:last-of-type):not(:where([class~="not-prose"],[class~="not-prose"] *))::after { + content: close-quote; +} + +.prose :where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 800; + font-size: 2.25em; + margin-top: 0; + margin-bottom: 0.8888889em; + line-height: 1.1111111; +} + +.prose :where(h1 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 900; + color: inherit; +} + +.prose :where(h2):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 700; + font-size: 1.5em; + margin-top: 2em; + margin-bottom: 1em; + line-height: 1.3333333; +} + +.prose :where(h2 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 800; + color: inherit; +} + +.prose :where(h3):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + font-size: 1.25em; + margin-top: 1.6em; + margin-bottom: 0.6em; + line-height: 1.6; +} + +.prose :where(h3 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 700; + color: inherit; +} + +.prose :where(h4):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + margin-top: 1.5em; + margin-bottom: 0.5em; + line-height: 1.5; +} + +.prose :where(h4 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 700; + color: inherit; +} + +.prose :where(img):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 2em; + margin-bottom: 2em; +} + +.prose :where(picture):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + display: block; + margin-top: 2em; + margin-bottom: 2em; +} + +.prose :where(video):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 2em; + margin-bottom: 2em; +} + +.prose :where(kbd):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 500; + font-family: inherit; + color: var(--tw-prose-kbd); + box-shadow: 0 0 0 1px rgb(var(--tw-prose-kbd-shadows) / 10%), 0 3px 0 rgb(var(--tw-prose-kbd-shadows) / 10%); + font-size: 0.875em; + border-radius: 0.3125rem; + padding-top: 0.1875em; + padding-inline-end: 0.375em; + padding-bottom: 0.1875em; + padding-inline-start: 0.375em; +} + +.prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-code); + font-weight: 600; + font-size: 0.875em; +} + +.prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::before { + content: "`"; +} + +.prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::after { + content: "`"; +} + +.prose :where(a code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(h1 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(h2 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + font-size: 0.875em; +} + +.prose :where(h3 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + font-size: 0.9em; +} + +.prose :where(h4 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(blockquote code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(thead th code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(pre):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-pre-code); + background-color: var(--tw-prose-pre-bg); + overflow-x: auto; + font-weight: 400; + font-size: 0.875em; + line-height: 1.7142857; + margin-top: 1.7142857em; + margin-bottom: 1.7142857em; + border-radius: 0.375rem; + padding-top: 0.8571429em; + padding-inline-end: 1.1428571em; + padding-bottom: 0.8571429em; + padding-inline-start: 1.1428571em; +} + +.prose :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + background-color: transparent; + border-width: 0; + border-radius: 0; + padding: 0; + font-weight: inherit; + color: inherit; + font-size: inherit; + font-family: inherit; + line-height: inherit; +} + +.prose :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *))::before { + content: none; +} + +.prose :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *))::after { + content: none; +} + +.prose :where(table):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + width: 100%; + table-layout: auto; + margin-top: 2em; + margin-bottom: 2em; + font-size: 0.875em; + line-height: 1.7142857; +} + +.prose :where(thead):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-bottom-width: 1px; + border-bottom-color: var(--tw-prose-th-borders); +} + +.prose :where(thead th):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + vertical-align: bottom; + padding-inline-end: 0.5714286em; + padding-bottom: 0.5714286em; + padding-inline-start: 0.5714286em; +} + +.prose :where(tbody tr):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-bottom-width: 1px; + border-bottom-color: var(--tw-prose-td-borders); +} + +.prose :where(tbody tr:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-bottom-width: 0; +} + +.prose :where(tbody td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + vertical-align: baseline; +} + +.prose :where(tfoot):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-top-width: 1px; + border-top-color: var(--tw-prose-th-borders); +} + +.prose :where(tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + vertical-align: top; +} + +.prose :where(th, td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + text-align: start; +} + +.prose :where(figure > *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + margin-bottom: 0; +} + +.prose :where(figcaption):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-captions); + font-size: 0.875em; + line-height: 1.4285714; + margin-top: 0.8571429em; +} + +.prose { + --tw-prose-body: #374151; + --tw-prose-headings: #111827; + --tw-prose-lead: #4b5563; + --tw-prose-links: #111827; + --tw-prose-bold: #111827; + --tw-prose-counters: #6b7280; + --tw-prose-bullets: #d1d5db; + --tw-prose-hr: #e5e7eb; + --tw-prose-quotes: #111827; + --tw-prose-quote-borders: #e5e7eb; + --tw-prose-captions: #6b7280; + --tw-prose-kbd: #111827; + --tw-prose-kbd-shadows: 17 24 39; + --tw-prose-code: #111827; + --tw-prose-pre-code: #e5e7eb; + --tw-prose-pre-bg: #1f2937; + --tw-prose-th-borders: #d1d5db; + --tw-prose-td-borders: #e5e7eb; + --tw-prose-invert-body: #d1d5db; + --tw-prose-invert-headings: #fff; + --tw-prose-invert-lead: #9ca3af; + --tw-prose-invert-links: #fff; + --tw-prose-invert-bold: #fff; + --tw-prose-invert-counters: #9ca3af; + --tw-prose-invert-bullets: #4b5563; + --tw-prose-invert-hr: #374151; + --tw-prose-invert-quotes: #f3f4f6; + --tw-prose-invert-quote-borders: #374151; + --tw-prose-invert-captions: #9ca3af; + --tw-prose-invert-kbd: #fff; + --tw-prose-invert-kbd-shadows: 255 255 255; + --tw-prose-invert-code: #fff; + --tw-prose-invert-pre-code: #d1d5db; + --tw-prose-invert-pre-bg: rgb(0 0 0 / 50%); + --tw-prose-invert-th-borders: #4b5563; + --tw-prose-invert-td-borders: #374151; + font-size: 1rem; + line-height: 1.75; +} + +.prose :where(picture > img):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + margin-bottom: 0; +} + +.prose :where(li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +.prose :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-start: 0.375em; +} + +.prose :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-start: 0.375em; +} + +.prose :where(.prose > ul > li p):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0.75em; + margin-bottom: 0.75em; +} + +.prose :where(.prose > ul > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 1.25em; +} + +.prose :where(.prose > ul > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-bottom: 1.25em; +} + +.prose :where(.prose > ol > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 1.25em; +} + +.prose :where(.prose > ol > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-bottom: 1.25em; +} + +.prose :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0.75em; + margin-bottom: 0.75em; +} + +.prose :where(dl):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 1.25em; + margin-bottom: 1.25em; +} + +.prose :where(dd):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0.5em; + padding-inline-start: 1.625em; +} + +.prose :where(hr + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; +} + +.prose :where(h2 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; +} + +.prose :where(h3 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; +} + +.prose :where(h4 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; +} + +.prose :where(thead th:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-start: 0; +} + +.prose :where(thead th:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-end: 0; +} + +.prose :where(tbody td, tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-top: 0.5714286em; + padding-inline-end: 0.5714286em; + padding-bottom: 0.5714286em; + padding-inline-start: 0.5714286em; +} + +.prose :where(tbody td:first-child, tfoot td:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-start: 0; +} + +.prose :where(tbody td:last-child, tfoot td:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-end: 0; +} + +.prose :where(figure):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 2em; + margin-bottom: 2em; +} + +.prose :where(.prose > :first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; +} + +.prose :where(.prose > :last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-bottom: 0; +} + +.prose-green { + --tw-prose-links: #16a34a; + --tw-prose-invert-links: #22c55e; +} + +.visible { + visibility: visible; +} + +.static { + position: static; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.bottom-0 { + bottom: 0px; +} + +.bottom-4 { + bottom: 1rem; +} + +.left-0 { + left: 0px; +} + +.right-2 { + right: 0.5rem; +} + +.top-0 { + top: 0px; +} + +.top-2 { + top: 0.5rem; +} + +.m-0 { + margin: 0px; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.block { + display: block; +} + +.flex { + display: flex; +} + +.hidden { + display: none; +} + +.h-2 { + height: 0.5rem; +} + +.h-\[700px\] { + height: 700px; +} + +.h-auto { + height: auto; +} + +.h-full { + height: 100%; +} + +.w-full { + width: 100%; +} + +.w-screen { + width: 100vw; +} + +.max-w-3xl { + max-width: 48rem; +} + +.max-w-none { + max-width: none; +} + +.max-w-xl { + max-width: 36rem; +} + +.flex-grow { + flex-grow: 1; +} + +.transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +@keyframes pulse { + 50% { + opacity: .5; + } +} + +.animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +.cursor-pointer { + cursor: pointer; +} + +.flex-row { + flex-direction: row; +} + +.flex-col { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-8 { + gap: 2rem; +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-x-auto { + overflow-x: auto; +} + +.text-wrap { + text-wrap: wrap; +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.bg-\[\#51ff00\] { + --tw-bg-opacity: 1; + background-color: rgb(81 255 0 / var(--tw-bg-opacity)); +} + +.bg-blue-500 { + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity)); +} + +.bg-gray-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + +.bg-gray-200 { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.bg-gray-300 { + --tw-bg-opacity: 1; + background-color: rgb(209 213 219 / var(--tw-bg-opacity)); +} + +.bg-gray-400 { + --tw-bg-opacity: 1; + background-color: rgb(156 163 175 / var(--tw-bg-opacity)); +} + +.bg-green-500 { + --tw-bg-opacity: 1; + background-color: rgb(34 197 94 / var(--tw-bg-opacity)); +} + +.bg-lime-400 { + --tw-bg-opacity: 1; + background-color: rgb(163 230 53 / var(--tw-bg-opacity)); +} + +.bg-red-500 { + --tw-bg-opacity: 1; + background-color: rgb(239 68 68 / var(--tw-bg-opacity)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-yellow-500 { + --tw-bg-opacity: 1; + background-color: rgb(234 179 8 / var(--tw-bg-opacity)); +} + +.p-12 { + padding: 3rem; +} + +.p-4 { + padding: 1rem; +} + +.p-6 { + padding: 1.5rem; +} + +.p-8 { + padding: 2rem; +} + +.px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.px-8 { + padding-left: 2rem; + padding-right: 2rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.font-mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.font-light { + font-weight: 300; +} + +.text-black { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + +.text-gray-800 { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity)); +} + +.text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.text-green-500 { + --tw-text-opacity: 1; + color: rgb(34 197 94 / var(--tw-text-opacity)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.opacity-0 { + opacity: 0; +} + +.invert { + --tw-invert: invert(100%); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-colors { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-opacity { + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-300 { + transition-duration: 300ms; +} + +/* Custom animations */ + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } +} + +.animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +/* Dark mode invert */ + +.dark .dark-invert { + filter: invert(1); +} + +@media (prefers-color-scheme: dark) { + .dark\:prose-invert { + --tw-prose-body: var(--tw-prose-invert-body); + --tw-prose-headings: var(--tw-prose-invert-headings); + --tw-prose-lead: var(--tw-prose-invert-lead); + --tw-prose-links: var(--tw-prose-invert-links); + --tw-prose-bold: var(--tw-prose-invert-bold); + --tw-prose-counters: var(--tw-prose-invert-counters); + --tw-prose-bullets: var(--tw-prose-invert-bullets); + --tw-prose-hr: var(--tw-prose-invert-hr); + --tw-prose-quotes: var(--tw-prose-invert-quotes); + --tw-prose-quote-borders: var(--tw-prose-invert-quote-borders); + --tw-prose-captions: var(--tw-prose-invert-captions); + --tw-prose-kbd: var(--tw-prose-invert-kbd); + --tw-prose-kbd-shadows: var(--tw-prose-invert-kbd-shadows); + --tw-prose-code: var(--tw-prose-invert-code); + --tw-prose-pre-code: var(--tw-prose-invert-pre-code); + --tw-prose-pre-bg: var(--tw-prose-invert-pre-bg); + --tw-prose-th-borders: var(--tw-prose-invert-th-borders); + --tw-prose-td-borders: var(--tw-prose-invert-td-borders); + } +} + +.hover\:bg-gray-300:hover { + --tw-bg-opacity: 1; + background-color: rgb(209 213 219 / var(--tw-bg-opacity)); +} + +@media (min-width: 640px) { + .sm\:w-auto { + width: auto; + } + + .sm\:min-w-\[350px\] { + min-width: 350px; + } + + .sm\:border { + border-width: 1px; + } + + .sm\:border-gray-200 { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); + } +} + +@media (min-width: 768px) { + .md\:p-16 { + padding: 4rem; + } +} + +@media (min-width: 1024px) { + .lg\:m-4 { + margin: 1rem; + } + + .lg\:block { + display: block; + } + + .lg\:hidden { + display: none; + } + + .lg\:h-screen { + height: 100vh; + } + + .lg\:w-auto { + width: auto; + } + + .lg\:max-w-prose { + max-width: 65ch; + } + + .lg\:flex-1 { + flex: 1 1 0%; + } + + .lg\:flex-row { + flex-direction: row; + } + + .lg\:overflow-y-auto { + overflow-y: auto; + } + + .lg\:p-20 { + padding: 5rem; + } +} + +@media (min-width: 1280px) { + .xl\:p-24 { + padding: 6rem; + } +} + +@media (prefers-color-scheme: dark) { + .dark\:bg-gray-700 { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); + } + + .dark\:bg-gray-800 { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); + } + + .dark\:bg-gray-900 { + --tw-bg-opacity: 1; + background-color: rgb(17 24 39 / var(--tw-bg-opacity)); + } + + .dark\:text-\[\#51ff00\] { + --tw-text-opacity: 1; + color: rgb(81 255 0 / var(--tw-text-opacity)); + } + + .dark\:text-gray-100 { + --tw-text-opacity: 1; + color: rgb(243 244 246 / var(--tw-text-opacity)); + } + + .dark\:text-gray-200 { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); + } + + .dark\:text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); + } + + .dark\:invert { + --tw-invert: invert(100%); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); + } + + .dark\:hover\:bg-gray-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(75 85 99 / var(--tw-bg-opacity)); + } + + @media (min-width: 640px) { + .dark\:sm\:border-gray-600 { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity)); + } + } +} diff --git a/src/QDL/firehose-jquery.js b/src/QDL/firehose-jquery.js new file mode 100644 index 0000000..03340ba --- /dev/null +++ b/src/QDL/firehose-jquery.js @@ -0,0 +1,126 @@ +// Global Firehose protocol object +window.FirehoseProtocol = (function() { + class Firehose { + constructor(usbdev) { + this.usbdev = usbdev; + this.cfg = { + SECTOR_SIZE_IN_BYTES: 512, + MAX_PAYLOAD_SIZE_IN_BYTES: 1048576 + }; + this.luns = [0]; + this.xmlParser = new window.XMLParserUtils.Parser(); + } + + async configure() { + const configureXml = ``; + await this.usbdev.write(new TextEncoder().encode(configureXml)); + + const response = await this.readResponse(); + if (!response || !response.includes('ACK')) { + throw new Error('Configure failed'); + } + + return true; + } + + async readResponse() { + const chunks = []; + while (true) { + const data = await this.usbdev.readWithTimeout(4096, 1000); + if (!data || data.length === 0) break; + chunks.push(data); + if (data[data.length - 1] === 0x0A) break; // \n + } + + if (chunks.length === 0) return null; + + const response = new TextDecoder().decode(window.QDLUtils.concatUint8Array(chunks)); + return response.trim(); + } + + async cmdProgram(lun, sector, blob, onProgress) { + const sectorSize = this.cfg.SECTOR_SIZE_IN_BYTES; + const maxPayloadSize = this.cfg.MAX_PAYLOAD_SIZE_IN_BYTES; + let offset = 0; + + while (offset < blob.size) { + const chunkSize = Math.min(maxPayloadSize, blob.size - offset); + const numSectors = Math.ceil(chunkSize / sectorSize); + + const programXml = ``; + await this.usbdev.write(new TextEncoder().encode(programXml)); + + const chunk = await blob.slice(offset, offset + chunkSize).arrayBuffer(); + await this.usbdev.write(new Uint8Array(chunk)); + + const response = await this.readResponse(); + if (!response || !response.includes('ACK')) { + throw new Error('Program failed'); + } + + offset += chunkSize; + if (onProgress) onProgress(offset / blob.size); + } + + return true; + } + + async cmdReadBuffer(lun, sector, numSectors) { + const readXml = ``; + await this.usbdev.write(new TextEncoder().encode(readXml)); + + const data = await this.usbdev.readWithTimeout(numSectors * this.cfg.SECTOR_SIZE_IN_BYTES, 5000); + if (!data) { + return { resp: false, error: 'Read failed' }; + } + + const response = await this.readResponse(); + if (!response || !response.includes('ACK')) { + return { resp: false, error: 'Read response failed' }; + } + + return { resp: true, data: data }; + } + + async cmdErase(lun, sector, numSectors) { + const eraseXml = ``; + await this.usbdev.write(new TextEncoder().encode(eraseXml)); + + const response = await this.readResponse(); + if (!response || !response.includes('ACK')) { + throw new Error('Erase failed'); + } + + return true; + } + + async cmdSetBootLunId(bootLunId) { + const setBootLunXml = ``; + await this.usbdev.write(new TextEncoder().encode(setBootLunXml)); + + const response = await this.readResponse(); + if (!response || !response.includes('ACK')) { + throw new Error('Set boot lun failed'); + } + + return true; + } + + async cmdReset() { + const resetXml = ``; + await this.usbdev.write(new TextEncoder().encode(resetXml)); + + const response = await this.readResponse(); + if (!response || !response.includes('ACK')) { + throw new Error('Reset failed'); + } + + return true; + } + } + + // Public API + return { + Firehose: Firehose + }; +})(); diff --git a/src/QDL/gpt-jquery.js b/src/QDL/gpt-jquery.js new file mode 100644 index 0000000..8fcbd5d --- /dev/null +++ b/src/QDL/gpt-jquery.js @@ -0,0 +1,146 @@ +// Global GPT utilities object +window.GPTUtils = (function() { + // Constants + const AB_FLAG_OFFSET = 48; + const AB_PARTITION_ATTR_SLOT_ACTIVE = 0x1; + const AB_PARTITION_ATTR_BOOT_SUCCESSFUL = 0x2; + const AB_PARTITION_ATTR_UNBOOTABLE = 0x4; + + class gptHeader { + constructor(data) { + const view = new DataView(data.buffer); + this.signature = data.slice(0, 8); + this.revision = view.getUint32(8, true); + this.headerSize = view.getUint32(12, true); + this.headerCrc32 = view.getUint32(16, true); + this.reserved = view.getUint32(20, true); + this.currentLba = view.getBigUint64(24, true); + this.backupLba = view.getBigUint64(32, true); + this.firstUsableLba = view.getBigUint64(40, true); + this.lastUsableLba = view.getBigUint64(48, true); + this.diskGuid = data.slice(56, 72); + this.partEntryStartLba = view.getBigUint64(72, true); + this.numPartEntries = view.getUint32(80, true); + this.partEntrySize = view.getUint32(84, true); + this.partArrayCrc32 = view.getUint32(88, true); + } + } + + class gptPartition { + constructor(data) { + const view = new DataView(data.buffer); + this.type = data.slice(0, 16); + this.guid = data.slice(16, 32); + this.sector = Number(view.getBigUint64(32, true)); + this.sectors = Number(view.getBigUint64(40, true)); + this.flags = view.getBigUint64(48, true); + + // Convert name from UTF-16LE to string + const nameBytes = data.slice(56, 128); + const nameArray = []; + for (let i = 0; i < nameBytes.length; i += 2) { + const code = (nameBytes[i+1] << 8) | nameBytes[i]; + if (code === 0) break; + nameArray.push(code); + } + this.name = String.fromCharCode(...nameArray); + } + + create() { + const data = new Uint8Array(128); + const view = new DataView(data.buffer); + + data.set(this.type, 0); + data.set(this.guid, 16); + view.setBigUint64(32, BigInt(this.sector), true); + view.setBigUint64(40, BigInt(this.sectors), true); + view.setBigUint64(48, this.flags, true); + + // Convert name to UTF-16LE + const encoder = new TextEncoder(); + const nameBytes = encoder.encode(this.name); + for (let i = 0; i < nameBytes.length && i < 36; i++) { + data[56 + i*2] = nameBytes[i]; + data[56 + i*2 + 1] = 0; + } + + return data; + } + } + + class gpt { + constructor() { + this.header = null; + this.partentries = {}; + } + + parseHeader(data, sectorSize) { + this.header = new gptHeader(data.slice(sectorSize)); + return this.header; + } + + parse(data, sectorSize) { + const partitionTableOffset = Number(this.header.partEntryStartLba) * sectorSize; + + for (let i = 0; i < this.header.numPartEntries; i++) { + const offset = partitionTableOffset + (i * this.header.partEntrySize); + const partitionData = data.slice(offset, offset + this.header.partEntrySize); + + // Skip empty partitions + if (partitionData.every(b => b === 0)) continue; + + const partition = new gptPartition(partitionData); + if (partition.name) { + this.partentries[partition.name] = { + sector: partition.sector, + sectors: partition.sectors, + flags: partition.flags, + entryOffset: offset + }; + } + } + } + + fixGptCrc(data) { + // Implementation of CRC32 calculation and fixing + // This would need to be implemented if needed + console.warn('GPT CRC fixing not implemented'); + } + } + + function setPartitionFlags(flags, active, isBoot) { + let newFlags = flags; + if (active) { + newFlags |= BigInt(AB_PARTITION_ATTR_SLOT_ACTIVE); + if (isBoot) { + newFlags |= BigInt(AB_PARTITION_ATTR_BOOT_SUCCESSFUL); + } + } else { + newFlags &= ~BigInt(AB_PARTITION_ATTR_SLOT_ACTIVE); + if (isBoot) { + newFlags |= BigInt(AB_PARTITION_ATTR_UNBOOTABLE); + } + } + return newFlags; + } + + function ensureGptHdrConsistency(gptData, backupGptData, guidGpt, backupGuidGpt) { + // Implementation of GPT header consistency check + // This would need to be implemented if needed + console.warn('GPT header consistency check not implemented'); + return gptData; + } + + // Public API + return { + gpt: gpt, + gptHeader: gptHeader, + gptPartition: gptPartition, + setPartitionFlags: setPartitionFlags, + ensureGptHdrConsistency: ensureGptHdrConsistency, + AB_FLAG_OFFSET: AB_FLAG_OFFSET, + AB_PARTITION_ATTR_SLOT_ACTIVE: AB_PARTITION_ATTR_SLOT_ACTIVE, + AB_PARTITION_ATTR_BOOT_SUCCESSFUL: AB_PARTITION_ATTR_BOOT_SUCCESSFUL, + AB_PARTITION_ATTR_UNBOOTABLE: AB_PARTITION_ATTR_UNBOOTABLE + }; +})(); diff --git a/src/QDL/qdl-jquery.js b/src/QDL/qdl-jquery.js new file mode 100644 index 0000000..179693c --- /dev/null +++ b/src/QDL/qdl-jquery.js @@ -0,0 +1,297 @@ +// Global QDL device class +window.qdlDevice = (function() { + class QDLDevice { + constructor() { + this.mode = ""; + this.cdc = new window.USBLib.usbClass(); + this.sahara = new window.SaharaProtocol.Sahara(this.cdc); + this.firehose = new window.FirehoseProtocol.Firehose(this.cdc); + this._connectResolve = null; + this._connectReject = null; + } + + async waitForConnect() { + return await new Promise((resolve, reject) => { + this._connectResolve = resolve; + this._connectReject = reject; + }); + } + + async connectToSahara() { + while (!this.cdc.connected) { + await this.cdc?.connect(); + if (this.cdc.connected) { + console.log("QDL device detected"); + let resp = await window.QDLUtils.runWithTimeout(this.sahara?.connect(), 10000); + if ("mode" in resp) { + this.mode = resp["mode"]; + console.log("Mode detected:", this.mode); + return resp; + } + } + } + return {"mode": "error"}; + } + + async connect() { + try { + let resp = await this.connectToSahara(); + let mode = resp["mode"]; + if (mode === "sahara") { + await this.sahara?.uploadLoader(); + } else if (mode === "error") { + throw "Error connecting to Sahara"; + } + await this.firehose?.configure(); + this.mode = "firehose"; + } catch (error) { + if (this._connectReject !== null) { + this._connectReject(error); + this._connectResolve = null; + this._connectReject = null; + } + } + + if (this._connectResolve !== null) { + this._connectResolve(undefined); + this._connectResolve = null; + this._connectReject = null; + } + return true; + } + + async getGpt(lun, startSector=1) { + let resp; + resp = await this.firehose.cmdReadBuffer(lun, 0, 1); + if (!resp.resp) { + console.error(resp.error); + return [null, null]; + } + let data = window.QDLUtils.concatUint8Array([resp.data, (await this.firehose.cmdReadBuffer(lun, startSector, 1)).data]); + let guidGpt = new window.GPTUtils.gpt(); + const header = guidGpt.parseHeader(data, this.firehose.cfg.SECTOR_SIZE_IN_BYTES); + if (window.QDLUtils.containsBytes("EFI PART", header.signature)) { + const partTableSize = header.numPartEntries * header.partEntrySize; + const sectors = Math.floor(partTableSize / this.firehose.cfg.SECTOR_SIZE_IN_BYTES); + data = window.QDLUtils.concatUint8Array([data, (await this.firehose.cmdReadBuffer(lun, header.partEntryStartLba, sectors)).data]); + guidGpt.parse(data, this.firehose.cfg.SECTOR_SIZE_IN_BYTES); + return [guidGpt, data]; + } else { + throw "Error reading gpt header"; + } + } + + async detectPartition(partitionName, sendFull=false) { + const luns = this.firehose.luns; + for (const lun of luns) { + const [guidGpt, data] = await this.getGpt(lun); + if (guidGpt === null) { + break; + } else { + if (partitionName in guidGpt.partentries) { + return sendFull ? [true, lun, data, guidGpt] : [true, lun, guidGpt.partentries[partitionName]]; + } + } + } + return [false]; + } + + async flashBlob(partitionName, blob, onProgress=()=>{}) { + let startSector = 0; + let dp = await this.detectPartition(partitionName); + const found = dp[0]; + if (found) { + let lun = dp[1]; + const imgSize = blob.size; + let imgSectors = Math.floor(imgSize / this.firehose.cfg.SECTOR_SIZE_IN_BYTES); + if (imgSize % this.firehose.cfg.SECTOR_SIZE_IN_BYTES !== 0) { + imgSectors += 1; + } + if (partitionName.toLowerCase() !== "gpt") { + const partition = dp[2]; + if (imgSectors > partition.sectors) { + console.error("partition has fewer sectors compared to the flashing image"); + return false; + } + startSector = partition.sector; + console.log(`Flashing ${partitionName}...`); + if (await this.firehose.cmdProgram(lun, startSector, blob, (progress) => onProgress(progress))) { + console.log(`partition ${partitionName}: startSector ${partition.sector}, sectors ${partition.sectors}`); + } else { + throw `Error while writing ${partitionName}`; + } + } + } else { + throw `Can't find partition ${partitionName}`; + } + return true; + } + + async erase(partitionName) { + const luns = this.firehose.luns; + for (const lun of luns) { + let [guidGpt] = await this.getGpt(lun); + if (partitionName in guidGpt.partentries) { + const partition = guidGpt.partentries[partitionName]; + console.log(`Erasing ${partitionName}...`); + await this.firehose.cmdErase(lun, partition.sector, partition.sectors); + console.log(`Erased ${partitionName} starting at sector ${partition.sector} with sectors ${partition.sectors}`); + } else { + continue; + } + } + return true; + } + + async getDevicePartitionsInfo() { + const slots = []; + const partitions = []; + const luns = this.firehose.luns; + for (const lun of luns) { + let [guidGpt] = await this.getGpt(lun); + if (guidGpt === null) { + throw "Error while reading device partitions"; + } + for (let partition in guidGpt.partentries) { + let slot = partition.slice(-2); + if (slot === "_a" || slot === "_b") { + partition = partition.substring(0, partition.length-2); + if (!slots.includes(slot)) { + slots.push(slot); + } + } + if (!partitions.includes(partition)) { + partitions.push(partition); + } + } + } + return [slots.length, partitions]; + } + + async getActiveSlot() { + const luns = this.firehose.luns; + for (const lun of luns) { + const [guidGpt] = await this.getGpt(lun); + if (guidGpt === null) { + throw "Cannot get active slot." + } + for (const partitionName in guidGpt.partentries) { + const slot = partitionName.slice(-2); + // backup gpt header is more reliable, since it would always has the non-corrupted gpt header + const [backupGuidGpt] = await this.getGpt(lun, guidGpt.header.backupLba); + const partition = backupGuidGpt.partentries[partitionName]; + const active = (((BigInt(partition.flags) >> (BigInt(window.GPTUtils.AB_FLAG_OFFSET) * BigInt(8)))) + & BigInt(window.GPTUtils.AB_PARTITION_ATTR_SLOT_ACTIVE)) === BigInt(window.GPTUtils.AB_PARTITION_ATTR_SLOT_ACTIVE); + if (slot == "_a" && active) { + return "a"; + } else if (slot == "_b" && active) { + return "b"; + } + } + } + throw "Can't detect slot A or B"; + } + + async setActiveSlot(slot) { + slot = slot.toLowerCase(); + const luns = this.firehose.luns; + let slot_a_status, slot_b_status; + + if (slot == "a") { + slot_a_status = true; + } else if (slot == "b") { + slot_a_status = false; + } + slot_b_status = !slot_a_status; + + for (const lunA of luns) { + let checkGptHeader = false; + let sameLun = false; + let hasPartitionA = false; + let [guidGptA, gptDataA] = await this.getGpt(lunA); + let [backupGuidGptA, backupGptDataA] = await this.getGpt(lunA, guidGptA.header.backupLba); + let lunB, gptDataB, guidGptB, backupGptDataB, backupGuidGptB; + + if (guidGptA === null) { + throw "Error while getting gpt header data"; + } + for (const partitionNameA in guidGptA.partentries) { + let slotSuffix = partitionNameA.toLowerCase().slice(-2); + if (slotSuffix !== "_a") { + continue; + } + const partitionNameB = partitionNameA.slice(0, partitionNameA.length-1) + "b"; + let sts; + if (!checkGptHeader) { + hasPartitionA = true; + if (partitionNameB in guidGptA.partentries) { + lunB = lunA; + sameLun = true; + gptDataB = gptDataA; + guidGptB = guidGptA; + backupGptDataB = backupGptDataA; + backupGuidGptB = backupGuidGptA; + } else { + const resp = await this.detectPartition(partitionNameB, true); + sts = resp[0]; + if (!sts) { + throw `Cannot find partition ${partitionNameB}`; + } + [sts, lunB, gptDataB, guidGptB] = resp; + [backupGuidGptB, backupGptDataB] = await this.getGpt(lunB, guidGptB.header.backupLba); + } + } + + if (!checkGptHeader && partitionNameA.slice(0, 3) !== "xbl") { // xbl partitions aren't affected by failure of changing slot, saves time + gptDataA = window.GPTUtils.ensureGptHdrConsistency(gptDataA, backupGptDataA, guidGptA, backupGuidGptA); + if (!sameLun) { + gptDataB = window.GPTUtils.ensureGptHdrConsistency(gptDataB, backupGptDataB, guidGptB, backupGuidGptB); + } + checkGptHeader = true; + } + + const partA = guidGptA.partentries[partitionNameA]; + const partB = guidGptB.partentries[partitionNameB]; + + let isBoot = false; + if (partitionNameA === "boot_a") { + isBoot = true; + } + const [pDataA, pOffsetA, pDataB, pOffsetB] = this.patchNewGptData( + gptDataA, gptDataB, guidGptA, partA, partB, slot_a_status, slot_b_status, isBoot + ); + + gptDataA.set(pDataA, pOffsetA); + guidGptA.fixGptCrc(gptDataA); + if (lunA === lunB) { + gptDataB = gptDataA; + } + gptDataB.set(pDataB, pOffsetB); + guidGptB.fixGptCrc(gptDataB); + } + + if (!hasPartitionA) { + continue; + } + const writeOffset = this.firehose.cfg.SECTOR_SIZE_IN_BYTES; + const gptBlobA = new Blob([gptDataA.slice(writeOffset)]); + await this.firehose.cmdProgram(lunA, 1, gptBlobA); + if (!sameLun) { + const gptBlobB = new Blob([gptDataB.slice(writeOffset)]); + await this.firehose.cmdProgram(lunB, 1, gptBlobB); + } + } + const activeBootLunId = (slot === "a") ? 1 : 2; + await this.firehose.cmdSetBootLunId(activeBootLunId); + console.log(`Successfully set slot ${slot} active`); + return true; + } + + async reset() { + await this.firehose.cmdReset(); + return true; + } + } + + return QDLDevice; +})(); diff --git a/src/QDL/sahara-jquery.js b/src/QDL/sahara-jquery.js new file mode 100644 index 0000000..fd35b01 --- /dev/null +++ b/src/QDL/sahara-jquery.js @@ -0,0 +1,180 @@ +// Global Sahara protocol object +window.SaharaProtocol = (function() { + // Constants from saharaDefs.js + const SAHARA_HELLO_REQ = 0x1; + const SAHARA_HELLO_RSP = 0x2; + const SAHARA_READ_DATA = 0x3; + const SAHARA_END_TRANSFER = 0x4; + const SAHARA_DONE = 0x5; + const SAHARA_DONE_RSP = 0x6; + const SAHARA_RESET = 0x7; + const SAHARA_RESET_RSP = 0x8; + const SAHARA_MEMORY_DEBUG = 0x9; + const SAHARA_MEMORY_READ = 0xA; + const SAHARA_CMD_READY = 0xB; + const SAHARA_SWITCH_MODE = 0xC; + const SAHARA_EXECUTE_REQ = 0xD; + const SAHARA_EXECUTE_RSP = 0xE; + const SAHARA_EXECUTE_DATA = 0xF; + const SAHARA_64BIT_MEMORY_DEBUG = 0x10; + const SAHARA_64BIT_MEMORY_READ = 0x11; + const SAHARA_64BIT_MEMORY_READ_DATA = 0x12; + + const SAHARA_MODE_IMAGE_TX_PENDING = 0x0; + const SAHARA_MODE_IMAGE_TX_COMPLETE = 0x1; + const SAHARA_MODE_MEMORY_DEBUG = 0x2; + const SAHARA_MODE_COMMAND = 0x3; + + class Sahara { + constructor(usbdev) { + this.usbdev = usbdev; + this.serial = null; + } + + async readHello() { + const data = await this.usbdev.readWithTimeout(0x30, 1000); + if (!data) return null; + + const view = new DataView(data.buffer); + const cmd = view.getUint32(0, true); + const len = view.getUint32(4, true); + const ver = view.getUint32(8, true); + const ver_min = view.getUint32(12, true); + const max_cmd_len = view.getUint32(16, true); + const mode = view.getUint32(20, true); + + // Extract serial number if present (44 bytes) + if (data.length >= 0x30) { + const serialBytes = data.slice(24, 24 + 44); + const nullIndex = serialBytes.findIndex(b => b === 0); + this.serial = new TextDecoder().decode( + serialBytes.slice(0, nullIndex !== -1 ? nullIndex : undefined) + ); + } + + return { + cmd: cmd, + len: len, + ver: ver, + ver_min: ver_min, + max_cmd_len: max_cmd_len, + mode: mode + }; + } + + async writeHello(mode = SAHARA_MODE_IMAGE_TX_PENDING) { + const data = new ArrayBuffer(0x30); + const view = new DataView(data); + + view.setUint32(0, SAHARA_HELLO_RSP, true); // cmd + view.setUint32(4, 0x30, true); // length + view.setUint32(8, 2, true); // version + view.setUint32(12, 1, true); // version_min + view.setUint32(16, 0x1000, true); // max_cmd_len + view.setUint32(20, mode, true); // mode + + return await this.usbdev.write(new Uint8Array(data)); + } + + async readData() { + const data = await this.usbdev.readWithTimeout(0x10, 1000); + if (!data) return null; + + const view = new DataView(data.buffer); + return { + cmd: view.getUint32(0, true), + len: view.getUint32(4, true), + offset: view.getUint32(8, true), + size: view.getUint32(12, true) + }; + } + + async writeData(offset, size, data) { + const header = new ArrayBuffer(0x10); + const view = new DataView(header); + + view.setUint32(0, SAHARA_READ_DATA, true); + view.setUint32(4, size + 0x10, true); + view.setUint32(8, offset, true); + view.setUint32(12, size, true); + + await this.usbdev.write(new Uint8Array(header)); + return await this.usbdev.write(data); + } + + async writeDone() { + const data = new ArrayBuffer(0x8); + const view = new DataView(data); + + view.setUint32(0, SAHARA_DONE, true); + view.setUint32(4, 0x8, true); + + return await this.usbdev.write(new Uint8Array(data)); + } + + async readDone() { + const data = await this.usbdev.readWithTimeout(0x8, 1000); + if (!data) return null; + + const view = new DataView(data.buffer); + return { + cmd: view.getUint32(0, true), + len: view.getUint32(4, true) + }; + } + + async connect() { + const hello = await this.readHello(); + if (!hello) { + throw new Error("Failed to read hello packet"); + } + + if (hello.cmd === SAHARA_HELLO_REQ) { + await this.writeHello(); + return { mode: "sahara" }; + } else if (hello.cmd === SAHARA_CMD_READY) { + return { mode: "streaming" }; + } + + return { mode: "error" }; + } + + async uploadLoader() { + // Load the programmer binary + const response = await fetch('QDL/sdm845_fhprg.bin'); + const programmerData = new Uint8Array(await response.arrayBuffer()); + + while (true) { + const request = await this.readData(); + if (!request) { + throw new Error("Failed to read data request"); + } + + if (request.cmd === SAHARA_READ_DATA) { + const chunk = programmerData.slice(request.offset, request.offset + request.size); + await this.writeData(request.offset, request.size, chunk); + } else if (request.cmd === SAHARA_END_TRANSFER) { + break; + } else { + throw new Error("Unexpected command during upload"); + } + } + + await this.writeDone(); + const done = await this.readDone(); + if (!done || done.cmd !== SAHARA_DONE_RSP) { + throw new Error("Upload failed"); + } + } + } + + // Public API + return { + Sahara: Sahara, + // Constants + SAHARA_MODE_IMAGE_TX_PENDING, + SAHARA_MODE_IMAGE_TX_COMPLETE, + SAHARA_MODE_MEMORY_DEBUG, + SAHARA_MODE_COMMAND + }; +})(); diff --git a/src/QDL/saharaDefs-jquery.js b/src/QDL/saharaDefs-jquery.js new file mode 100644 index 0000000..6931367 --- /dev/null +++ b/src/QDL/saharaDefs-jquery.js @@ -0,0 +1,60 @@ +// Global Sahara protocol definitions +window.SaharaDefs = { + // Commands + SAHARA_HELLO_REQ: 0x1, + SAHARA_HELLO_RSP: 0x2, + SAHARA_READ_DATA: 0x3, + SAHARA_END_TRANSFER: 0x4, + SAHARA_DONE: 0x5, + SAHARA_DONE_RSP: 0x6, + SAHARA_RESET: 0x7, + SAHARA_RESET_RSP: 0x8, + SAHARA_MEMORY_DEBUG: 0x9, + SAHARA_MEMORY_READ: 0xA, + SAHARA_CMD_READY: 0xB, + SAHARA_SWITCH_MODE: 0xC, + SAHARA_EXECUTE_REQ: 0xD, + SAHARA_EXECUTE_RSP: 0xE, + SAHARA_EXECUTE_DATA: 0xF, + SAHARA_64BIT_MEMORY_DEBUG: 0x10, + SAHARA_64BIT_MEMORY_READ: 0x11, + SAHARA_64BIT_MEMORY_READ_DATA: 0x12, + + // Modes + SAHARA_MODE_IMAGE_TX_PENDING: 0x0, + SAHARA_MODE_IMAGE_TX_COMPLETE: 0x1, + SAHARA_MODE_MEMORY_DEBUG: 0x2, + SAHARA_MODE_COMMAND: 0x3, + + // Status + SAHARA_STATUS_SUCCESS: 0x00, + SAHARA_NAK_INVALID_CMD: 0x01, + SAHARA_NAK_PROTOCOL_MISMATCH: 0x02, + SAHARA_NAK_INVALID_TARGET_PROTOCOL: 0x03, + SAHARA_NAK_INVALID_HOST_PROTOCOL: 0x04, + SAHARA_NAK_INVALID_PACKET_SIZE: 0x05, + SAHARA_NAK_UNEXPECTED_IMAGE_ID: 0x06, + SAHARA_NAK_INVALID_HEADER_SIZE: 0x07, + SAHARA_NAK_INVALID_DATA_SIZE: 0x08, + SAHARA_NAK_INVALID_IMAGE_TYPE: 0x09, + SAHARA_NAK_INVALID_TX_LENGTH: 0x0A, + SAHARA_NAK_INVALID_RX_LENGTH: 0x0B, + SAHARA_NAK_GENERAL_ERROR: 0x0C, + SAHARA_NAK_READ_DATA_ERROR: 0x0D, + SAHARA_NAK_UNSUPPORTED_NUM_PHDRS: 0x0E, + SAHARA_NAK_INVALID_PDHR_SIZE: 0x0F, + SAHARA_NAK_MULTIPLE_SHARED_SEG: 0x10, + SAHARA_NAK_UNINIT_PHDR_LOC: 0x11, + SAHARA_NAK_INVALID_DEST_ADDR: 0x12, + SAHARA_NAK_INVALID_IMAGE_HDR_SIZE: 0x13, + SAHARA_NAK_INVALID_IMAGE_HDR_DATA: 0x14, + SAHARA_NAK_INVALID_IMG_SIZE: 0x15, + SAHARA_NAK_FIRMWARE_ERROR: 0x16, + SAHARA_NAK_REMOTE_PROC_ERROR: 0x17, + SAHARA_NAK_ERROR_PACKET_TIMEOUT: 0x18, + SAHARA_NAK_ERROR_PACKET_LENGTH: 0x19, + SAHARA_NAK_ERROR_PACKET_DATA: 0x1A, + SAHARA_NAK_ERROR_UNKNOWN: 0x1B, + SAHARA_NAK_ERROR_AUTH_FAIL: 0x1C, + SAHARA_NAK_ERROR_TRANSMISSION: 0x1D +}; diff --git a/src/QDL/sparse-jquery.js b/src/QDL/sparse-jquery.js new file mode 100644 index 0000000..b6bb25d --- /dev/null +++ b/src/QDL/sparse-jquery.js @@ -0,0 +1,139 @@ +// Global sparse image handling utility +window.SparseUtils = (function() { + // Sparse image format constants + const SPARSE_HEADER_MAGIC = 0xed26ff3a; + const SPARSE_HEADER_MAJOR_VER = 1; + const SPARSE_HEADER_MINOR_VER = 0; + const SPARSE_HEADER_SIZE = 28; + const CHUNK_HEADER_SIZE = 12; + + // Chunk types + const CHUNK_TYPE_RAW = 0xCAC1; + const CHUNK_TYPE_FILL = 0xCAC2; + const CHUNK_TYPE_DONT_CARE = 0xCAC3; + const CHUNK_TYPE_CRC32 = 0xCAC4; + + class SparseImage { + constructor(data) { + this.data = data; + this.offset = 0; + this.totalBlocks = 0; + this.chunks = []; + this.parse(); + } + + readUint32() { + const value = new DataView(this.data.buffer).getUint32(this.offset, true); + this.offset += 4; + return value; + } + + parse() { + // Parse header + const magic = this.readUint32(); + if (magic !== SPARSE_HEADER_MAGIC) { + throw new Error('Invalid sparse image magic'); + } + + const majorVersion = this.readUint32(); + const minorVersion = this.readUint32(); + const fileHeaderSize = this.readUint32(); + const chunkHeaderSize = this.readUint32(); + const blockSize = this.readUint32(); + const totalBlocks = this.readUint32(); + const totalChunks = this.readUint32(); + this.readUint32(); // Skip CRC + + if (majorVersion !== SPARSE_HEADER_MAJOR_VER || + minorVersion !== SPARSE_HEADER_MINOR_VER || + fileHeaderSize !== SPARSE_HEADER_SIZE || + chunkHeaderSize !== CHUNK_HEADER_SIZE) { + throw new Error('Unsupported sparse image version'); + } + + this.blockSize = blockSize; + this.totalBlocks = totalBlocks; + + // Parse chunks + for (let i = 0; i < totalChunks; i++) { + const chunkType = this.readUint32(); + const chunkBlocks = this.readUint32(); + const chunkDataSize = this.readUint32(); + + this.chunks.push({ + type: chunkType, + blocks: chunkBlocks, + dataSize: chunkDataSize, + offset: this.offset + }); + + this.offset += chunkDataSize; + } + } + + async unpack() { + const totalBytes = this.totalBlocks * this.blockSize; + const output = new Uint8Array(totalBytes); + let outputOffset = 0; + + for (const chunk of this.chunks) { + const chunkBytes = chunk.blocks * this.blockSize; + + switch (chunk.type) { + case CHUNK_TYPE_RAW: + // Copy raw data + output.set( + new Uint8Array(this.data.buffer, chunk.offset, chunk.dataSize), + outputOffset + ); + break; + + case CHUNK_TYPE_FILL: + // Fill with 4-byte pattern + const pattern = new Uint32Array(this.data.buffer, chunk.offset, 1)[0]; + const patternBytes = new Uint8Array(4); + new DataView(patternBytes.buffer).setUint32(0, pattern, true); + + for (let i = 0; i < chunkBytes; i += 4) { + output.set(patternBytes, outputOffset + i); + } + break; + + case CHUNK_TYPE_DONT_CARE: + // Fill with zeros + output.fill(0, outputOffset, outputOffset + chunkBytes); + break; + + case CHUNK_TYPE_CRC32: + // Skip CRC chunks + break; + + default: + throw new Error(`Unknown chunk type: ${chunk.type}`); + } + + outputOffset += chunkBytes; + } + + return output; + } + + static isSparse(data) { + if (data.length < 4) return false; + const magic = new DataView(data.buffer).getUint32(0, true); + return magic === SPARSE_HEADER_MAGIC; + } + } + + // Public API + return { + SparseImage: SparseImage, + isSparse: SparseImage.isSparse, + // Constants + SPARSE_HEADER_MAGIC, + CHUNK_TYPE_RAW, + CHUNK_TYPE_FILL, + CHUNK_TYPE_DONT_CARE, + CHUNK_TYPE_CRC32 + }; +})(); diff --git a/src/QDL/usblib-jquery.js b/src/QDL/usblib-jquery.js new file mode 100644 index 0000000..feed043 --- /dev/null +++ b/src/QDL/usblib-jquery.js @@ -0,0 +1,93 @@ +// Global USB library object +window.USBLib = (function() { + class usbClass { + constructor() { + this.device = null; + this.interface = null; + this.endpointIn = null; + this.endpointOut = null; + this.connected = false; + } + + async connect() { + try { + this.device = await navigator.usb.requestDevice({ + filters: [ + { vendorId: 0x05c6, productId: 0x9008 }, // Qualcomm QDL + { vendorId: 0x18d1, productId: 0xd00d } // Google Fastboot + ] + }); + + await this.device.open(); + await this.device.selectConfiguration(1); + await this.device.claimInterface(0); + + this.interface = this.device.configuration.interfaces[0]; + this.endpointIn = this.interface.alternate.endpoints.find(e => e.direction === "in"); + this.endpointOut = this.interface.alternate.endpoints.find(e => e.direction === "out"); + + if (!this.endpointIn || !this.endpointOut) { + throw new Error("Device endpoints not found"); + } + + this.connected = true; + return true; + } catch (error) { + console.error("USB connection error:", error); + this.connected = false; + return false; + } + } + + async write(data) { + if (!this.connected) { + throw new Error("Device not connected"); + } + + try { + const result = await this.device.transferOut(this.endpointOut.endpointNumber, data); + return result.status === 'ok'; + } catch (error) { + console.error("USB write error:", error); + return false; + } + } + + async read(length) { + if (!this.connected) { + throw new Error("Device not connected"); + } + + try { + const result = await this.device.transferIn(this.endpointIn.endpointNumber, length); + if (result.status === 'ok') { + return new Uint8Array(result.data.buffer); + } + return null; + } catch (error) { + console.error("USB read error:", error); + return null; + } + } + + async readWithTimeout(length, timeout) { + return await window.QDLUtils.runWithTimeout(this.read(length), timeout); + } + + disconnect() { + if (this.device) { + this.device.close(); + } + this.device = null; + this.interface = null; + this.endpointIn = null; + this.endpointOut = null; + this.connected = false; + } + } + + // Public API + return { + usbClass: usbClass + }; +})(); diff --git a/src/QDL/utils-jquery.js b/src/QDL/utils-jquery.js new file mode 100644 index 0000000..78132a7 --- /dev/null +++ b/src/QDL/utils-jquery.js @@ -0,0 +1,53 @@ +// Global QDL utilities object +window.QDLUtils = (function() { + function concatUint8Array(arrays) { + const totalLength = arrays.reduce((acc, value) => acc + value.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + for (const array of arrays) { + result.set(array, offset); + offset += array.length; + } + return result; + } + + function containsBytes(needle, haystack) { + const needleBytes = new TextEncoder().encode(needle); + for (let i = 0; i <= haystack.length - needleBytes.length; i++) { + let found = true; + for (let j = 0; j < needleBytes.length; j++) { + if (haystack[i + j] !== needleBytes[j]) { + found = false; + break; + } + } + if (found) return true; + } + return false; + } + + async function runWithTimeout(promise, timeout) { + let timeoutId; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject(new Error(`Operation timed out after ${timeout}ms`)); + }, timeout); + }); + + try { + const result = await Promise.race([promise, timeoutPromise]); + clearTimeout(timeoutId); + return result; + } catch (error) { + clearTimeout(timeoutId); + throw error; + } + } + + // Public API + return { + concatUint8Array: concatUint8Array, + containsBytes: containsBytes, + runWithTimeout: runWithTimeout + }; +})(); diff --git a/src/QDL/xmlParser-jquery.js b/src/QDL/xmlParser-jquery.js new file mode 100644 index 0000000..bf66352 --- /dev/null +++ b/src/QDL/xmlParser-jquery.js @@ -0,0 +1,84 @@ +// Global XML parser utility object +window.XMLParserUtils = (function() { + class Parser { + constructor() { + this.parser = new DOMParser(); + } + + parse(xmlString) { + try { + const doc = this.parser.parseFromString(xmlString, 'text/xml'); + if (doc.documentElement.nodeName === 'parsererror') { + console.error('XML Parse Error:', doc.documentElement.textContent); + return null; + } + return this.nodeToObject(doc.documentElement); + } catch (error) { + console.error('XML Parse Error:', error); + return null; + } + } + + nodeToObject(node) { + const obj = { + tag: node.nodeName, + attributes: {}, + children: [] + }; + + // Get attributes + Array.from(node.attributes || []).forEach(attr => { + obj.attributes[attr.name] = attr.value; + }); + + // Get child nodes + Array.from(node.childNodes).forEach(child => { + if (child.nodeType === Node.ELEMENT_NODE) { + obj.children.push(this.nodeToObject(child)); + } else if (child.nodeType === Node.TEXT_NODE && child.textContent.trim()) { + obj.text = child.textContent.trim(); + } + }); + + return obj; + } + + findTag(obj, tagName) { + if (obj.tag.toLowerCase() === tagName.toLowerCase()) { + return obj; + } + + for (const child of obj.children) { + const found = this.findTag(child, tagName); + if (found) return found; + } + + return null; + } + + getAttribute(obj, attrName) { + return obj.attributes[attrName]; + } + + getText(obj) { + return obj.text || ''; + } + + hasAttribute(obj, attrName) { + return attrName in obj.attributes; + } + + getAttributeNames(obj) { + return Object.keys(obj.attributes); + } + + getChildren(obj) { + return obj.children; + } + } + + // Public API + return { + Parser: Parser + }; +})(); diff --git a/src/config-jquery.js b/src/config-jquery.js new file mode 100644 index 0000000..5af16a3 --- /dev/null +++ b/src/config-jquery.js @@ -0,0 +1,10 @@ +// Global configuration object +window.AppConfig = { + manifests: { + release: 'https://raw.githubusercontent.com/commaai/openpilot/release3/system/hardware/tici/agnos.json', + master: 'https://raw.githubusercontent.com/commaai/openpilot/master/system/hardware/tici/agnos.json' + }, + loader: { + url: 'https://raw.githubusercontent.com/commaai/flash/master/src/QDL/sdm845_fhprg.bin' + } +}; diff --git a/src/main-jquery.js b/src/main-jquery.js new file mode 100644 index 0000000..fecc777 --- /dev/null +++ b/src/main-jquery.js @@ -0,0 +1,297 @@ +// Constants +const Step = { + INITIALIZING: 0, + READY: 1, + CONNECTING: 2, + DOWNLOADING: 3, + UNPACKING: 4, + FLASHING: 6, + ERASING: 7, + DONE: 8 +}; + +const Error = { + UNKNOWN: -1, + NONE: 0, + UNRECOGNIZED_DEVICE: 1, + LOST_CONNECTION: 2, + DOWNLOAD_FAILED: 3, + UNPACK_FAILED: 4, + CHECKSUM_MISMATCH: 5, + FLASH_FAILED: 6, + ERASE_FAILED: 7, + REQUIREMENTS_NOT_MET: 8 +}; + +// UI States configuration +const UI_STATES = { + initializing: { + icon: 'src/assets/cloud.svg', + text: 'Initializing...', + description: '', + bgColor: '#9ca3af' + }, + ready: { + icon: 'src/assets/bolt.svg', + text: 'Ready', + description: 'Tap the button above to begin', + bgColor: '#51ff00' + }, + connecting: { + icon: 'src/assets/cable.svg', + text: 'Waiting for connection', + description: 'Follow the instructions to connect your device', + bgColor: '#eab308' + }, + downloading: { + icon: 'src/assets/cloud_download.svg', + text: 'Downloading...', + description: 'Do not unplug your device', + bgColor: '#3b82f6' + }, + flashing: { + icon: 'src/assets/system_update_c3.svg', + text: 'Flashing device...', + description: 'Do not unplug your device until the process is complete.', + bgColor: '#84cc16' + }, + done: { + icon: 'src/assets/done.svg', + text: 'Done', + description: 'Your device has been updated successfully.', + bgColor: '#22c55e' + } +}; + +const ERROR_STATES = { + requirements_not_met: { + icon: 'src/assets/exclamation.svg', + text: 'Requirements not met', + description: 'Your system does not meet the requirements to flash your device.', + bgColor: '#ef4444' + }, + unrecognized_device: { + icon: 'src/assets/device_question_c3.svg', + text: 'Unrecognized device', + description: 'The device connected to your computer is not supported.', + bgColor: '#eab308' + }, + lost_connection: { + icon: 'src/assets/cable.svg', + text: 'Lost connection', + description: 'The connection to your device was lost. Check your cables and try again.', + bgColor: '#ef4444' + }, + flash_failed: { + icon: 'src/assets/device_exclamation_c3.svg', + text: 'Flash failed', + description: 'The system image could not be flashed to your device.', + bgColor: '#ef4444' + } +}; + +class FlashApp { + constructor() { + this.step = Step.INITIALIZING; + this.error = Error.NONE; + this.progress = -1; + this.message = ''; + this.connected = false; + this.serial = null; + + // Cache jQuery selectors + this.$flash = $('#flash'); + this.$statusIcon = $('.status-icon'); + this.$progressContainer = $('.progress-container'); + this.$progressBar = $('.progress-bar'); + this.$statusText = $('.status-text'); + this.$statusDesc = $('.status-description'); + this.$retryButton = $('.retry-button'); + this.$deviceState = $('.device-state'); + + this.bindEvents(); + this.initialize(); + } + + bindEvents() { + this.$statusIcon.on('click', () => { + if (this.step === Step.READY) { + this.startConnection(); + } + }); + + this.$retryButton.on('click', () => { + window.location.reload(); + }); + + // Prevent leaving page during flash + $(window).on('beforeunload', (e) => { + if (Step.DOWNLOADING <= this.step && this.step <= Step.ERASING) { + e.preventDefault(); + return 'Flash in progress. Are you sure you want to leave?'; + } + }); + } + + async initialize() { + try { + // Check browser requirements + if (typeof navigator.usb === 'undefined') { + throw new Error('WebUSB not supported'); + } + + // Check configuration + if (!window.AppConfig || !window.AppConfig.manifests) { + throw new Error('Configuration not loaded'); + } + + // Initialize QDL device + if (typeof window.qdlDevice !== 'undefined') { + this.qdl = new window.qdlDevice(); + } else { + throw new Error('QDL support not available'); + } + + // Load manifest + const manifestUrl = window.AppConfig.manifests.release; + try { + console.debug('[QDL] Downloading manifest from', manifestUrl); + const manifestBlob = await window.BlobUtils.download(manifestUrl); + const manifestText = await manifestBlob.text(); + console.debug('[QDL] Manifest content:', manifestText); + + try { + this.manifest = window.ManifestUtils.createManifest(manifestText); + if (!Array.isArray(this.manifest) || this.manifest.length === 0) { + throw new Error('Invalid manifest format'); + } + console.debug('[QDL] Parsed manifest:', this.manifest); + this.updateUI('ready'); + } catch (parseErr) { + console.error('[QDL] Manifest parse error:', parseErr); + throw new Error(`Failed to parse manifest: ${parseErr.message}`); + } + } catch (manifestErr) { + console.error('[QDL] Manifest download/parse error:', manifestErr); + throw new Error(`Failed to load manifest: ${manifestErr.message}`); + } + } catch (err) { + console.error('[QDL] Initialization error:', err); + this.handleError('requirements_not_met'); + } + } + + async startConnection() { + this.updateUI('connecting'); + try { + await this.qdl.connect(); + const [slotCount, partitions] = await this.qdl.getDevicePartitionsInfo(); + + if (!this.isRecognizedDevice(slotCount, partitions)) { + this.handleError('unrecognized_device'); + return; + } + + this.serial = this.qdl.sahara.serial || 'unknown'; + this.connected = true; + this.updateDeviceState(); + + await this.startFlashing(); + } catch (err) { + console.error('[QDL] Connection error:', err); + this.handleError('lost_connection'); + } + } + + async startFlashing() { + this.updateUI('downloading'); + try { + // Download and flash process + for (const image of this.manifest) { + this.$statusText.text(`Downloading ${image.name}...`); + const blob = await window.BlobUtils.download(image.archiveUrl); + + this.updateUI('flashing'); + this.$statusText.text(`Flashing ${image.name}...`); + await this.qdl.flashBlob(image.name, blob, (progress) => { + this.updateProgress(progress); + }); + } + + this.updateUI('done'); + } catch (err) { + console.error('[QDL] Flash error:', err); + this.handleError('flash_failed'); + } + } + + updateUI(state) { + const uiState = UI_STATES[state]; + if (!uiState) return; + + this.$statusIcon + .find('img') + .attr('src', uiState.icon) + .toggleClass('animate-pulse', state !== 'done' && !this.error); + + this.$statusIcon.css('background-color', uiState.bgColor); + this.$statusText.text(uiState.text); + this.$statusDesc.text(uiState.description); + } + + updateProgress(value) { + if (value === -1) { + this.$progressContainer.css('opacity', 0); + } else { + this.$progressContainer.css('opacity', 1); + this.$progressBar.css('transform', `translateX(${(value * 100 - 100)}%)`); + } + } + + updateDeviceState() { + if (this.connected) { + this.$deviceState.show().find('.serial-number').text(this.serial); + } else { + this.$deviceState.hide(); + } + } + + handleError(type) { + const errorState = ERROR_STATES[type]; + if (!errorState) return; + + this.$statusIcon + .find('img') + .attr('src', errorState.icon) + .removeClass('animate-pulse'); + + this.$statusIcon.css('background-color', errorState.bgColor); + this.$statusText.text(errorState.text); + this.$statusDesc.text(errorState.description); + this.$retryButton.show(); + } + + isRecognizedDevice(slotCount, partitions) { + if (slotCount !== 2) { + console.error('[QDL] Unrecognised device (slotCount)'); + return false; + } + + const expectedPartitions = [ + "ALIGN_TO_128K_1", "ALIGN_TO_128K_2", "ImageFv", "abl", "aop", "apdp", "bluetooth", + "boot", "cache", "cdt", "cmnlib", "cmnlib64", "ddr", "devcfg", "devinfo", "dip", + "dsp", "fdemeta", "frp", "fsc", "fsg", "hyp", "keymaster", "keystore", "limits", + "logdump", "logfs", "mdtp", "mdtpsecapp", "misc", "modem", "modemst1", "modemst2", + "msadp", "persist", "qupfw", "rawdump", "sec", "splash", "spunvm", "ssd", "sti", + "storsec", "system", "systemrw", "toolsfv", "tz", "userdata", "vm-linux", + "vm-system", "xbl", "xbl_config" + ]; + + return partitions.every(partition => expectedPartitions.includes(partition)); + } +} + +// Initialize when document is ready +$(document).ready(() => { + new FlashApp(); +}); diff --git a/src/styles-jquery.css b/src/styles-jquery.css new file mode 100644 index 0000000..39ba2b9 --- /dev/null +++ b/src/styles-jquery.css @@ -0,0 +1,251 @@ +:root { + --primary-color: #51ff00; + --error-color: #ef4444; + --warning-color: #eab308; + --success-color: #22c55e; + --bg-dark: #111827; + --bg-light: #ffffff; + --text-dark: #1f2937; + --text-light: #f9fafb; +} + +/* Base Styles */ +body { + margin: 0; + font-family: 'Inter', sans-serif; + line-height: 1.5; + color: var(--text-dark); +} + +.container { + display: flex; + min-height: 100vh; +} + +/* Left Content Area */ +.content-left { + padding: 3rem 4rem; + max-width: 65ch; + background: var(--bg-light); +} + +.content-left h1 { + font-size: 2.25rem; + font-weight: 600; + margin-top: 1.5rem; +} + +.content-left h2 { + font-size: 1.875rem; + font-weight: 500; + margin-top: 2rem; +} + +.content-left h3 { + font-size: 1.5rem; + font-weight: 500; + margin-top: 1.5rem; +} + +.content-left p, .content-left ul, .content-left ol { + margin: 1rem 0; +} + +.content-left code { + font-family: 'JetBrains Mono', monospace; + background: #f3f4f6; + padding: 0.2rem 0.4rem; + border-radius: 0.25rem; +} + +.content-left hr { + margin: 2rem 0; + border: none; + border-top: 1px solid #e5e7eb; +} + +.content-left img { + max-width: 100%; + height: auto; + margin: 1rem 0; +} + +/* Flash Container */ +.flash-container { + flex: 1; + background: #f3f4f6; + display: flex; + justify-content: center; + align-items: center; +} + +.flash-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 2rem; + padding: 2rem; + position: relative; +} + +/* Status Icon */ +.status-icon { + padding: 2rem; + border-radius: 9999px; + cursor: pointer; + transition: background-color 0.3s; +} + +.status-icon img { + width: 128px; + height: 128px; +} + +/* Progress Bar */ +.progress-container { + width: 100%; + max-width: 48rem; + height: 0.5rem; + background: #e5e7eb; + border-radius: 9999px; + overflow: hidden; + opacity: 0; + transition: opacity 0.3s; +} + +.progress-container.active { + opacity: 1; +} + +.progress-bar { + height: 100%; + width: 100%; + background: var(--primary-color); + transform: translateX(-100%); + transition: transform 0.3s; +} + +/* Status Text */ +.status-text { + font-family: 'JetBrains Mono', monospace; + font-size: 1.875rem; + font-weight: 300; +} + +.status-description { + font-size: 1.25rem; + text-align: center; + max-width: 36rem; + padding: 0 2rem; +} + +/* Retry Button */ +.retry-button { + padding: 0.5rem 1rem; + border-radius: 0.375rem; + background: #e5e7eb; + border: none; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.2s; +} + +.retry-button:hover { + background: #d1d5db; +} + +/* Device State */ +.device-state { + position: absolute; + bottom: 1rem; + left: 50%; + transform: translateX(-50%); + background: white; + padding: 1rem; + border-radius: 0.375rem; + display: flex; + gap: 0.5rem; + min-width: 350px; + border: 1px solid #e5e7eb; +} + +.usb-indicator, .serial-indicator { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.usb-indicator svg { + color: var(--primary-color); +} + +/* Dark Mode */ +@media (prefers-color-scheme: dark) { + body { + background: var(--bg-dark); + color: var(--text-light); + } + + .content-left { + background: var(--bg-dark); + } + + .content-left code { + background: #374151; + color: var(--text-light); + } + + .content-left hr { + border-color: #374151; + } + + .flash-container { + background: #1f2937; + } + + .device-state { + background: #374151; + border-color: #4b5563; + color: var(--text-light); + } + + .retry-button { + background: #374151; + color: var(--text-light); + } + + .retry-button:hover { + background: #4b5563; + } + + .dark-invert { + filter: invert(1); + } +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .container { + flex-direction: column; + } + + .content-left { + padding: 2rem; + max-width: none; + } + + .flash-container { + min-height: 700px; + } +} + +@media (max-width: 640px) { + .content-left { + padding: 1rem; + } + + .device-state { + width: calc(100% - 2rem); + min-width: 0; + } +} diff --git a/src/utils/blob-jquery.js b/src/utils/blob-jquery.js new file mode 100644 index 0000000..e7f2451 --- /dev/null +++ b/src/utils/blob-jquery.js @@ -0,0 +1,38 @@ +// Global blob utility object +window.BlobUtils = (function() { + /** + * Downloads a blob from a URL + * @param {string} url - The URL to download from + * @returns {Promise} Promise resolving to the downloaded blob + */ + async function download(url) { + const response = await fetch(url, { mode: 'cors' }); + const reader = response.body.getReader(); + const contentLength = +response.headers.get('Content-Length'); + console.debug('[blob] Downloading', url, contentLength); + + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + + const blob = new Blob(chunks); + console.debug('[blob] Downloaded', url, blob.size); + if (blob.size !== contentLength) { + console.warn('[blob] Download size mismatch', { + url, + expected: contentLength, + actual: blob.size, + }); + } + + return blob; + } + + // Public API + return { + download: download + }; +})(); diff --git a/src/utils/manifest-jquery.js b/src/utils/manifest-jquery.js new file mode 100644 index 0000000..b5cc658 --- /dev/null +++ b/src/utils/manifest-jquery.js @@ -0,0 +1,68 @@ +// Global manifest utility object +window.ManifestUtils = (function() { + /** + * Represents a partition image defined in the AGNOS manifest. + * Image archives can be retrieved from archiveUrl. + */ + class Image { + constructor(json) { + this.name = json.name; + this.sparse = json.sparse; + + // before AGNOS 11 - flash alt skip-chunks image + // after AGNOS 11 - flash main non-sparse image + if (this.name === 'system' && this.sparse && json.alt) { + this.checksum = json.alt.hash; + this.fileName = `${this.name}-skip-chunks-${json.hash_raw}.img`; + this.archiveUrl = json.alt.url; + this.size = json.alt.size; + } else { + this.checksum = json.hash; + this.fileName = `${this.name}-${json.hash_raw}.img`; + this.archiveUrl = json.url; + this.size = json.size; + } + + this.archiveFileName = this.archiveUrl.split('/').pop(); + } + } + + /** + * Creates a manifest from JSON text + * @param {string} text - The JSON text to parse + * @returns {Image[]} Array of Image objects + */ + function createManifest(text) { + const expectedPartitions = ['aop', 'devcfg', 'xbl', 'xbl_config', 'abl', 'boot', 'system']; + const partitions = JSON.parse(text).map((image) => new Image(image)); + + // Sort into consistent order + partitions.sort((a, b) => expectedPartitions.indexOf(a.name) - expectedPartitions.indexOf(b.name)); + + // Check that all partitions are present + const missingPartitions = expectedPartitions.filter((name) => !partitions.some((image) => image.name === name)); + if (missingPartitions.length > 0) { + throw new Error(`Manifest is missing partitions: ${missingPartitions.join(', ')}`); + } + + return partitions; + } + + /** + * Fetches and creates a manifest from a URL + * @param {string} url - The URL to fetch the manifest from + * @returns {Promise} Promise resolving to array of Image objects + */ + function getManifest(url) { + return fetch(url) + .then((response) => response.text()) + .then(createManifest); + } + + // Public API + return { + Image: Image, + createManifest: createManifest, + getManifest: getManifest + }; +})(); diff --git a/tailwind.config.js b/tailwind.config.js index f1f20e2..3de114e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -2,6 +2,7 @@ export default { content: [ './index.html', + './index-jquery.html', './src/**/*.{js,jsx}', ], theme: {