Find the signal. See its state.
Search live channels and inspect values, units, freshness, and command authority in one place. Browse by module, task, or hardware configuration to adjust device settings and channel mappings.
Explore Channel SearchSoftware for live hardware
Acquire telemetry, command hardware, and automate test sequences in one connected workspace.
Search live channels and inspect values, units, freshness, and command authority in one place. Browse by module, task, or hardware configuration to adjust device settings and channel mappings.
Explore Channel SearchBring channels, plots, and interactive schematics together. Inspect live values and issue commands with the system in context.
Explore schematicsSequences, sensor calculations, events, and reusable controls—written in DCode.
Explore DCodeFill the run tank from supply, wait two seconds for ignition, then open the engine feed valves. Each stage checks the abort before commanding outputs.
task_timeline rocket_test 20:
clock:
start: T-2m
at T-2m (setup):
hold_at: next_bucket
execute:
test_name = promptString(
"Engine test recording",
"Name the data frame for this engine test.",
[],
"Enter a test name.",
0,
0
)
test_data_frame = startDataFrame(test_name, [
"rocket_sim_run_fill", "rocket_sim_run_fill_target", "rocket_sim_run_tank_pressure",
"rocket_sim_fill_position", "rocket_sim_vent_position", "rocket_sim_phase",
"rocket_sim_temperature_voted", "rocket_sim_pressure", "rocket_sim_flow"
])
at T-1m (tank_fill):
hold_at: next_bucket
execute:
wait_until(|rocket_sim_sensor_valid| == 1 and |rocket_sim_abort_active| == 0, 0, 15)
|rocket_sim_phase| = 1
|engine_tank_fill_complete| = 0
command |engine_tank_fill_running| = 1
command |engine_tank_fill_target_state| = 1
wait_until(|engine_tank_fill_complete| == 1, 0)
command |engine_tank_fill_target_state| = 0
wait_until(|engine_tank_fill_current_state| == 0, 0)
free |engine_tank_fill_target_state|
free |engine_tank_fill_running|
at T+0s (ignition):
hold_at: next_bucket
execute:
wait_until(|engine_tank_fill_complete| == 1, 0)
wait_until(|rocket_sim_sensor_valid| == 1 and |rocket_sim_abort_active| == 0, 0, 15)
|rocket_sim_phase| = 2
|engine_ignition_complete| = 0
command |engine_ignition_running| = 1
wait_until(|engine_ignition_complete| == 1, 0, 60)
|rocket_sim_phase| = 0
log("Engine test", "Test fire complete")
at T+15s (cleanup):
execute:
stopDataFrame(test_data_frame)
test_data_frame = nil
log("Engine test", "Recording saved")
abort:
if test_data_frame:
stopDataFrame(test_data_frame)
test_data_frame = nil
command |engine_tank_fill_target_state| = 0
command |engine_ignition_running| = 0
|rocket_sim_phase| = 0
stop:
if test_data_frame:
stopDataFrame(test_data_frame)
test_data_frame = nil
command |engine_tank_fill_target_state| = 0
command |engine_ignition_running| = 0
|rocket_sim_phase| = 0 # The periodic ox-tank task supplies tank values. This state machine controls
# the fill and vent requests; the device read task supplies applied positions.
task_state_machine engine_tank_fill:
start:
|engine_tank_fill_complete| = 0
states:
isolate
fill
vent
manual
state isolate:
if |rocket_sim_abort_active| == 0:
command |rocket_sim_fill_request| = 0
command |rocket_sim_vent_request| = 0
if target_state == states.fill:
|engine_tank_fill_complete| = 0
fill_target = promptNumerical(
"Ox tank fill target",
"Enter the target fill percentage for this engine test.",
["rocket_sim_run_fill_target"],
"Enter a percentage from 1 to 100.",
0,
0
)
if fill_target <= 0 or fill_target > 100:
error("Ox tank fill target must be between 1 and 100 percent.")
|rocket_sim_run_fill_target| = fill_target
transition fill
if target_state == states.manual:
transition manual
state fill:
if |rocket_sim_abort_active| != 0 or |rocket_sim_sensor_valid| != 1 or target_state == states.isolate:
transition isolate
elif target_state == states.manual:
transition manual
elif |rocket_sim_run_fill| >= |rocket_sim_run_fill_target|:
|engine_tank_fill_complete| = 1
transition isolate
elif |rocket_sim_run_tank_pressure| >= |rocket_sim_supply_tank_pressure| * 0.9:
transition vent
else:
command |rocket_sim_vent_request| = 0
command |rocket_sim_fill_request| = 1
state vent:
if |rocket_sim_abort_active| != 0 or |rocket_sim_sensor_valid| != 1 or target_state == states.isolate:
transition isolate
elif target_state == states.manual:
transition manual
elif |rocket_sim_run_fill| >= |rocket_sim_run_fill_target|:
|engine_tank_fill_complete| = 1
transition isolate
elif |rocket_sim_run_tank_pressure| <= 500:
transition fill
else:
command |rocket_sim_fill_request| = 0
command |rocket_sim_vent_request| = 1
state manual:
once:
free |rocket_sim_fill_request|
free |rocket_sim_vent_request|
if target_state == states.isolate:
transition isolate
if target_state == states.fill:
transition fill
end:
if |rocket_sim_abort_active| == 0:
command |rocket_sim_fill_request| = 0
command |rocket_sim_vent_request| = 0
free |rocket_sim_fill_request|
free |rocket_sim_vent_request| task_sequence engine_ignition:
start:
|engine_ignition_complete| = 0
wait_until(|rocket_sim_abort_active| == 0 and |rocket_sim_sensor_valid| == 1, 0, 30)
wait_until(|rocket_sim_abort_active| == 0 and |rocket_sim_sensor_valid| == 1, 0, 30)
log("Engine test", "Ignition: energizing the simulated igniter")
command |rocket_sim_igniter_command| = 1
wait(2)
wait_until(|rocket_sim_abort_active| == 0 and |rocket_sim_sensor_valid| == 1, 0, 30)
log("Engine test", "Firing: opening the simulated feed valves")
|rocket_sim_phase| = 3
command |rocket_sim_fuel_request| = 1
command |rocket_sim_oxidizer_request| = 1
wait(8)
|engine_ignition_complete| = 1
end:
command |rocket_sim_fuel_request| = 0
command |rocket_sim_oxidizer_request| = 0
command |rocket_sim_igniter_command| = 0 Derive a voted temperature as readings arrive. Dependent calculations run synchronously after the input commits, before the originating write returns—no polling loop or UI round trip.
# Median voting tolerates one biased input; validity is checked separately.
channel_calculation rocket_sim_temperature_voted:
a = |rocket_sim_temperature_1|
b = |rocket_sim_temperature_2|
c = |rocket_sim_temperature_3|
return a + b + c - math.min(a, b, c) - math.max(a, b, c)
channel_calculation rocket_sim_temperature_spread:
return math.max(
|rocket_sim_temperature_1|,
|rocket_sim_temperature_2|,
|rocket_sim_temperature_3|
) - math.min(
|rocket_sim_temperature_1|,
|rocket_sim_temperature_2|,
|rocket_sim_temperature_3|
) Convert transmitter current into engineering units in the same synchronous calculation chain. Scale first, then feed the result straight into derived channels and event conditions.
# The driver reads current; this calculation supplies engineering units.
channel_calculation rocket_sim_pressure:
return (|rocket_sim_pressure_current_ma| - 4) * 100 / 16 Evaluate the limit on dependency changes and apply explicit automatic overrides from on_firing, synchronously before the event record is built. The reaction frees them when firing stops or the event leaves the board. Device response still depends on the driver and hardware.
event rocket_engine_abort:
title: "Mock rocket automatic abort"
type: abort
description: "Voted temperature exceeds 1500 K, or the simulated sensor input is invalid."
resolution: "Stop the test, clear the injected fault, and verify the outputs before starting a new run."
system: "MOCK ROCKET"
subsystem: "AUTOMATION"
separation_seconds: 0
channels: |rocket_sim_temperature_voted|, >1500; |rocket_sim_sensor_valid|
trigger:
if |rocket_sim_temperature_voted| > 1500 or |rocket_sim_sensor_valid| != 1:
return 1
on_firing:
if value:
|rocket_sim_abort_active| = 1
override |rocket_sim_fuel_request| = 0
override |rocket_sim_oxidizer_request| = 0
override |rocket_sim_fill_request| = 0
override |rocket_sim_igniter_command| = 0
log("Rocket sequence", "Abort asserted: closing feed valves and disabling igniter")
else:
free |rocket_sim_fuel_request|
free |rocket_sim_oxidizer_request|
free |rocket_sim_fill_request|
free |rocket_sim_igniter_command|
|rocket_sim_abort_active| = 0
log("Rocket sequence", "Abort cleared: released overrides; verify state before restarting") Generate a loop per valve from YAML. When its request changes, pulse the open or close coil for half a second, then record the completed command.
params = yaml "./valves.yaml#valves"
template(params) loop rocket_valve_{params.valve} 10 disable_warnings:
once when true:
state.last_value = null
value = |rocket_sim_{params.valve}_request|
if value == state.last_value:
return
state.last_value = value
if value == 1:
command |rocket_sim_{params.valve}_open_coil| = 1
wait(0.5)
command |rocket_sim_{params.valve}_open_coil| = 0
set |rocket_sim_{params.valve}_state| = 1
elif value == 0:
command |rocket_sim_{params.valve}_close_coil| = 1
wait(0.5)
command |rocket_sim_{params.valve}_close_coil| = 0
set |rocket_sim_{params.valve}_state| = 0 valves:
- valve: fuel
- valve: oxidizer
- valve: fill
- valve: vent Generate warning events from YAML limits. Treat invalid or old readings as unavailable, and log when each condition fires and clears.
params = yaml "./sensors.yaml#sensors"
template(params) event rocket_warning_{params.name}:
title: "{params.title} warning"
type: warning
description: "The reading is unavailable or outside the example limits."
resolution: "Inspect the graph and injected faults; clear the fault, then acknowledge the occurrence."
system: "MOCK ROCKET"
subsystem: "SENSORS"
separation_seconds: 0
channels: |{params.channel}|, >{params.high}; |rocket_sim_sensor_valid|, |rocket_sim_sample_age_s|
trigger:
if |rocket_sim_sensor_valid| != 1 or |rocket_sim_sample_age_s| > 0.3:
return 1
if |{params.channel}| < {params.low} or |{params.channel}| > {params.high}:
return 1
on_firing:
if value:
log("Rocket warnings", "{params.title}: warning asserted")
else:
log("Rocket warnings", "{params.title}: condition cleared") sensors:
- name: temperature_1
channel: rocket_sim_temperature_1
title: Temperature sensor 1
low: 250
high: 1500
- name: temperature_2
channel: rocket_sim_temperature_2
title: Temperature sensor 2
low: 250
high: 1500
- name: temperature_3
channel: rocket_sim_temperature_3
title: Temperature sensor 3
low: 250
high: 1500
- name: pressure
channel: rocket_sim_pressure
title: Chamber pressure
low: -1
high: 80
- name: disagreement
channel: rocket_sim_temperature_spread
title: Temperature disagreement
low: 0
high: 100 Review warnings, errors, and operator prompts in the ARGUS events board. Open an event to understand its source, related channels, and available actions before responding.
Explore ARGUS EventsSave each test as a named DataFrame, keeping selected channel recordings, events, and logs together over the run's time range. Reopen a test to plot its signals, understand what happened, and export the data for analysis.
Review and export dataBuilt to be extended
Write native device drivers in C++ or extend the interface with new tools. Plugins let you build into DARTWIC, from the engine backend to the operator's workspace.
Explore plugin developmentC++ / Engine plugins
Your C++ plugin runs natively in the engine backend and talks directly to your device or its SDK. No separate client, network bridge, or extra network hop between your driver and the engine—just your device's own connection.
React / Interface plugins
Add new functionality directly to the DARTWIC interface: custom resource pages, task editors, settings panels, and schematic nodes. Build the tools your operators need into their existing workspace.
Connect to a Modbus TCP server and bring your PLC or networked device into the workspace.
Start with the existing client plugin, then adapt it to your device's register map.
Explore the plugin
Connect a LabJack T7 for a hardware-backed path from physical signals to your DARTWIC workspace.
Use the T7 plugin as a starting point for your acquisition setup.
Explore the pluginConnect an EtherCAT device chain and exchange process inputs and outputs through a native engine plugin.
Discover devices, map their process data to channels, and configure a cyclic I/O task.
Explore the pluginNames and marks identify the respective technologies; no endorsement is implied.
In use
Discover devices, configure channels and tasks, use channels directly in DCode, and build an operator interface around them.
Remote nodes
Connect DARTWIC nodes to share channels and events across your system. See how your interface, engine, and remote nodes connect in the Network Map.
Explore the Network MapNode to node
Link DARTWIC engines and bring remote telemetry and ARGUS events into your operator workspace. Keep the originating node in context as you work across the system.
Connect remote nodesYour system, connected
Write a custom TEMPEST Peer transport to exchange channels and events with a separate system—like a flight computer. Map its protocol into DARTWIC and back, exposing the data and actions your integration supports.
Explore the flight exampleBeyond the interface
Plugins bring hardware into the engine. Clients bring the engine into your tools—whether that's a Python script for analysis or a React dashboard built for your team.
Find channels, query recorded ranges, and call engine operations from your own scripts. Spend your time on the analysis, not rebuilding the connection.
from dartwic_client import DartwicClient
client = DartwicClient()
client.connect(ip="127.0.0.1", port=7000)
keys = client.get_channel_keys(query="rpm")
client.disconnect() Bring live telemetry and engine operations into a custom React app. Use a provider and hooks to build the views and controls your workflow needs.
// Wrap your app
<DartwicProvider>
<YourDashboard />
</DartwicProvider>
// Inside YourDashboard
const { channels, connect } = useDartwic(); Start building
Try a first session, connect your own hardware, or build a custom integration.
DARTWIC 2.1 brings together the changes made since DARTWIC 2.0: a simpler way to build connected applications, shared configuration editors, improvements to DCode and channel ownership, and a lot of smaller fixes across the Interface.
DARTWIC 2.0 expands the platform from a single-node acquisition and control workspace into a system for distributed operation, more capable automation, and recorded evidence of what happened.
I just released the DAQ and control software I've been building for two years. Here's what it can do, with numbers from the stress tests.