From a2aad1d1cd16159cae9ec74825754f33b5968bb5 Mon Sep 17 00:00:00 2001 From: thequux Date: Sat, 6 Aug 2011 20:02:43 +0000 Subject: [PATCH] Initial stab at bslv2 client git-svn-id: https://svn.code.sf.net/p/goodfet/code/trunk@1030 12e2690d-a6be-4b82-a7b7-67c4a43b65c8 --- client/goodfet.bslv2 | 321 +++++++++++++++++++++++++++++ client/hidapi.py | 221 ++++++++++++++++++++ client/lib/Makefile | 9 + client/lib/hidapi-linux2-x86_64.so | Bin 0 -> 52988 bytes 4 files changed, 551 insertions(+) create mode 100755 client/goodfet.bslv2 create mode 100644 client/hidapi.py create mode 100644 client/lib/Makefile create mode 100755 client/lib/hidapi-linux2-x86_64.so diff --git a/client/goodfet.bslv2 b/client/goodfet.bslv2 new file mode 100755 index 0000000..3b1d200 --- /dev/null +++ b/client/goodfet.bslv2 @@ -0,0 +1,321 @@ +#!/usr/bin/env python + +import hidapi +import struct +import time + +BSL_VID = 0x2047 +BSL_PID = 0x0200 + +def _fmt_addr(addr, count=3): + return "".join([chr((addr >> (n*8)) & 0xff) for n in range(count)]) + +RAM_BSL = """ +@2400 +E2 43 04 02 E2 43 02 02 80 00 04 25 +@2500 +00 05 04 34 31 40 90 33 B0 13 5E 2E 0C 93 00 24 +B0 13 F4 2D FF 3F 12 01 00 02 00 00 00 08 47 20 +00 02 04 01 00 00 00 01 06 00 FF 09 01 A1 01 85 +3F 95 3F 75 08 25 01 15 01 09 01 81 02 85 3F 95 +3F 75 08 25 01 15 01 09 01 91 02 C0 09 02 29 00 +01 01 00 80 32 09 04 00 00 02 03 00 00 00 09 21 +01 01 00 01 22 24 00 07 05 81 03 40 00 01 07 05 +01 03 40 00 01 +@2576 +F2 D2 20 09 F2 D2 22 09 10 01 5E 42 02 24 7E 93 +25 24 7E 90 09 00 04 28 7D 42 7E 82 5F 43 0C 3C +7E 92 02 2C 4D 4E 06 3C 7D 42 D2 93 0E 24 02 20 +4E 43 F4 3F 7E 43 4F 43 C2 4F 10 24 C2 4E 02 24 +4F 43 07 3C 1E 42 06 24 EF 4E 78 23 92 53 06 24 +5F 53 4F 9D F7 2B C2 4D 21 09 10 01 C2 43 10 24 +10 01 82 4C 06 24 5E 42 86 23 C2 9E 02 24 04 28 +C2 4E 02 24 4E 43 01 3C 5E 43 C2 4E 0E 24 80 00 +80 25 F2 B0 0F 00 84 23 14 20 C2 93 84 23 03 34 +5E 42 20 09 02 3C 5E 42 22 09 7E F2 C2 4E 60 24 +5E 42 60 24 42 19 4E 10 C2 4E 60 24 B0 13 C4 27 +09 3C C2 93 84 23 03 34 5E 42 C8 23 EE 3F 5E 42 +88 23 EB 3F 3C 40 60 24 80 00 D8 25 F2 43 02 24 +C2 43 10 24 C2 43 21 09 10 01 C2 93 82 23 12 20 +5E 42 84 23 7E F0 0F 00 02 20 80 00 42 26 5E 93 +0B 20 C2 93 84 23 03 34 F2 D2 C8 23 F6 3F F2 D2 +88 23 F3 3F B0 13 76 25 10 01 C2 93 80 23 04 34 +1F 43 D2 D3 3C 09 03 3C 0F 43 D2 C3 3C 09 5E 42 +80 23 7E B0 60 00 90 20 5D 42 81 23 4D 83 81 24 +5D 83 6B 24 6D 83 67 24 6D 83 45 24 5D 83 09 24 +6D 83 52 24 5D 83 46 24 5D 83 33 24 5D 83 54 24 +7B 3C 0F 93 79 24 5E 42 83 23 5E 83 08 24 5E 83 +0F 24 7E 80 1F 00 1C 24 5E 83 13 24 6D 3C C2 43 +23 09 F2 40 12 00 02 24 3C 40 16 25 80 00 D8 25 +C2 43 23 09 F2 40 29 00 02 24 3C 40 4C 25 80 00 +D8 25 F2 40 24 00 02 24 3C 40 28 25 80 00 D8 25 +C2 43 23 09 F2 40 09 00 02 24 3C 40 5E 25 80 00 +D8 25 0F 93 49 24 B0 13 C4 27 C2 43 60 24 D2 42 +01 24 61 24 3B 3C B0 13 CE 27 D2 42 82 23 3F 09 +80 00 42 26 B0 13 CE 27 D2 42 82 23 00 24 B0 13 +42 26 D2 43 12 24 10 01 C2 43 23 09 D2 43 02 24 +3C 40 00 24 80 00 D8 25 B0 13 CE 27 D2 42 84 23 +01 24 80 00 42 26 80 00 50 26 5E 42 84 23 7E F0 +0F 00 02 20 80 00 42 26 5E 93 18 20 C2 93 84 23 +04 34 F2 F0 D7 00 C8 23 F5 3F F2 F0 D7 00 88 23 +F1 3F 7E 90 80 00 03 20 B0 13 C4 27 43 3F 7E 90 +82 00 02 20 80 00 F8 25 B0 13 76 25 10 01 C2 43 +23 09 E2 43 02 24 10 01 D5 3E 1B 15 1F 42 5A 24 +5B 4F 03 00 5E 4F 01 00 5C 4F 02 00 8C 10 0C DE +0D 4B 0E 4F 2E 52 6A 4F 7A 80 10 00 29 24 5A 83 +14 24 5A 83 2A 24 5A 83 2E 24 6A 83 23 24 5A 83 +3A 24 5A 83 15 24 5A 83 3B 24 5A 83 3E 24 6A 83 +41 20 5F 43 B0 13 E2 2B 41 3C 1F 53 0C 4F B0 13 +38 2C 4C 93 02 20 4C 43 37 3C 7C 40 05 00 34 3C +B0 13 66 2E 03 20 B0 13 6E 2E F5 3F 6C 42 2C 3C +4F 43 E8 3F B0 13 D4 2D 27 3C 0E 4C 0F 4B 4C 43 +B0 13 D4 2C 21 3C B0 13 66 2E F0 23 4C 43 1F 42 +58 24 3F 50 40 00 1B 42 44 01 3B F0 10 00 0F 5B +82 4F 44 01 11 3C B0 13 3C 2E B0 13 12 2B 0E 3C +B0 13 3C 2E B0 13 66 29 09 3C 2E 42 3C 40 00 25 +0D 43 F8 3F 7C 40 07 00 B0 13 12 2E 1A 17 10 01 +E2 B2 3E 09 14 28 F2 40 80 00 23 09 03 3C F2 F0 +FA 00 3E 09 C2 43 10 24 C2 43 60 24 C2 43 61 24 +B0 13 80 26 D2 B3 3E 09 F2 2F E2 C2 3E 09 1F 42 +32 09 7F 90 0A 00 0C 20 B0 13 4A 2E B0 13 86 2C +B0 13 08 2A B2 F0 F9 FF 08 09 A2 D3 02 09 10 01 +7F 90 0C 00 06 20 B0 13 4A 2E B2 40 04 A5 20 01 +10 01 7F 90 12 00 0A 20 C2 43 23 09 D2 93 10 24 +02 20 80 00 80 25 F2 D2 20 09 10 01 7F 90 16 00 +02 20 80 00 08 2A 7F 90 18 00 0C 20 D2 43 11 24 +F2 C0 40 00 3E 09 B2 40 80 00 10 09 F2 40 20 00 +3D 09 10 01 7F 90 1A 00 0A 20 B0 13 86 2C F2 F0 +9F 00 3E 09 F2 40 C0 00 3D 09 C2 43 11 24 10 01 +7B 15 0A 4C 0B 4D 0F 4E 3F E3 0F 5F 0F 7F 08 4C +09 4D 08 5E 09 6F 47 43 0B 3C 1F 42 5C 24 FF 40 +3A 00 00 00 0C 46 1C 53 B0 13 2A 2E 0A 56 0B 63 +0B 99 03 28 34 20 0A 98 32 2C 47 93 30 20 0E 48 +0F 49 0E 8A 0F 7B 03 20 3E 90 3E 00 03 28 36 40 +3D 00 02 3C 06 48 06 8A 14 42 5C 24 14 53 0E 46 +0F 46 3F E3 0F 5F 0F 7F 0E 5A 0F 6B 09 3C 1F 15 +0D 16 6C 4D 0D 4E 0D 8A 05 44 05 5D C5 4C 00 00 +3E 53 3F 63 0F 9B C9 2B 02 20 0E 9A C6 2B B0 13 +66 2E ED 27 67 42 6C 42 B0 13 12 2E C7 3F 74 17 +10 01 F2 40 10 00 3C 09 C2 43 12 24 C2 43 11 24 +C2 43 00 24 C2 43 01 24 C2 43 3C 09 F2 43 02 24 +F2 43 04 24 C2 43 10 24 7E 40 80 00 C2 4E 21 09 +C2 4E 23 09 F2 40 8C 00 20 09 F2 40 8C 00 22 09 +F2 40 03 00 2F 09 F2 40 03 00 2E 09 C2 4E C8 23 +F2 40 10 00 C9 23 C2 4E CA 23 C2 4E CE 23 F2 40 +40 00 CF 23 C2 4E 88 23 C2 43 89 23 C2 43 8A 23 +F2 40 40 00 8F 23 F2 40 40 00 3C 09 C2 43 3E 09 +F2 40 C0 00 3D 09 10 01 7B 15 08 4C 07 4D 04 4F +4C 43 0A 48 0B 4D 0F 4E 3F E3 0F 5F 0F 7F 06 48 +06 5E 07 6F 02 3C 1A 53 0B 63 0B 97 03 28 2C 20 +0A 96 2A 2C 18 B3 08 2C 0E 46 0F 47 3E 53 3F 63 +0A 9E 19 20 0B 9F 17 20 6E 44 B0 13 66 2E 10 20 +4C 43 B0 13 56 2E 1B 15 0F 16 CF 4E 00 00 B0 13 +56 2E 1B 15 0F 16 6D 4F 4E 9D 03 24 5C 43 01 3C +6C 42 14 53 07 3C 3E 44 0C 4A 0D 4B B0 13 64 2D +1A 53 0B 63 4C 93 CF 27 74 17 10 01 3B 15 0A 4E +B2 43 54 01 08 4C 09 4D 07 3C 19 15 0E 16 6F 4E +C2 4F 52 01 18 53 09 63 0E 4C 0F 4D 0E 5A 0F 63 +09 9F 03 28 09 20 08 9E 07 2C B0 13 66 2E ED 27 +6C 42 B0 13 12 2E 15 3C 1E 42 54 01 1F 42 5C 24 +FF 40 3A 00 00 00 1B 42 5C 24 CB 4E 01 00 47 18 +0E 11 1F 42 5C 24 CF 4E 02 00 3C 40 03 00 B0 13 +2A 2E 38 17 10 01 32 C2 03 43 B2 40 02 1C 5A 24 +B2 40 17 24 5C 24 B2 40 28 96 00 09 82 43 02 09 +82 43 60 01 B2 40 F3 10 64 01 B2 40 40 00 62 01 +B2 40 44 02 68 01 C2 43 0E 24 C2 43 11 24 B2 40 +28 96 00 09 B2 40 40 1E 08 09 B2 40 80 00 04 09 +B0 13 4A 2E C2 43 12 24 B2 B2 08 09 06 28 B0 13 +86 2C B0 13 08 2A A2 D3 02 09 10 01 3B 15 4A 4F +6F 42 3B 40 58 24 B0 13 66 2E 08 20 4F 43 A2 4B +44 01 28 4B 38 50 40 00 82 48 40 01 4F 93 0B 20 +B2 90 05 00 5E 24 07 38 0F 4E 1E 42 5E 24 2E 82 +B0 13 8E 2A 4F 4C 4A 93 03 20 4C 4F B0 13 12 2E +A2 4B 40 01 2F 4B 3F 50 10 00 82 4F 44 01 38 17 +10 01 1B 15 21 83 0D 43 3A 40 E0 FF 0B 43 7E 4A +0F 4C 0F 5B 6F 4F 0E EF 0D DE 1B 53 3B 90 20 00 +F6 2B 0D 93 0E 20 B1 40 FF 7F 00 00 02 3C B1 53 +00 00 91 93 00 00 FB 37 B2 40 A5 A5 56 24 4C 43 +04 3C B0 13 D4 2D 7C 40 05 00 21 53 1A 17 10 01 +21 82 81 43 02 00 B2 40 28 96 00 09 92 D3 02 09 +92 42 14 24 12 09 B2 40 00 13 10 09 82 43 14 09 +81 43 00 00 02 3C 91 53 00 00 B1 90 64 00 00 00 +FA 2B 1F 41 02 00 0E 4F 1E 53 81 4E 02 00 3F 90 +E9 03 03 2C 82 93 14 09 E9 23 21 52 10 01 B0 13 +66 2E 0E 20 4C 43 B0 13 FA 2C 1D 42 58 24 2D 53 +82 4D 40 01 1F 15 0D 16 CD 43 00 00 80 00 08 2D +6C 42 10 01 92 B3 44 01 FD 2F 92 42 58 24 44 01 +10 01 92 B3 44 01 FD 2F 1F 42 58 24 3F 50 10 00 +82 4F 44 01 10 01 82 43 5E 24 C2 43 8A 23 B0 13 +A6 28 D2 93 12 24 0D 20 C2 93 11 24 0A 20 4F 43 +C2 93 8A 23 04 34 5F 42 8A 23 7F F0 7F 00 82 4F +5E 24 82 93 5E 24 EB 27 92 93 5E 24 06 38 5F 42 +01 1C 82 4F 5E 24 5C 43 10 01 4C 43 10 01 1B 15 +B0 13 66 2E 15 20 4F 43 B0 13 56 2E 1D 15 0A 16 +8A 4E 00 00 B0 13 56 2E 1D 15 0A 16 2B 4A 0E 9B +01 24 5F 43 92 B3 46 01 04 28 7F 40 03 00 01 3C +6F 42 4C 4F 1A 17 10 01 0A 12 7E 40 3F 00 C2 93 +CA 23 11 34 C2 4E 80 1C 3D 40 81 1C 4F 43 0A 4C +0A 5F ED 4A 00 00 1D 53 5F 53 4F 9E F8 2B F2 40 +40 00 CA 23 01 3C 4E 43 4C 4E 3A 41 10 01 B0 13 +FA 2C B0 13 56 2E 1F 42 58 24 3F 50 06 00 82 4F +40 01 C2 43 E0 FF B0 13 08 2D 4C 43 10 01 B2 40 +A5 A5 56 24 B2 40 00 A5 58 24 B0 13 7C 2B B0 13 +1C 2D 5C B3 FC 2B B0 13 D0 27 F9 3F 1F 42 5C 24 +FF 40 3B 00 00 00 1F 42 5C 24 CF 4C 01 00 2C 43 +80 00 2A 2E C2 4C 16 24 3C 40 16 24 B0 13 9E 2D +4C 93 FA 27 10 01 6E 4E 5F 4F 05 00 47 18 0F 5F +0E DF 10 01 03 43 3F 40 DE 2E 3F 53 FE 2F 10 01 +92 B3 44 01 FD 2F 10 01 B2 40 80 5A 5C 01 10 01 +B2 90 A5 A5 56 24 10 01 1D 15 10 01 +@FFFE +04 25 +q""" + +BLINK_BSL = """@8000 +31 40 00 34 B0 13 0C 80 B0 13 32 80 21 83 D2 43 +04 02 D2 43 02 02 B2 40 80 5A 5C 01 07 3C 91 53 +00 00 B1 93 00 00 FB 23 D2 E3 02 02 81 43 00 00 +F8 3F 80 00 36 80 80 00 3A 80 FF 3F +@FFE0 +12 34 56 78 99 10 11 12 13 14 15 16 17 18 19 20 +12 34 56 78 99 10 11 12 13 14 15 16 17 18 00 80 +q""" + +BLINK_BSL2 = """@2000 +31 40 00 34 B0 13 0C 20 B0 13 32 20 21 83 D2 43 +04 02 D2 43 02 02 B2 40 80 5A 5C 01 07 3C 91 53 +00 00 B1 93 00 00 FB 23 D2 E3 02 02 81 43 00 00 +F8 3F 80 00 36 20 80 00 3A 20 FF 3F +@FFE0 +12 34 56 78 99 10 11 12 13 14 15 16 17 18 19 20 +12 34 56 78 99 10 11 12 13 14 15 16 17 18 00 20 +q""" +def hexline(pkt): + return " ".join(c.encode("hex") for c in pkt) + +class TiHex(object): + def __init__(self, string): + self.content = content = [] + addr = 0 + for line in string.split("\n"): + if line.startswith("@"): + addr = int(line[1:],16) + elif line.startswith("q"): + return + elif len(line) == 0: + continue + else: + data = line.replace(" ", "").decode("hex") + if len(data) > 0: + content.append((addr, data)) + addr += len(data) + def __iter__(self): + for x in self.content: + yield x + + + +class BSL(object): + MSGS = ["SUCCESS", + "Flash write check failed", + "Flash Fail Bit Set", + "Voltage Change During Program", + "BSL Locked", + "BSL Password Error", + "Byte Write Forbidden", + "Unknown Command", + "Packet Length Exceeds Buffer Size"] + + def __init__(self, vid = BSL_VID, pid = BSL_PID): + self.vid = vid + self.pid = pid + self.device = hidapi.HidDevice(vid, pid) + + def _send_command(self, num, data, expect_response = True): + if len(data) >= 255: + raise Exception("Data too long") + packet = '\x3f' + chr(len(data) + 1) + chr(num) + data + #print " ".join(c.encode("hex") for c in packet) + r = self.device.write(packet) + #print r + if expect_response: + rdata = self.device.read(64) + if len(rdata) < 2: + raise Exception("Short response") + if rdata[0] != '\x3f': + raise Exception("Malformed packet") + print repr(rdata) + resp = rdata[2: 2+ord(rdata[1])] + if resp[0] == '\x3a': + return resp[1:] + elif resp[0] == '\x3b' and len(resp) == 2: + if resp[1] == '\0': + return True + else: + raise Exception(self.MSGS[ord(resp[1])]) + else: + raise Exception("Slightly malformed response") + + def RxPassword(self, passwd): + return self._send_command(0x11, passwd) + + + def RxDataBlock(self, addr, data): + return self._send_command(0x10, _fmt_addr(addr) + data) + + def RxDataBlockFast(self, addr, data): + return self._send_command(0x1b, _fmt_addr(addr) + data, False) + + def EraseSegment(self, addr): + return self._send_command(0x12, _fmt_addr(addr)) + + def ToggleInfoLock(self): + return self._send_command(0x13, "") + def MassErase(self): + return self._send_command(0x15, "") + def CrcCheck(self, addr, length): + return self._send_command(0x16, _fmt_addr(addr) + _fmt_addr(length, 2)) + def LoadPc(self, addr): + return self._send_command(0x17, _fmt_addr(addr), False) + def TxDataBlock(self, addr, length): + return self._send_command(0x18, _fmt_addr(addr) + _fmt_addr(length, 2)) + def TxBslVersion(self): + return self._send_command(0x19, "") + def TxBufferSize(self): + return self._send_command(0x1a, "") + + # Helpers + def RxTIHexFast(self, hexstring): + tihex = TiHex(hexstring) + for addr, value in tihex: + self.RxDataBlockFast(addr, value) + + def bounce_hid(self): + "Reconnect to the target" + self.device.close() + time.sleep(0.250) + self.device = hidapi.HidDevice(self.vid, self.pid) + print self.device._device + +def main(): + bsl = BSL() + #print BLINK_BSL + #return + try: + print repr(bsl.RxPassword('\xff' * 32)) + print repr(bsl.RxTIHexFast(RAM_BSL)) + print repr(bsl.LoadPc(0x2400)) + #print repr(bsl.RxTIHexFast(file("blinky.tihex","r").read())) + #print repr(bsl.LoadPc(0x2000)) + #bsl.device.close() + print repr("Bouncing...") + time.sleep(3) + print repr(bsl.bounce_hid()) + print repr("waiting") + print repr("Next") + print repr(bsl.TxBufferSize()) + print repr(bsl.TxBslVersion()) + finally: + bsl.device.close() + +main() diff --git a/client/hidapi.py b/client/hidapi.py new file mode 100644 index 0000000..6cf204c --- /dev/null +++ b/client/hidapi.py @@ -0,0 +1,221 @@ +from ctypes import * + +# Python doesn't define any sort of ABI string, so we need to manually probe for it. +# If your OS/arch isn't supported yet, see lib/README for help. + +import platform +import sys + +def _get_shared_lib_name(): + if sys.platform.startswith('linux'): + # This may fail for a x86_32 userland on a x86_64 kernel. platform.architecture can be used to work around it, if necessary + arch = platform.machine() + if arch == 'amd64': + # old 64-bit linux. + arch = 'x86_64' + if arch == 'x86_64' and platform.architecture()[0] == '32bit': + # Whee! 32-bit python on a 64-bit kernel. The horror! + arch = 'x86_32' + if arch[0] == 'i' and arch[2:] == '86': + arch = 'x86_32' + # Any other variations need to be dealt with here. + + return "lib/hidapi-%(platform)s-%(arch)s.so" % {"platform": sys.platform, "arch": arch} + elif sys.platform == 'darwin': + # This is completely backwards... both 32 and 64 report an i386. + # However, it may not matter; darwin supports fat binaries. + # If necessary, here's what I've found (very incomplete): + # + # arch* machine desired build + # 64bit i386 x86_64 + return "lib/hidapi-darwin.dylib" + elif sys.platform == 'win32': + # not yet fully supported. For now, assuming 32-bit x86. + return "lib/hidapi-win32-x86_32.dll" + else: + print >>sys.stderr, "Your platform is not yet supported. Please let the devs know, or read lib/README for help fixing it." + raise Exception("Unsupported platform") + +if __name__ == '__main__': + print "Would try to load %s" % _get_shared_lib_name() + +_hidapi = CDLL(_get_shared_lib_name()) + +class _HidDeviceInfo(Structure): + pass + +_HidDeviceInfo._fields_ = [ + ("path", c_char_p), + ("vendor_id", c_ushort), + ("product_id", c_ushort), + ("serial_number", c_wchar_p), + ("release_number", c_ushort), + ("manufacturer_string", c_wchar_p), + ("product_string", c_wchar_p), + ("usage_page", c_ushort), + ("usage", c_ushort), + ("interface_number", c_int), + ("next", POINTER(_HidDeviceInfo))] + +class HidDeviceInfo(object): + """User-facing version of the _HidDeviceInfo structure.""" + + def __init__(self, raw): + for attr in ("path", "vendor_id", "product_id", "serial_number", "release_number", "manufacturer_string", "product_string", "usage_page", "usage", "interface_number"): + setattr(self, attr, getattr(raw, attr)) + + def open(self): + return HidDevice(self.vendor_id, self.product_id, self.serial_number) + +class _HidDevice_p(c_void_p): + pass + +class HidDevice(object): + def __init__(self, vid, pid=None, serial=None): + if type(vid) is str: + assert pid is None and serial is None + self._device = _hidapi.hid_open_path(vid) + else: + self._device = _hidapi.hid_open(vid, pid, serial) + if not self._device: + raise IOError("Failed to open device") + + def write(self, string): + return _hidapi.hid_write(self._device, create_string_buffer(string), len(string)) + def read(self, length): + buf = create_string_buffer(length) + olen = _hidapi.hid_read(self._device, buf, length) + return buf.raw[:length] + def set_nonblocking(self, nonblocking = True): + _hidapi.hid_set_nonblocking(self._device, nonblocking) + def send_feature_report(self, report_id, data): + buf = create_string_buffer(chr(report_id) + data) + + return _hidapi.hid_send_feature_report(self._device, buf, len(data) + 1) + def get_feature_report(self, report_id, length): + # length does not include report id + buf = create_string_buffer(length + 1) + buf[0] = report_id + olen = _hidapi.hid_get_feature_report(self._device, buf, length + 1) + # BUG(thequux): Possible off-by-one error + return buf.raw[1:olen] + + def close(self): + _hidapi.hid_close(self._device) + self._device = None + + def get_manufacturer_string(_device): + buf = create_unicode_buffer(257) + _hidapi.hid_get_manufacturer_string(self._device, buf, 257) + return buf.value + + def get_product_string(_device): + buf = create_unicode_buffer(257) + _hidapi.hid_get_product_string(self._device, buf, 257) + return buf.value + + def get_serial_number_string(_device): + buf = create_unicode_buffer(257) + _hidapi.hid_get_serial_number_string(self._device, buf, 257) + return buf.value + + def get_indexed_string(_device, index): + buf = create_unicode_buffer(257) + _hidapi.hid_get_indexed_string(self._device, index, buf, 257) + return buf.value + + + + +class HidApiError(IOError): + pass + +def _check_hid_error(result, func, args): + if result == -1: + print args, _hidapi.hid_error(args[0]) + raise HidApiError(_hidapi.hid_error(args[0])) + return result + +# signatures +_hidapi.hid_enumerate.argtypes = [c_ushort, c_ushort] +_hidapi.hid_enumerate.restype = POINTER(_HidDeviceInfo) + +_hidapi.hid_free_enumeration.argtypes = [POINTER(_HidDeviceInfo)] + +_hidapi.hid_open.argtypes = [c_ushort, c_ushort, c_wchar_p] +_hidapi.hid_open.restype = _HidDevice_p + +_hidapi.hid_open_path.argtypes = [c_char_p] +_hidapi.hid_open_path.restype = _HidDevice_p + +_hidapi.hid_write.argtypes = [_HidDevice_p, POINTER(c_char), c_size_t] +_hidapi.hid_write.restype = c_int +_hidapi.hid_write.errcheck = _check_hid_error + +_hidapi.hid_read.argtypes = [_HidDevice_p, POINTER(c_char), c_size_t] +_hidapi.hid_read.restype = c_int +_hidapi.hid_read.errcheck = _check_hid_error + +_hidapi.hid_set_nonblocking.argtypes = [_HidDevice_p, c_int] +_hidapi.hid_set_nonblocking.restype = c_int +_hidapi.hid_set_nonblocking.errcheck = _check_hid_error + +_hidapi.hid_send_feature_report.argtypes = [_HidDevice_p, POINTER(c_char), c_size_t] +_hidapi.hid_send_feature_report.restype = c_int +_hidapi.hid_send_feature_report.errcheck = _check_hid_error + +_hidapi.hid_get_feature_report.argtypes = [_HidDevice_p, POINTER(c_char), c_size_t] +_hidapi.hid_get_feature_report.restype = c_int +_hidapi.hid_get_feature_report.errcheck = _check_hid_error + +_hidapi.hid_close.argtypes = [_HidDevice_p] + +_hidapi.hid_get_manufacturer_string.argtypes = [_HidDevice_p, POINTER(c_wchar), c_size_t] +_hidapi.hid_get_manufacturer_string.restype = c_int +_hidapi.hid_get_manufacturer_string.errcheck = _check_hid_error + +_hidapi.hid_get_product_string.argtypes = [_HidDevice_p, POINTER(c_wchar), c_size_t] +_hidapi.hid_get_product_string.restype = c_int +_hidapi.hid_get_product_string.errcheck = _check_hid_error + +_hidapi.hid_get_serial_number_string.argtypes = [_HidDevice_p, POINTER(c_wchar), c_size_t] +_hidapi.hid_get_serial_number_string.restype = c_int +_hidapi.hid_get_serial_number_string.errcheck = _check_hid_error + +_hidapi.hid_get_indexed_string.argtypes = [_HidDevice_p, c_int, POINTER(c_wchar), c_size_t] +_hidapi.hid_get_indexed_string.restype = c_int +_hidapi.hid_get_indexed_string.errcheck = _check_hid_error + +_hidapi.hid_error.argtypes = [_HidDevice_p] +_hidapi.hid_error.restype = c_wchar_p + +def hid_enumerate(vid=0, pid=0): + """Enumerate the HID devices. + + If vid == pid == 0, will enumerate all hid devices. Otherwise, just the ones with the given vid/pid. + + Returns: + List of HidDeviceInfo structures. + """ + + devs = _hidapi.hid_enumerate(vid,pid) + + raw_list = devs + ret = [] + while raw_list: + raw = raw_list.contents + raw_list = raw.next + ret.append(HidDeviceInfo(raw)) + + _hidapi.hid_free_enumeration(devs) + return ret + +def hid_open(vid, pid, serial_number = None): + return HidDevice(vid, pid, serial_number) + +def hid_open_path(path): + return HidDevice(path) + +if __name__ == '__main__': + for dev in hid_enumerate(): + print "%04x:%04x %30s <%s> <%s> %d" % (dev.vendor_id, dev.product_id, dev.serial_number, dev.manufacturer_string, dev.product_string, dev.interface_number) diff --git a/client/lib/Makefile b/client/lib/Makefile new file mode 100644 index 0000000..8f305be --- /dev/null +++ b/client/lib/Makefile @@ -0,0 +1,9 @@ + +hidapi: + git clone -b release git://github.com/thequux/hidapi.git + +hidapi-linux2-x86_64.so: + make -C hidapi/linux EXTRA_CFLAGS=-m64 clean hidapi.so && mv hidapi/linux/hidapi.so $@ + +hidapi-linux2-x86_32.so: + make -C hidapi/linux EXTRA_CFLAGS=-m32 clean hidapi.so && mv hidapi/linux/hidapi.so $@ diff --git a/client/lib/hidapi-linux2-x86_64.so b/client/lib/hidapi-linux2-x86_64.so new file mode 100755 index 0000000000000000000000000000000000000000..0d99edb7f6b58421843b100f0ec35e84db895a37 GIT binary patch literal 52988 zcmch=3wTu3)i-`-CYNEjWCn;vMHxvHPz(VD@q#9Vfe8jkb3st+Fi9rKNRo+jNdhgB z8cCz-b2 z_kW)6@C_2l2T)dk%Rw> zj58H)cB*JhZh25ck{X6r0n|ngK7KCu2fd((_EKUTaI3`V=Tz$10aLvWuGbOtf+E^W ziP2AjO{r6pMix@~?BQ~2TEu*k5-(j4 zIq+Gzrq0;8?7}aEetzTS4^Fsl_`!zXFP^n;>wc11j?Wr=a`7p~hc2qOT;+7R7x^Nl zeVi^rdKNx2@Hq*eS@=xFrvjhF_{en{XQm*fek{dj5tqn;n}AGy zu>?E|O?;)CpNrIG!p}s$1fTQpxe%XK_|QeJIaB2fV*%1>_?*J@cnR5(uFG^{lp&RI znuz>r#wDqg4AMMP-kL37K*o;9%Ne@}=^32A80p#g6ymcIpV|22<3ra3ofzci=i+lJ zJ}VeIhto@u&f)xIq%;TQnkN~gr*r-i6E=s#MG0|{mu33XI%CE53&yb;npv9y?OgLUz_<}R=Mw`cTaiYm%Yn=)H3Vq z56<}0L-WdJ{iSbx<#5-}{Kl3a-}hSGA6|smjF$u6>M?hJBX z$-sYS2L9JG^y_mO^y$eUXHACwLLI}4kM&xTLC+gA;B^_~Z-U_Q?Dn?|{dzEi{;y=f zH)p^nX3(=W1D>5h|EU@HN6_wg{(L$5H6DHsodray?9K!8T3CRgZ!H_=rb#WoKG|8^RnWf zWE`Un3)c%eQ8t@uOg8*<_%K}$sPUa-tmMr_mfinmIR(5y6hd#hzNqvcyFOp7^q*vW zfj8?{HM{W&(y2Is4DsfCCd;`LhYYvz-ss5nUgp%v7S`t^i#~qlcSh6sH*vdvEz|YS zM18uhReDY`Zllc)*LCO*U5hfb`#$&gn{+_JRj3nVnDv><`qMf~*RzawTl8GS_)OkJ z|Ek%IuQC2T=I>!Vhw&%5-5)T%g8TJ%I>6z&K_|u;%-=y9HLizrVi$NQ(ec5?Pz)_>POokS zbu)rCMMEK@ITR0Q4I4v&rbsl%Y>9To$oj_;^=;vJ$`Kh>&7@HAOL4ibAUu;DcBH7 z_PPNx3T{Nk?uv$y<8~BhG|^$|Jn_){OVy=Gr#eeT@WeoSq`jW(7jAD>B4Z(NG@*q= zG=#bxk!W1u8@I|O~x;f`peG0_mW!m&^^9Bk#eD1~socJeT4E)o`dAF@VC4kV-e|0>tXNhaSXg|1 z@xo+&VKIFL$7NUFwXi&^JZ~#WobCV+hvrgDjOdzFPuJCD`i_C*Q#_i>ZrJ|qe_DnO z($7_UoTj6vgz;6?VXS5>aVpVvV*_ZqN<&_i%6DO*H(K#(#b&fv@Kua=TkzG4_gL_C zjPJ4F8yMeb!MhkgV8ORCe$aw{h4Fq1emmnwE%lcQSs=f`6UyK@0w$j1O7x z2N^$M!GFZK(VRZ+&oJ(`;LkH&V8MUM_#6xV65}Nn{11$mTJT}U{T5vK*IV#UnSY}N zx7BLD>agHhjCWh`JjQog@TrXNvEVZp-*3TZGJe2<&u09P1wV)JehYpvqPWxxS1)s|J zQ43zk_<#kU%lM!LznJk63tr0j2@77!xHFvY4_7hnw&1Oddo6gB@i`WJJL5|%_?Hz4;OiMLu;3dRpJTxr z7%#ElF~&ev39t(aq z7$33V*D!v`&#~b5Grq)v|B&%g z3;rDARTlgO#@Ac$*BEcH;BPVBVZlFUyvKrn#`sPP?!H=&?>-BD6651^*7?b1e9M zjF(vOCmAob;Qzz8--5r$_<9TeGUFR9_-l-JSnxL(@3!C{Fuv1*f5P}43qFyb*V}Kw zJ@Oo%1wWbbLl*p0#``U}m+@m3ypZt$3qFVOAq##k<0BUQe8vs_U0e43#f&>G_)^9T zEO;s7UJJg8@e&KZn(-wTyqa;p1+Qbg%7QmBzR`lWGTvgrg}>W^f06lnEcnfg@3G)t zW_+Io-^2I;3w|5p2QBzF81J{>-(viz1;3l|k_J6bbFdtptd}J9{}K~kY{E-T`1vM$ zBjdTi>?XX$gr|Np!t<{tJj=x2ZNmAd8dch3!bw-TcA9W%TdqAOTzrwr_L=ZXVMe;& zgikQx2TZujgda5F6HWLb6F$j=_nYv^Cj6)gpJKv~neaRlK48M#CVbF@drbI{37=}h zM@;xM6Mn*kPdDL4qgPR6<9rkDG~p+iaJLDUXH2QMz=Ri=_`N2)+=S0D;is7J5)*!^ z314EuPcz}ACVYko_nUBe9+ir#Ot?H3OZa*dK9c~xHk$BN0wQfO;a(HoVZxW2@NN@+ zrU~yc;j>KmP7^NA?oja_6JBKE-)F+lGU5A8_}M1>fC-;%!VjA8IVSv&37>1i`%U;c zCj6)gKi7mGGvV`0_<#vN&x8+}@cAZu$b^@e@DUTfz=WSL;R{W;!M|^k|C`r6rwPBn z#P2rYi%fWd314i&y(auZ6F$d;FEQc%-glitPhv$Kx+)9*eFUHO_de@_)ivA_hw4@G~ zDEg;F(^5L@RrJ$D(-J!DR`jDp)6zL?DEfy)(~>zn@(%!KeV=GI(SwTqHqo?14j)tW zokY{pINY!3TZx`V^g%^`nP^%HhxaS`Mxtp69NweoEkx7OH{7G>DABZ)4tFT}I-+T* z8{VksdZKA58?I9HRYcPgHe9OcYNBcB8ZJ@vDxzu08uluB8PT*<4Z9V6G10U{4I7GH zNHi@?!y|vE{+~m%m*_!7&mx+Zmf>THK9y)%Qil5#J(Xx$N`?7gyB*}|B`503WiG* z{Zpc82^jV&`e~wR>JPgW{V36NL>V>|{X?Q@2^b#vOpQO$B}5M@`rAY=Ao`f1?9hdUH~9nmy(hc_y^ zp6H8+t^!T-*uN{guGnk%cl5{YqsP=V-Xm9C&(af@dNH~C-YToDN!0s$mo5RX5ufDW zwRAp24PAv!`gfmRKF@1Z?mp`RB#`&NP^WV4AN)`K&EfAoZu4I>5cB5ETuzHbzs=v5 z{Q%0#MyEdu)^8(=^5|R7Wq$=(+tp>)eEylHyqVjfBJnRBIT!8KL4o+GVE^y)zz$8P zeBQr%>90{VI!Nox=bzcLhj{#bTTl4=5+i-b{C%qjm-qRGD*ILs?DW0s-{m{u-&Hr_ z?{iZ&@8P<4cUDcH>~p0q2c^5}3i_(hEyXwJ@2e`&=ultPbVl7k=Wu=idGF_+QH}P# zTS5)1Mu*Voo25GwA2#**Mt1q$%zNZ$-Xn>TyoY@s_PpT8d*qmZ*VZADab$Usa|jDv z-=w07zE8^fM*4p7D6L~p^iaS1jtup~pPrWn+)o?@{3I=$LofdgChsFX@i_tn{3NiH zmO0MSip5!XDyxoEnpd5EbXFZmbk>{VEuyv*+H-(VTtxYgB;Rdg@{lfM-hLB>(Jpy{ zXzI?H=#Hw<+rK^u-a=g=so6Wh_9*QG$ui?pU6!(e8r>u}3B84Um=Fjhx-vy$NI~}Uz|YM5#MKI@#ClM@_pvtm-onW-@&F! zeD@@B4=LEwi*giQ;ydxh-zjBSjW4}h1-J3{J$?xN>l@j*<3~vSM|>x!FGH__fPNq6 zJyQ3vf7cGh^J}ukMVPlc`V-R_B0H% z#%7n&W;hpWKIePQ*yS6+BwsoTvdgUr|Gww8XOkSmzk87fn-}ITB|~l6lZX7hTZcvy zEQlPyzk7#jx3}N!@7r+{oghg=cK<$*1JpquYXZdI`R z4$b{zl7X=vs_4T4ab)P3zfxu2AX*+AjW0d2w&(+J>WP%9ORFA4-W{r`UH_j{k!ugU~29H z-)`T*vcC7q`UajTC1v`KDB}*@4t1aN{RO@A9V9u=QcJzhuD_aBpfA0YmdEYe`}20xk&NAUkPK6$bswury<3kOn+r_E$)(~BFkq}ywH^I= z+w&mii0@5mfB0qeK+1NNx_8yR>2LU@|EV{!{5B}%J9NZ1sQA92_qwm??A3vSayoZm0 zZJ(}xk6M+f{xUZTzrsvkNn_fUV)BCNtIYJ(G^VRkOmj#+`R)Pefcy}ar01IYs;FC( ze-?Qc(p!i|zJOA6tFP*UUf-Y%)S+Fa&P#lQd0(x=Le=Yg6POxu*gp&QzXY4@SEr&# zMjLIlqOIdud-NRLzpL&uJ@yz)bQjYLhUwjU%*eZ=pO#nhz>AgbeEXlkz^J8eg7Pij zNOINpVoqz@)9epGu@nAXrAv5ZkXP00AE;hZk(+w`{`*N90*(y5k)r1i{Y#;de@^E| zAQC&<))PkJ&SdK)eV_E9pzc^x-|F9D>I_)tHcU8xY4zK_UqjM2$=0;YkxP@hjiheg zBc)Eb-)|GEm~w#{NL;FH5vPHI+Es3(;y25o5mpDBz^Div^o<<%nhES9Ni(p{cR<{T z5hpQF82c|-1Sc}k(B;TbmG0$Gs%tRp07~&Gg-sOa0L+IOy@Ouhda?jRGV~u>40Q>G z1+*g^8QQMEr?qHmQ59hhYiT9^zNb_fss3}SaJOze#Rip>D-L2t$%zU3Qa`F58Tt;p ztbbPzt#vqnCeDOBbfpgU_9%=3DC8&v^BkI^jUVNNJRF`Fs zDWenb0QE5l6DEhE3PW)eB}Yj~q$fyUSXwPUBg8(H42Ut(o5F|hk$2af@b}$#kPL(& zFYD{q6`guTfS3{jU_;HadWep|JFySpNQ|177FF(^{2om|h@Y=5p5zA9uZN7!ip2B} zw0is;On_r#S)>Dvl>e@4m;^)9%FE#?Uo9!+59Va~IAzD{utY8|-Z8uq*qi`Q{C_7ub>P?*UWm z%x>SFq{{0@lw}8oufVqhY^Z7Jr2$?s*qep!2Qcs8XDLxE4ZG!J+*aT@@`(+cX*ENX7CU}hW(3{97v~FU5 z_3yev84T+;Z$cMp1$UgLs>0V(J$m;~G+aOG-TEUVZ|CVKhdq^nbc@^qY8bkaOChTF zge_6ZDve<{_s>azn@%+&wHX@0pA2Lh&>c8Uedeu9wUBE@78Y`@uDQ38a^oR zRLCRPRq5-h4+u>iRGoyXXQ-+Ly<7Ji@u_2~PU01f{6TNZALf1aJnf)^YPsCK1#A0L zIP&hr*+8A(hSp&)r~&;gkcOv7Bk-6bLx+yjZvQT=lSkDF;N85huE#Qg`P=Io0j%6Q z+1>~a0csD|-VOoH?1SRbdfzQo zM}xiumhZX&V#|BjP{Tvt?r4Ik&D`Tx2b$it0ULkm1M85o?zV1Zv3=9p3uJoUBe;if zqUQy>9*H6H-d$S<$Gdkx_cWh4lbi>tZCCT?L+l{NX1aSZw5#sJzCHaYJ%q6sJ>G#Y zwMIM}iVTol2h=fUKy8wwNZ!LEREb*Ky%pKI!Lpywy$b&F)cFogip)*Byh6dHMc8~URJC@idrW+6P+yNKm1biE_v2Jcf^*r8RBhHRjIQIp02`QYLnYM6 zsFmRuI8XFd6)87O-mNM%j{#OO>TXJM(rJ~L#JJvSbWdEkGlgaM_`g$_ww@>ci|15{QkZTBB^92mmW3?7HIr1=4g2at zaNhRcQEd!D>NrM}9D?AC8D)&EMNcNgvq1tgeTb@WiTdtEEXh}~`yrLB5dVdo+XgZ|V)nvE5s@u5xiq#L( z8i&S?0?ZI(F;A(}sB;|k|5*i0tMm$w{)-A;YlS{@1z2tCP+#IrwI27aK1MfW)hBVKEt_wlIXbvg5x(5Ik6(+OZ3NE8oG5!fgDR(vqe|eP zsKOgQ|3E^w|LH!T)4wZw&NOJ)dtx*W!=8Q`h`gDgk2~?3$}ap2a$+>`9{sq2TXuW) zsNYZTB8$?mrucoMcEx!M7OUj^g~nMW7j$3D|1Io>RgF4)V))eKQ-ekqzBo8 zWC<>|{8xI&c&ui5;#2=_J!((p3AIljvIXg6)dkSMv@hvFcA;@4-=gcMxR?JFK8Nsm z2%qob^Pl+Kj?XT9Zp3FZJ{|Zp<8v)OWlhoWrXXUy7{Ml^thGMa9uBq}vFnYpXs|xq z;62Yc2@9K=UKNQpLK~phMN*tsBvjlBpqsi zZHRb#O8ACQ8=}=f_-3*t@?`rK)Y}5H!St2EI2mFe^Sul2eB4mUT z#T$O4=f(k{Sf~2qL;ji0Z`9WU`&wV7Loy1CDnY<#R{)?zdUjpd1WLko0U6N>E0EZ~8DXYEDY&K~5?Acah}05_ zV)BAlXEqy%5!DD@ft8cMMF0omDY!BWM~sKs5eN%*Lt$&!RG8A0N)o9Ov#uy2T@hbt zlM!k){Gs}2s0-my3_pV1gb}93@Fy@q)P!tF7!?RQ1i!%i4aba%_QnYOfM!Lw(Wt;w zMV2~@+-3v8kHT>iYHk%}XmwSvBS>@FxGrd1ff)!nn<55QjZPBT+G$jV<1Gojq_<+6 zi#HTk7q2b$Vls!&GVIZ|$yg0X-Hb+6N{dumEYP-@;Wv72gIguqLt%pJ+X=?{A2QnF z$YL)*|JtI)nn<(@@x+w!%OacFT8;Jy0urU+Yc{t>qOnazRfHz4(b0-rG@fWq=tW5r zDkf9_yF*&pvS@Hin4(1m>Z3+=FcgoRR~^B4!x^J(hRW);Icr2|YgL!IFw~K#Cy$CW zc~?hT8zY@TFb1%gRVQL<5TXgm!XDM*u_uVV2HWK(%ryh+eoZJ^uiH*8%rS&L>M#}P zRlFt?k6|c+1~!12)(B!!(HO;A6%pKIz%n%vCCiA_8#NeMy}Th8ZuicqjRqUTafIOt zw$3#|u>e+&6i->S86mD{QAY{3t5nG{7#q$&cs&$PO2s0LkZC|0pg;;m1f@1~M-`;8 zM-UasTdme4)G@cPJko|m3DLBY2BX@I=Gwl7M6eNont+EIOwFxKU_nCMFx6gj+*E?S zs4)=@Vug$aT41lFhO0x}i1S8pTek^b0|lbN=7f@jcz3A^RWLdBcvL{dX%ig6SQ8jB zgNaZy9-(fk7COw`tkUu~t4fg0lLqP8U%HV~n$HWA%KGcO)BYGc^v;fgV%ZWA5c$Q2Wtj5-9lGZImwZcBY=6Q)4Y zr4w5Mi7gD%lAv9BT{sknb!j>13>)iPBA5@|Eyjj$BPncb4jWe`uxxIjGq>$D$0hWO z&J29IUmP8E+6?2vfzi;hmfwtH|hbT9k|7G0_knI!ByZu zOlI82UxM^Qr0bEMj2kcANRJ}jhqMd#tPde=$L;n3q<0}bf%F*Cf-Lk2zx6CZ`X!|6 zk(T18nQo+XP1lKG+fr@Vy4|+ZCgnP*Pes7V1}(or-yqAF;$AVOU{&74E@zK%`Kgzj zbAHh*s!BY|@pu3yvZu-7ONgep5!g6}@?so0%j2S><3Tx2JTmHW}}M@Mg<3Q}(0oYdnMl-ImA zI{Io_`LC1Z_oIBuAig6ZP1bkZr1T{DF96>R-#RI++>CUp0Py!qkoL5vy77qSd8Ij zL3r{)UHx26FAmTZVn%v#fUW|aAOfJi{0&}}!O*FzgZX9M%4PgyPWRCh=(q&;Pt`P* zB6U^q%SaQLmiF*Fk#l`-Ad36N6<2JWgIW_LVak`Gv zCQds!y@}JWar#|OALjHqPJhkm+nj#PY0g63{7Iai&FNxJS8%$H(D!!s%xTW~+&-sgbGn$*6`Zc)w29MBPH*D$Yn*oG#{c1*hvcZQ`_()0;T`8mHgo^kGh) zuBF7WoWZ!nuatO|wk5j0#Hp0Hl(tJXF7YV8!}4v@H7;={CC;Rc=exw0lz5Uho`4cZ zQsPG1=4mwmixOYaMjJ9NiL3Yww{I)b`14Flyu@=^{!5HY zoWpsH4_k4zr^G#!IEVBD6JU)rPH-X1p;_gYDy$Rf}Qo7-eOXBHG zDAKgV(UZ7&6WBi`UY^9qo8Z;_5*Kej%cIBL>5}+&67O!p1saz)cM{ib0{fxFvy=FB z6L{W9+&YON0aft^f@!uw~o)Y&>;=E1b@sjv%63=ZC+fCxQN!+$cJYEv7P2#g%%JL;H zo5W$el=YGLYZ7lQmvM=+CUMnrSs#g~Ch^m9Ss#g;CUMeoSs#gyCh^edyA)j#2TkIh z$$3lSok@JN$*hmWHIq1I7cnmJ%Oqae6vib^nZzZ_V>}s;>`JYN`VN7t#2u43V{Yb` z_+k=I%+2~s95IO-=3#z`7bfw+rZO&Z!6XjY*^Ep4FNyaxn{kQrC2_sxGcNJGB!1Ur zj7!`uiPLo%>nZWMy4UOeUd;RwhfCsatzlf^ZS75NNUtt8I!C4QE~%gSediIXL9 zu})(C0hTNAuTElqiF+k+u1;oL;#)~Ps{+O)j+Ml%Dq#5%uS()mox=PQmrCMLox=PQ ze@fy_tz~|RGbM4NPG$d>cv4B6?_)QY8$;IoT#A%YaOs-Nr{t}N#;xD;)y_L925@*R(r1>SjlEhPT@qCv! zN)k88#p|=gOOp6Vt`aRr;vz{LBp0s_68}iz9l89PU*a4|Tq76TS>hQ<{32JS=9jod z5~s+;`;o*al6XX}IhtSM5J}u2SCz&k-jKu>a`CxQ;tEL|A=h%vFY$vUUXW{v#wAXW z#07GBH7@ahB>qo{jPKRDT;lwcF)s0aB%aUdj7uCJiQ98J+ehN{NPM0O=9joU-fMI_ z%NdvWI}&e){>}+q5@$!^>da(Z;^|2IoCS=RvRsLiGmCMFk0bGLyo^g69EoF7&iYHd znW6)I7-{-aR-&~|6wXyG6<=%U0lHd0GDY&0mfl2Up+(D3v6ZobCRkSFZOZDeI-cY(N^B(REfp_;d$SGk{~ zXEW@DS*|cz$gS38P8U7dmRqaKCb{kgPwskM=60P8{d2F<)O1$>X3D*Kp3#r1ZH1Y0o5^;@WU|yC%BN5VXB+lsCZMc^^vo;CP0*jn)B1Ml zmJHXG)Ka(TTTY@j6Pvxz>7qO3xmz^lcKwW6{DP(mTv6)h7jHr<_Cl}gcKA;2RxM?Y z>jGFj_XbVPBVRcWjZ7@bjokr(x)a;T-ntV#3RRu>($kQqJMkT`s!q%|?C$~0y>Sze ziQe3w|8Ekax_?Y%Rn1?dy8oKgoN3taf+ytu@*og**2EcQxb@|^9gyL91wLc*&;yEg z&kx9VJtQE@6GcATa{^|_@w`C8<@pHRc6y4?!3m!0A;{(VB8Fo z*}fIMbS_+h?9|6lm}UFQE7aisfZgz11jOd~353}_bdq*>&W9q|p4~L=p8Ifyv zggSGLsjH!FmhHraB;pe=_1HXlfbi!!??lBcTaNAXRJ0DPO zJ%)lT+ex+uh^rKxFg)~Rugz14{~aDWh-Q0iXg9}0e>5-G^EmQO&v8_n;F%15m*-{h zO!WK=@+KM6{;s-IW_yX`t%atB=g;W0*YgAX-*0$s0^1<8+k%c}*=lTmBffRe%kbQ) zv}=WYyJsB??=Yq>L#3&<-L_ZJv;4ErA=_7N z#N?!W4{7~Ft~(_k9WiV_W->3oizFXn(w+Yhi8{=r$C$nWz4zLlv2BA@bpQT`3%P$k zWzv?vo&+Cb(w;v`f`7xLBmZaA``@xsS^4z6$o3kS=H%Z=#`pu1x%q#jx^FY-%)g10 zdPnJCFLdV*Qvd#>7H)fC0T$LBP%f8d&-4Q1=GZ>6RYHJP_pdC4)%}b~TmCU>a+FDX zJ`J$VZr5Yy$iI&?$YC-oKbH(XfywOrIMtoZWKRAMs52fWbMsH7(&;%3hi}gX4>n~Jk~&TrW$;?>wyy_plFB=; zB>O#Jzm5=nVR>|T}Hd-e8_Ql9zbWZJni^D+p`*W$uXvP zfpd~=lH(7km;W}^nBrKi3~%Jq4;?lSlUeycA^T2ubbu3^nBg2G7o6qz3+No@NBI9_ zn+>81=szI2NT#2KaWA!<;}`&uzlaPlPvvR6J|OY)9ooIvCl)%?ieES(|NA6;5tFX` zH4tFCh;^Byby>!`xV7RwN4-*fx>kG@lLh&ENz@wEMfg+xy;uNkpL5)#nCIlb4MW&! z9lu8d_QI0@(pEB{mA9 zyNV~kJ0?tm_@nk!6Cl=gEiIFkWD7dcJV?}9t+UfL2xdEvdX5H|R32>q;!3_G>g%RND+`X7UKuJdnG8W^s7iCVNmQBKzjsOP#+Q!dv{u#M{? zs)VWHx*a^OOLbX+>qltNb(t>ny4tB;six+*=-}ci*PJD;1tevKrWU&V(8N_in%N64 za2-U?U6l{37VtOQoeu-gandkN_%`84#O4 zY^7N?`KI7mjCy>%2yrvW{2#t*<+N2F<}0Gz$|CB(W1B0Cd^NBB`=zL0GzW_rJYjg z^pA4Do@G-zvGC@qNlj-nlwmYg$G}7#bL`!>t*}1PWY}|TROZ;be_NsQjaIezK7nGXbI!$#U$yof|#b(r5uU+EN%c z?589(IQ=fFe7?;_lS5jXb1_*DQxc}#4vloT&gT{n#Lfsec2gx9Fvs3|V!E4)Y{cu> zd+ZCkn~S-TjfHqX*sx!ynt=U_u0}Qc65Ab`J64E?G3}SwXpyw0X1gB-`Wtw) zfnqkw)40!|WdxOI3aKGc7D(7=pCvzl3szz}sV|HkV5+UljoG$EBA8Yv;lI6M2Ca%odfGh{OCQ_t+usQFJOf_RUHi``(R3U#B%-CzI>vpoUT8qowH%b=-IC9l4>X zl<2S79#E|h-B5U)VgEYIt1aA=BC3i`qY1c=jr7n3~QTq>62WcFY+#DwjYmW0PsCABw{s6eW$ZnHSkw3IdXRc{eF*?fnMRv7m>|I~j zXxPtYqGj$h`g(~g%4w0>9p-TPM%*;8kxSVZ+SPg{ve(fUGp>a90UyM=tK&TbPG zUM~eBF1l}b!8G1zh(XO>zxK5&rT*Nltob)eRV|}1if4=MSKHNoxVLKVJjVN^9Dh{U zuy5k>mZH7nf35n!ySEWCPJe-haF5+~69`d8{^)cMlzNnu`X+U1kDYk1DDVupm9=Wo z%hhdm-cgEZzq8+Nrhw?bScwku zhq?!$%a!=jID^hJVA_uwSkG?I7J7kosnVl!%&sSE&$gmlXj1&bPA*52RZY%c+GTR8 z#qU>2EiAm~m|=gBm254VOI2TDvSse|%g`jQ$9y`~Gj-5TU(6kQ2YIHxZr3w);0Buc zZ*c8Knzll0q-iU}HFF`yQ*dddErgFRiHGEhf-z2q|xNE&Ba6qVg;R73W?jYUt; z=+rXVQuIygRvnYAb6=r(iR&V6vWUJT+2dTk5l*~{6ztOqiemDY;`f93x|2k~zH|lq z^j8p3urH+`&8yuGJ+E5y2K*H+-3XJ!NQHg6MQKU?#=4PX7{5!lxG%lMed#UklNO8U z4!ZqL>GWAqyvSld?6}#lx=ypBlC1KSZc>_(Kd-(KP0l)5I{6eg*)rqXG@wth&_)P3 zN*?gM79y49Z?5-4$lXaH&!_ACeELv6FM8{t{24dFWA}n|LyzqLGAZy54fP+j0@ASj zQF$-4Eh>;+{4rgDKc*Y*kLd~wCtFm`^G;G9US~Gd#EypMMknGYyq9Pv!poWX_UbUAUZgmJ zQpxS$Q%BGpM$vMT+?cf;&;-ZEkmjOWL}yKzHqkNVv?(6jRGXWaPBI)*E}nSF#ENNH zt&5C_6E8zK?j3O!3TNj5#nFip;>tbSpv1{Fvx%56&E}XgaUuz_fpkqyN?igso_Mk? zRW>PA2E}c*ifL41%9#*4AI;=VGx55!OoV3+gwDVyPt8heY+91cCP~wiswgs_n}eF9 z%1P58eWMl3m1;ug{c7@ARZ15J}yys-bh_~QXz(xyOdtCG3<)-#doz)E^4>Pw$8iK7X0F%NlH+CG) zIBm|u4?NI&*WtSkA9g%wD|Y_uV*6$JS$4+_4c?7Ra19=u|~W6m=_ z_~@|XL!#e$CyE&Yjo?n#x0yPQvU_HK7h zzw565a$fPke?8dg+|aw@ac6I{Gx5P;=L3(~XHEOyH>#0u;eY$La-7qgCpqUQezSeY zVP~iFs+R4}iktA+j*sKA8y`RX_~VXSw-GO?HNvUmBg5I`_~ZuXRL7)C33J4~j$JnU zJeza+6$=jc&OiLt2OoTJSe0zP(J>*uz)4zN(TkrgvurQZe_v`zIPZF3;R6d4Q}j`X zC*JuHdUE)&w_kSNWuHCGdFEpuefXBM=&i(jCCLB6lg-YnowE+VTypqjhpYJT!o%Bt z_JQ+?&gjw#$3*)An{$D)aA9w+bE$5&$uZ&mvz;xDiQn3=(0S(euO&L!Dpx(|yvq4g zF^2Q7^9ARIZ+AOCaK7C9cH$c6%Pnu=|7p%?zi}2l=A60Zr6<00@Jn})%$)mknp>P_ zI%g#bN0Za5h*dd`Gn|(?|L81qJh<&qfhIY&dZ+fY~HY;`@!xG`{k!LKi}1S)u!f{{pj&dcAz=sVwX&VTFh6$_n9 zVbhO3bnad7o5X?#oOd}N>i%#kGSku3rHRB(A*o9Xf}ATBC@j2TL#M_%n=6+tJbdtF z7;Lq(8UJ_v@dGt5&MQCq(9!g3$7x=Oao&Y`$^XL6yAt5taJXy3`MvgwrtLLfU5?vr z#XI3e|K&`q?w#-KeIQYyI=N-N!*%}y^PP9?C$iQt;i#oF|NaejJmp{uS4AU@iH5jQ zUxwFcqwRSA5$}cK&CO=KeB2r6z+1qT70c>smIbPPpR4oL)COvNwSg7Bvf8?8pA?l> zmete*R?~kqwQH-(R{D&3UwdOm1dm#jH8$d_Ld*yRD$4__*RJu^mQ`=yl_e04Mer76 z!=_Li??%TOqT!A>-pa(w?V(^S6lhPh)rX>}xd|^W8?lx|yfM;+_srE|B5jLW<(0m& zYO7+k)n#jHR`{v|wHvB@fn{}-SKyUhRXu=3r!f$RaJ*={C4^l4@=#~EA%qD2<*h*| z9F0Wc+6H*17<^I@?+VAk&Q{S~FRNQ#v6hxdyayS_YsT&MSiPWivvjl>Fz$E(F?Uqi zq++>mO>M=Bit@7BinVJDbUJ`23W*SUya})6hZ+OX5P})t)$2xR(H`n1r5o`orO0j| zbLYxDDL{EDekXZW~lWy)uZLLfr>S?zUmca<(2`iSWP3F zvIIt#Jf;=zsK?@>O%tLC1XM{hf!CuE2%$aJq(%{Ar&9Pqt*_Eo<%ctfHfXe_wz^`? zO4Wb7(G5E)C!`)lI`I2}-g}br(5YY?Q3KjTT>;{Ovtsa!L@d}Gf=e}rM6&n?x$Saa zO?h=i69#~X z)E$T!^$m^7*^$~~;pX;Gqqm_2udid3KzxiC0t|FlMY`a{Y$UiX4;bE;rf(dKCwt_p zuExLw8{!F?CZYCb3_ZE4vYB2IePuORN>^hMklNbEsxTce4Pn`iM4Z!!I+7(2E{uOXaq0((-PSfjzWD>JwdBQAV4d& z$VggcZCx$RMcJBaanKc>WG~Ch)|C4yE1~HapASbv4Qer|C(ltET@7Ysi4jJW2F%bk zdWWMGArOz~g-(1jZgk)ce?*tz-NdK|x9qCP@zX}c#92*CT4WPqsnC$XVJXl|IEJMd z?qk%~Y(~VCw7qzZGE7@p9D`C1|F?+5EUH<%rmQmH_mx%F`pe6#+kN?3xF;N^##dff zT~WJ%CJ)XDVuX~x1nAr^R@3_G1uIZf@hynzE!N93+2l%?7_Efd`b*0%DCctX2rf#*bx}rS2^(!l?vHxH_I$bHuZTM^gEyb#fOE=F?+r>I!!?7*XoBh6YPoF)R{WDsM5Vr~Qi zN>QuvF;l`FVGJ!88X^g#+*_=5aJ3Z`E0bM8oR#DVNq?xDF*FcpLfjz?oh+EzsMy%` zHl$|+wkkTD81q{qZK3%HaCChl(LKL85@~D-#m|d%w$G0v!px@m4XvaQeeve6#K_`{ zwmKEIAZAW$$bbQw)ZT_uE81<>Co}6nDYu~sQ@ys*=&Fq%nogzO4A=#e>#XWz7ecv& z)OMcQ2d&eywn{9BlTdqOFiM+>@?(?X=y)g7N8sf8h>cwvwys(qJy~l#h_t_i+x5y? zFUlsTYC^V0nzN~al64bf*hE_@Qgt>ICluFYC!~#g!D6_N+NEH@U?XBPwIb#lmSVPy z93b`VB)3iTD;#zjaS%{nVMMyx+G#36vQ&?WAJ$mJuI&V3@i$Ffu&>BH#81@O8y5Y3%W8=t6ExuTx<+OmM zuYv436nU4 z4UxX|sevnBTUl9F>%)vu-wfGqYX0!IW7>Xs2OzC6C1eg-7D!ezwZq{X&+-7Bd!SnC zNN>>Us1GP<9ZR|^#Ha2LCaoePy|&hmqpbRlMS5tzpz|0V$GYf@jk!t#O`E>%BF&xT z-o_peCb{SqDP4A>iz4@^3pX9r1x(&fRG};hv>Tmnsh!YlsxD`fiIjDkcqG3$g}EjF zQWEQQOa9kMtPA)2(=RD|*~BCHS5lZ;^1tEEk^Yi@C5erdFJ-TqcqIS(6y}!vS-5{o z7s;3W*-31yd?}l4;*tD0DaylWfTk`bkgl==Jd?{-+@kl+EpT;`fl7BRbjg>EDhfO?^e}$v9a={Y`=*|@^_~&x8%>q(_126@)snrvGS#Ck%>q0OH!Cy@}o(t z(=B-eKRoI-$I6#7`f*ZtB%hVS+>)P_#5&!QFHB-%?v16xO@>`PFSou=+ z1rv|tZ%AQo$$vkIb-E>gUlJQDU&`(`@ksum6y}!vt4XZWE&1Ojv9a={>~|&}$q%M5 zx8x_Cl(LWHCnvG7@}+EwiAVCD6y}!vElI4?E&1I^Y^;1K`-+K2@_SR5Tk==naU;6O ze&r6yu3b4XFpw<`hPoqI;QJXE|J&1<n0`A+bf zJ&V^?dXJc{^EuU2>VH2TnxN~qcnXTH_c{H5Q;nwn-|~ORZxqKix5b0?NaInRwn(l$ z5)T#QSN`JqM7Xu_JRIs3)e^+F#Nx)y?Wn0!+~rjzo%jw#zh#(-0LmzQG*#eS2SV`{ z(~ky5F}}3p|0WP9i>PlL#i162SH}0wKuaU)3S|^G;K#Zc8q~>k2*OG-gKcgl!Cw?h(G3w&) z!pZs8p!XZ;lJt-!=?M#UNqr`bCCHFZOZ)xIC}|aS4wvXC^?9Bdr6|LL9Lf3;*H2P< zbklsXd6JMHIf|(#?MoazNx#XQqQCG-T8DCq(I<2PZ{U)aaD7sP+7|s8HX4y3*-~HP z0!k`z0i}JZFa2+~)ZcfmmMp2nC8YXfXKVd#V5AqG1X7oCL3%3faB*@Rg^nR(uJ7gg zk{+bNF>L1{^8Iry$jGS8A-*2hE@AH~b()%s;v$p?zt}krRh5~baKiBtj{UL5YQ~eh$^-Duu z#abHDH4a)fW?ibw=zqF#Lb^!*N3QpRGK`VyRgzJEU1}JwAww#b2*9b-pJBnJJCY8e zT=HhtNl{H%rjP%~HpMd!mONo{Dbgpmh=YzNDNvVjAA^UJT?n2ZPGotej?9j4rnoAuB-y!>9IzHKueJ&lJV#xlLj^`P&FQwydL-w0= z++)aol8#Ts{@^u3DvzKgSog=nr(-=I56{OsJsy6NA?vVVx*W#IhODn982(p)^>REp zmcs*Aa?r#3<6I7dPSayAhl&C|9)9{*5QxJ#BLhA&1MVGz(>%40Gww9svyAsftrcjU zw;5x{>l}rT9j_${A3I(?;MDF2pTj_^>uUUOGuc_zk?R;g!Szymo>s{E2aS z1zwt0;c94Cj(QcRoaZREJ+&*Zu&cir0FJX5m$%d_RV~9<#JIePPIjlu&$zriezS&+ zYZ;eUyzc}}R~zH zN%ru0hJs=tPLi=P=+zbY|Nmxw>pbmO_Ml%t@Hgg;8UTGyh8gb%ct`gf5l2` z_tOmc6l`uJN8aptl4}(xJWYfmiZb9Au^f4KN|x^`;FcqTk`u}xXB*3r_qs&RH!|=) z$o%rUGJRj7>lw!7l{Vh)4GINC{grp^exTV6`XR}Ub;G(&e!~3nf*dC;!(hM_zlDxVdj; z;HMwx$gkveT=lm&&`>tMn2jf&;t!68muA506h6t2_XuTPL^JT;o&o=U2K)(@FYgrg zu#sK@PV>Ua>$%wOl?-zJ%yJ~SgZi5)sPq}jvCc0izTA#yhnX4hOEci~s@8bx%yk*? zt_=818RUN>1AcD?{Miin+ZphGWWe)re{Z~god?`2gJDX`fRAU-4H@u`4EXj8_&va9 znc9{i@&fLHyWbxSR4-y z7dPOElybZ@5U8xc`!4}d%hRY;<))&>NT9hjQjh1s)O~UM{l{)RB-qx`8p7j3#fz3K zI^UoIJj&lq)FM0-L$}r&6K(htciax1i&D*~hnE9FQ8SWHwW4mqt5(vUz~N_{GQEw# z#pA!0Vt#$V@_vQ&37&DDq#pk{Da(UgsTU#AANn&tL}`AUiJoCnkC?OVlXk#+0{nDY zVEG0-(u`+)11l@nF2jS%YgeqGmltZwmR0%!>hUM_A_;Y&v~(pNQ7R8CEM8c=2+tqI z8rnM2F?vc#KOo4aQg!jeLwiHV=CQ2QJ$d{PO!VfDJjX4ME~(#FSeSaWD*gQ!YLKL) zJ=4dc)Ehcu8&!iL&mq#!JR-yLh!?@*JsTScso!OEar$dZqC7o;hBsxx@l@;Q7cVsM z^gMnb;?BtHTFK|dq=34E2dTrI_=A*Vp0(r3jiGoDZw73_i$ozznJAc}QeQure7`?R~5b31VVTr zD~<=>`FVf+3Y{9~^jgwISp?MIi5BoWydRmeD#jd|aF3PK%A?N$npY6&D-U7nO^cK; znzm!63XPIkzS#vB$Ge2#HW-ng{7ecI{aL0FSk)P*#@|=PpQ(+i2b1v=u84@>3E)U; zYPcAWCF)XQz@riRw|0h=338zuowHVg7w asp$=7bU^$uQT&zMl