Posted on ::

Recientemente estuve muy metido con rust, desde que conseguí el libro en físico (recomendadisimo para cualquier persona que quiera aprender y no sepa ingles) no pare de hacer pequeños ejercicios, incluso terminar el rustlings no sentí que sea suficiente porque hay todo un mundo que rodea este lenguaje, y nunca voy a terminar de especializarme en cada una de las aplicaciones del mismo.

Es por eso que tomé 3 decisiones:

  1. Iniciar este blog, un paso importante para recordar las habilidades que aprendo en el tiempo.

  2. Aprender inglés, estoy intentando ver las conferencias de Steve Klabnik quien es el autor del libro, así aprendo el idioma viendo cosas que me interesan

  3. Tomar un curso de rust.

Para este último punto busque en Udemy (recomendadisimo por cierto), y acá estamos.

Les presento mi proyecto: Un gateway de telemetria para szensores remotos.

Esto es en sintesis un traductor que escucha en tiempo real a traves del protocolo TCP los sensores, los cuales se comunican en código binario.

El proyecto consta de manejo de memoria mediante bytes crudos, esto en criollo quiere decir que si le erras por un milimetro a una latitud, las coordenadas son erradas y la altura te manda al medio del mar en vez del sitio donde se encuentra el sensor. Además me sirvió para terminar de entender el poder de la concurrencia con tokio.

La consigna era que este sistema simula los sistemas que sirven para la lógistica de camiones de gran escala con mercaderia. Yendo al nivel de producto, entendemos que cada camion tiene un sensor escondido que manda su ubicación y cuánta batería le queda. El sensor manda muy poca info mediante numeros binarios para que la batería le dure meses, y el gateway es el que se encarga de que esa misma info llegue de manera perfecta al centro del control.

Vamonos al código:

1. Los imports

Al principio del archivo traemos las herramientas que vamos a usar. tokio para que todo sea asíncrono, chrono para ponerle fecha y hora y serde para que Rust sepa cómo convertir nuestras estructuras en datos para que otros sistemas entiendan.

use std::net::SocketAddr;
use chrono::{DateTime, Utc};
use serde::Serialize;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};

2. Que forma tiene el struct

Acá defino qué es forma tienen los datos para nosotros. Lo importante es que, aunque el sensor mande números, nosotros debemos transformarlos en algo legible. El #[derive(Debug, Serialize)] es clave para poder imprimir los datos en la consola mientras probamos.

#[derive(Debug, Serialize)]
struct TelemetryFrame {
    timestamp: DateTime<Utc>,
    latitude_deg: f32,
    longitude_deg: f32,
    battery_level: u8,
}

3. El traductor

Esta es la parte más minuciosa para el sistema. El sensor manda 9 bytes y acá es donde los separamos precisamente sin errar

impl TelemetryFrame {
    fn from_bytes(raw: &[u8]) -> Result<Self, String> {
        if raw.len() != 9 {
            return Err(format!("invalid frame length: expected 9 bytes, got {}", raw.len()));
        }

        // Sacamos de los bytes
        let lat_bytes: [u8; 4] = raw[0..4].try_into().map_err(|_| "failed to extract latitude bytes".to_string())?;
        let lon_bytes: [u8; 4] = raw[4..8].try_into().map_err(|_| "failed to extract longitude bytes".to_string())?;

        // Convertimos de Big-Endian (el orden del sensor) a números que entienda nuestra PC
        let lat_microdeg = i32::from_be_bytes(lat_bytes);
        let lon_microdeg = i32::from_be_bytes(lon_bytes);
        let battery_level = raw[8];

        Ok(Self {
            timestamp: Utc::now(),
            latitude_deg: lat_microdeg as f32 / 1_000_000.0,
            longitude_deg: lon_microdeg as f32 / 1_000_000.0,
            battery_level,
        })
    }
}

4. El servidor principal en main

El main es el que pone todo en marcha. Primero definimos en qué dirección vamos a escuchar (puerto 9000). De lo más potente acá es el loop con el tokio::spawn. Cada vez que un camión se conecta (hilo asíncrono) lo dejamos comunicarse tranquilo sin frenar a los demás.

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let bind_addr = std::env::var("TELEMETRY_BIND_ADDR").unwrap_or_else(|_| "0.0.0.0:9000".into());
    let metrics_addr = std::env::var("METRICS_FORWARD_ADDR").unwrap_or_else(|_| "127.0.0.1:9100".into());

    let listener = TcpListener::bind(&bind_addr).await?;

    loop {
        let (socket, peer_addr) = listener.accept().await?;
        let metrics_addr = metrics_addr.clone();

        tokio::spawn(async move {
            if let Err(e) = handle_connection(socket, peer_addr, &metrics_addr).await {
                eprintln!("connection error from {}: {}", peer_addr, e);
            }
        });
    }
}

5. Atendiendo al sensor (handle_connection)

Esta función se encarga de la charla con cada sensor. Usa un BufReader para ir leyendo línea por línea lo que el camión manda en hexadecimal.

async fn handle_connection(socket: TcpStream, peer_addr: SocketAddr, metrics_addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let (reader, _writer) = socket.into_split();
    let mut buf_reader = BufReader::new(reader);
    let mut line = String::new();

    loop {
        line.clear();
        let bytes_read = buf_reader.read_line(&mut line).await?;
        if bytes_read == 0 { break; } // Conexión cerrada

        let hex_str = line.trim();
        if hex_str.is_empty() { continue; }

        match decode_and_parse_frame(hex_str) {
            Ok(frame) => {
                forward_to_metrics(&frame, metrics_addr).await?;
            }
            Err(err) => eprintln!("Error parseando: {}", err),
        }
    }
    Ok(())
}

6. El envío final (forward_to_metrics)

Para terminar, una vez que tenemos los datos procesados, abrimos otra conexión y se los mandamos al servidor central de métricas.

async fn forward_to_metrics(frame: &TelemetryFrame, metrics_addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let mut stream = TcpStream::connect(metrics_addr).await?;
    let line = format!(
        "telemetry lat={:.6},lon={:.6},battery={} timestamp={}\n",
        frame.latitude_deg, frame.longitude_deg, frame.battery_level, frame.timestamp
    );
    stream.write_all(line.as_bytes()).await?;
    stream.flush().await?;
    Ok(())
}

ese fue el último paso de nuestro sistema.