From 3f3afaeaee2fadb597f2ae522b96d24e4bf8e984 Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:59:25 -0700 Subject: [PATCH 1/6] Wire frame outputs on Interface base class frame_outputs was never populated, so dispatch_frame was a no-op. Add Interface::configure_frame_outputs(), called from camerad startup. --- camerad/camera_interface.cpp | 16 ++++++++++++++++ camerad/camera_interface.h | 1 + camerad/camerad.cpp | 1 + 3 files changed, 18 insertions(+) diff --git a/camerad/camera_interface.cpp b/camerad/camera_interface.cpp index ddc60ac..c112583 100644 --- a/camerad/camera_interface.cpp +++ b/camerad/camera_interface.cpp @@ -1,4 +1,5 @@ #include "camera_interface.h" +#include "frame_output_factory.h" namespace Camera { @@ -8,6 +9,21 @@ namespace Camera { this->server=s; } + /***** Camera::Interface::configure_frame_outputs ***************************/ + /** + * @brief build the in-band frame output sinks (FITS, shared memory) + * @details dispatch_frame fans every retrieved frame out to these + * + */ + void Interface::configure_frame_outputs() { + const std::string function("Camera::Interface::configure_frame_outputs"); + FrameOutputsConfig outcfg; + apply_config_overrides(outcfg, this->configfile); + this->frame_outputs = make_frame_outputs(outcfg); + logwrite(function, "configured "+std::to_string(this->frame_outputs.size())+" frame output(s)"); + } + /***** Camera::Interface::configure_frame_outputs ***************************/ + void Interface::func_shared() { std::string function("Camera::Interface::func_shared"); logwrite(function, "common implementation function"); diff --git a/camerad/camera_interface.h b/camerad/camera_interface.h index 629d1aa..c46a3b8 100644 --- a/camerad/camera_interface.h +++ b/camerad/camera_interface.h @@ -75,6 +75,7 @@ namespace Camera { // void set_server(Camera::Server* s); void func_shared(); + void configure_frame_outputs(); void disconnect_controller(); bool is_exposuremode_set() { return ( this->exposuremode && !this->exposuremode->get_type().empty() ); } diff --git a/camerad/camerad.cpp b/camerad/camerad.cpp index 02a173c..49d9746 100644 --- a/camerad/camerad.cpp +++ b/camerad/camerad.cpp @@ -48,6 +48,7 @@ int main( int argc, char** argv ) { camerad.interface->configure_controller(); camerad.interface->configure_interface(); camerad.interface->configure_instrument(); + camerad.interface->configure_frame_outputs(); } catch (const std::exception &e) { logwrite(function, "ERROR configuring system: "+std::string(e.what())); From 177704be88aea981e86d8e4c169cb554e48b6288 Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:59:25 -0700 Subject: [PATCH 2/6] Add Archon RAW (pre-CDS) retrieval and raw command Parse BUFnRAWOFFSET, size fetch from RAW geometry, read as 16-bit, reshape and dispatch in-band. New: raw config|set|read. Verified against emulator. --- camerad/archon_controller.cpp | 165 ++++++++++++++++++++++++++++++++-- camerad/archon_controller.h | 16 +++- camerad/archon_interface.cpp | 46 ++++++++++ camerad/archon_interface.h | 1 + camerad/camera_server.cpp | 4 + common/camerad_commands.h | 2 + 6 files changed, 224 insertions(+), 10 deletions(-) diff --git a/camerad/archon_controller.cpp b/camerad/archon_controller.cpp index bbf808d..eee3c18 100644 --- a/camerad/archon_controller.cpp +++ b/camerad/archon_controller.cpp @@ -582,8 +582,10 @@ namespace Camera { case 8: // BUFnCOMPLETE if (std::strncmp(suffix, "COMPLETE", 8)==0) this->frameinfo.bufcomplete[bufnum] = std::atoi(value_start); break; - case 9: // BUFnTIMESTAMP + case 9: // BUFnTIMESTAMP, RAWOFFSET if (std::strncmp(suffix, "TIMESTAMP", 9)==0) this->frameinfo.buftimestamp[bufnum] = std::strtoull(value_start, nullptr, 16); + else + if (std::strncmp(suffix, "RAWOFFSET", 9)==0) this->frameinfo.bufrawoffset[bufnum] = std::strtoul(value_start, nullptr, 10); break; case 11: // BUFnRETIMESTAMP, FETIMESTAMP if (std::strncmp(suffix, "RETIMESTAMP", 11)==0) this->frameinfo.bufretimestamp[bufnum] = std::strtoull(value_start, nullptr, 16); @@ -1608,8 +1610,10 @@ namespace Camera { this->get_configmap_value("FRAMEMODE", mode->geometry.framemode); this->get_configmap_value("RAWENABLE", mode->rawenable); this->get_configmap_value("RAWSEL", this->rawinfo.adchan); - this->get_configmap_value("RAWSAMPLES", this->rawinfo.rawsamples); - this->get_configmap_value("RAWENDLINE", this->rawinfo.rawlines); + this->get_configmap_value("RAWSAMPLES", this->rawinfo.samples); + this->get_configmap_value("RAWSTARTLINE", this->rawinfo.startline); + this->get_configmap_value("RAWENDLINE", this->rawinfo.endline); + this->get_configmap_value("RAWSTARTPIXEL", this->rawinfo.startpixel); // Read geometry from the mode's configmap (not the global one) since each // mode section can override LINECOUNT/PIXELCOUNT @@ -1911,11 +1915,10 @@ namespace Camera { // switch (this->frametype) { case Camera::ArchonController::FRAME_RAW: - // Archon buffer base address + // RAW data lives at bufbase + rawoffset; block count derives from the + // RAW geometry (RAWSAMPLES x rawlines x 2 bytes), never image_memory bufaddr = this->frameinfo.bufbase[index] + this->frameinfo.bufrawoffset[index]; - - // Calculate the number of blocks expected. image_memory is bytes per detector - bufblocks = (unsigned int) floor( (this->interface->camera_info.image_memory + BLOCK_LEN - 1 ) / BLOCK_LEN ); + bufblocks = (this->raw_frame_bytes() + BLOCK_LEN - 1) / BLOCK_LEN; break; case Camera::ArchonController::FRAME_IMAGE: @@ -2034,6 +2037,154 @@ namespace Camera { } + /***** Camera::ArchonController::is_raw_config_key *************************/ + /** + * @brief return true if key is one of the settable RAW config keywords + */ + bool ArchonController::is_raw_config_key(const std::string &key) { + for (const auto* raw_key : {"RAWENABLE", "RAWSEL", "RAWSTARTLINE", + "RAWENDLINE", "RAWSTARTPIXEL", "RAWSAMPLES"}) { + if (key == raw_key) return true; + } + return false; + } + /***** Camera::ArchonController::is_raw_config_key *************************/ + + + /***** Camera::ArchonController::raw_frame_bytes **************************/ + /** + * @brief size-aware byte count for one RAW capture + * @details RAW samples are always 16-bit. Count = rawlines x RAWSAMPLES, + * rawlines = RAWENDLINE - RAWSTARTLINE. RAWSTARTPIXEL only sets + * the reshape origin, so it does not change the sample count. + */ + uint32_t ArchonController::raw_frame_bytes() const { + const int lines = this->rawinfo.endline - this->rawinfo.startline; + const uint32_t rawlines = lines > 0 ? static_cast(lines) : 1u; + return rawlines * static_cast(this->rawinfo.samples) * sizeof(uint16_t); + } + /***** Camera::ArchonController::raw_frame_bytes **************************/ + + + /***** Camera::ArchonController::get_raw_config **************************/ + /** + * @brief report the current RAW configuration keywords + * @param[out] retstring space-delimited KEY=VALUE pairs + */ + long ArchonController::get_raw_config(std::string &retstring) { + std::ostringstream oss; + for (const auto* key : {"RAWENABLE", "RAWSEL", "RAWSTARTLINE", + "RAWENDLINE", "RAWSTARTPIXEL", "RAWSAMPLES"}) { + auto it = this->configmap.find(key); + oss << key << "=" << (it != this->configmap.end() ? it->second.value : "?") << " "; + } + retstring = oss.str(); + return NO_ERROR; + } + /***** Camera::ArchonController::get_raw_config **************************/ + + + /***** Camera::ArchonController::set_raw_config **************************/ + /** + * @brief set one or more RAW config keywords then APPLYALL + * @param[in] args "KEY VALUE [KEY VALUE ...]" + * @param[out] retstring resulting RAW configuration + */ + long ArchonController::set_raw_config(const std::string &args, std::string &retstring) { + const std::string function("Camera::ArchonController::set_raw_config"); + + std::vector tokens; + Tokenize(args, tokens, " "); + if (tokens.empty() || tokens.size() % 2 != 0) { + logwrite(function, "ERROR expected KEY VALUE pairs"); + retstring = "expected KEY VALUE pairs"; + return ERROR; + } + + bool changed = false; + for (size_t i = 0; i < tokens.size(); i += 2) { + std::string key = tokens[i]; + std::transform(key.begin(), key.end(), key.begin(), ::toupper); + if (!is_raw_config_key(key)) { + logwrite(function, "ERROR unknown RAW key: "+key); + retstring = "unknown RAW key: "+key; + return ERROR; + } + if (this->write_config_key(key.c_str(), tokens[i+1].c_str(), changed) != NO_ERROR) { + retstring = "failed writing "+key; + return ERROR; + } + } + + if (changed && this->send_cmd(APPLYALL) != NO_ERROR) { + logwrite(function, "ERROR applying RAW configuration"); + retstring = "failed to apply"; + return ERROR; + } + + // refresh cached geometry from the now-updated configmap + this->get_configmap_value("RAWSEL", this->rawinfo.adchan); + this->get_configmap_value("RAWSAMPLES", this->rawinfo.samples); + this->get_configmap_value("RAWSTARTLINE", this->rawinfo.startline); + this->get_configmap_value("RAWENDLINE", this->rawinfo.endline); + this->get_configmap_value("RAWSTARTPIXEL", this->rawinfo.startpixel); + + return this->get_raw_config(retstring); + } + /***** Camera::ArchonController::set_raw_config **************************/ + + + /***** Camera::ArchonController::read_raw *******************************/ + /** + * @brief retrieve RAW (pre-CDS) data from the newest frame buffer + * @details Reads the size-aware RAW region as 16-bit unsigned samples and + * dispatches it in-band as a (RAWSAMPLES x rawlines) frame, + * independent of the post-CDS pixel mode. + * @param[out] retstring "samples= lines= bytes=" + */ + long ArchonController::read_raw(std::string &retstring) { + const std::string function("Camera::ArchonController::read_raw"); + + this->get_frame_status(); + + const uint32_t bytes = this->raw_frame_bytes(); + if (bytes == 0) { + logwrite(function, "ERROR RAW geometry yields zero bytes; check RAW config"); + retstring = "invalid RAW geometry"; + return ERROR; + } + + const size_t nblocks = (bytes + BLOCK_LEN - 1) / BLOCK_LEN; + std::shared_ptr buffer(new char[nblocks * BLOCK_LEN]); + char* bufptr = buffer.get(); + + if (this->read_frame(FRAME_RAW, bufptr) != NO_ERROR) { + logwrite(function, "ERROR reading RAW frame"); + retstring = "read failed"; + return ERROR; + } + + const int lines = this->rawinfo.endline - this->rawinfo.startline; + const uint32_t rawlines = lines > 0 ? static_cast(lines) : 1u; + const auto index = this->frameinfo.index.load(); + + Camera::FrameMetadata meta; + meta.frame_number = this->frameinfo.bufframen[index]; + meta.timestamp = this->frameinfo.buftimestamp[index]; + meta.width = this->rawinfo.samples; + meta.height = rawlines; + meta.bytes_per_pixel = sizeof(uint16_t); + this->interface->dispatch_frame(buffer.get(), bytes, meta); + + std::ostringstream oss; + oss << "samples=" << this->rawinfo.samples << " lines=" << rawlines << " bytes=" << bytes; + retstring = oss.str(); + logwrite(function, retstring); + return NO_ERROR; + } + /***** Camera::ArchonController::read_raw *******************************/ + + /***** Camera::ArchonController::wait_for_readout ***************************/ /** * @brief creates a wait until the next completed frame buffer is ready diff --git a/camerad/archon_controller.h b/camerad/archon_controller.h index a4d0a04..1cffa74 100644 --- a/camerad/archon_controller.h +++ b/camerad/archon_controller.h @@ -249,10 +249,13 @@ namespace Camera { cfg_map_t configmap; param_map_t parammap; + /** @brief Archon RAW (pre-CDS) capture configuration, mirrors ACF keywords */ struct rawinfo_t { - int adchan; - uint16_t rawsamples; - uint16_t rawlines; + int adchan{0}; // RAWSEL: AD channel captured + uint16_t samples{0}; // RAWSAMPLES: 16-bit samples per line + uint16_t startline{0}; // RAWSTARTLINE + uint16_t endline{0}; // RAWENDLINE + uint16_t startpixel{0}; // RAWSTARTPIXEL } rawinfo; /** @@ -345,6 +348,13 @@ namespace Camera { long write_config_key(const char* key, const char* newvalue, bool &changed); long write_config_key(const char* key, int newvalue, bool &changed); + // RAW (pre-CDS) capture: configuration and retrieval + static bool is_raw_config_key(const std::string &key); + uint32_t raw_frame_bytes() const; // size-aware byte count for a RAW fetch + long set_raw_config(const std::string &args, std::string &retstring); + long get_raw_config(std::string &retstring); + long read_raw(std::string &retstring); + std::map modemap; diff --git a/camerad/archon_interface.cpp b/camerad/archon_interface.cpp index 156b0e1..0643cb2 100644 --- a/camerad/archon_interface.cpp +++ b/camerad/archon_interface.cpp @@ -68,6 +68,10 @@ namespace Camera { return this->set_camera_mode(args, retstring); } else + if ( cmd == CAMERAD_RAW ) { + return this->raw(args, retstring); + } + else if ( cmd == "autofetch_mode" ) { return this->autofetch_mode(args, retstring); } @@ -944,6 +948,48 @@ namespace Camera { /***** Camera::ArchonInterface::set_vcpu_inreg ******************************/ + /***** Camera::ArchonInterface::raw ****************************************/ + /** + * @brief configure and retrieve Archon RAW (pre-CDS) data + * @param[in] args "config" | "set [...]" | "read" + * @param[out] retstring RAW configuration or retrieval summary + * @return ERROR | NO_ERROR | HELP + * + */ + long ArchonInterface::raw( const std::string args, std::string &retstring ) { + const std::string function("Camera::ArchonInterface::raw"); + + if (args=="?" || args=="help") { + retstring = CAMERAD_RAW; + retstring.append( " [ config | set [...] | read ]\n" ); + retstring.append( " config report the RAW config keywords\n" ); + retstring.append( " set .. set RAW keyword(s) then apply\n" ); + retstring.append( " read retrieve RAW data in-band as 16-bit samples\n" ); + retstring.append( " Keys: RAWENABLE RAWSEL RAWSTARTLINE RAWENDLINE RAWSTARTPIXEL RAWSAMPLES\n" ); + return HELP; + } + + std::size_t sep = args.find_first_of(" "); + const std::string subcmd = args.substr(0, sep); + const std::string subargs = (sep==std::string::npos) ? "" : args.substr(sep+1); + + if (subcmd.empty() || subcmd=="config") { + return this->controller->get_raw_config(retstring); + } + if (subcmd=="set") { + return this->controller->set_raw_config(subargs, retstring); + } + if (subcmd=="read") { + return this->controller->read_raw(retstring); + } + + logwrite(function, "ERROR unrecognized subcommand: "+subcmd); + retstring = "unrecognized subcommand: "+subcmd; + return ERROR; + } + /***** Camera::ArchonInterface::raw ****************************************/ + + /***** Camera::ArchonInterface::native **************************************/ /** * @brief send native commands directly to Archon and log result diff --git a/camerad/archon_interface.h b/camerad/archon_interface.h index 8db6bcd..b2ad5c5 100644 --- a/camerad/archon_interface.h +++ b/camerad/archon_interface.h @@ -43,6 +43,7 @@ namespace Camera { long load_firmware( const std::string &args, std::string &retstring ) override; long native( const std::string args, std::string &retstring ) override; long power( const std::string args, std::string &retstring ) override; + long raw( const std::string args, std::string &retstring ); long test( const std::string args, std::string &retstring ) override; long do_expose() override; diff --git a/camerad/camera_server.cpp b/camerad/camera_server.cpp index 0149c6f..dff7f9a 100644 --- a/camerad/camera_server.cpp +++ b/camerad/camera_server.cpp @@ -321,6 +321,10 @@ namespace Camera { ret = interface->controller_cmd(cmd, args, retstring); } else + if ( cmd == CAMERAD_RAW ) { + ret = interface->controller_cmd(cmd, args, retstring); + } + else if ( cmd == "bob" ) { ret = interface->controller_cmd(cmd, args, retstring); } diff --git a/common/camerad_commands.h b/common/camerad_commands.h index 3da8f98..86e7758 100644 --- a/common/camerad_commands.h +++ b/common/camerad_commands.h @@ -44,6 +44,7 @@ const std::string CAMERAD_OPEN("open"); const int CAMERAD_OPEN_TIMEOUT(10000); const std::string CAMERAD_PAUSE("pause"); const std::string CAMERAD_POWER("power"); const std::string CAMERAD_PREEXPOSURES("preexposures"); +const std::string CAMERAD_RAW("raw"); const std::string CAMERAD_READACF("readacf"); const std::string CAMERAD_READOUT("readout"); const std::string CAMERAD_RESUME("resume"); @@ -88,6 +89,7 @@ const std::vector CAMERAD_SYNTAX = { CAMERAD_OPEN+" [ ? | ]", CAMERAD_PAUSE, CAMERAD_PREEXPOSURES, + CAMERAD_RAW+" [ ? | config | set [...] | read ]", CAMERAD_READACF+" [ ? | ]", CAMERAD_READOUT+" [ ? ] | [ | [ ] ]", CAMERAD_RESUME, From e81f0c8f383e5e11e03c1c9d603bdcf460f7f879 Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:48:24 -0700 Subject: [PATCH 3/6] fix RAW sizing, inclusive line count, per-line block padding from BUFnRAWBLOCKS/LINES, and APPLYCDS --- camerad/archon_controller.cpp | 86 ++++++++++++++++++++++++++--------- camerad/archon_controller.h | 10 +++- 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/camerad/archon_controller.cpp b/camerad/archon_controller.cpp index eee3c18..504e6e5 100644 --- a/camerad/archon_controller.cpp +++ b/camerad/archon_controller.cpp @@ -579,13 +579,17 @@ namespace Camera { else if (std::strncmp(suffix, "HEIGHT", 6)==0) this->frameinfo.bufheight[bufnum] = std::atoi(value_start); break; - case 8: // BUFnCOMPLETE + case 8: // BUFnCOMPLETE, RAWLINES if (std::strncmp(suffix, "COMPLETE", 8)==0) this->frameinfo.bufcomplete[bufnum] = std::atoi(value_start); + else + if (std::strncmp(suffix, "RAWLINES", 8)==0) this->frameinfo.bufrawlines[bufnum] = std::atoi(value_start); break; - case 9: // BUFnTIMESTAMP, RAWOFFSET + case 9: // BUFnTIMESTAMP, RAWOFFSET, RAWBLOCKS if (std::strncmp(suffix, "TIMESTAMP", 9)==0) this->frameinfo.buftimestamp[bufnum] = std::strtoull(value_start, nullptr, 16); else if (std::strncmp(suffix, "RAWOFFSET", 9)==0) this->frameinfo.bufrawoffset[bufnum] = std::strtoul(value_start, nullptr, 10); + else + if (std::strncmp(suffix, "RAWBLOCKS", 9)==0) this->frameinfo.bufrawblocks[bufnum] = std::atoi(value_start); break; case 11: // BUFnRETIMESTAMP, FETIMESTAMP if (std::strncmp(suffix, "RETIMESTAMP", 11)==0) this->frameinfo.bufretimestamp[bufnum] = std::strtoull(value_start, nullptr, 16); @@ -2051,17 +2055,44 @@ namespace Camera { /***** Camera::ArchonController::is_raw_config_key *************************/ + /***** Camera::ArchonController::raw_geometry ****************************/ + /** + * @brief resolve RAW capture geometry for the newest buffer + * @details Prefers the controller-reported BUFnRAWBLOCKS/BUFnRAWLINES, + * which already account for the per-line rounding to whole + * 1024-byte blocks. Falls back to the config keys when the + * controller has not reported them (e.g. emulator). The line + * range is inclusive: RAWSTARTLINE=0,RAWENDLINE=1 is two lines. + */ + ArchonController::raw_geometry_t ArchonController::raw_geometry() const { + const auto index = this->frameinfo.index.load(); + raw_geometry_t geom; + geom.samples = static_cast(this->rawinfo.samples); + geom.blocks_per_line = static_cast(this->frameinfo.bufrawblocks[index]); + geom.lines = static_cast(this->frameinfo.bufrawlines[index]); + + if (geom.blocks_per_line == 0 || geom.lines == 0) { + geom.blocks_per_line = + (static_cast(geom.samples) * sizeof(uint16_t) + BLOCK_LEN - 1) / BLOCK_LEN; + const int span = this->rawinfo.endline - this->rawinfo.startline + 1; + geom.lines = span > 0 ? static_cast(span) : 0u; + } + return geom; + } + /***** Camera::ArchonController::raw_geometry ****************************/ + + /***** Camera::ArchonController::raw_frame_bytes **************************/ /** - * @brief size-aware byte count for one RAW capture - * @details RAW samples are always 16-bit. Count = rawlines x RAWSAMPLES, - * rawlines = RAWENDLINE - RAWSTARTLINE. RAWSTARTPIXEL only sets - * the reshape origin, so it does not change the sample count. + * @brief padded, size-aware byte count for one RAW fetch + * @details Each line occupies whole 1024-byte blocks, so the fetch reads + * blocks_per_line x lines blocks. RAW samples are always 16-bit; + * RAWSTARTPIXEL only sets where sampling begins in a line and so + * does not change the count. */ uint32_t ArchonController::raw_frame_bytes() const { - const int lines = this->rawinfo.endline - this->rawinfo.startline; - const uint32_t rawlines = lines > 0 ? static_cast(lines) : 1u; - return rawlines * static_cast(this->rawinfo.samples) * sizeof(uint16_t); + const raw_geometry_t geom = this->raw_geometry(); + return geom.blocks_per_line * geom.lines * BLOCK_LEN; } /***** Camera::ArchonController::raw_frame_bytes **************************/ @@ -2116,7 +2147,7 @@ namespace Camera { } } - if (changed && this->send_cmd(APPLYALL) != NO_ERROR) { + if (changed && this->send_cmd(APPLYCDS) != NO_ERROR) { logwrite(function, "ERROR applying RAW configuration"); retstring = "failed to apply"; return ERROR; @@ -2147,16 +2178,16 @@ namespace Camera { this->get_frame_status(); - const uint32_t bytes = this->raw_frame_bytes(); - if (bytes == 0) { - logwrite(function, "ERROR RAW geometry yields zero bytes; check RAW config"); + const raw_geometry_t geom = this->raw_geometry(); + if (geom.samples == 0 || geom.lines == 0) { + logwrite(function, "ERROR RAW geometry is empty; check RAW config"); retstring = "invalid RAW geometry"; return ERROR; } - const size_t nblocks = (bytes + BLOCK_LEN - 1) / BLOCK_LEN; - std::shared_ptr buffer(new char[nblocks * BLOCK_LEN]); - char* bufptr = buffer.get(); + const size_t fetch_bytes = static_cast(geom.blocks_per_line) * geom.lines * BLOCK_LEN; + std::shared_ptr raw_buffer(new char[fetch_bytes]); + char* bufptr = raw_buffer.get(); if (this->read_frame(FRAME_RAW, bufptr) != NO_ERROR) { logwrite(function, "ERROR reading RAW frame"); @@ -2164,20 +2195,31 @@ namespace Camera { return ERROR; } - const int lines = this->rawinfo.endline - this->rawinfo.startline; - const uint32_t rawlines = lines > 0 ? static_cast(lines) : 1u; + // The Archon pads each raw line out to whole 1024-byte blocks, so copy only + // the valid RAWSAMPLES from each line into a contiguous lines x samples array + const size_t line_stride = static_cast(geom.blocks_per_line) * BLOCK_LEN; + const size_t payload_samples = static_cast(geom.lines) * geom.samples; + std::vector samples(payload_samples); + for (uint32_t line = 0; line < geom.lines; ++line) { + std::memcpy(samples.data() + static_cast(line) * geom.samples, + raw_buffer.get() + line * line_stride, + static_cast(geom.samples) * sizeof(uint16_t)); + } + const auto index = this->frameinfo.index.load(); + const size_t payload_bytes = payload_samples * sizeof(uint16_t); Camera::FrameMetadata meta; meta.frame_number = this->frameinfo.bufframen[index]; meta.timestamp = this->frameinfo.buftimestamp[index]; - meta.width = this->rawinfo.samples; - meta.height = rawlines; + meta.width = geom.samples; + meta.height = geom.lines; meta.bytes_per_pixel = sizeof(uint16_t); - this->interface->dispatch_frame(buffer.get(), bytes, meta); + this->interface->dispatch_frame(reinterpret_cast(samples.data()), + payload_bytes, meta); std::ostringstream oss; - oss << "samples=" << this->rawinfo.samples << " lines=" << rawlines << " bytes=" << bytes; + oss << "samples=" << geom.samples << " lines=" << geom.lines << " bytes=" << payload_bytes; retstring = oss.str(); logwrite(function, retstring); return NO_ERROR; diff --git a/camerad/archon_controller.h b/camerad/archon_controller.h index 1cffa74..59932de 100644 --- a/camerad/archon_controller.h +++ b/camerad/archon_controller.h @@ -349,8 +349,16 @@ namespace Camera { long write_config_key(const char* key, int newvalue, bool &changed); // RAW (pre-CDS) capture: configuration and retrieval + /** @brief resolved RAW capture geometry for the newest buffer */ + struct raw_geometry_t { + uint32_t samples; // valid 16-bit samples per line (RAWSAMPLES) + uint32_t blocks_per_line; // 1024-byte blocks per line, padded per Archon + uint32_t lines; // number of raw lines (RAWENDLINE-RAWSTARTLINE+1) + }; + static bool is_raw_config_key(const std::string &key); - uint32_t raw_frame_bytes() const; // size-aware byte count for a RAW fetch + raw_geometry_t raw_geometry() const; + uint32_t raw_frame_bytes() const; // padded, size-aware byte count for a RAW fetch long set_raw_config(const std::string &args, std::string &retstring); long get_raw_config(std::string &retstring); long read_raw(std::string &retstring); From 790bd56e72230dbe4ab6c17c7d13504a454bf77d Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:45:26 -0700 Subject: [PATCH 4/6] Address review: check get_frame_status() error in read_raw, use string_view for FRAME reply key matching --- camerad/archon_controller.cpp | 100 ++++++++++++++-------------------- 1 file changed, 42 insertions(+), 58 deletions(-) diff --git a/camerad/archon_controller.cpp b/camerad/archon_controller.cpp index 504e6e5..8e22e6a 100644 --- a/camerad/archon_controller.cpp +++ b/camerad/archon_controller.cpp @@ -7,6 +7,8 @@ * */ +#include + #include "archon_controller.h" #include "archon_interface.h" @@ -526,7 +528,7 @@ namespace Camera { while (reply_ptr < reply_end && *reply_ptr != '=' && *reply_ptr != ' ') reply_ptr++; if (reply_ptr >= reply_end || *reply_ptr != '=') break; - size_t key_len = reply_ptr - key_start; + const std::string_view key(key_start, reply_ptr - key_start); reply_ptr++; // skip "=" // find value @@ -535,68 +537,43 @@ namespace Camera { size_t valuelen = reply_ptr - value_start; // TIMER=XXXX pattern - if (key_len==5 && key_start[0]=='T' && std::strncmp(key_start, "TIMER", 5)==0) { + if (key == "TIMER") { this->frameinfo.timer.assign(value_start, valuelen); } else - if (key_len==4) { - // RBUF=XXXX pattern - if (key_start[0]=='R' && std::strncmp(key_start, "RBUF", 4)==0) { - this->frameinfo.rbuf = std::atoi(value_start); - } - else - // WBUF=XXXX pattern - if (key_start[0]=='W' && std::strncmp(key_start, "WBUF", 4)==0) { - this->frameinfo.wbuf = std::atoi(value_start); - } + // RBUF=XXXX pattern + if (key == "RBUF") { + this->frameinfo.rbuf = std::atoi(value_start); + } + else + // WBUF=XXXX pattern + if (key == "WBUF") { + this->frameinfo.wbuf = std::atoi(value_start); } else // BUFnXXXX=XXXX pattern... - if (key_len>3 && key_start[0]=='B' && key_start[1]=='U' && key_start[2]=='F') { - int bufnum = key_start[3]-'1'; // convert to 0-based - - // match suffix - const char* suffix = key_start+4; - size_t suffix_len = key_len-4; - - switch (suffix_len) { - case 4: // BUFnBASE, MODE - if (std::strncmp(suffix, "BASE", 4)==0) this->frameinfo.bufbase[bufnum] = std::strtoul(value_start, nullptr, 10); - else - if (std::strncmp(suffix, "MODE", 4)==0) this->frameinfo.bufmode[bufnum] = std::atoi(value_start); - break; - case 5: // BUFnFRAME, WIDTH, LINES - if (std::strncmp(suffix, "FRAME", 5)==0) this->frameinfo.bufframen[bufnum] = std::atoi(value_start); - else - if (std::strncmp(suffix, "WIDTH", 5)==0) this->frameinfo.bufwidth[bufnum] = std::atoi(value_start); - else - if (std::strncmp(suffix, "LINES", 5)==0) this->frameinfo.buflines[bufnum] = std::atoi(value_start); - break; - case 6: // BUFnSAMPLE, PIXELS, HEIGHT - if (std::strncmp(suffix, "SAMPLE", 6)==0) this->frameinfo.bufsample[bufnum] = std::atoi(value_start); - else - if (std::strncmp(suffix, "PIXELS", 6)==0) this->frameinfo.bufpixels[bufnum] = std::atoi(value_start); - else - if (std::strncmp(suffix, "HEIGHT", 6)==0) this->frameinfo.bufheight[bufnum] = std::atoi(value_start); - break; - case 8: // BUFnCOMPLETE, RAWLINES - if (std::strncmp(suffix, "COMPLETE", 8)==0) this->frameinfo.bufcomplete[bufnum] = std::atoi(value_start); - else - if (std::strncmp(suffix, "RAWLINES", 8)==0) this->frameinfo.bufrawlines[bufnum] = std::atoi(value_start); - break; - case 9: // BUFnTIMESTAMP, RAWOFFSET, RAWBLOCKS - if (std::strncmp(suffix, "TIMESTAMP", 9)==0) this->frameinfo.buftimestamp[bufnum] = std::strtoull(value_start, nullptr, 16); - else - if (std::strncmp(suffix, "RAWOFFSET", 9)==0) this->frameinfo.bufrawoffset[bufnum] = std::strtoul(value_start, nullptr, 10); - else - if (std::strncmp(suffix, "RAWBLOCKS", 9)==0) this->frameinfo.bufrawblocks[bufnum] = std::atoi(value_start); - break; - case 11: // BUFnRETIMESTAMP, FETIMESTAMP - if (std::strncmp(suffix, "RETIMESTAMP", 11)==0) this->frameinfo.bufretimestamp[bufnum] = std::strtoull(value_start, nullptr, 16); - else - if (std::strncmp(suffix, "FETIMESTAMP", 11)==0) this->frameinfo.buffetimestamp[bufnum] = std::strtoull(value_start, nullptr, 16); - break; - } // end switch(suffix_len) + if (key.size()>3 && key[0]=='B' && key[1]=='U' && key[2]=='F') { + int bufnum = key[3]-'1'; // convert to 0-based + + // match suffix; string_view compares length before bytes, so this + // stays as cheap as the switch-on-length it replaces + const std::string_view suffix = key.substr(4); + + if (suffix == "BASE") this->frameinfo.bufbase[bufnum] = std::strtoul(value_start, nullptr, 10); + else if (suffix == "MODE") this->frameinfo.bufmode[bufnum] = std::atoi(value_start); + else if (suffix == "FRAME") this->frameinfo.bufframen[bufnum] = std::atoi(value_start); + else if (suffix == "WIDTH") this->frameinfo.bufwidth[bufnum] = std::atoi(value_start); + else if (suffix == "LINES") this->frameinfo.buflines[bufnum] = std::atoi(value_start); + else if (suffix == "SAMPLE") this->frameinfo.bufsample[bufnum] = std::atoi(value_start); + else if (suffix == "PIXELS") this->frameinfo.bufpixels[bufnum] = std::atoi(value_start); + else if (suffix == "HEIGHT") this->frameinfo.bufheight[bufnum] = std::atoi(value_start); + else if (suffix == "COMPLETE") this->frameinfo.bufcomplete[bufnum] = std::atoi(value_start); + else if (suffix == "RAWLINES") this->frameinfo.bufrawlines[bufnum] = std::atoi(value_start); + else if (suffix == "TIMESTAMP") this->frameinfo.buftimestamp[bufnum] = std::strtoull(value_start, nullptr, 16); + else if (suffix == "RAWOFFSET") this->frameinfo.bufrawoffset[bufnum] = std::strtoul(value_start, nullptr, 10); + else if (suffix == "RAWBLOCKS") this->frameinfo.bufrawblocks[bufnum] = std::atoi(value_start); + else if (suffix == "RETIMESTAMP") this->frameinfo.bufretimestamp[bufnum] = std::strtoull(value_start, nullptr, 16); + else if (suffix == "FETIMESTAMP") this->frameinfo.buffetimestamp[bufnum] = std::strtoull(value_start, nullptr, 16); } // end if BUFnXXXX pattern } // end looping through reply @@ -2132,6 +2109,8 @@ namespace Camera { return ERROR; } + // accumulates across all keys: write_config_key only ever sets this to true, + // so APPLYCDS below fires if ANY key in the batch actually changed bool changed = false; for (size_t i = 0; i < tokens.size(); i += 2) { std::string key = tokens[i]; @@ -2176,7 +2155,12 @@ namespace Camera { long ArchonController::read_raw(std::string &retstring) { const std::string function("Camera::ArchonController::read_raw"); - this->get_frame_status(); + long error = this->get_frame_status(); + if (error != NO_ERROR) { + logwrite(function, "ERROR getting frame status"); + retstring = "frame status query failed"; + return error; + } const raw_geometry_t geom = this->raw_geometry(); if (geom.samples == 0 || geom.lines == 0) { From d65f5a9da82063b077714861d2ac0cbfc417e7c4 Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:19:43 -0700 Subject: [PATCH 5/6] Remove duplicate configure_frame_outputs (already added generically in main via #13) --- camerad/camera_interface.cpp | 15 --------------- camerad/camera_interface.h | 1 - 2 files changed, 16 deletions(-) diff --git a/camerad/camera_interface.cpp b/camerad/camera_interface.cpp index 0b28ef9..0bace50 100644 --- a/camerad/camera_interface.cpp +++ b/camerad/camera_interface.cpp @@ -12,21 +12,6 @@ namespace Camera { this->server=s; } - /***** Camera::Interface::configure_frame_outputs ***************************/ - /** - * @brief build the in-band frame output sinks (FITS, shared memory) - * @details dispatch_frame fans every retrieved frame out to these - * - */ - void Interface::configure_frame_outputs() { - const std::string function("Camera::Interface::configure_frame_outputs"); - FrameOutputsConfig outcfg; - apply_config_overrides(outcfg, this->configfile); - this->frame_outputs = make_frame_outputs(outcfg); - logwrite(function, "configured "+std::to_string(this->frame_outputs.size())+" frame output(s)"); - } - /***** Camera::Interface::configure_frame_outputs ***************************/ - void Interface::func_shared() { std::string function("Camera::Interface::func_shared"); logwrite(function, "common implementation function"); diff --git a/camerad/camera_interface.h b/camerad/camera_interface.h index 8e2a568..a9c52cc 100644 --- a/camerad/camera_interface.h +++ b/camerad/camera_interface.h @@ -82,7 +82,6 @@ namespace Camera { // void set_server(Camera::Server* s); void func_shared(); - void configure_frame_outputs(); void disconnect_controller(); long key(std::string args, std::string &retstring); long datacube(std::string args, std::string &retstring); From 36092f96e17e8e35a60fe64423b37a517be5b5cb Mon Sep 17 00:00:00 2001 From: Mike Langmayr <1809691+mikelangmayr@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:24:24 -0700 Subject: [PATCH 6/6] Always APPLYCDS in set_raw_config instead of gating on a changed flag --- camerad/archon_controller.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/camerad/archon_controller.cpp b/camerad/archon_controller.cpp index dacf3f1..b50d761 100644 --- a/camerad/archon_controller.cpp +++ b/camerad/archon_controller.cpp @@ -2376,7 +2376,7 @@ namespace Camera { /***** Camera::ArchonController::set_raw_config **************************/ /** - * @brief set one or more RAW config keywords then APPLYALL + * @brief set one or more RAW config keywords then APPLYCDS * @param[in] args "KEY VALUE [KEY VALUE ...]" * @param[out] retstring resulting RAW configuration */ @@ -2391,8 +2391,6 @@ namespace Camera { return ERROR; } - // accumulates across all keys: write_config_key only ever sets this to true, - // so APPLYCDS below fires if ANY key in the batch actually changed bool changed = false; for (size_t i = 0; i < tokens.size(); i += 2) { std::string key = tokens[i]; @@ -2408,7 +2406,9 @@ namespace Camera { } } - if (changed && this->send_cmd(APPLYCDS) != NO_ERROR) { + // Archon has no way to report whether a pending config write has already + // been applied, so always APPLYCDS instead of trying to infer if it's needed + if (this->send_cmd(APPLYCDS) != NO_ERROR) { logwrite(function, "ERROR applying RAW configuration"); retstring = "failed to apply"; return ERROR;