44import socket
55import time
66import threading
7+ from datetime import datetime , timedelta
78
89from 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
1012from limitlessled .group .white import WhiteGroup , WHITE
1113
1214
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'
1719SELECT_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
1830
1931
2032def group_factory (bridge , number , name , led_type ):
@@ -23,11 +35,13 @@ def group_factory(bridge, number, name, led_type):
2335 :param bridge: Member of this bridge.
2436 :param number: Group number (1-4).
2537 :param name: Name of group.
26- :param led_type: Either `RGBW` or `WHITE`.
38+ :param led_type: Either `RGBW`, `RGBWW`, `WHITE` or `BRIDGE_LED `.
2739 :returns: New group.
2840 """
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 )
3145 elif led_type == WHITE :
3246 return WhiteGroup (bridge , number , name )
3347 else :
@@ -37,34 +51,78 @@ def group_factory(bridge, number, name, led_type):
3751class Bridge (object ):
3852 """ Represents a LimitlessLED bridge. """
3953
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 ):
4156 """ Initialize bridge.
4257
43- Bridge version 3 through 5 (latest as of this release)
58+ Bridge version 6 (latest as of this release)
4459 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).
4763
4864 :param ip: IP address of bridge.
4965 :param port: Bridge port.
5066 :param version: Bridge version.
67+ :param bridge_led_name: Name of the bridge led group.
5168 """
69+ self .is_closed = False
5270 self .wait = MIN_WAIT
5371 self .reps = REPS
5472 self .groups = []
5573 self .ip = ip
5674 self .version = version
75+ self ._sn = STARTING_SEQUENTIAL_BYTE
5776 self ._socket = socket .socket (socket .AF_INET , socket .SOCK_DGRAM )
77+ self ._socket .settimeout (SOCKET_TIMEOUT )
5878 self ._socket .connect ((ip , port ))
5979 self ._command_queue = queue .Queue ()
6080 self ._lock = threading .Lock ()
6181 self .active = 0
6282 self ._selected_number = None
83+
6384 # Start queue consumer thread.
6485 consumer = threading .Thread (target = self ._consume )
6586 consumer .daemon = True
6687 consumer .start ()
6788
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+
68126 def incr_active (self ):
69127 """ Increment number of active groups. """
70128 with self ._lock :
@@ -87,21 +145,19 @@ def add_group(self, number, name, led_type):
87145 self .groups .append (group )
88146 return group
89147
90- def send (self , group , command , reps = REPS , wait = MIN_WAIT , select = False ):
148+ def send (self , command , reps = REPS , wait = MIN_WAIT ):
91149 """ Send a command to the physical bridge.
92150
93- :param group: Run on this group.
94- :param command: A bytearray.
151+ :param command: A Command instance.
95152 :param reps: Number of repetitions.
96153 :param wait: Wait time in seconds.
97- :param select: Select group if necessary.
98154 """
99155 # Enqueue the command.
100- self ._command_queue .put ((group , command , reps , wait , select ))
156+ self ._command_queue .put ((command , reps , wait ))
101157 # Wait before accepting another command.
102- # This keeps indvidual groups relatively synchronized.
158+ # This keeps individual groups relatively synchronized.
103159 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 :
105161 sleep += SELECT_WAIT
106162 time .sleep (sleep )
107163
@@ -118,17 +174,83 @@ def _consume(self):
118174
119175 TODO: Only wait when another command comes in.
120176 """
121- while True :
177+ while not self . is_closed :
122178 # Get command from queue.
123- (group , command , reps , wait , select ) = self ._command_queue .get ()
179+ (command , reps , wait ) = self ._command_queue .get ()
124180 # 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 )
127183 time .sleep (SELECT_WAIT )
128184 # Repeat command as necessary.
129185 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 )
133187 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