/* ArduinoFloppyReader (and writer) * * Copyright (C) 2017-2021 Robert Smith (@RobSmithDev) * https://amiga.robsmithdev.co.uk * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, see http://www.gnu.org/licenses/ */ //////////////////////////////////////////////////////////////////////////////////////// // Class to manage the communication between the computer and the Arduino V2.5 // //////////////////////////////////////////////////////////////////////////////////////// // // Purpose: // The class handles the command interface to the arduino. It doesn't do any decoding // Just open ports, switch motors on and off, seek to tracks etc. // // // #include "ArduinoInterface.h" #include #include #include #include #include "RotationExtractor.h" #ifndef _WIN32 #include #endif using namespace ArduinoFloppyReader; // Command that the ARDUINO Sketch understands #define COMMAND_VERSION '?' #define COMMAND_REWIND '.' #define COMMAND_GOTOTRACK '#' #define COMMAND_HEAD0 '[' #define COMMAND_HEAD1 ']' #define COMMAND_READTRACK '<' #define COMMAND_ENABLE '+' #define COMMAND_DISABLE '-' #define COMMAND_WRITETRACK '>' #define COMMAND_ENABLEWRITE '~' #define COMMAND_DIAGNOSTICS '&' #define COMMAND_ERASETRACK 'X' #define COMMAND_SWITCHTO_DD 'D' // Requires Firmware V1.6 #define COMMAND_SWITCHTO_HD 'H' // Requires Firmware V1.6 #define COMMAND_DETECT_DISK_TYPE 'M' // currently not implemented here // New commands for more direct control of the drive. Some of these are more efficient or dont turn the disk motor on for modded hardware #define COMMAND_READTRACKSTREAM '{' // Requires Firmware V1.8 #define COMMAND_WRITETRACKPRECOMP '}' // Requires Firmware V1.8 #define COMMAND_CHECKDISKEXISTS '^' // Requires Firmware V1.8 (and modded hardware for fast version) #define COMMAND_ISWRITEPROTECTED '$' // Requires Firmware V1.8 #define COMMAND_ENABLE_NOWAIT '*' // Requires Firmware V1.8 #define COMMAND_GOTOTRACK_REPORT '=' // Requires Firmware V1.8 #define COMMAND_DO_NOCLICK_SEEK 'O' // Requires Firmware V1.8a #define SPECIAL_ABORT_CHAR 'x' // Convert the last executed command that had an error to a string std::string lastCommandToName(LastCommand cmd) { switch (cmd) { case LastCommand::lcOpenPort: return "OpenPort"; case LastCommand::lcGetVersion: return "GetVersion"; case LastCommand::lcEnableWrite: return "EnableWrite"; case LastCommand::lcRewind: return "Rewind"; case LastCommand::lcDisableMotor: return "DisableMotor"; case LastCommand::lcEnableMotor: return "EnableMotor"; case LastCommand::lcGotoTrack: return "GotoTrack"; case LastCommand::lcSelectSurface: return "SelectSurface"; case LastCommand::lcReadTrack: return "ReadTrack"; case LastCommand::lcWriteTrack: return "WriteTrack"; case LastCommand::lcRunDiagnostics: return "RunDiagnostics"; case LastCommand::lcSwitchDiskMode: return "SetCapacity"; case LastCommand::lcReadTrackStream: return "ReadTrackStream"; case LastCommand::lcCheckDiskInDrive: return "CheckDiskInDrive"; case LastCommand::lcCheckDiskWriteProtected: return "CheckDiskWriteProtected"; case LastCommand::lcEraseTrack: return "EraseTrack"; default: return "Unknown"; } } // Uses the above fields to constructr a suitable error message, hopefully useful in diagnosing the issue const std::string ArduinoInterface::getLastErrorStr() const { std::stringstream tmp; switch (m_lastError) { case DiagnosticResponse::drOldFirmware: return "The Arduino is running an older version of the firmware/sketch. Please re-upload."; case DiagnosticResponse::drOK: return "Last command completed successfully."; case DiagnosticResponse::drPortInUse: return "The specified COM port is currently in use by another application."; case DiagnosticResponse::drPortNotFound: return "The specified COM port was not found."; case DiagnosticResponse::drAccessDenied: return "The operating system denied access to the specified COM port."; case DiagnosticResponse::drComportConfigError: return "We were unable to configure the COM port using the SetCommConfig() command."; case DiagnosticResponse::drBaudRateNotSupported: return "The COM port does not support the 2M baud rate required by this application."; case DiagnosticResponse::drErrorReadingVersion: return "An error occured attempting to read the version of the sketch running on the Arduino."; case DiagnosticResponse::drErrorMalformedVersion: return "The Arduino returned an unexpected string when version was requested. This could be a baud rate mismatch or incorrect loaded sketch."; case DiagnosticResponse::drCTSFailure: return "Diagnostics reports the CTS line is not connected correctly or is not behaving correctly."; case DiagnosticResponse::drTrackRangeError: return "An error occured attempting to go to a track number that was out of allowed range."; case DiagnosticResponse::drWriteProtected: return "Unable to write to the disk. The disk is write protected."; case DiagnosticResponse::drPortError: return "An unknown error occured attempting to open access to the specified COM port."; case DiagnosticResponse::drDiagnosticNotAvailable: return "CTS diagnostic not available, command GetCommModemStatus failed to execute."; case DiagnosticResponse::drSelectTrackError: return "Arduino reported an error seeking to a specific track."; case DiagnosticResponse::drTrackWriteResponseError: return "Error receiving status from Arduino after a track write operation."; case DiagnosticResponse::drSendDataFailed: return "Error sending track data to be written to disk. This could be a COM timeout."; case DiagnosticResponse::drRewindFailure: return "Arduino was unable to find track 0. This could be a wiring fault or power supply failure."; case DiagnosticResponse::drNoDiskInDrive: return "No disk in drive"; case DiagnosticResponse::drWriteTimeout: return "The Arduino could not receive the data quick enough to write to disk. Try connecting via USB2 and not using a USB hub.\n\nIf this still does not work, turn off precomp if you are using it."; case DiagnosticResponse::drFramingError: return "The Arduino received bad data from the PC. This could indicate poor connectivity, bad baud rate matching or damaged cables."; case DiagnosticResponse::drSerialOverrun: return "The Arduino received data faster than it could handle. This could either be a fault with the CTS connection or the USB/serial interface is faulty"; case DiagnosticResponse::drUSBSerialBad: return "The USB->Serial converter being used isn't suitable and doesnt run consistantly fast enough. Please ensure you use a genuine FTDI adapter."; case DiagnosticResponse::drError: tmp << "Arduino responded with an error running the " << lastCommandToName(m_lastCommand) << " command."; return tmp.str(); case DiagnosticResponse::drReadResponseFailed: switch (m_lastCommand) { case LastCommand::lcGotoTrack: return "Unable to read response from Arduino after requesting to go to a specific track"; case LastCommand::lcReadTrack: return "Gave up trying to read a full track from the disk."; case LastCommand::lcWriteTrack: return "Unable to read response to requesting to write a track."; default: tmp << "Error reading response from the Arduino while running command " << lastCommandToName(m_lastCommand) << "."; return tmp.str(); } case DiagnosticResponse::drSendFailed: if (m_lastCommand == LastCommand::lcGotoTrack) return "Unable to send the complete select track command to the Arduino."; else { tmp << "Error sending the command " << lastCommandToName(m_lastCommand) << " to the Arduino."; return tmp.str(); } case DiagnosticResponse::drSendParameterFailed: tmp << "Unable to send a parameter while executing the " << lastCommandToName(m_lastCommand) << " command."; return tmp.str(); case DiagnosticResponse::drStatusError: tmp << "An unknown response was was received from the Arduino while executing the " << lastCommandToName(m_lastCommand) << " command."; return tmp.str(); default: return "Unknown error."; } } // Constructor for this class ArduinoInterface::ArduinoInterface() { m_abortStreaming = true; m_version = { 0,0,false }; m_lastError = DiagnosticResponse::drOK; m_lastCommand = LastCommand::lcGetVersion; m_inWriteMode = false; m_isWriteProtected = false; m_diskInDrive = false; m_abortSignalled = false; m_isStreaming = false; } // Free me ArduinoInterface::~ArduinoInterface() { abortReadStreaming(); closePort(); } // Checks if the disk is write protected. If forceCheck=false then the last cached version is returned. This is also updated by checkForDisk() as well as this function DiagnosticResponse ArduinoInterface::checkIfDiskIsWriteProtected(bool forceCheck) { if (!forceCheck) { return m_isWriteProtected ? DiagnosticResponse::drWriteProtected : DiagnosticResponse::drOK; } // Test manually m_lastCommand = LastCommand::lcCheckDiskWriteProtected; // If no hardware support then return no change if ((m_version.major == 1) && (m_version.minor < 8)) { m_lastError = DiagnosticResponse::drOldFirmware; return m_lastError; } // Send and update flag m_lastError = checkForDisk(true); if ((m_lastError == DiagnosticResponse::drStatusError) || (m_lastError == DiagnosticResponse::drOK)) { m_lastCommand = LastCommand::lcCheckDiskWriteProtected; if (m_isWriteProtected) m_lastError = DiagnosticResponse::drWriteProtected; } return m_lastError; } // This only works normally after the motor has been stepped in one direction or another. This requires the 'advanced' configuration DiagnosticResponse ArduinoInterface::checkForDisk(bool forceCheck) { if (!forceCheck) { return m_diskInDrive ? DiagnosticResponse::drOK : DiagnosticResponse::drNoDiskInDrive; } // Test manually m_lastCommand = LastCommand::lcCheckDiskInDrive; // If no hardware support then return no change if ((m_version.major == 1) && (m_version.minor < 8)) { m_lastError = DiagnosticResponse::drOldFirmware; return m_lastError; } char response; m_lastError = runCommand(COMMAND_CHECKDISKEXISTS, '\0', &response); if ((m_lastError == DiagnosticResponse::drStatusError) || (m_lastError == DiagnosticResponse::drOK)) { if (response == '#') { m_lastError = DiagnosticResponse::drNoDiskInDrive; m_diskInDrive = false; } else { if (response == '1') m_diskInDrive = true; } // Also read the write protect status if (!deviceRead(&response, 1, true)) { m_lastError = DiagnosticResponse::drReadResponseFailed; return m_lastError; } if ((response == '1') || (response == '#')) m_isWriteProtected = response == '1'; std::this_thread::sleep_for(std::chrono::milliseconds(1)); } return m_lastError; } // Check if an index pulse can be detected from the drive DiagnosticResponse ArduinoInterface::testIndexPulse() { // Port opned. We need to check what happens as the pin is toggled m_lastError = runCommand(COMMAND_DIAGNOSTICS, '3'); if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcRunDiagnostics; return m_lastError; } return m_lastError; } // Check if data can be detected from the drive DiagnosticResponse ArduinoInterface::testDataPulse() { // Port opned. We need to check what happens as the pin is toggled m_lastError = runCommand(COMMAND_DIAGNOSTICS, '4'); if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcRunDiagnostics; return m_lastError; } return m_lastError; } // Check if the USB to Serial device can keep up properly DiagnosticResponse ArduinoInterface::testTransferSpeed() { // Port opned. We need to receive about 10 * 256 bytes of data and verify its all valid m_lastError = runCommand(COMMAND_DIAGNOSTICS, '5'); if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcRunDiagnostics; return m_lastError; } applyCommTimeouts(true); unsigned char buffer[256]; for (int a = 0; a <= 10; a++) { unsigned long read; read = m_comPort.read(buffer, sizeof(buffer)); // With the timeouts we have this shouldn't happen if (read != sizeof(buffer)) { m_lastCommand = LastCommand::lcRunDiagnostics; m_lastError = DiagnosticResponse::drUSBSerialBad; applyCommTimeouts(false); return m_lastError; } for (size_t c = 0; c < read; c++) { if (buffer[c] != c) { m_lastCommand = LastCommand::lcRunDiagnostics; m_lastError = DiagnosticResponse::drUSBSerialBad; applyCommTimeouts(false); return m_lastError; } } } applyCommTimeouts(false); return m_lastError; } // Check CTS status by asking the device to set it and then checking what happened DiagnosticResponse ArduinoInterface::testCTS() { for (int a = 1; a <= 10; a++) { // Port opned. We need to check what happens as the pin is toggled m_lastError = runCommand(COMMAND_DIAGNOSTICS, (a & 1) ? '1' : '2'); if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcRunDiagnostics; closePort(); return m_lastError; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); bool ctsStatus = m_comPort.getCTSStatus(); // This doesnt actually run a command, this switches the CTS line back to its default setting m_lastError = runCommand(COMMAND_DIAGNOSTICS); if (ctsStatus ^ ((a & 1) != 0)) { // If we get here then the CTS value isn't what it should be closePort(); m_lastError = DiagnosticResponse::drCTSFailure; return m_lastError; } // Pass. Try the other state std::this_thread::sleep_for(std::chrono::milliseconds(1)); } return DiagnosticResponse::drOK; } // Attempts to verify if the reader/writer is running on this port bool ArduinoInterface::isPortCorrect(const std::wstring& portName) { // Attempts to verify if the reader/writer is running on this port SerialIO port; std::string version; DiagnosticResponse dr = internalOpenPort(portName, false, true, version, port); port.closePort(); return dr == DiagnosticResponse::drOK; } // Attempt to sync and get version DiagnosticResponse ArduinoInterface::attemptToSync(std::string& versionString, SerialIO& port) { char buffer[10]; // Send 'Version' Request buffer[0] = SPECIAL_ABORT_CHAR; buffer[1] = COMMAND_VERSION; unsigned long size = port.write(&buffer[0], 2); if (size != 2) { // Couldn't write to device port.closePort(); return DiagnosticResponse::drPortError; } memset(buffer, 0, sizeof(buffer)); int counterNoData = 0; int counterData = 0; int bytesRead = 0; // Keep a rolling buffer looking for the 1Vxxx response for (;;) { size = port.read(&buffer[4], 1); bytesRead += size; // Was something read? if (size) { if ((buffer[0] == '1') && (buffer[1] == 'V') && ((buffer[2] >= '1') && (buffer[2] <= '9')) && ((buffer[3] == ',') || (buffer[3] == '.')) && ((buffer[4] >= '0') && (buffer[4] <= '9'))) { // Success port.purgeBuffers(); std::this_thread::sleep_for(std::chrono::milliseconds(1)); port.purgeBuffers(); versionString = &buffer[1]; return DiagnosticResponse::drOK; } // Move backwards for (int a = 0; a < 4; a++) buffer[a] = buffer[a + 1]; if (counterData++ > 2048) { port.closePort(); return DiagnosticResponse::drErrorMalformedVersion; } } else { std::this_thread::sleep_for(std::chrono::milliseconds(1)); if (counterNoData++ > 120) { port.closePort(); return DiagnosticResponse::drErrorReadingVersion; } if (((counterNoData % 10) == 9) && (bytesRead == 0)) { // Give it a kick buffer[0] = COMMAND_VERSION; size = port.write(&buffer[0], 1); if (size != 1) { // Couldn't write to device port.closePort(); return DiagnosticResponse::drPortError; } } } } } // Attempts to verify if the reader/writer is running on this port DiagnosticResponse ArduinoInterface::internalOpenPort(const std::wstring& portName, bool enableCTSflowcontrol, bool triggerReset, std::string& versionString, SerialIO& port) { switch (port.openPort(portName)) { case SerialIO::Response::rInUse:return DiagnosticResponse::drPortInUse; case SerialIO::Response::rNotFound:return DiagnosticResponse::drPortNotFound; case SerialIO::Response::rOK: break; default: return DiagnosticResponse::drPortError; } // Configure the port SerialIO::Configuration config; config.baudRate = 2000000; config.ctsFlowControl = enableCTSflowcontrol; if (port.configurePort(config) != SerialIO::Response::rOK) return DiagnosticResponse::drPortError; port.setBufferSizes(16, 16); port.setReadTimeouts(10, 250); port.setWriteTimeouts(2000, 200); // Try to get the version DiagnosticResponse response = attemptToSync(versionString, port); if (response != DiagnosticResponse::drOK) { // It failed. Issue a reset if we're allowed and try again if (triggerReset) { port.setDTR(true); std::this_thread::sleep_for(std::chrono::milliseconds(10)); port.setDTR(false); std::this_thread::sleep_for(std::chrono::milliseconds(10)); port.closePort(); std::this_thread::sleep_for(std::chrono::milliseconds(250)); // Now re-connect and try again if (port.openPort(portName) != SerialIO::Response::rOK) return DiagnosticResponse::drPortError; response = attemptToSync(versionString, port); if (response != DiagnosticResponse::drOK) { port.closePort(); return response; } } else { port.closePort(); return response; } } return response; } // Attempts to open the reader running on the COM port number provided. Port MUST support 2M baud DiagnosticResponse ArduinoInterface::openPort(const std::wstring& portName, bool enableCTSflowcontrol) { m_lastCommand = LastCommand::lcOpenPort; closePort(); // Quickly force streaming to be aborted m_abortStreaming = true; std::string versionString; m_lastError = internalOpenPort(portName, enableCTSflowcontrol, true, versionString, m_comPort); if (m_lastError != DiagnosticResponse::drOK) return m_lastError; // it's possible theres still redundant data in the buffer char buffer[2]; int counter = 0; for (;;) { unsigned long size = m_comPort.read(buffer, 1); if (size < 1) if (counter++>=10) break; } // Possibly a bit overkill m_comPort.setBufferSizes(RAW_TRACKDATA_LENGTH * 2, RAW_TRACKDATA_LENGTH); m_version.major = versionString[1] - '0'; m_version.minor = versionString[3] - '0'; m_version.fullControlMod = versionString[2] == ','; // Switch to normal timeouts applyCommTimeouts(false); // Ok, success return m_lastError; } // Apply and change the timeouts on the com port void ArduinoInterface::applyCommTimeouts(bool shortTimeouts) { if (shortTimeouts) { m_comPort.setReadTimeouts(5, 12); } else { m_comPort.setReadTimeouts(2000, 200); } m_comPort.setWriteTimeouts(2000, 200); } // Closes the port down void ArduinoInterface::closePort() { LastCommand old = m_lastCommand; if (m_comPort.isPortOpen()) { // Force the drive to power down enableReading(false); // And close the handle m_comPort.closePort(); } m_inWriteMode = false; m_isWriteProtected = false; m_diskInDrive = false; m_lastCommand = old; } // Returns true if the track actually contains some data, else its considered blank or unformatted bool ArduinoInterface::trackContainsData(const RawTrackData& trackData) const { int zerocount = 0, ffcount = 0; unsigned char lastByte = trackData[0]; for (unsigned int counter = 1; counter < RAW_TRACKDATA_LENGTH; counter++) { if (trackData[counter] == lastByte) { switch (lastByte) { case 0xFF: ffcount++; zerocount = 0; break; case 0x00: ffcount = 0; zerocount++; break; default: zerocount = 0; ffcount = 0; } } else { lastByte = trackData[counter]; zerocount = 0; ffcount = 0; } } // More than this in a row and its bad return ((ffcount < 20) && (zerocount < 20)); } // Turns on and off the writing interface. If irError is returned the disk is write protected DiagnosticResponse ArduinoInterface::enableWriting(const bool enable, const bool reset) { if (enable) { // Enable the device m_lastError = runCommand(COMMAND_ENABLEWRITE); if (m_lastError == DiagnosticResponse::drError) { m_lastCommand = LastCommand::lcEnableWrite; m_lastError = DiagnosticResponse::drWriteProtected; return m_lastError; } if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcEnableWrite; return m_lastError; } m_inWriteMode = true; // Reset? if (reset) { // And rewind to the first track m_lastError = findTrack0(); if (m_lastError != DiagnosticResponse::drOK) return m_lastError; // Lets know where we are return selectSurface(DiskSurface::dsUpper); } m_lastError = DiagnosticResponse::drOK; return m_lastError; } else { // Disable the device m_lastError = runCommand(COMMAND_DISABLE); if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcDisableMotor; return m_lastError; } m_inWriteMode = false; return m_lastError; } } DiagnosticResponse ArduinoInterface::findTrack0() { // And rewind to the first track char status = '0'; m_lastError = runCommand(COMMAND_REWIND, '\000', &status); if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcRewind; if (status == '#') return DiagnosticResponse::drRewindFailure; return m_lastError; } return m_lastError; } // Turns on and off the reading interface DiagnosticResponse ArduinoInterface::enableReading(const bool enable, const bool reset, const bool dontWait) { m_inWriteMode = false; if (enable) { // Enable the device m_lastError = runCommand(dontWait ? COMMAND_ENABLE_NOWAIT : COMMAND_ENABLE); if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcEnableMotor; return m_lastError; } // Reset? if (reset) { m_lastError = findTrack0(); if (m_lastError != DiagnosticResponse::drOK) return m_lastError; // Lets know where we are return selectSurface(DiskSurface::dsUpper); } m_lastError = DiagnosticResponse::drOK; m_inWriteMode = m_version.fullControlMod; return m_lastError; } else { // Disable the device m_lastError = runCommand(COMMAND_DISABLE); if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcDisableMotor; return m_lastError; } return m_lastError; } } // Check and switch to HD disk DiagnosticResponse ArduinoInterface::setDiskCapacity(bool switchToHD_Disk) { // Disable the device m_lastError = runCommand(switchToHD_Disk ? COMMAND_SWITCHTO_HD : COMMAND_SWITCHTO_DD); if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcSwitchDiskMode; return m_lastError; } return m_lastError; } // If the drive is on track 0, this does a test seek to -1 if supported DiagnosticResponse ArduinoInterface::performNoClickSeek() { // And send the command and track. This is sent as ASCII text as a result of terminal testing. Easier to see whats going on bool isV18 = (m_version.major > 1) || ((m_version.major == 1) && (m_version.minor >= 8)); if (!isV18) return DiagnosticResponse::drOldFirmware; if (!m_version.fullControlMod) return DiagnosticResponse::drOldFirmware; m_lastError = runCommand(COMMAND_DO_NOCLICK_SEEK); if (m_lastError == DiagnosticResponse::drOK) { // Read extended data char status; if (!deviceRead(&status, 1, true)) { m_lastError = DiagnosticResponse::drReadResponseFailed; return m_lastError; } // 'x' means we didnt check it if (status != 'x') m_diskInDrive = status == '1'; // Also read the write protect status if (!deviceRead(&status, 1, true)) { m_lastError = DiagnosticResponse::drReadResponseFailed; return m_lastError; } m_isWriteProtected = status == '1'; } return m_lastError; } // Select the track, this makes the motor seek to this position DiagnosticResponse ArduinoInterface::selectTrack(const unsigned char trackIndex, const TrackSearchSpeed searchSpeed, bool ignoreDiskInsertCheck) { if (trackIndex > 81) { m_lastError = DiagnosticResponse::drTrackRangeError; return m_lastError; // no chance, it can't be done. } // And send the command and track. This is sent as ASCII text as a result of terminal testing. Easier to see whats going on bool isV18 = (m_version.major > 1) || ((m_version.major == 1) && (m_version.minor >= 8)); char buf[8]; if (isV18) { char flags = 0; switch (searchSpeed) { case TrackSearchSpeed::tssNormal: flags = 1; break; // Speed - 1 case TrackSearchSpeed::tssFast: flags = 2; break; // Speed - 2 case TrackSearchSpeed::tssVeryFast: flags = 3; break; // Speed - 3 default: break; } if (!ignoreDiskInsertCheck) flags |= 4; #ifdef _WIN32 sprintf_s(buf, "%c%02i%c", COMMAND_GOTOTRACK_REPORT, trackIndex, flags); #else sprintf(buf, "%c%02i%c", COMMAND_GOTOTRACK_REPORT, trackIndex, flags); #endif } else { #ifdef _WIN32 sprintf_s(buf, "%c%02i", COMMAND_GOTOTRACK, trackIndex); #else sprintf(buf, "%c%02i", COMMAND_GOTOTRACK, trackIndex); #endif } // Send track number. if (!deviceWrite(buf, strlen(buf))) { m_lastCommand = LastCommand::lcGotoTrack; m_lastError = DiagnosticResponse::drSendFailed; return m_lastError; } // Get result char result; if (!deviceRead(&result, 1, true)) { m_lastCommand = LastCommand::lcGotoTrack; m_lastError = DiagnosticResponse::drReadResponseFailed; return m_lastError; } switch (result) { case '2': m_lastError = DiagnosticResponse::drOK; break; // already at track. No op needed. V1.8 only case '1': m_lastError = DiagnosticResponse::drOK; if (isV18) { // Read extended data char status; if (!deviceRead(&status, 1, true)) { m_lastError = DiagnosticResponse::drReadResponseFailed; return m_lastError; } // 'x' means we didnt check it if (status != 'x') m_diskInDrive = status == '1'; // Also read the write protect status if (!deviceRead(&status, 1, true)) { m_lastError = DiagnosticResponse::drReadResponseFailed; return m_lastError; } m_isWriteProtected = status == '1'; } break; case '0': m_lastCommand = LastCommand::lcGotoTrack; m_lastError = DiagnosticResponse::drSelectTrackError; break; default: m_lastCommand = LastCommand::lcGotoTrack; m_lastError = DiagnosticResponse::drStatusError; break; } return m_lastError; } // Erases the current track by writing 0xAA to it DiagnosticResponse ArduinoInterface::eraseCurrentTrack() { m_lastError = runCommand(COMMAND_ERASETRACK); if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcEraseTrack; return m_lastError; } char result; if (!deviceRead(&result, 1, true)) { m_lastCommand = LastCommand::lcEraseTrack; m_lastError = DiagnosticResponse::drReadResponseFailed; return m_lastError; } if (result == 'N') { m_lastCommand = LastCommand::lcEraseTrack; m_lastError = DiagnosticResponse::drWriteProtected; return m_lastError; } // Check result if (!deviceRead(&result, 1, true)) { m_lastCommand = LastCommand::lcEraseTrack; m_lastError = DiagnosticResponse::drReadResponseFailed; return m_lastError; } if (result != '1') { m_lastCommand = LastCommand::lcEraseTrack; m_lastError = DiagnosticResponse::drError; return m_lastError; } return m_lastError; } // Choose which surface of the disk to read from DiagnosticResponse ArduinoInterface::selectSurface(const DiskSurface side) { m_lastError = runCommand((side == DiskSurface::dsUpper) ? COMMAND_HEAD0 : COMMAND_HEAD1); if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcSelectSurface; return m_lastError; } return m_lastError; } void writeBit(RawTrackData& output, int& pos, int& bit, int value) { if (pos >= RAW_TRACKDATA_LENGTH) return; output[pos] <<= 1; output[pos] |= value; bit++; if (bit >= 8) { pos++; bit = 0; } } void unpack(const RawTrackData& data, RawTrackData& output) { int pos = 0; size_t index = 0; int p2 = 0; memset(output, 0, sizeof(output)); while (pos < RAW_TRACKDATA_LENGTH) { // Each byte contains four pairs of bits that identify an MFM sequence to be encoded for (int b = 6; b >= 0; b -= 2) { unsigned char value = (data[index] >> b) & 3; switch (value) { case 0: // This can't happen, its invalid data but we account for 4 '0' bits writeBit(output, pos, p2, 0); writeBit(output, pos, p2, 0); writeBit(output, pos, p2, 0); writeBit(output, pos, p2, 0); break; case 1: // This is an '01' writeBit(output, pos, p2, 0); writeBit(output, pos, p2, 1); break; case 2: // This is an '001' writeBit(output, pos, p2, 0); writeBit(output, pos, p2, 0); writeBit(output, pos, p2, 1); break; case 3: // this is an '0001' writeBit(output, pos, p2, 0); writeBit(output, pos, p2, 0); writeBit(output, pos, p2, 0); writeBit(output, pos, p2, 1); break; } } index++; if (index >= sizeof(data)) return; } // There will be left-over data } // Read RAW data from the current track and surface DiagnosticResponse ArduinoInterface::readCurrentTrack(RawTrackData& trackData, const bool readFromIndexPulse) { m_lastError = runCommand(COMMAND_READTRACK); RawTrackData tmp; // Allow command retry if (m_lastError != DiagnosticResponse::drOK) { // Clear the buffer deviceRead(&tmp, RAW_TRACKDATA_LENGTH); m_lastError = runCommand(COMMAND_READTRACK); if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcReadTrack; return m_lastError; } } unsigned char signalPulse = readFromIndexPulse ? 1 : 0; if (!deviceWrite(&signalPulse, 1)) { m_lastCommand = LastCommand::lcReadTrack; m_lastError = DiagnosticResponse::drSendParameterFailed; return m_lastError; } // Keep reading until he hit RAW_TRACKDATA_LENGTH or a null byte is recieved int bytePos = 0; int readFail = 0; for (;;) { unsigned char value; if (deviceRead(&value, 1, true)) { if (value == 0) break; else if (bytePos < RAW_TRACKDATA_LENGTH) tmp[bytePos++] = value; } else { readFail++; if (readFail > 4) { m_lastCommand = LastCommand::lcReadTrack; m_lastError = DiagnosticResponse::drReadResponseFailed; return m_lastError; } } } unpack(tmp, trackData); m_lastError = DiagnosticResponse::drOK; return m_lastError; } // Reads a complete rotation of the disk, and returns it using the callback function whcih can return FALSE to stop // An instance of RotationExtractor is required. This is purely to save on re-allocations. It is internally reset each time DiagnosticResponse ArduinoInterface::readRotation(RotationExtractor& extractor, const unsigned int maxOutputSize, RotationExtractor::MFMSample* firstOutputBuffer, RotationExtractor::IndexSequenceMarker& startBitPatterns, std::function onRotation) { if ((m_version.major == 1) && (m_version.minor < 8)) { m_lastCommand = LastCommand::lcReadTrackStream; m_lastError = DiagnosticResponse::drOldFirmware; return m_lastError; } // Who would do this, right? if ((maxOutputSize < 1) || (!firstOutputBuffer)) { m_lastError = DiagnosticResponse::drError; m_lastCommand = LastCommand::lcReadTrackStream; return m_lastError; } m_lastError = runCommand(COMMAND_READTRACKSTREAM); // Let the class know we're doing some streaming stuff m_isStreaming = true; m_abortStreaming = false; m_abortSignalled = false; // Number of times we failed to read anything int readFail = 0; // Buffer to read into unsigned char tempReadBuffer[64] = { 0 }; // Reset ready for extraction extractor.reset(); // Remind it if the 'index' data we want to sync to extractor.setIndexSequence(startBitPatterns); // Sliding window for abort char slidingWindow[5] = { 0,0,0,0,0 }; int noDataCounter = 0; bool timeout = false; for (;;) { // More efficient to read several bytes in one go unsigned long bytesAvailable = m_comPort.getBytesWaiting(); if (bytesAvailable < 1) bytesAvailable = 1; if (bytesAvailable > sizeof(tempReadBuffer)) bytesAvailable = sizeof(tempReadBuffer); unsigned long bytesRead = m_comPort.read(tempReadBuffer, m_abortSignalled ? 1 : bytesAvailable); for (size_t a = 0; a < bytesRead; a++) { if (m_abortSignalled) { // Make space for (int s = 0; s < 4; s++) slidingWindow[s] = slidingWindow[s + 1]; // Append the new byte slidingWindow[4] = tempReadBuffer[a]; // Watch the sliding window for the pattern we need if ((slidingWindow[0] == 'X') && (slidingWindow[1] == 'Y') && (slidingWindow[2] == 'Z') && (slidingWindow[3] == SPECIAL_ABORT_CHAR) && (slidingWindow[4] == '1')) { m_isStreaming = false; m_comPort.purgeBuffers(); m_lastCommand = LastCommand::lcReadTrackStream; m_lastError = timeout ? DiagnosticResponse::drNoDiskInDrive : DiagnosticResponse::drOK; applyCommTimeouts(false); return m_lastError; } } else { unsigned char byteRead = tempReadBuffer[a]; unsigned short readSpeed = (((unsigned long long)(64 + ((unsigned int)((byteRead & 0x07) * 16)) * 2000) / 128)); unsigned char tmp; RotationExtractor::MFMSequenceInfo sequence; // Now packet up the data in the format the rotation extractor expects it to be in tmp = (byteRead >> 5) & 0x03; sequence.mfm = (tmp == 0) ? RotationExtractor::MFMSequence::mfm0000 : (RotationExtractor::MFMSequence)(tmp - 1); sequence.timeNS = 3000 + ((unsigned int)sequence.mfm * 2000) + readSpeed; if (sequence.mfm == RotationExtractor::MFMSequence::mfm0000) noDataCounter++; else noDataCounter = 0; extractor.submitSequence(sequence, (byteRead & 0x80) != 0); tmp = (byteRead >> 3) & 0x03; sequence.mfm = (tmp == 0) ? RotationExtractor::MFMSequence::mfm0000 : (RotationExtractor::MFMSequence)(tmp - 1); sequence.timeNS = 3000 + ((unsigned int)sequence.mfm * 2000) + readSpeed; if (sequence.mfm == RotationExtractor::MFMSequence::mfm0000) noDataCounter++; else noDataCounter = 0; extractor.submitSequence(sequence, false); // Is it ready to extract? if (extractor.canExtract()) { unsigned int bits = 0; // Go! if (extractor.extractRotation(firstOutputBuffer, bits, maxOutputSize)) { m_diskInDrive = true; if (!onRotation(&firstOutputBuffer, bits)) { // And if the callback says so we stop. abortReadStreaming(); } // Always save this back extractor.getIndexSequence(startBitPatterns); } } else if ((extractor.totalTimeReceived() > 300000000) && (noDataCounter > 100)) { // No data, stop abortReadStreaming(); noDataCounter = 0; timeout = true; m_diskInDrive = false; // probably no disk } } } if (bytesRead < 1) { readFail++; if (readFail > 20) { m_abortStreaming = false; // force the 'abort' command to be sent abortReadStreaming(); m_lastCommand = LastCommand::lcReadTrackStream; m_lastError = DiagnosticResponse::drReadResponseFailed; m_isStreaming = false; applyCommTimeouts(false); return m_lastError; } else std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } } // Stops the read streamming immediately and any data in the buffer will be discarded. bool ArduinoInterface::abortReadStreaming() { if ((m_version.major == 1) && (m_version.minor < 8)) { return false; } if (!m_isStreaming) return true; if (!m_abortStreaming) { // We know what this is, but the Arduino doesn't unsigned char command = SPECIAL_ABORT_CHAR; m_abortSignalled = true; // Send a byte. This triggers the 'abort' on the Arduino if (!deviceWrite(&command, 1)) { return false; } } m_abortStreaming = true; return true; } // Read a bit from the data provided inline int readBit(const unsigned char* buffer, const unsigned int maxLength, int& pos, int& bit) { if (pos >= (int)maxLength) { bit--; if (bit < 0) { bit = 7; pos++; } return (bit & 1) ? 0 : 1; } int ret = (buffer[pos] >> bit) & 1; bit--; if (bit < 0) { bit = 7; pos++; } return ret; } #define PRECOMP_NONE 0x00 #define PRECOMP_ERLY 0x04 #define PRECOMP_LATE 0x08 /* If we have a look at the previous and next three bits, assume the current is a '1', then these are the only valid combinations that coudl be allowed I have chosen the PRECOMP rule based on trying to get the current '1' as far away as possible from the nearest '1' LAST CURRENT NEXT PRECOMP Hex Value 0 0 0 1 0 0 0 Normal 0 0 0 1 0 0 1 Early 0x09 0 0 0 1 0 1 0 Early 0x0A 0 1 0 1 0 0 0 Late 0x28 0 1 0 1 0 0 1 Late 0x29 0 1 0 1 0 1 0 Normal 1 0 0 1 0 0 0 Late 0x48 1 0 0 1 0 0 1 Normal 1 0 0 1 0 1 0 Early 0x4A */ // The precomp version of the above. Dont use the above function directly to write precomp mode, it wont work. Data must be passed with an 0xAA each side at least DiagnosticResponse ArduinoInterface::writeCurrentTrackPrecomp(const unsigned char* mfmData, const unsigned short numBytes, const bool writeFromIndexPulse, bool usePrecomp) { m_lastCommand = LastCommand::lcWriteTrack; if ((m_version.major == 1) && (m_version.minor < 8)) return DiagnosticResponse::drOldFirmware; // First step is we need to re-encode the supplied buffer into a packed format with precomp pre-calculated. // Each nybble looks like: xxyy // where xx is: 0=no precomp, 1=-ve, 2=+ve // yy is: 0: 4us, 1: 6us, 2: 8us, 3: 10us // *4 is a worse case situation, ie: if everything was a 01. The +128 is for any extra padding const unsigned int maxOutSize = (numBytes * 4) + 16; unsigned char* outputBuffer = (unsigned char*)malloc(maxOutSize); if (!outputBuffer) { m_lastError = DiagnosticResponse::drSendParameterFailed; return m_lastError; } // Original data was written from MSB downto LSB int pos = 0; int bit = 7; unsigned int outputPos = 0; unsigned char sequence = 0xAA; // start at 10101010 unsigned char* output = outputBuffer; int lastCount = 2; // Re-encode the data into our format and apply precomp. The +8 is to ensure theres some padding around the edge which will come out as 010101 etc while (pos < numBytes + 8) { *output = 0; for (int i = 0; i < 2; i++) { int b, count = 0; // See how many zero bits there are before we hit a 1 do { b = readBit(mfmData, numBytes, pos, bit); sequence = ((sequence << 1) & 0x7F) | b; count++; } while (((sequence & 0x08) == 0) && (pos < numBytes + 8)); // Validate range if (count < 2) count = 2; // <2 would be a 11 sequence, not allowed if (count > 5) count = 5; // max we support 01, 001, 0001, 00001 // Write to stream. Based on the rules above we apply some precomp int precomp = 0; if (usePrecomp) { switch (sequence) { case 0x09: case 0x0A: case 0x4A: // early; precomp = PRECOMP_ERLY; break; case 0x28: case 0x29: case 0x48: // late precomp = PRECOMP_LATE; break; default: precomp = PRECOMP_NONE; break; } } else precomp = PRECOMP_NONE; *output |= ((lastCount - 2) | precomp) << (i * 4); lastCount = count; } output++; outputPos++; if (outputPos >= maxOutSize) { // should never happen free(outputBuffer); m_lastError = DiagnosticResponse::drSendParameterFailed; return m_lastError; } } m_lastError = internalWriteTrack(outputBuffer, outputPos, writeFromIndexPulse, true); free(outputBuffer); return m_lastError; } // Writes RAW data onto the current track DiagnosticResponse ArduinoInterface::writeCurrentTrack(const unsigned char* data, const unsigned short numBytes, const bool writeFromIndexPulse) { return internalWriteTrack(data, numBytes, writeFromIndexPulse, false); } // Writes RAW data onto the current track DiagnosticResponse ArduinoInterface::internalWriteTrack(const unsigned char* data, const unsigned short numBytes, const bool writeFromIndexPulse, bool usePrecomp) { // Fall back if older firmware if ((m_version.major == 1) && (m_version.minor < 8) && usePrecomp) { return DiagnosticResponse::drOldFirmware; } m_lastError = runCommand(usePrecomp ? COMMAND_WRITETRACKPRECOMP : COMMAND_WRITETRACK); if (m_lastError != DiagnosticResponse::drOK) { m_lastCommand = LastCommand::lcWriteTrack; return m_lastError; } unsigned char chr; if (!deviceRead(&chr, 1, true)) { m_lastCommand = LastCommand::lcWriteTrack; m_lastError = DiagnosticResponse::drReadResponseFailed; return m_lastError; } // 'N' means NO Writing, aka write protected if (chr == 'N') { m_lastCommand = LastCommand::lcWriteTrack; m_lastError = DiagnosticResponse::drWriteProtected; return m_lastError; } if (chr != 'Y') { m_lastCommand = LastCommand::lcWriteTrack; m_lastError = DiagnosticResponse::drStatusError; return m_lastError; } // Now we send the number of bytes we're planning on transmitting unsigned char b = (numBytes >> 8); if (!deviceWrite(&b, 1)) { m_lastCommand = LastCommand::lcWriteTrack; m_lastError = DiagnosticResponse::drSendParameterFailed; return m_lastError; } b = numBytes & 0xFF; if (!deviceWrite(&b, 1)) { m_lastCommand = LastCommand::lcWriteTrack; m_lastError = DiagnosticResponse::drSendParameterFailed; return m_lastError; } // Explain if we want index pulse sync writing (slower and not required by normal AmigaDOS disks) b = writeFromIndexPulse ? 1 : 0; if (!deviceWrite(&b, 1)) { m_lastCommand = LastCommand::lcWriteTrack; m_lastError = DiagnosticResponse::drSendParameterFailed; return m_lastError; } unsigned char response; if (!deviceRead(&response, 1, true)) { m_lastCommand = LastCommand::lcWriteTrack; m_lastError = DiagnosticResponse::drReadResponseFailed; return m_lastError; } if (response != '!') { m_lastCommand = LastCommand::lcWriteTrack; m_lastError = DiagnosticResponse::drStatusError; return m_lastError; } if (!deviceWrite((const void*)data, numBytes)) { m_lastCommand = LastCommand::lcWriteTrack; m_lastError = DiagnosticResponse::drSendDataFailed; return m_lastError; } if (!deviceRead(&response, 1, true)) { m_lastCommand = LastCommand::lcWriteTrack; m_lastError = DiagnosticResponse::drTrackWriteResponseError; return m_lastError; } // If this is a '1' then the Arduino didn't miss a single bit! if (response != '1') { m_lastCommand = LastCommand::lcWriteTrack; switch (response) { case 'X': m_lastError = DiagnosticResponse::drWriteTimeout; break; case 'Y': m_lastError = DiagnosticResponse::drFramingError; break; case 'Z': m_lastError = DiagnosticResponse::drSerialOverrun; break; default: m_lastError = DiagnosticResponse::drStatusError; break; } return m_lastError; } m_lastError = DiagnosticResponse::drOK; return m_lastError; } // Returns a list of ports this coudl be available on void ArduinoInterface::enumeratePorts(std::vector& portList) { portList.clear(); std::vector prts; SerialIO prt; prt.enumSerialPorts(prts); for (const SerialIO::SerialPortInformation& port : prts) portList.push_back(port.portName); } // Run a command that returns 1 or 0 for its response DiagnosticResponse ArduinoInterface::runCommand(const char command, const char parameter, char* actualResponse) { unsigned char response; // Pause for I/O std::this_thread::sleep_for(std::chrono::milliseconds(1)); // Send the command if (!deviceWrite(&command, 1)) { m_lastError = DiagnosticResponse::drSendFailed; return m_lastError; } // Only send the parameter if its not NULL if (parameter != '\0') if (!deviceWrite(¶meter, 1)) { m_lastError = DiagnosticResponse::drSendParameterFailed; return m_lastError; } // And read the response if (!deviceRead(&response, 1, true)) { m_lastError = DiagnosticResponse::drReadResponseFailed; return m_lastError; } if (actualResponse) *actualResponse = response; // Evaluate the response switch (response) { case '1': m_lastError = DiagnosticResponse::drOK; break; case '0': m_lastError = DiagnosticResponse::drError; break; default: m_lastError = DiagnosticResponse::drStatusError; break; } return m_lastError; } // Read a desired number of bytes into the target pointer bool ArduinoInterface::deviceRead(void* target, const unsigned int numBytes, const bool failIfNotAllRead) { if (!m_comPort.isPortOpen()) return false; unsigned long read = m_comPort.read(target, numBytes); if (read < numBytes) { if (failIfNotAllRead) return false; // Clear the unread bytes char* target2 = ((char*)target) + read; memset(target2, 0, numBytes - read); } return true; } // Writes a desired number of bytes from the the pointer bool ArduinoInterface::deviceWrite(const void* source, const unsigned int numBytes) { return m_comPort.write(source, numBytes) == numBytes; }