From d4b17c8141605b9f5e0f6b1e6f22db4e3bf9a5e7 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Sun, 4 Oct 2026 14:32:11 +0300 Subject: [PATCH 1/2] Install NAND cameras on u-boot-xmedia SoCs as one UBI device, written with write.trimffs OpenIPC is retiring the HiSilicon split NAND layout (boot, env, a raw kernel at 0x100000, UBI at 0x400000; `run uknand; run urnand; run setnand`) on the SoCs whose bootloader is u-boot-xmedia. Their NAND is now mtdparts :768k(boot),256k(env),-(ubi): U-Boot at 0, env at 0xc0000, and one UBI device from 0x100000 to the end of the chip holding the kernel (FIT), rootfs and rootfs_data volumes. rootfs.ubi. is the only file an install writes, and it must go on with `nand write.trimffs`: a plain write programs the 0xFF padding pages, UBIFS programs them again later, and the ECC breaks (OpenIPC/firmware#2519). The same holds for a full image that contains it. Data-driven: a catalogue entry that names `uboot_nand_filename` is on the UBI-only layout and installs that bootloader on NAND; `uboot_filename` becomes its -nor.bin build. HI3516EV200, HI3516EV300, HI3518EV300 and HI3516DV200 move to u-boot--{nor,nand}.bin. GK7205V500, GK7205V510 and GK7205V530 join the Goke catalogue the same way (board gk7205v500, load address 0x42000000 per u-boot-xmedia's baseaddr). Every other NAND SoC keeps today's flow; its wizard document is byte for byte unchanged. For a UBI-only NAND camera the wizard now prints: U-Boot: tftpboot u-boot--nand.bin && nand erase 0x0 0xc0000 && nand write 0x0 0xc0000 (after mw.b 0xff 0xc0000) Linux: tftpboot rootfs.ubi. && nand erase 0x100000 0x7f00000 && nand write.trimffs 0x100000 ${filesize} (fatload from SD likewise; env/ethaddr/saveenv lines kept) Full: ... && nand erase 0x0 0x8000000 && nand write.trimffs 0x0 ${filesize} with no `run setnand` in any block and no bootloader macros advertised (bootloader_variables is empty; the page drops its printenv sentence when there is nothing to name). NOR installs are unchanged apart from the bootloader file name. The full NAND image for these SoCs is the NAND bootloader at 0 (refused past 0xc0000) and rootfs.ubi at 0x100000 (bounded by the 128 MiB chip), ending at the UBI image rounded up to a 2 KiB page, 0xFF between; no uImage. layoutVersion goes to 2 so every cached image is rebuilt. Availability and the download page's missing-bootloader message ask after the bootloader the requested flash type installs. The wizard document gains uboot_nand_filename, bl_nand_url and bootloader_nand_published only on the UBI-only SoCs, so the JSON stays backward compatible; the SoC page offers both bootloaders, labelled NOR and NAND, and the by-parts U-Boot step links the one its commands write. Goldens: documents.json changes for exactly hi3516dv200, hi3516ev200, hi3516ev300 and hi3518ev300, and gains gk7205v500/510/530. The release index fixture gains u-boot-{hi3516dv200,hi3516ev200,hi3518ev300}-{nor,nand}.bin and u-boot-hi3516ev300-nor.bin as synthetic rows (no digest), standing for files the firmware repository must publish before this deploys; until it does, those SoCs read as firmware_only. The sitemap golden and deploy/firmware-segments.tsv gain the three Goke SoCs. --- data/catalogue/goke.yml | 47 +++++++++- data/catalogue/hisilicon.yml | 12 ++- deploy/firmware-segments.tsv | 3 + .../apps/site/src/components/wizard/Form.tsx | 24 ++++- .../site/src/components/wizard/Result.tsx | 28 ++++-- frontend/apps/site/src/data/catalogue.json | 30 +++++++ frontend/apps/site/src/lib/sitemap.golden.xml | 63 +++++++++++++ .../apps/site/src/lib/wizard-export.test.ts | 43 +++++++++ frontend/apps/site/src/lib/wizard-export.ts | 29 ++++++ service/internal/catalogue/catalogue.go | 38 ++++++-- service/internal/firmware/availability.go | 23 +++-- service/internal/firmware/build.go | 29 +++--- service/internal/firmware/firmware_test.go | 89 +++++++++++++++++++ service/internal/firmware/handler.go | 10 ++- service/internal/firmware/layout.go | 65 ++++++++++---- .../firmware/testdata/availability.json | 2 +- .../internal/firmware/testdata/boards.json | 42 ++++++++- .../firmware/testdata/release-index.json | 42 +++++++++ service/internal/wizard/camera.go | 52 +++++++++++ service/internal/wizard/export.go | 30 ++++--- service/internal/wizard/lines.go | 47 +++++++--- .../internal/wizard/testdata/documents.json | 13 +-- service/internal/wizard/wizard_test.go | 80 +++++++++++++++++ 23 files changed, 748 insertions(+), 93 deletions(-) create mode 100644 frontend/apps/site/src/lib/wizard-export.test.ts diff --git a/data/catalogue/goke.yml b/data/catalogue/goke.yml index 0515b948..8c7076a8 100644 --- a/data/catalogue/goke.yml +++ b/data/catalogue/goke.yml @@ -1,4 +1,4 @@ -# Goke — 13 SoC(s). +# Goke — 16 SoC(s). # # Exported from the catalogue table (#161). This file is the source of # truth for the hardware pages: edit it in a pull request, not in the @@ -177,6 +177,51 @@ socs: build_status_url: https://github.com/OpenIPC/firmware/actions/workflows/gk7205v300_images.yml/badge.svg featured: true segment: cctv +- family: GK7205V500 + model: GK7205V500 + version: '' + status: done + load_address: '0x42000000' + sdk: '' + kernel: '' + uboot_filename: u-boot-gk7205v500-nor.bin + uboot_nand_filename: u-boot-gk7205v500-nand.bin + linux_filename: openipc.gk7205v500-nor-lite.tgz + notes: '' + urlname: gk7205v500 + build_status_url: https://github.com/OpenIPC/firmware/actions/workflows/gk7205v500_images.yml/badge.svg + featured: false + segment: cctv +- family: GK7205V500 + model: GK7205V510 + version: '' + status: done + load_address: '0x42000000' + sdk: '' + kernel: '' + uboot_filename: u-boot-gk7205v510-nor.bin + uboot_nand_filename: u-boot-gk7205v510-nand.bin + linux_filename: openipc.gk7205v500-nor-lite.tgz + notes: '' + urlname: gk7205v510 + build_status_url: https://github.com/OpenIPC/firmware/actions/workflows/gk7205v500_images.yml/badge.svg + featured: false + segment: cctv +- family: GK7205V500 + model: GK7205V530 + version: '' + status: done + load_address: '0x42000000' + sdk: '' + kernel: '' + uboot_filename: u-boot-gk7205v530-nor.bin + uboot_nand_filename: u-boot-gk7205v530-nand.bin + linux_filename: openipc.gk7205v500-nor-lite.tgz + notes: '' + urlname: gk7205v530 + build_status_url: https://github.com/OpenIPC/firmware/actions/workflows/gk7205v500_images.yml/badge.svg + featured: false + segment: cctv - family: GK7205V200 model: GK7605V100 version: '' diff --git a/data/catalogue/hisilicon.yml b/data/catalogue/hisilicon.yml index e1e3deaa..f1a655bf 100644 --- a/data/catalogue/hisilicon.yml +++ b/data/catalogue/hisilicon.yml @@ -133,7 +133,8 @@ socs: load_address: '0x42000000' sdk: '' kernel: '' - uboot_filename: u-boot-hi3516dv200-universal.bin + uboot_filename: u-boot-hi3516dv200-nor.bin + uboot_nand_filename: u-boot-hi3516dv200-nand.bin linux_filename: openipc.hi3516dv200-nor-lite.tgz notes: '' urlname: hi3516dv200 @@ -175,7 +176,8 @@ socs: load_address: '0x42000000' sdk: '' kernel: '' - uboot_filename: u-boot-hi3516ev200-universal.bin + uboot_filename: u-boot-hi3516ev200-nor.bin + uboot_nand_filename: u-boot-hi3516ev200-nand.bin linux_filename: openipc.hi3516ev200-nor-lite.tgz notes: f23 f37 gc2053 imx307 imx335 sc2231 sc2232h sc2239 sc2315e sc3235 sc4236 sp2305 urlname: hi3516ev200 @@ -189,7 +191,8 @@ socs: load_address: '0x42000000' sdk: '' kernel: '' - uboot_filename: u-boot-hi3516ev300-universal.bin + uboot_filename: u-boot-hi3516ev300-nor.bin + uboot_nand_filename: u-boot-hi3516ev300-nand.bin linux_filename: openipc.hi3516ev300-nor-lite.tgz notes: '' urlname: hi3516ev300 @@ -259,7 +262,8 @@ socs: load_address: '0x42000000' sdk: '' kernel: '' - uboot_filename: u-boot-hi3518ev300-universal.bin + uboot_filename: u-boot-hi3518ev300-nor.bin + uboot_nand_filename: u-boot-hi3518ev300-nand.bin linux_filename: openipc.hi3518ev300-nor-lite.tgz notes: '' urlname: hi3518ev300 diff --git a/deploy/firmware-segments.tsv b/deploy/firmware-segments.tsv index 42543113..fe623762 100644 --- a/deploy/firmware-segments.tsv +++ b/deploy/firmware-segments.tsv @@ -33,6 +33,9 @@ gk7202v300 goke GK7205V200 cctv gk7205v200 goke GK7205V200 cctv gk7205v210 goke GK7205V200 cctv gk7205v300 goke GK7205V200 cctv +gk7205v500 goke GK7205V500 cctv +gk7205v510 goke GK7205V500 cctv +gk7205v530 goke GK7205V500 cctv gk7605v100 goke GK7205V200 cctv gm8135 grainmedia GM8136 gm8136 grainmedia GM8136 diff --git a/frontend/apps/site/src/components/wizard/Form.tsx b/frontend/apps/site/src/components/wizard/Form.tsx index 223ace11..3043ce2c 100644 --- a/frontend/apps/site/src/components/wizard/Form.tsx +++ b/frontend/apps/site/src/components/wizard/Form.tsx @@ -72,8 +72,10 @@ export default function Form({ // filename: a SoC can name a bootloader upstream does not publish, and the // blank check let it through to a form offering to build an image around a // bootloader that does not exist. + // A UBI-only NAND SoC publishes its NAND bootloader apart from the NOR one, + // and either is enough to install with. const usable = doc === null - || (doc.bootloader_published && doc.linux_filename !== '' + || ((doc.bootloader_published || doc.bootloader_nand_published === true) && doc.linux_filename !== '' && doc.load_address !== '' && doc.availability !== 'none'); // Firmware but no bootloader, and a load address to work with. See @@ -359,10 +361,28 @@ function Unavailable({ t, facts, doc }: { t: Props['t']; facts: SocFacts; doc: W download being broken. Both of these are asked of the release index, not built from a filename. */} + {/* + Two links where the SoC has a NAND bootloader of its own, each named + for its flash the way the bundles below are; one, unnamed, where a + single bootloader serves both. + */} {doc.bootloader_published && ( + )} + {doc.bootloader_nand_published === true && doc.uboot_nand_filename && ( +
+ +
+ {`${show('bootloader')} NAND`} +

{show('for', { name: facts.fullName })}

)} diff --git a/frontend/apps/site/src/components/wizard/Result.tsx b/frontend/apps/site/src/components/wizard/Result.tsx index 116b4346..052dd8ad 100644 --- a/frontend/apps/site/src/components/wizard/Result.tsx +++ b/frontend/apps/site/src/components/wizard/Result.tsx @@ -9,7 +9,7 @@ */ import { useRef, useState } from 'preact/hooks'; import type { ComponentChildren } from 'preact'; -import type { Block, Combination, WizardDocument } from '../../lib/wizard-export'; +import { bootloaderFor, type Block, type Combination, type WizardDocument } from '../../lib/wizard-export'; import { fillHoles, type WizardSettings } from '../../lib/wizard-input'; import { toPermalink } from '../../lib/wizard-input'; import { @@ -380,12 +380,19 @@ export default function Result({ /> - `${name}`).join(', '), - })} - /> + {/* + Nothing to name on the UBI-only NAND layout: that install is + rootfs.ubi written by hand, not a bootloader macro, and the + sentence with an empty list reads as a fault. + */} + {(combination.bootloader_variables ?? []).length > 0 && ( + `${name}`).join(', '), + })} + /> + )} ) => t(`firmware.installation.${key}`, options); const nand = settings.flashType === 'nand'; + // The file the U-Boot commands below write: on a UBI-only NAND SoC that is + // the NAND build, not the NOR one uboot_filename names. + const bootloader = bootloaderFor(doc, settings.flashType); return ( <> @@ -563,11 +573,11 @@ function Experts({ t, doc, combination, settings, facts, sdcardRequired, edition

