/** * OTA Deployment Script * Uploads www.bin or universal.bundle to the ESP32 device. */ import { readFileSync, readdirSync, existsSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import http from 'http'; const __dirname = dirname(fileURLToPath(import.meta.url)); const projectRoot = resolve(__dirname, '..'); const binDir = resolve(projectRoot, 'bin'); const bundleDir = resolve(projectRoot, 'bundles'); /** * Simple .env parser to load VITE_API_BASE */ function loadEnv() { const envPath = resolve(projectRoot, '.env'); if (existsSync(envPath)) { const content = readFileSync(envPath, 'utf8'); content.split('\n').forEach(line => { const [key, ...valueParts] = line.split('='); if (key && valueParts.length > 0) { const value = valueParts.join('=').trim().replace(/^["']|["']$/g, ''); process.env[key.trim()] = value; } }); } } loadEnv(); const targetIP = process.env.VITE_API_BASE ? process.env.VITE_API_BASE.replace('http://', '') : null; if (!targetIP) { console.error('Error: VITE_API_BASE not found in frontend/.env. Please set it to the target device IP.'); process.exit(1); } const args = process.argv.slice(2); const isBundle = args.includes('--bundle'); const isFrontend = args.includes('--frontend'); if (!isBundle && !isFrontend) { console.error('Error: Specify --frontend or --bundle.'); process.exit(1); } function getLatestFile(dir, pattern) { if (!existsSync(dir)) return null; const files = readdirSync(dir) .filter(f => f.match(pattern)) .sort((a, b) => { const getParts = (s) => { const m = s.match(/v(\d+)\.(\d+)\.(\d+)/); return m ? m.slice(1).map(Number) : [0, 0, 0]; }; const [aMajor, aMinor, aRev] = getParts(a); const [bMajor, bMinor, bRev] = getParts(b); return (bMajor - aMajor) || (bMinor - aMinor) || (bRev - aRev); }); return files.length > 0 ? resolve(dir, files[0]) : null; } const filePath = isBundle ? getLatestFile(bundleDir, /^universal_v.*\.bundle$/) : getLatestFile(binDir, /^www_v.*\.bin$/); if (!filePath) { console.error(`Error: No recent ${isBundle ? 'bundle' : 'frontend bin'} found in ${isBundle ? bundleDir : binDir}`); process.exit(1); } const fileName = filePath.split(/[\\/]/).pop(); const versionMatch = fileName.match(/v(\d+\.\d+\.\d+)/); const version = versionMatch ? versionMatch[1] : 'unknown'; console.log('-------------------------------------------'); console.log(`šŸš€ Deployment Started`); console.log(`šŸ“ Target device: http://${targetIP}`); console.log(`šŸ“¦ Packaging type: ${isBundle ? 'Universal Bundle' : 'Frontend Only'}`); console.log(`šŸ“„ File: ${fileName}`); console.log(`šŸ”– Version: ${version}`); console.log('-------------------------------------------'); const fileBuf = readFileSync(filePath); const urlPath = isBundle ? '/api/ota/bundle' : '/api/ota/frontend'; const url = `http://${targetIP}${urlPath}`; console.log(`Uploading ${fileBuf.length} bytes using fetch API... (This mimics the browser UI upload)`); async function deploy() { try { const response = await fetch(url, { method: 'POST', body: fileBuf, headers: { 'Content-Type': 'application/octet-stream', 'Content-Length': fileBuf.length.toString() } }); if (response.ok) { const text = await response.text(); console.log('\nāœ… Success: Update committed!'); console.log('Server response:', text); console.log('The device will reboot in ~1 second.'); // Wait a few seconds to let the socket close gracefully before killing the process setTimeout(() => { console.log('Deployment script finished.'); process.exit(0); }, 3000); } else { const text = await response.text(); console.error(`\nāŒ Error (${response.status}):`, text); process.exit(1); } } catch (e) { console.error(`\nāŒ Problem with request:`, e.message); if (e.cause) console.error(e.cause); process.exit(1); } } deploy();