pamoja_sensors/ds18b20.rs
1//! Maxim DS18B20 1-Wire digital thermometer.
2//!
3//! The DS18B20 reports temperature as a 16-bit two's-complement number in a nine-byte
4//! scratchpad, with a CRC byte that covers the rest. This module decodes that
5//! temperature exactly as the datasheet's temperature/data table specifies, reads the
6//! resolution out of the configuration byte, and verifies the scratchpad's CRC with
7//! the Maxim 1-Wire polynomial so a read corrupted on the bus is caught rather than
8//! trusted.
9//!
10//! It is pure logic: a caller drives the 1-Wire transactions (convert, then read
11//! scratchpad) and hands the nine bytes to [`Scratchpad::parse`].
12
13use crate::SensorError;
14
15#[cfg(feature = "embedded-hal")]
16mod driver;
17
18#[cfg(feature = "embedded-hal")]
19pub use driver::Ds18b20;
20
21/// The function commands a DS18B20 answers once it has been addressed.
22pub mod command {
23 /// Start a temperature conversion; the result lands in the scratchpad.
24 pub const CONVERT_T: u8 = 0x44;
25 /// Send the nine scratchpad bytes, CRC last.
26 pub const READ_SCRATCHPAD: u8 = 0xBE;
27 /// Take the alarm thresholds and the configuration byte, three bytes in that order.
28 pub const WRITE_SCRATCHPAD: u8 = 0x4E;
29 /// Copy those three bytes from the scratchpad to the EEPROM.
30 pub const COPY_SCRATCHPAD: u8 = 0x48;
31 /// Reload the three EEPROM bytes into the scratchpad.
32 pub const RECALL_EEPROM: u8 = 0xB8;
33 /// Answer one bit: 1 for external power, 0 for parasite power.
34 pub const READ_POWER_SUPPLY: u8 = 0xB4;
35}
36
37/// How long a copy to the EEPROM takes, in microseconds.
38pub const EEPROM_WRITE_MICROS: u32 = 10_000;
39
40/// The 1-Wire family code that identifies a DS18B20 in the first ROM byte.
41pub const FAMILY_CODE: u8 = 0x28;
42
43/// The conversion resolution, selected by the R1/R0 bits of the configuration byte.
44///
45/// Higher resolution resolves smaller steps but takes longer to convert, a tradeoff
46/// the datasheet spells out. The temperature register always carries 1/16 °C per
47/// count; at lower resolutions the unused low bits simply read as zero.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum Resolution {
50 /// 9-bit, 0.5 °C steps, up to 93.75 ms per conversion.
51 Bits9,
52 /// 10-bit, 0.25 °C steps, up to 187.5 ms per conversion.
53 Bits10,
54 /// 11-bit, 0.125 °C steps, up to 375 ms per conversion.
55 Bits11,
56 /// 12-bit, 0.0625 °C steps, up to 750 ms per conversion. The power-on default.
57 Bits12,
58}
59
60impl Resolution {
61 /// Returns the number of significant bits this resolution produces.
62 ///
63 /// # Returns
64 ///
65 /// `9`, `10`, `11`, or `12`.
66 pub fn bits(self) -> u8 {
67 match self {
68 Resolution::Bits9 => 9,
69 Resolution::Bits10 => 10,
70 Resolution::Bits11 => 11,
71 Resolution::Bits12 => 12,
72 }
73 }
74
75 /// Returns the configuration-register byte that selects this resolution.
76 ///
77 /// The byte places R1/R0 in bits 6:5 over the datasheet's fixed surrounding
78 /// pattern (bit 7 clear, bit 4 set, bits 3:0 set), so 12-bit is `0x7F`, 11-bit
79 /// `0x5F`, 10-bit `0x3F`, and 9-bit `0x1F`.
80 ///
81 /// # Returns
82 ///
83 /// The configuration byte written to scratchpad byte 4.
84 pub fn config_byte(self) -> u8 {
85 let r1r0 = match self {
86 Resolution::Bits9 => 0b00,
87 Resolution::Bits10 => 0b01,
88 Resolution::Bits11 => 0b10,
89 Resolution::Bits12 => 0b11,
90 };
91 0b0001_1111 | (r1r0 << 5)
92 }
93
94 /// Reads the resolution out of a configuration byte's R1/R0 bits.
95 ///
96 /// # Arguments
97 ///
98 /// * `byte` - the configuration register (scratchpad byte 4).
99 ///
100 /// # Returns
101 ///
102 /// The resolution selected by bits 6:5.
103 pub fn from_config_byte(byte: u8) -> Resolution {
104 match (byte >> 5) & 0b11 {
105 0b00 => Resolution::Bits9,
106 0b01 => Resolution::Bits10,
107 0b10 => Resolution::Bits11,
108 _ => Resolution::Bits12,
109 }
110 }
111
112 /// Returns the temperature step this resolution resolves, in micro-degrees Celsius.
113 ///
114 /// # Returns
115 ///
116 /// `500000` (0.5 °C) for 9-bit down to `62500` (0.0625 °C) for 12-bit.
117 pub fn step_micro_celsius(self) -> u32 {
118 match self {
119 Resolution::Bits9 => 500_000,
120 Resolution::Bits10 => 250_000,
121 Resolution::Bits11 => 125_000,
122 Resolution::Bits12 => 62_500,
123 }
124 }
125
126 /// Returns the datasheet's maximum conversion time, in microseconds.
127 ///
128 /// # Returns
129 ///
130 /// `93750` for 9-bit, doubling up to `750000` for 12-bit.
131 pub fn max_conversion_micros(self) -> u32 {
132 match self {
133 Resolution::Bits9 => 93_750,
134 Resolution::Bits10 => 187_500,
135 Resolution::Bits11 => 375_000,
136 Resolution::Bits12 => 750_000,
137 }
138 }
139}
140
141/// Converts a raw temperature register value to micro-degrees Celsius, exactly.
142///
143/// Each count is 1/16 °C, which is 62500 micro-degrees, so the conversion is exact in
144/// integer arithmetic and needs no floating point.
145///
146/// # Arguments
147///
148/// * `raw` - the 16-bit two's-complement temperature register, as a signed value.
149///
150/// # Returns
151///
152/// The temperature in micro-degrees Celsius (millionths of a degree).
153pub fn temperature_to_micro_celsius(raw: i16) -> i32 {
154 raw as i32 * 62_500
155}
156
157/// Converts a raw temperature register value to degrees Celsius.
158///
159/// # Arguments
160///
161/// * `raw` - the 16-bit two's-complement temperature register, as a signed value.
162///
163/// # Returns
164///
165/// The temperature in degrees Celsius.
166pub fn temperature_to_celsius(raw: i16) -> f32 {
167 raw as f32 / 16.0
168}
169
170/// Converts micro-degrees Celsius to a raw temperature register value.
171///
172/// The result is truncated to the step `resolution` resolves, since the datasheet
173/// specifies that the bits below a selected resolution read as zero.
174///
175/// # Arguments
176///
177/// * `micro_celsius` - the temperature in millionths of a degree Celsius.
178/// * `resolution` - the resolution the part is configured for.
179///
180/// # Returns
181///
182/// The 16-bit two's-complement temperature register, as a signed value.
183pub fn temperature_from_micro_celsius(micro_celsius: i32, resolution: Resolution) -> i16 {
184 let counts = micro_celsius.div_euclid(62_500) as i16;
185 let unused = 12 - resolution.bits();
186 (counts >> unused) << unused
187}
188
189/// Converts degrees Celsius to a raw temperature register value.
190///
191/// # Arguments
192///
193/// * `celsius` - the temperature in degrees Celsius.
194/// * `resolution` - the resolution the part is configured for.
195///
196/// # Returns
197///
198/// The 16-bit two's-complement temperature register, as a signed value.
199pub fn temperature_from_celsius(celsius: f32, resolution: Resolution) -> i16 {
200 temperature_from_micro_celsius((celsius * 1_000_000.0) as i32, resolution)
201}
202
203/// Computes the Maxim 1-Wire CRC-8 over `data`.
204///
205/// This is the CRC the DS18B20 (and every Maxim 1-Wire device) appends to its ROM
206/// code and scratchpad. The polynomial is X^8 + X^5 + X^4 + 1, processed
207/// least-significant-bit first from a zero shift register, which is the reflected
208/// form `0x8C`.
209///
210/// # Arguments
211///
212/// * `data` - the bytes the CRC covers, in transmission order.
213///
214/// # Returns
215///
216/// The 8-bit CRC; for a correctly received message followed by its CRC byte, running
217/// this over all of them yields zero.
218pub fn crc8(data: &[u8]) -> u8 {
219 let mut crc = 0u8;
220 for &byte in data {
221 let mut bits = byte;
222 for _ in 0..8 {
223 let mix = (crc ^ bits) & 0x01;
224 crc >>= 1;
225 if mix != 0 {
226 crc ^= 0x8C;
227 }
228 bits >>= 1;
229 }
230 }
231 crc
232}
233
234/// A decoded, CRC-verified DS18B20 scratchpad.
235///
236/// The scratchpad is nine bytes: temperature LSB and MSB, the high and low alarm
237/// thresholds, the configuration byte, three reserved bytes, and a CRC. [`parse`]
238/// checks the CRC before exposing any of it.
239///
240/// [`parse`]: Scratchpad::parse
241///
242/// # Examples
243///
244/// ```
245/// use pamoja_sensors::ds18b20::{temperature_from_celsius, Resolution, Scratchpad};
246///
247/// // What a part sitting at +25.0625 °C, set to 12-bit steps with its thresholds at
248/// // +75/-10 °C, puts on the bus. A driver reads these nine bytes off the wire.
249/// let bytes = Scratchpad::new(
250/// temperature_from_celsius(25.0625, Resolution::Bits12),
251/// Resolution::Bits12,
252/// 75,
253/// -10,
254/// )
255/// .to_bytes();
256///
257/// let scratchpad = Scratchpad::parse(&bytes)?;
258/// assert_eq!(scratchpad.temperature_micro_celsius(), 25_062_500);
259/// assert_eq!(scratchpad.resolution(), Resolution::Bits12);
260/// # Ok::<(), pamoja_sensors::SensorError>(())
261/// ```
262#[derive(Clone, Copy, Debug, PartialEq, Eq)]
263pub struct Scratchpad {
264 raw_temperature: i16,
265 alarm_high: i8,
266 alarm_low: i8,
267 resolution: Resolution,
268}
269
270impl Scratchpad {
271 /// Parses and CRC-checks a nine-byte scratchpad.
272 ///
273 /// # Arguments
274 ///
275 /// * `bytes` - the nine scratchpad bytes in the order the device sends them, the
276 /// ninth being the CRC.
277 ///
278 /// # Returns
279 ///
280 /// The decoded scratchpad.
281 ///
282 /// # Errors
283 ///
284 /// Returns [`SensorError::Crc`] if the CRC byte does not match the first eight,
285 /// which means the read was corrupted and should be repeated.
286 pub fn parse(bytes: &[u8; 9]) -> Result<Scratchpad, SensorError> {
287 if crc8(&bytes[..8]) != bytes[8] {
288 return Err(SensorError::Crc);
289 }
290 let raw_temperature = i16::from_le_bytes([bytes[0], bytes[1]]);
291 Ok(Scratchpad {
292 raw_temperature,
293 alarm_high: bytes[2] as i8,
294 alarm_low: bytes[3] as i8,
295 resolution: Resolution::from_config_byte(bytes[4]),
296 })
297 }
298
299 /// Builds the scratchpad a part in the given state reports.
300 ///
301 /// This is the inverse of [`parse`](Self::parse), so a node can be developed and
302 /// tested against the bytes a thermometer would send without one attached.
303 ///
304 /// # Arguments
305 ///
306 /// * `raw_temperature` - the 16-bit two's-complement temperature register, as a
307 /// signed value; [`temperature_from_celsius`] builds one from a temperature.
308 /// * `resolution` - the resolution the part is configured for.
309 /// * `alarm_high` - the high alarm threshold in whole degrees Celsius (TH).
310 /// * `alarm_low` - the low alarm threshold in whole degrees Celsius (TL).
311 ///
312 /// # Returns
313 ///
314 /// The scratchpad holding those values.
315 pub fn new(
316 raw_temperature: i16,
317 resolution: Resolution,
318 alarm_high: i8,
319 alarm_low: i8,
320 ) -> Scratchpad {
321 Scratchpad {
322 raw_temperature,
323 alarm_high,
324 alarm_low,
325 resolution,
326 }
327 }
328
329 /// Returns the nine bytes a part holding this scratchpad puts on the bus.
330 ///
331 /// Bytes 5 to 7 are the reserved bytes, which a part reports but which carry no
332 /// reading; they are filled with the values its scratchpad figure shows so the
333 /// frame is the one a device would actually send. The ninth byte is the CRC over
334 /// the other eight, so the result parses.
335 ///
336 /// # Returns
337 ///
338 /// The nine scratchpad bytes in transmission order, CRC last.
339 pub fn to_bytes(&self) -> [u8; 9] {
340 let temperature = self.raw_temperature.to_le_bytes();
341 let mut bytes = [
342 temperature[0],
343 temperature[1],
344 self.alarm_high as u8,
345 self.alarm_low as u8,
346 self.resolution.config_byte(),
347 0xFF,
348 0x0C,
349 0x10,
350 0x00,
351 ];
352 bytes[8] = crc8(&bytes[..8]);
353 bytes
354 }
355
356 /// Returns the raw temperature register value.
357 ///
358 /// # Returns
359 ///
360 /// The 16-bit two's-complement register, as a signed value.
361 pub fn raw_temperature(&self) -> i16 {
362 self.raw_temperature
363 }
364
365 /// Returns the temperature in micro-degrees Celsius.
366 ///
367 /// # Returns
368 ///
369 /// The temperature, exact, in millionths of a degree Celsius.
370 pub fn temperature_micro_celsius(&self) -> i32 {
371 temperature_to_micro_celsius(self.raw_temperature)
372 }
373
374 /// Returns the temperature in degrees Celsius.
375 ///
376 /// # Returns
377 ///
378 /// The temperature in degrees Celsius.
379 pub fn temperature_celsius(&self) -> f32 {
380 temperature_to_celsius(self.raw_temperature)
381 }
382
383 /// Returns the configured conversion resolution.
384 pub fn resolution(&self) -> Resolution {
385 self.resolution
386 }
387
388 /// Returns the high temperature alarm threshold (TH), in whole degrees Celsius.
389 pub fn alarm_high(&self) -> i8 {
390 self.alarm_high
391 }
392
393 /// Returns the low temperature alarm threshold (TL), in whole degrees Celsius.
394 pub fn alarm_low(&self) -> i8 {
395 self.alarm_low
396 }
397}
398
399/// Decodes the text the Linux kernel's `w1_therm` driver serves for a thermometer.
400///
401/// The `w1_slave` file under `/sys/bus/w1/devices/28-*/` holds two lines: the nine
402/// scratchpad bytes in hex followed by `: crc=xx YES` or `NO`, then the same bytes
403/// followed by `t=` and the temperature in millidegrees. This takes the first line's
404/// bytes and parses them as a [`Scratchpad`], so the CRC is checked here as well; a
405/// `NO` from the kernel is reported without a second look.
406///
407/// # Arguments
408///
409/// * `text` - the file's contents.
410///
411/// # Returns
412///
413/// The scratchpad.
414///
415/// # Errors
416///
417/// Returns [`SensorError::Crc`] if the kernel or this decoder rejects the CRC, and
418/// [`SensorError::Invalid`] if the text is not in the driver's format.
419///
420/// # Examples
421///
422/// ```
423/// use pamoja_sensors::ds18b20::{parse_w1_slave, temperature_from_celsius, Resolution, Scratchpad};
424///
425/// // What the kernel prints for a part at 20.8125 C, built from the same library
426/// // that decodes it.
427/// let raw = temperature_from_celsius(20.8125, Resolution::Bits12);
428/// let bytes = Scratchpad::new(raw, Resolution::Bits12, 75, -10).to_bytes();
429/// let hex: Vec<String> = bytes.iter().map(|byte| format!("{byte:02x}")).collect();
430/// let text = format!("{} : crc={:02x} YES\n", hex.join(" "), bytes[8]);
431///
432/// let scratchpad = parse_w1_slave(&text)?;
433/// assert_eq!(scratchpad.temperature_micro_celsius(), 20_812_500);
434/// # Ok::<(), pamoja_sensors::SensorError>(())
435/// ```
436pub fn parse_w1_slave(text: &str) -> Result<Scratchpad, SensorError> {
437 let line = text.lines().next().ok_or(SensorError::Invalid)?;
438 let (hex, verdict) = line.split_once(": crc=").ok_or(SensorError::Invalid)?;
439 let verdict = verdict.trim();
440 if verdict.ends_with("NO") {
441 return Err(SensorError::Crc);
442 }
443 if !verdict.ends_with("YES") {
444 return Err(SensorError::Invalid);
445 }
446 let mut bytes = [0u8; 9];
447 let mut count = 0;
448 for token in hex.split_ascii_whitespace() {
449 if count == bytes.len() {
450 return Err(SensorError::Invalid);
451 }
452 bytes[count] = u8::from_str_radix(token, 16).map_err(|_| SensorError::Invalid)?;
453 count += 1;
454 }
455 if count != bytes.len() {
456 return Err(SensorError::Invalid);
457 }
458 Scratchpad::parse(&bytes)
459}
460
461/// Renders the text the Linux kernel's `w1_therm` driver serves for a scratchpad it read
462/// cleanly: the contents of a thermometer's `w1_slave` file.
463///
464/// This is the inverse of [`parse_w1_slave`], so a program that reads the kernel's files is
465/// written and tested with no thermometer and no kernel. The format is the one
466/// `w1_slave_show` prints in `drivers/w1/slaves/w1_therm.c`: the nine bytes as two
467/// lowercase hex digits and a space each, then `: crc=`, the CRC the kernel computed over
468/// the first eight, and `YES`; then the nine bytes again and `t=` with the temperature in
469/// millidegrees, which `w1_DS18B20_convert_temp` takes as the raw register times 1000
470/// divided by 16, truncated toward zero as C divides.
471///
472/// # Arguments
473///
474/// * `scratchpad` - the scratchpad the kernel read.
475///
476/// # Returns
477///
478/// The file's two lines.
479///
480/// # Examples
481///
482/// ```
483/// use pamoja_sensors::ds18b20::{
484/// parse_w1_slave, temperature_from_celsius, w1_slave_text, Resolution, Scratchpad,
485/// };
486///
487/// let raw = temperature_from_celsius(21.5, Resolution::Bits12);
488/// let scratchpad = Scratchpad::new(raw, Resolution::Bits12, 75, -10);
489/// let text = w1_slave_text(&scratchpad);
490/// assert!(text.ends_with(" t=21500\n"));
491/// assert_eq!(parse_w1_slave(&text)?, scratchpad);
492/// # Ok::<(), pamoja_sensors::SensorError>(())
493/// ```
494#[cfg(feature = "alloc")]
495pub fn w1_slave_text(scratchpad: &Scratchpad) -> alloc::string::String {
496 use core::fmt::Write;
497
498 let bytes = scratchpad.to_bytes();
499 let mut hex = alloc::string::String::with_capacity(27);
500 for byte in bytes {
501 let _ = write!(hex, "{byte:02x} ");
502 }
503 alloc::format!(
504 "{hex}: crc={:02x} YES\n{hex}t={}\n",
505 crc8(&bytes[..8]),
506 kernel_millidegrees(&bytes)
507 )
508}
509
510// The temperature the kernel prints, as `w1_DS18B20_convert_temp` works it out, including
511// the path it takes for a clone that sets bit 7 of the configuration register.
512#[cfg(feature = "alloc")]
513fn kernel_millidegrees(bytes: &[u8; 9]) -> i32 {
514 let register = u16::from_le_bytes([bytes[0], bytes[1]]);
515 if bytes[4] & 0x80 != 0 {
516 let extended = (register << 2) | u16::from(bytes[4] & 0x03);
517 i32::from(extended as i16) * 1000 / 64
518 } else {
519 i32::from(register as i16) * 1000 / 16
520 }
521}
522
523/// The kernel's own 1-Wire driver: thermometers as files under `/sys/bus/w1/devices`.
524///
525/// On a Raspberry Pi the `w1-gpio` overlay (`dtoverlay=w1-gpio` in `config.txt`)
526/// puts the bus on a GPIO, and the kernel's `w1_therm` driver lists every DS18B20 it
527/// finds as a directory named by its family code and serial, `28-000005e2fdc3`, with
528/// a `w1_slave` file that runs a conversion and prints the scratchpad on every read.
529/// A [`Thermometer`](linux::Thermometer) reads that file as a
530/// [`Sensor`](pamoja_core::Sensor), which is the right way to reach a DS18B20 from a
531/// Linux process, where the bit timing a [`Ds18b20`] needs cannot be held.
532#[cfg(feature = "linux")]
533pub mod linux {
534 use std::path::{Path, PathBuf};
535 use std::{fmt, fs, io};
536
537 use pamoja_core::Sensor;
538
539 use super::{parse_w1_slave, Scratchpad, FAMILY_CODE};
540 use crate::SensorError;
541
542 /// Where the kernel lists the devices on its 1-Wire buses.
543 pub const DEVICES: &str = "/sys/bus/w1/devices";
544
545 /// The name of the file that runs a conversion and prints the scratchpad.
546 pub const W1_SLAVE: &str = "w1_slave";
547
548 /// What can go wrong reading a thermometer through the kernel.
549 #[derive(Debug)]
550 pub enum ThermometerError {
551 /// The file could not be read: the overlay is off, the device is gone, or the
552 /// process lacks permission.
553 Io(io::Error),
554 /// The file was read but its scratchpad did not decode.
555 Sensor(SensorError),
556 }
557
558 impl fmt::Display for ThermometerError {
559 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
560 match self {
561 ThermometerError::Io(error) => write!(f, "reading the w1_slave file: {error}"),
562 ThermometerError::Sensor(error) => write!(f, "{error}"),
563 }
564 }
565 }
566
567 impl std::error::Error for ThermometerError {
568 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
569 match self {
570 ThermometerError::Io(error) => Some(error),
571 ThermometerError::Sensor(error) => Some(error),
572 }
573 }
574 }
575
576 impl From<ThermometerError> for pamoja_core::Error {
577 fn from(error: ThermometerError) -> Self {
578 match error {
579 ThermometerError::Io(io) => pamoja_core::Error::Io(io.to_string()),
580 ThermometerError::Sensor(sensor) => pamoja_core::Error::Codec(sensor.to_string()),
581 }
582 }
583 }
584
585 /// A DS18B20 the kernel exposes as a `w1_slave` file.
586 ///
587 /// # Examples
588 ///
589 /// ```no_run
590 /// use pamoja_core::Sensor;
591 /// use pamoja_sensors::ds18b20::linux::Thermometer;
592 ///
593 /// # async fn run() -> pamoja_core::Result<()> {
594 /// let mut probe = Thermometer::new("000005e2fdc3");
595 /// let scratchpad = probe.read().await?;
596 /// println!("{:.4} C", scratchpad.temperature_celsius());
597 /// # Ok(())
598 /// # }
599 /// ```
600 #[derive(Clone, Debug, PartialEq, Eq)]
601 pub struct Thermometer {
602 path: PathBuf,
603 }
604
605 impl Thermometer {
606 /// Names a thermometer by the serial in its directory name.
607 ///
608 /// # Arguments
609 ///
610 /// * `serial` - the twelve hex digits after `28-` in the device directory.
611 ///
612 /// # Returns
613 ///
614 /// The thermometer, reading `/sys/bus/w1/devices/28-<serial>/w1_slave`.
615 pub fn new(serial: &str) -> Thermometer {
616 let directory = format!("{FAMILY_CODE:02x}-{serial}");
617 Thermometer {
618 path: Path::new(DEVICES).join(directory).join(W1_SLAVE),
619 }
620 }
621
622 /// Names a thermometer by the path of its `w1_slave` file.
623 ///
624 /// # Arguments
625 ///
626 /// * `path` - the file to read.
627 ///
628 /// # Returns
629 ///
630 /// The thermometer.
631 pub fn at(path: impl Into<PathBuf>) -> Thermometer {
632 Thermometer { path: path.into() }
633 }
634
635 /// Returns the path of the file the thermometer reads.
636 pub fn path(&self) -> &Path {
637 &self.path
638 }
639
640 /// Returns the serial the kernel named the thermometer's directory after.
641 ///
642 /// # Returns
643 ///
644 /// The twelve hex digits after `28-`, as in `000005e2fdc3`, which tell one probe
645 /// from another and stay with the part for life. `None` when the file does not sit
646 /// in a DS18B20's directory, as a thermometer named by [`at`](Thermometer::at) may
647 /// not.
648 ///
649 /// # Examples
650 ///
651 /// ```
652 /// use pamoja_sensors::ds18b20::linux::Thermometer;
653 ///
654 /// assert_eq!(Thermometer::new("000005e2fdc3").serial(), Some("000005e2fdc3"));
655 /// assert_eq!(Thermometer::at("/tmp/w1_slave").serial(), None);
656 /// ```
657 pub fn serial(&self) -> Option<&str> {
658 let directory = self.path.parent()?.file_name()?.to_str()?;
659 let (family, serial) = directory.split_once('-')?;
660 let family = u8::from_str_radix(family, 16).ok()?;
661 (family == FAMILY_CODE && !serial.is_empty()).then_some(serial)
662 }
663
664 /// Lists every DS18B20 the kernel has found.
665 ///
666 /// # Returns
667 ///
668 /// One thermometer per `28-*` directory under [`DEVICES`].
669 ///
670 /// # Errors
671 ///
672 /// Returns the I/O error if the directory cannot be listed, which usually
673 /// means the 1-Wire overlay is not enabled.
674 pub fn discover() -> io::Result<Vec<Thermometer>> {
675 Thermometer::discover_in(Path::new(DEVICES))
676 }
677
678 /// Lists every DS18B20 directory under `devices`.
679 ///
680 /// # Arguments
681 ///
682 /// * `devices` - the directory the kernel lists its 1-Wire devices in.
683 ///
684 /// # Returns
685 ///
686 /// One thermometer per directory whose name starts with the DS18B20 family
687 /// code, sorted by name.
688 ///
689 /// # Errors
690 ///
691 /// Returns the I/O error if the directory cannot be listed.
692 pub fn discover_in(devices: &Path) -> io::Result<Vec<Thermometer>> {
693 let prefix = format!("{FAMILY_CODE:02x}-");
694 let mut found: Vec<Thermometer> = fs::read_dir(devices)?
695 .filter_map(|entry| entry.ok())
696 .filter(|entry| entry.file_name().to_string_lossy().starts_with(&prefix))
697 .map(|entry| Thermometer::at(entry.path().join(W1_SLAVE)))
698 .collect();
699 found.sort_by(|a, b| a.path.cmp(&b.path));
700 Ok(found)
701 }
702
703 /// Reads the file, which makes the kernel run a conversion, and decodes it.
704 ///
705 /// # Returns
706 ///
707 /// The CRC-checked scratchpad.
708 ///
709 /// # Errors
710 ///
711 /// Returns [`ThermometerError::Io`] if the file cannot be read and
712 /// [`ThermometerError::Sensor`] if it does not decode.
713 pub fn read_scratchpad(&self) -> Result<Scratchpad, ThermometerError> {
714 let text = fs::read_to_string(&self.path).map_err(ThermometerError::Io)?;
715 parse_w1_slave(&text).map_err(ThermometerError::Sensor)
716 }
717 }
718
719 impl Sensor for Thermometer {
720 type Reading = Scratchpad;
721
722 async fn read(&mut self) -> pamoja_core::Result<Scratchpad> {
723 self.read_scratchpad().map_err(pamoja_core::Error::from)
724 }
725 }
726
727 #[cfg(test)]
728 mod tests {
729 use super::*;
730 use crate::ds18b20::{temperature_from_celsius, Resolution};
731
732 fn kernel_text(celsius: f32) -> String {
733 let raw = temperature_from_celsius(celsius, Resolution::Bits12);
734 let bytes = Scratchpad::new(raw, Resolution::Bits12, 75, -10).to_bytes();
735 let hex: Vec<String> = bytes.iter().map(|byte| format!("{byte:02x}")).collect();
736 let hex = hex.join(" ");
737 format!(
738 "{hex} : crc={:02x} YES\n{hex} t={}\n",
739 bytes[8],
740 (celsius * 1000.0) as i32
741 )
742 }
743
744 #[test]
745 fn a_thermometer_reads_and_decodes_its_file() {
746 let dir = std::env::temp_dir().join(format!("pamoja-w1-{}", std::process::id()));
747 let device = dir.join("28-000005e2fdc3");
748 fs::create_dir_all(&device).unwrap();
749 fs::write(device.join(W1_SLAVE), kernel_text(21.5)).unwrap();
750 fs::create_dir_all(dir.join("w1_bus_master1")).unwrap();
751
752 let found = Thermometer::discover_in(&dir).unwrap();
753 assert_eq!(found.len(), 1);
754 assert_eq!(found[0].path(), device.join(W1_SLAVE));
755 assert_eq!(found[0].serial(), Some("000005e2fdc3"));
756 let scratchpad = found[0].read_scratchpad().unwrap();
757 assert_eq!(scratchpad.temperature_celsius(), 21.5);
758 assert_eq!(
759 pamoja_hal::script::block_on(found[0].clone().read()).unwrap(),
760 scratchpad
761 );
762
763 let missing = Thermometer::at(dir.join("28-none").join(W1_SLAVE));
764 assert!(matches!(
765 missing.read_scratchpad(),
766 Err(ThermometerError::Io(_))
767 ));
768 fs::remove_dir_all(&dir).unwrap();
769 }
770
771 #[test]
772 fn a_serial_names_the_kernel_path() {
773 let probe = Thermometer::new("000005e2fdc3");
774 assert_eq!(
775 probe.path(),
776 Path::new("/sys/bus/w1/devices/28-000005e2fdc3/w1_slave")
777 );
778 assert_eq!(probe.serial(), Some("000005e2fdc3"));
779 }
780
781 #[test]
782 fn only_a_ds18b20_directory_names_a_serial() {
783 let at = |path: &str| Thermometer::at(path);
784 assert_eq!(
785 at("/w1/28-0316a2795cff/w1_slave").serial(),
786 Some("0316a2795cff")
787 );
788 assert_eq!(at("/w1/10-000802f1e4a3/w1_slave").serial(), None);
789 assert_eq!(at("/w1/28-/w1_slave").serial(), None);
790 assert_eq!(at("/w1/w1_bus_master1/w1_slave").serial(), None);
791 assert_eq!(at("w1_slave").serial(), None);
792 }
793 }
794}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799
800 #[test]
801 fn temperature_table_matches_the_datasheet() {
802 // The DS18B20 datasheet's temperature/data relationship table, in 1/16 °C.
803 let table: &[(i16, i32)] = &[
804 (0x07D0, 125_000_000),
805 (0x0550, 85_000_000),
806 (0x0191, 25_062_500),
807 (0x00A2, 10_125_000),
808 (0x0008, 500_000),
809 (0x0000, 0),
810 (i16::from_le_bytes([0xF8, 0xFF]), -500_000),
811 (i16::from_le_bytes([0x5E, 0xFF]), -10_125_000),
812 (i16::from_le_bytes([0x6F, 0xFE]), -25_062_500),
813 (i16::from_le_bytes([0x90, 0xFC]), -55_000_000),
814 ];
815 for &(raw, micro) in table {
816 assert_eq!(temperature_to_micro_celsius(raw), micro, "raw {raw:#06x}");
817 }
818 }
819
820 #[test]
821 fn crc8_matches_the_published_check_value() {
822 // CRC-8/MAXIM-DOW check value for the ASCII string "123456789" is 0xA1.
823 assert_eq!(crc8(b"123456789"), 0xA1);
824 // An empty message leaves the zero-initialized register untouched.
825 assert_eq!(crc8(&[]), 0x00);
826 }
827
828 #[test]
829 fn crc_over_a_message_and_its_crc_is_zero() {
830 let data = [0x28, 0xFF, 0x64, 0x1E, 0x0C, 0x00, 0x00, 0x00];
831 let crc = crc8(&data);
832 let mut with_crc = [0u8; 9];
833 with_crc[..8].copy_from_slice(&data);
834 with_crc[8] = crc;
835 assert_eq!(crc8(&with_crc), 0x00);
836 }
837
838 #[test]
839 fn a_scratchpad_round_trips_through_parse() {
840 let mut bytes = [0x91, 0x01, 75, 0xF6, 0x7F, 0xFF, 0x00, 0x10, 0x00];
841 bytes[8] = crc8(&bytes[..8]);
842 let scratchpad = Scratchpad::parse(&bytes).expect("valid crc");
843 assert_eq!(scratchpad.raw_temperature(), 0x0191);
844 assert_eq!(scratchpad.temperature_micro_celsius(), 25_062_500);
845 assert_eq!(scratchpad.resolution(), Resolution::Bits12);
846 assert_eq!(scratchpad.alarm_high(), 75);
847 assert_eq!(scratchpad.alarm_low(), -10);
848 }
849
850 #[test]
851 fn a_corrupted_scratchpad_fails_the_crc() {
852 let mut bytes = [0x91, 0x01, 75, 0xF6, 0x7F, 0xFF, 0x00, 0x10, 0x00];
853 bytes[8] = crc8(&bytes[..8]);
854 bytes[0] ^= 0x01; // flip a temperature bit after the CRC was computed
855 assert_eq!(Scratchpad::parse(&bytes), Err(SensorError::Crc));
856 }
857
858 #[test]
859 fn resolution_config_bytes_match_the_datasheet() {
860 assert_eq!(Resolution::Bits9.config_byte(), 0x1F);
861 assert_eq!(Resolution::Bits10.config_byte(), 0x3F);
862 assert_eq!(Resolution::Bits11.config_byte(), 0x5F);
863 assert_eq!(Resolution::Bits12.config_byte(), 0x7F);
864 for resolution in [
865 Resolution::Bits9,
866 Resolution::Bits10,
867 Resolution::Bits11,
868 Resolution::Bits12,
869 ] {
870 assert_eq!(
871 Resolution::from_config_byte(resolution.config_byte()),
872 resolution
873 );
874 }
875 }
876 #[test]
877 fn a_built_scratchpad_parses_back_to_what_it_was_built_from() {
878 let built = Scratchpad::new(
879 temperature_from_celsius(25.0625, Resolution::Bits12),
880 Resolution::Bits12,
881 75,
882 -10,
883 );
884 let parsed = Scratchpad::parse(&built.to_bytes()).expect("a built scratchpad is valid");
885 assert_eq!(parsed, built);
886 assert_eq!(parsed.raw_temperature(), 0x0191);
887 assert_eq!(parsed.temperature_micro_celsius(), 25_062_500);
888 assert_eq!(parsed.alarm_high(), 75);
889 assert_eq!(parsed.alarm_low(), -10);
890 }
891
892 #[test]
893 fn a_built_scratchpad_carries_the_datasheet_temperature_bytes() {
894 // The +25.0625 °C row of the datasheet's temperature/data table is 0x0191, sent
895 // least-significant byte first, and byte 4 selects 12-bit resolution.
896 let bytes = Scratchpad::new(0x0191, Resolution::Bits12, 75, -10).to_bytes();
897 assert_eq!(bytes[0], 0x91);
898 assert_eq!(bytes[1], 0x01);
899 assert_eq!(bytes[4], 0x7F);
900 assert_eq!(bytes[8], crc8(&bytes[..8]));
901 }
902
903 #[test]
904 fn a_temperature_truncates_to_the_step_the_resolution_resolves() {
905 // 12-bit resolves a sixteenth of a degree; the coarser settings read the bits
906 // below their step as zero, so the same temperature lands on a lower count.
907 assert_eq!(temperature_from_celsius(25.0625, Resolution::Bits12), 401);
908 assert_eq!(temperature_from_celsius(25.0625, Resolution::Bits11), 400);
909 assert_eq!(temperature_from_celsius(25.0625, Resolution::Bits10), 400);
910 assert_eq!(temperature_from_celsius(25.0625, Resolution::Bits9), 400);
911 assert_eq!(
912 temperature_from_micro_celsius(-10_062_500, Resolution::Bits12),
913 -161
914 );
915 }
916
917 #[test]
918 fn every_temperature_register_survives_a_round_trip() {
919 for raw in [-880i16, -161, -1, 0, 1, 401, 1250] {
920 let micro = temperature_to_micro_celsius(raw);
921 assert_eq!(
922 temperature_from_micro_celsius(micro, Resolution::Bits12),
923 raw
924 );
925 }
926 }
927 #[test]
928 fn the_kernel_text_decodes_and_its_verdict_is_honored() {
929 let raw = temperature_from_celsius(20.8125, Resolution::Bits12);
930 let bytes = Scratchpad::new(raw, Resolution::Bits12, 75, -10).to_bytes();
931 let mut hex = alloc::string::String::new();
932 for byte in bytes {
933 hex.push_str(&alloc::format!("{byte:02x} "));
934 }
935 let text = alloc::format!("{hex}: crc={:02x} YES\n{hex}t=20812\n", bytes[8]);
936 let scratchpad = parse_w1_slave(&text).unwrap();
937 assert_eq!(scratchpad.temperature_micro_celsius(), 20_812_500);
938
939 let rejected = alloc::format!("{hex}: crc=00 NO\n{hex}t=20812\n");
940 assert_eq!(parse_w1_slave(&rejected), Err(SensorError::Crc));
941 assert_eq!(parse_w1_slave(""), Err(SensorError::Invalid));
942 assert_eq!(
943 parse_w1_slave("4d 01 : crc=e8 YES\n"),
944 Err(SensorError::Invalid)
945 );
946 assert_eq!(
947 parse_w1_slave("zz 01 4b 46 7f ff 03 10 e8 : crc=e8 YES\n"),
948 Err(SensorError::Invalid)
949 );
950 }
951
952 #[cfg(feature = "alloc")]
953 #[test]
954 fn the_kernel_text_prints_what_w1_therm_prints() {
955 // Rows of the datasheet's temperature/data table, with the millidegrees the kernel
956 // prints for each: the raw count times 1000 over 16, truncated toward zero, so
957 // -25.0625 C prints as -25062 rather than -25063.
958 let rows: &[(i16, i32)] = &[
959 (0x07D0, 125_000),
960 (0x0191, 25_062),
961 (0x0008, 500),
962 (0x0000, 0),
963 (i16::from_le_bytes([0x5E, 0xFF]), -10_125),
964 (i16::from_le_bytes([0x6F, 0xFE]), -25_062),
965 (i16::from_le_bytes([0x90, 0xFC]), -55_000),
966 ];
967 for &(raw, millidegrees) in rows {
968 let scratchpad = Scratchpad::new(raw, Resolution::Bits12, 75, -10);
969 let bytes = scratchpad.to_bytes();
970 let text = w1_slave_text(&scratchpad);
971 let (first, second) = text.split_once('\n').expect("two lines");
972 let hex: alloc::vec::Vec<alloc::string::String> = bytes
973 .iter()
974 .map(|byte| alloc::format!("{byte:02x}"))
975 .collect();
976 assert_eq!(
977 first,
978 alloc::format!("{} : crc={:02x} YES", hex.join(" "), bytes[8])
979 );
980 assert_eq!(
981 second,
982 alloc::format!("{} t={millidegrees}\n", hex.join(" "))
983 );
984 assert_eq!(parse_w1_slave(&text), Ok(scratchpad));
985 }
986
987 // A clone that sets bit 7 of the configuration register carries two more
988 // temperature bits there, which the kernel folds in before dividing by 64.
989 let mut clone = [0x91, 0x01, 75, 0xF6, 0x83, 0xFF, 0x00, 0x10, 0x00];
990 clone[8] = crc8(&clone[..8]);
991 assert_eq!(kernel_millidegrees(&clone), (0x0191 * 4 + 3) * 1000 / 64);
992 }
993}