klipper/src/linux/sensor_ds18b20.c
functionpointer 2dc20c011d ds18b20: Allow some read errors
Allows a limited number of DS18B20 read failures
before stopping the printer. This is designed to
tolerate spurious read errors, while still stopping
for serious issues.

The printer will stop when the sensor

fails to report a value five times in a row.

Implementation works as follows:
The MCU reports any read errors using a new "fault"
parameter in its answers.
The Python code tracks the number of errors
and triggers the shutdown. This paves the way for
more sophisticated error handling in the future,
as well as an example for other sensors to follow.

Signed-off-by: Lorenzo Pfeifer <Lorenzo.Pfeifer+github@googlemail.com>
2022-06-08 12:39:12 -04:00

288 lines
8.7 KiB
C

// Communicate with a DS18B20 temperature sensor on linux
//
// Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
#include <fcntl.h> // open
#include <stdio.h> // snprintf
#include <stdlib.h> // atof
#include <string.h> // memchr
#include <unistd.h> // read
#include <pthread.h> // pthread_create
#include <time.h> // clock_gettime
#include "basecmd.h" // oid_alloc
#include "board/irq.h" // irq_disable
#include "command.h" // DECL_COMMAND
#include "internal.h" // report_errno
#include "sched.h" // DECL_SHUTDOWN
#define W1_READ_TIMEOUT_SEC 5
// Status of a sensor
enum {
W1_IDLE = 0, // No read requested yet
W1_READ_REQUESTED = 1, // Reading or waiting to read
W1_READY = 2, // Read complete, waiting to report
W1_ERROR = 3, // Request shutdown
};
enum {
TS_PENDING = 1,
};
struct ds18_s {
struct timer timer;
uint32_t rest_time;
int32_t min_value, max_value;
uint8_t flags;
// Set by main thread in configuration phase.
// Should only be accessed by reader thread after configuration.
int fd;
// Used for guarding shared members.
pthread_mutex_t lock;
pthread_cond_t cond;
// Protect all reads/writes to the following members using the mutex
// once reader thread is initialized.
int temperature;
struct timespec request_time;
uint8_t status;
uint8_t error_count;
uint8_t max_error_count;
};
// Lock ds18_s mutex, set error status, unlock mutex.
static void
locking_handle_read_error(struct ds18_s *d)
{
pthread_mutex_lock(&d->lock);
d->status = W1_ERROR;
d->error_count++;
if (d->error_count <= d->max_error_count) {
pthread_mutex_unlock(&d->lock);
} else {
pthread_mutex_unlock(&d->lock);
pthread_exit(NULL);
}
}
// The kernel interface to DS18B20 sensors is a sysfs entry that blocks for
// around 750ms when read. Most of this is idle time waiting for the result
// to be ready. Read in a separate thread in order to avoid blocking time-
// sensitive work.
static void *
reader_start_routine(void *param) {
struct ds18_s *d = param;
for (;;) {
// Wait for requests to read temperature sensors
pthread_mutex_lock(&d->lock);
while (d->status != W1_READ_REQUESTED) {
pthread_cond_wait(&d->cond, &d->lock);
}
pthread_mutex_unlock(&d->lock);
// Read temp.
// The temperature data is at the end of the report, after a "t=".
// Example (3.062 degrees C):
//
// 31 00 4b 46 7f ff 0c 10 77 : crc=77 YES
// 31 00 4b 46 7f ff 0c 10 77 t=3062
char data[128];
int ret = read(d->fd, data, sizeof(data)-1);
if (ret < 0) {
report_errno("read DS18B20", ret);
locking_handle_read_error(d);
continue;
}
data[ret] = '\0';
char *temp_string = strstr(data, "t=");
if (temp_string == NULL || temp_string[2] == '\0') {
locking_handle_read_error(d);
continue;
}
// Don't pass 't' and '=' to atoi
temp_string += 2;
int val = atoi(temp_string);
// Store temperature
pthread_mutex_lock(&d->lock);
d->status = W1_READY;
d->temperature = val;
pthread_mutex_unlock(&d->lock);
// Seek file in preparation of next read
ret = lseek(d->fd, 0, SEEK_SET);
if (ret < 0) {
report_errno("seek DS18B20", ret);
locking_handle_read_error(d);
continue;
}
}
pthread_exit(NULL);
}
static struct task_wake ds18_wake;
static uint_fast8_t
ds18_event(struct timer *timer)
{
struct ds18_s *d = container_of(timer, struct ds18_s, timer);
// Trigger task to read and send results
sched_wake_task(&ds18_wake);
d->flags |= TS_PENDING;
d->timer.waketime += d->rest_time;
return SF_RESCHEDULE;
}
void
command_config_ds18b20(uint32_t *args)
{
// Open kernel port
uint8_t serial_len = args[1];
uint8_t *serial = command_decode_ptr(args[2]);
if (memchr(serial, '/', serial_len))
goto fail1;
char fname[56];
snprintf(fname, sizeof(fname), "/sys/bus/w1/devices/%.*s/w1_slave"
, serial_len, serial);
int fd = open(fname, O_RDONLY|O_CLOEXEC);
if (fd < 0) {
report_errno("open ds18", fd);
goto fail2;
}
struct ds18_s *d = oid_alloc(args[0], command_config_ds18b20, sizeof(*d));
d->max_error_count = args[3];
d->error_count = 0;
d->timer.func = ds18_event;
d->fd = fd;
d->status = W1_IDLE;
int ret;
ret = pthread_mutex_init(&d->lock, NULL);
if (ret)
goto fail3;
ret = pthread_cond_init(&d->cond, NULL);
if (ret)
goto fail4;
pthread_t reader_tid; // Not used
timer_disable_signals();
ret = pthread_create(&reader_tid, NULL, reader_start_routine, d);
timer_enable_signals();
if (ret)
goto fail5;
return;
fail1:
shutdown("Invalid DS18B20 serial id, must not contain '/'");
fail2:
shutdown("Invalid DS18B20 serial id, could not open for reading");
fail3:
shutdown("Could not start DS18B20 reader thread (mutex init)");
fail4:
shutdown("Could not start DS18B20 reader thread (cond init)");
fail5:
shutdown("Could not start DS18B20 reader thread");
}
DECL_COMMAND(command_config_ds18b20,
"config_ds18b20 oid=%c serial=%*s max_error_count=%c");
void
command_query_ds18b20(uint32_t *args)
{
struct ds18_s *d = oid_lookup(args[0], command_config_ds18b20);
sched_del_timer(&d->timer);
d->timer.waketime = args[1];
d->rest_time = args[2];
if (! d->rest_time)
return;
d->min_value = args[3];
d->max_value = args[4];
sched_add_timer(&d->timer);
}
DECL_COMMAND(command_query_ds18b20,
"query_ds18b20 oid=%c clock=%u rest_ticks=%u"
" min_value=%i max_value=%i");
// Report temperature if ready, and set back to pending.
static void
ds18_send_and_request(struct ds18_s *d, uint32_t next_begin_time, uint8_t oid)
{
struct timespec request_time;
int ret = clock_gettime(CLOCK_MONOTONIC, &request_time);
if (ret == -1) {
report_errno("get monotonic clock time", ret);
try_shutdown("Error getting monotonic clock time");
return;
}
pthread_mutex_lock(&d->lock);
if (d->status == W1_ERROR) {
if (d->error_count > d->max_error_count) {
try_shutdown("Error reading DS18B20 sensor");
pthread_mutex_unlock(&d->lock);
return;
} else {
sendf("ds18b20_result oid=%c next_clock=%u value=%i fault=%u"
, oid, next_begin_time, d->temperature, d->error_count);
d->status = W1_READ_REQUESTED;
}
} else if (d->status == W1_IDLE) {
// This happens the first time requesting a temperature.
// Nothing to report yet.
d->request_time = request_time;
d->status = W1_READ_REQUESTED;
} else if (d->status == W1_READY) {
// Report the previous temperature and request a new one.
sendf("ds18b20_result oid=%c next_clock=%u value=%i fault=%u"
, oid, next_begin_time, d->temperature, 0);
if (d->temperature < d->min_value || d->temperature > d->max_value) {
pthread_mutex_unlock(&d->lock);
try_shutdown("DS18B20 out of range");
return;
}
d->request_time = request_time;
d->status = W1_READ_REQUESTED;
d->error_count = 0; //successful reading, reset error count
} else if (d->status == W1_READ_REQUESTED) {
// Reader thread is already reading (or will be soon).
// This can happen if two queries come in quick enough
// succession. Wait for the existing read to finish.
// This could also happen if the reader thread has hung. In that case,
// shut down the MCU. To tell the difference, see if the request time
// is too far in the past.
if (request_time.tv_sec - d->request_time.tv_sec > W1_READ_TIMEOUT_SEC)
{
pthread_mutex_unlock(&d->lock);
try_shutdown("DS18B20 sensor didn't respond in time");
return;
}
}
pthread_cond_signal(&d->cond);
pthread_mutex_unlock(&d->lock);
}
// task to read temperature and send response
void
ds18_task(void)
{
if (!sched_check_wake(&ds18_wake))
return;
uint8_t oid;
struct ds18_s *d;
foreach_oid(oid, d, command_config_ds18b20) {
if (!(d->flags & TS_PENDING))
continue;
irq_disable();
uint32_t next_begin_time = d->timer.waketime;
d->flags &= ~TS_PENDING;
irq_enable();
ds18_send_and_request(d, next_begin_time, oid);
}
}
DECL_TASK(ds18_task);