ESP32でRustを使ってREST server
·
3min
Lチカが動いたので、REST serverをRustで実装してみる。
Lチカの時と同様に雛形を作る。名前はrest-serverにした
cargo generate --git https://github.com/esp-rs/esp-idf-template cargo
JSONが使えるようにCargo.tomlにserdeを追加。あと、http serverは、EspIOErrorというエラー型を使うようで、面倒なのでanyhowも使うことにした。
[dependencies]
...
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
main.rsはこんな感じ。
use esp_idf_svc::eventloop::EspSystemEventLoop;
use esp_idf_svc::hal::prelude::Peripherals;
use esp_idf_svc::http::server::{Configuration, EspHttpServer};
use esp_idf_svc::http::Method;
use esp_idf_svc::ipv4;
use esp_idf_svc::netif::{EspNetif, NetifConfiguration, NetifStack};
use esp_idf_svc::nvs::EspDefaultNvsPartition;
use esp_idf_svc::wifi::{AuthMethod, BlockingWifi, EspWifi, WifiDriver};
use esp_idf_svc::io::{EspIOError, Write};
use esp_idf_svc::ipv4::{
ClientConfiguration as IpClientConfiguration, ClientSettings as IpClientSettings,
Configuration as IpConfiguration, Mask, Subnet,
};
use esp_idf_svc::wifi::{ClientConfiguration, Configuration as WifiConfiguration};
use std::net::Ipv4Addr;
use std::result::Result;
use std::str::FromStr;
use log::info;
const SSID: &str = env!("ESP32_WIFI_SSID");
const PASSWORD: &str = env!("ESP32_WIFI_PASS");
const DEVICE_IP: &str = env!("ESP_DEVICE_IP");
const GATEWAY_IP: &str = env!("GATEWAY_IP");
const GATEWAY_NETMASK: Option<&str> = option_env!("GATEWAY_NETMASK");
fn main() -> anyhow::Result<()> {
esp_idf_svc::sys::link_patches();
esp_idf_svc::log::EspLogger::initialize_default();
let peripherals = Peripherals::take()?;
let sys_loop = EspSystemEventLoop::take()?;
let nvs = EspDefaultNvsPartition::take()?;
let wifi = WifiDriver::new(peripherals.modem, sys_loop.clone(), Some(nvs))?;
let wifi = configure_wifi(wifi)?;
let mut wifi = BlockingWifi::wrap(wifi, sys_loop)?;
connect_wifi(&mut wifi)?;
// サーバー設定とインスタンス化
let mut server = EspHttpServer::new(&Configuration::default())?;
// GETエンドポイントの登録
server.fn_handler("/", Method::Get, |req| {
req.into_ok_response()?.write_all(b"Hello from Rust REST Server!")?;
Ok::<(), EspIOError>(())
})?;
// POSTエンドポイント(JSONデータの受信など)
server.fn_handler("/api/data", Method::Post, |mut req| {
let mut buf = [0u8; 100];
let len = req.read(&mut buf).unwrap();
// ここでserde等を使ってbufをパースする処理
req.into_ok_response()?.write_all(b"Data received")?;
Ok::<(), EspIOError>(())
})?;
loop {
std::thread::sleep(std::time::Duration::from_secs(60));
}
}
fn configure_wifi(wifi: WifiDriver) -> anyhow::Result<EspWifi> {
let netmask = GATEWAY_NETMASK.unwrap_or("24");
let netmask = u8::from_str(netmask)?;
let gateway_addr = Ipv4Addr::from_str(GATEWAY_IP)?;
let static_ip = Ipv4Addr::from_str(DEVICE_IP)?;
let mut wifi = EspWifi::wrap_all(
wifi,
EspNetif::new_with_conf(&NetifConfiguration {
ip_configuration: Some(IpConfiguration::Client(IpClientConfiguration::Fixed(
IpClientSettings {
ip: static_ip,
subnet: Subnet {
gateway: gateway_addr,
mask: Mask(netmask),
},
// Can also be set to Ipv4Addrs if you need DNS
dns: None,
secondary_dns: None,
},
))),
..NetifConfiguration::wifi_default_client()
})?,
EspNetif::new(NetifStack::Ap)?,
)?;
let wifi_configuration = WifiConfiguration::Client(ClientConfiguration {
ssid: SSID.try_into().unwrap(),
bssid: None,
auth_method: AuthMethod::WPA2Personal,
password: PASSWORD.try_into().unwrap(),
channel: None,
..Default::default()
});
wifi.set_configuration(&wifi_configuration)?;
Ok(wifi)
}
fn connect_wifi(wifi: &mut BlockingWifi<EspWifi<'static>>) -> Result<(), esp_idf_svc::sys::EspError> {
let wifi_configuration: esp_idf_svc::wifi::Configuration = esp_idf_svc::wifi::Configuration::Client(esp_idf_svc::wifi::ClientConfiguration {
ssid: SSID.try_into().unwrap(),
bssid: None,
auth_method: AuthMethod::WPA2Personal,
password: PASSWORD.try_into().unwrap(),
channel: None,
..Default::default()
});
wifi.set_configuration(&wifi_configuration)?;
wifi.start()?;
info!("Wifi started");
wifi.connect()?;
info!("Wifi connected");
wifi.wait_netif_up()?;
info!("Wifi netif up");
Ok(())
}
Wi-FiのSSID/パスワードなどはビルドの時に環境変数で渡すので、これらの環境変数を設定してからcargo buildする。DNSは指定していないけど、必要なら同じように環境変数から渡せば良い。
const SSID: &str = env!("ESP32_WIFI_SSID");
const PASSWORD: &str = env!("ESP32_WIFI_PASS");
const DEVICE_IP: &str = env!("ESP_DEVICE_IP");
const GATEWAY_IP: &str = env!("GATEWAY_IP");
const GATEWAY_NETMASK: Option<&str> = option_env!("GATEWAY_NETMASK");
デフォルトのままだと、こんなエラーになる。
ESP-ROM:esp32c2-eco4-20240515
Build:May 15 2024
rst:0x1 (POWERON),boot:0xc (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:2
load:0x3fcd5c80,len:0x1608
load:0x403ac370,len:0xbec
load:0x403aeb70,len:0x2a90
entry 0x403ac37a
I (32) boot: ESP-IDF v5.5.1-838-gd66ebb86d2e 2nd stage bootloader
I (32) boot: compile time Nov 27 2025 10:07:13
I (32) boot: chip revision: v2.0
I (35) boot: efuse block revision: v0.1
I (40) boot.esp32c2: MMU Page Size : 64K
I (46) boot.esp32c2: SPI Speed : 30MHz
I (52) boot.esp32c2: SPI Mode : DIO
I (58) boot.esp32c2: SPI Flash Size : 4MB
I (63) boot: Enabling RNG early entropy source...
I (70) boot: Partition Table:
I (74) boot: ## Label Usage Type ST Offset Length
I (84) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (94) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (104) boot: 2 factory factory app 00 00 00010000 003f0000
I (114) boot: End of partition table
E (119) esp_image: Segment 0 load address 0x3c0c8020, doesn't match data 0x00010020
E (131) boot: Factory app partition is not bootable
E (138) boot: No bootable app partitions in the partition table
ビルドの時のログを良く見ると、
Chip type: esp32c2 (revision v2.0)
Crystal frequency: 26 MHz
Flash size: 4MB
Features: WiFi, BLE
MAC address: XXXXX
App/part. size: 1,141,120/4,128,768 bytes, 27.64%
アプリケーションのサイズが1MBを越えていることが分かる。デフォルトではプログラムサイズが最大で1MBまでしか書けないため、設定を変更する必要がある。
まずpartitions.csvというファイルを作る。factoryというところがプログラムのサイズのようだ。
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, , 0x4000,
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 0x200000,
sdkconfig.defaultsに以下を追加。ESP8684はフラッシュが4MBあるので、この設定で動く。
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="$ENV{CARGO_MANIFEST_DIR}/partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="$ENV{CARGO_MANIFEST_DIR}/partitions.csv"
Cargo.tomlの設定を追加して、よりサイズの削減を追求する。
[profile.release]
opt-level = "s"
lto = "fat" # リンク時最適化を最大にする
codegen-units = 1 # コンパイル単位を1にして最適化を強める
panic = "abort" # パニック時の巻き戻しを無効化(サイズ削減)
あとは、ビルドの時にreleaseビルドにする。
cargo clean
cargo run --release
これで動くようになった。
Chip type: esp32c2 (revision v2.0)
Crystal frequency: 26 MHz
Flash size: 4MB
Features: WiFi, BLE
MAC address: XXXXX
App/part. size: 890,256/4,128,768 bytes, 21.56%
アプリのサイズを見ていると890KBくらいまで小さくなった。
...
I (5004) rest_server: Wifi connected
I (5004) rest_server: Wifi netif up
I (5014) esp_idf_svc::http::server: Started Httpd server with config Configuration { http_port: 80, ctrl_port: 32768, https_port: 443, max_sessions: 16, session_timeout: 1200s, stack_size: 6144, max_open_sockets: 4, max_uri_handlers: 32, max_resp_headers: 8, lru_purge_enable: true, uri_match_wildcard: false }
I (5054) esp_idf_svc::http::server: Registered Httpd server handler Get for URI "/"
I (5064) wifi:AP's beacon interval = 102400 us, DTIM period = 1
I (5074) esp_idf_svc::http::server: Registered Httpd server handler Post for URI "/api/data"
こんな感じで"/"と"/api/data"で待ち受けているので、curlで試してみる。
$ curl http://192.168.0.252
Hello from Rust REST Server!$ curl -X POST http://192.168.0.252/api/data
Data received
192.168.0.252の部分は、ESP_DEVICE_IPで指定したもの。なおスタックが小さいので無造作に大きな配列などをローカル変数で取るとスタックが溢れるので注意せよとのこと。
るいもの戯言