This commit is contained in:
parent
2bd52ec375
commit
a2735194c9
Binary file not shown.
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
After Width: | Height: | Size: 496 KiB |
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
@ -0,0 +1,554 @@
|
|||||||
|
+++
|
||||||
|
title = "Re-implementare un protocollo in Rust"
|
||||||
|
summary = "Step 3: creare una libreria basata sul reverse engineering"
|
||||||
|
date = "2024-08-01"
|
||||||
|
|
||||||
|
tags = ["Libreria", "Lettore di presenze", "TCP", "Rust"]
|
||||||
|
categories = ["Progetti"]
|
||||||
|
series = ["Lettore di presenze"]
|
||||||
|
series_order = 3
|
||||||
|
|
||||||
|
draft = true
|
||||||
|
+++
|
||||||
|
|
||||||
|
Nell'articolo precedente siamo riusciti a comprendere il significato dei
|
||||||
|
pacchetti che vengono scambiati fra il client ufficiale ed il lettore di
|
||||||
|
presenze.
|
||||||
|
|
||||||
|
Rimane solo una cosa da fare: **Riscrivere l'API in Rust!**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Ricreare l'API ufficiale
|
||||||
|
|
||||||
|
Per iniziare [installiamo Rust](https://rust-lang.org/tools/install/) e creiamo
|
||||||
|
un nuovo progetto tramite Cargo, il *package manager* di Rust, con il seguente
|
||||||
|
comando:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo new r701
|
||||||
|
```
|
||||||
|
|
||||||
|
Possiamo poi aprire il progetto col nostro [text editor di
|
||||||
|
fiducia](https://neovim.io/).
|
||||||
|
|
||||||
|
Dato che dobbiamo creare una libreria, andiamo a creare il file `src/lib.rs` e
|
||||||
|
cominciamo a scrivere la struct che descriverà il nostro lettore:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/lib.rs
|
||||||
|
use std::io::Result;
|
||||||
|
use std::net::{TcpStream, ToSocketAddrs};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct R701 {
|
||||||
|
tcp_stream: TcpStream,
|
||||||
|
sequence_number: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl R701 {
|
||||||
|
pub fn connect(connection_info: impl ToSocketAddrs) -> Result<Self> {
|
||||||
|
// Create a new R701 struct
|
||||||
|
let mut new = Self {
|
||||||
|
tcp_stream: TcpStream::connect(connection_info)?,
|
||||||
|
sequence_number: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to ping the endpoint
|
||||||
|
new.ping()?;
|
||||||
|
Ok(new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La nostra struct contiene due campi:
|
||||||
|
|
||||||
|
* `tcp_stream`, che contiene il descrittore della connessione al nostro
|
||||||
|
lettore;
|
||||||
|
* `sequence_number`, che memorizza il numero dell'ultimo pacchetto inviato.
|
||||||
|
|
||||||
|
Per provare se la nostra struct si connette correttamente possiamo modificare
|
||||||
|
il file `src/main.rs` in modo che si connetta al nostro endpoint:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/main.rs
|
||||||
|
use r701::R701;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let r701 = R701::connect("127.0.0.1:5005").unwrap();
|
||||||
|
println!("{:?}", r701);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Se adesso eseguiamo `cargo run`...
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Urrà!** Il nostro client si connette con successo al server TCP!
|
||||||
|
|
||||||
|
Il prossimo step sarà quello di utilizzare la libreria
|
||||||
|
[std::net::TcpStream](https://doc.rust-lang.org/std/net/struct.TcpStream.html)
|
||||||
|
per andare ad eseguire le query che abbiamo ricavato dal nostro tentativo di
|
||||||
|
[reverse
|
||||||
|
engineering](/it/posts/2024/05/studying-a-communication-protocol)
|
||||||
|
e ottenere ed elaborare le risposte.
|
||||||
|
|
||||||
|
Dato che [tutte le richieste hanno una struttura
|
||||||
|
standard](/it/posts/2024/05/studying-a-communication-protocol#richieste),
|
||||||
|
possiamo andare a creare un metodo che prende in input il payload di una
|
||||||
|
richiesta (rappresentata da una slice di 12 `u8`) e ritorni un `Vec<u8>`
|
||||||
|
contenente la risposta:
|
||||||
|
|
||||||
|
```rust { hl_lines=["6-23"] }
|
||||||
|
// src/lib.rs
|
||||||
|
|
||||||
|
impl R701 {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
pub fn request(&mut self, payload: &[u8; 12]) -> Result<Vec<u8>> {
|
||||||
|
// Create a blank request
|
||||||
|
let mut request = [0x55, 0xaa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
|
||||||
|
// Insert the payload
|
||||||
|
request[2..14].clone_from_slice(payload);
|
||||||
|
|
||||||
|
// Insert the sequence number
|
||||||
|
request[14..].clone_from_slice(&self.sequence_number.to_le_bytes());
|
||||||
|
self.sequence_number += 1;
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
self.tcp_stream.write_all(&request)?;
|
||||||
|
|
||||||
|
// Create a buffer and return the response
|
||||||
|
let mut buffer = BufReader::new(&self.tcp_stream);
|
||||||
|
Ok(buffer.fill_buf()?.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Possiamo verificare che tutto funzioni correttamente inviando un [pacchetto di
|
||||||
|
ping](/it/posts/2024/05/studying-a-communication-protocol#ping) e aspettandoci
|
||||||
|
la risposta corretta:
|
||||||
|
|
||||||
|
```rust { hl_lines=["7-10"] }
|
||||||
|
// src/main.rs
|
||||||
|
use r701::R701;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let r701 = R701::connect("127.0.0.1:5005").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
r701.request(&[0x01, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap(),
|
||||||
|
[0xaa, 0x55, 0x01, 0x01, 0, 0, 0, 0, 0, 0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Potremmo addirittura rendere il ping un metodo a se stante nella nostra struct:
|
||||||
|
|
||||||
|
```rust { hl_lines=["6-16"] }
|
||||||
|
// src/lib.rs
|
||||||
|
|
||||||
|
impl R701 {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
pub fn ping(&mut self) -> Result<()> {
|
||||||
|
// Create a request with a payload of `01 80 00 00 00 00 00 00 00 00 00 00`
|
||||||
|
let response = self.request(&[0x01, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])?;
|
||||||
|
|
||||||
|
// If the response is not `aa 55 01 01 00 00 00 00 00 00` then return an error
|
||||||
|
if response != [0xaa, 0x55, 0x01, 0x01, 0, 0, 0, 0, 0, 0] {
|
||||||
|
return Err(Error::new(InvalidData, "Malformed response"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Con questo metodo possiamo andare a creare anche i metodi per ottenere il [nome
|
||||||
|
di un
|
||||||
|
dipendente](/it/posts/2024/05/studying-a-communication-protocol#nome-del-dipendente),
|
||||||
|
il [numero totale di
|
||||||
|
presenze](/it/posts/2024/05/studying-a-communication-protocol#numero-totale-di-presenze)
|
||||||
|
ed un [blocco di
|
||||||
|
presenze](/it/posts/2024/05/studying-a-communication-protocol#scaricamento-di-tutte-le-presenze).
|
||||||
|
|
||||||
|
Se siete interessati, tutto il codice sorgente è già presente su
|
||||||
|
[nicolabelluti/r701](https://git.nicolabelluti.me/nicolabelluti/r701/src/branch/main/src/r701.rs).
|
||||||
|
|
||||||
|
## Estrarre le presenze tramite il trait TryInto
|
||||||
|
|
||||||
|
Una volta creato il metodo che ci permette di estrarre un blocco di presenze,
|
||||||
|
bisogna trovare il modo idiomatico per trasformarlo da un array di byte a una
|
||||||
|
struct che rappresenti una singola presenza.
|
||||||
|
|
||||||
|
Per iniziare facciamo un po' di *refactoring* rinominando `src/lib.rs` in
|
||||||
|
`src/r701.rs` e creando un nuovo `src/lib.rs` contenente queste righe:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/lib.rs
|
||||||
|
mod r701;
|
||||||
|
pub use r701::R701;
|
||||||
|
```
|
||||||
|
|
||||||
|
In questo modo l'interfaccia esterna della nostra libreria non cambierà, però
|
||||||
|
così facendo possiamo organizzare il nostro codice in diversi file.
|
||||||
|
|
||||||
|
Aggiungiamo il file `src/record.rs` e includiamolo in `src/lib.rs`
|
||||||
|
|
||||||
|
```rust { hl_lines=[3,6] }
|
||||||
|
// src/lib.rs
|
||||||
|
mod r701;
|
||||||
|
mod record;
|
||||||
|
|
||||||
|
pub use r701::R701;
|
||||||
|
pub use record::{Record, Clock};
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/record.rs
|
||||||
|
use chrono::{DateTime, Local, TimeZone};
|
||||||
|
|
||||||
|
pub enum Clock {
|
||||||
|
FirstIn,
|
||||||
|
FirstOut,
|
||||||
|
SecondIn,
|
||||||
|
SecondOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Record {
|
||||||
|
pub employee_id: u32,
|
||||||
|
pub clock: Clock,
|
||||||
|
pub datetime: DateTime<Local>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Con questo codice abbiamo definito la struttura di una presenza che, come
|
||||||
|
abbiamo detto nell'articolo precedente, è composta dall'ID del dipendente,
|
||||||
|
dalla data e dall'ora alla quale è stata registrata e dallo stato (se è la
|
||||||
|
prima entrata, la prima uscita, la seconda entrata o la seconda uscita).
|
||||||
|
|
||||||
|
Dato che [non vogliamo impazzire gestendo il
|
||||||
|
tempo](https://www.youtube.com/watch?v=-5wpm-gesOY), andiamo ad importare il
|
||||||
|
*crate* [chrono](https://crates.io/crates/chrono/) per la gestione delle date:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo add chrono --no-default-features --features clock
|
||||||
|
```
|
||||||
|
|
||||||
|
Per facilitare la conversione da un vettore di byte alla nostra struct `Record`
|
||||||
|
possiamo implementare il trait
|
||||||
|
[TryInto](https://doc.rust-lang.org/std/convert/trait.TryInto.html):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/record.rs
|
||||||
|
impl TryFrom<&[u8]> for Record {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
fn try_from(record_bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Il codice finito è disponibile
|
||||||
|
[qua](https://git.nicolabelluti.me/nicolabelluti/r701/src/branch/main/src/record.rs#L32).
|
||||||
|
|
||||||
|
Possiamo testare se la conversione è corretta tramite un semplice test:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/record.rs
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn valid_record_conversion() {
|
||||||
|
let record_bytes: &[u8] = &[0x10, 0x23, 0x0b, 0x1d, 0x01, 0, 0, 0, 0xb2, 0x17, 0x01, 0];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
record_bytes.try_into(),
|
||||||
|
Ok(Record {
|
||||||
|
employee_id: 1,
|
||||||
|
clock: Clock::FirstIn,
|
||||||
|
datetime: Local.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).single().unwrap(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unire il tutto tramite gli iteratori
|
||||||
|
|
||||||
|
Una volta trovato un modo per estrarre dei byte dal dispositivo ed un modo per
|
||||||
|
convertirli in una struct, dobbiamo trovare il modo idiomatico per mettere
|
||||||
|
insieme le due cose, ed è proprio qua che entrano in gioco gli iteratori.
|
||||||
|
|
||||||
|
Per implemetare il trait
|
||||||
|
[Iterator](https://doc.rust-lang.org/std/iter/trait.Iterator.html) bisogna
|
||||||
|
definire solo il metodo `next()` che, partendo dal primo elemento, ritorna
|
||||||
|
l'elemento successivo.
|
||||||
|
|
||||||
|
Una volta definito questo metodo avremmo accesso a molti altri strumenti, come
|
||||||
|
[map()](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.map),
|
||||||
|
[filter()](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter),
|
||||||
|
[fold()](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.fold) e,
|
||||||
|
se andiamo ad importare il *crate*
|
||||||
|
[itertools](https://crates.io/crates/itertools), anche
|
||||||
|
[sorted()](https://docs.rs/itertools/0.12.1/itertools/trait.Itertools.html#method.sorted)
|
||||||
|
e
|
||||||
|
[into_group_map_by()](https://docs.rs/itertools/0.12.1/itertools/trait.Itertools.html#method.into_group_map_by),
|
||||||
|
giusto per elencarne alcuni.
|
||||||
|
|
||||||
|
Come prima cosa andiamo a creare una nuova struct `RecordIterator` con un
|
||||||
|
costruttore `from()` che ci permetta di generare un iteratore prendendo in
|
||||||
|
input una *reference* mutabile ad una struct `R701`:
|
||||||
|
|
||||||
|
```rust { hl_lines=[4,8] }
|
||||||
|
// src/lib.rs
|
||||||
|
mod r701;
|
||||||
|
mod record;
|
||||||
|
mod record_iterator;
|
||||||
|
|
||||||
|
pub use r701::R701;
|
||||||
|
pub use record::{Record, Clock};
|
||||||
|
pub use record_iterator::RecordIterator;
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/record_iterator.rs
|
||||||
|
use crate::R701;
|
||||||
|
use std::io::Result;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RecordIterator<'a> {
|
||||||
|
r701: &'a mut R701,
|
||||||
|
input_buffer: Vec<u8>,
|
||||||
|
sequence_number: u16,
|
||||||
|
total_records: u16,
|
||||||
|
record_count: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> RecordIterator<'a> {
|
||||||
|
pub fn from(r701: &'a mut R701) -> Result<Self> {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Il metodo `from()` richiede al lettore il numero totale di timbrate ed il primo
|
||||||
|
blocco di presenze , salvandoli rispettivamente nella variabile `total_records`
|
||||||
|
e nel vettore `input_buffer`.
|
||||||
|
|
||||||
|
Il metodo `next()` del trait `Iterator` andrà poi a prendere i primi 12 byte
|
||||||
|
dell'`input buffer` e li trasformerà in una struct `Record` tramite il trait
|
||||||
|
`TryInto` che abbiamo implementato nel capitolo precedente.
|
||||||
|
|
||||||
|
Quando `input_buffer` è vuoto allora viene richiesto al lettore un'altro blocco
|
||||||
|
di presenze, fino a che non vengono lette tutte.
|
||||||
|
|
||||||
|
Se siete interessati tutto il codice è già [disponibile su
|
||||||
|
Git](https://git.nicolabelluti.me/nicolabelluti/r701/src/branch/main/src/record_iterator.rs).
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/record_iterator.rs
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
impl<'a> Iterator for RecordIterator<'a> {
|
||||||
|
type Item = Record;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Giusto per completezza possiamo implementare un metodo `into_record_iter` nella
|
||||||
|
struct `R701`, per semplificare l'utilizzo dell'iteratore:
|
||||||
|
|
||||||
|
```rust { hl_lines=[2,"9-11"] }
|
||||||
|
// src/r701.rs
|
||||||
|
use crate::RecordIterator;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
impl R701 {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
pub fn into_record_iter(&mut self) -> Result<RecordIterator> {
|
||||||
|
RecordIterator::from(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rendere il tutto *Blazingly Fast*
|
||||||
|
|
||||||
|
Come prima cosa andiamo a creare un main che crei un file con la stessa
|
||||||
|
struttura del file `AGLog_001.txt` che abbiamo visto nel [primo
|
||||||
|
capitolo](/it/posts/2024/04/reverse-engineering-an-attendance-reader/#dump-delle-registrazioni-tramite-usb)
|
||||||
|
di questa serie:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/main.rs
|
||||||
|
use r701::R701;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut r701 = R701::connect("127.0.0.1:5005").unwrap();
|
||||||
|
|
||||||
|
println!("No\tMchn\tEnNo\t\tName\t\tMode\tIOMd\tDateTime\t");
|
||||||
|
r701.into_record_iter()
|
||||||
|
.unwrap()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.for_each(|(id, record)| {
|
||||||
|
let name = r701
|
||||||
|
.get_name(record.employee_id)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap_or(format!("user #{}", record.employee_id));
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{:0>6}\t{}\t{:0>9}\t{: <10}\t{}\t{}\t{}",
|
||||||
|
id + 1,
|
||||||
|
1,
|
||||||
|
record.employee_id,
|
||||||
|
name,
|
||||||
|
35,
|
||||||
|
record.clock as u8,
|
||||||
|
record.datetime.format("%Y/%m/%d %H:%M:%S"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Già con questo `main()` riusciamo ad ottenere tutti i record in un po' meno di
|
||||||
|
un minuto, che è la metà del tempo che impiega il [client closed source
|
||||||
|
ufficiale](/it/posts/2024/05/studying-a-communication-protocol/#configurazione-del-client).
|
||||||
|
|
||||||
|
Certo, stiamo leggermente barando dato che il nostro client non riesce ad
|
||||||
|
estrarre l'ID del registratore, la modalità di registrazione della presenza ed
|
||||||
|
i secondi del campo `DateTime`, ma per il momento possiamo ignorarli dato che
|
||||||
|
sono campi superflui.
|
||||||
|
|
||||||
|
### Memoizzare il nome dei dipendenti
|
||||||
|
|
||||||
|
Per velocizzare ancora di più le cose potremmo evitare di chiedere al tibratore
|
||||||
|
il nome dei dipendente per ogni record.
|
||||||
|
|
||||||
|
Possiamo creare un `HashMap` di nomi e, per ogni record, verificare se il nome
|
||||||
|
è già presente al suo interno. Se no, allora si può chiedere al timbratore il
|
||||||
|
nome del dipendente per poi salvarlo all'interno dell `HashMap`.
|
||||||
|
|
||||||
|
In questo modo andiamo a ridurre il numero di richieste al minimo
|
||||||
|
indispensabile.
|
||||||
|
|
||||||
|
```rust { hl_lines=[3,6,"16-20"] }
|
||||||
|
// src/main.rs
|
||||||
|
use r701::R701;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut names = HashMap::new();
|
||||||
|
let mut r701 = R701::connect("127.0.0.1:5005").unwrap();
|
||||||
|
|
||||||
|
println!("No\tMchn\tEnNo\t\tName\t\tMode\tIOMd\tDateTime\t");
|
||||||
|
r701.into_record_iter()
|
||||||
|
.unwrap()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.for_each(|(id, record)| {
|
||||||
|
let name = names.entry(record.employee_id).or_insert_with(|| {
|
||||||
|
r701.get_name(record.employee_id)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap_or(format!("user #{}", record.employee_id))
|
||||||
|
});
|
||||||
|
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Con questa semplice modifica passiamo da ottenere tutti i record in un minuto
|
||||||
|
ad ottenerli in **un secondo**. Questo sì che è *blazingly fast*!
|
||||||
|
|
||||||
|
### Limitare la lettura delle presenze ad un certo arco temporale
|
||||||
|
|
||||||
|
Dato che mi interessano i dati dell'ultimo mese, possiamo utilizzare i metodi
|
||||||
|
[take_while()](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.take_while)
|
||||||
|
e
|
||||||
|
[skip_while()](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.skip_while)
|
||||||
|
per escludere tutti gli elementi precedenti allo scorso mese e per fermare
|
||||||
|
l'iteratore una volta estratti tutti i record interessati:
|
||||||
|
|
||||||
|
```rust { hl_lines=[4,"7-8","16-17"] }
|
||||||
|
// src/main.rs
|
||||||
|
use r701::R701;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use chrono::{Local, TimeZone};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let start = Local.with_ymd_and_hms(2024, 7, 1, 0, 0, 0).unwrap();
|
||||||
|
let end = Local.with_ymd_and_hms(2024, 8, 1, 0, 0, 0).unwrap();
|
||||||
|
let mut names = HashMap::new();
|
||||||
|
|
||||||
|
let mut r701 = R701::connect("127.0.0.1:5005").unwrap();
|
||||||
|
|
||||||
|
println!("No\tMchn\tEnNo\t\tName\t\tMode\tIOMd\tDateTime\t");
|
||||||
|
r701.into_record_iter()
|
||||||
|
.unwrap()
|
||||||
|
.take_while(|record| record.datetime < end)
|
||||||
|
.skip_while(|record| record.datetime < start)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.for_each(|(id, record)| {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Questa modifica non migliora in alcun modo le performance, ma c'è un ultima
|
||||||
|
miglioria molto semplice che possiamo applicare per questo specifico caso
|
||||||
|
d'uso...
|
||||||
|
|
||||||
|
### Leggere le presenze al contrario
|
||||||
|
|
||||||
|
Al posto di iniziare dal primo record mai registrato ed escludere tutti i
|
||||||
|
record fino ad arrivare al primo del mese interessato potremmo leggere i record
|
||||||
|
al contrario, partendo da quello più recente ed andando verso a quello più
|
||||||
|
datato.
|
||||||
|
|
||||||
|
Questa miglioria richiede [un po' di
|
||||||
|
modifiche](https://git.nicolabelluti.me/nicolabelluti/r701/compare/f0ac5fe7..0dd05c0d#diff-44adb0ed617220e3fd4a4bbb2e361059ac47d9c4),
|
||||||
|
ma ne vale la pena considerando che ci fa passare da un po' meno di un secondo
|
||||||
|
a **0,2 secondi**!
|
||||||
|
|
||||||
|
```rust { hl_lines=["11-12",15] }
|
||||||
|
// src/main.rs
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
println!("No\tMchn\tEnNo\t\tName\t\tMode\tIOMd\tDateTime\t");
|
||||||
|
r701.into_record_iter()
|
||||||
|
.unwrap()
|
||||||
|
.take_while(|record| record.datetime >= start)
|
||||||
|
.skip_while(|record| record.datetime >= end)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.enumerate()
|
||||||
|
.for_each(|(id, record)| {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
}
|
||||||
|
``
|
@ -0,0 +1,545 @@
|
|||||||
|
+++
|
||||||
|
title = "Re-implementing a protocol in Rust"
|
||||||
|
summary = "Setp 3: Creating a library based on reverse engineering"
|
||||||
|
date = "2024-08-01"
|
||||||
|
|
||||||
|
tags = ["Library", "Attendance Reader", "TCP", "Rust"]
|
||||||
|
categories = ["Projects"]
|
||||||
|
series = ["Attendance Reader"]
|
||||||
|
series_order = 3
|
||||||
|
|
||||||
|
draft = true
|
||||||
|
+++
|
||||||
|
|
||||||
|
In the previous article, we managed to understand the meaning of the packets
|
||||||
|
exchanged between the official client and the attendance reader.
|
||||||
|
|
||||||
|
There is only one thing left to do: **Rewrite the API in Rust!**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Recreating the Official API
|
||||||
|
|
||||||
|
To start, let's [install Rust](https://rust-lang.org/tools/install/) and create
|
||||||
|
a new project using Cargo, Rust's package manager, using the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo new r701
|
||||||
|
```
|
||||||
|
|
||||||
|
We can then open the project with our [text editor of
|
||||||
|
choice](https://neovim.io/).
|
||||||
|
|
||||||
|
Since we need to create a library, let's create the file `src/lib.rs` and start
|
||||||
|
writing the struct that will describe our reader:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/lib.rs
|
||||||
|
use std::io::Result;
|
||||||
|
use std::net::{TcpStream, ToSocketAddrs};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct R701 {
|
||||||
|
tcp_stream: TcpStream,
|
||||||
|
sequence_number: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl R701 {
|
||||||
|
pub fn connect(connection_info: impl ToSocketAddrs) -> Result<Self> {
|
||||||
|
// Create a new R701 struct
|
||||||
|
let mut new = Self {
|
||||||
|
tcp_stream: TcpStream::connect(connection_info)?,
|
||||||
|
sequence_number: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to ping the endpoint
|
||||||
|
new.ping()?;
|
||||||
|
Ok(new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our struct contains two fields:
|
||||||
|
|
||||||
|
* `tcp_stream`, which contains the descriptor of the connection to our reader;
|
||||||
|
* `sequence_number`, which stores the number of the last packet sent.
|
||||||
|
|
||||||
|
To test if our struct connects correctly, we can modify the file `src/main.rs`
|
||||||
|
so that it connects to our endpoint:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/main.rs
|
||||||
|
use r701::R701;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let r701 = R701::connect("127.0.0.1:5005").unwrap();
|
||||||
|
println!("{:?}", r701);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If we now run `cargo run`...
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Hurray!** Our client successfully connects to the TCP server!
|
||||||
|
|
||||||
|
The next step will be to use the library
|
||||||
|
[std::net::TcpStream](https://doc.rust-lang.org/std/net/struct.TcpStream.html)
|
||||||
|
to execute the queries we derived from our attempt at [reverse
|
||||||
|
engineering](/posts/2024/05/studying-a-communication-protocol) and obtain and
|
||||||
|
process the responses.
|
||||||
|
|
||||||
|
Since [all requests have a standard
|
||||||
|
structure](/posts/2024/05/studying-a-communication-protocol#requests), we can
|
||||||
|
create a method that takes as input the payload of a request (represented by a
|
||||||
|
slice of 12 `u8`) and returns a `Vec<u8>` containing the response:
|
||||||
|
|
||||||
|
```rust { hl_lines=["6-23"] }
|
||||||
|
// src/lib.rs
|
||||||
|
|
||||||
|
impl R701 {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
pub fn request(&mut self, payload: &[u8; 12]) -> Result<Vec<u8>> {
|
||||||
|
// Create a blank request
|
||||||
|
let mut request = [0x55, 0xaa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
|
||||||
|
// Insert the payload
|
||||||
|
request[2..14].clone_from_slice(payload);
|
||||||
|
|
||||||
|
// Insert the sequence number
|
||||||
|
request[14..].clone_from_slice(&self.sequence_number.to_le_bytes());
|
||||||
|
self.sequence_number += 1;
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
self.tcp_stream.write_all(&request)?;
|
||||||
|
|
||||||
|
// Create a buffer and return the response
|
||||||
|
let mut buffer = BufReader::new(&self.tcp_stream);
|
||||||
|
Ok(buffer.fill_buf()?.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We can verify that everything works correctly by sending a [ping
|
||||||
|
packet](/posts/2024/05/studying-a-communication-protocol#ping)
|
||||||
|
and expecting the correct response:
|
||||||
|
|
||||||
|
```rust { hl_lines=["7-10"] }
|
||||||
|
// src/main.rs
|
||||||
|
use r701::R701;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let r701 = R701::connect("127.0.0.1:5005").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
r701.request(&[0x01, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap(),
|
||||||
|
[0xaa, 0x55, 0x01, 0x01, 0, 0, 0, 0, 0, 0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We could even make ping a method in our struct:
|
||||||
|
|
||||||
|
```rust { hl_lines=["6-16"] }
|
||||||
|
// src/lib.rs
|
||||||
|
|
||||||
|
impl R701 {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
pub fn ping(&mut self) -> Result<()> {
|
||||||
|
// Create a request with a payload of `01 80 00 00 00 00 00 00 00 00 00 00`
|
||||||
|
let response = self.request(&[0x01, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])?;
|
||||||
|
|
||||||
|
// If the response is not `aa 55 01 01 00 00 00 00 00 00` then return an error
|
||||||
|
if response != [0xaa, 0x55, 0x01, 0x01, 0, 0, 0, 0, 0, 0] {
|
||||||
|
return Err(Error::new(InvalidData, "Malformed response"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this way we can also create methods to obtain the [name of an
|
||||||
|
employee](/posts/2024/05/studying-a-communication-protocol#employee-name), the
|
||||||
|
[total number of
|
||||||
|
records](/posts/2024/05/studying-a-communication-protocol#total-number-of-records),
|
||||||
|
and a [block of
|
||||||
|
records](/posts/2024/05/studying-a-communication-protocol#downloading-all-records).
|
||||||
|
|
||||||
|
If you are interested, all the source code is already available at
|
||||||
|
[nicolabelluti/r701](https://git.nicolabelluti.me/nicolabelluti/r701/src/branch/main/src/r701.rs).
|
||||||
|
|
||||||
|
## Extracting Attendances via the TryInto Trait
|
||||||
|
|
||||||
|
Once we have created the method that allows us to extract a block of
|
||||||
|
attendances, we need to find the idiomatic way to transform it from an array of
|
||||||
|
bytes to a struct that represents a single attendance.
|
||||||
|
|
||||||
|
To start, let's do some *refactoring* by renaming `src/lib.rs` to `src/r701.rs`
|
||||||
|
and creating a new `src/lib.rs` containing these lines:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/lib.rs
|
||||||
|
mod r701;
|
||||||
|
pub use r701::R701;
|
||||||
|
```
|
||||||
|
|
||||||
|
This way, the external interface of our library will not change, but we can
|
||||||
|
organize our code into different files.
|
||||||
|
|
||||||
|
Let's add the file `src/record.rs` and include it in `src/lib.rs`
|
||||||
|
|
||||||
|
```rust { hl_lines=[3,6] }
|
||||||
|
// src/lib.rs
|
||||||
|
mod r701;
|
||||||
|
mod record;
|
||||||
|
|
||||||
|
pub use r701::R701;
|
||||||
|
pub use record::{Record, Clock};
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/record.rs
|
||||||
|
use chrono::{DateTime, Local, TimeZone};
|
||||||
|
|
||||||
|
pub enum Clock {
|
||||||
|
FirstIn,
|
||||||
|
FirstOut,
|
||||||
|
SecondIn,
|
||||||
|
SecondOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Record {
|
||||||
|
pub employee_id: u32,
|
||||||
|
pub clock: Clock,
|
||||||
|
pub datetime: DateTime<Local>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With this code, we have defined the structure of a record, which, as we
|
||||||
|
mentioned in the previous article, consists of the employee ID, the date and
|
||||||
|
time it was recorded, and the state (whether it is the first entry, the first
|
||||||
|
exit, the second entry, or the second exit).
|
||||||
|
|
||||||
|
Since we don't want to [go crazy managing
|
||||||
|
time](https://www.youtube.com/watch?v=-5wpm-gesOY), let's import the
|
||||||
|
[chrono](https://crates.io/crates/chrono/) *crate* for date management:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo add chrono --no-default-features --features clock
|
||||||
|
```
|
||||||
|
|
||||||
|
To facilitate the conversion from a byte vector to our `Record` struct, we can
|
||||||
|
implement the
|
||||||
|
[TryInto](https://doc.rust-lang.org/std/convert/trait.TryInto.html) trait:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/record.rs
|
||||||
|
impl TryFrom<&[u8]> for Record {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
fn try_from(record_bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The finished code is available
|
||||||
|
[here](https://git.nicolabelluti.me/nicolabelluti/r701/src/branch/main/src/record.rs#L32).
|
||||||
|
|
||||||
|
We can test if the conversion is correct through a simple test:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/record.rs
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn valid_record_conversion() {
|
||||||
|
let record_bytes: &[u8] = &[0x10, 0x23, 0x0b, 0x1d, 0x01, 0, 0, 0, 0xb2, 0x17, 0x01, 0];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
record_bytes.try_into(),
|
||||||
|
Ok(Record {
|
||||||
|
employee_id: 1,
|
||||||
|
clock: Clock::FirstIn,
|
||||||
|
datetime: Local.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).single().unwrap(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Putting It All Together with Iterators
|
||||||
|
|
||||||
|
Once we have found a way to extract bytes from the device and a way to convert
|
||||||
|
them into a struct, we need to find the idiomatic way to combine the two, and
|
||||||
|
this is where iterators come into play.
|
||||||
|
|
||||||
|
To implement the
|
||||||
|
[Iterator](https://doc.rust-lang.org/std/iter/trait.Iterator.html) trait, we
|
||||||
|
only need to define the `next()` method, which, starting from the first
|
||||||
|
element, returns the next element.
|
||||||
|
|
||||||
|
Once this method is defined, we will have access to many other tools, such as
|
||||||
|
[map()](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.map),
|
||||||
|
[filter()](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter),
|
||||||
|
[fold()](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.fold),
|
||||||
|
and, if we import the [itertools](https://crates.io/crates/itertools) *crate*,
|
||||||
|
also
|
||||||
|
[sorted()](https://docs.rs/itertools/0.12.1/itertools/trait.Itertools.html#method.sorted)
|
||||||
|
and
|
||||||
|
[into_group_map_by()](https://docs.rs/itertools/0.12.1/itertools/trait.Itertools.html#method.into_group_map_by),
|
||||||
|
just to name a few.
|
||||||
|
|
||||||
|
First, let's create a new struct `RecordIterator` with a `from()` constructor
|
||||||
|
that allows us to generate an iterator by taking a mutable reference to an
|
||||||
|
`R701` struct as input:
|
||||||
|
|
||||||
|
```rust { hl_lines=[4,8] }
|
||||||
|
// src/lib.rs
|
||||||
|
mod r701;
|
||||||
|
mod record;
|
||||||
|
mod record_iterator;
|
||||||
|
|
||||||
|
pub use r701::R701;
|
||||||
|
pub use record::{Record, Clock};
|
||||||
|
pub use record_iterator::RecordIterator;
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/record_iterator.rs
|
||||||
|
use crate::R701;
|
||||||
|
use std::io::Result;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RecordIterator<'a> {
|
||||||
|
r701: &'a mut R701,
|
||||||
|
input_buffer: Vec<u8>,
|
||||||
|
sequence_number: u16,
|
||||||
|
total_records: u16,
|
||||||
|
record_count: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> RecordIterator<'a> {
|
||||||
|
pub fn from(r701: &'a mut R701) -> Result<Self> {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `from()` method requires the reader to provide the total number of
|
||||||
|
timestamps and the first block of attendances, saving them respectively in the
|
||||||
|
`total_records` variable and the `input_buffer` vector.
|
||||||
|
|
||||||
|
The `next()` method of the `Iterator` trait will then take the first 12 bytes
|
||||||
|
from the `input_buffer` and transform them into a `Record` struct using the
|
||||||
|
`TryInto` trait that we implemented in the previous chapter.
|
||||||
|
|
||||||
|
When the `input_buffer` is empty, the reader is requested for another block of
|
||||||
|
attendances until all are read.
|
||||||
|
|
||||||
|
If you are interested, all the code is already [available on
|
||||||
|
Git](https://git.nicolabelluti.me/nicolabelluti/r701/src/branch/main/src/record_iterator.rs).
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/record_iterator.rs
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
impl<'a> Iterator for RecordIterator<'a> {
|
||||||
|
type Item = Record;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Just for completeness, we can implement an `into_record_iter` method in the
|
||||||
|
`R701` struct to simplify the use of the iterator:
|
||||||
|
|
||||||
|
```rust { hl_lines=[2,"9-11"] }
|
||||||
|
// src/r701.rs
|
||||||
|
use crate::RecordIterator;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
impl R701 {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
pub fn into_record_iter(&mut self) -> Result<RecordIterator> {
|
||||||
|
RecordIterator::from(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Making Everything *Blazingly Fast*
|
||||||
|
|
||||||
|
First, let's create a main that creates a file with the same structure as the
|
||||||
|
`AGLog_001.txt` file we saw in the [first
|
||||||
|
chapter](/posts/2024/04/reverse-engineering-an-attendance-reader/#dumping-the-records-via-usb)
|
||||||
|
of this series:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/main.rs
|
||||||
|
use r701::R701;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut r701 = R701::connect("127.0.0.1:5005").unwrap();
|
||||||
|
|
||||||
|
println!("No\tMchn\tEnNo\t\tName\t\tMode\tIOMd\tDateTime\t");
|
||||||
|
r701.into_record_iter()
|
||||||
|
.unwrap()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.for_each(|(id, record)| {
|
||||||
|
let name = r701
|
||||||
|
.get_name(record.employee_id)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap_or(format!("user #{}", record.employee_id));
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{:0>6}\t{}\t{:0>9}\t{: <10}\t{}\t{}\t{}",
|
||||||
|
id + 1,
|
||||||
|
1,
|
||||||
|
record.employee_id,
|
||||||
|
name,
|
||||||
|
35,
|
||||||
|
record.clock as u8,
|
||||||
|
record.datetime.format("%Y/%m/%d %H:%M:%S"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With this `main()`, we can obtain all the records in just under a minute, which
|
||||||
|
is half the time taken by the [official closed-source
|
||||||
|
client](/posts/2024/05/studying-a-communication-protocol/#client-configuration).
|
||||||
|
|
||||||
|
We are slightly cheating, as our client cannot extract the ID of the recorder,
|
||||||
|
the attendance recording method, and the seconds of the `DateTime` field, but
|
||||||
|
for now we can ignore them as they are superfluous fields.
|
||||||
|
|
||||||
|
### Memoizing Employee Names
|
||||||
|
|
||||||
|
To speed things up even more, we could avoid asking the reader for the name of
|
||||||
|
the employee for each record.
|
||||||
|
|
||||||
|
We can create a `HashMap` of names and, for each record, check if the name is
|
||||||
|
already present in it. If not, we can ask the reader for the employee's name
|
||||||
|
and then save it in the `HashMap`.
|
||||||
|
|
||||||
|
This way, we reduce the number of requests to the minimum required.
|
||||||
|
|
||||||
|
```rust { hl_lines=[3,6,"16-20"] }
|
||||||
|
// src/main.rs
|
||||||
|
use r701::R701;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut names = HashMap::new();
|
||||||
|
let mut r701 = R701::connect("127.0.0.1:5005").unwrap();
|
||||||
|
|
||||||
|
println!("No\tMchn\tEnNo\t\tName\t\tMode\tIOMd\tDateTime\t");
|
||||||
|
r701.into_record_iter()
|
||||||
|
.unwrap()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.for_each(|(id, record)| {
|
||||||
|
let name = names.entry(record.employee_id).or_insert_with(|| {
|
||||||
|
r701.get_name(record.employee_id)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap_or(format!("user #{}", record.employee_id))
|
||||||
|
});
|
||||||
|
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With this simple modification, we go from obtaining all records in a minute to
|
||||||
|
obtaining them in **one second**. Now that is *blazingly fast*!
|
||||||
|
|
||||||
|
### Limiting Attendance Reading to a Certain Time Frame
|
||||||
|
|
||||||
|
Since I am interested in the data from the last month, we can use the
|
||||||
|
[take_while()](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.take_while)
|
||||||
|
and
|
||||||
|
[skip_while()](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.skip_while)
|
||||||
|
methods to exclude all elements prior to last month and to stop the iterator
|
||||||
|
once all relevant records have been extracted:
|
||||||
|
|
||||||
|
```rust { hl_lines=[4,"7-8","16-17"] }
|
||||||
|
// src/main.rs
|
||||||
|
use r701::R701;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use chrono::{Local, TimeZone};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let start = Local.with_ymd_and_hms(2024, 7, 1, 0, 0, 0).unwrap();
|
||||||
|
let end = Local.with_ymd_and_hms(2024, 8, 1, 0, 0, 0).unwrap();
|
||||||
|
let mut names = HashMap::new();
|
||||||
|
|
||||||
|
let mut r701 = R701::connect("127.0.0.1:5005").unwrap();
|
||||||
|
|
||||||
|
println!("No\tMchn\tEnNo\t\tName\t\tMode\tIOMd\tDateTime\t");
|
||||||
|
r701.into_record_iter()
|
||||||
|
.unwrap()
|
||||||
|
.take_while(|record| record.datetime < end)
|
||||||
|
.skip_while(|record| record.datetime < start)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.for_each(|(id, record)| {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This modification does not improve performance in any way, but there is one
|
||||||
|
last very simple improvement we can apply for this specific use case...
|
||||||
|
|
||||||
|
### Reading Records in Reverse
|
||||||
|
|
||||||
|
Instead of starting from the first record ever registered and excluding all
|
||||||
|
records until we reach the first of the month we're interested in, we could
|
||||||
|
read the records in reverse, starting from the most recent one and going back
|
||||||
|
to the oldest.
|
||||||
|
|
||||||
|
This improvement requires [a few
|
||||||
|
modifications](https://git.nicolabelluti.me/nicolabelluti/r701/compare/f0ac5fe7..0dd05c0d#diff-44adb0ed617220e3fd4a4bbb2e361059ac47d9c4),
|
||||||
|
but it is worth it considering that it reduces the time from just under a
|
||||||
|
second to **0.2 seconds**!
|
||||||
|
|
||||||
|
```rust { hl_lines=["11-12",15] }
|
||||||
|
// src/main.rs
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
println!("No\tMchn\tEnNo\t\tName\t\tMode\tIOMd\tDateTime\t");
|
||||||
|
r701.into_record_iter()
|
||||||
|
.unwrap()
|
||||||
|
.take_while(|record| record.datetime >= start)
|
||||||
|
.skip_while(|record| record.datetime >= end)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.enumerate()
|
||||||
|
.for_each(|(id, record)| {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
Loading…
x
Reference in New Issue
Block a user