1
0

Improve device and version detection

This commit is contained in:
2026-03-19 12:19:18 +01:00
parent 7343666bbc
commit 7bb5c255f5
6 changed files with 146 additions and 26 deletions

View File

@@ -110,16 +110,20 @@ function setupFirmwareSymlink() {
* @param {import('@playwright/test').Page} page
* @param {object} opts
* @param {boolean} [opts.hasNickelMenu=false] - Whether .adds/nm/ exists on device
* @param {string} [opts.firmware='4.45.23646'] - Firmware version to report
* @param {string} [opts.serial='N4280A0000000'] - Serial number to report
*/
async function injectMockDevice(page, opts = {}) {
await page.evaluate(({ hasNickelMenu }) => {
const firmware = opts.firmware || '4.45.23646';
const serial = opts.serial || 'N4280A0000000';
await page.evaluate(({ hasNickelMenu, firmware, serial }) => {
// In-memory filesystem for the mock device
const filesystem = {
'.kobo': {
_type: 'dir',
'version': {
_type: 'file',
content: 'N4280A0000000,4.9.77,4.45.23646,4.9.77,4.9.77,00000000-0000-0000-0000-000000000390',
content: serial + ',4.9.77,' + firmware + ',4.9.77,4.9.77,00000000-0000-0000-0000-000000000390',
},
'Kobo': {
_type: 'dir',
@@ -205,7 +209,7 @@ async function injectMockDevice(page, opts = {}) {
// Override showDirectoryPicker
window.showDirectoryPicker = async () => rootHandle;
}, { hasNickelMenu: opts.hasNickelMenu || false });
}, { hasNickelMenu: opts.hasNickelMenu || false, firmware: firmware, serial: serial });
}
/**
@@ -686,6 +690,59 @@ test.describe('Custom patches', () => {
expect(actualHash, 'restored KoboRoot.tgz SHA1 mismatch').toBe(ORIGINAL_TGZ_SHA1);
});
test('with device — incompatible version 5.x shows error', async ({ page }) => {
await page.goto('/');
await injectMockDevice(page, { firmware: '5.0.0' });
await page.click('#btn-connect');
// Device info should be displayed
await expect(page.locator('#step-device')).not.toBeHidden();
await expect(page.locator('#device-model')).toHaveText('Kobo Libra Colour');
await expect(page.locator('#device-firmware')).toHaveText('5.0.0');
// Status message should show incompatibility warning
await expect(page.locator('#device-status')).toContainText('incompatible');
await expect(page.locator('#device-status')).toContainText('NickelMenu does not support it');
await expect(page.locator('#device-status')).toHaveClass(/error/);
// Continue and restore buttons should be hidden
await expect(page.locator('#btn-device-next')).toBeHidden();
await expect(page.locator('#btn-device-restore')).toBeHidden();
});
test('with device — unknown model shows warning and requires checkbox', async ({ page }) => {
await page.goto('/');
await injectMockDevice(page, { serial: 'X9990A0000000' });
await page.click('#btn-connect');
// Device info should be displayed with unknown model
await expect(page.locator('#step-device')).not.toBeHidden();
await expect(page.locator('#device-model')).toContainText('Unknown');
await expect(page.locator('#device-firmware')).toHaveText('4.45.23646');
// Warning should be visible with GitHub link
await expect(page.locator('#device-unknown-warning')).not.toBeHidden();
await expect(page.locator('#device-unknown-warning')).toContainText('file an issue on GitHub');
await expect(page.locator('#device-unknown-warning a')).toHaveAttribute('href', 'https://github.com/nicoverbruggen/kobopatch-webui/issues/new');
// Checkbox should be visible, Continue should be disabled
await expect(page.locator('#device-unknown-ack')).not.toBeHidden();
await expect(page.locator('#btn-device-next')).toBeVisible();
await expect(page.locator('#btn-device-next')).toBeDisabled();
// Restore Software should be hidden (no firmware URL for unknown model)
await expect(page.locator('#btn-device-restore')).toBeHidden();
// Checking the checkbox enables Continue
await page.check('#device-unknown-checkbox');
await expect(page.locator('#btn-device-next')).toBeEnabled();
// Custom patches should be disabled in mode selection (no firmware URL)
await page.click('#btn-device-next');
await expect(page.locator('#step-mode')).not.toBeHidden();
await expect(page.locator('input[name="mode"][value="patches"]')).toBeDisabled();
});
test('no device — both modes available in manual mode', async ({ page }) => {
await page.goto('/');

View File

@@ -522,6 +522,22 @@ button.btn-success:hover {
font-size: 0.88rem;
}
.device-unknown-ack {
display: flex;
align-items: flex-start;
gap: 0.6rem;
padding: 0.5rem 0;
font-size: 0.83rem;
color: var(--text);
cursor: pointer;
}
.device-unknown-ack input[type="checkbox"] {
flex-shrink: 0;
margin-top: 0.15rem;
accent-color: var(--primary);
}
/* Status banners */
.warning {
background: var(--warning-bg);

View File

@@ -28,7 +28,7 @@
@keyframes spin { to { transform: rotate(360deg); } }
[hidden] { display: none !important; }
</style>
<link rel="stylesheet" href="css/style.css?ts=1773771588">
<link rel="stylesheet" href="css/style.css?ts=1773916731">
<script src="js/jszip.min.js"></script>
</head>
<body>
@@ -122,6 +122,15 @@
</div>
</div>
<p id="device-status"></p>
<p id="device-unknown-warning" class="warning" hidden>
You seem to have a Kobo device that isn't currently being detected. While you can continue since a supported software version seems to be installed, please
<a href="https://github.com/nicoverbruggen/kobopatch-webui/issues/new" target="_blank">file an issue on GitHub</a>
so the developer can add this device to the list.
</p>
<label id="device-unknown-ack" class="device-unknown-ack" hidden>
<input type="checkbox" id="device-unknown-checkbox">
<span>I understand that this model is likely not officially supported by NickelMenu yet, but I wish to continue regardless, and I understand it may not work correctly.</span>
</label>
<div class="step-actions">
<button id="btn-device-restore" class="secondary">Restore Software</button>
<button id="btn-device-next" class="primary">Continue &#x203A;</button>
@@ -434,10 +443,10 @@
</dialog>
<!-- wasm_exec.js loaded by patch-worker.js inside the Web Worker -->
<script src="js/kobo-device.js?ts=1773771588"></script>
<script src="js/kobopatch.js?ts=1773771588"></script>
<script src="js/patch-ui.js?ts=1773771588"></script>
<script src="js/nickelmenu.js?ts=1773771588"></script>
<script src="js/app.js?ts=1773771588"></script>
<script src="js/kobo-device.js?ts=1773916731"></script>
<script src="js/kobopatch.js?ts=1773916731"></script>
<script src="js/patch-ui.js?ts=1773916731"></script>
<script src="js/nickelmenu.js?ts=1773916731"></script>
<script src="js/app.js?ts=1773916731"></script>
</body>
</html>

View File

@@ -97,6 +97,9 @@
const errorMessage = $('error-message');
const errorLog = $('error-log');
const deviceStatus = $('device-status');
const deviceUnknownWarning = $('device-unknown-warning');
const deviceUnknownAck = $('device-unknown-ack');
const deviceUnknownCheckbox = $('device-unknown-checkbox');
const patchContainer = $('patch-container');
const buildStatus = $('build-status');
const existingTgzWarning = $('existing-tgz-warning');
@@ -264,37 +267,66 @@
});
// Auto connect -> show device info
function displayDeviceInfo(info) {
$('device-model').textContent = info.model;
const serialEl = $('device-serial');
serialEl.textContent = '';
const prefixLen = info.serialPrefix.length;
const u = document.createElement('u');
u.textContent = info.serial.slice(0, prefixLen);
serialEl.appendChild(u);
serialEl.appendChild(document.createTextNode(info.serial.slice(prefixLen)));
$('device-firmware').textContent = info.firmware;
}
btnConnect.addEventListener('click', async () => {
try {
const info = await device.connect();
$('device-model').textContent = info.model;
const serialEl = $('device-serial');
serialEl.textContent = '';
const prefixLen = info.serialPrefix.length;
const u = document.createElement('u');
u.textContent = info.serial.slice(0, prefixLen);
serialEl.appendChild(u);
serialEl.appendChild(document.createTextNode(info.serial.slice(prefixLen)));
$('device-firmware').textContent = info.firmware;
displayDeviceInfo(info);
if (info.isIncompatible) {
deviceStatus.textContent =
'You seem to have an incompatible Kobo software version installed. ' +
'NickelMenu does not support it, and the custom patches are incompatible with this version.';
deviceStatus.classList.add('error');
btnDeviceNext.hidden = true;
btnDeviceRestore.hidden = true;
showStep(stepDevice);
return;
}
selectedPrefix = info.serialPrefix;
await availablePatchesReady;
const match = availablePatches.find(p => p.version === info.firmware);
configureFirmwareStep(info.firmware, info.serialPrefix);
if (match) {
await patchUI.loadFromURL('patches/' + match.filename);
patchUI.render(patchContainer);
updatePatchCount();
patchesLoaded = true;
configureFirmwareStep(info.firmware, info.serialPrefix);
btnDeviceRestore.hidden = false;
} else {
btnDeviceRestore.hidden = true;
}
deviceStatus.textContent = 'Your device has been recognized. You can continue to the next step!';
btnDeviceRestore.hidden = !patchesLoaded || !firmwareURL;
deviceStatus.classList.remove('error');
const isUnknownModel = info.model.startsWith('Unknown');
if (isUnknownModel) {
deviceStatus.textContent = '';
deviceUnknownWarning.hidden = false;
deviceUnknownAck.hidden = false;
deviceUnknownCheckbox.checked = false;
btnDeviceNext.disabled = true;
} else {
deviceStatus.textContent = 'Your device has been recognized. You can continue to the next step!';
deviceUnknownWarning.hidden = true;
deviceUnknownAck.hidden = true;
deviceUnknownCheckbox.checked = false;
btnDeviceNext.disabled = false;
}
btnDeviceNext.hidden = false;
showStep(stepDevice);
} catch (err) {
@@ -308,6 +340,10 @@
goToModeSelection();
});
deviceUnknownCheckbox.addEventListener('change', () => {
btnDeviceNext.disabled = !deviceUnknownCheckbox.checked;
});
btnDeviceRestore.addEventListener('click', () => {
if (!patchesLoaded) return;
selectedMode = 'patches';
@@ -329,10 +365,10 @@
// --- Step 2: Mode selection ---
function goToModeSelection() {
// In auto mode, disable custom patches if firmware isn't supported
// In auto mode, disable custom patches if firmware or download URL isn't available
const patchesRadio = $q('input[value="patches"]', stepMode);
const patchesCard = patchesRadio.closest('.mode-card');
const autoModeNoPatchesAvailable = !manualMode && !patchesLoaded;
const autoModeNoPatchesAvailable = !manualMode && (!patchesLoaded || !firmwareURL);
const patchesHint = $('mode-patches-hint');
if (autoModeNoPatchesAvailable) {

View File

@@ -154,6 +154,7 @@ class KoboDevice {
: serial.substring(0, 3);
const model = KOBO_MODELS[serialPrefix] || 'Unknown Kobo (' + serial.substring(0, 4) + ')';
const isSupported = SUPPORTED_FIRMWARE.includes(firmware);
const isIncompatible = firmware.startsWith('5.');
return {
serial,
@@ -162,6 +163,7 @@ class KoboDevice {
hardwareId,
model,
isSupported,
isIncompatible,
};
}

View File

@@ -10,7 +10,7 @@ async function loadWasm() {
const go = new Go();
const result = await WebAssembly.instantiateStreaming(
fetch('../wasm/kobopatch.wasm?ts=1773771588'),
fetch('../wasm/kobopatch.wasm?ts=1773916731'),
go.importObject
);
go.run(result.instance);