bkw777 / PDDuino

A hardware emulator of the Tandy Portable Disk Drive using an SD card for mass storage
GNU General Public License v3.0
27 stars 4 forks source link

Z88 compatibility #4

Open aotta opened 2 years ago

aotta commented 2 years ago

Z88 programs for handling PDDuino needs the "Drive condition" handling. I succesfully added it and now it works with Z88 too! changed in 3_tppd_commands:

void command_condition(){ // added for Z88 compatibility
  DEBUG_PRINTL(F("command_condition()"));
  return_drive(0);    //was: return_normal(ERR_SUCCESS);
}

and added this routines:

// Sends a Drive Condition return to the TPDD port 
void return_drive(byte errorCode){ 
  DEBUG_PRINTL(F("return_normal()"));
#if DEBUG > 1
  DEBUG_PRINT("R:Drive Cond ");
  DEBUG_PRINTIL(errorCode, HEX);
#endif
  tpddWrite(RET_CONDITION);  // Return type (normal)
  tpddWrite(0x01);  // Data size (1)
  tpddWrite(errorCode); // Error code
  tpddSendChecksum(); // Checksum
}
aotta commented 2 years ago

some update... Z88 needs some other corrections to code, first af all the "Attribute" management (byte no. 18 in request) and other ones, as its client don't use directory. For loading, save, rename (but this doesn't work always as there is some "name mismatch" in request and answer, cause to clients) and delete a file , i made further changes: in "PDDuino", i added the definition: byte ATTRIBUTE = 0x00; //Attribute for dir/file request

and following the "3_tppd_commands" sheet, with change noted with a final "AOAO" comment:

// Sends a Drive Condition return to the TPDD port 
void return_drive(byte errorCode){ 
  DEBUG_PRINTL(F("return_normal()"));
#if DEBUG > 1
  DEBUG_PRINT("R:Drive Cond ");
  DEBUG_PRINTIL(errorCode, HEX);
#endif
  tpddWrite(RET_CONDITION);  // Return type (normal)
  tpddWrite(0x01);  // Data size (1)
  tpddWrite(errorCode); // Error code
  tpddSendChecksum(); // Checksum
}

// Sends a normal return to the TPDD port with error code errorCode
void return_normal(byte errorCode){ 
  DEBUG_PRINTL(F("return_normal()"));
#if DEBUG > 1
  DEBUG_PRINT("R:Norm ");
  DEBUG_PRINTIL(errorCode, HEX);
#endif
  tpddWrite(RET_NORMAL);  // Return type (normal)
  tpddWrite(0x01);  // Data size (1)
  tpddWrite(errorCode); // Error code
  tpddSendChecksum(); // Checksum
}

// Sends a reference return to the TPDD port
void returnReference(const char *name, bool isDir, uint16_t size ) {
  uint8_t i, j;

  DEBUG_PRINTL(F("returnReference()"));

  tpddWrite(RET_DIRECTORY);    // Return type (reference) 
  tpddWrite(0x1C);    // Data size (1C)
  if(name == NULL) {
    for(i = 0; i < FILENAME_SZ; i++)
      tpddWrite(0x00);  // Write the reference file name to the TPDD port
  } else {
    if(isDir && DME) { // handle dirname.
      for(i = 0; (i < 6) && (name[i] != 0); i++)
      for(i = 0; (i < 6) && (name[i] != 0); i++)
        tpddWrite(name[i]);
      for(;i < 6; i++)
        tpddWrite(' '); // pad out the dir
      tpddWrite('.');  // Tack the expected ".<>" to the end of the name
      tpddWrite('<');
      tpddWrite('>');
      j = 9;
    } else {
      for(i = 0; (i < 6) && (name[i] != '.'); i++) {
        tpddWrite(name[i]);
      }
      for(j = i; j < 6; j++) {
        tpddWrite(' ');
      }
      for(; j < FILENAME_SZ && (name[i] != 0); j++) {
          tpddWrite(name[i++]);  // send the file extension
      }
    }
    for(; j < FILENAME_SZ; j++) {
      tpddWrite(0);  // pad out
    }
  }
  tpddWrite(ATTRIBUTE);  // Attribute, unused //AOAO use for Z88
  tpddWrite((uint8_t)(size >> 8));  // File size most significant byte
  tpddWrite((uint8_t)(size & 0xFF)); // File size least significant byte
  tpddWrite(0x80);  // Free sectors, SD card has more than we'll ever care about
  tpddSendChecksum(); // Checksum

}

