Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ had changed in them - which is the gap this file exists to close.

### Fixed

- **A board's cached capability row names the emulator it ran under.** On a
machine with both emulators set up, every nRF52 row was stamped with QEMU's
binary, so upgrading Renode retired none of them and upgrading QEMU retired
all of them. The fingerprint now follows the board, and also covers the chip
library and, for Renode, our peripherals. Rows cached by an earlier build
read as stale once.
- **Adding a scheduled send no longer repeats the ones already said.** Any
`schedule.add` made every one-shot send whose moment had passed go out
again.
Expand Down
3 changes: 3 additions & 0 deletions internal/firmware/emulated/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ func fileExists(p string) bool {
// under which a released bundle unpacks it.
const radioLibName = "virtual-sx1262"

// RadioLib is that name for FindTool, from outside this package.
const RadioLib = radioLibName

// radioLibFiles is what that library is actually called on disk here. A shared
// library is named by its platform rather than by what it is.
var radioLibFiles = func() []string {
Expand Down
67 changes: 58 additions & 9 deletions internal/sim/boardcheck/boardcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ package boardcheck
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"

hw "github.com/MeshBench/meshbench/internal/firmware/board"
Expand Down Expand Up @@ -78,16 +80,45 @@ func (r *BoardReport) set(c Capability, s State, detail string) {
r.Results[c] = Result{Capability: c, State: s, Detail: detail}
}

// EmulatorFingerprint identifies the emulator build in use, so a cached
// report can tell whether it is still about the binary that produced it.
// The size and mtime of the QEMU or Renode binary this run resolves to -
// cheap, and it changes exactly when a rebuild would invalidate the report.
func EmulatorFingerprint() string {
path := os.Getenv(emulated.EnvQEMU)
if path == "" {
path = os.Getenv("MESHBENCH_RENODE")
// EmulatorFingerprint identifies what a board runs under on this machine, so
// a cached report can tell whether it is still about the binaries that
// produced it.
//
// Which emulator follows from the board, as it does when the board is run:
// QEMU for the ESP32 boards and Renode for the nRF52 ones. It used to read
// MESHBENCH_QEMU first and fall back to Renode, so on a machine set up for
// both every nRF52 row was stamped with QEMU's binary - upgrading Renode
// retired none of them and upgrading QEMU retired all of them (#551).
//
// The chip library is part of it too, because the chip decides what a board
// demonstrates as much as the emulator does, and so for Renode are our C#
// peripherals beside it. Each part is the size and mtime of the file the
// lookup a boot does would find: cheap, and it changes exactly when a rebuild
// or a fetch would invalidate the report.
func EmulatorFingerprint(board string) string {
b, err := hw.BoardByName(board)
if err != nil {
return "unknown board"
}
tool := "qemu-system-xtensa"
if b.Renode != nil {
tool = "renode"
}
parts := []string{
tool + "=" + fileFingerprint(emulated.FindTool(tool)),
"chip=" + fileFingerprint(emulated.FindTool(emulated.RadioLib)),
}
if path == "" {
if b.Renode != nil {
parts = append(parts, "peripherals="+dirFingerprint(
filepath.Join(emulated.SupportDir(), "peripherals")))
}
return strings.Join(parts, ";")
}

// fileFingerprint is a file's path, size and mtime, or "unconfigured" when
// the lookup found nothing.
func fileFingerprint(path string, err error) string {
if err != nil {
return "unconfigured"
}
fi, err := os.Stat(path)
Expand All @@ -97,6 +128,24 @@ func EmulatorFingerprint() string {
return fmt.Sprintf("%s@%d-%d", path, fi.Size(), fi.ModTime().Unix())
}

// dirFingerprint is the total size and newest mtime of a directory's files.
func dirFingerprint(dir string) string {
ents, err := os.ReadDir(dir)
if err != nil {
return "unconfigured"
}
var size, newest int64
for _, e := range ents {
fi, err := e.Info()
if err != nil || fi.IsDir() {
continue
}
size += fi.Size()
newest = max(newest, fi.ModTime().Unix())
}
return fmt.Sprintf("%s@%d-%d", dir, size, newest)
}

// MatrixReports is the whole board list, agreeing with
// hw.EmulatableBoards() on the can-it-run question: a board that
// cannot be emulated at all is reported boot-failed with that reason,
Expand Down
77 changes: 74 additions & 3 deletions internal/sim/boardcheck/boardcheck_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package boardcheck

import (
"os"
"path/filepath"
"strings"
"testing"

hw "github.com/MeshBench/meshbench/internal/firmware/board"
"github.com/MeshBench/meshbench/internal/firmware/emulated"
"github.com/MeshBench/meshbench/internal/firmware/emulated/renode"
)

// A board never run reports "untested" in every column, never blank and
Expand Down Expand Up @@ -76,9 +80,9 @@ func TestALoadedReportIsStaleWhenTheEmulatorFingerprintDiffers(t *testing.T) {
}

got := Load("RAK_4631", "repeater-v1.17.0")
// EmulatorFingerprint() with no MESHBENCH_QEMU/RENODE set resolves to
// "unconfigured", which does not equal the saved fingerprint above - so
// this must read as stale, not as still current.
// Whatever EmulatorFingerprint finds on this machine, it is not the
// saved fingerprint above - so this must read as stale, not as still
// current.
if !got.Stale {
t.Error("a report saved under a different emulator fingerprint did not report stale")
}
Expand Down Expand Up @@ -155,3 +159,70 @@ func TestMatrixAgreesWithEmulatableBoards(t *testing.T) {
}
}
}

// The fingerprint names the emulator the board runs under, not whichever
// variable happens to be set: on a machine with both, an nRF52 row was stamped
// with QEMU's binary (#551). The chip library is part of it, and for Renode so
// are the peripherals beside it.
func TestTheFingerprintFollowsTheBoardsEmulator(t *testing.T) {
dir := t.TempDir()
file := func(name, body string) string {
p := filepath.Join(dir, name)
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
return p
}
qemu, renodeBin, chip := file("qemu", "q"), file("renode", "r"), file("chip", "c")
if err := os.MkdirAll(filepath.Join(dir, "support", "peripherals"), 0o755); err != nil {
t.Fatal(err)
}
periph := filepath.Join(dir, "support", "peripherals", "VirtualSX1262.cs")
if err := os.WriteFile(periph, []byte("p"), 0o644); err != nil {
t.Fatal(err)
}
t.Setenv(emulated.EnvQEMU, qemu)
t.Setenv(renode.EnvRenode, renodeBin)
t.Setenv(emulated.EnvRadioLib, chip)
t.Setenv(emulated.EnvRenodeSupport, filepath.Join(dir, "support"))

nrf := EmulatorFingerprint("RAK_4631")
if !strings.Contains(nrf, renodeBin+"@") || strings.Contains(nrf, qemu+"@") {
t.Errorf("an nRF52 board's fingerprint is not Renode's: %s", nrf)
}
esp := EmulatorFingerprint("LilyGo_TDeck")
if !strings.Contains(esp, qemu+"@") || strings.Contains(esp, renodeBin+"@") {
t.Errorf("an ESP32 board's fingerprint is not QEMU's: %s", esp)
}
for _, fp := range []string{nrf, esp} {
if !strings.Contains(fp, chip+"@") {
t.Errorf("the chip library is not in %s", fp)
}
}

// Each part moves the rows it should and only those.
touch := func(p, body string) {
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
touch(qemu, "qemu, rebuilt")
if EmulatorFingerprint("RAK_4631") != nrf {
t.Error("a new QEMU retired an nRF52 row")
}
if EmulatorFingerprint("LilyGo_TDeck") == esp {
t.Error("a new QEMU did not retire an ESP32 row")
}
touch(periph, "peripheral, changed")
if EmulatorFingerprint("RAK_4631") == nrf {
t.Error("a changed Renode peripheral did not retire an nRF52 row")
}
esp = EmulatorFingerprint("LilyGo_TDeck")
touch(chip, "chip, new release")
if EmulatorFingerprint("LilyGo_TDeck") == esp {
t.Error("a new chip library did not retire an ESP32 row")
}
if got := EmulatorFingerprint("No_Such_Board"); got != "unknown board" {
t.Errorf("an unknown board: %s", got)
}
}
2 changes: 1 addition & 1 deletion internal/sim/boardcheck/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,6 @@ func Load(board, version string) BoardReport {
return untestedReport(board, version)
}
}
r.Stale = r.EmulatorFP != EmulatorFingerprint()
r.Stale = r.EmulatorFP != EmulatorFingerprint(board)
return r
}
2 changes: 1 addition & 1 deletion internal/sim/boardcheck/probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (flatEarth) ElevationM(_, _ float64) (float64, bool) { return 0, true }
// rather than hanging the probe for whoever is waiting on the column.
func Probe(ctx context.Context, board, version string) (report BoardReport) {
report = untestedReport(board, version)
report.EmulatorFP = EmulatorFingerprint()
report.EmulatorFP = EmulatorFingerprint(board)
report.MeasuredAt = time.Now()

cacheDir := firmware.DefaultCacheDir()
Expand Down