Create: Computing AgesDoc Brass
Recettes : vrais projets

Panneau de commande tactile

Un mur de moniteurs avec un bouton par machine : un appui commute une fréquence de Liaison de redstone, et les états reviennent après un redémarrage.

Votre usine a grandi : des broyeurs d'un côté, des presses et des mixeurs de l'autre, une ligne de ventilateurs pour le lavage, des perceuses dans la carrière. Chacun a son levier quelque part, et tout arrêter le soir demande une promenade. Ce projet rassemble les interrupteurs sur un écran tactile : un mur de moniteurs dans la salle de contrôle, un gros bouton par machine. Un clic droit sur un bouton démarre ou arrête cette machine par une fréquence de Liaison de redstone, où qu'elle soit dans la base. Les états sont enregistrés dans un fichier : après un redémarrage du serveur, chaque machine revient comme vous l'avez laissée.

C'est la référence pour les écrans cliquables en Brass : dessiner des boutons avec gfx et term, lire les évènements click (e.x, e.y, e.source) et tenir un petit fichier d'état avec fs.

Ce qu'il vous faut

  • Un Micro-ordinateur (ou un Ordinateur moderne) : les couleurs rendent le panneau lisible d'un coup d'œil. N'importe quel ordinateur à partir de l'Ordinateur à transistors le fait tourner, en une seule couleur.
  • Un mur de Moniteurs LCD (ou de Moniteurs cathodiques), par exemple 3 de large et 2 de haut, contre l'ordinateur. Des moniteurs du même type côte à côte, tournés du même côté, forment un seul écran qui montre le terminal de l'ordinateur. Un clic droit dessus appuie sur l'écran tant que le programme attend des clics ; accroupissez-vous et faites un clic droit pour ouvrir plutôt le terminal.
  • Une Liaison de redstone par machine, réglée en réception, à côté du bloc qui commande la machine. Chacune reçoit deux objets dans ses emplacements de fréquence : le programme utilise un objet de la machine et un levier (Roue de concassage + Levier pour les broyeurs, Presse mécanique + Levier pour les presses...).
  • Pour les machines de Create, un Embrayage sur l'arbre qui entraîne chaque chaîne, avec la Liaison de redstone à côté.

Vu de côté :

      +-------+-------+-------+
      |  LCD  |  LCD  |  LCD  |        l'écran tactile, tourné vers la salle
      +-------+-------+-------+
      |  LCD  |  LCD  |  LCD  |
      +-------+-------+-------+
              |  PC   |  <- l'ordinateur derrière le mur, contre lui
              +-------+

  À chaque machine :   arbre ==[Embrayage]== arbre ==> presse mécanique
                                 [Liaison de redstone, réception : presse mécanique + levier]
Important

Un embrayage alimenté débraye son arbre. Pour faire tourner une machine derrière un embrayage, le programme ne doit rien émettre, et pour l'arrêter, émettre un signal. Le réglage embrayage = true inverse le signal pour ces machines. Une lampe, au contraire, s'allume quand elle est alimentée : pas d'embrayage pour l'éclairage.

Le programme, étape par étape

1. Les machines

Une ligne par bouton : le nom affiché, les deux objets de la fréquence, et si la machine passe par un embrayage.

Brass
local FICHIER_ETAT = "panneau/etat"
local MACHINES = {
  {nom = "Broyeurs", freq = {"create:crushing_wheel", "minecraft:lever"}, embrayage = true},
  {nom = "Presses", freq = {"create:mechanical_press", "minecraft:lever"}, embrayage = true},
  {nom = "Mixeurs", freq = {"create:mechanical_mixer", "minecraft:lever"}, embrayage = true},
  {nom = "Lavage", freq = {"create:encased_fan", "minecraft:lever"}, embrayage = true},
  {nom = "Perceuses", freq = {"create:mechanical_drill", "minecraft:lever"}, embrayage = true},
  {nom = "Lampes", freq = {"minecraft:redstone_lamp", "minecraft:lever"}},
}
local BL, BH = 23, 4       -- taille d'un bouton, en caractères

Les identifiants d'objets doivent exister : link.set s'arrête avec unknown item 'create:crushing_wheels' sur une faute de frappe. Le programme les vérifie tous au démarrage (étape 3) : une erreur se voit tout de suite.

2. Commuter une machine

link.set émet sur une fréquence depuis l'ordinateur lui-même, sans bloc Liaison de redstone à côté de lui. true vaut une puissance de 15, false vaut 0 :

Brass
local dernier = "rien pour l'instant"   -- la dernière action, pour la ligne d'état

