# Gate
Controls gates and garage doors, either directly or through an external gate controller
Gate
T
O
C
S
PO
IO
IC
SPO
SPC
OFF
TG
OP
CL
M
P
GS
# Inputs
| ID | Abbrev | Name | Type | Default | Description |
|---|---|---|---|---|---|
toggle | T | Toggle | BOOLEAN | false | Single button control. With direct motor control or separate pulses it cycles open, stop, close, stop. With a single control output the pulse is forwarded to the controller which owns the cycle. |
open | O | Open | BOOLEAN | false | Fully opens the gate |
close | C | Close | BOOLEAN | false | Fully closes the gate |
stop | S | Stop | BOOLEAN | false | Stops the movement. Available with direct motor control and the open/stop/close controller mode; ignored in controller modes without a stop function. |
partial_open | PO | Partial open | BOOLEAN | false | Moves the gate to the partial open position (e.g. pedestrian or ventilation position), according to the configured partial open mode |
is_open | IO | Is open | BOOLEAN | false | Limit switch reporting the fully open position. Corrects the simulated position and detects externally triggered movement. |
is_closed | IC | Is closed | BOOLEAN | false | Limit switch reporting the fully closed position. Corrects the simulated position and detects externally triggered movement. |
prevent_open | SPO | Prevent opening | BOOLEAN | false | Safety sensor input. While active, opening is blocked; an ongoing opening movement is stopped if the control mode allows stopping. |
prevent_close | SPC | Prevent closing | BOOLEAN | false | Safety sensor input, e.g. a photocell. While active, closing is blocked; an ongoing closing movement is stopped if the control mode allows stopping. |
off | OFF | Off | BOOLEAN | false | Stops the gate when activated and blocks all commands while active |
# Outputs
| ID | Abbrev | Name | Type | Default | Description |
|---|---|---|---|---|---|
control | TG | Control pulse | BOOLEAN | false | Pulse output for single control input controllers (open/stop/close or open/close cycle) |
open | OP | Open | BOOLEAN | false | With direct motor control: level signal driving the motor in the opening direction. With separate pulses: open pulse for the controller. |
close | CL | Close | BOOLEAN | false | With direct motor control: level signal driving the motor in the closing direction. With separate pulses: close pulse for the controller. |
moving | M | In motion | BOOLEAN | false | Active while the gate is moving |
position | P | Position | NUMBER | 0 | Current gate position as a percentage (0=fully closed, 100=fully open) |
gate_state | GS | Gate state | NUMBER | 0 | Current gate state: 0=closed, 1=opening, 2=open, 3=closing, 4=stopped midway, 5=partially open |
# Configuration
| ID | Name | Type | Default | Unit | Description |
|---|---|---|---|---|---|
gate_type | Gate type | ENUM | 0 | Type of the gate, used for the proper animation in client applications: Garage door, Gate (left wing), Gate (right wing), Double wing gate, Sliding gate (left), Sliding gate (right) Details: Values: Garage door, Gate (left wing), Gate (right wing), Double wing gate, Sliding gate (left), Sliding gate (right) | |
control_mode | Control mode | ENUM | 0 | How the gate is driven: direct motor control (level signals), single pulse output open/stop/close (3F cycle), single pulse output open/close toggle, or separate open and close pulse outputs. Details: Values: Direct motor control, Single control: open/stop/close, Single control: open/close, Separate open and close pulses | |
opening_duration | Opening duration | NUMBER | 30 | s | The time taken to fully open the gate, in seconds. Used to simulate the gate position. Details: > 0 |
closing_duration | Closing duration | NUMBER | 30 | s | The time taken to fully close the gate, in seconds. Used to simulate the gate position. Details: > 0 |
partial_open_position | Partial open position | NUMBER | 20 | % | Target position for the partial open command, as a percentage of fully open (0-100) Details: ≥ 0 ≤ 100 |
open_output_function | Open output function | ENUM | 0 | Controller function wired to the open pulse output: Open (Ot), Close (ZA), Open/Close (OZ), Open/Stop/Close (3F), Partial open (U), Partial open/Close (PC) Details: Values: Open, Close, Open/Close, Open/Stop/Close, Partial open, Partial open/Close Visible when control_mode = Separate open and close pulses | |
close_output_function | Close output function | ENUM | 1 | Controller function wired to the close pulse output: Open (Ot), Close (ZA), Open/Close (OZ), Open/Stop/Close (3F), Partial open (U), Partial open/Close (PC) Details: Values: Open, Close, Open/Close, Open/Stop/Close, Partial open, Partial open/Close Visible when control_mode = Separate open and close pulses | |
pulse_duration | Pulse duration | NUMBER | 0.5 | s | Duration of pulses on the control, open and close outputs, in seconds Details: Visible when control_mode = Single control: open/stop/close, Single control: open/close, Separate open and close pulses> 0 |
pulse_pause | Pulse pause | NUMBER | 0.5 | s | Minimal pause between two consecutive pulses, in seconds Details: Visible when control_mode = Single control: open/stop/close, Single control: open/close, Separate open and close pulses≥ 0 |
motor_lock_duration | Motor lock duration | NUMBER | 0.5 | s | Time the motor needs to reverse direction, in seconds. Protects the motor in direct mode and keeps the estimated position accurate in the other modes. Details: ≥ 0 |
use_sensors | Use limit switch sensors | BOOLEAN | true | When enabled, the is_open and is_closed inputs are the authoritative source for end-of-travel events. Position is still estimated from configured durations for display. Requires sensors to be connected; disable when no sensors are used. |
# State
| ID | Name | Type | Default | Unit | Description |
|---|---|---|---|---|---|
position | Position | NUMBER | 0.0 | % | Raw value of the simulated gate position as a percentage (0-100), not rounded |
moving_dir | Moving direction | NUMBER | 0 | Current movement direction: 0=stopped, 1=opening, -1=closing | |
last_dir | Last direction | NUMBER | -1 | Direction of the last movement: 1=opening, -1=closing | |
target | Target position | NUMBER | -1 | % | Target position for the current movement as a percentage, or -1 when moving to an end position |
partial_pending | Partial open pending | BOOLEAN | false | Indicates that the current movement was started by a partial open command | |
desired_action | Desired action | NUMBER | 0 | Pending action to perform: 0=none, 1=open, 2=close, 3=stop, 4=partial open via controller, 5=raw control pulse | |
cycle_next | Controller cycle position | NUMBER | 0 | Tracks the open/stop/close/stop cycle of an external controller behind the control output: action the next pulse will trigger (0=open, 1=stop, 2=close, 3=stop) | |
cycle_next_op | Controller cycle position (open output) | NUMBER | 0 | Tracks the open/stop/close/stop cycle of a controller channel behind the open pulse output | |
cycle_next_cl | Controller cycle position (close output) | NUMBER | 0 | Tracks the open/stop/close/stop cycle of a controller channel behind the close pulse output | |
target_stop_needed | Stop needed at target | BOOLEAN | false | Indicates that reaching the target position requires sending a stop pulse to the controller | |
partial_after_close | Partial open pending after close | BOOLEAN | false | Indicates that a partial open command is waiting for the gate to reach fully closed before issuing the controller pulse | |
pulse_out | Active pulse output | NUMBER | 0 | Output currently emitting a pulse: 0=none, 1=control, 2=open, 3=close | |
pulse_phase | Pulse phase | NUMBER | 0 | Phase of the pulse machinery: 0=idle, 1=pulse high, 2=pause or motor lock wait | |
pulse_ticks | Pulse ticks | NUMBER | 0 | Ticks remaining in the current pulse phase | |
move_lock_ticks | Movement lock ticks | NUMBER | 0 | Ticks the simulated position is held after a reversal while the motor lock elapses (non-direct modes) | |
tick_active | Tick active | BOOLEAN | false | Indicates whether the periodic simulation callback is scheduled | |
last_movement_ts | Last movement timestamp | NUMBER | -60000 | milliseconds | Timestamp of the last movement, used for the motor lock pause on direction change |
# Source Code
View Volang source
// Gate / garage door controller.
//
// Control modes (config control_mode):
// 0 - Direct motor control: open/close outputs are level signals driving the motor
// 1 - Single control output, controller cycles open/stop/close/stop on each pulse (3F)
// 2 - Single control output, controller toggles open/close on each pulse (no stop)
// 3 - Separate pulse outputs for open and close; the controller function behind each
// output is configured with open_output_function / close_output_function:
// 0 - Open (Ot), 1 - Close (ZA), 2 - Open/Close (OZ), 3 - Open/Stop/Close (3F),
// 4 - Partial open (U), 5 - Partial open/Close (PC)
//
// gate_state output values:
// 0 - closed, 1 - opening, 2 - open, 3 - closing, 4 - stopped midway, 5 - partially open
//
// desired_action state values:
// 0 - none, 1 - open, 2 - close, 3 - stop,
// 4 - partial open through a controller channel (dedicated partial pulse, or a
// close pulse on a closed gate for the Partial open/Close function),
// 5 - raw control pulse (hardware toggle pass-through)
//
// Position is simulated from opening/closing durations on a 100ms tick and corrected
// by the is_open / is_closed limit switch inputs when they are connected.
//
// motor_lock_duration is the time the motor needs to reverse direction. In direct mode it
// keeps the motor off for that time (motor protection); in the non-direct modes the
// controller owns the motor, so the lock instead holds the simulated position for the
// reversal window so the position estimate accounts for the time a reversal takes.
channel = input::channel()
value = input::value()
fn ticks_for(seconds) {
t = math::round((seconds * 1000) / 100)
if (t < 1) {
t = 1
}
return t
}
// Position margin (percent) within which a limit-switch re-assert right after the
// gate departs that limit is treated as contact bounce rather than a real arrival.
// Outside this margin the sensor is always trusted, so remote / external movement
// that drives the gate back to a limit is honored.
fn sensor_bounce_margin() {
return 5
}
fn set_gate_state(s) {
output::set("gate_state", s)
}
fn apply_stopped_state() {
if (state::get("partial_pending")) {
state::set("partial_pending", false)
set_gate_state(5)
return
}
pos = state::get("position")
if (pos >= 100) {
set_gate_state(2)
} else if (pos <= 0) {
set_gate_state(0)
} else {
set_gate_state(4)
}
}
fn set_dir(d) {
prev_dir = state::get("last_dir")
state::set("moving_dir", d)
output::set("moving", d != 0)
if (d != 0) {
// A direction reversal costs the motor lock time regardless of the control mode.
// In direct mode the motor is physically held off for that time (handled in
// process_desired). In the non-direct modes the controller owns the motor, so the
// pulse is sent right away, but the gate still needs that time to reverse - hold the
// simulated position for the remaining lock window so the estimate stays accurate.
if (config::get("control_mode") != 0 and prev_dir != d) {
lock_ms = math::round(config::get("motor_lock_duration") * 1000)
since = time::uptime() - state::get("last_movement_ts")
if (since < lock_ms) {
lt = math::round((lock_ms - since) / 100)
if (lt > 0) {
state::set("move_lock_ticks", lt)
}
}
}
state::set("last_dir", d)
state::set("last_movement_ts", time::uptime())
if (d == 1) {
set_gate_state(1)
} else {
set_gate_state(3)
}
} else {
state::set("move_lock_ticks", 0)
}
}
fn direct_apply(d) {
output::set("open", d == 1)
output::set("close", d == -1)
}
fn pulse_output_name(out_id) {
if (out_id == 1) {
return "control"
}
if (out_id == 2) {
return "open"
}
return "close"
}
fn cycle_state_name(out_id) {
if (out_id == 1) {
return "cycle_next"
}
if (out_id == 2) {
return "cycle_next_op"
}
return "cycle_next_cl"
}
// Returns the pulse output (2=open, 3=close) whose controller function matches f,
// or 0 when none is configured. Used in separate pulses mode only.
fn channel_with_function(f) {
if (config::get("open_output_function") == f) {
return 2
}
if (config::get("close_output_function") == f) {
return 3
}
return 0
}
// Mirror what the external controller does when it receives a pulse
// on a channel configured with the given function
fn apply_channel_function(out_id, f) {
if (f == 0) { // open
set_dir(1)
return
}
if (f == 1) { // close
set_dir(-1)
return
}
if (f == 2) { // open/close toggle
d = state::get("moving_dir")
if (d != 0) {
set_dir(0 - d)
} else if (state::get("last_dir") == 1) {
set_dir(-1)
} else {
set_dir(1)
}
return
}
if (f == 3) { // open/stop/close/stop cycle, tracked per channel
a = state::get(cycle_state_name(out_id))
nxt = a + 1
if (nxt > 3) {
nxt = 0
}
state::set(cycle_state_name(out_id), nxt)
if (a == 0) {
set_dir(1)
} else if (a == 2) {
set_dir(-1)
} else {
set_dir(0)
apply_stopped_state()
}
return
}
if (f == 4) { // partial open - controller moves the gate to its partial position
ppos = config::get("partial_open_position")
pos = state::get("position")
if (pos < ppos) {
state::set("target", ppos)
state::set("partial_pending", true)
set_dir(1)
} else if (pos > ppos) {
state::set("target", ppos)
state::set("partial_pending", true)
set_dir(-1)
} else {
set_gate_state(5)
}
return
}
// f == 5: partial open / close
if (state::get("position") <= 0) {
// controller interprets a pulse on a closed gate as partial open
state::set("target", config::get("partial_open_position"))
state::set("partial_pending", true)
set_dir(1)
} else {
set_dir(-1)
}
}
fn apply_pulse_semantics(out_id) {
mode = config::get("control_mode")
if (mode == 1) { // single control output with open/stop/close cycle
apply_channel_function(1, 3)
return
}
if (mode == 2) { // single control output with open/close toggle
apply_channel_function(1, 2)
return
}
// separate pulses - behaviour depends on the configured channel function
f = config::get("open_output_function")
if (out_id == 3) {
f = config::get("close_output_function")
}
apply_channel_function(out_id, f)
}
fn emit_pulse(out_id) {
output::set(pulse_output_name(out_id), true)
state::set("pulse_out", out_id)
state::set("pulse_phase", 1)
state::set("pulse_ticks", ticks_for(config::get("pulse_duration")))
apply_pulse_semantics(out_id)
}
fn process_desired() {
desired = state::get("desired_action")
if (desired == 0) {
return
}
mode = config::get("control_mode")
d = state::get("moving_dir")
if (desired == 5) { // hardware toggle pass-through
state::set("desired_action", 0)
if (mode == 1 or mode == 2) {
emit_pulse(1)
return
}
ch = channel_with_function(3)
if (ch == 0) {
ch = channel_with_function(2)
}
if (ch != 0) {
emit_pulse(ch)
}
return
}
if (desired == 4) { // partial open through a controller channel
state::set("desired_action", 0)
ch = channel_with_function(4)
if (ch == 0) {
ch = channel_with_function(5)
}
if (ch != 0) {
emit_pulse(ch)
}
return
}
// already satisfied?
if (desired == 1 and (d == 1 or state::get("position") >= 100)) {
state::set("desired_action", 0)
return
}
if (desired == 2 and (d == -1 or state::get("position") <= 0)) {
state::set("desired_action", 0)
return
}
if (desired == 3 and d == 0) {
state::set("desired_action", 0)
return
}
// safety sensors
if (desired == 1 and input::get("prevent_open")) {
state::set("desired_action", 0)
return
}
if (desired == 2 and input::get("prevent_close")) {
state::set("desired_action", 0)
return
}
if (mode == 0) { // direct motor control
if (desired == 3) {
set_dir(0)
direct_apply(0)
apply_stopped_state()
state::set("desired_action", 0)
return
}
new_dir = 1
if (desired == 2) {
new_dir = -1
}
if (d != 0) { // direction change - stop the motor first
set_dir(0)
direct_apply(0)
apply_stopped_state()
}
lock_ms = math::round(config::get("motor_lock_duration") * 1000)
since = time::uptime() - state::get("last_movement_ts")
if (since < lock_ms and state::get("last_dir") != new_dir) {
// wait for the motor lock to release, then retry (desired_action is kept)
lock_ticks = math::round((lock_ms - since) / 100)
if (lock_ticks < 1) {
lock_ticks = 1
}
state::set("pulse_phase", 2)
state::set("pulse_ticks", lock_ticks)
return
}
set_dir(new_dir)
direct_apply(new_dir)
state::set("desired_action", 0)
return
}
if (mode == 1) {
// keep pulsing through the controller cycle until the desired action is applied
emit_pulse(1)
return
}
if (mode == 2) { // open/close toggle - stop is not available
if (desired == 3) {
state::set("desired_action", 0)
return
}
emit_pulse(1)
return
}
// separate pulses - pick a channel that can perform the desired action
if (desired == 3) {
ch = channel_with_function(3) // only an open/stop/close channel can stop
if (ch == 0) {
state::set("desired_action", 0)
return
}
emit_pulse(ch)
return
}
if (desired == 1) {
ch = channel_with_function(0)
if (ch == 0) {
ch = channel_with_function(3)
}
if (ch == 0) {
ch = channel_with_function(2)
}
if (ch == 0) {
state::set("desired_action", 0)
return
}
emit_pulse(ch)
return
}
ch = channel_with_function(1)
if (ch == 0) {
ch = channel_with_function(3)
}
if (ch == 0) {
ch = channel_with_function(2)
}
if (ch == 0) {
ch = channel_with_function(5)
}
if (ch == 0) {
state::set("desired_action", 0)
return
}
emit_pulse(ch)
}
fn ensure_tick() {
if (state::get("tick_active")) {
return
}
if (state::get("pulse_phase") == 0 and state::get("moving_dir") == 0 and state::get("desired_action") == 0) {
return
}
state::set("tick_active", true)
callback::set(99, "onTick") // 99 instead of 100 to leave room for callback scheduling
}
fn command(action) {
if (action != 4) { // any new command cancels a pending partial-open target
state::set("target", -1)
state::set("partial_pending", false)
state::set("target_stop_needed", false)
state::set("partial_after_close", false)
}
state::set("desired_action", action)
if (state::get("pulse_phase") == 0) {
process_desired()
}
ensure_tick()
}
fn reach_open_end() {
state::set("position", 100.0)
output::set("position", 100)
set_dir(0)
if (config::get("control_mode") == 0) {
direct_apply(0)
}
// limit reached - a cycling controller continues with the closing direction
state::set("cycle_next", 2)
state::set("cycle_next_op", 2)
state::set("cycle_next_cl", 2)
state::set("target", -1)
state::set("partial_pending", false)
state::set("target_stop_needed", false)
state::set("partial_after_close", false)
set_gate_state(2)
}
fn reach_closed_end() {
state::set("position", 0.0)
output::set("position", 0)
set_dir(0)
if (config::get("control_mode") == 0) {
direct_apply(0)
}
// limit reached - a cycling controller continues with the opening direction
state::set("cycle_next", 0)
state::set("cycle_next_op", 0)
state::set("cycle_next_cl", 0)
state::set("target", -1)
state::set("partial_pending", false)
state::set("target_stop_needed", false)
set_gate_state(0)
// if a two-phase partial open was waiting for the gate to be fully closed,
// now issue the partial open pulse (PC channel interprets it as partial open)
if (state::get("partial_after_close")) {
state::set("partial_after_close", false)
command(4)
}
}
extern fn onTick() {
// pulse / pause / motor lock phases
phase = state::get("pulse_phase")
if (phase == 1) {
t = state::get("pulse_ticks") - 1
if (t <= 0) {
output::set(pulse_output_name(state::get("pulse_out")), false)
state::set("pulse_out", 0)
state::set("pulse_phase", 2)
state::set("pulse_ticks", ticks_for(config::get("pulse_pause")))
} else {
state::set("pulse_ticks", t)
}
} else if (phase == 2) {
t = state::get("pulse_ticks") - 1
if (t <= 0) {
state::set("pulse_phase", 0)
state::set("pulse_ticks", 0)
process_desired()
} else {
state::set("pulse_ticks", t)
}
}
// movement simulation; position is frozen while a stop pulse is pending
// (the gate is about to be stopped, waiting only for the pulse machinery)
d = state::get("moving_dir")
// reversal dead-time: in the non-direct modes the position is held while the motor
// lock is still elapsing, because the gate cannot have changed direction yet
move_lock = state::get("move_lock_ticks")
// in sensor mode, position does not advance while the departure sensor is still
// asserted: the gate may not have physically left the limit yet (e.g. power
// failure, stalled motor) and the sensor is the authoritative source
sensor_holds_pos = false
if (config::get("use_sensors")) {
if (d == 1 and input::get("is_closed")) {
sensor_holds_pos = true
}
if (d == -1 and input::get("is_open")) {
sensor_holds_pos = true
}
}
if (move_lock > 0 and d != 0) {
state::set("move_lock_ticks", move_lock - 1)
} else if (d != 0 and state::get("desired_action") != 3 and !sensor_holds_pos) {
pos = state::get("position")
if (d == 1) {
pos = pos + (100 * 100) / (config::get("opening_duration") * 1000.0)
} else {
pos = pos - (100 * 100) / (config::get("closing_duration") * 1000.0)
}
if (d == 1 and pos >= 100) {
if (config::get("use_sensors")) {
// clamp for display; actual end-of-travel is confirmed by the is_open sensor
state::set("position", 100.0)
output::set("position", 100)
state::set("last_movement_ts", time::uptime())
} else {
reach_open_end()
}
} else if (d == -1 and pos <= 0) {
if (config::get("use_sensors")) {
// clamp for display; actual end-of-travel is confirmed by the is_closed sensor
state::set("position", 0.0)
output::set("position", 0)
state::set("last_movement_ts", time::uptime())
} else {
reach_closed_end()
}
} else {
target = state::get("target")
arrived = false
if (target >= 0) {
if (d == 1 and pos >= target) {
arrived = true
}
if (d == -1 and pos <= target) {
arrived = true
}
}
if (arrived) {
pos = target
}
state::set("position", pos)
output::set("position", math::round(pos))
state::set("last_movement_ts", time::uptime())
if (arrived) {
state::set("target", -1)
mode = config::get("control_mode")
stop_needed = state::get("target_stop_needed")
state::set("target_stop_needed", false)
if (mode == 1 or (mode == 3 and stop_needed)) {
// stop through a controller pulse;
// the partial state is applied together with the stop pulse
state::set("desired_action", 3)
if (state::get("pulse_phase") == 0) {
process_desired()
}
} else {
// direct stop, or the controller stops on its own (partial open mode)
set_dir(0)
if (mode == 0) {
direct_apply(0)
}
apply_stopped_state()
}
}
}
}
// keep ticking while there is anything to do
if (state::get("pulse_phase") != 0 or state::get("moving_dir") != 0 or state::get("desired_action") != 0) {
callback::set(99, "onTick")
} else {
state::set("tick_active", false)
}
}
// ---- limit switches ----
if (channel == "is_open") {
if (value) {
// The sensor is authoritative (e.g. a remote may have driven the gate here),
// so honor it - except for contact bounce in the instant after the gate leaves
// the open limit: that shows up as a re-assert while closing and still within a
// small position margin of the open limit.
bounce = state::get("moving_dir") == -1 and state::get("position") > 100 - sensor_bounce_margin()
if (!bounce) {
reach_open_end()
}
} else if (state::get("moving_dir") == 0 and state::get("position") >= 100) {
// gate left the open limit without a command - assume external closing
set_dir(-1)
ensure_tick()
}
return
}
if (channel == "is_closed") {
if (value) {
// The sensor is authoritative (e.g. a remote may have driven the gate here),
// so honor it - except for contact bounce in the instant after the gate leaves
// the closed limit: that shows up as a re-assert while opening and still within a
// small position margin of the closed limit.
bounce = state::get("moving_dir") == 1 and state::get("position") < sensor_bounce_margin()
if (!bounce) {
reach_closed_end()
}
} else if (state::get("moving_dir") == 0 and state::get("position") <= 0) {
// gate left the closed limit without a command - assume external opening
set_dir(1)
ensure_tick()
}
return
}
// ---- safety inputs ----
if (channel == "prevent_open") {
if (value and state::get("moving_dir") == 1) {
command(3)
}
return
}
if (channel == "prevent_close") {
if (value and state::get("moving_dir") == -1) {
command(3)
}
return
}
if (channel == "off") {
if (value) {
command(3)
}
return
}
// ---- command inputs - rising edge only ----
if (value == false) {
return
}
if (input::get("off")) {
return
}
if (channel == "open") {
if (input::get("prevent_open")) {
return
}
command(1)
return
}
if (channel == "close") {
if (input::get("prevent_close")) {
return
}
command(2)
return
}
if (channel == "stop") {
command(3)
return
}
if (channel == "toggle") {
mode = config::get("control_mode")
if (mode == 1 or mode == 2) {
// forward directly as a hardware control pulse - the controller owns the cycle
command(5)
return
}
if (mode == 3) {
if (channel_with_function(3) != 0 or channel_with_function(2) != 0) {
// a cycling controller channel owns the open/stop/close sequence
command(5)
return
}
// no cycling channel - reverse while moving, otherwise alternate direction
d = state::get("moving_dir")
towards_close = false
if (d == 1) {
towards_close = true
}
if (d == 0 and state::get("last_dir") == 1) {
towards_close = true
}
if (towards_close) {
if (input::get("prevent_close")) {
return
}
command(2)
} else {
if (input::get("prevent_open")) {
return
}
command(1)
}
return
}
d = state::get("moving_dir")
if (d != 0) {
command(3)
} else if (state::get("last_dir") == 1) {
if (input::get("prevent_close")) {
return
}
command(2)
} else {
if (input::get("prevent_open")) {
return
}
command(1)
}
return
}
if (channel == "partial_open") {
mode = config::get("control_mode")
pos = state::get("position")
ppos = config::get("partial_open_position")
if (mode == 2) {
// open/close toggle controller cannot stop mid-travel
return
}
if (mode == 3) {
if (math::abs(pos - ppos) < 1) {
set_gate_state(5)
return
}
if (channel_with_function(4) != 0) {
// dedicated partial open channel - the controller handles it
command(4)
return
}
if (channel_with_function(5) != 0) {
if (pos <= 0 or input::get("is_closed")) {
// at closed position (by estimate or sensor);
// correct position estimate if sensor disagrees with it
if (pos > 0 and input::get("is_closed")) {
state::set("position", 0.0)
output::set("position", 0)
}
// the PC pulse on a closed gate is interpreted as partial open
command(4)
} else {
// close fully first; reach_closed_end will fire command(4) automatically
if (input::get("prevent_close")) {
return
}
command(2) // clears all flags, so partial_after_close must be set afterwards
state::set("partial_after_close", true)
}
return
}
if (channel_with_function(3) != 0) {
// stop at position using the open/stop/close channel
if (pos < ppos) {
if (input::get("prevent_open")) {
return
}
state::set("desired_action", 1)
} else {
if (input::get("prevent_close")) {
return
}
state::set("desired_action", 2)
}
state::set("target", ppos)
state::set("partial_pending", true)
state::set("target_stop_needed", true)
if (state::get("pulse_phase") == 0) {
process_desired()
}
ensure_tick()
}
return
}
if (math::abs(pos - ppos) < 1) {
set_gate_state(5)
return
}
if (pos < ppos) {
if (input::get("prevent_open")) {
return
}
state::set("desired_action", 1)
} else {
if (input::get("prevent_close")) {
return
}
state::set("desired_action", 2)
}
state::set("target", ppos)
state::set("partial_pending", true)
if (state::get("pulse_phase") == 0) {
process_desired()
}
ensure_tick()
return
}
