Big refactor to make history easier to work with.
Will Haldean Brown
9 years ago
15 | 15 | ) |
16 | 16 | |
17 | 17 | func (s *SousVide) GenerateChart(w http.ResponseWriter, req *http.Request) { |
18 | if s.History.End == 0 { | |
18 | if len(s.History) == 0 { | |
19 | 19 | w.WriteHeader(http.StatusNoContent) |
20 | 20 | return |
21 | 21 | } |
29 | 29 | c.YRange.TicSetting.HideLabels = true |
30 | 30 | |
31 | 31 | s.DataLock.Lock() |
32 | h := &s.History | |
33 | 32 | |
34 | c.XRange.Fixed(0, float64(h.End)+1, float64(h.End/10)) | |
33 | c.XRange.Fixed(0, float64(len(s.History))+1, float64(len(s.History)/10)) | |
35 | 34 | |
36 | temps := make([]chart.EPoint, 0, h.End) | |
37 | targets := make([]chart.EPoint, 0, h.End) | |
38 | errs := make([]chart.EPoint, 0, h.End) | |
35 | temps := make([]chart.EPoint, 0, len(s.History)) | |
36 | targets := make([]chart.EPoint, 0, len(s.History)) | |
37 | errs := make([]chart.EPoint, 0, len(s.History)) | |
39 | 38 | var ep chart.EPoint |
40 | for i, _ := range h.Times[:h.End] { | |
39 | for i, h := range s.History { | |
41 | 40 | ep = chart.EPoint{ |
42 | 41 | X: float64(i), |
43 | Y: h.Temps[i], | |
42 | Y: h.Temp, | |
44 | 43 | DeltaX: math.NaN(), |
45 | 44 | DeltaY: math.NaN(), |
46 | 45 | } |
48 | 47 | |
49 | 48 | ep = chart.EPoint{ |
50 | 49 | X: float64(i), |
51 | Y: h.Targets[i], | |
50 | Y: h.Target, | |
52 | 51 | DeltaX: math.NaN(), |
53 | 52 | DeltaY: math.NaN(), |
54 | 53 | } |
56 | 55 | |
57 | 56 | ep = chart.EPoint{ |
58 | 57 | X: float64(i), |
59 | Y: math.Abs(h.Temps[i] - h.Targets[i]), | |
58 | Y: math.Abs(h.Temp - h.Target), | |
60 | 59 | DeltaX: math.NaN(), |
61 | 60 | DeltaY: math.NaN(), |
62 | 61 | } |
0 | package main | |
1 | ||
2 | import ( | |
3 | "fmt" | |
4 | "net/http" | |
5 | ) | |
6 | ||
7 | func (h HistorySample) ToCsv() string { | |
8 | return fmt.Sprintf("%d,%v,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f\n", | |
9 | h.Time.Unix(), h.Heating, h.Temp, h.Target, h.AbsError, h.Pid.P, | |
10 | h.Pid.I, h.Pid.D, h.POutput, h.IOutput, h.DOutput, h.COutput) | |
11 | ||
12 | } | |
13 | ||
14 | func (s *SousVide) DumpCsv(w http.ResponseWriter, _ *http.Request) { | |
15 | w.Write([]byte("Time,Heating,Temperature,Target,Error,\"P Coeff\",\"I " + | |
16 | "Coeff\",D Coeff\",\"P Term\",\"I Term\",\"D Term\",Controller\n")) | |
17 | for _, h := range s.History { | |
18 | w.Write([]byte(h.ToCsv())) | |
19 | } | |
20 | } |
7 | 7 | "net/http" |
8 | 8 | "strconv" |
9 | 9 | ) |
10 | ||
11 | type apiData struct { | |
12 | Temps []float64 | |
13 | Targets []float64 | |
14 | LogErrors []float64 | |
15 | PidParams PidParams | |
16 | } | |
17 | 10 | |
18 | 11 | func floatData(w http.ResponseWriter, req *http.Request, arg string) (float64, error) { |
19 | 12 | valStr := req.FormValue(arg) |
39 | 32 | s.DataLock.Lock() |
40 | 33 | defer s.DataLock.Unlock() |
41 | 34 | |
42 | h := &s.History | |
43 | a := apiData{ | |
44 | h.Temps[:h.End], | |
45 | h.Targets[:h.End], | |
46 | h.LogErrors[:h.End], | |
47 | s.Pid, | |
35 | if len(s.History) == 0 { | |
36 | resp.WriteHeader(http.StatusNoContent) | |
37 | return | |
48 | 38 | } |
49 | 39 | |
50 | b, err := json.Marshal(a) | |
40 | b, err := json.Marshal(s.History[len(s.History)-1]) | |
51 | 41 | if err != nil { |
52 | 42 | log.Panicf("could not marshal temp data to json: %v", err) |
53 | 43 | } |
92 | 82 | http.Redirect(resp, req, "/", http.StatusSeeOther) |
93 | 83 | }) |
94 | 84 | |
85 | http.HandleFunc("/csv", s.DumpCsv) | |
86 | ||
95 | 87 | http.HandleFunc("/plot", s.GenerateChart) |
96 | 88 | |
97 | 89 | http.Handle("/", http.FileServer(http.Dir("static/"))) |
1 | 1 | |
2 | 2 | import ( |
3 | 3 | "log" |
4 | "math" | |
5 | 4 | "math/rand" |
6 | 5 | "sync" |
7 | 6 | "time" |
11 | 10 | InterruptDelay = 1 * time.Second |
12 | 11 | LogFile = "runlog.txt" |
13 | 12 | HistoryLength = 2048 |
13 | LowpassSamples = 3 | |
14 | 14 | ) |
15 | 15 | |
16 | 16 | type SousVide struct { |
17 | Temp Celsius | |
18 | Target Celsius | |
19 | History TempHistory | |
20 | Pid PidParams | |
21 | DataLock sync.Mutex | |
17 | Heating bool | |
18 | Temp Celsius | |
19 | Target Celsius | |
20 | History []HistorySample | |
21 | Pid PidParams | |
22 | DataLock sync.Mutex | |
23 | lastPOutput float64 | |
24 | lastIOutput float64 | |
25 | lastDOutput float64 | |
26 | lastControl float64 | |
22 | 27 | } |
23 | 28 | |
24 | type TempHistory struct { | |
25 | Times [HistoryLength]time.Time | |
26 | Temps [HistoryLength]float64 | |
27 | Targets [HistoryLength]float64 | |
28 | LogErrors [HistoryLength]float64 | |
29 | End int | |
29 | type HistorySample struct { | |
30 | Time time.Time | |
31 | Heating bool | |
32 | Temp float64 | |
33 | Target float64 | |
34 | AbsError float64 | |
35 | Pid PidParams | |
36 | POutput float64 | |
37 | IOutput float64 | |
38 | DOutput float64 | |
39 | COutput float64 | |
30 | 40 | } |
31 | 41 | |
32 | 42 | type PidParams struct { |
37 | 47 | |
38 | 48 | type Celsius float64 |
39 | 49 | |
50 | func New() *SousVide { | |
51 | s := new(SousVide) | |
52 | s.History = make([]HistorySample, 0, HistoryLength) | |
53 | return s | |
54 | } | |
55 | ||
56 | func (s *SousVide) Snapshot() HistorySample { | |
57 | return HistorySample{ | |
58 | Time: time.Now(), | |
59 | Heating: s.Heating, | |
60 | Temp: float64(s.Temp), | |
61 | Target: float64(s.Target), | |
62 | AbsError: float64(s.Error()), | |
63 | Pid: s.Pid, | |
64 | POutput: s.lastPOutput, | |
65 | IOutput: s.lastIOutput, | |
66 | DOutput: s.lastDOutput, | |
67 | COutput: s.lastControl, | |
68 | } | |
69 | } | |
70 | ||
40 | 71 | func (s *SousVide) checkpoint() { |
41 | // this would be better implemented by a ring buffer, but it doesn't | |
42 | // actually buy me anything because on every change I have to write it to a | |
43 | // flat array to plot it anyway. | |
44 | ||
45 | h := &s.History | |
46 | if h.End == HistoryLength { | |
72 | if len(s.History) == HistoryLength { | |
47 | 73 | for i := 0; i < HistoryLength-1; i++ { |
48 | h.Times[i] = h.Times[i+1] | |
49 | h.Temps[i] = h.Temps[i+1] | |
50 | h.Targets[i] = h.Targets[i+1] | |
51 | h.LogErrors[i] = h.LogErrors[i+1] | |
74 | s.History[i] = s.History[i+1] | |
52 | 75 | } |
53 | h.End -= 1 | |
76 | s.History[len(s.History)-1] = s.Snapshot() | |
77 | } else { | |
78 | s.History = append(s.History, s.Snapshot()) | |
54 | 79 | } |
55 | ||
56 | h.Times[h.End] = time.Now() | |
57 | h.Temps[h.End] = float64(s.Temp) | |
58 | h.Targets[h.End] = float64(s.Target) | |
59 | h.LogErrors[h.End] = math.Abs(math.Log10(math.Abs(float64(s.Error())))) | |
60 | h.End += 1 | |
61 | 80 | } |
62 | 81 | |
63 | 82 | func (s *SousVide) StartControlLoop() { |
64 | 83 | tick := time.Tick(InterruptDelay) |
65 | 84 | for _ = range tick { |
66 | 85 | s.DataLock.Lock() |
67 | s.Temp -= 0.1*s.Error() + Celsius(rand.Float64()-0.5) | |
86 | if s.Heating { | |
87 | s.Temp += Celsius(10 * rand.Float64()) | |
88 | } else { | |
89 | s.Temp -= Celsius(10 * rand.Float64()) | |
90 | } | |
68 | 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 | ||
69 | 97 | s.checkpoint() |
70 | 98 | s.DataLock.Unlock() |
71 | 99 | } |
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].AbsError - s.History[N-1].AbsError) / 2 | |
120 | s.lastDOutput = s.Pid.D * d | |
121 | } | |
122 | ||
123 | s.lastControl = s.lastPOutput + s.lastIOutput + s.lastDOutput | |
124 | return Celsius(s.lastControl) | |
72 | 125 | } |
73 | 126 | |
74 | 127 | func (s *SousVide) SetTarget(target Celsius) { |
80 | 133 | } |
81 | 134 | |
82 | 135 | func (s *SousVide) Error() Celsius { |
83 | return s.Temp - s.Target | |
136 | return s.Target - s.Temp | |
84 | 137 | } |
85 | 138 | |
86 | 139 | func main() { |
87 | s := new(SousVide) | |
140 | s := New() | |
88 | 141 | s.Target = 200 |
89 | 142 | s.Pid.P = 10 |
90 | 143 | s.Pid.I = 0.1 |
31 | 31 | } |
32 | 32 | |
33 | 33 | .label { |
34 | border-left: 14pt solid #000; | |
34 | border-left: 14pt solid #FFF; | |
35 | 35 | padding-left: 14pt; |
36 | 36 | } |
37 | 37 | .temp_label { border-color: #F00; } |
38 | 38 | .target_label { border-color: #00F; } |
39 | 39 | .error_label { border-color: #666; } |
40 | .hot { color: #F00; } | |
40 | 41 | |
41 | 42 | #target_change { |
42 | 43 | display: none; |
59 | 60 | <section> |
60 | 61 | <h2>status</h2> |
61 | 62 | <table> |
63 | <tr> | |
64 | <td class="label heater_label">Heater is:</td> | |
65 | <td class="val"><span id="heating"></span></td> | |
66 | </tr> | |
62 | 67 | <tr> |
63 | 68 | <td class="label temp_label">Current temperature:</td> |
64 | 69 | <td class="val"><span id="temp"></span>° C</td> |
0 | var tempElem, absErrElem, targetElem, absErrTdElem, plotElem | |
0 | var tempElem, absErrElem, targetElem, absErrTdElem, heatingElem, plotElem | |
1 | 1 | var targetDisplayElem, targetChangeElem, targetInputElem |
2 | 2 | var pInputElem, iInputElem, dInputElem |
3 | 3 | |
16 | 16 | function displayData(data) { |
17 | 17 | console.log(data) |
18 | 18 | |
19 | var i = data.Temps.length - 1 | |
20 | var temp = data.Temps[i], | |
21 | target = data.Targets[i], | |
19 | var temp = data.Temp, | |
20 | target = data.Target, | |
22 | 21 | err = temp - target; |
23 | 22 | |
24 | 23 | $(tempElem).text(temp.toFixed(2)); |
25 | 24 | $(targetElem).text(target.toFixed(2)); |
26 | 25 | $(absErrElem).text((err >= 0 ? '+' : '') + err.toFixed(2)); |
27 | 26 | |
28 | pInputElem.setAttribute('value', data.PidParams.P) | |
29 | iInputElem.setAttribute('value', data.PidParams.I) | |
30 | dInputElem.setAttribute('value', data.PidParams.D) | |
27 | pInputElem.setAttribute('value', data.Pid.P) | |
28 | iInputElem.setAttribute('value', data.Pid.I) | |
29 | dInputElem.setAttribute('value', data.Pid.D) | |
31 | 30 | |
32 | if (err > 0) { | |
33 | $(absErrTdElem).removeClass('cold') | |
34 | $(absErrTdElem).addClass('hot') | |
35 | } else if (err < 0) { | |
36 | $(absErrTdElem).removeClass('hot') | |
37 | $(absErrTdElem).addClass('cold') | |
31 | if (data.Heating) { | |
32 | $(heatingElem).addClass('hot') | |
33 | $(heatingElem).removeClass('cold') | |
34 | $(heatingElem).text('ON') | |
38 | 35 | } else { |
39 | $(absErrTdElem).removeClass('hot') | |
40 | $(absErrTdElem).removeClass('cold') | |
36 | $(heatingElem).addClass('cold') | |
37 | $(heatingElem).removeClass('hot') | |
38 | $(heatingElem).text('OFF') | |
41 | 39 | } |
42 | 40 | } |
43 | 41 | |
46 | 44 | absErrElem = document.getElementById('abs_err') |
47 | 45 | targetElem = document.getElementById('target') |
48 | 46 | absErrTdElem = document.getElementById('err_td') |
47 | heatingElem = document.getElementById('heating') | |
49 | 48 | plotElem = document.getElementById('plot') |
50 | 49 | |
51 | 50 | targetChangeElem = document.getElementById('target_change') |