local function appliquer(m)
  local puissance = m.on
  if m.embrayage then puissance = not m.on end
  link.set(m.freq[1], m.freq[2], puissance)
end

local function regler_machine(m, on, source)
  m.on = on
  appliquer(m)
  local mot = "ARRÊT"
  if on then mot = "MARCHE" end
  dernier = m.nom .. " " .. mot .. " (" .. source .. ")"
end

local function tout_arreter(source)
  for _, m in ipairs(MACHINES) do
    m.on = false
    appliquer(m)
  end
  dernier = "tout arrêté (" .. source .. ")"
end

Un ordinateur cesse d'émettre sur toutes ses fréquences quand il redémarre ou s'éteint : les embrayages des chaînes Create ne reçoivent alors plus rien, et leurs machines tournent. C'est pourquoi le programme réapplique les états enregistrés dès qu'il démarre.

3. Enregistrer et relire les états

Le fichier d'état est du texte simple, une machine par ligne : Presses=1. string.split découpe le fichier en lignes, et chaque ligne au = :

Brass
local function enregistrer()
  local lignes = {}
  for _, m in ipairs(MACHINES) do
    local valeur = "0"
    if m.on then valeur = "1" end
    table.insert(lignes, m.nom .. "=" .. valeur)
  end
  fs.write(FICHIER_ETAT, table.concat(lignes, "\n"))
end

local function charger_etats()
  local texte = fs.read(FICHIER_ETAT)
  if texte == nil then return end
  for _, ligne in ipairs(texte:split("\n")) do
    local morceaux = ligne:split("=")
    for _, m in ipairs(MACHINES) do
      if #morceaux == 2 and m.nom == morceaux[1] then m.on = morceaux[2] == "1" end
    end
  end
end

Les machines sont retrouvées par leur nom : vous pouvez réordonner MACHINES ou en ajouter une sans perdre l'état des autres.

Brass
local texte = "Broyeurs=1\nPresses=0\nLampes=1"
for _, ligne in ipairs(texte:split("\n")) do
  local morceaux = ligne:split("=")
  print(morceaux[1], morceaux[2] == "1")
end
Écran
Broyeurs    true
Presses false
Lampes  true

preparer place les boutons sur deux colonnes, charge les états et les applique. Un mauvais identifiant d'objet arrête le programme ici, avec le nom de la machine :

