Documented the public API and removed the missing_errors_doc allows
Check, format and test / build (push) Successful in 42s
Check, format and test / build (push) Successful in 42s
Added rustdoc comments to every public item (R701, Record, Clock, RecordIterator and their methods/fields/variants), each with an # Errors section where applicable, so the clippy::missing_errors_doc allows are no longer needed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,10 @@
|
||||
//! An async client library for the R701 attendance reader by I.P.S.
|
||||
//! Informatica, reverse-engineered from its TCP protocol.
|
||||
//!
|
||||
//! Connect with [`R701::connect`], then read attendance records with
|
||||
//! [`R701::into_record_iter`] and resolve employee names with
|
||||
//! [`R701::get_name`].
|
||||
|
||||
mod r701;
|
||||
mod record;
|
||||
mod record_iterator;
|
||||
|
||||
+65
-7
@@ -10,13 +10,22 @@ struct ConnectionState {
|
||||
sequence_number: u16,
|
||||
}
|
||||
|
||||
/// A connection to an R701 attendance reader.
|
||||
///
|
||||
/// Create one with [`R701::connect`].
|
||||
#[derive(Debug)]
|
||||
pub struct R701 {
|
||||
state: Mutex<ConnectionState>,
|
||||
}
|
||||
|
||||
impl R701 {
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
/// Connects to an R701 device at `connection_info` and pings it to
|
||||
/// make sure it's responding correctly.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `Err` if the TCP connection can't be established, or if the
|
||||
/// initial ping doesn't get the expected response.
|
||||
pub async fn connect(connection_info: impl ToSocketAddrs + Send) -> Result<Self> {
|
||||
// Create a new R701 struct
|
||||
let new = Self {
|
||||
@@ -31,7 +40,15 @@ impl R701 {
|
||||
Ok(new)
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
/// Wraps `payload` in the device's request envelope (header and
|
||||
/// sequence number) and sends it, then reads back exactly
|
||||
/// `response_len` bytes of response.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `Err` if writing the request or reading the response fails,
|
||||
/// including if the connection closes before `response_len` bytes have
|
||||
/// been received.
|
||||
// The lock must stay held for the whole write+read round-trip, so that
|
||||
// concurrent callers can't interleave their requests on the wire
|
||||
#[allow(clippy::significant_drop_tightening)]
|
||||
@@ -58,7 +75,12 @@ impl R701 {
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
/// Pings the device to check that it's connected and responding.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `Err` if the underlying request fails, or if the device
|
||||
/// responds with anything other than the expected acknowledgement.
|
||||
pub async fn ping(&self) -> Result<()> {
|
||||
// Create a request with a payload of `01 80 00 00 00 00 00 00 00 00 00
|
||||
// 00` and get the response
|
||||
@@ -75,7 +97,18 @@ impl R701 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
/// Looks up the name of the employee with the given `id`.
|
||||
///
|
||||
/// Returns `Ok(None)` if no employee with that ID is registered on the
|
||||
/// device. If one is registered but its name isn't valid UTF-8, the
|
||||
/// inner `Err` carries the original bytes (via
|
||||
/// `FromUtf8Error::as_bytes`), letting the caller choose to fail or
|
||||
/// fall back to a lossy rendering.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `Err` if the underlying request fails, or if the response
|
||||
/// has an unexpected header.
|
||||
pub async fn get_name(
|
||||
&self,
|
||||
id: u32,
|
||||
@@ -110,7 +143,13 @@ impl R701 {
|
||||
Ok(Some(name).filter(|name| !name.is_empty()).map(Ok))
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
/// Returns the total number of attendance records currently stored on
|
||||
/// the device.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `Err` if the underlying request fails, or if the response
|
||||
/// has an unexpected header.
|
||||
pub async fn get_total_record_count(&self) -> Result<u16> {
|
||||
// Create a request with a payload of `01 b4 08 00 00 00 00 00 ff ff 00
|
||||
// 00` and get the response
|
||||
@@ -130,7 +169,18 @@ impl R701 {
|
||||
Ok(u16::from_le_bytes(record_count_le))
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
/// Fetches one 1024-byte block of raw record data.
|
||||
///
|
||||
/// `sequence_number` selects which block to fetch, except that `0`
|
||||
/// always fetches the first block regardless of `total_records`,
|
||||
/// following the device's own request convention. Used internally by
|
||||
/// [`RecordIterator`] — most callers should use
|
||||
/// [`R701::into_record_iter`] instead.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `Err` if the underlying request fails, or if the response
|
||||
/// has an unexpected header or trailing bytes.
|
||||
pub async fn get_record_bytes(
|
||||
&self,
|
||||
total_records: u16,
|
||||
@@ -167,7 +217,15 @@ impl R701 {
|
||||
Ok(response[12..1036].to_vec())
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
/// Starts streaming all attendance records currently stored on the
|
||||
/// device, newest first — fetching the most recent records first is
|
||||
/// faster when you don't need the full history; reverse the order
|
||||
/// yourself if you need chronological output.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `Err` if fetching the total record count or the first block
|
||||
/// of record data fails.
|
||||
pub async fn into_record_iter(&self) -> Result<RecordIterator<'_>> {
|
||||
RecordIterator::from(self).await
|
||||
}
|
||||
|
||||
@@ -2,26 +2,45 @@ use chrono::{DateTime, Local, TimeZone};
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Which clock an employee punched: entering or leaving, for one of the
|
||||
/// device's two configured time slots.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum Clock {
|
||||
/// Entering during the first time slot
|
||||
FirstIn,
|
||||
/// Leaving during the first time slot
|
||||
FirstOut,
|
||||
/// Entering during the second time slot
|
||||
SecondIn,
|
||||
/// Leaving during the second time slot
|
||||
SecondOut,
|
||||
}
|
||||
|
||||
/// A single attendance record read from the device.
|
||||
///
|
||||
/// Identifies the employee who punched, whether they were entering or
|
||||
/// leaving, and when.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct Record {
|
||||
/// The employee's numeric ID, as configured on the device
|
||||
pub employee_id: u32,
|
||||
/// Whether the employee was entering or leaving
|
||||
pub clock: Clock,
|
||||
/// When the record was punched
|
||||
pub datetime: DateTime<Local>,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Record {
|
||||
type Error = &'static str;
|
||||
|
||||
/// Parses a single 12-byte record as read from the device.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `Err` if `record_bytes` isn't exactly 12 bytes long, or if
|
||||
/// the packed date/time bits don't form a valid calendar date.
|
||||
fn try_from(record_bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
// Return an error if the slice length is less than 12
|
||||
if record_bytes.len() != 12 {
|
||||
|
||||
+12
-1
@@ -4,12 +4,23 @@ use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio_stream::Stream;
|
||||
|
||||
/// An async stream of [`Record`]s read from an [`R701`] device.
|
||||
///
|
||||
/// Create one with [`R701::into_record_iter`]. Implements [`Stream`], so
|
||||
/// consume it with [`tokio_stream::StreamExt::next`] (or any other
|
||||
/// `Stream` combinator).
|
||||
pub struct RecordIterator<'a> {
|
||||
inner: Pin<Box<dyn Stream<Item = Result<Record>> + 'a>>,
|
||||
}
|
||||
|
||||
impl<'a> RecordIterator<'a> {
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
/// Builds a new record stream, immediately fetching the device's total
|
||||
/// record count and the first block of record data.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `Err` if fetching the total record count or the first block
|
||||
/// of record data fails.
|
||||
pub async fn from(r701: &'a R701) -> Result<Self> {
|
||||
// Get the total number of records
|
||||
let total_records = r701.get_total_record_count().await?;
|
||||
|
||||
Reference in New Issue
Block a user