void return_reference() {
  DEBUG_PRINTL(F("return_reference()"));
  entry.getName(tempRefFileName,FILENAME_SZ);  // Save the current file entry's name to the reference file name buffer
  returnReference(tempRefFileName, entry.isDirectory(), entry.fileSize()); 

#if DEBUG > 1
  DEBUG_PRINT("R:Ref:");
  DEBUG_PRINTL(F(tempRefFileName));
#endif
}

// Sends a blank reference return to the TPDD port
void return_blank_reference() {
  DEBUG_PRINTL(F("return_blank_reference()"));
  entry.getName(tempRefFileName,FILENAME_SZ);  // Save the current file entry's name to the reference file name buffer
  returnReference(NULL, false, 0);
#if DEBUG > 1
  DEBUG_PRINTL("R:BRef");
#endif
}

void return_parent_reference(){
  DEBUG_PRINTL(F("return_parent_reference()"));
  returnReference("PARENT", true, 0);
}

/*
 *
 * TPDD Port command handler routines
 *
 */

void command_reference(){ // Reference command handler
  byte searchForm = _cmd_buffer[0x19]; // The search form byte exists 0x19 bytes into the command
  byte refIndex = 0x00;  // Reference file name index
  ATTRIBUTE = _cmd_buffer[0x18]; // AOAO

  DEBUG_PRINTL(F("command_reference()"));

#if DEBUG > 1
  DEBUG_PRINT("SF:");
  DEBUG_PRINTIL(searchForm,HEX);
#endif

  if(searchForm == SF_NAME){ // Request entry by name
    for(uint8_t i = 0; i < FILENAME_SZ; i++){  // Put the reference file name into a buffer
      if(_cmd_buffer[i] != ' '){ // If the char pulled from the command is not a space character (0x20)...
        refFileName[refIndex++]=_cmd_buffer[i]; // write it into the buffer and increment the index.
      }
    }
    refFileName[refIndex] = 0x00; // Terminate the file name buffer with a null character

#if DEBUG > 1
    DEBUG_PRINT("Ref: ");
    DEBUG_PRINTL(refFileName);
#endif

    if(DME){  //        !!!Strips the ".<>" off of the reference name if we're in DME mode
      if(strstr(refFileName, ".<>") != 0x00){
        for(byte i=0x00; i<FILENAME_SZ; i++){  // Copies the reference file name to a scratchpad buffer with no directory extension if the reference is for a directory
          if(refFileName[i] != '.' && refFileName[i] != '<' && refFileName[i] != '>'){
            refFileNameNoDir[i]=refFileName[i];
          }else{
            refFileNameNoDir[i]=0x00; // If the character is part of a directory extension, don't copy it
          }
        }
      }else{
        for(byte i=0x00; i<FILENAME_SZ; i++) refFileNameNoDir[i]=refFileName[i]; // Copy the reference directly to the scratchpad buffer if it's not a directory reference
      }
    }

    directoryAppend(refFileNameNoDir);  // Add the reference to the directory buffer

#if DEBUG > 1
    DEBUG_PRINT("Ref: ");
    DEBUG_PRINTL(refFileName);
#endif

    SD_LED_ON
    if(SD.exists(refFileName)){ // If the file or directory exists on the SD card...  AOAO
      entry=SD.open(refFileName); // ...open it... AOAO   
      #if DEBUG > 1
        DEBUG_PRINT("opened file: ");
        DEBUG_PRINTL(refFileName);
      #endif

      return_reference(); // send a refernce return to the TPDD port with its info...
      entry.close();  // ...close the entry
    }else{  // If the file does not exist...
      return_blank_reference();
    }

    upDirectory();  // Strip the reference off of the directory buffer aoao

  }else if(searchForm == SF_FIRST){ // Request first directory block
    SD_LED_ON
    root.close();
    root = SD.open(directory);
    ref_openFirst();
  }else if(searchForm == SF_NEXT){ // Request next directory block
    SD_LED_ON
    root.close();
    root = SD.open(directory);
    ref_openNext();
  }else{  // Parameter is invalid
    return_normal(ERR_PARM);  // Send a normal return to the TPDD port with a parameter error
  }
  SD_LED_OFF
}

