Teal-Compatible Measurement Script Migration Guide¶
Overview¶
The instrument-script-server now uses blocking execution with structured return types for type safety and Teal static typing support. All measurement commands return MeasurementResponse objects with metadata.
New Script Format¶
Main Function Structure¶
Scripts should now define a main function that receives the runtime context:
-- Example: New Teal-compatible format with MeasurementResponse
function main(ctx, voltage)
ctx:log("Starting measurement")
-- ctx:call returns MeasurementResponse objects
local current_resp = ctx:call("DMM.MEASURE")
-- Extract the actual value
local current = current_resp:value()
-- Perform calculations
local power = current * voltage
-- Or use built-in math operations
local adjusted = current_resp:add_offset(-0.001):multiply_gain(1.05)
ctx:log(string.format("Power: %.6f W", power))
return adjusted:value()
end
Key Features¶
- Context Parameter: The
main(ctx)signature receives the runtime context - MeasurementResponse: All
ctx:call()operations return structured objects with metadata - Blocking Execution: Commands execute synchronously and return actual values
- Math Operations: Built-in
add_offset()andmultiply_gain()for signal processing - Explicit Returns: Main function must have a return statement
- Error Handling: Use
ctx:error(message)to report failures
MeasurementResponse API¶
Return Type Structure¶
-- MeasurementResponse object returned by ctx:call()
local response = ctx:call("INSTRUMENT.COMMAND")
-- Access metadata
response:instrument() -- string: Instrument name
response:verb() -- string: Command/verb name
response:type() -- string: "float"|"integer"|"string"|"boolean"|"buffer"
-- Extract value
response:value() -- any: The actual measurement value
-- Math operations (for numeric types)
response:add_offset(offset) -- MeasurementResponse: Add offset
response:multiply_gain(gain) -- MeasurementResponse: Multiply by gain
-- For array types
response:buffer() -- BufferHandle: Get buffer for array data
Example Usage¶
Scalar values:
function main(ctx)
-- Get measurement response
local voltage_resp = ctx:call("DMM.MEASURE_VOLTAGE")
-- Extract value for calculations
local voltage = voltage_resp:value()
-- Use in expressions
if voltage > 5.0 then
ctx:error("Voltage too high")
return nil
end
-- Apply corrections using built-in methods
local corrected = voltage_resp:add_offset(-0.01):multiply_gain(1.02)
return corrected:value()
end
Array buffers:
function main(ctx)
-- Get array measurement
local waveform_resp = ctx:call("Scope.GET_WAVEFORM")
-- Get buffer handle
local buffer = waveform_resp:buffer()
-- Apply signal processing
buffer:add_offset(-0.5) -- DC offset correction
buffer:multiply_gain(10.0) -- Amplification
-- Buffer metadata
ctx:log(string.format("Buffer has %d elements", buffer:size()))
return waveform_resp
end
API Changes¶
New Methods¶
ctx:call(command, args...) → MeasurementResponse¶
Executes an instrument command and returns a structured response object:
local response = ctx:call("INSTRUMENT.COMMAND", param1, param2)
local value = response:value()
MeasurementResponse:add_offset(offset) → MeasurementResponse¶
Adds an offset to numeric measurement values:
local corrected = response:add_offset(-0.001)
local value = corrected:value()
MeasurementResponse:multiply_gain(gain) → MeasurementResponse¶
Multiplies numeric measurement values by a gain factor:
local scaled = response:multiply_gain(1.05)
local value = scaled:value()
ctx:error(message)¶
Reports an error from the measurement script:
function main(ctx)
if some_condition then
ctx:error("Invalid configuration")
return nil
end
end
Backward Compatibility¶
Scripts without a main function continue to work but emit deprecation warnings:
-- Old format: still supported (deprecated)
context:log("Starting measurement")
local result = context:call("INSTRUMENT.COMMAND")
-- Warning: Returns raw values, not MeasurementResponse objects
Warning: Compatibility mode is deprecated and will be removed in a future version.
Migration Steps¶
1. Wrap Existing Code in Main Function¶
-- Before
context:log("Test")
local result = context:call("INSTRUMENT.GET_VALUE")
-- After
function main(ctx)
ctx:log("Test")
local result_resp = ctx:call("INSTRUMENT.GET_VALUE")
local result = result_resp:value()
return result
end
2. Update Value Extraction¶
-- Before (old format - raw values)
local voltage = context:call("DMM.MEASURE")
local current = context:call("DMM.MEASURE_CURRENT")
local power = voltage * current
-- After (new format - MeasurementResponse)
function main(ctx)
local voltage_resp = ctx:call("DMM.MEASURE")
local current_resp = ctx:call("DMM.MEASURE_CURRENT")
local voltage = voltage_resp:value()
local current = current_resp:value()
local power = voltage * current
return power
end
3. Use Built-in Math Operations¶
function main(ctx)
local raw_resp = ctx:call("INSTRUMENT.MEASURE")
-- Chain operations
local processed = raw_resp
:add_offset(-0.5) -- Remove DC offset
:multiply_gain(10.0) -- Apply gain
return processed:value()
end
4. Add Error Handling¶
function main(ctx)
local result_resp = ctx:call("INSTRUMENT.MEASURE")
if result_resp:type() == "void" then
ctx:error("Measurement returned no data")
return nil
end
return result_resp:value()
end
Teal Type Definitions¶
When using Teal, you can define strict types for the MeasurementResponse:
-- Type definitions for instrument-script-server
record MeasurementResponse
instrument: function(MeasurementResponse): string
verb: function(MeasurementResponse): string
type: function(MeasurementResponse): string
value: function(MeasurementResponse): any
add_offset: function(MeasurementResponse, number): MeasurementResponse
multiply_gain: function(MeasurementResponse, number): MeasurementResponse
buffer: function(MeasurementResponse): BufferHandle
end
record BufferHandle
id: function(BufferHandle): string
size: function(BufferHandle): integer
type: function(BufferHandle): string
add_offset: function(BufferHandle, number): boolean
multiply_gain: function(BufferHandle, number): boolean
end
record RuntimeContext
log: function(RuntimeContext, string)
call: function(RuntimeContext, string, ...any): MeasurementResponse
error: function(RuntimeContext, string)
parallel: function(RuntimeContext, function())
end
-- Typed main function
function main(ctx: RuntimeContext, voltage: number): number
ctx:log("Starting typed measurement")
-- Type-safe measurement with MeasurementResponse
local current_resp: MeasurementResponse = ctx:call("DMM.MEASURE")
local current: number = current_resp:value() as number
-- Apply corrections
local corrected: MeasurementResponse = current_resp
:add_offset(-0.001)
:multiply_gain(1.05)
local power: number = (corrected:value() as number) * voltage
return power
end
Error Handling¶
Script-Level Errors¶
Use ctx:error() for expected error conditions:
function main(ctx)
local resp = ctx:call("INSTRUMENT.MEASURE")
if resp:type() == "void" then
ctx:error("Measurement returned no data")
return nil
end
return resp:value()
end
Lua Runtime Errors¶
Lua runtime errors (exceptions) are automatically captured and included in the response:
function main(ctx)
-- This will be caught and reported
error("Unexpected error")
end
Both types of errors appear in the measurement response with appropriate status and error messages.
Response Format¶
Successful Measurement¶
{
"ok": true,
"script": "measurement.lua",
"results": [
{
"index": 0,
"instrument": "INSTRUMENT1",
"verb": "MEASURE",
"params": {},
"executed_at_ms": 1234567890,
"return": {
"type": "float",
"value": 3.14
}
}
]
}
Failed Measurement¶
{
"ok": false,
"error": "Measurement failed: no result",
"script": "measurement.lua",
"results": [...]
}
Best Practices¶
- Always define main(): Use the new format for all new scripts
- Validate inputs: Check required parameters at the start of main()
- Use context:error(): For expected error conditions
- Return explicitly: Always include
return nilor return value - Type annotations: Add Teal type annotations for compile-time checking
Testing¶
Test scripts for both formats are available in tests/data/test_scripts/:
simple_call_new_format.lua: Basic new format exampleerror_handling_new_format.lua: Error handling example- Original test scripts continue to work (backward compatibility)
Implementation Notes¶
- Both sync (RPC) and async (JobManager) execution paths support the new format
- Lua libraries are loaded once and cached via
package.preload - Multiple library paths enable modular measurement library organization
- Error state is tracked independently from measurement results