Create: Computing AgesDoc Brass
Piloter le monde

Réseaux

Câbles de données, Modems radio et Routeurs Wi-Fi : comment les ordinateurs se joignent, quand un message arrive, et comment concevoir leur conversation.

Un ordinateur pilote les blocs qui l'entourent. Un réseau en relie plusieurs : l'ordinateur de l'entrepôt compte le fer, celui de l'usine fait tourner les presses, et le tableau de bord de votre base montre les deux et donne des ordres. Le Câble de données fait aussi autre chose : il met à portée d'un ordinateur des blocs éloignés, comme périphériques.

Ce guide explique ce qui relie quoi, jusqu'où, quand un message est remis, et comment concevoir les messages qu'échangent vos programmes. Les fonctions elles-mêmes sont décrites dans net.

Qui peut quoi

ordinateurbibliothèque netCâble de donnéesModem radio et Routeur Wi-Fi
Calculateur à tubesnonnonnon
Ordinateur à transistorsnonnonnon
Mini-ordinateurouiouinon
Micro-ordinateurouiouioui
Microcontrôleurouiouioui
Ordinateur moderneouiouioui

Le Câble de données arrive avec le Mini-ordinateur (âge du circuit intégré), le Modem radio avec le Micro-ordinateur (âge du microprocesseur), le Routeur Wi-Fi avec l'Ordinateur moderne (âge numérique). Un Routeur Wi-Fi fonctionne aussi sur un Micro-ordinateur ou un Microcontrôleur.

Un Mini-ordinateur ignore une radio posée contre lui ou sur son câble. Il peut quand même parler à un Micro-ordinateur du même câble, mais celui-ci ne transmet pas ses messages par radio : les ordinateurs ne font jamais suivre un message d'eux-mêmes, seul un programme le peut (voir les relais).

Le Câble de données

Un Câble de données se raccorde aux autres câbles, aux ordinateurs à partir du Mini-ordinateur, et aux blocs qu'un ordinateur sait piloter. Les simples arbres, engrenages et boîtes de vitesses sont laissés de côté : un câble peut longer une transmission sans l'embarquer. Tout ce qui est raccordé ainsi forme un réseau câblé, qui sert à deux choses.

Les périphériques. Chaque appareil qui touche le réseau devient un périphérique de chaque ordinateur du réseau. Les blocs qui touchent l'ordinateur lui-même sont nommés d'après la face ("left", "top"...), les autres type@x,y,z : leur type et les coordonnées de leur bloc, celles qu'affiche F3. Sur un Mini-ordinateur avec un coffre à objet renforcé, un Feu tricolore et un Contrôleur de vitesse de rotation sur son câble :

appareils
for _, nom in ipairs(peripheral.list()) do
  print(nom)
end
Terminal
> appareils
inventory@-312,64,1048
traffic_light@-305,66,1040
kinetic@-298,64,1052

Vous aurez rarement à taper un tel nom : peripheral.find("traffic_light") prend le premier de ce type. Voir Périphériques et peripheral.

Les messages. Les ordinateurs du réseau (Mini-ordinateur et plus récents) s'échangent des messages, qui arrivent avec via = "cable". Ce ne sont pas des périphériques : c'est net.computers qui les liste.

Couper un côté. Faites un clic droit sur un côté d'un câble avec la Clé de Create : ce bras disparaît, et plus rien ne passe de ce côté, ni câble, ni appareil, ni ordinateur. Un autre coup de clé le rétablit. Ainsi, deux câbles côte à côte restent deux réseaux, et un coffre contre un câble reste en dehors. Quand deux câbles se touchent, les deux sont coupés d'un coup. Accroupi, un clic droit avec la Clé ramasse le câble.

Quelques règles :

  • Les câbles ne se branchent pas au Calculateur à tubes ni à l'Ordinateur à transistors : ils n'ont pas de port réseau.
  • Deux ordinateurs qui se touchent ne sont pas reliés : mettez un câble entre eux.
  • Un réseau compte au plus 2048 blocs de câble depuis un ordinateur.
  • Rien ne charge un chunk pour un câble : la partie d'un réseau qui se trouve dans un chunk non chargé est hors de portée jusqu'à ce que le chunk se recharge.
  • Un ordinateur réexamine les blocs autour de ses câbles au plus une fois par seconde : un coffre que vous venez de poser peut mettre une seconde à apparaître.
  • Sur un véhicule Create Aeronautics, les coordonnées des noms sont celles où Aeronautics range les blocs du véhicule, pas celles où il vole. Utilisez les noms que donne peripheral.list.

Le Modem radio

Un Modem radio donne la radio à un Micro-ordinateur, un Microcontrôleur ou un Ordinateur moderne. Posez-le contre l'ordinateur (sur n'importe quelle face), ou n'importe où sur son réseau câblé : tous les ordinateurs à radio de ce câble s'en servent alors.

Deux ordinateurs peuvent se parler par radio quand une radio de l'un est à 128 blocs au plus d'une radio de l'autre. La distance se mesure entre les radios, pas entre les ordinateurs, en ligne droite : un modem au bout d'un câble, sur un toit, porte plus loin que l'ordinateur resté à la cave. Les messages arrivent avec via = "radio".

Un modem est aussi un périphérique (de type radio_modem) : radio.range() donne sa portée en blocs.

Note

La radio ne fait pas de bonds. Avec trois ordinateurs alignés à 100 blocs d'écart, celui du milieu entend les deux autres, mais ceux des bouts ne s'entendent pas : l'ordinateur du milieu ne fait rien suivre, sauf si un programme s'en charge. Utilisez plutôt des Routeurs Wi-Fi, ou un relais.

Le Routeur Wi-Fi

Un Routeur Wi-Fi se pose comme un modem, contre l'ordinateur ou sur son câble, et porte à 256 blocs.

Sa vraie force, c'est son nom de réseau, le SSID. Tous les routeurs du même SSID, dans la même dimension, forment un seul réseau : la dorsale. Un message qui atteint un routeur de la dorsale ressort de tous les autres, à n'importe quelle distance. Avec un routeur à chaque bout de la carte, les deux bouts se parlent.

Un routeur reçoit son SSID d'un programme, avec radio.set_ssid() (32 caractères au plus). Tapez ceci une fois sur chaque ordinateur qui porte un routeur :

Brass
local routeur = peripheral.find("wifi_router")
routeur.set_ssid("forges")
print("SSID : " .. routeur.get_ssid())

Le SSID est enregistré dans le routeur et survit à un redémarrage du monde, mais pas à la casse du routeur : réglez-le de nouveau après en avoir déplacé un. Un routeur sans SSID n'est qu'une radio qui porte plus loin. Donner un SSID à un Modem radio arrête le programme avec only Wi-Fi routers have an SSID.

Dès qu'un routeur muni d'un SSID entre en jeu, les messages arrivent avec via = "wifi". net.networks montre les dorsales dont fait partie un ordinateur.

Le trajet d'un message

Pour décider si l'ordinateur S (l'expéditeur) joint l'ordinateur D (le destinataire), le mod essaie, dans cet ordre :

  1. Le câble. D est sur le même réseau câblé que S : remis, via = "cable".
  2. Pas de radio. S n'a ni Modem radio ni Routeur Wi-Fi (contre lui ou sur son câble) : D est hors de portée.
  3. La radio directe. Une radio de S et une radio de D sont à portée l'une de l'autre : remis, via = "radio" si ce sont deux modems ou des routeurs sans SSID, "wifi" sinon.
  4. Les dorsales de S. S fait partie de la dorsale de chaque SSID de ses propres routeurs, et de celle de chaque routeur muni d'un SSID qu'une de ses radios atteint.
  5. La sortie d'une dorsale. D est joint par l'une de ces dorsales quand l'un de ses routeurs porte ce SSID, ou quand l'une de ses radios est à portée d'un routeur de ce SSID : remis, via = "wifi".

Un message fait donc au plus trois sauts : un saut radio pour entrer dans la dorsale, la dorsale elle-même, et un saut radio pour en sortir. Ici, un Micro-ordinateur de la mine joint celui du port, à des milliers de blocs, avec de simples Modems radio aux deux bouts et deux routeurs nommés forges sur le chemin :

  Micro-ordinateur n°12, l'expéditeur, à la mine
  [Modem radio]
        |
        |  1. saut radio : 90 blocs (un modem porte à 128)
        v
  [Routeur Wi-Fi "forges"]   sur le Microcontrôleur n°3, près de la mine
        ||
        ||  2. la dorsale : tous les routeurs nommés "forges"
        ||     de cette dimension, à n'importe quelle distance
        ||
  [Routeur Wi-Fi "forges"]   sur le Microcontrôleur n°8, près du port
        |
        |  3. saut radio : 60 blocs (un modem porte à 128)
        v
  [Modem radio]
  Micro-ordinateur n°20, le destinataire, au port

Les routeurs d'une dorsale doivent appartenir à des ordinateurs : un routeur ne compte que s'il touche un Micro-ordinateur, un Microcontrôleur ou un Ordinateur moderne, ou s'il est sur son câble, dans un chunk chargé. Cet ordinateur n'a pas besoin de faire tourner un programme. Un routeur posé seul, ou sur un Mini-ordinateur, ne fait rien.

Portée, dimensions et chunks

  • La plus petite portée compte. Un Modem radio (128) et un Routeur Wi-Fi (256) se parlent jusqu'à 128 blocs.
  • Le serveur peut changer les portées. wirelessRangeMultiplier, dans la section [computers] de world/serverconfig/computingages-server.toml, multiplie la portée de tous les modems et routeurs (de 0,1 à 16, 1 par défaut). radio.range() donne la portée multiplicateur compris.
  • Une seule dimension. La radio et le Wi-Fi restent dans leur dimension : un routeur du Nether n'entend pas la surface.
  • Chunks chargés seulement. L'expéditeur, le destinataire et chaque ordinateur qui porte un routeur utilisé en chemin doivent être dans des chunks chargés et actifs (près d'un joueur, ou maintenus chargés). Rien ne charge un chunk pour un message.
  • Véhicules. Sur les véhicules Create Aeronautics, les distances utilisent la position réelle du véhicule dans le monde : une radio à bord d'un dirigeable se déplace avec lui, et sa portée aussi. Voir vehicle.
  • Les changements prennent une seconde. Un ordinateur regarde ses radios et leurs SSID au plus une fois par seconde : un modem que vous venez de poser, ou un nouveau SSID, compte dans la seconde.

Quand un message est remis

Un message entre dans la file d'évènements du destinataire dès le même tick, et les messages d'un ordinateur à un autre arrivent dans l'ordre où ils ont été envoyés. net.send renvoie true (et net.broadcast compte le destinataire) seulement si le destinataire :

  • est joignable, comme décrit plus haut ;
  • est allumé, dans un chunk chargé et actif ;
  • a de la place dans son tampon de réception : les évènements en attente dans sa file peuvent occuper jusqu'au quart de sa mémoire (4 K cellules sur un Microcontrôleur, 8 K sur un Mini-ordinateur, 32 K sur un Micro-ordinateur, 256 K sur un Ordinateur moderne). Un destinataire qui ne suit pas refuse les nouveaux messages au lieu de planter.

La suite dépend du destinataire :

  • Un programme qui attend dans net.receive ou os.pull_event reçoit le message à son tick suivant.
  • Un programme occupé, ou endormi, trouve le message dans sa file à son prochain net.receive : rien n'est perdu.
  • Un ordinateur sans programme lancé, à l'invite du shell, jette ses messages, même si net.send a renvoyé true. Lancez le programme qui reçoit depuis un fichier startup, pour qu'il démarre dès que l'ordinateur s'allume.
  • Un ordinateur qui ne tourne plus (sans rotation) reste allumé : il accepte les messages jusqu'à ce que son tampon soit plein, et les traite quand la rotation revient.
  • La file contient 256 évènements de toutes sortes : quand elle déborde, le plus ancien est perdu.

Les sorties qu'un message déclenche (rs.set, link.set, kinetic.set_speed()) atteignent le monde à la fin du tick du destinataire, et seule la dernière valeur du tick compte : un déluge de messages ne peut pas inonder le monde de mises à jour de blocs.

Concevoir un protocole

Un protocole, c'est l'accord entre vos programmes : quel canal ils utilisent, ce que contient un message, qui répond à quoi. Cinq minutes de réflexion évitent des heures à se demander pourquoi un tableau de bord reste vide.

Les canaux

Donnez à chaque conversation son canal : "stock", "usine", "alarme", ou un par installation, comme "forges". Un destinataire écarte tout de suite les canaux qui ne le concernent pas, et deux installations voisines ne mélangent pas leurs messages.

Un canal est une étiquette, pas un verrou : n'importe quel ordinateur joignable peut envoyer sur n'importe quel canal. Avant d'obéir à un ordre, vérifiez qui l'a envoyé (m.sender), et éventuellement par où (m.via == "cable" ne peut venir que d'un ordinateur câblé au vôtre).

Des tables avec un type

Envoyez des tables plutôt que des valeurs seules, et donnez à chacune un champ type qui dit ce qu'elle est. Un programme peut alors traiter plusieurs sortes de messages sur un même canal, et vous pourrez ajouter des champs plus tard sans rien casser :

Brass
net.broadcast({type = "stock", objet = "minecraft:iron_ingot", nombre = 1830}, "forges")
net.send(5, {type = "ordre", id = 17, action = "arret"}, "forges")
net.send(8, {type = "recu", id = 17, en_marche = false}, "forges")

Côté réception, une table de traitements, un par type, garde le programme bien rangé. Les messages sont ici écrits à la main pour montrer ce qui se passe :

Brass
local stock = 0
local traitements = {}

function traitements.stock(m)
  stock = m.data.nombre
  print("le stock vaut maintenant " .. stock)
end

function traitements.ping(m)
  print("ping du n°" .. m.sender)   -- un vrai programme répond avec net.send
end

local function aiguiller(m)
  if m.channel ~= "forges" or type(m.data) ~= "table" then return end
  local traitement = traitements[m.data.type]
  if traitement then
    traitement(m)
  else
    print("type inconnu : " .. tostring(m.data.type))
  end
end

aiguiller({sender = 3, channel = "forges", data = {type = "stock", nombre = 1830}})
aiguiller({sender = 8, channel = "forges", data = {type = "ping"}})
aiguiller({sender = 8, channel = "forges", data = {type = "danse"}})
aiguiller({sender = 8, channel = "chat", data = "salut"})
Écran
le stock vaut maintenant 1830
ping du n°8
type inconnu : danse

Dans le vrai programme, la boucle se résume à while true do aiguiller(net.receive()) end. Vérifiez toujours type(m.data) == "table" avant de lire des champs : un autre programme peut envoyer une valeur seule, ou pas de donnée du tout, et lire un champ arrête alors votre programme :

Brass
local m = {sender = 4, channel = "forges"}   -- un message envoyé sans donnée
local d = m.data
print(d.type)
Écran
snippet:3: attempt to index a nil value (local 'd')

Questions, réponses et identifiants

Quand un ordinateur pose une question, la réponse doit retrouver sa question. Mettez un id dans chaque requête (un compteur suffit) et faites-le recopier par l'autre côté dans sa réponse. Le demandeur ignore alors toute réponse dont l'id n'est pas celui qu'il attend, y compris les réponses tardives à une question précédente.

N'attendez jamais une réponse sans délai : net.receive(2) renvoie nil au bout de 2 secondes, net.receive() pourrait attendre pour toujours si l'autre ordinateur a disparu. net montre une fonction demander complète, avec nouvelles tentatives.

Accusés de réception et nouvelles tentatives

Quand net.send renvoie true, cela veut seulement dire que le message est dans la file de l'autre ordinateur. Pour un ordre important, faites-le confirmer par le destinataire avec un accusé de réception, un message "recu" qui porte le même id. L'expéditeur renvoie l'ordre quand aucun accusé n'arrive à temps, quelques fois, puis abandonne et le signale.

Une nouvelle tentative peut atteindre le destinataire deux fois : quand l'ordre est arrivé mais que l'accusé s'est perdu. Préférez donc des ordres qu'on peut répéter sans risque : « démarre », « arrête », « règle la vitesse à 64 », « ouvre la porte », plutôt que « bascule » ou « ajoute 16 tr/min ». Une bascule reçue deux fois ne fait rien. Quand un ordre ne peut pas être répété sans risque, le destinataire retient le dernier id exécuté pour chaque expéditeur et se contente d'accuser réception d'une répétition, et l'expéditeur garde son compteur dans un fichier (fs.write) pour ne pas repartir de 1 après un redémarrage.

Une seule boucle d'évènements

net.receive jette tous les autres évènements pendant qu'il attend : un programme coincé dans une boucle qui attend une réponse rate les clics, les touches et les minuteurs, et aussi les autres messages. Un ordinateur qui doit réagir à plusieurs choses utilise une seule boucle autour de os.pull_event(), et garde ce qu'il attend dans des variables : « un ordre est en cours, son échéance tombe à 42 secondes ». Un minuteur surveille les échéances. Le tableau de bord ci-dessous fonctionne ainsi.

Diffuser sans boucler

Répondez à une diffusion avec net.send(m.sender, ...), jamais par une autre diffusion sur le même canal.

Un relais est un programme qui répète les messages, pour relier des radios trop éloignées. Deux relais qui s'entendent se renverraient chaque message sans fin : chaque message porte donc son origine et un numéro de séquence, le relais retient ceux qu'il a déjà répétés, et un compteur de sauts arrête ce qui tournerait encore en rond :

Brass
-- relais : répète une fois chaque message du canal "courrier"
local vus = {}
local nb_vus = 0
while true do
  local m = net.receive()
  local d = m.data
  if m.channel == "courrier" and type(d) == "table" and d.origine and d.seq then
    local cle = d.origine .. ":" .. d.seq
    local sauts = d.sauts or 0
    if not vus[cle] and sauts < 8 then
      vus[cle] = true
      nb_vus = nb_vus + 1
      if nb_vus > 1000 then   -- oublier les vieux messages, pour ne pas remplir la mémoire
        vus = {}
        nb_vus = 0
      end
      d.sauts = sauts + 1
      net.broadcast(d, "courrier")
    end
  end
end

L'expéditeur donne à chaque message origine = net.id() et un nouveau seq, le destinataire ignore un origine:seq déjà vu (il peut recevoir le même message par plusieurs relais). Avec une dorsale Wi-Fi, les relais sont rarement utiles.

Un exemple complet : entrepôt, usine, tableau de bord

Trois ordinateurs font tourner ensemble une fonderie de fer :

  • L'entrepôt (Microcontrôleur n°3) : un Coffre à objet renforcé de Create contre son dos, un Modem radio dessus. Toutes les 10 secondes, il annonce à tout le monde combien de lingots de fer contient le coffre renforcé.
  • L'usine (Micro-ordinateur n°5) : un Contrôleur de vitesse de rotation posé dessus entraîne la ligne de Presses mécaniques, un Routeur Wi-Fi est sur son câble. Elle fait tourner la ligne plus vite quand l'entrepôt est plein, et obéit aux ordres de démarrage et d'arrêt du tableau de bord.
  • Le tableau de bord (Ordinateur moderne n°8) : à la base, à 600 blocs, avec un Moniteur LCD qui montre son écran et un Routeur Wi-Fi. Il montre la fonderie, et un clic sur son bouton arrête ou démarre l'usine.

Le modem de l'entrepôt est à 90 blocs du routeur de l'usine : moins de 128. Les deux routeurs s'appellent forges (avec la ligne set_ssid vue plus haut, tapée une fois sur l'usine et sur le tableau de bord). L'entrepôt rejoint donc la dorsale, et chaque ordinateur joint tous les autres.

Le protocole, entièrement sur le canal "forges" :

typeenvoyé paràchampsquand
stockl'entrepôttout le mondeobjet, nombretoutes les 10 secondes
etatl'usinetout le mondeen_marche, tr_min, stocktoutes les 5 secondes, et après chaque ordre
ordrele tableau de bordl'usineid, action ("marche" ou "arret")à chaque clic
recul'usinele tableau de bordid, en_marcheen réponse à chaque ordre

Chaque programme est enregistré sous le nom startup sur son ordinateur, pour repartir après un redémarrage ou quand le chunk se recharge.

L'entrepôt se contente de diffuser :

startup
-- entrepôt (Microcontrôleur n°3) : compte le fer du coffre renforcé et l'annonce toutes les 10 secondes
local coffre = peripheral.wrap("back")
local OBJET = "minecraft:iron_ingot"
while true do
  net.broadcast({type = "stock", objet = OBJET, nombre = coffre.count(OBJET)}, "forges")
  sleep(10)
end

L'usine réagit à deux sortes de messages et à un minuteur : elle utilise donc os.pull_event. Ses ordres sont absolus (« marche », « arrêt ») : un ordre reçu deux fois ne fait pas de mal, elle n'a donc pas besoin de retenir les identifiants, elle recopie seulement l'id dans son accusé. Elle n'accepte d'ordres que du tableau de bord :

startup
-- usine (Micro-ordinateur n°5) : vitesse de la ligne de presses selon le stock,
-- marche et arrêt sur ordre du tableau de bord, état toutes les 5 secondes
local TABLEAU = 8
local moteur = peripheral.wrap("top")   -- Contrôleur de vitesse de rotation
local en_marche = true
local stock = nil                        -- fer de l'entrepôt, nil jusqu'au premier rapport
local tr_min = 0

local function appliquer()
  if not en_marche then
    tr_min = 0
  elseif stock == nil or stock < 256 then
    tr_min = 32                          -- il reste peu de fer : on ralentit
  elseif stock < 2048 then
    tr_min = 96
  else
    tr_min = 192                         -- il y en a plein : pleine vitesse
  end
  moteur.set_speed(tr_min)
end

local function rapport()
  net.broadcast({type = "etat", en_marche = en_marche, tr_min = tr_min, stock = stock}, "forges")
end

appliquer()
rapport()
local minuteur = os.start_timer(5)
while true do
  local e = os.pull_event()
  if e.name == "message" and e.channel == "forges" and type(e.data) == "table" then
    local d = e.data
    if d.type == "stock" then
      stock = d.nombre
      appliquer()
    elseif d.type == "ordre" and e.sender == TABLEAU then
      if d.action == "marche" then en_marche = true end
      if d.action == "arret" then en_marche = false end
      appliquer()
      net.send(e.sender, {type = "recu", id = d.id, en_marche = en_marche}, "forges")
      rapport()
    end
  elseif e.name == "timer" and e.id == minuteur then
    rapport()
    minuteur = os.start_timer(5)
  end
end

Le tableau de bord attend les messages, les clics et un minuteur dans une seule boucle. Un clic envoie un ordre ; le minuteur, chaque seconde, le renvoie si aucun accusé n'est arrivé en 2 secondes, trois fois au plus ; l'accusé l'efface. Un rapport de l'usine vieux de plus de 15 secondes s'affiche en rouge : l'usine est éteinte, ou son chunk n'est pas chargé.

