SXTVRENDEZVOUS = $8000 - 4 SXKBRENDEZVOUS = SXTVRENDEZVOUS - 4 SDSPIRENDEZVOUS = SXKBRENDEZVOUS - 3 * 4 SXFS2RENDEZVOUS = SDSPIRENDEZVOUS - 4 * 4 ' four rendezvous variables SXFSRENDEZVOUS = SXFS2RENDEZVOUS - 4 * 4 ' four rendezvous variables METADATABUFFER = SXFSRENDEZVOUS - 512 _free = ($8000 - METADATABUFFER) / 4 { sxfsrendezvous sdspirendezvous --------+ +--------+ +-------+ | <===> command <===> | | <=====> command <=====> | | Spin | | | | | routines| <===> param0 <====> | SXFS | <=====> param <=====> | SDSPI | | | cog | | cog | | ----> param1 -----> | | ------> blockno ------> | | | | | +-------+ | ----> param2 -----> | | --------+ | | sxfs2rendezvous | | +-------+ | | <====> command <======> | | | | | | <==> (sxfsrendezvous) | | <====> param0 <=======> | SXFS2 | | | | cog | <==> (sdspirendezvous) | | | | +--------+ +-------+ SXFS cog handles file reading: open, read, execute, close(?). SXFS2 cog, under SXFS's direction, handles file writing: open, write, close. SDSPI cog handles SD card sector reading and writing. SXFS cog communicates with main program via sxfsrendezvous and Spin interface routines. SXFS cog communicates with SXFS2 cog via sxfs2rendezvous. SXFS cog communicates with SDSPI cog via sdspirendezvous. -1: eof, general error -100: bad command -101: not FAT16 -102: file not found -104: bad mode -106: file not closed -107: directory full -109: file not properly opened -110: FAT full. -113: bad filename } obj sxfs2: "sxfs2" sdspi : "sxsdspiq" pub Start( sdPin ) | pbrSector ' Determine if sxfs cog and, by association, sxfs2 and sdspi cogs, are already running. ' If not, start them up. ' Returns true if the cogs needed to be started, false if the cogs were already running. ifnot Ping result~~ ' start sdspi cog and initialize SD card long[pSdspiCommand] := "I" sdspi.start( sdPin, pSdspiCommand ) repeat while long[pSdspiCommand] if long[pSdspiParam] abort long[pSdspiParam] pbrSector := ReadPbr ' start sxfs cog long[pCommand] := "?" if cognew( @sxfsEntry, 0 ) < 0 abort -1 repeat while long[pCommand] ' start sxfs2 cog sxfs2.Start( pbrSector ) pri ReadPbr : pbrSector repeat sdspi.readblock( pbrSector, pMetadataBuffer ) if bytecompare( pMetadataBuffer+$36, string("FAT16"), 5 ) quit if pbrSector abort -101 ' not FAT16 pbrSector := PeekL( pMetadataBuffer + $1c6 ) 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 pri bytecompare( _p, _q, _n ) repeat _n if byte[_p++] <> byte[_q++] return return true pri PeekW( a ) return byte[a++] + byte[a] << 8 pri PeekL( a ) return byte[a++] + byte[a++] << 8 + byte[a++] << 16 + byte[a] << 24 pri Ping {{ returns 1 if bmfs cog is active, 0 otherwise. }} long[pCommand] := "?" if long[pCommand] return 0 else return 1 con SECTORSIZE = 512 SECTORSHIFT = 9 DIRSIZE = 32 con ' 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 dat {{ command param0 param1 param2 "?" -- -- -- Do nothing, but acknowledge to indicate that sxfs cog is active. "O" filestuff filename mode Open a file, populate filestuff fields. mode is "R" or "W". "R" filestuff buffer num Reads num bytes into buffer from file specified by filestuff. "W" filestuff buffer num Writes num bytes from buffer to file specified by filestuff. "C" filestuff -- -- Closes file specified by filestuff. "X" filestuff execmode cog Executes file. }} org 0 SXFSEntry AckNoReturnCode mov temp, #0 ' clear return code AckReturnCode ' enter here with temp = return code wrlong temp, pParam0 mov temp, #0 wrlong temp, pCommand ' clear command to signal operation complete. :ready ' wait for new command rdlong temp, pCommand wz if_z jmp #:ready cmp temp, #"?" wz if_e jmp #AckNoReturnCode cmp temp, #"O" wz if_e jmp #Open cmp temp, #"R" wz if_e jmp #Read 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 ' -100: bad command jmp #AckReturnCode '********************************************************************************************************************* '* * '* Open stuff * '* * '********************************************************************************************************************* Open ' ( param0 = pFilestuff, param1 = pFilename, param2 = mode ) { Return value: File found File not found Error mode = "R" 0 1 < 0 mode = "W" 0 0 < 0 Mode W will either open an existing file or create a new file. } rdword pFilestuff, pParam0 rdword pFilename, pParam1 mov p, pFilestuff add p, #_mode rdword temp, p wz if_nz neg temp, #106 ' -106: file not closed if_nz jmp #AckReturnCode call #MassageFilename rdword temp, pParam2 cmp temp, #"R" wz if_e jmp #:openForReading cmp temp, #"W" wz if_e jmp #:openForWriting neg temp, #104 ' -104: bad mode jmp #AckReturnCode :openForReading call #FindFile mov p, pFilestuff add p, #_sopDir ' pFilestuff->sopDir = 0 => file not found rdlong temp, p wz if_z mov temp, #1 ' return 1 to indicate file not found if_z jmp #AckReturnCode mov p, sop ' sop sector is still in memory and p, #$1ff add p, pMetadataBuffer add p, #$1a ' point to cluster information rdword temp, p mov q, pFilestuff add q, #_cluster wrword temp, q add p, #2 ' point to length information rdlong temp, p sub q, #_cluster-_length ' q points to length wrlong temp, q add q, #_leftInFile-_length ' q points to leftInFile wrlong temp, q add q, #_sector-_leftInFile ' q points to sector mov temp, #0 wrword temp, q add q, #_bp-_sector ' q points to bp wrword k512, q mov p, pFilestuff add p, #_mode mov temp, #"R" wrword temp, p ' save mode jmp #AckNoReturnCode :openForWriting mov sop, dirSector0 ' sop "points" to directory entries: shl sop, #9 ' incremented by 32 each time through the loop. mov sopDir, #0 ' sopDir "points" to the directory entry ' that will be used or reused for the file being opened. mov i, rootDirEntries :forEachDirEntry ' Loop through the directory entries, looking for a deleted entry, ' an empty entry, or an entry that matches pFilename. mov sector, sop shr sector, #9 call #ReadMetadataSector mov p, sop and p, #$1ff add p, pMetadataBuffer ' p points to the current directory entry add p, #$0b rdbyte temp, p ' p[$0b] = attribute byte sub p, #$0b and temp, #$02 wz ' If hidden, if_nz jmp #:next ' skip. Hidden dir entries include long name entries. rdbyte temp, p ' p[0] cmp temp, #$e5 wz ' = $e5? (deleted directory entry?) if_e cmp sopDir, #0 wz ' save pointer in sopDir (just the first time) if_e mov sopDir, sop tjz temp, #:quit ' empty directory entry? We're done mov q, pFilename ' matching filename? Done. mov n, #11 call #ByteComp if_e jmp #:quit :next add sop, #DIRSIZE djnz i, #:forEachDirEntry :quit cmp i, #0 wz ' did we go through the whole directory? if_z neg temp, #107 ' -107: directory full if_z jmp #AckReturnCode cmp sopDir, #0 wz ' if sopDir hasn't been set yet, if_e mov sopDir, sop ' set it to sop. mov p, pFilestuff add p, #_sopDir wrlong sopDir, p ' pFilestuff->sopDir := sopDir add p, #_sopTail-_sopDir add sopDir, #$1a wrlong sopDir, p ' pFilestuff->sopTail := sopDir + $1a ' (points to directory entry's cluster field). mov sector, sopDir shr sector, #9 ' Get the chosen directory entry into memory call #ReadMetadataSector ' if it isn't already. mov p, sopDir ' Recall that sopDir was changed and p, #$1ff ' to point to the cluster field, add p, pMetadataBuffer ' so p now points to the cluster field in the buffer. rdword cluster, p ' Save the cluster field. mov temp, #0 wrword temp, p ' Zero the cluster field. sub p, #$1a ' Point to start of directory entry rdbyte temp, p ' Check to see if this was a deleted dir entry. cmp temp, #$e5 wz ' If it was, we definitely do not want to zero the if_e mov cluster, #0 ' cluster chain, so set cluster to 0. mov q, pFilename ' Store the filename in the directory entry. mov n, #11 call #ByteCopy mov temp, #$20 ' Set the attribute byte (immediately following name) wrbyte temp, p add p, #$10-$0b ' Point to creation date. mov temp, aDate ' 2001/01/01 wrword temp, p add p, #$12-$10 ' Point to last access date. wrword temp, p add p, #$18-$12 ' Point to last update date. wrword temp, p mov dirty, #1 ' We've modified metadata. tjz cluster, #:done :forEachClusterInList ' Traverse the cluster list and zero each link. mov sector, cluster shr sector, #8 add sector, fatSector0 call #ReadMetadataSector ' Read FAT sector for current cluster mov p, cluster and p, #$ff shl p, #1 add p, pMetadataBuffer ' Point p at cluster entry in FAT rdword cluster, p ' Get next cluster link. cmp cluster, #1 wc, wz ' Break if cluster <= 1 if_be jmp #:done mov temp, #0 wrword temp, p ' Zero the cluster link. mov dirty, #1 ' We've modified metadata. cmp cluster, kfff0 wc, wz ' repeat unless new cluster >= $fff0 if_b jmp #:forEachClusterInList :done mov p, pFilestuff add p, #_sector mov temp, #0 wrword temp, p ' pFilestuff->sector := 0 add p, #_bp-_sector wrword temp, p ' pFilestuff->bp := 0 mov p, pFilestuff mov temp, #0 wrlong temp, p ' pFilestuff->length := 0 add p, #_mode mov temp, #"W" wrword temp, p ' save mode jmp #AckNoReturnCode MassageFilename { Take the null-terminated 8.3 filename pointed to by pFilename, move it into memory starting at pFilestuff->buffer and expand it to 11 characters; change pFilename to point to the expanded filename. } mov p, pFilestuff add p, #_buffer mov q, p add p, #1 mov temp, #" " wrbyte temp, q mov n, #10 call #ByteCopy mov q, pFilename ' q is src mov p, pFilestuff ' p is dst add p, #_buffer mov i, #9 ' Copy up to 8 chars (stop at . or null). :upTo8 rdbyte temp, q add q, #1 tjz temp, #:done cmp temp, #"." wz if_z jmp #:dot call #ValidateChar wrbyte temp, p add p, #1 djnz i, #:upTo8 ' If we fall through, we've copied 9 characters (tsk tsk). neg temp, #113 ' -113: bad filename jmp #AckReturnCode :dot mov p, pFilestuff add p, #_buffer+8 mov i, #4 ' Copy up to 3 chars (stop at null). :upTo3 rdbyte temp, q add q, #1 tjz temp, #:done call #ValidateChar wrbyte temp, p add p, #1 djnz i, #:upTo3 ' If we fall through, we've copied 4 characters (tsk tsk ). neg temp, #113 ' -113: bad filename jmp #AckReturnCode :done mov pFilename, pFilestuff add pFilename, #_buffer MassageFilename_ret ret FindFile { pFilename points to 11-character buffer (e.g. "BLAH TXT"). pFilestuff points to a filestuff structure. Return value: If file is found, pFilestuff->sopDir is disk address of directory entry. If file is not found, pFilestuff->sopDir is 0. } mov sop, dirSector0 ' sop starts at dirSector0<<9, counts up by DIRSIZE shl sop, #SECTORSHIFT mov i, rootDirEntries :loop mov sector, sop shr sector, #9 call #readMetadataSector mov p, sop and p, #$1ff add p, pMetadataBuffer rdbyte temp, p wz if_z jmp #:notfound add p, #$0b rdbyte temp, p ' p[$0b] = attribute byte sub p, #$0b and temp, #$02 wz ' If hidden, if_nz jmp #:next ' skip. Hidden dir entries include long name entries. mov q, pFilename mov n, #11 call #ByteComp if_e jmp #:found :next add sop, #DIRSIZE djnz i, #:loop :notfound mov sop, #0 :found mov p, pFilestuff add p, #_sopDir wrlong sop, p FindFile_ret ret '********************************************************************************************************************* '* * '* Read stuff * '* * '********************************************************************************************************************* Read ' ( param0 = pFilestuff, param1 = ptr, param2 = n ) { Read n bytes from file described by pIoblock into memory starting at p. Returns number of bytes actually read or -1 if attempting to read past EOF. } rdlong pFilestuff, pParam0 rdlong destPtr, pParam1 rdlong nBytes, pParam2 mov p, pFilestuff ' Verify that file was opened for reading. add p, #_mode rdword temp, p cmp temp, #"R" wz if_ne neg temp, #109 ' -109: file not properly opened if_ne jmp #AckReturnCode sub p, #_mode-_leftInFile ' Adjust nBytes depending on how much is left to read in file. ' E.g. if we're 10 bytes from EOF and we try to read 15 bytes, ' just read 10. If we're at EOF and try to read 15 bytes, return -1. rdlong temp, p max nBytes, temp ' nBytes is lesser of nBytes and leftInFile tjnz temp, #:x neg temp, #1 ' -1: eof jmp #ackReturnCode :x mov retcode, nBytes :while mov leftInSector, k512 ' leftInSector = 512 - pFilestuff->bp mov p, pFilestuff add p, #_bp rdword temp, p ' temp = bp sub leftInSector, temp cmp leftInSector, nBytes wc, wz if_ae jmp #:endwhile mov p, destPtr mov q, pFilestuff add q, #_buffer ' q points to buffer area add q, temp ' offset by bp (= temp) mov n, leftInSector call #ByteCopy ' bytemove( p, q, n ) add destPtr, leftInSector ' destPtr += leftInSector sub nBytes, leftInSector ' nBytes -= leftInSector mov p, pFilestuff ' long[pIoblock+_leftInFile] -= leftInSector add p, #_leftInFile rdlong temp, p sub temp, leftInSector wrlong temp, p ' sdspi.readblock( dataSector0 + (word[pFilestuff+_cluster] - 2) << clusterShift + word[pFilestuff+_sector], pFilestuff+_buffer ) add p, #_cluster-_leftInFile rdword temp, p ' temp = (cluster sub temp, #2 ' - 2) shl temp, clusterShift ' << clusterShift add temp, dataSector0 ' + dataSector0 add p, #_sector-_cluster rdword temp1, p add temp, temp1 ' + cluster wrlong temp, pSdspiBlockno ' Prepare to read sector add p, #_buffer-_sector wrlong p, pSdspiParam mov temp, #"R" call #SdspiCommand ' if ++word[pFilestuff+_sector] == sectorsPerCluster mov p, pFilestuff add p, #_sector rdword temp, p add temp, #1 wrword temp, p cmp temp, sectorsPerCluster wz if_ne jmp #:y ' word[pIoblock+_sector]~ mov temp, #0 wrword temp, p ' word[pFilestuff+_cluster] := NextCluster( word[pFilestuff+_cluster] ) sub p, #_sector-_cluster rdword cluster, p call #NextCluster '( cluster ) mov p, pFilestuff add p, #_cluster wrword cluster, p :y ' word[pFilestuff+_bp]~ mov p, pFilestuff add p, #_bp mov temp, #0 wrword temp, p jmp #:while :endwhile ' bytemove( destPtr, pIoblock + word[pIoblock+_bp], n ) mov p, destPtr mov q, pFilestuff add q, #_bp rdword temp, q add q, #_buffer-_bp add q, temp mov n, nBytes call #ByteCopy ' long[pIoblock+_leftInFile] -= n mov p, pFilestuff add p, #_leftInFile rdlong temp, p sub temp, nBytes wrlong temp, p ' word[pIoblock+_bp] += n add p, #_bp-_leftInFile rdword temp, p add temp, nBytes wrword temp, p mov temp, retcode jmp #AckReturnCode NextCluster ' ( cluster ) { Given cluster, determines next cluster in FAT. Result in cluster. } cmp cluster, #1 wc, wz if_be jmp #NextCluster_ret cmp cluster, kfff0 wc, wz if_ae jmp #NextCluster_ret mov sector, cluster shr sector, #8 add sector, fatSector0 call #ReadMetadataSector mov p, cluster and p, #$ff shl p, #1 add p, pMetadataBuffer rdword cluster, p NextCluster_ret ret kfff0 long $fff0 '********************************************************************************************************************* '* * '* Write stuff * '* * '********************************************************************************************************************* Write rdlong p, pParam0 ' p = pFilestuff add p, #_mode ' Verify that file was opened for writing. rdword temp, p cmp temp, #"W" wz if_ne neg temp, #109 ' -109: file not properly opened if_ne jmp #AckReturnCode call #FlushMetadataSector neg currentsector, #1 ' invalidate current sector 'cuz we're about to hand the reins ' to sxfs2 cog. mov temp, #"W" wrlong temp, pSxfs2Command :wait rdlong temp, pSxfs2Command wz if_nz jmp #:wait rdlong temp, pSxfs2Param jmp #AckReturnCode '********************************************************************************************************************* '* * '* Close stuff * '* * '********************************************************************************************************************* Close ' ( param0 = pFilestuff ) rdlong pFilestuff, pParam0 mov p, pFilestuff add p, #_mode rdword temp, p cmp temp, #"W" wz if_e jmp #:closew ' Files opened in mode "W" need special closing code, but for mode "R" or anything else ' (like file not even open) we can just do this: mov temp, #0 wrword temp, p ' clear mode jmp #AckNoReturnCode ' and we're done :closew mov temp, #0 ' clear mode wrword temp, p call #FlushMetadataSector neg currentsector, #1 ' invalidate current sector 'cuz we're about to hand the reins ' to sxfs2 cog. mov temp, #"C" wrlong temp, pSxfs2Command :wait rdlong temp, pSxfs2Command wz if_nz jmp #:wait rdlong temp, pSxfs2Param jmp #AckReturnCode '********************************************************************************************************************* '* * '* Execute stuff * '* * '********************************************************************************************************************* Execute ' ( pFilestuff, execmode, cogid ) call #FlushMetadataSector neg currentsector, #1 ' invalidate current sector 'cuz we're about to hand the reins ' to sxfs2 cog. mov temp, #"X" wrlong temp, pSxfs2Command :wait rdlong temp, pSxfs2Command wz if_nz jmp #:wait rdlong temp, pSxfs2Param jmp #AckReturnCode '********************************************************************************************************************* '* * '* Utility stuff * '* * '********************************************************************************************************************* 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 ByteComp ' ( p, q, n ) { Compares j bytes starting at p and q. Returns Z if they match, NZ if they differ. Destroys p, q, and j. } rdbyte temp, p add p, #1 rdbyte temp1, q add q, #1 cmp temp, temp1 wz if_z djnz n, #ByteComp ByteComp_ret ret ValidateChar { Make sure that temp is a valid filename character (for our purposes, 0-9, A-Z, _). } cmp temp, #"a" wc, wz if_b jmp #:notLowerCase cmp temp, #"z" wc, wz if_a jmp #:notLowerCase sub temp, #"a"-"A" ' convert to upper-case jmp #ValidateChar_ret :notLowerCase cmp temp, #"A" wc, wz if_b jmp #:notAlpha cmp temp, #"Z" wc, wz if_be jmp #ValidateChar_ret :notAlpha cmp temp, #"0" wc, wz if_b jmp #:notNumeric cmp temp, #"9" wc, wz if_be jmp #ValidateChar_ret :notNumeric cmp temp, #"_" wz if_e jmp #ValidateChar_ret mov temp, #113 ' -113: bad filename jmp #AckReturnCode ValidateChar_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 kFAT1 long "F" + "A"<<8 + "T"<<16 + "1"<<24 k512 long 512 dirty long 0 currentSector long -1 aDate long $2a21 pMetadataBuffer long METADATABUFFER pCommand long SXFSRENDEZVOUS+0 pParam0 long SXFSRENDEZVOUS+4 pParam1 long SXFSRENDEZVOUS+8 pParam2 long SXFSRENDEZVOUS+12 pSdspiCommand long SDSPIRENDEZVOUS+0 pSdspiParam long SDSPIRENDEZVOUS+4 pSdspiBlockno long SDSPIRENDEZVOUS+8 pSxfs2Command long SXFS2RENDEZVOUS+0 pSxfs2Param 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 temp1 res 1 i res 1 j res 1 n res 1 p res 1 q res 1 sop res 1 byte4 res 0 sopDir res 1 sector res 1 cluster res 1 pFilename res 1 pFilestuff res 1 destPtr res 1 nBytes res 1 leftInFile res 1 leftInSector 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. | +------------------------------------------------------------------------------------------------------------------------------+ }}