Effective Arc
Best practices and common pitfalls when writing Arc automations
This page covers what works well in Arc and what to avoid. These patterns come from building real control systems.
Best Practices
Safety Conditions First
Line order determines priority for conditional transitions (=>). Always list abort
conditions before normal operations:
stage pressurize {
// Define checks first
ox_pt_1 > 700 => abort
fuel_pt_1 > 500 => abort
abort_btn => abort
// Automation next
true -> press_vlv_cmd
ox_pt_1 > 500 => next
}If multiple transitions are true simultaneously, the first one wins.
Use -> for Streaming, => for Transitions
Continuous edges (->) fire whenever the source produces a value. Conditional edges
(=>) fire while the source is truthy.
// Streaming: continuously process sensor data
sensor -> filter{} -> output
// Transition: fire while the condition is truthy
pressure > 500 => nextFor sequence and stage transitions, => is what you want. -> into a stage name also
transitions but fires on the first data arrival regardless of truthiness, which rarely
matches operator intent.
Keep Stages Focused
Each stage should do one thing. Split complex operations:
// Good: clear purpose for each stage
stage check_sensors {}
stage pressurize {}
stage hold {}
// Avoid: one stage trying to do everything
stage do_everything {
// 50 lines of mixed logic
}Reach for a Stage Only When You Need Parallelism
Stages are for steps that need to watch multiple conditions at once: a success check with safety monitoring, or a control loop that runs while a timeout counts down. If a step is just a write or a wait, make it a sequence item directly:
// Good: the linear part is linear, stages exist only where they earn their keep
sequence prime {
false -> vent_vlv_cmd
true -> press_vlv_cmd
stage {
tank_pressure > 500 => next
time.wait{30s} => abort
}
false -> press_vlv_cmd
}
// Avoid: single-flow stages that exist only to hold one write
sequence prime {
stage close_vent {
false -> vent_vlv_cmd
time.wait{1ms} => next
}
stage open_press {
true -> press_vlv_cmd
time.wait{1ms} => next
}
stage wait_pressure {
tank_pressure > 500 => next
time.wait{30s} => abort
}
stage close_press {
false -> press_vlv_cmd
}
}A stage earns its place when it holds two or more concurrent flows. A stage that holds a single write is noise.
Initialize Stateful Variables Appropriately
Stateful variables ($=) persist across invocations. Consider whether your initial
value makes sense:
func rate(value f64) f64 {
prev $= 0.0 // First call returns full value as "rate"
d := value - prev
prev = value
return d
}The first execution computes value - 0, which may be misleading. Options:
// Option 1: Use a "first run" flag
func rate(value f64) f64 {
prev $= 0.0
first $= true
if first {
prev = value
first = false
return 0.0
}
d := value - prev
prev = value
return d
}Set Authority Below Maximum
Start programs at authority 200 (or lower) rather than the default 255:
import control
authority 200
sequence main {
stage normal {
sensor -> controller{} -> output
emergency_condition => emergency
}
stage emergency {
// Escalate to override all other writers
control.set_authority{value=255}
false -> press_vlv_cmd
true -> vent_vlv_cmd
}
}If you leave authority at the default 255, you cannot escalate above other writers when an emergency occurs. Operators using schematics also need to be able to override automations. Starting below 255 makes this possible without stopping the program.
Common Pitfalls
Using -> When => Is Needed
// Wrong: fires on every data arrival, regardless of truthiness
pressure > 500 -> next
// Right: fires while the condition is truthy
pressure > 500 => next-> into a stage name is legal syntax but transitions on the first value from the
source rather than on truthiness. Operators writing a condition like pressure > 500
almost always mean “while this is true,” which is =>.
Multiple Writes to Same Channel
When multiple flows write to the same channel, last write wins:
stage example {
false -> valve_cmd // Writes false
true -> valve_cmd // Writes true (overwrites false)
}This is usually a mistake. Use conditional logic instead:
pressure < 500 -> valve_cmdType Mismatches
Arc requires explicit type casting. No implicit conversions:
// Wrong
x i32 := 42
y f64 := x + 1.0 // Type error: i32 + f64
// Right
x i32 := 42
y f64 := f64(x) + 1.0The same applies when concatenating numbers with strings. Use str() to render a
numeric value as text first:
// Wrong
count i64 := 42
msg str := "count: " + count // Type error: str + i64
// Right
count i64 := 42
msg str := "count: " + str(count) // "count: 42"Division by Zero in Rates
Rate calculations divide by time. Protect against zero:
func rate{dt_ms f64} (value f64) f64 {
prev $= 0.0
d := value - prev
prev = value
dt_s := dt_ms / 1000.0
if dt_s <= 0 {
return 0.0 // Avoid division by zero
}
return d / dt_s
}Unhandled Transitions
Sequences can get stuck if no transition fires:
stage wait_forever {
...
0 > 1 => next // Stage is blocked from exiting
}Add timeouts:
stage wait_with_timeout {
some_condition => next
time.wait{duration=30s} => timeout_stage
}Performance Guidelines
Control Loop Rates
The C++ driver runtime runs control loops at 1kHz. Deterministic cycle times need a Linux real-time kernel.
For timing-critical applications:
- Use
time.interval{period=...}for consistent timing - Keep flow chains short
- Avoid complex calculations in hot paths
// 1kHz control loop
time.interval{period=1ms} -> fast_controller{}Minimize Work Per Cycle
Each function executes on every trigger. Avoid unnecessary computation:
// Less efficient: recalculates constants
func process(value f64) f64 {
scale := 2.0 * 3.14159 * 0.5 // Computed every call
return value * scale
}
// More efficient: use an input parameter
func process{scale f64} (value f64) f64 {
return value * scale
}
sensor -> process{scale=3.14159} -> outputFlow Chain Length
Long chains add latency. If timing matters, consider combining operations:
// Multiple nodes, multiple cycles of latency
sensor -> filter1{} -> filter2{} -> filter3{} -> output
// Single node, one cycle
sensor -> combined_filter{} -> outputDebugging
Check Task Status
When an Arc automation doesn’t behave as expected, check its status in Console. Runtime errors (division by zero, out-of-bounds access) stop the task and report the error.
Use Channel Outputs for Visibility
Write intermediate values to channels for debugging:
func debug_controller(value f64) f64 {
error := value - setpoint
// Write to a channel for visibiliy
debug_error = f"Error is: {error}"
return error * gain
}Monitor these channels in Console to trace data flow.
Start Simple
Build sequences incrementally:
- Test each stage in isolation
- Add transitions one at a time
- Test abort paths explicitly
- Run the complete sequence
Common Error Messages
Summary
- Put safety conditions first (line order = priority)
- Use
->for streaming data,=>for state transitions - Keep stages focused on one purpose
- Protect against edge cases (division by zero, first sample)
- Add timeouts to prevent sequences from hanging
- Test abort paths thoroughly