diff --git a/camerad/archon_controller.cpp b/camerad/archon_controller.cpp index 98adb23..9f26979 100644 --- a/camerad/archon_controller.cpp +++ b/camerad/archon_controller.cpp @@ -394,136 +394,217 @@ namespace Camera { * @throws std::runtime_error * */ + std::vector ArchonController::bias_banks(int modtype) { + switch (modtype) { + case MODTYPE_LVBIAS: + case MODTYPE_LVXBIAS: + return {{"LVLC", 1, 24, -14.0, +14.0, false, false}, + {"LVHC", 25, 6, -14.0, +14.0, true, true}}; + case MODTYPE_HVBIAS: + case MODTYPE_HVXBIAS: + return {{"HVLC", 1, 24, 0.0, +31.0, false, false}, + {"HVHC", 25, 6, 0.0, +31.0, true, true}}; + case MODTYPE_XVBIAS: + return {{"XVP", 1, XVBIAS_CHANS_PER_POLARITY, + 0.0, XVBIAS_VOLTS_MAGNITUDE, true, false}, + {"XVN", 1+XVBIAS_CHANS_PER_POLARITY, XVBIAS_CHANS_PER_POLARITY, + -XVBIAS_VOLTS_MAGNITUDE, 0.0, true, false}}; + default: + return {}; + } + } + + ArchonController::bias_config_t ArchonController::get_bias_config(int mod, int chan) const { std::ostringstream oss; - // Check that the module number is valid - if ( (mod < 0) || (mod > MAXNMODS) ) { - oss << "module " << mod << ": outside range {0:" << MAXNMODS << "}"; + if ( (mod < 1) || (mod > MAXNMODS) ) { + oss << "module " << mod << ": outside range {1:" << MAXNMODS << "}"; throw std::runtime_error(oss.str()); } - // Check that the channel number is valid - if ( (chan < 1) || (chan > 30) ) { - oss << "bias channel " << mod << ": outside range {1:30}"; + if (this->modtype[mod-1] == MODTYPE_NONE) { + oss << "module " << mod << " not installed"; throw std::runtime_error(oss.str()); } - bias_config_t info; - std::ostringstream biasconfig; - - // Use the module type to get LV or HV Bias - // and start building the bias configuration string. - float vmin, vmax; - switch ( this->modtype[ mod-1 ] ) { - case MODTYPE_NONE: - oss << "module " << mod << " not installed"; - throw std::runtime_error(oss.str()); - case MODTYPE_LVBIAS: - case MODTYPE_LVXBIAS: - biasconfig << "MOD" << mod << "/LV"; - info.vmin = -14.0; - info.vmax = +14.0; - break; - case MODTYPE_HVBIAS: - case MODTYPE_HVXBIAS: - biasconfig << "MOD" << mod << "/HV"; - info.vmin = 0.0; - info.vmax = +31.0; - break; - default: - oss << "module " << mod << " not a bias board"; - throw std::runtime_error(oss.str()); + const auto banks = bias_banks(this->modtype[mod-1]); + if (banks.empty()) { + oss << "module " << mod << " not a bias board"; + throw std::runtime_error(oss.str()); } - // append the channel to the bias configuration string - if (chan < 25) { - biasconfig << "LC_V" << chan; - } - else { - biasconfig << "HC_V" << (chan-24); + for (const auto &bank : banks) { + if (chan < bank.first_chan || chan >= bank.first_chan + bank.count) continue; + bias_config_t info; + info.bank = bank; + info.index = chan - bank.first_chan + 1; + info.vmin = bank.vmin; + info.vmax = bank.vmax; + return info; } - info.key = biasconfig.str(); - return info; + oss << "bias channel " << chan << ": outside range {1:" + << banks.back().first_chan + banks.back().count - 1 << "} for module " << mod; + throw std::runtime_error(oss.str()); } - /***** Camera::ArchonController::get_bias_config ****************************/ - /***** Camera::ArchonController::make_applymod_command **********************/ + std::string ArchonController::bias_key(int mod, const bias_config_t &info, BiasField field) { + std::ostringstream key; + key << "MOD" << mod << "/" << info.bank.prefix << "_"; + switch (field) { + case BiasField::VOLTS: key << "V"; break; + case BiasField::ORDER: key << "ORDER"; break; + case BiasField::ENABLE: key << "ENABLE"; break; + case BiasField::CURRENT_LIMIT: key << "IL"; break; + case BiasField::LABEL: key << "LABEL"; break; + } + key << info.index; + return key.str(); + } + + + /***** Camera::ArchonController::read_bias_channel **************************/ /** - * @brief creates an APPLYMODx string - * @returns std::string - * + * @brief report every attribute one bias channel carries + * @details Values come from configuration memory, so they are what was + * asked of the board, not a measurement of what it is doing. */ - std::string ArchonController::make_applymod_command(int mod) const { + std::string ArchonController::read_bias_channel(int mod, int chan) const { + const auto info = get_bias_config(mod, chan); + + auto value_of = [&](BiasField field) { + const auto it = this->configmap.find(bias_key(mod, info, field)); + return it == this->configmap.end() ? std::string() : it->second.value; + }; + + const std::string label = value_of(BiasField::LABEL); + std::ostringstream oss; - oss << "APPLYMOD" - << std::setfill('0') - << std::setw(2) - << std::hex - << (mod-1); + oss << "MOD" << mod << " chan " << chan << " " << info.bank.prefix + << " V=" << value_of(BiasField::VOLTS) + << " ORDER=" << value_of(BiasField::ORDER); + if (info.bank.has_enable) oss << " ENABLE=" << value_of(BiasField::ENABLE); + if (info.bank.has_current_limit) oss << " IL=" << value_of(BiasField::CURRENT_LIMIT); + oss << " LABEL=\"" << label << "\""; return oss.str(); } - /***** Camera::ArchonController::make_applymod_command **********************/ + /***** Camera::ArchonController::read_bias_channel **************************/ - /***** Camera::ArchonController::bias ***************************************/ + /***** Camera::ArchonController::set_bias_fields ****************************/ /** - * @brief parse the configuration file for controller-related parameters - * @details The config file has already been read into the Config class. - * @throws std::runtime_error - * + * @brief write one or more attributes of a bias channel, then apply once + * @details Batched because every APPLYMOD is a round trip, so setting + * three attributes separately would apply the module three times. */ - void ArchonController::bias(const int &mod, const int &chan, float &volts, const bool &should_write) { - const std::string function("Camera::ArchonController::bias"); - std::ostringstream oss; - std::ostringstream biasconfig; + long ArchonController::set_bias_fields(int mod, int chan, + const std::vector> &fields, + std::string &retstring) { + const std::string function("Camera::ArchonController::set_bias_fields"); - // nothing to do if no connection open to controller if (!this->archon.isconnected()) { throw std::runtime_error("connection not open to controller"); } - // creating the bias configuration key also validates mod and chan - auto info = get_bias_config(mod, chan); + const auto info = get_bias_config(mod, chan); + std::ostringstream oss; - // write the bias configuration line if needed - if (should_write) { - bool changed=false; - // check requested voltage is within range - if ( (volts < info.vmin) || (volts > info.vmax) ) { - oss << volts << " outside range {" << info.vmin << ":" << info.vmax << "}"; + for (const auto &[field, value] : fields) { + if (field == BiasField::ENABLE && !info.bank.has_enable) { + oss << "bank " << info.bank.prefix << " has no enable"; throw std::runtime_error(oss.str()); } - - // write the configuration line to update the bias voltage - std::string val = std::to_string(volts); - this->write_config_key(info.key.c_str(), val.c_str(), changed); - - // send the APPLYMODx command - long error = this->send_cmd(make_applymod_command(mod)); - - if (error != NO_ERROR) { - oss << "writing bias configuration " << info.key << "=" << val; + if (field == BiasField::CURRENT_LIMIT && !info.bank.has_current_limit) { + oss << "bank " << info.bank.prefix << " has no current limit"; throw std::runtime_error(oss.str()); } - else if (!changed) { - oss << "bias configuration " << info.key << "=" << val << " unchanged"; - logwrite(function, oss.str()); - return; + if (field == BiasField::VOLTS) { + const float volts = std::stof(value); + if (volts < info.vmin || volts > info.vmax) { + oss << volts << " outside range {" << info.vmin << ":" << info.vmax << "}"; + throw std::runtime_error(oss.str()); + } } - else { - oss << "updated bias configuration " << info.key << "=" << val; - logwrite(function, oss.str()); - return; + if (field == BiasField::CURRENT_LIMIT) { + const int milliamps = std::stoi(value); + if (milliamps < 0 || milliamps > BIAS_CURRENT_LIMIT_MAX_MA) { + oss << milliamps << " outside range {0:" << BIAS_CURRENT_LIMIT_MAX_MA << "} mA"; + throw std::runtime_error(oss.str()); + } + } + bool changed = false; + this->write_config_key(bias_key(mod, info, field).c_str(), value.c_str(), changed); + } + + if (this->send_cmd(make_applymod_command(mod)) != NO_ERROR) { + throw std::runtime_error("applying module "+std::to_string(mod)); + } + + retstring = read_bias_channel(mod, chan); + logwrite(function, retstring); + return NO_ERROR; + } + /***** Camera::ArchonController::set_bias_fields ****************************/ + + + /***** Camera::ArchonController::list_biases ********************************/ + /** + * @brief report every configured bias channel, for one module or all + * @param[in] mod module to report, or 0 for every bias board installed + */ + long ArchonController::list_biases(int mod, std::string &retstring) const { + std::ostringstream oss; + + for (int slot = 1; slot <= MAXNMODS; ++slot) { + if (mod != 0 && slot != mod) continue; + if (bias_banks(this->modtype[slot-1]).empty()) continue; + + for (const auto &bank : bias_banks(this->modtype[slot-1])) { + for (int i = 0; i < bank.count; ++i) { + const int chan = bank.first_chan + i; + // Skip channels the ACF never defined; the board has them, the + // configuration does not, so there is nothing to report + bias_config_t probe; + probe.bank = bank; + probe.index = i + 1; + if (this->configmap.find(bias_key(slot, probe, BiasField::VOLTS)) == + this->configmap.end()) continue; + oss << read_bias_channel(slot, chan) << "\n"; + } } } - // read the configuration - this->get_configmap_value(info.key, volts); + retstring = oss.str(); + if (retstring.empty()) { + retstring = (mod == 0) ? "no bias boards installed" + : "module "+std::to_string(mod)+" has no configured bias channels"; + } + return NO_ERROR; + } + /***** Camera::ArchonController::list_biases ********************************/ + /***** Camera::ArchonController::get_bias_config ****************************/ + + + /***** Camera::ArchonController::make_applymod_command **********************/ + /** + * @brief creates an APPLYMODx string + * @returns std::string + * + */ + std::string ArchonController::make_applymod_command(int mod) const { + std::ostringstream oss; + oss << "APPLYMOD" + << std::setfill('0') + << std::setw(2) + << std::hex + << (mod-1); + return oss.str(); } - /***** Camera::ArchonController::bias ***************************************/ + /***** Camera::ArchonController::make_applymod_command **********************/ + + /***** Camera::ArchonController::initiate_exposure **************************/ diff --git a/camerad/archon_controller.h b/camerad/archon_controller.h index 550385d..99c83c6 100644 --- a/camerad/archon_controller.h +++ b/camerad/archon_controller.h @@ -47,6 +47,13 @@ constexpr int MODTYPE_ADLN = 15; constexpr int MODTYPE_UNKNOWN = 16; constexpr int MODTYPE_ADM = 17; +// An XVBias module carries this many positive channels, then as many negative +constexpr int XVBIAS_CHANS_PER_POLARITY = 4; +constexpr float XVBIAS_VOLTS_MAGNITUDE = 95.0f; + +// Highest current limit a high-current bias channel accepts +constexpr int BIAS_CURRENT_LIMIT_MAX_MA = 250; + // AD and ADM modules are restricted to slots 5-8, the range RAWSEL addresses constexpr int AD_SLOT_FIRST = 5; constexpr int AD_SLOT_LAST = 8; @@ -170,13 +177,40 @@ namespace Camera { * @var struct bias_config_t * @details structure of bias configuration info */ + /** + * @var struct bias_bank_t + * @details one run of bias channels sharing a key prefix. The banks of a + * board differ in which attributes they carry, so this says what + * a channel supports rather than the caller assuming. + */ + struct bias_bank_t { + const char* prefix{nullptr}; //!< HVLC, HVHC, XVP, XVN, LVLC, LVHC + int first_chan{0}; //!< channel this bank starts at + int count{0}; + float vmin{0.0}; + float vmax{0.0}; + bool has_enable{false}; + bool has_current_limit{false}; + }; + struct bias_config_t { - std::string key; - float vmin; - float vmax; + float vmin{0.0}; + float vmax{0.0}; + bias_bank_t bank; + int index{0}; //!< 1-based channel number within the bank }; + /// an attribute of one bias channel, each a separate configuration key + enum class BiasField { VOLTS, ORDER, ENABLE, CURRENT_LIMIT, LABEL }; + + static std::vector bias_banks(int modtype); + static std::string bias_key(int mod, const bias_config_t &info, BiasField field); bias_config_t get_bias_config(int mod, int chan) const; + std::string read_bias_channel(int mod, int chan) const; + long set_bias_fields(int mod, int chan, + const std::vector> &fields, + std::string &retstring); + long list_biases(int mod, std::string &retstring) const; std::string make_applymod_command(int mod) const; /** @@ -342,7 +376,6 @@ namespace Camera { std::string abort_param; //!< parameter name to abort when set =1 (optional) void connect(); - void bias(const int &mod, const int &chan, float &volts, const bool &should_write); long initiate_exposure(const int &nexp); long get_frame_status(); template T get_parameter(const std::string ¶meter); diff --git a/camerad/archon_interface.cpp b/camerad/archon_interface.cpp index 62f70b5..ea4d818 100644 --- a/camerad/archon_interface.cpp +++ b/camerad/archon_interface.cpp @@ -36,6 +36,17 @@ namespace Camera { throw std::invalid_argument("unrecognized unit \""+unit+"\", expected s or ms"); } + + ArchonController::BiasField bias_field_from(const std::string &name) { + std::string mutable_name = name; + const std::string upper = to_uppercase(mutable_name); + if (upper == "V") return ArchonController::BiasField::VOLTS; + if (upper == "ORDER") return ArchonController::BiasField::ORDER; + if (upper == "ENABLE") return ArchonController::BiasField::ENABLE; + if (upper == "IL") return ArchonController::BiasField::CURRENT_LIMIT; + if (upper == "LABEL") return ArchonController::BiasField::LABEL; + throw std::invalid_argument("unrecognized field \""+name+"\", expected V ORDER ENABLE IL or LABEL"); + } } /***** Camera::ArchonInterface::ArchonInterface *****************************/ @@ -226,8 +237,11 @@ namespace Camera { // Help if (args=="?" || args=="help") { retstring = CAMERAD_BIAS; - retstring.append( " [ ]\n" ); - retstring.append( " set or optionally get a bias voltage\n" ); + retstring.append( " [ list [] | [ | ... ] ]\n" ); + retstring.append( " list [] report every configured bias channel\n" ); + retstring.append( " report one channel's attributes\n" ); + retstring.append( " set the voltage\n" ); + retstring.append( " Fields: V ORDER ENABLE IL LABEL, set together in one apply\n" ); return HELP; } @@ -235,28 +249,40 @@ namespace Camera { Tokenize(args, tokens, " "); try { - int mod, chan; - float volts; - size_t ntok = tokens.size(); - bool should_write=false; + if (tokens.empty()) throw std::runtime_error("expected or list"); - if (ntok != 2 && ntok != 3) { - throw std::runtime_error("expected [ ]"); + if (tokens.at(0) == "list") { + const int mod = (tokens.size() > 1) ? std::stoi(tokens.at(1)) : 0; + error = this->controller->list_biases(mod, retstring); + logwrite(function, retstring); + return error; } - mod = std::stoi(tokens.at(0)); - chan = std::stoi(tokens.at(1)); + if (tokens.size() < 2) throw std::runtime_error("expected "); + + const int mod = std::stoi(tokens.at(0)); + const int chan = std::stoi(tokens.at(1)); - if (ntok==3) { - volts = std::stof(tokens.at(2)); - should_write = true; + if (tokens.size() == 2) { + retstring = this->controller->read_bias_channel(mod, chan); + logwrite(function, retstring); + return NO_ERROR; } - this->controller->bias(mod, chan, volts, should_write); - std::ostringstream oss; - oss << std::fixed << std::setprecision(3) << volts; - retstring=oss.str(); - error=NO_ERROR; + std::vector> fields; + + // A lone value keeps the original "bias " form + if (tokens.size() == 3) { + fields.emplace_back(ArchonController::BiasField::VOLTS, tokens.at(2)); + } + else { + if ((tokens.size()-2) % 2 != 0) throw std::runtime_error("expected pairs"); + for (size_t i=2; i < tokens.size(); i+=2) { + fields.emplace_back(bias_field_from(tokens.at(i)), tokens.at(i+1)); + } + } + + error = this->controller->set_bias_fields(mod, chan, fields, retstring); } catch (const std::exception &e) { retstring=std::string(e.what()); diff --git a/config/bias_test/bias_test.cfg b/config/bias_test/bias_test.cfg new file mode 100644 index 0000000..3f32321 --- /dev/null +++ b/config/bias_test/bias_test.cfg @@ -0,0 +1,39 @@ +# camerad + emulator SHM/FITS frame_outputs testing (CI). Exercises the +# generic frame_outputs mechanism (Camera::Interface::configure_frame_outputs), +# not anything hispec_tracking_camera-specific; hispec_tracking_camera is +# just the build's INSTRUMENT since it's the only fully-working one today. + +DAEMON=no +IMDIR=/tmp +LOGPATH=/tmp +BASENAME=frameoutputstest +DIRMODE=0077 +TM_ZONE_LOG=local +TM_ZONE=UTC +TZ_ENV=PST8PDT,M3.2.0/2,M11.1.0/2 + +NBPORT=3130 +BLKPORT=3131 +EMULATOR_PORT=3132 +EMULATOR_SYSTEM=config/bias_test/bias_test.system + +ASYNCGROUP=none + +ARCHON_IP=localhost +ARCHON_PORT=3132 +DEFAULT_FIRMWARE=camerad/Instruments/hispec_tracking_camera/config/hispecatc.acf +EXPOSE_PARAM=Expose +ABORT_PARAM=abort +EXPTIME_MSEC_PARAM=exptime +READOUT_TIME=5000 +WRITE_TAPINFO_TO_FITS=no + +SHM_ENABLED=no +SHM_SEGMENT_NAME=ci_frame_outputs_shm +SHM_RING_BUFFER_SIZE=2 +SHM_DIR=/tmp/ci_milk_shm + +FITS_ENABLED=yes +FITS_OUTPUT_DIR=/tmp/bias_fits +FITS_AUTODIR=no +FITS_BASENAME=ci_frame_outputs diff --git a/config/bias_test/bias_test.system b/config/bias_test/bias_test.system new file mode 100644 index 0000000..7f9838d --- /dev/null +++ b/config/bias_test/bias_test.system @@ -0,0 +1,55 @@ +[SYSTEM] +BACKPLANE_ID=000000001B5DCF29 +BACKPLANE_REV=7 +BACKPLANE_TYPE=1 +BACKPLANE_VERSION=1.0.1183 +MOD10_ID=0000000000000001 +MOD10_REV=0 +MOD10_TYPE=9 +MOD10_VERSION=1.0.1175 +MOD11_ID=0000000000000000 +MOD11_REV=0 +MOD11_TYPE=0 +MOD11_VERSION=0.0.0 +MOD12_ID=0000000000000000 +MOD12_REV=0 +MOD12_TYPE=0 +MOD12_VERSION=0.0.0 +MOD1_ID=0000000000000000 +MOD1_REV=0 +MOD1_TYPE=0 +MOD1_VERSION=0.0.0 +MOD2_ID=0000000000000000 +MOD2_REV=0 +MOD2_TYPE=0 +MOD2_VERSION=0.0.0 +MOD3_ID=0000000000000000 +MOD3_REV=0 +MOD3_TYPE=0 +MOD3_VERSION=0.0.0 +MOD4_ID=0000000000000000 +MOD4_REV=0 +MOD4_TYPE=0 +MOD4_VERSION=0.0.0 +MOD5_ID=0000000000000000 +MOD5_REV=0 +MOD5_TYPE=0 +MOD5_VERSION=0.0.0 +MOD6_ID=0000000000000000 +MOD6_REV=0 +MOD6_TYPE=0 +MOD6_VERSION=0.0.0 +MOD7_ID=0000000000000000 +MOD7_REV=0 +MOD7_TYPE=0 +MOD7_VERSION=0.0.0 +MOD8_ID=0000000000000000 +MOD8_REV=0 +MOD8_TYPE=0 +MOD8_VERSION=0.0.0 +MOD9_ID=0000000000000000 +MOD9_REV=0 +MOD9_TYPE=0 +MOD9_VERSION=0.0.0 +MOD_PRESENT=1 +POWER_ID=000001786582 diff --git a/docs/commands/controller.md b/docs/commands/controller.md index dd4910f..c54f37d 100644 --- a/docs/commands/controller.md +++ b/docs/commands/controller.md @@ -139,6 +139,47 @@ The two strides disagree, and on a chassis with a mix of AD and ADM modules they verbatim beside the type of every slot it could refer to, leaving the reader to resolve it. ::: +## Bias voltages + +``` +bias [ list [] | [ | ... ] ] +``` + +A bias board's channels fall into banks that share a key prefix, and the banks differ in what they +carry, so a channel is addressed by a number that spans the whole board: + +| board | bank | channels | range | enable | current limit | +|---|---|---|---|---|---| +| LVBias, LVXBias | `LVLC`, `LVHC` | 1 to 24, 25 to 30 | -14 to +14 V | high current only | high current only | +| HVBias, HVXBias | `HVLC`, `HVHC` | 1 to 24, 25 to 30 | 0 to +31 V | high current only | high current only | +| XVBias | `XVP`, `XVN` | 1 to 4, 5 to 8 | 0 to +95 V, -95 to 0 V | yes | no | + +Fields are `V`, `ORDER`, `ENABLE`, `IL` and `LABEL`. Setting several in one command applies the +module once, where separate commands would apply it once each. Asking for a field the bank does not +carry is refused rather than writing a key the board ignores, and the current limit is capped at 250 +mA. + +Every read and every set reports the channel's label, so a voltage is never shown without saying +which bias it belongs to. `bias list` covers every installed bias board, or one of them, skipping +channels the loaded ACF never defined: + +``` +$ bias list 10 +MOD10 chan 1 LVLC V=2.3 ORDER=2 LABEL="Bias Gate" +MOD10 chan 2 LVLC V=3.3 ORDER=2 LABEL="Bias Power" +MOD10 chan 3 LVLC V=0.55 ORDER=2 LABEL="Diode Sub" +... +MOD10 chan 25 LVHC V=3.3 ORDER=3 ENABLE=1 IL=10 LABEL="PullUp" +MOD10 chan 26 LVHC V=0.0 ORDER=1 ENABLE=0 IL=50 LABEL="Misc 1" +``` + +The high-current channels carry the two extra fields, the low-current ones do not. + +:::{note} +Values come from configuration memory, so they report what the board was asked for rather than a +measurement of what it is doing. +::: + ## Heater and sensor For Archon **Heater** and **HeaterX** modules. Both require firmware to be loaded and a diff --git a/docs/data/commands.yaml b/docs/data/commands.yaml index 256e44d..4d17a5f 100644 --- a/docs/data/commands.yaml +++ b/docs/data/commands.yaml @@ -26,7 +26,7 @@ basename: controller: any bias: - summary: Set or get a bias voltage + summary: Read or set the attributes of a bias channel, or list them all controller: any bin: diff --git a/python/camera_interface_module.cpp b/python/camera_interface_module.cpp index 22d6406..94d9788 100644 --- a/python/camera_interface_module.cpp +++ b/python/camera_interface_module.cpp @@ -231,7 +231,7 @@ PYBIND11_MODULE(CAMERAD_MODULE_NAME, module) { return invoke((*self).bias(args, retstring), "bias", retstring); }, py::arg("args") = "", py::call_guard(), - "Query a bias voltage, or set it") + "Read or set the attributes of a bias channel, or list them all") .def("bin", [](CameraSession &self, const std::string &args) {