{install('flashing_uboot.title')}

- {doc.uboot_filename !== '' && ( + {bootloader.filename !== '' && (
- {install('flashing_uboot.link')} + {install('flashing_uboot.link')}

for {facts.fullName}

{install('flashing_uboot.info')}

diff --git a/frontend/apps/site/src/data/catalogue.json b/frontend/apps/site/src/data/catalogue.json index 3bc50f86..685cdd41 100644 --- a/frontend/apps/site/src/data/catalogue.json +++ b/frontend/apps/site/src/data/catalogue.json @@ -358,6 +358,36 @@ "featured": true, "segment": "cctv" }, + { + "model": "GK7205V500", + "family": "GK7205V500", + "version": "", + "urlname": "gk7205v500", + "status": "done", + "load_address": "0x42000000", + "featured": false, + "segment": "cctv" + }, + { + "model": "GK7205V510", + "family": "GK7205V500", + "version": "", + "urlname": "gk7205v510", + "status": "done", + "load_address": "0x42000000", + "featured": false, + "segment": "cctv" + }, + { + "model": "GK7205V530", + "family": "GK7205V500", + "version": "", + "urlname": "gk7205v530", + "status": "done", + "load_address": "0x42000000", + "featured": false, + "segment": "cctv" + }, { "model": "GK7605V100", "family": "GK7205V200", diff --git a/frontend/apps/site/src/lib/sitemap.golden.xml b/frontend/apps/site/src/lib/sitemap.golden.xml index e2a89960..e85b2acf 100644 --- a/frontend/apps/site/src/lib/sitemap.golden.xml +++ b/frontend/apps/site/src/lib/sitemap.golden.xml @@ -1576,6 +1576,69 @@ + + https://openipc.org/cameras/vendors/goke/socs/gk7205v500 + + + + + + + https://openipc.org/ru/cameras/vendors/goke/socs/gk7205v500 + + + + + + + https://openipc.org/zh/cameras/vendors/goke/socs/gk7205v500 + + + + + + + https://openipc.org/cameras/vendors/goke/socs/gk7205v510 + + + + + + + https://openipc.org/ru/cameras/vendors/goke/socs/gk7205v510 + + + + + + + https://openipc.org/zh/cameras/vendors/goke/socs/gk7205v510 + + + + + + + https://openipc.org/cameras/vendors/goke/socs/gk7205v530 + + + + + + + https://openipc.org/ru/cameras/vendors/goke/socs/gk7205v530 + + + + + + + https://openipc.org/zh/cameras/vendors/goke/socs/gk7205v530 + + + + + https://openipc.org/cameras/vendors/goke/socs/gk7605v100 diff --git a/frontend/apps/site/src/lib/wizard-export.test.ts b/frontend/apps/site/src/lib/wizard-export.test.ts new file mode 100644 index 00000000..fa95ec8c --- /dev/null +++ b/frontend/apps/site/src/lib/wizard-export.test.ts @@ -0,0 +1,43 @@ +/** + * Which bootloader the download link beside the U-Boot commands names. On the + * UBI-only NAND SoCs (u-boot-xmedia: hi3516ev300 and its family, the + * GK7205V500 family) upstream builds the NAND bootloader apart from the NOR + * one, and the commands for a NAND chip write the NAND file -- a link to the + * NOR file beside them would install a bootloader with the wrong mtdparts. + */ +import { describe, expect, test } from 'vitest'; +import { bootloaderFor } from './wizard-export'; + +const split = { + uboot_filename: 'u-boot-hi3516av100-universal.bin', + bl_url: 'https://example.invalid/u-boot-hi3516av100-universal.bin', + bootloader_published: true, +}; +const ubi = { + uboot_filename: 'u-boot-hi3516ev300-nor.bin', + bl_url: 'https://example.invalid/u-boot-hi3516ev300-nor.bin', + bootloader_published: true, + uboot_nand_filename: 'u-boot-hi3516ev300-nand.bin', + bl_nand_url: 'https://example.invalid/u-boot-hi3516ev300-nand.bin', + bootloader_nand_published: false, +}; + +describe('the bootloader a flash chip installs', () => { + test('one bootloader serves both chips where there is no NAND build', () => { + for (const chip of ['nor8m', 'nor16m', 'nand']) { + expect(bootloaderFor(split, chip)).toEqual({ + filename: split.uboot_filename, url: split.bl_url, published: true, + }); + } + }); + + test('NAND gets the NAND build, with its own published state', () => { + expect(bootloaderFor(ubi, 'nand')).toEqual({ + filename: 'u-boot-hi3516ev300-nand.bin', url: ubi.bl_nand_url, published: false, + }); + }); + + test('NOR keeps the NOR build on a SoC that has both', () => { + expect(bootloaderFor(ubi, 'nor16m').filename).toBe('u-boot-hi3516ev300-nor.bin'); + }); +}); diff --git a/frontend/apps/site/src/lib/wizard-export.ts b/frontend/apps/site/src/lib/wizard-export.ts index db6a9f47..d7b49d22 100644 --- a/frontend/apps/site/src/lib/wizard-export.ts +++ b/frontend/apps/site/src/lib/wizard-export.ts @@ -63,6 +63,15 @@ export interface WizardDocument { kernel_file: string; rootfs_file: string; bl_url: string; + /** + * The NAND bootloader, present only on the SoCs whose NAND is one UBI + * device (u-boot-xmedia's 768k(boot),256k(env),-(ubi)). Upstream builds it + * apart from the NOR one, which `uboot_filename` and `bl_url` then name. + * Absent everywhere else: one bootloader serves both flash types there. + */ + uboot_nand_filename?: string; + bl_nand_url?: string; + bootloader_nand_published?: boolean; published: PublishedBundle[]; patterns: Patterns; editions: Availability; @@ -99,3 +108,23 @@ export function combinationFor( && entry.network_interface === network && entry.sd_card_slot === sd) ?? null; } + +/** + * The bootloader a flash chip installs: the NAND build on NAND where the SoC + * has one, and the single (or NOR) build otherwise. The commands already name + * the right file; this is the download link beside them, which must match. + */ +export function bootloaderFor( + document: Pick, + flashType: string, +): { filename: string; url: string; published: boolean } { + if (flashType === 'nand' && document.uboot_nand_filename) { + return { + filename: document.uboot_nand_filename, + url: document.bl_nand_url ?? '', + published: document.bootloader_nand_published ?? false, + }; + } + return { filename: document.uboot_filename, url: document.bl_url, published: document.bootloader_published }; +} diff --git a/service/internal/catalogue/catalogue.go b/service/internal/catalogue/catalogue.go index 9a1d182f..fb94b91f 100644 --- a/service/internal/catalogue/catalogue.go +++ b/service/internal/catalogue/catalogue.go @@ -15,14 +15,22 @@ import ( ) type SoC struct { - URLName string `yaml:"urlname"` - Model string `yaml:"model"` - Family string `yaml:"family"` - Status string `yaml:"status"` - UBootFilename string `yaml:"uboot_filename"` - LinuxFilename string `yaml:"linux_filename"` - LoadAddress string `yaml:"load_address"` - Vendor *Vendor `yaml:"-"` + URLName string `yaml:"urlname"` + Model string `yaml:"model"` + Family string `yaml:"family"` + Status string `yaml:"status"` + UBootFilename string `yaml:"uboot_filename"` + // UBootNANDFilename is the bootloader for NAND when upstream builds one + // apart from the NOR one, and its presence is what moves the SoC's NAND + // installs onto the UBI-only layout: u-boot-xmedia's + // 768k(boot),256k(env),-(ubi), with the kernel a volume in the one UBI + // device rather than a raw partition at 0x100000. Empty everywhere else, + // which keeps the split layout (boot, env, kernel, ubi) and its `run + // uknand; run urnand` for every SoC that still boots that way. + UBootNANDFilename string `yaml:"uboot_nand_filename"` + LinuxFilename string `yaml:"linux_filename"` + LoadAddress string `yaml:"load_address"` + Vendor *Vendor `yaml:"-"` } type Vendor struct { @@ -97,5 +105,19 @@ func (c *Catalogue) All() []*SoC { return out } +// UBINand says whether this SoC's NAND is one UBI device from 0x100000 to the +// end of the chip, written from rootfs.ubi. alone; see +// UBootNANDFilename. +func (s *SoC) UBINand() bool { return s.UBootNANDFilename != "" } + +// Bootloader is the U-Boot file for a flash type ("nor" or "nand"): the NAND +// build where there is one, and the single universal or NOR build otherwise. +func (s *SoC) Bootloader(flashType string) string { + if flashType == "nand" && s.UBINand() { + return s.UBootNANDFilename + } + return s.UBootFilename +} + // ModelDowncase is what file names use. func (s *SoC) ModelDowncase() string { return strings.ToLower(s.Model) } diff --git a/service/internal/firmware/availability.go b/service/internal/firmware/availability.go index de55160b..d91b029c 100644 --- a/service/internal/firmware/availability.go +++ b/service/internal/firmware/availability.go @@ -30,13 +30,26 @@ func Availability(soc *catalogue.SoC, idx *Index) string { if len(idx.Releases(board, "nor")) == 0 && len(idx.Releases(board, "nand")) == 0 { return "none" } - if soc.UBootFilename == "" { - return "firmware_only" + // A flash type counts when it has firmware and the bootloader that flash + // type installs. That is one file for most SoCs, and two -- the NOR and + // the NAND build -- for the UBI-only ones, which upstream publishes apart. + for _, ft := range []string{"nor", "nand"} { + if len(idx.Releases(board, ft)) > 0 && BootloaderPublished(soc, idx, ft) { + return "wizard" + } } - if _, ok := idx.Asset(soc.UBootFilename); !ok { - return "firmware_only" + return "firmware_only" +} + +// BootloaderPublished says whether the bootloader a flash type installs is in +// the index. +func BootloaderPublished(soc *catalogue.SoC, idx *Index, flashType string) bool { + name := soc.Bootloader(flashType) + if name == "" { + return false } - return "wizard" + _, ok := idx.Asset(name) + return ok } // AvailabilityMap is every SoC's state, keyed by slug. diff --git a/service/internal/firmware/build.go b/service/internal/firmware/build.go index a12ccd34..ec9c3d8a 100644 --- a/service/internal/firmware/build.go +++ b/service/internal/firmware/build.go @@ -71,7 +71,10 @@ const BuildDeadline = 150 * time.Second // layoutVersion changes whenever the way an image is assembled changes, so // that every cached image made the old way stops matching and is rebuilt. -const layoutVersion = "1" +// +// 2: NAND images for the UBI-only SoCs became the NAND bootloader and +// rootfs.ubi at 0x100000, with no uImage. +const layoutVersion = "2" // Key names the image these inputs produce. func (in Inputs) Key() string { @@ -86,7 +89,7 @@ func (in Inputs) Key() string { // Resolve turns a spec into inputs against the current index. func Resolve(s Spec, idx *Index) (Inputs, error) { in := Inputs{Spec: s} - name := s.SoC.UBootFilename + name := s.Bootloader() if !plainName(name) { return in, ErrUnknownAsset{fmt.Sprintf("%q", name)} } @@ -228,7 +231,7 @@ type extent struct{ from, to int64 } // memory. Everything no member covers is 0xFF -- erased flash, not zeros. func (im *Images) assemble(in Inputs, ubootPath, linuxPath, dest string) error { s := in.Spec - parts := s.parts() + parts := s.parts(in.Kernel, in.Rootfs) filename := s.Filename() if err := os.MkdirAll(im.Root, 0o755); err != nil { @@ -269,22 +272,26 @@ func (im *Images) assemble(in Inputs, ubootPath, linuxPath, dest string) error { } written = append(written, extent{parts[0].offset, parts[0].offset + n}) - sizes, present, err := im.streamMembers(linuxPath, map[string]part{in.Kernel: parts[1], in.Rootfs: parts[2]}, f, fits) + members := parts[1:] + want := map[string]part{} + for _, p := range members { + want[p.member] = p + } + sizes, present, err := im.streamMembers(linuxPath, want, f, fits) if err != nil { return err } - for _, name := range []string{in.Kernel, in.Rootfs} { - if _, ok := sizes[name]; !ok { + for _, p := range members { + if _, ok := sizes[p.member]; !ok { sort.Strings(present) return ErrMissingMember{fmt.Sprintf("%s is not in %s (members: %s)", - name, filepath.Base(in.Linux.Name), strings.Join(present, ", "))} + p.member, filepath.Base(in.Linux.Name), strings.Join(present, ", "))} } + written = append(written, extent{p.offset, p.offset + sizes[p.member]}) } - written = append(written, - extent{parts[1].offset, parts[1].offset + sizes[in.Kernel]}, - extent{parts[2].offset, parts[2].offset + sizes[in.Rootfs]}) - size := s.imageSize(sizes[in.Rootfs]) + last := members[len(members)-1] + size := s.imageSize(last, sizes[last.member]) if err := fillErased(f, written, size); err != nil { return err } diff --git a/service/internal/firmware/firmware_test.go b/service/internal/firmware/firmware_test.go index 18992b5b..fa803b33 100644 --- a/service/internal/firmware/firmware_test.go +++ b/service/internal/firmware/firmware_test.go @@ -83,6 +83,7 @@ func TestBoardsMatchReference(t *testing.T) { } var want []struct { URLName, Vendor, Board, UBoot string + UBootNAND string `json:"uboot_nand"` NorLite string `json:"nor_lite"` NandLite string `json:"nand_lite"` } @@ -102,6 +103,10 @@ func TestBoardsMatchReference(t *testing.T) { if soc.UBootFilename != w.UBoot { t.Errorf("%s: bootloader %q, the reference says %q", w.URLName, soc.UBootFilename, w.UBoot) } + // Blank in the reference for every SoC still on the split NAND layout. + if soc.UBootNANDFilename != w.UBootNAND { + t.Errorf("%s: NAND bootloader %q, the reference says %q", w.URLName, soc.UBootNANDFilename, w.UBootNAND) + } for flash, name := range map[string]string{"nor": w.NorLite, "nand": w.NandLite} { s := Spec{SoC: soc, FlashType: flash, Release: "lite"} if got := s.LinuxAsset(idx); got != name { @@ -392,3 +397,87 @@ func TestLimiterCountsBuildsPerAddress(t *testing.T) { t.Error("still refused after the window") } } + +// The UBI-only NAND image (hi3516ev300 and the other u-boot-xmedia SoCs): the +// NAND bootloader at 0, rootfs.ubi -- the whole UBI device, kernel volume +// included -- at 0x100000, nothing of the tarball's uImage, erased flash in +// between, and the image ending at the UBI image rounded up to a page. A +// bootloader past the 768 KiB boot partition is refused, not written into the +// env. +func TestUBINandImage(t *testing.T) { + cat := loadCatalogue(t) + soc := cat.SoC("hi3516ev300") + if !soc.UBINand() { + t.Fatal("hi3516ev300 is not on the UBI-only NAND layout") + } + board := "hi3516ev300" + dir := t.TempDir() + releases := &Releases{Root: filepath.Join(dir, "rel")} + images := &Images{Root: filepath.Join(dir, "img"), Releases: releases} + uboot := synthetic("uboot-nand", 300_000) + ubi := synthetic("ubi", 3_000_001) // not a whole page: the image rounds up + kernel := synthetic("kernel", 1_500_000) + build := func(bootloader []byte) (string, error) { + idx := &Index{assets: map[string]Asset{}, builds: map[[2]string][]string{}} + put := func(name string, data []byte) { + sum := sha256.Sum256(data) + a := Asset{Name: name, Size: int64(len(data)), Digest: "sha256:" + hex.EncodeToString(sum[:])} + idx.assets[name] = a + os.MkdirAll(filepath.Join(releases.Root, "blobs"), 0o755) + os.WriteFile(releases.Path(a), data, 0o644) + } + // The NOR bootloader is there too, and must not be the one used. + put(soc.UBootFilename, synthetic("uboot-nor", 200_000)) + put(soc.UBootNANDFilename, bootloader) + put("openipc."+board+"-nand-lite.tgz", tgz(t, [][2]any{ + {"fitImage." + board, kernel}, {"uImage." + board, kernel}, + {"rootfs.ubifs." + board, synthetic("ubifs", 2_000_000)}, {"rootfs.ubi." + board, ubi}})) + spec, err := NewSpec(soc, "nand", "lite", 0, 0) + if err != nil { + t.Fatal(err) + } + in, err := Resolve(spec, idx) + if err != nil { + t.Fatal(err) + } + if in.UBoot.Name != soc.UBootNANDFilename || in.Kernel != "" || in.Rootfs != "rootfs.ubi."+board { + t.Fatalf("resolved %s, kernel %q, rootfs %q", in.UBoot.Name, in.Kernel, in.Rootfs) + } + return images.Build(context.Background(), in) + } + + path, err := build(uboot) + if err != nil { + t.Fatal(err) + } + data, _ := os.ReadFile(path) + end := int64(0x100000 + len(ubi)) + if want := (end + 2047) / 2048 * 2048; int64(len(data)) != want { + t.Fatalf("%d bytes, want %d", len(data), want) + } + if !bytes.Equal(data[:len(uboot)], uboot) { + t.Error("the NAND bootloader is not at 0") + } + if !bytes.Equal(data[0x100000:end], ubi) { + t.Error("rootfs.ubi is not at 0x100000") + } + if bytes.Contains(data, kernel[:64]) { + t.Error("the image carries the tarball's kernel outside the UBI image") + } + for i, b := range data[len(uboot):0x100000] { + if b != 0xFF { + t.Fatalf("byte 0x%x between the bootloader and the UBI image is 0x%02x, not erased", len(uboot)+i, b) + } + } + for _, b := range data[end:] { + if b != 0xFF { + t.Fatal("the page tail after the UBI image is not erased") + } + } + + if _, err := build(synthetic("uboot-big", 0xc0000+1)); err == nil { + t.Error("a bootloader past the 768 KiB boot partition was accepted") + } else if tl := (ErrTooLarge{}); !errors.As(err, &tl) { + t.Errorf("an oversized bootloader failed with %v, want ErrTooLarge", err) + } +} diff --git a/service/internal/firmware/handler.go b/service/internal/firmware/handler.go index 821fc550..6fffcd9c 100644 --- a/service/internal/firmware/handler.go +++ b/service/internal/firmware/handler.go @@ -57,7 +57,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { in, err := Resolve(spec, idx) if err != nil { h.Log.Warn("firmware: no upstream asset", "soc", soc.URLName, "err", err) - h.page(w, r, http.StatusNotFound, missingAssetMessage(soc, idx), soc) + h.page(w, r, http.StatusNotFound, missingAssetMessage(soc, spec.FlashType, idx), soc) return } @@ -137,14 +137,16 @@ func (h *Handler) buildFailed(w http.ResponseWriter, r *http.Request, soc *catal // missingAssetMessage says which half is missing, because for Xiongmai and the // GK7102 family it is the bootloader, permanently, and "does not exist" sends // that visitor looking for a fault that is not there. -func missingAssetMessage(soc *catalogue.SoC, idx *Index) string { +// +// The bootloader asked about is the one the requested flash type installs: a +// UBI-only SoC publishes its NAND bootloader apart from the NOR one. +func missingAssetMessage(soc *catalogue.SoC, flashType string, idx *Index) string { board := Board(soc, idx) published := len(idx.Releases(board, "nor")) > 0 || len(idx.Releases(board, "nand")) > 0 - _, bootloader := idx.Asset(soc.UBootFilename) switch { case !published: return "OpenIPC does not publish firmware for this SoC yet." - case soc.UBootFilename == "" || !bootloader: + case !BootloaderPublished(soc, idx, flashType): return "OpenIPC does not publish a bootloader for this SoC, so a full flash image cannot be " + "assembled for it. The firmware bundle on the SoC page is published and can be " + "installed with the bootloader your camera already has." diff --git a/service/internal/firmware/layout.go b/service/internal/firmware/layout.go index ab549e15..17921c84 100644 --- a/service/internal/firmware/layout.go +++ b/service/internal/firmware/layout.go @@ -45,6 +45,18 @@ const ( mb = 1 << 20 ) +// The UBI-only NAND layout (catalogue.SoC.UBINand), u-boot-xmedia's +// 768k(boot),256k(env),-(ubi): the NAND bootloader at 0 within its 768 KiB +// boot partition, the env left erased, and rootfs.ubi. -- the whole +// UBI device, kernel volume included -- at 0x100000, bounded by the end of +// the 128 MiB chip the installation page assumes. The image still ends at the +// UBI image rounded up to a page; the wizard writes it with write.trimffs. +const ( + ubiBootLimit = 0xc0000 + ubiOffset = 0x100000 + ubiChipEnd = 128 * mb +) + var ( norSizes = []int{8, 16, 32} norLayoutsMB = []int{8, 16} @@ -153,46 +165,65 @@ func (s Spec) LinuxAsset(idx *Index) string { return fmt.Sprintf("openipc.%s-%s-%s.tgz", Board(s.SoC, idx), s.FlashType, s.Release) } -// Members are the tarball entries the image is made of. +func (s Spec) ubi() bool { return s.nand() && s.SoC.UBINand() } + +// Bootloader is the U-Boot asset the image starts with. +func (s Spec) Bootloader() string { return s.SoC.Bootloader(s.FlashType) } + +// Members are the tarball entries the image is made of. On the UBI-only +// layout the kernel is a volume inside rootfs.ubi, so there is no kernel +// member and kernel is "". func (s Spec) Members(idx *Index) (kernel, rootfs string) { board := Board(s.SoC, idx) + if s.ubi() { + return "", "rootfs.ubi." + board + } if s.nand() { return "uImage." + board, "rootfs.ubi." + board } return "uImage." + board, "rootfs.squashfs." + board } -// part is one region of the image. +// part is one region of the image: the bootloader, or the tarball member +// named by member. type part struct { name string + member string offset, limit int64 limitName string } -// parts are the three regions, each bounded by the next. For NAND the rootfs -// is bounded only by the image, which it defines. -func (s Spec) parts() [3]part { +// parts are the regions in flash order, the bootloader first, each bounded by +// the next. For the split NAND layout the rootfs is bounded only by the +// image, which it defines; on the UBI-only layout by the end of the chip. +func (s Spec) parts(kernel, rootfs string) []part { + if s.ubi() { + return []part{ + {"u-boot", "", 0, ubiBootLimit, "the boot partition"}, + {"rootfs", rootfs, ubiOffset, ubiChipEnd, "the end of the chip"}, + } + } if s.nand() { - return [3]part{ - {"u-boot", 0, nandKernelOffset, "the kernel offset"}, - {"kernel", nandKernelOffset, nandRootfsOffset, "the rootfs offset"}, - {"rootfs", nandRootfsOffset, 1 << 40, "the end of the image"}, + return []part{ + {"u-boot", "", 0, nandKernelOffset, "the kernel offset"}, + {"kernel", kernel, nandKernelOffset, nandRootfsOffset, "the rootfs offset"}, + {"rootfs", rootfs, nandRootfsOffset, 1 << 40, "the end of the image"}, } } l := s.nor() - return [3]part{ - {"u-boot", 0, l.KernelOffset, "the kernel offset"}, - {"kernel", l.KernelOffset, l.RootfsOffset, "the rootfs offset"}, - {"rootfs", l.RootfsOffset, l.OverlayOffset, "the rootfs partition"}, + return []part{ + {"u-boot", "", 0, l.KernelOffset, "the kernel offset"}, + {"kernel", kernel, l.KernelOffset, l.RootfsOffset, "the rootfs offset"}, + {"rootfs", rootfs, l.RootfsOffset, l.OverlayOffset, "the rootfs partition"}, } } -// imageSize is the chip for NOR, and the end of the rootfs rounded up to a -// page for NAND. -func (s Spec) imageSize(rootfsBytes int64) int64 { +// imageSize is the chip for NOR, and the end of the rootfs (the last part) +// rounded up to a page for NAND. +func (s Spec) imageSize(rootfs part, rootfsBytes int64) int64 { if !s.nand() { return int64(s.SizeMB) * mb } - end := nandRootfsOffset + rootfsBytes + end := rootfs.offset + rootfsBytes return (end + nandPage - 1) / nandPage * nandPage } diff --git a/service/internal/firmware/testdata/availability.json b/service/internal/firmware/testdata/availability.json index f9a7af7d..49faefd7 100644 --- a/service/internal/firmware/testdata/availability.json +++ b/service/internal/firmware/testdata/availability.json @@ -1 +1 @@ -{"ak3916ev300":"none","ak3916ev301":"none","ak3918ev200":"none","ak3918ev300":"none","ak3918ev330":"none","dm365":"none","fh8626v100":"none","fh8632v100":"none","fh8652v100":"none","fh8833v100":"none","fh8852v100":"firmware_only","fh8852v200":"firmware_only","fh8852v210":"none","fh8856v100":"none","fh8856v200":"none","fh8856v210":"none","fh8858v200":"none","fh8858v210":"none","gk7102":"firmware_only","gk7102c":"firmware_only","gk7102ca":"firmware_only","gk7102ct":"firmware_only","gk7102cw":"firmware_only","gk7102s":"firmware_only","gk7201v200":"firmware_only","gk7201v300":"none","gk7202v300":"wizard","gk7205v200":"wizard","gk7205v210":"wizard","gk7205v300":"wizard","gk7605v100":"wizard","gm8135":"firmware_only","gm8136":"firmware_only","hi3516av100":"wizard","hi3516av200":"wizard","hi3516av300":"wizard","hi3516cv100":"wizard","hi3516cv200":"wizard","hi3516cv300":"wizard","hi3516cv500":"wizard","hi3516dv100":"wizard","hi3516dv200":"wizard","hi3516dv300":"wizard","hi3516ev100":"wizard","hi3516ev200":"wizard","hi3516ev300":"wizard","hi3518cv100":"wizard","hi3518ev100":"wizard","hi3518ev200":"wizard","hi3518ev201":"firmware_only","hi3518ev300":"wizard","hi3519v101":"wizard","hi3520dv100":"none","hi3520dv200":"firmware_only","hi3536cv100":"firmware_only","hi3536dv100":"firmware_only","msc313e":"none","msc316dc":"none","msc316dm":"none","nt98562":"firmware_only","nt98566":"firmware_only","rv1109":"firmware_only","rv1126":"firmware_only","s2l":"none","s3l":"none","ssc30kd":"wizard","ssc30kq":"wizard","ssc323":"none","ssc325":"wizard","ssc325d":"none","ssc326d":"none","ssc327":"none","ssc327d":"none","ssc327de":"none","ssc327q":"none","ssc328q":"none","ssc329q":"none","ssc333":"wizard","ssc333de":"none","ssc335":"wizard","ssc335de":"wizard","ssc336d":"none","ssc336q":"none","ssc337":"wizard","ssc337de":"wizard","ssc338d":"none","ssc338g":"none","ssc338q":"wizard","ssc339g":"none","ssc377":"wizard","ssc377d":"wizard","ssc377de":"wizard","ssc377qe":"wizard","ssc378de":"wizard","ssc378qe":"wizard","t10":"wizard","t20":"wizard","t21":"wizard","t23n":"wizard","t30l":"wizard","t31a":"wizard","t31al":"wizard","t31l":"wizard","t31lc":"wizard","t31n":"wizard","t31x":"wizard","t31zl":"wizard","t31zx":"wizard","t40a":"wizard","t40n":"wizard","t40nn":"wizard","t40xp":"wizard","t41a":"none","t41l":"none","t41lc":"none","t41lq":"none","t41n":"none","t41nq":"none","t41xq":"none","t41zl":"none","t41zn":"none","t41zx":"none","v831":"none","xm510":"firmware_only","xm530":"firmware_only","xm550":"firmware_only"} \ No newline at end of file +{"ak3916ev300":"none","ak3916ev301":"none","ak3918ev200":"none","ak3918ev300":"none","ak3918ev330":"none","dm365":"none","fh8626v100":"none","fh8632v100":"none","fh8652v100":"none","fh8833v100":"none","fh8852v100":"firmware_only","fh8852v200":"firmware_only","fh8852v210":"none","fh8856v100":"none","fh8856v200":"none","fh8856v210":"none","fh8858v200":"none","fh8858v210":"none","gk7102":"firmware_only","gk7102c":"firmware_only","gk7102ca":"firmware_only","gk7102ct":"firmware_only","gk7102cw":"firmware_only","gk7102s":"firmware_only","gk7201v200":"firmware_only","gk7201v300":"none","gk7202v300":"wizard","gk7205v200":"wizard","gk7205v210":"wizard","gk7205v300":"wizard","gk7205v500":"wizard","gk7205v510":"wizard","gk7205v530":"wizard","gk7605v100":"wizard","gm8135":"firmware_only","gm8136":"firmware_only","hi3516av100":"wizard","hi3516av200":"wizard","hi3516av300":"wizard","hi3516cv100":"wizard","hi3516cv200":"wizard","hi3516cv300":"wizard","hi3516cv500":"wizard","hi3516dv100":"wizard","hi3516dv200":"wizard","hi3516dv300":"wizard","hi3516ev100":"wizard","hi3516ev200":"wizard","hi3516ev300":"wizard","hi3518cv100":"wizard","hi3518ev100":"wizard","hi3518ev200":"wizard","hi3518ev201":"firmware_only","hi3518ev300":"wizard","hi3519v101":"wizard","hi3520dv100":"none","hi3520dv200":"firmware_only","hi3536cv100":"firmware_only","hi3536dv100":"firmware_only","msc313e":"none","msc316dc":"none","msc316dm":"none","nt98562":"firmware_only","nt98566":"firmware_only","rv1109":"firmware_only","rv1126":"firmware_only","s2l":"none","s3l":"none","ssc30kd":"wizard","ssc30kq":"wizard","ssc323":"none","ssc325":"wizard","ssc325d":"none","ssc326d":"none","ssc327":"none","ssc327d":"none","ssc327de":"none","ssc327q":"none","ssc328q":"none","ssc329q":"none","ssc333":"wizard","ssc333de":"none","ssc335":"wizard","ssc335de":"wizard","ssc336d":"none","ssc336q":"none","ssc337":"wizard","ssc337de":"wizard","ssc338d":"none","ssc338g":"none","ssc338q":"wizard","ssc339g":"none","ssc377":"wizard","ssc377d":"wizard","ssc377de":"wizard","ssc377qe":"wizard","ssc378de":"wizard","ssc378qe":"wizard","t10":"wizard","t20":"wizard","t21":"wizard","t23n":"wizard","t30l":"wizard","t31a":"wizard","t31al":"wizard","t31l":"wizard","t31lc":"wizard","t31n":"wizard","t31x":"wizard","t31zl":"wizard","t31zx":"wizard","t40a":"wizard","t40n":"wizard","t40nn":"wizard","t40xp":"wizard","t41a":"none","t41l":"none","t41lc":"none","t41lq":"none","t41n":"none","t41nq":"none","t41xq":"none","t41zl":"none","t41zn":"none","t41zx":"none","v831":"none","xm510":"firmware_only","xm530":"firmware_only","xm550":"firmware_only"} \ No newline at end of file diff --git a/service/internal/firmware/testdata/boards.json b/service/internal/firmware/testdata/boards.json index 29acff84..61051812 100644 --- a/service/internal/firmware/testdata/boards.json +++ b/service/internal/firmware/testdata/boards.json @@ -287,6 +287,36 @@ "nor_lite": "openipc.gk7205v300-nor-lite.tgz", "nand_lite": "openipc.gk7205v300-nand-lite.tgz" }, + { + "urlname": "gk7205v500", + "vendor": "Goke", + "model": "GK7205V500", + "board": "gk7205v500", + "uboot": "u-boot-gk7205v500-nor.bin", + "uboot_nand": "u-boot-gk7205v500-nand.bin", + "nor_lite": "openipc.gk7205v500-nor-lite.tgz", + "nand_lite": "openipc.gk7205v500-nand-lite.tgz" + }, + { + "urlname": "gk7205v510", + "vendor": "Goke", + "model": "GK7205V510", + "board": "gk7205v500", + "uboot": "u-boot-gk7205v510-nor.bin", + "uboot_nand": "u-boot-gk7205v510-nand.bin", + "nor_lite": "openipc.gk7205v500-nor-lite.tgz", + "nand_lite": "openipc.gk7205v500-nand-lite.tgz" + }, + { + "urlname": "gk7205v530", + "vendor": "Goke", + "model": "GK7205V530", + "board": "gk7205v500", + "uboot": "u-boot-gk7205v530-nor.bin", + "uboot_nand": "u-boot-gk7205v530-nand.bin", + "nor_lite": "openipc.gk7205v500-nor-lite.tgz", + "nand_lite": "openipc.gk7205v500-nand-lite.tgz" + }, { "urlname": "gk7605v100", "vendor": "Goke", @@ -391,7 +421,8 @@ "vendor": "HiSilicon", "model": "HI3516DV200", "board": "hi3516dv200", - "uboot": "u-boot-hi3516dv200-universal.bin", + "uboot": "u-boot-hi3516dv200-nor.bin", + "uboot_nand": "u-boot-hi3516dv200-nand.bin", "nor_lite": "openipc.hi3516dv200-nor-lite.tgz", "nand_lite": "openipc.hi3516dv200-nand-lite.tgz" }, @@ -418,7 +449,8 @@ "vendor": "HiSilicon", "model": "HI3516EV200", "board": "hi3516ev200", - "uboot": "u-boot-hi3516ev200-universal.bin", + "uboot": "u-boot-hi3516ev200-nor.bin", + "uboot_nand": "u-boot-hi3516ev200-nand.bin", "nor_lite": "openipc.hi3516ev200-nor-lite.tgz", "nand_lite": "openipc.hi3516ev200-nand-lite.tgz" }, @@ -427,7 +459,8 @@ "vendor": "HiSilicon", "model": "HI3516EV300", "board": "hi3516ev300", - "uboot": "u-boot-hi3516ev300-universal.bin", + "uboot": "u-boot-hi3516ev300-nor.bin", + "uboot_nand": "u-boot-hi3516ev300-nand.bin", "nor_lite": "openipc.hi3516ev300-nor-lite.tgz", "nand_lite": "openipc.hi3516ev300-nand-lite.tgz" }, @@ -472,7 +505,8 @@ "vendor": "HiSilicon", "model": "HI3518EV300", "board": "hi3518ev300", - "uboot": "u-boot-hi3518ev300-universal.bin", + "uboot": "u-boot-hi3518ev300-nor.bin", + "uboot_nand": "u-boot-hi3518ev300-nand.bin", "nor_lite": "openipc.hi3518ev300-nor-lite.tgz", "nand_lite": "openipc.hi3518ev300-nand-lite.tgz" }, diff --git a/service/internal/firmware/testdata/release-index.json b/service/internal/firmware/testdata/release-index.json index 1c7632b7..d9e68b06 100644 --- a/service/internal/firmware/testdata/release-index.json +++ b/service/internal/firmware/testdata/release-index.json @@ -885,6 +885,18 @@ "updated_at": "2026-05-08T12:00:24Z", "release": "latest" }, + "u-boot-hi3516dv200-nand.bin": { + "size": 301631, + "digest": null, + "updated_at": "2026-09-24T00:00:00Z", + "release": "latest" + }, + "u-boot-hi3516dv200-nor.bin": { + "size": 234627, + "digest": null, + "updated_at": "2026-09-24T00:00:00Z", + "release": "latest" + }, "u-boot-hi3516dv200-universal.bin": { "size": 234627, "digest": null, @@ -903,6 +915,18 @@ "updated_at": "2026-05-08T12:05:13Z", "release": "latest" }, + "u-boot-hi3516ev200-nand.bin": { + "size": 301631, + "digest": null, + "updated_at": "2026-09-24T00:00:00Z", + "release": "latest" + }, + "u-boot-hi3516ev200-nor.bin": { + "size": 234599, + "digest": null, + "updated_at": "2026-09-24T00:00:00Z", + "release": "latest" + }, "u-boot-hi3516ev200-universal.bin": { "size": 234599, "digest": "sha256:da7c54c1b8aa3295507637844a539f877d4860d4f387c9203e7f2bf1209ceccf", @@ -915,6 +939,12 @@ "updated_at": "2025-02-10T16:13:20Z", "release": "latest" }, + "u-boot-hi3516ev300-nor.bin": { + "size": 236195, + "digest": null, + "updated_at": "2026-09-24T00:00:00Z", + "release": "latest" + }, "u-boot-hi3516ev300-universal.bin": { "size": 236195, "digest": "sha256:6a54ad5a2b5ab5a604679f7711b3aee223dd52df2a008c6d9dbae9554583dcfd", @@ -963,6 +993,18 @@ "updated_at": "2026-08-27T10:55:33Z", "release": "latest" }, + "u-boot-hi3518ev300-nand.bin": { + "size": 301631, + "digest": null, + "updated_at": "2026-09-24T00:00:00Z", + "release": "latest" + }, + "u-boot-hi3518ev300-nor.bin": { + "size": 236099, + "digest": null, + "updated_at": "2026-09-24T00:00:00Z", + "release": "latest" + }, "u-boot-hi3518ev300-universal.bin": { "size": 236099, "digest": "sha256:1ccbbebe2b1214b24d6386a1e795a26e74d233cfe64b351987b3f909d8dad2b1", diff --git a/service/internal/wizard/camera.go b/service/internal/wizard/camera.go index 56ed0fdc..e68cd89a 100644 --- a/service/internal/wizard/camera.go +++ b/service/internal/wizard/camera.go @@ -40,6 +40,20 @@ const ( nandStagingSizeHex = "0x1800000" ) +// The UBI-only NAND layout, for the SoCs whose catalogue entry names a NAND +// bootloader (catalogue.SoC.UBINand): u-boot-xmedia's +// mtdparts=:768k(boot),256k(env),-(ubi). The kernel is a FIT +// volume inside the one UBI device, so there is no raw kernel partition, no +// uknand/urnand pair and no `run setnand` -- the bootloader's own mtdparts is +// already the layout, and rootfs.ubi. (volumes kernel, rootfs and +// rootfs_data) is the only file an install writes. The UBI region runs to the +// end of the 128 MiB chip the rest of the NAND page assumes. +const ( + ubiBootSize = "0xc0000" + ubiOffset = "0x100000" + ubiRegionSize = "0x7f00000" +) + type norTable struct{ kernelOffset, kernelMax, rootfsOffset, rootfsMax, overlayOffset int64 } // FlashLayout: keyed on the layout's size, and on the vendor for the two @@ -72,6 +86,34 @@ type camera struct { func (c *camera) nand() bool { return c.flashType == "nand" } +// ubi is a NAND install on the UBI-only layout; see ubiBootSize. +func (c *camera) ubi() bool { return c.nand() && c.soc.UBINand() } + +// bootloader is the U-Boot file this install writes. +func (c *camera) bootloader() string { return c.soc.Bootloader(c.flashTypeType()) } + +// bootSize is the boot partition: what the U-Boot step blanks, erases and +// writes. +func (c *camera) bootSize() string { + if c.ubi() { + return ubiBootSize + } + return "0x50000" +} + +// writeCmd is how an image holding UBI goes onto NAND. A plain `nand write` +// programs every page it is given, the 0xFF padding at the end of each PEB +// included; UBIFS later writes those pages again, and a page programmed twice +// fails its ECC (OpenIPC/firmware#2519). `write.trimffs` leaves trailing +// all-0xFF pages erased instead, which is what UBI expects of them. Only the +// UBI-only SoCs get it, so the split layout's commands stay as they were. +func (c *camera) writeCmd() string { + if c.ubi() { + return "write.trimffs" + } + return "write" +} + func (c *camera) flashTypeType() string { switch c.flashType { case "nor8m", "nor16m", "nor32m": @@ -213,6 +255,11 @@ func (c *camera) postFlashCommands() []string { } func (c *camera) layoutCommands() []string { + if c.ubi() { + // The NAND bootloader boots with the UBI-only mtdparts already; there + // is nothing to remap, and it defines no setnand to run. + return nil + } if !c.fixedMtdparts() { return []string{"run set" + c.bootloaderMacroSuffix()} } @@ -227,6 +274,11 @@ func (c *camera) layoutCommands() []string { } func (c *camera) bootloaderVariables() []string { + if c.ubi() { + // None of the install is a bootloader macro here, so the page has no + // uknand, urnand or setnand to point at `printenv` for. + return []string{} + } s := c.bootloaderMacroSuffix() names := []string{"uk" + s, "ur" + s} if !c.fixedMtdparts() { diff --git a/service/internal/wizard/export.go b/service/internal/wizard/export.go index 6272f6a2..4726505e 100644 --- a/service/internal/wizard/export.go +++ b/service/internal/wizard/export.go @@ -61,6 +61,14 @@ func Document(soc *catalogue.SoC, idx *firmware.Index) []byte { d.set("kernel_file", "uImage."+e.board) d.set("rootfs_file", "rootfs.squashfs."+e.board) d.set("bl_url", e.url(soc.UBootFilename)) + // Only where the SoC has a NAND bootloader of its own, so every other + // document stays byte for byte what it was. The page offers this one for + // NAND and uboot_filename for NOR. + if soc.UBINand() { + d.set("uboot_nand_filename", soc.UBootNANDFilename) + d.set("bl_nand_url", e.url(soc.UBootNANDFilename)) + d.set("bootloader_nand_published", e.published(soc.UBootNANDFilename)) + } published := []any{} for _, ft := range flashTypes { for _, rel := range e.releases(ft) { @@ -217,23 +225,21 @@ func (e *exporter) linuxFilename(release, ft string) string { return fmt.Sprintf("openipc.%s-%s-%s.tgz", e.board, ft, release) } -func (e *exporter) bootloaderPublished() bool { - if strings.TrimSpace(e.soc.UBootFilename) == "" { +// bootloaderPublished is the NOR (or only) bootloader's; the NAND one, where +// there is one, is bootloader_nand_published. +func (e *exporter) bootloaderPublished() bool { return e.published(e.soc.UBootFilename) } + +func (e *exporter) published(name string) bool { + if strings.TrimSpace(name) == "" { return false } - _, ok := e.idx.Asset(e.soc.UBootFilename) + _, ok := e.idx.Asset(name) return ok } -func (e *exporter) availability() string { - if len(e.releases("nor")) == 0 && len(e.releases("nand")) == 0 { - return "none" - } - if e.bootloaderPublished() { - return "wizard" - } - return "firmware_only" -} +// availability is firmware.Availability, which knows a SoC may publish its +// NOR and NAND bootloaders separately. +func (e *exporter) availability() string { return firmware.Availability(e.soc, e.idx) } func (e *exporter) specialPage(ft string) string { if e.soc.Vendor.Name == "SigmaStar" && ft == "nand" { diff --git a/service/internal/wizard/lines.go b/service/internal/wizard/lines.go index e028fe94..55861e37 100644 --- a/service/internal/wizard/lines.go +++ b/service/internal/wizard/lines.go @@ -71,12 +71,18 @@ func (c *camera) unlock(text []string) []string { // guardedFlash is the one line that erases only after the transfer worked. func (c *camera) guardedFlash(transfer, offset, eraseSize, writeSize string) string { + return c.guardedWrite(transfer, offset, eraseSize, "write", writeSize) +} + +// guardedWrite is guardedFlash with the write command named: `write.trimffs` +// for anything carrying a UBI image (see writeCmd). +func (c *camera) guardedWrite(transfer, offset, eraseSize, write, writeSize string) string { cmd := "sf" if c.flashType == "nand" { cmd = "nand" } return transfer + " && " + cmd + " erase " + offset + " " + eraseSize + " && " + cmd + - " write " + c.soc.LoadAddress + " " + offset + " " + writeSize + " " + write + " " + c.soc.LoadAddress + " " + offset + " " + writeSize } func (c *camera) writeSizeFor(fixed string) string { @@ -120,34 +126,38 @@ func (c *camera) flashingEverything() []string { fw := c.fullImageFilename() text := []string{doNotPaste, c.env(), "mw.b " + la + " 0xff " + c.stagingSizeHex()} text = c.unlock(text) + // The full image holds the UBI image, so on the UBI-only layout it goes + // on with write.trimffs like rootfs.ubi does on its own. + w := c.writeCmd() if c.sdWifi() { - text = append(text, c.guardedFlash("fatload mmc 0:1 "+la+" "+fw, "0x0", c.flashSizeHex(), "${filesize}")) + text = append(text, c.guardedWrite("fatload mmc 0:1 "+la+" "+fw, "0x0", c.flashSizeHex(), w, "${filesize}")) } else { text = append(text, - c.guardedFlash("tftpboot "+la+" "+fw, "0x0", c.flashSizeHex(), "${filesize}"), + c.guardedWrite("tftpboot "+la+" "+fw, "0x0", c.flashSizeHex(), w, "${filesize}"), "# if there is no tftpboot but tftp then run this instead", - c.guardedFlash("tftp "+la+" "+fw, "0x0", c.flashSizeHex(), "${filesize}")) + c.guardedWrite("tftp "+la+" "+fw, "0x0", c.flashSizeHex(), w, "${filesize}")) } return append(text, "reset") } func (c *camera) flashingUboot() []string { la := c.soc.LoadAddress - ub := c.soc.UBootFilename - ws := c.writeSizeFor("0x50000") + ub := c.bootloader() + size := c.bootSize() + ws := c.writeSizeFor(size) text := []string{doNotPaste} if c.iface != "wifi" { text = append(text, c.env()) } - text = append(text, "mw.b "+la+" 0xff 0x50000") + text = append(text, "mw.b "+la+" 0xff "+size) text = c.unlock(text) if c.sdWifi() { - text = append(text, c.guardedFlash("fatload mmc 0:1 "+la+" "+ub, "0x0", "0x50000", ws)) + text = append(text, c.guardedFlash("fatload mmc 0:1 "+la+" "+ub, "0x0", size, ws)) } else { text = append(text, - c.guardedFlash("tftpboot "+la+" "+ub, "0x0", "0x50000", ws), + c.guardedFlash("tftpboot "+la+" "+ub, "0x0", size, ws), "# if there is no tftpboot but tftp then run this instead", - c.guardedFlash("tftp "+la+" "+ub, "0x0", "0x50000", ws)) + c.guardedFlash("tftp "+la+" "+ub, "0x0", size, ws)) } return append(text, "reset") } @@ -163,6 +173,23 @@ func (c *camera) flashingLinux() []string { } text = append(text, "saveenv") } + if c.ubi() { + // One file, one region: the whole UBI device is erased and the image + // written with write.trimffs, never a plain write (see writeCmd). No + // staging blank: the write is ${filesize} long, and blanking the + // region would be 127 MiB of RAM the camera may not have. + ubi := "rootfs.ubi." + c.board + if c.sdWifi() { + text = append(text, c.guardedWrite("fatload mmc 0:1 "+la+" "+ubi, + ubiOffset, ubiRegionSize, c.writeCmd(), "${filesize}"), "") + } else { + text = append(text, + c.guardedWrite("tftpboot "+la+" "+ubi, ubiOffset, ubiRegionSize, c.writeCmd(), "${filesize}"), + "# if there is no tftpboot but tftp then run this instead", + c.guardedWrite("tftp "+la+" "+ubi, ubiOffset, ubiRegionSize, c.writeCmd(), "${filesize}")) + } + return append(text, "reset") + } if c.sdWifi() { text = append(text, "mw.b "+la+" 0xff 0x200000") text = c.unlock(text) diff --git a/service/internal/wizard/testdata/documents.json b/service/internal/wizard/testdata/documents.json index 9ab2b681..145abde0 100644 --- a/service/internal/wizard/testdata/documents.json +++ b/service/internal/wizard/testdata/documents.json @@ -1,5 +1,5 @@ { - "combinations": 9384, + "combinations": 9528, "files": { "ak3916ev300": "14b8730a659bf6d4f1aefcbeee4b40eb059430df947ade76387e718331643e54", "ak3916ev301": "5078060abb736bec52ce99854f21bbbca3a4d2bffcea91d876587e00c99e4693", @@ -31,6 +31,9 @@ "gk7205v200": "943c32e1c38a82a246c0154c57c5f775375e82b85dae6dd42419564eb5f3e1dd", "gk7205v210": "aeff55cc268f50246c91a2f2ace7d0d74e3d9d228c2ea6b0bca647b63b0ab211", "gk7205v300": "b7aab36c9cfa89b7cb1eaa37b4fab765712176af3f29328500a45dd92e3dde1a", + "gk7205v500": "f5aece93771f8a35ccdb2328f057876590ef0fdb51892af3fb487257d1562459", + "gk7205v510": "16ab8b5c89d2da3243773f9e22e26ec72dc8262ca2a97d0ba358b64c383386a4", + "gk7205v530": "2b0198c5ea8e6c87b0303663938f75c89e50f7e3f54522d1d4a0886d40cfffb6", "gk7605v100": "d20d539bce8fc4f9eb80cfc4d02d816a8c1f2aca7bc66e61d6cf7dfd4983839c", "gm8135": "c367db7ad5b32a232b0ef6e9f301ff91763a38b2c6dc8ee8df3d574aaa92d547", "gm8136": "8f491dca72bc63a626a015bdbdd4b58a74f2d0280ad676a0f82f994bce964fef", @@ -42,16 +45,16 @@ "hi3516cv300": "cc7e4e4a7d03f8683df827855157ad345268c982fe8bde2b3438851b965540df", "hi3516cv500": "2b8926b2eff46757ecc7bb69c3150643dbd7ce7522ff7f57b8019584ab8d4a1b", "hi3516dv100": "013051a4938344031a5fb25ffad178d18332b9012488c023cb2a1f9ec23e240c", - "hi3516dv200": "9aab13cea44c27c489d42e3801da5d79b7f6b8efea90f3b6e0cdae5026326bde", + "hi3516dv200": "ac94c377604c0187ebebb68a290d42926abb655afa615effa30a82bf5878f156", "hi3516dv300": "e64903d05af789169ddafd9d2d11db2abc9e97e1879edbf0957e4059c90bc69f", "hi3516ev100": "bc30ca2a98b204e48cca23a066be0894a79cb18133db40cc7f47212eac949da9", - "hi3516ev200": "cc090977f57de1619fc74f9cd91678ceff235ded180dc7d9286fea2b2e6858ee", - "hi3516ev300": "1ce40eda103709c8703d24f86bddbd29dfdee3d90d87acb6abeb40755c18b720", + "hi3516ev200": "27e1e186bcfd4bd3e5ad135f34e0f358404f8f8e64534e7b7bf287367dd3ba38", + "hi3516ev300": "075c54c7502ce196bf14d40f2374d52f5e00a6501ef05fffce2e6c8ee4ffaec0", "hi3518cv100": "b08c8580dd3e18f20a5967005733425911b53430b05017874798b11fa2909868", "hi3518ev100": "4533443ab52a57946bab06de2678a5e0b0a285b05a531023d24ef568b4cbe4de", "hi3518ev200": "6f3aabd0af3d4f4f530c9eb525156334c7d4254765b8dba19d89e66df9b7fb04", "hi3518ev201": "73ed154710b0cab632f8edfd5089f6632bea1f63045e8ef17b60754f64abddf4", - "hi3518ev300": "c2018f258312987b3c0fcd0a8e569832c616204e0b0e4427830d30bb742cece6", + "hi3518ev300": "a550f277a11a0dd8938e4169e8388538713061e350b6ca67d03118ec0496b179", "hi3519v101": "23466502aa228417ef33ff45ce97230d337c94a6a3460619796a7aa19182584d", "hi3520dv100": "4ea373391fe243d60d590b011969a53c4918051d0c0a94ed6241aa2c73849f0b", "hi3520dv200": "782f9cb791ab9084424ccae5d195d38b65209d551b726db11c40996f66eead24", diff --git a/service/internal/wizard/wizard_test.go b/service/internal/wizard/wizard_test.go index 0ab3880f..e1630f36 100644 --- a/service/internal/wizard/wizard_test.go +++ b/service/internal/wizard/wizard_test.go @@ -217,3 +217,83 @@ func TestHandler(t *testing.T) { t.Errorf("no index: %d, want 503", w.Code) } } + +// The UBI-only NAND layout (hi3516ev300 and the other u-boot-xmedia SoCs): +// the NAND bootloader into a 768 KiB boot partition, rootfs.ubi alone into +// the UBI region with write.trimffs, the full image with write.trimffs too, +// and no bootloader macro anywhere -- the NAND bootloader defines no +// uknand/urnand/setnand. A SoC still on the split layout keeps all of that. +func TestUBINandLines(t *testing.T) { + cat, idx := inputs(t) + d := decode(t, Document(cat.SoC("hi3516ev300"), idx)) + if d["uboot_nand_filename"] != "u-boot-hi3516ev300-nand.bin" || d["bootloader_nand_published"] != true || + !strings.HasSuffix(d["bl_nand_url"].(string), "/u-boot-hi3516ev300-nand.bin") { + t.Errorf("NAND bootloader fields: %v, %v, %v", d["uboot_nand_filename"], d["bootloader_nand_published"], d["bl_nand_url"]) + } + if d["uboot_filename"] != "u-boot-hi3516ev300-nor.bin" { + t.Errorf("uboot_filename %v, want the NOR build", d["uboot_filename"]) + } + pool := d["blocks"].(map[string]any) + block := func(c map[string]any, name string) string { + var lines []string + for _, l := range pool[c["blocks"].(map[string]any)[name].(string)].(map[string]any)["lines"].([]any) { + lines = append(lines, l.(string)) + } + return strings.Join(lines, "\n") + } + seen := 0 + for _, raw := range d["combinations"].([]any) { + c := raw.(map[string]any) + if c["flash_type"] != "nand" { + continue + } + seen++ + if v := c["bootloader_variables"].([]any); len(v) != 0 || c["layout_commands"] != false { + t.Errorf("NAND advertises bootloader macros %v (layout_commands %v)", v, c["layout_commands"]) + } + uboot := block(c, "flashing_uboot") + if !strings.Contains(uboot, "u-boot-hi3516ev300-nand.bin && nand erase 0x0 0xc0000 && nand write 0x42000000 0x0 0xc0000") || + !strings.Contains(uboot, "mw.b 0x42000000 0xff 0xc0000") || strings.Contains(uboot, "0x50000") { + t.Errorf("U-Boot block:\n%s", uboot) + } + linux := block(c, "flashing_linux") + if !strings.Contains(linux, "rootfs.ubi.hi3516ev300 && nand erase 0x100000 0x7f00000 && "+ + "nand write.trimffs 0x42000000 0x100000 ${filesize}") { + t.Errorf("Linux block:\n%s", linux) + } + full := block(c, "flashing_everything") + if !strings.Contains(full, "nand write.trimffs 0x42000000 0x0 ${filesize}") || strings.Contains(full, "nand write 0x") { + t.Errorf("full image block:\n%s", full) + } + for name := range c["blocks"].(map[string]any) { + for _, bad := range []string{"uknand", "urnand", "setnand", "uImage", "rootfs.squashfs"} { + if strings.Contains(block(c, name), bad) { + t.Errorf("%s mentions %s:\n%s", name, bad, block(c, name)) + } + } + } + } + if seen == 0 { + t.Fatal("hi3516ev300 offers no NAND combination") + } + + // hi3516av100 is still on the split layout, so nothing about it moves. + d = decode(t, Document(cat.SoC("hi3516av100"), idx)) + if _, ok := d["uboot_nand_filename"]; ok { + t.Error("a split-layout SoC grew a NAND bootloader") + } + pool = d["blocks"].(map[string]any) + for _, raw := range d["combinations"].([]any) { + c := raw.(map[string]any) + if c["flash_type"] != "nand" { + continue + } + if linux := block(c, "flashing_linux"); !strings.Contains(linux, "run uknand; run urnand") { + t.Errorf("split-layout Linux block:\n%s", linux) + } + if full := block(c, "flashing_everything"); strings.Contains(full, "trimffs") { + t.Errorf("split-layout full image block:\n%s", full) + } + break + } +} From 1d76d302cb014bf25464926b672bab721061c3c0 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:58:05 +0300 Subject: [PATCH 2/2] Give GK7205V510 its own NOR firmware and offer a NAND install only with its bootloader Review on #392. - GK7205V510 has a NOR build of its own, but its NAND firmware is the GK7205V500 build. A new catalogue field, nand_board, names the build a SoC's NAND firmware comes from. firmware.BoardFor resolves the build per flash type, and the tarball, the image members, availability, the wizard's releases and its install lines all go through it. - On a UBI-only SoC, each flash type installs its own bootloader. A flash type whose bootloader is not published is no longer offered: it gets no editions and no combinations, rather than the nothing-published menu. Old links fall back to an offered chip as before. The U-Boot download link only shows when its file is published. - The UBI install erases a 128 MiB chip. The Linux block now says so, and gives the 0xff00000 length for a 256 MiB part. New tests: TestUBINandWithoutItsBootloaderIsNotOffered and TestNANDBoardIsUsedForNANDOnly. boards.json now records V510's own NOR build. Go (service/run.sh test), frontend lint, typecheck, tests and build all pass. --- data/catalogue/goke.yml | 3 +- .../site/src/components/wizard/Result.tsx | 2 +- service/internal/catalogue/catalogue.go | 4 ++ service/internal/firmware/availability.go | 6 +-- service/internal/firmware/handler.go | 6 +-- service/internal/firmware/layout.go | 16 +++++- .../internal/firmware/testdata/boards.json | 4 +- service/internal/wizard/export.go | 21 ++++++-- service/internal/wizard/lines.go | 3 ++ .../internal/wizard/testdata/documents.json | 16 +++--- service/internal/wizard/wizard_test.go | 50 +++++++++++++++++++ 11 files changed, 108 insertions(+), 23 deletions(-) diff --git a/data/catalogue/goke.yml b/data/catalogue/goke.yml index 8c7076a8..bdd85566 100644 --- a/data/catalogue/goke.yml +++ b/data/catalogue/goke.yml @@ -201,7 +201,8 @@ socs: kernel: '' uboot_filename: u-boot-gk7205v510-nor.bin uboot_nand_filename: u-boot-gk7205v510-nand.bin - linux_filename: openipc.gk7205v500-nor-lite.tgz + linux_filename: openipc.gk7205v510-nor-lite.tgz + nand_board: gk7205v500 notes: '' urlname: gk7205v510 build_status_url: https://github.com/OpenIPC/firmware/actions/workflows/gk7205v500_images.yml/badge.svg diff --git a/frontend/apps/site/src/components/wizard/Result.tsx b/frontend/apps/site/src/components/wizard/Result.tsx index 052dd8ad..fb2bda57 100644 --- a/frontend/apps/site/src/components/wizard/Result.tsx +++ b/frontend/apps/site/src/components/wizard/Result.tsx @@ -573,7 +573,7 @@ function Experts({ t, doc, combination, settings, facts, sdcardRequired, edition

{install('flashing_uboot.title')}

- {bootloader.filename !== '' && ( + {bootloader.filename !== '' && bootloader.published && (
diff --git a/service/internal/catalogue/catalogue.go b/service/internal/catalogue/catalogue.go index fb94b91f..b64df776 100644 --- a/service/internal/catalogue/catalogue.go +++ b/service/internal/catalogue/catalogue.go @@ -28,6 +28,10 @@ type SoC struct { // which keeps the split layout (boot, env, kernel, ubi) and its `run // uknand; run urnand` for every SoC that still boots that way. UBootNANDFilename string `yaml:"uboot_nand_filename"` + // NANDBoard is the build name upstream publishes this SoC's NAND + // firmware under, when that is not the NOR one: GK7205V510 has a NOR + // build of its own, but its NAND firmware is the GK7205V500 build. + NANDBoard string `yaml:"nand_board"` LinuxFilename string `yaml:"linux_filename"` LoadAddress string `yaml:"load_address"` Vendor *Vendor `yaml:"-"` diff --git a/service/internal/firmware/availability.go b/service/internal/firmware/availability.go index d91b029c..e2979127 100644 --- a/service/internal/firmware/availability.go +++ b/service/internal/firmware/availability.go @@ -26,15 +26,15 @@ func Availability(soc *catalogue.SoC, idx *Index) string { } return "none" } - board := Board(soc, idx) - if len(idx.Releases(board, "nor")) == 0 && len(idx.Releases(board, "nand")) == 0 { + nor, nand := BoardFor(soc, idx, "nor"), BoardFor(soc, idx, "nand") + if len(idx.Releases(nor, "nor")) == 0 && len(idx.Releases(nand, "nand")) == 0 { return "none" } // A flash type counts when it has firmware and the bootloader that flash // type installs. That is one file for most SoCs, and two -- the NOR and // the NAND build -- for the UBI-only ones, which upstream publishes apart. for _, ft := range []string{"nor", "nand"} { - if len(idx.Releases(board, ft)) > 0 && BootloaderPublished(soc, idx, ft) { + if len(idx.Releases(BoardFor(soc, idx, ft), ft)) > 0 && BootloaderPublished(soc, idx, ft) { return "wizard" } } diff --git a/service/internal/firmware/handler.go b/service/internal/firmware/handler.go index 6fffcd9c..3477e201 100644 --- a/service/internal/firmware/handler.go +++ b/service/internal/firmware/handler.go @@ -141,8 +141,8 @@ func (h *Handler) buildFailed(w http.ResponseWriter, r *http.Request, soc *catal // The bootloader asked about is the one the requested flash type installs: a // UBI-only SoC publishes its NAND bootloader apart from the NOR one. func missingAssetMessage(soc *catalogue.SoC, flashType string, idx *Index) string { - board := Board(soc, idx) - published := len(idx.Releases(board, "nor")) > 0 || len(idx.Releases(board, "nand")) > 0 + published := len(idx.Releases(BoardFor(soc, idx, "nor"), "nor")) > 0 || + len(idx.Releases(BoardFor(soc, idx, "nand"), "nand")) > 0 switch { case !published: return "OpenIPC does not publish firmware for this SoC yet." @@ -217,7 +217,7 @@ func pathBase(path string) string { return filepath.Base(path) } // tooLargeMessage names the flash the build is made for when its build said so // (#285): "try the Lite edition" is no help to somebody already on Lite. func tooLargeMessage(spec Spec, idx *Index) string { - if f, ok := idx.Fit(Board(spec.SoC, idx), spec.Release); ok && f.FlashMB > spec.SizeMB { + if f, ok := idx.Fit(BoardFor(spec.SoC, idx, spec.FlashType), spec.Release); ok && f.FlashMB > spec.SizeMB { return fmt.Sprintf("This firmware is built for %d MB flash and does not fit %d MB. "+ "Choose a %d MB chip on the installation page.", f.FlashMB, spec.SizeMB, f.FlashMB) } diff --git a/service/internal/firmware/layout.go b/service/internal/firmware/layout.go index 17921c84..43edb361 100644 --- a/service/internal/firmware/layout.go +++ b/service/internal/firmware/layout.go @@ -160,9 +160,21 @@ func Board(soc *catalogue.SoC, idx *Index) string { return board } +// BoardFor is the build name a flash type's firmware is published under: +// Board, unless the SoC names a separate NAND build (nand_board). +func BoardFor(soc *catalogue.SoC, idx *Index, flashType string) string { + if flashType == "nand" && soc.NANDBoard != "" { + if idx != nil { + return idx.CanonicalBoard(soc.NANDBoard) + } + return soc.NANDBoard + } + return Board(soc, idx) +} + // LinuxAsset is the tarball carrying the kernel and rootfs. func (s Spec) LinuxAsset(idx *Index) string { - return fmt.Sprintf("openipc.%s-%s-%s.tgz", Board(s.SoC, idx), s.FlashType, s.Release) + return fmt.Sprintf("openipc.%s-%s-%s.tgz", BoardFor(s.SoC, idx, s.FlashType), s.FlashType, s.Release) } func (s Spec) ubi() bool { return s.nand() && s.SoC.UBINand() } @@ -174,7 +186,7 @@ func (s Spec) Bootloader() string { return s.SoC.Bootloader(s.FlashType) } // layout the kernel is a volume inside rootfs.ubi, so there is no kernel // member and kernel is "". func (s Spec) Members(idx *Index) (kernel, rootfs string) { - board := Board(s.SoC, idx) + board := BoardFor(s.SoC, idx, s.FlashType) if s.ubi() { return "", "rootfs.ubi." + board } diff --git a/service/internal/firmware/testdata/boards.json b/service/internal/firmware/testdata/boards.json index 61051812..5bb49377 100644 --- a/service/internal/firmware/testdata/boards.json +++ b/service/internal/firmware/testdata/boards.json @@ -301,10 +301,10 @@ "urlname": "gk7205v510", "vendor": "Goke", "model": "GK7205V510", - "board": "gk7205v500", + "board": "gk7205v510", "uboot": "u-boot-gk7205v510-nor.bin", "uboot_nand": "u-boot-gk7205v510-nand.bin", - "nor_lite": "openipc.gk7205v500-nor-lite.tgz", + "nor_lite": "openipc.gk7205v510-nor-lite.tgz", "nand_lite": "openipc.gk7205v500-nand-lite.tgz" }, { diff --git a/service/internal/wizard/export.go b/service/internal/wizard/export.go index 4726505e..ac1e07bb 100644 --- a/service/internal/wizard/export.go +++ b/service/internal/wizard/export.go @@ -127,7 +127,14 @@ func nonNil(o obj) obj { // releases is Soc#published_releases: what the index says, in display order. func (e *exporter) releases(ft string) []string { - rels := slices.Clone(e.idx.Releases(e.board, ft)) + // A UBI-only SoC publishes a bootloader per flash type, and its + // instructions write the one for the flash chosen: without it that flash + // type cannot be installed, so it is not offered at all. (Elsewhere a + // missing bootloader means installing with the camera's own, which stays.) + if e.soc.UBINand() && !firmware.BootloaderPublished(e.soc, e.idx, ft) { + return []string{} + } + rels := slices.Clone(e.idx.Releases(e.boardFor(ft), ft)) rank := func(r string) int { if i := slices.Index(releaseOrder, r); i >= 0 { return i @@ -222,9 +229,12 @@ func (e *exporter) needsFlashMB() int { } func (e *exporter) linuxFilename(release, ft string) string { - return fmt.Sprintf("openipc.%s-%s-%s.tgz", e.board, ft, release) + return fmt.Sprintf("openipc.%s-%s-%s.tgz", e.boardFor(ft), ft, release) } +// boardFor is the build a flash type's firmware comes from (nand_board). +func (e *exporter) boardFor(ft string) string { return firmware.BoardFor(e.soc, e.idx, ft) } + // bootloaderPublished is the NOR (or only) bootloader's; the NAND one, where // there is one, is bootloader_nand_published. func (e *exporter) bootloaderPublished() bool { return e.published(e.soc.UBootFilename) } @@ -272,6 +282,11 @@ func layoutsFor(ft string) []*string { } func (e *exporter) editionsFor(ft string, layout *string) []string { + // Not the nothing-published menu below: firmware is there, the bootloader + // this flash type installs is not (see releases). + if e.soc.UBINand() && !firmware.BootloaderPublished(e.soc, e.idx, familyOf(ft)) { + return nil + } published := e.releases(familyOf(ft)) offered := published if len(published) == 0 { @@ -325,7 +340,7 @@ func (e *exporter) combinations() []any { } func (e *exporter) camera(ft string, layout *string, edition, iface, sd, mac string) *camera { - c := &camera{soc: e.soc, board: e.board, flashType: ft, edition: edition, iface: iface, sd: sd, + c := &camera{soc: e.soc, board: e.boardFor(ft), flashType: ft, edition: edition, iface: iface, sd: sd, ip: ipaddr, server: serverip, mac: mac} if layout != nil { c.layout = *layout diff --git a/service/internal/wizard/lines.go b/service/internal/wizard/lines.go index 55861e37..09946779 100644 --- a/service/internal/wizard/lines.go +++ b/service/internal/wizard/lines.go @@ -179,6 +179,9 @@ func (c *camera) flashingLinux() []string { // staging blank: the write is ${filesize} long, and blanking the // region would be 127 MiB of RAM the camera may not have. ubi := "rootfs.ubi." + c.board + // The UBI partition runs to the end of the chip (-(ubi)); the erase + // below covers a 128 MiB one, the size the wizard installs for. + text = append(text, "# erases a 128 MiB chip: on a 256 MiB one use 0xff00000 instead of 0x7f00000") if c.sdWifi() { text = append(text, c.guardedWrite("fatload mmc 0:1 "+la+" "+ubi, ubiOffset, ubiRegionSize, c.writeCmd(), "${filesize}"), "") diff --git a/service/internal/wizard/testdata/documents.json b/service/internal/wizard/testdata/documents.json index 145abde0..c5d34499 100644 --- a/service/internal/wizard/testdata/documents.json +++ b/service/internal/wizard/testdata/documents.json @@ -1,5 +1,5 @@ { - "combinations": 9528, + "combinations": 9516, "files": { "ak3916ev300": "14b8730a659bf6d4f1aefcbeee4b40eb059430df947ade76387e718331643e54", "ak3916ev301": "5078060abb736bec52ce99854f21bbbca3a4d2bffcea91d876587e00c99e4693", @@ -31,9 +31,9 @@ "gk7205v200": "943c32e1c38a82a246c0154c57c5f775375e82b85dae6dd42419564eb5f3e1dd", "gk7205v210": "aeff55cc268f50246c91a2f2ace7d0d74e3d9d228c2ea6b0bca647b63b0ab211", "gk7205v300": "b7aab36c9cfa89b7cb1eaa37b4fab765712176af3f29328500a45dd92e3dde1a", - "gk7205v500": "f5aece93771f8a35ccdb2328f057876590ef0fdb51892af3fb487257d1562459", - "gk7205v510": "16ab8b5c89d2da3243773f9e22e26ec72dc8262ca2a97d0ba358b64c383386a4", - "gk7205v530": "2b0198c5ea8e6c87b0303663938f75c89e50f7e3f54522d1d4a0886d40cfffb6", + "gk7205v500": "4e4973d6bb2b5282cdb521bf734dd5a070345516b76876519a04ef988594c026", + "gk7205v510": "63c47f96eb0bd8a2a3fbfffcd2c46bfa36a1fed98682623f97c891fe07a0f3e3", + "gk7205v530": "cbb8d2771a0b24441b7f4143aab448ac2798265024accccee2b1ed3bb1513bdf", "gk7605v100": "d20d539bce8fc4f9eb80cfc4d02d816a8c1f2aca7bc66e61d6cf7dfd4983839c", "gm8135": "c367db7ad5b32a232b0ef6e9f301ff91763a38b2c6dc8ee8df3d574aaa92d547", "gm8136": "8f491dca72bc63a626a015bdbdd4b58a74f2d0280ad676a0f82f994bce964fef", @@ -45,16 +45,16 @@ "hi3516cv300": "cc7e4e4a7d03f8683df827855157ad345268c982fe8bde2b3438851b965540df", "hi3516cv500": "2b8926b2eff46757ecc7bb69c3150643dbd7ce7522ff7f57b8019584ab8d4a1b", "hi3516dv100": "013051a4938344031a5fb25ffad178d18332b9012488c023cb2a1f9ec23e240c", - "hi3516dv200": "ac94c377604c0187ebebb68a290d42926abb655afa615effa30a82bf5878f156", + "hi3516dv200": "d481e0adc36650363ef3350b056252ade986e4a87d7b536c461ded7badf7b019", "hi3516dv300": "e64903d05af789169ddafd9d2d11db2abc9e97e1879edbf0957e4059c90bc69f", "hi3516ev100": "bc30ca2a98b204e48cca23a066be0894a79cb18133db40cc7f47212eac949da9", - "hi3516ev200": "27e1e186bcfd4bd3e5ad135f34e0f358404f8f8e64534e7b7bf287367dd3ba38", - "hi3516ev300": "075c54c7502ce196bf14d40f2374d52f5e00a6501ef05fffce2e6c8ee4ffaec0", + "hi3516ev200": "b0e1b515ac53390b20f17238d3b4f9cb09621b10c3e2572779c3e1f2b3e60b1f", + "hi3516ev300": "a32163f61c32b36028757e4bd1a443cda6c1e74385ac79b8002a161ea7b02935", "hi3518cv100": "b08c8580dd3e18f20a5967005733425911b53430b05017874798b11fa2909868", "hi3518ev100": "4533443ab52a57946bab06de2678a5e0b0a285b05a531023d24ef568b4cbe4de", "hi3518ev200": "6f3aabd0af3d4f4f530c9eb525156334c7d4254765b8dba19d89e66df9b7fb04", "hi3518ev201": "73ed154710b0cab632f8edfd5089f6632bea1f63045e8ef17b60754f64abddf4", - "hi3518ev300": "a550f277a11a0dd8938e4169e8388538713061e350b6ca67d03118ec0496b179", + "hi3518ev300": "4d16f1710098b62d3828615b13ff8146db34d37fd4d95835b7b35bca1a99b1f9", "hi3519v101": "23466502aa228417ef33ff45ce97230d337c94a6a3460619796a7aa19182584d", "hi3520dv100": "4ea373391fe243d60d590b011969a53c4918051d0c0a94ed6241aa2c73849f0b", "hi3520dv200": "782f9cb791ab9084424ccae5d195d38b65209d551b726db11c40996f66eead24", diff --git a/service/internal/wizard/wizard_test.go b/service/internal/wizard/wizard_test.go index e1630f36..7e77eba5 100644 --- a/service/internal/wizard/wizard_test.go +++ b/service/internal/wizard/wizard_test.go @@ -297,3 +297,53 @@ func TestUBINandLines(t *testing.T) { break } } + +// A UBI-only SoC installs the bootloader its flash type names, so a flash type +// whose bootloader is not published is not offered at all: no editions, no +// combinations, no install lines pointing at a file that is not there. +func TestUBINandWithoutItsBootloaderIsNotOffered(t *testing.T) { + cat, idx := inputs(t) + soc := *cat.SoC("hi3516ev300") + soc.UBootNANDFilename = "u-boot-hi3516ev300-unpublished-nand.bin" + d := decode(t, Document(&soc, idx)) + if nand := d["editions"].(map[string]any)["nand"].([]any); len(nand) != 0 { + t.Errorf("NAND editions %v offered without a published NAND bootloader", nand) + } + for _, raw := range d["combinations"].([]any) { + if raw.(map[string]any)["flash_type"] == "nand" { + t.Fatalf("a NAND combination was exported without a published NAND bootloader") + } + } + if nor := d["editions"].(map[string]any)["nor"].([]any); len(nor) == 0 { + t.Errorf("NOR stopped being offered too") + } +} + +// GK7205V510 has a NOR build of its own; its NAND firmware is the GK7205V500 +// build (nand_board), and each flash type names its own. +func TestNANDBoardIsUsedForNANDOnly(t *testing.T) { + cat, idx := inputs(t) + d := decode(t, Document(cat.SoC("gk7205v510"), idx)) + if d["board"] != "gk7205v510" { + t.Errorf("board %v, want the NOR build gk7205v510", d["board"]) + } + pool := d["blocks"].(map[string]any) + sawNand := false + for _, raw := range d["combinations"].([]any) { + c := raw.(map[string]any) + if c["flash_type"] != "nand" { + continue + } + sawNand = true + var lines []string + for _, l := range pool[c["blocks"].(map[string]any)["flashing_linux"].(string)].(map[string]any)["lines"].([]any) { + lines = append(lines, l.(string)) + } + if all := strings.Join(lines, "\n"); !strings.Contains(all, "rootfs.ubi.gk7205v500") { + t.Errorf("NAND install does not use the gk7205v500 build:\n%s", all) + } + } + if !sawNand { + t.Errorf("no NAND combination for gk7205v510") + } +}