2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Raspberry Pi Pico 2 × Zephyr × PigweedでProtobuf RPC通信 ~Wi-Fi篇~

Posted at

はじめに

以前の記事で、Raspberry Pi Pico 2 と PC を pw_rpc (Pigweed) で繋ぎ、「gRPCのように型安全な」Lチカを実現しました。

その時はUSB経由での通信でした。

今回は、Wi-Fiから通信できるようにしたいと思います。
成果物はこちらです。

やるべきこと。

モノは前回と同じでOKです。
PCからも給電できますが、モバイルバッテリーにつないでみました。

image.png

注意点として、モバイルバッテリーはこちらのようにIoT機器対応でないとRaspberry Pi Picoは消費電力が少なすぎて、電力消費されてないと判断され、電源OFFになる可能性があります。

ソフトとしては、以下の3点を実装すればOKです。

  • FW
    • Zephyr RTOSで無線LANルーターに接続し、TCPサーバーを立ち上げる
    • TCPサーバーをUSB Serialと同様にRPC/LOG出力に紐づける
  • PCツール
    • Transport層として、Serial通信とSocketを交換できるようにする

簡単ですね。

ZephyrでのTCPサーバー

Zephyrの流儀に従えばいいのですが、結局一番ここが苦労しました...
Configはこのあたりです。

# Networking general
CONFIG_NETWORKING=y
CONFIG_NET_IPV4=y
CONFIG_NET_TCP=y
CONFIG_NET_DHCPV4=y
CONFIG_NET_SOCKETS=y
CONFIG_NET_UDP=y
CONFIG_MBEDTLS_ENABLE_HEAP=y
CONFIG_MBEDTLS_HEAP_SIZE=30000
CONFIG_WIFI_LOG_LEVEL_DBG=y
CONFIG_WIFI_LOG_LEVEL_ERR=y
CONFIG_WIFI_LOG_LEVEL_INF=y
CONFIG_NET_LOG=y

# Wi-Fi support
CONFIG_WIFI=y
CONFIG_WIFI_NM=y
CONFIG_NET_L2_WIFI_MGMT=y
CONFIG_WIFI_NM_WPA_SUPPLICANT_WPA3=n
CONFIG_NET_MGMT_EVENT_STACK_SIZE=4096

WPA3は明示的にOFFしないと動かないかもしれません。

苦戦の結果、こんな感じでWi-Fiに接続できました。

void wifi_connect(void) {
  struct net_if* iface = net_if_get_default();
  while (true) {
    net_mgmt(NET_REQUEST_WIFI_DISCONNECT, iface, NULL, 0);
    k_sleep(K_SECONDS(1));
    PW_LOG_INFO("Preparing to connect to Wi-Fi: SSID=%s", wifi_settings.ssid);
    struct wifi_connect_req_params params = {
        .ssid = (const uint8_t*)wifi_settings.ssid,
        .ssid_length = strlen(wifi_settings.ssid),
        .psk = (const uint8_t*)wifi_settings.password,
        .psk_length = strlen(wifi_settings.password),
        .channel = WIFI_CHANNEL_ANY,
        .security = WIFI_SECURITY_TYPE_PSK,
    };
    PW_LOG_INFO("Requesting Wi-Fi connection...");
    int ret = net_mgmt(NET_REQUEST_WIFI_CONNECT, iface, &params, sizeof(params));
    if (ret != 0 && ret != -120) {
      PW_LOG_ERROR("Initial request failed (ret=%d), retrying...", ret);
      k_sleep(K_SECONDS(2));
      continue;
    }
    //  Waiting for connection
    bool connected = false;
    for (int retry = 0; retry < 5; retry++) { 
      struct wifi_iface_status status;
      net_mgmt(NET_REQUEST_WIFI_IFACE_STATUS, iface, &status, sizeof(status));
      PW_LOG_INFO("Checking State: %d", status.state);
      if (status.state == 4 || status.state == 9) { // WIFI_STATE_COMPLETED
        connected = true;
        break; 
      }
      k_sleep(K_SECONDS(2)); 
    }
    if (connected) {
      return;
    }
    PW_LOG_ERROR("Connection timeout, resetting...");
  }
}

