pub struct PropertyObject { /* private fields */ }Expand description
Safe wrapper for TestStand™ PropertyObject (IPropertyObject).
Implementations§
Source§impl PropertyObject
impl PropertyObject
Sourcepub fn exists(&self, lookup_string: &str, options: i32) -> Result<bool, Error>
pub fn exists(&self, lookup_string: &str, options: i32) -> Result<bool, Error>
Checks if a property exists by lookup path (PropertyObject.Exists).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
23fn set_string(
24 container: &PropertyObject,
25 name: &str,
26 value: &str,
27) -> Result<(), rs_teststand::Error> {
28 if !container.exists(name, 0)? {
29 container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
30 }
31 container.set_val_string(name, 0, value)
32}
33
34/// Creates a variable, retypes it, clones it, then removes both.
35///
36/// The type is fixed at creation, so each "retype" is a delete followed by a
37/// fresh create, the same thing the sequence editor does behind the scenes.
38fn temporary_variable_lifecycle(container: &PropertyObject) -> Result<(), rs_teststand::Error> {
39 let name = "TempScratch";
40 let clone_name = "TempScratchCopy";
41 for stale in [name, clone_name] {
42 if container.exists(stale, 0)? {
43 container.delete_sub_property(stale, 0)?;
44 }
45 }
46
47 container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
48 container.set_val_string(name, 0, "scratch")?;
49 println!(
50 " created {name} (String) = '{}'",
51 container.get_val_string(name, 0)?
52 );
53
54 container.delete_sub_property(name, 0)?;
55 container.new_sub_property(name, PropValType::Number, false, "", INSERT_IF_MISSING)?;
56 container.set_val_number(name, 0, 42.0)?;
57 println!(
58 " retyped {name} -> Number = {}",
59 container.get_val_number(name, 0)?
60 );
61
62 // clone copies value and type; set_property_object attaches it under a new name.
63 let copy = container.clone_property(name, 0)?;
64 container.set_property_object(clone_name, INSERT_IF_MISSING, ©)?;
65 println!(
66 " cloned {name} -> {clone_name} = {}",
67 container.get_val_number(clone_name, 0)?
68 );
69
70 container.delete_sub_property(clone_name, 0)?;
71 container.delete_sub_property(name, 0)?;
72 println!(
73 " removed both: {name} exists={}, {clone_name} exists={}",
74 container.exists(name, 0)?,
75 container.exists(clone_name, 0)?
76 );
77 Ok(())
78}
79
80/// Opens a sequence file if one was named on the command line.
81///
82/// Locals, parameters and file globals all need a real file; without one the
83/// example still demonstrates station globals.
84fn open_sequence_file(engine: &Engine) -> Result<Option<SequenceFile>, rs_teststand::Error> {
85 let Some(path) = std::env::args().nth(1) else {
86 return Ok(None);
87 };
88 let file = engine.get_sequence_file_ex(
89 &path,
90 GetSeqFileOptions::DO_NOT_RUN_LOAD_CALLBACK,
91 ConflictHandler::Error,
92 )?;
93 Ok(Some(file))
94}
95
96/// Everything that lives on the station rather than in a file.
97///
98/// Separated because it is the one scope that outlives the process, so it is
99/// the one worth reading on its own.
100fn show_station_globals(engine: &Engine) -> Result<(), rs_teststand::Error> {
101 // Station globals: shared by every sequence file, and written to disk.
102 let station_globals = engine.globals()?;
103 if !station_globals.exists("StationInfo", 0)? {
104 station_globals.new_sub_property(
105 "StationInfo",
106 PropValType::Container,
107 false,
108 "",
109 INSERT_IF_MISSING,
110 )?;
111 }
112 let station_info = station_globals.get_property_object("StationInfo", 0)?;
113
114 // A container holds mixed types, so one station record can carry the name,
115 // a count and a flag without three separate globals.
116 set_string(&station_info, "StationName", "STATION_01")?;
117 station_info.set_val_number("CalibrationIntervalDays", INSERT_IF_MISSING, 90.0)?;
118 station_info.set_val_bool("FixtureInstalled", INSERT_IF_MISSING, true)?;
119
120 // A 64-bit count. `SetValNumber` stores a double, which starts losing whole
121 // numbers past 2^53, so anything that counts for the life of a station wants
122 // the integer path instead.
123 station_info.set_val_integer64("UnitsTestedTotal", INSERT_IF_MISSING, 9_007_199_254_740_993)?;
124
125 // A nested container, because station data is rarely one level deep.
126 if !station_info.exists("LastCalibration", 0)? {
127 station_info.new_sub_property(
128 "LastCalibration",
129 PropValType::Container,
130 false,
131 "",
132 INSERT_IF_MISSING,
133 )?;
134 }
135 let calibration = station_info.get_property_object("LastCalibration", 0)?;
136 set_string(&calibration, "Technician", "R. Alvarez")?;
137 set_string(&calibration, "Date", "2026-06-14")?;
138
139 println!("StationGlobals.StationInfo:");
140 println!(
141 " StationName = '{}'",
142 station_info.get_val_string("StationName", 0)?
143 );
144 println!(
145 " CalibrationIntervalDays = {}",
146 station_info.get_val_number("CalibrationIntervalDays", 0)?
147 );
148 println!(
149 " FixtureInstalled = {}",
150 station_info.get_val_bool("FixtureInstalled", 0)?
151 );
152 println!(
153 " UnitsTestedTotal = {}",
154 station_info.get_val_integer64("UnitsTestedTotal", 0)?
155 );
156 println!(
157 " LastCalibration.Technician = '{}' on {}",
158 calibration.get_val_string("Technician", 0)?,
159 calibration.get_val_string("Date", 0)?
160 );
161
162 // Walk the container rather than naming each field, which is what a host
163 // does when it does not know the shape in advance.
164 println!(
165 " walked, {} field(s):",
166 station_info.get_num_sub_properties("")?
167 );
168 for index in 0..station_info.get_num_sub_properties("")? {
169 println!(
170 " {}",
171 station_info.get_nth_sub_property_name("", index, 0)?
172 );
173 }
174
175 // Station globals live in memory until this is called. Without it the values
176 // above are gone when the engine goes away, which is the difference between
177 // a station global and a file global.
178 // `false` means do not prompt if another process changed the file first.
179 // An example must never raise a dialog, and neither must a headless host.
180 engine.commit_globals_to_disk(false)?;
181 println!(" committed to disk");
182
183 Ok(())
184}More examples
46fn describe(sequence: &Sequence) -> Result<(), rs_teststand::Error> {
47 let count = sequence.get_num_steps(StepGroup::Main)?;
48 println!("{} holds {count} step(s) in Main:", sequence.name()?);
49 for index in 0..count {
50 let step = sequence.get_step(index, StepGroup::Main)?;
51 let properties = step.as_property_object()?;
52 println!(" [{index}] {}", step.name()?);
53 if properties.exists("Limits.Low", NO_OPTIONS)? {
54 println!(
55 " limits: low={}, high={}",
56 properties.get_val_number("Limits.Low", NO_OPTIONS)?,
57 properties.get_val_number("Limits.High", NO_OPTIONS)?
58 );
59 }
60 let precondition = step.precondition()?;
61 if !precondition.is_empty() {
62 println!(" runs when: {precondition}");
63 }
64 }
65 Ok(())
66}108fn run(engine: &Engine, sequence_file: &SequenceFile, entry_point: &str) -> Result<(), Error> {
109 let execution = engine.new_execution(sequence_file, entry_point, None, false, 0)?;
110 println!("\nRunning {entry_point}...");
111
112 if !wait_for_end(engine, RUN_DEADLINE)? {
113 println!(" did not finish within {RUN_DEADLINE:?}; terminating");
114 execution.terminate()?;
115 return Ok(());
116 }
117 println!(" status: {}", execution.result_status()?);
118
119 let results = execution.result_object()?;
120 if !results.exists("ResultList", none())? {
121 println!(" no results recorded");
122 return Ok(());
123 }
124 let result_list = results.get_property_object("ResultList", none())?;
125 for index in 0..result_list.get_num_elements()? {
126 let entry = result_list.get_property_object_by_offset(index, none())?;
127 println!(
128 " {}: {}",
129 entry
130 .get_val_string("TS.StepName", none())
131 .unwrap_or_else(|_| "<unnamed>".to_owned()),
132 entry
133 .get_val_string("Status", none())
134 .unwrap_or_else(|_| "<no status>".to_owned())
135 );
136 }
137 Ok(())
138}114fn report(results: &rs_teststand::PropertyObject) -> Result<(), Error> {
115 if !results.exists("ResultList", none())? {
116 println!("No results recorded.");
117 return Ok(());
118 }
119 let result_list = results.get_property_object("ResultList", none())?;
120 let count = result_list.get_num_elements()?;
121 println!(
122 "\n{count} of {} defined test(s) recorded a result:",
123 TESTS.len()
124 );
125 if count < i32::try_from(TESTS.len()).unwrap_or(i32::MAX) {
126 println!(" (a failure ended the run before the rest could execute)");
127 }
128
129 for index in 0..count {
130 let entry = result_list.get_property_object_by_offset(index, none())?;
131 let name = entry
132 .get_val_string("TS.StepName", none())
133 .unwrap_or_else(|_| "<unnamed>".to_owned());
134 let status = entry
135 .get_val_string("Status", none())
136 .unwrap_or_else(|_| "<no status>".to_owned());
137 // Only test steps record a measurement; an Action has no Numeric.
138 match entry.get_val_number("Numeric", none()) {
139 Ok(measured) => println!(" {name}: {status} (measured {measured})"),
140 Err(_) => println!(" {name}: {status}"),
141 }
142 }
143 Ok(())
144}Sourcepub fn get_val_string(
&self,
lookup_string: &str,
options: i32,
) -> Result<String, Error>
pub fn get_val_string( &self, lookup_string: &str, options: i32, ) -> Result<String, Error>
Reads a string property by lookup path (PropertyObject.GetValString).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
108fn run(engine: &Engine, sequence_file: &SequenceFile, entry_point: &str) -> Result<(), Error> {
109 let execution = engine.new_execution(sequence_file, entry_point, None, false, 0)?;
110 println!("\nRunning {entry_point}...");
111
112 if !wait_for_end(engine, RUN_DEADLINE)? {
113 println!(" did not finish within {RUN_DEADLINE:?}; terminating");
114 execution.terminate()?;
115 return Ok(());
116 }
117 println!(" status: {}", execution.result_status()?);
118
119 let results = execution.result_object()?;
120 if !results.exists("ResultList", none())? {
121 println!(" no results recorded");
122 return Ok(());
123 }
124 let result_list = results.get_property_object("ResultList", none())?;
125 for index in 0..result_list.get_num_elements()? {
126 let entry = result_list.get_property_object_by_offset(index, none())?;
127 println!(
128 " {}: {}",
129 entry
130 .get_val_string("TS.StepName", none())
131 .unwrap_or_else(|_| "<unnamed>".to_owned()),
132 entry
133 .get_val_string("Status", none())
134 .unwrap_or_else(|_| "<no status>".to_owned())
135 );
136 }
137 Ok(())
138}
139
140fn main() -> Result<(), Box<dyn std::error::Error>> {
141 let engine = Engine::new()?;
142 engine.set_ui_message_polling_enabled(true)?;
143
144 let sequence_file = build(&engine)?;
145 println!("File holds {} sequence(s).", sequence_file.num_sequences()?);
146
147 // The conventional entry point.
148 run(&engine, &sequence_file, "MainSequence")?;
149
150 // A subsequence run on its own has no caller, so nothing supplies its
151 // parameters, they keep whatever default the sequence carries. Setting
152 // that default is therefore how a direct run is given its input.
153 let diagnostics = sequence_file.get_sequence_by_name(SUBSEQUENCE)?;
154 diagnostics
155 .parameters()?
156 .set_val_string(PARAMETER, none(), "FIXTURE-07")?;
157 println!(
158 "\n{SUBSEQUENCE}.{PARAMETER} default is now {:?}",
159 diagnostics
160 .parameters()?
161 .get_val_string(PARAMETER, none())?
162 );
163
164 run(&engine, &sequence_file, SUBSEQUENCE)?;
165
166 engine.release_sequence_file_ex(sequence_file, none())?;
167 Ok(())
168}More examples
114fn report(results: &rs_teststand::PropertyObject) -> Result<(), Error> {
115 if !results.exists("ResultList", none())? {
116 println!("No results recorded.");
117 return Ok(());
118 }
119 let result_list = results.get_property_object("ResultList", none())?;
120 let count = result_list.get_num_elements()?;
121 println!(
122 "\n{count} of {} defined test(s) recorded a result:",
123 TESTS.len()
124 );
125 if count < i32::try_from(TESTS.len()).unwrap_or(i32::MAX) {
126 println!(" (a failure ended the run before the rest could execute)");
127 }
128
129 for index in 0..count {
130 let entry = result_list.get_property_object_by_offset(index, none())?;
131 let name = entry
132 .get_val_string("TS.StepName", none())
133 .unwrap_or_else(|_| "<unnamed>".to_owned());
134 let status = entry
135 .get_val_string("Status", none())
136 .unwrap_or_else(|_| "<no status>".to_owned());
137 // Only test steps record a measurement; an Action has no Numeric.
138 match entry.get_val_number("Numeric", none()) {
139 Ok(measured) => println!(" {name}: {status} (measured {measured})"),
140 Err(_) => println!(" {name}: {status}"),
141 }
142 }
143 Ok(())
144}38fn temporary_variable_lifecycle(container: &PropertyObject) -> Result<(), rs_teststand::Error> {
39 let name = "TempScratch";
40 let clone_name = "TempScratchCopy";
41 for stale in [name, clone_name] {
42 if container.exists(stale, 0)? {
43 container.delete_sub_property(stale, 0)?;
44 }
45 }
46
47 container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
48 container.set_val_string(name, 0, "scratch")?;
49 println!(
50 " created {name} (String) = '{}'",
51 container.get_val_string(name, 0)?
52 );
53
54 container.delete_sub_property(name, 0)?;
55 container.new_sub_property(name, PropValType::Number, false, "", INSERT_IF_MISSING)?;
56 container.set_val_number(name, 0, 42.0)?;
57 println!(
58 " retyped {name} -> Number = {}",
59 container.get_val_number(name, 0)?
60 );
61
62 // clone copies value and type; set_property_object attaches it under a new name.
63 let copy = container.clone_property(name, 0)?;
64 container.set_property_object(clone_name, INSERT_IF_MISSING, ©)?;
65 println!(
66 " cloned {name} -> {clone_name} = {}",
67 container.get_val_number(clone_name, 0)?
68 );
69
70 container.delete_sub_property(clone_name, 0)?;
71 container.delete_sub_property(name, 0)?;
72 println!(
73 " removed both: {name} exists={}, {clone_name} exists={}",
74 container.exists(name, 0)?,
75 container.exists(clone_name, 0)?
76 );
77 Ok(())
78}
79
80/// Opens a sequence file if one was named on the command line.
81///
82/// Locals, parameters and file globals all need a real file; without one the
83/// example still demonstrates station globals.
84fn open_sequence_file(engine: &Engine) -> Result<Option<SequenceFile>, rs_teststand::Error> {
85 let Some(path) = std::env::args().nth(1) else {
86 return Ok(None);
87 };
88 let file = engine.get_sequence_file_ex(
89 &path,
90 GetSeqFileOptions::DO_NOT_RUN_LOAD_CALLBACK,
91 ConflictHandler::Error,
92 )?;
93 Ok(Some(file))
94}
95
96/// Everything that lives on the station rather than in a file.
97///
98/// Separated because it is the one scope that outlives the process, so it is
99/// the one worth reading on its own.
100fn show_station_globals(engine: &Engine) -> Result<(), rs_teststand::Error> {
101 // Station globals: shared by every sequence file, and written to disk.
102 let station_globals = engine.globals()?;
103 if !station_globals.exists("StationInfo", 0)? {
104 station_globals.new_sub_property(
105 "StationInfo",
106 PropValType::Container,
107 false,
108 "",
109 INSERT_IF_MISSING,
110 )?;
111 }
112 let station_info = station_globals.get_property_object("StationInfo", 0)?;
113
114 // A container holds mixed types, so one station record can carry the name,
115 // a count and a flag without three separate globals.
116 set_string(&station_info, "StationName", "STATION_01")?;
117 station_info.set_val_number("CalibrationIntervalDays", INSERT_IF_MISSING, 90.0)?;
118 station_info.set_val_bool("FixtureInstalled", INSERT_IF_MISSING, true)?;
119
120 // A 64-bit count. `SetValNumber` stores a double, which starts losing whole
121 // numbers past 2^53, so anything that counts for the life of a station wants
122 // the integer path instead.
123 station_info.set_val_integer64("UnitsTestedTotal", INSERT_IF_MISSING, 9_007_199_254_740_993)?;
124
125 // A nested container, because station data is rarely one level deep.
126 if !station_info.exists("LastCalibration", 0)? {
127 station_info.new_sub_property(
128 "LastCalibration",
129 PropValType::Container,
130 false,
131 "",
132 INSERT_IF_MISSING,
133 )?;
134 }
135 let calibration = station_info.get_property_object("LastCalibration", 0)?;
136 set_string(&calibration, "Technician", "R. Alvarez")?;
137 set_string(&calibration, "Date", "2026-06-14")?;
138
139 println!("StationGlobals.StationInfo:");
140 println!(
141 " StationName = '{}'",
142 station_info.get_val_string("StationName", 0)?
143 );
144 println!(
145 " CalibrationIntervalDays = {}",
146 station_info.get_val_number("CalibrationIntervalDays", 0)?
147 );
148 println!(
149 " FixtureInstalled = {}",
150 station_info.get_val_bool("FixtureInstalled", 0)?
151 );
152 println!(
153 " UnitsTestedTotal = {}",
154 station_info.get_val_integer64("UnitsTestedTotal", 0)?
155 );
156 println!(
157 " LastCalibration.Technician = '{}' on {}",
158 calibration.get_val_string("Technician", 0)?,
159 calibration.get_val_string("Date", 0)?
160 );
161
162 // Walk the container rather than naming each field, which is what a host
163 // does when it does not know the shape in advance.
164 println!(
165 " walked, {} field(s):",
166 station_info.get_num_sub_properties("")?
167 );
168 for index in 0..station_info.get_num_sub_properties("")? {
169 println!(
170 " {}",
171 station_info.get_nth_sub_property_name("", index, 0)?
172 );
173 }
174
175 // Station globals live in memory until this is called. Without it the values
176 // above are gone when the engine goes away, which is the difference between
177 // a station global and a file global.
178 // `false` means do not prompt if another process changed the file first.
179 // An example must never raise a dialog, and neither must a headless host.
180 engine.commit_globals_to_disk(false)?;
181 println!(" committed to disk");
182
183 Ok(())
184}
185
186fn main() -> Result<(), rs_teststand::Error> {
187 let engine = Engine::new()?;
188
189 show_station_globals(&engine)?;
190
191 // The other three scopes need a sequence file.
192 if let Some(sequence_file) = open_sequence_file(&engine)? {
193 // File globals: shared by every sequence in this file. These are the
194 // defaults stored in the file; a running execution gets its own copy.
195 let file_globals = sequence_file.file_globals_default_values()?;
196 set_string(&file_globals, "BatchID", "BATCH-2026-Q2-001")?;
197 println!(
198 "FileGlobals.BatchID = '{}'",
199 file_globals.get_val_string("BatchID", 0)?
200 );
201
202 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
203
204 // Locals: private to one call of this sequence.
205 let locals = main_sequence.locals()?;
206 set_string(&locals, "OperatorName", "Alice")?;
207 println!(
208 "MainSequence.Locals.OperatorName = '{}'",
209 locals.get_val_string("OperatorName", 0)?
210 );
211
212 // Parameters: supplied by whoever calls this sequence.
213 let parameters = main_sequence.parameters()?;
214 set_string(¶meters, "DUTSerial", "SN-000000")?;
215 println!(
216 "MainSequence.Parameters.DUTSerial = '{}'",
217 parameters.get_val_string("DUTSerial", 0)?
218 );
219
220 println!("\nTemporary variable lifecycle (Locals.TempScratch):");
221 temporary_variable_lifecycle(&locals)?;
222
223 // Nothing is saved: the file is left exactly as it was found.
224 engine.release_sequence_file_ex(sequence_file, 0)?;
225 } else {
226 println!("\n(pass a .seq path to also demonstrate locals, parameters and file globals)");
227 println!("\nTemporary variable lifecycle (StationGlobals.TempScratch):");
228 temporary_variable_lifecycle(&engine.globals()?)?;
229 }
230
231 engine.commit_globals_to_disk(false)?;
232 println!("\nStation globals committed to disk.");
233 Ok(())
234}131fn apply_templates(
132 sequence_file: &SequenceFile,
133 step: &PropertyObject,
134 sequence: &PropertyObject,
135 variable: &PropertyObject,
136) -> Result<(), Error> {
137 let main_sequence = sequence_file.get_sequence_by_name(MAIN_SEQUENCE)?;
138
139 // Inserting returns the copy on the Step interface, so it can be renamed
140 // and re-identified straight away. Without a new step ID the copy would go
141 // on claiming the prototype's identity.
142 let inserted_step = main_sequence.insert_step_from_template(step, 0, StepGroup::Main)?;
143 inserted_step.set_name("Step From Template")?;
144 inserted_step.create_new_unique_step_id()?;
145 println!(" step: {}", inserted_step.name()?);
146
147 let inserted_sequence = sequence_file.insert_sequence_from_template(sequence)?;
148 inserted_sequence.create_new_unique_step_ids()?;
149 println!(
150 " sequence: {} ({} step(s))",
151 inserted_sequence.name()?,
152 inserted_sequence.get_num_steps(StepGroup::Main)?
153 );
154
155 // A variable is the easy case: Locals is a property tree, so a clone drops
156 // straight in under the template's own name.
157 let locals = main_sequence.locals()?;
158 locals.set_property_object(
159 &variable.name()?,
160 PropertyOptions::INSERT_IF_MISSING.bits(),
161 &variable.clone_property("", PropertyOptions::NONE.bits())?,
162 )?;
163 println!(
164 " variable: {} = {:?}",
165 variable.name()?,
166 locals.get_val_string(&variable.name()?, PropertyOptions::NONE.bits())?
167 );
168
169 // A file that does not believe it changed will not write anything.
170 sequence_file
171 .as_property_object_file()?
172 .inc_change_count()?;
173 Ok(())
174}65fn main() -> Result<(), Box<dyn std::error::Error>> {
66 let engine = Engine::new()?;
67 describe_templates(&engine)?;
68
69 let prototype = build_prototype(&engine)?;
70 println!(
71 "Prototype: {} via {:?}, run mode {:?}",
72 prototype.name()?,
73 prototype.adapter_key_name()?,
74 prototype.run_mode()?
75 );
76
77 let sequence_file = engine.new_sequence_file()?;
78 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
79 let template = prototype.as_property_object()?;
80
81 for copy_number in 1..=COPIES {
82 let index = main_sequence.get_num_steps(StepGroup::Main)?;
83 let inserted =
84 main_sequence.insert_step_from_template(&template, index, StepGroup::Main)?;
85 inserted.set_name(&format!("Measure {copy_number} (from template)"))?;
86 // Each copy needs an identity of its own; the clone brought the
87 // prototype's.
88 inserted.create_new_unique_step_id()?;
89 }
90
91 println!(
92 "\nMainSequence now holds {} step(s):",
93 main_sequence.get_num_steps(StepGroup::Main)?
94 );
95 for index in 0..main_sequence.get_num_steps(StepGroup::Main)? {
96 let step = main_sequence.get_step(index, StepGroup::Main)?;
97 println!(
98 " [{index}] {} -> {}",
99 step.name()?,
100 step.as_property_object()?
101 .get_val_string(VI_PATH, PropertyOptions::NONE.bits())?
102 );
103 }
104
105 // The prototype is untouched and can go on producing copies.
106 println!("\nPrototype still named: {}", prototype.name()?);
107
108 let path = std::env::temp_dir().join("rs_teststand_from_template.seq");
109 let path = path.to_string_lossy().into_owned();
110 sequence_file.save(&path)?;
111 println!("Saved to {path}");
112 Ok(())
113}73fn main() -> Result<(), Box<dyn std::error::Error>> {
74 let engine = Engine::new()?;
75
76 // A file to insert into. Built here so the example depends on nothing that
77 // has to exist on the station first.
78 let sequence_file = engine.new_sequence_file()?;
79 let subsequence = engine.new_sequence()?;
80 subsequence.set_name("CustomSubsequence")?;
81 let existing = engine.new_step(AdapterKeyName::NoneAdapter.as_str(), "Action")?;
82 existing.set_name(TARGET_STEP)?;
83 subsequence.insert_step(&existing, 0, StepGroup::Main)?;
84 sequence_file.insert_sequence(&subsequence)?;
85
86 // Where to insert: in front of the target, or at the end if it is absent.
87 let insert_at = if let Some(index) = index_of(&subsequence, TARGET_STEP, StepGroup::Main)? {
88 println!("Found {TARGET_STEP} at index {index}; inserting in front of it.");
89 index
90 } else {
91 let end = subsequence.get_num_steps(StepGroup::Main)?;
92 println!("{TARGET_STEP} not found; appending at index {end}.");
93 end
94 };
95
96 for (offset, (name, vi, project, mode)) in [
97 (
98 "Measure Voltage (VI)",
99 r"instruments.lvlibp\measure_voltage.vi",
100 "",
101 RunMode::Normal,
102 ),
103 (
104 "Measure Current (VI)",
105 r"instruments.lvlibp\measure_current.vi",
106 r"instruments.lvproj",
107 RunMode::Skip,
108 ),
109 ]
110 .into_iter()
111 .enumerate()
112 {
113 let step = vi_call_step(&engine, name, vi, project, mode)?;
114 let at = insert_at + i32::try_from(offset).unwrap_or(0);
115 subsequence.insert_step(&step, at, StepGroup::Main)?;
116 }
117
118 println!(
119 "\n{} now holds {} step(s):",
120 subsequence.name()?,
121 subsequence.get_num_steps(StepGroup::Main)?
122 );
123 for index in 0..subsequence.get_num_steps(StepGroup::Main)? {
124 let step = subsequence.get_step(index, StepGroup::Main)?;
125 let properties = step.as_property_object()?;
126 let vi = properties
127 .get_val_string(VI_PATH, none())
128 .unwrap_or_else(|_| "<no VI>".to_owned());
129 println!(
130 " [{index}] {} - {:?}, run mode {:?}, records {:?}",
131 step.name()?,
132 step.adapter_key_name()?,
133 step.run_mode()?,
134 step.result_recording_option()?
135 );
136 if vi != "<no VI>" {
137 println!(" calls {vi}");
138 }
139 }
140
141 let path = std::env::temp_dir().join("rs_teststand_step_insert.seq");
142 let path = path.to_string_lossy().into_owned();
143 sequence_file.save(&path)?;
144 println!("\nSaved to {path}");
145 Ok(())
146}Sourcepub fn set_val_string(
&self,
lookup_string: &str,
options: i32,
value: &str,
) -> Result<(), Error>
pub fn set_val_string( &self, lookup_string: &str, options: i32, value: &str, ) -> Result<(), Error>
Writes a string property by lookup path (PropertyObject.SetValString).
§Errors
Error if the COM call fails.
Examples found in repository?
23fn set_string(
24 container: &PropertyObject,
25 name: &str,
26 value: &str,
27) -> Result<(), rs_teststand::Error> {
28 if !container.exists(name, 0)? {
29 container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
30 }
31 container.set_val_string(name, 0, value)
32}
33
34/// Creates a variable, retypes it, clones it, then removes both.
35///
36/// The type is fixed at creation, so each "retype" is a delete followed by a
37/// fresh create, the same thing the sequence editor does behind the scenes.
38fn temporary_variable_lifecycle(container: &PropertyObject) -> Result<(), rs_teststand::Error> {
39 let name = "TempScratch";
40 let clone_name = "TempScratchCopy";
41 for stale in [name, clone_name] {
42 if container.exists(stale, 0)? {
43 container.delete_sub_property(stale, 0)?;
44 }
45 }
46
47 container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
48 container.set_val_string(name, 0, "scratch")?;
49 println!(
50 " created {name} (String) = '{}'",
51 container.get_val_string(name, 0)?
52 );
53
54 container.delete_sub_property(name, 0)?;
55 container.new_sub_property(name, PropValType::Number, false, "", INSERT_IF_MISSING)?;
56 container.set_val_number(name, 0, 42.0)?;
57 println!(
58 " retyped {name} -> Number = {}",
59 container.get_val_number(name, 0)?
60 );
61
62 // clone copies value and type; set_property_object attaches it under a new name.
63 let copy = container.clone_property(name, 0)?;
64 container.set_property_object(clone_name, INSERT_IF_MISSING, ©)?;
65 println!(
66 " cloned {name} -> {clone_name} = {}",
67 container.get_val_number(clone_name, 0)?
68 );
69
70 container.delete_sub_property(clone_name, 0)?;
71 container.delete_sub_property(name, 0)?;
72 println!(
73 " removed both: {name} exists={}, {clone_name} exists={}",
74 container.exists(name, 0)?,
75 container.exists(clone_name, 0)?
76 );
77 Ok(())
78}More examples
36fn add_pass_fail(
37 engine: &Engine,
38 sequence: &Sequence,
39 name: &str,
40 source: &str,
41) -> Result<(), Error> {
42 let step = engine.new_step(AdapterKeyName::NoneAdapter.as_str(), "PassFailTest")?;
43 step.set_name(name)?;
44 step.as_property_object()?
45 .set_val_string("DataSource", insert_if_missing(), source)?;
46 sequence.insert_step(
47 &step,
48 sequence.get_num_steps(StepGroup::Main)?,
49 StepGroup::Main,
50 )
51}
52
53/// Adds a numeric limit test with a fixed measurement.
54fn add_numeric(
55 engine: &Engine,
56 sequence: &Sequence,
57 name: &str,
58 source: &str,
59 low: f64,
60 high: f64,
61) -> Result<(), Error> {
62 let step = engine.new_step(AdapterKeyName::NoneAdapter.as_str(), "NumericLimitTest")?;
63 step.set_name(name)?;
64 let properties = step.as_property_object()?;
65 properties.set_val_string("DataSource", insert_if_missing(), source)?;
66 properties.set_val_number("Limits.Low", insert_if_missing(), low)?;
67 properties.set_val_number("Limits.High", insert_if_missing(), high)?;
68 sequence.insert_step(
69 &step,
70 sequence.get_num_steps(StepGroup::Main)?,
71 StepGroup::Main,
72 )
73}22fn add_statement(
23 engine: &Engine,
24 sequence: &Sequence,
25 name: &str,
26 expression: &str,
27) -> Result<(), rs_teststand::Error> {
28 let step = engine.new_step(NO_ADAPTER, "Statement")?;
29 step.set_name(name)?;
30 step.as_property_object()?
31 .set_val_string("TS.PostExpr", INSERT_IF_MISSING, expression)?;
32 sequence.insert_step(
33 &step,
34 sequence.get_num_steps(StepGroup::Main)?,
35 StepGroup::Main,
36 )?;
37 Ok(())
38}28fn build_multimeter_type(engine: &Engine) -> Result<PropertyObject, rs_teststand::Error> {
29 let data_type = engine.new_property_object(PropValType::Container, false, "", NO_OPTIONS)?;
30 data_type.set_val_number("Resolution", INSERT_IF_MISSING, 6.5)?;
31 data_type.set_val_bool("AutoZero", INSERT_IF_MISSING, false)?;
32 data_type.set_val_string("Mode", INSERT_IF_MISSING, "Voltage")?;
33 data_type.set_val_number("Range", INSERT_IF_MISSING, 100.0)?;
34 data_type.set_name("DigitalMultimeter")?;
35 Ok(data_type)
36}
37
38/// Builds the array `UpdateEnumerators` expects.
39///
40/// One container per enumerator, each carrying `EnumeratorName` and
41/// `EnumeratorValue`. Strictness is an attribute of the array, not a member of
42/// it, a strict enumeration refuses values outside the declared set.
43fn enumerator_array(
44 engine: &Engine,
45 named_values: &[(&str, f64)],
46 strict: bool,
47) -> Result<PropertyObject, rs_teststand::Error> {
48 let array = engine.new_property_object(PropValType::Container, true, "", NO_OPTIONS)?;
49 array.set_num_elements(i32::try_from(named_values.len()).unwrap_or(0), NO_OPTIONS)?;
50 for (index, (name, value)) in named_values.iter().enumerate() {
51 let element =
52 array.get_property_object_by_offset(i32::try_from(index).unwrap_or(0), NO_OPTIONS)?;
53 element.set_val_string("EnumeratorName", INSERT_IF_MISSING, name)?;
54 element.set_val_number("EnumeratorValue", INSERT_IF_MISSING, *value)?;
55 }
56 array
57 .attributes()?
58 .set_val_bool(IS_STRICT_ATTRIBUTE, INSERT_IF_MISSING, strict)?;
59 Ok(array)
60}51fn build_prototype(engine: &Engine) -> Result<Step, Error> {
52 // The maintained LabVIEW adapter. The standard-prototype key still exists
53 // but the documentation marks it obsolete in favour of this one.
54 let step = engine.new_step(AdapterKeyName::LabView.as_str(), "Action")?;
55 step.set_name("Measure (VI)")?;
56 step.set_run_mode(RunMode::Normal)?;
57 step.as_property_object()?.set_val_string(
58 VI_PATH,
59 PropertyOptions::INSERT_IF_MISSING.bits(),
60 r"example.lvlibp\measure.vi",
61 )?;
62 Ok(step)
63}Sourcepub fn get_val_number(
&self,
lookup_string: &str,
options: i32,
) -> Result<f64, Error>
pub fn get_val_number( &self, lookup_string: &str, options: i32, ) -> Result<f64, Error>
Reads a numeric property by lookup path (PropertyObject.GetValNumber).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
86fn print_enumerators(definition: &PropertyObject) -> Result<(), rs_teststand::Error> {
87 let enumerators = definition.enumerators()?;
88 let strict = enumerators
89 .attributes()?
90 .get_val_bool(IS_STRICT_ATTRIBUTE, NO_OPTIONS)?;
91 println!(
92 " {} v{} (strict={strict}):",
93 definition.name()?,
94 definition.type_version()?
95 );
96 for index in 0..enumerators.get_num_elements()? {
97 let element = enumerators.get_property_object_by_offset(index, NO_OPTIONS)?;
98 let value = element.get_val_number("", COERCE_TO_NUMBER)?;
99 println!(
100 " {} -> {value}",
101 element.get_value_display_name("", NO_OPTIONS)?
102 );
103 }
104 Ok(())
105}More examples
46fn describe(sequence: &Sequence) -> Result<(), rs_teststand::Error> {
47 let count = sequence.get_num_steps(StepGroup::Main)?;
48 println!("{} holds {count} step(s) in Main:", sequence.name()?);
49 for index in 0..count {
50 let step = sequence.get_step(index, StepGroup::Main)?;
51 let properties = step.as_property_object()?;
52 println!(" [{index}] {}", step.name()?);
53 if properties.exists("Limits.Low", NO_OPTIONS)? {
54 println!(
55 " limits: low={}, high={}",
56 properties.get_val_number("Limits.Low", NO_OPTIONS)?,
57 properties.get_val_number("Limits.High", NO_OPTIONS)?
58 );
59 }
60 let precondition = step.precondition()?;
61 if !precondition.is_empty() {
62 println!(" runs when: {precondition}");
63 }
64 }
65 Ok(())
66}114fn report(results: &rs_teststand::PropertyObject) -> Result<(), Error> {
115 if !results.exists("ResultList", none())? {
116 println!("No results recorded.");
117 return Ok(());
118 }
119 let result_list = results.get_property_object("ResultList", none())?;
120 let count = result_list.get_num_elements()?;
121 println!(
122 "\n{count} of {} defined test(s) recorded a result:",
123 TESTS.len()
124 );
125 if count < i32::try_from(TESTS.len()).unwrap_or(i32::MAX) {
126 println!(" (a failure ended the run before the rest could execute)");
127 }
128
129 for index in 0..count {
130 let entry = result_list.get_property_object_by_offset(index, none())?;
131 let name = entry
132 .get_val_string("TS.StepName", none())
133 .unwrap_or_else(|_| "<unnamed>".to_owned());
134 let status = entry
135 .get_val_string("Status", none())
136 .unwrap_or_else(|_| "<no status>".to_owned());
137 // Only test steps record a measurement; an Action has no Numeric.
138 match entry.get_val_number("Numeric", none()) {
139 Ok(measured) => println!(" {name}: {status} (measured {measured})"),
140 Err(_) => println!(" {name}: {status}"),
141 }
142 }
143 Ok(())
144}38fn temporary_variable_lifecycle(container: &PropertyObject) -> Result<(), rs_teststand::Error> {
39 let name = "TempScratch";
40 let clone_name = "TempScratchCopy";
41 for stale in [name, clone_name] {
42 if container.exists(stale, 0)? {
43 container.delete_sub_property(stale, 0)?;
44 }
45 }
46
47 container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
48 container.set_val_string(name, 0, "scratch")?;
49 println!(
50 " created {name} (String) = '{}'",
51 container.get_val_string(name, 0)?
52 );
53
54 container.delete_sub_property(name, 0)?;
55 container.new_sub_property(name, PropValType::Number, false, "", INSERT_IF_MISSING)?;
56 container.set_val_number(name, 0, 42.0)?;
57 println!(
58 " retyped {name} -> Number = {}",
59 container.get_val_number(name, 0)?
60 );
61
62 // clone copies value and type; set_property_object attaches it under a new name.
63 let copy = container.clone_property(name, 0)?;
64 container.set_property_object(clone_name, INSERT_IF_MISSING, ©)?;
65 println!(
66 " cloned {name} -> {clone_name} = {}",
67 container.get_val_number(clone_name, 0)?
68 );
69
70 container.delete_sub_property(clone_name, 0)?;
71 container.delete_sub_property(name, 0)?;
72 println!(
73 " removed both: {name} exists={}, {clone_name} exists={}",
74 container.exists(name, 0)?,
75 container.exists(clone_name, 0)?
76 );
77 Ok(())
78}
79
80/// Opens a sequence file if one was named on the command line.
81///
82/// Locals, parameters and file globals all need a real file; without one the
83/// example still demonstrates station globals.
84fn open_sequence_file(engine: &Engine) -> Result<Option<SequenceFile>, rs_teststand::Error> {
85 let Some(path) = std::env::args().nth(1) else {
86 return Ok(None);
87 };
88 let file = engine.get_sequence_file_ex(
89 &path,
90 GetSeqFileOptions::DO_NOT_RUN_LOAD_CALLBACK,
91 ConflictHandler::Error,
92 )?;
93 Ok(Some(file))
94}
95
96/// Everything that lives on the station rather than in a file.
97///
98/// Separated because it is the one scope that outlives the process, so it is
99/// the one worth reading on its own.
100fn show_station_globals(engine: &Engine) -> Result<(), rs_teststand::Error> {
101 // Station globals: shared by every sequence file, and written to disk.
102 let station_globals = engine.globals()?;
103 if !station_globals.exists("StationInfo", 0)? {
104 station_globals.new_sub_property(
105 "StationInfo",
106 PropValType::Container,
107 false,
108 "",
109 INSERT_IF_MISSING,
110 )?;
111 }
112 let station_info = station_globals.get_property_object("StationInfo", 0)?;
113
114 // A container holds mixed types, so one station record can carry the name,
115 // a count and a flag without three separate globals.
116 set_string(&station_info, "StationName", "STATION_01")?;
117 station_info.set_val_number("CalibrationIntervalDays", INSERT_IF_MISSING, 90.0)?;
118 station_info.set_val_bool("FixtureInstalled", INSERT_IF_MISSING, true)?;
119
120 // A 64-bit count. `SetValNumber` stores a double, which starts losing whole
121 // numbers past 2^53, so anything that counts for the life of a station wants
122 // the integer path instead.
123 station_info.set_val_integer64("UnitsTestedTotal", INSERT_IF_MISSING, 9_007_199_254_740_993)?;
124
125 // A nested container, because station data is rarely one level deep.
126 if !station_info.exists("LastCalibration", 0)? {
127 station_info.new_sub_property(
128 "LastCalibration",
129 PropValType::Container,
130 false,
131 "",
132 INSERT_IF_MISSING,
133 )?;
134 }
135 let calibration = station_info.get_property_object("LastCalibration", 0)?;
136 set_string(&calibration, "Technician", "R. Alvarez")?;
137 set_string(&calibration, "Date", "2026-06-14")?;
138
139 println!("StationGlobals.StationInfo:");
140 println!(
141 " StationName = '{}'",
142 station_info.get_val_string("StationName", 0)?
143 );
144 println!(
145 " CalibrationIntervalDays = {}",
146 station_info.get_val_number("CalibrationIntervalDays", 0)?
147 );
148 println!(
149 " FixtureInstalled = {}",
150 station_info.get_val_bool("FixtureInstalled", 0)?
151 );
152 println!(
153 " UnitsTestedTotal = {}",
154 station_info.get_val_integer64("UnitsTestedTotal", 0)?
155 );
156 println!(
157 " LastCalibration.Technician = '{}' on {}",
158 calibration.get_val_string("Technician", 0)?,
159 calibration.get_val_string("Date", 0)?
160 );
161
162 // Walk the container rather than naming each field, which is what a host
163 // does when it does not know the shape in advance.
164 println!(
165 " walked, {} field(s):",
166 station_info.get_num_sub_properties("")?
167 );
168 for index in 0..station_info.get_num_sub_properties("")? {
169 println!(
170 " {}",
171 station_info.get_nth_sub_property_name("", index, 0)?
172 );
173 }
174
175 // Station globals live in memory until this is called. Without it the values
176 // above are gone when the engine goes away, which is the difference between
177 // a station global and a file global.
178 // `false` means do not prompt if another process changed the file first.
179 // An example must never raise a dialog, and neither must a headless host.
180 engine.commit_globals_to_disk(false)?;
181 println!(" committed to disk");
182
183 Ok(())
184}Sourcepub fn set_val_number(
&self,
lookup_string: &str,
options: i32,
value: f64,
) -> Result<(), Error>
pub fn set_val_number( &self, lookup_string: &str, options: i32, value: f64, ) -> Result<(), Error>
Writes a numeric property by lookup path (PropertyObject.SetValNumber).
§Errors
Error if the COM call fails.
Examples found in repository?
28fn build_multimeter_type(engine: &Engine) -> Result<PropertyObject, rs_teststand::Error> {
29 let data_type = engine.new_property_object(PropValType::Container, false, "", NO_OPTIONS)?;
30 data_type.set_val_number("Resolution", INSERT_IF_MISSING, 6.5)?;
31 data_type.set_val_bool("AutoZero", INSERT_IF_MISSING, false)?;
32 data_type.set_val_string("Mode", INSERT_IF_MISSING, "Voltage")?;
33 data_type.set_val_number("Range", INSERT_IF_MISSING, 100.0)?;
34 data_type.set_name("DigitalMultimeter")?;
35 Ok(data_type)
36}
37
38/// Builds the array `UpdateEnumerators` expects.
39///
40/// One container per enumerator, each carrying `EnumeratorName` and
41/// `EnumeratorValue`. Strictness is an attribute of the array, not a member of
42/// it, a strict enumeration refuses values outside the declared set.
43fn enumerator_array(
44 engine: &Engine,
45 named_values: &[(&str, f64)],
46 strict: bool,
47) -> Result<PropertyObject, rs_teststand::Error> {
48 let array = engine.new_property_object(PropValType::Container, true, "", NO_OPTIONS)?;
49 array.set_num_elements(i32::try_from(named_values.len()).unwrap_or(0), NO_OPTIONS)?;
50 for (index, (name, value)) in named_values.iter().enumerate() {
51 let element =
52 array.get_property_object_by_offset(i32::try_from(index).unwrap_or(0), NO_OPTIONS)?;
53 element.set_val_string("EnumeratorName", INSERT_IF_MISSING, name)?;
54 element.set_val_number("EnumeratorValue", INSERT_IF_MISSING, *value)?;
55 }
56 array
57 .attributes()?
58 .set_val_bool(IS_STRICT_ATTRIBUTE, INSERT_IF_MISSING, strict)?;
59 Ok(array)
60}More examples
28fn numeric_limit_test(
29 engine: &Engine,
30 name: &str,
31 precondition: &str,
32 low: f64,
33 high: f64,
34) -> Result<Step, rs_teststand::Error> {
35 let step = engine.new_step(NO_ADAPTER, "NumericLimitTest")?;
36 step.set_name(name)?;
37 step.set_precondition(precondition)?;
38 step.set_record_result(true)?;
39
40 let properties = step.as_property_object()?;
41 properties.set_val_number("Limits.High", INSERT_IF_MISSING, high)?;
42 properties.set_val_number("Limits.Low", INSERT_IF_MISSING, low)?;
43 Ok(step)
44}54fn add_numeric(
55 engine: &Engine,
56 sequence: &Sequence,
57 name: &str,
58 source: &str,
59 low: f64,
60 high: f64,
61) -> Result<(), Error> {
62 let step = engine.new_step(AdapterKeyName::NoneAdapter.as_str(), "NumericLimitTest")?;
63 step.set_name(name)?;
64 let properties = step.as_property_object()?;
65 properties.set_val_string("DataSource", insert_if_missing(), source)?;
66 properties.set_val_number("Limits.Low", insert_if_missing(), low)?;
67 properties.set_val_number("Limits.High", insert_if_missing(), high)?;
68 sequence.insert_step(
69 &step,
70 sequence.get_num_steps(StepGroup::Main)?,
71 StepGroup::Main,
72 )
73}62fn add_numeric_limit_test(
63 engine: &Engine,
64 sequence: &Sequence,
65 name: &str,
66 data_source: &str,
67 low: f64,
68 high: f64,
69) -> Result<(), Error> {
70 let step = engine.new_step(AdapterKeyName::NoneAdapter.as_str(), "NumericLimitTest")?;
71 step.set_name(name)?;
72 step.set_record_result(true)?;
73
74 let properties = step.as_property_object()?;
75 properties.set_val_string("DataSource", insert_if_missing(), data_source)?;
76 properties.set_val_number("Limits.Low", insert_if_missing(), low)?;
77 properties.set_val_number("Limits.High", insert_if_missing(), high)?;
78
79 sequence.insert_step(
80 &step,
81 sequence.get_num_steps(StepGroup::Main)?,
82 StepGroup::Main,
83 )
84}38fn temporary_variable_lifecycle(container: &PropertyObject) -> Result<(), rs_teststand::Error> {
39 let name = "TempScratch";
40 let clone_name = "TempScratchCopy";
41 for stale in [name, clone_name] {
42 if container.exists(stale, 0)? {
43 container.delete_sub_property(stale, 0)?;
44 }
45 }
46
47 container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
48 container.set_val_string(name, 0, "scratch")?;
49 println!(
50 " created {name} (String) = '{}'",
51 container.get_val_string(name, 0)?
52 );
53
54 container.delete_sub_property(name, 0)?;
55 container.new_sub_property(name, PropValType::Number, false, "", INSERT_IF_MISSING)?;
56 container.set_val_number(name, 0, 42.0)?;
57 println!(
58 " retyped {name} -> Number = {}",
59 container.get_val_number(name, 0)?
60 );
61
62 // clone copies value and type; set_property_object attaches it under a new name.
63 let copy = container.clone_property(name, 0)?;
64 container.set_property_object(clone_name, INSERT_IF_MISSING, ©)?;
65 println!(
66 " cloned {name} -> {clone_name} = {}",
67 container.get_val_number(clone_name, 0)?
68 );
69
70 container.delete_sub_property(clone_name, 0)?;
71 container.delete_sub_property(name, 0)?;
72 println!(
73 " removed both: {name} exists={}, {clone_name} exists={}",
74 container.exists(name, 0)?,
75 container.exists(clone_name, 0)?
76 );
77 Ok(())
78}
79
80/// Opens a sequence file if one was named on the command line.
81///
82/// Locals, parameters and file globals all need a real file; without one the
83/// example still demonstrates station globals.
84fn open_sequence_file(engine: &Engine) -> Result<Option<SequenceFile>, rs_teststand::Error> {
85 let Some(path) = std::env::args().nth(1) else {
86 return Ok(None);
87 };
88 let file = engine.get_sequence_file_ex(
89 &path,
90 GetSeqFileOptions::DO_NOT_RUN_LOAD_CALLBACK,
91 ConflictHandler::Error,
92 )?;
93 Ok(Some(file))
94}
95
96/// Everything that lives on the station rather than in a file.
97///
98/// Separated because it is the one scope that outlives the process, so it is
99/// the one worth reading on its own.
100fn show_station_globals(engine: &Engine) -> Result<(), rs_teststand::Error> {
101 // Station globals: shared by every sequence file, and written to disk.
102 let station_globals = engine.globals()?;
103 if !station_globals.exists("StationInfo", 0)? {
104 station_globals.new_sub_property(
105 "StationInfo",
106 PropValType::Container,
107 false,
108 "",
109 INSERT_IF_MISSING,
110 )?;
111 }
112 let station_info = station_globals.get_property_object("StationInfo", 0)?;
113
114 // A container holds mixed types, so one station record can carry the name,
115 // a count and a flag without three separate globals.
116 set_string(&station_info, "StationName", "STATION_01")?;
117 station_info.set_val_number("CalibrationIntervalDays", INSERT_IF_MISSING, 90.0)?;
118 station_info.set_val_bool("FixtureInstalled", INSERT_IF_MISSING, true)?;
119
120 // A 64-bit count. `SetValNumber` stores a double, which starts losing whole
121 // numbers past 2^53, so anything that counts for the life of a station wants
122 // the integer path instead.
123 station_info.set_val_integer64("UnitsTestedTotal", INSERT_IF_MISSING, 9_007_199_254_740_993)?;
124
125 // A nested container, because station data is rarely one level deep.
126 if !station_info.exists("LastCalibration", 0)? {
127 station_info.new_sub_property(
128 "LastCalibration",
129 PropValType::Container,
130 false,
131 "",
132 INSERT_IF_MISSING,
133 )?;
134 }
135 let calibration = station_info.get_property_object("LastCalibration", 0)?;
136 set_string(&calibration, "Technician", "R. Alvarez")?;
137 set_string(&calibration, "Date", "2026-06-14")?;
138
139 println!("StationGlobals.StationInfo:");
140 println!(
141 " StationName = '{}'",
142 station_info.get_val_string("StationName", 0)?
143 );
144 println!(
145 " CalibrationIntervalDays = {}",
146 station_info.get_val_number("CalibrationIntervalDays", 0)?
147 );
148 println!(
149 " FixtureInstalled = {}",
150 station_info.get_val_bool("FixtureInstalled", 0)?
151 );
152 println!(
153 " UnitsTestedTotal = {}",
154 station_info.get_val_integer64("UnitsTestedTotal", 0)?
155 );
156 println!(
157 " LastCalibration.Technician = '{}' on {}",
158 calibration.get_val_string("Technician", 0)?,
159 calibration.get_val_string("Date", 0)?
160 );
161
162 // Walk the container rather than naming each field, which is what a host
163 // does when it does not know the shape in advance.
164 println!(
165 " walked, {} field(s):",
166 station_info.get_num_sub_properties("")?
167 );
168 for index in 0..station_info.get_num_sub_properties("")? {
169 println!(
170 " {}",
171 station_info.get_nth_sub_property_name("", index, 0)?
172 );
173 }
174
175 // Station globals live in memory until this is called. Without it the values
176 // above are gone when the engine goes away, which is the difference between
177 // a station global and a file global.
178 // `false` means do not prompt if another process changed the file first.
179 // An example must never raise a dialog, and neither must a headless host.
180 engine.commit_globals_to_disk(false)?;
181 println!(" committed to disk");
182
183 Ok(())
184}Sourcepub fn get_val_bool(
&self,
lookup_string: &str,
options: i32,
) -> Result<bool, Error>
pub fn get_val_bool( &self, lookup_string: &str, options: i32, ) -> Result<bool, Error>
Reads a boolean property by lookup path (PropertyObject.GetValBoolean).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
86fn print_enumerators(definition: &PropertyObject) -> Result<(), rs_teststand::Error> {
87 let enumerators = definition.enumerators()?;
88 let strict = enumerators
89 .attributes()?
90 .get_val_bool(IS_STRICT_ATTRIBUTE, NO_OPTIONS)?;
91 println!(
92 " {} v{} (strict={strict}):",
93 definition.name()?,
94 definition.type_version()?
95 );
96 for index in 0..enumerators.get_num_elements()? {
97 let element = enumerators.get_property_object_by_offset(index, NO_OPTIONS)?;
98 let value = element.get_val_number("", COERCE_TO_NUMBER)?;
99 println!(
100 " {} -> {value}",
101 element.get_value_display_name("", NO_OPTIONS)?
102 );
103 }
104 Ok(())
105}More examples
100fn show_station_globals(engine: &Engine) -> Result<(), rs_teststand::Error> {
101 // Station globals: shared by every sequence file, and written to disk.
102 let station_globals = engine.globals()?;
103 if !station_globals.exists("StationInfo", 0)? {
104 station_globals.new_sub_property(
105 "StationInfo",
106 PropValType::Container,
107 false,
108 "",
109 INSERT_IF_MISSING,
110 )?;
111 }
112 let station_info = station_globals.get_property_object("StationInfo", 0)?;
113
114 // A container holds mixed types, so one station record can carry the name,
115 // a count and a flag without three separate globals.
116 set_string(&station_info, "StationName", "STATION_01")?;
117 station_info.set_val_number("CalibrationIntervalDays", INSERT_IF_MISSING, 90.0)?;
118 station_info.set_val_bool("FixtureInstalled", INSERT_IF_MISSING, true)?;
119
120 // A 64-bit count. `SetValNumber` stores a double, which starts losing whole
121 // numbers past 2^53, so anything that counts for the life of a station wants
122 // the integer path instead.
123 station_info.set_val_integer64("UnitsTestedTotal", INSERT_IF_MISSING, 9_007_199_254_740_993)?;
124
125 // A nested container, because station data is rarely one level deep.
126 if !station_info.exists("LastCalibration", 0)? {
127 station_info.new_sub_property(
128 "LastCalibration",
129 PropValType::Container,
130 false,
131 "",
132 INSERT_IF_MISSING,
133 )?;
134 }
135 let calibration = station_info.get_property_object("LastCalibration", 0)?;
136 set_string(&calibration, "Technician", "R. Alvarez")?;
137 set_string(&calibration, "Date", "2026-06-14")?;
138
139 println!("StationGlobals.StationInfo:");
140 println!(
141 " StationName = '{}'",
142 station_info.get_val_string("StationName", 0)?
143 );
144 println!(
145 " CalibrationIntervalDays = {}",
146 station_info.get_val_number("CalibrationIntervalDays", 0)?
147 );
148 println!(
149 " FixtureInstalled = {}",
150 station_info.get_val_bool("FixtureInstalled", 0)?
151 );
152 println!(
153 " UnitsTestedTotal = {}",
154 station_info.get_val_integer64("UnitsTestedTotal", 0)?
155 );
156 println!(
157 " LastCalibration.Technician = '{}' on {}",
158 calibration.get_val_string("Technician", 0)?,
159 calibration.get_val_string("Date", 0)?
160 );
161
162 // Walk the container rather than naming each field, which is what a host
163 // does when it does not know the shape in advance.
164 println!(
165 " walked, {} field(s):",
166 station_info.get_num_sub_properties("")?
167 );
168 for index in 0..station_info.get_num_sub_properties("")? {
169 println!(
170 " {}",
171 station_info.get_nth_sub_property_name("", index, 0)?
172 );
173 }
174
175 // Station globals live in memory until this is called. Without it the values
176 // above are gone when the engine goes away, which is the difference between
177 // a station global and a file global.
178 // `false` means do not prompt if another process changed the file first.
179 // An example must never raise a dialog, and neither must a headless host.
180 engine.commit_globals_to_disk(false)?;
181 println!(" committed to disk");
182
183 Ok(())
184}Sourcepub fn set_val_bool(
&self,
lookup_string: &str,
options: i32,
value: bool,
) -> Result<(), Error>
pub fn set_val_bool( &self, lookup_string: &str, options: i32, value: bool, ) -> Result<(), Error>
Writes a boolean property by lookup path (PropertyObject.SetValBoolean).
§Errors
Error if the COM call fails.
Examples found in repository?
28fn build_multimeter_type(engine: &Engine) -> Result<PropertyObject, rs_teststand::Error> {
29 let data_type = engine.new_property_object(PropValType::Container, false, "", NO_OPTIONS)?;
30 data_type.set_val_number("Resolution", INSERT_IF_MISSING, 6.5)?;
31 data_type.set_val_bool("AutoZero", INSERT_IF_MISSING, false)?;
32 data_type.set_val_string("Mode", INSERT_IF_MISSING, "Voltage")?;
33 data_type.set_val_number("Range", INSERT_IF_MISSING, 100.0)?;
34 data_type.set_name("DigitalMultimeter")?;
35 Ok(data_type)
36}
37
38/// Builds the array `UpdateEnumerators` expects.
39///
40/// One container per enumerator, each carrying `EnumeratorName` and
41/// `EnumeratorValue`. Strictness is an attribute of the array, not a member of
42/// it, a strict enumeration refuses values outside the declared set.
43fn enumerator_array(
44 engine: &Engine,
45 named_values: &[(&str, f64)],
46 strict: bool,
47) -> Result<PropertyObject, rs_teststand::Error> {
48 let array = engine.new_property_object(PropValType::Container, true, "", NO_OPTIONS)?;
49 array.set_num_elements(i32::try_from(named_values.len()).unwrap_or(0), NO_OPTIONS)?;
50 for (index, (name, value)) in named_values.iter().enumerate() {
51 let element =
52 array.get_property_object_by_offset(i32::try_from(index).unwrap_or(0), NO_OPTIONS)?;
53 element.set_val_string("EnumeratorName", INSERT_IF_MISSING, name)?;
54 element.set_val_number("EnumeratorValue", INSERT_IF_MISSING, *value)?;
55 }
56 array
57 .attributes()?
58 .set_val_bool(IS_STRICT_ATTRIBUTE, INSERT_IF_MISSING, strict)?;
59 Ok(array)
60}More examples
100fn show_station_globals(engine: &Engine) -> Result<(), rs_teststand::Error> {
101 // Station globals: shared by every sequence file, and written to disk.
102 let station_globals = engine.globals()?;
103 if !station_globals.exists("StationInfo", 0)? {
104 station_globals.new_sub_property(
105 "StationInfo",
106 PropValType::Container,
107 false,
108 "",
109 INSERT_IF_MISSING,
110 )?;
111 }
112 let station_info = station_globals.get_property_object("StationInfo", 0)?;
113
114 // A container holds mixed types, so one station record can carry the name,
115 // a count and a flag without three separate globals.
116 set_string(&station_info, "StationName", "STATION_01")?;
117 station_info.set_val_number("CalibrationIntervalDays", INSERT_IF_MISSING, 90.0)?;
118 station_info.set_val_bool("FixtureInstalled", INSERT_IF_MISSING, true)?;
119
120 // A 64-bit count. `SetValNumber` stores a double, which starts losing whole
121 // numbers past 2^53, so anything that counts for the life of a station wants
122 // the integer path instead.
123 station_info.set_val_integer64("UnitsTestedTotal", INSERT_IF_MISSING, 9_007_199_254_740_993)?;
124
125 // A nested container, because station data is rarely one level deep.
126 if !station_info.exists("LastCalibration", 0)? {
127 station_info.new_sub_property(
128 "LastCalibration",
129 PropValType::Container,
130 false,
131 "",
132 INSERT_IF_MISSING,
133 )?;
134 }
135 let calibration = station_info.get_property_object("LastCalibration", 0)?;
136 set_string(&calibration, "Technician", "R. Alvarez")?;
137 set_string(&calibration, "Date", "2026-06-14")?;
138
139 println!("StationGlobals.StationInfo:");
140 println!(
141 " StationName = '{}'",
142 station_info.get_val_string("StationName", 0)?
143 );
144 println!(
145 " CalibrationIntervalDays = {}",
146 station_info.get_val_number("CalibrationIntervalDays", 0)?
147 );
148 println!(
149 " FixtureInstalled = {}",
150 station_info.get_val_bool("FixtureInstalled", 0)?
151 );
152 println!(
153 " UnitsTestedTotal = {}",
154 station_info.get_val_integer64("UnitsTestedTotal", 0)?
155 );
156 println!(
157 " LastCalibration.Technician = '{}' on {}",
158 calibration.get_val_string("Technician", 0)?,
159 calibration.get_val_string("Date", 0)?
160 );
161
162 // Walk the container rather than naming each field, which is what a host
163 // does when it does not know the shape in advance.
164 println!(
165 " walked, {} field(s):",
166 station_info.get_num_sub_properties("")?
167 );
168 for index in 0..station_info.get_num_sub_properties("")? {
169 println!(
170 " {}",
171 station_info.get_nth_sub_property_name("", index, 0)?
172 );
173 }
174
175 // Station globals live in memory until this is called. Without it the values
176 // above are gone when the engine goes away, which is the difference between
177 // a station global and a file global.
178 // `false` means do not prompt if another process changed the file first.
179 // An example must never raise a dialog, and neither must a headless host.
180 engine.commit_globals_to_disk(false)?;
181 println!(" committed to disk");
182
183 Ok(())
184}Sourcepub fn get_num_sub_properties(&self, lookup_string: &str) -> Result<i32, Error>
pub fn get_num_sub_properties(&self, lookup_string: &str) -> Result<i32, Error>
How many sub-properties sit directly under lookup_string
(GetNumSubProperties).
Pass an empty string for this object itself.
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
100fn show_station_globals(engine: &Engine) -> Result<(), rs_teststand::Error> {
101 // Station globals: shared by every sequence file, and written to disk.
102 let station_globals = engine.globals()?;
103 if !station_globals.exists("StationInfo", 0)? {
104 station_globals.new_sub_property(
105 "StationInfo",
106 PropValType::Container,
107 false,
108 "",
109 INSERT_IF_MISSING,
110 )?;
111 }
112 let station_info = station_globals.get_property_object("StationInfo", 0)?;
113
114 // A container holds mixed types, so one station record can carry the name,
115 // a count and a flag without three separate globals.
116 set_string(&station_info, "StationName", "STATION_01")?;
117 station_info.set_val_number("CalibrationIntervalDays", INSERT_IF_MISSING, 90.0)?;
118 station_info.set_val_bool("FixtureInstalled", INSERT_IF_MISSING, true)?;
119
120 // A 64-bit count. `SetValNumber` stores a double, which starts losing whole
121 // numbers past 2^53, so anything that counts for the life of a station wants
122 // the integer path instead.
123 station_info.set_val_integer64("UnitsTestedTotal", INSERT_IF_MISSING, 9_007_199_254_740_993)?;
124
125 // A nested container, because station data is rarely one level deep.
126 if !station_info.exists("LastCalibration", 0)? {
127 station_info.new_sub_property(
128 "LastCalibration",
129 PropValType::Container,
130 false,
131 "",
132 INSERT_IF_MISSING,
133 )?;
134 }
135 let calibration = station_info.get_property_object("LastCalibration", 0)?;
136 set_string(&calibration, "Technician", "R. Alvarez")?;
137 set_string(&calibration, "Date", "2026-06-14")?;
138
139 println!("StationGlobals.StationInfo:");
140 println!(
141 " StationName = '{}'",
142 station_info.get_val_string("StationName", 0)?
143 );
144 println!(
145 " CalibrationIntervalDays = {}",
146 station_info.get_val_number("CalibrationIntervalDays", 0)?
147 );
148 println!(
149 " FixtureInstalled = {}",
150 station_info.get_val_bool("FixtureInstalled", 0)?
151 );
152 println!(
153 " UnitsTestedTotal = {}",
154 station_info.get_val_integer64("UnitsTestedTotal", 0)?
155 );
156 println!(
157 " LastCalibration.Technician = '{}' on {}",
158 calibration.get_val_string("Technician", 0)?,
159 calibration.get_val_string("Date", 0)?
160 );
161
162 // Walk the container rather than naming each field, which is what a host
163 // does when it does not know the shape in advance.
164 println!(
165 " walked, {} field(s):",
166 station_info.get_num_sub_properties("")?
167 );
168 for index in 0..station_info.get_num_sub_properties("")? {
169 println!(
170 " {}",
171 station_info.get_nth_sub_property_name("", index, 0)?
172 );
173 }
174
175 // Station globals live in memory until this is called. Without it the values
176 // above are gone when the engine goes away, which is the difference between
177 // a station global and a file global.
178 // `false` means do not prompt if another process changed the file first.
179 // An example must never raise a dialog, and neither must a headless host.
180 engine.commit_globals_to_disk(false)?;
181 println!(" committed to disk");
182
183 Ok(())
184}More examples
29fn main() -> Result<(), rs_teststand::Error> {
30 let engine = Engine::new()?;
31
32 // Passing no profile means the user inherits nothing.
33 let user = engine.new_user(None)?;
34 user.set_login_name("operator1")?;
35 user.set_full_name("Test Operator")?;
36 user.set_password("ts-secret")?;
37
38 println!("User: {} ({})", user.login_name()?, user.full_name()?);
39
40 // Check a credential without handling the stored value.
41 println!(
42 " password 'ts-secret' valid: {}",
43 user.validate_password("ts-secret")?
44 );
45 println!(
46 " password 'wrong' valid: {}",
47 user.validate_password("wrong")?
48 );
49
50 println!(" privilege checks:");
51 report_privileges(
52 &user,
53 &[
54 UserPrivilege::Operate,
55 UserPrivilege::Execute,
56 UserPrivilege::Develop,
57 UserPrivilege::Debug,
58 UserPrivilege::EditUsers,
59 ],
60 )?;
61
62 // A privilege can also be named by its full path, which is how a leaf
63 // inside a category is reached.
64 println!(
65 " Debug.RunSelectedTests: {}",
66 user.has_privilege_named("Debug.RunSelectedTests")?
67 );
68
69 // The privilege tree is nested: categories hold the individual rights.
70 let privileges = user.privileges()?;
71 let count = privileges.get_num_sub_properties("")?;
72 println!(" privilege tree ({count} categories):");
73 for index in 0..count {
74 let name = privileges.get_nth_sub_property_name("", index, 0)?;
75 let members = privileges
76 .get_property_object(&name, 0)?
77 .get_num_sub_properties("")?;
78 println!(" {name:<12} {members} member(s)");
79 }
80
81 // The station itself, read-only.
82 println!("\nStation:");
83 match engine.current_user()? {
84 Some(current) => println!(" logged in as {}", current.login_name()?),
85 None => println!(" nobody logged in (usual when login is not required)"),
86 }
87 println!(
88 " is 'operator1' a real account here? {}",
89 engine.user_name_exists("operator1")?
90 );
91
92 // Where accounts actually live. Everything above this point exists only in
93 // memory: `new_user` builds a User, it does not enrol one. A user becomes
94 // part of the station when it is inserted into this file's list and the
95 // file is written.
96 let users_file = engine.users_file()?;
97 let file = users_file.as_property_object_file()?;
98 println!(
99 "
100Users file:"
101 );
102 println!(" path {}", file.path()?);
103 println!(
104 " users {}",
105 users_file.user_list()?.get_num_elements()?
106 );
107 println!(
108 " groups {}",
109 users_file.user_group_list()?.get_num_elements()?
110 );
111 println!(
112 " profiles {}",
113 users_file.user_profile_list()?.get_num_elements()?
114 );
115
116 // Deliberately not run: it would edit the station this example is reading.
117 // The two calls a host makes to enrol the user built above are
118 //
119 // users_file.user_list()?.set_num_elements(count + 1, 0)?; // then fill
120 // file.save_file_if_modified(false)?; // then write
121 //
122 // `false` matters: with `true` the engine puts a save dialog on screen, and
123 // a host with no operator would wait for an answer that never comes.
124 println!(
125 "
126 (this example does not write; enrolling a user would alter the station)"
127 );
128
129 Ok(())
130}Sourcepub fn get_nth_sub_property_name(
&self,
lookup_string: &str,
index: i32,
options: i32,
) -> Result<String, Error>
pub fn get_nth_sub_property_name( &self, lookup_string: &str, index: i32, options: i32, ) -> Result<String, Error>
The name of the index-th sub-property (GetNthSubPropertyName).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
100fn show_station_globals(engine: &Engine) -> Result<(), rs_teststand::Error> {
101 // Station globals: shared by every sequence file, and written to disk.
102 let station_globals = engine.globals()?;
103 if !station_globals.exists("StationInfo", 0)? {
104 station_globals.new_sub_property(
105 "StationInfo",
106 PropValType::Container,
107 false,
108 "",
109 INSERT_IF_MISSING,
110 )?;
111 }
112 let station_info = station_globals.get_property_object("StationInfo", 0)?;
113
114 // A container holds mixed types, so one station record can carry the name,
115 // a count and a flag without three separate globals.
116 set_string(&station_info, "StationName", "STATION_01")?;
117 station_info.set_val_number("CalibrationIntervalDays", INSERT_IF_MISSING, 90.0)?;
118 station_info.set_val_bool("FixtureInstalled", INSERT_IF_MISSING, true)?;
119
120 // A 64-bit count. `SetValNumber` stores a double, which starts losing whole
121 // numbers past 2^53, so anything that counts for the life of a station wants
122 // the integer path instead.
123 station_info.set_val_integer64("UnitsTestedTotal", INSERT_IF_MISSING, 9_007_199_254_740_993)?;
124
125 // A nested container, because station data is rarely one level deep.
126 if !station_info.exists("LastCalibration", 0)? {
127 station_info.new_sub_property(
128 "LastCalibration",
129 PropValType::Container,
130 false,
131 "",
132 INSERT_IF_MISSING,
133 )?;
134 }
135 let calibration = station_info.get_property_object("LastCalibration", 0)?;
136 set_string(&calibration, "Technician", "R. Alvarez")?;
137 set_string(&calibration, "Date", "2026-06-14")?;
138
139 println!("StationGlobals.StationInfo:");
140 println!(
141 " StationName = '{}'",
142 station_info.get_val_string("StationName", 0)?
143 );
144 println!(
145 " CalibrationIntervalDays = {}",
146 station_info.get_val_number("CalibrationIntervalDays", 0)?
147 );
148 println!(
149 " FixtureInstalled = {}",
150 station_info.get_val_bool("FixtureInstalled", 0)?
151 );
152 println!(
153 " UnitsTestedTotal = {}",
154 station_info.get_val_integer64("UnitsTestedTotal", 0)?
155 );
156 println!(
157 " LastCalibration.Technician = '{}' on {}",
158 calibration.get_val_string("Technician", 0)?,
159 calibration.get_val_string("Date", 0)?
160 );
161
162 // Walk the container rather than naming each field, which is what a host
163 // does when it does not know the shape in advance.
164 println!(
165 " walked, {} field(s):",
166 station_info.get_num_sub_properties("")?
167 );
168 for index in 0..station_info.get_num_sub_properties("")? {
169 println!(
170 " {}",
171 station_info.get_nth_sub_property_name("", index, 0)?
172 );
173 }
174
175 // Station globals live in memory until this is called. Without it the values
176 // above are gone when the engine goes away, which is the difference between
177 // a station global and a file global.
178 // `false` means do not prompt if another process changed the file first.
179 // An example must never raise a dialog, and neither must a headless host.
180 engine.commit_globals_to_disk(false)?;
181 println!(" committed to disk");
182
183 Ok(())
184}More examples
29fn main() -> Result<(), rs_teststand::Error> {
30 let engine = Engine::new()?;
31
32 // Passing no profile means the user inherits nothing.
33 let user = engine.new_user(None)?;
34 user.set_login_name("operator1")?;
35 user.set_full_name("Test Operator")?;
36 user.set_password("ts-secret")?;
37
38 println!("User: {} ({})", user.login_name()?, user.full_name()?);
39
40 // Check a credential without handling the stored value.
41 println!(
42 " password 'ts-secret' valid: {}",
43 user.validate_password("ts-secret")?
44 );
45 println!(
46 " password 'wrong' valid: {}",
47 user.validate_password("wrong")?
48 );
49
50 println!(" privilege checks:");
51 report_privileges(
52 &user,
53 &[
54 UserPrivilege::Operate,
55 UserPrivilege::Execute,
56 UserPrivilege::Develop,
57 UserPrivilege::Debug,
58 UserPrivilege::EditUsers,
59 ],
60 )?;
61
62 // A privilege can also be named by its full path, which is how a leaf
63 // inside a category is reached.
64 println!(
65 " Debug.RunSelectedTests: {}",
66 user.has_privilege_named("Debug.RunSelectedTests")?
67 );
68
69 // The privilege tree is nested: categories hold the individual rights.
70 let privileges = user.privileges()?;
71 let count = privileges.get_num_sub_properties("")?;
72 println!(" privilege tree ({count} categories):");
73 for index in 0..count {
74 let name = privileges.get_nth_sub_property_name("", index, 0)?;
75 let members = privileges
76 .get_property_object(&name, 0)?
77 .get_num_sub_properties("")?;
78 println!(" {name:<12} {members} member(s)");
79 }
80
81 // The station itself, read-only.
82 println!("\nStation:");
83 match engine.current_user()? {
84 Some(current) => println!(" logged in as {}", current.login_name()?),
85 None => println!(" nobody logged in (usual when login is not required)"),
86 }
87 println!(
88 " is 'operator1' a real account here? {}",
89 engine.user_name_exists("operator1")?
90 );
91
92 // Where accounts actually live. Everything above this point exists only in
93 // memory: `new_user` builds a User, it does not enrol one. A user becomes
94 // part of the station when it is inserted into this file's list and the
95 // file is written.
96 let users_file = engine.users_file()?;
97 let file = users_file.as_property_object_file()?;
98 println!(
99 "
100Users file:"
101 );
102 println!(" path {}", file.path()?);
103 println!(
104 " users {}",
105 users_file.user_list()?.get_num_elements()?
106 );
107 println!(
108 " groups {}",
109 users_file.user_group_list()?.get_num_elements()?
110 );
111 println!(
112 " profiles {}",
113 users_file.user_profile_list()?.get_num_elements()?
114 );
115
116 // Deliberately not run: it would edit the station this example is reading.
117 // The two calls a host makes to enrol the user built above are
118 //
119 // users_file.user_list()?.set_num_elements(count + 1, 0)?; // then fill
120 // file.save_file_if_modified(false)?; // then write
121 //
122 // `false` matters: with `true` the engine puts a save dialog on screen, and
123 // a host with no operator would wait for an answer that never comes.
124 println!(
125 "
126 (this example does not write; enrolling a user would alter the station)"
127 );
128
129 Ok(())
130}Sourcepub fn get_nth_sub_property(
&self,
lookup_string: &str,
index: i32,
options: i32,
) -> Result<Self, Error>
pub fn get_nth_sub_property( &self, lookup_string: &str, index: i32, options: i32, ) -> Result<Self, Error>
Sourcepub fn get_val_integer64(
&self,
lookup_string: &str,
options: i32,
) -> Result<i64, Error>
pub fn get_val_integer64( &self, lookup_string: &str, options: i32, ) -> Result<i64, Error>
Reads a signed 64-bit integer property (GetValInteger64).
The engine stores a number as one of three things, a double, a signed
64-bit integer, or an unsigned one, and the accessor must match. Use
this when property_type reports
PropertyRepresentation::Int64;
get_val_number fails on such a property rather
than converting.
§Errors
Error if the COM call fails or the property is not stored as a
signed 64-bit integer.
Examples found in repository?
100fn show_station_globals(engine: &Engine) -> Result<(), rs_teststand::Error> {
101 // Station globals: shared by every sequence file, and written to disk.
102 let station_globals = engine.globals()?;
103 if !station_globals.exists("StationInfo", 0)? {
104 station_globals.new_sub_property(
105 "StationInfo",
106 PropValType::Container,
107 false,
108 "",
109 INSERT_IF_MISSING,
110 )?;
111 }
112 let station_info = station_globals.get_property_object("StationInfo", 0)?;
113
114 // A container holds mixed types, so one station record can carry the name,
115 // a count and a flag without three separate globals.
116 set_string(&station_info, "StationName", "STATION_01")?;
117 station_info.set_val_number("CalibrationIntervalDays", INSERT_IF_MISSING, 90.0)?;
118 station_info.set_val_bool("FixtureInstalled", INSERT_IF_MISSING, true)?;
119
120 // A 64-bit count. `SetValNumber` stores a double, which starts losing whole
121 // numbers past 2^53, so anything that counts for the life of a station wants
122 // the integer path instead.
123 station_info.set_val_integer64("UnitsTestedTotal", INSERT_IF_MISSING, 9_007_199_254_740_993)?;
124
125 // A nested container, because station data is rarely one level deep.
126 if !station_info.exists("LastCalibration", 0)? {
127 station_info.new_sub_property(
128 "LastCalibration",
129 PropValType::Container,
130 false,
131 "",
132 INSERT_IF_MISSING,
133 )?;
134 }
135 let calibration = station_info.get_property_object("LastCalibration", 0)?;
136 set_string(&calibration, "Technician", "R. Alvarez")?;
137 set_string(&calibration, "Date", "2026-06-14")?;
138
139 println!("StationGlobals.StationInfo:");
140 println!(
141 " StationName = '{}'",
142 station_info.get_val_string("StationName", 0)?
143 );
144 println!(
145 " CalibrationIntervalDays = {}",
146 station_info.get_val_number("CalibrationIntervalDays", 0)?
147 );
148 println!(
149 " FixtureInstalled = {}",
150 station_info.get_val_bool("FixtureInstalled", 0)?
151 );
152 println!(
153 " UnitsTestedTotal = {}",
154 station_info.get_val_integer64("UnitsTestedTotal", 0)?
155 );
156 println!(
157 " LastCalibration.Technician = '{}' on {}",
158 calibration.get_val_string("Technician", 0)?,
159 calibration.get_val_string("Date", 0)?
160 );
161
162 // Walk the container rather than naming each field, which is what a host
163 // does when it does not know the shape in advance.
164 println!(
165 " walked, {} field(s):",
166 station_info.get_num_sub_properties("")?
167 );
168 for index in 0..station_info.get_num_sub_properties("")? {
169 println!(
170 " {}",
171 station_info.get_nth_sub_property_name("", index, 0)?
172 );
173 }
174
175 // Station globals live in memory until this is called. Without it the values
176 // above are gone when the engine goes away, which is the difference between
177 // a station global and a file global.
178 // `false` means do not prompt if another process changed the file first.
179 // An example must never raise a dialog, and neither must a headless host.
180 engine.commit_globals_to_disk(false)?;
181 println!(" committed to disk");
182
183 Ok(())
184}Sourcepub fn set_val_integer64(
&self,
lookup_string: &str,
options: i32,
value: i64,
) -> Result<(), Error>
pub fn set_val_integer64( &self, lookup_string: &str, options: i32, value: i64, ) -> Result<(), Error>
Examples found in repository?
100fn show_station_globals(engine: &Engine) -> Result<(), rs_teststand::Error> {
101 // Station globals: shared by every sequence file, and written to disk.
102 let station_globals = engine.globals()?;
103 if !station_globals.exists("StationInfo", 0)? {
104 station_globals.new_sub_property(
105 "StationInfo",
106 PropValType::Container,
107 false,
108 "",
109 INSERT_IF_MISSING,
110 )?;
111 }
112 let station_info = station_globals.get_property_object("StationInfo", 0)?;
113
114 // A container holds mixed types, so one station record can carry the name,
115 // a count and a flag without three separate globals.
116 set_string(&station_info, "StationName", "STATION_01")?;
117 station_info.set_val_number("CalibrationIntervalDays", INSERT_IF_MISSING, 90.0)?;
118 station_info.set_val_bool("FixtureInstalled", INSERT_IF_MISSING, true)?;
119
120 // A 64-bit count. `SetValNumber` stores a double, which starts losing whole
121 // numbers past 2^53, so anything that counts for the life of a station wants
122 // the integer path instead.
123 station_info.set_val_integer64("UnitsTestedTotal", INSERT_IF_MISSING, 9_007_199_254_740_993)?;
124
125 // A nested container, because station data is rarely one level deep.
126 if !station_info.exists("LastCalibration", 0)? {
127 station_info.new_sub_property(
128 "LastCalibration",
129 PropValType::Container,
130 false,
131 "",
132 INSERT_IF_MISSING,
133 )?;
134 }
135 let calibration = station_info.get_property_object("LastCalibration", 0)?;
136 set_string(&calibration, "Technician", "R. Alvarez")?;
137 set_string(&calibration, "Date", "2026-06-14")?;
138
139 println!("StationGlobals.StationInfo:");
140 println!(
141 " StationName = '{}'",
142 station_info.get_val_string("StationName", 0)?
143 );
144 println!(
145 " CalibrationIntervalDays = {}",
146 station_info.get_val_number("CalibrationIntervalDays", 0)?
147 );
148 println!(
149 " FixtureInstalled = {}",
150 station_info.get_val_bool("FixtureInstalled", 0)?
151 );
152 println!(
153 " UnitsTestedTotal = {}",
154 station_info.get_val_integer64("UnitsTestedTotal", 0)?
155 );
156 println!(
157 " LastCalibration.Technician = '{}' on {}",
158 calibration.get_val_string("Technician", 0)?,
159 calibration.get_val_string("Date", 0)?
160 );
161
162 // Walk the container rather than naming each field, which is what a host
163 // does when it does not know the shape in advance.
164 println!(
165 " walked, {} field(s):",
166 station_info.get_num_sub_properties("")?
167 );
168 for index in 0..station_info.get_num_sub_properties("")? {
169 println!(
170 " {}",
171 station_info.get_nth_sub_property_name("", index, 0)?
172 );
173 }
174
175 // Station globals live in memory until this is called. Without it the values
176 // above are gone when the engine goes away, which is the difference between
177 // a station global and a file global.
178 // `false` means do not prompt if another process changed the file first.
179 // An example must never raise a dialog, and neither must a headless host.
180 engine.commit_globals_to_disk(false)?;
181 println!(" committed to disk");
182
183 Ok(())
184}Sourcepub fn get_val_unsigned_integer64(
&self,
lookup_string: &str,
options: i32,
) -> Result<u64, Error>
pub fn get_val_unsigned_integer64( &self, lookup_string: &str, options: i32, ) -> Result<u64, Error>
Reads an unsigned 64-bit integer property (GetValUnsignedInteger64).
Use this when the representation is
UInt64. The value crosses the
COM boundary as VT_UI8 and is returned with its bits intact, so the
full unsigned range survives.
§Errors
Error if the COM call fails or the property is not stored as an
unsigned 64-bit integer.
Sourcepub fn set_val_unsigned_integer64(
&self,
lookup_string: &str,
options: i32,
value: u64,
) -> Result<(), Error>
pub fn set_val_unsigned_integer64( &self, lookup_string: &str, options: i32, value: u64, ) -> Result<(), Error>
Sourcepub fn numeric_format(&self) -> Result<String, Error>
pub fn numeric_format(&self) -> Result<String, Error>
The per-property numeric format string (PropertyObject.NumericFormat).
A printf-style format that decides how
get_formatted_value renders a number, so
the same stored value can display as decimal, hex, octal or binary. It
is presentation only, the underlying number is unchanged.
Two departures from C: %b formats in binary, and a $ placed straight
after the % strips trailing zeros after the decimal point. An empty
string restores the default format.
§Errors
Error if the COM call fails or returns an unexpected type.
Sourcepub fn get_formatted_value(
&self,
lookup_string: &str,
options: i32,
format: &str,
use_value_format_if_defined: bool,
separator: &str,
) -> Result<String, Error>
pub fn get_formatted_value( &self, lookup_string: &str, options: i32, format: &str, use_value_format_if_defined: bool, separator: &str, ) -> Result<String, Error>
Renders a property’s value as display text (GetFormattedValue).
format overrides the formatting for this call; pass an empty string to
use the default. Set use_value_format_if_defined to honour the
property’s own numeric_format instead.
separator joins array elements.
Containers render as ... and an empty reference as Nothing, so the
result is always displayable text rather than an error.
§Errors
Error if the COM call fails or returns an unexpected type.
Sourcepub fn name(&self) -> Result<String, Error>
pub fn name(&self) -> Result<String, Error>
The object’s name (PropertyObject.Name).
A type definition must be named before it can be registered.
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
33fn describe_templates(engine: &Engine) -> Result<(), Error> {
34 let templates_file = engine.get_templates_file(GetTemplatesFileOptions::LOAD_IF_NOT_LOADED)?;
35 let root = templates_file
36 .data()?
37 .get_property_object("Root", PropertyOptions::NONE.bits())?;
38 for index in 0..root.get_num_elements()? {
39 let category = root.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
40 if category.name()? == "Steps" {
41 println!(
42 "Step templates defined on this station: {}",
43 category.get_num_elements()?
44 );
45 }
46 }
47 Ok(())
48}More examples
40fn describe_station_templates(engine: &Engine) -> Result<(), Error> {
41 let templates_file = engine.get_templates_file(GetTemplatesFileOptions::LOAD_IF_NOT_LOADED)?;
42 let root = templates_file
43 .data()?
44 .get_property_object("Root", PropertyOptions::NONE.bits())?;
45
46 println!("Station templates file: {}", templates_file.path()?);
47 for index in 0..root.get_num_elements()? {
48 let category = root.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
49 println!(
50 " {}: {} template(s)",
51 category.name()?,
52 category.get_num_elements()?
53 );
54 }
55 Ok(())
56}
57
58/// An empty array container to keep prototypes in.
59fn new_template_group(engine: &Engine) -> Result<PropertyObject, Error> {
60 engine.new_property_object(
61 PropValType::Container,
62 true,
63 "",
64 PropertyOptions::NONE.bits(),
65 )
66}
67
68/// Appends a detached copy of a prototype to the group.
69///
70/// The copy matters: the group must not share the object the caller keeps
71/// editing, or adding a second template would change the first.
72fn append_template(group: &PropertyObject, template: &PropertyObject) -> Result<(), Error> {
73 let index = group.get_num_elements()?;
74 group.set_num_elements(index + 1, PropertyOptions::NONE.bits())?;
75 group.set_property_object(
76 &format!("[{index}]"),
77 PropertyOptions::NONE.bits(),
78 &template.clone_property("", PropertyOptions::NONE.bits())?,
79 )
80}
81
82/// Finds a prototype in the group by name.
83fn find_template(group: &PropertyObject, name: &str) -> Result<Option<PropertyObject>, Error> {
84 for index in 0..group.get_num_elements()? {
85 let candidate = group.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
86 if candidate.name()? == name {
87 return Ok(Some(candidate));
88 }
89 }
90 Ok(None)
91}
92
93/// Turns "not found" into an error naming the template that is missing.
94fn require(found: Option<PropertyObject>, name: &'static str) -> Result<PropertyObject, Error> {
95 found.ok_or(Error::UnexpectedType {
96 expected: name,
97 actual: "no template of that name in the group",
98 })
99}
100
101/// A configured Statement step, taken as a property tree so it can be cloned.
102fn step_template(engine: &Engine) -> Result<PropertyObject, Error> {
103 let step = engine.new_step(NO_ADAPTER, "Statement")?;
104 step.set_name(STEP_TEMPLATE)?;
105 step.set_post_expression(r#"Locals.Result = "Hello from step template!""#)?;
106 step.as_property_object()
107}
108
109/// A whole sequence, one step included, as a single prototype.
110fn sequence_template(engine: &Engine) -> Result<PropertyObject, Error> {
111 let sequence = engine.new_sequence()?;
112 sequence.set_name(SEQUENCE_TEMPLATE)?;
113
114 let step = engine.new_step(NO_ADAPTER, "Statement")?;
115 step.set_name("Inside_Sequence_Template")?;
116 sequence.insert_step(&step, 0, StepGroup::Main)?;
117
118 sequence.as_property_object()
119}
120
121/// A string variable carrying its default value.
122fn variable_template(engine: &Engine) -> Result<PropertyObject, Error> {
123 let variable =
124 engine.new_property_object(PropValType::String, false, "", PropertyOptions::NONE.bits())?;
125 variable.set_name(VARIABLE_TEMPLATE)?;
126 variable.set_val_string("", PropertyOptions::NONE.bits(), "Template Variable Value")?;
127 Ok(variable)
128}
129
130/// Stamps one copy of each template into a file that is already on disk.
131fn apply_templates(
132 sequence_file: &SequenceFile,
133 step: &PropertyObject,
134 sequence: &PropertyObject,
135 variable: &PropertyObject,
136) -> Result<(), Error> {
137 let main_sequence = sequence_file.get_sequence_by_name(MAIN_SEQUENCE)?;
138
139 // Inserting returns the copy on the Step interface, so it can be renamed
140 // and re-identified straight away. Without a new step ID the copy would go
141 // on claiming the prototype's identity.
142 let inserted_step = main_sequence.insert_step_from_template(step, 0, StepGroup::Main)?;
143 inserted_step.set_name("Step From Template")?;
144 inserted_step.create_new_unique_step_id()?;
145 println!(" step: {}", inserted_step.name()?);
146
147 let inserted_sequence = sequence_file.insert_sequence_from_template(sequence)?;
148 inserted_sequence.create_new_unique_step_ids()?;
149 println!(
150 " sequence: {} ({} step(s))",
151 inserted_sequence.name()?,
152 inserted_sequence.get_num_steps(StepGroup::Main)?
153 );
154
155 // A variable is the easy case: Locals is a property tree, so a clone drops
156 // straight in under the template's own name.
157 let locals = main_sequence.locals()?;
158 locals.set_property_object(
159 &variable.name()?,
160 PropertyOptions::INSERT_IF_MISSING.bits(),
161 &variable.clone_property("", PropertyOptions::NONE.bits())?,
162 )?;
163 println!(
164 " variable: {} = {:?}",
165 variable.name()?,
166 locals.get_val_string(&variable.name()?, PropertyOptions::NONE.bits())?
167 );
168
169 // A file that does not believe it changed will not write anything.
170 sequence_file
171 .as_property_object_file()?
172 .inc_change_count()?;
173 Ok(())
174}86fn print_enumerators(definition: &PropertyObject) -> Result<(), rs_teststand::Error> {
87 let enumerators = definition.enumerators()?;
88 let strict = enumerators
89 .attributes()?
90 .get_val_bool(IS_STRICT_ATTRIBUTE, NO_OPTIONS)?;
91 println!(
92 " {} v{} (strict={strict}):",
93 definition.name()?,
94 definition.type_version()?
95 );
96 for index in 0..enumerators.get_num_elements()? {
97 let element = enumerators.get_property_object_by_offset(index, NO_OPTIONS)?;
98 let value = element.get_val_number("", COERCE_TO_NUMBER)?;
99 println!(
100 " {} -> {value}",
101 element.get_value_display_name("", NO_OPTIONS)?
102 );
103 }
104 Ok(())
105}Sourcepub fn set_name(&self, name: &str) -> Result<(), Error>
pub fn set_name(&self, name: &str) -> Result<(), Error>
Examples found in repository?
More examples
28fn build_multimeter_type(engine: &Engine) -> Result<PropertyObject, rs_teststand::Error> {
29 let data_type = engine.new_property_object(PropValType::Container, false, "", NO_OPTIONS)?;
30 data_type.set_val_number("Resolution", INSERT_IF_MISSING, 6.5)?;
31 data_type.set_val_bool("AutoZero", INSERT_IF_MISSING, false)?;
32 data_type.set_val_string("Mode", INSERT_IF_MISSING, "Voltage")?;
33 data_type.set_val_number("Range", INSERT_IF_MISSING, 100.0)?;
34 data_type.set_name("DigitalMultimeter")?;
35 Ok(data_type)
36}
37
38/// Builds the array `UpdateEnumerators` expects.
39///
40/// One container per enumerator, each carrying `EnumeratorName` and
41/// `EnumeratorValue`. Strictness is an attribute of the array, not a member of
42/// it, a strict enumeration refuses values outside the declared set.
43fn enumerator_array(
44 engine: &Engine,
45 named_values: &[(&str, f64)],
46 strict: bool,
47) -> Result<PropertyObject, rs_teststand::Error> {
48 let array = engine.new_property_object(PropValType::Container, true, "", NO_OPTIONS)?;
49 array.set_num_elements(i32::try_from(named_values.len()).unwrap_or(0), NO_OPTIONS)?;
50 for (index, (name, value)) in named_values.iter().enumerate() {
51 let element =
52 array.get_property_object_by_offset(i32::try_from(index).unwrap_or(0), NO_OPTIONS)?;
53 element.set_val_string("EnumeratorName", INSERT_IF_MISSING, name)?;
54 element.set_val_number("EnumeratorValue", INSERT_IF_MISSING, *value)?;
55 }
56 array
57 .attributes()?
58 .set_val_bool(IS_STRICT_ATTRIBUTE, INSERT_IF_MISSING, strict)?;
59 Ok(array)
60}
61
62/// Registers an enumeration, then returns its **registered** definition.
63///
64/// The distinction matters: enumerators can only be set on the definition the
65/// file holds, not on the loose object that was inserted.
66fn register_enum(
67 engine: &Engine,
68 types: &TypeUsageList,
69 name: &str,
70 named_values: &[(&str, f64)],
71 strict: bool,
72) -> Result<PropertyObject, rs_teststand::Error> {
73 let enum_type = engine.new_property_object(PropValType::Enum, false, "", NO_OPTIONS)?;
74 enum_type.set_name(name)?;
75 types.insert_type(
76 &enum_type,
77 types.num_types()?,
78 TypeCategory::CustomDataTypes,
79 )?;
80
81 let definition = types.get_type_definition(types.get_type_index(name)?)?;
82 definition.update_enumerators(&enumerator_array(engine, named_values, strict)?)?;
83 Ok(definition)
84}Sourcepub fn type_version(&self) -> Result<String, Error>
pub fn type_version(&self) -> Result<String, Error>
A type’s version, as major.minor.revision.build
(PropertyObject.TypeVersion).
Which field is bumped carries meaning: raising the lowest field signals a change the engine can apply to existing instances silently, while raising a higher one marks the change as deliberate.
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
86fn print_enumerators(definition: &PropertyObject) -> Result<(), rs_teststand::Error> {
87 let enumerators = definition.enumerators()?;
88 let strict = enumerators
89 .attributes()?
90 .get_val_bool(IS_STRICT_ATTRIBUTE, NO_OPTIONS)?;
91 println!(
92 " {} v{} (strict={strict}):",
93 definition.name()?,
94 definition.type_version()?
95 );
96 for index in 0..enumerators.get_num_elements()? {
97 let element = enumerators.get_property_object_by_offset(index, NO_OPTIONS)?;
98 let value = element.get_val_number("", COERCE_TO_NUMBER)?;
99 println!(
100 " {} -> {value}",
101 element.get_value_display_name("", NO_OPTIONS)?
102 );
103 }
104 Ok(())
105}
106
107fn main() -> Result<(), Box<dyn std::error::Error>> {
108 let engine = Engine::new()?;
109 let sequence_file = engine.new_sequence_file()?;
110 let file = sequence_file.as_property_object_file()?;
111 let types = file.type_usage_list()?;
112
113 types.insert_type(
114 &build_multimeter_type(&engine)?,
115 types.num_types()?,
116 TypeCategory::CustomDataTypes,
117 )?;
118 let coupling = register_enum(
119 &engine,
120 &types,
121 "Coupling",
122 &[("AC", 0.0), ("DC", 1.0)],
123 true,
124 )?;
125
126 // A variable of the enum type, so the change below has an instance to update.
127 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
128 main_sequence.locals()?.new_sub_property(
129 "InputCoupling",
130 PropValType::NamedType,
131 false,
132 "Coupling",
133 INSERT_IF_MISSING,
134 )?;
135
136 println!(
137 "Registered custom data types ({} in file):",
138 types.num_types()?
139 );
140 print_enumerators(&coupling)?;
141
142 // Evolve it: add an enumerator and raise the version.
143 println!(
144 "\nCoupling version before update: {}",
145 coupling.type_version()?
146 );
147 coupling.update_enumerators(&enumerator_array(
148 &engine,
149 &[("AC", 0.0), ("DC", 1.0), ("GND", 2.0)],
150 true,
151 )?)?;
152
153 // Raising the lowest field signals a change the engine applies silently;
154 // raising a higher one marks it as deliberate.
155 let version = coupling.type_version()?;
156 let mut fields = version.split('.');
157 let major: u32 = fields.next().unwrap_or("0").parse().unwrap_or(0);
158 let minor: u32 = fields.next().unwrap_or("0").parse().unwrap_or(0);
159 coupling.set_type_version(&format!("{major}.{}.0.0", minor + 1))?;
160 println!(
161 "Coupling version after update: {}",
162 coupling.type_version()?
163 );
164
165 println!("\nCoupling now defines (InputCoupling reflects this):");
166 print_enumerators(&coupling)?;
167
168 // Saving does nothing unless the file believes it changed.
169 file.inc_change_count()?;
170 let path = std::env::temp_dir().join("rs_teststand_with_custom_types.seq");
171 sequence_file.save(&path.to_string_lossy())?;
172 println!("\nSaved to {}", path.display());
173
174 engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
175 Ok(())
176}Sourcepub fn set_type_version(&self, version: &str) -> Result<(), Error>
pub fn set_type_version(&self, version: &str) -> Result<(), Error>
Examples found in repository?
107fn main() -> Result<(), Box<dyn std::error::Error>> {
108 let engine = Engine::new()?;
109 let sequence_file = engine.new_sequence_file()?;
110 let file = sequence_file.as_property_object_file()?;
111 let types = file.type_usage_list()?;
112
113 types.insert_type(
114 &build_multimeter_type(&engine)?,
115 types.num_types()?,
116 TypeCategory::CustomDataTypes,
117 )?;
118 let coupling = register_enum(
119 &engine,
120 &types,
121 "Coupling",
122 &[("AC", 0.0), ("DC", 1.0)],
123 true,
124 )?;
125
126 // A variable of the enum type, so the change below has an instance to update.
127 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
128 main_sequence.locals()?.new_sub_property(
129 "InputCoupling",
130 PropValType::NamedType,
131 false,
132 "Coupling",
133 INSERT_IF_MISSING,
134 )?;
135
136 println!(
137 "Registered custom data types ({} in file):",
138 types.num_types()?
139 );
140 print_enumerators(&coupling)?;
141
142 // Evolve it: add an enumerator and raise the version.
143 println!(
144 "\nCoupling version before update: {}",
145 coupling.type_version()?
146 );
147 coupling.update_enumerators(&enumerator_array(
148 &engine,
149 &[("AC", 0.0), ("DC", 1.0), ("GND", 2.0)],
150 true,
151 )?)?;
152
153 // Raising the lowest field signals a change the engine applies silently;
154 // raising a higher one marks it as deliberate.
155 let version = coupling.type_version()?;
156 let mut fields = version.split('.');
157 let major: u32 = fields.next().unwrap_or("0").parse().unwrap_or(0);
158 let minor: u32 = fields.next().unwrap_or("0").parse().unwrap_or(0);
159 coupling.set_type_version(&format!("{major}.{}.0.0", minor + 1))?;
160 println!(
161 "Coupling version after update: {}",
162 coupling.type_version()?
163 );
164
165 println!("\nCoupling now defines (InputCoupling reflects this):");
166 print_enumerators(&coupling)?;
167
168 // Saving does nothing unless the file believes it changed.
169 file.inc_change_count()?;
170 let path = std::env::temp_dir().join("rs_teststand_with_custom_types.seq");
171 sequence_file.save(&path.to_string_lossy())?;
172 println!("\nSaved to {}", path.display());
173
174 engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
175 Ok(())
176}Sourcepub fn attributes(&self) -> Result<Self, Error>
pub fn attributes(&self) -> Result<Self, Error>
The object’s attributes (PropertyObject.Attributes).
A property tree of its own, used for metadata that is not part of the value, an enumeration’s strictness flag, for instance.
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
43fn enumerator_array(
44 engine: &Engine,
45 named_values: &[(&str, f64)],
46 strict: bool,
47) -> Result<PropertyObject, rs_teststand::Error> {
48 let array = engine.new_property_object(PropValType::Container, true, "", NO_OPTIONS)?;
49 array.set_num_elements(i32::try_from(named_values.len()).unwrap_or(0), NO_OPTIONS)?;
50 for (index, (name, value)) in named_values.iter().enumerate() {
51 let element =
52 array.get_property_object_by_offset(i32::try_from(index).unwrap_or(0), NO_OPTIONS)?;
53 element.set_val_string("EnumeratorName", INSERT_IF_MISSING, name)?;
54 element.set_val_number("EnumeratorValue", INSERT_IF_MISSING, *value)?;
55 }
56 array
57 .attributes()?
58 .set_val_bool(IS_STRICT_ATTRIBUTE, INSERT_IF_MISSING, strict)?;
59 Ok(array)
60}
61
62/// Registers an enumeration, then returns its **registered** definition.
63///
64/// The distinction matters: enumerators can only be set on the definition the
65/// file holds, not on the loose object that was inserted.
66fn register_enum(
67 engine: &Engine,
68 types: &TypeUsageList,
69 name: &str,
70 named_values: &[(&str, f64)],
71 strict: bool,
72) -> Result<PropertyObject, rs_teststand::Error> {
73 let enum_type = engine.new_property_object(PropValType::Enum, false, "", NO_OPTIONS)?;
74 enum_type.set_name(name)?;
75 types.insert_type(
76 &enum_type,
77 types.num_types()?,
78 TypeCategory::CustomDataTypes,
79 )?;
80
81 let definition = types.get_type_definition(types.get_type_index(name)?)?;
82 definition.update_enumerators(&enumerator_array(engine, named_values, strict)?)?;
83 Ok(definition)
84}
85
86fn print_enumerators(definition: &PropertyObject) -> Result<(), rs_teststand::Error> {
87 let enumerators = definition.enumerators()?;
88 let strict = enumerators
89 .attributes()?
90 .get_val_bool(IS_STRICT_ATTRIBUTE, NO_OPTIONS)?;
91 println!(
92 " {} v{} (strict={strict}):",
93 definition.name()?,
94 definition.type_version()?
95 );
96 for index in 0..enumerators.get_num_elements()? {
97 let element = enumerators.get_property_object_by_offset(index, NO_OPTIONS)?;
98 let value = element.get_val_number("", COERCE_TO_NUMBER)?;
99 println!(
100 " {} -> {value}",
101 element.get_value_display_name("", NO_OPTIONS)?
102 );
103 }
104 Ok(())
105}Sourcepub fn enumerators(&self) -> Result<Self, Error>
pub fn enumerators(&self) -> Result<Self, Error>
An enumeration’s enumerators (PropertyObject.Enumerators).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
86fn print_enumerators(definition: &PropertyObject) -> Result<(), rs_teststand::Error> {
87 let enumerators = definition.enumerators()?;
88 let strict = enumerators
89 .attributes()?
90 .get_val_bool(IS_STRICT_ATTRIBUTE, NO_OPTIONS)?;
91 println!(
92 " {} v{} (strict={strict}):",
93 definition.name()?,
94 definition.type_version()?
95 );
96 for index in 0..enumerators.get_num_elements()? {
97 let element = enumerators.get_property_object_by_offset(index, NO_OPTIONS)?;
98 let value = element.get_val_number("", COERCE_TO_NUMBER)?;
99 println!(
100 " {} -> {value}",
101 element.get_value_display_name("", NO_OPTIONS)?
102 );
103 }
104 Ok(())
105}Sourcepub fn update_enumerators(&self, enumerators: &Self) -> Result<bool, Error>
pub fn update_enumerators(&self, enumerators: &Self) -> Result<bool, Error>
Replaces an enumeration’s enumerators (UpdateEnumerators).
Expects an array of containers, each holding EnumeratorName and
EnumeratorValue. Strictness rides on the array’s attributes rather
than on an element.
This only has an effect on a registered type definition; calling it on the loose object that was inserted changes nothing. Every loaded instance of the type is updated.
§Errors
Error if the COM call fails or the argument is not a live object.
Examples found in repository?
66fn register_enum(
67 engine: &Engine,
68 types: &TypeUsageList,
69 name: &str,
70 named_values: &[(&str, f64)],
71 strict: bool,
72) -> Result<PropertyObject, rs_teststand::Error> {
73 let enum_type = engine.new_property_object(PropValType::Enum, false, "", NO_OPTIONS)?;
74 enum_type.set_name(name)?;
75 types.insert_type(
76 &enum_type,
77 types.num_types()?,
78 TypeCategory::CustomDataTypes,
79 )?;
80
81 let definition = types.get_type_definition(types.get_type_index(name)?)?;
82 definition.update_enumerators(&enumerator_array(engine, named_values, strict)?)?;
83 Ok(definition)
84}
85
86fn print_enumerators(definition: &PropertyObject) -> Result<(), rs_teststand::Error> {
87 let enumerators = definition.enumerators()?;
88 let strict = enumerators
89 .attributes()?
90 .get_val_bool(IS_STRICT_ATTRIBUTE, NO_OPTIONS)?;
91 println!(
92 " {} v{} (strict={strict}):",
93 definition.name()?,
94 definition.type_version()?
95 );
96 for index in 0..enumerators.get_num_elements()? {
97 let element = enumerators.get_property_object_by_offset(index, NO_OPTIONS)?;
98 let value = element.get_val_number("", COERCE_TO_NUMBER)?;
99 println!(
100 " {} -> {value}",
101 element.get_value_display_name("", NO_OPTIONS)?
102 );
103 }
104 Ok(())
105}
106
107fn main() -> Result<(), Box<dyn std::error::Error>> {
108 let engine = Engine::new()?;
109 let sequence_file = engine.new_sequence_file()?;
110 let file = sequence_file.as_property_object_file()?;
111 let types = file.type_usage_list()?;
112
113 types.insert_type(
114 &build_multimeter_type(&engine)?,
115 types.num_types()?,
116 TypeCategory::CustomDataTypes,
117 )?;
118 let coupling = register_enum(
119 &engine,
120 &types,
121 "Coupling",
122 &[("AC", 0.0), ("DC", 1.0)],
123 true,
124 )?;
125
126 // A variable of the enum type, so the change below has an instance to update.
127 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
128 main_sequence.locals()?.new_sub_property(
129 "InputCoupling",
130 PropValType::NamedType,
131 false,
132 "Coupling",
133 INSERT_IF_MISSING,
134 )?;
135
136 println!(
137 "Registered custom data types ({} in file):",
138 types.num_types()?
139 );
140 print_enumerators(&coupling)?;
141
142 // Evolve it: add an enumerator and raise the version.
143 println!(
144 "\nCoupling version before update: {}",
145 coupling.type_version()?
146 );
147 coupling.update_enumerators(&enumerator_array(
148 &engine,
149 &[("AC", 0.0), ("DC", 1.0), ("GND", 2.0)],
150 true,
151 )?)?;
152
153 // Raising the lowest field signals a change the engine applies silently;
154 // raising a higher one marks it as deliberate.
155 let version = coupling.type_version()?;
156 let mut fields = version.split('.');
157 let major: u32 = fields.next().unwrap_or("0").parse().unwrap_or(0);
158 let minor: u32 = fields.next().unwrap_or("0").parse().unwrap_or(0);
159 coupling.set_type_version(&format!("{major}.{}.0.0", minor + 1))?;
160 println!(
161 "Coupling version after update: {}",
162 coupling.type_version()?
163 );
164
165 println!("\nCoupling now defines (InputCoupling reflects this):");
166 print_enumerators(&coupling)?;
167
168 // Saving does nothing unless the file believes it changed.
169 file.inc_change_count()?;
170 let path = std::env::temp_dir().join("rs_teststand_with_custom_types.seq");
171 sequence_file.save(&path.to_string_lossy())?;
172 println!("\nSaved to {}", path.display());
173
174 engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
175 Ok(())
176}Sourcepub fn get_value_display_name(
&self,
lookup_string: &str,
options: i32,
) -> Result<String, Error>
pub fn get_value_display_name( &self, lookup_string: &str, options: i32, ) -> Result<String, Error>
The display name of a value (GetValueDisplayName).
For an enumeration this is the enumerator’s name rather than its number.
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
86fn print_enumerators(definition: &PropertyObject) -> Result<(), rs_teststand::Error> {
87 let enumerators = definition.enumerators()?;
88 let strict = enumerators
89 .attributes()?
90 .get_val_bool(IS_STRICT_ATTRIBUTE, NO_OPTIONS)?;
91 println!(
92 " {} v{} (strict={strict}):",
93 definition.name()?,
94 definition.type_version()?
95 );
96 for index in 0..enumerators.get_num_elements()? {
97 let element = enumerators.get_property_object_by_offset(index, NO_OPTIONS)?;
98 let value = element.get_val_number("", COERCE_TO_NUMBER)?;
99 println!(
100 " {} -> {value}",
101 element.get_value_display_name("", NO_OPTIONS)?
102 );
103 }
104 Ok(())
105}Sourcepub fn evaluate(&self, expression: &str) -> Result<Self, Error>
pub fn evaluate(&self, expression: &str) -> Result<Self, Error>
Evaluates an expression in this object’s context (Evaluate).
Superseded by evaluate_ex, which adds an options
argument. This form is kept because it is the member available on
engines from TestStand 2016, which the crate supports, a caller
targeting the whole range can use it without a version check.
§Errors
Error if the expression is invalid or the COM call fails.
Sourcepub fn evaluate_ex(&self, expression: &str, options: i32) -> Result<Self, Error>
pub fn evaluate_ex(&self, expression: &str, options: i32) -> Result<Self, Error>
Evaluates an expression in this object’s context (EvaluateEx).
The object is the scope: the expression can name this property’s
subproperties directly. The result comes back as a PropertyObject
holding whatever type the expression produced, so read it with the
accessor that matches, or with to_value.
Evaluate is the obsolete form of this member; use this one.
§Errors
Error if the expression is invalid or the COM call fails.
Sourcepub fn property_type(&self) -> Result<PropertyObjectType, Error>
pub fn property_type(&self) -> Result<PropertyObjectType, Error>
Sourcepub fn get_type_display_string(
&self,
lookup_string: &str,
options: i32,
) -> Result<String, Error>
pub fn get_type_display_string( &self, lookup_string: &str, options: i32, ) -> Result<String, Error>
A human-readable name for a property’s type (GetTypeDisplayString).
Obsolete in the engine; prefer
property_type then display_string.
This is the in-only route to identifying a type. GetType reports the
same thing in more detail but returns three of its five arguments by
reference, which the dispatch seam does not yet support.
§Errors
Error if the COM call fails or returns an unexpected type.
Sourcepub fn get_type_flags(
&self,
lookup_string: &str,
options: i32,
) -> Result<PropertyValueTypeFlags, Error>
pub fn get_type_flags( &self, lookup_string: &str, options: i32, ) -> Result<PropertyValueTypeFlags, Error>
Sourcepub fn get_property_object_by_offset(
&self,
offset: i32,
options: i32,
) -> Result<Self, Error>
pub fn get_property_object_by_offset( &self, offset: i32, options: i32, ) -> Result<Self, Error>
An array element by position (GetPropertyObjectByOffset).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
33fn describe_templates(engine: &Engine) -> Result<(), Error> {
34 let templates_file = engine.get_templates_file(GetTemplatesFileOptions::LOAD_IF_NOT_LOADED)?;
35 let root = templates_file
36 .data()?
37 .get_property_object("Root", PropertyOptions::NONE.bits())?;
38 for index in 0..root.get_num_elements()? {
39 let category = root.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
40 if category.name()? == "Steps" {
41 println!(
42 "Step templates defined on this station: {}",
43 category.get_num_elements()?
44 );
45 }
46 }
47 Ok(())
48}More examples
40fn describe_station_templates(engine: &Engine) -> Result<(), Error> {
41 let templates_file = engine.get_templates_file(GetTemplatesFileOptions::LOAD_IF_NOT_LOADED)?;
42 let root = templates_file
43 .data()?
44 .get_property_object("Root", PropertyOptions::NONE.bits())?;
45
46 println!("Station templates file: {}", templates_file.path()?);
47 for index in 0..root.get_num_elements()? {
48 let category = root.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
49 println!(
50 " {}: {} template(s)",
51 category.name()?,
52 category.get_num_elements()?
53 );
54 }
55 Ok(())
56}
57
58/// An empty array container to keep prototypes in.
59fn new_template_group(engine: &Engine) -> Result<PropertyObject, Error> {
60 engine.new_property_object(
61 PropValType::Container,
62 true,
63 "",
64 PropertyOptions::NONE.bits(),
65 )
66}
67
68/// Appends a detached copy of a prototype to the group.
69///
70/// The copy matters: the group must not share the object the caller keeps
71/// editing, or adding a second template would change the first.
72fn append_template(group: &PropertyObject, template: &PropertyObject) -> Result<(), Error> {
73 let index = group.get_num_elements()?;
74 group.set_num_elements(index + 1, PropertyOptions::NONE.bits())?;
75 group.set_property_object(
76 &format!("[{index}]"),
77 PropertyOptions::NONE.bits(),
78 &template.clone_property("", PropertyOptions::NONE.bits())?,
79 )
80}
81
82/// Finds a prototype in the group by name.
83fn find_template(group: &PropertyObject, name: &str) -> Result<Option<PropertyObject>, Error> {
84 for index in 0..group.get_num_elements()? {
85 let candidate = group.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
86 if candidate.name()? == name {
87 return Ok(Some(candidate));
88 }
89 }
90 Ok(None)
91}43fn enumerator_array(
44 engine: &Engine,
45 named_values: &[(&str, f64)],
46 strict: bool,
47) -> Result<PropertyObject, rs_teststand::Error> {
48 let array = engine.new_property_object(PropValType::Container, true, "", NO_OPTIONS)?;
49 array.set_num_elements(i32::try_from(named_values.len()).unwrap_or(0), NO_OPTIONS)?;
50 for (index, (name, value)) in named_values.iter().enumerate() {
51 let element =
52 array.get_property_object_by_offset(i32::try_from(index).unwrap_or(0), NO_OPTIONS)?;
53 element.set_val_string("EnumeratorName", INSERT_IF_MISSING, name)?;
54 element.set_val_number("EnumeratorValue", INSERT_IF_MISSING, *value)?;
55 }
56 array
57 .attributes()?
58 .set_val_bool(IS_STRICT_ATTRIBUTE, INSERT_IF_MISSING, strict)?;
59 Ok(array)
60}
61
62/// Registers an enumeration, then returns its **registered** definition.
63///
64/// The distinction matters: enumerators can only be set on the definition the
65/// file holds, not on the loose object that was inserted.
66fn register_enum(
67 engine: &Engine,
68 types: &TypeUsageList,
69 name: &str,
70 named_values: &[(&str, f64)],
71 strict: bool,
72) -> Result<PropertyObject, rs_teststand::Error> {
73 let enum_type = engine.new_property_object(PropValType::Enum, false, "", NO_OPTIONS)?;
74 enum_type.set_name(name)?;
75 types.insert_type(
76 &enum_type,
77 types.num_types()?,
78 TypeCategory::CustomDataTypes,
79 )?;
80
81 let definition = types.get_type_definition(types.get_type_index(name)?)?;
82 definition.update_enumerators(&enumerator_array(engine, named_values, strict)?)?;
83 Ok(definition)
84}
85
86fn print_enumerators(definition: &PropertyObject) -> Result<(), rs_teststand::Error> {
87 let enumerators = definition.enumerators()?;
88 let strict = enumerators
89 .attributes()?
90 .get_val_bool(IS_STRICT_ATTRIBUTE, NO_OPTIONS)?;
91 println!(
92 " {} v{} (strict={strict}):",
93 definition.name()?,
94 definition.type_version()?
95 );
96 for index in 0..enumerators.get_num_elements()? {
97 let element = enumerators.get_property_object_by_offset(index, NO_OPTIONS)?;
98 let value = element.get_val_number("", COERCE_TO_NUMBER)?;
99 println!(
100 " {} -> {value}",
101 element.get_value_display_name("", NO_OPTIONS)?
102 );
103 }
104 Ok(())
105}108fn run(engine: &Engine, sequence_file: &SequenceFile, entry_point: &str) -> Result<(), Error> {
109 let execution = engine.new_execution(sequence_file, entry_point, None, false, 0)?;
110 println!("\nRunning {entry_point}...");
111
112 if !wait_for_end(engine, RUN_DEADLINE)? {
113 println!(" did not finish within {RUN_DEADLINE:?}; terminating");
114 execution.terminate()?;
115 return Ok(());
116 }
117 println!(" status: {}", execution.result_status()?);
118
119 let results = execution.result_object()?;
120 if !results.exists("ResultList", none())? {
121 println!(" no results recorded");
122 return Ok(());
123 }
124 let result_list = results.get_property_object("ResultList", none())?;
125 for index in 0..result_list.get_num_elements()? {
126 let entry = result_list.get_property_object_by_offset(index, none())?;
127 println!(
128 " {}: {}",
129 entry
130 .get_val_string("TS.StepName", none())
131 .unwrap_or_else(|_| "<unnamed>".to_owned()),
132 entry
133 .get_val_string("Status", none())
134 .unwrap_or_else(|_| "<no status>".to_owned())
135 );
136 }
137 Ok(())
138}114fn report(results: &rs_teststand::PropertyObject) -> Result<(), Error> {
115 if !results.exists("ResultList", none())? {
116 println!("No results recorded.");
117 return Ok(());
118 }
119 let result_list = results.get_property_object("ResultList", none())?;
120 let count = result_list.get_num_elements()?;
121 println!(
122 "\n{count} of {} defined test(s) recorded a result:",
123 TESTS.len()
124 );
125 if count < i32::try_from(TESTS.len()).unwrap_or(i32::MAX) {
126 println!(" (a failure ended the run before the rest could execute)");
127 }
128
129 for index in 0..count {
130 let entry = result_list.get_property_object_by_offset(index, none())?;
131 let name = entry
132 .get_val_string("TS.StepName", none())
133 .unwrap_or_else(|_| "<unnamed>".to_owned());
134 let status = entry
135 .get_val_string("Status", none())
136 .unwrap_or_else(|_| "<no status>".to_owned());
137 // Only test steps record a measurement; an Action has no Numeric.
138 match entry.get_val_number("Numeric", none()) {
139 Ok(measured) => println!(" {name}: {status} (measured {measured})"),
140 Err(_) => println!(" {name}: {status}"),
141 }
142 }
143 Ok(())
144}Sourcepub fn set_num_elements(&self, count: i32, options: i32) -> Result<(), Error>
pub fn set_num_elements(&self, count: i32, options: i32) -> Result<(), Error>
Examples found in repository?
More examples
43fn enumerator_array(
44 engine: &Engine,
45 named_values: &[(&str, f64)],
46 strict: bool,
47) -> Result<PropertyObject, rs_teststand::Error> {
48 let array = engine.new_property_object(PropValType::Container, true, "", NO_OPTIONS)?;
49 array.set_num_elements(i32::try_from(named_values.len()).unwrap_or(0), NO_OPTIONS)?;
50 for (index, (name, value)) in named_values.iter().enumerate() {
51 let element =
52 array.get_property_object_by_offset(i32::try_from(index).unwrap_or(0), NO_OPTIONS)?;
53 element.set_val_string("EnumeratorName", INSERT_IF_MISSING, name)?;
54 element.set_val_number("EnumeratorValue", INSERT_IF_MISSING, *value)?;
55 }
56 array
57 .attributes()?
58 .set_val_bool(IS_STRICT_ATTRIBUTE, INSERT_IF_MISSING, strict)?;
59 Ok(array)
60}Sourcepub fn get_num_elements(&self) -> Result<i32, Error>
pub fn get_num_elements(&self) -> Result<i32, Error>
The number of elements in an array property (GetNumElements).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
33fn describe_templates(engine: &Engine) -> Result<(), Error> {
34 let templates_file = engine.get_templates_file(GetTemplatesFileOptions::LOAD_IF_NOT_LOADED)?;
35 let root = templates_file
36 .data()?
37 .get_property_object("Root", PropertyOptions::NONE.bits())?;
38 for index in 0..root.get_num_elements()? {
39 let category = root.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
40 if category.name()? == "Steps" {
41 println!(
42 "Step templates defined on this station: {}",
43 category.get_num_elements()?
44 );
45 }
46 }
47 Ok(())
48}More examples
40fn describe_station_templates(engine: &Engine) -> Result<(), Error> {
41 let templates_file = engine.get_templates_file(GetTemplatesFileOptions::LOAD_IF_NOT_LOADED)?;
42 let root = templates_file
43 .data()?
44 .get_property_object("Root", PropertyOptions::NONE.bits())?;
45
46 println!("Station templates file: {}", templates_file.path()?);
47 for index in 0..root.get_num_elements()? {
48 let category = root.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
49 println!(
50 " {}: {} template(s)",
51 category.name()?,
52 category.get_num_elements()?
53 );
54 }
55 Ok(())
56}
57
58/// An empty array container to keep prototypes in.
59fn new_template_group(engine: &Engine) -> Result<PropertyObject, Error> {
60 engine.new_property_object(
61 PropValType::Container,
62 true,
63 "",
64 PropertyOptions::NONE.bits(),
65 )
66}
67
68/// Appends a detached copy of a prototype to the group.
69///
70/// The copy matters: the group must not share the object the caller keeps
71/// editing, or adding a second template would change the first.
72fn append_template(group: &PropertyObject, template: &PropertyObject) -> Result<(), Error> {
73 let index = group.get_num_elements()?;
74 group.set_num_elements(index + 1, PropertyOptions::NONE.bits())?;
75 group.set_property_object(
76 &format!("[{index}]"),
77 PropertyOptions::NONE.bits(),
78 &template.clone_property("", PropertyOptions::NONE.bits())?,
79 )
80}
81
82/// Finds a prototype in the group by name.
83fn find_template(group: &PropertyObject, name: &str) -> Result<Option<PropertyObject>, Error> {
84 for index in 0..group.get_num_elements()? {
85 let candidate = group.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
86 if candidate.name()? == name {
87 return Ok(Some(candidate));
88 }
89 }
90 Ok(None)
91}
92
93/// Turns "not found" into an error naming the template that is missing.
94fn require(found: Option<PropertyObject>, name: &'static str) -> Result<PropertyObject, Error> {
95 found.ok_or(Error::UnexpectedType {
96 expected: name,
97 actual: "no template of that name in the group",
98 })
99}
100
101/// A configured Statement step, taken as a property tree so it can be cloned.
102fn step_template(engine: &Engine) -> Result<PropertyObject, Error> {
103 let step = engine.new_step(NO_ADAPTER, "Statement")?;
104 step.set_name(STEP_TEMPLATE)?;
105 step.set_post_expression(r#"Locals.Result = "Hello from step template!""#)?;
106 step.as_property_object()
107}
108
109/// A whole sequence, one step included, as a single prototype.
110fn sequence_template(engine: &Engine) -> Result<PropertyObject, Error> {
111 let sequence = engine.new_sequence()?;
112 sequence.set_name(SEQUENCE_TEMPLATE)?;
113
114 let step = engine.new_step(NO_ADAPTER, "Statement")?;
115 step.set_name("Inside_Sequence_Template")?;
116 sequence.insert_step(&step, 0, StepGroup::Main)?;
117
118 sequence.as_property_object()
119}
120
121/// A string variable carrying its default value.
122fn variable_template(engine: &Engine) -> Result<PropertyObject, Error> {
123 let variable =
124 engine.new_property_object(PropValType::String, false, "", PropertyOptions::NONE.bits())?;
125 variable.set_name(VARIABLE_TEMPLATE)?;
126 variable.set_val_string("", PropertyOptions::NONE.bits(), "Template Variable Value")?;
127 Ok(variable)
128}
129
130/// Stamps one copy of each template into a file that is already on disk.
131fn apply_templates(
132 sequence_file: &SequenceFile,
133 step: &PropertyObject,
134 sequence: &PropertyObject,
135 variable: &PropertyObject,
136) -> Result<(), Error> {
137 let main_sequence = sequence_file.get_sequence_by_name(MAIN_SEQUENCE)?;
138
139 // Inserting returns the copy on the Step interface, so it can be renamed
140 // and re-identified straight away. Without a new step ID the copy would go
141 // on claiming the prototype's identity.
142 let inserted_step = main_sequence.insert_step_from_template(step, 0, StepGroup::Main)?;
143 inserted_step.set_name("Step From Template")?;
144 inserted_step.create_new_unique_step_id()?;
145 println!(" step: {}", inserted_step.name()?);
146
147 let inserted_sequence = sequence_file.insert_sequence_from_template(sequence)?;
148 inserted_sequence.create_new_unique_step_ids()?;
149 println!(
150 " sequence: {} ({} step(s))",
151 inserted_sequence.name()?,
152 inserted_sequence.get_num_steps(StepGroup::Main)?
153 );
154
155 // A variable is the easy case: Locals is a property tree, so a clone drops
156 // straight in under the template's own name.
157 let locals = main_sequence.locals()?;
158 locals.set_property_object(
159 &variable.name()?,
160 PropertyOptions::INSERT_IF_MISSING.bits(),
161 &variable.clone_property("", PropertyOptions::NONE.bits())?,
162 )?;
163 println!(
164 " variable: {} = {:?}",
165 variable.name()?,
166 locals.get_val_string(&variable.name()?, PropertyOptions::NONE.bits())?
167 );
168
169 // A file that does not believe it changed will not write anything.
170 sequence_file
171 .as_property_object_file()?
172 .inc_change_count()?;
173 Ok(())
174}
175
176fn main() -> Result<(), Box<dyn std::error::Error>> {
177 let engine = Engine::new()?;
178 describe_station_templates(&engine)?;
179
180 println!("\nBuilding an in-memory template group...");
181 let group = new_template_group(&engine)?;
182 append_template(&group, &step_template(&engine)?)?;
183 append_template(&group, &sequence_template(&engine)?)?;
184 append_template(&group, &variable_template(&engine)?)?;
185 println!(" {} template(s) stored", group.get_num_elements()?);
186
187 let step = require(find_template(&group, STEP_TEMPLATE)?, STEP_TEMPLATE)?;
188 let sequence = require(find_template(&group, SEQUENCE_TEMPLATE)?, SEQUENCE_TEMPLATE)?;
189 let variable = require(find_template(&group, VARIABLE_TEMPLATE)?, VARIABLE_TEMPLATE)?;
190
191 // Saving first, then reopening, is deliberate: templates are worth having
192 // because they are applied to files a program did not build in this run.
193 let path = std::env::temp_dir().join("rs_teststand_from_templates.seq");
194 let path = path.to_string_lossy().into_owned();
195 engine.new_sequence_file()?.save(&path)?;
196
197 println!("\nApplying templates to the saved file...");
198 let target = engine.get_sequence_file_ex(
199 &path,
200 GetSeqFileOptions::DO_NOT_RUN_LOAD_CALLBACK,
201 rs_teststand::ConflictHandler::Error,
202 )?;
203 apply_templates(&target, &step, &sequence, &variable)?;
204 target.save(&path)?;
205 println!("\nSaved to {path}");
206
207 engine.release_sequence_file_ex(target, PropertyOptions::NONE.bits())?;
208 Ok(())
209}86fn print_enumerators(definition: &PropertyObject) -> Result<(), rs_teststand::Error> {
87 let enumerators = definition.enumerators()?;
88 let strict = enumerators
89 .attributes()?
90 .get_val_bool(IS_STRICT_ATTRIBUTE, NO_OPTIONS)?;
91 println!(
92 " {} v{} (strict={strict}):",
93 definition.name()?,
94 definition.type_version()?
95 );
96 for index in 0..enumerators.get_num_elements()? {
97 let element = enumerators.get_property_object_by_offset(index, NO_OPTIONS)?;
98 let value = element.get_val_number("", COERCE_TO_NUMBER)?;
99 println!(
100 " {} -> {value}",
101 element.get_value_display_name("", NO_OPTIONS)?
102 );
103 }
104 Ok(())
105}108fn run(engine: &Engine, sequence_file: &SequenceFile, entry_point: &str) -> Result<(), Error> {
109 let execution = engine.new_execution(sequence_file, entry_point, None, false, 0)?;
110 println!("\nRunning {entry_point}...");
111
112 if !wait_for_end(engine, RUN_DEADLINE)? {
113 println!(" did not finish within {RUN_DEADLINE:?}; terminating");
114 execution.terminate()?;
115 return Ok(());
116 }
117 println!(" status: {}", execution.result_status()?);
118
119 let results = execution.result_object()?;
120 if !results.exists("ResultList", none())? {
121 println!(" no results recorded");
122 return Ok(());
123 }
124 let result_list = results.get_property_object("ResultList", none())?;
125 for index in 0..result_list.get_num_elements()? {
126 let entry = result_list.get_property_object_by_offset(index, none())?;
127 println!(
128 " {}: {}",
129 entry
130 .get_val_string("TS.StepName", none())
131 .unwrap_or_else(|_| "<unnamed>".to_owned()),
132 entry
133 .get_val_string("Status", none())
134 .unwrap_or_else(|_| "<no status>".to_owned())
135 );
136 }
137 Ok(())
138}114fn report(results: &rs_teststand::PropertyObject) -> Result<(), Error> {
115 if !results.exists("ResultList", none())? {
116 println!("No results recorded.");
117 return Ok(());
118 }
119 let result_list = results.get_property_object("ResultList", none())?;
120 let count = result_list.get_num_elements()?;
121 println!(
122 "\n{count} of {} defined test(s) recorded a result:",
123 TESTS.len()
124 );
125 if count < i32::try_from(TESTS.len()).unwrap_or(i32::MAX) {
126 println!(" (a failure ended the run before the rest could execute)");
127 }
128
129 for index in 0..count {
130 let entry = result_list.get_property_object_by_offset(index, none())?;
131 let name = entry
132 .get_val_string("TS.StepName", none())
133 .unwrap_or_else(|_| "<unnamed>".to_owned());
134 let status = entry
135 .get_val_string("Status", none())
136 .unwrap_or_else(|_| "<no status>".to_owned());
137 // Only test steps record a measurement; an Action has no Numeric.
138 match entry.get_val_number("Numeric", none()) {
139 Ok(measured) => println!(" {name}: {status} (measured {measured})"),
140 Err(_) => println!(" {name}: {status}"),
141 }
142 }
143 Ok(())
144}29fn main() -> Result<(), rs_teststand::Error> {
30 let engine = Engine::new()?;
31
32 // Passing no profile means the user inherits nothing.
33 let user = engine.new_user(None)?;
34 user.set_login_name("operator1")?;
35 user.set_full_name("Test Operator")?;
36 user.set_password("ts-secret")?;
37
38 println!("User: {} ({})", user.login_name()?, user.full_name()?);
39
40 // Check a credential without handling the stored value.
41 println!(
42 " password 'ts-secret' valid: {}",
43 user.validate_password("ts-secret")?
44 );
45 println!(
46 " password 'wrong' valid: {}",
47 user.validate_password("wrong")?
48 );
49
50 println!(" privilege checks:");
51 report_privileges(
52 &user,
53 &[
54 UserPrivilege::Operate,
55 UserPrivilege::Execute,
56 UserPrivilege::Develop,
57 UserPrivilege::Debug,
58 UserPrivilege::EditUsers,
59 ],
60 )?;
61
62 // A privilege can also be named by its full path, which is how a leaf
63 // inside a category is reached.
64 println!(
65 " Debug.RunSelectedTests: {}",
66 user.has_privilege_named("Debug.RunSelectedTests")?
67 );
68
69 // The privilege tree is nested: categories hold the individual rights.
70 let privileges = user.privileges()?;
71 let count = privileges.get_num_sub_properties("")?;
72 println!(" privilege tree ({count} categories):");
73 for index in 0..count {
74 let name = privileges.get_nth_sub_property_name("", index, 0)?;
75 let members = privileges
76 .get_property_object(&name, 0)?
77 .get_num_sub_properties("")?;
78 println!(" {name:<12} {members} member(s)");
79 }
80
81 // The station itself, read-only.
82 println!("\nStation:");
83 match engine.current_user()? {
84 Some(current) => println!(" logged in as {}", current.login_name()?),
85 None => println!(" nobody logged in (usual when login is not required)"),
86 }
87 println!(
88 " is 'operator1' a real account here? {}",
89 engine.user_name_exists("operator1")?
90 );
91
92 // Where accounts actually live. Everything above this point exists only in
93 // memory: `new_user` builds a User, it does not enrol one. A user becomes
94 // part of the station when it is inserted into this file's list and the
95 // file is written.
96 let users_file = engine.users_file()?;
97 let file = users_file.as_property_object_file()?;
98 println!(
99 "
100Users file:"
101 );
102 println!(" path {}", file.path()?);
103 println!(
104 " users {}",
105 users_file.user_list()?.get_num_elements()?
106 );
107 println!(
108 " groups {}",
109 users_file.user_group_list()?.get_num_elements()?
110 );
111 println!(
112 " profiles {}",
113 users_file.user_profile_list()?.get_num_elements()?
114 );
115
116 // Deliberately not run: it would edit the station this example is reading.
117 // The two calls a host makes to enrol the user built above are
118 //
119 // users_file.user_list()?.set_num_elements(count + 1, 0)?; // then fill
120 // file.save_file_if_modified(false)?; // then write
121 //
122 // `false` matters: with `true` the engine puts a save dialog on screen, and
123 // a host with no operator would wait for an answer that never comes.
124 println!(
125 "
126 (this example does not write; enrolling a user would alter the station)"
127 );
128
129 Ok(())
130}Sourcepub fn get_property_object(
&self,
lookup_string: &str,
options: i32,
) -> Result<Self, Error>
pub fn get_property_object( &self, lookup_string: &str, options: i32, ) -> Result<Self, Error>
Retrieves a nested PropertyObject by lookup path (PropertyObject.GetPropertyObject).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
33fn describe_templates(engine: &Engine) -> Result<(), Error> {
34 let templates_file = engine.get_templates_file(GetTemplatesFileOptions::LOAD_IF_NOT_LOADED)?;
35 let root = templates_file
36 .data()?
37 .get_property_object("Root", PropertyOptions::NONE.bits())?;
38 for index in 0..root.get_num_elements()? {
39 let category = root.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
40 if category.name()? == "Steps" {
41 println!(
42 "Step templates defined on this station: {}",
43 category.get_num_elements()?
44 );
45 }
46 }
47 Ok(())
48}More examples
40fn describe_station_templates(engine: &Engine) -> Result<(), Error> {
41 let templates_file = engine.get_templates_file(GetTemplatesFileOptions::LOAD_IF_NOT_LOADED)?;
42 let root = templates_file
43 .data()?
44 .get_property_object("Root", PropertyOptions::NONE.bits())?;
45
46 println!("Station templates file: {}", templates_file.path()?);
47 for index in 0..root.get_num_elements()? {
48 let category = root.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
49 println!(
50 " {}: {} template(s)",
51 category.name()?,
52 category.get_num_elements()?
53 );
54 }
55 Ok(())
56}108fn run(engine: &Engine, sequence_file: &SequenceFile, entry_point: &str) -> Result<(), Error> {
109 let execution = engine.new_execution(sequence_file, entry_point, None, false, 0)?;
110 println!("\nRunning {entry_point}...");
111
112 if !wait_for_end(engine, RUN_DEADLINE)? {
113 println!(" did not finish within {RUN_DEADLINE:?}; terminating");
114 execution.terminate()?;
115 return Ok(());
116 }
117 println!(" status: {}", execution.result_status()?);
118
119 let results = execution.result_object()?;
120 if !results.exists("ResultList", none())? {
121 println!(" no results recorded");
122 return Ok(());
123 }
124 let result_list = results.get_property_object("ResultList", none())?;
125 for index in 0..result_list.get_num_elements()? {
126 let entry = result_list.get_property_object_by_offset(index, none())?;
127 println!(
128 " {}: {}",
129 entry
130 .get_val_string("TS.StepName", none())
131 .unwrap_or_else(|_| "<unnamed>".to_owned()),
132 entry
133 .get_val_string("Status", none())
134 .unwrap_or_else(|_| "<no status>".to_owned())
135 );
136 }
137 Ok(())
138}114fn report(results: &rs_teststand::PropertyObject) -> Result<(), Error> {
115 if !results.exists("ResultList", none())? {
116 println!("No results recorded.");
117 return Ok(());
118 }
119 let result_list = results.get_property_object("ResultList", none())?;
120 let count = result_list.get_num_elements()?;
121 println!(
122 "\n{count} of {} defined test(s) recorded a result:",
123 TESTS.len()
124 );
125 if count < i32::try_from(TESTS.len()).unwrap_or(i32::MAX) {
126 println!(" (a failure ended the run before the rest could execute)");
127 }
128
129 for index in 0..count {
130 let entry = result_list.get_property_object_by_offset(index, none())?;
131 let name = entry
132 .get_val_string("TS.StepName", none())
133 .unwrap_or_else(|_| "<unnamed>".to_owned());
134 let status = entry
135 .get_val_string("Status", none())
136 .unwrap_or_else(|_| "<no status>".to_owned());
137 // Only test steps record a measurement; an Action has no Numeric.
138 match entry.get_val_number("Numeric", none()) {
139 Ok(measured) => println!(" {name}: {status} (measured {measured})"),
140 Err(_) => println!(" {name}: {status}"),
141 }
142 }
143 Ok(())
144}100fn show_station_globals(engine: &Engine) -> Result<(), rs_teststand::Error> {
101 // Station globals: shared by every sequence file, and written to disk.
102 let station_globals = engine.globals()?;
103 if !station_globals.exists("StationInfo", 0)? {
104 station_globals.new_sub_property(
105 "StationInfo",
106 PropValType::Container,
107 false,
108 "",
109 INSERT_IF_MISSING,
110 )?;
111 }
112 let station_info = station_globals.get_property_object("StationInfo", 0)?;
113
114 // A container holds mixed types, so one station record can carry the name,
115 // a count and a flag without three separate globals.
116 set_string(&station_info, "StationName", "STATION_01")?;
117 station_info.set_val_number("CalibrationIntervalDays", INSERT_IF_MISSING, 90.0)?;
118 station_info.set_val_bool("FixtureInstalled", INSERT_IF_MISSING, true)?;
119
120 // A 64-bit count. `SetValNumber` stores a double, which starts losing whole
121 // numbers past 2^53, so anything that counts for the life of a station wants
122 // the integer path instead.
123 station_info.set_val_integer64("UnitsTestedTotal", INSERT_IF_MISSING, 9_007_199_254_740_993)?;
124
125 // A nested container, because station data is rarely one level deep.
126 if !station_info.exists("LastCalibration", 0)? {
127 station_info.new_sub_property(
128 "LastCalibration",
129 PropValType::Container,
130 false,
131 "",
132 INSERT_IF_MISSING,
133 )?;
134 }
135 let calibration = station_info.get_property_object("LastCalibration", 0)?;
136 set_string(&calibration, "Technician", "R. Alvarez")?;
137 set_string(&calibration, "Date", "2026-06-14")?;
138
139 println!("StationGlobals.StationInfo:");
140 println!(
141 " StationName = '{}'",
142 station_info.get_val_string("StationName", 0)?
143 );
144 println!(
145 " CalibrationIntervalDays = {}",
146 station_info.get_val_number("CalibrationIntervalDays", 0)?
147 );
148 println!(
149 " FixtureInstalled = {}",
150 station_info.get_val_bool("FixtureInstalled", 0)?
151 );
152 println!(
153 " UnitsTestedTotal = {}",
154 station_info.get_val_integer64("UnitsTestedTotal", 0)?
155 );
156 println!(
157 " LastCalibration.Technician = '{}' on {}",
158 calibration.get_val_string("Technician", 0)?,
159 calibration.get_val_string("Date", 0)?
160 );
161
162 // Walk the container rather than naming each field, which is what a host
163 // does when it does not know the shape in advance.
164 println!(
165 " walked, {} field(s):",
166 station_info.get_num_sub_properties("")?
167 );
168 for index in 0..station_info.get_num_sub_properties("")? {
169 println!(
170 " {}",
171 station_info.get_nth_sub_property_name("", index, 0)?
172 );
173 }
174
175 // Station globals live in memory until this is called. Without it the values
176 // above are gone when the engine goes away, which is the difference between
177 // a station global and a file global.
178 // `false` means do not prompt if another process changed the file first.
179 // An example must never raise a dialog, and neither must a headless host.
180 engine.commit_globals_to_disk(false)?;
181 println!(" committed to disk");
182
183 Ok(())
184}29fn main() -> Result<(), rs_teststand::Error> {
30 let engine = Engine::new()?;
31
32 // Passing no profile means the user inherits nothing.
33 let user = engine.new_user(None)?;
34 user.set_login_name("operator1")?;
35 user.set_full_name("Test Operator")?;
36 user.set_password("ts-secret")?;
37
38 println!("User: {} ({})", user.login_name()?, user.full_name()?);
39
40 // Check a credential without handling the stored value.
41 println!(
42 " password 'ts-secret' valid: {}",
43 user.validate_password("ts-secret")?
44 );
45 println!(
46 " password 'wrong' valid: {}",
47 user.validate_password("wrong")?
48 );
49
50 println!(" privilege checks:");
51 report_privileges(
52 &user,
53 &[
54 UserPrivilege::Operate,
55 UserPrivilege::Execute,
56 UserPrivilege::Develop,
57 UserPrivilege::Debug,
58 UserPrivilege::EditUsers,
59 ],
60 )?;
61
62 // A privilege can also be named by its full path, which is how a leaf
63 // inside a category is reached.
64 println!(
65 " Debug.RunSelectedTests: {}",
66 user.has_privilege_named("Debug.RunSelectedTests")?
67 );
68
69 // The privilege tree is nested: categories hold the individual rights.
70 let privileges = user.privileges()?;
71 let count = privileges.get_num_sub_properties("")?;
72 println!(" privilege tree ({count} categories):");
73 for index in 0..count {
74 let name = privileges.get_nth_sub_property_name("", index, 0)?;
75 let members = privileges
76 .get_property_object(&name, 0)?
77 .get_num_sub_properties("")?;
78 println!(" {name:<12} {members} member(s)");
79 }
80
81 // The station itself, read-only.
82 println!("\nStation:");
83 match engine.current_user()? {
84 Some(current) => println!(" logged in as {}", current.login_name()?),
85 None => println!(" nobody logged in (usual when login is not required)"),
86 }
87 println!(
88 " is 'operator1' a real account here? {}",
89 engine.user_name_exists("operator1")?
90 );
91
92 // Where accounts actually live. Everything above this point exists only in
93 // memory: `new_user` builds a User, it does not enrol one. A user becomes
94 // part of the station when it is inserted into this file's list and the
95 // file is written.
96 let users_file = engine.users_file()?;
97 let file = users_file.as_property_object_file()?;
98 println!(
99 "
100Users file:"
101 );
102 println!(" path {}", file.path()?);
103 println!(
104 " users {}",
105 users_file.user_list()?.get_num_elements()?
106 );
107 println!(
108 " groups {}",
109 users_file.user_group_list()?.get_num_elements()?
110 );
111 println!(
112 " profiles {}",
113 users_file.user_profile_list()?.get_num_elements()?
114 );
115
116 // Deliberately not run: it would edit the station this example is reading.
117 // The two calls a host makes to enrol the user built above are
118 //
119 // users_file.user_list()?.set_num_elements(count + 1, 0)?; // then fill
120 // file.save_file_if_modified(false)?; // then write
121 //
122 // `false` matters: with `true` the engine puts a save dialog on screen, and
123 // a host with no operator would wait for an answer that never comes.
124 println!(
125 "
126 (this example does not write; enrolling a user would alter the station)"
127 );
128
129 Ok(())
130}Sourcepub fn set_property_object(
&self,
lookup_string: &str,
options: i32,
property_object_value: &Self,
) -> Result<(), Error>
pub fn set_property_object( &self, lookup_string: &str, options: i32, property_object_value: &Self, ) -> Result<(), Error>
Attaches a nested PropertyObject by lookup path (PropertyObject.SetPropertyObject).
§Errors
Error if the COM call fails.
Examples found in repository?
72fn append_template(group: &PropertyObject, template: &PropertyObject) -> Result<(), Error> {
73 let index = group.get_num_elements()?;
74 group.set_num_elements(index + 1, PropertyOptions::NONE.bits())?;
75 group.set_property_object(
76 &format!("[{index}]"),
77 PropertyOptions::NONE.bits(),
78 &template.clone_property("", PropertyOptions::NONE.bits())?,
79 )
80}
81
82/// Finds a prototype in the group by name.
83fn find_template(group: &PropertyObject, name: &str) -> Result<Option<PropertyObject>, Error> {
84 for index in 0..group.get_num_elements()? {
85 let candidate = group.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
86 if candidate.name()? == name {
87 return Ok(Some(candidate));
88 }
89 }
90 Ok(None)
91}
92
93/// Turns "not found" into an error naming the template that is missing.
94fn require(found: Option<PropertyObject>, name: &'static str) -> Result<PropertyObject, Error> {
95 found.ok_or(Error::UnexpectedType {
96 expected: name,
97 actual: "no template of that name in the group",
98 })
99}
100
101/// A configured Statement step, taken as a property tree so it can be cloned.
102fn step_template(engine: &Engine) -> Result<PropertyObject, Error> {
103 let step = engine.new_step(NO_ADAPTER, "Statement")?;
104 step.set_name(STEP_TEMPLATE)?;
105 step.set_post_expression(r#"Locals.Result = "Hello from step template!""#)?;
106 step.as_property_object()
107}
108
109/// A whole sequence, one step included, as a single prototype.
110fn sequence_template(engine: &Engine) -> Result<PropertyObject, Error> {
111 let sequence = engine.new_sequence()?;
112 sequence.set_name(SEQUENCE_TEMPLATE)?;
113
114 let step = engine.new_step(NO_ADAPTER, "Statement")?;
115 step.set_name("Inside_Sequence_Template")?;
116 sequence.insert_step(&step, 0, StepGroup::Main)?;
117
118 sequence.as_property_object()
119}
120
121/// A string variable carrying its default value.
122fn variable_template(engine: &Engine) -> Result<PropertyObject, Error> {
123 let variable =
124 engine.new_property_object(PropValType::String, false, "", PropertyOptions::NONE.bits())?;
125 variable.set_name(VARIABLE_TEMPLATE)?;
126 variable.set_val_string("", PropertyOptions::NONE.bits(), "Template Variable Value")?;
127 Ok(variable)
128}
129
130/// Stamps one copy of each template into a file that is already on disk.
131fn apply_templates(
132 sequence_file: &SequenceFile,
133 step: &PropertyObject,
134 sequence: &PropertyObject,
135 variable: &PropertyObject,
136) -> Result<(), Error> {
137 let main_sequence = sequence_file.get_sequence_by_name(MAIN_SEQUENCE)?;
138
139 // Inserting returns the copy on the Step interface, so it can be renamed
140 // and re-identified straight away. Without a new step ID the copy would go
141 // on claiming the prototype's identity.
142 let inserted_step = main_sequence.insert_step_from_template(step, 0, StepGroup::Main)?;
143 inserted_step.set_name("Step From Template")?;
144 inserted_step.create_new_unique_step_id()?;
145 println!(" step: {}", inserted_step.name()?);
146
147 let inserted_sequence = sequence_file.insert_sequence_from_template(sequence)?;
148 inserted_sequence.create_new_unique_step_ids()?;
149 println!(
150 " sequence: {} ({} step(s))",
151 inserted_sequence.name()?,
152 inserted_sequence.get_num_steps(StepGroup::Main)?
153 );
154
155 // A variable is the easy case: Locals is a property tree, so a clone drops
156 // straight in under the template's own name.
157 let locals = main_sequence.locals()?;
158 locals.set_property_object(
159 &variable.name()?,
160 PropertyOptions::INSERT_IF_MISSING.bits(),
161 &variable.clone_property("", PropertyOptions::NONE.bits())?,
162 )?;
163 println!(
164 " variable: {} = {:?}",
165 variable.name()?,
166 locals.get_val_string(&variable.name()?, PropertyOptions::NONE.bits())?
167 );
168
169 // A file that does not believe it changed will not write anything.
170 sequence_file
171 .as_property_object_file()?
172 .inc_change_count()?;
173 Ok(())
174}More examples
38fn temporary_variable_lifecycle(container: &PropertyObject) -> Result<(), rs_teststand::Error> {
39 let name = "TempScratch";
40 let clone_name = "TempScratchCopy";
41 for stale in [name, clone_name] {
42 if container.exists(stale, 0)? {
43 container.delete_sub_property(stale, 0)?;
44 }
45 }
46
47 container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
48 container.set_val_string(name, 0, "scratch")?;
49 println!(
50 " created {name} (String) = '{}'",
51 container.get_val_string(name, 0)?
52 );
53
54 container.delete_sub_property(name, 0)?;
55 container.new_sub_property(name, PropValType::Number, false, "", INSERT_IF_MISSING)?;
56 container.set_val_number(name, 0, 42.0)?;
57 println!(
58 " retyped {name} -> Number = {}",
59 container.get_val_number(name, 0)?
60 );
61
62 // clone copies value and type; set_property_object attaches it under a new name.
63 let copy = container.clone_property(name, 0)?;
64 container.set_property_object(clone_name, INSERT_IF_MISSING, ©)?;
65 println!(
66 " cloned {name} -> {clone_name} = {}",
67 container.get_val_number(clone_name, 0)?
68 );
69
70 container.delete_sub_property(clone_name, 0)?;
71 container.delete_sub_property(name, 0)?;
72 println!(
73 " removed both: {name} exists={}, {clone_name} exists={}",
74 container.exists(name, 0)?,
75 container.exists(clone_name, 0)?
76 );
77 Ok(())
78}Sourcepub fn new_sub_property(
&self,
lookup_string: &str,
value_type: PropValType,
as_array: bool,
type_name: &str,
options: i32,
) -> Result<(), Error>
pub fn new_sub_property( &self, lookup_string: &str, value_type: PropValType, as_array: bool, type_name: &str, options: i32, ) -> Result<(), Error>
Examples found in repository?
23fn set_string(
24 container: &PropertyObject,
25 name: &str,
26 value: &str,
27) -> Result<(), rs_teststand::Error> {
28 if !container.exists(name, 0)? {
29 container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
30 }
31 container.set_val_string(name, 0, value)
32}
33
34/// Creates a variable, retypes it, clones it, then removes both.
35///
36/// The type is fixed at creation, so each "retype" is a delete followed by a
37/// fresh create, the same thing the sequence editor does behind the scenes.
38fn temporary_variable_lifecycle(container: &PropertyObject) -> Result<(), rs_teststand::Error> {
39 let name = "TempScratch";
40 let clone_name = "TempScratchCopy";
41 for stale in [name, clone_name] {
42 if container.exists(stale, 0)? {
43 container.delete_sub_property(stale, 0)?;
44 }
45 }
46
47 container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
48 container.set_val_string(name, 0, "scratch")?;
49 println!(
50 " created {name} (String) = '{}'",
51 container.get_val_string(name, 0)?
52 );
53
54 container.delete_sub_property(name, 0)?;
55 container.new_sub_property(name, PropValType::Number, false, "", INSERT_IF_MISSING)?;
56 container.set_val_number(name, 0, 42.0)?;
57 println!(
58 " retyped {name} -> Number = {}",
59 container.get_val_number(name, 0)?
60 );
61
62 // clone copies value and type; set_property_object attaches it under a new name.
63 let copy = container.clone_property(name, 0)?;
64 container.set_property_object(clone_name, INSERT_IF_MISSING, ©)?;
65 println!(
66 " cloned {name} -> {clone_name} = {}",
67 container.get_val_number(clone_name, 0)?
68 );
69
70 container.delete_sub_property(clone_name, 0)?;
71 container.delete_sub_property(name, 0)?;
72 println!(
73 " removed both: {name} exists={}, {clone_name} exists={}",
74 container.exists(name, 0)?,
75 container.exists(clone_name, 0)?
76 );
77 Ok(())
78}
79
80/// Opens a sequence file if one was named on the command line.
81///
82/// Locals, parameters and file globals all need a real file; without one the
83/// example still demonstrates station globals.
84fn open_sequence_file(engine: &Engine) -> Result<Option<SequenceFile>, rs_teststand::Error> {
85 let Some(path) = std::env::args().nth(1) else {
86 return Ok(None);
87 };
88 let file = engine.get_sequence_file_ex(
89 &path,
90 GetSeqFileOptions::DO_NOT_RUN_LOAD_CALLBACK,
91 ConflictHandler::Error,
92 )?;
93 Ok(Some(file))
94}
95
96/// Everything that lives on the station rather than in a file.
97///
98/// Separated because it is the one scope that outlives the process, so it is
99/// the one worth reading on its own.
100fn show_station_globals(engine: &Engine) -> Result<(), rs_teststand::Error> {
101 // Station globals: shared by every sequence file, and written to disk.
102 let station_globals = engine.globals()?;
103 if !station_globals.exists("StationInfo", 0)? {
104 station_globals.new_sub_property(
105 "StationInfo",
106 PropValType::Container,
107 false,
108 "",
109 INSERT_IF_MISSING,
110 )?;
111 }
112 let station_info = station_globals.get_property_object("StationInfo", 0)?;
113
114 // A container holds mixed types, so one station record can carry the name,
115 // a count and a flag without three separate globals.
116 set_string(&station_info, "StationName", "STATION_01")?;
117 station_info.set_val_number("CalibrationIntervalDays", INSERT_IF_MISSING, 90.0)?;
118 station_info.set_val_bool("FixtureInstalled", INSERT_IF_MISSING, true)?;
119
120 // A 64-bit count. `SetValNumber` stores a double, which starts losing whole
121 // numbers past 2^53, so anything that counts for the life of a station wants
122 // the integer path instead.
123 station_info.set_val_integer64("UnitsTestedTotal", INSERT_IF_MISSING, 9_007_199_254_740_993)?;
124
125 // A nested container, because station data is rarely one level deep.
126 if !station_info.exists("LastCalibration", 0)? {
127 station_info.new_sub_property(
128 "LastCalibration",
129 PropValType::Container,
130 false,
131 "",
132 INSERT_IF_MISSING,
133 )?;
134 }
135 let calibration = station_info.get_property_object("LastCalibration", 0)?;
136 set_string(&calibration, "Technician", "R. Alvarez")?;
137 set_string(&calibration, "Date", "2026-06-14")?;
138
139 println!("StationGlobals.StationInfo:");
140 println!(
141 " StationName = '{}'",
142 station_info.get_val_string("StationName", 0)?
143 );
144 println!(
145 " CalibrationIntervalDays = {}",
146 station_info.get_val_number("CalibrationIntervalDays", 0)?
147 );
148 println!(
149 " FixtureInstalled = {}",
150 station_info.get_val_bool("FixtureInstalled", 0)?
151 );
152 println!(
153 " UnitsTestedTotal = {}",
154 station_info.get_val_integer64("UnitsTestedTotal", 0)?
155 );
156 println!(
157 " LastCalibration.Technician = '{}' on {}",
158 calibration.get_val_string("Technician", 0)?,
159 calibration.get_val_string("Date", 0)?
160 );
161
162 // Walk the container rather than naming each field, which is what a host
163 // does when it does not know the shape in advance.
164 println!(
165 " walked, {} field(s):",
166 station_info.get_num_sub_properties("")?
167 );
168 for index in 0..station_info.get_num_sub_properties("")? {
169 println!(
170 " {}",
171 station_info.get_nth_sub_property_name("", index, 0)?
172 );
173 }
174
175 // Station globals live in memory until this is called. Without it the values
176 // above are gone when the engine goes away, which is the difference between
177 // a station global and a file global.
178 // `false` means do not prompt if another process changed the file first.
179 // An example must never raise a dialog, and neither must a headless host.
180 engine.commit_globals_to_disk(false)?;
181 println!(" committed to disk");
182
183 Ok(())
184}More examples
59fn build(engine: &Engine) -> Result<SequenceFile, Error> {
60 let sequence_file = engine.new_sequence_file()?;
61
62 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
63 for name in ["Initialize", "Run Test", "Shut Down"] {
64 add_action(engine, &main_sequence, name)?;
65 }
66
67 let diagnostics = engine.new_sequence()?;
68 diagnostics.set_name(SUBSEQUENCE)?;
69 // A parameter is an ordinary property in the sequence's Parameters scope.
70 diagnostics.parameters()?.new_sub_property(
71 PARAMETER,
72 PropValType::String,
73 false,
74 "",
75 insert_if_missing(),
76 )?;
77 for name in ["Check Power Rails", "Check Clock"] {
78 add_action(engine, &diagnostics, name)?;
79 }
80 sequence_file.insert_sequence(&diagnostics)?;
81
82 Ok(sequence_file)
83}107fn main() -> Result<(), Box<dyn std::error::Error>> {
108 let engine = Engine::new()?;
109 let sequence_file = engine.new_sequence_file()?;
110 let file = sequence_file.as_property_object_file()?;
111 let types = file.type_usage_list()?;
112
113 types.insert_type(
114 &build_multimeter_type(&engine)?,
115 types.num_types()?,
116 TypeCategory::CustomDataTypes,
117 )?;
118 let coupling = register_enum(
119 &engine,
120 &types,
121 "Coupling",
122 &[("AC", 0.0), ("DC", 1.0)],
123 true,
124 )?;
125
126 // A variable of the enum type, so the change below has an instance to update.
127 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
128 main_sequence.locals()?.new_sub_property(
129 "InputCoupling",
130 PropValType::NamedType,
131 false,
132 "Coupling",
133 INSERT_IF_MISSING,
134 )?;
135
136 println!(
137 "Registered custom data types ({} in file):",
138 types.num_types()?
139 );
140 print_enumerators(&coupling)?;
141
142 // Evolve it: add an enumerator and raise the version.
143 println!(
144 "\nCoupling version before update: {}",
145 coupling.type_version()?
146 );
147 coupling.update_enumerators(&enumerator_array(
148 &engine,
149 &[("AC", 0.0), ("DC", 1.0), ("GND", 2.0)],
150 true,
151 )?)?;
152
153 // Raising the lowest field signals a change the engine applies silently;
154 // raising a higher one marks it as deliberate.
155 let version = coupling.type_version()?;
156 let mut fields = version.split('.');
157 let major: u32 = fields.next().unwrap_or("0").parse().unwrap_or(0);
158 let minor: u32 = fields.next().unwrap_or("0").parse().unwrap_or(0);
159 coupling.set_type_version(&format!("{major}.{}.0.0", minor + 1))?;
160 println!(
161 "Coupling version after update: {}",
162 coupling.type_version()?
163 );
164
165 println!("\nCoupling now defines (InputCoupling reflects this):");
166 print_enumerators(&coupling)?;
167
168 // Saving does nothing unless the file believes it changed.
169 file.inc_change_count()?;
170 let path = std::env::temp_dir().join("rs_teststand_with_custom_types.seq");
171 sequence_file.save(&path.to_string_lossy())?;
172 println!("\nSaved to {}", path.display());
173
174 engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
175 Ok(())
176}Sourcepub fn delete_sub_property(
&self,
lookup_string: &str,
options: i32,
) -> Result<(), Error>
pub fn delete_sub_property( &self, lookup_string: &str, options: i32, ) -> Result<(), Error>
Deletes a sub-property by lookup path (PropertyObject.DeleteSubProperty).
§Errors
Error if the COM call fails.
Examples found in repository?
38fn temporary_variable_lifecycle(container: &PropertyObject) -> Result<(), rs_teststand::Error> {
39 let name = "TempScratch";
40 let clone_name = "TempScratchCopy";
41 for stale in [name, clone_name] {
42 if container.exists(stale, 0)? {
43 container.delete_sub_property(stale, 0)?;
44 }
45 }
46
47 container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
48 container.set_val_string(name, 0, "scratch")?;
49 println!(
50 " created {name} (String) = '{}'",
51 container.get_val_string(name, 0)?
52 );
53
54 container.delete_sub_property(name, 0)?;
55 container.new_sub_property(name, PropValType::Number, false, "", INSERT_IF_MISSING)?;
56 container.set_val_number(name, 0, 42.0)?;
57 println!(
58 " retyped {name} -> Number = {}",
59 container.get_val_number(name, 0)?
60 );
61
62 // clone copies value and type; set_property_object attaches it under a new name.
63 let copy = container.clone_property(name, 0)?;
64 container.set_property_object(clone_name, INSERT_IF_MISSING, ©)?;
65 println!(
66 " cloned {name} -> {clone_name} = {}",
67 container.get_val_number(clone_name, 0)?
68 );
69
70 container.delete_sub_property(clone_name, 0)?;
71 container.delete_sub_property(name, 0)?;
72 println!(
73 " removed both: {name} exists={}, {clone_name} exists={}",
74 container.exists(name, 0)?,
75 container.exists(clone_name, 0)?
76 );
77 Ok(())
78}Sourcepub fn clone_property(
&self,
lookup_string: &str,
options: i32,
) -> Result<Self, Error>
pub fn clone_property( &self, lookup_string: &str, options: i32, ) -> Result<Self, Error>
Clones a sub-property by lookup path (PropertyObject.Clone).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
72fn append_template(group: &PropertyObject, template: &PropertyObject) -> Result<(), Error> {
73 let index = group.get_num_elements()?;
74 group.set_num_elements(index + 1, PropertyOptions::NONE.bits())?;
75 group.set_property_object(
76 &format!("[{index}]"),
77 PropertyOptions::NONE.bits(),
78 &template.clone_property("", PropertyOptions::NONE.bits())?,
79 )
80}
81
82/// Finds a prototype in the group by name.
83fn find_template(group: &PropertyObject, name: &str) -> Result<Option<PropertyObject>, Error> {
84 for index in 0..group.get_num_elements()? {
85 let candidate = group.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
86 if candidate.name()? == name {
87 return Ok(Some(candidate));
88 }
89 }
90 Ok(None)
91}
92
93/// Turns "not found" into an error naming the template that is missing.
94fn require(found: Option<PropertyObject>, name: &'static str) -> Result<PropertyObject, Error> {
95 found.ok_or(Error::UnexpectedType {
96 expected: name,
97 actual: "no template of that name in the group",
98 })
99}
100
101/// A configured Statement step, taken as a property tree so it can be cloned.
102fn step_template(engine: &Engine) -> Result<PropertyObject, Error> {
103 let step = engine.new_step(NO_ADAPTER, "Statement")?;
104 step.set_name(STEP_TEMPLATE)?;
105 step.set_post_expression(r#"Locals.Result = "Hello from step template!""#)?;
106 step.as_property_object()
107}
108
109/// A whole sequence, one step included, as a single prototype.
110fn sequence_template(engine: &Engine) -> Result<PropertyObject, Error> {
111 let sequence = engine.new_sequence()?;
112 sequence.set_name(SEQUENCE_TEMPLATE)?;
113
114 let step = engine.new_step(NO_ADAPTER, "Statement")?;
115 step.set_name("Inside_Sequence_Template")?;
116 sequence.insert_step(&step, 0, StepGroup::Main)?;
117
118 sequence.as_property_object()
119}
120
121/// A string variable carrying its default value.
122fn variable_template(engine: &Engine) -> Result<PropertyObject, Error> {
123 let variable =
124 engine.new_property_object(PropValType::String, false, "", PropertyOptions::NONE.bits())?;
125 variable.set_name(VARIABLE_TEMPLATE)?;
126 variable.set_val_string("", PropertyOptions::NONE.bits(), "Template Variable Value")?;
127 Ok(variable)
128}
129
130/// Stamps one copy of each template into a file that is already on disk.
131fn apply_templates(
132 sequence_file: &SequenceFile,
133 step: &PropertyObject,
134 sequence: &PropertyObject,
135 variable: &PropertyObject,
136) -> Result<(), Error> {
137 let main_sequence = sequence_file.get_sequence_by_name(MAIN_SEQUENCE)?;
138
139 // Inserting returns the copy on the Step interface, so it can be renamed
140 // and re-identified straight away. Without a new step ID the copy would go
141 // on claiming the prototype's identity.
142 let inserted_step = main_sequence.insert_step_from_template(step, 0, StepGroup::Main)?;
143 inserted_step.set_name("Step From Template")?;
144 inserted_step.create_new_unique_step_id()?;
145 println!(" step: {}", inserted_step.name()?);
146
147 let inserted_sequence = sequence_file.insert_sequence_from_template(sequence)?;
148 inserted_sequence.create_new_unique_step_ids()?;
149 println!(
150 " sequence: {} ({} step(s))",
151 inserted_sequence.name()?,
152 inserted_sequence.get_num_steps(StepGroup::Main)?
153 );
154
155 // A variable is the easy case: Locals is a property tree, so a clone drops
156 // straight in under the template's own name.
157 let locals = main_sequence.locals()?;
158 locals.set_property_object(
159 &variable.name()?,
160 PropertyOptions::INSERT_IF_MISSING.bits(),
161 &variable.clone_property("", PropertyOptions::NONE.bits())?,
162 )?;
163 println!(
164 " variable: {} = {:?}",
165 variable.name()?,
166 locals.get_val_string(&variable.name()?, PropertyOptions::NONE.bits())?
167 );
168
169 // A file that does not believe it changed will not write anything.
170 sequence_file
171 .as_property_object_file()?
172 .inc_change_count()?;
173 Ok(())
174}More examples
38fn temporary_variable_lifecycle(container: &PropertyObject) -> Result<(), rs_teststand::Error> {
39 let name = "TempScratch";
40 let clone_name = "TempScratchCopy";
41 for stale in [name, clone_name] {
42 if container.exists(stale, 0)? {
43 container.delete_sub_property(stale, 0)?;
44 }
45 }
46
47 container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
48 container.set_val_string(name, 0, "scratch")?;
49 println!(
50 " created {name} (String) = '{}'",
51 container.get_val_string(name, 0)?
52 );
53
54 container.delete_sub_property(name, 0)?;
55 container.new_sub_property(name, PropValType::Number, false, "", INSERT_IF_MISSING)?;
56 container.set_val_number(name, 0, 42.0)?;
57 println!(
58 " retyped {name} -> Number = {}",
59 container.get_val_number(name, 0)?
60 );
61
62 // clone copies value and type; set_property_object attaches it under a new name.
63 let copy = container.clone_property(name, 0)?;
64 container.set_property_object(clone_name, INSERT_IF_MISSING, ©)?;
65 println!(
66 " cloned {name} -> {clone_name} = {}",
67 container.get_val_number(clone_name, 0)?
68 );
69
70 container.delete_sub_property(clone_name, 0)?;
71 container.delete_sub_property(name, 0)?;
72 println!(
73 " removed both: {name} exists={}, {clone_name} exists={}",
74 container.exists(name, 0)?,
75 container.exists(clone_name, 0)?
76 );
77 Ok(())
78}