Merge pull request #64 from stakwork/ota

Add OTA updates support
This commit is contained in:
Evan Feenstra
2022-09-29 11:17:32 -07:00
committed by GitHub
25 changed files with 2503 additions and 52 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
target
Cargo.lock
!sphinx-key/Cargo.lock
!factory/Cargo.lock
.DS_Store
notes.md
test-flash

View File

@@ -1,13 +1,14 @@
[workspace]
members = [
"signer",
"parser",
"signer",
"tester",
]
exclude = [
"sphinx-key",
"persister",
"broker",
"factory",
"persister",
"sphinx-key",
]

View File

@@ -0,0 +1,34 @@
[build]
# Uncomment the relevant target for your chip here (ESP32, ESP32-S2, ESP32-S3 or ESP32-C3)
#target = "xtensa-esp32-espidf"
#target = "xtensa-esp32s2-espidf"
#target = "xtensa-esp32s3-espidf"
target = "riscv32imc-esp-espidf"
[target.xtensa-esp32-espidf]
linker = "ldproxy"
[target.xtensa-esp32s2-espidf]
linker = "ldproxy"
[target.xtensa-esp32s3-espidf]
linker = "ldproxy"
[target.riscv32imc-esp-espidf]
linker = "ldproxy"
# Future - necessary for the experimental "native build" of esp-idf-sys with ESP32C3
# See also https://github.com/ivmarkov/embuild/issues/16
rustflags = ["-C", "default-linker-libraries"]
[unstable]
build-std = ["std", "panic_abort"]
build-std-features = ["panic_immediate_abort"] # Required for older ESP-IDF versions without a realpath implementation
[env]
# Note: these variables are not used when using pio builder
# Enables the esp-idf-sys "native" build feature (`cargo build --features native`) to build against ESP-IDF stable (v4.4)
ESP_IDF_VERSION = { value = "release/v4.4" }
# Enables the esp-idf-sys "native" build feature (`cargo build --features native`) to build against ESP-IDF master (mainline)
#ESP_IDF_VERSION = { value = "master" }

1897
factory/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

33
factory/Cargo.toml Normal file
View File

@@ -0,0 +1,33 @@
[package]
name = "sphinx-key-factory"
version = "0.1.0"
authors = ["decentclock <decentclock.5uh2k@slmail.me>"]
edition = "2021"
[features]
pio = ["esp-idf-sys/pio"]
[dependencies]
esp-idf-sys = { version = "0.31.8", features = ["binstart"] }
esp-idf-svc = { version = "0.42.3", features = ["experimental", "alloc"] }
esp-idf-hal = "0.38.1"
embedded-hal = "0.2.7"
embedded-svc = "0.22.1"
anyhow = { version = "1.0.65", features = ["backtrace"] }
rand = "0.8.5"
log = "0.4.17"
bitflags = "1.3.2"
[build-dependencies]
embuild = "0.29"
anyhow = "1"
[package.metadata.espflash]
partition_table = "table.csv"
[profile.release]
strip = true # Automatically strip symbols from the binary.
opt-level = "z" # Optimize for size.
lto = true
codegen-units = 1
panic = "abort"

14
factory/README.md Normal file
View File

