git.haldean.org sousvide / 92be2b3
Big refactor to make history easier to work with. Will Haldean Brown 9 years ago
6 changed file(s) with 141 addition(s) and 72 deletion(s). Raw diff Collapse all Expand all
1515 )
1616
1717 func (s *SousVide) GenerateChart(w http.ResponseWriter, req *http.Request) {
18 if s.History.End == 0 {
18 if len(s.History) == 0 {
1919 w.WriteHeader(http.StatusNoContent)
2020 return
2121 }
2929 c.YRange.TicSetting.HideLabels = true
3030
3131 s.DataLock.Lock()
32 h := &s.History
3332
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))
3534
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))
3938 var ep chart.EPoint
40 for i, _ := range h.Times[:h.End] {
39 for i, h := range s.History {
4140 ep = chart.EPoint{
4241 X: float64(i),
43 Y: h.Temps[i],
42 Y: h.Temp,
4443 DeltaX: math.NaN(),
4544 DeltaY: math.NaN(),
4645 }
4847
4948 ep = chart.EPoint{
5049 X: float64(i),
51 Y: h.Targets[i],
50 Y: h.Target,
5251 DeltaX: math.NaN(),
5352 DeltaY: math.NaN(),
5453 }
5655
5756 ep = chart.EPoint{
5857 X: float64(i),
59 Y: math.Abs(h.Temps[i] - h.Targets[i]),
58 Y: math.Abs(h.Temp - h.Target),
6059 DeltaX: math.NaN(),
6160 DeltaY: math.NaN(),
6261 }
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 }
77 "net/http"
88 "strconv"
99 )
10
11 type apiData struct {
12 Temps []float64
13 Targets []float64
14 LogErrors []float64
15 PidParams PidParams
16 }
1710
1811 func floatData(w http.ResponseWriter, req *http.Request, arg string) (float64, error) {
1912 valStr := req.FormValue(arg)
3932 s.DataLock.Lock()
4033 defer s.DataLock.Unlock()
4134
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
4838 }
4939
50 b, err := json.Marshal(a)
40 b, err := json.Marshal(s.History[len(s.History)-1])
5141 if err != nil {
5242 log.Panicf("could not marshal temp data to json: %v", err)
5343 }
9282 http.Redirect(resp, req, "/", http.StatusSeeOther)
9383 })
9484
85 http.HandleFunc("/csv", s.DumpCsv)
86
9587 http.HandleFunc("/plot", s.GenerateChart)
9688
9789 http.Handle("/", http.FileServer(http.Dir("static/")))
11
22 import (
33 "log"
4 "math"
54 "math/rand"
65 "sync"
76 "time"
1110 InterruptDelay = 1 * time.Second
1211 LogFile = "runlog.txt"
1312 HistoryLength = 2048
13 LowpassSamples = 3
1414 )
1515
1616 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
2227 }
2328
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
3040 }
3141
3242 type PidParams struct {
3747
3848 type Celsius float64
3949
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
4071 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 {
4773 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]
5275 }
53 h.End -= 1
76 s.History[len(s.History)-1] = s.Snapshot()
77 } else {
78 s.History = append(s.History, s.Snapshot())
5479 }
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
6180 }
6281
6382 func (s *SousVide) StartControlLoop() {
6483 tick := time.Tick(InterruptDelay)
6584 for _ = range tick {
6685 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 }
6891 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
6997 s.checkpoint()
7098 s.DataLock.Unlock()
7199 }
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)
72125 }
73126
74127 func (s *SousVide) SetTarget(target Celsius) {
80133 }
81134
82135 func (s *SousVide) Error() Celsius {
83 return s.Temp - s.Target
136 return s.Target - s.Temp
84137 }
85138
86139 func main() {
87 s := new(SousVide)
140 s := New()
88141 s.Target = 200
89142 s.Pid.P = 10
90143 s.Pid.I = 0.1
3131 }
3232
3333 .label {
34 border-left: 14pt solid #000;
34 border-left: 14pt solid #FFF;
3535 padding-left: 14pt;
3636 }
3737 .temp_label { border-color: #F00; }
3838 .target_label { border-color: #00F; }
3939 .error_label { border-color: #666; }
40 .hot { color: #F00; }
4041
4142 #target_change {
4243 display: none;
5960 <section>
6061 <h2>status</h2>
6162 <table>
63 <tr>
64 <td class="label heater_label">Heater is:</td>
65 <td class="val"><span id="heating"></span></td>
66 </tr>
6267 <tr>
6368 <td class="label temp_label">Current temperature:</td>
6469 <td class="val"><span id="temp"></span>&deg; C</td>
0 var tempElem, absErrElem, targetElem, absErrTdElem, plotElem
0 var tempElem, absErrElem, targetElem, absErrTdElem, heatingElem, plotElem
11 var targetDisplayElem, targetChangeElem, targetInputElem
22 var pInputElem, iInputElem, dInputElem
33
1616 function displayData(data) {
1717 console.log(data)
1818
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,
2221 err = temp - target;
2322
2423 $(tempElem).text(temp.toFixed(2));
2524 $(targetElem).text(target.toFixed(2));
2625 $(absErrElem).text((err >= 0 ? '+' : '') + err.toFixed(2));
2726
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)
3130
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')
3835 } else {
39 $(absErrTdElem).removeClass('hot')
40 $(absErrTdElem).removeClass('cold')
36 $(heatingElem).addClass('cold')
37 $(heatingElem).removeClass('hot')
38 $(heatingElem).text('OFF')
4139 }
4240 }
4341
4644 absErrElem = document.getElementById('abs_err')
4745 targetElem = document.getElementById('target')
4846 absErrTdElem = document.getElementById('err_td')
47 heatingElem = document.getElementById('heating')
4948 plotElem = document.getElementById('plot')
5049
5150 targetChangeElem = document.getElementById('target_change')