sd-card: Only add header to the results file if it’s empty
[brewing-logger:firmware.git] / krausen-level.c
1 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
2 /*
3  * Brewing Logger.
4  * Copyright (C) Philip Withnall 2012 <philip@tecnocode.co.uk>
5  *
6  * Brewing Logger is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Brewing Logger is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Brewing Logger.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 #include <avr/io.h>
21
22 #include "adc.h"
23 #include "common.h"
24 #include "config.h"
25 #include "krausen-level.h"
26
27 /**
28  * \file
29  * \brief Krausen level sensor input handling.
30  *
31  * Handling of the krausen level sensor, which uses triangulated IR beams to work out the distance from the lid of the fermentation vessel to the top
32  * of the krausen (the foamy head on top of the fermenting liquid). This includes taking measurements and normalising them against the sensor's
33  * response curve.
34  */
35
36 void krausen_level_init (void)
37 {
38         /* We assume ADC0 below when setting ADMUX and DIDR0. */
39         STATIC_ASSERT (PROXIMITY_SENSOR_IN == PA1 /* == ADC1 */);
40
41         /* Set the proximity sensor pin as an input. */
42         PROXIMITY_SENSOR_DDR &= ~(1 << PROXIMITY_SENSOR_IN);
43
44         adc_init ();
45         DIDR0 |= (1 << ADC1D); /* disable digital input on ADC1 to save power */
46 }
47
48 /**
49  * \brief Convert measured voltage to centimetres.
50  *
51  * Convert a voltage output from a GP2Y0A21YK0F to a fixed point distance in tenths of centimetres. This is not a linear conversion, since the
52  * sensor's response is exponential. We approximate by linear interpolation between the data points given in the lower graph in Figure 2.
53  *
54  * Reference: GP2Y0A21YK0F data sheet, Figure 2.
55  */
56 static inline KrausenLevelReading _gp2y0a21yk0f_convert_voltage_to_distance (AdcReading voltage)
57 {
58         /* This table is lifted straight from the lower graph in Figure 2. It must be kept in increasing order of voltage. Note that the distances in
59          * this table are in centimetres, rather than tenths of centimetres, so need additional conversion to KrausenLevelReading format. */
60         static struct {
61                 AdcReading voltage; /* Volts */
62                 uint8_t distance; /* centimetres */
63         } characteristics[] = {
64                 { ADC_READING_FROM_VOLTAGE (0.4), 80 },
65                 { ADC_READING_FROM_VOLTAGE (0.6), 50 },
66                 { ADC_READING_FROM_VOLTAGE (0.7), 40 },
67                 { ADC_READING_FROM_VOLTAGE (0.9), 30 },
68                 { ADC_READING_FROM_VOLTAGE (1.1), 25 },
69                 { ADC_READING_FROM_VOLTAGE (1.3), 20 },
70                 { ADC_READING_FROM_VOLTAGE (1.6), 15 },
71                 { ADC_READING_FROM_VOLTAGE (2.3), 10 },
72                 { ADC_READING_FROM_VOLTAGE (2.7),  8 },
73                 { ADC_READING_FROM_VOLTAGE (3.0),  7 },
74                 { ADC_READING_FROM_VOLTAGE (3.1),  6 },
75                 /* data point for 5cm omitted because its voltage is lower than 3.1V */
76         };
77         #define NUM_CHARACTERISTICS (sizeof (characteristics) / sizeof (*characteristics))
78
79         uint8_t i = 0;
80         int16_t d_distance;
81         uint16_t d_voltage;
82
83         /* Handle the cases where voltage is lower than the lowest entry in the table or higher than the highest entry. Just clamp to the extremum's
84          * distance. */
85         if (voltage <= characteristics[0].voltage) {
86                 return (KrausenLevelReading) characteristics[0].distance * 10;
87         } else if (voltage >= characteristics[NUM_CHARACTERISTICS - 1].voltage) {
88                 return (KrausenLevelReading) characteristics[NUM_CHARACTERISTICS - 1].distance * 10;
89         }
90
91         /* Handle the typical cases where voltage lies within the table. */
92         for (i = 0; i + 1 < (uint8_t) NUM_CHARACTERISTICS; i++) {
93                 /* Does the voltage fall in the interval between characteristics[i] and characteristics[i + 1]? If so, linearly interpolate between
94                  * the two distances. */
95                 if (voltage >= characteristics[i].voltage && voltage < characteristics[i + 1].voltage) {
96                         break;
97                 }
98         }
99
100         /* We should still be inside the table, and some invariants should hold. */
101         DYNAMIC_ASSERT (i < NUM_CHARACTERISTICS - 1);
102         DYNAMIC_ASSERT (voltage >= characteristics[i].voltage);
103         DYNAMIC_ASSERT (characteristics[i + 1].voltage > characteristics[i].voltage);
104
105         /* Linear interpolation. */
106         d_distance = (characteristics[i + 1].distance - characteristics[i].distance); /* guaranteed to be negative */
107         d_voltage = (characteristics[i + 1].voltage - characteristics[i].voltage); /* guaranteed to be positive */
108
109         return ((int32_t) characteristics[i].distance * 10 /* convert to mm */) + ((int32_t) d_distance * 10 /* convert to mm */) *
110                (int32_t) (voltage - characteristics[i].voltage) / (int32_t) d_voltage;
111 }
112
113 MeasurementResponse krausen_level_take_measurement (KrausenLevelReading *measurement)
114 {
115         /* Make the measurement. */
116         *measurement = _gp2y0a21yk0f_convert_voltage_to_distance (adc_take_measurement (ADC1));
117
118         return MEASUREMENT_SUCCESS;
119 }