Skip to content

Commit c1b94aa

Browse files
committed
mqtt-gateway v0.7.2
1 parent e77fc54 commit c1b94aa

File tree

11 files changed

+277
-75
lines changed

11 files changed

+277
-75
lines changed

Dockerfile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
## build Go executable
2+
FROM golang:1.19-alpine as go-build
3+
4+
## copy entire local folder to container root directory
5+
COPY ./ /mqtt-gateway/
6+
7+
WORKDIR /mqtt-gateway/cmd/gateway
8+
9+
## build
10+
RUN go build -v
11+
12+
## deploy
13+
FROM alpine:3.17.0
14+
15+
## copy executable from go-build container
16+
COPY --from=go-build /mqtt-gateway/cmd/gateway/gateway /app/gateway
17+
18+
## http port
19+
EXPOSE 50000
20+
21+
## entrypoint is gateway
22+
ENTRYPOINT ["/app/gateway", "-httpHost", ""]

README.md

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,25 @@ The resources on a micro controller are limited and following the [KISS priciple
2424
- client libraries provide an idiomatic way in their respective programming language to communicate with the command station
2525
- and finally this gateway provides MQTT integration (like future components might provide integrations to additional protocols)
2626

27+
## Precondition
28+
- a running MQTT broker like [Mosquitto](https://mosquitto.org/)
29+
- the gateway executable or docker container
30+
- command station and model locomotive configuration files
31+
2732
## Build
2833

29-
For building there is two options available:
34+
For building there are the following options available:
3035

3136
- [local build](#local): install Go environment and build on your local machine
37+
- [deploy as docker container](#docker): build and deploy as docker container
3238
- [docker build](https://github.com/pico-cs/docker-buld): no toolchain installation but a running docker environment on your local machine is required
3339

3440
### Local
35-
To build go-hdb you need to have
41+
42+
#### Build
43+
To build the pico-cs mqtt-gateway you need to have
3644
- a working Go environment of the [latest Go version](https://golang.org/dl/) and
37-
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed
45+
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed.
3846

3947
```
4048
git clone https://github.com/pico-cs/mqtt-gateway.git
@@ -48,20 +56,12 @@ Example building executable for Raspberry Pi on Raspberry Pi OS
4856
GOOS=linux GOARCH=arm GOARM=7 go build
4957
```
5058

51-
## Run the gateway
52-
53-
Preconditions:
54-
- a running MQTT broker like [Mosquitto](https://mosquitto.org/)
55-
- the gateway executable
56-
- command station and model locomotive configuration files
57-
58-
### Gateway
59-
59+
#### Run
6060
A list of all gateway parameters can be printed via:
6161
```
6262
./gateway -h
6363
```
64-
Execute gateway with MQTT host listening at address 10.10.10.42 (default port 1883):
64+
Execute gateway with MQTT broker listening at address 10.10.10.42 (default port 1883):
6565
```
6666
./gateway -host 10.10.10.42
6767
```
@@ -71,6 +71,39 @@ Execute gateway reading configurations files stored in directory /pico-cs/config
7171
./gateway -configDir /pico-cs/config
7272
```
7373

74+
### Docker
75+
To build and run the pico-cs mqtt-gateway as docker container you need to have
76+
- a running [docker](https://docs.docker.com/engine/install/) environment and
77+
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed
78+
79+
#### Build
80+
```
81+
git clone https://github.com/pico-cs/mqtt-gateway.git
82+
cd mqtt-gateway
83+
docker build --tag pico-cs/mqtt-gateway .
84+
```
85+
86+
#### Run
87+
A list of all gateway parameters can be printed via:
88+
```
89+
docker run -it pico-cs/mqtt-gateway -h
90+
```
91+
92+
Execute container with
93+
- command station (pico) is connected to /dev/ttyACM0
94+
- MQTT broker listening at address 10.10.10.42 (default port 1883)
95+
- mqtt-gateway http endpoint (default 50000) should be made available on same host port 50000
96+
- run the container in detached mode
97+
98+
```
99+
docker run -d --device /dev/ttyACM0 -p 50000:50000 pico-cs/mqtt-gateway -mqttHost='10.10.10.42'
100+
```
101+
102+
Execute container in interactive mode:
103+
```
104+
docker run -it --device /dev/ttyACM0 -p 50000:50000 pico-cs/mqtt-gateway -mqttHost='10.10.10.42'
105+
```
106+
74107
### Configuration files
75108
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.
76109

cmd/gateway/gateway.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,17 @@ func (c *config) parseYaml(b []byte) error {
9191

9292
switch typ {
9393
case devices.CtCS:
94-
var csConfig devices.CSConfig
95-
if err := dd.Decode(&csConfig); err != nil {
94+
csConfig := devices.NewCSConfig()
95+
if err := dd.Decode(csConfig); err != nil {
9696
return err
9797
}
98-
c.csConfigMap[csConfig.Name] = &csConfig
98+
c.csConfigMap[csConfig.Name] = csConfig
9999
case devices.CtLoco:
100-
var locoConfig devices.LocoConfig
101-
if err := dd.Decode(&locoConfig); err != nil {
100+
locoConfig := devices.NewLocoConfig()
101+
if err := dd.Decode(locoConfig); err != nil {
102102
return err
103103
}
104-
c.locoConfigMap[locoConfig.Name] = &locoConfig
104+
c.locoConfigMap[locoConfig.Name] = locoConfig
105105
default:
106106
return fmt.Errorf("invalid configuration %v", m)
107107
}
@@ -174,8 +174,9 @@ func (s *deviceSets) register(config *config) error {
174174
}
175175

176176
func (s *deviceSets) registerHTTP(server *server.Server) {
177-
server.HandleFunc("/cs", s.csSet.HandleFunc(server.Addr()))
178-
server.HandleFunc("/loco", s.locoSet.HandleFunc(server.Addr()))
177+
server.HandleFunc("/", devices.HTTPHandler)
178+
server.Handle("/cs", s.csSet)
179+
server.Handle("/loco", s.locoSet)
179180
for name, cs := range s.csSet.Items() {
180181
server.Handle(fmt.Sprintf("/cs/%s", name), cs)
181182
}

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ go 1.19
77

88
require (
99
github.com/eclipse/paho.mqtt.golang v1.4.2
10-
github.com/pico-cs/go-client v0.4.0
11-
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30
10+
github.com/pico-cs/go-client v0.4.1
11+
golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3
1212
gopkg.in/yaml.v3 v3.0.1
1313
)
1414

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2
66
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
77
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
88
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
9-
github.com/pico-cs/go-client v0.4.0 h1:HaiXi1EQzJyXNeMZ34HKITmYvrkdliYlmDuOZSCRMPM=
10-
github.com/pico-cs/go-client v0.4.0/go.mod h1:n639HlI0L283jlN5cCZsbpVr9NbB8l6NCGQsKO8AjdE=
9+
github.com/pico-cs/go-client v0.4.1 h1:oAlCYXL8XTq/FeXK8zey3kNbYeZEp+HWx2XbgyavyZM=
10+
github.com/pico-cs/go-client v0.4.1/go.mod h1:n639HlI0L283jlN5cCZsbpVr9NbB8l6NCGQsKO8AjdE=
1111
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1212
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
1313
go.bug.st/serial v1.5.0 h1:ThuUkHpOEmCVXxGEfpoExjQCS2WBVV4ZcUKVYInM9T4=
1414
go.bug.st/serial v1.5.0/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
1515
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
16-
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 h1:m9O6OTJ627iFnN2JIWfdqlZCzneRO6EEBsHXI25P8ws=
17-
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
16+
golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3 h1:fJwx88sMf5RXwDwziL0/Mn9Wqs+efMSo/RYcL+37W9c=
17+
golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
1818
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
1919
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
2020
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=

internal/devices/config.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ type Filter struct {
7676
Excls []string `json:"excls"`
7777
}
7878

79+
// NewFilter returns a new Filter instance.
80+
func NewFilter() *Filter {
81+
return &Filter{Incls: []string{}, Excls: []string{}}
82+
}
83+
7984
func (f *Filter) filter() (*filter, error) { return newFilter(f.Incls, f.Excls) }
8085

8186
// CSIOConfig represents configuration data for a command station IO.
@@ -93,13 +98,22 @@ type CSConfig struct {
9398
// TCP/IP port (WiFi) or serial port (serial over USB)
9499
Port string `json:"port"`
95100
// filter of devices for which this command station should be a primary device
96-
Primary Filter `json:"primary"`
101+
Primary *Filter `json:"primary"`
97102
// filter of devices for which this command station should be a secondary device
98-
Secondary Filter `json:"secondary"`
103+
Secondary *Filter `json:"secondary"`
99104
// command station IO mapping (key is used in topic)
100105
IOs map[string]CSIOConfig `json:"ios"`
101106
}
102107

108+
// NewCSConfig returns a new CSConfig instance.
109+
func NewCSConfig() *CSConfig {
110+
return &CSConfig{
111+
Primary: NewFilter(),
112+
Secondary: NewFilter(),
113+
IOs: map[string]CSIOConfig{},
114+
}
115+
}
116+
103117
func (c *CSConfig) validate() error {
104118
if err := gateway.CheckLevelName(c.Name); err != nil {
105119
return fmt.Errorf("CSConfig name %s: %s", c.Name, err)
@@ -131,6 +145,11 @@ type LocoConfig struct {
131145
Fcts map[string]LocoFctConfig `json:"fcts"`
132146
}
133147

148+
// NewLocoConfig returns a new LocoConfig instance.
149+
func NewLocoConfig() *LocoConfig {
150+
return &LocoConfig{Fcts: map[string]LocoFctConfig{}}
151+
}
152+
134153
// ReservedFctNames is the list of reserved function names which cannot be used in loco configurations.
135154
var ReservedFctNames = []string{"dir", "speed"}
136155

internal/devices/cs.go

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"encoding/json"
55
"fmt"
66
"net/http"
7-
"net/url"
87
"sync"
98

109
"github.com/pico-cs/go-client/client"
@@ -52,27 +51,79 @@ func (s *CSSet) Close() error {
5251
return lastErr
5352
}
5453

55-
// HandleFunc returns a http.HandleFunc handler.
56-
func (s *CSSet) HandleFunc(addr string) func(w http.ResponseWriter, r *http.Request) {
57-
return func(w http.ResponseWriter, r *http.Request) {
58-
type tpldata struct {
59-
Title string
60-
Items map[string]*url.URL
54+
/*
55+
func (s *CSSet) idxTplExecute(w io.Writer) error {
56+
var b bytes.Buffer
57+
58+
renderLocos := func(title string, locos []*Loco) {
59+
if len(locos) > 0 {
60+
b.WriteString(`<!DOCTYPE html>
61+
<html>
62+
<head>
63+
<meta charset="UTF-8">
64+
<title>command stations</title>
65+
</head>
66+
<body>
67+
`)
68+
69+
for _, loco := range locos {
70+
link := &url.URL{Path: fmt.Sprintf("/loco/%s", loco.name())}
71+
fmt.Fprintf(&b, "<div><a href='%s'>%s</a></div>\n", link, html.EscapeString(loco.name()))
72+
}
6173
}
74+
}
6275
63-
data := &tpldata{Items: map[string]*url.URL{}}
76+
b.WriteString(`<!DOCTYPE html>
77+
<html>
78+
<head>
79+
<meta charset="UTF-8">
80+
<title>command stations</title>
81+
</head>
82+
<body>
83+
<ul>
84+
`)
85+
for name, cs := range s.csMap {
86+
link := &url.URL{Path: fmt.Sprintf("/cs/%s", name)}
87+
fmt.Fprintf(&b, "<div><a href='%s'>%s</a></div>\n", link, html.EscapeString(name))
6488
65-
data.Title = "command stations"
66-
for name := range s.csMap {
67-
data.Items[name] = &url.URL{Scheme: "http", Host: addr, Path: fmt.Sprintf("/cs/%s", name)}
68-
}
89+
b.WriteString(`<li>primary locos
6990
70-
w.Header().Set("Access-Control-Allow-Origin", "*")
71-
if err := idxTpl.Execute(w, data); err != nil {
72-
http.Error(w, err.Error(), http.StatusNotFound)
73-
return
91+
`)
92+
93+
94+
95+
96+
")
97+
fmt.Fprintf(&b, "<ul><a href='%s'>%s</a></div>\n", link, html.EscapeString(name))
98+
99+
100+
101+
renderLocos("primary locos", cs.filterLocos(func(loco *Loco) bool { return loco.isPrimary(cs) }))
102+
renderLocos("secondary locos", cs.filterLocos(func(loco *Loco) bool { return loco.isSecondary(cs) }))
103+
}
104+
b.WriteString(`</ul>
105+
</body>
106+
</html>`)
107+
_, err := w.Write(b.Bytes())
108+
return err
109+
}
110+
*/
111+
112+
// ServeHTTP implements the http.Handler interface.
113+
func (s *CSSet) ServeHTTP(w http.ResponseWriter, r *http.Request) {
114+
data := csTplData{CSMap: map[string]csTpl{}}
115+
for name, cs := range s.csMap {
116+
data.CSMap[name] = csTpl{
117+
Primaries: cs.filterLocos(func(loco *Loco) bool { return loco.isPrimary(cs) }),
118+
Secondaries: cs.filterLocos(func(loco *Loco) bool { return loco.isSecondary(cs) }),
74119
}
75120
}
121+
122+
w.Header().Set("Access-Control-Allow-Origin", "*")
123+
if err := csIdxTpl.Execute(w, data); err != nil {
124+
http.Error(w, err.Error(), http.StatusNotFound)
125+
return
126+
}
76127
}
77128

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

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

186+
// filterLocos returns a map of locos filtered by function filter.
187+
func (cs *CS) filterLocos(filter func(loco *Loco) bool) map[string]*Loco {
188+
locos := map[string]*Loco{}
189+
for name, loco := range cs.locos {
190+
if filter(loco) {
191+
locos[name] = loco
192+
}
193+
}
194+
return locos
195+
}
196+
135197
// close closes the command station and the underlying client connection.
136198
func (cs *CS) close() error {
137199
cs.lg.Printf("close command station %s", cs.name())

internal/devices/devices.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Package devices provides the pico-cs device and configuration types.
2+
package devices
3+
4+
import (
5+
"net/http"
6+
"strings"
7+
)
8+
9+
// ident for json marshalling.
10+
var indent = strings.Repeat(" ", 4)
11+
12+
// HTTPHandler is a anlder function providing the main html index for the devices.
13+
func HTTPHandler(w http.ResponseWriter, r *http.Request) {
14+
w.Header().Set("Access-Control-Allow-Origin", "*")
15+
w.Write([]byte(idxHTML))
16+
}

internal/devices/doc.go

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)