16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Launch Package",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/src/main.go"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
61
Makefile
Normal file
61
Makefile
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
PROJECT_NAME := printctrl
|
||||||
|
|
||||||
|
PREFIX ?= /usr/bin
|
||||||
|
|
||||||
|
CONFIG_DIR := /etc/$(PROJECT_NAME)
|
||||||
|
SYSTEM_DIR := /usr/lib/systemd/system
|
||||||
|
WEB_DIR := /var/lib/$(PROJECT_NAME)
|
||||||
|
|
||||||
|
CONFIG_FILE := config/octoprint/docker-compose.yml
|
||||||
|
BIN_FILE := build/bin/$(PROJECT_NAME)
|
||||||
|
UNIT_FILE := $(PROJECT_NAME).service
|
||||||
|
README_FILE := README.md
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: service
|
||||||
|
|
||||||
|
.PHONY: service
|
||||||
|
service:
|
||||||
|
mkdir -p bin
|
||||||
|
cd src && go build -o ../$(BIN_FILE)
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
go clean
|
||||||
|
rm -rf bin
|
||||||
|
-rm $(PROJECT_NAME).tar.gz
|
||||||
|
|
||||||
|
.PHONY: install
|
||||||
|
install: all
|
||||||
|
echo $(PWD)
|
||||||
|
@if [ -f $(CONFIG_DIR)/octoprint/$(notdir $(CONFIG_FILE)) ]; then \
|
||||||
|
echo "$(CONFIG_DIR)/$(notdir $(CONFIG_FILE)) already exists - skipping..."; \
|
||||||
|
else \
|
||||||
|
install -d $(CONFIG_DIR)/octoprint; \
|
||||||
|
install -m 0644 $(CONFIG_FILE) $(CONFIG_DIR)/octoprint; \
|
||||||
|
echo "install -d $(CONFIG_DIR)/octoprint"; \
|
||||||
|
echo "install -m 0644 $(CONFIG_FILE) $(CONFIG_DIR)/octoprint"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Binary
|
||||||
|
install -d $(PREFIX)
|
||||||
|
install -m 0755 $(BIN_FILE) $(PREFIX)
|
||||||
|
|
||||||
|
# System unit
|
||||||
|
install -d $(SYSTEM_DIR)
|
||||||
|
install -m 0644 $(UNIT_FILE) $(SYSTEM_DIR)
|
||||||
|
|
||||||
|
# Web ui
|
||||||
|
install -d $(WEB_DIR)
|
||||||
|
cp -r webui/* $(WEB_DIR)
|
||||||
|
|
||||||
|
.PHONY: uninstall
|
||||||
|
uninstall:
|
||||||
|
rm -rf $(CONFIG_DIR)
|
||||||
|
rm -rf $(SYSTEM_DIR)/$(UNIT_FILE)
|
||||||
|
rm -rf $(PREFIX)/$(PROJECT_NAME)
|
||||||
|
rm -rf $(WEB_DIR)
|
||||||
|
|
||||||
|
.PHONY: package
|
||||||
|
package: all
|
||||||
|
tar cvzf $(PROJECT_NAME).tar.gz $(CONFIG_FILE) $(BIN_FILE) $(UNIT_FILE) $(README_FILE) webui/build/*
|
40
config/octoprint/docker-compose.yml
Normal file
40
config/octoprint/docker-compose.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
version: '2.4'
|
||||||
|
|
||||||
|
services:
|
||||||
|
octoprint:
|
||||||
|
image: octoprint/octoprint
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
devices:
|
||||||
|
# use `python -m serial.tools.miniterm` to see what the name is of the printer, this requires pyserial
|
||||||
|
- /dev/ttyUSB0:/dev/ttyUSB0
|
||||||
|
- /dev/video0:/dev/video0
|
||||||
|
volumes:
|
||||||
|
- octoprint:/octoprint
|
||||||
|
# uncomment the lines below to ensure camera streaming is enabled when
|
||||||
|
# you add a video device
|
||||||
|
environment:
|
||||||
|
- ENABLE_MJPG_STREAMER=true
|
||||||
|
|
||||||
|
####
|
||||||
|
# uncomment if you wish to edit the configuration files of octoprint
|
||||||
|
# refer to docs on configuration editing for more information
|
||||||
|
####
|
||||||
|
|
||||||
|
#config-editor:
|
||||||
|
# image: linuxserver/code-server
|
||||||
|
# ports:
|
||||||
|
# - 8443:8443
|
||||||
|
# depends_on:
|
||||||
|
# - octoprint
|
||||||
|
# restart: unless-stopped
|
||||||
|
# environment:
|
||||||
|
# - PUID=0
|
||||||
|
# - PGID=0
|
||||||
|
# - TZ=America/Chicago
|
||||||
|
# volumes:
|
||||||
|
# - octoprint:/octoprint
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
octoprint:
|
10
printctrl.service
Normal file
10
printctrl.service
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=3d printer control service
|
||||||
|
After=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=idle
|
||||||
|
ExecStart=/usr/bin/printctrl -w /var/lib/printctrl
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
37
src/app/process/octoprint/octoprint.go
Normal file
37
src/app/process/octoprint/octoprint.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package octoprint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"powerswitch/app/process"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
start_cmd = "docker-compose -f /etc/printctrl/octoprint/docker-compose.yml up -d"
|
||||||
|
stop_cmd = "docker-compose -f /etc/printctrl/octoprint/docker-compose.yml down"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger log.Logger = *log.Default()
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logger.SetFlags(log.Llongfile | log.Ltime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Start() error {
|
||||||
|
p := process.NewProcess(start_cmd)
|
||||||
|
return p.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Stop() error {
|
||||||
|
p := process.NewProcess(stop_cmd)
|
||||||
|
return p.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReStart() error {
|
||||||
|
err := Stop()
|
||||||
|
if err != nil {
|
||||||
|
logger.Print(err)
|
||||||
|
}
|
||||||
|
return Start()
|
||||||
|
}
|
77
src/app/process/process.go
Normal file
77
src/app/process/process.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger log.Logger = *log.Default()
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logger.SetFlags(log.Llongfile | log.Ltime)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Process struct {
|
||||||
|
StdoutChannel chan string
|
||||||
|
StderrChannel chan string
|
||||||
|
|
||||||
|
process *exec.Cmd
|
||||||
|
stdout io.ReadCloser
|
||||||
|
stderr io.ReadCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProcess(command string) *Process {
|
||||||
|
p := new(Process)
|
||||||
|
p.process = exec.Command("bash", "-c", command)
|
||||||
|
var err error
|
||||||
|
p.stdout, err = p.process.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
logger.Panic(err)
|
||||||
|
}
|
||||||
|
p.stderr, err = p.process.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
logger.Panic(err)
|
||||||
|
}
|
||||||
|
p.StdoutChannel = make(chan string)
|
||||||
|
p.StderrChannel = make(chan string)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Process) Start() error {
|
||||||
|
err := p.process.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Process) Observe() {
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(p.stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
p.StdoutChannel <- scanner.Text()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(p.stderr)
|
||||||
|
for scanner.Scan() {
|
||||||
|
p.StderrChannel <- scanner.Text()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Process) Kill() {
|
||||||
|
p.process.Process.Kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Process) Wait() error {
|
||||||
|
err := p.process.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
9
src/go.mod
Normal file
9
src/go.mod
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module powerswitch
|
||||||
|
|
||||||
|
go 1.22.6
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/taigrr/systemctl v1.0.10
|
||||||
|
periph.io/x/conn/v3 v3.7.2
|
||||||
|
periph.io/x/host/v3 v3.8.5
|
||||||
|
)
|
8
src/go.sum
Normal file
8
src/go.sum
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||||
|
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||||
|
github.com/taigrr/systemctl v1.0.10 h1:J1ifqf9wXYpbGYjCOgDIz9niFLCdlpNpIHRn9cA1J7g=
|
||||||
|
github.com/taigrr/systemctl v1.0.10/go.mod h1:TpeHkNuHgYT63FI5jVLBf5VNAGbxEFH3FHqg5ReXnd0=
|
||||||
|
periph.io/x/conn/v3 v3.7.2 h1:qt9dE6XGP5ljbFnCKRJ9OOCoiOyBGlw7JZgoi72zZ1s=
|
||||||
|
periph.io/x/conn/v3 v3.7.2/go.mod h1:Ao0b4sFRo4QOx6c1tROJU1fLJN1hUIYggjOrkIVnpGg=
|
||||||
|
periph.io/x/host/v3 v3.8.5 h1:g4g5xE1XZtDiGl1UAJaUur1aT7uNiFLMkyMEiZ7IHII=
|
||||||
|
periph.io/x/host/v3 v3.8.5/go.mod h1:hPq8dISZIc+UNfWoRj+bPH3XEBQqJPdFdx218W92mdc=
|
98
src/internal/apiservice/data/data.go
Normal file
98
src/internal/apiservice/data/data.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package apiservice_data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
apiservice_relay "powerswitch/internal/apiservice/relay"
|
||||||
|
)
|
||||||
|
|
||||||
|
type state string
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
State state `json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateOn state = "on"
|
||||||
|
StateOff state = "off"
|
||||||
|
id int = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger log.Logger = *log.Default()
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logger.SetFlags(log.Llongfile | log.Ltime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddHandler() {
|
||||||
|
http.HandleFunc("GET /data/state", handle_get_powerstate)
|
||||||
|
http.HandleFunc("PATCH /data/state", handle_patch_powerstate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetState() (bool, error) {
|
||||||
|
res, err := apiservice_relay.GetRelay(id)
|
||||||
|
return !res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetState(state bool) error {
|
||||||
|
return apiservice_relay.SetRelay(id, !state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle_get_powerstate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-type", "application/json; charset=utf-8;")
|
||||||
|
state, err := apiservice_relay.GetRelay(id)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var res State
|
||||||
|
if state {
|
||||||
|
res.State = StateOff
|
||||||
|
} else {
|
||||||
|
res.State = StateOn
|
||||||
|
}
|
||||||
|
tmp, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle_patch_powerstate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-type", "application/json; charset=utf-8;")
|
||||||
|
|
||||||
|
tmp, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var state State
|
||||||
|
err = json.Unmarshal(tmp, &state)
|
||||||
|
if err != nil {
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
set_state := true
|
||||||
|
if state.State == StateOn {
|
||||||
|
set_state = false
|
||||||
|
}
|
||||||
|
err = apiservice_relay.SetRelay(id, set_state)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
98
src/internal/apiservice/power/power.go
Normal file
98
src/internal/apiservice/power/power.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package apiservice_power
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
apiservice_relay "powerswitch/internal/apiservice/relay"
|
||||||
|
)
|
||||||
|
|
||||||
|
type state string
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
State state `json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateOn state = "on"
|
||||||
|
StateOff state = "off"
|
||||||
|
id int = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger log.Logger = *log.Default()
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logger.SetFlags(log.Llongfile | log.Ltime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddHandler() {
|
||||||
|
http.HandleFunc("GET /power/state", handle_get_powerstate)
|
||||||
|
http.HandleFunc("PATCH /power/state", handle_patch_powerstate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetState() (bool, error) {
|
||||||
|
res, err := apiservice_relay.GetRelay(id)
|
||||||
|
return !res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetState(state bool) error {
|
||||||
|
return apiservice_relay.SetRelay(id, !state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle_get_powerstate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-type", "application/json; charset=utf-8;")
|
||||||
|
state, err := apiservice_relay.GetRelay(id)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var res State
|
||||||
|
if state {
|
||||||
|
res.State = StateOff
|
||||||
|
} else {
|
||||||
|
res.State = StateOn
|
||||||
|
}
|
||||||
|
tmp, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle_patch_powerstate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-type", "application/json; charset=utf-8;")
|
||||||
|
|
||||||
|
tmp, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var state State
|
||||||
|
err = json.Unmarshal(tmp, &state)
|
||||||
|
if err != nil {
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
set_state := true
|
||||||
|
if state.State == StateOn {
|
||||||
|
set_state = false
|
||||||
|
}
|
||||||
|
err = apiservice_relay.SetRelay(id, set_state)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
132
src/internal/apiservice/printer/printer.go
Normal file
132
src/internal/apiservice/printer/printer.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package apiservice_printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"powerswitch/app/process/octoprint"
|
||||||
|
apiservice_data "powerswitch/internal/apiservice/data"
|
||||||
|
apiservice_power "powerswitch/internal/apiservice/power"
|
||||||
|
)
|
||||||
|
|
||||||
|
type state string
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
State state `json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateOn state = "on"
|
||||||
|
StateOff state = "off"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger log.Logger = *log.Default()
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logger.SetFlags(log.Llongfile | log.Ltime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddHandler() {
|
||||||
|
http.HandleFunc("GET /printer/state", handle_get_printerstate)
|
||||||
|
http.HandleFunc("PATCH /printer/state", handle_patch_printerstate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle_get_printerstate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-type", "application/json; charset=utf-8;")
|
||||||
|
data_state, err := apiservice_data.GetState()
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
power_state, err := apiservice_power.GetState()
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var res State
|
||||||
|
if data_state && power_state {
|
||||||
|
res.State = StateOn
|
||||||
|
} else {
|
||||||
|
res.State = StateOff
|
||||||
|
}
|
||||||
|
tmp, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle_patch_printerstate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-type", "application/json; charset=utf-8;")
|
||||||
|
|
||||||
|
tmp, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var state State
|
||||||
|
err = json.Unmarshal(tmp, &state)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.State == StateOn {
|
||||||
|
err := apiservice_data.SetState(true)
|
||||||
|
if err != nil {
|
||||||
|
logger.Print(err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = apiservice_power.SetState(true)
|
||||||
|
if err != nil {
|
||||||
|
logger.Print(err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = octoprint.ReStart()
|
||||||
|
if err != nil {
|
||||||
|
logger.Print(err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := octoprint.Stop()
|
||||||
|
if err != nil {
|
||||||
|
logger.Print(err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = apiservice_power.SetState(false)
|
||||||
|
if err != nil {
|
||||||
|
logger.Print(err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = apiservice_data.SetState(false)
|
||||||
|
if err != nil {
|
||||||
|
logger.Print(err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
130
src/internal/apiservice/relay/relay.go
Normal file
130
src/internal/apiservice/relay/relay.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package apiservice_relay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"periph.io/x/conn/v3/gpio"
|
||||||
|
"periph.io/x/conn/v3/gpio/gpioreg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type relay struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Value bool `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger log.Logger = *log.Default()
|
||||||
|
gpios = []string{"GPIO26", "GPIO20", "GPIO21"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logger.SetFlags(log.Llongfile | log.Ltime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRelay(id int) (bool, error) {
|
||||||
|
if id >= len(gpios) {
|
||||||
|
return false, errors.New("index out of range")
|
||||||
|
}
|
||||||
|
p := gpioreg.ByName(gpios[id])
|
||||||
|
if p == nil {
|
||||||
|
return false, errors.New("gpio not found")
|
||||||
|
}
|
||||||
|
state := p.Read()
|
||||||
|
return bool(state), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetRelay(id int, state bool) error {
|
||||||
|
if id >= len(gpios) {
|
||||||
|
return errors.New("index out of range")
|
||||||
|
}
|
||||||
|
p := gpioreg.ByName(gpios[id])
|
||||||
|
if p == nil {
|
||||||
|
return errors.New("gpio not found")
|
||||||
|
}
|
||||||
|
return p.Out(gpio.Level(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddHandler() {
|
||||||
|
http.HandleFunc("GET /relay/{id}", handle_get_relays)
|
||||||
|
http.HandleFunc("PATCH /relay/{id}", handle_patch_relays)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle_get_relays(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-type", "application/json; charset=utf-8;")
|
||||||
|
id, err := strconv.Atoi(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if id >= len(gpios) {
|
||||||
|
logger.Print("Index out of range: ", id)
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p := gpioreg.ByName(gpios[id])
|
||||||
|
if p == nil {
|
||||||
|
logger.Print("unable to find gpio")
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state := p.Read()
|
||||||
|
logger.Print("state: ", state)
|
||||||
|
var rel relay
|
||||||
|
rel.Id = id
|
||||||
|
rel.Value = bool(state)
|
||||||
|
res, err := json.Marshal(rel)
|
||||||
|
if err != nil {
|
||||||
|
logger.Print("unable marshal obj to json")
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(res)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle_patch_relays(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-type", "application/json; charset=utf-8;")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if id >= len(gpios) {
|
||||||
|
logger.Print("Index out of range: ", id)
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(`{"error": "cannot read reqest body"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var rel relay
|
||||||
|
err = json.Unmarshal(tmp, &rel)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(`{"error": "cannot unmarshal json to object"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p := gpioreg.ByName(gpios[id])
|
||||||
|
if p == nil {
|
||||||
|
logger.Print("unable to find gpio")
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = p.Out(gpio.Level(rel.Value))
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(json.RawMessage(`{"error": "cannot set gpio"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
40
src/main.go
Normal file
40
src/main.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
apiservice_data "powerswitch/internal/apiservice/data"
|
||||||
|
apiservice_power "powerswitch/internal/apiservice/power"
|
||||||
|
apiservice_printer "powerswitch/internal/apiservice/printer"
|
||||||
|
apiservice_relay "powerswitch/internal/apiservice/relay"
|
||||||
|
|
||||||
|
host "periph.io/x/host/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger log.Logger = *log.Default()
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logger.SetFlags(log.Llongfile | log.Ltime)
|
||||||
|
logger.Println("Starting")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var webui_path string
|
||||||
|
flag.StringVar(&webui_path, "w", "../webui", "Specify path to serve the web ui. Default is ../webui")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
host.Init()
|
||||||
|
|
||||||
|
apiservice_data.AddHandler()
|
||||||
|
apiservice_power.AddHandler()
|
||||||
|
apiservice_printer.AddHandler()
|
||||||
|
apiservice_relay.AddHandler()
|
||||||
|
|
||||||
|
port := ":5005"
|
||||||
|
http.Handle("/", http.FileServer(http.Dir(webui_path)))
|
||||||
|
logger.Fatal(http.ListenAndServe(port, nil))
|
||||||
|
}
|
61
webui/css/slider.css
Normal file
61
webui/css/slider.css
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 60px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide default HTML checkbox */
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The slider */
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #5b5b5b;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus + .slider {
|
||||||
|
box-shadow: 0 0 1px #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
-webkit-transform: translateX(26px);
|
||||||
|
-ms-transform: translateX(26px);
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rounded sliders */
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
14
webui/css/style.css
Normal file
14
webui/css/style.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
display: table;
|
||||||
|
margin: auto;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
113
webui/index.html
Normal file
113
webui/index.html
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<link rel="stylesheet" href="css/slider.css">
|
||||||
|
<title id="title">3D Drucker</title>
|
||||||
|
<script type="text/javaScript">
|
||||||
|
const POWER_STATE_URL = "/power/state";
|
||||||
|
const DATA_STATE_URL = "/data/state";
|
||||||
|
const PRINTER_STATE_URL = "/printer/state";
|
||||||
|
function init() {
|
||||||
|
get_state(PRINTER_STATE_URL, "printer_slider");
|
||||||
|
get_state(POWER_STATE_URL, "power_slider");
|
||||||
|
get_state(DATA_STATE_URL, "data_slider");
|
||||||
|
setInterval(poll, 1000);
|
||||||
|
}
|
||||||
|
function poll() {
|
||||||
|
get_state(PRINTER_STATE_URL, "printer_slider");
|
||||||
|
get_state(POWER_STATE_URL, "power_slider");
|
||||||
|
get_state(DATA_STATE_URL, "data_slider");
|
||||||
|
}
|
||||||
|
function get_state(url, target) {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("GET", url);
|
||||||
|
xhr.send();
|
||||||
|
xhr.responseType = "json";
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.readyState == 4 && xhr.status == 200) {
|
||||||
|
let state = xhr.response
|
||||||
|
let btn = document.getElementById(target);
|
||||||
|
let chk = false;
|
||||||
|
if(state.state === "on") {
|
||||||
|
chk = true;
|
||||||
|
}
|
||||||
|
btn.checked = chk;
|
||||||
|
} else {
|
||||||
|
console.log(`Error: ${xhr.status}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function check(checkbox, name) {
|
||||||
|
var obj;
|
||||||
|
var url;
|
||||||
|
if(checkbox.checked) {
|
||||||
|
obj = '{"state":"on"}'
|
||||||
|
} else {
|
||||||
|
obj = '{"state":"off"}'
|
||||||
|
}
|
||||||
|
if(name === "power") {
|
||||||
|
url = POWER_STATE_URL;
|
||||||
|
} else if(name === "data") {
|
||||||
|
url = DATA_STATE_URL;
|
||||||
|
} else if(name === "printer") {
|
||||||
|
url = PRINTER_STATE_URL;
|
||||||
|
}
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("PATCH", url);
|
||||||
|
xhr.setRequestHeader("Content-type", "application/json; charset=utf-8");
|
||||||
|
// xhr.onload = () => {
|
||||||
|
// var data = JSON.parse(xhr.responseText);
|
||||||
|
// if (xhr.readyState == 4 && xhr.status == "202") {
|
||||||
|
// console.log(data);
|
||||||
|
// } else {
|
||||||
|
// console.log(`Error: ${xhr.status}`);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
xhr.send(obj);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body onload=init()>
|
||||||
|
<div class="headercontainer">
|
||||||
|
<!-- <div class="meta">
|
||||||
|
<div class="left" style="line-height: 24px;font-weight: bold;" id="headline">Wasser</div>
|
||||||
|
<div class="middle"></div>
|
||||||
|
<div class="right"><span></span></div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- <div class="header" id="header_cnt">
|
||||||
|
<div class="headerlogo"><img src="images/logo_perinet.png" width="258" height="94" alt="" /></div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<div id="content">
|
||||||
|
<h1>3D Drucker</h1>
|
||||||
|
<label class="switch">
|
||||||
|
<input id="printer_slider" type="checkbox" onchange="check(this, 'printer')">
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
<h2>Schalter</h2>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Strom:</td>
|
||||||
|
<td>
|
||||||
|
<label class="switch">
|
||||||
|
<input id="power_slider" type="checkbox" onchange="check(this, 'power')">
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Daten:</td>
|
||||||
|
<td>
|
||||||
|
<label class="switch">
|
||||||
|
<input id="data_slider" type="checkbox" onchange="check(this, 'data')">
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in New Issue
Block a user