diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d4de397..31dc0cce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ had changed in them - which is the gap this file exists to close. ### Added +- **Two more boards emulate.** Heltec E290 and Heltec Wireless Tracker V2 + passed the board probe against v1.17.1 (build, boot, radio, transmit, + receive and flood), so they are offered for emulation. - **Script input at an instant of simulated time.** `console.type`, `console.cli` and the `companion.*` verbs that write to a node take `at_ms`: the call is held and runs when the run reaches that instant, so a script @@ -52,6 +55,10 @@ had changed in them - which is the gap this file exists to close. ### Fixed +- **A board probe says when GitHub's rate limit stopped it.** Out of + unauthenticated API allowance, it reported that the firmware catalogue + could not be reached; it now says the allowance is used up, until when, and + that `GITHUB_TOKEN` raises it. - **Emulated nRF52 boards keep what they store.** The factory registers never said how big the flash was, so the SoftDevice refused every erase and write with `NRF_ERROR_FORBIDDEN`: the filesystem could not be formatted, and diff --git a/internal/firmware/board/board_heltec_e290.go b/internal/firmware/board/board_heltec_e290.go index 04e3f13f..b7f1fd17 100644 --- a/internal/firmware/board/board_heltec_e290.go +++ b/internal/firmware/board/board_heltec_e290.go @@ -20,6 +20,9 @@ var heltecE290Board = Board{ // Serial is the USB port on this board: its MeshCore variant is // built with ARDUINO_USB_CDC_ON_BOOT. ConsoleOnUSB: true, + // Watched booting, transmitting and hearing a neighbour by the board + // probe against v1.17.1 (#603). + Verified: true, }, Hardware: &Panel{ Parts: []Part{ diff --git a/internal/firmware/board/board_heltec_tracker_v2.go b/internal/firmware/board/board_heltec_tracker_v2.go index 36169293..eaee9c4d 100644 --- a/internal/firmware/board/board_heltec_tracker_v2.go +++ b/internal/firmware/board/board_heltec_tracker_v2.go @@ -17,6 +17,9 @@ var heltecTrackerV2Board = Board{ // Serial is the USB port on this board: its MeshCore variant is // built with ARDUINO_USB_CDC_ON_BOOT. ConsoleOnUSB: true, + // Watched booting, transmitting and hearing a neighbour by the board + // probe against v1.17.1 (#603). + Verified: true, }, Hardware: &Panel{ Parts: []Part{ diff --git a/internal/firmware/board/boards.go b/internal/firmware/board/boards.go index 05c7a637..fd2edf17 100644 --- a/internal/firmware/board/boards.go +++ b/internal/firmware/board/boards.go @@ -366,6 +366,11 @@ var EmulationVerified = []string{ "Xiao_S3", "Xiao_S3_WIO", "RAK_3112", + // Probed for #603 against v1.17.1: build, boot, radio, tx, rx and flood + // all passed. The E290's e-paper panel is still not drawn - the board + // runs, its display is the gap. + "Heltec_E290", + "heltec_tracker_v2", } // EmulationSupported reports whether a board can be run under emulation today. diff --git a/internal/firmware/emulated/board_catalogue.go b/internal/firmware/emulated/board_catalogue.go index b0eec904..e569f7d5 100644 --- a/internal/firmware/emulated/board_catalogue.go +++ b/internal/firmware/emulated/board_catalogue.go @@ -12,7 +12,9 @@ import ( "path/filepath" "regexp" "sort" + "strconv" "strings" + "time" "github.com/MeshBench/meshbench/internal/firmware" ) @@ -386,11 +388,33 @@ func (c *BoardCatalogue) get(ctx context.Context, url, accept string) (io.ReadCl } if resp.StatusCode != http.StatusOK { _ = resp.Body.Close() + if limited := rateLimited(resp); limited != "" { + return nil, fmt.Errorf("%s returned %s: %s", url, resp.Status, limited) + } return nil, fmt.Errorf("%s returned %s", url, resp.Status) } return resp.Body, nil } +// rateLimited says so when GitHub refused because this address has used its +// allowance, which reads otherwise as the catalogue being unreachable. Sixty +// unauthenticated requests an hour is a handful of board probes. +func rateLimited(resp *http.Response) string { + if resp.StatusCode != http.StatusForbidden && resp.StatusCode != http.StatusTooManyRequests { + return "" + } + if resp.Header.Get("X-RateLimit-Remaining") != "0" { + return "" + } + when := "" + if reset, err := strconv.ParseInt(resp.Header.Get("X-RateLimit-Reset"), 10, 64); err == nil { + when = " until " + time.Unix(reset, 0).Format("15:04") + } + return "GitHub's API allowance for this address is used up" + when + + "; set GITHUB_TOKEN (export GITHUB_TOKEN=$(gh auth token)) to raise it. " + + "Already-downloaded images keep working without it" +} + // countingReader reports as it goes, throttled to whole percent so a slow // link does not spend its time posting verbs instead of reading bytes. type countingReader struct { diff --git a/internal/firmware/emulated/ratelimit_test.go b/internal/firmware/emulated/ratelimit_test.go new file mode 100644 index 00000000..6701f869 --- /dev/null +++ b/internal/firmware/emulated/ratelimit_test.go @@ -0,0 +1,23 @@ +package emulated + +import ( + "net/http" + "strings" + "testing" +) + +// A refusal because this address has used its API allowance says so, rather +// than reading as a catalogue that could not be reached (#603: five board +// probes in a row failed that way). +func TestARateLimitIsSaid(t *testing.T) { + resp := &http.Response{StatusCode: http.StatusForbidden, Header: http.Header{}} + resp.Header.Set("X-RateLimit-Remaining", "0") + resp.Header.Set("X-RateLimit-Reset", "1792894182") + if got := rateLimited(resp); !strings.Contains(got, "GITHUB_TOKEN") { + t.Errorf("a rate limit read as %q", got) + } + resp.Header.Set("X-RateLimit-Remaining", "12") + if got := rateLimited(resp); got != "" { + t.Errorf("a 403 with allowance left read as a rate limit: %q", got) + } +}