const serialResultsDiv = document.getElementById("serialResults");
const readBar = document.getElementById("readBar");
// Buffer for accumulating received data before logging
let receivedDataBuffer = '';
// Software tool specific variables
let deviceSerialNumber = "";
let deviceVersion = "";
let deviceHwVersion = "";
let selectedSoftwareUrl = "";
// Serial port variables (port, writer, textEncoder, writableStreamClosed declared in PHP)
let reader;
let readableStreamClosed;
let keepReading = true;
// Function to log communication to API (reused from scripts.js)
async function logCommunication(data, direction) {
// Only log if debug mode is enabled
if (typeof DEBUG === 'undefined' || !DEBUG) {
return;
}
try {
const serviceToken = document.getElementById("servicetoken")?.innerHTML || '';
let serialNumber = '';
if (deviceSerialNumber) {
serialNumber = deviceSerialNumber;
}
const logData = {
data: data,
direction: direction,
timestamp: new Date().toISOString(),
serial_number: serialNumber,
maintenance_run: 0
};
const url = link + '/v2/com_log/log';
const bearer = 'Bearer ' + serviceToken;
const response = await fetch(url, {
method: 'POST',
withCredentials: true,
credentials: 'include',
headers: {
'Authorization': bearer,
'Content-Type': 'application/json'
},
body: JSON.stringify(logData)
});
if (!response.ok) {
console.warn('Failed to log communication:', response.status);
}
} catch (error) {
console.warn('Error logging communication:', error);
}
}
// Progress bar function (reused from scripts.js)
function progressBar(percentage, message, color){
readBar.style.background = color;
readBar.style.width = percentage +"%";
readBar.innerHTML = message;
}
// Connect device for software tool
async function connectDeviceForSoftware() {
//clear input
readBar.innerHTML = '';
serialResultsDiv.innerHTML = '';
document.getElementById("softwareCheckStatus").style.display = "none";
document.getElementById("softwareOptions").style.display = "none";
document.getElementById("noUpdatesMessage").style.display = "none";
document.getElementById("uploadSection").style.display = "none";
// Reset data
receivedDataBuffer = '';
deviceSerialNumber = "";
deviceVersion = "";
deviceHwVersion = "";
//set progress bar
progressBar("1", "", "");
// Check if DEBUG mode is enabled - use mock device data
if (typeof DEBUG !== 'undefined' && DEBUG) {
// TEST MODE: Use mock device data
deviceSerialNumber = "22110095";
deviceVersion = "03e615af";
deviceHwVersion = "0000080";
document.getElementById("Device_output").style.display = "block";
serialResultsDiv.innerHTML = `DEBUG MODE - Simulated Device Data:
SN=${deviceSerialNumber}
FW=${deviceVersion}
HW=${deviceHwVersion}`;
progressBar("60", "DEBUG MODE - Device data read - SN: " + deviceSerialNumber, "#ff9800");
await logCommunication(`DEBUG MODE: Simulated device - SN=${deviceSerialNumber}, Version=${deviceVersion}, HW=${deviceHwVersion}`, 'handshake');
// Proceed to check software availability
checkSoftwareAvailability();
return;
}
// Check if port is already open - if so, refresh the page to start clean
if (port) {
await logCommunication('Port already in use - refreshing page for clean reconnect', 'info');
location.reload();
return;
}
// Reset flags for new connection
keepReading = true;
try {
// Prompt user to select any serial port.
const filters = [{ usbVendorId: 1027, usbProductId: 24597 }];
port = await navigator.serial.requestPort({ filters });
// Log selected port details
const portInfo = port.getInfo();
const portDetails = {
processStep: 'Software',
usbVendorId: portInfo.usbVendorId,
usbProductId: portInfo.usbProductId,
readable: !!port.readable,
writable: !!port.writable,
opened: port.readable !== null && port.writable !== null
};
await logCommunication(`Selected USB device - ${JSON.stringify(portDetails)}`, 'connected');
await port.open({
baudRate: 56700,
dataBits: 8,
stopBits: 1,
parity: 'none',
flowControl: 'none'
});
progressBar("10", "Connecting", "#04AA6D");
// Log successful connection
await logCommunication('Port opened successfully', 'connected');
textEncoder = new TextEncoderStream();
writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
writer = textEncoder.writable.getWriter();
progressBar("20", "Connected, reading device...", "#04AA6D");
// Read device output
await readDeviceOutput();
// Close the port immediately after reading
await closePortAfterRead();
// Check for software updates (port is now closed)
checkSoftwareAvailability();
} catch (error) {
await logCommunication(`Connection error: ${error.message}`, 'error');
// Check for specific "No port selected" error and show user-friendly message
if (error.message && error.message.includes('No port selected by the user')) {
progressBar("100", "No device selected, please try again", "#ff6666");
} else {
progressBar("100", "Error: " + error.message, "#ff6666");
}
}
}
async function readDeviceOutput() {
const textDecoder = new TextDecoderStream();
readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
reader = textDecoder.readable.getReader();
document.getElementById("Device_output").style.display = "block";
let dataCompleteMarkerFound = false;
const startTime = Date.now();
const timeout = 10000; // 10 second timeout
try {
progressBar("40", "Reading device...", "#04AA6D");
while (keepReading) {
const { value, done } = await reader.read();
if (done || !keepReading) {
break;
}
receivedDataBuffer += value;
serialResultsDiv.innerHTML = receivedDataBuffer;
// Check if we have received PLUGTY which comes after all device data
// Or wait for at least FWDATE which comes near the end
if (receivedDataBuffer.indexOf("PLUGTY") > 0 || receivedDataBuffer.indexOf("FWDATE=") > 0) {
dataCompleteMarkerFound = true;
}
// Only parse and exit if we found the completion marker
if (dataCompleteMarkerFound) {
// Parse device info from complete output
parseDeviceInfo(receivedDataBuffer);
// Check if we have all required data
if (deviceSerialNumber && deviceVersion && deviceHwVersion) {
progressBar("60", "Device data read - SN: " + deviceSerialNumber, "#04AA6D");
await logCommunication(`Device identification data received: SN=${deviceSerialNumber}, Version=${deviceVersion}, HW=${deviceHwVersion}`, 'handshake');
// Exit cleanly
break;
}
}
// Timeout check
if (Date.now() - startTime > timeout) {
await logCommunication('Device read timeout - proceeding with partial data', 'error');
parseDeviceInfo(receivedDataBuffer);
break;
}
}
progressBar("65", "Device reading completed", "#04AA6D");
} catch (error) {
if (keepReading) {
await logCommunication(`Read error: ${error.message}`, 'error');
progressBar("0", "Error reading device: " + error.message, "#ff6666");
}
} finally {
// Clean up reader and cancel the stream
try {
if (reader) {
await reader.cancel();
reader.releaseLock();
}
} catch (e) {
console.log('Reader cleanup error:', e);
}
}
}
function parseDeviceInfo(data) {
// Extract SN, FW (firmware/software version), and HW from device output
// Device format: SN=12345678;FW=12345678;HW=12345678;STATE=...
// Match exact logic from scripts.js and readdevice.js
// Use exact same approach as scripts.js line 153 and readdevice.js line 649
const x = Array.from(new Set(data.split(";"))).toString();
// Parse SN= (8 characters, exact logic from scripts.js line 185-189)
if (x.indexOf("SN=") > 0 && !deviceSerialNumber) {
const a = x.indexOf("SN=");
const b = a + 3;
const c = b + 8;
deviceSerialNumber = x.substring(b, c);
console.log("Found SN:", deviceSerialNumber);
}
// Parse FW= (8 characters, exact logic from readdevice.js line 672-676)
if (x.indexOf("FW=") > 0 && !deviceVersion) {
const a = x.indexOf("FW=");
const b = a + 3;
const c = b + 8;
deviceVersion = x.substring(b, c);
console.log("Found FW/Version:", deviceVersion);
}
// Parse HW= (8 characters, exact logic from readdevice.js line 665-670)
if (x.indexOf("HW=") > 0 && !deviceHwVersion) {
const a = x.indexOf("HW=");
const b = a + 3;
const c = b + 8;
deviceHwVersion = x.substring(b, c);
console.log("Found HW Version:", deviceHwVersion);
}
}
async function closePortAfterRead() {
if (port) {
try {
await logCommunication('Closing port after reading device data', 'info');
// Reader is already cancelled and released by readDeviceOutput finally block
// Now abort the writer
if (writer) {
try {
await writer.abort();
} catch (e) {
console.log('Writer abort error:', e);
}
}
// Give time for streams to cleanup
await new Promise(resolve => setTimeout(resolve, 300));
// Close the port
await port.close();
await logCommunication('Port closed successfully', 'info');
// Reset for next connection
reader = null;
writer = null;
readableStreamClosed = null;
writableStreamClosed = null;
port = null;
} catch (error) {
console.error('Error closing port after read:', error);
await logCommunication(`Error closing port: ${error.message}`, 'error');
// Force reset even on error
reader = null;
writer = null;
readableStreamClosed = null;
writableStreamClosed = null;
port = null;
}
}
}
async function closePort() {
if (port) {
try {
keepReading = false;
// Wait for read operations to complete
await new Promise(resolve => setTimeout(resolve, 300));
// Release reader if still locked
if (reader) {
try {
reader.releaseLock();
} catch (e) {
console.log('Reader release error:', e);
}
}
// Wait a bit more
await new Promise(resolve => setTimeout(resolve, 200));
// Now close the port
try {
await port.close();
await logCommunication('Port closed', 'info');
} catch (e) {
console.log('Port close error (may already be closed):', e);
}
// Reset for next connection
reader = null;
port = null;
} catch (error) {
console.error('Error closing port:', error);
}
}
}
async function checkSoftwareAvailability() {
if (!deviceSerialNumber) {
progressBar("0", "Error: Serial number not found", "#ff6666");
await logCommunication('Serial number not found in device output', 'error');
return;
}
document.getElementById("softwareCheckStatus").style.display = "block";
progressBar("70", "Checking for software updates...", "#04AA6D");
try {
// Call software_available API
const availableUrl = link + "/v2/software_available/sn=" + deviceSerialNumber +
(deviceVersion ? "&version=" + deviceVersion : "") +
(deviceHwVersion ? "&hw_version=" + deviceHwVersion : "");
console.log("Calling API:", availableUrl);
await logCommunication(`Checking software availability for SN: ${deviceSerialNumber}`, 'sent');
const availableResponse = await fetch(availableUrl, {
method: "GET",
headers: {
"Authorization": "Bearer " + document.getElementById("servicetoken").textContent,
"Content-Type": "application/json"
}
});
const availableData = await availableResponse.json();
console.log("Available response:", availableData);
await logCommunication(`Software availability response: ${JSON.stringify(availableData)}`, 'received');
if (availableData.software_available === "error" || availableData.error) {
// Error checking for updates (e.g., invalid hardware version)
document.getElementById("softwareCheckStatus").style.display = "none";
progressBar("0", "Error: " + (availableData.error || "Unable to check for updates"), "#ff6666");
alert("Error checking for updates: " + (availableData.error || "Unknown error"));
} else if (availableData.software_available === "yes") {
// Software updates available, fetch options
progressBar("80", "Software updates found, loading options...", "#04AA6D");
await fetchSoftwareOptions();
} else {
// No updates available
document.getElementById("softwareCheckStatus").style.display = "none";
document.getElementById("noUpdatesMessage").style.display = "block";
progressBar("100", "No software updates available", "#04AA6D");
}
} catch (error) {
await logCommunication(`Software check error: ${error.message}`, 'error');
progressBar("0", "Error checking software: " + error.message, "#ff6666");
await closePort();
}
}
async function fetchSoftwareOptions() {
try {
// Call software_update API to get options
const updateUrl = link + "/v2/software_update/sn=" + deviceSerialNumber +
(deviceVersion ? "&version=" + deviceVersion : "") +
(deviceHwVersion ? "&hw_version=" + deviceHwVersion : "");
console.log("Fetching options from:", updateUrl);
await logCommunication(`Fetching software options for SN: ${deviceSerialNumber}`, 'sent');
const updateResponse = await fetch(updateUrl, {
method: "GET",
headers: {
"Authorization": "Bearer " + document.getElementById("servicetoken").textContent,
"Content-Type": "application/json"
}
});
const options = await updateResponse.json();
console.log("Software options:", options);
await logCommunication(`Software options response: ${JSON.stringify(options)}`, 'received');
if (options.error) {
document.getElementById("softwareCheckStatus").style.display = "none";
document.getElementById("noUpdatesMessage").style.display = "block";
progressBar("100", "No software updates available", "#04AA6D");
await closePort();
return;
}
// Display options in table
displaySoftwareOptions(options);
document.getElementById("softwareCheckStatus").style.display = "none";
document.getElementById("softwareOptions").style.display = "block";
progressBar("100", "Software options loaded", "#04AA6D");
} catch (error) {
await logCommunication(`Software options error: ${error.message}`, 'error');
progressBar("0", "Error loading options: " + error.message, "#ff6666");
await closePort();
}
}
function displaySoftwareOptions(options) {
const grid = document.getElementById("softwareOptionsGrid");
grid.innerHTML = "";
options.forEach((option, index) => {
const price = parseFloat(option.price);
const isFree = price === 0;
const isCurrent = option.is_current === true || option.is_current === 1;
// Create card with gradient background
const card = document.createElement("div");
card.style.cssText = `
background: ${isCurrent ? 'linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%)' : 'linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%)'};
border-radius: 4px;
padding: 25px 20px;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
position: relative;
overflow: visible;
transform: translateY(0px);
box-shadow: ${isCurrent ? '0 4px 12px rgba(0,0,0,0.08)' : '0 8px 20px rgba(0,0,0,0.12)'};
opacity: ${isCurrent ? '0.7' : '1'};
pointer-events: ${isCurrent ? 'none' : 'auto'};
min-height: 320px;
`;
if (!isCurrent) {
card.onmouseenter = () => {
card.style.transform = 'translateY(-8px) scale(1.02)';
card.style.boxShadow = '0 12px 28px rgba(0,0,0,0.2)';
card.style.borderColor = isFree ? '#038f5a' : '#FF4500';
};
card.onmouseleave = () => {
card.style.transform = 'translateY(0) scale(1)';
card.style.boxShadow = '0 8px 20px rgba(0,0,0,0.12)';
card.style.borderColor = isFree ? '#04AA6D' : '#FF6B35';
};
}
// Badge for current/free/paid - VISIBLE
const badge = document.createElement("div");
badge.style.cssText = `
position: absolute;
top: -10px;
right: 20px;
background: ${isCurrent ? '#6c757d' : (isFree ? 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)' : 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)')};
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
text-transform: uppercase;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
`;
if (isCurrent) {
badge.textContent = "INSTALLED";
} else if (isFree) {
badge.textContent = "FREE";
} else {
badge.textContent = "PREMIUM";
}
card.appendChild(badge);
// Name with icon
const name = document.createElement("h4");
name.style.cssText = `
margin: 0 0 12px 0;
color: #333;
font-size: 22px;
font-weight: 700;
`;
name.innerHTML = `${option.name || "Software Update"}`;
card.appendChild(name);
// Version with enhanced styling
const version = document.createElement("div");
version.style.cssText = `
color: #666;
font-size: 14px;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 6px;
`;
version.innerHTML = ` Version: ${option.version || "N/A"}`;
card.appendChild(version);
// Description with preserved newlines
const descContainer = document.createElement("div");
descContainer.style.cssText = `
color: #555;
font-size: 13px;
line-height: 1.7;
margin: 0 0 20px 0;
flex-grow: 1;
white-space: pre-line;
`;
descContainer.textContent = option.description || "No description available";
card.appendChild(descContainer);
// Price section
const priceSection = document.createElement("div");
priceSection.style.cssText = `
border-top: 2px solid ${isFree ? '#04AA6D20' : '#FF6B3520'};
padding-top: 20px;
margin-top: auto;
`;
const priceText = document.createElement("div");
priceText.style.cssText = `
font-size: ${isCurrent ? '18px' : '28px'};
font-weight: ${isCurrent ? '600' : '800'};
color: ${isCurrent ? '#6c757d' : (isFree ? '#04AA6D' : '#FF6B35')};
margin-bottom: 15px;
text-align: center;
letter-spacing: 0.5px;
`;
if (isCurrent) {
priceText.innerHTML = ' INSTALLED';
} else {
priceText.innerHTML = isFree
? 'Free'
: `${option.currency || "€"} ${price.toFixed(2)}`;
}
priceSection.appendChild(priceText);
// Action button with gradient for paid
const actionBtn = document.createElement("button");
actionBtn.className = "btn";
actionBtn.style.cssText = `
width: 100%;
background: ${isCurrent ? '#6c757d' : (isFree ? 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)' : 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)')};
color: white;
border: none;
cursor: ${isCurrent ? 'not-allowed' : 'pointer'};
transition: all 0.3s ease;
opacity: ${isCurrent ? '0.5' : '1'};
box-shadow: ${isCurrent ? 'none' : '0 4px 12px rgba(0,0,0,0.15)'};
letter-spacing: 0.5px;
text-transform: uppercase;
`;
if (isCurrent) {
actionBtn.innerHTML = ' Currently Installed';
actionBtn.disabled = true;
} else if (isFree) {
actionBtn.innerHTML = '';
actionBtn.onclick = () => selectUpgrade(option);
actionBtn.onmouseenter = () => {
actionBtn.style.background = 'linear-gradient(135deg, #038f5a 0%, #026b43 100%)';
actionBtn.style.transform = 'translateY(-2px)';
actionBtn.style.boxShadow = '0 6px 16px rgba(0,0,0,0.2)';
};
actionBtn.onmouseleave = () => {
actionBtn.style.background = 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)';
actionBtn.style.transform = 'translateY(0)';
actionBtn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
};
} else {
actionBtn.innerHTML = '';
actionBtn.onclick = () => selectUpgrade(option);
actionBtn.onmouseenter = () => {
actionBtn.style.background = 'linear-gradient(135deg, #FF4500 0%, #CC3700 100%)';
actionBtn.style.transform = 'translateY(-2px)';
actionBtn.style.boxShadow = '0 6px 16px rgba(255,107,53,0.4)';
};
actionBtn.onmouseleave = () => {
actionBtn.style.background = 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)';
actionBtn.style.transform = 'translateY(0)';
actionBtn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
};
}
priceSection.appendChild(actionBtn);
card.appendChild(priceSection);
grid.appendChild(card);
});
}
async function selectUpgrade(option) {
const price = parseFloat(option.price || 0);
const isFree = price === 0;
// If paid upgrade, show payment modal first
if (!isFree) {
showPaymentModal(option);
return;
}
// Free upgrade - show confirmation modal first
showFreeInstallModal(option);
}
function showFreeInstallModal(option) {
// Create modal overlay
const modal = document.createElement("div");
modal.id = "freeInstallModal";
modal.style.cssText = `
display: flex;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
`;
// Create modal content
const modalContent = document.createElement("div");
modalContent.style.cssText = `
background: white;
border-radius: 8px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
margin: 20px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
`;
modalContent.innerHTML = `
Version: ${option.version || "N/A"}
${option.description || ""}
Version: ${option.version || "N/A"}
${option.description || ""}
Installing Software...
Please keep your device connected and do not close this page
`; // Insert status before the hidden upload section document.getElementById("uploadSection").parentNode.insertBefore(installStatus, document.getElementById("uploadSection")); progressBar("60", "Starting automatic installation...", "#04AA6D"); // Enable the upload button and automatically click it setTimeout(() => { uploadBtn.disabled = false; // Start monitoring for completion if (typeof startUploadMonitoring === 'function') { startUploadMonitoring(); } uploadBtn.click(); }, 1000); } } catch (error) { await logCommunication(`Download error: ${error.message}`, 'error'); progressBar("0", "Error downloading software: " + error.message, "#ff6666"); alert("Error: " + error.message); } }