SEDSnet 4.0.6

A memory safe, no_std-capable networking stack with routing, discovery, reliability, and Rust/C/Python bindings.
Documentation
use serde_json::Value;
use std::collections::HashMap;
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};

fn required_str<'a>(value: &'a Value, key: &str) -> &'a str {
    value[key]
        .as_str()
        .unwrap_or_else(|| panic!("schema field `{key}` must be a string"))
}

fn rust_string(value: &str) -> String {
    format!("{value:?}")
}

fn generate_embedded_schema(schema_path: &Path, output_path: &Path) {
    let mut generated = String::from(
        "// Generated by build.rs. Static metadata stays in flash on no_std targets.\n",
    );

    if !schema_path.is_file() {
        generated.push_str("const EMBEDDED_SCHEMA_ENDPOINTS: &[EndpointDefinition] = &[];\n");
        generated.push_str("const EMBEDDED_SCHEMA_TYPES: &[DataTypeDefinition] = &[];\n");
        fs::write(output_path, generated).expect("write empty embedded schema");
        return;
    }

    let bytes = fs::read(schema_path).expect("read telemetry_config.json");
    let schema: Value = serde_json::from_slice(&bytes).expect("parse telemetry_config.json");
    let endpoints = schema["endpoints"]
        .as_array()
        .expect("schema endpoints must be an array");
    let types = schema["types"]
        .as_array()
        .expect("schema types must be an array");

    let mut endpoint_ids = HashMap::new();
    generated.push_str("const EMBEDDED_SCHEMA_ENDPOINTS: &[EndpointDefinition] = &[\n");
    for (index, endpoint) in endpoints.iter().enumerate() {
        let id = 100_u32 + u32::try_from(index).expect("too many schema endpoints");
        let name = required_str(endpoint, "name");
        let rust_name = endpoint["rust"].as_str().unwrap_or(name);
        let description = endpoint["doc"]
            .as_str()
            .or_else(|| endpoint["description"].as_str())
            .unwrap_or("");
        let link_local = endpoint["link_local_only"].as_bool().unwrap_or(false)
            || endpoint["broadcast_mode"].as_str() == Some("Never");
        endpoint_ids.insert(rust_name.to_owned(), id);
        writeln!(
            generated,
            "    EndpointDefinition {{ id: DataEndpoint({id}), name: {}, description: {}, link_local_only: {link_local} }},",
            rust_string(name),
            rust_string(description),
        )
        .unwrap();
    }
    generated.push_str("];\n");

    generated.push_str("const EMBEDDED_SCHEMA_TYPES: &[DataTypeDefinition] = &[\n");
    for (index, ty) in types.iter().enumerate() {
        let id = 100_u32 + u32::try_from(index).expect("too many schema types");
        let name = required_str(ty, "name");
        let description = ty["doc"]
            .as_str()
            .or_else(|| ty["description"].as_str())
            .unwrap_or("");
        let class = match required_str(ty, "class") {
            "Data" => "MessageClass::Data",
            "Error" => "MessageClass::Error",
            "Warning" => "MessageClass::Warning",
            other => panic!("unsupported message class `{other}`"),
        };
        let data_type = match required_str(&ty["element"], "data_type") {
            "Float64" => "MessageDataType::Float64",
            "Float32" => "MessageDataType::Float32",
            "UInt8" => "MessageDataType::UInt8",
            "UInt16" => "MessageDataType::UInt16",
            "UInt32" => "MessageDataType::UInt32",
            "UInt64" => "MessageDataType::UInt64",
            "UInt128" => "MessageDataType::UInt128",
            "Int8" => "MessageDataType::Int8",
            "Int16" => "MessageDataType::Int16",
            "Int32" => "MessageDataType::Int32",
            "Int64" => "MessageDataType::Int64",
            "Int128" => "MessageDataType::Int128",
            "Bool" => "MessageDataType::Bool",
            "String" => "MessageDataType::String",
            "Binary" => "MessageDataType::Binary",
            "NoData" => "MessageDataType::NoData",
            other => panic!("unsupported message data type `{other}`"),
        };
        let element = match required_str(&ty["element"], "kind") {
            "Static" => format!(
                "MessageElement::Static({}, {data_type}, {class})",
                ty["element"]["count"].as_u64().unwrap_or(1)
            ),
            "Dynamic" => format!("MessageElement::Dynamic({data_type}, {class})"),
            other => panic!("unsupported message element kind `{other}`"),
        };
        let reliable = match ty["reliable_mode"].as_str() {
            Some("Ordered") => "ReliableMode::Ordered",
            Some("Unordered") => "ReliableMode::Unordered",
            Some("None") | None if ty["reliable"].as_bool().unwrap_or(false) => {
                "ReliableMode::Ordered"
            }
            Some("None") | None => "ReliableMode::None",
            Some(other) => panic!("unsupported reliable mode `{other}`"),
        };
        let e2e = match ty["e2e_encryption"].as_str().unwrap_or("PreferOff") {
            "PreferOff" | "prefer_off" | "off" | "false" => "E2eEncryptionPolicy::PreferOff",
            "PreferOn" | "prefer_on" | "preferred" | "true" => "E2eEncryptionPolicy::PreferOn",
            "RequireOn" | "require_on" | "required" => "E2eEncryptionPolicy::RequireOn",
            other => panic!("unsupported e2e encryption policy `{other}`"),
        };
        let endpoint_names = ty["endpoints"]
            .as_array()
            .expect("type endpoints must be an array");
        let endpoint_expr = endpoint_names
            .iter()
            .map(|endpoint| {
                let name = endpoint.as_str().expect("type endpoint must be a string");
                let id = endpoint_ids
                    .get(name)
                    .unwrap_or_else(|| panic!("unknown endpoint `{name}`"));
                format!("DataEndpoint({id})")
            })
            .collect::<Vec<_>>()
            .join(", ");
        let priority = ty["priority"].as_u64().unwrap_or(0);
        writeln!(
            generated,
            "    DataTypeDefinition {{ id: DataType({id}), name: {}, description: {}, element: {element}, endpoints: &[{endpoint_expr}], reliable: {reliable}, priority: {priority}, e2e_encryption: {e2e} }},",
            rust_string(name),
            rust_string(description),
        )
        .unwrap();
    }
    generated.push_str("];\n");
    fs::write(output_path, generated).expect("write generated embedded schema");
}