@@ -0,0 +1,14 @@
# Sphinx Key Factory App
The main function of this app is to write any `update.bin` files from the sd card to the flash of the ESP, and configure the ESP so that on the next boot, it boots the freshly written app.
## Background Reading
- Partition Tables: https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/api-guides/partition-tables.html
- Over-the-Air Updates: https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/api-reference/system/ota.html
## Flashing factory and sphinx-key
- First flash the factory app here using the usual `espflash` command, but add the `--partition-table` flag and point it to `table.csv` here. See `espflash -h` for more info.
- Then use `esptool.py` to flash the sphinx-key binary at offset `0xc0000`.
- Finally use this command to tell the ESP to boot the sphinx-key binary first ( there is no update to write to the ESP yet, so we don't boot the factory app ): `otatool.py switch_ota_partition --slot 0`

5
factory/build.rs Normal file
View File

@@ -0,0 +1,5 @@
// Necessary because of this issue: https://github.com/rust-lang/cargo/issues/9641
fn main() -> anyhow::Result<()> {
embuild::build::CfgArgs::output_propagated("ESP_IDF")?;
embuild::build::LinkArgs::output_propagated("ESP_IDF")
}

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,11 @@
# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K)
CONFIG_ESP_MAIN_TASK_STACK_SIZE=64000
# CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y
# Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default).
# This allows to use 1 ms granuality for thread sleeps (10 ms by default).
#CONFIG_FREERTOS_HZ=1000
# Workaround for https://github.com/espressif/esp-idf/issues/7631
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n

41
factory/src/led.rs Normal file
View File

@@ -0,0 +1,41 @@
use embedded_hal::blocking::delay::DelayMs;
use esp_idf_hal::delay::Ets;
use esp_idf_hal::peripherals::Peripherals;
use esp_idf_hal::rmt::config::TransmitConfig;
use esp_idf_hal::rmt::{FixedLengthSignal, PinState, Pulse, Transmit};
use std::time::Duration;
pub fn set_ota_led() {
let peripherals = Peripherals::take().unwrap();
let led = peripherals.pins.gpio8.into_output().unwrap();
let channel = peripherals.rmt.channel0;
let config = TransmitConfig::new().clock_divider(1);
let mut tx = Transmit::new(led, channel, &config).unwrap();
let rgb = 0xffa500; // Orange
let ticks_hz = tx.counter_clock().unwrap();
let t0h = Pulse::new_with_duration(ticks_hz, PinState::High, &ns(350)).unwrap();
let t0l = Pulse::new_with_duration(ticks_hz, PinState::Low, &ns(800)).unwrap();
let t1h = Pulse::new_with_duration(ticks_hz, PinState::High, &ns(700)).unwrap();
let t1l = Pulse::new_with_duration(ticks_hz, PinState::Low, &ns(600)).unwrap();
let mut signal = FixedLengthSignal::<24>::new();
for i in 0..24 {
let bit = 2_u32.pow(i) & rotate_rgb(rgb) != 0;
let (high_pulse, low_pulse) = if bit { (t1h, t1l) } else { (t0h, t0l) };
signal.set(i as usize, &(high_pulse, low_pulse)).unwrap();
}
tx.start_blocking(&signal).unwrap();
Ets.delay_ms(10u8);
}
fn ns(nanos: u64) -> Duration {
Duration::from_nanos(nanos)
}
fn rotate_rgb(rgb: u32) -> u32 {
let b_mask: u32 = 0xff;
let blue = (rgb & b_mask) << 16;
blue | (rgb >> 8)
}

37
factory/src/main.rs Normal file
View File

@@ -0,0 +1,37 @@
mod led;
mod ota;
mod sdcard;
use crate::led::set_ota_led;
use esp_idf_sys as _; // If using the `binstart` feature of `esp-idf-sys`, always keep this module imported
use log::{error, info, warn};
use ota::{run_sdcard_ota_update, set_boot_main_app, UPDATE_BIN_PATH};
use std::path::Path;
use std::thread;
use std::time::Duration;
fn main() {
// Temporary. Will disappear once ESP-IDF 4.4 is released, but for now it is necessary to call this function once,
// or else some patches to the runtime implemented by esp-idf-sys might not link properly.
esp_idf_svc::log::EspLogger::initialize_default();
esp_idf_sys::link_patches();
thread::sleep(Duration::from_secs(10));
set_ota_led();
info!("Hello, world! Mounting sd card...");
sdcard::mount_sd_card();
info!("SD card mounted! Checking for update...");
if let Ok(true) = Path::new(UPDATE_BIN_PATH).try_exists() {
info!("Found update.bin file! Launching the update process...");
while let Err(e) = run_sdcard_ota_update() {
error!("OTA update failed: {}", e.to_string());
error!("Trying again...");
thread::sleep(Duration::from_secs(5));
}
info!("OTA update complete!");
} else {
warn!("Update file not found! Setting up main app boot...");
set_boot_main_app();
}
info!("Restarting ESP, booting the main app...");
unsafe { esp_idf_sys::esp_restart() };
}

51
factory/src/ota.rs Normal file
View File

@@ -0,0 +1,51 @@
use anyhow::Result;
use embedded_svc::io::Write;
use embedded_svc::ota::Ota;
use embedded_svc::ota::OtaUpdate;
use esp_idf_svc::ota::EspOta;
use esp_idf_sys::{esp, esp_ota_get_next_update_partition, esp_ota_set_boot_partition};
use log::info;
use std::fs::File;
use std::io::BufReader;
use std::io::Read;
use std::ptr;
pub const UPDATE_BIN_PATH: &str = "/sdcard/update.bin";
const BUFFER_LEN: usize = 1024;
pub fn run_sdcard_ota_update() -> Result<()> {
let f = File::open(UPDATE_BIN_PATH)?;
let mut reader = BufReader::with_capacity(BUFFER_LEN, f);
let mut ota = EspOta::new()?;
let mut ota = ota.initiate_update()?;
let mut buf = [0_u8; BUFFER_LEN];
let mut read_tot: usize = 0;
let mut write_tot: usize = 0;
let mut i = 0;
loop {
let r = reader.read(&mut buf)?;
if r == 0 {
break;
}
let w = ota.write(&buf[..r])?;
read_tot += r;
write_tot += w;
i += 1;
if i % 20 == 0 {
info!("Cumulative bytes read: {}", read_tot);
info!("Cumulative bytes written: {}", write_tot);
}
}
info!("TOTAL read: {}", read_tot);
info!("TOTAL write: {}", write_tot);
ota.complete()?;
Ok(())
}
pub fn set_boot_main_app() {
let partition = unsafe { esp_ota_get_next_update_partition(ptr::null()) };
esp!(unsafe { esp_ota_set_boot_partition(partition) })
.expect("Couldn't set next boot partition...");
}

138
factory/src/sdcard.rs Normal file
View File

@@ -0,0 +1,138 @@
use bitflags::bitflags;
use esp_idf_sys::c_types::c_char;
use esp_idf_sys::{
esp, esp_vfs_fat_sdmmc_mount_config_t, esp_vfs_fat_sdspi_mount, gpio_num_t, sdmmc_card_t,
sdmmc_host_t, sdspi_device_config_t, spi_bus_config_t, spi_bus_initialize, spi_host_device_t,
spi_host_device_t_SPI2_HOST,
};
use std::ptr;
use std::thread;
use std::time::Duration;
const C_MOUNT_POINT: &'static [u8] = b"/sdcard\0";
const SPI_HOST_SLOT: spi_host_device_t = spi_host_device_t_SPI2_HOST;
const SPI_GPIO_MOSI: gpio_num_t = 7;
const SPI_GPIO_CLK: gpio_num_t = 6;
const SPI_GPIO_MISO: gpio_num_t = 2;
const SPI_GPIO_CS: gpio_num_t = 10;
bitflags! {
struct SDMMCHostFlag: u32 {
/// host supports 1-line SD and MMC protocol
const BIT1 = 1 << 0;
/// host supports 4-line SD and MMC protocol
const BIT4 = 1 << 1;
/// host supports 8-line MMC protocol
const BIT8 = 1 << 2;
/// host supports SPI protocol
const SPI = 1 << 3;
/// host supports DDR mode for SD/MMC
const DDR = 1 << 4;
/// host `deinit` function called with the slot argument
const DEINIT_ARG = 1 << 5;
}
}
#[allow(dead_code)]
enum SDMMCFreq {
/// SD/MMC Default speed (limited by clock divider)
Default = 20000,
/// SD High speed (limited by clock divider)
HighSPeed = 40000,
/// SD/MMC probing speed
Probing = 400,
/// MMC 52MHz speed
_52M = 52000,
/// MMC 26MHz speed
_26M = 26000,
}
#[allow(unused)]
pub fn mount_sd_card() {
while let Err(e) = setup() {
println!("Failed to mount sd card. Make sure it is connected, trying again...");
thread::sleep(Duration::from_secs(5));
}
}
fn setup() -> anyhow::Result<()> {
let mount_config = esp_vfs_fat_sdmmc_mount_config_t {
format_if_mount_failed: false,
max_files: 5,
allocation_unit_size: 16 * 1024,
};
let mut card: *mut sdmmc_card_t = ptr::null_mut();
let bus_cfg = spi_bus_config_t {
__bindgen_anon_1: esp_idf_sys::spi_bus_config_t__bindgen_ty_1 {
mosi_io_num: SPI_GPIO_MOSI,
},
__bindgen_anon_2: esp_idf_sys::spi_bus_config_t__bindgen_ty_2 {
miso_io_num: SPI_GPIO_MISO,
},
sclk_io_num: SPI_GPIO_CLK,
__bindgen_anon_3: esp_idf_sys::spi_bus_config_t__bindgen_ty_3 { quadwp_io_num: -1 },
__bindgen_anon_4: esp_idf_sys::spi_bus_config_t__bindgen_ty_4 { quadhd_io_num: -1 },
data4_io_num: -1,
data5_io_num: -1,
data6_io_num: -1,
data7_io_num: -1,
max_transfer_sz: 4000,
flags: 0,
intr_flags: 0,
};
if let Err(error) = esp!(unsafe {
spi_bus_initialize(
SPI_HOST_SLOT as u32,
&bus_cfg,
esp_idf_sys::spi_common_dma_t_SPI_DMA_CH_AUTO,
)
}) {
if error.code() != 259 {
return Err(anyhow::Error::new(error));
}
}
println!("Initialized SPI BUS!");
let slot_config = sdspi_device_config_t {
host_id: SPI_HOST_SLOT,
gpio_cs: SPI_GPIO_CS,
gpio_cd: -1,
gpio_wp: -1,
gpio_int: -1,
};
let host = sdmmc_host_t {
flags: (SDMMCHostFlag::SPI | SDMMCHostFlag::DEINIT_ARG).bits, //SDMMC_HOST_FLAG_SPI | SDMMC_HOST_FLAG_DEINIT_ARG,
slot: SPI_HOST_SLOT as i32,
max_freq_khz: SDMMCFreq::Default as i32, //SDMMC_FREQ_DEFAULT,
io_voltage: 3.3f32,
init: Some(esp_idf_sys::sdspi_host_init),
set_bus_width: None,
get_bus_width: None,
set_bus_ddr_mode: None,
set_card_clk: Some(esp_idf_sys::sdspi_host_set_card_clk),
do_transaction: Some(esp_idf_sys::sdspi_host_do_transaction),
__bindgen_anon_1: esp_idf_sys::sdmmc_host_t__bindgen_ty_1 {
deinit_p: Some(esp_idf_sys::sdspi_host_remove_device),
},
io_int_enable: Some(esp_idf_sys::sdspi_host_io_int_enable),
io_int_wait: Some(esp_idf_sys::sdspi_host_io_int_wait),
command_timeout_ms: 0,
};
esp!(unsafe {
esp_vfs_fat_sdspi_mount(
C_MOUNT_POINT.as_ptr() as *const c_char,
&host,
&slot_config,
&mount_config,
&mut card as *mut *mut sdmmc_card_t,
)
})?;
Ok(())
}

7
factory/table.csv Normal file
View File

@@ -0,0 +1,7 @@
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 448K,
ota_0, app, ota_0, 0x80000, 3584K,
1 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs, data, nvs, 0x9000, 0x4000,
4 otadata, data, ota, 0xd000, 0x2000,
5 phy_init, data, phy, 0xf000, 0x1000,
6 factory, app, factory, 0x10000, 448K,
7 ota_0, app, ota_0, 0x80000, 3584K,

View File

@@ -2,7 +2,9 @@ use anyhow::Result;
use sphinx_auther::nonce;
use sphinx_auther::secp256k1::{PublicKey, SecretKey};
use sphinx_auther::token::Token;
pub use sphinx_glyph::types::{Config, ControlMessage, ControlResponse, Interval, Policy};
pub use sphinx_glyph::types::{
Config, ControlMessage, ControlResponse, Interval, OtaParams, Policy,
};
use std::sync::{Arc, Mutex};
// u64 is the nonce. Each signature must have a higher nonce
@@ -56,7 +58,7 @@ impl Controller {
Ok(rmp_serde::from_slice(input)?)
}
// return the OG message for further processing
pub fn handle(&mut self, input: &[u8]) -> anyhow::Result<(Vec<u8>, ControlMessage)> {
pub fn handle(&mut self, input: &[u8]) -> anyhow::Result<(ControlMessage, ControlResponse)> {
let msg_nonce = self.parse_msg_no_nonce(input)?;
let msg = msg_nonce.0;
// nonce must be higher each time
@@ -106,12 +108,11 @@ impl Controller {
ControlResponse::AllowlistUpdated(na)
}
ControlMessage::Ota(params) => {
// ...
// same as above comments, actually done in core/events.rs
ControlResponse::OtaConfirm(params)
}
};
let response = self.build_response(res)?;
Ok((response, msg))
Ok((msg, res))
}
}

