529 lines
28 KiB
Plaintext
529 lines
28 KiB
Plaintext
|
SXTVRENDEZVOUS = $8000 - 4
|
||
|
RESERVED = SXTVRENDEZVOUS - 4
|
||
|
SDSPIRENDEZVOUS = RESERVED - 3 * 4
|
||
|
SXFS2RENDEZVOUS = SDSPIRENDEZVOUS - 4 * 4 ' four rendezvous variables
|
||
|
SXFSRENDEZVOUS = SXFS2RENDEZVOUS - 4 * 4 ' four rendezvous variables
|
||
|
METADATABUFFER = SXFSRENDEZVOUS - 512
|
||
|
|
||
|
_free = ($8000 - METADATABUFFER) / 4
|
||
|
|
||
|
con
|
||
|
SECTORSIZE = 512
|
||
|
|
||
|
' filestuff definitions
|
||
|
' filestuff must be long-aligned
|
||
|
{ long } _length = 0
|
||
|
{ long } _leftInFile = 4
|
||
|
{ long } _sopDir = 8 ' sector/offset "pointer" to directory entry
|
||
|
{ long } _sopTail = 12 ' sector/offset "pointer" to last cluster# in FAT chain
|
||
|
{ word } _cluster = 16
|
||
|
{ word } _sector = 18 ' sector# within cluster; ranges from 0 to sectorsPerCluster-1
|
||
|
{ word } _bp = 20 ' byte pointer (actually an index into the sector buffer); range [0..511]
|
||
|
{ word } _mode = 22 ' 0: closed; "R": open for reading; "W": open for writing.
|
||
|
_buffer = 24 ' 512-byte buffer starts here
|
||
|
|
||
|
SIZEOFFILESTUFF = SECTORSIZE + 24
|
||
|
|
||
|
pub Start( pbrSector )
|
||
|
|
||
|
bytesPerSector := PeekW( pMetadataBuffer + $0b )
|
||
|
sectorsPerCluster := byte[pMetadataBuffer][$0d]
|
||
|
clusterShift := >| sectorsPerCluster - 1
|
||
|
reservedSectors := PeekW( pMetadataBuffer + $0e )
|
||
|
numberOfFats := byte[pMetadataBuffer][$10]
|
||
|
sectorsPerFat := PeekW( pMetadataBuffer + $16 )
|
||
|
RootDirEntries := PeekW( pMetadataBuffer + $11 )
|
||
|
fatSector0 := pbrSector + reservedSectors
|
||
|
dirSector0 := fatSector0 + numberOfFats * sectorsPerFat
|
||
|
dataSector0 := dirSector0 + rootDirEntries >> 4
|
||
|
|
||
|
' start sxfs2 cog
|
||
|
long[pCommand] := "?"
|
||
|
if cognew( @Sxfs2Entry, 0 ) < 0
|
||
|
abort -1
|
||
|
repeat while long[pCommand]
|
||
|
|
||
|
pri PeekW( a )
|
||
|
return byte[a++] + byte[a] << 8
|
||
|
|
||
|
pri PeekL( a )
|
||
|
return byte[a++] + byte[a++] << 8 + byte[a++] << 16 + byte[a] << 24
|
||
|
|
||
|
dat
|
||
|
org 0
|
||
|
Sxfs2Entry
|
||
|
AckNoReturnCode
|
||
|
mov temp, #0 ' clear return code
|
||
|
AckReturnCode ' enter here with temp = return code
|
||
|
wrlong temp, pParam
|
||
|
mov temp, #0
|
||
|
wrlong temp, pCommand ' clear command to signal operation complete.
|
||
|
|
||
|
mov dirty, temp ' Ensure that metatdata is flushed before coming here.
|
||
|
neg temp, #1
|
||
|
mov currentSector, temp
|
||
|
|
||
|
:ready ' wait for new command
|
||
|
rdlong temp, pCommand wz
|
||
|
if_z jmp #:ready
|
||
|
|
||
|
cmp temp, #"?" wz
|
||
|
if_e jmp #AckNoReturnCode
|
||
|
cmp temp, #"W" wz
|
||
|
if_e jmp #Write
|
||
|
cmp temp, #"C" wz
|
||
|
if_e jmp #Close
|
||
|
cmp temp, #"X" wz
|
||
|
if_e jmp #Execute
|
||
|
' else
|
||
|
neg temp, #100 ' return code -100 : bad command
|
||
|
jmp #AckReturnCode
|
||
|
|
||
|
'*********************************************************************************************************************
|
||
|
'* *
|
||
|
'* Write stuff *
|
||
|
'* *
|
||
|
'*********************************************************************************************************************
|
||
|
Write ' ( pFilestuff, ptr, n )
|
||
|
rdlong pFilestuff, pSxfsParam0
|
||
|
rdlong srcPtr, pSxfsParam1
|
||
|
rdlong nBytes, pSxfsParam2
|
||
|
|
||
|
mov retcode, nBytes
|
||
|
|
||
|
mov p, pFilestuff ' long[ pFilestuff+_length ] += n
|
||
|
'add p, #_length (unnecessary cuz _length = 0)
|
||
|
rdlong temp, p
|
||
|
add temp, nBytes
|
||
|
wrlong temp, p
|
||
|
|
||
|
|
||
|
' repeat while (leftInSector := SECTORSIZE - word[pIoblock+_bp]) < n
|
||
|
:while
|
||
|
mov p, pFilestuff
|
||
|
add p, #_bp ' p points to pFilestuff->bp
|
||
|
mov leftInSector, k512
|
||
|
rdword temp, p ' temp = pFilestuff->bp
|
||
|
sub leftInSector, temp
|
||
|
cmp leftInSector, nBytes wc, wz
|
||
|
if_ae jmp #:done
|
||
|
' bytemove( pFilestuff + _buffer + word[pFilestuff+_bp], ptr, leftInSector )
|
||
|
mov p, pFilestuff
|
||
|
add p, #_buffer
|
||
|
add p, temp ' p now points bp bytes into buffer.
|
||
|
mov q, srcPtr
|
||
|
mov n, leftInSector
|
||
|
call #ByteCopy
|
||
|
' ptr += leftInSector
|
||
|
add srcPtr, leftInSector ' advance the source pointer by #bytes written.
|
||
|
' n -= leftInSector
|
||
|
sub nBytes, leftInSector ' decrease #bytes to write by #bytes written.
|
||
|
' WriteNextSector( pIoblock )
|
||
|
call #WriteNextSector
|
||
|
|
||
|
jmp #:while
|
||
|
:done
|
||
|
' bytemove( pFilestuff + _buffer + word[pIoblock+_bp], ptr, n )
|
||
|
mov p, pFilestuff
|
||
|
add p, #_buffer
|
||
|
add p, temp ' temp = pFilestuff->bp
|
||
|
mov q, srcPtr
|
||
|
mov n, nBytes
|
||
|
call #ByteCopy
|
||
|
' word[pFilestuff+_bp] += n
|
||
|
mov p, pFilestuff
|
||
|
add p, #_bp
|
||
|
rdword temp, p
|
||
|
add temp, nBytes ' advance bp by #bytes written.
|
||
|
wrword temp, p
|
||
|
|
||
|
call #FlushMetadataSector
|
||
|
|
||
|
mov temp, retcode
|
||
|
jmp #AckReturnCode
|
||
|
|
||
|
WriteNextSector
|
||
|
mov p, pFilestuff
|
||
|
add p, #_sector
|
||
|
rdword temp, p wz
|
||
|
if_nz jmp #:writeSector ' sector# <> 0? Write that sector.
|
||
|
|
||
|
' Writing sector 0 implies that we're starting a new cluster, so find an empty slot in the FAT
|
||
|
mov cluster, #0
|
||
|
mov sector, fatSector0
|
||
|
mov i, sectorsPerFat
|
||
|
:forEachFatSector
|
||
|
call #ReadMetadataSector
|
||
|
|
||
|
mov p, pMetadataBuffer
|
||
|
mov j, #256
|
||
|
:forEachSlotInSector
|
||
|
rdword temp, p wz ' examine current slot
|
||
|
if_z jmp #:foundEmptySlot
|
||
|
|
||
|
add p, #2
|
||
|
add cluster, #1
|
||
|
djnz j, #:forEachSlotInSector
|
||
|
|
||
|
add sector, #1
|
||
|
djnz i, #:forEachFatSector
|
||
|
|
||
|
neg temp, #110 ' -110: FAT full.
|
||
|
jmp #AckReturnCode
|
||
|
:foundEmptySlot
|
||
|
neg temp, #1
|
||
|
wrword temp, p ' Claim empty slot by writing $ffff.
|
||
|
mov dirty, #1 ' We've modified metadata.
|
||
|
|
||
|
mov sop, sector
|
||
|
shl sop, #9
|
||
|
sub p, pMetadataBuffer
|
||
|
add sop, p ' sop "points" to slot.
|
||
|
|
||
|
mov p, pFilestuff
|
||
|
add p, #_cluster
|
||
|
wrword cluster, p ' save cluster# in pFilestuff->cluster
|
||
|
|
||
|
sub p, #_cluster-_sopTail ' p points to pFilestuff->sopTail
|
||
|
rdlong sopTail, p ' get "pointer" to end of cluster list.
|
||
|
|
||
|
mov sector, sopTail ' Read the sector that sopTail "points" to.
|
||
|
shr sector, #9
|
||
|
call #ReadMetadataSector
|
||
|
|
||
|
and sopTail, #$1ff
|
||
|
add sopTail, pMetadataBuffer ' Use sopTail as pointer into buffer.
|
||
|
wrword cluster, sopTail ' Now the word pointed to by pFilestuff->sopTail
|
||
|
' is updated with the new cluster#.
|
||
|
mov dirty, #1
|
||
|
|
||
|
wrlong sop, p ' p is still pointing to pFilestuff->sopTail so
|
||
|
' update pFilestuff->sopTail to point to new tail.
|
||
|
:writeSector
|
||
|
mov p, pFilestuff
|
||
|
add p, #_cluster ' p points to pFilestuff->cluster
|
||
|
rdword i, p ' using i for sector#
|
||
|
sub i, #2
|
||
|
shl i, clusterShift
|
||
|
add i, dataSector0 ' At this point, i is the first sector of the cluster.
|
||
|
|
||
|
add p, #_sector-_cluster ' p points to pFilestuff->sector
|
||
|
rdword temp, p
|
||
|
add i, temp ' Now i is the correct sector of the cluster.
|
||
|
add temp, #1
|
||
|
wrword temp, p ' Increment pFilestuff->sector
|
||
|
cmp temp, sectorsPerCluster wz
|
||
|
mov temp, #0 ' if pFilestuff->sector = sectorsPerCluster,
|
||
|
if_e wrword temp, p ' pFilestuff->sector := 0.
|
||
|
add p, #_bp-_sector ' p points to pFilestuff->bp
|
||
|
wrword temp, p ' pFilestuff->bp = 0.
|
||
|
|
||
|
wrlong i, pSdspiBlockno
|
||
|
add p, #_buffer-_bp
|
||
|
wrlong p, pSdspiParam
|
||
|
mov temp, #"W"
|
||
|
call #SdspiCommand
|
||
|
WriteNextSector_ret ret
|
||
|
|
||
|
'*********************************************************************************************************************
|
||
|
'* *
|
||
|
'* Close stuff *
|
||
|
'* *
|
||
|
'*********************************************************************************************************************
|
||
|
Close ' ( pFilestuff )
|
||
|
rdlong pFilestuff, pSxfsParam0
|
||
|
|
||
|
mov p, pFilestuff
|
||
|
add p, #_bp
|
||
|
rdword temp, p wz ' If there's stuff waiting to be written
|
||
|
if_nz call #WriteNextSector ' (i.e. bp <> 0), write it.
|
||
|
mov p, pFilestuff
|
||
|
add p, #_sopDir
|
||
|
rdlong sop, p ' sop "points" to directory entry.
|
||
|
|
||
|
mov sector, sop
|
||
|
shr sector, #9
|
||
|
call #ReadMetadataSector ' We want to update metadata length info.
|
||
|
and sop, #$1ff
|
||
|
add sop, pMetadataBuffer
|
||
|
add sop, #$1c ' sop points to length in metadata buffer.
|
||
|
mov p, pFilestuff ' p points to pFilestuff->length.
|
||
|
rdlong temp, p
|
||
|
wrlong temp, sop ' store length in metadata.
|
||
|
mov dirty, #1
|
||
|
call #FlushMetadataSector
|
||
|
|
||
|
jmp #AckNoReturnCode
|
||
|
|
||
|
'*********************************************************************************************************************
|
||
|
'* *
|
||
|
'* Execute stuff *
|
||
|
'* *
|
||
|
'*********************************************************************************************************************
|
||
|
Execute ' ( pFilestuff, execmode, cogid )
|
||
|
{
|
||
|
Loads 63 sectors from file and executes. Requires that the file be contiguous on the SD card,
|
||
|
which is generally accomplished by formatting the card with cluster size >= 32k.
|
||
|
This reads 63 sectors instead of 64 to avoid writing junk into the rendezvous areas.
|
||
|
It is conceivable that some executable files will not run because of this.
|
||
|
|
||
|
This routine does handle clock speed changes.
|
||
|
|
||
|
If execmode is 0, start Spin interpreter in cog specified by cogid.
|
||
|
If execmode is 1, start Spin interpreter in cog specified by cogid and stop all other cogs.
|
||
|
}
|
||
|
call #FlushMetadataSector ' just in case
|
||
|
neg currentsector, #1 ' invalidate current sector 'cuz metadata buffer is about to be overwritten
|
||
|
|
||
|
rdlong pFilestuff, pSxfsParam0
|
||
|
|
||
|
rdlong filesize, pFilestuff wz ' length of file
|
||
|
if_z neg temp, #1 ' if file is empty, return -1: EOF
|
||
|
if_z jmp #AckReturnCode ' This is a bit of a hack. The Sphinx compiler really
|
||
|
' should delete outdated .bin or .eep files to force
|
||
|
' (re)linking, but since sxfs doesn't know how to
|
||
|
' delete files (yet(?)), the compiler just makes
|
||
|
' a file empty instead of deleting it. This test
|
||
|
' here catches empty executable files and bails,
|
||
|
' preventing a crash.
|
||
|
|
||
|
rdlong execmode, pSxfsParam1
|
||
|
|
||
|
rdbyte callingCog, pSxfsParam2 ' stop Spin interpreter
|
||
|
cogstop callingCog
|
||
|
|
||
|
rdlong oldClkfreq, #0 ' save clkfreq
|
||
|
rdbyte oldClkmode, #4 ' save clkmode
|
||
|
|
||
|
mov p, pFilestuff
|
||
|
add p, #_cluster
|
||
|
rdword temp, p ' temp = (cluster
|
||
|
sub temp, #2 ' - 2)
|
||
|
shl temp, clusterShift ' << clusterShift
|
||
|
add temp, dataSector0 ' + dataSector0
|
||
|
mov p, temp ' p repurposed here as sector#
|
||
|
|
||
|
mov i, #63 ' read 63 contiguous sectors
|
||
|
mov q, #0
|
||
|
:loop
|
||
|
wrlong p, pSdspiBlockno ' Prepare to read sector p
|
||
|
wrlong q, pSdspiParam ' to memory pointed to by q
|
||
|
mov temp, #"R"
|
||
|
call #SdspiCommand
|
||
|
add p, #1 ' Next sector
|
||
|
add q, k512 ' Next 512-byte block of memory
|
||
|
djnz i, #:loop
|
||
|
|
||
|
' clear memory above the loaded file
|
||
|
mov i, #1
|
||
|
shl i, #15 ' i = 32768
|
||
|
sub i, filesize wz ' - file size
|
||
|
if_z jmp #:z
|
||
|
mov temp, #0
|
||
|
:y
|
||
|
wrbyte temp, filesize ' repurpose filesize as pointer
|
||
|
add filesize, #1
|
||
|
djnz i, #:y
|
||
|
:z
|
||
|
' store those nutty $fff9ffff longs
|
||
|
rdword p, #$0a
|
||
|
sub p, #4
|
||
|
wrlong kfff9ffff, p
|
||
|
sub p, #4
|
||
|
wrlong kfff9ffff, p
|
||
|
|
||
|
tjz execmode, #:dontChangeClock ' execmode = 0? skip cog-killing and clock change
|
||
|
|
||
|
cogid temp
|
||
|
mov i, #0
|
||
|
mov j, #8
|
||
|
:cogkill
|
||
|
cmp i, temp wz ' kill all cogs except self
|
||
|
if_ne cogstop i ' (suicide comes later)
|
||
|
add i, #1
|
||
|
djnz j, #:cogkill
|
||
|
|
||
|
' The following code is borrowed liberally from sdspiFemto:
|
||
|
rdlong temp, #0 ' new clock frequency
|
||
|
cmp temp, oldClkfreq wz ' compared to old
|
||
|
rdbyte temp, #4 ' get new clock mode in advance
|
||
|
if_ne jmp #:changeClock
|
||
|
cmp temp, oldClkmode wz ' compare new clock mode to old
|
||
|
if_e jmp #:dontChangeClock
|
||
|
|
||
|
:changeClock and temp, #$f8 ' Force use of RCFAST clock while
|
||
|
clkset temp ' letting requested clock start
|
||
|
mov i, time_xtal
|
||
|
:startupDelay djnz i, #:startupDelay ' Allow 20ms@20MHz for xtal/pll to settle
|
||
|
rdbyte temp, #4 ' Then switch to selected clock
|
||
|
clkset temp
|
||
|
jmp #:x
|
||
|
|
||
|
:dontChangeClock
|
||
|
wrlong oldClkfreq, #0 ' Restore old clock frequency
|
||
|
wrbyte oldClkmode, #4 ' and mode.
|
||
|
|
||
|
:x
|
||
|
or interpreter, callingCog
|
||
|
coginit interpreter ' start new Spin interpreter in the same cog.
|
||
|
|
||
|
tjz execmode, #AckNoReturnCode ' execmode = 0? Done.
|
||
|
|
||
|
cogid temp ' kill
|
||
|
cogstop temp ' self
|
||
|
|
||
|
|
||
|
interpreter long ($0004 << 16) | ($F004 << 2) | %0000 ' Thanks to sdspiFemto
|
||
|
time_xtal long 20 * 20000 / 4 / 1 ' 20ms (@20MHz, 1 inst/loop)
|
||
|
|
||
|
'*********************************************************************************************************************
|
||
|
'* *
|
||
|
'* Utility stuff *
|
||
|
'* *
|
||
|
'*********************************************************************************************************************
|
||
|
{
|
||
|
Out'(outch) '''
|
||
|
:w rdbyte outtemp, pTerm wz
|
||
|
if_nz jmp #:w
|
||
|
wrbyte outch, pTerm
|
||
|
Out_ret ret
|
||
|
|
||
|
OutHex'(outx, outn) '''
|
||
|
mov outtemp, outn
|
||
|
sub outtemp, #1
|
||
|
shl outtemp, #2
|
||
|
ror outx, outtemp
|
||
|
:loop
|
||
|
mov outch, outx
|
||
|
and outch, #$0f
|
||
|
add outch, #"0"
|
||
|
cmp outch, #"9" wc, wz
|
||
|
if_a add outch, #"a"-"0"-10
|
||
|
call #out
|
||
|
rol outx, #4
|
||
|
djnz outn, #:loop
|
||
|
Outhex_ret ret
|
||
|
pTerm long $7ffc
|
||
|
outch long 0
|
||
|
outx long 0
|
||
|
outn long 0
|
||
|
outtemp long 0 '''
|
||
|
}
|
||
|
|
||
|
ByteCopy ' ( p, q, n )
|
||
|
tjz n, #ByteCopy_ret
|
||
|
:loop rdbyte temp, q
|
||
|
add q, #1
|
||
|
wrbyte temp, p
|
||
|
add p, #1
|
||
|
djnz n, #:loop
|
||
|
|
||
|
ByteCopy_ret ret
|
||
|
|
||
|
ReadMetadataSector ' ( sector )
|
||
|
call #FlushMetadataSector
|
||
|
|
||
|
cmp sector, currentSector wz
|
||
|
if_e jmp #ReadMetadataSector_ret
|
||
|
wrlong pMetadataBuffer, pSdspiParam
|
||
|
wrlong sector, pSdspiBlockno
|
||
|
mov currentSector, sector
|
||
|
mov temp, #"R"
|
||
|
call #SdspiCommand
|
||
|
ReadMetadataSector_ret ret
|
||
|
|
||
|
FlushMetadataSector
|
||
|
tjz dirty, #FlushMetadataSector_ret
|
||
|
mov dirty, #0
|
||
|
|
||
|
' write current sector
|
||
|
wrlong pMetadataBuffer, pSdspiParam
|
||
|
wrlong currentSector, pSdspiBlockno
|
||
|
mov temp, #"W"
|
||
|
call #SdspiCommand
|
||
|
FlushMetadataSector_ret ret
|
||
|
|
||
|
SdspiCommand
|
||
|
wrlong temp, pSdspiCommand
|
||
|
:wait
|
||
|
rdlong temp, pSdspiCommand wz
|
||
|
if_nz jmp #:wait
|
||
|
|
||
|
rdlong temp, pSdspiParam wz
|
||
|
if_nz jmp #AckReturnCode
|
||
|
|
||
|
SdspiCommand_ret ret
|
||
|
|
||
|
|
||
|
k512 long 512
|
||
|
kfff9ffff long $fff9ffff
|
||
|
dirty long 0
|
||
|
currentSector long -1
|
||
|
pMetadataBuffer long METADATABUFFER
|
||
|
|
||
|
pSxfsCommand long SXFSRENDEZVOUS+0
|
||
|
pSxfsParam0 long SXFSRENDEZVOUS+4
|
||
|
pSxfsParam1 long SXFSRENDEZVOUS+8
|
||
|
pSxfsParam2 long SXFSRENDEZVOUS+12
|
||
|
|
||
|
pSdspiCommand long SDSPIRENDEZVOUS+0
|
||
|
pSdspiParam long SDSPIRENDEZVOUS+4
|
||
|
pSdspiBlockno long SDSPIRENDEZVOUS+8
|
||
|
|
||
|
pCommand long SXFS2RENDEZVOUS+0
|
||
|
pParam long SXFS2RENDEZVOUS+4
|
||
|
|
||
|
bytesPerSector long 0
|
||
|
sectorsPerCluster long 0
|
||
|
clusterShift long 0
|
||
|
reservedSectors long 0
|
||
|
numberOfFats long 0
|
||
|
sectorsPerFat long 0
|
||
|
rootDirEntries long 0
|
||
|
fatSector0 long 0
|
||
|
dirSector0 long 0
|
||
|
dataSector0 long 0
|
||
|
|
||
|
temp res 1
|
||
|
i res 1
|
||
|
j res 1
|
||
|
n res 1
|
||
|
p res 1
|
||
|
q res 1
|
||
|
pFilestuff res 1
|
||
|
sop res 1
|
||
|
sopTail res 1
|
||
|
srcPtr res 1
|
||
|
nBytes res 1
|
||
|
cluster res 1
|
||
|
sector res 1
|
||
|
leftInSector res 1
|
||
|
filesize res 1
|
||
|
execmode res 1
|
||
|
callingCog res 1
|
||
|
oldClkfreq res 1
|
||
|
oldClkmode res 1
|
||
|
|
||
|
retcode res 1
|
||
|
|
||
|
fit
|
||
|
|
||
|
{{
|
||
|
Copyright (c) 2009 Michael Park
|
||
|
+------------------------------------------------------------------------------------------------------------------------------+
|
||
|
| TERMS OF USE: MIT License |
|
||
|
+------------------------------------------------------------------------------------------------------------------------------+
|
||
|
|Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation |
|
||
|
|files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, |
|
||
|
|modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software|
|
||
|
|is furnished to do so, subject to the following conditions: |
|
||
|
| |
|
||
|
|The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.|
|
||
|
| |
|
||
|
|THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE |
|
||
|
|WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
|
||
|
|COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, |
|
||
|
|ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|
||
|
+------------------------------------------------------------------------------------------------------------------------------+
|
||
|
}}
|
||
|
|