mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-18 05:33:58 +00:00
working BlockDevice
This commit is contained in:
+60
-9
@@ -48,14 +48,65 @@ func main() {
|
|||||||
cid := sdcard.CID()
|
cid := sdcard.CID()
|
||||||
fmt.Printf("name=%s\ncsd=\n%s\n", cid.ProductName(), csd.String())
|
fmt.Printf("name=%s\ncsd=\n%s\n", cid.ProductName(), csd.String())
|
||||||
|
|
||||||
var buf [512]byte
|
const placeholderEraseSectorSize = 512
|
||||||
for i := 0; i < 11; i += 1 {
|
bd, err := sd.NewBlockDevice(sdcard, int(csd.ReadBlockLen()), csd.NumberOfBlocks(), placeholderEraseSectorSize)
|
||||||
err = sdcard.ReadBlocks(buf[:], int64(i))
|
if err != nil {
|
||||||
if err != nil {
|
panic("block device creation:" + err.Error())
|
||||||
println("err reading block", i, ":", err.Error())
|
}
|
||||||
continue
|
var mc MemChecker
|
||||||
}
|
|
||||||
expectCRC := sd.CRC16(buf[:])
|
ok, badBlkIdx, err := mc.MemCheck(bd, 2, 100)
|
||||||
fmt.Printf("block %d theircrc=%#x ourcrc=%#x:\n\t%#x\n", i, sdcard.LastReadCRC(), expectCRC, buf[:])
|
if err != nil {
|
||||||
|
panic("memcheck:" + err.Error())
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
println("bad block", badBlkIdx)
|
||||||
|
} else {
|
||||||
|
println("memcheck ok")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MemChecker struct {
|
||||||
|
rdBuf []byte
|
||||||
|
storeBuf []byte
|
||||||
|
wrBuf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *MemChecker) MemCheck(bd *sd.BlockDevice, blockIdx, numBlocks int64) (memOK bool, badBlockIdx int64, err error) {
|
||||||
|
size := bd.BlockSize() * numBlocks
|
||||||
|
if len(mc.rdBuf) < int(size) {
|
||||||
|
mc.rdBuf = make([]byte, size)
|
||||||
|
mc.wrBuf = make([]byte, size)
|
||||||
|
mc.storeBuf = make([]byte, size)
|
||||||
|
for i := range mc.wrBuf {
|
||||||
|
mc.wrBuf[i] = byte(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Start by storing the original block contents.
|
||||||
|
_, err = bd.ReadAt(mc.storeBuf, blockIdx)
|
||||||
|
if err != nil {
|
||||||
|
return false, blockIdx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the test pattern.
|
||||||
|
_, err = bd.WriteAt(mc.wrBuf, blockIdx)
|
||||||
|
if err != nil {
|
||||||
|
return false, blockIdx, err
|
||||||
|
}
|
||||||
|
// Read back the test pattern.
|
||||||
|
_, err = bd.ReadAt(mc.rdBuf, blockIdx)
|
||||||
|
if err != nil {
|
||||||
|
return false, blockIdx, err
|
||||||
|
}
|
||||||
|
for j := 0; j < len(mc.rdBuf); j++ {
|
||||||
|
// Compare the read back data with the test pattern.
|
||||||
|
if mc.rdBuf[j] != mc.wrBuf[j] {
|
||||||
|
badBlock := blockIdx + int64(j)/bd.BlockSize()
|
||||||
|
return false, badBlock, nil
|
||||||
|
}
|
||||||
|
mc.rdBuf[j] = 0
|
||||||
|
}
|
||||||
|
// Leave the card in it's previous state.
|
||||||
|
_, err = bd.WriteAt(mc.storeBuf, blockIdx)
|
||||||
|
return true, -1, nil
|
||||||
|
}
|
||||||
|
|||||||
+44
-30
@@ -18,10 +18,10 @@ var _ io.WriterAt = (*BlockDevice)(nil)
|
|||||||
type Card interface {
|
type Card interface {
|
||||||
// WriteBlocks writes the given data to the card, starting at the given block index.
|
// WriteBlocks writes the given data to the card, starting at the given block index.
|
||||||
// The data must be a multiple of the block size.
|
// The data must be a multiple of the block size.
|
||||||
WriteBlocks(data []byte, startBlockIdx int64) error
|
WriteBlocks(data []byte, startBlockIdx int64) (int, error)
|
||||||
// ReadBlocks reads the given number of blocks from the card, starting at the given block index.
|
// ReadBlocks reads the given number of blocks from the card, starting at the given block index.
|
||||||
// The dst buffer must be a multiple of the block size.
|
// The dst buffer must be a multiple of the block size.
|
||||||
ReadBlocks(dst []byte, startBlockIdx int64) error
|
ReadBlocks(dst []byte, startBlockIdx int64) (int, error)
|
||||||
// EraseSectors erases sectors starting at startSectorIdx to startSectorIdx+numSectors.
|
// EraseSectors erases sectors starting at startSectorIdx to startSectorIdx+numSectors.
|
||||||
EraseSectors(startSectorIdx, numSectors int64) error
|
EraseSectors(startSectorIdx, numSectors int64) error
|
||||||
}
|
}
|
||||||
@@ -59,13 +59,12 @@ func (bd *BlockDevice) ReadAt(p []byte, off int64) (n int, err error) {
|
|||||||
if off < 0 {
|
if off < 0 {
|
||||||
return 0, errNegativeOffset
|
return 0, errNegativeOffset
|
||||||
}
|
}
|
||||||
blockSize := len(bd.blockbuf)
|
|
||||||
blockIdx := bd.blk.idx(off)
|
blockIdx := bd.blk.idx(off)
|
||||||
blockOff := bd.blk.off(off)
|
blockOff := bd.blk.off(off)
|
||||||
if blockOff != 0 {
|
if blockOff != 0 {
|
||||||
// Non-aligned first block case.
|
// Non-aligned first block case.
|
||||||
println("read", len(bd.blockbuf), "bytes from block", blockIdx)
|
if _, err = bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
|
||||||
if err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
|
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
n += copy(p, bd.blockbuf[blockOff:])
|
n += copy(p, bd.blockbuf[blockOff:])
|
||||||
@@ -73,22 +72,22 @@ func (bd *BlockDevice) ReadAt(p []byte, off int64) (n int, err error) {
|
|||||||
blockIdx++
|
blockIdx++
|
||||||
}
|
}
|
||||||
|
|
||||||
remaining := len(p) - n
|
fullBlocksToRead := bd.blk.idx(int64(len(p)))
|
||||||
if remaining >= blockSize {
|
if fullBlocksToRead > 0 {
|
||||||
// 1 or more full blocks case.
|
// 1 or more full blocks case.
|
||||||
endOffset := remaining - int(bd.blk.off(int64(remaining)))
|
endOffset := fullBlocksToRead * bd.blk.size()
|
||||||
err = bd.card.ReadBlocks(p[:endOffset], blockIdx)
|
ngot, err := bd.card.ReadBlocks(p[:endOffset], blockIdx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return n, err
|
return n + ngot, err
|
||||||
}
|
}
|
||||||
p = p[endOffset:]
|
p = p[endOffset:]
|
||||||
n += endOffset
|
n += ngot
|
||||||
blockIdx += int64(endOffset / blockSize)
|
blockIdx += fullBlocksToRead
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(p) > 0 {
|
if len(p) > 0 {
|
||||||
// Non-aligned last block case.
|
// Non-aligned last block case.
|
||||||
if err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
|
if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
n += copy(p, bd.blockbuf)
|
n += copy(p, bd.blockbuf)
|
||||||
@@ -101,51 +100,66 @@ func (bd *BlockDevice) WriteAt(p []byte, off int64) (n int, err error) {
|
|||||||
if off < 0 {
|
if off < 0 {
|
||||||
return 0, errNegativeOffset
|
return 0, errNegativeOffset
|
||||||
}
|
}
|
||||||
blockSize := len(bd.blockbuf)
|
|
||||||
blockIdx := bd.blk.idx(off)
|
blockIdx := bd.blk.idx(off)
|
||||||
blockOff := bd.blk.off(off)
|
blockOff := bd.blk.off(off)
|
||||||
if blockOff != 0 {
|
if blockOff != 0 {
|
||||||
// Non-aligned first block case.
|
// Non-aligned first block case.
|
||||||
if err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
|
if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
n += copy(bd.blockbuf[blockOff:], p)
|
nexpect := copy(bd.blockbuf[blockOff:], p)
|
||||||
if err := bd.card.WriteBlocks(bd.blockbuf, blockIdx); err != nil {
|
ngot, err := bd.card.WriteBlocks(bd.blockbuf, blockIdx)
|
||||||
|
if err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
|
} else if ngot != len(bd.blockbuf) {
|
||||||
|
return n, io.ErrShortWrite
|
||||||
}
|
}
|
||||||
p = p[n:]
|
n += nexpect
|
||||||
|
p = p[nexpect:]
|
||||||
blockIdx++
|
blockIdx++
|
||||||
}
|
}
|
||||||
|
|
||||||
remaining := len(p) - n
|
fullBlocksToWrite := bd.blk.idx(int64(len(p)))
|
||||||
if remaining >= blockSize {
|
if fullBlocksToWrite > 0 {
|
||||||
// 1 or more full blocks case.
|
// 1 or more full blocks case.
|
||||||
endOffset := remaining - int(bd.blk.off(int64(remaining)))
|
endOffset := fullBlocksToWrite * bd.blk.size()
|
||||||
err = bd.card.WriteBlocks(p[:endOffset], blockIdx)
|
ngot, err := bd.card.WriteBlocks(p[:endOffset], blockIdx)
|
||||||
|
n += ngot
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
|
} else if ngot != int(endOffset) {
|
||||||
|
return n, io.ErrShortWrite
|
||||||
}
|
}
|
||||||
p = p[endOffset:]
|
p = p[ngot:]
|
||||||
n += endOffset
|
blockIdx += fullBlocksToWrite
|
||||||
blockIdx += int64(endOffset / blockSize)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(p) > 0 {
|
if len(p) > 0 {
|
||||||
// Non-aligned last block case.
|
// Non-aligned last block case.
|
||||||
if err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
|
if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
n += copy(bd.blockbuf, p)
|
copy(bd.blockbuf, p)
|
||||||
if err := bd.card.WriteBlocks(bd.blockbuf, blockIdx); err != nil {
|
ngot, err := bd.card.WriteBlocks(bd.blockbuf, blockIdx)
|
||||||
|
if err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
|
} else if ngot != len(bd.blockbuf) {
|
||||||
|
return n, io.ErrShortWrite
|
||||||
}
|
}
|
||||||
|
n += len(p)
|
||||||
}
|
}
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size returns the number of bytes in this block device.
|
// Size returns the number of bytes in this block device.
|
||||||
func (bd *BlockDevice) Size() int64 {
|
func (bd *BlockDevice) Size() int64 {
|
||||||
return int64(len(bd.blockbuf)) * bd.numblocks
|
return bd.BlockSize() * bd.numblocks
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockSize returns the size of a block in bytes.
|
||||||
|
func (bd *BlockDevice) BlockSize() int64 {
|
||||||
|
return bd.blk.size()
|
||||||
}
|
}
|
||||||
|
|
||||||
// EraseBlocks erases the given number of blocks. An implementation may
|
// EraseBlocks erases the given number of blocks. An implementation may
|
||||||
@@ -163,7 +177,7 @@ func (bd *BlockDevice) EraseBlockSize() int64 {
|
|||||||
return bd.eraseBlockSize
|
return bd.eraseBlockSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// blkIdxer is a helper for calculating block indexes and offsets.
|
// blkIdxer is a helper for calculating block indices and offsets.
|
||||||
type blkIdxer struct {
|
type blkIdxer struct {
|
||||||
blockshift int64
|
blockshift int64
|
||||||
blockmask int64
|
blockmask int64
|
||||||
|
|||||||
+74
-58
@@ -143,121 +143,136 @@ func (d *SPICard) updateCSDCID() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReadBlock reads to a buffer multiple of 512 bytes from sdcard into dst starting at block `startBlockIdx`.
|
// ReadBlock reads to a buffer multiple of 512 bytes from sdcard into dst starting at block `startBlockIdx`.
|
||||||
func (d *SPICard) ReadBlocks(dst []byte, startBlockIdx int64) error {
|
func (d *SPICard) ReadBlocks(dst []byte, startBlockIdx int64) (int, error) {
|
||||||
numblocks, err := d.checkBounds(startBlockIdx, len(dst))
|
numblocks, err := d.checkBounds(startBlockIdx, len(dst))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
if d.kind != TypeSDHC {
|
if d.kind != TypeSDHC {
|
||||||
startBlockIdx <<= 9 // Multiply by 512 for non high capacity SD cards.
|
startBlockIdx <<= 9 // Multiply by 512 for non high capacity SD cards.
|
||||||
}
|
}
|
||||||
|
|
||||||
d.csEnable(true)
|
d.csEnable(true)
|
||||||
defer d.endTx()
|
defer d.csEnable(false)
|
||||||
|
|
||||||
if numblocks == 1 {
|
if numblocks == 1 {
|
||||||
_, err := d.card_command(cmdReadSingleBlock, uint32(startBlockIdx))
|
return d.read_block_single(dst, startBlockIdx)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return d.read_data(dst)
|
|
||||||
|
|
||||||
} else if numblocks > 1 {
|
} else if numblocks > 1 {
|
||||||
|
// TODO: implement multi block transaction reading.
|
||||||
|
// Rust code is failing here.
|
||||||
blocksize := int(d.blk.size())
|
blocksize := int(d.blk.size())
|
||||||
_, err = d.card_command(cmdReadMultipleBlock, uint32(startBlockIdx))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < numblocks; i++ {
|
for i := 0; i < numblocks; i++ {
|
||||||
offset := i * blocksize
|
dataoff := i * blocksize
|
||||||
err = d.read_data(dst[offset : offset+blocksize])
|
d.csEnable(true)
|
||||||
|
_, err := d.read_block_single(dst[dataoff:dataoff+blocksize], int64(i)+startBlockIdx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return dataoff, err
|
||||||
}
|
}
|
||||||
|
d.csEnable(false)
|
||||||
}
|
}
|
||||||
return nil
|
return len(dst), nil
|
||||||
}
|
}
|
||||||
panic("unreachable numblocks<=0")
|
panic("unreachable numblocks<=0")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SPICard) endTx() {
|
|
||||||
d.card_command(cmdStopTransmission, 0)
|
|
||||||
d.csEnable(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *SPICard) EraseSectors(startSector, numberSectors int64) error {
|
func (d *SPICard) EraseSectors(startSector, numberSectors int64) error {
|
||||||
return errors.New("sd:erase not implemented")
|
return errors.New("sd:erase not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteBlocks writes to sdcard from a buffer multiple of 512 bytes from src starting at block `startBlockIdx`.
|
// WriteBlocks writes to sdcard from a buffer multiple of 512 bytes from src starting at block `startBlockIdx`.
|
||||||
func (d *SPICard) WriteBlocks(data []byte, startBlockIdx int64) error {
|
func (d *SPICard) WriteBlocks(data []byte, startBlockIdx int64) (int, error) {
|
||||||
numblocks, err := d.checkBounds(startBlockIdx, len(data))
|
numblocks, err := d.checkBounds(startBlockIdx, len(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
if d.kind != TypeSDHC {
|
if d.kind != TypeSDHC {
|
||||||
startBlockIdx <<= 9 // Multiply by 512 for non high capacity SD cards.
|
startBlockIdx <<= 9 // Multiply by 512 for non high capacity SD cards.
|
||||||
}
|
}
|
||||||
d.csEnable(true)
|
d.csEnable(true)
|
||||||
defer d.endTx()
|
defer d.csEnable(false)
|
||||||
|
|
||||||
writeTimeout := 2 * d.timeout
|
writeTimeout := 2 * d.timeout
|
||||||
if numblocks == 1 {
|
if numblocks == 1 {
|
||||||
_, err = d.card_command(cmdWriteBlock, uint32(startBlockIdx))
|
return d.write_block_single(data, startBlockIdx)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = d.write_data(tokSTART_BLOCK, data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = d.wait_not_busy(writeTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
status, err := d.card_command(cmdSendStatus, 0)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if status != 0 {
|
|
||||||
return makeResponseError(response1(status))
|
|
||||||
}
|
|
||||||
status, err = d.receive()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if status != 0 {
|
|
||||||
return errWrite
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
|
|
||||||
} else if numblocks > 1 {
|
} else if numblocks > 1 {
|
||||||
// Start multi block write.
|
// Start multi block write.
|
||||||
blocksize := 1 << d.blk.size()
|
blocksize := int(d.blk.size())
|
||||||
_, err = d.card_command(cmdWriteMultipleBlock, uint32(startBlockIdx))
|
_, err = d.card_command(cmdWriteMultipleBlock, uint32(startBlockIdx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < numblocks; i++ {
|
for i := 0; i < numblocks; i++ {
|
||||||
offset := i * blocksize
|
offset := i * blocksize
|
||||||
err = d.wait_not_busy(writeTimeout)
|
err = d.wait_not_busy(writeTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
err = d.write_data(tokWRITE_MULT, data[offset:offset+blocksize])
|
err = d.write_data(tokWRITE_MULT, data[offset:offset+blocksize])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Stop the multi write operation.
|
// Stop the multi write operation.
|
||||||
err = d.wait_not_busy(writeTimeout)
|
err = d.wait_not_busy(writeTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
return d.send(tokSTOP_TRAN)
|
err = d.send(tokSTOP_TRAN)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
_, err = d.card_command(cmdStopTransmission, 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(data), nil
|
||||||
}
|
}
|
||||||
panic("unreachable numblocks<=0")
|
panic("unreachable numblocks<=0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *SPICard) read_block_single(dst []byte, startBlockIdx int64) (int, error) {
|
||||||
|
_, err := d.card_command(cmdReadSingleBlock, uint32(startBlockIdx))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
err = d.read_data(dst)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(dst), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SPICard) write_block_single(data []byte, startBlockIdx int64) (_ int, err error) {
|
||||||
|
_, err = d.card_command(cmdWriteBlock, uint32(startBlockIdx))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
err = d.write_data(tokSTART_BLOCK, data)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
err = d.wait_not_busy(2 * d.timeout)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
status, err := d.card_command(cmdSendStatus, 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if status != 0 {
|
||||||
|
return 0, makeResponseError(response1(status))
|
||||||
|
}
|
||||||
|
status, err = d.receive()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if status != 0 {
|
||||||
|
return 0, errWrite
|
||||||
|
}
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *SPICard) checkBounds(startBlockIdx int64, datalen int) (numblocks int, err error) {
|
func (d *SPICard) checkBounds(startBlockIdx int64, datalen int) (numblocks int, err error) {
|
||||||
if startBlockIdx >= d.NumberOfBlocks() {
|
if startBlockIdx >= d.NumberOfBlocks() {
|
||||||
return 0, errOOB
|
return 0, errOOB
|
||||||
@@ -354,9 +369,10 @@ func (d *SPICard) read_data(data []byte) (err error) {
|
|||||||
status, err = d.receive()
|
status, err = d.receive()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
} else if status != 0xff {
|
||||||
if status != 0xff {
|
|
||||||
break
|
break
|
||||||
|
} else if tm.expired() {
|
||||||
|
return errReadTimeout
|
||||||
}
|
}
|
||||||
d.yield()
|
d.yield()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user