startup
-- tableau de bord (Ordinateur moderne n°8) : montre la fonderie sur son Moniteur LCD,
-- un clic sur la ligne du bouton démarre ou arrête l'usine
local USINE = 5
local LIGNE_BOUTON = 11
local stock = nil       -- dernier rapport de l'entrepôt
local etat = nil        -- dernier rapport de l'usine, avec son heure d'arrivée
local en_cours = nil    -- l'ordre qui attend son accusé
local prochain_id = 1
local avis = ""

local function ecrire(ligne, couleur, texte)
  term.set_cursor(2, ligne)
  term.set_fg(couleur)
  term.write(texte)
end

local function jauge(ligne, valeur, max)   -- une jauge de 40 cases
  local cases = math.max(0, math.min(40, math.floor(valeur / max * 40)))
  term.set_cursor(13, ligne)
  term.set_bg(term.colors.lime)
  term.write(string.rep(" ", cases))
  term.set_bg(term.colors.gray)
  term.write(string.rep(" ", 40 - cases))
  term.set_bg(term.colors.black)
end

local function dessiner()
  term.set_bg(term.colors.black)
  term.clear()
  ecrire(2, term.colors.yellow, "FORGES")
  if stock then
    ecrire(4, term.colors.white, "Entrepôt   " .. stock .. " lingots de fer")
    jauge(5, stock, 4096)
  else
    ecrire(4, term.colors.light_gray, "Entrepôt   pas encore de rapport")
  end
  if etat == nil then
    ecrire(7, term.colors.light_gray, "Usine      pas encore de rapport")
  elseif os.clock() - etat.quand > 15 then
    ecrire(7, term.colors.red, "Usine      muette depuis " .. math.floor(os.clock() - etat.quand) .. " s")
  elseif etat.en_marche then
    ecrire(7, term.colors.lime, "Usine      en marche à " .. etat.tr_min .. " tr/min")
    jauge(8, etat.tr_min, 256)
  else
    ecrire(7, term.colors.orange, "Usine      à l'arrêt")
  end
  if en_cours then
    term.set_bg(term.colors.gray)
    ecrire(LIGNE_BOUTON, term.colors.white, " envoi de l'ordre " .. en_cours.action .. "... ")
  elseif etat and etat.en_marche then
    term.set_bg(term.colors.red)
    ecrire(LIGNE_BOUTON, term.colors.white, " ARRÊTER L'USINE ")
  else
    term.set_bg(term.colors.green)
    ecrire(LIGNE_BOUTON, term.colors.white, " DÉMARRER L'USINE ")
  end
  term.set_bg(term.colors.black)
  ecrire(LIGNE_BOUTON + 2, term.colors.light_gray, avis)
end

local function transmettre()
  en_cours.essais = en_cours.essais + 1
  en_cours.echeance = os.clock() + 2
  net.send(USINE, {type = "ordre", id = en_cours.id, action = en_cours.action}, "forges")
end

local function ordonner(action)
  en_cours = {id = prochain_id, action = action, essais = 0, echeance = 0}
  prochain_id = prochain_id + 1
  avis = ""
  transmettre()
end

dessiner()
local minuteur = os.start_timer(1)
while true do
  local e = os.pull_event()
  if e.name == "message" and e.channel == "forges" and type(e.data) == "table" then
    local d = e.data
    if d.type == "stock" then
      stock = d.nombre
    elseif d.type == "etat" and e.sender == USINE then
      etat = d
      etat.quand = os.clock()
    elseif d.type == "recu" and e.sender == USINE and en_cours and d.id == en_cours.id then
      if d.en_marche then avis = "l'usine confirme : en marche" else avis = "l'usine confirme : à l'arrêt" end
      en_cours = nil
    end
    dessiner()
  elseif e.name == "click" and e.y == LIGNE_BOUTON and en_cours == nil then
    if etat and etat.en_marche then ordonner("arret") else ordonner("marche") end
    dessiner()
  elseif e.name == "timer" and e.id == minuteur then
    if en_cours and os.clock() >= en_cours.echeance then
      if en_cours.essais < 3 then
        transmettre()
      else
        avis = "pas de réponse de l'usine après 3 essais"
        en_cours = nil
      end
    end
    dessiner()
    minuteur = os.start_timer(1)
  end
