1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
|
-- lzb.lua
-- Enforced and/or automatic train override control, providing the on_train_approach callback
--[[
Documentation of train.lzb table
train.lzb = {
trav_index = Current index that the traverser has advanced so far
checkpoints = table containing oncoming signals, in order of index
{
pos = position of the point
index = where this is on the path
speed = speed allowed to pass. nil = no effect
callback = function(pos, id, train, index, speed, lzbdata)
-- Function that determines what to do on the train in the moment it drives over that point.
-- 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)
}
trav_lzbdata = currently active lzbdata table at traverser index
}
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: As of 2024-11-25, node enter callbacks as well as LZB callbacks are executed on passing the .0 index mark!
If an LZB checkpoint has speed 0, it will stop at the 0.9 index mark (for details, see SLOW_APPROACH in trainlogic.lua) and will not enter the node.
(previous behavior: node enter callback was called at .5 index mark)
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
]]
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 = 3,--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
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
local function look_ahead(id, train)
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
local acc = advtrains.get_acceleration(train, 1)
-- worst-case: the starting point is maximum speed
local vel = train.max_speed or train.velocity
local brakedst = ( -(vel*vel) / (2*acc) ) * params.DST_FACTOR
--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)
--atprint("LZB: looking ahead up to ", brake_i)
--local aware_i = advtrains.path_get_index_by_offset(train, brake_i, AWARE_ZONE)
local trav = lzb.trav_index
-- retrieve latest lzbdata
if not lzb.trav_lzbdata then
lzb.trav_lzbdata = resolve_latest_lzbdata(lzb.checkpoints, trav)
end
if lzb.trav_lzbdata.off_track then
--previous position was off track, do not scan any further
end
while trav <= brake_i and not lzb.zero_checkpoint do
local pos = advtrains.path_get(train, trav)
-- check offtrack
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)
else
-- run callbacks
-- Note: those callbacks are defined in trainlogic.lua for consistency with the other node callbacks
advtrains.tnc_call_approach_callback(pos, id, train, trav, lzb.trav_lzbdata)
end
trav = trav + 1
end
lzb.trav_index = trav
end
advtrains.lzb_look_ahead = look_ahead
local function call_runover_callbacks(id, train)
if not train.lzb then return end
local i = 1
local idx = atfloor(train.index)
local ckp = train.lzb.checkpoints
while ckp[i] do
if ckp[i].index <= idx then
--atprint("LZB: checkpoint run over: i=",ckp[i].index,"s=",ckp[i].speed,"p=",ckp[i].pos)
-- call callback
local it = ckp[i]
if it.callback then
it.callback(it.pos, id, train, it.index, it.speed, train.lzb.lzbdata)
end
-- note: lzbdata is always defined as look_ahead was called before
table.remove(ckp, i)
else
i = i + 1
end
end
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
--atprint("LZB: applying checkpoint: i=",checkpoint.index,"s=",checkpoint.speed,"p=",checkpoint.pos)
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
end
-- 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
end
end
--[[
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)
--advtrains.atprint_context_tid = train.id
--atprint("LZB: invalidate")
--advtrains.atprint_context_tid = nil
train.lzb = {
trav_index = atfloor(train.index) + 1,
checkpoints = {},
}
end
-- 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)
--advtrains.atprint_context_tid = train.id
--atprint("LZB: invalidate ahead i=",start_idx)
if train.lzb then
local idx = atfloor(start_idx)
--atprint("LZB: invalidate ahead p=",train.path[start_idx])
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
--advtrains.atprint_context_tid = nil
end
-- Add LZB control point
-- 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)
local lzb = train.lzb
local pos = advtrains.path_get(train, index)
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 = {
pos = pos,
index = index,
speed = speed,
callback = callback,
lzbdata = lzbdata_c,
udata = udata,
}
table.insert(lzb.checkpoints, ckp)
apply_checkpoint_to_path(train, ckp)
end
advtrains.te_register_on_new_path(function(id, train)
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)
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
-- 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)
end, true)
|