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 {
// SAFETY FIRST
ox_pt_1 > 700 => abort
fuel_pt_1 > 500 => abort
abort_btn => abort
// Then normal operations
1 -> 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 on every data arrival. 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 => next For stage and sequence 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 verify_sensors {}
/* check sensors */
stage pressurize {}
/* bring to pressure */
stage hold {}
/* maintain pressure */
// 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 {
0 -> vent_vlv_cmd
1 -> press_vlv_cmd
stage {
tank_pressure > 500 => next
wait{30s} => abort
}
0 -> press_vlv_cmd
}
// Avoid: single-flow stages that exist only to hold one write
sequence prime {
stage close_vent {
0 -> vent_vlv_cmd
wait{1ms} => next
}
stage open_press {
1 -> press_vlv_cmd
wait{1ms} => next
}
stage wait_pressure {
tank_pressure > 500 => next
wait{30s} => abort
}
stage close_press {
0 -> 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 $= 1
if first {
prev = value
first = 0
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:
authority 200
sequence main {
stage normal {
sensor -> controller{} -> output
emergency_condition => emergency
}
stage emergency {
// Escalate to override all other writers
set_authority{value=255}
0 -> press_vlv_cmd
1 -> 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.
Name Channels Clearly
Use descriptive snake_case names that indicate what the channel represents:
// Good: clear what each channel is
ox_pt_1 // oxidizer pressure transducer 1
fuel_tc_2 // fuel thermocouple 2
press_vlv_cmd // pressurization valve command
// Avoid: ambiguous names
p1, t2, cmd 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 {
0 -> valve_cmd // Writes 0
1 -> valve_cmd // Writes 1 (overwrites 0)
} This is usually a mistake. Use conditional logic instead:
func valve_control(condition u8) u8 {
if condition {
return 1
}
return 0
}
condition -> valve_control{} -> valve_cmd Type 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.0 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 {
some_condition => next // What if this never becomes true?
} Add timeouts:
stage wait_with_timeout {
some_condition => next
wait{duration=30s} => timeout_stage
} Performance Guidelines
Control Loop Rates
The C++ driver runtime supports control loops up to 1kHz. For timing-critical applications:
- Use
interval{period=...}for consistent timing - Keep flow chains short
- Avoid complex calculations in hot paths
// 1kHz control loop
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 config parameter
func process{scale f64} (value f64) f64 {
return value * scale
}
sensor -> process{scale=3.14159} -> output Flow 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{} -> output Debugging
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
debug_error = error // Write to channel for visibility
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