void ref_openFirst(){
  DEBUG_PRINTL(F("ref_openFirst()"));
  directoryBlock = 0x00; // Set the current directory entry index to 0
  if(DME && directoryDepth>0x00){ // Return the "PARENT.<>" reference if we're in DME mode
    SD_LED_OFF
    return_parent_reference();
  }else{
    ref_openNext();    // otherwise we just return the next reference
  }
}

void ref_openNext(){
  DEBUG_PRINTL(F("ref_openNext()"));
  directoryBlock++; // Increment the directory entry index
  SD_LED_ON
  root.rewindDirectory(); // Pull back to the begining of the directory
  for(byte i=0x00; i<directoryBlock-0x01; i++) root.openNextFile();  // skip to the current entry offset by the index

  entry = root.openNextFile();  // Open the entry

  if(entry){  // If the entry exists it is returned
    if(entry.isDirectory() && !DME){  // If it's a directory and we're not in DME mode
      entry.close();  // the entry is skipped over
      ref_openNext(); // and this function is called again
    }

    return_reference(); // Send the reference info to the TPDD port
    entry.close();  // Close the entry
    SD_LED_OFF
  }else{
    SD_LED_OFF
    return_blank_reference();
  }
}

void command_open(){  // Opens an entry for reading, writing, or appending

  _mode = (openmode_t)_cmd_buffer[0];  // The access mode is stored in the 1st byte of the command payload

  DEBUG_PRINTL(F("command_open()"));
  entry.close();

  if(DME && strcmp(refFileNameNoDir, "PARENT") == 0x00){ // If DME mode is enabled and the reference is for the "PARENT" directory
    upDirectory();  // The top-most entry in the directory buffer is taken away
    directoryDepth--; // and the directory depth index is decremented
  }else{
    directoryAppend(refFileNameNoDir);  // Push the reference name onto the directory buffer
    SD_LED_ON
    if(DME && strstr(refFileName, ".<>") != 0x00 && !SD.exists(directory)){ // If the reference is for a directory and the directory buffer points to a directory that does not exist
      SD.mkdir(directory);  // create the directory
      upDirectory();
    }else{
      entry=SD.open(directory); // Open the directory to reference the entry

    //AOAO  if(entry.isDirectory()){  //      !!!Moves into a sub-directory
    //AOAO    entry.close();  // If the entry is a directory
    //AOAO    directoryAppend("/"); // append a slash to the directory buffer
    //AOAO    directoryDepth++; // and increment the directory depth index
    //AOAO  }else{  // If the reference isn't a sub-directory, it's a file
        entry.close();
        switch(_mode){
          case F_OPEN_WRITE: entry = SD.open(refFileName, FILE_WRITE);  break; //AOAO
          case F_OPEN_APPEND: entry = SD.open(refFileName, FILE_WRITE | O_APPEND); break;
          case F_OPEN_READ: entry = SD.open(refFileName, FILE_READ); break;
     //   }   AOAO
     //   upDirectory(); AOAO
      }
    }
  }

  if(SD.exists(refFileName)){ // If the file actually exists... AOAO
    SD_LED_OFF
    return_normal(ERR_SUCCESS);  // ...send a normal return with no error.
  }else{  // If the file doesn't exist...
    SD_LED_OFF
    DEBUG_PRINTL(F("No file ERR")); // AOAO
    return_normal(ERR_NO_FILE);  // ...send a normal return with a "file does not exist" error.
  }
}

void command_close(){ // Closes the currently open entry
  DEBUG_PRINTL(F("command_close()"));
  entry.close();  // Close the entry
  SD_LED_OFF
  return_normal(ERR_SUCCESS);  // Normal return with no error
}

void command_read(){  // Read a block of data from the currently open entry
  DEBUG_PRINTL(F("command_read()"));
  SD_LED_ON
  byte bytesRead = entry.read(fileBuffer, FILE_BUFFER_SZ); // Try to pull 128 bytes from the file into the buffer
  SD_LED_OFF
#if DEBUG > 1
  DEBUG_PRINT("A: ");
  DEBUG_PRINTIL(entry.available(),HEX);
#endif
  if(bytesRead > 0x00){  // Send the read return if there is data to be read
    tpddWrite(RET_READ);  // Return type
    tpddWrite(bytesRead); // Data length
    for(byte i=0x00; i<bytesRead; i++) tpddWrite(fileBuffer[i]);
    tpddSendChecksum();
  }else{
    return_normal(ERR_EOF);  // send a normal return with an end-of-file error if there is no data left to read
  }
}

void command_write(){ // Write a block of data from the command to the currently open entry
  DEBUG_PRINTL(F("command_write()"));
  DEBUG_PRINTL(F(_cmd_buffer));
  DEBUG_PRINTL(F(_length));

  SD_LED_ON
  entry.write(_cmd_buffer, _length);
  SD_LED_OFF
  return_normal(ERR_SUCCESS);  // Send a normal return to the TPDD port with no error
}

void command_delete(){  // Delete the currently open entry
  DEBUG_PRINTL(F("command_delete()"));
  SD_LED_ON
  entry.close();  // Close any open entries
  directoryAppend(refFileNameNoDir);  // Push the reference name onto the directory buffer
  entry = SD.open(directory, FILE_READ);  // directory can be deleted if opened "READ"

  if(DME && entry.isDirectory()){
    entry.rmdir();  // If we're in DME mode and the entry is a directory, delete it
  }else{
    entry.close();  // Files can be deleted if opened "WRITE", so it needs to be re-opened
    entry = SD.open(refFileName, FILE_WRITE); // AOAO
    entry.remove();
  }
  SD_LED_OFF
  upDirectory();
  return_normal(ERR_SUCCESS);  // Send a normal return with no error
}

void command_format(){  // Not implemented
  DEBUG_PRINTL(F("command_format()"));
  return_normal(ERR_SUCCESS);
}

void command_status(){  // Drive status
  DEBUG_PRINTL(F("command_status()"));
  return_normal(ERR_SUCCESS);
}

void command_condition(){ // Not implemented AOAO
  DEBUG_PRINTL(F("command_condition()"));
  return_drive(0); //return_normal(ERR_SUCCESS);
}