Brass
local function preparer()
  for i, m in ipairs(MACHINES) do
    m.x = 3 + ((i - 1) % 2) * 24
    m.y = 3 + ((i - 1) // 2) * 5
    m.on = false
  end
  charger_etats()
  for _, m in ipairs(MACHINES) do
    local r = pcall(appliquer, m)
    if not r.ok then error("machine '" .. m.nom .. "' : " .. r.error) end
  end
end

4. Dessiner les boutons

Un bouton se dessine en deux couches. En dessous, avec gfx, un rectangle plein, un bord de deux pixels d'épaisseur et un voyant rond. Au-dessus, avec term, le numéro et le nom de la machine, et son état. La couche de pixels est sous le texte : les caractères s'affichent par-dessus le rectangle coloré.

Un caractère fait 6 pixels de large et 9 de haut : le caractère de la colonne c commence au pixel (c - 1) * 6 + 1. Deux petites fonctions changent les positions en caractères en pixels :

Brass
local function px(col) return (col - 1) * 6 + 1 end
local function py(ligne) return (ligne - 1) * 9 + 1 end

local function dessiner_bouton(m, numero)
  local fond, bord, voyant = "gray", "light_gray", "red"
  if m.on then fond, bord, voyant = "green", "lime", "lime" end
  local x, y, l, h = px(m.x), py(m.y), BL * 6, BH * 9
  gfx.rect(x, y, l, h, fond, true)
  gfx.rect(x, y, l, h, bord, 2)
  gfx.circle(x + l - 16, y + h // 2, 7, "black", true)
  gfx.circle(x + l - 16, y + h // 2, 5, voyant, true)
  term.set_cursor(m.x + 2, m.y + 1)
  term.set_fg(term.colors.white)
  term.write(numero .. "  " .. m.nom)
  term.set_cursor(m.x + 5, m.y + 2)
  if m.on then term.write("MARCHE") else term.write("ARRÊT") end
end

local function dessiner()
  term.set_bg(term.colors.black)
  term.clear()
  gfx.clear()
  local actives = 0
  for _, m in ipairs(MACHINES) do
    if m.on then actives = actives + 1 end
  end
  term.set_cursor(1, 1)
  term.set_bg(term.colors.blue)
  term.set_fg(term.colors.white)
  term.write(string.format(" %-36s%13s ", "PANNEAU DE COMMANDE", actives .. "/" .. #MACHINES .. " en marche"))
  term.set_bg(term.colors.black)
  for i, m in ipairs(MACHINES) do dessiner_bouton(m, i) end
  gfx.rect(px(3), py(18), 11 * 6, 9, "red", true)
  term.set_cursor(4, 18)
  term.set_fg(term.colors.white)
  term.write("TOUT ARRÊT")
  term.set_cursor(16, 18)
  term.set_fg(term.colors.light_gray)
  term.write("Fait : " .. dernier)
  term.set_cursor(3, 19)
  term.set_fg(term.colors.gray)
  term.write("Cliquez un bouton, ou tapez 1 à 6 (0 : tout).")
end

5. Clics et touches

Un évènement click donne le caractère sous le pointeur : e.x (colonne) et e.y (ligne), comptés depuis 1 en haut à gauche, que le clic vienne du terminal ou d'un moniteur. e.source dit lequel ("terminal" ou "monitor"), et la ligne d'état l'affiche. Trouver le bouton sous un clic, c'est un test sur son rectangle :

Brass
local function bouton_sous(x, y)
  for i, m in ipairs(MACHINES) do
    if x >= m.x and x < m.x + BL and y >= m.y and y < m.y + BH then return i end
  end
  return nil
end

La boucle principale attend n'importe quel évènement. Les clics appuient sur les boutons ; les chiffres tapés dans le terminal font de même, 0 voulant dire « tout arrêter ». Chaque changement est enregistré aussitôt.

Brass
preparer()
dessiner()
while true do
  local e = os.pull_event()
  if e.name == "click" then
    local i = bouton_sous(e.x, e.y)
    if i then
      regler_machine(MACHINES[i], not MACHINES[i].on, e.source)
      enregistrer()
      dessiner()
    elseif e.y == 18 and e.x >= 3 and e.x <= 13 then
      tout_arreter(e.source)
      enregistrer()
      dessiner()
    end
  elseif e.name == "char" then
    local n = tonumber(e.char)
    if n == 0 then
      tout_arreter("clavier")
      enregistrer()
      dessiner()
    elseif n ~= nil and MACHINES[n] ~= nil then
      regler_machine(MACHINES[n], not MACHINES[n].on, "clavier")
      enregistrer()
      dessiner()
    end
  end
end

Le programme attend avec os.pull_event() sans filtre : c'est ce qui fait d'un clic droit sur le mur de moniteurs un appui sur l'écran (un programme qui attend les évènements "click" fait de même). Voir Évènements pour ce genre de boucle.

Le programme complet

startup
-- Panneau de commande tactile : un bouton par machine sur un mur de
-- moniteurs. Chaque bouton commute une fréquence de Liaison de redstone ; les états
-- survivent à un redémarrage.

local FICHIER_ETAT = "panneau/etat"
local MACHINES = {
  -- embrayage = true : la chaîne passe par un embrayage de Create, qui débraye
  -- quand il est alimenté, donc « en marche » veut dire aucun signal.
  {nom = "Broyeurs", freq = {"create:crushing_wheel", "minecraft:lever"}, embrayage = true},
  {nom = "Presses", freq = {"create:mechanical_press", "minecraft:lever"}, embrayage = true},
  {nom = "Mixeurs", freq = {"create:mechanical_mixer", "minecraft:lever"}, embrayage = true},
  {nom = "Lavage", freq = {"create:encased_fan", "minecraft:lever"}, embrayage = true},
  {nom = "Perceuses", freq = {"create:mechanical_drill", "minecraft:lever"}, embrayage = true},
  {nom = "Lampes", freq = {"minecraft:redstone_lamp", "minecraft:lever"}},
}
local BL, BH = 23, 4       -- taille d'un bouton, en caractères

local dernier = "rien pour l'instant"   -- la dernière action, pour la ligne d'état

-- Machines ------------------------------------------------------------------------------

local function appliquer(m)
  local puissance = m.on
  if m.embrayage then puissance = not m.on end
  link.set(m.freq[1], m.freq[2], puissance)
end

local function regler_machine(m, on, source)
  m.on = on
  appliquer(m)
  local mot = "ARRÊT"
  if on then mot = "MARCHE" end
  dernier = m.nom .. " " .. mot .. " (" .. source .. ")"
end

local function tout_arreter(source)
  for _, m in ipairs(MACHINES) do
    m.on = false
    appliquer(m)
  end
  dernier = "tout arrêté (" .. source .. ")"
end

-- Fichier d'état ------------------------------------------------------------------------

local function enregistrer()
  local lignes = {}
  for _, m in ipairs(MACHINES) do
    local valeur = "0"
    if m.on then valeur = "1" end
    table.insert(lignes, m.nom .. "=" .. valeur)
  end
  fs.write(FICHIER_ETAT, table.concat(lignes, "\n"))
end

local function charger_etats()
  local texte = fs.read(FICHIER_ETAT)
  if texte == nil then return end
  for _, ligne in ipairs(texte:split("\n")) do
    local morceaux = ligne:split("=")
    for _, m in ipairs(MACHINES) do
      if #morceaux == 2 and m.nom == morceaux[1] then m.on = morceaux[2] == "1" end
    end
  end
end

local function preparer()
  for i, m in ipairs(MACHINES) do
    m.x = 3 + ((i - 1) % 2) * 24
    m.y = 3 + ((i - 1) // 2) * 5
    m.on = false
  end
  charger_etats()
  for _, m in ipairs(MACHINES) do
    local r = pcall(appliquer, m)
    if not r.ok then error("machine '" .. m.nom .. "' : " .. r.error) end
  end
end

-- Écran -----------------------------------------------------------------------------------

local function px(col) return (col - 1) * 6 + 1 end
local function py(ligne) return (ligne - 1) * 9 + 1 end

local function dessiner_bouton(m, numero)
  local fond, bord, voyant = "gray", "light_gray", "red"
  if m.on then fond, bord, voyant = "green", "lime", "lime" end
  local x, y, l, h = px(m.x), py(m.y), BL * 6, BH * 9
  gfx.rect(x, y, l, h, fond, true)
  gfx.rect(x, y, l, h, bord, 2)
  gfx.circle(x + l - 16, y + h // 2, 7, "black", true)
  gfx.circle(x + l - 16, y + h // 2, 5, voyant, true)
  term.set_cursor(m.x + 2, m.y + 1)
  term.set_fg(term.colors.white)
  term.write(numero .. "  " .. m.nom)
  term.set_cursor(m.x + 5, m.y + 2)
  if m.on then term.write("MARCHE") else term.write("ARRÊT") end
end

local function dessiner()
  term.set_bg(term.colors.black)
  term.clear()
  gfx.clear()
  local actives = 0
  for _, m in ipairs(MACHINES) do
    if m.on then actives = actives + 1 end
  end
  term.set_cursor(1, 1)
  term.set_bg(term.colors.blue)
  term.set_fg(term.colors.white)
  term.write(string.format(" %-36s%13s ", "PANNEAU DE COMMANDE", actives .. "/" .. #MACHINES .. " en marche"))
  term.set_bg(term.colors.black)
  for i, m in ipairs(MACHINES) do dessiner_bouton(m, i) end
  gfx.rect(px(3), py(18), 11 * 6, 9, "red", true)
  term.set_cursor(4, 18)
  term.set_fg(term.colors.white)
  term.write("TOUT ARRÊT")
  term.set_cursor(16, 18)
  term.set_fg(term.colors.light_gray)
  term.write("Fait : " .. dernier)
  term.set_cursor(3, 19)
  term.set_fg(term.colors.gray)
  term.write("Cliquez un bouton, ou tapez 1 à 6 (0 : tout).")
end

local function bouton_sous(x, y)
  for i, m in ipairs(MACHINES) do
    if x >= m.x and x < m.x + BL and y >= m.y and y < m.y + BH then return i end
  end
  return nil
end

-- Boucle principale -------------------------------------------------------------------------

preparer()
dessiner()
while true do
  local e = os.pull_event()
  if e.name == "click" then
    local i = bouton_sous(e.x, e.y)
    if i then
      regler_machine(MACHINES[i], not MACHINES[i].on, e.source)
      enregistrer()
      dessiner()
    elseif e.y == 18 and e.x >= 3 and e.x <= 13 then
      tout_arreter(e.source)
      enregistrer()
      dessiner()
    end
  elseif e.name == "char" then
    local n = tonumber(e.char)
    if n == 0 then
      tout_arreter("clavier")
      enregistrer()
      dessiner()
    elseif n ~= nil and MACHINES[n] ~= nil then
      regler_machine(MACHINES[n], not MACHINES[n].on, "clavier")
      enregistrer()
      dessiner()
    end
  end
end

À quoi ressemble le panneau

Le code de dessin du programme, avec quatre machines en marche et le dernier appui fait sur le mur de moniteurs :

Brass
local MACHINES = {
  {nom = "Broyeurs", on = true},
  {nom = "Presses", on = true},
  {nom = "Mixeurs", on = false},
  {nom = "Lavage", on = true},
  {nom = "Perceuses", on = false},
  {nom = "Lampes", on = true},
}
local BL, BH = 23, 4
local dernier = "Lavage MARCHE (monitor)"
for i, m in ipairs(MACHINES) do
  m.x = 3 + ((i - 1) % 2) * 24
  m.y = 3 + ((i - 1) // 2) * 5
end

local function px(col) return (col - 1) * 6 + 1 end
local function py(ligne) return (ligne - 1) * 9 + 1 end

local function dessiner_bouton(m, numero)
  local fond, bord, voyant = "gray", "light_gray", "red"
  if m.on then fond, bord, voyant = "green", "lime", "lime" end
  local x, y, l, h = px(m.x), py(m.y), BL * 6, BH * 9
  gfx.rect(x, y, l, h, fond, true)
  gfx.rect(x, y, l, h, bord, 2)
  gfx.circle(x + l - 16, y + h // 2, 7, "black", true)
  gfx.circle(x + l - 16, y + h // 2, 5, voyant, true)
  term.set_cursor(m.x + 2, m.y + 1)
  term.set_fg(term.colors.white)
  term.write(numero .. "  " .. m.nom)
  term.set_cursor(m.x + 5, m.y + 2)
  if m.on then term.write("MARCHE") else term.write("ARRÊT") end
end

local function dessiner()
  term.set_bg(term.colors.black)
  term.clear()
  gfx.clear()
  local actives = 0
  for _, m in ipairs(MACHINES) do
    if m.on then actives = actives + 1 end
  end
  term.set_cursor(1, 1)
  term.set_bg(term.colors.blue)
  term.set_fg(term.colors.white)
  term.write(string.format(" %-36s%13s ", "PANNEAU DE COMMANDE", actives .. "/" .. #MACHINES .. " en marche"))
  term.set_bg(term.colors.black)
  for i, m in ipairs(MACHINES) do dessiner_bouton(m, i) end
  gfx.rect(px(3), py(18), 11 * 6, 9, "red", true)
  term.set_cursor(4, 18)
  term.set_fg(term.colors.white)
  term.write("TOUT ARRÊT")
  term.set_cursor(16, 18)
  term.set_fg(term.colors.light_gray)
  term.write("Fait : " .. dernier)
  term.set_cursor(3, 19)
  term.set_fg(term.colors.gray)
  term.write("Cliquez un bouton, ou tapez 1 à 6 (0 : tout).")
end

dessiner()
Écran
Écran

L'essayer

  1. Posez une Liaison de redstone à côté d'une lampe, réglez-la en réception, et mettez une lampe à redstone et un levier dans ses emplacements de fréquence. Lancez le programme et appuyez sur 6 Lampes : la lampe s'allume ; appuyez de nouveau : elle s'éteint.
  2. Mettez un Embrayage sur l'arbre d'une Presse mécanique, une Liaison de redstone réceptrice à côté avec une presse mécanique et un levier. Appuyez sur 2 Presses : la presse tourne quand le bouton est vert, s'arrête quand il est gris.
  3. Mettez quelques machines en marche, puis redémarrez l'ordinateur (Ctrl+R dans le terminal) : le panneau revient avec les mêmes boutons allumés, et les machines suivent.
  4. Regardez les états enregistrés :
Terminal
> cat panneau/etat
Broyeurs=1
Presses=1
Mixeurs=0
Lavage=1
Perceuses=0
Lampes=1

Variantes

  • Plus de boutons. Sur un Ordinateur moderne (64 x 24), trois colonnes de 20 caractères et quatre rangées donnent douze boutons. Calculez m.x et m.y d'après la taille de l'écran donnée par term.get_size() plutôt qu'avec des nombres fixes.
  • Un retour des machines. Une machine peut répondre sur sa propre fréquence : une Liaison de redstone en émission à côté d'un capteur (un Détecteur de seuil sur un Coffre à objet renforcé, un capteur de lumière du jour...). link.get(a, b) la lit, et un évènement link arrive à chaque changement : coloriez le voyant du bouton avec ce que fait vraiment la machine.
  • Des minuteries. Ajoutez un bouton « Lampes jusqu'à l'aube » qui allume l'éclairage et surveille l'heure ; quand os.day_time() repasse par 0 (6 h du matin), éteignez-le. Voir Éclairage jour et nuit.
  • Un panneau à distance. Envoyez les appuis à un autre ordinateur avec net.send au lieu de link.set, pour des machines hors de portée des Liaisons de redstone : voir Télécommande par le réseau.
  • Une confirmation. Pour les machines dangereuses (les perceuses de la carrière), faites passer le bouton au jaune au premier appui, et ne commutez la machine qu'à un second appui dans les trois secondes, avec os.start_timer(3).