commit baa766ae3c1906337f749af3f61b314ee5d18a5d Author: Kovid Goyal Date: Tue Oct 31 17:35:03 2006 +0000 new project started (working ls implementation) diff --git a/README b/README new file mode 100644 index 0000000000..43046d2b9f --- /dev/null +++ b/README @@ -0,0 +1,16 @@ +Library implementing a reverse engineered protocol to communicate with the Sony Reader PRS-500. + +Requirements: +1) Python >= 2.5 +2) PyUSB >= 0.3.4 (http://sourceforge.net/projects/pyusb/) + +Usage: +At the moment all that it can do is a simple ls command. Add the following to /etc/udev/rules.d/90-local.rules +BUS=="usb", SYSFS{idProduct}=="029b", SYSFS{idVendor}=="054c", MODE="660", GROUP="plugdev" +and run udevstart to enable access to the reader for non-root users. You may have to adjust the GROUP and the location of the +rules file to suit your distribution. + +To see the listing +./communicate.py /path/to/see + +If the path does not exist, it will throw an Exception. diff --git a/communicate.py b/communicate.py new file mode 100755 index 0000000000..8944468075 --- /dev/null +++ b/communicate.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python +## Copyright (C) 2006 Kovid Goyal kovid@kovidgoyal.net +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License along +## with this program; if not, write to the Free Software Foundation, Inc., +## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import sys, usb +from data import * +from types import * +from exceptions import Exception + +### End point description for PRS-500 procductId=667 +### Endpoint Descriptor: +### bLength 7 +### bDescriptorType 5 +### bEndpointAddress 0x81 EP 1 IN +### bmAttributes 2 +### Transfer Type Bulk +### Synch Type None +### Usage Type Data +### wMaxPacketSize 0x0040 1x 64 bytes +### bInterval 0 +### Endpoint Descriptor: +### bLength 7 +### bDescriptorType 5 +### bEndpointAddress 0x02 EP 2 OUT +### bmAttributes 2 +### Transfer Type Bulk +### Synch Type None +### Usage Type Data +### wMaxPacketSize 0x0040 1x 64 bytes +### bInterval 0 +### +### +### Endpoint 0x81 is device->host and endpoint 0x02 is host->device. You can establish Stream pipes to/from these endpoints for Bulk transfers. +### Has two configurations 1 is the USB charging config 2 is the self-powered config. +### I think config management is automatic. Endpoints are the same + +class PathError(Exception): + def __init__(self, msg): + Exception.__init__(self, msg) + +class ControlError(Exception): + def __init__(self, query=None, response=None, desc=None): + self.query = query + self.response = response + Exception.__init__(self, desc) + + def __str__(self): + if self.query and self.response: + return "Got unexpected response:\n" + \ + "query:\n"+str(self.query.query)+"\n"+\ + "expected:\n"+str(self.query.response)+"\n" +\ + "actual:\n"+str(self.response) + if self.desc: + return self.desc + return "Unknown control error occurred" + +class DeviceDescriptor: + def __init__(self, vendor_id, product_id, interface_id) : + self.vendor_id = vendor_id + self.product_id = product_id + self.interface_id = interface_id + + def getDevice(self) : + """ + Return the device corresponding to the device descriptor if it is + available on a USB bus. Otherwise, return None. Note that the + returned device has yet to be claimed or opened. + """ + buses = usb.busses() + for bus in buses : + for device in bus.devices : + if device.idVendor == self.vendor_id : + if device.idProduct == self.product_id : + return device + return None + + +class PRS500Device: + SONY_VENDOR_ID = 0x054c + PRS500_PRODUCT_ID = 0x029b + PRS500_INTERFACE_ID = 0 + PRS500_BULK_IN_EP = 0x81 + PRS500_BULK_OUT_EP = 0x02 + + def __init__(self) : + self.device_descriptor = DeviceDescriptor(PRS500Device.SONY_VENDOR_ID, + PRS500Device.PRS500_PRODUCT_ID, + PRS500Device.PRS500_INTERFACE_ID) + self.device = self.device_descriptor.getDevice() + self.handle = None + + def open(self) : + self.device = self.device_descriptor.getDevice() + if not self.device: + print >> sys.stderr, "Unable to find Sony Reader. Is it connected?" + sys.exit(1) + self.handle = self.device.open() + if sys.platform == 'darwin' : + # XXX : For some reason, Mac OS X doesn't set the + # configuration automatically like Linux does. + self.handle.setConfiguration(1) + self.handle.claimInterface(self.device_descriptor.interface_id) + self.handle.reset() + + def close(self): + self.handle.releaseInterface() + self.handle, self.device = None, None + + def send_sony_control_query(self, query, timeout=100): + r = self.handle.controlMsg(0x40, 0x80, query.query) + if r != len(query.query): + raise ControlError(desc="Could not send control request to device\n" + str(query.query)) + res = normalize_buffer(self.handle.controlMsg(0xc0, 0x81, len(query.response), timeout=timeout)) + if res != query.response: + raise ControlError(query=query, response=res) + + def bulkRead(self, size): + return TransferBuffer(self.handle.bulkRead(PRS500Device.PRS500_BULK_IN_EP, size)) + + def initialize(self): + self.handle.reset() + for query in initialization: + self.send_sony_control_query(query) + if query.bulkTransfer and "__len__" not in dir(query.bulkTransfer): + self.bulkRead(query.bulkTransfer) + + def ls(self, path): + """ + ls path + + Packet scheme: query, bulk read, acknowledge; repeat + Errors, EOF conditions are indicated in the reply to query. They also show up in the reply to acknowledge + I haven't figured out what the first bulk read is for + """ + if path[len(path)-1] != "/": path = path + "/" + self.initialize() + q1 = LSQuery(path, type=1) + files, res1, res2, error_type = [], None, None, 0 + try: + self.send_sony_control_query(q1) + except ControlError, e: + if e.response == LSQuery.PATH_NOT_FOUND_RESPONSE: + error_type = 1 + raise PathError(path[:-1] + " does not exist") + elif e.response == LSQuery.IS_FILE_RESPONSE: error_type = 2 + elif e.response == LSQuery.NOT_MOUNTED_RESPONSE: + error_type = 3 + raise PathError(path + " is not mounted") + elif e.response == LSQuery.INVALID_PATH_RESPONSE: + error_type = 4 + raise PathError(path + " is an invalid path") + else: raise e + finally: + res1 = normalize_buffer(self.bulkRead(q1.bulkTransfer)) + self.send_sony_control_query(q1.acknowledge_query(1, error_type=error_type)) + + if error_type == 2: # If path points to a file + files.append(path[:-1]) + else: + q2 = LSQuery(path, type=2) + try: + self.send_sony_control_query(q2) + finally: + res2 = normalize_buffer(self.bulkRead(q2.bulkTransfer)) + self.send_sony_control_query(q1.acknowledge_query(2)) + + send_name = q2.send_name_query(res2) + buffer_length = 0 + while True: + try: + self.send_sony_control_query(send_name) + except ControlError, e: + buffer_length = 16 + e.response[28] + e.response[29] + e.response[30] + e.response[31] + res = self.bulkRead(buffer_length) + if e.response == LSQuery.EOL_RESPONSE: + self.send_sony_control_query(q2.acknowledge_query(0)) + break + else: + self.send_sony_control_query(q2.acknowledge_query(3)) + files.append("".join([chr(i) for i in list(res)[23:]])) + return files + + +def main(path): + dev = PRS500Device() + dev.open() + try: + print " ".join(dev.ls(path)) + except PathError, e: + print >> sys.stderr, e + finally: + dev.close() + +if __name__ == "__main__": + main(sys.argv[1]) diff --git a/data.py b/data.py new file mode 100755 index 0000000000..d55e4a8198 --- /dev/null +++ b/data.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python +## Copyright (C) 2006 Kovid Goyal kovid@kovidgoyal.net +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License along +## with this program; if not, write to the Free Software Foundation, Inc., +## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import sys, re +from prstypes import * + +# The sequence of control commands to send the device before attempting any operations. Should be preceeded by a reset? +initialization = [] +initialization.append(\ +ControlQuery(TransferBuffer((0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0)),\ + TransferBuffer((0, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)), bulkTransfer=24)) +initialization.append(\ +ControlQuery(TransferBuffer((0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 5, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)), \ + TransferBuffer((0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)))) +initialization.append(\ +ControlQuery(TransferBuffer((7, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 128, 2, 0)), \ + TransferBuffer((0, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 7, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)))) +initialization.append(\ +ControlQuery(TransferBuffer((0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0)), \ + TransferBuffer((0, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)), bulkTransfer=24)) +initialization.append(\ +ControlQuery(TransferBuffer((0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 5, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)), \ + TransferBuffer((0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)))) +initialization.append(\ +ControlQuery(TransferBuffer((6, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 45, 49, 0, 0, 0, 0, 0, 0)), \ + TransferBuffer((0, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)))) +initialization.append(\ +ControlQuery(TransferBuffer((1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0)), \ + TransferBuffer((0, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)))) + +end_transaction = \ +ControlQuery(TransferBuffer((1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0)),\ + TransferBuffer((0, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))) + +def string_to_buffer(string): + """ Convert a string to a TransferBuffer """ + return TransferBuffer([ ord(ch) for ch in string ]) + +class LSQuery(ControlQuery): + """ + Contains all the device specific data (packet formats) needed to implement a simple ls command. + See PRS500Device.ls() to understand how it is used. + """ + PATH_NOT_FOUND_RESPONSE = (0, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 24, 0, 0, 0, 215, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0) + IS_FILE_RESPONSE = (0, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 24, 0, 0, 0, 210, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0) + NOT_MOUNTED_RESPONSE = (0, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 24, 0, 0, 0, 200, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0) + INVALID_PATH_RESPONSE = (0, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 24, 0, 0, 0, 249, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0) + ACKNOWLEDGE_RESPONSE = (0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0x35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + ACKNOWLEDGE_COMMAND = (0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + SEND_NAME_COMMAND = (53, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4 , 0, 0, 0, 0, 0, 0, 0) + EOL_RESPONSE = (0, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 53, 0, 0, 0, 250, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0) + + + def __init__(self, path, type=1): + self.path = path + if len(self.path) >= 8: + self.path_fragment = self.path[8:] + for i in range(4 - len(self.path_fragment)): + self.path_fragment += '\x00' + self.path_fragment = [ ord(self.path_fragment[i]) for i in range(4) ] + else: + self.path_fragment = [ 0x00 for i in range(4) ] + src = [ 0x00 for i in range(20) ] + if type == 1: + src[0] = 0x18 + elif type == 2: + src[0] = 0x33 + src[4], src[12], src[16] = 0x01, len(path)+4, len(path) + query = TransferBuffer(src) + string_to_buffer(path) + src = [ 0x00 for i in range(32) ] + src[1], src[4], src[12], src[16] = 0x10, 0x01, 0x0c, 0x18 + if type == 2: src[16] = 0x33 + ControlQuery.__init__(self, query, TransferBuffer(src), bulkTransfer = 0x28) + + def acknowledge_query(self, type, error_type=0): + """ + Return the acknowledge query used after receiving data as part of an ls query + + type - should only take values 0,1,2,3 corresponding to the 4 different types of acknowledge queries. + If it takes any other value it is assumed to be zero. + + error_type - 0 = no error, 1 = path not found, 2 = is file, 3 = not mounted, 4 = invalid path + """ + if error_type == 1: + response = list(LSQuery.PATH_NOT_FOUND_RESPONSE) + response[4] = 0x00 + elif error_type == 2: + response = list(LSQuery.IS_FILE_RESPONSE) + response[4] = 0x00 + elif error_type == 3: + response = list(LSQuery.NOT_MOUNTED_RESPONSE) + response[4] = 0x00 + elif error_type == 4: + response = list(LSQuery.INVALID_PATH_RESPONSE) + response[4] = 0x00 + else: response = list(LSQuery.ACKNOWLEDGE_RESPONSE) + query = list(LSQuery.ACKNOWLEDGE_COMMAND) + response[-4:] = self.path_fragment + if type == 1: + query[16] = 0x03 + response[16] = 0x18 + elif type == 2: + query[16] = 0x06 + response[16] = 0x33 + elif type == 3: + query[16] = 0x07 + response[16] = 0x35 + else: # All other type values are mapped to 0, which is an EOL condition + response[20], response[21], response[22], response[23] = 0xfa, 0xff, 0xff, 0xff + + return ControlQuery(TransferBuffer(query), TransferBuffer(response)) + + def send_name_query(self, buffer): + """ + Return a ControlQuery that will cause the device to send the next name in the list + + buffer - TransferBuffer that contains 4 bytes of information that identify the directory we are listing. + + Note that the response to this command contains information (the size of the receive buffer for the next bulk read) thus + the expected response is set to null. + """ + query = list(LSQuery.SEND_NAME_COMMAND) + query[-4:] = list(buffer)[-4:] + response = [ 0x00 for i in range(32) ] + return ControlQuery(TransferBuffer(query), TransferBuffer(response)) + + +def main(file): + """ Convenience method for converting spike.pl output to python code. Used to read control packet data from USB logs """ + PSF = open(file, 'r') + lines = PSF.readlines() + + packets = [] + temp = [] + for line in lines: + if re.match("\s+$", line): + temp = "".join(temp) + packet = [] + for i in range(0, len(temp), 2): + packet.append(int(temp[i]+temp[i+1], 16)) + temp = [] + packets.append(tuple(packet)) + continue + temp = temp + line.split() + print r"seq = []" + for i in range(0, len(packets), 2): + print "seq.append(ControlQuery(TransferBuffer(" + str(packets[i]) + "), TransferBuffer(" + str(packets[i+1]) + ")))" + +if __name__ == "__main__": + main(sys.argv[1]) diff --git a/decoding b/decoding new file mode 100644 index 0000000000..b54e1ca2f9 --- /dev/null +++ b/decoding @@ -0,0 +1,27 @@ +Control packets (see pg 199 in usb1.1 spec) + +00 01 02 03 04 05 06 07 + +00 - Request Type +01 - Request +02,03 - Value +04,05 - Index/Offset +06,07 - Data length + + +Request Type +80 - device to host; type standard; recipient device +c0 - device to host; type vendor ; recipient device +40 - host to device; type vendor ; recipient device + +c0, 40 are used for sony communication + + +Request +06 - GET_DESCRIPTOR +80 - Sony proprietary. goes with 40 on first bit +81 - Sony proprietary. goes with c0 on first bit + + +Data length +Only the 6th byte seems to be used. The value of the 6th byte converted to decimal is the number of bytes to be read/written. diff --git a/pr5-500.e3p b/pr5-500.e3p new file mode 100644 index 0000000000..b0514cecbd --- /dev/null +++ b/pr5-500.e3p @@ -0,0 +1,101 @@ + + + + + + + Python + Console + Library to communicate with the Sony Reader prs-500 via USB + + Kovid Goyal + kovid@kovidgoyal.net + + + communicate.py + + + data.py + + + prstypes.py + + + + + + + + + + + + communicate.py + + + Subversion + (dp0 +S'status' +p1 +(lp2 +S'' +p3 +asS'log' +p4 +(lp5 +g3 +asS'global' +p6 +(lp7 +g3 +asS'update' +p8 +(lp9 +g3 +asS'remove' +p10 +(lp11 +g3 +asS'add' +p12 +(lp13 +g3 +asS'tag' +p14 +(lp15 +g3 +asS'export' +p16 +(lp17 +g3 +asS'commit' +p18 +(lp19 +g3 +asS'diff' +p20 +(lp21 +g3 +asS'checkout' +p22 +(lp23 +g3 +asS'history' +p24 +(lp25 +g3 +as. + (dp0 +S'standardLayout' +p1 +I01 +s. + + + + + + + + + diff --git a/prstypes.py b/prstypes.py new file mode 100755 index 0000000000..aca7c2177a --- /dev/null +++ b/prstypes.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +## Copyright (C) 2006 Kovid Goyal kovid@kovidgoyal.net +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License along +## with this program; if not, write to the Free Software Foundation, Inc., +## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Contains convenience wrappers for packet data that allow output in the same format as the logs produced by spike.pl +""" + + +def normalize_buffer(tb): + """ Replace negative bytes by 256 + byte """ + nb = list(tb) + for i in range(len(nb)): + if nb[i] < 0: + nb[i] = 256 + nb[i] + return TransferBuffer(nb) + +def phex(num): + """ + Return the hex representation of num without the 0x prefix. + + If the hex representation is only 1 digit it is padded to the left with a zero. + """ + index, sign = 2, "" + if num < 0: + index, sign = 3, "-" + h=hex(num)[index:] + if len(h) < 2: + h = "0"+h + return sign + h + + +class TransferBuffer(tuple): + """ + Thin wrapper around tuple to present the string representation of a transfer buffer as in the output of spike.pl """ + def __init__(self, packet): + tuple.__init__(packet) + self.packet = packet + + def __add__(self, tb): + return TransferBuffer(tuple.__add__(self, tb)) + + def __str__(self): + """ + Return a string representation of this packet in the same format as that produced by spike.pl + """ + ans = "" + for i in range(0, len(self), 2): + for b in range(2): + try: + ans = ans + phex(self[i+b]) + except IndexError: + break + ans = ans + " " + if (i+2)%16 == 0: + ans = ans + "\n" + return ans.strip() + +class ControlQuery: + """ + Container for all the transfer buffers that make up a single query. + + A query has a transmitted buffer, an expected response and an optional buffer that is either read + from or written to via a bulk transfer. + """ + + def __init__(self, query, response, bulkTransfer=None): + """ + Construct this query. + + query - A TransferBuffer that should be sent to the device on the control pipe + response - A TransferBuffer that the device is expected to return. Used for error checking. + bulkTransfer - If it is a number, it indicates that a buffer of size bulkTransfer should be read from the device via a + bulk read. If it is a TransferBuffer then it will be sent to the device via a bulk write. + """ + self.query = query + self.response = response + self.bulkTransfer = bulkTransfer + + def __eq__(self, cq): + """ Bulk transfers are not compared to decide equality. """ + return self.query == cq.query and self.response == cq.response + +