void command_rename(){  // Renames the currently open entry
  DEBUG_PRINTL(F("command_rename()"));

  directoryAppend(refFileNameNoDir);  // Push the current reference name onto the directory buffer

  SD_LED_ON

  if(entry) entry.close(); // Close any currently open entries
  entry = SD.open(directory); // Open the entry
  if(entry.isDirectory()) directoryAppend("/"); // Append a slash to the end of the directory buffer if the reference is a sub-directory

  copyDirectory();  // Copy the directory buffer to the scratchpad directory buffer
  upDirectory();  // Strip the previous directory reference off of the directory buffer

  uint8_t i;
  for(i = 0; i < FILENAME_SZ;i++) {
    if(_cmd_buffer[i] == 0 || _cmd_buffer[i] == ' ')
      break;
    refFileNameNoDir[i] = _cmd_buffer[i];   //AOAO
  }
  refFileNameNoDir[i] = 0x00; // Terminate the temporary reference name with a null //AOAO

  if(DME && entry.isDirectory()){ //      !!!If the entry is a directory, we need to strip the ".<>" off of the new directory name
    if(strstr(tempRefFileName, ".<>") != 0x00){
      for(byte i=0x00; i<FILENAME_SZ; i++){
        if(tempRefFileName[i] == '.' || tempRefFileName[i] == '<' || tempRefFileName[i] == '>'){
          tempRefFileName[i]=0x00;
        }
      }
    }
  }

  directoryAppend(tempRefFileName);
  if(entry.isDirectory()) directoryAppend("/");

  if(DME && entry.isDirectory()){ //      !!!If the entry is a directory, we need to strip the ".<>" off of the new directory name
    DEBUG_PRINTL(directory);
    DEBUG_PRINTL(tempDirectory);
    SD.rename(tempDirectory,directory);  // Rename the entry
  } else   // AOAO
  {   // AOAO
    DEBUG_PRINTL(tempRefFileName);   // AOAO
    DEBUG_PRINTL(refFileNameNoDir);  // AOAO
    DEBUG_PRINTL(tempDirectory);
    SD.rename(tempRefFileName,refFileNameNoDir);  // AOAO
  }
  upDirectory();
  entry.close();

  SD_LED_OFF

  return_normal(ERR_SUCCESS);  // Send a normal return to the TPDD port with no error
}

/*
 *
 * TS-DOS DME Commands
 *
 */

void command_DMEreq() {  // Send the dmeLabel

  DEBUG_PRINT(F("command_DMEReq(): dmeLabel[")); DEBUG_PRINT(dmeLabel); DEBUG_PRINTL(F("]"));

  DME = true;
  if (directoryDepth>0x00) setLabel(directory); else setLabel(ROOT_DME_LABEL);
  tpddWrite(RET_NORMAL);
  tpddWrite(0x0B);
  tpddWrite(0x20);
  for (byte i=0x00 ; i<0x06 ; i++) tpddWrite(dmeLabel[i]);
  tpddWrite('.');
  tpddWrite('<');
  tpddWrite('>');
  tpddWrite(0x20);
  tpddSendChecksum();
}
bkw777 commented 2 years ago

Awesome I'll check it out. I have a Z88 to test with. Thanks much.

aotta commented 2 years ago

Thank you! I'll made only a quick and dirty patch, and not tested back too M10 (I'm afraid a big code reworking is needed to have a version working fine with both z88 and Trs). It will be useful to manage the short names bug in renaming and deleting files too. Following your tests and improvement!

Il mar 10 mag 2022, 06:15 Brian K. White @.***> ha scritto:

Awesome I'll check it out. I have a Z88 to test with. Thanks much.

— Reply to this email directly, view it on GitHub https://github.com/bkw777/PDDuino/issues/4#issuecomment-1121898272, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACV2MY2RD3HGFRINXNDW26TVJHPHPANCNFSM5QK54V7A . You are receiving this because you authored the thread.Message ID: @.***>

bkw777 commented 2 years ago

Yeah after making https://github.com/bkw777/pdd.sh , I have a much better understanding of what a real drive does than I did the last time I worked on this, or dlplus, so this is ripe for some improvement, and some of it should be pretty easy.

Not counting the rest of the FCB record beyond the filename, a real drive just a 24 byte field and it doesn't care what's in any of those bytes at all except the special case of null in the first byte. It doesn't know or care about filename lengths, extensions, where the dot is or if there even is any dot, even nulls don't matter except in the first byte. All the formatting is created by the clients and interpreted by the clients.

That makes our job simpler, except for one issue that arises from the fact that a real drive is a filesystem, in that it presents a high level interface to clients and stores the file data on raw storage, while we are trying to emulate that filesystem, yet we are not storing the data in raw space but are in turn using another filesystem's already high-level client interface, which want's to deal with filenames not raw bytes.

