new project started (working ls implementation)

This commit is contained in:
Kovid Goyal 2006-10-31 17:35:03 +00:00
commit baa766ae3c
6 changed files with 610 additions and 0 deletions

16
README Normal file
View File

@ -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.

207
communicate.py Executable file
View File

@ -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])

163
data.py Executable file
View File

@ -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])

27
decoding Normal file
View File

@ -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.

101
pr5-500.e3p Normal file
View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Project SYSTEM "Project-3.9.dtd">
<!-- Project file for project pr5-500 -->
<!-- Saved: 2006-10-31, 09:35:02 -->
<!-- Copyright (C) 2006 Kovid Goyal, kovid@kovidgoyal.net -->
<Project version="3.9">
<ProgLanguage mixed="0">Python</ProgLanguage>
<UIType>Console</UIType>
<Description>Library to communicate with the Sony Reader prs-500 via USB</Description>
<Version></Version>
<Author>Kovid Goyal</Author>
<Email>kovid@kovidgoyal.net</Email>
<Sources>
<Source>
<Name>communicate.py</Name>
</Source>
<Source>
<Name>data.py</Name>
</Source>
<Source>
<Name>prstypes.py</Name>
</Source>
</Sources>
<Forms>
</Forms>
<Translations>
</Translations>
<Interfaces>
</Interfaces>
<Others>
</Others>
<MainScript>
<Name>communicate.py</Name>
</MainScript>
<Vcs>
<VcsType>Subversion</VcsType>
<VcsOptions>(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.</VcsOptions>
<VcsOtherData>(dp0
S'standardLayout'
p1
I01
s.</VcsOtherData>
</Vcs>
<FiletypeAssociations>
<FiletypeAssociation pattern="*.ui.h" type="FORMS" />
<FiletypeAssociation pattern="*.ui" type="FORMS" />
<FiletypeAssociation pattern="*.idl" type="INTERFACES" />
<FiletypeAssociation pattern="*.py" type="SOURCES" />
<FiletypeAssociation pattern="*.ptl" type="SOURCES" />
</FiletypeAssociations>
</Project>

96
prstypes.py Executable file
View File

@ -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