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 r701;
|
||||||
mod record;
|
mod record;
|
||||||
mod record_iterator;
|
mod record_iterator;
|
||||||
|
|||||||
+65
-7
@@ -10,13 +10,22 @@ struct ConnectionState {
|
|||||||
sequence_number: u16,
|
sequence_number: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A connection to an R701 attendance reader.
|
||||||
|
///
|
||||||
|
/// Create one with [`R701::connect`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct R701 {
|
pub struct R701 {
|
||||||
state: Mutex<ConnectionState>,
|
state: Mutex<ConnectionState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl R701 {
|
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> {
|
pub async fn connect(connection_info: impl ToSocketAddrs + Send) -> Result<Self> {
|
||||||
// Create a new R701 struct
|
// Create a new R701 struct
|
||||||
let new = Self {
|
let new = Self {
|
||||||
@@ -31,7 +40,15 @@ impl R701 {
|
|||||||
Ok(new)
|
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
|
// The lock must stay held for the whole write+read round-trip, so that
|
||||||
// concurrent callers can't interleave their requests on the wire
|
// concurrent callers can't interleave their requests on the wire
|
||||||
#[allow(clippy::significant_drop_tightening)]
|
#[allow(clippy::significant_drop_tightening)]
|
||||||
@@ -58,7 +75,12 @@ impl R701 {
|
|||||||
Ok(buffer)
|
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<()> {
|
pub async fn ping(&self) -> Result<()> {
|
||||||
// Create a request with a payload of `01 80 00 00 00 00 00 00 00 00 00
|
// Create a request with a payload of `01 80 00 00 00 00 00 00 00 00 00
|
||||||
// 00` and get the response
|
// 00` and get the response
|
||||||
@@ -75,7 +97,18 @@ impl R701 {
|
|||||||
Ok(())
|
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(
|
pub async fn get_name(
|
||||||
&self,
|
&self,
|
||||||
id: u32,
|
id: u32,
|
||||||
@@ -110,7 +143,13 @@ impl R701 {
|
|||||||
Ok(Some(name).filter(|name| !name.is_empty()).map(Ok))
|
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> {
|
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
|
// Create a request with a payload of `01 b4 08 00 00 00 00 00 ff ff 00
|
||||||
// 00` and get the response
|
// 00` and get the response
|
||||||
@@ -130,7 +169,18 @@ impl R701 {
|
|||||||
Ok(u16::from_le_bytes(record_count_le))
|
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(
|
pub async fn get_record_bytes(
|
||||||
&self,
|
&self,
|
||||||
total_records: u16,
|
total_records: u16,
|
||||||
@@ -167,7 +217,15 @@ impl R701 {
|
|||||||
Ok(response[12..1036].to_vec())
|
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<'_>> {
|
pub async fn into_record_iter(&self) -> Result<RecordIterator<'_>> {
|
||||||
RecordIterator::from(self).await
|
RecordIterator::from(self).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,45 @@ use chrono::{DateTime, Local, TimeZone};
|
|||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
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)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub enum Clock {
|
pub enum Clock {
|
||||||
|
/// Entering during the first time slot
|
||||||
FirstIn,
|
FirstIn,
|
||||||
|
/// Leaving during the first time slot
|
||||||
FirstOut,
|
FirstOut,
|
||||||
|
/// Entering during the second time slot
|
||||||
SecondIn,
|
SecondIn,
|
||||||
|
/// Leaving during the second time slot
|
||||||
SecondOut,
|
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)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct Record {
|
pub struct Record {
|
||||||
|
/// The employee's numeric ID, as configured on the device
|
||||||
pub employee_id: u32,
|
pub employee_id: u32,
|
||||||
|
/// Whether the employee was entering or leaving
|
||||||
pub clock: Clock,
|
pub clock: Clock,
|
||||||
|
/// When the record was punched
|
||||||
pub datetime: DateTime<Local>,
|
pub datetime: DateTime<Local>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&[u8]> for Record {
|
impl TryFrom<&[u8]> for Record {
|
||||||
type Error = &'static str;
|
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> {
|
fn try_from(record_bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||||
// Return an error if the slice length is less than 12
|
// Return an error if the slice length is less than 12
|
||||||
if record_bytes.len() != 12 {
|
if record_bytes.len() != 12 {
|
||||||
|
|||||||
+12
-1
@@ -4,12 +4,23 @@ use std::pin::Pin;
|
|||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
use tokio_stream::Stream;
|
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> {
|
pub struct RecordIterator<'a> {
|
||||||
inner: Pin<Box<dyn Stream<Item = Result<Record>> + 'a>>,
|
inner: Pin<Box<dyn Stream<Item = Result<Record>> + 'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> RecordIterator<'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> {
|
pub async fn from(r701: &'a R701) -> Result<Self> {
|
||||||
// Get the total number of records
|
// Get the total number of records
|
||||||
let total_records = r701.get_total_record_count().await?;
|
let total_records = r701.get_total_record_count().await?;
|
||||||
|
|||||||
Reference in New Issue
Block a user