initial commit
commit
737edd36bb
@ -0,0 +1,11 @@
|
|||||||
|
module git.metznet.ca/MetzNet/htmx-mqtt
|
||||||
|
|
||||||
|
go 1.21.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/eclipse/paho.mqtt.golang v1.4.3 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
|
golang.org/x/net v0.8.0 // indirect
|
||||||
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
|
nhooyr.io/websocket v1.8.10 // indirect
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
|
||||||
|
github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||||
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
|
||||||
|
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
@ -0,0 +1,189 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"fmt"
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"nhooyr.io/websocket"
|
||||||
|
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MQTTHandler struct {
|
||||||
|
sync.Mutex
|
||||||
|
Template string
|
||||||
|
Channels []chan mqtt.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *MQTTHandler) ProcessMessage(client mqtt.Client, message mqtt.Message) {
|
||||||
|
message.Ack()
|
||||||
|
handler.Lock()
|
||||||
|
defer handler.Unlock()
|
||||||
|
|
||||||
|
remaining := make([]chan mqtt.Message, 0, len(handler.Channels))
|
||||||
|
for _, channel := range(handler.Channels) {
|
||||||
|
select {
|
||||||
|
case channel <- message:
|
||||||
|
remaining = append(remaining, channel)
|
||||||
|
default:
|
||||||
|
os.Stderr.WriteString("Channel overflow\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.Channels = remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *MQTTHandler) AddChannel(channel chan mqtt.Message) func() {
|
||||||
|
handler.Lock()
|
||||||
|
defer handler.Unlock()
|
||||||
|
|
||||||
|
handler.Channels = append(handler.Channels, channel)
|
||||||
|
return func() {
|
||||||
|
handler.Lock()
|
||||||
|
defer handler.Unlock()
|
||||||
|
|
||||||
|
idx := slices.Index(handler.Channels, channel)
|
||||||
|
if idx < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.Channels[idx] = handler.Channels[len(handler.Channels)-1]
|
||||||
|
handler.Channels = handler.Channels[:len(handler.Channels)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MQTTHandlerClient struct {
|
||||||
|
mqtt.Client
|
||||||
|
Subscriptions map[*MQTTHandler]string
|
||||||
|
|
||||||
|
SubscribeTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMQTTHandlerClient(broker string, username string, password string, id string) (*MQTTHandlerClient, error) {
|
||||||
|
opts := mqtt.NewClientOptions()
|
||||||
|
opts.AddBroker(broker)
|
||||||
|
opts.SetClientID(id)
|
||||||
|
opts.SetUsername(username)
|
||||||
|
opts.SetPassword(password)
|
||||||
|
opts.SetKeepAlive(2 * time.Second)
|
||||||
|
opts.SetPingTimeout(1 * time.Second)
|
||||||
|
client := mqtt.NewClient(opts)
|
||||||
|
|
||||||
|
if token := client.Connect(); token.Wait() && token.Error() != nil {
|
||||||
|
return nil, token.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MQTTHandlerClient{
|
||||||
|
Client: client,
|
||||||
|
Subscriptions: map[*MQTTHandler]string{},
|
||||||
|
SubscribeTimeout: 1*time.Second,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *MQTTHandlerClient) NewHandler(subscription string, template string) (*MQTTHandler, error) {
|
||||||
|
handler := &MQTTHandler{
|
||||||
|
Template: template,
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_token := client.Subscribe(subscription, 0x00, handler.ProcessMessage)
|
||||||
|
|
||||||
|
timeout := sub_token.WaitTimeout(client.SubscribeTimeout)
|
||||||
|
if timeout == false || sub_token.Error() != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to subscribe to %s - %e", subscription, sub_token.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Subscriptions[handler] = subscription
|
||||||
|
return handler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *MQTTHandler) Format(message mqtt.Message) []byte {
|
||||||
|
return []byte(fmt.Sprintf(handler.Template, message.Payload()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *MQTTHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
channel := make(chan mqtt.Message, 1)
|
||||||
|
remove_channel := handler.AddChannel(channel)
|
||||||
|
defer remove_channel()
|
||||||
|
|
||||||
|
conn, err := websocket.Accept(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
os.Stderr.WriteString(fmt.Sprintf("websocket accept error: %s\n", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.CloseNow()
|
||||||
|
ctx, cancel_func := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
go func(conn *websocket.Conn, cancel_func context.CancelFunc) {
|
||||||
|
for true {
|
||||||
|
msg_type, data, err := conn.Read(ctx)
|
||||||
|
if err != nil {
|
||||||
|
os.Stderr.WriteString(fmt.Sprintf("websocket error: %s\n", err))
|
||||||
|
cancel_func()
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
os.Stderr.WriteString(fmt.Sprintf("websocket data(%s): %s\n", msg_type, string(data)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(conn, cancel_func)
|
||||||
|
|
||||||
|
running := true
|
||||||
|
done := ctx.Done()
|
||||||
|
for running == true {
|
||||||
|
select {
|
||||||
|
case <- done:
|
||||||
|
os.Stderr.WriteString("websocket context done")
|
||||||
|
running = false
|
||||||
|
case message := <- channel:
|
||||||
|
os.Stderr.WriteString(fmt.Sprintf("websocket write: %s\n", message.Payload()))
|
||||||
|
err := conn.Write(ctx, websocket.MessageText, message.Payload())
|
||||||
|
if err != nil {
|
||||||
|
os.Stderr.WriteString(fmt.Sprintf("websocket write error: %s\n", err))
|
||||||
|
running = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
handler_client, err := NewMQTTHandlerClient("tcp://localhost:1883", "", "", "htmx")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler_1, err := handler_client.NewHandler("test", "%s")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/", http.FileServer(http.Dir("./site")))
|
||||||
|
mux.Handle("/ws", handler_1)
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Handler: mux,
|
||||||
|
Addr: ":8080",
|
||||||
|
}
|
||||||
|
|
||||||
|
sigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGTERM, syscall.SIGKILL)
|
||||||
|
go func(sigs chan os.Signal, server *http.Server) {
|
||||||
|
<- sigs
|
||||||
|
server.Close()
|
||||||
|
}(sigs, server)
|
||||||
|
|
||||||
|
err = server.ListenAndServe()
|
||||||
|
if errors.Is(err, http.ErrServerClosed) == true {
|
||||||
|
os.Stderr.WriteString("Server closed on signal\n")
|
||||||
|
} else if err != nil {
|
||||||
|
os.Stderr.WriteString(fmt.Sprintf("Server error: %s\n", err))
|
||||||
|
} else {
|
||||||
|
os.Stderr.WriteString("Server closed\n")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE <!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>HTMX MQTT Test Page</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="style.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/htmx.org"></script>
|
||||||
|
<script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Test Page</h1>
|
||||||
|
<div hx-ext="ws" ws-connect="/ws">
|
||||||
|
<div id="messages" hx-swap-oob="morphdom">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue