git.haldean.org sousvide / 6fe908d
Add timers. Note that in-browser sounds don't work yet, the only indication that a timer is done is that it expires and the font turns red. Will Haldean Brown 8 years ago
9 changed file(s) with 257 addition(s) and 12 deletion(s). Raw diff Collapse all Expand all
2727 maxVal = h.Target
2828 }
2929 }
30 maxY := 10 * math.Ceil(maxVal / 10)
30 maxY := 10 * math.Ceil(maxVal/10)
3131 pxPerUnitY := float64(ImgHeight) / maxY
3232
3333 maxX := float64(N - 1)
4444 if h.Heating {
4545 x0 := int(float64(i) * pxPerUnitX)
4646 svgs.Rect(x0, 0, int(math.Ceil(pxPerUnitX)), ImgHeight,
47 "fill:#F7F7F7")
47 "fill:#F7F7F7")
4848 }
4949 }
5050 }
7171 targets := make([]int, N)
7272 for i, h := range s.History {
7373 xs[i] = int(float64(i) * pxPerUnitX)
74 temps[i] = ImgHeight - int(h.Temp * pxPerUnitY)
75 targets[i] = ImgHeight - int(h.Target * pxPerUnitY)
74 temps[i] = ImgHeight - int(h.Temp*pxPerUnitY)
75 targets[i] = ImgHeight - int(h.Target*pxPerUnitY)
7676 }
7777 svgs.Polyline(xs, temps, "stroke:#FF0000; stroke-width:1; fill:none")
7878 svgs.Polyline(xs, targets, "stroke:#0000FF; stroke-width:1; fill:none")
4444 }
4545
4646 func (s *SousVide) InitGpio() error {
47 var err error
4748 if s.Gpio.Stub {
48 s.Gpio.HeaterFd = os.Stdout
49 s.Gpio.HeaterFd, err = os.Open("/dev/null")
50 if err != nil {
51 log.Fatalf("could not open /dev/null: %v", err)
52 }
4953 return nil
5054 }
5155
52 err := checkHeaterExported()
56 err = checkHeaterExported()
5357 if err != nil {
5458 return err
5559 }
1010 )
1111
1212 var port = flag.Int("port", 80, "port for web interface")
13
14 func intData(w http.ResponseWriter, req *http.Request, arg string, def int64) (int64, error) {
15 valStr := req.FormValue(arg)
16 if valStr == "" {
17 return def, nil
18 }
19 val, err := strconv.ParseInt(valStr, 10, 64)
20 if err != nil {
21 http.Error(
22 w, fmt.Sprintf("could not parse %s: %v", arg, err),
23 http.StatusBadRequest)
24 return 0, err
25 }
26 return val, nil
27 }
1328
1429 func floatData(w http.ResponseWriter, req *http.Request, arg string) (float64, error) {
1530 valStr := req.FormValue(arg)
89104
90105 http.HandleFunc("/csv", s.DumpCsv)
91106 http.HandleFunc("/plot", s.GenerateChart2)
107 http.HandleFunc("/timer", AddTimerHandler)
108 http.HandleFunc("/timers", GetTimersHandler)
109 http.HandleFunc("/delete_timer", DeleteTimerHandler)
92110 http.Handle("/", http.FileServer(http.Dir("static/")))
93111 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
94112 }
2727
2828 type SousVide struct {
2929 Heating bool
30 Enabled bool
30 Enabled bool
3131 Temp Celsius
3232 Target Celsius
3333 History []HistorySample
3535 Gpio GpioParams
3636 DataLock sync.Mutex
3737 AccError float64
38 MaxError float64
38 MaxError float64
3939 lastPOutput float64
4040 lastIOutput float64
4141 lastDOutput float64
192192 return
193193 }
194194
195 go StartTimerUpdateLoop()
195196 go s.StartControlLoop()
196197 s.StartServer()
197198 }
Binary diff not shown
6161 color: #666;
6262 text-decoration: none;
6363 }
64
65 .time {
66 width: 24pt;
67 text-align: center;
68 }
69
70 .expired {
71 color: #F00;
72 }
73
74 #timers form {
75 margin-left: 10pt;
76 display: inline;
77 }
78
79 input[type=submit] {
80 border: 1px solid #DDD;
81 background: #FFF;
82 position: relative;
83 top: -1px;
84 }
6485 </style>
6586 <link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700|Source+Code+Pro' rel='stylesheet' type='text/css'>
6687
6990 </head>
7091 <body>
7192 <h1>sousvide control</h1>
93 <section>
94 <h2>timers</h2>
95 <table id="timers">
96 </table>
97 <form action="/timer" method="POST">
98 <input type="text" name="name" placeholder="New timer">
99 <input type="text" name="h" placeholder="HH" class="time">
100 <input type="text" name="m" placeholder="MM" class="time">
101 <input type="text" name="s" placeholder="SS" class="time">
102 <input type="submit" value="save">
103 </form>
104 <audio id="timernoise" src="/finished.wav" preload="auto" volume="1"></audio>
105 </section>
72106 <section>
73107 <h2>status</h2>
74108 <table>
11 var targetDisplayElem, targetChangeElem, targetInputElem
22 var pInputElem, iInputElem, dInputElem
33 var enabledElem, maxErrElem
4 var timerElem, timerAudio
45
56 function getApiData() {
67 $.ajax({
1516 }
1617
1718 function displayData(data) {
18 console.log(data)
19
2019 var temp = data.Temp,
2120 target = data.Target,
2221 err = temp - target;
4039 $(heatingElem).addClass('cold')
4140 $(heatingElem).removeClass('hot')
4241 $(heatingElem).text('OFF')
42 }
43 }
44
45 function getTimerData() {
46 $.ajax({
47 url: '/timers',
48 type: 'json',
49 success: function(resp) {
50 displayTimers(resp)
51 }
52 })
53 setTimeout(getTimerData, 1000)
54 }
55
56 function durationFormat(nano) {
57 var neg = nano < 0
58 if (neg) nano *= -1
59 var sec = Math.floor(nano / 1e9)
60 var min = Math.floor(sec / 60)
61 sec -= min * 60
62 var hr = Math.floor(min / 60)
63 min -= hr * 60
64 if (min < 10) min = '0' + min
65 if (sec < 10) sec = '0' + sec
66 return (neg ? '-' : '') + hr + 'h' + min + 'm' + sec + 's'
67 }
68
69 function makeTimer(timer) {
70 tr = document.createElement('tr')
71
72 td = document.createElement('td')
73 td.innerHTML = timer.Name + ' (' + durationFormat(timer.SetTime) + ')'
74
75 form = document.createElement('form')
76 form.setAttribute('method', 'POST')
77 form.setAttribute('action', 'delete_timer')
78 del = document.createElement('input')
79 del.setAttribute('type', 'submit')
80 del.setAttribute('value', 'dismiss')
81 form.appendChild(del)
82 hid = document.createElement('input')
83 hid.setAttribute('type', 'hidden')
84 hid.setAttribute('name', 'id')
85 hid.setAttribute('value', timer.Id)
86 form.appendChild(hid)
87 td.appendChild(form)
88
89 tr.appendChild(td)
90
91 td = document.createElement('td')
92 td.innerHTML = durationFormat(timer.TimeRemaining)
93 $(td).addClass('val')
94 if (timer.Expired) {
95 $(td).addClass('expired')
96 }
97 tr.appendChild(td)
98
99 return tr
100 }
101
102 function displayTimers(data) {
103 timerElem.innerHTML = ''
104 for (var i = 0; i < data.length; i++) {
105 timer = data[i]
106 timerElem.appendChild(makeTimer(timer));
43107 }
44108 }
45109
68132 }
69133
70134 getApiData()
135
136 timerElem = document.getElementById('timers')
137 timerAudio = document.getElementById('timernoise')
138 timerAudio.play()
139 getTimerData()
71140 })
3737
3838 if s.Gpio.Stub {
3939 s.Gpio.ThermFd, err = os.OpenFile(
40 "test_temp.txt", os.O_RDONLY | os.O_SYNC, 0666)
40 "test_temp.txt", os.O_RDONLY|os.O_SYNC, 0666)
4141 if err != nil {
4242 return err
4343 }
4949
5050 s.Gpio.ThermFd, err = os.OpenFile(
5151 fmt.Sprintf("/sys/bus/w1/devices/%s/w1_slave", serial),
52 os.O_RDONLY | os.O_SYNC, 0666)
52 os.O_RDONLY|os.O_SYNC, 0666)
5353 if err != nil {
5454 return err
5555 }
0 package main
1
2 import (
3 "encoding/json"
4 "fmt"
5 "log"
6 "net/http"
7 "sort"
8 "time"
9 )
10
11 type Timer struct {
12 Id int64
13 Name string
14 SetTime time.Duration
15 ExpiresAt time.Time
16 TimeRemaining time.Duration
17 Expired bool
18 Notified bool
19 }
20
21 type Timers []*Timer
22
23 var timers = make(Timers, 0)
24 var nextId = int64(0)
25
26 func StartTimerUpdateLoop() {
27 for _ = range time.Tick(time.Second) {
28 for _, t := range timers {
29 t.TimeRemaining = time.Now().Sub(t.ExpiresAt)
30 t.Expired = t.TimeRemaining < 0
31 }
32 }
33 }
34
35 func (t Timers) Len() int { return len(t) }
36 func (t Timers) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
37
38 func (t Timers) Less(i, j int) bool {
39 return t[i].TimeRemaining < t[j].TimeRemaining
40 }
41
42 func AddTimerHandler(w http.ResponseWriter, r *http.Request) {
43 name := r.FormValue("name")
44 if name == "" {
45 http.Error(w, "missing argument name", http.StatusBadRequest)
46 return
47 }
48 h, err := intData(w, r, "h", 0)
49 if err != nil {
50 return
51 }
52 m, err := intData(w, r, "m", 0)
53 if err != nil {
54 return
55 }
56 s, err := intData(w, r, "s", 0)
57 if err != nil {
58 return
59 }
60 if (h == 0 && m == 0 && s == 0) || (h < 0 || m < 0 || s < 0) {
61 http.Error(w, "must set timer for time in the future", http.StatusBadRequest)
62 return
63 }
64
65 t := &Timer{
66 Id: nextId,
67 Name: name,
68 SetTime: time.Duration(h)*time.Hour +
69 time.Duration(m)*time.Minute +
70 time.Duration(s)*time.Second,
71 }
72 t.ExpiresAt = time.Now().Add(t.SetTime)
73
74 nextId++
75 log.Printf("set timer %v\n", t)
76 timers = append(timers, t)
77 sort.Sort(timers)
78
79 http.Redirect(w, r, "/", http.StatusSeeOther)
80 }
81
82 func GetTimersHandler(w http.ResponseWriter, r *http.Request) {
83 w.Header().Set("Content-type", "application/json")
84 b, err := json.Marshal(timers)
85 if err != nil {
86 log.Panicf("could not marshal timer data to json: %v", err)
87 }
88 w.Write(b)
89 for _, t := range timers {
90 t.Notified = t.Expired
91 }
92 }
93
94 func DeleteTimerHandler(w http.ResponseWriter, r *http.Request) {
95 id, err := intData(w, r, "id", -1)
96 if err != nil {
97 return
98 } else if id == -1 {
99 http.Error(w, "must specify ID to delete", http.StatusBadRequest)
100 return
101 }
102 idx := -1
103 for i, t := range timers {
104 if t.Id == id {
105 idx = i
106 break
107 }
108 }
109 if idx == -1 {
110 http.Error(
111 w, fmt.Sprintf("could not find ID %d", id), http.StatusBadRequest)
112 return
113 }
114 timers[idx] = timers[len(timers)-1]
115 timers = timers[:len(timers)-1]
116 sort.Sort(timers)
117 http.Redirect(w, r, "/", http.StatusSeeOther)
118 }