|
| 1 | +import { FlashManager, StepCode, ErrorCode } from '../utils/manager' |
| 2 | +import { createImageManager } from '../utils/image' |
| 3 | +import { isLinux } from '../utils/platform' |
| 4 | +import config from '../config' |
| 5 | + |
| 6 | +import bolt from '../assets/bolt.svg' |
| 7 | +import cable from '../assets/cable.svg' |
| 8 | +import deviceExclamation from '../assets/device_exclamation_c3.svg' |
| 9 | +import deviceQuestion from '../assets/device_question_c3.svg' |
| 10 | +import done from '../assets/done.svg' |
| 11 | +import exclamation from '../assets/exclamation.svg' |
| 12 | +import systemUpdate from '../assets/system_update_c3.svg' |
| 13 | + |
| 14 | +const steps = { |
| 15 | + [StepCode.INITIALIZING]: { |
| 16 | + status: 'Initializing...', |
| 17 | + bgColor: 'bg-gray-400 dark:bg-gray-700', |
| 18 | + icon: bolt, |
| 19 | + }, |
| 20 | + [StepCode.READY]: { |
| 21 | + status: 'Tap to start', |
| 22 | + bgColor: 'bg-[#51ff00]', |
| 23 | + icon: bolt, |
| 24 | + iconStyle: '', |
| 25 | + }, |
| 26 | + [StepCode.CONNECTING]: { |
| 27 | + status: 'Waiting for connection', |
| 28 | + description: 'Follow the instructions to connect your device to your computer', |
| 29 | + bgColor: 'bg-yellow-500', |
| 30 | + icon: cable, |
| 31 | + }, |
| 32 | + [StepCode.REPAIR_PARTITION_TABLES]: { |
| 33 | + status: 'Repairing partition tables...', |
| 34 | + description: 'Do not unplug your device until the process is complete', |
| 35 | + bgColor: 'bg-lime-400', |
| 36 | + icon: systemUpdate, |
| 37 | + }, |
| 38 | + [StepCode.ERASE_DEVICE]: { |
| 39 | + status: 'Erasing device...', |
| 40 | + description: 'Do not unplug your device until the process is complete', |
| 41 | + bgColor: 'bg-lime-400', |
| 42 | + icon: systemUpdate, |
| 43 | + }, |
| 44 | + [StepCode.FLASH_SYSTEM]: { |
| 45 | + status: 'Flashing device...', |
| 46 | + description: 'Do not unplug your device until the process is complete', |
| 47 | + bgColor: 'bg-lime-400', |
| 48 | + icon: systemUpdate, |
| 49 | + }, |
| 50 | + [StepCode.FINALIZING]: { |
| 51 | + status: 'Finalizing...', |
| 52 | + description: 'Do not unplug your device until the process is complete', |
| 53 | + bgColor: 'bg-lime-400', |
| 54 | + icon: systemUpdate, |
| 55 | + }, |
| 56 | + [StepCode.DONE]: { |
| 57 | + status: 'Done', |
| 58 | + description: 'Your device was flashed successfully. It should now boot into the openpilot setup.', |
| 59 | + bgColor: 'bg-green-500', |
| 60 | + icon: done, |
| 61 | + }, |
| 62 | +} |
| 63 | + |
| 64 | +const errors = { |
| 65 | + [ErrorCode.UNKNOWN]: { |
| 66 | + status: 'Unknown error', |
| 67 | + description: 'An unknown error has occurred. Unplug your device, restart your browser and try again.', |
| 68 | + bgColor: 'bg-red-500', |
| 69 | + icon: exclamation, |
| 70 | + }, |
| 71 | + [ErrorCode.REQUIREMENTS_NOT_MET]: { |
| 72 | + status: 'Requirements not met', |
| 73 | + description: 'Your system does not meet the requirements to flash your device. Make sure to use a browser which supports WebUSB and is up to date.', |
| 74 | + }, |
| 75 | + [ErrorCode.STORAGE_SPACE]: { |
| 76 | + description: 'Your system does not have enough space available to download AGNOS. Your browser may be restricting the available space if you are in a private, incognito or guest session.', |
| 77 | + }, |
| 78 | + [ErrorCode.UNRECOGNIZED_DEVICE]: { |
| 79 | + status: 'Unrecognized device', |
| 80 | + description: 'The device connected to your computer is not supported. Try using a different cable, USB port, or computer. If the problem persists, join the #hw-three-3x channel on Discord for help.', |
| 81 | + bgColor: 'bg-yellow-500', |
| 82 | + icon: deviceQuestion, |
| 83 | + }, |
| 84 | + [ErrorCode.LOST_CONNECTION]: { |
| 85 | + status: 'Lost connection', |
| 86 | + description: 'The connection to your device was lost. Unplug your device and try again.', |
| 87 | + icon: cable, |
| 88 | + }, |
| 89 | + [ErrorCode.REPAIR_PARTITION_TABLES_FAILED]: { |
| 90 | + status: 'Repairing partition tables failed', |
| 91 | + description: 'Your device\'s partition tables could not be repaired. Try using a different cable, USB port, or computer. If the problem persists, join the #hw-three-3x channel on Discord for help.', |
| 92 | + icon: deviceExclamation, |
| 93 | + }, |
| 94 | + [ErrorCode.ERASE_FAILED]: { |
| 95 | + status: 'Erase failed', |
| 96 | + description: 'The device could not be erased. Try using a different cable, USB port, or computer. If the problem persists, join the #hw-three-3x channel on Discord for help.', |
| 97 | + icon: deviceExclamation, |
| 98 | + }, |
| 99 | + [ErrorCode.FLASH_SYSTEM_FAILED]: { |
| 100 | + status: 'Flash failed', |
| 101 | + description: 'AGNOS could not be flashed to your device. Try using a different cable, USB port, or computer. If the problem persists, join the #hw-three-3x channel on Discord for help.', |
| 102 | + icon: deviceExclamation, |
| 103 | + }, |
| 104 | +} |
| 105 | + |
| 106 | +if (isLinux) { |
| 107 | + errors[ErrorCode.LOST_CONNECTION].description += ' Did you forget to unbind the device from qcserial?' |
| 108 | +} |
| 109 | + |
| 110 | +function beforeUnloadListener(event) { |
| 111 | + event.preventDefault() |
| 112 | + return (event.returnValue = "Flash in progress. Are you sure you want to leave?") |
| 113 | +} |
| 114 | + |
| 115 | +class FlashView extends HTMLElement { |
| 116 | + constructor() { |
| 117 | + super() |
| 118 | + this.step = StepCode.INITIALIZING |
| 119 | + this.message = '' |
| 120 | + this.progress = -1 |
| 121 | + this.error = ErrorCode.NONE |
| 122 | + this.connected = false |
| 123 | + this.serial = null |
| 124 | + |
| 125 | + this.qdlManager = null |
| 126 | + this.imageManager = createImageManager() |
| 127 | + } |
| 128 | + |
| 129 | + connectedCallback() { |
| 130 | + this.render() |
| 131 | + fetch(config.loader.url) |
| 132 | + .then(res => res.arrayBuffer()) |
| 133 | + .then(programmer => { |
| 134 | + this.qdlManager = new FlashManager(config.manifests.release, programmer, { |
| 135 | + onStepChange: s => { this.step = s; this.render() }, |
| 136 | + onMessageChange: m => { this.message = m; this.render() }, |
| 137 | + onProgressChange: p => { this.progress = p; this.render() }, |
| 138 | + onErrorChange: e => { this.error = e; this.render() }, |
| 139 | + onConnectionChange: c => { this.connected = c; this.render() }, |
| 140 | + onSerialChange: s => { this.serial = s; this.render() }, |
| 141 | + }) |
| 142 | + return this.qdlManager.initialize(this.imageManager) |
| 143 | + }) |
| 144 | + .catch(err => { |
| 145 | + console.error('Error initializing Flash manager:', err) |
| 146 | + this.error = ErrorCode.UNKNOWN |
| 147 | + this.render() |
| 148 | + }) |
| 149 | + |
| 150 | + this.addEventListener('click', this.handleClick) |
| 151 | + } |
| 152 | + |
| 153 | + disconnectedCallback() { |
| 154 | + this.removeEventListener('click', this.handleClick) |
| 155 | + window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true }) |
| 156 | + } |
| 157 | + |
| 158 | + handleClick = (e) => { |
| 159 | + const { id } = e.target |
| 160 | + if (id === 'flash-start' && this.canStart()) { |
| 161 | + this.qdlManager?.start() |
| 162 | + } else if (id === 'flash-retry') { |
| 163 | + window.location.reload() |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + canStart() { |
| 168 | + return this.step === StepCode.READY && !this.error |
| 169 | + } |
| 170 | + |
| 171 | + render() { |
| 172 | + const uiState = { ...steps[this.step] } |
| 173 | + if (this.error) { |
| 174 | + Object.assign(uiState, errors[ErrorCode.UNKNOWN], errors[this.error]) |
| 175 | + } |
| 176 | + const { status, description, bgColor, icon, iconStyle = 'invert' } = uiState |
| 177 | + |
| 178 | + let title |
| 179 | + if (this.message && !this.error) { |
| 180 | + title = this.message + '...' |
| 181 | + if (this.progress >= 0) { |
| 182 | + title += ` (${(this.progress * 100).toFixed(0)}%)` |
| 183 | + } |
| 184 | + } else if (this.error === ErrorCode.STORAGE_SPACE) { |
| 185 | + title = this.message |
| 186 | + } else { |
| 187 | + title = status |
| 188 | + } |
| 189 | + |
| 190 | + if (this.step >= StepCode.REPAIR_PARTITION_TABLES && this.step <= StepCode.FINALIZING) { |
| 191 | + window.addEventListener('beforeunload', beforeUnloadListener, { capture: true }) |
| 192 | + } else { |
| 193 | + window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true }) |
| 194 | + } |
| 195 | + |
| 196 | + this.innerHTML = ` |
| 197 | + <div class="relative flex flex-col gap-8 justify-center items-center h-full"> |
| 198 | + <div id="flash-start" class="p-8 rounded-full ${bgColor}" style="cursor: ${this.canStart() ? 'pointer' : 'default'}"> |
| 199 | + <img src="${icon}" alt="cable" width="128" height="128" class="${iconStyle} ${!this.error && this.step !== StepCode.DONE ? 'animate-pulse' : ''}" /> |
| 200 | + </div> |
| 201 | + <div class="w-full max-w-3xl px-8 transition-opacity duration-300" style="opacity: ${this.progress === -1 ? 0 : 1}"> |
| 202 | + <div class="relative w-full h-2 bg-gray-200 rounded-full overflow-hidden"> |
| 203 | + <div class="absolute top-0 bottom-0 left-0 w-full transition-all ${bgColor}" style="transform: translateX(${(this.progress * 100) - 100}%)"></div> |
| 204 | + </div> |
| 205 | + </div> |
| 206 | + <span class="text-3xl dark:text-white font-mono font-light">${title}</span> |
| 207 | + <span class="text-xl dark:text-white px-8 max-w-xl">${description || ''}</span> |
| 208 | + ${this.error ? `<button id="flash-retry" class="px-4 py-2 rounded-md bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 transition-colors">Retry</button>` : ''} |
| 209 | + ${this.connected ? ` |
| 210 | + <div class="absolute bottom-0 m-0 lg:m-4 p-4 w-full sm:w-auto sm:min-w-[350px] sm:border sm:border-gray-200 dark:sm:border-gray-600 bg-white dark:bg-gray-700 text-black dark:text-white rounded-md flex flex-row gap-2" style="left:50%; transform:translate(-50%, -50%);"> |
| 211 | + <div class="flex flex-row gap-2"> |
| 212 | + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 96 960 960" class="text-green-500 dark:text-[#51ff00]" height="24" width="24"><path fill="currentColor" d="M480 976q-32 0-52-20t-20-52q0-22 11-40t31-29V724H302q-24 0-42-18t-18-42V555q-20-9-31-26.609-11-17.608-11-40.108Q200 456 220 436t52-20q32 0 52 20t20 52.411Q344 511 333 528.5T302 555v109h148V324h-80l110-149 110 149h-80v340h148V560h-42V416h144v144h-42v104q0 24-18 42t-42 18H510v111q19.95 10.652 30.975 29.826Q552 884 552 904q0 32-20 52t-52 20Z"/></svg> |
| 213 | + Device connected |
| 214 | + </div> |
| 215 | + <span class="text-gray-400">|</span> |
| 216 | + <div class="flex flex-row gap-2"> |
| 217 | + <span>Serial:<span class="ml-2 font-mono">${this.serial || 'unknown'}</span></span> |
| 218 | + </div> |
| 219 | + </div>` : ''} |
| 220 | + </div> |
| 221 | + ` |
| 222 | + } |
| 223 | +} |
| 224 | + |
| 225 | +customElements.define('flash-view', FlashView) |
| 226 | +export default FlashView |
| 227 | + |
0 commit comments