TCPサーバーの立ち上げ方は普通です、 zsock_がつくようです。
POSIXのAPIを使おうとしましたが、Pigweedの抽象レイヤー衝突したせいか、ビルドが通りませんでした。

  int serv = zsock_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  struct sockaddr_in bind_addr = {
      .sin_family = AF_INET,
      .sin_port = htons(RPC_PORT),
      .sin_addr = {.s_addr = INADDR_ANY},
  };
  zsock_bind(serv, (struct sockaddr*)&bind_addr, sizeof(bind_addr));
  zsock_listen(serv, 1);

RPC/Logとのつなぎ込み

USB向けに実装したUsbStreamWriterと同様にTcpStreamWriterを実装します。

class TcpStreamWriter : public pw::stream::NonSeekableWriter {
 public:
  TcpStreamWriter() : client_fd_(-1) {}
  void set_client_fd(int fd) { client_fd_ = fd; }

 private:
  pw::Status DoWrite(pw::ConstByteSpan data) override {
    if (client_fd_ < 0) return pw::Status::FailedPrecondition();
    ssize_t sent = zsock_send(client_fd_, data.data(), data.size(), 0);
    return (sent == static_cast<ssize_t>(data.size())) ? pw::OkStatus()
                                                       : pw::Status::Unknown();
  }
  int client_fd_;
};
...
TcpStreamWriter tcp_writer;

あとはそのtcp_writerをTCPサーバーと接続して、受信ループです。

      tcp_writer.set_client_fd(client);
      ThreadSafeHdlcChannelOutput hdlc_channel_output(
          tcp_writer, pw::hdlc::kDefaultRpcAddress, "HDLC_OUT");
      pw::rpc::Channel channels[] = {
          pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
      pw::rpc::Server server(channels);
      practice::rpc::DeviceService device_service;
      server.RegisterService(device_service);

サーバーのコマンド受信ループで、RPCを見つけたらUSBと同様に実行します。

    while (1) {
        char buf[1024];
        ssize_t len = zsock_recv(client, buf, sizeof(buf), 0);
        PW_LOG_DEBUG("Received %d bytes from client", len);
        if (len <= 0) {
          PW_LOG_INFO("Client disconnected");
          wifi_connected = false;
          break;
        }
        for (int i = 0; i < len; i++) {
          auto result = decoder.Process(static_cast<std::byte>(buf[i]));
          if (result.ok()) {
            server.ProcessPacket(result.value().data());
          }
        }
      }
      zsock_close(client);
    }

ログ出力は、USBとWi-Fi(つながってれば)の両方に出力するように実装しました。

mutexも共有で問題ないでしょう。

extern "C" void pw_log_tokenized_HandleLog(uint32_t metadata,
                                           const uint8_t log_buffer[],
                                           size_t size_bytes) {
  k_mutex_lock(&write_lock, K_FOREVER);
  std::array<std::byte, sizeof(uint32_t)> metadata_bytes =
      pw::bytes::CopyInOrder(pw::endian::little, metadata);
  // Send log with HDLC over USB
  pw::hdlc::Encoder encoder(serial_writer);
  encoder.StartUnnumberedFrame(pw::hdlc::kDefaultLogAddress);
  encoder.WriteData(metadata_bytes);
  encoder.WriteData(pw::as_bytes(pw::span(log_buffer, size_bytes)));
  encoder.FinishFrame();
  //Send log with HDLC over Wi-Fi
  if (wifi_connected) {
    pw::hdlc::Encoder tcp_encoder(tcp_writer);
    tcp_encoder.StartUnnumberedFrame(pw::hdlc::kDefaultLogAddress);
    tcp_encoder.WriteData(metadata_bytes);
    tcp_encoder.WriteData(pw::as_bytes(pw::span(log_buffer, size_bytes)));
    tcp_encoder.FinishFrame();
  }
  k_mutex_unlock(&write_lock);
}

Clientの実装

Wi-Fiと接続するsocketをserialと互換にするためのClassを作成します。

class TcpSocketWrapper:
    """A simple TCP socket wrapper to mimic serial.Serial interface for RPC communication."""

    def __init__(self, ip, port, timeout=1.0):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(timeout)
        self.sock.connect((ip, port))

    def read(self, num_bytes: int = 1):
        try:
            return self.sock.recv(num_bytes)
        except socket.timeout:
            return b""

    def write(self, data: bytes):
        return self.sock.sendall(data)

    def close(self):
        self.sock.close()

あとはClient作成の関数を拡張するだけです。

