diff --git a/src/configHandler.ts b/src/configHandler.ts new file mode 100644 index 0000000..94f9157 --- /dev/null +++ b/src/configHandler.ts @@ -0,0 +1,41 @@ +// configHandler.ts + +import { getFormValues } from './formHandler'; +import { buildProtobuf } from './protobufBuilder'; +import { generateQRCode } from './qrCodeGenerator'; +import { toUrlSafeBase64 } from './utils'; + +/** + * Generate a QR code and URL based on the form values. + */ +export async function generateConfig(): Promise { + const formValues = getFormValues(); + + // Validate channel name length + const byteLength = new TextEncoder().encode(formValues.channelName).length; + if (byteLength > 12) { + alert(`Channel name must be less than or equal to 12 bytes (current byte length: ${byteLength}).`); + return; + } + + // Generate the protobuf binary data for the form values + const channelSet = buildProtobuf(formValues); + const binaryData = channelSet.toBinary(); // Binary data from Protobuf + + // Convert to URL-safe Base64 string + const base64 = toUrlSafeBase64(binaryData); + + // Update the URL hash with the generated configuration + window.location.hash = `#${base64}`; + + // Create the Meshtastic URL + const meshtasticUrl = `https://meshtastic.org/e/#${base64}`; + console.log("Generated Meshtastic URL:", meshtasticUrl); + + // Update the generated URL in the input field + const urlField = document.getElementById('generatedUrl') as HTMLInputElement; + urlField.value = meshtasticUrl; + + // Generate the QR code from the URL + await generateQRCode(meshtasticUrl); +} diff --git a/src/formHandler.ts b/src/formHandler.ts index c7390e6..29ed39e 100644 --- a/src/formHandler.ts +++ b/src/formHandler.ts @@ -1,5 +1,7 @@ // formHandler.ts +import { determinePskType } from './pskHandler'; + /** * Get the values from the form using FormData API. */ @@ -64,13 +66,8 @@ export function populateForm(formValues: any) { (form.elements.namedItem('ignoreMqtt') as HTMLInputElement).checked = formValues.ignoreMqtt || false; (form.elements.namedItem('configOkToMqtt') as HTMLInputElement).checked = formValues.configOkToMqtt || false; - // Set the pskType based on the byte length of the psk (psk.length / 2) - let pskType = 'none'; // Default to 'none' - if (formValues.psk && formValues.psk.length === 64) { - pskType = 'aes256'; // AES-256 - } else if (formValues.psk && formValues.psk.length === 32) { - pskType = 'aes128'; // AES-128 - } + // Set the pskType based on the byte length of the PSK + const pskType = determinePskType(formValues.psk?.length || 0); // Update the pskType dropdown (form.elements.namedItem('pskType') as HTMLSelectElement).value = pskType; diff --git a/src/loadConfigurationFromHash.ts b/src/loadConfigurationFromHash.ts new file mode 100644 index 0000000..2488fa1 --- /dev/null +++ b/src/loadConfigurationFromHash.ts @@ -0,0 +1,43 @@ +// loadConfigurationFromHash.ts + +import { Protobuf } from "@meshtastic/js"; +import { populateForm } from './formHandler'; +import { determinePskType } from "./pskHandler"; +import { fromUrlSafeBase64 } from './utils'; + +/** + * Load the configuration from the hash and populate the form. + * @param {string} hash - The URL-safe Base64 configuration string. + */ +export function loadConfigurationFromHash(hash: string): void { + try { + const binaryData = fromUrlSafeBase64(hash); + const channelSet = Protobuf.AppOnly.ChannelSet.fromBinary(binaryData); + + // Extract the channel settings from the Protobuf message + const channelSettings = channelSet.settings[0]; + + // Determine PSK type based on the length of the PSK + const pskType = determinePskType(channelSettings.psk.length); + + const formValues = { + channelName: channelSettings.name, + pskType: pskType, // Derived from PSK length + psk: new TextDecoder().decode(channelSettings.psk), + uplinkEnabled: channelSettings.uplinkEnabled, + downlinkEnabled: channelSettings.downlinkEnabled, + positionPrecision: channelSettings.moduleSettings?.positionPrecision || 0, + isClientMuted: channelSettings.moduleSettings?.isClientMuted || false, + region: channelSet.loraConfig.region, + modemPreset: channelSet.loraConfig.modemPreset, + hopLimit: channelSet.loraConfig.hopLimit, + ignoreMqtt: channelSet.loraConfig.ignoreMqtt, + configOkToMqtt: channelSet.loraConfig.configOkToMqtt, + }; + + // Populate the form with these values using formHandler + populateForm(formValues); + } catch (error) { + console.error("Error loading configuration from URL hash:", error); + } +} diff --git a/src/main.ts b/src/main.ts index 84334bb..feb6267 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,17 +1,16 @@ // main.ts -import { getFormValues, populateForm } from './formHandler'; -import { generatePSK } from './pskGenerator'; -import { buildProtobuf } from './protobufBuilder'; -import { generateQRCode } from './qrCodeGenerator'; -import { getByteLength, toUrlSafeBase64, fromUrlSafeBase64 } from './utils'; -import { Protobuf } from "@meshtastic/js"; +import { generateConfig } from './configHandler'; +import { handlePSKTypeChange, handleGeneratePSK } from './pskHandler'; +import { loadConfigurationFromHash } from './loadConfigurationFromHash'; +import { copyUrlToClipboard } from './utils'; /** * Handle the DOMContentLoaded event. * Add event listeners to the buttons and form fields. */ document.addEventListener('DOMContentLoaded', () => { + // Check if there is a configuration in the URL hash and load it const urlHash = window.location.hash.substring(1); // Remove the "#" character if (urlHash) { @@ -35,148 +34,3 @@ document.addEventListener('DOMContentLoaded', () => { // Add click listener for copying the URL document.getElementById('copyUrlButton')?.addEventListener('click', copyUrlToClipboard); }); - -/** - * Load the configuration from the hash and populate the form. - * @param {string} hash - The URL-safe Base64 configuration string. - */ -function loadConfigurationFromHash(hash: string): void { - try { - const binaryData = fromUrlSafeBase64(hash); - const channelSet = Protobuf.AppOnly.ChannelSet.fromBinary(binaryData); - - // Extract the channel settings from the Protobuf message - const channelSettings = channelSet.settings[0]; - - // Determine PSK type based on the length of the PSK - const pskLength = channelSettings.psk.length; - let pskType = 'none'; - if (pskLength === 16) { - pskType = 'aes128'; - } else if (pskLength === 32) { - pskType = 'aes256'; - } - - const formValues = { - channelName: channelSettings.name, - pskType: pskType, // Derived from PSK length - psk: new TextDecoder().decode(channelSettings.psk), - uplinkEnabled: channelSettings.uplinkEnabled, - downlinkEnabled: channelSettings.downlinkEnabled, - positionPrecision: channelSettings.moduleSettings?.positionPrecision || 0, - isClientMuted: channelSettings.moduleSettings?.isClientMuted || false, - region: channelSet.loraConfig.region, - modemPreset: channelSet.loraConfig.modemPreset, - hopLimit: channelSet.loraConfig.hopLimit, - ignoreMqtt: channelSet.loraConfig.ignoreMqtt, - configOkToMqtt: channelSet.loraConfig.configOkToMqtt, - }; - - // Populate the form with these values using formHandler - populateForm(formValues); - } catch (error) { - console.error("Error loading configuration from URL hash:", error); - } -} - -/** - * Handle the change event on the PSK type select element. - * Enable or disable the PSK input field based on the selected PSK type. - */ -function handlePSKTypeChange(): void { - const pskType = (document.getElementById('pskType') as HTMLSelectElement).value; - const pskField = document.getElementById('psk') as HTMLInputElement; - - if (pskType === 'none') { - pskField.value = ''; - pskField.disabled = true; - } else { - pskField.disabled = false; - handleGeneratePSK(); - } - - // Re-generate config when PSK type changes - generateConfig(); -} - -/** - * Handle the click event on the "Generate PSK" button. - * Generate a PSK based on the selected PSK type and set the value in the input field. - */ -function handleGeneratePSK(): void { - const pskType = (document.getElementById('pskType') as HTMLSelectElement).value; - const psk = generatePSK(pskType); - (document.getElementById('psk') as HTMLInputElement).value = psk; - - // Re-generate config when PSK is generated - generateConfig(); -} - -/** - * Generate a QR code and URL based on the form values. - */ -async function generateConfig(): Promise { - const formValues = getFormValues(); - - // Validate channel name length - const byteLength = getByteLength(formValues.channelName); - if (byteLength > 12) { - alert(`Channel name must be less than or equal to 12 bytes (current byte length: ${byteLength}).`); - return; - } - - // Generate the protobuf binary data for the form values - const channelSet = buildProtobuf(formValues); - const binaryData = channelSet.toBinary(); // Binary data from Protobuf - - // Convert to URL-safe Base64 string - const base64 = toUrlSafeBase64(binaryData); - - // Update the URL hash with the generated configuration - window.location.hash = `#${base64}`; - - // Create the Meshtastic URL - const meshtasticUrl = `https://meshtastic.org/e/#${base64}`; - console.log("Generated Meshtastic URL:", meshtasticUrl); - - // Update the generated URL in the input field - const urlField = document.getElementById('generatedUrl') as HTMLInputElement; - urlField.value = meshtasticUrl; - - // Generate the QR code from the URL - await generateQRCode(meshtasticUrl); -} - -/** - * Copy the generated URL to the clipboard. - * @returns void - */ -function copyUrlToClipboard(): void { - const urlField = document.getElementById('generatedUrl') as HTMLInputElement; - - navigator.clipboard.writeText(urlField.value) - .then(() => { - showCopyNotification(); - }) - .catch(err => { - console.error('Failed to copy text: ', err); - alert('Failed to copy URL'); - }); -} - -/** - * Show the "URL copied" notification with animation - */ -function showCopyNotification(): void { - const notification = document.getElementById('copyNotification'); - - if (notification) { - notification.classList.add('show'); - - // Hide the notification after 2 seconds - setTimeout(() => { - notification.classList.remove('show'); - }, 2000); - } -} - diff --git a/src/pskGenerator.ts b/src/pskGenerator.ts deleted file mode 100644 index 5399995..0000000 --- a/src/pskGenerator.ts +++ /dev/null @@ -1,21 +0,0 @@ -// pskGenerator.ts - -/** - * Generate a PSK (Pre-Shared Key) for the given PSK type. - * @param pskType - * @returns - */ -export function generatePSK(pskType: string): string { - let pskBytes: Uint8Array; - - if (pskType === 'aes128') { - pskBytes = new Uint8Array(16); - } else if (pskType === 'aes256') { - pskBytes = new Uint8Array(32); - } else { - return ''; // No encryption - } - - window.crypto.getRandomValues(pskBytes); - return Array.from(pskBytes).map(b => ('0' + b.toString(16)).slice(-2)).join(''); -} diff --git a/src/pskHandler.ts b/src/pskHandler.ts new file mode 100644 index 0000000..e1d4d9b --- /dev/null +++ b/src/pskHandler.ts @@ -0,0 +1,82 @@ +// pskHandler.ts + +import { generateConfig } from './configHandler'; + +/** + * Generate a PSK (Pre-Shared Key) for the given PSK type. + * @param pskType + * @returns + */ +export function generatePSK(pskType: string): string { + const pskBytes = new Uint8Array(getPskLengthFromType(pskType)); + + if (pskBytes.length === 0) { + return ''; // No encryption + } + + window.crypto.getRandomValues(pskBytes); + return Array.from(pskBytes).map(b => ('0' + b.toString(16)).slice(-2)).join(''); +} + +/** + * Handle the change event on the PSK type select element. + * Enable or disable the PSK input field based on the selected PSK type. + */ +export function handlePSKTypeChange(): void { + const pskType = (document.getElementById('pskType') as HTMLSelectElement).value; + const pskField = document.getElementById('psk') as HTMLInputElement; + + if (pskType === 'none') { + pskField.value = ''; + pskField.disabled = true; + } else { + pskField.disabled = false; + handleGeneratePSK(); + } + + // Re-generate config when PSK type changes + generateConfig(); +} + +/** + * Handle the click event on the "Generate PSK" button. + * Generate a PSK based on the selected PSK type and set the value in the input field. + */ +export function handleGeneratePSK(): void { + const pskType = (document.getElementById('pskType') as HTMLSelectElement).value; + const psk = generatePSK(pskType); + (document.getElementById('psk') as HTMLInputElement).value = psk; + + // Re-generate config when PSK is generated + generateConfig(); +} + +/** + * Determine the PSK type based on the PSK length. + * @param pskLength - The length of the PSK string. + * @returns {string} - 'none', 'aes128', or 'aes256' + */ +export function determinePskType(pskLength: number): string { + if (pskLength === 32) { // AES-128 (16 bytes * 2 hex chars per byte) + return 'aes128'; + } else if (pskLength === 64) { // AES-256 (32 bytes * 2 hex chars per byte) + return 'aes256'; + } else { + return 'none'; + } +} + +/** + * Get the PSK byte length based on the PSK type. + * @param {string} pskType - 'none', 'aes128', or 'aes256' + * @returns {number} - The expected byte length for the PSK + */ +export function getPskLengthFromType(pskType: string): number { + if (pskType === 'aes128') { + return 16; // AES-128: 16 bytes + } else if (pskType === 'aes256') { + return 32; // AES-256: 32 bytes + } else { + return 0; // No encryption + } +} diff --git a/src/utils.ts b/src/utils.ts index fe4da57..cec2e30 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -39,3 +39,36 @@ export function fromUrlSafeBase64(base64String: string): Uint8Array { return toByteArray(paddedBase64); } + +/** + * Copy the generated URL to the clipboard. + * @returns void + */ +export function copyUrlToClipboard(): void { + const urlField = document.getElementById('generatedUrl') as HTMLInputElement; + + navigator.clipboard.writeText(urlField.value) + .then(() => { + showCopyNotification(); + }) + .catch(err => { + console.error('Failed to copy text: ', err); + alert('Failed to copy URL'); + }); +} + +/** + * Show the "URL copied" notification with animation + */ +export function showCopyNotification(): void { + const notification = document.getElementById('copyNotification'); + + if (notification) { + notification.classList.add('show'); + + // Hide the notification after 2 seconds + setTimeout(() => { + notification.classList.remove('show'); + }, 2000); + } +} diff --git a/tsconfig.json b/tsconfig.json index 0d33162..19afeef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,6 @@ "resolveJsonModule": true, "isolatedModules": true }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "src/loadConfigurationFromHash.ts", "src/configHandler.ts"], "exclude": ["node_modules"] }