Skip to content

Commit 56117d8

Browse files
2 parents 09beee9 + 105737c commit 56117d8

File tree

2 files changed

+70
-55
lines changed

2 files changed

+70
-55
lines changed

limitlessled/bridge.py

Lines changed: 69 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import queue
44
import socket
5+
import select
56
import time
67
import threading
78
from datetime import datetime, timedelta
@@ -25,6 +26,7 @@
2526
0xaf, 0xfe, 0xf7, 0x00, 0x00, 0x1e]
2627
KEEP_ALIVE_COMMAND_PREAMBLE = [0xD0, 0x00, 0x00, 0x00, 0x02]
2728
KEEP_ALIVE_RESPONSE_PREAMBLE = [0xd8, 0x0, 0x0, 0x0, 0x07]
29+
COMMAND_RESPONSE_PREAMBLE = [0x88, 0x00, 0x00, 0x00, 0x03, 0x00]
2830
KEEP_ALIVE_TIME = 5
2931
RECONNECT_TIME = 5
3032
SOCKET_TIMEOUT = 5
@@ -84,6 +86,7 @@ def __init__(self, ip, port=BRIDGE_PORT, version=BRIDGE_VERSION,
8486
self._socket.settimeout(SOCKET_TIMEOUT)
8587
self._socket.connect((ip, port))
8688
self._command_queue = queue.Queue()
89+
self._ack_queue = queue.Queue()
8790
self._lock = threading.Lock()
8891
self.active = 0
8992
self._selected_number = None
@@ -166,10 +169,11 @@ def send(self, command, reps=REPS, wait=MIN_WAIT):
166169
self._command_queue.put((command, reps, wait))
167170
# Wait before accepting another command.
168171
# This keeps individual groups relatively synchronized.
169-
sleep = reps * wait * self.active
170-
if command.select and self._selected_number != command.group_number:
171-
sleep += SELECT_WAIT
172-
time.sleep(sleep)
172+
if self.version < 6:
173+
sleep = reps * wait * self.active
174+
if command.select and self._selected_number != command.group_number:
175+
sleep += SELECT_WAIT
176+
time.sleep(sleep)
173177

174178
def _consume(self):
175179
""" Consume commands from the queue.
@@ -181,49 +185,54 @@ def _consume(self):
181185
be used by one thread at a time. Note that this can and
182186
will delay commands if multiple groups are attempting
183187
to communicate at the same time on the same bridge.
184-
185-
TODO: Only wait when another command comes in.
186188
"""
187189
while not self.is_closed:
190+
# Get command from queue.
191+
msg = self._command_queue.get()
192+
193+
# Closed
194+
if msg is None:
195+
return
196+
188197
# Use the lock so we are sure is_ready is not changed during execution
189198
# and the socket is not in use
190199
with self._lock:
191-
# Check if bridge is ready and there are
192-
if self.is_ready and not self._command_queue.empty():
193-
# Get command from queue.
194-
(command, reps, wait) = self._command_queue.get()
200+
# Check if bridge is ready
201+
if self.is_ready:
202+
(command, reps, wait) = msg
203+
195204
# Select group if a different group is currently selected.
196205
if command.select and self._selected_number != command.group_number:
197-
if not self._send_raw(command.select_command.get_bytes(self)):
206+
if self._send_raw(command.select_command.get_bytes(self)):
207+
self._selected_number = command.group_number
208+
time.sleep(SELECT_WAIT)
209+
else:
198210
# Stop sending on socket error
199211
self.is_ready = False
200-
continue
201212

202-
time.sleep(SELECT_WAIT)
203213
# Repeat command as necessary.
204-
for _ in range(reps):
205-
if not self._send_raw(command.get_bytes(self)):
214+
command_bytes = command.get_bytes(self)
215+
todo = reps
216+
while todo > 0 and self.is_ready:
217+
if self._send_raw(command_bytes):
218+
try:
219+
while self.sn != self._ack_queue.get(timeout=wait):
220+
pass
221+
222+
# ACK received, stop repeating
223+
todo = 0
224+
except queue.Empty:
225+
todo = todo - 1
226+
else:
206227
# Stop sending on socket error
207228
self.is_ready = False
208-
continue
209-
time.sleep(wait)
210-
211-
self._selected_number = command.group_number
212-
213-
# Wait a little time if queue is empty
214-
if self._command_queue.empty():
215-
time.sleep(MIN_WAIT)
216229

217230
# Wait if bridge is not ready, we're only reading is_ready, no lock needed
218-
if not self.is_ready:
219-
if self.is_closed:
220-
return
221-
222-
# Give the reconnect some time
223-
time.sleep(RECONNECT_TIME)
224-
231+
if not self.is_ready and not self.is_closed:
225232
# For older bridges, always try again, there's no keep-alive thread
226233
if self.version < 6:
234+
# Give the reconnect some time
235+
time.sleep(RECONNECT_TIME)
227236
self.is_ready = True
228237

229238
def _send_raw(self, command):
@@ -233,13 +242,19 @@ def _send_raw(self, command):
233242
"""
234243
try:
235244
self._socket.send(bytearray(command))
236-
self._sn = (self._sn + 1) % 256
237245
return True
238246
except (socket.error, socket.timeout):
239247
# We can get a socket.error or timeout exception if the bridge is disconnected,
240248
# but we are still sending data. In that case, return False to indicate that data is not sent.
241249
return False
242250

251+
def next_sn(self):
252+
"""
253+
Increases the sequential byte and returns it.
254+
"""
255+
self._sn = (self._sn + 1) % 256
256+
return self._sn
257+
243258
def _init_connection(self):
244259
"""
245260
Requests the session ids of the bridge.
@@ -276,42 +291,42 @@ def _reconnect(self):
276291

277292
def _keep_alive(self):
278293
"""
279-
Send keep alive messages continuously to bridge.
294+
Send keep alive messages continuously to bridge and
295+
handle command responses.
280296
"""
297+
send_next_keep_alive_at = 0
281298
while not self.is_closed:
282299
if not self.is_ready:
283300
self._reconnect()
284301
continue
285302

286-
# Acquire the lock to make sure we don't change self.is_ready
287-
# while _consume() is sending commands
288-
with self._lock:
303+
if time.monotonic() > send_next_keep_alive_at:
289304
command = KEEP_ALIVE_COMMAND_PREAMBLE + [self.wb1, self.wb2]
290305
self._send_raw(command)
291-
292-
start = datetime.now()
293-
connection_alive = False
294-
while datetime.now() - start < timedelta(seconds=SOCKET_TIMEOUT):
295-
response = bytearray(12)
296-
try:
297-
self._socket.recv_into(response)
298-
except socket.timeout:
299-
break
300-
301-
if response[:5] == bytearray(KEEP_ALIVE_RESPONSE_PREAMBLE):
302-
connection_alive = True
303-
break
304-
305-
if not connection_alive:
306+
need_response_by = time.monotonic() + KEEP_ALIVE_TIME
307+
308+
# Wait for responses
309+
timeout = max(0, need_response_by - time.monotonic())
310+
ready = select.select([self._socket], [], [], timeout)
311+
if ready[0]:
312+
response = bytearray(12)
313+
self._socket.recv_into(response)
314+
315+
if response.startswith(bytearray(KEEP_ALIVE_RESPONSE_PREAMBLE)):
316+
send_next_keep_alive_at = need_response_by
317+
elif response.startswith(bytearray(COMMAND_RESPONSE_PREAMBLE)):
318+
sn = response[len(COMMAND_RESPONSE_PREAMBLE)]
319+
self._ack_queue.put(sn)
320+
elif send_next_keep_alive_at < need_response_by:
321+
# Acquire the lock to make sure we don't change self.is_ready
322+
# while _consume() is sending commands
323+
with self._lock:
306324
self.is_ready = False
307325

308-
# Wait for KEEP_ALIVE_TIME seconds before sending next keep-alive message
309-
if self.is_ready:
310-
time.sleep(KEEP_ALIVE_TIME)
311-
312326
def close(self):
313327
"""
314328
Closes the connection to the bridge.
315329
"""
316330
self.is_closed = True
317331
self.is_ready = False
332+
self._command_queue.put(None)

limitlessled/group/commands/v6.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def get_bytes(self, bridge):
4141

4242
wb1 = bridge.wb1
4343
wb2 = bridge.wb2
44-
sn = bridge.sn
44+
sn = bridge.next_sn()
4545

4646
preamble = [0x80, 0x00, 0x00, 0x00, 0x11, wb1, wb2, 0x00, sn, 0x00]
4747
cmd = [0x31, self.PASSWORD_BYTE1, self.PASSWORD_BYTE2,

0 commit comments

Comments
 (0)