diff --git a/common/stream-model.cpp b/common/stream-model.cpp index f1d61ce1cb..9c15104f36 100644 --- a/common/stream-model.cpp +++ b/common/stream-model.cpp @@ -281,69 +281,67 @@ namespace rs2 // x,y remain the same, only update the width,height with new mouse position relative to starting mouse position roi_display_rect.w = mouse.cursor.x - roi_display_rect.x; roi_display_rect.h = mouse.cursor.y - roi_display_rect.y; - } - // Case 3: We are in middle of dragging (capturing) and mouse was released - if (!mouse.mouse_down[0] && capturing_roi && stream_rect.contains(mouse.cursor)) - { - // Update width,height one last time - roi_display_rect.w = mouse.cursor.x - roi_display_rect.x; - roi_display_rect.h = mouse.cursor.y - roi_display_rect.y; - capturing_roi = false; // Mark that we are no longer dragging - if (roi_display_rect) // If the rect is not empty? + // Case 3: We are in middle of dragging (capturing) and mouse was released + if( ! mouse.mouse_down[0] ) { - // Convert from local (pixel) coordinate system to device coordinate system - auto r = roi_display_rect; - r = r.normalize(stream_rect).unnormalize(_normalized_zoom.unnormalize(get_original_stream_bounds())); - dev->roi_rect = r; // Store new rect in device coordinates into the subdevice object - - // Send it to firmware: - // Step 1: get rid of negative width / height - region_of_interest roi{}; - roi.min_x = static_cast(std::min(r.x, r.x + r.w)); - roi.max_x = static_cast(std::max(r.x, r.x + r.w)); - roi.min_y = static_cast(std::min(r.y, r.y + r.h)); - roi.max_y = static_cast(std::max(r.y, r.y + r.h)); - - try + capturing_roi = false; // Mark that we are no longer dragging + + if (roi_display_rect) // If the rect is not empty? { - // Step 2: send it to firmware - if (sensor->is()) + // Convert from local (pixel) coordinate system to device coordinate system + auto r = roi_display_rect; + r = r.normalize(stream_rect).unnormalize(_normalized_zoom.unnormalize(get_original_stream_bounds())); + dev->roi_rect = r; // Store new rect in device coordinates into the subdevice object + + // Send it to firmware: + // Step 1: get rid of negative width / height + region_of_interest roi{}; + roi.min_x = static_cast(std::min(r.x, r.x + r.w)); + roi.max_x = static_cast(std::max(r.x, r.x + r.w)); + roi.min_y = static_cast(std::min(r.y, r.y + r.h)); + roi.max_y = static_cast(std::max(r.y, r.y + r.h)); + + try + { + // Step 2: send it to firmware + if (sensor->is()) + { + sensor->as().set_region_of_interest(roi); + } + } + catch (const error& e) { - sensor->as().set_region_of_interest(roi); + error_message = error_to_string(e); } } - catch (const error& e) + else // If the rect is empty { - error_message = error_to_string(e); - } - } - else // If the rect is empty - { - try - { - // To reset ROI, just set ROI to the entire frame - auto x_margin = (int)size.x / 8; - auto y_margin = (int)size.y / 8; + try + { + // To reset ROI, just set ROI to the entire frame + auto x_margin = (int)size.x / 8; + auto y_margin = (int)size.y / 8; + + // Default ROI behavior is center 3/4 of the screen: + if (sensor->is()) + { + sensor->as().set_region_of_interest({ x_margin, y_margin, + (int)size.x - x_margin - 1, + (int)size.y - y_margin - 1 }); + } - // Default ROI behavior is center 3/4 of the screen: - if (sensor->is()) + roi_display_rect = { 0, 0, 0, 0 }; + dev->roi_rect = { 0, 0, 0, 0 }; + } + catch (const error& e) { - sensor->as().set_region_of_interest({ x_margin, y_margin, - (int)size.x - x_margin - 1, - (int)size.y - y_margin - 1 }); + error_message = error_to_string(e); } - - roi_display_rect = { 0, 0, 0, 0 }; - dev->roi_rect = { 0, 0, 0, 0 }; - } - catch (const error& e) - { - error_message = error_to_string(e); } - } - dev->roi_checked = false; + dev->roi_checked = false; + } } // If we left stream bounds while capturing, stop capturing if (capturing_roi && !stream_rect.contains(mouse.cursor)) diff --git a/common/viewer.cpp b/common/viewer.cpp index 22b719ba0f..0073ed30dc 100644 --- a/common/viewer.cpp +++ b/common/viewer.cpp @@ -791,6 +791,7 @@ namespace rs2 _hidden_options.emplace(RS2_OPTION_FRAMES_QUEUE_SIZE); _hidden_options.emplace(RS2_OPTION_SENSOR_MODE); _hidden_options.emplace(RS2_OPTION_NOISE_ESTIMATION); + _hidden_options.emplace(RS2_OPTION_REGION_OF_INTEREST); } void viewer_model::update_configuration() diff --git a/include/librealsense2/h/rs_option.h b/include/librealsense2/h/rs_option.h index 2181c8088d..10d03fe2e8 100644 --- a/include/librealsense2/h/rs_option.h +++ b/include/librealsense2/h/rs_option.h @@ -123,7 +123,8 @@ extern "C" { RS2_OPTION_DEPTH_AUTO_EXPOSURE_MODE, /**< Select depth sensor auto exposure mode see rs2_depth_auto_exposure_mode for values */ RS2_OPTION_OHM_TEMPERATURE, /**< Temperature of the Optical Head Sensor */ RS2_OPTION_SOC_PVT_TEMPERATURE, /**< Temperature of PVT SOC */ - RS2_OPTION_GYRO_SENSITIVITY,/**< Control of the gyro sensitivity level, see rs2_gyro_sensitivity for values */ + RS2_OPTION_GYRO_SENSITIVITY,/**< Control of the gyro sensitivity level, see rs2_gyro_sensitivity for values */ + RS2_OPTION_REGION_OF_INTEREST,/**< The rectangular area used from the streaming profile */ RS2_OPTION_COUNT /**< Number of enumeration values. Not a valid input: intended to be used in for-loops. */ } rs2_option; @@ -149,6 +150,7 @@ extern "C" { RS2_OPTION_TYPE_FLOAT, RS2_OPTION_TYPE_STRING, RS2_OPTION_TYPE_BOOLEAN, + RS2_OPTION_TYPE_RECT, RS2_OPTION_TYPE_COUNT @@ -160,6 +162,16 @@ extern "C" { */ const char * rs2_option_type_to_string( rs2_option_type type ); + /** + * A rectangle expressed in 64 bits, used with rs2_option_value::as_rect. + * Same semantics as rs2_set_region_of_interest. + */ + typedef struct rs2_option_rect + { + int16_t x1, y1; + int16_t x2, y2; + } rs2_option_rect; + /** \brief The value of an option, in a known option type. */ typedef struct rs2_option_value @@ -167,11 +179,15 @@ extern "C" { rs2_option id; int is_valid; /**< 0 if no value available; 1 otherwise */ rs2_option_type type; - union { +#pragma pack(push,1) + union + { char const * as_string; /**< valid only while rs2_option_value is alive! */ float as_float; int64_t as_integer; /**< including boolean value */ + rs2_option_rect as_rect; }; +#pragma pack(pop) } rs2_option_value; /** \brief For SR300 devices: provides optimized settings (presets) for specific types of usage. */ diff --git a/src/dds/rs-dds-option.cpp b/src/dds/rs-dds-option.cpp index 2022ec827f..a624b0687a 100644 --- a/src/dds/rs-dds-option.cpp +++ b/src/dds/rs-dds-option.cpp @@ -2,6 +2,7 @@ // Copyright(c) 2024 Intel Corporation. All Rights Reserved. #include "rs-dds-option.h" +#include #include #include @@ -44,6 +45,8 @@ static rs2_option_type rs_type_from_dds_option( std::shared_ptr< realdds::dds_op return RS2_OPTION_TYPE_BOOLEAN; if( std::dynamic_pointer_cast< realdds::dds_integer_option >( dds_opt ) ) return RS2_OPTION_TYPE_INTEGER; + if( std::dynamic_pointer_cast< realdds::dds_rect_option >( dds_opt ) ) + return RS2_OPTION_TYPE_RECT; throw not_implemented_exception( "unknown DDS option type" ); } @@ -65,7 +68,65 @@ void rs_dds_option::set( float value ) if( ! _set_opt_cb ) throw std::runtime_error( "Set option callback is not set for option " + _dds_opt->get_name() ); - _set_opt_cb( value ); + // This is the legacy API for setting option values - it accepts a float. It's not always called via the rs2_... + // APIs, but can be called from within librealsense, so we have to convert to the proper type: + // (usually the range checks are only done at the level of rs2_...) + + auto validate_range = []( float const value, option_range const & range ) + { + if( range.min != range.max && range.step ) + { + if( value < range.min ) + { + throw librealsense::invalid_value_exception( + rsutils::string::from() << "value (" << value << ") less than minimum (" << range.min << ")" ); + } + if( value > range.max ) + { + throw librealsense::invalid_value_exception( + rsutils::string::from() << "value (" << value << ") greater than maximum (" << range.max << ")" ); + } + } + }; + + json j_value; + switch( _rs_type ) + { + case RS2_OPTION_TYPE_FLOAT: + validate_range( value, get_range() ); + j_value = value; + break; + + case RS2_OPTION_TYPE_INTEGER: + validate_range( value, get_range() ); + if( (int) value != value ) + throw invalid_value_exception( rsutils::string::from() << "not an integer: " << value ); + j_value = int( value ); + break; + + case RS2_OPTION_TYPE_BOOLEAN: + if( value == 0.f ) + j_value = false; + else if( value == 1.f ) + j_value = true; + else + throw invalid_value_exception( rsutils::string::from() << "not a boolean: " << value ); + break; + + case RS2_OPTION_TYPE_STRING: + // We can convert "enum" options to a float value + if( auto desc = get_value_description( value ) ) + j_value = desc; + else + throw not_implemented_exception( "use rs2_set_option_value to set string values" ); + break; + + default: + throw not_implemented_exception( rsutils::string::from() + << "use rs2_set_option_value to set " << get_string( _rs_type ) << " value" ); + } + + _set_opt_cb( j_value ); } diff --git a/src/dds/rs-dds-sensor-proxy.cpp b/src/dds/rs-dds-sensor-proxy.cpp index b110167f91..9c1fa46ec5 100644 --- a/src/dds/rs-dds-sensor-proxy.cpp +++ b/src/dds/rs-dds-sensor-proxy.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -44,6 +45,22 @@ dds_sensor_proxy::dds_sensor_proxy( std::string const & sensor_name, } +bool dds_sensor_proxy::extend_to( rs2_extension extension_type, void ** ptr ) +{ + if( extension_type == RS2_EXTENSION_ROI ) + { + // We do not extend roi_sensor_interface, as our support is enabled only if there's an option with the specific + // type! Instead, we expose through extend_to() only if such an option is found. See add_option(). + if( _roi_support ) + { + *ptr = _roi_support.get(); + return true; + } + } + return super::extend_to( extension_type, ptr ); +} + + void dds_sensor_proxy::add_dds_stream( sid_index sidx, std::shared_ptr< realdds::dds_stream > const & stream ) { auto & s = _streams[sidx]; @@ -529,6 +546,32 @@ void dds_sensor_proxy::stop() } +class dds_option_roi_method : public region_of_interest_method +{ + std::shared_ptr< rs_dds_option > _rs_option; + +public: + dds_option_roi_method( std::shared_ptr< rs_dds_option > const & rs_option ) + : _rs_option( rs_option ) + { + } + + void set( const region_of_interest & roi ) override + { + _rs_option->set_value( json::array( { roi.min_x, roi.min_y, roi.max_x, roi.max_y } ) ); + } + + region_of_interest get() const override + { + auto j = _rs_option->get_value(); + if( ! j.is_array() ) + throw std::runtime_error( "no ROI available" ); + region_of_interest roi{ j[0], j[1], j[2], j[3] }; + return roi; + } +}; + + void dds_sensor_proxy::add_option( std::shared_ptr< realdds::dds_option > option ) { bool const ok_if_there = true; @@ -564,6 +607,16 @@ void dds_sensor_proxy::add_option( std::shared_ptr< realdds::dds_option > option } ); register_option( option_id, opt ); _options_watcher.register_option( option_id, opt ); + + if( std::dynamic_pointer_cast< realdds::dds_rect_option >( option ) && option->get_name() == "Region of Interest" ) + { + if( _roi_support ) + throw std::runtime_error( "more than one ROI option in stream" ); + + auto roi = std::make_shared< roi_sensor_base >(); + roi->set_roi_method( std::make_shared< dds_option_roi_method >( opt ) ); + _roi_support = roi; + } } diff --git a/src/dds/rs-dds-sensor-proxy.h b/src/dds/rs-dds-sensor-proxy.h index 6b15f8687c..fdb4ac03ab 100644 --- a/src/dds/rs-dds-sensor-proxy.h +++ b/src/dds/rs-dds-sensor-proxy.h @@ -32,6 +32,7 @@ namespace librealsense { class dds_device_proxy; +class roi_sensor_interface; class dds_sensor_proxy : public software_sensor @@ -46,6 +47,8 @@ class dds_sensor_proxy : public software_sensor typedef realdds::dds_metadata_syncer syncer_type; static void frame_releaser( syncer_type::frame_type * f ) { static_cast< frame * >( f )->release(); } + std::shared_ptr< roi_sensor_interface > _roi_support; + protected: struct streaming_impl { @@ -64,6 +67,8 @@ class dds_sensor_proxy : public software_sensor software_device * owner, std::shared_ptr< realdds::dds_device > const & dev ); + bool extend_to( rs2_extension, void ** ptr ) override; // extendable_interface + const std::string & get_name() const { return _name; } void add_dds_stream( sid_index sidx, std::shared_ptr< realdds::dds_stream > const & stream ); diff --git a/src/rs.cpp b/src/rs.cpp index c0117bbfb5..3948e2320a 100644 --- a/src/rs.cpp +++ b/src/rs.cpp @@ -135,6 +135,24 @@ struct rs2_option_value_wrapper : rs2_option_value as_string = p_json->string_ref().c_str(); break; + case RS2_OPTION_TYPE_RECT: + if( ! p_json->is_array() || 4 != p_json->size() ) + throw invalid_value_exception( get_string( option_id ) + + " value is not a rect: " + p_json->dump() ); + try + { + p_json->at( 0 ).get_to( as_rect.x1 ); + p_json->at( 1 ).get_to( as_rect.y1 ); + p_json->at( 2 ).get_to( as_rect.x2 ); + p_json->at( 3 ).get_to( as_rect.y2 ); + } + catch( json::exception const & e ) + { + throw invalid_value_exception( get_string( option_id ) + + " value is not a rect: " + e.what() ); + } + break; + default: throw invalid_value_exception( "invalid " + get_string( option_id ) + " type " + get_string( option_type ) ); @@ -732,7 +750,7 @@ float rs2_get_option(const rs2_options* options, rs2_option option_id, rs2_error case RS2_OPTION_TYPE_BOOLEAN: return (float)option.get_value().get< bool >(); - case RS2_OPTION_TYPE_STRING: + case RS2_OPTION_TYPE_STRING: { // We can convert "enum" options to a float value auto r = option.get_range(); if( r.min == 0.f && r.step == 1.f ) @@ -749,6 +767,10 @@ float rs2_get_option(const rs2_options* options, rs2_option option_id, rs2_error } throw not_implemented_exception( "use rs2_get_option_value to get string values" ); } + + case RS2_OPTION_TYPE_RECT: + throw not_implemented_exception( "use rs2_get_option_value to get rect values" ); + } return option.query(); } HANDLE_EXCEPTIONS_AND_RETURN(0.f, options, option_id) @@ -778,46 +800,7 @@ void rs2_set_option(const rs2_options* options, rs2_option option, float value, { VALIDATE_NOT_NULL(options); VALIDATE_OPTION_ENABLED(options, option); - auto& option_ref = options->options->get_option(option); - auto range = option_ref.get_range(); - switch( option_ref.get_value_type() ) - { - case RS2_OPTION_TYPE_FLOAT: - if( range.min != range.max && range.step ) - VALIDATE_RANGE( value, range.min, range.max ); - option_ref.set( value ); - break; - - case RS2_OPTION_TYPE_INTEGER: - if( range.min != range.max && range.step ) - VALIDATE_RANGE( value, range.min, range.max ); - if( (int)value != value ) - throw invalid_value_exception( rsutils::string::from() << "not an integer: " << value ); - option_ref.set( value ); - break; - - case RS2_OPTION_TYPE_BOOLEAN: - if( value == 0.f ) - option_ref.set_value( false ); - else if( value == 1.f ) - option_ref.set_value( true ); - else - throw invalid_value_exception( rsutils::string::from() << "not a boolean: " << value ); - break; - - case RS2_OPTION_TYPE_STRING: - // We can convert "enum" options to a float value - if( (int)value == value && range.min == 0.f && range.step == 1.f ) - { - auto desc = option_ref.get_value_description( value ); - if( desc ) - { - option_ref.set_value( desc ); - break; - } - } - throw not_implemented_exception( "use rs2_set_option_value to set string values" ); - } + options->options->get_option(option).set( value ); } HANDLE_EXCEPTIONS_AND_RETURN(, options, option, value) @@ -856,6 +839,13 @@ void rs2_set_option_value( rs2_options const * options, rs2_option_value const * option.set_value( option_value->as_string ); break; + case RS2_OPTION_TYPE_RECT: + option.set_value( json::array( { option_value->as_rect.x1, + option_value->as_rect.y1, + option_value->as_rect.x2, + option_value->as_rect.y2 } ) ); + break; + default: throw not_implemented_exception( "unexpected option type " + get_string( option_type ) ); } @@ -1440,7 +1430,14 @@ void rs2_set_options_changed_callback( rs2_options * options, { rs2_options_list * updated_options_list = new rs2_options_list(); // Should be on heap if user will choose to save for later use. populate_options_list( updated_options_list, updated_options ); - callback( updated_options_list ); + try + { + callback( updated_options_list ); + } + catch( ... ) + { + LOG_ERROR( "Caught exception from options-changed callback" ); + } } ); } HANDLE_EXCEPTIONS_AND_RETURN( , options, callback ) @@ -1464,7 +1461,14 @@ void rs2_set_options_changed_callback_cpp( rs2_options * options, { rs2_options_list * updated_options_list = new rs2_options_list(); // Should be on heap if user will choose to save for later use. populate_options_list( updated_options_list, updated_options ); - cb->on_value_changed( updated_options_list ); + try + { + cb->on_value_changed( updated_options_list ); + } + catch( ... ) + { + LOG_ERROR( "Caught exception from options-changed callback" ); + } } ); } HANDLE_EXCEPTIONS_AND_RETURN( , options, callback ) diff --git a/src/to-string.cpp b/src/to-string.cpp index 96041aad22..16f1dad3da 100644 --- a/src/to-string.cpp +++ b/src/to-string.cpp @@ -459,6 +459,7 @@ std::string const & get_string_( rs2_option value ) CASE( OHM_TEMPERATURE ) CASE( SOC_PVT_TEMPERATURE ) CASE( GYRO_SENSITIVITY ) + arr[RS2_OPTION_REGION_OF_INTEREST] = "Region of Interest"; #undef CASE return arr; }(); diff --git a/third-party/realdds/doc/control.md b/third-party/realdds/doc/control.md index b4be07bdb2..be48c3e9ad 100644 --- a/third-party/realdds/doc/control.md +++ b/third-party/realdds/doc/control.md @@ -102,6 +102,8 @@ The same exact behavior for `set-option` except a `value` is provided in the con ``` New option values should conform to each specific option's value range as communicated when the device was [initialized](initialization.md). +The device server has final say whether an option value is valid or not, and should return an error if `set-option` specifies an unsupported or invalid value based on context. + ### `query-options`: bulk queries diff --git a/third-party/realdds/doc/initialization.md b/third-party/realdds/doc/initialization.md index 3b62de4a5a..c8b467213f 100644 --- a/third-party/realdds/doc/initialization.md +++ b/third-party/realdds/doc/initialization.md @@ -71,7 +71,7 @@ With those 3, one can compute IR2 to RGB, for example. ### `device-options` -This is optional: not all devices have options. See [device](device.md). +This is optional: not all devices have options. Device options will not be shown in the Viewer. See [device](device.md). ```JSON { @@ -83,33 +83,37 @@ This is optional: not all devices have options. See [device](device.md). } ``` -* `"options"` is an array of options - * Each option is an array of `[name, value, range..., default-value, description, [properties...]]`: - * The `name` is what will be displayed to the user - * The current `value` - * An optional `range` of valid values - * Numeric options (`float`, `int`), defined by a `minimum`, `maximum`, and `stepping` - * I.e., is-valid = one-of( `minimum`, `minimum+1*stepping`, `minimum+2*stepping`, ..., `maximum` ) - * Booleans can remove the range, e.g. `["Enabled", true, true, "Description"]` - * Booleans can be expressed as a range with `minimum=0`, `maximum=1`, `stepping=1` - * Free string options would likewise have no range, e.g. `["Name", "Bob", "", "The customer's name"]` - * `"IPv4"` is a string option that conforms to `W.X.Y.Z` (IP address) format - * Enum options are strings with an array of choices, e.g. `["Preset", "Maximum Quality", ["Maximum Range", "Maximum Quality", "Maximum Speed"], "Maximum Speed", "Standard preset combination of options"]` - * A `default-value` which also adheres to the range - * If this and the range are missing, the option is read-only - * A user-friendly description that describes the option, to be shown in any tooltip - * Additional `properties` describing behavior or nature, as an array of (case-sensitive) strings - * `"optional"` to note that it's possible for it to not have a value; lack of a value is denoted as `null` in the JSON - * If optional, a type must be deducible or present in the properties - * E.g., `["name", null, "description", ["optional", "string"]]` is an optional read-only string value that's currently unset - * Enums cannot be optional - * `"string"`, `"int"`, `"boolean"`, `"float"`, `"IPv4"`, `"enum"` can (and sometime must) indicate the value type - * If missing, the type will be deduced, if possible, from the values - * `"read-only"` options are not settable - * `set-option` will fail for these, though their value may change on the server side - * The device server has final say whether an option value is valid or not, and return an error if `set-option` specifies an unsupported or invalid value based on context - -Device options will not be shown in the Viewer. +* `"options"` is an array of options: + +#### Options + +Options are defined with a JSON array: `[name, value, range..., default-value, description, [properties...]]`: +* The `name` is what will be displayed to the user +* The current `value` +* An optional `range` of valid values + * Numeric options (`float`, `int`), defined by a `minimum`, `maximum`, and `stepping` + * I.e., is-valid = one-of( `minimum`, `minimum+1*stepping`, `minimum+2*stepping`, ..., `maximum` ) + * Booleans can remove the range, e.g. `["Enabled", true, true, "Description"]` + * Booleans can be expressed as a range with `minimum=0`, `maximum=1`, `stepping=1` + * Free string options would likewise have no range, e.g. `["Name", "Bob", "", "The customer's name"]` + * `"IPv4"` is a string option that conforms to `W.X.Y.Z` (IP address) format + * Enum options are strings with an array of choices, e.g. `["Preset", "Maximum Quality", ["Maximum Range", "Maximum Quality", "Maximum Speed"], "Maximum Speed", "Standard preset combination of options"]` + * Rectangles are defined with values that arrays themselves: `[x1, y1, x2, y2]` + * All four should be integers + * No range should be used + * E.g., `["name", [1,2,3,4], null, "description", ["optional"]]` +* A `default-value` which also adheres to the range + * If this and the range are missing, the option is read-only +* A user-friendly description that describes the option, to be shown in any tooltip +* Additional `properties` describing behavior or nature, as an array of (case-sensitive) strings + * `"optional"` to note that it's possible for it to not have a value; lack of a value is denoted as `null` in the JSON + * If optional, a type must be deducible or present in the properties + * E.g., `["name", null, "description", ["optional", "string"]]` is an optional read-only string value that's currently unset + * Enums cannot be optional + * `"string"`, `"int"`, `"boolean"`, `"float"`, `"IPv4"`, `"enum"`, `"rect"` can (and sometime must) indicate the value type + * If missing, the type will be deduced, if possible, from the values + * `"read-only"` options are not settable + * `set-option` will fail for these, though their value may change on the server side ### `stream-header` diff --git a/third-party/realdds/include/realdds/dds-option.h b/third-party/realdds/include/realdds/dds-option.h index 262ab1b210..df769d522d 100644 --- a/third-party/realdds/include/realdds/dds-option.h +++ b/third-party/realdds/include/realdds/dds-option.h @@ -208,4 +208,28 @@ class dds_ip_option : public dds_string_option }; +class dds_rect_option : public dds_option +{ + using super = dds_option; + +public: + struct type + { + int x1, y1; + int x2, y2; + + rsutils::json to_json() const; + static type from_json( rsutils::json const & j ) { return { j[0], j[1], j[2], j[3] }; } + }; + +public: + type get_rect() const { return type::from_json( get_value() ); } + + char const * value_type() const override { return "rect"; } + + void check_type( rsutils::json & value ) const override; + static type check_rect( rsutils::json const & ); +}; + + } // namespace realdds diff --git a/third-party/realdds/src/dds-option.cpp b/third-party/realdds/src/dds-option.cpp index 8592a35572..e6ac8e9ded 100644 --- a/third-party/realdds/src/dds-option.cpp +++ b/third-party/realdds/src/dds-option.cpp @@ -224,6 +224,17 @@ bool type_from_value( std::string & type, json const & j ) if( ! type.empty() ) return true; break; + + case json::value_t::array: + if( j.size() == 4 && j[0].is_number_integer() && j[1].is_number_integer() && j[2].is_number_integer() + && j[3].is_number_integer() ) + { + if( type.empty() ) + return type.assign( "rect", 4 ), true; + if( type.length() == 4 && type == "rect" ) + return true; + } + break; } return false; } @@ -262,6 +273,8 @@ static std::string parse_type( json const & j, size_t size, dds_option::option_p case 4: if( 0 == it->compare( "IPv4" ) ) break; + if( 0 == it->compare( "rect" ) ) + break; if( 5 == size && 0 == it->compare( "enum" ) ) break; continue; @@ -317,6 +330,8 @@ static std::string parse_type( json const & j, size_t size, dds_option::option_p return std::make_shared< dds_ip_option >(); if( type == "enum" ) return std::make_shared< dds_enum_option >(); + if( type == "rect" ) + return std::make_shared< dds_rect_option >(); return {}; } @@ -593,4 +608,27 @@ json dds_ip_option::props_to_json() const } +rsutils::json dds_rect_option::type::to_json() const +{ + return json::array( { x1, y1, x2, y2 } ); +} + + +/*static*/ dds_rect_option::type dds_rect_option::check_rect( json const & value ) +{ + if( ! value.is_array() || value.size() != 4 ) + DDS_THROW( runtime_error, "not [x1,y1,x2,y2]: " << value ); + if( ! value[0].is_number_integer() || ! value[1].is_number_integer() || ! value[2].is_number_integer() + || ! value[3].is_number_integer() ) + DDS_THROW( runtime_error, "non-integers found: " << value ); + return type::from_json( value ); +} + + +void dds_rect_option::check_type( json & value ) const +{ + check_rect( value ); +} + + } // namespace realdds diff --git a/tools/dds/dds-adapter/lrs-device-controller.cpp b/tools/dds/dds-adapter/lrs-device-controller.cpp index 1dfe8d8fa5..90b59dfd88 100644 --- a/tools/dds/dds-adapter/lrs-device-controller.cpp +++ b/tools/dds/dds-adapter/lrs-device-controller.cpp @@ -126,6 +126,53 @@ static std::string stream_name_from_rs2( rs2::sensor const & sensor ) } +std::vector< char const * > get_option_enum_values( rs2::sensor const & sensor, + rs2_option const opt, + rs2::option_range const & range, + float const current_value, + size_t * p_current_index, + size_t * p_default_index ) +{ + // Same logic as in Viewer's option-model... + if( range.step < 0.9f ) + return {}; + + size_t current_index = 0, default_index = 0; + std::vector< const char * > labels; + for( auto i = range.min; i <= range.max; i += range.step ) + { + auto label = sensor.get_option_value_description( opt, i ); + if( ! label ) + return {}; // Missing value - not an enum + + if( std::fabs( i - current_value ) < 0.001f ) + current_index = labels.size(); + if( std::fabs( i - range.def ) < 0.001f ) + default_index = labels.size(); + + labels.push_back( label ); + } + if( p_current_index ) + *p_current_index = current_index; + if( p_default_index ) + *p_default_index = default_index; + return labels; +} + + +static json json_from_roi( rs2::region_of_interest const & roi ) +{ + return realdds::dds_rect_option::type{ roi.min_x, roi.min_y, roi.max_x, roi.max_y }.to_json(); +} + + +static rs2::region_of_interest roi_from_json( json const & j ) +{ + auto roi = realdds::dds_rect_option::type::from_json( j ); + return { roi.x1, roi.y1, roi.x2, roi.y2 }; +} + + std::vector< std::shared_ptr< realdds::dds_stream_server > > lrs_device_controller::get_supported_streams() { std::map< std::string, realdds::dds_stream_profiles > stream_name_to_profiles; @@ -273,20 +320,35 @@ std::vector< std::shared_ptr< realdds::dds_stream_server > > lrs_device_controll json j = json::array(); json props = json::array(); j += option_name; - json option_value; // null - no value + // Even read-only options have ranges in librealsense + auto const range = sensor.get_option_range( option_id ); try { - option_value = sensor.get_option( option_id ); + // For now, assume (legacy) librealsense options are all floats + float option_value = sensor.get_option( option_id ); // may throw + size_t current_index, default_index; + auto const values = get_option_enum_values( sensor, option_id, range, option_value, ¤t_index, &default_index ); + if( ! values.empty() ) + { + // Translate to enum + j += values[current_index]; + j += values; + j += values[default_index]; + } + else + { + j += option_value; + j += range.min; + j += range.max; + j += range.step; + j += range.def; + } } catch( ... ) { // Some options can be queried only if certain conditions exist skip them for now props += "optional"; - } - j += option_value; - { - // Even read-only options have ranges in librealsense - auto const range = sensor.get_option_range( option_id ); + j += rsutils::null_json; j += range.min; j += range.max; j += range.step; @@ -308,6 +370,29 @@ std::vector< std::shared_ptr< realdds::dds_stream_server > > lrs_device_controll } } + if( auto roi_sensor = rs2::roi_sensor( sensor ) ) + { + // AE ROI is exposed as an interface in the librealsense API and through a "Region of Interest" + // rectangle option in DDS + json j = json::array(); + j += rs2_option_to_string( RS2_OPTION_REGION_OF_INTEREST ); + json option_value; // null - no value + try + { + option_value = json_from_roi( roi_sensor.get_region_of_interest() ); + } + catch( ... ) + { + // May be available only during streaming + } + j += option_value; + j += nullptr; // No default value + j += "Region of Interest for Auto Exposure"; // Description + j += json::array( { "optional" } ); // Properties + auto dds_opt = realdds::dds_option::from_json( j ); + stream_options.push_back( dds_opt ); + } + auto recommended_filters = sensor.get_recommended_filters(); std::vector< std::string > filter_names; for( auto const & filter : recommended_filters ) @@ -695,7 +780,10 @@ lrs_device_controller::lrs_device_controller( rs2::device dev, std::shared_ptr< switch( changed_option->type ) { case RS2_OPTION_TYPE_FLOAT: - value = changed_option->as_float; + if( auto e = std::dynamic_pointer_cast< realdds::dds_enum_option >( dds_option ) ) + value = e->get_choices().at( int( changed_option->as_float ) ); + else + value = changed_option->as_float; break; case RS2_OPTION_TYPE_STRING: value = changed_option->as_string; @@ -862,7 +950,7 @@ lrs_device_controller::get_rs2_profiles( realdds::dds_stream_profiles const & dd } -void lrs_device_controller::set_option( const std::shared_ptr< realdds::dds_option > & option, float new_value ) +void lrs_device_controller::set_option( const std::shared_ptr< realdds::dds_option > & option, json const & new_value ) { auto stream = option->stream(); if( ! stream ) @@ -873,7 +961,48 @@ void lrs_device_controller::set_option( const std::shared_ptr< realdds::dds_opti throw std::runtime_error( "no stream '" + stream->name() + "' in device" ); auto server = it->second; auto & sensor = _rs_sensors[server->sensor_name()]; - sensor.set_option( option_name_to_id( option->get_name() ), new_value ); + if( auto roi_option = std::dynamic_pointer_cast< realdds::dds_rect_option >( option ) ) + { + // ROI has its own API in librealsense + if( auto roi_sensor = rs2::roi_sensor( sensor ) ) + roi_sensor.set_region_of_interest( roi_from_json( roi_option->get_value() ) ); + else + throw std::runtime_error( "rect option in sensor that has no ROI" ); + } + else + { + // The librealsense API uses floats, so we need to convert from non-floats + float float_value; + switch( new_value.type() ) + { + case json::value_t::number_float: + case json::value_t::number_integer: + case json::value_t::number_unsigned: + float_value = new_value; + break; + + case json::value_t::boolean: + float_value = new_value.get< bool >(); + break; + + case json::value_t::string: + // Only way is for this to be an enum... + if( auto e = std::dynamic_pointer_cast< realdds::dds_enum_option >( option ) ) + { + auto const & choices = e->get_choices(); + auto it = std::find( choices.begin(), choices.end(), new_value.string_ref() ); + if( it == choices.end() ) + throw std::runtime_error( rsutils::string::from() << "not a valid enum value: " << new_value ); + float_value = float( it - choices.begin() ); + break; + } + // fall thru + default: + throw std::runtime_error( rsutils::string::from() << "unsupported value: " << new_value ); + } + + sensor.set_option( option_name_to_id( option->get_name() ), float_value ); + } } @@ -890,6 +1019,13 @@ json lrs_device_controller::query_option( const std::shared_ptr< realdds::dds_op auto & sensor = _rs_sensors[server->sensor_name()]; try { + if( auto roi_option = std::dynamic_pointer_cast< realdds::dds_rect_option >( option ) ) + { + if( auto roi_sensor = rs2::roi_sensor( sensor ) ) + return json_from_roi( roi_sensor.get_region_of_interest() ); + else + throw std::runtime_error( "rect option in sensor that has no ROI" ); + } return sensor.get_option( option_name_to_id( option->get_name() ) ); } catch( rs2::invalid_value_error const & ) diff --git a/tools/dds/dds-adapter/lrs-device-controller.h b/tools/dds/dds-adapter/lrs-device-controller.h index 74c11d78d7..0f091b0319 100644 --- a/tools/dds/dds-adapter/lrs-device-controller.h +++ b/tools/dds/dds-adapter/lrs-device-controller.h @@ -32,7 +32,7 @@ class lrs_device_controller : public std::enable_shared_from_this< lrs_device_co lrs_device_controller( rs2::device dev, std::shared_ptr< realdds::dds_device_server > dds_device_server ); ~lrs_device_controller(); - void set_option( const std::shared_ptr< realdds::dds_option > & option, float new_value ); + void set_option( const std::shared_ptr< realdds::dds_option > & option, rsutils::json const & new_value ); rsutils::json query_option( const std::shared_ptr< realdds::dds_option > & option ); bool is_recovery() const; diff --git a/unit-tests/dds/test-option-value.py b/unit-tests/dds/test-option-value.py index 635583865c..85c87aeda0 100644 --- a/unit-tests/dds/test-option-value.py +++ b/unit-tests/dds/test-option-value.py @@ -160,6 +160,26 @@ dds.option.from_json( ['ip', '1.2.3', 'desc', ['IPv4']] ), RuntimeError, 'not an IP address: "1.2.3"' ) +with test.closure( 'rect' ): + test.check_equal( dds.option.from_json( ['r', [0,0,1,1], 'r/o'] ).value_type(), 'rect' ) + test.check_equal( dds.option.from_json( ['r', None, 'r/o', ['rect','optional']] ).value_type(), 'rect' ) + test.check_equal( dds.option.from_json( ['r', [0,1,2,3], 'r/o'] ).get_value(), [0,1,2,3] ) + test.check_equal( dds.option.from_json( ['r', [0,1,2,3], [1,2,3,4], 'r/w'] ).value_type(), 'rect' ) + test.check_equal( dds.option.from_json( ['r', [0,1,2,3], [1,2,3,4], 'r/w'] ).get_default_value(), [1,2,3,4] ) + test.check_throws( lambda: # non-integer inside the default value + dds.option.from_json( ['r', [0,1,2,3], [1,2,3.,4], 'r/w'] ), + RuntimeError, 'cannot deduce value type: ["r",[0,1,2,3],[1,2,3.0,4],"r/w"]' ) + test.check_throws( lambda: # no range for rect + dds.option.from_json( ['r', [0,1,2,3], 0, 2, 1, [1,2,3,4], 'r/w'] ), + RuntimeError, 'not [x1,y1,x2,y2]: 0' ) + test.check_throws( lambda: # with 5 args, it looks like an enum + dds.option.from_json( ['r', [0,1,2,3], [1,2,3,4], [1,2,3,4], 'r/w'] ), + RuntimeError, 'non-string enum values' ) + # With a range supplied, operator<() for JSON takes effect + test.check_equal( dds.option.from_json( ['r', [1,2,3,4], [0,1,2,3], [2,3,4,5], [3,4,5,6], [2,3,4,5], 'r/w'] ).value_type(), 'rect' ) + test.check_throws( lambda: + dds.option.from_json( ['r', [1,2,3,4], [0,1,2,3.], [2,3,4,5], [3,4,5,6], [2,3,4,5], 'r/w'] ), + RuntimeError, 'non-integers found: [0,1,2,3.0]' ) ############################################################################################# test.print_results()