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:
| M | ecu-esp32/src/wifi.rs | | | 111 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- |
| M | notes.md | | | 101 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- |
| A | otaclient.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