Skip to main content

variables_manage/
variables_manage.rs

1//! Example: manage variables across all four scopes.
2//!
3//! A variable lives in one of four places, and which one decides who can see it
4//! and how long it lasts:
5//!
6//! * **Sequence locals**, private to one call of one sequence.
7//! * **Sequence parameters**, supplied by the caller of that sequence.
8//! * **File globals**, shared by every sequence in one file.
9//! * **Station globals**, shared by every file on the station, and persisted.
10//!
11//! The example writes one variable into each, then walks the lifecycle of a
12//! throwaway variable: a property's type is fixed when it is created, so
13//! "retyping" means deleting and recreating, which is what the editor does.
14
15use rs_teststand::{
16    ConflictHandler, Engine, GetSeqFileOptions, PropValType, PropertyObject, SequenceFile,
17};
18
19/// `PropOption_InsertIfMissing`: create the property if it is not there.
20const INSERT_IF_MISSING: i32 = 1;
21
22/// Creates a string variable if absent, then assigns it.
23fn set_string(
24    container: &PropertyObject,
25    name: &str,
26    value: &str,
27) -> Result<(), rs_teststand::Error> {
28    if !container.exists(name, 0)? {
29        container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
30    }
31    container.set_val_string(name, 0, value)
32}
33
34/// Creates a variable, retypes it, clones it, then removes both.
35///
36/// The type is fixed at creation, so each "retype" is a delete followed by a
37/// fresh create, the same thing the sequence editor does behind the scenes.
38fn temporary_variable_lifecycle(container: &PropertyObject) -> Result<(), rs_teststand::Error> {
39    let name = "TempScratch";
40    let clone_name = "TempScratchCopy";
41    for stale in [name, clone_name] {
42        if container.exists(stale, 0)? {
43            container.delete_sub_property(stale, 0)?;
44        }
45    }
46
47    container.new_sub_property(name, PropValType::String, false, "", INSERT_IF_MISSING)?;
48    container.set_val_string(name, 0, "scratch")?;
49    println!(
50        "  created {name} (String) = '{}'",
51        container.get_val_string(name, 0)?
52    );
53
54    container.delete_sub_property(name, 0)?;
55    container.new_sub_property(name, PropValType::Number, false, "", INSERT_IF_MISSING)?;
56    container.set_val_number(name, 0, 42.0)?;
57    println!(
58        "  retyped {name} -> Number = {}",
59        container.get_val_number(name, 0)?
60    );
61
62    // clone copies value and type; set_property_object attaches it under a new name.
63    let copy = container.clone_property(name, 0)?;
64    container.set_property_object(clone_name, INSERT_IF_MISSING, &copy)?;
65    println!(
66        "  cloned  {name} -> {clone_name} = {}",
67        container.get_val_number(clone_name, 0)?
68    );
69
70    container.delete_sub_property(clone_name, 0)?;
71    container.delete_sub_property(name, 0)?;
72    println!(
73        "  removed both: {name} exists={}, {clone_name} exists={}",
74        container.exists(name, 0)?,
75        container.exists(clone_name, 0)?
76    );
77    Ok(())
78}
79
80/// Opens a sequence file if one was named on the command line.
81///
82/// Locals, parameters and file globals all need a real file; without one the
83/// example still demonstrates station globals.
84fn open_sequence_file(engine: &Engine) -> Result<Option<SequenceFile>, rs_teststand::Error> {
85    let Some(path) = std::env::args().nth(1) else {
86        return Ok(None);
87    };
88    let file = engine.get_sequence_file_ex(
89        &path,
90        GetSeqFileOptions::DO_NOT_RUN_LOAD_CALLBACK,
91        ConflictHandler::Error,
92    )?;
93    Ok(Some(file))
94}
95
96/// Everything that lives on the station rather than in a file.
97///
98/// Separated because it is the one scope that outlives the process, so it is
99/// the one worth reading on its own.
100fn show_station_globals(engine: &Engine) -> Result<(), rs_teststand::Error> {
101    // Station globals: shared by every sequence file, and written to disk.
102    let station_globals = engine.globals()?;
103    if !station_globals.exists("StationInfo", 0)? {
104        station_globals.new_sub_property(
105            "StationInfo",
106            PropValType::Container,
107            false,
108            "",
109            INSERT_IF_MISSING,
110        )?;
111    }
112    let station_info = station_globals.get_property_object("StationInfo", 0)?;
113
114    // A container holds mixed types, so one station record can carry the name,
115    // a count and a flag without three separate globals.
116    set_string(&station_info, "StationName", "STATION_01")?;
117    station_info.set_val_number("CalibrationIntervalDays", INSERT_IF_MISSING, 90.0)?;
118    station_info.set_val_bool("FixtureInstalled", INSERT_IF_MISSING, true)?;
119
120    // A 64-bit count. `SetValNumber` stores a double, which starts losing whole
121    // numbers past 2^53, so anything that counts for the life of a station wants
122    // the integer path instead.
123    station_info.set_val_integer64("UnitsTestedTotal", INSERT_IF_MISSING, 9_007_199_254_740_993)?;
124
125    // A nested container, because station data is rarely one level deep.
126    if !station_info.exists("LastCalibration", 0)? {
127        station_info.new_sub_property(
128            "LastCalibration",
129            PropValType::Container,
130            false,
131            "",
132            INSERT_IF_MISSING,
133        )?;
134    }
135    let calibration = station_info.get_property_object("LastCalibration", 0)?;
136    set_string(&calibration, "Technician", "R. Alvarez")?;
137    set_string(&calibration, "Date", "2026-06-14")?;
138
139    println!("StationGlobals.StationInfo:");
140    println!(
141        "  StationName             = '{}'",
142        station_info.get_val_string("StationName", 0)?
143    );
144    println!(
145        "  CalibrationIntervalDays = {}",
146        station_info.get_val_number("CalibrationIntervalDays", 0)?
147    );
148    println!(
149        "  FixtureInstalled        = {}",
150        station_info.get_val_bool("FixtureInstalled", 0)?
151    );
152    println!(
153        "  UnitsTestedTotal        = {}",
154        station_info.get_val_integer64("UnitsTestedTotal", 0)?
155    );
156    println!(
157        "  LastCalibration.Technician = '{}' on {}",
158        calibration.get_val_string("Technician", 0)?,
159        calibration.get_val_string("Date", 0)?
160    );
161
162    // Walk the container rather than naming each field, which is what a host
163    // does when it does not know the shape in advance.
164    println!(
165        "  walked, {} field(s):",
166        station_info.get_num_sub_properties("")?
167    );
168    for index in 0..station_info.get_num_sub_properties("")? {
169        println!(
170            "    {}",
171            station_info.get_nth_sub_property_name("", index, 0)?
172        );
173    }
174
175    // Station globals live in memory until this is called. Without it the values
176    // above are gone when the engine goes away, which is the difference between
177    // a station global and a file global.
178    // `false` means do not prompt if another process changed the file first.
179    // An example must never raise a dialog, and neither must a headless host.
180    engine.commit_globals_to_disk(false)?;
181    println!("  committed to disk");
182
183    Ok(())
184}
185
186fn main() -> Result<(), rs_teststand::Error> {
187    let engine = Engine::new()?;
188
189    show_station_globals(&engine)?;
190
191    // The other three scopes need a sequence file.
192    if let Some(sequence_file) = open_sequence_file(&engine)? {
193        // File globals: shared by every sequence in this file. These are the
194        // defaults stored in the file; a running execution gets its own copy.
195        let file_globals = sequence_file.file_globals_default_values()?;
196        set_string(&file_globals, "BatchID", "BATCH-2026-Q2-001")?;
197        println!(
198            "FileGlobals.BatchID                   = '{}'",
199            file_globals.get_val_string("BatchID", 0)?
200        );
201
202        let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
203
204        // Locals: private to one call of this sequence.
205        let locals = main_sequence.locals()?;
206        set_string(&locals, "OperatorName", "Alice")?;
207        println!(
208            "MainSequence.Locals.OperatorName      = '{}'",
209            locals.get_val_string("OperatorName", 0)?
210        );
211
212        // Parameters: supplied by whoever calls this sequence.
213        let parameters = main_sequence.parameters()?;
214        set_string(&parameters, "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}