aboutsummaryrefslogtreecommitdiffstats
path: root/src/linux/sensor_ds18b20.c
blob: 2e9151ae1eebe429e7f3702b5ad284e80636799d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
// 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);