Skip to content

Commit d5dae45

Browse files
committed
mqtt-gateway v0.4.0
1 parent 5d883bf commit d5dae45

File tree

13 files changed

+386
-200
lines changed

13 files changed

+386
-200
lines changed

README.md

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -57,54 +57,54 @@ Preconditions:
5757

5858
### Gateway
5959

60-
Print help (parameters):
61-
60+
A list of all gateway parameters can be printed via:
6261
```
6362
./gateway -h
64-
65-
Usage of ./gateway:
66-
-configDir string
67-
configuration directory
68-
-host string
69-
MQTT host (default "localhost")
70-
-password string
71-
password
72-
-port string
73-
MQTT port (default "1883")
74-
-topicRoot string
75-
topic root (default "pico-cs")
76-
-username string
77-
user name
7863
```
64+
Execute gateway with MQTT host listening at address 10.10.10.42 (default port 1883):
65+
```
66+
./gateway -host 10.10.10.42
67+
```
68+
Execute gateway reading configurations files stored in directory /pico-cs/config
7969

80-
...todo
70+
```
71+
./gateway -configDir /pico-cs/config
72+
```
8173

8274
### Configuration files
83-
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 an object is found more than once the last one wins.
75+
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.
8476

85-
Each object needs to define a name. As the object name is part of the [MQTT topic](#mqtt-topics) it must fullfil the following conditions:
77+
Each device needs to define a name. As the device name is part of the [MQTT topic](#mqtt-topics) it must fullfil the following conditions:
8678
- consist of valid MQTT topic characters and
8779
- must not contain characters "/", "+" or "#"
8880

89-
All conditions are checked by the gateway on start and in case of a violation the program prints the respective error message and stops execution.
81+
If a device is assigned to a command station the command station acts whether as a primary or secondary.
82+
A primary command stations 'owns' the device, meaning that the command station registers for all commands of the device.
83+
A secondary command station listens and registers the events 'send' by the device and executes the correspondig commands to keep the device settings in sync with the primary command station.
84+
A device can be assigned to 0..1 primary command stations and 0..* secondary command stations.
9085

9186
### Embedded configuration files
9287
Beside using a configuration directory the configuration files can be embedded in the gateway executable:
9388
- store them in as part of the source code directory at mqtt-gateway/cmd/gateway/config and
9489
- build the binary
9590

96-
This is the prefered method using a static or default configuration. During the gateway start the embedded configuration files are scanned before the 'external' configuration files (configDir parameter), so an external object configuration would overwrite an embedded one.
91+
This is the prefered method using a static or default configuration. During the gateway start the embedded configuration files are scanned before the 'external' configuration files (configDir parameter), so an external device configuration would overwrite an embedded one.
9792

9893
### [Configuration examples](https://github.com/pico-cs/mqtt-gateway/tree/main/cmd/gateway/config_examples/)
9994

10095
## MQTT topics
10196
In MQTT a topic is a string the MQTT broker uses to determine which messages should be send to each of the connected clients. The client uses a topic to publish a message and uses topics to subscribe to messages it would like to receive from the broker.
10297
A topic can consist of more than one level - the character used to separate levels is '/'. A client might use wildcards when subscribing to topics, where '+' is the wildcard character for a dedicated level and '#' is the multi level wildcard character which can only be used as the last level of a topic. For further details about MQTT topics please see the [MQTT specification](https://mqtt.org/mqtt-specification/).
10398

104-
The topic schema used by the gateway:
99+
The topic schema used by the gateway is
100+
101+
```
102+
"<topic root>/<device type>/<device name>/<property>[/<command>]"
103+
```
105104

105+
with
106106
```
107-
"<topic root>/<object type>/<object name>/<property>[/<command>]"
107+
device type: cs | loco
108108
```
109109

110110
The message payload is whether a json encoded atomic field (aka string, number, boolean) or a json encoded object.

cmd/gateway/config.go

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ type configSet struct {
1919
logger *log.Logger
2020
csConfigMap map[string]*gateway.CSConfig
2121
locoConfigMap map[string]*gateway.LocoConfig
22-
csList []*gateway.CS
2322
}
2423

2524
func newConfigSet(logger *log.Logger) *configSet {
@@ -33,16 +32,6 @@ func newConfigSet(logger *log.Logger) *configSet {
3332
}
3433
}
3534

36-
func (c *configSet) close() error {
37-
var lastErr error
38-
for _, cs := range c.csList {
39-
if err := cs.Close(); err != nil {
40-
lastErr = err
41-
}
42-
}
43-
return lastErr
44-
}
45-
4635
func isCSConfig(m map[string]any) bool {
4736
if _, ok := m["host"]; ok {
4837
return true
@@ -130,37 +119,29 @@ func (c *configSet) load(fsys fs.FS, path string) error {
130119
}
131120

132121
func (c *configSet) register(gw *gateway.Gateway) error {
133-
locoMap := map[string]string{}
134-
135-
for csName, csConfig := range c.csConfigMap {
136-
c.logger.Printf("register central station %s", csName)
137-
cs, err := gateway.NewCS(csConfig, gw)
122+
for _, csConfig := range c.csConfigMap {
123+
cs, err := gw.AddCS(csConfig)
138124
if err != nil {
139125
return err
140126
}
141-
c.csList = append(c.csList, cs)
142-
143-
for locoName, locoConfig := range c.locoConfigMap {
144-
145-
csAssignedName, ok := locoMap[locoName]
146127

147-
controlsLoco, err := cs.AddLoco(locoConfig)
128+
for _, locoConfig := range c.locoConfigMap {
129+
loco, err := gw.AddLoco(locoConfig)
148130
if err != nil {
149131
return err
150132
}
151-
152-
if controlsLoco && ok {
153-
return fmt.Errorf("loco %s is controlled by more than one central station %s, %s", locoName, csName, csAssignedName)
133+
if err := cs.AddLoco(loco); err != nil {
134+
return err
154135
}
136+
}
137+
}
155138

156-
locoMap[locoName] = csName
139+
for _, cs := range gw.CSList() {
140+
c.logger.Printf("added command station %s", cs.Name())
141+
}
157142

158-
if controlsLoco {
159-
c.logger.Printf("added loco %s controlled by central station %s", locoConfig.Name, csConfig.Name)
160-
} else {
161-
c.logger.Printf("added loco %s to central station %s", locoConfig.Name, csConfig.Name)
162-
}
163-
}
143+
for _, loco := range gw.LocoList() {
144+
c.logger.Printf("added loco %s to central stations: primary %s secondaries %v", loco.Name(), loco.Primary(), loco.SecondaryList())
164145
}
165146
return nil
166147
}

cmd/gateway/config_examples/cs.yaml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# configure central station
22
name: cs01
33
port: /dev/ttyACM0 # connected to serial port
4-
incls:
5-
- .* # include all locos (regular expression)
6-
excls:
7-
- br18 # exclude br18
4+
primary:
5+
incls:
6+
- .* # primary command station for all devices (regular expression)...
7+
excls:
8+
- br18 # ...except br18
9+
secondary:
10+
incls:
11+
- .* # secondary command station for all remaining devices

cmd/gateway/config_examples/loco.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# configure loco
22
name: br01
3-
addr: 1
3+
addr: 1 # decoder address
44
fcts:
55
light:
6-
no: 0
6+
no: 0 # function number for light
77
horn:
8-
no: 5
8+
no: 5 # function number for horn

cmd/gateway/config_examples/multi.yaml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@
44
name: cs02
55
host: localhost # connected via WiFi TCP/IP connection
66
port: 4242
7-
incls:
8-
- br18
7+
primary:
8+
incls:
9+
- br18 # primary command station for br18
10+
secondary:
11+
incls:
12+
- .* # secondary command station for all remaining devices
913
---
1014
# configure loco
1115
name: br18
12-
addr: 18
16+
addr: 18 # decoder address
1317
fcts:
1418
light:
15-
no: 0
19+
no: 0 # function number for light
1620
bell:
17-
no: 5
21+
no: 5 # function number for bell
1822
whistle:
19-
no: 8
23+
no: 8 # function number for whistle
2024

2125

2226

cmd/gateway/config_test.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,41 @@ import (
1111
func testLoad(t *testing.T) {
1212
cmpCSConfigMap := map[string]*gateway.CSConfig{
1313
"cs01": {
14-
Name: "cs01",
15-
Port: "/dev/ttyACM0",
16-
Incls: []string{".*"},
17-
Excls: []string{"br18"},
14+
Name: "cs01",
15+
Port: "/dev/ttyACM0",
16+
Primary: gateway.Filter{
17+
Incls: []string{".*"},
18+
Excls: []string{"br18"},
19+
},
20+
Secondary: gateway.Filter{
21+
Incls: []string{".*"},
22+
},
1823
},
1924
"cs02": {
20-
Name: "cs02",
21-
Host: "localhost",
22-
Port: "4242",
23-
Incls: []string{"br18"},
25+
Name: "cs02",
26+
Host: "localhost",
27+
Port: "4242",
28+
Primary: gateway.Filter{
29+
Incls: []string{"br18"},
30+
},
31+
Secondary: gateway.Filter{
32+
Incls: []string{".*"},
33+
},
2434
},
2535
}
2636
cmpLocoConfigMap := map[string]*gateway.LocoConfig{
2737
"br01": {
2838
Name: "br01",
2939
Addr: 1,
30-
Fcts: map[string]gateway.LocoFctConfig{
40+
Fcts: map[string]gateway.LocoFct{
3141
"light": {No: 0},
3242
"horn": {No: 5},
3343
},
3444
},
3545
"br18": {
3646
Name: "br18",
3747
Addr: 18,
38-
Fcts: map[string]gateway.LocoFctConfig{
48+
Fcts: map[string]gateway.LocoFct{
3949
"light": {No: 0},
4050
"bell": {No: 5},
4151
"whistle": {No: 8},

cmd/gateway/gateway.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,16 @@ func lookupEnv(name, defVal string) string {
3535

3636
func main() {
3737

38-
logger := log.New(os.Stderr, "gateway", log.LstdFlags)
38+
logger := log.New(os.Stderr, "", log.LstdFlags)
3939

40-
config := &gateway.Config{}
40+
config := &gateway.Config{Logger: logger}
4141
configSet := newConfigSet(logger)
42-
defer configSet.close()
4342

44-
flag.StringVar(&config.TopicRoot, "topicRoot", lookupEnv(envTopicRoot, gateway.DefaultTopicRoot), "topic root")
43+
flag.StringVar(&config.TopicRoot, "topicRoot", lookupEnv(envTopicRoot, gateway.DefaultTopicRoot), "MQTT topic root")
4544
flag.StringVar(&config.Host, "host", lookupEnv(envHost, gateway.DefaultHost), "MQTT host")
4645
flag.StringVar(&config.Port, "port", lookupEnv(envPort, gateway.DefaultPort), "MQTT port")
47-
flag.StringVar(&config.Username, "username", lookupEnv(envUsername, ""), "user name")
48-
flag.StringVar(&config.Password, "password", lookupEnv(envPassword, ""), "password")
46+
flag.StringVar(&config.Username, "username", lookupEnv(envUsername, ""), "MQTT username")
47+
flag.StringVar(&config.Password, "password", lookupEnv(envPassword, ""), "MQTT password")
4948

5049
externConfigDir := flag.String("configDir", "", "configuration directory")
5150

0 commit comments

Comments
 (0)