From 34efaeefd5bd525186dc3176538a93618e20f4e0 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sat, 11 Apr 2026 07:25:16 +0900 Subject: [PATCH] Add list-tests protocol message, test subcommands, extract test handlers --- cmd/picomap/main.go | 70 ++++++++++++++++------ firmware/CMakeLists.txt | 2 +- firmware/include/test_handlers.h | 5 ++ firmware/include/wire.h | 13 +++++ firmware/lib/test_handlers.cpp | 98 +++++++++++++++++++++++++++++++ firmware/test.cpp | 99 +------------------------------- lib/client/client.go | 4 ++ lib/client/types.go | 7 +++ 8 files changed, 183 insertions(+), 115 deletions(-) create mode 100644 firmware/include/test_handlers.h create mode 100644 firmware/lib/test_handlers.cpp diff --git a/cmd/picomap/main.go b/cmd/picomap/main.go index 03f3762..ec21e0c 100644 --- a/cmd/picomap/main.go +++ b/cmd/picomap/main.go @@ -33,7 +33,7 @@ func main() { case "log": err = cmdLog(args) case "test": - err = cmdTest(args) + err = cmdTestGroup(args) default: slog.Error("usage: picomap [args...]") os.Exit(1) @@ -329,18 +329,11 @@ func cmdLoad(args []string) error { return nil } -func cmdTest(args []string) error { - if len(args) < 1 { - return fmt.Errorf("usage: picomap test ") - } - name := args[0] - +func findTestDevice() (string, error) { devs, err := client.ListSerial() if err != nil { - return err + return "", err } - - var testDev string for _, dev := range devs { log := slog.With("dev", dev) c, err := client.NewSerial(dev, 500*time.Millisecond) @@ -354,20 +347,63 @@ func cmdTest(args []string) error { log.Warn("info error", "err", err) continue } - log.Info("got info", "firmware", info.FirmwareName) if info.FirmwareName == "picomap_test" { - testDev = dev - break + return dev, nil } } - if testDev == "" { - return fmt.Errorf("no picomap_test device found") + return "", fmt.Errorf("no picomap_test device found") +} + +func cmdTestGroup(args []string) error { + if len(args) < 1 { + return fmt.Errorf("usage: picomap test [args...]") + } + switch args[0] { + case "list": + return cmdTestList(args[1:]) + case "run": + return cmdTestRun(args[1:]) + default: + return fmt.Errorf("usage: picomap test [args...]") + } +} + +func cmdTestList(_ []string) error { + dev, err := findTestDevice() + if err != nil { + return err + } + c, err := client.NewSerial(dev, 10*time.Second) + if err != nil { + return err + } + defer c.Close() + + result, err := c.ListTests() + if err != nil { + return fmt.Errorf("remote: %w", err) + } + for _, name := range result.Names { + slog.Info("test", "dev", dev, "name", name) + } + return nil +} + +func cmdTestRun(args []string) error { + if len(args) < 1 { + return fmt.Errorf("usage: picomap test run ") + } + name := args[0] + + dev, err := findTestDevice() + if err != nil { + return err } - log := slog.With("dev", testDev) + log := slog.With("dev", dev) log.Info("running test", "name", name) - c, err := client.NewSerial(testDev, 10*time.Second) + c, err := client.NewSerial(dev, 10*time.Second) if err != nil { return err } diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt index a40aab9..a5f708b 100644 --- a/firmware/CMakeLists.txt +++ b/firmware/CMakeLists.txt @@ -29,7 +29,7 @@ pico_set_binary_type(picomap copy_to_ram) pico_add_extra_outputs(picomap) target_link_libraries(picomap ${LIB_DEPS}) -add_executable(picomap_test test.cpp ${LIB_SOURCES}) +add_executable(picomap_test test.cpp lib/test_handlers.cpp ${LIB_SOURCES}) target_include_directories(picomap_test PRIVATE include w6300) target_compile_options(picomap_test PRIVATE -Wall -Wextra -Wno-unused-parameter) pico_generate_pio_header(picomap_test ${CMAKE_CURRENT_LIST_DIR}/w6300/qspi.pio) diff --git a/firmware/include/test_handlers.h b/firmware/include/test_handlers.h new file mode 100644 index 0000000..fdb0f79 --- /dev/null +++ b/firmware/include/test_handlers.h @@ -0,0 +1,5 @@ +#pragma once +#include "wire.h" + +ResponseListTests handle_list_tests(const RequestListTests&); +ResponseTest handle_test(const RequestTest&); diff --git a/firmware/include/wire.h b/firmware/include/wire.h index 920a51b..a13e0ee 100644 --- a/firmware/include/wire.h +++ b/firmware/include/wire.h @@ -74,6 +74,19 @@ struct ResponseLog { auto as_tuple() { return std::tie(entries); } }; +struct RequestListTests { + static constexpr int8_t ext_id = 125; + auto as_tuple() const { return std::tie(); } + auto as_tuple() { return std::tie(); } +}; + +struct ResponseListTests { + static constexpr int8_t ext_id = 124; + std::vector names; + auto as_tuple() const { return std::tie(names); } + auto as_tuple() { return std::tie(names); } +}; + struct RequestTest { static constexpr int8_t ext_id = 127; std::string name; diff --git a/firmware/lib/test_handlers.cpp b/firmware/lib/test_handlers.cpp new file mode 100644 index 0000000..1113898 --- /dev/null +++ b/firmware/lib/test_handlers.cpp @@ -0,0 +1,98 @@ +#include "test_handlers.h" +#include +#include "pico/stdlib.h" +#include "pico/time.h" +#include "w6300.h" + +static w6300::socket_id test_socket{1}; + +static ResponseTest test_discovery() { + ResponseTest resp; + resp.pass = true; + + uint8_t req_buf[1514]; + span_writer req_out(req_buf, sizeof(req_buf)); + auto req_len = encode_response_into(req_out, 0, RequestInfo{}); + if (!req_len) { + resp.pass = false; + resp.messages.push_back("encode: overflow"); + return resp; + } + auto send_result = w6300::send(test_socket, std::span{req_buf, *req_len}); + if (!send_result) { + resp.pass = false; + resp.messages.push_back("send: error " + std::to_string(static_cast(send_result.error()))); + return resp; + } + + uint8_t rx_buf[512]; + + auto deadline = make_timeout_time_ms(5000); + std::expected recv_result = std::unexpected(w6300::sock_error::busy); + while (get_absolute_time() < deadline) { + recv_result = w6300::recv(test_socket, std::span{rx_buf}); + if (recv_result || recv_result.error() != w6300::sock_error::busy) break; + } + + if (!recv_result) { + resp.pass = false; + if (recv_result.error() == w6300::sock_error::busy) { + resp.messages.push_back("recv: timed out after 5s"); + } else { + resp.messages.push_back("recv: error " + std::to_string(static_cast(recv_result.error()))); + } + return resp; + } + + resp.messages.push_back("received " + std::to_string(*recv_result) + " bytes"); + + auto info = decode_response(rx_buf, *recv_result); + if (!info) { + resp.pass = false; + resp.messages.push_back("decode: msgpack error " + std::to_string(static_cast(info.error()))); + return resp; + } + + if (info->firmware_name.empty()) { + resp.pass = false; + resp.messages.push_back("firmware_name is empty"); + } else { + resp.messages.push_back("firmware_name: " + info->firmware_name); + } + + bool mac_zero = true; + for (auto b : info->mac) { if (b != 0) { mac_zero = false; break; } } + if (mac_zero) { + resp.pass = false; + resp.messages.push_back("mac is all zeros"); + } + + bool ip_zero = true; + for (auto b : info->ip) { if (b != 0) { ip_zero = false; break; } } + if (ip_zero) { + resp.pass = false; + resp.messages.push_back("ip is all zeros"); + } + + return resp; +} + +using test_fn = ResponseTest (*)(); + +static const std::unordered_map tests = { + {"discovery", test_discovery}, +}; + +ResponseListTests handle_list_tests(const RequestListTests&) { + ResponseListTests resp; + for (const auto& [name, _] : tests) + resp.names.emplace_back(name); + return resp; +} + +ResponseTest handle_test(const RequestTest& req) { + auto it = tests.find(req.name); + if (it == tests.end()) + return {false, {"unknown test: " + req.name}}; + return it->second(); +} diff --git a/firmware/test.cpp b/firmware/test.cpp index a22d2fc..b515371 100644 --- a/firmware/test.cpp +++ b/firmware/test.cpp @@ -1,10 +1,8 @@ -#include #include "pico/stdlib.h" -#include "pico/time.h" #include "hardware/gpio.h" #include "dispatch.h" #include "handlers.h" -#include "w6300.h" +#include "test_handlers.h" static constexpr uint8_t LED_PIN = 25; @@ -15,104 +13,11 @@ static void led_toggle() { std::string_view firmware_name = "picomap_test"; -static constexpr uint16_t PICOMAP_DISCOVERY_PORT = 28777; - -static constexpr std::array picomap_discovery_ip = {239, 0, 112, 109}; - -static constexpr std::array picomap_discovery_mac = { - 0x01, 0x00, 0x5e, 0x00, 0x70, 0x6d, -}; - -static w6300::socket_id test_socket{1}; - -static ResponseTest test_discovery() { - ResponseTest resp; - resp.pass = true; - - uint8_t req_buf[1514]; - span_writer req_out(req_buf, sizeof(req_buf)); - auto req_len = encode_response_into(req_out, 0, RequestInfo{}); - if (!req_len) { - resp.pass = false; - resp.messages.push_back("encode: overflow"); - return resp; - } - auto send_result = w6300::send(test_socket, std::span{req_buf, *req_len}); - if (!send_result) { - resp.pass = false; - resp.messages.push_back("send: error " + std::to_string(static_cast(send_result.error()))); - return resp; - } - - uint8_t rx_buf[512]; - - auto deadline = make_timeout_time_ms(5000); - std::expected recv_result = std::unexpected(w6300::sock_error::busy); - while (get_absolute_time() < deadline) { - recv_result = w6300::recv(test_socket, std::span{rx_buf}); - if (recv_result || recv_result.error() != w6300::sock_error::busy) break; - } - - if (!recv_result) { - resp.pass = false; - if (recv_result.error() == w6300::sock_error::busy) { - resp.messages.push_back("recv: timed out after 5s"); - } else { - resp.messages.push_back("recv: error " + std::to_string(static_cast(recv_result.error()))); - } - return resp; - } - - resp.messages.push_back("received " + std::to_string(*recv_result) + " bytes"); - - auto info = decode_response(rx_buf, *recv_result); - if (!info) { - resp.pass = false; - resp.messages.push_back("decode: msgpack error " + std::to_string(static_cast(info.error()))); - return resp; - } - - if (info->firmware_name.empty()) { - resp.pass = false; - resp.messages.push_back("firmware_name is empty"); - } else { - resp.messages.push_back("firmware_name: " + info->firmware_name); - } - - bool mac_zero = true; - for (auto b : info->mac) { if (b != 0) { mac_zero = false; break; } } - if (mac_zero) { - resp.pass = false; - resp.messages.push_back("mac is all zeros"); - } - - bool ip_zero = true; - for (auto b : info->ip) { if (b != 0) { ip_zero = false; break; } } - if (ip_zero) { - resp.pass = false; - resp.messages.push_back("ip is all zeros"); - } - - return resp; -} - -using test_fn = ResponseTest (*)(); - -static const std::unordered_map tests = { - {"discovery", test_discovery}, -}; - -static ResponseTest handle_test(const RequestTest& req) { - auto it = tests.find(req.name); - if (it == tests.end()) - return {false, {"unknown test: " + req.name}}; - return it->second(); -} - static constexpr handler_entry handlers[] = { {RequestPICOBOOT::ext_id, typed_handler}, {RequestInfo::ext_id, typed_handler}, {RequestLog::ext_id, typed_handler}, + {RequestListTests::ext_id, typed_handler}, {RequestTest::ext_id, typed_handler}, }; diff --git a/lib/client/client.go b/lib/client/client.go index 031f0aa..75119ad 100644 --- a/lib/client/client.go +++ b/lib/client/client.go @@ -117,6 +117,10 @@ func (c *Client) Log() (*ResponseLog, error) { return first(roundTrip[ResponseLog](c, &RequestLog{})) } +func (c *Client) ListTests() (*ResponseListTests, error) { + return first(roundTrip[ResponseListTests](c, &RequestListTests{})) +} + func (c *Client) Test(name string) (*ResponseTest, error) { return first(roundTrip[ResponseTest](c, &RequestTest{Name: name})) } diff --git a/lib/client/types.go b/lib/client/types.go index e050e46..14072dd 100644 --- a/lib/client/types.go +++ b/lib/client/types.go @@ -24,6 +24,11 @@ type ResponseLog struct { Entries []LogEntry } +type RequestListTests struct{} +type ResponseListTests struct { + Names []string +} + type RequestTest struct { Name string } @@ -57,6 +62,8 @@ func init() { msgpack.RegisterExt(5, (*ResponseInfo)(nil)) msgpack.RegisterExt(6, (*RequestLog)(nil)) msgpack.RegisterExt(7, (*ResponseLog)(nil)) + msgpack.RegisterExt(125, (*RequestListTests)(nil)) + msgpack.RegisterExt(124, (*ResponseListTests)(nil)) msgpack.RegisterExt(127, (*RequestTest)(nil)) msgpack.RegisterExt(126, (*ResponseTest)(nil)) }