Skip to content

feat(firmware): on-demand firmware-update check#294

Open
pos-ei-don wants to merge 3 commits into
256foundation:masterfrom
pos-ei-don:feat-firmware-update-check-upstream
Open

feat(firmware): on-demand firmware-update check#294
pos-ei-don wants to merge 3 commits into
256foundation:masterfrom
pos-ei-don:feat-firmware-update-check-upstream

Conversation

@pos-ei-don

@pos-ei-don pos-ei-don commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Adds an on-demand "is newer firmware available?" check, separate from the telemetry poll (it hits the vendor's release server) — a UI can call it on a slow cadence (e.g. an HA update entity, ~daily) instead of every poll.

  • New FirmwareUpdate result (current/latest version, update_available, release date/url) from UpgradeFirmware::check_firmware_update (default: unsupported).
  • BraiinsOS 26.04 reads bos.checkForUpgrade + bos.info.version over the authenticated GraphQL client — a fully local check.
  • Python: Miner.check_firmware_update() + supports_check_firmware_update.

VNish — how its GUI does it, and an open question. VNish works differently: it shows the installed version locally, but "are you up to date?" compares that against the vendor's cloud changelogGET partner.anthill.farm/api/client/releases-notes returns a list of releases (series / version / stage), and the UI marks the latest stable vs. what's installed. There is no local API field for update-availability, so covering VNish means the lib making an external vendor-cloud call, unlike BraiinsOS's local check.

On our side (a Home Assistant integration alpha) we'd surface the VNish installed version there and could do the cloud comparison consumer-side. But we could also imagine putting it in the lib so check_firmware_update is uniform across firmwares. What's your preference — should a vendor-cloud lookup like VNish's live in asic-rs, or stay on the consumer side? I kept this PR to the local BraiinsOS check; happy to add VNish whichever way you'd like.

Adds an on-demand "is newer firmware available?" check, separate from the
telemetry poll (it hits the vendor's release server).

- New FirmwareUpdate result type (current/latest version, update_available,
  release date/url) returned by UpgradeFirmware::check_firmware_update, default
  unsupported.
- BraiinsOS 26.04 reads bos.checkForUpgrade + bos.info.version via the
  authenticated GraphQL client.
- Python: Miner.check_firmware_update() + supports_check_firmware_update.
Comment thread asic-rs-core/src/data/firmware.rs
Comment on lines +835 to 880

fn supports_check_firmware_update(&self) -> bool {
true
}

/// Reads the installed version and asks BOS to check the vendor's release
/// server (`bos.checkForUpgrade`) via the authenticated GraphQL client.
async fn check_firmware_update(&self) -> anyhow::Result<FirmwareUpdate> {
const GQL_CHECK_UPGRADE: MinerCommand = MinerCommand::GraphQL {
command: r#"{
bos {
info { version { full } }
checkForUpgrade {
__typename
... on UpgradeDetail {
latestRelease {
version
releaseDate
url
}
}
}
}
}"#,
};
let data = self.graphql.get_api_result(&GQL_CHECK_UPGRADE).await?;

let s = |p: &str| -> Option<String> {
data.pointer(p).and_then(|v| v.as_str()).map(String::from)
};
let current_version = s("/bos/info/version/full");
let latest_version = s("/bos/checkForUpgrade/latestRelease/version");
let release_date = s("/bos/checkForUpgrade/latestRelease/releaseDate");
let release_url = s("/bos/checkForUpgrade/latestRelease/url");
let update_available =
latest_version.is_some() && latest_version.as_deref() != current_version.as_deref();

Ok(FirmwareUpdate {
current_version,
latest_version,
update_available,
release_date,
release_url,
})
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be able to backport to all the other braiins versions.

@pos-ei-don

Copy link
Copy Markdown
Contributor Author

Thanks for the review — but before reworking I'd push back on this direction, because a couple of parts don't hold up across the firmwares:

semver won't work as the comparison basis. Miner firmware versions aren't semver: BraiinsOS is CalVer (26.04, 25.07), and Antminer/stock builds carry arbitrary version strings. semver::Version::parse("26.04") fails outright, so an update_available() built on semver would silently stop comparing for most backends — including the BOS ones this PR targets. I'd rather not bake in an assumption that breaks the majority case.

Folding the check result into a Local(FirmwareImage) / Remote(URL) enum overloads it. The check result and the apply source are different concerns. For VNish the check is vendor-cloud metadata (a version + a release-notes URL) — there's no downloadable FirmwareImage to return, so Local/Remote doesn't map onto a check at all. And dropping the release URL throws away the one actionable thing a check usually produces: where to get the firmware.

What I'd suggest instead: keep this as a small read-only check result (happy to rename it FirmwareStats), versions as plain strings, keep the optional URL, and leave a Local/Remote upgrade-source as a separate type for the actual upgrade path rather than fusing it into the check. The backport to the other Braiins versions I'm glad to do once the shape is settled.

If you do want the enum/semver route, could you sketch how update_available() is meant to behave for CalVer (26.04) and arbitrary version strings? That's the part I don't see working.

@b-rowan

b-rowan commented Jun 24, 2026

Copy link
Copy Markdown
Member

semver won't work as the comparison basis. Miner firmware versions aren't semver: BraiinsOS is CalVer (26.04, 25.07), and Antminer/stock builds carry arbitrary version strings. semver::Version::parse("26.04") fails outright, so an update_available() built on semver would silently stop comparing for most backends — including the BOS ones this PR targets. I'd rather not bake in an assumption that breaks the majority case.

It already does work as our comparison basis 😆..

impl MinerConstructor for Braiins {
fn new(ip: IpAddr, model: impl MinerModel, version: Option<semver::Version>) -> Box<dyn Miner> {
match version {
Some(ref v) if *v >= Version::new(26, 4, 0) => Box::new(BraiinsV2604::new(ip, model)),
Some(ref v) if *v >= Version::new(25, 7, 0) => Box::new(BraiinsV2507::new(ip, model)),
Some(ref v) if *v >= Version::new(25, 5, 0) => Box::new(BraiinsV2505::new(ip, model)),
Some(ref v) if *v >= Version::new(25, 3, 0) => Box::new(BraiinsV2503::new(ip, model)),
Some(ref v) if *v >= Version::new(24, 9, 0) => Box::new(BraiinsV2109::new(ip, model)),
_ => Box::new(BraiinsV2109::new(ip, model)),
}
}
}

async fn get_version(ip: IpAddr) -> Option<semver::Version> {
let response =
util::send_graphql_command(&ip, "{ bos { info { version { full } } } }").await?;
let full = response["data"]["bos"]["info"]["version"]["full"].as_str()?;
let version_str = full.split('-').rev().find(|s| s.contains('.'))?;
let normalized = version_str
.split('.')
.map(|part| part.trim_start_matches('0').to_string())
.map(|part| {
if part.is_empty() {
"0".to_string()
} else {
part
}
})
.collect::<Vec<_>>()
.join(".");
// pad if needed, semver requires major.minor.patch
let padded = match version_str.split('.').count() {
2 => format!("{}.0", normalized),
_ => normalized.to_string(),
};
semver::Version::parse(&padded).ok()
}
}

We just append a PATCH version if one doesn't exist, it doesn't need to be exactly semver, it just needs to be comparable, and calver is close enough. Some versions don't even need the extra value appended (see https://downloads.braiins.com/braiins-os/#6de99434-9996-4c9a-819b-e4121db4b3ca)

And dropping the release URL throws away the one actionable thing a check usually produces: where to get the firmware.

Not exactly what I meant.

struct FirmwareStats {
    current_version: Option<SemVer>,
    latest_version: Option<SemVer>,
    firmware: Option<FirmwareUpdate>
}

impl FirmwareStats {
    fn update_available {
        // compare current to latest
        ...
    }
}

enum FirmwareUpdate {
    Local(FirmwareImage),
    Remote(URL)
}

pos-ei-don and others added 2 commits June 24, 2026 18:45
…mwareUpdate enum

Per review: separate the version comparison from the update source, and compare
real versions instead of string inequality.

- FirmwareUpdate struct -> FirmwareStats { current_version, latest_version:
  Option<semver::Version>, firmware: Option<FirmwareUpdate> }
- update_available() derived by comparing versions (was a stored bool computed
  from string inequality, which flagged updates even for older "latest")
- new enum FirmwareUpdate { Local(FirmwareImage), Remote(String) } for the
  update source; drop non-actionable release_date
- braiins v26_04: parse versions via shared parse_bos_version helper (extracted
  from get_version), map release url -> Remote
- python: FirmwareStats pyclass with str version getters, PyFirmwareUpdate
  mirror enum; .pyi updated
@pos-ei-don

Copy link
Copy Markdown
Contributor Author

Done in fd3990e — reworked to your shape:

  • FirmwareUpdateFirmwareStats { current_version, latest_version: Option<Version>, firmware: Option<FirmwareUpdate> }
  • update_available() is now derived from comparing the versions
  • enum FirmwareUpdate { Local(FirmwareImage), Remote(String) } for the source; dropped release_date

You were right on the version point — the .0-pad approach handles CalVer fine, so I pulled the BOS parse out of get_version into a shared parse_bos_version helper and use it for both the installed and latest versions. As a bonus the derived comparison fixes a latent bug: the old latest != current string check flagged an update even when latest was older; latest > current is correct.

One note: nothing populates Local(FirmwareImage) today — the BOS check returns a URL, so it maps to Remote. I kept Local for the download-then-flash path (and to line up with upgrade_firmware(FirmwareImage)), but happy to drop it if you'd rather keep the enum to just what's produced now.

CI green (Cargo + Python).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants