From 60deefd9921b640c1895843edaa535da411ec443 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Thu, 6 Aug 2026 18:40:12 -0500 Subject: [PATCH] BrightSign: ask the volumes for their size instead of trusting the mount check GetStorageStatus() is documented for SD:/SSD:/USB: only, so it can never confirm internal flash, and roStorageHotplug may be absent entirely. Gating the probe on it made 'cannot say' read as 'no disk': a player with an NVMe reported 1025 MB, which is the widget's cache quota arriving through the page-side fallback. roStorageInfo is asked directly as a second pass, with the mount check kept first so a removable volume still wins over internal flash. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL --- brightsign/autorun.brs | 51 +++++++++++++++----- server/test/brightscript-api-surface.test.js | 26 ++++++++++ 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/brightsign/autorun.brs b/brightsign/autorun.brs index 8dc32c2..acdf7e1 100644 --- a/brightsign/autorun.brs +++ b/brightsign/autorun.brs @@ -390,7 +390,13 @@ Function StorageProbe() As Object hp = CreateObject("roStorageHotplug") ' Ask the platform which volumes exist rather than guessing; the static list is the fallback for ' an OS without the enumerator. Same shape BrightSign's own boilerplate uses. - volumes = ["SSD:", "SD:", "SD2:", "USB:"] + ' + ' FLASH: is on this list and is NOT on GetStorageStatus()'s. The documented drive strings for + ' that method are "SD:", "SSD:" and "USB:" — internal flash is simply not one of the things it + ' can answer about, so a player booting from flash (which is how the XT245 in this office ran + ' until an NVMe went in) can never be reported as mounted no matter what is actually there. + ' Hence the roStorageInfo pass below: the mount check is treated as a hint, not a gate. + volumes = ["SSD:", "SD:", "SD2:", "USB:", "FLASH:"] ' Feature-gated (see HasFindMember). A player that cannot be probed simply keeps the static list, ' which is the answer the enumerator would have given anyway on every model we ship. if hp <> invalid and HasFindMember() then @@ -415,22 +421,45 @@ Function StorageProbe() As Object end if if mounted then - result.present = true - result.volume = v - si = CreateObject("roStorageInfo", v) - if si <> invalid then - ' Real device capacity. The widget's storage quota — all the page can see via - ' navigator.storage.estimate() — is the cache budget, not the disk. - result.free_mb = si.GetFreeInMegabytes() - result.total_mb = si.GetSizeInMegabytes() - end if - return result + if FillStorage(result, v) then return result end if end for + ' Nothing claimed to be mounted — which is not the same as nothing being there. + ' + ' GetStorageStatus() cannot answer for FLASH:, roStorageHotplug may not exist at all on an older + ' build, and either way the whole probe hung on one call whose "no" was indistinguishable from + ' "cannot say". roStorageInfo is the direct question: a volume that reports a non-zero size IS + ' the disk, whatever the hotplug object thinks. The dashboard was showing 1025 MB for a player + ' with an NVMe in it — the widget's own cache quota, reported through the fallback in + ' st-bridge.js — because this function returned present:false and the page had nothing better. + ' + ' The mount check still runs FIRST: it is the more meaningful answer where it works, and it + ' picks the removable volume ahead of internal flash on a player that has both. + for each raw in volumes + if FillStorage(result, TrimDrive(raw)) then return result + end for + return result End Function +' Real device capacity for [drive], into [result]. True when the volume answered. +' +' The widget's storage quota — all the page can see via navigator.storage.estimate() — is the cache +' budget, not the disk, which is the entire reason the host is asked at all. +Function FillStorage(result As Object, drive As String) As Boolean + si = CreateObject("roStorageInfo", drive) + if si = invalid then return false + total = si.GetSizeInMegabytes() + if total = invalid or total <= 0 then return false + result.present = true + result.volume = drive + result.total_mb = total + free = si.GetFreeInMegabytes() + if free <> invalid and free >= 0 then result.free_mb = free + return true +End Function + ' Everything the page cannot ask the hardware directly. Sub SendProbeResult(widget As Object) di = CreateObject("roDeviceInfo") diff --git a/server/test/brightscript-api-surface.test.js b/server/test/brightscript-api-surface.test.js index 56cd48a..4c0f8f3 100644 --- a/server/test/brightscript-api-surface.test.js +++ b/server/test/brightscript-api-surface.test.js @@ -381,3 +381,29 @@ test('a second widget is never given the first screen\'s rectangle', () => { assert.notEqual(args[1], 'rect', 'the second widget must be positioned at the second output\'s canvas offset, not at rect'); }); + +test('storage is probed directly, not only through the mount check', () => { + // The mount check is roStorageHotplug.GetStorageStatus(), whose documented drive strings are + // "SD:", "SSD:" and "USB:" — internal FLASH: is not one of them, and the object may not exist on + // an older build at all. Gating the whole probe on it made "cannot say" indistinguishable from + // "no disk", and a player with an NVMe in it reported 1025 MB: the widget's own cache quota, + // reported through the navigator.storage.estimate() fallback in st-bridge.js, because the host + // had answered present:false. + // + // roStorageInfo is the direct question and needs no hotplug object. A volume that reports a + // non-zero size IS the disk. + const src = code('autorun.brs'); + const probe = /Function StorageProbe\(\)[\s\S]*?\nEnd Function/.exec(src); + assert.ok(probe, 'StorageProbe went missing'); + const body = probe[0]; + // A second pass that does not consult `mounted` at all. + const passes = body.split(/for each raw in volumes/).length - 1; + assert.ok(passes >= 2, 'the probe must fall back to asking the volumes directly'); + assert.ok(/FLASH:/i.test(body), + 'internal flash must be on the list the direct pass walks — it is how a card-less player boots'); + // And the direct pass must reject a volume that answers zero, or every player "has" every drive. + const fill = /Function FillStorage\([\s\S]*?\nEnd Function/.exec(src); + assert.ok(fill, 'FillStorage went missing'); + assert.match(fill[0], /total\s*=\s*invalid\s+or\s+total\s*<=\s*0/, + 'a volume that reports no size is not a disk'); +});