git.haldean.org sousvide / 1eb668e
Add GPIO heater relay support. Will Haldean Brown 9 years ago
4 changed file(s) with 177 addition(s) and 48 deletion(s). Raw diff Collapse all Expand all
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 }
00 package main
11
22 import (
3 "log"
4 "math/rand"
3 "flag"
4 "fmt"
5 "math"
6 "os"
57 "sync"
68 "time"
79 )
1012 InterruptDelay = 1 * time.Second
1113 LogFile = "runlog.txt"
1214 HistoryLength = 2048
13 LowpassSamples = 3
15 LowpassSamples = 2
16 AccErrorWindow = 32
1417 )
1518
1619 type SousVide struct {
1922 Target Celsius
2023 History []HistorySample
2124 Pid PidParams
25 Gpio GpioParams
2226 DataLock sync.Mutex
27 AccError float64
2328 lastPOutput float64
2429 lastIOutput float64
2530 lastDOutput float64
3237 Temp float64
3338 Target float64
3439 AbsError float64
40 AccError float64
3541 Pid PidParams
3642 POutput float64
3743 IOutput float64
4349 P float64
4450 I float64
4551 D float64
52 }
53
54 type GpioParams struct {
55 ThermFd *os.File
56 HeaterFd *os.File
57 Stub bool
4658 }
4759
4860 type Celsius float64
6072 Temp: float64(s.Temp),
6173 Target: float64(s.Target),
6274 AbsError: float64(s.Error()),
75 AccError: s.AccError,
6376 Pid: s.Pid,
6477 POutput: s.lastPOutput,
6578 IOutput: s.lastIOutput,
7790 } else {
7891 s.History = append(s.History, s.Snapshot())
7992 }
80 }
8193
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++
99100 }
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
125102 }
126103
127104 func (s *SousVide) SetTarget(target Celsius) {
137114 }
138115
139116 func main() {
117 flag.Parse()
118
140119 s := New()
141120 s.Target = 200
142121 s.Pid.P = 10
143122 s.Pid.I = 0.1
144123 s.Pid.D = 10
145124
125 err := s.InitGpio()
126 if err != nil {
127 fmt.Printf("could not initialize gpio: %v\n", err)
128 return
129 }
130
146131 go s.StartControlLoop()
147132 s.StartServer()
148133 }
4949 width: 100pt;
5050 text-align: right;
5151 }
52
53 .subtext {
54 display: block;
55 font-size: 9pt;
56 margin-top: -2pt;
57 color: #CCC;
58 }
5259 </style>
5360 <link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700|Source+Code+Pro' rel='stylesheet' type='text/css'>
5461
8693 <td class="label error_label">Error:</td>
8794 <td class="val" id="err_td"><span id="abs_err"></span>&deg; C</td>
8895 </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">&plusmn;<span id="acc_err"></span> &deg;C/sample</td>
104 </tr>
89105 </table>
90106 <img src="/plot" id="plot">
91107 </section>
0 var tempElem, absErrElem, targetElem, absErrTdElem, heatingElem, plotElem
0 var tempElem, absErrElem, targetElem, heatingElem, plotElem, accErrElem
11 var targetDisplayElem, targetChangeElem, targetInputElem
22 var pInputElem, iInputElem, dInputElem
33
2323 $(tempElem).text(temp.toFixed(2));
2424 $(targetElem).text(target.toFixed(2));
2525 $(absErrElem).text((err >= 0 ? '+' : '') + err.toFixed(2));
26 $(accErrElem).text(data.AccError.toFixed(2))
2627
2728 pInputElem.setAttribute('value', data.Pid.P)
2829 iInputElem.setAttribute('value', data.Pid.I)
4344 tempElem = document.getElementById('temp')
4445 absErrElem = document.getElementById('abs_err')
4546 targetElem = document.getElementById('target')
46 absErrTdElem = document.getElementById('err_td')
4747 heatingElem = document.getElementById('heating')
4848 plotElem = document.getElementById('plot')
49 accErrElem = document.getElementById('acc_err')
4950
5051 targetChangeElem = document.getElementById('target_change')
5152 targetDisplayElem = document.getElementById('target_display')