fn main() {
    println!("cargo:rustc-check-cfg=cfg(sedsnet_has_telemetry_config_json)");
    println!("cargo:rerun-if-changed=telemetry_config.json");
    let schema_path = Path::new("telemetry_config.json");
    if schema_path.is_file() {
        println!("cargo:rustc-cfg=sedsnet_has_telemetry_config_json");
    }
    let output_path =
        PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR")).join("embedded_schema.rs");
    generate_embedded_schema(schema_path, &output_path);
    for key in [
        "DEVICE_IDENTIFIER",
        "MAX_RECENT_RX_IDS",
        "STARTING_QUEUE_SIZE",
        "MAX_QUEUE_BUDGET",
        "MAX_QUEUE_SIZE",
        "QUEUE_GROW_STEP",
        "PAYLOAD_COMPRESS_THRESHOLD",
        "STATIC_STRING_LENGTH",
        "STATIC_HEX_LENGTH",
        "STRING_PRECISION",
        "MAX_STACK_PAYLOAD",
        "MAX_HANDLER_RETRIES",
        "RELIABLE_RETRANSMIT_MS",
        "RELIABLE_MAX_RETRIES",
        "RELIABLE_MAX_PENDING",
        "RELIABLE_MAX_RETURN_ROUTES",
        "RELIABLE_MAX_END_TO_END_PENDING",
        "RELIABLE_MAX_END_TO_END_ACK_CACHE",
        "SEDSNET_STATIC_SCHEMA_PATH",
        "SEDSNET_STATIC_IPC_SCHEMA_PATH",
    ] {
        println!("cargo:rerun-if-env-changed={key}");
    }
}