Documented the public API and removed the missing_errors_doc allows
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:
2026-10-01 15:07:40 +02:00
co-authored by Claude Sonnet 5
parent d8c6440a59
commit f697660ff4
4 changed files with 103 additions and 8 deletions
+7
View File
@@ -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
View File
@@ -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
}
+19
View File
@@ -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
View File
@@ -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?;