はじめに
以前の記事で、Raspberry Pi Pico 2 と PC を pw_rpc (Pigweed) で繋ぎ、「gRPCのように型安全な」Lチカを実現しました。
その時はUSB経由での通信でした。
今回は、Wi-Fiから通信できるようにしたいと思います。
成果物はこちらです。
やるべきこと。
モノは前回と同じでOKです。
PCからも給電できますが、モバイルバッテリーにつないでみました。
注意点として、モバイルバッテリーはこちらのように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, ¶ms, 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経由でコンソールからコマンドを投げつつ、ログを確認できるようになりました。
ね、簡単でしょ。
ほぼ、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追加できたり、実装方針が明確なのは慣れれば便利だと思いました。

