From 43aa5630c91fa6aa146b476c9d5ca5ced315e912 Mon Sep 17 00:00:00 2001 From: Thomas Schmid Date: Tue, 27 Jul 2021 21:56:18 +0200 Subject: [PATCH] Initial Commit Signed-off-by: Thomas Schmid --- cloudlaser.service | 8 + config.yaml | 11 + main.py | 124 ++++++ stupid_artnet/StupidArtnet.py | 364 ++++++++++++++++++ stupid_artnet/__init__.py | 1 + .../__pycache__/StupidArtnet.cpython-39.pyc | Bin 0 -> 9185 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 151 bytes 7 files changed, 508 insertions(+) create mode 100644 cloudlaser.service create mode 100644 config.yaml create mode 100644 main.py create mode 100644 stupid_artnet/StupidArtnet.py create mode 100644 stupid_artnet/__init__.py create mode 100644 stupid_artnet/__pycache__/StupidArtnet.cpython-39.pyc create mode 100644 stupid_artnet/__pycache__/__init__.cpython-39.pyc diff --git a/cloudlaser.service b/cloudlaser.service new file mode 100644 index 0000000..0dd0e06 --- /dev/null +++ b/cloudlaser.service @@ -0,0 +1,8 @@ +[Unit] +Description=cloudlaser + +[Service] +ExecStart=/usr/bin/python3 /home/root/laser_cloud/main.py --config /home/root/laser_cloud/config.yaml + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..4f5e2f0 --- /dev/null +++ b/config.yaml @@ -0,0 +1,11 @@ +target: 192.168.0.99 +universe: 0 +max_sleep: 10.0 +min_sleep: 3.0 +positions: + - x: 10 + y: 10 + color: "pattern" + - x: 50 + y: 20 + color: "yellow" \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..a772568 --- /dev/null +++ b/main.py @@ -0,0 +1,124 @@ +import stupid_artnet.StupidArtnet as artnet +import time +import yaml +import argparse +from voluptuous import Schema, Required +import random +from dataclasses import dataclass + +CONFIG_SCHEME = Schema( + { + Required('target'): str, + Required('universe'): int, + Required('min_sleep'): float, + Required('max_sleep'): float, + Required('positions'): list + } +) + +COLORS = {'red': 40, 'green': 80, 'yellow': 1, "pattern": 120, "magic": 160} + +@dataclass(frozen=True) +class Cloud: + x: int + y: int + color: str + +def laser_on(artnet): + artnet.set_single_value(10, 0) + artnet.show() + +def laser_off(artnet): + artnet.set_single_value(10, 17) + artnet.show() + +def set_mode(artnet, mode): + artnet.set_single_value(1, mode) + +def set_color(artnet, color): + artnet.set_single_value(8, color) + +def set_pos(artnet, pos): + artnet.set_single_value(5, pos[0]) + artnet.set_single_value(6, pos[1]) + +def intTryParse(value): + try: + return int(value), True + except ValueError: + return value, False + +def load_config(config_path): + with open(config_path, "r") as f: + data = yaml.safe_load(f) + # validate config + CONFIG_SCHEME(data) + + positions = list() + for p in data['positions']: + positions.append(Cloud(x = p['x'], y = p['y'], color=p['color'])) + + data['positions'] = positions + return data + +def interactive(an, config): + import keyboard + + x = 0 + y = 0 + + set_color(an, 0x100) + + while True: + if keyboard.is_pressed("w"): + x = x + 1 + if keyboard.is_pressed("s"): + x = x - 1 + if keyboard.is_pressed("a"): + y = y - 1 + if keyboard.is_pressed("d"): + y = y + 1 + + set_pos(an, (x, y)) + time.sleep(0.05) + +def main(args): + config = load_config(args.config) + + # setup laser + an = artnet.StupidArtnet(targetIP=config['target'], universe = config['universe']) + an.start() + set_mode(an, 0xFF) + + if args.interactive: + interactive(an, config) + + positions = config['positions'] + + # loop through colors + while True: + p = random.choice(positions) + + # exclude current position from next choises + positions = list(set(config['positions']) - set([p])) + + # set position and choose color + set_pos(an, (p.x, p.y)) + + if p.color == 'random': + c = random.choice(list(COLORS.values())) + else: + c = COLORS.get(p.color) + + set_color(an, c) + + # wait random amount of times + sleep_time = config['min_sleep'] + (config['max_sleep'] - config['min_sleep']) * random.uniform(0, 1) + time.sleep(sleep_time) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--config") + parser.add_argument("--interactive", action="store_true") + args = parser.parse_args() + main(args) diff --git a/stupid_artnet/StupidArtnet.py b/stupid_artnet/StupidArtnet.py new file mode 100644 index 0000000..28677a4 --- /dev/null +++ b/stupid_artnet/StupidArtnet.py @@ -0,0 +1,364 @@ +"""(Very) Simple Implementation of Artnet. + +Python Version: 3.6 +Source: http://artisticlicence.com/WebSiteMaster/User%20Guides/art-net.pdf + http://art-net.org.uk/wordpress/structure/streaming-packets/artdmx-packet-definition/ + +NOTES +- For simplicity: NET and SUBNET not used by default but optional + +""" + +import socket +from threading import Timer + + +class StupidArtnet(): + """(Very) simple implementation of Artnet.""" + + UDP_PORT = 6454 + + def __init__(self, targetIP='127.0.0.1', universe=0, packet_size=512, fps=30, receiver_needs_even_packet_size=True, broadcast=False): + """Class Initialization.""" + # Instance variables + self.TARGET_IP = targetIP + self.SEQUENCE = 0 + self.PHYSICAL = 0 + self.UNIVERSE = universe + self.SUB = 0 + self.NET = 0 + self.PACKET_SIZE = self.put_in_range(packet_size, 2, 512, receiver_needs_even_packet_size) + self.HEADER = bytearray() + self.BUFFER = bytearray(self.PACKET_SIZE) + + self.bMakeEven = receiver_needs_even_packet_size + + self.bIsSimplified = True # simplify use of universe, net and subnet + + # UDP SOCKET + self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + if broadcast: + self.s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Timer + self.fps = fps + + self.make_header() + + def __del__(self): + self.stop() + self.close() + + def __str__(self): + """Printable object state.""" + s = "===================================\n" + s += "Stupid Artnet initialized\n" + s += "Target IP: %s:%i \n" % (self.TARGET_IP, self.UDP_PORT) + s += "Universe: %i \n" % self.UNIVERSE + if not (self.bIsSimplified): + s += "Subnet: %i \n" % self.SUB + s += "Net: %i \n" % self.NET + s += "Packet Size: %i \n" % self.PACKET_SIZE + s += "===================================" + + return s + + def make_header(self): + """Make packet header.""" + # 0 - id (7 x bytes + Null) + self.HEADER = bytearray() + self.HEADER.extend(bytearray('Art-Net', 'utf8')) + self.HEADER.append(0x0) + # 8 - opcode (2 x 8 low byte first) + self.HEADER.append(0x00) + self.HEADER.append(0x50) # ArtDmx data packet + # 10 - prototocol version (2 x 8 high byte first) + self.HEADER.append(0x0) + self.HEADER.append(14) + # 12 - sequence (int 8), NULL for not implemented + self.HEADER.append(self.SEQUENCE) + # 13 - physical port (int 8) + self.HEADER.append(0x00) + # 14 - universe, (2 x 8 low byte first) + if (self.bIsSimplified): + # not quite correct but good enough for most cases: + # the whole net subnet is simplified + # by transforming a single uint16 into its 8 bit parts + # you will most likely not see any differences in small networks + v = self.shift_this(self.UNIVERSE) # convert to MSB / LSB + self.HEADER.append(v[1]) + self.HEADER.append(v[0]) + # 14 - universe, subnet (2 x 4 bits each) + # 15 - net (7 bit value) + else: + # as specified in Artnet 4 (remember to set the value manually after): + # Bit 3 - 0 = Universe (1-16) + # Bit 7 - 4 = Subnet (1-16) + # Bit 14 - 8 = Net (1-128) + # Bit 15 = 0 + # this means 16 * 16 * 128 = 32768 universes per port + # a subnet is a group of 16 Universes + # 16 subnets will make a net, there are 128 of them + self.HEADER.append(self.SUB << 4 | self.UNIVERSE) + self.HEADER.append(self.NET & 0xFF) + # 16 - packet size (2 x 8 high byte first) + v = self.shift_this(self.PACKET_SIZE) # convert to MSB / LSB + self.HEADER.append(v[0]) + self.HEADER.append(v[1]) + + def show(self): + """Finally send data.""" + packet = bytearray() + packet.extend(self.HEADER) + packet.extend(self.BUFFER) + try: + self.s.sendto(packet, (self.TARGET_IP, self.UDP_PORT)) + except Exception as e: + print("ERROR: Socket error with exception: %s" % e) + finally: + self.SEQUENCE = (self.SEQUENCE + 1) % 256 + + def close(self): + """Close UDP socket.""" + self.s.close() + + ## + # THREADING + ## + + def start(self): + """Starts thread clock.""" + self.show() + self.__clock = Timer((1000.0 / self.fps) / 1000.0, self.start) + self.__clock.daemon = True + self.__clock.start() + + def stop(self): + """Stops thread clock.""" + try: + self.__clock.cancel() + except: + pass + + ## + # SETTERS - HEADER + ## + + def set_universe(self, universe): + """Setter for universe (0 - 15 / 256). + + Mind if protocol has been simplified + """ + # This is ugly, trying to keep interface easy + # With simplified mode the universe will be split into two + # values, (uni and sub) which is correct anyway. Net will always be 0 + if (self.bIsSimplified): + self.UNIVERSE = self.put_in_range(universe, 0, 255, False) + else: + self.UNIVERSE = self.put_in_range(universe, 0, 15, False) + self.make_header() + + def set_subnet(self, sub): + """Setter for subnet address (0 - 15). + + Set simplify to false to use + """ + self.SUB = self.put_in_range(sub, 0, 15, False) + self.make_header() + + def set_net(self, net): + """Setter for net address (0 - 127). + + Set simplify to false to use + """ + self.NET = self.put_in_range(net, 0, 127, False) + self.make_header() + + def set_packet_size(self, packet_size): + """Setter for packet size (2 - 512, even only).""" + self.PACKET_SIZE = self.put_in_range(packet_size, 2, 512, self.bMakeEven) + self.make_header() + + ## + # SETTERS - DATA + ## + + def clear(self): + """Clear DMX buffer.""" + self.BUFFER = bytearray(self.PACKET_SIZE) + + def set(self, p): + """Set buffer.""" + if len(self.BUFFER) != self.PACKET_SIZE: + print("ERROR: packet does not match declared packet size") + return + self.BUFFER = p + + def set_16bit(self, address, value, high_first=False): + """Set single 16bit value in DMX buffer.""" + if address > self.PACKET_SIZE: + print("ERROR: Address given greater than defined packet size") + return + if address < 1 or address > 512 - 1: + print("ERROR: Address out of range") + return + value = self.put_in_range(value, 0, 65535, False) + + # Check for endianess + if (high_first): + self.BUFFER[address - 1] = (value >> 8) & 0xFF # high + self.BUFFER[address] = (value) & 0xFF # low + else: + self.BUFFER[address - 1] = (value) & 0xFF # low + self.BUFFER[address] = (value >> 8) & 0xFF # high + + def set_single_value(self, address, value): + """Set single value in DMX buffer.""" + if address > self.PACKET_SIZE: + print("ERROR: Address given greater than defined packet size") + return + if address < 1 or address > 512: + print("ERROR: Address out of range") + return + self.BUFFER[address - 1] = self.put_in_range(value, 0, 255, False) + + def set_single_rem(self, address, value): + """Set single value while blacking out others.""" + if address > self.PACKET_SIZE: + print("ERROR: Address given greater than defined packet size") + return + if address < 1 or address > 512: + print("ERROR: Address out of range") + return + self.clear() + self.BUFFER[address - 1] = self.put_in_range(value, 0, 255, False) + + def set_rgb(self, address, r, g, b): + """Set RGB from start address.""" + if address > self.PACKET_SIZE: + print("ERROR: Address given greater than defined packet size") + return + if address < 1 or address > 510: + print("ERROR: Address out of range") + return + + self.BUFFER[address - 1] = self.put_in_range(r, 0, 255, False) + self.BUFFER[address] = self.put_in_range(g, 0, 255, False) + self.BUFFER[address + 1] = self.put_in_range(b, 0, 255, False) + + ## + # AUX + ## + + def set_simplified(self, bDoSimplify): + """Builds Header accordingly. + + True - Header sends 16 bit universe value (OK but incorrect) + False - Headers sends Universe - Net and Subnet values as protocol + It is recommended that you set these values with .set_net() and set_physical + """ + if (bDoSimplify == self.bIsSimplified): + return + self.bIsSimplified = bDoSimplify + self.make_header() + + def see_header(self): + """Show header values.""" + print(self.HEADER) + + def see_buffer(self): + """Show buffer values.""" + print(self.BUFFER) + + def blackout(self): + """Sends 0's all across.""" + self.clear() + self.show() + + def flash_all(self): + """Sends 255's all across.""" + packet = bytearray(self.PACKET_SIZE) + [255 for i in packet] + # for i in range(self.PACKET_SIZE): + # packet[i] = 255 + self.set(packet) + self.show() + + ## + # UTILS + ## + + @staticmethod + def shift_this(number, high_first=True): + """Utility method: extracts MSB and LSB from number. + + Args: + number - number to shift + high_first - MSB or LSB first (true / false) + + Returns: + (high, low) - tuple with shifted values + + """ + low = (number & 0xFF) + high = ((number >> 8) & 0xFF) + if (high_first): + return((high, low)) + else: + return((low, high)) + print("Something went wrong") + return False + + @staticmethod + def put_in_range(number, range_min, range_max, make_even=True): + """Utility method: sets number in defined range. + + Args: + number - number to use + range_min - lowest possible number + range_max - highest possible number + make_even - should number be made even + + Returns: + number - number in correct range + + """ + if (number < range_min): + number = range_min + if (number > range_max): + number = range_max + if (make_even and number % 2 != 0): + number += 1 + return number + + +if __name__ == '__main__': + print("===================================") + print("Namespace run") + target_ip = '127.0.0.1' # typically in 2.x or 10.x range + universe = 15 # see docs + packet_size = 20 # it is not necessary to send whole universe + packet = bytearray(packet_size) + + a = StupidArtnet(target_ip, universe, packet_size) + a.set_simplified(False) + a.set_net(129) + a.set_subnet(16) + + print(a) + + a.set_single_value(13, 255) + a.set_single_value(14, 100) + a.set_single_value(15, 200) + + print("Sending values") + a.show() + a.see_buffer() + a.flash_all() + a.see_buffer() + a.show() + + print("Values sent") + + del a diff --git a/stupid_artnet/__init__.py b/stupid_artnet/__init__.py new file mode 100644 index 0000000..d8f773c --- /dev/null +++ b/stupid_artnet/__init__.py @@ -0,0 +1 @@ +# nothing yet diff --git a/stupid_artnet/__pycache__/StupidArtnet.cpython-39.pyc b/stupid_artnet/__pycache__/StupidArtnet.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..800a23d51599273be534dfa54524cf3b4d863b94 GIT binary patch literal 9185 zcmbta%WoVuvNAtS!0Vsie5=JSC&Odq|8{NxE#vfolVf&oa!N) zoYz+OSYl%oZz5fC3KA^#Oa-|(#~`-=$t}pA5cF{a1R#(cf(3HWVV&Ps-94Ncl5yZ| zYUvROx?GsNBWR`wt{ssVH3KT3xNE$o0C` z&?~yCJXCm!r(Y>N{Zgx>oHWnuXyUg{hG$=@s)F30oaZW8ZdMFFQpq`{lY6OFMx2zB zbqtjCO5Vxujq?0!8j;^C@X@$*f)`{dUhtI1k5 zwjlhbt|o(=>UT*UVh*U=rEb5}T#Y&djrx;BDb}-i+sq zPSx)Shf0p!aGRUcZM(YT_;M!Rcpg`$xl?nSF705(G*_OiFP6<|w$u{Lqg}dH*WaCI zD~s#QZgN(BdXI|Dmd`q#!`a3z!+5(>_t{3rXRS8PW7kb{AEKeG6xA@j?lv4zrE33^ zLFF!f-hU!-6v&QJ(YRXCIi!W(<~eS@QY&db!t<|`N`{Z}0`e?z8M#3mMxNuR_yls3 zpXQUuNB9{&g*?yC@^i>X`FVZ;d4a#h-$s6dU*rt=7=MSqi~J;ik6%JQ&Ohas`4xQe z6n~#zMau*)@(+-o<{$EF$S3(UFCjm}XZS4gDSn;LAwSDM;y*%uj^E%nk)L-i@H_k# z|M-;(S$+$(&-f>NUe?~`KZYdT4fAEc({_0zEW!J+P)Tw-Op4Y~Lgn5(t0-@B*XM4P zX7P7jKpvHSMMLs7O6yD2EVAUGGST=xe%@ar>8f4Df~F#co+8z{8d6=BQ!-6=mAyC4{W^uWS>Jf6AkXLCft`sN3 z-1@@W!^L%Lc{Mc3i$8g~xN?87Vyu4tv-0x&g-4 zo0hPfn@%{gx^VvsOi*5~EQYy_UEi^Vuy@1E=Zgyu7T3bey{Ah{$a5Qy?HyX+o>Q-dhHr~a$EO{3n(o)Y53D*;T+4F< zN1OpIDi?)Qb?B|tbR6zk4pwUQ^{V7HM9bz?;K;X%cnd?`+@0BKHJlm1Qp5IURz+*u zsR9c?f6P*?w^|(zs35ttfHVM_nSLoPwRc0q0wG=BvQA?@k0DX?jC$}lt{Q4y%V-&W zTuZ4FdPWOQ56qE_uM$6#hswliDX0@DKtvQ05#*{A6$PTAt`s#;$Zxg7G#2YQ->L%Y z5baHFP>>RYSKU8Bo)$5T#E#0eSQOopdMl=idmJ80T3Kv#n12(0#L408z zY>HC~E`9p_j~SecwijC;CefU<$>C-&zAoj7Ew9eA%ijEDmzhD~X)IZ2qI$mE*#N`i zT5hFRJh>_@2JBq`yG4E9y!ZVJ`m~r8Igk+g_+ZsqeX_PLMo<;RT#=__l#&xjidw|Q zNopW59b_d;mu1~RvqxhUNR^>Zs0GMeLH-%qA=b(vhgeH6B3b);6nsTu8nA3z-P3&O zy}Z=OpO7s&*N-Wuj%iOHv}1;&dZoqRN^Av&z9zw+9h%;jTk|b{%k?4= zIFkFJ`t=a+#T2HH{JMmKq8sWskR>1g3K~!*oma=y@%)rJj`lG%m>N*=K4cw`1s$`| z^5opVL4jm}6nu|(63IYRmNF3hA~=Wk)Lxpb=QhzfThqv!{7PxgrWD`U-By(ss0UMi z4mK2UKtAJF%Fh(8z0m$tYno9$tIVPm<4vxgfoD94vSl{Mhg)Vb4m?T+<4Z35sQNDR zKvKqS-!27L7T4CEtj)8sG(pS}0$$oP*WY5!^Qt3VFH-WAh+BYACE?RGY*jK#j8lo! zt&*Xc{gzZ!0XJU>bBoC|VY*F-Bw1P1rH+P~NMJ(M8R8ajd8_rTi5WZ+3PsDPV3nFu z&p}BqX~D?>)==L8o(vcO5;o8zfqoUVzy$XR@fpyBMb@_jYallv-a&uyE+vELAzuV& z_Xyb)Ef^nIgftxx3sOD2Tq))wdpU(&q;Bl+bK%|{+=!km5$Y> z2$5hwJ?3x0@8%4js_v9FX%))dzr4G?Qp^Z~G(iiK);vrD?S&ssSp93X$S7BL;QHE5 zqt!feleEr1$Im16R5U}KB5OCeRXO*et@d0)hJiuhfaDPDyb94#A;L`!;ux{uR2ims zXivn1TNqx+RBaq!>PIe-=Kl-);w5Sly@y2tb5|vw(2*W0_g45ZDzrWURfL*?9ps=& z_f#VVirZ328(JcEq`{6%uu^t>9Hv+ex{`QQ?CLC=X4h}988&z0W)bJLT<)<8J#%ZU zEn0r7+N!fH+hZG!(~QqS`Mu&(tMn;{`#?|0#zZ8`E`yNjt@_k5P2dD$c+S8<9rX-{+^-;o;7N6Oi(`hlG zM??v9OZL3W{1&U(b!Z<7IPvu;KwMc`0XE2eOI+{Ucc|k7!}})J*or>XaBqe-J9q2}*?sEJ- z83rHl6mB2P!S&cc$2{OI$CIIwhV56k5GbkEZGoUkAB(_( zP_H}9$S%um&EfMg3qrL$%m5t>_-FKp@pKG;pHhRdVZv9Xa-knRRPMnk0gm)$l%15A zB-W7mI3h(CdF5PnuHW2nefG6o?;s@7>=%MJH)1L;#Nf4wgK(2= z!rBtf`&)LCVp48%h|)tHPCNPu_0gPXgwPi9qO{kN$|J%~4v z%8>9uedr7tW@8u)({jC`x#ezdSv6PSFfyb+Inp1ww|~J%k3dAxOv%KEm1BBJom9uj z5KIm;H5ynYHzj$f++!`d>whR~`&lVPJQrN-W6Mzt5v0VC(| z@sQqqPRS5u#)vWz^DX(6e?upa;AxOR!CQyO(m(2W>iiTl{V?i~4JE`qPykRVl?Z%H zr4J8L>DiVGLEfn23=NrPQpo%*M085uN3mkMPuxDkx({Sdd_mPA+79Hgdgo$zXk3#&B zYbbTlV{n2Kj~J6I`^@zirffA3qUJzYz^%`ATOG{jqvF6*j7Q3Ysq|mr1zj!5xrtQm ztzFNp;*KDfqZ5RT3iUfvBO4D|alm!=7(u6d3G&A&niCnqtO+xiIaI8#t+-iPk)Kzr#FA@WB8Y z*AR-|vOv7jZ5;fet#?pJ29c{yACK(9Jt`4N6IcBsY8WyKx!#0lh_vL(5R?^kbJ(@$ zbsT^&;sw!I()ULwbfFn{5$!^*!SACoAYHI73}cP_Vt_j3h^Xq)Z;Xze|Mb*%>$pT@ z4F{JGd>+9nU)Z>NV2{i9Bq}^YE-}5?X>7pFr{nm7*!1R+M@@jIxPW7*3@YN)ftQ8X zG#5_zaw>AX*i~GBIym6V(`%8YTyttdF;HVKa56hmbl*AD0d zu}ua4Gt3#lGY%}03FLGqUbOU!zmF?%aDcAaEZjDOtRR2`MzjG0E?sRz6B9|JPWE$j zA_5)VOU7{Jxpf*WZ*}TCS=xrf8n`r*huR^=#*6QX3l#b#&m&C$Pdb}*qVT^v{%MS%|`tKnr21x1^ImzT}339g9AxrqT zPX8{_bz9SJIF=QfmepwSPMykm%lc)jf-aHaeNlR(w1tVn?j1P^xVHjCs1Z8@1Xw91AhKZJwc$dOo+8EW; z{Dh{;d5Ag<8~3`FxP;k)(G`%&!@=5NqSK6s`!QbZlgV1z|1&DmxA$f2hQfEENL5bB zzaj|=WDH2PBh?Q^zNE9K^ppJJgs^DHX(W|g6xc=AD1=Mf5`Rny!2n*%3pdMc3mN>A zQ4|T?1k#~uN5qtrmpqW9miAH7mdM&CWj*qR#_dQ5?vUhqw7Jv>utP6=qkN;>BlIB2 Vqdx;zA}0N5RHAE>=&elA_P_ZsOr!t+ literal 0 HcmV?d00001 diff --git a/stupid_artnet/__pycache__/__init__.cpython-39.pyc b/stupid_artnet/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b56ba846f1c39e8a810be42e0985cf9f0dda9683 GIT binary patch literal 151 zcmYe~<>g`k0)3y)34B2MF^Gc<7=auIATDMB5-AM944RC7D;bJF!U*D*jebUcZmNC> zkW4Jr4=BpdN=+^)*3U^SPA!U0&dJX&P0=qdDJ{rMiBBvl$xAKKkB`sH%PfhH*DI*J U#bJ}1pHiBWY6mj)GY~TX0B|uRoB#j- literal 0 HcmV?d00001