From 187efd4c16149a112b6d054a5d224cef18c908b3 Mon Sep 17 00:00:00 2001 From: AI Dev Date: Fri, 2 Oct 2026 10:01:04 +0000 Subject: [PATCH 01/13] Board catalogue: what owners send about their own boards, reviewed in the repository (#365, #366) The board panel asks a visitor with the board for a boot log or a U-Boot console, and the answer arrives as an issue. The catalogue had nowhere to put it: migration 003's `contributor` source and `contributed_by` were never written, and an owner report needs ipctool's YAML. boards/contributions.yml lists each reviewed contribution as a unit of an existing model, its text under contributions//, and the web role applies it on every start, as it does contents.yml: an unchanged unit is left alone, a changed one replaced, a removed one removed with its files, and an entry whose model is not in the catalogue waits for the import that brings it. The text is searchable and counted like any other unit's, its printenv becomes rows, and the panel credits the sender with the issue. The first entry is Anjoy MS-J10 from sansarus: the boot log (#365) and the U-Boot console (#366) of one board, IMX307 on SSC335 (the kernel's INFINITY6B0 SSC009A-S01A) with an 8 MB GD25Q64, stock 3.3.0.2. --- data/locales/boards.en.yml | 1 + data/locales/boards.ru.yml | 1 + data/locales/boards.zh.yml | 1 + .../site/src/components/boards/BoardPanel.tsx | 12 +- frontend/apps/site/src/i18n/boards.en.json | 1 + frontend/apps/site/src/i18n/boards.ru.json | 1 + frontend/apps/site/src/i18n/boards.zh.json | 1 + .../apps/site/src/lib/boards/boards.test.ts | 18 +- frontend/apps/site/src/lib/boards/model.ts | 21 +- service/cmd/openipc/boards.go | 28 +- service/cmd/openipc/main.go | 6 + service/internal/boards/contributions.go | 279 ++++++++++ service/internal/boards/contributions.yml | 31 ++ .../contributions/anjoy-ms-j10-c365/boot.log | 527 ++++++++++++++++++ .../contributions/anjoy-ms-j10-c365/uboot.txt | 147 +++++ service/internal/boards/contributions_test.go | 249 +++++++++ 16 files changed, 1318 insertions(+), 6 deletions(-) create mode 100644 service/internal/boards/contributions.go create mode 100644 service/internal/boards/contributions.yml create mode 100644 service/internal/boards/contributions/anjoy-ms-j10-c365/boot.log create mode 100644 service/internal/boards/contributions/anjoy-ms-j10-c365/uboot.txt create mode 100644 service/internal/boards/contributions_test.go diff --git a/data/locales/boards.en.yml b/data/locales/boards.en.yml index 0c6217d9..9ba7e5f6 100644 --- a/data/locales/boards.en.yml +++ b/data/locales/boards.en.yml @@ -373,4 +373,5 @@ en: new_tab: opens in a new tab files_title: Photos and files from_source: 'From {source}' + sent_by: 'Sent by {who}' known_sources: 'Sources: ' diff --git a/data/locales/boards.ru.yml b/data/locales/boards.ru.yml index a4e52bd3..e1425362 100644 --- a/data/locales/boards.ru.yml +++ b/data/locales/boards.ru.yml @@ -414,4 +414,5 @@ ru: new_tab: откроется в новой вкладке files_title: Фото и файлы from_source: 'Источник: {source}' + sent_by: 'Прислано: {who}' known_sources: 'Источники: ' diff --git a/data/locales/boards.zh.yml b/data/locales/boards.zh.yml index d470826b..0a31f90b 100644 --- a/data/locales/boards.zh.yml +++ b/data/locales/boards.zh.yml @@ -376,4 +376,5 @@ zh: new_tab: 在新标签页中打开 files_title: 照片与文件 from_source: '来源:{source}' + sent_by: '{who} 提供' known_sources: '来源:' diff --git a/frontend/apps/site/src/components/boards/BoardPanel.tsx b/frontend/apps/site/src/components/boards/BoardPanel.tsx index 3968119b..f38fb86e 100644 --- a/frontend/apps/site/src/components/boards/BoardPanel.tsx +++ b/frontend/apps/site/src/components/boards/BoardPanel.tsx @@ -16,7 +16,7 @@ import Firmware from './Firmware'; import ModelFirmware from './ModelFirmware'; import OwnerReports from '../reports/OwnerReports'; import { - HEADING_CLASS, couplerDevices, foundIn, frontPhoto, insideOf, firstMissing, formatBytes, heading, linkCodes, lines, paragraphs, printedCode, subtitle, unitFiles, unitPhotos, + HEADING_CLASS, couplerDevices, foundIn, frontPhoto, insideOf, firstMissing, formatBytes, heading, linkCodes, lines, paragraphs, printedCode, sentBy, subtitle, unitFiles, unitPhotos, type CodeIndex, type Entry, type Heading, type Inside, } from '../../lib/boards/model'; import type { BoardsT } from '../../lib/boards-i18n'; @@ -224,10 +224,18 @@ export default function BoardPanel({ id, entry, all, loaded, locale, t, sources, {entry.units.map((u) => { const photos = unitPhotos(u.files); const files = unitFiles(u.files); + const sent = sentBy(u); return (
- {t('from_source', { source: sourceName(u.source) })} + {sent + ? ( + + {t('sent_by', { who: sent.who })} + {sent.url && <> · {sent.label} } + + ) + : {t('from_source', { source: sourceName(u.source) })}} {u.sensor && {t('sensor')}: {u.sensor}} {(u.flash_chip || u.flash_size_mb) && ( {t('flash')}: diff --git a/frontend/apps/site/src/i18n/boards.en.json b/frontend/apps/site/src/i18n/boards.en.json index 28fa441b..65204cf9 100644 --- a/frontend/apps/site/src/i18n/boards.en.json +++ b/frontend/apps/site/src/i18n/boards.en.json @@ -285,6 +285,7 @@ "send_pinout": "Send us the pinout", "send_uboot_env": "Send us the U-Boot console", "sensor": "Sensor", + "sent_by": "Sent by {who}", "shared_and": " and ", "shared_colon": ": ", "shared_count": { diff --git a/frontend/apps/site/src/i18n/boards.ru.json b/frontend/apps/site/src/i18n/boards.ru.json index 9af882b5..5c26c7cd 100644 --- a/frontend/apps/site/src/i18n/boards.ru.json +++ b/frontend/apps/site/src/i18n/boards.ru.json @@ -307,6 +307,7 @@ "send_pinout": "Пришлите нам распиновку", "send_uboot_env": "Пришлите нам консоль U-Boot", "sensor": "Сенсор", + "sent_by": "Прислано: {who}", "shared_and": " и ", "shared_colon": ": ", "shared_count": { diff --git a/frontend/apps/site/src/i18n/boards.zh.json b/frontend/apps/site/src/i18n/boards.zh.json index 64504b87..7c44653c 100644 --- a/frontend/apps/site/src/i18n/boards.zh.json +++ b/frontend/apps/site/src/i18n/boards.zh.json @@ -285,6 +285,7 @@ "send_pinout": "把引脚图发给我们", "send_uboot_env": "把 U-Boot 控制台输出发给我们", "sensor": "传感器", + "sent_by": "{who} 提供", "shared_and": "和", "shared_colon": ":", "shared_count": { diff --git a/frontend/apps/site/src/lib/boards/boards.test.ts b/frontend/apps/site/src/lib/boards/boards.test.ts index 51c997ad..c2175be6 100644 --- a/frontend/apps/site/src/lib/boards/boards.test.ts +++ b/frontend/apps/site/src/lib/boards/boards.test.ts @@ -11,7 +11,7 @@ import zh from '../../i18n/boards.zh.json'; import { KNOWN_LINES, lineLabel, addsIPeye, buildGroups, bySeller, cardFiles, formatDay, foundIn, insideOf, cardPhotos, codeIndex, couplerDevices, deviceIdOf, entries, kindOf, tally, filterBoards, filterHits, heading, matchBoards, newestFirst, printedCode, firstMissing, flashOf, formatBytes, frontPhoto, - highlight, layout, lead, lineOptions, linkCodes, lines, normaliseCode, ownPhoto, paragraphs, sensorKey, sensorOptions, slug, + highlight, layout, lead, lineOptions, linkCodes, lines, normaliseCode, ownPhoto, paragraphs, sensorKey, sensorOptions, sentBy, slug, socKey, socOptions, stats, subtitle, unitFiles, unitPhotos, } from './model'; import { EMPTY, readQueryString, writeQueryString } from './url'; @@ -377,6 +377,22 @@ describe('stats', () => { }); }); +describe('who sent a unit', () => { + const unit = (source: string, source_ref: string, contributed_by: string) => ({ source, source_ref, contributed_by }); + + test('an owner, credited with the issue they sent it in', () => { + expect(sentBy(unit('contributor', 'https://github.com/OpenIPC/website/issues/365', 'sansarus'))) + .toEqual({ who: 'sansarus', url: 'https://github.com/OpenIPC/website/issues/365', label: '#365' }); + expect(sentBy(unit('contributor', 'https://github.com/OpenIPC/firmware/issues/12', 'x'))?.label).toBe('firmware#12'); + expect(sentBy(unit('contributor', 'https://example.org/post/1', 'x'))?.label).toBe('example.org'); + expect(sentBy(unit('contributor', 'not a url', 'x'))).toEqual({ who: 'x', url: '', label: '' }); + }); + + test('nobody, for a unit a catalogue brought', () => { + expect(sentBy(unit('openhisiipcam', 'https://github.com/OpenHisiIpCam', 'OpenHisiIpCam'))).toBeNull(); + }); +}); + describe('a card', () => { const m = model('e', {}, null, [ photo('photo_other', 'o.jpg'), photo('photo_front', 'f1.jpg'), photo('photo_front', 'f2.jpg'), diff --git a/frontend/apps/site/src/lib/boards/model.ts b/frontend/apps/site/src/lib/boards/model.ts index c1fe6859..6497a7ed 100644 --- a/frontend/apps/site/src/lib/boards/model.ts +++ b/frontend/apps/site/src/lib/boards/model.ts @@ -5,7 +5,7 @@ * a description that link to other boards. Pure functions, so they are * tested without a browser. */ -import type { BoardFile, BoardsFile, Content, Hit, Manufacturer, Model, ModelBuild, VendorFirmware } from './types'; +import type { BoardFile, BoardsFile, Content, Hit, Manufacturer, Model, ModelBuild, Unit, VendorFirmware } from './types'; import type { BoardsState, Missing } from './url'; /** A board model with the manufacturer it is filed under. */ @@ -58,6 +58,25 @@ export function firstMissing(m: Model): CoverageKey | null { return ASK.find((k) => !(k === 'photos' ? ownPhoto(m) : has(m, k))) ?? null; } +/** + * Who sent a unit and where, when an owner did (source contributor): the + * sender as they signed, and the issue as "#365" (another repository's as + * "firmware#12"), any other page as its host. Null for a unit a catalogue + * brought. + */ +export function sentBy(u: Pick): { who: string; url: string; label: string } | null { + if (u.source !== 'contributor' || !u.contributed_by) return null; + let url: URL; + try { + url = new URL(u.source_ref); + } catch { + return { who: u.contributed_by, url: '', label: '' }; + } + const issue = url.host === 'github.com' ? url.pathname.match(/^\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)$/) : null; + const label = !issue ? url.host : `${issue[1] === 'OpenIPC' && issue[2] === 'website' ? '' : issue[2]}#${issue[3]}`; + return { who: u.contributed_by, url: u.source_ref, label }; +} + /** * The key a sensor is filtered by: the part without its maker, upper-cased, * so "SONY IMX323" and "Sony imx323" are one sensor. diff --git a/service/cmd/openipc/boards.go b/service/cmd/openipc/boards.go index 3cc1e0fc..b73c6b8a 100644 --- a/service/cmd/openipc/boards.go +++ b/service/cmd/openipc/boards.go @@ -104,8 +104,32 @@ func importSnapshot(ctx context.Context, cfg *config.Config, log *slog.Logger, a return err } log.Info("boards: snapshot imported", "source", *source, "new_models", n) - // A confirmation waiting for a device this snapshot brought applies now. - return applyConfirmations(ctx, pool, log) + // A confirmation or a contribution waiting for a model this snapshot + // brought applies now. + if err := applyConfirmations(ctx, pool, log); err != nil { + return err + } + return applyContributions(ctx, pool, log, cfg.BoardsRoot) +} + +// applyContributions publishes contributions.yml: what owners sent about +// their own boards, as units credited to them. An entry whose model the +// catalogue does not have yet waits, reported, for the import that brings it. +func applyContributions(ctx context.Context, pool *pgxpool.Pool, log *slog.Logger, root string) error { + list, err := boards.Contributions() + if err != nil { + return err + } + im := &boards.Importer{Pool: pool, Log: log, Root: root} + missing, err := im.ApplyContributions(ctx, list) + if err != nil { + return fmt.Errorf("boards: contributions.yml: %w", err) + } + for _, m := range missing { + log.Warn("boards: contributions.yml names a model the catalogue does not have", "model", m) + } + log.Info("boards: contributions applied", "units", len(list)-len(missing)) + return nil } // applyConfirmations publishes contents.yml: what owners found inside the diff --git a/service/cmd/openipc/main.go b/service/cmd/openipc/main.go index 8898c203..181edf8c 100644 --- a/service/cmd/openipc/main.go +++ b/service/cmd/openipc/main.go @@ -320,6 +320,12 @@ func web(ctx context.Context, cfg *config.Config, log *slog.Logger, pool *pgxpoo lock.Release() return nil, err } + // What owners sent about their own boards (boards/contributions.yml), + // the same way. + if err := applyContributions(ctx, pool, log, cfg.BoardsRoot); err != nil { + lock.Release() + return nil, err + } store := &snapshots.Store{DB: pool, TokenKey: cfg.CameraTokenKey} wallFS := variants.Wall{Root: cfg.WallRoot} diff --git a/service/internal/boards/contributions.go b/service/internal/boards/contributions.go new file mode 100644 index 00000000..b3c38a65 --- /dev/null +++ b/service/internal/boards/contributions.go @@ -0,0 +1,279 @@ +package boards + +import ( + "context" + "crypto/sha256" + "embed" + "encoding/hex" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + "slices" + + "github.com/jackc/pgx/v5" + "go.yaml.in/yaml/v3" +) + +// What owners sent about their own boards (contributions.yml): a boot log or +// a U-Boot console pasted into an issue, reviewed in the repository and +// published as a unit of the model, source contributor, credited to them. + +// Contributors is the source of contributed units (migration 003's enum). +const Contributors = "contributor" + +//go:embed contributions.yml contributions +var contributionsFS embed.FS + +// Contribution is one entry of contributions.yml. +type Contribution struct { + Unit string `yaml:"unit"` + Model string `yaml:"model"` + By string `yaml:"by"` + Evidence []string `yaml:"evidence"` + Sensor string `yaml:"sensor"` + FlashChip string `yaml:"flash_chip"` + FlashMB int `yaml:"flash_mb"` + Note string `yaml:"note"` + Files []struct { + Kind string `yaml:"kind"` + File string `yaml:"file"` + } `yaml:"files"` +} + +// contributedKinds are what an owner's paste can be. A flash dump is not +// one: it is an owner report's backup, never a file in the repository. +var contributedKinds = map[string]bool{ + "uboot_env": true, "boot_log": true, "note": true, + "photo_front": true, "photo_back": true, "photo_other": true, "pinout": true, +} + +var ( + unitShape = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,159}$`) + fileShape = regexp.MustCompile(`^[A-Za-z0-9._-]+$`) +) + +// Contributions are the entries of contributions.yml, checked for shape, +// with every file they name present. +func Contributions() ([]Contribution, error) { + return parseContributions(contributionsFS) +} + +func parseContributions(fsys fs.FS) ([]Contribution, error) { + b, err := fs.ReadFile(fsys, "contributions.yml") + if err != nil { + return nil, err + } + var list []Contribution + if err := yaml.Unmarshal(b, &list); err != nil { + return nil, fmt.Errorf("contributions.yml: %w", err) + } + units, refs := map[string]bool{}, map[string]bool{} + for i, c := range list { + where := fmt.Sprintf("contributions.yml entry %d (%s)", i+1, c.Unit) + if !unitShape.MatchString(c.Unit) || c.Model == "" || c.By == "" || len(c.Evidence) == 0 || len(c.Files) == 0 { + return nil, fmt.Errorf("%s: needs unit, model, by, evidence and files", where) + } + if units[c.Unit] || refs[c.Evidence[0]] { + return nil, fmt.Errorf("%s: the unit or its first evidence is listed twice", where) + } + units[c.Unit], refs[c.Evidence[0]] = true, true + for _, e := range c.Evidence { + if !httpURL(e) { + return nil, fmt.Errorf("%s: evidence %q is not a web page", where, e) + } + } + if c.FlashMB < 0 { + return nil, fmt.Errorf("%s: flash_mb %d", where, c.FlashMB) + } + names := map[string]bool{} + for _, f := range c.Files { + if !contributedKinds[f.Kind] { + return nil, fmt.Errorf("%s: kind %q cannot be contributed", where, f.Kind) + } + if !fileShape.MatchString(f.File) || names[f.File] { + return nil, fmt.Errorf("%s: file name %q", where, f.File) + } + names[f.File] = true + if _, err := fs.Stat(fsys, "contributions/"+c.Unit+"/"+f.File); err != nil { + return nil, fmt.Errorf("%s: %w", where, err) + } + } + } + return list, nil +} + +// ApplyContributions makes the contributed units exactly what list says. +// An unchanged unit is left alone, a changed one is replaced, and one no +// longer listed is removed with its files. A model the catalogue does not +// have is reported and its entry waits. +func (im *Importer) ApplyContributions(ctx context.Context, list []Contribution) (missing []string, err error) { + return im.applyContributions(ctx, contributionsFS, list) +} + +func (im *Importer) applyContributions(ctx context.Context, fsys fs.FS, list []Contribution) (missing []string, err error) { + var keep []string + for i, c := range list { + var exists bool + if err := im.Pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM board_models WHERE id = $1)`, c.Model).Scan(&exists); err != nil { + return nil, err + } + if !exists { + missing = append(missing, c.Model) + continue + } + keep = append(keep, c.Unit) + u := &Unit{ID: c.Unit, Sensor: c.Sensor, FlashChip: c.FlashChip, FlashSizeMB: c.FlashMB, + SourceRef: c.Evidence[0], Position: 1000 + i, Source: Contributors, ContributedBy: c.By} + for _, f := range c.Files { + u.Files = append(u.Files, File{Kind: f.Kind, Name: f.File, Source: "contributions/" + c.Unit + "/" + f.File}) + } + same, err := im.unchanged(ctx, fsys, c, u) + if err != nil { + return nil, fmt.Errorf("%s: %w", c.Unit, err) + } + if same { + continue + } + // Checked before the directory is touched: it may be another + // source's unit. + var other string + err = im.Pool.QueryRow(ctx, `SELECT source::text FROM board_units WHERE id = $1`, c.Unit).Scan(&other) + if err == nil && other != Contributors { + return nil, fmt.Errorf("%s: the unit id is already taken by %s", c.Unit, other) + } + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return nil, err + } + if err := os.RemoveAll(filepath.Join(im.Root, c.Unit)); err != nil { + return nil, err + } + arts, err := im.files(fsys, u) + if err != nil { + return nil, fmt.Errorf("%s: %w", c.Unit, err) + } + if err := pgx.BeginFunc(ctx, im.Pool, func(tx pgx.Tx) error { + if _, err := tx.Exec(ctx, `DELETE FROM board_units WHERE source = $1 AND (id = $2 OR source_ref = $3)`, + Contributors, c.Unit, u.SourceRef); err != nil { + return err + } + var flash *int + if c.FlashMB > 0 { + flash = &c.FlashMB + } + if _, err := tx.Exec(ctx, ` + INSERT INTO board_units (id, model_id, sensor, flash_chip, flash_size_mb, source, source_ref, contributed_by, notes, position) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, + u.ID, c.Model, null(c.Sensor), null(c.FlashChip), flash, Contributors, u.SourceRef, c.By, null(c.Note), u.Position); err != nil { + return err + } + return insertArtifacts(ctx, tx, u.ID, arts) + }); err != nil { + return nil, fmt.Errorf("%s: %w", c.Unit, err) + } + im.Log.Info("boards: contribution applied", "unit", c.Unit, "files", len(arts)) + } + + if keep == nil { + keep = []string{} + } + err = pgx.BeginFunc(ctx, im.Pool, func(tx pgx.Tx) error { + rows, err := tx.Query(ctx, `DELETE FROM board_units WHERE source = $1 AND NOT id = ANY($2) RETURNING id`, Contributors, keep) + if err != nil { + return err + } + gone, err := pgx.CollectRows(rows, pgx.RowTo[string]) + if err != nil { + return err + } + for _, id := range gone { + if err := os.RemoveAll(filepath.Join(im.Root, id)); err != nil { + return err + } + im.Log.Info("boards: contribution removed", "unit", id) + } + // The source is listed only while it has a unit. + if len(keep) == 0 { + _, err = tx.Exec(ctx, `DELETE FROM board_sources WHERE id = $1`, Contributors) + return err + } + _, err = tx.Exec(ctx, ` + INSERT INTO board_sources (id, name, url, note, ref, position) + VALUES ($1, 'Board owners', 'https://github.com/OpenIPC/website/issues', + 'What owners sent about their own boards: boot logs and U-Boot consoles, each reviewed and credited to them.', '', + (SELECT coalesce(max(position), 0) + 1 FROM board_sources)) + ON CONFLICT (id) DO NOTHING`, Contributors) + return err + }) + return missing, err +} + +// unchanged says whether the unit is stored exactly as the entry describes +// it: the same fields, and the same files in the same order, byte for byte. +func (im *Importer) unchanged(ctx context.Context, fsys fs.FS, c Contribution, u *Unit) (bool, error) { + var model, ref, by string + var sensor, chip, note *string + var flash *int + err := im.Pool.QueryRow(ctx, ` + SELECT model_id, source_ref, coalesce(contributed_by, ''), sensor, flash_chip, flash_size_mb, notes + FROM board_units WHERE id = $1 AND source = $2`, c.Unit, Contributors). + Scan(&model, &ref, &by, &sensor, &chip, &flash, ¬e) + if errors.Is(err, pgx.ErrNoRows) { + return false, nil + } + if err != nil { + return false, err + } + if model != c.Model || ref != u.SourceRef || by != c.By || deref(sensor) != c.Sensor || + deref(chip) != c.FlashChip || deref(note) != c.Note || derefInt(flash) != c.FlashMB { + return false, nil + } + rows, err := im.Pool.Query(ctx, `SELECT kind::text || ' ' || name || ' ' || sha256 FROM board_artifacts WHERE unit_id = $1 ORDER BY position`, c.Unit) + if err != nil { + return false, err + } + stored, err := pgx.CollectRows(rows, pgx.RowTo[string]) + if err != nil { + return false, err + } + var want []string + for _, f := range u.Files { + b, err := fs.ReadFile(fsys, f.Source) + if err != nil { + return false, err + } + want = append(want, f.Kind+" "+f.Name+" "+sha256Hex(b)) + } + if !slices.Equal(stored, want) { + return false, nil + } + // The rows are right; the files must be there too (a restored database + // on an empty BOARDS_ROOT). + for _, f := range u.Files { + if _, err := os.Stat(filepath.Join(im.Root, c.Unit, f.Name)); err != nil { + return false, nil + } + } + return true, nil +} + +func deref(s *string) string { + if s == nil { + return "" + } + return *s +} + +func derefInt(n *int) int { + if n == nil { + return 0 + } + return *n +} + +func sha256Hex(b []byte) string { + sum := sha256.Sum256(b) + return hex.EncodeToString(sum[:]) +} diff --git a/service/internal/boards/contributions.yml b/service/internal/boards/contributions.yml new file mode 100644 index 00000000..b657e6cb --- /dev/null +++ b/service/internal/boards/contributions.yml @@ -0,0 +1,31 @@ +# What board owners sent about their own boards -- boot logs and U-Boot +# consoles, pasted into an issue -- each checked by a person and recorded +# here, the text under contributions//. The web role applies the file +# whenever it starts: each entry is one unit on an existing model, credited +# to whoever sent it, and the catalogue's contributed units are exactly +# these. A changed entry replaces its unit; a removed one is removed. +# +# unit: the unit's id, -c +# model: the catalogue's model id; a contribution never creates one +# by: who sent it, as they signed the issue +# evidence: where they sent it; the first is the unit's reference +# sensor, flash_chip, flash_mb: what the text itself shows, when it does +# note: anything a reader needs to read the files right +# files: kind (boot_log, uboot_env, note, or a photo kind) and the file +# under contributions// + +- unit: anjoy-ms-j10-c365 + model: anjoy-ms-j10 + by: sansarus + evidence: + - https://github.com/OpenIPC/website/issues/365 + - https://github.com/OpenIPC/website/issues/366 + sensor: IMX307 + flash_chip: GD25Q64 + flash_mb: 8 + note: Stock firmware 3.3.0.2 (builddate 202312281015). The kernel calls the SoC INFINITY6B0 SSC009A-S01A, Anjoy calls it SSC335. + files: + - kind: uboot_env + file: uboot.txt + - kind: boot_log + file: boot.log diff --git a/service/internal/boards/contributions/anjoy-ms-j10-c365/boot.log b/service/internal/boards/contributions/anjoy-ms-j10-c365/boot.log new file mode 100644 index 00000000..2e4ee2f2 --- /dev/null +++ b/service/internal/boards/contributions/anjoy-ms-j10-c365/boot.log @@ -0,0 +1,527 @@ +IPL g05c44be +D-15 +SPI 54M +64MB +BIST0_0001-OK +[SPI_NOR] +MXP found at 0x0000f000 +offset:00007000 +Checksum OK + +IPL_CUST g05c44be +runUBOOT() +[SPI_NOR] +MXP found at 0x0000f000 +offset:00020000 + -Decompress BDMA XZ + -Decompress BDMA XZ +XZ decomp_size=0x00049be4 + pvLoadAddr:23d00000 + pvRunAddr:23e00000 + -Verify CRC32 passed! + decomp_size=0x00000000 +Disable MMU and D-cache before jump to UBOOT▒ + +U-Boot 2015.01 (Nov 24 2022 - 17:53:59) + +Version: I6g####### +I2C: ready +DRAM: +WARNING: Caches not enabled +MMC: MStar SD/MMC: 0 +nor_flash_mxp allocated success!! +Flash is detected (0x0B03, 0xC8, 0x40, 0x17) +SF: Detected nor0 with total size 8 MiB +MXP found at mxp_offset[2]=0x0000F000, size=0x1000 +env_offset=0x3F000 env_size=0x1000 +Flash is detected (0x0B03, 0xC8, 0x40, 0x17) +SF: Detected nor0 with total size 8 MiB +In: serial +Out: serial +Err: serial +Net: MAC Address 00:00:B4:67:B7:76 +Auto-Negotiation... +Link Status Speed:100 Full-duplex:1 +sstar_emac +reset key up !!! +Init wifi gpio +run sdstar +_[sdmmc_0] Card Detect Fail! +** Bad device mmc 0 ** +aj_update_by_sdcard(dev=0, partion=0, ret=0) +_[sdmmc_0] Card Detect Fail! +** Bad device mmc 0 ** +aj_update_by_sdcard(dev=0, partion=1, ret=0) +MMC Device 1 not found +** Bad device mmc 1 ** +aj_update_by_sdcard(dev=1, partion=0, ret=0) +MMC Device 1 not found +** Bad device mmc 1 ** +aj_update_by_sdcard(dev=1, partion=1, ret=0) +run udpbc +uuid 0x850e 0xcf26 0x88a7 +uuid 0x850e 0xcf26 0x88a7 150254316324110 +Flash is detected (0x0B03, 0xC8, 0x40, 0x17) +SF: Detected nor0 with total size 8 MiB +SPI flash sector read 64 return 0 +anjvision_autoupdate::MC200J6_V0 +uuid = 150254316324110 +setenv ipaddr 192.168.1.167 --> return 0 +macaddr 86 0e cf 26 88 a7 +MAC Address 86:0E:CF:26:88:A7 +MSJ10_V1: Send upgrade request, ready to receive... +uboot net upgrade timeout +Flash is detected (0x0B03, 0xC8, 0x40, 0x17) +SF: Detected nor0 with total size 8 MiB +SF: 1835008 bytes @ 0x40000 Read: OK +## Booting kernel from Legacy Image at 21000000 ... + Image Name: MVX4##I6B0g#######KL_LX409##[BR: + Image Type: ARM Linux Kernel Image (lzma compressed) + Data Size: 1597176 Bytes = 1.5 MiB + Load Address: 20008000 + Entry Point: 20008000 + Verifying Checksum ... OK + Uncompressing Kernel Image ... +[XZ] !!!reserved 0x21000000 length=0x 1000000 for xz!! + XZ: uncompressed size=0x33a000, ret=7 +OK +atags:0x20000000 + +Starting kernel ... + +Booting Linux on physical CPU 0x0 +Linux version 4.9.84 (root@ubuntu) (gcc version 4.9.4 (Buildroot 2017.08-g239d7d0) ) #117 PREEMPT Fri Feb 5 11:24:04 CST 2021 +CPU: ARMv7 Processor [410fc075] revision 5 (ARMv7), cr=50c53c7d +CPU: div instructions available: patching division code +CPU: PIPT / VIPT nonaliasing data cache, VIPT aliasing instruction cache +early_atags_to_fdt() success +OF: fdt:Machine model: INFINITY6B0 SSC009A-S01A QFN88 +[ERR] LX_MEM, LX_MEM2, LX_MEM3 not 1MB aligned +LXmem is 0x3fe0000 PHYS_OFFSET is 0x20000000 +Add mem start 0x20000000 size 0x3fe0000!!!! + +LX_MEM = 0x20000000, 0x3fe0000 +LX_MEM2 = 0x0, 0x0 +LX_MEM3 = 0x0, 0x0 +EMAC_LEN= 0x0 +DRAM_LEN= 0x0 +deal_with_reserve_mma_heap memblock_reserve success mma_config[0].reserved_start= +0x22ab4000 + +cma: Reserved 2 MiB at 0x22800000 +Memory policy: Data cache writeback +CPU: All CPU(s) started in SVC mode. +Built 1 zonelists in Zone order, mobility grouping on. Total pages: 10846 +Kernel command line: console=ttyS0,115200 root=/dev/mtdblock2 rootfstype=squashfs ro init=/linuxrc LX_MEM=0x3fe0000 mma_heap=mma_heap_name0,miu=0,sz=0x152C000 mma_memblock_remove=1 +PID hash table entries: 256 (order: -2, 1024 bytes) +Dentry cache hash table entries: 8192 (order: 3, 32768 bytes) +Inode-cache hash table entries: 4096 (order: 2, 16384 bytes) +Memory: 37620K/43728K available (1888K kernel code, 203K rwdata, 996K rodata, 96K init, 137K bss, 4060K reserved, 2048K cma-reserved) +Virtual kernel memory layout: + vector : 0xffff0000 - 0xffff1000 ( 4 kB) + fixmap : 0xffc00000 - 0xfff00000 (3072 kB) + vmalloc : 0xc3000000 - 0xff800000 ( 968 MB) + lowmem : 0xc0000000 - 0xc2ab4000 ( 42 MB) + modules : 0xbf800000 - 0xc0000000 ( 8 MB) + .text : 0xc0008000 - 0xc01e0470 (1890 kB) + .init : 0xc02f6000 - 0xc030e000 ( 96 kB) + .data : 0xc030e000 - 0xc0340f18 ( 204 kB) + .bss : 0xc0342000 - 0xc03646b4 ( 138 kB) +SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1 +Preemptible hierarchical RCU implementation. + Build-time adjustment of leaf fanout to 32. +NR_IRQS:16 nr_irqs:16 16 +ms_init_main_intc: np->name=ms_main_intc, parent=gic +ms_init_pm_intc: np->name=ms_pm_intc, parent=ms_main_intc +ss_init_gpi_intc: np->name=ms_gpi_intc, parent=ms_main_intc +Find CLK_cpupll_clk, hook ms_cpuclk_ops +arm_arch_timer: Architected cp15 timer(s) running at 6.00MHz (virt). +clocksource: arch_sys_counter: mask: 0xffffffffffffff max_cycles: 0x1623fa770, max_idle_ns: 440795202238 ns +sched_clock: 56 bits at 6MHz, resolution 166ns, wraps every 4398046511055ns +Switching to timer-based delay loop, resolution 166ns +console [ttyS0] enabled +Calibrating delay loop (skipped), value calculated using timer frequency.. 12.00 BogoMIPS (lpj=60000) +pid_max: default: 4096 minimum: 301 +Mount-cache hash table entries: 1024 (order: 0, 4096 bytes) +Mountpoint-cache hash table entries: 1024 (order: 0, 4096 bytes) +CPU: Testing write buffer coherency: ok +Setting up static identity map for 0x200081c0 - 0x200081f0 +devtmpfs: initialized +VFP support v0.3: implementor 41 architecture 2 part 30 variant 7 rev 5 +clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 19112604462750000 ns +futex hash table entries: 16 (order: -4, 448 bytes) +NET: Registered protocol family 16 +DMA: preallocated 256 KiB pool for atomic coherent allocations + + +Version : MVX4##I6B0g#######KL_LX409##[BR:g]#XVM + +GPIO: probe end[ss_gpi_intc_domain_alloc] hw:42 -> v:49 +[MS_PM_INTC] hw:20 -> v:52 +hw-breakpoint: found 5 (+1 reserved) breakpoint and 4 watchpoint registers. +hw-breakpoint: maximum watchpoint size is 8 bytes. +clocksource: Switched to clocksource arch_sys_counter +NET: Registered protocol family 2 +TCP established hash table entries: 1024 (order: 0, 4096 bytes) +TCP bind hash table entries: 1024 (order: 2, 20480 bytes) +TCP: Hash tables configured (established 1024 bind 1024) +UDP hash table entries: 128 (order: 0, 6144 bytes) +UDP-Lite hash table entries: 128 (order: 0, 6144 bytes) +NET: Registered protocol family 1 +hw perfevents: enabled with armv7_cortex_a7 PMU driver, 5 counters available +workingset: timestamp_bits=30 max_order=14 bucket_order=0 +squashfs: version 4.0 (2009/01/31) Phillip Lougher +jffs2: version 2.2. © 2001-2006 Red Hat, Inc. +io scheduler noop registered +io scheduler deadline registered (default) +[Padmux]reset PAD61(reg 0xe00:1c; mask0x10) t0 GPIO (org: PM_IRIN_MODE) +[Padmux]reset PAD46(reg 0x101e00:f; mask0x3) t0 GPIO (org: EJ_MODE_2) +335 set wifi to low +335 set wifi to high +IRLED driver v2.0 2021025 +libphy: Fixed MDIO Bus: probed +i2c /dev entries driver +1f221000.uart0: ttyS0 at MMIO 0x0 (irq = 37, base_baud = 10800000) is a unknown +1f221200.uart1: ttyS1 at MMIO 0x0 (irq = 38, base_baud = 10800000) is a unknown +1f220400.uart2: ttyS2 at MMIO 0x0 (irq = 39, base_baud = 10800000) is a unknown +>> [sdmmc] ms_sdmmc_probe +[Padmux]reset PAD60(reg 0xe00:28; mask0x4000) t0 GPIO (org: PM_SD_CDZ_MODE) +[Padmux]reset PAD54(reg 0x101e00:8; mask0xc) t0 GPIO (org: SD_MODE) +>> [sdmmc_0] Probe Platform Devices +MSYS: DMEM request: [emac0_buff]:0x00000812 +MSYS: DMEM request: [emac0_buff]:0x00000812 success, CPU phy:@0x22843000, virt:@0xC2843000 +libphy: mdio: probed +mdio_bus mdio-bus@emac0: /soc/emac0/mdio-bus/ethernet-phy@0 has invalid PHY address +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 0 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 1 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 2 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 3 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 4 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 5 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 6 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 7 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 8 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 9 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 10 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 11 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 12 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 13 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 14 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 15 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 16 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 17 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 18 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 19 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 20 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 21 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 22 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 23 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 24 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 25 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 26 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 27 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 28 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 29 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 30 +mdio_bus mdio-bus@emac0: scan phy ethernet-phy at address 31 +[emac_phy_connect][3306] connected mac emac0 to PHY at mdio-bus@emac0:00 [uid=11112222, driver=SStar 10/100 Ethernet Phy] +ms_rtcpwc 1f006800.rtcpwc: rtc core: registered 1f006800.rtcpwc as rtc0 +[gpioi2c] sda-gpio=8, scl-gpio=9 +MSYS: DMEM request: [BDMA_FSP_WBUFF]:0x00010040 +MSYS: DMEM request: [BDMA_FSP_WBUFF]:0x00010040 success, CPU phy:@0x22850000, virt:@0xC2850000 +[Ser flash] phys=0x22850000, virt=0xc2850000, bus=0x02850000 len:0x10040 +[FSP] Flash is detected (0x0B03, 0xC8, 0x40, 0x17) ver1.1 +[FSP] 1-1-4 QUAD_READ MODE +mtd .name = NOR_FLASH, .size = 0x00800000 (8MiB) + .erasesize = 0x00010000 .numeraseregions = 0 +MXP_PARTS!! +MXP found at mxp_offset[2]=0x0000F000, size=0x1000 +Creating 5 MTD partitions on "NOR_FLASH": +0x000000000000-0x000000020000 : "BOOT" +0x000000040000-0x0000001d0000 : "KERNEL" +0x0000001d0000-0x0000007b0000 : "SYSTEM" +0x000000020000-0x000000040000 : "UBOOT" +0x0000007b0000-0x000000800000 : "DATA" +MSYS: DMEM request: [AESDMA_ENG]:0x00001000 +MSYS: DMEM request: [AESDMA_ENG]:0x00001000 success, CPU phy:@0x22844000, virt:@0xC2844000 +MSYS: DMEM request: [AESDMA_ENG1]:0x00001000 +MSYS: DMEM request: [AESDMA_ENG1]:0x00001000 success, CPU phy:@0x22845000, virt:@0xC2845000 +[ms_cpufreq_init] Current clk=799999872 +[NOTICE]pwm-isr(53) success. If not i6e or i6b0, pls confirm it on .dtsi +NET: Registered protocol family 17 +ThumbEE CPU extension supported. +Please set rtc timer (hwclock -w) +ms_rtcpwc 1f006800.rtcpwc: setting system clock to 1970-01-01 00:00:00 UTC (0) +OF: fdt:not creating '/sys/firmware/fdt': CRC check failed +VFS: Mounted root (squashfs filesystem) readonly on device 31:2. +devtmpfs: mounted +This architecture does not have kernel memory protection. +random: linuxrc: uninitialized urandom read (4 bytes read) +random: rcS: uninitialized urandom read (4 bytes read) +random: mount: uninitialized urandom read (4 bytes read) +random: mount: uninitialized urandom read (4 bytes read) +random: busybox: uninitialized urandom read (4 bytes read) +random: cat: uninitialized urandom read (4 bytes read) +SSC009A-S01A +random: mount: uninitialized urandom read (4 bytes read) +random: mkdir: uninitialized urandom read (4 bytes read) +random: mkdir: uninitialized urandom read (4 bytes read) +random: mount: uninitialized urandom read (4 bytes read) +mount: can't find devpts in /etc/fstab +umount: can't unmount /mnt/usb: Invalid argument +rmmod: can't unload 'mhal': unknown symbol in module, or unknown parameter +rmmod: can't unload 'mi_common': unknown symbol in module, or unknown parameter +rmmod: can't unload 'mi_sys': unknown symbol in module, or unknown parameter +rmmod: can't unload 'mi_ai': unknown symbol in module, or unknown parameter +rmmod: can't unload 'mi_ao': unknown symbol in module, or unknown parameter +rmmod: can't unload 'mi_rgn': unknown symbol in module, or unknown parameter +rmmod: can't unload 'mi_divp': unknown symbol in module, or unknown parameter +rmmod: can't unload 'mi_ipu': unknown symbol in module, or unknown parameter +rmmod: can't unload 'mi_vpe': unknown symbol in module, or unknown parameter +rmmod: can't unload 'mi_sensor': unknown symbol in module, or unknown parameter +rmmod: can't unload 'mi_vif': unknown symbol in module, or unknown parameter +rmmod: can't unload 'mi_venc': unknown symbol in module, or unknown parameter +rmmod: can't unload 'mi_shadow': unknown symbol in module, or unknown parameter +rmmod: can't unload 'imx307_MIPI': unknown symbol in module, or unknown parameter +mhal: loading out-of-tree module taints kernel. +mhal: module license 'PROPRIETARY' taints kernel. +Disabling lock debugging due to kernel taint +mhal driver init +[CSI] probe +Request CSI IRQ[0]#32 +CSI interrupt registered +vif driver probe +Create device file. vif_ints,0 +venc_probe:859 venc driver is probed. +jpe driver probed +[DRV_SCL_MODULE] [_DrvSclVpeModuleInit @ 140] +[Isp_Driver_Init] +[s32CurClkIdx] = 5 +[ISP] Request IRQ: 31, 57 +[IspMid_Driver_Init] +ispsclttl:0 +DivpProcInit 526 +[emac_phy_link_adjust] EMAC Link Up +module [sys] init +MI_SYSCFG_SetupMmapLoader default_config_path:/config/config_tool, argv1:/config/load_mmap,argv2:/config/mmap.ini +Function = init_glob_miu_kranges, Line = 654, Insert KProtect for LX @ MIU: 0 +Function = init_glob_miu_kranges, Line = 663, [INIT] for LX0 kprotect: from 0x20000000 to 0x23FE0000, using block 0 +function:parese_Cmdline,pCmd_Section:0x3fe0000 +mm +a_ +he +ap +_n +am +e0 + miu=0,sz=152c000 reserved_start=22ab4000 +r_front->miuBlockIndex:0,r_front->start_cpu_bus_pa:0x20000000,r_front->start_cpu_bus_pa+r_front->length:0x22ab4000 +mi_sys_mma_allocator_create success, heap_base_addr=22ab4000 length=152c000 +Kernel CONFIG_HZ = 100 +Sigmastar Module version: project_commit.e54754f sdk_commit.a45f135 build_time.20200319174625 +mknod: /dev/mi_sys: File exists +module [ai] init +mknod: /dev/mi_ai: File exists +module [ao] init +mknod: /dev/mi_ao: File exists +module [rgn] init +mknod: /dev/mi_rgn: File exists +module [divp] init +mknod: /dev/mi_divp: File exists +insmod: can't insert '/config/modules/4.9.84/mi_ipu.ko': No such file or directory +module [vpe] init +mknod: /dev/mi_vpe: File exists +module [sensor] init +mknod: /dev/mi_sensor: File exists +module [vif] init +mknod: /dev/mi_vif: File exists +module [venc] init Apr 15 2020 15:09:14 +mknod: /dev/mi_venc: File exists +module [shadow] init +mknod: /dev/mi_shadow: File exists +exFAT: file-system version 2.2.0-3arter97 +net.core.somaxconn = 1024 +net.core.rmem_default = 163840 +net.core.rmem_max = 163840 +net.core.wmem_default = 524288 +net.core.wmem_max = 1048576 +net.ipv4.tcp_mem = 924 1232 1848 +net.ipv4.tcp_rmem = 4096 87380 325120 +net.ipv4.tcp_wmem = 4096 131072 393216 +net.ipv4.tcp_tw_reuse = 1 +net.ipv4.tcp_fin_timeout = 30 + +(none) login: 7329 procman [main:1610] detected sensor type is 407 + +client [475] connected, module:sys +Get random encript data. +00 e5 d0 b4 67 b7 76 9f 4e 55 4c 4c 00 00 ff ff +procman SN: 00E5D0B467B7769F +7389 procman [redirect_print:1416] procman enable console + +/opt/ch/unicode_16x16.font: font count: 6915 + # # # # # # # + # # # # # # ######## + ############ # # #### ###### #### # ## ## ####### ###### # # # # # + # # # ######### ## ## ## ## ## ### ### ### ## ## ## ## ### ############ # # # + # # # # # ## ## ## ## # ## ## ####### ## # ## ## ## ## # # ####### # # + ###### ###### # ## # # ## ## ## ## ## ## ####### ## # ## ## ## ## ####### # # # # ###### ###### + # # # ############ ## ##### ## ## ## ## # ## #### ##### ## ## # # # # # # # # + ############### # # # ## ## ## ####### ## ## ## # ## ## ####### # # # # ## # # # + ###### ###### # # # # ## ## ## ## ## ## ## ## ## ## ## ## ######## # # ## # # # ###### ###### + # # # # ## ## ## # ## ## ## ## ## # ## ## ## ## # # # # # # # ## ## # + random: fast init done + ## # # ######### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## # ## # # # # # + ## # # # #### #### #### ## ## ## ## ####### ### ## ## ## # # # # # # # + ## # # # # ## # # # # + # # # # # # # # # # # # # # # + ## ## ############ # # # # # # # #### + ## # # # ## # # +7618 procman [redirect_print:1448] procman disable console + +procman (475): /proc/505/oom_adj is deprecated, please use /proc/505/oom_score_adj instead. +updating oom_score_adj for 475 (procman) from 0 to -1000 because it shares mm with 505 (procman). Report if this is unexpected. +[ss_gpi_intc_domain_alloc] hw:26 -> v:61 +[ss_gpi_intc_domain_alloc] hw:27 -> v:62 +[ss_gpi_intc_domain_alloc] hw:29 -> v:64 +[MS_PM_INTC] hw:4 -> v:65 +[ss_gpi_intc_domain_alloc] hw:69 -> v:66 +Connect IMX307_HDR_init_driver linear to sensor pad 0 +CamOsMutexInit already inited, LR:0xBF82641B +Connect IMX307_HDR_init_driver SEF to vif sensor pad 0 +CamOsMutexInit already inited, LR:0xBF82641B +Connect IMX307_HDR_init_driver LEF to sensor pad 0 +client [505] connected, module:sys +updating oom_score_adj for 475 (procman) from -1000 to 0 because it shares mm with 580 (procman). Report if this is unexpected. +client [584] connected, module:sys +updating oom_score_adj for 475 (procman) from 0 to -1000 because it shares mm with 595 (procman). Report if this is unexpected. +client [603] connected, module:sys +updating oom_score_adj for 475 (procman) from -1000 to 0 because it shares mm with 708 (procman). Report if this is unexpected. +client [708] connected, module:sys +[MI WRN ]: _MI_SYS_IMPL_GetOutputPortInfo[2810]: not found [eModId 7,u32DevId 0, chnID=0, outputPortId=2] +[MI WRN ]: _MI_SYS_IMPL_GetOutputPortInfo[2810]: not found [eModId 12,u32DevId 0, chnID=1, outputPortId=0] + +[MI WRN ]: _MI_SYS_IMPL_GetOutputPortInfo[2810]: not found [eModId 12,u32DevId 0, chnID=0, outputPortId=0] +[MI WRN ]: _MI_SYS_IMPL_GetOutputPortInfo[2810]: not found [eModId 4,u32DevId 0, chnID=0, outputPortId=0] + +client [584] connected, module:sensor +client [584] connected, module:vif +vif channel 0, workmode 3, dataprec 2, FBCmode 0 +client [584] connected, module:rgn +client [584] connected, module:vpe +chipidx 4 +[DRV_SCL_MODULE] [DrvSclModuleClkInit @ 314] Use Default Clk [240000000] +CmdqProcInit 674 +[CMDQ0] Virtual IRQ: 30 +CmdQ Free ID = 0, IspLocalCmdQHnd = 0xbf8f6e08 +CamOsMutexInit already inited, LR:0xBF836481 +CameraIsp_EnableChannel3A, ch:0 data: (null) +CameraIsp_EnableCus3A AE=1, AWB=0, AF=0 +CameraIsp_EnableChannel3A, ch:0 data: (null) +CameraIsp_EnableCus3A AE=1, AWB=1, AF=0 +client [584] connected, module:divp +DIP Device ID [0] has already init. +[DIP] Virtual IRQ: 36 + +client [584] connected, module:venc +Create mi DEV0 +Create mi DEV0 +[MI WRN ]: _MI_VENC_CreateMiDevice[6870]: mi device:0 has been inited +Create mi DEV1 +Create mhal DEV0. +[ven-m][wrap] fw_path /config/venc_fw/chagall.bin +[CMDQ1] Virtual IRQ: 30 +ChData Size = 99168 + +===== [MhalCameraOpen] begin ===== VifMask : 3 +Ch= 0, SNR ID: 0, VifCh = 0, PreOpen = 0 +3DNR = 2, Rotation = 0, AckTie : 0 +Crop: (0, 0, 1920, 1080) +PixelFmt: 2, BayerID: 0, YuvOrder: 0 +Mode: 0, HdrType: 0 +In (w, h) = (1920, 1080), Crop (1920, 1080), ROT = 0 +[ISP_STATIS]Sz=0x27a80 , Vir=0xc34c0000, Miu=0x2b35000, C=1 == +ISP Mload base remap: 0xFD000000 + +Mload mload_handle Size = 104 + +Mload MLOAD_IQ_MEM Size = 304 + +~$[Mload]SIZE_OF_MLOAD_LAYOUT=11456 +[CameraReadIqData] isp root is /config/iqfile +[ISP_WDR]Sz=0x800 , Vir=0xc355e000, Miu=0x2b60000, C=1 == +[DNR_INFO1]Sz=0x21c00 , Vir=0xc3580000, Miu=0x2b61000, C=0 == +[DNR_INFO0]Sz=0x32a000 , Vir=0xc3800000, Miu=0x2b83000, C=0 == +[ISP_CMDQ]Sz=0xe000 , Vir=0xc3560000, Miu=0x2ead000, C=1 == +Create mhal DEV1. +[ven-w][wrap] venc MMA callback function duplicate registration Force OverWrite!! +Create mhal DEV2. +client [584] connected, module:ao +[_MI_AO_Init:1228] Init Ao Gain. +IRCUT control too fast from last time, modify timer +[CMDQ2] Virtual IRQ: 30 +client [584] connected, module:ai +[_MI_AI_Init:3098] Init Ai Gain. +[_MI_AI_IMPL_AllocTmpBuffer:3542] tmp buffer addr[c35e2000]. +[IQ_API20_SET_DUMMY_EX] Dummy0[0~5] = IQ_YEE_SCALE_SFT_X +[IQ_API20_SET_DUMMY_EX] Dummy1[0~2] = IQ_NR3D_STICKY_SOLVER_EN, IQ_NR3D_STICKY_SOLVER_TH, IQ_NR3D_STICKY_SOLVER_EN_TH +[IQ_API20_SET_DUMMY_EX] Dummy3 = IQ_NR3D_HIST_MOT_MAP_SW +[IQ_API20_SET_DUMMY_EX] Dummy4 = ALSC table mirror/flip/rotate +[IQ_API20_SET_DUMMY_EX] Dummy0[0~5] = IQ_YEE_SCALE_SFT_X +[IQ_API20_SET_DUMMY_EX] Dummy1[0~2] = IQ_NR3D_STICKY_SOLVER_EN, IQ_NR3D_STICKY_SOLVER_TH, IQ_NR3D_STICKY_SOLVER_EN_TH +[IQ_API20_SET_DUMMY_EX] Dummy3 = IQ_NR3D_HIST_MOT_MAP_SW +[IQ_API20_SET_DUMMY_EX] Dummy4 = ALSC table mirror/flip/rotate +[IQ_API20_SET_DUMMY] Dummy0[0~3] = AE acc controls 3DNR MdTh and MdGain. +[IQ_API20_SET_DUMMY] Dummy0[0] = AEDiff_Th. If max of AE acc difference smaller than the value, MdTh and MdGain would be adjusted by the following ratio. +[IQ_API20_SET_DUMMY] Dummy0[1] = MdTh_Ratio. +[IQ_API20_SET_DUMMY] Dummy0[2] = MdGain_Ratio. +[IQ_API20_SET_DUMMY] Dummy0[3] = Log_En. +[IQ_API20_SET_DUMMY] Dummy1[0] = IQ_BNR_WEI_X +[IQ_API20_SET_DUMMY] Dummy1[1~32] = IQ_BNR_WEI_Y +[IQ_API20_SET_DUMMY] Dummy2[0~15] = IQ_YEE_DTL_OFFSET +[IQ_API20_SET_DUMMY_EX] Dummy0[0~5] = IQ_YEE_SCALE_SFT_X +[IQ_API20_SET_DUMMY_EX] Dummy1[0~2] = IQ_NR3D_STICKY_SOLVER_EN, IQ_NR3D_STICKY_SOLVER_TH, IQ_NR3D_STICKY_SOLVER_EN_TH +[IQ_API20_SET_DUMMY_EX] Dummy3 = IQ_NR3D_HIST_MOT_MAP_SW +[IQ_API20_SET_DUMMY_EX] Dummy4 = ALSC table mirror/flip/rotate +IRCUT close +reg=0x1F003410 clk=12000000, period=0x257 +reg=0x1F003408 clk=12000000, u32Duty=0x0 +reg=0x1F003410 clk=12000000, period=0x257 +reg=0x1F003408 clk=12000000, u32Duty=0x257 +reg=0x1F003410 clk=12000000, period=0x257 +reg=0x1F003408 clk=12000000, u32Duty=0x0 +reg=0x1F003490 clk=12000000, period=0x257 +reg=0x1F003488 clk=12000000, u32Duty=0x0 +reg=0x1F003490 clk=12000000, period=0x257 +reg=0x1F003488 clk=12000000, u32Duty=0x5 +reg=0x1F003490 clk=12000000, period=0x257 +reg=0x1F003488 clk=12000000, u32Duty=0x0 +updating oom_score_adj for 475 (procman) from 0 to -1000 because it shares mm with 791 (procman). Report if this is unexpected. +client [814] connected, module:sys +[IQ_API20_SET_DUMMY_EX] Dummy0[0~5] = IQ_YEE_SCALE_SFT_X +[IQ_API20_SET_DUMMY_EX] Dummy1[0~2] = IQ_NR3D_STICKY_SOLVER_EN, IQ_NR3D_STICKY_SOLVER_TH, IQ_NR3D_STICKY_SOLVER_EN_TH +[IQ_API20_SET_DUMMY_EX] Dummy3 = IQ_NR3D_HIST_MOT_MAP_SW +[IQ_API20_SET_DUMMY_EX] Dummy4 = ALSC table mirror/flip/rotate +[IQ_API20_SET_DUMMY_EX] Dummy0[0~5] = IQ_YEE_SCALE_SFT_X +[IQ_API20_SET_DUMMY_EX] Dummy1[0~2] = IQ_NR3D_STICKY_SOLVER_EN, IQ_NR3D_STICKY_SOLVER_TH, IQ_NR3D_STICKY_SOLVER_EN_TH +[IQ_API20_SET_DUMMY_EX] Dummy3 = IQ_NR3D_HIST_MOT_MAP_SW +[IQ_API20_SET_DUMMY_EX] Dummy4 = ALSC table mirror/flip/rotate +[IQ_API20_SET_DUMMY] Dummy0[0~3] = AE acc controls 3DNR MdTh and MdGain. +[IQ_API20_SET_DUMMY] Dummy0[0] = AEDiff_Th. If max of AE acc difference smaller than the value, MdTh and MdGain would be adjusted by the following ratio. +[IQ_API20_SET_DUMMY] Dummy0[1] = MdTh_Ratio. +[IQ_API20_SET_DUMMY] Dummy0[2] = MdGain_Ratio. +[IQ_API20_SET_DUMMY] Dummy0[3] = Log_En. +[IQ_API20_SET_DUMMY] Dummy1[0] = IQ_BNR_WEI_X +[IQ_API20_SET_DUMMY] Dummy1[1~32] = IQ_BNR_WEI_Y +[IQ_API20_SET_DUMMY] Dummy2[0~15] = IQ_YEE_DTL_OFFSET +[IQ_API20_SET_DUMMY_EX] Dummy0[0~5] = IQ_YEE_SCALE_SFT_X +[IQ_API20_SET_DUMMY_EX] Dummy1[0~2] = IQ_NR3D_STICKY_SOLVER_EN, IQ_NR3D_STICKY_SOLVER_TH, IQ_NR3D_STICKY_SOLVER_EN_TH +[IQ_API20_SET_DUMMY_EX] Dummy3 = IQ_NR3D_HIST_MOT_MAP_SW +[IQ_API20_SET_DUMMY_EX] Dummy4 = ALSC table mirror/flip/rotate +[IQ_API20_SET_DUMMY_EX] Dummy0[0~5] = IQ_YEE_SCALE_SFT_X +[IQ_API20_SET_DUMMY_EX] Dummy1[0~2] = IQ_NR3D_STICKY_SOLVER_EN, IQ_NR3D_STICKY_SOLVER_TH, IQ_NR3D_STICKY_SOLVER_EN_TH +[IQ_API20_SET_DUMMY_EX] Dummy3 = IQ_NR3D_HIST_MOT_MAP_SW +[IQ_API20_SET_DUMMY_EX] Dummy4 = ALSC table mirror/flip/rotate +random: crng init done +client [791] connected, module:sys +[MDev_EMAC_ndo_change_mtu][3390] change mtu size from 1500 to 1480 diff --git a/service/internal/boards/contributions/anjoy-ms-j10-c365/uboot.txt b/service/internal/boards/contributions/anjoy-ms-j10-c365/uboot.txt new file mode 100644 index 00000000..553cec35 --- /dev/null +++ b/service/internal/boards/contributions/anjoy-ms-j10-c365/uboot.txt @@ -0,0 +1,147 @@ +IPL g05c44be +D-15 +SPI 54M +64MB +BIST0_0001-OK +[SPI_NOR] +MXP found at 0x0000f000 +offset:00007000 +Checksum OK + +IPL_CUST g05c44be +runUBOOT() +[SPI_NOR] +MXP found at 0x0000f000 +offset:00020000 + -Decompress BDMA XZ + -Decompress BDMA XZ +XZ decomp_size=0x00049be4 + pvLoadAddr:23d00000 + pvRunAddr:23e00000 + -Verify CRC32 passed! + decomp_size=0x00000000 +Disable MMU and D-cache before jump to UBOOT▒ + +U-Boot 2015.01 (Nov 24 2022 - 17:53:59) + +Version: I6g####### +I2C: ready +DRAM: +WARNING: Caches not enabled +MMC: MStar SD/MMC: 0 +nor_flash_mxp allocated success!! +Flash is detected (0x0B03, 0xC8, 0x40, 0x17) +SF: Detected nor0 with total size 8 MiB +MXP found at mxp_offset[2]=0x0000F000, size=0x1000 +env_offset=0x3F000 env_size=0x1000 +Flash is detected (0x0B03, 0xC8, 0x40, 0x17) +SF: Detected nor0 with total size 8 MiB +In: serial +Out: serial +Err: serial +Net: MAC Address 00:00:B4:67:B7:76 +Auto-Negotiation... +Link Status Speed:100 Full-duplex:1 +sstar_emac +reset key up !!! +Init wifi gpio + + +Anjoy # printenv +baudrate=115200 +bootargs=console=ttyS0,115200 root=/dev/mtdblock2 rootfstype=squashfs ro init=/linuxrc LX_MEM=0x3fe0000 mma_heap=mma_heap_name0,miu=0,sz=0x152C000 mma_memblock_remove=1 +bootcmd=sf probe 0;sf read 0x21000000 0x00040000 0x001C0000;bootm 0x21000000 +bootdelay=0 +builddate=202312281015 +ethact=sstar_emac +ethaddr=00:00:B4:67:B7:76 +fileaddr=21000000 +filesize=70954c +fsversion=3.3.0.2 +ipaddr=192.168.1.197 +reset_key=up +sdupgrade=1 +serverip=192.168.1.253 +stderr=serial +stdin=serial +stdout=serial +version_kernel=MSTAR_I6B0_LINUX_335 +version_rootfs=MSJ10_V1 + +Environment size: 602/4092 bytes + + +Anjoy # help +? - alias for 'help' +aes - Control Mstar AES engine +base - print or set address offset +bootm - boot application image from memory +bootp - boot image via network using BOOTP/TFTP protocol +btime - Show booting time +cmp - memory compare +cp - memory copy +crc32 - checksum calculation +dbg - set debug message level. Default level is INFO +dcache - enable or disable data cache +debug - Disable uart rx via PAD_DDCA to use debug tool +dhcp - boot image via network using DHCP/TFTP protocol +dstar - script via SD/MMC +eeprom - EEPROM sub-system +env - environment handling commands +estar - script via network +estart - EMAC start +exit - exit script +false - do nothing, unsuccessfully +fatinfo - print information about filesystem +fatload - load binary file from a dos filesystem +fatls - list files in a directory (default /) +fatsize - determine a file's size +go - start application at address 'addr' +gpio - Config gpio port +help - print command description/usage +i2c - I2C sub-system +icache - enable or disable instruction cache +initDbgLevel- Initial varaible 'dbgLevel' +loop - infinite loop on address range +macaddr - setup EMAC MAC addr +md - memory display +mm - memory modify (auto-incrementing address) +mmc - MMC sub system +mmcinfo - display MMC info +mssdmmc - Mstar SD/MMC IP Verification System +mstar - script via TFTP +mw - memory write (fill) +mxp - MXP function for Mstar MXP partition +nm - memory modify (constant address) +ping - send ICMP ECHO_REQUEST to network host +printenv- print environment variables +pwm - Set 5000Hz duty 50% pwm waveform and use PAD_FUART_RX +Sample: pwm 0 100000 200000 14 + +reset - Perform RESET of the CPU +riu - riu - riu command + +run - run commands in an environment variable +saveenv - save environment variables to persistent storage +sdstar - script via sd package +secauth - Control Sstar security authenticate sequence +setenv - set environment variables +sf - SPI flash sub-system +sfbin - for uploading sf image to a server(via network using TFTP protocol) +showvar - print local hushshell variables +sigauth - Only verify digital signature and aes +srcfg - sensor pin and mclk configuration. +test - minimal test like /bin/sh +test_sig- Test runAuthenticate2 + +tftpboot- boot image via network using TFTP protocol +true - do nothing, successfully +uart - UART sub-system +udpbc - Send or receive UDP broadcast to/from server using UDP protocol +version - print monitor, compiler and linker version + +Anjoy # version + +U-Boot 2015.01 (Nov 24 2022 - 17:53:59) +arm-buildroot-linux-uclibcgnueabihf-gcc.br_real (Buildroot 2017.08-g239d7d0) 4.9.4 +GNU ld (GNU Binutils) 2.28.1 diff --git a/service/internal/boards/contributions_test.go b/service/internal/boards/contributions_test.go new file mode 100644 index 00000000..ddde3f9f --- /dev/null +++ b/service/internal/boards/contributions_test.go @@ -0,0 +1,249 @@ +package boards + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + "testing/fstest" +) + +// contributed is a contributions tree for the fixture's xiongmai-53h20-s. +func contributed(yml, console string) fstest.MapFS { + return fstest.MapFS{ + "contributions.yml": {Data: []byte(yml)}, + "contributions/xiongmai-53h20-s-c9/uboot.txt": {Data: []byte(console)}, + "contributions/xiongmai-53h20-s-c9/boot.log": {Data: []byte("Linux version 3.0.8\nhi3516cv100 System startup\n")}, + } +} + +const oneContribution = ` +- unit: xiongmai-53h20-s-c9 + model: xiongmai-53h20-s + by: someone + evidence: [https://github.com/OpenIPC/website/issues/9] + sensor: IMX222 + flash_chip: MX25L6406E + flash_mb: 8 + note: Stock firmware. + files: + - {kind: uboot_env, file: uboot.txt} + - {kind: boot_log, file: boot.log} +` + +const ownersConsole = "U-Boot 2010.06\nhisilicon # printenv\nbootcmd=sf probe 0;bootm 0x82000000\nethaddr=00:12:34:56:78:9a\n" + +// contributedTree is as much of GET /api/v1/boards as these tests read. +type contributedTree struct { + Manufacturers []struct { + Models []struct { + Units []struct { + ID string + ContributedBy *string `json:"contributed_by"` + Files []struct{ Kind string } + } + } + } + Sources []struct{ ID, Name string } +} + +func applyFS(t *testing.T, im *Importer, fsys fstest.MapFS) []string { + t.Helper() + list, err := parseContributions(fsys) + if err != nil { + t.Fatal(err) + } + missing, err := im.applyContributions(context.Background(), fsys, list) + if err != nil { + t.Fatal(err) + } + return missing +} + +func TestTheShippedContributionsParse(t *testing.T) { + list, err := Contributions() + if err != nil { + t.Fatal(err) + } + if len(list) == 0 { + t.Fatal("contributions.yml is empty") + } + // The MS-J10's console (#366) is a printenv the catalogue can query. + b, err := contributionsFS.ReadFile("contributions/anjoy-ms-j10-c365/uboot.txt") + if err != nil { + t.Fatal(err) + } + vars := UBootVars(string(b)) + if !strings.HasPrefix(vars["bootcmd"], "sf probe 0;") || vars["version_rootfs"] != "MSJ10_V1" { + t.Errorf("console variables: %v", vars) + } + if strings.Contains(string(b), "**") { + t.Error("the console still carries the issue's markdown") + } +} + +func TestAContributionIsAUnitCreditedToItsSender(t *testing.T) { + pool, root := imported(t) + ctx := context.Background() + im := &Importer{Pool: pool, Log: quiet(), Root: root} + if missing := applyFS(t, im, contributed(oneContribution, ownersConsole)); len(missing) != 0 { + t.Fatalf("missing %v", missing) + } + + var source, ref, by, notes string + if err := pool.QueryRow(ctx, `SELECT source::text, source_ref, contributed_by, notes FROM board_units WHERE id = 'xiongmai-53h20-s-c9'`). + Scan(&source, &ref, &by, ¬es); err != nil { + t.Fatal(err) + } + if source != "contributor" || ref != "https://github.com/OpenIPC/website/issues/9" || by != "someone" || notes != "Stock firmware." { + t.Errorf("unit: %s %s %s %q", source, ref, by, notes) + } + var v string + if err := pool.QueryRow(ctx, ` + SELECT v.value FROM board_uboot_vars v JOIN board_artifacts a ON a.id = v.artifact_id + WHERE a.unit_id = 'xiongmai-53h20-s-c9' AND v.key = 'ethaddr'`).Scan(&v); err != nil || v != "00:12:34:56:78:9a" { + t.Errorf("ethaddr %q, %v", v, err) + } + if b, err := os.ReadFile(filepath.Join(root, "xiongmai-53h20-s-c9", "boot.log")); err != nil || !strings.Contains(string(b), "System startup") { + t.Errorf("boot.log on disk: %q, %v", b, err) + } + + // The tree lists the unit with its credit and the source with a name, + // and search reads its text. + s := serve(t, pool) + var tree contributedTree + getJSON(t, s.URL+"/api/v1/boards", &tree) + found := false + for _, m := range tree.Manufacturers { + for _, md := range m.Models { + for _, u := range md.Units { + if u.ID == "xiongmai-53h20-s-c9" { + found = u.ContributedBy != nil && *u.ContributedBy == "someone" && len(u.Files) == 2 + } + } + } + } + if !found { + t.Error("the tree does not show the contributed unit with its credit and files") + } + named := false + for _, src := range tree.Sources { + named = named || (src.ID == "contributor" && src.Name != "") + } + if !named { + t.Errorf("sources: %+v", tree.Sources) + } + var a searchAnswer + getJSON(t, s.URL+"/api/v1/boards/search?q=sf+probe&kind=uboot_env", &a) + if len(a.Hits) != 1 || a.Hits[0].UnitID != "xiongmai-53h20-s-c9" { + t.Errorf("search: %+v", a.Hits) + } +} + +func TestApplyingTwiceChangesNothingAndAnEditReplacesTheUnit(t *testing.T) { + pool, root := imported(t) + ctx := context.Background() + im := &Importer{Pool: pool, Log: quiet(), Root: root} + applyFS(t, im, contributed(oneContribution, ownersConsole)) + stamp := func() (s string) { + _ = pool.QueryRow(ctx, `SELECT u.ingested_at::text || string_agg(a.id::text, ',' ORDER BY a.id) + FROM board_units u JOIN board_artifacts a ON a.unit_id = u.id + WHERE u.id = 'xiongmai-53h20-s-c9' GROUP BY u.ingested_at`).Scan(&s) + return s + } + first := stamp() + applyFS(t, im, contributed(oneContribution, ownersConsole)) + if stamp() != first { + t.Error("an unchanged contribution was stored again") + } + + // A file lost from disk (a database restored on an empty BOARDS_ROOT) + // is written back. + _ = os.RemoveAll(filepath.Join(root, "xiongmai-53h20-s-c9")) + applyFS(t, im, contributed(oneContribution, ownersConsole)) + if _, err := os.Stat(filepath.Join(root, "xiongmai-53h20-s-c9", "uboot.txt")); err != nil { + t.Error(err) + } + + applyFS(t, im, contributed(oneContribution, ownersConsole+"bootdelay=1\n")) + var n int + _ = pool.QueryRow(ctx, `SELECT count(*) FROM board_uboot_vars v JOIN board_artifacts a ON a.id = v.artifact_id + WHERE a.unit_id = 'xiongmai-53h20-s-c9'`).Scan(&n) + if n != 3 { + t.Errorf("an edited console has %d variables, want 3", n) + } +} + +func TestARemovedContributionLeavesNothingBehind(t *testing.T) { + pool, root := imported(t) + ctx := context.Background() + im := &Importer{Pool: pool, Log: quiet(), Root: root} + applyFS(t, im, contributed(oneContribution, ownersConsole)) + applyFS(t, im, contributed("[]", ownersConsole)) + var units, sources int + _ = pool.QueryRow(ctx, `SELECT (SELECT count(*) FROM board_units WHERE source = 'contributor'), + (SELECT count(*) FROM board_sources WHERE id = 'contributor')`).Scan(&units, &sources) + if units != 0 || sources != 0 { + t.Errorf("%d units, %d source rows left", units, sources) + } + if _, err := os.Stat(filepath.Join(root, "xiongmai-53h20-s-c9")); !os.IsNotExist(err) { + t.Errorf("the unit's directory: %v", err) + } + // The archive's own units are untouched. + var archive int + _ = pool.QueryRow(ctx, `SELECT count(*) FROM board_units`).Scan(&archive) + if archive != 6 { + t.Errorf("%d units, want the archive's 6", archive) + } +} + +func TestAContributionWaitsForItsModel(t *testing.T) { + pool, root := imported(t) + im := &Importer{Pool: pool, Log: quiet(), Root: root} + missing := applyFS(t, im, contributed(strings.Replace(oneContribution, "model: xiongmai-53h20-s", "model: nobody-x1", 1), ownersConsole)) + if len(missing) != 1 || missing[0] != "nobody-x1" { + t.Errorf("missing %v", missing) + } + var n int + _ = pool.QueryRow(context.Background(), `SELECT count(*) FROM board_units WHERE source = 'contributor'`).Scan(&n) + if n != 0 { + t.Errorf("%d contributed units stored for a model nobody has", n) + } +} + +func TestAContributionCannotTakeAnotherSourcesUnit(t *testing.T) { + pool, root := imported(t) + im := &Importer{Pool: pool, Log: quiet(), Root: root} + yml := strings.ReplaceAll(oneContribution, "xiongmai-53h20-s-c9", "xiongmai-53h20-s-u1") + fsys := fstest.MapFS{ + "contributions.yml": {Data: []byte(yml)}, + "contributions/xiongmai-53h20-s-u1/uboot.txt": {Data: []byte(ownersConsole)}, + "contributions/xiongmai-53h20-s-u1/boot.log": {Data: []byte("x\n")}, + } + list, err := parseContributions(fsys) + if err != nil { + t.Fatal(err) + } + if _, err := im.applyContributions(context.Background(), fsys, list); err == nil || !strings.Contains(err.Error(), "already taken") { + t.Errorf("err = %v", err) + } + // and the archive's files are where they were. + if _, err := os.Stat(filepath.Join(root, "xiongmai-53h20-s-u1", "front.jpg")); err != nil { + t.Error(err) + } +} + +func TestContributionsYAMLIsChecked(t *testing.T) { + for name, yml := range map[string]string{ + "a flash dump": strings.Replace(oneContribution, "kind: boot_log", "kind: flash_dump", 1), + "a lost file": strings.Replace(oneContribution, "file: boot.log", "file: boot2.log", 1), + "no evidence": strings.Replace(oneContribution, "evidence: [https://github.com/OpenIPC/website/issues/9]", "evidence: []", 1), + "not a web page": strings.Replace(oneContribution, "https://github.com/OpenIPC/website/issues/9", "issue 9", 1), + "twice": oneContribution + strings.TrimPrefix(oneContribution, "\n"), + } { + if _, err := parseContributions(contributed(yml, ownersConsole)); err == nil { + t.Errorf("%s: accepted", name) + } + } +} From 31c05a4f8cd8275393b445e2270cebb9ea0f79ca Mon Sep 17 00:00:00 2001 From: AI Dev Date: Fri, 2 Oct 2026 10:09:25 +0000 Subject: [PATCH 02/13] Board catalogue: an owner's unit follows the catalogue's own units of the model --- service/internal/boards/contributions.go | 14 ++++++++++---- service/internal/boards/contributions_test.go | 6 ++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/service/internal/boards/contributions.go b/service/internal/boards/contributions.go index b3c38a65..edee9ef5 100644 --- a/service/internal/boards/contributions.go +++ b/service/internal/boards/contributions.go @@ -24,6 +24,9 @@ import ( // Contributors is the source of contributed units (migration 003's enum). const Contributors = "contributor" +// contributedPosition is past any position an import gives a unit. +const contributedPosition = 1_000_000 + //go:embed contributions.yml contributions var contributionsFS embed.FS @@ -125,8 +128,10 @@ func (im *Importer) applyContributions(ctx context.Context, fsys fs.FS, list []C continue } keep = append(keep, c.Unit) + // After every catalogue's own units of the model: the source's photos + // and pinout lead, what an owner sent follows. u := &Unit{ID: c.Unit, Sensor: c.Sensor, FlashChip: c.FlashChip, FlashSizeMB: c.FlashMB, - SourceRef: c.Evidence[0], Position: 1000 + i, Source: Contributors, ContributedBy: c.By} + SourceRef: c.Evidence[0], Position: contributedPosition + i, Source: Contributors, ContributedBy: c.By} for _, f := range c.Files { u.Files = append(u.Files, File{Kind: f.Kind, Name: f.File, Source: "contributions/" + c.Unit + "/" + f.File}) } @@ -216,10 +221,11 @@ func (im *Importer) unchanged(ctx context.Context, fsys fs.FS, c Contribution, u var model, ref, by string var sensor, chip, note *string var flash *int + var position int err := im.Pool.QueryRow(ctx, ` - SELECT model_id, source_ref, coalesce(contributed_by, ''), sensor, flash_chip, flash_size_mb, notes + SELECT model_id, source_ref, coalesce(contributed_by, ''), sensor, flash_chip, flash_size_mb, notes, position FROM board_units WHERE id = $1 AND source = $2`, c.Unit, Contributors). - Scan(&model, &ref, &by, &sensor, &chip, &flash, ¬e) + Scan(&model, &ref, &by, &sensor, &chip, &flash, ¬e, &position) if errors.Is(err, pgx.ErrNoRows) { return false, nil } @@ -227,7 +233,7 @@ func (im *Importer) unchanged(ctx context.Context, fsys fs.FS, c Contribution, u return false, err } if model != c.Model || ref != u.SourceRef || by != c.By || deref(sensor) != c.Sensor || - deref(chip) != c.FlashChip || deref(note) != c.Note || derefInt(flash) != c.FlashMB { + deref(chip) != c.FlashChip || deref(note) != c.Note || derefInt(flash) != c.FlashMB || position != u.Position { return false, nil } rows, err := im.Pool.Query(ctx, `SELECT kind::text || ' ' || name || ' ' || sha256 FROM board_artifacts WHERE unit_id = $1 ORDER BY position`, c.Unit) diff --git a/service/internal/boards/contributions_test.go b/service/internal/boards/contributions_test.go index ddde3f9f..7d600508 100644 --- a/service/internal/boards/contributions_test.go +++ b/service/internal/boards/contributions_test.go @@ -105,6 +105,12 @@ func TestAContributionIsAUnitCreditedToItsSender(t *testing.T) { WHERE a.unit_id = 'xiongmai-53h20-s-c9' AND v.key = 'ethaddr'`).Scan(&v); err != nil || v != "00:12:34:56:78:9a" { t.Errorf("ethaddr %q, %v", v, err) } + // It follows the model's own units. + var after bool + if err := pool.QueryRow(ctx, `SELECT (SELECT position FROM board_units WHERE id = 'xiongmai-53h20-s-c9') > + (SELECT max(position) FROM board_units WHERE source <> 'contributor')`).Scan(&after); err != nil || !after { + t.Errorf("the contributed unit sorts before the archive's: %v", err) + } if b, err := os.ReadFile(filepath.Join(root, "xiongmai-53h20-s-c9", "boot.log")); err != nil || !strings.Contains(string(b), "System startup") { t.Errorf("boot.log on disk: %q, %v", b, err) } From 3902cb2212e293c0af5e3e29ad3e3a286a48cd29 Mon Sep 17 00:00:00 2001 From: AI Dev Date: Fri, 2 Oct 2026 10:49:44 +0000 Subject: [PATCH 03/13] The OpenIPC Club: sign in, send from the board panel, private dumps, stars on review Owners sent boot logs and consoles as GitHub issues (#365, #366) because the board panel's "Have this board?" ask opened one. The ask is now a form on the panel, and signing in -- optional -- makes what one sends one's own. internal/club holds accounts and sessions: Telegram (the asking browser shows a code, a QR code on a desktop; Start in the OpenIPC bot finishes the sign-in, and the bot later reports each review), GitHub (an active member of CLUB_MAINTAINER_ORG reviews) and an emailed link, each only when configured. The session cookie's path is /api/v1/club: pages stay static and cached, and a visitor who never signs in is sent no cookie. A sign-in is tied to the browser that started it and used once; POSTs from another site's page are refused. Everything about reports stays in internal/reports: a send for a known board needs no ipctool output and may carry a programmer's flash image; report_submissions records the sender and the board they named; a member's reports, their private dumps (to them and maintainers only), the review queue and the stars ledger are its own, guarded like the rest. Stars are written by a review only: +1 per accepted item, +10 per dump the catalogue did not hold, and the negative of each if a published report is rejected afterwards. `openipc reports publish|reject` takes the same path. A published report's text and photos become a contributed unit on its board (boards.ApplyReportUnits), searchable and counted, beside the units contributions.yml lists; neither list removes the other's. Pages: /club (sign-in, then the member's reports and stars), /club/review (maintainers), the send form in the board panel, and the member's stars in the navbar. The nightly purge drops expired sessions and sign-ins. --- CLAUDE.md | 19 +- data/locales/boards.en.yml | 120 ++++ data/locales/boards.ru.yml | 120 ++++ data/locales/boards.zh.yml | 120 ++++ deploy/nginx/sites-available/org.openipc | 21 + deploy/nginx/sites-available/org.openipc.dev | 36 ++ .../apps/site/src/components/SiteHeader.astro | 5 + .../site/src/components/boards/BoardPanel.tsx | 49 +- .../apps/site/src/components/club/Club.tsx | 327 +++++++++++ .../site/src/components/club/ClubBadge.tsx | 23 + .../apps/site/src/components/club/Review.tsx | 140 +++++ .../src/components/club/SendForm.test.tsx | 80 +++ .../site/src/components/club/SendForm.tsx | 113 ++++ .../apps/site/src/components/club/parts.tsx | 24 + .../apps/site/src/components/pages/Club.astro | 20 + .../src/components/pages/ClubReview.astro | 19 + frontend/apps/site/src/i18n/boards.en.json | 113 ++++ frontend/apps/site/src/i18n/boards.ru.json | 113 ++++ frontend/apps/site/src/i18n/boards.zh.json | 113 ++++ frontend/apps/site/src/i18n/en.json | 10 + frontend/apps/site/src/i18n/ru.json | 10 + frontend/apps/site/src/i18n/zh.json | 10 + .../apps/site/src/lib/boards/boards.test.ts | 1 + frontend/apps/site/src/lib/boards/model.ts | 6 +- frontend/apps/site/src/lib/club.ts | 165 ++++++ frontend/apps/site/src/lib/page-paths.ts | 5 + frontend/apps/site/src/lib/pages.ts | 4 + service/README.md | 19 + service/cmd/openipc/club.go | 89 +++ service/cmd/openipc/main.go | 35 +- service/cmd/openipc/reports.go | 14 +- service/internal/boards/contributions.go | 44 +- service/internal/boards/contributions_test.go | 36 ++ service/internal/club/api.go | 500 ++++++++++++++++ service/internal/club/api_test.go | 555 ++++++++++++++++++ service/internal/club/email.go | 152 +++++ service/internal/club/github.go | 166 ++++++ service/internal/club/members.go | 191 ++++++ service/internal/club/telegram.go | 386 ++++++++++++ service/internal/config/config.go | 22 + service/internal/db/migrations/019_club.sql | 71 +++ .../db/migrations/020_report_submissions.sql | 72 +++ service/internal/reports/club.go | 419 +++++++++++++ service/internal/reports/handler.go | 110 +++- service/internal/reports/store.go | 24 + service/routes.json | 75 +++ 46 files changed, 4714 insertions(+), 52 deletions(-) create mode 100644 frontend/apps/site/src/components/club/Club.tsx create mode 100644 frontend/apps/site/src/components/club/ClubBadge.tsx create mode 100644 frontend/apps/site/src/components/club/Review.tsx create mode 100644 frontend/apps/site/src/components/club/SendForm.test.tsx create mode 100644 frontend/apps/site/src/components/club/SendForm.tsx create mode 100644 frontend/apps/site/src/components/club/parts.tsx create mode 100644 frontend/apps/site/src/components/pages/Club.astro create mode 100644 frontend/apps/site/src/components/pages/ClubReview.astro create mode 100644 frontend/apps/site/src/lib/club.ts create mode 100644 service/cmd/openipc/club.go create mode 100644 service/internal/club/api.go create mode 100644 service/internal/club/api_test.go create mode 100644 service/internal/club/email.go create mode 100644 service/internal/club/github.go create mode 100644 service/internal/club/members.go create mode 100644 service/internal/club/telegram.go create mode 100644 service/internal/db/migrations/019_club.sql create mode 100644 service/internal/db/migrations/020_report_submissions.sql create mode 100644 service/internal/reports/club.go diff --git a/CLAUDE.md b/CLAUDE.md index dd04de11..a0171558 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -151,6 +151,19 @@ restores the image but never the schema, so keep migrations additive. (`/srv/www/shared/owner-reports` -- not `shared/reports`, which is the analytics'), and `internal/boards/survival_test.go` runs every importer over stored reports. +- `internal/club` -- **the OpenIPC Club**: signing in with Telegram (the bot + takes Start with a code the asking browser shows, then tells the member + what happened to what they sent), GitHub (a member of `CLUB_MAINTAINER_ORG` + reviews) or an emailed link, each only when configured + (`TELEGRAM_BOT_TOKEN`, `GITHUB_OAUTH_*`, `CLUB_SMTP_*`; mail must go through + a relay openipc.org's SPF names, not this host). Accounts and sessions + only: a member's reports, their private dumps, the review queue at + `/club/review` and the stars ledger (`report_stars`, written by a review's + decision, never by an upload) are `internal/reports`'. The board panel's + send form posts to `/api/v1/club/reports` instead of opening a GitHub + issue, and a published report's text and photos become a contributed unit + on its board (`boards.ApplyReportUnits`), as `boards/contributions.yml` + does for what arrived as issues (#365, #366). - `internal/tools`, `internal/nfsro` — **ipctool for stock firmware**, which has no curl and no TLS. ipctool's release job pushes each build to `PUT /api/v1/tools/{name}` (OIDC, `internal/tools/PUSH.md`); nginx serves @@ -185,8 +198,10 @@ restores the image but never the schema, so keep migrations additive. ### Conventions & gotchas -- There is no admin and no sign-in (#288). `/admin` answers 410; nothing on the - site sets a cookie. +- There is no admin (#288): `/admin` answers 410. The one sign-in is the + OpenIPC Club (`internal/club`), and its session cookie's path is + `/api/v1/club`: pages stay static and cached, and a visitor who never signs + in is never sent a cookie. - `deploy/static/reserved-paths` lists the addresses a bundle file must never shadow (the upload, the firmware download, the service's APIs, nginx's own locations); `service/deploytest` derives what must be in it and fails when an diff --git a/data/locales/boards.en.yml b/data/locales/boards.en.yml index 9ba7e5f6..695a9b01 100644 --- a/data/locales/boards.en.yml +++ b/data/locales/boards.en.yml @@ -30,6 +30,14 @@ en: fact2: "The MAC, the chip's serial and the cloud ID are replaced with hashes in everything published, in the report and in any log." fact3_title: "A backup is private unless you say otherwise" fact3_html: "It holds everything the camera stores, including Wi-Fi keys and passwords. It is published only if you sent it with --public." + club: + eyebrow: 'OpenIPC Club' + title: 'OpenIPC Club' + lede: 'Sign in to follow what you sent to the board catalogue, keep your flash dumps private, and collect stars for what is accepted.' + club_review: + eyebrow: 'OpenIPC Club' + title: 'Review queue' + lede: 'Owner reports waiting for a maintainer''s decision.' nav: boards: Camera boards cameras: @@ -375,3 +383,115 @@ en: from_source: 'From {source}' sent_by: 'Sent by {who}' known_sources: 'Sources: ' + club: + eyebrow: 'OpenIPC Club' + loading: 'Loading…' + load_failed: 'The club could not be reached. Try again in a minute.' + why_title: 'Why sign in' + why_1: 'See whether what you sent was accepted, and where it appears.' + why_2: 'Keep your flash dumps private: only you and OpenIPC''s maintainers can download them.' + why_3: 'Collect stars, and your name next to the boards you documented.' + why_note: 'You don''t need an account to browse or download firmware. The site sets no cookie for anyone who doesn''t sign in.' + tg_button: 'Continue with Telegram' + tg_hint: 'No password: tap Start in the OpenIPC bot' + gh_button: 'Continue with GitHub' + gh_hint: 'Maintainers sign in here' + or: 'or' + email_label: 'We''ll email you a link that signs you in. No password.' + email_placeholder: 'you@example.com' + email_button: 'Email me a link' + email_sent: 'Check {email}: the link signs you in once, within ten minutes.' + unavailable: 'Not available on this site yet.' + tg_title: 'Sign in with Telegram' + tg_step1: 'Scan the code with your phone''s camera, or open Telegram on this computer.' + tg_step2: 'Tap Start in the chat with the OpenIPC bot.' + tg_step3: 'Come back here. This page signs you in by itself.' + tg_open: 'Open Telegram' + tg_other: 'Use GitHub or email instead' + tg_waiting: 'Waiting for Telegram… This code works for {time} more.' + tg_expired: 'The code expired.' + tg_retry: 'Get a new code' + close: 'Close' + signin_expired: 'That sign-in link has expired or was already used. Start again below.' + signin_elsewhere: 'That sign-in belongs to the browser that started it. Start again here.' + signin_failed: 'The sign-in did not complete. Try again, or use another way in.' + pending: '+{n} waiting for review' + ways: 'Signed in with' + link_more: 'Add another way in' + provider_telegram: 'Telegram' + provider_github: 'GitHub' + provider_email: 'Email' + bot_on: 'The OpenIPC bot tells you when something you sent is reviewed.' + bot_muted: 'The bot''s messages are muted.' + mute: 'Mute the bot' + unmute: 'Unmute the bot' + sign_out: 'Sign out' + review_link: 'Review queue' + mine_title: 'My submissions' + mine_empty: 'Nothing sent yet. Open your board in the catalogue and send what it prints.' + boards_link: 'Camera boards' + col_submission: 'Submission' + col_status: 'Status' + col_stars: 'Stars' + status_pending: 'Waiting for review' + status_published: 'Accepted' + status_rejected: 'Not accepted' + status_withdrawn: 'Withdrawn' + duplicate: 'The catalogue already has this dump' + private: 'Only you and maintainers' + kind_backup: 'Full flash dump' + kind_photo: 'Photo' + kind_boot_log: 'Boot log' + kind_uboot_env: 'U-Boot console' + kind_note: 'Note' + kind_document: 'Document' + kind_yaml: 'ipctool report' + no_board: 'No board named' + rules_title: 'How stars work' + rules_item: 'Boot log, U-Boot console, photo, pinout, ipctool report' + rules_dump: 'Full flash dump the catalogue doesn''t have' + rules_none: 'A dump the catalogue already holds, or anything not accepted' + rules_note: 'Stars count when a maintainer accepts what you sent, never on upload.' + send_kinds_label: 'What you are sending' + send_kind_boot_log: 'Boot log' + send_kind_uboot_env: 'U-Boot console' + send_kind_photo: 'Photos' + send_kind_backup: 'Full flash dump' + send_paste: 'Paste the text here, or choose a file below' + send_file: 'File' + send_photos: 'Photos: the front, the back, the UART pins' + send_dump: 'The whole chip, as a programmer read it or as an ipctool backup' + send_public: 'Publish the dump with the report. It holds Wi-Fi keys and passwords: leave this off to keep it private.' + send_note: 'Anything else: where you bought it, what it is sold as' + send_button: 'Send to the catalogue' + sending: 'Sending…' + sent_member: 'Received as {id}. A maintainer reviews it before it is shown; follow it in My submissions.' + sent_guest: 'Received as {id}. A maintainer reviews it before it is shown.' + receipt: 'Receipt' + my_link: 'My submissions' + sign_in_link: 'Sign in' + send_signed_in: 'Signed in as {name}. Stars count once a maintainer accepts it.' + send_guest: 'You can send without signing in. Sign in to collect stars and follow what happens to it.' + send_failed: 'Not sent: {error}' + send_empty: 'Add a file, a photo or some text first.' + reward: '+{n} ★' + send_another: 'Send something else' + review_title: 'Review queue' + review_empty: 'Nothing is waiting.' + review_forbidden: 'Only OpenIPC''s maintainers review. Sign in with GitHub as a member of the OpenIPC organisation.' + review_from: 'From {who}' + review_anon: 'Sent without signing in' + review_board: 'Board named by the sender' + review_guess: 'ipctool says it looks like {board}' + review_models: 'Board ids to publish it on, comma-separated (empty: the board the sender named)' + review_note: 'Note for the sender' + publish: 'Publish' + reject: 'Reject' + review_points: '{points} ★' + review_done_publish: 'Published. {points} ★ to the sender.' + review_done_reject: 'Rejected.' + show_yaml: 'ipctool output' + tab_pending: 'Waiting' + tab_published: 'Published' + tab_rejected: 'Rejected' + header_club: 'Club' diff --git a/data/locales/boards.ru.yml b/data/locales/boards.ru.yml index e1425362..94423438 100644 --- a/data/locales/boards.ru.yml +++ b/data/locales/boards.ru.yml @@ -30,6 +30,14 @@ ru: fact2: "MAC, серийный номер чипа и облачный ID во всём опубликованном заменены хешами — и в отчёте, и в логах." fact3_title: "Бэкап закрыт, если вы не решите иначе" fact3_html: "В нём всё, что хранит камера, включая ключи Wi-Fi и пароли. Он публикуется, только если вы отправили его с --public." + club: + eyebrow: 'Клуб OpenIPC' + title: 'Клуб OpenIPC' + lede: 'Войдите, чтобы следить за тем, что вы прислали в каталог плат, держать дампы флеш-памяти закрытыми и получать звёзды за принятое.' + club_review: + eyebrow: 'Клуб OpenIPC' + title: 'Очередь проверки' + lede: 'Отчёты владельцев, которые ждут решения мейнтейнера.' nav: boards: Платы камер cameras: @@ -416,3 +424,115 @@ ru: from_source: 'Источник: {source}' sent_by: 'Прислано: {who}' known_sources: 'Источники: ' + club: + eyebrow: 'Клуб OpenIPC' + loading: 'Загрузка…' + load_failed: 'Не удалось связаться с клубом. Попробуйте через минуту.' + why_title: 'Зачем входить' + why_1: 'Видеть, приняли ли присланное и где оно появилось.' + why_2: 'Держать дампы флеш-памяти закрытыми: скачать их можете только вы и мейнтейнеры OpenIPC.' + why_3: 'Получать звёзды и видеть своё имя рядом с платами, которые вы описали.' + why_note: 'Чтобы смотреть каталог и скачивать прошивки, учётная запись не нужна. Тем, кто не входит, сайт не ставит ни одной cookie.' + tg_button: 'Войти через Telegram' + tg_hint: 'Без пароля: нажмите «Старт» в боте OpenIPC' + gh_button: 'Войти через GitHub' + gh_hint: 'Мейнтейнеры входят здесь' + or: 'или' + email_label: 'Пришлём на почту ссылку для входа. Без пароля.' + email_placeholder: 'you@example.com' + email_button: 'Прислать ссылку' + email_sent: 'Проверьте {email}: ссылка выполнит вход один раз в течение десяти минут.' + unavailable: 'На этом сайте пока недоступно.' + tg_title: 'Вход через Telegram' + tg_step1: 'Отсканируйте код камерой телефона или откройте Telegram на этом компьютере.' + tg_step2: 'Нажмите «Старт» в чате с ботом OpenIPC.' + tg_step3: 'Вернитесь сюда: страница выполнит вход сама.' + tg_open: 'Открыть Telegram' + tg_other: 'Войти через GitHub или почту' + tg_waiting: 'Ждём Telegram… Код действует ещё {time}.' + tg_expired: 'Код устарел.' + tg_retry: 'Получить новый код' + close: 'Закрыть' + signin_expired: 'Ссылка для входа устарела или уже использована. Начните заново ниже.' + signin_elsewhere: 'Этот вход принадлежит браузеру, в котором он начат. Начните заново здесь.' + signin_failed: 'Вход не завершился. Попробуйте ещё раз или выберите другой способ.' + pending: '+{n} ждут проверки' + ways: 'Способы входа' + link_more: 'Добавить способ входа' + provider_telegram: 'Telegram' + provider_github: 'GitHub' + provider_email: 'Почта' + bot_on: 'Бот OpenIPC сообщает, когда присланное проверено.' + bot_muted: 'Сообщения бота отключены.' + mute: 'Отключить бота' + unmute: 'Включить бота' + sign_out: 'Выйти' + review_link: 'Очередь проверки' + mine_title: 'Мои материалы' + mine_empty: 'Вы пока ничего не присылали. Откройте свою плату в каталоге и пришлите то, что она выводит.' + boards_link: 'Платы камер' + col_submission: 'Материал' + col_status: 'Статус' + col_stars: 'Звёзды' + status_pending: 'Ждёт проверки' + status_published: 'Принято' + status_rejected: 'Не принято' + status_withdrawn: 'Отозвано' + duplicate: 'Такой дамп в каталоге уже есть' + private: 'Только вам и мейнтейнерам' + kind_backup: 'Полный дамп флеш-памяти' + kind_photo: 'Фото' + kind_boot_log: 'Лог загрузки' + kind_uboot_env: 'Консоль U-Boot' + kind_note: 'Заметка' + kind_document: 'Документ' + kind_yaml: 'Отчёт ipctool' + no_board: 'Плата не указана' + rules_title: 'Как начисляются звёзды' + rules_item: 'Лог загрузки, консоль U-Boot, фото, распиновка, отчёт ipctool' + rules_dump: 'Полный дамп флеш-памяти, которого нет в каталоге' + rules_none: 'Дамп, который уже есть в каталоге, или непринятый материал' + rules_note: 'Звёзды начисляются, когда мейнтейнер принимает присланное, а не при загрузке.' + send_kinds_label: 'Что вы присылаете' + send_kind_boot_log: 'Лог загрузки' + send_kind_uboot_env: 'Консоль U-Boot' + send_kind_photo: 'Фото' + send_kind_backup: 'Полный дамп флеш-памяти' + send_paste: 'Вставьте текст сюда или выберите файл ниже' + send_file: 'Файл' + send_photos: 'Фото: лицевая сторона, обратная, контакты UART' + send_dump: 'Вся микросхема: как её считал программатор, или резервная копия ipctool' + send_public: 'Опубликовать дамп вместе с отчётом. В нём ключи Wi-Fi и пароли: не отмечайте, чтобы он остался закрытым.' + send_note: 'Что-нибудь ещё: где купили, под каким названием продаётся' + send_button: 'Отправить в каталог' + sending: 'Отправляем…' + sent_member: 'Получено, номер {id}. Мейнтейнер проверит перед публикацией; следите в разделе «Мои материалы».' + sent_guest: 'Получено, номер {id}. Мейнтейнер проверит перед публикацией.' + receipt: 'Квитанция' + my_link: 'Мои материалы' + sign_in_link: 'Войти' + send_signed_in: 'Вы вошли как {name}. Звёзды начислятся, когда мейнтейнер примет присланное.' + send_guest: 'Отправить можно и без входа. Войдите, чтобы получать звёзды и следить за проверкой.' + send_failed: 'Не отправлено: {error}' + send_empty: 'Сначала добавьте файл, фото или текст.' + reward: '+{n} ★' + send_another: 'Прислать что-то ещё' + review_title: 'Очередь проверки' + review_empty: 'Ничего не ждёт проверки.' + review_forbidden: 'Проверяют только мейнтейнеры OpenIPC. Войдите через GitHub как участник организации OpenIPC.' + review_from: 'От {who}' + review_anon: 'Прислано без входа' + review_board: 'Плата, указанная отправителем' + review_guess: 'По данным ipctool похоже на {board}' + review_models: 'Id плат для публикации через запятую (пусто — плата, указанная отправителем)' + review_note: 'Пояснение для отправителя' + publish: 'Опубликовать' + reject: 'Отклонить' + review_points: '{points} ★' + review_done_publish: 'Опубликовано. Отправителю {points} ★.' + review_done_reject: 'Отклонено.' + show_yaml: 'Вывод ipctool' + tab_pending: 'Ждут' + tab_published: 'Опубликованы' + tab_rejected: 'Отклонены' + header_club: 'Клуб' diff --git a/data/locales/boards.zh.yml b/data/locales/boards.zh.yml index 0a31f90b..81f58a1f 100644 --- a/data/locales/boards.zh.yml +++ b/data/locales/boards.zh.yml @@ -30,6 +30,14 @@ zh: fact2: "在所有公开内容中(报告和日志),MAC、芯片序列号和云 ID 都会被替换为哈希值。" fact3_title: "备份默认不公开" fact3_html: "备份包含摄像头存储的一切,包括 Wi-Fi 密钥和密码。只有使用 --public 发送时才会公开。" + club: + eyebrow: 'OpenIPC 俱乐部' + title: 'OpenIPC 俱乐部' + lede: '登录后可跟踪你提交到电路板目录的内容、让固件转储保持私密,并为通过审核的内容获得星星。' + club_review: + eyebrow: 'OpenIPC 俱乐部' + title: '审核队列' + lede: '等待维护者审核的用户报告。' nav: boards: 摄像头主板 cameras: @@ -378,3 +386,115 @@ zh: from_source: '来源:{source}' sent_by: '{who} 提供' known_sources: '来源:' + club: + eyebrow: 'OpenIPC 俱乐部' + loading: '加载中…' + load_failed: '无法连接俱乐部服务,请稍后再试。' + why_title: '为什么要登录' + why_1: '查看你提交的内容是否通过审核,以及出现在哪里。' + why_2: '让固件转储保持私密:只有你和 OpenIPC 维护者可以下载。' + why_3: '获得星星,并在你记录的电路板旁显示你的名字。' + why_note: '浏览目录或下载固件无需账户。对不登录的访客,本站不设置任何 cookie。' + tg_button: '使用 Telegram 登录' + tg_hint: '无需密码:在 OpenIPC 机器人中点击“开始”' + gh_button: '使用 GitHub 登录' + gh_hint: '维护者从这里登录' + or: '或' + email_label: '我们会通过邮件发送登录链接,无需密码。' + email_placeholder: 'you@example.com' + email_button: '发送登录链接' + email_sent: '请查收 {email}:链接仅可使用一次,十分钟内有效。' + unavailable: '本站暂不可用。' + tg_title: '使用 Telegram 登录' + tg_step1: '用手机相机扫描二维码,或在本电脑上打开 Telegram。' + tg_step2: '在与 OpenIPC 机器人的对话中点击“开始”。' + tg_step3: '回到此页面,它会自动完成登录。' + tg_open: '打开 Telegram' + tg_other: '改用 GitHub 或邮箱' + tg_waiting: '正在等待 Telegram… 此码还剩 {time} 有效。' + tg_expired: '此码已过期。' + tg_retry: '获取新码' + close: '关闭' + signin_expired: '该登录链接已过期或已被使用,请在下方重新开始。' + signin_elsewhere: '该登录只属于发起它的浏览器,请在此重新开始。' + signin_failed: '登录未完成,请重试或换一种方式。' + pending: '+{n} 等待审核' + ways: '登录方式' + link_more: '添加其他登录方式' + provider_telegram: 'Telegram' + provider_github: 'GitHub' + provider_email: '邮箱' + bot_on: '提交内容被审核后,OpenIPC 机器人会通知你。' + bot_muted: '机器人消息已静音。' + mute: '静音机器人' + unmute: '取消静音' + sign_out: '退出登录' + review_link: '审核队列' + mine_title: '我的提交' + mine_empty: '还没有提交。在目录中打开你的电路板,提交它的输出。' + boards_link: '摄像头电路板' + col_submission: '提交内容' + col_status: '状态' + col_stars: '星星' + status_pending: '等待审核' + status_published: '已通过' + status_rejected: '未通过' + status_withdrawn: '已撤回' + duplicate: '目录中已有此转储' + private: '仅你和维护者可见' + kind_backup: '完整闪存转储' + kind_photo: '照片' + kind_boot_log: '启动日志' + kind_uboot_env: 'U-Boot 控制台' + kind_note: '备注' + kind_document: '文档' + kind_yaml: 'ipctool 报告' + no_board: '未指定电路板' + rules_title: '星星如何计算' + rules_item: '启动日志、U-Boot 控制台、照片、引脚图、ipctool 报告' + rules_dump: '目录中没有的完整闪存转储' + rules_none: '目录中已有的转储,或未通过的内容' + rules_note: '维护者通过你的提交时才计星,上传时不计。' + send_kinds_label: '提交内容类型' + send_kind_boot_log: '启动日志' + send_kind_uboot_env: 'U-Boot 控制台' + send_kind_photo: '照片' + send_kind_backup: '完整闪存转储' + send_paste: '在此粘贴文本,或在下方选择文件' + send_file: '文件' + send_photos: '照片:正面、背面、UART 引脚' + send_dump: '整颗芯片:编程器读出的镜像,或 ipctool 备份' + send_public: '随报告公开此转储。其中包含 Wi-Fi 密钥和密码:不勾选则保持私密。' + send_note: '其他信息:购买渠道、商品名称' + send_button: '提交到目录' + sending: '提交中…' + sent_member: '已收到,编号 {id}。维护者审核后才会公开;可在“我的提交”中跟踪。' + sent_guest: '已收到,编号 {id}。维护者审核后才会公开。' + receipt: '回执' + my_link: '我的提交' + sign_in_link: '登录' + send_signed_in: '已以 {name} 登录。维护者通过后计星。' + send_guest: '无需登录也可提交。登录后可获得星星并跟踪审核进度。' + send_failed: '未提交:{error}' + send_empty: '请先添加文件、照片或文本。' + reward: '+{n} ★' + send_another: '再提交其他内容' + review_title: '审核队列' + review_empty: '没有待审核的内容。' + review_forbidden: '只有 OpenIPC 维护者可以审核。请以 OpenIPC 组织成员身份使用 GitHub 登录。' + review_from: '来自 {who}' + review_anon: '未登录提交' + review_board: '提交者指定的电路板' + review_guess: 'ipctool 显示类似 {board}' + review_models: '发布到的电路板 id,用逗号分隔(留空:提交者指定的电路板)' + review_note: '给提交者的说明' + publish: '发布' + reject: '拒绝' + review_points: '{points} ★' + review_done_publish: '已发布。提交者获得 {points} ★。' + review_done_reject: '已拒绝。' + show_yaml: 'ipctool 输出' + tab_pending: '待审核' + tab_published: '已发布' + tab_rejected: '已拒绝' + header_club: '俱乐部' diff --git a/deploy/nginx/sites-available/org.openipc b/deploy/nginx/sites-available/org.openipc index 1d102f5c..aaafde00 100644 --- a/deploy/nginx/sites-available/org.openipc +++ b/deploy/nginx/sites-available/org.openipc @@ -400,6 +400,27 @@ server { add_header X-Served-By go always; } + # The OpenIPC Club (service/internal/club): signing in, the send form, + # members' own reports and the maintainers' review. Its session cookie's + # path is this prefix, so nothing else on the site ever receives it, and + # nothing here is cached: every answer is about one browser. The send + # form can carry a flash dump, so the body is streamed as for reports. + location ^~ /api/v1/club/ { + client_max_body_size 300m; + client_body_timeout 300; + proxy_request_buffering off; + proxy_read_timeout 300; + proxy_send_timeout 300; + limit_req zone=boards_search burst=30 nodelay; + limit_req_status 429; + proxy_pass http://127.0.0.1:3002; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Request-Id $request_id; + add_header Strict-Transport-Security max-age=15768000; + add_header X-Served-By go always; + } + # Which catalogue boards ipctool's output may be from. Stores nothing. location = /api/v1/boards/identify { client_max_body_size 256k; diff --git a/deploy/nginx/sites-available/org.openipc.dev b/deploy/nginx/sites-available/org.openipc.dev index 763de940..83d5e3ca 100644 --- a/deploy/nginx/sites-available/org.openipc.dev +++ b/deploy/nginx/sites-available/org.openipc.dev @@ -350,6 +350,42 @@ server { add_header X-Robots-Tag "noindex, nofollow, noarchive" always; } + # The OpenIPC Club (service/internal/club): signing in, the send form, + # members' own reports and the maintainers' review. Its session cookie's + # path is this prefix, so nothing else on the site ever receives it, and + # nothing here is cached: every answer is about one browser. The send + # form can carry a flash dump, so the body is streamed as for reports. + location ^~ /api/v1/club/ { + client_max_body_size 300m; + client_body_timeout 300; + proxy_request_buffering off; + proxy_read_timeout 300; + proxy_send_timeout 300; + limit_req zone=boards_search burst=30 nodelay; + limit_req_status 429; + proxy_pass http://127.0.0.1:3012; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Request-Id $request_id; + add_header Strict-Transport-Security max-age=15768000; + add_header X-Served-By go always; + add_header X-Robots-Tag "noindex, nofollow, noarchive" always; + } + + # Telegram delivers what people write to the dev bot here, and Telegram + # has no staging password: the web role checks the bot's secret header. + location = /api/v1/club/telegram/webhook { + auth_basic off; + client_max_body_size 64k; + proxy_pass http://127.0.0.1:3012; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Request-Id $request_id; + add_header Strict-Transport-Security max-age=15768000; + add_header X-Served-By go always; + add_header X-Robots-Tag "noindex, nofollow, noarchive" always; + } + # Which catalogue boards ipctool's output may be from. Stores nothing. location = /api/v1/boards/identify { client_max_body_size 256k; diff --git a/frontend/apps/site/src/components/SiteHeader.astro b/frontend/apps/site/src/components/SiteHeader.astro index 8774a169..62be68ad 100644 --- a/frontend/apps/site/src/components/SiteHeader.astro +++ b/frontend/apps/site/src/components/SiteHeader.astro @@ -26,6 +26,7 @@ */ import { LOCALE_NAMES, alternates, pathFor, useTranslations, type Locale } from '../lib/i18n'; import { menuFor } from '../lib/nav'; +import ClubBadge from './club/ClubBadge.tsx'; import logo from '../assets/openipc-logo-white.svg'; import translate from '../assets/icons/translate.svg?raw'; import github from '../assets/icons/github.svg?raw'; @@ -98,6 +99,10 @@ const alt = alternates(path); ))} + +