38
sphinx-key/Cargo.lock generated
View File

@@ -280,12 +280,11 @@ dependencies = [
[[package]]
name = "cargo_toml"
version = "0.11.8"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e72c3ff59e3b7d24630206bb63a73af65da4ed5df1f76ee84dfafb9fee2ba60e"
checksum = "ee685beed1fe2ab3cb9eb95d65727413b5e27f2b34014a3ea9c92053f8c230fc"
dependencies = [
"serde",
"serde_derive",
"toml",
]
@@ -663,6 +662,7 @@ dependencies = [
"anyhow",
"embedded-io",
"enumset",
"futures",
"heapless",
"log",
"no-std-net",
@@ -695,9 +695,9 @@ dependencies = [
[[package]]
name = "embuild"
version = "0.30.3"
version = "0.30.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01f030081cf69373a6c4b811934a81c7c9d3a11066ef22b564327a33f9991125"
checksum = "c6ca66d283ba92dc33ce461ba27bb5085a769dd740bad3f66e60e70926b34d05"
dependencies = [
"anyhow",
"bindgen",
@@ -794,6 +794,7 @@ dependencies = [
"esp-idf-sys",
"heapless",
"log",
"uncased",
]
[[package]]
@@ -805,7 +806,7 @@ dependencies = [
"anyhow",
"bindgen",
"cargo_metadata",
"embuild 0.30.3",
"embuild 0.30.4",
"envy",
"libc",
"regex",
@@ -1488,9 +1489,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
version = "1.0.43"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58"
dependencies = [
"unicode-ident",
]
@@ -2234,18 +2235,18 @@ checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
[[package]]
name = "thiserror"
version = "1.0.35"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85"
checksum = "0a99cb8c4b9a8ef0e7907cd3b617cc8dc04d571c4e73c8ae403d80ac160bb122"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.35"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783"
checksum = "3a891860d3c8d66fec8e73ddb3765f90082374dbaaa833407b904a94f1a7eb43"
dependencies = [
"proc-macro2",
"quote",
@@ -2351,6 +2352,15 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "uncased"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.8"
@@ -2579,9 +2589,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "0.22.4"
version = "0.22.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf"
checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be"
dependencies = [
"webpki",
]

View File

@@ -25,7 +25,7 @@ esp-idf-sys = { version = "0.31.6", features = ["binstart"] }
sphinx-key-signer = { path = "../signer", optional = true }
sphinx-crypter = { git = "https://github.com/stakwork/sphinx-rs.git" }
embedded-svc = "0.22.1"
esp-idf-svc = "0.42.1"
esp-idf-svc = { version = "0.42.1", features = ["experimental", "alloc"] }
esp-idf-hal = "0.38.0"
embedded-hal = "=1.0.0-alpha.8"
anyhow = {version = "1", features = ["backtrace"]}

View File

@@ -1,6 +1,7 @@
# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K)
CONFIG_ESP_MAIN_TASK_STACK_SIZE=64000
#CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y
CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=10000
# CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y
# Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default).
# This allows to use 1 ms granuality for thread sleeps (10 ms by default).

View File

@@ -1,10 +1,12 @@
use crate::conn::mqtt::QOS;
use crate::ota::{update_sphinx_key, validate_ota_message};
use sphinx_key_signer::control::{Config, ControlMessage, ControlResponse, Controller, Policy};
use sphinx_key_signer::lightning_signer::bitcoin::Network;
use sphinx_key_signer::vls_protocol::model::PubKey;
use sphinx_key_signer::{self, make_init_msg, topics, InitResponse, ParserError, RootHandler};
use std::sync::mpsc;
use std::thread;
use embedded_svc::httpd::Result;
use embedded_svc::mqtt::client::utils::ConnState;
@@ -32,6 +34,7 @@ pub enum Status {
ConnectingToMqtt,
Connected,
Signing,
Ota,
}
pub const ROOT_STORE: &str = "/sdcard/store";
@@ -114,7 +117,11 @@ pub fn make_event_loop(
Event::Control(ref msg_bytes) => {
log::info!("GOT A CONTROL MSG");
let cres = ctrlr.handle(msg_bytes);
if let Some(res_data) = handle_control_response(&root_handler, cres, network) {
if let Some(res) =
handle_control_response(&root_handler, cres, network, led_tx.clone())
{
let res_data =
rmp_serde::to_vec(&res).expect("could not publish control response");
mqtt.publish(topics::CONTROL_RETURN, QOS, false, &res_data)
.expect("could not publish control response");
}
@@ -127,43 +134,66 @@ pub fn make_event_loop(
fn handle_control_response(
root_handler: &RootHandler,
cres: anyhow::Result<(Vec<u8>, ControlMessage)>,
cres: anyhow::Result<(ControlMessage, ControlResponse)>,
network: Network,
) -> Option<Vec<u8>> {
led_tx: mpsc::Sender<Status>,
) -> Option<ControlResponse> {
match cres {
Ok((mut response, parsed_msg)) => {
Ok((control_msg, mut control_res)) => {
// the following msg types require other actions besides Flash persistence
match parsed_msg {
match control_msg {
ControlMessage::UpdatePolicy(new_policy) => {
if let Err(e) =
sphinx_key_signer::set_policy(&root_handler, network, new_policy)
{
log::error!("set policy failed {:?}", e);
control_res = ControlResponse::Error(format!("set policy failed {:?}", e))
}
}
ControlMessage::UpdateAllowlist(al) => {
if let Err(e) = sphinx_key_signer::set_allowlist(&root_handler, &al) {
log::error!("set allowlist failed {:?}", e);
control_res =
ControlResponse::Error(format!("set allowlist failed {:?}", e))
}
}
// overwrite the real Allowlist response, loaded from Node
ControlMessage::QueryAllowlist => {
if let Ok(al) = sphinx_key_signer::get_allowlist(&root_handler) {
response = rmp_serde::to_vec(&ControlResponse::AllowlistCurrent(al))
.expect("couldnt build ControlResponse::AllowlistCurrent");
match sphinx_key_signer::get_allowlist(&root_handler) {
Ok(al) => control_res = ControlResponse::AllowlistCurrent(al),
Err(e) => {
log::error!("read allowlist failed {:?}", e);
control_res =
ControlResponse::Error(format!("read allowlist failed {:?}", e))
}
}
}
ControlMessage::Ota(params) => {
if let Err(e) = validate_ota_message(params.clone()) {
log::error!("OTA update cannot launch {:?}", e.to_string());
control_res =
ControlResponse::Error(format!("OTA update cannot launch {:?}", e))
} else {
log::error!("read allowlist failed");
thread::spawn(move || {
led_tx.send(Status::Ota).unwrap();
if let Err(e) = update_sphinx_key(params, led_tx) {
log::error!("OTA update failed {:?}", e.to_string());
} else {
log::info!("OTA flow complete, restarting esp...");
unsafe { esp_idf_sys::esp_restart() };
}
});
log::info!("OTA update launched...");
}
}
_ => (),
};
Some(response)
Some(control_res)
}
Err(e) => {
let response = rmp_serde::to_vec(&ControlResponse::Error(e.to_string()))
.expect("couldnt build ControlResponse::Error");
let control_res = ControlResponse::Error(e.to_string());
log::warn!("error parsing ctrl msg {:?}", e);
Some(response)
Some(control_res)
}
}
}

View File

@@ -1,6 +1,7 @@
#![feature(once_cell)]
mod conn;
mod core;
mod ota;
mod periph;
use crate::core::control::{controller_from_seed, FlashPersister};

123
sphinx-key/src/ota.rs Normal file
View File

@@ -0,0 +1,123 @@
use crate::core::events::Status;
use anyhow::{anyhow, Result};
use embedded_svc::http::client::Client;
use embedded_svc::http::client::Request;
use embedded_svc::http::client::Response;
use embedded_svc::http::Status as HttpStatus;
use embedded_svc::io::Read;
use embedded_svc::ota::Ota;
use esp_idf_svc::http::client::EspHttpClient;
use esp_idf_svc::http::client::EspHttpClientConfiguration;
use esp_idf_svc::http::client::FollowRedirectsPolicy::FollowNone;
use esp_idf_svc::ota::EspOta;
use log::{error, info};
use sphinx_key_signer::control::OtaParams;
use std::fs::{remove_file, File};
use std::io::BufWriter;
use std::io::Write;
use std::sync::mpsc;
const BUFFER_LEN: usize = 3072;
const UPDATE_BIN_PATH: &str = "/sdcard/update.bin";
fn factory_reset() -> Result<()> {
let mut ota = EspOta::new()?;
if ota.is_factory_reset_supported()? {
info!("Factory reset supported, attempting reset...");
ota.factory_reset()?;
Ok(())
} else {
error!("FACTORY RESET CURRENTLY NOT SUPPORTED!");
error!("Only wrote the update binary to the sdcard");
Err(anyhow!("Factory reset not supported"))
}
}
fn get_update(params: OtaParams, led_tx: mpsc::Sender<Status>) -> Result<()> {
let configuration = EspHttpClientConfiguration {
buffer_size: Some(BUFFER_LEN),
buffer_size_tx: Some(BUFFER_LEN / 3),
follow_redirects_policy: FollowNone,
use_global_ca_store: true,
crt_bundle_attach: None,
};
let mut client = EspHttpClient::new(&configuration)?;
let full_url = params_to_url(params);
let mut response = client.get(&full_url)?.submit()?;
let mut reader = response.reader();
let _ = remove_file(UPDATE_BIN_PATH);
let file = File::create(UPDATE_BIN_PATH)?;
let mut writer = BufWriter::new(file);
let mut buf = [0_u8; BUFFER_LEN];
let mut read_tot: usize = 0;
let mut write_tot: usize = 0;
let mut i = 0;
loop {
let r = reader.read(&mut buf)?;
if r == 0 {
break;
}
let w = writer.write(&buf[..r])?;
read_tot += r;
write_tot += w;
i += 1;
if i % 20 == 0 {
led_tx.send(Status::Ota).unwrap();
info!("Cumulative bytes read: {}", read_tot);
info!("Cumulative bytes written: {}", write_tot);
}
}
info!("TOTAL read: {}", read_tot);
info!("TOTAL written: {}", write_tot);
Ok(())
}
pub fn update_sphinx_key(params: OtaParams, led_tx: mpsc::Sender<Status>) -> Result<()> {
info!("Getting the update...");
get_update(params, led_tx)?;
info!("Update written to sd card, performing factory reset");
factory_reset()?;
info!("Factory reset completed!");
Ok(())
}
pub fn validate_ota_message(params: OtaParams) -> Result<()> {
let configuration = EspHttpClientConfiguration {
buffer_size: Some(BUFFER_LEN / 3),
buffer_size_tx: Some(BUFFER_LEN / 3),
follow_redirects_policy: FollowNone,
use_global_ca_store: true,
crt_bundle_attach: None,
};
let mut client = EspHttpClient::new(&configuration)?;
let full_url = params_to_url(params);
info!("Pinging this url for an update: {}", full_url);
let response = client.get(&full_url)?.submit()?;
let status = response.status();
if status == 200 {
info!("Got valid OTA url! Proceeding with OTA update...");
Ok(())
} else if status == 404 {
error!("got 404, update not found on server, make sure the url and version are correct");
Err(anyhow!(
"got 404, update not found on server, make sure the url and version are correct"
))
} else {
error!(
"got {} code when fetching update, something is wrong",
&status.to_string()
);
Err(anyhow!(
"got {} code when fetching update, something is wrong",
&status.to_string()
))
}
}
fn params_to_url(params: OtaParams) -> String {
let mut url = params.url.clone();
url.push_str(&params.version.to_string());
url
}

View File

@@ -19,15 +19,16 @@ pub struct Led {
fn states() -> BTreeMap<Status, (Color, Time)> {
let mut s = BTreeMap::new();
s.insert(Status::Starting, (0x000001, 100));
s.insert(Status::MountingSDCard, (0x000102, 100));
s.insert(Status::SyncingTime, (0x000122, 100));
s.insert(Status::WifiAccessPoint, (0x000100, 100));
s.insert(Status::Configuring, (0x010000, 20));
s.insert(Status::ConnectingToWifi, (0x010100, 350));
s.insert(Status::ConnectingToMqtt, (0x010001, 100));
s.insert(Status::Connected, (0x000101, 400));
s.insert(Status::Signing, (0x111111, 100));
s.insert(Status::Starting, (0x000001, 100)); // Blue
s.insert(Status::MountingSDCard, (0x000102, 100)); // Cyan
s.insert(Status::SyncingTime, (0x000122, 100)); // Cyan
s.insert(Status::WifiAccessPoint, (0x000100, 100)); // Green
s.insert(Status::Configuring, (0x010000, 20)); // Red
s.insert(Status::ConnectingToWifi, (0x010100, 350)); // Yellow
s.insert(Status::ConnectingToMqtt, (0x010001, 100)); // Purple
s.insert(Status::Connected, (0x000101, 400)); // Cyan
s.insert(Status::Signing, (0x111111, 100)); // White
s.insert(Status::Ota, (0xffa500, 100)); // Orange
s
}

View File

@@ -25,6 +25,7 @@ serde_json = "1.0"
urlencoding = "2.1.0"
dotenv = "0.15.0"
rocket = "0.5.0-rc.2"
rmp-serde = "1.1.0"
[[bin]]
name = "config"
@@ -36,4 +37,4 @@ path = "src/server.rs"
[[bin]]
name = "ctrl"
path = "src/ctrl.rs"
path = "src/ctrl.rs"

View File

@@ -1,6 +1,6 @@
use dotenv::dotenv;
use serde::{Deserialize, Serialize};
use sphinx_key_parser::control::{ControlMessage, Controller};
use sphinx_key_parser::control::{ControlMessage, Controller, OtaParams};
use sphinx_key_signer::lightning_signer::bitcoin::Network;
use std::env;
use std::time::Duration;
@@ -25,6 +25,15 @@ async fn main() -> anyhow::Result<()> {
let seed = hex::decode(seed_string).expect("yo");
let mut ctrl = controller_from_seed(&Network::Regtest, &seed, nonce);
let version_string: String = env::var("VERSION").unwrap_or("0".to_string());
let version: u64 = version_string.parse::<u64>().expect("failed to parse version");
let ota_url: String = env::var("OTA_URL").unwrap_or("http://192.168.1.10/sphinx-update-".to_string());
let control_message = ControlMessage::Ota(OtaParams {
version: version,
url: ota_url
});
//let msg = ctrl.build_msg(control_message)?;
let msg = ctrl.build_msg(ControlMessage::Nonce)?;
let msg_hex = hex::encode(&msg);

View File

@@ -118,13 +118,14 @@ async fn run_main(mut eventloop: EventLoop, client: &AsyncClient, mut ctrlr: Con
}
topics::CONTROL => {
match ctrlr.handle(&msg_bytes) {
Ok((response, _new_policy)) => {
Ok((_msg, res)) => {
let res_data = rmp_serde::to_vec(&res).expect("could not build control response");
client
.publish(
topics::CONTROL_RETURN,
QoS::AtMostOnce,
false,
response,
res_data,
)
.await
.expect("could not mqtt publish");
@@ -175,13 +176,14 @@ async fn run_test(mut eventloop: EventLoop, client: &AsyncClient, mut ctrlr: Con
}
topics::CONTROL => {
match ctrlr.handle(&msg_bytes) {
Ok((response, _new_policy)) => {
Ok((_msg, res)) => {
let res_data = rmp_serde::to_vec(&res).expect("could not build control response");
client
.publish(
topics::CONTROL_RETURN,
QoS::AtMostOnce,
false,
response,
res_data,
)
.await
.expect("could not mqtt publish");