|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +"""Python Client library for Open Pixel Control |
| 4 | +http://github.com/zestyping/openpixelcontrol |
| 5 | +
|
| 6 | +Sends pixel values to an Open Pixel Control server to be displayed. |
| 7 | +http://openpixelcontrol.org/ |
| 8 | +
|
| 9 | +Recommended use: |
| 10 | +
|
| 11 | + import opc |
| 12 | +
|
| 13 | + # Create a client object |
| 14 | + client = opc.Client('localhost:7890') |
| 15 | +
|
| 16 | + # Test if it can connect (optional) |
| 17 | + if client.can_connect(): |
| 18 | + print('connected to %s' % ADDRESS) |
| 19 | + else: |
| 20 | + # We could exit here, but instead let's just print a warning |
| 21 | + # and then keep trying to send pixels in case the server |
| 22 | + # appears later |
| 23 | + print('WARNING: could not connect to %s' % ADDRESS) |
| 24 | +
|
| 25 | + # Send pixels forever at 30 frames per second |
| 26 | + while True: |
| 27 | + my_pixels = [(255, 0, 0), (0, 255, 0), (0, 0, 255)] |
| 28 | + if client.put_pixels(my_pixels, channel=0): |
| 29 | + print('...') |
| 30 | + else: |
| 31 | + print('not connected') |
| 32 | + time.sleep(1/30.0) |
| 33 | +
|
| 34 | +""" |
| 35 | + |
| 36 | +import socket |
| 37 | +import struct |
| 38 | +import sys |
| 39 | + |
| 40 | +class Client(object): |
| 41 | + |
| 42 | + def __init__(self, server_ip_port, long_connection=True, verbose=False): |
| 43 | + """Create an OPC client object which sends pixels to an OPC server. |
| 44 | +
|
| 45 | + server_ip_port should be an ip:port or hostname:port as a single string. |
| 46 | + For example: '127.0.0.1:7890' or 'localhost:7890' |
| 47 | +
|
| 48 | + There are two connection modes: |
| 49 | + * In long connection mode, we try to maintain a single long-lived |
| 50 | + connection to the server. If that connection is lost we will try to |
| 51 | + create a new one whenever put_pixels is called. This mode is best |
| 52 | + when there's high latency or very high framerates. |
| 53 | + * In short connection mode, we open a connection when it's needed and |
| 54 | + close it immediately after. This means creating a connection for each |
| 55 | + call to put_pixels. Keeping the connection usually closed makes it |
| 56 | + possible for others to also connect to the server. |
| 57 | +
|
| 58 | + A connection is not established during __init__. To check if a |
| 59 | + connection will succeed, use can_connect(). |
| 60 | +
|
| 61 | + If verbose is True, the client will print debugging info to the console. |
| 62 | +
|
| 63 | + """ |
| 64 | + self.verbose = verbose |
| 65 | + |
| 66 | + self._long_connection = long_connection |
| 67 | + |
| 68 | + self._ip, self._port = server_ip_port.split(':') |
| 69 | + self._port = int(self._port) |
| 70 | + |
| 71 | + self._socket = None # will be None when we're not connected |
| 72 | + |
| 73 | + def _debug(self, m): |
| 74 | + if self.verbose: |
| 75 | + print(' %s' % str(m)) |
| 76 | + |
| 77 | + def _ensure_connected(self): |
| 78 | + """Set up a connection if one doesn't already exist. |
| 79 | +
|
| 80 | + Return True on success or False on failure. |
| 81 | +
|
| 82 | + """ |
| 83 | + if self._socket: |
| 84 | + self._debug('_ensure_connected: already connected, doing nothing') |
| 85 | + return True |
| 86 | + |
| 87 | + try: |
| 88 | + self._debug('_ensure_connected: trying to connect...') |
| 89 | + self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 90 | + self._socket.connect((self._ip, self._port)) |
| 91 | + self._debug('_ensure_connected: ...success') |
| 92 | + return True |
| 93 | + except socket.error: |
| 94 | + self._debug('_ensure_connected: ...failure') |
| 95 | + self._socket = None |
| 96 | + return False |
| 97 | + |
| 98 | + def disconnect(self): |
| 99 | + """Drop the connection to the server, if there is one.""" |
| 100 | + self._debug('disconnecting') |
| 101 | + if self._socket: |
| 102 | + self._socket.close() |
| 103 | + self._socket = None |
| 104 | + |
| 105 | + def can_connect(self): |
| 106 | + """Try to connect to the server. |
| 107 | +
|
| 108 | + Return True on success or False on failure. |
| 109 | +
|
| 110 | + If in long connection mode, this connection will be kept and re-used for |
| 111 | + subsequent put_pixels calls. |
| 112 | +
|
| 113 | + """ |
| 114 | + success = self._ensure_connected() |
| 115 | + if not self._long_connection: |
| 116 | + self.disconnect() |
| 117 | + return success |
| 118 | + |
| 119 | + def put_pixels(self, pixels, channel=0): |
| 120 | + """Send the list of pixel colors to the OPC server on the given channel. |
| 121 | +
|
| 122 | + channel: Which strand of lights to send the pixel colors to. |
| 123 | + Must be an int in the range 0-255 inclusive. |
| 124 | + 0 is a special value which means "all channels". |
| 125 | +
|
| 126 | + pixels: A list of 3-tuples representing rgb colors. |
| 127 | + Each value in the tuple should be in the range 0-255 inclusive. |
| 128 | + For example: [(255, 255, 255), (0, 0, 0), (127, 0, 0)] |
| 129 | + Floats will be rounded down to integers. |
| 130 | + Values outside the legal range will be clamped. |
| 131 | +
|
| 132 | + Will establish a connection to the server as needed. |
| 133 | +
|
| 134 | + On successful transmission of pixels, return True. |
| 135 | + On failure (bad connection), return False. |
| 136 | +
|
| 137 | + The list of pixel colors will be applied to the LED string starting |
| 138 | + with the first LED. It's not possible to send a color just to one |
| 139 | + LED at a time (unless it's the first one). |
| 140 | +
|
| 141 | + """ |
| 142 | + self._debug('put_pixels: connecting') |
| 143 | + is_connected = self._ensure_connected() |
| 144 | + if not is_connected: |
| 145 | + self._debug('put_pixels: not connected. ignoring these pixels.') |
| 146 | + return False |
| 147 | + |
| 148 | + # build OPC message |
| 149 | + len_hi_byte = int(len(pixels)*3 / 256) |
| 150 | + len_lo_byte = (len(pixels)*3) % 256 |
| 151 | + command = 0 # set pixel colors from openpixelcontrol.org |
| 152 | + |
| 153 | + header = struct.pack("BBBB", channel, command, len_hi_byte, len_lo_byte) |
| 154 | + |
| 155 | + pieces = [ struct.pack( "BBB", |
| 156 | + min(255, max(0, int(g))), |
| 157 | + min(255, max(0, int(r))), |
| 158 | + min(255, max(0, int(b)))) for r, g, b in pixels ] |
| 159 | + |
| 160 | + if sys.version_info[0] == 3: |
| 161 | + # bytes! |
| 162 | + message = header + b''.join(pieces) |
| 163 | + else: |
| 164 | + # strings! |
| 165 | + message = header + ''.join(pieces) |
| 166 | + |
| 167 | + self._debug('put_pixels: sending pixels to server') |
| 168 | + try: |
| 169 | + self._socket.send(message) |
| 170 | + except socket.error: |
| 171 | + self._debug('put_pixels: connection lost. could not send pixels.') |
| 172 | + self._socket = None |
| 173 | + return False |
| 174 | + |
| 175 | + if not self._long_connection: |
| 176 | + self._debug('put_pixels: disconnecting') |
| 177 | + self.disconnect() |
| 178 | + |
| 179 | + return True |
| 180 | + |
| 181 | + |
0 commit comments