Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta http-equiv="Permissions-Policy" content="serial=(self)"> | |
| <title>ESP32/Arduino Code Uploader</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.43.0/min/vs/loader.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| #editor { | |
| height: 400px; | |
| border: 1px solid #e5e7eb; | |
| border-radius: 0.375rem; | |
| } | |
| #serialMonitor { | |
| height: 200px; | |
| overflow-y: auto; | |
| background-color: #1e293b; | |
| color: #f8fafc; | |
| font-family: monospace; | |
| padding: 0.5rem; | |
| border-radius: 0.375rem; | |
| } | |
| .tab-active { | |
| border-bottom: 2px solid #3b82f6; | |
| color: #3b82f6; | |
| } | |
| .progress-bar { | |
| transition: width 0.3s ease; | |
| } | |
| .serial-line { | |
| margin: 0; | |
| padding: 0; | |
| line-height: 1.2; | |
| } | |
| .blink { | |
| animation: blink 1s step-end infinite; | |
| } | |
| @keyframes blink { | |
| from, to { opacity: 1 } | |
| 50% { opacity: 0.5 } | |
| } | |
| .port-option { | |
| display: flex; | |
| align-items: center; | |
| } | |
| .port-icon { | |
| margin-right: 8px; | |
| color: #4b5563; | |
| } | |
| .permission-modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: rgba(0,0,0,0.5); | |
| z-index: 1000; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .permission-content { | |
| background-color: white; | |
| padding: 2rem; | |
| border-radius: 0.5rem; | |
| max-width: 500px; | |
| width: 90%; | |
| } | |
| .browser-warning { | |
| background-color: #fef3c7; | |
| border-left: 4px solid #f59e0b; | |
| padding: 1rem; | |
| margin-bottom: 1rem; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 min-h-screen"> | |
| <!-- Permission Request Modal --> | |
| <div id="permissionModal" class="permission-modal"> | |
| <div class="permission-content"> | |
| <h2 class="text-xl font-bold mb-4">Serial Port Access Required</h2> | |
| <p class="mb-4">To connect to your device, you need to grant permission to access serial ports. Please click the button below and select your device from the browser's prompt.</p> | |
| <div class="flex flex-col space-y-4"> | |
| <button id="requestPermissionBtn" class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-6 rounded-md flex items-center justify-center"> | |
| <i class="fas fa-key mr-2"></i> Grant Permission | |
| </button> | |
| <button id="learnMoreBtn" class="text-blue-600 hover:text-blue-800 py-2 px-6 rounded-md flex items-center justify-center"> | |
| <i class="fas fa-info-circle mr-2"></i> Learn More About Serial Access | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="mb-8"> | |
| <h1 class="text-3xl font-bold text-gray-800 flex items-center"> | |
| <i class="fas fa-microchip mr-3 text-blue-500"></i> | |
| ESP32/Arduino Code Uploader | |
| </h1> | |
| <p class="text-gray-600 mt-2">Upload and manage your sketches for ESP32 and Arduino devices</p> | |
| </header> | |
| <!-- Browser Warning (hidden by default) --> | |
| <div id="browserWarning" class="browser-warning hidden"> | |
| <div class="flex items-start"> | |
| <div class="flex-shrink-0"> | |
| <i class="fas fa-exclamation-triangle text-yellow-600"></i> | |
| </div> | |
| <div class="ml-3"> | |
| <h3 class="text-sm font-medium text-yellow-800">Browser Compatibility Notice</h3> | |
| <div class="mt-2 text-sm text-yellow-700"> | |
| <p>This application requires the Web Serial API which is currently only supported in Chrome/Edge 89+ and Opera 76+.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
| <!-- Left Column - Connection & Upload --> | |
| <div class="lg:col-span-1 space-y-6"> | |
| <!-- Connection Panel --> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i class="fas fa-plug mr-2 text-green-500"></i> | |
| Device Connection | |
| </h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Port</label> | |
| <div class="flex"> | |
| <select id="portSelect" class="flex-grow border border-gray-300 rounded-l-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <option value="">Select a port</option> | |
| </select> | |
| <button id="refreshPorts" class="bg-gray-200 hover:bg-gray-300 px-3 py-2 rounded-r-md border border-l-0 border-gray-300"> | |
| <i class="fas fa-sync-alt"></i> | |
| </button> | |
| </div> | |
| <p id="portHelp" class="mt-1 text-xs text-gray-500">Connect your device and click refresh</p> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Board Type</label> | |
| <select id="boardSelect" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <optgroup label="ESP32 Boards"> | |
| <option value="esp32">ESP32 Dev Module</option> | |
| <option value="esp32s2">ESP32-S2</option> | |
| <option value="esp32c3">ESP32-C3</option> | |
| <option value="esp32s3">ESP32-S3</option> | |
| <option value="cyd_esp32">CYD ESP32-2432S028</option> | |
| </optgroup> | |
| <optgroup label="Arduino Boards"> | |
| <option value="uno">Arduino UNO</option> | |
| <option value="nano">Arduino Nano</option> | |
| <option value="mega">Arduino Mega 2560</option> | |
| </optgroup> | |
| </select> | |
| </div> | |
| <div id="boardOptions" class="hidden"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Board Options</label> | |
| <div id="specificOptions" class="space-y-2"> | |
| <!-- Options will be populated based on board selection --> | |
| </div> | |
| </div> | |
| <div class="pt-2"> | |
| <button id="connectBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed"> | |
| <i class="fas fa-link mr-2"></i> | |
| Connect | |
| </button> | |
| </div> | |
| <div id="connectionStatus" class="hidden mt-3 p-3 rounded-md bg-gray-100 text-gray-700 flex items-center"> | |
| <i class="fas fa-circle mr-2 text-gray-400"></i> | |
| <span>Disconnected</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Upload Panel --> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i class="fas fa-upload mr-2 text-purple-500"></i> | |
| Upload Sketch | |
| </h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Sketch File</label> | |
| <div class="flex items-center"> | |
| <input type="file" id="fileInput" accept=".ino,.cpp,.h" class="hidden"> | |
| <input type="text" id="fileName" placeholder="No file selected" readonly class="flex-grow border border-gray-300 rounded-l-md px-3 py-2 focus:outline-none"> | |
| <button id="browseBtn" class="bg-gray-200 hover:bg-gray-300 px-3 py-2 rounded-r-md border border-l-0 border-gray-300"> | |
| <i class="fas fa-folder-open"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="uploadProgress" class="hidden"> | |
| <div class="flex justify-between text-sm text-gray-600 mb-1"> | |
| <span>Uploading...</span> | |
| <span id="progressPercent">0%</span> | |
| </div> | |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> | |
| <div id="progressBar" class="progress-bar bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="pt-2"> | |
| <button id="uploadBtn" disabled class="w-full bg-purple-600 hover:bg-purple-700 text-white py-2 px-4 rounded-md flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed"> | |
| <i class="fas fa-cloud-upload-alt mr-2"></i> | |
| Upload to Device | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Column - Editor & Serial Monitor --> | |
| <div class="lg:col-span-2 space-y-6"> | |
| <!-- Code Editor Tabs --> | |
| <div class="bg-white rounded-lg shadow"> | |
| <div class="border-b border-gray-200"> | |
| <nav class="-mb-px flex"> | |
| <button id="editorTab" class="tab-active py-4 px-6 text-sm font-medium flex items-center"> | |
| <i class="fas fa-code mr-2"></i> | |
| Code Editor | |
| </button> | |
| <button id="serialTab" class="py-4 px-6 text-sm font-medium text-gray-500 hover:text-gray-700 flex items-center"> | |
| <i class="fas fa-terminal mr-2"></i> | |
| Serial Monitor | |
| </button> | |
| </nav> | |
| </div> | |
| <!-- Editor Content --> | |
| <div id="editorContent" class="p-4"> | |
| <div id="editor"></div> | |
| <div class="mt-4 flex justify-between"> | |
| <div> | |
| <button id="newFileBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-700 py-1 px-3 rounded-md text-sm mr-2"> | |
| <i class="fas fa-file mr-1"></i> New | |
| </button> | |
| <button id="saveFileBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-700 py-1 px-3 rounded-md text-sm"> | |
| <i class="fas fa-save mr-1"></i> Save | |
| </button> | |
| </div> | |
| <div> | |
| <button id="verifyBtn" class="bg-blue-100 hover:bg-blue-200 text-blue-700 py-1 px-3 rounded-md text-sm"> | |
| <i class="fas fa-check-circle mr-1"></i> Verify | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Serial Monitor Content --> | |
| <div id="serialContent" class="hidden p-4"> | |
| <div id="serialMonitor"></div> | |
| <div class="mt-4 flex"> | |
| <input type="text" id="serialInput" placeholder="Enter command..." class="flex-grow border border-gray-300 rounded-l-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <button id="serialSendBtn" class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-r-md"> | |
| Send | |
| </button> | |
| </div> | |
| <div class="mt-3 flex justify-between items-center"> | |
| <div class="flex items-center"> | |
| <label class="inline-flex items-center"> | |
| <input type="checkbox" id="autoScroll" checked class="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"> | |
| <span class="ml-2 text-sm text-gray-600">Auto-scroll</span> | |
| </label> | |
| <label class="inline-flex items-center ml-4"> | |
| <input type="checkbox" id="showTimestamps" class="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"> | |
| <span class="ml-2 text-sm text-gray-600">Timestamps</span> | |
| </label> | |
| </div> | |
| <div> | |
| <button id="clearSerialBtn" class="text-sm text-gray-600 hover:text-gray-800 flex items-center"> | |
| <i class="fas fa-trash-alt mr-1"></i> Clear | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Output Console --> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i class="fas fa-terminal mr-2 text-yellow-500"></i> | |
| Compilation Output | |
| </h2> | |
| <div id="outputConsole" class="bg-gray-800 text-green-100 font-mono text-sm p-3 rounded-md h-40 overflow-y-auto"> | |
| <p>> Ready to compile and upload code to your device</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Board configurations | |
| const boardConfigurations = { | |
| // ESP32 Boards | |
| 'esp32': { | |
| name: 'ESP32 Dev Module', | |
| baudRate: 115200, | |
| flashMode: 'dio', | |
| flashFreq: '80m', | |
| uploadSpeed: 921600, | |
| partitionScheme: 'default', | |
| cpuFreq: '240', | |
| flashSize: '4MB', | |
| options: [ | |
| { id: 'flashMode', label: 'Flash Mode', type: 'select', options: ['qio', 'qout', 'dio', 'dout'], default: 'dio' }, | |
| { id: 'flashFreq', label: 'Flash Frequency', type: 'select', options: ['80m', '40m'], default: '80m' }, | |
| { id: 'uploadSpeed', label: 'Upload Speed', type: 'select', options: ['115200', '230400', '460800', '921600'], default: '921600' } | |
| ] | |
| }, | |
| 'cyd_esp32': { | |
| name: 'CYD ESP32-2432S028', | |
| baudRate: 115200, | |
| flashMode: 'dio', | |
| flashFreq: '80m', | |
| uploadSpeed: 460800, | |
| partitionScheme: 'huge_app', | |
| cpuFreq: '240', | |
| flashSize: '16MB', | |
| options: [ | |
| { id: 'flashMode', label: 'Flash Mode', type: 'select', options: ['dio', 'dout'], default: 'dio' }, | |
| { id: 'flashFreq', label: 'Flash Frequency', type: 'select', options: ['80m', '40m'], default: '80m' }, | |
| { id: 'uploadSpeed', label: 'Upload Speed', type: 'select', options: ['115200', '230400', '460800'], default: '460800' }, | |
| { id: 'touchScreen', label: 'Touch Screen', type: 'checkbox', default: true } | |
| ] | |
| }, | |
| // Arduino Boards | |
| 'uno': { | |
| name: 'Arduino UNO', | |
| baudRate: 9600, | |
| processor: 'atmega328p', | |
| programmer: 'arduino', | |
| options: [ | |
| { id: 'programmer', label: 'Programmer', type: 'select', options: ['arduino', 'avrisp', 'usbtiny'], default: 'arduino' } | |
| ] | |
| }, | |
| 'nano': { | |
| name: 'Arduino Nano', | |
| baudRate: 9600, | |
| processor: 'atmega328p', | |
| programmer: 'arduino', | |
| options: [ | |
| { id: 'processor', label: 'Processor', type: 'select', options: ['atmega328p', 'atmega328'], default: 'atmega328p' }, | |
| { id: 'programmer', label: 'Programmer', type: 'select', options: ['arduino', 'avrisp', 'usbtiny'], default: 'arduino' } | |
| ] | |
| }, | |
| 'mega': { | |
| name: 'Arduino Mega 2560', | |
| baudRate: 115200, | |
| processor: 'atmega2560', | |
| programmer: 'wiring', | |
| options: [ | |
| { id: 'programmer', label: 'Programmer', type: 'select', options: ['wiring', 'arduino', 'avrisp'], default: 'wiring' } | |
| ] | |
| } | |
| }; | |
| // Global variables for serial connection | |
| let port = null; | |
| let reader = null; | |
| let writer = null; | |
| let isReading = false; | |
| let isConnected = false; | |
| let activePorts = []; | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Check browser compatibility first | |
| checkBrowserCompatibility(); | |
| // Tab switching | |
| const editorTab = document.getElementById('editorTab'); | |
| const serialTab = document.getElementById('serialTab'); | |
| const editorContent = document.getElementById('editorContent'); | |
| const serialContent = document.getElementById('serialContent'); | |
| editorTab.addEventListener('click', () => { | |
| editorTab.classList.add('tab-active'); | |
| serialTab.classList.remove('tab-active'); | |
| editorContent.classList.remove('hidden'); | |
| serialContent.classList.add('hidden'); | |
| }); | |
| serialTab.addEventListener('click', () => { | |
| serialTab.classList.add('tab-active'); | |
| editorTab.classList.remove('tab-active'); | |
| serialContent.classList.remove('hidden'); | |
| editorContent.classList.add('hidden'); | |
| }); | |
| // Initialize Monaco Editor | |
| require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.43.0/min/vs' }}); | |
| require(['vs/editor/editor.main'], function() { | |
| window.editor = monaco.editor.create(document.getElementById('editor'), { | |
| value: [ | |
| 'void setup() {', | |
| ' Serial.begin(115200);', | |
| ' pinMode(LED_BUILTIN, OUTPUT);', | |
| '}', | |
| '', | |
| 'void loop() {', | |
| ' digitalWrite(LED_BUILTIN, HIGH);', | |
| ' delay(1000);', | |
| ' digitalWrite(LED_BUILTIN, LOW);', | |
| ' delay(1000);', | |
| '}' | |
| ].join('\n'), | |
| language: 'cpp', | |
| theme: 'vs', | |
| minimap: { enabled: false }, | |
| fontSize: 14, | |
| lineNumbers: 'on', | |
| roundedSelection: true, | |
| scrollBeyondLastLine: false, | |
| automaticLayout: true | |
| }); | |
| }); | |
| // Board selection handler | |
| const boardSelect = document.getElementById('boardSelect'); | |
| const boardOptions = document.getElementById('boardOptions'); | |
| const specificOptions = document.getElementById('specificOptions'); | |
| boardSelect.addEventListener('change', function() { | |
| const boardType = this.value; | |
| const config = boardConfigurations[boardType]; | |
| if (config && config.options && config.options.length > 0) { | |
| boardOptions.classList.remove('hidden'); | |
| specificOptions.innerHTML = ''; | |
| config.options.forEach(option => { | |
| const optionDiv = document.createElement('div'); | |
| optionDiv.className = 'flex items-center justify-between'; | |
| const label = document.createElement('label'); | |
| label.className = 'text-sm text-gray-600'; | |
| label.textContent = option.label; | |
| let input; | |
| if (option.type === 'select') { | |
| input = document.createElement('select'); | |
| input.className = 'text-sm border border-gray-300 rounded px-2 py-1'; | |
| input.id = option.id; | |
| option.options.forEach(opt => { | |
| const optionEl = document.createElement('option'); | |
| optionEl.value = opt; | |
| optionEl.textContent = opt; | |
| if (opt === option.default) { | |
| optionEl.selected = true; | |
| } | |
| input.appendChild(optionEl); | |
| }); | |
| } else if (option.type === 'checkbox') { | |
| const container = document.createElement('div'); | |
| container.className = 'flex items-center'; | |
| input = document.createElement('input'); | |
| input.type = 'checkbox'; | |
| input.className = 'h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500'; | |
| input.id = option.id; | |
| input.checked = option.default; | |
| container.appendChild(input); | |
| input = container; | |
| } | |
| optionDiv.appendChild(label); | |
| optionDiv.appendChild(input); | |
| specificOptions.appendChild(optionDiv); | |
| }); | |
| } else { | |
| boardOptions.classList.add('hidden'); | |
| } | |
| }); | |
| // File handling | |
| const fileInput = document.getElementById('fileInput'); | |
| const fileName = document.getElementById('fileName'); | |
| const browseBtn = document.getElementById('browseBtn'); | |
| const newFileBtn = document.getElementById('newFileBtn'); | |
| const saveFileBtn = document.getElementById('saveFileBtn'); | |
| browseBtn.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', function() { | |
| if (this.files.length > 0) { | |
| const file = this.files[0]; | |
| fileName.value = file.name; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| window.editor.setValue(e.target.result); | |
| }; | |
| reader.readAsText(file); | |
| document.getElementById('uploadBtn').disabled = false; | |
| } | |
| }); | |
| newFileBtn.addEventListener('click', () => { | |
| fileName.value = 'new_sketch.ino'; | |
| window.editor.setValue([ | |
| 'void setup() {', | |
| ' // Initialize your hardware here', | |
| '}', | |
| '', | |
| 'void loop() {', | |
| ' // Your main code here', | |
| '}' | |
| ].join('\n')); | |
| }); | |
| saveFileBtn.addEventListener('click', () => { | |
| const content = window.editor.getValue(); | |
| const blob = new Blob([content], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = fileName.value || 'arduino_sketch.ino'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }); | |
| // Serial monitor | |
| const serialMonitor = document.getElementById('serialMonitor'); | |
| const serialInput = document.getElementById('serialInput'); | |
| const serialSendBtn = document.getElementById('serialSendBtn'); | |
| const clearSerialBtn = document.getElementById('clearSerialBtn'); | |
| const autoScroll = document.getElementById('autoScroll'); | |
| function addToSerialMonitor(text, type = 'output') { | |
| const line = document.createElement('p'); | |
| line.className = 'serial-line'; | |
| if (type === 'input') { | |
| line.innerHTML = `<span class="text-blue-400">>></span> ${text}`; | |
| } else { | |
| line.textContent = text; | |
| } | |
| serialMonitor.appendChild(line); | |
| if (autoScroll.checked) { | |
| serialMonitor.scrollTop = serialMonitor.scrollHeight; | |
| } | |
| } | |
| async function sendSerialData(data) { | |
| if (!isConnected || !writer) { | |
| addToOutputConsole('Not connected to a device', 'error'); | |
| return; | |
| } | |
| try { | |
| addToSerialMonitor(data, 'input'); | |
| // Add newline if not present | |
| if (!data.endsWith('\n')) { | |
| data += '\n'; | |
| } | |
| await writer.write(new TextEncoder().encode(data)); | |
| } catch (error) { | |
| addToOutputConsole(`Error sending data: ${error}`, 'error'); | |
| } | |
| } | |
| serialSendBtn.addEventListener('click', () => { | |
| const command = serialInput.value.trim(); | |
| if (command) { | |
| sendSerialData(command); | |
| serialInput.value = ''; | |
| } | |
| }); | |
| serialInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| serialSendBtn.click(); | |
| } | |
| }); | |
| clearSerialBtn.addEventListener('click', () => { | |
| serialMonitor.innerHTML = ''; | |
| }); | |
| // Serial port handling | |
| const portSelect = document.getElementById('portSelect'); | |
| const refreshPorts = document.getElementById('refreshPorts'); | |
| const connectBtn = document.getElementById('connectBtn'); | |
| const connectionStatus = document.getElementById('connectionStatus'); | |
| const uploadBtn = document.getElementById('uploadBtn'); | |
| const portHelp = document.getElementById('portHelp'); | |
| const permissionModal = document.getElementById('permissionModal'); | |
| const requestPermissionBtn = document.getElementById('requestPermissionBtn'); | |
| const learnMoreBtn = document.getElementById('learnMoreBtn'); | |
| const browserWarning = document.getElementById('browserWarning'); | |
| // Check browser compatibility | |
| function checkBrowserCompatibility() { | |
| if (!('serial' in navigator)) { | |
| browserWarning.classList.remove('hidden'); | |
| refreshPorts.disabled = true; | |
| connectBtn.disabled = true; | |
| portHelp.textContent = 'Web Serial API not supported in this browser. Use Chrome/Edge 89+ or Opera 76+.'; | |
| addToOutputConsole('Web Serial API not supported in this browser. Try Chrome/Edge 89+ or Opera 76+.', 'error'); | |
| return false; | |
| } | |
| return true; | |
| } | |
| // Show permission modal | |
| function showPermissionModal() { | |
| permissionModal.style.display = 'flex'; | |
| } | |
| // Hide permission modal | |
| function hidePermissionModal() { | |
| permissionModal.style.display = 'none'; | |
| } | |
| // Learn more button handler | |
| learnMoreBtn.addEventListener('click', () => { | |
| window.open('https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API', '_blank'); | |
| }); | |
| // Request permission button handler | |
| requestPermissionBtn.addEventListener('click', async () => { | |
| try { | |
| // Request permission to access serial ports | |
| const port = await navigator.serial.requestPort({ | |
| filters: [ | |
| { usbVendorId: 0x2341 }, // Arduino | |
| { usbVendorId: 0x2A03 }, // Arduino (alternative) | |
| { usbVendorId: 0x10C4 }, // ESP32 | |
| { usbVendorId: 0x1A86 } // CH340 (common USB-to-serial) | |
| ] | |
| }); | |
| // If we get here, permission was granted | |
| hidePermissionModal(); | |
| await refreshPortList(); | |
| } catch (error) { | |
| if (error.name === 'NotFoundError') { | |
| addToOutputConsole('No device was selected', 'info'); | |
| portHelp.textContent = 'No device selected. Make sure your device is connected.'; | |
| } else if (error.name === 'SecurityError') { | |
| addToOutputConsole('Permission denied for serial port access', 'error'); | |
| portHelp.textContent = 'Permission denied. Please allow serial port access.'; | |
| } else { | |
| addToOutputConsole(`Error: ${error.message}`, 'error'); | |
| portHelp.textContent = `Error: ${error.message}`; | |
| } | |
| hidePermissionModal(); | |
| } | |
| }); | |
| async function refreshPortList() { | |
| if (!checkBrowserCompatibility()) return; | |
| portSelect.innerHTML = '<option value="">Select a port</option>'; | |
| activePorts = []; | |
| try { | |
| // Get list of ports we already have permission for | |
| const ports = await navigator.serial.getPorts(); | |
| if (ports.length === 0) { | |
| portSelect.innerHTML = '<option value="">No ports found</option>'; | |
| portHelp.textContent = 'No serial ports found. Connect a device and click refresh.'; | |
| addToOutputConsole('No serial ports found. Connect a device and click refresh.'); | |
| // Show permission modal since we don't have any ports | |
| showPermissionModal(); | |
| return; | |
| } | |
| // Store ports for later reference | |
| activePorts = ports; | |
| ports.forEach((port, index) => { | |
| const option = document.createElement('option'); | |
| option.value = index; // Using index as value since port objects can't be serialized | |
| const portInfo = port.getInfo(); | |
| const portName = portInfo.usbVendorId ? | |
| `Serial Port (Vendor: 0x${portInfo.usbVendorId.toString(16)}, Product: 0x${portInfo.usbProductId.toString(16)})` : | |
| 'Serial Port'; | |
| const optionContent = document.createElement('div'); | |
| optionContent.className = 'port-option'; | |
| optionContent.innerHTML = ` | |
| <i class="fas fa-microchip port-icon"></i> | |
| <span>${portName}</span> | |
| `; | |
| option.textContent = portName; | |
| portSelect.appendChild(option); | |
| }); | |
| portHelp.textContent = `Found ${ports.length} serial port(s)`; | |
| addToOutputConsole(`Found ${ports.length} serial port(s)`); | |
| } catch (error) { | |
| if (error.name === 'SecurityError') { | |
| addToOutputConsole('Permission denied for serial port access. Please grant permission.', 'error'); | |
| portHelp.textContent = 'Permission denied. Please allow serial port access.'; | |
| showPermissionModal(); | |
| } else { | |
| addToOutputConsole(`Error accessing serial ports: ${error}`, 'error'); | |
| portSelect.innerHTML = '<option value="">Error accessing ports</option>'; | |
| portHelp.textContent = 'Error accessing ports. Please try again.'; | |
| } | |
| } | |
| } | |
| refreshPorts.addEventListener('click', async () => { | |
| refreshPorts.classList.add('blink'); | |
| addToOutputConsole('Searching for serial ports...'); | |
| portHelp.textContent = 'Searching for devices...'; | |
| try { | |
| // First try to get ports we already have permission for | |
| await refreshPortList(); | |
| } finally { | |
| refreshPorts.classList.remove('blink'); | |
| } | |
| }); | |
| async function connectToPort() { | |
| if (!portSelect.value) { | |
| addToOutputConsole('Please select a port first', 'error'); | |
| return; | |
| } | |
| const portIndex = parseInt(portSelect.value); | |
| if (isNaN(portIndex) || portIndex < 0 || portIndex >= activePorts.length) { | |
| addToOutputConsole('Invalid port selection', 'error'); | |
| return; | |
| } | |
| try { | |
| port = activePorts[portIndex]; | |
| if (!port) { | |
| throw new Error('Selected port not found'); | |
| } | |
| // Get board configuration | |
| const boardType = boardSelect.value; | |
| const config = boardConfigurations[boardType]; | |
| // Open the port with the board's baud rate | |
| await port.open({ baudRate: config.baudRate }); | |
| // Set up the reader and writer | |
| writer = port.writable.getWriter(); | |
| reader = port.readable.getReader(); | |
| // Start reading from the port | |
| isReading = true; | |
| readSerialData(); | |
| // Update UI | |
| isConnected = true; | |
| connectBtn.innerHTML = '<i class="fas fa-unlink mr-2"></i> Disconnect'; | |
| connectBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700'); | |
| connectBtn.classList.add('bg-red-600', 'hover:bg-red-700'); | |
| connectionStatus.classList.remove('hidden', 'bg-gray-100', 'text-gray-700'); | |
| connectionStatus.classList.add('bg-green-100', 'text-green-700'); | |
| connectionStatus.innerHTML = '<i class="fas fa-circle mr-2 text-green-500"></i><span>Connected to ' + portSelect.options[portSelect.selectedIndex].text + '</span>'; | |
| addToOutputConsole(`Connected to ${portSelect.options[portSelect.selectedIndex].text} at ${config.baudRate} baud`); | |
| uploadBtn.disabled = false; | |
| // Send a test message to check connection | |
| setTimeout(() => { | |
| if (isConnected) { | |
| sendSerialData("AT"); // Common test command | |
| } | |
| }, 500); | |
| } catch (error) { | |
| addToOutputConsole(`Error connecting to port: ${error}`, 'error'); | |
| portHelp.textContent = `Connection failed: ${error.message}`; | |
| if (port) { | |
| try { | |
| await port.close(); | |
| } catch (e) { | |
| console.error('Error closing port:', e); | |
| } | |
| port = null; | |
| } | |
| // Reset connection state | |
| isConnected = false; | |
| connectBtn.innerHTML = '<i class="fas fa-link mr-2"></i> Connect'; | |
| connectBtn.classList.remove('bg-red-600', 'hover:bg-red-700'); | |
| connectBtn.classList.add('bg-blue-600', 'hover:bg-blue-700'); | |
| connectionStatus.classList.remove('bg-green-100', 'text-green-700'); | |
| connectionStatus.classList.add('bg-gray-100', 'text-gray-700'); | |
| connectionStatus.innerHTML = '<i class="fas fa-circle mr-2 text-gray-400"></i><span>Disconnected</span>'; | |
| uploadBtn.disabled = true; | |
| } | |
| } | |
| async function disconnectFromPort() { | |
| isReading = false; | |
| if (reader) { | |
| try { | |
| await reader.cancel(); | |
| } catch (error) { | |
| console.error('Error cancelling reader:', error); | |
| } | |
| reader = null; | |
| } | |
| if (writer) { | |
| try { | |
| await writer.releaseLock(); | |
| } catch (error) { | |
| console.error('Error releasing writer:', error); | |
| } | |
| writer = null; | |
| } | |
| if (port) { | |
| try { | |
| await port.close(); | |
| } catch (error) { | |
| console.error('Error closing port:', error); | |
| } | |
| port = null; | |
| } | |
| // Update UI | |
| isConnected = false; | |
| connectBtn.innerHTML = '<i class="fas fa-link mr-2"></i> Connect'; | |
| connectBtn.classList.remove('bg-red-600', 'hover:bg-red-700'); | |
| connectBtn.classList.add('bg-blue-600', 'hover:bg-blue-700'); | |
| connectionStatus | |
| </html> |