Add GPIO heater relay support.
Will Haldean Brown
9 years ago
0 | // hat tip to github.com/aqua/raspberrypi for inspiration for the GPIO code | |
1 | ||
2 | package main | |
3 | ||
4 | import ( | |
5 | "flag" | |
6 | "fmt" | |
7 | "math/rand" | |
8 | "os" | |
9 | "time" | |
10 | ) | |
11 | ||
12 | const HeaterGpioPin = 18 | |
13 | ||
14 | var StubGpio = flag.Bool("stub_gpio", false, "stub GPIO calls for testing") | |
15 | ||
16 | func checkHeaterExported() error { | |
17 | _, err := os.Stat(fmt.Sprintf("/sys/class/gpio/gpio%d", HeaterGpioPin)) | |
18 | if err == nil { | |
19 | return nil | |
20 | } | |
21 | if !os.IsNotExist(err) { | |
22 | return err | |
23 | } | |
24 | ||
25 | fd, err := os.OpenFile( | |
26 | "/sys/class/gpio/export", os.O_WRONLY | os.O_SYNC, 0666) | |
27 | if err != nil { | |
28 | return err | |
29 | } | |
30 | defer fd.Close() | |
31 | ||
32 | _, err = fmt.Fprintf(fd, "%d\n", HeaterGpioPin) | |
33 | return err | |
34 | } | |
35 | ||
36 | func setHeaterOutputMode() error { | |
37 | fd, err := os.OpenFile( | |
38 | fmt.Sprintf("/sys/class/gpio/gpio%d/direction", HeaterGpioPin), | |
39 | os.O_WRONLY | os.O_SYNC, 0666) | |
40 | if err != nil { | |
41 | return err | |
42 | } | |
43 | defer fd.Close() | |
44 | ||
45 | fmt.Fprintf(fd, "out") | |
46 | return nil | |
47 | } | |
48 | ||
49 | func (s *SousVide) InitGpio() error { | |
50 | s.Gpio.Stub = *StubGpio | |
51 | if s.Gpio.Stub { | |
52 | s.Gpio.HeaterFd = os.Stdout | |
53 | return nil | |
54 | } | |
55 | ||
56 | err := checkHeaterExported() | |
57 | if err != nil { | |
58 | return err | |
59 | } | |
60 | err = setHeaterOutputMode() | |
61 | if err != nil { | |
62 | return err | |
63 | } | |
64 | s.Gpio.HeaterFd, err = os.OpenFile( | |
65 | fmt.Sprintf("/sys/class/gpio/gpio%d/value", HeaterGpioPin), | |
66 | os.O_WRONLY | os.O_SYNC, 0666) | |
67 | if err != nil { | |
68 | return err | |
69 | } | |
70 | return nil | |
71 | } | |
72 | ||
73 | func (s *SousVide) StartControlLoop() { | |
74 | tick := time.Tick(InterruptDelay) | |
75 | for _ = range tick { | |
76 | s.DataLock.Lock() | |
77 | s.UpdateHardware() | |
78 | ||
79 | if s.Heating { | |
80 | s.Temp += Celsius(10 * rand.Float64()) | |
81 | } else { | |
82 | s.Temp -= Celsius(10 * rand.Float64()) | |
83 | } | |
84 | ||
85 | co := s.ControllerResult() | |
86 | s.Heating = co > 0 | |
87 | ||
88 | s.checkpoint() | |
89 | s.DataLock.Unlock() | |
90 | } | |
91 | } | |
92 | ||
93 | func (s *SousVide) ControllerResult() Celsius { | |
94 | s.lastPOutput = s.Pid.P * float64(s.Error()) | |
95 | ||
96 | if len(s.History) > 0 { | |
97 | integral := float64(0) | |
98 | for _, h := range s.History { | |
99 | integral += h.AbsError | |
100 | } | |
101 | integral /= float64(len(s.History)) | |
102 | s.lastIOutput = s.Pid.I * integral | |
103 | } | |
104 | ||
105 | // ignore derivative term if we have no history to use | |
106 | if len(s.History) > LowpassSamples { | |
107 | // use weighted window over three samples instead of two to act as a | |
108 | // low-pass filter | |
109 | N := len(s.History) | |
110 | d := (s.History[N-LowpassSamples-1].Temp - s.History[N-1].Temp) / 2 | |
111 | s.lastDOutput = s.Pid.D * d | |
112 | } | |
113 | ||
114 | s.lastControl = s.lastPOutput + s.lastIOutput + s.lastDOutput | |
115 | return Celsius(s.lastControl) | |
116 | } | |
117 | ||
118 | func (s *SousVide) UpdateHardware() { | |
119 | var heatVal string | |
120 | if s.Heating { | |
121 | heatVal = "1\n" | |
122 | } else { | |
123 | heatVal = "0\n" | |
124 | } | |
125 | fmt.Fprintf(s.Gpio.HeaterFd, heatVal) | |
126 | } |
0 | 0 | package main |
1 | 1 | |
2 | 2 | import ( |
3 | "log" | |
4 | "math/rand" | |
3 | "flag" | |
4 | "fmt" | |
5 | "math" | |
6 | "os" | |
5 | 7 | "sync" |
6 | 8 | "time" |
7 | 9 | ) |
10 | 12 | InterruptDelay = 1 * time.Second |
11 | 13 | LogFile = "runlog.txt" |
12 | 14 | HistoryLength = 2048 |
13 | LowpassSamples = 3 | |
15 | LowpassSamples = 2 | |
16 | AccErrorWindow = 32 | |
14 | 17 | ) |
15 | 18 | |
16 | 19 | type SousVide struct { |
19 | 22 | Target Celsius |
20 | 23 | History []HistorySample |
21 | 24 | Pid PidParams |
25 | Gpio GpioParams | |
22 | 26 | DataLock sync.Mutex |
27 | AccError float64 | |
23 | 28 | lastPOutput float64 |
24 | 29 | lastIOutput float64 |
25 | 30 | lastDOutput float64 |
32 | 37 | Temp float64 |
33 | 38 | Target float64 |
34 | 39 | AbsError float64 |
40 | AccError float64 | |
35 | 41 | Pid PidParams |
36 | 42 | POutput float64 |
37 | 43 | IOutput float64 |
43 | 49 | P float64 |
44 | 50 | I float64 |
45 | 51 | D float64 |
52 | } | |
53 | ||
54 | type GpioParams struct { | |
55 | ThermFd *os.File | |
56 | HeaterFd *os.File | |
57 | Stub bool | |
46 | 58 | } |
47 | 59 | |
48 | 60 | type Celsius float64 |
60 | 72 | Temp: float64(s.Temp), |
61 | 73 | Target: float64(s.Target), |
62 | 74 | AbsError: float64(s.Error()), |
75 | AccError: s.AccError, | |
63 | 76 | Pid: s.Pid, |
64 | 77 | POutput: s.lastPOutput, |
65 | 78 | IOutput: s.lastIOutput, |
77 | 90 | } else { |
78 | 91 | s.History = append(s.History, s.Snapshot()) |
79 | 92 | } |
80 | } | |
81 | 93 | |
82 | func (s *SousVide) StartControlLoop() { | |
83 | tick := time.Tick(InterruptDelay) | |
84 | for _ = range tick { | |
85 | s.DataLock.Lock() | |
86 | if s.Heating { | |
87 | s.Temp += Celsius(10 * rand.Float64()) | |
88 | } else { | |
89 | s.Temp -= Celsius(10 * rand.Float64()) | |
90 | } | |
91 | log.Printf("read temperature %f deg C", s.Temp) | |
92 | ||
93 | co := s.ControllerResult() | |
94 | s.Heating = co > 0 | |
95 | log.Printf("controller returned %v", co) | |
96 | ||
97 | s.checkpoint() | |
98 | s.DataLock.Unlock() | |
94 | s.AccError = 0 | |
95 | N := len(s.History) | |
96 | l := float64(0) | |
97 | for i := N - 1; i >= N-AccErrorWindow-1 && i >= 0; i-- { | |
98 | s.AccError += math.Abs(s.History[i].AbsError) | |
99 | l++ | |
99 | 100 | } |
100 | } | |
101 | ||
102 | func (s *SousVide) ControllerResult() Celsius { | |
103 | s.lastPOutput = s.Pid.P * float64(s.Error()) | |
104 | ||
105 | if len(s.History) > 0 { | |
106 | integral := float64(0) | |
107 | for _, h := range s.History { | |
108 | integral += h.AbsError | |
109 | } | |
110 | integral /= float64(len(s.History)) | |
111 | s.lastIOutput = s.Pid.I * integral | |
112 | } | |
113 | ||
114 | // ignore derivative term if we have no history to use | |
115 | if len(s.History) > LowpassSamples { | |
116 | // use weighted window over three samples instead of two to act as a | |
117 | // low-pass filter | |
118 | N := len(s.History) | |
119 | d := (s.History[N-LowpassSamples-1].Temp - s.History[N-1].Temp) / 2 | |
120 | s.lastDOutput = s.Pid.D * d | |
121 | } | |
122 | ||
123 | s.lastControl = s.lastPOutput + s.lastIOutput + s.lastDOutput | |
124 | return Celsius(s.lastControl) | |
101 | s.AccError /= l | |
125 | 102 | } |
126 | 103 | |
127 | 104 | func (s *SousVide) SetTarget(target Celsius) { |
137 | 114 | } |
138 | 115 | |
139 | 116 | func main() { |
117 | flag.Parse() | |
118 | ||
140 | 119 | s := New() |
141 | 120 | s.Target = 200 |
142 | 121 | s.Pid.P = 10 |
143 | 122 | s.Pid.I = 0.1 |
144 | 123 | s.Pid.D = 10 |
145 | 124 | |
125 | err := s.InitGpio() | |
126 | if err != nil { | |
127 | fmt.Printf("could not initialize gpio: %v\n", err) | |
128 | return | |
129 | } | |
130 | ||
146 | 131 | go s.StartControlLoop() |
147 | 132 | s.StartServer() |
148 | 133 | } |
49 | 49 | width: 100pt; |
50 | 50 | text-align: right; |
51 | 51 | } |
52 | ||
53 | .subtext { | |
54 | display: block; | |
55 | font-size: 9pt; | |
56 | margin-top: -2pt; | |
57 | color: #CCC; | |
58 | } | |
52 | 59 | </style> |
53 | 60 | <link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700|Source+Code+Pro' rel='stylesheet' type='text/css'> |
54 | 61 | |
86 | 93 | <td class="label error_label">Error:</td> |
87 | 94 | <td class="val" id="err_td"><span id="abs_err"></span>° C</td> |
88 | 95 | </tr> |
96 | <tr> | |
97 | <td class="label"> | |
98 | Mean error per sample: | |
99 | <span class="subtext"> | |
100 | Taken over 32-sample sliding window | |
101 | </span> | |
102 | </td> | |
103 | <td class="val">±<span id="acc_err"></span> °C/sample</td> | |
104 | </tr> | |
89 | 105 | </table> |
90 | 106 | <img src="/plot" id="plot"> |
91 | 107 | </section> |
0 | var tempElem, absErrElem, targetElem, absErrTdElem, heatingElem, plotElem | |
0 | var tempElem, absErrElem, targetElem, heatingElem, plotElem, accErrElem | |
1 | 1 | var targetDisplayElem, targetChangeElem, targetInputElem |
2 | 2 | var pInputElem, iInputElem, dInputElem |
3 | 3 | |
23 | 23 | $(tempElem).text(temp.toFixed(2)); |
24 | 24 | $(targetElem).text(target.toFixed(2)); |
25 | 25 | $(absErrElem).text((err >= 0 ? '+' : '') + err.toFixed(2)); |
26 | $(accErrElem).text(data.AccError.toFixed(2)) | |
26 | 27 | |
27 | 28 | pInputElem.setAttribute('value', data.Pid.P) |
28 | 29 | iInputElem.setAttribute('value', data.Pid.I) |
43 | 44 | tempElem = document.getElementById('temp') |
44 | 45 | absErrElem = document.getElementById('abs_err') |
45 | 46 | targetElem = document.getElementById('target') |
46 | absErrTdElem = document.getElementById('err_td') | |
47 | 47 | heatingElem = document.getElementById('heating') |
48 | 48 | plotElem = document.getElementById('plot') |
49 | accErrElem = document.getElementById('acc_err') | |
49 | 50 | |
50 | 51 | targetChangeElem = document.getElementById('target_change') |
51 | 52 | targetDisplayElem = document.getElementById('target_display') |