Create: Computing AgesDoc Brass
Recettes : vrais projets

Protection contre la surcharge

Lire le stress et la capacité d'un réseau Create, ralentir un Contrôleur de vitesse de rotation avant la surcharge, donner l'alarme et dessiner une barre de stress avec son historique.

Quand un réseau Create consomme plus de stress que ses générateurs n'en fournissent, tout s'arrête dessus : les broyeurs, la presse, les tapis, même les ordinateurs qu'il fait tourner. Cela arrive quand vous ajoutez une machine de trop, ou quand une contraption démarre et que sa charge rejoint le réseau d'un coup. On construit alors en général une grosse marge de sécurité, qui gaspille des roues à eau.

Ce projet confie la marge à un ordinateur. Un Contrôleur de vitesse de rotation alimente les machines ; l'ordinateur lit le stress et la capacité du réseau à travers lui, et quand le réseau approche de sa limite, il baisse la vitesse du contrôleur. Les machines derrière le contrôleur consomment en proportion de leur vitesse : une chaîne plus lente consomme moins. Quand la place revient, l'ordinateur remonte la vitesse jusqu'à celle que vous avez demandée. Une lampe s'allume tant que le réseau est près de sa limite, et l'écran dessine une barre de stress et l'historique de la dernière minute.

Le projet montre comment piloter un bloc cinétique de Create (Blocs cinétiques de Create), et comment dessiner un graphique en direct avec gfx, avec l'astuce qui rend un graphique défilant bon marché : gfx.scroll.