end

Comme le programme attend avec os.pull_event() sans filtre, un clic droit sur le Moniteur LCD appuie sur l'écran (un clic n'importe où sur la ligne du bouton compte). Voici l'écran avec l'usine en marche, dessiné par la même fonction dessiner avec des valeurs d'exemple :

Brass
local LIGNE_BOUTON = 11
local stock = 1830
local etat = {en_marche = true, tr_min = 96, quand = os.clock()}
local en_cours = nil
local avis = "l'usine confirme : en marche"

local function ecrire(ligne, couleur, texte)
  term.set_cursor(2, ligne)
  term.set_fg(couleur)
  term.write(texte)
end

local function jauge(ligne, valeur, max)   -- une jauge de 40 cases
  local cases = math.max(0, math.min(40, math.floor(valeur / max * 40)))
  term.set_cursor(13, ligne)
  term.set_bg(term.colors.lime)
  term.write(string.rep(" ", cases))
  term.set_bg(term.colors.gray)
  term.write(string.rep(" ", 40 - cases))
  term.set_bg(term.colors.black)
end

local function dessiner()
  term.set_bg(term.colors.black)
  term.clear()
  ecrire(2, term.colors.yellow, "FORGES")
  if stock then
    ecrire(4, term.colors.white, "Entrepôt   " .. stock .. " lingots de fer")
    jauge(5, stock, 4096)
  else
    ecrire(4, term.colors.light_gray, "Entrepôt   pas encore de rapport")
  end
  if etat == nil then
    ecrire(7, term.colors.light_gray, "Usine      pas encore de rapport")
  elseif os.clock() - etat.quand > 15 then
    ecrire(7, term.colors.red, "Usine      muette depuis " .. math.floor(os.clock() - etat.quand) .. " s")
  elseif etat.en_marche then
    ecrire(7, term.colors.lime, "Usine      en marche à " .. etat.tr_min .. " tr/min")
    jauge(8, etat.tr_min, 256)
  else
    ecrire(7, term.colors.orange, "Usine      à l'arrêt")
  end
  if en_cours then
    term.set_bg(term.colors.gray)
    ecrire(LIGNE_BOUTON, term.colors.white, " envoi de l'ordre " .. en_cours.action .. "... ")
  elseif etat and etat.en_marche then
    term.set_bg(term.colors.red)
    ecrire(LIGNE_BOUTON, term.colors.white, " ARRÊTER L'USINE ")
  else
    term.set_bg(term.colors.green)
    ecrire(LIGNE_BOUTON, term.colors.white, " DÉMARRER L'USINE ")
  end
  term.set_bg(term.colors.black)
  ecrire(LIGNE_BOUTON + 2, term.colors.light_gray, avis)
end

dessiner()
Écran
Écran

Ce que chaque partie enseigne :

  • L'entrepôt ne connaît personne : il diffuse, et qui est intéressé écoute. Ajouter un second tableau de bord ne change rien aux autres ordinateurs.
  • L'usine vérifie l'expéditeur de chaque ordre, répond à chacun par un accusé, et continue sur le dernier stock connu si l'entrepôt se tait.
  • Le tableau de bord ne bloque jamais : une seule boucle, un ordre en cours avec une échéance, un minuteur pour les nouvelles tentatives et pour repérer une usine muette.

Pour aller plus loin : affichez aussi l'âge du rapport de l'entrepôt, notez chaque ordre dans un journal avec fs.append, ou faites refuser le démarrage à l'usine tant que la ligne est en surcharge (kinetic.overstressed()). Le livre de recettes construit des projets voisins pas à pas : Tableau de bord d'usine et Télécommande par le réseau.