diff --git a/flasher/configurator.html b/flasher/configurator.html new file mode 100644 index 0000000..622f3cc --- /dev/null +++ b/flasher/configurator.html @@ -0,0 +1,521 @@ + + + + + + MeshCore Configurator + + + + + + + +
+ + +
+
+ +

{{ app.busy }}

+
+
+ +
{{ snackbar.text }}
+ + +
+
💻 Подключение к устройству
+

Нажми «Подключиться» и выбери порт T114 в окне браузера.

+ +
+ + +
+
+ + + + +
+ +
+
+ Версия: {{ app.device.version || '—' }} +
+
+ Роль: {{ app.device.role || '—' }} +
+
+ Clock: {{ app.device.clock || '—' }} +
+
+ +
+
Public Key
+ {{ app.device.pubKey || '—' }} +
+ +
+ +
+ Name & Location +
+ + +
+
{{ nameBytes }} / {{ nameMaxBytes }} bytes
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+ Access +
+ + +
+
+ + +
+
+ + +
+ Radio settings +
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + + Airtime factor: {{ app.device.vars.af }} +
+
+
+ + +
+ Advertising +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
ⓘ Есть несохранённые изменения
+ +
+ + + + + + + +
+ + + + diff --git a/flasher/lib/serial-cli.js b/flasher/lib/serial-cli.js new file mode 100644 index 0000000..a075459 --- /dev/null +++ b/flasher/lib/serial-cli.js @@ -0,0 +1,756 @@ +/** + * SerialCLI - A class for communicating with devices via Web Serial API + * Handles sending commands, receiving responses, and parsing multi-line data + */ + +export class SerialCLI { + constructor(debug = false) { // Added debug parameter + this.port = null; + this.reader = null; + this.writer = null; + this.readBuffer = ""; + this.isReading = false; + this.commandQueue = []; + this.currentCommand = null; + this.decoder = new TextDecoder(); + this.encoder = new TextEncoder(); + this.responseTimeout = 5000; // 5 seconds timeout for responses + this.commandDelay = 100; // 100ms delay between commands + this.debug = debug; // Initialize debug mode + } + + /** + * Enable or disable debug logging + * @param {boolean} enabled - True to enable debug mode, false to disable + */ + setDebug(enabled) { + this.debug = enabled; + if (this.debug) { + console.log("SerialCLI Debug Mode Enabled"); + } else { + console.log("SerialCLI Debug Mode Disabled"); + } + } + + /** + * Connect to a serial device + * @param {number} baudRate - Baud rate to use (default: 115200) + * @returns {Promise} True if connected, false otherwise + */ + async connect(baudRate = 115200) { + if (!('serial' in navigator)) { + console.error('Web Serial API not supported in this browser'); + return false; + } + + try { + this.port = await navigator.serial.requestPort(); + await this.port.open({ baudRate }); + + this.reader = this.port.readable.getReader(); + this.writer = this.port.writable.getWriter(); + + if (this.debug) { + console.log(`SerialCLI: Connected to port, baud rate ${baudRate}`); + } + + this.startReading(); + return true; // Indicate successful connection + } catch (error) { + console.error("SerialCLI: Failed to connect", error); + this.port = null; // Reset port on failure + return false; // Indicate failed connection + } + } + + /** + * Disconnect from the serial device + */ + async disconnect() { + if (this.reader) { + try { + this.isReading = false; + await this.reader.cancel(); + // releaseLock() is handled implicitly by cancel() or closing the port + } catch (error) { + if (this.debug) console.error("SerialCLI: Error cancelling reader", error); + } finally { + this.reader = null; + } + } + + if (this.writer) { + try { + // Ensure writer is closed before releasing lock + if (!this.writer.closed) { + await this.writer.close(); + } + } catch (error) { + if (this.debug) console.error("SerialCLI: Error closing writer", error); + } finally { + try { + this.writer.releaseLock(); + } catch(lockError) { + // Ignore error if lock was already released + } + this.writer = null; + } + } + + + if (this.port) { + try { + await this.port.close(); + if (this.debug) console.log("SerialCLI: Port closed"); + } catch (error) { + if (this.debug) console.error("SerialCLI: Error closing port", error); + } finally { + this.port = null; + } + } + } + + /** + * Start reading data from the serial port + * @private + */ + startReading() { + if (!this.reader) return; + + this.isReading = true; + this.readLoop(); + if (this.debug) console.log("SerialCLI: Started reading loop"); + } + + /** + * Main read loop for serial data + * @private + */ + async readLoop() { + while (this.isReading && this.reader) { + try { + const { value, done } = await this.reader.read(); + if (done) { + // Allow the serial port to be closed later. + this.reader.releaseLock(); + if (this.debug) console.log("SerialCLI: Reader stream closed"); + break; + } + + const textChunk = this.decoder.decode(value, { stream: true }); // Use stream option for potentially multi-byte chars split across chunks + if (this.debug) { + console.log("SerialCLI <<< RECV:", JSON.stringify(textChunk)); // Log received data + } + this.processIncomingData(textChunk); + + } catch (error) { + console.error("SerialCLI: Error in read loop:", error); + this.isReading = false; // Stop reading on error + try { + this.reader.releaseLock(); + } catch (lockError) { + // Ignore lock release error if already released + } + this.reader = null; + // Consider attempting to reconnect or notify the user + break; + } + } + + // Redundant check, but safe + if (this.isReading && this.port?.readable && !this.reader) { + try { + this.reader = this.port.readable.getReader(); + this.readLoop(); // Restart loop if needed and possible + if (this.debug) console.log("SerialCLI: Restarted reading loop after temporary reader release"); + } catch(err) { + console.error("SerialCLI: Failed to re-acquire reader", err); + this.isReading = false; + } + } else if (!this.isReading && this.debug) { + console.log("SerialCLI: Reading loop stopped."); + } + } + + /** + * Process incoming data from the serial port + * @param {string} data - The data received from the serial port + * @private + */ + processIncomingData(data) { + this.readBuffer += data; + if (this.debug) console.log("SerialCLI: Buffer:", JSON.stringify(this.readBuffer)); + + // Check if we're waiting for a response + if (this.currentCommand) { + this.checkForResponse(); + } else if (this.commandQueue.length > 0) { + // If no current command but queue has items, try to execute next command + // This should ideally only happen after a response is fully processed + // or if the device sends unsolicited data. + if (this.debug) console.log("SerialCLI: Received data while idle, buffer:", JSON.stringify(this.readBuffer)); + // Let's not automatically execute next command here, wait for command completion logic + } + } + + /** + * Check if a complete response has been received + * @private + */ + checkForResponse() { + if (!this.currentCommand) return; + + // --- Refined Response Parsing Logic --- + // A typical interaction looks like: + // 1. Send command: `my_command\r` + // 2. Device echoes: `my_command\r\n` (optional, depends on device) + // 3. Device processes and sends response: ` -> OK\r\n` or multi-line for log + // We need to find the "->" marker *after* the potential echo. + + const { command, isLogCommand } = this.currentCommand; + const commandWithCR = command + '\r'; // Command as sent + const commandWithCRLF = command + '\r\n'; // Potential echo format + + // Find the end of the command echo (could be with or without \n) + let echoEndIndex = this.readBuffer.indexOf(commandWithCRLF); + if (echoEndIndex !== -1) { + echoEndIndex += commandWithCRLF.length; + } else { + echoEndIndex = this.readBuffer.indexOf(commandWithCR); + if (echoEndIndex !== -1) { + echoEndIndex += commandWithCR.length; + } else { + // Command echo might not have arrived fully yet, or device doesn't echo + // Let's proceed cautiously, but this might lead to issues if echo is partial + echoEndIndex = 0; // Assume start of buffer if no echo found yet + if (this.debug) console.log("SerialCLI: Command echo not found yet or device doesn't echo."); + } + } + + + // Look for the response marker "->" *after* the potential echo + const responseMarker = " -> "; + const responseStartIndex = this.readBuffer.indexOf(responseMarker, echoEndIndex); + + if (responseStartIndex === -1) { + if (this.debug) console.log("SerialCLI: Response marker '->' not found after echo index", echoEndIndex); + return; // Response marker not found yet + } + + const responsePayloadStartIndex = responseStartIndex + responseMarker.length; + + // Find the end of the response (\r\n) + // Search *after* the start of the response payload + const newlineIndex = this.readBuffer.indexOf('\r\n', responsePayloadStartIndex); + + if (newlineIndex === -1) { + if (this.debug) console.log("SerialCLI: Response newline not found after payload start index", responsePayloadStartIndex); + return; // Full response line hasn't arrived + } + + // Extract the response content + const responseLine = this.readBuffer.substring(responsePayloadStartIndex, newlineIndex).trim(); + const consumedUntilIndex = newlineIndex + 2; // Include the \r\n + + if (this.debug) console.log(`SerialCLI: Found response line: "${responseLine}"`); + + // Special handling for log command which has multi-line response ending with EOF + if (isLogCommand) { + // For log, the first line might just be the confirmation, e.g., "-> OK" or similar. + // The actual log data follows, ending with " EOF\r\n" + const eofMarker = " EOF"; + // Look for EOF *after* the initial response line we just found + const eofIndex = this.readBuffer.indexOf(eofMarker, consumedUntilIndex); + + if (eofIndex !== -1) { + const eofNewlineIndex = this.readBuffer.indexOf('\r\n', eofIndex); + if (eofNewlineIndex !== -1) { + // Extract the log data between the first response line and the EOF marker + const logData = this.readBuffer.substring(consumedUntilIndex, eofIndex).trim(); + const finalConsumedIndex = eofNewlineIndex + 2; + if (this.debug) console.log(`SerialCLI: Log EOF found. Log data length: ${logData.length}`); + + this.readBuffer = this.readBuffer.substring(finalConsumedIndex); // Consume everything including EOF line + this.completeCommand(logData); // Resolve with the extracted log data + } else { + if (this.debug) console.log("SerialCLI: Log EOF marker found, but newline missing."); + } + } else { + if (this.debug) console.log("SerialCLI: Log command response started, waiting for EOF."); + } + } else { + // For standard commands, the single line is the response + this.readBuffer = this.readBuffer.substring(consumedUntilIndex); // Consume the processed part + this.completeCommand(responseLine); // Resolve with the single response line + } + + if (this.debug) console.log("SerialCLI: Buffer after processing:", JSON.stringify(this.readBuffer)); + } + + + /** + * Complete a command and resolve its promise with the response + * @param {string} response - The response from the device + * @private + */ + completeCommand(response) { + if (!this.currentCommand) return; + + clearTimeout(this.currentCommand.timeout); + const { resolve, command } = this.currentCommand; + if (this.debug) console.log(`SerialCLI: Command "${command}" completed with response:`, response); + + this.currentCommand = null; + resolve(response); + + // Schedule next command execution after a delay + if (this.commandQueue.length > 0) { + if (this.debug) console.log(`SerialCLI: Scheduling next command in ${this.commandDelay}ms`); + setTimeout(() => this.executeNextCommand(), this.commandDelay); + } else { + if (this.debug) console.log("SerialCLI: Command queue empty."); + } + } + + /** + * Execute the next command in the queue + * @private + */ + async executeNextCommand() { + // Prevent starting a new command if one is already in progress + if (this.currentCommand) { + if (this.debug) console.log("SerialCLI: executeNextCommand called, but a command is already active."); + return; + } + if (this.commandQueue.length === 0) { + if (this.debug) console.log("SerialCLI: executeNextCommand called, but queue is empty."); + return; + } + if (!this.writer) { + console.error("SerialCLI: Cannot execute command, writer is not available."); + // Reject the command? Or just log and wait? Let's reject. + const nextCmd = this.commandQueue.shift(); + nextCmd.reject(new Error("Serial writer not available")); + // Check if more commands need rejecting or if we should stop. + if (this.commandQueue.length > 0) { + setTimeout(() => this.executeNextCommand(), this.commandDelay); // Process next potential rejection + } + return; + } + + + this.currentCommand = this.commandQueue.shift(); + const { command, reject } = this.currentCommand; + + if (this.debug) console.log(`SerialCLI: Executing command: "${command}"`); + + // Set response timeout + this.currentCommand.timeout = setTimeout(() => { + if (this.currentCommand && this.currentCommand.command === command) { // Ensure it's still the same command + const timeoutMsg = `Command timeout: ${command}`; + console.error("SerialCLI:", timeoutMsg); + reject(new Error(timeoutMsg)); + this.currentCommand = null; // Clear current command on timeout + + // Try the next command after a delay + if (this.commandQueue.length > 0) { + if (this.debug) console.log("SerialCLI: Scheduling next command after timeout."); + setTimeout(() => this.executeNextCommand(), this.commandDelay); + } + } + }, this.responseTimeout); + + try { + const dataToSend = this.encoder.encode(command + '\r'); + if (this.debug) { + console.log("SerialCLI >>> SEND:", JSON.stringify(command + '\\r')); // Log data being sent + } + await this.writer.write(dataToSend); + } catch (error) { + console.error(`SerialCLI: Error writing command "${command}":`, error); + clearTimeout(this.currentCommand.timeout); + reject(error); + this.currentCommand = null; // Clear current command on write error + + // Try the next command after a delay + if (this.commandQueue.length > 0) { + if (this.debug) console.log("SerialCLI: Scheduling next command after write error."); + setTimeout(() => this.executeNextCommand(), this.commandDelay); + } + } + } + + /** + * Send a command to the device + * @param {string} command - The command to send + * @param {boolean} isLogCommand - Whether this is a log command with multi-line response ending in EOF + * @returns {Promise} The device's response + */ + sendCommand(command, isLogCommand = false) { + if (!this.port || !this.writer) { + const errorMsg = 'Serial connection not open or writer unavailable'; + console.error("SerialCLI:", errorMsg); + return Promise.reject(new Error(errorMsg)); + } + + return new Promise((resolve, reject) => { + this.commandQueue.push({ command, resolve, reject, isLogCommand }); + if (this.debug) console.log(`SerialCLI: Queued command: "${command}". Queue length: ${this.commandQueue.length}`); + + // If no current command is active, start execution immediately + if (!this.currentCommand) { + if (this.debug) console.log("SerialCLI: Triggering command execution from sendCommand."); + this.executeNextCommand(); + } + }); + } + + // ============= CONVENIENCE METHODS ============= + + /** + * Get the device firmware version + * @returns {Promise} Version information + */ + async getVersion() { + return this.sendCommand('ver'); + } + + /** + * Get the current clock time + * @returns {Promise} Current time + */ + async getClock() { + return this.sendCommand('clock'); + } + + /** + * Set the time (in epoch seconds) + * @param {number} seconds - Epoch seconds + * @returns {Promise} Response from device + */ + async setTime(seconds) { + // Ensure seconds is a valid number + if (typeof seconds !== 'number' || !Number.isInteger(seconds) || seconds < 0) { + return Promise.reject(new Error("Invalid time value. Must be a non-negative integer.")); + } + return this.sendCommand(`time ${seconds}`); + } + + /** + * Reboot the device + * @returns {Promise} Response from device (Note: response might not be received if reboot is immediate) + */ + async reboot() { + // Don't necessarily expect a standard response format for reboot + // Consider adding a short delay after sending if needed by the calling code + return this.sendCommand('reboot'); + } + + /** + * Erase filesystem (factory reset) + * @returns {Promise} Response from device + */ + async erase() { + return this.sendCommand('erase'); + } + + /** + * Force device to send an advertisement + * @returns {Promise} Response from device + */ + async sendAdvert() { + return this.sendCommand('advert'); + } + + /** + * Start OTA update + * @returns {Promise} Response from device + */ + async startOTA() { + // Might need specific node name from prefs? The C++ code suggests yes. + // This JS version doesn't store prefs, so we send the basic command. + // Consider adding a parameter if the node name is needed. + return this.sendCommand('start ota'); + } + + /** + * Get a variable value + * @param {string} variable - The variable name (e.g., 'name', 'lat', 'tx') + * @returns {Promise} Raw variable value string from device (e.g., "> MyNode", "> 10", "> 433.125") + */ + async getVariable(variable) { + return this.sendCommand(`get ${variable}`); + } + + /** + * Set a variable value + * @param {string} variable - The variable name + * @param {string|number|boolean} value - The value to set + * @returns {Promise} Response from device (usually "OK" or an error) + */ + async setVariable(variable, value) { + // Convert boolean 'true'/'false' to 'on'/'off' if appropriate for specific vars later + return this.sendCommand(`set ${variable} ${value}`); + } + + /** + * Set admin password + * @param {string} password - Admin password + * @returns {Promise} Response from device + */ + async setPassword(password) { + // Basic validation: ensure password is a non-empty string + if (typeof password !== 'string' || password.length === 0) { + return Promise.reject(new Error("Password cannot be empty.")); + } + // Potentially add checks for invalid characters if needed + return this.sendCommand(`password ${password}`); + } + + /** + * Retrieve log data + * @returns {Promise} Log contents (multi-line string) + */ + async getLog() { + return this.sendCommand('log', true); // Mark as log command for multi-line EOF handling + } + + /** + * Start logging + * @returns {Promise} Response from device + */ + async startLogging() { + return this.sendCommand('log start'); + } + + /** + * Stop logging + * @returns {Promise} Response from device + */ + async stopLogging() { + return this.sendCommand('log stop'); + } + + /** + * Erase the log file + * @returns {Promise} Response from device + */ + async eraseLog() { + return this.sendCommand('log erase'); + } + + /** + * Parse response from getVariable commands, removing the "> " prefix and attempting type conversion. + * @param {string} response - The raw response string from a getVariable command (e.g., "> MyNode", "> 10", "> on") + * @returns {string|number|boolean|null} The parsed value, or null if parsing fails or response format is unexpected. + */ + parseVariableResponse(response) { + if (typeof response !== 'string' || !response.startsWith('> ')) { + if(this.debug) console.warn(`SerialCLI: Unexpected format for parseVariableResponse: "${response}"`); + return null; // Or return the original response? Returning null indicates parsing issue. + } + + const value = response.substring(2).trim(); // Remove "> " and trim whitespace + + // Check for empty value after prefix removal + if (value === '') { + return ''; // Return empty string if that was the actual value + } + + // Try to parse as number (integer or float) + // Updated regex to handle negative numbers and ensure it's the *entire* string + if (/^-?\d+(\.\d+)?$/.test(value)) { + return Number(value); // Use Number() to handle both int and float + } + + // Handle boolean 'on'/'off' (case-insensitive) + if (value.toLowerCase() === 'on') return true; + if (value.toLowerCase() === 'off') return false; + + // Return as string for all other cases + return value; + } + + // ============= SPECIFIC VARIABLE GETTERS/SETTERS (using parseVariableResponse) ============= + + async getRole() { + const response = await this.getVariable('role'); + return this.parseVariableResponse(response); + } + + async getPubKey() { + const response = await this.getVariable('public.key'); + return this.parseVariableResponse(response); + } + + async getName() { + const response = await this.getVariable('name'); + return this.parseVariableResponse(response); + } + + async setName(name) { + if (typeof name !== 'string') return Promise.reject(new Error("Name must be a string.")); + // Add validation for length or characters based on device limits if known + return this.setVariable('name', name); + } + + async getLatitude() { + const response = await this.getVariable('lat'); + return this.parseVariableResponse(response); + } + + async setLatitude(lat) { + if (typeof lat !== 'number') return Promise.reject(new Error("Latitude must be a number.")); + // Add validation for range (-90 to 90) if needed + return this.setVariable('lat', lat); + } + + async getLongitude() { + const response = await this.getVariable('lon'); + return this.parseVariableResponse(response); + } + + async setLongitude(lon) { + if (typeof lon !== 'number') return Promise.reject(new Error("Longitude must be a number.")); + // Add validation for range (-180 to 180) if needed + return this.setVariable('lon', lon); + } + + async getRadioConfig() { + const response = await this.getVariable('radio'); + const parsed = this.parseVariableResponse(response); + if (typeof parsed === 'string') { + const parts = parsed.split(','); + if (parts.length === 4) { + return { + freq: parseFloat(parts[0]) || null, + bw: parseFloat(parts[1]) || null, + sf: parseInt(parts[2], 10) || null, + cr: parseInt(parts[3], 10) || null + }; + } + } + if (this.debug) console.warn("SerialCLI: Could not parse radio config response:", response); + return null; // Indicate parsing failure + } + + async setRadioConfig(freq, bw, sf, cr) { + // Add validation for types and ranges if necessary + if (typeof freq !== 'number' || typeof bw !== 'number' || typeof sf !== 'number' || typeof cr !== 'number') { + return Promise.reject(new Error("Invalid radio parameters. All must be numbers.")); + } + return this.setVariable('radio', `${freq},${bw},${sf},${cr}`); + } + + async getTxPower() { + const response = await this.getVariable('tx'); + return this.parseVariableResponse(response); + } + + async setTxPower(power) { + if (typeof power !== 'number') return Promise.reject(new Error("TX Power must be a number.")); + // Add validation for range based on device capabilities if known (e.g., 1-30) + return this.setVariable('tx', power); + } + + async getAirtimeFactor() { + const response = await this.getVariable('af'); + return this.parseVariableResponse(response); + } + + async setAirtimeFactor(factor) { + if (typeof factor !== 'number') return Promise.reject(new Error("Airtime factor must be a number.")); + // Add validation for range (e.g., 0-9) + return this.setVariable('af', factor); + } + + async getRepeat() { + const response = await this.getVariable('repeat'); + return this.parseVariableResponse(response); // Should return true/false + } + + async setRepeat(enabled) { + if (typeof enabled !== 'boolean') return Promise.reject(new Error("Repeat value must be boolean (true/false).")); + return this.setVariable('repeat', enabled ? 'on' : 'off'); + } + + // Note: 'allow.read.only' is not in the C++ code provided, assuming it might exist elsewhere or is hypothetical. + // If it exists and uses 'on'/'off', the pattern is the same as 'setRepeat'. + // async getAllowReadOnly() { ... } + // async setAllowReadOnly(enabled) { ... } + + async getAdvertInterval() { + // C++ stores as interval/2, retrieves as interval*2 (minutes) + const response = await this.getVariable('advert.interval'); + return this.parseVariableResponse(response); + } + + async setAdvertInterval(minutes) { + if (typeof minutes !== 'number' || !Number.isInteger(minutes)) return Promise.reject(new Error("Advert interval must be an integer (minutes).")); + // Add validation based on C++ code (min 60, max 240, or 0 for off) + if (minutes !== 0 && (minutes < 60 || minutes > 240)) { + return Promise.reject(new Error("Advert interval must be 0 (off) or between 60 and 240 minutes.")); + } + return this.setVariable('advert.interval', minutes); + } + + // Note: 'flood.advert.interval' is not in the C++ code provided. + // async getFloodAdvertInterval() { ... } + // async setFloodAdvertInterval(hours) { ... } + + async getGuestPassword() { + const response = await this.getVariable('guest.password'); + return this.parseVariableResponse(response); + } + + async setGuestPassword(password) { + if (typeof password !== 'string') return Promise.reject(new Error("Guest password must be a string.")); + // Consider adding length/character validation + return this.setVariable('guest.password', password); + } + + async getRxDelay() { + const response = await this.getVariable('rxdelay'); + return this.parseVariableResponse(response); + } + + async setRxDelay(delay) { + if (typeof delay !== 'number' || delay < 0) return Promise.reject(new Error("RX Delay must be a non-negative number.")); + // Add validation for range (e.g., 0-20) + return this.setVariable('rxdelay', delay); + } + + async getTxDelay() { + const response = await this.getVariable('txdelay'); + return this.parseVariableResponse(response); + } + + async setTxDelay(delay) { + if (typeof delay !== 'number' || delay < 0) return Promise.reject(new Error("TX Delay factor must be a non-negative number.")); + // Add validation for range (e.g., 0-2) + return this.setVariable('txdelay', delay); + } + + async getDirectTxDelay() { + const response = await this.getVariable('direct.txdelay'); + return this.parseVariableResponse(response); + } + + async setDirectTxDelay(delay) { + if (typeof delay !== 'number' || delay < 0) return Promise.reject(new Error("Direct TX Delay factor must be a non-negative number.")); + // Add validation for range (e.g., 0-2) + return this.setVariable('direct.txdelay', delay); + } + + async getFloodMax() { + const response = await this.getVariable('flood.max'); + return this.parseVariableResponse(response); + } + + async setFloodMax(max) { + if (typeof max !== 'number' || !Number.isInteger(max) || max < 0 || max > 64) { + return Promise.reject(new Error("Flood Max must be an integer between 0 and 64.")); + } + return this.setVariable('flood.max', max); + } +} \ No newline at end of file diff --git a/flasher/lib/vanity-key-generator.js b/flasher/lib/vanity-key-generator.js new file mode 100644 index 0000000..13ad77d --- /dev/null +++ b/flasher/lib/vanity-key-generator.js @@ -0,0 +1,118 @@ +export class VanityKeyGenerator { + constructor() { + this.workers = []; + this.running = false; + this._resolve = null; + this._reject = null; + this._totalAttempts = 0; + this.onProgress = null; + } + + static get numCores() { + return navigator.hardwareConcurrency || 4; + } + + /** + * Estimate time for a given hex prefix length + * @param {number} prefixLen - number of hex chars + * @param {number} keysPerSec - estimated throughput + * @returns {string} human-readable estimate + */ + static estimateTime(prefixLen, keysPerSec) { + if (prefixLen === 0) return 'instant'; + const expected = Math.pow(16, prefixLen); + const seconds = expected / keysPerSec; + + if (seconds < 1) return 'less than a second'; + if (seconds < 60) return `~${Math.ceil(seconds)} seconds`; + if (seconds < 3600) return `~${Math.ceil(seconds / 60)} minutes`; + if (seconds < 86400) return `~${Math.ceil(seconds / 3600)} hours`; + return `~${Math.ceil(seconds / 86400)} days`; + } + + get attempts() { + return this._totalAttempts; + } + + /** + * Start generating a vanity key + * @param {string} prefix - hex prefix to match (1-6 chars) + * @returns {Promise<{ privKey: string, pubKey: string, attempts: number } | null>} + */ + generate(prefix) { + if (this.running) throw new Error('Already running'); + + prefix = prefix.replace(/[^0-9a-fA-F]/g, ''); + if (prefix.length === 0 || prefix.length > 6) { + throw new Error('Prefix must be 1-6 hex characters'); + } + + this.running = true; + this._totalAttempts = 0; + const numWorkers = VanityKeyGenerator.numCores; + + return new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + + for (let i = 0; i < numWorkers; i++) { + const worker = new Worker( + new URL('./vanity-key-worker.js', import.meta.url), + { type: 'module' } + ); + + worker.onmessage = (e) => { + if (!this.running) return; + const data = e.data; + + if (data.type === 'progress') { + this._totalAttempts += data.attempts; + if (this.onProgress) this.onProgress(this._totalAttempts); + } else if (data.type === 'match') { + this._totalAttempts += data.attempts; + const result = { + privKey: data.privKey, + pubKey: data.pubKey, + attempts: this._totalAttempts, + }; + this._stopWorkers(); + this.running = false; + resolve(result); + } else if (data.type === 'error') { + this._stopWorkers(); + this.running = false; + reject(new Error(data.message)); + } else if (data.type === 'stopped') { + this._totalAttempts += data.attempts; + } + }; + + worker.onerror = (err) => { + this._stopWorkers(); + this.running = false; + reject(new Error(err.message || 'Worker error')); + }; + + worker.postMessage({ type: 'start', prefix, progressInterval: 200 }); + this.workers.push(worker); + } + }); + } + + _stopWorkers() { + for (const worker of this.workers) { + try { worker.postMessage({ type: 'stop' }); } catch (e) {} + setTimeout(() => worker.terminate(), 500); + } + this.workers = []; + } + + cancel() { + const reject = this._reject; + this._stopWorkers(); + this.running = false; + if (reject) reject(new Error('Cancelled')); + this._resolve = null; + this._reject = null; + } +}