succd: implement rp/dp/vent/pumpdown control
This commit is contained in:
parent
d01263fead
commit
070f45b1bc
|
@ -3,7 +3,11 @@ succd
|
||||||
|
|
||||||
A little daemon for monitoring the SEM. This is a temporary solution that runs on a BeagleBone Enhanced (`succbone.lab`) and *SHOULD* be replaced with a proper PLC/SCADA system and general process control in the future.
|
A little daemon for monitoring the SEM. This is a temporary solution that runs on a BeagleBone Enhanced (`succbone.lab`) and *SHOULD* be replaced with a proper PLC/SCADA system and general process control in the future.
|
||||||
|
|
||||||
Currently it monitors the state of the Pirani gauge via the BBE's builtin ADC.
|
Features:
|
||||||
|
|
||||||
|
1. Monitors the state of the Pirani gauge via the BBE's builtin ADC.
|
||||||
|
2. Allows enabling/disabling the diffusion/roughing pumps (builtin controller always keeps them enabled).
|
||||||
|
3. Allows for simulating vent/pumpdown button presses.
|
||||||
|
|
||||||
Accessing at the lab
|
Accessing at the lab
|
||||||
---
|
---
|
||||||
|
|
92
succbone/succd/gpio.go
Normal file
92
succbone/succd/gpio.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"k8s.io/klog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// gpio is an abstract GPIO output.
|
||||||
|
type gpio interface {
|
||||||
|
// set returns the GPIO value. The meaning of the logic level is
|
||||||
|
// implementation-dependent.
|
||||||
|
set(state bool) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// bbGPIO implements gpio using BeagleBone's built-in GPIO pins.
|
||||||
|
//
|
||||||
|
// The value of a GPIO is logically non-inverted: false is 0V, true is 3.3V.
|
||||||
|
//
|
||||||
|
// The GPIO can be repeatedly set to the same value without performance penalty
|
||||||
|
// - only level changes are actually written to hardware registers.
|
||||||
|
type bbGPIO struct {
|
||||||
|
path string
|
||||||
|
mu sync.Mutex
|
||||||
|
state bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBBGPIO returns a BeagleBone GPIO for a given GPIO number.
|
||||||
|
//
|
||||||
|
// See the following for GPIO pin numbers ('GPIO NO.' column):
|
||||||
|
//
|
||||||
|
// https://vadl.github.io/images/bbb/P8Header.png
|
||||||
|
// https://vadl.github.io/images/bbb/P9Header.png
|
||||||
|
func newBBGPIO(num int, value bool) (*bbGPIO, error) {
|
||||||
|
path := fmt.Sprintf("/sys/class/gpio/gpio%d", num)
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not access: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pathDir := path + "/direction"
|
||||||
|
if err := os.WriteFile(pathDir, []byte("out"), 0); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not set direction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pathValue := path + "/value"
|
||||||
|
res := &bbGPIO{
|
||||||
|
path: pathValue,
|
||||||
|
}
|
||||||
|
if err := res.set(value); err != nil {
|
||||||
|
return nil, fmt.Errorf("when setting initial value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bbGPIO) set(state bool) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
if state && !b.state {
|
||||||
|
if err := os.WriteFile(b.path, []byte("1"), 0); err != nil {
|
||||||
|
return fmt.Errorf("could not turn on: %w", err)
|
||||||
|
}
|
||||||
|
} else if !state && b.state {
|
||||||
|
if err := os.WriteFile(b.path, []byte("0"), 0); err != nil {
|
||||||
|
return fmt.Errorf("could not turn off: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.state = state
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fakeGPIO implements a GPIO that logs state changes.
|
||||||
|
type fakeGPIO struct {
|
||||||
|
desc string
|
||||||
|
mu sync.Mutex
|
||||||
|
state bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *fakeGPIO) set(state bool) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
if state && !b.state {
|
||||||
|
klog.Infof("%s on", b.desc)
|
||||||
|
} else if !state && b.state {
|
||||||
|
klog.Infof("%s off", b.desc)
|
||||||
|
}
|
||||||
|
b.state = state
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -51,6 +51,8 @@ func (d *daemon) httpIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
volts, mbar := d.pirani()
|
volts, mbar := d.pirani()
|
||||||
|
rp := d.rpGet()
|
||||||
|
dp := d.dpGet()
|
||||||
|
|
||||||
loadB, err := os.ReadFile("/proc/loadavg")
|
loadB, err := os.ReadFile("/proc/loadavg")
|
||||||
load := "unknown"
|
load := "unknown"
|
||||||
|
@ -67,6 +69,8 @@ func (d *daemon) httpIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
templateIndex.Execute(w, map[string]any{
|
templateIndex.Execute(w, map[string]any{
|
||||||
"volts": formatVolts(volts),
|
"volts": formatVolts(volts),
|
||||||
"mbar": formatMbar(mbar),
|
"mbar": formatMbar(mbar),
|
||||||
|
"rp": rp,
|
||||||
|
"dp": dp,
|
||||||
"hostname": hostname,
|
"hostname": hostname,
|
||||||
"load": load,
|
"load": load,
|
||||||
})
|
})
|
||||||
|
@ -93,14 +97,20 @@ func (d *daemon) httpStream(w http.ResponseWriter, r *http.Request) {
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
// TODO(q3k): don't poll, get notified when new ADC readout is available.
|
// TODO(q3k): don't poll, get notified when new ADC readout is available.
|
||||||
volts, mbar := d.pirani()
|
volts, mbar := d.pirani()
|
||||||
|
rp := d.rpGet()
|
||||||
|
dp := d.dpGet()
|
||||||
v := struct {
|
v := struct {
|
||||||
Volts string
|
Volts string
|
||||||
Mbar string
|
Mbar string
|
||||||
MbarFloat float32
|
MbarFloat float32
|
||||||
|
RPOn bool
|
||||||
|
DPOn bool
|
||||||
}{
|
}{
|
||||||
Volts: formatVolts(volts),
|
Volts: formatVolts(volts),
|
||||||
Mbar: string(formatMbar(mbar)),
|
Mbar: string(formatMbar(mbar)),
|
||||||
MbarFloat: mbar,
|
MbarFloat: mbar,
|
||||||
|
RPOn: rp,
|
||||||
|
DPOn: dp,
|
||||||
}
|
}
|
||||||
if err := wsjson.Write(ctx, c, v); err != nil {
|
if err := wsjson.Write(ctx, c, v); err != nil {
|
||||||
klog.Errorf("Websocket write failed: %v", err)
|
klog.Errorf("Websocket write failed: %v", err)
|
||||||
|
@ -119,3 +129,31 @@ func (d *daemon) httpMetrics(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintf(w, "# TYPE sem_pressure_mbar gauge\n")
|
fmt.Fprintf(w, "# TYPE sem_pressure_mbar gauge\n")
|
||||||
fmt.Fprintf(w, "sem_pressure_mbar %f\n", mbar)
|
fmt.Fprintf(w, "sem_pressure_mbar %f\n", mbar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *daemon) httpRoughingPumpEnable(w http.ResponseWriter, r *http.Request) {
|
||||||
|
d.rpSet(true)
|
||||||
|
fmt.Fprintf(w, "succ on\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *daemon) httpRoughingPumpDisable(w http.ResponseWriter, r *http.Request) {
|
||||||
|
d.rpSet(false)
|
||||||
|
fmt.Fprintf(w, "succ off\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *daemon) httpDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) {
|
||||||
|
d.dpSet(true)
|
||||||
|
fmt.Fprintf(w, "deep succ on\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *daemon) httpDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) {
|
||||||
|
d.dpSet(false)
|
||||||
|
fmt.Fprintf(w, "deep succ off\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *daemon) httpButtonPumpDown(w http.ResponseWriter, r *http.Request) {
|
||||||
|
d.pumpDownPress()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *daemon) httpButtonVent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
d.ventPress()
|
||||||
|
}
|
||||||
|
|
|
@ -29,12 +29,17 @@ h2 {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
|
button {
|
||||||
|
height: 4.5em;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
padding-right: 1.5em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<h1>succd</h1>
|
<h1>succd</h1>
|
||||||
<h2>nothing more permanent than a temporary solution</h2>
|
<h2>nothing more permanent than a temporary solution</h2>
|
||||||
|
|
||||||
<p style="margin-top: 5em;">
|
<p style="margin-top: 2em;">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Voltage</th>
|
<th>Voltage</th>
|
||||||
|
@ -44,6 +49,25 @@ h2 {
|
||||||
<th>Pressure</th>
|
<th>Pressure</th>
|
||||||
<td id="mbar">{{.mbar}}</td>
|
<td id="mbar">{{.mbar}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Roughing Pump</th>
|
||||||
|
<td id="rp">{{ if .rp }}ON{{ else }}OFF{{ end }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Diffusion Pump</th>
|
||||||
|
<td id="dp">{{ if .dp }}ON{{ else }}OFF{{ end }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Evac Control</th>
|
||||||
|
<td>
|
||||||
|
<button id="pd">Pump Down</button>
|
||||||
|
<button id="vent">Vent</button>
|
||||||
|
<button id="rpon">RP On</button>
|
||||||
|
<button id="rpoff">RP Off</button>
|
||||||
|
<button id="dpon">DP On</button>
|
||||||
|
<button id="dpoff">DP Off</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<td id="status">OK</td>
|
<td id="status">OK</td>
|
||||||
|
@ -197,6 +221,12 @@ window.addEventListener("load", (_) => {
|
||||||
let volts = document.querySelector("#volts");
|
let volts = document.querySelector("#volts");
|
||||||
let mbar = document.querySelector("#mbar");
|
let mbar = document.querySelector("#mbar");
|
||||||
let ping = document.querySelector("#ping");
|
let ping = document.querySelector("#ping");
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
let pd = document.querySelector("#pd");
|
||||||
|
let vent = document.querySelector("#vent");
|
||||||
|
let rpon = document.querySelector("#rpon");
|
||||||
|
let rpoff = document.querySelector("#rpoff");
|
||||||
canvas = document.querySelector("#graph").getContext("2d");
|
canvas = document.querySelector("#graph").getContext("2d");
|
||||||
|
|
||||||
// TODO(q3k): unhardcode this.
|
// TODO(q3k): unhardcode this.
|
||||||
|
@ -226,6 +256,18 @@ window.addEventListener("load", (_) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
volts.innerHTML = data.Volts;
|
volts.innerHTML = data.Volts;
|
||||||
mbar.innerHTML = data.Mbar;
|
mbar.innerHTML = data.Mbar;
|
||||||
|
rp.innerHTML = data.RPOn ? "ON" : "OFF";
|
||||||
|
if (data.RPOn) {
|
||||||
|
rp.style = "background-color: #60f060";
|
||||||
|
} else {
|
||||||
|
rp.style = "background-color: #f06060";
|
||||||
|
}
|
||||||
|
dp.innerHTML = data.DPOn ? "ON" : "OFF";
|
||||||
|
if (data.DPOn) {
|
||||||
|
dp.style = "background-color: #60f060";
|
||||||
|
} else {
|
||||||
|
dp.style = "background-color: #f06060";
|
||||||
|
}
|
||||||
historicalPush(data.MbarFloat);
|
historicalPush(data.MbarFloat);
|
||||||
ping.innerHTML = Date.now();
|
ping.innerHTML = Date.now();
|
||||||
});
|
});
|
||||||
|
@ -243,5 +285,24 @@ window.addEventListener("load", (_) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
|
pd.addEventListener("click", async (event) => {
|
||||||
|
await fetch("/button/pumpdown");
|
||||||
|
});
|
||||||
|
vent.addEventListener("click", async (event) => {
|
||||||
|
await fetch("/button/vent");
|
||||||
|
});
|
||||||
|
rpon.addEventListener("click", async (event) => {
|
||||||
|
await fetch("/rp/on");
|
||||||
|
});
|
||||||
|
rpoff.addEventListener("click", async (event) => {
|
||||||
|
await fetch("/rp/off");
|
||||||
|
});
|
||||||
|
dpon.addEventListener("click", async (event) => {
|
||||||
|
await fetch("/dp/on");
|
||||||
|
});
|
||||||
|
dpoff.addEventListener("click", async (event) => {
|
||||||
|
await fetch("/dp/off");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
|
@ -21,11 +21,23 @@ type daemon struct {
|
||||||
// Pirani gauge.
|
// Pirani gauge.
|
||||||
adcPirani adc
|
adcPirani adc
|
||||||
|
|
||||||
|
gpioDiffusionPump gpio
|
||||||
|
gpioRoughingPump gpio
|
||||||
|
gpioBtnPumpDown gpio
|
||||||
|
gpioBtnVent gpio
|
||||||
|
|
||||||
// mu guards state variables below.
|
// mu guards state variables below.
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
// adcPiraniVolts is a moving window of read ADC values, used to calculate a
|
// adcPiraniVolts is a moving window of read ADC values, used to calculate a
|
||||||
// moving average.
|
// moving average.
|
||||||
adcPiraniVolts []float32
|
adcPiraniVolts []float32
|
||||||
|
rpOn bool
|
||||||
|
dpOn bool
|
||||||
|
// ventScheduled and pumpdownScheduled are timers which expire when the
|
||||||
|
// vent/pumpdown relays should be deactivated. This allows these outputs to
|
||||||
|
// be controlled momentarily.
|
||||||
|
ventScheduled time.Time
|
||||||
|
pumpdownScheduled time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// process runs the pain acquisition and control loop of succd.
|
// process runs the pain acquisition and control loop of succd.
|
||||||
|
@ -56,12 +68,24 @@ func (d *daemon) processOnce(_ context.Context) error {
|
||||||
return fmt.Errorf("when reading ADC: %w", err)
|
return fmt.Errorf("when reading ADC: %w", err)
|
||||||
}
|
}
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
d.adcPiraniVolts = append(d.adcPiraniVolts, v)
|
d.adcPiraniVolts = append(d.adcPiraniVolts, v)
|
||||||
trim := len(d.adcPiraniVolts) - 100
|
trim := len(d.adcPiraniVolts) - 100
|
||||||
if trim > 0 {
|
if trim > 0 {
|
||||||
d.adcPiraniVolts = d.adcPiraniVolts[trim:]
|
d.adcPiraniVolts = d.adcPiraniVolts[trim:]
|
||||||
}
|
}
|
||||||
d.mu.Unlock()
|
if err := d.gpioRoughingPump.set(d.rpOn); err != nil {
|
||||||
|
return fmt.Errorf("when configuring RP: %w", err)
|
||||||
|
}
|
||||||
|
if err := d.gpioDiffusionPump.set(!d.dpOn); err != nil {
|
||||||
|
return fmt.Errorf("when configuring RP: %w", err)
|
||||||
|
}
|
||||||
|
if err := d.gpioBtnPumpDown.set(!d.pumpdownScheduled.After(time.Now())); err != nil {
|
||||||
|
return fmt.Errorf("when configuring pumpdown: %w", err)
|
||||||
|
}
|
||||||
|
if err := d.gpioBtnVent.set(!d.ventScheduled.After(time.Now())); err != nil {
|
||||||
|
return fmt.Errorf("when configuring vent: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -84,6 +108,52 @@ func (d *daemon) pirani() (volts float32, mbar float32) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rpSet enables/disables the roughing pump.
|
||||||
|
func (d *daemon) rpSet(state bool) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
d.rpOn = state
|
||||||
|
}
|
||||||
|
|
||||||
|
// rpGet returns whether the roughing pump is enabled/disabled.
|
||||||
|
func (d *daemon) rpGet() bool {
|
||||||
|
d.mu.RLock()
|
||||||
|
defer d.mu.RUnlock()
|
||||||
|
return d.rpOn
|
||||||
|
}
|
||||||
|
|
||||||
|
// dpSet enables/disables the diffusion pump.
|
||||||
|
func (d *daemon) dpSet(state bool) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
d.dpOn = state
|
||||||
|
}
|
||||||
|
|
||||||
|
// dpGet returns whether the diffusion pump is enabled/disabled.
|
||||||
|
func (d *daemon) dpGet() bool {
|
||||||
|
d.mu.RLock()
|
||||||
|
defer d.mu.RUnlock()
|
||||||
|
return d.dpOn
|
||||||
|
}
|
||||||
|
|
||||||
|
// pumpDownPressed toggles the pump down relay for 500ms.
|
||||||
|
func (d *daemon) pumpDownPress() {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
if d.pumpdownScheduled.Before(time.Now()) {
|
||||||
|
d.pumpdownScheduled = time.Now().Add(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ventPress toggles the vent relay for 500ms.
|
||||||
|
func (d *daemon) ventPress() {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
if d.ventScheduled.Before(time.Now()) {
|
||||||
|
d.ventScheduled = time.Now().Add(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
flagFake bool
|
flagFake bool
|
||||||
flagListenHTTP string
|
flagListenHTTP string
|
||||||
|
@ -96,21 +166,49 @@ func main() {
|
||||||
|
|
||||||
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
|
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
|
||||||
d := daemon{}
|
d := daemon{
|
||||||
|
rpOn: true,
|
||||||
|
}
|
||||||
if flagFake {
|
if flagFake {
|
||||||
klog.Infof("Starting with fake Pirani probe")
|
klog.Infof("Starting with fake peripherals")
|
||||||
d.adcPirani = &fakeADC{}
|
d.adcPirani = &fakeADC{}
|
||||||
|
d.gpioRoughingPump = &fakeGPIO{desc: "rp"}
|
||||||
|
d.gpioDiffusionPump = &fakeGPIO{desc: "~dp"}
|
||||||
|
d.gpioBtnPumpDown = &fakeGPIO{desc: "~pd"}
|
||||||
|
d.gpioBtnVent = &fakeGPIO{desc: "~vent"}
|
||||||
} else {
|
} else {
|
||||||
adc, err := newBBADC(0)
|
adc, err := newBBADC(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
klog.Exitf("Failed to setup Pirani ADC: %v", err)
|
klog.Exitf("Failed to setup Pirani ADC: %v", err)
|
||||||
}
|
}
|
||||||
d.adcPirani = adc
|
d.adcPirani = adc
|
||||||
|
|
||||||
|
for _, c := range []struct {
|
||||||
|
out *gpio
|
||||||
|
num int
|
||||||
|
}{
|
||||||
|
{&d.gpioRoughingPump, 115},
|
||||||
|
{&d.gpioDiffusionPump, 49},
|
||||||
|
{&d.gpioBtnPumpDown, 48},
|
||||||
|
{&d.gpioBtnVent, 60},
|
||||||
|
} {
|
||||||
|
// Relay, active low.
|
||||||
|
*c.out, err = newBBGPIO(c.num, true)
|
||||||
|
if err != nil {
|
||||||
|
klog.Exitf("Failed to setup GPIO: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
http.HandleFunc("/", d.httpIndex)
|
http.HandleFunc("/", d.httpIndex)
|
||||||
http.HandleFunc("/stream", d.httpStream)
|
http.HandleFunc("/stream", d.httpStream)
|
||||||
http.HandleFunc("/metrics", d.httpMetrics)
|
http.HandleFunc("/metrics", d.httpMetrics)
|
||||||
|
http.HandleFunc("/button/vent", d.httpButtonVent)
|
||||||
|
http.HandleFunc("/button/pumpdown", d.httpButtonPumpDown)
|
||||||
|
http.HandleFunc("/rp/on", d.httpRoughingPumpEnable)
|
||||||
|
http.HandleFunc("/rp/off", d.httpRoughingPumpDisable)
|
||||||
|
http.HandleFunc("/dp/on", d.httpDiffusionPumpEnable)
|
||||||
|
http.HandleFunc("/dp/off", d.httpDiffusionPumpDisable)
|
||||||
|
|
||||||
klog.Infof("Listening for HTTP at %s", flagListenHTTP)
|
klog.Infof("Listening for HTTP at %s", flagListenHTTP)
|
||||||
go func() {
|
go func() {
|
||||||
|
|
Loading…
Reference in a new issue