2020-11-16 17:58:14 +01:00
-- lzb.lua
-- Enforced and/or automatic train override control, providing the on_train_approach callback
--[[
Documentation of train.lzb table
train.lzb = {
2021-02-19 14:15:18 +01:00
trav_index = Current index that the traverser has advanced so far
checkpoints = table containing oncoming signals , in order of index
2020-11-16 17:58:14 +01:00
{
pos = position of the point
2021-02-19 14:15:18 +01:00
index = where this is on the path
speed = speed allowed to pass . nil = no effect
callback = function ( pos , id , train , index , speed , lzbdata )
2020-11-16 17:58:14 +01:00
-- Function that determines what to do on the train in the moment it drives over that point.
2021-02-19 14:15:18 +01:00
-- When spd==0, called instead when train has stopped in front
-- nil = no effect
lzbdata = { }
-- Table of custom data filled in by approach callbacks
-- Whenever an approach callback inserts an LZB checkpoint with changed lzbdata,
-- all consecutive approach callbacks will see these passed as lzbdata table.
udata = arbitrary user data , no official way to retrieve ( do not use )
2020-11-16 17:58:14 +01:00
}
2021-02-19 14:15:18 +01:00
trav_lzbdata = currently active lzbdata table at traverser index
2020-11-16 17:58:14 +01:00
}
2021-02-19 14:15:18 +01:00
The LZB subsystem keeps track of " checkpoints " the train will pass in the future , and has two main tasks :
1. run approach callbacks , and run callbacks when passing LZB checkpoints
2. keep track of the permitted speed at checkpoints , and make sure that the train brakes accordingly
To perform 2 , it populates the train.path_speed table which is handled along with the path subsystem .
This table is used in trainlogic.lua / train_step_b ( ) and applied to the velocity calculations .
Note : in contrast to node enter callbacks , which are called when the train passes the .5 index mark , LZB callbacks are executed on passing the .0 index mark !
If an LZB checkpoint has speed 0 , the train will still enter the node ( the enter callback will be called ) , but will stop at the 0.9 index mark ( for details , see SLOW_APPROACH in trainlogic.lua )
The start point for the LZB traverser ( and thus the first node that will receive an approach callback ) is floor ( train.index ) + 1. This means , once the LZB checkpoint callback has fired ,
this path node will not receive any further approach callbacks for the same approach situation
2020-11-16 17:58:14 +01:00
] ]
local params = {
BRAKE_SPACE = 10 ,
AWARE_ZONE = 50 ,
ADD_STAND = 2.5 ,
ADD_SLOW = 1.5 ,
ADD_FAST = 7 ,
ZONE_ROLL = 2 ,
ZONE_HOLD = 5 , -- added on top of ZONE_ROLL
ZONE_VSLOW = 3 , -- When speed is <2, still allow accelerating
DST_FACTOR = 1.5 ,
SHUNT_SPEED_MAX = advtrains.SHUNT_SPEED_MAX ,
}
function advtrains . set_lzb_param ( par , val )
if params [ par ] and tonumber ( val ) then
params [ par ] = tonumber ( val )
else
error ( " Inexistant param or not a number " )
end
end
2021-02-19 14:15:18 +01:00
local function resolve_latest_lzbdata ( ckp , index )
local i = # ckp
local ckpi
while i > 0 do
ckpi = ckp [ i ]
if ckpi.index <= index and ckpi.lzbdata then
return ckpi.lzbdata
end
i = i - 1
end
return { }
end
2020-11-16 17:58:14 +01:00
local function look_ahead ( id , train )
2021-02-19 14:15:18 +01:00
local lzb = train.lzb
if lzb.zero_checkpoint then
-- if the checkpoints list contains a zero checkpoint, don't look ahead
-- in order to not trigger approach callbacks on the wrong path
return
end
2020-11-16 17:58:14 +01:00
local acc = advtrains.get_acceleration ( train , 1 )
2021-02-19 14:15:18 +01:00
-- worst-case: the starting point is maximum speed
local vel = train.max_speed or train.velocity
2020-11-16 17:58:14 +01:00
local brakedst = ( - ( vel * vel ) / ( 2 * acc ) ) * params.DST_FACTOR
2021-02-19 14:15:18 +01:00
--local brake_i = advtrains.path_get_index_by_offset(train, train.index, brakedst + params.BRAKE_SPACE)
-- worst case (don't use index_by_offset)
local brake_i = atfloor ( train.index + brakedst + params.BRAKE_SPACE )
2021-11-16 11:03:22 +01:00
--atprint("LZB: looking ahead up to ", brake_i)
2021-02-19 14:15:18 +01:00
2020-11-16 17:58:14 +01:00
--local aware_i = advtrains.path_get_index_by_offset(train, brake_i, AWARE_ZONE)
2021-02-19 14:15:18 +01:00
local trav = lzb.trav_index
-- retrieve latest lzbdata
if not lzb.trav_lzbdata then
lzb.trav_lzbdata = resolve_latest_lzbdata ( lzb.checkpoints , trav )
end
2020-11-16 17:58:14 +01:00
2021-02-19 14:15:18 +01:00
if lzb.trav_lzbdata . off_track then
--previous position was off track, do not scan any further
end
2020-11-16 17:58:14 +01:00
2021-02-19 14:15:18 +01:00
while trav <= brake_i and not lzb.zero_checkpoint do
2020-11-16 17:58:14 +01:00
local pos = advtrains.path_get ( train , trav )
-- check offtrack
2021-02-19 14:15:18 +01:00
if trav - 1 == train.path_trk_f then
lzb.trav_lzbdata . off_track = true
advtrains.lzb_add_checkpoint ( train , trav - 1 , 0 , nil , lzb.trav_lzbdata )
2020-11-16 17:58:14 +01:00
else
-- run callbacks
-- Note: those callbacks are defined in trainlogic.lua for consistency with the other node callbacks
2021-02-19 14:15:18 +01:00
advtrains.tnc_call_approach_callback ( pos , id , train , trav , lzb.trav_lzbdata )
2020-11-16 17:58:14 +01:00
end
2021-02-19 14:15:18 +01:00
trav = trav + 1
2020-11-16 17:58:14 +01:00
end
2021-02-19 14:15:18 +01:00
lzb.trav_index = trav
2020-11-16 17:58:14 +01:00
end
2021-02-19 14:15:18 +01:00
advtrains.lzb_look_ahead = look_ahead
2020-11-16 17:58:14 +01:00
2021-02-19 14:15:18 +01:00
local function call_runover_callbacks ( id , train )
if not train.lzb then return end
2020-11-16 17:58:14 +01:00
local i = 1
2021-02-19 14:15:18 +01:00
local idx = atfloor ( train.index )
local ckp = train.lzb . checkpoints
while ckp [ i ] do
if ckp [ i ] . index <= idx then
2021-11-16 11:03:22 +01:00
--atprint("LZB: checkpoint run over: i=",ckp[i].index,"s=",ckp[i].speed,"p=",ckp[i].pos)
2021-02-19 14:15:18 +01:00
-- call callback
local it = ckp [ i ]
if it.callback then
it.callback ( it.pos , id , train , it.index , it.speed , train.lzb . lzbdata )
2020-11-16 17:58:14 +01:00
end
2021-02-19 14:15:18 +01:00
-- note: lzbdata is always defined as look_ahead was called before
table.remove ( ckp , i )
2020-11-16 17:58:14 +01:00
else
i = i + 1
end
end
2021-02-19 14:15:18 +01:00
end
-- Flood-fills train.path_speed, based on this checkpoint
local function apply_checkpoint_to_path ( train , checkpoint )
if not checkpoint.speed then
return
end
2021-11-16 11:03:22 +01:00
--atprint("LZB: applying checkpoint: i=",checkpoint.index,"s=",checkpoint.speed,"p=",checkpoint.pos)
2020-11-16 17:58:14 +01:00
2021-02-19 14:15:18 +01:00
if checkpoint.speed == 0 then
train.lzb . zero_checkpoint = true
end
-- make sure path exists until checkpoint
local pos = advtrains.path_get ( train , checkpoint.index )
local brake_accel = advtrains.get_acceleration ( train , 11 )
-- start with the checkpoint index at specified speed
local index = checkpoint.index
local p_speed -- speed in path_speed
local c_speed = checkpoint.speed -- calculated speed at current index
while true do
p_speed = train.path_speed [ index ]
if ( p_speed and p_speed <= c_speed ) or index < train.index then
--we're done. train already slower than wanted at this position
return
2020-11-16 17:58:14 +01:00
end
2021-02-19 14:15:18 +01:00
-- insert calculated target speed
train.path_speed [ index ] = c_speed
-- calculate c_speed at previous index
advtrains.path_get ( train , index - 1 )
local eldist = train.path_dist [ index ] - train.path_dist [ index - 1 ]
-- Calculate the start velocity the train would have if it had a end velocity of c_speed and accelerating with brake_accel, after a distance of eldist:
-- v0² = v1² - 2*a*s
c_speed = math.sqrt ( ( c_speed * c_speed ) - ( 2 * brake_accel * eldist ) )
index = index - 1
2020-11-16 17:58:14 +01:00
end
end
2021-02-19 14:15:18 +01:00
--[[
Distance needed to accelerate from v0 to v1 with constant acceleration a :
v1 - v0 a / v1 - v0 \ 2 v1 ^ 2 - v0 ^ 2
s = v0 * ------- + - * | ------- | = -----------
a 2 \ a / 2 * a
] ]
-- Removes all LZB checkpoints and restarts the traverser at the current train index
function advtrains . lzb_invalidate ( train )
2021-11-16 11:03:22 +01:00
--advtrains.atprint_context_tid = train.id
--atprint("LZB: invalidate")
--advtrains.atprint_context_tid = nil
2020-11-16 17:58:14 +01:00
train.lzb = {
2021-02-19 14:15:18 +01:00
trav_index = atfloor ( train.index ) + 1 ,
checkpoints = { } ,
2020-11-16 17:58:14 +01:00
}
end
2021-02-19 14:15:18 +01:00
-- LZB part of path_invalidate_ahead. Clears all checkpoints that are ahead of start_idx
-- in contrast to path_inv_ahead, doesn't complain if start_idx is behind train.index, clears everything then
function advtrains . lzb_invalidate_ahead ( train , start_idx )
2021-11-16 11:03:22 +01:00
--advtrains.atprint_context_tid = train.id
--atprint("LZB: invalidate ahead i=",start_idx)
2021-02-19 14:15:18 +01:00
if train.lzb then
local idx = atfloor ( start_idx )
2021-11-16 11:03:22 +01:00
--atprint("LZB: invalidate ahead p=",train.path[start_idx])
2021-02-19 14:15:18 +01:00
local i = 1
while train.lzb . checkpoints [ i ] do
if train.lzb . checkpoints [ i ] . index >= idx then
table.remove ( train.lzb . checkpoints , i )
else
i = i + 1
end
end
train.lzb . trav_index = idx
-- FIX reset trav_lzbdata (look_ahead fetches these when required)
train.lzb . trav_lzbdata = nil
-- re-apply all checkpoints to path_speed
train.path_speed = { }
train.lzb . zero_checkpoint = false
for _ , ckp in ipairs ( train.lzb . checkpoints ) do
apply_checkpoint_to_path ( train , ckp )
end
end
2021-11-16 11:03:22 +01:00
--advtrains.atprint_context_tid = nil
2020-11-16 17:58:14 +01:00
end
-- Add LZB control point
2021-02-19 14:15:18 +01:00
-- lzbdata: If you modify lzbdata in an approach callback, you MUST add a checkpoint AND pass the (modified) lzbdata into it.
-- If you DON'T modify lzbdata, you MUST pass nil as lzbdata. Always modify the lzbdata table in place, never overwrite it!
-- udata: user-defined data, do not use externally
function advtrains . lzb_add_checkpoint ( train , index , speed , callback , lzbdata , udata )
2020-11-16 17:58:14 +01:00
local lzb = train.lzb
local pos = advtrains.path_get ( train , index )
2021-02-19 14:15:18 +01:00
local lzbdata_c = nil
if lzbdata then
-- make a shallow copy of lzbdata
lzbdata_c = { }
for k , v in pairs ( lzbdata ) do lzbdata_c [ k ] = v end
end
local ckp = {
2020-11-16 17:58:14 +01:00
pos = pos ,
2021-02-19 14:15:18 +01:00
index = index ,
speed = speed ,
callback = callback ,
lzbdata = lzbdata_c ,
2020-11-16 17:58:14 +01:00
udata = udata ,
2021-02-19 14:15:18 +01:00
}
table.insert ( lzb.checkpoints , ckp )
apply_checkpoint_to_path ( train , ckp )
2020-11-16 17:58:14 +01:00
end
advtrains.te_register_on_new_path ( function ( id , train )
2021-02-19 14:15:18 +01:00
advtrains.lzb_invalidate ( train )
-- Taken care of in pre-move hook (see train_step_b)
--look_ahead(id, train)
end )
advtrains.te_register_on_invalidate_ahead ( function ( id , train , start_idx )
advtrains.lzb_invalidate_ahead ( train , start_idx )
2020-11-16 17:58:14 +01:00
end )
advtrains.te_register_on_update ( function ( id , train )
if not train.path or not train.lzb then
atprint ( " LZB run: no path on train, skip step " )
return
end
2021-02-19 14:15:18 +01:00
-- Note: look_ahead called from train_step_b before applying movement
-- TODO: if more pre-move hooks are added, make a separate callback hook
--look_ahead(id, train)
call_runover_callbacks ( id , train )
2020-11-16 17:58:14 +01:00
end , true )