4
4
import socket
5
5
import time
6
6
import threading
7
+ from datetime import datetime , timedelta
7
8
8
9
from limitlessled import MIN_WAIT , REPS
9
- from limitlessled .group .rgbw import RgbwGroup , RGBW
10
+ from limitlessled .group .rgbw import RgbwGroup , RGBW , BRIDGE_LED
11
+ from limitlessled .group .rgbww import RgbwwGroup , RGBWW
10
12
from limitlessled .group .white import WhiteGroup , WHITE
11
13
12
14
13
- BRIDGE_PORT = 8899
14
- BRIDGE_VERSION = 5
15
- BRIDGE_SHORT_VERSION_MIN = 3
16
- BRIDGE_LONG_BYTE = 0x55
15
+ BRIDGE_PORT = 5987
16
+ BRIDGE_VERSION = 6
17
+ BRIDGE_LED_GROUP = 1
18
+ BRIDGE_LED_NAME = 'bridge'
17
19
SELECT_WAIT = 0.025
20
+ BRIDGE_INITIALIZATION_COMMAND = [0x20 , 0x00 , 0x00 , 0x00 , 0x16 , 0x02 , 0x62 ,
21
+ 0x3a , 0xd5 , 0xed , 0xa3 , 0x01 , 0xae , 0x08 ,
22
+ 0x2d , 0x46 , 0x61 , 0x41 , 0xa7 , 0xf6 , 0xdc ,
23
+ 0xaf , 0xfe , 0xf7 , 0x00 , 0x00 , 0x1e ]
24
+ KEEP_ALIVE_COMMAND_PREAMBLE = [0xD0 , 0x00 , 0x00 , 0x00 , 0x02 ]
25
+ KEEP_ALIVE_RESPONSE_PREAMBLE = [0xd8 , 0x0 , 0x0 , 0x0 , 0x07 ]
26
+ KEEP_ALIVE_TIME = 1
27
+ RECONNECT_TIME = 5
28
+ SOCKET_TIMEOUT = 5
29
+ STARTING_SEQUENTIAL_BYTE = 0x02
18
30
19
31
20
32
def group_factory (bridge , number , name , led_type ):
@@ -23,11 +35,13 @@ def group_factory(bridge, number, name, led_type):
23
35
:param bridge: Member of this bridge.
24
36
:param number: Group number (1-4).
25
37
:param name: Name of group.
26
- :param led_type: Either `RGBW` or `WHITE`.
38
+ :param led_type: Either `RGBW`, `RGBWW`, `WHITE` or `BRIDGE_LED `.
27
39
:returns: New group.
28
40
"""
29
- if led_type == RGBW :
30
- return RgbwGroup (bridge , number , name )
41
+ if led_type in [RGBW , BRIDGE_LED ]:
42
+ return RgbwGroup (bridge , number , name , led_type )
43
+ elif led_type == RGBWW :
44
+ return RgbwwGroup (bridge , number , name )
31
45
elif led_type == WHITE :
32
46
return WhiteGroup (bridge , number , name )
33
47
else :
@@ -37,34 +51,78 @@ def group_factory(bridge, number, name, led_type):
37
51
class Bridge (object ):
38
52
""" Represents a LimitlessLED bridge. """
39
53
40
- def __init__ (self , ip , port = BRIDGE_PORT , version = BRIDGE_VERSION ):
54
+ def __init__ (self , ip , port = BRIDGE_PORT , version = BRIDGE_VERSION ,
55
+ bridge_led_name = BRIDGE_LED_NAME ):
41
56
""" Initialize bridge.
42
57
43
- Bridge version 3 through 5 (latest as of this release)
58
+ Bridge version 6 (latest as of this release)
44
59
can use the default parameters. For lower versions,
45
- use port 50000. Lower versions also require sending a
46
- larger payload to the bridge (slower).
60
+ use port 8899 (3 to 5) or 50000 (lower then 3).
61
+ Lower versions also require sending a larger payload
62
+ to the bridge (slower).
47
63
48
64
:param ip: IP address of bridge.
49
65
:param port: Bridge port.
50
66
:param version: Bridge version.
67
+ :param bridge_led_name: Name of the bridge led group.
51
68
"""
69
+ self .is_closed = False
52
70
self .wait = MIN_WAIT
53
71
self .reps = REPS
54
72
self .groups = []
55
73
self .ip = ip
56
74
self .version = version
75
+ self ._sn = STARTING_SEQUENTIAL_BYTE
57
76
self ._socket = socket .socket (socket .AF_INET , socket .SOCK_DGRAM )
77
+ self ._socket .settimeout (SOCKET_TIMEOUT )
58
78
self ._socket .connect ((ip , port ))
59
79
self ._command_queue = queue .Queue ()
60
80
self ._lock = threading .Lock ()
61
81
self .active = 0
62
82
self ._selected_number = None
83
+
63
84
# Start queue consumer thread.
64
85
consumer = threading .Thread (target = self ._consume )
65
86
consumer .daemon = True
66
87
consumer .start ()
67
88
89
+ # Version specific stuff
90
+ self ._wb1 = None
91
+ self ._wb2 = None
92
+ self ._bridge_led = None
93
+ if self .version >= 6 :
94
+ # Create bridge led group
95
+ self ._bridge_led = group_factory (self , BRIDGE_LED_GROUP ,
96
+ bridge_led_name , BRIDGE_LED )
97
+
98
+ # Initialize connection to retrieve bridge session ids (wb1, wb2)
99
+ self ._init_connection ()
100
+
101
+ # Start keep alive thread.
102
+ keep_alive_thread = threading .Thread (target = self ._keep_alive )
103
+ keep_alive_thread .daemon = True
104
+ keep_alive_thread .start ()
105
+
106
+ @property
107
+ def sn (self ):
108
+ """ Gets the current sequential byte. """
109
+ return self ._sn
110
+
111
+ @property
112
+ def wb1 (self ):
113
+ """ Gets the bridge session id 1. """
114
+ return self ._wb1
115
+
116
+ @property
117
+ def wb2 (self ):
118
+ """ Gets the bridge session id 2. """
119
+ return self ._wb2
120
+
121
+ @property
122
+ def bridge_led (self ):
123
+ """ Get the group to control the bridge led. """
124
+ return self ._bridge_led
125
+
68
126
def incr_active (self ):
69
127
""" Increment number of active groups. """
70
128
with self ._lock :
@@ -87,21 +145,19 @@ def add_group(self, number, name, led_type):
87
145
self .groups .append (group )
88
146
return group
89
147
90
- def send (self , group , command , reps = REPS , wait = MIN_WAIT , select = False ):
148
+ def send (self , command , reps = REPS , wait = MIN_WAIT ):
91
149
""" Send a command to the physical bridge.
92
150
93
- :param group: Run on this group.
94
- :param command: A bytearray.
151
+ :param command: A Command instance.
95
152
:param reps: Number of repetitions.
96
153
:param wait: Wait time in seconds.
97
- :param select: Select group if necessary.
98
154
"""
99
155
# Enqueue the command.
100
- self ._command_queue .put ((group , command , reps , wait , select ))
156
+ self ._command_queue .put ((command , reps , wait ))
101
157
# Wait before accepting another command.
102
- # This keeps indvidual groups relatively synchronized.
158
+ # This keeps individual groups relatively synchronized.
103
159
sleep = reps * wait * self .active
104
- if select and self ._selected_number != group . number :
160
+ if command . select and self ._selected_number != command . group_number :
105
161
sleep += SELECT_WAIT
106
162
time .sleep (sleep )
107
163
@@ -118,17 +174,83 @@ def _consume(self):
118
174
119
175
TODO: Only wait when another command comes in.
120
176
"""
121
- while True :
177
+ while not self . is_closed :
122
178
# Get command from queue.
123
- (group , command , reps , wait , select ) = self ._command_queue .get ()
179
+ (command , reps , wait ) = self ._command_queue .get ()
124
180
# Select group if a different group is currently selected.
125
- if select and self ._selected_number != group . number :
126
- self ._socket . send ( bytearray ( group . get_select_cmd ()) )
181
+ if command . select and self ._selected_number != command . group_number :
182
+ self ._send_raw ( command . select_command . bytes )
127
183
time .sleep (SELECT_WAIT )
128
184
# Repeat command as necessary.
129
185
for _ in range (reps ):
130
- if self .version < BRIDGE_SHORT_VERSION_MIN :
131
- command .append (BRIDGE_LONG_BYTE )
132
- self ._socket .send (bytearray (command ))
186
+ self ._send_raw (command .bytes )
133
187
time .sleep (wait )
134
- self ._selected_number = group .number
188
+ self ._selected_number = command .group_number
189
+
190
+ def _send_raw (self , command ):
191
+ """
192
+ Sends an raw command directly to the physical bridge.
193
+ :param command: A bytearray.
194
+ """
195
+ self ._socket .send (bytearray (command ))
196
+ self ._sn = (self ._sn + 1 ) % 256
197
+
198
+ def _init_connection (self ):
199
+ """
200
+ Requests the session ids of the bridge.
201
+ :returns: True, if initialization was successful. False, otherwise.
202
+ """
203
+ try :
204
+ response = bytearray (22 )
205
+ self ._send_raw (BRIDGE_INITIALIZATION_COMMAND )
206
+ self ._socket .recv_into (response )
207
+ self ._wb1 = response [19 ]
208
+ self ._wb2 = response [20 ]
209
+ except socket .timeout :
210
+ return False
211
+
212
+ return True
213
+
214
+ def _reconnect (self ):
215
+ """
216
+ Try continuously to reconnect to the bridge.
217
+ """
218
+ while not self .is_closed :
219
+ if self ._init_connection ():
220
+ return
221
+
222
+ time .sleep (RECONNECT_TIME )
223
+
224
+ def _keep_alive (self ):
225
+ """
226
+ Send keep alive messages continuously to bridge.
227
+ """
228
+ while not self .is_closed :
229
+ command = KEEP_ALIVE_COMMAND_PREAMBLE + [self .wb1 , self .wb2 ]
230
+ self ._send_raw (command )
231
+
232
+ start = datetime .now ()
233
+ connection_alive = False
234
+ while datetime .now () - start < timedelta (seconds = SOCKET_TIMEOUT ):
235
+ response = bytearray (12 )
236
+ try :
237
+ self ._socket .recv_into (response )
238
+ except socket .timeout :
239
+ break
240
+
241
+ if response [:5 ] == bytearray (KEEP_ALIVE_RESPONSE_PREAMBLE ):
242
+ connection_alive = True
243
+ break
244
+
245
+ if not connection_alive :
246
+ self ._reconnect ()
247
+ continue
248
+
249
+ time .sleep (KEEP_ALIVE_TIME )
250
+
251
+ def close (self ):
252
+ """
253
+ Closes the connection to the bridge.
254
+ """
255
+ self .is_closed = True
256
+
0 commit comments