git.haldean.org sousvide / 0dcc7e2
Add ability to tune PID params from web interface Will Haldean Brown 9 years ago
5 changed file(s) with 153 addition(s) and 50 deletion(s). Raw diff Collapse all Expand all
1111 const (
1212 imgWidth = 600
1313 imgHeight = 400
14 useSvg = true
14 useSvg = true
1515 )
1616
1717 func (s *SousVide) GenerateChart(w http.ResponseWriter, req *http.Request) {
2828 c.YRange.TicSetting.Grid = 1
2929 c.YRange.TicSetting.HideLabels = true
3030
31 s.HistoryLock.Lock()
31 s.DataLock.Lock()
3232 h := &s.History
3333
34 c.XRange.Fixed(0, float64(h.End) + 1, float64(h.End / 10))
34 c.XRange.Fixed(0, float64(h.End)+1, float64(h.End/10))
3535
3636 temps := make([]chart.EPoint, 0, h.End)
3737 targets := make([]chart.EPoint, 0, h.End)
6262 }
6363 errs = append(errs, ep)
6464 }
65 s.HistoryLock.Unlock()
65 s.DataLock.Unlock()
6666
6767 c.AddData("Temperature", temps, chart.PlotStyleLines, chart.Style{
6868 LineColor: color.NRGBA{0xFF, 0x00, 0x00, 0xFF}, LineWidth: 2,
11
22 import (
33 "encoding/json"
4 "errors"
45 "fmt"
56 "log"
67 "net/http"
1112 Temps []float64
1213 Targets []float64
1314 LogErrors []float64
15 PidParams PidParams
16 }
17
18 func floatData(w http.ResponseWriter, req *http.Request, arg string) (float64, error) {
19 valStr := req.FormValue(arg)
20 if valStr == "" {
21 http.Error(
22 w, fmt.Sprintf("missing argument %s", arg), http.StatusBadRequest)
23 return 0, errors.New("argument not supplied in request")
24 }
25 val, err := strconv.ParseFloat(valStr, 64)
26 if err != nil {
27 http.Error(
28 w, fmt.Sprintf("could not parse %s: %v", arg, err),
29 http.StatusBadRequest)
30 return 0, err
31 }
32 return val, nil
1433 }
1534
1635 func (s *SousVide) StartServer() {
1736 http.HandleFunc("/api_data", func(resp http.ResponseWriter, req *http.Request) {
1837 resp.Header().Set("Content-type", "application/json")
1938
20 s.HistoryLock.Lock()
21 defer s.HistoryLock.Unlock()
39 s.DataLock.Lock()
40 defer s.DataLock.Unlock()
2241
2342 h := &s.History
2443 a := apiData{
2544 h.Temps[:h.End],
2645 h.Targets[:h.End],
2746 h.LogErrors[:h.End],
47 s.Pid,
2848 }
2949
3050 b, err := json.Marshal(a)
3555 })
3656
3757 http.HandleFunc("/target", func(resp http.ResponseWriter, req *http.Request) {
38 t_str := req.FormValue("target")
39 if t_str == "" {
40 http.Error(resp, "no target specified", http.StatusBadRequest)
58 s.DataLock.Lock()
59 defer s.DataLock.Unlock()
60
61 t, err := floatData(resp, req, "target")
62 if err != nil {
4163 return
4264 }
43 target, err := strconv.ParseFloat(t_str, 64)
65 s.Target = Celsius(t)
66 s.checkpoint()
67 http.Redirect(resp, req, "/", http.StatusSeeOther)
68 })
69
70 http.HandleFunc("/pid", func(resp http.ResponseWriter, req *http.Request) {
71 log.Printf("acquire lock")
72 s.DataLock.Lock()
73 defer s.DataLock.Unlock()
74 log.Printf("acquired lock")
75
76 p, err := floatData(resp, req, "p")
4477 if err != nil {
45 http.Error(
46 resp, fmt.Sprintf("could not parse target temp: %v", err),
47 http.StatusBadRequest)
4878 return
4979 }
50 s.Target = Celsius(target)
80 i, err := floatData(resp, req, "i")
81 if err != nil {
82 return
83 }
84 d, err := floatData(resp, req, "d")
85 if err != nil {
86 return
87 }
88 s.Pid.P = p
89 s.Pid.I = i
90 s.Pid.D = d
5191 s.checkpoint()
5292 http.Redirect(resp, req, "/", http.StatusSeeOther)
5393 })
1414 )
1515
1616 type SousVide struct {
17 Temp Celsius
18 Target Celsius
19 History TempHistory
20 HistoryLock sync.Mutex
17 Temp Celsius
18 Target Celsius
19 History TempHistory
20 Pid PidParams
21 DataLock sync.Mutex
2122 }
2223
23 // A ring buffer to store historical data for plotting
2424 type TempHistory struct {
2525 Times [HistoryLength]time.Time
2626 Temps [HistoryLength]float64
2929 End int
3030 }
3131
32 type PidParams struct {
33 P float64
34 I float64
35 D float64
36 }
37
3238 type Celsius float64
3339
3440 func (s *SousVide) checkpoint() {
35 s.HistoryLock.Lock()
36 defer s.HistoryLock.Unlock()
37
3841 // this would be better implemented by a ring buffer, but it doesn't
3942 // actually buy me anything because on every change I have to write it to a
4043 // flat array to plot it anyway.
6063 func (s *SousVide) StartControlLoop() {
6164 tick := time.Tick(InterruptDelay)
6265 for _ = range tick {
66 s.DataLock.Lock()
6367 s.Temp -= 0.1*s.Error() + Celsius(rand.Float64()-0.5)
6468 log.Printf("read temperature %f deg C", s.Temp)
6569 s.checkpoint()
70 s.DataLock.Unlock()
6671 }
6772 }
6873
6974 func (s *SousVide) SetTarget(target Celsius) {
75 s.DataLock.Lock()
76 defer s.DataLock.Unlock()
77
7078 s.Target = target
7179 s.checkpoint()
7280 }
7886 func main() {
7987 s := new(SousVide)
8088 s.Target = 200
89 s.Pid.P = 10
90 s.Pid.I = 0.1
91 s.Pid.D = 10
92
8193 go s.StartControlLoop()
8294 s.StartServer()
8395 }
44 <style>
55 body {
66 font-family: 'Source Sans Pro', sans-serif;
7 margin: 100px 50px;
7 margin: 50px 50px;
88 max-width: 600px;
99 font-size: 14pt;
10 }
11
12 section {
13 border-left: 7pt solid #EEE;
14 padding-left: 10pt;
1015 }
1116
1217 .val {
3742 display: none;
3843 }
3944
40 #target_input {
45 #target_input, .pid_param {
4146 font-size: 18pt;
4247 font-family: 'Source Code Pro', monospace;
4348 width: 100pt;
44 display: inline;
49 text-align: right;
4550 }
4651 </style>
4752 <link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700|Source+Code+Pro' rel='stylesheet' type='text/css'>
5156 </head>
5257 <body>
5358 <h1>sousvide control</h1>
54 <table>
55 <tr>
56 <td class="label temp_label">Current temperature:</td>
57 <td class="val"><span id="temp"></span>&deg; C</td>
58 </tr>
59 <tr>
60 <td class="label target_label">Target temperature:</td>
61 <td class="val">
62 <span id="target_display">
63 <span id="target"></span>&deg; C
64 </span>
65 <span id="target_change">
66 <form method="POST" action="/target">
67 <input type="text" id="target_input" name="target">
68 <input type="submit" style="display:none;">
69 </form>
70 </span>
71 </td>
72 </tr>
73 <tr>
74 <td class="label error_label">Error:</td>
75 <td class="val" id="err_td"><span id="abs_err"></span>&deg; C</td>
76 </tr>
77 </table>
78 <img src="/plot" id="plot">
59 <section>
60 <h2>status</h2>
61 <table>
62 <tr>
63 <td class="label temp_label">Current temperature:</td>
64 <td class="val"><span id="temp"></span>&deg; C</td>
65 </tr>
66 <tr>
67 <td class="label target_label">Target temperature:</td>
68 <td class="val">
69 <span id="target_display">
70 <span id="target"></span>&deg; C
71 </span>
72 <span id="target_change">
73 <form method="POST" action="/target">
74 <input type="text" id="target_input" name="target">
75 <input type="submit" style="display:none;">
76 </form>
77 </span>
78 </td>
79 </tr>
80 <tr>
81 <td class="label error_label">Error:</td>
82 <td class="val" id="err_td"><span id="abs_err"></span>&deg; C</td>
83 </tr>
84 </table>
85 <img src="/plot" id="plot">
86 </section>
87 <section>
88 <h2>controller</h2>
89 <form method="POST" action="/pid">
90 <table>
91 <tr>
92 <td>Proportional</td>
93 <td class="val">
94 <input type="text" class="pid_param" name="p" id="pid_p">
95 </td>
96 </tr>
97 <tr>
98 <td>Integral</td>
99 <td class="val">
100 <input type="text" class="pid_param" name="i" id="pid_i">
101 </td>
102 </tr>
103 <tr>
104 <td>Derivative</td>
105 <td class="val">
106 <input type="text" class="pid_param" name="d" id="pid_d">
107 </td>
108 </tr>
109 <tr>
110 <td></td>
111 <td class="val">
112 <input value="update" type="submit">
113 </td>
114 </tr>
115 </table>
116 </form>
117 </section>
79118 </body>
80119 </html>
00 var tempElem, absErrElem, targetElem, absErrTdElem, plotElem
11 var targetDisplayElem, targetChangeElem, targetInputElem
2 var pInputElem, iInputElem, dInputElem
23
34 function getApiData() {
45 $.ajax({
1314 }
1415
1516 function displayData(data) {
17 console.log(data)
18
1619 var i = data.Temps.length - 1
1720 var temp = data.Temps[i],
1821 target = data.Targets[i],
2124 $(tempElem).text(temp.toFixed(2));
2225 $(targetElem).text(target.toFixed(2));
2326 $(absErrElem).text((err >= 0 ? '+' : '') + err.toFixed(2));
27
28 pInputElem.setAttribute('value', data.PidParams.P)
29 iInputElem.setAttribute('value', data.PidParams.I)
30 dInputElem.setAttribute('value', data.PidParams.D)
2431
2532 if (err > 0) {
2633 $(absErrTdElem).removeClass('cold')
4047 targetElem = document.getElementById('target')
4148 absErrTdElem = document.getElementById('err_td')
4249 plotElem = document.getElementById('plot')
50
4351 targetChangeElem = document.getElementById('target_change')
4452 targetDisplayElem = document.getElementById('target_display')
4553 targetInputElem = document.getElementById('target_input')
54
55 pInputElem = document.getElementById('pid_p')
56 iInputElem = document.getElementById('pid_i')
57 dInputElem = document.getElementById('pid_d')
4658
4759 targetElem.onclick = function() {
4860 $(targetDisplayElem).css('display', 'none')