So, we get "A . B A " from the client, and all else being equal, all we're supposed to do is store that and then later read that back out, verbatim in both cases. Brainless, blind, easy.

One way to do that could be, we could actually create filenames in that format on the sd card. Fat32 supports over 24 bytes length and space characters in the name, so we could just write the given strings and read them back verbatim.

But that would make filenames that aren't convenient when you take the sd card out of the device and access it directly from a modern machine. So ideally we want to collaps that to "A.BA" to create a normal filename on the sd card.

But that means that, if we want to do the actual proper emulation job that will be the most compatible to any client that works with real drives, then later we should expand that back out to the original 24 bytes when reading back out to the client. And that means we need to either make an unsafe assumption (the original filename might have been exactly "A.BA" or a 6.2 version, or an 8.2 version, or an 8.3 version, or who knows what else), or we need to record the original string somehow. A cheap way to do that would be just to url-encode the filename when writing to the sdcard, and decode when reading back to a client. That removes spaces from the on-disk name, but is not exactly any better to have a name like "A%20%20%20%20%20.BA" vs "A _ .BA" (We can at least safely discard trailing spaces, just not leading or embedded.)

Maybe the most practical answer is just do the simple assumption, always expand according to a simple rule, but have it user-configurable. Have a config file on disk with a setting that says to treat all the filenames as Kyotronic/Tandy/NEC/Olivetti, or WP-2 or Z88 or raw or whatever other format.

The user just dedicates the sdcard to some particular machine they use it with. That will be fine in most cases. If you only have a z88 you play with, you set the z88 option and pretty much forget about it after that. The only problem would be from moving a given sd card from something like a WP-2 which has 8.2 filenames to a Model 100 which has only 6.2.

One of the modes would be a "raw" mode, which would write the filenames to the sd card without re-formatting, which would be less convenient for accessing the sdcard outside of the device, but would automatically be compatible with ANY client even if we never saw it before and have no idea what it expects.

This way we don't have to have some hidden file containing the original filenames or something. That would just be a constant source of problems.

Maybe an even better option is a slightly technically wrong behavior but maybe it's fine, where we collapse embedded spaces and don't bother to re-expand them.

It's technically wrong because a real drive sends back the original embedded spaces, and so all clients expect to read that, and so there COULD be a client that would be broken without it. But, I don't think it actually breaks any clients to send back the collapsed version. A Model 100 running Floppy or TS-DOS will accept "A.BA" from the drive I think. The drive is just blindly counting bytes, but I think all the clients are parsing the string and will find the dot wherever it is. Maybe this is the best mode to make the default mode with no config file, and then the config option could force it to behave more strictly like a real drive if there turns out to be some client that needs it.

Also now I understand what the "F" byte does which I didn't last time I worked on this so I know better how to handle it now.

aotta commented 2 years ago

Brian, thank you for the great analysis of the naming related bug, i read on FB Z88's users that other emulator didn't work so i was in doubt if it was a bug of the Z88 disk manager, not becouse of a not accurate drive emulation. Anyway, i'm now in a fully immersion with a different (but similar) project (emulating an ATA Drive with a Beaglebone black and a customized cape i designed), and i won't come back to Z88 or M10 for testing for a while. In the meanwhile, I'll follow with interest this project, and vote for the good idea of a "config" file for different platform: even if the code is the same, in most case (i think to my Z88 and Olivetti) the connector and adapter are different, so i think all users (like me) have different "box" for each retrocomputer, with its own sdcard (and config). And again, thank you for your amazing PDDuino!

bkw777 commented 2 years ago

I just got done doing a major re-work of dlplus over the last couple weeks and I'm really happy with it now. Now I'm starting to re-work PDDuino the same way. It's easier for me to follow and definitely more solid and correct, and probably even more efficient since it's now more like a set of small machines that each have limited and well-defined jobs and it's easy to know that they are doing their things right, and easy to debug when some new issue turns up. And since dlplus is all plain c, it should map to this almost cut & paste.