Moto V3cm module and OBEX support (was Re: USTRING "encoding" behavior different from STRING's ?)
Perry Nguyen <[email protected]>
| Newsgroups | gmane.comp.mobile.bitpim.devel |
|---|---|
| Message-ID | <[email protected]> |
Joe Pham wrote: >> I'm in the process of writing support for my Motorola V3c (VZW), and >> I mostly have the OBEX support complete. > > I was just about to start doing that for the V710. If you're willing to contribute your code to the project, please post or email me your code and I'll roll it into the main trunk. In fact, I am willing to contribute the code, I was just waiting for my implementation to be more complete. For the OBEX portion, I have completed folder listings and object retrieval. OBEX PUT and DELETE (variation of PUT) have not been implemented yet. I would be glad to share my work. Attached is the source of my modules. (my com_motov3cm.py is starting to inherit from com_moto.py, but I actually haven't used anything from the superclass yet). >> I still haven't figured out how to switch from mode OBEX back to >> MODEM. > > Not sure what you meant by that. Please be more specific. Once I do AT+MODE=22, switching to OBEX mode, I am no longer able to send AT commands, the device will not respond; it only responds to OBEX requests, even once disconnected. I have been doing port snoops of Motorola Phone tools on Windows and seen that it simply closes, resets and re-opens the port. But I have not been able to duplicate that behavior on Linux yet. (Then again, I haven't been able to spend that much time on it). >> I noticed that there's the V710 being added recently and it does >> share a lot of common features that should possibly moved to a >> superclass. com_motov.py maybe. > > There's already a com_moto. Right, but there is stuff in com_motov710.py that is common to my com_motov3cm.py, e.g. groups. > -Joe Pham
com_motov3cm.py
(text/plain, 7.1 KB)
### BITPIM ### ### Author: Perry Nguyen <[email protected]> ### I'm sure much of this can be refactored to a moto v-series base class. """Communicate with a Motorola V3cm""" from DSV import DSV from os.path import basename from sys import exc_info import com_moto import com_obex class Phone(com_moto.Phone,com_obex.ObexProtocol): "Talk to a Motorola V3cm" desc="Motorola V3cm" serialsname="motv3cm" __pb_recno = 0 __pb_number = 1 __pb_number_type = 2 __pb_name = 3 __pb_record_type = 4 __pb_record_image = 13 __obex_wallpapers = "picture" __obex_ringtones = "audio" # index 3 = main, but we'll make it 'home' for bitpim __pb_record_types = ( 'office', 'home', 'home', 'cell', \ 'fax', 'pager' ) # record_types actually extends to these two, but we should never hit them # , 'Email', 'Mailing list' ) __pb_email_types = [6,7] _esn = None MODEMOTOROLA = "modemotorola" def __init__(self, logtarget, commport): com_moto.Phone.__init__(self, logtarget, commport) com_obex.ObexProtocol.__init__(self) self.mode=self.MODENONE def close(self): self.setmode(self.MODENONE) com_moto.Phone.close(self) def setmode(self, desired): if self.mode == self.MODEOBEX and desired != self.MODEOBEX: self.obexdisconnect() self.comm.write("AT\r") try: self.comm.readsome() except: info = exc_info() self.log("Exception while trying AT command: %s: %s" % (info[0], info[1])) return com_moto.Phone.setmode(self, desired) def _setmodemotorola(self): self.comm.sendatcommand("E0Q0V1") self.comm.sendatcommand("+MODE=2") return True def _setmodenone(self): self.comm.sendatcommand("E0Q0V1") self.comm.sendatcommand("+MODE=0") return True def _setmodeobex(self): try: self.comm.sendatcommand("+MODE=22") self.obexconnect() except: info = exc_info() self.log("Exception switching to modeobex: %s: %s" % (info[0], info[1])) try: self.obexdisconnect() except: info = exc_info() self.log("Exception cancelling obexconnect: %s: %s" % (info[0], info[1])) pass return False return True def getfundamentals(self, results): if self._esn is None: self.setmode(self.MODENONE) # do this instead of brew to accomodate bluetooth connections # can't do brew over bluetooth s = self.comm.sendatcommand("+CGSN") # there seems to be a bug in sendatcommand, it has the results # of previous sendatcommands, either that or a bug in the phone. for i in s: if i.find("+CGSN:") != -1: idx = i.index(": ") self._esn = i[idx + 2:] break if len(self._esn) == 0: self.log("WARNING: ESN not found!") else: self.log("ESN is " + self._esn) else: self.log("ESN is already cached") results['uniqueserial'] = self._esn def getwallpapers(self, results): self.setmode(self.MODEOBEX) files = self.getobexfolderlist(self.__obex_wallpapers) media = {} mediaindex = {} index = 0 countFiles = len(files) for i in files: self.log("Attempting to read: %s/%s" % (self.__obex_wallpapers, i)) self.progress(index, countFiles - 1, "Loading wallpaper: %s" % i) media[i] = self.getobexfile(self.__obex_wallpapers, i) mediaindex[index] = { 'name': i, 'origin': 'images' } index += 1 results['wallpapers'] = media results['wallpaper-index'] = mediaindex self.setmode(self.MODENONE) def getringtones(self, results): pass def getcalendar(self, results): pass def getphonebook(self, results): self.setmode(self.MODEMOTOROLA) self.comm.sendatcommand("+CPBS=\"ME\"") entries = {} s = self.comm.sendatcommand("+MPBR=?") for j in s: self.log("Phone book info: %s" % j) for i in range(1, 1000, 15): upper = i + 15 upper = min(upper, 1000) s = self.comm.sendatcommand("+MPBR=%d,%d" % (i, upper)) self.progress(i, 1000, "Reading %d to %d" % (i, upper)) for j in s: self.parse_pb_entry(entries, j, results) pbook = {} k = 0 for i in entries.values(): pbook[k] = i k += 1 results['phonebook'] = pbook def parse_pb_entry(self, entries, entry, fundamentals): idx = entry.index(": ") entry = entry[idx + 2:] self.log(entry) e = DSV.importDSV([ entry ])[0] fullname = e[self.__pb_name] if entries.has_key(fullname): res = entries[fullname] else: res = {} res['serials'] = [ {'sourcetype': self.serialsname, 'sourceuniqueid': fundamentals['uniqueserial'], 'serial1': e[self.__pb_recno]} ] res['names'] = [ {'full': fullname } ] res['numbers'] = [] res['emails'] = [] res['wallpapers'] = [] wallpaper = e[self.__pb_record_image] if len(wallpaper) > 0: res['wallpapers'].append({ 'wallpaper': basename(wallpaper), 'use': 'call' }) self.log("Using wallpaper: " + basename(wallpaper)) number = e[self.__pb_number] n_type = int(e[self.__pb_record_type]) n_type_str = self.__pb_record_types[n_type] speed_dial = int(e[self.__pb_recno]) if n_type in self.__pb_email_types: res['emails'].append({ 'email': number }) else: res['numbers'].append({ 'number': number, 'type': n_type_str, 'speeddial': speed_dial }) entries[fullname] = res return res class Profile(com_moto.Profile): # Declare our phone model for autodetection phone_manufacturer = "Motorola" phone_model = "Motorola CDMA V3c Phone" _supportedsyncs=( ('phonebook', 'read', None), # all phonebook reading ('wallpaper', 'read', None), # all phonebook reading ) deviceclasses=("modem") # It seems usbids is only used if detectphone is defined? usbids = ( ( 0x22b8, 0x2a62, 3 ), ) # WALLPAPER_WIDTH = 176 # WALLPAPER_HEIGHT = 220 # WALLPAPER_CONVERT_FORMAT = ("gif", "jpg") # MAX_WALLPAPER_BASENAME_LENGTH = 32 def __init__(self): com_moto.Profile.__init__(self)
com_obex.py
(text/plain, 11.2 KB)
### BITPIM ### ### Author: Perry Nguyen <[email protected]> from socket import htonl, htons, ntohs, ntohl import p_obex import prototypes import common import cStringIO import time from array import array from xml.dom.minidom import parseString from sys import exc_info """ OBEX Protocol handler, used to transfer files to and from the phone. This does not necessarily mean Bluetooth as OBEX is a generic protocol over many transports, including IrDA, TCP/IP, RFCOMM, Serial, etc """ # OBEX headers OBEX_HDR_EMPTY = 0x00 OBEX_HDR_COUNT = 0xc0 OBEX_HDR_NAME = 0x01 OBEX_HDR_TYPE = 0x42 OBEX_HDR_TIME = 0x44 OBEX_HDR_TIME2 = 0xC4 OBEX_HDR_LENGTH = 0xc3 OBEX_HDR_DESCRIPTION = 0x05 OBEX_HDR_TARGET = 0x46 OBEX_HDR_BODY = 0x48 OBEX_HDR_BODY_END = 0x49 OBEX_HDR_WHO = 0x4a OBEX_HDR_APPARAM = 0x4c OBEX_HDR_AUTHCHAL = 0x4d OBEX_HDR_AUTHRESP = 0x4e OBEX_HDR_OBJCLASS = 0x4f OBEX_HDR_CONNECTION = 0xcb # OBEX header types OBEX_HI_MASK = 0xc0 OBEX_UNICODE = 0x00 OBEX_BYTE_STREAM = 0x40 OBEX_BYTE = 0x80 OBEX_INT = 0xc0 OBEX_FINAL = 0x80 OBEX_RSP_SUCCESS = 0x20 OBEX_RSP_CONTINUE = 0x10 # We seem to start losing data with higher values. OBEX_MTU = 1024 OBEX_LISTING_TYPE = "x-obex/folder-listing" OBEX_RAW_HEADERS_EMPTY = "no value has been initialized for this field yet" class ObexCommandException(Exception): def __init__(self, str="OBEX Command Exception"): Exception.__init__(self, str) class ObexConnectException(ObexCommandException): def __init__(self, str): ObexCommandException.__init__(self, str) class ObexIncompletePacketException(ObexCommandException): def __init__(self,str): ObexCommandException.__init__(self, str) class ObexDisconnectException(ObexCommandException): def __init__(self, str): ObexCommandException.__init__(self, str) class ObexProtocol: OBEX_FTP_TARGET = ( 0xf9, 0xec, 0x7b, 0xc4, 0x95, 0x3c, 0x11, 0xd2, \ 0x98, 0x4e, 0x52, 0x54, 0x00, 0xdc, 0x9e, 0x09 ) MODEOBEX = "modeobex" _mtu = -1 # hopefully there should not be any concurrency issue storing this as # a member. 0x0 seems to be a reasonable default. _connId = 0x0 _goodrate = 0 _connected = False def __init__(self): self._connected = False def obexconnect(self): obex_ftp_target = array('B', self.OBEX_FTP_TARGET) req = p_obex.obex_connect_request() targetstr = obex_ftp_target.tostring() req.target.data = prototypes.DATA(**{ 'value': targetstr }) resp = self.sendobexcommand(req, p_obex.obex_connect_response) self.log("response code = 0x%x" % resp.response) if OBEX_FINAL | OBEX_RSP_SUCCESS != resp.response: raise ObexConnectionException( "Expected response 0xA0, got 0x%x" % resp.response) self.log("obex version = 0x%x" % resp.obexVersion) self._mtu = ntohs(resp.maxLength) self.log("max packet length = %d" % self._mtu) for i in resp.headers: if i.headerId == OBEX_HDR_CONNECTION: self.connId = i.headerData self.log("Found connection ID: 0x%x" % self.connId) self.log("Successfully connected to OBEX FTP server") self._connected = True def obexdisconnect(self): if not self._connected: self.log("Already disconnected, ignoring OBEX disconnect request.") return req = p_obex.obex_disconnect_request() req.connId.data = prototypes.DATA(**{ 'value': self.connId }) resp = self.sendobexcommand(req, p_obex.obex_disconnect_response) if OBEX_FINAL | OBEX_RSP_SUCCESS != resp.response: raise ObexDisconnectionException( "Expected response 0xA0, got 0x%x" % resp.response) self.log("Successfully disconnected from OBEX FTP server") self.connId = -1 self._connected = False def createheader(self, htype, data): header = p_obex.obex_header(**{ 'headerId': htype }) header.data = prototypes.DATA(**{ 'value': data }) return header # pass in an empty name for root def setobexfolder(self, name): req = p_obex.obex_setpath_request() req.headers = self.newheaderlist() req.flags = 0x02 # go forward req.headers.append(self.createheader(OBEX_HDR_CONNECTION, self.connId)) req.headers.append(self.createheader(OBEX_HDR_NAME, name)) resp = self.sendobexcommand(req, p_obex.obex_setpath_response) if resp.response != OBEX_FINAL | OBEX_RSP_SUCCESS: raise ObexCommandException( "Expected 0xA0, got 0x%x" % resp.response) def newheaderlist(self): return prototypes.LIST(**{ 'elementclass': p_obex.obex_header }) # returns a string containing the contents of the file def readobexfile(self, req): resp = self.sendobexcommand(req, p_obex.obex_get_file_response) req = p_obex.obex_get_file_request() req.headers = self.newheaderlist() b = cStringIO.StringIO() while True: for i in resp.headers: if i.headerId in (OBEX_HDR_BODY, OBEX_HDR_BODY_END): b.write(i.headerData) if resp.response == OBEX_FINAL | OBEX_RSP_SUCCESS: self.log("Successfully transferred") break elif resp.response == OBEX_FINAL | OBEX_RSP_CONTINUE: resp = self.sendobexcommand(req, p_obex.obex_get_file_response) else: raise ObexCommandException( "Unknown response: 0x%x" % resp.response) return b.getvalue() # returns a string containing the contents of the file def getobexfile(self, directory, filename): self.setobexfolder("") self.setobexfolder(directory) req = p_obex.obex_get_file_request() req.headers = self.newheaderlist() req.headers.append(self.createheader(OBEX_HDR_CONNECTION, self.connId)) req.headers.append(self.createheader(OBEX_HDR_NAME, filename)) return self.readobexfile(req) def getobexfolderlist(self, name=None): self.setobexfolder("") if name is not None: self.setobexfolder(name) req = p_obex.obex_get_file_request() req.headers = self.newheaderlist() req.headers.append(self.createheader(OBEX_HDR_CONNECTION, self.connId)) req.headers.append(self.createheader(OBEX_HDR_TYPE, OBEX_LISTING_TYPE)) xmlstr = self.readobexfile(req) folderDom = parseString(xmlstr) files = folderDom.getElementsByTagName("file") fileList = [] for i in files: n = i.getAttribute("name") t = i.getAttribute("type") self.log("Folder contents: %s - %s" % (n, t)) fileList.append(n) folderDom.unlink() return fileList def sendobexcommand(self, req, responsetype): buff = prototypes.buffer() req.packetLength = 1 size = req.packetsize() req.packetLength = htons(size) req.writetobuffer(buff, logtitle="OBEX request") data = buff.getvalue() self.comm.write(data) return self.readobexresponse(responsetype) # read the first 3 bytes of an obex packet def readobexpacketheader(self): resp = p_obex.obex_response() r = self.comm.read(numchars=3) if len(r) == 0: raise ObexCommandException("No response to OBEX command") b = prototypes.buffer(r) resp.readfrombuffer(b) resp.packetLength = ntohs(resp.packetLength) return resp def readobexresponse(self, responsetype): r = self.readobexpacketheader() resp = responsetype() resp.response = r.response resp.packetLength = r.packetLength length = resp.packetLength length -= 3 if length > 0: remain = length cbuf = cStringIO.StringIO() readAttempts = 0 while remain > 0 and readAttempts < 3: rd = self.comm.read(numchars=remain) remain -= len(rd) cbuf.write(rd) readAttempts += 1 if remain > 0: # this seems to be a problem on my V3c # can't read anymore data without doing a reset # or sending another packet, the former means # we will lose the rest of this packet, while # the latter means we will be completely out of # sync with the OBEX server. We must perform a # port reset. self.log("WARNING: can't read entire packet, data lost!") self.comm.reset() self.comm.setbaudrate(self._goodrate) packet = cbuf.getvalue() bytesread = len(packet) if bytesread != length: self.log("WARNING: bytes read != packet length") self.log("WARNING: consider lowering OBEX_MTU") resp.packetLength = bytesread b = prototypes.buffer(packet) resp.readfrombuffer(b) if resp.rawHeaders is not OBEX_RAW_HEADERS_EMPTY: headerstream = cStringIO.StringIO(resp.rawHeaders) while True: header = self.readobexheader(headerstream) if header is None: break resp.headers.append(header) return resp def readobexheader(self, stream): headerId = stream.read(1) if len(headerId) == 0: return None headerId = self.readuint8(headerId) header = p_obex.obex_response_header() header.headerId = headerId if headerId & OBEX_HI_MASK == OBEX_UNICODE: # -3 to accomodate for headerId uint8 and packetsize word headerSize = self.readuint16(stream.read(2)) - 3 data = stream.read(headerSize) stringheader = p_obex.obex_string_header() stringheader.readfrombuffer(prototypes.buffer(data)) headerData = stringheader.data if headerId & OBEX_HI_MASK == OBEX_BYTE_STREAM: # -3 to accomodate for headerId uint8 and packetsize word headerSize = self.readuint16(stream.read(2)) - 3 streamheader = p_obex.obex_stream_header() data = stream.read(headerSize) streamheader.readfrombuffer(prototypes.buffer(data)) headerData = streamheader.data if headerId & OBEX_HI_MASK == OBEX_BYTE: headerData = self.readuint8(stream.read(1)) if headerId & OBEX_HI_MASK == OBEX_INT: headerData = self.readuint32(stream.read(4)) header.data = prototypes.DATA(**{ 'value': headerData }) header.packetsize() return header def readuint8(self, data): r = p_obex.obex_uint8() r.readfrombuffer(prototypes.buffer(data)) return r.data def readuint32(self, data): r = p_obex.obex_int_header() r.readfrombuffer(prototypes.buffer(data)) return r.data def readuint16(self, data): ui16 = p_obex.obex_uint16() ui16.readfrombuffer(prototypes.buffer(data)) return ntohs(ui16.data)
p_obex.p
(text/plain, 6.1 KB)
### ### BITPIM ### ### Author: Perry Nguyen <[email protected]> %{ """OBEX protocol definitions""" from prototypes import * from socket import htonl, htons, ntohs, ntohl # OBEX uses MSB bitpim doesn't seem to have it, so we'll convert ourselves. # I suck at python and don't know how to write a UINTmsb... UINT=UINTlsb # Constants copied from OpenOBEX # OBEX commands OBEX_CMD_CONNECT = 0x00 OBEX_CMD_DISCONNECT = 0x01 OBEX_CMD_PUT = 0x02 OBEX_CMD_GET = 0x03 OBEX_CMD_COMMAND = 0x04 OBEX_CMD_SETPATH = 0x05 OBEX_CMD_ABORT = 0x7f OBEX_FINAL = 0x80 # OBEX version 1.1 OBEX_VERSION = 0x10 # OBEX File Browsing UUID = F9EC7BC4-953C-11D2-984E-525400DC9E09 import com_obex %} PACKET obex_header: P DATA data "the actual header data" 1 UINT headerId "one of the OBEX_HDR types" if self.headerId & com_obex.OBEX_HI_MASK == com_obex.OBEX_UNICODE: # additional +1 to account for terminator # utf_16be = big endian, seems obex goes by this? 2 UINT { 'value': htons(len(self.data.encode("utf_16be")) + 4) } +headerSize * USTRING { 'encoding': "utf_16be", 'value': self.data } +headerData if self.headerId & com_obex.OBEX_HI_MASK == com_obex.OBEX_BYTE_STREAM: 2 UINT { 'value': htons(len(self.data) + 3) } +headerSize * DATA { 'value': self.data } +headerData if self.headerId & com_obex.OBEX_HI_MASK == com_obex.OBEX_BYTE: 1 UINT { 'value': self.data } +headerData if self.headerId & com_obex.OBEX_HI_MASK == com_obex.OBEX_INT: 4 UINT { 'value': htonl(self.data) } +headerData PACKET obex_response_header: P UINT headerId "one of the OBEX_HDR types" P DATA data "the actual header data" if self.headerId & com_obex.OBEX_HI_MASK == com_obex.OBEX_UNICODE: * USTRING { 'value': self.data } +headerData if self.headerId & com_obex.OBEX_HI_MASK == com_obex.OBEX_BYTE_STREAM: * DATA { 'value': self.data } +headerData if self.headerId & com_obex.OBEX_HI_MASK == com_obex.OBEX_BYTE: 1 UINT { 'value': self.data } +headerData if self.headerId & com_obex.OBEX_HI_MASK == com_obex.OBEX_INT: 4 UINT { 'value': self.data } +headerData "in MSB first" PACKET obex_response: 1 UINT response 2 UINT packetLength PACKET obex_uint8: 1 UINT data PACKET obex_uint16: 2 UINT data PACKET obex_byte_header: 1 UINT data PACKET obex_int_header: 4 UINT data PACKET obex_string_header: * USTRING data PACKET obex_stream_header: * DATA data PACKET obex_connect_request: 1 UINT { 'value': (OBEX_CMD_CONNECT | OBEX_FINAL) } +connectop 2 UINT packetLength 1 UINT { 'constant': OBEX_VERSION } +obexVersion 1 UINT { 'value': 0x00 } +flags 2 UINT { 'value': htons(com_obex.OBEX_MTU) } +maxLength * obex_header { 'headerId': com_obex.OBEX_HDR_TARGET } +target PACKET obex_connect_response: P UINT response "0x41 = unauthorized, 0xA0 = success" P UINT packetLength 1 UINT obexVersion 1 UINT flags 2 UINT maxLength "in MSB first" * DATA { 'sizeinbytes': self.packetLength - 7 } +rawHeaders P LIST { 'elementclass': obex_response_header } headers PACKET obex_disconnect_request: 1 UINT { 'value': OBEX_CMD_DISCONNECT | OBEX_FINAL } +disconnectop 2 UINT packetLength * obex_header { 'headerId': com_obex.OBEX_HDR_CONNECTION } +connId PACKET obex_disconnect_response: P UINT response "must be 0xA0" P UINT packetLength * DATA { 'sizeinbytes': self.packetLength - 3, 'default': com_obex.OBEX_RAW_HEADERS_EMPTY } +rawHeaders P LIST { 'elementclass': obex_response_header } headers PACKET obex_setpath_request: 1 UINT { 'value': OBEX_CMD_SETPATH | OBEX_FINAL } +setpathop 2 UINT packetLength 1 UINT flags "go up = 0x03, go forward = 0x02" 1 UINT { 'value': 0x00 } +setpathconstants * LIST { 'elementclass': obex_header } headers # * obex_header { 'headerId': com_obex.OBEX_HDR_CONNECTION } +connId # * obex_header { 'headerId': com_obex.OBEX_HDR_NAME } +folderName "no folder name if go up, empty if root" PACKET obex_setpath_response: P UINT response "0xA0 = success, 0xC4 = not exist or already at root, 0xC1 = not permitted" P UINT packetLength * DATA { 'sizeinbytes': self.packetLength - 3, 'default': com_obex.OBEX_RAW_HEADERS_EMPTY } +rawHeaders P LIST { 'elementclass': obex_response_header } headers PACKET obex_delete_file_request: 1 UINT { 'value': OBEX_CMD_PUT | OBEX_FINAL } +putop 2 UINT packetLength * obex_header { 'headerId': com_obex.OBEX_HDR_CONNECTION } +connId * obex_header { 'headerId': com_obex.OBEX_HDR_NAME } +name "name to delete" PACKET obex_delete_file_response: P UINT response "0xA0 = success, 0xC1 unauthorized, 0xC4 not exist" P UINT packetLength * DATA { 'sizeinbytes': self.packetLength - 3 } +responseData PACKET obex_get_file_request: 1 UINT { 'value': OBEX_CMD_GET | OBEX_FINAL } +getop 2 UINT packetLength * LIST { 'elementclass': obex_header } headers # * obex_header { 'headerId': OBEX_HDR_CONNECTION } +connId # * obex_header { 'headerId': OBEX_HDR_TYPE } +objType # * obex_header { 'headerId': OBEX_HDR_NAME } +name PACKET obex_get_file_response: P UINT response "0x90 = continue, 0xA0 = done" P UINT packetLength "in MSB first" * DATA { 'sizeinbytes': self.packetLength - 3 } +rawHeaders P LIST { 'elementclass': obex_response_header } headers PACKET obex_put_file_request: 1 UINT { 'value': OBEX_CMD_PUT } +putop 2 UINT packetLength * obex_header { 'headerId': com_obex.OBEX_HDR_CONNECTION } +connId * obex_header { 'headerId': com_obex.OBEX_HDR_NAME } +name * obex_header { 'headerId': com_obex.OBEX_HDR_BODY } +body PACKET obex_put_file_end_request: 1 UINT { 'value': OBEX_CMD_PUT | OBEX_FINAL } +putop 2 UINT packetLength * obex_header { 'headerId': com_obex.OBEX_HDR_CONNECTION } +connId * obex_header { 'headerId': com_obex.OBEX_HDR_NAME } +name * obex_header { 'headerId': com_obex.OBEX_HDR_BODY_END } +body PACKET obex_put_file_response: P UINT response "0x90 = continue, 0xA0 = success" P UINT packetLength * DATA { 'sizeinbytes': self.packetLength - 3 } +responseData