- {doc.uboot_filename !== '' && (
+ {bootloader.filename !== '' && bootloader.published && (
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")
+ }
+}