feat: half baked fw upload
This commit is contained in:
107
index.js
107
index.js
@@ -1,5 +1,6 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const SporeApiClient = require('./src/client');
|
||||
|
||||
const app = express();
|
||||
@@ -77,6 +78,112 @@ app.get('/api/node/status/:ip', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// File upload endpoint for firmware updates
|
||||
app.post('/api/node/update', express.raw({ type: 'multipart/form-data', limit: '50mb' }), async (req, res) => {
|
||||
try {
|
||||
const nodeIp = req.query.ip || req.headers['x-node-ip'];
|
||||
|
||||
if (!nodeIp) {
|
||||
return res.status(400).json({
|
||||
error: 'Node IP address is required',
|
||||
message: 'Please provide the target node IP address'
|
||||
});
|
||||
}
|
||||
|
||||
// Parse multipart form data manually
|
||||
const boundary = req.headers['content-type']?.split('boundary=')[1];
|
||||
if (!boundary) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid content type',
|
||||
message: 'Expected multipart/form-data with boundary'
|
||||
});
|
||||
}
|
||||
|
||||
// Parse the multipart data to extract the file
|
||||
const fileData = parseMultipartData(req.body, boundary);
|
||||
|
||||
if (!fileData.file) {
|
||||
return res.status(400).json({
|
||||
error: 'No file data received',
|
||||
message: 'Please select a firmware file to upload'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Uploading firmware to node ${nodeIp}, file size: ${fileData.file.data.length} bytes, filename: ${fileData.file.filename}`);
|
||||
|
||||
// Create a temporary client for the specific node
|
||||
const nodeClient = new SporeApiClient(`http://${nodeIp}`);
|
||||
|
||||
// Send the firmware data to the node using multipart form data
|
||||
const updateResult = await nodeClient.updateFirmware(fileData.file.data, fileData.file.filename);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Firmware uploaded successfully',
|
||||
nodeIp: nodeIp,
|
||||
fileSize: fileData.file.data.length,
|
||||
filename: fileData.file.filename,
|
||||
result: updateResult
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error uploading firmware:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to upload firmware',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to parse multipart form data
|
||||
function parseMultipartData(buffer, boundary) {
|
||||
const data = {};
|
||||
const boundaryBuffer = Buffer.from(`--${boundary}`);
|
||||
const endBoundaryBuffer = Buffer.from(`--${boundary}--`);
|
||||
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
|
||||
while (true) {
|
||||
// Find the start of the next part
|
||||
start = buffer.indexOf(boundaryBuffer, end);
|
||||
if (start === -1) break;
|
||||
|
||||
// Find the end of this part
|
||||
end = buffer.indexOf(boundaryBuffer, start + boundaryBuffer.length);
|
||||
if (end === -1) {
|
||||
end = buffer.indexOf(endBoundaryBuffer, start + boundaryBuffer.length);
|
||||
if (end === -1) break;
|
||||
}
|
||||
|
||||
// Extract the part content
|
||||
const partBuffer = buffer.slice(start + boundaryBuffer.length + 2, end);
|
||||
|
||||
// Parse headers and content
|
||||
const headerEnd = partBuffer.indexOf('\r\n\r\n');
|
||||
if (headerEnd === -1) continue;
|
||||
|
||||
const headers = partBuffer.slice(0, headerEnd).toString();
|
||||
const content = partBuffer.slice(headerEnd + 4);
|
||||
|
||||
// Parse the Content-Disposition header to get field name and filename
|
||||
const nameMatch = headers.match(/name="([^"]+)"/);
|
||||
const filenameMatch = headers.match(/filename="([^"]+)"/);
|
||||
|
||||
if (nameMatch) {
|
||||
const fieldName = nameMatch[1];
|
||||
const filename = filenameMatch ? filenameMatch[1] : null;
|
||||
|
||||
data[fieldName] = {
|
||||
data: content,
|
||||
filename: filename
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Start the server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on http://localhost:${PORT}`);
|
||||
|
||||
@@ -187,6 +187,7 @@ function displayNodeDetails(container, nodeStatus) {
|
||||
📁 Choose Firmware File
|
||||
</button>
|
||||
<div class="upload-info">Select a .bin or .hex file to upload</div>
|
||||
<div id="upload-status" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -244,6 +245,18 @@ function setupTabs(container) {
|
||||
fileInput.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Set up file input change handler
|
||||
const fileInput = container.querySelector('#firmware-file');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', async (e) => {
|
||||
e.stopPropagation();
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
await uploadFirmware(file, container);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +310,84 @@ async function loadTasksData(container, nodeStatus) {
|
||||
}
|
||||
}
|
||||
|
||||
// Function to upload firmware
|
||||
async function uploadFirmware(file, container) {
|
||||
const uploadStatus = container.querySelector('#upload-status');
|
||||
const uploadBtn = container.querySelector('.upload-btn');
|
||||
const originalText = uploadBtn.textContent;
|
||||
|
||||
try {
|
||||
// Show upload status
|
||||
uploadStatus.style.display = 'block';
|
||||
uploadStatus.innerHTML = `
|
||||
<div class="upload-progress">
|
||||
<div>📤 Uploading ${file.name}...</div>
|
||||
<div style="font-size: 0.8rem; opacity: 0.7;">Size: ${(file.size / 1024).toFixed(1)}KB</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Disable upload button
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.textContent = '⏳ Uploading...';
|
||||
|
||||
// Get the member IP from the card
|
||||
const memberCard = container.closest('.member-card');
|
||||
const memberIp = memberCard.dataset.memberIp;
|
||||
|
||||
if (!memberIp) {
|
||||
throw new Error('Could not determine target node IP address');
|
||||
}
|
||||
|
||||
// Create FormData for multipart upload
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// Upload to backend
|
||||
const response = await fetch('/api/node/update?ip=' + encodeURIComponent(memberIp), {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Show success
|
||||
uploadStatus.innerHTML = `
|
||||
<div class="upload-success">
|
||||
<div>✅ Firmware uploaded successfully!</div>
|
||||
<div style="font-size: 0.8rem; opacity: 0.7;">Node: ${memberIp}</div>
|
||||
<div style="font-size: 0.8rem; opacity: 0.7;">Size: ${(file.size / 1024).toFixed(1)}KB</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
console.log('Firmware upload successful:', result);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Firmware upload failed:', error);
|
||||
|
||||
// Show error
|
||||
uploadStatus.innerHTML = `
|
||||
<div class="upload-error">
|
||||
<div>❌ Upload failed: ${error.message}</div>
|
||||
</div>
|
||||
`;
|
||||
} finally {
|
||||
// Re-enable upload button
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = originalText;
|
||||
|
||||
// Clear file input
|
||||
const fileInput = container.querySelector('#firmware-file');
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to display cluster members
|
||||
function displayClusterMembers(members, expandedCards = new Map()) {
|
||||
const container = document.getElementById('cluster-members-container');
|
||||
|
||||
@@ -422,6 +422,43 @@ p {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* Upload Status Styles */
|
||||
#upload-status {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
text-align: center;
|
||||
color: #ffa726;
|
||||
}
|
||||
|
||||
.upload-success {
|
||||
text-align: center;
|
||||
color: #4caf50;
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border: 1px solid rgba(76, 175, 80, 0.2);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.upload-error {
|
||||
text-align: center;
|
||||
color: #f44336;
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border: 1px solid rgba(244, 67, 54, 0.2);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.upload-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.no-tasks {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
|
||||
@@ -192,6 +192,22 @@ class SporeApiClient {
|
||||
async factoryReset() {
|
||||
return this.request('POST', '/api/device/factory-reset');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update firmware on the device
|
||||
* @param {Buffer|Uint8Array} firmwareData - Firmware binary data
|
||||
* @returns {Promise<Object>} Update response
|
||||
*/
|
||||
async updateFirmware(firmwareData, filename) {
|
||||
// Send the raw firmware data directly to the SPORE device
|
||||
// The SPORE device expects the file data, not re-encoded multipart
|
||||
return this.request('POST', '/api/node/update', {
|
||||
body: firmwareData,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export the client class
|
||||
|
||||
Reference in New Issue
Block a user