eculocate

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit c8dd5e04149aa135a001fbab3afe8f905c3c2ed2
parent 40536c8dcae395d519f9785baa5139e86ee610e3
Author: Daniel Barlow <dan@telent.net>
Date:   Fri, 16 Jan 2026 23:23:19 +0000

add tcp listener for OTA updating and session keys

* actual OTA updating not yet implemented, we read from the socket and
  throw the data away

* session keys are ignored

Diffstat:
Mecu-esp32/src/wifi.rs | 111++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mnotes.md | 101++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Aotaclient.rb | 11+++++++++++
3 files changed, 217 insertions(+), 6 deletions(-)

diff --git a/ecu-esp32/src/wifi.rs b/ecu-esp32/src/wifi.rs @@ -34,7 +34,9 @@ use esp_radio::{ }, }; -use log::info; +use embassy_net::Stack; + +use log::{warn,info}; use crate::mdns; @@ -106,7 +108,7 @@ pub async fn start_wifi(spawner: &Spawner, let (stack, runner) = embassy_net::new( wifi_interface, config, - mk_static!(StackResources<4>, StackResources::<4>::new()), + mk_static!(StackResources<5>, StackResources::<5>::new()), seed, ); @@ -134,7 +136,10 @@ pub async fn start_wifi(spawner: &Spawner, let mut serial = None; join( - crate::streamer::loop_udp_thing(stack, ecu), + join( + crate::streamer::loop_udp_thing(stack, ecu), + run_tcp_handler(stack), + ), async { loop { let mdns_thread = mdns::responder( @@ -203,3 +208,103 @@ async fn connection(mut controller: WifiController<'static>) { async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) { runner.run().await } + +async fn read_all(socket: &mut embassy_net::tcp::TcpSocket<'_>, + buf: &mut [u8], + len: usize) -> usize { + let mut n = 0; + while n < len { + match socket.read(&mut buf[n..]).await { + Ok(0) => { + warn!("read EOF"); + break; + }, + Ok(count) => { + n = n + count; + } + Err(e) => { + warn!("read err {:?}", e); + return 0; // ew + } + } + } + n +} + +async fn write_ota(sock: &mut embassy_net::tcp::TcpSocket<'_>, len: usize) { + let mut buf = [0u8; 4096]; + // let mut ota = esp_ota::OtaUpdate::begin()?; + let mut len = len; + while len > 0 { + match sock.read(&mut buf).await { + Ok(chunksize) => { + info!("{} bytes to write to flash", chunksize); + // ota.write(&buf[0..chunksize])?; + len = len - chunksize; + }, + Err(x) => { + warn!("error reading ota image: {:?}", x); + break; + } + } + } + if len == 0 { + info!("wrote all bytes"); + } else { + warn!("stream ended early, was expecting another {} bytes", len); + } +} + +fn register_session_key(key : &[u8]) { + info!("got session key{:?}", key); +} + + +async fn run_tcp_handler(stack : Stack<'_>) { + let mut rx_buffer = [0; 4096]; + let mut tx_buffer = [0; 4096]; + let mut buf = [0u8; 68]; + + loop { + let mut socket = embassy_net::tcp::TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + socket.set_timeout(Some(Duration::from_secs(10))); + + if let Err(e) = socket.accept(5000).await { + warn!("error accepting connection: {:?}", e); + continue; + } + + info!("Received connection from {:?}", socket.remote_endpoint()); + + let n = read_all(&mut socket, &mut buf, 68).await; + + if n == 68 { + info!("got a thing"); + match &buf[0..4] { + b"ROM0" => { + info!("OTA request"); + let mut len_bytes = [0u8; 4]; + let n = read_all(&mut socket, &mut len_bytes[..], 4).await; + let len = u32::from_be_bytes(len_bytes); + info!("reading {} bytes", len); + write_ota(&mut socket, len as usize).await; + info!("write {:?}", socket.write(b"BYE\n").await); + socket.flush().await; + socket.close(); + }, + b"KEY0" => { + info!("sessionkey"); + let _sig = &buf[4..68]; + let mut key = [0u8; 32]; // 256 bits + if read_all(&mut socket, &mut key, 32).await == 32 { + register_session_key(&key); + } + }, + _ => { + warn!("unrecognised command"); + } + + } + } + } +} diff --git a/notes.md b/notes.md @@ -126,11 +126,29 @@ according to https://forum.pgmfi.org/viewtopic.php?p=140964#p140964 table 0 is "ecmid" -## TODO +# TODO + +* OTA updates so that we can run it without a USB connection + + - over wifi, using tcp + - perhaps just add a minimal http server that accepts a suitably authenticated POST request + - this would be simple to automate as well as not impossible to design into a + client app + - what does authentication look like? + +we can generate a ECDSA key pair at build time. embed the pub key in the binary +and sign the firmware with the private key. use the "ring" library to verify the +signature of anything that's http uploaded + +we probably don't want to mdns-advertise the http server (what would +be the point?) . just run it on our ip address at 5001 + + + * wifi provisioning over ble -* ecu needs to timestamp the table reads before sending +[done] * ecu needs to timestamp the table reads before sending. * add the 12v->5v buck converter, make a cable to plug into diag port @@ -138,4 +156,81 @@ table 0 is "ecmid" * add an IMU to get lean angles -* OTA updates might be handy once it's installed on the bike +* some kind of security for the udp stream + +could we use the firmware keypair somehow? if the client has the +private key, it can send an http request containing a proposed session +key which the esp32 verifies, then it can use that session key when it +sends subscription requests + +do we need to bulk out the request so that we have something big enought sign? +https://stackoverflow.com/questions/4676828/when-generating-a-sha256-512-hash-is-there-a-minimum-safe-amount-of-data-to thinks not + + +if we do it like this then we probably _should_ advertise the http +service + + +does it need to be http or could we just do tcp streams? there's +https://crates.io/crates/httparse + +the only two things we need to send are session keys and firmware + +0: "ROM0" +4: eddsa sig +68: length of payload +72: .........payload....... + + +0: "KEY0" +4: eddsa sig +68: proposed session key (32 bytes) +100: ...ends + + +the 0 in KEY0/ROM0 is a version number, in case we want future +versions of eculocate to accept different formats for these things + + +the signature also means we know the data is intact/untampered + + + +<ecdsa> +nnnn0765f6ac86e65e87a76d65f5c\0 + +ROMnnnn765d76e0765f............6ac86e7a765f5c\0<ecdsa> + +it might be better to send strings with leading length instead of using \0 +for end, because then we don't need to ascii-encode. esp32-c3 can support up to +16MB (2^24) of flash, so a 32 bit length will be plenty + + +### TODO + +- add ring crate +- create an eddsa keypair with openssl +- read the pubkey into the rust binary +- sign something with the key using openssl +- as PoC, add the signed thing into the rust binary and verify the signature at startup +- make a TCP listener +- parse tcp messages to extract command/signature/payload +- verify signature +- implement flashing support +- maintain a hash of session keys and expire them eventually +- update the subscription request parsing to expect a session key, validate it + +or we could do flashing without signatures first + +- make a TCP listener +- parse tcp messages to extract command/signature/payload +- discard signature +- implement flashing support +- add ring crate +- create an eddsa keypair with openssl +- read the pubkey into the rust binary +- sign something with the key using openssl +- as PoC, add the signed thing into the rust binary and verify the signature at startup +- verify signature before flashing +- maintain a hash of session keys and expire them eventually +- update the subscription request parsing to expect a session key, validate it diff --git a/otaclient.rb b/otaclient.rb @@ -0,0 +1,11 @@ +require 'socket' + +sock = TCPSocket.new("eculocate.local", 5000) + +image_len = (rand* 64_000).to_i + +payload = "ROM0" + ("z" * 64) + [image_len].pack("L>") + ("i" * image_len) + +sock.write(payload) + +puts sock.readline