diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1680a6e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +## build Go executable +FROM golang:1.19-alpine as go-build + +## copy entire local folder to container root directory +COPY ./ /mqtt-gateway/ + +WORKDIR /mqtt-gateway/cmd/gateway + +## build +RUN go build -v + +## deploy +FROM alpine:3.17.0 + +## copy executable from go-build container +COPY --from=go-build /mqtt-gateway/cmd/gateway/gateway /app/gateway + +## http port +EXPOSE 50000 + +## entrypoint is gateway +ENTRYPOINT ["/app/gateway", "-httpHost", ""] diff --git a/README.md b/README.md index f0e4574..aa0ebcc 100644 --- a/README.md +++ b/README.md @@ -24,17 +24,25 @@ The resources on a micro controller are limited and following the [KISS priciple - client libraries provide an idiomatic way in their respective programming language to communicate with the command station - and finally this gateway provides MQTT integration (like future components might provide integrations to additional protocols) +## Precondition +- a running MQTT broker like [Mosquitto](https://mosquitto.org/) +- the gateway executable or docker container +- command station and model locomotive configuration files + ## Build -For building there is two options available: +For building there are the following options available: - [local build](#local): install Go environment and build on your local machine +- [deploy as docker container](#docker): build and deploy as docker container - [docker build](https://github.com/pico-cs/docker-buld): no toolchain installation but a running docker environment on your local machine is required ### Local -To build go-hdb you need to have + +#### Build +To build the pico-cs mqtt-gateway you need to have - a working Go environment of the [latest Go version](https://golang.org/dl/) and -- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed +- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed. ``` git clone https://github.com/pico-cs/mqtt-gateway.git @@ -48,20 +56,12 @@ Example building executable for Raspberry Pi on Raspberry Pi OS GOOS=linux GOARCH=arm GOARM=7 go build ``` -## Run the gateway - -Preconditions: -- a running MQTT broker like [Mosquitto](https://mosquitto.org/) -- the gateway executable -- command station and model locomotive configuration files - -### Gateway - +#### Run A list of all gateway parameters can be printed via: ``` ./gateway -h ``` -Execute gateway with MQTT host listening at address 10.10.10.42 (default port 1883): +Execute gateway with MQTT broker listening at address 10.10.10.42 (default port 1883): ``` ./gateway -host 10.10.10.42 ``` @@ -71,6 +71,39 @@ Execute gateway reading configurations files stored in directory /pico-cs/config ./gateway -configDir /pico-cs/config ``` +### Docker +To build and run the pico-cs mqtt-gateway as docker container you need to have +- a running [docker](https://docs.docker.com/engine/install/) environment and +- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed + +#### Build +``` +git clone https://github.com/pico-cs/mqtt-gateway.git +cd mqtt-gateway +docker build --tag pico-cs/mqtt-gateway . +``` + +#### Run +A list of all gateway parameters can be printed via: +``` +docker run -it pico-cs/mqtt-gateway -h +``` + +Execute container with +- command station (pico) is connected to /dev/ttyACM0 +- MQTT broker listening at address 10.10.10.42 (default port 1883) +- mqtt-gateway http endpoint (default 50000) should be made available on same host port 50000 +- run the container in detached mode + +``` +docker run -d --device /dev/ttyACM0 -p 50000:50000 pico-cs/mqtt-gateway -mqttHost='10.10.10.42' +``` + +Execute container in interactive mode: +``` +docker run -it --device /dev/ttyACM0 -p 50000:50000 pico-cs/mqtt-gateway -mqttHost='10.10.10.42' +``` + ### Configuration files To configure the gateway's command station and loco parameters [YAML files](https://yaml.org/) are used. The entire configuration can be stored in one file or in multiple files. During the start of the gateway the configuration directory (parameter configDir) and it's subdirectories are scanned for valid configuration files with file extension '.yaml' or '.yml'. The directory tree scan is a depth-first search and within a directory the files are visited in a lexical order. If a configuration for a device is found more than once the last one wins. diff --git a/cmd/gateway/gateway.go b/cmd/gateway/gateway.go index a34b307..e96235f 100644 --- a/cmd/gateway/gateway.go +++ b/cmd/gateway/gateway.go @@ -91,17 +91,17 @@ func (c *config) parseYaml(b []byte) error { switch typ { case devices.CtCS: - var csConfig devices.CSConfig - if err := dd.Decode(&csConfig); err != nil { + csConfig := devices.NewCSConfig() + if err := dd.Decode(csConfig); err != nil { return err } - c.csConfigMap[csConfig.Name] = &csConfig + c.csConfigMap[csConfig.Name] = csConfig case devices.CtLoco: - var locoConfig devices.LocoConfig - if err := dd.Decode(&locoConfig); err != nil { + locoConfig := devices.NewLocoConfig() + if err := dd.Decode(locoConfig); err != nil { return err } - c.locoConfigMap[locoConfig.Name] = &locoConfig + c.locoConfigMap[locoConfig.Name] = locoConfig default: return fmt.Errorf("invalid configuration %v", m) } @@ -174,8 +174,9 @@ func (s *deviceSets) register(config *config) error { } func (s *deviceSets) registerHTTP(server *server.Server) { - server.HandleFunc("/cs", s.csSet.HandleFunc(server.Addr())) - server.HandleFunc("/loco", s.locoSet.HandleFunc(server.Addr())) + server.HandleFunc("/", devices.HTTPHandler) + server.Handle("/cs", s.csSet) + server.Handle("/loco", s.locoSet) for name, cs := range s.csSet.Items() { server.Handle(fmt.Sprintf("/cs/%s", name), cs) } diff --git a/go.mod b/go.mod index cab9b20..346181d 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ go 1.19 require ( github.com/eclipse/paho.mqtt.golang v1.4.2 - github.com/pico-cs/go-client v0.4.0 - golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 + github.com/pico-cs/go-client v0.4.1 + golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 3bbd46c..fc18e7a 100644 --- a/go.sum +++ b/go.sum @@ -6,15 +6,15 @@ github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/pico-cs/go-client v0.4.0 h1:HaiXi1EQzJyXNeMZ34HKITmYvrkdliYlmDuOZSCRMPM= -github.com/pico-cs/go-client v0.4.0/go.mod h1:n639HlI0L283jlN5cCZsbpVr9NbB8l6NCGQsKO8AjdE= +github.com/pico-cs/go-client v0.4.1 h1:oAlCYXL8XTq/FeXK8zey3kNbYeZEp+HWx2XbgyavyZM= +github.com/pico-cs/go-client v0.4.1/go.mod h1:n639HlI0L283jlN5cCZsbpVr9NbB8l6NCGQsKO8AjdE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= go.bug.st/serial v1.5.0 h1:ThuUkHpOEmCVXxGEfpoExjQCS2WBVV4ZcUKVYInM9T4= go.bug.st/serial v1.5.0/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 h1:m9O6OTJ627iFnN2JIWfdqlZCzneRO6EEBsHXI25P8ws= -golang.org/x/exp v0.0.0-20221230185412-738e83a70c30/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3 h1:fJwx88sMf5RXwDwziL0/Mn9Wqs+efMSo/RYcL+37W9c= +golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= diff --git a/internal/devices/config.go b/internal/devices/config.go index b88fa88..cf63f61 100644 --- a/internal/devices/config.go +++ b/internal/devices/config.go @@ -76,6 +76,11 @@ type Filter struct { Excls []string `json:"excls"` } +// NewFilter returns a new Filter instance. +func NewFilter() *Filter { + return &Filter{Incls: []string{}, Excls: []string{}} +} + func (f *Filter) filter() (*filter, error) { return newFilter(f.Incls, f.Excls) } // CSIOConfig represents configuration data for a command station IO. @@ -93,13 +98,22 @@ type CSConfig struct { // TCP/IP port (WiFi) or serial port (serial over USB) Port string `json:"port"` // filter of devices for which this command station should be a primary device - Primary Filter `json:"primary"` + Primary *Filter `json:"primary"` // filter of devices for which this command station should be a secondary device - Secondary Filter `json:"secondary"` + Secondary *Filter `json:"secondary"` // command station IO mapping (key is used in topic) IOs map[string]CSIOConfig `json:"ios"` } +// NewCSConfig returns a new CSConfig instance. +func NewCSConfig() *CSConfig { + return &CSConfig{ + Primary: NewFilter(), + Secondary: NewFilter(), + IOs: map[string]CSIOConfig{}, + } +} + func (c *CSConfig) validate() error { if err := gateway.CheckLevelName(c.Name); err != nil { return fmt.Errorf("CSConfig name %s: %s", c.Name, err) @@ -131,6 +145,11 @@ type LocoConfig struct { Fcts map[string]LocoFctConfig `json:"fcts"` } +// NewLocoConfig returns a new LocoConfig instance. +func NewLocoConfig() *LocoConfig { + return &LocoConfig{Fcts: map[string]LocoFctConfig{}} +} + // ReservedFctNames is the list of reserved function names which cannot be used in loco configurations. var ReservedFctNames = []string{"dir", "speed"} diff --git a/internal/devices/cs.go b/internal/devices/cs.go index 8014f68..678135b 100644 --- a/internal/devices/cs.go +++ b/internal/devices/cs.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "net/http" - "net/url" "sync" "github.com/pico-cs/go-client/client" @@ -52,27 +51,79 @@ func (s *CSSet) Close() error { return lastErr } -// HandleFunc returns a http.HandleFunc handler. -func (s *CSSet) HandleFunc(addr string) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - type tpldata struct { - Title string - Items map[string]*url.URL +/* +func (s *CSSet) idxTplExecute(w io.Writer) error { + var b bytes.Buffer + + renderLocos := func(title string, locos []*Loco) { + if len(locos) > 0 { + b.WriteString(` + + + + command stations + + + `) + + for _, loco := range locos { + link := &url.URL{Path: fmt.Sprintf("/loco/%s", loco.name())} + fmt.Fprintf(&b, "
%s
\n", link, html.EscapeString(loco.name())) + } } + } - data := &tpldata{Items: map[string]*url.URL{}} + b.WriteString(` + + + + command stations + + +