feat: add firmware versioning and device_type support (NebuleAir/ModuleAir)

- Add VERSION file (1.0.0) and changelog.json for firmware tracking
- Add device_type config param (nebuleair_pro default, backward compatible via INSERT OR IGNORE)
- Add device_type select in admin.html Protected Settings
- Add version badge and changelog modal in Updates section
- Add get_firmware_version and get_changelog PHP endpoints
- Display firmware version in update_firmware.sh after git pull

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
PaulVua
2026-02-11 16:12:18 +01:00
parent dc1739e033
commit 50a8cdd938
6 changed files with 230 additions and 2 deletions

1
VERSION Normal file
View File

@@ -0,0 +1 @@
1.0.0

21
changelog.json Normal file
View File

@@ -0,0 +1,21 @@
{
"versions": [
{
"version": "1.0.0",
"date": "2026-02-11",
"changes": {
"features": [
"Support multi-device : NebuleAir Pro / ModuleAir Pro",
"Systeme de versioning firmware",
"Changelog viewer dans l'interface web"
],
"improvements": [],
"fixes": [],
"compatibility": [
"Les capteurs existants sont automatiquement configures en 'nebuleair_pro'"
]
},
"notes": "Premiere version tracee. Les capteurs anterieurs recevront device_type=nebuleair_pro par defaut lors de la mise a jour."
}
]
}

View File

@@ -174,6 +174,13 @@
</label> </label>
</div> </div>
<div class="mb-3">
<label for="device_type" class="form-label">Device Type</label>
<select class="form-select protected-checkbox" id="device_type" onchange="update_config_sqlite('device_type', this.value)" disabled>
<option value="nebuleair_pro">NebuleAir Pro</option>
<option value="moduleair_pro">ModuleAir Pro</option>
</select>
</div>
<div class="input-group mb-3" id="sondes_envea_div"></div> <div class="input-group mb-3" id="sondes_envea_div"></div>
@@ -219,7 +226,11 @@
<!-- UPDATE--> <!-- UPDATE-->
<div class="col-lg-4 col-12"> <div class="col-lg-4 col-12">
<h3 class="mt-4">Updates</h3> <div class="d-flex align-items-center mt-4 mb-2">
<h3 class="mb-0 me-2">Updates</h3>
<span id="firmwareVersionBadge" class="badge bg-secondary">Version...</span>
<button type="button" class="btn btn-sm btn-outline-info ms-2" onclick="showChangelogModal()">Changelog</button>
</div>
<button type="submit" class="btn btn-primary" onclick="updateFirmware()" id="updateBtn"> <button type="submit" class="btn btn-primary" onclick="updateFirmware()" id="updateBtn">
<span id="updateBtnText">Update firmware</span> <span id="updateBtnText">Update firmware</span>
@@ -334,6 +345,28 @@
</div> </div>
<!-- Changelog Modal -->
<div class="modal fade" id="changelogModal" tabindex="-1" aria-labelledby="changelogModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="changelogModalLabel">Changelog</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="changelogModalBody">
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</main> </main>
</div> </div>
</div> </div>
@@ -437,6 +470,12 @@ window.onload = function() {
checkbox_aircarto.checked = response["send_aircarto"]; checkbox_aircarto.checked = response["send_aircarto"];
checkbox_miotiq.checked = response["send_miotiq"]; checkbox_miotiq.checked = response["send_miotiq"];
// Set device type
const device_type_select = document.getElementById("device_type");
if (response["device_type"]) {
device_type_select.value = response["device_type"];
}
// Set CPU power mode // Set CPU power mode
const cpu_power_mode_select = document.getElementById("cpu_power_mode"); const cpu_power_mode_select = document.getElementById("cpu_power_mode");
if (response["cpu_power_mode"]) { if (response["cpu_power_mode"]) {
@@ -555,7 +594,10 @@ window.onload = function() {
// Load services on page load // Load services on page load
refreshServices(); refreshServices();
// Load firmware version
loadFirmwareVersion();
} //end window.onload } //end window.onload
@@ -1644,6 +1686,123 @@ function toggleProtectedSettings() {
} }
} }
/*
__ __ _ _
\ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _
\ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` |
\ V / __/ | \__ \ | (_) | | | | | | | | (_| |
\_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, |
|___/
*/
function loadFirmwareVersion() {
$.ajax({
url: 'launcher.php?type=get_firmware_version',
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
const badge = document.getElementById('firmwareVersionBadge');
if (response.success) {
badge.textContent = 'v' + response.version;
badge.className = 'badge bg-primary';
} else {
badge.textContent = 'Version unknown';
badge.className = 'badge bg-secondary';
}
},
error: function() {
const badge = document.getElementById('firmwareVersionBadge');
badge.textContent = 'Version unknown';
badge.className = 'badge bg-secondary';
}
});
}
function showChangelogModal() {
const modal = new bootstrap.Modal(document.getElementById('changelogModal'));
modal.show();
// Load changelog data
$.ajax({
url: 'launcher.php?type=get_changelog',
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
if (response.success && response.changelog) {
displayChangelog(response.changelog);
} else {
document.getElementById('changelogModalBody').innerHTML =
'<div class="alert alert-warning">Could not load changelog.</div>';
}
},
error: function() {
document.getElementById('changelogModalBody').innerHTML =
'<div class="alert alert-danger">Failed to load changelog.</div>';
}
});
}
function displayChangelog(data) {
const container = document.getElementById('changelogModalBody');
let html = '';
data.versions.forEach(function(version) {
html += `<div class="card mb-3">`;
html += `<div class="card-header d-flex justify-content-between align-items-center">`;
html += `<h5 class="mb-0">v${version.version}</h5>`;
html += `<span class="text-muted">${version.date}</span>`;
html += `</div>`;
html += `<div class="card-body">`;
// Features
if (version.changes.features && version.changes.features.length > 0) {
html += `<h6 class="text-success">Features</h6><ul>`;
version.changes.features.forEach(function(f) {
html += `<li>${f}</li>`;
});
html += `</ul>`;
}
// Improvements
if (version.changes.improvements && version.changes.improvements.length > 0) {
html += `<h6 class="text-info">Improvements</h6><ul>`;
version.changes.improvements.forEach(function(i) {
html += `<li>${i}</li>`;
});
html += `</ul>`;
}
// Fixes
if (version.changes.fixes && version.changes.fixes.length > 0) {
html += `<h6 class="text-danger">Fixes</h6><ul>`;
version.changes.fixes.forEach(function(f) {
html += `<li>${f}</li>`;
});
html += `</ul>`;
}
// Compatibility
if (version.changes.compatibility && version.changes.compatibility.length > 0) {
html += `<div class="alert alert-warning mt-2 mb-0"><strong>Compatibility:</strong><ul class="mb-0">`;
version.changes.compatibility.forEach(function(c) {
html += `<li>${c}</li>`;
});
html += `</ul></div>`;
}
// Notes
if (version.notes) {
html += `<p class="text-muted mt-2 mb-0"><em>${version.notes}</em></p>`;
}
html += `</div></div>`;
});
container.innerHTML = html;
}
</script> </script>

View File

@@ -1515,6 +1515,47 @@ if ($type == "detect_envea_device") {
|___/ |___/
*/ */
// Get firmware version from VERSION file
if ($type == "get_firmware_version") {
$versionFile = '/var/www/nebuleair_pro_4g/VERSION';
if (file_exists($versionFile)) {
$version = trim(file_get_contents($versionFile));
echo json_encode([
'success' => true,
'version' => $version
]);
} else {
echo json_encode([
'success' => false,
'version' => 'unknown'
]);
}
}
// Get changelog from changelog.json
if ($type == "get_changelog") {
$changelogFile = '/var/www/nebuleair_pro_4g/changelog.json';
if (file_exists($changelogFile)) {
$changelog = json_decode(file_get_contents($changelogFile), true);
if ($changelog !== null) {
echo json_encode([
'success' => true,
'changelog' => $changelog
]);
} else {
echo json_encode([
'success' => false,
'error' => 'Invalid changelog format'
]);
}
} else {
echo json_encode([
'success' => false,
'error' => 'Changelog file not found'
]);
}
}
// Get current CPU power mode // Get current CPU power mode
if ($type == "get_cpu_power_mode") { if ($type == "get_cpu_power_mode") {
try { try {

View File

@@ -51,6 +51,7 @@ config_entries = [
("MPPT", "0", "bool"), ("MPPT", "0", "bool"),
("NOISE", "0", "bool"), ("NOISE", "0", "bool"),
("modem_version", "XXX", "str"), ("modem_version", "XXX", "str"),
("device_type", "nebuleair_pro", "str"),
("language", "fr", "str"), ("language", "fr", "str"),
("wifi_power_saving", "0", "bool"), ("wifi_power_saving", "0", "bool"),
("cpu_power_mode", "normal", "str") ("cpu_power_mode", "normal", "str")

View File

@@ -57,6 +57,11 @@ fi
git pull origin $(git branch --show-current) git pull origin $(git branch --show-current)
check_status "Git pull" check_status "Git pull"
# Display firmware version
if [ -f "/var/www/nebuleair_pro_4g/VERSION" ]; then
print_status "Firmware version: $(cat /var/www/nebuleair_pro_4g/VERSION)"
fi
# Step 2: Update database configuration # Step 2: Update database configuration
print_status "" print_status ""
print_status "Step 2: Updating database configuration..." print_status "Step 2: Updating database configuration..."