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 one-shot 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 (->) run every cycle. One-shot edges (=>) fire once and stop until
the stage is re-entered.
// Streaming: continuously process sensor data
sensor -> filter{} -> output
// Transition: fire once when condition becomes true
pressure > 500 => next Common mistake: using -> for a transition and wondering why it keeps firing.
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
} 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
}
// Option 2: Accept that first sample is special
// Document this behavior and handle it downstream 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 every cycle, not just once
pressure > 500 -> next // Syntax error anyway, but shows intent
// Right: fires once when condition becomes true
pressure > 500 => next Forgetting That => Resets on Stage Re-Entry
One-shot transitions reset when you re-enter a stage. If you loop back:
stage retry {
attempt_operation{},
operation_failed => retry, // Goes back to this stage
operation_succeeded => next // This resets when re-entering
} Each time the stage is entered, all => transitions can fire again.
Expecting Loops
Arc has no loops. Use stateful variables with reactive execution:
// Wrong: trying to loop
// for i := 0; i < 10; i++ { ... }
// Right: stateful counter triggered by interval
func counter() i64 {
count $= 0
count = count + 1
return count
}
interval{period=100ms} -> counter{} -> count_output 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)
}
// Result: valve_cmd receives 1 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