Skip to content

Commit

Permalink
mqtt-gateway v0.7.2
Browse files Browse the repository at this point in the history
  • Loading branch information
stfnmllr committed Jan 6, 2023
1 parent e77fc54 commit c1b94aa
Show file tree
Hide file tree
Showing 11 changed files with 277 additions and 75 deletions.
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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", ""]
59 changes: 46 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
```
Expand All @@ -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.

Expand Down
17 changes: 9 additions & 8 deletions cmd/gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
23 changes: 21 additions & 2 deletions internal/devices/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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"}

Expand Down
94 changes: 78 additions & 16 deletions internal/devices/cs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"sync"

"github.com/pico-cs/go-client/client"
Expand Down Expand Up @@ -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(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>command stations</title>
</head>
<body>
`)
for _, loco := range locos {
link := &url.URL{Path: fmt.Sprintf("/loco/%s", loco.name())}
fmt.Fprintf(&b, "<div><a href='%s'>%s</a></div>\n", link, html.EscapeString(loco.name()))
}
}
}
data := &tpldata{Items: map[string]*url.URL{}}
b.WriteString(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>command stations</title>
</head>
<body>
<ul>
`)
for name, cs := range s.csMap {
link := &url.URL{Path: fmt.Sprintf("/cs/%s", name)}
fmt.Fprintf(&b, "<div><a href='%s'>%s</a></div>\n", link, html.EscapeString(name))
data.Title = "command stations"
for name := range s.csMap {
data.Items[name] = &url.URL{Scheme: "http", Host: addr, Path: fmt.Sprintf("/cs/%s", name)}
}
b.WriteString(`<li>primary locos
w.Header().Set("Access-Control-Allow-Origin", "*")
if err := idxTpl.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
`)
")
fmt.Fprintf(&b, "<ul><a href='%s'>%s</a></div>\n", link, html.EscapeString(name))
renderLocos("primary locos", cs.filterLocos(func(loco *Loco) bool { return loco.isPrimary(cs) }))
renderLocos("secondary locos", cs.filterLocos(func(loco *Loco) bool { return loco.isSecondary(cs) }))
}
b.WriteString(`</ul>
</body>
</html>`)
_, err := w.Write(b.Bytes())
return err
}
*/

// ServeHTTP implements the http.Handler interface.
func (s *CSSet) ServeHTTP(w http.ResponseWriter, r *http.Request) {
data := csTplData{CSMap: map[string]csTpl{}}
for name, cs := range s.csMap {
data.CSMap[name] = csTpl{
Primaries: cs.filterLocos(func(loco *Loco) bool { return loco.isPrimary(cs) }),
Secondaries: cs.filterLocos(func(loco *Loco) bool { return loco.isSecondary(cs) }),
}
}

w.Header().Set("Access-Control-Allow-Origin", "*")
if err := csIdxTpl.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
}

// A CS represents a command station.
Expand Down Expand Up @@ -132,6 +183,17 @@ func newCS(lg logger.Logger, config *CSConfig, gw *gateway.Gateway) (*CS, error)

func (cs *CS) name() string { return cs.config.Name }

// filterLocos returns a map of locos filtered by function filter.
func (cs *CS) filterLocos(filter func(loco *Loco) bool) map[string]*Loco {
locos := map[string]*Loco{}
for name, loco := range cs.locos {
if filter(loco) {
locos[name] = loco
}
}
return locos
}

// close closes the command station and the underlying client connection.
func (cs *CS) close() error {
cs.lg.Printf("close command station %s", cs.name())
Expand Down
16 changes: 16 additions & 0 deletions internal/devices/devices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Package devices provides the pico-cs device and configuration types.
package devices

import (
"net/http"
"strings"
)

// ident for json marshalling.
var indent = strings.Repeat(" ", 4)

// HTTPHandler is a anlder function providing the main html index for the devices.
func HTTPHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write([]byte(idxHTML))
}
2 changes: 0 additions & 2 deletions internal/devices/doc.go

This file was deleted.

Loading

0 comments on commit c1b94aa

Please sign in to comment.