Ce qu'il vous faut

  • Un ordinateur qui dessine : un Ordinateur à transistors ou plus récent (gfx n'existe pas sur le Calculateur à tubes). Un Micro-ordinateur affiche les couleurs des zones ; l'Ordinateur à transistors et le Mini-ordinateur montrent tout dans une seule couleur.
  • Un Contrôleur de vitesse de rotation entre vos générateurs et les machines, contre un côté de l'ordinateur. À partir du Mini-ordinateur, il peut aussi être sur un Câble de données : il s'appelle alors kinetic@x,y,z.
  • Une lampe à redstone (ou une cloche) sur l'ordinateur : l'alarme.
  • L'ordinateur a aussi besoin de sa propre rotation, comme tout ordinateur : un arbre dans son dos, venu d'un autre réseau (une roue à eau à lui). Un ordinateur entraîné par le réseau qu'il protège se fige avec lui pendant une surcharge, et ne pourrait plus le relancer.

Vu de dessus (la grande roue dentée du contrôleur est posée dessus, et entraîne les machines) :

                         +------------+
  roues à eau ========== | Contrôleur | =========  l'arbre continue
                         +------------+
                         | Ordinateur |  -> écran       lampe sur l'ordinateur
                    ===> +------------+
            arbre dans le dos de l'ordinateur

Le contrôleur touche le flanc de l'ordinateur. Réglez REGULATEUR sur ce côté, "left" ou "right" vu en face de l'écran.

Comment Create compte le stress

Chaque machine a un impact sur le stress par tr/min : une presse mécanique à 64 tr/min consomme deux fois plus qu'à 32 tr/min. Chaque générateur donne une capacité (une roue à eau qui tourne à sa vitesse donne une quantité fixe). Les deux se comptent en unités de stress (us). Le réseau est en surcharge quand la somme des impacts dépasse la somme des capacités.

Le contrôleur fait partie du même réseau que les machines qu'il entraîne : kinetic.stress() et kinetic.capacity() appelés sur lui lisent les totaux du réseau entier, les générateurs et chaque machine avant et après lui. Et comme les machines derrière lui consomment en proportion de sa vitesse, kinetic.set_speed() agit directement sur la charge.

méthoderenvoie
regulateur.stress()le stress consommé par le réseau, en SU
regulateur.capacity()le stress que les générateurs peuvent fournir, en SU (0 sans rotation)
regulateur.overstressed()true quand le réseau est en surcharge (tout est arrêté)
regulateur.set_speed(tr_min)règle la vitesse visée : un nombre entier (le contrôleur va de -256 à 256 tr/min)

Deux détails comptent. set_speed veut un nombre entier : set_speed(90.5) arrête le programme avec bad argument #1 to 'set_speed' (number has no integer representation). Et chaque nouvelle vitesse reconstruit le réseau cinétique : le programme ne l'appelle donc que quand la vitesse change vraiment.

Le programme, étape par étape

1. Les réglages

Brass
local REGULATEUR = "left"   -- le Contrôleur de vitesse de rotation : un côté ou "kinetic@x,y,z"
local ALARME = "top"        -- la lampe : allumée tant que le réseau est près de sa limite
local SENS = 1              -- 1 ou -1 : le sens de rotation des machines
local VITESSE_MIN = 16      -- la protection ne descend jamais plus bas
local PAS = 8               -- tr/min changés à chaque vérification
local HAUT = 0.90           -- au-dessus de 90 % de la capacité : ralentir
local BAS = 0.75            -- en dessous de 75 % : réaccélérer
local PERIODE = 0.5         -- secondes entre deux vérifications

local cible = 128           -- la vitesse voulue ; les touches + et - la changent

Entre BAS et HAUT, la protection garde la vitesse telle quelle. Sans cette bande, elle accélérerait et ralentirait à chaque vérification autour d'une seule limite.

2. Lire le réseau

Brass
local regulateur = nil

local function lire_reseau()
  return {
    contrainte = regulateur.stress(),
    capacite = regulateur.capacity(),
    surcharge = regulateur.overstressed(),
  }
end

En Brass, une fonction renvoie une seule valeur : les trois mesures reviennent donc dans une table. Le programme l'appelle à travers pcall : si le contrôleur est cassé ou si son chunk se décharge, la lecture échoue par une erreur au lieu d'arrêter le programme.

3. Choisir la vitesse

Chaque vérification calcule charge, la part de la capacité utilisée, et choisit une nouvelle vitesse :

  • surcharge : directement à VITESSE_MIN, pour relancer le réseau au plus vite ;
  • **au-dessus de HAUT** : un PAS plus lent (si elle est déjà à VITESSE_MIN, la protection ne peut plus rien : « AU MINIMUM ») ;
  • cible baissée avec la touche - : jusqu'à la cible tout de suite ;
  • **en dessous de BAS** et plus lent que la cible : un PAS plus vite ;
  • sinon : maintenir.
Brass
local vitesse = VITESSE_MIN    -- la vitesse réglée par la protection, sans le sens
local contrainte, capacite, charge = 0, 0, 0
local etat = "DÉMARRAGE"

local function appliquer(nouvelle)
  if nouvelle == vitesse then return end
  local r = pcall(regulateur.set_speed, nouvelle * SENS)
  if r.ok then vitesse = nouvelle else etat = "PAS DE CONTRÔLEUR" end
end

local function verifier()
  local r = pcall(lire_reseau)
  if not r.ok then
    etat = "PAS DE CONTRÔLEUR"
    charge = 0
    rs.set(ALARME, true)
    return
  end
  contrainte = r.value.contrainte
  capacite = r.value.capacite
  if capacite <= 0 then
    charge = 0
    etat = "SANS ROTATION"
    rs.set(ALARME, false)
    return
  end
  charge = contrainte / capacite
  local voulue = vitesse
  if r.value.surcharge then
    voulue = VITESSE_MIN
    etat = "SURCHARGE"
  elseif charge > HAUT then
    voulue = math.max(VITESSE_MIN, vitesse - PAS)
    if voulue == vitesse then etat = "AU MINIMUM" else etat = "RALENTIT" end
  elseif vitesse > cible then
    voulue = cible
    etat = "OK"
  elseif charge < BAS and vitesse < cible then
    voulue = math.min(cible, vitesse + PAS)
    etat = "ACCÉLÈRE"
  elseif vitesse < cible then
    etat = "MAINTIEN"
  else
    etat = "OK"
  end
  appliquer(voulue)
  rs.set(ALARME, r.value.surcharge or charge > HAUT)
end

Sans rotation (capacite vaut 0), il n'y a rien à protéger : la protection attend, et ne divise pas par zéro.

4. La barre de stress

Le dessin utilise la couche de pixels de l'écran : 306 x 171 pixels sur un Micro-ordinateur, 6 x 9 par caractère. La barre occupe la largeur de l'écran sous la troisième ligne de texte. Deux traits blancs marquent BAS et HAUT, avec leur pourcentage écrit dans la petite police 3 x 5 de gfx.text.

Brass
local BX, BY, BL, BH = 7, 30, 294, 14     -- la barre : x, y, largeur, hauteur en pixels

local function couleur_zone(c)
  if c >= HAUT then return "red" end
  if c >= BAS then return "yellow" end
  return "lime"
end

local function dessiner_barre(c)
  gfx.rect(BX, BY, BL, BH, "gray", true)
  local l = math.floor(math.min(1, c) * BL + 0.5)
  if l > 0 then gfx.rect(BX, BY, l, BH, couleur_zone(c), true) end
  for _, niveau in ipairs({BAS, HAUT}) do
    local x = BX + math.floor(niveau * BL + 0.5)
    gfx.line(x, BY - 2, x, BY + BH + 1, "white")
    gfx.text(x - 3, BY + BH + 4, tostring(math.floor(niveau * 100 + 0.5)), "light_gray")
  end
end

gfx.rect(x, y, l, h, couleur, true) remplit le rectangle ; dessiner le fond gris d'abord puis la partie colorée par dessus est plus simple que de dessiner deux morceaux côte à côte.

5. Le graphique de l'historique, avec gfx.scroll

Redessiner tout un graphique à chaque vérification coûterait une boucle sur toutes les mesures. À la place, le programme décale l'image du graphique de trois pixels vers la gauche avec gfx.scroll, et ne dessine que la nouvelle colonne au bord droit. Chaque colonne reçoit aussi un pixel de chaque ligne de seuil, ce qui les dessine en pointillés qui défilent avec les données.

Brass
local GX, GY, GL, GH = 7, 93, 294, 66     -- la zone du graphique, en pixels
local COL = 3                             -- largeur d'une mesure

local function y_niveau(c)
  return GY + GH - math.floor(math.min(1, c) * GH + 0.5)
end

local function colonne_graphe(c)
  gfx.scroll(-COL, 0, GX, GY, GL, GH)
  local x = GX + GL - COL
  gfx.rect(x, GY, COL, GH, "black", true)
  local h = GY + GH - y_niveau(c)
  if h > 0 then gfx.rect(x, GY + GH - h, COL, h, couleur_zone(c), true) end
  gfx.pixel(x + 1, y_niveau(HAUT), "red")
  gfx.pixel(x + 1, y_niveau(BAS), "yellow")
end

Avec 294 pixels et 3 pixels par mesure, le graphique garde 98 mesures : 49 secondes à une vérification par demi-seconde.

6. Le texte et l'écran

Le texte passe par-dessus le dessin : term.clear efface les caractères mais laisse les pixels, si bien que le programme peut réécrire toutes les lignes à chaque vérification sans toucher au graphique.

Brass
local COULEURS_ETAT = {
  ["DÉMARRAGE"] = "light_gray", ["OK"] = "lime", ["ACCÉLÈRE"] = "light_blue",
  ["MAINTIEN"] = "light_gray", ["RALENTIT"] = "yellow", ["AU MINIMUM"] = "red",
  ["SURCHARGE"] = "red", ["SANS ROTATION"] = "gray", ["PAS DE CONTRÔLEUR"] = "red",
}

local function dessiner()
  term.set_bg(term.colors.black)
  term.clear()
  term.set_cursor(1, 1)
  term.set_bg(term.colors.blue)
  term.set_fg(term.colors.white)
  term.write(string.format(" %-49s ", "PROTECTION CONTRE LA SURCHARGE"))
  term.set_bg(term.colors.black)
  term.set_cursor(1, 3)
  term.write(string.format(" Stress      %d / %d us", math.floor(contrainte + 0.5), math.floor(capacite + 0.5)))
  term.set_cursor(44, 3)
  term.set_fg(term.colors[couleur_zone(charge)])
  term.write(string.format("%5d %%", math.floor(charge * 100 + 0.5)))
  dessiner_barre(charge)
  term.set_cursor(1, 7)
  term.set_fg(term.colors.white)
  term.write(string.format(" Vitesse     %d tr/min (cible %d tr/min)", vitesse * SENS, cible * SENS))
  term.set_cursor(1, 8)
  term.write(" État        ")
  term.set_fg(term.colors[COULEURS_ETAT[etat]])
  term.write(etat)
  term.set_cursor(1, 10)
  term.set_fg(term.colors.gray)
  term.write(" Charge, dernières " .. math.floor(GL / COL * PERIODE) .. " s")
  term.set_cursor(1, 19)
  term.write(" + et - changent la vitesse visée")
end

term.colors[couleur_zone(charge)] transforme un nom de couleur en nombre, comme l'attend term.set_fg : term.colors.red et term.colors["red"] sont la même chose. %% dans string.format écrit un signe pour cent.

7. Le démarrage et la boucle principale

Au démarrage, la protection prend le contrôleur et le règle sur VITESSE_MIN : un démarrage en douceur, les machines accélèrent pas à pas au lieu de tout d'un coup, et la protection voit la charge monter. Si le bloc de ce côté est cinétique sans être un Contrôleur de vitesse de rotation, set_speed échoue et le programme s'arrête avec un message clair.

Brass
local function preparer()
  regulateur = peripheral.wrap(REGULATEUR)
  if regulateur == nil or regulateur.set_speed == nil then
    error("pas de Contrôleur de vitesse de rotation sur '" .. REGULATEUR .. "'")
  end
  local r = pcall(regulateur.set_speed, VITESSE_MIN * SENS)
  if not r.ok then error(r.error) end
  term.set_bg(term.colors.black)
  term.clear()
  gfx.clear()
  gfx.rect(GX - 1, GY - 1, GL + 2, GH + 2, "gray")
end

preparer()
local minuteur = os.start_timer(PERIODE)
while true do
  local e = os.pull_event()
  if e.name == "timer" and e.id == minuteur then
    verifier()
    colonne_graphe(charge)
    dessiner()
    minuteur = os.start_timer(PERIODE)
  elseif e.name == "char" and e.char == "+" then
    cible = math.min(256, cible + PAS)
    dessiner()
  elseif e.name == "char" and e.char == "-" then
    cible = math.max(VITESSE_MIN, cible - PAS)
    dessiner()
  end
end

L'évènement char porte le caractère tapé dans le terminal : les touches marchent tant que le terminal est ouvert.

Le programme complet

startup
-- Protection contre la surcharge : lit le stress d'un réseau Create à
-- travers un Contrôleur de vitesse de rotation, le ralentit avant la
-- surcharge, le réaccélère quand la place revient, et dessine une barre et son
-- historique.

local REGULATEUR = "left"   -- le Contrôleur de vitesse de rotation : un côté ou "kinetic@x,y,z"
local ALARME = "top"        -- la lampe : allumée tant que le réseau est près de sa limite
local SENS = 1              -- 1 ou -1 : le sens de rotation des machines
local VITESSE_MIN = 16      -- la protection ne descend jamais plus bas
local PAS = 8               -- tr/min changés à chaque vérification
local HAUT = 0.90           -- au-dessus de 90 % de la capacité : ralentir
local BAS = 0.75            -- en dessous de 75 % : réaccélérer
local PERIODE = 0.5         -- secondes entre deux vérifications

local cible = 128           -- la vitesse voulue ; les touches + et - la changent

local BX, BY, BL, BH = 7, 30, 294, 14     -- la barre, en pixels
local GX, GY, GL, GH = 7, 93, 294, 66     -- la zone du graphique, en pixels
local COL = 3                             -- largeur d'une mesure

local COULEURS_ETAT = {
  ["DÉMARRAGE"] = "light_gray", ["OK"] = "lime", ["ACCÉLÈRE"] = "light_blue",
  ["MAINTIEN"] = "light_gray", ["RALENTIT"] = "yellow", ["AU MINIMUM"] = "red",
  ["SURCHARGE"] = "red", ["SANS ROTATION"] = "gray", ["PAS DE CONTRÔLEUR"] = "red",
}

local regulateur = nil
local vitesse = VITESSE_MIN    -- la vitesse réglée par la protection, sans le sens
local contrainte, capacite, charge = 0, 0, 0
local etat = "DÉMARRAGE"

-- Lire et décider ------------------------------------------------------------------

local function lire_reseau()
  return {
    contrainte = regulateur.stress(),
    capacite = regulateur.capacity(),
    surcharge = regulateur.overstressed(),
  }
end

local function appliquer(nouvelle)
  if nouvelle == vitesse then return end
  local r = pcall(regulateur.set_speed, nouvelle * SENS)
  if r.ok then vitesse = nouvelle else etat = "PAS DE CONTRÔLEUR" end
end

local function verifier()
  local r = pcall(lire_reseau)
  if not r.ok then
    etat = "PAS DE CONTRÔLEUR"
    charge = 0
    rs.set(ALARME, true)
    return
  end
  contrainte = r.value.contrainte
  capacite = r.value.capacite
  if capacite <= 0 then
    charge = 0
    etat = "SANS ROTATION"
    rs.set(ALARME, false)
    return
  end
  charge = contrainte / capacite
  local voulue = vitesse
  if r.value.surcharge then
    voulue = VITESSE_MIN
    etat = "SURCHARGE"
  elseif charge > HAUT then
    voulue = math.max(VITESSE_MIN, vitesse - PAS)
    if voulue == vitesse then etat = "AU MINIMUM" else etat = "RALENTIT" end
  elseif vitesse > cible then
    voulue = cible
    etat = "OK"
  elseif charge < BAS and vitesse < cible then
    voulue = math.min(cible, vitesse + PAS)
    etat = "ACCÉLÈRE"
  elseif vitesse < cible then
    etat = "MAINTIEN"
  else
    etat = "OK"
  end
  appliquer(voulue)
  rs.set(ALARME, r.value.surcharge or charge > HAUT)
end

-- Dessin ----------------------------------------------------------------------------

local function couleur_zone(c)
  if c >= HAUT then return "red" end
  if c >= BAS then return "yellow" end
  return "lime"
end

local function dessiner_barre(c)
  gfx.rect(BX, BY, BL, BH, "gray", true)
  local l = math.floor(math.min(1, c) * BL + 0.5)
  if l > 0 then gfx.rect(BX, BY, l, BH, couleur_zone(c), true) end
  for _, niveau in ipairs({BAS, HAUT}) do
    local x = BX + math.floor(niveau * BL + 0.5)
    gfx.line(x, BY - 2, x, BY + BH + 1, "white")
    gfx.text(x - 3, BY + BH + 4, tostring(math.floor(niveau * 100 + 0.5)), "light_gray")
  end
end

local function y_niveau(c)
  return GY + GH - math.floor(math.min(1, c) * GH + 0.5)
end

local function colonne_graphe(c)
  gfx.scroll(-COL, 0, GX, GY, GL, GH)
  local x = GX + GL - COL
  gfx.rect(x, GY, COL, GH, "black", true)
  local h = GY + GH - y_niveau(c)
  if h > 0 then gfx.rect(x, GY + GH - h, COL, h, couleur_zone(c), true) end
  gfx.pixel(x + 1, y_niveau(HAUT), "red")
  gfx.pixel(x + 1, y_niveau(BAS), "yellow")
end

local function dessiner()
  term.set_bg(term.colors.black)
  term.clear()
  term.set_cursor(1, 1)
  term.set_bg(term.colors.blue)
  term.set_fg(term.colors.white)
  term.write(string.format(" %-49s ", "PROTECTION CONTRE LA SURCHARGE"))
  term.set_bg(term.colors.black)
  term.set_cursor(1, 3)
  term.write(string.format(" Stress      %d / %d us", math.floor(contrainte + 0.5), math.floor(capacite + 0.5)))
  term.set_cursor(44, 3)
  term.set_fg(term.colors[couleur_zone(charge)])
  term.write(string.format("%5d %%", math.floor(charge * 100 + 0.5)))
  dessiner_barre(charge)
  term.set_cursor(1, 7)
  term.set_fg(term.colors.white)
  term.write(string.format(" Vitesse     %d tr/min (cible %d tr/min)", vitesse * SENS, cible * SENS))
  term.set_cursor(1, 8)
  term.write(" État        ")
  term.set_fg(term.colors[COULEURS_ETAT[etat]])
  term.write(etat)
  term.set_cursor(1, 10)
  term.set_fg(term.colors.gray)
  term.write(" Charge, dernières " .. math.floor(GL / COL * PERIODE) .. " s")
  term.set_cursor(1, 19)
  term.write(" + et - changent la vitesse visée")
end

-- Démarrage et boucle principale -------------------------------------------------

local function preparer()
  regulateur = peripheral.wrap(REGULATEUR)
  if regulateur == nil or regulateur.set_speed == nil then
    error("pas de Contrôleur de vitesse de rotation sur '" .. REGULATEUR .. "'")
  end
  local r = pcall(regulateur.set_speed, VITESSE_MIN * SENS)
  if not r.ok then error(r.error) end
  term.set_bg(term.colors.black)
  term.clear()
  gfx.clear()
  gfx.rect(GX - 1, GY - 1, GL + 2, GH + 2, "gray")
end

preparer()
local minuteur = os.start_timer(PERIODE)
while true do
  local e = os.pull_event()
  if e.name == "timer" and e.id == minuteur then
    verifier()
    colonne_graphe(charge)
    dessiner()
    minuteur = os.start_timer(PERIODE)
  elseif e.name == "char" and e.char == "+" then
    cible = math.min(256, cible + PAS)
    dessiner()
  elseif e.name == "char" and e.char == "-" then
    cible = math.max(VITESSE_MIN, cible - PAS)
    dessiner()
  end
end

À quoi ressemble l'écran

Le code de dessin du programme, nourri avec 98 mesures inventées au lieu d'un vrai réseau : la charge monte, la protection la tient sous 90 %, une grosse contraption démarre et met le réseau en surcharge un instant, la protection tombe à VITESSE_MIN puis remonte. Dernière mesure : 1792 SU utilisées sur 2048, la protection tient 96 tr/min au lieu de 128.

Brass
local HAUT, BAS, PERIODE, SENS = 0.90, 0.75, 0.5, 1
local BX, BY, BL, BH = 7, 30, 294, 14
local GX, GY, GL, GH = 7, 93, 294, 66
local COL = 3
local COULEURS_ETAT = {
  ["DÉMARRAGE"] = "light_gray", ["OK"] = "lime", ["ACCÉLÈRE"] = "light_blue",
  ["MAINTIEN"] = "light_gray", ["RALENTIT"] = "yellow", ["AU MINIMUM"] = "red",
  ["SURCHARGE"] = "red", ["SANS ROTATION"] = "gray", ["PAS DE CONTRÔLEUR"] = "red",
}
local cible, vitesse = 128, 96
local contrainte, capacite, charge = 1792, 2048, 0.875
local etat = "MAINTIEN"

local function couleur_zone(c)
  if c >= HAUT then return "red" end
  if c >= BAS then return "yellow" end
  return "lime"
end

local function dessiner_barre(c)
  gfx.rect(BX, BY, BL, BH, "gray", true)
  local l = math.floor(math.min(1, c) * BL + 0.5)
  if l > 0 then gfx.rect(BX, BY, l, BH, couleur_zone(c), true) end
  for _, niveau in ipairs({BAS, HAUT}) do
    local x = BX + math.floor(niveau * BL + 0.5)
    gfx.line(x, BY - 2, x, BY + BH + 1, "white")
    gfx.text(x - 3, BY + BH + 4, tostring(math.floor(niveau * 100 + 0.5)), "light_gray")
  end
end

local function y_niveau(c)
  return GY + GH - math.floor(math.min(1, c) * GH + 0.5)
end

local function colonne_graphe(c)
  gfx.scroll(-COL, 0, GX, GY, GL, GH)
  local x = GX + GL - COL
  gfx.rect(x, GY, COL, GH, "black", true)
  local h = GY + GH - y_niveau(c)
  if h > 0 then gfx.rect(x, GY + GH - h, COL, h, couleur_zone(c), true) end
  gfx.pixel(x + 1, y_niveau(HAUT), "red")
  gfx.pixel(x + 1, y_niveau(BAS), "yellow")
end

local function dessiner()
  term.set_bg(term.colors.black)
  term.clear()
  term.set_cursor(1, 1)
  term.set_bg(term.colors.blue)
  term.set_fg(term.colors.white)
  term.write(string.format(" %-49s ", "PROTECTION CONTRE LA SURCHARGE"))
  term.set_bg(term.colors.black)
  term.set_cursor(1, 3)
  term.write(string.format(" Stress      %d / %d us", math.floor(contrainte + 0.5), math.floor(capacite + 0.5)))
  term.set_cursor(44, 3)
  term.set_fg(term.colors[couleur_zone(charge)])
  term.write(string.format("%5d %%", math.floor(charge * 100 + 0.5)))
  dessiner_barre(charge)
  term.set_cursor(1, 7)
  term.set_fg(term.colors.white)
  term.write(string.format(" Vitesse     %d tr/min (cible %d tr/min)", vitesse * SENS, cible * SENS))
  term.set_cursor(1, 8)
  term.write(" État        ")
  term.set_fg(term.colors[COULEURS_ETAT[etat]])
  term.write(etat)
  term.set_cursor(1, 10)
  term.set_fg(term.colors.gray)
  term.write(" Charge, dernières " .. math.floor(GL / COL * PERIODE) .. " s")
  term.set_cursor(1, 19)
  term.write(" + et - changent la vitesse visée")
end

-- mesures inventées : une charge qui monte, tenue sous 90 %, puis une surcharge
local function mesure(i)
  if i <= 30 then return 0.35 + i * 0.017 end
  if i <= 55 then return 0.84 + 0.04 * math.sin(i * 0.7) end
  if i <= 58 then return 1.08 end
  if i <= 62 then return 0.30 + (i - 58) * 0.05 end
  return math.min(0.875, 0.50 + (i - 62) * 0.02)
end

gfx.clear()
gfx.rect(GX - 1, GY - 1, GL + 2, GH + 2, "gray")
for i = 1, 98 do
  colonne_graphe(mesure(i))
end
dessiner()
Écran
Écran

L'essayer

  1. Alimentez un Contrôleur de vitesse de rotation avec quelques roues à eau, et mettez une grosse charge derrière : plusieurs presses mécaniques, une rangée de roues de concassage ou un grand mur de ventilateurs. Posez l'ordinateur contre le contrôleur, la lampe dessus.
  2. Lancez le programme : la vitesse part de 16 tr/min et monte de 8 chaque demi-seconde. Regardez la barre grandir.
  3. Si la charge est assez lourde, la montée s'arrête avant que la barre n'atteigne la zone rouge : l'état affiche MAINTIEN, et la vitesse reste sous 128.
  4. Ajoutez une presse pendant que ça tourne : la barre bondit, la lampe s'allume, l'état affiche RALENTIT, et la vitesse baisse jusqu'à ce que la barre repasse sous 90 %.
  5. Cassez une roue à eau : la capacité chute, le réseau peut être en surcharge un instant ; la protection tombe au minimum puis remonte jusqu'à la vitesse que les roues restantes peuvent porter.
  6. Tapez - plusieurs fois dans le terminal : la cible baisse, et le contrôleur suit aussitôt.

Variantes

  • Une cloche ou une Liaison de redstone. Remplacez rs.set(ALARME, ...) par link.set("minecraft:redstone", "minecraft:bell", ...) pour donner l'alarme n'importe où dans la base. Ne la sonnez que quand l'état passe à SURCHARGE, comme la cloche de Alarme de stock et panneau d'affichage.
  • Plusieurs chaînes, une protection. Prenez plusieurs contrôleurs, chacun avec sa priorité. Quand le réseau manque, ralentissez d'abord la chaîne la moins importante (le générateur de pierre avant le fer).
  • Un journal. Ajoutez l'heure et la charge à un fichier chaque minute, pour savoir quand le réseau manque (Enregistreur de données et graphique).
  • Un écran à distance. Envoyez charge et etat au tableau de bord de l'usine avec net.broadcast (Tableau de bord d'usine).
  • Un pas plus malin. Le stress des machines derrière le contrôleur croît avec la vitesse : la vitesse qui ramène la charge à, disons, 85 % vaut environ vitesse * 0.85 / charge. Utilisez-la au lieu de PAS pour réagir plus vite, arrondie avec math.floor puisque set_speed veut un nombre entier.