Add ability to tune PID params from web interface
Will Haldean Brown
9 years ago
11 | 11 | const ( |
12 | 12 | imgWidth = 600 |
13 | 13 | imgHeight = 400 |
14 | useSvg = true | |
14 | useSvg = true | |
15 | 15 | ) |
16 | 16 | |
17 | 17 | func (s *SousVide) GenerateChart(w http.ResponseWriter, req *http.Request) { |
28 | 28 | c.YRange.TicSetting.Grid = 1 |
29 | 29 | c.YRange.TicSetting.HideLabels = true |
30 | 30 | |
31 | s.HistoryLock.Lock() | |
31 | s.DataLock.Lock() | |
32 | 32 | h := &s.History |
33 | 33 | |
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)) | |
35 | 35 | |
36 | 36 | temps := make([]chart.EPoint, 0, h.End) |
37 | 37 | targets := make([]chart.EPoint, 0, h.End) |
62 | 62 | } |
63 | 63 | errs = append(errs, ep) |
64 | 64 | } |
65 | s.HistoryLock.Unlock() | |
65 | s.DataLock.Unlock() | |
66 | 66 | |
67 | 67 | c.AddData("Temperature", temps, chart.PlotStyleLines, chart.Style{ |
68 | 68 | LineColor: color.NRGBA{0xFF, 0x00, 0x00, 0xFF}, LineWidth: 2, |
1 | 1 | |
2 | 2 | import ( |
3 | 3 | "encoding/json" |
4 | "errors" | |
4 | 5 | "fmt" |
5 | 6 | "log" |
6 | 7 | "net/http" |
11 | 12 | Temps []float64 |
12 | 13 | Targets []float64 |
13 | 14 | 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 | |
14 | 33 | } |
15 | 34 | |
16 | 35 | func (s *SousVide) StartServer() { |
17 | 36 | http.HandleFunc("/api_data", func(resp http.ResponseWriter, req *http.Request) { |
18 | 37 | resp.Header().Set("Content-type", "application/json") |
19 | 38 | |
20 | s.HistoryLock.Lock() | |
21 | defer s.HistoryLock.Unlock() | |
39 | s.DataLock.Lock() | |
40 | defer s.DataLock.Unlock() | |
22 | 41 | |
23 | 42 | h := &s.History |
24 | 43 | a := apiData{ |
25 | 44 | h.Temps[:h.End], |
26 | 45 | h.Targets[:h.End], |
27 | 46 | h.LogErrors[:h.End], |
47 | s.Pid, | |
28 | 48 | } |
29 | 49 | |
30 | 50 | b, err := json.Marshal(a) |
35 | 55 | }) |
36 | 56 | |
37 | 57 | 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 { | |
41 | 63 | return |
42 | 64 | } |
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") | |
44 | 77 | if err != nil { |
45 | http.Error( | |
46 | resp, fmt.Sprintf("could not parse target temp: %v", err), | |
47 | http.StatusBadRequest) | |
48 | 78 | return |
49 | 79 | } |
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 | |
51 | 91 | s.checkpoint() |
52 | 92 | http.Redirect(resp, req, "/", http.StatusSeeOther) |
53 | 93 | }) |
14 | 14 | ) |
15 | 15 | |
16 | 16 | 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 | |
21 | 22 | } |
22 | 23 | |
23 | // A ring buffer to store historical data for plotting | |
24 | 24 | type TempHistory struct { |
25 | 25 | Times [HistoryLength]time.Time |
26 | 26 | Temps [HistoryLength]float64 |
29 | 29 | End int |
30 | 30 | } |
31 | 31 | |
32 | type PidParams struct { | |
33 | P float64 | |
34 | I float64 | |
35 | D float64 | |
36 | } | |
37 | ||
32 | 38 | type Celsius float64 |
33 | 39 | |
34 | 40 | func (s *SousVide) checkpoint() { |
35 | s.HistoryLock.Lock() | |
36 | defer s.HistoryLock.Unlock() | |
37 | ||
38 | 41 | // this would be better implemented by a ring buffer, but it doesn't |
39 | 42 | // actually buy me anything because on every change I have to write it to a |
40 | 43 | // flat array to plot it anyway. |
60 | 63 | func (s *SousVide) StartControlLoop() { |
61 | 64 | tick := time.Tick(InterruptDelay) |
62 | 65 | for _ = range tick { |
66 | s.DataLock.Lock() | |
63 | 67 | s.Temp -= 0.1*s.Error() + Celsius(rand.Float64()-0.5) |
64 | 68 | log.Printf("read temperature %f deg C", s.Temp) |
65 | 69 | s.checkpoint() |
70 | s.DataLock.Unlock() | |
66 | 71 | } |
67 | 72 | } |
68 | 73 | |
69 | 74 | func (s *SousVide) SetTarget(target Celsius) { |
75 | s.DataLock.Lock() | |
76 | defer s.DataLock.Unlock() | |
77 | ||
70 | 78 | s.Target = target |
71 | 79 | s.checkpoint() |
72 | 80 | } |
78 | 86 | func main() { |
79 | 87 | s := new(SousVide) |
80 | 88 | s.Target = 200 |
89 | s.Pid.P = 10 | |
90 | s.Pid.I = 0.1 | |
91 | s.Pid.D = 10 | |
92 | ||
81 | 93 | go s.StartControlLoop() |
82 | 94 | s.StartServer() |
83 | 95 | } |
4 | 4 | <style> |
5 | 5 | body { |
6 | 6 | font-family: 'Source Sans Pro', sans-serif; |
7 | margin: 100px 50px; | |
7 | margin: 50px 50px; | |
8 | 8 | max-width: 600px; |
9 | 9 | font-size: 14pt; |
10 | } | |
11 | ||
12 | section { | |
13 | border-left: 7pt solid #EEE; | |
14 | padding-left: 10pt; | |
10 | 15 | } |
11 | 16 | |
12 | 17 | .val { |
37 | 42 | display: none; |
38 | 43 | } |
39 | 44 | |
40 | #target_input { | |
45 | #target_input, .pid_param { | |
41 | 46 | font-size: 18pt; |
42 | 47 | font-family: 'Source Code Pro', monospace; |
43 | 48 | width: 100pt; |
44 | display: inline; | |
49 | text-align: right; | |
45 | 50 | } |
46 | 51 | </style> |
47 | 52 | <link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700|Source+Code+Pro' rel='stylesheet' type='text/css'> |
51 | 56 | </head> |
52 | 57 | <body> |
53 | 58 | <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>° 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>° 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>° 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>° 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>° 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>° 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> | |
79 | 118 | </body> |
80 | 119 | </html> |
0 | 0 | var tempElem, absErrElem, targetElem, absErrTdElem, plotElem |
1 | 1 | var targetDisplayElem, targetChangeElem, targetInputElem |
2 | var pInputElem, iInputElem, dInputElem | |
2 | 3 | |
3 | 4 | function getApiData() { |
4 | 5 | $.ajax({ |
13 | 14 | } |
14 | 15 | |
15 | 16 | function displayData(data) { |
17 | console.log(data) | |
18 | ||
16 | 19 | var i = data.Temps.length - 1 |
17 | 20 | var temp = data.Temps[i], |
18 | 21 | target = data.Targets[i], |
21 | 24 | $(tempElem).text(temp.toFixed(2)); |
22 | 25 | $(targetElem).text(target.toFixed(2)); |
23 | 26 | $(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) | |
24 | 31 | |
25 | 32 | if (err > 0) { |
26 | 33 | $(absErrTdElem).removeClass('cold') |
40 | 47 | targetElem = document.getElementById('target') |
41 | 48 | absErrTdElem = document.getElementById('err_td') |
42 | 49 | plotElem = document.getElementById('plot') |
50 | ||
43 | 51 | targetChangeElem = document.getElementById('target_change') |
44 | 52 | targetDisplayElem = document.getElementById('target_display') |
45 | 53 | targetInputElem = document.getElementById('target_input') |
54 | ||
55 | pInputElem = document.getElementById('pid_p') | |
56 | iInputElem = document.getElementById('pid_i') | |
57 | dInputElem = document.getElementById('pid_d') | |
46 | 58 | |
47 | 59 | targetElem.onclick = function() { |
48 | 60 | $(targetDisplayElem).css('display', 'none') |