1
1
// Package gateway provides a pico-cs MQTT broker gateway.
2
2
package gateway
3
3
4
+ // use paho mqtt 3.1 broker instead the mqtt 5 version github.com/eclipse/paho.golang/paho
5
+ // because couldn't get the retain message handling work properly which is an essential part
6
+ // of this gateway
7
+
4
8
import (
5
- "context"
6
9
"encoding/json"
7
10
"fmt"
8
11
"io"
9
12
"log"
10
- "net/url"
11
13
"sync"
12
- "time"
13
14
14
- "github.com/eclipse/paho.golang/autopaho"
15
- "github.com/eclipse/paho.golang/paho"
15
+ MQTT "github.com/eclipse/paho.mqtt.golang"
16
16
"golang.org/x/exp/maps"
17
17
)
18
18
@@ -21,17 +21,19 @@ const defChanSize = 100
21
21
type hndFn func (payload any ) (any , error )
22
22
23
23
type pubMsg struct {
24
- topic string
25
- value any
24
+ topic string
25
+ retain bool
26
+ value any
26
27
}
27
28
28
29
type errMsg struct {
29
- topic string
30
- err error
30
+ topic string
31
+ retain bool
32
+ err error
31
33
}
32
34
33
35
type hndMsg struct {
34
- topic topic
36
+ topic string
35
37
fn hndFn
36
38
value any
37
39
}
@@ -44,8 +46,10 @@ type subscription struct {
44
46
45
47
// Gateway represents a MQTT broker gateway.
46
48
type Gateway struct {
47
- config * Config
48
- connectionManager * autopaho.ConnectionManager
49
+ config * Config
50
+ client MQTT.Client
51
+
52
+ listening bool
49
53
50
54
mu sync.RWMutex
51
55
csMap map [string ]* CS
@@ -88,35 +92,24 @@ func New(config *Config) (*Gateway, error) {
88
92
wg : new (sync.WaitGroup ),
89
93
}
90
94
91
- pahoConfig := autopaho.ClientConfig {
92
- BrokerUrls : []* url.URL {{Scheme : "tcp" , Host : config .address ()}},
93
- OnConnectError : func (err error ) { log .Println (err ) },
94
- ClientConfig : paho.ClientConfig {
95
- Router : paho .NewSingleHandlerRouter (gw .handler ),
96
- },
97
- }
98
-
99
- pahoConfig .SetUsernamePassword (config .Username , []byte (config .Password ))
100
-
101
- connectionManager , err := autopaho .NewConnection (context .Background (), pahoConfig )
102
- //cancel()
103
- if err != nil {
104
- return nil , err
95
+ // MQTT:
96
+ // starting with a clean seesion without client id as receiving
97
+ // retained messages should be enough initializing the
98
+ // command stations
99
+ opts := MQTT .NewClientOptions ()
100
+ opts .AddBroker (config .address ())
101
+ opts .SetUsername (config .Username )
102
+ opts .SetPassword (config .Password )
103
+ opts .SetAutoReconnect (true )
104
+ opts .SetCleanSession (true )
105
+ opts .SetDefaultPublishHandler (gw .handler )
106
+
107
+ client := MQTT .NewClient (opts )
108
+ if token := client .Connect (); token .Wait () && token .Error () != nil {
109
+ return nil , token .Error ()
105
110
}
111
+ gw .client = client
106
112
107
- gw .connectionManager = connectionManager
108
-
109
- // don't wait forever in case of connection issues like invalid host or port.
110
- ctx , cancel := context .WithTimeout (context .Background (), 20 * time .Second )
111
- defer cancel ()
112
-
113
- if err := connectionManager .AwaitConnection (ctx ); err != nil {
114
- return nil , err
115
- }
116
-
117
- if err := gw .subscribeBroker (); err != nil {
118
- return nil , err
119
- }
120
113
gw .logger .Printf ("connected to broker %s" , config .address ())
121
114
122
115
// start go routines
@@ -126,6 +119,11 @@ func New(config *Config) (*Gateway, error) {
126
119
return gw , nil
127
120
}
128
121
122
+ const (
123
+ defaultQoS = 1
124
+ wait = 250 // waiting time for client disconnect in ms
125
+ )
126
+
129
127
// Close closes the gateway and the MQTT connection.
130
128
func (gw * Gateway ) Close () error {
131
129
gw .mu .RLock ()
@@ -147,8 +145,20 @@ func (gw *Gateway) Close() error {
147
145
gw .wg .Wait ()
148
146
gw .logger .Printf ("disconnect from broker %s" , gw .config .address ())
149
147
gw .unsubscribeBroker () // ignore error
148
+ gw .client .Disconnect (wait )
149
+ return nil
150
+ }
150
151
151
- return gw .connectionManager .Disconnect (context .Background ())
152
+ // Listen starts the gateway listening to subscriptions.
153
+ func (gw * Gateway ) Listen () error {
154
+ // separated to start listen after subscriptions not to miss retained messages
155
+ gw .mu .Lock ()
156
+ defer gw .mu .Unlock ()
157
+ if gw .listening {
158
+ return fmt .Errorf ("gateway is already listening" )
159
+ }
160
+ // subscribe
161
+ return gw .subscribeBroker ()
152
162
}
153
163
154
164
// CSList returns the list of command stations assigned to this gateway.
@@ -166,25 +176,15 @@ func (gw *Gateway) LocoList() []*Loco {
166
176
}
167
177
168
178
func (gw * Gateway ) subscribeBroker () error {
169
- sub := & paho.Subscribe {
170
- Subscriptions : map [string ]paho.SubscribeOptions {
171
- gw .subTopic : {QoS : 1 }, //QoS 1: at least once
172
- },
173
- }
174
- if suback , err := gw .connectionManager .Subscribe (context .Background (), sub ); err != nil {
175
- gw .logger .Printf ("subscribe suback %v error %s" , suback , err )
176
- return err
179
+ if token := gw .client .Subscribe (gw .subTopic , defaultQoS , gw .handler ); token .Wait () && token .Error () != nil {
180
+ return token .Error ()
177
181
}
178
182
return nil
179
183
}
180
184
181
185
func (gw * Gateway ) unsubscribeBroker () error {
182
- unsub := & paho.Unsubscribe {
183
- Topics : []string {gw .subTopic },
184
- }
185
- if unsuback , err := gw .connectionManager .Unsubscribe (context .Background (), unsub ); err != nil {
186
- gw .logger .Printf ("unsubscribe unsuback %v error %s" , unsuback , err )
187
- return err
186
+ if token := gw .client .Unsubscribe (gw .subTopic ); token .Wait () && token .Error () != nil {
187
+ return token .Error ()
188
188
}
189
189
return nil
190
190
}
@@ -238,13 +238,21 @@ func (gw *Gateway) unsubscribe(owner any, topic string) {
238
238
}
239
239
}
240
240
241
- func (gw * Gateway ) handler (p * paho. Publish ) {
242
- topic , err := parseTopic (p .Topic )
241
+ func (gw * Gateway ) handler (client MQTT. Client , msg MQTT. Message ) {
242
+ topic , err := parseTopic (msg .Topic () )
243
243
if err != nil {
244
- gw .errCh <- & errMsg {topic : p .Topic , err : err }
244
+ gw .errCh <- & errMsg {topic : msg .Topic () , err : err }
245
245
return
246
246
}
247
247
248
+ var value any
249
+ if err := json .Unmarshal (msg .Payload (), & value ); err != nil {
250
+ gw .errCh <- & errMsg {topic : msg .Topic (), err : err }
251
+ return
252
+ }
253
+
254
+ gw .logger .Printf ("receive: topic %s retained %t value %v\n " , msg .Topic (), msg .Retained (), value )
255
+
248
256
gw .subMu .RLock ()
249
257
defer gw .subMu .RUnlock ()
250
258
@@ -253,16 +261,8 @@ func (gw *Gateway) handler(p *paho.Publish) {
253
261
return // nothing to do
254
262
}
255
263
256
- var value any
257
- if err := json .Unmarshal (p .Payload , & value ); err != nil {
258
- gw .errCh <- & errMsg {topic : p .Topic , err : err }
259
- return
260
- }
261
-
262
- // log.Printf("unmarshall payload %[1]v %[1]s value %[2]T %[2]v\n", p.Payload, payload)
263
-
264
264
for _ , subscription := range subscriptions {
265
- subscription .hndCh <- & hndMsg {topic : topic , fn : subscription .fn , value : value }
265
+ subscription .hndCh <- & hndMsg {topic : msg . Topic () , fn : subscription .fn , value : value }
266
266
}
267
267
}
268
268
@@ -275,23 +275,17 @@ func (gw *Gateway) publish(wg *sync.WaitGroup, pubCh <-chan *pubMsg, errCh chan<
275
275
continue // nothing to publish
276
276
}
277
277
278
- gw .logger .Printf ("publish: topic %s value %v" , msg .topic , msg .value )
278
+ gw .logger .Printf ("publish: topic %s retain %t value %v\n " , msg .topic , msg . retain , msg .value )
279
279
280
280
payload , err := json .Marshal (msg .value )
281
281
if err != nil {
282
282
errCh <- & errMsg {topic : msg .topic , err : err }
283
283
continue
284
284
}
285
285
286
- publish := & paho.Publish {
287
- QoS : 1 , // QoS == 1
288
- Retain : true , // retain msg, so that new joiners will get the latest message
289
- Topic : msg .topic ,
290
- Payload : payload ,
291
- }
292
-
293
- if _ , err := gw .connectionManager .Publish (context .Background (), publish ); err != nil {
294
- errCh <- & errMsg {topic : msg .topic , err : err }
286
+ token := gw .client .Publish (msg .topic , defaultQoS , msg .retain , payload )
287
+ if token .Wait () && token .Error () != nil {
288
+ errCh <- & errMsg {topic : msg .topic , err : token .Error ()}
295
289
}
296
290
}
297
291
}
@@ -307,24 +301,18 @@ func (gw *Gateway) publishError(wg *sync.WaitGroup, errCh <-chan *errMsg) {
307
301
308
302
for msg := range errCh {
309
303
310
- gw .logger .Printf ("publish error: %s" , msg .err )
304
+ gw .logger .Printf ("publish: topic %s retain %t error %s\n " , msg . topic , msg . retain , msg .err )
311
305
312
306
payload , err := json .Marshal (& errPayload {Topic : msg .topic , Error : msg .err .Error ()})
313
307
if err != nil {
314
308
// hm, we can only log...
315
309
gw .logger .Printf ("publish error: topic %s err %s" , msg .topic , err )
316
310
}
317
311
318
- publish := & paho.Publish {
319
- QoS : 1 , // QoS == 1
320
- Retain : false ,
321
- Topic : gw .errorTopic ,
322
- Payload : payload ,
323
- }
324
-
325
- if _ , err := gw .connectionManager .Publish (context .Background (), publish ); err != nil {
312
+ token := gw .client .Publish (gw .errorTopic , defaultQoS , msg .retain , payload )
313
+ if token .Wait () && token .Error () != nil {
326
314
// hm, we can only log...
327
- gw .logger .Printf ("publish error: topic %s error %s" , msg .topic , err )
315
+ gw .logger .Printf ("publish error: topic %s err %s" , msg .topic , token . Error () )
328
316
}
329
317
}
330
318
}
0 commit comments