Smart intersection
Two Traffic Lights and two Inductive Loop Detectors run by a state machine that serves the waiting road, with a minimum green, amber and red-amber phases, and flashing amber at night.
A crossroads in your town: the main road carries the trains of carts and the villagers' traffic, a side road comes from the farms. A fixed cycle stops the main road every thirty seconds even when nobody is on the side road. This project does what real controllers do: the main road keeps the green, and an Inductive Loop Detector in the side road calls for the green only when something stands over it. Each change goes through amber, a moment of red on both sides to clear the junction, and red and amber together before the green, like British and German lights. At night both lights flash amber.
The program is a state machine: a variable holds the current phase, and a function called four times a second decides whether it is time to move to the next one. It is the clearest way to write anything that goes through steps: lights, a train station, a lift, a sequence of machines.
What you need
- A Microcontroller (or any computer from the Minicomputer on): it is small, and its Data Cable port reaches every block of the junction. Its screen is 40 x 12; the program draws the two lights on it. It is driven from below.
- Two Traffic Lights, one facing each road (or more: each road takes a list of lights). A light set by a computer stays under its control and ignores redstone until
traffic_light.release()(Traffic Light). - Two Inductive Loop Detectors, set into the road a few blocks before the junction. A loop sees mobs, players, minecarts, boats and Create Aeronautics vehicles up to two blocks above it (
Inductive Loop Detector). - Data Cable under the road, touching the Microcontroller, both lights and both loops. Each block on the cable is named after its type and position:
traffic_light@10,65,4,inductive_loop@10,63,9.
Seen from above:
| main |
| road |
| L1 | L1, L2: Inductive Loop Detectors in the road,
| | a few blocks before the junction
-----------+ +-----------
side L2 T1 T1: Traffic Light for the main road
road T2 T2: Traffic Light for the side road
-----------+ +-----------
| | [M] M: the Microcontroller at the corner
| |
a Data Cable under the road joins M, T1, T2, L1 and L2The program, step by step
1. The roads and the timings
Each road has a name, a list of lights and a list of loops. The first road is the main road: it gets the green back when nobody waits anywhere.
local ROADS = {
{name = "Main road", lights = {"traffic_light@10,65,4"}, loops = {"inductive_loop@10,63,9"}},
{name = "Side road", lights = {"traffic_light@14,65,0"}, loops = {"inductive_loop@19,63,0"}},
}
local MIN_GREEN = 8 -- seconds of green before the light may change
local MAX_GREEN = 30 -- seconds of green at most while the other road waits
local AMBER = 3 -- seconds of amber
local ALL_RED = 1 -- seconds with both roads red, to clear the junction
local RED_AMBER = 1.5 -- seconds of red and amber before the green
local NIGHT_START = 13000 -- os.day_time() at nightfall (7 pm)
local NIGHT_END = 23000 -- os.day_time() at dawn (5 am)
local TICK = 0.25 -- seconds between two looks at the loopsMIN_GREEN keeps a light from changing again right after it turned green; MAX_GREEN makes sure a busy main road cannot keep a waiting side road on red forever.
To find the names of your blocks, run the program with list after its name: it prints them and stops. The devices example (devices) also shows them, on a map.
if arg ~= nil and arg[1] == "list" then
for _, kind in ipairs({"traffic_light", "inductive_loop"}) do
for _, name in ipairs(peripheral.list(kind)) do print(name) end
end
return
end2. Talking to the blocks
The program never wraps the blocks once and for all. It calls them by name with peripheral.call, through pcall, so that a light broken by a creeper or a loop in an unloaded chunk does not stop the whole junction: the name goes into broken, and the screen reports it.
local aspects = {"off", "off"} -- what each road shows
local counts = {0, 0} -- what stands over the loops of each road
local waiting = {false, false} -- true when something waits on a road
local broken = {} -- names of blocks that did not answer
local function set_road(i, aspect)
aspects[i] = aspect
for _, name in ipairs(ROADS[i].lights) do
local r = pcall(peripheral.call, name, "set", aspect)
if r.ok then broken[name] = nil else broken[name] = true end
end
end
local function read_loops(i)
local total, all_ok = 0, true
for _, name in ipairs(ROADS[i].loops) do
local r = pcall(peripheral.call, name, "count")
if r.ok then
total = total + r.value
broken[name] = nil
else
broken[name] = true
all_ok = false
end
end
counts[i] = total
waiting[i] = total > 0 or not all_ok
endA loop that does not answer counts as a car waiting. That is the safe choice: the road keeps getting its turn on a regular cycle, instead of never getting the green again because its detector is broken.
The aspects a Traffic Light understands are "off", "red", "red_amber", "amber", "green" and "flashing" (flashing amber). Any other word stops the call with bad aspect 'yellow' (off, red, red_amber, amber, green, flashing).
3. The state machine
The state is three variables: the phase, the road that has (or is about to get) the green, and the moment the phase began, read from os.clock() (seconds since the computer started). enter moves to a phase and notes the time:
local phase = "green" -- "green", "amber", "all_red", "red_amber" or "night"
local go = 1 -- the road that has, or is about to get, the green
local since = 0 -- os.clock() when the phase began
local forced = false -- flashing forced by hand (the f key)
local function enter(new_phase)
phase = new_phase
since = os.clock()
end
local function is_night()
local t = os.day_time()
return forced or (t >= NIGHT_START and t < NIGHT_END)
endstep runs four times a second. It reads the loops, then looks at the current phase:
| phase | lights of road go | moves on when | to |
|---|---|---|---|
green | green | the other road waits and go is empty (or MAX_GREEN passed), after MIN_GREEN | amber |
amber | amber | AMBER seconds passed | all_red |
all_red | red | ALL_RED seconds passed (the other road becomes go) | red_amber |
red_amber | red and amber | RED_AMBER seconds passed | green |
night | flashing | the night ends | all_red |
local function step()
read_loops(1)
read_loops(2)
local other = 3 - go
local elapsed = os.clock() - since
if is_night() then
if phase ~= "night" then
set_road(1, "flashing")
set_road(2, "flashing")
enter("night")
end
elseif phase == "night" then
set_road(1, "red")
set_road(2, "red")
go = 2 -- so that the main road gets the next green
enter("all_red")
elseif phase == "green" then
local change = false
if elapsed >= MIN_GREEN then
if waiting[other] and (not waiting[go] or elapsed >= MAX_GREEN) then change = true end
if go ~= 1 and not waiting[go] then change = true end -- back to the main road
end
if change then
set_road(go, "amber")
enter("amber")
end
elseif phase == "amber" and elapsed >= AMBER then
set_road(go, "red")
enter("all_red")
elseif phase == "all_red" and elapsed >= ALL_RED then
go = other
set_road(go, "red_amber")
enter("red_amber")
elseif phase == "red_amber" and elapsed >= RED_AMBER then
set_road(go, "green")
enter("green")
end
end3 - go is the other road: 2 when go is 1, 1 when go is 2. The lights only change when the phase changes, so step costs almost nothing the rest of the time.
4. The screen
The left half of the screen lists the roads with their aspect and their loop; the right half draws the two light heads with gfx: a grey body and three round lamps, lit or black. In flashing mode the amber lamp follows os.clock(), on for half a second, off for half a second.
local ASPECT_TEXT = {off = "OFF", red = "RED", red_amber = "RED+AMBER", amber = "AMBER",
green = "GREEN", flashing = "FLASHING"}
local ASPECT_COLORS = {off = "gray", red = "red", red_amber = "orange", amber = "orange",
green = "lime", flashing = "orange"}
local PHASE_TEXT = {green = "green", amber = "amber", all_red = "all red",
red_amber = "red and amber", night = "night, flashing"}
local function clock_text()
local t = (os.day_time() + 6000) % 24000 -- 0 is 6 am
return string.format("%02d:%02d", t // 1000, t % 1000 * 60 // 1000)
end
local function lamp(x, y, lit, color)
if lit then gfx.circle(x, y, 8, color, true) else gfx.circle(x, y, 8, "black", true) end
end
local function draw_head(x, aspect, label)
local blink = math.floor(os.clock() * 2) % 2 == 0
gfx.rect(x, 14, 26, 62, "gray", true)
lamp(x + 13, 25, aspect == "red" or aspect == "red_amber", "red")
lamp(x + 13, 45, aspect == "amber" or aspect == "red_amber" or (aspect == "flashing" and blink), "orange")
lamp(x + 13, 65, aspect == "green", "lime")
gfx.text(x + 11, 81, label, "light_gray")
end
local function draw()
local w = term.get_size().w
term.set_bg(term.colors.black)
term.clear()
gfx.clear()
term.set_cursor(1, 1)
term.set_bg(term.colors.blue)
term.set_fg(term.colors.white)
term.write(string.format(" %-" .. (w - 8) .. "s%6s ", "INTERSECTION", clock_text()))
term.set_bg(term.colors.black)
for i, road in ipairs(ROADS) do
term.set_cursor(2, i * 3)
term.set_fg(term.colors.white)
term.write(i .. " " .. road.name)
term.set_cursor(4, i * 3 + 1)
term.set_fg(term.colors[ASPECT_COLORS[aspects[i]]])
term.write(string.format("%-10s", ASPECT_TEXT[aspects[i]]))
term.set_fg(term.colors.light_gray)
term.write("loop: " .. counts[i])
draw_head(w * 6 - 84 + (i - 1) * 40, aspects[i], tostring(i))
end
term.set_cursor(2, 9)
term.set_fg(term.colors.light_gray)
term.write(string.format("%s, %d s", PHASE_TEXT[phase], math.floor(os.clock() - since)))
local missing = 0
for _, _ in pairs(broken) do missing = missing + 1 end
if missing > 0 then
term.set_cursor(2, 10)
term.set_fg(term.colors.red)
term.write(missing .. " block(s) not answering")
end
term.set_cursor(2, 11)
term.set_fg(term.colors.gray)
term.write("f: flashing on/off")
end5. Starting and the main loop
At start the main road gets the green and the side road the red. The loop waits for its timer, and for the f key, which forces the flashing mode by hand: handy during roadworks.
set_road(1, "green")
set_road(2, "red")
enter("green")
local timer = os.start_timer(TICK)
while true do
local e = os.pull_event()
if e.name == "timer" and e.id == timer then
step()
draw()
timer = os.start_timer(TICK)
elseif e.name == "char" and e.char == "f" then
forced = not forced
end
endThe whole program
-- Smart intersection: two roads, each with its Traffic Lights and its
-- Inductive Loop Detectors. The main road keeps the green until something
-- waits on the side road; at night, every light flashes amber.
-- "startup list" prints the names of the lights and loops on the cable.
local ROADS = {
{name = "Main road", lights = {"traffic_light@10,65,4"}, loops = {"inductive_loop@10,63,9"}},
{name = "Side road", lights = {"traffic_light@14,65,0"}, loops = {"inductive_loop@19,63,0"}},
}
local MIN_GREEN = 8 -- seconds of green before the light may change
local MAX_GREEN = 30 -- seconds of green at most while the other road waits
local AMBER = 3 -- seconds of amber
local ALL_RED = 1 -- seconds with both roads red, to clear the junction
local RED_AMBER = 1.5 -- seconds of red and amber before the green
local NIGHT_START = 13000 -- os.day_time() at nightfall (7 pm)
local NIGHT_END = 23000 -- os.day_time() at dawn (5 am)
local TICK = 0.25 -- seconds between two looks at the loops
if arg ~= nil and arg[1] == "list" then
for _, kind in ipairs({"traffic_light", "inductive_loop"}) do
for _, name in ipairs(peripheral.list(kind)) do print(name) end
end
return
end
local ASPECT_TEXT = {off = "OFF", red = "RED", red_amber = "RED+AMBER", amber = "AMBER",
green = "GREEN", flashing = "FLASHING"}
local ASPECT_COLORS = {off = "gray", red = "red", red_amber = "orange", amber = "orange",
green = "lime", flashing = "orange"}
local PHASE_TEXT = {green = "green", amber = "amber", all_red = "all red",
red_amber = "red and amber", night = "night, flashing"}
local aspects = {"off", "off"} -- what each road shows
local counts = {0, 0} -- what stands over the loops of each road
local waiting = {false, false} -- true when something waits on a road
local broken = {} -- names of blocks that did not answer
local phase = "green" -- "green", "amber", "all_red", "red_amber" or "night"
local go = 1 -- the road that has, or is about to get, the green
local since = 0 -- os.clock() when the phase began
local forced = false -- flashing forced by hand (the f key)
-- Blocks -------------------------------------------------------------------------------
local function set_road(i, aspect)
aspects[i] = aspect
for _, name in ipairs(ROADS[i].lights) do
local r = pcall(peripheral.call, name, "set", aspect)
if r.ok then broken[name] = nil else broken[name] = true end
end
end
local function read_loops(i)
local total, all_ok = 0, true
for _, name in ipairs(ROADS[i].loops) do
local r = pcall(peripheral.call, name, "count")
if r.ok then
total = total + r.value
broken[name] = nil
else
broken[name] = true
all_ok = false
end
end
counts[i] = total
waiting[i] = total > 0 or not all_ok
end
-- State machine ------------------------------------------------------------------------
local function enter(new_phase)
phase = new_phase
since = os.clock()
end
local function is_night()
local t = os.day_time()
return forced or (t >= NIGHT_START and t < NIGHT_END)
end
local function step()
read_loops(1)
read_loops(2)
local other = 3 - go
local elapsed = os.clock() - since
if is_night() then
if phase ~= "night" then
set_road(1, "flashing")
set_road(2, "flashing")
enter("night")
end
elseif phase == "night" then
set_road(1, "red")
set_road(2, "red")
go = 2 -- so that the main road gets the next green
enter("all_red")
elseif phase == "green" then
local change = false
if elapsed >= MIN_GREEN then
if waiting[other] and (not waiting[go] or elapsed >= MAX_GREEN) then change = true end
if go ~= 1 and not waiting[go] then change = true end -- back to the main road
end
if change then
set_road(go, "amber")
enter("amber")
end
elseif phase == "amber" and elapsed >= AMBER then
set_road(go, "red")
enter("all_red")
elseif phase == "all_red" and elapsed >= ALL_RED then
go = other
set_road(go, "red_amber")
enter("red_amber")
elseif phase == "red_amber" and elapsed >= RED_AMBER then
set_road(go, "green")
enter("green")
end
end
-- Screen ---------------------------------------------------------------------------------
local function clock_text()
local t = (os.day_time() + 6000) % 24000 -- 0 is 6 am
return string.format("%02d:%02d", t // 1000, t % 1000 * 60 // 1000)
end
local function lamp(x, y, lit, color)
if lit then gfx.circle(x, y, 8, color, true) else gfx.circle(x, y, 8, "black", true) end
end
local function draw_head(x, aspect, label)
local blink = math.floor(os.clock() * 2) % 2 == 0
gfx.rect(x, 14, 26, 62, "gray", true)
lamp(x + 13, 25, aspect == "red" or aspect == "red_amber", "red")
lamp(x + 13, 45, aspect == "amber" or aspect == "red_amber" or (aspect == "flashing" and blink), "orange")
lamp(x + 13, 65, aspect == "green", "lime")
gfx.text(x + 11, 81, label, "light_gray")
end
local function draw()
local w = term.get_size().w
term.set_bg(term.colors.black)
term.clear()
gfx.clear()
term.set_cursor(1, 1)
term.set_bg(term.colors.blue)
term.set_fg(term.colors.white)
term.write(string.format(" %-" .. (w - 8) .. "s%6s ", "INTERSECTION", clock_text()))
term.set_bg(term.colors.black)
for i, road in ipairs(ROADS) do
term.set_cursor(2, i * 3)
term.set_fg(term.colors.white)
term.write(i .. " " .. road.name)
term.set_cursor(4, i * 3 + 1)
term.set_fg(term.colors[ASPECT_COLORS[aspects[i]]])
term.write(string.format("%-10s", ASPECT_TEXT[aspects[i]]))
term.set_fg(term.colors.light_gray)
term.write("loop: " .. counts[i])
draw_head(w * 6 - 84 + (i - 1) * 40, aspects[i], tostring(i))
end
term.set_cursor(2, 9)
term.set_fg(term.colors.light_gray)
term.write(string.format("%s, %d s", PHASE_TEXT[phase], math.floor(os.clock() - since)))
local missing = 0
for _, _ in pairs(broken) do missing = missing + 1 end
if missing > 0 then
term.set_cursor(2, 10)
term.set_fg(term.colors.red)
term.write(missing .. " block(s) not answering")
end
term.set_cursor(2, 11)
term.set_fg(term.colors.gray)
term.write("f: flashing on/off")
end
-- Main loop ------------------------------------------------------------------------------
set_road(1, "green")
set_road(2, "red")
enter("green")
local timer = os.start_timer(TICK)
while true do
local e = os.pull_event()
if e.name == "timer" and e.id == timer then
step()
draw()
timer = os.start_timer(TICK)
elseif e.name == "char" and e.char == "f" then
forced = not forced
end
endWhat the screen looks like
The drawing code of the program on the Microcontroller, at the moment the main road turns amber because a cart waits over the loop of the side road:
local ROADS = {{name = "Main road"}, {name = "Side road"}}
local ASPECT_TEXT = {off = "OFF", red = "RED", red_amber = "RED+AMBER", amber = "AMBER",
green = "GREEN", flashing = "FLASHING"}
local ASPECT_COLORS = {off = "gray", red = "red", red_amber = "orange", amber = "orange",
green = "lime", flashing = "orange"}
local PHASE_TEXT = {green = "green", amber = "amber", all_red = "all red",
red_amber = "red and amber", night = "night, flashing"}
local aspects = {"amber", "red"}
local counts = {0, 1}
local broken = {}
local phase = "amber"
local since = os.clock() - 1
local function clock_text()
local t = (os.day_time() + 6000) % 24000
return string.format("%02d:%02d", t // 1000, t % 1000 * 60 // 1000)
end
local function lamp(x, y, lit, color)
if lit then gfx.circle(x, y, 8, color, true) else gfx.circle(x, y, 8, "black", true) end
end
local function draw_head(x, aspect, label)
local blink = math.floor(os.clock() * 2) % 2 == 0
gfx.rect(x, 14, 26, 62, "gray", true)
lamp(x + 13, 25, aspect == "red" or aspect == "red_amber", "red")
lamp(x + 13, 45, aspect == "amber" or aspect == "red_amber" or (aspect == "flashing" and blink), "orange")
lamp(x + 13, 65, aspect == "green", "lime")
gfx.text(x + 11, 81, label, "light_gray")
end
local function draw()
local w = term.get_size().w
term.set_bg(term.colors.black)
term.clear()
gfx.clear()
term.set_cursor(1, 1)
term.set_bg(term.colors.blue)
term.set_fg(term.colors.white)
term.write(string.format(" %-" .. (w - 8) .. "s%6s ", "INTERSECTION", clock_text()))
term.set_bg(term.colors.black)
for i, road in ipairs(ROADS) do
term.set_cursor(2, i * 3)
term.set_fg(term.colors.white)
term.write(i .. " " .. road.name)
term.set_cursor(4, i * 3 + 1)
term.set_fg(term.colors[ASPECT_COLORS[aspects[i]]])
term.write(string.format("%-10s", ASPECT_TEXT[aspects[i]]))
term.set_fg(term.colors.light_gray)
term.write("loop: " .. counts[i])
draw_head(w * 6 - 84 + (i - 1) * 40, aspects[i], tostring(i))
end
term.set_cursor(2, 9)
term.set_fg(term.colors.light_gray)
term.write(string.format("%s, %d s", PHASE_TEXT[phase], math.floor(os.clock() - since)))
local missing = 0
for _, _ in pairs(broken) do missing = missing + 1 end
if missing > 0 then
term.set_cursor(2, 10)
term.set_fg(term.colors.red)
term.write(missing .. " block(s) not answering")
end
term.set_cursor(2, 11)
term.set_fg(term.colors.gray)
term.write("f: flashing on/off")
end
draw()
Testing it
- Build the junction, run
startup listand copy the names intoROADS: the light and the loop of the main road in the first line, those of the side road in the second. - Run
startup. The main road is green, the side road red, and both loops read 0. - Walk onto the loop of the side road and wait. After at most
MIN_GREENseconds, the main road goes amber, then red; a second later, the side road shows red and amber, then green. - Step off the loop: after
MIN_GREENseconds of green, the side road goes back through amber, and the main road gets the green again. - Stand on both loops (bring a friend, or leave a cow on one): the green alternates, each road keeping it at most
MAX_GREENseconds. - Type
fin the terminal: both lights flash amber. Typefagain, or wait for the night (/time set night) and the morning: the junction goes back to red on both roads, then green on the main road. - Break the loop of the side road: the screen reports
1 block(s) not answering, and the side road still gets its turn on a regular cycle.
Variations
- Pedestrian button. A button by the road, wired to the computer: in
step, treatrs.get("left") > 0as a car waiting on the side road. - Count the traffic. Count the rising edges of each loop (
counts[i]going from 0 to more) and append them to a file every hour: seeData logger and graph. - Four roads. With four approaches, keep the two pairs that never cross (north-south, east-west) and give each pair a list of lights and loops: the program does not change,
ROADSjust gets longer lists. - Give the lights back to redstone. Before leaving the program, call
traffic_light.release()on each light (peripheral.call(name, "release")): the lights follow redstone again, like a Traffic Light without a computer. Without it, a light keeps the last aspect the program set, even after a reboot or once the computer is broken. - A level crossing. Replace the side road by a minecart track: an Inductive Loop Detector under the rails sees each cart, and the road gets the red while one is coming.