diff --git a/data/catalogue/goke.yml b/data/catalogue/goke.yml index 0515b948..bdd85566 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,52 @@ 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.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 + 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 && (
-
{show('bootloader')}
+
+ + {doc.uboot_nand_filename ? `${show('bootloader')} NOR` : show('bootloader')} + +
+

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

+
+ )} + {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..fb2bda57 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 !== '' && bootloader.published && (
- {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..b64df776 100644 --- a/service/internal/catalogue/catalogue.go +++ b/service/internal/catalogue/catalogue.go @@ -15,14 +15,26 @@ 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"` + // 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:"-"` } type Vendor struct { @@ -97,5 +109,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..e2979127 100644 --- a/service/internal/firmware/availability.go +++ b/service/internal/firmware/availability.go @@ -26,17 +26,30 @@ 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" } - 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(BoardFor(soc, idx, ft), 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..3477e201 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 { - board := Board(soc, idx) - published := len(idx.Releases(board, "nor")) > 0 || len(idx.Releases(board, "nand")) > 0 - _, bootloader := idx.Asset(soc.UBootFilename) +// +// 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 { + 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." - 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." @@ -215,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 ab549e15..43edb361 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} @@ -148,51 +160,82 @@ 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) } -// 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) + board := BoardFor(s.SoC, idx, s.FlashType) + 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..5bb49377 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": "gk7205v510", + "uboot": "u-boot-gk7205v510-nor.bin", + "uboot_nand": "u-boot-gk7205v510-nand.bin", + "nor_lite": "openipc.gk7205v510-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..ac1e07bb 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) { @@ -119,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 @@ -214,26 +229,27 @@ 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) } -func (e *exporter) bootloaderPublished() bool { - if strings.TrimSpace(e.soc.UBootFilename) == "" { +// 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) } + +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" { @@ -266,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 { @@ -319,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 e028fe94..09946779 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,26 @@ 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 + // 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}"), "") + } 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..c5d34499 100644 --- a/service/internal/wizard/testdata/documents.json +++ b/service/internal/wizard/testdata/documents.json @@ -1,5 +1,5 @@ { - "combinations": 9384, + "combinations": 9516, "files": { "ak3916ev300": "14b8730a659bf6d4f1aefcbeee4b40eb059430df947ade76387e718331643e54", "ak3916ev301": "5078060abb736bec52ce99854f21bbbca3a4d2bffcea91d876587e00c99e4693", @@ -31,6 +31,9 @@ "gk7205v200": "943c32e1c38a82a246c0154c57c5f775375e82b85dae6dd42419564eb5f3e1dd", "gk7205v210": "aeff55cc268f50246c91a2f2ace7d0d74e3d9d228c2ea6b0bca647b63b0ab211", "gk7205v300": "b7aab36c9cfa89b7cb1eaa37b4fab765712176af3f29328500a45dd92e3dde1a", + "gk7205v500": "4e4973d6bb2b5282cdb521bf734dd5a070345516b76876519a04ef988594c026", + "gk7205v510": "63c47f96eb0bd8a2a3fbfffcd2c46bfa36a1fed98682623f97c891fe07a0f3e3", + "gk7205v530": "cbb8d2771a0b24441b7f4143aab448ac2798265024accccee2b1ed3bb1513bdf", "gk7605v100": "d20d539bce8fc4f9eb80cfc4d02d816a8c1f2aca7bc66e61d6cf7dfd4983839c", "gm8135": "c367db7ad5b32a232b0ef6e9f301ff91763a38b2c6dc8ee8df3d574aaa92d547", "gm8136": "8f491dca72bc63a626a015bdbdd4b58a74f2d0280ad676a0f82f994bce964fef", @@ -42,16 +45,16 @@ "hi3516cv300": "cc7e4e4a7d03f8683df827855157ad345268c982fe8bde2b3438851b965540df", "hi3516cv500": "2b8926b2eff46757ecc7bb69c3150643dbd7ce7522ff7f57b8019584ab8d4a1b", "hi3516dv100": "013051a4938344031a5fb25ffad178d18332b9012488c023cb2a1f9ec23e240c", - "hi3516dv200": "9aab13cea44c27c489d42e3801da5d79b7f6b8efea90f3b6e0cdae5026326bde", + "hi3516dv200": "d481e0adc36650363ef3350b056252ade986e4a87d7b536c461ded7badf7b019", "hi3516dv300": "e64903d05af789169ddafd9d2d11db2abc9e97e1879edbf0957e4059c90bc69f", "hi3516ev100": "bc30ca2a98b204e48cca23a066be0894a79cb18133db40cc7f47212eac949da9", - "hi3516ev200": "cc090977f57de1619fc74f9cd91678ceff235ded180dc7d9286fea2b2e6858ee", - "hi3516ev300": "1ce40eda103709c8703d24f86bddbd29dfdee3d90d87acb6abeb40755c18b720", + "hi3516ev200": "b0e1b515ac53390b20f17238d3b4f9cb09621b10c3e2572779c3e1f2b3e60b1f", + "hi3516ev300": "a32163f61c32b36028757e4bd1a443cda6c1e74385ac79b8002a161ea7b02935", "hi3518cv100": "b08c8580dd3e18f20a5967005733425911b53430b05017874798b11fa2909868", "hi3518ev100": "4533443ab52a57946bab06de2678a5e0b0a285b05a531023d24ef568b4cbe4de", "hi3518ev200": "6f3aabd0af3d4f4f530c9eb525156334c7d4254765b8dba19d89e66df9b7fb04", "hi3518ev201": "73ed154710b0cab632f8edfd5089f6632bea1f63045e8ef17b60754f64abddf4", - "hi3518ev300": "c2018f258312987b3c0fcd0a8e569832c616204e0b0e4427830d30bb742cece6", + "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 0ab3880f..7e77eba5 100644 --- a/service/internal/wizard/wizard_test.go +++ b/service/internal/wizard/wizard_test.go @@ -217,3 +217,133 @@ 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 + } +} + +// 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") + } +}