pub struct Engine { /* private fields */ }Expand description
The TestStand™ Engine.
Constructing an Engine creates the underlying COM object; dropping it
releases it. It is the entry point of the API:
use rs_teststand::Engine;
let engine = Engine::new()?;
println!("TestStand {}", engine.version_string()?);Implementations§
Source§impl Engine
impl Engine
Sourcepub fn new() -> Result<Self, Error>
pub fn new() -> Result<Self, Error>
Creates the engine (STA COM apartment plus the TestStand.Engine object).
§Errors
Error::Com if COM cannot be initialized or the engine class
cannot be created (e.g. no TestStand™ installation is registered).
Examples found in repository?
19fn main() -> Result<(), Error> {
20 let engine = Engine::new()?;
21
22 println!("version : {}", engine.version_string()?);
23 println!(
24 "numeric : {}.{}.{} build {}",
25 engine.major_version()?,
26 engine.minor_version()?,
27 engine.revision_version()?,
28 engine.build_version()?
29 );
30 println!("bitness : {}", if engine.is_64bit()? { 64 } else { 32 });
31 println!("install : {}", engine.teststand_directory()?);
32
33 Ok(())
34}More examples
5fn main() -> Result<(), rs_teststand::Error> {
6 let engine = Engine::new()?;
7
8 println!("Creating a new workspace file...");
9 let ws_file = engine.new_workspace_file()?;
10 let root_obj = ws_file.root_workspace_object()?;
11
12 println!("Root workspace object:");
13 println!(" Display Name: '{}'", root_obj.display_name()?);
14 println!(" Object Type: {}", root_obj.object_type()?);
15 println!(" Child Objects: {}", root_obj.num_contained_objects()?);
16
17 println!("\nCreating project and folder structure...");
18 let project = root_obj.new_folder("Project Alpha")?;
19 println!(" Created project folder: '{}'", project.display_name()?);
20
21 let seq_entry = project.new_file("MainTest.seq")?;
22 println!(
23 " Created sequence file entry: '{}'",
24 seq_entry.display_name()?
25 );
26
27 println!(
28 "\nUpdated root child objects count: {}",
29 root_obj.num_contained_objects()?
30 );
31
32 Ok(())
33}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}5fn main() -> Result<(), rs_teststand::Error> {
6 let engine = Engine::new()?;
7 let station_options = engine.station_options()?;
8
9 station_options.set_tracing_enabled(true)?;
10 station_options.set_disable_results(false)?;
11 station_options.set_breakpoints_enabled(true)?;
12 station_options.set_check_out_files_when_edited(false)?;
13 station_options.set_language("English")?;
14 station_options.set_always_goto_cleanup_on_failure(true)?;
15 station_options.set_show_hidden_properties(true)?;
16 station_options.set_prompt_to_find_files(false)?;
17 station_options.set_auto_login_system_user(true)?;
18 station_options.set_ui_message_delay(100)?;
19 station_options.set_ui_message_min_delay(10)?;
20 station_options.set_station_id("STATION_RUST_01")?;
21 station_options.set_use_station_model(true)?;
22 station_options.set_allow_other_models(false)?;
23 station_options.set_use_localized_decimal_point(false)?;
24 station_options.set_time_limit(0, 0, 60.0)?;
25 station_options.set_time_limit_enabled(0, 0, true)?;
26
27 engine.commit_globals_to_disk(false)?;
28 println!("Station options updated and committed to disk.");
29
30 Ok(())
31}146fn main() -> Result<(), Box<dyn std::error::Error>> {
147 let engine = Engine::new()?;
148 // Nothing reaches the queue until this is on, and without the queue there
149 // is no way to know the run ended.
150 engine.set_ui_message_polling_enabled(true)?;
151
152 let sequence_file = engine.new_sequence_file()?;
153 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
154 for (name, data_source, low, high) in TESTS {
155 add_numeric_limit_test(&engine, &main_sequence, name, data_source, low, high)?;
156 }
157
158 let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
159 println!("Running {} headless...", execution.display_name()?);
160
161 if wait_for_end(&engine, RUN_DEADLINE)? {
162 println!("Finished with status: {}", execution.result_status()?);
163 report(&execution.result_object()?)?;
164 } else {
165 // Reported rather than ignored: a host that assumes success here would
166 // publish results from a run that never finished.
167 println!("The run did not finish within {RUN_DEADLINE:?}; terminating.");
168 execution.terminate()?;
169 }
170
171 engine.release_sequence_file_ex(sequence_file, none())?;
172 Ok(())
173}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}Sourcepub fn startup_dialogs(&self) -> &[DialogInfo]
pub fn startup_dialogs(&self) -> &[DialogInfo]
Dialogs that were closed while this engine was being created.
A non-empty list is worth logging: it is the only record that something asked a question and was answered by closing the window.
Empty means nothing owned by this process was found, which is not the same as no dialog having appeared. Detection cannot see another process’s windows, and whether the engine’s unreleased-files warning is raised in-process has not been established. Do not treat an empty list as proof that startup was clean.
Sourcepub fn load_type_palette_files_ex(
&self,
handler: ConflictHandler,
options: i32,
) -> Result<(), Error>
pub fn load_type_palette_files_ex( &self, handler: ConflictHandler, options: i32, ) -> Result<(), Error>
Sourcepub fn load_type_palette_files(&self) -> Result<(), Error>
pub fn load_type_palette_files(&self) -> Result<(), Error>
Sourcepub fn unload_type_palette_files(&self) -> Result<(), Error>
pub fn unload_type_palette_files(&self) -> Result<(), Error>
Sourcepub fn major_version(&self) -> Result<i32, Error>
pub fn major_version(&self) -> Result<i32, Error>
The engine’s major version number (Engine.MajorVersion): the two-digit
major, so TestStand™ 2026 reports 26 and 2016 reports 16.
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
19fn main() -> Result<(), Error> {
20 let engine = Engine::new()?;
21
22 println!("version : {}", engine.version_string()?);
23 println!(
24 "numeric : {}.{}.{} build {}",
25 engine.major_version()?,
26 engine.minor_version()?,
27 engine.revision_version()?,
28 engine.build_version()?
29 );
30 println!("bitness : {}", if engine.is_64bit()? { 64 } else { 32 });
31 println!("install : {}", engine.teststand_directory()?);
32
33 Ok(())
34}Sourcepub fn minor_version(&self) -> Result<i32, Error>
pub fn minor_version(&self) -> Result<i32, Error>
The engine’s minor version number (Engine.MinorVersion).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
19fn main() -> Result<(), Error> {
20 let engine = Engine::new()?;
21
22 println!("version : {}", engine.version_string()?);
23 println!(
24 "numeric : {}.{}.{} build {}",
25 engine.major_version()?,
26 engine.minor_version()?,
27 engine.revision_version()?,
28 engine.build_version()?
29 );
30 println!("bitness : {}", if engine.is_64bit()? { 64 } else { 32 });
31 println!("install : {}", engine.teststand_directory()?);
32
33 Ok(())
34}Sourcepub fn revision_version(&self) -> Result<i32, Error>
pub fn revision_version(&self) -> Result<i32, Error>
The engine’s revision version number (Engine.RevisionVersion).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
19fn main() -> Result<(), Error> {
20 let engine = Engine::new()?;
21
22 println!("version : {}", engine.version_string()?);
23 println!(
24 "numeric : {}.{}.{} build {}",
25 engine.major_version()?,
26 engine.minor_version()?,
27 engine.revision_version()?,
28 engine.build_version()?
29 );
30 println!("bitness : {}", if engine.is_64bit()? { 64 } else { 32 });
31 println!("install : {}", engine.teststand_directory()?);
32
33 Ok(())
34}Sourcepub fn build_version(&self) -> Result<i32, Error>
pub fn build_version(&self) -> Result<i32, Error>
The engine’s build version number (Engine.BuildVersion).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
19fn main() -> Result<(), Error> {
20 let engine = Engine::new()?;
21
22 println!("version : {}", engine.version_string()?);
23 println!(
24 "numeric : {}.{}.{} build {}",
25 engine.major_version()?,
26 engine.minor_version()?,
27 engine.revision_version()?,
28 engine.build_version()?
29 );
30 println!("bitness : {}", if engine.is_64bit()? { 64 } else { 32 });
31 println!("install : {}", engine.teststand_directory()?);
32
33 Ok(())
34}Sourcepub fn version_string(&self) -> Result<String, Error>
pub fn version_string(&self) -> Result<String, Error>
The engine’s full version string (Engine.VersionString).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
19fn main() -> Result<(), Error> {
20 let engine = Engine::new()?;
21
22 println!("version : {}", engine.version_string()?);
23 println!(
24 "numeric : {}.{}.{} build {}",
25 engine.major_version()?,
26 engine.minor_version()?,
27 engine.revision_version()?,
28 engine.build_version()?
29 );
30 println!("bitness : {}", if engine.is_64bit()? { 64 } else { 32 });
31 println!("install : {}", engine.teststand_directory()?);
32
33 Ok(())
34}Sourcepub fn is_64bit(&self) -> Result<bool, Error>
pub fn is_64bit(&self) -> Result<bool, Error>
Returns true if the TestStand™ engine is running as a 64-bit process (Engine.Is64Bit).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
19fn main() -> Result<(), Error> {
20 let engine = Engine::new()?;
21
22 println!("version : {}", engine.version_string()?);
23 println!(
24 "numeric : {}.{}.{} build {}",
25 engine.major_version()?,
26 engine.minor_version()?,
27 engine.revision_version()?,
28 engine.build_version()?
29 );
30 println!("bitness : {}", if engine.is_64bit()? { 64 } else { 32 });
31 println!("install : {}", engine.teststand_directory()?);
32
33 Ok(())
34}Sourcepub fn teststand_directory(&self) -> Result<String, Error>
pub fn teststand_directory(&self) -> Result<String, Error>
The path to the TestStand™ root directory (Engine.TestStandDirectory).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
19fn main() -> Result<(), Error> {
20 let engine = Engine::new()?;
21
22 println!("version : {}", engine.version_string()?);
23 println!(
24 "numeric : {}.{}.{} build {}",
25 engine.major_version()?,
26 engine.minor_version()?,
27 engine.revision_version()?,
28 engine.build_version()?
29 );
30 println!("bitness : {}", if engine.is_64bit()? { 64 } else { 32 });
31 println!("install : {}", engine.teststand_directory()?);
32
33 Ok(())
34}Sourcepub fn bin_directory(&self) -> Result<String, Error>
pub fn bin_directory(&self) -> Result<String, Error>
The path to the TestStand™ Bin directory (Engine.BinDirectory).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
49fn main() -> Result<(), rs_teststand::Error> {
50 let engine = Engine::new()?;
51 let search_directories = engine.search_directories()?;
52
53 println!("Total search directories: {}", search_directories.count()?);
54 for (index, directory) in search_directories.iter()?.enumerate() {
55 print_entry(index, &directory?)?;
56 }
57
58 // Insert at the front, so the new entry is searched first.
59 println!("\nInserting a new explicit search directory...");
60 let new_path = engine.bin_directory()?;
61 search_directories.insert(&new_path, 0, true, "", false, false)?;
62 println!("Total after insert: {}", search_directories.count()?);
63
64 let inserted = search_directories.get(0)?;
65 println!(
66 "New [0] Path: '{}', Subdirs: {}",
67 inserted.path()?,
68 inserted.search_subdirectories()?
69 );
70
71 // A disabled entry stays in the list but is not searched.
72 println!("Disabling the new directory...");
73 inserted.set_disabled(true)?;
74 println!("New [0] Disabled: {}", inserted.disabled()?);
75
76 // Order matters: entries are searched in list order.
77 println!("Moving the new directory to index 1...");
78 search_directories.move_search_directory(0, 1)?;
79 println!(
80 "Directory at index 1 is now: '{}'",
81 search_directories.get(1)?.path()?
82 );
83
84 println!("Removing the added directory to clean up...");
85 search_directories.remove(1)?;
86 println!("Total after cleanup: {}", search_directories.count()?);
87
88 // The engine writes search directories out at shutdown anyway. Committing
89 // now makes the change visible to other processes immediately, and passing
90 // `false` keeps a save conflict from raising a dialog.
91 engine.commit_globals_to_disk(false)?;
92 println!("Committed search directories configuration to disk.");
93
94 Ok(())
95}Sourcepub fn config_directory(&self) -> Result<String, Error>
pub fn config_directory(&self) -> Result<String, Error>
Sourcepub fn station_options(&self) -> Result<StationOptions, Error>
pub fn station_options(&self) -> Result<StationOptions, Error>
Accesses the station’s configuration settings (Engine.StationOptions).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
5fn main() -> Result<(), rs_teststand::Error> {
6 let engine = Engine::new()?;
7 let station_options = engine.station_options()?;
8
9 station_options.set_tracing_enabled(true)?;
10 station_options.set_disable_results(false)?;
11 station_options.set_breakpoints_enabled(true)?;
12 station_options.set_check_out_files_when_edited(false)?;
13 station_options.set_language("English")?;
14 station_options.set_always_goto_cleanup_on_failure(true)?;
15 station_options.set_show_hidden_properties(true)?;
16 station_options.set_prompt_to_find_files(false)?;
17 station_options.set_auto_login_system_user(true)?;
18 station_options.set_ui_message_delay(100)?;
19 station_options.set_ui_message_min_delay(10)?;
20 station_options.set_station_id("STATION_RUST_01")?;
21 station_options.set_use_station_model(true)?;
22 station_options.set_allow_other_models(false)?;
23 station_options.set_use_localized_decimal_point(false)?;
24 station_options.set_time_limit(0, 0, 60.0)?;
25 station_options.set_time_limit_enabled(0, 0, true)?;
26
27 engine.commit_globals_to_disk(false)?;
28 println!("Station options updated and committed to disk.");
29
30 Ok(())
31}Sourcepub fn new_sequence_file(&self) -> Result<SequenceFile, Error>
pub fn new_sequence_file(&self) -> Result<SequenceFile, Error>
Creates an empty sequence file (Engine.NewSequenceFile).
The file exists only in memory until it is saved.
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
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}More examples
146fn main() -> Result<(), Box<dyn std::error::Error>> {
147 let engine = Engine::new()?;
148 // Nothing reaches the queue until this is on, and without the queue there
149 // is no way to know the run ended.
150 engine.set_ui_message_polling_enabled(true)?;
151
152 let sequence_file = engine.new_sequence_file()?;
153 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
154 for (name, data_source, low, high) in TESTS {
155 add_numeric_limit_test(&engine, &main_sequence, name, data_source, low, high)?;
156 }
157
158 let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
159 println!("Running {} headless...", execution.display_name()?);
160
161 if wait_for_end(&engine, RUN_DEADLINE)? {
162 println!("Finished with status: {}", execution.result_status()?);
163 report(&execution.result_object()?)?;
164 } else {
165 // Reported rather than ignored: a host that assumes success here would
166 // publish results from a run that never finished.
167 println!("The run did not finish within {RUN_DEADLINE:?}; terminating.");
168 execution.terminate()?;
169 }
170
171 engine.release_sequence_file_ex(sequence_file, none())?;
172 Ok(())
173}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}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}68fn main() -> Result<(), Box<dyn std::error::Error>> {
69 let engine = Engine::new()?;
70 let sequence_file = engine.new_sequence_file()?;
71 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
72
73 // Steps are placed by group and index, so order is explicit.
74 main_sequence.insert_step(
75 &numeric_limit_test(
76 &engine,
77 "Temperature Check",
78 "Locals.TempSensorPresent == True",
79 15.0,
80 85.0,
81 )?,
82 0,
83 StepGroup::Main,
84 )?;
85 main_sequence.insert_step(
86 &numeric_limit_test(
87 &engine,
88 "Voltage Monitor",
89 "Locals.DUTPowered == True",
90 4.75,
91 5.25,
92 )?,
93 1,
94 StepGroup::Main,
95 )?;
96
97 // A subsequence, so a later example has something to call.
98 let subsequence = engine.new_sequence()?;
99 subsequence.set_name("CustomSubsequence")?;
100 sequence_file.insert_sequence(&subsequence)?;
101
102 let init_step = engine.new_step(NO_ADAPTER, "Action")?;
103 init_step.set_name("Initialize Hardware")?;
104 subsequence.insert_step(&init_step, 0, StepGroup::Main)?;
105
106 describe(&main_sequence)?;
107 println!();
108 describe(&subsequence)?;
109
110 // Cleanup runs even when Main fails, which is why it is worth showing.
111 println!(
112 "\nGroup sizes in MainSequence: setup={}, main={}, cleanup={}",
113 main_sequence.get_num_steps(StepGroup::Setup)?,
114 main_sequence.get_num_steps(StepGroup::Main)?,
115 main_sequence.get_num_steps(StepGroup::Cleanup)?
116 );
117
118 let path = std::env::temp_dir().join("rs_teststand_built_sequence.seq");
119 sequence_file.save(&path.to_string_lossy())?;
120 println!("\nSaved to {}", path.display());
121
122 engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
123 Ok(())
124}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 new_execution(
&self,
sequence_file: &SequenceFile,
sequence_name: &str,
process_model: Option<&SequenceFile>,
break_at_first_step: bool,
execution_type_mask: i32,
) -> Result<Execution, Error>
pub fn new_execution( &self, sequence_file: &SequenceFile, sequence_name: &str, process_model: Option<&SequenceFile>, break_at_first_step: bool, execution_type_mask: i32, ) -> Result<Execution, Error>
Starts a sequence running (Engine.NewExecution).
The execution begins immediately.
Pass None for process_model to run the sequence directly; supply one
to run a process-model entry point instead. execution_type_mask is
normally 0.
§Errors
Error if the sequence cannot be started or the COM call fails.
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}More examples
146fn main() -> Result<(), Box<dyn std::error::Error>> {
147 let engine = Engine::new()?;
148 // Nothing reaches the queue until this is on, and without the queue there
149 // is no way to know the run ended.
150 engine.set_ui_message_polling_enabled(true)?;
151
152 let sequence_file = engine.new_sequence_file()?;
153 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
154 for (name, data_source, low, high) in TESTS {
155 add_numeric_limit_test(&engine, &main_sequence, name, data_source, low, high)?;
156 }
157
158 let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
159 println!("Running {} headless...", execution.display_name()?);
160
161 if wait_for_end(&engine, RUN_DEADLINE)? {
162 println!("Finished with status: {}", execution.result_status()?);
163 report(&execution.result_object()?)?;
164 } else {
165 // Reported rather than ignored: a host that assumes success here would
166 // publish results from a run that never finished.
167 println!("The run did not finish within {RUN_DEADLINE:?}; terminating.");
168 execution.terminate()?;
169 }
170
171 engine.release_sequence_file_ex(sequence_file, none())?;
172 Ok(())
173}109fn main() -> Result<(), Box<dyn std::error::Error>> {
110 let engine = Engine::new()?;
111 engine.set_ui_message_polling_enabled(true)?;
112
113 let sequence_file = engine.new_sequence_file()?;
114 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
115
116 add_pass_fail(&engine, &main_sequence, "Verify Power Rail", "True")?;
117 add_unrecorded_filler(&engine, &main_sequence, "Filler (not recorded)")?;
118 add_numeric(
119 &engine,
120 &main_sequence,
121 "Verify Current Draw",
122 "1.5",
123 1.0,
124 2.0,
125 )?;
126 add_pass_fail(&engine, &main_sequence, "Verify Ground Bond", "True")?;
127
128 let step_count = main_sequence.get_num_steps(StepGroup::Main)?;
129 println!("Running {step_count} steps...");
130 let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
131
132 if !wait_for_end(&engine, RUN_DEADLINE)? {
133 println!("The run did not finish within {RUN_DEADLINE:?}; terminating.");
134 execution.terminate()?;
135 return Ok(());
136 }
137 println!("Finished with status: {}\n", execution.result_status()?);
138
139 let results = execution.result_list()?;
140 let parsed = results.parse()?;
141
142 println!("{} of {step_count} steps recorded a result:", parsed.len());
143 for (index, result) in parsed.iter().enumerate() {
144 let measurement = match &result.value {
145 Some(ResultValue::Number(number)) => format!("measured {number}"),
146 Some(ResultValue::Text(text)) => format!("measured {text:?}"),
147 None => "no measurement".to_owned(),
148 };
149 println!(
150 " [{index}] {} ({}) -> {}, {measurement}",
151 result.name, result.step_type, result.status
152 );
153 }
154
155 // The step whose recording was switched off is absent, by design.
156 println!(
157 "\nThe unrecorded step left no entry, which is why {} < {step_count}.",
158 parsed.len()
159 );
160
161 // The parsed results are plain data, so they outlive the run and can be
162 // handed to anything: a report writer, a database, a serde format.
163 let failures: Vec<&str> = parsed
164 .iter()
165 .filter(|result| result.status == "Failed")
166 .map(|result| result.name.as_str())
167 .collect();
168 if failures.is_empty() {
169 println!("No failures.");
170 } else {
171 println!("Failed steps: {}", failures.join(", "));
172 }
173
174 engine.release_sequence_file_ex(sequence_file, none())?;
175 Ok(())
176}40fn main() -> Result<(), Box<dyn std::error::Error>> {
41 let engine = Engine::new()?;
42
43 // Nothing reaches the queue until polling is switched on.
44 engine.set_ui_message_polling_enabled(true)?;
45
46 let sequence_file = engine.new_sequence_file()?;
47 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
48
49 // Posted through the engine and tagged with the execution. Synchronous, so
50 // the sequence waits until the host acknowledges it.
51 add_statement(
52 &engine,
53 &main_sequence,
54 "Report Stage",
55 &format!(
56 "RunState.Engine.PostUIMessage(RunState.Execution, RunState.Thread, {STAGE_MESSAGE}, \
57 1, \"stage: configuring instruments\", Nothing, True)"
58 ),
59 )?;
60 // Posted through the thread. Asynchronous, so the sequence carries on.
61 add_statement(
62 &engine,
63 &main_sequence,
64 "Report Progress",
65 &format!(
66 "RunState.Thread.PostUIMessageEx({PROGRESS_MESSAGE}, 50, \"progress: halfway\", \
67 Nothing, False)"
68 ),
69 )?;
70
71 let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
72 println!(
73 "Execution {} started; polling for messages...",
74 execution.id()?
75 );
76
77 // The end is detected from the message stream, not by waiting on the
78 // execution: a wait call does not pump the queue, so a synchronous message
79 // would sit unacknowledged and both sides would stop.
80 let started = Instant::now();
81 let mut ended = false;
82 while started.elapsed() < Duration::from_secs(60) && !ended {
83 while !engine.is_ui_message_queue_empty()? {
84 let message = engine.get_ui_message()?;
85 let code = message.event()?;
86 if matches!(
87 UIMessageCode::from_bits(code),
88 Ok(UIMessageCode::EndExecution)
89 ) {
90 ended = true;
91 }
92 let origin = if UIMessageCode::is_user_message(code) {
93 "sequence".to_owned()
94 } else {
95 format!("engine {:?}", UIMessageCode::from_bits(code))
96 };
97 println!(
98 " [{origin}] code={code} numeric={} string={:?} synchronous={}",
99 message.numeric_data()?,
100 message.string_data()?,
101 message.is_synchronous()?
102 );
103 // Required: releases a synchronous poster and asks for the next.
104 message.acknowledge()?;
105 }
106 }
107
108 println!("\nExecution ended: {ended}");
109 engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
110 Ok(())
111}Sourcepub fn post_ui_message(
&self,
execution: &Execution,
thread: &Thread,
event_code: i32,
numeric_data: f64,
string_data: &str,
activex_data: Option<&PropertyObject>,
synchronous: bool,
) -> Result<(), Error>
pub fn post_ui_message( &self, execution: &Execution, thread: &Thread, event_code: i32, numeric_data: f64, string_data: &str, activex_data: Option<&PropertyObject>, synchronous: bool, ) -> Result<(), Error>
Posts a message on behalf of an execution (Engine.PostUIMessage).
The counterpart to
Thread::post_ui_message_ex, for
code that is not itself running inside the sequence and therefore has no
current thread to post from. Because there is no implied context, the
execution and thread the message belongs to are given explicitly.
activex_data carries structured data, read back by the host from
UIMessage::activex_data. Pass None
to leave the slot empty.
Pass synchronous = true in the ordinary case; see
Thread::post_ui_message_ex for why
the blocking form is the safe default.
§Errors
Error if the COM call fails, or if a wrapper has no COM identity.
Sourcepub fn set_current_user(&self, user: Option<&User>) -> Result<(), Error>
pub fn set_current_user(&self, user: Option<&User>) -> Result<(), Error>
Logs a user in, or logs the current one out (Engine.CurrentUser).
Some(user) makes that user current; None clears it, which the engine
documents as logging out.
This does not check the password. Setting the property is the act of
logging in, not an authentication step: a host that cares must call
User::validate_password first and
refuse on false. Written this way because the engine draws the same
line, and hiding a check inside a setter would make it unclear which one
a caller had actually performed.
A host built on the ActiveX UI controls should use their own login
method instead, so the controls raise the event they expect; this is the
headless path.
§Errors
Error if the COM call fails, or if user has no COM identity.
Sourcepub fn terminate_all(&self) -> Result<(), Error>
pub fn terminate_all(&self) -> Result<(), Error>
Asks every execution to stop (Engine.TerminateAll).
Termination, not abort: cleanup groups still run, so hardware is left in
a safe state. Like Execution::terminate
it is a request, and returns before the runs have finished unwinding. A
caller that needs them stopped must then wait for
UIMessageCode::EndExecution.
§Errors
Error if the COM call fails.
Sourcepub fn abort_all(&self) -> Result<(), Error>
pub fn abort_all(&self) -> Result<(), Error>
Stops every execution without running cleanup (Engine.AbortAll).
The blunt counterpart to terminate_all. Cleanup
groups do not run, so anything a sequence would have switched off
stays on. Prefer terminating unless the point is to stop now.
§Errors
Error if the COM call fails.
Sourcepub fn license_type(&self) -> Result<LicenseType, Error>
pub fn license_type(&self) -> Result<LicenseType, Error>
The license the engine is currently using (Engine.LicenseType).
Using, not holding. A freshly created engine has acquired nothing
and reports LicenseType::NoLicense
even on a fully licensed station; the answer only becomes meaningful
after something acquires. Use
require_license to ask whether the station
can license this host.
Reads state, so it acquires nothing and raises no dialog.
§Errors
Error if the COM call fails, or Error::UnknownLicenseType if the
engine reports a type this build does not name.
Sourcepub fn require_license(&self) -> Result<HeldLicense<'_>, Error>
pub fn require_license(&self) -> Result<HeldLicense<'_>, Error>
Acquires a license, or fails if the station cannot grant one.
The check a headless host should make before anything else, and the object it should keep alive while it runs.
Acquiring is what makes a license real.
license_type reports the license the engine is
using, and a freshly created engine is using none, measured on a
station with a valid development system license, it reads NoLicense
until something acquires. So reading before acquiring answers the wrong
question, and this method acquires first.
The request is ApplicationLicense::Unspecified, which lets the engine
grant whatever it has. Naming a kind can be refused even when the
station is properly licensed: on a development system station,
ApplicationLicense::OperatorInterface is turned down while
unspecified succeeds. Ask for a specific kind through
acquire_license only when the host genuinely
requires that one.
The startup dialog is suppressed, so an unlicensed station returns an error rather than opening a window nobody will close.
Refusal is retried for a few seconds before it is believed. The licensing subsystem is not ready the instant the engine object exists: measured on a properly licensed station, acquiring immediately after construction is refused, while the same call half a second later succeeds. A host that trusted the first answer would report an unlicensed station to its operator and stop. So a refusal is retried until it stops changing, which costs an unlicensed station a few seconds once, at startup.
Success is the handle, not the type.
HeldLicense::kind reports what the engine
says it is using and can still read
NoLicense after an unspecified
request was granted, so treat it as information rather than as the
verdict.
§Errors
Error::NoLicense if no license can be acquired, or Error if the
COM call fails.
Sourcepub fn get_license_description(&self) -> Result<String, Error>
pub fn get_license_description(&self) -> Result<String, Error>
A description of the current license (Engine.GetLicenseDescription).
Free text meant for a person, so log it rather than branch on it; use
license_type for decisions.
§Errors
Error if the COM call fails or returns an unexpected type.
Sourcepub fn application_license(&self) -> Result<ApplicationLicense, Error>
pub fn application_license(&self) -> Result<ApplicationLicense, Error>
The license this application requested (Engine.ApplicationLicense).
§Errors
Error if the COM call fails, or Error::UnknownLicenseType if the
engine reports a value this build does not name.
Sourcepub fn acquire_license(
&self,
license: ApplicationLicense,
options: AcquireLicenseOptions,
) -> Result<i32, Error>
pub fn acquire_license( &self, license: ApplicationLicense, options: AcquireLicenseOptions, ) -> Result<i32, Error>
Acquires a license and returns its handle (Engine.AcquireLicense).
Release it with release_license; the license
is held until every handle for it is released.
Pass AcquireLicenseOptions::SUPPRESS_STARTUP_DIALOG on any station
without a person at it. Without it, an engine that cannot acquire the
license opens a window offering to evaluate, activate or buy, and waits.
A headless host stops there until something kills it. With it, the same
situation returns an error this method propagates.
Prefer
ApplicationLicense::Unspecified,
which lets the engine grant whatever it has. Naming a kind is a
constraint, not a preference, and a smaller request is not a safer one:
on a station licensed for a development system,
OperatorInterface is
refused while unspecified succeeds. Name a kind only when the host truly
requires it.
Most callers want require_license instead,
which acquires and hands back a guard that releases on drop.
§Errors
Error::NoLicense if the license was not granted, or Error if the
COM call fails.
A handle of zero is treated as refusal. The reference says this member returns an error when it cannot acquire the license; measured against an unlicensed station it succeeds and hands back zero instead. A caller that trusted the documented behavior would carry on unlicensed, so the zero is turned into the error the caller was promised.
Sourcepub fn unload_all_modules(&self) -> Result<(), Error>
pub fn unload_all_modules(&self) -> Result<(), Error>
Releases every code module the engine has loaded
(Engine.UnloadAllModules).
Loading a sequence file loads its modules, and they stay loaded until that file is closed. That is what makes the second run fast, and also what holds a DLL open against the build that wants to replace it. Unloading here frees them all at once, without closing anything.
Call it between runs, not during one: a module in use by a live execution is not a candidate, and the next run reloads whatever it needs.
State inside a module does not survive. Anything a module kept in a static or a global is gone once it is unloaded, and the reload starts from nothing. A station whose modules carry state between steps that way should keep that state in the engine instead, or not call this.
§Errors
Error if the COM call fails.
Sourcepub fn breakpoints_enabled(&self) -> Result<bool, Error>
pub fn breakpoints_enabled(&self) -> Result<bool, Error>
Whether breakpoints stop an execution (Engine.BreakpointsEnabled).
The master switch. With it off, breakpoints stay set but nothing stops on them, which is how a station runs unattended without anyone having to strip a sequence file of the breakpoints someone left in it.
Distinct from the station option of the same name, which is the setting written to disk. This is the engine’s live state.
§Errors
Error if the COM call fails or returns an unexpected type.
Sourcepub fn persist_breakpoints(&self) -> Result<bool, Error>
pub fn persist_breakpoints(&self) -> Result<bool, Error>
Whether breakpoints survive the file they are set in
(Engine.PersistBreakpoints).
On, the engine remembers them across a close and reopen. A host that sets breakpoints on behalf of a remote panel usually wants this off, so that a debugging session leaves nothing behind on the station.
§Errors
Error if the COM call fails or returns an unexpected type.
Sourcepub fn do_dot_net_garbage_collection(&self) -> Result<(), Error>
pub fn do_dot_net_garbage_collection(&self) -> Result<(), Error>
Runs a .NET garbage collection now
(Engine.DoDotNetGarbageCollection).
Only relevant to a station whose steps call .NET code. Collection is otherwise periodic, on the interval; this forces one, which is worth doing between runs on a long-lived host rather than during a measurement, since collection pauses the runtime.
§Errors
Error if the COM call fails.
Sourcepub fn dot_net_garbage_collection_interval(&self) -> Result<i32, Error>
pub fn dot_net_garbage_collection_interval(&self) -> Result<i32, Error>
How often the engine collects .NET garbage, in milliseconds
(Engine.DotNetGarbageCollectionInterval).
Zero or less means automatic collection is off. A host built on this
crate will normally read -1, and that is correct rather than broken:
the three-second default belongs to applications built on the UI
control, and a headless host does not create one. Nothing collects on a
timer unless this is set to a positive interval, so a long-lived host
that runs .NET steps should either set one or call
do_dot_net_garbage_collection
between runs.
§Errors
Error if the COM call fails or returns an unexpected type.
Sourcepub fn set_dot_net_garbage_collection_interval(
&self,
milliseconds: i32,
) -> Result<(), Error>
pub fn set_dot_net_garbage_collection_interval( &self, milliseconds: i32, ) -> Result<(), Error>
Sourcepub fn dot_net_clr_version(&self) -> Result<String, Error>
pub fn dot_net_clr_version(&self) -> Result<String, Error>
Sourcepub fn users_file(&self) -> Result<UsersFile, Error>
pub fn users_file(&self) -> Result<UsersFile, Error>
The station’s user list, as a file (Engine.UsersFile).
The users the engine loaded at startup, and the only route to writing
them back. new_user builds a user in memory; without
saving through this file the station is unchanged once the process
exits.
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
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 ui_message_polling_enabled(&self) -> Result<bool, Error>
pub fn ui_message_polling_enabled(&self) -> Result<bool, Error>
Sourcepub fn set_ui_message_polling_enabled(&self, enabled: bool) -> Result<(), Error>
pub fn set_ui_message_polling_enabled(&self, enabled: bool) -> Result<(), Error>
Turns message polling on or off (Engine.UIMessagePollingEnabled).
Off by default. A headless host must turn it on before anything appears in the queue, without it the queue stays empty however much a sequence posts.
§Errors
Error if the COM call fails.
Examples found in repository?
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
146fn main() -> Result<(), Box<dyn std::error::Error>> {
147 let engine = Engine::new()?;
148 // Nothing reaches the queue until this is on, and without the queue there
149 // is no way to know the run ended.
150 engine.set_ui_message_polling_enabled(true)?;
151
152 let sequence_file = engine.new_sequence_file()?;
153 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
154 for (name, data_source, low, high) in TESTS {
155 add_numeric_limit_test(&engine, &main_sequence, name, data_source, low, high)?;
156 }
157
158 let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
159 println!("Running {} headless...", execution.display_name()?);
160
161 if wait_for_end(&engine, RUN_DEADLINE)? {
162 println!("Finished with status: {}", execution.result_status()?);
163 report(&execution.result_object()?)?;
164 } else {
165 // Reported rather than ignored: a host that assumes success here would
166 // publish results from a run that never finished.
167 println!("The run did not finish within {RUN_DEADLINE:?}; terminating.");
168 execution.terminate()?;
169 }
170
171 engine.release_sequence_file_ex(sequence_file, none())?;
172 Ok(())
173}109fn main() -> Result<(), Box<dyn std::error::Error>> {
110 let engine = Engine::new()?;
111 engine.set_ui_message_polling_enabled(true)?;
112
113 let sequence_file = engine.new_sequence_file()?;
114 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
115
116 add_pass_fail(&engine, &main_sequence, "Verify Power Rail", "True")?;
117 add_unrecorded_filler(&engine, &main_sequence, "Filler (not recorded)")?;
118 add_numeric(
119 &engine,
120 &main_sequence,
121 "Verify Current Draw",
122 "1.5",
123 1.0,
124 2.0,
125 )?;
126 add_pass_fail(&engine, &main_sequence, "Verify Ground Bond", "True")?;
127
128 let step_count = main_sequence.get_num_steps(StepGroup::Main)?;
129 println!("Running {step_count} steps...");
130 let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
131
132 if !wait_for_end(&engine, RUN_DEADLINE)? {
133 println!("The run did not finish within {RUN_DEADLINE:?}; terminating.");
134 execution.terminate()?;
135 return Ok(());
136 }
137 println!("Finished with status: {}\n", execution.result_status()?);
138
139 let results = execution.result_list()?;
140 let parsed = results.parse()?;
141
142 println!("{} of {step_count} steps recorded a result:", parsed.len());
143 for (index, result) in parsed.iter().enumerate() {
144 let measurement = match &result.value {
145 Some(ResultValue::Number(number)) => format!("measured {number}"),
146 Some(ResultValue::Text(text)) => format!("measured {text:?}"),
147 None => "no measurement".to_owned(),
148 };
149 println!(
150 " [{index}] {} ({}) -> {}, {measurement}",
151 result.name, result.step_type, result.status
152 );
153 }
154
155 // The step whose recording was switched off is absent, by design.
156 println!(
157 "\nThe unrecorded step left no entry, which is why {} < {step_count}.",
158 parsed.len()
159 );
160
161 // The parsed results are plain data, so they outlive the run and can be
162 // handed to anything: a report writer, a database, a serde format.
163 let failures: Vec<&str> = parsed
164 .iter()
165 .filter(|result| result.status == "Failed")
166 .map(|result| result.name.as_str())
167 .collect();
168 if failures.is_empty() {
169 println!("No failures.");
170 } else {
171 println!("Failed steps: {}", failures.join(", "));
172 }
173
174 engine.release_sequence_file_ex(sequence_file, none())?;
175 Ok(())
176}40fn main() -> Result<(), Box<dyn std::error::Error>> {
41 let engine = Engine::new()?;
42
43 // Nothing reaches the queue until polling is switched on.
44 engine.set_ui_message_polling_enabled(true)?;
45
46 let sequence_file = engine.new_sequence_file()?;
47 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
48
49 // Posted through the engine and tagged with the execution. Synchronous, so
50 // the sequence waits until the host acknowledges it.
51 add_statement(
52 &engine,
53 &main_sequence,
54 "Report Stage",
55 &format!(
56 "RunState.Engine.PostUIMessage(RunState.Execution, RunState.Thread, {STAGE_MESSAGE}, \
57 1, \"stage: configuring instruments\", Nothing, True)"
58 ),
59 )?;
60 // Posted through the thread. Asynchronous, so the sequence carries on.
61 add_statement(
62 &engine,
63 &main_sequence,
64 "Report Progress",
65 &format!(
66 "RunState.Thread.PostUIMessageEx({PROGRESS_MESSAGE}, 50, \"progress: halfway\", \
67 Nothing, False)"
68 ),
69 )?;
70
71 let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
72 println!(
73 "Execution {} started; polling for messages...",
74 execution.id()?
75 );
76
77 // The end is detected from the message stream, not by waiting on the
78 // execution: a wait call does not pump the queue, so a synchronous message
79 // would sit unacknowledged and both sides would stop.
80 let started = Instant::now();
81 let mut ended = false;
82 while started.elapsed() < Duration::from_secs(60) && !ended {
83 while !engine.is_ui_message_queue_empty()? {
84 let message = engine.get_ui_message()?;
85 let code = message.event()?;
86 if matches!(
87 UIMessageCode::from_bits(code),
88 Ok(UIMessageCode::EndExecution)
89 ) {
90 ended = true;
91 }
92 let origin = if UIMessageCode::is_user_message(code) {
93 "sequence".to_owned()
94 } else {
95 format!("engine {:?}", UIMessageCode::from_bits(code))
96 };
97 println!(
98 " [{origin}] code={code} numeric={} string={:?} synchronous={}",
99 message.numeric_data()?,
100 message.string_data()?,
101 message.is_synchronous()?
102 );
103 // Required: releases a synchronous poster and asks for the next.
104 message.acknowledge()?;
105 }
106 }
107
108 println!("\nExecution ended: {ended}");
109 engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
110 Ok(())
111}Sourcepub fn is_ui_message_queue_empty(&self) -> Result<bool, Error>
pub fn is_ui_message_queue_empty(&self) -> Result<bool, Error>
Whether the message queue is empty (Engine.IsUIMessageQueueEmpty).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
86fn wait_for_end(engine: &Engine, deadline: Duration) -> Result<bool, Error> {
87 let started = Instant::now();
88 while started.elapsed() < deadline {
89 if pump_thread_messages() {
90 return Ok(false);
91 }
92 while !engine.is_ui_message_queue_empty()? {
93 let message = engine.get_ui_message()?;
94 let ended = matches!(
95 UIMessageCode::from_bits(message.event()?),
96 Ok(UIMessageCode::EndExecution)
97 );
98 message.acknowledge()?;
99 if ended {
100 return Ok(true);
101 }
102 }
103 }
104 Ok(false)
105}More examples
88fn wait_for_end(engine: &Engine, deadline: Duration) -> Result<bool, Error> {
89 let started = Instant::now();
90 while started.elapsed() < deadline {
91 if pump_thread_messages() {
92 return Ok(false);
93 }
94 while !engine.is_ui_message_queue_empty()? {
95 let message = engine.get_ui_message()?;
96 let ended = matches!(
97 UIMessageCode::from_bits(message.event()?),
98 Ok(UIMessageCode::EndExecution)
99 );
100 message.acknowledge()?;
101 if ended {
102 return Ok(true);
103 }
104 }
105 }
106 Ok(false)
107}90fn wait_for_end(engine: &Engine, deadline: Duration) -> Result<bool, Error> {
91 let started = Instant::now();
92 while started.elapsed() < deadline {
93 if pump_thread_messages() {
94 return Ok(false);
95 }
96 while !engine.is_ui_message_queue_empty()? {
97 let message = engine.get_ui_message()?;
98 let ended = matches!(
99 UIMessageCode::from_bits(message.event()?),
100 Ok(UIMessageCode::EndExecution)
101 );
102 // Acknowledging is what releases a synchronous poster; skipping it
103 // stalls the sequence rather than merely losing a notification.
104 message.acknowledge()?;
105 if ended {
106 return Ok(true);
107 }
108 }
109 }
110 Ok(false)
111}40fn main() -> Result<(), Box<dyn std::error::Error>> {
41 let engine = Engine::new()?;
42
43 // Nothing reaches the queue until polling is switched on.
44 engine.set_ui_message_polling_enabled(true)?;
45
46 let sequence_file = engine.new_sequence_file()?;
47 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
48
49 // Posted through the engine and tagged with the execution. Synchronous, so
50 // the sequence waits until the host acknowledges it.
51 add_statement(
52 &engine,
53 &main_sequence,
54 "Report Stage",
55 &format!(
56 "RunState.Engine.PostUIMessage(RunState.Execution, RunState.Thread, {STAGE_MESSAGE}, \
57 1, \"stage: configuring instruments\", Nothing, True)"
58 ),
59 )?;
60 // Posted through the thread. Asynchronous, so the sequence carries on.
61 add_statement(
62 &engine,
63 &main_sequence,
64 "Report Progress",
65 &format!(
66 "RunState.Thread.PostUIMessageEx({PROGRESS_MESSAGE}, 50, \"progress: halfway\", \
67 Nothing, False)"
68 ),
69 )?;
70
71 let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
72 println!(
73 "Execution {} started; polling for messages...",
74 execution.id()?
75 );
76
77 // The end is detected from the message stream, not by waiting on the
78 // execution: a wait call does not pump the queue, so a synchronous message
79 // would sit unacknowledged and both sides would stop.
80 let started = Instant::now();
81 let mut ended = false;
82 while started.elapsed() < Duration::from_secs(60) && !ended {
83 while !engine.is_ui_message_queue_empty()? {
84 let message = engine.get_ui_message()?;
85 let code = message.event()?;
86 if matches!(
87 UIMessageCode::from_bits(code),
88 Ok(UIMessageCode::EndExecution)
89 ) {
90 ended = true;
91 }
92 let origin = if UIMessageCode::is_user_message(code) {
93 "sequence".to_owned()
94 } else {
95 format!("engine {:?}", UIMessageCode::from_bits(code))
96 };
97 println!(
98 " [{origin}] code={code} numeric={} string={:?} synchronous={}",
99 message.numeric_data()?,
100 message.string_data()?,
101 message.is_synchronous()?
102 );
103 // Required: releases a synchronous poster and asks for the next.
104 message.acknowledge()?;
105 }
106 }
107
108 println!("\nExecution ended: {ended}");
109 engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
110 Ok(())
111}Sourcepub fn get_ui_message(&self) -> Result<UIMessage, Error>
pub fn get_ui_message(&self) -> Result<UIMessage, Error>
Takes the next message from the queue (Engine.GetUIMessage).
Check is_ui_message_queue_empty
first. The message must be acknowledged once handled, see
UIMessage::acknowledge.
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
86fn wait_for_end(engine: &Engine, deadline: Duration) -> Result<bool, Error> {
87 let started = Instant::now();
88 while started.elapsed() < deadline {
89 if pump_thread_messages() {
90 return Ok(false);
91 }
92 while !engine.is_ui_message_queue_empty()? {
93 let message = engine.get_ui_message()?;
94 let ended = matches!(
95 UIMessageCode::from_bits(message.event()?),
96 Ok(UIMessageCode::EndExecution)
97 );
98 message.acknowledge()?;
99 if ended {
100 return Ok(true);
101 }
102 }
103 }
104 Ok(false)
105}More examples
88fn wait_for_end(engine: &Engine, deadline: Duration) -> Result<bool, Error> {
89 let started = Instant::now();
90 while started.elapsed() < deadline {
91 if pump_thread_messages() {
92 return Ok(false);
93 }
94 while !engine.is_ui_message_queue_empty()? {
95 let message = engine.get_ui_message()?;
96 let ended = matches!(
97 UIMessageCode::from_bits(message.event()?),
98 Ok(UIMessageCode::EndExecution)
99 );
100 message.acknowledge()?;
101 if ended {
102 return Ok(true);
103 }
104 }
105 }
106 Ok(false)
107}90fn wait_for_end(engine: &Engine, deadline: Duration) -> Result<bool, Error> {
91 let started = Instant::now();
92 while started.elapsed() < deadline {
93 if pump_thread_messages() {
94 return Ok(false);
95 }
96 while !engine.is_ui_message_queue_empty()? {
97 let message = engine.get_ui_message()?;
98 let ended = matches!(
99 UIMessageCode::from_bits(message.event()?),
100 Ok(UIMessageCode::EndExecution)
101 );
102 // Acknowledging is what releases a synchronous poster; skipping it
103 // stalls the sequence rather than merely losing a notification.
104 message.acknowledge()?;
105 if ended {
106 return Ok(true);
107 }
108 }
109 }
110 Ok(false)
111}40fn main() -> Result<(), Box<dyn std::error::Error>> {
41 let engine = Engine::new()?;
42
43 // Nothing reaches the queue until polling is switched on.
44 engine.set_ui_message_polling_enabled(true)?;
45
46 let sequence_file = engine.new_sequence_file()?;
47 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
48
49 // Posted through the engine and tagged with the execution. Synchronous, so
50 // the sequence waits until the host acknowledges it.
51 add_statement(
52 &engine,
53 &main_sequence,
54 "Report Stage",
55 &format!(
56 "RunState.Engine.PostUIMessage(RunState.Execution, RunState.Thread, {STAGE_MESSAGE}, \
57 1, \"stage: configuring instruments\", Nothing, True)"
58 ),
59 )?;
60 // Posted through the thread. Asynchronous, so the sequence carries on.
61 add_statement(
62 &engine,
63 &main_sequence,
64 "Report Progress",
65 &format!(
66 "RunState.Thread.PostUIMessageEx({PROGRESS_MESSAGE}, 50, \"progress: halfway\", \
67 Nothing, False)"
68 ),
69 )?;
70
71 let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
72 println!(
73 "Execution {} started; polling for messages...",
74 execution.id()?
75 );
76
77 // The end is detected from the message stream, not by waiting on the
78 // execution: a wait call does not pump the queue, so a synchronous message
79 // would sit unacknowledged and both sides would stop.
80 let started = Instant::now();
81 let mut ended = false;
82 while started.elapsed() < Duration::from_secs(60) && !ended {
83 while !engine.is_ui_message_queue_empty()? {
84 let message = engine.get_ui_message()?;
85 let code = message.event()?;
86 if matches!(
87 UIMessageCode::from_bits(code),
88 Ok(UIMessageCode::EndExecution)
89 ) {
90 ended = true;
91 }
92 let origin = if UIMessageCode::is_user_message(code) {
93 "sequence".to_owned()
94 } else {
95 format!("engine {:?}", UIMessageCode::from_bits(code))
96 };
97 println!(
98 " [{origin}] code={code} numeric={} string={:?} synchronous={}",
99 message.numeric_data()?,
100 message.string_data()?,
101 message.is_synchronous()?
102 );
103 // Required: releases a synchronous poster and asks for the next.
104 message.acknowledge()?;
105 }
106 }
107
108 println!("\nExecution ended: {ended}");
109 engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
110 Ok(())
111}Sourcepub fn new_step(
&self,
adapter_key_name: &str,
step_type_name: &str,
) -> Result<Step, Error>
pub fn new_step( &self, adapter_key_name: &str, step_type_name: &str, ) -> Result<Step, Error>
Creates a step (Engine.NewStep).
adapter_key_name selects the code-module adapter, see
AdapterKeyName. step_type_name names the
step type, for example NumericLimitTest or Action.
An empty key does not mean “no code module”. It means the step type
chooses, falling back to the station’s DefaultAdapter when the type
designates none, so an empty key on an Action yields whatever adapter
the station happens to default to. Pass
AdapterKeyName::NoneAdapter to
actually mean no code module.
The step is not part of any sequence until it is inserted.
§Errors
Error if the step type is unknown or the COM call fails.
Examples found in repository?
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}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}
74
75/// Adds a statement step that records nothing, to show the gap it leaves.
76fn add_unrecorded_filler(engine: &Engine, sequence: &Sequence, name: &str) -> Result<(), Error> {
77 let step = engine.new_step(AdapterKeyName::NoneAdapter.as_str(), "Statement")?;
78 step.set_name(name)?;
79 step.set_result_recording_option(ResultRecordingOption::Disabled)?;
80 sequence.insert_step(
81 &step,
82 sequence.get_num_steps(StepGroup::Main)?,
83 StepGroup::Main,
84 )
85}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 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}
45
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}
67
68fn main() -> Result<(), Box<dyn std::error::Error>> {
69 let engine = Engine::new()?;
70 let sequence_file = engine.new_sequence_file()?;
71 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
72
73 // Steps are placed by group and index, so order is explicit.
74 main_sequence.insert_step(
75 &numeric_limit_test(
76 &engine,
77 "Temperature Check",
78 "Locals.TempSensorPresent == True",
79 15.0,
80 85.0,
81 )?,
82 0,
83 StepGroup::Main,
84 )?;
85 main_sequence.insert_step(
86 &numeric_limit_test(
87 &engine,
88 "Voltage Monitor",
89 "Locals.DUTPowered == True",
90 4.75,
91 5.25,
92 )?,
93 1,
94 StepGroup::Main,
95 )?;
96
97 // A subsequence, so a later example has something to call.
98 let subsequence = engine.new_sequence()?;
99 subsequence.set_name("CustomSubsequence")?;
100 sequence_file.insert_sequence(&subsequence)?;
101
102 let init_step = engine.new_step(NO_ADAPTER, "Action")?;
103 init_step.set_name("Initialize Hardware")?;
104 subsequence.insert_step(&init_step, 0, StepGroup::Main)?;
105
106 describe(&main_sequence)?;
107 println!();
108 describe(&subsequence)?;
109
110 // Cleanup runs even when Main fails, which is why it is worth showing.
111 println!(
112 "\nGroup sizes in MainSequence: setup={}, main={}, cleanup={}",
113 main_sequence.get_num_steps(StepGroup::Setup)?,
114 main_sequence.get_num_steps(StepGroup::Main)?,
115 main_sequence.get_num_steps(StepGroup::Cleanup)?
116 );
117
118 let path = std::env::temp_dir().join("rs_teststand_built_sequence.seq");
119 sequence_file.save(&path.to_string_lossy())?;
120 println!("\nSaved to {}", path.display());
121
122 engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
123 Ok(())
124}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}44fn add_action(engine: &Engine, sequence: &Sequence, name: &str) -> Result<(), Error> {
45 // The None adapter is what actually means "no code module"; an empty key
46 // would let the step type pick, and a step on a real adapter fails at run
47 // time with "module has not yet been specified".
48 let step = engine.new_step(AdapterKeyName::NoneAdapter.as_str(), "Action")?;
49 step.set_name(name)?;
50 step.set_record_result(true)?;
51 sequence.insert_step(
52 &step,
53 sequence.get_num_steps(StepGroup::Main)?,
54 StepGroup::Main,
55 )
56}Sourcepub fn new_sequence(&self) -> Result<Sequence, Error>
pub fn new_sequence(&self) -> Result<Sequence, Error>
Creates a sequence (Engine.NewSequence).
The sequence is not part of any file until it is inserted.
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
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}68fn main() -> Result<(), Box<dyn std::error::Error>> {
69 let engine = Engine::new()?;
70 let sequence_file = engine.new_sequence_file()?;
71 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
72
73 // Steps are placed by group and index, so order is explicit.
74 main_sequence.insert_step(
75 &numeric_limit_test(
76 &engine,
77 "Temperature Check",
78 "Locals.TempSensorPresent == True",
79 15.0,
80 85.0,
81 )?,
82 0,
83 StepGroup::Main,
84 )?;
85 main_sequence.insert_step(
86 &numeric_limit_test(
87 &engine,
88 "Voltage Monitor",
89 "Locals.DUTPowered == True",
90 4.75,
91 5.25,
92 )?,
93 1,
94 StepGroup::Main,
95 )?;
96
97 // A subsequence, so a later example has something to call.
98 let subsequence = engine.new_sequence()?;
99 subsequence.set_name("CustomSubsequence")?;
100 sequence_file.insert_sequence(&subsequence)?;
101
102 let init_step = engine.new_step(NO_ADAPTER, "Action")?;
103 init_step.set_name("Initialize Hardware")?;
104 subsequence.insert_step(&init_step, 0, StepGroup::Main)?;
105
106 describe(&main_sequence)?;
107 println!();
108 describe(&subsequence)?;
109
110 // Cleanup runs even when Main fails, which is why it is worth showing.
111 println!(
112 "\nGroup sizes in MainSequence: setup={}, main={}, cleanup={}",
113 main_sequence.get_num_steps(StepGroup::Setup)?,
114 main_sequence.get_num_steps(StepGroup::Main)?,
115 main_sequence.get_num_steps(StepGroup::Cleanup)?
116 );
117
118 let path = std::env::temp_dir().join("rs_teststand_built_sequence.seq");
119 sequence_file.save(&path.to_string_lossy())?;
120 println!("\nSaved to {}", path.display());
121
122 engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
123 Ok(())
124}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 new_user(&self, profile: Option<&User>) -> Result<User, Error>
pub fn new_user(&self, profile: Option<&User>) -> Result<User, Error>
Creates a user account object (Engine.NewUser).
Pass an existing user as profile to inherit its privileges; the new
user does not join any group the profile belongs to. Pass None for
a user with no privileges.
The result exists only in memory, nothing is written to the station’s users file by creating one.
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
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 user_name_exists(&self, login_name: &str) -> Result<bool, Error>
pub fn user_name_exists(&self, login_name: &str) -> Result<bool, Error>
Whether a login name is already taken (Engine.UserNameExists).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
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 current_user(&self) -> Result<Option<User>, Error>
pub fn current_user(&self) -> Result<Option<User>, Error>
The user currently logged in (Engine.CurrentUser).
Returns None when nobody is logged in, which is the normal state on a
station that does not require a login.
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
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 current_user_has_privilege(
&self,
privilege: UserPrivilege,
) -> Result<bool, Error>
pub fn current_user_has_privilege( &self, privilege: UserPrivilege, ) -> Result<bool, Error>
Sourcepub fn new_property_object(
&self,
value_type: PropValType,
as_array: bool,
type_name: &str,
options: i32,
) -> Result<PropertyObject, Error>
pub fn new_property_object( &self, value_type: PropValType, as_array: bool, type_name: &str, options: i32, ) -> Result<PropertyObject, Error>
Creates a standalone PropertyObject (Engine.NewPropertyObject).
The object belongs to no sequence file or station; it is useful as the
root of a tree you build in memory. Pass a type name only when
value_type is NamedType.
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
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}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 close(self, timeout: Duration) -> Result<bool, Error>
pub fn close(self, timeout: Duration) -> Result<bool, Error>
Shuts the engine down and leaves this thread’s COM apartment.
For a host that owns the engine on a spawned thread. Such a thread really does detach when it ends, so the apartment it initialized has to be closed or the COM runtime is left believing a live thread still owns one. The process’s main thread does not need this: it is ending anyway.
Consuming self is what makes the ordering safe, the engine is
released before the apartment closes, and no caller can hold a reference
across the boundary.
§Errors
Error if a COM call during shutdown fails. The apartment is closed
either way.
Sourcepub fn shutdown(&self, timeout: Duration) -> Result<bool, Error>
pub fn shutdown(&self, timeout: Duration) -> Result<bool, Error>
Closes files, terminates executions, and waits for the engine to say it
is done (Engine.ShutDown).
ShutDown is asynchronous. It returns as soon as the request is
accepted, having only started terminating executions and closing
files; the engine reports completion later by posting
UIMessageCode::ShutDownComplete
to its message queue. So a caller that simply calls it and drops the
engine tears down COM underneath work that is still running.
This does the whole protocol: enables message polling, asks the engine
to shut down, then pumps and drains until the engine confirms or
timeout elapses.
Returns true when the engine confirmed. false means the timeout came
first, or the engine posted
ShutDownCanceled, which a
sequence can cause, for instance by refusing to terminate. Either way the
wait is bounded: an unattended host must not be able to hang here.
Shutting down twice is harmless; the second call simply finds nothing to do and returns once the engine answers.
§Errors
Error if a COM call fails.
Sourcepub fn get_templates_file(
&self,
options: GetTemplatesFileOptions,
) -> Result<PropertyObjectFile, Error>
pub fn get_templates_file( &self, options: GetTemplatesFileOptions, ) -> Result<PropertyObjectFile, Error>
The station’s templates file (Engine.GetTemplatesFile).
Holds the variable, step and sequence prototypes the editor offers when inserting. It is a station-wide file, so it is empty until someone adds templates to it, an empty one is the normal state, not a failure.
A template is an ordinary PropertyObject, not
a type of its own, so a program is free to keep its own prototypes in a
container it builds itself rather than in this file.
§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}Sourcepub fn get_sequence_file_ex(
&self,
path: &str,
options: GetSeqFileOptions,
handler: ConflictHandler,
) -> Result<SequenceFile, Error>
pub fn get_sequence_file_ex( &self, path: &str, options: GetSeqFileOptions, handler: ConflictHandler, ) -> Result<SequenceFile, Error>
Opens a sequence file, or returns the already-loaded one
(Engine.GetSequenceFileEx).
The engine caches the file and counts load references, so every
successful call must be paired with
release_sequence_file_ex.
Both option arguments matter on an unattended host:
crate::sequence::GetSeqFileOptions::DO_NOT_RUN_LOAD_CALLBACK suppresses a load
callback that could raise a dialog, and crate::sequence::ConflictHandler::Error
fails the load instead of prompting.
§Errors
Error if the file cannot be opened or the COM call fails.
Examples found in repository?
More examples
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}Sourcepub fn release_sequence_file_ex(
&self,
sequence_file: SequenceFile,
options: i32,
) -> Result<bool, Error>
pub fn release_sequence_file_ex( &self, sequence_file: SequenceFile, options: i32, ) -> Result<bool, Error>
Drops one load reference on a sequence file
(Engine.ReleaseSequenceFileEx).
Returns true when that was the last reference and the engine has
discarded the file. false means something else still holds it open,
so the file stays loaded, which is why only the true case also
releases the wrapper’s own COM reference.
§Errors
Error if the COM call fails.
Examples found in repository?
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
146fn main() -> Result<(), Box<dyn std::error::Error>> {
147 let engine = Engine::new()?;
148 // Nothing reaches the queue until this is on, and without the queue there
149 // is no way to know the run ended.
150 engine.set_ui_message_polling_enabled(true)?;
151
152 let sequence_file = engine.new_sequence_file()?;
153 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
154 for (name, data_source, low, high) in TESTS {
155 add_numeric_limit_test(&engine, &main_sequence, name, data_source, low, high)?;
156 }
157
158 let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
159 println!("Running {} headless...", execution.display_name()?);
160
161 if wait_for_end(&engine, RUN_DEADLINE)? {
162 println!("Finished with status: {}", execution.result_status()?);
163 report(&execution.result_object()?)?;
164 } else {
165 // Reported rather than ignored: a host that assumes success here would
166 // publish results from a run that never finished.
167 println!("The run did not finish within {RUN_DEADLINE:?}; terminating.");
168 execution.terminate()?;
169 }
170
171 engine.release_sequence_file_ex(sequence_file, none())?;
172 Ok(())
173}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}68fn main() -> Result<(), Box<dyn std::error::Error>> {
69 let engine = Engine::new()?;
70 let sequence_file = engine.new_sequence_file()?;
71 let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
72
73 // Steps are placed by group and index, so order is explicit.
74 main_sequence.insert_step(
75 &numeric_limit_test(
76 &engine,
77 "Temperature Check",
78 "Locals.TempSensorPresent == True",
79 15.0,
80 85.0,
81 )?,
82 0,
83 StepGroup::Main,
84 )?;
85 main_sequence.insert_step(
86 &numeric_limit_test(
87 &engine,
88 "Voltage Monitor",
89 "Locals.DUTPowered == True",
90 4.75,
91 5.25,
92 )?,
93 1,
94 StepGroup::Main,
95 )?;
96
97 // A subsequence, so a later example has something to call.
98 let subsequence = engine.new_sequence()?;
99 subsequence.set_name("CustomSubsequence")?;
100 sequence_file.insert_sequence(&subsequence)?;
101
102 let init_step = engine.new_step(NO_ADAPTER, "Action")?;
103 init_step.set_name("Initialize Hardware")?;
104 subsequence.insert_step(&init_step, 0, StepGroup::Main)?;
105
106 describe(&main_sequence)?;
107 println!();
108 describe(&subsequence)?;
109
110 // Cleanup runs even when Main fails, which is why it is worth showing.
111 println!(
112 "\nGroup sizes in MainSequence: setup={}, main={}, cleanup={}",
113 main_sequence.get_num_steps(StepGroup::Setup)?,
114 main_sequence.get_num_steps(StepGroup::Main)?,
115 main_sequence.get_num_steps(StepGroup::Cleanup)?
116 );
117
118 let path = std::env::temp_dir().join("rs_teststand_built_sequence.seq");
119 sequence_file.save(&path.to_string_lossy())?;
120 println!("\nSaved to {}", path.display());
121
122 engine.release_sequence_file_ex(sequence_file, NO_OPTIONS)?;
123 Ok(())
124}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}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 search_directories(&self) -> Result<SearchDirectories, Error>
pub fn search_directories(&self) -> Result<SearchDirectories, Error>
Accesses the collection of search directories (Engine.SearchDirectories).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
49fn main() -> Result<(), rs_teststand::Error> {
50 let engine = Engine::new()?;
51 let search_directories = engine.search_directories()?;
52
53 println!("Total search directories: {}", search_directories.count()?);
54 for (index, directory) in search_directories.iter()?.enumerate() {
55 print_entry(index, &directory?)?;
56 }
57
58 // Insert at the front, so the new entry is searched first.
59 println!("\nInserting a new explicit search directory...");
60 let new_path = engine.bin_directory()?;
61 search_directories.insert(&new_path, 0, true, "", false, false)?;
62 println!("Total after insert: {}", search_directories.count()?);
63
64 let inserted = search_directories.get(0)?;
65 println!(
66 "New [0] Path: '{}', Subdirs: {}",
67 inserted.path()?,
68 inserted.search_subdirectories()?
69 );
70
71 // A disabled entry stays in the list but is not searched.
72 println!("Disabling the new directory...");
73 inserted.set_disabled(true)?;
74 println!("New [0] Disabled: {}", inserted.disabled()?);
75
76 // Order matters: entries are searched in list order.
77 println!("Moving the new directory to index 1...");
78 search_directories.move_search_directory(0, 1)?;
79 println!(
80 "Directory at index 1 is now: '{}'",
81 search_directories.get(1)?.path()?
82 );
83
84 println!("Removing the added directory to clean up...");
85 search_directories.remove(1)?;
86 println!("Total after cleanup: {}", search_directories.count()?);
87
88 // The engine writes search directories out at shutdown anyway. Committing
89 // now makes the change visible to other processes immediately, and passing
90 // `false` keeps a save conflict from raising a dialog.
91 engine.commit_globals_to_disk(false)?;
92 println!("Committed search directories configuration to disk.");
93
94 Ok(())
95}Sourcepub fn globals(&self) -> Result<PropertyObject, Error>
pub fn globals(&self) -> Result<PropertyObject, Error>
Accesses the station global variables container (Engine.Globals).
§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}
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}Sourcepub fn new_workspace_file(&self) -> Result<WorkspaceFile, Error>
pub fn new_workspace_file(&self) -> Result<WorkspaceFile, Error>
Creates a new workspace file object (Engine.NewWorkspaceFile).
§Errors
Error if the COM call fails or returns an unexpected type.
Examples found in repository?
5fn main() -> Result<(), rs_teststand::Error> {
6 let engine = Engine::new()?;
7
8 println!("Creating a new workspace file...");
9 let ws_file = engine.new_workspace_file()?;
10 let root_obj = ws_file.root_workspace_object()?;
11
12 println!("Root workspace object:");
13 println!(" Display Name: '{}'", root_obj.display_name()?);
14 println!(" Object Type: {}", root_obj.object_type()?);
15 println!(" Child Objects: {}", root_obj.num_contained_objects()?);
16
17 println!("\nCreating project and folder structure...");
18 let project = root_obj.new_folder("Project Alpha")?;
19 println!(" Created project folder: '{}'", project.display_name()?);
20
21 let seq_entry = project.new_file("MainTest.seq")?;
22 println!(
23 " Created sequence file entry: '{}'",
24 seq_entry.display_name()?
25 );
26
27 println!(
28 "\nUpdated root child objects count: {}",
29 root_obj.num_contained_objects()?
30 );
31
32 Ok(())
33}Sourcepub fn open_workspace_file(
&self,
path: &str,
read_only: bool,
options: i32,
) -> Result<WorkspaceFile, Error>
pub fn open_workspace_file( &self, path: &str, read_only: bool, options: i32, ) -> Result<WorkspaceFile, Error>
Sourcepub fn commit_globals_to_disk(
&self,
prompt_on_save_conflicts: bool,
) -> Result<(), Error>
pub fn commit_globals_to_disk( &self, prompt_on_save_conflicts: bool, ) -> Result<(), Error>
Flushes modified station globals and configuration to disk (Engine.CommitGlobalsToDisk).
§Errors
Error if the COM call fails.
Examples found in repository?
5fn main() -> Result<(), rs_teststand::Error> {
6 let engine = Engine::new()?;
7 let station_options = engine.station_options()?;
8
9 station_options.set_tracing_enabled(true)?;
10 station_options.set_disable_results(false)?;
11 station_options.set_breakpoints_enabled(true)?;
12 station_options.set_check_out_files_when_edited(false)?;
13 station_options.set_language("English")?;
14 station_options.set_always_goto_cleanup_on_failure(true)?;
15 station_options.set_show_hidden_properties(true)?;
16 station_options.set_prompt_to_find_files(false)?;
17 station_options.set_auto_login_system_user(true)?;
18 station_options.set_ui_message_delay(100)?;
19 station_options.set_ui_message_min_delay(10)?;
20 station_options.set_station_id("STATION_RUST_01")?;
21 station_options.set_use_station_model(true)?;
22 station_options.set_allow_other_models(false)?;
23 station_options.set_use_localized_decimal_point(false)?;
24 station_options.set_time_limit(0, 0, 60.0)?;
25 station_options.set_time_limit_enabled(0, 0, true)?;
26
27 engine.commit_globals_to_disk(false)?;
28 println!("Station options updated and committed to disk.");
29
30 Ok(())
31}More examples
49fn main() -> Result<(), rs_teststand::Error> {
50 let engine = Engine::new()?;
51 let search_directories = engine.search_directories()?;
52
53 println!("Total search directories: {}", search_directories.count()?);
54 for (index, directory) in search_directories.iter()?.enumerate() {
55 print_entry(index, &directory?)?;
56 }
57
58 // Insert at the front, so the new entry is searched first.
59 println!("\nInserting a new explicit search directory...");
60 let new_path = engine.bin_directory()?;
61 search_directories.insert(&new_path, 0, true, "", false, false)?;
62 println!("Total after insert: {}", search_directories.count()?);
63
64 let inserted = search_directories.get(0)?;
65 println!(
66 "New [0] Path: '{}', Subdirs: {}",
67 inserted.path()?,
68 inserted.search_subdirectories()?
69 );
70
71 // A disabled entry stays in the list but is not searched.
72 println!("Disabling the new directory...");
73 inserted.set_disabled(true)?;
74 println!("New [0] Disabled: {}", inserted.disabled()?);
75
76 // Order matters: entries are searched in list order.
77 println!("Moving the new directory to index 1...");
78 search_directories.move_search_directory(0, 1)?;
79 println!(
80 "Directory at index 1 is now: '{}'",
81 search_directories.get(1)?.path()?
82 );
83
84 println!("Removing the added directory to clean up...");
85 search_directories.remove(1)?;
86 println!("Total after cleanup: {}", search_directories.count()?);
87
88 // The engine writes search directories out at shutdown anyway. Committing
89 // now makes the change visible to other processes immediately, and passing
90 // `false` keeps a save conflict from raising a dialog.
91 engine.commit_globals_to_disk(false)?;
92 println!("Committed search directories configuration to disk.");
93
94 Ok(())
95}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}