Data AcQuisition And Real-Time AnalysisScope - Spectrum - Spectrogram - Signal Generator
Software for Windows
Science with your Sound Card!
Contact us about
Macro String Arrays Str0-Str7, StrV
A string is simply text, such as "This is a string!". It is an array of bytes, each a displayable ASCII character, including numerals and the uppercase and lowercase letters of the English alphabet, plus all the symbols on a standard keyboard.
A string is directly displayable because the sequence of character bytes can be sent to a display device without further conversion. In this respect strings are conceptually different from numerical values.
A Daqarta variable with a numerical value (such as a voltage or frequency) must first be converted to a string if it is to be displayed via Msg, for example. Usually that happens automatically, such as Msg=SmplRate + " Hz"; the sample rate is first converted to a string, such as "48000", then " Hz" is appended before the resultant "48000 Hz" message is displayed.
If you first set Str0=SmplRate + " Hz", then the sample rate is converted at that time; if you later use Msg=Str0 the string "48000 Hz" is simply copied to the message display.
Daqarta provides 8 string arrays, Str0 through Str7, which can accept strings just like the Msg example above. This allows the string to be manipulated before subsequent use, such as being displayed by Msg or a meter, added to Notes, output to a log file, or used to label a meter or control.
For example, Str0="Stimulus: " + L.0.ToneFreq + " Hz" could be followed by Mtr0=Str0 to display a Generator stimulus frequency on a large resizeable meter, which might be shown as "Stimulus: 1000.000 Hz". You might also want to use Notes=Notes +n +Str0 to add it to Notes (on a new line, via the 'n'), which will be saved with a .DQA file of the data. In addition, you could use LogTxt=n + Str0 + " at " +t to send it to the log file on a new line, with the current time appended via the 't'.
The above use allows Str0 to be written once but used for multiple purposes without repeating the entire "Stimulus: "... entry for each use. Note, however, that variables like ToneFreq will be converted to text at the time Str0 is defined, and simply copied when it is used. See the Numerical Evaluation subtopic below for an alternative that evaluates variables when used.
Each of the 8 string arrays can hold up to 2048 characters. You may not often need to store a single string that long, although it can be used to hold a maximally-full Notes. But you aren't limited to a single string in each array: You can divide an array into as many smaller strings as will fit. See Multiple Substrings, below.
The Auto_Recorder macro mini-app provides examples of using string arrays to hold multiple Custom Controls labels for easy changes when a button state changes. It also uses string arrays to hold copies of file paths and display names for repeated later use, such as specifying where to write data file blocks, and for setting the Log File name and data headers.
Instead of explicit Str0 to Str7 forms you can use StrV to select the string via the Channel Select variable Ch. For example, if the current value of Ch is 3, then StrV will be equivalent to Str3 wherever it appears.
You can also use these "string" arrays to hold binary values. This allows compact and orderly storage of variables, used extensively by the DaquinOscope macro mini-app.
The _Get_String macro subroutine included with Daqarta manipulates a string array to first show a prompt such as "Pin Number: " via a Msg macro, then accept characters via WaitKey and add them to the displayed string one at a time, allow simple backspace editing, and finally, when the Enter key is hit, return to the calling macro with a string that holds the user entry. That string can then be used with numerical evaluation to set a variable.
Write text to string: Str0="Text" Immediate text string Str0="Text" Indexed string Str0=UA Integer variable Str0="Hex=" +UA(h) +"h" String expression Str0=Str0 + " Added text" Appended text Str0="Preface " + Str0 Prepended text Str0="Text1" +z +"Text2" +z Null-terminated strings Str1=Str0 String copy Str1=Str0[0,100] Copy between indexes Str1=Str0[100,0] Reverse copy Str1=Str0[100,-23] Copy 23 chars from 100 on Str1#A=Str0 Copy All of Str0 to Str1, nulls included Str0#A=Buf0(aE) Copy 2048 bytes from Buf0, with nulls Read from string: Field1=Str0[100 + 16 * UA] Read selected string UN=Str0?N String size UN=Str0?N[100,123] String size between indexes UA=Str0?R Raw integer at index, nulls included A=Str0?E[100,123] Evaluate between indexes Msg=Str0?U[100,123] Uppercase Msg=Str0?L[100,123] Lowercase Write binary to string: Str0#b=UA Copy low binary byte from UA Str0#1=UA Same as above Str0#w=UA Copy low binary word from UA Str0#2=UA Same as above Str0#d=UA Copy 4-byte dword from UA Str0#4=UA Same as above Str0#q=A Copy 8-byte qword from A Str0#8=A Same as above Str0#f=A Copy 10-byte float from A Read binary from string: UA=Str0?b Read binary byte into UA UA=Str0?1 Same as above UA=Str0?w Read binary word into UA UA=Str0?2 Same as above UA=Str0?d Read 4-byte dword into UA UA=Str0?4 Same as above A=Str0?q Read 8-byte qword into A A=Str0?8 Same as above A=Str0?f Read 10-byte float into A
A single 2048-character string variable such as Str0 can be accessed at arbitrary locations via indexes. Index values start at 0 and run to 2047. The index is the starting byte of the substring to write or read. If you use two indexes, the second is the ending byte.
For example, if Str0="1234ABCD" then Msg=Str0 would display ABCD, while Msg=Str0[0,3] would display 1234.
If the second index is negative, its absolute value is used as a count instead of an actual index. In the above example, Msg=Str0[2,-4] would display 34AB.
The above rules apply to StrN strings on either the left or right side of the command. However, when used on the left side the lowest index must come first; on the right side the order may be reversed to copy that string in reverse. See Reading And Copying Strings, below.
You are not limited to immediate values for indexes; you can use variables and numeric expressions as needed. For example, you can use Str0[16*UA]="New Text" to space entries 16 characters apart based on variable UA.
Note that since indexes are always integers, you can use integer variables U0-UZ or Q0-QZ and save the floating-point A-Z and 64-bit fixed-point Var0-VarZ for calculations that really need fractions.
You can write immediate text to a string by surrounding it with quotes, as in Str0="Some Text".
Numerical values can be entered the same way, such as Str0="123.456", but they will be stored as strings. That's fine if they are only intended for later display, but not for direct use in calculations... see Numerical Evaluation for that.
You can also set a string with any Daqarta variable, as in Str0=UA or Str0=SmplRate. The current value of the variable will be converted to a string, and stored as such. As noted above, fine for display but not calculation.
You can send a string expression to a string, such as Str0="THD: " + A(0.4) + " at " + L.0.ToneFreq + " Hz". Note that string expressions can use all the usual formatting options here, such as n for 'newline' and b(UA) to insert UA blank spaces, as well as d to insert the current date and t to insert the current time.
However, in this usage the number given with the p (position) and f (fill) commands is now treated as a simple character count, not a column number. So f_(10) (or f_10) will simply insert 10 underscores, and p(UA) will insert UA spaces just like b(UA).
One option that is only available with StrN arrays is z to insert one or more nulls, which are binary 0 (not the ASCII character for "0" which is 30 hex). z10 or z(10) will insert 10 nulls, but the first format only allows a count up to 999, while the second allows up to 2048 (as does the z(UA) format) to fill the entire string with nulls.
You can uses indexes to write to an arbitrary portion of the 2048-character string array. For example, if you have previously used Str0="1234ABCD", followed by Str0="56", then Msg=Str0 will display 123456CD.
If instead of Str0="56" you use Str0="EFGH", then Msg=Str0 will show 1234ABCDEFGH.
However, if instead you write to the next-higher index via Str0="EFGH", then (assuming that the string was empty before beginning) there would be a null between the D at index 7 and the E at index 9. So Msg=Str0 would show the original 1234ABCD, while Msg=Str0 would show EFGH.
You can use two indexes to write to a limited region of the string array, as in Str0[100,115]="1234" will write the given 1234 text to bytes 100-103 and clear everything from 104-115. Conversely, Str0[100,103]="12345678" will write the same 1234 to bytes 100-103, ignoring the rest of the given string and likewise not affecting 104 or above.
Instead of a second index you can give the negative of the desired size, including both ends of the target region. Str0[100,-16]="1234" is equivalent to Str0[100,115]="1234" above, and Str0[100,-4]="12345678" is equivalent to Str0[100,103]="12345678".
Clearing a string is the same as filling it with nulls, which are binary zeros and not the ASCII character for "0" (which is 30 hex).
You can clear an entire string via Str0=, with no value given.
Str0=z(2048) will accomplish the same thing.
You can clear everything starting from a given point by using an index, as in Str0=. This will clear the top half of the string, positions 1024 through 2047.
Str0=z(1024) is equivalent. (You could just as well use a too-large z value such as Str0=z(9999), since the clearing will automatically stop at the end of the string.)
When you write to a string using only a single index, or no index to write to the start, then everything past the end of the string is automatically cleared. So Str0="0123" will write to bytes 0-3 and clear bytes 4-2047.
If you use two indexes to write to a specific region, the region is cleared from the end of the text to the end of the region. Str0[100,120]="ABCD" will write to bytes 100-103 and clear 104-120. Everything above 120 will be left as-is. The same is true if the second index is negative to specify the size of the region, as in Str0[100,-11]="ABCD".
If you want to clear the whole region, you can use Str0[100,120]=z or Str0[100,-11]=z. Please note that you can not use Str0[100,120]=. If you do, the second index (or negative size) is ignored; everything above the first index will be cleared.
Reading and copying string variables are really equivalent, since "reading" involves copying to some other string such as a Label or Field, a message buffer, or another StrN array.
Field1=Str0[100 + 16 * UA] copies a string from the computed position in Str0 to Field1. Fields (and Labels) can only hold a maximum of 15 characters; if the string at the computed position is longer than that, it will be truncated. Otherwise, the copy will stop at the first null encountered.
If you want to pack strings together without terminal nulls to separate them, you can use 2 indexes; the second one indicates the final position to be copied, or (more usefully) the negative of the maximum number of characters. In the above example, the strings could be 15 characters apart and accessed via Field1=Str0[100 + 15 * UA,-15].
Either or both index / count values can be given as numeric variables or expressions.
If you use two index values, rather than an index and a negative count, the string will be copied in reverse if the first index is greater than the second. For example, if the raw string is Str0="1234ABCD" and you use Str1=Str0[7,3] followed by Msg=Str1, the displayed message will be DCBA4. (You could get the same display by skipping the Str1 copy and just using Msg=Str0[7,3].)
Note that, as mentioned in Writing to Strings above, any numerical values that have been written will be stored as strings of ASCII text characters. They can be read or copied for display uses, but not directly used for calculations without first converting back to raw numbers; see Numerical Evaluation, below.
For example, if Str0="1234", then Field1=Str0 will show 1234. But UA=Str0 will set it with the hex value 31323334 since "1" = 31h, "2" = 32h, etc. You can see that if you display it with Msg=UA(h), while if you use Msg=UA you'll see the decimal equivalent of 825373492. If you want to see the original string you can use Msg=UA(A), but note that this is limited to 4 characters for integers like U0-UZ and Q0-QZ. For floating-point variables A-Z or 64-bit fixed-point values like Var0-VarZ you can get up to 8 characters, including a decimal point, if any.
Note that in the above example UA=Str0 is equivalent to UA="1234"; the string is just copied to UA from Str0 instead of from immediate data.
There are special cases where you want to copy everything from the right side of the command to the string on the left. Those cases are where you are copying another Str0-7 string or a Buf0-7 macro array buffer, including any nulls they may contain. If you just use Str1=Str0, the copy process will stop at the first null encountered in Str0. To make sure they are included, use Str1#A=Str0.
Similarly, a macro array buffer Buf0-7 may hold many null-terminated strings. (See String Storage for more details, including size specifiers.) You could use Str0#A=Buf0(aE) to copy the first 2048 bytes of Buf0 to Str0, nulls included. Since Buf0 can hold up to 8192 bytes (8 bytes per index), it could be used to fill 4 different StrN strings. You could use Str1#A=Buf0(aE), plus Str2#A=Buf0(aE) and Str3#A=Buf0(aE) to copy them all.
One reason you might want to do this is to copy a set of strings from a file, since BufN commands support file operations and StrN commands don't.
To copy a full StrN to a BufN, such as for a subsequent file save, use Buf0#aE=Str0[0,2047] or equivalent. Note that in this case you need to use two indexes on the right side to copy everything; if you use Buf0#aE=Str0 it will stop at the first null.
You can use smaller ranges as desired. For example, use Buf0#aD=Str0[0,1023] to copy the first 1024 bytes from Str0, or Buf0#aC=Str0[512,1023] to copy the second 512 bytes. Likewise, you can send them to different portions of the BufN by changing its starting index. (Note that BufN arrays hold 8 bytes per index, while StrN arrays are one byte per index.)
Suppose you need a bunch of labels, such as for Custom Controls whose labels change according to current state, with the longest label being (say) 15 characters. You could store the strings spaced 16 characters apart (allowing a terminal null after each), and refer to them via an index variable like UA. To get the UAth string (starting from 0), you would use Str0[16 * UA] where UA ranges from 0 to 15.
These "substrings" can be of arbitrary mixed lengths, as long as you keep track of them. For instance, you might have 10 of the labels discussed in the above example at 16 bytes each, followed by 12 strings of up to 40 bytes each, including terminal nulls. If these strings are numbered 0-11 you could find the location of the UBth string via Str0[16 * 10 + 40 * UB].
You can use two-index addressing to eliminate the need for terminal nulls, or to select only a portion of a substring. For example, the Auto_Recorder macro mini-app sets the following substrings near the start of the macro:
Str0="RMS Event Threshold, %FS" +z ;Ctrl0 w. Btn2 = 0 Str0="Peak Event Threshold, %FS" +z ;Ctrl0 w. Btn2 = 1 Str0[2*32]="RMS Quiet Threshold, %FS" +z ;Ctrl1 w. Btn2 = 0 Str0[3*32]="Peak Quiet Threshold, %FS" +z ;Ctrl1 w. Btn2 = 1 Str0="RMS Mode" +z ;Btn2 = 0 Str0[130 + 16]="Peak Mode" +z ;Btn2 = 1 Str0="Event = Left " +z ;Btn4 = 0 Str0[170 + 16]="Event = Right" +z ;Btn4 = 1 Str0[170 + 2*16]="Event = Both " +z ;Btn4 = 2 Str0="Setup TC = 10 " +z ;Btn5 = 0 Str0[220 + 16]="Setup TC = 100 " +z ;Btn5 = 1 Str0[220 + 2*16]="Setup TC = 1000" +z ;Btn5 = 2
These are all null-terminated (the +z after each inserts a zero), and they are used that way for normal labels. But these same strings are used to send shortened versions of current settings to a log file. The Mode may be either "RMS" or "Peak", depending on the Btn2 setting, but the whole "RMS Mode" or "Peak Mode" strings are not sent. Instead, only the first 4 characters are used by setting the second index to -4. (See String Indexes, above.)
The mode thus could have been sent as Str0[130 + 16*Btn2],-4, but note that the Ctrl0 labels at Str0 also include the current mode at the start, so Str0[32*Btn2],-4 does the same job in less space. See, for example, the Btn3 Event Logging toggle code in _Auto_Rec_Ctrls custom controls handler, where the relevant substring is italicized here for clarity:
... IF.Ctrls=7 ;Btn3 = Event Logging toggle ... LogTxt=n +"Chan" +p7 +"Mode" +p13 +"Thresh" +p21 _ ;Settings +"QuTh" +p27 +"PreSt" +p34 +"MinQu" +p42 +"TC" _ +n + Str0[178 + 16*Btn4,-5] +p8 +Str0[32*Btn2,-4] _ +p16 +Ctrl0(0) +p23 +Ctrl1(0) +p29 +Ctrl2(0) _ +p36 +Ctrl3(0) +p43 +Str0[231 + 16*Btn5]
Likewise, the Btn4 Event Channel strings start at Str0 but the leading "Event = " portion is not sent to the log. Instead, the start index is advanced to 178 to skip over that portion, and only the following 5 characters are sent via Str0[178 + 16*Btn4,-5]. (Note that an extra space was included after "Left " and "Both " to pad them up to 5 characters to match "Right".)
The same strategy is used to show the Btn5 Setup TC. Those strings start at index 220, but by starting instead at 231 we skip over the "Setup TC = " portion and only send the values of "10 ", "100 ", or "1000" via Str0[231 + 16*Btn5]. In this case the second index can be omitted since we are using the tail end of these strings.
UN=Str0?N sets variable UN with the size of the string in Str0. The count includes all displayable characters before the first null.
If you have multiple substrings, say at 16 characters apart, you can compute the size of the UAth string via UN=Str0?N[16*UA]. Note, however, that this assumes that each substring has a terminal null; if not, the count will continue until it finds a null or the end of the entire array.
To deal with such packed strings, you can supply a (negative) count as the second index: UN=Str0?N[16*UA,-16]. This will set UN to 16 maximum, or to the actual size if a null is encountered before that.
You can also find the size of the first string between two arbitrary indexes, as in UN=Str0?N[100,123].
Either or both index / count values can be given as numeric variables or expressions.
These are special forms of reading or copying that force the case of the text to upper or lower. For example, if there is a substring starting at index 100 you can use Msg=Str0?U to display it in Uppercase, or Msg=Str0?L to display it in Lower.
The Uppercase option is useful to make certain text stand out, such as for use as a header when used in a log file that includes lots of other information.
As with other read and copy operations, you can specify a second index to limit the text copied, as in LogTxt=n + Str0?U[100,123]. Alternatively, the second value can be a negative count of the maximum number of characters to copy, as in LogTxt=n + Str0?U[100,-24].
Either or both index / count values can be given as numeric variables or expressions.
If you use two index values, rather than an index and a negative count, the string will be copied in reverse if the first index is greater than the second.
UA=Str0?R reads the Raw value, which consists of the 4 bytes starting at the given index. The UA value is "raw" because it is exactly as-is, including nulls. For example, if Str0="12" +z +"4", then the raw value (in hexadecimal, viewed via Msg=UA(h)) would be 31320034, where hex 31 is the ASCII value corresponding to "1", etc. Conversely, with the ?R omitted, UA=Str0 would give hex 00003132 because reading would stop at the null.
This function can be useful in debugging a string macro that doesn't seem to be giving the expected results.
Note that this function only returns integer values. Since it reads 4 bytes, it is a perfect match for 32-bit integer variables like U0-Z or Q0-Z. If you use it with floating-point variables like A-Z or 64-bit fixed-point variables like Var0-VarZ, the fractional part will be null. If you use 32-bit fixed-point variables like Ua-Uz or Qa-Qz, the 16-bit integer part can only hold 2 ASCII characters, so unless the first 2 string bytes happen to be nulls the raw value will overflow the integer portion and be limited to hex FFFF.
As noted under Writing to Strings and Reading and Copying Strings above, if you write a value to a string such as Str0="1.234" or Str0=A it will be stored as its directly-displayable ASCII text equivalent... not a numerical value that can be used directly in calculations.
However, you can extract the value by using the Evaluate function ?E, as in A=Str0?E. You can then use the extracted value in calculations. (Note, however, that you can not use the Str0?E in calculations directly, as in B=C * Str0?E + D; you have to extract it as a separate step, and then use the extracted value in the calculation.)
In fact, you can do much more than simply extract a previously stored value; you can evaluate any expression that Daqarta's macro system can handle. For example, if Str0="pi * R^2" then if R is 10, A=Str0?E will set A to 314.159265359. It will use the value of R that is current at evaluation time, not at the time the string was stored.
Note: The string expression within StrN must not be a complete equation itself; it must not have an '=' sign. Effectively, the string expression becomes the right-hand portion of the equation, assigned to the left-hand variable (A in the above example).
If you have multiple expressions stored in the string, you can select the desired expression via indexes. If the expressions all have the same maximum size and are terminated by nulls, you only need to compute and use the starting index, as in A=Str0?E[UI * US] where each string is up to US-1 characters, plus the null. Without terminal nulls, you'd use A=Str0?E[UI * US,-US] to specify the (negative) size in the second term.
Note that the evaluation can't be used as an index, except under limited conditions: You can only have a single evaluation on the right side of the command, and if it is used to form an index you can only use the single-index form of the evaluation. For example, Msg=Str0[Str1?E[UB]] is OK, but A=Str0[Str1?E[UB,UC]] is not.
Expressions for evaluation can use all standard Daqarta variables, like SmplRate, L.0.ToneFreq, and TrigLevel, just like normal macro expressions. This could be used to create a "select case" macro that performs different computations based on the value of selector UI, as opposed to a whole bunch of IF.UI=0..., IF.UI=1... statements.
Since StrV can be used to select the particular string, the set of expressions can be responsive to changing situations. A Buf0-7 macro array can store up to 4 complete StrN strings using the #aE 2048-byte string size format, and two adjacent BufNs can be saved as a 2-channel file, so you can replace all 8 StrNs with a single file read.
Although Str0-Str7 are called string arrays, they also allow pure binary values of 5 different sizes from single bytes to 10-byte floats. A single 2048-byte string array can thus provide storage for:
2048 byte values 1024 words (2 bytes each) 512 dwords (4 bytes each) 256 qwords (8 bytes each) 204 floats (10 bytes each)
These can be combined in any desired arrangement, along with ordinary text storage. This approach is not only more compact than schemes using the numerical evaluation of strings, but is much easier to use.
For example, the DaquinOscope macro mini-app uses single bytes to store Arduino pin numbers selected for oscillator outputs, 4-byte values to store their step sizes and accumulator start positions, and 8-byte values to store frequencies and phases. It also uses single bytes to store bitmaps of digital input configurations.
By being in an array of fixed sizes, these values can be accessed by simple indexing schemes. For example, to save the frequency Z of oscillator Q0 it uses Str7[1500 + 8 * Q0]#q=Z. Since Q0 can run from 0 to 3, and each frequency takes 8 bytes (indicated by the #q), these values run from index  through . No separators are needed. To read the stored value later it uses Z=Str7?q[1500 + 8 * Q0].
When storing a value to the string, the size code follows the index with a '#', as in the above Str7[1500 + 8 * Q0]#q=Z.
When reading a value from the string, the size code precedes the index with a '?', as in Z=Str7?q[1500 + 8 * Q0].
You may optionally use a digit instead of a letter code for size. Only the following sizes and codes are accepted:
Write binary to string: Str0#b=UA Copy low binary byte from UA Str0#1=UA Same as above Str0#w=UA Copy low binary word from UA Str0#2=UA Same as above Str0#d=UA Copy 4-byte dword from UA Str0#4=UA Same as above Str0#q=A Copy 8-byte qword from A Str0#8=A Same as above Str0#f=A Copy 10-byte float from A Read binary from string: UA=Str0?b Read binary byte into UA UA=Str0?1 Same as above UA=Str0?w Read binary word into UA UA=Str0?2 Same as above UA=Str0?d Read 4-byte dword into UA UA=Str0?4 Same as above A=Str0?q Read 8-byte qword into A A=Str0?8 Same as above A=Str0?f Read 10-byte float into A
Note that although it's possible to use the Binary-to-String Format typically used in Port Access commands (as discussed in String Variables and Expressions) to store binary values, this is not recommended.
For example, you could use Str0=$b(UA) to store a byte, Str0=$w(UA) to store a word, or Str0=$d(UA) to store a dword. However, the numerical equivalents are not the same for 2 or 4 bytes, since $2 and $4 store the bytes in reverse order. Plus, this system doesn't include a complementary way to read values from the string, and does not support 8-byte or 10-byte values.
You may want to use different subroutines depending upon certain conditions, such as the type of serial device found by _ComDev_Scan. For example, the DC_Chart_Recorder macro mini-app supports 5 different devices (Arduino plus 4 Numato models), and the code needed to support each is substantially different. Instead of a single named subroutine macro that would need many separate device tests (which would slow operation), each device has its own subroutine that is called on each acquisition time point.
Suppose variable UN has been set to some identifying value that is different for each device (0, 8, 16, 32, or 64) If simple named subroutine calls were used, that would still require multiple IF tests to determine which subroutine to call at each time point:
IF.UN=0 @_DC_Chart_Arduino ENDIF. IF.UN=8 @_DC_Chart_Numato08 ENDIF. IF.UN=16 @_DC_Chart_Numato16 ENDIF. IF.UN=32 @_DC_Chart_Numato32 ENDIF. IF.UN=64 @_DC_Chart_Numato64 ENDIF.
Instead, the DC_Chart_Recorder uses a single indirect macro subroutine call through string variable Str7:
Note the double @@ symbols. This uses a null-terminated string that starts at index 400. This string is created when DC_Chart_Recorder starts up, after setting UN as above based on the results of _ComDev_Scan. It then uses:
IF.UN=0 Str7="_DC_Chart_Arduino" ELSE. Str7="_DC_Chart_Numato"+UN(A) ENDIF.
The UN(A) at the end of the ELSE branch converts the numeric value (8-64) to ASCII string text characters.
Questions? Comments? Contact us!We respond to ALL inquiries, typically within 24 hrs.
Over 30 Years of Innovative Instrumentation
© Copyright 2007 - 2020 by Interstellar Research
All rights reserved