Precision machines
The Coil Winder, Vacuum Chamber, Photolithography Machine, Ion Implanter, Pick-and-Place Machine and Crystal Puller: what they work on, and why they wait.
The precision machines of Computing Ages work like a Create Mechanical Press: they hang above a belt or a depot and bring their head down on the part under them, one step of a vacuum tube or a chip at a time. They hold what they work with (a spool of wire, a dopant, a photomask...) and need rotation, often fast. When a machine waits, Engineer's Goggles tell why: too slow, an ingredient missing... A computer reads the same thing, and can act on it: speed the line up, call for more wire, warn you.
local implanter = peripheral.find("ion_implanter")
if implanter.running() then
print("working: " .. math.floor(implanter.progress() * 100) .. " %")
else
print("waiting: " .. (implanter.problem() or "nothing to fix"))
endmachine.running() | Whether the machine is working: from the moment its head starts down on a part until it is back up. |
machine.progress() | How far the current cycle is, from 0 (the head leaves the top) to 1 (it is back up), rounded to 0.001. 0 while the machine is idle. A faster shaft makes the cycle shorter. |
machine.item() | The part waiting under the machine, as an item table: the one on the belt or the depot two blocks below, or else the one lying on the ground right below. nil when nothing is there. count is the size of that stack. |
machine.recipe() | The recipe the machine would run on the part below: |
machine.problem() | What keeps the machine from working on the part below, as the goggles show it. nil when there is nothing below, or when the part can be worked on now. |
machine.tier() | The tier of the light source loaded in a Photolithography Machine: 1 for a UV Lamp, 2 for an Excimer Laser (DUV), 3 for an EUV Light Source. 0 when none is loaded, and always 0 on the four other machines. |
puller.running() | Whether a crystal is being pulled. |
puller.progress() | How far the crystal is pulled, from 0 (the seed touches the melt) to 1 (the boule is out), rounded to 0.001. The goggles show it as a percentage. |
puller.max_speed() | The speed limit of the puller, in RPM: the server config crystalPullerMaxSpeed, 32 by default. The pull is fastest at exactly this speed. |
puller.problem() | What keeps the puller from working: |
Setting up
Five machines, one set of methods. Each machine has its own type, the one you look for; all five have the methods of this page, written precision_machine.* here.
| type | block | what it holds (its slots) |
|---|---|---|
coil_winder | Coil Winder | 1: the spool of wire or foil |
vacuum_chamber | Vacuum Chamber | 1: the deposition target |
lithography_machine | Photolithography Machine | 2: the photomask, then the light source |
ion_implanter | Ion Implanter | 1: the dopant |
pick_and_place | Pick-and-Place Machine | 8: the component feeders |
precision_machine itself is not a type: peripheral.find("precision_machine") returns nil. Ask for the machine you want, like peripheral.find("coil_winder"). On a Data Cable (Minicomputer and newer), a machine is named after its type and its place: ion_implanter@120,64,-35.
Also kinetic and inventory. A precision machine turns and holds items, so it is also a kinetic block and an inventory, with all their methods: machine.speed() reads its RPM, machine.overstressed() tells whether its network has stopped, and machine.list() shows the slots of the table above (how much wire is left on a Coil Winder, for example). peripheral.find("inventory") finds them too.
Where the part is. A machine works on the part on the belt or the depot two blocks below it (with one block of air in between, as for a Mechanical Press), or, when there is no belt or depot there, on an item lying on the ground right under it.
The Crystal Puller works over a Basin instead, and has its own methods: see its section at the end of this page.
When the machine is gone, its methods stop the program with machine removed (crystal puller removed for the Crystal Puller); when its chunk is not loaded, with peripheral is not loaded.
The head
machine.running()
Whether the machine is working: from the moment its head starts down on a part until it is back up.
- boolean
truewhile the head is in a cycle
A "busy" lamp on top of the computer:
local winder = peripheral.find("coil_winder")
while true do
rs.set("top", winder.running())
sleep(0.5)
endSee also precision_machine.progress()
machine.progress()
How far the current cycle is, from 0 (the head leaves the top) to 1 (it is back up), rounded to 0.001. 0 while the machine is idle. A faster shaft makes the cycle shorter.
- number
- how far the head's cycle is, from 0 to 1; 0 when idle
local chamber = peripheral.find("vacuum_chamber")
print("vacuum cycle: " .. math.floor(chamber.progress() * 100) .. " %")See also precision_machine.running()
The part below
The part waiting under the machine, as an item table: the one on the belt or the depot two blocks below, or else the one lying on the ground right below. nil when nothing is there. count is the size of that stack.
- table|nil
- the part under the machine,
{name =, count =, display =}; nil when there is none
local litho = peripheral.find("lithography_machine")
local part = litho.item()
if part then
print("under the machine: " .. part.display)
else
print("no wafer below")
endSee also precision_machine.recipe()
The recipe the machine would run on the part below:
- table|nil
{id =, output =, min_speed =, tier =}; nil without a part, or when nothing can be made of it
| field | content |
|---|---|
id | the id of the recipe |
output | the item id of what it makes |
min_speed | the slowest rotation the recipe accepts, in RPM; 0 for any speed |
tier | the light source it needs on a Photolithography Machine, from 1 to 3; 0 on the other machines |
Several recipes can start from the same part: every chip begins as the same coated wafer under the Photolithography Machine. The machine picks the one it is equipped for, so the photomask, dopant or wire you load decides the product, and recipe() tells which one it will be. In a sequenced assembly, output is the unfinished part this step gives, until the last step.
local implanter = peripheral.find("ion_implanter")
local r = implanter.recipe()
if r then
print("makes " .. r.output .. ", at " .. r.min_speed .. " RPM or more")
endSee also precision_machine.item() precision_machine.problem()
Why it waits
What keeps the machine from working on the part below, as the goggles show it. nil when there is nothing below, or when the part can be worked on now.
- string|nil
"no_recipe","too_slow","tier_too_low"or"missing_ingredient"; nil when the part below can be worked on, or when nothing is below
| problem | why | what to do |
|---|---|---|
"no_recipe" | this machine makes nothing of the part below | send the part to the right machine |
"too_slow" | the machine does not turn, or turns slower than the recipe's min_speed | speed it up, or add power if the network is overstressed |
"tier_too_low" | the light source of the Photolithography Machine is too weak for the mask, or missing | load a stronger light source |
"missing_ingredient" | the spool, target, dopant, mask or a component the recipe takes from the machine is missing | refill the machine |
One problem at a time, in this order: once the speed is right, the machine may still lack its dopant. Ask again after each fix.
local winder = peripheral.find("coil_winder")
local problem = winder.problem()
if problem == "missing_ingredient" then
print("the Coil Winder needs a new spool of wire")
elseif problem == "too_slow" then
print("the Coil Winder does not turn fast enough")
endSee also precision_machine.recipe() precision_machine.tier()
machine.tier()
The tier of the light source loaded in a Photolithography Machine: 1 for a UV Lamp, 2 for an Excimer Laser (DUV), 3 for an EUV Light Source. 0 when none is loaded, and always 0 on the four other machines.
- number
- the tier of the light source in a Photolithography Machine, from 1 to 3; 0 without one, and on the other machines
A recipe runs when the machine's tier is at least the tier of the recipe: a stronger light works for older masks too.
local litho = peripheral.find("lithography_machine")
local r = litho.recipe()
if r and litho.tier() < r.tier then
print("this mask needs a light source of tier " .. r.tier)
endSee also precision_machine.recipe()
Crystal Puller
The Crystal Puller grows a single crystal out of the melt in the Basin two blocks below it, by the Czochralski process: a silicon boule from molten silicon, or synthetic quartz. Its type is crystal_puller, and it is also a kinetic block.
It wants a slow shaft. Above its speed limit (32 RPM by default) the crystal would break: the puller does not start, and a pull already under way waits until the shaft slows down. Below the limit, the slower it turns, the longer the pull: twice as long at half the limit, up to eight times as long. The length is set when the pull starts, so the best speed is the limit itself, set before the pull begins.
puller.running()
Whether a crystal is being pulled.
- boolean
truewhile it pulls a crystal
local puller = peripheral.find("crystal_puller")
print(puller.running())See also crystal_puller.progress()
puller.progress()
How far the crystal is pulled, from 0 (the seed touches the melt) to 1 (the boule is out), rounded to 0.001. The goggles show it as a percentage.
- number
- how far the pull is, from 0 to 1; 0 when it is not pulling
local puller = peripheral.find("crystal_puller")
if puller.running() then
print("boule " .. math.floor(puller.progress() * 100) .. " % pulled")
endSee also crystal_puller.running()
puller.max_speed()
The speed limit of the puller, in RPM: the server config crystalPullerMaxSpeed, 32 by default. The pull is fastest at exactly this speed.
- number
- the fastest rotation before the crystal would break, in RPM
With the cogwheel of a Rotation Speed Controller turning the puller directly, this gives the fastest pull the server allows (see speed_controller.set_target_speed()):
local puller = peripheral.find("crystal_puller")
local controller = peripheral.find("speed_controller")
controller.set_target_speed(puller.max_speed())See also crystal_puller.problem()
puller.problem()
What keeps the puller from working:
- string|nil
"too_fast","too_slow","no_basin", or nil
| problem | why |
|---|---|
"too_fast" | it turns faster than max_speed(): the pull waits |
"too_slow" | it does not turn |
"no_basin" | there is no Basin two blocks below |
nil means none of these, not that a pull is coming: the Basin still needs the right melt and enough heat (a superheated Blaze Burner for a silicon boule).
local puller = peripheral.find("crystal_puller")
if puller.problem() == "too_fast" then
print("slow down: " .. puller.max_speed() .. " RPM at most")
endSee also crystal_puller.max_speed()
Examples
Never too slow
The recipes of the Ion Implanter ask for 128 RPM. A Rotation Speed Controller drives it, and this program raises the controller step by step while the implanter is too slow, up to Create's limit. When the network is overstressed, more speed would only make it worse: the program says so instead.
local implanter = peripheral.find("ion_implanter")
local controller = peripheral.find("speed_controller")
local STEP = 16 -- RPM added each time
while true do
if implanter.overstressed() then
print("overstressed: add power, more speed would not help")
elseif implanter.problem() == "too_slow" then
local target = controller.target_speed()
if math.abs(target) >= 256 then
print("the controller is at full speed already")
else
local rpm = math.min(256, math.abs(target) + STEP)
if target < 0 then
rpm = -rpm -- keep the direction
end
controller.set_target_speed(rpm)
print("too slow: controller set to " .. rpm .. " RPM")
end
end
sleep(2)
endIt works through any gearing between the controller and the implanter, since it only watches the implanter's own problem. overstressed() comes from the kinetic methods every machine has.
A status board for the line
A Minicomputer on the Data Cable that runs along a chip line prints one line per machine it reaches: working with its progress, waiting with its problem, or idle. The Crystal Puller has running, progress and problem too, so it fits on the same board.
local NAMES = {
coil_winder = "Coil Winder",
vacuum_chamber = "Vacuum Chamber",
lithography_machine = "Photolithography",
ion_implanter = "Ion Implanter",
pick_and_place = "Pick-and-Place",
crystal_puller = "Crystal Puller",
}
local function status(machine)
if machine.running() then
return "working, " .. math.floor(machine.progress() * 100) .. " %"
end
return machine.problem() or "idle"
end
while true do
term.clear()
term.set_cursor(1, 1)
for _, name in ipairs(peripheral.list()) do
local label = NAMES[peripheral.type(name)]
if label then
print(string.format("%-17s %s", label, status(peripheral.wrap(name))))
end
end
sleep(2)
endperipheral.type gives the first type of each name, which for these blocks is their own (coil_winder...), not kinetic or inventory.