mirror of
https://github.com/andreili/klipper.git
synced 2025-08-24 03:44:06 +02:00
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>
288 lines
8.7 KiB
C
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);
|