diff --git a/DeviceAdapters/Arduino/AOTFcontroller/AOTFcontroller.ino b/DeviceAdapters/Arduino/AOTFcontroller/AOTFcontroller.ino index 136bc97f7..84ce70aef 100644 --- a/DeviceAdapters/Arduino/AOTFcontroller/AOTFcontroller.ino +++ b/DeviceAdapters/Arduino/AOTFcontroller/AOTFcontroller.ino @@ -107,6 +107,41 @@ * Returns: 35 followed by 1 byte with the number of digital output pins * Available as of version 5 * + * Get DA channel voltage range and resolution: 36x + * Where x is the DA channel (0-based). + * Returns: 36 followed by channel, then min voltage, max voltage, and the + * maximum digital code for that channel: + * - min voltage: signed long (int32_t), microvolts, 4 bytes, highbyte first + * - max voltage: signed long (int32_t), microvolts, 4 bytes, highbyte first + * - max digital code: unsigned long (uint32_t), 4 bytes, highbyte first + * (e.g. 4095 for a 12-bit DAC channel) + * A min/max voltage of 0/0 means the voltage range is not known. + * Available as of version 6 + * + * Get max number of DA sequence events per channel: 37 + * Returns: 37 followed by the max number of events per channel, unsigned int, 2 bytes, highbyte first + * Available as of version 6 + * + * Upload a DA voltage sequence for one channel: 38xccvv... + * Where x is the DA channel (0-based), cc is the number of events (unsigned int, 2 bytes, + * highbyte first), and vv... is cc pairs of bytes, each pair a 12-bit significant number + * (msb, lsb) as in command 3. + * Controller returns 38, channel, then the number of events actually stored (2 bytes, + * highbyte first) - a value less than cc means the upload was truncated (e.g. timed out). + * Available as of version 6 + * + * Start DA (analog) triggered sequence output: 39 + * Every DA channel with a non-empty uploaded sequence advances to its next value on each + * transition (rising or falling) of the trigger input pin (the same pin used for digital + * triggered mode, command 8). All channels share one step index, so channels with + * sequences of equal length stay in lock-step. Controller returns 39. + * Available as of version 6 + * + * Stop DA (analog) triggered sequence output: 43 + * Controller returns 43x where x is the number of triggers received during the last DA + * sequence run. Output is left at its last value (not forced to zero). + * Available as of version 6 + * * * Read digital state of analogue input pins 0-5: 40 * Returns raw value of PINC (two high bits are not used) @@ -121,24 +156,59 @@ * Get digital patterm * Get Number of digital patterns */ - - unsigned int version_ = 5; -// If you have one of these DA chips attached, uncomment the appropriate define + + /************* For DA accessory chips, edit this section ********/ + +// If you have one of these DA chips attached, uncomment the appropriate define. +// WARNING: if none of these are defined, numDAChannels_ below becomes 0 and every +// DA channel silently does nothing - single-value writes (command 3) and sequence +// uploads (command 38) will report "success" or "0 events stored" respectively, +// with no error, even though the host may already have "Volts"/"MaxVolt"/"Sequence" +// properties configured for DA channels from a previous session. Double-check this +// matches your attached hardware before troubleshooting anything else DA-related. // #define TLV5618 // #define TLV56x8 +// #define MCP4728 +// Voltage range and resolution of the DA channels, reported to the host via +// command 36. Currently the same fixed values are used for all channels; +// change these if the attached DAC hardware differs. +const int32_t DA_MIN_VOLTAGE_MICROV = 0; // 0 V +const int32_t DA_MAX_VOLTAGE_MICROV = 5000000L; // 5 V +const uint32_t DA_NUM_STEPS = 4095; // max digital code (12-bit) + +/**************** End editing DA accessory chips section ********/ + +const unsigned int version_ = 6; + +#ifdef MCP4728 +#include +#include +#include +#endif + + +#if defined TLV5618 +const uint8_t numDAChannels_ = 2; +#elif defined TLV56x8 +const uint8_t numDAChannels_ = 4; +#elif defined MCP4728 +const uint8_t numDAChannels_ = 4; +#else +const uint8_t numDAChannels_ = 0; +#endif - // const uint8_t numDAChannels_ = 0; // Set to appropriate number depending on attached DA chip - #if defined TLV5618 - const uint8_t numDAChannels_ = 2; - #elif defined TLV56x8 - const uint8_t numDAChannels_ = 4; - #else - const uint8_t numDAChannels_ = 0; - #endif - const uint8_t numDigitalPins_ = 6; +#ifdef MCP4728 +static const uint8_t MCP4728_ADDR = 0x60; +static const uint8_t LDAC_PIN = 4; +static const uint8_t RDY_PIN = 3; +Adafruit_MCP4728 mcp; +bool mcp_ok = false; +#endif + +const uint8_t numDigitalPins_ = 6; // pin on which to receive the trigger (2 and 3 can be used with interrupts, although this code does not use interrupts) int inPin_ = 2; @@ -146,9 +216,11 @@ int inPinBit_ = 1 << inPin_; // bit mask // pin connected to DIN of TLV5618 + #if defined TLV5618 || defined TLV56x8 int dataPin = 3; // pin connected to SCLK of TLV5618 int clockPin = 4; + #endif // pin connected to CS of TLV5618 #ifdef TLV5618 int latchPin = 5; @@ -173,14 +245,28 @@ bool blankOnHigh_ = false; bool triggerMode_ = false; boolean triggerState_ = false; - + + // Analog (DA) voltage sequence support - mirrors the digital pattern sequence above, but + // stores a 12-bit code sequence per DA channel and is driven off the same trigger pin + // (inPin_) via a single shared step index (see command 39's doc comment). + const uint8_t MAX_DA_CHANNELS_ = 4; // largest numDAChannels_ across all chip branches + const uint16_t DA_SEQUENCELENGTH = 32; // per channel; increase with care - see SEQUENCELENGTH comment above + uint16_t daSequence_[MAX_DA_CHANNELS_][DA_SEQUENCELENGTH]; + uint16_t daSequenceLength_[MAX_DA_CHANNELS_] = {0, 0, 0, 0}; + volatile long daTriggerNr_; // total # of triggers in this DA-sequence run + volatile long daSequenceNr_; // shared step index into every channel's daSequence_[] + bool daTriggerMode_ = false; + boolean daTriggerState_ = false; + void setup() { // Higher speeds do not appear to be reliable Serial.begin(57600); pinMode(inPin_, INPUT); + #if defined TLV5618 || defined TLV56x8 pinMode (dataPin, OUTPUT); pinMode (clockPin, OUTPUT); + #endif #ifdef TLV5618 pinMode (latchPin, OUTPUT); #endif @@ -208,6 +294,28 @@ digitalWrite(CS2, HIGH); #endif + #ifdef MCP4728 + pinMode(LDAC_PIN, OUTPUT); + digitalWrite(LDAC_PIN, LOW); + pinMode(RDY_PIN, INPUT_PULLUP); + Wire.begin(); + mcp_ok = mcp.begin(MCP4728_ADDR); + { + const int INIT_FLAG_ADDR = 0; + const byte INIT_DONE = 0xA5; + if (mcp_ok) { + byte initFlag = EEPROM.read(INIT_FLAG_ADDR); + if (initFlag != INIT_DONE) { + mcp.fastWrite(0, 0, 0, 0); + if (mcp.saveToEEPROM()) + EEPROM.update(INIT_FLAG_ADDR, INIT_DONE); + } else { + mcp.fastWrite(0, 0, 0, 0); + } + } + } + #endif + for (unsigned int i = 0; i < SEQUENCELENGTH; i++) { triggerPattern_[i] = 0; triggerDelay_[i] = 0; @@ -246,7 +354,8 @@ msb &= B00001111; if (waitForSerial(timeOut_)) { byte lsb = Serial.read(); - analogueOut(channel, msb, lsb); + if (channel >= 0 && channel < numDAChannels_ && channel < MAX_DA_CHANNELS_) + analogueOut(channel, msb, lsb); Serial.write( byte(3)); Serial.write( channel); Serial.write(msb); @@ -442,6 +551,76 @@ Serial.write(byte(numDigitalPins_)); break; + // Returns the voltage range and max digital code (signed V, unsigned steps) of the given DA channel + case 36: + if (waitForSerial(timeOut_)) { + int channel = Serial.read(); + int32_t minMicroV = 0, maxMicroV = 0; + uint32_t numSteps = 0; + getDaVoltageRangeMicroV(channel, minMicroV, maxMicroV, numSteps); + Serial.write(byte(36)); + Serial.write(byte(channel)); + Serial.write(byte((minMicroV >> 24) & 0xFF)); + Serial.write(byte((minMicroV >> 16) & 0xFF)); + Serial.write(byte((minMicroV >> 8) & 0xFF)); + Serial.write(byte(minMicroV & 0xFF)); + Serial.write(byte((maxMicroV >> 24) & 0xFF)); + Serial.write(byte((maxMicroV >> 16) & 0xFF)); + Serial.write(byte((maxMicroV >> 8) & 0xFF)); + Serial.write(byte(maxMicroV & 0xFF)); + Serial.write(byte((numSteps >> 24) & 0xFF)); + Serial.write(byte((numSteps >> 16) & 0xFF)); + Serial.write(byte((numSteps >> 8) & 0xFF)); + Serial.write(byte(numSteps & 0xFF)); + } + break; + + // Returns the maximum number of DA sequence events that can be uploaded per channel + case 37: + Serial.write(byte(37)); + Serial.write(highByte(DA_SEQUENCELENGTH)); + Serial.write(lowByte(DA_SEQUENCELENGTH)); + break; + + // Uploads a DA voltage sequence for one channel + case 38: + if (waitForSerial(timeOut_)) { + int channel = Serial.read(); + if (waitForSerial(timeOut_)) { + unsigned int hi = Serial.read(); + if (waitForSerial(timeOut_)) { + unsigned int lo = Serial.read(); + unsigned int expectedCount = (hi << 8) | lo; + unsigned int count = 0; + if (channel >= 0 && channel < numDAChannels_ && channel < MAX_DA_CHANNELS_ + && expectedCount <= DA_SEQUENCELENGTH) { + while (count < expectedCount && waitForSerial(timeOut_)) { + byte msb = Serial.read(); + if (!waitForSerial(timeOut_)) break; + byte lsb = Serial.read(); + daSequence_[channel][count] = (((uint16_t)(msb & 0x0F)) << 8) | (uint16_t) lsb; + count++; + } + daSequenceLength_[channel] = count; + } + Serial.write(byte(38)); + Serial.write(byte(channel)); + Serial.write(highByte(count)); + Serial.write(lowByte(count)); + } + } + } + break; + + // Starts DA (analog) triggered sequence output + case 39: + daSequenceNr_ = 0; + daTriggerNr_ = 0; + daTriggerState_ = digitalRead(inPin_) == HIGH; + daTriggerMode_ = true; + Serial.write(byte(39)); + break; + case 40: Serial.write( byte(40)); Serial.write( PINC); @@ -479,6 +658,13 @@ } break; + // Stops DA (analog) triggered sequence output + case 43: + daTriggerMode_ = false; + Serial.write(byte(43)); + Serial.write(daTriggerNr_); + break; + } } @@ -513,10 +699,25 @@ } else { if (! (PIND & inPinBit_)) PORTB = 0; - else + else PORTB = currentPattern_; } } + + if (daTriggerMode_) { + boolean tmp = PIND & inPinBit_; + if (tmp != daTriggerState_) { + for (uint8_t ch = 0; ch < numDAChannels_ && ch < MAX_DA_CHANNELS_; ch++) { + if (daSequenceLength_[ch] > 0) { + uint16_t code = daSequence_[ch][daSequenceNr_ % daSequenceLength_[ch]]; + analogueOut(ch, (byte)(code >> 8), (byte)(code & 0xFF)); + } + } + daSequenceNr_++; + daTriggerNr_++; + daTriggerState_ = tmp; + } + } } @@ -593,12 +794,47 @@ void analogueOut(int channel, byte msb, byte lsb) digitalWrite(CS1, HIGH); digitalWrite(CS2, HIGH); } + +#elif defined MCP4728 + +void analogueOut(int channel, byte msb, byte lsb) { + if (!mcp_ok) return; + int ch = channel; + if (ch < 0 || ch > 3) return; + uint16_t value12 = ((uint16_t)(msb & 0x0F) << 8) | (uint16_t)lsb; + MCP4728_channel_t mcp_ch = MCP4728_CHANNEL_A; + if (ch == 1) mcp_ch = MCP4728_CHANNEL_B; + else if (ch == 2) mcp_ch = MCP4728_CHANNEL_C; + else if (ch == 3) mcp_ch = MCP4728_CHANNEL_D; + mcp.setChannelValue(mcp_ch, value12, MCP4728_VREF_VDD, MCP4728_GAIN_1X, + MCP4728_PD_MODE_NORMAL, false); +} + #else void analogueOut(int channel, byte msb, byte lsb) {}; // noop #endif +// Reports the fixed voltage range and resolution (DA_MIN_VOLTAGE_MICROV / +// DA_MAX_VOLTAGE_MICROV / DA_NUM_STEPS) for the given channel, if a DA chip +// is compiled in and the channel is valid; otherwise reports 0/0/0 (unknown). +bool getDaVoltageRangeMicroV(int channel, int32_t &minMicroV, int32_t &maxMicroV, uint32_t &numSteps) { +#if defined TLV5618 || defined TLV56x8 || defined MCP4728 + if (channel < 0 || channel >= numDAChannels_) { minMicroV = 0; maxMicroV = 0; numSteps = 0; return false; } + #if defined MCP4728 + if (!mcp_ok) { minMicroV = 0; maxMicroV = 0; numSteps = 0; return false; } + #endif + minMicroV = DA_MIN_VOLTAGE_MICROV; + maxMicroV = DA_MAX_VOLTAGE_MICROV; + numSteps = DA_NUM_STEPS; + return true; +#else + minMicroV = 0; maxMicroV = 0; numSteps = 0; + return false; +#endif +} + diff --git a/DeviceAdapters/Arduino/Arduino.cpp b/DeviceAdapters/Arduino/Arduino.cpp index 1360c5212..03852cb59 100644 --- a/DeviceAdapters/Arduino/Arduino.cpp +++ b/DeviceAdapters/Arduino/Arduino.cpp @@ -16,6 +16,7 @@ #include "ModuleInterface.h" #include #include +#include #include #ifdef WIN32 @@ -49,7 +50,7 @@ const char* g_DeviceNameArduinoMagnifier = "Arduino-Magnifier"; // Global info about the state of the Arduino. This should be folded into a class const int g_Min_MMVersion = 1; -const int g_Max_MMVersion = 5; +const int g_Max_MMVersion = 6; // version of the firmware code const char* g_versionProp = "Version"; // space to provide more information about the firmware. @@ -132,8 +133,13 @@ CArduinoHub::CArduinoHub() : version_(0), extendedVersion_(0), maxNumPatterns_(12), + maxDASeqLength_(0), numDAChannels_(2), numDigitalPins_(6), + daMinV_(g_MaxDAChannels + 1, 0.0), + daMaxV_(g_MaxDAChannels + 1, 0.0), + daRangeKnown_(g_MaxDAChannels + 1, false), + daNumSteps_(g_MaxDAChannels + 1, 4095UL), magnifier_(0), switchState_ (0), shutterState_ (0) @@ -414,6 +420,66 @@ int CArduinoHub::Initialize() numDigitalPins_ = answer2[1]; } + if (version_ >= 6) + { + for (unsigned ch0 = 0; ch0 < numDAChannels_ && ch0 < (unsigned) g_MaxDAChannels; ch0++) + { + unsigned char command[2]; + command[0] = 36; + command[1] = (unsigned char) ch0; // 0-based wire channel + ret = WriteToComPortH((const unsigned char*) command, 2); + if (ret != DEVICE_OK) return ret; + + MM::MMTime startTime = GetCurrentMMTime(); + const unsigned int nrBytes = 14; + unsigned long bytesRead = 0; + unsigned char answer[nrBytes] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + while ((bytesRead < nrBytes) && ((GetCurrentMMTime() - startTime).getMsec() < 250)) { + unsigned long br; + ret = ReadFromComPortH(answer + bytesRead, nrBytes - bytesRead, br); + if (ret != DEVICE_OK) return ret; + bytesRead += br; + } + if (answer[0] != 36 || answer[1] != ch0) + return ERR_COMMUNICATION; + + int32_t minMicroV = (int32_t) (((uint32_t) answer[2] << 24) | ((uint32_t) answer[3] << 16) | + ((uint32_t) answer[4] << 8) | (uint32_t) answer[5]); + int32_t maxMicroV = (int32_t) (((uint32_t) answer[6] << 24) | ((uint32_t) answer[7] << 16) | + ((uint32_t) answer[8] << 8) | (uint32_t) answer[9]); + uint32_t numSteps = ((uint32_t) answer[10] << 24) | ((uint32_t) answer[11] << 16) | + ((uint32_t) answer[12] << 8) | (uint32_t) answer[13]; + unsigned idx = ch0 + 1; // store 1-based + if (minMicroV == 0 && maxMicroV == 0) { + daRangeKnown_[idx] = false; + } else { + daMinV_[idx] = minMicroV / 1000000.0; + daMaxV_[idx] = maxMicroV / 1000000.0; + daNumSteps_[idx] = numSteps; + daRangeKnown_[idx] = true; + } + } + + unsigned char seqLenCommand[1] = { 37 }; + ret = WriteToComPortH(seqLenCommand, 1); + if (ret != DEVICE_OK) return ret; + + MM::MMTime seqLenStartTime = GetCurrentMMTime(); + const unsigned int seqLenNrBytes = 3; + unsigned long seqLenBytesRead = 0; + unsigned char seqLenAnswer[seqLenNrBytes] = { 0, 0, 0 }; + while ((seqLenBytesRead < seqLenNrBytes) && ((GetCurrentMMTime() - seqLenStartTime).getMsec() < 250)) { + unsigned long br; + ret = ReadFromComPortH(seqLenAnswer + seqLenBytesRead, seqLenNrBytes - seqLenBytesRead, br); + if (ret != DEVICE_OK) return ret; + seqLenBytesRead += br; + } + if (seqLenAnswer[0] != 37) + return ERR_COMMUNICATION; + + maxDASeqLength_ = (((unsigned int) seqLenAnswer[1]) << 8) | seqLenAnswer[2]; + } + pAct = new CPropertyAction(this, &CArduinoHub::OnExtendedVersion); std::ostringstream seversion; seversion << extendedVersion_; @@ -431,6 +497,18 @@ int CArduinoHub::Initialize() } +bool CArduinoHub::GetDAVoltageRange(unsigned channel, double& minV, double& maxV, unsigned long& numSteps) +{ + if (version_ < 6) return false; + if (channel < 1 || channel >= daRangeKnown_.size()) return false; + if (!daRangeKnown_[channel]) return false; + minV = daMinV_[channel]; + maxV = daMaxV_[channel]; + numSteps = daNumSteps_[channel]; + return true; +} + + int CArduinoHub::DetectInstalledDevices() { if (MM::CanCommunicate == DetectDevice()) @@ -1222,9 +1300,16 @@ CArduinoDA::CArduinoDA(int channel) : maxV_(5.0), volts_(0.0), gatedVolts_(0.0), - channel_(channel), + channel_(channel), maxChannel_(2), - gateOpen_(true) + gateOpen_(true), + physMinV_(0.0), + physMaxV_(5.0), + hasPhysRange_(false), + numSteps_(4095), + sequenceOn_(true), + daSeqSupported_(false), + daMaxSeqLength_(0) { InitializeDefaultErrorMessages(); @@ -1234,16 +1319,12 @@ CArduinoDA::CArduinoDA(int channel) : SetErrorText(ERR_WRITE_FAILED, "Failed to write data to the device"); SetErrorText(ERR_CLOSE_FAILED, "Failed closing the device"); SetErrorText(ERR_NO_PORT_SET, "Hub Device not found. The Arduino Hub device is needed to create this device"); - - /* Channel property is not needed - CPropertyAction* pAct = new CPropertyAction(this, &CArduinoDA::OnChannel); - CreateProperty("Channel", channel_ == 1 ? "1" : "2", MM::Integer, false, pAct, true); - for (int i=1; i<= 2; i++){ - std::ostringstream os; - os << i; - AddAllowedValue("Channel", os.str().c_str()); - } - */ + SetErrorText(ERR_DA_CHANNEL_NOT_AVAILABLE, "This DA channel is not available on the connected firmware." + " Either no DA chip is compiled into the firmware (all #define TLV5618/TLV56x8/MCP4728 lines are" + " commented out), or this channel number exceeds the firmware's reported DA channel count."); + SetErrorText(ERR_DA_SEQUENCE_UPLOAD_FAILED, "The firmware did not store the full DA voltage sequence that was" + " sent (it reported storing fewer events than were uploaded). This usually means the DA channel is not" + " actually available on the connected firmware."); CPropertyAction* pAct = new CPropertyAction(this, &CArduinoDA::OnMaxVolt); CreateProperty("MaxVolt", "5.0", MM::Float, false, pAct, true); @@ -1288,6 +1369,32 @@ int CArduinoDA::Initialize() maxChannel_ = hub->GetNumDAChannels(); + if (channel_ > maxChannel_) + { + LogMessage("This DA channel exceeds the number of DA channels reported by the connected" + " firmware - is a DA chip compiled into the firmware, and does it match the attached hardware?", false); + return ERR_DA_CHANNEL_NOT_AVAILABLE; + } + + if (hub->GetDAVoltageRange(channel_, physMinV_, physMaxV_, numSteps_)) + { + hasPhysRange_ = true; + minV_ = physMinV_; + if (maxV_ > physMaxV_) + maxV_ = physMaxV_; + } + + daSeqSupported_ = (hub->GetControllerVersionCached() >= 6); + if (daSeqSupported_) + { + daMaxSeqLength_ = (long) hub->GetMaxDASequenceLength(); + CPropertyAction* pSeqAct = new CPropertyAction(this, &CArduinoDA::OnSequence); + int seqRet = CreateProperty("Sequence", g_On, MM::String, false, pSeqAct); + if (seqRet != DEVICE_OK) + return seqRet; + AddAllowedValue("Sequence", g_On); + AddAllowedValue("Sequence", g_Off); + } // Refuse to initialize a DA device for a channel the firmware does not // expose (e.g. DAC3-8 on an original Arduino that reports 2 channels). if (channel_ < 1 || (unsigned) channel_ > maxChannel_) @@ -1360,11 +1467,15 @@ int CArduinoDA::WriteToPort(unsigned long value) int CArduinoDA::WriteSignal(double volts) { + double refMin = hasPhysRange_ ? physMinV_ : minV_; + double refMax = hasPhysRange_ ? physMaxV_ : maxV_; + double span = refMax - refMin; + long value = (span > 0.0) ? (long) ((volts - refMin) / span * numSteps_) : 0; + if (value < 0) value = 0; + if (value > (long) numSteps_) value = (long) numSteps_; if (maxV_ <= 0.0) return DEVICE_INVALID_PROPERTY_VALUE; - long value = (long) ( (volts - minV_) / maxV_ * 4095); - std::ostringstream os; os << "Volts: " << volts << " Max Voltage: " << maxV_ << " digital value: " << value; LogMessage(os.str().c_str(), true); @@ -1372,6 +1483,147 @@ int CArduinoDA::WriteSignal(double volts) return WriteToPort(value); } +int CArduinoDA::IsDASequenceable(bool& isSequenceable) const +{ + isSequenceable = daSeqSupported_ && sequenceOn_; + return DEVICE_OK; +} + +int CArduinoDA::GetDASequenceMaxLength(long& nrEvents) const +{ + nrEvents = daMaxSeqLength_; + return DEVICE_OK; +} + +int CArduinoDA::ClearDASequence() +{ + sequence_.clear(); + return DEVICE_OK; +} + +int CArduinoDA::AddToDASequence(double voltage) +{ + if ((long) sequence_.size() >= daMaxSeqLength_) + return DEVICE_SEQUENCE_TOO_LARGE; + sequence_.push_back(voltage); + return DEVICE_OK; +} + +int CArduinoDA::SendDASequence() +{ + CArduinoHub* hub = static_cast(GetParentHub()); + if (!hub || !hub->IsPortAvailable()) + return ERR_NO_PORT_SET; + + double refMin = hasPhysRange_ ? physMinV_ : minV_; + double refMax = hasPhysRange_ ? physMaxV_ : maxV_; + double span = refMax - refMin; + + std::vector payload; + payload.reserve(sequence_.size() * 2); + for (size_t i = 0; i < sequence_.size(); i++) + { + long value = (span > 0.0) ? (long) ((sequence_[i] - refMin) / span * numSteps_) : 0; + if (value < 0) value = 0; + if (value > (long) numSteps_) value = (long) numSteps_; + payload.push_back((unsigned char) (value / 256L)); + payload.push_back((unsigned char) (value & 255)); + } + + const std::lock_guard lock(hub->GetLock()); + hub->PurgeComPortH(); + + unsigned char header[4]; + header[0] = 38; + header[1] = (unsigned char) (channel_ - 1); // 0-based wire channel + unsigned int count = (unsigned int) sequence_.size(); + header[2] = (unsigned char) ((count >> 8) & 0xFF); + header[3] = (unsigned char) (count & 0xFF); + int ret = hub->WriteToComPortH(header, 4); + if (ret != DEVICE_OK) return ret; + + if (!payload.empty()) + { + ret = hub->WriteToComPortH(payload.data(), (unsigned) payload.size()); + if (ret != DEVICE_OK) return ret; + } + + MM::MMTime startTime = GetCurrentMMTime(); + const unsigned int nrBytes = 4; + unsigned long bytesRead = 0; + unsigned char answer[nrBytes] = {0, 0, 0, 0}; + while ((bytesRead < nrBytes) && ((GetCurrentMMTime() - startTime).getMsec() < 2500)) { + unsigned long br; + ret = hub->ReadFromComPortH(answer + bytesRead, nrBytes - bytesRead, br); + if (ret != DEVICE_OK) return ret; + bytesRead += br; + } + if (answer[0] != 38 || answer[1] != (unsigned char) (channel_ - 1)) + return ERR_COMMUNICATION; + + unsigned int storedCount = (((unsigned int) answer[2]) << 8) | answer[3]; + if (storedCount != count) + return ERR_DA_SEQUENCE_UPLOAD_FAILED; // firmware rejected, truncated, or timed out the upload + + return DEVICE_OK; +} + +int CArduinoDA::StartDASequence() +{ + CArduinoHub* hub = static_cast(GetParentHub()); + if (!hub || !hub->IsPortAvailable()) + return ERR_NO_PORT_SET; + + const std::lock_guard lock(hub->GetLock()); + hub->PurgeComPortH(); + unsigned char command[1] = { 39 }; + int ret = hub->WriteToComPortH(command, 1); + if (ret != DEVICE_OK) return ret; + + MM::MMTime startTime = GetCurrentMMTime(); + unsigned long bytesRead = 0; + unsigned char answer[1] = {0}; + while ((bytesRead < 1) && ((GetCurrentMMTime() - startTime).getMsec() < 250)) { + unsigned long br; + ret = hub->ReadFromComPortH(answer, 1, br); + if (ret != DEVICE_OK) return ret; + bytesRead += br; + } + if (answer[0] != 39) + return ERR_COMMUNICATION; + return DEVICE_OK; +} + +int CArduinoDA::StopDASequence() +{ + CArduinoHub* hub = static_cast(GetParentHub()); + if (!hub || !hub->IsPortAvailable()) + return ERR_NO_PORT_SET; + + const std::lock_guard lock(hub->GetLock()); + unsigned char command[1] = { 43 }; + int ret = hub->WriteToComPortH(command, 1); + if (ret != DEVICE_OK) return ret; + + MM::MMTime startTime = GetCurrentMMTime(); + const unsigned int nrBytes = 2; + unsigned long bytesRead = 0; + unsigned char answer[nrBytes] = {0, 0}; + while ((bytesRead < nrBytes) && ((GetCurrentMMTime() - startTime).getMsec() < 250)) { + unsigned long br; + ret = hub->ReadFromComPortH(answer + bytesRead, nrBytes - bytesRead, br); + if (ret != DEVICE_OK) return ret; + bytesRead += br; + } + if (answer[0] != 43) + return ERR_COMMUNICATION; + + std::ostringstream os; + os << "DA sequence had " << (int) answer[1] << " transitions"; + LogMessage(os.str().c_str(), false); + return DEVICE_OK; +} + int CArduinoDA::SetSignal(double volts) { volts_ = volts; @@ -1414,6 +1666,39 @@ int CArduinoDA::OnVolts(MM::PropertyBase* pProp, MM::ActionType eAct) pProp->Get(volts); return SetSignal(volts); } + else if (eAct == MM::IsSequenceable) + { + if (daSeqSupported_ && sequenceOn_) + pProp->SetSequenceable(daMaxSeqLength_); + else + pProp->SetSequenceable(0); + } + else if (eAct == MM::AfterLoadSequence) + { + std::vector seq = pProp->GetSequence(); + if ((long) seq.size() > daMaxSeqLength_) + return DEVICE_SEQUENCE_TOO_LARGE; + + ClearDASequence(); + for (unsigned int i = 0; i < seq.size(); i++) + { + std::istringstream is(seq[i]); + double v; + is >> v; + int ret = AddToDASequence(v); + if (ret != DEVICE_OK) + return ret; + } + return SendDASequence(); + } + else if (eAct == MM::StartSequence) + { + return StartDASequence(); + } + else if (eAct == MM::StopSequence) + { + return StopDASequence(); + } return DEVICE_OK; } @@ -1437,8 +1722,10 @@ int CArduinoDA::OnMaxVolt(MM::PropertyBase* pProp, MM::ActionType eAct) return DEVICE_INVALID_PROPERTY_VALUE; } maxV_ = maxV; + if (hasPhysRange_ && maxV_ > physMaxV_) + maxV_ = physMaxV_; if (HasProperty("Volts")) - SetPropertyLimits("Volts", 0.0, maxV_); + SetPropertyLimits("Volts", minV_, maxV_); } return DEVICE_OK; @@ -1460,6 +1747,21 @@ int CArduinoDA::OnChannel(MM::PropertyBase* pProp, MM::ActionType eAct) return DEVICE_OK; } +int CArduinoDA::OnSequence(MM::PropertyBase* pProp, MM::ActionType eAct) +{ + if (eAct == MM::BeforeGet) + { + pProp->Set(sequenceOn_ ? g_On : g_Off); + } + else if (eAct == MM::AfterSet) + { + std::string state; + pProp->Get(state); + sequenceOn_ = (state == g_On); + } + return DEVICE_OK; +} + /////////////////////////////////////////////////////////////////////////////// // CArduinoShutter implementation diff --git a/DeviceAdapters/Arduino/Arduino.h b/DeviceAdapters/Arduino/Arduino.h index 44ecf1fed..ba4d43ee6 100644 --- a/DeviceAdapters/Arduino/Arduino.h +++ b/DeviceAdapters/Arduino/Arduino.h @@ -21,6 +21,7 @@ #include #include #include +#include ////////////////////////////////////////////////////////////////////////////// // Error codes @@ -34,6 +35,8 @@ #define ERR_COMMUNICATION 107 #define ERR_NO_PORT_SET 108 #define ERR_VERSION_MISMATCH 109 +#define ERR_DA_CHANNEL_NOT_AVAILABLE 110 +#define ERR_DA_SEQUENCE_UPLOAD_FAILED 111 class ArduinoInputMonitorThread; class CArduinoMagnifier; @@ -61,8 +64,10 @@ class CArduinoHub : public HubBase unsigned int GetMaxNumPatterns() { return maxNumPatterns_; }; + unsigned int GetMaxDASequenceLength() { return maxDASeqLength_; } unsigned int GetNumDAChannels() { return numDAChannels_; } unsigned int GetNumDigitalPins() { return numDigitalPins_; } + bool GetDAVoltageRange(unsigned channel /*1-based*/, double& minV, double& maxV, unsigned long& numSteps); // custom interface for child devices bool IsPortAvailable() {return portAvailable_;} @@ -95,8 +100,14 @@ class CArduinoHub : public HubBase int version_; long extendedVersion_; unsigned int maxNumPatterns_; + unsigned int maxDASeqLength_; unsigned int numDAChannels_; unsigned int numDigitalPins_; + // indexed 1-based (index 0 unused), sized g_MaxDAChannels+1, matches channel_ numbering + std::vector daMinV_; + std::vector daMaxV_; + std::vector daRangeKnown_; + std::vector daNumSteps_; CArduinoMagnifier* magnifier_; std::mutex mutex_; unsigned switchState_; @@ -207,13 +218,20 @@ class CArduinoDA : public CSignalIOBase int GetSignal(double& volts) {volts_ = volts; return DEVICE_UNSUPPORTED_COMMAND;} int GetLimits(double& minVolts, double& maxVolts) {minVolts = minV_; maxVolts = maxV_; return DEVICE_OK;} - int IsDASequenceable(bool& isSequenceable) const {isSequenceable = false; return DEVICE_OK;} + int IsDASequenceable(bool& isSequenceable) const; + int GetDASequenceMaxLength(long& nrEvents) const; + int StartDASequence(); + int StopDASequence(); + int ClearDASequence(); + int AddToDASequence(double voltage); + int SendDASequence(); // action interface // ---------------- int OnVolts(MM::PropertyBase* pProp, MM::ActionType eAct); int OnMaxVolt(MM::PropertyBase* pProp, MM::ActionType eAct); int OnChannel(MM::PropertyBase* pProp, MM::ActionType eAct); + int OnSequence(MM::PropertyBase* pProp, MM::ActionType eAct); private: int WriteToPort(unsigned long lnValue); @@ -229,6 +247,14 @@ class CArduinoDA : public CSignalIOBase unsigned maxChannel_; bool gateOpen_; std::string name_; + double physMinV_; + double physMaxV_; + bool hasPhysRange_; + unsigned long numSteps_; + bool sequenceOn_; // user opt-in, "Sequence" property (On/Off) + bool daSeqSupported_; // true if firmware version >= 6 + long daMaxSeqLength_; // per-channel max sequence length (from firmware command 37) + std::vector sequence_; // pending sequence being built via Clear/AddToDASequence }; class CArduinoInput : public CGenericBase