use anyhow::Result;
use bytes::{Buf, BytesMut};
use espflash::elf::ElfFirmwareImage;
use futures_util::{SinkExt, StreamExt};
use serde_json::{json, Value};
use std::path::PathBuf;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::net::{TcpListener, TcpStream};
use tokio::signal;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::task::JoinSet;
use tokio_tungstenite::accept_async;
use wokwi_server::{GdbInstruction, SimulationPacket};
use espflash::{Chip, PartitionTable};
const PORT: u16 = 9012;
const GDB_PORT: u16 = 9333;
use clap::Parser;
#[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(short, long, env = "WOKWI_HOST")]
host: Option<String>,
#[clap(short, long)]
chip: Chip,
#[clap(short, long)]
bootloader: Option<PathBuf>,
#[clap(short, long)]
partition_table: Option<PathBuf>,
#[clap(short, long)]
id: Option<String>,
elf: PathBuf,
}
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
#[cfg(feature = "tokio-console")]
console_subscriber::init();
let opts = Args::parse();
if !matches!(opts.chip, Chip::Esp32 | Chip::Esp32c3 | Chip::Esp32s2) {
anyhow::bail!("Chip not supported in Wokwi. See available chips and features at https://docs.wokwi.com/guides/esp32#simulation-features");
}
let (wsend, wrecv) = tokio::sync::mpsc::channel(1);
let (gsend, grecv) = tokio::sync::mpsc::channel(1);
let mut set = JoinSet::new();
set.spawn(wokwi_task(opts, gsend, wrecv));
set.spawn(gdb_task(wsend, grecv));
loop {
tokio::select! {
_ = signal::ctrl_c() => {
set.shutdown().await;
break;
},
task = set.join_next() => {
match task {
Some(Err(e)) => {
println!("Task failed: {:?}", e);
set.shutdown().await;
break;
}
Some(Ok(_)) => {}
None => break,
}
}
}
}
Ok(())
}
async fn wokwi_task(
opts: Args,
mut send: Sender<String>,
mut recv: Receiver<GdbInstruction>,
) -> Result<()> {
let server = TcpListener::bind(("127.0.0.1", PORT)).await?;
let project_id = match opts.id.clone() {
Some(id) => id,
None => match opts.chip {
Chip::Esp32 => "338154815612781140".to_string(),
Chip::Esp32s2 => "338154940543271506".to_string(),
Chip::Esp32c3 => "338322025101656660".to_string(),
_ => unreachable!(),
},
};
let mut url = format!(
"https://wokwi.com/_alpha/wembed/{}?partner=espressif&port={}&data=demo",
project_id, PORT
);
if let Some(h) = opts.host.as_ref() {
url.push_str(&format!("&_host={}", h))
}
println!(
"Open the following link in the browser\r\n\r\n{}\r\n\r\n",
url
);
opener::open_browser(url).ok();
loop {
let (stream, _) = server.accept().await?;
process(opts.clone(), stream, (&mut send, &mut recv)).await?;
}
}
async fn process(
opts: Args,
stream: TcpStream,
(send, recv): (&mut Sender<String>, &mut Receiver<GdbInstruction>),
) -> Result<()> {
let websocket = accept_async(stream).await?;
let (mut outgoing, mut incoming) = websocket.split();
let msg = incoming.next().await; println!("Client connected: {:?}", msg);
let bytes = tokio::fs::read(&opts.elf).await?;
let elf = xmas_elf::ElfFile::new(&bytes).expect("Invalid elf file");
let firmware = ElfFirmwareImage::new(elf);
let p = if let Some(p) = &opts.partition_table {
Some(PartitionTable::try_from_str(String::from_utf8_lossy(
&tokio::fs::read(p).await?,
))?)
} else {
None
};
let b = if let Some(b) = &opts.bootloader {
Some(tokio::fs::read(b).await?)
} else {
None
};
let image = opts.chip.get_flash_image(&firmware, b, p, None, None, None, None, None)?;
let parts: Vec<_> = image.flash_segments().collect();
let bootloader = &parts[0];
let partition_table = &parts[1];
let app = &parts[2];
let simdata = SimulationPacket {
r#type: "start".to_owned(),
elf: base64::encode(&bytes),
esp_bin: vec![
vec![
Value::Number(bootloader.addr.into()),
Value::String(base64::encode(&bootloader.data)),
],
vec![
Value::Number(partition_table.addr.into()),
Value::String(base64::encode(&partition_table.data)),
],
vec![
Value::Number(app.addr.into()),
Value::String(base64::encode(&app.data)),
],
],
};
outgoing
.send(tungstenite::Message::Text(serde_json::to_string(&simdata)?))
.await?;
loop {
tokio::select! {
Some(msg) = incoming.next() => {
let msg = msg?;
if msg.is_text() {
let v: Value = serde_json::from_str(msg.to_text()?)?;
match &v["type"] {
Value::String(s) if s == "uartData" => {
if let Value::Array(bytes) = &v["bytes"] {
let bytes: Vec<u8> =
bytes.iter().map(|v| v.as_u64().unwrap() as u8).collect();
tokio::io::stdout().write_all(&bytes).await?;
}
}
Value::String(s) if s == "gdbResponse" => {
let s = v["response"].as_str().unwrap();
send.send(s.to_owned()).await?;
}
_ => unreachable!(),
}
}
},
Some(command) = recv.recv() => {
match command {
GdbInstruction::Command(s) => {
outgoing
.send(tungstenite::Message::Text(serde_json::to_string(
&json!({
"type": "gdb",
"message": s
}))?
)).await?;
},
GdbInstruction::Break => {
outgoing
.send(tungstenite::Message::Text(serde_json::to_string(
&json!({
"type": "gdbBreak"
}))?
)).await?;
},
}
}
}
}
}
async fn gdb_task(mut send: Sender<GdbInstruction>, mut recv: Receiver<String>) -> Result<()> {
let server = TcpListener::bind(("127.0.0.1", GDB_PORT)).await?;
loop {
let (stream, _) = server.accept().await?;
println!("GDB client connected.");
match handle_gdb_client(stream, &mut send, &mut recv).await {
Ok(_) => println!("GDB Session ended cleanly."),
Err(e) => println!("GDB Session ended with error: {:?}", e),
}
}
}
async fn handle_gdb_client(
mut stream: TcpStream,
send: &mut Sender<GdbInstruction>,
recv: &mut Receiver<String>,
) -> Result<()> {
stream.write_all(b"+").await?;
let mut buffer = BytesMut::with_capacity(1024);
loop {
tokio::select! {
r = stream.read_buf(&mut buffer) => {
let n = r?;
if n == 0 {
anyhow::bail!("GDB End of stream");
}
loop {
let raw_command = String::from_utf8_lossy(buffer.as_ref());
let start = raw_command.find('$').map(|i| i + 1); let end = raw_command.find('#');
match (start, end) {
(Some(start), Some(end)) => {
let command = &raw_command[start..end];
let end = end + 1; let checksum = &raw_command[end..];
let len = if gdb_checksum(command, checksum).is_err() {
stream.write_all(b"-").await?;
end
} else {
stream.write_all(b"+").await?;
send.send(GdbInstruction::Command(command.to_owned()))
.await?;
end + 2
};
buffer.advance(len);
}
(None, Some(end)) => buffer.advance(end),
(Some(_), None) => break,
(None, None) => {
if let Some(_index) = buffer.iter().position(|&x| x == 0x03) {
send.send(GdbInstruction::Break).await?;
}
buffer.advance(buffer.remaining());
break;
}
}
}
}
resp = recv.recv() => {
let resp = resp.ok_or_else(|| anyhow::anyhow!("Channel closed unexpectedly"))?;
stream.write_all(resp.as_bytes()).await?;
}
}
}
}
fn gdb_checksum(cmd: &str, checksum: &str) -> Result<()> {
let cs = cmd.as_bytes().iter().map(|&n| n as u16).sum::<u16>() & 0xff;
let cs = format!("{:02x}", cs);
if cs != checksum {
println!("Invalid checksum, expected {}, calculated {}", checksum, cs);
anyhow::bail!("Invalid checksum, expected {}, calculated {}", checksum, cs);
}
Ok(())
}