def get_rpc_client(
    serial_port="",
    baud_rate=115200,
    vid_pid="2fe3:0100",
    ip_address="",
    port=8888,
    elf_path="build/zephyr/zephyr.elf",
):
    # TCP接続ループ
    if ip_address:
        logger.info(f"Connecting to TCP {ip_address}:{port}...")
        while True:
            try:
                #socketの時はtransport = TcpSocketWrapper
                transport = TcpSocketWrapper(ip_address, port)
                logger.info("Connected via TCP")
                break
            except Exception as e:
                logger.info(f"Failed to connect to {ip_address}: {e}, retrying...")
                time.sleep(1)
    else...
        # seiralの時はtransport = Serial
        transport = serial.Serial(actual_port, baud_rate, timeout=0.1)
    # あとは共通
    ...
    def write(data):
        transport.write(data) # serial or socket
    ...
    client = HdlcRpcClient(transport, [service_pb2], default_channels(write), output=detoken)  # type: ignore
    ...

あとは、スクリプトの引数を変えて、TCPから接続するときはIPアドレスを指定して、

python tools/console.py -i 192.168.0.10

とするようにしました。

*mDNSとか使ったらカッコいいですが、今回はIPアドレスとします。

Wi-Fi経由でコンソールからコマンドを投げつつ、ログを確認できるようになりました。 

image.png

ね、簡単でしょ。

ほぼ、ZephyrからSocket通信しただけですね。

コマンドからSSID/Passwordの登録

SSID/passwordをソースにべた書きするのは事故の元なので、コマンドからFlashに保存するようにします。

まずは保存場所をDevice Treeで設定します。
Pico 2 (RP2350) はデフォルトでパーティションが切られていないため、app.overlay で明示的に切る必要があります。

コード領域の後に64KのKVS(Key Value Store)を定義しました。

&flash0 {
    partitions {
        compatible = "fixed-partitions";
        #address-cells = <1>;
        #size-cells = <1>;

        code_partition: partition@0 {
            label = "code-partition";
            reg = <0x00000000 DT_SIZE_M(3)>;
            read-only;
        };

        storage_partition: partition@300000 {
            label = "storage";
            reg = <0x00300000 DT_SIZE_K(64)>;
        };
    };
};

prj.confは省略して、RPCを定義します。

message WifiSettings {
  string ssid = 1 [(nanopb).max_size = 32];
  string password = 2 [(nanopb).max_size = 64];
}
service DeviceService {
  rpc ConfigureWifi(WifiSettings) returns (Empty);
}

RPCからKVSにSSIDとPasswordを保存して、

  ::pw::Status ConfigureWifi(const practice_rpc_WifiSettings& request,
                             practice_rpc_Empty& response) {
    settings_save_one("wifi/ssid", request.ssid, strlen(request.ssid) + 1);
    settings_save_one("wifi/password", request.password, strlen(request.password) + 1);
    PW_LOG_INFO("Configuring Wi-Fi: SSID=%s", request.ssid);
    return pw::OkStatus();
  }

main冒頭で読み込めば完成です!
KVSの読み込みには、Callbackを作らないといけないようです。

//Callbackを作成
static int handle_wifi(const char* name, size_t len, settings_read_cb read_cb,
                       void* cb_arg) {
  if (strcmp(name, "ssid") == 0) {
    PW_LOG_INFO("Loading Wi-Fi SSID from settings");
    ssize_t ret =
        read_cb(cb_arg, wifi_settings.ssid, sizeof(wifi_settings.ssid));
    return (ret < 0) ? (int)ret : 0;
  }
  if (strcmp(name, "password") == 0) {
    PW_LOG_INFO("Loading Wi-Fi password from settings");
    ssize_t ret =
        read_cb(cb_arg, wifi_settings.password, sizeof(wifi_settings.password));
    return (ret < 0) ? (int)ret : 0;
  }
  return -ENOENT;
}
struct settings_handler wifi_conf = {.name = "wifi", .h_set = handle_wifi};


int main() {
  settings_subsys_init();
  settings_register(&wifi_conf);
  settings_load();
    ...

まとめ

今回はRaspberry Pi Pico 2 × Zephyr × Pigweedの環境で、Wi-FiからもRPC実行とログの取得ができるようにしました。

ほぼZephyrの紹介で、Pigweed関係なかったですね。
ただ、サクッとRPC追加できたり、実装方針が明確なのは慣れれば便利だと思いました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?