mirror of https://github.com/hak5/ToorChat.git
Update rflib to support YARD Stick One
parent
923647f50d
commit
0cbaa989a9
|
@ -1,9 +1,12 @@
|
||||||
#!/usr/bin/env ipython
|
#!/usr/bin/env ipython -i --no-banner
|
||||||
from chipcon_nic import *
|
from chipcon_nic import *
|
||||||
|
import rflib.bits as rfbits
|
||||||
|
|
||||||
RFCAT_START_SPECAN = 0x40
|
RFCAT_START_SPECAN = 0x40
|
||||||
RFCAT_STOP_SPECAN = 0x41
|
RFCAT_STOP_SPECAN = 0x41
|
||||||
|
|
||||||
|
MAX_FREQ = 936e6
|
||||||
|
|
||||||
class RfCat(FHSSNIC):
|
class RfCat(FHSSNIC):
|
||||||
def RFdump(self, msg="Receiving", maxnum=100, timeoutms=1000):
|
def RFdump(self, msg="Receiving", maxnum=100, timeoutms=1000):
|
||||||
try:
|
try:
|
||||||
|
@ -20,10 +23,10 @@ class RfCat(FHSSNIC):
|
||||||
self.RFdump("Clearing")
|
self.RFdump("Clearing")
|
||||||
self.lowball(lowball)
|
self.lowball(lowball)
|
||||||
self.setMdmDRate(drate)
|
self.setMdmDRate(drate)
|
||||||
print "Scanning range: "
|
print "Scanning range: "
|
||||||
while not keystop():
|
while not keystop():
|
||||||
try:
|
try:
|
||||||
print
|
print "(press Enter to quit)"
|
||||||
for freq in xrange(int(basefreq), int(basefreq+(inc*count)), int(inc)):
|
for freq in xrange(int(basefreq), int(basefreq+(inc*count)), int(inc)):
|
||||||
print "Scanning for frequency %d..." % freq
|
print "Scanning for frequency %d..." % freq
|
||||||
self.setFreq(freq)
|
self.setFreq(freq)
|
||||||
|
@ -40,14 +43,13 @@ class RfCat(FHSSNIC):
|
||||||
freq, delta = self._doSpecAn(basefreq, inc, count)
|
freq, delta = self._doSpecAn(basefreq, inc, count)
|
||||||
|
|
||||||
import rflib.ccspecan as rfspecan
|
import rflib.ccspecan as rfspecan
|
||||||
if not hasattr(self, "_qt_app") or self._qt_app is None:
|
rfspecan.ensureQapp()
|
||||||
self._qt_app = rfspecan.QtGui.QApplication([])
|
|
||||||
|
|
||||||
fhigh = freq + (delta*(count+1))
|
fhigh = freq + (delta*(count+1))
|
||||||
|
|
||||||
window = rfspecan.Window(self, freq, fhigh, delta, 0)
|
window = rfspecan.Window(self, freq, fhigh, delta, 0)
|
||||||
window.show()
|
window.show()
|
||||||
self._qt_app.exec_()
|
rfspecan._qt_app.exec_()
|
||||||
|
|
||||||
def _doSpecAn(self, basefreq, inc, count):
|
def _doSpecAn(self, basefreq, inc, count):
|
||||||
'''
|
'''
|
||||||
|
@ -55,6 +57,9 @@ class RfCat(FHSSNIC):
|
||||||
'''
|
'''
|
||||||
if count>255:
|
if count>255:
|
||||||
raise Exception("sorry, only 255 samples per pass... (count)")
|
raise Exception("sorry, only 255 samples per pass... (count)")
|
||||||
|
if (count * inc) + basefreq > MAX_FREQ:
|
||||||
|
raise Exception("Sorry, %1.3f + (%1.3f * %1.3f) is higher than %1.3f" %
|
||||||
|
(basefreq, count, inc))
|
||||||
self.getRadioConfig()
|
self.getRadioConfig()
|
||||||
self._specan_backup_radiocfg = self.radiocfg
|
self._specan_backup_radiocfg = self.radiocfg
|
||||||
|
|
||||||
|
@ -79,7 +84,9 @@ class RfCat(FHSSNIC):
|
||||||
def rf_configure(*args, **k2args):
|
def rf_configure(*args, **k2args):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def rf_redirection(self, fdtup):
|
def rf_redirection(self, fdtup, use_rawinput=False, printable=False):
|
||||||
|
buf = ''
|
||||||
|
|
||||||
if len(fdtup)>1:
|
if len(fdtup)>1:
|
||||||
fd0i, fd0o = fdtup
|
fd0i, fd0o = fdtup
|
||||||
else:
|
else:
|
||||||
|
@ -90,30 +97,82 @@ class RfCat(FHSSNIC):
|
||||||
if hasattr(fd0i, 'recv'):
|
if hasattr(fd0i, 'recv'):
|
||||||
fdsock = True
|
fdsock = True
|
||||||
|
|
||||||
while True:
|
try:
|
||||||
x,y,z = select.select([fd0i ], [], [], .1)
|
while True:
|
||||||
#if self._pause:
|
#if self._pause:
|
||||||
# continue
|
# continue
|
||||||
|
|
||||||
if fd0i in x:
|
try:
|
||||||
if fdsock:
|
x,y,z = select.select([fd0i ], [], [], .1)
|
||||||
data = fd0i.recv(self.max_packet_size)
|
if fd0i in x:
|
||||||
else:
|
# FIXME: make this aware of VLEN/FLEN and the proper length
|
||||||
data = fd0i.read(self.max_packet_size)
|
if fdsock:
|
||||||
|
data = fd0i.recv(self.max_packet_size)
|
||||||
|
else:
|
||||||
|
data = fd0i.read(self.max_packet_size)
|
||||||
|
|
||||||
if not len(data): # terminated socket
|
if not len(data): # terminated socket
|
||||||
break
|
break
|
||||||
|
|
||||||
self.RFxmit(data)
|
buf += data
|
||||||
|
pktlen, vlen = self.getPktLEN()
|
||||||
|
if vlen:
|
||||||
|
pktlen = ord(buf[0])
|
||||||
|
|
||||||
try:
|
#FIXME: probably want to take in a length struct here and then only send when we have that many bytes...
|
||||||
data = self.RFrecv(0)
|
data = buf[:pktlen]
|
||||||
if fdsock:
|
if use_rawinput:
|
||||||
fd0o.sendall(data)
|
data = eval('"%s"'%data)
|
||||||
else:
|
|
||||||
fd0o.write(data)
|
if len(buf) >= pktlen:
|
||||||
except ChipconUsbTimeoutException:
|
self.RFxmit(data)
|
||||||
pass
|
|
||||||
|
except ChipconUsbTimeoutException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
data, time = self.RFrecv(1)
|
||||||
|
|
||||||
|
if printable:
|
||||||
|
data = "\n"+str(time)+": "+repr(data)
|
||||||
|
else:
|
||||||
|
data = struct.pack("<L", time) + struct.pack("<H", len(data)) + data
|
||||||
|
|
||||||
|
if fdsock:
|
||||||
|
fd0o.sendall(data)
|
||||||
|
else:
|
||||||
|
fd0o.write(data)
|
||||||
|
|
||||||
|
except ChipconUsbTimeoutException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
#special handling of specan dumps... somewhat set in solid jello
|
||||||
|
try:
|
||||||
|
data, time = self.recv(APP_SPECAN, 1, 1)
|
||||||
|
data = struct.pack("<L", time) + struct.pack("<H", len(data)) + data
|
||||||
|
if fdsock:
|
||||||
|
fd0o.sendall(data)
|
||||||
|
else:
|
||||||
|
fd0o.write(data)
|
||||||
|
|
||||||
|
except ChipconUsbTimeoutException:
|
||||||
|
#print "this is a valid exception, run along... %x"% APP_SPECAN
|
||||||
|
pass
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.setModeIDLE()
|
||||||
|
|
||||||
|
class InverseCat(RfCat):
|
||||||
|
def setMdmSyncWord(self, word, radiocfg=None):
|
||||||
|
FHSSNIC.setMdmSyncWord(self, word ^ 0xffff, radiocfg)
|
||||||
|
|
||||||
|
def RFrecv(self, timeout=1000):
|
||||||
|
global data
|
||||||
|
data,timestamp = RfCat.RFrecv(self, timeout)
|
||||||
|
return rfbits.invertBits(data),timestamp
|
||||||
|
|
||||||
|
def RFxmit(self, data):
|
||||||
|
return RfCat.RFxmit(self, rfbits.invertBits(data) )
|
||||||
|
|
||||||
def cleanupInteractiveAtExit():
|
def cleanupInteractiveAtExit():
|
||||||
try:
|
try:
|
||||||
|
@ -143,16 +202,24 @@ def interactive(idx=0, DongleClass=RfCat, intro=''):
|
||||||
|
|
||||||
except ImportError, e:
|
except ImportError, e:
|
||||||
try:
|
try:
|
||||||
from IPython.frontend.terminal.interactiveshell import TerminalInteractiveShell
|
from IPython.terminal.interactiveshell import TerminalInteractiveShell
|
||||||
ipsh = TerminalInteractiveShell()
|
ipsh = TerminalInteractiveShell()
|
||||||
ipsh.user_global_ns.update(gbls)
|
ipsh.user_global_ns.update(gbls)
|
||||||
ipsh.user_global_ns.update(lcls)
|
ipsh.user_global_ns.update(lcls)
|
||||||
ipsh.autocall = 2 # don't require parenthesis around *everything*. be smart!
|
ipsh.autocall = 2 # don't require parenthesis around *everything*. be smart!
|
||||||
ipsh.mainloop(intro)
|
ipsh.mainloop(intro)
|
||||||
except ImportError, e:
|
except ImportError, e:
|
||||||
print e
|
try:
|
||||||
shell = code.InteractiveConsole(gbls)
|
from IPython.frontend.terminal.interactiveshell import TerminalInteractiveShell
|
||||||
shell.interact(intro)
|
ipsh = TerminalInteractiveShell()
|
||||||
|
ipsh.user_global_ns.update(gbls)
|
||||||
|
ipsh.user_global_ns.update(lcls)
|
||||||
|
ipsh.autocall = 2 # don't require parenthesis around *everything*. be smart!
|
||||||
|
ipsh.mainloop(intro)
|
||||||
|
except ImportError, e:
|
||||||
|
print e
|
||||||
|
shell = code.InteractiveConsole(gbls)
|
||||||
|
shell.interact(intro)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
163
rflib/bits.py
163
rflib/bits.py
|
@ -1,5 +1,76 @@
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
|
fmtsLSB = [None, "B", "<H", "<I", "<I", "<Q", "<Q", "<Q", "<Q"]
|
||||||
|
fmtsMSB = [None, "B", ">H", ">I", ">I", ">Q", ">Q", ">Q", ">Q"]
|
||||||
|
sizes = [ 0, 1, 2, 4, 4, 8, 8, 8, 8]
|
||||||
|
masks = [ (1<<(8*i))-1 for i in xrange(9) ]
|
||||||
|
|
||||||
|
def wtfo(string):
|
||||||
|
outstr = []
|
||||||
|
bitlen = len(outstr) * 8
|
||||||
|
for x in range(8):
|
||||||
|
outstr.append(shiftString(string, x))
|
||||||
|
|
||||||
|
string = strBitReverse(string)
|
||||||
|
for x in range(8):
|
||||||
|
outstr.append(shiftString(string, x))
|
||||||
|
|
||||||
|
return outstr
|
||||||
|
|
||||||
|
def strBitReverse(string):
|
||||||
|
# FIXME: this is really dependent upon python's number system. large strings will not convert well.
|
||||||
|
# FIXME: break up array of 8-bit numbers and bit-swap in the array
|
||||||
|
num = 0
|
||||||
|
bits = len(string)*8
|
||||||
|
# convert to MSB number
|
||||||
|
for x in range(len(string)):
|
||||||
|
ch = string[x]
|
||||||
|
#num |= (ord(ch)<<(8*x)) # this is LSB
|
||||||
|
num <<= 8
|
||||||
|
num |= ord(ch)
|
||||||
|
|
||||||
|
print (hex(num))
|
||||||
|
rnum = bitReverse(num, bits)
|
||||||
|
print (hex(rnum))
|
||||||
|
|
||||||
|
# convert back from MSB number to string
|
||||||
|
out = []
|
||||||
|
for x in range(len(string)):
|
||||||
|
out.append(chr(rnum&0xff))
|
||||||
|
rnum >>= 8
|
||||||
|
out.reverse()
|
||||||
|
print(''.join(out).encode('hex'))
|
||||||
|
return ''.join(out)
|
||||||
|
|
||||||
|
def strXorMSB(string, xorval, size):
|
||||||
|
'''
|
||||||
|
lsb
|
||||||
|
pads end of string with 00
|
||||||
|
'''
|
||||||
|
out = []
|
||||||
|
strlen = len(string)
|
||||||
|
string += "\x00" * sizes[size]
|
||||||
|
|
||||||
|
for idx in range(0, strlen, size):
|
||||||
|
tempstr = string[idx:idx+sizes[size]]
|
||||||
|
temp, = struct.unpack( fmtsMSB[size], tempstr )
|
||||||
|
temp ^= xorval
|
||||||
|
temp &= masks[size]
|
||||||
|
tempstr = struct.pack( fmtsMSB[size], temp )[-size:]
|
||||||
|
out.append(tempstr)
|
||||||
|
return ''.join(out)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def bitReverse(num, bitcnt):
|
||||||
|
newnum = 0
|
||||||
|
for idx in range(bitcnt):
|
||||||
|
newnum <<= 1
|
||||||
|
newnum |= num&1
|
||||||
|
num >>= 1
|
||||||
|
return newnum
|
||||||
|
|
||||||
def shiftString(string, bits):
|
def shiftString(string, bits):
|
||||||
carry = 0
|
carry = 0
|
||||||
news = []
|
news = []
|
||||||
|
@ -10,15 +81,66 @@ def shiftString(string, bits):
|
||||||
news.append("%c"%newc)
|
news.append("%c"%newc)
|
||||||
return "".join(news)
|
return "".join(news)
|
||||||
|
|
||||||
def findDword(byts, inverted=False):
|
def getNextByte_feedbackRegister7bitsMSB():
|
||||||
|
'''
|
||||||
|
this returns a byte of a 7-bit feedback register stemming off bits 4 and 7
|
||||||
|
the register is 7 bits long, but we return a more usable 8bits (ie.
|
||||||
|
'''
|
||||||
|
global fbRegister
|
||||||
|
|
||||||
|
retval = 0
|
||||||
|
for x in range(8): #MSB,
|
||||||
|
retval <<= 1
|
||||||
|
retval |= (fbRegister >> 6) # start with bit 7
|
||||||
|
nb = ( ( fbRegister>>3) ^ (fbRegister>>6)) &1
|
||||||
|
fbRegister = ( ( fbRegister << 1 ) | nb ) & 0x7f # do shifting
|
||||||
|
#print "retval: %x fbRegister: %x bit7: %x nb: %x" % (retval, fbRegister, (fbRegister>>6), nb)
|
||||||
|
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def getNextByte_feedbackRegister7bitsLSB():
|
||||||
|
'''
|
||||||
|
this returns a byte of a 7-bit feedback register stemming off bits 4 and 7
|
||||||
|
the register is 7 bits long, but we return a more usable 8bits (ie.
|
||||||
|
'''
|
||||||
|
global fbRegister
|
||||||
|
|
||||||
|
retval = 0
|
||||||
|
for x in range(8): #MSB,
|
||||||
|
retval >>= 1
|
||||||
|
retval |= ((fbRegister << 1)&0x80) # start with bit 7
|
||||||
|
|
||||||
|
nb = ( ( fbRegister>>3) ^ (fbRegister>>6)) &1
|
||||||
|
fbRegister = ( ( fbRegister << 1 ) | nb ) & 0x7f # do shifting
|
||||||
|
#print "retval: %x fbRegister: %x bit7: %x nb: %x" % (retval, fbRegister, (fbRegister>>6), nb)
|
||||||
|
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
def whitenData(data, seed=0xffff, getNextByte=getNextByte_feedbackRegister7bitsMSB):
|
||||||
|
global fbRegister
|
||||||
|
fbRegister = seed
|
||||||
|
|
||||||
|
carry = 0
|
||||||
|
news = []
|
||||||
|
for x in xrange(len(data)-1):
|
||||||
|
newc = ((ord(data[x]) ^ getNextByte() ) & 0xff)
|
||||||
|
news.append("%c"%newc)
|
||||||
|
return "".join(news)
|
||||||
|
|
||||||
|
def findSyncWord(byts, sensitivity=4, minpreamble=2):
|
||||||
|
'''
|
||||||
|
seek SyncWords from a raw bitstream.
|
||||||
|
assumes we capture at least two (more likely 3 or more) preamble bytes
|
||||||
|
'''
|
||||||
possDwords = []
|
possDwords = []
|
||||||
# find the preamble (if any)
|
# find the preamble (if any)
|
||||||
while True:
|
while True: # keep searching through string until we don't find any more preamble bits to pick on
|
||||||
sbyts = byts
|
sbyts = byts
|
||||||
pidx = byts.find("\xaa\xaa")
|
pidx = byts.find("\xaa"*minpreamble)
|
||||||
if pidx == -1:
|
if pidx == -1:
|
||||||
pidx = byts.find("\x55\x55")
|
pidx = byts.find("\x55"*minpreamble)
|
||||||
byts = shiftString(byts,1)
|
byts = shiftString(byts, 1)
|
||||||
|
|
||||||
if pidx == -1:
|
if pidx == -1:
|
||||||
return possDwords
|
return possDwords
|
||||||
|
@ -57,7 +179,8 @@ def findDword(byts, inverted=False):
|
||||||
# bits1 >>= 2
|
# bits1 >>= 2
|
||||||
#print "bits: %x" % (bits1)
|
#print "bits: %x" % (bits1)
|
||||||
|
|
||||||
for frontbits in xrange((0,1)[inverted], 17, 2):
|
bitcount = min( 2 * sensitivity, 17 )
|
||||||
|
for frontbits in xrange( bitcount ): # with so many bit-inverted systems, let's not assume we know anything about the bit-arrangement. \x55\x55 could be a perfectly reasonable preamble.
|
||||||
poss = (bits1 >> frontbits) & 0xffff
|
poss = (bits1 >> frontbits) & 0xffff
|
||||||
if not poss in possDwords:
|
if not poss in possDwords:
|
||||||
possDwords.append(poss)
|
possDwords.append(poss)
|
||||||
|
@ -65,7 +188,7 @@ def findDword(byts, inverted=False):
|
||||||
|
|
||||||
return possDwords
|
return possDwords
|
||||||
|
|
||||||
def findDwordDoubled(byts):
|
def findSyncWordDoubled(byts):
|
||||||
possDwords = []
|
possDwords = []
|
||||||
# find the preamble (if any)
|
# find the preamble (if any)
|
||||||
bitoff = 0
|
bitoff = 0
|
||||||
|
@ -345,3 +468,29 @@ def reprBitArray(bitAry, width=194):
|
||||||
bots = "".join(bot)
|
bots = "".join(bot)
|
||||||
return "\n".join([tops, mids, bots])
|
return "\n".join([tops, mids, bots])
|
||||||
|
|
||||||
|
def invertBits(data):
|
||||||
|
output = []
|
||||||
|
ldata = len(data)
|
||||||
|
off = 0
|
||||||
|
|
||||||
|
if ldata&1:
|
||||||
|
output.append( chr( ord( data[0] ) ^ 0xff) )
|
||||||
|
off = 1
|
||||||
|
|
||||||
|
if ldata&2:
|
||||||
|
output.append( struct.pack( "<H", struct.unpack( "<H", data[off:off+2] )[0] ^ 0xffff) )
|
||||||
|
off += 2
|
||||||
|
|
||||||
|
#method 1
|
||||||
|
#for idx in xrange( off, ldata, 4):
|
||||||
|
# output.append( struct.pack( "<I", struct.unpack( "<I", data[idx:idx+4] )[0] & 0xffff) )
|
||||||
|
|
||||||
|
#method2
|
||||||
|
count = ldata / 4
|
||||||
|
#print ldata, count
|
||||||
|
numlist = struct.unpack( "<%dI" % count, data[off:] )
|
||||||
|
modlist = [ struct.pack("<L", (x^0xffffffff) ) for x in numlist ]
|
||||||
|
output.extend(modlist)
|
||||||
|
|
||||||
|
return ''.join(output)
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -29,6 +29,10 @@ import cPickle as pickle
|
||||||
from PySide import QtCore, QtGui
|
from PySide import QtCore, QtGui
|
||||||
from PySide.QtCore import Qt, QPointF, QLineF
|
from PySide.QtCore import Qt, QPointF, QLineF
|
||||||
|
|
||||||
|
def ensureQapp():
|
||||||
|
global _qt_app
|
||||||
|
if not globals().get("_qt_app"):
|
||||||
|
_qt_app = QtGui.QApplication([])
|
||||||
|
|
||||||
|
|
||||||
APP_SPECAN = 0x43
|
APP_SPECAN = 0x43
|
||||||
|
|
1985
rflib/chipcon_nic.py
1985
rflib/chipcon_nic.py
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,965 @@
|
||||||
|
#!/usr/bin/env ipython
|
||||||
|
import sys, threading, time, struct, select
|
||||||
|
import usb
|
||||||
|
|
||||||
|
import bits
|
||||||
|
from chipcondefs import *
|
||||||
|
from rflib_version import *
|
||||||
|
|
||||||
|
DEFAULT_USB_TIMEOUT = 1000
|
||||||
|
|
||||||
|
EP_TIMEOUT_IDLE = 400
|
||||||
|
EP_TIMEOUT_ACTIVE = 10
|
||||||
|
|
||||||
|
USB_MAX_BLOCK_SIZE = 512
|
||||||
|
USB_RX_WAIT = 1000
|
||||||
|
USB_TX_WAIT = 10000
|
||||||
|
|
||||||
|
USB_BM_REQTYPE_TGTMASK =0x1f
|
||||||
|
USB_BM_REQTYPE_TGT_DEV =0x00
|
||||||
|
USB_BM_REQTYPE_TGT_INTF =0x01
|
||||||
|
USB_BM_REQTYPE_TGT_EP =0x02
|
||||||
|
|
||||||
|
USB_BM_REQTYPE_TYPEMASK =0x60
|
||||||
|
USB_BM_REQTYPE_TYPE_STD =0x00
|
||||||
|
USB_BM_REQTYPE_TYPE_CLASS =0x20
|
||||||
|
USB_BM_REQTYPE_TYPE_VENDOR =0x40
|
||||||
|
USB_BM_REQTYPE_TYPE_RESERVED =0x60
|
||||||
|
|
||||||
|
USB_BM_REQTYPE_DIRMASK =0x80
|
||||||
|
USB_BM_REQTYPE_DIR_OUT =0x00
|
||||||
|
USB_BM_REQTYPE_DIR_IN =0x80
|
||||||
|
|
||||||
|
USB_GET_STATUS =0x00
|
||||||
|
USB_CLEAR_FEATURE =0x01
|
||||||
|
USB_SET_FEATURE =0x03
|
||||||
|
USB_SET_ADDRESS =0x05
|
||||||
|
USB_GET_DESCRIPTOR =0x06
|
||||||
|
USB_SET_DESCRIPTOR =0x07
|
||||||
|
USB_GET_CONFIGURATION =0x08
|
||||||
|
USB_SET_CONFIGURATION =0x09
|
||||||
|
USB_GET_INTERFACE =0x0a
|
||||||
|
USB_SET_INTERFACE =0x11
|
||||||
|
USB_SYNCH_FRAME =0x12
|
||||||
|
|
||||||
|
APP_GENERIC = 0x01
|
||||||
|
APP_DEBUG = 0xfe
|
||||||
|
APP_SYSTEM = 0xff
|
||||||
|
|
||||||
|
|
||||||
|
SYS_CMD_PEEK = 0x80
|
||||||
|
SYS_CMD_POKE = 0x81
|
||||||
|
SYS_CMD_PING = 0x82
|
||||||
|
SYS_CMD_STATUS = 0x83
|
||||||
|
SYS_CMD_POKE_REG = 0x84
|
||||||
|
SYS_CMD_GET_CLOCK = 0x85
|
||||||
|
SYS_CMD_BUILDTYPE = 0x86
|
||||||
|
SYS_CMD_BOOTLOADER = 0x87
|
||||||
|
SYS_CMD_RFMODE = 0x88
|
||||||
|
SYS_CMD_PARTNUM = 0x8e
|
||||||
|
SYS_CMD_RESET = 0x8f
|
||||||
|
|
||||||
|
EP0_CMD_GET_DEBUG_CODES = 0x00
|
||||||
|
EP0_CMD_GET_ADDRESS = 0x01
|
||||||
|
EP0_CMD_POKEX = 0x01
|
||||||
|
EP0_CMD_PEEKX = 0x02
|
||||||
|
EP0_CMD_PING0 = 0x03
|
||||||
|
EP0_CMD_PING1 = 0x04
|
||||||
|
EP0_CMD_RESET = 0xfe
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG_CMD_STRING = 0xf0
|
||||||
|
DEBUG_CMD_HEX = 0xf1
|
||||||
|
DEBUG_CMD_HEX16 = 0xf2
|
||||||
|
DEBUG_CMD_HEX32 = 0xf3
|
||||||
|
DEBUG_CMD_INT = 0xf4
|
||||||
|
|
||||||
|
EP5OUT_MAX_PACKET_SIZE = 64
|
||||||
|
EP5IN_MAX_PACKET_SIZE = 64
|
||||||
|
# EP5OUT_BUFFER_SIZE must match firmware/include/chipcon_usb.h definition
|
||||||
|
EP5OUT_BUFFER_SIZE = 516
|
||||||
|
|
||||||
|
LC_USB_INITUSB = 0x2
|
||||||
|
LC_MAIN_RFIF = 0xd
|
||||||
|
LC_USB_DATA_RESET_RESUME = 0xa
|
||||||
|
LC_USB_RESET = 0xb
|
||||||
|
LC_USB_EP5OUT = 0xc
|
||||||
|
LC_RF_VECTOR = 0x10
|
||||||
|
LC_RFTXRX_VECTOR = 0x11
|
||||||
|
|
||||||
|
LCE_USB_EP5_TX_WHILE_INBUF_WRITTEN = 0x1
|
||||||
|
LCE_USB_EP0_SENT_STALL = 0x4
|
||||||
|
LCE_USB_EP5_OUT_WHILE_OUTBUF_WRITTEN = 0x5
|
||||||
|
LCE_USB_EP5_LEN_TOO_BIG = 0x6
|
||||||
|
LCE_USB_EP5_GOT_CRAP = 0x7
|
||||||
|
LCE_USB_EP5_STALL = 0x8
|
||||||
|
LCE_USB_DATA_LEFTOVER_FLAGS = 0x9
|
||||||
|
LCE_RF_RXOVF = 0x10
|
||||||
|
LCE_RF_TXUNF = 0x11
|
||||||
|
|
||||||
|
LCS = {}
|
||||||
|
LCES = {}
|
||||||
|
lcls = locals()
|
||||||
|
for lcl in lcls.keys():
|
||||||
|
if lcl.startswith("LCE_"):
|
||||||
|
LCES[lcl] = lcls[lcl]
|
||||||
|
LCES[lcls[lcl]] = lcl
|
||||||
|
if lcl.startswith("LC_"):
|
||||||
|
LCS[lcl] = lcls[lcl]
|
||||||
|
LCS[lcls[lcl]] = lcl
|
||||||
|
|
||||||
|
CHIPS = {
|
||||||
|
0x91: "CC2511",
|
||||||
|
0x81: "CC2510",
|
||||||
|
0x11: "CC1111",
|
||||||
|
0x01: "CC1110",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def keystop(delay=0):
|
||||||
|
return len(select.select([sys.stdin],[],[],delay)[0])
|
||||||
|
|
||||||
|
def getRfCatDevices():
|
||||||
|
'''
|
||||||
|
returns a list of USB device objects for any rfcats that are plugged in
|
||||||
|
NOTE: if any rfcats are in bootloader mode, this will cause python to Exit
|
||||||
|
'''
|
||||||
|
rfcats = []
|
||||||
|
for bus in usb.busses():
|
||||||
|
for dev in bus.devices:
|
||||||
|
# OpenMoko assigned or Legacy TI
|
||||||
|
if (dev.idVendor == 0x0451 and dev.idProduct == 0x4715) or (dev.idVendor == 0x1d50 and (dev.idProduct == 0x6047 or dev.idProduct == 0x6048 or dev.idProduct == 0x605b)):
|
||||||
|
rfcats.append(dev)
|
||||||
|
|
||||||
|
elif (dev.idVendor == 0x1d50 and (dev.idProduct == 0x6049 or dev.idProduct == 0x604a)):
|
||||||
|
print "Already in Bootloader Mode... exiting"
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
return rfcats
|
||||||
|
|
||||||
|
class ChipconUsbTimeoutException(Exception):
|
||||||
|
def __str__(self):
|
||||||
|
return "Timeout waiting for USB response."
|
||||||
|
|
||||||
|
direct=False
|
||||||
|
|
||||||
|
class USBDongle:
|
||||||
|
######## INITIALIZATION ########
|
||||||
|
def __init__(self, idx=0, debug=False, copyDongle=None, RfMode=RFST_SRX):
|
||||||
|
self.rsema = None
|
||||||
|
self.xsema = None
|
||||||
|
self._bootloader = False
|
||||||
|
self._init_on_reconnect = True
|
||||||
|
self._do = None
|
||||||
|
self.idx = idx
|
||||||
|
self.cleanup()
|
||||||
|
self._debug = debug
|
||||||
|
self._quiet = False
|
||||||
|
self._threadGo = threading.Event()
|
||||||
|
self._recv_time = 0
|
||||||
|
self.radiocfg = RadioConfig()
|
||||||
|
self._rfmode = RfMode
|
||||||
|
self._radio_configured = False
|
||||||
|
|
||||||
|
self.ctrl_thread = threading.Thread(target=self.run_ctrl)
|
||||||
|
self.ctrl_thread.setDaemon(True)
|
||||||
|
self.ctrl_thread.start()
|
||||||
|
|
||||||
|
self.recv_thread = threading.Thread(target=self.runEP5_recv)
|
||||||
|
self.recv_thread.setDaemon(True)
|
||||||
|
self.recv_thread.start()
|
||||||
|
|
||||||
|
self.send_thread = threading.Thread(target=self.runEP5_send)
|
||||||
|
self.send_thread.setDaemon(True)
|
||||||
|
self.send_thread.start()
|
||||||
|
|
||||||
|
self.resetup(copyDongle=copyDongle)
|
||||||
|
self.max_packet_size = USB_MAX_BLOCK_SIZE
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
self._usberrorcnt = 0;
|
||||||
|
self.recv_queue = ''
|
||||||
|
self.recv_mbox = {}
|
||||||
|
self.recv_event = threading.Event()
|
||||||
|
self.xmit_event = threading.Event()
|
||||||
|
self.reset_event = threading.Event()
|
||||||
|
self.xmit_queue = []
|
||||||
|
self.xmit_event.clear()
|
||||||
|
self.reset_event.clear()
|
||||||
|
self.trash = []
|
||||||
|
|
||||||
|
def setRFparameters(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run_ctrl(self):
|
||||||
|
'''
|
||||||
|
we wait for reset events and run resetup
|
||||||
|
'''
|
||||||
|
while True:
|
||||||
|
self.reset_event.wait()
|
||||||
|
self.resetup(False)
|
||||||
|
self.reset_event.clear()
|
||||||
|
time.sleep(4)
|
||||||
|
|
||||||
|
def setup(self, console=True, copyDongle=None):
|
||||||
|
global dongles
|
||||||
|
|
||||||
|
if copyDongle is not None:
|
||||||
|
self.devnum = copyDongle.devnum
|
||||||
|
self._d = copyDongle._d
|
||||||
|
self._do = copyDongle._do
|
||||||
|
self._usbmaxi = copyDongle._usbmaxi
|
||||||
|
self._usbmaxo = copyDongle._usbmaxo
|
||||||
|
self._usbcfg = copyDongle._usbcfg
|
||||||
|
self._usbintf = copyDongle._usbintf
|
||||||
|
self._usbeps = copyDongle._usbeps
|
||||||
|
self._threadGo.set()
|
||||||
|
self.ep5timeout = EP_TIMEOUT_ACTIVE
|
||||||
|
copyDongle._threadGo.clear() # we're taking over from here.
|
||||||
|
self.rsema = copyDongle.rsema
|
||||||
|
self.xsema = copyDongle.xsema
|
||||||
|
return
|
||||||
|
|
||||||
|
dongles = []
|
||||||
|
self.ep5timeout = EP_TIMEOUT_ACTIVE
|
||||||
|
|
||||||
|
for dev in getRfCatDevices():
|
||||||
|
if self._debug: print >>sys.stderr,(dev)
|
||||||
|
do = dev.open()
|
||||||
|
iSN = do.getDescriptor(1,0,50)[16]
|
||||||
|
devnum = dev.devnum
|
||||||
|
dongles.append((devnum, dev, do))
|
||||||
|
|
||||||
|
dongles.sort()
|
||||||
|
if len(dongles) == 0:
|
||||||
|
raise(Exception("No Dongle Found. Please insert a RFCAT dongle."))
|
||||||
|
|
||||||
|
self.rsema = threading.Lock()
|
||||||
|
self.xsema = threading.Lock()
|
||||||
|
|
||||||
|
# claim that interface!
|
||||||
|
do = dongles[self.idx][2]
|
||||||
|
|
||||||
|
try:
|
||||||
|
do.claimInterface(0)
|
||||||
|
except Exception,e:
|
||||||
|
if console or self._debug: print >>sys.stderr,("Error claiming usb interface:" + repr(e))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
self.devnum, self._d, self._do = dongles[self.idx]
|
||||||
|
self._usbmaxi, self._usbmaxo = (EP5IN_MAX_PACKET_SIZE, EP5OUT_MAX_PACKET_SIZE)
|
||||||
|
self._usbcfg = self._d.configurations[0]
|
||||||
|
self._usbintf = self._usbcfg.interfaces[0][0]
|
||||||
|
self._usbeps = self._usbintf.endpoints
|
||||||
|
for ep in self._usbeps:
|
||||||
|
if ep.address & 0x80:
|
||||||
|
self._usbmaxi = ep.maxPacketSize
|
||||||
|
else:
|
||||||
|
self._usbmaxo = ep.maxPacketSize
|
||||||
|
|
||||||
|
self._threadGo.set()
|
||||||
|
|
||||||
|
self.getRadioConfig()
|
||||||
|
chip = self.getPartNum()
|
||||||
|
chipstr = CHIPS.get(chip)
|
||||||
|
|
||||||
|
self.chipnum = chip
|
||||||
|
self.chipstr = chipstr
|
||||||
|
|
||||||
|
if chip == None:
|
||||||
|
print "Older firmware, consider upgrading."
|
||||||
|
else:
|
||||||
|
self.chipstr = "unrecognized dongle: %s" % chip
|
||||||
|
|
||||||
|
if self._init_on_reconnect:
|
||||||
|
if self._radio_configured:
|
||||||
|
self._clear_buffers()
|
||||||
|
self.setRadioConfig()
|
||||||
|
else:
|
||||||
|
self.setRFparameters()
|
||||||
|
self._radio_configured = True
|
||||||
|
|
||||||
|
def resetup(self, console=True, copyDongle=None):
|
||||||
|
self._do=None
|
||||||
|
if self._bootloader:
|
||||||
|
return
|
||||||
|
#self._threadGo = True
|
||||||
|
if self._debug: print >>sys.stderr,("waiting (resetup) %x" % self.idx)
|
||||||
|
while (self._do==None):
|
||||||
|
try:
|
||||||
|
self.setup(console, copyDongle)
|
||||||
|
if copyDongle is None:
|
||||||
|
self._clear_buffers(False)
|
||||||
|
self.ping(3, wait=10, silent=True)
|
||||||
|
self.setRfMode(self._rfmode)
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
#if console: sys.stderr.write('.')
|
||||||
|
if not self._quiet:
|
||||||
|
print >>sys.stderr,("Error in resetup():" + repr(e))
|
||||||
|
#if console or self._debug: print >>sys.stderr,("Error in resetup():" + repr(e))
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
######## BASE FOUNDATIONAL "HIDDEN" CALLS ########
|
||||||
|
def _sendEP0(self, request=0, buf=None, value=0x200, index=0, timeout=DEFAULT_USB_TIMEOUT):
|
||||||
|
if buf == None:
|
||||||
|
buf = 'HELLO THERE'
|
||||||
|
#return self._do.controlMsg(USB_BM_REQTYPE_TGT_EP|USB_BM_REQTYPE_TYPE_VENDOR|USB_BM_REQTYPE_DIR_OUT, request, "\x00\x00\x00\x00\x00\x00\x00\x00"+buf, value, index, timeout), buf
|
||||||
|
return self._do.controlMsg(USB_BM_REQTYPE_TGT_EP|USB_BM_REQTYPE_TYPE_VENDOR|USB_BM_REQTYPE_DIR_OUT, request, buf, value, index, timeout), buf
|
||||||
|
|
||||||
|
def _recvEP0(self, request=0, length=64, value=0, index=0, timeout=100):
|
||||||
|
retary = ["%c"%x for x in self._do.controlMsg(USB_BM_REQTYPE_TGT_EP|USB_BM_REQTYPE_TYPE_VENDOR|USB_BM_REQTYPE_DIR_IN, request, length, value, index, timeout)]
|
||||||
|
if len(retary):
|
||||||
|
return ''.join(retary)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _sendEP5(self, buf=None, timeout=DEFAULT_USB_TIMEOUT):
|
||||||
|
global direct
|
||||||
|
if (buf==None):
|
||||||
|
buf = "\xff\x82\x07\x00ABCDEFG"
|
||||||
|
if direct:
|
||||||
|
self._do.bulkWrite(5, buf, timeout)
|
||||||
|
return
|
||||||
|
|
||||||
|
while (len(buf)>0):
|
||||||
|
drain = buf[:self._usbmaxo]
|
||||||
|
buf = buf[self._usbmaxo:]
|
||||||
|
|
||||||
|
if self._debug: print >>sys.stderr,"XMIT:"+repr(drain)
|
||||||
|
try:
|
||||||
|
self._do.bulkWrite(5, drain, timeout)
|
||||||
|
except Exception, e:
|
||||||
|
if self._debug: print >>sys.stderr,"requeuing on error '%s' (%s)" % (repr(drain), e)
|
||||||
|
self.xsema.acquire()
|
||||||
|
msg = self.xmit_queue.insert(0, drain)
|
||||||
|
self.xmit_event.set()
|
||||||
|
self.xsema.release()
|
||||||
|
if self._debug: print >>sys.stderr, repr(self.xmit_queue)
|
||||||
|
'''
|
||||||
|
drain = buf[:self._usbmaxo]
|
||||||
|
buf = buf[self._usbmaxo:]
|
||||||
|
if len(buf):
|
||||||
|
if self._debug: print >>sys.stderr,"requeuing '%s'" % repr(buf)
|
||||||
|
self.xsema.acquire()
|
||||||
|
msg = self.xmit_queue.insert(0, buf)
|
||||||
|
self.xsema.release()
|
||||||
|
if self._debug: print >>sys.stderr, repr(self.xmit_queue)
|
||||||
|
if self._debug: print >>sys.stderr,"XMIT:"+repr(drain)
|
||||||
|
try:
|
||||||
|
self._do.bulkWrite(5, drain, timeout)
|
||||||
|
except Exception, e:
|
||||||
|
if self._debug: print >>sys.stderr,"requeuing on error '%s' (%s)" % (repr(drain), e)
|
||||||
|
self.xsema.acquire()
|
||||||
|
msg = self.xmit_queue.insert(0, drain)
|
||||||
|
self.xsema.release()
|
||||||
|
if self._debug: print >>sys.stderr, repr(self.xmit_queue)
|
||||||
|
|
||||||
|
---
|
||||||
|
while (len(buf)>0):
|
||||||
|
drain = buf[:self._usbmaxo]
|
||||||
|
buf = buf[self._usbmaxo:]
|
||||||
|
|
||||||
|
if self._debug: print >>sys.stderr,"XMIT:"+repr(drain)
|
||||||
|
self._do.bulkWrite(5, drain, timeout)
|
||||||
|
time.sleep(1)
|
||||||
|
---
|
||||||
|
if (len(buf) > self._usbmaxo):
|
||||||
|
drain = buf[:self._usbmaxo]
|
||||||
|
buf = buf[self._usbmaxo:]
|
||||||
|
self.xsema.acquire()
|
||||||
|
msg = self.xmit_queue.insert(0, buf)
|
||||||
|
self.xsema.release()
|
||||||
|
else:
|
||||||
|
drain = buf[:]
|
||||||
|
if self._debug: print >>sys.stderr,"XMIT:"+repr(drain)
|
||||||
|
self._do.bulkWrite(5, drain, timeout)
|
||||||
|
---
|
||||||
|
while (len(buf)>0):
|
||||||
|
if (len(buf) > self._usbmaxo):
|
||||||
|
drain = buf[:self._usbmaxo]
|
||||||
|
buf = buf[self._usbmaxo:]
|
||||||
|
else:
|
||||||
|
drain = buf[:]
|
||||||
|
if self._debug: print >>sys.stderr,"XMIT:"+repr(drain)
|
||||||
|
self._do.bulkWrite(5, drain, timeout)
|
||||||
|
time.sleep(1)
|
||||||
|
'''
|
||||||
|
|
||||||
|
def _recvEP5(self, timeout=100):
|
||||||
|
retary = ["%c"%x for x in self._do.bulkRead(0x85, 500, timeout)]
|
||||||
|
if self._debug: print >>sys.stderr,"RECV:"+repr(retary)
|
||||||
|
if len(retary):
|
||||||
|
return ''.join(retary)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def _clear_buffers(self, clear_recv_mbox=False):
|
||||||
|
threadGoSet = self._threadGo.isSet()
|
||||||
|
self._threadGo.clear()
|
||||||
|
if self._debug:
|
||||||
|
print >>sys.stderr,("_clear_buffers()")
|
||||||
|
if clear_recv_mbox:
|
||||||
|
for key in self.recv_mbox.keys():
|
||||||
|
self.trash.extend(self.recvAll(key))
|
||||||
|
elif self.recv_mbox.get(APP_SYSTEM) != None:
|
||||||
|
self.trash.extend(self.recvAll(APP_SYSTEM))
|
||||||
|
self.trash.append((time.time(),self.recv_queue))
|
||||||
|
self.recv_queue = ''
|
||||||
|
# self.xmit_queue = [] # do we want to keep this?
|
||||||
|
if threadGoSet: self._threadGo.set()
|
||||||
|
|
||||||
|
|
||||||
|
######## TRANSMIT/RECEIVE THREADING ########
|
||||||
|
def runEP5_send(self):
|
||||||
|
msg = ''
|
||||||
|
self.send_threadcounter = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
self._threadGo.wait()
|
||||||
|
self.send_threadcounter = (self.send_threadcounter + 1) & 0xffffffff
|
||||||
|
|
||||||
|
#### transmit stuff. if any exists in the xmit_queue
|
||||||
|
self.xmit_event.wait() # event driven xmit
|
||||||
|
msgsent = False
|
||||||
|
try:
|
||||||
|
if len(self.xmit_queue):
|
||||||
|
self.xsema.acquire()
|
||||||
|
|
||||||
|
msg = self.xmit_queue.pop(0)
|
||||||
|
if not len(self.xmit_queue): # if there was only one message
|
||||||
|
self.xmit_event.clear() # clear the queue, within the lock
|
||||||
|
|
||||||
|
self.xsema.release()
|
||||||
|
|
||||||
|
self._sendEP5(msg)
|
||||||
|
msgsent = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
if self._debug>3: sys.stderr.write("NoMsgToSend ")
|
||||||
|
except:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
|
||||||
|
def runEP5_recv(self):
|
||||||
|
msg = ''
|
||||||
|
self.recv_threadcounter = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
self._threadGo.wait()
|
||||||
|
if self._debug>3: sys.stderr.write(".")
|
||||||
|
|
||||||
|
self.recv_threadcounter = (self.recv_threadcounter + 1) & 0xffffffff
|
||||||
|
msgrecv = False
|
||||||
|
|
||||||
|
#### handle debug application
|
||||||
|
try:
|
||||||
|
q = None
|
||||||
|
b = self.recv_mbox.get(APP_DEBUG, None)
|
||||||
|
if (b != None):
|
||||||
|
for cmd in b.keys():
|
||||||
|
q = b[cmd]
|
||||||
|
if len(q):
|
||||||
|
buf,timestamp = q.pop(0)
|
||||||
|
if self._debug > 1: print >>sys.stderr,("recvthread: buf length: %x\t\t cmd: %x\t\t(%s)"%(len(buf), cmd, repr(buf)))
|
||||||
|
|
||||||
|
if (cmd == DEBUG_CMD_STRING):
|
||||||
|
if (len(buf) < 4):
|
||||||
|
if (len(q)):
|
||||||
|
buf2 = q.pop(0)
|
||||||
|
buf += buf2
|
||||||
|
q.insert(0,buf)
|
||||||
|
if self._debug: sys.stderr.write('*')
|
||||||
|
else:
|
||||||
|
length, = struct.unpack("<H", buf[2:4])
|
||||||
|
if self._debug >1: print >>sys.stderr,("len=%d"%length)
|
||||||
|
if (len(buf) < 4+length):
|
||||||
|
if (len(q)):
|
||||||
|
buf2 = q.pop(0)
|
||||||
|
buf += buf2
|
||||||
|
q.insert(0,buf)
|
||||||
|
if self._debug: sys.stderr.write('&')
|
||||||
|
else:
|
||||||
|
printbuf = buf[4:4+length]
|
||||||
|
requeuebuf = buf[4+length:]
|
||||||
|
if len(requeuebuf):
|
||||||
|
if self._debug>1: print >>sys.stderr,(" - DEBUG..requeuing %s"%repr(requeuebuf))
|
||||||
|
q.insert(0,requeuebuf)
|
||||||
|
print >>sys.stderr,("DEBUG: (%.3f) %s" % (timestamp, repr(printbuf)))
|
||||||
|
elif (cmd == DEBUG_CMD_HEX):
|
||||||
|
#print >>sys.stderr, repr(buf)
|
||||||
|
print >>sys.stderr, "DEBUG: (%.3f) %x"%(timestamp, struct.unpack("B", buf[4:5])[0])
|
||||||
|
elif (cmd == DEBUG_CMD_HEX16):
|
||||||
|
#print >>sys.stderr, repr(buf)
|
||||||
|
print >>sys.stderr, "DEBUG: (%.3f) %x"%(timestamp, struct.unpack("<H", buf[4:6])[0])
|
||||||
|
elif (cmd == DEBUG_CMD_HEX32):
|
||||||
|
#print >>sys.stderr, repr(buf)
|
||||||
|
print >>sys.stderr, "DEBUG: (%.3f) %x"%(timestamp, struct.unpack("<L", buf[4:8])[0])
|
||||||
|
elif (cmd == DEBUG_CMD_INT):
|
||||||
|
print >>sys.stderr, "DEBUG: (%.3f) %d"%(timestamp, struct.unpack("<L", buf[4:8])[0])
|
||||||
|
else:
|
||||||
|
print >>sys.stderr,('DEBUG COMMAND UNKNOWN: %x (buf=%s)'%(cmd,repr(buf)))
|
||||||
|
|
||||||
|
except:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
|
||||||
|
#### receive stuff.
|
||||||
|
if self._debug>2: print >> sys.stderr, "recvthread: Doing receiving...",self.ep5timeout
|
||||||
|
try:
|
||||||
|
#### first we populate the queue
|
||||||
|
msg = self._recvEP5(timeout=self.ep5timeout)
|
||||||
|
if len(msg) > 0:
|
||||||
|
self.recv_queue += msg
|
||||||
|
msgrecv = True
|
||||||
|
except usb.USBError, e:
|
||||||
|
#sys.stderr.write(repr(self.recv_queue))
|
||||||
|
#sys.stderr.write(repr(e))
|
||||||
|
errstr = repr(e)
|
||||||
|
if self._debug>4: print >>sys.stderr,repr(sys.exc_info())
|
||||||
|
if ('No error' in errstr):
|
||||||
|
pass
|
||||||
|
elif ('Connection timed out' in errstr):
|
||||||
|
pass
|
||||||
|
elif ('Operation timed out' in errstr):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if ('could not release intf' in errstr):
|
||||||
|
if self._debug: print "skipping"
|
||||||
|
pass
|
||||||
|
elif ('No such device' in errstr):
|
||||||
|
self._threadGo.clear()
|
||||||
|
#self.resetup(False) ## THIS IS A PROBLEM.
|
||||||
|
self.reset_event.set()
|
||||||
|
print "===== RESETUP set from recv thread"
|
||||||
|
elif ('Input/output error' in errstr): # USBerror 5
|
||||||
|
self._threadGo.clear()
|
||||||
|
#self.resetup(False) ## THIS IS A PROBLEM.
|
||||||
|
self.reset_event.set()
|
||||||
|
print "===== RESETUP set from recv thread"
|
||||||
|
|
||||||
|
else:
|
||||||
|
if self._debug: print "Error in runEP5() (receiving): %s" % errstr
|
||||||
|
if self._debug>2: sys.excepthook(*sys.exc_info())
|
||||||
|
self._usberrorcnt += 1
|
||||||
|
pass
|
||||||
|
except AttributeError,e:
|
||||||
|
if "'NoneType' object has no attribute 'bInterfaceNumber'" in str(e):
|
||||||
|
print "Error: dongle went away. USB bus problems?"
|
||||||
|
self._threadGo.clear()
|
||||||
|
#self.resetup(False)
|
||||||
|
self.reset_event.set()
|
||||||
|
|
||||||
|
except:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
|
||||||
|
if self._debug>2: print >> sys.stderr, "recvthread: Sorting mail..."
|
||||||
|
#### parse, sort, and deliver the mail.
|
||||||
|
try:
|
||||||
|
# FIXME: is this robust? or just overcomplex?
|
||||||
|
if len(self.recv_queue):
|
||||||
|
idx = self.recv_queue.find('@')
|
||||||
|
if (idx==-1):
|
||||||
|
if self._debug > 3:
|
||||||
|
sys.stderr.write('@')
|
||||||
|
else:
|
||||||
|
if (idx>0):
|
||||||
|
if self._debug: print >>sys.stderr,("runEP5(): idx>0?")
|
||||||
|
self.trash.append(self.recv_queue[:idx])
|
||||||
|
self.recv_queue = self.recv_queue[idx:]
|
||||||
|
|
||||||
|
# recv_queue is vulnerable here, but it's ok because we only modify it earlier in this same thread
|
||||||
|
# DON'T CHANGE recv_queue from other threads!
|
||||||
|
msg = self.recv_queue
|
||||||
|
msglen = len(msg)
|
||||||
|
#if self._debug > 2: print >> sys.stderr, "Sorting msg", len(msg), msg.encode("hex")
|
||||||
|
while (msglen>=5): # if not enough to parse length... we'll wait.
|
||||||
|
if not self._recv_time: # should be 0 to start and when done with a packet
|
||||||
|
self._recv_time = time.time()
|
||||||
|
app = ord(msg[1])
|
||||||
|
cmd = ord(msg[2])
|
||||||
|
length, = struct.unpack("<H", msg[3:5])
|
||||||
|
|
||||||
|
if self._debug>1: print>>sys.stderr,("recvthread: app=%x cmd=%x len=%x"%(app,cmd,length))
|
||||||
|
|
||||||
|
if (msglen >= length+5):
|
||||||
|
#### if the queue has enough characters to handle the next message... chop it and put it in the appropriate recv_mbox
|
||||||
|
msg = self.recv_queue[1:length+5] # drop the initial '@' and chop out the right number of chars
|
||||||
|
self.recv_queue = self.recv_queue[length+5:] # chop it out of the queue
|
||||||
|
|
||||||
|
b = self.recv_mbox.get(app,None)
|
||||||
|
|
||||||
|
if self.rsema.acquire(): # THREAD SAFETY DANCE
|
||||||
|
try:
|
||||||
|
if (b == None):
|
||||||
|
b = {}
|
||||||
|
self.recv_mbox[app] = b
|
||||||
|
except:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
finally:
|
||||||
|
self.rsema.release() # THREAD SAFETY DANCE COMPLETE
|
||||||
|
|
||||||
|
q = b.get(cmd)
|
||||||
|
|
||||||
|
if self.rsema.acquire(): # THREAD SAFETY DANCE
|
||||||
|
try:
|
||||||
|
if (q is None):
|
||||||
|
q = []
|
||||||
|
b[cmd] = q
|
||||||
|
|
||||||
|
q.append((msg, self._recv_time))
|
||||||
|
|
||||||
|
# notify receivers that a new msg is available
|
||||||
|
self.recv_event.set()
|
||||||
|
self._recv_time = 0 # we've delivered the current message
|
||||||
|
|
||||||
|
except:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
finally:
|
||||||
|
self.rsema.release() # THREAD SAFETY DANCE COMPLETE
|
||||||
|
|
||||||
|
else:
|
||||||
|
if self._debug>1: sys.stderr.write('=')
|
||||||
|
|
||||||
|
msg = self.recv_queue
|
||||||
|
msglen = len(msg)
|
||||||
|
# end of while loop
|
||||||
|
|
||||||
|
except:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
|
||||||
|
if self._debug>2: print >> sys.stderr, "readthread: Loop finished"
|
||||||
|
if not (msgrecv or len(msg)) :
|
||||||
|
#time.sleep(.1)
|
||||||
|
self.ep5timeout = EP_TIMEOUT_IDLE
|
||||||
|
else:
|
||||||
|
self.ep5timeout = EP_TIMEOUT_ACTIVE
|
||||||
|
if self._debug > 5: sys.stderr.write(" %s:%d .-P."%(msgrecv,len(msg)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
######## APPLICATION API ########
|
||||||
|
def recv(self, app, cmd=None, wait=USB_RX_WAIT):
|
||||||
|
'''
|
||||||
|
high-level USB EP5 receive.
|
||||||
|
checks the mbox for app "app" and command "cmd" and returns the next one in the queue
|
||||||
|
if any of this does not exist yet, wait for a RECV event until "wait" times out.
|
||||||
|
RECV events are generated by the low-level recv thread "runEP5_recv()"
|
||||||
|
'''
|
||||||
|
startTime = time.time()
|
||||||
|
self.recv_event.clear() # an event is only interesting if we've already failed to find our message
|
||||||
|
|
||||||
|
while (time.time() - startTime)*1000 < wait:
|
||||||
|
try:
|
||||||
|
b = self.recv_mbox.get(app)
|
||||||
|
if b:
|
||||||
|
if self._debug: print>>sys.stderr, "Recv msg",app,b,cmd
|
||||||
|
if cmd is None:
|
||||||
|
keys = b.keys()
|
||||||
|
if len(keys):
|
||||||
|
cmd = b.keys()[-1] # just grab one. no guarantees on the order
|
||||||
|
|
||||||
|
if b is not None and cmd is not None:
|
||||||
|
q = b.get(cmd)
|
||||||
|
if self._debug: print >>sys.stderr,"debug(recv) q='%s'"%repr(q)
|
||||||
|
|
||||||
|
if q is not None and self.rsema.acquire(False):
|
||||||
|
if self._debug>3: print ("rsema.UNlocked", "rsema.locked")[self.rsema.locked()],2
|
||||||
|
try:
|
||||||
|
resp, rt = q.pop(0)
|
||||||
|
|
||||||
|
self.rsema.release()
|
||||||
|
if self._debug>3: print ("rsema.UNlocked", "rsema.locked")[self.rsema.locked()],2
|
||||||
|
|
||||||
|
# bring it on home... this is the way out.
|
||||||
|
return resp[4:], rt
|
||||||
|
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.rsema.release()
|
||||||
|
|
||||||
|
self.recv_event.wait((wait - (time.time() - startTime)*1000)/1000) # wait on recv event, with timeout of remaining time
|
||||||
|
self.recv_event.clear() # clear event, if it's set
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
|
||||||
|
time.sleep(0.001)
|
||||||
|
|
||||||
|
raise(ChipconUsbTimeoutException())
|
||||||
|
|
||||||
|
def recvAll(self, app, cmd=None):
|
||||||
|
retval = self.recv_mbox.get(app,None)
|
||||||
|
if retval is not None:
|
||||||
|
if cmd is not None:
|
||||||
|
b = retval
|
||||||
|
if self.rsema.acquire():
|
||||||
|
#if self._debug: print ("rsema.UNlocked", "rsema.locked")[self.rsema.locked()],3
|
||||||
|
try:
|
||||||
|
retval = b.get(cmd)
|
||||||
|
b[cmd]=[]
|
||||||
|
if len(retval):
|
||||||
|
retval = [ (d[4:],t) for d,t in retval ]
|
||||||
|
except:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
finally:
|
||||||
|
self.rsema.release()
|
||||||
|
#if self._debug: print ("rsema.UNlocked", "rsema.locked")[self.rsema.locked()],3
|
||||||
|
else:
|
||||||
|
if self.rsema.acquire():
|
||||||
|
#if self._debug: print ("rsema.UNlocked", "rsema.locked")[self.rsema.locked()],4
|
||||||
|
try:
|
||||||
|
self.recv_mbox[app]={}
|
||||||
|
finally:
|
||||||
|
self.rsema.release()
|
||||||
|
#if self._debug: print ("rsema.UNlocked", "rsema.locked")[self.rsema.locked()],4
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def send(self, app, cmd, buf, wait=USB_TX_WAIT):
|
||||||
|
msg = "%c%c%s%s"%(app,cmd, struct.pack("<H",len(buf)),buf)
|
||||||
|
self.xsema.acquire()
|
||||||
|
self.xmit_queue.append(msg)
|
||||||
|
self.xmit_event.set()
|
||||||
|
self.xsema.release()
|
||||||
|
if self._debug: print "Sent Msg",msg.encode("hex")
|
||||||
|
return self.recv(app, cmd, wait)
|
||||||
|
|
||||||
|
def getDebugCodes(self, timeout=100):
|
||||||
|
x = self._recvEP0(timeout=timeout)
|
||||||
|
if (x != None and len(x)==2):
|
||||||
|
return struct.unpack("BB", x)
|
||||||
|
else:
|
||||||
|
return x
|
||||||
|
|
||||||
|
def ep0GetAddr(self):
|
||||||
|
addr = self._recvEP0(request=EP0_CMD_GET_ADDRESS)
|
||||||
|
return addr
|
||||||
|
def ep0Reset(self):
|
||||||
|
x = self._recvEP0(request=0xfe, value=0x5352, index=0x4e54)
|
||||||
|
return x
|
||||||
|
|
||||||
|
def ep0Peek(self, addr, length, timeout=100):
|
||||||
|
x = self._recvEP0(request=EP0_CMD_PEEKX, value=addr, length=length, timeout=timeout)
|
||||||
|
return x#x[3:]
|
||||||
|
|
||||||
|
def ep0Poke(self, addr, buf='\x00', timeout=100):
|
||||||
|
x = self._sendEP0(request=EP0_CMD_POKEX, buf=buf, value=addr, timeout=timeout)
|
||||||
|
return x
|
||||||
|
|
||||||
|
def ep0Ping(self, count=10):
|
||||||
|
good=0
|
||||||
|
bad=0
|
||||||
|
for x in range(count):
|
||||||
|
#r = self._recvEP0(3, 10)
|
||||||
|
try:
|
||||||
|
r = self._recvEP0(request=2, value=count, length=count, timeout=DEFAULT_USB_TIMEOUT)
|
||||||
|
print "PING: %d bytes received: %s"%(len(r), repr(r))
|
||||||
|
except ChipconUsbTimeoutException, e:
|
||||||
|
r = None
|
||||||
|
print "Ping Failed.",e
|
||||||
|
if r==None:
|
||||||
|
bad+=1
|
||||||
|
else:
|
||||||
|
good+=1
|
||||||
|
return (good,bad)
|
||||||
|
|
||||||
|
def debug(self, delay=1):
|
||||||
|
while True:
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print >>sys.stderr, ("DONGLE RESPONDING: mode :%x, last error# %d"%(self.getDebugCodes()))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
print >>sys.stderr,('recv_queue:\t\t (%d bytes) "%s"'%(len(self.recv_queue),repr(self.recv_queue)[:len(self.recv_queue)%39+20]))
|
||||||
|
print >>sys.stderr,('trash: \t\t (%d bytes) "%s"'%(len(self.trash),repr(self.trash)[:len(self.trash)%39+20]))
|
||||||
|
print >>sys.stderr,('recv_mbox \t\t (%d keys) "%s"'%(len(self.recv_mbox),repr(self.recv_mbox)[:len(repr(self.recv_mbox))%79]))
|
||||||
|
for x in self.recv_mbox.keys():
|
||||||
|
print >>sys.stderr,(' recv_mbox %d\t (%d records) "%s"'%(x,len(self.recv_mbox[x]),repr(self.recv_mbox[x])[:len(repr(self.recv_mbox[x]))%79]))
|
||||||
|
"""
|
||||||
|
print self.reprRadioState()
|
||||||
|
print self.reprClientState()
|
||||||
|
|
||||||
|
x,y,z = select.select([sys.stdin],[],[], delay)
|
||||||
|
if sys.stdin in x:
|
||||||
|
sys.stdin.read(1)
|
||||||
|
break
|
||||||
|
|
||||||
|
def getPartNum(self):
|
||||||
|
try:
|
||||||
|
r = self.send(APP_SYSTEM, SYS_CMD_PARTNUM, "", 10000)
|
||||||
|
r,rt = r
|
||||||
|
except ChipconUsbTimeoutException, e:
|
||||||
|
r = None
|
||||||
|
print "SETUP Failed.",e
|
||||||
|
|
||||||
|
return ord(r)
|
||||||
|
|
||||||
|
|
||||||
|
def ping(self, count=10, buf="ABCDEFGHIJKLMNOPQRSTUVWXYZ", wait=DEFAULT_USB_TIMEOUT, silent=False):
|
||||||
|
good=0
|
||||||
|
bad=0
|
||||||
|
start = time.time()
|
||||||
|
for x in range(count):
|
||||||
|
istart = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = self.send(APP_SYSTEM, SYS_CMD_PING, buf, wait)
|
||||||
|
r,rt = r
|
||||||
|
istop = time.time()
|
||||||
|
if not silent:
|
||||||
|
print "PING: %d bytes transmitted, received: %s (%f seconds)"%(len(buf), repr(r), istop-istart)
|
||||||
|
except ChipconUsbTimeoutException, e:
|
||||||
|
r = None
|
||||||
|
if not silent:
|
||||||
|
print "Ping Failed.",e
|
||||||
|
if r==None:
|
||||||
|
bad+=1
|
||||||
|
else:
|
||||||
|
good+=1
|
||||||
|
stop = time.time()
|
||||||
|
return (good,bad,stop-start)
|
||||||
|
|
||||||
|
def bootloader(self):
|
||||||
|
'''
|
||||||
|
switch to bootloader mode. based on Fergus Noble's CC-Bootloader (https://github.com/fnoble/CC-Bootloader)
|
||||||
|
this allows the firmware to be updated via USB instead of goodfet/ccdebugger
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
self._bootloader = True
|
||||||
|
r = self.send(APP_SYSTEM, SYS_CMD_BOOTLOADER, "", wait=1)
|
||||||
|
except ChipconUsbTimeoutException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def RESET(self):
|
||||||
|
try:
|
||||||
|
r = self.send(APP_SYSTEM, SYS_CMD_RESET, "RESET_NOW\x00")
|
||||||
|
except ChipconUsbTimeoutException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def peek(self, addr, bytecount=1):
|
||||||
|
r, t = self.send(APP_SYSTEM, SYS_CMD_PEEK, struct.pack("<HH", bytecount, addr))
|
||||||
|
return r
|
||||||
|
|
||||||
|
def poke(self, addr, data):
|
||||||
|
r, t = self.send(APP_SYSTEM, SYS_CMD_POKE, struct.pack("<H", addr) + data)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def pokeReg(self, addr, data):
|
||||||
|
r, t = self.send(APP_SYSTEM, SYS_CMD_POKE_REG, struct.pack("<H", addr) + data)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def getBuildInfo(self):
|
||||||
|
r, t = self.send(APP_SYSTEM, SYS_CMD_BUILDTYPE, '')
|
||||||
|
return r
|
||||||
|
|
||||||
|
def getInterruptRegisters(self):
|
||||||
|
regs = {}
|
||||||
|
# IEN0,1,2
|
||||||
|
regs['IEN0'] = self.peek(IEN0,1)
|
||||||
|
regs['IEN1'] = self.peek(IEN1,1)
|
||||||
|
regs['IEN2'] = self.peek(IEN2,1)
|
||||||
|
# TCON
|
||||||
|
regs['TCON'] = self.peek(TCON,1)
|
||||||
|
# S0CON
|
||||||
|
regs['S0CON'] = self.peek(S0CON,1)
|
||||||
|
# IRCON
|
||||||
|
regs['IRCON'] = self.peek(IRCON,1)
|
||||||
|
# IRCON2
|
||||||
|
regs['IRCON2'] = self.peek(IRCON2,1)
|
||||||
|
# S1CON
|
||||||
|
regs['S1CON'] = self.peek(S1CON,1)
|
||||||
|
# RFIF
|
||||||
|
regs['RFIF'] = self.peek(RFIF,1)
|
||||||
|
# DMAIE
|
||||||
|
regs['DMAIE'] = self.peek(DMAIE,1)
|
||||||
|
# DMAIF
|
||||||
|
regs['DMAIF'] = self.peek(DMAIF,1)
|
||||||
|
# DMAIRQ
|
||||||
|
regs['DMAIRQ'] = self.peek(DMAIRQ,1)
|
||||||
|
return regs
|
||||||
|
|
||||||
|
def reprHardwareConfig(self):
|
||||||
|
output= []
|
||||||
|
|
||||||
|
hardware= self.getBuildInfo()
|
||||||
|
output.append("Dongle: %s" % hardware.split(' ')[0])
|
||||||
|
try:
|
||||||
|
output.append("Firmware rev: %s" % hardware.split('r')[1])
|
||||||
|
except:
|
||||||
|
output.append("Firmware rev: Not found! Update needed!")
|
||||||
|
# see if we have a bootloader by loooking for it's recognition semaphores
|
||||||
|
# in SFR I2SCLKF0 & I2SCLKF1
|
||||||
|
if(self.peek(0xDF46,1) == '\xF0' and self.peek(0xDF47,1) == '\x0D'):
|
||||||
|
output.append("Bootloader: CC-Bootloader")
|
||||||
|
else:
|
||||||
|
output.append("Bootloader: Not installed")
|
||||||
|
return "\n".join(output)
|
||||||
|
|
||||||
|
def reprSoftwareConfig(self):
|
||||||
|
output= []
|
||||||
|
|
||||||
|
output.append("rflib rev: %s" % RFLIB_VERSION)
|
||||||
|
return "\n".join(output)
|
||||||
|
|
||||||
|
def printClientState(self, width=120):
|
||||||
|
print self.reprClientState(width)
|
||||||
|
|
||||||
|
def reprClientState(self, width=120):
|
||||||
|
output = ["="*width]
|
||||||
|
output.append(' client thread cycles: %d/%d' % (self.recv_threadcounter,self.send_threadcounter))
|
||||||
|
output.append(' client errored cycles: %d' % self._usberrorcnt)
|
||||||
|
output.append(' recv_queue: (%d bytes) %s'%(len(self.recv_queue),repr(self.recv_queue)[:width-42]))
|
||||||
|
output.append(' trash: (%d blobs) "%s"'%(len(self.trash),repr(self.trash)[:width-44]))
|
||||||
|
output.append(' recv_mbox (%d keys) "%s"'%(len(self.recv_mbox),repr([hex(x) for x in self.recv_mbox.keys()])[:width-44]))
|
||||||
|
for app in self.recv_mbox.keys():
|
||||||
|
appbox = self.recv_mbox[app]
|
||||||
|
output.append(' app 0x%x (%d records)'%(app,len(appbox)))
|
||||||
|
for cmd in appbox.keys():
|
||||||
|
output.append(' [0x%x] (%d frames) "%s"'%(cmd, len(appbox[cmd]), repr(appbox[cmd])[:width-36]))
|
||||||
|
output.append('')
|
||||||
|
return "\n".join(output)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def unittest(self, mhz=24):
|
||||||
|
print "\nTesting USB ping()"
|
||||||
|
self.ping(3)
|
||||||
|
|
||||||
|
print "\nTesting USB ep0Ping()"
|
||||||
|
self.ep0Ping()
|
||||||
|
|
||||||
|
print "\nTesting USB enumeration"
|
||||||
|
print "getString(0,100): %s" % repr(self._do.getString(0,100))
|
||||||
|
|
||||||
|
print "\nTesting USB EP MAX_PACKET_SIZE handling (ep0Peek(0xf000, 100))"
|
||||||
|
print repr(self.ep0Peek(0xf000, 100))
|
||||||
|
|
||||||
|
print "\nTesting USB EP MAX_PACKET_SIZE handling (peek(0xf000, 300))"
|
||||||
|
print repr(self.peek(0xf000, 400))
|
||||||
|
|
||||||
|
print "\nTesting USB poke/peek"
|
||||||
|
data = "".join([chr(c) for c in xrange(120)])
|
||||||
|
where = 0xf300
|
||||||
|
self.poke(where, data)
|
||||||
|
ndata = self.peek(where, len(data))
|
||||||
|
if ndata != data:
|
||||||
|
print " *FAILED*\n '%s'\n '%s'" % (data.encode("hex"), ndata.encode("hex"))
|
||||||
|
raise(Exception(" *FAILED*\n '%s'\n '%s'" % (data.encode("hex"), ndata.encode("hex"))))
|
||||||
|
else:
|
||||||
|
print " passed '%s'" % (ndata.encode("hex"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
idx = 0
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
idx = int(sys.argv.pop())
|
||||||
|
d = USBDongle(idx=idx, debug=False)
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
||||||
|
RFLIB_VERSION=323
|
Loading…
Reference in New Issue